Runtime使用場景及相關面試題解答

1、了解runtime嗎?是什么?
2、你怎么知道的?
3、對象如何找到對應方法去調用的

于是我總結了很多網上被問到的一些關于runtime的題目,并做了詳細的回答,并在后面補充了我在學習runtime時敲的一些代碼,如果想吃透runtime的朋友,可以把后面補充的內容好好看完

一、你會被問到的關于runtime筆試題:

1. runtime怎么添加屬性、方法等
2. runtime 如何實現 weak 屬性
3. runtime如何通過selector找到對應的IMP地址?(分別考慮類方法和實例方法)
4. 使用runtime Associate方法關聯的對象,需要在主對象dealloc的時候釋放么?
5. _objc_msgForward函數是做什么的?直接調用它將會發生什么?
6. 能否向編譯后得到的類中增加實例變量?能否向運行時創建的類中添加實例變量?為什么?
7. 簡述下Objective-C中調用方法的過程(runtime)
8. 什么是method swizzling(俗稱黑魔法)

如果上面的題目你全部答得出來,那就不要浪費時間,直接return吧,程序猿的時間很寶貴的

二、解答

1. runtime怎么添加屬性、方法等
  • ivar表示成員變量

  • class_addIvar

  • class_addMethod

  • class_addProperty

  • class_addProtocol

  • class_replaceProperty

2. runtime 如何實現 weak 屬性

首先要搞清楚weak屬性的特點

weak策略表明該屬性定義了一種“非擁有關系” (nonowning relationship)。為這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign類似;然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)

那么runtime如何實現weak變量的自動置nil?

runtime對注冊的類,會進行布局,會將 weak 對象放入一個 hash 表中。用 weak 指向的對象內存地址作為 key,當此對象的引用計數為0的時候會調用對象的 dealloc 方法,假設 weak 指向的對象內存地址是a,那么就會以a為key,在這個 weak hash表中搜索,找到所有以a為key的 weak 對象,從而設置為 nil。

weak屬性需要在dealloc中置nil么

在ARC環境無論是強指針還是弱指針都無需在 dealloc 設置為 nil , ARC 會自動幫我們處理

即便是編譯器不幫我們做這些,weak也不需要在dealloc中置nil

在屬性所指的對象遭到摧毀時,屬性值也會清空

objc// 模擬下weak的setter方法,大致如下- (void)setObject:(NSObject *)object{ objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN); [object cyl_runAtDealloc:^{ _object = nil; }];}

3. runtime如何通過selector找到對應的IMP地址?(分別考慮類方法和實例方法)
  • 每一個類對象中都一個對象方法列表(對象方法緩存)
  • 類方法列表是存放在類對象中isa指針指向的元類對象中(類方法緩存)

  • 方法列表中每個方法結構體中記錄著方法的名稱,方法實現,以及參數類型,其實selector本質就是方法名稱,通過這個方法名稱就可以在方法列表中找到對應的方法實現.

  • 當我們發送一個消息給一個NSObject對象時,這條消息會在對象的類對象方法列表里查找

  • 當我們發送一個消息給一個類時,這條消息會在類的Meta Class對象的方法列表里查找

4. 使用runtime Associate方法關聯的對象,需要在主對象dealloc的時候釋放么?

無論在MRC下還是ARC下均不需要被關聯的對象在生命周期內要比對象本身釋放的晚很多,它們會在被 NSObject -dealloc 調用的object_dispose()方法中釋放

補充:對象的內存銷毀時間表,分四個步驟

>1、調用 -release :引用計數變為零
* 對象正在被銷毀,生命周期即將結束. 
* 不能再有新的 __weak 弱引用,否則將指向 nil.
* 調用 [self dealloc]
>
2、 父類調用 -dealloc 
* 繼承關系中最直接繼承的父類再調用 -dealloc 
* 如果是 MRC 代碼 則會手動釋放實例變量們(iVars)
* 繼承關系中每一層的父類 都再調用 -dealloc

