weex 源碼理解

源碼

初始化環(huán)境 [WXSDKEngine initSDKEnvironment] 做了哪些?

//有篩減
+ (void)initSDKEnvironment
{
//加載本地的一個js文件,并且以參數(shù)的形式 傳給sdk
    NSString *fileName = @"weex-main-jsfm";
    NSString *filePath = [[NSBundle bundleForClass:self] pathForResource:fileName ofType:@"js"];
    if (filePath == nil) {
        filePath = [[NSBundle mainBundle] pathForResource:fileName ofType:@"js"];
    }
    NSString *script = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
    [WXSDKEngine initSDKEnvironment:script];
...//(配置模擬器相關代碼)
    }

[WXSDKEngine initSDKEnvironment:script]; 做了什么?

+ (void)initSDKEnvironment:(NSString *)script
{
    WX_MONITOR_PERF_START(WXPTInitalize)
    WX_MONITOR_PERF_START(WXPTInitalizeSync)
    
    if (!script || script.length <= 0) {
        NSMutableString *errMsg = [NSMutableString stringWithFormat:@"[WX_KEY_EXCEPTION_SDK_INIT_JSFM_INIT_FAILED] script don't exist:%@",script];
        [WXExceptionUtils commitCriticalExceptionRT:@"WX_KEY_EXCEPTION_SDK_INIT" errCode:[NSString stringWithFormat:@"%d", WX_KEY_EXCEPTION_SDK_INIT] function:@"initSDKEnvironment" exception:errMsg extParams:nil];
        WX_MONITOR_FAIL(WXMTJSFramework, WX_ERR_JSFRAMEWORK_LOAD, errMsg);
        return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self registerDefaults];//注冊內容
        [[WXSDKManager bridgeMgr] executeJsFramework:script];//執(zhí)行JSFramwork
    });
    
    WX_MONITOR_PERF_END(WXPTInitalizeSync)
    
}

 WX_MONITOR_FAIL(WXMTJSFramework, WX_ERR_JSFRAMEWORK_LOAD, errMsg);
從這句話可以看出來  JSFramwork  指的是 這個本地weex-main-jsfm.js 文件

registerDefaults 注冊了那些東西

+ (void)registerDefaults
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self _registerDefaultComponents];//默認的控件
        [self _registerDefaultModules];//默認模塊
        [self _registerDefaultHandlers];//默認的handler
    });
}

_registerDefaultComponents 具體注冊了什么

// register some default components when the engine initializes.
+ (void)_registerDefaultComponents
{
    [self registerComponent:@"container" withClass:NSClassFromString(@"WXDivComponent") withProperties:nil];
    [self registerComponent:@"div" withClass:NSClassFromString(@"WXComponent") withProperties:nil];
    [self registerComponent:@"text" withClass:NSClassFromString(@"WXTextComponent") withProperties:nil];
    [self registerComponent:@"image" withClass:NSClassFromString(@"WXImageComponent") withProperties:nil];
    [self registerComponent:@"richtext" withClass:NSClassFromString(@"WXRichText") withProperties:nil];
    
    [self registerComponent:@"scroller" withClass:NSClassFromString(@"WXScrollerComponent") withProperties:nil];
    [self registerComponent:@"list" withClass:NSClassFromString(@"WXListComponent") withProperties:nil];
    [self registerComponent:@"recycler" withClass:NSClassFromString(@"WXRecyclerComponent") withProperties:nil];
    [self registerComponent:@"waterfall" withClass:NSClassFromString(@"WXRecyclerComponent") withProperties:nil];
    
    [self registerComponent:@"header" withClass:NSClassFromString(@"WXHeaderComponent")];
    [self registerComponent:@"cell" withClass:NSClassFromString(@"WXCellComponent")];
    [self registerComponent:@"embed" withClass:NSClassFromString(@"WXEmbedComponent")];
    [self registerComponent:@"a" withClass:NSClassFromString(@"WXAComponent")];
    
    [self registerComponent:@"select" withClass:NSClassFromString(@"WXSelectComponent")];
    [self registerComponent:@"switch" withClass:NSClassFromString(@"WXSwitchComponent")];
    [self registerComponent:@"input" withClass:NSClassFromString(@"WXTextInputComponent")];
    [self registerComponent:@"video" withClass:NSClassFromString(@"WXVideoComponent")];
    [self registerComponent:@"indicator" withClass:NSClassFromString(@"WXIndicatorComponent")];
    [self registerComponent:@"slider" withClass:NSClassFromString(@"WXCycleSliderComponent")];
    [self registerComponent:@"cycleslider" withClass:NSClassFromString(@"WXCycleSliderComponent")];
    [self registerComponent:@"web" withClass:NSClassFromString(@"WXWebComponent")];
    [self registerComponent:@"loading" withClass:NSClassFromString(@"WXLoadingComponent")];
    [self registerComponent:@"loading-indicator" withClass:NSClassFromString(@"WXLoadingIndicator")];
    [self registerComponent:@"refresh" withClass:NSClassFromString(@"WXRefreshComponent")];
    [self registerComponent:@"textarea" withClass:NSClassFromString(@"WXTextAreaComponent")];
    [self registerComponent:@"canvas" withClass:NSClassFromString(@"WXCanvasComponent")];
    [self registerComponent:@"slider-neighbor" withClass:NSClassFromString(@"WXSliderNeighborComponent")];
    
    [self registerComponent:@"recycle-list" withClass:NSClassFromString(@"WXRecycleListComponent")];
    [self registerComponent:@"cell-slot" withClass:NSClassFromString(@"WXCellSlotComponent") withProperties: @{@"append":@"tree", @"isTemplate":@YES}];
    
    // other non-default components should be checked with affine-base types.
    [self _registerAffineTypes];
}
把所有默認的組件全部注冊進去

