一、A/B升级之系统image的生成

一、A/B升级之系统image的生成

    本篇将对AB升级打开宏开关后make 和 makeotapackage的流程做分析,下面这张图是之前文档中所提到的按照对应文件打开宏开关,即可开启AB升级,但是代码里面针对该宏控也有对应代码处理,本次先分析device 和 build下的修改

一、device主要修改

最初加入了MTK_AB_OTA_UPDATER = yes

去除cache分区编译的控制开关

ifneq ($(strip $(MTK_AB_OTA_UPDATER)), yes)

BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ext4

endif

diff --git a/mediateksample/k39tv1_64_bsp/BoardConfig.mk b/mediateksample/k39tv1_64_bsp/BoardConfig.mk
index 13eb004..fd32381 100644
--- a/mediateksample/k39tv1_64_bsp/BoardConfig.mk
+++ b/mediateksample/k39tv1_64_bsp/BoardConfig.mk
@@ -6,10 +6,11 @@ include device/mediatek/mt6739/BoardConfig.mk
 
 #Config partition size
 -include $(MTK_PTGEN_OUT)/partition_size.mk
+ifneq ($(strip $(MTK_AB_OTA_UPDATER)), yes)
 BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ext4
+endif
 BOARD_FLASH_BLOCK_SIZE := 4096
 
