探索 iOS 编码对包大小的影响
source link: https://www.51cto.com/article/701465.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
本文讲述的技术点属于比较极致和新颖的包大小优化技术,文章会从二进制、汇编指令的层面来分析 oc 代码对包大小的影响。接下来会从以下三个方面进行讲述:
- 二进制层面分析 oc 代码对包大小的影响
- 编码上优化包大小的 tips
- 总结各种 tips 的收益
从二进制文件层面来分析编码对包大小影响
以分析属性为例子,介绍一种“从二进制文件层面来分析编码对包大小影响”的分析方法。
实验背景:用真机 iphone11,iOS13.5.1,release,build setting 默认设置,linkmap file 使用 arm64 进行实验。
.o 级别对比
通过对比 ViewControler 没有属性、有一个属性、两个属性的情况:
@interface ViewController : UIViewController
@property (nonatomic, strong) NSString *myString1;
@property (nonatomic, strong) NSString *myString2;
@end
interface ViewController : UIViewController@property (nonatomic, strong) NSString *myString1;@property (nonatomic, strong) NSString *myString2;@end
ViewController.o占用字节
- 没有属性 0.36k
- 一个属性 0.67k
- 两个属性 0.92k
当一个类减少一个属性,会减少 0.25k,这里有一个前提,这个类还有其他属性。
因此,我们可以得到一个结论:一个属性占用 0.25K。
查看 linkmap file 中的符号属于哪个 section 的 tips
# Sections:
# Address Size Segment Section
0x10000623C 0x00000224 __TEXT __text
0x100006460 0x00000090 __TEXT __stubs
__TEXT.__text 的地址范围是 0x10000623C - 0x100006460,那么 linkmap file 中的 Symbols 对应的地址,如果是在这个范围内,就属于__TEXT__.text。
0x10000623C 0x00000034 [ 1] -[ViewController viewDidLoad]
0x100006270 0x00000010 [ 1] -[ViewController myString1]
0x100006280 0x00000014 [ 1] -[ViewController setMyString1:]
0x100006294 0x00000010 [ 1] -[ViewController myString2]
0x1000062A4 0x00000014 [ 1] -[ViewController setMyString2:]
0x1000062B8 0x00000040 [ 1] -[ViewController .cxx_destruct]
0x1000062F8 0x00000008 [ 2] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100006300 0x0000008C [ 2] -[AppDelegate application:configurationForConnectingSceneSession:options:]
0x10000638C 0x00000004 [ 2] -[AppDelegate application:didDiscardSceneSessions:]
0x100006390 0x00000080 [ 3] _main
0x100006410 0x00000004 [ 4] -[SceneDelegate scene:willConnectToSession:options:]
0x100006414 0x00000004 [ 4] -[SceneDelegate sceneDidDisconnect:]
0x100006418 0x00000004 [ 4] -[SceneDelegate sceneDidBecomeActive:]
0x10000641C 0x00000004 [ 4] -[SceneDelegate sceneWillResignActive:]
0x100006420 0x00000004 [ 4] -[SceneDelegate sceneWillEnterForeground:]
0x100006424 0x00000004 [ 4] -[SceneDelegate sceneDidEnterBackground:]
0x100006428 0x00000010 [ 4] -[SceneDelegate window]
0x100006438 0x00000014 [ 4] -[SceneDelegate setWindow:]
0x10000644C 0x00000014 [ 4] -[SceneDelegate .cxx_destruct]
0x100006460 0x0000000C [ 5] _NSStringFromClass
0x10000623C 到 0x100006460 的符号都属于__TEXT.__text。
探索一个属性会产生哪些符号
将 section(节) size 不同拎出来,数字的单位:字节
__TEXT 段:
__DATA 段:
- $1 表示没有属性和一个属性的对比
- $2 表示一个属性和两个属性的对比
TEXT.text 分析
Linkmap File 差异:
// ViewController没有属性,差异点是没有-[ViewController .cxx_destruct]符号
// 一个属性: 16 + 20 + 20 = 56(与上方的 $1是对应上的)
0x100006318 0x00000010 [ 1] -[ViewController myString1]
0x100006328 0x00000014 [ 1] -[ViewController setMyString1:]
0x10000633C 0x00000014 [ 1] -[ViewController .cxx_destruct]
// 两个属性 16 + 20 + 16 + 20 +64 = 136
// 一个属性与两个属性比较 136 - 56 = 80(与上方的 $2是对应上的)
0x100006270 0x00000010 [ 1] -[ViewController myString1]
0x100006280 0x00000014 [ 1] -[ViewController setMyString1:]
0x100006294 0x00000010 [ 1] -[ViewController myString2]
0x1000062A4 0x00000014 [ 1] -[ViewController setMyString2:]
0x1000062B8 0x00000040 [ 1] -[ViewController .cxx_destruct]
疑惑点:.cxx_destruct的大小不一样,以下用两个截图回答这个问题,因为.cxx_destruct 内部的实现不一样,导致汇编的指令数量也不一样。
一个属性 .cxx_destruct内部实现的汇编代码:
两个属性 .cxx_destruct内部实现的汇编代码:
TEXT.objc_methname 分析
// 比较字节方法如上
// 一个属性
0x1000065FC 0x0000000A [ 1] literal string: myString1
0x100006606 0x0000000E [ 1] literal string: setMyString1:
0x100006614 0x0000000E [ 1] literal string: .cxx_destruct
0x100006622 0x0000000B [ 1] literal string: _myString1
// 两个属性
0x1000065A4 0x0000000A [ 1] literal string: myString1
0x1000065AE 0x0000000E [ 1] literal string: setMyString1:
0x1000065BC 0x0000000A [ 1] literal string: myString2
0x1000065C6 0x0000000E [ 1] literal string: setMyString2:
0x1000065D4 0x0000000E [ 1] literal string: .cxx_destruct
0x1000065E2 0x0000000B [ 1] literal string: _myString1
0x1000065ED 0x0000000B [ 1] literal string: _myString2
这里补充额外的一个点Dead Stripped Symbols相关的知识点:
// Text.text
0x1000062B8 0x00000040 [ 1] -[ViewController .cxx_destruct]
0x10000644C 0x00000014 [ 4] -[SceneDelegate .cxx_destruct]
// TEXT.objc_methname
0x1000065D4 0x0000000E [ 1] literal string: .cxx_destruct
<<dead>> 0x0000000E [ 4] literal string: .cxx_destruct
Dead Stripped Symbols的意思是.o 文件中不必要的符号去掉;
例如[ 4] literal string: .cxx_destruct就是不必要的符号,[ 4]是指SceneDelegate.o之所以不必要是因为对于objc_methname节来说,不需要有同名的符号。
// linkfile map内容,[ 4]指SceneDelegate.o
[ 4] /Users/xxxx/Library/Developer/Xcode/DerivedData/PacketSize-athynpqwkehwhtfsynewxvtjyvdz/Build/Intermediates.noindex/PacketSize.build/Release-iphoneos/PacketSize.build/Objects-normal/arm64/SceneDelegate.o
Xcode 中Dead Stripped Symbols相关的设置:
TEXT.objc_methtype
// 有NSString属性,有以下符号
0x1000073CF 0x00000008 [ 1] literal string: @16@0:8
0x1000073D7 0x0000000B [ 1] literal string: v24@0:8@16
0x1000073E2 0x0000000C [ 1] literal string: @"NSString"
// 同样Dead Stripped Symbols中也有@16@0:8、v24@0:8@16
TEXT.cstring 分析
// 一个属性 10 + 29 = 39
0x100007EE2 0x0000000A [ 1] literal string: myString1
0x100007EEC 0x0000001D [ 1] literal string: T@"NSString",&,N,V_myString1
// 两个属性
0x100007EAF 0x0000000A [ 1] literal string: myString1
0x100007EB9 0x0000001D [ 1] literal string: T@"NSString",&,N,V_myString1
0x100007ED6 0x0000000A [ 1] literal string: myString2
0x100007EE0 0x0000001D [ 1] literal string: T@"NSString",&,N,V_myString2
TEXT.objc_classname 分析
两个属性,比其他多占用了两个字节,目前还不知道这两个字节是怎么排列出来的,字节对齐?
0x100007322 0x0000000F [ 1] literal string: ViewController
0x100007331 0x00000002 [ 1] literal string:
0x100007333 0x0000000C [ 2] literal string: AppDelegate
0x10000733F 0x00000016 [ 2] literal string: UIApplicationDelegate
0x100007355 0x00000009 [ 2] literal string: NSObject
0x10000735E 0x0000000E [ 4] literal string: SceneDelegate
0x10000736C 0x00000016 [ 4] literal string: UIWindowSceneDelegate
0x100007382 0x00000010 [ 4] literal string: UISceneDelegate
0x100007392 0x00000002 [ 4] literal string:
DATA.objc_const 分析
没有属性
0x100008110 0x00000020 [ 1] __OBJC_$_INSTANCE_METHODS_ViewController
一个属性 40 + 24 + (104 - 32) = 136
0x100008178 0x00000028 [ 1] __OBJC_$_INSTANCE_VARIABLES_ViewController
0x100008110 0x00000068 [ 1] __OBJC_$_INSTANCE_METHODS_ViewController
0x1000081A0 0x00000018 [ 1] __OBJC_$_PROP_LIST_ViewController
两个属性 (152 - 104) + (72 - 40) + (40 - 24) = 96
0x100008110 0x00000098 [ 1] __OBJC_$_INSTANCE_METHODS_ViewController
0x1000081A8 0x00000048 [ 1] __OBJC_$_INSTANCE_VARIABLES_ViewController
0x1000081F0 0x00000028 [ 1] __OBJC_$_PROP_LIST_ViewController
DATA.objc_ivar 分析
一个属性
0x1000094B0 0x00000004 [ 1] _OBJC_IVAR_$_ViewController._myString1
两个属性
0x100009510 0x00000004 [ 1] _OBJC_IVAR_$_ViewController._myString1
0x100009514 0x00000004 [ 1] _OBJC_IVAR_$_ViewController._myString2
总结:通过以上的介绍,我们掌握了“从二进制文件层面分析编码对包大小的影响”的分析方法,用此方法我们可以继续分析“函数、direct、block”对包大小的影响。
编码上优化包大小的 Tips
属性动态化
探索对属性添加@dynamic 会对二进制文件产生的影响,探索方式跟上面一样,还是通过 link file map 的符号分析:
@interface ViewController()
// 对比myString1属性有无添加@dynamic修饰的区别
@property (nonatomic, copy) NSString *myString1;
@end
TEXT.text 分析
// 添加dynamic修饰没有以下部分
// 16 + 20 + 20 = 56
0x100006318 0x00000010 [ 1] -[ViewController myString1]
0x100006328 0x00000014 [ 1] -[ViewController setMyString1:]
0x10000633C 0x00000014 [ 1] -[ViewController .cxx_destruct]
说明添加了@dynamic修饰不会生成 getter、setter,并且没有实例变量,自然也不需要在dealloc中执行置 nil 的代码。
TEXT.objc_methname 分析
// 添加dynamic修饰没有以下部分
// 10 + 14 + 11 = 35
0x1000065FC 0x0000000A [ 1] literal string: myString1
0x100006606 0x0000000E [ 1] literal string: setMyString1:
0x100006622 0x0000000B [ 1] literal string: _myString1
说明添加了@dynamic修饰不会生成 getter、setter,并且没有实例变量,自然就不需要生成对应的常量符号。
TEXT.objc_methtype 分析
// 添加dynamic修饰没有下面的符号
// 但是在计算添加dynamic收益时,不能将这个计算入内,原因是一个项目一般都是这些符号的
0x1000073E2 0x0000000C [ 1] literal string: @"NSString"
TEXT.cstring 分析
一个属性
0x100007EEC 0x0000001D [ 1] literal string: T@"NSString",&,N,V_myString1
dynamic属性
0x100007EF9 0x00000013 [ 1] literal string: T@"NSString",&,D,N
【T@"NSString",&,N,V_myString1】和【T@"NSString",&,D,N】是属性类型字符串, property type stringapple 的官方文档中有这个介绍
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101 // 关于属性类型字符串介绍
DATA.objc_const 分析
一个属性
0x100008110 0x00000068 [ 1] __OBJC_$_INSTANCE_METHODS_ViewController
0x100008178 0x00000028 [ 1] __OBJC_$_INSTANCE_VARIABLES_ViewController
dynamic属性
0x100008110 0x00000020 [ 1] __OBJC_$_INSTANCE_METHODS_ViewController
(104 - 32) + 40 = 112
实例方法列表没有 myString1 的 getter、setter。
DATA.objc_ivar 分析
dynamic 属性没有以下符号
0x1000094B0 0x00000004 [ 1] _OBJC_IVAR_$_ViewController._myString1
没有实例变量,自然就没有_OBJC_IVAR_$_ViewController._myString1符号。
结论:用@dynamic优化一个属性,收益是(229 - 12)= 217B,12 是__objc_methtype 不能算进来;应用场景是对 model 类添加@dynamic。
方法调用、函数调用、direct 方法调用
实验方法:
// 对照组
#define Test // 先空实现
void func(ViewController *mSelf)
{
[mSelf method];
}
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self test];
}
- (void)method
{
NSLog(@"");
}
- (void)test
{
// 2000个 Test 宏的调用,注意,对照组的宏是空实现
}
// 方法调用 的 实验
// 修改Test宏
#define Test [self method];
// 函数调用 的 实验
#define Test func(self);
// direct方法调用 的 实验
#define Test [self method];
- (void)test __attribute__((objc_direct));
使用函数的汇编指令:
使用方法的汇编指令:
以上两个截图的汇编指令基本一样,说明在调用 func(self)函数时,因为函数内部实现过于简单,被编译优化了,直接优化为内敛函数。
修改 func 函数的实现,避开编译优化:
// 不影响调用逻辑的情况下,将函数实现改复杂点
void func(ViewController *mSelf)
{
if ([mSelf isKindOfClass:[ViewController class]]) {
[mSelf method];
}
}
另外还做了一组方法名长度不同的实验:
- 短方法名:method
- 长方法名:methodmethodmethodmethodmethodmethodmethodmethod
短方法名和长方法名对二进制大小的影响?
分别查看短方法名和长方法名的 TEXT.text
// 短方法名
0x100004560 0x00005FB4 __TEXT __text
0x1000045CC 0x00005DDC [ 1] -[ViewController test]
// 长方法名
0x100004534 0x00005FB4 __TEXT __text
0x1000045A0 0x00005DDC [ 1] -[ViewController test]
大小都是0x00005FB4
短方法名和长方法名对二进制大小的影响非常小,TEXT.text 的大小是一样的,原因是方法调用主要是操作函数指针,此时方法名长度对此没有影响;
那么主要影响是TEXT.objc_methname
// 短方法名
0x10000A664 0x00000D96 __TEXT __objc_methname
// 长方法名
0x10000A638 0x00000DC0 __TEXT __objc_methname
本实验中,函数调用和方法调用的区别?
先对比下方法调用和函数调用,在二进制上的区别:
// 函数和方法的区别
// 函数调用
0x100006448 0x000040CC __TEXT __text
// 方法调用
0x100004560 0x00005FB4 __TEXT __text
区别还是在 TEXT.text 上。
函数调用的汇编:
方法调用的汇编:
方法调用需要更多的指令,因此将方法调用改为函数调用,随着调用处越多,包大小的收益越大。
函数调用和 direct 方法调用的区别?
// 函数和direct的区别
// 函数调用
0x100006448 0x000040CC __TEXT __text
0x10000A664 0x00000D96 __TEXT __objc_methname
0x10000BF90 0x00000070 __TEXT __unwind_info
0x10000C0F0 0x00001358 __DATA __objc_const
0x10000D448 0x00000038 __DATA __objc_selrefs
0x10000D480 0x00000018 __DATA __objc_classrefs
// direct调用
0x1000064C4 0x00004060 __TEXT __text
0x10000A674 0x00000D8F __TEXT __objc_methname
0x10000BF9C 0x00000064 __TEXT __unwind_info
0x10000C0F0 0x00001340 __DATA __objc_const
0x10000D430 0x00000028 __DATA __objc_selrefs
0x10000D458 0x00000010 __DATA __objc_classrefs
// direct方法调用TEXT.text中没有[ViewController method]符号,但是当method方法内部的实现比较多时,就会有[ViewController method]符号;原因是method过于简单被编译器优化为内联了
// 因为method被内联了,所以__objc_selrefs少了一个符号;但是当method实现复杂了,会多一个method方法的调用,__objc_selrefs就多一个符号
// 函数在TEXT.objc_methname多了以下两个符号
0x10000A664 0x00000006 [ 1] literal string: class
0x10000A66A 0x0000000F [ 1] literal string: isKindOfClass:
// 函数调用实例方法列表尺寸比direct方法调用大,原因direct没有实例方法
// 函数调用
0x10000C138 0x00000050 [ 1] __OBJC_$_INSTANCE_METHODS_ViewController
// direct方法调用
0x10000C138 0x00000038 [ 1] __OBJC_$_INSTANCE_METHODS_ViewController
结论:direct 方法收益和函数调用基本一致,每一处调用优化收益是 3.93b。
函数调用、direct 方法调用为何可以减少二进制文件大小?
正常方法调用:
函数调用:
direct 方法调用:
x0是储存函数的第一个参数,msgSend的第一个参数肯定是self,所以x0是self
cbz先判断self是否存在,如果不存在,goto 0x1000063ac->ret,ret是return;
- (void)method
{
NSLog(@"");
}
如果self存在,则执行 NSLog
因为method方法实现过于简单,被编译器优化为内联了。
当方法实现比较简单,使用 direct 修饰,可以优化调用效率,但是包大小收益可能是负收益;因为很多处调用 direct 方法被内联。
回到一开始的问题:函数调用、direct 方法调用可以减少二进制文件大小的原因是因为相比方法调用,指令变少了。
去 Block
实验内容:
// 有block和无block的代码区别
- (void)blockTest
{
[self.blockProvider blockInterface1:1];
}
- (void)blockTest
{
[self.blockProvider blockInterface:^{
}];
}
block 调用会产生哪些符号?
// 一个block调用与两个block调用的区别
// __DATA.const(没有初始化过的常量) 多一个调用就会多一个___block_literal_global符号,这个占用32个字节
0x100008070 0x00000020 [ 1] ___block_descriptor_32_e5_v8?0l
0x100008090 0x00000020 [ 1] ___block_literal_global
0x1000080B0 0x00000020 [ 1] ___block_literal_global.12
// ___block_literal_global就是Block_layout结构体对象
struct Block_layout {
void *isa; int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
每多一处 block 的调用,会多一个___block_literal_global 符号,占用 32b。
// 无block调用
0x1000060A4 0x00000044 [ 1] -[ViewController blockTest]
// 一个block调用
0x100006094 0x00000048 [ 1] -[ViewController blockTest]
0x1000060DC 0x00000004 [ 1] ___27-[ViewController blockTest]_block_invoke
// 包含_block_invoke这个符号,则说明[ViewController blockTest]方法的实现中,有实现block
// 两个block调用
// 如果有两个block调用,会多出 ___29-[ViewController viewDidLoad]_block_invoke_2
// 有一个block调用比无block调用多出 36 + 4 = 40b
// 有两个block调用比有一个block调用多出24 + 4 = 28b
每多一处 block 的调用,会多一个_block_invoke_2 符号,占用 4b。
block 调用和方法调用,在调用处的汇编指令差别?
方法调用:
- (void)blockTest
{
[self.blockProvider blockInterface1:1];
}
block 调用:
在调用处的指令,block 调用只是比方法调用多了一个指令,多了 4 个字节(maplinkfile 计算出来的)。
去 Block 优化的示例
含有 block 参数方法的实现:
// 定义含有block的方法
- (void)blockInterface:(void(^)(void))block
{
[self shill];
!block ? : block();
}
- (void)shill
{
NSString *string = @"123";
if ([string isKindOfClass:[NSString class]]) {
NSLog(@"123");
}
}
// 去Block的优化
#define CallBlock(blockProvider,BLOCK)\
[blockProvider shill];\
BLOCK
实验对比:
- (void)blockTest
{
[self.blockProvider blockInterface:^{
NSLog(@"block");
}];
}
- (void)blockTest
{
CallBlock(self.blockProvider,{
NSLog(@"block");
})
}
// 去Block,相当于把block的实现内联了
0x100006064 0x00000050 [ 1] -[ViewController blockTest]
// 调用含有block的方法
// TEXT.text
0x10000604C 0x00000048 [ 1] -[ViewController blockTest]
0x100006094 0x0000001C [ 1] ___27-[ViewController blockTest]_block_invoke
// DATA.const
0x100008070 0x00000020 [ 1] ___block_descriptor_32_e5_v8?0l
0x100008090 0x00000020 [ 1] ___block_literal_global
调用含有 block 的方法:
去 Block:
结论:去 Block 的主要收益在于减少了___block_literal_global 和_block_invoke 符号,但是去 block 相当于把 block 的实现内联了,_block_invoke 主要大小的占用是 block 的实现,这块内联和_block_invoke 的差异差不多有 0x0000001C(28) - (0x00000050 - 0x00000048) = 20b;
___block_literal_global 固定 20b;
所以去 block 的收益是 40b。
总结介绍了从二进制文件层面分析编码对包大小的影响;
介绍了属性、方法调用、函数调用、direct 方法调用、block 调用在汇编下的差别,让我们在平时编码中,能有个大概的印象,该代码对应的汇编大概是什么样子;
得出一些收益数据:
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK