Android Apk 编译原理解析

本文基于AOSP-7.1.1-R9源码分析,源码可以参见build/+/android-7.1.1_r9

简介

在Android系统中,所有的应用都是以apk的形式存在,那这个apk是如何生成的呢?对于刚开始接触系统开发的开发者来说,经常会使用如下命令编译apk或者系统固件。

source build/envsetup.sh;
lunch
make -j8 
or
mmm packages/app/Settings
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这些命令背后的原理是什么呢?最终为什么会生成一个apk文件?接下来的文章将会对相关原理进行剖析。

本文使用AOSP frameworks/base/tests/Split/的源码来进行解析。由于关注点在apk的生成原理,所以重点分析和apk相关的编译原理,对于生成系统固件的原理不是本文讨论的重点,当了解了apk的生成原理之后,其他的编译原理也就好理解了。

apk编译系统

接下来我们将分析Android编译系统里面用来生成apk的原理。大致会分为如下几个步骤:

step1. 通过source 命令,读入envsetup.sh里面定义的各种命令,比如mm、mma、mmm、godir、croot等,方便我们在当前终端进行相关的命令输入。

step2.lunch将要编译的产品,生成产品相关的参数配置。本文不做深入讲解。

source build/envsetup.sh;
lunch
  • 1
  • 2

step3. 开始执行编译命令,生成目标的依赖关系。

mmm frameworks/base/tests/Split
  • 1

查看build/envsetup.sh的源码,可以看到,当我们执行如上命令的时候,实际上执行的是如下的命令:

    ONE_SHOT_MAKEFILE= frameworks/base/tests/Split/Android.mk  make -C /home/tanfuhai/data/code/opengrok/android_n -f build/core/main.mk  all_modules
  • 1

接下来就直接进入到Android编译系统,可以看到入口makefile文件是main.mk,下面对引用的makefile文件一一讲解:

  • build/core/main.mk 编译系统的入口。Android所有的生成目标都是从这个main.mk开始,Android默认的make targetdroiddroid依赖于droid_targets,Makefile里面定义了各种依赖,当执行make的时候,可以编译出我们需要的所有image,droid的依赖关系如下图:
    .PHONY: droid
    DEFAULT_GOAL := droid
    $(DEFAULT_GOAL): droid_targets

    .PHONY: droid_targets
    droid_targets:

    droid_targets: apps_only

    droid_targets: droidcore dist_files
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

本文主要讲解apk生成的原理,当我们执行mmm的时候,此时的target是all_modules。make的过程中,首先会读取所有的makefile文件。由于我们传入了ONE_SHOT_MAKEFILE= frameworks/base/tests/Split/Android.mk,所以此时会include这个ONE_SHOT_MAKEFILEall_modules的依赖定义如下:

# phony target that include any targets in $(ALL_MODULES)
.PHONY: all_modules
ifndef BUILD_MODULES_IN_PATHS
all_modules: $(ALL_MODULES)
else
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

引入 ONE_SHOT_MAKEFILE文件,代码如下:

    ifneq ($(ONE_SHOT_MAKEFILE),)
    # We've probably been invoked by the "mm" shell function
    # with a subdirectory's makefile.
    include $(ONE_SHOT_MAKEFILE)
  • 1
  • 2
  • 3
  • 4

