[TOC]
## 編譯流程

1. 通過 aapt 打包 res 資源文件,生成 R.java、resources.arsc 和 res 文件(二進制 & 非二進制如 res/raw 和 pic 保持原樣);
2. 處理 .aidl 文件,生成對應的 Java 接口文件;
3. 通過 Java Compiler 編譯 R.java、Java 接口文件、Java 源文件,生成 .class 文件;
4. 通過 dex 命令,將 .class 文件和第三方庫中的 .class 文件處理生成 classes.dex;
5. 通過 apkbuilder 工具,將 aapt 生成的 resources.arsc 和 res 文件、assets 文件和 classes.dex 一起打包生成 apk;
6. 通過 Jarsigner 工具,對上面的 apk 進行 debug 或 release 簽名;
7. 通過 zipalign 工具,將簽名后的 apk 進行對齊處理。
看起來我們貌似已經回答出了這個問題的答案,但是今天是來屠龍的,所以我們不能就這么簡單的放過這個題目。
## 從gradle Task看編譯流程
先貼一段gradle打印task耗時的代碼
1. 項目根目錄build.gradle打開
2. 加入下面代碼
~~~
import java.util.concurrent.TimeUnit
// Log timings per task.
class TimingsListener implements TaskExecutionListener, BuildListener {
private long startTime
private timings = []
@Override
void beforeExecute(Task task) {
startTime = System.nanoTime()
}
@Override
void afterExecute(Task task, TaskState taskState) {
def ms = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
timings.add([ms, task.path])
task.project.logger.warn "${task.path} took ${ms}ms"
}
@Override
void buildFinished(BuildResult result) {
println "Task timings:"
for (timing in timings) {
if (timing[0] >= 50) {
printf "%7sms %s\n", timing
}
}
}
@Override
void buildStarted(Gradle gradle) {}
@Override
void projectsEvaluated(Gradle gradle) {}
@Override
void projectsLoaded(Gradle gradle) {}
@Override
void settingsEvaluated(Settings settings) {}
}
gradle.addListener new TimingsListener()
復制代碼
~~~
當項目運行完之后會輸出類似如下的日志,表示一個run執行之后gradle所執行的task的時間以及任務名。
~~~
1543ms :compiler:kaptGenerateStubsKotlin
144ms :RouterLib:packageDebugResources
1166ms :compiler:kaptKotlin
816ms :compiler:compileKotlin
401ms :compiler:compileJava
65ms :compiler:jar
122ms :app:mergeDebugResources
56ms :EmptyLoader:compileJava
170ms :app:processDebugManifest
171ms :RouterLib:parseDebugLocalResources
60ms :app:checkDebugDuplicateClasses
2416ms :RouterLib:compileDebugKotlin
122ms :RouterLib:compileDebugJavaWithJavac
124ms :secondmoudle:mergeDebugNativeLibs
1185ms :app:processDebugResources
70ms :secondmoudle:kaptGenerateStubsDebugKotlin
202ms :RouterLib:mergeDebugNativeLibs
350ms :secondmoudle:kaptDebugKotlin
158ms :secondmoudle:compileDebugJavaWithJavac
1108ms :app:kaptGenerateStubsDebugKotlin
91ms :secondmoudle:bundleLibRuntimeToJarDebug
129ms :app:mergeDebugNativeLibs
430ms :app:kaptDebugKotlin
1008ms :app:compileDebugKotlin
120ms :app:compileDebugJavaWithJavac
265ms :app:mergeDebugJavaResource
181ms :app:transformClassesAndResourcesWithAuto_registerForDebug
7262ms :app:dexBuilderDebug
1308ms :app:mergeProjectDexDebug
344ms :app:packageDebug
復制代碼
~~~
從上述Task列表中可以看出,其實最上面這張圖所說的編譯流程其實并不完整。
## kapt和apt
我上篇文章說了,javaCompiler執行之前會先執行apt,生成java代碼,其任務名就是kaptGenerateStubsDebugKotlin。
[聊聊AbstractProcessor和Java編譯流程](https://juejin.cn/post/6844904197775687694 "https://juejin.cn/post/6844904197775687694")
## compiler 混入了奇怪的東西
kotlin已經被引入了很多版本了,但是kotlin的compiler其實和java compiler是不一樣的。
如果按照標準答案去回答這個問題吧,總感覺還是有所欠缺的,所以我們需要補充的一個點就是**compileDebugKotlin**。
## 當然少不了transform
當我們使用字節碼插樁之后其實就增加了個transform的流程,也就是這個**transformClassesAndResourcesWithAuto\_registerForDebug**。
## 那么是不是還有什么可以補充的呢?
AGP在不同版本的差異還是比較大的。特別是在3.2版本之上的版本被引入了D8編譯器之后。
低版本先使用DX編譯器將class轉化為dex。
而高版本采用**d8**編譯器將class轉化為dex。

### desugar是干嘛的?
Android Studio 為使用部分 Java 8 語言功能及利用這些功能的第三方庫提供內置支持。默認工具鏈對 javac 編譯器的輸出執行字節碼轉換(稱為 desugar),從而實現新語言功能。
**語法糖香歸香,但是最后.dex可是不認識你的。**
### 那么D8的優勢是什么呢???
話不多,直接上圖。


可以看到D8在編譯速度以及編譯出來的文件體積上有了明顯的提升。
## 那么混淆呢??
看看最一開始的圖,有沒有發現少了混淆的流程呢!!!
在AGP3.4版本上引入了R8,也就是混淆升級版本。而且在高版本上,整體流程也其實發生了微妙的變更,將原先的流程進行了合并。
1. R8開啟前的編譯流程

2. R8開啟后的編譯流程

說句題外話,但是R8更吃內存,機器辣雞的老哥慎重點。
## 關于簽名
之前寫的東西有點遺漏啊,谷歌官方有說明,下面是引用啊
> 注意:您必須在應用構建過程中的兩個特定時間點之一使用 zipalign,具體在哪個時間點使用,取決于您所使用的應用簽名工具:
> 如果您使用的是 apksigner,則只能在為 APK 文件簽名之前執行 zipalign。如果您在使用 apksigner 為 APK 簽名之后對 APK 做出了進一步更>改,簽名便會失效。
> 如果您使用的是 jarsigner,則只能在為 APK 文件簽名之后執行 zipalign。
[鏈接地址](https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.android.google.cn%2Fstudio%2Fcommand-line%2Fzipalign.html "https://developer.android.google.cn/studio/command-line/zipalign.html")
那么當使用V1簽名時,編譯流程順序還是6-7
而當使用的是V2的簽名時,則編譯流程順序是7-6
## Gradle 生命周期
Gradle的構建過程可以分為三部分:**初始化階段**、**配置階段**和**執行階段**。
簡單的說下就是buildSrc先編譯,之后是根目錄的settings.gradle, 根build.gradle,最后才是module build

## apt是編譯中哪個階段
APT解析的是java 抽象語法樹(AST),屬于javac的一部分流程。大概流程:.java -> AST -> .class
> [聊聊AbstractProcessor和Java編譯流程](https://juejin.cn/post/6844904197775687694 "https://juejin.cn/post/6844904197775687694")
## Dex和class有什么區別
[鏈接傳送門](https://link.juejin.cn?target=https%3A%2F%2Fwww.dazhuanlan.com%2Fharmless%2Ftopics%2F1137050 "https://www.dazhuanlan.com/harmless/topics/1137050")
Class與dex的區別
1)虛擬機: class用jvm執行,dex用dvm執行
2)文檔: class中冗余信息多,dex會去除冗余信息,包含所有類,查找方便,適合手機端
JVM與DVM
1)JVM基于棧(使用棧幀,內存),DVM基于寄存器,速度更快,適合手機端
2)JVM執行Class字節碼,DVM執行DEX
3)JVM只能有一個實例,一個應用啟動運行在一個DVM
DVM與ART
1)DVM:每次運行應用都需要一次編譯,效率降低。JIT
2)ART:Android5.0以上默認為ART,系統會在進程安裝后進行一次預編譯,將代碼轉為機器語言存在本地,這樣在每次運行時不用再進行編譯,提高啟動效率;。 AOP & JIT
## Transform是如何被執行的
Transform 在編譯過程中會被封裝成Task 依賴其他編譯流程的Task執行。

