神操做 之 「玲珑宝塔」优化 Apk 包大小

九分喜欢,一分尊严,放弃你,也放过本身,愿你安好,在多年之后不要记起深爱你的我。javascript

絮絮叨

工做不长不短,以前不曾考虑过深处,只是停留写出来了,即是完美。html

而今的处境,不尴不尬,岁月恰好,背起行囊,继续前行。java

现在的 5 G 也在万众瞩目瞩目下翩翩起舞,而 Android 近些年也惹得很多争议,所谓的谣言,不过尔尔。android

每一个人的追求不同,尽本身最大努力吧。git

如何减小 Apk 大小,一直以来都是处于观望状态,懒得折腾,其实仍是不会,Low 的一批。github

Today,一块儿来搞一波~web

欢迎各位指正~算法

现学现卖~api

一脑图,览无余

玲珑宝塔镇万物

首先附上一张如今 Apk 大小图:android-studio

在这里插入图片描述

未作任何处理原包大小为 10 MB,加固以后将近 11 MB。

以此为例,一块儿看看通过咱们玲珑宝塔升级完,最终还剩下多少精华?

一层镇妖魔(减小 4.1 MB)

来到第一层,咱们先来简单分析下是什么形成 Apk 包如此“庞大”?

在这里插入图片描述

上图可看到 lib 下兼容了全面的 CPU 架构,试想一下,假设将来的将来多了短视频、直播、地图导航等等(不接受杠精),这块的大小会不会成倍数的增加。

在这里插入图片描述

上图可看到默认支持了 89 种语言类型,目前的应用暂时未国际化,这块也可直接设置兼容中文便可,原谅我这个强迫症。

占比排行榜依次为:源代码、资源文件、lib。

咱们先挑个软柿子玩玩。

1.1 设置支持语言(减小 0.2 MB)

关于这块,我的以为虽然占比较小,可是用啥玩啥,用不到的直接干掉。

在 build.gradle 中设置仅支持中文:

defaultConfig {
        ...
        // 仅支持 中文
        resConfigs "zh"
    }
复制代码

这块主要是根据现有项目需求来定,中心思想只有一个,兼容哪儿个就设置哪儿个国家语言,其余的直接忽略。

设置完以后打个包,看下有没有什么变化。

在这里插入图片描述

从上图中能够很清晰的看到,通过设置仅支持的国家语言后,包大小减小了 0.2 MB。随后咱们看下资源映射文件中关于 string 中会有什么变化。

在这里插入图片描述

默认语言中设置为中文,且应用也只支持了中文,少了好多东西,爽得很~

1.2 设置支持的 CPU 架构类型(减小 1.5 MB)

话说这里的 lib 为什么兼容了这么多的 CPU 架构类型???

正好走到这里,关于这块的小知识再次重温下,瞅瞅 Google 为咱们提供的解释:

不一样的 Android 手机使用不一样的 CPU,而不一样的 CPU 支持不一样的指令集。CPU 与指令集的每种组合都有专属的应用二进制接口,即 ABI。ABI 能够很是精确地定义应用的机器代码在运行时如何与系统交互。您必须为应用要使用的每一个 CPU 架构指定 ABI。

貌似 Google 商店如今支持对应的架构模式分发对应的 Apk 包,这点爽的每一个包只须要兼容一种就行了。But,ummm。

目前而言,项目中使用到真正用到 So 库没几个,所有兼容太过于浪费,听说 arm 属于通用,那么这里同语言设置同样,仅支持 arm 便可。

defaultConfig {
        ...
        ndk {
            // 设置支持的SO库架构
            abiFilters "armeabi"
        }
    }
复制代码

打包运行后,继续查看如今包大小:

在这里插入图片描述

这块一直属于个心病,以前的项目光是 So 库就占了很大一部分空间,很湿蛋疼。

1.3 开启压缩、混淆(减小 2.4 MB)

根据 Google 官网解释,当咱们使用 Android Gradle 3.4.0 或者更高版本时,默认会启用 R8 编译器进行压缩、混淆以及优化,主要项以及做用以下:

  • 代码优化: 经过检测并安全移除未使用的类、字段、方法和属性;
  • 资源压缩: 从应用中移除未使用的资源,此过程包含移除库依赖项中未使用的资源文件。此项经常和代码压缩配合使用;
  • 混淆: 缩短类和成员的名称,从而减少 Dex 文件大小;
  • 优化: 检查并重写代码,进一步减少 Dex 文件大小。例如,若是 R8 检测到从未采用过给定 if/else 语句的 else {} 分支,R8 便会移除 else {} 分支的代码。

