0%

startActivity 的两种形式

我们知道,最常见的启动Activity的方式:

  • 在Activity 中通过Activity 的startActivity 来启动
  • 利用Context 的startActivity 方式

在前面我们了解过了,二者最终都是使用 Instrumentation 的 EXEStartActivity 来实现的,只不过前面的步骤略有不同而已。

对Activity 的startActivity 方法进行Hook

Activity1通过startActivity 启动Activity2,这个流程很长,上半场是:Activity1通知AMS要启动Activity2;下半场是: AMS 通知App进程,要启动Activity2 。针对上半场,我们可以Hook的地方包括:

  • Activity 的startActivityForResult方法
  • Activity 的 mInstrumentation 字段
  • AMN 的 getDefault 方法获取到的对象

针对下半场,我们可以Hook的地方包括:

  • H 的 mCallback 字段
  • ActivityThread 的 mInstrumentation 对象,对应的 newActivity 方法和 callActivityOnCreate 方法

注意:这里为什么说只能Hook这些地方,一开始我也不理解。经过请教旭哥(刘旭),我们只能访问到自己所在进程的内容,所以只能Hook自己本身进程,而不能Hook到其他进程去。这也是为什么我们不能Hook到AMS的原因,因为压根就不运行在同一个进程。经国斌大神指出,只能Hook到自己的进程以及子进程,如果要Hook其他进程,得有Root权限

Hook Activity 的 startActivityForResult 方法

实际上就是为App写个BaseActivity基类,重写其startActivityForResult方法,这样,不论调用 startActivity 还是 startActivityForResult ,都会执行这个重写逻辑。实际上这都不能称为Hook,只是覆写了而已。

对Activity 的mInstrumentation 字段进行Hook

Activity 的startActivityForResult 最终会用 mInstrumentation 去调用 execStartActivity ,mInstrumentation 是private的,可以通过反射来获取这个对象,之后把它Hook成我们自己写的 EvilInstrumentation 类型对象,这次我们只是在调用 execStartActivity 之前打印一行日志。

对AMN的getDefault 方法进行Hook

在之前曾经介绍过,AMN的getDefault 返回的是IActivityManager 类型,**IActivityManager 是个接口,那么我们就可以使用 Proxy.newProxyInstance 这种动态代理,把这个IActivityManager 接口类型的对象Hook成我们自定义类MockClass1生成的对象。在实际应用的框架中,一般在 Application 的 attachBaseContext 方法中进行 Hook,这样可以在进入任意一个Activity的时候就能应用这个Hook。

对H类的mCallback 字段进行Hook

因为 App 在收到 AMS 发送的 LAUNCH_ACTIVITY 命令后,会通过 Handler 类型的 H 类发送消息,以启动指定的Activity,我们知道,在Handler 内部有个 CallBack 类型的 mCallback 对象,所以我们可以对 H 类的 mCallback 字段进行Hook,拦截这个过程。这时候,你也许会问,为什么不直接Hook了ActivityThread 的mH字段,答案是: 实现不了。截止现在,可以回顾下:

  • 使用静态代理,只有两个类,一个是Handler.Callback,另一个是 Instrumentation,参与Android运转的类,系统只暴露了这两个
  • 使用动态代理,只有两个接口: 一个是IActivityManager ,一个是IpackageManager,这很符合Proxy.newProxyInstance 方法特性,它只能对接口类型对象进行Hook

再次对Instrumentation字段进行Hook

与前面不同,我们这次截获的是 Instrumentation 的 newActivity 和 callActivityOncreate 方法,这两个方法会创建目标 Activity 实例,并且调用它的 onCreate。

对AMN的getDefault 方法进行Hook是一劳永逸的

Instrumentation 调用execStartActivity ,最终调用 AMN.getDefault().startActivity() 方法来启动Activity。我们前面知道,Context 和 Activity 都是通过 Instrumentation 来启动Activity。所以如果我们对 AMN 的 getDefault 方法进行 Hook,那么,不管是从Context 进行startActivity还是从 Activity 进行 startActivityForResult,都能生效,是一劳永逸的。

启动没有在AndroidManifest 中声明的Activity

我们插件的App一般是没有在宿主App的AndroidManifest中声明的。

“欺骗AMS”策略分析

这要从Activity 页面跳转流程说起:

启动Activity的时序图

AMS在第2步会检查Activity是否在AndroidManifest中声明,如果不存在就会报错。如果要让AMS检查不到要启动的Activity怎么办呢?难道要Hook AMS ?不行,做不到的,AMS还管理着其他的App,如果这么做,所有App都受影响了,Android整个的安全性都会有问题了。既然如此,我们就只能在第 1 步(检查之前) 和第 5 步上做文章了。基本思路是:

  • 在第 1 步,发送要启动的Activity 信息给AMS 之前,把这个Activity 替换为一个在AndroidManifest中声明的StubActivity,这样就能绕过AMS的检查了。在替换过程中,要把原来的Activity信息存放在Bundle中。
  • 在第 5 步,AMS通知App启动StubActivity的时候,我们肯定不是启动StubActivity ,而是要替换成目标 Activity,原先的Activity 存在Bundle中,取出来就行。

整个流程如下图所示:

Hook Activity 启动流程

Hook 的上半场

前面说了,对AMN进行Hook,可以一劳永逸,这里我们就按照整个思路来:

1
//代码参见: https://github.comBaoBaoJianqiang/Hook31

MockClass1 的基本思路是:拦截startActivity 方法,从参数中取出原油的Intent,替换为启动StubActivity的newIntent,同时把原有的Intent保存在newIntent中,后面换回来的时候还会用到。

Hook的下半场:对H类的mCallback字段进行Hook

经过前面的AMS欺骗,在第4步的时候AMS就会通知App启动 StubActivity了,我们没有权限修改AMS进程,只能需改第5步。本节的解决方案是基于对H类的mCallback字段进行Hook。

1
//代码可以参考: https://github.com/BaoBaoJianqiang/Hook31

这里主要是将“替身”换成“真身”。

Hook下半场:对ActivityThread的mInstrumentation字段Hook

上一节是通过 Hook Handler 的mCallback 把真身换回来,这节换个思路:

1
//代码可以参考: https://github.com/BaoBaoJianqiang/Hook32

对Instrumentation的 newActivity 和 callActivityOnCreate 方法进行拦截。虽然我们没有在源头把真身换回来,但是我在创建目标Activity的对象时,创建的是目标Activity,并调用目标Activity的onCreate 。

