DynamicLoadApk插件框架的动态加载代码分析

导读:学习了任玉刚的动态加载框架技术,进行插件化的开发,实现host项目+plugin项目的独立开发,和动态加载,这里通过demo详细梳理一下流程

DynamicLoaderApkDemo 项目

github地址: https://github.com/George-Soros/DynamicLoaderApkDemo

一:介绍代码结构

DynamicLoaderApkdemo中包括了HostApp,PluginApp ,先介绍一下他们:

1:HostApp

com.ryg.dynamicload包中的文件是动态插件框架提供的,其中包括了DL相关的代理类。


image.png

com.ryg.sample包中的MainActivity是一个启动页面如下:

image.png

2:PluginApp

插件需要加入动态加载库

其中libs中加入动态加载库dl-apk.jar包,它只参与编译,不打包到apk中,这个包就是host中的com.ryg.dynamic下面的代码打包而来!

dependencies {
//    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.google.code.gson:gson:2.8.0'
    provided files('libs/dl-lib.jar')
}
image.png
页面继承动态库页面

com.practise.pluginapp.MainActivity,启动页面,需要继承了动态加载库提供的DLBasePluginFragmentActivity类(当然可以是DLBasePluginActivity,或者对于service等),方便实现host加载时代理该类Activity的责任和生命周期。

二:动态加载分析流程

1:加载插件PluginApp.apk到内存中的过程

HostApp->MainActivity->loadPlugin, 加载手机sd卡中的PluginApp.apk, 其中PluginApp.apk是PluginApp项目打包而来

private void loadPlugin() {

        //apk名称
        String apkName = "PluginApp.apk";

        //插件apk的位置
        String apkAllPath = Environment.getExternalStorageDirectory().getAbsolutePath()
                + "/" + apkName;

        //加载插件apk到内存中
        DLPluginPackage dlPluginPackage = DLPluginManager.getInstance(this).loadApk(apkAllPath);

        if(dlPluginPackage.classLoader != null){
            Toast.makeText(this, "加载插件apk到内存中成功!",Toast.LENGTH_SHORT).show();
        }
    }

DLPluginManager.getInstance(this).loadApk(apkAllPath);

上面的这个逻辑是主要的加载apk到内存的过程

 public DLPluginPackage loadApk(String dexPath) {
        // when loadApk is called by host apk, we assume that plugin is invoked
        // by host.
        return loadApk(dexPath, true);
    }

    /**
     * @param dexPath
     *            plugin path
     * @param hasSoLib
     *            whether exist so lib in plugin
     * @return
     */
    public DLPluginPackage loadApk(final String dexPath, boolean hasSoLib) {
        mFrom = DLConstants.FROM_EXTERNAL;

        PackageInfo packageInfo = mContext.getPackageManager().getPackageArchiveInfo(dexPath,
                PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
        if (packageInfo == null) {
            return null;
        }
        //加载插件apk,获取到apk的资源
        DLPluginPackage pluginPackage = preparePluginEnv(packageInfo, dexPath);
        if (hasSoLib) {
            copySoLib(dexPath);
        }

        return pluginPackage;
    }

loadApk方法中,主要看

DLPluginPackage pluginPackage = preparePluginEnv(packageInfo, dexPath);

private DLPluginPackage preparePluginEnv(PackageInfo packageInfo, String dexPath) {

        DLPluginPackage pluginPackage = mPackagesHolder.get(packageInfo.packageName);
        if (pluginPackage != null) {
            return pluginPackage;
        }
        //下面的3个主要方法,完成了类加载,和资源的获取!!
        DexClassLoader dexClassLoader = createDexClassLoader(dexPath);
        AssetManager assetManager = createAssetManager(dexPath);
        Resources resources = createResources(assetManager);
        // create pluginPackage
        pluginPackage = new DLPluginPackage(dexClassLoader, resources, packageInfo);
        mPackagesHolder.put(packageInfo.packageName, pluginPackage);
        return pluginPackage;
    }

其中准备好了DexClassLoader的类加载,并加载完成插件apk,还有插件apk的资源获取到了!

mPackagesHolders是一个Map文件,存储了对于插件apk的信息(类加载器,资源等), 方便了下一步启动插件apk中的页面需要的资源信息等

private DexClassLoader createDexClassLoader(String dexPath) {
        File dexOutputDir = mContext.getDir("dex", Context.MODE_PRIVATE);
        dexOutputPath = dexOutputDir.getAbsolutePath();
        DexClassLoader loader = new DexClassLoader(dexPath, dexOutputPath, mNativeLibDir, mContext.getClassLoader());
        return loader;
    }

android提供的类加载器DexClassLoader,传入了apk的路径,指定class.dex的路径,本地方法库,父类加载器等,完成了apk中class.dex文件的加载到内存,到这里插件Apk的动态加载就完成了!

2:启动插件页面,实现插件的功能

tv_start_plugin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v){
                Toast.makeText(MainActivity.this, "开始启动插件页面....",Toast.LENGTH_SHORT).show();
                startPluginActivity(MainActivity.this, new DLIntent("com.practise.pluginapp"
                        , "com.practise.pluginapp.TestActivity"));
            }
        });
