面試的信心來(lái)源于過(guò)硬的基礎(chǔ)(iOS開(kāi)發(fā)方向)

在過(guò)去的一年很多人不滿于公司沒(méi)有福利儡羔、人際關(guān)系不好相處、沒(méi)有發(fā)展前途的境遇等等舷嗡,想著在開(kāi)年來(lái)?yè)Q一份工作來(lái)重新開(kāi)始自己,那么 你 準(zhǔn)備好了嗎嵌莉?

下面是本人整理的一份面試材料进萄,本想自己用的,但是新年第一天 公司突然給了我個(gè)驚喜锐峭,漲工資了V惺蟆!沿癞!

UIView和CALayer是什么關(guān)系

UIView繼承自UIResponder類援雇,可以響應(yīng)事件
CALayer直接繼承自NSObject類,不可以響應(yīng)事件
UIView是CALayer的delegate(CALayerDelegate)
UIView主要處理事件椎扬,CALayer負(fù)責(zé)繪制
每個(gè)UIView內(nèi)部都有一個(gè)CALayer在背后提供內(nèi)容的繪制和顯示惫搏,并且UIView的尺寸樣式都由內(nèi)部的Layer所提供具温。兩者都有樹(shù)狀層級(jí)結(jié)構(gòu),Layer內(nèi)部有SubLayers筐赔,View內(nèi)部有SubViews铣猩,但是Layer比View多了個(gè)AnchorPoint
NSCache和NSMutableDictionary的相同點(diǎn)與區(qū)別

相同點(diǎn):
NSCache和NSMutableDictionary功能用法基本是相同的
區(qū)別:
NSCache是線程安全的,NSMutableDictionary線程不安全茴丰,Mutable開(kāi)發(fā)的類一般都是線程不安全的
當(dāng)內(nèi)存不足時(shí)NSCache會(huì)自動(dòng)釋放內(nèi)存(所以從緩存中取數(shù)據(jù)的時(shí)候總要判斷是否為空)
NSCache可以指定緩存的限額达皿,當(dāng)緩存超出限額自動(dòng)釋放內(nèi)存
NSCache的Key只是對(duì)對(duì)象進(jìn)行了Strong引用,而非拷貝贿肩,所以不需要實(shí)現(xiàn)NSCopying協(xié)議

atomic的實(shí)現(xiàn)機(jī)制峦椰;為什么不能保證絕對(duì)的線程安全(最好可以結(jié)合場(chǎng)景來(lái)說(shuō))

在過(guò)去的一年很多人不滿于公司沒(méi)有福利、人際關(guān)系不好相處尸曼、沒(méi)有發(fā)展前途的境遇等等们何,想著在開(kāi)年來(lái)?yè)Q一份工作來(lái)重新開(kāi)始自己,那么 你 準(zhǔn)備好了嗎控轿?

下面是本人整理的一份面試材料冤竹,本想自己用的,但是新年第一天 公司突然給了我個(gè)驚喜茬射,漲工資了p腥洹!在抛!

  1. UIViewCALayer是什么關(guān)系

    • UIView繼承自UIResponder類钟病,可以響應(yīng)事件
    • CALayer直接繼承自NSObject類,不可以響應(yīng)事件
    • UIViewCALayerdelegate(CALayerDelegate)
    • UIView主要處理事件刚梭,CALayer負(fù)責(zé)繪制
    • 每個(gè)UIView內(nèi)部都有一個(gè)CALayer在背后提供內(nèi)容的繪制和顯示肠阱,并且UIView的尺寸樣式都由內(nèi)部的Layer所提供。兩者都有樹(shù)狀層級(jí)結(jié)構(gòu)朴读,Layer內(nèi)部有SubLayers屹徘,View內(nèi)部有SubViews,但是LayerView多了個(gè)AnchorPoint
  2. NSCacheNSMutableDictionary的相同點(diǎn)與區(qū)別

    相同點(diǎn):
    NSCacheNSMutableDictionary功能用法基本是相同的
    區(qū)別:
    NSCache是線程安全的衅金,NSMutableDictionary線程不安全噪伊,Mutable開(kāi)發(fā)的類一般都是線程不安全的
    當(dāng)內(nèi)存不足時(shí)NSCache會(huì)自動(dòng)釋放內(nèi)存(所以從緩存中取數(shù)據(jù)的時(shí)候總要判斷是否為空)
    NSCache可以指定緩存的限額,當(dāng)緩存超出限額自動(dòng)釋放內(nèi)存
    NSCacheKey只是對(duì)對(duì)象進(jìn)行了Strong引用氮唯,而非拷貝鉴吹,所以不需要實(shí)現(xiàn)NSCopying協(xié)議

  3. atomic的實(shí)現(xiàn)機(jī)制;為什么不能保證絕對(duì)的線程安全(最好可以結(jié)合場(chǎng)景來(lái)說(shuō))

    • atomic會(huì)對(duì)屬性的setter/getter方法進(jìn)行加鎖惩琉,這僅僅只能保證在操作setter/getter方法是安全的豆励。不能保證其他線程的安全
    • 例如:線程1調(diào)用了某一屬性的setter方法并進(jìn)行到了一半,線程2調(diào)用其getter方法琳水,那么會(huì)執(zhí)行完setter操作后肆糕,再執(zhí)行getter操作般堆,線程2會(huì)獲取到線程1setter后的完整的值;當(dāng)幾個(gè)線程同時(shí)調(diào)用同一屬性的setter诚啃、getter方法時(shí)淮摔,會(huì)獲取到一個(gè)完整的值,但獲取到的值不可控
  4. iOS 中內(nèi)省的幾個(gè)方法

