Vas Sonic的源碼分析

最近在研讀Vas Sonic的源碼,Sonic是一款輕量級的高性能Hybrid框架,由騰訊QQ會員團隊開發(fā),專注于提升H5頁面首屏加載速度檐春。

首屏就是指用戶在沒有滾動時候看到的內(nèi)容渲染完成并且可以交互的時間。至于加載時間么伯,則是整個頁面滾動到底部疟暖,所有內(nèi)容加載完畢并可交互的時間。

H5以其開發(fā)和維護的成本較低田柔,開發(fā)周期較短的天然優(yōu)勢滿足了APP快速迭代的需求俐巴。目前很多APP或多或少接入了H5頁面,但H5存在的缺點是加載速度慢硬爆,造成不好的用戶體驗欣舵。因此,如何優(yōu)化H5的加載速度可以有效提升用戶的滿意度缀磕。

話不多說缘圈,接下來我們看看Sonic這個開源庫到底是一個什么樣的實現(xiàn)原理,首先給大家奉上Sonic的GitHub地址 點我

看一個開源庫,我通常會摸清楚其類層次關(guān)系袜蚕,從整體把握其組件糟把,然后在抽繭剝絲。不然會像走入一個迷宮牲剃,有種“只在此山中遣疯,云深不知處”的感覺。

下面是我繪制Sonic iOS庫的UML類圖:


可以梳理出其包含SonicURLProtocol, SonicEngine, SonicSession, SonicSever和SonicConnection五個組件及其相互之間的聯(lián)系凿傅。下面我們從源碼中來分析這幾個組件的角色和發(fā)揮的作用缠犀。

1. SonicURLProtocol

看到SonicURLProtocol這個類,我們立刻就能聯(lián)想到Foundation庫的NSURLProtocol類狭归,用戶可以通過子類化NSURLProtocol類來對上層的URLRequest請求做攔截,并根據(jù)自己的需求場景做定制化響應(yīng)處理文判。具體介紹詳見iOS 開發(fā)中使用 NSURLProtocol 攔截 HTTP 請求过椎。SonicURLProtocol利用這個原理來對UIWebView的請求進行攔截,實現(xiàn)自定義頁面數(shù)據(jù)加載和緩存戏仓。

SonicURLProtocol有三個重要的方法:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{    
 NSString *value = [request.allHTTPHeaderFields objectForKey:SonicHeaderKeyLoadType];
 if (value.length != 0 && [value isEqualToString:SonicHeaderValueWebviewLoad]) {
     NSString * delegateId = [request.allHTTPHeaderFields objectForKey:SonicHeaderKeyDelegateId];
     if (delegateId.length != 0) {
         NSString * sessionID = sonicSessionID(request.URL.absoluteString);
         SonicSession *session = [[SonicEngine sharedEngine] sessionWithDelegateId:delegateId];
         if (session && [sessionID isEqualToString:session.sessionID]) {
             return YES;
         }
       ...
     }
 }
 return NO;
}

這個方法重寫了NSURLProtocol類的方法疚宇,主要過濾需要攔截的請求,只有這個方法返回YES我們才能夠繼續(xù)后續(xù)的處理赏殃。通過這個方法的實現(xiàn)里面進行請求的過濾敷待,篩選出webView的網(wǎng)絡(luò)請求進行處理的請求。也就是請求頭中含有key值SonicHeaderKeyLoadType對應(yīng)的值為SonicHeaderValueWebviewLoad的NSURLRequest需要被攔截仁热。

接著會根據(jù)請求頭中的delegate去SonicEngine中尋找SonicSession,如果找到了對應(yīng)的SonicSession榜揖,接下來會對這個request進行攔截。 那么SonicSession是什么時候被初始化并注冊到SonicEngine中的呢?后面我們會進行講解举哟。

下面我們繼續(xù)看第2個方法思劳,代碼如下:


- (void)startLoading 
{    
 NSThread *currentThread = [NSThread currentThread];

 NSString *sessionID = [self.request valueForHTTPHeaderField:SonicHeaderKeySessionID];
 
 __weak typeof(self) weakSelf = self;
 
 [[SonicEngine sharedEngine] registerURLProtocolCallBackWithSessionID:sessionID completion:^(NSDictionary *param) {
     
     [weakSelf performSelector:@selector(callClientActionWithParams:) onThread:currentThread withObject:param waitUntilDone:NO];
     
 }];
}

這個方法是在請求開始的時候,會被執(zhí)行妨猩。這里做的主要操作是注冊了回調(diào)潜叛,也就是請求結(jié)束返回結(jié)果后作出對應(yīng)的操作。核心操作是回調(diào)了callClientActionWithParams壶硅,也就是我們將要呈現(xiàn)的第三個方法威兜,代碼如下:

 - (void)callClientActionWithParams:(NSDictionary *)params
{
   SonicURLProtocolAction action = [params[kSonicProtocolAction]integerValue];
   switch (action) {
       case SonicURLProtocolActionRecvResponse:
       {
           NSHTTPURLResponse *resp = params[kSonicProtocolData];
           [self.client URLProtocol:self didReceiveResponse:resp cacheStoragePolicy:NSURLCacheStorageNotAllowed];
       }
           break;
       case SonicURLProtocolActionLoadData:
       {
           NSData *recvData = params[kSonicProtocolData];
           if (recvData.length > 0) {
               [self.client URLProtocol:self didLoadData:recvData];
           }
       }
           break;
       case SonicURLProtocolActionDidSuccess:
       {
           [self.client URLProtocolDidFinishLoading:self];
       }
           break;
       case SonicURLProtocolActionDidFaild:
       {
           NSError *err = params[kSonicProtocolData];
           [self.client URLProtocol:self didFailWithError:err];
       }
           break;
   }
}
 

可以看到根據(jù)返回的不同的Action作出相應(yīng)的處理。這里主要是把數(shù)據(jù)傳回請求發(fā)起者client(這里就是UIWebView)庐椒,幫助其正確渲染椒舵。

可以看到這個類和我們平時使用一樣,主要就是攔截瀏覽器的請求扼睬,然后自定義請求得到數(shù)據(jù)返回給瀏覽器逮栅,進行渲染。

2. SonicEngine

順藤摸瓜窗宇,我們看到第二個組件類SonicEngine,這是個單例對象類措伐,通過上面的類圖可以看到它主要作用是用來創(chuàng)建和管理SonicSession類,其對外暴露了兩個重要的接口:

通過url和delegate來創(chuàng)建SonicSession

- (void)createSessionWithUrl:(NSString *)url withWebDelegate:(id<SonicSessionDelegate>)aWebDelegate withConfiguration:(SonicSessionConfiguration *)configuration

外部請求發(fā)起者注冊結(jié)果回調(diào)處理

- (void)registerURLProtocolCallBackWithSessionID:(NSString *)sessionID completion:(SonicURLProtocolCallBack)protocolCallBack

這個類主要充當(dāng)中轉(zhuǎn)站的作用军俊,同時管理SonicSession類侥加,根據(jù)請求者和請求的URL分配Session來完成網(wǎng)絡(luò)請求。比較簡單粪躬,下面我們具體看下核心處理類SonicSession担败。

3. SonicSession

SonicSession由請求的url和delegate(WebView的持有者)唯一確定。首先我們看SonicSession何時后會被初始化镰官。

在Sonic iOS提供的官方例子中可以看到提前,他們推薦在WebView初始化的時候,創(chuàng)建的SonicSession泳唠。Session的創(chuàng)建由SonicEngine來完成狈网,代碼如下:

- (void)createSessionWithUrl:(NSString *)url withWebDelegate:(id<SonicSessionDelegate>)aWebDelegate withConfiguration:(SonicSessionConfiguration *)configuration
{
...    
 
 [self.lock lock];
 SonicSession *existSession = self.tasks[sonicSessionID(url)];
 if (existSession && existSession.delegate != nil) {
     //session can only owned by one delegate
     [self.lock unlock];
     return;
 }
 
 if (!existSession) {
     //創(chuàng)建Session
     existSession = [[SonicSession alloc] initWithUrl:url withWebDelegate:aWebDelegate Configuration:configuration];
     
     NSURL *cUrl = [NSURL URLWithString:url];
     existSession.serverIP = [self.ipDomains objectForKey:cUrl.host];
     
     __weak typeof(self) weakSelf = self;
     __weak typeof(existSession)weakSession = existSession;
     [existSession setCompletionCallback:^(NSString *sessionID){
         [weakSession cancel];
         [weakSelf.tasks removeObjectForKey:sessionID];
     }];
     
     [self.tasks setObject:existSession forKey:existSession.sessionID];
     [existSession start]; //啟動Session
     [existSession release];

 } else {
     
     if (existSession.delegate == nil) {
         existSession.delegate = aWebDelegate;
     }
 }
 
 [self.lock unlock];
}

可以看到SonicSession是由url唯一確定,并一次只能綁定到WebView上笨腥。

Sonic在初始化的時候會嘗試從本地緩存中讀取數(shù)據(jù)拓哺,如果數(shù)據(jù)存在,則直接將數(shù)據(jù)返回給請求著脖母,否則士鸥,會等待請求結(jié)束后,將數(shù)據(jù)返回過去谆级,并且將這次請求數(shù)據(jù)緩存下來烤礁。如果服務(wù)器最新的數(shù)據(jù)到達后讼积,會根據(jù)返回碼來選擇性對瀏覽器已經(jīng)渲染的視圖進行修正。

這里我們需要介紹下Sonic的緩存和更新思路鸽凶,Sonic將Html代碼人為分為模板(Template)和數(shù)據(jù)(Data)币砂。通過代碼注釋的方式,增加了“sonicdiff-xxx”來標(biāo)注一個數(shù)據(jù)塊的開始與結(jié)束玻侥。模板就是將數(shù)據(jù)塊摳掉之后的Html决摧,然后通過{albums}來表示這個是一個數(shù)據(jù)塊占位。數(shù)據(jù)就是JSON格式凑兰,直接Key-Value掌桩。如圖是官方一張圖

由于我們HTML頁面模板更新的頻率比較低,而HTML需要展示的數(shù)據(jù)則更新頻繁姑食。通過這個思路波岛,就可以實現(xiàn)HTML頁面的增量更新。具體思想可以前往VasSonic:手Q開源Hybrid框架介紹音半。

這樣客戶端就可以根據(jù)服務(wù)器返回的請求頭來增量更新HTML頁面则拷。具體代碼如下:

- (void)updateDidSuccess
{
 switch (self.sonicServer.response.statusCode) {//獲得Response響應(yīng)頭
     case 304: //完全使用緩存
     {
         self.sonicStatusCode = SonicStatusCodeAllCached;
         self.sonicStatusFinalCode = SonicStatusCodeAllCached;
         //update headers
         [[SonicCache shareCache] saveResponseHeaders:self.sonicServer.response.allHeaderFields withSessionID:self.sessionID];
     }
         break;
     case 200: // Only need to request dynamic data.
     {
         if (![self.sonicServer isSonicResponse]) {
             [[SonicCache shareCache] removeCacheBySessionID:self.sessionID];
             NSLog(@"Clear cache because while not sonic repsonse!");
             break;
         }
         
         if ([self isTemplateChange]) {
             
             [self dealWithTemplateChange];
             
         }else{
             
             [self dealWithDataUpdate];
         }
         
         NSString *policy = [self.sonicServer responseHeaderForKey:SonicHeaderKeyCacheOffline];
         if ([policy isEqualToString:SonicHeaderValueCacheOfflineStore] || [policy isEqualToString:SonicHeaderValueCacheOfflineStoreRefresh] || [policy isEqualToString:SonicHeaderValueCacheOfflineRefresh]) {
             [[SonicCache shareCache] removeServerDisableSonic:self.sessionID];
         }
         
         if ([policy isEqualToString:SonicHeaderValueCacheOfflineRefresh] || [policy isEqualToString:SonicHeaderValueCacheOfflineDisable]) {
             
             if ([policy isEqualToString:SonicHeaderValueCacheOfflineRefresh]) {
                 
                 [[SonicCache shareCache]removeCacheBySessionID:self.sessionID];
             }
             
             if ([policy isEqualToString:SonicHeaderValueCacheOfflineDisable]) {
                 [[SonicCache shareCache] saveServerDisableSonicTimeNow:self.sessionID];
             }
         }
         
     }
         break;
     default:
     {
         
     }
         break;
 }
 
 //use the call back to tell web page which mode used
 if (self.webviewCallBack) {
     NSDictionary *resultDict = [self sonicDiffResult];
     if (resultDict) {
         self.webviewCallBack(resultDict);
     }
 }
 ...
}