-
 MTK_INTERNAL_CDEFS := $(foreach t,$(AUTO_ADD_GLOBAL_DEFINE_BY_NAME),$(if $(filter-out no NO none NONE false FALSE,$($(t))),-D$(t)))
 MTK_INTERNAL_CDEFS += $(foreach t,$(AUTO_ADD_GLOBAL_DEFINE_BY_VALUE),$(if $(filter-out no NO none NONE false FALSE,$($(t))),$(foreach v,$(shell echo $($(t)) | tr '[a-z]' '[A-Z]'),-D$(v))))
 MTK_INTERNAL_CDEFS += $(foreach t,$(AUTO_ADD_GLOBAL_DEFINE_BY_NAME_VALUE),$(if $(filter-out no NO none NONE false FALSE,$($(t))),-D$(t)=\"$(strip $($(t)))\"))
diff --git a/mediateksample/k39tv1_64_bsp/ProjectConfig.mk b/mediateksample/k39tv1_64_bsp/ProjectConfig.mk
index 99aebd3..3419c65 100755
--- a/mediateksample/k39tv1_64_bsp/ProjectConfig.mk
+++ b/mediateksample/k39tv1_64_bsp/ProjectConfig.mk
@@ -685,7 +685,8 @@ TRUSTONIC_TEE_SUPPORT = no
 USE_FRAUNHOFER_AAC = no
 USE_XML_AUDIO_POLICY_CONF = 1
 WIFI_WEP_KEY_ID_SET = no
-MTK_AB_OTA_UPDATER = no
+CONFIG_MTK_AB_OTA_UPDATER = yes
+MTK_AB_OTA_UPDATER = yes

搜索宏控相关文件如下

libiaobiao:~/code/s219_ab/device$ grep -rni "MTK_AB_OTA_UPDATER"

mediateksample/k39tv1_64_bsp/BoardConfig.mk:9:ifneq ($(strip $(MTK_AB_OTA_UPDATER)), yes)
mediateksample/k39tv1_64_bsp/ProjectConfig.mk:688:CONFIG_MTK_AB_OTA_UPDATER = yes
mediateksample/k39tv1_64_bsp/ProjectConfig.mk:689:MTK_AB_OTA_UPDATER = yes

mediatek/common/device.mk:3643:ifeq ($(strip $(MTK_AB_OTA_UPDATER)), yes)
mediatek/common/BoardConfig.mk:228:  ifeq ($(strip $(MTK_AB_OTA_UPDATER)), yes)
mediatek/mt6739/BoardConfig.mk:132:ifneq ($(strip $(MTK_AB_OTA_UPDATER)),yes)

mediatek/build/build/tools/ptgen/MT6739/ptgen.mk:71:  MTK_AB_OTA_UPDATER=${MTK_AB_OTA_UPDATER} \
mediatek/build/build/tools/ptgen/MT6739/ptgen.pl:195:    $ArgList{MTK_AB_OTA_UPDATER}         = $ENV{MTK_AB_OTA_UPDATER};
mediatek/build/build/tools/ptgen/MT6739/ptgen.pl:260:            if ($ArgList{MTK_AB_OTA_UPDATER} eq "yes")
mediatek/build/build/tools/ptgen/MT6739/ptgen.pl:271:            if ($ArgList{MTK_AB_OTA_UPDATER} eq "yes")
mediatek/build/build/tools/ptgen/MT6739/ptgen.pl:768:            if ($ArgList{MTK_AB_OTA_UPDATER} eq "yes")
mediatek/build/build/tools/partition/gen-partition.py:123:  if os.getenv("MTK_AB_OTA_UPDATER") == "yes":

1、mediatek/common/device.mk

# A/B System updates
ifeq ($(strip $(MTK_AB_OTA_UPDATER)), yes)

# Squashfs config system文件格式使用squashfs,这是与ext4不同的格式,目前没用到
#BOARD_SYSTEMIMAGE_FILE_SYSTEM_TYPE := squashfs
#PRODUCT_PACKAGES += mksquashfs
#PRODUCT_PACKAGES += mksquashfsimage.sh
#PRODUCT_PACKAGES += libsquashfs_utils
#将recovery ramdisk放到boot.img文件内
BOARD_USES_RECOVERY_AS_BOOT := true
#不再编译recovery.img
TARGET_NO_RECOVERY := true
#打开AB_OTA_UPDATER宏,这个本身是google原生的打开AB的主控开
AB_OTA_UPDATER := true

 # A/B OTA partitions AB升级中可升级的分区
AB_OTA_PARTITIONS := \
boot \
system \
lk \
preloader
#编译update_engine update_verifier brillo_update_payload模块
PRODUCT_PACKAGES += \
update_engine \
shflags \
delta_generator \
bsdiff \
brillo_update_payload \
update_engine_sideload \
update_verifier \
#这两个时debug的调试工具update_engine_client 可以在adblog中输出升级流程 
#bootctl是boot_control模块调用的工具,可以通过命令调用接口
PRODUCT_PACKAGES_DEBUG += \
update_engine_client \
bootctl

# bootctrl HAL and HIDL  编译启动相关的bootctrl hal层
PRODUCT_PACKAGES += \
        bootctrl.$(MTK_PLATFORM_DIR) \
        android.hardware.boot@1.0-impl \
        android.hardware.boot@1.0-service

PRODUCT_STATIC_BOOT_CONTROL_HAL := bootctrl.$(MTK_PLATFORM_DIR)

# A/B OTA dexopt package
PRODUCT_PACKAGES += otapreopt_script

# Install odex files into the other system image
#编译了system_other 并将odex文件放入
BOARD_USES_SYSTEM_OTHER_ODEX := true

# A/B OTA dexopt update_engine hookup
AB_OTA_POSTINSTALL_CONFIG += \
    RUN_POSTINSTALL_system=true \
    POSTINSTALL_PATH_system=system/bin/otapreopt_script \
    FILESYSTEM_TYPE_system=ext4 \
    POSTINSTALL_OPTIONAL_system=true

# Tell the system to enable copying odexes from other partition.
PRODUCT_PACKAGES += \
        cppreopts.sh

PRODUCT_PROPERTY_OVERRIDES += \
    ro.cp_system_other_odex=1

DEVICE_MANIFEST_FILE += device/mediatek/common/project_manifest/manifest_boot.xml
endif
#下面一个单独的判断,定义是否将rootfs放入system,
ifneq ($(strip $(SYSTEM_AS_ROOT)), no)
BOARD_BUILD_SYSTEM_ROOT_IMAGE := true
endif

2、mediatek/common/BoardConfig.mk

这部分的修改与odex化的编译有关,以下时相关的资料

开odex优化首次开机速度,是牺牲空间换取时间的做法,仅限于空间足够的设备。开了odex之后,在编译的时候,整个system image就会被预先优化。由于在启动时不再需要进行app的dex文件进行优化(dex2oat操作)从而提升其启动速度。 

关于odex,有几个下面几个宏开关:

1、WITH_DEXPREOPT

这个开关在6.0 USER版本上是默认开启的,意思就是USER版本要开odex预编译。会导致system image中的所有东西都被提前优化(pre-optimized)。这可能导致system image非常大。

那么问题就来了,既然 WITH_DEXPREOPT := true 默认开启,那么为什么首次启动依然耗时很长呢?这个就和第二个宏开关——DONT_DEXPREOPT_PREBUILTS有关了。

2、DONT_DEXPREOPT_PREBUILTS

如果我们不想把prebuilts目录中的第三方应用进行预先优化(这些应用在他们的Android.mk文件中有include$(BUILD_PREBUILT) ),而是希望这些app通过playstore 或者app提供商进行升级,那么我们可以打开这个宏开关。

事实上,6.0上面,这个宏开关也是默认开启的。我们全局搜索一下“(BUILD_PREBUILT) ”会发现很多结果,这也就是为什么默认odex都开了,为什么开机并没有觉得快的原因了。


因此我们在做odex优化的时候,都会关闭DONT_DEXPREOPT_PREBUILTS,然后重新给我们预置的App添加 LOCAL_DEX_PREOPT :=false 让它们不进行预编译,这样也就能节省一些不必要的空间消耗。同时因为关闭了DONT_DEXPREOPT_PREBUILTS,很多可以随ROM升级的系统App也就进行了预编译,因此开机速度就有了明显的提高。

开AB后DONT_DEXPREOPT_PREBUILTS  :=false  看到这里其实看不出什么,后面分析Makefile可以看出具体生成,不过暂时根据out下生成的system 和 system_other  system下面是单独的APK 而system_other是单独的odex和vdex,

ifeq ($(BUILD_GMS),yes)
  ifeq ($(strip $(MTK_AB_OTA_UPDATER)), yes)
    DONT_DEXPREOPT_PREBUILTS := false
  else
    DONT_DEXPREOPT_PREBUILTS := true
  endif
else
  ifeq ($(TARGET_BUILD_VARIANT),userdebug)
    DEX_PREOPT_DEFAULT := nostripping
  endif
endif

3、mediatek/mt6739/BoardConfig.mk  

以下是谷歌原生文档关于该宏的介绍,如果打开AB升级,将会关闭该宏

在非 A/B 设备的恢复映像中添加 DTBO

为防止非 A/B 设备上出现 OTA 失败的情况,恢复分区必须“自给自足”,不得依赖于其他分区。

启动到恢复模式时,引导加载程序必须加载与恢复映像兼容的 DTBO 映像。在执行 OTA 期间,如果在 DTBO 映像更新后(但在完成全部更新之前)出现问题,设备将尝试启动到恢复模式,以完成 OTA。不过,由于 DTBO 分区已更新,恢复映像(尚未更新)可能会出现不匹配的情况。

为防止出现这种情况,在 Android 9 中,恢复映像也必须包含来自 DTBO 映像的信息。非 A/B 设备的恢复映像还必须包含附加到内核的设备 DTB,以便在更新期间不依赖于 DTB 分区。

实现

虽然搭载 Android 9 的所有设备都必须使用新的启动映像标头(版本 1),但只有非 A/B 设备才必须填充恢复映像的 recovery_dtbo 部分。要在 BoardConfig.mk 设备的 recovery.img 中添加 recovery_dtbo,请执行以下操作:

  • BOARD_INCLUDE_RECOVERY_DTBO 配置设置为 true

BOARD_INCLUDE_RECOVERY_DTBO := true

  • 扩展 BOARD_MKBOOTIMG_ARGS 变量以指定启动映像标头版本:

BOARD_MKBOOTIMG_ARGS := --ramdisk_offset $(BOARD_RAMDISK_OFFSET) --tags_offset $(BOARD_KERNEL_TAGS_OFFSET) --header_version $(BOARD_BOOTIMG_HEADER_VERSION)

  • 确保将 BOARD_PREBUILT_DTBOIMAGE 变量设置为 DTBO 映像的路径。Android 编译系统会使用该变量在创建恢复映像时设置 mkbootimg 工具的 recovery_dtbo 参数。
  • 如果变量 BOARD_INCLUDE_RECOVERY_DTBOBOARD_MKBOOTIMG_ARGSBOARD_PREBUILT_DTBOIMAGE 均正确设置,Android 编译系统会将变量 BOARD_PREBUILT_DTBOIMAGE 指定的 DTBO 添加到 recovery.img 中。
ifeq ($(strip $(MTK_DTBO_FEATURE)),yes)
ifeq ($(strip $(MTK_DTBO_UPGRADE_FROM_ANDROID_O)), yes)
BOARD_PREBUILT_DTBOIMAGE := $(MTK_PTGEN_PRODUCT_OUT)/obj/PACKAGING/dtboimage/odmdtbo.img
else
BOARD_PREBUILT_DTBOIMAGE := $(MTK_PTGEN_PRODUCT_OUT)/obj/PACKAGING/dtboimage/dtbo.img
endif
ifneq ($(strip $(MTK_AB_OTA_UPDATER)),yes)
BOARD_INCLUDE_RECOVERY_DTBO := true
endif
endif

mediatek/build/build/tools/ptgen/MT6739/ptgen.mk

4、mediatek/build/build/tools/ptgen/MT6739/ptgen.pl

MTK分区表存放位置:device/mediatek/build/build/tools/ptgen/xxx/xxx.xls  

ptgen.pl文件会把xls文件解析成xxxAndroid_scatter.txt放在out/target/product/xxx/中

mtk的flashtool工具会读取这个文件把相关的镜像烧写到rom中

这里是根据判断选择对应分区文件,如果开了MTK_AB_OTA_UPDATER,选择我们emmc_ab的分区表,否则选择emmc的

        $Partition_layout_xls = "$ptgen_location/partition_table_$ArgList{PLATFORM}";
        if ($ArgList{EMMC_SUPPORT} eq "yes")
        {
            if ($ArgList{MTK_AB_OTA_UPDATER} eq "yes")
            {
                $ArgList{SHEET_NAME} = "emmc_ab";
            }
            else
            {
                $ArgList{SHEET_NAME} = "emmc";
            }
        }
        elsif ($ArgList{UFS_BOOTING} eq "yes")
        {
            if ($ArgList{MTK_AB_OTA_UPDATER} eq "yes")
            {
                $ArgList{SHEET_NAME} = "ufs_ab";
            }
            else
            {
                $ArgList{SHEET_NAME} = "ufs";
            }
        }
        else
        {
            $ArgList{SHEET_NAME} = "nand";
        }
    }

二、images生成流程

device修改很多开关主要作用时更改image的生成,而这部分全部是在makefile中,这是我们主要分析的文件,继续开车

1、recovery.img

ifeq (,$(filter true, $(TARGET_NO_KERNEL) $(TARGET_NO_RECOVERY)))
INSTALLED_RECOVERYIMAGE_TARGET := $(PRODUCT_OUT)/recovery.img
else
INSTALLED_RECOVERYIMAGE_TARGET :=
endif

 

device.mk中定义了TARGET_NO_RECOVERY := true 由于TARGET_NO_KERNEL :=false ,所以条件不成立,

INSTALLED_RECOVERYIMAGE_TARGET :=,也就是不再生成recovery.img

 

2、boot.img

 

INSTALLED_BOOTIMAGE_TARGET := $(PRODUCT_OUT)/boot.img
MTK_BOOTIMAGE_TARGET := $(PRODUCT_OUT)/boot.img
INSTALLED_BOOTIMAGE_TARGET := $(call intermediates-dir-for,PACKAGING,boot)/boot.img
#这里判断条件较多,不过android对那个if 对应那个endif 还进行了标注,方便我们查看代码
#TARGET_NO_KERNEL 如果是true 条件不成立,这里我加了打印,其实它的值是
fasle,进入if条件
ifneq ($(strip $(TARGET_NO_KERNEL)),true)
$(warning "TARGET_NO_KERNEL is not supported anymore")
# -----------------------------------------------------------------
# the boot image, which is a collection of other images.
INTERNAL_BOOTIMAGE_ARGS := \
  $(addprefix --second ,$(INSTALLED_2NDBOOTLOADER_TARGET)) \
  --kernel $(INSTALLED_KERNEL_TARGET)

ifneq ($(BOARD_BUILD_SYSTEM_ROOT_IMAGE),true)
INTERNAL_BOOTIMAGE_ARGS += --ramdisk $(INSTALLED_RAMDISK_TARGET)
endif

INTERNAL_BOOTIMAGE_FILES := $(filter-out --%,$(INTERNAL_BOOTIMAGE_ARGS))

ifdef BOARD_KERNEL_BASE
  INTERNAL_BOOTIMAGE_ARGS += --base $(BOARD_KERNEL_BASE)
endif

ifdef BOARD_KERNEL_PAGESIZE
  INTERNAL_BOOTIMAGE_ARGS += --pagesize $(BOARD_KERNEL_PAGESIZE)
endif

ifeq ($(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_SUPPORTS_VERITY),true)
ifeq ($(BOARD_BUILD_SYSTEM_ROOT_IMAGE),true)
VERITY_KEYID := veritykeyid=id:`openssl x509 -in $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_VERITY_SIGNING_KEY).x509.pem -text \
                | grep keyid | sed 's/://g' | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]' | sed 's/keyid//g'`
endif
endif

INTERNAL_KERNEL_CMDLINE := $(strip $(BOARD_KERNEL_CMDLINE) buildvariant=$(TARGET_BUILD_VARIANT) $(VERITY_KEYID))
ifdef INTERNAL_KERNEL_CMDLINE
INTERNAL_BOOTIMAGE_ARGS += --cmdline "$(INTERNAL_KERNEL_CMDLINE)"
endif

INTERNAL_MKBOOTIMG_VERSION_ARGS := \
    --os_version $(PLATFORM_VERSION) \
    --os_patch_level $(PLATFORM_SECURITY_PATCH)

# BOARD_USES_RECOVERY_AS_BOOT = true must have BOARD_BUILD_SYSTEM_ROOT_IMAGE = true.
#这里的意思是说BOARD_USES_RECOVERY_AS_BOOT和BOARD_BUILD_SYSTEM_ROOT_IMAGE必须同时定义
ifeq ($(BOARD_USES_RECOVERY_AS_BOOT),true)
ifneq ($(BOARD_BUILD_SYSTEM_ROOT_IMAGE),true)
  $(error BOARD_BUILD_SYSTEM_ROOT_IMAGE must be enabled for BOARD_USES_RECOVERY_AS_BOOT.)
endif
endif

# We build recovery as boot image if BOARD_USES_RECOVERY_AS_BOOT is true.
#如果BOARD_USES_RECOVERY_AS_BOOT为true,不走下面所有的代码,所以开启这个宏后,普通生成boot.img
#的代码将全部失效
ifneq ($(BOARD_USES_RECOVERY_AS_BOOT),true)
ifeq ($(TARGET_BOOTIMAGE_USE_EXT2),true)
$(error TARGET_BOOTIMAGE_USE_EXT2 is not supported anymore)

else ifeq (true,$(BOARD_AVB_ENABLE)) # TARGET_BOOTIMAGE_USE_EXT2 != true

$(INSTALLED_BOOTIMAGE_TARGET): $(MKBOOTIMG) $(AVBTOOL) $(INTERNAL_BOOTIMAGE_FILES) $(BOARD_AVB_BOOT_KEY_PATH)
  $(call pretty,"Target boot image: $@")
  $(hide) $(MKBOOTIMG) $(INTERNAL_BOOTIMAGE_ARGS) $(INTERNAL_MKBOOTIMG_VERSION_ARGS) $(BOARD_MKBOOTIMG_ARGS) --output $@
  $(hide) $(call assert-max-image-size,$@,$(call get-hash-image-max-size,$(BOARD_BOOTIMAGE_PARTITION_SIZE)))
  $(hide) $(AVBTOOL) add_hash_footer \
    --image $@ \
    --partition_size $(BOARD_BOOTIMAGE_PARTITION_SIZE) \
    --partition_name boot $(INTERNAL_AVB_BOOT_SIGNING_ARGS) \
    $(BOARD_AVB_BOOT_ADD_HASH_FOOTER_ARGS)

.PHONY: bootimage-nodeps
bootimage-nodeps: $(MKBOOTIMG) $(AVBTOOL) $(BOARD_AVB_BOOT_KEY_PATH)
  @echo "make $@: ignoring dependencies"
  $(hide) $(MKBOOTIMG) $(INTERNAL_BOOTIMAGE_ARGS) $(INTERNAL_MKBOOTIMG_VERSION_ARGS) $(BOARD_MKBOOTIMG_ARGS) --output $(INSTALLED_BOOTIMAGE_TARGET)
  $(hide) $(call assert-max-image-size,$(INSTALLED_BOOTIMAGE_TARGET),$(call get-hash-image-max-size,$(BOARD_BOOTIMAGE_PARTITION_SIZE)))
  $(hide) $(AVBTOOL) add_hash_footer \
    --image $(INSTALLED_BOOTIMAGE_TARGET) \
    --partition_size $(BOARD_BOOTIMAGE_PARTITION_SIZE) \
    --partition_name boot $(INTERNAL_AVB_BOOT_SIGNING_ARGS) \
    $(BOARD_AVB_BOOT_ADD_HASH_FOOTER_ARGS)

else ifeq (true,$(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_SUPPORTS_BOOT_SIGNER)) # BOARD_AVB_ENABLE != true

$(INSTALLED_BOOTIMAGE_TARGET): $(MKBOOTIMG) $(INTERNAL_BOOTIMAGE_FILES) $(BOOT_SIGNER)
  $(call pretty,"Target boot image: $@")
  $(hide) $(MKBOOTIMG) $(INTERNAL_BOOTIMAGE_ARGS) $(INTERNAL_MKBOOTIMG_VERSION_ARGS) $(BOARD_MKBOOTIMG_ARGS) --output $@
  $(BOOT_SIGNER) /boot $@ $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_VERITY_SIGNING_KEY).pk8 $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_VERITY_SIGNING_KEY).x509.pem $@
  $(hide) $(call assert-max-image-size,$@,$(BOARD_BOOTIMAGE_PARTITION_SIZE))

