组件化,插件化,热更新

组件化

拆成多个 module 开发就是组件化

插件化

App 的部分功能模块在打包时并不以传统方式打包进 apk 文件中,而是以另⼀种形式⼆次封装进 apk内部,或者放在网络上适时下载,在需要的时候动态对这些功能模块进行加载,称之为插件化。这些单独⼆次封装的功能模块 apk ,就称作「插件」,初始安装的 apk 称作「宿主」。插件化是组件化的更进⼀步推进。

插件化步骤:

主Apk分析:

  1. 主App打包完成后,会形成dex,image,xml资源
  2. Dex靠PathClassLoader加载
  3. 图片以及xml资源靠Resource加载

代码实现:

  1. 创建DexClassLoader加载插件代码
  2. 创建Resource加载资源文件
  3. 管理插件Activity生命周期
插件化基础:反射
  • 使用反射的目的:
    Java 既然提供了可见性关键字 public private 等等,用来限制代码之间的可见性。可见性特性的支持不是为了代码不被坏⼈使用,而是是为了程序开发的简洁性。可见性的支持提供的是 Safety 的安全,而不是 Security 的安全。即,可见性的支持让程序更不容易写出 bug,而不是更不容易被人入侵。反射的支持可以让开发者在可见性的例外场景中,可以突破可见性限制来调⽤自己需要的 API。这是基于对开发者「在使⽤反射时已经足够了解和谨慎」的假设的。所以,可见性的支持不是为了防御外来者⼊侵,因此反射功能的⽀持并没有什么不合理。

