前言

正常的App开发流程基本上是这样的:开发功能-->测试--->上线,上线后发现有大bug,紧急修复---->发新版本---->用户更新----->bug修复。从发现bug到修复bug花了很长时间。我们希望bug的修复是立马生效的,用户无感知就能自动修复bug。当然,Android端的动态修复bug已经有不少框架了,不过我们今天讲的是另一个话题:Android的插件化。Android插件化有很多好处:热插拔、静默升级、bug动态修复、代码解耦等。正是因为如此,才有越来越多的公司选择插件化。

分析

Android插件化有很多的开源框架,基本上都是两种思路:代理和替换系统的一些关键变量,通过替换这些变量,达到欺骗系统的目的(又称Hook)。代理的思路比较简单,就是通过代理类Proxy,把主工程和插件工程的组件连接起来。代理类相当于傀儡,当主工程想要启动插件工程时,实际上会先调用代理类的相应方法,然后再通过代理类调用插件工程的组件,间接达到调用插件工程组件的目的。为何不直接调用插件工程的组件呢?因为通过DexClassLoader加载到内存的Activity等组件只是一个普通的类,没有上下文环境,意味着拿不到Context,意味着没有生命周期。

让Activity有"生命"

得益于Java语言的类加载器可以动态加载类的特性,在Android中加载一个普通的类是很容易的

DexClassLoader
Class<?> mClassLaunchActivity = (Class<?>)
classLoader.loadClass(mLaunchActivity);
mPluginActivity = (IPluginActivity)
mClassLaunchActivity.newInstance();

通过ClassLoader可以加载一个类,并生成类的实例。但是这个mPluginActivity只是一个普通的类,并没有Activity的生命周期,所以我们需要借助PluginProxyActivity来完成代理工作


public class PluginProxyActivity extends Activity {
IPluginActivity mPluginActivity;
String mPluginApkFilePath;
String mLaunchActivity;
private String mPluginName; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle bundle = getIntent().getExtras();
if(bundle == null){
return;
}
mPluginName = bundle.getString(PluginUtils.PARAM_PLUGIN_NAME);
mLaunchActivity = bundle.getString(PluginUtils.PARAM_LAUNCH_ACTIVITY);
File pluginFile = PluginUtils.getInstallPath(PluginProxyActivity.this, mPluginName);
if(!pluginFile.exists()){
return;
}
mPluginApkFilePath = pluginFile.getAbsolutePath();
try {
initPlugin();
mPluginActivity.IOnCreate(savedInstanceState);
} catch (Exception e) {
mPluginActivity = null;
e.printStackTrace();
}
} @Override
protected void onResume() {
super.onResume();
if(mPluginActivity != null){
mPluginActivity.IOnResume();
}
} @Override
protected void onStart() {
super.onStart();
if(mPluginActivity != null) {
mPluginActivity.IOnStart();
}
}
........ //省略部分代码
private void initPlugin() throws Exception {
PackageInfo packageInfo;
try {
PackageManager pm = getPackageManager();
packageInfo = pm.getPackageArchiveInfo(mPluginApkFilePath, PackageManager.GET_ACTIVITIES);
} catch (Exception e) {
throw e;
} ClassLoader classLoader = PluginUtils.getOrCreateClassLoaderByPath(this, mPluginName, mPluginApkFilePath);
// get default launchActivity if target Activity is null
if (mLaunchActivity == null || mLaunchActivity.length() == 0) {
if (packageInfo == null || (packageInfo.activities == null) || (packageInfo.activities.length == 0)) {
throw new ClassNotFoundException("Launch Activity not found");
}
mLaunchActivity = packageInfo.activities[0].name;
}
Class<?> mClassLaunchActivity = (Class<?>) classLoader.loadClass(mLaunchActivity); mPluginActivity = (IPluginActivity) mClassLaunchActivity.newInstance();
mPluginActivity.IInit(mPluginApkFilePath, this, classLoader, packageInfo);
} protected Class<? extends PluginProxyActivity> getProxyActivity(String pluginActivityName) {
return getClass();
} @Override
public void startActivityForResult(Intent intent, int requestCode) {
boolean pluginActivity = intent.getBooleanExtra(PluginUtils.PARAM_IS_IN_PLUGIN, false);
if (pluginActivity) {
String launchActivity = null;
ComponentName componentName = intent.getComponent();
if(null != componentName) {
launchActivity = componentName.getClassName();
}
intent.putExtra(PluginUtils.PARAM_IS_IN_PLUGIN, false);
if (launchActivity != null && launchActivity.length() > 0) {
Intent pluginIntent = new Intent(this, getProxyActivity(launchActivity));
pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_NAME, mPluginName);
pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_PATH, mPluginApkFilePath);
pluginIntent.putExtra(PluginUtils.PARAM_LAUNCH_ACTIVITY, launchActivity);
startActivityForResult(pluginIntent, requestCode);
}
} else {
super.startActivityForResult(intent, requestCode);
}
}
}

