Direct-Load-apk启动插件的原理

1.前言

 

  在这个移动应用蓬勃发展的时代,追求新颖成为了软件开发的首要纲领,所以应用会自然而然的爆棚(方法数超过了一个 Dex 最大方法数 65535 的上限 ),然后Android插件化也就理所当然的出现了。

  这并不是一篇对于插件化研究的早期文章,但是文章介绍的插件化方式的突破确是可以载入史册的:) 

2、概念

Android 插件化 —— 是指将一个程序划分为不同的部分,比QQ的皮肤样式就可以看成一个插件
Android 组件化 —— 这个概念实际跟上面相差不那么明显,组件和插件较大的区别就是:组件是指通用及复用性较高的构件,比如图片缓存就可以看成一个组件被多个 App 共用
Android 动态加载 —— 这个实际是更高层次的概念,也有叫法是热加载或 Android 动态部署,指容器(App)在运⾏状态下动态加载某个模块,从而新增功能或改变某⼀部分行为这也是本文所要实现的。

3.相关开源框架

  

(1)https://github.com/singwhatiwanna/dynamic-load-apk 

这个项目的原理是把一个从ClassLoader中加载的自定义Activity类当成一个Object创建,然后使用一个代理Activity在相应的生命周期调用相应的方法。

  这个项目里有几个问题没解决,一个是 FragmentActivity 或是 ActionBarActiviy 的代理方式不行,因为存在 ClassLoader 冲突问题,必须在插件和宿主中只留下一份Android.support的jar。第二个问题是必须使用that指针代替this,因为直接new的Object不具有Activity特性。

(2)https://github.com/mmin18/AndroidDynamicLoader

这个项目的插件化方式和上面有很大的不同,他不是用代理 Activity 的方式实现的,而是用 Fragment 以及 schema 的方式实现总体上讲开发有一定的复杂性

 

(3)https://github.com/houkx/android-pluginmgr 

  这个框架比上面两个都要牛,它不需要对插件有任何约束,可以直接启动一个apk,原理是使用DexMaker的动态热部署生成一个Activity,让这个Activity继承目标插件所在的Activity,这样类名就被固定下来,唯一的改变是继承的父类在改变。虽说使用这个框架加载插件没有约束,但是由于是基于热部署,框架的稳定性就大打折扣了, 其中的OOM问题特别突出,因此实际中能够满足加载体验的只有一些轻量级的小型APK。

 

 

 

 

 

4.更强大的解决方案

  经过数月对Android源码的研究,一款名为Direct-load-apk的插件加载框架终于诞生,这款框架结合了Dynamic-Load-apk 和 PluginMgr 的弱点,使用了新的思路,成功实现了启动普通的apk。

 

  <1>介绍

Direct-load-apk基于注入和伪装的代理机制,通过转接现有的Activity,来实现动态创建和加载插件中的资源和类,因此可以正常使用this指针,而不像Dynamic-Load-apk那样需要使用that指针来代替this。

