iOS m3u8本地緩存播放(控制下載并發(fā)鹉戚、暫拖式洌恢復(fù))

目錄
一、m3u8緩存播放的整個流程
二抹凳、控制媒體下載的并發(fā)數(shù)
三遏餐、控制單個媒體的切片下載并發(fā)數(shù)
四、下載的中斷和恢復(fù)
五赢底、注意的問題與思路延伸
更新(相關(guān)demo會繼續(xù)完善境输,使用operation實(shí)現(xiàn)了并發(fā)控制的版本)

一蔗牡、m3u8緩存播放的整個流程

1.下載m3u8文件
2.解析m3u8文件獲取視頻切片單元的信息。
3.根據(jù)2.獲取的視頻切片信息中的切片鏈接下載切片并保持到本地嗅剖。
3.根據(jù)獲取的切片信息與本地服務(wù)器的配置信息辩越,拼接出切片的本地地址、生成新的m3u8文件并保存到本地信粮。
4.開啟本地服務(wù)器黔攒,使用本地url播放本地m3u8文件。

附上:時序圖强缘,具體可看demo

image.png

二督惰、控制媒體下載的并發(fā)數(shù)

   這里使用信號量來控制并發(fā)數(shù)
- (void)downloadVideoWithUrlString:(NSString *)urlStr downloadProgressHandler:(ZBLM3u8ManagerDownloadProgressHandler)downloadProgressHandler downloadSuccessBlock:(ZBLM3u8ManagerDownloadSuccessBlock) downloadSuccessBlock
{
    dispatch_async(_downloadQueue, ^{
        dispatch_semaphore_wait(_movieSemaphore, DISPATCH_TIME_FOREVER);
        __weak __typeof(self) weakself = self;
        [[self downloadContainerWithUrlString:urlStr] startDownloadWithUrlString:urlStr  downloadProgressHandler:^(float progress) {
            downloadProgressHandler(progress);
        } completaionHandler:^(NSString *locaLUrl, NSError *error) {
            if (!error) {
                [weakself.downloadContainerDictionary removeObjectForKey:[ZBLM3u8Setting uuidWithUrl:urlStr]];
                NSLog(@"下載完成:%@",urlStr);
                downloadSuccessBlock(locaLUrl);
            }
            else
            {
                NSLog(@"下載失敗:%@",error);
                [self resumeDownload];
            }
            NSLog(@"%@",weakself.downloadContainerDictionary.allKeys);
            dispatch_semaphore_signal(_movieSemaphore);
        }];
    });
}

這里可以設(shè)置_movieSemaphore的的初始值為具體的可同時下載數(shù)。
Example:_movieSemaphore = dispatch_semaphore_create(1),意味著同一時間只允許下載一個視頻旅掂,等同于視頻的串行下載赏胚。

三、控制單個媒體的切片下載并發(fā)數(shù)

   開始的時候商虐,考慮使用AFURLSessionManager中的operationQueue.maxConcurrentOperationCount來控制并發(fā)觉阅。但這是行不通的。因?yàn)檫@個queue是用于回調(diào)而不是用于下載隊(duì)列秘车。
/**
 The operation queue on which delegate callbacks are run.
 */
@property (readonly, nonatomic, strong) NSOperationQueue *operationQueue;

再看AF中初始化

- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
    self = [super init];
    if (!self) {
        return nil;
    }

    if (!configuration) {
        configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    }

    self.sessionConfiguration = configuration;

    self.operationQueue = [[NSOperationQueue alloc] init];
    self.operationQueue.maxConcurrentOperationCount = 1;

    self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
...
   這個queue確實(shí)是用于回調(diào)典勇,而我是需要控制下載并發(fā)。這似乎不滿足叮趴。而且實(shí)測中也發(fā)現(xiàn)確實(shí)不行割笙。那么,只能在任務(wù)發(fā)起哪里做并發(fā)控制眯亦,同樣伤溉,這里還是采用信號量。這里的控制相對復(fù)雜一點(diǎn)妻率、因?yàn)楹竺娴娜蝿?wù)恢復(fù)乱顾、失敗任務(wù)重新創(chuàng)建也要做控制。
