美文网首页
Android插件化原理

Android插件化原理

作者: JxMY | 来源:发表于2018-03-01 15:21 被阅读0次

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

    相关文章

      网友评论

          本文标题:Android插件化原理

          本文链接:https://www.haomeiwen.com/subject/wvggxftx.html