Práctica de optimización de recursos de volumen de paquete de Android Dewu

En la optimización del tamaño del paquete, la optimización de recursos es generalmente la primera y más efectiva dirección de optimización. La optimización de recursos consiste en optimizar el tamaño del paquete optimizando los elementos de recursos en el APK. En este artículo, presentaremos algunas prácticas de la aplicación Dewu en la optimización de recursos.

1. Optimización de complementos

Los recursos de optimización de complementos ganan 12 MB en la última versión de la aplicación Dewu. Los registros de optimización de complementos se muestran específicamente en la plataforma de volumen de paquetes, que también proporciona una trazabilidad de los problemas de recursos.100.png

1.1 Configuración del entorno de complementos

El complemento primero inicializará la configuración del entorno.Si el entorno operativo no está instalado en la máquina, irá a oss para descargar el archivo ejecutable correspondiente.101.png

1.2 Compresión de imágenes

En la etapa de desarrollo, los estudiantes de desarrollo primero comprimirán activamente las imágenes a través de herramientas como TinyPNG, y para las bibliotecas de terceros y algunas imágenes perdidas por negocios, se comprimirán a través del complemento Gradle cuando se empaqueten. El complemento de compresión de imágenes usa cwebp para convertir imágenes a webp, usa guetzli para comprimir JPEG, usa pngquant para comprimir PNG y usa gifsicle para comprimir gif. Durante el proceso de implementación, webp procesa preferentemente los archivos del directorio res y los archivos del directorio de activos se comprimen en el mismo formato. Lo siguiente primero presenta el modo de trabajo y el principio del complemento de compresión de recursos.

1.2.1 Compresión de imágenes de resolución

  • El primer paso es encontrar ```lenguaje
到并遍历 ap_ 文件

![103.png](https://cdn.poizon.com/ctoo/072011/103.png)

###### AAPT2这个工具在打包过程中主要做了下列工作:
把"assets"和"res/raw"目录下的所有资源进行打包(会根据不同的文件后缀选择压缩或不压缩),而"res/"目录下的其他资源进行编译或者其他处理(具体处理方式视文件后缀不同而不同,例如:".xml"会编译成二进制文件,".png"文件会进行优化等等)后才进行打包;
会对除了assets资源之外所有的资源赋予一个资源ID常量,并且会生成一个资源索引表resources.arsc;
编译AndroidManifest.xml成二进制的XML文件;
把上面3个步骤中生成结果保存在一个*.ap_文件,并把各个资源ID常量定义在一个 R.java\ R.txt中;

- 第二步,解压 ap_ 文件,找到 res/drawable 、res/mipmap 、res/raw 目录下的图片进行压缩

```fun compressImg(imgFile: File): Long {
    if (ImageUtil.isJPG(imgFile) || ImageUtil.isGIF(imgFile) || ImageUtil.isPNG(imgFile)) {
        val lastIndexOf = imgFile.path.lastIndexOf(".")
        if (lastIndexOf < 0) {
            println("compressImg ignore ${imgFile.path}")
            return 0
        }
        val tempFilePath =
                "${imgFile.path.substring(0, lastIndexOf)}_temp${imgFile.path.substring(lastIndexOf)}"

        if (ImageUtil.isJPG(imgFile)) {
            Tools.cmd("guetzli", "--quality 85 ${imgFile.path} $tempFilePath")
        } else if (ImageUtil.isGIF(imgFile)) {
            Tools.cmd("gifsicle", "-O3 --lossy=25 ${imgFile.path} -o $tempFilePath")
        } else if (ImageUtil.isPNG(imgFile)) {
            Tools.cmd(
                    "pngquant",
                    "--skip-if-larger --speed 1 --nofs --strip --force  --quality=75  ${imgFile.path} --output $tempFilePath"
            )
        }
        val oldSize = imgFile.length()
        val tempFile = File(tempFilePath)
        val newSize = tempFile.length()
        return if (newSize in 1 until oldSize) {
            val imgFileName: String = imgFile.path
            if (imgFile.exists()) {
                imgFile.delete()
            }
            tempFile.renameTo(File(imgFileName))
            oldSize - newSize
        } else {
            if (tempFile.exists()) {
                tempFile.delete()
            }
            0L
        }
    }
    return 0
}

