作者 | 收纳箱,绿洲iOS研发工程师,绿洲ID:收纳箱KeepFit

0.序言

之前的文章《我是如何让微博绿洲的启动速度提升30%的》收到了很多朋友的反馈。

其中,动态库转静态库的收益相比于二进制重排收益更大,但在实际操作中大家也遇到了一些问题。

本着装完B就跑,自己装的B,跪着也要装完的原则,在这里我详细来讲一讲这些问题。

1. 修改Mach-O Type到底改变了什么?

我们先来看看动态库。这里我做了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分开存放。

• 静态库会和主工程的Mach-O合并在一起。

2. 静态库可能带来的问题

之前我们看到静态库会和主工程的Mach-O合并在一起,这会引起什么问题呢?

• 符号冲突

• Bundle的获取

2.1 符号冲突

回顾下 -ObjC 、 -all_load 、-force_load这三个flag的区别:

• -ObjC 链接器会加载静态库中所有的Objective-C类和Category;(导致可执行文件变大)

• -all_load 链接器会加载静态库中所有的Objective-C类和Category(这里和上面一样);当静态库只有Category时 -ObjC会失效,需要使用这个flag;

• -force_load 加载特定静态库的全部类,与 -all_load类似但是只限定于特定静态库,所以 -force_load需要指定静态库;当两个静态库存在同样的符号时,使用 -all_load会出现 duplicate symbol的错误,此时可以根据情况选择将其中一个库 -force_load。

我们在Pod1库中复制一份Pod2Object.{h,m},同时在Build Settings中的Other Linker Flags中添加 -all_load。

先执行Clean Build Folder(或⇧+⌘+K),然后再⌘+B进行构建,这时就会出现duplicate symbols报错:

解决办法:
任意一个或者都不使用静态库。虽然这么说,其实这也是不安全的。如果能改名字就改一下吧。

2.2 Bundle的获取

我们在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。

2.3 CocoaPods

我们在执行了pod install之后,CocoaPods会在主工程的Build Phase添加一个 [CP] Embed Pods Frameworks脚本:

这个脚本会在Build之后执行。我们之前静态化后,把三方库install_framework相关的代码注释(或者删除)了,来解决Archive之后在Organizer中尝试Validate App时会报错的问题:

其实,这个操作过于简单粗暴,会导致资源文件的丢失。

之前三方库中资源文件较少,没有发现这个问题,感谢大家的提醒。

我们看仔细看一下install_framework到底是干嘛的。

# Copies and strips a vendored frameworkinstall_framework(){# 设置source变量,三方库构建之后的路径if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; thenlocal source="${BUILT_PRODUCTS_DIR}/$1"elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; thenlocal source="${BUILT_PRODUCTS_DIR}/$(basename "$1")"elif [ -r "$1" ]; thenlocal source="$1"fi

# 设置destination变量,三方库需要移动到的路径local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"

# 判断source是否为链接文件,需要指向原来的文件if [ -L "${source}" ]; thenecho "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-Olocal basename  basename="$(basename -s .framework "$1")"  binary="${destination}/${basename}.framework/${basename}"

if ! [ -r "$binary" ]; then    binary="${destination}/${basename}"elif [ -L "${binary}" ]; thenecho "Destination binary is symlinked..."    dirname="$(dirname "${binary}")"    binary="${dirname}/$(readlink "${binary}")"fi

# 去掉无效的架构# Strip invalid architectures so "fat" simulator / device frameworks work on deviceif [[ "$(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 ]; thenlocal 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; doecho "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\""      rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}"      code_sign_if_enabled "${destination}/${lib}"donefi}

把这部分注释了,相当于说不会把构建好的 *.framework包复制到App的Frameworks文件夹下,自然 *.framework中的资源文件也就丢失了。

现在问题已经明了了:

• 静态化会导致Bundle变为Main Bundle。

• 资源没有从 *.framework中转移到App中。

解决办法:

既然现在拿到的Bundle是Main Bundle,我们构建之后利用脚本把资源拷贝到App文件夹下不就好了。

