1. 程式人生 > >Android編譯系統分析之幾個關鍵點(三)

Android編譯系統分析之幾個關鍵點(三)

已開通新的部落格,後續文字都會發到新部落格

http://www.0xfree.top


Android 編譯系統解析系列文件

解析lunch的執行過程以及make執行過程中include檔案的順序

關注一些make執行過程中的幾個關鍵點

對一些獨特的語法結構進行解析


這篇文章的主要內容我們來分析關於模組配置檔案Android.mk載入的一些關鍵的知識點

躲不開的背景知識"mm"與"mmm"

在分析模組配置檔案Android.mk檔案的載入過程之前,我們需要先了解一段背景知識,那就是Android.mk的使用情景是什麼樣的?

還記得我們在前邊分析lunch的時候提到的原始碼的全編與模組編譯嗎?

控制全編與模組編譯的命令就在envsetup.sh檔案中定義,其中全編直接執行make,模組單編需要用到mm與mmm兩條命令,定義如下

function findmakefile()
{
    TOPFILE=build/core/envsetup.mk
    local HERE=$PWD
    T=
    while [ \( ! \( -f $TOPFILE \) \) -a \( $PWD != "/" \) ]; do
        T=`PWD= /bin/pwd`
        if [ -f "$T/Android.mk" ]; then
            echo $T/Android.mk
            \cd $HERE
            return
        fi
        \cd ..
    done
    \cd $HERE
}

function mm()
{
    local T=$(gettop)
    local DRV=$(getdriver $T)
    # If we're sitting in the root of the build tree, just do a
    # normal make.
    if [ -f build/core/envsetup.mk -a -f Makefile ]; then
        $DRV make 
[email protected]
else # Find the closest Android.mk file. local M=$(findmakefile) local MODULES= local GET_INSTALL_PATH= local ARGS= # Remove the path to top as the makefilepath needs to be relative local M=`echo $M|sed 's:'$T'/::'` if [ ! "$T" ]; then echo "Couldn't locate the top of the tree. Try setting TOP." elif [ ! "$M" ]; then echo "Couldn't locate a makefile from the current directory." else for ARG in
[email protected]
; do case $ARG in GET-INSTALL-PATH) GET_INSTALL_PATH=$ARG;; esac done if [ -n "$GET_INSTALL_PATH" ]; then MODULES= ARGS=GET-INSTALL-PATH else MODULES=all_modules [email protected] fi ONE_SHOT_MAKEFILE=$M $DRV make -C $T -f build/core/main.mk $MODULES $ARGS fi fi }

先看mm命令,如果執行這條命令的路徑為TOP目錄,那麼就等價於直接使用make命令
如果不是,我們就以當前目錄為基點,遞迴向上查詢距離最近的Android.mk檔案,這個查詢的過程在findmakefile()函式中定義

Android編譯系統在使用mm命令的時候為我們提供了一個引數,可以方便我們打印出所要編譯模組的最終安裝路徑,這個引數就是GET-INSTALL-PATH

  • 如果編譯系統在檢查到正在使用mm時加了這個引數,我們就不執行編譯的操作,只打印這個引數的值

這個邏輯的實現其實只是將GET-INSTALL-PATH定義為了一個target,我們使用mm時,會將這個target傳進去,從而呼叫到這個target定義的命令,我們後邊遇到模組解析程式碼的時候就會看到這個target相關程式碼

  • 如果沒有這個引數,我們就指定編譯全部的模組(all_modules),然後將mm後邊的引數全部作為MAKEGOALS傳入

以上就是mm執行的全部過程,有三點需要注意:

  • $DRV,這個變數的作用是加一些靜態分析的選項以及路徑
  • ARG引數的新增可以讓我們使用-B這樣的make自帶的引數
  • ONE_SHOT_MAKEFILE是區別全編與模組編譯的關鍵變數

對於mmm函式,使用方法為直接指定Android.mk所在的資料夾,除此之外最終呼叫的命令與mm是一樣的,有興趣的讀者可以自己來試著解析

瞭解了使用方法之後,mm在呼叫的時候會傳入ONE_SHOT_MAKEFILE引數,這個引數是區別全編和模組編譯的重點,接下來我們來具體看看這個引數帶來的實質的影響

