本片文章的主要内容如下:
.apk文件其实就是一个压缩包,把文件的后缀改成.zip,用压缩软件解压搜就可的下图(我是mac)
解压缩.png
我们就把上面的内容简单介绍下:
现在官网支持中文哦,强烈大家先去把官网读一遍 Android Developer官网,点击查看 如下图
官网.png
下图的是官网对于Android编译打包流程的介绍:
官方流程.png
虚线方框是打包APK的操作,现在开发Android都是使用的Android Studio基于gradle来构建项目,所有打包操作都是执行gradle脚本来完成,gradle编译脚本具有强大的功能,我们可以在里面完成多渠道,多版本,不同版本使用不同代码,不同的资源,编译后的文件重命名,混淆签名验证等等配置,虽然都是基于AndroidSdk的platform-tools的文件夹下面的工工具来完成的,但是有了gradle这个配置文件,这样就便捷了。
相关介绍.png.png
PS:这里补充下apkbuilder在SDK3.0之前使用apkbuilder去打包,在SDK3.0之后就弃用了,而使用sdklib.jar打包apk。
如果你有下载Android系统源码,会发现源码目录下搜索apkbuilder,在sdk中有apkbuilder文件夹,里面有个readme文档,说明如下:
The apkbuilder command linetool is deprecated, and is not maintained anymore.
It is lacking recent buildimprovements such as support for Library Projects.
Its source code has been movedinto sdklib.
It is recommended to directlyuse the com.android.sdklib.build.ApkBuilder class instead.
就是说明此命令行工具新版本SDK目录去掉了,不再支持使用,它的代码放进了sdklib.jar可以直接使用。 其实,以前的apkbuilder.bat内部也是执行
com.android.sdklib.build.ApkBuilderMain
这个类其实就是在sdklib里面。
提供一张APK打包流程图如下:
APK打包流程图.png
整体概述如下:
下面我们就详细看下
aapt
生成过程主要是调用了aapt源码目录下的Resouce.cpp文件的buildResources()函数,该函数首先检查AndroidManifest.xml的合法性,然后对res目录下的资源目录进行处理,处理函数为makeFileResource(),处理的内容包括资源文件名的合法性检查,向资源表table添加条目等,处理完后调用compileResourceFile()函数编译res与asserts目录下的资源并生成resource.arsc文件,compileResourceFile()函数位于appt源码目录的ResourceTable.cpp文件中,该函数最后会调用parseAndAddEntry()函数生成R.java文件,完成资源编译后,接下来调用compileXmlfile()函数对res目录的子目录下的xml文件进行编译,这样处理过的xml文件就简单的被"加密"了,最后将所有资源与编译生成的resource.arsc文件以及"加密"过的AndroidManifest.xml打包压缩成resources.ap_文件。
上面涉及的源码代码位置在:
打包好的资源包括:
打包资源的工具aapt,大部分文本格式的XML资源文件会被编译成二进制格式的XML资源文件,除了assets和res/raw资源被原封不动地打包进APK之外,其他资源都会被编译或者处理。
PS:
大体情况如下:
image.png
源码文件、aidl文件、framework.aidl文件
AIDL工具
对应的.java文件
对于没有使用到的aidl的android工程,这一步可以跳过,aidl工具解析接口定义文件并生成相应的.java文件,供程序调用
源码文件包括
javac 工具
这里调用了javac编译工程的src目录下所有的java源文件,生成的class文件位于工程的bin\classess目录下,上面假定编译源代码时程序是基于android SDK 开发的,实际开发过程中,也有可能会使用android NDK来编译native代码,因此,如果可能的话,这一步还需要使用android NDK编译C/C++代码,当然,编译C/C++代码的步骤也可以提前到第一步或第二步。
.class文件
.class文件,主要包括AIDL生成的.class文件,R生成的.class文件,源文件生成的.class文件、.jar库文件
dx
前面提到,Android系统的dalvik虚拟机的可执行文件为dex格式,程序运行所需的classes.dex文件就是在这一步生成的,使用的工具为dx,dx工具主要的工作是将java字节码转换为dalvik字节码、压缩常量池、消除冗余信息等。
.dex文件
class与dex.png
3.0之前用apkbuilder工具,但是apkbuilder内部也是引用sdklib的ApkBuilderMain,所以3.0之后直接使用了sdklib的ApkBuilderMain
打包工具为apkbuilder,apkbuilder为一个脚本文件,实际调用的是android-sdk/tools/lib/sdklib.jar文件中的com.android.sdklib.build.ApkBuilderMain类。它的代码实现位于android系统源码的sdk/sdkmanager/libs/sdklib/src/com/android/sdklib/build/ApkBuilderMain.java文件,代码构建了一个ApkBuilder类,然后以包含resources.arsc文件为基础生成一个apk文件,这个文件一般为ap_结尾,接着调用addSourceFolder()函数添加工程资源,addSourceFolder()会调用processFileForResource()函数往apk文件中添加资源,处理内容包括res目录和asserts目录中的文件,添加完资源后调用addResourceFromJar()函数往apk文件中写入依赖库,接着调用 addNativeLibraries()函数添加工程libs目录下的Nativie库,最后调用sealApk(),关闭apk文件。
未签名的.apk文件
未签名的.apk文件
jarsigner
android的应用程序需要签名才能在android设备上安装,签名apk文件有两种情况:
签名的apk文件
对齐的作用就是减少运行内存的使用。
签名后的.apk文件
zipalign工具
这一步需要使用的工具为zipalign,它位于android-sdk/tools目录,源码位于android系统资源的build/tools/zipalign目录,它的主要工作是将apk包进行对齐处理,使apk包中的所有资源文件举例文件起始偏移为4字节的整数倍,这样通过内存映射访问apk时的速度会更快,验证apk文件是否对齐过的工作由ZipAlign.cpp文件的verify()函数完成,处理对齐的工作则由process()函数完成。
对齐后的apk文件
整体的细节流程如下图:
流程细节.png
在Android.mk中有LOCAL_AAPT_FLAGS配置项,在gradle中也有aaptOptions,那么aapt到底是干什么?
aapt即Android Asset Packaging Tool (Android 打包工具),在SDK的build-tools目录下。大家可以自行查看。它可以将资源文件编译成二级制文件,尽管你可能没有直接使用过aapt工具,但是build scripts 和IDE插件会使用这个工具打包APK文件构成Android应用程序
aapt传统的打包主要指的是res和Java代码的打包,aapt打包走的是单线程,流水式的任务从上到下进行打包构建。传统的aapt打包,aapt会执行2次,第一次是生成R.java,参与javac编译,第二次是对res里面的资源文件进行编译,最后将Dex文件与编译好的资源文件打包成apk,进行签名。整个流程下来没有任务缓存,没有并发,也没有增量,每次构建都是一个全新的流程。所以每次构建时间也比较恒定,代码量,资源量越多,构建的时间越慢。
如下图
aapt.png
主要是因为两个原因:
这个问题是由于DEX文件格式限制,一个DEX文件中的method个数采用使用原生类型short来索引文件的方法,也就是4个字节共计最多表达65536个method,field/class个数也均有此限制,对于DEX文件,则是将工程所需要全部class文件合并压缩到一个DEX文件期间,也就是Android打包的DEX过程中,单个DEX文件可被引用的方法总数(自己开发的代码以及所引用的Android框架、类库的代码)被限制为66536。
对齐是为了加快资源的访问速度。如果每个资源的开始位置上都是一个资源之后的4n字节,那么访问下一个资源就不用遍历,直接跳到4字节,那么访问下一个资源就不用遍历,直接跳到4*n字节处判断是不是一个新的资源即可。有点类似于资源数组化,数组的访问速度当然比链表块
aapt工具对每个资源文件都生成了唯一的ID,这些ID保存在R.java文件中。资源ID是一个4字节的的无符号证书,在R.java文件中用16进程表示。其中,最高的1字节表示Package ID,次高1个字节表示Type ID,最低2字节表示Entry ID。只有一个ID 如何能引用到实际资源?实际上aapt工具还生成一个文件resources.arsc,相当于一个资源索引表,或者你理解成一个map也行,map的key是资源ID,value是资源在apk文件中的路径。resource.arsc里面还有其他信息,这里就不多说了。通过resource.arsc配合,就能引用到实际的资源文件。
说到打包就不能不提一下混淆,说到混淆就不能不提ProGuard。
ProGuard是一个混淆代码的开源项目,它的主要作用是混淆代码,但是其实它主要有4个功能如下:
ProGuard的官网,根据官网的翻译:
Progurad是一个Java类文件的压缩器、优化器、混淆器、预检测器。压缩环节会检测以及移除没有用到的类、字段、方法以及属性。优化环节会分析以及优化方法的字节码。混淆环节会用无意义的端变量去重命名类、变量、方法。这些步骤让代码更加精简、更搞笑,也更难被逆向破解。
PS:
现在大多数开发者都是用了Android Studio,只有很少的一部分才使用Eclipse,所以我两部分都说下
在build.gradle中修改minifyEnable修改为true即可
代码如下
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
在Eclipse中,文件根目录有如下两个文件 projiect.properties 和 proguard-project.txt。开启混淆打包只需要在 projiect.properties 中,被注释的有如下一句话
proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
把他注释去掉即可
在开启混淆中,Android Studio和eclipse中都有一个文件proguard-android.txt,这是混淆的一个默认文件,该默认文件为android提供的一个默认规则,如果我们的工程没有引入第三方库等,那么很简单的就能混淆了。
ProGuard 由shrink、optimize、obfuscate和preverify四个步骤组成,每个步骤都是可选的,需要那些步骤都可以在脚本中配置。
ProGuard流程如下:
ProGuard流程图.png
Input jars、Library jars——>shrink ——>Shrunk code——>optimize ——>Optim.code——>obfuscate——>Obfusc.code ——>preverify——>Output jars、Library jars
ProGuard使用Library jars来辅助对input jars类之间的依赖关系进行解析,Library jars本身不会被处理,也不会被包含到output jars中。
混淆中会移除没有用到的代码,所以这里就产生一个疑问,ProGuard怎么知道这个代码没有被用到?
这里引入到一个Entry Point(入口点) 概念,Entry Point是在ProGurad过程中不会被处理的类或方法。在压缩的过程中,ProGuard会从上述的Entry Point开始递归遍历,搜索哪些类和类的成员在使用,对于没有使用的类和类的成员,就会在压缩端被丢弃,在接下来的优化过程中,那些非Entry Point类、方法都会被设置为private、static或final,不实用的参数会被移除,此外,有些方法会被标记为内联的,在混淆的不会走中,ProGuard会对非Entry Point的类和方法进行重命名。
ProGuard的工具目录.png
编写ProGuard文件有3个步骤:
下面我们就来详细看下
基本混淆又可以分为
混淆文件的基本配置信息,任何APP都要使用,可以作为模板使用,具体如下:
# 代码混淆压缩比,在0和7之间,默认为5,一般不需要改
-optimizationpasses 5
# 混淆时不使用大小写混合,混淆后的类名为小写
-dontusemixedcaseclassnames
# 指定不去忽略非公共的库的类
-dontskipnonpubliclibraryclasses
# 指定不去忽略非公共的库的类的成员
-dontskipnonpubliclibraryclassmembers
# 不做预校验,preverify是proguard的4个步骤之一
# Android不需要preverify,去掉这一步可加快混淆速度
-dontpreverify
# 有了verbose这句话,混淆后就会生成映射文件
# 包含有类名->混淆后类名的映射关系
# 然后使用printmapping指定映射文件的名称
-verbose
-printmapping proguardMapping.txt
# 指定混淆时采用的算法,后面的参数是一个过滤器
# 这个过滤器是谷歌推荐的算法,一般不改变
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
# 保护代码中的Annotation不被混淆,这在JSON实体映射时非常重要,比如fastJson
-keepattributes *Annotation*
# 避免混淆泛型,这在JSON实体映射时非常重要,比如fastJson
-keepattributes Signature
//抛出异常时保留代码行号,在异常分析中可以方便定位
-keepattributes SourceFile,LineNumberTable
-dontskipnonpubliclibraryclasses用于告诉ProGuard,不要跳过对非公开类的处理。默认情况下是跳过的,因为程序中不会引用它们,有些情况下人们编写的代码与类库中的类在同一个包下,并且对包中内容加以引用,此时需要加入此条声明。
-dontusemixedcaseclassnames,这个是给Microsoft Windows用户的,因为ProGuard假定使用的操作系统是能区分两个只是大小写不同的文件名,但是Microsoft Windows不是这样的操作系统,所以必须为ProGuard指定-dontusemixedcaseclassnames选项
# 保留所有的本地native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留了继承自Activity、Application这些类的子类
# 因为这些子类有可能被外部调用
# 比如第一行就保证了所有Activity的子类不要被混淆
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-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
# 如果有引用android-support-v4.jar包,可以添加下面这行
-keep public class com.null.test.ui.fragment.** {*;}
# 保留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 {
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*(***);
*** get* ();
}
# 保留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 void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 对R文件下的所有类及其方法,都不能被混淆
-keepclassmembers class **.R$* {
*;
}
# 对于带有回调函数onXXEvent的,不能混淆
-keepclassmembers class * {
void *(**On*Event);
}
针对APP量身定制里面又包含
下面我们就来一一介绍
对于实体,保留他们的set和get方法,对于boolean型get方法,有人喜欢命名isXXX,所以不要遗漏。如下:
# 保留实体类和成员不被混淆
-keep public class com.xxxx.entity.** {
public void set*(***);
public *** get*();
public *** is*();
}
一种好的做法就是把所有实体都放到一个包下进行管理,遮掩只写一次混淆就够了,避免以后在别的包中新增的实体而忘记保留,代码在混淆后因为找不到相应的实体类而崩溃。
内部类经常会被混淆,结果在调用的时候为空就崩溃了,最好的解决办法就是把这个内部类拿出来,单独成为一个类。如果一定要内置,那么这个类就必须在混淆的时候保留,比如:
# 保留内嵌类不被混淆
-keep class com.example.xxx.MainActivity$* { *; }
这个$符号就是用来分割内部类与其母体的标志。
# 对WebView的处理
-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, java.lang.String);
}
# 保留JS方法不被混淆
-keepclassmembers class com.example.xxx.MainActivity$JSInterface1 {
<methods>;
}
其中JSInterface是MainActivity的子类
在程序中使用SomeClass.class.method这样静态方法,在ProGuard中是在压缩过程中被保留的,那么对于Class.forName("SomeClass")呢,SomeClass不会被压缩过程中移除,它会检查程序中使用的Class.forName方法,对参数SomeClass法外开恩,不会被移除。但是在混淆过程中,无论是Class.forName("SomeClass"),还是SomeClass.class,都不能蒙混过关,SomeClass这个类名称会被混淆,因此,我们要在ProGuard.cfg文件中保留这个名称。
Class.forName("SomeClass")
SomeClass.class
SomeClass.class.getField("someField")
SomeClass.class.getDeclaredField("someField")
SomeClass.class.getMethod("someMethod", new Class[] {})
SomeClass.class.getMethod("someMethod", new Class[] { A.class })
SomeClass.class.getMethod("someMethod", new Class[] { A.class, B.class })
SomeClass.class.getDeclaredMethod("someMethod", new Class[] {})
SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class })
SomeClass.class.getDeclaredMethod("someMethod", new Class[] { A.class, B.class })
AtomicIntegerFieldUpdater.newUpdater(SomeClass.class, "someField")
AtomicLongFieldUpdater.newUpdater(SomeClass.class, "someField")
AtomicReferenceFieldUpdater.newUpdater(SomeClass.class, SomeType.class, "someField")
在混淆的时候,要在项目中搜索一下上述方法,将相应的类或者方法名称进行保留而不被混淆。
但凡在Layout目录下XML布局文件配置的自定义View,都不能进行混淆。为此要遍历Layout下所有XML布局文件,找到那些自定义的View,然后确认其是否在ProGuard文件中保留。有一种思路是,在我们使用自定义View时,前面都必须加上我们的包名,比如com.a.b.customview,我们可以遍历所有Layout下的XML布局文件,查找所有匹配的com.a.b标签即可
但凡在Layout目录下的XML布局文件配置的自定义View,都不能进行混淆。为此要遍历Layout下的所有的XML布局文件,找到那些自定义View,然后确认其是否在ProGuard文件中保留。有一种思路是,在我们使用自定义View时,前面都必须加上我们的包名,比如com.a.b.customeview,我们可以遍历所有Layout下的XML布局文件,查找所有匹配com.a.b的标签即可
我们在Android项目中不可避免要使用很多第三方提供的SDK,一般而言,这些SDK是经过ProGuard混淆的,而我们所需要做的就是避免这些SDK的类和方法在我们APP被混淆。
# 针对android-support-v4.jar的解决方案
-libraryjars libs/android-support-v4.jar
-dontwarn android.support.v4.**
-keep class android.support.v4.** { *; }
-keep interface android.support.v4.app.** { *; }
-keep public class * extends android.support.v4.**
-keep public class * extends android.app.Fragment
这个就取决于第三方包的混淆策略,一般都有在各自的SDK中有关于混淆的说明文字,比如支付宝如下:
# 对alipay的混淆处理
-libraryjars libs/alipaysdk.jar
-dontwarn com.alipay.android.app.**
-keep public class com.alipay.** { *; }
PS:
值得注意的是,不是每个第三方SDK都需要-dontwarn指令、这取决于混淆时第三方SDK是否出现警告,需要的时候再机上。
在使用ProGuard过程中,还有一些注意事项如下:
@keep @keepPublicGetterSetters public class Bean{ public boolean booleanProperty; public int intProperty; public String stringProperty; }
答:xml里面都是各种字符,不利于快速遍历。编译成二进制文件,用数字替换各种符号,一方面能快速访问,另一方面也能减少大小。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有