【0174】Android 面试- Android项目构建相关

1.Android 的构建流程

【说明】

流程概述:
1、打包资源文件,生成R.java文件
2、处理aidl文件,生成相应java 文件
3、编译工程源代码,生成相应class 文件
4、转换所有class文件,生成classes.dex文件
5、打包生成apk
6、对apk文件进行签名
7、对签名后的apk文件进行对其处理

 

打包过程使用的工具
名称
功能介绍 在操作系统中的路径 源码路径
aapt
(Android Asset Package Tool)
Android资源打包工具
${ANDROID_SDK_HOME} /build-tools/
 ANDROID_VERSION/aapt
frameworks\base\tools\aap
aidl
(android interface definition language)

Android接口描述语言,

将aidl转化为.java文件的工具

${ANDROID_SDK_HOME}/build-tools/
 ANDROID_VERSION/aidl
frameworks\base\tools\aidl
javac Java Compiler

${JDK_HOME}/java

c或/usr/bin/javac

 
dex
转化.class文件为Davik VM
能识别的.dex文件
${ANDROID_SDK_HOME}/build-tools/
 ANDROID_VERSION/dx
 
apkbuilder
生成apk包
${ANDROID_SDK_HOME}/tools/
 apkbuilder
sdk\sdkmanager\libs\sdklib\
 src\com\android\sdklib\build\
 ApkBuilderMain.java
jarsigner .jar文件的签名工具 ${JDK_HOME}/jarsigner或/usr/bin/jarsigner
 
zipalign 字节码对齐工具

${ANDROID_SDK_HOME}/tools

 /zipalign

 
 
第一步:打包资源文件,生成R.java文件。
【输入】Resource文件(就是工程中res中的文件)、Assets文件(相当于另外一种资源,这种资源Android系统并不像对res中的文件那样优化它)、AndroidManifest.xml文件(包名就是从这里读取的,因为生成R.java文件需要包名)、Android基础类库(Android.jar文件)
【工具】aapt工具
【输出】打包好的资源(bin目录中的resources.ap_文件)、R.java文件(gen目录中)
打包资源的工具aapt,大部分文本格式的XML资源文件会被编译成二进制格式的XML资源文件,除了assets和res/raw资源被原装不动地打包进APK之外,其它的资源都会被编译或者处理。 。
生成过程主要是调用了aapt源码目录下的Resource.cpp文件中的buildResource()函数,该函数首先检查AndroidManifest.xml的合法性,然后对res目录下的资源子目录进行处理,处理的函数为makeFileResource(),处理的内容包括资源文件名的合法性检查,向资源表table添加条目等,处理完后调用compileResourceFile()函数编译res与asserts目录下的资源并生成resources.arsc文件,compileResourceFile()函数位于aapt源码目录的ResourceTable.cpp文件中,该函数最后会调用parseAndAddEntry()函数生成R.java文件,完成资源编译后,接下来调用compileXmlfile()函数对res目录的子目录下的xml文件分别进行编译,这样处理过的xml文件就简单的被“加密”了,最后将所有的资源与编译生成的resorces.arsc文件以及“加密”过的AndroidManifest.xml文件打包压缩成resources.ap_文件(使用Ant工具命令行编译则会生成与build.xml中“project name”指定的属性同名的ap_文件)。
关于这一步更详细的流程可阅读http://blog.csdn.net/luoshengyang/article/details/8744683
 
第二步:处理aidl文件,生成相应的java文件。
【输入】源码文件、aidl文件、framework.aidl文件
【工具】aidl工具
【输出】对应的.java文件
对于没有使用到aidl的android工程,这一步可以跳过。aidl工具解析接口定义文件并生成相应的java代码供程序调用。
 
第三步:编译工程源代码,生成下相应的class文件。
【输入】源码文件(包括R.java和AIDL生成的.java文件)、库文件(.jar文件)
【工具】javac工具
【输出】.class文件
这一步调用了javac编译工程src目录下所有的java源文件,生成的class文件位于工程的bin\classes目录下,上图假定编译工程源代码时程序是基于android SDK开发的,实际开发过程中,也有可能会使用android NDK来编译native代码,因此,如果可能的话,这一步还需要使用android NDK编译C/C++代码,当然,编译C/C++代码的步骤也可以提前到第一步或第二步。
 
第四步:转换所有的class文件,生成classes.dex文件。
【输入】 .class文件(包括Aidl生成.class文件,R生成的.class文件,源文件生成的.class文件),库文件(.jar文件)
【工具】javac工具
【输出】.dex文件
前面多次提到,android系统dalvik虚拟机的可执行文件为dex格式,程序运行所需的classes.dex文件就是在这一步生成的,使用的工具为dx,dx工具主要的工作是将java字节码转换为dalvik字节码、压缩常量池、消除冗余信息等。
 
第五步:打包生成apk。
【输入】打包后的资源文件、打包后类文件(.dex文件)、libs文件(包括.so文件,当然很多工程都没有这样的文件,如果你不使用C/C++开发的话)
【工具】apkbuilder工具
【输出】未签名的.apk文件
打包工具为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目录下的Native库(通过android NDK编译生成的so或bin文件),最后调用sealApk()关闭apk文件。
 
第六步:对apk文件进行签名。
【输入】未签名的.apk文件
【工具】jarsigner
【输出】签名的.apk文件
android的应用程序需要签名才能在android设备上安装,签名apk文件有两种情况:一种是在调试程序时进行签名,使用eclipse开发android程序时,在编译调试程序时会自己使用一个debug.keystore对apk进行签名;另一种是打包发布时对程序进行签名,这种情况下需要提供一个符合android开发文档中要求的签名文件。签名的方法也分两种:一种是使用jdk中提供的jarsigner工具签名;另一种是使用android源码中提供的signapk工具,它的代码位于android系统源码build\tools\signapk目录下。
 
第七步:对签名后的apk文件进行对齐处理。
【输入】签名后的.apk文件
【工具】zipalign工具
【输出】对齐后的.apk文件
这一步需要使用的工具为zipalign,它位于android-sdk\tools目录,源码位于android系统源码的build\tools\zipalign目录,它的主要工作是将spk包进行对齐处理,使spk包中的所有资源文件距离文件起始偏移为4字节整数倍,这样通过内存映射访问apk文件时速度会更快,验证apk文件是否对齐过的工作由ZipAlign.cpp文件的verify()函数完成,处理对齐的工作则由process()函数完成。

【手动】

o为了能够手动对齐程序包,Android 1.6及以后的SDK的tools/文件夹下都有zipalign工具。你可以使用它来对齐任何版本下的程序包。你必须在签名apk文件后进行,

        使用以下命令:zipalign -v 4 source.apk destination.apk

【验证对齐】

o以下的命令用于检查程序包是否进行了对齐:zipalign -c -v 4 application.apk

【使用Android studio】

android studio 中的build.gradle文件中加入zipAlignEnabled  true
类似于buildTypes {

              release { 

                 minifyEnabled false 

                 proguardFiles getDefaultProguardFile ('proguard-android.txt' ), 'proguard-rules.txt'

     zipAlignEnabled true

                     }

  }

2.jenkins的认识-构建或持续集成

【参考文章1】https://www.jianshu.com/p/b524b151d35f

【参考文章2】https://www.yiibai.com/jenkins/   Jenkins教程

 3.Git使用

 

【说明】一般的项目开发的使用流程如下:自己远程仓库与项目远程仓库分离;

当你想更正别人仓库里的错误时,要走一个流程:
  1. 先 fork 别人的仓库,相当于拷贝一份,相信我,不会有人直接让你改修原仓库的
  2. clone 到本地分支,做一些 bug fix
  3. 发起 pull request 给原仓库,让他看到你修改的 bug
  4. 原仓库 review 这个 bug,如果是正确的话,就会 merge 到他自己的项目中

至此,整个 pull request 的过程就结束了。

【实例演示】

理解了 pull request 的含义和流程,具体操作也就简单了。以 Github 排名最高的 为例说明。
1. 先点击 fork 仓库,项目现在就在你的账号下了

 

