0%

一、Window 总览

window 是屏幕上一块区域,但不是实实在在的,实际显示的是 View 。WMS、WindowManager 以及 Window 之间的关系如下:

Window相关方的关系

  • Window: 窗口的概念,Android 中所有的师徒都依赖于 Window 显示

  • WindowManager : 管理 Window ,包括 增、删、更新 等操作,类似一个代理的概念,最终要丢给 WMS 去实际操作。

  • WMS : Window 最终管理者,负责 window 的启动、添加、删除,Window 的大小和层级也是 WMS 管理的

1.1 Window 分类

Application Window

xxxx

statusBar 和 searchBar 等居然也都是 System Window 。

打电话时,脸靠近手机的时候,黑屏 以及 脸不能按屏幕上的按钮,这些都是 Window 的标记来实现的。

二、WindowManager

WindowManager 在应用层。WindowManager 是继承了 ViewManager 的,这里发散以下, ViewGroup 也是实现了 ViewManager ,联动一下。

Activity 的 attach 方法里面,会创建 Window ,以及 绑定到 WindowManager 。

Activity 中 Window 下最直接管理的 View 是 DecorView ,示意图如下:

DecorView

注意,我们平时在代码中调用 setContentView 是我们上图蓝色部分的 ContentView 吗?答案不是的,我们 setContentView 的 View 到时候会作为上述 蓝色 ContentView 的子 View 。

我们都说onResume 的时候,是可见可操作的。这里其实应该是Activity 可见 ,就是 Activity 已经创建好了!但其实里面的元素还是不可见的,因为 performResume 之后,才会执行 window.addView 将 View 添加进去,所以,要注意区分。

WindowManagerGlobal 主要的职责是:

  • 设置view的参数(layoutparams)

  • 创建 ViewRootImpl

其中,WindowManagerGlobal 的 addView 方法中会创建 ViewRootImpl 的对象。

总结一下各方关系就是:每个 Activity 都有一个 PhoneWindow ,每个 PhoneWindow 中有个根 View——DecorView ,并且PhoneWindow 中有个 WindowManagerImpl 用于管理 Activity 中 VIew 的细节,然后 WindowManagerGlobal 用于全局管理所有的 WindowManagerImpl ,示意图如下所示:

Window各方关系如下

三、绘制流程

在 ViewRootImpl 中,接收到同步信号的时候,就会触发 performTraversalses() 方法,在里面会执行 performMeasure、performLayout、performDraw 等,

ViewRootImpl

ViewRootImpl 的功能主要体现在如下几个方面:

  • 作为 View 树的树根(注意,它自己并不是View,只是树根的意思而已)并管理 View 树

  • 触发 View的测量、布局和绘制

  • 输入响应的中转站

  • 负责与 WMS 进行进程间通信

关于输入响应的中转站,我们可以看如下图:

ViewRootImpl是输入的中转站

事件产生后,会传递到 ViewRootImpl 中,之后被传递给 DecorView ,我们根据这个步骤可以看到,这里面传递很弯弯绕绕,DecorView 传递给Acitivty ,第 3 步的时候,又回到了 DecorView 中。

为什么要这么设计呢?省略中间商赚差价直接到 ViewGroup 行不行?答案是不行。因为诸如打电话脸贴到屏幕上时需要黑屏,点击操作不能响应等操作, View 和 ViewGroup 上面没有相关的处理机制的,都在 PhoneWindow 里面,所以必须经过 PhoneWindow 。

view 或者 Window 的刷新都依赖于 ViewRootImpl 中的 scheduleTraversals 方法。

UI刷新

以 2 个问题开始:

  • TextView 连续 2 次setTextView ,那么会触发几次重绘?

  • Android 为什么要求帧率是 60?

答案:UI必须等待 16ms 的间隔才会绘制下一帧,所以连续2次 setTextView 只会触发一次重绘。同时必须每秒 60 帧(电影是 24 帧)用户才不会感觉卡顿,所以就 16ms 就要一帧。

关于刷新的整体流程我们可以参考如下图:

刷新整体流程

从图可以看出,setText 之后,就会触发 invalidate ,由于 View 自己不能刷新,需要向上请示老大,一直到 ViewRootImpl ,而 ViewRootImpl 自己也不能刷新,只能插入同步栅栏,等待下一次的 Vsync 信号(通过编舞者postcallback)来了之后再绘制。等到 Vsync 信号来了之后,我们会把同步栅栏给移除掉

小结

总结一下主要有几点需要注意:

  • Android 一般是 60FPS ,是 VSYNC 决定的,每 16ms 最多一帧

  • VSYNC 要客户端主动申请,才又 VSYNC 到来才会刷新

  • UI没有更改的话,不会请求 VSYNC 也就不会有刷新

  • UI局部重绘其实只会去重绘有更新的 View

SurfaceFlinger

SurfaceFlinger 是整个 Android 系统渲染的核心进程,整个流程是这样的:每个 DecorView 对应一个 Surface ,每个 Surface 里面包含一个 canvas ,每个 surface 对应一个 layer(图层), SurfaceFlinger 将各类的图层合成。

SurfaceFlinger 的整体流程如下图所示:

SurfaceFlinger整体流程

面试题

onResume 里面度量的宽高有效吗?

分情况。Activity 第一次调用 onResume 的时候是无效的,从 Activity A 跳转到 B 再返回来,这时候 A 的onResume 中是有效的。因为 onResume 的时候,还没有 执行 window.addView 呢。

Activity 、Window、view 三者的联系和区别。

先整体,再细节。所以先讲这3个是啥,Activity 是xxx,window 是xxx,View 是xxx。Activity 没有界面,委托给 Window 展示,

首次 View 的绘制流程是什么时候触发的?

WindowManagerImpl.addView -> WindowManagerGlobal.addView

ViewRootImpl.setView -> ViewRootImpl.requestLayout

ViewRootImpl.scheduleTraversals

这时候就触发了首次绘制

我们调用 invalidate() 之后会马上进行屏幕刷新吗?