.PHONY: bootimage-nodeps
bootimage-nodeps: $(MKBOOTIMG) $(BOOT_SIGNER)
  @echo "make $@: ignoring dependencies"
  $(hide) $(MKBOOTIMG) $(INTERNAL_BOOTIMAGE_ARGS) $(INTERNAL_MKBOOTIMG_VERSION_ARGS) $(BOARD_MKBOOTIMG_ARGS) --output $(INSTALLED_BOOTIMAGE_TARGET)
  $(BOOT_SIGNER) /boot $(INSTALLED_BOOTIMAGE_TARGET) $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_VERITY_SIGNING_KEY).pk8 $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_VERITY_SIGNING_KEY).x509.pem $(INSTALLED_BOOTIMAGE_TARGET)
  $(hide) $(call assert-max-image-size,$(INSTALLED_BOOTIMAGE_TARGET),$(BOARD_BOOTIMAGE_PARTITION_SIZE))

else ifeq (true,$(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_SUPPORTS_VBOOT)) # PRODUCT_SUPPORTS_BOOT_SIGNER != true

$(INSTALLED_BOOTIMAGE_TARGET): $(MKBOOTIMG) $(INTERNAL_BOOTIMAGE_FILES) $(VBOOT_SIGNER) $(FUTILITY)
  $(call pretty,"Target boot image: $@")
  $(hide) $(MKBOOTIMG) $(INTERNAL_BOOTIMAGE_ARGS) $(INTERNAL_MKBOOTIMG_VERSION_ARGS) $(BOARD_MKBOOTIMG_ARGS) --output $@.unsigned
  $(VBOOT_SIGNER) $(FUTILITY) $@.unsigned $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_VBOOT_SIGNING_KEY).vbpubk $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_VBOOT_SIGNING_KEY).vbprivk $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_VBOOT_SIGNING_SUBKEY).vbprivk $@.keyblock $@
  $(hide) $(call assert-max-image-size,$@,$(BOARD_BOOTIMAGE_PARTITION_SIZE))