由于Android.mk里面定义了include $(BUILD_PACKAGE),在引用BUILD_PACKAGE对应的makefile时,会依次引用到如下的makefile文件。

  • build/core/clear_vars.mk 把相关的宏给清理掉。
  • build/core/package.mk 对应include $(BUILD_PACKAGE),入口。
  • build/core/multilib.mk 判断Android.mk里面定义的LOCAL_32_BIT_ONLY或者LOCAL_MULTILIB的值,并且赋值给my_module_multilib,,方便下面的makefile使用。
  • build/core/module_arch_supported.mk 根据my_module_multilib的值来判断当前模块是否需要编译支持32和64位。
  • build/core/package_internal.mk 编译apk定义的规则。会收集我们在Android.mk里面定义的各种变量。比如当前的模块名、资源文件路径等。这个mk定义了很多生成apk的规则和依赖。

    • LOCAL_PACKAGE_OVERRIDES 指定覆盖其他的包,比如系统现在同时编译了Launcher1和Launcher2,我们只需要Launcher2,那么我们可以在Launcher2的Android.mk里面定义LOCAL_PACKAGE_OVERRIDES := Launcher1。
    • LOCAL_PACKAGE_NAME 最后生成的apk的名字.
    • LOCAL_MODULE_SUFFIX 模块的后缀,默认是apk
    • LOCAL_MODULE 模块名,和LOCAL_PACKAGE_NAME一样,只能定义一个,比如Split
    • LOCAL_MODULE_CLASS 模块类型,默认是APPS
    • LOCAL_ASSET_DIR asset目录,默认是assets
    • LOCAL_RESOURCE_DIR 资源文件目录,默认是res
    • PRODUCT_PACKAGE_OVERLAYS DEVICE_PACKAGE_OVERLAYS 资源覆盖目录,资源覆盖机制,DEVICE_PACKAGE_OVERLAYS指定的优先级低。
    • LOCAL_PROGUARD_ENABLED 代码混淆是不是生效。
    • LOCAL_CERTIFICATE 签名文件,产品自己用PRODUCT_DEFAULT_DEV_CERTIFICATE指定,如果也没有指定,默认build/target/product/security/testkey。比如我们在设置模块指定了LOCAL_CERTIFICATE := platform,那么最终使用的签名文件就是build/target/product/security/下面的platform.x509.pem和platform.pk8,同理如果LOCAL_CERTIFICATE := media,那么就用media.x509.pem和media.pk8。
    • LOCAL_PACKAGE_SPLITS 是否定义了apk分包。并且定义分包的相关规则。
    • LOCAL_BUILT_MODULE 定义编译apk的所有文件依赖关系。比如aapt编译生成package.apk、R.stamp、签名依赖,可以说编译apk的整个依赖完全是由这个宏定义生成。
  • build/core/configure_local_jack.mk 定义是否用jack编译,在Android O上,jack已经被弃用。如果定义了,定义jack编译的规则。

  • build/core/android_manifest.mk 主要做两件事情。

    • 获取LOCAL_MANIFEST_FILE宏的值,默认是AndroidManifest.xml文件,应用可以通过LOCAL_FULL_MANIFEST_FILE指定自己的AndroidManifest.xml文件。
    • 合并aar库文件里面的AndroidManifest.xml文件,LOCAL_STATIC_JAVA_AAR_LIBRARIES 指定引用的aar包,和jar相比,aar里面包含有相关的资源。
  • build/core/java.mk

    • 收集所有的java文件,包括.proto文件、RenderScript脚本资源、aidl文件、logtags文件以及编译过程中产生的文件。
    • 收集产生full_classes_jar的所有依赖。
    • 定义生成built_dex_intermediate的依赖关系。
  • build/core/base_rules.mk 主要作用:

    • 判断LOCAL_MODULELOCAL_MODULE_TAGSLOCAL_MODULE_CLASS是否唯一、判断当前模块是否有init.rc文件。
    • 定义ALL_MODULES LOCAL_BUILT_MODULE LOCAL_INSTALLED_MODULE的依赖。展开build/core/base_rules.mk之后,我们前面在main.mk里面的依赖就和对应的模块名Split联系起来了,相当于all_modules依赖于SplitSplit最终依赖于LOCAL_BUILT_MODULELOCAL_INSTALLED_MODULE。依赖规则如下:
    ALL_MODULES += $(my_register_name)
    ifdef OVERRIDE_BUILT_MODULE_PATH
    ifneq ($(LOCAL_MODULE_CLASS),SHARED_LIBRARIES)
        $(error $(LOCAL_PATH): Illegal use of OVERRIDE_BUILT_MODULE_PATH)
    endif
        built_module_path := $(OVERRIDE_BUILT_MODULE_PATH)
    else
        built_module_path := $(intermediates)
    endif
    LOCAL_BUILT_MODULE := $(built_module_path)/$(my_built_module_stem)
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • built_module_path: 最终为out/target/product/generic_x86_64/obj/APPS/Split_intermediates
    • my_built_module_stem: 默认是 package.apk
    • my_module_path: 等于out/target/product/generic_x86_64/data/app/Split
    • my_installed_module_stem: Split.apk
    • $(LOCAL_BUILT_MODULE): out/target/product/generic_x86_64/obj/APPS/Split_intermediates/package.apk
    • $(LOCAL_INSTALLED_MODULE): out/target/product/generic_x86_64/data/app/Split/Split.apk,
  • build/core/configure_module_stem.mk 获取编译模块的中间件名字。

    • my_module_stem ,如果没有定义LOCAL_MODULE_STEM,那么就是模块名,比如Split
    • my_built_module_stem 对于apk来说通常都是package.apk。
    • my_installed_module_stem 最终生成的apk名字,比如 Split.apk
  • build/core/notice_files.mk 编译版权声明相关的文件的规则。
  • build/core/java_common.mk

    • 判断java版本号。
    • 把.proto文件编译生成java文件。
    • 查找LOCAL_JAVA_RESOURCE_DIRS指定的资源目录下的文件,收集LOCAL_JAVA_RESOURCE_FILES指定的资源文件。
    • 指定java的私有变量。如$(LOCAL_INTERMEDIATE_TARGETS): PRIVATE_ASSET_DIR
    • 如果jack编译可用,指定一些变量。如PRIVATE_STATIC_JACK_LIBRARIES等。
  • build/core/dex_preopt_odex_install.mk 定义安装odex的一些规则。比如是否要进行odex优化,一般产品会自己定义WITH_DEXPREOPT = true

  • build/core/install_jni_libs.mk 定义如何安装apk需要的jn库。

  • build/core/install_jni_libs_internal.mk 安装JNI库。

