服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

Android性能优化:ProGuard,混淆,R8优化

日期: 来源:郭霖收集编辑:小余同学啊


/   今日科技快讯   /

近日Canalys(科纳仕)发布的2023年第一季度全球智能手机市场的出货调查报告称,中国智能手机市场经历了连续下滑。苹果公司的iPhone 14系列延续去年Q4季度的热度,以20%市场占有率位居中国大陆市场榜首,出货量为1330万部;而OPPO凭借一加新品的助力,OPPO(含一加)以19%市场占有率排名第二,出货量为1260万部;vivo和荣耀的出货量分别为1130万部、970万部,排名下滑至第三和第四,小米则以850万部排在第五名。

/   作者简介   /

明天就是五一小长假了,祝大家节日快乐,我们节后再见!

本篇文章转自小余的自习室的博客,文章主要讲解了ProGuard,混淆,R8优化,相信会对大家有所帮助!

原文地址:
https://juejin.cn/post/7225511164120891453

/   前言   /

使用java编写的源代码编译后生成了对于的class文件,但是class文件是一个非常标准的文件,市面上很多软件都可以对class文件进行反编译,为了我们app的安全性,就需要使用到Android代码混淆这一功能。

针对 Java 的混淆,ProGuard 就是常用的混淆工具,且他不仅仅是混淆工具,它同时可以对代码进行 压缩、优化 、混淆。

下面我们来简单介绍下ProGuard工作流程。

ProGuard 工作流程

ProGuard工作过程包括四个步骤:shrink,optimize,obfuscate,preverigy。这四个步骤都是可选,但是顺序都是不变的



  • shrink:检测并删除项目中未使用到的类,字段,方法以及属性
  • optimize:优化字节码,移除无用指令,或者进行指令优化。
  • obfuscate:代码混淆,将代码中的类,字段,方法和属性等名称使用无意义的名称进行表示,减少代码反编译后的可读性。
  • preverify:针对 Java 1.6 及以上版本进行预校验, 校验 StackMap /StackMapTable 属性.在编译时可以关闭,加快编译速度。

/   代码优化及混淆   /

Android构建中,在AGP3.4.0之前也是使用的ProGuard 进行代码优化混淆,但是在3.4.0之后,谷歌将这一工作赋予给了性能更佳的R8编译器。

虽然摒弃了ProGuard,但是R8编译器还是兼容ProGuard的配置规则

使用R8编译器可以做以下优化:

1.代码缩减
2.资源缩减
3.代码混淆
4.代码优化

1.代码缩减:

代码缩减指的是:R8编译期智能检测代码中未使用到的类、字段、方法和属性等,并移除。

比如你项目中依赖了很多库,但是只使用了库里面少部分代码,为了移除这部分代码,R8会根据配置文件确定应用代码的所有入口点:包括应用启动的第一个Activity或者服务等,R8会根据入口,检测应用代码,并构建出一张图表,列出应用运行过程中可能访问的方法,成员变量和类等,并对图中没有关联到的代码,视为可移除代码。


图中入口位置:MainActivity,整个调用链路中,使用到了foo,bar函数以及AwesomeApi类中的faz函数,所以这部分代码会被构建到依赖图中,而OkayApi类以及其baz函数都未访问到,则这部分代码就可以被优化。

使用方式:

android {
    buildTypes {
        release {
            ...
            minifyEnabled true
        }
    }
    ...
}

minifyEnabled 设置为true,会默认启用R8代码缩减功能。

代码优化需要注意的两种情况:

1. 反射调用的情况
2. JNI调用的情况

R8并未对反射以及JNI等情况进行检测,如果配置文件中未处理,则这部分代码就会被丢弃,会出现NoClassFindException的异常,如何解决呢?

1:在配置文件中使用-keep对这部分类进行说明:

-keep public class MyClass

2:给需要保留的代码添加@keep注解

前提条件:1.声明了使用AndroidX 2.使用AGP默认的ProGuard文件

  • R8配置文件

R8使用ProGuard 规则文件来决定哪部分代码需要保留,配置文件来源分为下面几个:

1.AndroidStudio:

位置:/proguard-rules.pro