一定不。需要等到下一个 Vsync 信号来了才会

我们说丢帧是因为主线程做了耗时操作,为什么做了耗时操作就会引起丢帧?

一言以蔽之:主线程的耗时操作会影响下一帧的绘制。我们知道有以下信息:

  • 在 ViewRootImpl 的 scheduleTravesals 方法中会去发送同步屏障,接着发送异步 Message,用于处理 UI 更新

  • 在Handler 机制中碰到 target == null 这种 Message 的时候,就知道这是同步屏障了,此后,就只执行异步 Message

  • 我们知道主线程耗时操作的体现形式也是 Message

  • 如果有耗时操作之前的消息有耗时操作,那么可能耗时导致推迟执行 同步屏障那个 Message ,也就推迟了后续的 异步 Message的执行,也就影响了下一帧的绘制

一、前情回顾

init 操作主要是解析 init.rc 文件,它是用户空间的鼻祖。

zygote 是 Java 进程的鼻祖,系统启动的过程在 zygote 之前都是运行在 c/c++ ,初始化运行环境 Android RunTime ,zygote 开始才进入 Java。

zygote 通过 fork 创建 SystemServer 进程,这里面有各种 Android 的服务,比如 AMS 等

Activity 启动通信过程中需要注意一点:Launcher 进程获取通信代理对象(下图第1步)在 10.0 之前 是 AMS ,在 10.0 之后获取到的是 ATMS ,老师说在面试的时候要说下,整个流程如下图所示:

Activity启动通信图

1.1 Activity 的管理

在哪里对 Activity 的栈进行管理,Activity 信息的管理?

答案是在 ActivityStarter 这个类当中(com.android.server.wm 这个包名下的)。在 AMS里面会执行 ActivityStarter.execute() 方法,在 execute 方法里面,会调用 ActivityStarter 的内部类 Request 的 resolveActivity ,在这里面首先获取到 ResolveInfo 数据,之后根据 ResolveInfo 数据获取到 activityInfo 数据**,至此 Activity 信息就获取到了:

1
activityInfo = supervisor.resolveActivity(intent, resolveInfo, startFlags, profilerInfo);

App 层面的一个 Activity 对应在 AMS 中的 一个 ActivityRecord 对象。 还有个细节,在 ActivityStarter 类里面,会通过 computeLaunchingTaskFlags 方法计算 Activity 的启动模式。还有个关键点,ActivityStarter 中会通过 recycleTask 方法清除要启动的 Activity 上面的那些 Activity (比如 SingleTask 模式下)。

1.2 任务栈

如果你指定了 taskAffinity ,想让某个 Activity 运行在指定的任务栈,但是如果没有在 Intent 里面指定 FLAG_ACTIVITY_NEW_TASK 这个 flag 的话,还是会在默认的栈里面(与启动这个 Activity 的 Activity 是同一个栈),不会在指定的栈里面。其他的诸如两个 App 之间通过隐式启动另一个 App 中的 Activity 时,最好也使用 FLAG_ACTIVITY_NEW_TASK,不然还是在当前 App 的任务栈中。

小知识,如果项目里面有多个任务栈,那么在按任务键 的时候,就能看到有多个缩略图(自己可以写demo试试),另外如果需要对比添加 FLAG_ACTIVITY_NEW_TASK 和不添加 FLAG_ACTIVITY_NEW_TASK 的情形的效果,需要卸载App 重新安装,课程里面就出现了这样的小插曲,不生效。

单独任务栈有什么作用?就比如你的图库,需要在单独的任务栈中,这样与普通业务不影响,我个人理解的是在诸如 singleTask 等模式下,假如需要清理掉栈顶那些 Activity 时,不会被清除掉。

任务栈在数据层面表现就是 ActivityStack ,它管理 ActivityRecord 。

1.3 FLAG_ACTIVITY_FORWARD_RESULT

FLAG_ACTIVITY_FORWARD_RESULT 的作用是当前 Activity 忽略 ActivityResult ,这样就会将结果传给上一级。举个例子,Activity A启动 B ,B 启动 C ,我们想在 C 结束的时候,将结果跳过 B 传递给 A ,此时我们只需要在 B 启动 C 的时候,在intent 中添加这个flag 就行(表示忽略那个结果):

1
2
3
Intent intent = new Intent(BBActivity.this, CCActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
startActivity(intent);

注意最后一行的 startActivity ,这里不能用 startActivityForResult 的,FLAG_ACTIVITY_FORWARD_RESULT 本来是忽略结果,startActivityForResult 是要结果,如果同时使用会报错。自己写的测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Activity A
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_a);

mButton = (Button) findViewById(R.id.btn);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(AAActivity.this, BBActivity.class);
startActivityForResult(intent, 100);
}
});
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Log.e("forwardresult","AAActivity requestCode:" + requestCode + ", resultCode:" + resultCode + "data = " + data.getStringExtra("haha"));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_b);

mButton = (Button) findViewById(R.id.btn);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(BBActivity.this, CCActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
startActivity(intent);
}
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_c);

mButton = findViewById(R.id.btn);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.putExtra("haha", "我是c的数据");
setResult(RESULT_OK, intent);

finish();
}
});
}

这样,当 Activity B 结束的时候,A 就能收到 C 关闭时候的数据 “我是c的数据” 了。注意:C 结束后,就显示出 B 了,只有手动关闭 B 页面后,数据才会传给 A 。

关于这个标志位,更多内容可以参考 FLAG_ACTIVITY_FORWARD_RESULT的使用 - 简书 (jianshu.com)

二、启动未注册的 Activity

启动未注册的 Activity 会报错,那么这个报错是在哪个地方报出来的?我们看到 Instrumentation 这个类,看到其中的 execStartActivity 这个方法,在里面会有启动 Activity 并对启动结果进行检查的逻辑:

1
2
3
4
5
int result = ActivityTaskManager.getService().startActivity(whoThread,
who.getBasePackageName(), who.getAttributionTag(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()), token,
target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
checkStartActivityResult(result, intent);

在 ActivityStarter 类中,执行 executeRequest 方法时,如果有错误,就会将错误结果体现到上述的 result 里面。最后,在 checkStartActivityResult 中通过 switch-case 去处理各种异常和正常结果。

Hook 目的就是为了改变原有流程。不管你是通过反射还是插桩都行。

2.1 思路

实现未注册的 Activity 的启动的思路如下图所示:

实现未注册的Activity 的启动思路

找Hook点的原则(尽量):

  • 找静态变量或者单例,这种可以直接用反射,还不轻易改变

  • public 修饰的,也不容易改变

一个原则: 在 startActivity 之后,AMS 检测之前将 目标Activity 替换成占位 Activity。在 AMS 检测之后,Activity 创建出来之前(感觉老师说的Activity生命周期之前是有问题的),将占位 Activity 替换回 目标 Activity。

2.2 实践

通过 AMS 启动 Activity 的时候,getService 获取 AMS 的代理的时候,是静态的的,并且 IActivityManager 是一个接口,所以可以使用动态代理。

前面说的 10 之前是 AMS ,10之后是 ATMS ,所以要注意适配。

还有个点需要注意,使用动态代理的时候,需要传入 ClassLoader

,可以使用当前线程的 ClassLoader :

1
Thread.currentThread().getContextClassLoader()

ActivityThread 中 H 类handler 本身的 callback 是 null ,又 Handler 中 dispatchMessage 的时候,msg 的callback 存在的话,就执行 msg 的callback ,其他的就不执行了;如果 msg.callback 是空的话,则先执行 Handler 本身的 callback.handlerMessage (msg) ,只有当这个返回true 的时候,才不会执行我们自定义的 handleMessage;false 的话,还是会继续执行 handleMessage 的,所以我们可以强行new 出来一个 Handler 的 callback ,只不过 callbac.handleMessage 返回false 即可。之后将这个 callback 通过 Hook 设置给 H 类即可。

这样 Hook 是没有风险的,因为流程并没有改变,,Handler 的callback 本身就是空的,我们 Hook 之后,handleMessage 还是会执行

ZygoteInit.main 方法执行会进入 Java 层

forkSystemServer 方法:

在 fork 之前会给一些 args ,比如很有意思的一个名字 –nice-name ,就是指的进程名,这里的值时 : system_server 。

Zygote.forkSystemServer 返回值问题:因为 fork 操作返回的就是 子进程的 pid ,所以,在父进程(Zygote 进程)中,返回的是 SystemServer 这个进程的 id ,假如是 6000 (反正肯定是 大于0的);而在 SystemServer 中而言,由于它没有子进程,所以它返回的是 0 。

所以,在 Zygote 执行 Zygote.forkSystemServer 之后,需要判断 if (pid == 0) 去区分当前是 SystemServer 进程,然后才执行 handleSystemServer 这个方法,这里面会执行 SystemServer 进程的 main 方法。

还记得前面说的,所有的进程开始执行的时候都是执行其 main 方法的

handleSystemServer 最终会调用到 ZygoteInit.java 类中的 zygoteInit 方法:

1
public static final Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges, String[] argv, ClassLoader classLoader)

在这个方法里面会执行 ZygoteInit.nativeZygoteInit(); 方法去启动 Binder 线程池,在 native 层最终执行的代码是这样的:

1
proc -> startThreadPool();

这也从侧面说明了, Zygote 进程 fork 出来的进程都会去创建 Binder 线程池。

获取到 SystemServer 的 Class ,然后通过反射调用其 main() 方法。这样就把 SystemServer 进程给启动起来了。

在 SystemServer 的 main 方法中,主要执行其 run 方法,主要操作是:

  • 创建 SystemServiceManager 对象

  • startBootstrapServices //启动引导服务 —— AMS 等

  • startCoreService //核心服务

  • startOtherService //其他服务,如 WMS 等

一个 App 可以有多个进程?那肯定可以,我们平时一个 App 就可以开启多个进程; 那么一个进程可以有多个 App 吗? 答案当然也是可以!我们可以用 sharedId 去实现 。

在 SystemServer 的 main 方法中,最终会创建 ActivityThread 对象,看到这里我蒙了。。。不是只有 App 的进程才会创建 ActivityThread 对象么? 不过呢,我们能从 ActivityThread 的 attach 方法中看到端倪,在里面会判断是否是 system(也就是是否是系统),在 SystemServer.main 里面这种情况下,我们肯定是 system 的:

1
2
3
4
5
6
7
8
9
//ActivityThread
private void attach(boolean system, long startSeq) {
if (!system) {

} else {

}
//省略无关代码
}

SystemServer 的main 方法执行的时候,会有创建和创建 Application 的 Context 以及创建 ActivityThread 的操作,和我们的 App 进程创建有点类似,老师的解释是,SystemServer 进程中有 App ,至于这个 App 是什么 App ,老师也说不知道。

SystemServerManager

startService

四大组件管理者以前说的都是 AMS ,但是 ANdroid 10 之后,这么说就不准确的,新增了一个 ATMS (ActivityTaskManagerService), 其中 ATMS 专门管理 Activity , AMS 管理其他三个以及其他的。

这里要注意 2 个概念, SystemServerManager 和 ServiceManager 的区别:

  • SystemServerManager : 主要负责 Service 的生命周期,比如 Service 的 onCreate、onStart 等

  • ServiceManager : 负责 Service 的管理,Service 创建后,要添加到 ServiceManager 中来,也就是注册。

SystemServer 中 startOtherService 中有几个值得注意的:

  • mActivityManagerService.systemReady 方法会启动 Launcher

  • startSystemUi 方法

ActivityStater.execute 里面会执行到 executeRequest 方法,这些方法非常重要,我们说的是 Activity 的栈管理 就是在 ActivityStarter 这个类里面。启动 Activity 的时候,一般会有 2 个 AcitivityRecord ,一个是启动 Activity 的信息,另一个是待启动的 Activity 的信息,比如 Activity A 启动 Activity B 。