- (void)startDownload
{
    //因?yàn)檫@是外部調(diào)用的方法舌涨,操作的執(zhí)行要放到異步線程中。避免因?yàn)椴l(fā)控制中的等待而堵塞外部線程
    dispatch_async(self.downloadQueue, ^{
        if (!_fileDownloadInfos.count) {
            _completaionHandler(nil);
            return;
        }
        NSLog(@"downloadInfoCount:%ld",(long)_fileDownloadInfos.count);
        
        [_fileDownloadInfos enumerateObjectsUsingBlock:^(ZBLM3u8FileDownloadInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            //控制切片下載并發(fā)
            dispatch_semaphore_wait(self.tsSemaphore, DISPATCH_TIME_FOREVER);
            if ([ZBLM3u8FileManager exitItemWithPath:obj.filePath]) {
                obj.success = YES;
                [self verifyDownloadCountAndCallbackByDownloadSuccess:YES];
            }
            else
            {
                //如果收到中斷信號扔字,中斷下載流程釋放信號量并返回
                if (self.suspend) {
                    obj.beStopCreateTask = YES;
                    dispatch_semaphore_signal(self.tsSemaphore);
                    NSLog(@"suspend and return! don not createDownloadTask!");
                    return ;
                }
                else
                {
                    //真正的創(chuàng)建下載任務(wù)
                    [self createDownloadTaskWithIndex:idx];
                }
            }
        }];
    });
}