.PHONY: bootimage-nodeps
bootimage-nodeps: $(MKBOOTIMG) $(VBOOT_SIGNER) $(FUTILITY)
  @echo "make $@: ignoring dependencies"
  $(hide) $(MKBOOTIMG) $(INTERNAL_BOOTIMAGE_ARGS) $(INTERNAL_MKBOOTIMG_VERSION_ARGS) $(BOARD_MKBOOTIMG_ARGS) --output $(INSTALLED_BOOTIMAGE_TARGET).unsigned
  $(VBOOT_SIGNER) $(FUTILITY) $(INSTALLED_BOOTIMAGE_TARGET).unsigned $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_VBOOT_SIGNING_KEY).vbpubk $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_VBOOT_SIGNING_KEY).vbprivk $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_VBOOT_SIGNING_SUBKEY).vbprivk $(INSTALLED_BOOTIMAGE_TARGET).keyblock $(INSTALLED_BOOTIMAGE_TARGET)
  $(hide) $(call assert-max-image-size,$(INSTALLED_BOOTIMAGE_TARGET),$(BOARD_BOOTIMAGE_PARTITION_SIZE))

else # PRODUCT_SUPPORTS_VBOOT != true

$(INSTALLED_BOOTIMAGE_TARGET): $(MKBOOTIMG) $(INTERNAL_BOOTIMAGE_FILES)
  $(call pretty,"Target boot image: $@")
  $(hide) $(MKBOOTIMG) $(INTERNAL_BOOTIMAGE_ARGS) $(INTERNAL_MKBOOTIMG_VERSION_ARGS) $(BOARD_MKBOOTIMG_ARGS) --output $@
  $(hide) $(call assert-max-image-size,$@,$(BOARD_BOOTIMAGE_PARTITION_SIZE))

