前言
作为一个应用工程师,除了写一些业务代码,性能优化也是我们需要关注的点!
如果想要去做启动优化,那么去了解启动过程就是一个绕不过去的坎儿。
那么除了关于启动过程的那些代码,我们还应该去知道什么呢?
卷
一、多进程那些事儿
在大家很早学习 Android 的时候,想必就知道,每一个 Android App 就代表着一个进程。
1. 为什么要开启进程?
为什么要开启一个新的进程呢?
在 Linux 中,线程和进程可没多大区别,内核并没有给线程准备特别的调度算法或者特殊的数据结构,相反,线程被视为一个与其他进程共享某些资源的进程。
看到了吗?线程之间可是会共享资源的,比如地址空间,你肯定不想发送微信的时候,其他应用都能知道,你发了什么吧。
2. 如何开启一个应用进程
想要开启一个应用进程可不容易,大家可能都听说过,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 。
3. Android特有的多进程传输方式
你以为要分析 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机制?》
❞
二、dex那些事儿
我们的知道,解压一个应用的 apk 文件,里面可能会有若干个 dex 文件。
apk解压以
后它是经工具软件将所有的 Java 字节码文件(.class)转化形成的。
Dex文件
这些 dex 文件怎么就可以直接运行了呢?
转化过程
从图片中我们也可以看出来了,需要根据情况讨论,因为 Android 的虚拟机分为 Dalvik 和 ART。
对于 Dalvik,安装过程会提取出 .odex ,不过这个只是优化过的字节码文件,最后,在运行过程,Dalvik 还要通过 Jit(即时编译) 还需要转化成机器可以识别的机器码。
对于 ART,它会在安装过程中,解析成 .oat 文件,这个文件装的可不是字节码,而是实实在在的机器码,少了 Jit,整个运行速度和启动速度都大大加快了。
不过呢,在ART中,整个安装过程和应用升级都比较耗时,所以,在 Android 7.0 以后,既采用了 JIT 又采用了 AOT,简单来说就是:
第一次启动采用 JIT,将热点函数包含的 dex 解析成字节码。 等到应用空闲的时候,再执行 AOT 过程,进行编译。经过了这一大串, dex 文件就变成了机器可以识别的机器码了,这里再提醒一下,Android 系统可不是直接识别 .class 文件的,它需要将 .dex 映射到内存中,并通过 PathClassLoader 和 DexClassLoader 将 APP 中的类加载到内存里。
三、进入Android启动过程
从上面我们已经知道了,我们的应用启动入口在 ActivityThread#main() 方法。
1. 启动过程中的通信机制
在正式了解启动过程之前,我想我们还得了解一下启动过程的通信机制,这也是启动过程的主线,虽然我知道你们已经迫不及待了!
等待
稍微了解过启动过程的同学应该都知道 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 的通信正如图上所描述的那样。
2. 入口ActivityThread
终于到了代码解释环节了!
进入 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 对象传了过去。
2. 进入AMS
点进 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 了,没毛病!
3. 进入ApplicationThread
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()4. 创建Application
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() 就是这个时候回调的。
5. 初始化ContentProvider
这里没什么好说的,只有一点, ContentProvider 的初始化时机要早于 Application#onCreate() 方法。
因为之前我就出现过在 ContentProvider#onCreate() 调用了某个 SDK,而这个 SDK 在 Application#onCreate() 才回完成初始化,结果就闪退了。
表情
6. 调用 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内核设计与实现》
❞
查看更多关于Android启动这些事儿,你都拎得清吗?的详细内容...