胡琪

为今天工作,为明天投资,为未来孵化一些东西!

【插件化】从插件化看Activity的启动流程

前言

关于Activity的启动流程网上有很多分析文章,但绝大部分都是直接分析源码,这是我一直不赞成的一种方式,首先Activity的启动流程非常复杂,如果直接分析源码那么文章必然长篇大论,让人看的云里雾里抓不住重点,只见树木不见森林。另一方面个人认为单纯从理论角度分析源码是没有意义的,分析源码应该是为了解决某一个业务难题或开发疑惑带着这个问题或疑惑从源码中去探寻解决这个问题的思路或方法。也就是输出倒逼输入。比如现在老板让你做一个Activity的插件化方案,你不知道该如何下手,假设网上也没这方面的资料,那么你是不是可以尝试从源码中去看下,去分析下Activity的启动流程,看能否从Activity的启动流程中找到一些关键节点,在这些关键节点上做文章,比如hook某些关键类或函数。在比如老板让你做个热修复框架,假设网上也没这方面的资料,那么我们思考下热修复本质就是用修复bug的类替换存在bug的类,那么是不是可以尝试从安卓ClassLoader,dex加载或类加载相关的源码入手,去分析下系统这方面的源码是如何实现的,然后尝试提炼自己的解决思路,然后写demo进行验证。也就是分析源码一定是为了解决某个问题而去分析,只有这样分析源码才有目标感,才会刻意关注那些对解决目标问题有帮助的点,从而对能解决目标问题的关键点进行深挖,而对其他点忽略放行。而且不同的目的分析源码时的侧重点也是不一样的。

说了那么多,言归正传,个人觉得Activity的插件化这个业务场景是一个非常好的理解Activity的启动流程的切入点。

Activity的插件化需要解决的几个核心问题

【插件化】开篇总述中大致提到过Activity插件化需要解决的核心问题主要包括以下4点,当然细节部分远不止这4点。但是以下4点是Activity插件化的理论基础的核心。

如何加载进而创建Activity对象

对于已经安装到系统上的apk,当Activity启动时系统会使用默认的类加载器PathClassLoader从apk安装后释放的对应的dex文件中去加载该Activity所对应的字节码,但是对于插件中的Activity而言系统是无法加载的,因为系统根本就不知道该插件apk/dex所在的位置。如果单抛开Activity和普通java对象的差异性来看的话,那么这个问题本上就是如何动态的加载一个类/dex。如果你对热修复比较熟悉,那么这个问题就很简单了,主要方法包括如下三种

  • 类MultiDex的插件dex插桩:将插件dex构造的DexFile对象放到DexPathList对象的dexElements数组中
  • 直接自定义ClassLoader加载dex:类似java中的插件jar加载,直接自定义一个ClassLoader去加载该dex
  • 类Instant Run的ClassLoader hack:利用双亲委派模型,给系统默认的PathClassLoader设置一个自定义的父加载器PatchClassLoader,让该自定义加载器的dex加载路径指向插件dex。当然双亲委派只是该方案的最最基础的理论,该方案的核心不是该理论,感兴趣的同学可以参看我的热修复专栏系列文章进行了解

如果打算采用单ClassLoader方案,也就是如果存在多个插件和宿主共用一个类加载器,那么个人推荐第3种方式不推荐第1种方式,因为第3种方式代码简洁,逻辑清晰,而且通用性高,因为方案一本质上是安卓特有的属性,而方案三是基于Classloader的hack,思想在java平台也可以通杀。

这个问题解决后只是解决了加载普通java类的问题,而Activity的特殊性主要包括需要在Manifest中注册才能启动,否则运行时会抛出未声明异常,包含UI界面,因此需要使用资源文件拥有自己的生命周期回调函数等,这就是下面将要提到的几个关键问题。

如何保证宿主调用插件中的Activity时不出现未声明异常?

解决思路见第3小节相关内容

如何保证被宿主调起的插件Activity的资源是插件自身的资源而不是宿主的?

解决思路见第3小节相关内容

被宿主调起的插件Activity的生命周期如何管理与同步?

解决思路见第3小节相关内容

 

从Activity的启动流程探寻解决上述核心问题的思路

避免宿主调用插件中的Activity时出现未声明异常

对于这个点,我们知道当一个Activity未在AndroidManifest清单文件中声明却被启动那么系统会抛出一个异常,异常提示信息如下:

那么我们可以去分析Activity的启动流程,看这个异常是在Activity的启动流程的哪个节点抛出,抛出时需要满足哪些条件?如果要让系统不抛出这个异常,我们就只需要破坏这个成立的条件即可。

保证被宿主调起的插件Activity使用的资源是插件自身的

