IdleHandler
执行的,所以退出界面后 onStop 不会立即执行,而是等到主线程中当前没有消息要执行的时候才会执行,具体可见我的另一篇文章分析:Activity 销毁的延迟。但必现 10s 左右才被执行肯定是异常的,可能有消息导致主线程一直没有 处理 IdleHandler,为验证这一点,可以在 onPause 的时候添加一个 IdleHandler 到主线程消息队列中1 |
|
结果果然是在 onStop 之后才会被执行,所以可以确认是由于主线程一直有没有执行 IdleHandler 导致的,初步怀疑是有地方一直在往主线程 Looper 中 添加消息,接下来就需要定位是什么消息导致主线程一直处于非空闲状态。
我们可以通过 Looper.setMessageLogging()
来打印出主线程中执行的消息:
1 |
|
发现有个动画组件的消息不停地被执行,移除这个动画组件后,问题依然存在,并且未发现其他比较明显的异常消息。
既然主线程中一直非空闲状态,那么我们就把主线程消息队列中的所有消息打印出来看看?
通过反射,可在每一帧的时候打印出主线程消息队列中的所有消息:
1 | private Field messagesField; |
得到如下日志:
可以看到消息队列的头部始终是一个 SyncBarrier 消息,有这个消息存在的时候,MessageQueue 只会取下一个 异步消息,让系统的 UI 事件消息得到优先处理。由此猜测,有地方不停地向 MessageQueue 中添加 SyncBarrier,通过对 MessageQueue#postSyncBarrier()
方法打调试断点后,终于发现了罪魁祸首。
查看代码后发现,虽然这位童鞋在收到 onGlobalLayout() 回调时的确调用了View.getViewTreeObserver().removeOnGlobalLayoutListener()
,但在上面截图中的 fixBtnLayout
方法中又把监听器重新添加上了,所以实际并没有成功移除,相当于这里出现一个“异步的死循环“。
在 OnGlobalLayoutListener 中更新了布局的 LayoutParams 导致触发了 requestLayout(),而 requestLayout 会调用 MessageQueue#postSyncBarrier()
,至于为什么主线程中有 SyncBarrier 消息时,IdleHandler 没有被执行的原因可以看 MessageQueue#next()
的源码:
1 | // If first time idle, then get the number of idlers to run. |
由于这个时候是有消息的,所以 mMessages != null,由上面的日志可以看到 消息队列头部消息始终是 when 为负值,导致 now < mMessages.when
也不成立,所以 pendingIdleHandlerCount 会一直为0,直到调用了 MessageQueue#removeSyncBarrier()
来移除 SyncBarrier 消息。
那么为什么 Activity 还是能回调 onStop()
呢?过滤掉 ActivityManager 的消息后可以看到一条这样的日志:
1 | W/ActivityManager: Launch timeout has expired, giving up wake lock! |
这是 ActivityManagerService 的超时机制,而这个时间正好是10s,具体可见 ActivityStack 中 STOP_TIMEOUT
。超时后会把 Activity 强制置为 stop 状态,这时候不会再触发 onGlobalLayout,从而不会再有 SyncBarrier 消息,所以最终 IdleHandler 得到执行的机会。
再看另一个快速重新打开 LifeActivity
的 Case:
这里我采用代码模拟快速重新打开
LifeActivity
,finish() 后延迟 300ms 再启动LifeActivity
。因为我们的 Activity 里没做什么事,所以很难手动重现快速重新打开 Activity 的异常 Case,而实际项目因为逻辑复杂,往往在1~2s或者更长的时间里很容易复现这种情况。
出现了诡异的事:LifeActivity[33732136] 是旧的 Activity,但它却在新的 LifeActivity[212157058] 显示之后才被销毁的,看到这个可能你已经心头一凉。这会导致什么问题呢?这会让我们依赖 Activity 生命周期回调来做资源回收的代码变得不可靠。
举个栗子:如果我们在 onStart() 中启动相机在 onStop() 中关闭相机,正常重新打开这个页面时相机的状态操作:打开 -> 关闭 -> 打开,快速重新打开这个 Activity 时就可能不是这个顺序了:打开->打开->关闭。然后用户就遭殃了,他将无法正常使用你的应用了,而用户只是因为手速过快。
那么为什么 onPause() 是马上被调用,而 onStop() 和 onDestroy() 却被延迟这么久呢?
关于 Activity 销毁流程的源码我不会做详细分析,具体可以查看相关源码或者搜索其他人的文章看下
调用 finish 方法后会经过以下流程向 ActivityManagerService
请求销毁当前 Activity:
1 | MyActivity.finish() |
然后 ActivityManagerService
就会请求执行当前 Activity 的 onPause() 方法:
1 | IApplicationThread.schedulePauseActivity() |
所以 onPause() 是立即被执行的,执行完 onPause() 后并没有马上销毁 Activity,而是先让一个 Activity 显示出来,这个 Activity 可能是当前应用 Activity 栈中的一个 Activity 也可能是 Launcher 或者其他应用的 Activity,不管是哪个都大同小异,在上面的 Case 中就是 MainActivity。
执行上一个 Activity 即 MainActivity 的 onResume()
1 | ActivityStack.resumeTopActivityLocked() |
执行完上一个 Activity 的 onResume 之后,该进行 Activity 的销毁操作了吧?
通过反向分析,发现 Activity 的销毁时通过请求 ActivityManagerService 的 activityIdle() 方法,销毁流程如下:
1 | Looper.myQueue().addIdleHandler(new Idler()) |
这个 Idler 实现的是 MessageQueue.IdleHandler
,IdleHandler 会等到 MessageQueue 中当前没有可执行的消息时才会执行,也就是说 Activity 会一直等待主线程消息队列中当前消息都处理完毕了才会进行销毁,这也就是 Activity 的销毁不是立即执行的根本原因。
手速快并不是用户的锅,要避免这种情况,可以用个静态变量保存当前 Activity,并且在销毁的时候判断下是不是与保存的一致,以下给出示例代码,如果你有更好的方案,欢迎告诉我 :)
1 | public class LifeActivity extends AppCompatActivity { |
分析内存发现是 ArrayList 对它进行了持有,但的确调用了 remove 来移除这个监听器呀。打断点发现注册监听器的方法被调用了两次,即调用了两次 ArrayList 的 add 方法。
1 | public boolean add(E e) { |
ArrayList 的 add 方法并没有进行去重操作,所以两次 add 都会成功。但 remove 方法却只调用了一次,来看看 remove 的源码:
1 | public boolean remove(Object o) { |
可以看出,remove 方法会遍历数组中的元素,一旦找到这个监听器,就会 return,即一次 remove 只会移除一个引用。但我们调用了两次 add 方法,所以 ArrayList 依然持有 这个监听器的引用。
其实使用 ArrayList 来作为存放监听器的集合是很常见的,比如在 RecyclerView
中 mItemDecorations
、mOnItemTouchListeners
、mOnChildAttachStateListeners
、
mPendingAccessibilityImportanceChange
、mScrollListeners
等属性都是使用 ArrayList 来保存的,并且没有做去重处理。如果这个 ArrayList 放在单例中并且只 remove 了一次,重复的添加就会导致内存泄露;而在使用时的遍历又会因重复调用导致性能或其他问题。
所以我们平时在遇到往 ArrayList 中添加对象时一定要注意这点,当然我个人更建议使用 HashSet 来保存,这样可以更好地避免团队里其他成员在使用时出现问题。
]]>在 GitHub 查看
Android 开发中,会有很多情况下用到观察者模式,如果你自定义了一个 Listener,在需要调用他们来通知观察者时,或许会遇到以下几点头疼的问题:
这个 Listener 是 Nullable 的,那么每次调用前都需要作判空处理
1 | if (listener != null) { |
事件是在其他线程中发出,而观察者需要在另一个线程中(通常是 UI 线程)处理,所以需要在每次调用的时候做 Handler#post
操作
1 | if(Looper.myLooper() != Looper.getMainLooper()) { |
如果观察者可以注册多个,那每次在调用的时候的时候都需要遍历一下所有 Listener
1 | for(final FooListener listener : listener) { |
这三种情况可能会同时出现,比如在做与 Service 通信时是很常见的,如果回调方法更多时,代码写起来那是相当痛苦😂
基于上面的原因,于是我撸一个轮子,就叫「Wrapper」。它利用 AnnotationProcessor 在构建过程中自动生成指定 Listener 的 Wrapper 类,在需要时只需简简单单的调用一下方法即可:
1 | listener.foo(); |
Wrapper 会根据需要生成判空处理、post、遍历等代码,从编写繁琐无趣的代码中解放出来。
1 |
|
经过「Wrapper」的处理,会生成如下代码:
1 | package com.linroid.wrapper; |
在你的 build.gradle
:
1
2
3
4dependencies {
annotationProcessor 'com.linroid.wrapper:compiler:0.1.0'
compile 'com.linroid.wrapper:library:0.1.0'
}
@WrapperClass
对单个接口 / 类进行处理,默认只会进行判空处理
1 |
|
@UiThread
需要进行 Handler#post
处理,可以用在方法或者类 / 接口上,如果用在类 / 接口,会对所有方法进行处理
1 |
|
@WrapperMultiple
支持多个 Listener
1 |
|
@WrapperGenerator
与@WrapperClass
不同,你可以创建一个空的 Class,将所有需要处理的接口 / 类添加进来(这样就可以处理你无法修改的一些 Listener 了,比如 Android SDK 中的)。
1 | ( |
经过 Wrapper 的处理,会生成一个包名相同的 XXXWrapper
的类,如果添加了 @ WrapperMultiple
注解,会额外生成一个 XXXMultiWrapper
类。
需要注意的是,如果处理的是一个接口,那么生成的 Wrapper 会实现这个接口;而如果处理的是一个类,那么生成的 Wrapper 不会继承这个类。
添加完注解在执行一次 build 后,Wrapper 就会生成好相应的 XXXWrapper
类,使用它们非常简单:
1 | SomeListenerWrapper wrapper = new SomeListenerWrapper(listener); |
如果你有什么好的建议可以在评论留言,也可以提 PR :)
(嗯,我知道 kotlin 大法🐒
首先我毕业了,学校的很多小伙伴们都很难再相见,很想念他们:)
在学校的最后一段时间里认识我的她,我们一起毕业,一起经历由学生变成打工仔的过程。很高兴遇到你,我们的路还很长:)
辞了两次工作,一次是因为两千多公里的距离,一次是为了让自己过得更好
这一年中在技术上我开始更注重基础理论知识和架构设计,对过去追求的各种华丽的库兴趣逐渐下降。GitHub 上面的动态少了许多:(
工作上,开始注重与同事的相处,能够和产品撕逼
去过海边,坐过游轮,到过草原,玩过过山车,这次回家还能坐下飞机;经历过在北京的冬天里吃西瓜,广州的冬天里穿短袖=。=
Instagram
尝试自己做饭吃,虽然做得不好,但能让自己吃饱:)
2017 年,希望自己能赚更多的钱:)
]]>Handler
容易引起内存泄露,这是大家都知道的,所以你应该会在适当的时候调用 removeCallbacks()
方法来移除消息。但当以下使用场景时,依然可能会出现内存泄露。1 | // ... |
mTimerRunnable
是一个在非主线程中运行的循环操作,一旦启动了它,就会每隔 DELAY_INTERVAL
时间被执行一次,所以在 Activity 退出时,应该停止它,否则就会出现内存泄露。停止它的方式就是调用 removeCallbacks()
把它从mDaemonHandler
的消息队列中移除。但这一操作如果处在 mDaemonHandler
线程正在执行 longTimeOperation()
时,mTimerRunnable
之后还是会被添加到 mDaemonHandler
线程的消息队列中。
要保证消息能够移除掉,可以这样写:1
2
3
4
5
6 mDaemonHandler.post(new Runnable() {
public void run() {
mDaemonHandler.removeCallbacks(mTimerRunnable);
}
});
这样就保证了移除 mTimerRunnable
的操作和 mDaemonHandler
在同一线程中,不会和 mTimerRunnable
『并发执行』。写成更通用的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38import android.os.Handler;
import android.os.Looper;
/**
* @author linroid <linroid@gmail.com>
* @since 25/11/2016
*/
public class SafetyUtils {
public static void removeCallbacks(final Handler handler, final Runnable callback) {
if (handler.getLooper() == Looper.myLooper()) {
handler.removeCallbacks(callback);
} else {
handler.post(new Runnable() {
public void run() {
handler.removeCallbacks(callback);
}
});
}
}
public static void removeMessages(final Handler handler, final int what) {
removeMessages(handler, what, null);
}
public static void removeMessages(final Handler handler, final int what, final Object obj) {
if (handler.getLooper() == Looper.myLooper()) {
handler.removeMessages(what, obj);
} else {
handler.post(new Runnable() {
public void run() {
handler.removeMessages(what, obj);
}
});
}
}
}
1 | public boolean isOrientationPortrait() { |
通过调试发现应用切后台之后, isOrientationPortrait()
会返回 true,于是换上另一种横竖屏判断:
1
2
3public boolean isOrientationPortrait() {
return getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
}
这两种方式的区别在于 Configuration.orientation
拿到的是设备的方向,而 getRequestedOrientation()
拿到的是 Activity 请求的方向(通过AndroidManifes.xml
中设置或者通过 setRequestOrientation()
改变)。
修改之后,应用处于后台时方向能判断正常,但一些后台时 inflate
的 View 却不正常或是抛出异常。于是写了一个 Demo
测试一下:Gist。1
2
3
4
5
6
7
8
9
10
11V/MainActivity: ⇢ onPause()
D/MainActivity: getRequestedOrientation: landscape
D/MainActivity: Configuration.orientation: landscape
D/MainActivity: Layout View: landscape
V/MainActivity: ⇠ onPause [2ms]
V/MainActivity: ⇢ onStop()
D/MainActivity: getRequestedOrientation: landscape
D/MainActivity: Configuration.orientation: portrait
D/MainActivity: Layout View: portrait
V/MainActivity: ⇠ onStop [2ms]
从中可以发现,从应用横屏状态切到后台 Configuration.orientation
会在 onStop()
发生改变,而 LayoutInflater#inflate()
加载的 View 取决于Configuration.orientation
,与 getRequestedOrientation()
无关。
由 LayoutInflater#inflate()
可以找到布局文件路径是在 Resources#loadXmlResourceParser()
中拿到的:
1
2
3
4
5
6
7
8
9
10
11XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
// ...
} finally {
releaseTempTypedValue(value);
}
}
ResourcesImpl#getValue()
:
1
2
3
4
5
6
7
8void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
throws NotFoundException {
boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
if (found) {
return;
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
}
AssetsManager#getResourceValue()
会调用 AssetsManager#loadResourceValue()
,这是一个 Native 方法,那么 Native 层是怎样获取方向的呢?ResourcesImpl#updateConfiguration()
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public void updateConfiguration(Configuration config, DisplayMetrics metrics,
CompatibilityInfo compat) {
// ...
mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc,
adjustLanguageTag(mConfiguration.getLocales().get(0).toLanguageTag()),
mConfiguration.orientation,
mConfiguration.touchscreen,
mConfiguration.densityDpi, mConfiguration.keyboard,
keyboardHidden, mConfiguration.navigation, width, height,
mConfiguration.smallestScreenWidthDp,
mConfiguration.screenWidthDp, mConfiguration.screenHeightDp,
mConfiguration.screenLayout, mConfiguration.uiMode,
Build.VERSION.RESOURCES_SDK_INT);
// ...
}
AssetsManager#setConfiguration()
是 Native 层的方法,由此可以得出结论,Native 找到布局文件路径是通过 Configuration.orientation 来判断方向的,所以应用后台时会加载竖屏的资源。
横竖屏的判断:
可以通过给 View 设置 Tag 或者判断某个 View 是否存在,来判断加载的是哪个状态的布局文件:
1 | public boolean isOrientationPortrait() { |
后台时资源的加载:
如果你的应用处于横屏状态, 尽量不要在应用后台时加载与屏幕方向有关的资源,如果非要加载可以采取以下方法:
1 | /** |
我也有一些其他强迫症,比如代码中的空格(注释也不放过),不同编程语言会遵行不同的代码风格。一部分强迫症被强行扳正,比如应用里的小红点(当然,偶尔还是会清空一下=。=);一部分强迫症是受其他人影响,比如中英文要空格是在知乎里养成的。
有的强迫症就跟信仰一样,不容冒犯,时刻约束着自己写出更『优雅』的代码。有的强迫症就是自己作死,心理会暗示自己去克服。
愿所有强迫症患者安好:)
]]> Support library 的 SwitchCompat.java
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void setChecked(boolean checked) {
super.setChecked(checked);
// Calling the super method may result in setChecked() getting called
// recursively with a different value, so load the REAL value...
checked = isChecked();
if (getWindowToken() != null && ViewCompat.isLaidOut(this) && isShown()) {
animateThumbToCheckedState(checked);
} else {
// Immediately move the thumb to the new position.
cancelPositionAnimator();
setThumbPosition(checked ? 1 : 0);
}
}
Framework 的 Switch.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void setChecked(boolean checked) {
super.setChecked(checked);
// Calling the super method may result in setChecked() getting called
// recursively with a different value, so load the REAL value...
checked = isChecked();
if (isAttachedToWindow() && isLaidOut()) {
animateThumbToCheckedState(checked);
} else {
// Immediately move the thumb to the new position.
cancelPositionAnimator();
setThumbPosition(checked ? 1 : 0);
}
}
可以看到 SwitchCompat 多了一个 isShown()
的判定条件
1 | /** |
isShown()
会向上递归,如果 parent 为 null 就返回 false。
而 Preference 中,如果点击了,就会调用 notifyDataSetChanged()
刷新整个 RecyclerView,SwitchCompat 的 setChecked()
是在 onBindViewHolder
时调用的,这个时候还没有添加到 parent 中,所以 isShown()
就会 return false,从而动画不执行。
因为 Preference 的特殊性,所有状态改变都通过 notifyDataSetChanged()
来生效,所以这里通过以下 hack 的方式来解决,其他地方使用到 SwitchCompat
立即 setChecked() 就不会出现这个问题
创建 SwitchCompatFixed
继承 SwitchCompat 重写isShown()
方法
1
2
3
4
5
6
7
8
9
10
11
public boolean isShown() {
ViewParent parent = getParent();
if (parent != null && parent instanceof ViewGroup) {
ViewGroup widgetFrame = (ViewGroup) parent;
if (widgetFrame.getId() == android.R.id.widget_frame) {
return true;
}
}
return super.isShown();
}
主题中修改 SwitchPreferenceCompat 的样式
1
<item name="switchPreferenceCompatStyle">@style/Preference.SwitchPreferenceCompatFixed</item>
定义样式1
2
3
4<style name="Preference.SwitchPreferenceCompatFixed" parent="Preference.SwitchPreferenceCompat">
<item name="android:layout">@layout/preference_material</item>
<item name="android:widgetLayout">@layout/preference_widget_switch_fixed</item>
</style>
创建布局文件preference_widget_switch_fixed.xml
1
2
3
4
5
6
7<android.support.v7.widget.SwitchCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:clickable="false"
android:background="@null" />
日常开发中,如果服务端在本地,通常可通过改hosts、写死IP、动态域名等方式来设置服务端地址,但总觉很麻烦,不灵活;比如更换网络导致IP变化,就得重新设置。
今天突然想到,利用xip.io 和 gradle来自动设置服务端地址
xip.io 是一个直接使用域名来指定 IP 的域名服务,无需手动设置 DNS,同时也不需要任何注册。这解决了使用 IP 无法使用多个 Virtual Host 而使用域名又得很麻烦改地DNS的问题。
xip.io 支持 {custom_prefix}.{host_ip}.xip.io
的域名格式,解析出来的 ip 就是 {host_id}。如:
exmple.com.127.0.0.1.xip.ip
会解析为127.0.0.1
下面进行 gradle 配置:
1 | buildTypes { |
在 debug 下,gradle 会获取当前电脑的 IP,然后写入到 BuildConfig 类中的 ENDPOINT
属性。gradle sync 一下后,BuildConfig.ENDPOINT
就被赋值为 http://example.com.${hostIp}.xip.io/api
当然,还可以放到 xml 文件中:)
1 | resValue "string", "host_url", "http://example.com.${hostIp}.xip.io"; |
最后记得在 apache/nginx 等配置 Virtual Host 时使用宽域名,如 Nginx 中:
1 | server_name example.com.* |
MeiZhi
,然后发现drakeet的是Meizhi
,所以我也改为Meizhi
来保持一致。在 GitHub 上查看时发现名称并没有改变,原因是git默认忽略了大小写。git config core.ignorecase false
push后,在GitHub上查看,发现Meizhi
和MeiZhi
等都同时存在,而自己在本地 ls 并没有异常。OSX 的文件名大小写不敏感但大小写保留,所以并没有都显示出来。
解决:
ssh 到 vps 上,clone 代码,然后删除重复的文件,再 push 到 GitHub。
本地 pull 后,Meizhi
目录也不存在了,git status
查看,显示被删除,使用 git reset ./
来恢复
之后如果再遇到只修改字母大小时可以通过 git mv --force FileName Filename
来避免这个问题。
寒假时参考着 Android程序员简历模板 开始写自己的简历,3.3日收到三翼的老人肖彬学长的阿里巴巴内推邮件,由此进入了找实习的苦逼日子。简历通过,进入内推的电话面试阶段,当时很高兴的还截图发了朋友圈,结果作为处女面,被面试官完虐,这里不得不感叹下阿里招聘系统的效率,挂完电话十多分钟就出面试结果,迅速泯灭了自己的侥幸心理。
面试失败的挫败感让自己开始反思,搜了一些面经,看到一句话: 经验并不能代表能力
。看着自己简历上的项目经历,挺多。然而自己面试时候连大端、小端这样的基础问题都没回答上来,深感自己就是传说中的“码农”。于是开始补基础,不能让自己输的太惨。看了以下的几本书:
带着目的挑了一些觉得可能面试会问到的内容看(今后一定要认真啃完这些书!一定!!!)。
那段时间,知乎马天翼刚拿到阿里的实习 offer,正在他建的群里帮 winter 大大招实习生,同学看到后向他吹了我几句,然后让我联系他。发了简历后,肯定了我几句,说一定能进阿里,并把简历转交给 winter 了。几天后趴在教室座位熟睡时,接到另一位工程师的电面,这次的感觉语气轻多了,问的问题如 Android 架构分层的好处、Retrofit 的缺点如果让我设计会怎样设计,聊了聊开源的东西。最后因为过了内推时间,他让我再参加正常的实习生校招流程,于是进入阿里校招官网再次投递…
接着投了豆瓣,笔试题很简单,两道算法二选一,剩下的都是 Android 基础问题,题目有些多,1个小时答题时间,最后没做完。
某天心血来潮,厚着脸皮,QQ 上找 @碎碎iKe 要了豌豆荚和小米的推荐。豌豆荚很速度,HR隔天就联系我预约了电面时间。豌豆荚的电面每轮一小时左右,都有在线 coding 阶段(通过 collabedit 这个网站)。过了三次电面,需要我去北京现场参加面试 ( 报销来往高铁票和一晚的住宿费好评!)。
去北京之前进行了豆瓣的电面,问的都是 Android 方面的内容,回答的很顺畅。
提前来到贵荚公司附近,被周围的环境惊艳到了,可以秒杀很多公园,让我对贵荚更加向往了。现场面的第一轮是位姐姐,面试过程就跟普通聊天一样,还表扬了我UI方面的知识。第二轮的面试官一上来就让在纸上写代码,都不知道自己当时哪来的自信写完没检查就给他看了,结果漏洞百出=_=,然后全程紧张,问的思维题完全没思路。
在北京浪了两天一个人爬了长城、看了速7,坐高铁回学校的途中通过云招求职的微信号查到了自己的面试结果:已拒绝。当时感觉比失恋还难受,很受打击。
回到学校后,不想看书,玩了几天游戏,想想挂在第五面就心累…
然后QQ突然收到小米HR的加好友请求,都一个月了啊,才联系我=_=于是又被小米的工程师进行电面,那个面试官感觉水平很一般,几个问题都是问我时“xx 会不会”,我说会,并且进行了一些说明,并没有追问我其他的,问的问题都是非常基础的东西。
有意思的是某天玩无秘时,在自己加的一个 BAT 的群里问了下有谁要实习生不,结果真有一个人私聊了我,加了微信,然后让我发把简历发到 email,根据 email 知道他居然是新美互通的一个联合创始人(本只是个外包公司的,新美互通的产品有几亿用户呢,主要做海外市场,Android 上的字体管家就是他们做的,还有一个输入法在 play 工具免费榜排前十)。电面的过程也没难度,问的都是 Android 方面的~随后通知通过了电面,让我去北京面试,又是可以报销高铁啊,哈哈,IT 公司就是财大气粗。
5.3 离开学校和三翼的老大一起去广州参加阿里的现场面试,因为当时有可能拿到豆瓣、小米、新美互通等公司 offer,所以对这次面试并不是特在意,即使失败,也应该有一家收留吧(╥﹏╥)(其实是奔着想去陌生的广州耍耍的,所以面试地点选择了更远的广州,去的时候坐的绿皮车真够折腾的…)。在酒店等待被呼叫的过程中,还接到豆瓣HR的电话,说我通过了面试,要给我 offer。心中一阵狂喜啊,终于有人收留了!!!
阿里的这次面试太幸运了,很感谢第一轮的面试官,把简历递给他时,一行一行指着看,每个链接都手动输入到电脑查看。在他看我 GitHub 时就感觉有戏了,他的话语让我并没有觉得很紧张,还说发现我的 commit 大部分都是中午和晚上,认真吧!!!不过我也犯傻了,问了我一道判断链表环路的算法,因为之前豌豆的电面被问过,当他说说完问题后,我就立马说出了思路,他也立马就换题了=_=然后我没做出来,不过他连忙说没事没事。二面的面试官问的都是关于项目经验的了,很轻松的过了。不过刚回到等待室就被通知进行下一场面试( HR 面),都没来得及喝水啊!!! 面试官姐姐看出来了,给我拿了杯水,太感动了,哈哈。问的问题不算刁难,各个方面的问题都有,自己回答的挺谨慎,生怕哪个问题回答得与 HR 三观不符被刷了 (´Д` )。
晚上收到短信通过了阿里面试,发朋友圈秀一下!嘿。第二天早上又收到小米 HR 的电话,给我 offer,问我入职的事(什么鬼,之前说肯定有现场面试啊,结果一轮就让我过了 =_=? 真是人品爆发啊)。至此已经有了豆瓣、小米、阿里的 offer,果断拒绝了新美的面试。参加阿里的圆桌沙龙被 anli 后,便拒绝了小米和豆瓣 的 offer~。
以上就是我找实习的流水账了(深感不如小学生作文水平,看来以后要多码字了),总结起来,要重视基础,不然会死得很惨;面试机会有很多途径,自己去争取,比如我就从无秘拿到了;内推可以省去很多麻烦,如果有不错的成就很容易拿到内推,我大概是靠 GitHub 上700多的 star 拿的内推;不要太在意一场面试,机会还有很多,太在意可能会过于紧张导致发挥失常;平时多积攒人品…
附上一些很有用的一些资源:
codeKK 开源项目源码分析 能对源码分析,面试时可以给面试官很好的印象~
Data Structure Visualizations 数据结构可视化,很叼啊!
pedrovgs/Algorithms 常用算法的Java实现
leetcode 面试遇到的算法题很多都在上面,相比ACM的OJ更人性化;平时就练练,每道题弄透。
结构之法 算法之道 很多人推荐的
Download the source to use it as library project
这唯一使用途径,居然没有 gradle/maven ?作为 Android Studio 的忠实用户,自己写的库怎么能只提供这么麻烦的方法!!!于是决定把它提交到 Maven Central 中。
虽然 AS 的 gradle 默认使用的是 jcenter 仓库,但我们只需要提交到 Maven Central 即可,jcenter 会自动同步。
如果还没有账号先去 Maven Central 注册: Sign up
并到 Create Issue 提交工单等待管理员的回复,填 groupId 时,请使用顶级 groupId,比如我只需要填写 com.linroid
,就可以发布到任何 com.linroid.*
下的groupId。
我是早上提交的,到晚上0:30的时候收到回复。Why the wait?
Configuration has been prepared, now you can:
Deploy snapshot artifacts into repository https://oss.sonatype.org/content/repositories/snapshots
Deploy release artifacts into the staging repository https://oss.sonatype.org/service/local/staging/deploy/maven2
Promote staged artifacts into repository ‘Releases’
Download snapshot and release artifacts from group https://oss.sonatype.org/content/groups/public
Download snapshot, release and staged artifacts from staging group https://oss.sonatype.org/content/groups/staging
please comment on this ticket when you promoted your first release, thanks
然后通过下面的方法 Release,再次回复那个 issue,几分钟后又收到回复:
Central sync is activated for com.linroid. After you successfully release, your component will be published to Central, typically within 10 minutes, though updates to search.maven.org can take up to two hours.
Chris Banes大神很早前写了一个插件 gradle-mvn-push(终于有机会用它了^﹏^),让你通过一条gradle命令就可以自动构建好aar并提交到 Maven Central。下面介绍这个插件的使用方法。
配置用于上传的认证信息
配置文件默认在${HOME}/.gradle/gradle.properties
,如果没有则自己创建。
1 | NEXUS_USERNAME=linroid |
NEXUS_USERNAME
和 NEXUS_PASSWORD
是你注册的用户名和密码,下面的是用于 GPG 校验的配置信息,关于 GPG 的使用可以参见阮一峰的博文:GPG 入门教程
配置版本信息
在你的module目录创建gradle.properties
文件,添加配置:
1 | POM_NAME=Android FilterMenu Library |
根据你的项目修改吧(‘・ω・’)
添加gradle-mvn-push插件
在 library module 的build.gradle
文件中添加
1 | apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/master/gradle-mvn-push.gradle |
或者可以将 gradle-mvn-push.gradle 文件下载下来,然后将上面的url该为本地路径。
执行gradle task
输入下面的命令,就可以自动构建并上传啦
1 | $ gradle clean build uploadArchives |
出现如下结果就说明上传成功了:)
还没结束,此时你的库并没有发布.
最后一步:close staging repositories
登陆 Sonatype Nexus Professional 点开左边 Build Promotion 的 Staging Repositories ,滚到最下面找到你最新上传的(可以点Content确保是你上传的),选中之后点击上面的 Close
按钮 和 Release
按钮(多谢@drak11t的提示)。
Ok 了,还需要等待一小段时间才能在 http://search.maven.org 搜索到你的包。
##链接
]]>
躲在某一时间,想念一段时间的掌纹;躲在某一地点,聆听四季的声音。
下载地址
写完 http://radio.sky31.com 的后台后,想把 app 也写出来.正好人机交互和 Java 也要交课程设计,就开始写了-.-
这次偷懒,数据缓存直接用 DiskLruCache 来管理没有使用数据库,也没有使用 MVP 模式.
播放器使用 SDK 中的 MediaPlayer实现,暂时还没实现播放缓存。
使用了如下的开源库:
源码放在 GitHub 上了,有兴趣的可以看看:): Github地址
]]>1 | public void setCursor(Cursor cursor){ |
想当然地像使用 ListView 的适配器一样调用 notifyDataSetChanged()
,用但当添加删除城市后 ViewPager 里的视图并没有得到更新。Google 了下,Stackoverflow 里的一个回答让重写 getItemPosition() 方法:
1 |
|
加了之后,Fragment 的数目的确增加/删除了,但 Fragment 里的内容并没有更新,会出现两个一模一样的城市(因为我设置新增的城市显示在前面,不然就不会重复了)。
想了一下,既然是 fragment 应该不会不会像普通视图一样在数据更新后直接销毁掉,于是查看了 FragmentPagerAdapter 的源码。PagerAdapter 通过 instantiateItem(ViewGroup container, int position)
来获得一个视图, FragmentPagerAdapter 中的实现如下:
1 |
|
从中可以看出当在实例化 position 位置的 fragment 时,首先从 FragmentManager 查找在该 position 位置是否已经创建了 fragment,如果存在直接使用这个 fragment,从而达到复用.
假如现在有了三个城市A、B、C,position 分别为0、1、2.然后添加了一个城市 Z,Z 的 position为 0,C 为 3。FragmentManager 里已经存在了 position 为0,1,2的 fragment,所以前三个视图没有改变,但现在需要的 fragment 的数目变了,增加1。会创建 position 为3的 fragment,而此时是城市C,所以这样就会导致显示两次C,而新增的Z城市没有显示。
想起之前写 FragmentPagerAdapter 还会手动在 getItem() 方法里判断 Fragment 是否已经创建,完全没必要嘛, FragmentPagerAdapter 已经为我们做了这点-.-
解决的办法:
在通知数据更新之前从 FragmentManager 里移除所有 Fragment:
1 | private void removeALlFragments(){ |
这样有些暴力了,毕竟创建一个 fragment 需要消耗较大的资源.决定重写 instantiateItem() 方法,当数据更新时,复用已有的 fragment,更新里面的数据。
重写 instantiateItem()
方法:
1 |
|
在 WeatherFragment 里添加下面的方法:
1 | public void setWeatherData(final Weather weather){ |
bash
sudo archlinux-java set java-7-openjdk
sudo ln -sf /usr/bin/python2.7 /usr/bin/python
yaourt -S libtinfo
Webhook
,当收到 push 后会通知到设定的 url,救星来啦~~~git-hook.php
文件放到博客目录下:1 |
|
通过 ssh 执行 php git-hook.php
成功,但 url 访问时失败了。vps 上是通过 php-fpm 执行 php 的,用户为 www-data
,shell 为/usr/sbin/nologin
,会找不到 git 命令,需要使用 git 的绝对路径:$shell = sprintf("cd %s && /usr/bin/git pull", WWW_ROOT);
出现权限问题error: cannot open .git/FETCH_HEAD: Permission denied
修改目录所属:sudo chown -R www-data:www-data ./linroid.com
在仓库的webhook里添加url http://linroid.com/git-hook.php
,然后 vps 就可以从 GitHub 自动 pull 了~
openssl req -new -nodes -keyout alwen_me.key -out alwen_me.csr
需要将这几个密钥放到一个文件中:1
cat alwen_me.crt COMODORSADomainValidationSecureServerCA.crt COMODORSAAddTrustCA.crt AddTrustExternalCARoot.crt > ssl_bundle.cer
接下来就是设置 nginx 配置文件开启 ssl 了:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# HTTPS server
server {
listen 443;
server_name alwen.me;
root /www/alwen/;
index index.html index.htm;
ssl on;
ssl_certificate ssl/ssl_bundle.crt;
ssl_certificate_key ssl/alwen_me.key;
ssl_session_timeout 5m;
ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
ssl_prefer_server_ciphers on;
location / {
try_files $uri $uri/ =404;
}
}
并且将 http 访问跳转至 https 连接:1
2
3
4
5server {
listen 80;
server_name alwen.me www.alwen.me;
return 301 https://alwen.me$request_uri;
}
大功告成(>▽<),现在访问 alwen.me 浏览器就显示出安全标志了.
必须感谢 GitHub 提供的 学生礼包 啊,还有100美刀的 DigitalOcean 消费劵用来作梯子再好不过啦~不过现在好像国内的 .edu 邮箱被屏蔽,申请不了了 >﹏<
SwipeRefreshLayout
在使用上非常简单,只需要把要刷新的可滚动组件放到SwipeRefreshLayout
中,1 | <android.support.v4.widget.SwipeRefreshLayout |
然后调用setOnRefreshListener(OnRefreshListener listener)
和 setColorScheme(int colorRes1, int colorRes2, int colorRes3, int colorRes4)
两个方法进行设置,在OnRefreshListener中实现onRefresh()
方法即可:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
swipeLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_container);
swipeLayout.setOnRefreshListener(this);
swipeLayout.setColorScheme(android.R.color.holo_blue_bright,
android.R.color.holo_green_light,
android.R.color.holo_orange_light,
android.R.color.holo_red_light);
}
public void onRefresh() {
new Handler().postDelayed(new Runnable() {
public void run() {
swipeLayout.setRefreshing(false);
}
}, 5000);
}
SwipeRefreshLayout
实现了Google Now中刷新的风格,与之类似的有一个开源库ActionBar-PullToRefresh则实现了Google Plus的风格。
Google Now即时卡贴中的刷新
Google Plus中的刷新
虽然ActionBar-PullToRefresh
的开发者在前不久建议大家使用SwipeRefreshLayout
但是比较一下两者还是有一定的区别,SwipeRefreshLayout并不能完全取代
layout_height
和 layout_width
无效,如果要在当前布局中显示其他非要被刷新的组件,则需要给SwipeRefreshLayout外套一个父容器:1 | <RelativeLayout |