分析div [self registerComponent:@"div" withClass:NSClassFromString(@"WXComponent") withProperties:nil];

+ (void)registerComponent:(NSString *)name withClass:(Class)clazz withProperties:(NSDictionary *)properties
{
    if (!name || !clazz) {
        return;
    }

    WXAssert(name && clazz, @"Fail to register the component, please check if the parameters are correct !");
    //在工廠類里面 注冊
    [WXComponentFactory registerComponent:name withClass:clazz withPros:properties];
    NSMutableDictionary *dict = [WXComponentFactory componentMethodMapsWithName:name];
    dict[@"type"] = name;
    if (properties) {
        NSMutableDictionary *props = [properties mutableCopy];
        if ([dict[@"methods"] count]) {
            [props addEntriesFromDictionary:dict];
        }
        [[WXSDKManager bridgeMgr] registerComponents:@[props]];
    } else {
        [[WXSDKManager bridgeMgr] registerComponents:@[dict]];
    }
}

工廠方法做了哪些?

[WXComponentFactory registerComponent:name withClass:clazz withPros:properties];

- (void)registerComponent:(NSString *)name withClass:(Class)clazz withPros:(NSDictionary *)pros
{
    WXAssert(name && clazz, @"name or clazz must not be nil for registering component.");
    
    WXComponentConfig *config = nil;
    [_configLock lock];
    config = [_componentConfigs objectForKey:name];
    
    if(config){
        WXLogInfo(@"Overrider component name:%@ class:%@, to name:%@ class:%@",
                  config.name, config.class, name, clazz);
    }
    
    config = [[WXComponentConfig alloc] initWithName:name class:NSStringFromClass(clazz) pros:pros];
//WXComponentConfig  只保存了pros
    [_componentConfigs setValue:config forKey:name];
//存入到工廠方法中的字典中
    [config registerMethods];
//通過while循環(huán)  將類里面所有的方法 分為兩類 _syncMethods  _asyncMethods  放到這個兩個集合里面使用
    [self registerAffineType:name withClass:clazz];
    [_configLock unlock];
}

[config registerMethods]; 做了那些

- (void)registerMethods
{
    Class currentClass = NSClassFromString(_clazz);
    
    if (!currentClass) {
        WXLogWarning(@"The module class [%@] doesn't exit毛秘!", _clazz);
        return;
    }
    
    while (currentClass != [NSObject class]) {
        unsigned int methodCount = 0;
        Method *methodList = class_copyMethodList(object_getClass(currentClass), &methodCount);
        for (unsigned int i = 0; i < methodCount; i++) {
            NSString *selStr = [NSString stringWithCString:sel_getName(method_getName(methodList[i])) encoding:NSUTF8StringEncoding];
            BOOL isSyncMethod = NO;
            if ([selStr hasPrefix:@"wx_export_method_sync_"]) {
                isSyncMethod = YES;
            } else if ([selStr hasPrefix:@"wx_export_method_"]) {
                isSyncMethod = NO;
            } else {
                continue;
            }
            
            NSString *name = nil, *method = nil;
            SEL selector = NSSelectorFromString(selStr);
            if ([currentClass respondsToSelector:selector]) {
                method = ((NSString* (*)(id, SEL))[currentClass methodForSelector:selector])(currentClass, selector);
            }
            
            if (method.length <= 0) {
                WXLogWarning(@"The module class [%@] doesn't has any method饭寺!", _clazz);
                continue;
            }
            
            NSRange range = [method rangeOfString:@":"];
            if (range.location != NSNotFound) {
                name = [method substringToIndex:range.location];
            } else {
                name = method;
            }
            
            NSMutableDictionary *methods = isSyncMethod ? _syncMethods : _asyncMethods;
            [methods setObject:method forKey:name];
        }
        
        free(methodList);
        currentClass = class_getSuperclass(currentClass);
    }
    
}