由以上代碼可以看到,首頁會判斷服務(wù)器返回response的狀態(tài)碼曹鸠,304表示HTML的模板和數(shù)據(jù)均沒有更新煌茬,直接使用緩存的數(shù)據(jù)。這個時候客服端不需要進行任何操作彻桃。

如果是200坛善,則需要判斷是模板更新還是數(shù)據(jù)更新。接下來我們具體看下數(shù)據(jù)更新和模板更新會做什么操作:

  • 數(shù)據(jù)更新函數(shù)代碼如下:
   - (void)dealWithDataUpdate
{
    NSString *htmlString = nil;
    if (self.sonicServer.isInLocalServerMode) {
        NSDictionary *serverResult = [self.sonicServer sonicItemForCache];
        htmlString = serverResult[kSonicHtmlFieldName];
    }
    
    SonicCacheItem *cacheItem = [[SonicCache shareCache] updateWithJsonData:self.sonicServer.responseData withHtmlString:htmlString withResponseHeaders:self.sonicServer.response.allHeaderFields withUrl:self.url];
    
    if (cacheItem) {
        
        self.sonicStatusCode = SonicStatusCodeDataUpdate;
        self.sonicStatusFinalCode = SonicStatusCodeDataUpdate;
        self.localRefreshTime = cacheItem.lastRefreshTime;
        self.cacheFileData = cacheItem.htmlData;
        self.cacheResponseHeaders = cacheItem.cacheResponseHeaders;
        
        if (_diffData) {
            [_diffData release];
            _diffData = nil;
        }
        _diffData = [cacheItem.diffData copy];
        
        self.isDataFetchFinished = YES;
    }
}

這里主要工作是取出緩存的html模板邻眷,然后將更新后的數(shù)據(jù)和html模板進行合并眠屎。生成新的cacheItem,隨后更新本地數(shù)據(jù)。這里更新數(shù)據(jù)的格式是JSON格式肆饶,key為elementTag Id,value為tag顯示的數(shù)據(jù)改衩。可能有人會有疑惑驯镊,最新的數(shù)據(jù)怎么更新瀏覽器的顯示葫督。我們看到上面updateDidSuccess函數(shù)末尾有一段代碼。調(diào)用了webViewCallback回調(diào)阿宅,這個操作就完成了將更新的數(shù)據(jù)通過native調(diào)用js的方式來更新數(shù)據(jù)候衍。

  • Html模板更新:
  - (void)dealWithTemplateChange
{
   NSDictionary *serverResult = [self.sonicServer sonicItemForCache];
   SonicCacheItem *cacheItem = [[SonicCache shareCache] saveHtmlString:serverResult[kSonicHtmlFieldName] templateString:serverResult[kSonicTemplateFieldName] dynamicData:serverResult[kSonicDataFieldName] responseHeaders:self.sonicServer.response.allHeaderFields withUrl:self.url];//更新緩存
   
   if (cacheItem) {
       
       self.sonicStatusCode = SonicStatusCodeTemplateUpdate;
       self.sonicStatusFinalCode = SonicStatusCodeTemplateUpdate;
       self.localRefreshTime = cacheItem.lastRefreshTime;
       self.cacheFileData = self.sonicServer.responseData;
       self.cacheResponseHeaders = cacheItem.cacheResponseHeaders;
       
       self.isDataFetchFinished = YES;
       
       if (!self.didFinishCacheRead) {
           return;
       }
       
       NSString *opIdentifier  =  dispatchToMain(^{
           NSString *policy = [self.sonicServer responseHeaderForKey:SonicHeaderKeyCacheOffline];
           if ([policy isEqualToString:SonicHeaderValueCacheOfflineStoreRefresh] || [policy isEqualToString:SonicHeaderValueCacheOfflineRefresh]) {
               if (self.delegate && [self.delegate respondsToSelector:@selector(session:requireWebViewReload:)]) { //通知瀏覽器重新加載頁面
                   NSURLRequest *sonicRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:self.url]];
                   [self.delegate session:self requireWebViewReload:[SonicUtil sonicWebRequestWithSession:self withOrigin:sonicRequest]];
               }
           }
       });
       [self.mainQueueOperationIdentifiers addObject:opIdentifier];
   }
}

