本文转载自:杂谈——Android从启动到程序运行发生的事情
好久没有写博客了,瞬间感觉好多学了的东西不进行一个自我的总结与消化总归变不成自己的。通过博客可能还可以找到一些当初在学习的时候没有想到的问题。想了半天,从大二上学期自学Android以来还没有对Android从启动到程序运行期间进行一个完整的归纳,刚好最近又学到了一些新东西,那就以这篇博客为媒介,总结一下从Android启动到程序运行期间发生的所有事吧。包括什么ClassLoader, JVM,IPC, 消息处理机制要是总结到了就顺带BB一下。但是这里就不包含很多细节了,比如为什么PMS内部为什么要这么构造,好处是什么,如果我来设计的话我会怎么设计啊这种暂时就不总结了,因为我觉得以我现在的水平还有学习精力来说把这些细节都一个个的弄清楚有点没抓住重点。现阶段还是先能够了解整个流程,有个大局观才是最重要的。至于以后如果有需要或者是有精力的时候再一个个的突破。
发现本文的错误或者遗漏后会立刻更改
在正式开始之前还是忍不住想要BB一下最近参加的京东笔试,被坑得有点憋屈。憋屈啥勒,被编译器坑了。这次京东的笔试说实话感觉真的好简单,真的没有什么技术上的难点,但是尼玛编程题把我坑了。提前一个小时把代码在本地编译器上编译完成并通过,当时心里还有些小激动,一提交,在线编译器说得不到指定结果,尼玛,顿时整个人都斯巴达了。最开始的时候还以为是自己本身代码的Bug,后来顺着思路又理了几遍,完全没问题啊,又自己创了几个新的输入也都能够运行,返回正常结果。整个人都是崩溃的,在这上面花了20多分钟时候不经意间瞥了一下左边的样例输入和输出,哦豁,这下全懂了。
因为我没有很多这种参加在线笔试的经验,也没在网上怎么刷题,所以在样例输入和输出那里掺杂了一些自己想当然的想法。
题目要求的样例输入是一直输入,有两种情况,一种情况返回No,一种情况返回Yes并返回对应的结果。是要求连续输入的,也就是你在输入的时候我至少要用一个数组或者是List、Map来保存你的输入。当检测到输入为空也就是直接按了回车的同时就开始运行,然后再一次性的打印出结果。我不知道啊,第一次看这种样例输入输出,一看以为只要能返回就好了,然后就是分开做的,输入错的就返回No,输入对的就返回Yes和结果,并不能够一起输入及返回。而这个时候时间又过了好多了,改代码的话整个代码的架构都要变,时间上完全来不及。这笔试要是编程题错了那估计是没戏了。
这其实也怪自己吧,怨不得别的,只好等下次了,只是这次的题真的简单,错过了好可惜,毕竟还是非常想进京东锻炼锻炼的,就算进不了去体验京东的面试,知道哪里有不足也是好的。
上面BB了这么多,也是超过了我的预料,这里就正式开始这篇博客了。
首先,我们知道,Android是基于Linux的一个操作系统,它可以分为五层,下面是它的层次架构图,可以记一下,因为后面应该会总结到SystemServer这些Application Framework层的东西
Android的五层架构从上到下依次是应用层,应用框架层,库层,运行时层以及Linux内核层。
而在Linux中,它的启动可以归为一下几个流程:
Boot Loader-》初始化内核-》。。。。。。
当初始化内核之后,就会启动一个相当重要的祖先进程,也就是init进程,在Linux中所有的进程都是由init进程直接或间接fork出来的。
而对于Android来说,前面的流程都是一样的,而当init进程创建之后,会fork出一个Zygote进程,这个进程是所有Java进程的父进程。我们知道,Linux是基于C的,而Android是基于Java的(当然底层也是C)。所以这里就会fork出一个Zygote Java进程用来fork出其他的进程。【断点1】
总结到了这里就提一下之后会谈到的几个非常重要的对象以及一个很重要的概念。
Android系统中的客户端和服务器的概念
在Android系统中其实也存在着服务器和客户端的概念,服务器端指的就是所有App共用的系统服务,比如上面的AMS,PackageManagerService等等,这些系统服务是被所有的App共用的,当某个App想要实现某个操作的时候,就会通知这些系统服务。
当Zygote被初始化的时候,会fork出System Server进程,这个进程在整个的Android进程中是非常重要的一个,地位和Zygote等同,它是属于Application Framework层的,Android中的所有服务,例如AMS, WindowsManager, PackageManagerService等等都是由这个SystemServer fork出来的。所以它的地位可见一斑。
而当System Server进程开启的时候,就会初始化AMS,同时,会加载本地系统的服务库,创建系统上下文,创建ActivityThread及开启各种服务等等。而在这之后,就会开启系统的Launcher程序,完成系统界面的加载与显示。【断点2】
Context是一个抽象类,下面是它的注释信息,摘自源码。
/** * Interface to global information about an application environment. This is * an abstract class whose implementation is provided by * the Android system. It * allows access to application-specific resources and classes, as well as * up-calls for application-level operations such as launching activities, * broadcasting and receiving intents, etc. */ public abstract class Context {
从上面的这段话可以简单理解一下,Context是一个关于应用程序环境的全局变量接口,通过它可以允许去获得资源或者类,例如启动Activity,广播,intent等等。
我的理解:Context的具体实现是Application, Activity,Service,通过Context能够有权限去做一些事情,其实我觉得就是一个运行环境的问题。
需要注意的地方
Android开发中由于很多地方都包含了Context的使用,因此就必须要注意到内存泄露或者是一些可能会引起的问题。
例如在Toast中,它的Context就最好设置为Application Context,因为如果Toast在显示东西的时候Activity关闭了,但是由于Toast仍然持有Activity的引用,那么这个Activity就不会被回收掉,也就造成了内存泄露。
上面举例的时候举到了Toast,其实Toast也是很有意思的一个东西,它的show方法其实并不是显示一个东西这么简单。
Toast实际上是一个队列,会通过show方法把新的任务加入到队列当中去,列队中只要存在消息就会弹出来使用,而队列的长度据说默认是40个(这是网上搜出来的,我在源码中没找到对应的设置,感觉也没啥必要就没找了)。
所以这里就要注意一下show这个操作了,它并不是显示内容,而是把内容入队列。
/** * Show the view for the specified duration. */ public void show() { if (mNextView == null) { throw new RuntimeException("setView must have been called"); } INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } }
对于Handler来说,如果我们直接在AndroidStudio中创建一个非静态内部类Handler,那么Handler这一大片的区域会被AS标记为黄色,这个应该很多人都遇到过吧。实际上是因为这样设置会造成内存泄露,因为每一个非静态内部类都会持有一个外部类的引用,那么这里也就产生了一个内存泄露的可能点,如果当Activity被销毁时没有与Handler解除,那么Handler仍然会持有对该Activity的引用,那么就造成了内存泄露。
解决方案
使用static修饰Handler,这样也就成了一个静态内部类,那么就不会持有对外部类的引用了。而这个时候就可以在Handler中创建一个WeakReference(弱引用)来持有外部的对象。只要外部解除了与该引用的绑定,那么垃圾回收器就会在发现该弱引用的时候立刻回收掉它。
关于垃圾回收的相关总结看我之前的博客,传送门:JVM原理及底层探索
上面扯到了弱引用,就再BB一下四种引用方式吧。
类加载器按层次从顶层到下依次为Boorsrtap ClassLoader(启动类加载器),Extension ClassLoader(拓展类加载器),ApplicationClassLoader(应用程序类加载器)
判断两个类是否是同一个类就是看它们是否是由同一个类加载器加载而来。
这里就需要介绍一下双亲委派模式了:
双亲委派模式的意思就是:除了启动类加载器之外,其余的加载器都需要指定一个父类的加载器,当需要加载的时候会先让父类去试着加载,如果父类无法加载也就是找不到这个类的话就会让子类去加载
好处:防止内存中出现多份同样的字节码
比如类A和类B都要加载system类,如果不是委托的话,类A就会加载一份,B也会加载一份,那么就会出现两份SYstem字节码
如果使用委托机制,会递归的向父类查找,也就是首选用Bootstrap尝试加载,如果找不到再向下,如果A用这个已经加载了的话会直接返回内存中的system而不需要重新加载。那么就只会存在一份
对于Java来说,类是需要使用到时才会加载,这里也就出现了一个延迟加载的效果。而在延迟加载的时候,会默认保持同步。这也就产生了一种单例模式的方式,具体的看我之前的博客:设计模式_单例模式
我觉得在android所有的创建单例模式方法中里延迟加载方式是最好吧,虽然枚举比延迟加载更好,effiective java中也很推荐,但是并不怎么适用于Android,Android里枚举的消耗是static的两倍,延迟加载的话只要我们在使用延迟加载方式时做好反序列化的返回值readResolve()准备就好了。
上面BB了太多其他的,现在有点缓不过来,下次自己看自己博客的时候会不会都被自己的思路带得乱七八糟的。
上面的时候我们就已经完成了整个Android系统的开机以及初始化。接下来就可以B一下从点击APP图标开始到APP内部程序运行起来的流程了。
当我们点击屏幕时,触摸屏的两层电极会连接在一起,也就产生了一个电压(具体的我忘了,书上有,图找不到了),当产生电压的时候,就可以通过对应的驱动把当前按压点的XY坐标传给上层,这里也就是操作系统。操作系统在获取到XY值的时候,就会对按压点的范围进行一个判断,如果确定按压点处于一个APP图标或者是Button等等的范围中时,操作系统也就会认为用户当前已经点击了这个东西,启动对应的监听。
而当系统判断我们点击的是APP图标时,该App就由Launcher开始启动了【断点3】
Launcher是一个继承自Activity,同时实现了点击事件,长按事件等等的一个应用程序。
public final class Launcher extends Activity implements View.OnClickListener,OnLongClickListener, LauncherModel.Callbacks,View.OnTouchListener
@Override public void startActivity(Intent intent, @Nullable Bundle options) { if (options != null) { startActivityForResult(intent, -1, options); } else { // Note we want to go through this call for compatibility with // applications that may have overridden the method. startActivityForResult(intent, -1); } }
所以实际上,我对整个Android的界面是这样理解的:
当系统完成初始化以及各种服务的创建之后,就会启动Launcher这个应用程序(它也是继承自Activity的,包含自己对应的xml布局文件),然后再把各种图标按照一个正常APP布局的方式放在上面,当我们点击APP图标时,也就相当于在Launcher这个APP应用程序中通过startActivity(在底层最后会转为startActivityForResult)来启动这个APP。简单的讲,我觉得就是一个主要的APP(Launcher)里面启动了其他的功能APP,例如QQ、微信这些。【个人理解,如果以后发现不对再修改】
当我们手指按下时,Android是如何处理点击事件的呢?如何确定是让哪一个控件来处理呢?
简单一句话:层层传递-冒泡的方式处理
举个例子:现在公司来了个小项目,老板一看分配给经理做,经理一看分配给小组长,小组长一看好简单,分配给组员。如果在这个传递过程中(也就是还为分配到最底部时),某一层觉得我来负责这个比较好的话就会拦截掉这个消息,然后把它处理了,下面的就收不到有消息的这个通知。如果一直到了底层的话,组员如果能完成,就完成它。如果不能完成,那么就报告给组长,说组长我做不来,边学边做要影响进度。组长一看我也做不来,就给经理,经理一看我也不会,就给老板。这样也就一层层的传递了。
总结一下就是消息从上到下依次传递,如果在传递的过程中被拦截了就停止下传。如果没有被拦截,就一直传递到底部,如果底部不能够消耗该消息,那么就又一层层的返回来,返给上层,直到被消耗或者是到达最顶层。
在Android中,存在三个重要的方法:
第一个方法负责事件的分发,它的返回值就是表示是否消耗当前事件。
第二个方法是用于判断是否拦截该消息,如果当前View拦截了某个时间,那么在同一个事件序列中,此方法不会被再次调用。返回结果表示是否拦截当前事件
第三个方法就是处理事件。返回结果表示是否消耗当前事件,如果不小号,则在同一时间序列中,当前View无法再次接收到事件。
对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,调用它的dispath方法。如果这个ViewGroup的onIntercept方法返回true就表示它要拦截当前事件,false就表示不拦截,这个时候事件就会继续传递给子元素,接着调用子元素的dispath方法,直到被处理。
顺带总结一下滑动冲突的解决吧
View的滑动冲突一般可以分为三种:
比如说一个常见的,外部一个ListView,里面一个ScrollView。这个时候该怎么解决呢?其实这里想到了ViewPager,它里面实际上是解决了滑动冲突的,可以借鉴一下它的。
滑动处理规则
一般来说,我们可以根据用户手指滑动的方向以及角度来判断用户是要朝着哪个方向去滑动。而很多时候还可以根据项目的需求来指定一套合适的滑动方案。
外部拦截法
这种方法就是指所有的点击时间都经过父容器的拦截处理,如果父容器需要此时间就拦截,如果不需要此事件就不拦截。通过重写父容器的onInterceptTouchEvent方法:
case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: if(父类容器需要) { intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; return intercepted;
这里有一点需要注意,ACTION_DOWN事件父类容器就必须返回false,因为如果父类容器拦截了的话,后面的Move等所有事件都会直接由父类容器处理,就无法传给子元素了。UP事件也要返回false,因为它本身来说没有太多的意义,但是对于子元素就不同了,如果拦截了,那么子元素的onClick事件就无法触发。
内部拦截法
这种方法指的是父容器不拦截任何时间,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理。它需要配合requestDisallowInterceptTouchEvent方法才能正常工作。我们需要重写子元素的dispatch方法
case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true); break; MotionEvent.ACTION_MOVE: if(父容器需要此类点击事件) { parent.requestDisallowInterceptTouchEvent(false); } break; return super.dispatchTouchEvent(event);
这种方法的话父类容器需要默认拦截除了ACTION_DOWN以外的其他时间,这样当子元素调用request方法的时候父元素才能继续拦截所需的事件。
其他的
如果觉得上面两个方式太复杂,看晕了,其实也可以自己根据项目的实际需要来指定自己的策略实现。例如根据你手指按的点的位置来判断你当前触碰的是哪个控件,以此来猜测用户是否是要对这个控件进行操作。如果点击的是空白的地方,就操作外部控件即可。
【等有时间了就把ViewPager的处理总结一下,挺重要的】
Android的消息机制主要是指Handler的运行机制,Handler的运行需要底层的MessageQueue和Looper的支撑
虽然MessageQueue叫做消息队列,但是实际上它内部的存储结构是单链表的方式。由于Message只是一个消息的存储单元,它不能去处理消息,这个时候Looper就弥补了这个功能,Looper会以无限循环的形式去查找是否有新消息,如果有的话就处理消息,否则就一直等待(机制等会介绍)。而对于Looper来说,存在着另外的一个很重要的概念,就是ThreadLocal。
ThreadLocal它并不是一个线程,而是一个可以在每个线程中存储数据的数据存储类,通过它可以在指定的线程中存储数据,数据存储之后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到该线程的数据。
举个例子,多个线程通过同一个ThreadLocal获取到的东西是不一样的,就算有的时候出现的结果是一样的(偶然性,两个线程里分别存了两份相同的东西),但他们获取的本质是不同的。
那为什么有这种区别呢?为什么要这样设计呢?
先来研究一下为什么会出现这个结果。
在ThreadLocal中存在着两个很重要的方法,get和set方法,一个读取一个设置。
/** * Returns the value of this variable for the current thread. If an entry * doesn't yet exist for this variable on this thread, this method will * create an entry, populating the value with the result of * {@link #initialValue()}. * * @return the current value of the variable for the calling thread. */ @SuppressWarnings("unchecked") public T get() { // Optimized for the fast path. Thread currentThread = Thread.currentThread(); Values values = values(currentThread); if (values != null) { Object[] table = values.table; int index = hash & values.mask; if (this.reference == table[index]) { return (T) table[index + 1]; } } else { values = initializeValues(currentThread); } return (T) values.getAfterMiss(this); } /** * Sets the value of this variable for the current thread. If set to * {@code null}, the value will be set to null and the underlying entry will * still be present. * * @param value the new value of the variable for the caller thread. */ public void set(T value) { Thread currentThread = Thread.currentThread(); Values values = values(currentThread); if (values == null) { values = initializeValues(currentThread); } values.put(this, value); }
摘自源码
首先研究它的get方法吧,从注释上可以看出,get方法会返回一个当前线程的变量值,如果数组不存在就会创建一个新的。
这里有几个很重要的词,就是“当前线程”和“数组”。
这里提到的数组对于每个线程来说都是不同的,values.table,而values是通过当前线程获取到的一个Values对象,因此这个数组是每个线程唯一的,不能共用,而下面的几句话也更直接了,获取一个索引,再返回通过这个索引找到数组中对应的值。这也就解释了为什么多个线程通过同一个ThreadLocal返回的是不同的东西。
那这里为什么要这么设置呢?翻了一下书,搜了一下资料:
上面提到了Handler/Looper/Message Queue,它们实际上是一个整体,只不过我们在开发中接触更多的是Handler而已,Handler的主要作用是将一个任务切换到某个指定的线程中去执行,而Android之所以提供这个机制是因为Android规定UI只能在主线程中进程,如果在子线程中访问UI就会抛出异常。
为什么Android不允许在子线程访问UI
其实这一点不仅仅是对于Android,对于其他的所有图形界面现在都采用的是单线程模式。
因为对于一个多线程来说,如果子线程更改了UI,那么它的相关操作就必须对其他子线程可见,也就是Java并发中很重要的一个概念,线程可见性,Happen-before原则【下篇博客总结一下自己对Java并发的理解吧,挺重要的,总结完后再把传送门贴过来】而一般来说,对于这种并发访问,一般都是采用加锁的机制,但是加锁的机制存在很明显的问题:让UI访问间的逻辑变得复杂,同时效率也会降低。甚至有的时候还会造成死锁的情况,这个时候就麻烦了。
而至于究竟能不能够实现这种UI界面的多线程呢?SUN公司的某个大牛(忘了是谁,很久之前看的,好像是前副总裁)说:“行肯定是没问题,但是非常考技术,因为必须要考虑到很多种情况,这个时候就需要技术专家来设计。而这种设计出来的东西对于广大普通程序员来说又是异常头疼的,就算是实现了多线程,普通人用起来也是怨声载道的。所以建议还是单线程”。
顺带着BB一下死锁。
死锁的四个必要条件
举个常见的死锁例子:进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。
处理死锁的方法
MessageQueue主要包含两个操作:插入和读取,读取操作本身会伴随着删除操作,插入和读取对应的方法分别为enqueueMessage和next,其中enqueueMessage的作用是往消息队列中插入一条消息,而next的作用是从消息队列中取出一条消息并将其从消息队列中移除。这也就是为什么使用的是一个单链表的数据结构来维护消息列表,因为它在插入和删除上比较有优势(把下一个连接的点切换一下就完成了)。
而对于MessageQueue的插入操作来说,没什么可以看的,也就这样吧,主要需要注意的是它的读取方法next。
Message next() { // Return here if the message loop has already quit and been disposed. // This can happen if the application tries to restart a looper after quit // which is not supported. final long ptr = mPtr; if (ptr == 0) { return null; } int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; for (;;) { if (nextPollTimeoutMillis != 0) { Binder.flushPendingCommands(); } nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // Try to retrieve the next message. Return if found. final long now = SystemClock.uptimeMillis(); Message prevMsg = null; Message msg = mMessages; if (msg != null && msg.target == null) { // Stalled by a barrier. Find the next asynchronous message in the queue. do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } if (msg != null) { if (now < msg.when) { // Next message is not ready. Set a timeout to wake up when it is ready. nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); } else { // Got a message. mBlocked = false; if (prevMsg != null) { prevMsg.next = msg.next; } else { mMessages = msg.next; } msg.next = null; if (DEBUG) Log.v(TAG, "Returning message: " + msg); msg.markInUse(); return msg; } } else { // No more messages. nextPollTimeoutMillis = -1; } // Process the quit message now that all pending messages have been handled. if (mQuitting) { dispose(); return null; } // If first time idle, then get the number of idlers to run. // Idle handles only run if the queue is empty or if the first message // in the queue (possibly a barrier) is due to be handled in the future. if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { pendingIdleHandlerCount = mIdleHandlers.size(); } if (pendingIdleHandlerCount <= 0) { // No idle handlers to run. Loop and wait some more. mBlocked = true; continue; } if (mPendingIdleHandlers == null) { mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; } mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); } // Run the idle handlers. // We only ever reach this code block during the first iteration. for (int i = 0; i < pendingIdleHandlerCount; i++) { final IdleHandler idler = mPendingIdleHandlers[i]; mPendingIdleHandlers[i] = null; // release the reference to the handler boolean keep = false; try { keep = idler.queueIdle(); } catch (Throwable t) { Log.wtf(TAG, "IdleHandler threw exception", t); } if (!keep) { synchronized (this) { mIdleHandlers.remove(idler); } } } // Reset the idle handler count to 0 so we do not run them again. pendingIdleHandlerCount = 0; // While calling an idle handler, a new message could have been delivered // so go back and look again for a pending message without waiting. nextPollTimeoutMillis = 0; } }
源码有点长,总结一下就是:
next方法它是一个死循环,如果消息队列中没有消息,那么next方法就会一直阻塞在这里,当有新的消息来的时候,next方法就会返回这条信息并将其从单链表中移除。
而这个时候勒Looper就等着的,它也是一直循环循环,不停地从MessageQueue中查看是否有新消息,如果有新消息就会立刻处理,否则就会一直阻塞在那里。而对于Looper来说,它是只能创建一个的,这个要归功与它的prepare方法。
/** Initialize the current thread as a looper. * This gives you a chance to create handlers that then reference * this looper, before actually starting the loop. Be sure to call * {@link #loop()} after calling this method, and end it by calling * {@link #quit()}. */ public static void prepare() { prepare(true); } private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); }
/** * Run the message queue in this thread. Be sure to call * {@link #quit()} to end the loop. */ public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; // Make sure the identity of this thread is that of the local process, // and keep track of what that identity token actually is. Binder.clearCallingIdentity(); final long ident = Binder.clearCallingIdentity(); for (;;) { Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } // This must be in a local variable, in case a UI event sets the logger Printer logging = me.mLogging; if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } msg.target.dispatchMessage(msg); if (logging != null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } // Make sure that during the course of dispatching the // identity of the thread wasn't corrupted. final long newIdent = Binder.clearCallingIdentity(); if (ident != newIdent) { Log.wtf(TAG, "Thread identity changed from 0x" + Long.toHexString(ident) + " to 0x" + Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass().getName() + " " + msg.callback + " what=" + msg.what); } msg.recycleUnchecked(); } }
从这里面我们可以看到它也是个死循环,会不停的调用queue.next()方法来获取信息,如果没有,就return,如果有就处理。
注意
当然了,这里有一个很重要的点,一般可能会忘,那就是在子线程中如果手动为其创建了Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待状态,而如果退出Looper之后,这个线程就会立刻终止,所以建议不需要使用的时候终止Looper。
Handler
上面总结了Looper和MessageQueue,这里就对Handler进行一个总结吧。它的工作主要包含消息的发送和接受过程,消息的发送可以通过post的一系列方法以及send的一系列方法来实现,post的一系列方法最终是通过send的一系列方法来实现的。
实际上它发送消息的过程仅仅是向消息队列中插入了一条消息,MessageQueue的next方法就会返回这条消息给Looper,Looper在收到消息之后就会开始处理了。最后由Looper交给Handler处理(handleMessage()方法)。
上面总结完了Android的消息处理机制,那么就顺带总结一下IPC通信吧,毕竟上面提到过那么多次Binder和Socket。
资料:为什么Android要采用Binder作为IPC机制?
知乎上面的回答相当的好,这个博主对系统底层也是颇有钻研,学习。
这里就结合上面的知乎回答以及加上《Linux程序设计》还有一本Linux内核剖析(书名忘了但是讲得真的非常好),掺杂一些个人的理解。
进程的定义
UNIX标准把进程定义为:“一个其中运行着一个或多个进程的地址控件和这些线程所需要的系统资源”。目前,可以简单的把进程看做正在运行的程序。
进程都会被分配一个唯一的数字编号,我们成为PID(也就是进程标识符),它通常是一个取值范围从2到32768的正整数。当进程被启动时,系统将按顺序选择下一个未被使用的数字作为PID,当数字已经回绕一圈时,新的PID重新从2开始,数字1一般是为init保留的。在进程中,存在一个自己的栈空间,用于保存函数中的局部变量和控制函数的调用与返回。进程还有自己的环境空间,包含专门为这个进程建立的环境变量,同时还必须要维护自己的程序计数器,这个计数器用来记录它执行到的位置,即在执行线程中的位置。
在Linux中可以通过system函数来启动一个进程
守护进程
这里就需要提到一个守护进程了,这个在所有的底层中经常都会被提到。
在linux或者unix操作系统中在系统引导的时候会开启很多服务,这些服务就叫做守护进程。为了增加灵活性,root可以选择系统开启的模式,这些模式叫做运行级别,每一种运行级别以一定的方式配置系统。 守护进程是脱离于终端并且在后台运行的进程。守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断。
守护进程常常在系统引导装入时启动,在系统关闭时终止。如果想要某个进程不因为用户或终端或其他的变化而受到影响,那么就必须把这个进程变成一个守护进程
防止手机服务后台被杀死
是不是在手机的设置界面看当前正在运行的服务时会发现有的APP不止存在一个服务?有的APP后台存在两个,有的存在三个?有的流氓软件也会这么设置,这样的话就可以一直运行在后台,用户你关也关不了(倒不是说所有这么设置的都是流氓软件,因为有的软件需要保持一个长期的后台在线,这是由功能决定的)。
这里有两种方法(可能还有更多,这里只总结我了解的):
IPC通信
上面总结了进程的相关基础,这里就开始总结一下进程间通信(IPC
)的问题了。
现在Linux现有的所有IPC方式:
到了这里,就有了问题,为什么在Linux已经存在这么多优良的IPC方案时,Android还要采取一种新的Binder机制呢?
猜测:我觉得Android采用这种新的方式(当然也大面积的同时使用Linux的IPC通信方式),最多两个原因:
资料
对于Binder来说,存在着以下的优势:
刚才谈到Binder的时候提了一下效率的问题,那这里就不得不讲到反射了。
反射它允许一个类在运行过程中获得任意类的任意方法,这个是Java语言的一个很重要的特性。它方便了程序员的编写,但是降低了效率。
实际上,对于只要不是特别大的项目(非Android),反射对于效率的影响微乎其微,而与之对比的开发成本来说就更划算了。
但是,Android是一个用于手机的,它的硬件设施有限,我们必须要考虑到它的这个因素,用户体验是最重要的。以前看到过国外的一项统计。在一个APP中的Splash中使用了反射,结果运行时间增加了一秒,这个已经算是很严重的效率影响了。
为什么反射影响效率呢
这里就需要提到一个东西,JIT编译器。JIT编译器它可以把字节码文件转换为机器码,这个是可以直接让处理器使用的,经过它处理的字节码效率提升非常大,但是它有一个缺点,就是把字节码转换成机器码的过程很慢,有的时候甚至还超过了不转换的代码效率(转换之后存在一个复用的问题,对于转换了的机器码,使用的次数越多就越值的)。因此,在JVM虚拟机中,也就产生了一个机制,把常用的、使用频率高的字节码通过JIT编译器转换,而频率低的就不管它。而反射的话则是直接越过了JIT编译器,不管是常用的还是非常用的字节码一律没有经过JIT编译器的转化,所以效率就会低。
而在Android里面,5.0之前使用的是Davlik虚拟机,它就是上面的机制,而在Android5.0之后Google使用了一个全新的ART虚拟机全面代替Davlik虚拟机。
ART虚拟机会在程序安装时直接把所有的字节码全部转化为机器码,虽然这样会导致安装时间边长,但是程序运行的效率提升非常大。
【疑问:那在Android5.0之后的系统上,反射会不会没影响了?由于现在做项目的时候更多考虑的是向下兼容,单独考虑5.0的情况还没有,等以后有需求或者是有机会的时候再深入了解一下,以后更新】
刚才总结了Android的消息处理机制和IPC通信,那么我们主线程的消息处理机制是什么时候开始的呢?因为我们知道在主线程中我们是不需要手动调用Looper.prepare()和Looper.loop()的。
Android的主线程就是ActivityThread,主线程的入口方法是main方法,在main方法中系统会通过Looper.prepareMainLooper()来创建主线程的Looper以及MessageQueue,并通过Looper.loop来开启消息循环,所以这一步实际上是系统已经为我们做了,我们就不再需要自己来做。
ActivityThread通过AppplicationThread和AMS进行进程件通信,AMS以进程间通信的方式完成ActivityThread的请求后会回调ApplicationThread中的Binder方法,然后ApplicationThread会向Handler发送消息,Handler收到消息后会将ApplicationThread中的逻辑切换到主线程中去执行,这个过程就是主线程的消息循环模型。
上面总结到了APP开始运行,依次调用onCreate/onStart/onResume等方法,那么在onCreate方法中我们经常使用的setContentView和findViewById做了什么事呢?
首先,就考虑到第一个问题,也就是setContentView这个东西做了什么事,这里就要对你当前继承的Activity分类了,如果是继承的Activity,那么setContentView源码是这样的:
/** * Set the activity content from a layout resource. The resource will be * inflated, adding all top-level views to the activity. * * @param layoutResID Resource ID to be inflated. * * @see #setContentView(android.view.View) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */ public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); } /** * Set the activity content to an explicit view. This view is placed * directly into the activity's view hierarchy. It can itself be a complex * view hierarchy. When calling this method, the layout parameters of the * specified view are ignored. Both the width and the height of the view are * set by default to {@link ViewGroup.LayoutParams#MATCH_PARENT}. To use * your own layout parameters, invoke * {@link #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)} * instead. * * @param view The desired content to display. * * @see #setContentView(int) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */ public void setContentView(View view) { getWindow().setContentView(view); initWindowDecorActionBar(); } /** * Set the activity content to an explicit view. This view is placed * directly into the activity's view hierarchy. It can itself be a complex * view hierarchy. * * @param view The desired content to display. * @param params Layout parameters for the view. * * @see #setContentView(android.view.View) * @see #setContentView(int) */ public void setContentView(View view, ViewGroup.LayoutParams params) { getWindow().setContentView(view, params); initWindowDecorActionBar(); }
@Override public void setContentView(@LayoutRes int layoutResID) { getDelegate().setContentView(layoutResID); } @Override public void setContentView(View view) { getDelegate().setContentView(view); } @Override public void setContentView(View view, ViewGroup.LayoutParams params) { getDelegate().setContentView(view, params); }一样的3个重载函数,只是里面没有了上面的那个init方法,取而代之的是一个getDelegate().setContentView,这个delegate从字面上可以了解到它是一个委托的对象,源码是这样的:
/** * @return The {@link AppCompatDelegate} being used by this Activity. */ @NonNull public AppCompatDelegate getDelegate() { if (mDelegate == null) { mDelegate = AppCompatDelegate.create(this, this); } return mDelegate; }而在AppCompatDelegate.Create方法中,则会返回一个很有意思的东西:
/** * Create a {@link android.support.v7.app.AppCompatDelegate} to use with {@code activity}. * * @param callback An optional callback for AppCompat specific events */ public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) { return create(activity, activity.getWindow(), callback); } private static AppCompatDelegate create(Context context, Window window, AppCompatCallback callback) { final int sdk = Build.VERSION.SDK_INT; if (sdk >= 23) { return new AppCompatDelegateImplV23(context, window, callback); } else if (sdk >= 14) { return new AppCompatDelegateImplV14(context, window, callback); } else if (sdk >= 11) { return new AppCompatDelegateImplV11(context, window, callback); } else { return new AppCompatDelegateImplV7(context, window, callback); } }
这里会根据SDK的等级来返回不同的东西,这样的话就不深究了,底层的话我撇了一下,应该原理和Activity是一样的,可能存在一些区别。这里就用Activity来谈谈它的setContentView方法做了什么事。
在setContentView上面有段注释:
Set the activity content from a layout resource. The resource will be inflated, adding all top-level views to the activity.这里就介绍了它的功能,它会按照一个布局资源去设置Activity的内容,而这个布局资源将会被引入然后添加所有顶级的Views到这个Activity当中。
这里是整个Activity的层级,最外面一层是我们的Activity,它包含里面的所有东西。
再上一层是一个PhoneWindow,这个PhoneWindow是由Window类派生出来的,每一个PhoneWindow中都含有一个DecorView对象,Window是一个抽象类。
再上面一层就是一个DecorView,我理解这个DecorView就是一个ViewGroup,就是装View的。
而在DecoreView中,最上面的View就是我们的TitleActionBar,下面就是我们要设置的content。所以在上面的initWindowDecorActionBar就能猜到是什么意思了吧。
而在initWindowDecorActionBar方法中,有一段代码:
/** * Creates a new ActionBar, locates the inflated ActionBarView, * initializes the ActionBar with the view, and sets mActionBar. */ private void initWindowDecorActionBar() { Window window = getWindow(); // Initializing the window decor can change window feature flags. // Make sure that we have the correct set before performing the test below. window.getDecorView(); if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) { return; } mActionBar = new WindowDecorActionBar(this); mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp); mWindow.setDefaultIcon(mActivityInfo.getIconResource()); mWindow.setDefaultLogo(mActivityInfo.getLogoResource()); }
注意上面的window.getDecoreView()方法的注释,该方法会设置一些window的标志位,而当这个方法执行完之后,就再也不能更改了,这也就是为什么很多第三方SDK设置window的标志位时一定要求要在setContentView方法前调用。
我们通过一个findViewById方法可以实现对象的绑定,那它底层究竟是怎么实现的呢?
findViewById根据继承的Activity类型的不同也存在着区别,老规矩,还是以Activity的来。
/** * Finds a view that was identified by the id attribute from the XML that * was processed in {@link #onCreate}. * * @return The view if found or null otherwise. */ @Nullable public View findViewById(@IdRes int id) { return getWindow().findViewById(id); }从源码来看,findViewById也是经过了一层层的调用,它的功能如同它上面的注释一样,通过一个view的id属性查找view,这里也可以看到一个熟悉的getWindow方法,说明findViewById()实际上Activity把它也是交给了自己的window来做
/** * Finds a view that was identified by the id attribute from the XML that * was processed in {@link android.app.Activity#onCreate}. This will * implicitly call {@link #getDecorView} for you, with all of the * associated side-effects. * * @return The view if found or null otherwise. */ @Nullable public View findViewById(@IdRes int id) { return getDecorView().findViewById(id); }而在这里面,又调用了getDecorView的findViewById()方法,这也相当于是一个层层传递的过程,因为DecorView我理解为就是一个ViewGroup,而当运行getDecorView().findViewById()方法时,就会运行View里面的findViewById方法。它会使用这个被给予的id匹配子View的Id,如果匹配,就返回这个View,完成View的绑定
/** * Look for a child view with the given id. If this view has the given * id, return this view. * * @param id The id to search for. * @return The view that has the given id in the hierarchy or null */ @Nullable public final View findViewById(@IdRes int id) { if (id < 0) { return null; } return findViewTraversal(id); } /** * {@hide} * @param id the id of the view to be found * @return the view of the specified id, null if cannot be found */ protected View findViewTraversal(@IdRes int id) { if (id == mID) { return this; } return null; }
最后总结一下(Activity中),findViewById的过程是这样的:
Activity -> Window -> DecorView -> View