//传人的DLIntent中包括了需要启动插件的包名和页面
private void startPluginActivity(Context context, DLIntent intent) {
        DLPluginManager dlPluginManager = DLPluginManager.getInstance(context);
        if (!dlPluginManager.isHostPackageSet()){
            //当前host的包名
            dlPluginManager.setHostPackageName("com.ryg");
        }
        dlPluginManager.startPluginActivity(this, intent);
    }
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public int startPluginActivityForResult(Context context, DLIntent dlIntent, int requestCode) {
        //如果已经执行了动态加载apk,mfrom为1跳过if
        if (mFrom == DLConstants.FROM_INTERNAL) {
            if (isHostPackageSet() && dlIntent.getPluginPackage() != null
                    && !hostPackageName.equals(dlIntent.getPluginPackage())){
                return DLPluginManager.START_RESULT_NO_PKG;
            }
            dlIntent.setClassName(context, dlIntent.getPluginClass());
            performStartActivityForResult(context, dlIntent, requestCode);
            return DLPluginManager.START_RESULT_SUCCESS;
        }
        //得到插件的包名
        String packageName = dlIntent.getPluginPackage();
        if (TextUtils.isEmpty(packageName)) {
            throw new NullPointerException("disallow null packageName.");
        }
        //这里看到mPackagesHolder这个map获取之前保存的信息
        DLPluginPackage pluginPackage = mPackagesHolder.get(packageName);
        if (pluginPackage == null) {
            return START_RESULT_NO_PKG;
        }

        final String className = getPluginActivityFullPath(dlIntent, pluginPackage);
        //关键点来了:通过DexClassLoader类加载器将插件页面(已经在内存中的)生成对于的class对象
        Class<?> clazz = loadPluginClass(pluginPackage.classLoader, className);
        if (clazz == null) {
            return START_RESULT_NO_CLASS;
        }

        // get the proxy activity class, the proxy activity will launch the
        // plugin activity.
       //根据生成的对象,判断它的代理页面,就是我们插件页面继承的DLBasePluginFragmentActivity,这样就实现了启动代理页面,然后再调用生成对象中的方法,代理了插件页面的生命周期
        Class<? extends Activity> activityClass = getProxyActivityClass(clazz);
        if (activityClass == null) {
            return START_RESULT_TYPE_ERROR;
        }

        // put extra data
        dlIntent.putExtra(DLConstants.EXTRA_CLASS, className);
        dlIntent.putExtra(DLConstants.EXTRA_PACKAGE, packageName);
        dlIntent.setClass(mContext, activityClass);
        performStartActivityForResult(context, dlIntent, requestCode);
        return START_RESULT_SUCCESS;
    }

下面的方法是DexClassLoader来加载生成对象,不太明白类加载的可以看看:虚拟机的类加载器理解和实践