“欺骗AMS”的弊端

这种欺骗AMS手段有个大大的问题——AMS会认为每次打开的都是StubActivity。在AMS端有个栈,会存放每次打开的Activity,那么现在栈上就都是StubActivity了,这就相当于那些没有在AndroidManifest中声明的Activity的LaunchMode就只能是standard类型了,即使为此设置了singleTask或者singleTop也不会生效

概述

插件化用到代理模式,所以要掌握代理模式。随着泛型引进,代理模式分为动态代理和静态代理,在插件化中分别表现为对 Instrumentation 和 对 AMN 进行Hook。

代理模式的UML图如下:

代理模式类图

其中RealSubject 和 Proxy 都是Subject 的子类,在Proxy内部有一个RealSubject类型的成员变量,Proxy 的Reqeust 方法会调用 RealSubject 的Request方法,代码如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract public class Subject {
abstract public void Request();
}

public class RealSubject : Subject {
public override void Request(){
//完成一些任务
}
}

public class Proxy : Subject {
private RealSubject realSubject;

public override void Request() {
//关键是这句话
realSubject.Request();
}
}

简单的UML图和这短短的代码,就是代理的精髓,在介绍代理模式的使用场景时,就知道它的威力了。

远程代理(AIDL)

在Android系统中,远程代理的设计实现就是AIDL。以之前的例子来说,add 方法把a和b两个整数写入data中,通过mRemote的transact方法,把data和reply发送到AIDL的另一端,replay是回调函数,会把计算结果传递回来,UML图如下所示:

AIDL的UML图

给生活加点料(记日记)

Class1有个doSomething方法,我们想在这个方法之前记录一行日志,一般的做法是,直接在在doSomething 方法的最前端写一行代码用于记录日志,在学习了代理模式后,我们可以设计一个Class1Proxy,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Class1Proxy implements Class1Interface {

Class 1 clazz = new Class1();

@Override
public void doSomething() {
println("begin log");
clazz.doSomething();
println("End log");
}
}

//接下来使用ClassProxy 而不是使用 Class1:

Class1Proxy proxy = new Class1Proxy();
proxy.doSomething();

静态代理和动态代理

前面讲的是“静态代理”,但是其实这样是有问题的,每个类都要有个对应的Proxy类,Proxy类的数量会很多,但其实它们的逻辑大同小异。接下来介绍“动态代理”,基于 Proxy 类的 newProxyInstance 方法,它的声明如下:

static Object newProxyInstance(ClassLoder loader, Class<?>[] interfaces, InvocationHandler h)

其中,interfaces 设置为目标对象Class1 所实现的接口类型,对应上面的例子是 Class1Interface,我们通过h对象,可以实现把目标对象 class1 注入,我们看下在实际应用中自己实现的这个h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class InvocationHandlerTest implements InvocationHandler {
private Object target;

public InvocationHandlerTest(Object target) {
this.target = target;
}

@Overrid
public Object invoke(Object o, Method method,Object[] objects) throws Throwable {
println("start log");
Object obj = method.invoke(target,objects);
println("end log");
retun obj;
}
}

注意 method.invoke(target,objects),其中target 就是 class1 对象,接下来调用就是如下方式:

1
2
Class1Interface proxy = new Class1();
Class1Interface class1Proxy = (Class1Interface)Proxy.newProxyInstance(class1.getclass.getClassLoder(),class1.getclass.getInterfaces(),new InvocationHandlerTest(class1));

所以,invoke方法实际上就是在执行 class1 的doSomething 方法。

对AMN的Hook

后续有时间补上

对PMS的Hook

PMS 是系统服务,是没办法Hook的,这个标题只是为了容易理解。我们只能修改它在Android App 进程中的代理对象,它是PackageManager 类型的在很多类中都有这个代理的对象,比如,在ActivityThread 中,有一个字段 sPackageManager ,以及 ApplicationPackageManager 的 mPM字段,我们可以尝试Hook这些字段。

代码:略,后续补上。

整体逻辑

  1. 通过提供的接口调用方法,打算启动
  2. 判断AppId是否为空,空则return;否则继续进入
  3. 启动跨进程的Service(如果之前没有启动的话)
  4. 根据AppId获取小程序配置(可能会在内存中有缓存,通过map存储起来,key为appId,value为配置的Bean),如果有缓存,则先直接使用缓存进入下一步,同时网络拉取配置供下次启动使用
  5. 根据配置判断小程序url是否合法、是否需要登录态,需要强制登录,则判断当前登录态(通过startActivityForResult 登录,在 onActivityResult 中跨进程更新登录态信息);否则拉取js信息。
  6. 正式进入小程序打开小程序

细节问题

  • 整个小程序体系包含:小程序 Loading 页的 Activity 以及 正式的小程序展示页面的 Activity

  • 对于单个小程序,会由 mainFragment 和 newFragment 来展示,其中 mainFragment 用于展示小程序主页面,打开新的子页面使用 newFragment 来承载

  • 跨进程用的服务 AIDLService 在主进程中

  • 小程序 Loading 页与小程序 Activity 在 :miniapp 进程中

  • 为了加快用户进入App就进入小程序这种应用场景,我们创建了一个空的 MiniAppPreService (这是一个 IntentService) ,并且这个 Service 在 :miniapp 进程中,在闪屏页就启动这个 Service。

  • 当小程序需要用户信息时,会有授权流程

  • 判断是否还是当前用户(比如需要涉及到支付的时候,需要做这一步)

  • 提供外部浏览器打开某个链接方式

  • 提供打开本地页面的接口

  • 提供向本地向小程序反馈结果的方法

可能的问题

1、为什么要跨进程?多进程带来的问题?

数据安全(获取用户信息必须得通过接口提供)、小程序崩溃不影响主程序、降低主程序的内存占用

多进程的问题:

  • 谨慎使用多进程,新进程会带有一些公共资源,是会消耗内存的
  • Application 会执行多次
  • SharedPreference 不可靠
  • 静态成员失效
  • 断点调试问题,在实际开发中,调试的时候先去除多进程

2、为什么要降低主程序内存占用,或者说内存占用过大会怎么样?

内存占用过大容易引起gc

3、为什么要用 mainFragment 和 newFragment 来承载小程序?

方便小程序内部页面跳转,以及小程序内部回退。

自己覆写了 onBackPressed 重写按返回键的行为,按返回键会移除新打开的页面

4、怎样启动newFragment?

