天天看點

AOSP代碼管理1. 概要2. AOSP的代碼線、分支和釋出3. AOSP的代碼自動流4. 總結

目錄

  • 1. 概要
  • 2. AOSP的代碼線、分支和釋出
  • 3. AOSP的代碼自動流
    • 3.1 cherry-pick
    • 3.2 rebase
    • 3.3 merge
    • 3.4 merge strategy
  • 4. 總結

請尊重原創版權,轉載注明出處。

1. 概要

與大部分裝置廠商一樣,Google也面臨着AOSP(Android Open Source Project)多分支的管理問題, AOSP需要使用多個分支來維護不同的Android版本,裝置廠商除了要接受Android版本的不同分支,還要使用更多的分支來處理不同裝置之間的差異。 維護多個分支始終是一項繁重的任務,當裝置廠商的産品越來越多,而且還存在跨多個Android版本開發的時候(既有基于Android 4.4的裝置,也有基于Android 5.0的裝置),多分支管理簡直就是噩夢。

一般而言,裝置廠商都是盡可能地減少分支,是以,在裝置廠商的代碼中,會有很多手段來避免新增分支:

  • 編譯時過濾。通過編譯開關來相容代碼差異,以便于一個分支下,通過不同的開關配置,編譯出不同的版本。靜态編譯開關以MTK的方案為代表,HTC、SONY等大廠商都有采用這種方式。
  • 運作時反射。多份功能類似的代碼都經過編譯,但運作時,根據配置資訊,選擇加載的類。運作時反射以CM的方案為代表,尤其是RIL層的反射架構極為精彩。
  • 基于SDK開發。很多應用層的開發都轉向了基于SDK或基于廠商自己的中間件。即便架構層有差異,需要新增分支,但應用層仍然是共分支的,不需要額外增加。

以上手段能夠減少分支的膨脹,但并不能完全避免新增分支。不同的晶片廠商都會提供自己的平台方案,譬如MTK, QCOM, SAMSUNG等,裝置廠商通常都不是完全基于AOSP進行開發,而是基于不同晶片的平台方案。 不同的晶片平台方案的差異,導緻裝置廠商不得不額外新增分支,如此一來,多分支管理的噩夢依舊揮之不去。

本文分析了AOSP代碼的管理方式,包括AOSP的分支結構和釋出政策。面對多分支,AOSP采用了代碼自動合并的技術來降低維護成本,其背後的技術原理或許能夠舒緩一下裝置廠商面對多分支的煩惱。

2. AOSP的代碼線、分支和釋出

在每一次Android釋出新版本的時間段内,AOSP會存在多條代碼線(codelines)和一個最新的釋出分支(release)。 代碼線可以了解為AOSP維護代碼的一個次元,它可能包含多個分支(branches)。 譬如Android先釋出了4.4版本(對應kitkat-release分支),随後又釋出了4.4.1版本(對應kitkat-mr1-release分支), 那麼這兩個版本,就可以了解為同一條代碼線下的不同分支。

當最新的Android版本

N

釋出時,廠商和開發者就可以基于最新的釋出分支(N release)來解決Bug,适配機型。與此同時,AOSP内部仍然在演進Android版本

N+1

       

