1. 程式人生 > >iOS Objc Runtime 教程+例項Demo

iOS Objc Runtime 教程+例項Demo

例子Demo
歡迎給我star!我會繼續分享的。

概述

Objc Runtime使得C具有了面向物件能力,在程式執行時建立,檢查,修改類、物件和它們的方法。Runtime是C和彙編編寫的,這裡http://www.opensource.apple.com/source/objc4/可以下到蘋果維護的開原始碼,GNU也有一個開源的runtime版本,他們都努力的保持一致。

應用場景

  1. 將某些OC程式碼轉為執行時程式碼,探究底層,比如block的實現原理
  2. 攔截系統自帶的方法呼叫(Swizzle 黑魔法),比如攔截imageNamed:、viewDidLoad、alloc
  3. 實現分類也可以增加屬性
  4. 實現NSCoding的自動歸檔和自動解檔
  5. 實現字典和模型的自動轉換。(MJExtension)
  6. 修BUG神器,如果大型框架的BUG 通過Runtime來解決,非常好用。

一些常用型別

Method

Method
An opaque type that represents a method in a class definition.
Declaration
typedef struct objc_method *Method;

代表類定義中的方法的不透明型別。

Class

Class
An opaque type that represents an Objective-C class.
Declaration
typedef struct objc_class *Class;

代表Objective-C中的類

Ivar

An opaque type that represents an instance variable.
Declaration
typedef struct objc_ivar *Ivar;

代表例項變數

IMP

IMP
A pointer to the start of a method implementation.

指向方法實現的開始的記憶體地址的指標。

SEL

SEL
Defines an opaque type that represents a method selector.
Declaration
typedef struct objc_selector *SEL;

代表方法的選擇器

類與物件操作函式

runtime有很多的函式可以操作類和物件。類相關的是class為字首,物件相關操作是objc或object_為字首。

類相關操作函式

name

// 獲取類的類名

const char * class_getName ( Class cls );

super_class和meta-class

// 獲取類的父類

Class class_getSuperclass ( Class cls );

// 判斷給定的Class是否是一個meta class

BOOL class_isMetaClass ( Class cls );

instance_size

// 獲取例項大小

size_t class_getInstanceSize ( Class cls );

成員變數(ivars)及屬性

//成員變數操作函式
// 獲取類中指定名稱例項成員變數的資訊
Ivar class_getInstanceVariable ( Class cls, const char *name );

// 獲取類成員變數的資訊
Ivar class_getClassVariable ( Class cls, const char *name );

// 新增成員變數
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types ); //這個只能夠向在runtime時建立的類新增成員變數

// 獲取整個成員變數列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount ); //必須使用free()來釋放這個陣列

//屬性操作函式
// 獲取類中指定名稱例項成員變數的資訊
Ivar class_getInstanceVariable ( Class cls, const char *name );

// 獲取類成員變數的資訊
Ivar class_getClassVariable ( Class cls, const char *name );

// 新增成員變數
BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );

// 獲取整個成員變數列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );

methodLists


// 新增方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types ); //和成員變數不同的是可以為類動態新增方法。如果有同名會返回NO,修改的話需要使用method_setImplementation

// 獲取例項方法
Method class_getInstanceMethod ( Class cls, SEL name );

// 獲取類方法
Method class_getClassMethod ( Class cls, SEL name );

// 獲取所有方法的陣列
Method * class_copyMethodList ( Class cls, unsigned int *outCount );

// 替代方法的實現
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );

// 返回方法的具體實現
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );

// 類例項是否響應指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );

objc_protocol_list

// 新增協議
BOOL class_addProtocol ( Class cls, Protocol *protocol );

// 返回類是否實現指定的協議
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );

// 返回類實現的協議列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );

version

// 獲取版本號
int class_getVersion ( Class cls );

// 設定版本號
void class_setVersion ( Class cls, int version );

例項

//-----------------------------------------------------------
// MyClass.h
@interface MyClass : NSObject <NSCopying, NSCoding>
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, copy) NSString *string;
- (void)method1;
- (void)method2;
+ (void)classMethod1;
@end

//-----------------------------------------------------------
// MyClass.m
#import "MyClass.h"
@interface MyClass () {
NSInteger _instance1;
NSString * _instance2;
}
@property (nonatomic, assign) NSUInteger integer;
- (void)method3WithArg1:(NSInteger)arg1 arg2:(NSString *)arg2;
@end

