該文章屬于劉小壯原創(chuàng)呀潭,轉(zhuǎn)載請(qǐng)注明:劉小壯
NSObject
之前的定義
在OC1.0
中睛蛛,Runtime
很多定義都寫在NSObject.h
文件中锨侯,如果之前研究過(guò)Runtime
的同學(xué)可以應(yīng)該見過(guò)下面的定義,定義了一些基礎(chǔ)的信息鸭你。
// 聲明Class和id
typedef struct objc_class *Class;
typedef struct objc_object *id;
// 聲明常用變量
typedef struct objc_method *Method;
typedef struct objc_ivar *Ivar;
typedef struct objc_category *Category;
typedef struct objc_property *objc_property_t;
// objc_object和objc_class
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
之前的Runtime
結(jié)構(gòu)也比較簡(jiǎn)單稿械,都是一些很直接的結(jié)構(gòu)體定義,現(xiàn)在新版的Runtime
在操作的時(shí)候乏沸,各種地址偏移操作和位運(yùn)算淫茵。
之后的定義
后來(lái)可能蘋果也不太想讓開發(fā)者知道Runtime
內(nèi)部的實(shí)現(xiàn),所以就把源碼定義從NSObject
中搬到Runtime
中了蹬跃。而且之前的定義也不用了匙瘪,通過(guò)OBJC_TYPES_DEFINED
預(yù)編譯指令,將之前的代碼廢棄調(diào)了蝶缀。
現(xiàn)在NSObject
中的定義非常簡(jiǎn)單丹喻,直接就是一個(gè)Class
類型的isa
變量,其他信息都隱藏起來(lái)了翁都。
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
這是最新的一些常用Runtime
定義碍论,和之前的定義也不太一樣了,用了最新的結(jié)構(gòu)體對(duì)象柄慰,之前的結(jié)構(gòu)體也都廢棄了鳍悠。
typedef struct objc_class *Class;
typedef struct objc_object *id;
typedef struct method_t *Method;
typedef struct ivar_t *Ivar;
typedef struct category_t *Category;
typedef struct property_t *objc_property_t;
對(duì)象結(jié)構(gòu)體
objc_object定義
在OC
中每個(gè)對(duì)象都是一個(gè)結(jié)構(gòu)體税娜,結(jié)構(gòu)體中都包含一個(gè)isa
的成員變量,其位于成員變量的第一位藏研。isa
的成員變量之前都是Class
類型的敬矩,后來(lái)蘋果將其改為isa_t
。
struct objc_object {
private:
isa_t isa;
};
OC
中的類和元類也是一樣蠢挡,都是結(jié)構(gòu)體構(gòu)成的弧岳。由于類的結(jié)構(gòu)體定義繼承自objc_object
,所以其也是一個(gè)對(duì)象业踏,并且具有對(duì)象的isa
特征禽炬。
所以可以通過(guò)isa_t
來(lái)查找對(duì)應(yīng)的類或元類,查找方法應(yīng)該是通過(guò)uintptr_t
類型的bits
勤家,通過(guò)按位操作來(lái)查找isa_t
指向的類的地址腹尖。
實(shí)例對(duì)象或類對(duì)象的方法,并不會(huì)定義在各個(gè)對(duì)象中却紧,而是都定義在isa_t
指向的類中桐臊。查找到對(duì)應(yīng)的類后,通過(guò)類的class_data_bits_t
類型的bits
結(jié)構(gòu)體查找方法晓殊,對(duì)象断凶、類、元類都是同樣的查找原理巫俺。
isa_t定義
isa_t
是一個(gè)union
的結(jié)構(gòu)對(duì)象认烁,union
類似于C++
結(jié)構(gòu)體,其內(nèi)部可以定義成員變量和函數(shù)介汹。在isa_t
中定義了cls
却嗡、bits
、isa_t
三部分嘹承,下面的struct
結(jié)構(gòu)體就是isa_t
的結(jié)構(gòu)體構(gòu)成窗价。
下面對(duì)isa_t
中的結(jié)構(gòu)體進(jìn)行了位域聲明,地址從nonpointer
起到extra_rc
結(jié)束叹卷,從低到高進(jìn)行排列撼港。位域也是對(duì)結(jié)構(gòu)體內(nèi)存布局進(jìn)行了一個(gè)聲明,通過(guò)下面的結(jié)構(gòu)體成員變量可以直接操作某個(gè)地址骤竹。位域總共占8
字節(jié)帝牡,所有的位域加在一起正好是64
位。
小提示:union
中bits
可以操作整個(gè)內(nèi)存區(qū)蒙揣,而位域只能操作對(duì)應(yīng)的位靶溜。
下面的代碼是不完整代碼,只保留了arm64
部分,其他部分被忽略掉了罩息。
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1; // 是32位還是64位
uintptr_t has_assoc : 1; // 對(duì)象是否含有或曾經(jīng)含有關(guān)聯(lián)引用嗤详,如果沒有關(guān)聯(lián)引用,可以更快的釋放對(duì)象
uintptr_t has_cxx_dtor : 1; // 表示是否有C++析構(gòu)函數(shù)或OC的析構(gòu)函數(shù)
uintptr_t shiftcls : 33; // 對(duì)象指向類的內(nèi)存地址扣汪,也就是isa指向的地址
uintptr_t magic : 6; // 對(duì)象是否初始化完成
uintptr_t weakly_referenced : 1; // 對(duì)象是否被弱引用或曾經(jīng)被弱引用
uintptr_t deallocating : 1; // 對(duì)象是否被釋放中
uintptr_t has_sidetable_rc : 1; // 對(duì)象引用計(jì)數(shù)太大断楷,是否超出存儲(chǔ)區(qū)域
uintptr_t extra_rc : 19; // 對(duì)象引用計(jì)數(shù)
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
# elif __x86_64__
// ····
# else
// ····
# endif
};
在ARM64
架構(gòu)下,isa_t
以以下結(jié)構(gòu)進(jìn)行布局崭别。在不同的CPU
架構(gòu)下,布局方式會(huì)有所不同恐锣,但參數(shù)都是一樣的茅主。
類結(jié)構(gòu)體
objc_class結(jié)構(gòu)體
在Runtime
中類也是一個(gè)對(duì)象,類的結(jié)構(gòu)體objc_class
是繼承自objc_object
的土榴,具備對(duì)象所有的特征诀姚。在objc_class
中定義了三個(gè)成員變量,superclass
是一個(gè)objc_class
類型的指針玷禽,指向其父類的objc_class
結(jié)構(gòu)體赫段。cache
用來(lái)處理已調(diào)用方法的緩存。
bits
是objc_class
的主角矢赁,其內(nèi)部只定義了一個(gè)uintptr_t
類型的bits
成員變量糯笙,存儲(chǔ)了class_rw_t
的地址。bits
中還定義了一些基本操作撩银,例如獲取class_rw_t
给涕、raw isa
狀態(tài)、是否swift
等函數(shù)额获。objc_class
結(jié)構(gòu)體中定義的一些函數(shù)够庙,其內(nèi)部都是通過(guò)bits
實(shí)現(xiàn)的。
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
// .....
}
從objc_class
的源碼可以看出抄邀,可以通過(guò)bits
結(jié)構(gòu)體的data()
函數(shù)耘眨,獲取class_rw_t
指針。我們進(jìn)入源代碼中看一下境肾,可以看出是通過(guò)對(duì)uintptr_t
類型的bits
變量剔难,做位運(yùn)算查找對(duì)應(yīng)的值。
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
uintptr_t
本質(zhì)上是一個(gè)unsigned long
的typedef
准夷,unsigned long
在64
位處理器中占8
字節(jié)钥飞,正好是64
位二進(jìn)制。通過(guò)FAST_DATA_MASK
轉(zhuǎn)換為二進(jìn)制后衫嵌,是取bits
中的47-3
的位置读宙,正好是取出class_rw_t
指針。
在OC
中一個(gè)指針的長(zhǎng)度是47
楔绞,例如打印一個(gè)UIViewController
的地址是0x7faf1b580450
结闸,轉(zhuǎn)換為二進(jìn)制是11111111010111100011011010110000000010001010000
唇兑,最后面三位是占位的,所以在取地址的時(shí)候會(huì)忽略最后三位桦锄。
// 查找第0位扎附,表示是否swift
#define FAST_IS_SWIFT (1UL<<0)
// 當(dāng)前類或父類是否定義了retain、release等方法
#define FAST_HAS_DEFAULT_RR (1UL<<1)
// 類或父類需要初始化isa
#define FAST_REQUIRES_RAW_ISA (1UL<<2)
// 數(shù)據(jù)段的指針
#define FAST_DATA_MASK 0x00007ffffffffff8UL
// 11111111111111111111111111111111111111111111000 總共47位
因?yàn)樵?code>bits中最后三位是沒用的结耀,所以可以用來(lái)存儲(chǔ)一些其他信息留夜。在class_data_bits_t
還定義了三個(gè)宏,用來(lái)對(duì)后三位做位運(yùn)算图甜。
class_ro_t和class_rw_t
和class_data_bits_t
相關(guān)的有兩個(gè)很重要結(jié)構(gòu)體碍粥,class_rw_t
和class_ro_t
,其中都定義著method list
黑毅、protocol list
嚼摩、property list
等關(guān)鍵信息。
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
};
在編譯后class_data_bits_t
指向的是一個(gè)class_ro_t
的地址矿瘦,這個(gè)結(jié)構(gòu)體是不可變的(只讀)枕面。在運(yùn)行時(shí),才會(huì)通過(guò)realizeClass
函數(shù)將bits
指向class_rw_t
缚去。
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved;
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};
在程序開始運(yùn)行后會(huì)初始化Class
潮秘,在這個(gè)過(guò)程中,會(huì)把編譯器存儲(chǔ)在bits
中的class_ro_t
取出病游,然后創(chuàng)建class_rw_t
唇跨,并把ro
賦值給rw
,成為rw
的一個(gè)成員變量衬衬,最后把rw
設(shè)置給bits
买猖,替代之前bits
中存儲(chǔ)的ro
。除了這些操作外滋尉,還會(huì)有一些其他賦值的操作玉控,下面是初始化Class
的精簡(jiǎn)版代碼。
static Class realizeClass(Class cls)
{
const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;
if (!cls) return nil;
if (cls->isRealized()) return cls;
ro = (const class_ro_t *)cls->data();
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
isMeta = ro->flags & RO_META;
rw->version = isMeta ? 7 : 0;
supercls = realizeClass(remapClass(cls->superclass));
metacls = realizeClass(remapClass(cls->ISA()))
cls->superclass = supercls;
cls->initClassIsa(metacls);
cls->setInstanceSize(ro->instanceSize);
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}
methodizeClass(cls);
return cls;
}
在上面的代碼中我們還發(fā)現(xiàn)了兩個(gè)函數(shù)狮惜,addRootClass
和addSubclass
函數(shù)高诺,這兩個(gè)函數(shù)的職責(zé)是將某個(gè)類的子類串成一個(gè)列表,大致是下面的鏈接順序碾篡。由此可知虱而,我們是可以通過(guò)class_rw_t
,獲取到當(dāng)前類的所有子類开泽。
superClass.firstSubclass -> subClass1.nextSiblingClass -> subClass2.nextSiblingClass -> ...
初始化rw
和ro
之后牡拇,rw
的method list
、protocol list
、property list
都是空的惠呼,需要在下面methodizeClass
函數(shù)中進(jìn)行賦值导俘。函數(shù)中會(huì)把ro
的list
都取出來(lái),然后賦值給rw
剔蹋,如果在運(yùn)行時(shí)動(dòng)態(tài)修改旅薄,也是對(duì)rw
做的操作。所以ro
中存儲(chǔ)的是編譯時(shí)就已經(jīng)決定的原數(shù)據(jù)泣崩,rw
才是運(yùn)行時(shí)動(dòng)態(tài)修改的數(shù)據(jù)少梁。
static void methodizeClass(Class cls)
{
bool isMeta = cls->isMetaClass();
auto rw = cls->data();
auto ro = rw->ro;
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
rw->methods.attachLists(&list, 1);
}
property_list_t *proplist = ro->baseProperties;
if (proplist) {
rw->properties.attachLists(&proplist, 1);
}
protocol_list_t *protolist = ro->baseProtocols;
if (protolist) {
rw->protocols.attachLists(&protolist, 1);
}
if (cls->isRootMetaclass()) {
// root metaclass
addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
}
// Attach categories.
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);
}
假設(shè)創(chuàng)建一個(gè)類LXZObject
,繼承自NSObject
律想,并為其加入一個(gè)testMethod
方法猎莲,不做其他操作。因?yàn)樵诰幾g后objc_class
的bits
對(duì)應(yīng)的是class_ro_t
結(jié)構(gòu)體技即,所以我們打印一下結(jié)構(gòu)體的成員變量,看一下編譯后的class_ro_t
是什么樣的樟遣。
struct class_ro_t {
flags = 128
instanceStart = 8
instanceSize = 8
reserved = 0
ivarLayout = 0x0000000000000000 <no value available>
name = 0x0000000100000f7a "LXZObject"
baseMethodList = 0x00000001000010c8
baseProtocols = 0x0000000000000000
ivars = 0x0000000000000000
weakIvarLayout = 0x0000000000000000 <no value available>
baseProperties = 0x0000000000000000
}
經(jīng)過(guò)打印可以看出而叼,一個(gè)類的class_ro_t
中只會(huì)包含當(dāng)前類的信息,不會(huì)包含其父類的信息豹悬,在LXZObject
類中只會(huì)包含name
和baseMethodList
兩個(gè)字段葵陵,而baseMethodList
中只有一個(gè)testMethod
方法。由此可知瞻佛,class_rw_t
結(jié)構(gòu)體也是一樣的脱篙。
初始化過(guò)程
下面是已經(jīng)初始化后的isa_t
結(jié)構(gòu)體的布局,以及各個(gè)結(jié)構(gòu)體成員在結(jié)構(gòu)體中的位置伤柄。
union
經(jīng)常配合結(jié)構(gòu)體使用绊困,第一次使用union
就是對(duì)結(jié)構(gòu)體區(qū)域做初始化。在對(duì)象初始化時(shí)适刀,會(huì)對(duì)isa_t
的bits
字段賦值為ISA_MAGIC_VALUE
秤朗,這就是對(duì)union
聯(lián)合體初始化的過(guò)程。
// 在objc-723中已經(jīng)沒有了
inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor)
{
if (!indexed) {
isa.cls = cls;
} else {
isa.bits = ISA_MAGIC_VALUE;
isa.has_cxx_dtor = hasCxxDtor;
isa.shiftcls = (uintptr_t)cls >> 3;
}
}
在對(duì)象通過(guò)initIsa()
函數(shù)初始化時(shí)笔喉,會(huì)通過(guò)ISA_MAGIC_VALUE
對(duì)isa
進(jìn)行初始化取视。ISA_MAGIC_VALUE
是一個(gè)16
進(jìn)制的值,將其轉(zhuǎn)換為二進(jìn)制后常挚,會(huì)發(fā)現(xiàn)ISA_MAGIC_VALUE
是對(duì)nonpointer
和magic
做初始化作谭。
nonpointer
是對(duì)之前32
位處理器的兼容。在訪問(wèn)對(duì)象所屬的類時(shí)奄毡,如果是32
位則返回之前的isa
指針地址折欠,否則表示是64
位處理器,則返回isa_t
結(jié)構(gòu)體。
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
二進(jìn)制:11010000000000000000000000000000000000001
補(bǔ)全二進(jìn)制:23個(gè)零+11010000000000000000000000000000000000001
隨后會(huì)通過(guò)位域怨酝,對(duì)has_cxx_dtor
和shiftcls
做初始化傀缩,這時(shí)候就已經(jīng)有四個(gè)字段被初始化了。has_cxx_dtor
表示是否有C++
或OC
的析構(gòu)方法农猬,在打印方法列表時(shí)赡艰,經(jīng)常能看到一個(gè)名為.cxx_destruct
的方法,就和這個(gè)字段有關(guān)系斤葱。
在計(jì)算機(jī)中為了對(duì)存儲(chǔ)區(qū)(Memory or Disk)
讀取方便慷垮,所以在寫入和讀取時(shí),會(huì)對(duì)內(nèi)存有對(duì)其操作揍堕。一般是以字節(jié)為單位進(jìn)行對(duì)其料身,這樣也是對(duì)讀寫速度的優(yōu)化。在對(duì)shiftcls
進(jìn)行賦值時(shí)衩茸,對(duì)Class
的指針進(jìn)行了位移操作芹血,向右位移三位。這是因?yàn)轭愔羔槥榱藘?nèi)存對(duì)其楞慈,將最后三位用0
填充幔烛,所以這三位是沒有意義的。
isa結(jié)構(gòu)體
0000000001011101100000000000000100000000001110101110000011111001
0x5d8001003ae0f8
類對(duì)象地址
100000000001110101110000011111000
0x1003ae0f8
將類對(duì)象地址右移三位為100000000001110101110000011111囊蓝,正好符合isa_t地址中shiftcls的部分饿悬,前面不足補(bǔ)零。
外界獲取Class
時(shí)聚霜,應(yīng)該通過(guò)ISA()
函數(shù)狡恬,而不是像之前一樣直接訪問(wèn)isa
指針。在ISA()
函數(shù)中蝎宇,是對(duì)isa_t
的結(jié)構(gòu)體做與運(yùn)算弟劲,是通過(guò)ISA_MASK
宏進(jìn)行的,轉(zhuǎn)換為二進(jìn)制的話夫啊,正好是把shiftcls
的地址取出來(lái)函卒。
inline Class
objc_object::ISA()
{
return (Class)(isa.bits & ISA_MASK);
}
#define ISA_MASK 0x0000000ffffffff8ULL
111111111111111111111111111111111000
Tagged Pointer
從iPhone5s
開始,iOS
設(shè)備開始引入了64
位處理器撇眯,之前的處理器一直都是32
位的报嵌。
但是在64
位處理器中,指針長(zhǎng)度以及一些變量所占內(nèi)存都發(fā)生了改變熊榛,32
位一個(gè)指針占用4
字節(jié)锚国,但64
位一個(gè)指針占用8
字節(jié);32
位一個(gè)long
占用4
字節(jié)玄坦,64
位一個(gè)long
占用8
字節(jié)等血筑,所以在64
位上內(nèi)存占用會(huì)多出很多绘沉。
蘋果為了優(yōu)化這個(gè)問(wèn)題,推出了Tagged Pointer
新特性豺总。之前一個(gè)指針指向一個(gè)地址车伞,而Tagged Pointer
中一個(gè)指針就代表一個(gè)值,以NSNumber
為例喻喳。
NSNumber *number1 = @1;
NSNumber *number2 = @3;
NSNumber *number3 = @54;
// 輸出
(lldb) p number1
(__NSCFNumber *) $3 = 0xb000000000000012 (int)1
(lldb) p number2
(__NSCFNumber *) $4 = 0xb000000000000032 (int)3
(lldb) p number3
(__NSCFNumber *) $5 = 0xb000000000000362 (int)54
通過(guò)上面代碼可以看出另玖,使用了Tagged Pointer
新特性后,指針中就存儲(chǔ)著對(duì)象的值表伦。例如一個(gè)值為1
的NSNumber
谦去,指針就是0xb000000000000012
,如果拋去前面的0xb
和后面的2
蹦哼,中間正好就是16
進(jìn)制的值鳄哭。
蘋果通過(guò)Tagged Pointer
的特性,明顯的提升了執(zhí)行效率并節(jié)省了很多內(nèi)存纲熏。在64
位處理器下妆丘,內(nèi)存占用減少了將近一半,執(zhí)行效率也大大提升局劲。由于通過(guò)指針來(lái)直接表示數(shù)值飘痛,所以沒有了malloc
和free
的過(guò)程,對(duì)象的創(chuàng)建和銷毀速度提升幾十倍容握。
isa_t
對(duì)于對(duì)象指針也是一樣,在OC1.0
時(shí)代isa
是一個(gè)真的指針车柠,指向一個(gè)堆區(qū)的地址剔氏。而OC2.0
時(shí)代,一個(gè)指針長(zhǎng)度是八字節(jié)也就是64
位竹祷,在64
位中直接存儲(chǔ)著對(duì)象的信息谈跛。當(dāng)查找對(duì)象所屬的類時(shí),直接在isa
指針中進(jìn)行位運(yùn)算即可塑陵,而且由于是在棧區(qū)進(jìn)行操作感憾,查找速度是非常快的令花。
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
例如isa_t
本質(zhì)上是一個(gè)結(jié)構(gòu)體阻桅,如果創(chuàng)建結(jié)構(gòu)體再用指針指向這個(gè)結(jié)構(gòu)體,內(nèi)存占用是很大的兼都。但是Tagged Pointer
特性中嫂沉,直接把結(jié)構(gòu)體的值都存儲(chǔ)到指針中,這就相當(dāng)節(jié)省內(nèi)存了扮碧。
蘋果不允許直接訪問(wèn)isa
指針趟章,和Tagged Pointer
也是有關(guān)系的杏糙。因?yàn)樵?code>Tagged Pointer的情況下,isa
并不是一個(gè)指針指向另一塊內(nèi)存區(qū)蚓土,而是直接表示對(duì)象的值宏侍,所以通過(guò)直接訪問(wèn)isa
獲取到的信息是錯(cuò)誤的。
簡(jiǎn)書由于排版的問(wèn)題蜀漆,閱讀體驗(yàn)并不好谅河,布局、圖片顯示嗜愈、代碼等很多問(wèn)題旧蛾。所以建議到我Github
上,下載Runtime PDF
合集蠕嫁。把所有Runtime
文章總計(jì)九篇锨天,都寫在這個(gè)PDF
中,而且左側(cè)有目錄剃毒,方便閱讀病袄。
下載地址:Runtime PDF
麻煩各位大佬點(diǎn)個(gè)贊,謝謝赘阀!??