AOSP代碼管理1. 概要2. AOSP的代碼線、分支和釋出3. AOSP的代碼自動流4. 總結
  • Upstream Projects這條代碼線表示Android的上遊項目,因為Android本身也利用的很多開源項目(譬如Linux Kernel、Webkit、external目錄下的項目)會有代碼更新,是以,Android會定期從這些開源項目中移植最新的代碼。
  • K Release這條代碼線用于Kitkat的版本釋出。每一個Kitkat小版本的釋出,都是基于K Release這條代碼線。譬如,4.4釋出後(對應kitkcat-release分支),OEM廠商基于kitkat-release分支來适配機型。 4.4釋出後,K Release代碼線并沒有停止向前,Google官方會持續做一些Bug修複,會從Upstream Projects選擇代碼更新,也會從K Experimental代碼線選擇代碼,直到可以釋出下一個新版本4.4.1。 每一次新版本釋出,就意味着Android的一個正式版,伴随而來的是SDK版本和Android API版本的更新。
  • K Experimental這條代碼線用于實驗最新的特性。AOSP會從Android社群選擇一些新功能或是Bug修複放到該代碼線上。 Android社群一般是由第三方合作廠商來貢獻代碼,譬如HTC、Moto、SAMSUNG等一些大廠;還有一些活躍的基于AOSP的第三方項目,譬如CyanogenMod。 當一個Android版本釋出後,就會建立新的Experimental分支,譬如4.4釋出,就會基于4.4的釋出分支建立一個K Experimental分支。 當第三方合入K Experimental分支的代碼經過一段時間達到穩定狀态,被驗證通過後,就會合入K Release中的分支,作為下一個釋出版本4.4.1的新入代碼。
  • L Release這條代碼線用于Lollipop的版本釋出。在K Release代碼線釋出了4.4後,Google内部已經在運作Lollipop的5.0版本了(對應分支lollipop-release), 在5.0正式釋出之前,L Release代碼線都是非公開的,Google也會從K Experimental代碼線選擇性的合并代碼。 直到5.0正式釋出,L Experimental代碼線建構,又重新進入了同樣的開發流程。

總體而言,AOSP的代碼管理涉及到以下幾個方面:

  • 多代碼線并存:版本

    N

    的釋出代碼線和實驗代碼線,版本

    N+1

    的釋出代碼線,第三方開源代碼線。同一條代碼線可能存在多個多支,在一個多分支的環境下,就會存在”一個改動适應于哪些分支”的問題。
  • 兩方代碼合入:Google官方和第三方社群Community,第三方社群的構成元素很多,這也展現了Android的開源特性:“衆人拾柴火焰高”
  • 控制版本釋出:任何時候,都隻有一個最新的釋出版本,但内部會有多個版本齊頭并進。

    N+1

    版本在釋出以前,并非開源的,完全由Google内部主導,雖然包含很多取自于開源社群的代碼,但Google的态度是“取之有道,用之有度”。

3. AOSP的代碼自動流

同時維護多個分支,意味着在一個分支上的變更,也可能适應于其他分支,這種情況下,開發人員可以在其他分支上送出相同的内容。 然而,随着分支數量的膨脹,重複送出不僅繁瑣而且容易遺漏。是以,有必要引入自動送出的機制:當一個分支上有變更發生時,自動将這個變更記錄送出到到其他分支。

Android采用

git merge

的方式,自動将一個分支的改動合并到其他分支。從整個分支來看,就是代碼從一個分支上流向了其他分支,流的起點稱為上遊分支(upstream),流的終點稱為下遊分支(downstream)。 然而,自動代碼流雖然了避免了人工重複地送出,但也引入了新的問題:有一些改動僅适應于上遊分支,自動流向下遊分支會導緻出錯。是以,還需要對自動流進行控制。

在AOSP的送出記錄中(譬如:lollipop-release分支), 經常可以看到

DO NOT MERGE

關鍵字,為什麼送出記錄中會出現這種關鍵字?有什麼用途呢?

AOSP代碼管理1. 概要2. AOSP的代碼線、分支和釋出3. AOSP的代碼自動流4. 總結

來看一下AOSP的官方負責人Jean-Baptiste Queru的解釋:

We (Google) routinely develop on multiple branches at the same time. In order to make sure that the later branch (e.g. ics-mr1) contains all the new features and bugfixes developed in an older branch (e.g.ics-mr0), we have a server that automatically takes every commit made in ics-mr0 and merges it into ics-mr1. However, sometimes the engineer making a change in ics-mr0 knows that this change doesn’t apply to ics-mr1, e.g. because a similar issue was fixed differently in ics-mr1 and the fix from ics-mr0 wasn’t necessary. In that case, the engineer includes the words “do not merge” in their change description, and the auto-merger performs a “git merge -s ours” instead of “git merge” when handling that change. There’s a bit more complexity involved, but that’s the high-level view.
           

在上下遊分支之間,有一個自動合并代碼的工具auto-merger,對于上遊分支的每一個送出而言,都會通過