//信號量在每個任務(wù)的回調(diào)后都會釋放一次
- (void)verifyDownloadCountAndCallbackByDownloadSuccess:(BOOL) isSuccess
{
    dispatch_semaphore_signal(self.tsSemaphore);
...
  • 這里也看到信號量的控制問題囊嘉,必須理清信號量的獲得和釋放時機(jī),一次獲得必須有一次釋放革为。不釋放或者重復(fù)釋放扭粱,都會導(dǎo)致并發(fā)的控制不準(zhǔn)。如果這樣震檩,那這里的并發(fā)控制就沒有意義了琢蛤。

  • 要做到準(zhǔn)確獲取和釋放蜓堕,重點(diǎn)在于理清程序的執(zhí)行路徑。在每一條執(zhí)行路徑中都必須釋放信號量博其。這個跟鎖的使用也是一樣的套才。

  • 調(diào)試現(xiàn)象:如果程序不像預(yù)料中運(yùn)行,又沒有什么錯誤慕淡,那很有可能就是堵塞了背伴。鎖沒有釋放或者信號量的處理有問題。
    處理步驟:點(diǎn)擊xcode調(diào)試欄的暫停按鈕峰髓,查看程序的調(diào)用棧傻寂,分析每個線程的運(yùn)行情況,找到堵塞具體執(zhí)行代碼携兵。根據(jù)具體的邏輯修正問題疾掰。

四、下載的中斷和恢復(fù)

   這里有幾個小問題:根據(jù)判斷NSURLSessionTask 提供的3個方法可以做一些中斷和恢復(fù)處理
  • (void)suspend;
    掛起任務(wù)徐紧,但只能掛起執(zhí)行中的任務(wù)静檬。對于已經(jīng)創(chuàng)建而且執(zhí)行resum方法但并沒真正執(zhí)行的任務(wù)無效(這里非常坑)浪汪。
    通常我們使用這個方法的時候會判斷下任務(wù)的具體狀態(tài)巴柿,如果是task.state == NSURLSessionTaskStateRunning采取執(zhí)行 [task suspend]。但這個判斷是不準(zhǔn)確的死遭。如果一個任務(wù)創(chuàng)建并執(zhí)行resume但并沒真正執(zhí)行广恢,它的狀態(tài)也是為NSURLSessionTaskStateRunning。如果這個時候程序收到中斷消息呀潭,對狀態(tài)為NSURLSessionTaskStateRunning 的任務(wù)全部執(zhí)行suspend操作钉迷,你會發(fā)現(xiàn)有些任務(wù)不聽話,繼續(xù)執(zhí)行钠署。到底什么搞鬼...
    我的理解是這樣的糠聪,這些不聽話的任務(wù)正是那些添加到下載隊(duì)列中等待執(zhí)行的任務(wù),而在等待狀態(tài)下收到suspend消息是不管用的谐鼎。但它接收cannel消息是管用的舰蟆。那么問題的解決就是找出這些等待的任務(wù)。
    處理辦法:通過判斷接收字節(jié)數(shù)來區(qū)分狀態(tài)±旯鳎現(xiàn)在我只面向你接收的字節(jié)數(shù)身害,而不管你真開啟還是假開啟了。
switch (obj.downloadTask.state) {
                case NSURLSessionTaskStateRunning:
                {
                    //等待中草戈,假開啟狀態(tài)塌鸯,
                    if (obj.downloadTask.countOfBytesReceived <= 0) {
                        [obj.downloadTask cancel];
                    }
                    else
                    {
                    //正在下載,真開啟狀態(tài)唐片,
                        [obj.downloadTask suspend];
                    }
                }
                    break;
  • (void)resume;
    官方文檔是這么說明的:Resumes the task, if it is suspended.意思是指只能發(fā)起被掛起的任務(wù)丙猬。
    存在兩種情況:
    1.新創(chuàng)建的任務(wù)并沒有執(zhí)行resume涨颜,此時狀態(tài)為:NSURLSessionTaskStateSuspended
    2.執(zhí)行suspend方法后被手動掛起的任務(wù),狀態(tài)同為NSURLSessionTaskStateSuspended茧球。
    同時這里提供了額外信息:狀態(tài)為NSURLSessionTaskStateCompleted的任務(wù)是不能通過resume重新發(fā)起的庭瑰。而在某些情況下我們需要對這種狀態(tài)的任務(wù)重新發(fā)起,包括手動cannel的袜腥、執(zhí)行失敗的见擦。這種情況下只能根據(jù)具體的情況,重新創(chuàng)建任務(wù)并發(fā)起羹令。
if (obj.downloadTask.error && 
    obj.downloadTask.state == NSURLSessionTaskStateCompleted)
{
              //下載失敗的任務(wù)重新創(chuàng)建
                [self createDownloadTaskWithIndex:idx];
 }
  • (void)cancel;
    官方文檔說明:

This method returns immediately, marking the task as being canceled. Once a task is marked as being canceled, URLSession:task:didCompleteWithError: will be sent to the task delegate, passing an error in the domain NSURLErrorDomain with the code NSURLErrorCancelled. A task may, under some circumstances, send messages to its delegate before the cancelation is acknowledged.
This method may be called on a task that is suspended.

簡而言之:調(diào)用這個方法可以cannel任意狀態(tài)的任務(wù)包括掛起的任務(wù)鲤屡。并執(zhí)行didCompleteWithError回調(diào)(AF中會執(zhí)行回調(diào)并返回錯誤NSURLErrorCancelled),狀態(tài)被標(biāo)記為NSURLSessionTaskStateCompleted福侈。故上面恢復(fù)失敗任務(wù)的時候酒来,通過task.state和task.error共同判斷。

總結(jié)下任務(wù)生命周期中的任務(wù)狀態(tài)變化:

1.創(chuàng)建成功:...Suspended
2.執(zhí)行resume:...Running(可以通過判斷countOfBytesReceived來區(qū)分任務(wù)處于等待還是下載中)
3.執(zhí)行suspend:...Suspended
4.執(zhí)行cannel:(中間狀態(tài)...Canceling)->...Completed(可以結(jié)合task.error判斷任務(wù)執(zhí)行結(jié)果)

回歸正題:
視頻單元的中斷和恢復(fù)肪凛,中斷就是調(diào)用suspend方法掛起正在執(zhí)行的任務(wù)堰汉,cannel掉等待的任務(wù),代碼跟上面說明方法的時候非常雷同伟墙。這里著重說明下恢復(fù)翘鸭。如果單單恢復(fù)其實(shí)很簡單,恢復(fù)掛起的任務(wù)和重新創(chuàng)建錯誤的任務(wù)戳葵。這只是從程序的角度看待就乓,要使一個程序有更高的可用性,應(yīng)在功能實(shí)現(xiàn)的同時做的更加的合理拱烁。應(yīng)優(yōu)先恢復(fù)掛起的任務(wù)生蚁、然后重新創(chuàng)建錯誤的任務(wù)。這樣做都是為了承前啟后更快的把一個下載任務(wù)完成戏自。而且這個視頻切片是講究有序的邦投,所以我們恢復(fù)的時候也要遵從FIFO的原則。

五擅笔、注意的問題與思路延伸

1.解析m3u8注意的問題
meu8的解析格式太多志衣,很容易出現(xiàn)問題,應(yīng)使用try/catch來保證程序的健壯性猛们。

2.根據(jù)url獲取原始m3u8文件信息念脯,這個操作太耗時了。為了提高程序的效率阅懦,獲取到原始m3u8文件后應(yīng)做本地持久化并用于二次下載和二。如果整個流程下載成功徘铝,可以選擇刪除該文件耳胎,或者不操作惯吕。由于是純文本文件,少量的文件冗余是允許的怕午。

3.文件的操作通過開啟一個同步隊(duì)列來處废登。可以設(shè)定low優(yōu)先級避免占用太高的cpu資源郁惜。其實(shí)高cpu占用會伴隨著另外一個問題堡距,手機(jī)的發(fā)熱量。

4.key的處理問題
如果存在key的下載兆蕉,需要把key下載到本地羽戒,約定好key的名稱和新建m3u8文件中的key鏈接。這樣本地播放就能正常加解密虎韵。
例如下載到本地的key保存為.../key易稠。那么鏈接應(yīng)該是http:localhost:port/.../key

5.中斷的優(yōu)先級
程序的中斷操作擁有最高優(yōu)先級的,因?yàn)橐魏螤顟B(tài)下都能中斷下載包蓝。無論是為了程序的流暢性驶社、網(wǎng)絡(luò)變?yōu)橐苿有盘柋苊馐褂糜脩舻囊苿恿髁康劝l(fā)出中斷命令,都必須立即響應(yīng)测萎。

