58 city Android Qigsaw upgrade-v1.0 4.1 multi ABI construction

58 city Android Qigsaw upgrade-v1.0 4.1 multi ABI construction

1. Background

Qigsaw is an open source framework used by Wafers' dynamic capability, which carries the operation of 58App, arbitrary gate and Mocha. The group started from qigsaw v1.0 in 2020 / 03 3.2.2 fork access

https://github.com/iqiyi/Qigsaw

v_ Version 1.3.2.2 only supports single ABI dimension construction. As more and more domestic app stores need to upload 64 bit apks, there is a growing demand for Qigsaw to support Base APK and Split APKs to split based on ABI dimension

Qigsaw v1.4.0 supports APK splitting based on ABI dimension:

2. Scheme investigation and design

Summarized as:

  • Align the latest version code of Qigsaw
  • Retain the old custom business logic and bug fix
  • Transform 58App packaging script, ftp backup, AVM release process, any door background
  • Wireless basic capability test, arbitrary gate, Mocha service line test

Original contracting process:

Target contracting process:

3. Upgrade process

3.1 Qigsaw

(1) Get qigsaw v1 4.1 latest code

Based on a new branch, pull qigsaw v1 4.1 latest code (this branch is completely the latest code without any old modification code)

(2) Manually add previous modification points

#changelog
1maven publishing script
2Implement split apk upload logic
3SplitConfiguration adds interface support for setting fakeactivity / service / receiver
4Remove the group call restriction of AABExtension class
5Handle the resource loading problem when the current basic component does not jump to the scene
6Add multi dex to install Activity and start Activity filtering configuration
7Add external processing options for mobile network download pop-up
8Breakpoint download, parallel downloader repair
9Add task to upload split config file
10Add the option of Activity list property of plug-in filter injected resources
11Fix 5 and 6 system adaptation problems
12Fix exception thrown by resource loading
13fix downloader nullpointer
14Adapt agp4 0 (the latest version has been officially repaired)
15Support R8 compilation

(3) Problems encountered

Resguard adaptation problem
AndroResGuard is a resource obfuscation tool that can be used to reduce the size of APK. People in the wechat team open source the project, which directly modifies the APK file

Resguard principle:

(1) Confusing resources
When calling resources, Android uses int values instead of res name directly. This corresponding relationship will be stored in resource after packaging In the ArsC file. AndResGuard reduces the file size by changing res name to a, b and c

(2) 7z compression
AndResGuard uses the 7z limit to compress images and resource files (including resource.arsc mentioned above) to reduce the size of apk

58App AndResGuard packaging process (package with v7a output):

Packaging process after upgrading Qigsaw:

Obviously, the process is incorrect, which will increase the size of the subcontracted abi package. The basic package should be resguard before subcontracting. Therefore, modify the Qigsaw packaging script as follows:

// source: buildSrc/com.iqiyi.qigsaw.buildtool.gradle/QigsawAppBasePlugin:

 if (QigsawSplitExtensionHelper.isMultipleApkForABIs(project)) {
        SplitBaseApkForABIsTask splitBaseApkForABIs = project.tasks.create("split${baseVariant.name.capitalize()}BaseApkForABIs", SplitBaseApkForABIsTask)
        splitBaseApkForABIs.baseVariant = baseVariant
        splitBaseApkForABIs.apkSigner = apkSigner
        splitBaseApkForABIs.dynamicFeaturesNames = dynamicFeaturesNames
        splitBaseApkForABIs.supportABIs = QigsawSplitExtensionHelper.getSupportABIs(project)
        splitBaseApkForABIs.baseAppCpuAbiListFile = baseAppCpuAbiListFile
        splitBaseApkForABIs.baseApkFiles = baseApkFiles
        splitBaseApkForABIs.packageAppDir = packageAppDir
        splitBaseApkForABIs.baseApksDir = baseApksDir
        Task resguardTask = AGPCompat.getResguardTask(project, "${baseVariant.name.capitalize()}")
        // apply plugin: 'AndResGuard' must be applied after the qigsaw application plug-in, otherwise the resguardTask will not be found
        if (resguardTask != null) {
            SplitLogger.w("found resguardTask")
            resguardTask.dependsOn baseAssemble
            baseAssemble.finalizedBy resguardTask
            splitBaseApkForABIs.dependsOn resguardTask
            resguardTask.finalizedBy splitBaseApkForABIs
         } else {
             SplitLogger.w("not found resguardTask")
             baseAssemble.dependsOn splitBaseApkForABIs
             packageApp.finalizedBy splitBaseApkForABIs
         }
   }

Here is a knowledge point:

Host App apply plug-ins A and B. if Apply A first and then Apply B, the task in plug-in B cannot be obtained in plug-in a Therefore, you need to adjust the application order of 58App Qigsaw plug-in and resguard plug-in