git merge

指令将送出内容自動合并到下遊分支。 不需要合并到下遊分支的送出,可以通過在送出注解(Comments)中加入DO NOT MERGE關鍵字,auto-merger如果遇到此關鍵字,則使用

git merge -s ours

來合并這個送出。 達到的效果是:送出記錄合并到下遊分支了,但送出所涉及到的代碼改動沒有合并。

AOSP代碼管理1. 概要2. AOSP的代碼線、分支和釋出3. AOSP的代碼自動流4. 總結

那麼問題來了,将一個分支的送出合并到另一個分支的方法有很多,諸如cherry-pick, rebase, format-patch和am等git指令都能夠實作,但為什麼AOSP采用的是git merge的方式? 而且,如果遇到DO NOT MERGE關鍵字,不往下遊分支合并就可以了,為什麼還要采用git merge -s ours這種方式?

我們的場景是将上遊分支(upstream)的代碼合并到下遊分支(downstream),下面我們來分析一下幾種代碼合并方式在同一個場景下的異同。

3.1 cherry-pick

在downstream上,使用cherry-pick從upstream選擇所需要的送出

$ git cherry-pick E
$ git cherry-pick F
           
AOSP代碼管理1. 概要2. AOSP的代碼線、分支和釋出3. AOSP的代碼自動流4. 總結

上遊分支的送出 E 和 F ,會依次重新送出到下遊分支,如果産生沖突,則cherry-pick失敗,需要解決沖突後重新送出,直到産生新的送出記錄 E’ 和 F’ 。 新送出的Commit ID(送出記錄的SHA1)已經發生了變化,即便改動内容完全一樣,Commit ID與原來upstream上的不一樣。

這種方式實際上也可以通過format-patch和am和來實作,同樣做到了從上遊分支選擇所需要的送出記錄,合并到下遊分支,Commit ID也會發生變化。

$ git checkout upstream
$ git format-patch E,F       # 在上遊分支,生成E,F兩個改動的patch

$ git checkout downstream
$ git am                      # 在下遊分支,應用已有的patch
           

我們再進一步考慮,基于送出 D 又拉出了下遊分支downstream2,并且有一個新增的送出 H, 仍然需要将upstream的改動合并到downstream2上。

AOSP代碼管理1. 概要2. AOSP的代碼線、分支和釋出3. AOSP的代碼自動流4. 總結

這時,仍然可以使用cherry-pick的方式,downstream2分支将會出現新的送出 E’’ 和 F’’ , 與之前的Commit ID都不同。

每一次當上遊分支有新的送出時,都需要将其cherry-pick到所有的下遊分支。雖然,這種方式能夠實作自動送出,讓送出記錄流起來, 但随着上遊分支送出的不斷增多,它與下遊分支漸行漸遠,要向前追溯很遠才能找到公共的父送出 B 。此時,噩夢就來了:

  • 如果使用merge将upstream合并到downstream,則會産生非常多的沖突。因為merge會找到兩個分支的公共父送出,然後做一個三路合并,upstream和downstream的公共父送出早已遠在 B 。
  • 如果在downstream演進一段時間後,基于某個送出又拉出了新的下遊分支downstream3,那很難精确的找到應該從upstream的哪個送出開始cherry-pick,因為downstream上已經包含了若幹upstream的送出内容; 如果又重新開始從公共父送出 B 開始 cherry-pick, 那同樣可能産生很多沖突,因為經過一段時間的開發,送出内容的變化已經很大。
  • 如果發現downstream更加适合做downstream2的上遊分支(很多送出隻适應于downstream和downstream2,不适合于upstream),那麼需要重建立立上、下遊分支的關系。 downstream的改動很難精确的cherry-pick到downstream2,因為downstream和downstream2相當于兩個獨立演進的分支,即便它們的代碼改動都差不多(都是從upstream上cherry-pick而來,但兩條分支線并沒有交合)。 同樣,也無法将downstream merge到downstream2,因為它們的公共父送出早已遠在 D。

3.2 rebase

在downstream上,使用rebase,将downstream變基到upstream

$ git rebase upstream
           
