Objective-C的Runtime機制的應用示例總結

Objective-C是一門動態語言,不同於許多靜態語言,例如C語言,只能在編譯和鏈接階段把程序運行的上下文做好,在運行期間,無法修改,缺少動態性。Objective-C的動態性,給開發者提供了一種在運行期,修改程序執行流程的機會,這要歸功於其強大的Runtime機制。
這篇文章主要介紹,目前,Runtime機制在我們項目中的應用場景。

  • 前言

ObjC語言中,runtime運行機制主要依賴於兩個頭文件

#include <objc/runtime.h>
#include <objc/message.h>

其中runtime.h聲明瞭一些類,實例變量操作相關的東西,message.h聲明瞭一些消息發送相關的東西。
下面列舉一下,runtime.h中常用的方法,以及它們的用法。

object_getClass:    獲取一個對象所屬的類
object_getIvar:     讀取一個對象中某個變量的值
objc_getClass:      通過字符串獲取到類Class
class_getSuperclass:獲取一個類的父類
class_getInstanceMethod:獲取實例方法,返回Method
class_getMethodImplementation:獲取實例方法的實現
class_getProperty:      獲取某個類的屬性
class_addMethod:        增加方法。
...等等

上面大部分列舉了get相關的方法。runtime.h提供了獲取(get),設置(set)(類的屬性,實例變量,實例方法,類方法)的操作。

下面列舉一下message.h的相關方法。

objc_msgSend:       給對象發送消息。
objc_msgSendSuper:  向父類的發送消息

message.h類,主要提供了這兩個關鍵的方法。

  • 下面總結一下Runtime有哪些使用場景,並通過具體的代碼,來說明各種使用場景如何運用Runtime。

總結了如下四種應用場景:

 1.動態拼接URL
 2.交換兩個方法的實現(用自己的方法,替換系統的方法)
 3.給系統已有的方法添加新的功能(不影響系統方法原來的功能)
 4.消息轉發中Runtime的應用

下面結合項目中使用到Runtime的地方進行說明,並列舉了一些代碼示例。

  • 第一個使用場景:動態拼接URL

    在做app開發的時候,肯定會遇到http請求,URL拼接的問題。初期大部分採取的方案是,採用NSString的格式化方法,自己手動去拼接URL. 例如:

    NSMutableString *stringUrl = [NSMutableString stringWithFormat:@"http:mytest/host/code=%@",code];
    [stringUrl appendString:@"&uin=45"];
    [stringUrl appendString:@"&my=5"];
    [stringUrl appendString:@"&your=6"];

這樣寫有幾個問題:
1. 手動書寫,很容易寫錯。
2. 如果很多類似的請求,都需要uin或者其他通用的參數,那麼就會每個請求都需要寫一遍。不滿足OOP的特性。
3. 參數所代表的含義不夠清晰,需要開發去猜測。
解決方法:
把url拼接,抽象成一個類。
上面的參數code, uin,my,your可以當成類的屬性。通用的屬性有uin,可以抽象出一個基類。實現方法如下:
CURLParamBase類

@interface CURLParamBase : NSObject

@property (nonatomic, strong)       NSString        *uin;

@end

需要構造的請求類XXX,如下定義:

@interface CURLParamXXX : CURLParamBase

@property (nonatomic, strong)       NSString*   my;
@property (nonatomic, strong)       NSString*   your;

@end

使用:定義CURLParamXXX的類的實例,使用Runtime來獲取屬性名和屬性的value來拼接參數,如下:

     unsigned int propertyCount;
     //獲取所有屬性
     objc_property_t *properties =
        class_copyPropertyList(class, &propertyCount);
     for (unsigned int i = 0; i < propertyCount; i++)
     {
          objc_property_t property = properties[i];
          const char *propertyName = property_getName(property);
//獲取實例變量,某個屬性的值       
object_getInstanceVariable(self,propertyName,&value);
     }

-第二個使用場景:交換兩個方法的實現