// apply plugin: 'AndResGuard' must be applied after the qigsaw application plug-in, otherwise the resguardTask will not be found
if (Boolean.parseBoolean(ON_JENKINS)) {
    apply from: 'andResguard.gradle'
}
if (Boolean.parseBoolean(AAB_SWITCH)) {
    apply from: '../DynamicFeatures/qigsaw-application-apply.gradle'
}

Qigsaw 7z depth compression problem
58when using the AndroResGuard plug-in, 7z compression is turned on, so 7z compression also needs to be turned on in the Qigsaw plug-in to keep the package size consistent

// 58App AndResGuard configuration
apply plugin: 'AndResGuard'

andResGuard {
    enableResGuard = Boolean.parseBoolean(rootProject.enableResGuard)
    mappingFile = file("./resource_mapping.txt")
    use7zip = true // Turn on 7z deep compression
    useSign = true
    // ....
}

The Qigsaw packaging plug-in also has 7z compression options:

// buildSrc/com.iqiyi.qigsaw.buildtool.gradle.task.SplitBaseApkForABIsTask
// Recompress the subcontracted apk packets
if (use7z) {
    run7zCmd("7za", "a", "-tzip", unsignedBaseApk.absolutePath, unzipBaseApkDirForAbi.absolutePath + File.separator + "*", "-mx9")
} else {
    ZipUtils.zipFiles(Arrays.asList(unzipBaseApkDirForAbi.listFiles()), unzipBaseApkDirForAbi, unsignedBaseApk, compress)
}

After opening, it is found that the 58App v7a package is reduced to 113M, and the exception of resource not found occurs during operation.

Let's start with the following 7z compression:

7z is a mainstream and efficient compression format, which has a very high compression ratio

