作为一个应用工程师,除了写一些业务代码,性能优化也是我们需要关注的点!
如果想要去做启动优化,那么去了解启动过程就是一个绕不过去的坎儿。
那么除了关于启动过程的那些代码,我们还应该去知道什么呢?
卷
在大家很早学习 Android 的时候,想必就知道,每一个 Android App 就代表着一个进程。
为什么要开启一个新的进程呢?
在 Linux 中,线程和进程可没多大区别,内核并没有给线程准备特别的调度算法或者特殊的数据结构,相反,线程被视为一个与其他进程共享某些资源的进程。
看到了吗?线程之间可是会共享资源的,比如地址空间,你肯定不想发送微信的时候,其他应用都能知道,你发了什么吧。
想要开启一个应用进程可不容易,大家可能都听说过,Linux 进程的创建都通过 fork()
方法,Android 自然也是同理,所有的应用进程的父进程都是 Zygote
进程,它是 Android 系统的两大之主进程之一。
image.png
点击 App 图标后,Launcher
进程会通知 AMS(ActivityManagerService)
,AMS
就会调用自身的 startProcessLocked
方法,接着会调用 Process#start()
方法,透过层层嵌套,我们可以看到连接 Socket
的地方:
public static ZygoteState connect(LocalSocketAddress address) throws IOException {
//...
final LocalSocket zygoteSocket = new LocalSocket();
try {
zygoteSocket.connect(address);
zygoteInputStream = new DataInputStream(zygoteSocket.getInputStream());
zygoteWriter = new BufferedWriter(new OutputStreamWriter(zygoteSocket.getOutputStream()), 256);
} catch (IOException ex) {
//...
}
//...
return new ZygoteState(zygoteSocket, zygoteInputStream, zygoteWriter, Arrays.asList(abiListString.split(",")));
}
Zygote
进程会开启 Socket
等待连接,连上了以后,最终会触发它的 Zygote#forkAndSpecialize()
方法:
static int forkAndSpecialize(int uid, int gid, int[] gids, int runtimeFlags,
int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose,
int[] fdsToIgnore, boolean startChildZygote, String instructionSet, String appDataDir,
boolean isTopApp, String[] pkgDataInfoList, String[] whitelistedDataInfoList,
boolean bindMountAppDataDirs, boolean bindMountAppStorageDirs) {
ZygoteHooks.preFork();
int pid = nativeForkAndSpecialize(
uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, fdsToClose,
fdsToIgnore, startChildZygote, instructionSet, appDataDir, isTopApp,
pkgDataInfoList, whitelistedDataInfoList, bindMountAppDataDirs,
bindMountAppStorageDirs);
//...
ZygoteHooks.postForkCommon();
return pid;
}
可以看到,一个新的进程就创建好了,并返回进程ID。
应用进程创建完了,应用还得和 AMS
、PMS
等其他服务或者进程通信啊,所以还得创建 Binder
线程池,不然就没法和外界交流。
接下来,可到了重点部分了,系统会调用 RuntimeInit#findStaticMain()
方法,该方法最终会调用一个 Runnable
方法:
static class MethodAndArgsCaller implements Runnable {
//...
public void run() {
try {
mMethod.invoke(null, new Object[] { mArgs });
} catch (IllegalAccessException ex) {
// ...
} catch (InvocationTargetException ex) {
// ...
}
}
}
mMethod
就是 ActivityThread#main()
方法,参数都是之前传递进去的,最终通过反射去启动我们的 ActivityThread
。
你以为要分析 ActivityThread#main()
,不,我们没有。
上面我们谈到了 Socket,但 Binder 才是 Android 中特有的多进程通信方式,并且在下面会被我多次提及。
往简单了说,因为 Android 中的进程的内存地址是各自独立的,但是它们都得通过内核的限制与硬件层进行交流。
如果不存在内核,就会发生很多糟糕的情况,比如说,一共有8GB的运行内存,起点读书说我全要了,剩下的其他应用还怎么玩!
生气
多进程通信也得通过内核,分给进程的一般都是虚拟地址,每一段虚拟地址都有实际的物理地址与之对应。当发生进程通信时,一般都会将数据从进程A的用户空间(用户可以操作的内存空间)复制到内核的缓冲区,再从内核的缓冲区复制到进程B的用户空间。
多进程通信
但是 Binder
就不同了,Binder
会通过 mmap
将内核中的虚拟地址和用户空间的虚拟地址做映射,如果进程B使用了 Binder
,当数据从进程A复制到内存缓存区的时候,整个通信过程就结束了,因为使用了内存映射,数据直接映射到了进程B的内存空间。
Binder通信
看到这儿,相信大家对 Binder
有了基础的印象了,再来看一下它的使用流程即可。
Binder使用流程
Binder的实现是典型的 C\S
的结构:
Client
:服务的请求方。Server
:服务的提供方。Service Manager
:为Server
提供Binder
的注册服务,为Client
提供Binder
的查询服务,Server
、Client
和Service Manager
的通讯都是通过Binder。Binder驱动
:负责Binder通信机制的建立,提供一系列底层支持。从上图中,Binder
通信的过程是这样的:
Server
在 Service Manager
中注册:Server
进程在创建的时候,也会创建对应的Binder实体,如果要提供服务给 Client
,就必须为 Binder
实体注册一个名字。Client
通过 Service Manager
获取服务:Client
知道服务中 Binder
实体的名字后,通过名字从 Service Manager
获取 Binder
实体的引用。Client
使用服务与 Server
进行通信:Client
通过调用 Binder
实体与 Server
进行通信。以上的知识可能有点浅显,权当抛砖引玉,如果想要深入的学习,大家可自行了解。
看到这里,相信大家可能会有一个疑惑,Binder
是 Android 中特有的高效跨进程传输方式,Zygote 为什么没有选择 Binder
而是选择 Socket
作为跨进程通信方式呢?
其实我也有点疑惑,大家可以看看下面的解答:
❝《知乎:zygote进程为什么不启用binder机制?》
❞
我们的知道,解压一个应用的 apk 文件,里面可能会有若干个 dex 文件。
apk解压以
后它是经工具软件将所有的 Java 字节码文件(.class)转化形成的。
Dex文件
这些 dex 文件怎么就可以直接运行了呢?
转化过程
从图片中我们也可以看出来了,需要根据情况讨论,因为 Android 的虚拟机分为 Dalvik 和 ART。
对于 Dalvik,安装过程会提取出 .odex
,不过这个只是优化过的字节码文件,最后,在运行过程,Dalvik 还要通过 Jit(即时编译) 还需要转化成机器可以识别的机器码。
对于 ART,它会在安装过程中,解析成 .oat
文件,这个文件装的可不是字节码,而是实实在在的机器码,少了 Jit,整个运行速度和启动速度都大大加快了。
不过呢,在ART中,整个安装过程和应用升级都比较耗时,所以,在 Android 7.0 以后,既采用了 JIT 又采用了 AOT,简单来说就是:
经过了这一大串,dex
文件就变成了机器可以识别的机器码了,这里再提醒一下,Android 系统可不是直接识别 .class
文件的,它需要将 .dex
映射到内存中,并通过 PathClassLoader
和 DexClassLoader
将 APP 中的类加载到内存里。
从上面我们已经知道了,我们的应用启动入口在 ActivityThread#main()
方法。
在正式了解启动过程之前,我想我们还得了解一下启动过程的通信机制,这也是启动过程的主线,虽然我知道你们已经迫不及待了!
等待
稍微了解过启动过程的同学应该都知道 ActivityThread
、ApplicationThread
和 ActivityManagerService
这三个角色:
角色 | |
---|---|
ActivityThread | (下称AT):应用的启动入口,进行应用启动的工作。 |
ActivityManagerService | (下称AMS):Android系统中最重要的系统服务之一,负责四大组件的启动、管理和调度,同时也管理应用进程。 |
ApplicationThread | AMS 与 AT 通信的桥梁,AT 的内部类。 |
上面我们了解过 Android 系统的两大支柱进程之一的 Zygote 进程
,另外一个就是 SystemServer
进程。
AMS
就处于 SystemServer
进程,还有很多大家耳熟能详的服务都在这里边,AMS
基于 Binder
实现的,所以 ActivityThread
能够在应用进程联系远在 SystemServer
进程的 AMS
。
那 AMS
如何联系 ActivityThread
呢?
巧了,也是 Binder
,它的实现类是 ApplicationThread
,不过它是一个匿名 Binder
(没有在 Server Manager
注册的 Binder
),之所以这么设计,我想更多的是基于安全方面考虑的。
ActivityThread通信.png
AT
和 AMS
的通信正如图上所描述的那样。
终于到了代码解释环节了!
进入 main
方法:
public static void main(String[] args) {
//...
Looper.prepareMainLooper();
//... 省略
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
//...
Looper.loop();
}
重点是 ActivityThread#attach()
方法,system
变量传递的是 false
:
private void attach(boolean system, long startSeq) {
// ...
if (!system) {
//. ...
RuntimeInit.setApplicationObject(mAppThread.asBinder());
final IActivityManager mgr = ActivityManager.getService();
try {
mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
// ...
}
// Watch for getting close to heap limit.
BinderInternal.addGcWatcher(new Runnable() {
@Override public void run() {
//...
}
});
} else {
//...
}
//...
}
我们可以看到这个方法先是直接获取 AMS
,之后就直接将 ApplicationThread
对象传了过去。
点进 ActivityManagerService#attachApplication()
方法,该方法又调用了 ActivityManagerService#attachApplicationLocked()
方法,简化一下:
private final boolean attachApplicationLocked(IApplicationThread thread, int pid) {
ProcessRecord app;
if (pid != MY_PID && pid >= 0) {
synchronized (mPidsSelfLocked) {
app = mPidsSelfLocked.get(pid); // 根据pid获取ProcessRecord
}
}
...
ApplicationInfo appInfo = app.instrumentationInfo != null
? app.instrumentationInfo : app.info;
thread.bindApplication(processName, appInfo, providers, app.instrumentationClass,
profilerInfo, app.instrumentationArguments, app.instrumentationWatcher,
app.instrumentationUiAutomationConnection, testMode, enableOpenGlTrace,
isRestrictedBackupMode || !normalMode, app.persistent,
new Configuration(mConfiguration), app.compat,
getCommonServicesLocked(app.isolated),
mCoreSettingsObserver.getCoreSettingsLocked());
...
return true;
}
注意一下,上面的方法在 AMS
中,就直接调用了 ApplicationThread#bindApplication()
方法了,这可是跨进程。
之前我们也提过了,ApplicationThread
也是 Binder
,并且我们也没有发现 ApplicationThread
向 Service Manager
注册 Binder
的任何代码,这也更加证实它是一个匿名 Binder
,不信,我们可以看一下 ApplicationThread
的代码:
private class ApplicationThread extends IApplicationThread.Stub {
//...
}
public class ActivityManagerService extends IActivityManager.Stub
implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
// ...
}
ApplicationThread
和 ActivityManagerService
貌似都改成 AIDL 去实现 Binder
了,没毛病!
ApplicationThread
自己并没有处理,而是交给了 H
的实例,从下面的代码中,我们就能看出 H
是一个 Handler
:
public final void bindApplication(
//... 参数省略
) {
//...
AppBindData data = new AppBindData();
//...
sendMessage(H.BIND_APPLICATION, data);
}
H
也是 ActivityThread
的内部类,于是直接调用 ActivityThread#handleBindApplication()
方法,简化一下:
private void handleBindApplication(AppBindData data) {
mBoundApplication = data;
Process.setArgV0(data.processName);//设置进程名
// ...
//获取LoadedApk对象
data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
// ...
// 创建ContextImpl上下文
final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
//...
try {
// 此处data.info是指LoadedApk, 通过反射创建目标应用Application对象
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
// 初始化ContentProvider
installContentProviders(app, data.providers);
mInitialApplication = app;
// ...
mInstrumentation.onCreate(data.instrumentationArgs);
//回调onCreate
mInstrumentation.callApplicationOnCreate(app);
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
}
我们可以看见,主要做了以下几件事:
ContextImpl
Application
ContentProvider
Application#onCreate()
data.info
的类型是 LoadedApk
,它保存了很多跟 Apk 相关的信息。
进入 LoadedApk#makeApplication()
方法,这是 Application
的创建方法,注意第二个参数传了一个 null
:
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
Application app = null;
//...
try {
final java.lang.ClassLoader cl = getClassLoader();
// ...
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
// ...
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
} catch (Exception e) {
//...
}
if (instrumentation != null) {
// instrumentation为空,所以走不进这个方法
try {
instrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
//...
}
}
return app;
}
实际的创建 Application
方法又交给了 mActivityThread.mInstrumentation
,它的类型 Instrumentation
,这个类可是一个大管家,无论是 Application
还是 Activity
,最后都会交给它来处理:
public Application newApplication(ClassLoader cl, String className, Context context) {
// 利用反射创建的Application
Application app = getFactory(context.getPackageName())
.instantiateApplication(cl, className);
app.attach(context);
return app;
}
Application
创建完成后还会调用 Application#attach()
方法,在这个方法,我们看到了熟悉的方法:
final void attach(Context context) {
attachBaseContext(context);
// ...
}
常用的方法 Application#attachBaseContext()
就是这个时候回调的。
这里没什么好说的,只有一点,ContentProvider
的初始化时机要早于 Application#onCreate()
方法。
因为之前我就出现过在 ContentProvider#onCreate()
调用了某个 SDK,而这个 SDK 在 Application#onCreate()
才回完成初始化,结果就闪退了。
表情
Application#oncreate()
回到步骤3中的方法,mInstrumentation
的类型是 Instrumentation
,它也是通过反射创建的,最终会执行 Instrumentation#callApplicationOnCreate
方法:
public void callApplicationOnCreate(Application app) {
app.onCreate();
}
在这个方法中,就可以看到我们的 Application#onCreate()
方法得以执行,这也是我们通常用来初始化 SDK
的地方。
在这完成以后,就会通过 AMS
启动第一个 Activity
,还是同样的通信方式,就不和大伙继续展开了。
看到这儿,相信大伙儿对应用已经有了初步的认识,如果有什么争议的地方,评论区见!
再谈一些关于知识点的事,可能大家会认为学习一个个知识点是比较枯燥的事。
比如去了解Linux内核的一些知识、去了解Apk的组成、去看应用的启动流程,但是当你把这些看似不连贯的点都能够连贯起来的时候,你就会发现这还是挺有意思的!
❝《Dalvik,ART与ODEX相爱相生》
《说一说Android的Dalvik,ART与JIT,AOT》
《Android AMS 与 APP 进程通信》
《理解Application创建过程》
《深入理解Android虚拟机》
《Linux内核设计与实现》❞
Copyright© 2013-2019