对于资源的加载,同样的我们可以去分析安卓系统是如何做到资源加载的,然后找到其中的关键点看能否应用到插件apk上,当然由于这个问题在业界已经有了通用的解决方案了,这里就不带大家分析资源加载机制部分的源码了,那就是通过反射调用AssetManager的addAssetPath将插件Apk路径与AssetManager相关联,然后使用该AssetManager作为参数创建Resources。该Resources就能使用插件Apk中的资源了。这里需要注意的一点就是要做到插件和宿主以及插件和其他插件的资源隔离,否则容易出现插件资源和宿主/其他插件资源id相同但类型不同导致冲突的问题。因此建议对每一个插件都维护一个指向其自己资源的Resources对象。

被宿主调起插件Activity的生命周期的管理

同样的,我们也可以从Activity的启动流程中去找答案,去看宿主的Activity的生命周期是如何管理的?当然对于绝大部分开发者而言对AMS其实是不陌生的,我们知道App所在进程的所有Activity的启动本质上都是被系统进程中的AMS服务来完成的,所以这里本质就是系统进程中的AMS是如何标记用户进程的Activity的从而调用其相关生命周期函数的?

从插件化来看Activity启动的几大关键节点

通过上面的分析,我们对Activity的启动的一些细节有了宏观的把控和了解,那么现在来复盘看下,从插件化角度来看,Activity的启动流程到底可以概括为那些关键节点呢?

  • 组件未声明异常是在哪里抛出的?抛出需要满足哪些条件?如何做到破坏或绕过这些条件
  • 插件Activity的Resource对象是在什么时候创建的?在哪里替换默认的Resource对象为指向插件的Resource比较合适?
  • 插件Activity的生命周期如何管理与同步?是否需要插件框架去同步?还是说能够做到像宿主Activity那样本身由系统控制?

从插件化角度分析Activity启动流程

当我们通过startActivity启动一个Actiivity时尽管该方法包含多个重载形式,但是最终都是会调到Activity#startActivityForResult。该方法本质上是调用一个名为mInstrumentation的Instrumentation对象的execStartActivity

所以我们只需要跟进Instrumentation#execStartActivity

从上面的代码可以看到该方法做了2件非常重要的事情:

  • 首先调用AMS的startActivity其启动Activity,即:

  • 将AMS的startActivity的返回值作为一个参数传递给checkStartActivityResult方法,而该方法就是检测到Activity未声明时抛出异常的地方

那么到这里第一个目标问题就出现了,即组件未声明异常是在Instrumentation#execStartActivity中抛出的,那么如何解决或避免该异常呢?这里先提下业界Activity组件化对此的解决方案,就是预先在插件化框架(也可以认为是宿主)的Manifest清单文件中声明一些StubActivity,然后在启动插件Activity之前将Intent中待启动的插件Activity替换为StubActivity,最终在检测通过之后真正启动之前又替换回来,这里就涉及到2个问题在哪里替换为StubActivity比较合适?又在哪里替换回原Activity比较合适?很显然既然异常是在该方法中抛出的,那么至少一点就是要在该方法执行之前替换掉,那么是不是越早越好呢?也不是,因为我们去替换某个Intent启动信息为StubActivity需要是对所有的启动流程生效,也就是说我们需要找到一个点,所有的Activity启动流程都会经过这个点,熟悉hook的同学就应该明白这个意思,和寻找hook点类似,在这之前的流程是Activity类中的,而Activity不是唯一的,不好hook,那么能否hook Instrumentation#execStartActivity呢?表明上看貌似也不能,因为通过前面分析我们知道Activity的启动流程跳到Instrumentation时是调用的Activity的一个内部变量mInstrumentation的execStartActivity,也就是这个execStartActivity方法是Activity的内部成员mInstrumentation的,意味着Activity不同实例有不同的mInstrumentation实例引用,但是实际上是可以hook该类的,因为虽然不同的Activity实例确实有不同的mInstrumentation实例引用,但是实际上mInstrumentation指向的Instrumentation对象是同一个,都是主线程ActivityThread中创建的mInstrumentation对象,而主线程ActivityThread很显然是唯一的。所以如果要通过替换Intent启动信息为StubActivity的方式绕过未声明异常的话,hook Instrumentation在其execStartActivity方法中做偷梁换柱行为是比较合适和恰当的点。

这里实际上就涉及到Activity的mInstrumentation成员的赋值问题,感兴趣的同学可以去看源码跟进下,本质上最终来自的是主线程ActivityThread中创建的mInstrumentation对象。同样的Context#startActivity最终也是来自与主线程的mInstrumentation对象,不过Context#startActivity更为明显:

上面的这段代码来自ConetxtImpl(Context的真正实现类),可以直观看到如果要在非Activity的Context中启动一个Activity则启动的Intent必须加上FLAG_ACTIVITY_NEW_TASK标志,而且可以很直观的看到本质上也是调用了主线程ActivityThread的mInstrumentation对象的execStartActivity方法。

