在前一部分里面previous part ,我们深入挖掘了 looper 和 handler的处理机制,以及他们是怎么与Anroid主线程关联起来的。

现在,我们来深入探讨一下主线程与 安卓组件的生命周期之间是怎么交互的。

Activities 屏幕方向变化是一个常见的问题

我们首先还是从activity的生命周期,以及activity处理 configuration changes【比如屏幕方向的变化】的机制谈起。

为什么要探讨这一问题

本文的初衷来自于 在 Square Register 中真实发生的崩溃bug。下面是引起问题的相关代码【简化版】:
public class MyActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
public void run() {
doSomething();
}
});
} void doSomething() {
// Uses the activity instance
}
}
 
 
如我们所见,当触发 configuration change事件时,doSomething()是可能在activity的onDestroy()方法之后才被调用的,而这时你已经不能再使用activity实例了。

让我们再来复习一下orientation changes事件

设备的方向是可能在任何时候被改变的. 我们使用Activity#setRequestedOrientation(int) 方法来模拟orientation change 行为。

你知道下面的log输出的是啥么?

public class MyActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("Square", "onCreate()");
if (savedInstanceState == null) {
Log.d("Square", "Requesting orientation change");
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
} protected void onResume() {
super.onResume();
Log.d("Square", "onResume()");
} protected void onPause() {
super.onPause();
Log.d("Square", "onPause()");
} protected void onDestroy() {
super.onDestroy();
Log.d("Square", "onDestroy()");
}
}

If you know the Android lifecycle, you probably predicted this:

如果你知道Android lifecycle这篇文章,你可能这么预测:

onCreate()
Requesting orientation change
onResume()
onPause()
onDestroy()
onCreate()
onResume()
The Android Lifecycle goes on normally, the activity is created, resumed, and then the orientation change is taken into account and the activity is paused, destroyed, and a new activity is created and resumed. 正常来说应该是这样的,activity created, resumed,然后才考虑orientation change行为的触发,这时, activity 再执行 paused, destroyed,然后新的activity重新执行,created 和 resumed方法。
 

Orientation changes 和主线程

此处有一个重要的细节需要注意: orientation change行为会导致activity被重新创建。而这一行为仅通过

post一个消息给主线程的方式来实现。
下面我们可以通过反射来检测主循环队列 中的内容
public class MainLooperSpy {
private final Field messagesField;
private final Field nextField;
private final MessageQueue mainMessageQueue; public MainLooperSpy() {
try {
Field queueField = Looper.class.getDeclaredField("mQueue");
queueField.setAccessible(true);
messagesField = MessageQueue.class.getDeclaredField("mMessages");
messagesField.setAccessible(true);
nextField = Message.class.getDeclaredField("next");
nextField.setAccessible(true);
Looper mainLooper = Looper.getMainLooper();
mainMessageQueue = (MessageQueue) queueField.get(mainLooper);
} catch (Exception e) {
throw new RuntimeException(e);
}
} public void dumpQueue() {
try {
Message nextMessage = (Message) messagesField.get(mainMessageQueue);
Log.d("MainLooperSpy", "Begin dumping queue");
dumpMessages(nextMessage);
Log.d("MainLooperSpy", "End dumping queue");
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
} public void dumpMessages(Message message) throws IllegalAccessException {
if (message != null) {
Log.d("MainLooperSpy", message.toString());
Message next = (Message) nextField.get(message);
dumpMessages(next);
}
}
}
如你所见,消息队列仅仅是个链表
下面是 orientation change 触发之后的 消息队列中的记录:
public class MyActivity extends Activity {
private final MainLooperSpy mainLooperSpy = new MainLooperSpy(); protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("Square", "onCreate()");
if (savedInstanceState == null) {
Log.d("Square", "Requesting orientation change");
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
mainLooperSpy.dumpQueue();
}
}
}
Here is the output:
onCreate()
Requesting orientation change
Begin dumping queue
{ what=118 when=-94ms obj={1.0 208mcc15mnc en_US ldltr sw360dp w598dp h335dp 320dpi nrml land finger -keyb/v/h -nav/h s.44?spn} }
{ what=126 when=-32ms obj=ActivityRecord{41fd2b48 token=android.os.BinderProxy@41fcce50 no component name} }
End dumping queue

通过查看ActivityThread的实现,我们可以知道消息118,126代表的是什么:

public final class ActivityThread {
private class H extends Handler {
public static final int CONFIGURATION_CHANGED = 118;
public static final int RELAUNCH_ACTIVITY = 126;
}
}

当发起一个orientation change操作时,实际上就是把 CONFIGURATION_CHANGED 和 RELAUNCH_ACTIVITY消息添加到了主循环队列中。

下面我们来一步步的分析:

当activity第一次启动时,消息队列是空的。 当前正在执行的消息是LAUNCH_ACTIVITY。