private Class<?> loadPluginClass(ClassLoader classLoader, String className) {
        Class<?> clazz = null;
        try {
            clazz = Class.forName(className, true, classLoader);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        return clazz;
    }

判断class对象是继承的哪个页面

private Class<? extends Activity> getProxyActivityClass(Class<?> clazz) {
        Class<? extends Activity> activityClass = null;
        if (DLBasePluginActivity.class.isAssignableFrom(clazz)) {
            activityClass = DLProxyActivity.class;
        } else if (DLBasePluginFragmentActivity.class.isAssignableFrom(clazz)) {
            activityClass = DLProxyFragmentActivity.class;
        }

        return activityClass;
    }

这样根据生成的对象,判断它的代理页面,就是我们插件页面继承的DLBasePluginFragmentActivity,这样就实现了启动代理页面DLProxyFragmentActivity。

来看看DLProxyFragmentActivity,
public class DLProxyFragmentActivity extends FragmentActivity implements DLAttachable {

    protected DLPlugin mRemoteActivity;
    private DLProxyImpl impl = new DLProxyImpl(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        /**
         * DLProxyImpl中启动的插件的oncreate (mPluginActivity.onCreate(bundle);)
         *
         * 相当于在当前调用  mRemoteActivity.onCreate(savedInstanceState);
         */
        impl.onCreate(getIntent());
    }

    /**
     * 通过这个DlProxyFragmentActivity 代理
     * @param remoteActivity
     * @param pluginManager DLPluginManager instance, manager the plugins
     */
    @Override
    public void attach(DLPlugin remoteActivity, DLPluginManager pluginManager) {
        mRemoteActivity = remoteActivity;
    }

    @Override
    public AssetManager getAssets() {
        return impl.getAssets() == null ? super.getAssets() : impl.getAssets();
    }

    @Override
    public Resources getResources() {
        return impl.getResources() == null ? super.getResources() : impl.getResources();
    }

    @Override
    public Theme getTheme() {
        return impl.getTheme() == null ? super.getTheme() : impl.getTheme();
    }

    @Override
    public ClassLoader getClassLoader() {
        return impl.getClassLoader();
    }

........
其中的一个关键, DLProxyImpl实现了host代理和插件页面的类加载等,通过attach接口实现了关联!

private DLProxyImpl impl = new DLProxyImpl(this);

public void onCreate(Intent intent) {

        // set the extra's class loader
        intent.setExtrasClassLoader(DLConfigs.sPluginClassloader);

        mPackageName = intent.getStringExtra(DLConstants.EXTRA_PACKAGE);
        mClass = intent.getStringExtra(DLConstants.EXTRA_CLASS);
        Log.d(TAG, "mClass=" + mClass + " mPackageName=" + mPackageName);

        mPluginManager = DLPluginManager.getInstance(mProxyActivity);
        mPluginPackage = mPluginManager.getPackage(mPackageName);
        if (mPluginPackage == null){
            if (mProxyActivity != null) {
                mProxyActivity.finish();
            }
            return;
        }
        mAssetManager = mPluginPackage.assetManager;
        mResources = mPluginPackage.resources;

        initializeActivityInfo();
        handleActivityInfo();
        launchTargetActivity();
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    protected void launchTargetActivity() {
        try {
            Class<?> localClass = getClassLoader().loadClass(mClass);
            Constructor<?> localConstructor = localClass.getConstructor(new Class[] {});
            Object instance = localConstructor.newInstance(new Object[] {});
            mPluginActivity = (DLPlugin) instance;
            ((DLAttachable) mProxyActivity).attach(mPluginActivity, mPluginManager);
            Log.d(TAG, "instance = " + instance);
            // attach the proxy activity and plugin package to the mPluginActivity
            mPluginActivity.attach(mProxyActivity, mPluginPackage);

            Bundle bundle = new Bundle();
            bundle.putInt(DLConstants.FROM, DLConstants.FROM_EXTERNAL);
            mPluginActivity.onCreate(bundle);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
最后attach方法中传入了插件页面,当启动host的代理DLProxyFragmentActivity的生命周期,就调用插件mRemoteActivity的生命周期,实现了代理,插件页面的生命周期。 代理模式看结构型设计模式之代理, 享元, 门面, 桥接

然后就启动了插件页面,如下:


image.png

需要看动态加载插件开发的参考:
https://github.com/singwhatiwanna/dynamic-load-apk 开源代码dlapk-lib

http://blog.csdn.net/singwhatiwanna/article/details/39937639 任玉刚的分析

http://blog.csdn.net/fanpeihua123/article/details/51364521 范陪华的分析 , 很棒, 重点看 ! !

http://blog.csdn.net/u012124438/article/details/53241755 在范陪华文章,加入了更多的代码解读

http://blog.csdn.net/u012124438/article/details/53242838 在范陪华文章,加入了更多的代码解读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345