创建新的 newFragment 对象,将 url 穿进去。之后,通过 FragmentTransaction 的 add 方法,将 newFragment 添加进去,在移除的时候,使用 FragmentTransaction 的 remove 方法移除。

5、fragment的 commit 和 commitAllowLoss 的理解?

6、AIDLService里面做了什么?

创建Binder 对象,用以从主进程获取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class AIDLService : Service() {


private val binder: Binder = object : IUserInfoAidlInterface.Stub() {

override fun getAidlUserInfo(): AidlUserInfo {
val info = AidlUserInfo()
info.ck = UserInfoInstance.getInstance().ck
info.userName = UserInfoInstance.getInstance().username
info.latitude = CommonUtil.getLatitude()
info.longitude = CommonUtil.getLongitude()
info.encryptNetTime = TimerUtil.encryptNetTime
return info
}

}

override fun onBind(intent: Intent?): IBinder? {

return binder
}
}

7、js 用来干什么

为小程序提供调用 Native 方法的接口,比如调用获取用户信息,比如调用打开 App 中的页面

8、aidl 怎么使用,支持哪些数据格式

自己编写 aidl 文件即可,Android Studio 会自动生成 Java 文件,关于这块的知识可以参考以前的博客 ,aidl 文件类似下面的格式:

1
2
3
4
5
6
7
8
9
10
文件:IUserInfoAidlInterface.aidl

//一定要记得手动加这个import
import com.xxx.userinfoprovider.model.AidlUserInfo;

interface IUserInfoAidlInterface {

AidlUserInfo getAidlUserInfo();

}

支持基本的类型,int、long、boolean、float、double、String, 我们项目中需要支持对象,所以我们还需要再定义个 aidl 文件(这个Bean就这一行代码):

1
2
3
4
文件:AidlUserInfo.aidl


parcelable AidlUserInfo;

所以我们写 AIDL,如果需要传输 bean 的话,需要写 n+1 个 aidl 文件,这个 n 就是需要传输的 Bean 的个数。

9、怎么为小程序 Activity 设置进程

在 AndroidManifest.xml 中设置 android:process=”:miniapp”

10、MiniAppPreService 中做了什么?为什么是 IntentService ? IntentService 的作用 ?

里面啥也没做,使用它主要是因为 IntentService 在没有任务的时候自己会结束,不需要手工干预。

11、授权流程怎样的

  1. 与主进程同步数据
  2. 检查是否登录了,未登录先登录
  3. 与后台请求以前是否有授权过,如果授权过,直接将数据返回(json 形式,通过 webview.loadUrl(“javascript:” ) 形式返回)
  4. 未授权过,弹窗,让用户选择是否授权

12、如何检测是否还是当前用户?

因为在小程序里面支付的时候需要,所以支付之前需要检测是否是当前用户

检测方式: 将小程序自己持有的信息以参数形式传过来,之后传给后台,让后台判断

13、如何反馈信息给小程序?

通过使用 webview.loadUrl ,url 为 javascript: 开头的信息:

webview.loadUrl(“javascript:” )

14、不是会内存裁剪?提前启动 Service 会有作用吗?

接下来章节要介绍以下概念,掌握了这些底层知识,就算是迈进了Android插件化的大门了:

  • Binder
  • AIDL
  • AMS
  • 四大组件工作原理
  • PMS
  • App安装过程
  • ClassLoader以及双亲委托

Binder 原理

对于Binder的原理,只需要了解以下过程即可:

  1. 首先,Server 在 SM 中注册
  2. 如果Client 需要调用Server 的 add 方法,就需要先获取Server 的对象,但是SM不会把真正的 Server 对象返回给 Client,而是把 Server 的一个代理对象(Proxy)返回给Client
  3. Client 调用Proxy 的add方法,SM 会帮它去调用 Server 的add 方法,并把结果返回给 Client

具体过程可以参考下图:

Binder原理

AIDL原理

我们自定义一个aidl 文件时(比如 MyAidl.aidl,里面有个sum方法),在sync 和 rebuild 之后,Android studio 会为我们自动生成 MyAidl.java 文件,类图如下图所示:

aidl类图

为什么要把3个类都放在一个文件里面呢?因为如果有多个aidl文件,那么就会有很多的Stub和Proxy类,这样就会重名,把它们放在各自的文件里面,就区分开了,实现了内聚。3个类的代码如下:

1

可以看到,在Stub的asInterface 方法中会根据 IBinder 类型的obj 对象来判断是不是有跨进程,如果未跨进程,直接返回 obj 通过 queryLocalInterface 得到的iin;如果有跨进程,则新建 Stub.Proxy ,并返回。

还有,Stub 的 onTransact 指的是接收Server 返回的数据,这里面会根据code 来做相应的操作,对Server的每个操作都会有唯一的code对应,因为底层并没支持类似 sum 这种method标识。

此外,Parcel 类型的写和读也值得注意,它并不需要key-value的方式,而是直接将value写入(reply.writeString(),reply.wrteInt() 等方式)。可以理解为(这里与旭哥聊过,我自己没看过源码,不一定准确):这些数据都是挨着排放,会存储每个值的偏移值(curr_position)以及大小(size),取的时候,Parcel 并不知道里面存的是什么,只能靠自己正确知道类型按照存入的顺序取出来,比如 data.readInt()、data.redString(),通过偏移值(curr_position)以及大小(size),就能正确读取值。这里可以参考csdn上的博客

对于Proxy 而言,它的sum方法,只是通过 IBinder 类型的 mRemote 将参数和code(用来唯一标记一个操作,对应 onTransact 接收server 返回的数据时的code)发送给server (用_data发送数据,用_reply接收数据),而并没有真正计算。

经过以上的分析,完整的 AIDL 类图应该如下图所示:

完整的AIDL类图

AMS

站在四大组件角度看,AMS就是Binder的Server,四大组件都归他管。

这里引申两个问题:

  • App 的安装过程,为什么不把apk解压到本地,这样读取图片就不用每次都从apk中读取了。
  • 为什么Hook永远是在Binder 的Client端,也就是四大组件这边,而不是在 AMS 端。

对于第二个问题,拿Android的剪贴板来说,它也是个Binder服务,如果在AMS层面把剪贴板给Hook了,那会导致Android系统中所有的剪贴板功能被Hook了,所有App都会受到影响,这不就是病毒嘛。。。所以Android肯定不会允许。

Activity 工作原理

App怎么启动

Launcher 是个App,与我们的各种应用App没什么不同。我们在开发一个App时,在AndroidManifest文件中需要定义默认启动Activity:

1
2
3
4
5
6
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

这些信息在App安装或者 (Android系统启动)的时候,PackageManager-Service 会从apk 中读取封装到 Launcher 显示的桌面图标中。

启动App并非那么简单

上一节只是简单描述,Launcher 与 我们要启动的App(假设是斗鱼App)位于两个不同的进程,所以这一系列过程需要进程间通信Binder来完成的,AMS 也出场了,以启动斗鱼App为例,整体分为以下7个阶段(基于Android 6.0源码):

  1. Launcher通知AMS,要启动斗鱼App,并指定启动的页面
  2. AMS 通知 Launcher “我知道了,没你什么事了”,同时,把要启动的首页记下来
  3. Launcher 页面进入 Pause 状态,然后通知AMS “我睡了,你找斗鱼App去”
  4. AMS 检查斗鱼App是否启动,是,则直接唤起;否则,就启动一个新进程。AMS在新进程中创建一个 ActivityThread 对象,启动其中的main函数
  5. 斗鱼App启动后,通知 AMS “我启动好了”
  6. AMS 翻出步骤 2 中保存的值,告诉斗鱼App,启动哪个页面。
  7. 斗鱼App启动首页,创建 Context 并与首页Activity关联,之后调用首页 Activity 的onCreate函数

至此,App启动流程已经完成。

以上步骤总体可以分为三个部分: 一、Launcher 通知AMS;二、AMS处理Launcher传过来的信息;三、Launcher休眠,通知AMS:“我真的已经睡了”;四、AMS启动新的进程;五:新进程启动,以ActivityThread 的main函数作为入口;六、AMS告诉新的App启动哪个Activity;7、App启动Activity。

Launcher页面本身也是Activity,所以它startActivity 最终也会调用到 Activity 的startActivityForResult,在里面,最终使用 Instrumentation 的 execStartActivity 来实现功能:

Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options)

其中,

  • mMainThread.getApplicationThread() 获取的是一个Binder 对象,这个对象类型为 ApplicationThread(是 MainThread的内部类),这个对象代表了 Launcher 所在的进程。
  • mToken 也是一个Binder对象,代表Launcher这个Activity ,通过 Instrumentation 传给 AMS 后,AMS 一查就能知道是谁向 AMS 发起了请求。

Launcher通知AMS 的流程如下图所示:

Launcher通知AMS

这里要注意一点,ActivityThread 是UI县城,它代表应用程序,我们平常认知 Application 是这个角色。其实Application 就是一个上下文而已,对开发者也许很重要,但是对Android系统中没那么重要。

Instrumentation 的execStartActivity 方法执行时,其实就是将数据透传给 ActivityManagerNativ:

1
2
3
4
5
6
7
8
9
public class Instrumentation{
//方法省略了参数
public ActivityResult exeStartActivity(xxx,xxx,...) {
try{
int result = ActivityManagerNative.getDefault().startActivity(xx,xx,....);

}
}
}

ActivityManagerNative(AMN) 这个类会反复用到。AMN通过getDefault从ServiceManager中取得一个名为 activity 的对象,然后把它包装成 ActivityManagerProxy(AMP) 对象, AMP 就是 AMS 的代理对象。AMN 和 AMP 都实现了 IActivityManager 接口,AMS 继承自 AMN,对照之前的 AIDL 的UML,就不难理解了,下图是 他们的类图:

AMN/AMP的类图

第二阶段就是 AMS 处理Launcher 传过来的信息,主要有如下步骤:

  1. Binder(即 AMN/AMP) 和 AMS 通信,比如这次因为是要启动斗鱼App首页,那么是发送 START_ACTIVITY 请求给 AMS。
  2. AMS 收到后,就会检查斗鱼 App 中 AndroidManifest文件,是否存在这个目标Activity,如果不存在就抛出 Activity not found。
  3. AMS 通知Launcher,“没你事了,洗洗睡”。

我们想想第3步AMS 怎样给 Launcher 发送消息,之前我们说启动过程把 Launcher 以及它所在的进程传过来了,它在AMS 这边保存了一个 ActivityRecord 对象,这个对象里有个 ApplicationThreadProxy,这就是一个Binder代理,它的Binder 真身,就是 ApplicationThread。因此,答案就是:AMS通过ApplicationThreadProxy 发送消息,而App通过 ApplicationThread来接收这个消息。

ApplicationThread(APT)在接收到AMS的消息后,会调用ActivityThread的sendMessage向Launcher主线程Handler(H对象)发送一个 ACTIVITY_PAUSE消息,即进入第三阶段:Launcher休眠,并且通知AMS:“我真的已经睡了”。

这个pause的实现原理:在ActivityThread里面有一个 mActivities 集合,保存当前App(目前是Launcher)中所有的Activity,这时候把它们都找出来,让它们全部休眠。之后,就通知AMS “已经休眠”。

这时候,应该到第四阶段: AMS应该给Zygote 进程发送消息创建新进程了,并且在创建进程后,马上创建 ActivityThread 对象,并且在其中创建主线程Looper、创建Application(注意,Application是在这里生成的)。

具体过程是:ActivityThread 在收到 BIND_APPLICATION消息后,根据传过来的 ApplicationInfo 创建一个对应的 LoadedApk 对象(标志当前的APK信息),接着反射创建Application。在App创建好之后,就通知 AMS “我启动好了”,同时把自己的 ActivityThread 对象发送给AMS,AMS 就登记这个App信息,AMS 以后也能通过这个 ActivityThread (我的理解是ActivityThread中的ApplicationThread的代理对象)对象向这个 APP 发送消息。

接下来就是AMS告诉App要启动哪个Activity,App通过 H 类启动这个新的 Activity。

App内部页面跳转

有了前面的页面跳转,内部跳转就更容易了,流程大同小异,这里不多说

Context家族史

可以用一张图来展示 Context 家族的关系:

Context家族

两种启动Activity方式的差别(这是我自己加的)

通过Activity可以直接 startActivity 来启动新的Activity,也可以在Activity 中 getApplicationContext 来获取 Context 上下文通过 ContextImpl 来最终启动Activity,这二者的区别是什么呢?首先看下下图:

两种启动Activity方式

Context的 startActivity ,其实也是通过 ContextImpl 拿 mMainThread对象,从 mMainThread 中获取 Instrumentation,让它来执行 execStartActivity,和 Activity 自己的方法实现是一样一样的(其实这里在书上说得并不详细,至少没说为什么 ApplicationContext 在startActivity 时 为什么要 NEW_TASK 标记)。

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
29
30
31
32
33
34
35
36
//Context 的 startActivity
public void startActivity(Intent intent, Bundle options) {
...
if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ " Is this really what you want?");
}
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity) null, intent, -1, options);
}



