标签:Instant Run

Instant Run

从Instant Run看安卓Application的启动流程与替换

关于Application启动流程网上有很多文章分析,但绝大多数文章都是为了讲Application启动流程而讲的,因此绝大多数文章就是分析源码流程,但是大家都懂的,Application启动流程的这个源码是相当的的复杂,绝大多数人看了不一定能够很好的理解,因为源码细节太繁琐,只见树木不见森林,没有一个很好的目标感,而我觉得分析源码最好的方式就是带着某个问题去看,即我要分析这个源码是为了解决什么疑惑?这样才能更好的做到点到为止,能放能收,不过分注重细节,也不漏掉某些重要细节(比如关于Instant Run中类加载器部分的逻辑网上绝大部分文章就是一句简单的双亲委派带过,但是实际上Instan Run关于类加载器部分的精妙之处却是其设计思想,不清楚的同学可以看从ClassNotFoundException看java的类加载机制和Instant Run类加载部分的设计)但很遗憾网上的绝大多数文章就是把源码流程分析一遍。最近在看Instant Run相关的代码,发现Instant Run中的替换Application这个过程的细节代码需要了解Application的启动流程,因此觉得这是一个很好的去了解Applicaiton启动流程的契机,即了解Application的启动流程来解决看Instant Run中的替换Application这个过程的细节代码的疑惑。 那么为何需要替换Application呢?或者说那些业务场景下需要替换Application呢?有些时候我们为了是实现一些特殊的需求,比如在自定义的ClassLoader中加载一个dex的所有代码(当然包括Application类),再比如一般的apk加固是会用自己的Applicaiotn去接管用户的apk的启动,再比如Instant Run,这些场景都有一个共同的特点就是希望自己的某些代码在用户的代码运行之前执行,而我们知道对于应用层而言,用户的Application(假设叫做OriginalApplication)是最早执行的代码,那这就产生了一个悖论,OriginalApplication是最早执行的,在其执行前没有任何入口可以提供给我们去执行我们的代码,因此只能用我们的Application(假设叫做ProxyApplication)替换掉用户的Application,然后在ProxyApplication中去接管执行OriginalApplication的业务逻辑。 前面说过Instan Run中存在替换用户自定义Application的过程,我们看下Instant Run是如何实现的,主要逻辑在MonkeyPatcher.java这个类的monkeyPatchApplication函数中。如何下是替换核心逻辑。 [crayon-5f03a958ed49d037259244/] 可以看到这个函数绝大部分都是注释,而且从注释中可以看到替换Application主要包括3部分: 将ActivityThread类的mInitialApplication成员替换为真实的Application,所谓真实Application即用户原本定义的Application 将ActivityThread类的mAllApplications域(为ArrayList<Application>对象)所有的值替换为真实的Application(Replace all instance of the stub application in ActivityThread#mAllApplications with the real one) 枚举所有的LoadedApk或者PackageInfo对象(这2个对象的作用基本一致,只不过在不同版本的sdk中因为历史原因起名不同而已),将这些对象中的mApplication域设置为真实的Application。同样也要把Application的mLoadedApk设置为该LoadedApk对象。(Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and ActivityThread#mResourcePackages and do two things:1 Replace the Application instance in its mApplication field with the real one.2 Set Application#mLoadedApk to the found LoadedApk instance) 可以看到替换Application对象涉及到2个重要类,ActivityThread和LoadedApk(或者PackageInfo),替换Application就是把ActivityThread对象中已经赋过值的mInitialApplication,mAllApplications和LoadedApk对象中的mApplication替换为用户原本的Application。那么为何替换Application需要对这2个类进行相关操作呢?这就需要从安卓Application的启动流程来找答案了。 我们都知道当我们在桌面上点击app图标后,安卓的Zygote进程会fork一个新进程作为app的进程空间,然后调用经过一系列的复杂流程后进入到ActivityThread的main函数,因为安卓是用java语言开发的,在java程序中程序的入口即使main函数,安卓也不例外,安卓的入口main函数是ActivityThread的main函数。我们来看下其代码。以下源码流程分析都是基于安卓6.0.0_r1。 [crayon-5f03a958ed4ab022465205/] 为了更好的抓住主要逻辑,省略了很多细节代码,可以看到main函数主要做了2件事 创建主消息循环,执行loop循环(因为这个不在分析app启动流程的重点中,所以不进行过多介绍) 创建ActivityThread对象,调用该对象的attach方法。 接下来我们跟进到attach函数中看下 [crayon-5f03a958ed4b0623393186/] 同样的省略若干不重要细节代码,可以看到该函数分2中情况进行处理,系统app和非系统app,因为我们研究的是用户app,所以系统app这块逻辑我们忽略。可以看到在处理用户app时会创建一个ActivityManagerService对象,然后调用该对象的attachApplication方法 [crayon-5f03a958ed4b3419994895/] 传入进去的是mAppThread是ApplicationThread对象,该对象是一个binder对象,用来在ActivityManagerService(以下简称AMS)和ActivityThread这2者之间进行跨进程通信。到这逻辑就跳转到了AMS中了,AMS的attachApplication方法主要逻辑是调用了自己的attachApplicationLocked方法,因此我们直接看下attachApplicationLocked方法。 [crayon-5f03a958ed4b5780472846/] 可以看到该函数主要逻辑是调用了传进来的ApplicationThread的bindApplication方法。这是一个binder跨进程调用,流程逻辑重新回到ApplicationThread的bindApplication方法。跟进到ApplicationThread的bindApplication方法,看下逻辑: [crayon-5f03a958ed4b7874645531/] 可以看到该方法的主要逻辑就是把一系列参数打包为data,然后通过H这个Handler以BIND_APPLICATION消息发送出去(这里的H和ApplicationThread一样是ActivityThread的内部类),因此接下来我们看下H时如何处理BIND_APPLICATION这个消息的。 [crayon-5f03a958ed4b9931389380/] 可以看到调用了ActivityTread的handleBindApplication这个方法,该方法时Application启动流程最核心的方法,跟进该方法: [crayon-5f03a958ed4bb358364783/] 可以看到该方法主要做了以下几件事: 创建app运行时的上下文环境ContextImpl 调用LoadedApk的makeApplication方法创建Application对象,同时将该对象赋值给ActivityThread的mInitialApplication成员变量,同时需要注意的是LoadedApk的makeApplication方法会把创建的Application对象赋值给自己内部定义的mApplication成员变量 如果存在ContentProvider,先加载ContentProvider 通过Instrumentationd的callApplicationOnCreate函数间接调用当前Application的onCreate函数,至此Application启动流程就跑起来了。 其中LoadedApk的makeApplication方法逻辑如下: [crayon-5f03a958ed4c0109936750/] 可以看到将创建的Application对象添加进了ActivityThread的mAllApplications中,同时也将其赋值给了自己的成员变量mApplication。 现在回过头来看Instan Run替换Application的过程是不是就非常清晰明朗了,而且从这个过程中看到安卓中的四大组件以及Application之所以不能够像java那样直接new一个就可以用是因为涉及到一系列复杂的流程初始化,会在framework层来回牵扯,互相赋值,直接new的四大组件和Application对象没有这些赋值过程,因此不具备生命周期,上下文环境等一些安卓上特有的属性,另外也可以看到ContenProvider的执行是在Applicaton的onCreate之前。 先总结下Application的启动流程的主要逻辑 ActivityThread#main->ActivityThread#attach->ActivityManagerService#attachApplication,此时流程逻辑跳转到了AMS AMS#attachApplication->ActivityManagerService#attachApplicationLocked-> ApplicationThread#bindApplication,此时AMS通过传进来的ApplicationThread进行binder通信调用ApplicationThread的bindApplication,此时逻辑重新回到ActivityThread 在bindApplication中通过H这个Handler发送BIND_APPLICATION消息,该消息被ActivityThread#handleBindApplication处理 handleBindApplication是Application的核启动流程的核心方法,在该方法中会直接或间接创建LoadedApk对象,创建app上下文环境ContextImpl,创建Application 再总结下替换Application的主要逻辑 将ActivityThread类的mInitialApplication成员替换为真实的Application 将ActivityThread类的mAllApplications这个ArrayList<Application>对象的所有的子元素替换为真实的Application 枚举所有的LoadedApk或者PackageInfo对象,将这些对象中的mApplication域设置为真实的Application。同样也要把Application的mLoadedApk设置为该LoadedApk对象    

Instant Run

从ClassNotFoundException看java的类加载机制和Instant Run类加载部分的设计

我们都知道java中的类加载机制采用的是双亲委派机制(其实更严格的说法应该是未经加载过的类采用双亲委派机制加载,已经加载过的不是)。利用这种特性我们可以做到自定义ClassLoader加载额外路径的jar进而做到代码热更新,考虑如下一种场景:在宿主jar中存在一个类X,在该类中存在一个函数someFun,在另外一个插件jar中包含一个和宿主jar同包名同类名的类X,在该类中也存在和宿主jar中X的相同签名的函数someFun,在宿主jar的main函数中调用了类X的someFun函数,那么如何做到让main函数中调用的函数someFun是来自插件jar中而不是宿主jar中的呢?根据java的双亲委派机制,我们很容易想到可以自定义一个ClassLoader用来加载插件jar,然后将这个自定义ClassLoader设置为当前ClassLoader的父加载器即可,因为按照双亲委派加载,在main函数中使用到类X时会委托当前ClassLoader的父加载器去加载类X,而此时其父加载器为我们自定义的类加载器,所以会优先从插件jar中加载类X,这样调用的方法也即为插件jar中的X类的同名方法。 比如:存在类com.test.caseclass.X,宿主jar和插件jar的这个类X中都包含一个print函数,宿主jar和插件jar中的print函数的实现不一样,为了简单起见,这里只是简单的打印一句输出作为不同的实现,宿主jar中的X类代码如下: [crayon-5f03a958ed9d4265982291/] 插件jar的X类代码如下: [crayon-5f03a958ed9d9713868066/] 在宿主的main方法中存在一处调用类X的print函数的代码, [crayon-5f03a958ed9db773551732/] 那么很显然此时调用的是宿主jar中的类X的print函数,因此输出为: [crayon-5f03a958ed9dc746885090/] 那么我们如果想要让X.print调用的是插件jar中的print函数应该怎么做呢?我们只需要自定义一个ClassLoader,然后将其加载路径设置为插件jar,然后将该自定义ClassLoader设置为当前系统默认应用类AppClassLoader的父加载器即可(在java中用户的类的默认加载器是AppClassloader)。整个类加载器的委托加载关系转换如下图所示: 此时代码如下: [crayon-5f03a958ed9de257041537/] 运行程序,输出如下: 可以看到确实是加载了插件jar中的类X而不是宿主jar中的类X,这个很好理解,因为双亲委派机制,查找类X时会优先从myClassloader中查找,因此查找到的是插件jar的类X,所以输出是插件jar中的X.print()。接着考虑复杂一点的情况,如果在类X中引用了类Y,而这个类Y只在宿主jar中存在,在插件jar中不存在,那么程序的输出会时什么呢?比如此时类X的print函数代码是这样的: [crayon-5f03a958ed9e1927660810/] 其中的Y.print仅仅只是打印一句:System.out.print(“this msg is from Y class”);注意此时我们的插件jar中不存在类Y,类似代码热修复场景仅仅类X的print函数代码出现bug,我们打算修复类X的print函数,类X引用到的类Y还是使用原本在宿主jar中存在的类Y。那么此时程序的输出是什么呢?请大家先思考几秒钟再往下看。 正确答案是运行时抛出ClassNotFoundException。如下图所示: 纳尼?为何会是类找不到异常呢?类Y不是在宿主jar中吗?不是父加载器(myClassLoader)找不到应该到子加载器即此时的系统类加载器(AppClassLoader)中查找吗?那应该能够找到类Y啊。为何会是ClassNotFoundException呢? 这就需要对java中的类加载机制和双亲委派模型细节有较为深入的了解了,因为java中的类加载器从加载过程来看实际上都可以概括为2类加载器,一类是初始加载器(initiating loader),一类是定义加载器(defining loader),前者是调用loadClass方法的加载器,后者是调用defineClass方法的加载器,而双亲委派意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器很大的可能不是同一个加载器,在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。而双亲委派模型中的父加载器找不到该类会到子加载器中查找是针对loadClass而言的,也即是初始化加载器而言的,而上述示例中在Main.class中引用了类X,因此java虚拟机会去加载类X的字节码,此时java默认的加载器AppClassLoader就是类X的初始类加载器(调用loadClass),但是因为双亲委派机制实际加载类X的加载器是我们自定义的myClassLoader,因为myClassLoader是AppClassLoader的父加载器,同时myClassLoader的加载路径插件plug.jar中能够找到类X的字节码内容。此时在加载类X的字节码的时候发现类X引用了类Y,所以会调用loadClass去加载类Y,所以此时类Y的初始化加载器是myClassLoader,所以按照双亲委派模型,会委托myClassLoader的父加载器ExtClassLoader去加载类Y,很显然父加载器找不到,然后自己尝试加载,但是在myClassLoader的加载路径插件plug.jar中找不到类Y,所以抛出异常,注意事实上此时是不会到myClassLoader的子加载器AppClassLoader中查找类Y的,即使在其子加载器AppClassLoader加载路径中能够找到该类的字节码。因为类Y的初始化加载器是myClassLoader。 两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器,如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。 方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。 关于java中ClassLoader的更多详细权威的讲解请参看:深入探讨 Java 类加载器   相信大家知道了抛出异常的原因之后很容易想出一个非常简单的解决办法,即在插件jar中也保存一份类Y的代码,这样myClassLoader查找类Y时就能够找到该类,作为一个简单的demo这样做是可以的,但是如果是在实际使用中,当我们想动态替换一个类的功能的时候,如果这个类引用的类比较多,其引用的类也存在引用其他类的情况,因此我们需要将所有使用到的类都放到插件jar中,这种方式本质上相当于重新打包发布了一个宿主jar,只不过这个宿主jar中的类X修改了一部分代码,很显然这样做和我们的初衷动态的修复某一个类是相违背的,我们只希望去动态的修复那个出现bug的类,而其他的类都不用处理。如果大家对主流的热修复比较了解的话,大家可能就清楚这种在插件jar中保留宿主jar中的类的方案类似热修复中的全量替换,即我全部都不使用原来的宿主jar中的类,重新发布一个jar包作为插件jar去动态加载。那能否不把插件类X引用到的类都放到插件jar中同时不抛出异常呢?即插件jar中只保留要修复的类去动态加载,其引用的类仍然使用原来的宿主jar中的。答案是肯定的,谷歌的Instant Run中类加载部分的设计可以用来解决这类问题。 网上关于Instant Run的类加载部分的讲解都是简单的一句双亲委派机制带过,但是事实上,双亲委派机制不是Instant Run类加载部分设计的精妙之处,简单的双亲委派和前面提到的那个例子一样会抛出ClassNotFoundException,事实上Instant Run关于类加载部分设计的精妙之处在于双亲委派和代理加载构成闭环,如下图所示是谷歌Instant Run的设计图: (注:图片来源于网络) 即将安卓中的默认加载器PathClassLoader(类似java中的AppClassLoader)的父加载器设置为自定义的IncrementalClassLoader,但是IncrementalClassLoader只是个空壳,其对类的加载实际上是代理给了其内部定义的一个DelegateClassLoader去加载,同时将PathClassLoader也设置为DelegateClassLoader的父加载器,也就是说系统默认的加载器PathClassLoader的父加载器是自定义IncrementalClassLoader,其内部的DelegateClassLoader的父加载器是PathClassLoader,这样构成了一个闭环,用前面的示例来举例的话,当PathClassLoader加载类X的时候会委托IncrementalClassLoader加载,而IncrementalClassLoader会把该加载代理给DelegateClassLoader,这样DelegateClassLoader加载到类X,同时加载类X的时候发现类X引用了类Y,所以此时类Y的初始化加载器为DelegateClassLoader,根据前面的分析DelegateClassLoader是无法加载类Y的,但此时因为设置了PathClassLoader为DelegateClassLoader的父加载器,因此会委托PathClassLoader加载,而类Y在PathClassLoader的加载路径中,因此能够成功加载到类Y,从而解决前面示例中的问题。 关于谷歌Instant Run类加载部分的代码因为不是本节重点,因此不做详细描述,而且谷歌Instant Run类加载部分的代码非常简单,大家可以从官网进行了解:https://android.googlesource.com/platform/tools/base/+/gradle_2.0.0/instant-run/instant-run-server/src/main/java/com/android/tools/fd/runtime/IncrementalClassLoader.java 重点是弄清楚前面提到的示例中为何当插件jar中的类X引用的类Y不在插件jar中时会抛出异常和如何解决该异常,以及Instant Run类加载部分设计的思想。 前面说过Instant Run类加载部分的设计可以用来解决前面示例提到的插件jar中类X引用的类Y不在插件jar中时导致的ClassNotFoundException,推而广之,该设计可以用来解决广义上的类似热修复的补丁类/动态加载的插件类引用了原始jar/dex中的某个类导致的ClassNotFoundException,能够很好的做到补丁类代码和业务代码互不影响的效果,即能够被代理ClassLoader加载的类会通过DelegateClassLoader加载,不能被代理ClassLoader加载的类会被系统默认的ClassLoader加载(java中的AppClassLoader和安卓中的PathClassLoader),就像没使用代理ClassLoadr一样透明,从而很好的做到了动态加载的类代码和原业务代码相隔离的效果。即动态加载时只需要关心补丁类而不用关心业务代码类(宿主jar/dex中的类),即只用修复bug类,而无需全量合成替换。 虽然Instan Run的设计思想很好的解决了动态加载的插件jar的类代码和原宿主jar业务代码相隔离的效果,但是并不意味着这种思想可以直接移植到java代码运行环境中而不加任何修改,原因是安卓中的ClassLoader的findClass方法和java中的ClassLoader的findClass的原理是不一样的,安卓中的findClass是从dex文件中查找类,和java中的loadClass方法一样安卓查找类时如果一个类以及加载会从缓存中取该类的Class对象。而java中的这个逻辑是在loadClass方法中的。如果直接采用Instan Run的这个设计思想,将自定义的MyClassLoader的类查找委托给代理的DelegateClassLoader的findClass方法,那么此时我们重写的肯定是ClassLoader的findClass方法,举个简单例子如下: [crayon-5f03a958ed9e9339968299/] 考虑这样一种情况,如果插件jar中的某个类X引用了插件jar中的类Y,那么当加载类X时,会使用代理ClassLoader的findClass方法去加载类Y,然后当宿主jar中也用到类Y的时候此时按照这个设计思想很显然代理ClassLoader还是会通过findClass方法加载类Y,那么此时就会抛出一个attempted duplicate class definition的异常,也就是某个类被同一个ClassLoader加载了2次,也即对同一个类使用同一个ClassLoader实例调用了2次findClass方法,这在java中是不允许的。 之所以会调用2次findClass方法原因是我们重写的是findClass方法,而java中一个类已经加载后下次使用会从缓存中加载的逻辑是在loadClass方法中的。而Instan Run的关于ClassLoader整个设计思想是自定义的ClassLoader直接调用代理DelegateClassLoader的findClass方法而不是loadClass,所以直接引用到java平台的话第二次还是会调用代理DelegateClassLoader的findClass方法。为何安卓上没这个问题,前面说过安卓中的findClass方法和java中是不一样的,安卓中的findClass方法实际上是委托DexPathList的findClass方法来完成类的查找的,不清楚的小伙伴可以参看也谈安卓dex的动态加载与MultiDex和热修复 那么如何解决呢?知道原因是第二次还会调用findClass导致的就很容易解决了,我们可以在DelegateClassLoader中设置一个缓存Map,用来保存已经加载的类名称与其对应的Class对象,然后在findClass方法中加载类的时候先判断在缓存Map中是否存在该类的对象,如果存在直接返回,否则在调用父类的findClass方法即可,换而言之就是把java中ClassLoader的loadClass方法中缓存逻辑加到代理DelegateClassLoader中即可。