Android插件化原理探究

Android插件化原理探究

一、简介

android动态加载插件机制一直以来就是探索的热门领域,各种动态加载框架层出不穷,动态插件机制能有效解决一些线上bug进而避免频繁的版本发布。本文一不对当前流行的框架进行探讨(如果有需要人家已经开源),二不追求去实现这么一个完整的动态加载框架(这一般都是大厂所为,耗时耗力,而且如果真有机会去实现,熟知原理就会有方案可寻),只是总结下相关原理,这样不仅对动态加载有一定的认知,而且对理解Android系统大有裨益。

二、热修复与插件化

通常谈到插件化的时候紧密相连的就事热修复。这里对二者进行一些小小的区分。热修复一般是修复出现bug的类或者方法,该技术一般需要能做到替换原有类或者方法的目的;而插件化可以认为比热修复稍简单些,该技术一般只需要加载一个独立的插件化的apk即可,而不必进行原有类的替换,能满足动态添加的功能,这对一个超级app的开发有很大意义。

如果从java层面入手,二者本质上都会涉及到对类加载的操作、对资源加载的操作等。而本文就从java层面总结下插件化相关的一些技术原理,而不是native hook层次方面的原理(这个需要深入了解native层面下的东西)。

三、插件化要解决的问题

(1)代码的加载。

宿主app如何加载插件apk?因为class loader是全局唯一的,在宿主启动的时候就已经知道该加载宿主的apk了,然而插件的apk如何加载?

(2)组件生命周期的管理

对于普通的类加载,我们很容易借助于class loader完成,但是对于android中的activity等组件来说,这还远远满足不了需求,首先android拒绝加载没有在manifest文件注册的activity,而插件中apk的组件显然不会在宿主manifest文件中注册;其次类似于activity等组件的声明周期怎么维持?

(3)资源的加载

如何进行资源的加载?这里的资源是指插件apk中的资源。因为宿主显然对插件中的资源无感知,当我们应用R.xx.id进行资源查找时,必然会报错。

四、问题解决原理

1、代码的加载

熟悉java的一般都会知道代码的加载最终是由ClassLoader进行加载的,java中的classloader采用的是双亲委派模型,即首先会委托给父加载器进行加载,父加载器加载不了再自己去加载。java类加载器的这个逻辑有助于我们实现对插件apk的加载。

在android中加载类的class loader是DexClassLoader,因此首先我们要做到如何能加载插件apk(因为插件apk可能存在任何地方,宿主ClassLoader显然不会知道他在那里,加载就更无从谈起了)。

(1)自定义ClassLoader

这个原理就是要替换掉宿主的ClassLoader,采用我们自己的ClassLoader,进而实现加载我们插件apk的目的。这里想要替换一般会hook掉系统中的缓存的ClassLoader,进而截断这个过程。这样我们自己的classloader就可以实现针对插件apk进行加载。不过这种方法涉及到大量的对系统代码的反射操作,因为想要构建出可用的classloader,必须要确保合理的入参。所以难度实现起来较大。对于刚刚提到的hook点,这里可以给出一段代码:

public final LoadedApk getPackageInfo(String packageName, CompatibilityInfo compatInfo,  int flags, int userId) {  

final boolean differentUser = (UserHandle.myUserId() != userId);  

synchronized (mResourcesManager) {  

            WeakReference ref;  

if (differentUser) {  

// Caching not supported across users  

ref =null;  

}else if ((flags & Context.CONTEXT_INCLUDE_CODE) != 0) {  

                ref = mPackages.get(packageName);  

}else {  

                ref = mResourcePackages.get(packageName);  

            }  

LoadedApk packageInfo = ref !=null ? ref.get() : null;  

//Slog.i(TAG, "getPackageInfo " + packageName + ": " + packageInfo);  

//if (packageInfo != null) Slog.i(TAG, "isUptoDate " + packageInfo.mResDir  

//        + ": " + packageInfo.mResources.getAssets().isUpToDate());  

if (packageInfo != null && (packageInfo.mResources == null  

                    || packageInfo.mResources.getAssets().isUpToDate())) {  

if (packageInfo.isSecurityViolation()  

&& (flags&Context.CONTEXT_IGNORE_SECURITY) ==0) {  

throw new SecurityException(  

"Requesting code from " + packageName  

+" to be run in process "   + mBoundApplication.processName  

+"/" + mBoundApplication.appInfo.uid);  }  

return packageInfo;   }  }  

ApplicationInfo ai =null;  

try {  

            ai = getPackageManager().getApplicationInfo(packageName,  

                    PackageManager.GET_SHARED_LIBRARY_FILES, userId);  

}catch (RemoteException e) {  

// Ignore  

        }  

if (ai != null) {  

return getPackageInfo(ai, compatInfo, flags);  }  

return null;   }  