通過while循環(huán)  將類里面所有的方法 分為兩類 _syncMethods  _asyncMethods  放到這個兩個集合里面使用

從渲染的方向來看

 self.view.backgroundColor = [UIColor whiteColor];
     _instance = [[WXSDKInstance alloc] init];
    _instance.viewController = self;
    _instance.frame = CGRectMake(0, 100, ScreenWidth, SCREEN_HEIGHT - 100);
    __weak WeexViewController * weakSelf = self;
    _instance.onCreate = ^(UIView *view) {
        [weakSelf.weexView removeFromSuperview];
        weakSelf.weexView = view;
        [weakSelf.view addSubview:weakSelf.weexView];
    };
   NSDictionary *options = @{@"url":@"http://pic26.nipic.com/20121221/9252150_142515375000_2.jpg"};
//整個渲染的入口
    [self.instance renderWithURL:[[NSBundle mainBundle]URLForResource:@"index" withExtension:@"js"] options:options data:nil];//他具體做了什么呢

[self.instance renderWithURL:[[NSBundle mainBundle]URLForResource:@"index" withExtension:@"js"] options:options data:nil]; 主要做了哪些

- (void)renderWithURL:(NSURL *)url options:(NSDictionary *)options data:(id)data
{
    if (!url) {
        WXLogError(@"Url must be passed if you use renderWithURL");
        return;
    }

    _scriptURL = url;
    [self _checkPageName];
    [self.apmInstance startRecord:self.instanceId];
    self.apmInstance.isStartRender = YES;
    
    self.needValidate = [[WXHandlerFactory handlerForProtocol:@protocol(WXValidateProtocol)] needValidate:url];
    WXResourceRequest *request = [WXResourceRequest requestWithURL:url resourceType:WXResourceTypeMainBundle referrer:@"" cachePolicy:NSURLRequestUseProtocolCachePolicy];
   * [self _renderWithRequest:request options:options data:data];//做請求*

    NSURL* nsURL = [NSURL URLWithString:options[@"DATA_RENDER_JS"]];
    [self _downloadAndExecScript:nsURL];
}

[self _renderWithRequest:request options:options data:data];//做請求*

- (void)_renderWithRequest:(WXResourceRequest *)request options:(NSDictionary *)options data:(id)data;
{
/*
省略一坨代碼
*/
//核心就是下載下來了js 去渲染
 [strongSelf _renderWithMainBundleString:jsBundleString];
}

[strongSelf _renderWithMainBundleString:jsBundleString];
渲染的第一步

