一 前言
异步编程是android初学者的一个难点,却也是始终不能绕过的一个坎。可以说几乎每个app都逃不了网络编程,而网络编程又往往建立在异步的机制之上(你不应该也无法在UI线程里执行网络请求,你也不应该在UI线程中频繁的进行IO操作)。等等,你不知道什么是线程?那就对了,我们一起来回忆一下大学课本的知识,一切从进程讲起。
二 进程和线程
我曾经在知乎上听一个朋友说一个优秀的程序员一定会有着极强的对抽象的理解能力,我很赞同这句话,我心里一直鼓励自己:当你对抽象不再惧怕的时候,可能你正在成为一名真正的coder。
1.进程(process)
A process is the operating system’s abstraction for a running program
这是csapp中的原话,我觉得两个词特别重要,一个是abstraction,说明进程是一种抽象,是人为的一种定义,另一个是running,说明进程是正在执行的程序,而不是保存在磁盘上的一个程序文件。不管你现在怎么理解进程,你都得看下面一段代码:
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
这可能是我们人生写得第一行代码,让我们在终端里gcc得到可执行文件a.out,然后执行它,好,在你按下return键的那一瞬间到终端里打印出hello,world(好吧我承认我词穷了,其实就是a.out被执行时),进程动态产生,动态消亡。怎么直观的感受它呢,来改一下代码:
#include <stdio.h>
#include <unistd.h>
int main(){
printf("Hello World from process ID %ld \n",(long)getpid());
return 0;
}
编译,运行得到:
Hello World from process ID 20289
在这里我们得到了这个进程的ID(UNIX系统确保每个进程都有一个唯一的数字标识符,称为进程ID,进程ID总是一个非负整数。),这也算进程存在的一点痕迹吧。我们再改动一下代码:
#include <stdio.h>
#include <unistd.h>
int doSomething();
int main(){
printf("Hello World from process ID %ld \n",(long)getpid());
doSomething();
return 0;
}
int doSomething(){
printf("let us doing something from ID %ld\n",(long)getpid());
return 0;
编译,执行:
Hello World from process ID 20777
let us doing something from ID 20777
可以看到这两个函数的进程ID是一样的,其实你进一步调用getppid()函数得到父进程的函数其实也是一样的。细心的朋友就会发现,上一次执行后得到的ID是20289,这次执行得到的ID却是20777,同样的文件为什么每次执行得到的ID却是不同的呢?这就需要我们好好体会进程是动态产生动态消亡的了,抽象吗?
2.线程(Thread)
可以这么说,一切的抽象都是为了解放生产力。系统为什么要抽象出进程的概念?一个直观的解释就是它可以让每个进程独立的拥有虚拟地址空间、代码、数据和其它各种系统资源,它还可以让多个进程同时执行,让你在写代码的同时还能挂着微信,放着音乐。可是这还不够,因为一个进程在某一时刻只能做一件事情,为了进一步提高效率,又抽象出进程的概念,来看下面这段话:
线程是进程内部的一个执行单元。系统创建好进程后,实际上就启动执行了该进程的主执行线程。主执行线程终止了,进程也就随之终止。
也就是说,对线程来说,进程相当于一个容器,可以有许多线程同时在一个进程里执行。
3.安卓中的进程与线程
这里引用官方文档的解释,也不知是谁翻译的,总之献上膝盖看原网页点这里为了阅读方便把原文贴出来了并改正了一些错别字
当一个Android应用程序组件启动时候,如果此时这个程序的其他组件没有正在运行,那么系统会为这个程序以单一线程的形式启动一个新的Linux 进程。 默认情况下,同一应用程序下的所有组件都运行在相同的进程和线程(一般称为程序的“主”线程)中。如果一个应用组件启动但这个应用的进程已经存在了(因为这个应用的其他组件已经在之前启动了),那么这个组件将会在这个进程中启动,同时在这个应用的主线程里面执行。然而,你也可以让你的应用里面的组件运行在 不同的进程里面,也可以为任何进程添加额外的线程。
这片文章讨论了Android程序里面的进程和线程如何运作的。
进程
默认情况下,同一程序的所有组件都运行在相同的进程里面,大多数的应用都是这样的。然而,如果你发现你需要让你的程序里面的某个组件运行在特定的进程里面,你可以在manifest 文件里面设置。
manifest 文件里面为每一个组件元素—<activity>, <service>, <receiver>, 和<provider>—提供了 android:process 属 性。通过设置这个属性你可以让组件运行在特定的进程中。你可以设置成每个组件运行在自己的进程中,也可以让一些组件共享一个进程而其他的不这样。你还可以 设置成不同应用的组件运行在同一个进程里面—这样可以让这些应用共享相同的Linux user ID同时被相同的证书所认证。
<application> 元素也支持 android:process 属性,设置这个属性可以让这个应用里面的所有组件都默认继承这个属性。
Android 可能在系统剩余内存较少,而其他直接服务用户的进程又要申请内存的时候shut down 一个进程, 这时这个进程里面的组件也会依次被kill掉。当这些组件有新的任务到达时,他们对应的进程又会被启动。
在决定哪些进程需要被kill的时候,Android系统会权衡这些进程跟用户相关的重要性。比如,相对于那些承载这可见的activities的 进程,系统会更容易的kill掉那些承载不再可见activities的进程。决定是否终结一个进程取决于这个进程里面的组件运行的状态。下面我们会讨论 kill进程时所用到的一些规则。
进程的生命周期
作为一个多任务的系统,Android 当然系统能够尽可能长的保留一个应用进程。但是由于新的或者更重要的进程需要更多的内存,系统不得不逐渐终结老的进程来获取内存。为了声明哪些进程需要保 留,哪些需要kill,系统根据这些进程里面的组件以及这些组件的状态为每个进程生成了一个“重要性层级” 。处于最低重要性层级的进程将会第一时间被清除,接着是重要性高一点,然后依此类推,根据系统需要来终结进程。
在这个重要性层级里面有5个等级。下面的列表按照重要性排序展示了不同类型的进程(第一种进程是最重要的,因此将会在最后被kill):
Foreground进程 一个正在和用户进行交互的进程,如果一个进程处于下面的状态之一,那么我们可以把这个进程称为 foreground 进程:
进程包含了一个与用户交互的 Activity (这个 Activity的 onResume() 方法被调用)。
进程包含了一个绑定了与用户交互的activity的 Service 。
进程包含了一个运行在”in the foreground”状态的 Service —这个 service 调用了 startForeground()方法。
进程包含了一个正在运行的它的生命周期回调函数 (onCreate(), onStart(), oronDestroy())的 Service 。
进程包含了一个正在运行 onReceive() 方法的 BroadcastReceiver 。
一般说来,任何时候,系统中只存在少数的 foreground 进程。 只有在系统内存特别紧张以至于都无法继续运行下去的时候,系统才会通过kill这些进程来缓解内存压力。在这样的时候系统必须kill一些 (Generally, at that point, the device has reached a memory paging state,这句如何翻译较好呢)foreground 进程来保证 用户的交互有响应。
Visible进程 一个进程没有任何 foreground 组件, 但是它还能影响屏幕上的显示。 如果一个进程处于下面的状态之一,那么我们可以把这个进程称为 visible 进程:
进程包含了一个没有在foreground 状态的 Activity ,但是它仍然被用户可见 (它的 onPause() 方法已经被调用)。这种情况是有可能出现的,比如,一个 foreground activity 启动了一个 dialog,这样就会让之前的 activity 在dialog的后面部分可见。
进程包含了一个绑定在一个visible(或者foreground)activity的 Service 。
一个 visible 进程在系统中是相当重要的,只有在为了让所有的foreground 进程正常运行时才会考虑去kill visible 进程。
Service进程 一个包含着已经以 startService() 方法启动的 Service 的 进程,同时还没有进入上面两种更高级别的种类。尽管 service 进程没有与任何用户所看到的直接关联,但是它们经常被用来做用户在意的事情(比如在后台播放音乐或者下载网络数据),所以系统也只会在为了保证所有的 foreground and visible 进程正常运行时kill掉 service 进程。
Background进程 一个包含了已不可见的activity的 进程 (这个 activity 的 onStop() 已 经被调用)。这样的进程不会直接影响用户的体验,系统也可以为了foreground 、visible 或者 service 进程随时kill掉它们。一般说来,系统中有许多的 background 进程在运行,所以将它们保持在一个LRU (least recently used)列表中可以确保用户最近看到的activity 所属的进程将会在最后被kill。如果一个 activity 正确的实现了它的生命周期回调函数,保存了自己的当前状态,那么kill这个activity所在的进程是不会对用户在视觉上的体验有影响的,因为当用户 回退到这个 activity时,它的所有的可视状态将会被恢复。查看 Activities 可以获取更多如果保存和恢复状态的文档。
Empty 进程 一个不包含任何活动的应用组件的进程。 这种进程存在的唯一理由就是缓存。为了提高一个组件的启动的时间需要让组件在这种进程里运行。为了平衡进程缓存和相关内核缓存的系统资源,系统需要kill这些进程。
Android是根据进程中组件的重要性尽可能高的来评级的。比如,如果一个进程包含来一个 service 和一个可见 activity,那么这个进程将会被评为 visible 进程,而不是 service 进程。
另外,一个进程的评级可能会因为其他依附在它上面的进程而被提升—一个服务其他进程的进程永远不会比它正在服务的进程评级低的。比如,如果进程A中 的一个 content provider 正在为进程B中的客户端服务,或者如果进程A中的一个 service 绑定到进程B中的一个组件,进程A的评级会被系统认为至少比进程B要高。
因为进程里面运行着一个 service 的评级要比一个包含background activities的进程要高,所以当一个 activity 启动长时操作时,最好启动一个 service 来 做这个操作,而不是简单的创建一个worker线程—特别是当这个长时操作可能会拖垮这个activity。比如,一个需要上传图片到一个网站的 activity 应当开启一个来执行这个上传操作。这样的话,即使用户离开来这个activity也能保证上传动作在后台继续。使用 service 可以保证操作至少处于”service process” 这个优先级,无论这个activity发生了什么。这也是为什么 broadcast receivers 应该使用 services 而不是简单的将耗时的操作放到线程里面。
线程
当一个应用启动的时候,系统会为它创建一个线程,称为“主线程”。这个线程很重要因为它负责处理调度事件到相关的 user interface widgets,包括绘制事件。你的应用也是在这个线程里面与来自Android UI toolkit (包括来自 android.widget 和 android.view 包的组件)的组件进行交互。因此,这个主线程有时候也被称为 UI 线程。
系统没有为每个组件创建一个单独的线程。同一进程里面的所有组件都是在UI 线程里面被实例化的,系统对每个组件的调用都是用过这个线程进行调度的。所以,响应系统调用的方法(比如 onKeyDown() 方法是用来捕捉用户动作或者一个生命周期回调函数)都运行在进程的UI 线程里面。
比如,当用户点击屏幕上的按钮,你的应用的UI 线程会将这个点击事件传给 widget,接着这个widget设置它的按压状态,然后发送一个失效的请求到事件队列。这个UI 线程对请求进行出队操作,然后处理(通知这个widget重新绘制自己)。
当你的应用与用户交互对响应速度的要求比较高时,这个单线程模型可能会产生糟糕的效果(除非你很好的实现了你的应用)。特别是,当应用中所有的事情 都发生在UI 线程里面,那些访问网络数据和数据库查询等长时操作都会阻塞整个UI线程。当整个线程被阻塞时,所有事件都不能被传递,包括绘制事件。这在用户看来,这个 应用假死了。甚至更糟糕的是,如果UI 线程被阻塞几秒(当前是5秒)以上,系统将会弹出臭名昭著的 “application not responding” (ANR) 对话框。这时用户可能选择退出你的应用甚至卸载。
另外,Android的UI 线程不是线程安全的。所以你不能在一个worker 线程操作你的UI—你必须在UI线程上对你的UI进行操作。这有两条简单的关于Android单线程模型的规则:
不要阻塞 UI 线程
不要在非UI线程里访问 Android UI toolkit
Worker 线程
由于上面对单一线程模型的描述,保证应用界面的及时响应同时UI线程不被阻塞变得很重要。如果你不能让应用里面的操作短时被执行玩,那么你应该确保把这些操作放到独立的线程里(“background” or “worker” 线程)。
比如,下面这段代码在一个额外的线程里面下载图片并在一个 ImageView显示:
new Thread(new Runnable(){
public void run(){
Bitmap b = loadImageFromNetwork("http://example.com/image.png");
mImageView.setImageBitmap(b);
}
}).start();}
起先这段代码看起来不错,因为它创建一个新的线程来处理网络操作。然而,它违反来单一线程模型的第二条规则: 不在非UI线程里访问 Android UI toolkit—这个例子在一个worker线程修改了 ImageView 。这会导致不可预期的结果,而且还难以调试。
为了修复这个问题,Android提供了几个方法从非UI线程访问Android UI toolkit 。详见下面的这个列表:
Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
那么,你可以使用 View.post(Runnable) 方法来修改之前的代码:
public void onClick(View v){
new Thread(new Runnable(){
public void run(){
final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
mImageView.post(new Runnable(){
public void run(){
mImageView.setImageBitmap(bitmap);
}
});
}
}).start();}
现在这个方案的线程安全的:这个网络操作在独立线程中完成后,UI线程便会对ImageView 进行操作。
然而,随着操作复杂性的增长,代码会变得越来越复杂,越来越难维护。为了用worker 线程处理更加复杂的交互,你可以考虑在worker线程中使用Handler ,用它来处理UI线程中的消息。也许最好的方案就是继承 AsyncTask 类,这个类简化了需要同UI进行交互的worker线程任务的执行。
使用 AsyncTask
AsyncTask 能让你在UI上进行异步操作。它在一个worker线程里进行一些阻塞操作然后把结果交给UI主线程,在这个过程中不需要你对线程或者handler进行处理。
使用它,你必须继承 AsyncTask 并实现 doInBackground() 回调方法,这个方法运行在一个后台线程池里面。如果你需要更新UI,那么你应该实现onPostExecute(),这个方法从 doInBackground() 取出结果,然后在 UI 线程里面运行,所以你可以安全的更新你的UI。你可以通过在UI线程调用 execute()方法来运行这个任务。
比如,你可以通过使用 AsyncTask来实现之前的例子:
public void onClick(View v){
new DownloadImageTask().execute("http://example.com/image.png");
}
private class DownloadImageTask extends AsyncTask<String,Void,Bitmap>{
protected Bitmap doInBackground(String... urls){
return loadImageFromNetwork(urls[0]);
}
protected void onPostExecute(Bitmap result){
mImageView.setImageBitmap(result);
}}
现在UI是安全的了,代码也更加简单了,因为AsyncTask把worker线程里做的事和UI线程里要做的事分开了。
你应该阅读一下 AsyncTask 的参考文档以便更好的使用它。下面就是一个对 AsyncTask 如何作用的快速的总览:
你可以具体设置参数的类型,进度值,任务的终值,使用的范型
doInBackground() 方法自动在 worker 线程执行
onPreExecute(), onPostExecute(), 和 onProgressUpdate() 方法都是在UI线程被调用
doInBackground() 的返回值会被送往 onPostExecute()方法
你可以随时在 doInBackground()方法里面调用 publishProgress() 方法来执行UI 线程里面的onProgressUpdate() 方法
你可以从任何线程取消这个任务
注意: 你在使用worker线程的时候可能会碰到的另一个问题就是因为runtime configuration change (比如用户改变了屏幕的方向)导致你的activity不可预期的重启,这可能会kill掉你的worker线程。为了解决这个问题你可以参考 Shelves 这个项目。
线程安全的方法
在某些情况下,你实现的方法可能会被多个线程所调用,因此你必须把它写出线程安全的。
大家先不要困在上面这篇文章中的具体代码实现上,把关注点放在Android中进程,线程和android基本组件之间的关系上。我们看完了如何在java中进行线程操作之后再去学习Android相关机制就会相对容易一些。
三 java并发编程
java并发编程是一个很庞大的话题,我不会也没有能力讲得过于深入,我没办法告诉你淘宝网是怎么处理每秒成千上万次的点击而屹立不倒,我只会讲一下为什么我们可以利用java并发编程让应用在下载文件的同时UI不会卡顿。java并发操作可以让我们把一个程序分成几部分,各自独立的去完成任务。好首先我们来定义一下这里的任务(tasks)。
1.定义tasks
一个线程承载着一个任务,如何描述它呢?java中提供Runnable这个接口,来,上代码:
public class ExampleTask implements Runnable {
private static int taskCount = 0;
private final int id = taskCount++;
protected int count = 10;
private String status(){
return "#"+id+": "+"count is "+count;
}
@Override
public void run() {
while (count -- > 0){
System.out.println(status());
Thread.yield();//the part of the Java threading mechanism that moves the CPU from one thread to the next
}
}
}
注意静态变量taskCount和final变量int,是为了该类每次被实例化时能有一个独一无二的id。
在覆写的run方法中我们通常放入一个循环,先不用理会yield方法。
然后我们在一个线程中将它实例化并调用run方法:
public class MainThread {
public static void main(String args[]){
ExampleTask exampleTask = new ExampleTask();
exampleTask.run();
}
}
结果如下:
#0: count is 9
#0: count is 8
#0: count is 7
#0: count is 6
#0: count is 5
#0: count is 4
#0: count is 3
#0: count is 2
#0: count is 1
#0: count is 0
这里并没有什么特别之处,只是被main方法调用而已(也就是存在于系统分配给main的线程中)。
2 Thread类
Thread类被实例化时,即在当前进程中创建一个新的线程,来看代码:
public class BasicThread {
public static void main(String[] args){
Thread t = new Thread(new ExampleTask());
t.start();
System.out.println("ExampleTask任务即将开始");
}
}
可以看出我们需要将ExampleTask传给Thread的构造方法,上面说过任务是对线程的描述,这里也就不难理解了。我们先看一下执行结果:
结果一
#0: count is 9
ExampleTask任务即将开始
#0: count is 8
#0: count is 7
#0: count is 6
#0: count is 5
#0: count is 4
#0: count is 3
#0: count is 2
#0: count is 1
#0: count is 0
结果二
ExampleTask任务即将开始
#0: count is 9
#0: count is 8
#0: count is 7
#0: count is 6
#0: count is 5
#0: count is 4
#0: count is 3
#0: count is 2
#0: count is 1
#0: count is 0
不用奇怪我为什么给出这两种结果(尤其是第一种),因为在多次运行试验中确确实实出现了这两种结果。我们来分析一下,当我们实例化Thread并将Task传递给它时,当前进程将在main()线程之外重新创建一个t线程,然后我们执行t.start(),这个方法会做一些必要的线程初始化的工作然后就通知t线程里的ExampleTask任务需要执行run方法了,然后start会迅速return到main()线程,所以我们不必等到ExampleTask里的run方法里面的循环执行完就可以看见
ExampleTask任务即将开始
至于为什么会发现第一种情况,我猜测是由于start返回的不够快,让t线程抢先了(对,就这么生动的理解线程你就不会怕了,虽然解释的很糟糕)
再看看下面这代码:
public class MoreBasicThread {
public static void main(String args[]){
for (int i=0;i<5;i++){
Thread t = new Thread(new ExampleTask());
t.start();
}
System.out.println("前方高能!多个线程即将开始打架!");
}
}
现在你可以回过头去看一下这段代码了:
public void onClick(View v){
new Thread(new Runnable(){
public void run(){
final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
mImageView.post(new Runnable(){
public void run(){
mImageView.setImageBitmap(bitmap);
}
});
}
}).start();}
四 AsyncTask
有了上面这些知识的铺垫,我们回到Android中。我们设想一个场景,当用户点击某个Button时,我们想从网络上加载一些文本到当前UI,前面说了我们没办法在UI线程中直接进行网络请求(因为可能会有阻塞UI线程的风险),现在我们很容易想到在当前进程中再创建一个线程,让其执行网络请求,请求完成后再来更新UI,比如上面的方案,我们还可以用安卓给我们提供的AsyncTask,使用起来更加方便,也更容易维护,操作起来:
1.准备工作
public class Loader {
public byte[] getUrlBytes(String urlSpecfic)throws IOException{
URL url = new URL(urlSpecfic);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK){
throw new IOException(connection.getResponseMessage()+"with"+urlSpecfic);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
InputStream in = connection.getInputStream();
byte[] buffer = new byte[1024];
int byteRead = 0;
while ((byteRead = in.read(buffer))>0){
out.write(buffer,0,byteRead);
}
out.close();
return out.toByteArray();
}finally {
connection.disconnect();
}
}
public String getUrlString(String urlSpecific) throws IOException{
return new String(getUrlBytes(urlSpecific));
}
}
这个类的主要作用是请求特定url的网络资源,不理解的话要么跳过,要么去找一本java书回顾一下java网络编程。
接下来是布局文件:很简单,一个TextView,一个Button
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="aaa"
android:id="@+id/url_text"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/url_button"
android:text="urlButton"/>
</LinearLayout>
2.使用AsyncTask
public class MainActivity extends AppCompatActivity {
private TextView urlText;
private Button urlButton;
private Loader loader = new Loader();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
urlText = (TextView) findViewById(R.id.url_text);
urlButton = (Button) findViewById(R.id.url_button);
urlButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new DownLoader().execute("https://segmentfault.com/");
}
});
}
private class DownLoader extends AsyncTask<String,Void,String>{
@Override
protected String doInBackground(String... params) {
try {
return loader.getUrlString(params[0]);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(String s) {
urlText.setText(s);
}
}
}
这里覆写了两个方法,doInBackground会在一个新的线程里执行,参数类型由AsyncTask的第一个泛型参数决定,返回参数由AsyncTask的第三个泛型参数决定,其返回值会传递给onPostExecute方法。而onPostExecute方法是可以操作UI线程的,故用其为urlText赋值。好,编译,运行,点击按钮,几秒钟后urlText里的内容便被请求回来的segmentfault的首页html所替换。
五 后记
设想如果我们需要请求的内容远不止一个html文件,可能是一个非常庞大的json数据或者是无穷无尽的图片资源,如果还用上面的方法,恐怕用户会在urlText前等到终老,别担心,安卓提供了非常令人头痛但是也同样非常高效的异步机制HandlerThread,Looper,Handler以及Message。别怕,别虚。下次我们一起征服。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。