最近一直在学习Android的插件化开发,看了很多大神的博客,醍醐灌顶。但是纸上得来终觉浅,自己便寻思做了个demo,加深学习理解。在实现的过程中,也发现了很多问题,因此写下这篇博客,记录这个学习过程。
背景
当项目越来越大的时候,需要通过插件化来减轻应用的内存和CPU占用;
可以实现热插拔,在不发布新版本的情况下更新某些模块。
插件化的有几种模式:简单的加载、使用代理、根据apk动态创建Activity,社区也有很多开源的插件化库,大家可以去找一找。
本文浅谈了使用Activity代理实现插件化。实现步骤
在宿主app中可以加载手机上已安装和未安装的apk。若加载已安装的apk,就与使用插件更新app的思想相悖了,因此研究如何加载手机上未安装的apk;
将插件apk放在手机sdcard中,通过相关api即可在宿主app中获取插件apk;
获取插件apk中应用的包结构,提取包信息;
使用DexClassLoader,加载插件apk中指定的插件Activity;
获取apk包中的Resources资源,在宿主apk中使用;
使用反射的方式得到插件Activity的实例,并将其与代理Activity关联;
以接口方式管理插件Activity的生命周期。待实现点
1 内存存储和外存存储
●内部存储;
内部存储不是内存,它位于系统中很特殊的一个位置,默认只能被你的应用访问。应用安装、运行后所创建的文件存在于内部存储中以应用包名为名的目录下。
当应用卸载后,其对应的内部存储文件也随之删除。
内部存储也是系统本身和系统应用程序主要的数据存储所在地。
●外部存储;
外部存储,即手机可移动的sdcard或手机自带的固定的sdcard,且都是用相同的api访问得到其根目录。
外部存储的public文件是可以被自由访问的,且应用删除后,仍保留;外部存储的private文件由于位于外部存储中,也可被访问,但是其对于其他应用没有访问价值。应用删除后,private文件也删除。
可以使用getExternalStoragePublicDirectory()来获取外部存储的根目录。
2 获取apk中的包信息
/*
*params
* archiveFilePath: 未安装的apk所在位置,可通过
* getExternalStoragePublicDirectory()+File.separator+fileName获取;
*return : 包含包信息的PackageInfo对象
*/
private PackageInfo getUninstalledApkInfo(String archiveFilePath){
PackageManager pm=mSelf.getPackageManager();
PackageInfo pkgInfo=pm.getPackageArchiveInfo(archiveFilePath,PackageManager.GET_ACTIVITIES);
return pkgInfo;
}
上述的mSelf,即代表宿主app的Context对象。
3 加载插件Activity
File optimizedDirectoryFile=mSelf.getDir("dex", Context.MODE_PRIVATE);
DexClassLoader dexClassLoader=new DexClassLoader(mApkPath,optimizedDirectoryFile.toString(),null,mSelf.getClassLoader());
try {
mPluginClass=dexClassLoader.loadClass(packageName+".PluginActivity");
} catch (ClassNotFoundException e) {
Log.d(TAG, "loadApk: loading class errors.");
e.printStackTrace();
return false;
}
DexClassLoader中要使用的数据必须位于内部存储中。因此,创建DexClassLoader时会将外部获得的数据拷贝、优化,存放在内部存储的指定目录中。再从其中获取数据创建类加载器。
Context.getDir()会在内部存储的根目录下创建指定文件夹;
第一个参数为待读取apk路径;
第二个参数为内部存储下,数据的优化存储路径;
第三个参数为原生库的搜索路径,一般为null;
第四个参数指定了父类加载器。
获得DexClassLoader后就可以通过loadClass方法,加载指定的插件Activity。4 获得apk包中的Resources资源,并使用
private void loadResources(){
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath" ,
String.class);
addAssetPath.invoke(assetManager,mApkPath);
mAssetManager=assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes=super.getResources();
mResources=new
Resources(mAssetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
}
通过反射,调用AssetManager的addAssetPath方法,将apk资源加入至AssetManager中。
最后通过AssetManager和父类获得的Resources创建Resources对象。
注意,此时得到的mResources对象包含了插件apk中的资源。宿主app中的资源仍在宿主的Resources对象中,二者没有混淆。
宿主app的资源可通过Context.getResources()来获得。
5 覆写getResources(),让代理Activity使用插件中的资源
@Override
public Resources getResources() {
return mResources==null ? super.getResources():mResources;
}
代理Activity只是一个空壳。它的作用是载入插件Activity,管理插件Activity,并让插件Activity发挥功能。
因此,如果插件能够成功导入,且资源能够成功获取,那么mResources(插件的资源)就不应该为null。代理Activity就需要使用插件Activity里的资源让其发挥效用。
若插件无法成功导入或资源不能成功获取,则应该手动将mResources置为null。让Activity使用宿主app中的资源。
6 获取插件Activity实例,并与宿主app关联
private boolean initPlugin(){
try {
//创建插件对象
Constructor constructor=mPluginClass.getConstructor(new Class[]{});
Object obj=constructor.newInstance(new Object[]{});
mPluginActivity=(Attachable) obj;
mPluginActivity.attach(mSelf);
mPluginActivity.onCreate(null);
} catch (Exception e) {
e.printStackTrace();
mResources=null;
return false;
}
return true;
}
根据反射的方式,通过获取的Class对象获取相关的构造器,并创建插件Activity对象。
获得插件Activity后,将其转化为Attachable接口对象,并调用attach方法,将它与宿主代理Activity关联。
调用插件Activity的onCreate方法,在代理中初始化并使用。
Attachable接口应该作为公共类资源放在宿主app和插件app中作为支撑。否则,当获取插件Activity后,将无法把插件和代理关联。
7 Attachable接口
public interface Attachable {
void attach(Activity activity, Resources resources , PackageInfo packageInfo);
void onCreate(Bundle savedInstanceState);
void onRestart();
void onStart();
void onResume();
void onPause();
void onStop();
void onDestroy();
/***************根据需要,还可声明更多的生命周期方法*************/
}
8 插件Activity的实现
public class PluginActivity extends Activity implements Attachable {
public static final String TAG=new String("PluginActivity");
private Activity mProxyActivity;
@Override
public void attach(Activity activity) {
this.mProxyActivity=activity;
}
@Override
public View findViewById(int id) {
return mProxyActivity.findViewById(id);
}
public void onCreate(Bundle paramBundle){
mProxyActivity.setContentView(R.layout.plugin_layout);
mImgView=(ImageView) mProxyActivity.findViewById(R.id.pic);
mImgView.setImageResource(R.drawable.imgd);
mButton1=(Button) mProxyActivity.findViewById(R.id.button1);
mButton2=(Button) mProxyActivity.findViewById(R.id.button2);
mButtond=(Button) mProxyActivity.findViewById(R.id.buttond);
mButton1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mImgView.setImageResource(R.drawable.img1);
}
});
mButton2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mImgView.setImageResource(R.drawable.img2);
}
});
mButtond.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mImgView.setImageResource(R.drawable.imgd);
}
});
}
@Override
public void setContentView(int layoutResID){
mProxyActivity.setContentView(layoutResID);
}
@Override
public void onDestroy() { }
@Override
public void onPause() { }
@Override
public void onRestart() { }
@Override
public void onResume() { }
@Override
public void onStart() {}
@Override
public void onStop() {}
}
插件Activity只是简单的实现了点击Button,并切换图片的功能。可以看到,在其内部,所有的动作都是由代理Activity来替它完成的。这就是使用代理进行动态加载的基本思想。
9 代理Activity的实现,并管理插件的生命周期
public class ProxyActivity extends Activity{
private ProxyActivity mSelf;
private Attachable mPluginActivity;
private AssetManager mAssetManager;
private Resources mResources;
//………
//……其他的成员变量
@Override
public Resources getResources() {
return mResources==null ? super.getResources():mResources;
}
private PackageInfo getUninstalledApkInfo(String archiveFilePath){//上文中已实现………… }
private boolean initPlugin(){ //上文中已实现………… }
private void loadResources(){ //上文中已实现………… }
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSelf=this;
if (!loadApk("plugindemo.apk")){
//载入apk出现问题时,调用setContentView,将页面转置出错状态
setContentView(R.layout.load_plugin_error);
return;
}
if (!initPlugin()){
//初始化插件出问题时,调用setContentView,将页面转置出错状态
setContentView(R.layout.load_plugin_error);
return;
}
}
private boolean loadApk(String apkFile){
//获取sdcard上apk包的完整路径
mApkPath=Environment.getExternalStorageDirectory().getAbsolutePath()+
File.separator+apkFile;
mPkgInfo=getUninstalledApkInfo(mApkPath);
if (mPkgInfo==null){
Log.d(TAG, "loadApk: can't get package information");
return false;
}
//获取apk包中package的名称
String packageName=mPkgInfo.packageName;
//创建DexClassLoader的优化目录,在机器的内部存储中
File optimizedDirectoryFile=mSelf.getDir("dex", Context.MODE_PRIVATE);
DexClassLoader dexClassLoader=new DexClassLoader(mApkPath,optimizedDirectoryFile.toString(),null,mSelf.getClassLoader());
try {
mPluginClass=dexClassLoader.loadClass(packageName+".PluginActivity");
} catch (ClassNotFoundException e) {
Log.d(TAG, "loadApk: loading class errors.");
e.printStackTrace();
return false;
}
//获取apk资源
loadResources();
if (mResources==null){
return false;
}
Log.d(TAG, "loadApk: load apk successfully.");
return true;
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mPluginActivity!=null)
mPluginActivity.onDestroy();
}
@Override
protected void onStart() {
super.onStart();
if (mPluginActivity!=null)
mPluginActivity.onStart();
}
// 其余的生命周期方法不再一一列出……………
//……………………………
上述即为代理Activity的实现。在onCreate中进行了apk的加载和插件的初始化。若出错则将Activity显示为出错界面。
可以看到,通过代理Activity的生命周期方法间接的调用了插件Activity的生命周期方法。从而实现了代理“套壳”的作用。
效果
执行效果:
结语
本篇博客实现了一个简单的通过代理Activity管理和展示动态加载的插件的功能。
文章中代理Activity只是实现了功能,并没有做优化和高效的封装。
若各位想深入学习,可以看看社区里很多开源的插件化框架,这些框架对插件的管理、类加载器的管理都实现了很好的封装,不仅实现了Activity的代理,还有Service等其他组件的代理。
若大家发现了什么问题或错误可以留言多多交流。