//Activity 的 startActivity
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,@Nullable Bundle options) {
if (mParent == null) {
...
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
...
} else {
if (options != null) {
mParent.startActivityFromChild(this, intent, requestCode, options);
} else {
// Note we want to go through this method for compatibility with
// existing applications that may have overridden it.
mParent.startActivityFromChild(this, intent, requestCode);
}
}
}

主要是Context 的startActivity 时,要求一定要有 FLAG_ACTIVITY_NEW_TASK,否则就不能执行下去。还有,Activity 的startActivityForResult 中执行的 mParent.startActivityFromChild(),最终也是通过 mInstrumentation.execStartActivity实现的,因此我们可以说Activity 的startActivity 最终也是通过 Instrumentation 的 execStartActivity 实现

Service 工作原理

Service 有两套流程,一套是启动流程,另一套是绑定流程:

Service两套流程

在新进程启动 service

在新进程启动Service主要分为 5 个阶段:

  1. App向 AMS 发送一个启动Service 的消息
  2. AMS例行检查(是否在AndroidManifest中声明),查看目标Service 是否存在,如果存在就不管;如果不存在,则把Service信息保存下来,之后创建新进程
  3. 新进程启动后,通知AMS “我可以了”
  4. AMS 把保存的Service信息发给新进程
  5. 新进程启动Service

当然,这个启动新进程,还是会启动 ActivityThread ,并且新进程启动Service 也是向 ActivityThread 的 H 类发送 START_SERVICE 来启动。

不管是前面的Activity ,还是后面的Service,创建对象时,都是通过 packageInfo(它是一个LoadedApk对象)去获取 ClassLoader,之后再通过反射创建出来 Activity 或者 Service 对象,四大组件的逻辑都是如此。所以,我们如果要做插件化,可以在这里做文章,换成插件的classLoder,加载插件中的四大组件

BroadCast 原理

注册过程

用Activity或者Context去注册,其实都是使用Context的registerReceiver方法,然后通过 AMN/AMP 把一个Receiver 传给AMS,在注册过程中,会使用 PMS 获取包信息,业绩 LoadedApk 对象。这个LoadedApk 对象的 getReceiverDispatcher 方法将传过来的 Receiver 封装成一个实现了 IIntentReceiver 接口的Binder 对象。我们实际上就是将这个 Binder 对象和 IntentFilter 对象传递给了 AMS。不过,这时发送广播是不知道发给谁的,所以Activity所在的进程还要把自身的对象也发送给AMS。

这里都是动态注册广播,那静态广播呢?它是在App安装的时候注册。动态Receiver 与静态Receiver 分别存在AMS中不同的变量中,在发送广播时,会把两种 Receiver 合并到一起,其中动态的排在静态的前面,然后依次发送,因此,动态Receiver 永远先于静态Receiver收到消息。

发送过程

发送时,通过 AMN/AMP 发送广播给 AMS,广播中也带着 IntentFilter 对象。AMS 收到广播后,根据Filter 找到对应的Receiver,向广播接收者所在的进程发送广播。Receiver 所在进程收到广播后,并不会直接发给Receiver,而是将广播封装成一个消息,(通过H类)发送到主线程的消息队列,当这个消息被处理时,才会把消息中的广播发送给 Receiver。

广播种类

无序广播、有序广播、粘性广播。这个粘性广播,用一个例子来了解:当电量小于 20%时,就会提示用户,而获取电池电量信息就是通过广播来实现的,一般的广播发完就完了,我们需要这样一种广播,发出后还能一直存在,未来的注册者也能收到这个广播,这种广播就是粘性广播

ContentProvider 工作原理

ContentProvider 的本质是把数据存储在SQLite 数据库中,但是封装成了统一的访问方式,比如对于数据集合而言,必须提供增删改查功能。我们在SQLite 上封装了一层,就成了 ContentProvider。

匿名共享内存(ASM)

ContentProvider 读取数据使用了 ASM,并且,ASM实质上也是个Binder通信,下图为 ASM 类的交互关系图:

ASM的类交互关系

图中的 CursorWindow 就是匿名共享内存,这个流程简单来说分为3个步骤:

  1. Client内部有一个 CursorWindow 对象,发送请求时,把这个CursorWindow 类型对象传过去,这个对象暂时为空
  2. Server 收到请求,搜集数据,填充到这个CursorWindow 对象中
  3. Client 读取内部这个CursorWindow 对象,获取数据。

举个例子就是: 你定牛奶,在家门口放个空箱子,送奶人每天早上往这个箱子放牛奶,你睡醒了去箱子里取牛奶,这个奶箱就是匿名共享内存

ContentProvider 与 AMS 的通信

  1. App2发送消息给AMS,想要访问 App1 中的 ContentProvider
  2. AMS 检查发现,App1的ContentProvider没有启动过(也即App1没启动,因为我们知道App启动时,在 Application的onCreate 之前就会启动ContentProvider,这也是 LeakCanary 新版免install的原理),为此新开一个进程启动App1
  3. AMS获取App1启动的ContentProvider ,并把 ContentProvider 的代理对象返回给 App2
  4. App2 拿到 ContentProvider 的代理对象,也就是 IContentProvider,就调用它的增删改查方法。
  5. 接下来就是使用 ASM(匿名共享内存) 传输数据,也就是上面提到的 CursorWindow 类,取得数据或者操作结果即可

PMS 及 App 安装过程

PMS简介

在前面我们介绍过,AMS会使用 PackageManagerService(简称PMS)加载包的信息,之后AMS会将这个信息封装在 LoadedApk 这个类对象中,然后我们就可以从中取出在AndroidManifest 中声明的四大组件信息了。

在安装App的过程中,会把 apk 复制到 data/app 目录下。apk是一个zip压缩包,在文件头会记录压缩包的大小,所以就算在后续文件尾追加一部电影,都不会对解压造成任何影响。Android的多渠道打包其中的一种思路就是这样,在 apk 尾巴上追加几个字节,来标记apk的渠道,apk启动时,从apk尾巴上读取这个渠道值。不过,后来google发现了这个漏洞,在新版本系统中,系统安装apk时,会检查apk实际大小,二者不相等就会报错安装失败

