亲自动手实现Android App插件化

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将插件的路径添加到原来的数组中。

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

https://my.oschina.net/android520/blog/794715

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的加载原理参考 https://my.oschina.net/android520/blog/795599

主要通过Hook系统的IActivityManager完成

3. 资源加载

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

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

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

具体原理参照:http://www.jb51.net/article/100245.htm

使用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) ;
 }
 }
}

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

(0)

相关推荐

  • Android实现使用微信登录第三方APP的方法

    本文实例讲述了Android实现使用微信登录第三方APP的方法.分享给大家供大家参考,具体如下: 使用微信登录APP,免去注册过程,现在已经有很多的类似应用了.集成该功能过程不复杂,但还是有一些地方需要注意的. 开始之前,需要做下面的准备工作. 1.到微信开放平台注册你的APP,并申请开通微信登录的权限.参考这里: https://open.weixin.qq.com// 2.下载Android SDK和签名查看工具,请参考: https://open.weixin.qq.com/cgi-bin

  • Android下保存简单网页到本地(包括简单图片链接转换)实现代码

    最近在做一个项目涉及到将包含图片的简单网页下载到本地,方便离线时观看,在这里分享一下,大家做下简单修改就可以用到自己的项目中了.(这里用到了AQuery库) 复制代码 代码如下: package com.nekocode.xuedao.utils; import java.io.File;import java.io.FileOutputStream;import java.util.ArrayList;import java.util.regex.Matcher;import java.uti

  • iOS和Android用同一个二维码实现跳转下载链接的方法

    前言 最近一个项目需要iOS和安卓使用一个二维码,让扫描的机器自己识别操作系统实现跳转到相应的下载链接.比如iPhone用微信进行扫描就让他跳转appStore的下载页面,安卓机器使用微信扫描就直接跳浏览器下载.但是这二维码还有一个需求就是,用户已经下载了这个app,当用户打开app进入到注册页面时,再次扫描这个二维码时,自动填写邀请码进行注册.那么该如何实现,细节就不说了,直接上代码. 使用js实现,其实代码非常简单. 使用时直接拷贝代码,改掉相应的链接就好. PS:该链接在微信环境打开时还是

  • Android小挂件(APP Widgets)设计指导

    应用小挂件(也叫做窗口小挂件)在android1.5的时候被第一次引出,后来再android3.0和android3.1中得到了极大的发展,他们可以展示一些应用的常用信息或者一些相关的信息到桌面上,标准的Android系统镜像中有很多自带的创口小挂件,例如:闹钟.音乐等 Figure 1. Example app widgets in Android 4.0. 本文将描述怎么去设计小挂件,以便于能很好的与其他挂件搭配的很默契,同时也会介绍一些小技巧. AppWidget 剖析 一个典型的andr

  • Android如何动态改变App桌面图标

    时不时的我们就会发现,一些我们常见的应用,比如某宝,某东,在一些特殊的日子中,比如双十一,元旦,为了迎合这样一个日子的气氛,在桌面的应用图标就会发生改变,其实对于这样的一个桌面图标更换,Android中为我们提供了AndroidManifest.xml里的<activity-alias>标签实现方式(更多文章请关注我的微信公众账号,左边二维码). 我们知道,我们每写一个 Activity就要在AndroidManifest进行配置一下,我们才可以正常的启动它,除此之外,我们还可以对它设置一个别

  • Android中Textview超链接实现方式

    TextView中的超链接可以通过几种方式实现: 一.Html.fromHtml方式 TextView,本身就支持部分的Html格式标签.这其中包括常用的字体大小颜色设置,文本链接等.使用起来也比较方便,只需要使用Html类转换一下即可: textView.setText(Html.fromHtml(str)); 代码如下: public class Test10Activity extends Activity { TextView textView ; @Override protected

  • Android开发技巧之在a标签或TextView控件中单击链接弹出Activity(自定义动作)

    在5.2.1节和5.2.2节介绍了<a>标签以及TextView自动识别的特殊文本(网址.电话号.Email等),这些都可以通过单击来触发不同的动作.虽然这些单击动作已经可以满足大多数需要了,但如果读者想在单击链接时执行任意自定义的动作,那么本节的内容非看不可. 现在让我们使用5.2.1节介绍的方法重新查看Html.java文件的内容,随便找一个处理Html标签的方法,例 如,endA方法.该方法用于处理</a>标签.我们会发现在该方法中如下的语句. text.setSpan(ne

  • Android实现TextView中文字链接的4种方式介绍及代码

    Android 的实现TextView中文字链接的方式有很多种. 总结起来大概有4种: 1.当文字中出现URL.E-mail.电话号码等的时候,可以将TextView的android:autoLink属性设置为相应的的值,如 果是所有的类型都出来就是android:autoLink="all".当然也可以在java代码里 做,textView01.setAutoLinkMask(Linkify.ALL); 2.将要处理的文字写到一个资源文件,如string.xml,然后的java代码里

  • Android开发之获取网络链接状态

    网络开发是Android程序设计一个非常重要的内容,今天本文就和大家一起分享一下Android网络开发的一点经验. 本文主要通过实例形式说明了Android获取网络链接状态的方法.具体内容如下: 就目前的Android手机来说,可能存在如下5种网络状态: ----无网络(这种状态可能是因为手机停机,网络没有开启,信号不好等原因) ----使用WIFI上网 ----CMWAP(中国移动代理) ----CMNET上网 ----2G/3G/4G上网 很多时候我们需要判断用户是否开启网络设置,通常通过C

  • Android中捕获TTextView文本中的链接点击事件方法

    Android中的TTextView很强大,我们可以不仅可以设置纯文本为其内容,还可以设置包含网址和电子邮件地址的内容,并且使得这些点击可以点击.但是我们可以捕获并控制这些链接的点击事件么,当然是可以的. 本文将一个超级简单的例子介绍一下如何实现在Android TextView 捕获链接的点击事件. 关键实现 实现原理就是将所有的URL设置成ClickSpan,然后在它的onClick事件中加入你想要的控制逻辑就可以了. 复制代码 代码如下: private void setLinkClick

  • Android编程实现点击链接打开APP功能示例

    本文实例讲述了Android编程实现点击链接打开APP功能.分享给大家供大家参考,具体如下: 在Android中点击链接打开APP是一个很常见的需求.例如,电商为用户发送优惠券之后经常会下发一条短信:某某优惠券已发送到您的账户中,点击 xxx 链接即可查看!此时当用户点击链接之后会直接打开本地APP,进入相关页面. 功能实现: 1.在manifest中为相应的activity添加intent-filter: <activity android:name=".TestActivity&quo

随机推荐