欲誠其意者,先致其知;致知在格物。物格然後知至,知至然後意誠。現代漢語詞典中將格物致知解釋爲: "推究事物的原理,從而得到知識"。html
在編程中咱們接觸最多的也是最基本的就是類和對象,當咱們在建立類或者實例化對象時,是否考慮過類和對象究竟是什麼?理解其本質才能真正掌握一門語言。本文將從結構類型角度並結合實際應用探討下Objective-C的類和對象。git
在Objective-C中,對象是廣義的概念,類也是對象,因此嚴謹的說法應該是類對象和實例對象。既然實例對象所屬的類稱爲類對象,那類對象有所屬的類嗎?有,稱之爲元類(Metaclass)。程序員
類對象(Class)是由程序員定義並在運行時由編譯器建立的,它沒有本身的實例變量,這裏須要注意的是類的成員變量和實例方法列表是屬於實例對象的,但其存儲於類對象當中的。咱們在/usr/include/objc/objc.h
下看看Class
的定義:github
/// An opaque type that represents an Objective-C class. typedef struct objc_class *Class; 複製代碼
能夠看到類是由Class
類型來表示的,它是一個objc_class
結構類型的指針。咱們接着來看objc_class
結構體的定義:面試
struct objc_class { Class isa; // 指向所屬類的指針(_Nonnull) Class super_class; // 父類 const char *name; // 類名(_Nonnull) long version; // 類的版本信息(默認爲0) long info; // 類信息(供運行期使用的一些位標識) long instance_size; // 該類的實例變量大小 struct objc_ivar_list *ivars; // 該類的成員變量鏈表 struct objc_method_list * *methodLists; // 方法定義的鏈表 struct objc_cache *cache; // 方法緩存 struct objc_protocol_list *protocols; // 協議鏈表 }; 複製代碼
isa指針是和Class
同類型的objc_class
結構指針,類對象的指針指向其所屬的類,即元類。元類中存儲着類對象的類方法,當訪問某個類的類方法時會經過該isa指針從元類中尋找方法對應的函數指針objective-c
super_class爲該類所繼承的父類對象,若是該類已是最頂層的根類(如NSObject
或NSProxy
), 則 super_class爲NULL
編程
ivars是一個指向objc_ivar_list
類型的指針,用來存儲每個實例變量的地址緩存
info爲運行期使用的一些位標識,好比: CLS_CLASS (0x1L)
表示該類爲普通類, CLS_META (0x2L)
則表示該類爲元類bash
methodLists用來存放方法列表,根據info中的標識信息,當該類爲普通類時,存儲的方法爲實例方法;若是是元類則存儲的類方法markdown
cache用於緩存最近使用的方法。系統在調用方法時會先去cache中查找,在沒有查找到時纔會去methodLists中遍歷獲取須要的方法
實例對象是咱們對類對象alloc
或者new
操做時所建立的,在這個過程當中會拷貝實例所屬的類的成員變量,但並不拷貝類定義的方法。調用實例方法時,系統會根據實例的isa指針去類的方法列表及父類的方法列表中尋找與消息對應的selector指向的方法。一樣的,咱們也來看下其定義:
/// Represents an instance of a class. struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; }; 複製代碼
能夠看到,這個結構體只有一個isa變量,指向實例對象所屬的類。任何帶有以指針開始並指向類結構的結構均可以被視做objc_object
, 對象最重要的特色是能夠給其發送消息. NSObject類的alloc
和allocWithZone:
方法使用函數class_createInstance
來建立objc_object
數據結構。
另外咱們常見的id類型,它是一個objc_object
結構類型的指針。該類型的對象能夠轉換爲任何一種對象,相似於C語言中void *
指針類型的做用。其定義以下所示:
/// A pointer to an instance of a class. typedef struct objc_object *id; 複製代碼
元類(Metaclass)就是類對象的類,每一個類都有本身的元類,也就是objc_class
結構體裏面isa指針所指向的類. Objective-C的類方法是使用元類的根本緣由,由於其中存儲着對應的類對象調用的方法即類方法。
因此由上圖能夠看到,在給實例對象或類對象發送消息時,尋找方法列表的規則爲:
元類,就像以前的類同樣,它也是一個對象,也能夠調用它的方法。因此這就意味着它必須也有一個類。全部的元類都使用根元類做爲他們的類。好比全部NSObject的子類的元類都會以NSObject的元類做爲他們的類。
根據這個規則,全部的元類使用根元類做爲他們的類,根元類的元類則就是它本身。也就是說基類的元類的isa指針指向他本身。
咱們能夠經過代碼來實際驗證下, Runtime提供了object_getClass
函數:
Class _Nullable object_getClass(id _Nullable obj) 複製代碼
來獲取對象所屬的類,看到這個函數你也許會好奇這個和咱們日常接觸的NSObject的[obj class]
有什麼區別?
// NSObject.h - (Class)class; + (Class)class; 複製代碼
咱們繼續從runtime的源碼裏面尋找答案:
Class object_getClass(id obj) { return _object_getClass(obj); } 複製代碼
object_getClass
實際調用的是_object_getClass
函數,咱們接着看其實現:
static inline Class _object_getClass(id obj) { #if SUPPORT_TAGGED_POINTERS if (OBJ_IS_TAGGED_PTR(obj)){ uint8_t slotNumber = ((uint8_t)(uint64_t) obj) & 0x0F; Class isa = _objc_tagged_isa_table[slotNumber]; return isa; } #endif if (obj) return obj->isa; else return Nil; } 複製代碼
顯然_object_getClass
函數就是返回對象的isa指針,也就是返回該對象所指向的所屬類。咱們接着看[obj class]
的具體實現(包括類方法和實例方法兩種):
+ (Class)class { return self; // 返回自身指針 } - (Class)class { return object_getClass(self); // 調用'object_getClass'返回isa指針 } 複製代碼
從代碼中能夠看出+ (Class)class
返回的是其自己,而- (Class)class
則等價於object_getClass
函數。
咱們來寫個測試代碼,看看這些函數的實際返回值是否和上面的所述保持一致,好比咱們有個RJObject繼承自NSObject:
RJObject *obj = [RJObject new]; Class clsClass0 = [RJObject class]; // 返回RJObject類對象的自己的地址 Class objClass0 = [obj class]; // isa指向的RJObject類對象的地址 Class ogcClass0 = object_getClass(obj); // isa指向的RJObject類對象的地址 NSLog(@"clsClass0 -> %p", clsClass0); // -> 0x10fb22068 NSLog(@"objClass0 -> %p", objClass0); // -> 0x10fb22068 NSLog(@"ogcClass0 -> %p", ogcClass0); // -> 0x10fb22068 複製代碼
打印結果能夠看出,當obj爲實例變量時, object_getClass(obj)
與[obj class]
輸出結果一致,均返回該對象的isa指針,即指向RJObject類對象的指針。而[RJObject class]
則直接返回RJObject類對象自己的地址,因此與前面二者返回的地址相同。
// 'objClass0'爲RJObject類對象(RJObject Class) Class objClass1 = [objClass0 class]; // 返回RJObject類對象自己的地址 Class ogcClass1 = object_getClass(objClass0); // isa指向的RJObject元類的地址 NSLog(@"objClass1 -> %p", objClass1); // -> 0x10fb22068 NSLog(@"ogcClass1 -> %p", ogcClass1); // -> 0x10fb22040 複製代碼
此時objClass0
爲RJObject的類對象,因此類方法[objClass0 class]
返回的objClass1
爲self
, 即RJObject類對象自己的地址,故結果與上面的地址相同。而ogcClass1
返回的爲RJObject元類的地址。
// 'ogcClass1'爲RJObject的元類(RJObject metaClass) Class objClass2 = [ogcClass1 class]; // 返回RJObject元類對象的自己的地址 Class ogcClass2 = object_getClass(ogcClass1); // isa指向的RJObject元類的元類地址 NSLog(@"objClass2 -> %p", objClass2); // -> 0x10fb22040 NSLog(@"ogcClass2 -> %p", ogcClass2); // -> 0x110ad9e58 複製代碼
同理,這邊ogcClass2
爲RJObject元類的元類的地址,那問題來了,某個類它的元類的元類的是什麼類呢?這樣下去豈不是元類無窮盡了?擒賊先擒王,咱們先來看看根類NSObject的元類和它元類的元類分別是什麼:
Class rootMetaCls0 = object_getClass([NSObject class]); // 返回NSObject元類(根元類)的地址 Class rootMetaCls1 = object_getClass(rootMetaCls0); // 返回NSObject元類(根元類)的元類地址 NSLog(@"rootMetaCls0 -> %p", rootMetaCls0); // -> 0x110ad9e58 NSLog(@"rootMetaCls1 -> %p", rootMetaCls1); // -> 0x110ad9e58 複製代碼
看到結果就一目瞭然了,根元類的isa指針指向本身,也就是根元類的元類即其自己。另外,能夠發現ogcClass2
的地址和根元類isa的地址相同,說明任意元類的isa指針都指向根元類,這樣就構成一個封閉的循環。
另外,咱們能夠經過class_isMetaClass
函數來判斷某個類是不是元類,好比:
NSLog(@"ogcClass0 is metaClass: %@", class_isMetaClass(objClass0) ? @"YES" : @"NO"); NSLog(@"ogcClass1 is metaClass: %@", class_isMetaClass(ogcClass1) ? @"YES" : @"NO"); 複製代碼
輸出結果爲:
LearningClass[58516:3424874] ogcClass0 is metaClass: NO LearningClass[58516:3424874] ogcClass1 is metaClass: YES 複製代碼
日誌代表ogcClass0
爲類對象,而ogcClass1
則爲元類對象,這與咱們上面的分析是一致的。
類和元類的父類指向狀況也能夠參照上面的步驟,經過
class_getSuperclass
或者[obj superClass]
函數來獲取分析,這邊就再也不贅述了。
除了isa聲明瞭實例與所屬類的關係,還有superClass代表了類和元類的繼承關係,類對象和元類對象都有父類。一樣,爲了造成一個閉環,根類的父類爲nil
, 根元類的父類則指向其根類。咱們能夠經過一張示意圖來看下三種對象之間的鏈接關係:
總結一下實例對象,類對象以及元類對象之間的isa指向和繼承關係的規則爲:
規則一: 實例對象的isa指向該類,類的isa指向元類(metaClass)
規則二: 類的superClass指向其父類,若是該類爲根類則值爲nil
規則三: 元類的isa指向根元類,若是該元類是根元類則指向自身
規則四: 元類的superClass指向父元類,若根元類則指向該根類
Objective-C做爲動態語言的優點在於它能在運行時建立類和對象,並向類中增長方法和實例變量。具體示例以下:
Class newClass = objc_allocateClassPair([NSObject class], "RJInfo", 0); if (!class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:")) { NSLog(@"Add method 'report' failed!"); } if (!class_addIvar(newClass, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *))) { NSLog(@"Add ivar '_name' failed!"); } objc_registerClassPair(newClass); 複製代碼
上面代碼建立了一個RJInfo
的類,並分別添加了_name
成員變量和report
實例方法。須要注意的是,方法和變量必須在objc_allocateClassPair
和objc_registerClassPair
之間進行添加。因此,在運行時建立一個類只須要3個步驟:
首先是調用objc_allocateClassPair
爲新建的類分配內存,三個參數依次爲newClass的父類,newClass的名稱,第三個參數一般爲0, 從這個函數名字能夠看到新建的類是一個pair, 也就是成對的類,那爲何新建一個類會出現一對類呢?是的,元類!類和元類是成對出現的,每一個類都有本身所屬的元類,因此新建一個類須要同時建立類以及它的元類。
而後就能夠向newClass中添加變量及方法了,注意若要添加類方法,需用objc_getClass(newClass)
獲取元類,而後向元類中添加類方法。由於示例方法是存儲在類中的,而類方法則是存儲在元類中。最後必須把newClass註冊到運行時系統,不然系統是不能識別這個類的。
上面的代碼中添加了一個成員變量_name
, 咱們來看下實際應用中如何獲取和使用這個變量:
unsigned int varCount; Ivar *varList = class_copyIvarList(newClass, &varCount); for (int i = 0; i < varCount; i++) { NSLog(@"var name: %s", ivar_getName(varList[i])); } free(varList); id infoInstance = [[newClass alloc] init]; Ivar nameIvar = class_getInstanceVariable(newClass, "_name"); object_setIvar(infoInstance, nameIvar, @"Ryan Jin"); NSLog(@"var value: %@",object_getIvar(infoInstance, nameIvar)); 複製代碼
咱們能夠經過class_copyIvarList
來查看實例變量列表,注意獲取的varList
列表須要調用free()
函數釋放。當前只添加了一個變量,因此varCount
爲1
, 在調用ivar_getName
打印出變量的名字。如若對_name
賦值,則須要先實例化newClass對象,並取出對象的該變量後調用object_setIvar
進行賦值操做。示例代碼的輸出結果爲:
LearningClass[58516:3424874] var name: _name LearningClass[58516:3424874] var value: Ryan Jin 複製代碼
好了,驗證完變量的添加,繼續看方法的添加和使用。上文的示例中添加了report
方法,但僅僅是作了SEL
方法名的聲明,咱們來接着完成其IMP
所指向函數ReportFunction
的具體實現:
void ReportFunction(id self, SEL _cmd) { Class currentClass = [self class]; Class metaClass = objc_getMetaClass(class_getName(currentClass)); NSLog(@"Class is %@, and super - %@.", currentClass, [self superclass]); NSLog(@"%@'s meta class is %p.", NSStringFromClass(currentClass), metaClass); } 複製代碼
在函數實現中咱們打印了類,父類以及元類的相關信息,爲了運行ReportFunction
, 咱們須要建立一個動態實例來建立類的實例對象並調用report
方法:
id instanceOfNewClass = [[newClass alloc] init]; [instanceOfNewClass performSelector:@selector(report)]; 複製代碼
輸出結果:
LearningClass[58516:3424874] Class is RJInfo, and super - NSObject.
LearningClass[58516:3424874] RJInfo's meta class is 0x600000253920.
複製代碼
除了給類添加方法,咱們一樣也能夠動態修改已存在方法的實現,好比:
class_replaceMethod(newClass, @selector(report), (IMP)ReportReplacedFunction, "v@:"); 複製代碼
這樣就將report
這個SEL
所指向的IMP
實現換成了ReportReplacedFunction
. 若是類中不存在name
指定的方法, class_replaceMethod
則相似於class_addMethod函數同樣會添加方法;若是類中已存在name
指定的方法,則相似於method_setImplementation
同樣替代原方法的實現。
看到
class_replaceMethod
的解釋,相信你已經發現了,這不就是Method Swizzling嗎?沒錯,所謂的黑魔法,其實就是底層原理的應用而已!
知其然亦知其因此然纔是獲取知識的正確方式,理解了類和對象的本質後,咱們來看看格物致知後的理論能夠引導出哪些應用和認識:
在Objective-C中,屬性(property)和成員變量是不一樣的。那麼,屬性的本質是什麼?它和成員變量之間有什麼區別?簡單來講屬性是添加了存取方法的成員變量,也就是:
@property = ivar + getter + setter; 複製代碼
所以,咱們每定義一個@property
都會添加對應的ivar
, getter
和setter
到類結構體objc_class
中。具體來講,系統會在objc_ivar_list
中添加一個成員變量的描述,而後在methodLists
中分別添加setter
和getter
方法的描述。
如上文所述,方法調用是經過查詢對象的isa指針所指向歸屬類中的methodLists
來完成。這裏咱們經過孫源在runtime分享會上的一道題目來理解下。假設咱們有一個類RJSark
定義以下:
@interface RJSark : NSObject - (void)speak; @end 複製代碼
而後經過以下方式調用speak
方法:
@implementation RJViewController - (void)viewDidLoad { [super viewDidLoad]; id cls = [RJSark class]; void *obj = &cls; [(__bridge id)obj speak]; } @end 複製代碼
這裏會正常完成調用,並不會致使程序crash. 這又是爲何呢?咱們先來看下cls
. 顯然,它是RJSark
的類對象,通過void *obj = &cls
賦值後obj
爲指向cls
的指針,再經過(__bridge id)
將其轉換爲id
對象。上文中咱們提到id
實際上是一個objc_object
結構體,裏面存放了指向所屬類的isa指針,因此調用[obj speak]
可以找到它的isa所指向的類對象(也就是RJSark
類)的方法列表並完成調用,但其實obj
並非RJSark
的實例對象,它僅僅擁有和RJSark
實例對象同樣的isa指針而已。
空說無憑,咱們將上面的代碼稍微修改後驗證下:
id cls = [RJSark class]; RJSark *sark = [[cls alloc] init]; void *obj = &cls; NSLog(@"cls = %p", cls); NSLog(@"sark = %p", objc_getClass(object_getClassName(sark))); NSLog(@"obj = %p", objc_getClass(object_getClassName((__bridge id)obj))); 複製代碼
輸出結果爲:
LearningClass[58516:3424874] cls = 0x10fbd02d0 LearningClass[58516:3424874] sark = 0x10fbd02d0 LearningClass[58516:3424874] obj = 0x10fbd02d0 複製代碼
能夠發現obj
和sark
的isa指針所指向的地址相同且與cls
的地址一致,也就是它們都指向cls
類對象。
注意這邊用的是
objc_getClass
方法,該方法只是單純的返回本類的地址,上文用到的object_getClass
方法返回的纔是isa
指針所指向的(元)類對象地址
咱們仍是直接來看一個面試題, Father
繼承與NSObject
, Son
則繼承於Father
類,分別調用[self class]
和[super class]
, 輸出結果是?
@implementation Son : Father - (instancetype)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; } @end 複製代碼
輸出結果都爲Son
, 爲何[super class]
的結果不是Father
? 咱們簡單分析下就明白了。實例對象的方法列表是存放在isa所指向的類對象中的,因此調用[self class]
的時候會去self
的isa所指向的Son
類對象中尋找該方法,在沒有重載[obj class]
的狀況下, Son
類對象是沒有這個方法的,此時會接着在父類對象的方法列表中查找,最終會發現NSObject
存儲了該方法,因此[self class]
會返回實例對象(self)所屬的Son
這個類對象。
而[super class]
則指定從父類Father
的方法列表開始去查找- (Class)class
這個方法,顯然Father
沒有這個方法,最終仍是要查找到NSObject
類對象的方法列表中,須要注意的是不論是[self class]
仍是[super class]
, 它們都是調用的實例對象的- (Class)class
方法,雖然其指向的類對象不一樣,但實例對象都是self
自己,再強調下區分開實例對象和類對象!於是返回的也是當前self
的isa所指向的Son
類。
其實
super
是objc_super
類型的結構體,它包含了當前的實例對象self
以及父類的類對象。更詳細的解答能夠參考@iOS程序犭袁的博文。
除了用super
來指向父類外,咱們還能夠用isKindOfClass
和isMemberOfClass
來判斷對象的繼承關係。這兩個函數有什麼區別呢?一樣,先來看一個測試題:
BOOL r1 = [[NSObject class] isKindOfClass:[NSObject class]]; // -> YES BOOL r2 = [[RJObject class] isKindOfClass:[RJObject class]]; // -> NO BOOL r3 = [[NSObject class] isMemberOfClass:[NSObject class]]; // -> NO BOOL r4 = [[RJObject class] isMemberOfClass:[RJObject class]]; // -> NO 複製代碼
爲何只有r1
是YES
? 實際上isKindOfClass
是判斷對象是否爲Class
的實例或子類,而isMemberOfClass
則是判斷對象是否爲Class
的實例。仍是不明白?不要緊,咱們直接來看看這兩個函數的源碼實現,看看它們本質上是以什麼做爲判斷標準的:
+ (BOOL)isKindOfClass:(Class)cls { for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } - (BOOL)isKindOfClass:(Class)cls { for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } + (BOOL)isMemberOfClass:(Class)cls { return object_getClass((id)self) == cls; } - (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls; } 複製代碼
注意上面的題目是調用的類方法,因此咱們分析下類方法的實現,至於實例方法也是相似的。能夠看到isMemberOfClass
的判斷是先調用object_getClass
獲取isa所指向的歸屬類,也就是元類,而後直接判斷cls
是否就是被比較的對象的元類。而[NSObject class]
的元類是根元類,顯然不等於[NSObject class]
自己,因此r3
返回NO
, r4
也是同理。
而isKindOfClass
也是先獲取當前對象的元類,可是會循環獲取其isa所指向類的父類進行比較,只要該元類或者元類的父類與cls
相對則返回YES
. RJObject
的元類,以及父元類(最終指向根元類)都不等於RJObject
對象,因此r2
返回NO
. 那爲何r1
返回YES
呢?還記得上文所說的閉環嗎?根元類的父類指向根類自己!顯然, r1
符合了isKindOfClass
的判斷標準。
到這裏理論部分就結束了。那麼,問題來了,理解了類和對象的本質原理有什麼實際應用價值嗎?可讓咱們更優雅的解決項目中遇到的問題和需求嗎?Talk is cheap, show me the code:
好比App常見的記錄用戶行爲的數據統計需求,俗稱埋點。具體來講假設咱們須要記錄用戶對按鈕的點擊。一般狀況下,咱們會在按鈕的點擊事件裏面直接加上數據統計的代碼,但這樣作的問題在於會對業務代碼進行侵入,且統計的代碼散落各處,難以維護。
固然,咱們還能夠建立一個UIButton的子類,在子類中重載點擊事件的響應函數,並在其中加上統計數據部分的代碼:
-(void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event 複製代碼
這樣作是能夠的,可是現有工程中全部須要支持數據統計的按鈕都必須替換成該子類,並且若是哪天不須要支持埋點功能了並須要遷移複用業務代碼,那還得一個個再改回去。因此,咱們須要一個更優雅的實現。
咱們能夠利用動態建立類並添加方法的思路來實現這個需求,這邊只是以埋點做爲示例,你也能夠利用該思路擴展任意須要處理的需求和功能。簡單來講就是咱們建立一個UIButton的Category, 而後在須要埋點的狀況下動態生成一個新的UIButton子類,並給其添加一個能夠記錄數據的事件響應方法來替代默認的方法,以下所示:
// // UIButton+Tracking.m // LearningClass // // Created by Ryan Jin on 07/03/2018. // Copyright © 2018 ArcSoft. All rights reserved. // #import "UIButton+Tracking.h" #import <objc/runtime.h> #import <objc/message.h> @implementation UIButton (Tracking) - (void)enableEventTracking { NSString *className = [NSString stringWithFormat:@"EventTracking_%@",self.class]; Class kClass = objc_getClass([className UTF8String]); if (!kClass) { kClass = objc_allocateClassPair([self class], [className UTF8String], 0); } SEL setterSelector = NSSelectorFromString(@"sendAction:to:forEvent:"); Method setterMethod = class_getInstanceMethod([self class], setterSelector); object_setClass(self, kClass); // 轉換當前類從UIButton到新建的EventTracking_UIButton類 const char *types = method_getTypeEncoding(setterMethod); class_addMethod(kClass, setterSelector, (IMP)eventTracking_SendAction, types); objc_registerClassPair(kClass); } static void eventTracking_SendAction(id self, SEL _cmd, SEL action ,id target , UIEvent *event) { struct objc_super superclass = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)) }; void (*objc_msgSendSuperCasted)(const void *, SEL, SEL, id, UIEvent *) = (void *)objc_msgSendSuper; // to do event tracking... NSLog(@"Click event record: target = %@, action = %@, event = %ld", target, NSStringFromSelector(action), (long)event.type); objc_msgSendSuperCasted(&superclass, _cmd, action, target, event); } @end 複製代碼
而後在添加按鈕的地方,若是須要數據統計功能,則調用enableEventTracking
函數來內嵌打點功能。使用示例以下:
- (void)viewDidLoad { [super viewDidLoad]; UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 50, 30)]; button.layer.borderColor = [[UIColor redColor] CGColor]; button.layer.borderWidth = 1.0f; button.layer.cornerRadius = 4.0f; button.layer.masksToBounds = YES; [button addTarget:self action:@selector(trackingButtonAction:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; [button enableEventTracking]; } - (void)trackingButtonAction:(UIButton *)sender { // to do whatever you want... NSLog(@"%s", __func__); } 複製代碼
打印輸出信息爲:
LearningClass[58516:3424874] Click event record: target = <ViewController: 0x7f97a5d0cb80>, action = trackingButtonAction:, event = 0 LearningClass[58516:3424874] -[ViewController trackingButtonAction:] 複製代碼
浮於表面探究問題不失爲一種方法,可是弄清楚本質纔是真正意義上的解決疑惑。