ObjC中的TypeEncodings

HarrisonXi_發表於2019-03-04

參考 Apple Developer 官方文件:Type EncodingsObjective-C Runtime

原文地址:蘋果梨的部落格

我們在 JSON <-> Dictionary <-> Model 中面臨的一個很大的問題就是判斷資料需要轉換成什麼樣的型別。好在 ObjC 作為一款動態語言,利用 runtime 可以輕鬆解決這個問題。再配合轉換器和 KVC,就可以輕鬆把我們解析好的值放進對應 Model 裡。今天要給大家介紹的就是這個型別編碼(Type Encodings)的具體細節。

ObjC 的 type encodings 列表

編碼 意義
c char 型別
i int 型別
s short 型別
l long 型別,僅用在 32-bit 裝置上
q long long 型別
C unsigned char 型別
I unsigned int 型別
S unsigned short 型別
L unsigned long 型別
Q unsigned long long 型別
f float 型別
d double 型別,long double 不被 ObjC 支援,所以也是指向此編碼
B bool 或 _Bool 型別
v void 型別
* C 字串(char *)型別
@ 物件(id)型別
# Class 型別
: SEL 型別
[array type] C 陣列型別(注意這不是 NSArray)
{name=type…} 結構體型別
(name=type…) 聯合體型別
bnum 位段(bit field)型別用 b 表示,num 表示位元組數,這個型別很少用
^type 一個指向 type 型別的指標型別
? 未知型別

C 語言基礎資料型別的 type encodings

整型和浮點型資料

簡單給大家舉個例子,我們先來看看常用的數值型別,用下面的程式碼來列印日誌:

NSLog(@"char     : %s, %lu", @encode(char), sizeof(char));
NSLog(@"short    : %s, %lu", @encode(short), sizeof(short));
NSLog(@"int      : %s, %lu", @encode(int), sizeof(int));
NSLog(@"long     : %s, %lu", @encode(long), sizeof(long));
NSLog(@"long long: %s, %lu", @encode(long long), sizeof(long long));
NSLog(@"float    : %s, %lu", @encode(float), sizeof(float));
NSLog(@"double   : %s, %lu", @encode(double), sizeof(double));
NSLog(@"NSInteger: %s, %lu", @encode(NSInteger), sizeof(NSInteger));
NSLog(@"CGFloat  : %s, %lu", @encode(CGFloat), sizeof(CGFloat));
NSLog(@"int32_t  : %s, %lu", @encode(int32_t), sizeof(int32_t));
NSLog(@"int64_t  : %s, %lu", @encode(int64_t), sizeof(int64_t));
複製程式碼

在 32-bit 裝置上輸出日誌如下:

char     : c, 1
short    : s, 2
int      : i, 4
long     : l, 4
long long: q, 8
float    : f, 4
double   : d, 8
NSInteger: i, 4
CGFloat  : f, 4
int32_t  : i, 4
int64_t  : q, 8
複製程式碼

大家注意下上面日誌裡的 long 型別輸出結果,然後我們再看下在 64-bit 裝置上的輸出日誌:

char     : c, 1
short    : s, 2
int      : i, 4
long     : q, 8
long long: q, 8
float    : f, 4
double   : d, 8
NSInteger: q, 8
CGFloat  : d, 8
int32_t  : i, 4
int64_t  : q, 8
複製程式碼

可以看到 long 的長度變成了 8,而且型別編碼也變成 q,這就是表格裡那段話的意思。

所以呢,一般如果想要整形的長度固定且長度能被一眼看出,建議使用例子最後的 int32_tint64_t,儘量少去使用 long 型別。

然後要提一下 NSIntegerCGFloat,這倆都是針對不同 CPU 分開定義的:

#if __LP64__ || (TARGET_OS_EMBEDDED && !TARGET_OS_IPHONE) || TARGET_OS_WIN32 || NS_BUILD_32_LIKE_64
typedef long NSInteger;
#else
typedef int NSInteger;
#endif

#if defined(__LP64__) && __LP64__
# define CGFLOAT_TYPE double
#else
# define CGFLOAT_TYPE float
#endif
typedef CGFLOAT_TYPE CGFloat;
複製程式碼

所以他們在 32-bit 裝置上長度為 4,在 64-bit 裝置上長度為 8,對應型別編碼也會有變化。

布林資料

用下面的程式碼列印日誌:

NSLog(@"bool     : %s, %lu", @encode(bool), sizeof(bool));
NSLog(@"_Bool    : %s, %lu", @encode(_Bool), sizeof(_Bool));
NSLog(@"BOOL     : %s, %lu", @encode(BOOL), sizeof(BOOL));
NSLog(@"Boolean  : %s, %lu", @encode(Boolean), sizeof(Boolean));
NSLog(@"boolean_t: %s, %lu", @encode(boolean_t), sizeof(boolean_t));
複製程式碼

在 32-bit 裝置上輸出日誌如下:

bool     : B, 1
_Bool    : B, 1
BOOL     : c, 1
Boolean  : C, 1
boolean_t: i, 4
複製程式碼

在 64-bit 裝置上輸出日誌如下:

bool     : B, 1
_Bool    : B, 1
BOOL     : B, 1
Boolean  : C, 1
boolean_t: I, 4
複製程式碼

可以看到我們最常用的 BOOL 型別還真的是有點妖,這個妖一句兩句還說不清楚,我在下一篇部落格裡會介紹一下。在本篇部落格裡,這個變化倒是對我們解析模型不會產生很大的影響,所以先略過。

void、指標和陣列

用下面的程式碼列印日誌:

NSLog(@"void    : %s, %lu", @encode(void), sizeof(void));
NSLog(@"char *  : %s, %lu", @encode(char *), sizeof(char *));
NSLog(@"short * : %s, %lu", @encode(short *), sizeof(short *));
NSLog(@"int *   : %s, %lu", @encode(int *), sizeof(int *));
NSLog(@"char[3] : %s, %lu", @encode(char[3]), sizeof(char[3]));
NSLog(@"short[3]: %s, %lu", @encode(short[3]), sizeof(short[3]));
NSLog(@"int[3]  : %s, %lu", @encode(int[3]), sizeof(int[3]));
複製程式碼

在 64-bit 裝置上輸出日誌如下:

void    : v, 1
char *  : *, 8
short * : ^s, 8
int *   : ^i, 8
char[3] : [3c], 3
short[3]: [3s], 6
int[3]  : [3i], 12
複製程式碼

在 32-bit 裝置上指標型別的長度會變成 4,這個就不多介紹了。

可以看到只有 C 字串型別比較特殊,會處理成 * 編碼,其它整形資料的指標型別還是正常處理的。

結構體和聯合體

用下面的程式碼列印日誌:

NSLog(@"CGSize: %s, %lu", @encode(CGSize), sizeof(CGSize));
複製程式碼

在 64-bit 裝置上輸出日誌如下:

CGSize: {CGSize=dd}, 16
複製程式碼

因為 CGSize 內部的欄位都是 CGFloat 的,在 64-bit 裝置上實際是 double 型別,所以等於號後面是兩個 d 編碼,總長度是 16。

聯合體的編碼格式十分類似,不多贅述。而位段現在用到的十分少,也不介紹了,有興趣瞭解位段的可以參考維基百科

ObjC 資料型別的 type encodings

ObjC 資料型別大部分情況下要配合 runtime 使用,單獨用 @encode 操作符的話,基本上也就能做到下面這些:

NSLog(@"Class   : %s", @encode(Class));
NSLog(@"NSObject: %s", @encode(NSObject));
NSLog(@"NSString: %s", @encode(NSString));
NSLog(@"id      : %s", @encode(id));
NSLog(@"Selector: %s", @encode(SEL));
複製程式碼

輸出日誌:

Class   : #
NSObject: {NSObject=#}
NSString: {NSString=#}
id      : @
Selector: :
複製程式碼

可以看到物件的類名稱的編碼方式跟結構體相似,等於號後面那個 # 就是 isa 指標了,是一個 Class 型別的資料。

類屬性和成員變數的 type encodings

我們可以用 runtime 去獲得類的屬性對應的 type encoding:

objc_property_t property = class_getProperty([NSObject class], "description");
if (property) {
    NSLog(@"%s - %s", property_getName(property), property_getAttributes(property));
} else {
    NSLog(@"not found");
}
複製程式碼

我們會獲得這麼一段輸出:

description - T@"NSString",R,C
複製程式碼

這裡的 R 表示 readonlyC 表示 copy,這都是屬性的修飾詞,不過在本篇先不多介紹。

主要要說的是這裡的 T,也就是 type,後面跟的這段 @"NSString" 就是 type encoding 了。可以看到 runtime 比較貼心的用雙引號的方式告訴了我們這個物件的實際型別是什麼。

關於屬性的修飾詞,更多內容可以參考 Apple 文件。其中 T 段始終會是第一個 attribute,所以處理起來會簡單點。

而如果是成員變數的話,我們可以用類似下面的辦法去獲得 type encoding:

@interface TestObject : NSObject {
    int testInt;
    NSString *testStr;
}
@end

Ivar ivar = class_getInstanceVariable([TestObject class], "testInt");
if (ivar) {
    NSLog(@"%s - %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
} else {
    NSLog(@"not found");
}
ivar = class_getInstanceVariable([TestObject class], "testStr");
if (ivar) {
    NSLog(@"%s - %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
} else {
    NSLog(@"not found");
}
複製程式碼

獲得的輸出會是這樣:

testInt - i
testStr - @"NSString"
複製程式碼

因為成員變數沒有屬性修飾詞那些,所以直接獲得的就是 type encoding,格式和屬性的 T attribute 一樣。

類方法的 type encoding

有的時候模型設定資料的方式並不是用屬性的方式,而是用方法的方式。我們舉個例子:

Method method = class_getInstanceMethod([UIView class], @selector(setFrame:));
if (method) {
    NSLog(@"%@ - %s", NSStringFromSelector(method_getName(method)), method_getTypeEncoding(method));
} else {
    NSLog(@"not found");
}
複製程式碼

可以獲得輸出:

setFrame: - v48@0:8{CGRect={CGPoint=dd}{CGSize=dd}}16
複製程式碼

輸出就是整個類方法的 type encoding,關於這個我沒找到官方文件的介紹,所以只能根據自己的推測來介紹這個編碼的格式:

  • 第一個字元 v 是表示函式的返回值是 void 型別
  • 後續的 48 表示函式參數列的長度(指返回值之後的所有引數,雖然返回值在 runtime 裡也算是個引數)
  • 後續的 @ 表示一個物件,在 ObjC 裡這裡傳遞的是 self,例項方法是要傳遞例項物件給函式的
  • 後續的 0 上面引數對應的 offset
  • 後續的 : 表示一個 selector,用來指出要呼叫的函式是哪個
  • 後續的 8 是 selector 引數的 offset,因為這是跑在 64-bit 裝置上的,所以 @: 的長度都是 8
  • 後續的 {CGRect={CGPoint=dd}{CGSize=dd}} 是 CGRect 結構體的 type encoding,從這裡也可以看出結構體巢狀使用時對應的 type encoding 是這種格式的,這個結構體包含 4 個 double 型別的資料,所以總長度應該是 32
  • 最後的 16 是最後一個引數的 offset,加上剛剛的引數長度 32 正好是整個函式參數列的長度

我們拿另一個類方法來驗證下:

Method method = class_getInstanceMethod([UIViewController class], @selector(title));
if (method) {
    NSLog(@"%@ - %s", NSStringFromSelector(method_getName(method)), method_getTypeEncoding(method));
} else {
    NSLog(@"not found");
}
複製程式碼

輸出:

@16@0:8
複製程式碼

可以看到很可惜,NSString 型別在類方法的 type encoding 裡是不會有引號內容的,所以我們只能知道這個引數是個 id 型別。編碼的具體解析:

  • @ – 返回 id 型別
  • 16 – 參數列總長度
  • @ – 用來傳遞 self,是 id 型別
  • 0self 引數的 offset
  • : – 傳遞具體要呼叫哪個方法,selector 型別
  • 8 – selector 引數的 offset

如果是類的靜態方法而不是例項方法,我們可以用類似這樣的程式碼獲得 Method 結構體:

Method method = class_getClassMethod([TestObject class], @selector(testMethod));
複製程式碼

不過說起來這種格式的編碼還是不容易解析,所以我們可以用另一種方式直接拿對應位置的引數的編碼:

Method method = class_getInstanceMethod([UIView class], @selector(setFrame:));
if (method) {
    NSLog(@"%@ - %d", NSStringFromSelector(method_getName(method)), method_getNumberOfArguments(method));
    NSLog(@"%@ - %s", NSStringFromSelector(method_getName(method)), method_copyArgumentType(method, 2));
} else {
    NSLog(@"not found");
}
複製程式碼

輸出內容如下,這裡是獲得了 index 為 2 的引數的編碼:

setFrame: - 3
setFrame: - {CGRect={CGPoint=dd}{CGSize=dd}}
複製程式碼

這樣就只會獲得 type encoding 而不會帶上 offset 資訊,就容易解析多了。

另外從這裡也可以看到,返回值其實也是算一個引數。

其它一些 type encodings 細節

還有些 type encodings 的細節和解析模型其實不太相關,不過也在這裡介紹一下。

protocol 型別的 type encoding

用以下程式碼列印日誌:

objc_property_t property = class_getProperty([UIScrollView class], "delegate");
if (property) {
    NSLog(@"%s - %s", property_getName(property), property_getAttributes(property));
} else {
    NSLog(@"not found");
}
複製程式碼

會獲得輸出:

delegate - T@"<UIScrollViewDelegate>",W,N,V_delegate
複製程式碼

可以看到在屬性的 type encoding 裡,會用雙引號和尖括號表示出 protocol 的型別

但是去檢視方法的話:

Method method = class_getInstanceMethod([UIScrollView class], @selector(setDelegate:));
if (method) {
    NSLog(@"%@ - %d", NSStringFromSelector(method_getName(method)), method_getNumberOfArguments(method));
    NSLog(@"%@ - %s", NSStringFromSelector(method_getName(method)), method_copyArgumentType(method, 2));
} else {
    NSLog(@"not found");
}
複製程式碼

依然還是隻能得到這樣的編碼:

setDelegate: - 3
setDelegate: - @
複製程式碼

protocol 型別在模型解析中並沒有很大的指導作用,因為我們無法知道具體實現了 protocol 協議的 class 是什麼。

block 型別的 type encoding

直接亮結果吧,獲得的 type encoding 是 @?,沒有任何參考意義,還好我們做模型解析用不到這個。

關於方法引數的記憶體對齊

setEnable: 方法取 type encoding 的話會得到:

setEnabled: - v20@0:8B16
複製程式碼

可是 bool 的長度明明只有 1 啊,所以這是為什麼呢?感興趣的朋友可以瞭解下記憶體對齊

總結

關於 Type Encodings,要講的差不多就這麼多了。暫時沒有想到還有什麼要補充的,後面想到了再補上來吧。

希望對大家有幫助,也歡迎大家指正錯誤或者進行討論。

相關文章