说明:创建新的模块时,会在当前模块目录下创建一个默认的:proguard-rules.pro 文件

2.AGP插件

位置:由AGP在编译时生成的proguard-android-optimize.txt

说明:Android Gradle 插件会生成 proguard-android-optimize.txt(其中包含了对大多数 Android 项目都有用的规则),并启用 @Keep* 注解。

编译后在\build\intermediates\proguard-files\目录下会生成3个文件:


其中proguard-android-optimize.txt-4.1.1是需要进行optimize代码优化的ProGuard配置文件,而proguard-android.txt-4.1.1表示不需要进行optimize代码优化的ProGuard文件。4.1.1:表示当前模块的AGP插件版本。

3.库依赖项

位置:

AAR 库:/proguard.txt
JAR 库:/META-INF/proguard/

说明:引入的aar或者jar包的库中,默认包含proguard优化规则,则在编译过程中也会被纳入R8配置项中,所以特别注意aar中引入的proguad和原项目规则冲突的情况。

4.AAPT2(Android资源打包工具)

位置:使用 minifyEnabled true 构建项目后:/build/intermediates/proguard-rules/debug/aapt_rules.txt

说明:AAPT2 会根据对应用清单中的类、布局及其他应用资源的引用,生成保留规则。例如,AAPT2 会为您在应用清单中注册为入口点的每个 activity 添加一个保留规则。

5.自定义配置文件

位置:默认情况下,当您使用 Android Studio 创建新模块时,IDE 会创建 /proguard-rules.pro,以便您添加自己的规则。

说明:您可以添加其他配置(https://developer.android.com/studio/build/shrink-code?hl=zh-cn#add-configuration),R8 会在编译时应用这些配置。如果您将 minifyEnabled 属性设为 true,R8 会将来自上述所有可用来源的规则组合在一起,但是需要注意依赖库引入导致的规则冲突问题。

如果需要输出 R8 在构建项目时应用的所有规则的完整报告:可以添加下面语句到proguard-rules.pro中

// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt

如果需要添加额外的proguad文件:可以通过将相应文件添加到模块的 build.gradle 文件的 proguardFiles 属性中:

如分别给每个productflavor添加规则可以使用下面这种方式:

android {
    ...
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles
                getDefaultProguardFile('proguard-android-optimize.txt'),
                // List additional ProGuard rules for the given build type here. By default,
                // Android Studio creates and includes an empty rules file for you (located
                // at the root directory of each module).
                'proguard-rules.pro'
        }
    }
    flavorDimensions "version"
    productFlavors {
        flavor1 {
            ...
        }
        flavor2 {
            proguardFile 'flavor2-rules.pro'
        }
    }
}

2.资源缩减

资源缩减是在代码缩减之后进行的,只有去除了不需要的代码后, 才可以知道哪些代码里面的资源也是不被引入,可以移除的。

资源缩减只要在模块gradle下面添加shrinkResources属性即可:

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            ...
        }
    }
}

注意:资源缩减需要提前开启代码缩减minifyEnabled。

当然你也可以对资源文件添加白名单:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />

使用tools:keep指定需要保留的资源文件,使用tools:discard指定可以舍弃的资源文件

3.代码混淆

混淆指的是将类名,方法名,属性名使用无意义的字符来表示:看下图:


  • 如何进行混淆?

混淆的一些基本规则:

一颗星:表示保留当前包下的类名,如果有子包,子包中的类会被混淆

-keep class cn.hadcn.test.*

两颗星:表示保留当前包下的类名,如果有子包,子包中的类名也会被保留。

-keep class cn.hadcn.test.**

上面的方式虽然保留了类名,但是内容还是会被混淆,使用下面方式保留内容:

-keep class cn.hadcn.test.* {*;}

在此基础上,我们也可以使用Java的基本规则来保护特定类不被混淆,比如我们可以用extends,implements等这些Java规则。如下例子就避免所有继承Activity的类被混淆:

-keep public class * extends android.app.Activity

如果我们要保留一个类中的内部类不被混淆则需要用$符号,如下例子表示保持ScriptFragment内部类JavaScriptInterface中的所有public内容不被混淆。

