之前的文章《我是如何让微博绿洲的启动速度提升30%的》收到了很多朋友的反馈。
其中,动态库转静态库的收益相比于二进制重排收益更大,但在实际操作中大家也遇到了一些问题。
本着装完B就跑,自己装的B,跪着也要装完的原则,在这里我详细来讲一讲这些问题。
我们先来看看动态库。这里我做了2个库Pod1和Pod2:
Podfile文件中配置了use_frameworks!
,然后进行pod install
,这样生成的就是动态库。
要怎么确定这个是动态库呢?
首先,这个库的Mach-O Type是动态库。
执行⌘+B构建之后,我们还是来到Products文件中的app:
在生成的Demo.app文件包上面点右键,选择显示包内容:
打开Framewoks文件夹,我们可以看到里面有我们创建的两个动态Pod1.framework和Pod2.framework。文件夹里面有代码签名、资源、Info.plist、Pod1(Mach-O)、bundle。
也就是说,如果我们使用的是动态库,在Framewoks文件夹就会看到它的身影,同时主工程的Mach-O文件中是没有相关的代码的。
下面我们修改Build Settings中的Mach-O Type,将其设置为静态库Static Library。
同时按照上一篇文章说的,删除Pods-Demo-frameworks.sh中install_framework相关的部分:
先执行Clean Build Folder(或⇧+⌘+K),然后再⌘+B进行构建。完成之后,我们还是来打开Demo.app文件包:
这次我们发现,Framewoks文件夹是空的!我们再看看主工程的Mach-O文件:
我们看到我们在两个库中创建的类Pod1Object
和Pod2Object
来到了主工程的Mach-O文件中!
现在应该明白了:
之前我们看到静态库会和主工程的Mach-O合并在一起,这会引起什么问题呢?
回顾下 -ObjC 、 -all_load 、-force_load这三个flag的区别:
我们在Pod1库中复制一份Pod2Object.{h,m},同时在Build Settings中的Other Linker Flags中添加 -all_load。
先执行Clean Build Folder(或⇧+⌘+K),然后再⌘+B进行构建,这时就会出现duplicate symbols报错:
解决办法:
任意一个或者都不使用静态库。虽然这么说,其实这也是不安全的。如果能改名字就改一下吧。
我们在Pod1Object
和Pod2Object
中添加以下方法:
- (nullable NSBundle *)getBundle { return [NSBundle bundleForClass:[self class]]; } 复制代码
再在主工程的ViewController
中添加:
- (void)viewDidLoad { [super viewDidLoad]; NSBundle *main = [NSBundle mainBundle]; NSBundle *pod1 = [[Pod1Object new] getBundle]; NSBundle *pod2 = [[Pod2Object new] getBundle]; NSLog(@"%@", main); NSLog(@"%@", pod1); NSLog(@"%@", pod2); } 复制代码
我们先看一下动态库的情况:
我们看到Main Bundle是我们的App,而我们的Pod1 Bundle和Pod2 Bundle分别是其对应的framework,类似于它们有自己的沙盒。
我们再来看看静态库:
可以看到3个Bundle都变成了我们的Main Bundle!
这是因为静态库被合并到了主工程Mach-O文件中:
[NSBundle bundleForClass:[self class]]; 复制代码
[self class]
现在在主工程的Mach-O中,那么上面找到的自然是主工程的Bundle,即Main Bundle。
这个问题解决起来比符号冲突简单一些,但解决这个问题前,我要先讲一下CocoaPods。
我们在执行了pod install
之后,CocoaPods会在主工程的Build Phase添加一个 [CP] Embed Pods Frameworks脚本:
这个脚本会在Build之后执行。我们之前静态化后,把三方库install_framework相关的代码注释(或者删除)了,来解决Archive之后在Organizer中尝试Validate App时会报错的问题:
其实,这个操作过于简单粗暴,会导致资源文件的丢失。
之前三方库中资源文件较少,没有发现这个问题,感谢大家的提醒。
我们看仔细看一下install_framework到底是干嘛的。
# Copies and strips a vendored framework install_framework() { # 设置source变量,三方库构建之后的路径 if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then local source="${BUILT_PRODUCTS_DIR}/$1" elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" elif [ -r "$1" ]; then local source="$1" fi # 设置destination变量,三方库需要移动到的路径 local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" # 判断source是否为链接文件,需要指向原来的文件 if [ -L "${source}" ]; then echo "Symlinked..." source="$(readlink "${source}")" fi # rsync --delete无差异同步,可以简单理解为网盘同步,或者复制 # 想详细了解rsync,可以在命令行中输入man rsync # 这里相当于把source的文件(文件夹)同步到destination # 即把*.framework复制到Frameworks文件夹下 # Use filter instead of exclude so missing patterns don't throw errors. echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" # 下面是找到二进制文件,即framework的Mach-O local basename basename="$(basename -s .framework "$1")" binary="${destination}/${basename}.framework/${basename}" if ! [ -r "$binary" ]; then binary="${destination}/${basename}" elif [ -L "${binary}" ]; then echo "Destination binary is symlinked..." dirname="$(dirname "${binary}")" binary="${dirname}/$(readlink "${binary}")" fi # 去掉无效的架构 # Strip invalid architectures so "fat" simulator / device frameworks work on device if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then strip_invalid_archs "$binary" fi # 进行代码签名 # Resign the code if required by the build settings to avoid unstable apps code_sign_if_enabled "${destination}/$(basename "$1")" # Swift的运行时库,Xcode 7之后就用不到了,可以不管 # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then local swift_runtime_libs swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) for lib in $swift_runtime_libs; do echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" code_sign_if_enabled "${destination}/${lib}" done fi } 复制代码
把这部分注释了,相当于说不会把构建好的 *.framework包复制到App的Frameworks文件夹下,自然 *.framework中的资源文件也就丢失了。
现在问题已经明了了:
解决办法:
既然现在拿到的Bundle是Main Bundle,我们构建之后利用脚本把资源拷贝到App文件夹下不就好了。
install_framework_bundle() { # 设置source变量,三方库构建之后的路径 if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then local source="${BUILT_PRODUCTS_DIR}/$1" elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" elif [ -r "$1" ]; then local source="$1" fi # 设置destination变量,三方库需要移动到的路径 local destination="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" # 遍历framework下的文件,找到bundle和图片,有其他资源自己改一下 for filename in `ls ${source} | grep ".*\.bundle\|.*\.jpg\|.*\.jpeg\|.*\.png"` do full_path=${source}/${filename} # 把资源同步到Main Bundle中 rsync -abrv --suffix .conflict "${full_path}" "${destination}" done } 复制代码
现在我们的操作就是把被静态化的三方库从install_framework方法改为install_framework_bundle:
if [[ "$CONFIGURATION" == "Debug" ]]; then install_framework_bundle "${BUILT_PRODUCTS_DIR}/Pod1/Pod1.framework" install_framework_bundle "${BUILT_PRODUCTS_DIR}/Pod2/Pod2.framework" fi if [[ "$CONFIGURATION" == "Release" ]]; then install_framework_bundle "${BUILT_PRODUCTS_DIR}/Pod1/Pod1.framework" install_framework_bundle "${BUILT_PRODUCTS_DIR}/Pod2/Pod2.framework" fi 复制代码
我们来对比一下:
现在资源都能正确访问了。
// Pod1Object @implementation Pod1Object - (nullable NSBundle *)getBundle { return [NSBundle bundleForClass:[self class]]; } - (nullable NSBundle *)getResourceBundle { NSBundle *bundle = [self getBundle]; return [NSBundle bundleWithPath:[bundle pathForResource:@"image1" ofType:@"bundle"]]; } @end // Pod2Object @implementation Pod2Object - (nullable NSBundle *)getBundle { return [NSBundle bundleForClass:[self class]]; } - (nullable NSBundle *)getResourceBundle { NSBundle *bundle = [self getBundle]; return [NSBundle bundleWithPath:[bundle pathForResource:@"image" ofType:@"bundle"]]; } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSBundle *pod1 = [[Pod1Object new] getResourceBundle]; NSBundle *pod2 = [[Pod2Object new] getResourceBundle]; UIImage *image1 = [[UIImage alloc] initWithContentsOfFile:[pod1 pathForResource:@"icon121" ofType:@"png"]]; UIImageView *imageView1 = [[UIImageView alloc] initWithFrame:CGRectMake(0, 100, 100, 100)]; imageView1.contentMode = UIViewContentModeCenter; [self.view addSubview:imageView1]; imageView1.image = image1; UIImage *image2 = [[UIImage alloc] initWithContentsOfFile:[pod2 pathForResource:@"icon120" ofType:@"png"]]; UIImageView *imageView2 = [[UIImageView alloc] initWithFrame:CGRectMake(0, 200, 100, 100)]; imageView2.contentMode = UIViewContentModeCenter; [self.view addSubview:imageView2]; imageView2.image = image2; } @end 复制代码
注意:
install_framework_bundle中,我没有处理重名问题。
-b --suffix .conflict会把重名文件添加后缀 .conflict,这个后缀是可配的。
处理完你可以用find扫一遍App文件夹,看一下有没有重名的资源被 .conflict标记出来。
check_conflict() { local destination="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" conflict_list=`find ${destination} -regex '.*\.conflict'` conflict_list=(${conflict_list/ /}) count=${#conflict_list[*]} if [ $count -gt 0 ]; then echo "Found conflicts:" for var in ${conflict_list[@]} do echo $var done exit 1 fi } 复制代码
如果资源重名,可能就没方法静态化了。
- 如果三方库代码写得不好,可能发生崩溃。
- 如果没有发生崩溃,代码行为可能受到影响。
还有什么问题欢迎大家提出来~