在上篇文章Android热修复与插件化之–ClassLoader(类加载器)详解和双亲委派模型以及如何自定义类加载器详细介绍了ClassLoader原理,为今天的插件化开发做了些铺垫;可能很多人没有接触过插件化,但是你的生活中肯定有用到过与插件化技术有关的应用,看下图
想必大家能看出来,图一是支付宝的应用界面,图二是微信的应用界面,这两个APP都集成了很多第三方应用,那你有没有想过它们是怎么集成这么多应用的呢?难道是将这么多应用的代码与主应用一起打包成APP?这想想感觉就不可能,这么多的代码和资源文件,这支付宝和微信的APK体积不得上天;难不成是需要用户手机里安装这些APP,然后跳转?这也是不对的,我们在使用的时候都是在主应用内跳转的;那它们到底是怎么做到的呢?这就涉及到今天所讲的插件化技术了
有插件一般都会伴随着宿主的存在,宿主APK实现了一套插件的加载和管理的框架,它作为应用的主工程存在,插件APK是依附于宿主APK存在的
插件也称为Plug-in,或者add-in,俗称外挂,是宿主应用的功能扩展,对宿主来说可有可无,但是一定程度上能提高宿主应用的高可用性;插件化就是在Android开发领域,在不改变宿主应用的情况下,通过插件动态扩展应用功能,在运行时将功能植入到应用系统中
正因如此,支付宝微信等这些大厂的应用才能集成如此之多的第三方应用
插件化技术听起来很美好,但是要注意的一个问题就是使用插件化技术的APP是不能在Google Play上线的,因为Google是禁止开发商这种行为的;为什么呢?防止开发商窃取用户隐私!想想你一个做闹钟的应用,通过Google应用商店审核用户安装后,通过插件化技术给自己的APP增加了额外的功能,这就有很大的风险了,接下来你想做什么Google就不能控制其风险了
再者说这种技术有点钻操作系统空子,与原生系统对着干的意思,但是在国内特殊环境下,却发展的挺火热的,有时还成为了一道技术面试的门槛,可能这就是XXX特色吧,为什么会这样呢?
这里在Andriod开发–如何实现组件化开发以及解决ButterKnife报错,了解一下文章的基础上继续开发;下面给出的插件化实现只是众多方案中的一种
场景介绍:首先用户安装了一个宿主APK,这里就仿微信了,在第四个tab页面,如下:
接着点击滴滴出行,这时候就需要去服务端下载滴滴APK到本地,然后加载对应页面
接下来就进行实战了,首先以一幅图来展示下代码实现逻辑
这里就把插件APK放在工程里一起开发了,主界面几个tab也是module,只不过是library形式
上图里说的很清楚了,加载一个APK需要加载dex文件,加载资源文件,所以就需要DexClassLoader,Resource,AssetManager等几个对象,所以就对封装一个APK加载对象,如下
public class PluginApk {
private PackageInfo mPackageInfo;
private DexClassLoader mClassLoader;
private Resources mResources;
private AssetManager mManager;
public PluginApk(PackageInfo mPackageInfo, DexClassLoader mClassLoader, Resources mResources) {
this.mPackageInfo = mPackageInfo;
this.mClassLoader = mClassLoader;
this.mResources = mResources;
if (mResources != null) {
mManager = mResources.getAssets();
}
}
}
接下来就需要创建PluginApk对象,定义一个插件管理类来实例化它
public class PluginManager {
private static class Instance{
static final PluginManager INSTANCE = new PluginManager();
}
private PluginManager() {
}
public static PluginManager getInstance(){
return Instance.INSTANCE;
}
private Context mContext;
private PluginApk mPluginApk;
public void init(Context context){
//避免单例对象引起内存泄漏
mContext = context.getApplicationContext();
}
/**
* 根据APK 路径实例化PluginApk对象
* @param path
*/
public void loadPluginApk(String path){
PackageInfo packageInfo = mContext.getPackageManager().getPackageArchiveInfo(path,
PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
if (packageInfo == null) {
return;
}
DexClassLoader classLoader = createDexClassLoader(path);
AssetManager assetManager;
try {
assetManager = createAssetManager(path);
} catch (Exception e) {
e.printStackTrace();
return;
}
Resources resources = createResource(assetManager);
mPluginApk = new PluginApk(packageInfo,classLoader,resources);
}
public PluginApk getPluginApk(){
return mPluginApk;
}
/**
* 创建访问插件APK dex文件的类加载器
* @param path
* @return
*/
private DexClassLoader createDexClassLoader(String path) {
/**
* 在宿主APK的内部存储中的data/data/包名 目录上创建一个文件夹,存放优化后的文件
*/
File file = mContext.getDir("odex",Context.MODE_PRIVATE);
return new DexClassLoader(path,file.getAbsolutePath(),null,mContext.getClassLoader());
}
private AssetManager createAssetManager(String path) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
/**
* AssetManager的构造方和addAssetPath方法都是hide的,需要使用反射构造
*/
AssetManager assetManager = AssetManager.class.newInstance();
Method method = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
method.invoke(assetManager,path);
return assetManager;
}
/**
* 创建访问插件APK资源的Resource
* @param assetManager
* @return
*/
private Resources createResource(AssetManager assetManager) {
Resources resources = mContext.getResources();
return new Resources(assetManager,resources.getDisplayMetrics(),resources.getConfiguration());
}
}
接下来就是加载Activity了,这里通过代理的形式加载插件Activity
public class ProxyActivity extends AppCompatActivity {
private String mActivityClassName;
private PluginApk mPluginApk;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mActivityClassName = getIntent().getStringExtra("classname");
mPluginApk = PluginManager.getInstance().getPluginApk();
lunchActivity();
}
private void lunchActivity() {
if (mPluginApk == null) {
throw new NullPointerException("未获取到插件APK");
}
DexClassLoader classLoader = mPluginApk.getmClassLoader();
try {
Class clazz = classLoader.loadClass(mActivityClassName);
//将Activity加载到内存中了
Object o = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
但是这样加载进来的Activity并没有生命周期,所以就需要先定义一套规则,这样插件里的Activity才能进行生命周期方法的回调
规则如下
public interface IPluginActivity {
//内部跳转
int FORM_INTERNAL = 1;
//外部跳转
int FROM_EXTERNAL = 2;
void attach(Activity proxyActivity);
void onCreate(Bundle savedInstanceState);
void onStart();
void onRestart();
void onResume();
void onPause();
void onStop();
void onDestroy();
void onActivityResult(int requestCode, int resultCode, Intent data)
}
这样ProxyActivity就可以这样写了
public class ProxyActivity extends AppCompatActivity {
private String mActivityClassName;
private PluginApk mPluginApk;
private IPluginActivity mIPluginActivity;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mActivityClassName = getIntent().getStringExtra("classname");
mPluginApk = PluginManager.getInstance().getPluginApk();
lunchActivity();
}
private void lunchActivity() {
if (mPluginApk == null) {
throw new NullPointerException("未获取到插件APK");
}
DexClassLoader classLoader = mPluginApk.getmClassLoader();
try {
Class clazz = classLoader.loadClass(mActivityClassName);
//将Activity加载到内存中了
Object o = clazz.newInstance();
//判断要跳转到的插件Activity是否实现了规则接口
if (o instanceof IPluginActivity) {
mIPluginActivity = (IPluginActivity) o;
//赋予插件Activity上下文信息
mIPluginActivity.attach(this);
Bundle bundle = new Bundle();
//表明是由宿主Activity跳转过去的
bundle.putInt("from",IPluginActivity.FROM_INTERNAL);
//回调插件Activity的onCreate方法,使其具有生命周期回调
mIPluginActivity.onCreate(bundle);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onStart() {
//回调插件Activity的onStart方法
mIPluginActivity.onStart();
super.onStart();
}
@Override
protected void onRestart() {
mIPluginActivity.onRestart();
super.onRestart();
}
@Override
protected void onResume() {
mIPluginActivity.onResume();
super.onResume();
}
@Override
protected void onPause() {
mIPluginActivity.onPause();
super.onPause();
}
@Override
protected void onStop() {
mIPluginActivity.onStop();
super.onStop();
}
@Override
protected void onDestroy() {
mIPluginActivity.onDestroy();
super.onDestroy();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
mIPluginActivity.onActivityResult(requestCode,resultCode,data);
super.onActivityResult(requestCode, resultCode, data);
}
/**
* 下面这三个对象当前Activity是不具备的
* 需要返回PluginApk对象的
* @return
*/
@Override
public Resources getResources() {
return mIPluginActivity == null ? super.getResources() : mPluginApk.getmResources();
}
@Override
public AssetManager getAssets() {
return mIPluginActivity == null ? super.getAssets() : mPluginApk.getmManager();
}
@Override
public ClassLoader getClassLoader() {
return mIPluginActivity == null ? super.getClassLoader() : mPluginApk.getmClassLoader();
}
}
接下来还需要编写一个插件Activity的父类,以处理跳转过来的是宿主APK中的Activity还是插件APK中的Activity
public class PluginActivityImpl extends AppCompatActivity implements IPluginActivity {
private int from = FROM_INTERNAL;
//赋予当前Activity上下文
private Activity mProxyActivity;
@Override
public void attach(Activity proxyActivity) {
mProxyActivity = proxyActivity;
}
@Override
public void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) {
from = savedInstanceState.getInt("from");
}
if (from == FROM_INTERNAL) {
super.onCreate(savedInstanceState);
mProxyActivity = this;
}
}
@Override
public void setContentView(int layoutResID) {
if (from == FROM_INTERNAL) {
super.setContentView(layoutResID);
} else {
mProxyActivity.setContentView(layoutResID);
}
}
@Override
public View findViewById(int id) {
if (from == FROM_INTERNAL) {
return super.findViewById(id);
} else {
return mProxyActivity.findViewById(id);
}
}
@Override
public void onStart() {
if (from == FROM_INTERNAL) {
super.onStart();
}
}
@Override
public void onRestart() {
if (from == FROM_INTERNAL) {
super.onRestart();
}
}
@Override
public void onResume() {
if (from == FROM_INTERNAL) {
super.onResume();
}
}
@Override
public void onPause() {
if (from == FROM_INTERNAL) {
super.onPause();
}
}
@Override
public void onStop() {
if (from == FROM_INTERNAL) {
super.onStop();
}
}
@Override
public void onDestroy() {
if (from == FROM_INTERNAL) {
super.onDestroy();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (from == FROM_INTERNAL) {
super.onActivityResult(requestCode,resultCode,data);
}
}
}
插件Activity需要继承PluginActivityImpl
package com.example.didi;
import android.os.Bundle;
import com.mango.library.plugin.PluginActivityImpl;
public class DidiActivity extends PluginActivityImpl {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
正常情况下是通过服务器下载需要执行的插件APK,我这里就从Assets目录获取apk文件模拟下载了
public static String copyAssetFile2APPCache(Context context,String fileName){
//获取应用内部存储中私有缓冲目录 data/data/包名/cache
File cacheDir = context.getCacheDir();
if (cacheDir.exists()) {
cacheDir.mkdirs();
}
try {
File outFile = new File(cacheDir,fileName);
if (outFile.exists()) {
return outFile.getAbsolutePath();
} else {
BufferedInputStream bis = new BufferedInputStream(context.getResources().getAssets().open(fileName));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(outFile));
int len;
byte[] buff = new byte[1024*8];
while ((len = bis.read(buff)) != -1) {
bos.write(buff,0,len);
}
bos.flush();
bis.close();
bos.close();
SharedPreferences sp = context.getSharedPreferences("APK",Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putBoolean("load",true);
editor.putString("path",outFile.getAbsolutePath());
editor.commit();
return outFile.getAbsolutePath();
}
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
主要是将插件apk文件拷贝到私有缓存目录去,当然了也可以直接将插件apk放在SD卡上
然后在点击滴滴出行的时候再调用该方法
@Override
public void onClick(View v) {
super.onClick(v);
SharedPreferences sp = getContext().getSharedPreferences("APK",Context.MODE_PRIVATE);
boolean load = sp.getBoolean("load",false);
if (load) {
String path = sp.getString("path","");
intentActivity(path);
} else {
//模拟下载过程
new LoadApk().execute();
}
}
class LoadApk extends AsyncTask{
@Override
protected String doInBackground(Void... voids) {
return FileUtil.copyAssetFile2APPCache(getContext(),"didi.apk");
}
@Override
protected void onPostExecute(String path) {
super.onPostExecute(path);
intentActivity(path);
}
}
private void intentActivity(String path){
Toast.makeText(getContext(),path,Toast.LENGTH_LONG).show();
if (TextUtils.isEmpty(path)) {
} else {
PluginManager.getInstance()
.init(getContext())
.loadPluginApk(path);//如果插件APK是放在SD卡上,那这里的path就是文件路径,也就不需要上面的拷贝操作了
Intent intent = new Intent(getContext(),ProxyActivity.class);
intent.putExtra("classname","com.example.didi.DidiActivity");
startActivity(intent);
}
}
这样就从宿主APK成功加载一个未安装的APK中的Activity了