深入理解Android插件化技术

<div id="article_content" class="article_content clearfix csdn-tracking-statistics" data-pid="blog" data-mod="popu_307" data-dsm="post" style="height: 1518px; overflow: hidden;">
                    <div class="markdown_views">
                <p>插件化技术可以说是Android高级工程师所必须具备的技能之一,从2012年插件化概念的提出(Android版本),到2016年插件化的百花争艳,可以说,插件化技术引领着Android技术的进步。本篇文章转载自腾讯bugly,觉得写得不错,转载分享给大家。</p>






<h1 id="插件化提要"><a name="t0"></a>插件化提要</h1>


<p><img src="https://img-blog.csdn.net/20180114185313720" alt="这里写图片描述" title=""></p>


<p>可以说,插件化技术涉及得非常广泛,其中最核心的就是Android的类加载机制和反射机制,相关原理请大家自行百度。</p>






<h2 id="插件化发展历史"><a name="t1"></a>插件化发展历史</h2>


<p>插件化技术最初源于免安装运行apk的想法,这个免安装的apk可以理解为插件。支持插件化的app可以在运行时加载和运行插件,这样便可以将app中一些不常用的功能模块做成插件,一方面减小了安装包的大小,另一方面可以实现app功能的动态扩展。想要实现插件化,主要是解决下面三个问题:</p>


<ul>
<li>插件中代码的加载和与主工程的互相调用</li>
<li>插件中资源的加载和与主工程的互相访问</li>
<li>四大组件生命周期的管理</li>
</ul>


<p>下面是比较出名的几个开源的插件化框架,按照出现的时间排序。研究它们的实现原理,可以大致看出插件化技术的发展,根据实现原理可以将这几个框架划分成了三代。 <br>
<img src="https://img-blog.csdn.net/20180114185632199" alt="这里写图片描述" title=""></p>


<p><strong>第一代</strong>:dynamic-load-apk最早使用ProxyActivity这种静态代理技术,由ProxyActivity去控制插件中PluginActivity的生命周期。该种方式缺点明显,插件中的activity必须继承PluginActivity,开发时要小心处理context。而DroidPlugin通过Hook系统服务的方式启动插件中的Activity,使得开发插件的过程和开发普通的app没有什么区别,但是由于hook过多系统服务,异常复杂且不够稳定。 <br>
<strong>第二代</strong>:为了同时达到插件开发的低侵入性(像开发普通app一样开发插件)和框架的稳定性,在实现原理上都是趋近于选择尽量少的hook,并通过在manifest中预埋一些组件实现对四大组件的插件化。另外各个框架根据其设计思想都做了不同程度的扩展,其中Small更是做成了一个跨平台,组件化的开发框架。 <br>
<strong>第三代</strong>:VirtualApp比较厉害,能够完全模拟app的运行环境,能够实现app的免安装运行和双开技术。Atlas是阿里今年开源出来的一个结合组件化和热修复技术的一个app基础框架,其广泛的应用与阿里系的各个app,其号称是一个容器化框架。</p>






<h1 id="插件化原理"><a name="t2"></a>插件化原理</h1>






<h2 id="类加载"><a name="t3"></a>类加载</h2>


<p>Android中常用的有两种类加载器,DexClassLoader和PathClassLoader,它们都继承于BaseDexClassLoader。相关源码如下:</p>






<pre class="prettyprint" name="code"><code class="hljs java has-numbering"><span class="hljs-comment">// DexClassLoaderpublic class DexClassLoader extends BaseDexClassLoader {    public DexClassLoader(String dexPath, String optimizedDirectory,</span>
            String libraryPath, ClassLoader parent) {        <span class="hljs-keyword">super</span>(dexPath, <span class="hljs-keyword">new</span> File(optimizedDirectory), libraryPath, parent);
    }
}
<span class="hljs-comment">// PathClassLoader</span>


<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PathClassLoader</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BaseDexClassLoader</span> {</span>    <span class="hljs-keyword">public</span> <span class="hljs-title">PathClassLoader</span>(String dexPath, ClassLoader parent) {     
   <span class="hljs-keyword">super</span>(dexPath, <span class="hljs-keyword">null</span>, <span class="hljs-keyword">null</span>, parent);
    } 


 <span class="hljs-keyword">public</span> <span class="hljs-title">PathClassLoader</span>(String dexPath, String libraryPath,
            ClassLoader parent) {    
    <span class="hljs-keyword">super</span>(dexPath, <span class="hljs-keyword">null</span>, libraryPath, parent);
    }
}</code><ul class="pre-numbering" style=""><li style="color: rgb(153, 153, 153);">1</li><li style="color: rgb(153, 153, 153);">2</li><li style="color: rgb(153, 153, 153);">3</li><li style="color: rgb(153, 153, 153);">4</li><li style="color: rgb(153, 153, 153);">5</li><li style="color: rgb(153, 153, 153);">6</li><li style="color: rgb(153, 153, 153);">7</li><li style="color: rgb(153, 153, 153);">8</li><li style="color: rgb(153, 153, 153);">9</li><li style="color: rgb(153, 153, 153);">10</li><li style="color: rgb(153, 153, 153);">11</li><li style="color: rgb(153, 153, 153);">12</li><li style="color: rgb(153, 153, 153);">13</li><li style="color: rgb(153, 153, 153);">14</li><li style="color: rgb(153, 153, 153);">15</li></ul></pre>


<p>区别在于调用父类构造器时,DexClassLoader多传了一个optimizedDirectory参数,这个目录必须是内部存储路径,用来缓存系统创建的Dex文件。而PathClassLoader该参数为null,只能加载内部存储目录的Dex文件。所以我们可以用DexClassLoader去加载外部的apk,用法如下:</p>