@implementation MyClass
+ (void)classMethod1 {
}

- (void)method1 {
     NSLog(@"call method method1");
}

- (void)method2 {
}

- (void)method3WithArg1:(NSInteger)arg1 arg2:(NSString *)arg2 {
     NSLog(@"arg1 : %ld, arg2 : %@", arg1, arg2);
}

@end

//-----------------------------------------------------------
// main.h

#import "MyClass.h"
#import "MySubClass.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
     @autoreleasepool {
          MyClass *myClass = [[MyClass alloc] init];
          unsigned int outCount = 0;
          Class cls = myClass.class;
          // 類名
          NSLog(@"class name: %s", class_getName(cls));    
          NSLog(@"==========================================================");

          // 父類
          NSLog(@"super class name: %s", class_getName(class_getSuperclass(cls)));
          NSLog(@"==========================================================");

          // 是否是元類
          NSLog(@"MyClass is %@ a meta-class", (class_isMetaClass(cls) ? @"" : @"not"));
          NSLog(@"==========================================================");

          Class meta_class = objc_getMetaClass(class_getName(cls));
          NSLog(@"%s's meta-class is %s", class_getName(cls), class_getName(meta_class));
          NSLog(@"==========================================================");

          // 變數例項大小
          NSLog(@"instance size: %zu", class_getInstanceSize(cls));
          NSLog(@"==========================================================");

          // 成員變數
          Ivar *ivars = class_copyIvarList(cls, &outCount);
          for (int i = 0; i < outCount; i++) {
               Ivar ivar = ivars[i];
               NSLog(@"instance variable's name: %s at index: %d", ivar_getName(ivar), i);
          }

          free(ivars);

          Ivar string = class_getInstanceVariable(cls, "_string");
          if (string != NULL) {
               NSLog(@"instace variable %s", ivar_getName(string));
          }

          NSLog(@"==========================================================");

          // 屬性操作
          objc_property_t * properties = class_copyPropertyList(cls, &outCount);
          for (int i = 0; i < outCount; i++) {
               objc_property_t property = properties[i];
               NSLog(@"property's name: %s", property_getName(property));
          }

          free(properties);

          objc_property_t array = class_getProperty(cls, "array");
          if (array != NULL) {
               NSLog(@"property %s", property_getName(array));
          }

          NSLog(@"==========================================================");

          // 方法操作
          Method *methods = class_copyMethodList(cls, &outCount);
          for (int i = 0; i < outCount; i++) {
               Method method = methods[i];
               NSLog(@"method's signature: %s", method_getName(method));
          }

          free(methods);

          Method method1 = class_getInstanceMethod(cls, @selector(method1));
          if (method1 != NULL) {
               NSLog(@"method %s", method_getName(method1));
          }

          Method classMethod = class_getClassMethod(cls, @selector(classMethod1));
          if (classMethod != NULL) {
               NSLog(@"class method : %s", method_getName(classMethod));
          }

          NSLog(@"MyClass is%@ responsd to selector: method3WithArg1:arg2:", class_respondsToSelector(cls, @selector(method3WithArg1:arg2:)) ? @"" : @" not");

          IMP imp = class_getMethodImplementation(cls, @selector(method1));
          imp();

          NSLog(@"==========================================================");

          // 協議
          Protocol * __unsafe_unretained * protocols = class_copyProtocolList(cls, &outCount);
          Protocol * protocol;
          for (int i = 0; i < outCount; i++) {
               protocol = protocols[i];
               NSLog(@"protocol name: %s", protocol_getName(protocol));
          }

          NSLog(@"MyClass is%@ responsed to protocol %s", class_conformsToProtocol(cls, protocol) ? @"" : @" not", protocol_getName(protocol));

          NSLog(@"==========================================================");
     }
     return 0;
}

動態建立類和物件

動態建立類

// 建立一個新類和元類
Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes ); //如果建立的是root class,則superclass為Nil。extraBytes通常為0

// 銷燬一個類及其相關聯的類
void objc_disposeClassPair ( Class cls ); //在執行中還存在或存在子類例項,就不能夠呼叫這個。

