iOS Runtime & libExtObjc

Runtime

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

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

  • https://opensource.apple.com/tarballs/objc4/
  • https://github.com/RetVal/objc-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

image

_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

  • https://github.com/steipete/Aspects

JsPatch

  • https://github.com/bang590/JSPatch

GYBootingProtection

  • https://github.com/liuslevis/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 
@concrete
+ (NSUInteger)meaningfulNumber;
- (NSString *)getSomeString;
@end
@protocol SubProtocol 
@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  {}
@end
@implementation TestClass
+ (NSUInteger)meaningfulNumber {
return 0;
}
@end
@interface TestClass5 : TestClass  {}
@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))); \
}

你可能感兴趣的:(iOS Runtime & libExtObjc)