<pre class="prettyprint" name="code"><code class="hljs scss has-numbering"><span class="hljs-comment">//第一个参数为apk的文件目录</span>
<span class="hljs-comment">//第二个参数为内部存储目录</span>
<span class="hljs-comment">//第三个为库文件的存储目录</span>
<span class="hljs-comment">//第四个参数为父加载器</span>
new <span class="hljs-function">DexClassLoader(apk.<span class="hljs-function">getAbsolutePath()</span>, dexOutputPath, libsDir.<span class="hljs-function">getAbsolutePath()</span>, parent)</span></code><ul class="pre-numbering" style=""><li style="color: rgb(153, 153, 153);">1</li><li style="color: rgb(153, 153, 153);">2</li><li style="color: rgb(153, 153, 153);">3</li><li style="color: rgb(153, 153, 153);">4</li><li style="color: rgb(153, 153, 153);">5</li></ul></pre>


<p>其实,关于类加载更详细的内容,笔者也深入剖析过,可以查看下面的链接:<a href="http://blog.csdn.net/xiangzhihong8/article/details/52880327" target="_blank">类加载机制详解</a></p>






<h2 id="双亲委托机制"><a name="t4"></a>双亲委托机制</h2>


<p>ClassLoader调用loadClass方法加载类,代码如下:</p>






<pre class="prettyprint" name="code"><code class="hljs java has-numbering"><span class="hljs-keyword">protected</span> Class&lt;?&gt; <span class="hljs-title">loadClass</span>(String className, <span class="hljs-keyword">boolean</span> resolve) <span class="hljs-keyword">throws</span> ClassNotFoundException { 
       <span class="hljs-comment">//首先从已经加载的类中查找</span>
        Class&lt;?&gt; clazz = findLoadedClass(className);    
    <span class="hljs-keyword">if</span> (clazz == <span class="hljs-keyword">null</span>) {
            ClassNotFoundException suppressed = <span class="hljs-keyword">null</span>;     
           <span class="hljs-keyword">try</span> {   
                <span class="hljs-comment">//如果没有加载过,先调用父加载器的loadClass</span>
                clazz = parent.loadClass(className, <span class="hljs-keyword">false</span>);
            } <span class="hljs-keyword">catch</span> (ClassNotFoundException e) {
                suppressed = e;
            }      
        <span class="hljs-keyword">if</span> (clazz == <span class="hljs-keyword">null</span>) {        
                <span class="hljs-keyword">try</span> {           


                  <span class="hljs-comment">//父加载器都没有加载,则尝试加载</span>
                    clazz = findClass(className);
                } <span class="hljs-keyword">catch</span> (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);       
                     <span class="hljs-keyword">throw</span> e;
                }
            }
        }    
            <span class="hljs-keyword">return</span> clazz;
    }</code><ul class="pre-numbering" style=""><li style="color: rgb(153, 153, 153);">1</li><li style="color: rgb(153, 153, 153);">2</li><li style="color: rgb(153, 153, 153);">3</li><li style="color: rgb(153, 153, 153);">4</li><li style="color: rgb(153, 153, 153);">5</li><li style="color: rgb(153, 153, 153);">6</li><li style="color: rgb(153, 153, 153);">7</li><li style="color: rgb(153, 153, 153);">8</li><li style="color: rgb(153, 153, 153);">9</li><li style="color: rgb(153, 153, 153);">10</li><li style="color: rgb(153, 153, 153);">11</li><li style="color: rgb(153, 153, 153);">12</li><li style="color: rgb(153, 153, 153);">13</li><li style="color: rgb(153, 153, 153);">14</li><li style="color: rgb(153, 153, 153);">15</li><li style="color: rgb(153, 153, 153);">16</li><li style="color: rgb(153, 153, 153);">17</li><li style="color: rgb(153, 153, 153);">18</li><li style="color: rgb(153, 153, 153);">19</li><li style="color: rgb(153, 153, 153);">20</li><li style="color: rgb(153, 153, 153);">21</li><li style="color: rgb(153, 153, 153);">22</li><li style="color: rgb(153, 153, 153);">23</li><li style="color: rgb(153, 153, 153);">24</li></ul></pre>


<p>可以看出ClassLoader加载类时,先查看自身是否已经加载过该类,如果没有加载过会首先让父加载器去加载,如果父加载器无法加载该类时才会调用自身的findClass方法加载,该机制很大程度上避免了类的重复加载。</p>






<h3 id="dexpathlist"><a name="t5"></a>DexPathList</h3>


<p>这里要重点说一下DexClassLoader的DexPathList。DexClassLoader重载了findClass方法,在加载类时会调用其内部的DexPathList去加载。DexPathList是在构造DexClassLoader时生成的,其内部包含了DexFile。如下图所示: <br>
<img src="https://img-blog.csdn.net/20180114190418050" alt="这里写图片描述" title=""></p>


<p>DexPathList的loadClass会去遍历DexFile直到找到需要加载的类。</p>






<pre class="prettyprint" name="code"><code class="hljs cs has-numbering"><span class="hljs-keyword">public</span> Class <span class="hljs-title">findClass</span>(String name, List&lt;Throwable&gt; suppressed) { 
       <span class="hljs-comment">//循环dexElements,调用DexFile.loadClassBinaryName加载class</span>
        <span class="hljs-keyword">for</span> (Element element : dexElements) {
            DexFile dex = element.dexFile;    
        <span class="hljs-keyword">if</span> (dex != <span class="hljs-keyword">null</span>) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);   
                     <span class="hljs-keyword">if</span> (clazz != <span class="hljs-keyword">null</span>) {   
                       <span class="hljs-keyword">return</span> clazz;
                }
            }
        }  
      <span class="hljs-keyword">if</span> (dexElementsSuppressedExceptions != <span class="hljs-keyword">null</span>) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }     
         <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
    }</code><ul class="pre-numbering" style=""><li style="color: rgb(153, 153, 153);">1</li><li style="color: rgb(153, 153, 153);">2</li><li style="color: rgb(153, 153, 153);">3</li><li style="color: rgb(153, 153, 153);">4</li><li style="color: rgb(153, 153, 153);">5</li><li style="color: rgb(153, 153, 153);">6</li><li style="color: rgb(153, 153, 153);">7</li><li style="color: rgb(153, 153, 153);">8</li><li style="color: rgb(153, 153, 153);">9</li><li style="color: rgb(153, 153, 153);">10</li><li style="color: rgb(153, 153, 153);">11</li><li style="color: rgb(153, 153, 153);">12</li><li style="color: rgb(153, 153, 153);">13</li><li style="color: rgb(153, 153, 153);">14</li><li style="color: rgb(153, 153, 153);">15</li><li style="color: rgb(153, 153, 153);">16</li></ul></pre>