對(duì)象在運(yùn)行時(shí)獲取其類型的能力稱為內(nèi)省始赎。內(nèi)省可以有多種方法實(shí)現(xiàn)
OC運(yùn)行時(shí)內(nèi)省的4個(gè)方法:

  • 判斷對(duì)象類型:
-(BOOL) isKindOfClass:            // 判斷是否是這個(gè)類或者這個(gè)類的子類的實(shí)例
-(BOOL) isMemberOfClass:      // 判斷是否是這個(gè)類的實(shí)例

  • 判斷對(duì)象/類是否有這個(gè)方法
-(BOOL) respondsToSelector:                      // 判斷實(shí)例是否有這樣方法
+(BOOL) instancesRespondToSelector:      // 判斷類是否有這個(gè)方法

作為一個(gè)開(kāi)發(fā)者和橙,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,這是一個(gè)iOS交流群:1001906160 造垛,不管你是小白還是大牛歡迎入駐 魔招,分享BAT,阿里面試題、面試經(jīng)驗(yàn)五辽,討論技術(shù)办斑, 大家一起交流學(xué)習(xí)成長(zhǎng)!

image
  1. objc在向一個(gè)對(duì)象發(fā)送消息時(shí)杆逗,發(fā)生了什么

    根據(jù)對(duì)象的isa指針找到該對(duì)象所屬的類乡翅,去objc的對(duì)應(yīng)的類中找方法
    1.首先,在相應(yīng)操作的對(duì)象中的緩存方法列表中找調(diào)用的方法罪郊,如果找到蠕蚜,轉(zhuǎn)向相應(yīng)實(shí)現(xiàn)并執(zhí)行
    2.如果沒(méi)找到,在相應(yīng)操作的對(duì)象中的方法列表中找調(diào)用的方法悔橄,如果找到靶累,轉(zhuǎn)向相應(yīng)實(shí)現(xiàn)執(zhí)行
    3.如果沒(méi)找到,去父類指針?biāo)赶虻膶?duì)象中執(zhí)行1癣疟,2.
    4.以此類推挣柬,如果一直到根類還沒(méi)找到,轉(zhuǎn)向攔截調(diào)用睛挚,走消息轉(zhuǎn)發(fā)機(jī)制
    5.如果沒(méi)有重寫(xiě)攔截調(diào)用的方法凛忿,程序報(bào)錯(cuò)

  2. 你是否接觸過(guò)OC中的反射機(jī)制?簡(jiǎn)單聊一下概念和使用

  • class反射
  • 通過(guò)類名的字符串形式實(shí)例化對(duì)象
Class class = NSClassFromString(@"student"); 
Student *stu = [[class alloc] init];

  • 將類名變?yōu)樽址?/li>
Class class = [Student class];
NSString *className = NSStringFromClass(class);

  • SEL的反射

  • 通過(guò)方法的字符串形式實(shí)例化方法

SEL selector = NSSelectorFromString(@"setName");
[stu performSelector:selector withObject:@"Mike"];

  • 將方法變成字符串
    NSStringFromSelector(@selector(setName:));
  1. 這個(gè)寫(xiě)法會(huì)出什么問(wèn)題@property (nonatomic, copy) NSMutableArray *arr;

    添加竞川,刪除,修改數(shù)組內(nèi)元素的時(shí)候叁熔,程序會(huì)因?yàn)檎也坏綄?duì)應(yīng)的方法而崩潰委乌。原因:是因?yàn)?code>copy就是復(fù)制一個(gè)不可變NSArray的對(duì)象,不能對(duì)NSArray對(duì)象進(jìn)行添加/修改

  2. 如何讓自己的類用copy修飾符

    若想令自己所寫(xiě)的對(duì)象具有拷貝功能荣回,則需實(shí)現(xiàn)NSCopying協(xié)議遭贸。如果自定義的對(duì)象分為可變版本與不可變版本,那么就要同時(shí)實(shí)現(xiàn)NSCopyingNSMutableCopying協(xié)議心软。
    具體步驟:
    1.需聲明該類遵從NSCopying協(xié)議
    2.實(shí)現(xiàn)NSCopying協(xié)議的方法壕吹,具體區(qū)別戳這里

  • NSCopying協(xié)議方法為:
- (id)copyWithZone:(NSZone *)zone {
  MyObject *copy = [[[self class] allocWithZone: zone] init];
  copy.username = self.username;
  return copy;
}

  1. 為什么assign不能用于修飾對(duì)象

    首先我們需要明確著蛙,對(duì)象的內(nèi)存一般被分配到堆上,基本數(shù)據(jù)類型和oc數(shù)據(jù)類型的內(nèi)存一般被分配在棧上
    如果用assign修飾對(duì)象耳贬,當(dāng)對(duì)象被釋放后踏堡,指針的地址還是存在的,也就是說(shuō)指針并沒(méi)有被置為nil咒劲,從而造成了野指針顷蟆。因?yàn)閷?duì)象是分配在堆上的,堆上的內(nèi)存由程序員分配釋放腐魂。而因?yàn)橹羔槢](méi)有被置為nil帐偎,如果后續(xù)的內(nèi)存分配中,剛好分配到了這塊內(nèi)存蛔屹,就會(huì)造成崩潰
    assign修飾基本數(shù)據(jù)類型或oc數(shù)據(jù)類型削樊,因?yàn)榛緮?shù)據(jù)類型是分配在棧上的,由系統(tǒng)分配和釋放兔毒,所以不會(huì)造成野指針

  2. 請(qǐng)寫(xiě)出以下代碼輸出

int a[5] = {1, 2, 3, 4, 5};
int *ptr = (int *)(&a + 1);
printf("%d, %d", *(a + 1), *(ptr + 1));