6.切片數(shù)量的全局分配
多個視頻同時下載亡电,多個切片同時并發(fā)。如果要做到控制全局的切片并發(fā)數(shù)而不是單個視頻的切片并發(fā)數(shù)硅瞧。這就要設(shè)計(jì)一個算法在全局Manger哪里做分配和回收份乒。

7.保證app流程,監(jiān)控網(wǎng)速開啟和中斷下載
下載視頻的功能應(yīng)該要保證app本身網(wǎng)絡(luò)請求的正常運(yùn)行零酪。
app如何獲取到網(wǎng)絡(luò)的帶寬冒嫡,好像只能通過下載文件方式來推算∷奈可在應(yīng)用請求空閑時通過短時間下載一個可用源來計(jì)算帶寬孝凌,同時監(jiān)控app 實(shí)時網(wǎng)絡(luò)吞吐,適當(dāng)?shù)拈_關(guān)下載月腋。雖然很難做到實(shí)時蟀架,但是在切換網(wǎng)絡(luò)的時候進(jìn)行帶寬重測、又或者地理位置變化一定距離后進(jìn)行帶寬重測榆骚、又或者定時作帶寬重測片拍。還有就是考慮wifi狀態(tài)下才進(jìn)行下載。

8.切換網(wǎng)絡(luò)后請求失去連接妓肢,恢復(fù)下載的問題
因?yàn)榫W(wǎng)絡(luò)切換本地ip變化捌省,發(fā)起的請求會失去連接。如果通過downloadTask是沒辦法做到恢復(fù)下載的碉钠。雖然可以用resumeData來恢復(fù)下載纲缓,但是這個只能在cannel操作的時候獲取卷拘,至于失去連接的情況下是沒辦法獲取到的(系統(tǒng)提供的api中沒有在失敗回調(diào)哪里返回resumeData的)。(思路是這樣祝高,不一定能實(shí)現(xiàn))這個時候需要自己創(chuàng)建文件句柄栗弟,使用dataTask做到文件續(xù)下。初始化dataTask的時候設(shè)定請求頭'Accept-Ranges'參數(shù)為文件的已下載字節(jié)數(shù)(需要服務(wù)器支持)工闺,就可以獲取到未下載的部分?jǐn)?shù)據(jù)乍赫。