(框架地址:

github:https://github.com/FinalLody/Direct-Load-apk,

oschina:http://git.oschina.net/lody/Direct-load-apk/)

 

<2>框架解读

有了上面的理论知识,我们来开始深入探讨如何才能真正做到不安装而直接启动apk

 

主要涉及以下的类:

*com.lody.plugin.LActivityProxy

*com.lody.plugin.LPluginDexLoader

*com.lody.plugin.LPluginInsrument

*com.lody.plugin.bean.LPlugin

 

*LActivityProxy是真正的Activity,在宿主的AndroidManifest.xml中需要声明,所有的插件事务都会转交给它,甚至囊括插件的资源,这一点有点像dynamic-load-apk 

*LPluginDexLoader负责提取和加载插件apk中的Dex文件,并加载到插件化框架中。

*LPluginInsrument 继承自android.app.Instrumentation ,它的作用极为突出,也是笔者当初克服的难题之一,可以说如果没有它,框架就不能实现插件间跳转。

*com.lody.plugin.bean.LPlugin 负责维护一个插件的信息,由com.lody.plugin.LPluginManager来管理。

 

以前DL的作者等人其实写过挺多文章的,有兴趣的朋友可以先阅读,对于下面的理解有很大的帮助:

http://blog.csdn.net/singwhatiwanna/article/details/40283117

http://blog.csdn.net/singwhatiwanna/article/details/22597587

http://blog.csdn.net/singwhatiwanna/article/details/39937639

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1207/2123.html

 

面我们来跳过基础讲讲难点:

 

  读者应该知道,ClassLoader加载的类只能算是一个普通的对象,不具备生命周期,因此如果自己new一个Activity,是没有任何实际意义的,那么为什么系统创建的Activity具有生命周期呢? 原因很简单,因为系统会将创建的Activity保存下来,进行管理(主要涉及ActivityThread,ActivityManagerService,ActivityManager,ActivityStack

 

  现在没有生命周期的原因找到了,我们来对症下药,何不使用系统创建的Activity来间接管理我们自己加载的Activity呢?

  如dynamic-load-apk框架所描述的,由于自己创建的Activity并不是真正意义上的Activity,因此this不指向当前dynamic-load-apk的解决办法是让插件继承自定义的Activity,使用that指向代理的Activity,代替this指针,这就是dynamic-load-apk失败的地方。

 那么这个问题有解决办法吗?答案是有。Direct-Load-apk 就很好的解决了这个问题,我们来看看是怎么解决的:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//开始伪装插件为实体Activity
         proxyRef = Reflect.on(proxy);
         pluginRef = Reflect.on(plugin);
  
             pluginRef.set( "mBase" , proxy);
             pluginRef.set( "mDecor" , proxyRef.get( "mDecor" ));
             pluginRef.set( "mTitleColor" , proxyRef.get( "mTitleColor" ));
             pluginRef.set( "mWindowManager" , proxyRef.get( "mWindowManager" ));
             pluginRef.set( "mWindow" , proxy.getWindow());
             pluginRef.set( "mManagedDialogs" , proxyRef.get( "mManagedDialogs" ));
             pluginRef.set( "mCurrentConfig" , proxyRef.get( "mCurrentConfig" ));
             pluginRef.set( "mSearchManager" , proxyRef.get( "mSearchManager" ));
             pluginRef.set( "mMenuInflater" , proxyRef.get( "mMenuInflater" ));
             pluginRef.set( "mConfigChangeFlags" , proxyRef.get( "mConfigChangeFlags" ));
             pluginRef.set( "mIntent" , proxyRef.get( "mIntent" ));
             pluginRef.set( "mToken" , proxyRef.get( "mToken" ));
             Instrumentation instrumentation = proxyRef.get( "mInstrumentation" );
  
             pluginRef.set( "mInstrumentation" new  LPluginInsrument(instrumentation));
             pluginRef.set( "mMainThread" , proxyRef.get( "mMainThread" ));
             pluginRef.set( "mEmbeddedID" , proxyRef.get( "mEmbeddedID" ));
             pluginRef.set( "mApplication" ,app ==  null  ? proxy.getApplication() : app);
             pluginRef.set( "mComponent" , proxyRef.get( "mComponent" ));
             pluginRef.set( "mActivityInfo" , proxyRef.get( "mActivityInfo" ));
             pluginRef.set( "mAllLoaderManagers" , proxyRef.get( "mAllLoaderManagers" ));
             pluginRef.set( "mLoaderManager" , proxyRef.get( "mLoaderManager" ));
             if  (Build.VERSION.SDK_INT >=  13 ) {
                 //在android 3.2 以后,Android引入了Fragment.
                 FragmentManager mFragments = proxy.getFragmentManager();
                 pluginRef.set( "mFragments" , mFragments);
                 pluginRef.set( "mContainer" , proxyRef.get( "mContainer" ));
             }
             if  (Build.VERSION.SDK_INT >=  12 ) {
                 //在android 3.0 以后,Android引入了ActionBar.
                 pluginRef.set( "mActionBar" , proxyRef.get( "mActionBar" ));
             }
  
             pluginRef.set( "mUiThread" , proxyRef.get( "mUiThread" ));
             pluginRef.set( "mHandler" , proxyRef.get( "mHandler" ));
             pluginRef.set( "mInstanceTracker" , proxyRef.get( "mInstanceTracker" ));
             pluginRef.set( "mTitle" , proxyRef.get( "mTitle" ));
             pluginRef.set( "mResultData" , proxyRef.get( "mResultData" ));
             pluginRef.set( "mDefaultKeySsb" , proxyRef.get( "mDefaultKeySsb" ));
        pluginRef.call( "attachBaseContext" ,proxy);
             plugin.getWindow().setCallback(plugin);

读者应该看出来了,我们自己创建的Activity之所以不具备Activity是因为它内部的数据全部为Null,如果我们把它们全部替换成代理的Activity,那么问题是不是迎刃而解了呢?

注意上面最关键的一句话,pluginRef.call("attachBaseContext",proxy);

这一句的作用尤为关键,我们知道,Activity继承自ContextThemeWarpper,ContextThemeWarpper又继承自ContextWarpper,我们不妨阅读它的代码,看透它的本质:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public  class  ContextWrapper  extends  Context {
     Context mBase;
  
     public  ContextWrapper(Context base) {
         mBase = base;
     }
     
     /**
      * Set the base context for this ContextWrapper.  All calls will then be
      * delegated to the base context.  Throws
      * IllegalStateException if a base context has already been set.
     
      * @param base The new base context for this wrapper.
      */
     protected  void  attachBaseContext(Context base) {
         if  (mBase !=  null ) {
             throw  new  IllegalStateException( "Base context already set" );
         }
         mBase = base;
}
  
public  Context getBaseContext() {
         return  mBase;
     }
  
     @Override
     public  AssetManager getAssets() {
         return  mBase.getAssets();
     }
  
     @Override
     public  Resources getResources()
     {
         return  mBase.getResources();
     }
  
     @Override
     public  PackageManager getPackageManager() {
         return  mBase.getPackageManager();
     }
  
     @Override
     public  ContentResolver getContentResolver() {
         return  mBase.getContentResolver();
     }
  
     @Override
     public  Looper getMainLooper() {
         return  mBase.getMainLooper();
     }
     
     @Override
     public  Context getApplicationContext() {
         return  mBase.getApplicationContext();
     }
     
     @Override
     public  void  setTheme( int  resid) {
         mBase.setTheme(resid);
     }
...

 可以看到,ContextWarpper实际上就是一个包装代理类,它的全部工作都转交其中的mBase来实现,这么做是为了把ContextImp隐藏起来。

 

看到这里,读者应该明白了,pluginRef.call("attachBaseContext",proxy)的作用就是把mBase指向代理的Activity,那么this就能够很好的工作了。

 

 

第二个问题:插件的跳转:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
首先来看看Activity的startActivity方法:
     @Override
     public  void  startActivity(Intent intent) {
         startActivity(intent,  null );
     }
...
public  void  startActivity(Intent intent, Bundle options) {
         if  (options !=  null ) {
             startActivityForResult(intent, - 1 , options);
         else  {
       startActivityForResult(intent, - 1 );
         }
     }
...
  
    public  void  startActivityForResult(Intent intent,  int  requestCode) {
         startActivityForResult(intent, requestCode,  null );
}
...
//真正的跳转处理类:
  public  void  startActivityForResult(Intent intent,  int  requestCode, Bundle options) {
         if  (mParent ==  null ) {
             Instrumentation.ActivityResult ar =
                 mInstrumentation.execStartActivity(
                     this , mMainThread.getApplicationThread(), mToken,  this ,
                     intent, requestCode, options);
             if  (ar !=  null ) {
                 mMainThread.sendActivityResult(
                     mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                     ar.getResultData());
             }
             if  (requestCode >=  0 ) {
                ...
             }
  
             final  View decor = mWindow !=  null  ? mWindow.peekDecorView() :  null ;
             if  (decor !=  null ) {
                 decor.cancelPendingInputEvents();
             }
                     else  {
             if  (options !=  null ) {
                 mParent.startActivityFromChild( this , intent, requestCode, options);
             else  {
                 
                 mParent.startActivityFromChild( this , intent, requestCode);
             }
         }
     }

可以看到真正处理跳转的是mInstrumentation.execStartActivity(this,mMainThread.getApplicationThread(), mToken, this,intent, requestCode, options);

那么问题就来了,我们接触不到插件,因此无法复写startActivity转移跳转目标,最后想到了注入Instrumentation,来看看execStartActivity:

?
1
2
3
4
5
6
7
8
/*
  * {@hide}
  */
     public  ActivityResult execStartActivity(
             Context who, IBinder contextThread, IBinder token, Activity target,
             Intent intent,  int  requestCode, Bundle options) {
...
}

呵呵,看到了吧,这个方法被隐藏了,不能复写。

那么怎么办呢?我们来试试自定义Instrumentation,强制写一个execStartActivity(

            Context who, IBinder contextThread, IBinder token, Activity target,

            Intent intent, int requestCode, Bundle options)方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
  * Created by lody  on 2015/3/27.
  *
  * @author Lody
  *
  * 负责转移插件的跳转目标<br>
  * @see android.app.Activity#startActivity(android.content.Intent)
  */
  
public  class  LPluginInsrument  extends  Instrumentation {
     Instrumentation pluginIn;
     Reflect instrumentRef;
     public  LPluginInsrument(Instrumentation pluginIn){
         this .pluginIn = pluginIn;
         instrumentRef = Reflect.on(pluginIn);
     }
  
     /**@Override*/
     public  ActivityResult execStartActivity(
             Context who, IBinder contextThread, IBinder token, Activity target,
             Intent intent,  int  requestCode, Bundle options) {
  
         Intent gotoPluginOrHost =  new  Intent();
         ComponentName componentName = intent.getComponent();
         if (componentName ==  null ){
             return  instrumentRef.call( "execStartActivity" ,who,contextThread,token,target,intent,requestCode,options).get();
         }
         String className = componentName.getClassName();
         gotoPluginOrHost.setClass(who, LActivityProxy. class );
         gotoPluginOrHost.putExtra(LPluginConfig.KEY_PLUGIN_DEX_PATH, LPluginManager.finalApkPath);
         gotoPluginOrHost.putExtra(LPluginConfig.KEY_PLUGIN_ACT_NAME,className);
  
         gotoPluginOrHost.setAction(intent.getAction());
         gotoPluginOrHost.setData(intent.getData());
         gotoPluginOrHost.setType(intent.getType());
         if (Build.VERSION.SDK_INT >=  16 )
             gotoPluginOrHost.setClipData(intent.getClipData());
         gotoPluginOrHost.setFlags(intent.getFlags());
  
         return  instrumentRef.call( "execStartActivity" ,who,contextThread,token,target,gotoPluginOrHost,requestCode,options).get();
  
     }

经过实践,这种复写方法是可行的。

 

到此,一切问题基本解决,开始体验Direct-Load-Apk 带给你的使用this的快感吧!

 OSChina : http://git.oschina.net/lody/Direct-load-apk

 Github : https://github.com/FinalLody/Direct-Load-apk/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值