亲自动手实现Android App插件化
Android插件化目前国内已经有很多开源的工程了,不过如果不实际开发一遍,很难掌握的很好。
下面是自己从0开始,结合目前开源的项目和博客,动手开发插件化方案。
按照需要插件化主要解决下面的几种问题:
1.代码的加载
(1)要解决纯Java代码的加载
(2)Android组件加载,如Activity、Service、BroadcastReceiver、ContentProvider,因为它们是有生命周期的,所以要特殊处理
(3)AndroidNative代码的加载
(4)Android特殊控件的处理,如Notification等
2.资源加载
不同插件的资源如何管理,是公用一套还是插件独立管理?
因为在Android中访问资源,都是通过R.实现的,
下面就一步步解决上面的问题
1.纯Java代码的加载
主要就是通过ClassLoader、更改DexElements将插件的路径添加到原来的数组中。
详细的分析可以参考我转载的一篇文章,因为感觉原贴命名和结构有点乱,所以转载记录下。
https://my.oschina.net/android520/blog/794715
Android提供DexClassLoader和PathClassLoader,都继承BaseDexClassLoader,只是构造方法的参数不一样,即optdex的路径不一样,源码如下
//DexClassLoader.java
publicclassDexClassLoaderextendsBaseDexClassLoader{
publicDexClassLoader(StringdexPath,StringoptimizedDirectory,
StringlibraryPath,ClassLoaderparent){
super(dexPath,newFile(optimizedDirectory),libraryPath,parent);
}
}
//PathClassLoader.java
publicclassPathClassLoaderextendsBaseDexClassLoader{
publicPathClassLoader(StringdexPath,ClassLoaderparent){
super(dexPath,null,null,parent);
}
publicPathClassLoader(StringdexPath,StringlibraryPath,
ClassLoaderparent){
super(dexPath,null,libraryPath,parent);
}
}
其中,optimizedDirectory是用来存储opt后的dex目录,必须是内部存储路径。
DexClassLoader可以加载外部的dex或apk,只要opt的路径通过参数设置一个内部存储路径即可。
PathClassLoader只能加载已安装的apk,因为opt路径会使用默认的dex路径,外部的不可以。
下面介绍下如何通过DexClassLoader实现加载Java代码,参考Nuwa
这种方式类似于热修复,如果插件和宿主代码有相互访问,则需要在打包中使用插桩技术实现。
publicstaticbooleaninjectDexAtFirst(StringdexPath,StringdexOptPath){
//获取系统的dexElements
ObjectbaseDexElements=getDexElements(getPathList(getPathClassLoader()));
//获取patch的dexElements
DexClassLoaderpatchDexClassLoader=newDexClassLoader(dexPath,dexOptPath,dexPath,getPathClassLoader());
ObjectpatchDexElements=getDexElements(getPathList(patchDexClassLoader));
//组合最新的dexElements
ObjectallDexElements=combineArray(patchDexElements,baseDexElements);
//将最新的dexElements添加到系统的classLoader中
ObjectpathList=getPathList(getPathClassLoader());
FieldUtils.writeField(pathList,"dexElements",allDexElements);
}
publicstaticClassLoadergetPathClassLoader(){
returnDexUtils.class.getClassLoader();
}
/**
*反射调用getPathList方法,获取数据
*@paramclassLoader
*@return
*@throwsClassNotFoundException
*@throwsNoSuchFieldException
*@throwsIllegalAccessException
*/
publicstaticObjectgetPathList(ClassLoaderclassLoader)throwsClassNotFoundException,NoSuchFieldException,IllegalAccessException{
returnFieldUtils.readField(classLoader,"pathList");
}
/**
*反射调用pathList对象的dexElements数据
*@parampathList
*@return
*@throwsNoSuchFieldException
*@throwsIllegalAccessException
*/
publicstaticObjectgetDexElements(ObjectpathList)throwsNoSuchFieldException,IllegalAccessException{
LogUtils.d("ReflectToGetDexElements");
returnFieldUtils.readField(pathList,"dexElements");
}
/**
*拼接dexElements,将patch的dex插入到原来dex的头部
*@paramfirstElement
*@paramsecondElement
*@return
*/
publicstaticObjectcombineArray(ObjectfirstElement,ObjectsecondElement){
LogUtils.d("CombineDexElements");
//取得一个数组的Class对象,如果对象是数组,getClass只能返回数组类型,而getComponentType可以返回数组的实际类型
ClassobjTypeClass=firstElement.getClass().getComponentType();
intfirstArrayLen=Array.getLength(firstElement);
intsecondArrayLen=Array.getLength(secondElement);
intallArrayLen=firstArrayLen+secondArrayLen;
ObjectallObject=Array.newInstance(objTypeClass,allArrayLen);
for(inti=0;i<allArrayLen;i++){
if(i<firstArrayLen){
Array.set(allObject,i,Array.get(firstElement,i));
}else{
Array.set(allObject,i,Array.get(secondElement,i-firstArrayLen));
}
}
returnallObject;
}
使用上面的方式启动的Activity,是有生命周期的,应该是使用系统默认的创建Activity方式,而不是自己newActivity对象,所以打开的Activity生命周期正常。
但是上面的方式,必须保证Activity在宿主AndroidManifest.xml中注册。
2.下面介绍下如何加载未注册的Activity功能
Activity的加载原理参考https://my.oschina.net/android520/blog/795599
主要通过Hook系统的IActivityManager完成
3.资源加载
资源访问都是通过R.方式,实际上Android会生成一个0x7f******格式的int常量值,关联对应的资源。
如果资源有更改,如layout、id、drawable等变化,会重新生成R.java内容,int常量值也会变化。
因为插件中的资源没有参与宿主程序的资源编译,所以无法通过R.进行访问。
具体原理参照:https://www.nhooo.com/article/100245.htm
使用addAssetPath方式将插件路径添加到宿主程序后,因为插件是独立打包的,所以资源id也是从1开始,而宿主程序也是从1开始,可能会导致插件和宿主资源冲突,系统加载资源时以最新找到的资源为准,所以无法保证界面展示的是宿主的,还是插件的。
针对这种方式,可以在打包时,更改每个插件的资源id生成的范围,可以参考public.xml介绍。
代码参考Amigo
publicstaticvoidloadPatchResources(Contextcontext,StringapkPath)throwsException{
AssetManagernewAssetManager=AssetManager.class.newInstance();
invokeMethod(newAssetManager,"addAssetPath",apkPath);
invokeMethod(newAssetManager,"ensureStringBlocks");
replaceAssetManager(context,newAssetManager);
}
privatestaticvoidreplaceAssetManager(Contextcontext,AssetManagernewAssetManager)
throwsException{
Collection<WeakReference<Resources>>references;
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.KITKAT){
Class<?>resourcesManagerClass=Class.forName("android.app.ResourcesManager");
ObjectresourcesManager=invokeStaticMethod(resourcesManagerClass,"getInstance");
if(getField(resourcesManagerClass,"mActiveResources")!=null){
ArrayMap<?,WeakReference<Resources>>arrayMap=
(ArrayMap)readField(resourcesManager,"mActiveResources",true);
references=arrayMap.values();
}else{
references=(Collection)readField(resourcesManager,"mResourceReferences",true);
}
}else{
HashMap<?,WeakReference<Resources>>map=
(HashMap)readField(ActivityThreadCompat.instance(),"mActiveResources",true);
references=map.values();
}
AssetManagerassetManager=context!=null?context.getAssets():null;
for(WeakReference<Resources>wr:references){
Resourcesresources=wr.get();
if(resources==null)continue;
try{
writeField(resources,"mAssets",newAssetManager);
originalAssetManager=assetManager;
}catch(Throwableignore){
ObjectresourceImpl=readField(resources,"mResourcesImpl",true);
writeField(resourceImpl,"mAssets",newAssetManager);
}
resources.updateConfiguration(resources.getConfiguration(),
resources.getDisplayMetrics());
}
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){
for(WeakReference<Resources>wr:references){
Resourcesresources=wr.get();
if(resources==null)continue;
//android.util.Pools$SynchronizedPool<TypedArray>
ObjecttypedArrayPool=readField(resources,"mTypedArrayPool",true);
//Clearallthepools
while(invokeMethod(typedArrayPool,"acquire")!=null);
}
}
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。