<p>腾讯的qq空间热修复技术正是利用了DexClassLoader的加载机制,将需要替换的类添加到dexElements的前面,这样系统会使用先找到的修复过的类。</p>






<h3 id="单dexclassloader与多dexclassloader"><a name="t6"></a>单DexClassLoader与多DexClassLoader</h3>


<p>通过给插件apk生成相应的DexClassLoader便可以访问其中的类,这边又有两种处理方式,有单DexClassLoader和多DexClassLoader两种结构。 <br>
对于多DexClassLoader结构来说,可以用下面的模型来标识。 <br>
<img src="https://img-blog.csdn.net/20180114190739289" alt="这里写图片描述" title=""> <br>
对于每个插件都会生成一个DexClassLoader,当加载该插件中的类时需要通过对应DexClassLoader加载。这样不同插件的类是隔离的,当不同插件引用了同一个类库的不同版本时,不会出问题,RePlugin采用的就是此方案。</p>


<p>对于单DexClassLoader来说,其模型如下: <br>
<img src="https://img-blog.csdn.net/20180114190907812" alt="这里写图片描述" title=""> <br>
将插件的DexClassLoader中的pathList合并到主工程的DexClassLoader中。这样做的好处时,可以在不同的插件以及主工程间直接互相调用类和方法,并且可以将不同插件的公共模块抽出来放在一个common插件中直接供其他插件使用。Small采用的是这种方式。</p>


<p>插件和主工程的互相调用涉及到以下两个问题: <br>
<strong>插件调用主工程</strong> <br>
在构造插件的ClassLoader时会传入主工程的ClassLoader作为父加载器,所以插件是可以直接可以通过类名引用主工程的类。 <br>
<strong>主工程调用插件</strong></p>


<ul>
<li>若使用多ClassLoader机制,主工程引用插件中类需要先通过插件的ClassLoader加载该类再通过反射调用其方法。插件化框架一般会通过统一的入口去管理对各个插件中类的访问,并且做一定的限制。</li>
<li>若使用单ClassLoader机制,主工程则可以直接通过类名去访问插件中的类。该方式有个弊病,若两个不同的插件工程引用了一个库的不同版本,则程序可能会出错,所以要通过一些规范去避免该情况发生。</li>
</ul>


<p>关于双亲委托更详细的资料,大家也可以访问我博客之前的介绍:<a href="http://blog.csdn.net/xiangzhihong8/article/details/65446152" target="_blank">classloader双亲委托模式</a></p>






<h2 id="资源加载"><a name="t7"></a>资源加载</h2>


<p>Android系统通过Resource对象加载资源,下面代码展示了该对象的生成过程。</p>






<pre class="prettyprint" name="code"><code class="hljs cs has-numbering"><span class="hljs-comment">//创建AssetManager对象 </span>
AssetManager assets = <span class="hljs-keyword">new</span> AssetManager();
 <span class="hljs-comment">//将apk路径添加到AssetManager中</span>
  <span class="hljs-keyword">if</span> (assets.addAssetPath(resDir) == <span class="hljs-number">0</span>){              
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;  
}
 <span class="hljs-comment">//创建Resource对象</span>


r = <span class="hljs-keyword">new</span> Resources(assets, metrics, getConfiguration(), compInfo);</code><ul class="pre-numbering" style=""><li style="color: rgb(153, 153, 153);">1</li><li style="color: rgb(153, 153, 153);">2</li><li style="color: rgb(153, 153, 153);">3</li><li style="color: rgb(153, 153, 153);">4</li><li style="color: rgb(153, 153, 153);">5</li><li style="color: rgb(153, 153, 153);">6</li><li style="color: rgb(153, 153, 153);">7</li><li style="color: rgb(153, 153, 153);">8</li><li style="color: rgb(153, 153, 153);">9</li></ul></pre>


<p>因此,只要将插件apk的路径加入到AssetManager中,便能够实现对插件资源的访问。</p>


<p>具体实现时,由于AssetManager并不是一个public的类,需要通过反射去创建,并且部分Rom对创建的Resource类进行了修改,所以需要考虑不同Rom的兼容性。</p>






<h3 id="资源路径的处理"><a name="t8"></a>资源路径的处理</h3>


<p>和代码加载相似,插件和主工程的资源关系也有两种处理方式:</p>


<ul>
<li>合并式:addAssetPath时加入所有插件和主工程的路径;</li>
<li>独立式:各个插件只添加自己apk路径</li>
</ul>


<p><img src="https://img-blog.csdn.net/20180114191404985" alt="这里写图片描述" title=""></p>


<p>合并式由于AssetManager中加入了所有插件和主工程的路径,因此生成的Resource可以同时访问插件和主工程的资源。但是由于主工程和各个插件都是独立编译的,生成的资源id会存在相同的情况,在访问时会产生资源冲突。</p>


<p>独立式时,各个插件的资源是互相隔离的,不过如果想要实现资源的共享,必须拿到对应的Resource对象。</p>






<h3 id="context的处理"><a name="t9"></a>Context的处理</h3>