这里回答前面提出的问题:为什么App安装不把它解压?其实,每次从apk读取资源,并不是先解压再找资源,而是解析apk中的resources.arsc文件,这个文件存储着资源的所有信息,包括资源在apk中的地址、大小等,按图索骥,可以很快拿到资源文件,这是一种很高效的算法。不解压的好处自然是节省空间

App安装流程

Android 系统使用PMS解析apk的AndroidManifest 文件,包括:

  • 四大组件信息
  • 分配用户id和用户组id,用户ID是唯一的,用户组id指的是各种权限,每个权限都在一个用户组中
  • Launcher 生成一个icon,icon中保存着默认启动Activity的信息
  • App 安装的最后,会把上面的信息记录在一个 xml文件中,以备下次安装使用

Android手机系统每次启动时,都会使用PMS,把Android系统中所有的apk都安装一遍,共4个步骤:

App安装流程

关于第1步,因为app安装过后,都会xml保存安装信息,所以Android系统再次启动后,就可以直接读取之前保存的xml 了。第2步就是从所有目录安装apk。

PakageParser

PMS是系统进程,我们是不能Hook的。PMS中有个类:PackageParser ,它是专门用来解析 AndroidManifest 文件的,以获取四大组件信息以及用户权限。它有一个parsePackage 方法,接收一个apkFile 文件参数,这个参数既可以是 当前apk文件,也可以是 外部apk文件,我们可以使用这个方法来读取插件apk的信息,虽然PackageParser 类不对外开放,但是我们可以反射来获取这个类。

ActivityThread 与 PackageManager

对于App开发人员,可以通过 Context.getPackageManager() 来获取当前的 Apk 信息,但其实它在ContextImpl中的真正实现是通过 ActivityThread.getPackageManager() 来实现的。所以,我们一般可以通过反射 ActivityThread 来获取Apk 信息,注意,这里一般是获取宿主Apk 的包信息,而不是插件Apk包信息

ClassLoader 家族史

Android 插件化能加载外部下载的 Apk,就在于 ClassLoader。其中最重要的是 PathClassLoader 和 DexClassLoader,及其父类 BaseDexClassLoader。 PathClassLoader 和 DexClassLoader这两个类都很简单,粗看没啥区别,仔细看,构造函数第2个参数 optimizedDirectory 的值不一样,PathClassLoader 把这个参数设置为null,DexClassLoader 把它设置为一个非空的值。其实,这个值是用来缓存我们需要加载的dex文件的,并创建一个DexFile对象,如果为null,那么就会直接使用dex文件原有的路径来创建DexFile对象。由于DexClassLoader 可以指定自己的 optimizedDirectory,所以它可以用来加载外部的dex;而PathDexClassLoader 没有这个参数,只能加载内部的dex(存在于系统中的已经安装过的apk里面)。

MultiDex

方法数超过65536 时,会有著名的 “65536”问题,一般来说我们使用 MultiDexApplication就能解决。Google还推出了 MultiDex 工具,它就是把原先的一个dex文件,拆分成多个dex文件,每个dex的方法数量不超过 65536。其中 classes.dex 称为主dex ,由App 使用PathClassLoader 加载,而 classes2.dex 等 dex 会在App 启动之后使用 DexClassLoader 进行加载。

在Android 5.0之后,虽然能够在dex中容纳比65536更多的方法数,但是dex的体积变大了,为了加快速度,我们还是可对dex进行拆分,classes.dex只保留App启动所需的类以及首页的代码,从而确保App花最少的时间启动并进入首页,而把其他的模块代码转移到其他dex中,这个技术称为手动分包,在后续会介绍。

插件化是什么

如果做一个游戏平台,不可能把所有的游戏都包含进去,因为这样体积太大,浪费流量,浪费用户存储空间。只有用户需要玩什么游戏的时候,再动态下载安装某个游戏,这就是插件化的思想。

为什么需要插件化

如果某个插件功能有问题,则只需要改好bug打包这个插件就ok,不需要发版,用户不需要去应用商店更新App,就能够修复好的版本。这样,交易不会丢失。

此外,对于和竞争对手抢占市场的情况下,需要不断迭代发布新版本,发版周期太长,用户体验不到新功能;太短会引起用户反感,如果有插件化技术,做完新功能就能让用户看到新功能,这样就非常具有竞争力了。

后续发展趋势

在插件化风行一段时间之后,Android热修复技术和 React Native 开始出现,与Android的插件化平分秋色,插件化技术不再是唯一选择。

插件化的用途

真是场景下,插件化 80% 的应用场景是为了修复线上bug,在这一点上,插件化与 Tinker、Robust 这类修复工具有相同的能力,甚至更出色。

其实,插件化更适合游戏领域,游戏经常上线新英雄,或者新的皮肤,这些都不用发版。

还有一个很好的场景: ABTest,当产品为两种风格的设计举棋不定的时候,那么把两种策略做成两个插件包,让 50% 的用户下载A策略,另外 50% 下载B策略,一周后看数据即可。

更好的替代品:React Native

React Native 这种映射思路(Android 和 ios 中的控件在RN中都能找到映射),能够跨越 Android 和 ios 的系统差别。使用RN和纯原生相比,性能差距不大,在两个平台都很流畅。对于中小型企业来说,还能节省一端人力。

在jsPatch 被禁止后,RN 其实是ios 上最合适的热修复方案了。

Android存储安全

在4.3以前,应用都在自己的沙盒里,沙盒使用标准Linux保护机制,为每个应用创建唯一 Linux UID 来定义。简单来说,就是保证微信不能访问淘宝的数据

4.3以后引入SELinux,进一步定义应用沙盒边界。即使我们进程有root权限也不能为所欲为(想干事情必须先在安全策略配置文件中赋予权限)。

数据加密:Android 两种加密,全盘加密和文件加密。全盘加密在4.4引入,在 5.0 后默认打开

常见数据存储方法

综合来看,Android提供了 SharedPreferences /ContentProvider/文件/数据库

SharedPreferences

非常简便,但是问题比较多:

  • 跨进程不安全
  • 加载缓慢(使用异步加载,且没设置线程优先级,就有可能出现主线程等待低优先级线程所问题)
  • 全量写入(无论commit还是apply,即使只改动一个条目,也会把全部内容写入,并且多次写入一个同一文件,也不会合并为一次)
  • 卡顿(收到系统广播或onPause等时机,系统会强制把sp文件写到磁盘,此过程或阻塞)
  • 还有,存储json等复杂文件时,会有转义等操作,会额外耗时