这里须要注意一下:

  • 默认状况下并不启用压缩、混淆和代码优化功能。 由于开启后会形成 Debug 模式下编译时间较久。

关于混淆文件,这里须要正好学习一下。

混淆的意义在于什么?(引入官方解释)

  • 混淆处理的目的是经过缩短应用的类、方法和字段的名称来减少应用的大小

混淆效果(摘自官方):

androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
    androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
        android.content.Context mContext -> a
        int mListItemLayout -> O
        int mViewSpacingRight -> l
        android.widget.Button mButtonNeutral -> w
        int mMultiChoiceItemLayout -> M
        boolean mShowTitle -> P
        int mViewSpacingLeft -> j
        int mButtonPanelSideLayout -> K
复制代码

混淆需注意:

  • Android 四大组件不能混淆;
  • 反射、注解、枚举不能混淆;
  • JS、Native 调用的方法不能混淆;
  • 基础 Bean 类以及序列化实体类不能混淆;
  • 自定义控件不能混淆;
  • 资源文件不能混淆(固然也有骚操做);

随后列举经常使用混淆规则(语法):

  • 保留某个类 -keep public class com.hlq.Love
  • 保留某包下的全部类及其内部类 -keep class com.hlq.** {*;}
  • 不显示指定类警告 dontwarn com.hlq.**

具体规则可文末查看官方手册。

接下来跟着官网一块儿实践一波~

buildTypes {
        release {
            // 打开资源压缩
            shrinkResources true
            // 开启混淆操做
            minifyEnabled true 
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.config
        }
        debug {
            // 关闭资源压缩以及混淆操做
            shrinkResources false
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.config
        }
    }
复制代码

这里须要注意,在 Debug 模式下须要关闭资源压缩以及混淆操做,不然会增长编译时间,通常在发布正式包时打开便可。

这里附上如今项目使用的混淆文件,基于大芬儿提供混淆文件作了部分修改:

#############################################
#
# 混淆基本指令
#
#############################################
# 代码混淆压缩比,在0~7之间,默认为5,通常不作修改
-optimizationpasses 5

# 混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames

# 指定不去忽略非公共库的类
-dontskipnonpubliclibraryclasses

# 这句话可以使咱们的项目混淆后产生映射文件
# 包含有类名->混淆后类名的映射关系
-verbose

# 指定不去忽略非公共库的类成员
-dontskipnonpubliclibraryclassmembers

# 不作预校验,preverify是proguard的四个步骤之一,Android不须要preverify,去掉这一步可以加快混淆速度。
-dontpreverify

# 保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses

# 避免混淆泛型
-keepattributes Signature

# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable

# 指定混淆是采用的算法,后面的参数是一个过滤器
# 这个过滤器是谷歌推荐的算法,通常不作更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*

# 忽略警告
-ignorewarnings

#############################################
#
# 须要保留的公共部分
#
#############################################

# 保留咱们使用的四大组件,自定义的 Application 等等这些类不被混淆
# 由于这些子类都有可能被外部调用
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService

# 保留support下的全部类及其内部类
-keep class android.support.** {*;}

# 保留继承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**

# 保留R下面的资源
-keep class **.R$* {*;}

# 保留本地native方法不被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留在Activity中的方法参数是view的方法,保障layout中写的onClick不会被影响
-keepclassmembers class * extends android.app.Activity{
    public void *(android.view.View);
}

# 保留枚举类不被混淆
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保留咱们自定义控件(继承自View)不被混淆
-keep public class * extends android.view.View{
    *** get*();
    void set*(***);
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

# 保留Parcelable序列化类不被混淆
-keep class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator *;
}

# 保留Serializable序列化的类不被混淆
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    !static !transient <fields>;
    !private <fields>;
    !private <methods>;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

# 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆
-keepclassmembers class * {
    void *(**On*Event);
    void *(**On*Listener);
}

# webView处理,项目中没有使用到webView忽略便可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
    public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
    public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.webView, jav.lang.String);
}

# 移除Log类打印各个等级日志的代码,打正式包的时候能够作为禁log使用,这里能够做为禁止log打印的功能使用
# 记得proguard-android.txt中必定不要加-dontoptimize才起做用
# 另外的一种实现方案是经过BuildConfig.DEBUG的变量来控制
-assumenosideeffects class android.util.Log {
    public static int v(...);
    public static int i(...);
    public static int w(...);
    public static int d(...);
    public static int e(...);
}