<p>通常我们通过Context对象访问资源,光创建出Resource对象还不够,因此还需要一些额外的工作。 对资源访问的不同实现方式也需要不同的额外工作。以VirtualAPK的处理方式为例。 <br>
第一步:创建Resource</p>






<pre class="prettyprint" name="code"><code class="hljs cs has-numbering"><span class="hljs-keyword">if</span> (Constants.COMBINE_RESOURCES) {
    <span class="hljs-comment">//插件和主工程资源合并时需要hook住主工程的资源</span>
    Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());
    ResourcesManager.hookResources(context, resources);  
      <span class="hljs-keyword">return</span> resources;
} <span class="hljs-keyword">else</span> {  
      <span class="hljs-comment">//插件资源独立,该resource只能访问插件自己的资源</span>
    Resources hostResources = context.getResources();
    AssetManager assetManager = createAssetManager(context, apk);  
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}</code><ul class="pre-numbering" style=""><li style="color: rgb(153, 153, 153);">1</li><li style="color: rgb(153, 153, 153);">2</li><li style="color: rgb(153, 153, 153);">3</li><li style="color: rgb(153, 153, 153);">4</li><li style="color: rgb(153, 153, 153);">5</li><li style="color: rgb(153, 153, 153);">6</li><li style="color: rgb(153, 153, 153);">7</li><li style="color: rgb(153, 153, 153);">8</li><li style="color: rgb(153, 153, 153);">9</li><li style="color: rgb(153, 153, 153);">10</li><li style="color: rgb(153, 153, 153);">11</li></ul></pre>


<p>第二步:hook主工程的Resource <br>
对于合并式的资源访问方式,需要替换主工程的Resource,下面是具体替换的代码。</p>