编译依赖规则

经过以上的步骤,生成目标Split的依赖就清晰了,如下图所示: 
这里写图片描述

查看大图

我们可以看到,有如下规则:

  • rule10 aapt工具把所有的资源文件和AndroidManifes.xml文件一起编译生成out/target/common/obj/APPS/Split_intermediates/src/R.stamp
  • rule12 jack工具会把当前的java文件,以及依赖的相关系统库文件,比如out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.dex.toc一起编译生成out/target/common/obj/APPS/Split_intermediates/with-local/classes.dex

  • rule9 zip命令打包out/target/common/obj/APPS/Split_intermediates/with-local/classes.dexaapt生成的资源文件out/target/product/generic_x86_64/obj/APPS/Split_intermediates/package.apk生成新的out/target/product/generic_x86_64/obj/APPS/Split_intermediates/package.apk,然后通过签名工具进行签名。

  • rule8 acp命令把out/target/product/generic_x86_64/obj/APPS/Split_intermediates/package.apk拷贝到out/target/product/generic_x86_64/data/app/Split/Split.apk
  • rule15 17 19 21 生成分包 Split_hdpi-v4.apk、Split_mdpi-v4.apk 等。

至此apk就生成出来了。附rule规则对应的代码:

rule12 规则

    $(built_dex_intermediate): $(jack_all_deps) | setup-jack-server
    @echo Building with Jack: $@
    $(jack-java-to-dex)
  • 1
  • 2
  • 3

rule8 规则

@build/core/package_internal.apk

    @echo "target Package: $(PRIVATE_MODULE) ($@)"
ifdef LOCAL_USE_AAPT2
ifdef LOCAL_JACK_ENABLED
    $(call copy-file-to-new-target)
else
    @# TODO: implement merge-two-packages.
    $(if $(PRIVATE_SOURCE_ARCHIVE),\
      $(call merge-two-packages,$(PRIVATE_RES_PACKAGE) $(PRIVATE_SOURCE_ARCHIVE),$@),
      $(call copy-file-to-new-target))
