iOS运行时-使用Runtime向Category中添加属性以及运行时介绍
前言
了解OC的都應(yīng)該知道,在一般情況下,我們是不能向Category中添加屬性的,只能添加方法,但有些情況向,我們確實(shí)需要向Category中添加屬性,而且很多系統(tǒng)的API也有一些在Category添加屬性的情況,例如我們屬性的UITableView的section和row屬性,就是定義在一個(gè)名為NSIndexPath的分類里的,如下?
那這到底是怎么實(shí)現(xiàn)的呢?
iOS運(yùn)行時(shí)機(jī)制簡(jiǎn)介
iOS運(yùn)行時(shí)機(jī)制,簡(jiǎn)單來(lái)說(shuō),就是蘋(píng)果給開(kāi)發(fā)這提供的一套在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建類、添加屬性/方法(不止這些,還有一些其他功能)的API,它是一套純C語(yǔ)言的API,使用相應(yīng)的API就可以通過(guò)Category給一個(gè)原本存在的類添加屬性。
實(shí)例:使用Runtime向Category中添加屬性
<code class="hljs objectivec has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#import <span class="hljs-title" style="box-sizing: border-box;"><Foundation/Foundation.h></span></span> <span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#import <span class="hljs-title" style="box-sizing: border-box;"><objc/runtime.h></span></span><span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">@interface</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">NSObject</span> (<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">CategoryWithProperty</span>)</span><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">/*** 要在Category中擴(kuò)展的屬性*/</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">@property</span> (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">nonatomic</span>, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">strong</span>) <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">NSObject</span> *property;<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">@end</span><span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">@implementation</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">NSObject</span> (<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">CategoryWithProperty</span>)</span>- (<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">NSObject</span> *)property {<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> objc_getAssociatedObject(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">@selector</span>(property)); }- (<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span>)setProperty:(<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">NSObject</span> *)value {objc_setAssociatedObject(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">self</span>, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">@selector</span>(property), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">@end</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li></ul>這樣就可以在Category中添加屬性了。
一、什么是運(yùn)行時(shí)(Runtime)?
- 運(yùn)行時(shí)是蘋(píng)果提供的純C語(yǔ)言的開(kāi)發(fā)庫(kù)(運(yùn)行時(shí)是一種非常牛逼、開(kāi)發(fā)中經(jīng)常用到的底層技術(shù))
二、運(yùn)行時(shí)的作用?
- 能獲得某個(gè)類的所有成員變量
- 能獲得某個(gè)類的所有屬性
- 能獲得某個(gè)類的所有方法
- 交換方法實(shí)現(xiàn)
- 能動(dòng)態(tài)添加一個(gè)成員變量
- 能動(dòng)態(tài)添加一個(gè)屬性
- 能動(dòng)態(tài)添加一個(gè)方法
三、案例:運(yùn)行時(shí)獲取成員變量名稱
- 1、分析
- 2、獲取UITextFiled成員變量的名稱
// 成員變量的數(shù)量unsigned int outCount = 0;// 獲得所有的成員變量Ivar *ivars = class_copyIvarList([UITextField class], &outCount);// 遍歷所有的成員變量for (int i = 0; i<outCount; i++) {// 取出i位置對(duì)應(yīng)的成員變量Ivar ivar = ivars[i];// 獲得成員變量的名字NSLog(@"%s", ivar_getName(ivar));}// 如果函數(shù)名中包含了copy\new\retain\create等字眼,那么這個(gè)函數(shù)返回的數(shù)據(jù)就需要手動(dòng)釋放free(ivars);
四、iOS底層
1、The Runtime 簡(jiǎn)單介紹
- Objective-C是一門簡(jiǎn)單的語(yǔ)言,95%是C。只是在語(yǔ)言層面上加了些關(guān)鍵字和語(yǔ)法。真正讓Objective-C如此強(qiáng)大的是它的運(yùn)行時(shí)。它很小但卻很強(qiáng)大。它的核心是消息分發(fā)。
Messages
- 執(zhí)行一個(gè)方法,有些語(yǔ)言,編譯器會(huì)執(zhí)行一些額外的優(yōu)化和錯(cuò)誤檢查,因?yàn)檎{(diào)用關(guān)系很直接也很明顯。但對(duì)于消息分發(fā)來(lái)說(shuō),就不那么明顯了。在發(fā)消息前不必知道某個(gè)對(duì)象是否能夠處理消息。你把消息發(fā)給它,它可能會(huì)處理,也可能轉(zhuǎn)給其他的Object來(lái)處理。一個(gè)消息不必對(duì)應(yīng)一個(gè)方法,一個(gè)對(duì)象可能實(shí)現(xiàn)一個(gè)方法來(lái)處理多條消息。
- 在Objective-C中,消息是通過(guò)objc_msgSend()這個(gè)runtime方法及相近的方法來(lái)實(shí)現(xiàn)的。這個(gè)方法需要一個(gè)target,selector,還有一些參數(shù)。理論上來(lái)說(shuō),編譯器只是把消息分發(fā)變成objc_msgSend來(lái)執(zhí)行。比如下面這兩行代碼是等價(jià)的。
Objects, Classes, MetaClasses
- 大多數(shù)面向?qū)ο蟮恼Z(yǔ)言里有 classes 和 objects 的概念。Objects通過(guò)Classes生成。但是在Objective-C中,classes本身也是objects,也可以處理消息,這也是為什么會(huì)有類方法和實(shí)例方法。具體來(lái)說(shuō),Objective-C中的Object是一個(gè)結(jié)構(gòu)體(struct),第一個(gè)成員是isa,指向自己的class。這是在objc/objc.h中定義的。
- object的class保存了方法列表,還有指向父類的指針。但classes也是objects,也會(huì)有isa變量,那么它又指向哪兒呢?這里就引出了第三個(gè)類型: metaclasses。一個(gè) metaclass被指向class,class被指向object。它保存了所有實(shí)現(xiàn)的方法列表,以及父類的metaclass。如果想更清楚地了解objects,classes以及metaclasses是如何一起工作地,可以閱讀這篇文章。
Methods, Selectors and IMPs
-
我們知道了運(yùn)行時(shí)會(huì)發(fā)消息給對(duì)象。我們也知道一個(gè)對(duì)象的class保存了方法列表。那么這些消息是如何映射到方法的,這些方法又是如何被執(zhí)行的呢?
-
第一個(gè)問(wèn)題的答案很簡(jiǎn)單。class的方法列表其實(shí)是一個(gè)字典,key為selectors,IMPs為value。一個(gè)IMP是指向方法在內(nèi)存中的實(shí)現(xiàn)。很重要的一點(diǎn)是,selector和IMP之間的關(guān)系是在運(yùn)行時(shí)才決定的,而不是編譯時(shí)。這樣我們就能玩出些花樣。
-
IMP通常是指向方法的指針,第一個(gè)參數(shù)是self,類型為id,第二個(gè)參數(shù)是_cmd,類型為SEL,余下的是方法的參數(shù)。這也是self和_cmd被定義的地方。下面演示了Method和IMP
- 現(xiàn)在我們知道了objects,classes,selectors,IMPs以及消息分發(fā),那么運(yùn)行時(shí)到底能做什么呢?
運(yùn)行時(shí)到底能做什么呢?
-
作用:
- 創(chuàng)建、修改、自省classes和objects
- 消息分發(fā)
-
之前已經(jīng)提過(guò)消息分發(fā),不過(guò)這只是一小部分功能。所有的運(yùn)行時(shí)方法都有特定的前綴。下面是一些有意思的方法:
class
- class開(kāi)頭的方法是用來(lái)修改和自省classes。
- 方法如:
- 能拿到一個(gè)class的所有內(nèi)容 class_addIvar, class_addMethod, class_addProperty和class_addProtocol允許重建classes。class_copyIvarList, class_copyMethodList, class_copyProtocolList和class_copyPropertyList
- 返回單個(gè)內(nèi)容 class_getClassMethod, class_getClassVariable, class_getInstanceMethod, class_getInstanceVariable, class_getMethodImplementation和class_getProperty
- 一些通用的自省方法 class_conformsToProtocol, class_respondsToSelector, class_getSuperclass
- 創(chuàng)建一個(gè)object class_createInstance來(lái)創(chuàng)建一個(gè)object
ivar
- 這些方法能讓你得到名字,內(nèi)存地址和Objective-C type encoding。
method
- 這些方法主要用來(lái)自省,比如:
- 也有一些修改的方法,包括: method_setImplementation和method_exchangeImplementations
objc
- 一旦拿到了object,你就可以對(duì)它做一些自省和修改。你可以get/set ivar, 使用object_copy和object_dispose來(lái)copy和free object的內(nèi)存。不僅是拿到一個(gè)class,而是可以使用object_setClass來(lái)改變一個(gè)object的class。
property
- 屬性保存了很大一部分信息。除了拿到名字,你還可以使用property_getAttributes來(lái)發(fā)現(xiàn)property的更多信息,如返回值、是否為atomic、getter/setter名字、是否為dynamic、背后使用的ivar名字、是否為弱引用。
protocol
- Protocols有點(diǎn)像classes,但是精簡(jiǎn)版的,運(yùn)行時(shí)的方法是一樣的。你可以獲取method, property, protocol列表, 檢查是否實(shí)現(xiàn)了其他的protocol。
sel
- 最后我們有一些方法可以處理 selectors,比如獲取名字,注冊(cè)一個(gè)selector等等。
2、運(yùn)行時(shí)能干什么?(舉例)
2.1 Classes And Selectors From Strings
-
比較基礎(chǔ)的一個(gè)動(dòng)態(tài)特性是通過(guò)String來(lái)生成Classes和Selectors。Cocoa提供了NSClassFromString和NSSelectorFromString方法,使用起來(lái)很簡(jiǎn)單:
Class stringclass = NSClassFromString(@"NSString") -
于是我們就得到了一個(gè)string class。接下來(lái):
NSString *myString = [stringclass stringWithString:@"Hello World"]; -
為什么要這么做呢?直接使用Class不是更方便?通常情況下是,但有些場(chǎng)景下這個(gè)方法會(huì)很有用。首先,可以得知是否存在某個(gè)class,NSClassFromString 會(huì)返回nil,如果運(yùn)行時(shí)不存在該class的話。
-
另一個(gè)使用場(chǎng)景是根據(jù)不同的輸入返回不同的class或method。比如你在解析一些數(shù)據(jù),每個(gè)數(shù)據(jù)項(xiàng)都有要解析的字符串以及自身的類型(String,Number,Array)。你可以在一個(gè)方法里搞定這些,也可以使用多個(gè)方法。其中一個(gè)方法是獲取type,然后使用if來(lái)調(diào)用匹配的方法。另一種是根據(jù)type來(lái)生成一個(gè)selector,然后調(diào)用之。以下是兩種實(shí)現(xiàn)方式:
- 可一看到,你可以把7行帶if的代碼變成1行。將來(lái)如果有新的類型,只需增加實(shí)現(xiàn)方法即可,而不用再去添加新的 else if。
2.2 Method Swizzling
-
之前我們講過(guò),方法由兩個(gè)部分組成。Selector相當(dāng)于一個(gè)方法的id;IMP是方法的實(shí)現(xiàn)。這樣分開(kāi)的一個(gè)便利之處是selector和IMP之間的對(duì)應(yīng)關(guān)系可以被改變。比如一個(gè) IMP 可以有多個(gè) selectors 指向它。
-
而 Method Swizzling 可以交換兩個(gè)方法的實(shí)現(xiàn)。或許你會(huì)問(wèn)“什么情況下會(huì)需要這個(gè)呢?”。我們先來(lái)看下Objective-C中,兩種擴(kuò)展class的途徑。首先是 subclassing。你可以重寫(xiě)某個(gè)方法,調(diào)用父類的實(shí)現(xiàn),這也意味著你必須使用這個(gè)subclass的實(shí)例,但如果繼承了某個(gè)Cocoa class,而Cocoa又返回了原先的class(比如 NSArray)。這種情況下,你會(huì)想添加一個(gè)方法到NSArray,也就是使用Category。99%的情況下這是OK的,但如果你重寫(xiě)了某個(gè)方法,就沒(méi)有機(jī)會(huì)再調(diào)用原先的實(shí)現(xiàn)了。
-
Method Swizzling 可以搞定這個(gè)問(wèn)題。你可以重寫(xiě)某個(gè)方法而不用繼承,同時(shí)還可以調(diào)用原先的實(shí)現(xiàn)。通常的做法是在category中添加一個(gè)方法(當(dāng)然也可以是一個(gè)全新的class)。可以通過(guò)method_exchangeImplementations這個(gè)運(yùn)行時(shí)方法來(lái)交換實(shí)現(xiàn)。來(lái)看一個(gè)demo,這個(gè)demo演示了如何重寫(xiě)addObject:方法來(lái)紀(jì)錄每一個(gè)新添加的對(duì)象。
- 我們把方法交換放到了load中,這個(gè)方法只會(huì)被調(diào)用一次,而且是運(yùn)行時(shí)載入。如果指向臨時(shí)用一下,可以放到別的地方。注意到一個(gè)很明顯的遞歸調(diào)用logAddObject:。這也是Method Swizzling容易把我們搞混的地方,因?yàn)槲覀円呀?jīng)交換了方法的實(shí)現(xiàn),所以其實(shí)調(diào)用的是addObject:
動(dòng)態(tài)繼承、交換
-
我們可以在運(yùn)行時(shí)創(chuàng)建新的class,這個(gè)特性用得不多,但其實(shí)它還是很強(qiáng)大的。你能通過(guò)它創(chuàng)建新的子類,并添加新的方法。
-
但這樣的一個(gè)子類有什么用呢?別忘了Objective-C的一個(gè)關(guān)鍵點(diǎn):object內(nèi)部有一個(gè)叫做isa的變量指向它的class。這個(gè)變量可以被改變,而不需要重新創(chuàng)建。然后就可以添加新的ivar和方法了。可以通過(guò)以下命令來(lái)修改一個(gè)object的class.
object_setClass(myObject, [MySubclass class]); -
這可以用在Key Value Observing。當(dāng)你開(kāi)始o(jì)bserving an object時(shí),Cocoa會(huì)創(chuàng)建這個(gè)object的class的subclass,然后將這個(gè)object的isa指向新創(chuàng)建的subclass。
動(dòng)態(tài)方法處理
-
目前為止,我們討論了方法交換,以及已有方法的處理。那么當(dāng)你發(fā)送了一個(gè)object無(wú)法處理的消息時(shí)會(huì)發(fā)生什么呢?很明顯,"it breaks"。大多數(shù)情況下確實(shí)如此,但Cocoa和runtime也提供了一些應(yīng)對(duì)方法。
-
首先是動(dòng)態(tài)方法處理。通常來(lái)說(shuō),處理一個(gè)方法,運(yùn)行時(shí)尋找匹配的selector然后執(zhí)行之。有時(shí),你只想在運(yùn)行時(shí)才創(chuàng)建某個(gè)方法,比如有些信息只有在運(yùn)行時(shí)才能得到。要實(shí)現(xiàn)這個(gè)效果,你需要重寫(xiě)+resolveInstanceMethod: 和/或 +resolveClassMethod:。如果確實(shí)增加了一個(gè)方法,記得返回YES。
- 那Cocoa在什么場(chǎng)景下會(huì)使用這些方法呢?Core Data用得很多。NSManagedObjects有許多在運(yùn)行時(shí)添加的屬性用來(lái)處理get/set屬性和關(guān)系。那如果Model在運(yùn)行時(shí)被改變了呢?
消息轉(zhuǎn)發(fā)
-
如果 resolve method 返回NO,運(yùn)行時(shí)就進(jìn)入下一步驟:消息轉(zhuǎn)發(fā)。有兩種常見(jiàn)用例。1) 將消息轉(zhuǎn)發(fā)到另一個(gè)可以處理該消息的object。2) 將多個(gè)消息轉(zhuǎn)發(fā)到同一個(gè)方法。
-
消息轉(zhuǎn)發(fā)分兩步。首先,運(yùn)行時(shí)調(diào)用-forwardingTargetForSelector:,如果只是想把消息發(fā)送到另一個(gè)object,那么就使用這個(gè)方法,因?yàn)楦咝АH绻胍薷南?#xff0c;那么就要使用-forwardInvocation:,運(yùn)行時(shí)將消息打包成NSInvocation,然后返回給你處理。處理完之后,調(diào)用invokeWithTarget:。
-
Cocoa有幾處地方用到了消息轉(zhuǎn)發(fā),主要的兩個(gè)地方是代理(Proxies)和響應(yīng)鏈(Responder Chain)。NSProxy是一個(gè)輕量級(jí)的class,它的作用就是轉(zhuǎn)發(fā)消息到另一個(gè)object。如果想要惰性加載object的某個(gè)屬性會(huì)很有用。NSUndoManager也有用到,不過(guò)是截取消息,之后再執(zhí)行,而不是轉(zhuǎn)發(fā)到其他的地方。
-
響應(yīng)鏈?zhǔn)顷P(guān)于Cocoa如何處理與發(fā)送事件與行為到對(duì)應(yīng)的對(duì)象。比如說(shuō),使用Cmd+C執(zhí)行了copy命令,會(huì)發(fā)送-copy:到響應(yīng)鏈。首先是First Responder,通常是當(dāng)前的UI。如果沒(méi)有處理該消息,則轉(zhuǎn)發(fā)到下一個(gè)-nextResponder。這么一直下去直到找到能夠處理該消息的object,或者沒(méi)有找到,報(bào)錯(cuò)。
使用Block作為Method IMP
- iOS 4.3帶來(lái)了很多新的runtime方法。除了對(duì)properties和protocols的加強(qiáng),還帶來(lái)一組新的以 imp 開(kāi)頭的方法。通常一個(gè) IMP 是一個(gè)指向方法實(shí)現(xiàn)的指針,頭兩個(gè)參數(shù)為 object(self)和selector(_cmd)。iOS 4.0和Mac OS X 10.6 帶來(lái)了block,imp_implementationWithBlock() 能讓我們使用block作為 IMP,下面這個(gè)代碼片段展示了如何使用block來(lái)添加新的方法。
- 可以看到,Objective-C 表面看起來(lái)挺簡(jiǎn)單,但還是很靈活的,可以帶來(lái)很多可能性。動(dòng)態(tài)語(yǔ)言的優(yōu)勢(shì)在于在不擴(kuò)展語(yǔ)言本身的情況下做很多很靈巧的事情。比如Key Value Observing,提供了優(yōu)雅的API可以與已有的代碼無(wú)縫結(jié)合,而不需要新增語(yǔ)言級(jí)別的特性。
總結(jié)
以上是生活随笔為你收集整理的iOS运行时-使用Runtime向Category中添加属性以及运行时介绍的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: iOS中监测来电方案
- 下一篇: iOS 够逼格的注释总结