2. 在你自己的机器上 git clone 这个仓库,切换分支(也可以在 master 下),做一些修改。
~  git clone https://github.com/beepony/bootstrap.git
~  cd bootstrap
~  git checkout -b test-pr
~  git add . && git commit -m "test-pr"
~  git push origin test-pr

3. 完成修改之后,回到 test-pr 分支,点击旁边绿色的 Compare & pull request 按钮

4. 添加一些注释信息,确认提交
5. 仓库作者看到,你提的确实是对的,就会 merge,合并到他的项目中

以上就是 pull reqesut 的整个流程

4.gradle 相关

 

5. Proguard

 

  1. 压缩(Shrink):检测并移除代码中无用的类、字段、方法和特性(Attribute)。
  2. 优化(Optimize):对字节码进行优化,移除无用的指令。
  3. 混淆(Obfuscate):使用a,b,c,d这样简短而无意义的名称,对类、字段和方法进行重命名。
  4. 预检(Preveirfy):在Java平台上对处理后的代码进行预检,确保加载的class文件是可执行的。

【为什么需要进行混淆】

 java是一种跨平台的解释性语言,java的源码会编译成为字节码存在.class文件中,由于跨平台的需要,java的字节码包含了许多的源码的信息,包括变量名方法名等等;

 并且可以通过这些名称访问变量名和方法,这些信息很多是无用的,但是容易被编译成为java源码,防止被反编译,需要对java源码进行混淆;

混淆就是对release版本的程序进行重新的组织和处理;处理之后的代码具有相同的功能,但是代码是不一样的,同时代码不容易被反编译,即使反编译成功之后也不容易被读懂;

被混淆之后的代码仍然遵循原来的格式进行调用,执行的结果一样;对外保证了程序的安全性;对内是透明的,执行的结果是一样的;

 【参考文章】

ProGuard工作原理

ProGuar由shrink、optimize、obfuscate和preveirfy四个步骤组成,每个步骤都是可选的,我们可以通过配置脚本来决定执行其中的哪几个步骤。
 
混淆就是移除没有用到的代码,然后对代码里面的类、变量、方法重命名为人可读性很差的简短名字。
那么有一个问题,ProGuard怎么知道这个代码没有被用到呢?
这里引入一个Entry Point(入口点)概念,Entry Point是在ProGuard过程中不会被处理的类或方法
在压缩的步骤中,ProGuard会从上述的Entry Point开始递归遍历,搜索哪些类和类的成员在使用,对于没有被使用的类和类的成员,就会在压缩段丢弃,在接下来的优化过程中,那些非Entry Point的类、方法都会被设置为private、static或final,不使用的参数会被移除,此外,有些方法会被标记为内联的,在混淆的步骤中,ProGuard会对非Entry Point的类和方法进行重命名。
那么这个入口点怎么来呢?就是从ProGuard的配置文件来,只要这个配置了,那么就不会被移除

如何编写一个ProGuard文件

有个三步走的过程:
  • 基本混淆
  • 针对APP的量身定制
  • 针对第三方jar包的解决方案
基本混淆
混淆文件的基本配置信息,任何APP都要使用,可以作为模板使用,具体如下。
1,基本指令
复制代码
# 代码混淆压缩比,在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选项
复制代码

 2,需要保留的东西

复制代码
# 保留所有的本地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.xxxx.app.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 {
    *** 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;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}
 
# 对于R(资源)下的所有类及其方法,都不能被混淆
-keep class **.R$* {
    *;
}
 
# 对于带有回调函数onXXEvent的,不能被混淆
-keepclassmembers class * {
    void *(**On*Event);
}
复制代码
针对APP的量身定制
1,保留实体类和成员被混淆
对于实体,保留它们的set和get方法,对于boolean型get方法,有人喜欢命名isXXX的方式,所以不要遗漏。如下:
复制代码
# 保留实体类和成员不被混淆
-keep public class com.xxxx.entity.** {
    public void set*(***);
    public *** get*();
    public *** is*();
}
复制代码

 一种好的做法是把所有实体都放在一个包下进行管理,这样只写一次混淆就够了,避免以后在别的包中新增的实体而忘记保留,代码在混淆后因为找不到相应的实体类而崩溃。