參考答案:2漫贞,隨機(jī)值
分析:
a代表有5個(gè)元素的數(shù)組的首地址,a[5]的元素分別是1眼刃,2绕辖,3,4擂红,5仪际。接下來(lái),a + 1表示數(shù)據(jù)首地址加1昵骤,那么就是a[1]树碱,也就是對(duì)應(yīng)于值為2,但是变秦,這里是&a + 1成榜,因?yàn)?code>a代表的是整個(gè)數(shù)組,它的空間大小為5 * sizeof(int)蹦玫,因此&a + 1就是a + 5赎婚。a是個(gè)常量指針,指向當(dāng)前數(shù)組的首地址樱溉,指針+1就是移動(dòng)sizeof(int)個(gè)字節(jié)
因此挣输,ptr是指向int *類型的指針,而ptr指向的就是a + 5福贞,那么ptr + 1也相當(dāng)于a + 6撩嚼,所以最后的*(ptr + 1)就是一個(gè)隨機(jī)值了。而*(ptr – 1)就相當(dāng)于a + 4,對(duì)應(yīng)的值就是5

  1. 一個(gè)view已經(jīng)初始化完畢完丽,view上面添加了n個(gè)button(可能使用循環(huán)創(chuàng)建)恋技,除用viewtag之外,還可以采用什么辦法來(lái)找到自己想要的button來(lái)修改Button的值
> 第一種:如果是點(diǎn)擊某個(gè)按鈕后逻族,才會(huì)刷新它的值蜻底,其它不用修改,那么不用引用任何按鈕瓷耙,直接在回調(diào)時(shí)朱躺,就已經(jīng)將接收響應(yīng)的按鈕給傳過(guò)來(lái)了,直接通過(guò)它修改即可
> 第二種:點(diǎn)擊某個(gè)按鈕后搁痛,所有與之同類型的按鈕都要修改值长搀,那么可以通過(guò)在創(chuàng)建按鈕時(shí)將按鈕存入到數(shù)組中,在需要的時(shí)候遍歷查找

  1. UIViewControllerviewDidUnload鸡典、viewDidLoadloadView分別什么時(shí)候調(diào)用源请?UIViewdrawRectlayoutSubviews分別起什么作用
> 第一個(gè)問(wèn)題:
> 在控制器被銷毀前會(huì)調(diào)用`viewDidUnload`(`MRC`下才會(huì)調(diào)用)
> 在控制器沒(méi)有任何`view`時(shí),會(huì)調(diào)用`loadView`
> 在`view`加載完成時(shí)彻况,會(huì)調(diào)用`viewDidLoad`
> 第二個(gè)問(wèn)題:
> 在調(diào)用`setNeedsDisplay`后谁尸,會(huì)調(diào)用`drawRect`方法,我們通過(guò)在此方法中可以獲取到`context`(設(shè)置上下文)纽甘,就可以實(shí)現(xiàn)繪圖
> 在調(diào)用`setNeedsLayout`后良蛮,會(huì)調(diào)用`layoutSubviews`方法,我們可以通過(guò)在此方法去調(diào)整UI悍赢。當(dāng)然能引起`layoutSubviews`調(diào)用的方式有很多種的决瞳,比如添加子視圖、滾動(dòng)`scrollview`左权、修改視圖的`frame`等

  1. 自動(dòng)釋放池工作原理
> 自動(dòng)釋放池是`NSAutorelease`類的一個(gè)實(shí)例皮胡,當(dāng)向一個(gè)對(duì)象發(fā)送`autorelease`消息時(shí),該對(duì)象會(huì)自動(dòng)入池赏迟,待池銷毀時(shí)屡贺,將會(huì)向池中所有對(duì)象發(fā)送一條`release`消息,釋放對(duì)象
> `[pool release]锌杀、[pool drain]`表示的是池本身不會(huì)銷毀甩栈,而是池子中的臨時(shí)對(duì)象都被發(fā)送`release`,從而將對(duì)象銷毀

  1. 蘋(píng)果是如何實(shí)現(xiàn)autoreleasepool
> `autoreleasepool`是由`AutoreleasePoolPage`以雙向鏈表的方式實(shí)現(xiàn)的糕再,主要通過(guò)下列三個(gè)函數(shù)完成:
> 
> *   由`objc_autoreleasePoolPush`作為自動(dòng)釋放池作用域的第一個(gè)函數(shù)
> *   使用`objc_autorelease`將對(duì)象加入自動(dòng)釋放池
> *   由`objc_autoreleasePoolPop`作為自動(dòng)釋放池作用域的最后一個(gè)函數(shù)

  1. autorelease的對(duì)象何時(shí)被釋放
RunLoop在每個(gè)事件循環(huán)結(jié)束后會(huì)去自動(dòng)釋放池將所有自動(dòng)釋放對(duì)象的引用計(jì)數(shù)減一谤职,若引用計(jì)數(shù)變成了0,則會(huì)將對(duì)象真正銷毀掉亿鲜,回收內(nèi)存。
在沒(méi)有手動(dòng)添加Autorelease Pool的情況下,autorelease的對(duì)象是在每個(gè)事件循環(huán)結(jié)束后蒿柳,自動(dòng)釋放池才會(huì)對(duì)所有自動(dòng)釋放的對(duì)象的引用計(jì)數(shù)減一饶套,若引用計(jì)數(shù)變成了0,則釋放對(duì)象垒探,回收內(nèi)存妓蛮。因此,若想要早一點(diǎn)釋放掉autorelease對(duì)象圾叼,那么我們可以在對(duì)象外加一個(gè)自動(dòng)釋放池蛤克。比如,在循環(huán)處理數(shù)據(jù)時(shí)夷蚊,臨時(shí)變量要快速釋放构挤,就應(yīng)該采用這種方式:

// 通過(guò)alloc創(chuàng)建的對(duì)象,直接加入@autoreleasepool沒(méi)有作用惕鼓,需在創(chuàng)建對(duì)象后面顯式添加autorelease
// 通過(guò)類方法創(chuàng)建的對(duì)象不需要顯式添加autorelease筋现,原因是類方法創(chuàng)建的對(duì)象系統(tǒng)會(huì)自動(dòng)添加autorelease
for (int i = 0; i < 1000000; i++) {
  @autoreleasepool {
    NSString *str = @"Abc";
    str = [str lowercaseString];
    str = [str stringByAppendingString:@"xyz"];
    NSLog(@"%@", str);
  } // 出了這里,就會(huì)去遍歷該自動(dòng)釋放池了
}

  1. 簡(jiǎn)述內(nèi)存管理基本原則
> OC內(nèi)存管理遵循`誰(shuí)創(chuàng)建箱歧,誰(shuí)釋放矾飞,誰(shuí)引用,誰(shuí)管理`的機(jī)制呀邢,當(dāng)使用`alloc洒沦、copy(mutableCopy)或者retian`一個(gè)對(duì)象時(shí),你就有義務(wù)向它發(fā)送一條`release或者autorelease`消息釋放該對(duì)象价淌,其他方法創(chuàng)建的對(duì)象申眼,不需要由你來(lái)管理內(nèi)存,當(dāng)對(duì)象引用計(jì)數(shù)為0時(shí)输钩,系統(tǒng)將釋放該對(duì)象豺型,這是OC的手動(dòng)管理機(jī)制(`MRC`)
> 向一個(gè)對(duì)象發(fā)送一條`autorelease`消息,這個(gè)對(duì)象并不會(huì)立即銷毀买乃,而是將這個(gè)對(duì)象放入了自動(dòng)釋放池姻氨,待池子釋放時(shí),它會(huì)向池中每一個(gè)對(duì)象發(fā)送一條`release`消息剪验,以此來(lái)釋放對(duì)象
> 向一個(gè)對(duì)象發(fā)送`release`消息肴焊,并不意味著這個(gè)對(duì)象被銷毀了,而是當(dāng)這個(gè)對(duì)象的引用計(jì)數(shù)為0時(shí)功戚,系統(tǒng)才會(huì)調(diào)用`dealloc`方法釋放該對(duì)象和對(duì)象本身所擁有的實(shí)例

  1. sizeof關(guān)鍵字
> `sizeof`是在編譯階段處理娶眷,且不能被編譯為機(jī)器碼。`sizeof`的結(jié)果等于對(duì)象或類型所占的內(nèi)存字節(jié)數(shù)啸臀。`sizeof`的返回值類型為`size_t`
> 變量:`int a; sizeof(a)`為4届宠;
> 指針:`int *p; sizeof(p)`為4烁落;
> 數(shù)組:`int b[10]; sizeof(b)`為數(shù)組的大小4*10;`int c[0]; sizeof(c)`等于0
> `sizeof(void)`等于1
> `sizeof(void *)`等于4

  1. 什么是離屏渲染豌注?什么情況下會(huì)觸發(fā)伤塌?離屏渲染消耗性能的原因
> 離屏渲染就是在當(dāng)前屏幕緩沖區(qū)以外,新開(kāi)辟一個(gè)緩沖區(qū)進(jìn)行操作
> 離屏渲染觸發(fā)的場(chǎng)景有以下:
> 
> *   圓角(同時(shí)設(shè)置`layer.masksToBounds = YES轧铁、layer.cornerRadius`大于0)
> *   圖層蒙版
> *   陰影每聪,`layer.shadowXXX`,如果設(shè)置了`layer.shadowPath`就不會(huì)產(chǎn)生離屏渲染
> *   遮罩齿风,`layer.mask`
> *   光柵化药薯,`layer.shouldRasterize = YES`

離屏渲染消耗性能的原因
需要?jiǎng)?chuàng)建新的緩沖區(qū),離屏渲染的整個(gè)過(guò)程救斑,需要多次切換上下文環(huán)境童本,先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen)等到離屏渲染結(jié)束以后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上系谐,又需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕

  1. ARC 下巾陕,不顯式指定任何屬性關(guān)鍵字時(shí),默認(rèn)的關(guān)鍵字都有哪些
> 基本數(shù)據(jù)類型默認(rèn)關(guān)鍵字是:`atomic, readwrite, assign`
> 普通`Objective-C`對(duì)象默認(rèn)關(guān)鍵字是:`atomic, readwrite, strong`

  1. OC中的類方法和實(shí)例方法有什么本質(zhì)區(qū)別和聯(lián)系
> 類方法:
> 
> *   類方法是屬于類對(duì)象的
> *   類方法只能通過(guò)類對(duì)象調(diào)用
> *   類方法中的 self 是類對(duì)象
> *   類方法可以調(diào)用其他的類方法
> *   類方法中不能訪問(wèn)成員變量
> *   類方法中不能直接調(diào)用對(duì)象方法

實(shí)例方法:

  • 實(shí)例方法是屬于實(shí)例對(duì)象的
  • 實(shí)例方法只能通過(guò)實(shí)例對(duì)象調(diào)用
  • 實(shí)例方法中的 self 是實(shí)例對(duì)象
  • 實(shí)例方法中可以訪問(wèn)成員變量
  • 實(shí)例方法中直接調(diào)用實(shí)例方法
  • 實(shí)例方法中也可以調(diào)用類方法(通過(guò)類名)
  1. 能否向編譯后得到的類中增加實(shí)例變量纪他?能否向運(yùn)行時(shí)創(chuàng)建的類中添加實(shí)例變量鄙煤?為什么?
