0%

第5章:Android 进程/线程管理

Android 进程和线程

进程(process) 是程序的一个运行实例,而线程(Thread)则是 CPU 调度的基本单位。

对于Android应用开发者而言,通常面对的都是 Activity、Service等组件,并不需要特别关心进程是什么,因而产生了一些误区,如部分研发者认为系统四大组件就是进程的载体。

很遗憾,虽然四大组件很符合我们对进程的印象,但是他们不能算是完整的进程实例,最多只能算进程的组成部分,从 AndroidManifest.xml 中也可以得到一点提示(这个xml是对应用程序的声明和描述):

1
2
3
4
<application android:label="launch performance">
<activity android:name="SimpleActivity">

...

可以看到,Activity的外围有一个名为 的标签,换句话说,四大组件都只是 “application”的零件。通过例子来分析让读者有个更全面的认识,通过Activity A启动Activity B,在 B 的onCreate(Bundle savedInstanceState) 打断点,查看断点详情可以看到如下图所示的内容:

activity启动

从这个实验中还解决了一个重要问题,即主线程到底怎么产生的,从上图的函数堆栈可以知道:主线程由ZygoteInit启动,经由一系列调用后最终才执行Activity的onCreate函数,并且,Zygote为Activity创建的主线程是 ActivityThread。以下是源码展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
CloseGuard.setEnabled(false);

//只有主线程才能调用这个函数,普通线程应该使用prepare(),具体见对 Looper 的讲解
Looper.prepareMainLooper();

//主线程对应的handler
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}

//这个main()是static的,因此这里需要创建一个实例
ActivityThread thread = new ActivityThread();

//Activity是由界面显示的,这个函数将与WindowManagerService 建立联系
thread.attach(false, startSeq);

//主循环开始
Looper.loop();

//如果程序运行到了这里,说明退出了上面的Looper循环
throw new RuntimeException("Main thread loop unexpectedly exited");
}

其实,启动Activity后,除了main thread 外还有多个binder线程,如下图所示(用于Binder的那些线程是在什么时候创建的,这个问题留到后面Binder章节详细解答):

运行的thread图

图中可以看到,B 被启动之后,主线程也始终只有一个,此时A退出了运行,但没有被杀掉,只是被压入了栈中。同样,如果我们启动一个Service,并把断点打在Service的onCreate方法的中,我们会发现,Service也是寄存于ActivityThread之中的,并且启动流程和Activity基本上一致,并且启动Service时,同样有Binder线程支持。限于篇幅,这里不做截图和代码。

按照Android系统设计:”By default,all componets of the same application run in the same process and thread(called the “main” thread)”,这可以理解为,对于同一个AndroidManifest中定义的四大组件,除非有特别的声明,否则它们都运行在同一个进程中(并且均由主线程处理事件)。如何证明呢?根据前面操作系统的基础知识,如果两个对象处于同一个进程空间,那么内存区域应该是可共享访问的,利用这个原理我们可以论证下:

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
//第1个activity
public class MainActivity extends BaseActivity {

static int ConstTemp = -1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//在启动SecondActivity 前,将ConstTemp值改为2,如果他们不是处于同一个进程,那么在
// SecondActivity中是无法获得更新后的值 2 的,只可能是 -1
ConstTemp = 2;
startActivity(new Intent(this,SecondActivity.class));
}
}


//第2个acitivity
public class SecondActivity extends BaseActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);

Log.e("SecondActivity","ConstTemp = " + MainActivity. ConstTemp);
}
}

最终输出的结果是2,说明都是在同一个进程中,当然,我们还可以通过查看二者的 PID 和 TID 的方法证明这两个Activity默认确实在同一个进程中。当然,Android还提供了特殊方式让不是同一个包的组件也可以运行于相同的进程中,这样做的优势是,它们可以方便地资源共享,而不用大费周章地进程间通信。这可以分为两种情况:

  1. 在AndroidManifest中的四大组件标签中加入 android:process 来表明这一组件想要运行在哪个进程空间。
  2. 针对整个程序包,可以直接在 标签中,加入 android:process 属性来指明想要依存的进程环境。

