共计 9796 个字符,预计需要花费 25 分钟才能阅读完成。
该文章属于 < 简书 — 刘小壮 > 原创,转载请注明:
< 简书 — 刘小壮 > https://www.jianshu.com/p/4a22a39b69c5
attribute
__attribute__
是一套编译器指令,被 GNU
和LLVM
编译器所支持,允许对于 __attribute__
增加一些参数,做一些高级检查和优化。
__attribute__
的语法是,在后面加两个括号,然后写属性列表,属性列表以逗号分隔。在 iOS 中,很多例如 NS_CLASS_AVAILABLE_IOS
的宏定义,内部也是通过 __attribute__
实现的。
__attribute__((attribute1, attribute2));
下面是一些 __attribute__
的常用属性,更完整的属性列表可以到 llvm 的官网查看。
objc_subclassing_restricted
objc_subclassing_restricted
属性表示被修饰的类不能被其他类继承,否则会报下面的错误。
__attribute__((objc_subclassing_restricted))
@interface TestObject : NSObject
@property (nonatomic, strong) NSObject *object;
@property (nonatomic, assign) NSInteger age;
@end
@interface Child : TestObject
@end
错误信息:Cannot subclass a class that was declared with the 'objc_subclassing_restricted' attribute
objc_requires_super
objc_requires_super
属性表示子类必须调用被修饰的方法super
,否则报黄色警告。
@interface TestObject : NSObject
- (void)testMethod __attribute__((objc_requires_super));
@end
@interface Child : TestObject
@end
警告信息:(不报错)
Method possibly missing a [super testMethod] call
constructor / destructor
constructor
属性表示在 main
函数执行之前,可以执行一些操作。destructor
属性表示在 main
函数执行之后做一些操作。constructor
的执行时机是在所有 load
方法都执行完之后,才会执行所有 constructor
属性修饰的函数。
__attribute__((constructor)) static void beforeMain() {NSLog(@"before main");
}
__attribute__((destructor)) static void afterMain() {NSLog(@"after main");
}
int main(int argc, const char * argv[]) {
@autoreleasepool {NSLog(@"execute main");
}
return 0;
}
执行结果:debug-objc[23391:1143291] before main
debug-objc[23391:1143291] execute main
debug-objc[23391:1143291] after main
在有多个 constructor
或destructor
属性修饰的函数时,可以通过设置优先级来指定执行顺序。格式是 __attribute__((constructor(101)))
的方式,在属性后面直接跟优先级。
__attribute__((constructor(103))) static void beforeMain3() {NSLog(@"after main 3");
}
__attribute__((constructor(101))) static void beforeMain1() {NSLog(@"after main 1");
}
__attribute__((constructor(102))) static void beforeMain2() {NSLog(@"after main 2");
}
在 constructor
中根据优先级越低,执行顺序越高。而 destructor
则相反,优先级越高则执行顺序越高。
overloadable
overloadable
属性允许定义多个同名但不同参数类型的函数,在调用时编译器会根据传入参数类型自动匹配函数。这个有点类似于 C++
的函数重载,而且都是发生在编译期的行为。
__attribute__((overloadable)) void testMethod(int age) {}
__attribute__((overloadable)) void testMethod(NSString *name) {}
__attribute__((overloadable)) void testMethod(BOOL gender) {}
int main(int argc, const char * argv[]) {
@autoreleasepool {testMethod(18);
testMethod(@"lxz");
testMethod(YES);
}
return 0;
}
objc_runtime_name
objc_runtime_name
属性可以在编译时,将 Class
或Protocol
指定为另一个名字,并且新名字不受命名规范制约,可以以数字开头。
__attribute__((objc_runtime_name("TestObject")))
@interface Object : NSObject
@end
NSLog(@"%@", NSStringFromClass([TestObject class]));
执行结果:TestObject
这个属性可以用来做代码混淆,例如写一个宏定义,宏定义内部实现混淆逻辑。例如通过 MD5
对Object
做混淆,32 位的混淆结果就是497031794414a552435f90151ac3b54b
,谁能看出来这是什么类。如果怕彩虹表匹配出来,再增加加盐逻辑。
cleanup
通过 cleanup
属性,可以指定给一个变量,当变量释放之前执行一个函数。指定的函数执行的时间,是在 dealloc
之前的。在指定的函数中,可以传入一个形参,参数就是 cleanup
修饰的变量,形参是一个地址。
static void releaseBefore(NSObject **object) {NSLog(@"%@", *object);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {TestObject *object __attribute__((cleanup(releaseBefore))) = [[TestObject alloc] init];
}
return 0;
}
如果遇到同一个代码块中,同时出现多个 cleanup
属性时,在代码块作用域结束时,会以添加的顺序进行调用。
unused
还有一个属性很实用,在项目里经常会有未使用的变量,会报一个黄色警告。有时候可能会通过其他方式获取这个对象,所以不想出现这个警告,可以通过 unused
属性消除这个警告。
NSObject *object __attribute__((unused)) = [[NSObject alloc] init];
系统定义
在系统里也大量使用了 __attribute__
关键字,只不过系统不会直接在外部使用__attribute__
,一般都是将其定义为宏定义,以宏定义的形式出现在外面。
// NSLog
FOUNDATION_EXPORT void NSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2) NS_NO_TAIL_CALL;
#define NS_FORMAT_FUNCTION(F,A) __attribute__((format(__NSString__, F, A)))
// 必须调用父类的方法
#define NS_REQUIRES_SUPER __attribute__((objc_requires_super))
// 指定初始化方法,必须直接或间接调用修饰的方法
#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
ORM
对象关系映射 (Object Relational Mapping)
,简称ORM
,用于面向对象语言中不同系统数据之间的转换。
可以通过对象关系映射来实现 JSON
转模型,使用比较多的是 Mantle
、MJExtension
、YYKit
、JSONModel
等框架,这些框架在进行转换的时候,都是使用 Runtime
的方式实现的。
Mantle
使用和 MJExtension
有些类似,只不过 MJExtension
使用起来更加方便。Mantle
在使用时主要是通过继承的方式处理,而 MJExtension
是通过 Category
处理,代码依赖性更小,无侵入性。
性能评测
这些第三方中 Mantle
功能最强大,但是太臃肿,使用起来性能比其他第三方都差一些。JSONModel
、MJExtension
这些第三方几乎都在一个水平级,YYKit
相对来说性能可以比肩手写赋值代码,性价比最高。
对于模型转换需求不是太大的工程来说,尽量用 YYKit
来进行转换性能会更好一些。功能可能略逊于MJExtension
,我个人还是比较习惯用MJExtension
。
YYKit 作者评测
实现思路
也可以自己实现模型转换的逻辑,以字典转模型为例,大体逻辑如下:
- 创建一个
Category
用来做模型转换,对外提供方法并传入字典对象。 - 通过
Runtime
对应的函数,获取属性列表并遍历,根据属性名从字典中取出对应的对象。 - 通过
KVC
将从字典中取出的值,赋值给对象。 - 有时候会遇到多层嵌套的情况,例如字典包含数组,数组中还是一个字典。这种情况就可以做判断,如果模型对象是数组则取出字典对应字段的数组,然后遍历数组再调用字典赋值的方法。
下面简单实现了一个字典转模型的代码,通过 Runtime
遍历属性列表,并根据属性名取出字典中的对象,然后通过 KVC
进行赋值操作。调用方式和 MJExtension
、YYModel
类似,直接通过模型类调用类方法即可。如果想在其他类中也使用的话,应该把下面的实现写在 NSObject
的Category
中,这样所有类都可以调用。
// 调用部分
NSDictionary *dict = @{@"name" : @"lxz",
@"age" : @18,
@"gender" : @YES};
TestObject *object = [TestObject objectWithDict:dict];
// 实现代码
@interface TestObject : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) BOOL gender;
+ (instancetype)objectWithDict:(NSDictionary *)dict;
@end
@implementation TestObject
+ (instancetype)objectWithDict:(NSDictionary *)dict {return [[TestObject alloc] initWithDict:dict];
}
- (instancetype)initWithDict:(NSDictionary *)dict {self = [super init];
if (self) {
unsigned int count = 0;
objc_property_t *propertys = class_copyPropertyList([self class], &count);
for (int i = 0; i < count; i++) {objc_property_t property = propertys[i];
const char *name = property_getName(property);
NSString *nameStr = [[NSString alloc] initWithUTF8String:name];
id value = [dict objectForKey:nameStr];
[self setValue:value forKey:nameStr];
}
free(propertys);
}
return self;
}
@end
通过 Runtime
可以获取到对象的 Method List
、Property List
等,不只可以用来做字典模型转换,还可以做很多工作。例如还可以通过 Runtime
实现自动归档和反归档,下面是自动进行归档操作。
// 1. 获取所有的属性
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([NJPerson class], &count);
// 遍历所有的属性进行归档
for (int i = 0; i < count; i++) {
// 取出对应的属性
Ivar ivar = ivars[i];
const char * name = ivar_getName(ivar);
// 将对应的属性名称转换为 OC 字符串
NSString *key = [[NSString alloc] initWithUTF8String:name];
// 根据属性名称利用 KVC 获取数据
id value = [self valueForKeyPath:key];
[encoder encodeObject:value forKey:key];
}
free(ivars);
我写了一个简单的 Category
,可以自动实现NSCoding
、NSCopying
协议。这是开源地址:EasyNSCoding
Runtime 面试题
题 1
下面的代码输出什么?
@implementation Son : Father
- (id)init {self = [super init];
if (self) {NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
答案:都输出Son
。
第一个 NSLog
输出 Son
肯定是不用说的。
第二个输出中,[super class]
会被转换为下面代码。
struct objc_super objcSuper = {
self,
class_getSuperclass([self class]),
};
id (*sendSuper)(struct objc_super*, SEL) = (void *)objc_msgSendSuper;
sendSuper(&objcSuper, @selector(class));
super
的调用会被转换为 objc_msgSendSuper
的调用,并传入一个 objc_super
类型的结构体。结构体有两个参数,第一个就是接受消息的对象,第二个是 [super class]
对应的父类。
struct objc_super {
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class super_class;
};
由此可知,虽然调用的是 [super class]
,但是接受消息的对象还是self
。然后来到父类Father
的class
方法中,输出 self
对应的类Son
。
题 2
下面代码的结果?
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
答案:
除了第一个是YES
,其他三个都是NO
。
在推测结果之前,首先要明白两个问题。isKindOfClass
和 isMemberOfClass
的区别是什么?isKindOfClass:class
,调用该方法的对象所属的类,继承者链中包含传入的 class
则返回 YES
。isMemberOfClass:class
,调用改方法的对象所属的类,必须是传入的class
则返回YES
。
我们从 Runtime
源码的角度来分析一下结果。
+ (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
和isMemberOfClass
,但是在 NSObject
类中还隐式的实现了类方法版本。不只这两个方法,其他 NSObject
中的对象方法,都有其对应的类方法版本。因为在 OC 中,类和元类也都是对象。这四个调用由于都是类对象发起调用的,所以最终执行的都是类方法版本。
先把 Runtime
的对象模型拿出来,方便后面的分析。
第一次调用方是 NSObject
类对象,调用 isKindOfClass
方法传入的也是类对象。因为调用类的 class
方法,会把类自身直接返回,所以还是类对象自己。
然后进入到 for
循环中,会从 NSObject
的元类开始遍历,所以第一次 NSObject meta class!= NSObject class
,匹配失败。第二次循环将tcls
设置为 superclass
的NSObject class
,NSObject class == NSObject class
,匹配成功。
NSObject
能匹配成功,是因为这个类比较特殊,在第二次获取 superclass
的时候,NSObject
元类的 superclass
就是 NSObject
的类对象,所以会匹配成功。而其他三种匹配,则都会失败,各位同学可以去自己分析一下剩下三种。
题 3
下面的代码会?Compile Error
/ Runtime Crash
/ NSLog…
?
@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo {NSLog(@"IMP: -[NSObject (Sark) foo]");
}
@end
// 测试代码
[NSObject foo];
[[NSObject new] performSelector:@selector(foo)];
答案:
全都正常输出,编译和运行都没有问题。
这道题和上一道题很相似,第二个调用肯定没有问题,第一个调用后会从元类中查找方法,然而方法并不在元类中,所以找元类的 superclass
。方法定义在是NSObject
的Category
,由于 NSObject
的对象模型比较特殊,元类的 superclass
是类对象,所以从类对象中找到了方法并调用。
题 4
下面的代码会?Compile Error
/ Runtime Crash
/ NSLog…
?
@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Sark
- (void)speak {NSLog(@"my name's %@", self.name);
}
@end
// 测试代码
@implementation ViewController
- (void)viewDidLoad {[super viewDidLoad];
id cls = [Sark class];
void *obj = &cls;
[(__bridge id)obj speak];
}
@end
答案:
正常执行,不会导致Crash
。
执行 [Sark class]
后获取到类对象,然后通过 obj
指针指向获取到的类对象首地址,这就构成了对象的基本结构,可以进行正常调用。
原题出处
Sunnyxx- 神经病院 objc runtime 入院考试
题 5
为什么 MRC
下没有weak
?
其实 MRC
下并不是没有 weak
,在MRC
环境下也可以通过 Runtime
源码调用 weak
源码的。weak
源码定义在 Private Headers
私有文件夹下,需要引入 #import "objc-internal.h"
文件。
以以下 ARC
的源码为例,定义了一个 TestObject
类型的对象,并用一个 weak
指针指向已创建对象。
int main(int argc, const char * argv[]) {
@autoreleasepool {TestObject *object = [[TestObject alloc] init];
__weak TestObject *newObject = object;
}
return 0;
}
这段代码会被编译器转移为下面代码,这段代码中的两个函数就是 weak
的实现函数,在 MRC
下也可以调用这两个函数。
objc_initWeak(&newObject, object);
objc_destroyWeak(&newObject);
题 6
相同的一个类,创建不同的对象,怎样实现指定的某个对象在 dealloc
时打印一段文字?
这个问题最简单的方法就是在类的 .h
文件里,定义一个标记属性,如果属性被赋值为 YES
,则在dealloc
中打印文字。但是,这种实现方式显然不是面试官想要的,会被直接 pass~
可以参考 KVO
的实现方案,在运行时动态创建一个类,这个类是对象的子类,将新创建类的 dealloc
实现指向自定义的 IMP
,并在IMP
中打印一段文字。将对象的 isa
设置为新创建的类,当执行 dealloc
方法时就会执行 isa
所指向的新类。
思考
小问题
什么叫做技术大牛,怎样就表示技术强?
我前段时间看过一句话,我感觉可以解释上面的问题:“市面上所有应用的功能,产品提出来我都能做”。
这句话并不够全面,应该不只是做出来,而是更好的做出来。这个好要从很多方面去评估,性能、可维护性、完成时间、产品效果等,如果这些都做的很好,那足以证明这个人技术很强大。
Runtime 有什么用?
Runtime
是比较偏底层的,但是研究这么深有什么用吗,有什么实际意义吗?
Runtime
当然是由实际用处的,先不说整个 OC 都是通过 Runtime
实现的。例如现在需要实现消息转发的功能,这时候就需要用到Runtime
,或者是拦截方法,也需要用到Method Swizzling
,除了这些,还有更多的用法待我们去发掘。
不只是使用,其实最重要的是,通过 Runtime 了解一个语言的设计。Runtime 中不只是各种函数调用,从整体来看,可以明白 OC 的对象模型是什么样的。
简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我 Github
上,下载 Runtime PDF
合集。把所有 Runtime
文章总计九篇,都写在这个 PDF
中,而且左侧有目录,方便阅读。
下载地址:Runtime PDF
麻烦各位大佬点个赞,谢谢!????