Android插件化原理

2018-03-01  本文已影响0人  JxMY

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) {

        // 这里抛出了NotFound异常,留意一下,先不管

        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放到列表的前面,还可以当热修复来使用,大家可以简单思考下~)

上一篇下一篇

猜你喜欢

热点阅读