美文网首页安卓集中营
动态加载的一些坑

动态加载的一些坑

作者: 妖怪来了 | 来源:发表于2019-03-13 22:45 被阅读1次

    背景

    前一段时间,做了一个需求,需要动态加载一个so,还有一个classes.dex,还有一些资源。看上去是一个还行的需求,原理就是通过 classloader 进行动态加载,知易行难,真正做起来,还是遇到了下面的这些坑。

    问题

    0x01类冲突

    什么是类冲突呢?就是说我们的代码中可能有两个一模一样的类,包名,类名都一模一样。有人可能会问,怎么会有这种情况呢?因为模块走的动态加载,没有走统一编译,这种问题就会变得无法避免。难免有人脑子想到一起,就产生了重复的类了。

    众所周知,java是通过classloader进行类加载的,类加载机制就是著名的双亲委派,不太了解的同学,我简单描述一下就是:如果有一家三代,就先去爷爷那里找有没有这个类,如果没有就去爸爸那里找,爸爸找不到就从儿子这里找,儿子找不到就 ClassNotFoundException 了。 所以,当我们进行动态加载的时候,一般都是使用 DexClassLoader (关于如何动态加载,这里不多说,网上文章很多),这个DexClassLoader会把参数里面的路径下的dex文件加载起来,那么你的类就可以通过这个 classloader 进行加载了。

    这个时候,问题就来了,如果有重名的类,已经加载过了,那么,你肯定就加载不到你自己的类了,这样加载到的类就不是你想要的那个类,错误就产生了。如何避免呢?先看如下代码:

    public class CustomClassLoader extends DexClassLoader {
        private ClassLoader mParentClassLoader;
        public CustomClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
            super(dexPath, optimizedDirectory, libraryPath, new ClassLoader() {
                @Override
                public Class<?> loadClass(String name) throws ClassNotFoundException {
                    return null;
                }
            });
            mParentClassLoader = parent;
        }
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            Class<?> clazz = null;
            try {
                clazz = super.loadClass(name);
            } catch (Exception ex) {
                // ignore
            }
            if (clazz != null) {
                return clazz;
            }
            if (mParentClassLoader == null) {
                return null;
            }
            return mParentClassLoader.loadClass(name);
        }
        //....
    }
    

    可以看到,我们自定义了一个CustomClassLoader继承自DexClassLoader,有两个重点:

    • super() 调用的第三个参数传了一个重写了 loadClass 的方法,里面直接返回null,这个参数是父classloader的一起,这里就是把爸爸设置成一个什么都没有的 classloader。如果不设置,在安卓6.0以下都会报一个错误,父classloader不能为null的错误。
    • loadClass() 方法,先调用super.loadClass() 方法,出异常再调用传递进来的真正的爸爸classloader加载。

    通过这样一个逻辑,就能保证先加载自己的类,再去加载爷爷和爸爸那里的类了。这样即使内存里面已经有了这个类,通过这个加载逻辑也能加载成功自己的类了。不过这样就违背了java的双亲委派机制,不过这也是没有办法的事情,java自己也违背过,哈哈哈。

    0x02 资源加载不起来

    我们的classes.dex 和资源文件不是同一个apk,也就是说他们不是一起进行打包的,这就带来了另外一个问题,两边分开进行打包,资源id对不上。要解决这个问题,就要把我们的资源apk路径加载到系统寻找资源的路径上面来,关键方法如下:

        public static boolean addResource(Context context, String apkDir) {
            if (TextUtils.isEmpty(apkDir)) {
                return false;
            }
    
            try {
                Method m = getAddAssetPathMethod();
                Log.e("getAddAssetPathMethod m = " + m);
                if (m != null) {
                    int ret = (int) m.invoke(context.getAssets(), apkDir);
                    Log.e("invoke ret = " + ret);
                    return ret > 0;
                }
            } catch (Exception e) {
                Log.d("invoke method error ! ", e.toString());
            }
    
            return false;
        }
    
        private static Method getAddAssetPathMethod() {
            Method m = null;
            Class c = AssetManager.class;
    
            if (Build.VERSION.SDK_INT >= 24) {
                try {
                    m = c.getDeclaredMethod("addAssetPathAsSharedLibrary", String.class);
                    m.setAccessible(true);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                }
                return m;
            }
    
            try {
                m = c.getDeclaredMethod("addAssetPath", String.class);
                m.setAccessible(true);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
    
            return m;
        }
    

    然后自己构造一个 ContextThemeWrapper类,进行资源的查找。大致实现如下:

    public class ResourcesContext extends ContextThemeWrapper {
        private final ClassLoader mNewClassLoader;
        Resources mNewResources;
        public ResourcesContext(Context base, int themeres, ClassLoader cl, Resources r) {
            super(base, themeres);
            mNewResources = r;
            mNewClassLoader = cl;
        }
        @Override
        public Resources getResources() {
            if (mNewResources != null) {
                return mNewResources;
            }
            return super.getResources();
        }
    }
    

    通过传递进来的 mNewResources 进行资源的查找。最终使用这个类进行资源的查找,通过context去查找资源的方法如下:

    resourceContext.getString(R.xxx);
    

    必须通过这个resourceContext进行资源的查找。

    这样我们就解决了资源查找的问题,还有一个问题,就是资源id错乱对不上的问题。这个解决比较简单,就是把所有的id在初始化的时候统一进行一次重新赋值,让dex中的id都被赋值为资源apk中的id值。

    0x03 资源错乱

    在demo中运行良好,兴高采烈去客户端进行集成。一集成完毕,就发现app莫名奇妙的崩溃,很多资源找不到, 而且基本是什么资源都会崩溃。找了很久问题的根源,发现是资源id冲突。看来只能在我们自己编译资源apk的时候,进行资源id的修改了。那么aapt这个工具就闪亮登场了。在build.gradle中的android节点加入:

    aaptOptions {
            additionalParameters  "--package-id", "0x66","--allow-reserved-package-id"
        }
        buildToolsVersion '28.0.3'
    

    0x66 是自己定义的id,这样我们生成的资源就都是0x66开头的了,而系统默认都是 0x7f开头。注意此工具必须在高版本的gradle中才能使用。

    总结

    动态加载过程中,资源问题是最令人头痛的一个地方,好在也会有各种各样的办法去修复他。欢迎大家一起交流。

    相关文章

      网友评论

        本文标题:动态加载的一些坑

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