install_framework_bundle(){# 设置source变量,三方库构建之后的路径if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; thenlocal source="${BUILT_PRODUCTS_DIR}/$1"elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; thenlocal source="${BUILT_PRODUCTS_DIR}/$(basename "$1")"elif [ -r "$1" ]; thenlocal 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"fiif [[ "$CONFIGURATION" == "Release" ]]; then  install_framework_bundle "${BUILT_PRODUCTS_DIR}/Pod1/Pod1.framework"  install_framework_bundle "${BUILT_PRODUCTS_DIR}/Pod2/Pod2.framework"fi

我们来对比一下:

现在资源都能正确访问了。

// Pod1Object@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 ]; thenecho "Found conflicts:"for var in ${conflict_list[@]}doecho $vardoneexit 1fi}

如果资源重名,可能就没方法静态化了。

如果三方库代码写得不好,可能发生崩溃。
如果没有发生崩溃,代码行为可能受到影响。

3. 动态库和静态库的选择

虽然这是一个老生常谈的问题了,这里既然在讨论静态库和动态库就简单说一下。

iOS平台上规定不允许存在动态库,同时在iOS8之前因为App都是运行在沙盒当中,不同的程序之间不能共享代码:

• iOS是单进程的,就算使用了动态库,也没有可以共享代码的对象。

• 动态下载代码是被苹果明令禁止的,也没法发挥出动态库的优势。(如果你不需要上架App Store倒是可以使用)

综上,所以上动态库也就没有存在的必要了。

iOS8之后,iOS有了App Extesion特性。由于iOS主App和Extension需要共享代码,于是苹果后来提出了Embedded Framework。这种动态库允许App和App Extension共享代码,但是这份动态库的作用范围被限定在一个App进程内,且需要拷贝到目标程序中。

简单点可以理解为被阉割的动态库:因为系统的动态库是不需要拷贝到目标程序中,且可以被多个进程使用;而我们的动态库(Embedded Framework)没有这么大的能力。

建议:

如果程序使用了App Extesion,且主工程和Extension使用了相同的三方库:

• 可以使用动态库来节约内存,减少包的大小。

• 如果涉及的库较多,又想提升启动速度,可以考虑合并多个动态库,减少动态库的数量。

还有什么问题欢迎大家提出来~

推荐阅读

• 海外开发者账号上架总结• 基于桥的全量方法 Hook 方案- 开源 TrampolineHook• 我是如何让微博绿洲的启动速度提升30%的• 微软是如何适配 Dark Mode 的?

就差您点一下了 ???

pyspider all 只启动了_我是如何让微博绿洲的启动速度提升30%的(二)相关推荐

  1. pyspider all 只启动了_【麦芽口腔】全民爱牙月正式启动!

    每天2次正确刷牙 每年2次定期检查.洁牙 你做到了吗? 今年9月20日 是我国第32个"全国爱牙日" 9月5日 麦芽口腔"全民爱牙月" 系列活动正式启动! 我们 ...

  2. pyspider all 只启动了_Python 爬虫:Pyspider 安装与测试

    一次性付费进群,长期免费索取教程,没有付费教程. 进微信群回复公众号:微信群:QQ群:460500587  教程列表 见微信公众号底部菜单 |  本文底部有推荐书籍  微信公众号:计算机与网络安全 I ...

  3. win7关闭开机启动项_老司机给你传授 win7系统设置开机不启动360安全卫士只启动软件小助手的图文教程 -win7系统使用教程...

    win7旗舰版是用户量最大的一款操作系统:有不少人在使用中都遇见了win7系统设置开机不启动360安全卫士只启动软件小助手的问题,太多的用户是不想看到win7系统设置开机不启动360安全卫士只启动软件 ...

  4. 关于如何实现程序一天只启动一次的想法(C++实现)

    问题描述: 我们在程序开发当中,经常会遇到某些子程序需要实现一天只启动一次的功能,该功能实现的方法有很多种,其原理都是通过记录标记为来实现的.本次要分享的也是利用程序标记为来实现的,而且只需要使用一个 ...

  5. 【Android UI设计与开发】3.引导界面(三)实现应用程序只启动一次引导界面

    大部分的引导界面基本上都是千篇一律的,只要熟练掌握了一个,基本上也就没什么好说的了,要想实现应用程序只启动一次引导界面这样的效果,只要使用SharedPreferences类,就会让程序变的非常简单, ...

  6. linux 检查mps版本,linux_mps启动流程_存储相关.doc

    linux_mps启动流程_存储相关 Linux-mips启动流程 -存储相关 linux内核启动的第一个阶段是从 /arch/mips/kernel/head.s文件开始的.而此处正是内核入口函数k ...

  7. win10pe  win10pe Nvme 启动盘_大白菜 uefi_   什么是UEFI启动

    win10pe  win10pe Nvme 启动盘_ 大白菜官网,大白菜winpe,大白菜U盘装系统, u盘启动盘制作工具 uefi_ 什么是UEFI启动? == win10pe 可以下载 大白菜 . ...

  8. excel diy工具箱_我是工具控:excel最酷工具箱 — 方方格子

    EXCEL好学吗? " 只需99元10天包你学会VBA,提升10倍工作效率 " " 一节9.9元,45分钟的Excel专业课程,数据分析从此脱胎换骨!" &qu ...

  9. C#上位机基础学习_基于SOCKET实现与PLC服务器的TCP通信(二)

    C#上位机基础学习_基于SOCKET实现与PLC服务器的TCP通信(二) 测试软件: TIA PORTAL V15.1 S7-PLCSIM ADVANCED V3.0 Visual Studio 20 ...

最新文章

  1. 用计算机计算教学反思,《用计算器计算》教学反思
  2. 中国电信的新媒体营销尝试
  3. 个人博客前端模板_腾讯前端开发工程师,教你极速搭建一个个人博客网站
  4. jpa 查询 列表_终极JPA查询和技巧列表–第1部分
  5. oracle+行换列,Oracle的数据表中行转列与列转行的操作实例讲解
  6. kali修改root密码
  7. 帝国cms 6.6 采集入库多记录时出现空白 解决办法
  8. 读取jar包所在目录和jar包内文件
  9. SqlServer Alwayson 搭建报错:19405
  10. rem和mod的区别
  11. java面试| 精选基础题(1)
  12. 2017年云南职称计算机考试,云南省2017年职称计算机考试内容及考试方式
  13. 使用python打开多台IMAGINGSOURCE工业相机
  14. Qt QLabel的修改形状显示圆形
  15. NetDxf读取DXF文件
  16. 将mac打造成和linux差不多的c语言开发环境,完全新手版
  17. 【SQLServer】用SQL语句更改数据库名,表名,列名
  18. 雾计算中的数据安全问题综述
  19. rpm安装Mysql的rpm包,提示/bin/sh is needed by MySql.rpm 错误的问题解决
  20. java调用java程序,详细说明

热门文章

  1. eclipse build慢问题
  2. EasyUI-dialog
  3. Innodb内核线程并发机制
  4. 快速通道30秒申请QQ!
  5. Javascript学习历程之事件
  6. [转] new 和delete
  7. 洛谷P2698 花盆Flowerpot【单调队列】
  8. LOJ#6284. 数列分块入门 8
  9. 二月草的博客开通啦……
  10. windows程序窗体创建流程模型A--利用基本数据类型