-keepclassmembers class cc.ninty.chat.ui.fragment.ScriptFragment$JavaScriptInterface {
   public *;
}

再者,如果一个类中你不希望保持全部内容不被混淆,而只是希望保护类下的特定内容,就可以使用:

<init>;     //匹配所有构造器
<fields>;   //匹配所有域
<methods>;  //匹配所有方法方法

你还可以在或前面加上private 、public、native等来进一步指定不被混淆的内容,如

-keep class cn.hadcn.test.One {
    public <methods>;
}

当然你还可以加入参数,比如以下表示用JSONObject作为入参的构造函数不会被混淆:

-keep class cn.hadcn.test.One {
   public <init>(org.json.JSONObject);
}

有时候你是不是还想着:我不需要保持类名,我只需要把该类下的特定方法保持不被混淆就好,那你就不能用keep方法了,keep方法会保持类名,而需要用keepclassmembers ,如此类名就不会被保持,为了便于对这些规则进行理解,官网给出了以下表格:


# -keep关键字
# keep:包留类和类中的成员,防止他们被混淆
# keepnames:保留类和类中的成员防止被混淆,但成员如果没有被引用将被删除
# keepclassmembers :只保留类中的成员,防止被混淆和移除。
# keepclassmembernames:只保留类中的成员,但如果成员没有被引用将被删除。
# keepclasseswithmembers:如果当前类中包含指定的方法,则保留类和类成员,否则将被混淆。
# keepclasseswithmembernames:如果当前类中包含指定的方法,则保留类和类成员,如果类成员没有被引用,则会被移除。

  • 注意事项

1. 使用AS4.1.1版本进行混淆编译后,在 /build/outputs/mapping/release/下面会生成以下四个文件:

  • configuration:所有ProGuard文件整合后的规则文件:

  • mapping:混淆前后的映射文件,这个文件在使用反混淆的时候有用。

  • seeds:未进行混淆的类和成员。

  • usage:未使用的文件,也就是移除后的文件。


2. 有使用jni的情况下,jni方法不可混淆

-keepclasseswithmembernames class * {    
    native <methods>;
}

3. 反射用到的类不混淆。

4. AndroidMainfest中的类不混淆,所有四大组件和Application的子类和Framework层下所有的类默认不会进行混淆,自定义的View默认也不会被混淆。所以像网上贴的很多排除自定义View,或四大组件被混淆的规则在Android Studio中是无需加入的。

5. 与服务端交互时,使用GSON、fastjson等框架解析服务端数据时,所写的JSON对象类不混淆,否则无法将JSON解析成对应的对象;

6. 使用第三方开源库或者引用其他第三方的SDK包时,如果有特别要求,也需要在混淆文件中加入对应的混淆规则;

7. 有用到WebView的JS调用也需要保证写的接口方法不混淆,原因和第一条一样;

8. Parcelable的子类和Creator静态成员变量不混淆,否则会产生Android.os.BadParcelableException异常;

# 保持Parcelable不被混淆    
-keep class * implements Android.os.Parcelable {         
    public static final Android.os.Parcelable$Creator *;
}

9. 使用enum类型时需要注意避免以下两个方法混淆,因为enum类的特殊性,以下两个方法会被反射调用,见第二条规则。

-keepclassmembers enum * {  
    public static **[] values();  
    public static ** valueOf(java.lang.String);  
}

  • 混淆模板

下面是一个混淆模板:可根据自身需要进行添加和删除:

#--------------------------1.实体类---------------------------------
# 如果使用了Gson之类的工具要使被它解析的JavaBean类即实体类不被混淆。(这里填写自己项目中存放bean对象的具体路径)
-keep class com.php.soldout.bean.**{*;}

#--------------------------2.第三方包-------------------------------

#Gson
-keepattributes Signature
-keepattributes *Annotation*
-keep class sun.misc.Unsafe { *; }
-keep class com.google.gson.stream.** { *; }
-keep class com.google.gson.examples.android.model.** { *; }
-keep class com.google.gson.* { *;}
-dontwarn com.google.gson.**

#butterknife
-keep class butterknife.** { *; }
-dontwarn butterknife.internal.**
-keep class **$$ViewBinder { *; }

#-------------------------3.与js互相调用的类------------------------