// 在應用中註冊由objc_allocateClassPair建立的類
void objc_registerClassPair ( Class cls ); //建立了新類後,然後使用class_addMethod,class_addIvar函式為新類新增方法,例項變數和屬性後再呼叫這個來註冊類,再之後就能夠用了。

使用例項

Class cls = objc_allocateClassPair(MyClass.class, "MySubClass", 0);
class_addMethod(cls, @selector(submethod1), (IMP)imp_submethod1, "[email protected]:");
class_replaceMethod(cls, @selector(method1), (IMP)imp_submethod1, "[email protected]:");
class_addIvar(cls, "_ivar1", sizeof(NSString *), log(sizeof(NSString *)), "i");

objc_property_attribute_t type = {"T", "@\"NSString\""};
objc_property_attribute_t ownership = { "C", "" };
objc_property_attribute_t backingivar = { "V", "_ivar1"};
objc_property_attribute_t attrs[] = {type, ownership, backingivar};

class_addProperty(cls, "property2", attrs, 3);
objc_registerClassPair(cls);

id instance = [[cls alloc] init];
[instance performSelector:@selector(submethod1)];
[instance performSelector:@selector(method1)];

動態建立物件

// 建立類例項
id class_createInstance ( Class cls, size_t extraBytes ); //會在heap裡給類分配記憶體。這個方法和+alloc方法類似。

// 在指定位置建立類例項
id objc_constructInstance ( Class cls, void *bytes );

// 銷燬類例項
void * objc_destructInstance ( id obj ); //不會釋放移除任何相關引用

測試下效果

//可以看出class_createInstance和alloc的不同
id theObject = class_createInstance(NSString.class, sizeof(unsigned));
id str1 = [theObject init];
NSLog(@"%@", [str1 class]);
id str2 = [[NSString alloc] initWithString:@"test"];
NSLog(@"%@", [str2 class]);

例項操作函式

這些函式是針對建立的例項物件的一系列操作函式。

整個物件操作的函式

// 返回指定物件的一份拷貝
id object_copy ( id obj, size_t size );
// 釋放指定物件佔用的記憶體
id object_dispose ( id obj );

應用場景

//把a轉換成佔用更多空間的子類b
NSObject *a = [[NSObject alloc] init];
id newB = object_copy(a, class_getInstanceSize(MyClass.class));
object_setClass(newB, MyClass.class);
object_dispose(a);

物件例項變數進行操作的函式

// 修改類例項的例項變數的值
Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
// 獲取物件例項變數的值
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
// 返回指向給定物件分配的任何額外位元組的指標
void * object_getIndexedIvars ( id obj );
// 返回物件中例項變數的值
id object_getIvar ( id obj, Ivar ivar );
// 設定物件中例項變數的值
void object_setIvar ( id obj, Ivar ivar, id value );

對物件類操作

// 返回給定物件的類名
const char * object_getClassName ( id obj );
// 返回物件的類
Class object_getClass ( id obj );
// 設定物件的類
Class object_setClass ( id obj, Class cls );

獲取類定義

// 獲取已註冊的類定義的列表
int objc_getClassList ( Class *buffer, int bufferCount );

// 建立並返回一個指向所有已註冊類的指標列表
Class * objc_copyClassList ( unsigned int *outCount );

// 返回指定類的類定義
Class objc_lookUpClass ( const char *name );
Class objc_getClass ( const char *name );
Class objc_getRequiredClass ( const char *name );

// 返回指定類的元類
Class objc_getMetaClass ( const char *name );

演示如何使用

int numClasses;
Class * classes = NULL;
numClasses = objc_getClassList(NULL, 0);

if (numClasses > 0) {
     classes = malloc(sizeof(Class) * numClasses);
     numClasses = objc_getClassList(classes, numClasses);
     NSLog(@"number of classes: %d", numClasses);

     for (int i = 0; i < numClasses; i++) {
          Class cls = classes[i];
          NSLog(@"class name: %s", class_getName(cls));
     }
     free(classes);
}

設定關聯值

Example : 在category 中新增物件

//.h
#import <UIKit/UIKit.h>
#import <objc/runtime.h>


@interface UIView (AssociatedObject)

@property (nonatomic, strong) id associatedObject;

@end

//.m
#import "UIView+AssociatedObject.h"

@implementation UIView (AssociatedObject)

