前言

正常的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. 五、vue nextTick

    主线程的执行过程就是一个 tick,而所有的异步结果都是通过 "任务队列" 来调度被调度. 消息队列中存放的是一个个的任务(task). 规范中规定 task 分为两大类,分别是 ...

  2. [codeforces] 578C Weakness and Poorness || 三分

    原题 题目定义了两个变量: poorness表示一个区间内和的绝对值. weakness表示一个所有区间最大的poornesss 题目要求你求一个x使得 a1 − x, a2 − x, ..., an ...

  3. hust 1605 bfs

    思路:直接用优先队列优化bfs. #include<map> #include<queue> #include<vector> #include<cmath& ...

  4. macOS Sierra下如何打开任何来源(10.12系统)

    转载声明:本站文章无特别说明皆为原创,转载请注明:史蒂芬周的博客, 一定有很多朋友和小子一样,迫不及待的升级到了macOS Sierra,随之而来的是第三方应用都无法打开了,提示无法打开或者扔进废纸篓 ...

  5. Oracle 对字符串去重函数

    CREATE OR REPLACE FUNCTION ZZMES."REMOVESAMESTR" (oldStr varchar2, sign varchar2) return v ...

  6. 移动端web开发 浅析

    1. viewport ① viewport在移动端承载网页的区域:具有默认格式 ②设置viewport属性,适配移动端设备 主流设置: <meta name = ”viewport” cont ...

  7. Windows下MySQL安装配置与使用

    1.下载. 下载地址: http://downloads.mysql.com/archives/get/file/mysql-5.7.11-winx64.zip. NavicatforMySQL:ht ...

  8. 汕头市队赛 SRM13 T3

    这道题可以贪心 维护一个答案队列 枚举位置 每次将比当前位置大的队尾全部替代掉 记录删了多少了就好了 #include<cstdio> #include<iostream> # ...

  9. Hibernate中用注解配置一对多双向关联和多对一单向关联

    Hibernate中用注解配置一对多双向关联和多对一单向关联 Hibernate提供了Hibernate Annotations扩展包,使用注解完成映射.在Hibernate3.3之前,需单独下载注解 ...

  10. 插件 原生js 省市区 三级联动 源码

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...