Android启动这些事儿,你都拎得清吗?

3,563 阅读11分钟

前言

作为一个应用工程师,除了写一些业务代码,性能优化也是我们需要关注的点!

如果想要去做启动优化,那么去了解启动过程就是一个绕不过去的坎儿。

那么除了关于启动过程的那些代码,我们还应该去知道什么呢?

卷

一、多进程那些事儿

在大家很早学习 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。

应用进程创建完了,应用还得和 AMSPMS 等其他服务或者进程通信啊,所以还得创建 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的查询服务,ServerClientService Manager的通讯都是通过Binder。
  • Binder驱动:负责Binder通信机制的建立,提供一系列底层支持。

从上图中,Binder 通信的过程是这样的:

  1. ServerService Manager 中注册:Server 进程在创建的时候,也会创建对应的Binder实体,如果要提供服务给 Client,就必须为 Binder 实体注册一个名字。
  2. Client 通过 Service Manager 获取服务:Client 知道服务中 Binder 实体的名字后,通过名字从 Service Manager 获取 Binder 实体的引用。
  3. 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 映射到内存中,并通过 PathClassLoaderDexClassLoader 将 APP 中的类加载到内存里。

三、进入Android启动过程

从上面我们已经知道了,我们的应用启动入口在 ActivityThread#main() 方法。

1. 启动过程中的通信机制

在正式了解启动过程之前,我想我们还得了解一下启动过程的通信机制,这也是启动过程的主线,虽然我知道你们已经迫不及待了!

等待

稍微了解过启动过程的同学应该都知道 ActivityThreadApplicationThreadActivityManagerService 这三个角色:

角色作用
ActivityThread(下称AT):应用的启动入口,进行应用启动的工作。
ActivityManagerService(下称AMS):Android系统中最重要的系统服务之一,负责四大组件的启动、管理和调度,同时也管理应用进程。
ApplicationThreadAMSAT 通信的桥梁,AT 的内部类。

上面我们了解过 Android 系统的两大支柱进程之一的 Zygote 进程,另外一个就是 SystemServer 进程。

AMS 就处于 SystemServer 进程,还有很多大家耳熟能详的服务都在这里边,AMS 基于 Binder 实现的,所以 ActivityThread 能够在应用进程联系远在 SystemServer 进程的 AMS

AMS 如何联系 ActivityThread 呢?

巧了,也是 Binder,它的实现类是 ApplicationThread,不过它是一个匿名 Binder(没有在 Server Manager 注册的 Binder),之所以这么设计,我想更多的是基于安全方面考虑的。

ActivityThread通信.png

ATAMS 的通信正如图上所描述的那样。

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,并且我们也没有发现 ApplicationThreadService Manager 注册 Binder 的任何代码,这也更加证实它是一个匿名 Binder,不信,我们可以看一下 ApplicationThread 的代码:

private class ApplicationThread extends IApplicationThread.Stub {
    //...
}

public class ActivityManagerService extends IActivityManager.Stub
        implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
    // ...    
}

ApplicationThreadActivityManagerService 貌似都改成 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);
    }
}

我们可以看见,主要做了以下几件事:

  1. 创建 ContextImpl
  2. 创建 Application
  3. 初始化 ContentProvider
  4. 回调 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内核设计与实现》