每次启动新的Activity的时候,都会调用startActivityForResult,在此方法中,进行了Intent的替换,启动的新Activity还是会跳到ProxyActivity中。

 Intent pluginIntent = new Intent(this, getProxyActivity(launchActivity));
pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_NAME, mPluginName);
pluginIntent.putExtra(PluginUtils.PARAM_PLUGIN_PATH, mPluginApkFilePath);
pluginIntent.putExtra(PluginUtils.PARAM_LAUNCH_ACTIVITY, launchActivity);
startActivityForResult(pluginIntent, requestCode);

所有的插件工程都需要继承于BasePluginActivity,其主要代码如下


public class BasePluginActivity extends Activity implements IPluginActivity { private boolean mIsRunInPlugin;
private ClassLoader mDexClassLoader;
private Activity mOutActivity;
private String mApkFilePath;
private PackageInfo mPackageInfo;
private PluginContext mContext;
private View mContentView;
private Activity mActivity;
private boolean mFinished; @Override
protected void onCreate(Bundle savedInstanceState) {
if (mIsRunInPlugin) {
mActivity = mOutActivity;
} else {
super.onCreate(savedInstanceState);
mActivity = this;
}
} @Override
public void setContentView(int layoutResID) {
if (mIsRunInPlugin) {
mContentView = LayoutInflater.from(mContext).inflate(layoutResID, null);
mActivity.setContentView(mContentView);
} else {
super.setContentView(layoutResID);
}
} @Override
public void setContentView(View view) {
if (mIsRunInPlugin) {
mContentView = view;
mActivity.setContentView(mContentView);
} else {
super.setContentView(view);
}
} @Override
public View findViewById(int id) {
if (mIsRunInPlugin && mContentView != null) {
View v = mContentView.findViewById(id);
if (null == v) {
v = super.findViewById(id);
}
return v;
} else {
return super.findViewById(id);
}
} @Override
public void IOnCreate(Bundle savedInstanceState) {
onCreate(savedInstanceState);
} @Override
public void IOnResume() {
onResume();
} @Override
public void IOnStart() {
onStart();
} @Override
public void IOnPause() {
onPause();
} @Override
public void IOnStop() {
onStop();
} @Override
public void IOnDestroy() {
onDestroy();
} @Override
public void IOnRestart() {
onRestart();
} @Override
public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo) {
mIsRunInPlugin = true;
mDexClassLoader = classLoader;
mOutActivity = context;
mApkFilePath = path;
mPackageInfo = packageInfo; mContext = new PluginContext(context, 0, mApkFilePath, mDexClassLoader);
attachBaseContext(mContext);
} @Override
protected void onResume() {
if (mIsRunInPlugin) {
return;
}
super.onResume();
} @Override
protected void onPause() {
if (mIsRunInPlugin) {
return;
}
super.onPause(); } @Override
protected void onStart() {
if (mIsRunInPlugin) {
return;
}
super.onStart();
} @Override
protected void onRestart() {
if (mIsRunInPlugin) {
return;
}
super.onRestart();
} @Override
protected void onStop() {
if (mIsRunInPlugin) {
return;
}
super.onStop();
} @Override
protected void onDestroy() {
if (mIsRunInPlugin) {
mDexClassLoader = null;
return;
}
super.onDestroy();
} @Override
public void finish() {
if (mIsRunInPlugin) {
int resultCode = Activity.RESULT_CANCELED;
Intent data = null;
synchronized (this) {
Field field;
try {
field = Activity.class.getDeclaredField("mResultCode");
field.setAccessible(true);
resultCode = (Integer) field.get(this);
field = Activity.class.getDeclaredField("mResultData");
field.setAccessible(true);
data = (Intent) field.get(this);
} catch (Exception e) {
}
}
mOutActivity.setResult(resultCode, data);
mOutActivity.finish();
mFinished = true;
} else {
super.finish();
}
} @Override
public boolean isFinishing() {
if (mIsRunInPlugin) {
return mFinished;
} else {
return super.isFinishing();
}
} @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (mIsRunInPlugin) {
return;
} else {
super.onActivityResult(requestCode, resultCode, data);
}
} @Override
public LayoutInflater getLayoutInflater() {
if (mContext != null) {
return LayoutInflater.from(mContext);
} else {
return LayoutInflater.from(mActivity);
}
} @Override
public WindowManager getWindowManager() {
if (mIsRunInPlugin) {
return mOutActivity.getWindowManager();
} else {
return super.getWindowManager();
}
} @Override
public void startActivityForResult(Intent intent, int requestCode) {
if (mIsRunInPlugin) {
intent.putExtra(PluginUtils.PARAM_IS_IN_PLUGIN, true);
mActivity.startActivityForResult(intent, requestCode);
} else {
super.startActivityForResult(intent, requestCode);
}
}
}