Handler、MessageQueue、Runable与Looper

参考专题里面的内容(现在还没放上去)

UI主线程-ActivityThread

前面提到Activity的部分源码,这里精简下再贴出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {

Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);

if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}

Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");
}

普通线程使用Looper的代码如下:

1
2
3
4
5
6
7
8
9
10
11
class LooperThread extends Thread{
public Handler mHandler;
public void run(){
Looper.prepare();
mHandler = new Handler(){
public void handleMessage(Message msg){}
}

Looper.loop();
}
}

比较二者的代码可知其中的区别主要体现在:

  • prepareMainLooper 和 prepare,普通线程prepare就可以了,而主线程使用 prepareMainLooper ,主要是多了一步给sMainLooper 赋值的操作,这样,我们就能从主线程中通过 getMainLooper() 方式获得主线程的 Looper。

  • Handler不同,普通线程申城一个与Looper绑定的Handler,而主线程是从当前线程中获取的Handler,也就是说,ActivityThread 提供了一个“事件管家”,以处理主线程中各种消息。

Thread 类

Thread 类的内部原理

Thread 实现了 Runnable ,也就是说线程是“可执行的代码”。我们一般通过2种方式使用Thread :

1
2
3
4
5
6
//第1种
MyThread thr = new MyThread(...);
thr.start();

//第2种
new Thread(Runnable target).start();

这两种方法最终都通过 start 启动,它会间接调用Runable 的 run 实现.

线程的休眠和唤醒

控制线程的相关方法我们至少可以想到以下几个: wait()、notify()、notifyAll()、interrupt()、join() 和 sleep()。

  1. wait 和 notify/notifyAll

和其他方法不同,这3个函数是由 Object 类定义的——意味着它们是任何类的共有“属性”,那为什么这么设计呢?官方对wait的解释是:

Causes the calling thread to wait until another thread calls the notify() or notifyAll() method of this object

当某个线程调用一个 Object 的wait 方法时,系统就要在这个 Object 中记录这个请求。因为调用者很可能不止一个,所以可使用列表的形式来逐一添加它们。当后期唤醒条件满足时, Object 既可以使用 notify 来唤醒列表中的一个等待线程,也可以通过 notifyAll 来唤醒列表中的所有线程。值得注意的是,调用者只有称为 Object 的 monitor 后,才能调用它的 wait 方法,而称为一个对象的 monitor 有以下3种途径:

  • 执行这个 object 的 synchronize 方法
  • 执行一段 synchronize 代码,并且是基于这个 object 做的同步
  • 如果 object 是 Class 类,可以执行它的 synchronize static 方法
  1. interrupt :如果说wait是自愿行为, 那 interrupt 就是 “被迫” 的了,它的意思就是“中断”。
  2. join,用以下例子说明:
1
2
3
4
5
Thread t1 = new Thread(new ThreadA());
Thread t2 = new Thread(new ThreadB());
t1.start();
t1.join();
t2.start();

它希望达到的目的就是只有当 t1 线程执行完成时,我们才接着执行后面的 t2.start() 。这样就保证了两个线程顺序执行。

  1. sleep:它和 wait 一样都是属于“自愿”的行为,只不过 wait 是等待某个 object ,而sleep 是等待时间,一旦设置的时间到了就会被唤醒。

Thread 实例

讲一个典型范例来理解Thread:假如我们使用 SeekBar 开控制系统音效,要求:(1)UI界面响应流畅,(2)并且要能反映出音效的变化,(3)并且系统稳定。同时,有以下几个前提:

  1. 向系统发送音效调整的命令是个耗时操作
  2. 频繁向系统发送调整命令会导致死机
  3. 用户的操作是随意的,没有规律的(用户可能飞快地拉动,也有可能慢慢拉动)

对于要求1,可以将发送命令这个耗时操作放到一个独立线程执行,这样就不会影响UI,保证流畅。
而根据3个前提条件,条件2和条件3是有矛盾的,如果要实时听到音效变化,在seekbar进度变化错城中需要不停地发送请求,而用户快速滑动导致产生大量请求,可能会引起死机,从而违背第3个要求。