.PHONY: bootimage-nodeps
bootimage-nodeps: $(MKBOOTIMG)
  @echo "make $@: ignoring dependencies"
  $(hide) $(MKBOOTIMG) $(INTERNAL_BOOTIMAGE_ARGS) $(INTERNAL_MKBOOTIMG_VERSION_ARGS) $(BOARD_MKBOOTIMG_ARGS) --output $(INSTALLED_BOOTIMAGE_TARGET)
  $(hide) $(call assert-max-image-size,$(INSTALLED_BOOTIMAGE_TARGET),$(BOARD_BOOTIMAGE_PARTITION_SIZE))
#这里是对每个判断条件的结尾进行了注释
endif # TARGET_BOOTIMAGE_USE_EXT2
endif # BOARD_USES_RECOVERY_AS_BOOT

else  # TARGET_NO_KERNEL

根据代码的判断,因为BOARD_USES_RECOVERY_AS_BOOT 为true 所以不走普通的生成方式,那么真正生成boot.img是在哪里呢,字面翻译这个宏的意思时说我们要用recovery作为现在的boot,代码如下

 

#条件成立,进入ifeq
ifeq ($(BOARD_USES_RECOVERY_AS_BOOT),true)
#添加依赖BOOT_SIGNER
ifeq (true,$(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_SUPPORTS_BOOT_SIGNER))
$(INSTALLED_BOOTIMAGE_TARGET) : $(BOOT_SIGNER)
endif
#添加依赖VBOOT_SIGNER
ifeq (true,$(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_SUPPORTS_VBOOT))
$(INSTALLED_BOOTIMAGE_TARGET) : $(VBOOT_SIGNER)
endif
#添加依赖AVBTOOL BOARD_AVB_BOOT_KEY_PATH
ifeq (true,$(BOARD_AVB_ENABLE))
$(INSTALLED_BOOTIMAGE_TARGET) : $(AVBTOOL) $(BOARD_AVB_BOOT_KEY_PATH)
endif
#具体生成boot的部分,添加依赖,并调用build-recoveryimage-target 生成boot.img
$(INSTALLED_BOOTIMAGE_TARGET): $(MKBOOTFS) $(MKBOOTIMG) $(MINIGZIP) $(ADBD) \
    $(INSTALLED_RAMDISK_TARGET) \
    $(INTERNAL_RECOVERYIMAGE_FILES) \
    $(recovery_initrc) $(recovery_sepolicy) $(recovery_kernel) \
    $(INSTALLED_2NDBOOTLOADER_TARGET) \
    $(recovery_build_props) $(recovery_resource_deps) \
    $(recovery_fstab) \
    $(RECOVERY_INSTALL_OTA_KEYS) \
    $(INSTALLED_VENDOR_DEFAULT_PROP_TARGET) \
    $(BOARD_RECOVERY_KERNEL_MODULES) \
    $(DEPMOD)
    $(call pretty,"Target boot image from recovery: $@")
    $(call build-recoveryimage-target, $@)
endif
#以下是生成recovery.img的代码,其实跟上面是一模一样的
$(INSTALLED_RECOVERYIMAGE_TARGET): $(MKBOOTFS) $(MKBOOTIMG) $(MINIGZIP) $(ADBD) \
    $(INSTALLED_RAMDISK_TARGET) \
    $(INSTALLED_BOOTIMAGE_TARGET) \
    $(INTERNAL_RECOVERYIMAGE_FILES) \
    $(recovery_initrc) $(recovery_sepolicy) $(recovery_kernel) \
    $(INSTALLED_2NDBOOTLOADER_TARGET) \
    $(recovery_build_props) $(recovery_resource_deps) \
    $(recovery_fstab) \
    $(RECOVERY_INSTALL_OTA_KEYS) \
    $(INSTALLED_VENDOR_DEFAULT_PROP_TARGET) \
    $(BOARD_RECOVERY_KERNEL_MODULES) \
    $(DEPMOD)
    $(call build-recoveryimage-target, $@)

 

device.mk中定义了BOARD_USES_RECOVERY_AS_BOOT :=true,所以不再走原来生成boot.img的流程,使用生成recovery.img的方法打包成boot.img

 

3、cache.img

 

# -----------------------------------------------------------------
# cache partition image
#因为我们更改了BoardConfig.mk,所以BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE没有定义,不再生成cache.img
ifdef BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE
INTERNAL_CACHEIMAGE_FILES := \
    $(filter $(TARGET_OUT_CACHE)/%,$(ALL_DEFAULT_INSTALLED_MODULES))

