写给Android App开发人员看的Android底层知识合集(1-8)
转自包老师:http://www.cnblogs.com/Jax/p/6864103.html
写给Android App开发人员看的Android底层知识(1)
(一)引言
早在我还是Android菜鸟的时候,有很多技术我都不太明白,也都找不到答案,比如apk是怎么安装的,比如资源是怎么加载的。
再比如说,每本书都会讲AIDL,但我却从来没用过。四大组件也是这个问题,我只用过Activity,其它三个组件,不但没用过,甚至连它们是做什么的,都不是很清楚。
之所以这样,是因为我一直从事的是电商类App开发工作,对于这类App,基本就是由列表页和详情页组成的,所以我们每天面对的是Activity,会写这两类页面,把网络底层封装的足够强大就够了。
绝大多数App开发人员,都是如此。
但直到接触Android的插件化编程和热修复技术,才发现只掌握上述这些技术是远远不够的。
(二)还是引言
市场上有很多介绍Android底层的书籍,网上也有很多文章,但大都是给ROM开发人员看的,动辄贴出几页代码,不适合App开发人员去阅读学习。
我曾经在微信中问过老罗和老邓,你们写的书为什么我们App开发人员看不懂啊,他们就呵呵了,跟我说,他们的书就是写给ROM开发人员看的。
于是,这几年来,我一直在寻找这样一类知识,App开发人员看了能有助于他们更好的编写App程序,而又不需要知道太多这门技术底层的代码实现。
这类知识分为两种。
一种是知道概念即可,就比如说Zygote,其实App开发人员是不需要了解Zygote的,知道有这么个东西是“孕育天地”的就够了,类似的还有SurfaceFlinger、WMS这些概念。
还有一种是需要知道内部原理,就比如说Binder。关于Binder的介绍铺天盖地,但对于我们App开发人员,需要了解的是它的架构模型,只要有Client和Server,以及SM就足够了。
四大组件的底层通信机制都是基于Binder的,我们需要知道每个组件中,分别是哪些类扮演了Binder Client,哪些类扮演了Binder Server。知道这些概念,有助于我们App开发人员进行插件化编程。
(三)目录
我这个系列的文章,已经写好了下面的内容,会在接下来的每天发布一篇,共计8篇,看了这8篇文章,就可以迈进Android插件化的大门了。
Binder
AIDL
AMS
Activity
Service
ContentProvider
匿名共享内存
BroadcastReceiver
PMS及App安装过程
Android底层知识,还应该包括以下内容,但是和插件化关系不大,也不是我擅长的领域,所以我只列出了大纲,没有继续写下去:
View和ViewGroup
Message、Looper和Handler
权限管理
Android SDK工具内部原理
有兴趣的同学,可以按照我这个思路继续写下去,记得,一,少贴代码。多画图,二,一定要有趣。
接下来就详细讲那些App开发人员需要知道的Android底层知识。
(三)Binder
Binder是为了解决跨进程通信。
关于Binder的文章实在是太多了,每篇文章都能从Java层讲到C++层,App开发人员其实是没必要了解这么多内容的。我们看对App开发有用的几个点:
1)首先,Binder分为Client和Server两个进程。
注意,Client和Server是相对的。谁发消息,谁就是Client,谁接收消息,谁就是Server。
举个例子,两个进程A和B之间使用Binder通信,进程A发消息给进程B,那么这时候A是Binder Client,B是Binder Server;进程B发消息给进程A,那么这时候B是Binder Client,A是Binder Server——其实这么说虽然简单了,但还是不太严谨,我们先这么理解着。
2)其次,我们看下面这个图(摘自田维术的博客),基本说明白了Binder的组成解构:
图中的IPC就是进程间通信的意思。
图中的ServiceManager,负责把Binder Server注册到一个容器中。
有人把ServiceManager比喻成电话局,存储着每个住宅的座机电话,还是很恰当的。张三给李四打电话,拨打电话号码,会先转接到电话局,电话局的接线员查到这个电话号码的地址,因为李四的电话号码之前在电话局注册过,所以就能拨通;没注册,就会提示该号码不存在。
对照着Android Binder机制,对着上面这图,张三就是Binder Client,李四就是Binder Server,电话局就是ServiceManager,电话局的接线员在这个过程中做了很多事情,对应着图中的Binder驱动
3)接下来我们看Binder通信的过程,还是摘自田维术博客的一张图:
注:图中的SM也就是ServiceManager。
我们看到,Client想要直接调用Server的add方法,是不可以的,因为它们在不同的进程中,这时候就需要Binder来帮忙了。
首先是Server在SM这个容器中注册。
其次,Client想要调用Server的add方法,就需要先获取Server对象, 但是SM不会把真正的Server对象返回给Client,而是把Server的一个代理对象返回给Client,也就是Proxy。
然后,Client调用Proxy的add方法,SM会帮他去调用Server的add方法,并把结果返回给Client。
以上这3步,Binder驱动出了很多力,但我们不需要知道Binder驱动的底层实现,涉及到C++的代码了——把有限的时间去做更有意义的事情。
App开发人员对Binder的掌握,这些内容就足够了。
是时候总结一波了:
学习Binder,是为了更好的理解AIDL,基于AIDL模型,进而了解四大组件的原理。
理解了Binder,再看AMS和四大组件的关系,就像是Binder的两个进程Server和Client通信。
(四)AIDL
AIDL是Binder的延伸。一定要先看懂我前面介绍的Binder,再来看AIDL。要按顺序阅读。
Android系统中很多系统服务都是aidl,比如说剪切板。举这个例子,是为了让App开发人员知道AIDL无处不在,和我们距离非常近。
AIDL中需要知道下面几个类:
IBinder
IInterface
Binder
Proxy
Stub
当我们自定义一个aidl文件时(比如MyAidl.aidl,里面有一个sum方法),Android Studio会帮我们生成一个类文件MyAidl.java,如下图所示:
MyAidl.java这个生成文件中,包括MyAidl接口,以及Stub和Proxy两个实现了MyAidl接口的类,其中Stub是定义在MyAidl接口中的,而Proxy则定义在Stub类中。
我曾经很不理解,为什么不是生成3个文件,一个接口,两个类,清晰明了。都放在一个文件中,这是导致很多人看不懂AIDL的一个门槛。其实Android这么设计是有道理的。当有多个AIDL类的时候,Stub和Proxy类就会重名,把它们放在各自的AIDL接口中,就必须MyAidl.Stub这样去使用,就区分开了。
对照这张图,我们继续来分析,Stub的sum方法是怎么调用到Proxy的sum方法?然后又调用另一个进程的sum方法的?
起决定意义的是Stub的asInterface方法和onTransact方法。其实这个图没有画全,把完整的Binder Server也画上,就应该是这样:
1)先从Client看起,对于AIDL的使用者,我们这么写程序:
MyAidl.Stub.asInterface(某IBinder对象).sum(1, 2); //最好在执行sum方法前判空。
asInterface方法的作用是判断参数——也就是IBinder对象,和自己是否在同一个进程:
是,则直接转换、直接使用,接下来就跟Binder跨进程通信无关啦;
否,则把这个IBinder参数包装成一个Proxy对象,这时调用Stub的sum方法,间接调用Proxy的sum方法。
return new MyAidl.Stub.Proxy(obj);
2)Proxy在自己的sum方法中,会使用Parcelable来准备数据,把函数名称、函数参数都写入_data,让_reply接收函数返回值。最后使用IBinder的transact方法,把数据就传给Binder的Server端了。
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0); //这里的mRemote就是asInterface方法传过来的obj参数
———————我是Binder分界线—————-
3)Server则是通过onTransact方法接收Client进程传过来的数据,包括函数名称、函数参数,找到对应的函数,这里是sum,把参数喂进去,得到结果,返回。
所以onTransact函数经历了读数据-->执行要调用的函数-->把执行结果再写数据的过程。
下一篇文章要介绍的四大组件的原理,我们都可以对照着AIDL的这张图来看,比如说,四大组件的启动和后续流程,都是在和ActivityManagerService(简称AMS)来来回回的通信,四大组件给AMS发消息,四大组件就是Binder Client,而AMS就是Binder Server;AMS发消息通知四大组件,那么角色就互换。
那么四大组件中,比如说Activity,又是哪个类扮演了Stub的角色,哪个类扮演了Proxy的角色呢?这也是我下一篇文章要介绍的,包括AMS、四大组件各自的运行原理。
好戏即将开始。
写给Android App开发人员看的Android底层知识(2)
(五)AMS
如果站在四大组件的角度来看,AMS就是Binder中的Server。
AMS全称是ActivityManagerService,看字面意思是管理Activity的,但其实四大组件都归它管。估计是Android底层开发人员先写了ActivityManagerService用来管理Activity,后来写Service、Receiver、CP的时候发现代码都差不多,于是就全都用ActivityManagerService,但是却忘记改名字了——我也是猜的,纯属八卦。
由此而说到了插件化,我记得16年和Lody、张勇、林光亮一起吃夜宵的时候,我当时问了困惑已久的两个问题:
1)App的安装过程,为什么不把apk解压缩到本地,这样读取图片就不用每次从apk包中读取了——这个问题,我们放到PMS那一节再详细说。
2)为什么Hook永远是在Binder Client端,也就是四大组件这边,而不是在AMS那一侧进行Hook。
这里要说清楚第二个问题。就拿Android剪切板举例吧。前面说过,这也是个Binder服务。
AMS要负责和所有App的四大组件进行通信,也真够他忙的。如果在一个App中,在AMS层面把剪切板功能给篡改了,那会导致Android系统所有的剪切板功能被篡改——这就是病毒了,如果是这样的话,Android系统早就死翘翘了。所以Android系统不允许我们这么做。
我们只能在AMS的另一侧,Client端,也就是四大组件这边做篡改,这样即使我们把剪切板功能篡改了,也只影响篡改代码所在的App,在别的App中,剪切板功能还是正常的。
关于AMS我们就说这么多,下面介绍四大组件时,会反复提到四大组件和AMS的跨进程通信。
(六)Activity 第1讲
对于做App的开发人员而言,Activity是四大组件中用的最多的,也是最复杂的,我这里只讲Activity的启动和通信原理。还有一些相关的概念,比如说View、Looper、Intent、Resource,我以后另起章节来介绍。
注:我对四大组件的分析,都是基于罗升阳的那本分析Android底层的书,我把其中罗列的大部分代码都删掉了,只保留那些对App开发人员有用的一些代码片段和一些关键类名,并融入了我对四大组件的理解。
1)首先要搞清,App是怎么启动的。
在手机屏幕上点击某个App的Icon,假设就是斗鱼App吧,这个App的首页(或引导页)就出现在我们面前了。这个看似简单的操作,背后经历了Activity和AMS的反反复复的通信过程。
首先要搞清楚,在手机屏幕上点击App的icon快捷图标,此时手机屏幕就是一个Activity,而这个Activity所在的App,业界称之为Launcher。Launcher是手机系统厂商提供的,类似小米华为这样的手机,比拼的就是谁的Launcher绚丽和人性化。
Launcher这个App,其实和我们做的各类应用类App没有什么不同,我们大家用过华为、小米之类的手机,预装App以及我们下载的各种App,都显示在Launcher上,每个App表现为一个Icon。Icon多了可以分页,可以分组,此外,Launcher也会发起网络请求,调用天气的数据,显示在屏幕上,所谓的人性化界面。
还记得我们在开发一款App时,在Manifest文件中是怎么定义默认启动Activity的么?如下所示:
而Launcher中为每个App的icon提供了启动这个App所需要的Intent信息,如下所示(比如说斗鱼的包名是):
action:android.intent.action.MAIN
category: android.intent.category.LAUNCHER
cmp: 斗鱼的包名+ 首页Activity名
这些信息是App安装(或Android系统启动)的时候,PackageManagerService从斗鱼的apk包的manifest文件中读取到的。
所以点击icon就启动了斗鱼App中的首页。
2)启动App哪有那么简单
前面介绍的只是App启动的一个最简单的概述。
仔细看,我们会发现,Launcher和斗鱼是两个不同的App,他们位于不同的进程中,它们之间的通信是通过Binder完成的——这时候AMS出场了。
仍然以启动斗鱼App为例子,整体流程是:
Launcher通知AMS,要启动斗鱼App,而且指定要启动斗鱼的哪个页面(也就是首页)。
AMS通知Launcher,好了我知道了,没你什么事了,同时,把要启动的首页记下来。
Launcher当前页面进入Paused状态,然后通知AMS,我睡了,你可以去找斗鱼App了。
AMS检查斗鱼App是否已经启动了。是,则唤起斗鱼App即可。否,就要启动一个新的进程。AMS在新进程中创建一个ActivityThread对象,启动其中的main函数。
斗鱼App启动后,通知AMS,说我启动好了。
AMS翻出之前在第二步存的值,告诉斗鱼App,启动哪个页面。
斗鱼App启动首页,创建Context并与首页Activity关联。然后调用首页Activity的onCreate函数。
至此启动流程完成,分成两部分,1-3步,Launcher和AMS相互通信,而后面几步,斗鱼App和AMS相互通信。
这会牵扯一堆类进来,列举如下,在接下来的分析中,我们都会遇到:
Instrumentation
ActivityThread
H
LoadedApk
AMS
ActivityManagerNative和ActivityManagerProxy
ApplicationThread和ApplicationThreadProxy
第1阶段 Launcher通知AMS
这是我根据老罗那本书中对Activity的分析,自己手绘的UML图,一共七张,也就是Activity启动所经历的七个阶段。建议各位读者也亲自手绘一遍,从而就加深理解。
第1步、第2步
从上图中我们看到,点击Launcher上的斗鱼App的icon快捷图标,这时会调用Launcher的startActivitySafely方法,其实还是会调用Activity的startActivity方法,intent中带着要启动斗鱼App所需要的关键信息,如下所示:
action = “android.intent.action.MAIN”
category = “android.intent.category.LAUNCHER”
cmp = “com.douyu.activity.MainActivity”
第3行代码是我猜的,就是斗鱼App在Mainfest文件中指定为首页的那个Activity。这样,我们终于明白,为什么在Mainfest中,给首页指定action和category了。在app的安装过程中,会把这个信息“记录”在Launcher的斗鱼启动快捷图标中。关于App的安装过程,我会在后面的文章详细介绍。
startActivity这个方法,如果我们看它的实现,会发现它调来调去,经过一系列startActivity的重载方法,最后会走到startActivityForResult方法。
我们知道startActivityForResult需要两个参数,一个是intent,另一个是code,这里code是-1,表示Launcher才不关心斗鱼的App是否启动成功了呢。
第3步: startActivityForResult
Activity内部会保持一个对Instrumentation的引用,但凡是做过App单元测试的同学,对这个类都很熟悉,称之为仪表盘。
在startActivityForResult方法的实现中,会调用Instrumentation的execStartActivity方法。
看到这里,我们发现有个mMainThread变量,这是一个ActivityThread类型的变量。
这个家伙的来头可不小。
ActivityThread,就是主线程,也就是UI线程,它是在App启动时创建的,它代表了App应用程序。
啥?ActivityThread代表了App应用程序,那Application类岂不是被架空了?其实,Application对我们App开发人员来说也许很重要,但是在Android系统中还真的没那么重要,他就是个上下文。Activity不是有个Context上下文吗?Application就是整个ActivityThread的上下文。
ActivityThread则没有那么简单了。它里面有main函数。
我们知道大部分程序都有main函数,比如java、C#,远了不说,iPhone App用到的Objective-C,也有main函数,那么Android的main函数藏在哪里?就在ActivityThread中,如下所示,代码太多,我只截取了一部分
又有人会问?不是说谁写的程序,谁就要提供main函数,作为入口吗?但Android App却不是这样的。Android App的main函数,在ActivityThread里面,而这个类是Android系统提供的底层类,不是我们提供的。
所以这就是Andoid有趣的地方。Android App的入口是Mainifest中定义默认启动Activity。这是由Android AMS与四大组件的通信机制决定的。
最近在看吴越版的西游记,就发现这个西天取经为啥用了十几年啊?因为这师徒四个取经路上爱管闲事,所以耽搁了很久。我这篇文章也是如此,经常讲着讲着就跑题了,再这么写下去,不知道要写到啥时候,所以我们一路向西,径直往前走,再遇到奇怪的类,先不要理它。
在回到代码来,这里要传递2个很重要的参数:
通过ActivityThread的getApplicationThread方法取到一个Binder对象,它的类型为ApplicationThread,它代表着Launcher所在的App进程。
mToken,这也是个Binder对象,它代表了Launcher这个Activity,这里也通过Instrumentation传给AMS,AMS一查电话簿,就知道是谁向AMS发起请求了。
这两个参数是伏笔,传递给AMS,以后AMS想反过来通知Launcher,就能通过这两个参数,找到Launcher。
第4步,Instrumentation的execStartActivity方法
Instrumentation绝对是Adnroid测试团队的最爱,因为它可以帮我们启动Activity。
回到我们的App启动过程来,在Instrumentation的execStartActivity方法中,
我理解这就是一个透传,Activity把数据借助Instrumentation,传递给ActivityManagerNative,没太多有趣的内容,就不多讲了。
第5步:AMN的getDefault方法
ActivityManagerNative,简称AMN。这个类后面会反复用到。
AMN通过getDefault方法,从ServiceManager中取得一个名为activity的对象,然后把它包装成一个ActivityManagerProxy对象(简称AMP),AMP就是AMS的代理对象。
备注1:ServiceManager是一个容器类。
备注2: AMN的getDefault方法返回类型为IActivityManager,而不是AMP。IActivityManager是一个实现了IInterface的接口,里面定义了四大组件所有的生命周期。
AMN和AMP都实现了IActivityManager接口,AMS继承自AMN(好乱),那么对照着前面AIDL的UML,就不难理解了:
第6步,AMP的startActivity方法
看到这里,你会发现AMP的startActivity方法,和AIDL的Proxy方法,是一模一样的,写入数据到另一个进程,也就是AMS,然后等待AMS返回结果。
至此,第一阶段的工作就做完了。
后续流程请参加下一篇文章。
写给Android App开发人员看的Android底层知识(3)
(七)App启动流程第2篇
书接上文,App启动一共有七个阶段,上篇文章篇幅所限,我们只看了第一阶段,接下来讲剩余的六个阶段,仍然是拿斗鱼App举例子。
简单回顾一下第一阶段的流程,就是Launcher向AMS发送一个跨进程通信,通过AMN/AMP,告诉AMS,我要启动斗鱼App。
画一个图,描述一下启动App所经历的7个阶段:
第2阶段 AMS处理Launcher传过来的信息
这个阶段主要是Binder的Server端在做事情。因为我们是没有机会修改Binder的Server端逻辑的,所以这个阶段看起来非常枯燥,我尽量说的简单些。
首先Binder,也就是AMN/AMP,和AMS通信,肯定每次是做不同的事情,就比如说这次Launcher要启动斗鱼App,那么会发送类型为START_ACTIVITY——TRANSACTION的请求给AMS,同时会告诉AMS要启动哪个Activity。
AMS说,好,我知道了,然后它会干一件很有趣的事情,就是检查斗鱼App中的Manifest文件,是否存在要启动的Activity。如果不存在,就抛出Activity not found的错误,各位做App的同学对这个异常应该再熟悉不过了,经常写了个Activity而忘记在Manifest中声明了,就报这个错,就是因为AMS在这里做检查。不管是新启动一个App的首页,还是在App内部跳转到另一个Activity,都会做这个检查。
但是Launcher还活着啊,所以接下来AMS会通知Launcher,哥们儿没你什么事了,你“停薪留职”吧。那么AMS是通过什么途径告诉Launcher的呢?
前面讲过,Binder的双方进行通信是平等的,谁发消息,谁就是Client,接收的一方就是Server。Client这边会调用Server的代理对象。
对于从Launcher发来的消息,通过AMS的代理对象AMP,发送给AMS。
那么当AMS想给Launcher发消息,又该怎么办呢?前面不是把Launcher以及它所在的进程给传过来了吗?它在AMS这边保存为一个ActivityRecord对象,这个对象里面有一个ApplicationThreadProxy,单单从名字看就出卖了它,这就是一个Binder代理对象。它的Binder真身,也就是ApplicationThread。
站在AIDL的角度,来画这张图,是这样的:
所以结论是,AMS通过ApplicationThreadProxy发送消息,而App端则是通过ApplicationThread来接收这个消息。
第3阶段 Launcher去休眠,然后通知AMS,我真的已经“停薪留职”了,没有吃空饷
ApplicationThread(简称APT),它和ApplicationThreadProxy(简称ATP)的关系,我们在第三阶段已经介绍过了。
APT接收到来自AMS的消息后,就调用ActivityThread的sendMessage方法,向Launcher的主线程消息队列发送一个PAUSE_ACTIVITY消息。
前面说过,ActivityThread就是主线程(UI线程)
看到下面的代码是不是很亲切?
发送消息是通过一个名为H的Handler类的完成的,这个H类的名字真特么有个性,想记不住都难。
做App的同学都知道,继承自Handler类的子类,就要实现handleMessage方法,这里是一个switch…case语句,处理各种各样的消息,PAUSE_ACTIVITY消息只是其中一种,由此也能预见到,AMS给Activity发送的所有消息,以及给其它三大组件发送的所有消息,都从H这里经过,为什么要强调这一点呢,既然四大组件都走这条路,那么这里就可以做点手脚,从而做插件化技术,这个我们以后介绍插件化技术的时候会讲到。
H对于PAUSE_ACTIVITY消息的处理,如上面的代码,是调用ActivityThread的handlePauseActivity方法。这个方法干两件事:
ActivityThread里面有一个mActivities集合,保存当前App也就是Launcher中所有打开的Activity,把它找出来,让它休眠。
通过AMP通知AMS,我真的休眠了。
你可能会找不到H和APT这两个类文件,那是因为它们都是ActivityThread的内嵌类。
至此,Launcher的工作完成了。你可以看到在这个过程中,各个类都起到了什么作用。芸芸众生,粉墨登场:
APT
ActivityThread
H
第4阶段 AMS启动新的进程
接下来又轮到AMS做事了,你们会发现我不太喜欢讲解AMS的流程,甚至都不画UML图,因为这部分逻辑和App开发人员关系不是很大,我尽量说的简单一些,把流程说清楚就好。
AMS接下来要启动斗鱼App的首页,因为斗鱼App不在后台进程中,所以要启动一个新的进程。这里调用的是Process.start方法,并且指定了ActivityThread的main函数为入口函数。
第5阶段 新的进程启动,以ActivityThread的main函数作为入口
启动新进程,其实就是启动一个新的App。
在启动新进程的时候,为这个进程创建ActivityThread对象,这就是我们耳熟能详的主线程(UI线程)。
创建好UI线程后,立刻进入ActivityThread的main函数,接下来要做2件具有重大意义的事情:
1)创建一个主线程Looper,也就是MainLooper。看见没,MainLooper就是在这里创建的。
2)创建Application。记住,Application是在这里生成的。
- - - - - - - - - - - - - -华丽的分割线 开始- - - - - - - - - - - - - - -
App开发人员对Application非常熟悉,因为我们可以在其中写代码,进行一些全局的控制,所以我们通常认为Application是掌控全局的,其实Application的地位在App中并没有那么重要,它就是一个Context上下文,仅此而已。
App中的灵魂是ActivityThread,也就是主线程,只是这个类对于App开发人员是访问不到的——使用反射倒是可以修改这个类的一些行为。
- - - - - - - - - - - - - - -华丽的分割线 结束- - - - - - - - - - - - - - -
创建新App的最后,就是告诉AMS,我启动好了,同时把自己的ActivityThread对象发送给AMS,从此以后,AMS的电话簿中就多了这个新的App的登记信息,AMS以后向这个App发送消息,就通过这个ActivityThread对象。
第6阶段 AMS告诉新App启动哪个Activity
AMS把传入的ActivityThread对象,转为一个ApplicationThread对象,用于以后和这个App跨进程通信。还记得APT和ATP的关系吗?这就又回到第2阶段的那张关系图了。
还记得第1阶段,Launcher发送给AMS要启动斗鱼App的哪个Activity吗?这个信息被AMS存下来了。
那么在第6阶段,AMS从过去的记录中翻出来要启动哪个Activity,然后通过ATP告诉App。
第7阶段 启动斗鱼首页Activity
毕其功于一役,尽在第7阶段。这是最后一步。
App,这个Binder的另一端,通过APT接收到AMS的消息,仍然是在H的handleMessage方法的switch语句中处理,只不过,这次消息的类型是LAUNCH_ACTIVITY:
ActivityClientRecord是什么?这是AMS传递过来的要启动的Activity。
还是这里,我们仔细看那个getPackageInfoNoCheck方法,这个方法会提取Apk中的所有资源,然后设置给r的packageInfo属性。这个属性的类型很有名,叫做LoadedApk。各位记住这里,这个地方可以干坏事,也是插件化技术渗入的一个点。
在H的这个分支中,又反过来回调ActivityThread的handleLaunchActivity方法,你要是觉得很绕那就对了。其实我一直觉得,ActivityThread和H合并成一个类也没问题。
重新看一下这个过程,每次都是APT执行ActivityThread的sendMessage方法,在这个方法中,把消息拼装一下,然后扔个H的swicth语句去分析,来决定要执行ActivityThread的那个方法。每次都是这样,习惯就好了。
handleLaunchActivity方法都做哪些事呢?
1)通过Instrumentation的newActivity方法,创建出来要启动的Activity实例。
2)为这个Activity创建一个上下文Context对象,并与Activity进行关联。
3)通过Instrumentation的callActivityOnCreate方法,执行Activity的onCreate方法,从而启动Activity。看到这里是不是很熟悉很亲切?
至此,App启动完毕。这个流程是经过了很多次握手, App和ASM,频繁的向对方发送消息,而发送消息的机制,是建立在Binder的基础之上的。
下一篇文章,我们讲Context家族。
写给Android App开发人员看的Android底层知识(4)
(八)App内部的页面跳转
在介绍完App的启动流程后,我们发现,其实就是启动一个App的首页。
接下来我们看App内部页面的跳转。
从ActivityA跳转到ActivityB,其实可以把ActivityA看作是Launcher,那么这个跳转过程,和App的启动过程就很像了。
有了前面的分析基础,会发现,这个过程不需要重新启动一个新的进程,所以可以省略App启动过程中的一些步骤,流程简化为:
1)ActivityA向AMS发送一个启动ActivityB的消息。
2)AMS保存ActivityB的信息,然后通知App,你可以休眠了(onPaused)。
3)ActivityA进入休眠,然后通知AMS,我休眠了。
4)AMS发现ActivityB所在的进程就是ActivityA所在的进程,所以不需要重新启动新的进程,所以它就会通知App,启动ActivityB。
5)App启动ActivityB。
不想看上述文字的,看我画的这个图:
整体流程我就不多说了,和上一篇文章介绍的App启动流程是基本一致的。
以上的分析,仅限于ActivityA和ActivityB在相同的进程中,如果在Manifest中指定这两个Activity不在同一个进程中,那么就又是另一套流程了,但是整体流程大同小异。
(九)Context家族史
Activity和Service都有Context,这三个类,还有Application,其实是亲戚一家子。
Activity因为有了一层Theme,所以中间有个ContextThemeWrapper,相当于它是Service和Application的侄子。
ContextWrapper只是一个包装类,没有任何具体的实现,真正的逻辑都在ContextImpl里面。
一个应用包含的Context个数:Service个数+Activity个数+1(Application类本身对应一个Context对象)。
应用程序中包含多个ContextImpl对象,而其内部变量mPackageInfo指向同一个PackageInfo对象。
- - - - - - - - - - - - - 华丽的分割线,以下是例子- - - - - - - - - - - - - - - - - - - -
我们就拿Activity举例子,看看Activity和Context的联系和区别。
我们知道,跳转到一个新的Activity要这么写:
我们还知道,也可以在Activity中使用getApplicationContext方法获取Context上下文信息,然后使用Context 的startActivity方法,启动一个新的Activity:
这二者的区别是什么?我们画个图,就看明白了:
因为Context的startActivity方法,我看了在ContextImpl中的源码实现,仍然是从ActivityThread中取出Instrumentation,然后执行execStartActivity方法,这和使用Activity的startActivity方法的流程是一样的。
还记得我们前面分析的App启动流程么?在第五阶段,创建App进程的时候,先创建的ActivityThread,再创建的Application。Application的生命周期是跟着整个App走的。
而getApplicationContext得到的Context,就是从ActivityThread中取出来的Application对象,所以这个Context上下文,使用时要当心,容易引起内存泄露。
下一篇文章,我们就要迈进Service的世界了。
写给Android App开发人员看的Android底层知识(5)
(十)Service
Service有两套流程,一套是启动流程,另一套是绑定流程。我们做App开发的同学都应该知道。
1)在新进程启动Service
我们先看Service启动过程,假设要启动的Service是在一个新的进程中,分为5个阶段:
1)App向AMS发送一个启动Service的消息。
2)AMS检查启动Service的进程是否存在,如果不存在,先把Service信息存下来,然后创建一个新的进程。
3)新进程启动后,通知AMS说我可以啦。
4)AMS把刚才保存的Service信息发送给新进程
5)新进程启动Service
我们仔细看一下这5个阶段:
第1阶段
和Activity非常像,仍然是通过AMM/AMP把要启动的Service信息发送给AMS。
第2阶段
AMS检查Service是否在Manifest中声明了,没声明会直接报错。
AMS检查启动Service的进程是否存在,如果不存在,先把Service信息存下来,然后创建一个新的进程。
在AMS中,每个Service,都使用ServiceRecord对象来保存。
第3阶段
Service所在的新进程启动的过程,就和前面介绍App启动时的过程差不多。
新进程启动后,也会创建新的ActivityThread,然后把ActivityThread对象通过AMP传递给AMS,告诉AMS,新进程启动成功了。
第4阶段
AMS把传进来的ActivityThread对象改造为ApplicationThreadProxy,也就是ATP,通过ATP,把要启动的Service信息发送给新进程。
第5阶段
新进程通过ApplicationThread接收到AMS的信息,和前面介绍的启动Activity的最后一步相同,借助于ActivityThread和H,执行Service的onCreate方法。在此期间,为Service创建了Context上下文对象,并与Service相关联。
需要重点关注的是ActivityThread的handleCreateService方法,
你会发现,这段代码和前面介绍的handleLaunchActivity差不多,都是从PMS中取出包的信息packageInfo,这是一个LoadedApk对象,然后获取它的classloader,反射出来一个类的对象,在这里反射的是Service。
四大组件的逻辑都是如此,所以我们要做插件化,可以在这里做文章,换成插件的classloader,加载插件中的四大组件。
至此,我们在一个新的进程中启动了一个Service。
2)启动统一进程的Service
如果是在当前进程启动这个Service,那么上面的步骤就简化为:
1)App向AMS发送一个启动Service的消息。
2)AMS例行检查,比如Service是否声明了,把Service在AMS这边注册。AMS发现要启动的Service就是App所在的Service,就通知App启动这个Service。
3)App启动Service。
我们看到,没有了启动新进程的过程。
3)在同一进程绑定Service
如果是在当前进程绑定这个Service呢?过程是这样的:
1)App向AMS发送一个绑定Service的消息。
2)AMS例行检查,比如Service是否声明了,把Service在AMS这边注册。AMS发现要启动的Service就是App所在的Service,就先通知App启动这个Service,然后再通知App,对Service进行绑定操作。
3)App收到AMS第1个消息,启动Service,
4)App收到AMS第2个消息,绑定Service,并把一个Binder对象传给AMS
5)AMS把接收到的Binder对象,发送给App
6)App收到Binder对象,就可以使用了。
你也许会问,都在一个进程,App内部直接使用Binder对象不就好了,其实吧,要考虑不在一个进程的场景,代码又不能写两份,两套逻辑,所以就都放在一起了,即使在同一个进程,也要绕着AMS走一圈。
第1阶段:App向AMS发送一个绑定Service的消息。
第4阶段:处理第2个消息
第5阶段和第6阶段:
这一步是要仔细说的,因为AMS把Binder对象传给App,这里没用ATP和APT,而是用到了AIDL来实现,这个AIDL的名字是IServiceConnection。
ServiceDispatcher的connect方法,最终会调用ServiceConneciont的onServiceConnected方法,这个方法我们就很熟悉了。App开发人员在这个方法中拿到connection,就可以做自己的事情了。
好了,关于Service的底层知识,我们就全都介绍完了。当你再去编写一个Service时,是否感觉对这个组件理解的更透彻了呢?
下一篇我们聊一聊BroadcastReceiver。
写给Android App开发人员看的Android底层知识(6)
(十一)BroadcastReceiver
BroadcastReceiver,也就是广播,简称Receiver。
很多App开发人员表示,从来没用过Receiver。其实吧,对于音乐播放类App,用Service和Receiver还是蛮多的,如果你用过QQ音乐,App退到后台,音乐照样播放不会停止,这就是你写的Service在后台起作用。
在前台的Activity,点击停止按钮,就会给后台Service发送一个Receiver,通知它停止播放音乐;点击播放按钮,仍然是发送这个Receiver,只是携带的值变了,所以Service收到请求后播放音乐。
反过来,后台Service播放每播放完一首音乐,接下来准备播放下一首音乐的时候,就会给前台Activity发Receiver,让Activity显示下一首音乐的名称。
所以音乐播放器的原理,就是一个前后台Activity和Service互相发送和接收Receiver的过程。
Receiver分静态广播和动态广播两种。
在Manifest中声明的Receiver,是静态广播:
在程序中手动写注册代码的,是动态广播:
二者具有相同的功能,只是写法不同。既然如此,我们就可以把所有静态广播,都改为动态广播,这就省的在Manifest文件中声明了,避免了AMS检查。接下来你想到什么?对,Receiver的插件化解决方案,就是这个思路。
接下来我们看Receiver是怎么和AMS打交道的,分为两部分,一是注册,二是发送广播。
你只有注册了这个广播,发送这个广播时,才能通知到你,执行onReceive方法。
我们就拿音乐播放器来举例,在Activity注册Receiver,在Service发送广播。Service播放下一首音乐时,会通知Activity修改当前正在播放的音乐名称。
注册过程如下所示:
1)在Activity中,注册Receiver,并通知AMS。
这里Activity使用了Context提供的registerReceiver方法,然后通过AMN/AMP,把一个receiver传给AMS。
在创建这个Receiver对象的时候,需要为receiver指定IntentFilter,这个filter就是Receiver的身份证,用来描述receiver。
在Context的registerReceiver方法中,它会使用PMS获取到包的信息,也就是LoadedApk对象。
就是这个LoadedApk对象,它的getReceiverDispatcher方法,将Receiver封装成一个实现了IIntentReceiver接口的Binder对象。
我们就是将这个Binder对象和filter传递给AMS。
只传递Receiver给AMS是不够的,当发送广播时,AMS不知道该发给谁啊?所以Activity所在的进程还要把自身对象也发送给AMS。
2)AMS收到消息后,就会把上面这些信息,存在一个列表中,这个列表中保存了所有的Receiver。
注意了,这里忙活半天,都是在注册动态receiver。
静态receiver什么时候注册到AMS的呢?是在App安装的时候。PMS会解析Manifest中的四大组件信息,把其中的receiver存起来。
动态receiver和静态receiver分别存在AMS不同的变量中,在发送广播的时候,会把两种receiver合并到一起,然后以此发送。其中动态的排在静态的前面,所以动态receiver永远优先于静态receiver收到消息。
此外,Android系统每次启动的时候,也会把静态广播接收者注册到AMS。因为Android系统每次启动时,都会重新安装所有的apk,详细流程,我们会在后面PMS的相关章节看到。
- - - - - - - - - - - - - - - 华丽的分界线------------------------
发送广播的流程如下:
1)在Service中,通过AMM/AMP,发送广播给AMS,广播中携带着Filter。
2)AMS收到这个广播后,在receiver列表中,根据filter找到对应的receiver,可能是多个,把它们都放到一个广播队列中。最后向AMS的消息队列发送一个消息。
当消息队列中的这个消息被处理时,AMS就从广播队列中找到合适的receiver,向广播接收者所在的进程发送广播。
3)receiver所在的进程收到广播,并没有把广播直接发给receiver,而是将广播封装成一个消息,发送到主线程的消息队列中,当这个消息被处理时,才会把这个消息中的广播发送给receiver。
我们下面通过图,仔细看一下这3个阶段:
第1步,Service发送广播给AMS
发送广播,是通过Intent这个参数,携带了Filter,从而告诉AMS,什么样的receiver能接受这个广播。
第2步,AMS接收广播,发送广播。
接收广播和发送广播是不同步的。AMS每接收到一个广播,就把它扔到广播发送队列中,至于发送是否成功,它就不管了。
因为receiver分为无序receiver和有序receiver,所以广播发送队列也分为两个,分别发送这两种广播。
AMS发送广播给客户端,这又是一个跨进程通信,还是通过ATP,把消息发给APT。因为要传递Receiver这个对象,所以它也是一个Binder对象,才可以传过去。我们前面说过,在把Receiver注册到AMS的时候,会把Receiver封装为一个IIntentReceiver接口的Binder对象。那么接下来,AMS就是把这个IIntentReceiver接口对象传回来。
第3步,App处理广播
这个流程描述如下:
1)消息从AMS传到客户端,把AMS中的IIntentReceiver接口对象转为InnerReceiver对象,这就是receiver,这是一个AIDL跨进程通信。
2)然后在ReceiverDispatcher中封装一个Args对象(这是一个Runnable对象,要实现run方法),包括广播接收者所需要的所有信息,交给ActivtyThread来发送
3)接下来要走的路就是我们所熟悉的了,ActivtyThread把Args消息扔到H这个Hanlder中,向主线程消息队列发送消息。等到执行Args消息的时候,自然是执行Args的run方法。
4)在Args的run方法中,实例化一个Receiver对象,调用它的onReceiver方法。
5)最后,在Args的run方法中,随着Receiver的onReceiver方法调用结束,会通过AMN/AMP发送一个消息个AMS,告诉AMS,广播发送成功了。AMS得到通知后,就发送广播给下一个Receiver。
注意:InnerReceiver是IIntentReceiver的stub,是Binder对象的接收端。
广播的种类
Android广播按发送方式分类有三种:无序广播、有序广播(OrderedBroadcast)和粘性广播(StickyBroadcast)。
1)无序广播是最普通的广播。
2)有序广播区别于无序广播,就在于它可以指定优先级。
这两种receiver存在AMS不同的变量中,可以认为是两个receiver集合,发送不同类别的广播。
3)粘性广播是无序广播的一种。
粘性广播,我们平常见的不多,但我说一个场景你就明白了,那就是电池电量。当电量小于20%的时候,就会提示用户。
而获取电池的电量信息,就是通过广播来实现的。
但是一般的广播,发完就完了。我们需要有这样一种广播,发出后,还能一直存在,未来的注册者也能收到这个广播,这种广播就是粘性广播。
由于动态receiver只能在Activity的onCreate()方法调用时才能注册再接收广播,所以当程序没有运行就不能接受到广播;但是静态注册的则不依赖于程序是否处于运行状态。
至此,关于广播的所有概念,就全都介绍完了,就连我都比较惊讶的是,我居然没贴什么代码。希望上述这两千多字,能引导App开发人员进入一个神奇的世界。
下一篇文章,我们去看看ContentProvider。
写给Android App开发人员看的Android底层知识(7)
(十二)ContentProvider
(1)ContentProvider是什么?
ContentProvider,简称CP。
做App开发的同学,尤其是电商类App,对CP并不熟悉,对这个概念的最大程度的了解,也仅仅是建立在书本上,它是Android四大组件中的一个。
做系统管理类的App,比如说手机助手这种,有机会频繁使用CP。
而对于应用类App,数据通常存在服务器端,其它应用类App也想使用的时候,一般都是从服务器取数据,所以没机会使用到CP。
有时候我们会在自己的App中读取通信录或者短信的数据,这时候就需要用到CP了。通信录或者短信的数据,是以CP的形式提供的,我们在App这边,是使用方。
对于做应用类App的同学,很少有机会自定义CP供其它App使用。
我们快速回顾一下在App中怎么使用CP。
1)定义CP的App1:
在App1中定义一个CP的子类MyContentProvider,并在Manifest中声明,为此要在MyContentProvider中实现CP的增删改查四个方法:
2)使用CP的App2:
在App2访问App1中定义的CP,为此,要使用到ContentResolver,它也提供了增删改查4个方法,用于访问App1中定义的CP:
首先我们看一下ContentResolver的增删改查这4个方法的底层实现,其实都是和AMS通信,最终调用App1的CP的增删改查4个方法,后面我们会讲到这个流程是怎么样的。
其次,URI是CP的身份证,唯一标识。
我们在App1中为CP声明URI,也就是authorities的值为baobao,那么在App2中想使用它,就在ContentResolver的增删改查4个方法中指定URI,格式为:
uri = Uri.parse("content://baobao/");
接下来把两个App都进入debug模式,就可以从App2调试进入App1了,比如说,query操作。
(2)CP的本质
CP的本质是把数据存储在SQLite数据库中。
各种数据源,有各种格式,比如短信、通信录,它们在SQLite中就是不同的数据表,但是对外界的使用者而言,就需要封装成统一的访问方式,比如说对于数据集合而言,必须要提供增删改查四个方法,于是我们在SQLite之上封装了一层,也就是CP。
(3)匿名共享内存(ASM)
CP读取数据使用到了匿名共享内存,英文简称ASM,所以你看上面CP和AMS通信忙的不亦乐乎,其实下面别有一番风景。
关于ASM的概念,它其实也是个Binder通信,我画个图哦,你们就明白了:
什么?还没看懂?那我再画一个类的交互关系图:
这里的CursorWindow就是匿名共享内存。
这个流程,简单来说是这样的:
1)Client内部有一个CursorWindow对象,发送请求的时候,把这个CursorWindow类型的对象传过去,这个对象暂时为空。
2)Server收到请求,搜集数据,填充到这个CursorWindow对象。
3)Client读取内部的这个CursorWindow对象,获取到数据。
由此可见,这个CursorWindow对象,就是匿名共享内存,这是同一块匿名内存。
举个生活中的例子就是,你定牛奶,在你家门口放个箱子,送牛奶的人每天早上往这个箱子放一袋牛奶,你睡醒了去箱子里取牛奶。这个牛奶箱就是匿名共享内存。
(4)CP与AMS的通信流程
接下来我们看一下CP是怎么和AMS通信的。
能坚持看到这里的人,都不容易。我努力多贴图,不贴代码,即使有代码,也是App开发人员能看懂的代码。
还是拿App2想访问App1中定义的CP为例子。我们就看CP的insert方法。
上面这5行代码,包括了启动CP和执行CP方法两部分,分水岭在insert方法,insert方法的实现,前半部分仍然是在启动CP,当CP启动后获取到CP的代理对象,后半部分是通过代理对象,调用insert方法。
整体的流程如下图所示:
1)App2发送消息给AMS,想要访问App1中的CP。
2)AMS检查发现,App1中的CP没启动过,为此新开一个进程,启动App1,然后获取到App1启动的CP,把CP的代理对象返回给App2。
3)App2拿到CP的代理对象,也就是IContentProvider,就调用它的增删改查4个方法了,接下来就是使用ASM来传输数据或者修改数据了,也就是上面提到的CursorWindow这个类,取得数据或者操作结果即可,作为App的开发人员,不需要知道太多底层的详细信息,用不上。
至此,关于CP的介绍就结束了。下一篇文章,我们看一下App的安装流程,也就PMS。
写给Android App开发人员看的Android底层知识(8)
(十)PMS及App安装过程
PMS,全称PackageManagerService,是用来获取Apk包的信息的。
在前面分析四大组件与AMS通信的时候,我们介绍过,AMS总是会使用PMS加载包的信息,将其封装在LoadedApk这个类对象中,然后我们就可以从中取出在manifest声明的四大组件信息了。
(一)
在下载并安装App的过程,会把Apk存放在data/app目录下。
Apk是一个zip压缩包,在文件头会记录压缩包的大小,所以后续在文件尾巴就算是追加一部小电影,也不会对解压造成影响——木马其实就是这个思路,在可执行文件exe尾巴上挂一个木马病毒,执行exe的同时也会执行这个木马,然后你就中招了。
我们可以把木马思想运用在Android多渠道打包上。在比较老的Android 4.4版本中,我们会在Apk尾巴上追加几个字节,来标记Apk的渠道。Apk启动的时候,从apk中的尾巴上读取这个渠道值。
后来Google也发现这个安全漏洞了,在新版本的系统中,就会在Apk安装的时候,检查Apk的实际大小,看这个值与Apk的头部记录的压缩包大小,是否相等,不相等就会报错说安装失败。
(二)
我们继续说App的安装过程。Android系统使用PMS解析这个Apk中的manifest文件,包括:
四大组件的信息,比如说,前面讲过的静态Receiver。比如说默认启动的Activity。
分配用户Id和用户组Id。用户Id是唯一的,因为Android是一个Linux系统。用户组Id指的是各种权限,每个权限都在一个用户组中,比如读写SD卡,比如网络访问,分配了哪些用户组Id,就拥有了哪些权限。
3)在Launcher生成一个icon,icon中保存着默认启动的Activity的信息。
4)App安装过程的最后,是把上面这些信息记录在一个xml文件中,以备下次安装时再次使用。
(三)
其实,在Android手机系统每次启动的时候,都会使用PMS,把Android系统中的所有Apk都安装一遍,一共4个步骤,如下所示:
其中的第3步、第4步,和单独安装一个App的步骤是一样的。我们分析一下前两步:
第1步,因为结束安装的时候,都会把安装信息保存在xml文件中,所以Android系统再次启动时,再次重新安装所有的Apk,就可以直接读取之前保存的xml文件了。
第2步,从5个目录中读取并安装所有的apk。
最后,回答前面提及的一个问题,为什么App安装时,不把它解压呢?直接从解压文件中读取资源文件比如图片是不是更快呢?其实并不是这样的,这部分逻辑需要到底层C++的代码去寻找,我没有具体看过,只是道听途说问过Lody,他是这么给我解释的:
每次从apk中读取资源,并不是先解压再找图片资源,而是解析Apk中的Resource.arsc文件,这个文件中存储着资源的所有信息,包括资源在Apk中的地址、大小等等,按图索骥,从这个文件中快速找到相应的资源文件。这是一种很高效的算法。
不解压Apk的好处,自然是节省空间。