再来想想其它简单方法,当启动一个新的线程处理调整请求时,显然需要把这些请求先放入消息队列中再排队处理,假设用户1s内产生了24个请求,那么队列中的数量将会陆续增加(假设500ms才处理完一个),直到用户操作结束。那么,实际上这些请求值是有优先级的,即后产生的调整值更贴近用户想要的效果。根据这个思想,我们可以适当控制消息队列中元素的数量,比如:

  • 当产生新的调整值时,先清空消息队列,然后再把请求入队。
  • 当产生新的调整值时,先判断消息队列的数量,根据实际情况删除部分消息,然后才请求入队。
    采用这种方式可以保证最后一个入队的请求总是可以被处理的,这也就意味着用户最终选择的音效值是可以体现出来的。示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private Thread mBusinessThread = null;
private boolean mBusinessThreadStarted = false;
private BusinessThreadHandler mBusinessThreadHandler = null;
private void startBusinessThread(){
if(true == mBusinessThreadStarted)
return;
else
mBusinessThreadStarted = true;

mBusinessThread = new Thread(new Runnable(){
@Override
public void run(){
Looper.prepare();
mBusinessThreadHandler = new BusinessThreadHandler();
Looper.loop();
}
});
mBusinessThread.start();
}

上述代码使用Looper.loop 来不断处理调整请求,这些请求是通过 mBusinessThreadHandler 发送到 mBusinessThread 的消息队列中的,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BusinessThreadHandler extends Handler{
//重写 sendMessage
@Override
public boolean sendMessage(int what,int arg1,int arg2){
//清理消息队列中未处理的请求
removeMessages(what);
//发送消息到队列
return super.sendMessage(obtainMessage(what,arg1,arg2));
}

public void handleMessage(Message msg){
switch(msg.what){
case MSG_CODE:
//执行耗时操作
break;
default:
break;
}
}
}

sendMessage 方法中首先清除了消息队列中还未被处理的请求,这样一方面降低了程序向系统发送请求的频率,加快了相应速度和UI流畅性;另一方面保证 BusinessThreadHandler 下次取到的是优先级较高的调整请求值,保证用户听到实时的音效变化。

Android 应用程序的典型启动流程

我们了解到 Android 系统中一个应用程序的主体是由 ActivityThread 构成的,并且 Android 系统是基于 Linux 的,原则上说它的应用程序并不只是 APK 一种类型,换句话说,所有 Linux 支持的应用程序都可以通过一定方式运行在 Android 上(一些系统级应用程序就是以这种方式存在的),为了叙述统一,我们这里所指的应用程序都是 APK 类型的应用程序。它们通常由两种方式在系统中被启动:

  • 在 Launcher 中点击相应的应用程序图标启动
  • 通过 startActivity 启动

这两种启动方式的流程基本上是一致的,最终都会调用 ActivityManagerService(以下简称 AMS) 的 startActivity 来完成。在新的 Activity 启动钱,原先处于 resume 状态的 Activity 会被 pause ,这种管理方式比 Windows 的多窗口系统简单得多,将一个 Activity 置为 pause 主要通过此 Activity 所属进程的 ApplicationThread.schedulePauseActivity 方法完成,ApplicationThread 是应用程序进程提供给 AMS 的一个 Binder 通道。假设即将启动的 Activity 所属的进程并不存在,那么 AMS 还需要先把它启动起来。

ActivityThread 启动并做好相应的初始化工作后,需要调用 attachApplication 来通知 AMS ,后者才能继续执行未完成的 startActivity 流程。具体而言, AMS 通过 ApplicationThread.scheduleLaunchActivity 请求应用程序来启动一个指定的 Activity ,之后一系列工作就要靠应用进程自己来完成,如 Activity 创建 Window,遍历 View Tree 等。

在进入后面的章节前,这里先大略熟悉 startActivity 的流程。

谢谢你的鼓励