在BasePluginActivity中,覆写了很多父类Activity的方法,用来判断当前Activity是独立运行还是作为插件运行,如果是在插件中运行,则是调用插件中设置进来的代理类ProxyActivity(mActivity)的相应方法。同时使用IPluginActivity来模拟Activity的生命周期

public interface IPluginActivity {
public void IOnCreate(Bundle savedInstanceState); public void IOnResume(); public void IOnStart(); public void IOnPause(); public void IOnStop(); public void IOnDestroy(); public void IOnRestart(); public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo);
}

至此Activity已经有生命周期了!但是还有个问题,插件的资源如何获取?

插件资源的获取

这里就不卖关子了。直接通过反射调用AssetManager的addAssetPath方法把资源加载到Resource对象中,即可获取插件中的资源。为了后续方便,我们直接继承ContextWrapper类自己实现getAssets和getResources方法即可。代码如下

class PluginContext extends ContextWrapper {

    private AssetManager mAsset;
private Resources mResources;
private Theme mTheme;
private int mThemeResId;
private ClassLoader mClassLoader;
private Context mOutContext; private AssetManager getSelfAssets(String apkPath) {
AssetManager instance = null;
try {
instance = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(instance, apkPath);
} catch (Throwable e) {
e.printStackTrace();
}
return instance;
} private Resources getSelfRes(Context ctx, AssetManager selfAsset) {
DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();
Configuration con = ctx.getResources().getConfiguration();
return new Resources(selfAsset, metrics, con);
} private Theme getSelfTheme(Resources selfResources) {
Theme theme = selfResources.newTheme();
mThemeResId = getInnerRIdValue("com.android.internal.R.style.Theme");
theme.applyStyle(mThemeResId, true);
return theme;
} private int getInnerRIdValue(String rStrnig) {
int value = -1;
try {
int rindex = rStrnig.indexOf(".R.");
String Rpath = rStrnig.substring(0, rindex + 2);
int fieldIndex = rStrnig.lastIndexOf(".");
String fieldName = rStrnig.substring(fieldIndex + 1, rStrnig.length());
rStrnig = rStrnig.substring(0, fieldIndex);
String type = rStrnig.substring(rStrnig.lastIndexOf(".") + 1, rStrnig.length());
String className = Rpath + "$" + type; Class<?> cls = Class.forName(className);
value = cls.getDeclaredField(fieldName).getInt(null); } catch (Throwable e) {
e.printStackTrace();
}
return value;
} public PluginContext(Context base, int themeres, String apkPath, ClassLoader classLoader) {
super(base, themeres);
mClassLoader = classLoader;
mAsset = getSelfAssets(apkPath);
mResources = getSelfRes(base, mAsset);
mTheme = getSelfTheme(mResources);
mOutContext = base;
} @Override
public Resources getResources() {
return mResources;
} @Override
public AssetManager getAssets() {
return mAsset;
} @Override
public Theme getTheme() {
return mTheme;
}
}

至此,插件化的两大难题已经解决。现在插件中的Activity可以启动了!用同样的思路可以完成其他组件的代码编写。

源码下载

PluginDemo

延伸阅读

资源加载和activity生命周期管理

基于Proxy思想的Android插件框架