模組檔案載入解析過程

如果讀過我之前make解析文章的同學一定還記得include的順序,沒錯,Android.mk檔案的解析的主要程式碼是在build/core/main.mk檔案中,附程式碼如下:

# Before we go and include all of the module makefiles, stash away
# the PRODUCT_* values so that later we can verify they are not modified.
stash_product_vars:=true
ifeq ($(stash_product_vars),true)
  $(call stash-product-vars, __STASHED)
endif

ifneq ($(ONE_SHOT_MAKEFILE),)
# We've probably been invoked by the "mm" shell function
# with a subdirectory's makefile.
include $(ONE_SHOT_MAKEFILE)
# Change CUSTOM_MODULES to include only modules that were
# defined by this makefile; this will install all of those
# modules as a side-effect.  Do this after including ONE_SHOT_MAKEFILE
# so that the modules will be installed in the same place they
# would have been with a normal make.
CUSTOM_MODULES := $(sort $(call get-tagged-modules,$(ALL_MODULE_TAGS)))
FULL_BUILD :=
# Stub out the notice targets, which probably aren't defined
# when using ONE_SHOT_MAKEFILE.
NOTICE-HOST-%: ;
NOTICE-TARGET-%: ;

# A helper goal printing out install paths
.PHONY: GET-INSTALL-PATH
GET-INSTALL-PATH:
	@$(foreach m, $(ALL_MODULES), $(if $(ALL_MODULES.$(m).INSTALLED), \
		echo 'INSTALL-PATH: $(m) $(ALL_MODULES.$(m).INSTALLED)';))

else # ONE_SHOT_MAKEFILE

ifneq ($(dont_bother),true)
#
# Include all of the makefiles in the system
#

# Can't use first-makefiles-under here because
# --mindepth=2 makes the prunes not work.
subdir_makefiles := \
	$(shell build/tools/findleaves.py --prune=$(OUT_DIR) --prune=.repo --prune=.git $(subdirs) Android.mk)

$(foreach mk, $(subdir_makefiles), $(info including $(mk) ...)$(eval include $(mk)))

endif # dont_bother

endif # ONE_SHOT_MAKEFILE

# Now with all Android.mks loaded we can do post cleaning steps.
include $(BUILD_SYSTEM)/post_clean.mk

ifeq ($(stash_product_vars),true)
  $(call assert-product-vars, __STASHED)
endif

在真正的呼叫ONE_SHOT_MAKEFILE變數判斷全編還是模組編譯之前,我們還有一件事需要做:

暫存PRODUCT_*系列變數(stash-product-vars)

這項操作的用意很明顯,我們對於Product級的配置已經結束,接下來載入的模組級別的配置是不能影響干擾到Product的相關配置,所以我們需要暫存變數,來方便後邊比對是否修改了這些變數(assert-product-vars)

從這個操作我們也可以看出,Android編譯系統對於Product配置在載入模組配置檔案Android.mk檔案之前就已經結束,從include檔案順序表中我們可以看到也就是在lunch的全部宣告週期做完了Product的配置的載入,這裡我們之所以不說是完成配置,而是完成載入,是因為對於PRODUCT_COPY_FILES這個變數我們還有操作需要處理,這塊內容我們會在後序的文章中說明

接下來我們又遇到一個新的關鍵字TAG,如果編寫過模組程式碼,那麼對這個TAG應該不陌生,常用的定義有user,eng,tests,optional等,你可以指定對應的TAG,使得它在指定的編譯型別中生效

這裡使用一個get-tagged-modules函式來根據我們當前的編譯的varient來挑選出合適的模組加入待編譯列表

瞭解這個函式之前,我們需要知道傳入的引數ALL_MODULE_TAGS的作用是什麼

我們之前在解析各編譯檔案的作用時曾經提到過,envsetup.mk的作用主要是定義一些編譯系統需要用到的巨集,而definitions.mk檔案則是用來定義一些公有的函式,這些公有函式主要用在模組編譯規則檔案Android.mk的編寫,所以在遇到ALL_MODULE_TAGS這個變數,我們首先想到的就是去definitions.mk檔案中檢視,我們發現
definitions.mk中定義了ALL_MODULE_TAGS以及操作這個變數的相關函式,但是真正的為這個變數賦值的操作發生在base_rules.mk中,那麼這個base_rules.mk與definitions.mk之間是什麼關係呢?