9.并發(fā)中鎖的處理
要理清那些代碼可能存在并發(fā),那些操作要保證原子性陆蟆。難就難在一個方法中會存在部分代碼塊是并發(fā)執(zhí)行的雷厂,這有利于效率的提高;部分些代碼要原子操作叠殷。最優(yōu)的做法就是對原子操作用鎖來保證罗侯,沒有任何多余的代碼加入到同步操作中,這樣也是效率最高的溪猿。而拿捏不準(zhǔn)的情況下钩杰,可以鎖定更多的代碼,至少這樣不會因?yàn)椴l(fā)而導(dǎo)致問題诊县,但這樣就犧牲了效率和及時性讲弄。當(dāng)一個簡單的系統(tǒng),要做到最優(yōu)好像并不難依痊,但是一個復(fù)雜的系統(tǒng)做到最優(yōu)就非常難了或者是要花費(fèi)非常大的精力避除。基于這個demo的實(shí)現(xiàn)多線程流了不少坑胸嘁,總結(jié)下多線程還是復(fù)雜瓶摆。開發(fā)中優(yōu)先考慮線程安全,再提高性能吧性宏。

10.是否需要全部切片下載完成才能播放
其實(shí)并不需要全部下載完成就能播放的群井。保證key 先下載下來,而且要保證有序下載毫胜,然后下載一定量的切片文件书斜,這個時候就可以組裝m3u8文件到本地,發(fā)起播放酵使。只要后面下載的切片能滿足播放器的播放荐吉,就不會出現(xiàn)問題。但如果供應(yīng)不足視頻就會停了口渔,播放不了样屠,盡管后面文件下載下來了,還是不能自動恢復(fù),仿佛失去了緩沖功能痪欲。這里就是跟直接請求服務(wù)器的差別了混巧,直接請求服務(wù)器,因?yàn)槲募旧硎谴嬖诘那诳l(fā)起的請求是存在的,如果網(wǎng)速慢秘蛔,播放器的反應(yīng)是緩沖陨亡;而本地服務(wù)播放就不同了,如果文件在播放前沒有下載下來深员,發(fā)起的請求立馬就掛了负蠕,這個請求不存在,當(dāng)然就不存在緩沖倦畅。

11.線程多開占用資源遮糖,每個線程占用512K到1M空間。建議使用單線程下載叠赐,且穩(wěn)定性高欲账。

dome雖然實(shí)現(xiàn)了多線程下載,偶發(fā)死鎖的問題會存在芭概,就是有坑H弧!罢洲!踢故。但m3u8文本文件跟數(shù)據(jù)解析部分處理是穩(wěn)定的。(已更新修正部分問題惹苗。)
鏈接:https://github.com/zmubai/ZBLM3U8DownLoadTest