AOSP代碼管理1. 概要2. AOSP的代碼線、分支和釋出3. AOSP的代碼自動流4. 總結

在downstream上使用rebase,表示要改變目前的基節點的位置,通過rebase到upstream,就意味着将基節點切換到upstream的最新送出 F。 本來downstream和upstream公共的父節點是 B , 使用完rebase後,則會将 C 和 D 兩個送出記錄挑出來,重新送出到 F 之後, 這同樣會生成兩個新的送出記錄 C’ 和 C’ , Commit ID 與之前 C 和 D 的是不同的。

再進一步考慮,基于送出 D 拉出下遊分支downstream2,新增送出 H :

AOSP代碼管理1. 概要2. AOSP的代碼線、分支和釋出3. AOSP的代碼自動流4. 總結

這時,繼續使用rebase,将upstream合并到downstream2,會出現新的送出 C’’ , D’’ , H’‘, 與之前的Commit ID都不同。

每次當上遊分支有新的送出時,都需要重新rebase到上遊分支最新的送出,所有下遊分支公共父親節點之後都需要被重新送出一次。 一旦中間某一次送出産生沖突,就會停下來,直到沖突解決完畢,才能繼續rebase。

這種方式不适合代碼自動流:

  • rebase中斷,需要人工參與解決沖突。當分支演進到一定程度時,中斷的機率非常高,需要大量的人工參與,也就喪失了代碼自動送出的意義。
  • downstream和downstream2兩條分支雖然代碼改動差不多,但實際上是兩條差異很大的分支,因為它們的Commit ID不同。 如果要将這兩條分支作為上、下遊關系,面臨着與cherry-pick同樣窘境。

3.3 merge

在downstream上,使用merge,将upstream和送出合并到downstream

$ git merge upstream
           
AOSP代碼管理1. 概要2. AOSP的代碼線、分支和釋出3. AOSP的代碼自動流4. 總結

upstream和downstream兩個分支merge,會出現一個新的送出 M1 , 它的父送出有兩個,分别是 D 和 F。 如果産生沖突,則會一次性提示所有代碼改動産生的沖突,這與rebase不一樣,有一個很形象的比喻來形容merge和rebase進行代碼合并的差別:

将一堆玩具整理到一個箱子中,rebase是一件一件挪,如果箱子滿了(産生沖突),則需要整理一下箱子,騰出空間,再接着挪; 而merge是一股腦将玩具扔到箱子中,箱子滿了,再一起整理。

注意:

一次merge操作不一定會生成新的合并送出 M1, 預設情況下,

git merge

是采用fast-forward模式的,隻是改變指針位置。 如果不出現沖突,則分支圖中不會出現分叉的情況,還是保持所有的送出一條直線。這種情況下并不會産生新的送出記錄, E 和 F還是保留了原來的Commit ID。

再進一步考慮,基于downstream的送出記錄 D 又拉了新的分支downstream2 ,并增加了新的送出 H , 仍然采用merge将upstream合并到downstream2:

AOSP代碼管理1. 概要2. AOSP的代碼線、分支和釋出3. AOSP的代碼自動流4. 總結

這時産生了一個新的合并送出 M2 , 它的父送出是 F 和 H 。downstream和downstreanm2的公共父送出 F。

這裡有一個細節,upstream合并到downstream2,相當于(B, F, H)的三路合并,在此之前,将upstream合并到downstream,相當于(B, F, D)的三路合并。 E, F與C, D合并可能會産生沖突,一旦沖突解決,則沖突解決的方法就被git記錄下來了,再将E, F與C, D, H合并時,會利用之前解決的沖突,這樣沖突數量會減少很多。 具體可以參見

git-rerere - Reuse recorded resolution of conflicted merges

機制。

随着upstream的不斷演進,會不斷地merge到downstream和downstream2, 所有的下遊分支的公共父送出始終都跟蹤到upstream的最新送出記錄。