一個Android.mk的示例

我們選擇一個Android.mk來看看include之後發生了什麼
示例Android.mk檔案內容:

LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE_TAGS := optional

LOCAL_SRC_FILES := \
        $(call all-java-files-under, src)

LOCAL_PACKAGE_NAME := Nfc

LOCAL_JNI_SHARED_LIBRARIES := libnfc_mt6605_jni libmtknfc_dynamic_load_jni

LOCAL_PROGUARD_ENABLED := disabled

include $(BUILD_PACKAGE)

以上檔案是一個NFC模組的Android.mk檔案,我們從前邊執行mm的流程可以得知,如果我們在NFC模組規則檔案Android.mk檔案所在的目錄下執行mm,實際執行的操作是找到這個Android.mk檔案,並將這個檔案賦值給ONE_SHOT_MAKEFILE,然後在main.mk檔案中載入進來,也就是我們會在main.mk檔案中依次執行以上檔案的內容:

  • 使用$(CLEAR_VARS)清零各變數
  • 定義幾個以LOCAL_*開頭的變數
  • 載入一個對應的編譯規則檔案$(BUILD_PACKAGE)

我們就以這個檔案為例,來解析一下一般Android.mk檔案載入的流程:
首先$(CLEAR_VARS)對應的是一個makefile檔案clear_vars.mk,內容是對各個LOCAL_*變數的清零操作,這個巨集的定義是在config.mk檔案中,也就是在載入模組規則檔案Android.mk檔案之前

然後$(BUILD_PACKAGE)也是一個makefile檔案package.mk,內容是關於對一個package編譯的規則,Android編譯系統定義了一系列的巨集來將編譯各種型別模組的規則打包,我們只需要在每個模組定義的最後引用就可以

瞭解以上兩點,我們就可以使用之前分析make命令執行流程的方法來分析這個示例檔案被include到main.mk之後的執行過程,以下是include Android.mk檔案之後的include檔案順序:

#模組編譯時的include順序,以package.mk為例
config.mk (
    62-97: BUILD_SYSTEM_INTERNAL_FILE (
        CLEAR_VARS:clear_vars.mk
        ......
        BUILD_STATIC_LIBRARY:static_library.mk
        BUILD_SHARED_LIBRARY:shared_library.mk
        BUILD_PACKAGE:package.mk (
            6:include multilib.mk
            53:include module_arch_supported.mk
            55:include package_internal.mk (
                204:include android_manifest.mk
                207:include java.mk (
                    307:include base_rules.mk (
                        165:include configure_module_stem.mk
                        688:include $(BUILD_NOTICE_FILE)
                    )
                    314:include dex_preopt_odex_install.mk
                )
                356:include install_jni_libs.mk (
                    81:include install_jni_libs_internal.mk
                )
            )
        )
        BUILD_PHONY_PACKAGE:phone_package.mk
        ......
        BUILD_PREBUILT:prebuilt.mk
        ......
    )
)

從以上的include順序圖中,我們可以很清晰的發現base_rulse.mk的身影,這裡我們只關心ALL_MODULE_TAGS,所以直接來看base_rules.mk檔案中對這個變數的處理:

my_module_tags := $(LOCAL_MODULE_TAGS)

LOCAL_UNINSTALLABLE_MODULE := $(strip $(LOCAL_UNINSTALLABLE_MODULE))
my_module_tags := $(sort $(my_module_tags))
ifeq (,$(my_module_tags))
  my_module_tags := optional
endif

# User tags are not allowed anymore.  Fail early because it will not be installed
# like it used to be.
ifneq ($(filter $(my_module_tags),user),)
  $(warning *** Module name: $(LOCAL_MODULE))
  $(warning *** Makefile location: $(LOCAL_MODULE_MAKEFILE))
  $(warning * )
  $(warning * Module is attempting to use the 'user' tag.  This)
  $(warning * used to cause the module to be installed automatically.)
  $(warning * Now, the module must be listed in the PRODUCT_PACKAGES)
  $(warning * section of a product makefile to have it installed.)
  $(warning * )
  $(error user tag detected on module.)