#-------------------------4.反射相关的类和方法----------------------


#-------------------------5.基本不用动区域--------------------------
#指定代码的压缩级别
-optimizationpasses 5

#包明不混合大小写
-dontusemixedcaseclassnames

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

#混淆时是否记录日志
-verbose

#优化  不优化输入的类文件
-dontoptimize

#预校验
-dontpreverify

# 保留sdk系统自带的一些内容 【例如:-keepattributes *Annotation* 会保留Activity的被@override注释的onCreate、onDestroy方法等】
-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod

# 记录生成的日志数据,gradle build时在本项根目录输出
# apk 包内所有 class 的内部结构
-dump proguard/class_files.txt
# 未混淆的类和成员
-printseeds proguard/seeds.txt
# 列出从 apk 中删除的代码
-printusage proguard/unused.txt
# 混淆前后的映射
-printmapping proguard/mapping.txt


# 避免混淆泛型
-keepattributes Signature
# 抛出异常时保留代码行号,保持源文件以及行号
-keepattributes SourceFile,LineNumberTable

#-----------------------------6.默认保留区-----------------------
# 保持 native 方法不被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}

-keepclassmembers public class * extends android.view.View {
 public <init>(android.content.Context);
 public <init>(android.content.Context, android.util.AttributeSet);
 public <init>(android.content.Context, android.util.AttributeSet, int);
 public void set*(***);
}

#保持 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();
}

# 保持自定义控件类不被混淆
-keepclasseswithmembers class * {
    public <init>(android.content.Context,android.util.AttributeSet);
}
# 保持自定义控件类不被混淆
-keepclasseswithmembers class * {
    public <init>(android.content.Context,android.util.AttributeSet,int);
}
# 保持自定义控件类不被混淆
-keepclassmembers class * extends android.app.Activity {
    public void *(android.view.View);
}

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

# 保持 Parcelable 不被混淆
-keep class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator *;
}

# 不混淆R文件中的所有静态字段,我们都知道R文件是通过字段来记录每个资源的id的,字段名要是被混淆了,id也就找不着了。
-keepclassmembers class **.R$* {
    public static <fields>;
}

#如果引用了v4或者v7包
-dontwarn android.support.**

# 保持哪些类不被混淆
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Fragment
-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.preference.Preference

-keep class com.zhy.http.okhttp.**{*;}
-keep class com.wiwide.util.** {*;}

# ============忽略警告,否则打包可能会不成功=============
-ignorewarnings

代码优化

为了进一步缩减应用,R8 会在更深的层次上检查代码,以移除更多不使用的代码,或者在可能的情况下重写代码,以使其更简洁。下面是此类优化的几个示例:

  • 如果您的代码从未采用过给定 if/else 语句的 else {} 分支,R8 可能会移除 else {} 分支的代码
  • 如果您的代码只在一个位置调用某个方法,R8 可能会移除该方法并将其内嵌在这一个调用点
  • 如果 R8 确定某个类只有一个唯一子类且该类本身未实例化(例如,一个仅由一个具体实现类使用的抽象基类),它就可以将这两个类组合在一起并从应用中移除一个类。

