在iOS中Mach-O
文件主要有以下三种:
可执行文件;
目标文件,如.o文件;
动态库,如dylib,framework文件;
Mach-O
文件的格式一般包括一个Mach-O头,一系列的载入命令,一个或多个Section,如下图:
Header:记录平台属性,CPU架构、版本,文件类型,加载指令数量、大小,文件状态等信息;
Load Commands:Section定义内存空间,获取动态链接器路径,获取应用程序入口,加载动态库等指令;
Section
Section段存放的内容通过MachOView
工具查看主要有两大类:__Text
和__DATA
。
而不管是__Text
还是__DATA
下面还都包含若干子项,为了弄清楚每个子项中存放的内容,我们编写一个测试工程,里面尽量包含Objc语音的特性:类,继承,分类,协议,实例方法,类方法等
Person.h
@interface Person : NSObject
- (void)sleep;
- (void)walk;
@end
Person.m
NSString *string1 = @"it is string1";
NSString *string2;
@implementation Person
- (void)sleep {
NSLog(@"i am sleeping");
}
- (void)walk {
printf("i am walking");
}
+ (void)grow {
static NSString *string3 = @"it is string3";
static NSString *string4;
NSLog(@"%@_%@",string3,string4);
}
@end
给Person
类增加分类:
Person+other.h
@interface Person (other)
- (void)ill;
@end
Person+other.m
@implementation Person (other)
- (void)ill {
NSLog(@"i am fall ill");
}
@end
添加StudentProtocol
协议:
@protocol StudentProtocol
- (void)study;
+ (void)playgame;
@end
最后实现Person
类的子类Student
,同时实现StudentProtocol
协议:
Student.h
@interface Student : Person
@end
Student.m
@implementation Student
- (void)study {
NSLog(@"i am studying");
}
+ (void)playgame {
NSLog(@"i am playing game");
}
@end
为了防止Xcode的dead code
的优化,我们在main函数里面显示调用下上面的类/方法:
int main(int argc, const char * argv[]) {
Person *p = [[Person alloc] init];
[p sleep];
[p walk];
[p ill];
Student *student = [[Student alloc] init];
[student study];
[Student playgame];
return 0;
}
同时,为了保持工程的简洁,我们创建的是Command Line Tool
工程。将生成的可执行文件用MachOView
打开:
下面我们看下每个子项存储的内容:
-
__TEXT,__text
和__TEXT,__stubs
图中所示,__TEXT,__text
存放的为具体的方法实现,如Person类的sleep和walk方法,MachOView
已经帮我们翻译成汇编代码。引用的外部符号如NSLog
记录在偏移地址0x100001C98
的位置,而0x100001C98
在__TEXT,__stubs
中,作为App启动时动态链接的辅助,动态链接的内容我们以后介绍:
-
__TEXT,__cstring
和__Text,__cfstring
很好理解,__cstring
中保存的是c
字符串,比如sleep
方法中引用的 "i am sleeping"
。那么,__TEXT,__cfstring
保存的是什么内容呢?
从上图可知,实际上__TEXT,__cfstring
中保存的是Objc字符串,包括类型,值,大小,如从偏移地址00002058
开始记录了Objc字符串@"i am sleeping"
的信息,类型是__CFConstantStringClassReference
,大小为13,保存的值为0x100001CF6
,从__cstring
中可查到0x100001CF6
指向的正是"i am sleeping"
。从上面的分析看一个Objc字符串占用的空间比c字符串多个块__TEXT,__cfstring
的存储。
-
__TEXT,__objc_classname
__objc_classname
中保存的是类名、协议名、Category名。 -
__TEXT,__objc_methname
__TEXT,__objc_methname
中保存了方法名,包括系统库中的方法名。 -
__TEXT,__objc_methtype
方法类型的描述。 -
__DATA,__objc_classlist
和__DATA,__objc_data
__DATA,__objc_classlist
中保存了自定义的类,而类的具体信息在__DATA,__objc_data
中:
如
0x100002718
的地址保存了Person
类的信息:ISA、父类、缓存、虚表等。
-
__DATA,__objc_protolist
和__DATA,__data
和__DATA,__objc_classlist
类型,协议的保存在__DATA,__objc_protolist
和__DATA,__data
中
协议的信息保存的很详细,协议没有ISA指针,同时对实例方法,类方法,是不是optional的都进行了记录。
-
__DATA,__objc_const
__DATA,__objc_const
记录了方法、协议、属性列表和类概要信息。
-
__DATA,__objc_selrefs
和-__DATA,__objc_classrefs
记录了被使用的类和方法,因为Objc是动态语言,这里只记录了被显式使用的类和方法。
-
__DATA,__objc_catlist
为什么__DATA,__objc_catlist
在最后写呢?因为上面我们构建的工程的__DATA,__objc_catlist
段是空的,我们明明给Person
类加了Person+other
分类,为什么在分类列表中没有相关的信息呢?
再想下分类的作用,我们使用分类更多的是给非自定义类添加方法,如系统类或者第三方库中的类,既然自定义的类都以源码形式,Xcode会不会将分类的方法当成普通方法处理了呢?为了证实猜想,给系统类NSString
添加分类NSString+Trim
:
果然__objc_catlist
中出现了数据,同时可以在__DATA,__objc_const
看到Person
分类中的ill
方法和主类中的sleep
、walk
方法在内存中是连续的。以上猜想得到了论证:
那这个优化是发生在编译阶段还是链接阶段呢?
我们改下工程Build Setting
中把Mach-O Type
从Executable
改成Static Library
,即将可执行文件改成静态库工程,然后再查看下目标文件:
从上图可以看到,编译/汇编阶段主类和分类都生成了.o目标文件,优化的过程发生在链接阶段。
以上是Objc代码在可执行文件中的存储方式,了解代码的存储方式可以让我们更深入的理解Objc的运行时,同时给我们优化可执行文件大小以及解决Appstore对__TEXT段大小限制提供思路:
- 如通过链接参数进行修改数据存放位置:
-Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname
-Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype
-Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname
-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring
-Wl,-rename_section,__TEXT,__const,__RODATA,__const
- 通过分析
__DATA,__objc_selrefs
和-__DATA,__objc_classrefs
扫描无语类和方法等。