## Transform和其他系統Transform執行的順序
其實這個題目已經是個過期了,后面對這些都合并整合了,而且最新版的api也做了替換,要不然考慮下回懟下面試官?
[Transform和Task之間有關?](https://juejin.cn/post/6875141808825991181 "https://juejin.cn/post/6875141808825991181")
## 如何監控編譯速度變慢問題
~~~
./gradlew xxxxx -- scan
復制代碼
~~~
之后會生成一個gradle的網頁,填寫下你的郵箱就好了。
另外一個相對來說比較簡單了。通過gradle原生提供的listener進行就行了。
~~~
// 耗時統計kt化
class TimingsListener : TaskExecutionListener, BuildListener {
private var startTime: Long = 0L
private var timings = linkedMapOf<String, Long>()
override fun beforeExecute(task: Task) {
startTime = System.nanoTime()
}
override fun afterExecute(task: Task, state: TaskState) {
val ms = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS)
task.path
timings[task.path] = ms
project.logger.warn("${task.path} took ${ms}ms")
}
override fun buildFinished(result: BuildResult) {
project.logger.warn("Task timings:")
timings.forEach {
if (it.value >= 50) {
project.logger.warn("${it.key} cos ms ${it.value}\n")
}
}
}
override fun buildStarted(gradle: Gradle) {
}
override fun settingsEvaluated(settings: Settings) {
}
override fun projectsLoaded(gradle: Gradle) {
}
override fun projectsEvaluated(gradle: Gradle) {
}
}
gradle.addListener(TimingsListener())
復制代碼
~~~
## Gradle中如何給一個Task前后插入別的任務
最簡單的可以考慮直接獲取到Task實例,之后在after和before插入一些你所需要的代碼。
另外一個就是通過`dependOn`前置和`finalizedBy`掛載一個任務 mustAfter
[Gradle 使用指南 -- Gradle Task](https://link.juejin.cn?target=https%3A%2F%2Fwww.heqiangfly.com%2F2016%2F03%2F13%2Fdevelopment-tool-gradle-task%2F "https://www.heqiangfly.com/2016/03/13/development-tool-gradle-task/")
9. ksp APT Transform的區別
ksp 是kotlin專門獨立的ast語法樹
apt 是java 的ast語法樹
transform是 agp 專門修改字節碼的一個方法。
反殺時刻`AsmClassVisitorFactory`,可以看看我之前寫的那篇文章。
## Transform上的編譯優化能做哪些?
雖然是個即將過期的api,但是大家對他的改動還是都比較多的。
首先肯定是需要完成增量編譯的,具體的可以參考我的demo工程。記住,所有的transfrom都要全量。
另外可以考慮多線程優化,將轉化操作移動到子線程內,建議使用gradle內部的共享線程。
參考agp最新做法,抽象出一個新的interface,之后通過spi串聯,之后將asm鏈式調用。我的文章也介紹過,具體的點在哪里自己盤算。
[現在準備好告別Transform了嗎](https://juejin.cn/post/7016147287889936397 "https://juejin.cn/post/7016147287889936397")
## aar 源碼切換插件原理
這個前幾天剛介紹過,原理和方案業內都差不多,`mulite-repo`應該都需要這個東西的。我的版本也比較簡陋,大廠內部肯定都會有些魔改的。
相對來說功能肯定會更豐富,更全面一點。
> [aar和源碼切換插件Plus](https://juejin.cn/post/7028599249675747341 "https://juejin.cn/post/7028599249675747341")
## 你們有哪些保證代碼質量的手段
最簡單的方式還是通過靜態掃描+pipline 處理,之后在合并mr之前進行一次攔截。
靜態掃描方式比較多,下面給大家簡單的介紹下
阿里的sonar 但是對kt的支持很糟糕,因為阿里使用,所以有很多現成的規則可以使用,但是如果從0-1接入,你可能會直接放棄。
原生的lint,可以基于原生提供的lint api,對其進行開發,支持種類也多,基本上算是一個非常優秀的方案了,但是由于文檔資料較少,對于開發的要求可能會較高。
> [AndroidLint](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2FLeifzhang%2FAndroidLint "https://github.com/Leifzhang/AndroidLint")
13. 如何對第三方的依賴做靜態檢查?
魔高一尺道高一丈。lint還是能解決這個問題的。
[Tree Api+ClassScanner = 識別三方隱私權限調用](https://juejin.cn/post/7009210297340657701 "https://juejin.cn/post/7009210297340657701")
14. R.java code too large 解決方案
又是一個過期的問題,盡早升級agp版本,讓R8幫你解決這個問題,R文件完全可以內聯的。
或者用別的AGP插件的R inline也可以解決這個問題。
15. R inline 你需要注意些什么?
預掃描,先收集調用的信息,之后在進行替換。還有javac 的時候可能就因為文件過大,直接掛掉了。
16. 一個類替換父類 比如所有activity實現類替換baseactivity
`class node` 直接替換 `superName` ,想起了之前另外一個問題,感覺主要是要對構造函數進行修改,否則也會出異常。
17. R8 D8 以及混淆相關的,還有R8除了混淆還能干些什么? 混淆規則有沒有碰到什么奇怪的問題?
`D8`和`Dx`的區別,主要涉及到編譯速度以及編譯產物的體積,包體積大概小11%。
`R8` 則是變更了整個編譯流程的,其中我覺得最微妙的就是`java8 lambda`相關的,脫糖前后的差別還是比較大的。同時R8也少了很多之前的Transform。
R8的混淆部分,混淆除了能增加代碼閱讀難度意外,更多的是對于代碼優化方面的。 比如無效代碼優化, 同時也刪除代碼等等都可以做。
18. 編譯的時候有沒有碰到javac的常量優化
javac會將靜態常量直接優化成具體的數值。但是尤其是多模塊場景下尤其容易出現異常,看起來是個實際的常量引用,但是產物上卻是一個具體的常量值了。
。
## 參考資料
[聊聊Android編譯流程](https://juejin.cn/post/6845166890759749645)
[Android 基礎架構組面試題 | 面試](https://juejin.cn/post/7032625978023084062)
- Android
- 四大組件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介紹
- MessageQueue詳細
- 啟動流程
- 系統啟動流程
- 應用啟動流程
- Activity啟動流程
- View
- view繪制
- view事件傳遞
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大數據
- Binder小結
- Android組件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 遷移與修復
- Sqlite內核
- Sqlite優化v2
- sqlite索引
- sqlite之wal
- sqlite之鎖機制
- 網絡
- 基礎
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP進化圖
- HTTP小結
- 實踐
- 網絡優化
- Json
- ProtoBuffer
- 斷點續傳
- 性能
- 卡頓
- 卡頓監控
- ANR
- ANR監控
- 內存
- 內存問題與優化
- 圖片內存優化
- 線下內存監控
- 線上內存監控
- 啟動優化
- 死鎖監控
- 崩潰監控
- 包體積優化
- UI渲染優化
- UI常規優化
- I/O監控
- 電量監控
- 第三方框架
- 網絡框架
- Volley
- Okhttp
- 網絡框架n問
- OkHttp原理N問
- 設計模式
- EventBus
- Rxjava
- 圖片
- ImageWoker
- Gilde的優化
- APT
- 依賴注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 協程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 運行期Java-hook技術
- 編譯期hook
- ASM
- Transform增量編譯
- 運行期Native-hook技術
- 熱修復
- 插件化
- AAB
- Shadow
- 虛擬機
- 其他
- UI自動化
- JavaParser
- Android Line
- 編譯
- 疑難雜癥
- Android11滑動異常
- 方案
- 工業化
- 模塊化
- 隱私合規
- 動態化
- 項目管理
- 業務啟動優化
- 業務架構設計
- 性能優化case
- 性能優化-排查思路
- 性能優化-現有方案
- 登錄
- 搜索
- C++
- NDK入門
- 跨平臺
- H5
- Flutter
- Flutter 性能優化
- 數據跨平臺