> *   不能向編譯后得到的類中增加實(shí)例變量
> *   能向運(yùn)行時(shí)創(chuàng)建的類中添加實(shí)例變量
> *   因?yàn)榫幾g后的類已經(jīng)注冊(cè)在`runtime`中茶袒,類結(jié)構(gòu)體中的`objc_ivar_list`實(shí)例變量的鏈表和`instance_size`實(shí)例變量的內(nèi)存大小已經(jīng)確定梯刚,同時(shí)`runtime`會(huì)調(diào)用`class_setIvarLayout`或`class_setWeakIvarLayout`來(lái)處理`strong weak`引用,所以不能向存在的類中添加實(shí)例變量
>     運(yùn)行時(shí)創(chuàng)建的類是可以添加實(shí)例變量薪寓,調(diào)用`class_addIvar`函數(shù)亡资。但是得在調(diào)用`objc_allocateClassPair`之后,`objc_registerClassPair`之前向叉,原因同上

  1. runtime如何通過(guò)selector找到對(duì)應(yīng)的IMP地址(分別考慮實(shí)例方法和類方法)Selector锥腻、Method 和 IMP的有什么區(qū)別與聯(lián)系
> 對(duì)于實(shí)例方法,每個(gè)實(shí)例的`isa`指針指向著對(duì)應(yīng)類對(duì)象母谎,而每一個(gè)類對(duì)象中都有一個(gè)對(duì)象方法列表瘦黑。對(duì)于類方法,每個(gè)類對(duì)象的`isa`指針都指向著對(duì)應(yīng)的元類對(duì)象奇唤,而每一個(gè)元類對(duì)象中都有一個(gè)類方法列表幸斥。方法列表中記錄著方法的名稱,方法實(shí)現(xiàn)咬扇,以及參數(shù)類型甲葬,其實(shí)`selector`本質(zhì)就是方法名稱,通過(guò)這個(gè)方法名稱就可以在方法列表中找到對(duì)應(yīng)的方法實(shí)現(xiàn)
> `Selector懈贺、Method 和 IMP`的關(guān)系可以這樣描述:在運(yùn)行期分發(fā)消息经窖,方法列表中的每一個(gè)實(shí)體都是一個(gè)方法(`Method`)它的名字叫做選擇器(`SEL`)對(duì)應(yīng)著一種方法實(shí)現(xiàn)(`IMP`)

  1. objc_msgSend坡垫、_objc_msgForward都是做什么的?OC 中的消息調(diào)用流程是怎樣的