cacheimage_intermediates := \
    $(call intermediates-dir-for,PACKAGING,cache)
BUILT_CACHEIMAGE_TARGET := $(PRODUCT_OUT)/cache.img

define build-cacheimage-target
  $(call pretty,"Target cache fs image: $(INSTALLED_CACHEIMAGE_TARGET)")
  @mkdir -p $(TARGET_OUT_CACHE)
  @mkdir -p $(cacheimage_intermediates) && rm -rf $(cacheimage_intermediates)/cache_image_info.txt
  $(call generate-userimage-prop-dictionary, $(cacheimage_intermediates)/cache_image_info.txt, skip_fsck=true)
  $(hide) PATH=$(foreach p,$(INTERNAL_USERIMAGES_BINARY_PATHS),$(p):)$$PATH \
      build/make/tools/releasetools/build_image.py \
      $(TARGET_OUT_CACHE) $(cacheimage_intermediates)/cache_image_info.txt $(INSTALLED_CACHEIMAGE_TARGET) $(TARGET_OUT)
  $(hide) $(call assert-max-image-size,$(INSTALLED_CACHEIMAGE_TARGET),$(BOARD_CACHEIMAGE_PARTITION_SIZE))
endef

# We just build this directly to the install location.
INSTALLED_CACHEIMAGE_TARGET := $(BUILT_CACHEIMAGE_TARGET)
$(INSTALLED_CACHEIMAGE_TARGET): $(INTERNAL_USERIMAGES_DEPS) $(INTERNAL_CACHEIMAGE_FILES) $(BUILD_IMAGE_SRCS)
  $(build-cacheimage-target)

.PHONY: cacheimage-nodeps
cacheimage-nodeps: | $(INTERNAL_USERIMAGES_DEPS)
  $(build-cacheimage-target)

else # BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE
# we need to ignore the broken cache link when doing the rsync
#我们需要在执行rsync时忽略已损坏的缓存链接
IGNORE_CACHE_LINK := --exclude=cache
endif # BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE

 

BoardConfig.mk中更改了代码

ifneq ($(strip $(MTK_AB_OTA_UPDATER)), yes)

BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ext4

endif

BOARD_FLASH_BLOCK_SIZE := 4096

所以生成cache.img的方法将不再执行,并忽略与cache相关的损坏的链接

 

4、system.img

 

# $(1): output file
define build-systemimage-target
  @echo "Target system fs image: $(1)"
  #调用$(call create-system-vendor-symlink)创建符号链接
  $(call create-system-vendor-symlink)
  #调用$(call create-system-product-symlink)创建符号链接
  $(call create-system-product-symlink)
  #删除之前的system_image_info.txt
  @mkdir -p $(dir $(1)) $(systemimage_intermediates) && rm -rf $(systemimage_intermediates)/system_image_info.txt
  #调用call generate-userimage-prop-dictionary,重新生成system_image_info.txt
  $(call generate-userimage-prop-dictionary, $(systemimage_intermediates)/system_image_info.txt, \
      skip_fsck=true)
  #调用build_image.py 传入system_image_info.txt和$(PRODUCT_OUT)/system创建system.img文件
  $(hide) PATH=$(foreach p,$(INTERNAL_USERIMAGES_BINARY_PATHS),$(p):)$$PATH \
      build/make/tools/releasetools/build_image.py \
      $(TARGET_OUT) $(systemimage_intermediates)/system_image_info.txt $(1) $(TARGET_OUT) \
      || ( echo "Out of space? the tree size of $(TARGET_OUT) is (MB): " 1>&2 ;\
           du -sm $(TARGET_OUT) 1>&2;\
           if [ "$(INTERNAL_USERIMAGES_EXT_VARIANT)" == "ext4" ]; then \
               maxsize=$(BOARD_SYSTEMIMAGE_PARTITION_SIZE); \
               echo "The max is $$(( maxsize / 1048576 )) MB." 1>&2 ;\
           else \
               echo "The max is $$(( $(BOARD_SYSTEMIMAGE_PARTITION_SIZE) / 1048576 )) MB." 1>&2 ;\
           fi; \
           mkdir -p $(DIST_DIR); cp $(INSTALLED_FILES_FILE) $(DIST_DIR)/installed-files-rescued.txt; \
           exit 1 )
endef

$(BUILT_SYSTEMIMAGE): $(FULL_SYSTEMIMAGE_DEPS) $(INSTALLED_FILES_FILE) $(BUILD_IMAGE_SRCS)
  $(call build-systemimage-target,$@)

INSTALLED_SYSTEMIMAGE := $(PRODUCT_OUT)/system.img
SYSTEMIMAGE_SOURCE_DIR := $(TARGET_OUT)

这部分在之前分析make 生成 system.img时候提到过,传入的info文件将决定system.img中打包的内容

 

# $(1): the path of the output dictionary file
# $(2): additional "key=value" pairs to append to the dictionary file.
#调用该方法生成system_image_info.txt,其中BOARD_BUILD_SYSTEM_ROOT_IMAGE 宏是打开的会将如下两个值
#写入到info文件中
define generate-userimage-prop-dictionary
$(if $(filter true,$(BOARD_BUILD_SYSTEM_ROOT_IMAGE)),\
    $(hide) echo "system_root_image=true" >> $(1);\
    echo "ramdisk_dir=$(TARGET_ROOT_OUT)" >> $(1))
$(if $(2),$(hide) $(foreach kv,$(2),echo "$(kv)" >> $(1);))
endef

 

ext_mkuserimg=mkuserimg_mke2fs.sh
fs_type=ext4
system_size=2684354560
userdata_size=3221225472
vendor_fs_type=ext4
vendor_size=578813952
extfs_sparse_flag=-s
squashfs_sparse_flag=-s
selinux_fc=out/target/product/k39tv1_64_bsp/obj/ETC/file_contexts.bin_intermediates/file_contexts.bin
boot_signer=true
verity=true
verity_key=build/target/product/security/verity
verity_signer_cmd=verity_signer
verity_fec=true
system_verity_block_device=/dev/block/platform/bootdevice/by-name/system
vendor_verity_block_device=/dev/block/platform/bootdevice/by-name/vendor
recovery_as_boot=true
system_root_image=true
ramdisk_dir=out/target/product/k39tv1_64_bsp/root
skip_fsck=true