La compresión de imágenes tiene el mayor beneficio, es simple de implementar y tiene el menor riesgo.Es la primera opción para la optimización de recursos.

1.2.2 Compresión de imágenes de activos

El método de procesamiento de la compresión de imágenes de activos es similar al de res. La única diferencia es que la tarea montada es diferente del modo de compresión. Los recursos de orden de activos se obtienen por nombre a través de AssetsManager, y el escenario de uso es incontrolable. Bajo la premisa de no poder percibir claramente si el formato es requerido para uso comercial, la compresión en el mismo formato es una solución relativamente segura.

mergeAssets.doLast { task ->
    (task as MergeSourceSetFolders).outputDir.asFileTree.files.filter {
        val originalPath = it.absolutePath.replace(task.outputDir.get().toString() + "/", "")
        val filter = context.compressAssetsExtension.whiteList.contains(originalPath)
        if (filter) {
            println("Assets compress ignore:$originalPath")
        }
        !filter
    }.forEach { file ->
        val originalPath = file.absolutePath.replace(task.outputDir.get().toString() + "/", "")
        val reduceSize = CompressUtil.compressImg(file)
        if (reduceSize > 0) {
            assetsShrinkLength += reduceSize
            assetsList.add("$originalPath => reduce[${byteToSize(reduceSize)}]")
        }
    }
    println("assets optimized:${byteToSize(assetsShrinkLength)}")
}

1.3 Deduplicación de recursos

En comparación con la compresión, la deduplicación de recursos requiere un poco de comprensión del formato de archivo arsc. Para facilitar la comprensión, aquí hay una breve introducción al archivo binario arsc. El archivo resource.arsc es un archivo de índice de recursos generado durante el proceso de empaquetado de Apk. Es un archivo binario y el código fuente ResourceTypes.h define su estructura de datos. Al aprender la estructura del archivo resource.arsc, puede ayudarnos a obtener una comprensión profunda de la eliminación de recursos duplicados y las tecnologías de ofuscación del nombre del archivo de recursos utilizadas en la optimización del tamaño del paquete apk. Si está interesado en los detalles específicos del archivo ARSC, consulte: https://huanle19891345.github.io/en/android/hotfix bytecode/tinker/source code analysis/resource.arsc generación y estructura/ 105.png Abra el apk usando AS para ver la información almacenada en resource.arsc

106.png

Hablando de deduplicación de recursos, el principio de la deduplicación es muy simple: encuentre el mismo archivo en el directorio de archivos de recursos, luego elimine el archivo duplicado y finalmente modifique el registro en arsc para reemplazar el nombre de índice del archivo eliminado. Dado que la eliminación de recursos duplicados en arsc solo reemplaza la ruta en el conjunto de constantes, no elimina los registros en arsc ni modifica el contenido del conjunto de constantes en PackageChunk, que corresponde al campo Nombre en la figura anterior, por lo que la seguridad de eliminar recursos duplicados es relativamente alta. El plan de implementación específico se presenta a continuación:

  • El primer paso es recorrer el archivo ap y encontrar el mismo archivo a través del algoritmo crc32. La razón por la que se elige crc32 es que el archivo de entrada de gralde tiene su propio valor crc32 y no se requiere ningún cálculo adicional, pero crc32 tiene un riesgo de conflicto, por lo que la verificación secundaria md5 se realiza en el resultado repetido de crc32.
  • El segundo paso es eliminar el archivo duplicado original.
  • El tercer paso es modificar el contenido del grupo de constantes ResourceTableChunk para redirigir los recursos.