> *   `objc_msgSend`是用來(lái)做消息發(fā)送的画侣。在`OC`中葛虐,對(duì)方法的調(diào)用都會(huì)被轉(zhuǎn)換成內(nèi)部的消息發(fā)送執(zhí)行
> *   `_objc_msgForward`是`IMP`類型(函數(shù)指針)用于消息轉(zhuǎn)發(fā)的:當(dāng)向一個(gè)對(duì)象發(fā)送一條消息,但它并沒(méi)有實(shí)現(xiàn)的時(shí)候棉钧,`_objc_msgForward`會(huì)嘗試做消息轉(zhuǎn)發(fā)
> *   在消息調(diào)用的過(guò)程中,`objc_msgSend`的動(dòng)作比較清晰:首先在`Class`中的緩存查找`IMP`(沒(méi)緩存則初始化緩存)如果沒(méi)找到涕蚤,則向父類的`Class`查找宪卿。如果一直查找到根類仍舊沒(méi)有實(shí)現(xiàn),則用`_objc_msgForward`函數(shù)指針代替`IMP`万栅。最后佑钾,執(zhí)行這個(gè)`IMP`。當(dāng)調(diào)用一個(gè)`NSObject`對(duì)象不存在的方法時(shí)烦粒,并不會(huì)馬上拋出異常休溶,而是會(huì)經(jīng)過(guò)多層轉(zhuǎn)發(fā),層層調(diào)用對(duì)象的`-resolveInstanceMethod:扰她、-forwardingTargetForSelector:兽掰、-methodSignatureForSelector:、-forwardInvocation:`等方法徒役。其中最后`-forwardInvocation:`是會(huì)有一個(gè)`NSInvocation`對(duì)象孽尽,這個(gè)`NSInvocation`對(duì)象保存了這個(gè)方法調(diào)用的所有信息,包括`Selector名忧勿,參數(shù)和返回值類型`杉女,可以從這個(gè)`NSInvocation`對(duì)象里拿到調(diào)用的所有參數(shù)值
>     ![image](https://upload-images.jianshu.io/upload_images/1948913-f236e31f679fe683.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

  1. class方法和objc_getClass方法有什么區(qū)別
> `object_getClass(obj)`返回的是`obj`中的`isa`指針,即指向類對(duì)象的指針鸳吸;而`[obj class]`則分兩種情況:一是當(dāng)`obj`為實(shí)例對(duì)象時(shí)熏挎,`[obj class]`中`class`是實(shí)例方法,返回的是`obj`對(duì)象中的`isa`指針晌砾;二是當(dāng)`obj`為類對(duì)象(包括元類和根類以及根元類)時(shí)坎拐,調(diào)用的是類方法,返回的結(jié)果為其本身

  1. OC中向一個(gè)nil對(duì)象發(fā)送消息將會(huì)發(fā)生什么
> 在`OC`中向`nil`發(fā)送消息是完全有效的贡羔,只是在運(yùn)行時(shí)不會(huì)有任何作用廉白;向一個(gè)`nil`對(duì)象發(fā)送消息,首先在尋找對(duì)象的`isa`指針時(shí)就是`0地址`返回了乖寒,所以不會(huì)出現(xiàn)任何錯(cuò)誤猴蹂,也不會(huì)崩潰

  1. _objc_msgForward函數(shù)是做什么的?直接調(diào)用它將會(huì)發(fā)生什么
> `_objc_msgForward`是一個(gè)函數(shù)指針(和`IMP`的類型一樣)用于消息轉(zhuǎn)發(fā)楣嘁;當(dāng)向一個(gè)對(duì)象發(fā)送一條消息磅轻,但它并沒(méi)有實(shí)現(xiàn)的時(shí)候珍逸,`_objc_msgForward`會(huì)嘗試做消息轉(zhuǎn)發(fā)
> `objc_msgSend`在`消息傳遞`中的作用。在`消息傳遞`過(guò)程中聋溜,`objc_msgSend`的動(dòng)作比較清晰:首先在`Class`中的緩存查找`IMP`(`沒(méi)有緩存則初始化緩存`)如果沒(méi)找到谆膳,則向`父類的Class`查找。如果一直查找到`根類`仍舊沒(méi)有實(shí)現(xiàn)撮躁,則用`_objc_msgForward`函數(shù)指針代替`IMP`漱病,最后執(zhí)行這個(gè)`IMP`
> 一旦調(diào)用了`_objc_msgForward`,將跳過(guò)查找`IMP`的過(guò)程把曼,直接觸發(fā)`消息轉(zhuǎn)發(fā)`杨帽,如果調(diào)用了`_objc_msgForward`,即使這個(gè)對(duì)象確實(shí)已經(jīng)實(shí)現(xiàn)了這個(gè)方法嗤军,你也會(huì)告訴`objc_msgSend`注盈,我沒(méi)有在這個(gè)對(duì)象里找到這個(gè)方法的實(shí)現(xiàn),如果用不好會(huì)直接導(dǎo)致程序`Crash`

  1. 什么時(shí)候會(huì)報(bào)unrecognized selector的異常
  • 當(dāng)調(diào)用該對(duì)象上某個(gè)方法叙赚,而該對(duì)象上沒(méi)有實(shí)現(xiàn)這個(gè)方法的時(shí)候老客。可以通過(guò)消息轉(zhuǎn)發(fā)進(jìn)行解決震叮,流程見(jiàn)下圖

    image
  • OC在向一個(gè)對(duì)象發(fā)送消息時(shí)胧砰,runtime庫(kù)會(huì)根據(jù)對(duì)象的isa指針找到該對(duì)象實(shí)際所屬的類,然后在該類中的方法列表以及其父類方法列表中尋找方法運(yùn)行冤荆,如果在最頂層的父類中依然找不到相應(yīng)的方法時(shí)朴则,程序在運(yùn)行時(shí)會(huì)掛掉并拋出異常unrecognized selector sent to XXX
    但是在這之前,OC的運(yùn)行時(shí)會(huì)給出三次拯救程序崩潰的機(jī)會(huì)

  • Method resolution(消息動(dòng)態(tài)解析)
    OC運(yùn)行時(shí)會(huì)調(diào)用+resolveInstanceMethod:或者+resolveClassMethod:钓简,讓你有機(jī)會(huì)提供一個(gè)函數(shù)實(shí)現(xiàn)乌妒。如果你添加了函數(shù),那運(yùn)行時(shí)系統(tǒng)就會(huì)重新啟動(dòng)一次消息發(fā)送的過(guò)程外邓,否則撤蚊,運(yùn)行時(shí)就會(huì)移到下一步,消息轉(zhuǎn)發(fā)(Message Forwarding

// 重寫(xiě) resolveInstanceMethod: 添加對(duì)象方法實(shí)現(xiàn)
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 如果是執(zhí)行 run 函數(shù)损话,就動(dòng)態(tài)解析侦啸,指定新的 IMP
    if (sel == NSSelectorFromString(@"run:")) {
        // class: 給哪個(gè)類添加方法
        // SEL: 添加哪個(gè)方法
        // IMP: 方法實(shí)現(xiàn) => 函數(shù) => 函數(shù)入口 => 函數(shù)名
        // type: 方法類型:void用v來(lái)表示,id參數(shù)用@來(lái)表示丧枪,SEL用:來(lái)表示
        class_addMethod(self, sel, (IMP)runMethod, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//新的 run 函數(shù)
void runMethod(id self, SEL _cmd, NSNumber *meter) {
    NSLog(@"跑了%@", meter);
}

  • Fast forwarding(消息接受者重定向)
    如果目標(biāo)對(duì)象實(shí)現(xiàn)了-forwardingTargetForSelector:光涂,Runtime這時(shí)就會(huì)調(diào)用這個(gè)方法,給你把這個(gè)消息轉(zhuǎn)發(fā)給其他對(duì)象的機(jī)會(huì)拧烦。只要這個(gè)方法返回的不是nilself忘闻,整個(gè)消息發(fā)送的過(guò)程就會(huì)被重啟,當(dāng)然發(fā)送的對(duì)象會(huì)變成你返回的那個(gè)對(duì)象恋博。否則齐佳,就會(huì)繼續(xù)Normal Fowarding私恬。 這里叫Fast,只是為了區(qū)別下一步的轉(zhuǎn)發(fā)機(jī)制炼吴。因?yàn)檫@一步不會(huì)創(chuàng)建任何新的對(duì)象本鸣,但下一步轉(zhuǎn)發(fā)會(huì)創(chuàng)建一個(gè)NSInvocation對(duì)象,所以相對(duì)更快點(diǎn)
// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(run:)) {
        return [[Person alloc] init];
        // 返回 Person 對(duì)象硅蹦,讓 Person 對(duì)象接收這個(gè)消息
    }
    return [super forwardingTargetForSelector:aSelector];
}

  • Normal forwarding(消息重定向)
    這一步是Runtime最后一次給你挽救的機(jī)會(huì)荣德。首先它會(huì)發(fā)送-methodSignatureForSelector:消息獲得函數(shù)的參數(shù)和返回值類型。如果-methodSignatureForSelector:返回nil童芹,Runtime則會(huì)發(fā)出-doesNotRecognizeSelector:消息命爬,程序這時(shí)也就掛掉了。如果返回了一個(gè)函數(shù)簽名辐脖,Runtime就會(huì)創(chuàng)建一個(gè)NSInvocation對(duì)象并發(fā)送-forwardInvocation:消息給目標(biāo)對(duì)象
// 獲取函數(shù)的參數(shù)和返回值類型,返回簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"run:"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 從 anInvocation 中獲取消息
    SEL sel = anInvocation.selector;
    if (sel == NSSelectorFromString(@"run:")) {
        // 1\. 指定當(dāng)前類的一個(gè)方法作為IMP
        // anInvocation.selector = @selector(readBook:);
        // [anInvocation invoke];

        // 2\. 指定其他類來(lái)執(zhí)行這個(gè)IMP
        Person *p = [[Person alloc] init];
        // 判斷 Person 對(duì)象方法是否可以響應(yīng) sel
        if([p respondsToSelector:sel]) {
            // 若可以響應(yīng)皆愉,則將消息轉(zhuǎn)發(fā)給其他對(duì)象處理
            [anInvocation invokeWithTarget:p];
        } else {
            // 若仍然無(wú)法響應(yīng)嗜价,則報(bào)錯(cuò):找不到響應(yīng)方法
            [self doesNotRecognizeSelector:sel];
        }
    }else{
        [super forwardInvocation:anInvocation];
    }
}

