使用runtime給類動態添加方法並調用 - class_addMethod

来源:http://www.cnblogs.com/chipmuck/archive/2016/08/26/5807190.html
-Advertisement-
Play Games

上手開發 iOS 一段時間後,我發現並不能只著眼於完成需求,利用閑暇之餘多研究其他的開發技巧,才能在有限時間內提升自己水平。當然,“其他開發技巧”這個命題對於任何一個開發領域都感覺不找邊際,而對於我來說,嘗試接觸 objc/runtime 不失為是開始深入探索 iOS 開發的第一步。 剛瞭解 run ...


上手開發 iOS 一段時間後,我發現並不能只著眼於完成需求,利用閑暇之餘多研究其他的開發技巧,才能在有限時間內提升自己水平。當然,“其他開發技巧”這個命題對於任何一個開發領域都感覺不找邊際,而對於我來說,嘗試接觸 objc/runtime 不失為是開始深入探索 iOS 開發的第一步。

剛瞭解 runtime 當然要從比較簡單的 api 開始,今天就羅列整理一下 class_addMethod 的相關點:

首先從文檔開始。

/** 
 * Adds a new method to a class with a given name and implementation.
 * 
 * @param cls The class to which to add a method.
 * @param name A selector that specifies the name of the method being added.
 * @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
 * @param types An array of characters that describe the types of the arguments to the method. 
 * 
 * @return YES if the method was added successfully, otherwise NO 
 *  (for example, the class already contains a method implementation with that name).
 *
 * @note class_addMethod will add an override of a superclass's implementation, 
 *  but will not replace an existing implementation in this class. 
 *  To change an existing implementation, use method_setImplementation.
 */
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                                 const char *types) 
     __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);

大意翻譯一下,這個方法的作用是,給類添加一個新的方法和該方法的具體實現。分析一下這個方法需要的參數:

Class cls

cls 參數表示需要添加新方法的類。

SEL name

name 參數表示 selector 的方法名稱,可以根據喜好自己進行命名。

IMP imp

imp 即 implementation ,表示由編譯器生成的、指向實現方法的指針。也就是說,這個指針指向的方法就是我們要添加的方法。

const char *types

最後一個參數 *types 表示我們要添加的方法的返回值和參數。

 

簡要介紹了 class_addMethod 中所需要的參數以及作用之後,我們就可以開始利用這個方法進行添加我們所需要的方法啦!在使用之前,我們首先要明確 Objective-C 作為一種動態語言,它會將部分代碼放置在運行時的過程中執行,而不是編譯時,所以在執行代碼時,不僅僅需要的是編譯器,也同時需要一個運行時環境(Runtime),為了滿足一些需求,蘋果開源了 Runtime Source 並提供了開放的 api 供開發者使用。

其次,我們需要知道在什麼情況下需要調用 class_addMethod 這個方法。當項目中,需要繼承某一個類(subclass),但是父類中並沒有提供我需要的調用方法,而我又不清楚父類中某些方法的具體實現;或者,我需要為這個類寫一個分類(category),在這個分類中,我可能需要替換/新增某個方法(註意:不推薦在分類中重寫方法,而且也無法通過 super 來獲取所謂父類的方法)。大致在這兩種情況下,我們可以通過 class_addMethod 來實現我們想要的效果。 

好了,說了這麼多那麼到底應該如何調用呢?如果不清楚使用方法,那麼看說明書就是最好的方法。在 Apple 提供的文檔中就有詳細的使用方法(Objective-C Runtime Programming Guide - Dynamic Method Resolution),以下內容就以 myCar 這個類來詳細說明一下具體的使用規則:

首先,既然要給某個類添加我們的方法,就應該繼承或者給這個類寫一個分類,這裡我新建一個名為「myCar」的類,作為「Car」類的分類。

#import "Car+myCar.h"

@implementation Car (myCar)

@end

我們知道,在 Objective-C 中,正常的調用方法是通過消息機制(message)來實現的,那麼如果類中沒有找到發送的消息方法,系統就會進入找不到該方法的處理流程中,如果在這個流程中,我們加入我們所需要的新方法,就能實現運行過程中的動態添加了。這個流程或者說機制,就是 Objective-C 的 Message Forwarding 

這個機制中所涉及的方法主要有兩個:

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

 兩個方法的唯一區別在於需要添加的是靜態方法還是實例方法。我們就拿前者來說,既然要添加方法,我們就在「myCar」類中實現它,代碼如下:

#import "Car+myCar.h"

void startEngine(id self, SEL _cmd) {
    NSLog(@"my car starts the engine");
}

@implementation Car (myCar)

@end

至此,我們實現了我們要添加的 startEngine 這個方法。這是一個 C 語言的函數,它至少包含了 self 和 _cmd 兩個參數(self 代表著函數本身,而 _cmd 則是一個 SEL 數據體,包含了具體的方法地址)。如果要在這個方法中新增參數呢?見如下代碼:

#import "Car+myCar.h"

void startEngine(id self, SEL _cmd, NSString *brand) {
    NSLog(@"my %@ car starts the engine", brand);
}

@implementation Car (myCar)

@end

只要在那兩個必須的參數之後添加所需要的參數和類型就可以了,返回值同樣道理,只要把方法名之前的 void 修改成我們想要的返回類型就可以,這裡我們不需要返回值。