如果我们想的话,可以使用 MMKV 替代sp,它利用文件锁保证跨进程安全,并且性能也比较好

ContentProvider

ContentProvider 的生命周期默认在 Application 的onCreate 之前,而且是在主线程的,ContentProvider 跨进程传递数据是利用 Android 的 Binder 和匿名共享内存机制。简单来说,就是通过 Binder 传递 (CusorWindow 对象内部的)匿名共享内存的文件描述符,这样数据无需跨进程。

ContentProvider 主要存在以下几个问题:

  • 自定义的 ContentProvider 的构造函数、静态代码块、onCreate 函数尽量不要做耗时操作
  • 性能。传输数据比较小的时候,使用 ContentProvider 不一定划算
  • 安全:ContentProvider本身提供了很好的安全,但是如果是exported,当支持执行 sql 语句时,就要注意 sql 注入问题。

简而言之,ContentProvider 适合相对比较笨重,适合传输量大的数据

UI优化<下>

ui渲染测量

测试工具: Profile GPU Rendeing 和 Show GPU Overdraw ,定位方法可以参考官方文档,检查GPU渲染速度和过度绘制

问题定位工具可以使用 Systrace 和 Tracer for OpenGL ES,具体可以参考官方文档:渲染速度慢

适用于自动化测试场景的测试方式(自己加的标题)

虽然图形化界面工具非常好用,但是难以用在自动化测试场景,以下测量方式可以用于自动化测试:

gfxinfo

gfxinfo 可以输出包含各个阶段发生的动画以及帧相关的性能信息,以及渲染相关的内存和View hierachy 信息,命令如下:

adb shell dumpsys gfxinfo

在Android 6.0 之后,gxfinfo 命令新增了 framestats 参数,可以拿到最近120帧每个绘制阶段的耗时信息:

adb shell dumpsys gfxinfo framestats

SurfaceFlinger

除了耗时,还需要关心渲染使用的内存,4.1以后每个 Surface 都会有 3个 Graphic Buffer,那如何查看 Graphic Buffer 占用的内存,可以通过如下命令查看相应信息:

adb shell dumpsys SurfaceFlinger

这部分内存大小和 屏幕分辨率,以及Surface的个数有关。

UI优化的常用手段

我们的目标是实现app的帧率达到 60fps,意味着所有操作要在 16.7 ms 内完成,这期间要做的事情如下所示:

UI渲染流程

我们优化,就是拆解渲染的各个阶段耗时,找到瓶颈,加以优化。ui优化的方法如下:

1、尽量使用硬件加速。

硬件加速绘制的性能是远远高于软件绘制的,所以ui优化第一个手段应该尽量使用硬件加速。但是有些api不支持硬件加速,这个需要注意,比如 渐变、磨砂、圆角等,它们的渲染性能比较低。

2、View 的创建优化

View 的创建在UI线程,对于复杂的界面,这部分耗时不容忽视。View 的创建过程中,会包括xml的读取io,解析xml 以及生成对象的时间(Framework会大量使用反射)

因此建议:

1、使用代码创建view 对象缺点是不能直接预览
2、提前创建、异步创建
3、View 重用(模仿ListView、RecyclerView,不过重用要注意先清空状态)。

如果异步创建,那么会导致系统抛出异常。这时候,我们可以通过非常取巧的方式来异步创建ui:先把线程的Looper的MessageQueue替换成Looper的Queue:

子线程创建ui

要注意的是,在创建完成view之后,需要把线程的Looper恢复成原来的。

3、measure/layout 优化

这两部分也是在ui线程执行的,一般的优化方法有:

  • 减少UI布局层次
  • 优化layout开销。尽量不要使用 RelativeLayout 和 weighted LinearLayout ,它们开销很大,建议使用 ConstranLayout
  • 背景优化。尽量不要重复设置背景,尤其注意的是主题背景,theme默认会是一个纯色背景,如果我们自定义了界面背景,那么主题背景对我们来说是无用的,由于主题背景是设置在DecorView 中的,所以会带来重复绘制,也会带来绘制性能损耗。

4、UI优化进阶

可以采用flutter

二叉查找树(也称二叉搜索树)

  • 左子树上的节点值都小于等于根节点的值
  • 右子树上的节点值都大于等于根节点的值
  • 左右子树也是二叉搜索树

它是基于二分查找的思想,查找最大的次数为二叉树高度

查找代价

  • 当左右子树高度大致平衡时,时间复杂度在 O(logN)
  • 当先后插入的关键字有序时,退化成链表,查找的时间复杂度就在 O(N)了

插入代价

新节点插入到树的叶子节点上,因此,插入节点和查找一个不存在的数据的代价相同

删除代价

  • 如果被删除的节点左、右 有一个为null时,代价仅为 O(1)
  • 如果左右子树都存在,时间复杂度最大也不会超过O(logN)

缺陷:

极端情况可能退化成链表,时间复杂度为 n。这主要是由于树不平衡导致的

平衡二叉查找树(平衡二叉搜索树)

是严格的平衡二叉树,它是空树或者左右两个子树的高度差 小于等于1,同时,左右两个子树也是平衡二叉搜索树

查找代价

时间很稳定,查找效率最好最坏都是 O(logN)

插入代价

由于要保证严格的平衡,插入时可能要进行再平衡(最多旋转一次),因此插入的整体代价还在 O(logN)

删除代价

和插入一样,要考虑再平衡,但是最多需要O(logN)次旋转,所以时间复杂度为 O(2logN)

红黑树(Red-Black Tree)

它并不严格地平衡,最长路径长度不超过最短路径长度的2倍。它删除和插入引起平衡性改变的概率要远低于平衡二叉搜索树

查找代价

查找代价基本上维持在 O(logN) 级别,最差情况下肯定比平衡二叉搜索树要差,因为没有那么平衡

插入代价

不容易引起失衡,整体代价和平衡二叉搜索树差不多,也是 O(logN) 级别(虽然涉及变色,但是变色的代价很小)

删除代价

相对平衡二叉搜索树,不容易引起失衡,时间复杂度也在 O(logN) 级别

补充

平衡二叉搜索树由于插入和删除,会引起需要调整,可以通过 :变色、左旋转、右旋转 三种方式调整。是否需要调整要根据红黑树的特性:

  • 节点是红色或黑色
  • 根节点是黑色
  • 叶子节点都是黑色的空节点
  • 红色节点的两个子节点都是黑的(红节点不能连续出现)
  • 任一点到每个叶子节点的路径包含相同数目的黑节点