相比采用cherry-pick和rebase實作代碼自動流,merge的方式更适應多分支的場景:

  • 上遊分支的Commit ID得以保留,在使用Gerrit進行代碼Review的時候,舊的Commit ID并不會産生新的Review任務。是以在使用Gerrit進行代碼審查的場景下,并不會增加無效的Review任務。
    • Change-ID:Gerrit針對每一個Review任務,引入了一個Change-ID,每一個送出上傳到Gerrit,都會對應到一個Change-ID, 為了區分于Commit-ID,Gerrit設定Change-ID都是以大寫字母 “I” 打頭的。 Change-ID與Commit-ID并非一一對應的,每一個Commit-ID都會關聯到一個Change-ID,但Change-ID可以關聯到多個Commit-ID
    • Patch-Set:目前需要Review的改動内容。一個Change-ID關聯多個Commit-ID,就是通過Patch-Set來表現的,當通過git commit –amend指令修正上一次的送出并上傳時, Commit-ID已經發生了變化,但仍可以保持Change-ID不變,這樣,在Gerrit原來的Review任務下,就會出現新的Patch-Set。修正多少次,就會出現多少個Patch-Set, 可以了解,隻有最後一次修正才是我們想要的結果,是以,在所有的Patch-Set中,隻有最新的一個是真正有用的,能夠合并的。
  • 增加一個下遊分支的維護成本并不高,利用

    git-rerere

    機制,隻需要解決少量沖突,就能從上遊分支合并最新的代碼。後續演進,再使用merge自動從上遊分支合并代碼即可。
  • 多個下遊分支之間可以靈活進行合并,譬如,需要将downstream作為downstream2的上遊分支,隻需要在downstream2分支上merge一下downstream即可, 因為多個下遊分支的最近公共父送出就是上遊分支的最新送出(上圖中的 F ),并不需要向前追溯很遠。

3.4 merge strategy

我們分析了采用merge方式實作代碼自動流的原因。當然,cherry-pick和rebase也有自己的用武之地,它們能夠做到對每個送出進行精确控制,但merge做不到, 面對有送出不需要自動合并到下遊分支的情況,merge需要提供一種政策。

AOSP就是通過送出描述中的DO NOT MERGE關鍵字,來判斷目前送出是否需要往下遊分支合并。前文中我們提到了

git merge -s ours

的指令,實際上就是merge的一種分支合并政策。

如果某個送出不需要合并到下遊分支,那這個送出還必須得告訴git,否則,下一次再使用merge時,還是會找到上、下遊分支的上一次的公共父送出,将這個送出的代碼改動合并到下遊。 

-s ours

就表示合并這個送出記錄,但忽略這個送出的代碼修改。從整個送出記錄上看,分支圖中還是進行的一次merge操作。 這樣一來,上、下遊分支的公共父送出的位置發生了變化,下一次使用merge時,就會從這個公共父送出開始進行三路合并,不會引入

-s ours

這個送出的代碼改動。

4. 總結

本文介紹了AOSP的代碼管理方式,深入分析了代碼自動流的技術方案,通過merge實作代碼自動流,能夠降低多分支的維護成本。 誠然,要真正做到有效的代碼自動流,僅merge操作是不夠的,需要在多分支之間搭建一套代碼自動合并的方案,譬如:代碼自動送出的觸發時機、沖突的處理辦法都是需要考慮的問題, 另外,豐富一下merge操作送出時的注釋内容,也能夠幫助我們更好的回溯問題。

本文對merge, cherry-pick和rebase操作進行了對比,但這隻是在分支合并的場景下,并不是為了說明merge就是萬能的。cherry-pick和rebase操作都是很有用的指令, 譬如在topic分支上進行開發,可以使用rebase指令與master分支保持同步,而且所有的送出記錄都是線性的,不像merge操作一樣,形成複雜的網狀,網狀的分支圖會使得曆史送出記錄很難被追溯。

AOSP的代碼自動流政策,還比較自然,不同Android版本之間的代碼流起來,沖突也不會特别多,如果沖突很多,肯定也就說明問題來了,代碼差異越大隻會導緻越來越難維護,這自然不是AOSP期望看到的。 對于裝置廠商而言,面對的情況比AOSP要複雜,在跨Android版本、跨晶片平台的場景下,分支隻會更多,差異也隻會更大。

繼續閱讀