• 【Android开发学习笔记】【高级】【随笔】插件化——初探


    背景


      随着tencentmap项目的越来越庞大,终于有一天我们的App在Android 2.*以下手机上安装时出现INSTALL_FAILED_DEXOPT,导致安装失败。

      INSTALL_FAILED_DEXOPT导致无法安装的问题,从根本上来说,可能是两个原因造成的:

      (1)单个dex文件方法总数65K的限制。

      (2)Dexopt的LinearAlloc限制。

      当Android系统安装一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt是在第一次加载Dex文件的时候执行的。这个过程会生成一个ODEX文件,即Optimised Dex。执行ODEX的效率会比直接执行Dex文件的效率要高很多。

      但是在早期的Android系统中,DexOpt有两个问题。

      (1)DexOpt会把每一个类的方法id检索起来,存在一个链表结构里面,但是这个链表的长度是用一个short类型来保存的,导致了方法id的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。

      (2):Dexopt使用LinearAlloc来存储应用的方法信息。Dalvik LinearAlloc是一个固定大小的缓冲区。在Android版本的历史上,LinearAlloc分别经历了4M/5M/8M/16M限制。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB或16MB。当方法数量过多导致超出缓冲区大小时,也会造成dexopt崩溃。

      尽管在新版本的Android系统中,DexOpt修复了方法数65K的限制问题,并且扩大了LinearAlloc限制,但是我们仍然需要对低版本的Android系统做兼容。

     ——p.s. 上面内容是我的一位同事jintaoli在研究dex分包的时候总结的,我觉得很详细,因此在征得他同意后贴了上来,非常感谢

      因此我们决定去做“插件化” 这件事,将与核心地图业务逻辑关联不大的模块进行插件化,从而来解决掉上述问题。

    优势&原理


      也有人可能会说,如果代码中的方法数太多的话,是否可以将native替换成H5呢,这样不是也可以解决问题吗? 当然这也是可以的,但得“插件化”相比较H5来说更有优势:

      1、模块间真正的解耦;

      2、开发时可以并行完成,更加高效;

      3、按需加载,减少App内存;

      4、插件动态升级,不用在发fix版本;

      5、主App安装包体积减小,升级时更节省流量。

      为了这么多的好处,我们至少也应该去尝试一下插件化吧。

      目前tencentmap采用动态加载Apk的方法。关于动态加载apk,理论上可以用到的有DexClassLoader、PathClassLoader和URLClassLoader,我们先来看看这三种方法的差别:

      DexClassLoader :可以加载文件系统上的jar、dex、apk
      PathClassLoader :可以加载/data/app目录下的apk,因此只能加载已经安装的apk
      URLClassLoader :可以加载jar,但是由于dalvik不能直接识别jar,所以此方法在android中无法使用。

      因此我们选择更适合我们的DexClassLoader。 

      具体实现是:宿主程序启动一个代理的Activity,在这个Activity中通过dexClassLoader将插件App动态的加载进来,我们拿到实例之后,通过反射的方法来执行插件中的接口,从而实现插件在宿主中运行。

    实现


     一、宿主程序

      宿主的MainActiviy有一句话和一个按钮,如图:

      点击按钮就会调转到ProxyActivity,跳转的过程中,传递一个插件apk所在的路径。代码如下:

     1     class GoPlugin implements OnClickListener
     2     {
     3         @Override
     4         public void onClick(View arg0) 
     5         {
     6             Intent intent = new Intent();
     7             intent.setClass(MainActivity.this, ProxyActivity.class);
     8             intent.putExtra("ExtraDexPath", "/mnt/sdcard/plugin.apk");        
     9             startActivity(intent);
    10         }  
    11     }

      紧接着,ProxyActivity被唤起,我们看一下ProxyActivity中如何处理。代码如下:

     1 package com.bryan.host;
     2 
     3 import java.lang.reflect.Constructor;
     4 import java.lang.reflect.InvocationTargetException;
     5 import java.lang.reflect.Method;
     6 
     7 import dalvik.system.DexClassLoader;
     8 
     9 import android.annotation.SuppressLint;
    10 import android.app.Activity;
    11 import android.content.pm.PackageInfo;
    12 import android.content.pm.PackageManager;
    13 import android.os.Bundle;
    14 
    15 public class ProxyActivity extends Activity
    16 {
    17     /* 接收mainActivity传来的*/
    18     protected String mExtraClass;  
    19     protected String mExtraDexPath;
    20     
    21     /* classloder来的object*/
    22     protected Class<?> mlocaClass;
    23     protected Object mobject; 
    24 
    25     
    26     @Override
    27     protected void onCreate(Bundle savedInstanceState) 
    28     {
    29         super.onCreate(savedInstanceState);
    30         
    31         mExtraClass = getIntent().getStringExtra("ExtraClass");
    32         mExtraDexPath = getIntent().getStringExtra("ExtraDexPath");
    33         
    34         if (mExtraClass == null) 
    35         {
    36             OpenDefaultActivity();
    37         }
    38         else
    39         {
    40             OpenAppointActivity(mExtraClass);
    41         }        
    42     }
    43     
    44     /* 加载插件的主activity*/
    45     protected void OpenDefaultActivity()
    46     {
    47         PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(mExtraDexPath, PackageManager.GET_ACTIVITIES);
    48         if ((packageInfo.activities != null) && (packageInfo.activities.length >0)) 
    49         {
    50             mExtraClass = packageInfo.activities[0].name;
    51             OpenAppointActivity(mExtraClass);
    52         }
    53     }
    54     
    55     /* 加载插件的指定activity*/
    56     @SuppressLint("NewApi") protected void OpenAppointActivity(final String className)
    57     {
    58         File dexOutputDir = this.getDir("dex", 0);  
    59         final String dexOutputPath = dexOutputDir.getAbsolutePath();  
    60         
    61         DexClassLoader dexClassLoader = new DexClassLoader(mExtraDexPath, dexOutputPath, null,  ClassLoader.getSystemClassLoader() );
    62         
    63         try 
    64         {
    65 66             mlocaClass = dexClassLoader.loadClass(className);
    67             Constructor<?> localConstructor = mlocaClass.getConstructor(new Class[]{});
    68             mobject = localConstructor.newInstance(new Object[]{});
    69             
    70             /* 反射 调用插件中的设置代理 */
    71             Method setProxy = mlocaClass.getMethod("setProxy", new Class[] {Activity.class});
    72             setProxy.setAccessible(true);
    73             setProxy.invoke(mobject, new Object[]{this});
    74             /* 反射告诉插件是被宿主调起的*/
    75             Method onCreate = mlocaClass.getDeclaredMethod("onCreate", new Class[] {Bundle.class});
    76             onCreate.setAccessible(true);
    77             Bundle bundle = new Bundle();  
    78             bundle.putInt("Host", 1);  
    79             onCreate.invoke(mobject, new Object[]{bundle});
    80             
    81         } catch (Exception e) 
    82         {
    83             e.printStackTrace();  
    84         }
    85     }
    86 }

      从上面代码不难看出,执行onCreate之后,首先会进入OpenDefaultActivity,其实这里就是获取了一下插件安装包的主Activity,然后调用OpenAppointActivity(className),这里面是真正动态加载的过程,代码61到68行就是加载的过程。加载完毕之后,我们就得到了插件主Activity的Class对象和Object对象,因此利用这两个对象进行反射。

      反射一共调用了两个方法:setProxy是将当前ProxyActivity的对象传递给插件,让插件实际是在调用代理当中的内容,另外就是调用onCreate,因为我们通过classloader加载进来插件工程的MainActiviy后,该类就变成了一个普通的类,启动的过程中它的生命周期函数就会失效,因此我们需要反射的将onCreate执行,同时传递一个int值给插件让插件知道,它自己是被宿主程序调用起来的,而不是自己起来的,这样可以让插件针对这两种不同的情况做不同的处理(具体可以看插件的代码)。 

    二、插件程序

      为了让ProxyActivity可以接管插件中Activity的操作,我们可以定义一个基类BaseActivity来处理代理相关的事情,同时对是否使用的代理,做出两种处理方式,这样继承了BaseActivity的Activity在使用的时候,还是正常的使用,不会有感知,而BaseActivity就需要处理好插件工程独立运行时和被代理运行时的区别。我们可以看看实现:

      1 package com.bryan.plugin;
      2 
      3 
      4 import android.app.Activity;
      5 import android.content.Intent;
      6 import android.content.res.AssetManager;
      7 import android.content.res.Resources;
      8 import android.os.Bundle;
      9 import android.view.View;
     10 import android.view.ViewGroup.LayoutParams;
     11 
     12 public class BaseActivity extends Activity
     13 {
     14     /* 宿主工程中的代理Activity*/
     15     protected Activity mProxyActivity;
     16     
     17     /* 判断是被谁调起的,如果是宿主调起的为1 */
     18     int Who = 0;
     19     
     20     public void setProxy(Activity proxyActivity)
     21     {
     22         mProxyActivity = proxyActivity;
     23     }
     24 
     25     @Override
     26     protected void onCreate(Bundle savedInstanceState) 
     27     {        
     28         if (savedInstanceState != null) 
     29         {
     30             Who = savedInstanceState.getInt("Host", 0);
     31         }
     32         if (Who == 0) 
     33         {
     34             super.onCreate(savedInstanceState);
     35             mProxyActivity = this;
     36         }
     37     }
     38     
     39     protected void StartActivityByProxy(String className)
     40     {
     41         if (mProxyActivity == this) 
     42         {
     43             Intent intent = new Intent();
     44             intent.setClassName(this, className);
     45             this.startActivity(intent);
     46         }
     47         else 
     48         {
     49             Intent intent = new Intent();
     50             intent.setAction("android.intent.action.ProxyVIEW");
     51             intent.putExtra("ExtraDexPath", "/mnt/sdcard/plugin.apk");
     52             intent.putExtra("ExtraClass", className);
     53             mProxyActivity.startActivity(intent);
     54         }
     55     }
     56 
     57     
     58     /* 重写几个重要的添加布局的类 */    
     59     @Override
     60     public void setContentView(View view) 
     61     {
     62         if (mProxyActivity == this) 
     63         {
     64             super.setContentView(view);
     65         }
     66         else
     67         {
     68             mProxyActivity.setContentView(view);
     69         }    
     70     }
     71 
     72 
     73     @Override
     74     public void addContentView(View view, LayoutParams params) 
     75     {
     76         if (mProxyActivity == this) 
     77         {
     78             super.addContentView(view, params);
     79         }
     80         else 
     81         {
     82             mProxyActivity.addContentView(view, params);
     83         }    
     84     }
     85 
     86     
     87     @Override
     88     public void setContentView(int layoutResID) 
     89     {
     90         if (mProxyActivity == this) 
     91         {
     92             super.setContentView(layoutResID);
     93         }
     94         else 
     95         {
     96             mProxyActivity.setContentView(layoutResID);
     97         }
     98         
     99     }
    100 
    101 
    102     @Override
    103     public void setContentView(View view, LayoutParams params) 
    104     {
    105         if (mProxyActivity == this) 
    106         {
    107             super.setContentView(view, params);
    108         }
    109         else 
    110         {
    111             mProxyActivity.setContentView(view, params);
    112         }
    113     }
    114 
    115 }

      从上面的代码可以看出,当插件程序是自己启动的话,走入OnCreate,最终mProxyActivity就是BaseActivity,而当插件是被宿主调起的话,执行了setProxy()后,mProxyActivity实际上就是宿主工程中的ProxyActivity。因此后面的函数在实现的时候需要判断一次,如果不是被宿主启动,那么还走原来的流程,如果是宿主启动,走宿主中的该方法。这里需要重点说明一下StartActivityByProxy(className)这个函数:

     1     protected void StartActivityByProxy(String className)
     2     {
     3         if (mProxyActivity == this) 
     4         {
     5             Intent intent = new Intent();
     6             intent.setClassName(this, className);
     7             this.startActivity(intent);
     8         }
     9         else 
    10         {
    11             Intent intent = new Intent();
    12             intent.setAction("android.intent.action.ProxyVIEW");
    13             intent.putExtra("ExtraDexPath", "/mnt/sdcard/plugin.apk");
    14             intent.putExtra("ExtraClass", className);
    15             mProxyActivity.startActivity(intent);
    16         }
    17     }

      根据函数名,我们就可以知道这个方法是用来启动一个Activity的,当插件是自启动的时候,Intent我们采用显式调用的方式,将我们要启动的Activity拉起。而当我们是宿主代理启动时,因为宿主和插件工程不在同一个工程,因此显示调用是不行的,而隐式调用的方法,前提是必须要App安装,但是我们的插件动态加载技术是不需要安装App,这个地方刚开始困扰了我好久,后来才明白,这里的action需要配置在宿主的ProxyActivity中。这样新起来的Activity依旧是被ProxyActivity代理,所以就形成了一个循环。

      接下来实现插件工程的入口类,由于宿主接管了插件后,插件的Context对象就变成了宿主的Context,而这样的话我们就没有办法去通过Context对象去获取我们的资源,因此入口类的UI布局需要用代码进行动态布局,如下所示:

     1 package com.bryan.plugin;
     2 
     3 import android.content.Context;
     4 import android.graphics.Color;
     5 import android.os.Bundle;
     6 import android.view.View;
     7 import android.view.View.OnClickListener;
     8 import android.view.ViewGroup.LayoutParams;
     9 import android.widget.Button;
    10 import android.widget.LinearLayout;
    11 
    12 public class MainActivity extends BaseActivity {
    13 
    14     @Override
    15     protected void onCreate(Bundle savedInstanceState) {
    16         super.onCreate(savedInstanceState);
    17         //setContentView(R.layout.activity_main);
    18         
    19         // 初始化处理布局
    20         InitView();
    21     }
    22     
    23     private void InitView()
    24     {
    25         View view = CreateView(mProxyActivity); 
    26         mProxyActivity.setContentView(view);
    27     }
    28     
    29     private View CreateView(final Context context)
    30     {
    31         LinearLayout linearLayout = new LinearLayout(context);
    32         
    33         linearLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    34         linearLayout.setBackgroundColor(Color.parseColor("#F4F4D6"));        
    35         Button button = new Button(context);  
    36         button.setText("plugin button");  
    37         linearLayout.addView(button, LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT); 
    38    
    39         button.setOnClickListener(new OnClickListener() {            
    40             @Override
    41             public void onClick(View v) {
    42                 StartActivityByProxy("com.bryan.plugin.TestActivity");
    43             }
    44         });
    45         return linearLayout;    
    46     }
    47 
    48  
    49 }

      代码中42行StartActivityByProxy() 就是启动一个新的Activity的方式,前面已经介绍是通过隐式调用调起ProxyActivity,然后动态加载com.bryan.plugin.TestActivity类的方法,这里不再赘述,我们看下子Activity的实现吧:

     1 package com.bryan.plugin;
     2 
     3 import android.content.Context;
     4 import android.os.Bundle;
     5 import android.view.View;
     6 import android.view.ViewGroup.LayoutParams;
     7 import android.widget.LinearLayout;
     8 import android.widget.TextView;
     9 
    10 public class TestActivity extends BaseActivity
    11 {
    12     @Override
    13     protected void onCreate(Bundle savedInstanceState) 
    14     {
    15         super.onCreate(savedInstanceState);    
    16         mProxyActivity.setContentView(CreateView(mProxyActivity));
    17     }
    18     
    19     private View CreateView(final Context context)
    20     {
    21         LinearLayout linearLayout = new LinearLayout(context);
    22         
    23         linearLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    24         TextView textView = new TextView(context);
    25         textView.setText("bryan test plugin");
    26         linearLayout.addView(textView);
    27         return linearLayout;
    28     }
    29 }

      同样布局采用的也是代码来创建的。

    结果


      我们下看看插件独立运行时的情景:

     

      再看看通过宿主程序拉起的效果:

      可以看到,被独立运行和插件运行,执行效果是一样的,但是由于采用了反射,所以执行效率会略微降低,其次,我们可以看到应用的标题发生了改变,这也说明,尽管apk在宿主程序中被执行了,但是它毕竟是在宿主程序里面执行的,所以它还是属于宿主程序的,因此apk未安装被执行时其标题不是自己的。

     

    改进


      到此为止,我们已经实现了一个插件化的demo,但是仍然存在不少的问题:

      1、资源无法加载:由于插件的Context对象已经被宿主程序接管,因此无法通过Context对象获得插件的资源文件,因此无法加载。

      2、Activity的生命周期函数失效:宿主使用classloader加载插件进来后,插件的Activity就被当成了一个普通的类来处理,此时系统就不再接管它的生命周期。

      那么我们如果解决这样的问题呢?看下文吧

     

  • 相关阅读:
    游记&退役记
    Nothing to say
    学习知识点的比较好的blog
    计划做题列表
    后缀自动机小专题
    复数
    FFT学习
    P2900 [USACO08MAR]土地征用Land Acquisition
    # 数位DP入坑
    Hdu 4035 Maze(概率DP)
  • 原文地址:https://www.cnblogs.com/by-dream/p/5029164.html
Copyright © 2020-2023  润新知