iOS Runtime & libExtObjc

Runtime

Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统(runtime system) 来执行编译后的代码。这就是 Objective-C Runtime.
RunTime简称运行时。OC就是运行时机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

Runtime基本是用C和汇编写的,苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。

id&Class

define id&class

!Objc
typedef struct objc_class *Class;
typedef struct objc_object *id;
struct objc_object {
    Class isa;
};
struct objc_class {
    Class isa;
}
/// 不透明结构体, selector
typedef struct objc_selector *SEL;
/// 函数指针, 用于表示对象方法的实现
typedef id (*IMP)(id, SEL, ...);

Define

  • 对对象进行操作的方法一般以object_开头
  • 对类进行操作的方法一般以class_开头
  • 对类或对象的方法进行操作的方法一般以method_开头
  • 对成员变量进行操作的方法一般以ivar_开头
  • 对属性进行操作的方法一般以property_开头
  • 对协议进行操作的方法一般以protocol_开头

根据以上的函数的前缀 可以大致了解到层级关系。对于以objc_开头的方法,则是runtime最终的管家,可以获取内存中类的加载信息,类的列表,关联对象和关联属性等操作。

important method

objc_copyClassList

class_copyIvarList

class_copyPropertyList

ivarList可以获取到@property关键字定义的属性 ,而propertyList不可以获取到成员变量。使用ivarList是可以将所有的成员变量和属性都获取的。

+ (BOOL)resolveClassMethod:(SEL)sel 
+ (BOOL)resolveInstanceMethod:(SEL)sel

class_addIvar

class_addMethod

NSCoding default implement

!Objc
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(self.class, &count);
for (int i = 0; i < count; i++) {
    const char *cname = ivar_getName(ivars[i]);
    NSString *name = [NSString stringWithUTF8String:cname];
    NSString *key = [name substringFromIndex:1];
    
    id value = [self valueForKey:key]; // KVC隐性数据转换
    [aCoder encodeObject:value forKey:key]; // 编码
    }
}
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList(self.class, &count);
    for (int i = 0; i < count; i++) {
        const char *cname = ivar_getName(ivars[i]);
        NSString *name = [NSString stringWithUTF8String:cname];
        NSString *key = [name substringFromIndex:1];
        
        id value = [aDecoder decodeObjectForKey:key]; // 解码
        [self setValue:value forKey:key]; // KVC隐性数据转换
    }
}
return self;    
}

Add method Dynamic

!Objc
void abc(id self, SEL _cmd){
NSLog(@"%@说了hello", [self name]);
}

@implementation Person

//动态添加方法:在resolve中添加相应的方法,注意是类方法还是对象方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if ([NSStringFromSelector(sel) isEqualToString:@"sayHi"]) {
    class_addMethod(self, sel, abc, "v@:"); // 为sel指定实现为abc
}
return YES;
}
@end

Add Class Dynamic

!Objc
// 添加一个NSString的变量,第四个参数是对其方式,第五个参数是参数类型
if (class_addIvar(classStudent, "schoolName", sizeof(NSString *), 0, "@")) {
    NSLog(@"添加成员变量schoolName成功");
}
// 为Student类添加方法 "v@:"这种写法见参数类型连接
if (class_addMethod(classStudent, @selector(printSchool), (IMP)printSchool, "v@:")) {
    NSLog(@"添加方法printSchool:成功");
}
// 注册这个类到runtime系统中就可以使用他了
objc_registerClassPair(classStudent); // 返回void
// 使用创建的类
id student = [[classStudent alloc] init];
NSString *schoolName = @"清华大学";
[student setValue:schoolName forKey:@"schoolName"];
[student performSelector:@selector(printSchool) withObject:nil]; // 动态调用未显式在类中声明的方法