#############################################
#
# 处理项目中咱们的部分
#
#############################################

#-----------处理实体类---------------
# 在开发的时候咱们能够将全部的实体类放在一个包内,这样咱们写一次混淆就好了
-keep public class 实体类.**{*;}

# Js 交互
-keepclassmembers class JS 交互类地址{
  public *;
}

-keepattributes *JavascriptInterface*

#############################################
#
# 处理第三方依赖库部分
#
#############################################

# 此处按照实际项目中使用去官方查找对应的混淆代码块
复制代码

随后咱们继续打包,查看混淆、资源压缩后 Apk 大小以及部分变化:

在这里插入图片描述

dex 从 3 个下降到 2 个。未 Keep 的文件均已混淆,而 Keep 的文件依旧傲娇挺立,以下图:

在这里插入图片描述

混淆操做,在必定程度增大了破解的难度。固然,也没有绝对的安全。

R8 每次运行时都会建立一个 mapping.txt 文件,其中列出了混淆过的类、方法和字段名称与原始名称的映射关系。此映射文件还包含用于将行号映射回原始源文件行号的信息。R8 将此文件保存在 /build/outputs/mapping// 目录中。

在这里插入图片描述

线上版本确定要进行混淆,那么针对线上版本报出的异常,咱们又该如何处理呢?毕竟关键内容都变成无心义字符,鉴名其意不存在了。

在这里插入图片描述

iTerm 2 打开:

在这里插入图片描述

点击 ReTrace:

在这里插入图片描述

这块步骤以下:

  • 导入 Mapping 文件
  • 将混淆后错误日志拷贝黏贴到 Obfuscated stack trace 中
  • 点击右下角的 ReTrace!

1.4 开启 Zipalign 优化

这块我看的很湿懵逼,估计惟有鸡大行云流水了。简单摘自官方解释:

zipalign 是一种归档对齐工具,可对 Android 应用文件进行重要的优化。其目的是要确保全部未压缩数据的开头均相对于文件开头部分执行特定的对齐。具体来讲,它会使 APK 中的全部未压缩数据(例如图片或原始文件)在 4 字节边界上对齐。这样一来,便可使用 mmap() 直接访问全部部分,即便其中包含具备对齐限制的二进制数据也不要紧。这样作的好处是能够减小运行应用时消耗的 RAM 容量。

如何使用?非常 easy~

buildTypes {
        release {
            // 开启Zipalign 优化
            zipAlignEnabled true
        }
        debug {
            zipAlignEnabled false
        }
    }
复制代码

看一下结果:

在这里插入图片描述

貌似没啥用哦,能加仍是加上吧。

二层镇仙神(减少 1.5 MB)

来到第二层,咱们再来开下资源映射文件中关于图片这块:

在这里插入图片描述

其实对于图片而言,真的是个很蛋疼的操做,不过还好,一些简单的小背景、小效果,如今大部分都直接采用 shape、selector 等实现,多少也避免引入了一些图片。

对于图片优化,主要分为如下几点:

  • 套图的优化 -SVG
  • 套图的优化 - Thit 着色器的应用
  • webp 的使用

2.1 套图的优化 - SVG

什么是套图呢?

比如应用中的某个 Icon,通常来说,UI 都会为咱们提供 n 套图,以便于咱们适配不一样分辨率记性,大概的目录以下:

在这里插入图片描述

例以下面的这些大大小小的 Icon,一个个拷贝、更名也是比较痛苦的:

在这里插入图片描述

这个时候,SVG 便派上了用场。

可缩放矢量图形(英語:Scalable Vector Graphics,SVG)是一种基于可扩展标记语言(XML),用于描述二维矢量图形的图形格式。 SVG由W3C制定,是一个开放标准。

SVG 优点:

  • 节约空间、内存

SVG 劣势:

  • 不支持透明度以及渐变

ummm,须要注明一点,Android 6.0 + 支持,6.0 如下须要作兼容处理。不过如今应该也不必兼容那么低的版本了吧?

如何在 Android Studio 中建立一张 SVG 图片呢?以下所示:

在这里插入图片描述

弹出以下界面,在此页面能够选择直接导入 Android 内置 Icon 库图,仍是手动加载 SVG or PSD 格式,看需选择。

在这里插入图片描述

放个操做图省事儿点:

在这里插入图片描述

使用也很 easy:

<androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/iv_toolbar_back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="fitXY"
            app:srcCompat="@drawable/ic_arrow_back" />
复制代码

别忘记在 build 里面对此设置:

defaultConfig {
        ...
        // 强制 Gradle 在编译时不自动生成兼容低版本的位图资源
        vectorDrawables.useSupportLibrary = true
        // 生成指定类型的图片资源
        vectorDrawables.generatedDensities = ['xhdpi', 'xxhdpi', 'xxxhdpi'] 
    }
复制代码

建议直接下载 svg,导入 svg,这个真心比较爽。

推荐下良心企业,阿里的 iconfont:

通过一通折腾,阿里下载 SVG,以后 Android Studio 导入 SVG,巴拉巴拉,好歹改完了,哈哈哈

2.2 套图的优化 - Tint 着色器

先来举个天马行空的例子,例如同一张图片在不一样的状态下显示不一样,好比成功绿色,失败红色等等。按照以往的习惯,那确定要求至少提供每种状态对应的图片,否则我怎么搞?

可是有个实实在在的问题,那就是,图片同样,只是颜色发生了变化,假设五种状态,咱们就须要引入至少五张图片,那么,能否只须要一张图片,针对不一样的状态,咱们渲染不一样的颜色呢?

固然能够,这就是今天要说到的 Tint 着色器。有了它,最简洁明了一点,至少能帮我剩下不少可谓是“无用”图片,大大节省了不少空间,咱们的 Apk 更加“干练”。

再举一个咱们项目中常见的例子,首页 Tab 栏,以下图:

在这里插入图片描述

Tab 切换,字体变色、图片变色,这个见怪不怪了吧。上来至少给我提供八张图,四张默认,四张选中,而后经过 selector 文件设置,不给图无法作。对吧,这就是以前最实际的想法,嗯,还感受本身可 dei 了。

一块儿先来回顾下之前的 low 写法:

Step 1:至少提供八张图后,设置 icon 引用 selector 文件:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/nav_home"
        android:icon="@drawable/selector_menu_home"
        android:title="@string/nav_home" />
    ...
</menu>
复制代码

Step 2:定义 Selector:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="true" android:drawable="@drawable/ic_tab_home_sel"/>
    <item android:drawable="@drawable/ic_tab_home"/>
</selector>
复制代码

这样写当然木有问题,可是无缘无故多了不少图片,在今天的我来看,必须不能忍,今天只用四张图,干他~

这里吐槽下,因为以前底部导航采用 BottomNavigationView 方式,折腾好半天踩折腾出来,中间无数次想放弃了。但回头一想,我好歹也是跟随我鸡大的,何况抽烟的时候还要和文哥交流呢。艾玛,不容易,容小弟我抽根烟,ummm,没烟了😅😅😅。

先来看个效果图吧,毕竟我费了好大力气~ (其实想得瑟下~)

在这里插入图片描述

首先是布局:

<com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/nav_bottom_menu"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/colorWhite"
            app:itemBackground="@null"
            app:itemIconTint="@color/tint_selector_menu_color" // 重点在这里
            app:itemTextColor="@color/tint_selector_menu_color"
            app:labelVisibilityMode="labeled"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:menu="@menu/nav_bottom_menu" />
复制代码

编写渲染颜色选择器:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/comm_app_color" android:state_checked="true" />
    <item android:color="@color/color_tab_def" />
</selector>
复制代码

最后,修改咱们的 menu 文件中 icon 直接采用默认图便可:

在这里插入图片描述

2.3 图片的升级 - WebP

引入官方描述:

WebP 是 Google 开发的一种图片文件格式,提供有损压缩(如 JPEG)并支持透明性(如 PNG),不过与 JPEG 或 PNG 相比,这种格式能够提供更好的压缩效果。Android 4.0(API 级别 14)及更高版本支持有损 WebP 图片,Android 4.3(API 级别 18)及更高版本支持无损且透明的 WebP 图片。

注意:因为只有 Android 4.3 及更高版本支持无损且透明的 WebP 图片,所以您的项目声明的 minSdkVersion 必须为 18 或更高,才能使用 Android Studio 建立无损或透明的 WebP 图片。

操做步骤以下:

点击要转换的图片,选择 Convert to WebP...

在这里插入图片描述

采用默认配置:

在这里插入图片描述

预览结果图:

在这里插入图片描述

200 多 kb 降到 20 多 kb,若是项目中此类图片较多时,使用 WebP 确实很爽啊~

固然,关于转换可单张、多张,看我的心情。

经历了最痛苦的时候,咱们一块儿打包看下通过二层过滤,咱们精简了多少?

在这里插入图片描述