更多优化点可以查看:Jake Wharton 写的关于 R8 优化的博文(https://jakewharton.com/blog/)。

/   总结   /

本篇文章主要讲解了关于R8编译器在整个编译过程中对apk代码以及资源的一些优化操作,主要集中在代码缩减,资源缩减,代码混淆,代码优化这几部分,其中对代码混淆做了一个比较全面的分析,后期还会讲解一些关于代码反混淆的知识,这篇文章就讲到这里了

参考:

Android官方R8优化(https://developer.android.com/studio/build/shrink-code?hl=zh-cn#optimization)

guardsquare(https://www.guardsquare.com/manual/home)

Android编译优化:D8和R8(https://blog.csdn.net/wwj_748/article/details/115874571)

android打包混淆及语法规则详解(https://blog.51cto.com/u_15298568/3052858)

深入 Android 混淆实践:ProGuard 通关秘籍(https://blog.csdn.net/lovecluo/article/details/127020150)

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
腾讯零反射全动态Android插件框架,一文解析
微软的人工智能 Copilot 到底有多强大?带你来体验一下

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注

相关阅读

  • 掌握 Linux 命令行的 10 个高级 Shell 脚本命令

  • 击上方蓝字 ● 关注Linux公社 Shell脚本是一种强大的工具,可以让你的工作更轻松高效。如果你是Linux用户,你可能熟悉基本的shell命令,比如echo,cd和ls。然而,还有更高级的命
  • 如何利用 GPT 开发智能应用?微软提出这九大原则

  • 点击上方蓝字关注我们(本文阅读时间:6分钟)毫无疑问,GPT 的出现以日新月异的速度改写了软件开发的工作方式。兴奋,也许也有点焦虑。作为开发人员,如何在 GPT 时代与人工智能协作?
  • 【电手珍藏】电脑必装神级软件推荐合集(1)

  • 新电脑到手,或者说重装系统后,有哪些软件是你一定会第一时间装上的呢?作为在电脑界摸麻滚她的老司机,为了让广大电粉都能用上真正良心,无广的优质软件,小淙决定掏出压箱底的珍藏,造
  • 命令执行漏洞利用及绕过方式总结

  • 命令注入常见的方法1.常见管道符  Windows系统支持的管道符  Linux系统支持的管道符2.空格过滤  ${IFS}  重定向符<>  %09(需要php环境)3.黑名单绕过  拼接  ba
  • CTF-web--命令注入

  • 大佬总结的文章,本篇文章阅读时间大约30分钟。一 、基本原理命令注入指的是,利用没有验证过的恶意命令或代码,对网站或服务器进行渗透攻击。注入有很多种,并不仅仅只有SQL注入。
  • Kotlin跨平台技术,使用Kotlin编写Web应用程序

  • / 今日科技快讯 /4月26日新闻报道,英国竞争和市场管理局(CMA)采取了行动阻止微软收购动视暴雪的交易,以保护云游戏中的创新和选择。英国竞争和市场管理局系英国最高竞争监管
  • 太强了!Linux 文件目录快速自动跳转命令

  • 击上方蓝字 ● 关注Linux公社 那些主要通过控制台/终端使用 Linux 命令行的 Linux 用户应该感受到了 Linux 的真正力量。然而,对于新手来说,导航 Linux 分层文件系统有时
  • 2023 年,我建议创业公司选择 Flutter

  • 作者 | CHRISTIAN FINDLAY
    译者 | 核子可乐
    策划 | 丁晓昀 作为一家初创企业,为自己的首款应用程序选择正确技术堆栈无疑至关重要。您的具体技术选择将直接影响到产品

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四
  • 美国对华2000亿关税清单,到底影响有多大?

  • 1 今天A股大跌,上证最大跌幅超过2%。直接导火索是美国证实计划对华2000亿美元产品加征25%关税。 听起来,2000亿美元数目巨大,我们来算笔账。 2000亿美元,按现在人民币汇率

最新文章

  • Android性能优化:ProGuard,混淆,R8优化

  • / 今日科技快讯 /近日Canalys(科纳仕)发布的2023年第一季度全球智能手机市场的出货调查报告称,中国智能手机市场经历了连续下滑。苹果公司的iPhone 14系列延续去年Q4季度的
  • 因“利益冲突”,马斯克离开 OpenAI

  • 点蓝字关注,一起程序员弯道超车之路在 ChatGPT 的影响下,目前 OpenAI 成为了全球最火爆的 AI 公司。然而回顾过去,它与马斯克之间的恩怨从创立之初就埋下了。原文链接:https://w
  • 大家好,我是...

  • 大家好,我是子木,一直在从事 SaaS 软件行业,并一直研究付费软件。现在我刚刚 30 岁,预计还有 10 年时间来磨砺这个方向。感谢 360+ 圈友的信任和支持,在知识星球上写相关笔记,目前
  • 掌握 Linux 命令行的 10 个高级 Shell 脚本命令

  • 击上方蓝字 ● 关注Linux公社 Shell脚本是一种强大的工具,可以让你的工作更轻松高效。如果你是Linux用户,你可能熟悉基本的shell命令,比如echo,cd和ls。然而,还有更高级的命