精华内容
下载资源
问答
  • 2021-05-29 21:27:36

    Android 插件化系列文章目录

    【Android 插件化】插件化简介 ( 组件化与插件化 )
    【Android 插件化】插件化原理 ( JVM 内存数据 | 类加载流程 )
    【Android 插件化】插件化原理 ( 类加载器 )
    【Android 插件化】“ 插桩式 “ 插件化框架 ( 原理与实现思路 )
    【Android 插件化】“ 插桩式 “ 插件化框架 ( 类加载器创建 | 资源加载 )
    【Android 插件化】“ 插桩式 “ 插件化框架 ( 注入上下文的使用 )
    【Android 插件化】“ 插桩式 “ 插件化框架 ( 获取插件入口 Activity 组件 | 加载插件 Resources 资源 )
    【Android 插件化】“ 插桩式 “ 插件化框架 ( 运行应用 | 代码整理 )






    一、组件化与插件化



    组件化 是将应用分成若干 Module 模块 , 每个模块称为一个组件 ;

    组件化 项目中 , 分为两种模式 , " 集成模式 " " 组件模式 " ;

    在开发过程中的 " 组件模式 " 下这些组件可以 独立运行 , 在 " 集成模式 " 下 , 这些组件 相互依赖拼装成一个 APK 安装包 ;


    组件化开发的弊端 :

    多个模块必须是并发开发 , 模块之间相互依赖 , 如果修改了一个模块 , 那就必须重新打包 ;

    插件化开发 , 解决了上述问题 ;


    插件化 将应用拆分成若干模块 , 其中有 1 1 1" 宿主 " 模块 , 若干 " 插件 " 模块 ;

    最终打包时 , 将 " 宿主 " 模块 和 " 插件 " 模块 分开进行打包 ;

    " 宿主 " 模块" 插件 " 模块 都各自是一个单独 apk 安装文件 ;


    插件化 中 , " 宿主 " 模块 和 " 插件 " 模块 可以分开进行编译 , 二者之间互不影响 , 各个模块可以并发进行开发 , " 宿主 " 模块 可以 动态更新插件 ,





    二、插件化示例



    如像 支付宝 等类似的特大型应用 , 内部提供了几百个小的应用模块 , 不可能在开发时就将其集成进去 , 这些软件的本体只有 100 MB 左右 , 不可能将所有的应用都纳入进去 ;

    分辨原生组件与 Web 组件 : 在大型应用中 , 有些应用是使用 WebView 嵌入前端小程序 , 有些是远程组件 , 在 " 开发者选项 " 中 , 打开 " 显示布局边界 " 选项 , 如果是 WebView 就只有一个框 , 如果是原生组件 , 对应的 TextView , ImageView 等都有各自的边界 , 借助布局边界显示可分辨出该第三方小程序是 前端 WebView 界面还是原生应用 ;

    打开 " 显示布局边界 " 选项 :

    在这里插入图片描述

    支付宝主界面效果 : 主界面是原生界面 ;

    在这里插入图片描述

    饿了么界面时 WebView 界面 , 主要内容是 前端小程序 开发的 ;

    在这里插入图片描述

    财富管理模块的股票模块 , 是原生应用 ;

    在这里插入图片描述

    支付宝的主体框架是 Android 原生应用 , 其中的第三方功能 , 小程序 , 基本都是 Web 前端页面 ;

    目前这类应用的开发趋势是使用 Web 应用 替换 原生应用 ;


    股票模块这类 原生应用 , 一般不会在支付宝开发时 , 打包在其中 , 而是通过 插件化 机制 , 动态部署其插件 apk ;

    这类软件第一次打开时 , 需要下载该模块的 插件 apk , 并安装 , 此时会卡顿以下 , 之后可以流畅访问 ;





    三、插件化标准引入



    插件化 中 , " 宿主 " 模块 和 " 插件 " 模块 可以分开进行编译 , 二者之间互不影响 , 各个模块可以并发进行开发 , " 宿主 " 模块 可以 动态更新插件 ;

    " 宿主 " 模块是当前运行的应用 , " 插件 " 模块是下载的插件模块编译后打包的 apk 文件 ;

    在不安装 插件 apk 的情况下 , 调用该 插件中的功能 , 如 Activity , Service , 代码逻辑等 ;


    不是任意 apk 文件都可以接入到 " 宿主 " 插件中 , 要接入的 apk 必须符合一定的标准 ;

    在 apk 插件没有安装 , 因此不存在上下文 , 调用插件中的 Activity 界面时 , 需要将上下文传给插件的 Activity ,

    更多相关内容
  • Android插件化之资源动态加载 一.概述 Android插件化的一个重要问题就是插件资源访问问题,先列出会面对的问题 1.如何加载插件资源 2.如何处理插件资源与宿主资源的处突:插件化资源问题要做到的效果是,如果我们...
  • 这是完整的Android插件化入门工程,当中介绍了宿主和插件的代码合并、资源合并 以及宿主如何调用插件中的Activity的两种方法。
  • Android插件化:从入门到放弃》全书完整版。。完整书签。。刚在微信买的。。
  • 采用360提供的DroidPlugin方案,通过插件化加载安装,并启动外部apk。需要为插件预申请对应权限。
  • Android插件化框架研究.zip,AndroidPluginFramework公司
  • Android插件化目前国内已经有很多开源的工程了,不过如果不实际开发一遍,很难掌握的很好。 下面是自己从0开始,结合目前开源的项目和博客,动手开发插件化方案。 按照需要插件化主要解决下面的几种问题: 1. 代码的...
  • android 插件化

    2017-08-18 16:08:15
    android插件化开发简单易懂的demo
  • 主要介绍了Android插件化-RePlugin项目集成与使用详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 本篇文章是本系列比较核心的一篇文章,我计划这篇文章把插件化的大体技术给讲清楚。

    Hello,各位朋友们,我们继续插件化系列的学习吧。下面是我这个系列文章的行文思路,
    在这里插入图片描述
    本篇文章是本系列比较核心的一篇文章,我计划这篇文章把插件化的大体技术给讲清楚。期间会涉及到系列的前两篇文章的内容,推荐先阅读前面的两篇基础文章:

    【Android插件化系列一】开篇前言:Binder机制,ClassLoader
    【Android插件化系列二】资源与打包流程

    本篇文章预计需要半小时以上时间阅读。读完本篇文章,你将会了解:

    1. 插件化的发展和流派
    2. 插件化技术
    • 如何加载插件中的类和资源
    • 如何解析插件中的信息
    • 如何利用aapt等方法解决宿主和插件资源冲突的问题
    • 如何支持四大组件的插件化

    1. 发展历史和流派

    先稍微介绍一下插件化的发展历史。插件化技术,主要用在新闻,电商,阅读,出行,视频等领域,可以看到包含了我们生活的很多场景。在应用迭代的过程中,1.能快速的修复应用出问题的部分,2.为了抢占市场,快速的根据市场反应进行迭代,3.将不常用功能模块做成插件,减少包体积,这几点对于应用的发展都是相当重要的事情。在这种背景下,插件化技术应运而生。

    下面是比较出名的几个插件化框架,根据出现的时间排序,通过研究他们的原理,可以把发展历史大概分成三代。

    时代代表库特点
    远古AndroidDynamicLoader(屠毅敏)adl基于动态替换Fragment来实现页面的切换,虽然局限大,但是给我们提供了想象的基础
    第一代dynamic-load-apk(任玉刚),DroidPlugin(张勇)dla通过创建ProxyActivity来进行分发,插件必须继承ProxyActivity, 侵入性强且必须小心处理context。DroidPlugin是通过hook系统服务来进行Activity跳转,缺点是hook太多,代码复杂且不够稳定。
    第二代VirtualApk, Small(林光亮),RePlugin为了同时达到插件开发的低侵入性(像开发普通app一样开发插件)和框架的稳定性,在实现原理上都是趋近于选择尽量少的hook,并通过在manifest中预埋一些组件实现对四大组件的插件化。
    第三代VirtualApp,Atlas在这一代中,插件兼容性,稳定性提升到更高的层次。同时,容器化框架的概念越来越流行。

    在这一代中,插件兼容性,稳定性提升到更高的层次。同时,容器化框架的概念越来越流行。

    2.插件化技术

    插件化技术的技术主要可以概括为以下几点:

    1. 插件和宿主之间的代码和资源互用
    2. 插件的四大组件支持和跳转

    这里我们说到了插件和宿主之间的代码和资源互用。其实这里也是有学问的。插件根据是否需要共享资源代码分为独立插件和耦合插件。独立插件是单独运行在一个进程中的,与宿主完全隔离,崩溃不会影响到宿主。但是耦合插件却是和宿主运行在一个进程中,所以插件崩溃,宿主也崩溃了。所以一般业务要根据资源和代码的耦合程度,插件的可靠性等综合考虑插件类型。

    我们接下来慢慢讲解。

    2.1 代码和资源互通

    插件与dex

    因为可能看我文章的还有没接触过插件化的同学,所以增加这一部分讲解插件和dex到底是怎么一种存在形式,插件,我们可以理解为一个单独打包出来的apk。在项目中我们可以建立module并且在模块的build.gradle中把apply plugin: 'com.android.library’改为apply plugin: ‘com.android.application’。这样对这个模块打包的产物就是apk。

    apk在打包的过程中,有一个class文件打入dex的操作,最终Apk中存在的是dex。加载这种dex中的类,使用的ClassLoader也很有讲究。前面我们在Android插件化系列一: 开篇前言,Binder机制,ClassLoader中讲到过,Android常用的就是PathClassLoader和DexClassLoader。PathClassLoader适用于已经安装了的apk,一般作为默认加载器。而这里插件的apk是没有安装的,所以我们需要使用DexClassLoader来加载插件dex中的类。下面是一段基本代码,演示了如何从插件apk的dex中读取类。

    // 生成ClassLoader
    File apkFile = File(apkPath, apkName);
    String dexPath = apkFile.getPath();
    File releaseFile = context.getDir("dex", 0);
    DexClassLoader loader = new DexClassLoader(dexPath, releaseFile.getAbsolutePath(), null, getClassLoader());
    
    // 加载类,使用类的方法
    Class bean = loader.loadClass("xxx.xxx.xxx")  // 填入类的包名
    Object obj = bean.newInstance();
    Method method = bean.getMethod("xxx")  // 填入方法名
    method.setAccessible(true);
    method.invoke(obj)
    

    这样,我们就可以通过反射来获取到类,并使用相应的方法了。

    面向接口编程

    大家会看到,如果像上面那样大量的使用反射,代码是相当丑陋的,扩展性能也差。这让我们想到了,能不能参考依赖倒置原则中的面向接口或抽象编程的思想,预先定义好接口。这样等需要使用的时候,就只需要把对象转换为接口,就能调用接口的方法了。

    比如我们app模块和插件模块plugin依赖了接口模块interface, interface中定义了接口IPlugin。IPlugin的定义是

    interface IPlugin {
        void sayHello(String name)
    }
    

    plugin中就可以定义实现类

    class PluginImpl implement IPlugin {
        @override
        void sayHello(String name) {
            Log.d("log""hello world" + name);
        }
    }
    

    这样,我们就可以在宿主app模块中去使用。具体的使用方法可以有反射和服务发现机制。为了简单,这里只用反射来调用具体的实现类。

    Class pluginImpl = loader.loadClass("xxx.xxx.xxx")  // PluginIMpl 类的包名
    Object obj = pluginImpl.newInstance();              // 生成PluginImpl对象
    IPlugin plugin = (IPlugin)obj;
    plugin.sayHello("AndroidEarlybird");
    

    既然接口都给出了,我们想做别的事情肯定就得心应手了。但是值得注意的是这里的前提是宿主和插件都需要依赖接口模块,也就是说双方是有代码和资源依赖的,因此这种方法只适用于耦合插件,独立插件的话就只能用反射来调用了。

    PMS

    在插件化技术中,ActivityManagerServiche(AMS)和PackageManagerService(PMS)都是相当重要的系统服务。AMS自不用说,四大组件各种操作都需要跟它打交道,PMS也十分重要,完成了诸如权限校捡(checkPermission,checkUidPermission),Apk meta信息获取(getApplicationInfo等),四大组件信息获取(query系列方法)等重要功能。

    使用PMS

    android一般使用PMS来进行应用安装,安装的时候PMS需要借助于PackageParser进行apk解析工作,主要负责解析出一个PackageParser.Package对象,这个对象还是很大用途的。下面是这个Package对象的一些属性值。
    在这里插入图片描述
    可以看到我们通过这个类可以拿到apk中的四大组件,权限等信息,在插件化中,我们有时候会需要利用这个类去拿到广播的信息来处理插件中的静态广播。

    那么如何使用PackageParser这个类呢?下面是VirtualApk的一些使用

        public static final PackageParser.Package parsePackage(final Context context, final File apk, final int flags) throws PackageParser.PackageParserException {
            if (Build.VERSION.SDK_INT >= 24) {
                return PackageParserV24.parsePackage(context, apk, flags);
            } else if (Build.VERSION.SDK_INT >= 21) {
                return PackageParserLollipop.parsePackage(context, apk, flags);
            } else {
                return PackageParserLegacy.parsePackage(context, apk, flags);
            }
        }
    
        private static final class PackageParserV24 {
            static final PackageParser.Package parsePackage(Context context, File apk, int flags) throws PackageParser.PackageParserException {
                PackageParser parser = new PackageParser();
                PackageParser.Package pkg = parser.parsePackage(apk, flags);
                ReflectUtil.invokeNoException(PackageParser.class, null, "collectCertificates",
                        new Class[]{PackageParser.Package.class, int.class}, pkg, flags);
                return pkg;
            }
        }
    

    因为PackageParser针对系统版本变化很大,所以VirtualApk对这个类做了多版本的适配,我们这里只展示了一种。

    Hook PMS

    正如我们需要hook AMS去进行一些插件化的一些工作,有时候我们也得对PMS进行hook。通过看源码,我们知道PMS的获取也是通过Context获取的,直奔ContextImpl类的getPackageManager方法。

    public PackageManager getPackageManager() {
        if (mPackageManager != null) {
            return mPackageManager;
        }
    
        IPackageManager pm = ActivityThread.getPackageManager();
        if (pm != null) {
            // Doesn't matter if we make more than one instance.
            return (mPackageManager = new ApplicationPackageManager(this, pm));
        }
        return null;
    }
    
    // 继续跟进到ActivityThread的getPackageManager方法中
    public static IPackageManager getPackageManager() {
        if (sPackageManager != null) {
            return sPackageManager;
        }
        IBinder b = ServiceManager.getService("package");
        sPackageManager = IPackageManager.Stub.asInterface(b);
        return sPackageManager;
    }
    

    这里我们可以看到,要想hook PMS需要把这两个地方都hook住:

    • ActivityThread的静态字段sPackageManager
    • 通过Context类的getPackageManager方法获取到的ApplicationPackageManager对象里面的mPM字段。

    示例代码如下:

    // 获取全局的ActivityThread对象
    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    Object currentActivityThread = currentActivityThreadMethod.invoke(null);
    
    // 获取ActivityThread里面原始的 sPackageManager
    Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
    sPackageManagerField.setAccessible(true);
    Object sPackageManager = sPackageManagerField.get(currentActivityThread);
    
    // 准备好代理对象, 用来替换原始的对象
    Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
    Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),
            new Class<?>[] { iPackageManagerInterface },
            new HookHandler(sPackageManager));
    
    // 1. 替换掉ActivityThread里面的 sPackageManager 字段
    sPackageManagerField.set(currentActivityThread, proxy);
    
    // 2. 替换 ApplicationPackageManager里面的 mPM对象
    PackageManager pm = context.getPackageManager();
    Field mPmField = pm.getClass().getDeclaredField("mPM");
    mPmField.setAccessible(true);
    mPmField.set(pm, proxy);
    

    管理ClassLoader

    上面我们讲到了如何利用ClassLoader来加载dex中的类,现在我们再来深入聊聊这个话题。首先,需要明确的是,因为我们插件的类都是位于没有安装的apk的dex中,所以我们不能直接使用主app的ClassLoader。那么就会有多种解决方案。

    比较直接的思想是通过对每一个插件都新建一个ClassLoader来做加载。那么如果我们插件很多的时候,我们需要做的就是把每个插件的ClassLoader给记录下来,当使用某个插件的类的时候,用它对应的ClassLoader去加载。正如我们上节的例子中展示的那样。

    另一种思想是直接操作dex数组。宿主和插件的ClassLoader都会对应一个dex数组。那么我们如果能把插件的dex数组合并到宿主的dex数组里面去的话,我们就能用宿主的ClassLoader来反射加载插件的dex数组中的类了。这样做的目的是不需要管理插件的ClassLoader,只要用宿主的ClassLoader就行了。比如我们曾经在Android插件化系列一: 开篇前言,Binder机制,ClassLoader中讲到DexClassLoader的源代码。

    public class BaseDexClassLoader extends ClassLoader {
        private final DexPathList pathList;
    
        public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
            super(parent);  //见下文
            //收集dex文件和Native动态库【见小节3.2】
            this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
        }
    }
    
    public class DexPathList {
        private Element[] dexElements;
    
        public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {
            this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext);
        }
    
        private static List<File> splitDexPath(String path) {
           return splitPaths(path, false);
        }
    
        private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
            List<File> result = new ArrayList<>(); 
            if (searchPath != null) {
                for (String path : searchPath.split(File.pathSeparator)) {
                    // 省略
                }
            }
            return result;
         }
    }
    

    从上面我们可以看出,dexPath字符串是由多个分号分割的。拆分成字符串数组以后,每个path都是一个外部的dex/apk路径。那么我们很自然的想到,能不能把插件的dex路径手动添加到宿主的dexElements数组中呢?答案当然是ok的,方案就是使用Hook。我们可以先反射获取到ClassLoader的dexPathList,然后再获取这个list的dexElements数组,然后手动把插件构建出Element,再拷贝到dexElements数组中。热修复框架Nuwa也是使用这种思想。

    第三种思路是ClassLoader delegate。本文推荐这种方法。首先我们自定义ClassLoader,取代原先宿主的ClassLoader,并且把宿主作为Parent,同时在自定义的ClassLoader中用一个集合放置所有插件的ClassLoader,然后这个自定义ClassLoader在加载任何一个类的时候,依据双亲委托机制,加载类都会先从宿主的ClassLoader中寻找,没有的话再遍历ClassLoader集合寻找能加载这个类的插件ClassLoader。当然这里又会有提高效率的优化点,比如遍历集合的方式可以改为先从已加载过的集合中寻找,再从未加载过的集合中寻找。下面是示例代码。

    class PluginManager {
        public static void init(Application application) {
            //初始化一些成员变量和加载已安装的插件
            mPackageInfo = RefInvoke.getFieldObject(application.getBaseContext(), "mPackageInfo");
            mBaseContext = application.getBaseContext();
            mNowResources = mBaseContext.getResources();
    
            mBaseClassLoader = mBaseContext.getClassLoader();
            mNowClassLoader = mBaseContext.getClassLoader();
            
            ZeusClassLoader classLoader = new ZeusClassLoader(mBaseContext.getPackageCodePath(), mBaseContext.getClassLoader());
    
            File dexOutputDir = mBaseContext.getDir("dex", Context.MODE_PRIVATE);
            final String dexOutputPath = dexOutputDir.getAbsolutePath();
            for(PluginItem plugin: plugins) {
                DexClassLoader dexClassLoader = new DexClassLoader(plugin.pluginPath,
                        dexOutputPath, null, mBaseClassLoader);
                classLoader.addPluginClassLoader(dexClassLoader);
            }
            // 替换原有的宿主的ClassLoader为自定义ClassLoader,将原来的宿主ClassLoader作为自定义ClassLoader的
            RefInvoke.setFieldObject(mPackageInfo, "mClassLoader", classLoader);
            Thread.currentThread().setContextClassLoader(classLoader);
            mNowClassLoader = classLoader;
        }
    }
    
    class ZeusClassLoader extends PathClassLoader {
        private List<DexClassLoader> mClassLoaderList = null;
    
        public ZeusClassLoader(String dexPath, ClassLoader parent, PathClassLoader origin) {
            super(dexPath, parent);
    
            mClassLoaderList = new ArrayList<DexClassLoader>();
        }
    
        /**
         * 添加一个插件到当前的classLoader中
         */
        protected void addPluginClassLoader(DexClassLoader dexClassLoader) {
            mClassLoaderList.add(dexClassLoader);
        }
    
        @Override
        protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
            Class<?> clazz = null;
            try {
                //先查找parent classLoader,这里实际就是系统帮我们创建的classLoader,目标对应为宿主apk
                clazz = getParent().loadClass(className);
            } catch (ClassNotFoundException ignored) {
            }
    
            if (clazz != null) {
                return clazz;
            }
    
            //挨个的到插件里进行查找
            if (mClassLoaderList != null) {
                for (DexClassLoader classLoader : mClassLoaderList) {
                    if (classLoader == null) continue;
                    try {
                        //这里只查找插件它自己的apk,不需要查parent,避免多次无用查询,提高性能
                        clazz = classLoader.loadClass(className);
                        if (clazz != null) {
                            return clazz;
                        }
                    } catch (ClassNotFoundException ignored) {
                    }
                }
            }
            throw new ClassNotFoundException(className + " in loader " + this);
        }
    }
    

    资源

    Resources&AssetManager

    android中的资源大致分为两类:一类是res目录下存在的可编译的资源文件,比如anim,string之类的,第二类是assets目录下存放的原始资源文件。因为Apk编译的时候不会编译这些文件,所以不能通过id来访问,当然也不能通过绝对路径来访问。于是Android系统让我们通过Resources的getAssets方法来获取AssetManager,利用AssetManager来访问这些文件。

    Resources resources = context.getResources();
    AssetManager manager = resources.getAssets();
    InputStream is = manager.open("filename");
    

    Resources和AssetManager的关系就像销售和研发。Resources负责对外,外部需要的getString, getText等各种方法都是通过Resources这个类来调用的。而这些方法其实都是调用的AssetManager的私有方法。所以最终两类资源都是AssetManager在兢兢业业的向Android系统要资源,为外界服务着。

    AssetManager里有个很重要的方法addAssetPath(String path)方法,App启动的时候会把当前apk的路径传进去,然后AssetManager就能访问这个路径下的所有资源也就是宿主apk的资源了。那么idea就冒出来了,如果我们把插件的地址也传进这个方法去,是不是就能得到一个能同时访问宿主和插件的所有资源的“超级”AssetManager了呢?答案是肯定的,这也是插件化对资源的一种解决方案。

    下面是一段示例代码展示了获取宿主的Resources中的AssetManager,然后调用addAssetPath添加插件路径,最后生成一个新的Resources的方法

    // 新生成AssetManager,调用addAssetPath
    AssetManager assetManager = resources.getAssets();  // 先通过Resources拿到示例代码
    Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
    addAssetPath.invoke(assetManager, dexPath1);
    mAssetManager = assetManager;
    
    // 根据新生成的AssetManager生成Resources
    mResources = new Resources(mAssetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
    

    接下来我们要分别将宿主和插件的原有Resources替换成我们上面生成的Resources。注意这里传入的application应该是宿主和插件对应的Application。

    Object contextImpl = RefInvoke.getFieldObject("android.app.ContextImpl", application, "getImpl")  // 获取Application的context
    LoadedApk loadedApk = (LoadedApk)RefInvoke.getFieldObject(contextImpl, "mPackageInfo")
    RefInvoke.setFieldObject(loadedApk, "mResources", resources);
    RefInvoke.setFieldObject(application.getBaseContext(), "mResources", resources);
    

    除了需要替换Application的Resources对象,我们也需要替换Activity的Resources对象,宿主和插件的Resources都需要替换。这是因为他们都是Context,只替换Application的并不能影响到Activity。我们可以在Instrumentation回调callActivityOnCreate的时候去替换。这点在后面Activity插件化处理部分再详细讲解。

    上面只是展示了使用,想了解更多信息的可以查看VirtualApk

    解决资源冲突

    Android插件化系列二: 资源与打包流程中我们提到了插件和宿主分别打包的时候可能会存在资源id冲突的情况,上面我们使用了一个超级Resource之后,id如果重复了,运行的时候使用id来查找资源就会报错。
    为了解决id冲突的问题一般有三种方案:

    1. 修改android打包的aapt工具,将插件资源前缀改为0x02 - 0x7e之间的数值
    2. 进入到哪个插件,就为哪个插件生成新的AssetManager和Resources

    其中,方案二比较复杂,并且不利于宿主和插件资源的互相调用。所以我们在上节采用的是超级Resources的方案,所以这里我们介绍一下方案一,也就是修改aapt工具。
    aapt是android打包资源的处理工具,大多数的插件话开源库对齐进行改造无外乎都是两种方式:

    • 修改aapt源码,定制aapt工具,编译期间修改资源id的PP段,可以参考Android中如何修改编译的资源ID值, DynamicApk就是这样使用的。
    • 修改aapt的产物,即resources.arsc,编译后期重新整理插件Apk的资源,编排ID。VirtualApk就是采用了这种方案,自定义gradle transform插件,hook了gradle打包的ProcessAndroidResources task,在apk资源编译任务完成后,重新设置插件的resources.arsc文件中的资源id,并更新R.java文件。可以参考插件化-解决插件资源ID与宿主资源ID冲突的问题这篇文章。

    可以看到aapt(1)处理插件化的资源并不是很友好,开发和维护难度都比较大,后来google推出了Android App Bundle这个和插件很类似的feature,就推出了aapt2来支持了资源分包。我们注意官网上这几个aapt2的打包参数:

    在这里插入图片描述

    是不是发现官方已经给我们支持好了按package区分资源前缀id,多美好啊哈哈。

    当然这里也是有坑的。那就是需要buildTools版本大于28.0.0
    在buildTools 28.0.0以前,aapt2自带了资源分区,通过–package-id参数指定。但是该分区只支持>0x7f的PP段,而在Android 8.0之前,是不支持>0x7f的PP段资源的,运行时会抛异常。但是当指定了一个<0x7f的PP段资源后,编译资源时却会报错

    error: invalid package ID 0x15. Must be in the range 0x7f-0xff..
    

    所以对于Android P之前使用的buildTools版本(<28.0.0),我们必须通过修改aapt2的源码达到资源分区的目的。而在28.0.0以后,aapt2支持了<0x7f预留PP段分区的功能,只需要指定参数–allow-reserved-package-id即可。

    --allow-reserved-package-id --package-id package-id
    

    插件使用宿主资源
    在我们为宿主开发插件的时候,经常不可避免的出现插件要使用宿主中资源的情况,如果我们把宿主的资源copy一份放在插件中,那无疑会大大增加包的大小,并且这些都是重复资源,是不应该在App中存在的。那么我们就得想办法让插件使用宿主的资源。比如这样

    在这里插入图片描述

    前面已经讲到了,我们可以通过为插件和宿主一起构建一个超级Resources,包括了插件和宿主所有的资源,理论上可以通过资源id获取到所有的资源,那么问题来了,插件中的R文件是不包含宿主的R文件的,我们在编码的时候怎么使用呢?

    下面分代码使用和xml使用两种使用方式来说解决方案:
    代码使用:在插件资源打包任务processResourcesTask完成后将宿主的R.txt文件(打包过程中产生,位置在build/intermediates/symbols/xx/xx/R.txt)合并到插件的R.txt文件,然后再生成R.java,这样就可以正常的使用R文件来索引资源了
    xml使用:我们需要在aapt2打包的时候指定-I参数。

    在这里插入图片描述

    这样,我们通过-I指定宿主的资源包,就可以在xml中使用宿主的资源了。

    总结

    本节我们首先介绍插件代码的dex加载,给出了利用反射和面向接口编程来获取插件中的代码的方法,然后介绍了通过自定义delegate ClassLoader的方法来更好的加载插件和宿主中的类,接下来介绍了PMS如何获取插件的信息以及如何进行自定义hook,最后讲到了插件使用宿主资源的一些知识。到了这一步,我们已经可以获取到插件的各种信息,可以实现宿主和插件中的代码互通,可以实现插件调用宿主的资源,基本上算是迈出了一大步!但是只有代码和资源是不够的,接下来我们看看怎么处理android的四大组件,这一块才是重头戏,也是插件化的精髓

    2.2 四大组件支持

    android的四大组件其实有挺多的共通之处,比如他们都接受ActivityManagerService(AMS)的管理,都需要通过Binder机制请求AMS服务。并且他们的请求流程也是基本相通的,其中Activity又是最重要的组件,出镜最多,同时也是日常开发接触最多的组件,我们将会主要以Activity为例,讲解插件化对四大组件的支持,其余三个组件有不同或值得注意的地方我们会另外指出来。当然,针对四大组件的解决方案有很多种,本文限于篇幅只介绍DroidPlugin的动态替换方案。

    android的四大组件其实有挺多的共通之处,比如他们都接受ActivityManagerService(AMS)的管理,都需要通过Binder机制请求AMS服务。并且他们的请求流程也是基本相通的,其中Activity又是最重要的组件,出镜最多,同时也是日常开发接触最多的组件,我们将会主要以Activity为例,讲解插件化对四大组件的支持,其余三个组件有不同或值得注意的地方我们会另外指出来。当然,针对四大组件的解决方案有很多种,本文限于篇幅只介绍DroidPlugin的动态替换方案。

    Activity

    AndroidManifest.xml预占位
    相信做过Android开发的都知道,四大组件基本都是要在AndroidManifest.xml中定义的,不然系统就会报错,然后问你 have you declared this activity in your AndroidManifest.xml?
    **【必须在AndroidMainfest.xml中定义四大组件】**这一点对插件化确实是比较严重的限制,毕竟我们并没有办法提前就把插件中的Activity声明进去,但是这个限制也并不是没办法解决的。比如DroidPlugin就采用了预占位Activity到AndroidManifest.xml中的方案。

    DroidPlugin的方案思想很简单,先在AndroidManifest.xml中预定义好各种LaunchMode的占位Activity和其余三大组件。比如

    <activity
        android:name=".StubSingleTaskActivity1"
        android:exported="true"
        android:launchMode="singleTask"
        android:theme="@style/Theme.NoActionBar"
        android:screenOrientation="portrait" />
    
    <activity
        android:name=".StubSingleTopActivity1"
        android:exported="true"
        android:launchMode="singleTop"
        android:theme="@style/Theme.NoActionBar"
        android:screenOrientation="portrait" />
    

    这样的话,我们就要实行狸猫换太子的方法,把本来想要开启的Activity换成StubActivity,然后躲过了系统对【必须在AndroidMainfest.xml中定义四大组件】的审查真正的开始start Activity的时候再去打开真正的目的Activity。那么我们怎么去实现这个想法呢,这就要求我们熟悉Activity的启动流程了。
    startActivity流程
    startActivity的流程比较繁杂,甚至可以作为一篇单独的文章来讲解。网上有很多的文章在讲解,比较详细牛逼的是老罗的Android应用程序的Activity启动过程简要介绍和学习计划。大家如果有兴趣的话可以参考。我这里只简明扼要的讲解部分的流程。
    首先先看一个流程图
    在这里插入图片描述
    首先我们都是从startActivity进去的,辗转发现它调用了Instrumentation的execStartActivity方法,接着在这个函数里面调用了ActivityManagerNative类的startActivity方法,请求到了ActivityManagerService的服务。这一点就是我们在Android插件化系列一: 开篇前言,Binder机制,ClassLoader讲到过的Binder机制在Activity启动过程中的体现。可以看到就是在AMS的startActivity的方法中校验了Activity是否注册,确定了Activity的启动模式,AMS我们没办法改啊,所以咱们得出个结论一定要在校验前的流程里把Activity给替换掉。继续往下看,可以看到ActivityStackSupervisor把启动的重任最终委托给了ApplicationThread。

    我们在前面的系列一中说过,Binder机制其实是互为Client和Server的,在app申请AMS服务的时候,AMS是Server,AMP是AMS在app的代理。而在申请到AMS服务以后,AMS需要请求App进行后续控制的时候,ApplicationThread就是Server,ApplicationThreadProxy就是ApplicationThread在AMS侧的代理。
    在这里插入图片描述
    继续往下看,可以看到ActivityThread调用了H类,最终调用了handleLaunchActivity方法,由Instrumentation创建出了Activity对象,启动流程结束。

    “狸猫换太子”

    看完了上面的启动流程,大家可以想到,在这个流程中我只要在调用AMS前把目标Activity替换成StubActivity(上半场),在AMS校验完,马上要打开Activity的时候替换为目标Activity(下半场),这样就可以达到“狸猫换太子”启动目标Activity的目的了啊。因为流程较长,参与的类较多,所以我们可以选择的hook点也是相当多的,但是我们越早hook,后续的操作越多越容易出问题,所以我们选择比较后面的流程去hook。这里选择:

    • 上半场,hook ActivityManagerNative对于startActivity方法的调用
    • 下半场,hook H.mCallback对象,替换为我们的自定义实现,

    hook AMN
    下面是一些示例代码,可以看到我们替换掉交给AMS的intent对象,将里面的TargetActivity的暂时替换成已经声明好的替身StubActivity。

    if ("startActivity".equals(method.getName())) {
        // 只拦截这个方法
        // 替换参数, 任你所为;甚至替换原始Activity启动别的Activity偷梁换柱
    
        // 找到参数里面的第一个Intent 对象
        Intent raw;
        int index = 0;
    
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof Intent) {
                index = i;
                break;
            }
        }
        raw = (Intent) args[index];
    
        Intent newIntent = new Intent();
    
        // 替身Activity的包名, 也就是我们自己的包名
        String stubPackage = raw.getComponent().getPackageName();
    
        // 这里我们把启动的Activity临时替换为 StubActivity
        ComponentName componentName = new ComponentName(stubPackage, StubActivity.class.getName());
        newIntent.setComponent(componentName);
    
        // 把我们原始要启动的TargetActivity先存起来
        newIntent.putExtra(AMSHookHelper.EXTRA_TARGET_INTENT, raw);
    
        // 替换掉Intent, 达到欺骗AMS的目的
        args[index] = newIntent;
    
        Log.d(TAG, "hook success");
        return method.invoke(mBase, args);
    
    }
    

    hook H.mCallback 前面我们说过,ActivityThread是借助于H这个类完成四大组件的操作管理。H继承自Handler,我们看看Handler处理消息的dispatchMessage方法。

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    

    而H的handleMessage方法中正是处理LAUNCH_ACTIVITY,CREATE_SERVICE等消息的地方。所以我们就会想,在mCallback.handleMessage中替换回原来的Activity应该就是最晚的时间点了吧。下面是自定义的Callback类,反射设置为ActivityThread的H的mCallback就行了。

    class MockClass2 implements Handler.Callback {
    
        Handler mBase;
    
        public MockClass2(Handler base) {
            mBase = base;
        }
    
        @Override
        public boolean handleMessage(Message msg) {
    
            switch (msg.what) {
                // ActivityThread里面 "LAUNCH_ACTIVITY" 这个字段的值是100
                // 本来使用反射的方式获取最好, 这里为了简便直接使用硬编码
                case 100:
                    handleLaunchActivity(msg);
                    break;
    
            }
    
            mBase.handleMessage(msg);
            return true;
        }
    
        private void handleLaunchActivity(Message msg) {
            // 这里简单起见,直接取出TargetActivity;
            Object obj = msg.obj;
    
            // 把替身恢复成真身
            Intent intent = (Intent) RefInvoke.getFieldObject(obj, "intent");
    
            Intent targetIntent = intent.getParcelableExtra(AMSHookHelper.EXTRA_TARGET_INTENT);
            intent.setComponent(targetIntent.getComponent());
        }
    }
    

    替换Resources

    还记得我们在第一节留下了一个问题吗,就是Activity的资源替换要在Instrumentation回调callActivityOnCreate的时候进行。这个时间点比较临近onCreate,Instrumentation也比较方便去hook。下面展示这个技术,需要传入超级Resources。

    public void hookInstrumentation(){
        Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread");
        // 拿到原始的 mInstrumentation字段
        Instrumentation mInstrumentation = (Instrumentation) RefInvoke.getFieldObject(currentActivityThread, "mInstrumentation");
        // 创建代理对象
        Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation, resources);  // 这里的resources是我们的超级Resources
        RefInvoke.setFieldObject(currentActivityThread, "mInstrumentation", evilInstrumentation);
    }
    
    // 这里的Activity是在
    public class EvilInstrumentation extends Instrumentation {
        Instrumentation mBase;
        Resources mRes;
    
        public EvilInstrumentation(Instrumentation base,Resources res) {
            mBase = base;
            mRes = res;
        }
    
        @override
        public void callActivityOnCreate(Activity activity, Bundle bundle) {
            // 替换Resources
            if (mRes != null) {
                RefInvoke.setFieldObject(activity.getBaseContext().getClass(), activity.getBaseContext(), "mResources", mRes);
            }
            super.callActivityOnCreate(activity, bundle);
        }
    }
    

    Service

    Service的处理和Activity的基本一样,区别是调用多次startService并不会启动多个Service实例,而是只有一个实例,所以我们的占位Service得多定义一些。

    BroadcastReceiver

    BroadcastReceiver的插件化和Activity的不太一样。Android中的广播分为两种:静态广播和动态广播,动态广播不需要和AMS交互,就是一个普通类,只要按照前面的ClassLoader方案保证他能加载就行了。但是静态广播比较麻烦,除了需要在AndroidManifest.xml中进行注册以外,他和Activity不一样的是,他还附加了IntentFilter信息。而IntentFilter信息是随机的,无法被预占位的。这个时候就只能把取出插件中的静态广播改为动态广播了。虽然会有一些小问题,但是影响不大

    前面我们讲到了PackageParser可以获取到插件的四大组件的信息,存储到Package对象中,那么我们就有个思路,通过PMS获取到BroadcastReceiver,然后把其中的静态广播改为动态广播.

    public static void preLoadReceiver(Context context, File apkFile) {
        // 首先调用parsePackage获取到apk对象对应的Package对象
        Object packageParser = RefInvoke.createObject("android.content.pm.PackageParser");
        Class[] p1 = {File.class, int.class};
        Object[] v1 = {apkFile, PackageManager.GET_RECEIVERS};
        Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage", p1, v1);
    
        // 读取Package对象里面的receivers字段,注意这是一个 List<Activity> (没错,底层把<receiver>当作<activity>处理)
        // 接下来要做的就是根据这个List<Activity> 获取到Receiver对应的 ActivityInfo (依然是把receiver信息用activity处理了)
        List receivers = (List) RefInvoke.getFieldObject(packageObj, "receivers");
    
        for (Object receiver : receivers) {
            registerDynamicReceiver(context, receiver);
        }
    }
    
    // 解析出 receiver以及对应的 intentFilter
    // 手动注册Receiver
    public static void registerDynamicReceiver(Context context, Object receiver) {
        //取出receiver的intents字段
        List<? extends IntentFilter> filters = (List<? extends IntentFilter>) RefInvoke.getFieldObject(
                "android.content.pm.PackageParser$Component", receiver, "intents");
    
        try {
            // 把解析出来的每一个静态Receiver都注册为动态的
            for (IntentFilter intentFilter : filters) {
                ActivityInfo receiverInfo = (ActivityInfo) RefInvoke.getFieldObject(receiver, "info");
    
                BroadcastReceiver broadcastReceiver = (BroadcastReceiver) RefInvoke.createObject(receiverInfo.name);
                context.registerReceiver(broadcastReceiver, intentFilter);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

    ContentProvider

    ContentProvider的插件化方法和BroadcastReceiver的很像,但是和BroadcastReceiver不同的是,BroadcastReceiver中的广播叫做注册,但ContentProvider是要“安装”。方案是:

    首先,调用PackageParser的parsePackage方法,把得到的Package对象通过generateProviderInfo转换为ProviderInfo对象。

    public static List<ProviderInfo> parseProviders(File apkFile) throws Exception {
    
        //获取PackageParser对象实例
        Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
        Object packageParser = packageParserClass.newInstance();
    
        // 首先调用parsePackage获取到apk对象对应的Package对象
        Class[] p1 = {File.class, int.class};
        Object[] v1 = {apkFile, PackageManager.GET_PROVIDERS};
        Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage",p1, v1);
    
        // 读取Package对象里面的services字段
        // 接下来要做的就是根据这个List<Provider> 获取到Provider对应的ProviderInfo
        List providers = (List) RefInvoke.getFieldObject(packageObj, "providers");
    
        // 调用generateProviderInfo 方法, 把PackageParser.Provider转换成ProviderInfo
    
        //准备generateProviderInfo方法所需要的参数
        Class<?> packageParser$ProviderClass = Class.forName("android.content.pm.PackageParser$Provider");
        Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
        Object defaultUserState = packageUserStateClass.newInstance();
        int userId = (Integer) RefInvoke.invokeStaticMethod("android.os.UserHandle", "getCallingUserId");
        Class[] p2 = {packageParser$ProviderClass, int.class, packageUserStateClass, int.class};
    
        List<ProviderInfo> ret = new ArrayList<>();
        // 解析出intent对应的Provider组件
        for (Object provider : providers) {
            Object[] v2 = {provider, 0, defaultUserState, userId};
            ProviderInfo info = (ProviderInfo) RefInvoke.invokeInstanceMethod(packageParser, "generateProviderInfo",p2, v2);
            ret.add(info);
        }
    
        return ret;
    }
    

    然后我们需要调用ActivityThread的installContentProviders方法把这些ContentProvider“安装”到宿主中。

       public static void installProviders(Context context, File apkFile) throws Exception {
            List<ProviderInfo> providerInfos = parseProviders(apkFile);
    
            for (ProviderInfo providerInfo : providerInfos) {
                providerInfo.applicationInfo.packageName = context.getPackageName();
            }
    
            Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");
    
            Class[] p1 = {Context.class, List.class};
            Object[] v1 = {context, providerInfos};
    
            RefInvoke.invokeInstanceMethod(currentActivityThread, "installContentProviders", p1, v1);
        }
    

    ContentProvider的插件化还需要注意:

    1. App安装自己的ContentProvider是在进程启动时候进行,比Application的onCreate还要早,所以我们要在Application的attachBaseContext方法中手动执行上述操作。
    2. 让外界App直接调用插件的App,并不是一件特别好的事情,最好是由App的ContentProvider作为中转。因为字符串是ContentProvider的唯一标志,转发机制就特别适用。

    在这里插入图片描述

    总结

    本文首先介绍了插件化中宿主和插件代码和资源互通的方式,然后介绍了四大组件的插件化方法,因为插件化技术太过繁杂,并没有把所有的细节都覆盖到,所介绍的方案也只是当今比较实用,经受过考验的一套,并没有介绍太多的方法。目的是让读者们和我一起,先从整体上理解插件化的机制,然后就容易去区分各种开源库的原理和思路了。

    参考

    感谢下面的各位老师的书籍或文章,让我受益匪浅。

    1.包建强《Android插件化开发指南》
    2.田维术的博客
    3.插件化加载dex跟资源原理
    4.深入理解Android插件化技术
    5.插件化-解决插件资源ID与宿主资源ID冲突的问题
    6.官网aapt2
    7.再谈 aapt2 资源分区

    展开全文
  • android插件化

    千次阅读 2020-09-02 17:52:35
    插件化是一种动态升级app功能的解决方案,不同于热修复(仅仅是修复功能),类似于RN、Weex(目的类似)。都是为了在不发版本的情况下,可以让用户用上最新的功能。不过RN、Weex还额外支持跨平台。相对于RN和Weex,...

     

    概述

           插件化是一种动态升级app功能的解决方案,不同于热修复(仅仅是修复功能),类似于RN、Weex(目的类似)。都是为了在不发版本的情况下,可以让用户用上最新的功能。不过RN、Weex还额外支持跨平台。相对于RN和Weex,插件化有以下的优缺点:

     

    优点:

    • 对于业务方,无额外的学习成本,基本无感知
    • 性能等同于原生、可以做任何原生可以做的事情
    • 天然代码隔离、使得插件化的代码更加的“高内聚、低耦合”
    • 插件并发开发,开发之间互不影响

     

    缺点:

    • 稳定性差,使用了大量大反射来实现,尤其是Android P以后Google对系统API的调用做出了限制,虽然有办法跳过(后面会说),但是却无疑增加了使用风险。
    • 安全性低,恶意插件将会有比较大的权限,来做破坏软件的事情。(一般会添加签名校验)
    • 插件化目前没有一套通用的规范,基本上都是各用各的,导致插件无法通用
    • 只适用于android,无法和iOS保持方案统一

     

    事实上,有一部分的项目采用动态化、不仅仅是为了其动态性,更是为了对模块化的升级,对于大型复杂项目而言,单纯的“模块化”已经不能很好的隔离分解项目的复杂性,而“插件化”可以帮助项目,将独立且足够内聚的业务独立出来成为一个插件,然后交给其他团队来维护。

     

    康威定律:软件源代码的组织结构要与开发团队的组织结构尽量保持一致

     

     

     

    详情

     

    类加载

     

    说到插件化,第一个谈到的是类加载器,这个是插件化的核心,所有的一切都是围绕着这个转的。与JAVA类加载的过程类似、android的类加载也是双亲委派、如下图:

     

    不同的是android使用的类加载器是PathClassLoader(加载已经安装过的APK)和DexClassLoader(未安装的apk、dex、jar)。对于一般情况,可以使用类加载器直接加载下载下来的apk文件,不需要做任何处理,android平台上有些双开程序也是利用这个原理,因为apk是完整的。但是考虑到插件体积的问题,我们会将宿主中已经存在的代码从插件中剔除,来保证插件的最小化。为了保证不冲突,我们会想办法优先加载插件里面的class,由以下两种方式可以实现:

     

    通过在dexPathList的前面插入插件的DexFile来实现优先加载的逻辑

     

     

    另外这种通过更改类加载模型,从双亲委派更改为,优先子加载器加载,加载失败才从父加载器加载。

     

     

    资源加载

    资源加载是插件化的另外一个问题,幸运的是android也提供的API来加载额外的资源文件、不过可惜的是方法并没有暴露出来,只能通过反射进行调用。

     

     

     

     

    遇到的问题

     

    四大组件启动

    在android中,四大组件想要启动必须事先定义到AndroidManifest文件中,而插件由于是动态下发的,无法事先确定内部使用到的四大组件,导致启动会出现异常。目前的解决方案有两种:“占坑法”、“欺上瞒下”

     

    占坑法:通过实现在AndroidManifest中定义一系列的四大组件、通过代理的方式来将实现委托给插件来完成。

    代理模式:使用代理类来接收用户的请求,而真正的处理交给被代理类来进行处理,它们的关系基本上在编译阶段就已经确定下来了(这点和装饰器模式不同,装饰器在运行时指定),代理主要关注的是请求的访问控制,比如插件化中Activity的代理会将实际的跳转意图,控制到占位Activity中来实现四大组件的启动。

    代理分类:静态代理、动态代理(目前android只支持接口)、字节码代理(静态代理的自动化版本,在编译期间自动生成代理对象)。

    以下是个人的一些理解,不一定正确,我单独提出来:

    代理模式,我认为因为把代理一词让出来,可以叫它委托模式,我对代理的理解是,它代表的是一个更广阔的概念,可以理解为它是其他一些模式的基础(装饰者、中介、外观、适配器等等)事实上都是对其他对象的代理,帮助行使职责,而仅仅只是关注点不同。装饰器,关注功能的增强、委托,关注访问的控制、中介,关注多组对象的交互、外观、更关注一组功能的融合。

     

     

    欺上瞒下:思路大概是,先在宿主中定义一个替身Activity,然后也是在启动Activity过程中通过反射Hook两处地方代码,第一处是在准备跨进程调用ActivityManagerService前,即Instrumentation.java的execStartActivity方法中通过ActivityManagerNative.getDefault()它返回一个IActivityManager对象,我们对它创建一个代理,在IActivityManager对象去调用startActivity前把目标Activity替换成替身Activity,以达到欺骗目的。然后第二处是在ActivityManagerSerfvice跨进程回来后,在ActivityThread.java中接收LAUNCH_ACTIVITY消息前,可以对Handle的callback进行代码,让其消息接收前将目标Activity换回来。这样做就能达到只需要在宿主中只声明一个替身Activity就能满足于插件中Actvitiy

     

     

     

    资源命名冲突

    插件的资源在运行时会进行加载,并合入到宿主插件资源中,因为插件和宿主的编译过程是独立中,无法保证资源ID不进行冲突。而一旦资源冲突,将会导致冲突的资源被覆盖,将会导致显示内容混乱,影响插件正常展示。

     

    解决思路:

     

    1. 修改aapt源码,定制aapt工具,编译期间修改PP段。(PP字段是资源id的第一个字节,表示包空间)

    DynamicAPK的做法就是如此,定制aapt,替换google的原始aapt,在编译的时候可以传入参数修改PP段:例如传入0x05编译得到的资源的PP段就是0x05。对于具体实现可以参考这篇博客https://blog.csdn.net/jiangwei0910410003/article/details/50820219

     

    2. 修改aapt的产物,即,编译后期重新整理插件Apk的资源,编排ID。

    VirtualApk采用的就是这个方案。对于具体实现可以参考这篇博客https://blog.csdn.net/weixin_43887839/article/details/86651232

     

    3. 使用aapt2的新能力

     

     

    Android P以后反射限制

    google为了app生态的稳定性和安全性,在Android P(9.0)对系统私有API的访问进行了限制。不幸的是,插件化使用了大量的系统私有API,我们不得不想办法来绕过这些限制。

    目前来看,最简单的方式就是使用“元反射”

    http://weishu.me/2019/03/16/another-free-reflection-above-android-p/

     

     

    插件和宿主三方库冲突

    插件可能会使用到宿主已经存在的库, 为了降低插件的体积,我么需要想办法在打包阶段将重复代码提出调。Gradle有提供compileOnly来实现仅仅在编译阶段依赖三方库,但是如果你依赖了三方库的资源,那么这种方式会导致编译失败。

    自定义编译脚本,在编译时仅仅删除代码,不删除资源来实现该功能。可以参考:

    https://blog.csdn.net/xuexiangjys/article/details/84147652

     

     

    最后

     

    实现插件化的方式很多,但不管怎么样,都绕不开一些黑科技,需要Hook住系统的关键节点。但是Android P以后google为了安全性和稳定性的考虑,已经开始收紧了对系统api的反射调用,虽然目前有办法绕过,但是绝对不是长久之技。除此之外有那些替代方案呢?

    我们回忆下我们做插件化的目的:

    • 边界隔离
    • 并行开发
    • 动态部署
    • 快速编译
    • 模块解耦
    • 降低大小

     

    探索道路

    ● Follow官方的方案

    这种方案只解决了包大小的问题,开发者将app分为三部分(基本apk、资源apk、非必须apk),且依赖googlePlay。

     

    ● 组件化方案

    ● 大前端:RN, Weex, 快应用等

     

    展开全文
  • Android插件化实现方案

    千次阅读 2021-02-19 15:38:55
    插件化对于广大Android开发者而言,应该是一个不陌生的名词。许多APP都有用到插件化技术,例如逢年过节,许多APP都会动态换上许多营造节日氛围的ui特效,但是却不需要用户去重新下载APP;再者下载一款游戏平台APP,...

    插件化对于广大Android开发者而言,应该是一个不陌生的名词。许多APP都有用到插件化技术,例如逢年过节,许多APP都会动态换上许多营造节日氛围的ui特效,但是却不需要用户去重新下载APP;再者下载一款游戏平台APP,若一次性下载一个包含所有游戏的平台APP,那肯定需要很长时间,而用户往往只玩其中一两款,这时候就需要“按需下载”,在用户想玩的时候在下载相应游戏的插件,这样体验就好多了。

           目前来说实现Android实现插件化的方案大致分为两个方向:

           1、动态替换方案:提供对Android底层的各种类进行Hook,来实现加载插件中的四大组件,以DroidPlugin框架为代表;

           2、静态代理方案:通过ProxyActivity统一加载插件中的所有Activity,以that框架为代表。

           本文以方案二静态代理来实现Activity的插件化。

           本文项目结构如下:

           本文项目分为三大模块,主APP模块,插件APP模块,以及充当两者桥梁,起到解耦作用的pluginlib Library部分。

    接下来我们分别讲解这三个部分。

    一、主APP模块

           我们知道项目build完后会生成apk文件,作为主APP模块,我们需要加载外部的插件apk,正常的加载流程应该如下:

           1、从服务器下载插件dex到手机SDCard,为此需要申请SDCard读写权限;

           2、读取插件apk中的dex文件,生成对应的DexClassLoader;

           3、使用DexClassLoader的loadClass方法读取插件dex中的任何一个类。

           为了模拟从服务器下载插件的方法,我们首先build插件APP,生成相应的apk文件,然后将apk文件重命名放到主APP模块的assets文件中,APP启动后会把asset目录中的插件复制到内存中,如下图所示:

    相关代码如下:

     

    二、Library模块

           首先需要一个插件APK的实体类,代码如下:

           其中DexClassLoader用于加载插件中的类,Resources和AssetManager用于加载插件中的资源,PackageInfo用于获取插件中的包信息。

           同时需要一个PluginManager类来管理实体对象,代码如下:

           另外,我们需要意识到,插件中的Activity不同于我们正常启动的Activity有AMS管理调用其生命周期,对于插件Activity,我们需要管理它的生命周期,为此我们写了一个ProxyActivity来充当这个管理的角色,如下:

           为了更好地管理和调用插件Activity,我们需要制定一套规范,让插件Activity遵循我们这套规则,为此写了如下接口:

            同时写了一个BasePluginActivity来让插件Activity继承:

           如上代码所示,若Activity属于主APK,即mFrom=FROM_INTERNAL,则走常规的启动流程,若属于插件APK,则走插件管理的启动流程。

    三、插件APP模块

           插件APP这边比较简单,继承于BasePluginActivity,代码如下:

            整体效果如下,点击加载APK文件成功后,点击跳转,跳转到插件Activity。

          

         

    四、总结

           插件化是Android开发中比较重要的一块,涉及的东西很多,如Android四大组件的插件化,动态代理,四大组件的启动过程,反射机制等,本文的介绍只是冰山一角,后续需要研究学习的地方还很多,有兴趣的朋友可以一起探讨。

           PS:许多同行对插件化有个误区,认为插件化能极大减少APK的体积,把需要“按需下载”的APK在需要时才下载到主APK中,其实不然。确实插件化一定程度上能减少APK的体积,但是假设一个插件APK大小为10M,让用户从服务器下载一个10M的插件新版本,需要耗费很长时间。合理的做法应该把插件1.0版本放在主APP中,APP发版后需要更新插件,通过增量更新的方式来加载。

           最后,附上项目Github地址,有问题可交流:https://github.com/xudongstorm/PlugProject

     

    原文转载自:https://blog.csdn.net/m0_37451060/article/details/99614847?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control

    展开全文
  • 插件化简介 插件化概念 插件化开发就是将整个app拆分成很多模块,每个模块都是一个apk,最终打包的时候将宿主apk和插件apk分开打包,插件apk通过动态下发到宿主apk。 插件化优点 宿主和插件分开编译 可并发开发,都...
  • Android插件化方案实践

    千次阅读 2020-07-27 10:18:59
    插件化概述 1、插件化和组件化的区别 组件化是将一个app拆分为多个模块进行协作开发,每个模块都是一个单独的组件,这些组件可以相互依赖,也可以单独调试运行。但是最终发布的时候,这些组件会合并在一起,组成一...
  • GMTC全球移动技术大会ppt 作者: 包建强 主题: Android插件化介绍
  • Android插件化开发之(一)实现

    千次阅读 2021-12-13 13:43:04
    简单、明细的说明Android插件化开发的实现方式,帮助有需要者更深入的了解插件化开发,让大家的开发兼容性更强,代码质量更高!!!拿走,不谢。
  • android技术特别成熟了,热修复,组件化......等框架已经层出不穷,如果还仅限于使用框架上,技术永远很难...这一篇文章教大家手写出插件化框架,插件化技术是Android工程师必备的技术之一,我们要懂其思想,知其原理。
  • 一、Altas ( 阿里巴巴 )、 二、DynamicAPK ( 携程 )、 三、VirtualApp ( 罗迪 )、 四、DroidPlugin ( 360 )、 五、dynamic-load-apk ( 任玉刚 )、 六、Shadow ( 腾讯 )、 七、Phantom ( 满帮 )、 ...
  • Android插件化demo

    2016-07-21 17:46:06
    Android插件化demo
  • 四大组件的插件化插件化技术的核心知识点,而Activity插件化更是重中之重,Activity插件化主要有三种实现方式,分别是反射实现、接口实现和Hook技术实现。反射实现会对性能有所影响,主流的插件化框架没有采用此...
  • android 插件化例子demo下载地址
  • Android插件化开发Demo

    2016-02-16 18:04:00
    Android动态加载插件技术指的是在Android程序运行的过程中,使用java的反射,动态调用存放于应用程序目录下(SD卡即可)的APK文件,从而动态生成界面。
  • 360官方开源的Android插件化开发的例子,可以直接动态加载apk。带源码,带例子
  • Android插件化原理和开发实战
  • Android app bundle(aab) aab是谷歌提出的动态发布方案,是一种改进的应用程序打包方案,可以大幅度减少应用程序体积。 优点 size更小 安装更快 支持动态发布 限制 仅限于通过Google Play发布的应用 最低支持Android...
  • Android插件化主流框架和实现原理

    万次阅读 多人点赞 2020-03-02 23:03:07
    这几年移动开发业界兴起的「 插件化技术 」的旋风,各个大厂都推出了自己的插件化框架,各种开源框架都评价自身功能优越性,令人目不暇接。随着公司业务快速发展,项目增多,开发资源却有限,如何能在有限资源内满足...
  • 好的朋友们,新的一周开始了,让我们继续来学习插件化的知识吧。先回顾一下系列文章架构。 根据我的行文思路,本篇文章讲解资源和App打包的一些知识。算是插件化系列的第二篇基础文章。阅读完本文后,你应该会了解:...
  • 博客地址 http://blog.csdn.net/sbsujjbcy/article/details/47446733
  • Android插件化开发笔记

    2021-09-10 22:22:47
    Android插件化开发笔记,讲解插件化开发内容

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 98,269
精华内容 39,307
关键字:

android插件化

友情链接: AheadLib-x86-x64-master.zip