更新:
  1. 之前的demo問題不少殿较,就抽空改了下,問題有所改善桩蓉,但還不能達(dá)到穩(wěn)定淋纲。就又弄了一個簡單的demo,把主要功能實(shí)現(xiàn)院究,做到簡單點(diǎn)穩(wěn)定點(diǎn)帚戳。
    地址:https://github.com/zmubai/m3u8DownloadSimpleDemo[2019-4-7]
  2. 要實(shí)現(xiàn)并發(fā)控制,使用信號量的方式是可以實(shí)現(xiàn)的儡首,但有一個很大的缺點(diǎn)片任,就是暫停控制變得麻煩蔬胯。假設(shè)并發(fā)控制數(shù)為2对供,同時發(fā)起10個,那么有8個在等待,如果執(zhí)行暫停产场,那么已經(jīng)執(zhí)行的2個可以暫停鹅髓,但等待的8個取消不了,需要讓他們發(fā)起京景,然后再取消窿冯,操作變得很麻煩。衡量了一下确徙,可以使用NSOperationQueue去控制并發(fā)醒串,繼承NSOperation,創(chuàng)建子類,并通過設(shè)置finish變量為true鄙皇,讓任務(wù)完成芜赌。只有當(dāng)finish為true,任務(wù)才會在隊(duì)列中移除,這樣就能控制并發(fā)伴逸,并且更易于執(zhí)行cannel等操作缠沈。[2019-4-7]

3.由于之前版本的實(shí)現(xiàn)不是特別滿意,就使用operation實(shí)現(xiàn)的版本错蝴。(支持使用cocoaPods 安裝)
支持媒體并發(fā)控制洲愤,支持單個媒體文件并發(fā)控制。支持任務(wù)取消顷锰,支持任務(wù)掛起和恢復(fù)禽篱。

地址:https://github.com/zmubai/BNM3u8Cache[2019-12]

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市馍惹,隨后出現(xiàn)的幾起案子躺率,更是在濱河造成了極大的恐慌,老刑警劉巖万矾,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悼吱,死亡現(xiàn)場離奇詭異,居然都是意外死亡良狈,警方通過查閱死者的電腦和手機(jī)后添,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來薪丁,“玉大人遇西,你說我怎么就攤上這事⊙鲜龋” “怎么了粱檀?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長漫玄。 經(jīng)常有香客問我茄蚯,道長压彭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任渗常,我火速辦了婚禮壮不,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘皱碘。我一直安慰自己询一,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布癌椿。 她就那樣靜靜地躺著健蕊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪如失。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天送粱,我揣著相機(jī)與錄音褪贵,去河邊找鬼。 笑死抗俄,一個胖子當(dāng)著我的面吹牛脆丁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播动雹,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼槽卫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了胰蝠?” 一聲冷哼從身側(cè)響起歼培,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎茸塞,沒想到半個月后躲庄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钾虐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年噪窘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片效扫。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡倔监,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出菌仁,到底是詐尸還是另有隱情浩习,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布济丘,位于F島的核電站瘦锹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜弯院,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一辱士、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧听绳,春花似錦颂碘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鼠证,卻和暖如春峡竣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背量九。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工适掰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人荠列。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓类浪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親肌似。 傳聞我的和親對象是個殘疾皇子费就,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,104評論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件川队、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,102評論 4 62
  • 強(qiáng)烈推薦snapseed+spring力细! 快點(diǎn)上手學(xué)起來吧! 人人都是“新手大師”固额!
    菩葉生閱讀 173評論 0 0
  • (在家訪工作推進(jìn)會上的發(fā)言) 各位領(lǐng)導(dǎo)艳汽、老師: 大家下午好!今天能與各位交流我的家訪故事和心得感...
    常星慧閱讀 1,376評論 0 2
  • 《我的前半生》經(jīng)典語錄对雪,句句是毒藥 我的前半生河狐,就像一場夢。你的后半生瑟捣,愿君多珍重馋艺。結(jié)婚為什么?不就是人生不易迈套,要...
    張錦全閱讀 268評論 0 0