异步编程是android初学者的一个难点,却也是始终不能绕过的一个坎。能够说几乎每一个app都逃不了网络编程,而网络编程又每每创建在异步的机制之上(你不该该也没法在UI线程里执行网络请求,你也不该该在UI线程中频繁的进行IO操做)。等等,你不知道什么是线程?那就对了,咱们一块儿来回忆一下大学课本的知识,一切从进程讲起。html
我曾经在知乎上听一个朋友说一个优秀的程序员必定会有着极强的对抽象的理解能力,我很赞同这句话,我内心一直鼓励本身:当你对抽象再也不害怕的时候,可能你正在成为一名真正的coder。java
1.进程(process)
A process is the operating system’s abstraction for a running program
这是csapp中的原话,我以为两个词特别重要,一个是abstraction,说明进程是一种抽象,是人为的一种定义,另外一个是running,说明进程是正在执行的程序,而不是保存在磁盘上的一个程序文件。无论你如今怎么理解进程,你都得看下面一段代码:android
#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;
编译,执行:json
Hello World from process ID 20777 let us doing something from ID 20777
能够看到这两个函数的进程ID是同样的,其实你进一步调用getppid()函数获得父进程的函数其实也是同样的。细心的朋友就会发现,上一次执行后获得的ID是20289,此次执行获得的ID倒是20777,一样的文件为何每次执行获得的ID倒是不一样的呢?这就须要咱们好好体会进程是动态产生动态消亡的了,抽象吗?segmentfault
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并发编程让应用在下载文件的同时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();}
有了上面这些知识的铺垫,咱们回到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。别怕,别虚。下次咱们一块儿征服。