val groupResources = ZipFile(apFile).groupsResources()
// 获取
val resourcesFile = File(unZipDir, "resources.arsc")
val md5Map = HashMap<String, HashSet<ZipEntry>>()
val newResouce = FileInputStream(resourcesFile).use { stream ->
    val resouce = ResourceFile.fromInputStream(stream)
    groupResources.asSequence()
        .filter { it.value.size > 1 }
        .map { entry ->
            entry.value.forEach { zipEntry ->
                if (whiteList.isEmpty() || !whiteList.contains(zipEntry.name)) {
                    val file = File(unZipDir, zipEntry.name)
                    MD5Util.computeMD5(file).takeIf { it.isNotEmpty() }?.let {
                        val set = md5Map.getOrDefault(it, HashSet())
                        set.add(zipEntry)
                        md5Map[it] = set
                    }
                }
            }
            md5Map.values
        }
        .filter { it.size > 1 }
        .forEach { collection ->
            // 删除多余资源
            collection.forEach { it ->
                val zips = it.toTypedArray()
                // 所有的重复资源都指定到这个第一个文件上
                val coreResources = zips[0]
                for (index in 1 until zips.size) {
                    // 重复的资源
                    val repeatZipFile = zips[index]
                    result?.add("${repeatZipFile.name} => ${coreResources.name}    reduce[${byteToSize(repeatZipFile.size)}]")
                    // 删除解压的路径的重复文件
                    File(unZipDir, repeatZipFile.name).delete()
                    // 将这些重复的资源都重定向到同一个文件上
                    resouce
                        .chunks
                        .filterIsInstance<ResourceTableChunk>()
                        .forEach { chunk ->
                            val stringPoolChunk = chunk.stringPool
                            val index = stringPoolChunk.indexOf(repeatZipFile.name)
                            if (index != -1) {
                                // 进行剔除重复资源
                                stringPoolChunk.setString(index, coreResources.name)
                            }
                        }
                }
            }
        }

    resouce
}

1.4 Confusión de recursos