eg: 7z -tZip a test.zip ./test/* -mx0
Compress all files in the test folder into ZIP files in the storage compression mode. The compressed file is test zip

a add options for
-tZip is in compressed format (-t7z...)
The specific parameters of compression ratio are as follows:
-mx0 only stores uncompressed, the fastest, uncompressed, and the zip size is the same as the original folder
-mx1 extreme compression
-mx3 fast compression
-mx5 standard compression
-mx7 maximum compression
-mx9 ultimate compression

Let's first look at Qigsaw subcontracting logic:

Therefore, the reasons for the above problems are:

The AndResGuard plug-in has conducted a mx9 deep compression of the 58App basic package APK, because the Qigsaw plug-in needs to decompress the original package when subcontracting,
Process LIBS and ABI configuration files, and then compress them. Whether 7z or zip is used for re compression, the original basic package will be destroyed

Solution:

The APK is not decompressed during subcontracting, and the zip operation is directly used to modify and re sign. While solving this problem, it can also speed up the packaging speed

// buildSrc/com.iqiyi.qigsaw.buildtool.gradle.task.SplitBaseApkForABIsTask
 File baseApk = baseApkFiles[0]
        List<String> abiList = supportABIs != null ? supportABIs.split(",") : null
        if (abiList == null || abiList.isEmpty()) {
            SplitLogger.e("Base apk ${baseApk.absolutePath} has no native-library abi folder, multiple apks don't need.")
            return
        }
        if (abiList.size() == 1) {
            SplitLogger.e("Base apk ${baseApk.absolutePath} has only one native-library abi folder, multiple apks don't need.")
            return
        }
        abiList.each { String abi ->
            if (SUPPORTED_ABIS.contains(abi)) {
                // You cannot directly use 7z depth compression, which will affect resources The use of ArsC causes the resource not to be found, so the zip command is used here
                // Copy base apk
                File copyBaseApk = new File(baseApksDir, "${project.name}-${baseVariant.name.uncapitalize()}-${abi}${SdkConstants.DOT_ANDROID_PACKAGE}")
                if (!copyBaseApk.parentFile.exists()) {
                    copyBaseApk.parentFile.mkdirs()
                }
                if (copyBaseApk.exists()) {
                    copyBaseApk.delete()
                }
                FileUtils.copyFile(baseApk, copyBaseApk)
                String copyBaseApkPath = copyBaseApk.getAbsolutePath()

                // Delete signature related files
                runCmd("zip", "-d", copyBaseApkPath, "META-INF/CERT.RSA")
                runCmd("zip", "-d", copyBaseApkPath, "META-INF/CERT.SF")
                runCmd("zip", "-d", copyBaseApkPath, "META-INF/MANIFEST.MF")

                Set<String> masterSplitHandleFlags= new HashSet<>()
                abiList.each { String ABI ->
                    if (abi != ABI) {
                        // Delete other ABI's lib
                        runCmd("zip", "-d", copyBaseApkPath, "lib/${ABI}/**")

                        // Delete other ABI's built-in splits (include master)
                        dynamicFeaturesNames.each { String splitName ->
                            if (!masterSplitHandleFlags.contains(splitName)) {
                                runCmd("zip", "-d", copyBaseApkPath, "assets/qigsaw/${splitName}-master**.zip")
                                masterSplitHandleFlags.add(splitName)
                            }
                            runCmd("zip", "-d", copyBaseApkPath, "assets/qigsaw/${splitName}-${ABI}**.zip")
                        }
                    }
                }

                // Update base apk cpu abi list file
                File baseAppCpuAbiListFileForAbi = new File(baseApksDir,"assets/${baseAppCpuAbiListFile.name}")
                if (!baseAppCpuAbiListFileForAbi.parentFile.exists()) {
                    baseAppCpuAbiListFileForAbi.parentFile.mkdirs()
                }
                if (baseAppCpuAbiListFileForAbi.exists()) {
                    baseAppCpuAbiListFileForAbi.delete()
                }
                baseAppCpuAbiListFileForAbi.write("abiList=${abi}")
                // ProcessBuilder execute multi commands
                File baseAppCpuAbiScript = new File(baseApksDir,"baseAppCpuAbiScript")
                if (baseAppCpuAbiScript.exists()) {
                    baseAppCpuAbiScript.delete()
                }
                baseAppCpuAbiScript.write("#!/usr/bin/env bash\ncd \$1\nzip -d \$2 \$3\nzip -m \$2 \$3")
                runCmd("chmod", "755", baseAppCpuAbiScript.getAbsolutePath())
                runCmd(baseAppCpuAbiScript.getAbsolutePath(), copyBaseApk.getParent(), copyBaseApk.getName(), "assets/${baseAppCpuAbiListFile.name}")

                // Resign apk if need
                SigningConfig signingConfig = null
                try {
                    signingConfig = apkSigner.getSigningConfig()
                } catch (Throwable ignored) {
                }
                boolean isSigningNeed = signingConfig != null && signingConfig.isSigningReady()
                if (isSigningNeed) {
                    File signedBaseApk = new File(baseApksDir, "${project.name}-${baseVariant.name.uncapitalize()}-${abi}-signed${SdkConstants.DOT_ANDROID_PACKAGE}")
                    if (signedBaseApk.exists()) {
                        signedBaseApk.delete()
                    }
                    apkSigner.signApkIfNeed(copyBaseApk, signedBaseApk)
                    File destBaseApk = new File(packageAppDir, signedBaseApk.name)
                    if (destBaseApk.exists()) {
                        destBaseApk.delete()
                    }
                    FileUtils.copyFile(signedBaseApk, destBaseApk)
                } else {
                    File destBaseApk = new File(packageAppDir, copyBaseApk.name)
                    if (destBaseApk.exists()) {
                        destBaseApk.delete()
                    }
                    FileUtils.copyFile(copyBaseApk, destBaseApk)
                }
            }
        }	

Here is also a knowledge point:

When you execute a command using ProcessBuilder, you can only execute a single command at a time. If you want to execute multiple commands at a time, you can wrap them into a script file and then use ProcessBuilder to execute them

The above two questions have submitted merge request s to Qigsaw:

Fix the problem of increasing the size of the subcontracting ABI APK package after the resguard plug-in is applied to the host app & 7z deep compression causes the resource not to be found #57

https://github.com/iqiyi/Qigsaw/pull/57

3.2 58App

(1) Remove SPLITS_APK,UNIVERSAL_APK related configuration

// After removing the splits configuration, Qigsaw has implemented a set of subcontracting mechanism, which conflicts with the system subcontracting mechanism
splits {
        abi {
            enable !Boolean.parseBoolean(AAB_SWITCH) && Boolean.parseBoolean(rootProject.SPLITS_APK)
            reset()
            universalApk Boolean.parseBoolean(rootProject.UNIVERSAL_APK)  // If true, also generate a universal APK
            rootProject.CPU_ARCH.split(",").each { value ->
                include value
            }
        }
 }

(2) Modify WubaPackage jenkins packaging configuration

  • v7a is the default package, 58Client_vxxx_release.apk
  • Backup all packages, v7a, v8a packages to FTP
  • Any gate dynamically updates the download address of the basic package to the full package address

(3) Qigsaw API change

// WubaQigsawLib/QigsawManager
public static void applicationOnCreate() {
    Qigsaw.onApplicationCreated();
    Qigsaw.registerSplitActivityLifecycleCallbacks(new QigsawSplitActivityLifecycleCallbacks());
    // Qigsaw needs to be called manually Preloadinstalledsplits, otherwise the Application with splits installed will not be called at startup
    try {
       Set<String> splitNames = AABExtension.getInstance().getSplitNames();
       Qigsaw.preloadInstalledSplits(splitNames);
    } catch (Exception e) {
       LOGGER.e(e);
    }
}

3.3 AVM

The following changes are involved:

  • Distribution management - package configuration and add CPU_ARCHES=armeabi-v7a,arm64-v8a
  • Version distribution management - basic package changed to v7a package
  • Any gate - the base package is changed to the full package address
  • Any door - basic package download address shows full package, v7a, v8a

Keywords: Android

Added by bogins on Fri, 21 Jan 2022 20:24:48 +0200