AssociatedObject&Category

  • objc_setAssociatedObject

  • objc_getAssociatedObject

  • objc_removeAssociatedObjects

      !Objc
      enum {
      OBJC_ASSOCIATION_ASSIGN = 0,
      OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
      OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
      OBJC_ASSOCIATION_RETAIN = 01401,
      OBJC_ASSOCIATION_COPY = 01403
      };
    

Category invoke order: Category => self => super

当同一个类的有多个Category时,调用同名方法,谁编译最后,谁就会被调用。最后编译的那个Category,其方法被放在了方法列表(无论是类里的实例方法列表还是元类里的类方法列表)的前面,当objc_msgSend查找方法时会优先找到了它。

message

[receiver message];

向receiver发送名为message的消息。

clang -rewrite-objc MyClass.m

执行上面的命令,将这一句重写为C代码,是这样的:

((void (*)(id, SEL))(void *)objc_msgSend)((id)receiver, sel_registerName("message"));

去掉那些强制转换,最终[receiver message]会由编译器转化为以下的纯C调用。

objc_msgSend(receiver, @selector(message));

method forwarding

<img src="http://upload-images.jianshu.io/upload_images/44181-09a882edcb98d535.JPEG?imageMogr2/auto-orient/strip%7CimageView2/2" width = "700" height = "500" alt="image" align=center />

_objc_msgForward消息转发做了如下几件事:
1.调用resolveInstanceMethod:方法,允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回。如果仍没实现,继续下面的动作。

2.调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。

3.调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。

4.调用forwardInvocation:方法,将地3步获取到的方法签名包装成Invocation传入,如何处理就在这里面了。

上面这4个方法均是模板方法,开发者可以override,由runtime来调用。最常见的实现消息转发,就是重写方法3和4,吞掉一个消息或者代理给其他对象都是没问题的。

如果直到NSObject,继承体系中的其它类都无法处理这个消息转发,就会由NSObject调用该方法,并在该方法中调用doesNotRecognizeSelector,以抛出异常。

method swizzling

!Objc
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    Class class = [self class];
    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(xxx_viewWillAppear:);
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    // 如果 swizzling 的是类方法, 采用如下的方式:
    // Class class = object_getClass((id)self);
    // ...
    // Method originalMethod = class_getClassMethod(class, originalSelector);
    // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
    //交换实现
    method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

ThirdParty

Aspect

JsPatch

GYBootingProtection

libExtObjc

source code

  • https://github.com/jspahrsummers/libextobjc
  • Safe categories
  • Concrete protocols
  • Simpler and safer key paths
  • Easier use of weak variables in blocks
  • Scope-based resource cleanup
  • Algebraic data types
  • Block-based coroutines
  • EXTNil
  • Lots of extensions

swift defer

defer 是 Swift 在 2.0 时代加入的一个关键字,它提供了一种非常安全并且简单的方法声明一个在作用域结束时执行的代码块。

!swift
func hello() {
defer {
    print("4")
}
if true {
    defer {
        print("2")
    }
    defer {
        print("1")
    }
}
print("3")
}

hello()

output

1  2  3  4 

Objc implement of defer

libextobjc 实现的 defer 并没有基于 Objective-C 的动态特性,甚至也没有调用已有的任何方法,而是使用了 Variable Attributes 这一特性。同样在 GCC 中也存在用于修饰函数的 Function Attributes.

Variable Attributes 其实是 GCC 中用于描述变量的一种修饰符。我们可以使用 attribute 来修饰一些变量来参与静态分析等编译过程;而在 Cocoa Touch 中很多的宏其实都是通过 attribute 来实现的,例如:

#define NS_ROOT_CLASS __attribute__((objc_root_class))

cleanup 就是在这里会使用的变量属性:

The cleanup attribute runs a function when the variable goes out of scope. This attribute can only be applied to auto function scope variables; it may not be applied to parameters or variables with static storage duration. The function must take one parameter, a pointer to a type compatible with the variable. The return value of the function (if any) is ignored.

