Runtime系列(二)--Runtime的使用場景

Runtime 理解介紹的文章很是多,我只想講講Runtime 能夠用在哪裏,而我在項目裏哪些地方用到了runtime。多以實際使用過程爲主,來介紹runtime的使用。git

那麼runtime 怎麼使用?能夠用在哪些場景下呢?github

首先,使用runtime 相關API,要#import <objc/runtime.h>objective-c

1.運行時獲取某個類的屬性或函數

運行時動態獲取某個類的屬性或者函數等,能夠用來作不少事情,如json 解析、數據庫結果解析、判斷某個類的子類等。數據庫

1.1解析、轉化爲Model
// 獲取屬性列表
objc_property_t * class_copyPropertyList(Class cls, unsigned int *outCount) 
// 獲取屬性名
const char *property_getName(objc_property_t property)
// 獲取屬性類型
const char *property_getAttributes(objc_property_t property)
複製代碼

以上方法能夠用來:json

  • 解析json數據,轉化爲Model對象。
  • 解析數據庫查詢結果,轉化爲Model 對象。

這裏有動態獲取類的屬性的示例代碼片斷:數組

unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList([self class], &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        //獲取屬性名
        NSString *propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
        //獲取屬性類型等參數
        NSString *propertyType = [NSString stringWithCString: property_getAttributes(property) encoding:NSUTF8StringEncoding];
        /*
         各類符號對應類型,部分類型在新版SDK中有所變化,如long 和long long
         c char         C unsigned char
         i int          I unsigned int
         l long         L unsigned long
         s short        S unsigned short
         d double       D unsigned double
         f float        F unsigned float
         q long long    Q unsigned long long
         B BOOL
         @ 對象類型 //指針 對象類型 如NSString 是@「NSString」
         propertyType,你能夠打印出來,看看它是什麼。
         要判斷某個屬性的類型,只須要[propertyType hasPrefix:@"Ti"]
          這表明它是int 類型。
         */
    }
    free(properties);
複製代碼
1.2判斷某個類的子類

有時候咱們在程序中須要判斷某個類是不是另外一個類的子類。這個功能也能夠利用runtime類實現,這裏有示例代碼:bash

    int numClasses;
    Class *classes = NULL;
    numClasses = objc_getClassList(NULL,0);
    
    if (numClasses >0 )
    {
        classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
        numClasses = objc_getClassList(classes, numClasses);
        for (int i = 0; i < numClasses; i++) {
            if (class_getSuperclass(classes[i]) == [xxxxClass class]){
                id class = classes[i];
                // 執行某個方法 或者 作其餘事情
                [class performSelector:@selector(xxxxMethod) withObject:nil];
            }
        }
        free(classes);
    }
複製代碼

以上兩段示例代碼摘自我以前寫的FMDB Model 封裝:JKDBModel,你能夠去看更詳盡的解析和使用過程。服務器

1.3獲取某個類的實例變量

若是你還須要獲取某個類的實例變量作什麼操做的話,可使用以下這幾個API:app

// 獲取實例變量數組
Ivar * class_copyIvarList(Class cls, unsigned int *outCount)
// 獲取實例變量名稱
const char * ivar_getName( Ivar ivar)
// 獲取實例變量類型
const char * ivar_getTypeEncoding( Ivar ivar)
複製代碼

這面有獲取實例變量的示例代碼片斷:函數

    unsigned int outCount, i;
    
    Ivar *ivaries = class_copyIvarList([Son class], &outCount);
    for (i = 0; i < outCount; i++) {
        Ivar ivar = ivaries[i];
        NSString *ivarName = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
        NSString *ivarType = [NSString stringWithCString:ivar_getTypeEncoding(ivar) encoding:NSUTF8StringEncoding];
        NSLog(@"名稱:%@---類型:%@",ivarName,ivarType);
          /*
         各類符號對應類型,部分類型在新版SDK中有所變化,如long 和long long
         c char         C unsigned char
         i int          I unsigned int
         l long         L unsigned long
         s short        S unsigned short
         d double       D unsigned double
         f float        F unsigned float
         q long long    Q unsigned long long
         B BOOL
         @ 對象類型 //指針 對象類型 如NSString 是@「NSString」
         */
    }
    free(ivaries);

複製代碼
1.4獲取某個類的方法

獲取某個類的方法,會包含這個類的property 的set 和get 方法,可是不包括父類的property set 和get 方法,不包括父類的方法(若是在當前類覆寫,就包括)。

主要API:

// 獲取方法數組
Method * class_copyMethodList(Class cls, unsigned int *outCount)
// 獲取方法的 SEL
SEL method_getName( Method method)
// 獲取方法名
const char* sel_getName(SEL aSelector)
複製代碼

獲取方法數組的示例代碼片斷:

    unsigned int outMethodCount, j;
    Method *methods = class_copyMethodList([Son class], &outMethodCount);
    for (j = 0; j < outMethodCount; j++) {
        Method method = methods[j];
        SEL selector = method_getName(method);
        if (selector) {
            NSString *methodName = [NSString  stringWithCString:sel_getName(selector) encoding:NSUTF8StringEncoding];
            NSLog(@"方法:%@",methodName);
        }
    }
    free(methods);
複製代碼

2.運行時替換方法(Method Swizzling)

Method Swizzling 的使用須要謹慎,由於一不當心可能就會致使沒法排查的Bug,畢竟它替換的是官方的API,有些API內部作了什麼事情,很難徹底把握。

使用場景,須要監控用戶常常打開的界面,以及在某界面停留的時長。

咱們能夠怎麼作呢?

寫一個UIViewController 的Category,而後在類別中,添加自定義的方法:如-xxxviewDidAppear:和-xxxviewDidDisappear:方法,而後在-load 方法中,用自定義的方法替換原來的方法。

+ (void)load {
        static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];         
        // When swizzling a class method, use the following:
                    // Class class = object_getClass((id)self);

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

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

        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
        [self xxx_viewWillAppear:animated];
        NSLog(@"xxx_viewWillAppear: %@", self);
        // 在這裏,咱們能夠發送一個消息到服務器,或者作其餘事情等。
}
複製代碼

以上示例代碼摘自:Objective-C Runtime 運行時之四:Method Swizzling

關於Method Swizzling,他是把兩個方法的實現部分互換了。

好比上面咱們調用-xxx_viewWillAppear:由於-xxx_viewWillAppear:-viewWillAppear:的實現部分互換後,其實執行的時候,並不會執行上面的這個實現,而是調用-viewWillAppear:的內部實現。因此上面的代碼,徹底不會產生循環調用。

仍是寫段代碼說明吧:

- (void)viewWillAppear:(BOOL)animated {
    NSLog(@"這是原來的方法");
}

- (void)xxx_viewWillAppear:(BOOL)animated {
    NSLog(@"xxx_viewWillAppear: %@", self);
    // 在這裏,咱們能夠發送一個消息到服務器,或者作其餘事情等。
}
複製代碼

假如上面這倆方法用method swizzling 替換後,咱們調用-xxx_viewWillAppear:會打印這是原來的方法;而調用-viewWillAppear:會打印xxx_viewWillAppear:。這裏須要細細體會一下。

關於Method Swizzling更多的注意點請看原文Method Swizzling

3.對象關聯(Associated Objects)

對象關聯(或稱爲關聯引用)原本是Objective-C 運行時的一個重要特性,它能讓開發者對已經存在的類在擴展中添加自定義的屬性

須要用的如下三個函數:

void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)

id objc_getAssociatedObject(id object, void *key)

void objc_removeAssociatedObjects(id object)
複製代碼

衆所周知,OC 中的Category 中不能添加新的屬性,可是咱們經過Associated Objects能夠間接的實現往類上添加自定義的屬性。

不能添加屬性的根本緣由是不會幫咱們自動添加對象的實例變量,也不會幫咱們生成set 和get方法,雖然set /get 方法能夠本身實現,可是沒有實例變量來存儲數據。

很容易看懂官方文檔對參數的描述,可是key 須要注意一下: 一般推薦的作法是添加的屬性最好是 static char類型的,固然更推薦是指針型的。一般來講該屬性應該是常量、惟一的、在適用範圍內用getter和setter訪問到,因此一般咱們這樣寫:

static char kAssociatedObjectKey;