<pre class="prettyprint" name="code"><code class="hljs cs has-numbering"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">hookResources</span>(Context <span class="hljs-keyword">base</span>, Resources resources) { 
   <span class="hljs-keyword">try</span> {
            ReflectUtil.setField(<span class="hljs-keyword">base</span>.getClass(), <span class="hljs-keyword">base</span>, <span class="hljs-string">"mResources"</span>, resources);
            Object loadedApk = ReflectUtil.getPackageInfo(<span class="hljs-keyword">base</span>);
            ReflectUtil.setField(loadedApk.getClass(), loadedApk, <span class="hljs-string">"mResources"</span>, resources);


            Object activityThread = ReflectUtil.getActivityThread(<span class="hljs-keyword">base</span>);
            Object resManager = ReflectUtil.getField(activityThread.getClass(), activityThread, <span class="hljs-string">"mResourcesManager"</span>);       
     <span class="hljs-keyword">if</span> (Build.VERSION.SDK_INT &lt; <span class="hljs-number">24</span>) {
                Map&lt;Object, WeakReference&lt;Resources&gt;&gt; map = (Map&lt;Object, WeakReference&lt;Resources&gt;&gt;) ReflectUtil.getField(resManager.getClass(), resManager, <span class="hljs-string">"mActiveResources"</span>);
                Object key = map.keySet().iterator().next();
                map.put(key, <span class="hljs-keyword">new</span> WeakReference&lt;&gt;(resources));
            } <span class="hljs-keyword">else</span> {                <span class="hljs-comment">// still hook Android N Resources, even though it's unnecessary, then nobody will be strange.</span>
                Map map = (Map) ReflectUtil.getFieldNoException(resManager.getClass(), resManager, <span class="hljs-string">"mResourceImpls"</span>);
                Object key = map.keySet().iterator().next();
                Object resourcesImpl = ReflectUtil.getFieldNoException(Resources.class, resources, <span class="hljs-string">"mResourcesImpl"</span>);
                map.put(key, <span class="hljs-keyword">new</span> WeakReference&lt;&gt;(resourcesImpl));
            }
    } <span class="hljs-keyword">catch</span> (Exception e) {
        e.printStackTrace();</code><ul class="pre-numbering" style=""><li style="color: rgb(153, 153, 153);">1</li><li style="color: rgb(153, 153, 153);">2</li><li style="color: rgb(153, 153, 153);">3</li><li style="color: rgb(153, 153, 153);">4</li><li style="color: rgb(153, 153, 153);">5</li><li style="color: rgb(153, 153, 153);">6</li><li style="color: rgb(153, 153, 153);">7</li><li style="color: rgb(153, 153, 153);">8</li><li style="color: rgb(153, 153, 153);">9</li><li style="color: rgb(153, 153, 153);">10</li><li style="color: rgb(153, 153, 153);">11</li><li style="color: rgb(153, 153, 153);">12</li><li style="color: rgb(153, 153, 153);">13</li><li style="color: rgb(153, 153, 153);">14</li><li style="color: rgb(153, 153, 153);">15</li><li style="color: rgb(153, 153, 153);">16</li><li style="color: rgb(153, 153, 153);">17</li><li style="color: rgb(153, 153, 153);">18</li><li style="color: rgb(153, 153, 153);">19</li><li style="color: rgb(153, 153, 153);">20</li></ul></pre>


<p>注意下上述代码hook了几个地方,包括以下几个hook点: <br>
替换了主工程context中LoadedApk的mResource对象。 <br>
将新的Resource添加到主工程ActivityThread的mResourceManager中,并且根据Android版本做了不同处理。 <br>
第三步:关联resource和Activity</p>






<pre class="prettyprint" name="code"><code class="hljs avrasm has-numbering">Activity activity = mBase<span class="hljs-preprocessor">.newActivity</span>(plugin<span class="hljs-preprocessor">.getClassLoader</span>(), targetClassName, intent)<span class="hljs-comment">;</span>
activity<span class="hljs-preprocessor">.setIntent</span>(intent)<span class="hljs-comment">;</span>
//设置Activity的mResources属性,Activity中访问资源时都通过mResources


ReflectUtil<span class="hljs-preprocessor">.setField</span>(ContextThemeWrapper<span class="hljs-preprocessor">.class</span>, activity, <span class="hljs-string">"mResources"</span>, plugin<span class="hljs-preprocessor">.getResources</span>())<span class="hljs-comment">;</span></code><ul class="pre-numbering" style=""><li style="color: rgb(153, 153, 153);">1</li><li style="color: rgb(153, 153, 153);">2</li><li style="color: rgb(153, 153, 153);">3</li><li style="color: rgb(153, 153, 153);">4</li><li style="color: rgb(153, 153, 153);">5</li></ul></pre>


<p>上述代码是在Activity创建时被调用的(后面会介绍如何hook Activity的创建过程),在activity被构造出来后,需要替换其中的mResources为插件的Resource。由于独立式时主工程的Resource不能访问插件的资源,所以如果不做替换,会产生资源访问错误。</p>


<p>做完以上工作后,则可以在插件的Activity中放心的使用setContentView,inflater等方法加载布局了。</p>






<h3 id="解决资源冲突"><a name="t10"></a>解决资源冲突</h3>


<p>合并式的资源处理方式,会引入资源冲突,原因在于不同插件中的资源id可能相同,所以解决方法就是使得不同的插件资源拥有不同的资源id。</p>


<p>资源id是由8位16进制数表示,表示为0xPPTTNNNN。PP段用来区分包空间,默认只区分了应用资源和系统资源,TT段为资源类型,NNNN段在同一个APK中从0000递增。如下表所示: <br>
<img src="https://img-blog.csdn.net/20180114191720075" alt="这里写图片描述" title=""></p>


<p>所以思路是修改资源ID的PP段,对于不同的插件使用不同的PP段,从而区分不同插件的资源。具体实现方式有两种:</p>


<ul>
<li>修改aapt源码,编译期修改PP段。</li>
<li>修改resources.arsc文件,该文件列出了资源id到具体资源路径的映射。</li>
</ul>






<h1 id="四大组件支持"><a name="t11"></a>四大组件支持</h1>


<p>Android开发中有一些特殊的类,是由系统创建的,并且由系统管理生命周期。如常用的四大组件,Activity,Service,BroadcastReceiver和ContentProvider。 仅仅构造出这些类的实例是没用的,还需要管理组件的生命周期。其中以Activity最为复杂,不同框架采用的方法也不尽相同。下面以Activity为例详细介绍插件化如何支持组件生命周期的管理。 大致分为两种方式:</p>


<ul>
<li>ProxyActivity代理</li>
<li>预埋StubActivity,hook系统启动Activity的过程</li>
</ul>






<h2 id="proxyactivity代理"><a name="t12"></a>ProxyActivity代理</h2>


<p>ProxyActivity代理的方式最早是由dynamic-load-apk提出的,其思想很简单,在主工程中放一个ProxyActivy,启动插件中的Activity时会先启动ProxyActivity,在ProxyActivity中创建插件Activity,并同步生命周期。下图展示了启动插件Activity的过程。 <br>
<img src="https://img-blog.csdn.net/20180114191928737" alt="这里写图片描述" title=""> <br>
具体的过程如下:</p>


<ol>
<li>首先需要通过统一的入口(如图中的PluginManager)启动插件Activity,其内部会将启动的插件Activity信息保存下来,并将intent替换为启动ProxyActivity的intent。</li>
<li>ProxyActivity根据插件的信息拿到该插件的ClassLoader和Resource,通过反射创建PluginActivity并调用其onCreate方法。</li>
<li>PluginActivty调用的setContentView被重写了,会去调用ProxyActivty的setContentView。由于ProxyActivity重写了getResource返回的是插件的Resource,所以setContentView能够访问到插件中的资源。同样findViewById也是调用ProxyActivity的。</li>
<li>ProxyActivity中的其他生命周期回调函数中调用相应PluginActivity的生命周期。</li>
</ol>


<p><strong>理解ProxyActivity代理方式主要注意两点:</strong></p>


<ul>
<li>ProxyActivity中需要重写getResouces,getAssets,getClassLoader方法返回插件的相应对象。生命周期函数以及和用户交互相关函数,如onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged等需要转发给插件。</li>
<li>PluginActivity中所有调用context的相关的方法,如setContentView,getLayoutInflater,getSystemService等都需要调用ProxyActivity的相应方法。</li>
</ul>


<p><strong>缺点</strong></p>


<ul>
<li>插件中的Activity必须继承PluginActivity,开发侵入性强。</li>
<li>如果想支持Activity的singleTask,singleInstance等launchMode时,需要自己管理Activity栈,实现起来很繁琐。</li>
<li>插件中需要小心处理Context,容易出错。</li>
<li>如果想把之前的模块改造成插件需要很多额外的工作。</li>
</ul>


<p>该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,dynamic-load-apk之后的插件化方案很少继续使用该方式,而是通过hook系统启动Activity的过程,让启动插件中的Activity像启动主工程的Activity一样简单。</p>






<h2 id="hook方式"><a name="t13"></a>hook方式</h2>


<p>在介绍hook方式之前,先用一张图简要的介绍下系统是如何启动一个Activity的。 <br>
<img src="https://img-blog.csdn.net/20180114192244626" alt="这里写图片描述" title=""></p>


<p>上图列出的是启动一个Activity的主要过程,具体步骤如下:</p>


<ol>
<li>Activity1调用startActivity,实际会调用Instrumentation类的execStartActivity方法,Instrumentation是系统用来监控Activity运行的一个类,Activity的整个生命周期都有它的影子。</li>
<li>通过跨进程的binder调用,进入到ActivityManagerService中,其内部会处理Activity栈。之后又通过跨进程调用进入到Activity2所在的进程中。</li>
<li>ApplicationThread是一个binder对象,其运行在binder线程池中,内部包含一个H类,该类继承于类Handler。ApplicationThread将启动Activity2的信息通过H对象发送给主线程。</li>
<li>主线程拿到Activity2的信息后,调用Instrumentation类的newActivity方法,其内通过ClassLoader创建Activity2实例。</li>
</ol>


<p>下面介绍如何通过hook的方式启动插件中的Activity,需要解决以下两个问题:</p>


<ul>
<li>插件中的Activity没有在AndroidManifest中注册,如何绕过检测。</li>
<li>如何构造Activity实例,同步生命周期</li>
</ul>


<p>解决方法有很多种,以VirtualAPK为例,核心思路如下:</p>


<ol>
<li>先在Manifest中预埋StubActivity,启动时hook上图第1步,将Intent替换成StubActivity。</li>
<li>hook第10步,通过插件的ClassLoader反射创建插件Activity</li>
<li>之后Activity的所有生命周期回调都会通知给插件Activity</li>
</ol>






<h3 id="替换系统instrumentation"><a name="t14"></a>替换系统Instrumentation</h3>


<p>VirtualAPK在初始化时会调用hookInstrumentationAndHandler,该方法hook了系统的Instrumentaiton类,由上文可知该类和Activity的启动息息相关。</p>






<pre class="prettyprint" name="code"><code class="hljs java has-numbering"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">hookInstrumentationAndHandler</span>() { 
   <span class="hljs-keyword">try</span> {  
         <span class="hljs-comment">//获取Instrumentation对象</span>
        Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(<span class="hljs-keyword">this</span>.mContext);   
              <span class="hljs-comment">//构造自定义的VAInstrumentation</span>
        <span class="hljs-keyword">final</span> VAInstrumentation instrumentation = <span class="hljs-keyword">new</span> VAInstrumentation(<span class="hljs-keyword">this</span>, baseInstrumentation); 
                     <span class="hljs-comment">//设置ActivityThread的mInstrumentation和mCallBack</span>
        Object activityThread = ReflectUtil.getActivityThread(<span class="hljs-keyword">this</span>.mContext);
        ReflectUtil.setInstrumentation(activityThread, instrumentation);
        ReflectUtil.setHandlerCallback(<span class="hljs-keyword">this</span>.mContext, instrumentation); 
         <span class="hljs-keyword">this</span>.mInstrumentation = instrumentation;
    } <span class="hljs-keyword">catch</span> (Exception e) {
        e.printStackTrace();
    }
 }</code><ul class="pre-numbering" style=""><li style="color: rgb(153, 153, 153);">1</li><li style="color: rgb(153, 153, 153);">2</li><li style="color: rgb(153, 153, 153);">3</li><li style="color: rgb(153, 153, 153);">4</li><li style="color: rgb(153, 153, 153);">5</li><li style="color: rgb(153, 153, 153);">6</li><li style="color: rgb(153, 153, 153);">7</li><li style="color: rgb(153, 153, 153);">8</li><li style="color: rgb(153, 153, 153);">9</li><li style="color: rgb(153, 153, 153);">10</li><li style="color: rgb(153, 153, 153);">11</li><li style="color: rgb(153, 153, 153);">12</li><li style="color: rgb(153, 153, 153);">13</li><li style="color: rgb(153, 153, 153);">14</li><li style="color: rgb(153, 153, 153);">15</li></ul></pre>


