动态加载插件apk中的资源

场景

前段时间,产品又提出了新的需求,要把app的主题换成圣诞节的主题,过后再换回来。一种思路就是跟夜间模式那样,准备多套主题资源放在app内的资源文件夹内,切换时调用不用的主题即可,但这样无疑增加了app的包体积,而且如果有新的主题资源包要加进来,用户又得更新整个app,这样的更新方式肯定是不好的,这种情况下我们可以考虑另外一种思路,动态加载资源主题包的apk文件。

先看看最终实现的效果对比:

1701036.png

原理

动态加载apk有两种方式:

  • 一种是将资源主题包的apk安装到手机上再读取apk内的资源,这种方式的原理是将宿主app和插件app设置相同的sharedUserId,这样两个app将会在同一个进程中运行,并可以相互访问内部资源了。
  • 一种是不用安装资源apk的方式。其原理是通过DexClassLoader类加载器去加载指定路径下的apk、dex或者jar文件,反射出R类中相应的内部类然后根据资源名来获取我们需要的资源id,然后根据资源id得到对应的图片或者xml文件。

实现

无论是哪种方式,我们都需要新的资源包,我们新建一个android工程,把需要更换的新图片和xml资源文件放在这个工程对应的目录下,注意,文件名必需和宿主app内对应的文件名相同,因为后面反射是根据资源名去找资源id。然后将这个工程打包成apk并使用跟宿主app相同的签名文件签名,在app启动的Activity中需要加一个检查是否有新的资源包和是否需要删掉资源包的接口(需要后台人员配合写接口),如果有就下载apk,至于安装apk和不安装apk这两种方式哪种更好,我觉得安装apk这种方式不太友好,即使我们可以做到安装后在桌面上没有启动图标,但还是有一个安装的过程,对用户来说,可能不知道这是什么东西,以为又安装了什么新应用,所以我会使用不安装apk来更新这种方式,这里也还是要记录下安装apk方式是怎么做的。

准备资源包

新建工程Skin-Plugin,将要更换的图片或者xml文件放在对应的drawable文件夹内,在AndroidManifest.xml中增加shareUserId,然后打包成apk文件。如果是不需要安装apk的,就不用设置shareUserId了。

shareUserId这个值可以随意设定,但是必须和宿主app里面的设置为相同才行。

12081.png

我这里只更新几个icon图标和底部tab的selector资源。

12082.png

AndroidManifest配置如上图所示,需要注意的是,让app不在桌面上生成应用图标,需要将启动activity去掉下面的过滤配置:

<intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>

去掉上述配置后这个工程是无法执行Run操作了,但是不要紧,不影响打包成apk。

加载安装的apk

前面说过要提供一个接口下载新的资源包,下载后自动安装,我们在使用这些资源的地方去检查资源apk有没有安装,如果有,就加载资源包中的资源,将检查apk是否安装的方法写到工具类中,这里需要传入资源app的包名。

/**
 * apk是否已安装
 * @param packageName
 * @return true已经安装,false未安装或者已经卸载。
 */
public static boolean checkApkInstalled(Context context, String packageName) {
    if (packageName == null || "".equals(packageName)) {
        return false;
    }
    try {
        ApplicationInfo info = context.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
        return true;
    } catch (PackageManager.NameNotFoundException e) {
        return false;
    }
}

检查到安装了插件apk后,需要创建一个插件apk内的上下文对象,因为只有插件apk的上下文对象才能获取到它的Resourece对象,从而通过插件上下文获取资源id。

//获取对应插件中的上下文,通过它可得到插件的Resource  
Context pluginContext = this.createPackageContext(packageName, CONTEXT_IGNORE_SECURITY | CONTEXT_INCLUDE_CODE);  
//获取资源id
int resId = pluginContext.getResources().getIdentifier(......);

加载未安装的apk

同样的这种方式也要提供一个资源包,用户启动app时在后台静默下载插件apk文件,保存到指定的路径下。我们要加载这个插件,就需要一个插件的类加载器,而不是宿主app的类加载器,这时候只能去手动构建DexClassLoader,再通过类加载器,反射出R类中相应的内部类进而获取我们需要的资源id。

/**
 * 加载apk获得内部资源id
 * @param context
 * @param pluginPath apk路径
 */
