探秘Runtime - Runtime的应用

栏目: IOS · 发布时间: 4年前

内容简介:下面是一些

__attribute__ 是一套编译器指令,被 GNULLVM 编译器所支持,允许对于 __attribute__ 增加一些参数,做一些高级检查和优化。

__attribute__ 的语法是,在后面加两个括号,然后写属性列表,属性列表以逗号分隔。在iOS中,很多例如 NS_CLASS_AVAILABLE_IOS 的宏定义,内部也是通过 __attribute__ 实现的。

__attribute__((attribute1, attribute2));
复制代码

下面是一些 __attribute__ 的常用属性,更完整的属性列表可以到llvm的官网查看。

探秘Runtime - Runtime的应用

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
复制代码

在有多个 constructordestructor 属性修饰的函数时,可以通过设置优先级来指定执行顺序。格式是 __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 属性可以在编译时,将 ClassProtocol 指定为另一个名字,并且新名字不受命名规范制约,可以以数字开头。

__attribute__((objc_runtime_name("TestObject")))
@interface Object : NSObject
@end

NSLog(@"%@", NSStringFromClass([TestObject class]));

执行结果:
TestObject
复制代码

这个属性可以用来做代码混淆,例如写一个宏定义,宏定义内部实现混淆逻辑。例如通过 MD5Object 做混淆,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 转模型,使用比较多的是 MantleMJExtensionYYKitJSONModel 等框架,这些框架在进行转换的时候,都是使用 Runtime 的方式实现的。

Mantle 使用和 MJExtension 有些类似,只不过 MJExtension 使用起来更加方便。 Mantle 在使用时主要是通过继承的方式处理,而 MJExtension 是通过 Category 处理,代码依赖性更小,无侵入性。

性能评测

这些第三方中 Mantle 功能最强大,但是太臃肿,使用起来性能比其他第三方都差一些。 JSONModelMJExtension 这些第三方几乎都在一个水平级, YYKit 相对来说性能可以比肩手写赋值代码,性价比最高。

对于模型转换需求不是太大的工程来说,尽量用 YYKit 来进行转换性能会更好一些。功能可能略逊于 MJExtension ,我个人还是比较习惯用 MJExtension

YYKit作者评测

实现思路

也可以自己实现模型转换的逻辑,以字典转模型为例,大体逻辑如下:

Category
Runtime
KVC

下面简单实现了一个字典转模型的代码,通过 Runtime 遍历属性列表,并根据属性名取出字典中的对象,然后通过 KVC 进行赋值操作。调用方式和 MJExtensionYYModel 类似,直接通过模型类调用类方法即可。如果想在其他类中也使用的话,应该把下面的实现写在 NSObjectCategory 中,这样所有类都可以调用。

// 调用部分
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 ListProperty 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 ,可以自动实现 NSCodingNSCopying 协议。这是开源地址: 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 。然后来到父类 Fatherclass 方法中,输出 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

在推测结果之前,首先要明白两个问题。 isKindOfClassisMemberOfClass 的区别是什么? isKindOfClass:class ,调用该方法的对象所属的类,继承者链中包含传入的 class 则返回 YESisMemberOfClass: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;
}
复制代码

平时开发过程中只会接触到对象方法的 isKindOfClassisMemberOfClass ,但是在 NSObject 类中还隐式的实现了类方法版本。不只这两个方法,其他 NSObject 中的对象方法,都有其对应的类方法版本。因为在OC中,类和元类也都是对象。这四个调用由于都是类对象发起调用的,所以最终执行的都是类方法版本。

先把 Runtime 的对象模型拿出来,方便后面的分析。

探秘Runtime - Runtime的应用

第一次调用方是 NSObject 类对象,调用 isKindOfClass 方法传入的也是类对象。因为调用类的 class 方法,会把类自身直接返回,所以还是类对象自己。

然后进入到 for 循环中,会从 NSObject 的元类开始遍历,所以第一次 NSObject meta class != NSObject class ,匹配失败。第二次循环将 tcls 设置为 superclassNSObject classNSObject 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 。方法定义在是 NSObjectCategory ,由于 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 - Runtime的应用

下载地址: Runtime PDF 麻烦各位大佬点个赞,谢谢!:grin:


以上所述就是小编给大家介绍的《探秘Runtime - Runtime的应用》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Maven实战

Maven实战

许晓斌 / 机械工业出版社 / 2010年12月 / 65.00元

你是否早已厌倦了日复一日的手工构建工作?你是否对各个项目风格迥异的构建系统感到恐惧?Maven——这一Java社区事实标准的项目管理工具,能帮你从琐碎的手工劳动中解脱出来,帮你规范整个组织的构建系统。不仅如此,它还有依赖管理、自动生成项目站点等超酷的特性,已经有无数的开源项目使用它来构建项目并促进团队交流,每天都有数以万计的开发者在访问中央仓库以获取他们需要的依赖。 本书内容全面而系统,Ma......一起来看看 《Maven实战》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具