<p>该段代码将主线程中的Instrumentation对象替换成了自定义的VAInstrumentation类。在启动和创建插件activity时,该类都会偷偷做一些手脚。</p>






<h3 id="hook-activity启动过程"><a name="t15"></a>hook activity启动过程</h3>


<p>VAInstrumentation类重写了execStartActivity方法,相关代码如下:</p>






<pre class="prettyprint" name="code"><code class="hljs cs has-numbering"><span class="hljs-keyword">public</span> ActivityResult <span class="hljs-title">execStartActivity</span>(
    //省略了无关参数
    Intent intent) {
<span class="hljs-comment">//转换隐式intent</span>
    mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent); 
   <span class="hljs-keyword">if</span> (intent.getComponent() != <span class="hljs-keyword">null</span>) {  
      <span class="hljs-comment">//替换intent中启动Activity为StubActivity</span>
        <span class="hljs-keyword">this</span>.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
    }        


    <span class="hljs-comment">//调用父类启动Activity的方法}</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">markIntentIfNeeded</span>(Intent intent) { 
   <span class="hljs-keyword">if</span> (intent.getComponent() == <span class="hljs-keyword">null</span>) {  
         <span class="hljs-keyword">return</span>;
    }


    String targetPackageName = intent.getComponent().getPackageName();
    String targetClassName = intent.getComponent().getClassName();    <span class="hljs-comment">// search map and return specific launchmode stub activity</span>
    <span class="hljs-keyword">if</span> (!targetPackageName.equals(mContext.getPackageName()) &amp;&amp; mPluginManager.getLoadedPlugin(targetPackageName) != <span class="hljs-keyword">null</span>) {
        intent.putExtra(Constants.KEY_IS_PLUGIN, <span class="hljs-keyword">true</span>);
        intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
        intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
        dispatchStubActivity(intent);
    }
}</code><ul class="pre-numbering" style=""><li style="color: rgb(153, 153, 153);">1</li><li style="color: rgb(153, 153, 153);">2</li><li style="color: rgb(153, 153, 153);">3</li><li style="color: rgb(153, 153, 153);">4</li><li style="color: rgb(153, 153, 153);">5</li><li style="color: rgb(153, 153, 153);">6</li><li style="color: rgb(153, 153, 153);">7</li><li style="color: rgb(153, 153, 153);">8</li><li style="color: rgb(153, 153, 153);">9</li><li style="color: rgb(153, 153, 153);">10</li><li style="color: rgb(153, 153, 153);">11</li><li style="color: rgb(153, 153, 153);">12</li><li style="color: rgb(153, 153, 153);">13</li><li style="color: rgb(153, 153, 153);">14</li><li style="color: rgb(153, 153, 153);">15</li><li style="color: rgb(153, 153, 153);">16</li><li style="color: rgb(153, 153, 153);">17</li><li style="color: rgb(153, 153, 153);">18</li><li style="color: rgb(153, 153, 153);">19</li><li style="color: rgb(153, 153, 153);">20</li><li style="color: rgb(153, 153, 153);">21</li><li style="color: rgb(153, 153, 153);">22</li><li style="color: rgb(153, 153, 153);">23</li><li style="color: rgb(153, 153, 153);">24</li><li style="color: rgb(153, 153, 153);">25</li></ul></pre>