接著,我們重載 resolveInstanceMethod: 這個函數:

#import "Car+myCar.h"
#import <objc/runtime.h>

void startEngine(id self, SEL _cmd, NSString *brand) {
    NSLog(@"my %@ car starts the engine", brand);
}

@implementation Car (myCar)

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(drive)) {
        class_addMethod([self class], sel, (IMP)startEngine, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

@end

 解釋一下,這個函數在 runtime 環境下,如果沒有找到該方法的實現的話就會執行。第一行判斷的是傳入的 SEL 名稱是否匹配,接著調用 class_addMethod 方法,傳入相應的參數。其中第三個參數傳入的是我們添加的 C 語言函數的實現,也就是說,第三個參數的名稱要和添加的具體函數名稱一致。第四個參數指的是函數的返回值以及參數內容。

至於該類方法的返回值,在我測試的時候,無論這個 BOOL 值是多少,並不會影響我們的執行目標,一般返回 YES 即可。

如果覺得用 C 語言風格寫新函數比較不適應,那麼可以改寫成以下的代碼:

@implementation Car (myCar)

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(drive)) {
        class_addMethod([self class], sel, class_getMethodImplementation(self, @selector(startEngine:)), "s@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

- (void)startEngine:(NSString *)brand {
    NSLog(@"my %@ car starts the engine", brand);
}

@end

其中 class_getMethodImplementation 意思就是獲取 SEL 的具體實現的指針。

然後創建一個新的類「DynamicSelector」,在這個新類中我們實現對「Car」的動態添加方法。

#import "DynamicSelector.h"
#import "Car+myCar.h"

@implementation DynamicSelector

- (void)dynamicAddMethod {
    Car *c = [[Car alloc] init];
    [c performSelector:@selector(drive) withObject:@"bmw"];
}

@end

註意,在這裡就不能使用 [self method:] 進行調用了,因為我們添加的方法是在運行時才執行,而編譯器只負責編譯時的方法檢索,一旦對一個對象沒有檢索到它的 drive 方法,就會報錯,所以這裡我們使用 performSelector:withObject: 來進行調用,保存,運行。

2016-08-26 10:50:17.207 objc-runtime[76618:3031897] my bmw car starts the engine
Program ended with exit code: 0

列印結果符合我們期望實現的目標。如果需要返回值,方法類似。

 

項目已上傳至 https://github.com/zhangqifan/class_addMethod 有需要的可以 clone 源碼,如需指正請 push issue。

 

本人原創,轉載請註明鏈接。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1、重覆添加某個文件。解決辦法:搜索工程,刪除多餘的文件; 2、文件添加引用錯誤,即尾碼 .m 誤寫為 .h 。解決辦法:改正,編譯通過。 ...
  • Handoff簡介 Handoff是iOS 8 和 OS X v10.10中引入的功能,可以讓同一個用戶在多台設備間傳遞項目。In iOS 9 and OS X v10.11 支持了Spotlight中搜索並打開應用。 Handoff交互: 在iOS中這個user activity object是U ...
  • 蘋果提供的NSURLSessionDownloadTask雖然能實現斷點續傳,但是有些情況是無法處理的,比如程式強制退出或沒有調用 cancelByProducingResumeData取消方法,這時就無法斷點續傳了。 使用NSURLSession和NSURLSessionDataTask實現斷點續 ...
  • 隨著項目中動態鏈接庫越來越多,我們也遇到了很多奇怪的問題,比如只在某一種 OS 上會出現的 `java.lang.UnsatisfiedLinkError`,但是明明我們動態庫名稱沒錯,ABI 也沒錯,方法也能對應的上,而且還只出現在某一些機型上,搞的我們百思不得其解。為了找到出現千奇百怪問題的原因... ...
  • 最近一段時間,在網上不斷看了一些技術人員寫的代碼demo,由於前段時間一直在寫一個電商項目,記得有一個功能和看到的demo中類似,但是截然2種不同的處理方法,個人覺得我的這個方法更為簡潔一些,所以我把代碼中的這個簡單的小功能跟大家分享出來,希望對大家有幫助。 功能:實現類似於支付寶的提現密碼輸入功能 ...
  • 首先是OC調用C++的代碼。 創建一個Objective-C的項目,並創建c++文件MyCppFile.hpp和MyCppFile.cpp。 把要調用Cpp代碼的文件名改成mm尾碼名,項目代碼的結構如下: 實現C++部分的代碼: MyCppFile.hpp MyCppFile.cpp 在main.m ...
  • 1 . 音視頻處理的一般流程: 數據採集→數據編碼→數據傳輸(流媒體伺服器) →解碼數據→播放顯示1、數據採集:攝像機及拾音器收集視頻及音頻數據,此時得到的為原始數據涉及技術或協議:攝像機:CCD、CMOS拾音器:聲電轉換裝置(咪頭)、音頻放大電路2、數據編碼:使用相關硬體或軟體對音視頻原始數據進行 ...
  • ListView的模板寫法 ListView模板寫法的完整代碼: "android代碼優化 ListView中自定義adapter的封裝(ListView的模板寫法)" 以後每寫一個ListView,就這麼做:直接 導入ViewHolder.java 和 ListViewAdapter ,然後寫一個 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...