• Android 插件化


    1.插件化

    插件可以理解为免安装的Apk,而支持插件的app称为宿主。

    一个Apk会包含如下几个部分:

    classes.dex:Java代码字节码

    res:资源文件

    lib:so 文件

    assets:静态资产文件

    AndroidManifest.xml:清单文件

    Android系统在打开应用之后,只是开辟进程,然后使用ClassLoader加载classes.dex至进程中,执行对应的组件而已。

     

    插件化让Apk中的代码(主要是指Android组件)能够免安装运行,这样带来很多好处:

    ①减少安装Apk的体积、按需下载模块

    ②动态更新插件

    ③宿主和插件分开编译,提升开发效率

    ④解决方法数超过65535的问题

     

    插件化和热修复不是同一个概念,虽然在技术实现的角度来说,它们都是从系统加载器的角度出发,通过“欺骗”Android 系统的方式来让宿主正常的加载和运行插件/补丁中的内容,但是它们的出发点是不同的。插件化是把需要实现的模块或功能独立出来,减少宿主的规模,当需要使用到相应的功能时再去加载相应的模块。热修复则是从修复bug的角度出发,强调的是在不需要二次安装应用的前提下修复已知的bug。

     

    插件化与组件化的区别:

    组件化是将一个App分成多个模块,每个模块都是一个组件(module),开发过程中可以让这些组件相互依赖或独立编译、调试部分组件,但是这些组件最终会合并成一个完整的Apk去发布到应用市场。组件化开发有一个弊端 ,就是多个模块必须是并发开发 , 模块之间相互依赖 , 如果修改了一个模块 ,那就必须重新打包 。

    插件化是将整个app拆分成若干模块,其中有1个宿主模块、若干插件模块 。宿主模块和插件模块可以分开进行编译,二者之间互不影响,各个模块可以并发进行开发。打包时,将宿主模块和插件模块分开进行打包,即宿主模块和插件模块都各自是一个单独apk安装文件 。只需发布宿主Apk到应用市场,插件Apk通过动态按需下发到宿主Apk,实现宿主模块动态更新插件。

     

    现在大型Android项目基本都是组件化+插件化开发。项目架构上都是组件化的框架,某些修改频繁的Module模块设置成插件模块,编译成独立的APK文件,以插件的形式进行部署,供宿主模块调用。应用运行时,点击启动某个插件APK中的界面,首先下载对应的插件APK文件,将其放在内置存储区中,然后加载该APK文件,主要是类加载器加载dex文件中的Class字节码数据。

     

    插件化的思路是:在宿主模块应用中提前预留好插件组件对应的代理组件接口,将插件模块apk插入到程序中,就可以调用插件apk安装包中的功能。将插件模块编译打包成apk文件,该文件包含classes.dex文件,被打包在了apk压缩包中。因此只要拿到了apk文件,就可以将其解压,获取到其中的classes.dex文件。可以自己实现一个DexClassLoader加载该dex文件,进而调用其中封装的字节码类对象。


    2.插件化实现步骤

    ①加载并执行插件Apk中的代码

    要让插件Apk真正运行起来,首先要找到插件Apk的存放位置,然后解析加载Apk里面的代码。

    由于插件是未安装的apk,系统不会处理其中的类,所以需要使用ClassLoader加载Apk,然后反射里面的代码。

    在Android系统中,ClassLoader用来加载dex文件。dex文件是一种对class文件优化的产物,在Android中应用打包时会把所有class文件进行合并、优化(把不同的class文件重复的东西只保留一份),然后生成一个最终的class.dex文件。

    PathClassLoader:用来加载系统类和应用程序类,可以加载已经安装的apk目录下的dex文件。

    DexClassLoader:用来加载dex文件,可以从存储空间加载dex文件。

    在插件化中一般使用的是DexClassLoader。创建出DexClassLaoder实例以后,调用其loadClass( className) 方法就可以加载插件中的类了。然后通过反射执行类的方法:

    Class loadClass = pluginClassLoader.loadClass( activityName);

    loadClass.getMethod("test",null).invoke(loadClass);

    这个过程叫做ClassLoader注入。完成注入后,所有来自宿主的类使用宿主的ClassLoader进行加载,所有来自插件Apk的类使用插件ClassLoader 进行加载。由于ClassLoader的双亲委派机制,实际上系统类会不受ClassLoader的类隔离机制影响,这样宿主Apk就可以在宿主进程中使用来自于插件的组件类了。

    ②让系统能调用插件Apk中的组件

    Android系统中四大组件是需要在系统中注册的,并且由系统管理生命周期,具体是在Android系统的AMS和PMS中注册,而四大组件的解析和启动都需要依赖AMS和PMS。而插件是动态加载的,所以插件的四大组件不可能注册到宿主的Manifest文件中,因此不能和系统直接进行交互的。 而且仅仅构造出四大组件的实例是没用的,还需要管理组件的生命周期。

    插件化如何支持组件生命周期的管理,大致分为两种方式:运行时容器技术(ProxyActivity代理)和预埋StubActivity,hook系统启动Activity的过程。

    这里使用运行时容器技术,就是在宿主Apk中预埋一些空的Android组件。以Activity为例,可以在宿主中预置一个ContainerActivity extends Activity ,并且在AndroidManifest.xml中注册它。

    ContainerActivity作为插件Activity的容器,它从Intent接收插件的不同信息,如pluginName、pluginApkPath、pluginActivityName等。当ContainerActivity启动时,就加载插件的 ClassLoader、Resource,并反射pluginActivityName对应的Activity类。当完成加载后,ContainerActivity要做两件事:

    ①转发所有来自系统的生命周期回调至插件Activity

    ②接受Activity方法的系统调用,并转发回系统

    第一步可以通过复写ContainerActivity的生命周期方法来完成,而第二步需要定义一个 PluginActivity,然后在编写插件Apk中的Activity 组件时,需要继承自PluginActivity。

    public class ContainerActivity extends Activity {

        private PluginActivity pluginActivity;

        @Override

        protected void onCreate(Bundle savedInstanceState) {

            String pluginActivityName = getIntent().getString("pluginActivityName", "");

            pluginActivity = PluginLoader.loadActivity( pluginActivityName, this);

            if (pluginActivity == null) {

                super.onCreate(savedInstanceState);

                return;

            }

            pluginActivity.onCreate();

        }

        @Override

        protected void onResume() {

            if (pluginActivity == null) {

                super.onResume();

                return;

            }

            pluginActivity.onResume();

        }

        @Override

        protected void onPause() {

            if (pluginActivity == null) {

                super.onPause();

                return;

            }

            pluginActivity.onPause();

        }

        // ...

    }

    PluginActivity如下:

    public class PluginActivity {

        private ContainerActivity containerActivity;

        public PluginActivity(ContainerActivity containerActivity) {

            this.containerActivity = containerActivity;

        }

        @Override

        public T findViewById(int id){

            return containerActivity.findViewById(id);

        }

        // ...

    }

    // 插件Apk中真正写的组件

    public class TestActivity extends PluginActivity {

        // ......

    }

    大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统。

    该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,插件中的Activity必须继承PluginActivity,如果想把之前的模块改造成插件需要很多额外的工作。

    class TestActivity extends Activity {}

    改为

    class TestActivity extends PluginActivity {}

    为了让插件组件的编写与原来没有任何差别,可以使用字节码插桩。Android中Transform Api可以介入项目的构建过程,在字节码生成后、dex 文件生成前,对代码进行某些变换。也就是用户配置Gradle插件后,正常开发,依然编写:

    class TestActivity extends Activity {}

    然后完成编译后,最后的字节码中,显示的却是:

    class TestActivity extends PluginActivity {}

    ③正确识别插件Apk中的资源

    最后一步就是资源注入,Android应用的开发其实崇尚的是逻辑与资源分离的理念,所有资源(layout、values 等)都会被打包到Apk中,然后生成一个对应的R类,其中包含对所有资源的引用 id。

    资源的注入主要依靠两个接口:

    ①PackageManager#getPackageArchiveInfo:根据Apk路径解析一个未安装的Apk的PackageInfo

    ②PackageManager#getResourcesForApplication:根据ApplicationInfo创建一个Resources实例

    因此可以在上面ContainerActivity#onCreate中加载插件Apk的时候,用这两个方法创建出一份插件资源实例。具体来说就是先用 PackageManager#getPackageArchiveInfo拿到插件Apk的PackageInfo,有了PacakgeInfo之后就可以组装一份ApplicationInfo,然后通过 PackageManager#getResourcesForApplication来创建资源实例,大概代码像这样:

    PackageManager packageManager = getPackageManager();

    PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(

        pluginApkPath,

        PackageManager.GET_ACTIVITIES

        | PackageManager.GET_META_DATA

        | PackageManager.GET_SERVICES

        | PackageManager.GET_PROVIDERS

        | PackageManager.GET_SIGNATURES

    );

    packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;

    packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;

    Resources injectResources = null;

    try {

        injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);

    } catch (PackageManager.NameNotFoundExcep tion e) {

        // ...

    }

    拿到资源实例后,需要将宿主的资源和插件资源Merge一下,编写一个新的Resources类,用这样的方式完成自动代理:

    public class PluginResources extends Resources {

        private Resources hostResources;

        private Resources injectResources;

        public PluginResources(Resources hostResources, Resources injectResources) {

            super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());

            this.hostResources = hostResources;

            this.injectResources = injectResources;

        }

        @Override

        public String getString(int id, Object... formatArgs) throws NotFoundException {

            try {

                return injectResources.getString(id, formatArgs);

            } catch (NotFoundException e) {

                return hostResources.getString(id, formatArgs);

            }

        }

        // ...

    }

    然后在ContainerActivity完成插件组件加载后,创建一份Merge资源,再复写ContainerActivity# getResources,将获取到的资源替换掉:

    public class ContainerActivity extends Activity {

        private Resources pluginResources;

        @Override

        protected void onCreate(Bundle savedInstanceState) {

            // ...

            pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));

            // ...

        }

        @Override

        public Resources getResources() {

            if (pluginActivity == null) {

                return super.getResources();

            }

            return pluginResources;

        }

    }

    这样就完成了资源的注入。

     

     

  • 相关阅读:
    java计算机毕业设计ssm物流快递管理系统
    LQ0019 数的拆分【素数】
    <蓝桥杯软件赛>零基础备赛20周--第5周--杂题-2
    1851. 包含每个查询的最小区间 扫描线/优先队列
    dubbo(5):使用dubbo进行业务分离与dubbo-admin的使用
    XML语法、约束
    Springboot实现登录功能(token、redis、登录拦截器、全局异常处理)
    从PMP理论看华为销售项目运作与管理
    [hadoop全分布部署]虚拟机Hadoop集群交换 SSH 密钥与验证SSh无密码登录
    Java ZGC 算法调优
  • 原文地址:https://blog.csdn.net/zenmela2011/article/details/126304850