<p>execStartActivity中会先去处理隐式intent,如果该隐式intent匹配到了插件中的Activity,将其转换成显式。之后通过markIntentIfNeeded将待启动的的插件Activity替换成了预先在AndroidManifest中占坑的StubActivity,并将插件Activity的信息保存到该intent中。其中有个dispatchStubActivity函数,会根据Activity的launchMode选择具体启动哪个StubActivity。VirtualAPK为了支持Activity的launchMode在主工程的AndroidManifest中对于每种启动模式的Activity都预埋了多个坑位。</p>






<h3 id="hook-activity的创建过程"><a name="t16"></a>hook Activity的创建过程</h3>


<p>上一步欺骗了系统,让系统以为自己启动的是一个正常的Activity。当来到图 3.2的第10步时,再将插件的Activity换回来。此时调用的是VAInstrumentation类的newActivity方法。</p>






<pre class="prettyprint" name="code"><code class="hljs java has-numbering"><span class="hljs-annotation">@Override</span>
<span class="hljs-keyword">public</span> Activity <span class="hljs-title">newActivity</span>(ClassLoader cl, String className, Intent intent){
    <span class="hljs-keyword">try</span> {
        cl.loadClass(className);
    } <span class="hljs-keyword">catch</span> (ClassNotFoundException e) {
        <span class="hljs-comment">//通过LoadedPlugin可以获取插件的ClassLoader和Resource</span>
        LoadedPlugin plugin = <span class="hljs-keyword">this</span>.mPluginManager.getLoadedPlugin(intent);
                <span class="hljs-comment">//获取插件的主Activity</span>
        String targetClassName = PluginUtil.getTargetActivity(intent);
                <span class="hljs-keyword">if</span> (targetClassName != <span class="hljs-keyword">null</span>) { 
                   <span class="hljs-comment">//传入插件的ClassLoader构造插件Activity</span>
            Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
            activity.setIntent(intent);
                    <span class="hljs-comment">//设置插件的Resource,从而可以支持插件中资源的访问</span>
            <span class="hljs-keyword">try</span> {
                ReflectUtil.setField(ContextThemeWrapper.class, activity, <span class="hljs-string">"mResources"</span>, plugin.getResources());
            } <span class="hljs-keyword">catch</span> (Exception ignored) { 
                                   <span class="hljs-comment">// ignored.</span>
            }  
          <span class="hljs-keyword">return</span> activity;
        }
    }    <span class="hljs-keyword">return</span> mBase.newActivity(cl, className, intent);
}</code><ul class="pre-numbering" style=""><li style="color: rgb(153, 153, 153);">1</li><li style="color: rgb(153, 153, 153);">2</li><li style="color: rgb(153, 153, 153);">3</li><li style="color: rgb(153, 153, 153);">4</li><li style="color: rgb(153, 153, 153);">5</li><li style="color: rgb(153, 153, 153);">6</li><li style="color: rgb(153, 153, 153);">7</li><li style="color: rgb(153, 153, 153);">8</li><li style="color: rgb(153, 153, 153);">9</li><li style="color: rgb(153, 153, 153);">10</li><li style="color: rgb(153, 153, 153);">11</li><li style="color: rgb(153, 153, 153);">12</li><li style="color: rgb(153, 153, 153);">13</li><li style="color: rgb(153, 153, 153);">14</li><li style="color: rgb(153, 153, 153);">15</li><li style="color: rgb(153, 153, 153);">16</li><li style="color: rgb(153, 153, 153);">17</li><li style="color: rgb(153, 153, 153);">18</li><li style="color: rgb(153, 153, 153);">19</li><li style="color: rgb(153, 153, 153);">20</li><li style="color: rgb(153, 153, 153);">21</li><li style="color: rgb(153, 153, 153);">22</li><li style="color: rgb(153, 153, 153);">23</li></ul></pre>


<p>由于AndroidManifest中预埋的StubActivity并没有具体的实现类,所以此时会发生ClassNotFoundException。之后在处理异常时取出插件Activity的信息,通过插件的ClassLoader反射构造插件的Activity。</p>






<h3 id="其他操作"><a name="t17"></a>其他操作</h3>


<p>插件Activity构造出来后,为了能够保证其正常运行还要做些额外的工作。</p>






