Android插件化原理
1,Android平时开发过程中会用到系统资源(android.R),这些资源并不在我们自己的APK中,为何可以引用?
TestActivity.java
Drawable drawable = getResources().getDrawable(android.R.drawable.sym_def_app_icon);
2,DexClassLoader的构造方法中,dexPath可以传APK路径?
DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
/**
* ...
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* ...
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
...
}
下面针对上面的这两点疑问,我们来逐一分析下。
(一)Resources的创建过程
既然要分析资源如何引用,那么我们就直接从资源的获取开始吧,以获取Drawable为例(getDrawable)
Resources.java
...
public Drawable getDrawable(@DrawableRes int id) throws NotFoundException {
final Drawable d = getDrawable(id, null);// 1,调用了下面的getDrawable
if (d != null && d.canApplyTheme()) {
Log.w(TAG, "Drawable " + getResourceName(id) + " has unresolved theme "
+ "attributes! Consider using Resources.getDrawable(int, Theme) or "
+ "Context.getDrawable(int).", new RuntimeException());
}
return d;
}
...
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException {
TypedValue value;
synchronized (mAccessLock) {
value = mTmpValue;
if (value == null) {
value = new TypedValue();
} else {
mTmpValue = null;
}
getValue(id, value, true);
}
final Drawable res = loadDrawable(value, id, theme);//2,又调用了下面的loadDrawable
synchronized (mAccessLock) {
if (mTmpValue == null) {
mTmpValue = value;
}
}
return res;
}
...
Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
...
Drawable dr;//3,dr赋值的地方,这里出现了3处
if (cs != null) {
dr = cs.newDrawable(this);// 3.1,从ConstantState中创建,首次获取走不到这里
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);//3.2,是一个颜色Drawable
} else {
dr = loadDrawableForCookie(value, id, null);//3.3,图片就是这个方法里面加载出来的,那么继续看
}
...
return dr;
}
...
private Drawable loadDrawableForCookie(TypedValue value, int id, Theme theme) {
if (value.string == null) {
throw new NotFoundException("Resource \"" + getResourceName(id) + "\" ("
+ Integer.toHexString(id) + ") is not a Drawable (color or path): " + value);
}
final String file = value.string.toString();
if (TRACE_FOR_MISS_PRELOAD) {
// Log only framework resources
if ((id >>> 24) == 0x1) {
final String name = getResourceName(id);// 这里获取了一下Resource Name,只是为了Log下? 留意一下,也不用管
if (name != null) {
Log.d(TAG, "Loading framework drawable #" + Integer.toHexString(id)
+ ": " + name + " at " + file);
}
}
}
if (DEBUG_LOAD) {
Log.v(TAG, "Loading drawable for cookie " + value.assetCookie + ": " + file);
}
final Drawable dr;//4,这里开始,准备加载图片了
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
try {
if (file.endsWith(".xml")) {
// 4.1,这里是获取.xml的,不用看
final XmlResourceParser rp = loadXmlResourceParser(
file, id, value.assetCookie, "drawable");
dr = Drawable.createFromXml(this, rp, theme);
rp.close();
} else {
// 4.2,那么获取图片的地方,就是这里了,走到了mAssets里面
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
dr = Drawable.createFromResourceStream(this, value, is, file, null);
is.close();
}
} catch (Exception e) {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
final NotFoundException rnf = new NotFoundException(
"File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
rnf.initCause(e);
throw rnf;
}
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
return dr;
}
既然跑到了mAssets里面,那么继续进入mAssets里面分析了,查看Resources中mAssets这个全局变量,可以知道这个mAssets是一个AssetManager对象,
那么我们就去AssetManager中继续查看这个openNonAsset方法:
AssetManager.java
...
public final InputStream openNonAsset(String fileName) throws IOException {
return openNonAsset(0, fileName, ACCESS_STREAMING);//5,走到了下面的openNonAsset方法
}
...
public final InputStream openNonAsset(int cookie, String fileName, int accessMode)
throws IOException {
synchronized (this) {
if (!mOpen) {
throw new RuntimeException("Assetmanager has been closed");//
}
long asset = openNonAssetNative(cookie, fileName, accessMode);//6,走到了下面的native方法里面
if (asset != 0) {
AssetInputStream res = new AssetInputStream(asset);//根据native返回的long值,得到了我们想要的图片流
incRefsLocked(res.hashCode());
return res;
}
}
throw new FileNotFoundException("Asset absolute file: " + fileName);//
}
...
private native final long openNonAssetNative(int cookie, String fileName, int accessMode);
...
到这里,如果继续分析,一方面是语言方面的阻力,另一方面容易忘记我们最初的目的(Resource如何加载一张图片);
好,我们到这里先停下来回顾一下刚才的流程,整个流程中,出现异常的地方有4处,,
其中和的异常,正常的图片加载过程中,不会出现,不用管,抛出的是FileNotFound异常,属于找具体资源文件时抛出的异常,
我们知道,ID和资源是一一对应的,也就是说ID和资源的名称ResourceName也是一一对应的,假如能通过ID找到对于的ResourceName,一般这个
资源就可以被正常加载了,对照上面的代码,也就是假如没有抛出异常,基本上也不会抛出异常。
(关于资源的详细打包&获取,大家可以阅读下老罗关于资源的系列分析文章:传送门)
好,那我们继续看下对应的方法getResourceName:
Resources.java
...
public String getResourceName(@AnyRes int resid) throws NotFoundException {
String str = mAssets.getResourceName(resid);// 这里又走到了mAssets里面
if (str != null) return str;
throw new NotFoundException("Unable to find resource ID #0x"
+ Integer.toHexString(resid));
}
...
继续分析
AssetManager.java
...
/*package*/ native final String getResourceName(int resid);
...
我们发现又进入了native代码,同样,我们不再继续分析native代码,现在我们冷静的回顾一下之前的整个流程,我们通过传入一个图片ID,
一步步走,无论是获取ResourceName,还是获取最终的图片流,都会走到这个AssetManager里面。
好,到这里,我们结合一下一开始开发出的疑问【Android平时开发过程中会用到系统资源(android.R),这些资源并不在我们自己的APK中,为何可以引用?】,
应用程序是通过一个Resources实例来获取资源的,而Resource又是通过一个AssetManager来获取资源的,那么这个AssetManager在应用程序初始化的时候,
一定会有添加系统资源的环节,要么在构造方法里面,要么就是提供了public的添加接口。
我们先分析AssetManager的构造方法:
AssetManager.java
...
public AssetManager() {
synchronized (this) {
if (DEBUG_REFS) {
mNumRefs = 0;
incRefsLocked(this.hashCode());
}
init(false);// 1,调用了native的init方法
if (localLOGV) Log.v(TAG, "New asset manager: " + this);
ensureSystemAssets();// 2,构造一个SystemAsset
}
}
private static void ensureSystemAssets() {
synchronized (sSync) {
if (sSystem == null) {
AssetManager system = new AssetManager(true);//3,构造一个system的AssetManager
system.makeStringBlocks(null);
sSystem = system;
}
}
}
private AssetManager(boolean isSystem) {
if (DEBUG_REFS) {
synchronized (this) {
mNumRefs = 0;
incRefsLocked(this.hashCode());
}
}
init(true);//4,同样调用了native的init方法
if (localLOGV) Log.v(TAG, "New asset manager: " + this);
}
...
private native final void init(boolean isSystem);
...
sSystem?一个static的AssetManager,难道这个就是用来加载系统资源的?
如果是,我们传入系统资源id: android.R.drawable.sym_def_app_icon,调用getResourceName的时候,一定会调用了这个sSystem实例,
而getResourceName是一个native方法,难道native里面使用了sSystem这个Java层的变量了?
android_util_AssetManager.cpp
...
static jstring android_content_AssetManager_getResourceName(JNIEnv* env, jobject clazz,
jint resid)
{
...
ResTable::resource_name name;
if (!am->getResources().getResourceName(resid, &name)) {
return NULL;
}
...
}
...
走到了ResourceTypes.cpp
ResourceTypes.cpp
...
bool ResTable::getResourceName(uint32_t resID, resource_name* outName) const
{
if (mError != NO_ERROR) {
return false;
}
const ssize_t p = getResourcePackageIndex(resID);
const int t = Res_GETTYPE(resID);
const int e = Res_GETENTRY(resID);
if (p < 0) {
if (Res_GETPACKAGE(resID)+1 == 0) {
LOGW("No package identifier when getting name for resource number 0x%08x", resID);
} else {
LOGW("No known package when getting name for resource number 0x%08x", resID);
}
return false;
}
if (t < 0) {
LOGW("No type identifier when getting name for resource number 0x%08x", resID);
return false;
}
const PackageGroup* const grp = mPackageGroups[p];
if (grp == NULL) {
LOGW("Bad identifier when getting name for resource number 0x%08x", resID);
return false;
}
if (grp->packages.size() > 0) {
const Package* const package = grp->packages[0];
const ResTable_type* type;
const ResTable_entry* entry;
ssize_t offset = getEntry(package, t, e, NULL, &type, &entry, NULL);
if (offset <= 0) {
return false;
}
outName->package = grp->name.string();
outName->packageLen = grp->name.size();
outName->type = grp->basePackage->typeStrings.stringAt(t, &outName->typeLen);
outName->name = grp->basePackage->keyStrings.stringAt(
dtohl(entry->key.index), &outName->nameLen);
// If we have a bad index for some reason, we should abort.
if (outName->type == NULL || outName->name == NULL) {
return false;
}
return true;
}
return false;
}
...
追踪了下native层的代码,发现并没有使用mSystem这个变量,那说明:系统资源的获取并不是通过mSystem这个实例来获取的;
回顾之前的AssetManager,两个构造方法里面都调用了native的init方法:
android_util_AssetManager.cpp
...
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem)
{
if (isSystem) {
verifySystemIdmaps();
}
AssetManager* am = new AssetManager();
if (am == NULL) {
jniThrowException(env, "java/lang/OutOfMemoryError", "");
return;
}
am->addDefaultAssets();
ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);
env->SetLongField(clazz, gAssetManagerOffsets.mObject, reinterpret_cast(am));
}
...
走到AssetManager.cpp中:
AssetManager.cpp
...
static const char* kSystemAssets = "framework/framework-res.apk";// 系统资源文件
...
bool AssetManager::addDefaultAssets()
{
const char* root = getenv("ANDROID_ROOT");
LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");
String8 path(root);// 获取系统root目录,即 /system/
path.appendPath(kSystemAssets);// 拼接得到 /system/framework/framework-res.apk
return addAssetPath(path, NULL);// 添加系统资源路径
}
...
果然是在这个native的init里面添加系统资源的,找一个root过的手机进到这个路劲下看了下:
原来系统资源就是这么简单的添加进去的,那么这个addAssetPath在Java层有没有提供入口呢,反过来看AssetManager的代码:
AssetManager.java
...
/**
* 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) {
synchronized (this) {
int res = addAssetPathNative(path);
makeStringBlocks(mStringBlocks);
return res;
}
}
...
果然有这个方法,虽然是hide的,但是有反射,就不成问题。
那么既然可以通过添加一个APK的路径到AssetManager中,那么插件资源的获取自然也就不是问题了,
APNP就是通过这种方法,添加插件的APK路径,并且更换相应的AssetManager来获取插件资源的。
下面我继续分析DexClassLoader的疑问(DexClassLoader的构造方法中,dexPath可以传APK路径?)
(二)DexClassLoader加载过程
说到DexClassLoader,单纯Java层的使用,其实并没有太多内容可说,但是基于Dalvik虚拟机(4.4出现ART)虚拟机如何加载class文件,
倒是有些内容需要大家提前了解下,也正是这块的内容决定了Android插件化的可行性。
1,Android的安装文件APK到底是啥?
APK即Android安装包文件,但是对于Android虚拟机来说,APK并不能直接执行,需要在运行的时候将APK文件中的DEX翻译成机器码再执行,
应用程序安装的时候,虚拟机会抽取apk中的dex文件,并且进行一定的优化(即dexopt,ART下为dex2oat),最后生成odex文件(即optimized dex);
在运行的时候,则通过DexClassLoader加载优化过后的odex文件翻译成机器码后执行;
apk中还包括了Android的资源文件.arsc,资源文件的加载就是上面的AssetManager来操作的,这里不再详情描述,所以大家只要把APK文件理解成
Java类 + 资源文件的统一存储文件好了。
2,Dex文件过大无法运行?
随着业务的迭代,大家可能都遇到了“65535”这个问题,这个是Dalvik虚拟机的限制,也就是说方法、类、属性值过多了,虚拟机记录不了了,
所以在生成DEX文件的时候,如果方法、类、属性数量超过就抛出异常。
后来Google通过MultiDex解决了这个问题,即一个APK文件不再是只有一个dex文件,可以有多个dex,也正是这项技术的出现,让插件化技术浮出水面。
(其实MultiDex只是给开发者提供了一种多dex的方案,翻读1.6的DexClassLoader源码就可以发现,那个时候插件化就可以做了)
3,插件化类加载的最终原理
既然虚拟机是通过DexClassLoader来加载类的,我们先来看一下这个DexClassLoader(android 4.0)
DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
很显然,DexClassLoader只是一个空壳子,其实在早些版本的时候,几乎加载类的所有逻辑都在DexClassLoader中,只是4.0在原来的基础上,
让代码结构更合理,更优化了,继续看BaseDexClassLoader:
BaseDexClassLoader.java
...
public class BaseDexClassLoader extends ClassLoader {
...
private final DexPathList pathList;
...
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
...
}
在上面的findClass中,BaseDexClassLoader是通过全局变量DexPathList来寻找目标类的,继续看DexPathList:
DexPathList.java
...
/*package*/ final class DexPathList {
...
private final Element[] dexElements;
...
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
this.dexElements =
makeDexElements(splitDexPath(dexPath), optimizedDirectory);
...
}
...
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
...
/*package*/ static class Element {
...
public final DexFile dexFile;
public Element(File file, ZipFile zipFile, DexFile dexFile) {
...
this.dexFile = dexFile;
}
...
}
}
到这里,似乎已经很明显了,DexPathList这有一个Element列表,每个Element中包含一个DexFile,如果大家去看下makeDexElements的源码就会发现:
每个DexFile都对应一个Dex文件!
DexFile最终会通过一个native方法加载目标类,我们无须关注这个native方法是如何加载的,到这里我们来看下APK和BaseDexClassLoader的对应关系:
虚拟机最终都会通过DexPathList来加载class,通过DexPathList的findClass方法可以知道,它正是通过遍历每个DexFile来寻找目标class的。
那么现在我们思考一下,假如我们想加载一个其他的APK文件中的类,怎么办?
返回到我们最初的疑问,DexClassLoader是可以自己构造的,并且必须需要传入一个APK的路径,并通过一些列的解析,把APK文件最终分解成一个DexFile数组,
既然这样,假如我们构造一个其他APK的DexClassLoader,然后它里面的DexFile数组合并到系统的DexClassLoader中去,这样系统不就可以找到其他APK的类了么!
基本操作如下:
通过把我们自己构造出来的DexClassLoader合并到系统的Classloader中,我们就可以成功的加载一个未安装的APK文件中的class了,而这正是MultiDex的基本原理!
(其实,如果上面把插件的DexFile放到列表的前面,还可以当热修复来使用,大家可以简单思考下~)