背景
随着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就被当成了一个普通的类来处理,此时系统就不再接管它的生命周期。
那么我们如果解决这样的问题呢?看下文吧