上述代码实质上是获取缓存的package info,返回的对象类型是LoadApk,LoadApk中包含了package信息,而package信息中就包含了classloader信息。具体可以自行查看。

(2)委托加载

这中方案的原理不在追求替换掉系统的classloader,而是选择了一个相对妥协的方案,即依然运行系统的classloader去进行类加载,那么此时又如何做到加载我们插件apk的呢?

这个就又涉及到android系统类加载的过程了,上文提到android提供了DexClassLoader进行类加载,实际上他继承于BaseDexClassLoader,在其findClass(类加载器建议通过该方法查找class)中有一端下面代码(源码地址参见:http://androidxref.com/6.0.1_r10/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java):

Override  

protected Class findClass(String name) throws ClassNotFoundException {  

List suppressedExceptions =new ArrayList();  

        Class c = pathList.findClass(name, suppressedExceptions);  

if (c == null) {  

ClassNotFoundException cnfe =new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);  

for (Throwable t : suppressedExceptions) {  

                cnfe.addSuppressed(t);  

            }  

throw cnfe;  

        }  

return c;  

    }  

从代码中可以看出,这里主要通过pathList完成了类的查找。pathList类型是DexPathList,查找代码(http://androidxref.com/6.0.1_r10/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java),实现如下:

public Class findClass(String name, List suppressed) {  

for (Element element : dexElements) {  

            DexFile dex = element.dexFile;  

if (dex != null) {  

                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);  

if (clazz != null) {  

return clazz;  

                }   }   }  

if (dexElementsSuppressedExceptions != null) {  

            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));  

        }  

return null;      }  

从代码中可以看出,这里findClass会遍历dexElements数组完成最终的加载,对!这就是这种方案的突破口!我们只需要将我们插件的dex插入到这个数组中就可以实现对我们插件apk的加载(通过反射去构建dexElements,将宿主和插件apk的dex一起copy进去即可)!

至此,上面两种方式可以实现对插件apk的加载。

(3)生命周期的管理

对于普通的类加载,我们很容易借助于class loader完成,但是对于android中的activity等组件来说,这还远远满足不了需求,因为android拒绝加载没有在manifest文件注册的activity,而插件中apk的组件显然不会再宿主manifest文件中注册,因此这是首要面临的问题。下面给出两种流行的方案。

4、生命周期的管理

对于生命周期的管理,目前我得知有两种方案

(1)hook机制。

类似于解决classloader加载class类一样,hook住关键点,进而达到欺骗Framework的目的。这里首先会在manifest中注册代理activity(此处暂时称为ProxyActivity),在跳转的时候跳转的指向的是ProxyActivity,而在真正launch的时候launch的是真正的目标Actitity(此处暂时称为TargetActivity)。话是这么说,但是如何做到无缝替换?其实这种方案就是从系统源码入手,在startActivity开始,到进入system_server进程ActivityManagerService之前保持目标activity指向ProxyActivity,以解决activity在manifet中未注册问题;在从ActivityManagerService进程回到当前启动进程时用TargetActivity替换掉ProxyActivity已达到欺骗AMS的目的,具体可查阅相关资料。

这种做法可以使activity有完整的生命周期,因为都是有AMS再进行管理。

(2)代理机制

这种做法在ProxyActivity的注册上与上述方法一致,但在声明周期的管理上则是有动态加载框架实现了一系列代理目标组件的生命周期接口来完成的。当启动目标Activity的时候,实际上是有代理Activity完全代理了其声明周期。

3、资源的加载实现原理(取自DL框架对于资源加载原理描述)

加载的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources中,由于addAssetPath是隐藏api我们无法直接调用,所以只能通过反射,其入参传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了,然后再通过AssetManager来创建一个新的Resources对象,这个对象就是我们可以使用的apk中的资源了,这样我们的问题就解决了。

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

推荐阅读更多精彩内容