- (void)doesNotRecognizeSelector:(SEL)aSelector {
    [super doesNotRecognizeSelector:aSelector];
}

既然-forwardingTargetForSelector:-forwardInvocation:都可以將消息轉(zhuǎn)發(fā)給其他對(duì)象處理,那么兩者的區(qū)別在哪幕庐?
區(qū)別就在于-forwardingTargetForSelector:只能將消息轉(zhuǎn)發(fā)給一個(gè)對(duì)象久锥。而-forwardInvocation:可以把消息存儲(chǔ),在你覺(jué)得合適的時(shí)機(jī)轉(zhuǎn)發(fā)出去异剥,或者不處理這個(gè)消息瑟由。修改消息的target,selector冤寿,參數(shù)等歹苦。將消息轉(zhuǎn)發(fā)給多個(gè)對(duì)象

  1. iOS layoutSubviews什么時(shí)候會(huì)被調(diào)用
> *   `init`方法不會(huì)調(diào)用`layoutSubviews`,但是是用`initWithFrame`進(jìn)行初始化時(shí)督怜,當(dāng)`rect`的值不為`CGRectZero`時(shí)殴瘦,會(huì)觸發(fā)
> *   `addSubview`會(huì)觸發(fā)`layoutSubviews`方法
> *   `setFrame`只有當(dāng)設(shè)置的`frame`的參數(shù)的`size`與原來(lái)的`size`不同,才會(huì)觸發(fā)其`view`的`layoutSubviews`方法
> *   滑動(dòng)`UIScrollView`會(huì)調(diào)用`scrollview`及`scrollview`上的`view`的`layoutSubviews`方法
> *   旋轉(zhuǎn)設(shè)備只會(huì)調(diào)用`VC`的`view`的`layoutSubviews`方法
> *   直接調(diào)用`[self setNeedsLayout];`(這個(gè)在上面蘋(píng)果官方文檔里有說(shuō)明)
>     `-layoutSubviews`方法:這個(gè)方法默認(rèn)沒(méi)有做任何事情号杠,需要子類進(jìn)行重寫(xiě)
>     `-setNeedsLayout`方法:標(biāo)記為需要重新布局蚪腋,異步調(diào)用`layoutIfNeeded`刷新布局,不立即刷新姨蟋,但`layoutSubviews`一定會(huì)被調(diào)用
>     `-layoutIfNeeded`方法:如果有需要刷新的標(biāo)記屉凯,立即調(diào)用`layoutSubviews`進(jìn)行布局(如果沒(méi)有標(biāo)記,不會(huì)調(diào)用`layoutSubviews`)
>     如果要立即刷新眼溶,要先調(diào)用`[view setNeedsLayout]`悠砚,把標(biāo)記設(shè)為需要布局,然后馬上調(diào)用`[view layoutIfNeeded]`偷仿,實(shí)現(xiàn)布局
>     在視圖第一次顯示之前哩簿,標(biāo)記總是`需要刷新`的宵蕉,可以直接調(diào)用`[view layoutIfNeeded]`

  1. 下面代碼會(huì)發(fā)生什么問(wèn)題
@property (nonatomic, strong) NSString *str;

dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
    dispatch_async(queue, ^{
        self.str = [NSString stringWithFormat:@"changzifuchaung:%d",i];
    });
}

會(huì)crash。因?yàn)樵诓⑿嘘?duì)列DISPATCH_QUEUE_CONCURRENT中異步dispatch_async對(duì)str屬性進(jìn)行賦值节榜,就會(huì)導(dǎo)致str已經(jīng)被release了羡玛,還會(huì)執(zhí)行release。這就是向已釋放內(nèi)存的對(duì)象發(fā)送消息而發(fā)生crash
詳細(xì)解析:對(duì)str屬性strong修飾進(jìn)行賦值宗苍,相當(dāng)與MRC中的

- (void)setStr:(NSString *)str{
    if (str == _str) return;
    id pre = _str;
    [str retain];//1.先保留新值
    _str = str;//2.再進(jìn)行賦值
    [pre release];//3.釋放舊值
}