2,内嵌类

内嵌类经常会被混淆,结果在调用的时候为空就崩溃了,最好的解决方法就是把这个内嵌类拿出来,单独成为一个类。如果一定要内置,那么这个类就必须在混淆的时候保留,比如如下:

# 保留内嵌类不被混淆
-keep class com.example.xxx.MainActivity$* { *; }

这个$符号就是用来分割内嵌类与其母体的标志。

3,对WebView的处理

复制代码
# 对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)
}
复制代码

4,对JavaScript的处理

# 保留JS方法不被混淆
-keepclassmembers class com.example.xxx.MainActivity$JSInterface1 {
    <methods>;
}

 其中JSInterface是MainActivity的子类

5,处理反射

在程序中使用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")

在混淆的时候,要在项目中搜索一下上述方法,将相应的类或者方法的名称进行保留而不被混淆。

 

6,对于自定义View的解决方案
但凡在Layout目录下的XML布局文件配置的自定义View,都不能进行混淆。为此要遍历Layout下的所有的XML布局文件,找到那些自定义View,然后确认其是否在ProGuard文件中保留。有一种思路是,在我们使用自定义View时,前面都必须加上我们的包名,比如com.a.b.customeview,我们可以遍历所有Layout下的XML布局文件,查找所有匹配com.a.b的标签即可。
 
针对第三方jar包的解决方案
我们在Android项目中不可避免要使用很多第三方提供的SDK,一般而言,这些SDK是经过ProGuard混淆的,而我们所需要做的就是避免这些SDK的类和方法在我们APP被混淆。
1,针对android-support-v4.jar的解决方案
复制代码
# 针对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
复制代码

 2,其他的第三方jar包的解决方案

这个就取决于第三方包的混淆策略了,一般都有在各自的SDK中有关于混淆的说明文字,比如支付宝如下:

# 对alipay的混淆处理
-libraryjars libs/alipaysdk.jar
-dontwarn com.alipay.android.app.**
-keep public class com.alipay.**  { *; }
值得注意的是,不是每个第三方SDK都需要-dontwarn 指令,这取决于混淆时第三方SDK是否出现警告,需要的时候再加上。

其他注意事项

当然在使用ProGuard过程中,还有一些注意的事项,如下。
1,如何确保混淆不会对项目产生影响
  • 测试工作要基于混淆包进行,才能尽早发现问题
  • 每天开发团队的冒烟测试,也要基于混淆包
  • 发版前,重点的功能和模块要额外的测试,包括推送,分享,打赏
2,打包时忽略警告
当导出包的时候,发现很多could not reference class之类的warning信息,如果确认App在运行中和那些引用没有什么关系,可以添加-dontwarn 标签,就不会提示这些警告信息了
 
3,对于自定义类库的混淆处理
比如我们引用了一个叫做AndroidLib的类库,我们需要对Lib也进行混淆,然后在主项目的混淆文件中保留AndroidLib中的类和类的成员。
 
4,使用annotation避免混淆
另一种类或者属性被混淆的方式是,使用annotation,比如这样:
复制代码
@keep
@keepPublicGetterSetters
public class Bean{
    public  boolean booleanProperty;
    public  int intProperty;
    public  String stringProperty;
}
复制代码
5,在项目中指定混淆文件
到最后,发现没有介绍如何在项目中指定混淆文件。在项目中有一个project.properties文件,在其中写这么一句话,就可以确保每次手动打包生成的apk是混淆过的。
proguard.config=proguard.cfg
其中,proguard.cfg是混淆文件的名称。

小结

总之ProGuard是一个比较枯燥的过程,但Android项目没有了ProGuard就真不行了,这样可以保证我们开发出的APK可以更健壮,毕竟很多核心代码质量也算是一个APK的核心竞争力吧。
 

阅读扩展

源于对掌握的Android开发基础点进行整理,罗列下已经总结的文章,从中可以看到技术积累的过程。
posted @ 2018-05-23 16:03  OzTaking  阅读(503)  评论(0)    收藏  举报