当前位置: 首页 > 科技观察

请吃一顿Android困惑综合餐

时间:2023-03-19 00:52:18 科技观察

在Android的日常开发过程中,困惑是我们开发App的必备技能。只要是亲身经历过APP打包上线的过程,都或多或少需要了解一些代码混淆的基本操作。那么,究竟什么是迷惑呢?它有什么好处?具体作用是什么?别着急,让我们一一探索它的“独特”魅力。混淆简介代码混淆(Obfuscatedcode)是将程序中的代码按照一定的规则转换成难以阅读和理解的代码的行为。混淆的好处混淆的好处在于其目的:使APK难以被逆向工程,即大大增加反编译的成本。此外,Android中的“混淆”还可以在打包时去除无用资源,显着降低APK体积。最后,有一个解决方法可以避免Android中常见的64k方法引用限制。我们来看一下混淆前后的APK结构对比:从上面两张图可以看出,经过混淆后,我们APK中的包名、类名、成员名等都被随机替换成了无意义的名字。增加了代码阅读和理解的难度,增加了反编译的成本。细心的朋友可能又注意到了:混淆前后的APK大小从2.7M缩小到1.4M,体积缩小了近一倍!真的有这么神奇吗?哈哈,真是太神奇了,让我们慢慢揭开它的神秘面纱吧。Android中的混乱在Android中,我们通常所说的“混乱”其实有两层含义,一是Java代码的混乱,二是资源的压缩。其实两者并没有什么联系,只是习惯性的搭配在一起使用而已。那么,说了这么多,如何在Android平台启用混淆呢?开启混淆...android{buildTypes{release{minifyEnabledtrueshrinkResourcestrueproguardFilesgetDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'}}}以上就是开启混淆的基本操作,通过设置minifyEnabled为true来开启混淆.同时可以将shrinkResources设置为true来开启资源压缩。不难看出,我们一般只在启动发布包的时候开启混淆,因为混淆会增加额外的编译时间,所以不建议在debug模式下开启。另外需要注意的是,资源压缩只有开启混淆才会生效!上面代码中的proguard-android.txt代表的是Android系统默认提供的混淆规则文件,而proguard-rules.pro是我们要自定义的混淆规则,至于如何自定义混淆规则,后面会讲到接下来。代码混淆其实Java平台为我们提供了Proguard混淆工具,可以帮助我们快速对代码进行混淆。根据Java官方介绍,Proguard的具体中文定义如下:是一个包含代码文件压缩、优化、混淆、校验等功能的工具。它可以检测和删除无用的类、变量、方法和属性。它可以优化字节。它可以将类、变量和方法的名称重命名为无意义的名称,以实现混淆。最后,它还会对处理后的代码进行校验,主要针对Java6及以上版本和JavaME资源压缩在Android中,编译器为我们提供了另一个强大的功能:资源压缩。资源压缩可以帮助我们去除项目和依赖仓库中不用的资源,有效的减小apk包的体积。由于资源压缩和代码混淆是协同工作的,如果需要开启资源压缩,记得先开启代码混淆,否则会出现以下问题:-resources.htmlfororeinformation.AffectedModules:app自定义要保留的资源。当我们启用资源压缩时,系统会默认为我们移除所有未使用的资源。如果我们需要保留一些特定的资源,我们可以在我们的项目中添加一个标记的XML文件(如res/raw/keep.xml),并在tools:keep属性中指定每个要保留的资源,并且每个tools:discard属性中要丢弃的资源。这两个属性都接受以逗号分隔的资源名称列表。同样,我们可以使用字符*作为通配符。如:启用严格检查模式一般情况下,资源压缩器可以准确判断系统是否使用资源。但是,如果您的代码(包括库)调用Resources.getIdentifier(),这意味着您的代码将根据动态生成的字符串查找资源名称。此时,资源压缩器采取防御措施,将所有具有匹配名称模式的资源标记为可能使用且无法删除。例如,以下代码会将所有以img_为前缀的资源标记为已使用:Stringname=String.format("img_%1d",angle+1);res=getResources().getIdentifier(name,"drawable",getPackageName());这时候我可以开启资源的严格审核模式,只保留确定要使用的资源。移除备用资源Gradle资源压缩器只会移除应用程序未引用的资源,这意味着它不会移除不同设备配置的备用资源。必要的时候,我们可以使用AndroidGradle插件的resConfigs属性,去掉你的应用不需要的备份资源文件(常用的strings.xml用于国际化支持,layout.xml用于适配等):android{defaultConfig{...//保留中英文国际化支持resConfigs"en","zh"}}自定义混淆规则尝完上面的“小菜”,我们来尝尝本文的“主菜”:自定义混淆规则。首先,让我们看一下常见的令人困惑的命令。keep命令这里所说的keep命令是指以-keep开头的一系列命令,主要用于保留Java中不需要混淆的元素。以下是常用的-keep命令:-keep功能:保留指定的类和成员,防止混淆。例如:#keep包:com.moos.media.entity下的类和类成员-keeppublicclasscom.moos.media.entity.**#keepclass:NumberProgressBar-keeppublicclasscom.moos.media.widget.NumberProgressBar{*;}-keepclassmembers功能:保留指定类的成员(变量/方法),不会混淆。如:#Reserve类的成员:MediaUtils类中的具体成员方法-keepclassmembersclasscom.moos.media.MediaUtils{publicstatic***getLocalVideos(android.content.Context);publicstatic***getLocalPictures(android.content.Context);}-keepclasseswithmembers功能:保留指定的类及其成员(变量/方法),前提是它们在压缩阶段没有被删除。类似于-keep用法:#keepclass:BaseMediaEntity的子类-keepclasseswithmemberspublicclass*extendscom.moos.media.entity.BaseMediaEntity{*;}#keepclass:OnProgressBarListener接口的实现类-keeppublicclass*implementscom.moos.media.widget.OnProgressBarListener{*;}@Keep除了上述方法外,您还可以选择使用@Keep注解来保留想要的代码,防止它们被混淆。比如我们通过@Keep修改一个类,让它不被混淆:@KeepdataclassCloudMusicBean(varcreateDate:String,varid:Long,varname:String,varurl:String,valimgUrl:String)同样,我们也可以让@Keep被修改方法或字段来保存它们。其他命令dontwarn-dontwarn命令一般在我们引入新库时使用,常用于处理库中无法解决的警告。例如:-keepclasstwitter4j.**{*;}-dontwarntwitter4j.**其他命令用法可以参考Android系统默认提供的混淆规则:#混淆时不生成大小写混合的类名-dontusemixedcaseclassnames#不跳过非-publicLibraryclass-dontskipnonpubliclibraryclasses#在混淆过程中记录日志-verbose#关闭预验证-dontpreverify#关闭优化-dontoptimize#保留注解-keepattributes*Annotation*#保留所有类名和本地方法名与本地方法-keepclasseswithmembernamesclass*{native;}#保留自定义View的get和set方法-keepclassmemberspublicclass*extendsandroid.view.View{voidset*(***);***get*();}#保留View及其children在Activity类中输入参数的方法,如:onClick(android.view.View)-keepclassmembersclass*extendsandroid.app.Activity{publicvoid*(android.view.View);}#keepenumeration-keepclassmembersenum*{**[]$VALUES;public*;}#保留序列号zedclass-keepclassmembersclass*implementsandroid.os.Parcelable{publicstaticfinalandroid.os.Parcelable$CreatorCREATOR;}#保留R文件的静态成员-keepclassmembersclass**.R$*{publicstatic;}-dontwarnandroid.support.**-keepclassandroid.support.annotation.Keep-keep@android.support.annotation.Keepclass*{*;}-keepclasseswithmembersclass*{@android.support.annotation.Keep;}-keepclasseswithmembersclass*{@android.support.annotation.Keep;}-keepclasseswithmembersclass*{@android.support.annotation.Keep(...);}混淆《黑名单》我们了解了混淆的基本命令后,应该很多人还是很困惑:应该对哪些内容进行混淆?事实上,当我们使用代码混淆时,ProGuard混淆了我们项目中的大部分代码。为了防止编译时出错,我们应该通过keep命令来防止一些元素被混淆。因此,我们只需要知道哪些元素不应该混淆:枚举项中不可避免地可能会使用枚举类型,但不能参与混淆。去。原因是:枚举类内部有一个values方法。混淆后,该方法将被重命名并抛出NoSuchMethodException。好在Android系统默认的混淆规则已经加入了对枚举类的处理,我们不需要做额外的工作。如果想了解更多枚举的内部细节,可以查看源码,限于篇幅不再赘述。反射元素使用的类、变量、方法、包名等不应混淆。原因是在代码混淆的过程中,反射使用的元素会被重命名,但是反射还是按照之前的名字去寻找元素,所以经常会出现NoSuchMethodException和NoSuchFiledException。实体类实体类就是我们常说的“数据类”,当然它往往伴随着序列化和反序列化操作。很多人应该也想到了,混淆就是把原本有特定含义的“元素”变成一个没有意义的名字。所以,经过混淆的“洗礼”,序列化后value对应的key变成了无意义的字段,这绝对不是我们想要的。同时,反序列化过程中对象的创建从根本上是基于反射的。混淆之后key会变,所以也会违背我们预期的效果。四个组件Android中的四个组件也不应该混淆。原因是四大组件在使用前需要在AndroidManifest.xml文件中注册声明。但是经过混淆后,四大组件的类名会被篡改,实际使用的类与manifest中注册的类不匹配。因此错误。当其他应用程序访问组件时,它们可能会使用类的包名加上类名。如果混淆了,可能会找不到对应的组件或者产生异常。JNI调用的Java方法当JNI调用的Java方法经过混淆后,方法名会变成无意义的名字,与C++中原来的Java方法名不匹配,从而找不到被调用的方法。其他不应该混淆的自定义控件不需要混淆。不应混淆对Java方法的Javascript调用。不应混淆Java本机方法。项目中引用的第三方库不建议混淆。ProGuard混淆了混淆的堆栈跟踪代码。之后,读取StackTrace(堆栈跟踪)信息变得非常困难。因为方法名和类名被混淆了,即使程序崩溃也很难定位到问题所在。幸运的是,ProGuard为我们提供了补救措施,在继续之前,让我们先看看每次构建后ProGuard会生成什么。混淆输出结果混淆构建完成后,会在/build/outputs/mapping/release/目录下生成如下文件:dump.txt描述了APK中所有class文件的内部结构。mapping.txt提供混淆前后的内容对比表,内容主要包括类、方法、类的成员变量。seeds.txt列出了未混淆的类和成员。usage.txt列出了从APK中删除的代码。恢复stacktrace了解了混淆完成后的输出,再来看之前的问题:混淆处理后,StackTrace很难定位。如何恢复StackTrace的定位能力?系统为我们提供了retrace工具,结合上面提到的mapping.txt文件,可以将混淆后的crashstacktrace信息恢复为正常的StackTrace信息。有两种主要的方法来恢复StackTrace。为了方便理解,我们以下面的崩溃信息为例,通过两种方式恢复:java.lang.RuntimeException:UnabletostartactivityCausedby:kotlin.KotlinNullPointerExceptionatcom.moos.media.ui.ImageSelectActivity.k(ImageSelectActivity.kt:71)atcom.moos.media.ui.ImageSelectActivity.onCreate(ImageSelectActivity.kt:58)atandroid.app.Activity.performCreate(Activity.java:6237)atandroid.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1107)通过retrace脚本tool,首先我们需要进入AndroidSDK路径的/tools/proguard/bin目录下。这里以Mac系统为例,可以看到如下内容:可以看到上面三个文件,其中proguardgui.sh就是我们需要的retrace脚本(Windows下是proguardgui.bat)。Windows系统下,只需双击脚本proguardgui.bat即可运行。Mac系统,如果你没有做任何配置,只需要将proguardgui.sh脚本拖到Mac自带的终端,回车即可运行。然后,我们会看到如下界面:选择ReTrace栏,在我们的项目中添加混淆生成的mapping.txt文件的位置,然后将我们混淆后的崩溃信息复制到Obfuscatedstacktrace栏,点击ReTrace!button我们的crashlog信息就可以恢复了,如上图,我们之前的混淆日志:atcom.moos.media.ui.ImageSelectActivity.k(ImageSelectActivity.kt:71)恢复到atcom.moos。media.ui.ImageSelectActivity.initView(ImageSelectActivity.kt:71)。ImageSelectActivity.k是我们混淆后的方法名,ImageSelectActivity.initView是原来未混淆后的方法名。借助ReTrace工具,我们可以像之前一样快速定位到崩溃代码区域。通过retrace命令行,我们首先需要将crash信息复制到一个txt格式的文件中(如:proguard_stacktrace.txt)并保存,然后执行如下命令(MAC系统):retrace.sh-verbosemapping.txtproguard_stacktrace.txt如果你是windows系统,可以执行如下命令:retrace.bat-verbosemapping.txtproguard_stacktrace.txt最终恢复结果和之前一样:可能你在通过以上两种方式恢复stackTrace的时候发现UnknownSourceproblems:值得注意的是,记得在混淆规则中添加如下配置,提高我们的StackSource搜索效率:#keepthesourcefilenameandspecificcodelinenumber-keepattributesSourceFile,LineNumberTable另外,每次使用ProGuard创建releasebuild时,我们会覆盖之前版本的mapping.txt文件,所以每次发布新版本都要小心保存一份。通过为每个发布版本保留mapping.txt文件的副本,我们可以在用户提交的混淆StackTraces中调试和修复旧版本应用程序的问题。升势的操作经过上面的介绍我们知道,APK经过混淆后,包名、类名、成员名都被转化为无意义、不可理解的名字,增加了反编译的成本。AndroidProGuard为我们提供了一个默认的“混淆字典”,它将元素名称转换为英文小写字母。那么,我们可以定义自己的混淆字典吗?先来看一张效果图:这波操作是不是有点“出众”?哈哈,不是什么技巧,其实很简单,生成你自己的txt格式的混淆字典,然后在混淆规则Proguard-rules.pro中应用:本文使用的混淆字典可以在这里查看和下载的当然,你也可以自定义自己的“混淆词典”,增加反编译的难度。一路走来,我们可以发现,从混淆技术的必要性和优势来看,还是值得我们深入研究的和研究。本文为您展示的只是“冰山一角”,由于本人技术水平有限,如果您发现任何问题或解释不当,请指出并指正。