1. 貨拉拉 Android 動態(tài)資源管理系統(tǒng)原理與實踐(下)

        共 22453字,需瀏覽 45分鐘

         ·

        2022-07-22 12:44

        點擊上方藍字關注我,知識會給你力量


        1. so資源動態(tài)化方案

        • so資源打包問題

        在打包so資源的過程中,我們遇到了如下問題。

        • 如何移除apk中的so文件,并將他們收集起來?
        • 如何將多個so文件壓縮打包,并生成對應的信息?
        • 如何保證第三方sdk缺少so文件時,不崩潰?
        • so資源打包解決方案

        • 移除并收集apk中的so文件

        看到移除 so文件可能有些同學會問,這不是只要在as中刪除libs目錄就搞定了么?這樣會有幾個問題

        • 對于多個module的工程,我們需要逐個刪除每個module下的libs目錄,麻煩而且容易出錯。
        • 對于三方aar包中的so文件,我們就沒法刪除了。
        • so文件變化需要人工維護,容易出錯。

        出于以上考慮,我們認為,在編譯時期,自動刪除并收集so文件是最優(yōu)解,那么在編譯時期進行以上操作呢?我們注意到as在進行build時,會有大量的系統(tǒng)提供的task在運行,那么這些系統(tǒng)task是否就完成了編譯并收集各個地方的so文件,并把他們打包進apk的任務呢?

        看一眼這幅超級復雜的apk構建流程圖,嗯,可以看到,系統(tǒng)確實會在apkBuilder構建前,將本地的c/c++文件編譯成so庫,并將第三方的so庫一起打包到apk中,我們需要尋找的就是收集所有so庫的系統(tǒng)Task

        通過查找資料,我們發(fā)現(xiàn),確實有2個系統(tǒng)task會用來處理合并so庫并且刪除debug符號(注意,task名稱可能與此處不完全相同)。

        Task名稱實現(xiàn)類作用結果保存目錄
        mergeDebugNativeLibsMergeNativeLibsTask合并所有依賴的 native 庫intermediates/merged_native_libs
        stripDebugDebugSymbolsStripDebugSymbolsTask從 Native 庫中移除 Debug 符號intermediates/stripped_native_libs
        • 一般來說,應該在stripSymbols結束后去剔除 stripped_native_libs 目錄下的文件。
        • 但是剔除debug符號操作,可能導致不同as版本得到的so文件md5碼不相同。
        • 因此,我們采用了可配置方案,可以由用戶配置決定,在MergeNativeLibsTask或者stripDebugDebugSymbols后,執(zhí)行刪除輸出文件夾中so文件操作。
        • 第三方 so 一般都是 Release 編譯出來的,不進行strip影響也不大。而我們自己的so文件,則strip操作可能會對so體積造成較大影響。
        • 下面我們以在MergeNativeLibsTask之后,執(zhí)行刪除輸出文件夾中so文件的方式,進行講解。

        由于我們有多個gradle task需要執(zhí)行,因此我們創(chuàng)建了一個名為dynamic_plugin的android plugin工程,內(nèi)部包含了多個gradle task。關于as中新建插件的方法,請自行搜索其他博客,本文因為篇幅問題,不進行講解。

        在我們的dynamic_plugin插件內(nèi)部,我們新建一個名為DeleteAndCopySo的gradle task并將它插入到系統(tǒng)的merge和strip之間,利用該Task完成刪除merged_native_libs目錄下對應so文件,并將其拷貝到我們指定的新目錄下。這樣apk打包時,就不會包含動態(tài)化的so文件了

        //獲取系統(tǒng)的mergeTask

        Task mergeNativeTask = TaskUtil.getMergeNativeTask(project);

        //獲取系統(tǒng)的skipTask

        Task stripTask = TaskUtil.getStripSymbol(project);

        //創(chuàng)建我們的DeleteAndCopySo task

        Task deleteTask = project.getTasks().create(PluginConst.Task.DELETE_SO);

        deleteTask.doLast(new Action<Task>() {

            @Override

            public void execute(Task task) {

                deleteAndCopySo(project, param);

            }

        });

        //將我們的Task插入到merge和strip之間

        stripTask.dependsOn(deleteTask);

        deleteTask.dependsOn(mergeNativeTask);
        • 如何將多個so文件壓縮打包,并生成對應的信息?

        上一步中,我們已經(jīng)將so文件從系統(tǒng)apk構建流程中刪除,并且拷貝到了指定目錄下。那么現(xiàn)在我們應該做什么呢?

        • 將so文件打包成.zip壓縮包。
        • 生成該資源對應的實體類DynamicPkgInfo。包括文件id,文件名稱,文件類型,版本號,下載地址等基本信息,以及文件md5,文件長度等校驗信息。以及壓縮包下的所有子文件及文件夾相關信息。
        • 將該zip文件上傳到服務器,以方便下載和使用。

        對于上述這些步驟,在我們的貨拉拉動態(tài)管理系統(tǒng)初始版本中,我們采用了自己打zip包,自己寫java代碼來生成資源信息的方式。

        但是在后來的使用過程中,我們發(fā)現(xiàn),手動進行這些步驟,很繁瑣且容易出錯,我們需要有一種自動化的方式進行上述過程。

        我們在dynamic_plugin插件內(nèi)部,再新增一個ZipSoTask來進行壓縮so文件夾,以及生成資源信息常量的操作。該task在DeleteAndCopySo之后,stripe系統(tǒng)task之前執(zhí)行。

        //執(zhí)行將so文件夾壓縮成.zip操作

        List<DynamicUtil.ZipInfo> zips = DynamicUtil.zipFolder(new File(param.getmInputSo()),outDir);

        //根據(jù)so文件和zip壓縮包信息,生成md5,length等校驗信息并存儲

        DynamicUtil.createPkgDatas(mPkgList,zips,PluginConst.Type.SO);

        //根據(jù)資源信息類生產(chǎn)java文件

        param.getmFileCreate().createFile(mPkgList,param);
        • 前2步,壓縮so文件,和根據(jù)so文件,zip文件生成校驗信息并存儲比較簡單,就不詳細說了。
        • 第3步,根據(jù)前面的信息,直接生產(chǎn)java文件,我們使用了第三方的開源庫javapoet。
        • JavaPoet 是 Square 公司推出的開源 Java代碼生成框架,提供接口生成 Java 源文件。這個框架功能非常有用,我們可以很方便的使用它根據(jù)注解、數(shù)據(jù)庫模式、協(xié)議格式等來對應生成代碼。通過這種自動化生成代碼的方式,可以讓我們用更加簡潔優(yōu)雅的方式要替代繁瑣冗雜的重復工作。
        • 大致的生產(chǎn)代碼如下,首先生成一個DynamicResConst類,之后遍歷zip壓縮資源列表,為列表中的每一個資源,生成一個static final的常量,表示每個資源,最后生成java文件。
        //創(chuàng)建DynamicResConst類,用來存儲資源實體常量

        TypeSpec.Builder typeBuilder = TypeSpec.classBuilder( "DynamicResConst" )

                .addModifiers(Modifier.PUBLIC, Modifier.FINAL);

        //遍歷資源列表,生成對應實體類DynamicPkgInfo        

        for (DynamicPkgInfo pkg : pkgs) {

            FieldSpec fsc = createField(pkg);

            typeBuilder.addField(fsc);

        }

        //插件java文件,并寫入

        JavaFile javaFile = JavaFile.builder(param.getmCreateJavaPkgName(), typeBuilder.build()).build();

        try {

            javaFile.writeTo(new File(param.getmOutputPath()));

        } catch (Exception e) {

            

        }

        至于最后一步,將so壓縮包上傳到服務器,我們在配置文件中提供了一個上傳方法,不過默認實現(xiàn)為空,用戶可以手動上傳也可以修改默認方法實現(xiàn)自動上傳。自動生成的資源文件中,版本號需要手動修改控制,下載地址手動上傳的話,也需要手動修改。

        • 保證第三方sdk在缺少so文件時,不崩潰

        很多三方sdk都要求在應用啟動時,進行初始化,一個使用so庫的類的典型類代碼如下:

        public class ThirdLib{

        //靜態(tài)方法加載so庫

        static{

            System.loadLibrary("third");

        }

        //native方法示例

        public native void testNative();

        //java方法示例

        public void test();

        //......其他內(nèi)容省略

        }
        • 如果此時so庫沒有被加載好,直接使用ThirdLib類,則會執(zhí)行static代碼段中的System.loadLibrary方法,導致UnsatisfiedLinkError的錯誤,造成App崩潰。由于我們無法直接修改第三方sdk的源碼,因此我們只能采用動態(tài)字節(jié)碼技術,替換掉System.loadLibrary方法了。
        • 我們采用android的transform加asm技術,動態(tài)的將System.loadLibrary替換成我們自己的SoLoadUtil中的loadLibrary方法。
        • Gradle Transform 是 Android 官方提供給開發(fā)者在項目構建階段,即由 .class 到 .dex 轉(zhuǎn)換期間修改 .class 文件的一套 API, 無論是class還是jar都可以控制。
        • ASM是一種通用Java字節(jié)碼操作和分析框架。它可以用于修改現(xiàn)有的class文件或動態(tài)生成class文件。

        具體執(zhí)行替換的代碼如下,在Asm框架中的MethodVisitor類中,重寫visitMethodInsn方法,判斷該方法的擁有者,名稱和參數(shù)列表和System.loadLibrary對應,則我們將他替換為我們的SoLoadUtil.loadLibrary方法

        @Override

        public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {



            if(TextUtil.equals(owner, PluginConst.SYSTEM_CLASS) &&

                    TextUtil.equals(name, PluginConst.LOAD_LIBRARY_METHOD) &&

                    TextUtil.equals(descriptor, PluginConst.LOAD_LIBRARY_DESC)){

                owner = "com/xxx/xxx/dynamicres/util/SoLoadUtil" ;

                mv.visitMethodInsn(Opcodes.INVOKESTATIC, owner, name, descriptor, false);

                return;

            }

            mv.visitMethodInsn(opcode, owner, name, descriptor, isInterface);

        }

        替換后的方法主要邏輯為,使用第三方庫Relinker替代System.loadLibrary方法進行so文件加載,并且catch住加載異常,來防止應用直接奔潰,并且在加載so庫異常時,將該庫的名稱保存下來,在我們的so包被正常下發(fā)加載后,再次調(diào)用本方法,將so庫load到系統(tǒng)中。

        protected void realSoLoad(Context c, String libName) {

         try {

         ReLinker. recursively ().loadLibrary(c, libName);

         removeFormWaitList(libName);

         } catch (Throwable t) {

         addToWaitList(libName);

         }

         }
        • 對于so庫沒有加載完成,直接使用ThirdLib類導致System.loadLibrary方法被調(diào)用,導致的應用崩潰問題,我們已經(jīng)解決了。
        • 而對于直接調(diào)用ThirdLib類的testNative方法,導致的應用崩潰問題,則無法解決。因此需要看情況決定是否能夠接受該種崩潰,以及是否將引發(fā)該問題的so庫進行動態(tài)化。

        我們只需要在工程的主Application中,直接調(diào)用loadSo方法,對so動態(tài)資源進行加載。加載完成后,so庫就能正常使用了。



        public void loadSo(DynamicSoInfo soInfo, ILoadSoListener listener) {

            if (soInfo == null) {

                return;

            }

            //根據(jù)本機abi,獲取適合的動態(tài)資源實體類DynamicPkgInfo

            DynamicPkgInfo pkg = soInfo.getPkgInfo(Build.SUPPORTED_ABIS);

            if (pkg == null) {

                return;

            }

            //如果該so資源,已經(jīng)被加載緩存過了,直接listener的成功回調(diào),并返回

            if (isLoadAndDispatchSo(pkg, listener)) {

                return;

            }

            //開啟資源加載,和普通資源流程一致

            DynamicResManager manager = DynamicResManager.getInstance();

            manager.load(pkg, new DefaultLoadResListener() {

                @Override

                public void onSucceed(LoadResInfo info) {

                    super.onSucceed(info);

                    //so成功下載校驗后,執(zhí)行加載邏輯

                    handleLoadSoSucceed(pkg, info, listener);

                }

            });

        }
        • so資源加載和應用問題

        在so資源的加載和應用過程中,我們發(fā)現(xiàn)了如下問題

        • 如何判斷系統(tǒng)需要哪些so文件,并按需正確加載?
        • 如何下載so文件,并保證它的正確性?
        • 如何將下載的動態(tài)so文件,正確應用到系統(tǒng)中?
        • so資源加載和應用解決方案

        • 如何判斷系統(tǒng)需要哪些so文件,并正確下載安裝?

        我們把arm64-v8a,armeabi-v7a等abi分開打包,上傳到服務器。使用時,本地判斷abi支持,下載對應的abi包。這樣做的優(yōu)點是節(jié)省流量和下載后占據(jù)的空間。

        至于判斷系統(tǒng)需要哪些abi的so包,并按需正確應用,則比較簡單,讀取系統(tǒng)的SUPPORTED_ABIS常量,這里包含了系統(tǒng)支持的abi列表,而排在前面的表示優(yōu)先級更高。我們只要遍歷它,然后查找我們的動態(tài)資源包是否有匹配,就達到了正確加載的目標。



        private Map<String,DynamicPkgInfo> mSoInfos;



        public DynamicPkgInfo getPkgInfo(){

            //獲取本地系統(tǒng)支持的abi列表

            String[] supportAbis = Build.SUPPORTED_ABIS;

            if(supportAbis==null || supportAbis.length== 0 ){

                return null;

            }

            //遍歷abi支持列表

            for(String abi : supportAbis){

                //從so動態(tài)資源中,查找對應的abi信息

                DynamicPkgInfo pkg = mSoInfos.get(abi);

                //找到則直接返回該信息

                if(pkg != null){

                    return pkg;

                }

            }

            return null;

        }
        • 如何下載so文件,并保證它的正確性?

        復用通過資源加載流程即可。

        • 如何將下載的動態(tài)so文件,正確應用到系統(tǒng)中?

        這里需要首先了解一下,系統(tǒng)加載so庫的工作流程,當我們調(diào)用 System#loadLibrary("xxx" ) 后,Android Framework 都干了些了啥?Android 的 so 加載機制,大致可以分為以下四個環(huán)節(jié)。

        • 安裝 APK 包的時候,PMS 根據(jù)當前設備的 abi 信息,從 APK 包里拷貝相應的 so 文件。
        • 啟動 APP 的時候, Android Framework 創(chuàng)建應用的 ClassLoader 實例,并將當前應用相關的所有 so 文件所在目錄注入到當前 ClassLoader 相關字段。
        • 調(diào)用 System.loadLibrary("xxx"), framework 從當前上下文 ClassLoader 實例(或者用戶指定)的目錄數(shù)組里查找并加載名為 libxxx.so 的文件。
        • 調(diào)用 so 相關 JNI 方法。

        而我們這里,由于so文件不存在于apk當中,而是需要動態(tài)下載,所以我們顯然不能直接使用系統(tǒng)的System.loadLibrary方法加載so文件。

        而動態(tài)加載so的方法,在熱修復和插件化框架中,已經(jīng)比較成熟了,我們參考了市面上的開源框架后,選擇了騰訊的Tinker框架的加載方案,即使用反射classloader 將 so 包的路徑寫入 nativeLibraryPathElements 數(shù)組最前面,其流程圖和解釋如下圖所示 。注意,此方法不同的android版本將有不同的實現(xiàn)。下面示例代碼基于android9.0版本。



                private static void install(ClassLoader classLoader, File soFolder) throws Throwable {

         Field pathListField = findField(classLoader, "pathList" );

                    Object dexPathList = pathListField.get(classLoader);

                    Field nativeLibraryDirectories = findField(dexPathList, "nativeLibraryDirectories" );

                    List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);

                    libDirs.add(0, soFolder);

                    Field systemNativeLibraryDirectories =

                            findField(dexPathList, "systemNativeLibraryDirectories" );

                    List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);

                    Method makePathElements =

                            findMethod(dexPathList, "makePathElements" , List.class);

                    libDirs.addAll(systemLibDirs);

                    Object[] elements = (Object[]) makePathElements.

                            invoke(dexPathList, libDirs);

                    Field nativeLibraryPathElements = findField(dexPathList, "nativeLibraryPathElements" );

                    nativeLibraryPathElements.setAccessible(true);

                    nativeLibraryPathElements.set(dexPathList, elements);

                }
        • pathList變量:DexPathList類的實例。
        • nativeLibraryDirectories列表:包含了本App自帶so文件的查找路徑(如data/app/包名/lib/arm64)
        • systemNativeLibraryDirectories列表:包含系統(tǒng)so文件查找路徑(如system/lib64)
        • makePathElements:系統(tǒng)使用此方法,為所有so文件,生成對應的 NativeLibraryElement對象
        • nativeLibraryPathElements數(shù)組:系統(tǒng)用來存儲所有的so文件路徑

        當外界調(diào)用System.loadLibrary方法時,系統(tǒng)最終會調(diào)用到DexPathList類的findLibrary方法,該方法會在nativeLibraryPathElements數(shù)組中查找對應的路徑,我們將自己的so加入到nativeLibraryPathElements最前面,由此達到動態(tài)加入so的目標。

        1. so資源動態(tài)化的tips

        • 為何要使用Relinker加載So文件

          • 假如我們有2個so文件,libA.so 和 libB.so,libA依賴libB,則當我們調(diào)用System.loadLibrary("libA") 的時候,android framework 會通過上面提到的調(diào)用鏈最終通過 dlopen 加載 libA.so 文件,并接著通過其依賴信息,自動使用 dlopen 加載 libB.so。
          • 在 Android N 以前,只要將 libA.so 和 libB.so 所在的文件目錄路徑都注入到當前 ClassLoader 的 nativeLibraryPathElements 里,則在加載 so 插件的時候,這兩個文件都能正常被找到。
          • 從 N 開始,libA.so 能正常加載,而 libB.so 會出現(xiàn)加載失敗錯誤。
          • 因為Android Native 用來鏈接 so 庫的 Linker.cpp dlopen 函數(shù) 的具體實現(xiàn)變化比較大(主要是引入了 Namespace 機制):以往的實現(xiàn)里,Linker 會在 ClassLoder 實例的 nativeLibraryPathElements 里的所有路徑查找相應的 so 文件。
          • 更新之后,Linker 里檢索的路徑在創(chuàng)建 ClassLoader 實例后就被系統(tǒng)通過 Namespace 機制綁定了,當我們注入新的路徑之后,雖然 ClassLoader 里的路徑增加了,但是 Linker 里 Namespace 已經(jīng)綁定的路徑集合并沒有同步更新,所以出現(xiàn)了 libA.so 文件能找到,而 libB.so 找不到的情況。
          • 至于 Namespace 機制的工作原理了,可以簡單認為是一個以 ClassLoader 實例 HashCode 為 Key 的 Map,Native 層通過 ClassLoader 實例獲取 Map 里存放的 Value(也就是 so 文件路徑集合)。

        解決該問題有如下幾種思路:

        • 自定義 System.loadLibrary,加載 SO 前,先解析 SO 的依賴信息,再遞歸加載其依賴的 SO 文件,這是開源庫soLoader的解決方案。
        • 自定義 Linker,完全自己控制 SO 文件的檢索邏輯 ,這是開源庫Relinder的解決方案。
        • 替換 ClassLoader 。

        本著不重復造輪子的原則,項目中使用了Relinker開源庫,用來加載so文件。

        • so庫依賴分析工具

        想要把 so 動態(tài)化技術應用到 APK 的瘦身項目中來,除了分析哪些 so 文件體積占比比較大之外,最好的做法是將其依賴的所有 so 文件一定挪到插件包里。怎么了解 APK 里所有 so 文件具體的依賴信息呢?這里推薦一款 Google 開源的 APK 解析工具android-classyshark,除了提供分析 APK dex/so 依賴信息之外,它還提供了 GUI 可視化界面,非常適合快速上手。

        1. so動態(tài)化流程

        • so資源應用流程

        • 獲取系統(tǒng)支持abi列表,根據(jù)該列表,找到合適的so動態(tài)資源實體類。
        • 如果該資源已經(jīng)被加載緩存,則直接回調(diào)加載成功。
        • 否則,開始資源通用加載流程,并異步等待資源加載成功(流程見第5章)。
        • 再次判斷下載校驗后的資源,是否支持本機abi。
        • 將so包路徑加入DexPathList的數(shù)組頭部。
        • 遍歷等待加載so列表,嘗試加載所有so文件,并將成功加載的so文件,移除該列表。
        • 將資源id和本地路徑加入緩存,防止so被重復加載。
        • 回調(diào)加載完成監(jiān)聽器。
        • SoLoadUtil.loadLibrary方法流程

        從上一章我們知道,我們會使用transform api加asm框架,將系統(tǒng)的System.loadLibrary方法替換成我們的SoloadUtil.loadLibrary方法。我們替換系統(tǒng)方法的目的。一個是為了保證so庫不存在時,程序不崩潰,另外一個就是so庫下載校驗完成后,能自動完成之前失敗的加載,為此,我們設計了如下流程。

        • 其他方法調(diào)用到我們的SoloadUtil時,我們判斷我們的加載系統(tǒng)是否初始化完成
        • 已完成,則調(diào)用Relinkder庫嘗試加載so文件,未完成則將該so庫加入待加載隊列中。
        • 如果Relinker加載so文件成功,我們從待加載隊列中移除so,并且完成本次加載。
        • 否則我們依然將so文件加入待加載隊列中。
        • 根據(jù)上面的so加載流程,當so動態(tài)資源真正下載校驗完成后,我們會遍歷待加載隊列,并完成所有之前未成功的so庫加載。
        1. dynamic_plugin插件流程

        • 整體流程

        前面我們已經(jīng)分析了通用資源加載,內(nèi)置資源應用,完成了動態(tài)資源管理系統(tǒng)的主要部分。只剩下資源打包部分了,而所有資源的打包操作,都由dyanmic_plugin插件來完成。為了完成打包功能,我們決定在這個dynamic_plugin插件內(nèi)部,新建3個Task。

        • Hook System.loadLibrary方法的TransformTask。
        • 系統(tǒng)打包流程中,刪除并拷貝so文件的DeleteAndCopySoTask。
        • 壓縮so資源和其他多個文件資源(例如幀動畫)的ZipResTask。
        • 為每個動態(tài)資源生成其對應的DynamicPkgInfo常量的功能,僅實現(xiàn)為一個普通方法。

        所以主流程也就出來了

        • 讀取并解析dynamic_plugin.gradle配置文件。
        • 根據(jù)配置信息,決定是否將3個task加入任務隊列。
        • 啟動任務隊列。
        • TransformTask流程

        該task流程,主要就是通過tranform api和asm框架的使用,我們在其中加入了掃描class范圍的可配置項。

        • 等待asm框架掃描class。
        • 判斷該class名稱是否在我們配置的替換列表中,如果不在,就直接返回。
        • 創(chuàng)建ClassVisitor和MethodVisitor,等待asm框架掃描每個方法。
        • 如果該方法的名稱,參數(shù)列表和調(diào)用者,都和System.loadLibrary方法相符合。
        • 我們替換為自己的SoloadUtil.loadLibrary方法。
        • DeleteAndCopySoTask流程

        • 根據(jù)配置文件,找到系統(tǒng)的merge和strip task。
        • 將我們的task插入到2個系統(tǒng)task之間,并等待系統(tǒng)回調(diào)我們的doLast方法。
        • 遍歷系統(tǒng)的mergeTask的輸出目錄,判斷該so文件是否在我們配置的待掃描列表中。
        • 如果配置了需要拷貝so文件,則我們將它拷貝到指定位置。
        • 如果配置了需要刪除so文件,則我們將該so文件刪除。
        • ZipResTask流程

        • 拷貝字體文件,將文件信息加入資源列表。
        • 壓縮幀動畫文件,將壓縮后的文件信息加入資源列表。
        • 壓縮so文件,將壓縮后的文件信息加入資源列表。
        • 壓縮zip文件夾下文件,將壓縮后的文件信息加入資源列表。
        • 遍歷資源文件,為其生成相應的資源實體類DynamicPkgInfo。
        1. dynamic_plugin插件類設計

        可以與第4章,整體架構圖結合起來看。

        • 系統(tǒng)插件層

        DynamicPlugin類,實現(xiàn)了系統(tǒng)gradle插件的plugin接口,為我們整個插件的入口,主要解析配置文件,并按照配置文件創(chuàng)建task信息。

        DynamicParam類,提供了存儲并解析dyanmic_plugin配置文件的方法。

        • 任務模塊層

        ITask接口,代表了一個我們定義的任務。

        DeleteAndCopySoTask,刪除并拷貝so文件任務。

        TransfomrTask,替換系統(tǒng)System.loadLibrary方法任務。

        ZipResTask,壓縮so和其他文件,并生成對應的java資源實體類方法。

        • 底層實現(xiàn)層

        SystemLoadClassVisitor類,Asm框架的class訪問類。

        SystemLoadMethodVisitor類,Asm框架的method訪問類,用于替換System.loadLibrary方法。

        JavaFileCreate類,使用javapoet框架產(chǎn)生java文件。

        其他輔助類,在此省略

        • 類uml圖

        1. dynamic_config.gradle配置文件

        該配置文件主要包含了配置dynamic_plugin插件運行步驟,插件輸入輸出路徑,so文件掃描路徑等信息。



        dynamic_config = [

                //是否執(zhí)行替換System.loadlibrary操作

          is_replace_load_library: false,

                //是否執(zhí)行替換System.load操作

          is_replace_load : false,

                //是否執(zhí)行刪除so文件操作

          is_delete_so : false,

                //是否執(zhí)行將so文件拷貝到其他目錄操作

          is_copy_so : false,

                //是否執(zhí)行將動態(tài)資源打包,并生成java文件操作

          is_zip_res : false,

                //是否執(zhí)行將so文件打包,并生成java文件操作

          is_zip_so : false,

                //是否自動上傳所有資源,上傳方法為dynamic_upload

          is_upload_res : false,

                //插件是否工作在Release模式下

          is_release_type : isReleaseBuildType(),

                //是否打印debug日志

          is_debug_log : true,

                //自動創(chuàng)建java文件時的包名

          create_java_pkg_name : 'com.test' ,



        ]



         /**

         * 配置要刪除和拷貝的so文件

         * map的key為壓縮包名稱,值為壓縮包包含的so文件列表

         * key為debug_all_test時,會壓縮所有so包

         */

        dynamic_scan_so_map = [

                guang_dong : [ 'libpajf.so' , 'libpajf_av.so' , 'libsqlite.so' ],

        ]



        dynamic_so_config = [

                //so文件忽略列表,該表中的文件,不會被掃描。不在該列表中的文件都會被掃描

         // (dynamic_scan_so_map為空時,本列表才生效)

          ignore_so_files: [],

                //so文件掃描abi目錄,不在該目錄下的so將不被掃描

          scan_so_abis : [ "arm64-v8a" , "armeabi-v7a" ],

                //拷貝出來的so文件夾前綴,ignore_so_files生效時使用

          so_input_prefix: 'test' ,

        ]



        dynamic_lib_list = [

                //只有該列表中的包名,才會執(zhí)行替換System.loadlibrary操作

         //輸入debug_all_test,則會替換所有System.loadLibrary方法,用于測試

          scan_load_library_pkgs : [],

                //在該列表中的包名或者類名,不會執(zhí)行替換System.loadlibrary操作,和上面的配置可以同時生效

          ignore_load_library_pkgs: [],

        ]



         //該配置不要改動內(nèi)容,需要改變路徑的,直接改變對應的方法內(nèi)容即可

        dynamic_dir = [

                //產(chǎn)生文件的輸出目錄

          output : createOrGetOutputPath(),

                //字體資源輸入目錄

          typeface_input : createOrGetInputTypafacePath(),

                //幀動畫資源輸入目錄

          frame_anim_input: createOrGetInputFrameAnimPath(),

                //so文件資源輸入目錄

          so_input : createOrGetInputSoPath(),

                //zip包輸入目錄

          zip_input : createOrGetInputZipPath()

        ]



         //該配置項,配置了android 2個gradle task的名稱

         //主工程的mergeNativeLibs合并所有依賴的 native 庫

         //主工程的stripDebugSymbols從 Native 庫中移除 Debug 符號。

        dynamic_task = [

                //自定義的task運行哪里

         //true為mergeNativeLibs之后,stripDebugSymbols之前

         //false為stripDebugSymbols之后,package之前

          isTaskRunAfterMerge : true,

                //debug狀態(tài)下,mergeNativeLibs的task名稱

          debugMergeNativeLibs : "mergeDebugNativeLibs" ,

                //release狀態(tài)下,mergeNativeLibs的task名稱

          releaseMergeNativeLibs : "mergeReleaseNativeLibs" ,

                //debug狀態(tài)下,stripDebugSymbols的task名稱

          debugStripDebugSymbols : "stripDebugDebugSymbols" ,

                //release狀態(tài)下,stripDebugSymbols的task名稱

          releaseStripDebugSymbols: "stripReleaseDebugSymbols" ,

                //debug狀態(tài)下,系統(tǒng)打包task名稱

          debugPackage : "packageDebug" ,

                //release狀態(tài)下,系統(tǒng)打包task名稱

          releasePackage : "packageRelease" ,

                //debug狀態(tài)下,mergeNativeLibs的輸出目錄

          debugNativeOutputPath : " $ { projectDir }/app/build/intermediates/merged_native_libs/debug/out/lib" ,

                //release狀態(tài)下,mergeNativeLibs的輸出目錄

          releaseNativeOutputPath : " $ { projectDir }/app/build/intermediates/merged_native_libs/release/out/lib" ,

                //debug狀態(tài)下,,stripDebugSymbols的輸出目錄

          debugStripOutputPath : " $ { projectDir }/app/build/intermediates/stripped_native_libs/debug/out/lib" ,

                //release狀態(tài)下,,stripDebugSymbols的輸出目錄

          releaseStripOutputPath : " $ { projectDir }/app/build/intermediates/stripped_native_libs/release/out/lib" ,

        ]

         //該閉包可以自動將文件上傳到服務器,參數(shù)列表為資源id,資源文件路徑

         //我們可以再次執(zhí)行上傳服務器操作,并返回對應的url。

         //當然也可以不實現(xiàn)上傳操作,并自己手動上傳資源。

        dynamic_upload = {

         id, path ->

         println( "dynamic_upload id $ { id } ,path $ { path }" )

                return 'http://url'

         }
        1. 優(yōu)化效果

        通過引入動態(tài)資源管理系統(tǒng),并將一鍵報警sdk相關的so文件和其他普通資源動態(tài)化后,貨拉拉用戶端的包體積減少了8M,從54M變?yōu)榱?6M。后繼將會繼續(xù)嘗試進行其他so文件的動態(tài)化。

        1. 參考文獻

        https://www.jianshu.com/p/260137fdf7c5

        https://mp.weixin.qq.com/s/X58fK02imnNkvUMFt23OAg

        向大家推薦下我的網(wǎng)站 https://xuyisheng.top/  點擊原文一鍵直達

        專注 Android-Kotlin-Flutter 歡迎大家訪問



        往期推薦


        本文原創(chuàng)公眾號:群英傳,授權轉(zhuǎn)載請聯(lián)系微信(Tomcat_xu),授權后,請在原創(chuàng)發(fā)表24小時后轉(zhuǎn)載。
        < END >
        作者:徐宜生

        更文不易,點個“三連”支持一下??


        瀏覽 92
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 又黄又爽无遮挡 | 少妇寂寞小伙满足少妇在线观看 | 九色|PORNY|自拍视频 | 日韩五十路 | 日韩av小说 |