Activity 启动过程中,会经过 ActivityStackSupervisor.java 的 startSpecificActivity 方法,这个方法很关键,会在里面判断是否存在目标Activity 所在的进程,如果存在才执行 realStartActivityLocked ;否则,启动 App 的进程(AMS 通过 socket 通知 zygote 创建进程)。

有个 主 zygote 和 从 zygote 的概念,这个需要理解下,因为之前我们讲过系统启动的时候,有几种配置,是 32 位 或者 64位,还是说32_64 (先尝试32位不行再 64位),以及 64_32(先尝试64位不行再 32位)

启动 Launcher 的时候,会执行到 ActivityThread.main 方法(其实和普通的App一样,Launcher也是个 App),在 ActivityThread.main 方法中也会调用到 attachApplication 方法,这是将App 的句柄发送给AMS,方便后续 AMS 向 App 发送消息。这个句柄是 ApplicationThread ,他是个 Binder 。

一定要自己去看源码,画时序图和流程图:

  • 时序图,哪个类的方法到哪个类的方法

  • 流程图: 整个过程做的重要事情

一、粗略过一遍

在内核里面没有进程和线程的区分的。

init 是用户空间鼻祖。zygote 是Java 进程的鼻祖,此前一直在 native 层。

Systemserver 进程里面大约有 90 多个进程。

init 进程中,做挂载、创建文件夹之类的操作

SetupSelinux 里面实现了 linux 这块的安全策略,

native 层这些知识,比如 init.rc 的解析等,看刘望舒的书籍就可以了,所以这里大概略过,总结一下init处理的重要事情:

  1. 挂载文件

  2. 设置 selinux –> 安全策略

  3. 开启属性服务,注册到 epoll 机制

  4. 解析 init.rc 文件

  5. 循环处理脚本,启动zygote 进程

  6. 循环等待,主要就是接受子进程的 SIGCHLD 信号,防止子进程变为僵尸进程

linux 中一切接文件,输入输出也是,在代码中写 system.out.println() 也是将内容写入到某个目录,然后再显示出来。

二、Zygote

Zygote 有一部分运行在 native 层,有一部分运行在 Java 层,从这里开始,我们后续的代码就进入了 java 层运行。

adb shell

kill 9 6158

上述命令就可以将 zygote 进程(其中6158是zygote 进程的进程号,需要确认下是不是这个)杀掉,然后 Android 就崩了。说明 Android 的关键进程是不能被结束的

每一个进程启动的时候,都是执行 main 方法

Android的运行环境(Android RunTime)是 zygote 给启动的,调用的是 runTime.start(xx,xx,xx) 方法,在 runtime.start()方法中,做了几件事情:

  • 通过 startVM 就是启动虚拟机

  • 之后通过 startReg 来注册 JNI ,

从上面看出来,我们在 zygote 进程中startVM 启动了虚拟机,虚拟机的作用: 进程管理。所以,直播课学员问的,先有进程还是先有VM 虚拟机,答案就明确了:先有进程,VM 只是进程里面的一段功能代码而已;如果从内核空间和用户空间来讲,VM 显然是在用户空间

每个 App 的内核空间,在物理上都对应同一块地方。

2.1 注册 JNI?

Java 与 Native 代码互相调用, 注册 JNI 就是将 Java 的本地方法和 native 方法关联起来。

进程是没有 Java进程 和 Native进程 的区分的,所以,zygote 执行的时候,不论是在 Native 层还是 Java 层,我们都说是在 Zygote 进程。看源码的时候,如何判断是否进入到了其他进程,就看是否 fork 了。

2.2 zygoteServer

它是一个 Socket ,为什么要用 Socket 而不用 Binder 进程间通信?

因为 Binder 是多线程的,fork 可能会导致死锁,所以这里用 Socket

Zygote 的 preload 加载资源啊(Android内部的资源com.android.internal.R.xx 之类的)、类啊(有个文件配置了需要预加载哪些类),这样在fork出来的 App 进程中不用去加载了,加快速度

关于预加载,老师的一张图很有说明性,复制下:

Android应用进程共享内存图

2.3 总结:zygote 进程启动干了什么事情?

native 层面:

  • 初始化运行环境,创建vm

  • 注册jni

  • 调用 zygoteinit.main 进入 Java 环境

Java 层面

  • 预加载–加快APp进程启动

  • 创建 server 类型的 socket 接收fork 新进程的信息

  • 通过 fork 创建 SystemServer 进程

  • 循环等待,等待 SystemServer 进程的 fork 其你去

三、fork (老师发的资料里面的补充内容)

3.1 fork 如何导致死锁

在 POSIX 标准中,fork 行为是这样的:复制整个用户空间的数据(通常使用 copy-on-write 策略)以及所有系统对象,然后仅仅复制当前线程到子进程,这就意味着,所有父进程中其他线程,到了子进程中都是突然政法掉了。

所以,如果非当前线程获取到了锁,fork完成后,当前线程去获取锁,会发现一直占用,无法获取到

3.2 fork 返回值

  • 返回 0 表示成功创建子进程,并且接下来进入子进程

  • 返回 pid > 0 ,表示成功创建子进程,并且继续执行父进程流程

  • 返回 pid < 0 创建子进程失败(可能内存不足等原因)

3.3 孤儿进程、僵尸进程

fork 调用后,父子进程交替执行,执行顺序不定:

  • 如果父进程先退出,子进程还没退出,则子进程的父进程会变为init进程(托孤,因为任何一个进程都必须有父进程)

  • 如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕捉到了子进程的退出状态才真正结束,否则这个子进程会变为僵尸进程(只保留一些退出信息供父进程查询)

一、储备知识

很多同学回答 Binder 是什么的时候,只会说 Binder 是一种进程间通信机制,这是不完善的,我们要说出以下 3 个点才算完整:

  • 机制:Binder 是一个进程间通信机制
  • 驱动: Binder 是一个虚拟物理设备驱动
  • 应用层:Binder 是一个能发起通信的 Java 类(Service里面就有用到Binder类)