这两个参数的加入

system_root_image=true

ramdisk_dir=out/target/product/k39tv1_64_bsp/root

决定了真正最后生成的system.img文件会将ramdisk和filesystem一并打包,也就是说目前的system.img包含了rootfs(查看9.0的代码可以发现,即使是非ab,也有这两个参数从存在了)

 

5、system_other.img

 

# -----------------------------------------------------------------
# system_other partition image
#该BOARD_USES_SYSTEM_OTHER_ODEX打开后 会设定BOARD_USES_SYSTEM_OTHER
ifeq ($(BOARD_USES_SYSTEM_OTHER_ODEX),true)
BOARD_USES_SYSTEM_OTHER := true

# Marker file to identify that odex files are installed
INSTALLED_SYSTEM_OTHER_ODEX_MARKER := $(TARGET_OUT_SYSTEM_OTHER)/system-other-odex-marker
ALL_DEFAULT_INSTALLED_MODULES += $(INSTALLED_SYSTEM_OTHER_ODEX_MARKER)
$(INSTALLED_SYSTEM_OTHER_ODEX_MARKER):
  $(hide) touch $@
endif
#BOARD_USES_SYSTEM_OTHER该宏为true,进入if生成system_other.img
ifdef BOARD_USES_SYSTEM_OTHER
INTERNAL_SYSTEMOTHERIMAGE_FILES := \
    $(filter $(TARGET_OUT_SYSTEM_OTHER)/%,\
      $(ALL_DEFAULT_INSTALLED_MODULES)\
      $(ALL_PDK_FUSION_FILES)) \
    $(PDK_FUSION_SYMLINK_STAMP)

INSTALLED_FILES_FILE_SYSTEMOTHER := $(PRODUCT_OUT)/installed-files-system-other.txt
$(INSTALLED_FILES_FILE_SYSTEMOTHER) : $(INTERNAL_SYSTEMOTHERIMAGE_FILES) $(FILESLIST)
  @echo Installed file list: $@
  @mkdir -p $(dir $@)
  @rm -f $@
  $(hide) $(FILESLIST) $(TARGET_OUT_SYSTEM_OTHER) > $(@:.txt=.json)
  $(hide) build/make/tools/fileslist_util.py -c $(@:.txt=.json) > $@

systemotherimage_intermediates := \
    $(call intermediates-dir-for,PACKAGING,system_other)
BUILT_SYSTEMOTHERIMAGE_TARGET := $(PRODUCT_OUT)/system_other.img

# Note that we assert the size is SYSTEMIMAGE_PARTITION_SIZE since this is the 'b' system image.
define build-systemotherimage-target
  $(call pretty,"Target system_other fs image: $(INSTALLED_SYSTEMOTHERIMAGE_TARGET)")
  @mkdir -p $(TARGET_OUT_SYSTEM_OTHER)
  @mkdir -p $(systemotherimage_intermediates) && rm -rf $(systemotherimage_intermediates)/system_other_image_info.txt
  $(call generate-userimage-prop-dictionary, $(systemotherimage_intermediates)/system_other_image_info.txt, skip_fsck=true)
  $(hide) PATH=$(foreach p,$(INTERNAL_USERIMAGES_BINARY_PATHS),$(p):)$$PATH \
      build/make/tools/releasetools/build_image.py \
      $(TARGET_OUT_SYSTEM_OTHER) $(systemotherimage_intermediates)/system_other_image_info.txt $(INSTALLED_SYSTEMOTHERIMAGE_TARGET) $(TARGET_OUT)
  $(hide) $(call assert-max-image-size,$(INSTALLED_SYSTEMOTHERIMAGE_TARGET),$(BOARD_SYSTEMIMAGE_PARTITION_SIZE))
endef

# We just build this directly to the install location.
INSTALLED_SYSTEMOTHERIMAGE_TARGET := $(BUILT_SYSTEMOTHERIMAGE_TARGET)
ifneq (true,$(SANITIZE_LITE))
# Only create system_other when not building the second stage of a SANITIZE_LITE build.
$(INSTALLED_SYSTEMOTHERIMAGE_TARGET): $(INTERNAL_USERIMAGES_DEPS) $(INTERNAL_SYSTEMOTHERIMAGE_FILES) $(INSTALLED_FILES_FILE_SYSTEMOTHER)
  $(build-systemotherimage-target)
endif

.PHONY: systemotherimage-nodeps
systemotherimage-nodeps: | $(INTERNAL_USERIMAGES_DEPS)
  $(build-systemotherimage-target)

endif # BOARD_USES_SYSTEM_OTHER

 

device.mk打开ab后定义了生成system_other相关的宏# A/B BOARD_USES_SYSTEM_OTHER_ODEX := true,将system/app 和 system/priv-app下的odex文件存储到system_other.img中

 

这个img的生成有什么作用呢,查到这样一个说明

50% of system image is precompiled odex files (-2048MiB)

  • Moved them to B partition
  • Copied to /data on first boot
  • See BOARD_USES_SYSTEM_OTHER_ODEX BoardConfig option
  • Now A/B /system takes same space as non-A/B /system

意思是说将原来system里面的odex文件放到system_other.img中,刷机的时候放入B分区,并在首次开机的时候拷贝到data区进行预加载,这样ab升级中的system分区与非ab的情况一样,目的是为了减小system分区的大小,不过百分之50有些夸张了,我们编译出来的实际只有100多M,

 

开启AB升级方案的项目,因为很多需要升级的镜像都有两份,所以存储空间将会增大。为缓解此问题,有个针对odex的优化方案。

编译版本会生成两个system镜像:system.img和system_other.img,其中,system_other.img中存储的就是odex文件,这样system.img就能小很多,意味着可以为system分区划分较小的空间。

在首次开机时,假设system.img镜像存储在A slot,那么此时的B slot是闲置的。所以可以把system.img刷入A slot的system分区,把system_other.img刷入B slot的system分区。在首次开机时,再把system_other.img中的odex文件拷贝到data分区。

 