由于模板更新會更新整個網(wǎng)頁結(jié)構(gòu)笼蛛,因此洒放,避免不了需要重新刷新瀏覽器。首先會解析出返回htmlString中包含的模板和數(shù)據(jù),然后分別進行存儲滨砍,并更新本地變量往湿。最后利用 [self.delegate session:self requireWebViewReload:[SonicUtil sonicWebRequestWithSession:self withOrigin:sonicRequest]]; 這句代碼來通知瀏覽器刷新妖异。

以上分析的是存在緩存,處理的結(jié)果领追,如果是第一次加載頁面他膳,那么就不會走這個策略。直接通知瀏覽器渲染并存儲網(wǎng)頁數(shù)據(jù)绒窑。比較簡單棕孙,這里就不再贅述。

4. SonicServer

SonicServer是一個中間層些膨,用來對HTTP連接的request和response進行預(yù)處理蟀俊。SonicServer內(nèi)部自己組裝NSHTTPURLResponse對象,然后返回給上層订雾。此外肢预,SonicServer還支持本地服務(wù)模式。當(dāng)設(shè)置enableLocalSever為true時洼哎,如果本地存在緩存烫映,則直接將本地緩存數(shù)據(jù)傳給瀏覽器進行渲染,等到網(wǎng)絡(luò)數(shù)據(jù)接收完成時候噩峦,才通知數(shù)據(jù)的更新锭沟。

下面我們具體分析下在LocalServerMode模式下數(shù)據(jù)接收完成的處理操作。

