Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 381|回复: 0

[Android Pro] so 动态加载—解决sdk过大问题

[复制链接]
  • TA的每日心情
    奋斗
    4 天前
  • 签到天数: 789 天

    [LV.10]以坛为家III

    2049

    主题

    2107

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    722638
    发表于 2021-6-21 02:34:40 | 显示全部楼层 |阅读模式

    原文地址: https://blog.csdn.net/Rong_L/article/details/75212472

    前言

    相信Android 开发中大家或多或少都会集成一些第三方sdk, 而其中难免要会使用到他们的so文件。但有时,你会发现这些so文件过多,对于一些需要经常更新的应用来说,这将会大大浪费用户的流量。而有些sdk的集成仅仅是只为了一个不是必须的功能,我们完全有充足的理由用一些技术的手段来解决因这部分sdk集成带来的安装包大小问题。

    so目录

    观察发现,很多sdk的大小主要集中在so文件上。为了尽可能多的适应不同cup,sdk通常会提供不同二进制文件,这些文件被分门别类地放在armeabi,x86,mips等目录下。这里我们有必要了解下这些目录的含义。

    目录 cpu类型
    armeabi ARM 通用cpu
    armeabi-v7a 支持浮点运算ARM cpu,向下兼容armeabi
    arm64-v8a ARM 64位cpu, 向下兼容armeabi-v7a
    x86 x86通用型cpu
    x86_64 x86 64位cpu
    不同cpu在apk应用安装时,会查找对应的目录,比如,arm64位机子,会优先查看apk中是否有arm64-v8a目录,如果有,则采用该目录下的so文件,如果没有,则会查找兼容的目录。一旦确定下目录之后,其他的目录便不会再去管了。(日后如果在确定的目录下没有找到对应的so文件,也不会去其他目录中找到)

    目前市面上大部分手机都兼容armeabi-v7a,哪怕x86的cpu也会兼容(性能会有损耗)。所以armeabi-v7a目录建议一定配置,其相比armeabi在性能上有很大的提升。

    动态加载so

    再次回到前言中的问题,我们有没有什么办法能够减少so的大小,从而减少apk安装包的大小呢?
    1. 如果不太在意性能的损耗,那么我们完全可以只适配armeabi-v7a包和x86包,让64位机器运行32位的so文件。
    2. 单独出arm版本和x86版本,这样也可以减少一半的so大小。

    可如果你觉得这样包还是太大,比如我们现在用的crosswalk浏览器内核,单个so文件就达到了27M,同时适配x86的话,会达到58M, 这是我们所无法接受的事情!

    于是乎开始想有没有什么办法能把so文件与apk文件分离开来,在程序运行的时候来把so文件下载下来,并引导程序去加载。从而实现动态加载so文件的目的。

    System.load 与 System.loadLibrary

    google出的结果直接导向了System.load和System.loadLibrary这两个方法。
    system.load 参数中加载的so的路径,比如:system.load(“/data/data/com.codemao.android/libs/libcrosswalk.so”)
    system.loadLibrary参数中传入的是so的名称,比如system.loadLibrary(“crosswalk”), 系统会自动根据名称与机器的cpu型号,找到对应的so目录,并加载对应的lib crosswalk.so文件。
    (两者文件都只能在app的私有目录下)

    那这样子的话,是不是我们从远程下载完so文件之后,解压到app私有目录下,在调用sdk的地方调用system.load主动加载so之后,就可以实现动态加载so文件了呢?

    同学,你真是太天真啦!我们回想下自己写的so文件是如何调用的?是不是在需要使用的类里主动调了system.loadLibrary呢?sdk也一样,sdk在自己的代码里主动调用了system.loadLibrary。而这时,我们so文件因为没有随着apk安装到手机上,并不在它的寻找范围之内,最后的结果是你即使调用了system.load加载了so文件,理论是可以找到相应的native方法了,但是sdk在调用system.loadLibrary时会抛出找不到对应的so文件的错误。

    插件化如何处理so

    这该如何处理sdk内部调用loadLibrary抛出的异常信息呢?apk内的so文件最终被放到了/data/app/com.codemao.android/lib/下面,我们总不能把远程下载下来的so文件放入这里吧,可/data/app这个目录下面的文件我们是没有权限去执行读写操作的。

    这里我们想到了另一个问题,插件化可以运行另一个apk,而apk里面难免会有so,那宿主程序又是如何处理插件的so文件呢?
    查询之后发现:原文地址

    有时候我们在开发插件的时候,可能会调用so文件,一般来说有两种方案:
    一种是在加载插件的时候,先把插件中的so文件释放到本地目录,然后在把目录设置到DexClassLoader类加载器的nativeLib中。
    一种在插件初始化的时候,释放插件中的so文件到本地目录,然后使用System.load方法去全路径加载so文件
    这两种方式的区别在于,
    第一种方式的代码逻辑放在了宿主工程中,同时so文件可以放在插件的任意目录中,然后在解压插件文件找到这个so文件释放即可。
    第二种方式的代码逻辑是放在了插件中,同时so文件只能放在插件的assets目录中,然后通过把插件文件设置到程序的AssetManager中,最后通过访问assets中的so文件进行释放。

    我们自己apk使用的classloader是pathclassloader, 那我们是不是只要把so所在的目录加入到pathclassloader的nativeLib之中就好了呢? 
    让我们再次来看下system.loadLibrary:

    public static void loadLibrary(String libname) {  
        Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);  
    } 

    Runtime.java

    synchronized void loadLibrary0(ClassLoader loader, String libname) {  
        if (libname.indexOf((int)File.separatorChar) != -1) {  
            throw new UnsatisfiedLinkError(  
    "Directory separator should not appear in library name: " + libname);  
        }  
        String libraryName = libname;  
        //loader这里传入的是pathclassloader, 不为空
        if (loader != null) {  
            //调用findLibrary找到so路径
            String filename = loader.findLibrary(libraryName);  
            if (filename == null) {    
                throw new UnsatisfiedLinkError(loader + " couldn't find \"" +  
                                               System.mapLibraryName(libraryName) + "\"");  
            }  
            //调用doLoad加载找到的so文件
            String error = doLoad(filename, loader);  
            if (error != null) {  
                throw new UnsatisfiedLinkError(error);  
            }  
            return;  
        }
        ...//以下逻辑我们可以暂且忽略
        }

    好的,这里我们看到加载的过程主要两步: 
    1. 调用pathclassloader.findLibrary,先找到对应的so文件 
    2. 调用doLoad加入找到的so文件

    那我们来看下classloader.findLibrary是如何找到对应so文件的:

    @Override  
    public String findLibrary(String name) {  
        return pathList.findLibrary(name);  
    }

    pathList 是BaseDexClassLoader 里的DexPathList对象(注意6.0 开始nativeLibraryDirectories放的不在是File, 不过加载逻辑是一样的, 要注意适配。)

    public String findLibrary(String libraryName) {
            String fileName = System.mapLibraryName(libraryName);
            for (File directory : nativeLibraryDirectories) {
                File file = new File(directory, fileName);
                if (file.exists() && file.isFile() && file.canRead()) {
                    return file.getPath();
                }
            }
            return null;
        }

    这里主要做的事:
    1. 调用system.mapLibraryName, 补全名称, 如比libraryName=crosswalk, 补全之后会是lib crosswalk.so
    2. 遍历nativeLibraryDirectories,看下目录下面有对应的文件吗

    哈哈,到这里,机会来了,我们只要把远程下载so的目录通过反射的方式放入nativeLibraryDirectories中就ok啦,真是太激动啦!!!

    适配与实现方案

    为了尽量减少性能损耗,我们先根据cpu的类型确定自己要下载的so文件,之后再用反射的方式把so的目录加入到classloader中,这样便可以解决so过大而引起apk包过大的问题。

    但我们前面说过,6.0之后的DexPathList与6.0之前的DexPathList不一样,这里要注意适配的问题,

    6.0之后findLibrary 变为了:

     public String findLibrary(String libraryName) {
            String fileName = System.mapLibraryName(libraryName);
            for (Element element : nativeLibraryPathElements) {
                String path = element.findNativeLibrary(fileName);
                if (path != null) {
                    return path;
                }
            }
            return null;
    }

    6.0和之前的:

    public String findLibrary(String libraryName) {
            String fileName = System.mapLibraryName(libraryName);
            for (File directory : nativeLibraryDirectories) {
                File file = new File(directory, fileName);
                if (file.exists() && file.isFile() && file.canRead()) {
                    return file.getPath();
                }
            }
            return null;
        }

     

    Element 中的代码如下:

    public String findNativeLibrary(String name) {
                maybeInit();
                if (isDirectory) {
                    String path = new File(dir, name).getPath();
                    if (IoUtils.canOpenReadOnly(path)) {
                        return path;
                    }
                } else if (zipFile != null) {
                    String entryName = new File(dir, name).getPath();
                    if (isZipEntryExistsAndStored(zipFile, entryName)) {
                      return zip.getPath() + zipSeparator + entryName;
                    }
                }
                return null;
     }

    所以我这里直接给出适配好的关键代码,供大家参考

    /**
    * 将 so所在的目录放入PathClassLoader里的nativeLibraryDirectories中
    *
    * @param context
    */
    public void installSoDir(Context context) {
    
        //安卓4.0以下不维护
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            return ;
        }
        File soDirFile = context.getDir(soDir, Context.MODE_PRIVATE);
        if(!soDirFile.exists()) {
            soDirFile.mkdirs();
        }
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            v23Install(soDirFile, context);
        } else {
            v14Install(soDirFile, context);
        }
    }
    
    private void v14Install(File soDirFile, Context context) {
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            Object pathList = getPathList(pathClassLoader);
            if(pathList != null) {
                //获取当前类的属性
                try {
                    Field nativeLibraryDirectoriesField = pathList.getClass().getDeclaredField("nativeLibraryDirectories");
                    nativeLibraryDirectoriesField.setAccessible(true);
                    Object list = nativeLibraryDirectoriesField.get(pathList);
                    if(list instanceof List) {
                        ((List) list).add(soDirFile);
                    } else if(list instanceof File[]) {
                        File[] newList = new File[((File[]) list).length + 1];
                        System.arraycopy(list, 0 , newList, 0, ((File[]) list).length);
                        newList[((File[]) list).length] = soDirFile;
                        nativeLibraryDirectoriesField.set(pathList, newList);
                    }
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    
    private void v23Install(File soDirFile, Context context) {
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            Object pathList = getPathList(pathClassLoader);
            if(pathList != null) {
                //获取当前类的属性
                try {
                    Field nativeLibraryPathField = pathList.getClass().getDeclaredField("nativeLibraryPathElements");
                    nativeLibraryPathField.setAccessible(true);
                    Object list = nativeLibraryPathField.get(pathList);
                    Class<?> elementType = nativeLibraryPathField.getType().getComponentType();
                    Constructor<?> constructor = elementType.getConstructor(File.class, boolean.class, File.class, DexFile.class);
                    constructor.setAccessible(true);
                    Object element = constructor.newInstance(soDirFile, true, null, null);
                    if(list instanceof List) {
                        ((List) list).add(element);
                    } else if(list instanceof Object[]) {
                        Object[] newList = new File[((Object[]) list).length + 1];
                        System.arraycopy(list, 0 , newList, 0, ((Object[]) list).length);
                        newList[((Object[]) list).length] = element;
                        nativeLibraryPathField.set(pathList, newList);
                    }
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    private Object getPathList(Object classLoader) {
            Class cls = null;
            String pathListName = "pathList";
            try {
                cls = Class.forName("dalvik.system.BaseDexClassLoader");
                Field declaredField = cls.getDeclaredField(pathListName);
                declaredField.setAccessible(true);
                return declaredField.get(classLoader);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            return null;
        }

    参考文章

    Android中so使用知识和问题总结以及插件开发过程中加载so的方案解析
    Android项目针对libs(armeabi,armeabi-v7a,x86)进行平台兼容
    Android JNI之System.loadLibrary()流程

     

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-9-10 18:49 , Processed in 1.053064 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表