一、Android插件化技术

我们在平时的开发过程中,会经常遇到产品需求的变更或者出现bug,在传统的模式中,我们需要首先需要修改代码,然后重新打包Apk,再交给公司的运营去官网或者应用商店上线,用户在打开应用的时候就会进行更新了。但是这种模式有几个缺点,一是上线周期长,从修改代码到用户更新需要较长的时间;二是用户更新代价较大,每次用户更新都需要下载整个Apk包,整个Apk包包括了一个应用的所有代码,要消耗用户较多的流量,并且,如果是一些重要的更新,为了确保用户都能更新到,还需要用到强制更新,即用户打开App后如果不更新应用则退出应用,这种对用户来说是极其不友好的。还有另外一种情况,某些较大的App功能很多,比如支付宝、微信等,如果将这些功能全部塞到一个Apk中,那将会是一个巨型Apk,用户在安装或者更新Apk时将会经过漫长的等待时间。基于以上两点,Android的插件化技术应运而生,插件化技术即将Apk按照功能模块划分,不同的功能打包成不同的Apk,然后应用的主Apk按需加载对应功能的Apk,用户只需要安装应用的主Apk即可,主Apk相当于一个壳,它会按需加载其他功能模块的Apk。通过这种模式,不仅解决了巨型Apk的问题,而且当某个功能模块需要变化时,也只需要修改对应功能的代码,打包功能Apk并更新即可,这样不仅可以让用户及时更新,而且更新的代价也很小。但是,我们知道,在Android中,没有安装的apk是不能直接运行的,那么要想实现插件化,我们就必须能够让主Apk能够加载功能Apk并运行。在这篇文章中,我们就一步步的分析如何实现Android的插件化。

二、需求分析

需求:实现在一个Apk中加载另外一个Apk并运行。

一般来说,Apk只有安装了才能够运行,Android在安装Apk时会解析Apk包,解析其中的AndroidManifest.xml文件,我们的四大组件(除了动态注册的广播)等都配置在其中,解析过程是由PackageManagerService完成的,PMS在解析Apk完成后,会将声明的四大组件的信息都注册在其中,并且应用在获取Apk资源时,也是需要通过PMS的协助完成,我们的应用可以通过上下文环境Context来和PMS打交道,对于一个没有安装过的Apk,其配置信息是没有在PMS中注册的,那么通过宿主Apk的上下文环境是无法去获取功能Apk中的四大组件以及资源等信息的,也就无法运行功能Apk。我们知道,PMS和AMS都运行在System Server进程中,我们的宿主Apk是运行在自己的应用进程中,不同的进程直接的数据是隔离的,我们无法在自己的应用进程中直接操作到运行在System Server进程中的PMS和AMS,两者之间的通信过程是通过Binder机制来完成的,也就是说,要想在宿主Apk中运行功能Apk的四大组件,就需要欺骗运行在System Server中的PMS和AMS,让其误以为我们的宿主Apk运行的是自己应用中的组件。基于以上思想,我们可以想到以下思路,首先来看一下针对Activity的处理:

1、在宿主Apk中注册一个代理的Activity,暂定为ProxyActivity;

2、当我们在宿主应用中要加载功能Apk时,首先要解析功能Apk,将其加载到对应的ClassLoader中;

3、启动ProxyActivity,因为ProxyActivity是在宿主Apk中注册过的,所有可以启动;

4、从功能Apk的ClassLoader中找到要功能Activity类;

5、通过反射创建功能Activity实例;

6、在ProxyActivity的所有生命周期回调函数中都调用功能Activity对应的回调函数,这样功能Activity要完成的功能就都在ProxyActivity中完成了。

可以看到,这是一种典型的代理模式,通过启动ProxyActivity来欺骗AMS,让AMS认为我们要启动的Activity是宿主Apk中的Activity,然而实际上ProxyActivity只是一个壳,它的主要作用是用来回调功能Activity的生命周期函数,这样就通过ProxyActivity完成了功能Activity的功能。前面说到过,没有安装过的Apk是没有在PMS中注册的,我们通过在宿主应用的Context是无法访问到功能Activity的资源的,这里我们暂时不讨论资源的加载问题,先只看如何通过ProxyActivity运行到功能Activity的代码。

如何动态加载Apk

首先,我们要知道如何将功能Apk加载到对应的ClassLoader中,关于动态加载apk,理论上可以用到的有DexClassLoader、PathClassLoader和URLClassLoader,看一下它们的对比:

DexClassLoader :可以加载文件系统上的jar、dex、apk

PathClassLoader :可以加载/data/app目录下的apk,这也意味着,它只能加载已经安装的apk

URLClassLoader :可以加载java中的jar,但是由于dalvik不能直接识别jar,所以此方法在android中无法使用,尽管还有这个类

关于jar、dex和apk,dex和apk是可以直接加载的,因为它们都是或者内部有dex文件,而原始的jar是不行的,必须转换成Android虚拟机所能识别的字节码文件,通过以上介绍,我们需要用DexClassLoader来加载功能Apk。

代码

首先我们在宿主应用中新建ProxyActivity,代码如下:

package com.liunian.androidbasic.dynamicload;import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;public class ProxyActivity extends Activity {public static final String PLUGIN_DEX_PATH = "plugin.dex.path";public static final String PLUGIN_ACTIIVTY_CLASS_NAME = "plugin.activity.class.name";Activity mPluginActivity = null;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);Intent intent = getIntent();if (intent != null) {// 从Intent中获得要启动的功能Apk的路径和Activity完整类名String pluginDexPath = intent.getStringExtra(PLUGIN_DEX_PATH);String pluginActivityClassName = intent.getStringExtra(PLUGIN_ACTIIVTY_CLASS_NAME);if (TextUtils.isEmpty(pluginDexPath) || TextUtils.isEmpty(pluginActivityClassName)) {return;}// 根据apk路径加载apk代码到DexClassLoader中File dexOutputDir = this.getDir("dex", 0);DexClassLoader dexClassLoader = new DexClassLoader(pluginDexPath,dexOutputDir.getAbsolutePath(), null, ClassLoader.getSystemClassLoader());if (dexClassLoader == null) {return;}// 从DexClassLoader中获得功能Activity Class对象并通过反射创建一个功能Activity实例Class pluginActivityClass = null;try {pluginActivityClass = dexClassLoader.loadClass(pluginActivityClassName);mPluginActivity = (Activity) pluginActivityClass.newInstance();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();}// 调用功能Activity的setProxyActivity方法,给其设置代理Activitytry {Method setProxyActivityMethod = pluginActivityClass.getDeclaredMethod("setProxyActivity", Activity.class);setProxyActivityMethod.invoke(mPluginActivity, this);} catch (NoSuchMethodException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}// 调用功能Activity实例的onCreate方法try {Method onCreateMethod = Activity.class.getDeclaredMethod("onCreate", Bundle.class);onCreateMethod.setAccessible(true);onCreateMethod.invoke(mPluginActivity, savedInstanceState);} catch (NoSuchMethodException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}
}

宿主应用中启动功能Apk中的Activity:

        Intent intent = new Intent();intent.putExtra(ProxyActivity.PLUGIN_DEX_PATH, "/sdcard/pluginmodule-debug.apk");intent.putExtra(ProxyActivity.PLUGIN_ACTIIVTY_CLASS_NAME, "com.alipay.pluginmodule.MainActivity");intent.setClass(MainActivity.this, ProxyActivity.class); // 其实启动的还是ProxyActivity

在Android studio中新建一个应用工程,MainActivity内容如下:

package com.alipay.pluginmodule;import android.app.Activity;
import android.os.Bundle;
import android.util.Log;public class MainActivity extends Activity {Activity mProxyActivity = null;public void setProxyActivity(Activity proxyActivity) {mProxyActivity = proxyActivity;}@Overrideprotected void onCreate(Bundle savedInstanceState) {Log.i("liunianprint:", "MainActivity onCreate");mProxyActivity.setContentView(R.layout.activity_main);}
}

编译功能Apk,名称为pluginmodule-debug.apk,将其push到手机的sdcard目录下,在宿主应用中启动它,发现"MainActivity onCreate"有打印:

02-14 15:18:33.944 22742-22742/com.alipay.pluginmodule I/liunianprint:: MainActivity onCreate

但是界面上确没有显示任何内容,我们再MainActivity中通过setContentView给页面设置的布局并没有显示,为什么会这样呢?原因是因为我们通过宿主应用的上下文环境无法加载到在功能Apk中的资源,所以setContentView中设置的布局也就无效了,那么我们让功能Activity显示内容呢?既然无法通过设置布局id的方式给功能Activity添加布局,我们何不尝试一下手动给功能Activity添加控件呢?