- (void)connectionDidCompleteWithoutError:(SonicConnection *)connection
{
    self.isCompletion = YES;
    if (self.isInLocalServerMode) {//本地服務(wù)器模式壕探,這個時候請求徹底完成冈钦,數(shù)據(jù)也接收完成,直接更新緩存了
        
        do {
            //if http status is 304, there is nothing changed
            if (self.response.statusCode == 304) {
                NSLog(@"response status 304!");
                break;
            }
            
            self.htmlString = [[[NSString alloc]initWithData:self.responseData encoding:[self encodingFromHeaders]] autorelease];
            NSDictionary *splitResult = [SonicUtil splitTemplateAndDataFromHtmlData:self.htmlString];
            if (splitResult) {
                self.templateString = splitResult[kSonicTemplateFieldName];
                self.data = splitResult[kSonicDataFieldName];
            }
            
            NSMutableDictionary *headers = [[_response.allHeaderFields mutableCopy]autorelease];
            
            if (![headers objectForKey:SonicHeaderKeyCacheOffline]) { // refresh this time
                [headers setValue:@"true" forKey:[SonicHeaderKeyCacheOffline lowercaseString]];
            }
            NSString *htmlSha1 = nil;
            NSString *responseEtag = [headers objectForKey:[SonicHeaderKeyETag lowercaseString]];
            if (!responseEtag) {
                responseEtag = htmlSha1 = getDataSha1([self.htmlString dataUsingEncoding:NSUTF8StringEncoding]);
                [headers setObject:responseEtag forKey:[SonicHeaderKeyETag lowercaseString]];
            }
            NSString *requestEtag = [self.request.allHTTPHeaderFields objectForKey:HTTPHeaderKeyIfNoneMatch];
            if ([responseEtag isEqualToString:requestEtag]) { // Case:hit 304
                [headers setValue:@"false" forKey:[SonicHeaderKeyTemplateChange lowercaseString]];
                NSHTTPURLResponse *newResponse = [[[NSHTTPURLResponse alloc]initWithURL:_response.URL statusCode:304 HTTPVersion:nil headerFields:headers]autorelease];
                // Update response data
                ...
                break;
            }
            
            NSString *responseTemplateTag = [headers objectForKey:[SonicHeaderKeyTemplate lowercaseString]];
            if (!responseTemplateTag) {
                responseTemplateTag = getDataSha1([self.templateString dataUsingEncoding:NSUTF8StringEncoding]);
                [headers setValue:responseTemplateTag forKey:[SonicHeaderKeyTemplate lowercaseString]];
            }
            NSString *requestTemplateTag = [self.request.allHTTPHeaderFields objectForKey:SonicHeaderKeyTemplate];
            if ([responseTemplateTag isEqualToString:requestTemplateTag]) { // Case:data update
                NSError *jsonError = nil;
                NSMutableDictionary *jsonDict = [NSMutableDictionary dictionaryWithDictionary:self.data];
                if (!htmlSha1) {
                    htmlSha1 = getDataSha1([self.htmlString dataUsingEncoding:NSUTF8StringEncoding]);
                }
                [jsonDict setObject:htmlSha1 forKey:SonicHeaderKeyHtmlSha1];
                NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonDict options:NSJSONWritingPrettyPrinted error:&jsonError];
                if (!jsonError) {
                    [headers setValue:@"false" forKey:[SonicHeaderKeyTemplateChange lowercaseString]];
                    NSHTTPURLResponse *newResponse = [[[NSHTTPURLResponse alloc]initWithURL:_response.URL statusCode:200 HTTPVersion:nil headerFields:headers]autorelease];
                    // Update response data
                  ...
                    break;
                }
            }
            
            // Case:template-change
            [headers setValue:@"true" forKey:[SonicHeaderKeyTemplateChange lowercaseString]];
            NSHTTPURLResponse *newResponse = [[[NSHTTPURLResponse alloc]initWithURL:_response.URL statusCode:200 HTTPVersion:nil headerFields:headers]autorelease];
            ...
            _response = [newResponse retain];
            break;
            
        } while (true);
        
        //First request need't to load again
        if (![self isFirstLoadRequest]) {
            [self.delegate server:self didRecieveResponse:self.response];
            [self.delegate server:self didReceiveData:self.responseData];
        }
    }
    [self.delegate serverDidCompleteWithoutError:self];
}

上面代碼流程如下:

  1. 判斷服務(wù)器返回的網(wǎng)頁HtmlString和本地緩存的HtmlString的hash值是否一致李请,如果一致瞧筛,說明網(wǎng)頁沒有改變,命中304导盅,客戶端什么操作都不用做较幌。
  2. 判斷服務(wù)器返回的網(wǎng)頁模板和本地緩存HTML模板的差異,如果沒有變更白翻,那么判斷是data變更乍炉。這時候更新本地數(shù)據(jù)
  3. 最后只可能是網(wǎng)頁模板更新,則在請求頭標(biāo)記模板更新滤馍,交由上層進行處理岛琼。也就是SonicSession

最后回調(diào)SonicSession的serverDidCompleteWithoutError方法。

5. SonicConnection