总结一句话:反射可以让程序员抛开代码的限制,任意使用各种方法。

        try {
            Class clz = Class.forName("com.king.reflect.Apple");
            Constructor constructor = clz.getDeclaredConstructors()[0];
            constructor.setAccessible(true);
            Object object = constructor.newInstance();
            Method method = clz.getDeclaredMethod("setPrice", int.class);
            method.setAccessible(true);
            method.invoke(object, 4);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
插件化原理:动态加载

通过自定义 ClassLoader 来加载新的 dex 文件,从而让程序员原本没有的类可以被使用,这就是插件化的原理。

  • 关于 DEX:
    class: java编译后的文件,每个类对应⼀个class文件
    dex: Dalvik EXecutable 把class打包在⼀起,⼀个dex可以包含多个class文件
    odex: Optimized DEX针对系统的优化,例如某个方法的调用指令,会把虚拟的调用转换为使用具体的 index,这样在执行的时候就不用再查找了
    oat: Optimized Android file Type。使用AOT策略对dex预先编译(解释)成本地指令,这样再运行阶段就不需再经历⼀次解释过程,程序的运行可以更快
    AOT: Ahead-Of-Time compilation预先编译
        File dexfile = new File(getCacheDir() + "/plugin.dex");
        if (!dexfile.exists()) {
            try {
                InputStream is = getAssets().open("apk/dex/plugin.dex");
                int size = is.available();
                byte[] buffer = new byte[size];
                is.read(buffer);
                is.close();

                FileOutputStream fos = new FileOutputStream(dexfile);
                fos.write(buffer);
                fos.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        try {
            DexClassLoader classLoader = new DexClassLoader(dexfile.getPath(), getCacheDir().getPath(), null, null);
            Class pluginUtilsClass = classLoader.loadClass("com.king.reflect.Apple");
            Constructor utilsConstructor = pluginUtilsClass.getDeclaredConstructors()[0];
            Object utils = utilsConstructor.newInstance();
            Method shoutMethod = pluginUtilsClass.getDeclaredMethod("sell");
            shoutMethod.invoke(utils);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

DexClassLoader构造方法参数说明:
dexPath:apk/dex/jar文件路径
optimizedDirectory:文件解压路径(这个路径下保存的是.dex文件不是.class)
libraryPath:加载时用到的so库(可能理解有问题)
parent:父加载器(这个比较重要与Android加载class的机制有关)

    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
  • 需要注册的组件(例如 Activity)如何打开
    解决方式⼀:代理 Activity
    解决方式⼆:欺骗系统
    解决方式三:重写 gradle 打包过程,合并 AndroiManifest.xml

我也只是稍微了解方式一的方法。首先在宿主apk中留一个注册的ProxyActivity,生命周期的方法由RealActivity来实现,RealActivity通过插件化的反射来实现。

public class ProxyActivity extends Activity {
    Object realActivity;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        realActivity.onCreate(savedInstanceState);

    }
   
.....
}
  • 资源文件如何加载
    解决方式:自定义 AssetManager 和 Resources 对象,重写getResources() 和 getAssets() 方法。
public class ProxyActivity extends Activity {

    @Override
    public AssetManager getAssets() {
        try {
            Class assetManagerClass = AssetManager.class;
            AssetManager assetManager = (AssetManager) assetManagerClass.newInstance();
            Method addAssetPath = assetManagerClass.getDeclaredMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, "apkPath");
            return assetManager;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return super.getAssets();
    }

    @Override
    public Resources getResources() {
        return new Resources(getAssets(), getResources().getDisplayMetrics(), getResources().getConfiguration());
    }
}
  • 插件化的作用
    解决 dex 65535 问题。现在由 multidex 工具来专门解决。
    减小安装包大小。
    动态部署。

热更新

不安装新版本的软件,直接从网络下载新功能模块来对软件进行局部更新

  • 热更新和插件化的区别
  1. 插件化的内容在原 App 中没有,而热更新是原 App 中的内容做了改动
  2. 插件化在代码中有固定的入口,而热更新则可能改变任何⼀个位置的代码
  • 热更新的原理
    ClassLoader 的 dex 文件替换
    直接修改字节码
  • loadClass() 的类加载过程(双亲委托):是⼀个带缓存的、从上到下的加载过程(如下图所示):
    灰色部分为缓存:
    双亲委托.png

对于具体的⼀个 ClassLoader:
先从自己的缓存中取
自己没有缓存,就找父ClassLoader 要(parent.loadClass())
父ClassLoader也没有,就自己加载(findClass())BaseDexClassLoader 或者它的⼦类(DexClassLoader、 PathClassLoader 等)的findClass():通过 pathList.findClass(),它的 pathList.loadClass() 通过 DexPathList 的 dexElements 的 findClass()。

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    //            
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
 
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

所以热更新的关键在于,把补丁 dex 文件加载放进⼀个 Element,并且插入到 dexElements 这个数组的前面(插入到后面的话会被忽略掉)

  • 热更新代码
    因为无法在更新之前就指定要更新谁;所以不能定义新的 ClassLoader,而只能选择对ClassLoader 进行修改,让它能够加载补丁里面的类。因为补丁的类在原先的 App 中已经存在,所以应该把补丁的 Element 对象插入到 dexElements 的前面才行,插入到后面会被忽略掉。

具体的代码:

  1. 自己用补丁创建⼀个 PathClassLoader
  2. 把补丁 PathClassLoader 里面的 elements 替换到就的里面去, 尽早加载热更新(通用手段是把加载过程放在 Application.attachBaseContext())
            try {
                ClassLoader classLoader = getClassLoader();
                Class loaderClass = BaseDexClassLoader.class;
                Field pathListField = loaderClass.getDeclaredField("pathList");
                pathListField.setAccessible(true);
                Object pathListObject = pathListField.get(classLoader);
                Class pathListClass = pathListObject.getClass();
                Field dexElementsField = pathListClass.getDeclaredField("dexElements");
                dexElementsField.setAccessible(true);
                Object dexElementsObject = dexElementsField.get(pathListObject);

                // classLoader.pathList.dexElements = 热更新内容elements ;
                PathClassLoader newClassLoader = new PathClassLoader(apk.getPath(), null);
                Object newPathListObject = pathListField.get(newClassLoader);
                Object newDexElementsObject = dexElementsField.get(newPathListObject);

                //将newElement添加到Element[]的前面
                int oldLength = Array.getLength(dexElementsObject);
                int newLength = Array.getLength(newDexElementsObject);
                Object concatDexElementsObject = Array.newInstance(dexElementsObject.getClass().getComponentType(), oldLength + newLength);
                for (int i = 0; i < newLength; i++) {
                    Array.set(concatDexElementsObject, i, Array.get(newDexElementsObject, i));
                }
                for (int i = 0; i < oldLength; i++) {
                    Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObject, i));
                }

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

推荐阅读更多精彩内容