>3、NSObject 調 -dealloc 
* 只做一件事:調用 Objective-C runtime 中object_dispose() 方法

>4. 調用 object_dispose()
* 為 C++ 的實例變量們(iVars)調用 destructors
* 為 ARC 狀態下的 實例變量們(iVars) 調用 -release 
* 解除所有使用 runtime Associate方法關聯的對象 
* 解除所有 __weak 引用 
* 調用 free()

5. _objc_msgForward函數是做什么的?直接調用它將會發生什么?

_objc_msgForward是IMP類型,用于消息轉發的:當向一個對象發送一條消息,但它并沒有實現的時候,_objc_msgForward會嘗試做消息轉發

直接調用_objc_msgForward是非常危險

的事,這是把雙刃刀,如果用不好會直接導致程序Crash,但是如果用得好,能做很多非常酷的事

JSPatch就是直接調用_objc_msgForward來實現其核心功能的

詳細解說參見這里的第一個問題解答

6. 能否向編譯后得到的類中增加實例變量?能否向運行時創建的類中添加實例變量?為什么?
  • 不能向編譯后得到的類中增加實例變量;
  • 能向運行時創建的類中添加實例變量;

  • 分析如下:

    • 因為編譯后的類已經注冊在runtime中,類結構體中的objc_ivar_list 實例變量的鏈表和instance_size實例變量的內存大小已經確定,同時runtime 會調用class_setIvarLayout 或 class_setWeakIvarLayout來處理strong weak引用,所以不能向存在的類中添加實例變量

    • 運行時創建的類是可以添加實例變量,調用 class_addIvar函數,但是得在調用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。

7. 簡述下Objective-C中調用方法的過程(runtime)
  • Objective-C是動態語言,每個方法在運行時會被動態轉為消息發送,即:objc_msgSend(receiver, selector),整個過程介紹如下:
* objc在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象實際所屬的類
* 然后在該類中的方法列表以及其父類方法列表中尋找方法運行
* 如果,在最頂層的父類(一般也就NSObject)中依然找不到相應的方法時,程序在運行時會掛掉并拋出異常unrecognized selector sent to XXX
* 但是在這之前,objc的運行時會給出三次拯救程序崩潰的機會,這三次拯救程序奔潰的說明見問題《什么時候會報unrecognized selector的異常》中的說明

  • 補充說明:Runtime 鑄就了Objective-C 是動態語言的特性,使得C語言具備了面向對象的特性,在程序運行期創建,檢查,修改類、對象及其對應的方法,這些操作都可以使用runtime中的對應方法實現。
8. 什么是method swizzling(俗稱黑魔法)
  • 簡單說就是進行方法交換
  • 在Objective-C中調用一個方法,其實是向一個對象發送消息,查找消息的唯一依據是selector的名字。利用Objective-C的動態特性,可以實現在運行時偷換selector對應的方法實現,達到給方法掛鉤的目的

  • 每個類都有一個方法列表,存放著方法的名字和方法實現的映射關系,selector的本質其實就是方法名,IMP有點類似函數指針,指向具體的Method實現,通過selector就可以找到對應的IMP

[圖片上傳失敗...(image-1c1c5f-1515728553053)]

  • 交換方法的幾種實現方式

    • 利用 method_exchangeImplementations 交換兩個方法的實現

    • 利用 class_replaceMethod 替換方法的實現

    • 利用 method_setImplementation 來直接設置某個方法的IMP

      [圖片上傳失敗...(image-d0dcc9-1515728553053)]

三、補充(重要)

1、消息機制

  • 1、方法調用底層實現

    [圖片上傳失敗...(image-1a2231-1515728553053)]

  • 2、runtime:千萬不要隨便使用,不得已才使用

//消息機制:

//作用:調用已知私有方法,如調用沒有在.h文件申明但是在.m文件實現了的方法

// 用runtime調用私有方法:方法編號后面開始,依次就是傳入給方法的參數