La ofuscación de recursos es un paso más sobre la base de la reimpresión de recursos. Es consistente con la idea de ofuscación de código. Reemplaza rutas cortas con rutas largas. Primero, reduce el tamaño de los nombres de los archivos, y segundo, reduce el tamaño de los archivos binarios en el grupo constante en arsc. Reemplace la ruta larga con la ruta corta y modifique ResourceTableChunk, que es exactamente igual que el procesamiento de recursos duplicados. Al mismo tiempo, encontramos que los campos en el grupo de constantes en PackageChunk siguen siendo el contenido original, pero no afecta el funcionamiento de apk. Porque el recurso cargado por getDrawable( R.drawable.xxx ) corresponde al contenido hexadecimal de getDrawable(0x7f08xxxx) después de la compilación, que en realidad corresponde al ID en arsc, y el campo Nombre no se usa . Y a través de getResources().g

        val newResouce = FileInputStream(resourcesFile).use { inputStream ->
            val resouce = ResourceFile.fromInputStream(inputStream)
            resouce
                .chunks
                .filterIsInstance<ResourceTableChunk>()
                .forEach { chunk ->
                    val stringPoolChunk = chunk.stringPool
                    // 获取所有的路径
                    val strings = stringPoolChunk.getStrings() ?: return@forEach

                    for (index in 0 until stringPoolChunk.stringCount) {
                        val v = strings[index]

                        if (v.startsWith("res")) {
                            if (ignore(v, context.proguardResourcesExtension.whiteList)) {
                                println("resProguard  ignore  $v ")
                                // 把文件移到新的目录
                                val newPath = v.replaceFirst("res", whiteTempRes)
                                val parent = File("$unZipDir${File.separator}$newPath").parentFile
                                if (!parent.exists()) {
                                    parent.mkdirs()
                                }
                                keeps.add(newPath)
                                // 移动文件
                                File("$unZipDir${File.separator}$v").renameTo(File("$unZipDir${File.separator}$newPath"))
                                continue
                            }
                            // 判断是否有相同的
                            val newPath = if (mappings[v] == null) {
                                val newPath = createProcessPath(v, builder)
                                // 创建路径
                                val parent = File("$unZipDir${File.separator}$newPath").parentFile
                                if (!parent.exists()) {
                                    parent.mkdirs()
                                }
                                // 移动文件
                                val isOk =
                                    File("$unZipDir${File.separator}$v").renameTo(File("$unZipDir${File.separator}$newPath"))
                                if (isOk) {
                                    mappings[v] = newPath
                                    newPath
                                } else {
                                    mappings[v] = v
                                    v
                                }
                            } else {
                                mappings[v]
                            }
                            strings[index] = newPath!!
                        }
                    }

                    val str2 = mappings.map {
                        val startIndex = it.key.lastIndexOf("/") + 1
                        var endIndex = it.key.lastIndexOf(".")

                        if (endIndex < 0) {
                            endIndex = it.key.length
                        }
                        if (endIndex < startIndex) {
                            it.key to it.value
                        } else {
//                            val vStartIndex = it.value.lastIndexOf("/") + 1
//                            var vEndIndex = it.value.lastIndexOf(".")
//                            if (vEndIndex < 0) {
//                                vEndIndex = it.value.length
//                            }
//                            val result = it.value.substring(vStartIndex, vEndIndex)
                            // 使用相同的字符串,以减小体积
                            it.key.substring(startIndex, endIndex) to "du"
                        }
                    }.toMap()

                    // 修改 arsc PackageChunk 字段
                    chunk.chunks.values.filterIsInstance<PackageChunk>()
                        .flatMap { it.chunks.values }
                        .filterIsInstance<StringPoolChunk>()
                        .forEach {
                            for (index in 0 until it.stringCount) {
                                it.getStrings()?.forEachIndexed { index, s ->
                                    str2[s]?.let { result ->
                                        it.setString(index, result)
                                    }
                                }
                            }
                        }

                    // 将 mapping 映射成 指定格式文件,供给反混淆服务使用
                    val mMappingWriter: Writer = BufferedWriter(FileWriter(file, false))
                    val packageName = context.proguardResourcesExtension.packageName
                    val pathMappings = mutableMapOf<String, String>()
                    val idMappings = mutableMapOf<String, String>()
                    mappings.filter { (t, u) -> t != u }.forEach { (t, u) ->
                        result?.add(" $t => $u")
                        compress[t]?.let {
                            compress[u] = it
                            compress.remove(t)
                        }
                        val pathKey = t.substring(0, t.lastIndexOf("/"))
                        pathMappings[pathKey] = u.substring(0, u.lastIndexOf("/"))
                        val typename = t.split("/")[1].split("-")[0]
                        val path1 = t.substring(t.lastIndexOf("/") + 1, t.indexOf("."))
                        val path2 = u.substring(u.lastIndexOf("/") + 1, u.indexOf("."))
                        val path = "$packageName.R.$typename.$path1"
                        val pathV = "$packageName.R.$typename.$path2"
                        if (idMappings[path].isNullOrEmpty()) {
                            idMappings[path] = pathV
                        }
                    }
                    generalFileResMapping(mMappingWriter, pathMappings)
                    generalResIDMapping(mMappingWriter, idMappings)
                }

            // 删除res下的文件
            FileOperation.deleteDir(File("$unZipDir${File.separator}res"))
            // 将白名单的文件移回res
            keeps.forEach {
                val newPath = it.replaceFirst(whiteTempRes, "res")
                val parent = File("$unZipDir${File.separator}$newPath").parentFile
                if (!parent.exists()) {
                    parent.mkdirs()
                }
                File("$unZipDir${File.separator}$it").renameTo(File("$unZipDir${File.separator}$newPath"))
            }
            // 收尾删除 res2
            FileOperation.deleteDir(File("$unZipDir${File.separator}$whiteTempRes"))
            resouce
        }

  • La configuración de la lista blanca es esencial para garantizar que los recursos de llamadas reflexivos no participen en la confusión.
  • createProcessPath se usa para modificar una ruta larga a una ruta corta
  • Modifique el grupo constante en PackageChunk para el recorte extremo del cuerpo del paquete, reduzca el cuerpo del paquete en 300 kb antes de descomprimirlo y reduzca el cuerpo del paquete en 70 kb después de la compresión arsc