到这里第一个问题就解决一半了,即绕过了未声明异常检查,另一半就是在哪里将Intent信息替换回原目标Activity,这个在后面进一步分析。这里先进一步聊下Instrumentation这个类,看下这个类的execStartActivity方法的部分注释:

从上面这段注释可以看到一个非常有意思的功能:

Execute a startActivity call made by the application.  The default implementation takes care of updating any active {@link ActivityMonitor} objects and dispatches this call to the system activity manager; you can override this to watch for the application to start an activity, and modify what happens when it does. 

翻译过来差不多就是

执行应用程序发出的startActivity调用。并将此调用分派给AMS;您可以覆盖它,以监视应用程序启动活动,并修改活动启动时发生的情况。

也就是说安卓系统的设计着提供这个类的目的之一就是用来监控或检测Activity的启动的,这点其实从该类的名称就可以看出来。所以我们不仅可以通过hook该类实现绕过Activity的未声明异常,实际上还可以做更多和监控或检测Activity启动相关的事情。当然不仅仅包括启动,也包括Activity的创建(newActivity)以及Application的创建(newApplication)。

接着前面Instrumentation#execStartActivity分析,前面提到过该方法做的另外一件很重要的事情就是调用AMS#startActivity。关于AMS的该方法调用链非常长,对于我们而言无需关心其调用链,因为具体操作在系统进程,即使知道也没法hook或做其他操作,类似日常开发中客户端App与服务端的后台接口交互一样,我们只需要关心输入与输出即可,即AMS向App进程要了哪些参数去做操作,操作完后返回了哪些结果,至于具体的操作完全可以看作是一个黑盒。

