欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

亲自动手实现Android App插件化

程序员文章站 2024-02-27 16:54:21
android插件化目前国内已经有很多开源的工程了,不过如果不实际开发一遍,很难掌握的很好。 下面是自己从0开始,结合目前开源的项目和博客,动手开发插件化方案。 按照需...

android插件化目前国内已经有很多开源的工程了,不过如果不实际开发一遍,很难掌握的很好。

下面是自己从0开始,结合目前开源的项目和博客,动手开发插件化方案。

按照需要插件化主要解决下面的几种问题:

1. 代码的加载

(1) 要解决纯java代码的加载

(2) android组件加载,如activity、service、broadcast receiver、contentprovider,因为它们是有生命周期的,所以要特殊处理

(3) android native代码的加载

(4) android 特殊控件的处理,如notification等

2. 资源加载

不同插件的资源如何管理,是公用一套还是插件独立管理?

因为在android中访问资源,都是通过r. 实现的, 

下面就一步步解决上面的问题

1. 纯java代码的加载

主要就是通过classloader、更改dexelements将插件的路径添加到原来的数组中。

详细的分析可以参考我转载的一篇文章,因为感觉原贴命名和结构有点乱,所以转载记录下。

android提供dexclassloader和pathclassloader,都继承basedexclassloader,只是构造方法的参数不一样,即optdex的路径不一样,源码如下

// dexclassloader.java
public class dexclassloader extends basedexclassloader {
 public dexclassloader(string dexpath, string optimizeddirectory,
  string librarypath, classloader parent) {
 super(dexpath, new file(optimizeddirectory), librarypath, parent);
 }
}

// pathclassloader.java
public class pathclassloader extends basedexclassloader {
 public pathclassloader(string dexpath, classloader parent) {
 super(dexpath, null, null, parent);
 }

 public pathclassloader(string dexpath, string librarypath,
  classloader parent) {
 super(dexpath, null, librarypath, parent);
 }
}

其中,optimizeddirectory是用来存储opt后的dex目录,必须是内部存储路径。

dexclassloader可以加载外部的dex或apk,只要opt的路径通过参数设置一个内部存储路径即可。

pathclassloader只能加载已安装的apk,因为opt路径会使用默认的dex路径,外部的不可以。

下面介绍下如何通过dexclassloader实现加载java代码,参考nuwa

这种方式类似于热修复,如果插件和宿主代码有相互访问,则需要在打包中使用插桩技术实现。

public static boolean injectdexatfirst(string dexpath, string dexoptpath) {

 // 获取系统的dexelements
 object basedexelements = getdexelements(getpathlist(getpathclassloader()));

 // 获取patch的dexelements
 dexclassloader patchdexclassloader = new dexclassloader(dexpath, dexoptpath, dexpath, getpathclassloader());
 object patchdexelements = getdexelements(getpathlist(patchdexclassloader));

 // 组合最新的dexelements
 object alldexelements = combinearray(patchdexelements, basedexelements);

 // 将最新的dexelements添加到系统的classloader中
 object pathlist = getpathlist(getpathclassloader());
 fieldutils.writefield(pathlist, "dexelements", alldexelements);
}

public static classloader getpathclassloader() {
 return dexutils.class.getclassloader();
}

/**
 * 反射调用getpathlist方法,获取数据
 * @param classloader
 * @return
 * @throws classnotfoundexception
 * @throws nosuchfieldexception
 * @throws illegalaccessexception
 */
public static object getpathlist(classloader classloader) throws classnotfoundexception, nosuchfieldexception, illegalaccessexception {
 return fieldutils.readfield(classloader, "pathlist");
}

/**
 * 反射调用pathlist对象的dexelements数据
 * @param pathlist
 * @return
 * @throws nosuchfieldexception
 * @throws illegalaccessexception
 */
public static object getdexelements(object pathlist) throws nosuchfieldexception, illegalaccessexception {
 logutils.d("reflect to get dexelements");
 return fieldutils.readfield(pathlist, "dexelements");
}

/**
 * 拼接dexelements,将patch的dex插入到原来dex的头部
 * @param firstelement
 * @param secondelement
 * @return
 */
public static object combinearray(object firstelement, object secondelement) {

 logutils.d("combine dexelements");

 // 取得一个数组的class对象, 如果对象是数组,getclass只能返回数组类型,而getcomponenttype可以返回数组的实际类型
 class objtypeclass = firstelement.getclass().getcomponenttype();

 int firstarraylen = array.getlength(firstelement);
 int secondarraylen = array.getlength(secondelement);
 int allarraylen = firstarraylen + secondarraylen;

 object allobject = array.newinstance(objtypeclass, allarraylen);
 for (int i = 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));
 }
 }
 return allobject;
}

使用上面的方式启动的activity,是有生命周期的,应该是使用系统默认的创建activity方式,而不是自己new activity对象,所以打开的activity生命周期正常。

但是上面的方式,必须保证activity在宿主androidmanifest.xml中注册。

2. 下面介绍下如何加载未注册的activity功能

activity的加载原理参考

主要通过hook系统的iactivitymanager完成

3. 资源加载

资源访问都是通过r.方式,实际上android会生成一个0x7f******格式的int常量值,关联对应的资源。

如果资源有更改,如layout、id、drawable等变化,会重新生成r.java内容,int常量值也会变化。

因为插件中的资源没有参与宿主程序的资源编译,所以无法通过r.进行访问。

具体原理参照:

使用addassetpath方式将插件路径添加到宿主程序后,因为插件是独立打包的,所以资源id也是从1开始,而宿主程序也是从1开始,可能会导致插件和宿主资源冲突,系统加载资源时以最新找到的资源为准,所以无法保证界面展示的是宿主的,还是插件的。

针对这种方式,可以在打包时,更改每个插件的资源id生成的范围,可以参考public.xml介绍。

代码参考amigo

public static void loadpatchresources(context context, string apkpath) throws exception {
 assetmanager newassetmanager = assetmanager.class.newinstance();
 invokemethod(newassetmanager, "addassetpath", apkpath);
 invokemethod(newassetmanager, "ensurestringblocks");
 replaceassetmanager(context, newassetmanager);
}

private static void replaceassetmanager(context context, assetmanager newassetmanager)
  throws exception {
 collection<weakreference<resources>> references;
 if (build.version.sdk_int >= build.version_codes.kitkat) {
 class<?> resourcesmanagerclass = class.forname("android.app.resourcesmanager");
 object resourcesmanager = 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();
 }

 assetmanager assetmanager = context != null ? context.getassets() : null;
 for (weakreference<resources> wr : references) {
 resources resources = wr.get();
 if (resources == null) continue;

 try {
  writefield(resources, "massets", newassetmanager);
  originalassetmanager = assetmanager;
 } catch (throwable ignore) {
  object resourceimpl = 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) {
  resources resources = wr.get();
  if (resources == null) continue;

  // android.util.pools$synchronizedpool<typedarray>
  object typedarraypool = readfield(resources, "mtypedarraypool", true);

  // clear all the pools
  while (invokemethod(typedarraypool, "acquire") != null) ;
 }
 }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。