文档章节

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

Lody
 Lody
发布于 2015/03/29 13:50
字数 2274
阅读 4431
收藏 37

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 就很好的解决了这个问题,我们来看看是怎么解决的:

//开始伪装插件为实体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,我们不妨阅读它的代码,看透它的本质:

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就能够很好的工作了。

 

 

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

首先来看看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:

/*
 * {@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)方法:

/**
 * 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/


© 著作权归作者所有

共有 人打赏支持
Lody

Lody

粉丝 45
博文 3
码字总数 4482
作品 1
高级程序员
私信 提问
加载中

评论(8)

最新动弹
最新动弹
谢谢楼主无私奉献0
剑客蓝桥
剑客蓝桥
来顶一下。。。
徐伟涛
徐伟涛
学习一下,感觉对源码理解的特别透啊79
ouhoo
ouhoo
支持
fiend
fiend
好叼,强烈支持,谢谢。
f
faithbro
53
Honghe
Honghe
强悍
Honghe
Honghe
期待Service的支持
Android Plugin 插件化技术-Small插件框架

版权声明:本文为博主原创文章,未经博主允许不得转载。 目录(?)[+] 本篇文章只是整理了一些流行的开源插件化技术,其中言论纯属开源作者,不代表本人观点。 一、Small 简介:做最轻巧的跨平...

guozhendan
2017/02/03
0
0
自己动手写Android插件化框架,让老板对你刮目相看

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由达文西发表于云+社区专栏 最近在工作中接触到了Android插件内的开发,发现自己这种技术还缺乏最基本的了解,以至于在一些基...

腾讯云加社区
10/15
0
0
Android 插件化的过去-现在-未来

本文原创,转载请以链接形式注明地址:http://kymjs.com/code/2016/05/04/01 第一篇文章,作为序文,并没有什么实质性内容,仅仅是一些八卦和历史,重效率的朋友可以选择直接跳过。 过去 三年...

kymjs张涛
2016/05/05
1K
0
Android 【插件化】"偷梁换柱"的高手-VirtualApk源码解析

关于VirtualApk VirtualApk github : https://github.com/didi/VirtualAPK VirtualAPK wiki : https://github.com/didi/VirtualAPK/wiki 工程介绍 工程结构 CoreLibrary是VirtualApk(以下简称......

qq_17250009
04/12
0
0
如何将「插件化」接入到项目之中?

本期移动开发精英社群讨论的主题是「插件化」,上网查了一下,发现一篇 CSDN 博主写的文章《Android 使用动态加载框架DL进行插件化开发》。此处引用原作者的话: 随着应用的不断迭代,应用的...

OneAPM蓝海讯通
2016/03/30
46
0

没有更多内容

加载失败,请刷新页面

加载更多

linux脚本中父shell与子shell 执行的几种方式

本文主要介绍以下几个命令的区别: shell subshell source $ (commond) `commond` Linux执行Scripts有两种方式,主要区别在于是否建立subshell 1. source filename or . filename 不创建sub...

问题终结者
13分钟前
1
0
安装jdk和Tomcat

12月12日任务 16.1 Tomcat介绍 16.2 安装jdk 16.3 安装Tomcat Tomcat介绍 Tomcat是apache软件基金会(Apache Software Foundation)的Jakarta项目中的一个核心项目,由apache、Sun和其他一些...

robertt15
14分钟前
3
0
Beetl 免费视频

来自 https://my.oschina.net/gking?q=Beetl ,Beetl终于有人录制视频了 项目git地址:https://gitee.com/gavink/beetl-blog 视频地址:下载下来会更清晰,视频比较长,可使用倍速看 百度网盘...

闲大赋
26分钟前
0
0
isEmpty和null的区别

isEmpty和null的区别: 1.一个是对象为空(IsNull),一个是值为空(IsEmpty) 2.IsNull指任务类型变量是否为空包括对象类型的变量。 IsNull函数: 功能:返回Boolean的值,指明表达是否不包...

DemonsI
52分钟前
3
0
Centos7 安装mysql与php

https://blog.csdn.net/qq_36431213/article/details/79576025 官网下载安装mysql-server 依次使用下面三个命令安装 wget http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.r......

Yao--靠自己
今天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部