endif

# Only the tags mentioned in this test are expected to be set by module
# makefiles. Anything else is either a typo or a source of unexpected
# behaviors.
ifneq ($(filter-out debug eng tests optional samples,$(my_module_tags)),)
$(warning unusual tags $(my_module_tags) on $(LOCAL_MODULE) at $(LOCAL_PATH))
endif

從上到下,依次說明了這幾件事:

  • 如果LOCAL_MODULE_TAG未定義,那麼預設使用optional
  • user這個TAG已經廢棄,如果需要定義這個TAG,可以將其加入到PRODUCT_PACKAGES變數中
  • TAG只能是debugengtestsoptionalsamples這幾個

我們在這裡拿到tag之後,就需要對其進行處理:

# Keep track of all the tags we've seen.
ALL_MODULE_TAGS := $(sort $(ALL_MODULE_TAGS) $(my_module_tags))

# Add this module to the tag list of each specified tag.
# Don't use "+=". If the variable hasn't been set with ":=",
# it will default to recursive expansion.
$(foreach tag,$(my_module_tags),\
    $(eval ALL_MODULE_TAGS.$(tag) := \
        $(ALL_MODULE_TAGS.$(tag)) \
        $(LOCAL_INSTALLED_MODULE)))

這個示例中Android.mk只定義了一個模組,所以這裡的ALL_MODULE_TAGS就是Android.mk定義的LOCAL_MODULTE_TAG,如果是多個模組,這裡就是多個模組的綜合
然後使用不同的TAG字尾,將對應TAG的模組賦值給ALL_MODULE_TAGS.$(tag),這裡模組只有一個,所以其值也是唯一,這樣對應TAG的模組我們就可以拿到了

我們回過頭繼續看CUSTOM_MODULE的值是需要get-tagged-modules取出來的,我們來看這個函式:

define modules-for-tag-list
$(sort $(foreach tag,$(1),$(ALL_MODULE_TAGS.$(tag))))
endef

# Same as modules-for-tag-list, but operates on
# ALL_MODULE_NAME_TAGS.
# $(1): tag list
define module-names-for-tag-list
$(sort $(foreach tag,$(1),$(ALL_MODULE_NAME_TAGS.$(tag))))
endef

# Given an accept and reject list, find the matching
# set of targets.  If a target has multiple tags and
# any of them are rejected, the target is rejected.
# Reject overrides accept.
# $(1): list of tags to accept
# $(2): list of tags to reject
#TODO(dbort): do $(if $(strip $(1)),$(1),$(ALL_MODULE_TAGS))
#TODO(jbq): as of 20100106 nobody uses the second parameter
define get-tagged-modules
$(filter-out \
	$(call modules-for-tag-list,$(2)), \
	    $(call modules-for-tag-list,$(1)))
endef

get-tagged-modules有兩個引數,第一個引數對應的是我們想要取出的模組的tag,第二個引數對應我們不想取出的模組對應的tag,獲取CUSTOM_MODULE時,只傳入了我們想要取出的模組tag,所以我們我們看到,對於傳入的要取出的對應tag的模組,我們只是從ALL_MODULE_TAGS對應tag字尾中取出即可

雖然一個簡單的模組編譯規則繞了這麼一大圈,但是這只是對於單個模組而言,這套模組編譯系統的強大之處對於多個模組編譯才能真正體現出來,也就是我們進行全編的時候才會見識到它真正的威力

ALL_DEFAULT_INSTALLED_MODULES
挑選完模組之後,我們就看到前邊解析mm時要到的列印目標模組安裝路徑的那個target,然後模組的單編也就完成了引數的傳入

回過頭來我們看看全編過程對Android.mk檔案的處理,當include全部的Android.mk之後,我們會發現所有的模組檔案都各自被這兩條語句包括著:


include $(CLEAR_VARS)

......

include $(BUILD_PACKAGE)

