【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就被当成了一个普通的类来处理,此时系统就不再接管它的生命周期。

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

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值