Android 插件化框架 DL 学习笔记
原文链接 http://www.rogerblog.cn/2016/10/28/DL/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。
Android 插件化框架 DL 学习笔记
早在两年前, android 的插件化技术就火得不行,一直只是浅略的翻看一些博客,没有深入研究其原理及其所使用到的新技术,这段时间恶补了一下这方面的知识,准备在博客中记录一下,做一些输出,加深理解。
Android 的插件化技术现在比较火的,比较完善的框架有两套,第一个是任玉刚大神写的 DL 框架 Link ,第二个是 360手机助手的一种新的插件机制 Link,这两种插件化方式可以说是有本质上的区别的,下面对这 DL 框架的实现进行简单的总结。
DL 框架的实现原理可以参考玉刚大神的博客 Link,其主要思想是:
在宿主apk中,有一个ProxyActivity,即代理Activity,这个Activity相当于一个空壳,插件中的Activity依靠ProxyActivity来对生命周期回调、资源加载以及启动另一个Activity等等。总而言之,ProxyActivity提供Context,插件Activity依靠ProxyActivity来做自己想做的事情。
如图,我们可以看到,图中那个发起请求的 activity 是宿主中正常的 activity,它通过一个 intent 调起 proxy , proxy 就是这个代理 acitivity ;在这个代理 activity 中再去加载位于 sd 卡中的插件 apk ,其中主要的困难有两点,第一个是插件 apk 中资源文件的访问,由于插件 apk 并未实际安装,所有插件 apk 中的资源无法通过宿主程序的上下文获取,所以必须通过其他方法来读取插件中的资源到宿主程序中来使用。第二个是插件 apk 的生命周期问题,一般来说 android 中的生命周期是由 android framework 来管理的,但是插件并未安装,所以所有的生命周期必须由我们自己来控制。暂时不管这两个问题,下面记录一下宿主程序如何调用插件程序的 apk , 主要通过一下代码:
1. @Override
2. protected void onCreate(Bundle savedInstanceState) {
3. super.onCreate(savedInstanceState);
4. mDexPath = getIntent().getStringExtra(EXTRA_DEX_PATH);
5. mClass = getIntent().getStringExtra(EXTRA_CLASS);
6.
7. Log.d(TAG, "mClass=" + mClass + " mDexPath=" + mDexPath);
8. if (mClass == null) {
9. launchTargetActivity();
10. } else {
11. launchTargetActivity(mClass);
12. }
13. }
14.
15. @SuppressLint("NewApi")
16. protected void launchTargetActivity() {
17. PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(
18. mDexPath, 1);
19. if ((packageInfo.activities != null)
20. && (packageInfo.activities.length > 0)) {
21. String activityName = packageInfo.activities[0].name;
22. mClass = activityName;
23. launchTargetActivity(mClass);
24. }
25. }
26.
27. @SuppressLint("NewApi")
28. protected void launchTargetActivity(final String className) {
29. Log.d(TAG, "start launchTargetActivity, className=" + className);
30. File dexOutputDir = this.getDir("dex", 0);
31. final String dexOutputPath = dexOutputDir.getAbsolutePath();
32. ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();
33. DexClassLoader dexClassLoader = new DexClassLoader(mDexPath,
34. dexOutputPath, null, localClassLoader);
35. try {
36. Class<?> localClass = dexClassLoader.loadClass(className);
37. Constructor<?> localConstructor = localClass
38. .getConstructor(new Class[] {});
39. Object instance = localConstructor.newInstance(new Object[] {});
40. Log.d(TAG, "instance = " + instance);
41.
42. Method setProxy = localClass.getMethod("setProxy",
43. new Class[] { Activity.class });
44. setProxy.setAccessible(true);
45. setProxy.invoke(instance, new Object[] { this });
46.
47. Method onCreate = localClass.getDeclaredMethod("onCreate",
48. new Class[] { Bundle.class });
49. onCreate.setAccessible(true);
50. Bundle bundle = new Bundle();
51. bundle.putInt(FROM, FROM_EXTERNAL);
52. onCreate.invoke(instance, new Object[] { bundle });
53. } catch (Exception e) {
54. e.printStackTrace();
55. }
56. }
在 onCreate 中获取到路径名称和要打开的 mClass 名称,一般第一次加载时 mClass 为空,调用到 launchTargetActivity() 方法进行 apk 包的解析并获取到排第一位的 activity ,接下来调用 launchTargetActivity ,通过 DexClassLoader 将解析到的 class 加载进内存,然后通过反射 (37-39 行)运行类的构造方法, 42-45行 反射调用 setProxy 方法将宿主程序的上下文注入到插件 activity 中,47-52行 反射调用 onCreate 方法,最终将插件程序启动起来。
接下来研究一下如何解决资源加载的问题:
首先我们要知道 Context 中有两个抽象方法是用来获取资源的:
/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();
其真正的实现是在 ContextImpl 中,所以我们可以实现这两个方法来加载资源:
1. @Override
2. public AssetManager getAssets() {
3. return mAssetManager == null ? super.getAssets() : mAssetManager;
4. }
5.
6. @Override
7. public Resources getResources() {
8. return mResources == null ? super.getResources() : mResources;
9. }
其中两个值的赋值逻辑如下:
1. protected void loadResources() {
2. try {
3. AssetManager assetManager = AssetManager.class.newInstance();
4. Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
5. addAssetPath.invoke(assetManager, mDexPath);
6. mAssetManager = assetManager;
7. } catch (Exception e) {
8. e.printStackTrace();
9. }
10. Resources superRes = super.getResources();
11. mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),
12. superRes.getConfiguration());
13. mTheme = mResources.newTheme();
14. mTheme.setTo(super.getTheme());
15. }
玉刚大神的原文是这样说的:
加载的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources中,由于addAssetPath是隐藏api我们无法直接调用,所以只能通过反射,下面是它的声明,通过注释我们可以看出,传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了,然后再通过AssetManager来创建一个新的Resources对象,这个对象就是我们可以使用的apk中的资源了,这样我们的问题就解决了。
下面是解决 Activity 生命周期的管理问题
具体的实现是通过反射的方法来实现插件的 activity 和宿主 activity 的生命周期同步:
1. protected void instantiateLifecircleMethods(Class<?> localClass) {
2. String[] methodNames = new String[] {
3. "onRestart",
4. "onStart",
5. "onResume",
6. "onPause",
7. "onStop",
8. "onDestory"
9. };
10. for (String methodName : methodNames) {
11. Method method = null;
12. try {
13. method = localClass.getDeclaredMethod(methodName, new Class[] { });
14. method.setAccessible(true);
15. } catch (NoSuchMethodException e) {
16. e.printStackTrace();
17. }
18. mActivityLifecircleMethods.put(methodName, method);
19. }
20.
21. Method onCreate = null;
22. try {
23. onCreate = localClass.getDeclaredMethod("onCreate", new Class[] { Bundle.class });
24. onCreate.setAccessible(true);
25. } catch (NoSuchMethodException e) {
26. e.printStackTrace();
27. }
28. mActivityLifecircleMethods.put("onCreate", onCreate);
29.
30. Method onActivityResult = null;
31. try {
32. onActivityResult = localClass.getDeclaredMethod("onActivityResult",
33. new Class[] { int.class, int.class, Intent.class });
34. onActivityResult.setAccessible(true);
35. } catch (NoSuchMethodException e) {
36. e.printStackTrace();
37. }
38. mActivityLifecircleMethods.put("onActivityResult", onActivityResult);
39. }
以上代码将所有的方法放入 mActivityLifecircleMethods 这个 map 中,然后在系统调用代理 activity 生命周期方法时,通过反射来调用插件 activity 的方法:
1. @Override
2. protected void onResume() {
3. super.onResume();
4. Method onResume = mActivityLifecircleMethods.get("onResume");
5. if (onResume != null) {
6. try {
7. onResume.invoke(mRemoteActivity, new Object[] { });
8. } catch (Exception e) {
9. e.printStackTrace();
10. }
11. }
12. }
13.
14. @Override
15. protected void onPause() {
16. Method onPause = mActivityLifecircleMethods.get("onPause");
17. if (onPause != null) {
18. try {
19. onPause.invoke(mRemoteActivity, new Object[] { });
20. } catch (Exception e) {
21. e.printStackTrace();
22. }
23. }
24. super.onPause();
25. }
在初始版本中生命周期的管理基本都是使用反射来调用的,这样对性能的消耗还是比较大的,所以在 DL 2.0 中,已经将管理生命周期的方法改为通过使用接口的方式来处理:
public interface DLPlugin {
public void onStart();
public void onRestart();
public void onActivityResult(int requestCode, int resultCode, Intent data);
public void onResume();
public void onPause();
public void onStop();
public void onDestroy();
public void onCreate(Bundle savedInstanceState);
public void setProxy(Activity proxyActivity, String dexPath);
public void onSaveInstanceState(Bundle outState);
public void onNewIntent(Intent intent);
public void onRestoreInstanceState(Bundle savedInstanceState);
public boolean onTouchEvent(MotionEvent event);
public boolean onKeyUp(int keyCode, KeyEvent event);
public void onWindowAttributesChanged(LayoutParams params);
public void onWindowFocusChanged(boolean hasFocus);
public void onBackPressed();
...
}
在代理类中的实现:
...
@Override
protected void onStart() {
mRemoteActivity.onStart();
super.onStart();
}
@Override
protected void onRestart() {
mRemoteActivity.onRestart();
super.onRestart();
}
@Override
protected void onResume() {
mRemoteActivity.onResume();
super.onResume();
}
@Override
protected void onPause() {
mRemoteActivity.onPause();
super.onPause();
}
@Override
protected void onStop() {
mRemoteActivity.onStop();
super.onStop();
}
...
通过接口来管理大大的提高了性能。
由于插件 apk 是通过代理 activity 的调用来运行的,所以对于插件 apk 中 activity 的代码还是有一些规范需要注意的:
慎用this(接口除外):因为this指向的是当前对象,即apk中的activity,但是由于activity已经不是常规意义上的activity,所以this是没有意义的,但是如果this表示的是一个接口而不是context,比如activity实现了而一个接口,那么this继续有效。
使用that:既然this不能用,那就用that,that是apk中activity的基类BaseActivity中的一个成员,它在apk安装运行的时候指向this,而在未安装的时候指向宿主程序中的代理activity,anyway,that is better than this。
activity的成员方法调用问题:原则来说,需要通过that来调用成员方法,但是由于大部分常用的api已经被重写,所以仅仅是针对部分api才需要通过that去调用用。同时,apk安装以后仍然可以正常运行。
启动新activity的约束:启动外部activity不受限制,启动apk内部的activity有限制,首先由于apk中的activity没注册,所以不支持隐式调用,其次必须通过BaseActivity中定义的新方法startActivityByProxy和startActivityForResultByProxy,还有就是不支持LaunchMode。
目前暂不支持Service、BroadcastReceiver等需要注册才能使用的组件,但广播可以采用代码动态注册。