Linux 一切皆文件

多进程的好处?

  • 突破内存限制,如图库内存占用过多
  • 功能稳定性,比如长连接,独立进程
  • 规避内存泄漏,比如 webview 正常使用会有内存泄漏
  • 隔离风险: 不稳定的功能放入独立的进程,比如dump 内存

为什么Android采用 Binder:

  • 性能: 只需要一次
  • 特点:基于C/S,易用性高(如共享内存使用很复杂,参考多线程共享变量,涉及的线程安全加锁)
  • 安全性,为每个App分配 UID,同时支持匿名和实名。公交车,只要能到那里,都能坐车;Binder 既有实名又有匿名,实名的服务是大家都可以访问,比如 AMS、WMS ,匿名服务,类似滴滴打车,你是不能直接联系到司机的,只能通过滴滴给你虚拟号。匿名服务只能通过代理去真正获取服务。(实名和匿名的区别在于是否注册)。

自己写的服务可以实名吗?是可以的,调用 API 即可

一个进程会分为 用户空间和内核空间。如果是 32位系统(总共有 2^32 这么大,即 4G),那么用户空间一般 3G,内核空间 1G

所有进程的内核空间都是映射到物理内存上是同一块空间,

内核空间是地球仪,物理内存是地球;进程1的用户空间是 月球仪器,对应的物理内存是月球;进程2的用户空间是火星仪,对应的物理内存是火星。

我们平时说的拷贝次数,指的就是 copy_from_user 或者 copy_to_user 这种系统调用,每调用一次就是所说的类似 Binder 只用拷贝一次这种说法。

为什么只关心这个系统的调用呢?这是因为在用户态和内核态切换非常耗时,它有上下文切换的,需要保存当前运行状态。

传统的IPC就是 copy_from_user ,接着再 copy_to_user ,2次

服务端和内核端,google 已经实现了,所以开发者只要开发 客户端。降低 Binder 的使用难度

为什么不从两个应用之间映射?还需要经过内核?

老师说的是,如果直接2个应用之间,就变成了 内存共享,google 做 Binder 就是因为内存共享比较难控制,所以宁愿用 Binder 来浪费一次性能。感觉说服力不够,得自己想想。

二、Binder

MMKV 也是使用 mmap 实现的。

2.1 AIDL 生成的类细节

AIDL 类似黄牛,帮我们代办一些事情,降低办事的复杂度。帮我们生成 Java 代码。AIDL 生成的 Java 代码,我们自己去手写也是一样的。

Proxy 是给客户端使用的,Stub 服务端使用。asInterface 判断如果是跨进程,那么返回的是代理对象,否则,返回的是本身。Binder 通信会创建 2个 Parcel ,一个是数据包,一个是结果包。

ServiceManager 也是个服务,它的 handle 句柄是固定的: 0 。Service 创建之后,可以去 ServiceManager 中注册,建立 Service 和 handle 句柄的映射关系。

客户端调用 transact 方法的时候,服务端那边就会响应 onTransact 方法。我们一般调用一个类的方法,可以写明全路径,比如一般是: com.xx.haha.Demo ,但是在 Binder 中,觉得传全路径过去占用的空间比较大,所以对方法做了精简,比如,用 1 代表 addPerson、2代表getPersonList 等,节约空间。这个体现在 Stub 中的那些静态常量。

onTransact 中,如果调用的方法没有返回值,则执行完就完了;否则,将执行结果写到 reply 中。 客户端调用服务端一般是同步的,所以计算过程客户端会挂起,所以一般要在子线程去做这种Binder 调用。如果要使用异步调用,就使用 oneWay

2.2 Intent 为什么不能传送大的数据

为什么只能 1M - 8k ?这是因为 Binder 是一个驱动,通过 mmap 创建的空间大小就是 1M - 8k (就是 1M - 2 * pageSize ,pageSize 是 4k),如果是异步的话 ,则是 (1M - 8k)/2 。

至于为什么是 1M - 8k ,那总得给个数字,不可能无限大。可能是为了充分利用存储空间。

mmp 最开始只会给 1 页,也就是 4k ,如果有需要才会扩充,最大就是上面说的 1M - 8k (oneWay 就减半)。

其实还有一个打包需要占用空间,所以真正可用的空间还不到 1M - 8k

补充视频里面有代码的详细讲解,不过目前面试用不到,先不看

线程的跨越是怎么实现的?

内存是不分线程的,可以在子线程和主线程都使用。

在子线程里面执行某个函数(比如调用 Handler 的 sendMessage 方法),这个函数就在子线程里面

事件变为内存了,MessageQueue.enqueueMessage(msg) 将这一块内存放入了 MessageQueue 了。

Looper.loop 是在主线程中,所以取出来的 Message 是通过 dispatchMessage 放在 主线程执行的

所以上述就是子线程切换到主线程的流程。

问题

有学员问存在以下几个条件:

  1. 所有 Looper 对象(不论主线程还是子线程的)中放 Looper 对象的 ThreadLocal 都是同一个对象

    这个没问题,因为 Looper 类中的 用于存放Looper 对象的 ThreadLocal 是 static final 的,整个 App 只有一个

  2. 那么看起来,所有线程也都公用同一个 Looper 对象了啊

看下 Looper 的源码:

1
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

所以学员说得没错, sThreadLocal 是整个 App 唯一的。但是,如果我们了解 ThreadLocal 的源码的话,就能得出如下的式子:

Thread1 -> 有自己的 ThreadLocalMap1 -> 存储的键值对样式 <sThreadLocal, Looper1>

Thread2 -> 有自己的 ThreadLocalMap2 -> 存储的键值对样式<sThreadLocal, Looper2>

能看到,在线程1中键值对存在ThreadLocalMap1 这个map 对象中, 样式是 <sThreadLocal, Looper1> ;线程2 中键值对存在 ThreadLocalMap2 这个 map 对象中,的样式是 <sThreadLocal, Looper2>

