欲誠其意者,先致其知;致知在格物。物格而後知至,知至而後意誠。現代漢語詞典中將格物致知解釋為: "推究事物的原理,從而獲得知識"。
在程式設計中我們接觸最多的也是最基本的就是類和物件,當我們在建立類或者例項化物件時,是否考慮過類和物件到底是什麼?理解其本質才能真正掌握一門語言。本文將從結構型別角度並結合實際應用探討下Objective-C的類和物件。
在Objective-C中,物件是廣義的概念,類也是物件,所以嚴謹的說法應該是類物件和例項物件。既然例項物件所屬的類稱為類物件,那類物件有所屬的類嗎?有,稱之為元類(Metaclass)。
類物件
類物件(Class)是由程式設計師定義並在執行時由編譯器建立的,它沒有自己的例項變數,這裡需要注意的是類的成員變數和例項方法列表是屬於例項物件的,但其儲存於類物件當中的。我們在/usr/include/objc/objc.h
下看看Class
的定義:
/// 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指標從元類中尋找方法對應的函式指標 -
super_class為該類所繼承的父類物件,如果該類已經是最頂層的根類(如
NSObject
或NSProxy
), 則 super_class為NULL
-
ivars是一個指向
objc_ivar_list
型別的指標,用來儲存每一個例項變數的地址 -
info為執行期使用的一些位標識,比如:
CLS_CLASS (0x1L)
表示該類為普通類,CLS_META (0x2L)
則表示該類為元類 -
methodLists用來存放方法列表,根據info中的標識資訊,當該類為普通類時,儲存的方法為例項方法;如果是元類則儲存的類方法
-
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:]
複製程式碼
浮於表面探究問題不失為一種方法,但是弄清楚本質才是真正意義上的解決疑惑。