Android基于代理的插件化思路分析的更多相关文章

  1. android 基于dex的插件化开发

    安卓里边可以用DexClassLoader实现动态加载dex文件,通过访问dex文件访问dex中封装的方法,如果dex文件本身还调用了native方法,也就间接实现了runtime调用native方法 ...

  2. 基于AOP的插件化(扩展)方案

    在项目迭代开发中经常会遇到对已有功能的改造需求,尽管我们可能已经预留了扩展点,并且尝试通过接口或扩展类完成此类任务.可是,仍然有很多难以预料的场景无法通过上述方式解决.修改原有代码当然能够做到,但是这 ...

  3. Android架构设计之插件化、组件化

    如今移动app市场已经是百花齐放,其中有不乏有很多大型公司.巨型公司都是通过app创业发展起来的:app类型更加丰富,有电子商务.有视频.有社交.有工具等等,基本上涵盖了各行各业每个角落,为了更加具有 ...

  4. 基于Fragment的插件化

    --<摘自android插件化开发指南> 1.有些项目,整个app只有一个Activity,切换页面全靠Fragment,盛行过一时,但有点极端 2.Activity切换fragment页 ...

  5. Android 模块化/热修复/插件化 框架选用

    概念汇总 动态加载:在程序运行的时候,加载一些程序自身原本不存在的文件并运行这些文件里的代码逻辑.动态加载是热修复与插件化实现的基础. 热修复:修改部分代码,不用重新发包,在用户不知情的情况下,给ap ...

  6. Android热修复、插件化、组件化

    模块化:项目按照独立的模块进行划分 组件化:将项目按照单一的组件来进行划分结构 项目组件化的重要环节在于,将项目按照模块来进行拆分,拆分成一个个业务module和其他支撑module(lib),各个业 ...

  7. Android 插件化和热修复知识梳理

    概述 在Android开发中,插件化和热修复的话题越来越多的被大家提及,同时随着技术的迭代,各种框架的发展更新,插件化和热修复的框架似乎已经日趋成熟,许多开发者也把这两项技术运用到实际开发协作和正式的 ...

  8. 《Android插件化开发指南》面世

    本书在京东购买地址:https://item.jd.com/31178047689.html 本书Q群:389329264 (一)这是一本什么书 如果只把本书当作纯粹介绍Android插件化技术的书籍 ...

  9. 又一开源项目爆火于GitHub,Android高级插件化强化实战

    一.插件化起源 插件化技术最初源于免安装运行 Apk的想法,这个免安装的 Apk 就可以理解为插件,而支持插件的 app 我们一般叫 宿主. 想必大家都知道,在 Android 系统中,应用是以 Ap ...

随机推荐

  1. KVC 开发详情

    目录 概述 KVC基本技术 KVC访问函数 KVC搜索顺序 KVC集合操作 一.概述 kvc全名是Key-value coding,kvc是一种通过字符串间接的访问oc对象的属性的一种技术. 一个oc ...

  2. 基于HTTP协议的轻量级开源简单队列服务:HTTPSQS[转]

    HTTPSQS(HTTP Simple Queue Service)是一款基于 HTTP GET/POST 协议的轻量级开源简单消息队列服务,使用 Tokyo Cabinet 的 B+Tree Key ...

  3. CSS绘制三角形的原理剖析

    今天学习Bootstrap时候,看到按钮的向下三角形源码: .caret { display: inline-block; ; ; margin-left: 2px; vertical-align: ...

  4. [NC189A]数字权重

    题目大意:有一个$n$位的数,设第$i$位为$a_i$(最高位为$a_1$).问满足$(\sum\limits_{i=2}^n(a_i-a_{i-1}))==k$的数的个数(不含前导零) 题解:发现$ ...

  5. 【CZY选讲·棋盘迷宫】

    题目描述 一个N*M的棋盘,’.’表示可以通过,’#’表示不能通过,给出Q个询问,给定起点和终点,判断两点是否联通,如联通输出“Yes”,否则输出“No”. 数据范围 N,M <=500,Q ...

  6. typedef函数用法

    转载自:http://www.cnblogs.com/ggjucheng/archive/2011/12/27/2303238.html 引言 typedef 声明,简称 typedef,为现有类型创 ...

  7. saltstack 实现haproxy+keepalived

    1.目录结构规划如下 mkdir -p /srv/salt/prod/haproxy mkdir -p /srv/salt/prod/keepalived mkdir -p /srv/salt/pro ...

  8. elementui 日期选择器设置当前默认日期(picker-options),以及当前日期以后的无法选择(default-value)

    目前官方的日期默认是当前日期,打开之后长这样子:现在是三月13日,但是有的需求是当前日期在后面. 就像这样: 代码如下: default-value是设置当前日期默认值的."timeDefa ...

  9. 洛谷noip 模拟赛 day1 T3

    T7983 大芳的逆行板载 题目背景 大芳有一个不太好的习惯:在车里养青蛙.青蛙在一个n厘米(11n毫米s)的Van♂杆子上跳来跳去.她时常盯着青蛙看,以至于突然逆行不得不开始躲交叉弹.有一天他突发奇 ...

  10. SpringBoot程序启动时执行初始化代码

    因项目集成了Redis缓存部分数据,需要在程序启动时将数据加载到Redis中,即初始化数据到Redis. 在SpringBoot项目下,即在容器初始化完毕后执行我们自己的初始化代码. 第一步:创建实现 ...