public static int getResId(Context context, String pluginPath, String apkPackageName, String resName) {
    try {
        //在应用安装目录下创建一个名为app_dex文件夹目录,如果已经存在则不创建
        File optimizedDirectoryFile = context.getDir("dex", Context.MODE_PRIVATE);
        // 构建插件的DexClassLoader类加载器,参数:
        // 1、包含dex的apk文件或jar文件的路径,
        // 2、apk、jar解压缩生成dex存储的目录,
        // 3、本地library库目录,一般为null,
        // 4、父ClassLoader
        DexClassLoader dexClassLoader = new DexClassLoader(pluginPath, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
        //通过使用apk自己的类加载器,反射出R类中相应的内部类进而获取我们需要的资源id
        Class<?> clazz = dexClassLoader.loadClass(apkPackageName + ".R$drawable");
        Field field = clazz.getDeclaredField(resName);//得到名为resName的这张图片字段
        return field.getInt(R.id.class);//得到图片id
    } catch (Exception e) {
        e.printStackTrace();
    }
    return 0;
}

其中第二个参数是插件apk的全路径,文件名必需是带.apk,第三个参数是插件apk的包名,第四个参数是资源名。

/**
 * 获取插件apk的包名
 * @param context
 * @param pluginPath 插件apk的绝对路径
 * @return
 */
public static String getPluginPackagename(Context context, String pluginPath) {
    PackageManager pm = context.getPackageManager();
    PackageInfo pkgInfo = pm.getPackageArchiveInfo(pluginPath, PackageManager.GET_ACTIVITIES);
    if (pkgInfo != null) {
        ApplicationInfo appInfo = pkgInfo.applicationInfo;
        String pkgName = appInfo.packageName;//包名
        return pkgName;
    }
    return null;
}

只有资源id还不够,还需要插件apk的Resources对象,因为只有它才能根据资源id获取到对应的资源。

/**
 * 获取对应插件的Resource对象
 * @param context
 * @param pluginPath 插件apk的路径,带apk名
 * @return
 */
public static Resources getPluginResources(Context context, String pluginPath) {
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        // 反射调用方法addAssetPath(String path)
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        // 将插件Apk文件添加进AssetManager
        addAssetPath.invoke(assetManager, pluginPath);
        // 获取宿主apk的Resources对象
        Resources superRes = context.getResources();
        // 获取插件apk的Resources对象
        Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
        return mResources;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

获取插件的Resources对象其实就是用反射的方式调用了AssetManager类的addAssetPath方法,这个方法的目的是将插件apk里的资源都加载到AssetManager对象中进行管理,然后来构建插件apk的Resources。至于为什么要用反射,看看addAssetPath的源码:

/** 
 * Add an additional set of assets to the asset manager.  This can be 
 * either a directory or ZIP file.  Not for use by applications.  Returns 
 * the cookie of the added asset, or 0 on failure. 
 * {@hide} 
 */  
public final int addAssetPath(String path) {  
    int res = addAssetPathNative(path);  
    return res;  
}  

这里有个注解@hide,表示即使它是public的,但是外界仍然无法访问它的,因为android sdk导出的时候会自动忽略隐藏的api,因此只能通过反射来调用。

// 根据资源名去加载新的资源
String pluginPath = Environment.getExternalStorageDirectory().toString() + "/dynamicload/download/skin-plugin.apk";
if (item.getResName() != null) {
    Drawable drawable = Util.getPluginResources(mContext, pluginPath).getDrawable(Util.getResId(mContext, pluginPath, Util.getPluginPackagename(mContext, pluginPath), item.getResName()));
    imageView.setImageDrawable(drawable);
}

至此就完成了动态加载插件apk资源,当我们需要切换回原来的资源时,只需要将资源包删除即可,或者重新构建一个资源包,让用户去下载,由于我们是运行时加载,所以当更换了资源包时,第一次打开只是去下载这个插件资源包,再次打开时才会去加载。

代码下载地址:
https://github.com/shenhuniurou/BlogDemos/tree/master/DynamicLoadDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,563评论 25 707
  • 动态加载技术 介绍 在程序运行的时候,加载一些程序自身原本不存在的可执行文件并运行这些文件里的代码逻辑。 动态调用...
    冰点k阅读 3,762评论 1 11
  • 首先引入一个概念,动态加载技术是什么?为什么要引入动态加载?它有什么好处呢?首先要明白这几个问题,我们先从应用程序...
    CHSmile阅读 1,563评论 0 10
  • 最近几年移动开发业界兴起了「 插件化技术 」的旋风,各个大厂都推出了自己的插件化框架,各种开源框架都评价自身功能优...
    斜杠时光阅读 3,910评论 1 36
  • 启动服务器时加载过滤器的实例调用init()方法初始化实例,在项目启动时候调用一次;每一请求只调用方法的doFil...
    小孩真笨阅读 1,809评论 0 0