我們前邊已經講到過CLEAR_VARS就是一個全部LOCAL_*變數的清零操作的mk合集,而BUILD_*這型別的檔案定義了各種型別的模組的編譯規則

這裡還有一個dont_bother的問題,dont_bother的出現,是因為Android編譯系統定義了一些特殊的目標,在編譯這些目標時,不需要載入Android.mk檔案,具體的定義在build/core/main.mk

# These goals don't need to collect and include Android.mks/CleanSpec.mks
# in the source tree.
dont_bother_goals := clean clobber dataclean installclean \
    help out \
    snod systemimage-nodeps \
    stnod systemtarball-nodeps \
    userdataimage-nodeps userdatatarball-nodeps \
    cacheimage-nodeps \
    vendorimage-nodeps \
    ramdisk-nodeps \
    bootimage-nodeps

ifneq ($(filter $(dont_bother_goals), $(MAKECMDGOALS)),)
dont_bother := true
endif

包括clean, help, 以及一些image的打包等,這些目標都是獨立的,不需要依賴,因此對於程式碼相關的模組是不需要參與編譯的

找到系統所有的Android.mk

對於全編過程中所有模組檔案的載入我們用到了findleaves.py

import os
import sys

def perform_find(mindepth, prune, dirlist, filename):
  result = []
  pruneleaves = set(map(lambda x: os.path.split(x)[1], prune))
  for rootdir in dirlist:
    rootdepth = rootdir.count("/")
    for root, dirs, files in os.walk(rootdir, followlinks=True):
      # prune
      check_prune = False
      for d in dirs:
        if d in pruneleaves:
          check_prune = True
          break
      if check_prune:
        i = 0
        while i < len(dirs):
          if dirs[i] in prune:
            del dirs[i]
          else:
            i += 1
      # mindepth
      if mindepth > 0:
        depth = 1 + root.count("/") - rootdepth
        if depth < mindepth:
          continue
      # match
      if filename in files:
        result.append(os.path.join(root, filename))
        del dirs[:]
  return result

def usage():
  sys.stderr.write("""Usage: %(progName)s [<options>] <dirlist> <filename>
Options:
   --mindepth=<mindepth>
       Both behave in the same way as their find(1) equivalents.
   --prune=<dirname>
       Avoids returning results from inside any directory called <dirname>
       (e.g., "*/out/*"). May be used multiple times.
""" % {
      "progName": os.path.split(sys.argv[0])[1],
    })
  sys.exit(1)

def main(argv):
  mindepth = -1
  prune = []
  i=1
  while i<len(argv) and len(argv[i])>2 and argv[i][0:2] == "--":
    arg = argv[i]
    if arg.startswith("--mindepth="):
      try:
        mindepth = int(arg[len("--mindepth="):])
      except ValueError:
        usage()
    elif arg.startswith("--prune="):
      p = arg[len("--prune="):]
      if len(p) == 0:
        usage()
      prune.append(p)
    else:
      usage()
    i += 1
  if len(argv)-i < 2: # need both <dirlist> and <filename>
    usage()
  dirlist = argv[i:-1]
  filename = argv[-1]
  results = list(set(perform_find(mindepth, prune, dirlist, filename)))
  results.sort()
  for r in results:
    print r

if __name__ == "__main__":
  main(sys.argv)

雖然函式內容很少,但是我們可以從中看出一些細節實現,所以我們在這裡簡單分析一下:
首先來看使用方法,就是定義的函式usage(),我們從

Usage: %(progName)s [<options>] <dirlist> <filename>

可以看出函式後加兩個可選引數和兩個不可省略引數,我們分開來看他們的規則

  1. 可選引數
    • –mindepth:相對於查詢目錄的最淺深度,如果沒有達到,不會查詢filename,可以有多個引數,取最後一個定義
    • –prune:略過查詢的目錄,可以定義多個目錄
  2. 不可省略引數
    • dirlist:要執行查詢操作的目錄列表,需要有1個或多個引數
    • filename:引數唯一,需要執行查詢的檔名

Android編譯系統在這裡使用的命令是

build/tools/findleaves.py --prune=$(OUT_DIR) --prune=.repo --prune=.git $(subdirs) Android.mk