107.png

  • Generar un archivo de mapeo de confusión de recursos y proporcionarlo al servicio de volumen de paquetes para la restauración del nombre del recurso

El proceso de aterrizaje de la confusión de recursos debe ser cauteloso. Para el código de acciones, en la aplicación Dewu, primero escaneamos el código de bytes para encontrar todos los lugares donde los recursos son llamados por reflejo, y luego configuramos el archivo de conservación. Para las llamadas reflexivas recién agregadas en el desarrollo comercial posterior, los problemas se pueden encontrar temprano a través del proceso de prueba.

Compresión 1.5 ARSC

El volumen reducido de compresión de Arsc es muy considerable, el arsc comprimido es de 700kb, y el descomprimido es de unos 7MB. Se puede implementar comprimiendo el archivo arsc con 7zip.

108.png

Pero la compresión arsc de Target Sdk por encima de 30 está prohibida. Aunque la compresión de resources.arsc puede traer beneficios en el cuerpo del paquete, también tiene desventajas, como la memoria y la velocidad de ejecución. El sistema resources.arsc sin comprimir puede usar mmap para ahorrar uso de memoria (los recursos de una aplicación están en manos de al menos 3 procesos: él mismo, el lanzador, el sistema) y los recursos.arsc comprimidos existirán en cada proceso.

2. Distribución de recursos

La plataforma de volumen del paquete detecta los grandes recursos en el Apk después del empaquetado, y se programa el tratamiento de los recursos problemáticos. La entrega dinámica y la eliminación inútil son medios comunes para manejar los recursos de stock y, al mismo tiempo, a través del control previo de CI para controlar la situación de que los recursos recién agregados son demasiado grandes.

El cuerpo principal de la distribución de recursos son los archivos y las imágenes, y la gestión y el control de los recursos distribuidos deben gestionarse a través de la plataforma. El bloqueo es peor que escaso, y los recursos que se pueden distribuir se distribuyen, lo cual es una gran arma para la optimización del cuerpo del paquete.

109.png

Los recursos distribuidos se procesan a través de la plataforma de gestión dinámica de recursos

110.png

3. Eliminar recursos inútiles

La detección de recursos inútiles combina el período de compilación resCheck de bytex y los resultados del análisis de correo electrónico matrix-apk-canary para mostrar la parte del negocio que se puede procesar en la plataforma Durante el proceso de iteración de la versión, itera y administra, lo que puede prevenir eficazmente el deterioro continuo de los recursos inútiles.

111.png

4. Resumen

Este artículo presenta principalmente algunas acciones realizadas por la optimización de recursos de la aplicación Dewu y se centra en el modo de trabajo del complemento de optimización de recursos. Por supuesto, todavía hay muchas formas de mejorar los recursos, como proporcionar una solución de entrega de 9 imágenes simple y eficiente, agregar capacidades de detección de similitud de imágenes a la plataforma de volumen de paquetes y entregar algunos recursos secundarios a través de paquetes complementarios Estos son lugares que se pueden probar en el futuro.

::: hljs-center

*Texto/Jay

:::

Este artículo es un artículo original de Dewu Technology. Para obtener más artículos interesantes, consulte: Sitio web oficial de Dewu Technology.
La reimpresión sin el permiso de Dewu Technology está estrictamente prohibida, de lo contrario, se investigará la responsabilidad legal de acuerdo con la ley.

RustDesk 1.2: Usando Flutter para reescribir la versión de escritorio, apoyando a Wayland acusado de deepin V23 adaptándose con éxito a los lenguajes de programación WSL 8 con la mayor demanda en 2023: PHP es fuerte, la demanda de C/C++ se ralentiza ¿ React está experimentando el momento de Angular.js? El proyecto CentOS afirma estar "abierto a todos" Se lanzan oficialmente MySQL 8.1 y MySQL 8.0.34 Se lanza la versión estable de Rust 1.71.0
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/5783135/blog/10089769
Recomendado
Clasificación