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

1.Android 的构建流程

【说明】html

流程概述:
一、打包资源文件,生成R.java文件
二、处理aidl文件,生成相应java 文件
三、编译工程源代码,生成相应class 文件
四、转换全部class文件,生成classes.dex文件
五、打包生成apk
六、对apk文件进行签名
七、对签名后的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接口描述语言,java

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

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

${JDK_HOME}/javagit

c或/usr/bin/javacgithub

 
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}/toolsweb

 /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()函数完成。

【手动】bootstrap

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开发基础点进行整理,罗列下已经总结的文章,从中能够看到技术积累的过程。
相关文章
相关标签/搜索