1 概述
bp文件的由來在此之前你需要先了解mk文件
.mk 文件通常是 Makefile 文件,用于定義編譯系統(tǒng)的規(guī)則和操作材泄。在 Android 源代碼中,.mk 文件被用來描述如何構(gòu)建和編譯特定模塊或組件吨岭,包括定義依賴關(guān)系拉宗、編譯參數(shù)、鏈接庫等。這些文件通常用于使用 GNU Make 構(gòu)建系統(tǒng)進行項目的構(gòu)建旦事。
.bp 文件是 Blueprint 文件的縮寫魁巩,這是 Android 構(gòu)建系統(tǒng)(Soong)所采用的一種新的構(gòu)建描述語言。Blueprint 文件用于描述 Android 源代碼中的各種模塊姐浮,包括庫谷遂、應(yīng)用程序、插件等的信息卖鲤,例如模塊的依賴關(guān)系肾扰、編譯選項、安裝路徑等蛋逾。相比傳統(tǒng)的 Makefile 格式集晚,Blueprint 使用更加結(jié)構(gòu)化的語法,并且能夠更好地處理模塊間的依賴關(guān)系区匣。
也就是說mk文件和bp文件都是用來定義android系統(tǒng)的編譯規(guī)則和操作的
對比倆者
更現(xiàn)代化的構(gòu)建系統(tǒng):
1 Blueprint 是為了適應(yīng) Android 構(gòu)建系統(tǒng)(Soong)的需求而設(shè)計的偷拔,它提供了一種更現(xiàn)代化、更靈活的描述方式來定義模塊和構(gòu)建規(guī)則亏钩。相比較傳統(tǒng)的 Makefile 格式莲绰,Blueprint 提供了更加結(jié)構(gòu)化的語法,并且更容易閱讀和維護
第一點 bp文件相較于mk文件姑丑,語法更規(guī)范蛤签,更方便閱讀
該點在學(xué)習(xí)完bp文件后,我們也會找倆個文件進行對比
2 Blueprint 文件能夠更好地管理模塊之間的依賴關(guān)系彻坛,包括動態(tài)依賴顷啼、跨模塊依賴等,使得 Android 系統(tǒng)的各個模塊能夠更好地協(xié)同工作
該點在后續(xù)內(nèi)容中我們會提到
3 Android 構(gòu)建系統(tǒng)使用了基于 Blueprint 的描述文件昌屉,能夠更好地實現(xiàn)高度并行化的構(gòu)建钙蒙,從而提高構(gòu)建速度和效率。相比傳統(tǒng)的 Makefile 格式间驮,Blueprint 能夠更好地利用多核處理器進行并行構(gòu)建躬厌,加快整體構(gòu)建過程。
第三點主要指明BP文件在構(gòu)建的速度上領(lǐng)先與mk
4 Blueprint 提供了更靈活的方式來描述模塊的構(gòu)建規(guī)則竞帽,包括編譯選項扛施、鏈接庫、安裝路徑等屹篓,能夠更好地滿足復(fù)雜項目的定制化需求疙渣。
2 編譯結(jié)構(gòu)
如圖所示
- 1 bp文件需要通過soong+blueprint解析為ninja文件
- 2 mk文件需要通過kati解析為ninja文件
- 3 ninja文件被系統(tǒng)編譯框架ninja解析構(gòu)建系統(tǒng)
- 4 mk文件可以通過android工具androidmk 轉(zhuǎn)換為bp文件
注意點 mk自動轉(zhuǎn)換bp文件的前提是mk文件沒有控制邏輯
如果存在需要手動轉(zhuǎn)換
3 android.mk自動轉(zhuǎn)換android.bp
通過make androidmk 命令可以在out目錄下生成androidmk
然后執(zhí)行androidmk android.mk來生成bp文件
但是我本地編譯后,out目錄沒看到androidmk堆巧,所以就先當(dāng)了解一下吧
4 android.bp文件語法
介紹一些常用的bp命令
java_library 會把aidl java 等文件編譯成 .jar 庫
android_library 會把 xml 資源文件妄荔, aidl java 等文件 編譯成 .aar 庫
java_import 預(yù)編譯 .jar 庫 (引用 第三方 jar 庫)
android_library_import 這是預(yù)編譯 .aar 庫 (引用第三方aar庫)
android_app_import 這是 預(yù)編譯 apk泼菌,相當(dāng)于 BUILD_PREBUILT
android_app 編譯成apk,相當(dāng)于 BUILD_PACKAGE
這里提到了預(yù)編譯 apk和編譯成apk啦租,但是倆者是有區(qū)別的
相同點 倆者都生成了apk文件
不同點 預(yù)編譯會在編譯完apk后進行進一步操作例如代碼混淆哗伯,簽名等等
除此之外還有deafult cc_default java_default
default如何使用
# 定義默認(rèn)配置規(guī)則
defaults(
name = "my_defaults",
visibility = ["http://visibility:public"],
# 設(shè)置通用屬性值,如編譯器選項等
cpp_compile_flags = ["-Wall", "-O2"],
java_source_version = "8",
)
# 定義 C/C++ 目標(biāo)
cc_library(
name = "my_cc_lib",
srcs = ["my_cc_lib.cpp"],
deps = [":my_defaults"],
)
# 定義 Java 目標(biāo)
java_library(
name = "my_java_lib",
srcs = ["MyJavaClass.java"],
deps = [":my_defaults"],
)
可以看到我在default中定義相關(guān)的配置項篷角,可見性焊刹,cpp編譯規(guī)則,java源碼版本
而我們在構(gòu)建其他的庫時可以直接通過deps來引用我們的默認(rèn)規(guī)則
作用
通過這種方式恳蹲,我們可以確保所有的 C/C++ 目標(biāo)和 Java 目標(biāo)都繼承了相同的默認(rèn)屬性設(shè)置虐块,避免了重復(fù)配置,并使得整個項目的構(gòu)建行為更加一致和可維護嘉蕾。
除了配置選項還可以引入依賴庫
# 定義默認(rèn)配置規(guī)則
cc_defaults(
name = "my_cc_defaults",
visibility = ["http://visibility:public"],
deps = ["http://path/to:common_lib"], # 加入庫依賴
cpp_compile_flags = ["-Wall", "-O2"],
)
# 定義 C/C++ 目標(biāo)
cc_library(
name = "my_cc_lib",
srcs = ["my_cc_lib.cpp"],
defaults = ["my_cc_defaults"], # 引用默認(rèn)配置
)
# 定義 Java 目標(biāo)
java_defaults(
name = "my_java_defaults",
visibility = ["http://visibility:public"],
deps = [":common_java_lib"], # 加入庫依賴
java_source_version = "8",
)
java_library(
name = "my_java_lib",
srcs = ["MyJavaClass.java"],
defaults = ["my_java_defaults"], # 引用默認(rèn)配置
)
這里我們定義c的默認(rèn)配置和java的默認(rèn)配置非凌,同時引入了相關(guān)依賴庫,其他庫在構(gòu)建時可以通過deps引用默認(rèn)配置荆针,來直接引用相關(guān)庫
這比較適用于你在編譯bp文件時存在大量模塊存在共通的配置才會這么做
5 android.bp與android.mk對比
Android.mk
include $(BUILD_JAVA_LIBRARY)
Android.bp
java_library {
......
}
mk生成jar包BUILD_JAVA_LIBRARY
bp生成jar包使用java_library
Android.mk
include $(BUILD_STATIC_JAVA_LIBRARY)
Android.bp
java_library_static {
......
}
mk生成java靜態(tài)庫 BUILD_STATIC_JAVA_LIBRARY
bp生成java靜態(tài)庫使用java_library_static
Android.mk
include $(BUILD_PACKAGE)
Android.bp
android_app {
......
}
mk使用BUILD_PACKAGE生成apk
bp使用android_app生成apk
Android.mk
include $(BUILD_SHARED_LIBRARY)
Android.bp
cc_library_shared {
......
}
mk BUILD_SHARED_LIBRARY 生成native庫
bp cc_library_shared 生成native庫
Android.mk
include $(BUILD_STATIC_LIBRARY)
Android.bp
cc_library_static {
......
}
mk BUILD_STATIC_LIBRARY 生成native靜態(tài)庫
bp cc_library_static 生成native靜態(tài)庫
Android.mk
include $(BUILD_EXECUTABLE)
Android.bp
cc_binary {
......
}
mk BUILD_EXECUTABLE native可執(zhí)行程序
bp cc_binary native可執(zhí)行程序
Android.mk
include $(BUILD_HEADER_LIBRARY)
Android.bp
cc_library_headers {
......
}
頭文件庫
mk BUILD_HEADER_LIBRARY
bp cc_library_headers
Android.mk
LOCAL_C_INCLUDES :=
Android.bp
local_include_dirs: ["xxx", ...]
本地頭文件路徑
mk LOCAL_C_INCLUDES
bp local_include_dirs
Android.mk
LOCAL_EXPORT_C_INCLUDE_DIRS :=
Android.bp
export_include_dirs: ["xxx", ...]
排除頭文件
mk LOCAL_EXPORT_C_INCLUDE_DIRS
bp export_include_dirs
Android.mk
LOCAL_RESOURCE_DIR :=
Android.bp
resource_dirs: ["xxx", ...]
資源文件
mk LOCAL_RESOURCE_DIR
bp resource_dirs
Android.mk
LOCAL_STATIC_LIBRARIES :=
Android.bp
static_libs: ["xxx", "xxx", ...]
依賴靜態(tài)庫
這里我們先了解靜態(tài)庫與動態(tài)庫的區(qū)別
對于靜態(tài)庫,如果其中的文件內(nèi)容發(fā)生了修改颁糟,你需要重新編譯整個項目航背,并且在鏈接階段將新的靜態(tài)庫文件鏈接到可執(zhí)行文件中。因為靜態(tài)庫的內(nèi)容在編譯時就被完全復(fù)制到可執(zhí)行文件中棱貌,所以任何對靜態(tài)庫的修改都需要重新構(gòu)建整個項目玖媚。
而對于動態(tài)庫,如果其中的文件內(nèi)容發(fā)生了修改婚脱,你只需要編譯動態(tài)庫本身今魔,并將新的動態(tài)庫文件替換掉舊的文件即可。由于動態(tài)庫是在運行時加載的障贸,因此不會影響到已經(jīng)編譯好的可執(zhí)行文件错森,只需確保新的動態(tài)庫文件和舊的動態(tài)庫文件保持二進制兼容性即可。
總的來說篮洁,動態(tài)庫的修改更加靈活涩维,只需要重新編譯動態(tài)庫本身,而不需要重新整編整個系統(tǒng)袁波。這也是動態(tài)庫在軟件開發(fā)中更常用的一個原因之一瓦阐。
Android.mk
LOCAL_SHARED_LIBRARIES :=
Android.bp
shared_libs: ["xxx", "xxx", ...]
依賴動態(tài)庫
Android.mk
LOCAL_HEADER_LIBRARIES :=
Android.bp
header_libs: ["xxx", "xxx", ...]
依賴頭文件庫
目前就更新這么多,后續(xù)我們在具體看幾個bp文件的時候篷牌,如果又不知道的我們在記錄
6 bp文件學(xué)習(xí)
下面舉出幾個android系統(tǒng)的bp文件
java側(cè)以/frameworks/base/services/core/android.bp為例
package {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_license"],
}
filegroup {
name: "services.core-sources-am-wm",
srcs: [
"java/com/android/server/am/**/*.java",
"java/com/android/server/wm/**/*.java",
],
path: "java",
visibility: ["http://frameworks/base/services"],
}
filegroup {
name: "services.core-sources",
srcs: ["java/**/*.java"],
exclude_srcs: [
":services.core-sources-am-wm",
],
path: "java",
visibility: [
"http://frameworks/base/services",
"http://frameworks/base/core/java/com/android/internal/protolog",
],
}
genrule {
name: "services.core.protologsrc",
srcs: [
":protolog-groups",
":services.core-sources-am-wm",
],
tools: ["protologtool"],
cmd: "$(location protologtool) transform-protolog-calls " +
"--protolog-class com.android.internal.protolog.common.ProtoLog " +
"--protolog-impl-class com.android.internal.protolog.ProtoLogImpl " +
"--protolog-cache-class 'com.android.server.wm.ProtoLogCache' " +
"--loggroups-class com.android.internal.protolog.ProtoLogGroup " +
"--loggroups-jar $(location :protolog-groups) " +
"--output-srcjar $(out) " +
"$(locations :services.core-sources-am-wm)",
out: ["services.core.protolog.srcjar"],
}
genrule {
name: "generate-protolog.json",
srcs: [
":protolog-groups",
":services.core-sources-am-wm",
],
tools: ["protologtool"],
cmd: "$(location protologtool) generate-viewer-config " +
"--protolog-class com.android.internal.protolog.common.ProtoLog " +
"--loggroups-class com.android.internal.protolog.ProtoLogGroup " +
"--loggroups-jar $(location :protolog-groups) " +
"--viewer-conf $(out) " +
"$(locations :services.core-sources-am-wm)",
out: ["services.core.protolog.json"],
}
genrule {
name: "checked-protolog.json",
srcs: [
":generate-protolog.json",
":services.core.protolog.json",
],
cmd: "cp $(location :generate-protolog.json) $(out) && " +
"{ ! (diff $(out) $(location :services.core.protolog.json) | grep -q '^<') || " +
"{ echo -e '\\n\\n################################################################\\n#\\n" +
"# ERROR: ProtoLog viewer config is stale. To update it, run:\\n#\\n" +
"# cp $(location :generate-protolog.json) " +
"$(location :services.core.protolog.json)\\n#\\n" +
"################################################################\\n\\n' >&2 && false; } }",
out: ["services.core.protolog.json"],
}
genrule {
name: "statslog-art-java-gen",
tools: ["stats-log-api-gen"],
cmd: "$(location stats-log-api-gen) --java $(out) --module art" +
" --javaPackage com.android.internal.art --javaClass ArtStatsLog --worksource",
out: ["com/android/internal/art/ArtStatsLog.java"],
}
genrule {
name: "statslog-contexthub-java-gen",
tools: ["stats-log-api-gen"],
cmd: "$(location stats-log-api-gen) --java $(out) --module contexthub" +
" --javaPackage com.android.server.location.contexthub --javaClass ContextHubStatsLog",
out: ["com/android/server/location/contexthub/ContextHubStatsLog.java"],
}
java_library_static {
name: "services.core.unboosted",
defaults: ["platform_service_defaults"],
srcs: [
":android.hardware.biometrics.face-V2-java-source",
":statslog-art-java-gen",
":statslog-contexthub-java-gen",
":services.core-sources",
":services.core.protologsrc",
":dumpstate_aidl",
":framework_native_aidl",
":gsiservice_aidl",
":installd_aidl",
":storaged_aidl",
":vold_aidl",
":platform-compat-config",
":platform-compat-overrides",
":display-device-config",
":display-layout-config",
":device-state-config",
"java/com/android/server/EventLogTags.logtags",
"java/com/android/server/am/EventLogTags.logtags",
"java/com/android/server/wm/EventLogTags.logtags",
"java/com/android/server/policy/EventLogTags.logtags",
],
libs: [
"services.net",
"android.hardware.common-V2-java",
"android.hardware.light-V2.0-java",
"android.hardware.gnss-V2-java",
"android.hardware.vibrator-V2-java",
"app-compat-annotations",
"framework-tethering.stubs.module_lib",
"service-permission.stubs.system_server",
"service-sdksandbox.stubs.system_server",
],
required: [
"default_television.xml",
"gps_debug.conf",
"protolog.conf.json.gz",
],
static_libs: [
"time_zone_distro",
"time_zone_distro_installer",
"android.hardware.authsecret-V1.0-java",
"android.hardware.boot-V1.0-java",
"android.hardware.boot-V1.1-java",
"android.hardware.boot-V1.2-java",
"android.hardware.broadcastradio-V2.0-java",
"android.hardware.health-V1.0-java", // HIDL
"android.hardware.health-V2.0-java", // HIDL
"android.hardware.health-V2.1-java", // HIDL
"android.hardware.health-V1-java", // AIDL
"android.hardware.health-translate-java",
"android.hardware.light-V1-java",
"android.hardware.tv.cec-V1.1-java",
"android.hardware.weaver-V1.0-java",
"android.hardware.biometrics.face-V1.0-java",
"android.hardware.biometrics.fingerprint-V2.3-java",
"android.hardware.biometrics.fingerprint-V2-java",
"android.hardware.oemlock-V1.0-java",
"android.hardware.configstore-V1.1-java",
"android.hardware.ir-V1-java",
"android.hardware.rebootescrow-V1-java",
"android.hardware.soundtrigger-V2.3-java",
"android.hardware.power.stats-V1-java",
"android.hardware.power-V3-java",
"android.hidl.manager-V1.2-java",
"capture_state_listener-aidl-java",
"icu4j_calendar_astronomer",
"netd-client",
"overlayable_policy_aidl-java",
"SurfaceFlingerProperties",
"com.android.sysprop.watchdog",
],
javac_shard_size: 50,
}
java_genrule {
name: "services.core.priorityboosted",
srcs: [":services.core.unboosted"],
tools: ["lockedregioncodeinjection"],
cmd: "$(location lockedregioncodeinjection) " +
" --targets \"Lcom/android/server/am/ActivityManagerService;,Lcom/android/server/am/ActivityManagerGlobalLock;,Lcom/android/server/wm/WindowManagerGlobalLock;\" " +
" --pre \"com/android/server/am/ActivityManagerService.boostPriorityForLockedSection,com/android/server/am/ActivityManagerService.boostPriorityForProcLockedSection,com/android/server/wm/WindowManagerService.boostPriorityForLockedSection\" " +
" --post \"com/android/server/am/ActivityManagerService.resetPriorityAfterLockedSection,com/android/server/am/ActivityManagerService.resetPriorityAfterProcLockedSection,com/android/server/wm/WindowManagerService.resetPriorityAfterLockedSection\" " +
" -o $(out) " +
" -i $(in)",
out: ["services.core.priorityboosted.jar"],
}
java_library {
name: "services.core",
static_libs: ["services.core.priorityboosted"],
}
java_library_host {
name: "core_cts_test_resources",
srcs: ["java/com/android/server/notification/SmallHash.java"],
}
prebuilt_etc {
name: "gps_debug.conf",
src: "java/com/android/server/location/gnss/gps_debug.conf",
}
genrule {
name: "services.core.json.gz",
srcs: [":checked-protolog.json"],
out: ["services.core.protolog.json.gz"],
cmd: "$(location minigzip) -c < $(in) > $(out)",
tools: ["minigzip"],
}
prebuilt_etc {
name: "protolog.conf.json.gz",
src: ":services.core.json.gz",
}
首先了解package
package {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_license"],
}
可以理解為構(gòu)建默認(rèn)許可證睡蟋,作用對知識產(chǎn)權(quán)的保護
filegroup {
name: "services.core-sources",
srcs: ["java/**/*.java"],
exclude_srcs: [
":services.core-sources-am-wm",
],
path: "java",
visibility: [
"http://frameworks/base/services",
"http://frameworks/base/core/java/com/android/internal/protolog",
],
}
這里定義的文件組
name 文件組名稱 "services.core-sources
src 加載的文件路徑 "java/*/.java"
exclude_srcs 排除構(gòu)建的文件組,這里填入的是文件組的名字
path 指明文件組中的文件java文件夾下
visibility 定義該模塊對其他模塊的可見性
genrule {
name: "services.core.protologsrc",
srcs: [
":protolog-groups",
":services.core-sources-am-wm",
],
tools: ["protologtool"],
cmd: "$(location protologtool) transform-protolog-calls " +
"--protolog-class com.android.internal.protolog.common.ProtoLog " +
"--protolog-impl-class com.android.internal.protolog.ProtoLogImpl " +
"--protolog-cache-class 'com.android.server.wm.ProtoLogCache' " +
"--loggroups-class com.android.internal.protolog.ProtoLogGroup " +
"--loggroups-jar $(location :protolog-groups) " +
"--output-srcjar $(out) " +
"$(locations :services.core-sources-am-wm)",
out: ["services.core.protolog.srcjar"],
}
主要是定義bp編譯規(guī)則
name 規(guī)則名稱
srcs 編譯依賴的相關(guān)規(guī)則和文件組
tools 編譯工具
cmd 編譯工具執(zhí)行的相關(guān)命令
out 執(zhí)行命令生成的jar包
這個genrule的作用就是使用定義的工具執(zhí)行cmd指令生成目標(biāo)文件
java_library_static {
name: "services.core.unboosted",
defaults: ["platform_service_defaults"],
srcs: [
":android.hardware.biometrics.face-V2-java-source",
":statslog-art-java-gen",
":statslog-contexthub-java-gen",
":services.core-sources",
":services.core.protologsrc",
":dumpstate_aidl",
":framework_native_aidl",
":gsiservice_aidl",
":installd_aidl",
":storaged_aidl",
":vold_aidl",
":platform-compat-config",
":platform-compat-overrides",
":display-device-config",
":display-layout-config",
":device-state-config",
"java/com/android/server/EventLogTags.logtags",
"java/com/android/server/am/EventLogTags.logtags",
"java/com/android/server/wm/EventLogTags.logtags",
"java/com/android/server/policy/EventLogTags.logtags",
],
libs: [
"services.net",
"android.hardware.common-V2-java",
"android.hardware.light-V2.0-java",
"android.hardware.gnss-V2-java",
"android.hardware.vibrator-V2-java",
"app-compat-annotations",
"framework-tethering.stubs.module_lib",
"service-permission.stubs.system_server",
"service-sdksandbox.stubs.system_server",
],
required: [
"default_television.xml",
"gps_debug.conf",
"protolog.conf.json.gz",
],
static_libs: [
"time_zone_distro",
"time_zone_distro_installer",
"android.hardware.authsecret-V1.0-java",
"android.hardware.boot-V1.0-java",
"android.hardware.boot-V1.1-java",
"android.hardware.boot-V1.2-java",
"android.hardware.broadcastradio-V2.0-java",
"android.hardware.health-V1.0-java", // HIDL
"android.hardware.health-V2.0-java", // HIDL
"android.hardware.health-V2.1-java", // HIDL
"android.hardware.health-V1-java", // AIDL
"android.hardware.health-translate-java",
"android.hardware.light-V1-java",
"android.hardware.tv.cec-V1.1-java",
"android.hardware.weaver-V1.0-java",
"android.hardware.biometrics.face-V1.0-java",
"android.hardware.biometrics.fingerprint-V2.3-java",
"android.hardware.biometrics.fingerprint-V2-java",
"android.hardware.oemlock-V1.0-java",
"android.hardware.configstore-V1.1-java",
"android.hardware.ir-V1-java",
"android.hardware.rebootescrow-V1-java",
"android.hardware.soundtrigger-V2.3-java",
"android.hardware.power.stats-V1-java",
"android.hardware.power-V3-java",
"android.hidl.manager-V1.2-java",
"capture_state_listener-aidl-java",
"icu4j_calendar_astronomer",
"netd-client",
"overlayable_policy_aidl-java",
"SurfaceFlingerProperties",
"com.android.sysprop.watchdog",
],
javac_shard_size: 50,
}
構(gòu)建java靜態(tài)庫
name services.core.unboosted
srcs 構(gòu)建需要的文件組或者編譯規(guī)則
libs 動態(tài)庫
required 構(gòu)建必需的配置文件枷颊,或者參數(shù),在構(gòu)建時會去查找這些文件是否存在
static_libs 靜態(tài)庫
prebuilt_etc {
name: "gps_debug.conf",
src: "java/com/android/server/location/gnss/gps_debug.conf",
}
可以理解為預(yù)構(gòu)建戳杀,prebuilt_etc 規(guī)則的作用是將指定的文件復(fù)制到輸出目錄中该面,以便在編譯后的系統(tǒng)鏡像或其他環(huán)境中使用該文件。這有助于將特定的配置文件或數(shù)據(jù)文件集成到編譯系統(tǒng)的構(gòu)建流程中
java_library_host {
name: "core_cts_test_resources",
srcs: ["java/com/android/server/notification/SmallHash.java"],
}
豺瘤,主機端 Java 庫通常用于在開發(fā)機器上進行本地測試吆倦、集成測試、工具開發(fā)或模擬數(shù)據(jù)生成等場景下坐求。這些庫有助于提高開發(fā)人員的工作效率蚕泽,并支持在開發(fā)過程中進行快速迭代和測試