static char kAssociatedObjectKey;


- (void)setAssociatedObject:(id)associatedObject {
    objc_setAssociatedObject(self, &kAssociatedObjectKey, associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)associatedObject {
    return objc_getAssociatedObject(self, &kAssociatedObjectKey);
}

objc_setAssociatedObject,給指定的物件設定關聯值。

objc_setAssociatedObject
Sets an associated value for a given object using a given key and association policy.
Declaration
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
Parameters
object
The source object for the association.
key
The key for the association.
value
The value to associate with the key key for object. Pass nil to clear an existing association.
policy
The policy for the association. For possible values, see Associative Object Behaviors.

  • object 指定的物件
  • const void *key key
  • value 值
  • policy 儲存策略
Behavior @property Equivalent Description
OBJC_ASSOCIATION_ASSIGN @property (assign) 或 @property (unsafe_unretained) 指定一個關聯物件的弱引用。
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) 指定一個關聯物件的強引用,不能被原子化使用。
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) 指定一個關聯物件的copy引用,不能被原子化使用。
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) 指定一個關聯物件的強引用,能被原子化使用。
OBJC_ASSOCIATION_COPY @property (atomic, copy) 指定一個關聯物件的copy引用,能被原子化使用。

objc_getAssociatedObject
Returns the value associated with a given object for a given key.
Declaration
id objc_getAssociatedObject(id object, const void *key);
Parameters
object
The source object for the association.
key
The key for the association.
Return Value
The value associated with the key key for object.

objc_getAssociatedObject

返回給定物件的key的關聯值
- object 關聯的源物件
- key 關聯的key
- Return Value 與物件的key相關聯的值。

objc_removeAssociatedObjects

objc_removeAssociatedObjects
Removes all associations for a given object.
Declaration
void objc_removeAssociatedObjects(id object);
Parameters
object
An object that maintains associated objects.
Discussion
The main purpose of this function is to make it easy to return an object to a “pristine state”. You should not use this function for general removal of associations from objects, since it also removes associations that other clients may have added to the object. Typically you should use objc_setAssociatedObject with a nil value to clear an association.

刪除給定物件的所有關聯。
- object 物件(關聯了許多值)
- 這個函式的主要目的是使物件返回一個“原始狀態”,你不應該使用這個函式從物件中刪除關聯,因為它也刪除了其他客戶端可能新增到物件的關聯 。通常應該使用帶有nil值的objc_setAssociatedObject來清除關聯。

優秀樣例

  • 新增私有屬性用於更好地去實現細節。當擴充套件一個內建類的行為時,保持附加屬性的狀態可能非常必要。注意以下說的是一種非常教科書式的關聯物件的用例:AFNetworking在 UIImageView 的category上用了關聯物件來保持一個operation物件,用於從網路上某URL非同步地獲取一張圖片。

  • 新增public屬性來增強category的功能。有些情況下這種(通過關聯物件)讓category行為更靈活的做法比在用一個帶變數的方法來實現更有意義。在這些情況下,可以用關聯物件實現一個一個對外開放的屬性。回到上個AFNetworking的例子中的 UIImageView category,它的 imageResponseSerializer方法允許圖片通過一個濾鏡來顯示、或在快取到硬碟之前改變圖片的內容。

  • 建立一個用於KVO的關聯觀察者。當在一個category的實現中使用KVO時,建議用一個自定義的關聯物件而不是該物件本身作觀察者。ng an associated observer for KVO**. When using KVO in a category implementation, it is recommended that a custom associated-object be used as an observer, rather than the object observing itself.

反例

  • 當值不需要的時候建立一個關聯物件。一個常見的例子就是在view上建立一個方便的方法去儲存來自model的屬性、值或者其他混合的資料。如果那個資料在之後根本用不到,那麼這種方法雖然是沒什麼問題的,但用關聯到物件的做法並不可取。

  • 當一個值可以被其他值推算出時建立一個關聯物件。例如:在呼叫 cellForRowAtIndexPath: 時儲存一個指向view的 UITableViewCell 中accessory view的引用,用於在 tableView:accessoryButtonTappedForRowWithIndexPath: 中使用。

  • 用關聯物件替代X,這裡的X可以代表下列含義:

    1. 當繼承比擴充套件原有的類更方便時用子類化。
    2. 為事件的響應者新增響應動作。
    3. 當響應動作不方便使用時使用的手勢動作捕捉。
    4. 行為可以在其他物件中被代理實現時要用代理(delegate)。
    5. 用NSNotification 和 NSNotificationCenter進行鬆耦合化的跨系統的事件通知。