<pre class="prettyprint" name="code"><code class="hljs avrasm has-numbering">@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) { 
   final Intent intent = activity<span class="hljs-preprocessor">.getIntent</span>()<span class="hljs-comment">;</span>
       if (PluginUtil<span class="hljs-preprocessor">.isIntentFromPlugin</span>(intent)) {
        Context base = activity<span class="hljs-preprocessor">.getBaseContext</span>()<span class="hljs-comment">;</span>
               try {
            LoadedPlugin plugin = this<span class="hljs-preprocessor">.mPluginManager</span><span class="hljs-preprocessor">.getLoadedPlugin</span>(intent)<span class="hljs-comment">;</span>
            ReflectUtil<span class="hljs-preprocessor">.setField</span>(base<span class="hljs-preprocessor">.getClass</span>(), base, <span class="hljs-string">"mResources"</span>, plugin<span class="hljs-preprocessor">.getResources</span>())<span class="hljs-comment">;</span>
            ReflectUtil<span class="hljs-preprocessor">.setField</span>(ContextWrapper<span class="hljs-preprocessor">.class</span>, activity, <span class="hljs-string">"mBase"</span>, plugin<span class="hljs-preprocessor">.getPluginContext</span>())<span class="hljs-comment">;</span>
            ReflectUtil<span class="hljs-preprocessor">.setField</span>(Activity<span class="hljs-preprocessor">.class</span>, activity, <span class="hljs-string">"mApplication"</span>, plugin<span class="hljs-preprocessor">.getApplication</span>())<span class="hljs-comment">;</span>
            ReflectUtil<span class="hljs-preprocessor">.setFieldNoException</span>(ContextThemeWrapper<span class="hljs-preprocessor">.class</span>, activity, <span class="hljs-string">"mBase"</span>, plugin<span class="hljs-preprocessor">.getPluginContext</span>())<span class="hljs-comment">;</span>
               // <span class="hljs-keyword">set</span> screenOrientation
            ActivityInfo activityInfo = plugin<span class="hljs-preprocessor">.getActivityInfo</span>(PluginUtil<span class="hljs-preprocessor">.getComponent</span>(intent))<span class="hljs-comment">; </span>
              if (activityInfo<span class="hljs-preprocessor">.screenOrientation</span> != ActivityInfo<span class="hljs-preprocessor">.SCREEN</span>_ORIENTATION_UNSPECIFIED) {
                activity<span class="hljs-preprocessor">.setRequestedOrientation</span>(activityInfo<span class="hljs-preprocessor">.screenOrientation</span>)<span class="hljs-comment">;</span>
            }
        } catch (Exception e) {
            e<span class="hljs-preprocessor">.printStackTrace</span>()<span class="hljs-comment">;</span>
        }


    }


    mBase<span class="hljs-preprocessor">.callActivityOnCreate</span>(activity, icicle)<span class="hljs-comment">;</span>
}</code><ul class="pre-numbering" style=""><li style="color: rgb(153, 153, 153);">1</li><li style="color: rgb(153, 153, 153);">2</li><li style="color: rgb(153, 153, 153);">3</li><li style="color: rgb(153, 153, 153);">4</li><li style="color: rgb(153, 153, 153);">5</li><li style="color: rgb(153, 153, 153);">6</li><li style="color: rgb(153, 153, 153);">7</li><li style="color: rgb(153, 153, 153);">8</li><li style="color: rgb(153, 153, 153);">9</li><li style="color: rgb(153, 153, 153);">10</li><li style="color: rgb(153, 153, 153);">11</li><li style="color: rgb(153, 153, 153);">12</li><li style="color: rgb(153, 153, 153);">13</li><li style="color: rgb(153, 153, 153);">14</li><li style="color: rgb(153, 153, 153);">15</li><li style="color: rgb(153, 153, 153);">16</li><li style="color: rgb(153, 153, 153);">17</li><li style="color: rgb(153, 153, 153);">18</li><li style="color: rgb(153, 153, 153);">19</li><li style="color: rgb(153, 153, 153);">20</li><li style="color: rgb(153, 153, 153);">21</li><li style="color: rgb(153, 153, 153);">22</li><li style="color: rgb(153, 153, 153);">23</li><li style="color: rgb(153, 153, 153);">24</li></ul></pre>


<p>这段代码主要是将Activity中的Resource,Context等对象替换成了插件的相应对象,保证插件Activity在调用涉及到Context的方法时能够正确运行。</p>


<p>经过上述步骤后,便实现了插件Activity的启动,并且该插件Activity中并不需要什么额外的处理,和常规的Activity一样。那问题来了,之后的onResume,onStop等生命周期怎么办呢?答案是所有和Activity相关的生命周期函数,系统都会调用插件中的Activity。原因在于AMS在处理Activity时,通过一个token表示具体Activity对象,而这个token正是和启动Activity时创建的对象对应的,而这个Activity被我们替换成了插件中的Activity,所以之后AMS的所有调用都会传给插件中的Activity。</p>






<h2 id="其他组件"><a name="t18"></a>其他组件</h2>


<p>四大组件中Activity的支持是最复杂的,其他组件的实现原理要简单很多,简要概括如下:</p>


<ul>
<li><strong>Service</strong>:Service和Activity的差别在于,Activity的生命周期是由用户交互决定的,而Service的生命周期是我们通过代码主动调用的,且Service实例和manifest中注册的是一一对应的。实现Service插件化的思路是通过在manifest中预埋StubService,hook系统startService等调用替换启动的Service,之后在StubService中创建插件Service,并手动管理其生命周期。</li>
<li><strong>BroadCastReceiver</strong>:解析插件的manifest,将静态注册的广播转为动态注册。</li>
<li><strong>ContentProvider</strong>:类似于Service的方式,对插件ContentProvider的所有调用都会通过一个在manifest中占坑的ContentProvider分发。</li>
</ul>






<h1 id="小结"><a name="t19"></a>小结</h1>


<p>VirtualAPK通过替换了系统的Instrumentation,hook了Activity的启动和创建,省去了手动管理插件Activity生命周期的繁琐,让插件Activity像正常的Activity一样被系统管理,并且插件Activity在开发时和常规一样,即能独立运行又能作为插件被主工程调用。</p>


<p>其他插件框架在处理Activity时思想大都差不多,无非是这两种方式之一或者两者的结合。在hook时,不同的框架可能会选择不同的hook点。如360的RePlugin框架选择hook了系统的ClassLoader,即图3.2中构造Activity2的ClassLoader,在判断出待启动的Activity是插件中的时,会调用插件的ClassLoader构造相应对象。另外RePlugin为了系统稳定性,选择了尽量少的hook,因此它并没有选择hook系统的startActivity方法来替换intent,而是通过重写Activity的startActivity,因此其插件Activity是需要继承一个类似PluginActivity的基类的。不过RePlugin提供了一个Gradle插件将插件中的Activity的基类换成了PluginActivity,用户在开发插件Activity时也是没有感知的。</p>            </div>
            <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css">
                </div>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值