所以,根本原因在于: 每个线程都有自己的 ThreadLocalMap,虽然所有的线程中的不一样的 Looper 对象都有同一个 ThreadLocal 对象,但是这些线程有不一样的 ThreadLocalMap ,所以用同一个 ThreadLocal 对象作为 key 在不同的 ThreadLocalMap 中取值,取到的肯定是不同的 Looper ,这下明白了。

如何获取 Message

使用 Message.obtain() ,因为 Message 使用完成后,都会回收,所以我们可以一直使用,这是享元设计模式。这样就维持一个池子,避免内存抖动,防止 OOM。new 了必然会回收,回收了就可能有碎片。

在 Looper.loop 方法里面,msg.target.dispathcMessage(msg) 执行完后,在 for(; ;) 死循环最后一句的时候会调用 msg.recycleUnchecked(); 进行回收,将回收的msg 插入到池子的头部

Looper 死循环不会 ANR ?

不是同一个层面的东西。毫不相关的问题。点击 5秒没响应 ANR ,其实这个点击事件它也是一个 Message。

HandlerThread

HandlerThread 存在的意义:

  • 方便使用

  • 线程安全,getLooper 的线程安全问题

wait 会释放锁,这样其他的函数才能获取到锁执行 notify 操作。notify 不会释放锁, 要等 synchronized 代码全部执行完才释放锁(notify操作可能不在synchronized 代码块最后 )。

IntentService

处理后台耗时任务。一个任务分为多个子任务,子任务按照顺序执行完成后,任务才能算完成,这时候可以使用 IntentService ,这样可以保证所有的子任务在同一线程执行。关于要执行的多个任务,代码写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 请求1
Intent i = new Intent("cn.scu.finch");
Bundle bundle = new Bundle();
bundle.putString("taskName", "task1");
i.putExtras(bundle);
startService(i);

// 请求2
Intent i2 = new Intent("cn.scu.finch");
Bundle bundle2 = new Bundle();
bundle2.putString("taskName", "task2");
i2.putExtras(bundle2);
startService(i2);

startService(i); //多次启动

具体可以参考Android多线程:IntentService使用教程(含实例讲解)-腾讯云开发者社区-腾讯云 (tencent.com)

类似的思想也体现在 Fragment 的管理上,:

在 FragmentPagerAdapter 中,会有 instantiateItem 方法。里面会执行 mCurTrasaction.attach(fragment) ,去 attach fragment ,但是这个 attach 方法会立即执行么? 不会的,这时候只会加入list 中,等 commit 的时候才会一起执行。

并且可以保证 attach 一定是在 detach 后面执行,也是用 msg 来实现

具体得自己再去看下源码,老师讲得不详细。

glide 中的巧妙使用

假设有这样一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
fun with(){
val fragmentManager = supportFragmentManager
val f: MyFragment? = fragmentManager.findFragmentByTag("tag") as MyFragment?
if (f == null) {
fragmentManager.beginTransaction()
.add(MyFragment(), "tag")
.commitAllowingStateLoss()

//通过某种途径得到commit 时候用到的 handler
//handler.sendMessage(removeFragmentMessage)
}
}

如果连续调用 2 次,会怎样?可能你会想,第一次已经创建了 MyFragment 实例了,第二次直接通过 findFragmentByTag 就能找出来了。但是不是的,可能会创建 2 次 MyFragment 。这是因为最后在 commitAllowingStateLoss 的时候,最终是将这个操作作为一个 msg 扔到 Handler 里面。所以,第二次调用 with 的时候,第一次不一定执行完!所以,可能执行 2 次。 这个在 glide 的源码中有体现:

  1. 创建一个 HashMap ,以 tag 为key ,第一次的时候,首先从 hashmap中获取,如果没有,再从 fragmentManager 中获取,如果还没有,就创建一个,然后将其加入到 HashMap 中

  2. 第二次的时候,首先判断 HashMap 里面有没有,如果有的话,就忽略,因为这个 Fragment 最终肯定会被创建出来,此时应该是commit 的中提交到 handler 中的 msg 还没执行。

  3. 在上述代码的注释部分,可以看到最终通过handler 发送一个消息,这个消息用于将 fragment 从 HashMap 中移除 Fragment ,防止内存泄漏。

第三点为什么可以这么做呢?因为同一个handler ,commit 的时候发的消息,肯定在前面,后续用于移除 msg 的 msg 肯定在后面,这样很巧妙地保证了用完fragmengt 就从 HashMap 中移除。

上述代码其实就是 glide 中的一段代码设计思想,具体可以参考 Glide生命周期管理 - 简书 (jianshu.com) 中 2.0.1 创建RequestManagerFragment 这个章节。

避免内存抖动:共享内存池。

ActivityThread 的 main 函数不会退出,这是因为 Looper.loop

ActivityThread 扮演后台的角色,Looper.loop 扮演的是心跳的角色

Android 中 App 的所有事务都是 Message ,所以,当我们碰到异常的时候,看到的 Log 都是从 ActivityThread 中的 loop 处开始的

我们根据 Looper.loop 中打印日志的时间,来判断每个 Message 的执行耗时,这是 BlackCanary 的原理,能判断卡顿

没有 Msg 的时候,就休眠了,此时 ANR ?其实是混淆概念,这是2个事情, ANR 的含义是事务在定时范围内没有完成,执行事务前埋雷,执行完成后挖出雷,就不会爆炸;如果到了时间还没挖雷,雷就爆了。

什么是 epoll ?

上层有 n 个 I/O 事件,要如何才能去处理多个流。其实系统底层用一个线程死循环,去判断(多线程去判断更慢)。

epoll 与线程之间的数量没有对应关系,不用搞混。epoll 机制是需要注册的,要说明针对哪个事件去 阻塞和唤醒。

同步屏障(消息屏障)