動態新增方法

Example:

- (IBAction)addMethod:(id)sender {
    [self addMethodForPerson];
    if ([self.xjy respondsToSelector:@selector(speakMyName)]) {
        [self.xjy performSelector:@selector(speakMyName)];
    } else {
        NSLog(@"未新增成功");
    }
}

- (void)addMethodForPerson {
    class_addMethod([self.xjy class], @selector(speakMyName), (IMP)speakMyName, "[email protected]:*");
}

void speakMyName(id self,SEL _cmd) {
    NSLog(@"新增成功啊QAQ");
}

class_addMethod

class_addMethod
Adds a new method to a class with a given name and implementation.
Declaration
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
Parameters
cls
The class to which to add a method.
name
A selector that specifies the name of the method being added.
imp
A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
types
An array of characters that describe the types of the arguments to the method. For possible values, see Objective-C Runtime Programming Guide > Type Encodings. Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).
Return Value
YES if the method was added successfully, otherwise NO (for example, the class already contains a method implementation with that name).

給一個類新增方法
- cls 被新增方法的類
- name 新增的方法的名稱的SEL
- imp 方法的實現。該函式必須至少要有兩個引數,self,_cmd.

class_addMethod新增實現將覆蓋父類的實現,但不會替換此類中的現有實現。 要更改現有實現,請使用method_setImplementation。
Objective-C方法只是一個C函式,至少需要兩個引數 - self和_cmd。 例如,給定以下函式:

void myMethodIMP(id self,SEL _cmd)
{
     // implementation ....
}}

你可以動態地將它新增到類作為一個方法(稱為resolveThisMethodDynamically)像這樣:

class_addMethod([self class],@selectorresolveThisMethodDynamically),(IMPmyMethodIMP,“v @:”);

型別編碼

Type Encodings
To assist the runtime system, the compiler encodes the return and argument types for each method in a character string and associates the string with the method selector.

為了輔助執行時系統,編譯器對字串中每個方法的返回和引數型別進行編碼,並將字串與方法選擇器相關聯。 它使用的編碼方案在其他上下文中也很有用,因此可以通過@encode()編譯器指令公開獲得。 當給定型別規範時,@encode()返回該型別的字串編碼。 型別可以是基本型別,例如int,指標,標記結構或聯合,或類名 - 實際上可以用作C sizeof()運算子的引數的任何型別。

動態交換方法實現

Example:

#import "UIViewController+LogTracking.h"
#import <objc/runtime.h>

@implementation UIViewController (LogTracking)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        Class class = [self class];
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xjy_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class,originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);

        //judge the method named  swizzledMethod is already existed.
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        // if swizzledMethod is already existed.
        if (didAddMethod) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}


- (void)xjy_viewWillAppear:(BOOL)animated {
    [self xjy_viewWillAppear:animated];
    NSLog(@"viewWillAppear : %@",self);
}
@end

+load vs +initialize

swizzling應該只在+load中完成。 在 Objective-C 的執行時中,每個類有兩個方法都會自動呼叫。+load 是在一個類被初始裝載時呼叫,+initialize 是在應用第一次呼叫該類的類方法或例項方法前呼叫的。兩個方法都是可選的,並且只有在方法被實現的情況下才會被呼叫。

dispatch_once

swizzling 應該只在 dispatch_once 中完成

由於 swizzling 改變了全域性的狀態,所以我們需要確保每個預防措施在執行時都是可用的。原子操作就是這樣一個用於確保程式碼只會被執行一次的預防措施,就算是在不同的執行緒中也能確保程式碼只執行一次。Grand Central Dispatch 的 dispatch_once 滿足了所需要的需求,並且應該被當做使用 swizzling 的初始化單例方法的標準。

method_getImplementation

method_getImplementation
Returns the implementation of a method.
Declaration
IMP method_getImplementation(Method m);
Parameters
method
The method to inspect.
Return Value
A function pointer of type IMP.

返回方法的實現
- method Method

method_getTypeEncoding