- (void)_renderWithMainBundleString:(NSString *)mainBundleString
{
/*
省略一坨代碼
*/
//生成容器
 WXPerformBlockOnMainThread(^{
        _rootView = [[WXRootView alloc] initWithFrame:self.frame];
        _rootView.instance = self;
        if(self.onCreate) {
            self.onCreate(_rootView);
        }
    });

 // 確保全部注冊
    [WXSDKEngine registerDefaults];
//將數(shù)據(jù)交給 JS執(zhí)行環(huán)境
 [[WXSDKManager bridgeMgr] createInstance:self.instanceId template:mainBundleString options:dictionary data:_jsData];
}
- (void)createInstance:(NSString *)instanceIdString
              template:(NSString *)jsBundleString
               options:(NSDictionary *)options
                  data:(id)data
{
//獲取JSFramwork 的上下文環(huán)境
[self callJSMethod:@"createInstanceContext" args:@[instanceIdString, newOptions, data?:@[]] onContext:nil completion:^(JSValue *instanceContextEnvironment) {

......
//執(zhí)行index.js
[sdkInstance.instanceJavaScriptContext executeJavascript:jsBundleString];
.....
              }
}

到此 為止 就是 原生與JS的通信

最初這句代碼時 他的內部還執(zhí)行了 注冊JS調用原生的方法
[[WXSDKManager bridgeMgr] executeJsFramework:script];//執(zhí)行JSFramwork
這些都是注冊可調用的原生方法

/**
 * Register callback when call __updateComponentData tasks occur. only use for data render
 */
- (void)registerCallUpdateComponentData:(WXJSCallUpdateComponentData)callUpdateComponentData;

/**
 * Register callback when call native tasks occur
 */
- (void)registerCallNative:(WXJSCallNative)callNative;

/**
 * Register callback when addElement tasks occur
 */
- (void)registerCallAddElement:(WXJSCallAddElement)callAddElement;

/**
 * Register callback when createBody tasks occur
 */
- (void)registerCallCreateBody:(WXJSCallCreateBody)callCreateBody;

/**
 * Register callback when removeElement tasks occur
 */
- (void)registerCallRemoveElement:(WXJSCallRemoveElement)callRemoveElement;

/**
 * Register callback when removeElement tasks occur
 */
- (void)registerCallMoveElement:(WXJSCallMoveElement)callMoveElement;

/**
 * Register callback when updateAttrs tasks occur
 */
- (void)registerCallUpdateAttrs:(WXJSCallUpdateAttrs)callUpdateAttrs;

/**
 * Register callback when updateStyle tasks occur
 */
- (void)registerCallUpdateStyle:(WXJSCallUpdateStyle)callUpdateStyle;

/**
 * Register callback when addEvent tasks occur
 */
- (void)registerCallAddEvent:(WXJSCallAddEvent)callAddEvent;

/**
 * Register callback when removeEvent tasks occur
 */
- (void)registerCallRemoveEvent:(WXJSCallRemoveEvent)callRemoveEvent;

/**
 * Register callback when createFinish tasks occur
*/
- (void)registerCallCreateFinish:(WXJSCallCreateFinish)callCreateFinish;

/**
 * Register callback for global js function `callNativeModule`
 */
- (void)registerCallNativeModule:(WXJSCallNativeModule)callNativeModuleBlock;

/**
 * Register callback for global js function `callNativeComponent`
 */
- (void)registerCallNativeComponent:(WXJSCallNativeComponent)callNativeComponentBlock;

注冊流程

- (void)registerCallCreateBody:(WXJSCallCreateBody)callCreateBody
{
//生明一個代碼塊 用來相應JS調用的方法
    id WXJSCallCreateBodyBlock = ^(JSValue *instanceId, JSValue *body,JSValue *ifCallback) {
        
        NSString *instanceIdString = [instanceId toString];
        NSDictionary *bodyData = [body toDictionary];
        
        WXLogDebug(@"callCreateBody...%@, %@,", instanceIdString, bodyData);
        return [JSValue valueWithInt32:(int32_t)callCreateBody(instanceIdString, bodyData) inContext:[JSContext currentContext]];
    };
    //把方法注冊給JS
    _jsContext[@"callCreateBody"] = WXJSCallCreateBodyBlock;
}

處理回調回來的數(shù)據(jù)

  [_jsBridge registerCallCreateBody:^NSInteger(NSString *instanceId, NSDictionary *bodyData) {
        
        WXPerformBlockOnComponentThread(^{
            [WXCoreBridge callCreateBody:instanceId data:bodyData];
        });
        
        return 0;
    }];

[WXCoreBridge callCreateBody:instanceId data:bodyData];
對傳回來的數(shù)據(jù)解析 得出 樣式 事件

 NSDictionary* styles = data[@"style"];
        [self _parseStyleBeforehand:styles key:@"margin" render:render];
        [self _parseStyleBeforehand:styles key:@"padding" render:render];
        [self _parseStyleBeforehand:styles key:@"borderWidth" render:render];
        [styles enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            if ([key isEqualToString:@"margin"] || [key isEqualToString:@"padding"] || [key isEqualToString:@"borderWidth"]) {
                return;
            }
            ConvertToCString(obj, ^(const char * value) {
                if (value != nullptr) {
                    render->AddStyle([key UTF8String], value);
                }
            });
        }];
        
        for (id obj in data[@"event"]) {
            ConvertToCString(obj, ^(const char * value) {
                if (value != nullptr) {
                    render->AddEvent(value);
                }
            });
        }
        
        int childIndex = 0;
        for (NSDictionary* obj in data[@"children"]) {
            [self _parseRenderObject:obj parent:render index:childIndex ++ pageId:pageId];
        }