GCC 文档中对 Cleanup 属性的介绍告诉我们,在 cleanup 中必须传入只有一个参数的函数并且这个参数需要与变量的类型兼容。

!c
void cleanup_block(int *a) {
printf("%d\n", *a);
}

int variable __attribute__((cleanup(cleanup_block))) = 2;

onExit

libextobjc 中并没有使用 defer 这个名字,而是使用了 onExit(表示代码是在退出作用域时执行

!Objc
#define onExit \
ext_keywordify \
__strong ext_cleanupBlock_t metamacro_concat(ext_exitBlock_, __LINE__) __attribute__((cleanup(ext_executeCleanupBlock), unused)) = ^

既然它只是一个宏,那么上面的代码其实是可以展开的

autoreleasepool {}
__strong ext_cleanupBlock_t ext_exitBlock_19    __attribute__((cleanup(ext_executeCleanupBlock), unused)) = ^ {
NSLog("Log when out of scope.");
};

ext_keywordify 也是一个宏定义,它通过添加在宏之前添加 autoreleasepool {} 强迫 onExit 前必须加上 @ 符号

#define ext_keywordify autoreleasepool {}

ext_cleanupBlock_t 是一个类型:

typedef void (^ext_cleanupBlock_t)();

metamacro_concat(ext_exitBlock_, LINE) 会将 ext_exitBlock 和当前行号拼接成一个临时的的变量名,例如:ext_exitBlock_19。

__attribute__((cleanup(ext_executeCleanupBlock), unused)) 

将 cleanup 函数设置为 ext_executeCleanupBlock;并将当前变量 ext_exitBlock_19 标记为 unused 来抑制 Unused variable 警告。

变量 ext_exitBlock_19 的值为 ^{ NSLog("Log when out of scope."); },是一个类型为 ext_cleanupBlock_t 的 block。

在这个变量离开作用域时,会把上面的 block 的指针传入 cleanup 函数,也就是 ext_executeCleanupBlock:

void ext_executeCleanupBlock (__strong ext_cleanupBlock_t *block)   {
    (*block)();
}

Corutines&Yield

TestCase

!Objc
__block int i;
int (^myCoroutine)(void) = coroutine(void)({
     for (i = 0;i < 3;++i) {
            yield i;
        }
    });
XCTAssertEqual(myCoroutine(), 0, @"expected first coroutine call to yield 0");
XCTAssertEqual(myCoroutine(), 1, @"expected second coroutine call to yield 1");
XCTAssertEqual(myCoroutine(), 2, @"expected third coroutine call to yield 2");
XCTAssertEqual(myCoroutine(), 0, @"expected restarted coroutine call to yield 0");
XCTAssertEqual(myCoroutine(), 1, @"expected second coroutine call to yield 1");
    
 myCoroutine = coroutine(void)({
        NSLog(@"invoke step 1");
        yield 5;
        NSLog(@"invoke step 2");
        yield 18;
        NSLog(@"invoke step 3");
 });
 XCTAssertEqual(myCoroutine(), 5, @"expected first coroutine call to yield 5");
 XCTAssertEqual(myCoroutine(), 18, @"expected second coroutine call to yield 18");
 XCTAssertEqual(myCoroutine(), 5, @"expected restarted coroutine call to yield 5");

Corutines&Yield Implement

!Objc
#define coroutine(...) \
^{ \
    __block unsigned long ext_coroutine_line_ = 0; \
    \
    return [ \
        ^(__VA_ARGS__) coroutine_body
        
#define yield \
if ((ext_coroutine_line_ = __LINE__) == 0) \
    case __LINE__: \
        ; \
else \
    return
#define coroutine_body(STATEMENT) \
        { \
            for (;; ext_coroutine_line_ = 0) \
                switch (ext_coroutine_line_) \
                    default: \
                        STATEMENT \
        } \
    copy]; \
}()

Concrete protocols

TestCase

!Objc
@protocol MyProtocol <NSObject>
@concrete
+ (NSUInteger)meaningfulNumber;
- (NSString *)getSomeString;
@end
@protocol SubProtocol <MyProtocol>
@concrete
- (void)additionalMethod;
@end
@concreteprotocol(MyProtocol)
+ (void)initialize {
NSLog(@"  MyProtocol  +initialize should only be invoked once per   concrete protocol");
}
+ (NSUInteger)meaningfulNumber {
return 42;
}
- (NSString *)getSomeString {
return @"MyProtocol";
}
@end
/*** SubProtocol ***/
@concreteprotocol(SubProtocol)
+ (void)initialize {
NSLog(@"SubProtocol +initialize should only be invoked once per         concrete protocol");
}
- (void)additionalMethod {}
// this should take precedence over the implementation in MyProtocol
- (NSString *)getSomeString {
return @"SubProtocol";
}
@end
@interface TestClass : NSObject <MyProtocol> {}
@end
@implementation TestClass
+ (NSUInteger)meaningfulNumber {
return 0;
}
@end
@interface TestClass5 : TestClass <SubProtocol> {}
@end
@implementation TestClass5
@end




!Objc
TestClass *obj = [[TestClass alloc] init];
XCTAssertNotNil(obj, @"could not allocate concreteprotocol'd class");
    XCTAssertEqualObjects([obj getSomeString], @"MyProtocol", @"TestClass should be using protocol implementation of getSomeString");
    
    obj = [[TestClass5 alloc] init];
    XCTAssertNotNil(obj, @"could not allocate concreteprotocol'd class");
    XCTAssertTrue([obj respondsToSelector:@selector(additionalMethod)], @"TestClass5 should have protocol implementation of additionalMethod");
    XCTAssertEqualObjects([obj getSomeString], @"SubProtocol", @"TestClass5 should be using SubProtocol implementation of getSomeString");
    
    XCTAssertEqual([TestClass5 meaningfulNumber], (NSUInteger)0, @"TestClass5 should not be using protocol implementation of meaningfulNumber");

Concrete protocols implement

!Objc
#define concreteprotocol(NAME) \
/*
 * create a class used to contain all the methods used in this protocol
 */ \
interface NAME ## _ProtocolMethodContainer : NSObject < NAME > {} \
@end \
\
@implementation NAME ## _ProtocolMethodContainer \
/*
 * when this class is loaded into the runtime, add the concrete protocol
 * into the list we have of them
 */ \
+ (void)load { \
    /*
     * passes the actual protocol as the first parameter, then this class as
     * the second
     */ \
    if (!ext_addConcreteProtocol(objc_getProtocol(metamacro_stringify(NAME)), self)) \
        fprintf(stderr, "ERROR: Could not load concrete protocol %s\n", metamacro_stringify(NAME)); \
} \
\
/*
 * using the "constructor" function attribute, we can ensure that this
 * function is executed only AFTER all the Objective-C runtime setup (i.e.,
 * after all +load methods have been executed)
 */ \
__attribute__((constructor)) \
static void ext_ ## NAME ## _inject (void) { \
    /*
     * use this injection point to mark this concrete protocol as ready for
     * loading
     */ \
    ext_loadConcreteProtocol(objc_getProtocol(metamacro_stringify(NAME))); \
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,676评论 0 9
  • objc_getAssociatedObject返回与给定键的特定对象关联的值。ID objc_getAssoci...
    有一种再见叫青春阅读 1,552评论 0 7
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 741评论 0 1
  • 不知道大家有没有被老师喊家长的经历,有没有上课时间老师打电话过来让你的孩子亲口告诉你,刚刚在校半天已经发生的四五件...
    周京京阅读 488评论 3 2
  • 屎尿屁是今早忽然想到的,第一时间是觉得低俗,哈哈,后来又发自内心的觉得贴切。 可不是么,每天就是围绕着这些个指标在...
    NiraraX阅读 540评论 0 0