public class MainActivity extends Activity {Activity mProxyActivity = null;public void setProxyActivity(Activity proxyActivity) {mProxyActivity = proxyActivity;}@Overrideprotected void onCreate(Bundle savedInstanceState) {Log.i("liunianprint:", "MainActivity onCreate");mProxyActivity.setContentView(generateLayout()); // 给代理Activity设置手动生成的控件布局}private View generateLayout() {TextView textView = new TextView(mProxyActivity);textView.setText("我是Plugin的MainActivity");return textView;}
}

这个时候在从宿主Apk中启动功能Apk中的Activity,显示如下:

可以看到,成功的在宿主Apk中显示了功能Apk的内容,万里长征终于迈出了第一步,总结一下使用到的技术:

1、通过DexClassLoader动态的加载了功能Apk;

2、通过反射我们可以创建功能Activity的实例,并调用其函数;

3、通过注册代理Activity,然后在代理Activity的生命周期函数中调用功能Activity的生命周期函数,以达到让代理Activity实现功能Activity的效果;

4、功能Activity相当于只是一个代码块的封装,而代理Activity是实际启动的Activity,代理Activity可以看做一个壳,它会调用功能Activity的方法来完成功能Activity的效果。

加载Apk中的资源

在上面的代码中,我们通过动态加载技术成功的执行了功能Activity的代码,但是,却不能使用Apk中的资源,原因是因为通过宿主Apk的Context无法访问到功能Apk中的资源,那么,有没有什么办法可以让宿主Apk访问到功能Apk中的资源呢?下面我们就一起探寻一下解决办法。

Android的资源加载过程是一个复杂的过程,后面我们专门写一篇文章来讨论,这里,我们只探究一下如何通过Android系统提供的Api达到加载其他Apk中的资源的目的,首先,我们来看一下Activity的setContentView方法,看其是如何加载布局资源的:

    public void setContentView(@LayoutRes int layoutResID) {getWindow().setContentView(layoutResID); // 其实是将布局资源设置给了Activity的Window,Window的具体实现类为PhoneWindowinitWindowDecorActionBar();}

其实是将布局资源设置给了Activity的Window,Window的具体实现类为PhoneWindow,继续看PhoneWindow的setContentView方法:

  @Overridepublic void setContentView(int layoutResID) {...mLayoutInflater.inflate(layoutResID, mContentParent); // 调用了LayoutInflater的inflate方法解析layoutResID对应的布局文件,生成布局树,并将其添加到mContentParent上...}

调用了LayoutInflater的inflate方法解析layoutResID对应的布局文件,生成布局树,并将其添加到mContentParent上,mContentParent是PhoneWindow中的根View---DecorView下的一个子控件,这样我们就通过setContentView方法将布局添加到了DecorView的子控件中。我们再来看一下LayoutInflater的inflate方法:

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {final Resources res = getContext().getResources(); // 根据上下文环境获得Resources对象final XmlResourceParser parser = res.getLayout(resource); // 根据Resources对象获得对应的布局文件try {return inflate(parser, root, attachToRoot); // 解析布局文件并生成控件树,如有必要,还需要将其添加到root中} finally {parser.close();}}

可以看到,Android获得资源首先是根据上下文环境获得一个Resources对象,然后在根据这个Resources对象即可获得其中的资源。那么我们如何构造一个加载了未安装Apk的Resource对象呢?我们来看一下Resources类的构造函数:

    @Deprecatedpublic Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {this(null);mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());}

其中,AssetManager 表示资源管理器,Resources内部就是通过它来读取Apk中的资源的,DisplayMetrics表示屏幕分比率,Configuration表示设备配置。我们可以通过调用AssetManager的addAssetPath方法将Apk中的资源目录添加到AssetManager中管理,那么通过Resources去查找资源时,就会查找Apk中的资源。我们可以通过反射来构建一个AssetMananger对象,然后调用其addAssetPath方法将Apk中的资源目录添加到AssetManager中管理,再通过AssetMananger、DisplayMetrics以及Configuration来构建一个Resources对象,这样就可以访问到Resources中的资源了。代码如下:

public class ProxyActivity extends Activity {public static final String PLUGIN_DEX_PATH = "plugin.dex.path";public static final String PLUGIN_ACTIIVTY_CLASS_NAME = "plugin.activity.class.name";Activity mPluginActivity = null;Resources mPluginResourcs = null;String mPluginDexPath;String mPluginActivityClassName;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);Intent intent = getIntent();if (intent != null) {// 从Intent中获得要启动的功能Apk的路径和Activity完整类名mPluginDexPath = intent.getStringExtra(PLUGIN_DEX_PATH);mPluginActivityClassName = intent.getStringExtra(PLUGIN_ACTIIVTY_CLASS_NAME);if (TextUtils.isEmpty(mPluginDexPath) || TextUtils.isEmpty(mPluginActivityClassName)) {return;}loadApkResources(); // 加载资源// 根据apk路径加载apk代码到DexClassLoader中File dexOutputDir = this.getDir("dex", 0);DexClassLoader dexClassLoader = new DexClassLoader(mPluginDexPath,dexOutputDir.getAbsolutePath(), null, ClassLoader.getSystemClassLoader());if (dexClassLoader == null) {return;}// 从DexClassLoader中获得功能Activity Class对象并通过反射创建一个功能Activity实例Class pluginActivityClass = null;try {pluginActivityClass = dexClassLoader.loadClass(mPluginActivityClassName);mPluginActivity = (Activity) pluginActivityClass.newInstance();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();}// 调用功能Activity的setProxyActivity方法,给其设置代理Activitytry {Method setProxyActivityMethod = pluginActivityClass.getDeclaredMethod("setProxyActivity", Activity.class);setProxyActivityMethod.invoke(mPluginActivity, this);} catch (NoSuchMethodException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}// 调用功能Activity实例的onCreate方法try {Method onCreateMethod = Activity.class.getDeclaredMethod("onCreate", Bundle.class);onCreateMethod.setAccessible(true);onCreateMethod.invoke(mPluginActivity, savedInstanceState);} catch (NoSuchMethodException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}// 加载插件Apk的资源private void loadApkResources() {try {AssetManager assetManager = AssetManager.class.newInstance(); // 通过反射创建一个AssetManager对象Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); // 获得AssetManager对象的addAssetPath方法addAssetPathMethod.invoke(assetManager, mPluginDexPath); // 调用AssetManager的addAssetPath方法,将apk的资源添加到AssetManager中管理mPluginResourcs = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration()); // 根据AssetMananger创建一个Resources对象} catch (InstantiationException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}// 重写ProxyActivity的getResources方法,让其返回插件Apk的资源对象@Overridepublic Resources getResources() {if (mPluginResourcs != null) {return mPluginResourcs;}return super.getResources();}
}

同时,修改插件Apk的代码:

public class MainActivity extends Activity {Activity mProxyActivity = null;public void setProxyActivity(Activity proxyActivity) {mProxyActivity = proxyActivity;}@Overrideprotected void onCreate(Bundle savedInstanceState) {Log.i("liunianprint:", "MainActivity onCreate");mProxyActivity.setContentView(R.layout.activity_main);}}

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="I am plugin's MainActivity!"android:layout_centerInParent="true"/></RelativeLayout>

重新打包插件Apk和宿主Apk,运行程序:

哈哈,我们成功的显示了插件Apk中的布局文件,总结一下使用到的技术:

1、通过反射构造一个资源管理AssetManager对象;

2、通过反射调用AssetManager的addAssetPath方法,将插件Apk中的资源添加到AssetManager对象中管理;

3、通过AssetManager对象、屏幕分辨率以及设置配置信息构造一个Resources对象;

4、重写ProxyActivity的getResources方法,让其返回可以访问插件Apk资源的Resources对象。

三、总结

这篇文章通过代理的方式实现简单的插件化,插件化主要要做到三件事:

1、加载未安装的Apk中的代码;

2、加载未安装的Apk中的资源;

3、欺骗AMS,绕过检测。

其中欺骗AMS,现在一般是使用Hook的方式实现的,Hook的方式需要我们对Activity的启动流程比较了解,可以Hook Ams的代理或者Hook Instrumentation,即欺骗AMS要启动的Activity是已经注册过的Activity,绕过检测,但是在创建Activity时在将其替换成插件中的Activity,这种方式我们在后面专门写一篇文章分析。

Android插件化技术相关推荐

  1. 《Android插件化技术——原理篇》

    | 导语 插件化技术最早从2012年诞生至今,已经走过了5个年头.从最初只支持Activity的动态加载发展到可以完全模拟app运行时的沙箱系统,各种开源项目层出不穷,在此挑选了几个代表性的框架,总结 ...

  2. Android插件化技术调研

    一.技术背景 Android的插件化技术,目前已经比较成熟,微信.淘宝.携程.360手机助手中都应用到了插件化.插件化技术的特点是无需单独安装apk,即可运行,即插即用,无需升级宿主应用,减少app的 ...

  3. class加载原理和Dex加载的原理-----android插件化技术

    2019独角兽企业重金招聘Python工程师标准>>> class加载原理和Dex加载的原理 转载于:https://my.oschina.net/quguangle/blog/15 ...

  4. Android插件化开发指南——插件化技术简介

    文章目录 1. 为什么需要插件化技术 2. 插件化技术的历史 3. 插件化实现思路 3.1 InfoQ:您在 GMTC 中的议题叫做<Android 插件化:从入门到放弃>,请问这个标题代 ...

  5. Android 插件化总结

    2019独角兽企业重金招聘Python工程师标准>>> 1.Android中插件开发篇总结和概述 2.Android组件化和插件化开发 3.携程Android App插件化和动态加载 ...

  6. VirtualAPK:滴滴 Android 插件化的实践之路

    一.前言 在 Android 插件化技术日新月异的今天,开发并落地一款插件化框架到底是简单还是困难,这个问题不同人会有不同的答案.但是我相信,完成一个插件化框架的 Demo 并不是多难的事儿,然而要开 ...

  7. Android插件化原理解析——概要

    2015年是Android插件化技术突飞猛进的一年,随着业务的发展各大厂商都碰到了Android Native平台的瓶颈: 从技术上讲,业务逻辑的复杂导致代码量急剧膨胀,各大厂商陆续出到65535方法 ...

  8. Android插件化开发之动态加载三个关键问题详解

    本文摘选自任玉刚著<Android开发艺术探索>,介绍了Android插件化技术的原理和三个关键问题,并给出了作者自己发起的开源插件化框架. 动态加载技术(也叫插件化技术)在技术驱动型的公 ...

  9. Android插件化:从入门到放弃

    喜欢 | 作者 包建强 发布于 2016年7月14日. 估计阅读时间: 1 分钟 | 道AI风控.Serverless架构.EB级存储引擎,尽在ArchSummit!讨论 分享到:微博微信Facebo ...

最新文章

  1. 批量修改图片以及加水印
  2. 360数科发布2020全年财报:全年收入上涨47.1%,科技为运营效率提供第一动力
  3. 【FluidSynth】FluidSynth 简介 ( 相关资源链接 | 简介 | 特征 )
  4. ORA-29275:部分多字节字符
  5. 【手把手教你树莓派3 (二)】 启动wifi模块
  6. Win环境安装VMware Server 2.0手记
  7. SAP License:FI-CO集成
  8. java swing复选框大小_Java Swing界面编程(28)---复选框:JCheckBox
  9. 网站锁定php文件命令,PHP文件的锁定机制
  10. SQL Server 连接查询(内连接查询)
  11. Java基础知识强化21:Java中length、length()、size()区别
  12. 分享400多道算法题,来挑战吧
  13. java 各种数据结构,几种常见的数据结构的JAVA实现
  14. 使用HTML+CSS实现图片滚动效果
  15. 极速手游加速SDK快速指入指南+极速手游加速器SDK参考文档
  16. 为什么表格后面打不出0_用excel制作表格为什么打不出数字?
  17. 贩卖个人信息非法获利300余万,平安惠普、拍拍贷都是买方
  18. 多路耦合器(有源分离器)在无线通讯中的应用
  19. Android的MVP架构
  20. python工商银行流水_python爬取银行名称和官网地址

热门文章

  1. 连续型随机变量及其概率密度
  2. Shell中单引号和双引号区别
  3. GVINS编译出现大量的error: ‘CV_CALIB_CB_ADAPTIVE_THRESH’ was not declared in this scope错误
  4. 《肉瘤公公》——太宰治
  5. 微擎(微赞)初入坑一(转自a_haoGG的博客)
  6. 基于深度学习的目标检测DET - SSD
  7. android 百度地图拖动定位,百度地图获取定位,实现拖动marker定位,返回具体的位置名...
  8. 关于火星探测器的试题
  9. c-plus day2
  10. 笔记本硬盘故障与简单维修