+ (void)callMoveElement:(NSString*)pageId ref:(NSString*)ref parentRef:(NSString*)parentRef index:(int)index
{
    WeexCore::WeexCoreManager::Instance()->script_bridge()->core_side()->MoveElement([pageId UTF8String] ?: "", [ref UTF8String] ?: "", [parentRef UTF8String] ?: "", index);
}

+ (void)callUpdateAttrs:(NSString*)pageId ref:(NSString*)ref data:(NSDictionary*)data
{
    WeexCore::RenderManager::GetInstance()->UpdateAttr([pageId UTF8String] ?: "", [ref UTF8String] ?: "", [self _parseMapValuePairs:data]);
}

+ (void)callUpdateStyle:(NSString*)pageId ref:(NSString*)ref data:(NSDictionary*)data
{
    WeexCore::RenderManager::GetInstance()->UpdateStyle([pageId UTF8String] ?: "", [ref UTF8String] ?: "", [self _parseMapValuePairs:data]);
}

+ (void)callAddEvent:(NSString*)pageId ref:(NSString*)ref event:(NSString*)event
{
    WeexCore::WeexCoreManager::Instance()->script_bridge()->core_side()->AddEvent([pageId UTF8String] ?: "", [ref UTF8String] ?: "", [event UTF8String] ?: "");
}

+ (void)callRemoveEvent:(NSString*)pageId ref:(NSString*)ref event:(NSString*)event
{
    WeexCore::WeexCoreManager::Instance()->script_bridge()->core_side()->RemoveEvent([pageId UTF8String] ?: "", [ref UTF8String] ?: "", [event UTF8String] ?: "");
}
+ (void)callUpdateFinish:(NSString*)pageId
{
    WeexCore::WeexCoreManager::Instance()->script_bridge()->core_side()->UpdateFinish([pageId UTF8String] ?: "", nullptr, 0, nullptr, 0);
}

 int IOSSide::UpdateFinish(const char* page_id, const char* task, int taskLen,
                                   const char* callback, int callbackLen)
    {
        RenderPage *page = RenderManager::GetInstance()->GetPage(page_id);
        if (page == nullptr) {
            return -1;
        }
        
        NSString* ns_instanceId = NSSTRING(page_id);
//到這里開始給 空間開始處理
        WXComponentManager* manager = [WXSDKManager instanceForID:ns_instanceId].componentManager;
        if (!manager.isValid) {
            return -1;
        }
        [manager startComponentTasks];
        [manager updateFinish];

        return 0;
    }

總結一下


整體流程
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市熔脂,隨后出現(xiàn)的幾起案子佩研,更是在濱河造成了極大的恐慌,老刑警劉巖霞揉,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件旬薯,死亡現(xiàn)場離奇詭異,居然都是意外死亡适秩,警方通過查閱死者的電腦和手機绊序,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門硕舆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人骤公,你說我怎么就攤上這事抚官。” “怎么了阶捆?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵凌节,是天一觀的道長。 經(jīng)常有香客問我洒试,道長倍奢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任垒棋,我火速辦了婚禮卒煞,結果婚禮上,老公的妹妹穿的比我還像新娘叼架。我一直安慰自己畔裕,他們只是感情好,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布乖订。 她就那樣靜靜地躺著扮饶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪垢粮。 梳的紋絲不亂的頭發(fā)上贴届,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天靠粪,我揣著相機與錄音蜡吧,去河邊找鬼。 笑死占键,一個胖子當著我的面吹牛昔善,可吹牛的內容都是我干的。 我是一名探鬼主播畔乙,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼君仆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了牲距?” 一聲冷哼從身側響起返咱,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎牍鞠,沒想到半個月后咖摹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡难述,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年萤晴,在試婚紗的時候發(fā)現(xiàn)自己被綠了吐句。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡店读,死狀恐怖嗦枢,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情屯断,我是刑警寧澤文虏,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站殖演,受9級特大地震影響择葡,放射性物質發(fā)生泄漏。R本人自食惡果不足惜剃氧,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一敏储、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧朋鞍,春花似錦已添、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至坎吻,卻和暖如春缆蝉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瘦真。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工刊头, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人诸尽。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓原杂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親您机。 傳聞我的和親對象是個殘疾皇子穿肄,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內容