入口(ActivityManagerService#startActivity):

中间省略n多无意义的复杂调用链

出口(ActivityStackSupervisor#realStartActivityLocked):

从上面可以看到AMS经过大量复杂调用链之后最终会调用出口参数中的IApplicationThread对象的scheduleLaunchActivity,也就是ApplicationThread#scheduleLaunchActivity。该类是主线程ActivityThread的一个内部类,跟进该方法:

这个方法做的事情很简单,就是把AMS的输出包装为一个ActivityClientRecord对象,然后向主线程ActivityThread的H这个Handler发送一个启动Activity的消息,同时透传该对象。这里注意一个细节,就是第一个参数是名为token的IBinder对象,也就是说AMS在对原待启动Activity经过一系列操作之后进行输出时返回的是一个token,往后App进程与AMS的交互对该Activity的所有操作都是通过该token来标识的,也就是说token是建立与AMS系统服务对该Activity管理的纽带。另外从这个方法上面的注释可以证实这点:

// we use token to identify this activity without having to send the

// activity itself back to the activity manager. (matters more with ipc)

翻译过来大意就是:

我们使用token来标识该Activity,而不必将Activity本身发送给AMS。(ipc更重要)

这就为我们前面提到的插件Activity的生命周期管理提供了很大的方便,因为不是通过Activity的一些信息与AMS交互(比如Activity类名)而是通过token,那么只要我们在App进程中对该token的所有操作都是指向同一个Activity就能保证该Activity之后的所有生命周期都是自动管理的。也就是说只要第一次创建Activity时该token与哪个Activity建立了关联那么该token就标志了哪个Activity。而与Activity的名称无关,所以前面提到的通过偷梁换柱将给AMS#startActivity时的类名换成StubActivity然后后续流程真正启动与创建Activity时换回原目标Activity的做法就不用插件化框架(或宿主)去管理插件Activity的生命周期了。

到这里第3个问题就解决了,即插件Activity的生命周期不需要插件化框架去管理,就像宿主Activity的生命周期一样,由系统自动同步与管理。

接着前面启动流程的分析,ApplicationThread#scheduleLaunchActivity中通过H这个Handler发送了启动Activity的消息之后,将会进入到H的handleMessage方法,跟进该方法:

从上面可以看到实际上调用的是ActivityThread#handleLaunchActivity。另外因为该类是位于ActivityThread中的,而ActivityThread是唯一的,而且所有的消息都会经过该H进行处理,所以这里也是一个很好的hook点,我们可以hook该H的mCallback接口,然后覆写该接口的handleMessage,在该方法中就能够拦截到所有的Activity的启动消息,然后可以在这里将前面欺骗AMS时设置的StubSctivity替换为真实的目标Activity,这样在调用ActivityThread#handleLaunchActivity时创建与启动的就是真实的Activity了。到这里第一个目标问题的另一半就解决了。

现在就剩下第二个目标问题了。继续跟进ActivityThread#handleLaunchActivity:

从这里可以看到,会调用ActivityThread#performLaunchActivity来创建Activity(该方法执行完成后onCreate和onStart方法已经被回调),如果创建成功会调用ActivityThread#handleResumeActivity来调用其onResume方法,此时就基本可以认为Activity显示出来了。所以从这里可以看到实际上创建与启动Activity的最核心方法是ActivityThread#performLaunchActivity,跟进该方法,尽管这个方法很长,但是因为该方法是最核心方法,所以对其完整源码进行分析:

这个函数是启动Activity的核心函数,主要干了以下几点最核心的事情:

  1. 调用Instrumentation的newActivity创建Activity对象
  2. 调用PackageInfo的makeApplication创建Application对象,通过前面对Instrumentation的分析实际上可以知道PackageInfo最终还是调用了Instrumentation的newApplication来完成Application的创建
  3. 创建上下文Context对象,其真正实现类是ContextImpl
  4. 调用Activity#attach方法将前面创建的ContextApplicationAMS返回的token等信息透传给该Activity,另外从这里其实也可以看到Activity内部的mInstrumentation实际上就是从ActivityThread类中获取的,这也是前面为何可以通过hook系统Instrumentation来拦截Actiivty启动的根本原因
  5. 调用Activity的onCreate生命周期函数,本质上是通过Instrumentation#callActivityOnCreate来完成的

其中第4点就是前面提到的目标问题2的核心所在,即在哪里替换插件的Resource对象比较合适?我们知道对于Activity而言获取Resource对象本质上都是通过Activity内部的mBase(来自ContextWrapper,具体实现为ContextImpl)这个成员的getResources方法来获取的,不清楚的同学可以参看:https://www.cnblogs.com/kangqi001/p/8381355.html  。  所以我们完全可以为插件单独维护一个PluginContextImpl,重写该Context的getResources方法,在该方法中返回指向插件资源的Resource对象,然后直接替换Activity的Context即可(mBase),所以目标问题2就转化为了在哪里替换Activity的Context比较合适?很显然必须在attach方法之后,所以可以通过hook Instrumentation#callActivityOnCreate,在该方法中将插件Activity的mBase这个Context对象替换为插件的Context。

到这里Activity插件化的3大最核心的理论都从Activity的启动流程中找到了答案。同时也熟悉了Activity的启动流程,岂不美哉!

总结

从插件化的角度来看Activity的启动流程总共涉及到以下几大核心类

  • Instrumentation:工具类,用来辅助启动Activity,创建Activty,检查Activity是否声明以及创建Application,及调用Activity的onCreate,所有的Activity实例中的mInstrumentation实际上都是来自主线程的mInstrumentation,可以通过mock主线程的该对象,做很多有趣的操作
  • AMS:用来登记与管理所有的Activity,通过IApplicationThread类型的binder对象与app进程中的ApplicationThread通信
  • ApplicationThread:ActivityThread的内部类,用来在用户进程与AMS之间进行通信,是AMS调用startActivity启动Activity进行一系列复杂调用链后返回结果给用户进程的出口
  • ActivityThread:用户App主线程,在该类中最终调用performLaunchActivity来创建与启动Activity,是Activity启动的最最核心方法
  • H:ActivityThread的内部类,继承自Hanlder,用来在ApplicationThread#scheduleLaunchActivity中将逻辑切换到主线程的消息循环进而让ActivityThread执行handleLaunchActivity,最终执行performLaunchActivity创建启动Activity。可以通过mock主线程中的mH的mCallback对象来拦截所有的消息,进而监控Activity的启动消息

从插件化的角度来看Activity的启动流程涉及以下3大关键节点:

  • Activity#startActivity–>Activity#startActivityForResult–>Instrumentation#execStartActivity—>AMS#startActivity

在Instrumentation#execStartActivity这个关键节点中做了2件很重要的事情,一件就是调用AMS#startActivity让AMS去启动Activity,另一件就是通过AMS#startActivity的返回值对Activity启动进行检查,比如未声明则抛异常

  • AMS#startActivity–>ApplicationThread#scheduleLaunchActivity–>H#sendMessage

在ApplicationThread#scheduleLaunchActivity这个关键节点也做了2件非常重要的事情,一件就是将AMS调用startActivity后的输出包装为一个ActivityClientRecord对象,在这里节点可以知道对于AMS而言是通过token来标识待启动Activity的,另一件就是通过H这个Handler对象的sendMessage方法将上面的ActivityClientRecord发送给主线程的消息处理函数

  • H#sendMessage–>ActivityThread#handleLaunchActivity–>ActivityThread#performLaunchActivity

在ActivityThread#performLaunchActivity这个关键节点,该方法是Activity启动的最最核心方法,做了很多重要的事情,创建Activity对象,创建Application,创建Activity的Context,调用Activity的attach方法将该前面创建的Context,Application,AMS返回的token等信息透传给该Activity。调用Activity的onCreate方法

从插件化的角度是不是更容易捋清楚Activity的启动流程呢?更容易透过复杂的调用链抓住最核心的关键节点。

打赏

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注