SonicConnection最接近網(wǎng)絡(luò)層巢株,直接操作的是NSURLSession槐瑞,然后將NSURLSession返回的數(shù)據(jù)直接向上傳遞,比較簡單阁苞。

思考

這樣Sonic iOS的整體源碼就分析完成了困檩。如有不當(dāng)之處祠挫,歡迎大家批評指正

總的來說悼沿,Sonic通過服務(wù)器和客戶端約定的協(xié)議等舔,可以實現(xiàn)網(wǎng)頁的緩存和部分動態(tài)更新功能,為有效提升H5的加載速度提供了一種新的思路糟趾,可以有效提升用戶體驗慌植。但是Sonic需要人為對網(wǎng)頁結(jié)構(gòu)進行劃分,還需要服務(wù)器的協(xié)助义郑,具有過高的侵入性涤浇,引入成本比較高。是否引入需要考慮項目對WebView的依賴程度魔慷。
值得學(xué)習(xí)的是Sonic在首次加載頁面的時候在未創(chuàng)建UIWebView之前建立起網(wǎng)絡(luò)鏈接只锭,等待UIWebView發(fā)起主資源請求到NSURLProtocol層完成攔截,并且將提前發(fā)起的數(shù)據(jù)流通過NSURLProtocol返回給WebKit,實現(xiàn)網(wǎng)絡(luò)提前的并行加載院尔。

參考資源

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蜻展,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子邀摆,更是在濱河造成了極大的恐慌纵顾,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件栋盹,死亡現(xiàn)場離奇詭異施逾,居然都是意外死亡,警方通過查閱死者的電腦和手機例获,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門汉额,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人榨汤,你說我怎么就攤上這事蠕搜。” “怎么了收壕?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵妓灌,是天一觀的道長。 經(jīng)常有香客問我蜜宪,道長虫埂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任圃验,我火速辦了婚禮掉伏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己岖免,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布照捡。 她就那樣靜靜地躺著颅湘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪栗精。 梳的紋絲不亂的頭發(fā)上闯参,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音悲立,去河邊找鬼鹿寨。 笑死,一個胖子當(dāng)著我的面吹牛薪夕,可吹牛的內(nèi)容都是我干的脚草。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼原献,長吁一口氣:“原來是場噩夢啊……” “哼馏慨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起姑隅,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤写隶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后讲仰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體慕趴,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年鄙陡,在試婚紗的時候發(fā)現(xiàn)自己被綠了冕房。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡趁矾,死狀恐怖毒费,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情愈魏,我是刑警寧澤觅玻,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站培漏,受9級特大地震影響溪厘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜牌柄,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一畸悬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧珊佣,春花似錦蹋宦、人聲如沸披粟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽守屉。三九已至,卻和暖如春蒿辙,著一層夾襖步出監(jiān)牢的瞬間拇泛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工思灌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留俺叭,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓泰偿,卻偏偏與公主長得像熄守,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子耗跛,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,504評論 25 707
  • 這些概念性的東西可以在這里查看柠横,這篇文章我們主要來分析iOS端的表象然后再分析源碼。咋們還是放一張使用Sonic前...
    Thebloodelves閱讀 3,240評論 4 20
  • iOS網(wǎng)絡(luò)架構(gòu)討論梳理整理中课兄。牍氛。。 其實如果沒有APIManager這一層是沒法使用delegate的烟阐,畢竟多個單...
    yhtang閱讀 5,165評論 1 23
  • 《莊子》解搬俊,每章一讀。 文: 一雀適羿蜒茄,羿必得之唉擂,威也;以天下為之籠檀葛,則雀無所逃玩祟。是故湯以胞人籠伊尹,秦穆公以五羊...
    千里飄蓬閱讀 679評論 0 0
  • 小時候屿聋,爸爸媽媽出去打工空扎,我一直是跟著奶奶生活。 一天润讥,奶奶突然癱瘓了转锈,前幾天奶奶一直說腰疼,現(xiàn)在已經(jīng)站不起來...
    糯米的天使閱讀 212評論 0 0