objc_msgSend(p, @selector(run: str:),20,@"haha");
objc_msgSend(p, @selector(eat));
// [p eat] => objc_msgSend(p, @selector(eat));

  • 3、對象如何找到對應的方法去調用

    // 方法保存到什么地方?對象方法保存到類中,類方法保存到元類(meta class),每一個類都有方法列表methodList

    //明確去哪個類中調用,通過isa指針

    • 1.根據對象的isa去對應的類查找方法,isa:判斷去哪個類查找對應的方法 指向方法調用的類

    • 2.根據傳入的方法編號SEL,里面有個哈希列表,在列表中找到對應方法Method(方法名)

    • 3.根據方法名(函數入口)找到函數實現,函數實現在方法區

2、交換方法

  • 1、需求:比如我有個項目,已經開發2年,之前都是使用UIImage去加載圖片,組長想要在調用imageNamed,就給我提示,是否加載成功,如果用方法2,每個調用imageNamed方法的,都要改成xmg_imageNamed:才能擁有這個功能,很麻煩。解決:用runtime交換方法,即下面方法3
   ①解決方式 自定義UIImage類,缺點:每次用要導入自己的類
   ②解決方法:UIImage分類擴充一個這樣方法,缺點:需要導入,無法寫super和self,會干掉系統方法,解決:給系統方法加個前綴,與系統方法區分,如:xmg_imageNamed:
   ③交互方法實現,步驟: 1.提供分類 2.寫一個有這樣功能方法 3.用系統方法與這個功能方法交互實現,在+load方法中實現

注意:在分類一定不要重寫系統方法,就直接把系統方法干掉,如果真的想重寫,在系統方法前面加前綴,方法里面去調用系統方法

思想:什么時候需要自定義,系統功能不完善,就自定義一個這樣類,去擴展這個類

/#import "UIImage+Image.h"
/#import <objc/message.h>
@implementation UIImage (Image)
// 加載類的時候調用,肯定只會調用一次

 +(void)load
{
    // 交互方法實現xmg_imageNamed,imageNamed
    /**
     獲取類方法名
     @param Class cls,#> 獲取哪個類方法 description#>
     @param SEL name#> 方法編號 description#>
     @return 返回Method(方法名)
     class_getClassMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
     */
    /**
     獲取對象方法名
     @param Class cls,#> 獲取哪個對象方法 description#>
     @param SEL name#> 方法編號 description#>
     @return 返回Method(方法名)
     class_getInstanceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>)
     */

   Method imageNameMethod = class_getClassMethod(self, @selector(imageNamed:));
    Method xmg_imageNameMethod = class_getClassMethod(self, @selector(xmg_imageNamed:));
    //用runtime對imageNameMethod和xmg_imageNameMethod方法進行交換
    method_exchangeImplementations(imageNameMethod, xmg_imageNameMethod);
}
//外界調用imageNamed:方法,其實是調用下面方法,調用xmg_imageNamed就是調用imageNamed:
+ (UIImage *)xmg_imageNamed:(NSString *)name
{
    //已經把xmg_imageNamed換成imageNamed,所以下面其實是調用的imageNamed:
   UIImage *image = [UIImage xmg_imageNamed:name];

    if (image == nil) {
        NSLog(@"加載失敗");
    }
    return image;
}
@end

3、動態添加方法

動態添加方法:

為什么動態添加方法? OC都是懶加載,有些方法可能很久不會調用

應用場景:電商,視頻,社交,收費項目:會員機制中,只要會員才擁有這些功能

  • 1、美團面試題:有沒有使用過performSelector,使用,什么時候使用?動態添加方法的時候使用? 為什么動態添加方法
// 默認OC方法都有兩個默認存在的隱式參數,self(哪個類的方法),_cmd(方法編號)
void run(id self, SEL _cmd, NSNumber *metre) {
    NSLog(@"跑了%@",metre);
}

  • 2、什么時候調用:只要調用沒有實現的方法 就會調用方法去解決,這里可以拿到那個未實現的方法名