objc_setAssociatedObject(self, &kAssociatedObjectKey, object, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(self, &kAssociatedObjectKey);
複製代碼

固然,對於key 還有更好的作法,那就是selector。用selector 的示例在下面。

下面用代碼演示如何在Category中添加一個新的屬性。

這是Son+AssociatedObject.h

#import "Son.h"

@interface Son (AssociatedObject)

/** 家庭住址 */
@property (copy, nonatomic) NSString            *address;
/** 身高 */
@property (assign, nonatomic)   int             height;

@end
複製代碼

這是Son+AssociatedObject.m

#import "Son+AssociatedObject.h"
#import <objc/runtime.h>

@implementation Son (AssociatedObject)

- (void)setAddress:(NSString *)address
{
    objc_setAssociatedObject(self, @selector(address), address, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)address
{
    return objc_getAssociatedObject(self, @selector(address));
}

- (void)setHeight:(int)height
{
    NSNumber *heighNum = [NSNumber numberWithInt:height];
    objc_setAssociatedObject(self, @selector(height), heighNum, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (int)height
{
    NSNumber *heightNum = objc_getAssociatedObject(self, @selector(height));
    return heightNum.intValue;
}

@end
複製代碼

雖然上面有提到void objc_removeAssociatedObjects(id object),可是不要輕易使用這個函數,由於它會移除全部的關聯對象。咱們通常要移除某個關聯對象,只須要用objc_setAssociatedObject傳入nil便可。

補充一個關聯對象的使用場景:

你在使用AlertView 或者ActionSheet的時候,有沒有很苦惱不能在點擊的代理方法中方便的獲取到Model對象呢?

除了在控制器中添加一個property 這種方式外,咱們也能夠爲AlertView 或者ActionSheet 添加一個關聯對象,這樣就能夠在代理方法中方便的獲取到Model 對象啦。

這裏若是咱們爲AlertView 或者ActionSheet 添加Category來實現的話,代碼跟上面爲Son 添加類別基本同樣,對象類型改成id 類型便可。

或者咱們在控制器中調用的時候,添加關聯對象也能夠。這時候就用這種方式:

static char kAssociatedObjectKey;
objc_setAssociatedObject(self, &kAssociatedObjectKey, object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(self, &kAssociatedObjectKey);
複製代碼

UIAlertController 也跟上面同樣。

關於Associated Objects的使用,有兩個爲Category擴展功能,使得Category中也能方便的添加屬性以及相應的getter 和setter 的例子。

OC 自動生成分類屬性方法 一個庫--DProperty

4.運行時動態建立一個類

我在某控制器中測試寫了這麼一個方法,來建立一個MyClass 類。項目中並不存在叫MyClass 的類文件。

- (void)createClass
{
    Class MyClass = objc_allocateClassPair([NSObject class], "MyClass", 0);
    // 1.添加一個叫name 類型爲NSString的實例變量,第四個參數是對其方式,第五個參數是參數類型
    if (class_addIvar(MyClass, "name", sizeof(NSString *), 0, "@")) {
        NSLog(@"add ivar success");
    }
    // 2.添加一個property
    // 這裏須要注意,添加property以前須要先添加一個與之對應的實例變量
    if (class_addIvar(MyClass, "_address", sizeof(NSString *), 0, "@")) {
        NSLog(@"add ivar success");
    }
    
    objc_property_attribute_t type = {"T", "@\"NSString\""};
    objc_property_attribute_t ownership = { "C", "" };
    objc_property_attribute_t backingivar = { "V", "_address"};
    objc_property_attribute_t attrs[] = {type, ownership, backingivar};
    class_addProperty(MyClass, "address", attrs,2);
    
    // 3.添加函數, myclasstest是已經實現的函數,"v@:"這種寫法見參數類型鏈接
    class_addMethod(MyClass, @selector(myclasstest:), (IMP)myclasstest, "v@:");
    // 4.註冊這個類到runtime系統中就可使用他了
    objc_registerClassPair(MyClass);
    // 5.生成了一個實例化對象
    id myobj = [[MyClass alloc] init];
    NSString *str = @"名字";
    // 6.給剛剛添加的變量賦值
    //    object_setInstanceVariable(myobj, "itest", (void *)&str);在ARC下不容許使用
    [myobj setValue:str forKey:@"name"];
    [myobj setValue:@"這是地址" forKey:@"address"];
    // 7.調用myclasstest方法,也就是給myobj這個接受者發送myclasstest這個消息
    [myobj myclasstest:10];
}

//這個方法實際上沒有被調用,可是必須實現不然不會調用下面的方法
- (void)myclasstest:(int)a
{
    NSLog(@"啊啊啊啊啊");
}
//調用的是這個方法
static void myclasstest(id self, SEL _cmd, int a) //self和_cmd是必須的,在以後能夠添加其餘參數
{
    Ivar v = class_getInstanceVariable([self class], "name");
    //返回名爲name的ivar的變量的值
    id o = object_getIvar(self, v);
    //成功打印出結果
    NSLog(@"name is %@", o);
    NSLog(@"參數 a is %d", a);
    
    objc_property_t property = class_getProperty([self class], "address");
    NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
    id value = [self valueForKey:propertyName];
    NSLog(@"address is %@", value);
}
複製代碼

關於運行時建立一個新類,上面的註釋已經寫的很詳細了。

Have Fun!

相關文章
相關標籤/搜索