LAUNCH_ACTIVITY消息的处理过程是这样的:首先创建一个

activity 实例,然后调用onCreate方法和onResume方法。然后LAUNCH_ACTIVITY消息就算完成了,主循环继续去运行下一条消息。
When a device orientation change is detected, a RELAUNCH_ACTIVITY is posted to the queue.
当orientation change事件被监测到的时候,RELAUNCH_ACTIVITY消息被添加到队列中来。
消息处理过程是这样的:
  • 依次调用旧activity的 onSaveInstanceState()onPause()onDestroy() 方法
  • 创建一个新的activity实例,
  • 依次调用新activity实例的onCreate() 和onResume()方法。
这一过程都包含在一个消息中处理。也就是说,在上述过程中,任何post调用都会在onResume() 方法之后被调用。

现在放到一起来看下

如果你在onCreate中post一个消息, 而此时恰好orientation change事件也被触发,会出现什么样的情况?

看下面两个例子,注意orientation change前后的log:

public class MyActivity extends Activity {
private final MainLooperSpy mainLooperSpy = new MainLooperSpy(); protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("Square", "onCreate()");
if (savedInstanceState == null) {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
public void run() {
Log.d("Square", "Posted before requesting orientation change");
}
});
Log.d("Square", "Requesting orientation change");
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
handler.post(new Runnable() {
public void run() {
Log.d("Square", "Posted after requesting orientation change");
}
});
mainLooperSpy.dumpQueue();
}
} protected void onResume() {
super.onResume();
Log.d("Square", "onResume()");
} protected void onPause() {
super.onPause();
Log.d("Square", "onPause()");
} protected void onDestroy() {
super.onDestroy();
Log.d("Square", "onDestroy()");
}
}

Here is the output:

onCreate()
Requesting orientation change
Begin dumping queue
{ what=0 when=-129ms }
{ what=118 when=-96ms obj={1.0 208mcc15mnc en_US ldltr sw360dp w598dp h335dp 320dpi nrml land finger -keyb/v/h -nav/h s.46?spn} }
{ what=126 when=-69ms obj=ActivityRecord{41fd6b68 token=android.os.BinderProxy@41fd0ae0 no component name} }
{ what=0 when=-6ms }
End dumping queue
onResume()
Posted before requesting orientation change
onPause()
onDestroy()
onCreate()
onResume()
Posted after requesting orientation change
总结一下:在onCreate方法执行到最后时,消息队列中含有4个消息。第一个消息时orientation change之前添加的post消息,紧接着的两个消息是与orientation change相关联的两个消息,然后是orientation change之后添加的post消息。可以在日志中看到他们是有序进行的。
也就是说,任何在orientation change之前添加的消息都会在onPause调用之前被调用,任何在orientation change之后添加的消息都会在
onResume调用之后被调用。
实际的意义在于,当你发生一个post消息时,你不能保证activity实例在消息执行时还存在(即便你是在 onCreate() or onResume()中调用的post).如果你的消息持有View或者activity的引用,则activty将不会被及时回收。
 

所以,我们应该怎么做呢?

彻底的解决办法

不要在主线程下啊调用handler.post(). 在大部分情况下,handler.post() 被用于快速修复一些与时序相关的问题。

推荐的做法是修改你的程序逻辑,而不是乱调handler.post()方法。

如果确实不得不调用post

请确定你在做后台操作时,不持有activity的引用~

好吧,如果你确实需要引用activity

那么请在activityonPause里面调用

handler.removeCallbacks()来确保消息队列清空。

当然,如果你想被开除的话,你可以这么做:

使用handler.postAtFrontOfQueue() 去确保消息总是在 onPause() 之前抵用. 然后你的代码就会变得很难懂。。。

顺便提一下runOnUiThread()

你有没注意到我们使用 handler.post()而不是直接调用Activity.runOnUiThread()?

看下面你就明白了:

public class Activity {
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
}

Unlike handler.post()runOnUiThread() does not post the runnable if the current thread is already the main thread. Instead, it calls run() synchronously.

handler.post()不一样runOnUiThread()下,如果当前线程就是主线程的时候,它是直接同步调用runnable方法的。

关于服务

这有一个常见的误解需要澄清一下: service 并不运行在后台线程中

所有的,service 生命周期相关方法如onCreate()onStartCommand(),都是在主线程中运行的

不管是在service还是在activity中,长时间的任务都应该交给后台线程。后台线程的存活期可以和应用本身一样长。

但是,有时候 系统可能会杀掉app进程。

使用service 有助于我们尽可能的延长引用的存活期。

边注: 当IBinder接收到另一个进程的调用时,方法将会在后台线程中调用。

Take the time to read the Service documentation – it’s pretty good.

结论

大部分Android 生命周期相关的方法都在主线程中调用。

必须再啰嗦一句:不要阻塞主线程!!!