6、vendor.img  和 userdata.img

 

这两个img的生成在打开ab后没有变化,生成流程于system.img相同,根据对应info,调用build_image.py生成对应的文件

 

三、make 和make otapackage过程中的变化

1、build.prop中加入字段体现

make/tools/buildinfo.sh加入

if [ -n "$AB_OTA_UPDATER" ] ; then

 echo "ro.build.ab_update=$AB_OTA_UPDATER"

fi

Makefile中会执行buildinfo.sh文件

$(intermediate_system_build_prop): $(BUILDINFO_SH) $(INTERNAL_BUILD_ID_MAKEFILE) $(BUILD_SYSTEM)/version_defaults.mk $(system_prop_file) $(INSTALLED_ANDROID_INFO_TXT_TARGET)
            ......
            ......
          bash $(BUILDINFO_SH) >> $@

system/build.prop中增加:ro.build.ab_update=true

 

2、签名相关 Makefile

# Carry the public key for update_engine if it's a non-IoT target that
# uses the AB updater. We use the same key as otacerts but in RSA public key
# format.
#如果update_engine是非IoT目标,则携带公钥
#使用AB更新程序。 我们使用与otacerts相同的密钥但使用RSA公钥format。
ifeq ($(AB_OTA_UPDATER),true)
ifneq ($(PRODUCT_IOT),true)
ALL_DEFAULT_INSTALLED_MODULES += $(TARGET_OUT_ETC)/update_engine/update-payload-key.pub.pem
$(TARGET_OUT_ETC)/update_engine/update-payload-key.pub.pem: $(addsuffix .x509.pem,$(DEFAULT_KEY_CERT_PAIR))
  $(hide) rm -f $@
  $(hide) mkdir -p $(dir $@)
  $(hide) openssl x509 -pubkey -noout -in $< > $@

ALL_DEFAULT_INSTALLED_MODULES += $(TARGET_RECOVERY_ROOT_OUT)/etc/update_engine/update-payload-key.pub.pem
$(TARGET_RECOVERY_ROOT_OUT)/etc/update_engine/update-payload-key.pub.pem: $(TARGET_OUT_ETC)/update_engine/update-payload-key.pub.pem
  $(hide) cp -f $< $@
endif
endif

 

3、obj包的生成相关

#指定对应依赖 这个built_ota_tools其实生成的就是updater,如果是ab升级,obj包里不需要打包这个文件
ifeq ($(AB_OTA_UPDATER),true)
updater_dep := system/update_engine/update_engine.conf
else
# Build OTA tools if not using the AB Updater.
updater_dep := $(built_ota_tools)
endif
$(BUILT_TARGET_FILES_PACKAGE): $(updater_dep)

 

$(BUILT_TARGET_FILES_PACKAGE):
......
......
    #如果是非AB,拷贝对应image和生成ota_update_list.txt,如果是ab,生成ab_partitions.txt,拷贝对应image
  @# Copy raw images which need OTA updates from out folder to zip_root/IMAGES folder
  $(hide) BOARD_AVB_ENABLE="$(BOARD_AVB_ENABLE)" AB_OTA_UPDATER="$(AB_OTA_UPDATER)" AB_OTA_PARTITIONS="$(AB_OTA_PARTITIONS)" $(TARGET_RELEASETOOLS_EXTENSIONS)/mt_ota_preprocess.py $(zip_root) $(PRODUCT_OUT) $(PRODUCT_OUT)/ota_update_list.txt
ifneq ($(INSTALLED_RECOVERYIMAGE_TARGET),)
  $(hide) PATH=$(foreach p,$(INTERNAL_USERIMAGES_BINARY_PATHS),$(p):)$$PATH MKBOOTIMG=$(MKBOOTIMG) \
      build/make/tools/releasetools/make_recovery_patch $(zip_root) $(zip_root)
endif
ifeq ($(AB_OTA_UPDATER),true)
  @# When using the A/B updater, include the updater config files in the zip.
    #拷贝update_engine.conf 到META/update_engine_config.txt
  $(hide) cp $(TOPDIR)system/update_engine/update_engine.conf $(zip_root)/META/update_engine_config.txt
  $(hide) for part in $(AB_OTA_PARTITIONS); do \
      #把AB_OTA_PARTITIONS定义的分区拷贝到META/ab_partitions.txt
    echo "$${part}" >> $(zip_root)/META/ab_partitions.txt; \
  done
  $(hide) for conf in $(AB_OTA_POSTINSTALL_CONFIG); do \
      #把AB_OTA_POSTINSTALL_CONFIG定义的内容拷贝到META/postinstall_config.txt
    echo "$${conf}" >> $(zip_root)/META/postinstall_config.txt; \
  done
  @# Include the build type in META/misc_info.txt so the server can easily differentiate production builds.
  #build_type放入misc_info
    $(hide) echo "build_type=$(TARGET_BUILD_VARIANT)" >> $(zip_root)/META/misc_info.txt
  #ab_update放入misc_info
    $(hide) echo "ab_update=true" >> $(zip_root)/META/misc_info.txt
ifdef BRILLO_VENDOR_PARTITIONS
  $(hide) mkdir -p $(zip_root)/VENDOR_IMAGES
  $(hide) for f in $(BRILLO_VENDOR_PARTITIONS); do \
    pair1="$$(echo $$f | awk -F':' '{print $$1}')"; \
    pair2="$$(echo $$f | awk -F':' '{print $$2}')"; \
    src=$${pair1}/$${pair2}; \
    dest=$(zip_root)/VENDOR_IMAGES/$${pair2}; \
    mkdir -p $$(dirname "$${dest}"); \
    cp $${src} $${dest}; \
  done;
endif
......
......

 

4、整包生成相关

AB情况下添加依赖,可执行文件brillo_update_payload,非AB情况下添加依赖brotli

ifeq ($(AB_OTA_UPDATER),true)
$(INTERNAL_OTA_PACKAGE_TARGET): $(BRILLO_UPDATE_PAYLOAD)
else
$(INTERNAL_OTA_PACKAGE_TARGET): $(BROTLI)
endif

 

 

关于device和build大致整理了这么多内容,因为我也是刚开始看,可能没有整理全,后面再继续填坑。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值