屏障消息就是target 为 null 的消息。一般的消息都是通过 Handler 放到 MessageQuue 。消息有 3 种 :

  • 同步消息
  • 异步消息:async 标记为 true ,普通的同步msg 没人设置 async 标记
  • 同步屏障消息

如果打印出 skip 30 frames ,the Application may be doing too many work …. 之类的错误,那就说明在主线程阻塞了,某个 msg 耗时过长,导致里面 30 个异步消未能执行。

在 ViewRootImpl 类的 scheduleTraversals 方法中,执行了 mHandler.getQueue().postSyncBarrier() 方法,在 MessageQueue 中添加了同步屏障。接着,在这个方法里面最终会执行到 performMeasure 、performLayout 和 performDraw 方法,这就很熟悉了,就是平时说view 绘制的步骤,或者说自定义View 尤其要关注的回调。

post 同步屏障的时候,会把屏障消息放到 MessageQueue 的最前面吗?不是的,只是普通的以当前时刻放入

一个线程一个 Looper 是由 static final 的 ThreadLocal 保证的,并且prepare 只能搞一次,第二次判断有 looper 的时候就报错了,不能多次 prepare 。

每个安卓应用都有自己的 vm,所以,每个应用也都有自己的main函数,那么这个 main 函数在哪里呢?在 ActivityThread 里面(省略部分无关代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal {

public static void main(String[] args) {
//省略无关代码
// Install selective syscall interception
AndroidOs.install();

Looper.prepareMainLooper();

ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);

if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}

Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
}

Looper.loop是个死循环,如何才能终止呢?通过MessageQueue.next获取到一个null类型的Message,就退出了​,代码如下:

1
dfsda

messagequeue.quit 也可以达到退出 Looper.loop 死循环的效果。

Message 就是一块内存,内存不分线程(只区分进程吧),因此 Handler 这个过程可以看做是内存共享, 不同线程之间的共享 Msg 这块内存。因此某种程度上,可以

单链表实现的优先级队列

为什么要把 Looper 的初始化只能在 prepare() 方法中进行,而不能让人 new Looper() ?

为什么一个线程只能有一个 Looper ,因为 Looper 存储在 ThreadLocal 中,而我们知道key 是 ThreadLocal 的对象,是唯一的,所以 value 也是唯一的。并且,prepare 的时候,首先通过 threadLocal.get 去获取 Looper ,发现不空就报错,只能 Looper 为空的时候才能 prepare 。

Looper 维持一个 MessageQueue 。一个线程只能创建一个 Looper ,一个 Looper 创建一个 MessageQueue。

Handler 内存泄漏问题的原因?为什么其他内部类没有说有这个问题?比如说 RecyclerView 的 Adapter 里面一般会有一个 ViewHolder 内部类,它为什么不会内存泄漏?

内存泄漏的原因是生命周期不一致。

msg delay 了 20s 才执行,msg 持有的了handler ,handler 是非静态内部类,持有外部对象,所以可能导致内存泄漏。

子线程new handler 要做什么准备?

需要prepare ,有 Looper 才行。

子线程中维护 Looper,消息队列无消息的时候的处理方案是什么?有什么用?

需要调用 MessageQueue.quit, 这时候会将 mQuitting 标志位置为 true 。由于之前已经没有消息了,那么此时应该是处于 nativePollOnce 无限睡眠阶段,quit 方法同样会去调用 nativeWake 方法唤醒,这样就会继续 loope 方法,在里面判断到 mQuitting 为true ,于是return 了一个 null 类型的 Msg ,从而导致 Looper.loop 退出死循环

MessageQueue 为什么不设置容量上限?如果设置了,那么系统的消息都不能进去了,整个系统就死掉了。

两个方面的阻塞:

  • Message 还不到时间,时间到了会自动唤醒

  • MessageQueue 为空,nextPollTimeoutMillis 值为 -1 ,就会进入 nativePollOnce 的无限睡眠了。在enqueueMessage (有新的 Msg 加入的时候)的时候,才会被唤醒(调用nativeWake)

线程阻塞了,所以cpu 就不会来调度它了,节省了cpu 性能。

既然可以存在多个 Handler 往 MessageQueue 中添加数据(发消息的时候,各个Handler 可能在不同的线程),那么它的内部是如何保证线程安全的?

  • 首先,1个线程只有一个 MessageQueue

  • 其次,MessageQueue 操作加锁了。在 MessageQueue 中,不论是 enqueue 方法,还是 next 方法,都会在里面锁代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 boolean enqueueMessage(Message msg, long when) {
//省略无关代码

synchronized (this) {
//省略无关代码
}
}


Message next() {
//省略无关代码
for (;;) {
synchronized (this) {
//省略无关代码
}
}
}

从代码可以看出,锁的是整个 MessageQueue 对象,所以,对于同一个 MessageQueue 来说,每次只能一个线程存/取。

为什么取的时候也要加锁?明明都是从头取?

这是因为你取的时候,我这边有线程正在加入新的 Msg ,此时就存在同步问题。

一、前情回顾

GC 的时候,Java 线程、Native 线程都会中断,中断就会卡顿,会影响性能。

上节课提到,执行一次 GC 后,间隔5s 或者 500ms 再次执行一下GC ,这样基本上能触发 GC ,这是什么原理呢?在 Android 5.0 以前,System.gc() 基本上就能触发 GC 行为,它的代码是这样的:

1
2
3
4
5
6
7
8
/**
* Indicates to the VM that it would be a good time to run the
* garbage collector. Note that this is a hint only. There is no guarantee
* that the garbage collector will actually be run.
*/
public static void gc() {
Runtime.getRuntime().gc();
}

但是在 5.0 及以后,调用 System.gc 或者 runTime.gc 不一定会触发 GC 了,这是因为在 5.0 及以后的 gc 方法里面会有标记判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Indicates to the VM that it would be a good time to run the
* garbage collector. Note that this is a hint only. There is no guarantee
* that the garbage collector will actually be run.
*/
public static void gc() {
boolean shouldRunGC;
synchronized(lock) {
shouldRunGC = justRanFinalization;
if (shouldRunGC) {
justRanFinalization = false;
} else {
runGC = true;
}
}
if (shouldRunGC) {
Runtime.getRuntime().gc();
}
}

