【转】Android源代码编译命令mmmmmmmake分析--不错
在前⽂中,我们分析了Android编译环境的初始化过程。Android编译环境初始化完成后,我们就可以⽤m/mm/mmm/make命令编译源代码了。当然,这要求每⼀个模块都有⼀个Android.mk⽂件。Android.mk实际上是⼀个Makefile脚本,⽤来描述模块编译信息。Android编译系统通过整合Android.mk⽂件完成编译过程。本⽂就对Android源代码的编译过程进⾏详细分析。
从前⾯这篇⽂章可以知道,lunch命令其实是定义在build/envsetup.sh⽂件中的函数lunch提供的。与lunch命令⼀样,m、mm和mmm命令也分别是由定义在
build/envsetup.sh⽂件中的函数m、mm和mmm提供的,⽽这三个函数⼜都是通过make命令来对源代码进⾏编译的。事实上,命令m就是对make命令的简单封装,并且是⽤来对整个Android源代码进⾏编译,⽽命令mm和mmm都是通过make命令来对Android源码中的指定模块进⾏编译。接下来我们就先分别介绍⼀下函数m、mm和mmm的实现,然后进⼀步分析它们是如何通过make命令来编译代码的。
函数m的实现如下所⽰:
[plain]
1. function m()
2. {
3. T=$(gettop)
4. if [ "$T" ]; then
5. make -C $T $@
6. else
7. echo "Couldn't locate the top of the tree. Try setting TOP."
8. fi
9. }
函数m调⽤函数gettop得到的是Android源代码根⽬录T。在执⾏make命令的时候,先通过-C选项指定⼯作⽬录为T,即Android源代码根⽬录,接着⼜将执⾏命令m指定的参数$@作为命令make的参数。从这⾥就可以看出,命令m实际上就是对命令make的简单封装。
函数mm的实现如下所⽰:
[plain]
1. function mm()
2. {
3. # If we're sitting in the root of the build tree, just do a
4. # normal make.
5. if [ -f build/core/envsetup.mk -a -f Makefile ]; then
6. make $@
7. else
8. # Find the closest Android.mk file.
9. T=$(gettop)
10. local M=$(findmakefile)
11. # Remove the path to top as the makefilepath needs to be relative
12. local M=`echo $M|sed 's:'$T'/::'`
13. if [ ! "$T" ]; then
14. echo "Couldn't locate the top of the tree. Try setting TOP."
15. elif [ ! "$M" ]; then
16. echo "Couldn't locate a makefile from the current directory."
17. else
18. ONE_SHOT_MAKEFILE=$M make -C $T all_modules $@
19. fi
20. fi
21. }
函数mm⾸先是判断当前⽬录是否就是Android源码根⽬录,即当前⽬录下是否存在⼀个build/core/envsetup.mk⽂件和⼀个Makefile⽂件。如果是的话,就将命令mm当作是⼀个普通的make命令来执⾏。否则的话,就调⽤函数findmakefile从当前⽬录开始⼀直往上寻是否存在⼀个Android.mk⽂件。如果在寻的过程中,发现了⼀个Android.mk⽂件,那么就获得它的绝对路径,并且停⽌上述寻过程。
由于接下来执⾏make命令时,我们需要指定的是要编译的Android.mk⽂件的相对于Android源码根⽬录路径,因此函数mm需要将刚才到的Android.mk绝对⽂件路径M中与Android源码根⽬录T相同的那部分路径去掉。这是通过sed命令来实现的,也就是将字符串M前⾯与字符串T相同的⼦串删掉。
最后,将到的Android.mk⽂件的相对路径设置给环境变量ONE_SHOT_MAKE,表⽰接下来要对它进⾏编译。另外,函数mm还将make命令⽬标设置为all_modules。这是什么意思呢?我们知道,⼀个Android.mk⽂件同时可以定义多个模块,因此,all_modules就表⽰要对前⾯指定的Android.mk⽂件中定义的所有模块进⾏编译。
函数mmm的实现如下所⽰:
[plain]
1. function mmm()
2. {
3. T=$(gettop)
4. if [ "$T" ]; then
5. local MAKEFILE=
6. local MODULES=
7. local ARGS=
8. local DIR TO_CHOP
9. local DASH_ARGS=$(echo "$@" | awk -v RS=" " -v ORS=" " '/^-.*$/')
10. local DIRS=$(echo "$@" | awk -v RS=" " -v ORS=" " '/^[^-].*$/')
11. for DIR in $DIRS ; do
12. MODULES=`echo $DIR | sed -n -e 's/.*:.∗$/\1/p' | sed 's/,/ /'`
13. if [ "$MODULES" = "" ]; then
14. MODULES=all_modules
15. fi
16. DIR=`echo $DIR | sed -e 's/:.*//' -e 's:/$::'`
17. if [ -f $DIR/Android.mk ]; then
18. TO_CHOP=`(cd -P -- $T && pwd -P) | wc -c | tr -d ' '`
19. TO_CHOP=`expr $TO_CHOP + 1`
20. START=`PWD= /bin/pwd`
21. MFILE=`echo $START | cut -c${TO_CHOP}-`
22. if [ "$MFILE" = "" ] ; then
23. MFILE=$DIR/Android.mk
24. else
25. MFILE=$MFILE/$DIR/Android.mk
26. fi
27. MAKEFILE="$MAKEFILE $MFILE"
28. else
29. if [ "$DIR" = snod ]; then
30. ARGS="$ARGS snod"
31. elif [ "$DIR" = showcommands ]; then
32. ARGS="$ARGS showcommands"
33. elif [ "$DIR" = dist ]; then
34. ARGS="$ARGS dist"
35. elif [ "$DIR" = incrementaljavac ]; then
36. ARGS="$ARGS incrementaljavac"
37. else
38. echo "No Android.mk in $DIR."
39. return 1
40. fi
41. fi
42. done
43. ONE_SHOT_MAKEFILE="$MAKEFILE" make -C $T $DASH_ARGS $MODULES $ARGS
44. else
45. echo "Couldn't locate the top of the tree. Try setting TOP."
46. fi
47. }
函数mmm的实现就稍微复杂⼀点,我们详细解释⼀下。
⾸先,命令mmm可以这样执⾏:
[plain]
1. $ mmm <dir-1> <dir-2> ... <dir-N>[:module-1,module-2,...,module-M]
其中,dir-1、dir-2、dir-N都是包含有Android.mk⽂件的⽬录。在最后⼀个⽬录dir-N的后⾯可以带⼀个冒号,冒号后⾯可以通过逗号分隔⼀系列的模块名称module-1、module-2和module-M,⽤来表⽰要编译前⾯指定的Android.mk中的哪些模块。
知道了命令mmm的使⽤⽅法之后,我们就可以分析函数mmm的执⾏逻辑了:
1. 调⽤函数gettop获得Android源码根⽬录。
2. 通过命令awk将执⾏命令mmm时指定的选项参数提取出来,也就是将以横线“-”开头的字符串提取出来,并且保存在变量DASH_ARGS中。
3. 通过命令awk将执⾏命令mmm时指定的⾮选项参数提取出来,也就是将⾮以横线“-”开头的字符串提取出来,并且保存在变量DIRS中。这⾥得到的实际上就是跟在命令mmm后⾯的字符串“<dir-1> <dir-2> ... <dir-N>[:module-1,module-2,...,module-M]”。
4. 变量DIRS保存的字符串可以看成是⼀系以空格分隔的⼦字符串,因此,就可以通过⼀个for循环来对这些⼦字府串进⾏遍历。每⼀个⼦字符串DIR描述的都是⼀个包含有Android.mk⽂件的⽬录。对每⼀个⽬录DIR执⾏以下操作:
4.1 由于⽬录DIR后⾯可能会通过冒号指定有模块名称,因此就先通过两个sed命令来获得这些模块名称。第⼀个sed命令获得的是⼀系列以逗号分隔的模块名称列表,第⼆个sed命令⽤来将前⾯获得的以逗号分隔的模块名称列表转化为以空格分隔的模块名称列表。最后,获得的以空格分隔的模块名称列表保存在变量MODULES中。由于⽬录DIR后⾯也可能不指定有模块名称,因此前⾯得到的变量MODULES的值就会为空。在这种情况下,需要将变量MODULES的值设置为“all_modules”,表⽰要编译的是所有模块。
4.2 通过两个sed命令获得真正的⽬录DIR。第⼀个sed命令将原来DIR字符串后⾯的冒号以及冒号后⾯
的模块列表字符串删掉。第⼆个sed命令将执⾏前⾯⼀个sed命令获得的⽬录后⾯的"/"斜线去掉,最后就得到⼀个末尾不带有斜线“/”的路径,并且保存在变量DIR中。
4.3 如果变量DIR描述的是⼀个真正的路径,也就是在该路径下存在⼀个Android.mk⽂件,那么就进⾏以下处理:
4.3.1 统计Android源码根⽬录T包含的字符数,并且将这个字符数加1,得到的值保存在变量TO_CHOP中。
4.3.2 通过执⾏/bin/pwd命令获得当前执⾏命令mmm的⽬录START。
4.3.3 通过cut命令获得当前⽬录START相对于Android源码根⽬录T的路径,并且保存在变量MFILE中。
4.3.4 如果变量MFILE的值等于空,就表明是在Android源码根⽬录T中执⾏mmm命令,这时候就表明变量DIR描述的就是相对Android源码根⽬录T的⼀个⽬录,这时候指定的Android.mk⽂件相对于Android源码根⽬录T的路径就为$DIR/Android.mk。
4.3.5 如果变量MFILE的值不等于空,就表明是在Android源码根⽬录T的某⼀个⼦⽬录中执⾏mmm命令,这时候$MFILE/$DIR/Android.mk表⽰的Android.mk⽂件路径才是相对于Android源码根⽬录T的。
4.3.6 将获得的Android.mk路径MFILE附加在变量MAKEFILE描述的字符串的后⾯,并且以空格分隔。
4.4 如果变量DIR描述的不是⼀个真正的路径,并且它的值等于"snod"、"showcomands"、“dist”或者“incrementaljavac”,那么它描述的其实是make修饰命令。这四个修饰命令的含义分别如下所⽰:
4.4.1 snod是“systemimage with no dependencies”的意思,表⽰忽略依赖性地重新打包system.img。
4.4.2 showcommands表⽰显⽰编译过程中执⾏的命令。
4.4.3 dist表⽰将编译后产⽣的发布⽂件拷贝到out/dist⽬录中。
4.4.4 incrementaljavac表⽰对Java源⽂件采⽤增量式编译,也就是如果⼀个Java⽂件如果没有修改过,那么就不要重新⽣成对应的class⽂件。
5. 上⾯的for循环执⾏完毕,变量MAKEFILE保存的是要编译的Android.mk⽂件列表,它们都是相对于Android源码根⽬录的路径,变量DASH_ARGS保存的是原来执⾏mmm 命令时带的选项参数,变量MODULES保存的是指定要编译的模块名称,变量ARGS保存的是修饰命令。其中,变量MAKEFILE的内容通过环境变量ONE_SHOT_MAKEFILE传递给make命令,⽽其余变量都是通过参数的形式传递给make命令,并且变量MODULES作为make命令的⽬标。
明⽩了函数m、mm和mmm的实现之后,我们就可以知道:
1. mm和mmm命令是类似的,它们都是⽤来编译某些模块。
2. m命令⽤来编译所有模块。
如果我们理解了mm或者mmm命令的编译过程,那么⾃然也会明⽩m命令的编译过程,因为所有模块的编译过程就等于把每⼀个模块的编译都编译出来,因此,接下来我们就选择具有代表性的、常⽤的编译命令mmm来分析Android源码的编译过程,如图1所⽰:
图1 mmm命令的编译过程
函数mmm在Android源码根⽬录执⾏make命令的时候,没有通过-f指定Makefile⽂件,因此默认就使⽤Android源码根⽬录下的Makefile⽂件,它的内容如下所⽰:
[plain]
1. ### DO NOT EDIT THIS FILE ###
2. include build/core/main.mk
3. ### DO NOT EDIT THIS FILE ###
它仅仅是将build/core/main.mk⽂件加载进来。build/core/main.mk是Android编译系统的⼊⼝⽂件,它通过加载其它的mk⽂件来对Android源码中的各个模块进⾏编译,以及将编译出来的⽂件打包成各种镜像⽂件。以下就是build/core/main.mk⽂件的主要内容:
[plain]
1. ......
2.
3. # This is the default target. It must be the first declared target.
4. .PHONY: droid
5. DEFAULT_GOAL := droid
6. $(DEFAULT_GOAL):
7. ......
8.
9. # Set up various standard variables based on configuration
10. # and host information.
11. include $(BUILD_SYSTEM)/config.mk
12. ......
13.
14. # Bring in standard build system definitions.
15. include $(BUILD_SYSTEM)/definitions.mk
16. ......
17.
18. # These targets are going to delete stuff, don't bother including
19. # the whole directory tree if that's all we're going to do
20. ifeq ($(MAKECMDGOALS),clean)
21. dont_bother := true
22. endif
23. ifeq ($(MAKECMDGOALS),clobber)
24. dont_bother := true
25. endif
26. ifeq ($(MAKECMDGOALS),dataclean)
27. dont_bother := true
28. endif
29. ifeq ($(MAKECMDGOALS),installclean)
30. dont_bother := true
31. endif
32.
33. # Bring in all modules that need to be built.
34. ifneq ($(dont_bother),true)
35.
36. ......
37.
38. ifneq ($(ONE_SHOT_MAKEFILE),)
39. # We've probably been invoked by the "mm" shell function
40. # with a subdirectory's makefile.
41. include $(ONE_SHOT_MAKEFILE)
42. ......
43. else # ONE_SHOT_MAKEFILE
44.
45. #
46. # Include all of the makefiles in the system
47. #
48.
49. # Can't use first-makefiles-under here because
50. # --mindepth=2 makes the prunes not work.
51. subdir_makefiles := \
52. $(shell build/tools/findleaves.py --prune=out --prune=.repo --prune=.git $(subdirs) Android.mk)
git常用指令53.
54. include $(subdir_makefiles)
55.
56. endif # ONE_SHOT_MAKEFILE
57.
58. ......
59.
60. # -------------------------------------------------------------------
61. # Define dependencies for modules that require other modules.
62. # This can only happen now, after we've read in all module makefiles.
63. #
64. # TODO: deal with the fact that a bare module name isn't
65. # unambiguous enough. Maybe declare short targets like
66. # APPS:Quake or HOST:SHARED_LIBRARIES:libutils.
67. # BUG: the system image won't know to depend on modules that are
68. # brought in as requirements of other modules.
69. define add-required-deps
70. $(1): $(2)
71. endef
72. $(foreach m,$(ALL_MODULES), \
73. $(eval r := $(ALL_MODULES.$(m).REQUIRED)) \
74. $(if $(r), \
75. $(eval r := $(call module-installed-files,$(r))) \
76. $(eval $(call add-required-deps,$(ALL_MODULES.$(m).INSTALLED),$(r))) \
77. ) \
78. )
79. ......
80.
81. modules_to_install := $(sort \
82. $(ALL_DEFAULT_INSTALLED_MODULES) \
83. $(product_FILES) \
84. $(foreach tag,$(tags_to_install),$($(tag)_MODULES)) \
85. $(call get-tagged-modules, shell_$(TARGET_SHELL)) \
86. $(CUSTOM_MODULES) \
87. )
88. ......
89.
90. # build/core/Makefile contains extra stuff that we don't want to pollute this
91. # top-level makefile with. It expects that ALL_DEFAULT_INSTALLED_MODULES
92. # contains everything that's built during the current make, but it also further
93. # extends ALL_DEFAULT_INSTALLED_MODULES.
94. ALL_DEFAULT_INSTALLED_MODULES := $(modules_to_install)
95. include $(BUILD_SYSTEM)/Makefile
96. modules_to_install := $(sort $(ALL_DEFAULT_INSTALLED_MODULES))
97. ALL_DEFAULT_INSTALLED_MODULES :=
98.
99. endif # dont_bother
100.
101. ......
102.
103. # -------------------------------------------------------------------
104. # This is used to to get the ordering right, you can also use these,
105. # but they're considered undocumented, so don't complain if their
106. # behavior changes.
107. .PHONY: prebuilt
108. prebuilt: $(ALL_PREBUILT)
109. ......
110.
111. # All the droid stuff, in directories
112. .PHONY: files
113. files: prebuilt \
114. $(modules_to_install) \
115. $(modules_to_check) \
116. $(INSTALLED_ANDROID_INFO_TXT_TARGET)
117. ......
118.
119. # Build files and then package it into the rom formats
120. .PHONY: droidcore
121. droidcore: files \
122. systemimage \
123. $(INSTALLED_BOOTIMAGE_TARGET) \
124. $(INSTALLED_RECOVERYIMAGE_TARGET) \
125. $(INSTALLED_USERDATAIMAGE_TARGET) \
126. $(INSTALLED_CACHEIMAGE_TARGET) \
127. $(INSTALLED_FILES_FILE)
128.
129. ......
130.
131. # Dist for droid if droid is among the cmd goals, or no cmd goal is given.
132. ifneq ($(filter droid,$(MAKECMDGOALS))$(filter ||,|$(filter-out $(INTERNAL_MODIFIER_TARGETS),$(MAKECMDGOALS))|),) 133.
134. ifneq ($(TARGET_BUILD_APPS),)
135. # If this build is just for apps, only build apps and not the full system by default.
136. ......
137.
138. .PHONY: apps_only
139. apps_only: $(unbundled_build_modules)
140.
141. droid: apps_only
142.
143. else # TARGET_BUILD_APPS
144. ......
145.
146. # Building a full system-- the default is to build droidcore
147. droid: droidcore dist_files
148.
149. endif # TARGET_BUILD_APPS
150. endif # droid in $(MAKECMDGOALS)
151. ......
152.
153. # phony target that include any targets in $(ALL_MODULES)
154. .PHONY: all_modules
155. all_modules: $(ALL_MODULES)
156.
157. ......
接下来我们就先对build/core/main.mk⽂件的核⼼逻辑进⾏分析,然后再进⼀步对其中涉及到的关键点进⾏分析。
build/core/main.mk⽂件的执⾏过程如下所⽰:
1. 定义默认make⽬标为droid。⽬标droid根据不同的情形有不同的依赖关系。如果在初始化编译环境时,指定了TARGET_BUILD_APPS环境变量,那么就表⽰当前只编译特定的模块,这些特定的模块保存在变量unbundled_build_modules中,这时候⽬标droid就透过另外⼀个伪⽬标app_only依赖它们。如果在初始化编译环境时没有指定TARGET_BUILD_APPS环境变量,那么⽬标droid就依赖于另外两个⽂件droidcore和dist_files。droidcore是⼀个make伪⽬标,它依赖于各种预编译⽂件,以及syst
em.img、boot.img、recovery.img和userdata.img等镜像⽂件。dist_files也是⼀个make伪⽬标,⽤来指定⼀些需要在编译后拷贝到out/dist⽬录的⽂件。也就是说,当我们在Android源码⽬录中执⾏不带⽬标的make命令时,默认就会对⽬标droid进⾏编译,也就是会将整个Android系统编译出来。
2. 加载build/core/config.mk⽂件。从前⾯⼀⽂可以知道,在加载build/core/config.mk⽂件的过程中,会在执⾏make命令的进程中完成对Android编译环境的初始化过程,也就是会指定好⽬标设备以及编译类型。
3. 加载build/croe/definitions.mk⽂件。该⽂件定义了很多在编译过程中要⽤到的宏,相当于就是定义了很多通⽤函数,供编译过程调⽤。
4. 如果在执⾏make命令时,指定的不是清理⽂件相关的⽬标,也就是不是clean、clobber、dataclean和installclean等⽬标,那么就会将变量dont_bother的值设置为true,表⽰接下来要执⾏的是编译命令。
5. 在变量dont_bother的值等于true的情况下,如果环境变量ONE_SHOT_MAKEFILE的值不等于空,也就是我们执⾏的是mm或者mmm命令,那么就表⽰要编译的是特定的模块。这些指定要编译的模块的Android.mk⽂件路径就保存在环境变量ONE_SHOT_MAKEFILE中,因此直接将这些Android,mk⽂件加载进来就获得相应的编译规则。另⼀⽅⾯,如果环境变量ONE_SHOT_MAKEFILE的值等于空,
那么就说明我们执⾏的是m或者make命令,那么就表⽰要对Android源代码中的所有模块进⾏编译,这时候就通过build/tools/findleaves.py脚本获得Android源代码⼯程下的所有Android.mk⽂件的路径列表,并且将这些Android.mk⽂件加载进⾏获得相应的编译规则。
6. 上⼀步指定的Android.mk⽂件加载完成之后,变量ALL_MODULES就包含了所有要编译的模块的名称,这些模块名称以空格来分隔形成成⼀个列表。
7. ⽣成模块依赖规则。每⼀个模块都可以通过LOCAL_REQUIRED_MODULES来指定它所依赖的其它模块,也就是说当⼀个模块被安装时,它所依赖的其它模块也同样会被安装。每⼀个模块m依赖的所有模块都会被保存在ALL_MODULES.$(m).REQUIRED变量中。对于每⼀个被依赖模块r,我们需要获得它的安装⽂件,也就是最终⽣成的模块⽂件的⽂件路径,以便可以⽣成相应的编译规则。获得⼀个模块m的安装⽂件是通过调⽤函数module-installed-files来实现的,实质上就是保存在
$(ALL_MODULES.$(m).INSTALLED变量中。知道了⼀个模块m的所依赖的模块的安装⽂件路径之后,我们就可以通过函数add-required-deps来指定它们之间的依赖关系了。注意,这⾥实际上指定的是模块m的安装⽂件与它所依赖的模块r的安装⽂件的依赖关系。
8. 将所有要安装的模块都保存在变量ALL_DEFAULT_INSTALLED_MODULES中,并且将build/core/Makefie⽂件加载进来。 build/core/Makefie⽂件会根据要安装的模块产成system.img、boot.
img和recovery.img等镜像⽂件的⽣成规则。
9. 前⾯提到,当执⾏mm命令时,make⽬标指定为all_moudles。另外,当执⾏mmm命令时,默认的make⽬标也指定为all_moudles。因此,我们需要指定⽬标all_modules 的编译规则,实际上只要将它依赖于当前要编译的所有模块就⾏了,也就是依赖于由变量ALL_MODULES所描述的模块。
在上述过程中,最核⼼的就是第5步和第8步。由于本⽂只关⼼Android源码的编译过程,因此我们只分析第5步的执⾏过程。在接下来⼀篇⽂章中分析Android镜像⽂件的⽣成过程时,我们再分析第8步的执⾏过程。
第5步实际上就是将指定模块的Android.mk⽂件加载进来。⼀个典型的Android.mk⽂件如下所⽰:
[plain]
1. LOCAL_PATH:= $(call my-dir)
2. include $(CLEAR_VARS)
3.
4. LOCAL_MODULE_TAGS := optional
5. LOCAL_MODULE := libdis
6.
7. LOCAL_SHARED_LIBRARIES := \
8. liblog \
9. libdl
10.
11. LOCAL_SRC_FILES := \
12. dispatcher.cpp \
13. ../common/common.cpp
14.
15. include $(BUILD_SHARED_LIBRARY)
以LOCAL开头的变量都是属于模块局部变量,也就是说,⼀个模块在开始编译之前,必须要先对它们进⾏清理,然后再进⾏初始化。Android编译系统定义了⾮常多的模块局部变量,因此我们不可能⼿动地⼀个⼀个清理,需要加载⼀个由变量CLEAR_VARS指定的Makefile脚本来帮我们⾃动清理。变量CLEAR_VARS的值定义在
build/core/config.mk⽂件,它的值等于build/core/clear_vars.mk。
Android.mk⽂件中还有⼀个重要的变量LOCAL_PATH,⽤来指定当前正在编译的模块的⽬录,我们可以通过调⽤宏my-dir来获得。宏my-dir定义在build/core/definitions.mk ⽂件,它实际上就是将当前正在加载的Android.mk⽂件路径的⽬录名提取出来。
Android.mk⽂件接下来就是通过其它的LOCAL变量定义模块名称、源⽂件,以及所要依赖的各种库⽂件等等。例如,在我们这个例⼦,模块名称定义为libdis,参与编译的源⽂件为dispatcher.cpp和common.cpp、依赖的库⽂件为liblog和libdl。
最后,Android⽂件通过加载⼀个模板⽂件来告诉编译系统它所要编译的模块的类型。例如,在我们这个例⼦中,就是通过加载由变量BUILD_SHARED_LIBRARY指定的模板⽂件来告诉编译系统我们要编译的模块是⼀个动态链接库。变量BUILD_SHARED_LIBRARY的值定义在build/core/config.mk⽂件,它的值等于build/core/shared_library.mk。 Android编译系统定义了⾮常多的模板⽂件,每⼀个模板
⽂件都对应⼀种类型的模块,例如除了我们上⾯的动态链接库模板⽂件之外,还有:
BUILD_PACKAGE:指向build/core/package.mk,⽤来编译APK⽂件。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论