使用newSEL方法名,新的newIMP實現,替換原有的origSEL方法及其實現。
實現代碼如下,代碼中添加了相關的註釋。

/**
 **新方法的實現替換舊方法
 **成功:返回YES,
 **失敗:返回NO.
 */
BOOL replaceMethodNewImpl(Class c, SEL origSEL, SEL newSEL, IMP newIMP)
{
    //新方法已經存在
    if ([c instancesRespondToSelector:newSEL])
    {
        return YES;
    }

    //舊方法
    Method origMethod = class_getInstanceMethod(c, origSEL);

    //先把新方法加入到class的方法列表中
    if (!class_addMethod(c, newSEL, newIMP, method_getTypeEncoding(origMethod)))
    {
        //如果加入失敗,直接返回
        NSLog(@"Failed to add method:%@ on %@",NSStringFromSelector(newSEL),c);
        return NO;
    }
    else
    {
        //如果加入成功
        Method newMethod = class_getInstanceMethod(c, newSEL);
        //給original添加新的實現
        //有可能失敗失敗原因:(for example, the class already contains a method implementation with that name).
        if (class_addMethod(c, origSEL, method_getImplementation(newMethod), method_getTypeEncoding(origMethod)))
        {
            //新方法的實現替換成舊方法的實現。
            class_replaceMethod(c, newSEL, method_getImplementation(origMethod), method_getTypeEncoding(newMethod));
        }
        else
        {
            //交換實現
            method_exchangeImplementations(origMethod, newMethod);
        }
    }

    return YES;
}
  • 第三個使用場景:給系統已有的方法添加功能
    例如給系統的UIView下的方法drawRect,添加NSLog的功能,這個可以參考上面說明的第三種使用場景。變化的只是,新方法的實現要去調用老的方法的實現。在override_drawRect方法中,調用drawRect方法。代碼如下:
 - (void)override_drawRect:(CGRect)r
{
    // 調用舊的實現。因爲它們已經被替換了
    [self override_drawRect: r];

    NSLog(@"rect = %@",NSStringFromCGRect(r));
}
  • 第四使用場景:消息轉發
    開發過程中,經常遇到unrecognized selector sent to instance 0x87 Terminating app due to uncaught exception
    NSInvalidArgumentException’, 這個問題。
    這是什麼原因?直觀上看,是系統沒有處理某個消息。
    情況分兩種,第一,接受消息的對象錯了。第二,對象沒錯,發送的消息不對。
    消息轉發的整個流程如下圖所示:
    消息轉發過程
    總體來說,就是給某個實例,發送某個消息。
    首先如果沒找到響應方法,系統會給你轉到其他方法的機會,只要實現了resolveInstanceMethod方法即可。
    其次,如果沒有實現,你還可以修改接受消息的對象,讓其他對象去響應消息,覆蓋方法forwardingTargetSelector即可。
    如果這兩者都沒做,你還可以在forwardInvocation,做自己的邏輯處理,是否繼續處理消息。
    這篇文章不具體討論這個轉發流程的細節,只是爲了說明runtime在整個過程中的運用。
    Runtime在其中的運用。
    在resolveInstanceMethod方法中,可以通過覆蓋resolveInstanceMethod方法,如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString hasPrefix:@"set"]) {
        class_addMethod(self, sel, (IMP)mySetMethod, "v@:@");
    } else {
        class_addMethod(self, sel, (IMP)myGetMethod, "@@:");
    }
    return YES;
}

代碼中,本類實現了動態的添加選擇子,把選擇子關聯到自己定義的方法上,這樣在訪問某個屬性的時候,會動態的訪問我們自己定義的方法。在消息轉發的流程中,實現了動態添加方法實現的能力。

綜上所述,Runtime提供了Objective-C強大的動態性,可謂是方便靈活,運用起來能做很多事情,也歡迎大家補充說明在你們的項目中,Runtime都爲你們做了什麼。

(完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章