可以看到,是否执行 gc 还有赖于 justRanFinalization 变量,这个变量在哪里赋值为true 呢?是在 runFinalization 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Provides a hint to the VM that it would be useful to attempt
* to perform any outstanding object finalization.
*/
public static void runFinalization() {
boolean shouldRunGC;
synchronized(lock) {
shouldRunGC = runGC;
runGC = false;
}
if (shouldRunGC) {
Runtime.getRuntime().gc();
}
Runtime.getRuntime().runFinalization();
synchronized(lock) {
justRanFinalization = true;
}
}

从上述代码可以看出,我们直接调用 System.gc 并不会调用到 Runtime.getRunTime().gc() ,只是做了个标记将 runGc 设置为 true ,然后在下一次 GC 的时候,就能真正 GC 了。那为什么要间隔5s 或者 500ms 呢?这个跟线程调度、线程的中断状态有关。

所以,上一节课也提到,内存监控自己做 GC 的时候,也可以使用 runFinalization() + runTime.gc 的方式去GC,这样也是可以的。

由于路哥在课程里面对这个讲得不是太清晰,更详细的内容可以参考

二、 Alpha 源码讲解

面试中怎么讲:

  • Executor :线程池,调用方可以自己实现,也可以使用默认的,参数怎么设置

  • Task : 是个线程,有任务自己的状态、有自己的子任务

  • 接口: 执行前、执行后、失败

仿照 Alpha 的框架没有实现 DAG 算法,是个弊端

1、主要要实现的功能

线程池、线程等待、线程切换、主进程/子进程

三、总结之前的启动框架

内存、ANR 、启动,这3个点,性能优化就能把握了

四、卡顿

根据前面说的,能够在 Activity 的 onWindowFocusChanged 方法中停止方法的采集,因为这个时候恰好是 window 切换,要么是新的 window 启动了,要么是关闭了某个 window,所以,如果我们要监测到第一个 Activity 的耗时,可以从Application 的 onCreate 中去start,在 MainActivity 中去 stop 这个 trace 去实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
//Application
class MyApplication : Application {
override fun onCreate(xx) {
Debug.startMethodTracingSampling(tracePath, bufferSize, untervalUs)
}
}

//MainActivity
class MainActivity : Activity {
override fun onWindowFocusChanged(hasFocus) {
Debug.stopMethodTracing()
}
}

你可以获取到更细粒度更精准的耗时,比如在 ListView 中 getView 方法,在方法调用前start ,在调用完成之后,调用 stop 。

根据上述获取的 trace 文件,拖到 AS 中就能看到方法的耗时,线程相关内容等。

systrace 比上面的方法更加精准。systrace 就是个 shell 脚本嘛,他需要使用 python 执行,比如定位到 SDK 目录下(platform-tools)的 systrace 时,调用 python systrace.py 即可。systrace 可以在 chrome 上打开,使用地址:

chrome://tracing

线上如何做卡顿。卡顿的原理。自己定义阈值,超过某个值就说是卡顿,比如 1000ms ,

CountDownLatch 实现让主线程等待所有的初始化完成才继续执行的。

第一个版本仿照 Alpha ,怎么保证顺序?

第二个版本 DAG (有向无环图)保证顺序

今天主要从组件化 + 责任链 的角度去讲启动。主要讲代码的可维护性、解耦,跟启动速度没什么关系

有些错误的做法是 在宿主的 Application 中初始化所有的组件。

app 启动包括三个部分: Application 过程、 Splash 过程、 Mainactivity 过程(第一个 Activity)

有可能在 Application 中用户同意了某些条款和不同意的情况下初始化还不一样。工信部要求App 这样。所以可能宿App 中定义一个 BaseApplication 接口,除了其他方法之外,在其中至少定义几个方法:

  • 不论同不同意用户协议都需要执行的方法

  • 用户同意了协议才执行的方法

  • 用户拒绝后执行的方法

然后,让每个组件去实现这个接口,实现上述的方法,自己实现各自的逻辑。可以利用 autoService 将这些组件的 Application 注册到宿主中,并不需要自己去遍历添加到list 里面。这是在编译期间将这些组件的 Application 添加到宿主的list 中的,所以对于运行效率是没有影响的。

在 autoService 的 gradle 中实现的,可以看看他的 gradle 文件

compose 是一套新的 UI 体系,和以前的 View 和 ViewGroup 有啥区别?

前者是声明式的,数据驱动的,后者是命令驱动的

compose 和 databinding 的区别:

  • databinding : 只能绑定属性,只能绑定xml 属性中的值

  • compose 可以根据数据的变化改变视图的结构

只有在主进程才需要执行我们的逻辑,有些SDK 可能会开启自己的进程,所以需要判断下。

老师说写 SplashActivity ,在 manifest 中给这个 SplashActivity 设置 theme ,在 theme 里面设置 background ,但是其实前面说启动优化的时候,给整个 App 设置 theme ,然后设置 android:windowSplashscreenContent 属性就好了,注意区分和甄别。

SplashActivity 启动 MainActivity ,那么 SplashActivity 在什么时候可以结束?我们知道,在启动过程肯定是 SplashActivity.onPause -> MainActivity.onCreate -> MainActivity.onResume -> SplashActivity.onStop 这样的顺序执行的,当然,MainActivity的其他无关的回调省略了。所以,在 SplashActivity 的 onStop 的时候, SplashActivity 就没有什么意义了,所以可以在 SplashActivity 的 onStop 回调中执行 finish 结束。

为什么这样做呢?因为如果在 SplashActivity 中 start MainActivity的时候直接 finish SplashActivity ,某种情况就可能会出现白屏,或者 MainActivity 崩溃了,就看不到我们 App 了

讲到 1 小时40分钟的时候直接听不懂了,放弃,后续有时间看