B-树和B+ 树

我们所谓的B-树,其实并不是B减树,中间是横线,不是减号;B + 就是 B加树了

如 os 的文件目录存储、数据库中的索引结构的存储,不可能在内存中建立查找结构,必须在磁盘中建立好结构。

在磁盘组织结构下,从任何一个节点指向其他节点都可能读取一次磁盘,再将数据写入内存比较。这回带来大量的IO操作,所以我们需要新的数据结构,即 B树和B+树。

B树是一种多路平衡查找树,每个节点最多包含k个孩子,k称为B树的阶。K大小取决于磁盘页的大小

以上内容参考自知乎专栏知乎csdn博客

UI优化<上>-20讲

UI渲染的背景知识

ppi 像素密度,每英寸包含的像素数,这是物理参数,不可改

dpi 像素密度,指的是单位尺寸像素数量。这是可以人为调整的

density 密度,每平方英寸中包含的像素点数,density = dpi / 160

dp : px = dp * density

屏幕适配方案

使用dp

限制符适配

CPU 与 GPU

软件绘制与硬件绘制

由上面的图可以知道,软件绘制使用的是Skia 库,硬件绘制是通过 open GL 之后在GPU 上实现的

在Android 7.0以后,添加了对 Vulkan 的支持,它比 OpenGL 功耗和多核优化上更优秀

Android 渲染的演进

可以通过下图整体看下Android 图形体系:

Android图形体系

各个部分的功能可以比喻成以下内容:

  • 画笔:Skia 或者 OpenGL 。Skia 使用CPU 绘制,OpenGL 使用 GPU 绘制
  • 画纸:Surface。所有元素都在 Surface 这张画纸上绘制和渲染。在Android中,Window 是View的容器,每个Window 都会关联一个Surface。windowManager 负责管理这些 window ,并且把它们的数据传递给 SurfaceFlinger。
  • 画板:Graphic Buffer。Graphic Buffer 缓冲用于应用程序图形的绘制,Android 4.1 之前使用的是双缓冲;4.1之后使用三缓冲
  • 显示:SurfaceFliger 。将WindowManager 提供的所有 Surface ,通过Hardware Composer 合成并输出到显示屏

开启硬件加速

软件绘制流程图如下:

软件绘制流程

  • Surface: 每个View 都由某个Window 管理,每个Window 关联一个Surface
  • Cavas。通过Surface的lock 函数获得一个Cavas,Cavas 可以理解成Skia 底层接口的封装
  • Grapic Buffer。 SurfaceFlinger 帮我们托管 BufferQueue ,我们从BufferQueue 中拿到 Graphic Buffer,然后通过Canvas 和 Skia 将绘制内容栅格化到上面(个人理解为栅格化后的数据保存在这个buffer中)。
  • SurfaceFlinger 。通过 Swap Buffer 把 Front Graphic Buffer 的内容交给 SurfaceFlinger ,最后硬件合成器 Hardware Composer 合成并输出到显示屏。

硬件加速绘制流程如下图(3.0以后支持硬件加速):

硬件绘制流程

硬件绘制与软件绘制最核心的区别是硬件绘制通过GPU完成 Graphic Buffer内容的绘制,此外,硬件绘制引入了 DisplayList 的概念,每个View内部都有一个DisplayList,当某个View需要重绘时,将其标记为Dirty,重绘也仅仅只需要重绘一个View的DisplayList,这样,无需像软件绘制那样向上递归,大大减少绘图的操作数量,提高了渲染效率,更新的过程示意如下:

硬件绘制更新Display

硬件加速虽然极大地提高了显示和刷新速度,但是它也存在一些问题,一方面是内存消耗,另一方面是部分绘制函数不支持

Project Butter (黄油计划)

4.1的时候,提出黄油计划,主要包括两个方面,一是 VSYNC ,一是 Triple Bufferfing (三缓冲)。

在4.0 及以前,cpu可能在忙别的事情,导致没来得及处理UI 绘制,为了解决这个问题,VSYNC 出现了,它类似于时钟中断,这个信号到来时,CPU立即准备Buffer数据,大部分设备刷新频率都是60Hz,所以一帧数据的准备工作要在 16ms内完成。

4.0及以前,Android使用双缓冲,一般不同的View或者Activity 都会公用一个Window,也就是公用一个Surface,每个Surface 会有一个BufferQueue 缓存队列,这个队列由SurfaceFlinger 管理,通过匿名共享内存与App应用层交互。示意图如下:

app与ui机制交互图

整个流程如下:

每个Surface对应的 BufferQueue 内有有两个Graphic Buffer,一个用于绘制,一个用于显示。

同一时刻可能有多个Surface (可能是不同应用的Surface,也可能是同一个应用里面类似SurfaceView 和TexureView ,它们都会有自己单独的Surface),SurfaceFlinger 把所有的Surface 要显示的内容统一交给 Hardware Composer,它会最终合成屏幕显示的内容。

如果只有两个Buffer,当CPU/GPU 绘制时间过长,则两个缓冲区分别被显示设备和GPU 占用,cpu 无法准备下一帧数据,造成浪费。三个缓冲区的话,cpu、gpu 显示设备都能使用各自的缓冲区工作,个不影响,最大限度利用空闲时间。

在黄油计划之后,Android 5.0 推出了 RenderThread ,将所有GL 命令执行放到 RenderThread 中执行,减轻UI 线程的负担。

数据测量

可以通过开发者选项中查看过度绘制的情况

还可以使用 Systrace 性能数据采样和分析工具

4.1及以后,可以采用 Tracer for OpenGL ES 逐帧分析

第18课-耗电优化<上>

准确的测量电量并不是那么容易,在《大众点评App的短视频耗电量优化实战》一文中,为我们总结了下面几种电量测试的方法:

耗电量测试方法

当测试反馈耗电问题时,bug report 结合 Battery Historian 是最好的排除方法:

1
2
3
4
5
6
//7.0和7.0以后
$ adb bugreport bugreport.zip
//6.0和6.0之前:
$ adb bugreport > bugreport.txt
//通过historian图形化展示结果
python historian.py -a bugreport.txt > battery.html

19讲-耗电优化<下>

如何优化

  • 耗电优化的第一个优化方向是优化应用后台耗电
  • 第二个优化方向是符合系统的规则,让系统认为你的耗电是正常的。

比如,Android P 通过 Android Vitals 监控后台耗电,所以我们需要符合它的规则,它的规则如下:

Android-Vitals规则