Android动态加载

Android插件化

如觉得文章排版格式不方便阅读,请移位

来源及使用

随着业务功能慢慢的增加,apk的体积会越来越大,为了减小包的体积,可以利用Android的动态加载技术实现Android插件化,使用Android插件化开发,安装apk包体积减小,用户可以根据自己的需要安装下载插件,不需要的时候卸载;Android的动态加载技术不仅仅可以用到插件化上,还可以用于apk的热更,安全加壳等

Android动态加载技术

所谓动态加载技术,就是在apk应用程序在运行时,动态加载apk、dex等包,加载到内存当中运行,动态加载技术需要了解Android的ClassLoader、ActivityThread、java反射以及Android的资源加载知识;通常来说动态加载技术大致分为以下几个步骤(宿主工程;被加载进去的包,以下简称插件dex)

  • 提供插件dex,不仅限于dex,可以是apk或者zip等
  • 使用ClassLoader将dex加载到内存
  • 利用ClassLoader的loadClass加载你需要的类
  • 使用加载的类执行它自己的逻辑功能

注意Attention:上述第三步加载你需要的类后,如果这个类时Android的四大组件,如Activity类似有生命周期方法,则需要你在做一些额外工作;生命周期是由Android系统来调用的,所以这里就需要使用反射来获取Android系统的API,让它管理我们的加载类,这样我们的类就有了灵魂,有正常的生命周期方法了;

这里写图片描述

原理容易,但实现起来就很复杂,要想系统调用它的生命周期,首先你就得分析类似于Activity、Service这样的组件生命周期在framework层是如何运行的,后面会做简单的介绍讲解

ClassLoader

Android系统上,加载class的有两种:DexClassLoader和PathClassLoader

  • DexClassLoader可以自定义load路径,并且可以把apk文件解析后输出dex文件,所以DexClassLoader需要提供输入apk和输出路径
  • PathClassLoader不能自定义load路径,它固定解析/data/app路径下的内容,并且也不能主动释放出apk里面的内容

除此之外,还需要了解classLoader的双亲委派机制,简而言之就,就是:每个ClassLoader都有一个父的ClassLoader,当加载某一个Class的时候会先去父ClassLoader查询,如果父类已经加载过这个Class时就直接获取就行,不再去加载;反之,则自己去加载;
还有一点,Android的classLoader从dex1中加载出了com.jack.A;与此同时,另一个ClassLoader又从dex2中加载出com.jack.A;这种情况是不允许的,相同的类不允许在不同的dex解析出来

普通java类的动态加载

  1. 宿主项目 --- MainProject
  2. 插件工程 --- PluginProject
  • 插件工程完成正常的业务逻辑
  • 宿主工程主要完成dex的加载,找到相应的class并执行即可

插件工程

这里写图片描述

指定插件标准接口,宿主项目能更好的使用

//插件接口
public interface DynamicImple {

    public void init();
    
    public void makeToast(Context context, String meg);
    
}

//业务逻辑
public class IPluginImple implements DynamicImple{

    private static final String TAG = "jackzhous";
    
    @Override
    public void init() {
        // TODO Auto-generated method stub
        Log.i(TAG, "plugin method init()");
    }

    @Override
    public void makeToast(Context context, String meg) {
        // TODO Auto-generated method stub
        Log.i(TAG, "plugin method makeToast()");
        Toast.makeText(context, "plugin: " + meg, Toast.LENGTH_SHORT).show();
    }
}

完成插件工程后,到处为apk,最后需要使用apktool解包把插件接口代码删掉,在合包成apk使用;删掉的目的是为了防止插件dex和宿主dex都有这个接口

