將iOS專案進行子工程化
在iOS專案開發中,隨著專案的越來越大,工程的結構化會變差,編譯的速度也會越來越慢。使用靜態庫或動態庫的方式來構建子工程不僅可以加快專案的編譯速度,從結構上,也優化了專案的組織。有兩種方式來來對專案進行子工程化,可以在專案中建立子專案,也可以建立並列的專案,建立專案依賴。需要注意,無論哪種方式,你都應該儘量保證子工程不要用到主工程中的內容,如果必須這樣做,你可以採用代理或其他回撥程式設計方式來轉交給主工程自己處理。
一、建立子工程的一個示例
使用Xcode新建一個命名為ProjectDemo的工程,在ProjectDemo工程中再次新建一個framework庫工程,點選新建檔案中的Project…選項,選擇其中的Cocoa Touch Framework工程(建立Cocoa Touch Static Library則會打包為靜態庫)。
將新建立的工程命名為LoginLib,用來模擬專案中的登入模組。需要注意,新建工程時,需要將其加入ProjectDemo組,如下圖:
對於建立的LoginLib工程,你可以建立一個LoginLib.h標頭檔案用來公開外界需要使用到的類,便於演示,我在裡面建立一個檢視控制器和一個類別工具類,結構如下:
配置LoginLib的標頭檔案選項,將外界需要用到的進行公開,如下:
現在,分別編譯LoginLib工程和ProjectDemo工程,都沒有問題,但是你依然無法在ProjectDemo工程中使用LoginLib庫中的內容,你需要建立主子工程的關聯,在ProjectDemo工程中建立依賴工程並接入動態庫,如下所示
配置Target Dependencies的作用是確保每次主工程編譯前都會先對所依賴的工程進行編譯。之後,在ProjectDemo工程中匯入LoginLib相關標頭檔案即可使用其中功能。
注意,如果報錯找不到標頭檔案,你需要設定一下標頭檔案的尋找路徑,在ProjectDemo的Build Setting中搜索header,如下圖
設定Header Search Paths如下即可。
二、建立依賴模組工程的一個示例
開發中還有一種場景,公司可能有一組App,這些App中可能有很多相似的模組,例如某些應用程式分為使用者端和老闆端,他們都有相同的登入模組,我們可以使用workspace來進行專案和模組的管理。新建一個資料夾命名為Projects,在其中建立一個workspace檔案,也命名為Projects。在workspace檔案中新建兩個專案工程和一個動態庫工程,在建立時,注意選擇加入workspace,如下圖:
建立的3個工程分別命名為UserProject,BossProject和LoginLib,結構如下:
類似我們的第一個示例,配置完標頭檔案路徑後,將動態庫引入UserProject和BossProject工程,即實現了LoginLib模組的複用。
三、如果子工程只能夠有資原始檔
如果子工程中有資原始檔,無論是plist檔案還是圖片素材,在主工程呼叫動態庫時,這些檔案都是沒有被打包進來的。有兩種方式來處理這個問題:
1.將資原始檔打包成Bundle包,從包中取資源
Xcode可以建立Bundle資源包,這種檔案建立後編譯時會自動打包成Bundle檔案。需要注意,Xcode只能建立MacOS下的Bundle模板,建立後需要將編譯選項設定為iOS。這種方式有很大的弊端,首先主工程必須引入編譯後的Bundle包,如果每次新增或修改資源,都要重新打包和匯入。其次,在子工程中對素材進行使用時,都必須以Bundle為媒介,增加的複雜度。
2.使用shell拷貝資源指令碼
這種方式每次在編譯時都會將資源進行拷貝,類似CocoaPods的管理模式,推薦使用。例如,在主工程的編譯選項中新建一個指令碼檔案,如圖:
編寫如下指令碼程式碼即可:
#!/bin/sh
# set -e
mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
install_resource()
{
if [[ "$1" = /* ]] ; then
RESOURCE_PATH="$1"
fi
if [[ ! -e "$RESOURCE_PATH" ]] ; then
cat << EOM
error: Resource "$RESOURCE_PATH" not found.
EOM
exit 1
fi
case $RESOURCE_PATH in
*.storyboard)
echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}"
ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS}
;;
*.xib)
echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}"
ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS}
;;
*.framework)
echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
echo "rsync -av $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
rsync -av "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
;;
*.xcdatamodel)
echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\""
xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom"
;;
*.xcdatamodeld)
echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\""
xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd"
;;
*.xcmappingmodel)
echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\""
xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm"
;;
*.xcassets)
echo "all xcassets will compile later!"
;;
*)
echo "$RESOURCE_PATH"
echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY"
;;
esac
}
install_project_resouces()
{
PROJECT_RESOURCE_DIR="${PROJECT_DIR}/../$1"
if [[ ! -e "${PROJECT_RESOURCE_DIR}" ]]; then
cat << EOM
error: PROJECT_RESOURCE_DIR "${PROJECT_RESOURCE_DIR}" not found
EOM
exit 1
fi
echo "copy resources in ${PROJECT_RESOURCE_DIR} to ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
ALL_RESOURCES=()
FIND_ALL_RESOURCES=$(find "$PROJECT_RESOURCE_DIR" -iname "*.xcassets" -o -iname "*.xib" -o -iname "*.storyboard" -o -iname "*.plist" ! -iname "Info.plist")
while read line; do
ALL_RESOURCES+=("$line")
done << "$RESOURCES_TO_COPY"
case "${TARGETED_DEVICE_FAMILY}" in
1,2)
TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone"
;;
1)
TARGET_DEVICE_ARGS="--target-device iphone"
;;
2)
TARGET_DEVICE_ARGS="--target-device ipad"
;;
3)
TARGET_DEVICE_ARGS="--target-device tv"
;;
*)
TARGET_DEVICE_ARGS="--target-device mac"
;;
esac
for i in ${ALL_RESOURCES[@]}; do
install_resource "${i}"
done
mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then
mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
fi
rm -f "$RESOURCES_TO_COPY"
}
for module in ${MODULES}; do
install_project_resouces "${module}"
done
XCASSETS_SEARCH_DIR="${PROJECT_DIR}/.."
XCASSET_FILES=()
if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ]
then
# Find all other xcassets (this unfortunately includes those of path pods and other targets).
ALL_XCASSETS=$(find "$XCASSETS_SEARCH_DIR" -iname "*.xcassets" -type d)
while read line; do
if [[ $line != "${PODS_ROOT}*" ]]; then
XCASSET_FILES+=("$line")
fi
done <<<"$ALL_XCASSETS"
echo "compile all xcassets: ${XCASSET_FILES[@]}"
printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
fi
echo "all done!"
四、一點小體悟
本部落格所討論的,只是從工程結構上實現模組化與元件化的方式,一個公司可能會有很多個App產品,但其中一定有某些基礎模組是可以複用的,除了進行靜態庫封裝或動態庫封裝外,進行並列工程化也是一種很好的選擇,這樣可以同步開發,迭代更快。除了公用的模組,還有一些模組可能並不公用但是確可以獨立開發,例如資訊類專案中可能會有使用者模組,社交模組和內容模組,將這些拆分為專案內的子工程可以使專案的結構更加清晰,模組化測試也更容易進行。
最後,僅僅專案結構上的模組化遠遠達不到真正實現元件化專案的要求,遵守協議為標準,以函數語言程式設計為方式,全域性著眼的介面設計與路由規劃,良好的程式設計習慣與統一的程式碼風格,這種程式碼層面的專案開發管理才真正任重道遠。後面有時間我會陸續通過其他部落格來探討這些問題。希望一起交流,共同學習!