也就是排除了out,.repo,.git三個目錄,在根目錄下查詢Android.mk檔案

main函式為入口函式,在perform_find函式之前主要是對引數進行處理,將需要排除的目錄放入prune陣列中,執行實際查詢的函式主要在perform_find中,我們來看

def perform_find(mindepth, prune, dirlist, filename):
  result = []
  pruneleaves = set(map(lambda x: os.path.split(x)[1], prune))
  for rootdir in dirlist:
    rootdepth = rootdir.count("/")
    for root, dirs, files in os.walk(rootdir, followlinks=True):
      # prune
      check_prune = False
      for d in dirs:
        if d in pruneleaves:
          check_prune = True
          break
      if check_prune:
        i = 0
        while i < len(dirs):
          if dirs[i] in prune:
            del dirs[i]
          else:
            i += 1
      # mindepth
      if mindepth > 0:
        depth = 1 + root.count("/") - rootdepth
        if depth < mindepth:
          continue
      # match
      if filename in files:
        result.append(os.path.join(root, filename))
        del dirs[:]
  return result

這個函式主要做了以下幾件事:

  • 首先記錄了目錄深度,用於在後邊判斷是否達到最淺目錄深度(mindepth)
  • 然後遍歷傳入的目錄,也就是原始碼根目錄
  • 檢查是否需要在當前目錄略過指定的目錄(加快搜索速度)
  • 如果檢查到需要略過的目錄,刪掉當前目錄下的子目錄中所有指定的目錄
  • 判斷是否達到最淺的搜尋深度
  • 如果檔名匹配,將它放到reslut陣列中
  • 返回查詢到的所有的Android.mk

函式很簡單,但是需要注意兩點:

  • 如果最小目錄深度mindepth沒有達到,那麼不會匹配當前目錄的檔案,直到達到目錄深度才會執行開始匹配
  • 如果當前資料夾下匹配到了Android.mk,那麼就清空子目錄列表,也就是停止繼續向下查詢

以上兩點是很重要的,第一點可以讓我們可以自由的控制從哪個目錄深度開始查詢,第二點可以讓我們自由的拓展目錄的深度與廣度,可以自由的控制深度目錄下的模組的編譯與否,Android編譯系統提供了兩個函式來搭配這種查詢方式all-makefiles-underfirst-makefiles-under

一個小插曲

指令碼解析完了,這裡還有一個小插曲說明一下:
findleaves.py是一個python指令碼,這點我們已經瞭解,你可能會想到為什麼不是一個shell指令碼,額,沒錯,你想的沒錯,這之前確實是一個bash指令碼,09年的8月份被替換掉了,google支援python的時間還真是久遠,原因是因為可以大大縮短解析的時間

下邊作者當初的提交,讓我們來看看來究竟比bash強大在什麼地方
使用python指令碼替換bash指令碼的git變更提交

作者在提交裡說明,使用python重寫的原因有兩個

  • 可以使用多重prune來排除目錄
  • 有效的縮短查詢時間

確實,從作者的提交記錄來看,從30秒縮減到不到1秒,確實提升很多,我們看到這裡已對python頂禮膜拜,敬仰之情滔滔不絕

然並xxx

其實shell指令碼該實現的也都已經實現,並且在搜尋效能上不僅不差python,還更勝一籌,下圖是實際的對比結果

實際比對結果

作者當初選擇python重寫,一者可能是因為喜愛python,另一個原因可能是他根本不會用那個shell指令碼…

好了,小插曲過後,我們回過頭來繼續研究,我們讀了兩個指令碼之後,也明白了Android.mk檔案的搜尋條件,簡而言之:
從$(subdirs)也就是原始碼根目錄開始搜尋,排除.repo,out和.git目錄,搜尋各個目錄下的Android.mk並列印,關於其中的查詢規則,前邊已經說明,我們不再贅述

結束語

至此,關於Android.mk相關的內容也已經解析完畢,對於模組編譯的內容的解析可能不是那麼深,因為模組編譯有單獨的一套規則,且相對獨立,在一般的系統開發中的出問題的可能性比較小,所以對於這方面待日後遇到問題再來詳細補充