那么假如并發(fā)隊(duì)列里調(diào)度的線程A執(zhí)行到步驟1稼稿,還沒(méi)到步驟2時(shí),線程B執(zhí)行到步驟3讳窟,那么當(dāng)線程A再執(zhí)行步驟3時(shí)让歼,舊值就會(huì)被過(guò)度釋放,導(dǎo)致向已釋放內(nèi)存的對(duì)象發(fā)送消息而崩潰

  • 追問(wèn):怎么修改這段代碼變?yōu)椴槐罎⒛?/p>

    1丽啡、使用串行隊(duì)列
    set方法改成在串行隊(duì)列中執(zhí)行就行谋右,這樣即使異步,但所有block操作追加在隊(duì)列最后依次執(zhí)行
    2补箍、使用atomic
    atomic關(guān)鍵字相當(dāng)于在setter方法加鎖改执,這樣每次執(zhí)行setter都是線程安全的,但這只是單獨(dú)針對(duì)setter方法而言的狹義的線程安全
    3坑雅、使用weak關(guān)鍵字
    weaksetter沒(méi)有保留新值的操作辈挂,所以不會(huì)引發(fā)重復(fù)釋放。當(dāng)然這個(gè)時(shí)候要看具體情況能否使用weak裹粤,可能值并不是所需要的值
    4终蒂、使用互斥鎖,保證數(shù)據(jù)訪問(wèn)的唯一性@synchronized (self) {self.str = [NSString stringWithFormat:@"changzifuchaung:%d",i];}
    5遥诉、使用Tagged Pointer
    Tagged Pointer是蘋(píng)果在64位系統(tǒng)引入的內(nèi)存技術(shù)拇泣。簡(jiǎn)單來(lái)說(shuō)就是對(duì)于NSString(內(nèi)存小于60位的字符串)或NSNumber(小于2^31),64位的指針有8個(gè)字節(jié)矮锈,完全可以直接用這個(gè)空間來(lái)直接表示值挫酿,這樣的話其實(shí)會(huì)將NSStringNSNumber對(duì)象由一個(gè)指針轉(zhuǎn)換成一個(gè)值類型,而值類型的setter和getter又是原子的愕难,從而線程安全

  • 發(fā)散:下面代碼會(huì)crash

@property (nonatomic, strong) NSString *str;

dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
    dispatch_async(queue, ^{
        // 相比上面早龟,僅字符串變短了
        self.str = [NSString stringWithFormat:@"%d",i];
        NSLog(@"%d, %s, %p", i, object_getClassName(self.str), self.str);
    });
}

不會(huì)crash。而且發(fā)現(xiàn)str這個(gè)字符串類型是NSTaggedPointerString
Tagged Pointer是一個(gè)能夠提升性能猫缭、節(jié)省內(nèi)存的有趣的技術(shù)
Tagged Pointer專門(mén)用來(lái)存儲(chǔ)小的對(duì)象葱弟,例如NSNumberNSDate(后來(lái)可以存儲(chǔ)小字符串)
Tagged Pointer指針的值不再是地址了,而是真正的值猜丹。所以芝加,實(shí)際上它不再是一個(gè)對(duì)象了,它只是一個(gè)披著對(duì)象皮的普通變量而已
它的內(nèi)存并不存儲(chǔ)在中,也不需要malloc和free藏杖,所以擁有極快的讀取和創(chuàng)建速度

學(xué)習(xí)的圈子很重要

最后推薦個(gè)我的高級(jí)iOS交流群:1001906160 将塑,有一個(gè)共同的圈子很重要,結(jié)識(shí)人脈蝌麸!里面都是iOS開(kāi)發(fā)点寥,全棧發(fā)展,歡迎入駐来吩,共同進(jìn)步8冶纭(群內(nèi)會(huì)免費(fèi)提供一些群主收藏的免費(fèi)學(xué)習(xí)書(shū)籍資料以及整理好的幾百道面試題和答案文檔!)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末弟疆,一起剝皮案震驚了整個(gè)濱河市戚长,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌怠苔,老刑警劉巖同廉,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異柑司,居然都是意外死亡恤溶,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)帜羊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人鸠天,你說(shuō)我怎么就攤上這事讼育。” “怎么了稠集?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵奶段,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我剥纷,道長(zhǎng)痹籍,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任晦鞋,我火速辦了婚禮蹲缠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘悠垛。我一直安慰自己线定,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布确买。 她就那樣靜靜地躺著斤讥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪湾趾。 梳的紋絲不亂的頭發(fā)上芭商,一...
    開(kāi)封第一講書(shū)人閱讀 52,268評(píng)論 1 309
  • 那天派草,我揣著相機(jī)與錄音,去河邊找鬼铛楣。 笑死近迁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蛉艾。 我是一名探鬼主播钳踊,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼勿侯!你這毒婦竟也來(lái)了拓瞪?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤助琐,失蹤者是張志新(化名)和其女友劉穎祭埂,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體兵钮,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蛆橡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了掘譬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泰演。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖葱轩,靈堂內(nèi)的尸體忽然破棺而出睦焕,到底是詐尸還是另有隱情,我是刑警寧澤靴拱,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布垃喊,位于F島的核電站,受9級(jí)特大地震影響袜炕,放射性物質(zhì)發(fā)生泄漏本谜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一偎窘、第九天 我趴在偏房一處隱蔽的房頂上張望乌助。 院中可真熱鬧,春花似錦陌知、人聲如沸眷茁。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)上祈。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間登刺,已是汗流浹背籽腕。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纸俭,地道東北人皇耗。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像揍很,于是被迫代替她去往敵國(guó)和親郎楼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359