不过,你有没想过阻塞主线程是啥效果?请看下章

Have you ever wondered what blocking the main thread actually means? That’s the subject of the next part!


【翻译】configuration changes与handler.post的更多相关文章

  1. CritterAI 翻译 Configuration Parameters

    翻译自: http://www.critterai.org/projects/nmgen_study/config.html 参考: http://blog.csdn.net/kun1234567/a ...

  2. This Handler class should be static or leaks might occur(null) 解决办法 (转)

    原文链接:http://blog.csdn.net/wuleihenbang/article/details/17126371 首先解释下这句话This Handler class should be ...

  3. handler与anr机制

    1. handler 参考资料:http://blog.csdn.net/ly502541243/article/details/52062179/ 首先说明Android的两个特性: 1. 只能在主 ...

  4. asp.net core microservices 架构之eureka服务发现

    一 简介 微服务将需多的功能拆分为许多的轻量级的子应用,这些子应用相互调度.好处就是轻量级,完全符合了敏捷开发的精神.我们知道ut(单元测试),不仅仅提高我们的程序的健壮性,而且可以强制将类和方法的设 ...

  5. jmeter测试TCP服务器/模拟发送TCP请求 设置16进制发送(转)

    转载留存:http://blog.sina.com.cn/s/blog_46d0362d0102v8ii.html 性能测试需要模拟多种场景,经常受制于资源限制,没办法建立贴近实际部署环境的场景.因而 ...

  6. Vert.x Core 文档手册

    Vert.x Core 文档手册 中英对照表 Client:客户端 Server:服务器 Primitive:基本(描述类型) Writing:编写(有些地方译为开发) Fluent:流式的 Reac ...

  7. WebView与JS的几种交互

    http://www.jianshu.com/p/0042d8eb67c0 最近整理了一下原生与H5之间的交互方式,简单的做个总结.OC端与JS的交互,大致有这几种:拦截协议.JavaScriptCo ...

  8. SharePoint 2013 Apps TokenHelper SharePointContext OAuth Provider-Hosted App (抄袭,测试 csc.rsp 用)

    namespace Microshaoft.SharePointApps { using Microsoft.IdentityModel; using Microsoft.IdentityModel. ...

  9. 上传文件swfUploadConfig.js

    /*/* * Unobstrusive swf upload widget using jQuery. *example : $(':file.uo_widget_form_input_file_sw ...

随机推荐

  1. Javascript中bind、call、apply函数用法

    js 里函数调用有 4 种模式:方法调用.正常函数调用.构造器函数调用.apply/call 调用. 同时,无论哪种函数调用除了你声明时定义的形参外,还会自动添加 2 个形参,分别是 this 和ar ...

  2. 关于yuv格式

    首先,内存分布        1:YUV420          (1):I420:              YYYYYYYY UU VV    =>YUV420P          (2): ...

  3. Web Api 简介

    ASP.NET Web API 简介  ASP.NET MVC 4 包含了 ASP.NET Web API, 这是一个创建可以连接包括浏览器.移动设备等多种客户端的 Http 服务的新框架, ASP. ...

  4. ML-分类与逻辑回归

    布尔分类(binary classification)问题: 训练集:$S=\{(x^{(i)}, y^{(i)})\}$ 输入:特征向量$x$ 期望输出:$y\in\{0, 1\}$ 这里使用的假设 ...

  5. 如何站在使用者的角度来设计SDK-微信公众号开发SDK(消息处理)设计之抛砖引玉

    0.SDK之必备的基本素质 在项目中免不了要用到各种各样的第三方的sdk,在我现在的工作中就在公司内部积累了各种各样的的公共库(基于.net的,基于silverlight的等等),托管到了内部的nug ...

  6. wkhtmltopdf 安装使用笔记(CentOS6)

    1. 在官网下载安装文件. http://wkhtmltopdf.org/ 安装时如果提示某些库找不到的话,使用yum安装即可. 2. 命令行测试 $ wkhtmltopdf http://news. ...

  7. SVN服务器搭建和使用(一)

    SVN服务器搭建和使用(一) Subversion是优秀的版本控制工具,其具体的的优点和详细介绍,这里就不再多说. 首先来下载和搭建SVN服务器. 现在Subversion已经迁移到apache网站上 ...

  8. 微信小程序开发感受

    研究了大概有一个多星期的小程序了,说一下感受,之后会随时更新,一边学习,一边加上一部分学习代码和心得.我是一个前端厂里的新手,搬砖的时间不是很长,所以到一部分知识的理解浅之又浅,所以只能说自己的理解, ...

  9. NSUserDefaults的使用

    创建一个user defaults方法有多个,最简单得快速创建方法: NSUserDefaults *accountDefaults = [NSUserDefaultsstandardUserDefa ...

  10. [Bug]枚举数组,并找到某些元素删除

    lldb报错:Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <_ ...