Android 插件化框架 DL 学习笔记

Keep Learning

Posted by Roger on October 28, 2016

Android 插件化框架 DL 学习笔记

早在两年前, android 的插件化技术就火得不行,一直只是浅略的翻看一些博客,没有深入研究其原理及其所使用到的新技术,这段时间恶补了一下这方面的知识,准备在博客中记录一下,做一些输出,加深理解。

Android 的插件化技术现在比较火的,比较完善的框架有两套,第一个是任玉刚大神写的 DL 框架 Link ,第二个是 360手机助手的一种新的插件机制 Link,这两种插件化方式可以说是有本质上的区别的,下面对这 DL 框架的实现进行简单的总结。

DL 框架的实现原理可以参考玉刚大神的博客 Link,其主要思想是:

在宿主apk中,有一个ProxyActivity,即代理Activity,这个Activity相当于一个空壳,插件中的Activity依靠ProxyActivity来对生命周期回调、资源加载以及启动另一个Activity等等。总而言之,ProxyActivity提供Context,插件Activity依靠ProxyActivity来做自己想做的事情。

image

如图,我们可以看到,图中那个发起请求的 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 的代码还是有一些规范需要注意的:

  1. 慎用this(接口除外):因为this指向的是当前对象,即apk中的activity,但是由于activity已经不是常规意义上的activity,所以this是没有意义的,但是如果this表示的是一个接口而不是context,比如activity实现了而一个接口,那么this继续有效。

  2. 使用that:既然this不能用,那就用that,that是apk中activity的基类BaseActivity中的一个成员,它在apk安装运行的时候指向this,而在未安装的时候指向宿主程序中的代理activity,anyway,that is better than this。

  3. activity的成员方法调用问题:原则来说,需要通过that来调用成员方法,但是由于大部分常用的api已经被重写,所以仅仅是针对部分api才需要通过that去调用用。同时,apk安装以后仍然可以正常运行。

  4. 启动新activity的约束:启动外部activity不受限制,启动apk内部的activity有限制,首先由于apk中的activity没注册,所以不支持隐式调用,其次必须通过BaseActivity中定义的新方法startActivityByProxy和startActivityForResultByProxy,还有就是不支持LaunchMode。

  5. 目前暂不支持Service、BroadcastReceiver等需要注册才能使用的组件,但广播可以采用代码动态注册。