Apk 大小减小 1.5 MB,res 占比从 28 % 下降到 15.5 %。其实里面仍是有很大的优化空间,奈何懒癌上身,矫情开始做祟。

三层镇万物(偷个懒,困,减小了 0.4 MB)

通过前俩层的打怪历练,咱们终于要见大 Boss 了。Come on,篇幅过长,灰常感谢可以看到这里,比个心心~

3.1 AndResGuard - 微信资源压缩应用(减小 0.4 MB)

在正式玩微信资源压缩前,咱们先来回顾下以前的混淆,说白了混淆不只仅优化了代码,并且将关键的一些信息经过无心义的标示进行替换,从而进一步加深了反编译破解的难度,仅仅是加深了。而咱们的布局、图片仍是属于赤裸裸的状态,以下所示:

在这里插入图片描述

针对某些安全要求比较高或者有那么一丢丢追求的小伙伴,就是不想赤裸裸给你看,怎么办呢?

AndResGuard 上场~ (对比美团的方案,微信真香)

Step 1:项目根目录 build 添加依赖

dependencies {
        ...
        classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.17'
    }
复制代码

Step 2:app 目录下新建 wechat-ResGuard.gradle 文件:

apply plugin: 'AndResGuard'

andResGuard {
    // mappingFile = file("./resource_mapping.txt")
    mappingFile = null
    use7zip = true
    useSign = true
    // 打开这个开关,会keep住全部资源的原始路径,只混淆资源的名字
    keepRoot = false
    // 设置这个值,会把arsc name列混淆成相同的名字,减小string常量池的大小
    fixedResName = "arg"
    // 打开这个开关会合并全部哈希值相同的资源,但请不要过分依赖这个功能去除去冗余资源
    mergeDuplicatedRes = true
    whiteList = [
            // App Logo 这里对应找本身的 App Logo
            "R.mipmap.ic_launcher",
            "R.mipmap.ic_launcher_foreground",
            "R.mipmap.ic_launcher_round",
            // for fabric
            "R.string.com.crashlytics.*",
            // for google-services
            "R.string.google_app_id",
            "R.string.gcm_defaultSenderId",
            "R.string.default_web_client_id",
            "R.string.ga_trackingId",
            "R.string.firebase_database_url",
            "R.string.google_api_key",
            "R.string.google_crash_reporting_api_key"
    ]
    compressFilePattern = [
            "*.png",
            "*.jpg",
            "*.jpeg",
            "*.gif",
    ]
    sevenzip {
        artifact = 'com.tencent.mm:SevenZip:1.2.17'
        //path = "/usr/local/bin/7za"
    }

    /**
     * 可选: 若是不设置则会默认覆盖assemble输出的apk
     **/
    // finalApkBackupPath = "${project.rootDir}/final.apk"

    /**
     * 可选: 指定v1签名时生成jar文件的摘要算法
     * 默认值为“SHA-1”
     **/
    // digestalg = "SHA-256"
}
复制代码

Step 3:app 下 build 中添加末尾添加依赖

apply from: 'wechat-ResGuard.gradle'
复制代码

Step 4:打包

在这里插入图片描述
在这里插入图片描述

最后咱们看下包里面资源相应的资源是否被混淆了?

在这里插入图片描述

Apk 减小 0.4 MB,资源被混淆~

看了下双十一数据,真有钱。。。

3.2 Link 检查

Analyze 选中 Run Inspection by Name...

在这里插入图片描述

输入 unused re,选择 Unused resources:

在这里插入图片描述

直接采用默认便可:

在这里插入图片描述

根据提示修改便可:

在这里插入图片描述

困了,不改了,改天再说。

3.3 Remove Unused Resources(不推荐使用,除非和我同样脸皮厚,喜欢做)

物理删除未使用的资源文件。

在这里插入图片描述

记得点击中间的,否则给你删没了

在这里插入图片描述

一个个对应,而后开始删除吧~

在这里插入图片描述

选中要移除的,直接 Remove 便可

在这里插入图片描述

丢个包瞅瞅。

在这里插入图片描述

ummm,我好像啥都没弄。算求了。呼呼呼。。。

结束语

墨迹了好几天,终于迈出了一小步。

加油呀~

参考资料

  1. ABI 管理
  2. 使用 Translations Editor 本地化界面
  3. 压缩、混淆和优化您的应用
  4. ProGuard manual
  5. zipalign
  6. 添加多密度矢量图形
  7. Android支持库23.2
  8. 建立 WebP 图片
  9. ImageView
  10. AndResGuard
相关文章
相关标签/搜索