method_getTypeEncoding
Returns a string describing a method’s parameter and return types.
Declaration
const char * method_getTypeEncoding(Method m);
Parameters
method
The method to inspect.
Return Value
A C string. The string may be NULL.

返回一個C 字串,描述方法的引數和返回型別.
- method Method

class_replaceMethod

class_replaceMethod
Replaces the implementation of a method for a given class.
Declaration
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
Parameters
cls
The class you want to modify.
name
A selector that identifies the method whose implementation you want to replace.
imp
The new implementation for the method identified by name for the class identified by cls.
types
An array of characters that describe the types of the arguments to the method. For possible values, see Objective-C Runtime Programming Guide > Type Encodings. Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).
Return Value
The previous implementation of the method identified by name for the class identified by cls.

替換指定方法的實現
- cls class
- name selector
- imp 新的IMP
- types 型別編碼

此函式以兩種不同的方式執行:
1. 如果通過名稱標識的方法不存在,則會像呼叫class_addMethod一樣新增它。 由型別指定的型別編碼按給定使用。
2. 如果按名稱標識的方法存在,那麼將替換其IMP,就好像呼叫了method_setImplementation。 將忽略由types指定的型別編碼。

method_exchangeImplementations

method_exchangeImplementations
Exchanges the implementations of two methods.
Declaration
void method_exchangeImplementations(Method m1, Method m2);

交換兩個方法的實現.

原子版本的實現:

IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);

Selectors, Methods, & Implementations

在 Objective-C 的執行時中,selectors, methods, implementations 指代了不同概念,然而我們通常會說在訊息傳送過程中,這三個概念是可以相互轉換的。 下面是蘋果 Objective-C Runtime Reference中的描述:

  • Selector(typedef struct objc_selector *SEL):在執行時 Selectors 用來代表一個方法的名字。Selector 是一個在執行時被註冊(或對映)的C型別字串。Selector由編譯器產生並且在當類被載入進記憶體時由執行時自動進行名字和實現的對映。
  • Method(typedef struct objc_method *Method):方法是一個不透明的用來代表一個方法的定義的型別。
  • Implementation(typedef id (*IMP)(id, SEL,…)):這個資料型別指向一個方法的實現的最開始的地方。該方法為當前CPU架構使用標準的C方法呼叫來實現。該方法的第一個引數指向呼叫方法的自身(即記憶體中類的例項物件,若是呼叫類方法,該指標則是指向元類物件metaclass)。第二個引數是這個方法的名字selector,該方法的真正引數緊隨其後。

理解 selector, method, implementation 這三個概念之間關係的最好方式是:在執行時,類(Class)維護了一個訊息分發列表來解決訊息的正確傳送。每一個訊息列表的入口是一個方法(Method),這個方法映射了一對鍵值對,其中鍵是這個方法的名字 selector(SEL),值是指向這個方法實現的函式指標 implementation(IMP)。 Method swizzling 修改了類的訊息分發列表使得已經存在的 selector 映射了另一個實現 implementation,同時重新命名了原生方法的實現為一個新的 selector。

思考

很多人認為交換方法實現會帶來無法預料的結果。然而採取了以下預防措施後, method swizzling 會變得很可靠:

  • 在交換方法實現後記得要呼叫原生方法的實現(除非你非常確定可以不用呼叫原生方法的實現):APIs 提供了輸入輸出的規則,而在輸入輸出中間的方法實現就是一個看不見的黑盒。交換了方法實現並且一些回撥方法不會呼叫原生方法的實現這可能會造成底層實現的崩潰。
  • 避免衝突:為分類的方法加字首,一定要確保呼叫了原生方法的所有地方不會因為你交換了方法的實現而出現意想不到的結果。
  • 理解實現原理:只是簡單的拷貝貼上交換方法實現的程式碼而不去理解實現原理不僅會讓 App 很脆弱,並且浪費了學習 Objective-C 執行時的機會。閱讀 Objective-C Runtime Reference 並且瀏覽 能夠讓你更好理解實現原理。
  • 持續的預防:不管你對你理解 swlzzling 框架,UIKit 或者其他內嵌框架有多自信,一定要記住所有東西在下一個發行版本都可能變得不再好使。做好準備,在使用這個黑魔法中走得更遠,不要讓程式反而出現不可思議的行為。