// 作用:去解決沒有實現方法,動態添加方法
+(BOOL)resolveInstanceMethod:(SEL)sel{
        class:給誰添加方法
        SEL:添加哪個方法
        IMP:方法實現,函數入口,函數名,如:(IMP)run,方法名run強轉成IMP
        type:方法類型,通過查蘋果官方文檔,V:void,
     class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)
    // [NSStringFromSelector(sel) isEqualToString:@"eat"];
    if (sel == @selector(run:)) {
        // 添加方法
        class_addMethod(self, sel, (IMP)run,"v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

4、動態添加屬性

  • 1、需求:給NSObject添加一個name屬性,動態添加屬性 -> runtime

思考:

①給NSObject添加分類,在分類中添加屬性。問題:@property在分類中作用:僅僅是生成get,set方法聲明,不會生成get,set方法實現和下劃線成員屬性,所以要在.m文件實現setter/getter方法,用static保存下滑線屬性,這樣一來,當對象銷毀時,屬性無法銷毀

②用runtime動態添加屬性:本質是讓屬性與某個對象產生一段關聯

使用場景:給系統的類添加屬性時

#import <objc/message.h>
@implementation NSObject (Property)
//static  NSString *_name;      //通過這樣去保存屬性沒法做到對象銷毀,屬性也銷毀,static依然會讓屬性存在緩存池中,所以需要動態的添加成員屬性
// 只要想調用runtime方法,思考:誰的事情
-(void)setName:(NSString *)name
{
    // 保存name
    // 動態添加屬性 = 本質:讓對象的某個屬性與值產生關聯
    /*
        object:保存到哪個對象中 
        key:用什么屬性保存 屬性名
        value:保存值
        policy:策略,strong,weak
     objc_setAssociatedObject(<#id object#>, <#const void *key#>, <#id value#>, <#objc_AssociationPolicy policy#>)
     */
    objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//    _name = name;

}

- (NSString *)name
{
    return objc_getAssociatedObject(self, "name");
//    return _name;

}
@end

5、自動生成屬性代碼

開發中,從網絡數據中解析出字典數組,將數組轉為模型時,如果有幾百個key需要用,要寫很多@property成員屬性,下面提供一個萬能的方法,可直接將字典數組轉為全部@property成員屬性,打印出來,這樣直接復制在模型中就好了

#import "NSDictionary+PropertyCode.h"
@implementation NSDictionary (PropertyCode)

//1??通過這個方法,自動將字典轉成模型中需要用的屬性代碼

// 私有API:真實存在,但是蘋果沒有暴露出來,不給你。如BOOL值,不知道類型,打印得知是__NSCFBoolean,但是無法敲出來,只能用NSClassFromString(@"__NSCFBoolean")

// isKindOfClass:判斷下是否是當前類或者子類,BOOL是NSNumber的子類,要先判斷BOOL
- (void)createPropetyCode
{
    // 模型中屬性根據字典的key
    // 有多少個key,生成多少個屬性
    NSMutableString *codes = [NSMutableString string];
    // 遍歷字典
    [self enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull value, BOOL * _Nonnull stop) {
        NSString *code = nil;

//        NSLog(@"%@",[value class]);

        if ([value isKindOfClass:[NSString class]]) {
          code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSString *%@;",key];
        } else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")]){
            code = [NSString stringWithFormat:@"@property (nonatomic, assign) BOOL %@;",key];
        } else if ([value isKindOfClass:[NSNumber class]]) {
             code = [NSString stringWithFormat:@"@property (nonatomic, assign) NSInteger %@;",key];
        } else if ([value isKindOfClass:[NSArray class]]) {
            code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSArray *%@;",key];
        } else if ([value isKindOfClass:[NSDictionary class]]) {
            code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSDictionary *%@;",key];
        }

        // 拼接字符串
        [codes appendFormat:@"%@\n",code];

    }];

    NSLog(@"%@",codes);

}

