传统方式的问题 进行iOS静态库的开发和维护时,经常有这样的操作:每次SDK的代码修改完之后,需要在SDK工程中构建得到静态库,再将静态库拷贝至测试App工程(测试App用于测试SDK的功能)的目录下,再进行测试App的构建。这样做有2个明显的缺点: 1、不能针对SDK进行断点调试,如果要得知一些运行时的变量值或者观察程序运行流程时就需要添加很多打印信息,从控制台的运行结果中来查看。 2、产生很多不必要的重复劳动。
明确需求 做为SDK的开发人员,其实最想要的,就是能有这么一个工具,能在修改完SDK的代码后做到如下2点: 1、执行一次手动操作就能在测试App中对SDK进行验证,不需要额外手动执行静态库的构建和拷贝工作。 2、在运行测试App时能够针对SDK进行断点调试。
解决方案 1、关联工程 研究后发现,Xcode的workspace其实就具有这样的功能,它的功能就是将多个project放到同一个工作空间里进行关联。在同一个workspace下,scheme可以将各个project中的target都添加进来。workspace、project、scheme、target的关系可以用下图表示:
假设当前场景下进行SDK的开发和维护需要2个工程: (1)1个测试App工程。 (2)1个SDK工程。 我们将测试App工程和SDK工程添加到同一个workspace中。具体操作步骤如下: (1)新建workspace,Xcode顶部菜单File -> New -> Workspace。 (2)在workspace左侧资源管理页,右键 -> Add Files To “xx”…,选择要关联的SDK工程文件和测试App工程文件。 (3)新建scheme,添加构建测试App的target,并将scheme的归属(Container)设置为workspace。
到此为止,SDK和测试App的关联就完成了,之后进行开发和维护使用第3步新建的scheme即可。进行测试App的构建时,Xcode会自动检测App中使用到的静态库,如果SDK工程的成果物就是测试工具所需的静态库,那么会同时进行SDK的构建,并将SDK构建后的成果物链接到测试工具中,形成成果物之间的隐式依赖关系。
2、配置SDK的编译链接选项 经过测试发现,测试App和SDK的debug、release选项会跟随当前所执行的scheme中选定的配置,所以要实现断点调试,就需要在scheme中配置debug模式,同时SDK的工程配置也要做相应配合。为了实现debug模式下能对SDK进行断点调试,并保证release模式下的代码保密性,在SDK的编译链接选项配置就很有讲究,正常情况下按照下面表格中的内容进行配置。
选项名称
含义
Debug
Release
Generate Debug Symbols
是否生成Debug符号
开启
关闭
Debug Information Level
调试信息等级
设置为Compiler default
Line tables only(设置无效)
Deployment Postprocessing
是否开启后处理,下面3个选项的总开关
关闭
开启
Strip Linked Product
是否剥离依赖库的符号
关闭
开启
Strip Style
剥离符号的等级
默认为Debugging Symbols(设置无效)
设置为Non-Global Symbols
Strip Debug Symbols During Copy
是否剥离Debug符号
关闭
开启
根据上表的选项进行配置,既可以保证在Debug模式下能够对SDK进行断点调试,又可以保证以Release模式构建得到的成果物的内部细节不会暴露给外界调用方,还大大减少了Release包的体积,去掉了其中许多无用的内容。
3、使用shell脚本完成静态库的构建和替换 经过测试发现,workspace中使用SDK工程自动构建出来的静态库对于开发者来说是不可见的,并且不会将测试App目录下的SDK进行文件意义上的替换。所以要完成拷贝操作,我们需要首先自行构建出静态库,然后执行拷贝操作,这个操作可以使用shell脚本来完成。具体流程如下图所示:
脚本如何执行,可以有2种做法,推荐使用第2种做法: (1)一种做法就是在SDK的project中添加一个aggregate类型的target,该类型的target可以让我们在其中添加shell语言编写的脚本,将构建和拷贝的操作作为一个target,添加到scheme中,可以与其他target并行执行。 (2)另一种做法是将脚本放在workspace独有的scheme中,作为scheme的pre-actions或post-actions,与App工程和SDK工程区分开来。从项目管理的角度分析,这样的做法更加合乎逻辑,因为App工程和SDK工程原本就是能够独立构建成果物的,是workspace将这两者关联在了一起,因此一些额外的操作放在workspace的scheme中更加合适。
最后贴一下脚本代码吧
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 #!/bin/sh #------------------------------iOS静态库批量构建脚本------------------------------ #------------------------------自定义变量配置------------------------------ # 项目根路径 SOURCE_PROJECT_PATH="/Users/xxx/xxx/xxx" # 需要打包的工程路径,全部是绝对路径,路径必须到.xcodeproj # 如果该工程有多个Target,需要指定一个特定的Target来编译,在路径后面加(两个下划线)__Target名称 # 例如:xxx/projectName.xcodeproj__TargetName PROJECT_PATH_ARRAY=( "${SOURCE_PROJECT_PATH}/xxx.xcodeproj" \ ) # 需要将SDK输出的路径 SDK_OUTPUT_PATH_ARRAY=( "${SOURCE_PROJECT_PATH}/测试App存放静态库的路径" \ ) # 当前静态库的名称(xxx.a) CURRENT_FULL_PRODUCT_NAME="" # 当前target的名称 CURRENT_TARGET_NAME="" #------------------------------路径配置------------------------------ # 编译后成果物的根路径,此路径默认是桌面路径,可以自己指定对应的路径 ROOT_PATH="${HOME}/Desktop" # 获取开始时间 start_seconds=$(date +%s); CURRENT_DATE=`date +%Y-%m-%d_%H-%M-%S` # 编译后所有成果物的存放路径 ROOT_BUILD_PATH="${ROOT_PATH}/MobilePlayControl-${CURRENT_DATE}" # 编译过程产生的日志文件的存放路径 LOG_DIR="${ROOT_BUILD_PATH}/Build_Log" # 当前库编译成果的路径 CURRENT_BUILD_PATH="" # BuildSetting导出的临时目录 TMP_BUILDSETTING_DIR="${ROOT_BUILD_PATH}/TMP_BuildSetting" # 编译和打包产生结果的临时输出目录(若编译时没配置OBJROOT,会包含OBJROOT的内容) TMP_SYMROOT="${ROOT_BUILD_PATH}/TMP_result" # 产生.a和.hmap文件的临时目录 TMP_OBJROOT="${ROOT_BUILD_PATH}/TMP_object" #------------------------------编译选项配置------------------------------ # 将PROJECT_PATH_ARRAY配置的地址【全部】Build出来的SDK所支持架构,设置0或者1 # 0:支持真机和模拟器;1:只支持真机 BUILD_SUPPORT_PLATFORM=1 # build类型 有Release和Debug两种选项 BUILD_TYPE="Release" #------------------------------函数定义------------------------------ # 创建文件路径 # 清除某个目录里面的内容,如果有则清除内容,没有的直接创建该目录 # 参数1:目录 clearDirAll(){ if [ ! -d $1 ]; then mkdir -p $1 else # 先删除,再创建 rm -rf $1 mkdir -p $1 fi return 0 } # 创建Build根目录 clearDirAll ${ROOT_BUILD_PATH} clearDirAll ${TMP_BUILDSETTING_DIR} clearDirAll ${LOG_DIR} # 合并真机和模拟器 # 参数1:当前创建的Build目录 mergeSDK(){ # 1.找到真机和模拟器路径 tmpIphonesPath="${CURRENT_BUILD_PATH}/${BUILD_TYPE}-iphoneos" tmpIphonesimulatorPath="${CURRENT_BUILD_PATH}/${BUILD_TYPE}-iphonesimulator" tmpSDKName="" # 2.获取当前SDK名称 for file in ${tmpIphonesPath}/* do # 拿到SDK文件名称 tmpName=`basename ${file}` if [[ $tmpName =~ $CURRENT_FULL_PRODUCT_NAME ]];then tmpSDKName=${tmpName} fi done # 3.根据BUILD_SUPPORT_PLATFORM配置项判断Build模拟器还是真机 if [ $BUILD_SUPPORT_PLATFORM -eq 0 ]; then # 支持真机和模拟器 # 判断当前的SDK时.a类型的还是.framework类型的,并且各自合并 tmpAStr=".a" # 当前是.a形式的SDK tmpFStr=".framework" # 当前是.framework形式的SDK if [[ $tmpSDKName =~ $tmpFStr ]]; then # 获取.framework文件的名称 tmpFrameWorkName=${tmpSDKName%.*} # 将真机模式下的framework拷贝一份到根目录下 cp -r ${tmpIphonesPath}/${tmpSDKName} $1/${tmpSDKName} # 合并.framework中的二进制文件,输出至之前拷贝到根目录下的.framework当中 lipo -create "${tmpIphonesPath}/${tmpSDKName}/${tmpFrameWorkName}" "${tmpIphonesimulatorPath}/${tmpSDKName}/${tmpFrameWorkName}" -output "$1/${tmpSDKName}/${tmpFrameWorkName}" elif [[ $tmpSDKName =~ $tmpAStr ]]; then # 合并.a lipo -create "${tmpIphonesPath}/${tmpSDKName}" "${tmpIphonesimulatorPath}/${tmpSDKName}" -output "$1/${tmpSDKName}" fi elif [ $BUILD_SUPPORT_PLATFORM -eq 1 ]; then # 只支持真机 # 如果只支持真机,就直接将真机目录下的SDK拷贝到根目录下就可以 cp -r ${tmpIphonesPath}/${tmpSDKName} ${CURRENT_BUILD_PATH}/${tmpSDKName} fi # 4.将include中的文件(.h和资源文件)拷贝到.a文件的同级目录下 includePath="${tmpIphonesPath}/include/${CURRENT_TARGET_NAME}" cp -rf ${includePath}/* ${CURRENT_BUILD_PATH} # find $1 -maxdepth 1 -type d -name "*.h" -exec rm -rf {} \; # find ${tmpIphonesPath} -maxdepth 1 -type f -name "*.h" -exec mv -f {} $1 \; # 5.移除iphones目录和iphonesimulator目录 rm -rf "${tmpIphonesPath}" rm -rf "${tmpIphonesimulatorPath}" } # .a库打包方法,接收两个参数 # 参数1:工程路径,精确到xxx.xcodeproj # 参数2:TARGET名称 buildLibrary(){ if [ -n $1 ]; then if [ -n $2 ]; then # 创建每个.a的Build路径 CURRENT_BUILD_PATH="${ROOT_BUILD_PATH}/$2" currentResultPath="${TMP_SYMROOT}/$2" currentObjectPath="${TMP_OBJROOT}/$2" echo "-----------------正在编译 $2-----------------" # 创建目录 clearDirAll ${CURRENT_BUILD_PATH} # 设置日志文件路径 logFilePath="${LOG_DIR}/$2-Build.log" # 根据BUILD_SUPPORT_PLATFORM配置项判断Build模拟器还是真机 if [ $BUILD_SUPPORT_PLATFORM -eq 0 ]; then # 支持真机和模拟器 echo "-----------------开始Build模拟器-----------------" >>${logFilePath} # 开始Build模拟器 xcodebuild -configuration "${BUILD_TYPE}" ONLY_ACTIVE_ARCH=NO -project "$1" -target "$2" SYMROOT="${currentResultPath}" OBJROOT="${currentObjectPath}" BUILD_DIR="${CURRENT_BUILD_PATH}" -sdk iphonesimulator clean build >>${logFilePath} echo "-----------------开始Build真机-----------------" >>${logFilePath} # 开始Build真机 xcodebuild -configuration "${BUILD_TYPE}" ONLY_ACTIVE_ARCH=NO -project "$1" -target "$2" SYMROOT="${currentResultPath}" OBJROOT="${currentObjectPath}" BUILD_DIR="${CURRENT_BUILD_PATH}" -sdk iphoneos clean build >>${logFilePath} elif [ $BUILD_SUPPORT_PLATFORM -eq 1 ]; then # 只支持真机 echo "-----------------开始Build真机-----------------" >>${logFilePath} # 开始Build真机 xcodebuild -configuration "${BUILD_TYPE}" ONLY_ACTIVE_ARCH=NO -project "$1" -target "$2" SYMROOT="${currentResultPath}" OBJROOT="${currentObjectPath}" BUILD_DIR="${CURRENT_BUILD_PATH}" -sdk iphoneos clean build >>${logFilePath} fi echo "-----------------$2 编译完成-----------------" # 3.合并真机和模拟器的SDK mergeSDK; # # 4.移除工程根目录下的build目录(build目录额外指定后,工程根目录下不会生成build目录) # tmpPath=$1 # projectPath=${tmpPath%/*} # %/*表示从头开始取到最后一个slash(/)之前的所有内容 # rm -rf "${projectPath}/build" else echo "Target不能为空" fi else echo "工程路径不能为空" fi } # 导出BuildSetting文件并且找出TARGET_NAME和PRODUCT_NAME环境变量的值 # param1:工程路径 # param2:TARGET名称,如果没有可以传nil readBuildSetting(){ # 1.清除全局变量的值 CURRENT_TARGET_NAME="" CURRENT_FULL_PRODUCT_NAME="" # 2.将工程工程对应Target的BuildSetting文件导出到本地 BuildSettingFilePath="${TMP_BUILDSETTING_DIR}/tmp_buildSetting.txt" if [ -n "$1" ]; then cmdStr="xcodebuild -list -project $1 -showBuildSettings >${BuildSettingFilePath}" if [ -n "$2" ]; then cmdStr="xcodebuild -list -project $1 -target $2 -showBuildSettings >${BuildSettingFilePath}" fi # 将命令中的括号进行字符替换,防止命令执行失败 cmdStr1=${cmdStr//"("/"\("} finalCommand=${cmdStr1//")"/"\)"} echo "命令:${finalCommand}" # 执行导出BuildSetting文件的命令 eval ${finalCommand} fi # 3.解析导出的BuildSetting文件,找出其中的TARGET_NAME和FULL_PRODUCT_NAME IFS='=' while read k v do if [[ "$k" == *FULL_PRODUCT_NAME* ]];then CURRENT_FULL_PRODUCT_NAME=$(echo $v | sed 's/[[:space:]]//g') elif [[ "$k" == *TARGET_NAME* ]];then CURRENT_TARGET_NAME=$(echo $v | sed 's/[[:space:]]//g') fi done < ${BuildSettingFilePath} echo "CURRENT_FULL_PRODUCT_NAME=${CURRENT_FULL_PRODUCT_NAME} CURRENT_TARGET_NAME=${CURRENT_TARGET_NAME}" # rm -rf "${BuildSettingFilePath}" } startBuild(){ # 1.遍历数组,根据路径截取到相应的工程路径以及工程名 for proPath in ${PROJECT_PATH_ARRAY[*]} do # 1.获取工程路径 projectPath=${proPath} targetName="" # 2.判断工程路径中是否包含__,如果包含了则说明指定了Target tmpStr="__" if [[ $proPath =~ $tmpStr ]] then projectPath=${proPath%__*} # 取到需要的Target名称 targetName=${proPath#*__} fi # # 3.判断targetName是否为空,如果为空则代表TargetName和工程名称相同 # if [ -z "${targetName}" ]; then # xcodeproj=${projectPath##*/} # projectName=${xcodeproj%.*} # targetName=${projectName} # fi # 4.读取TARGET_NAME和PRODUCT_NAME readBuildSetting ${projectPath} ${targetName} # 5.调用buildLibrary函数进行Build buildLibrary ${projectPath} ${CURRENT_TARGET_NAME} # 6.将编译好的库输出至指定目录 outputPath="${SDK_OUTPUT_PATH_ARRAY[i]}" sourcePath="${ROOT_BUILD_PATH}/${CURRENT_TARGET_NAME}" \cp ${sourcePath}/* ${outputPath} done } #------------------------------编译流程------------------------------ echo "-----------------开始构建-----------------" # 开始构建 startBuild; # 移除BuildSetting工作目录 rm -rf "${TMP_BUILDSETTING_DIR}" # 移除临时文件 rm -rf "${TMP_SYMROOT}" rm -rf "${TMP_OBJROOT}" # 移除编译成果物输出的路径 rm -rf "${ROOT_BUILD_PATH}" end_seconds=$(date +%s); echo "-----------------构建完成,耗时:$((end_seconds-start_seconds))s-----------------" # 打开编译后成果物的目录 #open ${ROOT_BUILD_PATH}