
什么是Link Map File Link Map File中文直译为链接映射文件,它是在Xcode生成可执行文件的同时生成的链接信息文件,用于描述可执行文件的构造部分,包括了代码段和数据段的分布情况。Xcode在生成可执行文件的时候默认情况下不生成该文件,需要开发者手动设置Target --> Build Setting --> Write Link Map File为YES:这里还可以设置Link Map存放的位置,默认的位置为: $(TARGET_TEMP_DIR)/$(PRODUCT_NAME)-LinkMap-$(CURRENT_VARIANT)-$(CURRENT_ARCH).txt 例如: /Users/zhanggui/Library/Developer/Xcode/DerivedData/LinkMapTest-ffnpzjkbsmhwvdcxorqbxpyvjtob/Build/Intermediates.noindex/LinkMapTest.build/Debug-iphonesimulator/LinkMapTest.build/LinkMapTest-LinkMap-normal-x86_64.txt 开发者也可以根据自己的需要自行设置该文件的位置。 Link Map File的组成 打开Link Map File,里面包含了以下几个部分: 1. Path # Path: /Users/zhanggui/Library/Developer/Xcode/DerivedData/LinkMapTest-ffnpzjkbsmhwvdcxorqbxpyvjtob/Build/Products/Debug-iphonesimulator/LinkMapTest.app/LinkMapTest Path是生成可执行文件的路径。 2. Arch # Arch: x86_64 Arch指代架构类型。 3. Object files: # Object files: [ 0] linker synthesized [ 1] /Users/zhanggui/Library/Developer/Xcode/DerivedData/LinkMapTest-ffnpzjkbsmhwvdcxorqbxpyvjtob/Build/Intermediates.noindex/LinkMapTest.build/Debug-iphonesimulator/LinkMapTest.build/LinkMapTest.app-Simulated.xcent [ 2] /Users/zhanggui/Library/Developer/Xcode/DerivedData/LinkMapTest-ffnpzjkbsmhwvdcxorqbxpyvjtob/Build/Intermediates.noindex/LinkMapTest.build/Debug-iphonesimulator/LinkMapTest.build/Objects-normal/x86_64/ViewController.o [ 3] /Users/zhanggui/Library/Developer/Xcode/DerivedData/LinkMapTest-ffnpzjkbsmhwvdcxorqbxpyvjtob/Build/Intermediates.noindex/LinkMapTest.build/Debug-iphonesimulator/LinkMapTest.build/Objects-normal/x86_64/main.o [ 4] /Users/zhanggui/Library/Developer/Xcode/DerivedData/LinkMapTest-ffnpzjkbsmhwvdcxorqbxpyvjtob/Build/Intermediates.noindex/LinkMapTest.build/Debug-iphonesimulator/LinkMapTest.build/Objects-normal/x86_64/AppDelegate.o [ 5] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.1.sdk/System/Library/Frameworks//Foundation.framework/Foundation.tbd [6]/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.1.sdk/usr/lib/libobjc.tbd [7]/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.1.sdk/System/Library/Frameworks//UIKit.framework/UIKit.tbd Object Files列举了可执行文件里所有的obj以及tbd。每一行代表对文件的编号。例如ViewController.o文件,其编号为2。编号的具体作用稍后介绍。 4. Sections # Sections: # Address Size Segment Section 0x100001730 0x00000333 __TEXT __text 0x100001A64 0x0000002A __TEXT __stubs 0x100001A90 0x00000056 __TEXT __stub_helper 0x100001AE6 0x00000A27 __TEXT __objc_methname 0x10000250D 0x0000003C __TEXT __objc_classname 0x100002549 0x0000086D __TEXT __objc_methtype 0x100002DB6 0x0000007A __TEXT __cstring 0x100002E30 0x00000182 __TEXT __entitlements 0x100002FB4 0x00000048 __TEXT __unwind_info 0x100003000 0x00000010 __DATA __nl_symbol_ptr 0x100003010 0x00000010 __DATA __got 0x100003020 0x00000038 __DATA __la_symbol_ptr 0x100003058 0x00000010 __DATA __objc_classlist 0x100003068 0x00000010 __DATA __objc_protolist 0x100003078 0x00000008 __DATA __objc_imageinfo 0x100003080 0x00000BE8 __DATA __objc_const 0x100003C68 0x00000010 __DATA __objc_selrefs 0x100003C78 0x00000008 __DATA __objc_classrefs 0x100003C80 0x00000008 __DATA __objc_superrefs 0x100003C88 0x00000008 __DATA __objc_ivar 0x100003C90 0x000000A0 __DATA __objc_data 0x100003D30 0x000000C0 __DATA __data 单从字面含义理解:每个Section包含了Address、Size、Segment以及Section。介绍之前,这里先简单介绍一下Mach-O文件。上面第一部分的Path是可执行文件的路径,使用iTerm进去到该文件夹,然后使用file命令即可查看该文件的类型: file LinkMapTest 输出结果为: LinkMapTest: Mach-O 64-bit executable x86_64 可以知道该文件是一个Mach-O格式的文件,它是iOS系统应用执行文件格式。Mach-O文件中的虚拟地址最终会被映射到物理地址上,这些地址会被分为不同的段类型:_ TEXT 、 _ DATA以及_ _ LINKEDIT等。各个段的含义如下: TEXT包含了被执行的代码。这些代码是只读、可执行 DATA包含了包含了将会被更改的数据,例如全局变量、静态变量等,可读写,但是不可执行 LINKEDIT 包含了加载程序的元数据,比如函数名称和地址,只读。 Segment又被划分成了不同的Section,不同的Section存储了不同的信息,例如 objc _ methname 为方法的名称。 再回顾上面的Sections,Address是起始位置、Size是大小、Segment是段、Section。 5. Symbols # Address Size File Name 0x100001730 0x0000003C [ 2] -[ViewController viewDidLoad] 0x100001770 0x00000092 [ 3] _main 0x100001810 0x00000080 [ 4] -[AppDelegate application:didFinishLaunchingWithOptions:] 0x100001890 0x00000040 [ 4] -[AppDelegate applicationWillResignActive:] 0x1000018D0 0x00000040 [ 4] -[AppDelegate applicationDidEnterBackground:] 0x100001910 0x00000040 [ 4] -[AppDelegate applicationWillEnterForeground:] 0x100001950 0x00000040 [ 4] -[AppDelegate applicationDidBecomeActive:] 0x100001990 0x00000040 [ 4] -[AppDelegate applicationWillTerminate:] 0x1000019D0 0x00000020 [ 4] -[AppDelegate window] 0x1000019F0 0x00000040 [ 4] -[AppDelegate setWindow:] 0x100001A30 0x00000033 [ 4] -[AppDelegate .cxx_destruct] 0x100001A64 0x00000006 [ 5] _NSStringFromClass 0x100001A6A 0x00000006 [ 7] _UIApplicationMain 0x100001A70 0x00000006 [ 6] _objc_autoreleasePoolPop 0x100001A76 0x00000006 [ 6] _objc_autoreleasePoolPush 0x100001A7C 0x00000006 [ 6] _objc_msgSendSuper2 0x100001A82 0x00000006 [ 6] _objc_retainAutoreleasedReturnValue 0x100001A88 0x00000006 [ 6] _objc_storeStrong 0x100001A90 0x00000010 [ 0] helper helper 0x100001AA0 0x0000000A [ 5] _NSStringFromClass 0x100001AAA 0x0000000A [ 6] _objc_autoreleasePoolPop 0x100001AB4 0x0000000A [ 6] _objc_autoreleasePoolPush 0x100001ABE 0x0000000A [ 6] _objc_msgSendSuper2 0x100001AC8 0x0000000A [ 6] _objc_retainAutoreleasedReturnValue 0x100001AD2 0x0000000A [ 6] _objc_storeStrong 0x100001ADC 0x0000000A [ 7] _UIApplicationMain 0x100001AE6 0x0000000C [ 2] literal string: viewDidLoad . . . 根据Sections的起始地址,可以将Symbols分为Sections个数的组,例如0x100001730到0x100001A64之间,就是 test代码区。 Symbols包含的信息有: Address:起始地址 Size:所占内存大小,这里使用16进制表示。 File:该Name所在的文件编号,也就是Object files部分的中括号的数字,例如-[ViewController viewDidLoad]对应的文件编号为2,根据Object files部分可以看到所属的文件为:ViewController.o。这样可以计算某个o文件所占内存的大小。只需要把Symbols中编号为o编号Symbols累加统计即可。 Name就是该Sybmols的名称。 6. Dead Stripped Symbols # Dead Stripped Symbols: # Size File Name <<dead>> 0x00000018 [ 2] CIE <<dead>> 0x00000018 [ 3] CIE <<dead>> 0x00000006 [ 4] literal string: class <<dead>> 0x00000008 [ 4] literal string: v16@0:8 <<dead>> 0x00000018 [ 4] CIE . . . 上面便是对Link map file做了简单的介绍。 itools 花了两天的时间,根据对Link Map File的学习,使用Ruby写了一个脚本文件,可以方便地统计出指定Link Map File中的组件或者tbd占用内存大小,类似: AppDelegate.o 8.50KB ViewController.o 735B LinkMapDemo.app-Simulated.xcent 386B main.o 192B linker synthesized 128B libobjc.tbd 120B Foundation.tbd 24B UIKit.tbd 24B 总大小为(仅供参考):10.07KB 想了解更多可以访问https://github.com/ScottZg/itools 总结 苹果开发还是有很多细节的东西需要去学习去了解。 学习一门脚本语言,也会给平时的开发带来很大的方便。 参考 Mach-O可执行文件 iOS调优|深入理解Link Map File iOS APP可执行文件的组成
随着Hybrid APP的流行,对其调试变得必不可少。使用Xcode我们能看到的仅仅是WebView,要想进一步查看里面的a标签、button和其他元素,Xcode是心有余而力不足。但是不用担心,Safari的调试功能能够弥补Xcode的不足,让你能够更方便地调试此类APP。要想使用Safari来调试,你要做的有三步: 配置Safari。 配置iPhone 开始调试 接下来就详细对此功能进行解释。 1.配置Safari 打开Safari,查看菜单栏里面是否有“开发”这个菜单,如果有,你的Safari就已经配置好了。如果没有,你要做的操作就是:打开Safari的偏好设置,然后设置高级选项里面选中“在菜单栏中显示开发菜单”。具体看图: 设置完成。 2.配置iPhone 打开系统设置,然后找到Safari 浏览器,点击高级选项,然后打开“JavaScript”和“Web检查器”即可。具体看图: 到此,iPhone配置成功。 3.开始调试 设置好前两个步骤后,将手机和电脑使用数据线连接。打开电脑上Safari,然后打开开发菜单,你就会看到你的手机(和你的电脑名称挨着)。此时打开你的Hybrid APP,你就可以调试你的APP了。如图: 点击你要调试的网页域名,即可弹出调试页面,这个页面的具体功能就自己摸索吧,和网页开发时调试页面差不多。如下图: 到此,你就完成了如何使用Safari进行调试。 几点需要知道的 根据测试,手机端的Safari可以直接连接到电脑上查看加载网页的代码。开发过程中的Hybrid APP也可以通过此方式查看。已经上线的Hybrid APP无法通过此种方式查看。所以在要想通过此方式调试,一定要用Debug包进行调试。 如果在Safari开发菜单中看不到你的手机,那就看看步骤二是否正确配置。 总结 随着H5和CSS3的进一步发展,Hybrid APP变得更加火爆,具有Native APP的良好交互,又有Web APP跨平台开发的优势,而且需求更新版本不必每次都更新APP。 在未来,你看到的手机端APP不一定就是原生的,而是Web,更让人疯狂的是你还无法分辨是否是原生还是Web。 所以,移动开发的小伙伴,为了你的未来,多学学前端也是很有必要的。
先看一下消息转发流程: 在forwardInvocation这一步,你必须要实现一个方法: - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE(""); 该方法用于说明消息的返回值和参数类型。NSMethodSignature是方法签名,它是用来记录返回值和参数类型的一个对象。看一下与该类相关的方法: //在NSMethodSignature.h中 + (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types; //1 //在NSObject.h中 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE(""); //2 + (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE(""); //3 2和3两个方法是根据SEL来构造NSMethodSignature,而1方法则是今天的主角,那里面的types究竟是什么呢? 根据1的方法名称可以猜想,types是ObjCTypes,它是一个是字符串数组,该数组包含了方法的类型编码。那如果我们用该方法实例化NSMethodSignature的时候究竟如何写types呢?先来举个例子: - (void)goToSchoolWithPerson:(Person *)person; [zhangsan goToSchoolWithPerson:lisi]; 其ObjcTypes就是 "v@:@"。那究竟是如何得来该字符串呢?其实我们有两种方式: 直接查表。在Type Encodings里面列出了对应关系。 使用 @encode()计算。( NSLog(@"%s",@encode(BOOL))的结果为B ) 我们都知道消息发送会被转换成objc _ msgSend(id reciever,SEL sel,prarams1,params2,....)。所以上面的方法会被转换成: void objc_msgSend(zhangsan,@selector(goToSchoolWithPerson:),lisi); //包含两个隐藏参数 这里的 “v@:@”就代表: "v":代表返回值void "@":代表一个对象,这里指代的id类型zhangsan,也就是消息的receiver ":":代表SEL "@":代表参数lisi 再举个例子: - (BOOL)ifSuccess:(NSString *)tag 其ObjCTypes为:"B@:@",其中: "B":代表BOOL。 // NSLog(@"%s",@encode(BOOL))的结果为B "@":一个id类型的对象,第一个参数类型,也就是objc _ msgSend的第一个参数 ":":代表对应的SEL,第二个参数 "@":一个id类型的对象,也就是tag。 到此,我们就知道了该如何书写ObjCTypes了。 附 1.Type Encodings
Runtime那些事 前言 从字面意思看,就是运行时。但是这个运行时究竟什么意思?可以把它理解成:不是在编译期也不是在链接期,而是在运行时。那究竟在运行期间做了什么呢?按照苹果官方的说法,就是把一些决策(方法的调用,类的添加等)推迟,推迟到运行期间。只要有可能,程序就可以动态的完成任务,而不是我们在编译期已经决定它要完成什么任务。这就意味了OC不仅仅需要编译器,还需要一个运行时的系统来支撑。 目录 接下来就对Runtime做一个系统的介绍,主要内容包括: 简介 涉及到的数据结构 runtime.h解析 如何可以触及到RunTime? 消息 动态消息解析 消息转发 Runtime的使用场景 1.简介 根据前言,你已经了解了Runtime大概是个什么鬼,在OC发展历程中,它主要有两个版本:Legacy和Modern。Legacy版本采用的是OC1.0版本;Modern版本采用的OC2.0版本,而且相比Legacy也添加了一些新特性。最明显的区别在于: 在legacy版本,如果你改变了类的布局,那么你必须重新编译继承自它的类。 在modern版本,如果你改变了类的布局,你不必重新编辑继承自它的类。 平台 iPhone的应用程序以及OS X v10.5版本的64位机器使用的是modern版本的runtime。 其他(OS X桌面应用32位程序)使用的是legacy版本的runtime。 2.涉及到的数据结构 这里主要介绍一下在runtime.h里面涉及到的一些数据结构。 Ivar Ivar从字面意思来讲,它就是代表的实例变量,它也是一个结构体指针,包含了变量的名称、类型、偏移量以及所占空间。 SEL 选择器,每个方法都有自己的选择器,其实就是方法的名字,但是不仅仅是方法的名字,在objc.h中,我们可以看到它的定义: /// An opaque type that represents a method selector.一个不透明类型,用来代表一个方法选择器 typedef struct objc_selector *SEL; 由定义可知它是一个objc_selector的结构体指针,尴尬的是在runtime源码中并没有找到该结构体。猜想它内部应该就是一个char 的字符串。 你可以使用: NSLog(@"%s",@selector(description)); //%s用来输出一个字符串 打印出来description。在这里你可以把它理解成一个选择器,可以标识某个方法。 IMP 它是一个函数指针,指向方法的实现,在objc.h里面它的定义是这样的: /// A pointer to the function of a method implementation. #if !OBJC_OLD_DISPATCH_PROTOTYPES typedef void (*IMP)(void /* id, SEL, ... */ ); #else typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); #endif id id是一个我们经常使用的类型,可用于作为类型转换的中介者。它类似于Java里面的Object,可以转换为任何的数据类型。它在objc.h里面是这样定义的: /// Represents an instance of a class. struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; }; /// A pointer to an instance of a class. typedef struct objc_object *id; 它其实是一个objc object的结构体指针,而在后面将要提到的Class其实是个objc class的指针,而objc _ class是继承自objc _o bject的,因此可以相互转换,这也是为什么id可以转换为其他任何的数据类型的原因。 Method 方法,它其实是一个objc_method的结构体指针,其定义如下: /// An opaque type that represents a method in a class definition. typedef struct objc_method *Method; struct objc_method { SEL _Nonnull method_name OBJC2_UNAVAILABLE; char * _Nullable method_types OBJC2_UNAVAILABLE; IMP _Nonnull method_imp OBJC2_UNAVAILABLE; } 这个就比较好理解了,该结构体包含了方法的名称(SEL),方法的类型以及方法的IMP。 Class 它是一个objc_class的结构体指针,在runtime.h中的定义如下: /// An opaque type that represents an Objective-C class.一个不透明类型,代表OC的类 typedef struct objc_class *Class; struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE; const char * _Nonnull name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; 该结构体中各部分介绍如下: isa:是一个Class类型的指针,每个对象的实例都有isa指针,他指向对象的类。而Class里面也有个isa指针,它指向meteClass(元类),元类保存了类方法的列表。 name:对象的名字 version:类的版本号,必须是0 info:供运行期间使用的位标识 instance_size:该类的实例大小 ivars:成员变量数组,包含了该类包含的成员变量 methodLists:包含方法的数组列表,也是一个结构体,该结构体里面还包含了一个obsolete的指针,表示废弃的方法的列表 cache:缓存。这个比较复杂,在后面会提到,这里先忽略。 protocols:协议列表,也是一个数组 而在objc-runtime-new.h中,你会发现这样的定义(在runtime中并没有完全暴露objc_class的实现): struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags class_rw_t *data() { return bits.data(); } //其他的省略 其实objc class继承自objc object。所以这也说明了为什么id能够转换为其他的类型。 3.runtime.h解析 我们先看一下在usr/include/objc/runtime.h,这个是任何一个工程都可以直接找到的,它是SDK的一部分。主要定义了以下内容: 定义了一些类型,例如Method/Ivar/Category等,还有一些结构体。 函数。函数里面有分了几大类: 关于对象实例的方法,例如object getClass、object setClass以及object _ getIvar等。这些函数大多以object开头。 用来获取属性或者对对象进行操作。 获得类定义的方法,例如objc getClass/objc getMetaClass等,这些方法更多的是获取Class或者在Class级别上进行操作。 多以objc开头 和类相关的方法。例如class getName/class isMetaClass等,这些更多的是获取Class的一些属性。比如该类的属性列表、方法列表、协议列表等。传参大多为Class。 多以class开头 实例化类的一些方法。例如class _ createInstance方法,就是相当于平时的alloc init。 添加类的方法。例如你可以使用这些方法冬天的注册一个类。使用objc allocateClassPair创建一个新类,使用 objc registerClassPair对类进行注册 等等。。。 另外就是一些废弃的方法和类型。 4. 如何可以触及到RunTime? 有三种不同的方式可以让OC编程和runtime系统交互。 OC源代码 大多数情况下,我们写的OC代码,其实它底层的实现就是runtime。runtime系统在背后自动帮我们处理了操作。例如我们编译一个类,编译器器会创建一个结构体,然后这个结构体会从类中捕获信息,包括方法、属性、Protocol等。 NSObject的一些方法 在Foundation框架里面有个NSObject.h,在usr/include/objc里面也有一个NSObject.h。而我们平时用到的类的基类是/usr/include/objc里面的这个NSObject.h,Foundation里面的NSObject.h只是NSObject的一个Category。所以这里我们更关注一下/usr/include/objc里面的NSObject.h。 由于大多数对象都是NSObject的子类,所以在NSObject.h里面定义的方法都可以使用。 在这些方法里面,有一些方法能够查询runtime系统的信息,例如: - (BOOL)isKindOfClass:(Class)aClass; //用来检测一个对象是否是某各类的实例对象,aClass也有可能是父类,同样可以检测出来。 - (BOOL)isMemberOfClass:(Class)aClass; //而该方法只能检测一个对象是否是某各类的实例对象。但如果aClass不能为该类的父类,如果是父类则该方法返回NO - (BOOL)respondsToSelector:(SEL)aSelector; - (BOOL)conformsToProtocol:(Protocol *)aProtocol; - (IMP)methodForSelector:(SEL)aSelector; 这里用代码对isKindOfClass和isMemberOfClass做个简单介绍: //stu是Student的实例对象,Student的父类为Person,Person的父类为NSObject。 [stu isKindOfClass:[Student class]]; //YES [stu isKindOfClass:[Person class]]; //YES [stu isKindOfClass:[NSObject class]]; //YES [stu isMemberOfClass:[Student class]]; //YES [stu isMemberOfClass:[Person class]]; //NO [stu isMemberOfClass:[NSObject class]]; //NO 我们可以在objc源代码中的NSObject.mm中看到相应的实现: + (BOOL)isMemberOfClass:(Class)cls { return object_getClass((id)self) == cls; } - (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls; } + (BOOL)isKindOfClass:(Class)cls { for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } - (BOOL)isKindOfClass:(Class)cls { for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } 从具体实现可知,为什么isKindOfClass能够检测出superclass。另外,在NSObject.h中,并没有看到两个方法的类方法声明,但是在实现里面却包含了类方法的实现。这里有个疑问:为什么没有对外声明的两个类方法依然可以在外部调用呢?(比如我可以直接使用[Student isMemberOfClass:[NSObject class]])。 这里还用到了class方法,这个方法声明如下: + (Class)class OBJC_SWIFT_UNAVAILABLE("use 'aClass.self' instead"); - (Class)class OBJC_SWIFT_UNAVAILABLE("use 'type(of: anObject)' instead"); + (Class)class { //返回当前的self return self; } - (Class)class { return object_getClass(self); } 这里重要的是理解self究竟代表着什么: 当self为实例对象的时候,[self class] 和 object_getClass(self)是等价的。object_getClass([self class])得到的是元类。 当self为类对象的时候,[self class]返回的是自身,还是self。object_getClass(self) 与object_getClass([self class])等价。拿到的是元类。 Runtime函数 runtime系统其实就是一个动态共享的Library,它是由在/usr/include/objc目录的公共接口中的函数和数据结构组成。 5. 消息 在Objective-C中,消息直到运行时才将其与消息的实现绑定,编译器会将 [receiver message]; 转换成 objc_msgSend(receiver,selector); //1 objc_msgSend(receiver,selector,arg1,arg2,...); //2 如果包含参数,那么就会执行2方法。其实除了该方法,还有以下几个方法: objc_msgSend_stret objc_msgSendSuper objc_msgSendSuper_stret 当想一个对象的父类发送message时,会使用 objc_msgSendSuper 如果方法的返回值是一个结构体,那么就会使用 objc_msgSend_stret objc_msgSendSuper_stret 这里我们可以打开objc源码,然后你会发现里面有多个.s文件: 这里之所以有objc-msg-类的不同文件,我猜想应该是对不同的CPU指令集(指令不一样)做了分别处理。因为这些.s文件名称中包含的是不同的arm指令集。而且打开.s文件你会发现里面的实现是汇编语言,所以苹果为了效率还是蛮拼的,直接用汇编语言实现。其中就能找到objc _ msgSend的实现(objc-msg-i386.s中): 虽然对汇编了解不是太多,但是这个文件中的注释很详细,从注释可以看出objc_msgSend方法的执行过程: 先加载receiver和selector到寄存器,然后判断receiver是否为空,如果为空,则函数执行结束; 如果receiver不为空,开始搜索缓存,查看方法缓存列表里面是否有改selector,如果有则执行; 如果没有缓存,则搜索方法列表,如果在方法列表中找到,则跳转到具体的imp实现。没有则执行结束。 使用了隐藏参数 在发送一个消息的时候,会被编译成objc_msgSend,此时该消息的参数将会传入objc_msgSend方法里面。除此之外,还会包含两个隐藏的参数: receiver method的selector 这两个参数在上面也有提到。其中的receiver就是消息的发送方,而selector就是选择器,也可以直接用 cmd来指代( cmd用来代表当前所在方法的SEL)。之所以隐蔽是因为在方法声明中并没有被明确声明,在源代码中我们仍然可以引用它们。 获取方法地址 我们每次发送消息都会走objc_msgSend()方法,那么有没有办法避开消息绑定直接获取方法的地址并调用方法呢?答案当然是有的。我们上面简单介绍了IMP,其实我们可以使用NSObject的 - (IMP)methodForSelector:(SEL)aSelector; 方法,通过该方法获得IMP,然后调用该方法。但是避开消息绑定而直接调用的使用并不常见,但是如果你要多次循环调用的话,直接获取方法地址并调用不失为一个省时操作。看下面的代码: void (*setter)(id,SEL,BOOL); setter = (void(*)(id,SEL,BOOL))[stu2 methodForSelector:@selector(learning)]; NSDate *startDate = [NSDate date]; for (int i = 0;i<100000;i++) { setter(stu2,@selector(learning),YES); } double deltaTime = [[NSDate date] timeIntervalSinceDate:startDate]; NSLog(@"----%f",deltaTime); NSDate *startDate1 = [NSDate date]; for (int i = 0;i<100000;i++) { [stu2 learning]; } double deltaTime1 = [[NSDate date] timeIntervalSinceDate:startDate1]; NSLog(@"----%f",deltaTime1); 你可以自行跑一下,看一下时间差异。你会发现:获取方法地址直接调用更省时间,但请注意使用场景。 6. 动态消息解析 这里介绍一下如果动态地提供方法的实现。 动态方法解析 在开发过程中,你可能想动态地提供一个方法的实现。比如我们对一个对象声明了一个属性,然后我们使用了 @dynamic 标识符: @dynamic propertyName; 该标识符的目的就是告诉编译器:和这个属性相关的getter和setter方法会动态地提供(当然你也可以直接手动在代码里面实现)。这个时候你就会用到NSObject.h里面的两个方法 + (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); + (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); 来提供方法的实现。 其实OC方法就是一个简单的C函数,它至少包含了两个参数self和 _ cmd,你可以自己声明一个方法: void dynamicMethodIMP(id self, SEL _cmd) { //这里是方法的具体实现 } 此时我们可以在声明属性的类中实现上面提到的两个方法(一个是解析类方法,一个是解析实例方法),例如我在Person里面这样写: @dynamic address; //也就意味着我们需要手动/动态实现该属性的getter和setter方法。 你会发现当我们运行下面的代码时,程序会crash: Person *zhangsan = [[Person alloc] init]; zhangsan.address = @"he nan xinxiang "; NSLog(@"%@",zhangsan.address); // crash reason // -[Person setAddress:]: unrecognized selector sent to instance 0x1d4449630 这里简单的做一个动态方法解析: void setter(id self,SEL _cmd) { NSLog(@"set address"); } + (BOOL)resolveInstanceMethod:(SEL)sel { NSString *selStr = NSStringFromSelector(sel); if ([selStr hasPrefix:@"set"]) { class_addMethod([self class], sel, (IMP)setter, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; } 所以我们需要自己去实现setAddress: 方法。(这里判断用hasPrefix不太准确,开发者可以自行根据需求调整)。转发消息(下面会讲到)和动态解析是正交的。也就是说一个class有机会再消息转发机制前去动态解析此方法,也可以将动态解析方法返回NO,然后将操作转发给消息转发。 动态加载 OC编程也允许我们在程序运行的时候动态去创建和链接一个类或者分类。这些创建的类或者分类将会和运行app前创建的类一样,没有差别。 动态加载在开发的过程中可以做好多事情,例如系统设置中的不同模块就是动态加载的。 在Cocoa环境中,最经典的就是Xcode,它可以安装不同的插件,这个也是动态加载的方式实现的。 7. 消息转发 发送一个消息给对象,如果对象不能处理,那么就会产生错误。然而,在产生错误之前,runtime 系统会给对象第二次机会去处理该消息。这里详细已经在深入浅出理解消息的传递和转发文章中做了介绍,这里就不再介绍了。 8. Runtime的使用场景 Runtime的使用几乎无处不在,OC本身就是一门运行时语言,Class的生成、方法的调用等等,都是Runtime。另外,我们可以用Runtime做一些其他的事情。 字典转换Model 平时我们从服务端拿到的数据是json字符串,我们可以将其转换成成NSDictionary,然后通过runtime中的一些方法做一个转换: 先拿到model的所有属性或者成员变量,然后将其和字典中的key做映射,然后通过KVC对属性赋值即可。更多可参见class_copyIvarList方法获取实例变量问题引发的思考中的例子。 热更新(JSPatch的实现) JSPatch能做到JS调用和改写OC方法的根本原因就是OC是动态语言,OC上的所有方法的调用/类的生成都通过OC Runtime在运行时进行,我们可以根据名称/方法名反射得到相应的类和方法。例如 Class class = NSClassFromString("UIViewController"); id viewController = [[class alloc] init]; SEL selector = NSSelectorFromString("viewDidLoad"); [viewController performSelector:selector]; 也正是鉴于此,才实现了热更新。 给Category添加属性 我们可以使用runtime在Category中给类添加属性,这个主要使用了两个runtime钟的方法: OBJC_EXPORT void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy); OBJC_EXPORT id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key); 具体使用可参见:给分类(Category)添加属性。 Method Swizzling 它是改变一个已存在的selector的实现的技术,比如你想将viewDidload方法替换为我们自定义的方法,给系统的方法添加一些需要的功能,来实现某些需求。比如你想跟踪每个ViewController展示的次数,你可以使用该技术重写ViewDidAppear方法,然后做一些自己的处理。可以参见Method Swizzling里面的讲解。 总结 Objective-c本身就是一门冬天语言,所以了解runtime有助于我们更加深入地了解其内部的实现原理。也会把一些看似很难的问题通过runtime很快解决。 参考链接: 1.Objective-C Runtime Programming Guide 2.Objective-C Runtime 3.objc4 4.深入浅出理解消息的传递和转发 5.class_copyIvarList方法获取实例变量问题引发的思考 6.JSPatch 实现原理详解 7.给分类(Category)添加属性 8.Method Swizzling
在runtime.h中,你可以通过其中的class_copyIvarList方法来获取实例变量。具体的实现如下(记得导入头文件): - (NSArray *)ivarArray:(Class)cls { unsigned int stuIvarCount = 0; Ivar *ivars = class_copyIvarList(cls, &stuIvarCount); if (stuIvarCount == 0) { return nil; } NSMutableArray *arr = [[NSMutableArray alloc] initWithCapacity:stuIvarCount]; for (int i = 0;i<stuIvarCount;i++) { Ivar ivar = ivars[i]; NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)]; NSLog(@"%@",ivarName); [arr addObject:ivarName]; } free(ivars); return arr; } 如上面代码。其中cls就是你要获取实例变量的类,stuIvarCount用来承载要获取类的实例变量的个数。打印出来的ivarName就是cls的实例变量。接下来对这个方法进行解析:首先看一下里面的Ivar,先看一下定义: /// An opaque type that represents an instance variable. typedef struct objc_ivar *Ivar; struct objc_ivar { char * _Nullable ivar_name OBJC2_UNAVAILABLE; //变量名字 char * _Nullable ivar_type OBJC2_UNAVAILABLE; //变量类型 int ivar_offset OBJC2_UNAVAILABLE; //偏移量 #ifdef __LP64__ int space OBJC2_UNAVAILABLE; //存储空间 #endif } Ivar是一个叫做objc_ivar的结构体指针,其中的 ifdef判断是判断当前设备是否是64位设备,这里可以延伸出一个方法: //判断当前设备是否是64位设备,也可以用这个方法判断是否是32位设备 - (BOOL)is64Bit { #if defined(__LP64__) && __LP64__ return YES; #else return NO; #endif } OBJC_EXPORT Ivar _Nonnull * _Nullable class_copyIvarList(Class _Nullable cls, unsigned int * _Nullable outCount) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); class_copyIvarList的注释如下:它返回的是一个Ivar的数组,这个数组里面包含了你要查看类的所有实例变量,但是不包括从父类继承过来的。如果你传入的类没有实例变量或者改class为Nil,那么该方法返回的就是NULL,count值也就变成了0。有一点需要注意:你必须使用free()方法将该数组释放。然后就是通过for循环遍历,通过ivar _ getName拿到ivarName。以上便是对clas_copyIvarList的介绍。 它还有一个最常用的使用方式(在开发中经常用到的):根据字典或者json字符串转化为model,在网络请求返回数据时经常用到。使用方法就是自己写一个基类的model,然后让项目中用到的model都继承自此基类,基类中的关键代码如下: + (instancetype)zg_modelFromDic:(NSDictionary *)dataDic { id model = [[self alloc] init]; unsigned int count = 0; Ivar *ivarsA = class_copyIvarList(self, &count); if (count == 0) { return model; } for (int i = 0;i < count; i++) { Ivar iv = ivarsA[i]; NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(iv)]; ivarName = [ivarName substringFromIndex:1]; id value = dataDic[ivarName]; [model setValue:value forKey:ivarName]; } free(ivarsA); return model; } 这里是把字典转成model,先用class_copyIvar获取该model的所有实例变量,然后通过kvc对属性进行赋值。最终返回model。这里有个点需要注意以下几点: 你的model的属性名称要和服务端返回的数据一致,比如你的model有个属性叫做name,那么你服务端返回的数据字典里面的对应属性也要叫做name,因为这个方法是根据属性从字典里面拿数据的。你也可以做一个映射,让自定义的实例变量名称映射到服务端提供的变量名称。 实现里面有个substringFromIndex:操作,其目的就是把使用该方法拿到的实例变量前面的" "去掉。所以你最好使用 @property进行属性声明,并且不要去修改自动生成的实例变量。(@property = getter + setter + ivar,这里的 _ ivar其实就是编译器帮我们生成的实例变量) 接下来你可以尝试去获取UILabel的实例变量列表: [self ivarArray:[UILabel class]] 你会发现拿到的结果是这样的: ( "_size", "_highlightedColor", "_numberOfLines", "_measuredNumberOfLines", "_baselineReferenceBounds", "_lastLineBaseline", "_previousBaselineOffsetFromBottom", "_firstLineBaseline", "_previousFirstLineBaseline", "_minimumScaleFactor", "_content", "_synthesizedAttributedText", "_defaultAttributes", "_fallbackColorsForUserInterfaceStyle", "_minimumFontSize", "_lineSpacing", "_layout", "_scaledMetrics", "_cachedIntrinsicContentSize", "_contentsFormat", "_cuiCatalog", "_cuiStyleEffectConfiguration", "_textLabelFlags", "_adjustsFontForContentSizeCategory", "__textColorFollowsTintColor", "_preferredMaxLayoutWidth", "_multilineContextWidth", "__visualStyle" ) 但是跳转到UILabel.h,你会发现里面有好多的属性不包含在我们根据该方法得出的属性数组里面,而且使用该方法得到的属性在UILabel.h里面并没有。这个是什么原因呢? 先看一下好多UILabel里面的属性没有在数组里面打印问题:猜想应该是在UILabel.m里面使用了@dynamic。导致没有自动生成getter、setter和ivar,所以没有在数组里面包含。 @synthsize:如果没有手动实现setter/getter方法那么会自动生成,自动生成_var变量。如果不写,默认生成getter/setter和_var。你也可以使用该关键字自己设置自动变量的名称。 @dynamic告诉编译器:属性的setter/getter需要用户自己实现,不自动生成,而且也不会产生_var变量。 也就是说在UILabel里面虽然有个text的属性,也许在UILabel.m里面已经包含: @dynamic text; 这样的话在实现里面没有产生实例变量,只是手动实现了getter和setter,所以就不会显示text属性在刚才得到的数组里面了。 至于数组中有UILabel.h里面没有的变量,这个就好理解了,有可能在UILabel.m里面添加了一些实例变量或者在运行时添加了这些实例变量。 除此方法之外,你还可以使用class_copyPropertyList方法,这个是拿到的所有用 @property声明的属性,包括在.m里面添加的属性(所以打印出来的可能要比真实在.h里面看到的多),具体实现和上面的获取方法类似: - (NSArray *)propertyArr:(Class)cls { unsigned count = 0; objc_property_t *properties = class_copyPropertyList(cls, &count); if (count == 0) { return nil; } NSMutableArray *arr = [[NSMutableArray alloc] initWithCapacity:count]; for (int i = 0; i < count; i ++) { objc_property_t property = properties[i]; NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)] ; [arr addObject:propertyName]; } free(properties); return arr; } 其中的copyPropertyList方法解释如下:记得使用过后也要调用free去释放数组。(PS:在源代码中暂未找到objc_property结构体的说明)因此,你可以通过使用该方法来实现字典或者json字符串转model操作: + (instancetype)zg_modelFromDic:(NSDictionary *)dataDic { id model = [[self alloc] init]; unsigned int count = 0; objc_property_t *properties = class_copyPropertyList([self class], &count); if (count == 0) { return model; } for (int i = 0;i < count; i++) { objc_property_t property = properties[i]; NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)]; id value = dataDic[propertyName]; [model setValue:value forKey:propertyName]; } free(properties); return model; } 两种方式均可实现model转换操作。以上便是由class_copyIvarList所引发的思考。 转载请标明来源:https://yq.aliyun.com/articles/328092