@end

6、KVC字典轉模型

// 需求:就是在開發中,通常后臺會給你很多數據,但是并不是每個數據都有用,這些沒有用的數據,需不需要保存到模型中

@implementation Status
+ (instancetype)statusWithDict:(NSDictionary *)dict{
    // 創建模型
    Status *s = [[self alloc] init];

    // 字典value轉模型屬性保存
    [s setValuesForKeysWithDictionary:dict];

//    s.reposts_count = dict[@"reposts_count"];
    // 4??MJExtension:可以字典轉模型,而且可以不用與字典中屬性一一對應,runtime,遍歷模型中有多少個屬性,直接去字典中取出對應value,給模型賦值

    // 1??setValuesForKeysWithDictionary:方法底層實現:遍歷字典中所有key,去模型中查找對應的屬性,把值給模型屬性賦值,即調用下面方法:
    /*
    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        // source
        // 這行代碼才是真正給模型的屬性賦值
        [s setValue:dict[@"source"] forKey:@"source"];
     //底層實現是:
       2?? [s setValue:dict[@"source"] forKey:@"source"];
              1.首先會去模型中查找有沒有setSource方法,直接調用set方法 [s setSource:dict[@"source"]];
              2.去模型中查找有沒有source屬性,source = dict[@"source"]
              3.去米線中查找有沒有_source屬性,_source = dict[@"source"]
              4.調用對象的 setValue:forUndefinedKey:直接報錯
              [s setValue:obj forKey:key];
    }];
    */
    return s;
}

// 3??用KVC,不想讓系統報錯,重寫系統方法思想:
// 1.想給系統方法添加功能
// 2.不想要系統實現
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
}

@end

7、MJExtention的底層實現

#import "NSObject+Model.h"
#import <objc/message.h>

//    class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>) 獲取屬性列表

@implementation NSObject (Model)

/**
 字典轉模型
 @param dict 傳入需要轉模型的字典
 @return 賦值好的模型
 */

+ (instancetype)modelWithDict:(NSDictionary *)dict

{
    id objc = [[self alloc] init];

    //思路: runtime遍歷模型中屬性,去字典中取出對應value,在給模型中屬性賦值
    // 1.獲取模型中所有屬性 -> 保存到類
    // ivar:下劃線成員變量 和 Property:屬性
    // 獲取成員變量列表
    // class:獲取哪個類成員變量列表
    // count:成員變量總數
    //這個方法得到一個裝有成員變量的數組
    //class_copyIvarList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)

    int count = 0;
    // 成員變量數組 指向數組第0個元素
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍歷所有成員變量
    for (int i = 0; i < count; i++) {

        // 獲取成員變量 user
        Ivar ivar = ivarList[i];
        // 獲取成員變量名稱,即將C語言的字符轉為OC字符串
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 獲取成員變量類型,用于獲取二級字典的模型名字
        NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

        //  將type這樣的字符串@"@\"User\"" 轉成 @"User"
        type = [type stringByReplacingOccurrencesOfString:@"@\"" withString:@""];
        type = [type stringByReplacingOccurrencesOfString:@"\"" withString:@""];

        // 成員變量名稱轉換key,即去掉成員變量前面的下劃線
        NSString *key = [ivarName substringFromIndex:1];

        // 從字典中取出對應value dict[@"user"] -> 字典
        id value = dict[key];

        // 二級轉換
        // 并且是自定義類型,才需要轉換
        if ([value isKindOfClass:[NSDictionary class]] && ![type containsString:@"NS"]) { // 只有是字典才需要轉換

            Class className = NSClassFromString(type);

            // 字典轉模型
            value = [className modelWithDict:value];
        }

        // 給模型中屬性賦值 key:user value:字典 -> 模型
        if (value) {
            [objc setValue:value forKey:key];
        }

    }

    return objc;

}

@end

轉載自:http://www.lxweimin.com/p/8345a79fd572

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370