宿主项目

    //使用DexClassLoader加载dex
    private DexClassLoader initClassLoader(){
        String apkPath = Environment.getExternalStorageDirectory() + File.separator + "PluginProject.apk";
        String dexOutPath = getCacheDir().getAbsolutePath();
        //参数分别为源apk路径 输出路径  libarary库路径和父类classLoader
        DexClassLoader dexL = new DexClassLoader(apkPath, dexOutPath, null, getClassLoader());
        
        loadResources(apkPath);
        return dexL;
    }
    

    //加载插件当中的代码
    @SuppressLint("NewApi") public void doInit(View v) {
        if(loader != null){
            try {
                Class pluginClass = loader.loadClass("com.jack.plugin.IPluginImple");
                
                Object obj = pluginClass.newInstance();
                //使用接口方式,效率快一些
                if(obj instanceof DynamicImple){
                    DynamicImple imple = (DynamicImple)obj;
                    imple.init();
                }
                
                
                //也可以使用放射方式,但是这种效率比较低,原因主要是在getMethod和getField里面
                //Method method = pluginClass.getMethod("init", null);
                //method.invoke(obj, null);
                
            } catch (ClassNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (InstantiationException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } /*catch (NoSuchMethodException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }  catch (InvocationTargetException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }*/
        }

    }

    //如果需要使用插件当中的资源需要在做以下工作
    /*============================使用插件的资源需要做如下处理==========================================*/
    /**
     * 1. 将插件资源add进来
     * 2. 使用该资源并获得Resource索引
     * 3. 重写父类方法使用Resource即可
     */
    
    private AssetManager mAssetManager;
    private Resources    mResources;
    private Theme        mTheme;
    
    protected void loadResources(String dexPath) {    
        try {    
            AssetManager assetManager = AssetManager.class.newInstance();    
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);    
            addAssetPath.invoke(assetManager, dexPath);    
            mAssetManager = assetManager;    
        } catch (Exception e) {    
            e.printStackTrace();    
        }    
        Resources superRes = super.getResources();    
        superRes.getDisplayMetrics();    
        superRes.getConfiguration();    
        mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());    
        mTheme = mResources.newTheme();    
        mTheme.setTo(super.getTheme());  
    }  
    
    
    @Override    
    public AssetManager getAssets() {    
        return mAssetManager == null ? super.getAssets() : mAssetManager;    
    }    
      
    @Override    
    public Resources getResources() {    
        return mResources == null ? super.getResources() : mResources;    
    }    
      
    @Override    
    public Theme getTheme() {    
        return mTheme == null ? super.getTheme() : mTheme;    
    }  
    /*============================使用插件的资源需要做如下处理==========================================*/
    
    //使用插件资源
    
    @SuppressLint("NewApi") public void doPicture(View v) {
        if(loader != null){
            try {
                Class pluginUiutilClass = loader.loadClass("com.jack.plugin.UIUtil");
                
                Object obj0 = pluginUiutilClass.newInstance();
                
                Drawable message = ((UIUtilImp)obj0).getDrawableFromPlugin(this, 0);
                
                mImageView.setImageDrawable(message);
                
                
            } catch (ClassNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (InstantiationException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

把插件工程按照上面的操作后拷贝到手机目录下,在运行宿主项目就可以执行插件的东西了

项目下载demo

有生命周期类的动态加载

以Activity的动态加载为例,大的分类有两种:

  • 反射方式替换宿主项目的ClassLoader或者反射合并宿主和插件的pathList,pathList里面存放了dex的索引
  • 静态代理方式,宿主ProxyActivity持有插件Activity的引用,ProxyActivity生命周期变化执行插件Activity对应变化

反射方式

替换宿主ClassLoader

为什么替换ClassLoader后,就能够执行插件类的生命周期方法了?
众所周知,Activity由ClassLoader加载进来后就有ActivityThread来管理执行其生命周期,但是内部如何管理很复杂,不在此讲解,了解详情点击这里,我们只需要知道加载Activity的ClassLoader在哪里即可,查看源码发现在ActivityThread的成员变量ArrayMap mPackages,在LoadApk里面的ClassLoader里面,看到这逻辑,我们就可以用反射获取了


文档后续完成中.............................


生命周期类的总结

  • 反射的方式需要在宿主项目的Androidmanifest.xml里面配置Activity组件信息,插件有多少个就需要配置多少个,一经ClassLoader就完成任务了
  • 静态代理不要在Androidmanifest.xml配置Activity信息,只需要配置代理Activity一个即可;需要管理手动的去管理插件中Activity的生命周期方法,难度复杂

Java反射耗时在什么地方?

getMethod和getDeclaredField方法会比invoke和set方法耗时;

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

推荐阅读更多精彩内容