endif
else  # LOCAL_USE_AAPT2
ifdef LOCAL_JACK_ENABLED
    $(create-empty-package)
else
    $(if $(PRIVATE_SOURCE_ARCHIVE),\
      $(call initialize-package-file,$(PRIVATE_SOURCE_ARCHIVE),$@),\
      $(create-empty-package))
endif
    $(add-assets-to-package)
endif  # LOCAL_USE_AAPT2
ifneq ($(jni_shared_libraries),)
    $(add-jni-shared-libs-to-package)
endif
ifeq ($(full_classes_jar),)
# We don't build jar, need to add the Java resources here.
    $(if $(PRIVATE_EXTRA_JAR_ARGS),$(call add-java-resources-to,$@))
else  # full_classes_jar
    $(add-dex-to-package)
endif  # full_classes_jar
ifdef LOCAL_JACK_ENABLED
    $(add-carried-jack-resources)
endif
ifdef LOCAL_DEX_PREOPT
ifneq ($(BUILD_PLATFORM_ZIP),)
    @# Keep a copy of apk with classes.dex unstripped
    $(hide) cp -f $@ $(dir $@)package.dex.apk
endif  # BUILD_PLATFORM_ZIP
ifneq (nostripping,$(LOCAL_DEX_PREOPT))
    $(call dexpreopt-remove-classes.dex,$@)
endif
endif
    $(sign-package)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

rule15 17 19 21 规则

@build/core/package_internal.apk

## APK splits
ifdef LOCAL_PACKAGE_SPLITS
# LOCAL_PACKAGE_SPLITS is a list of resource labels.
# aapt will convert comma inside resource lable to underscore in the file names.
my_split_suffixes := $(subst $(comma),_,$(LOCAL_PACKAGE_SPLITS))
built_apk_splits := $(foreach s,$(my_split_suffixes),$(built_module_path)/package_$(s).apk)
installed_apk_splits := $(foreach s,$(my_split_suffixes),$(my_module_path)/$(LOCAL_MODULE)_$(s).apk)

# The splits should have been built in the same command building the base apk.
# This rule just runs signing.
# Note that we explicily check the existence of the split apk and remove the
# built base apk if the split apk isn't there.
# That way the build system will rerun the aapt after the user changes the splitting parameters.
$(built_apk_splits): PRIVATE_PRIVATE_KEY := $(private_key)
$(built_apk_splits): PRIVATE_CERTIFICATE := $(certificate)
$(built_apk_splits) : $(built_module_path)/%.apk : $(LOCAL_BUILT_MODULE)
    $(hide) if [ ! -f $@ ]; then \
      echo 'No $@ generated, check your apk splitting parameters.' 1>&2; \
      rm $<; exit 1; \
    fi
    $(sign-package)

# Rules to install the splits
$(installed_apk_splits) : $(my_module_path)/$(LOCAL_MODULE)_%.apk : $(built_module_path)/package_%.apk | $(ACP)
    @echo "Install: $@"
    $(copy-file-to-new-target)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

总结

当我们编译frameworks/base/tests/Split/的时候,最终会生成out/target/product/generic_x86_64/data/app/Split/Split.apk,从文件的角度来看,有如下的解析图。

查看大图

查看大图

  1. aapt工具把所有的资源文件编译生成R.stamp,和R.java内容一致,主要用来当做R.java的编译目标。
  2. jack工具把所有的java资源文件以及依赖库编译生成classes.dex文件。
  3. aapt工具将生成的所有文件进行打包,生成package.apk和分包文件,比如package_hdpi-v4.apk,然后通过签名工具进行签名。
  4. 把apk从中间目录拷贝到我们最终需要生成的目录。并且更换名称为Split.apk

apk的编译原理就分析完成了,后面会写一篇文章讲解aapt是如何生成R.java和package.apk。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值