iOS-RunLoop在實際開發(fā)過程中的應(yīng)用

參考文章

RunLoop的使用場景

下面介紹一下奥额,可以使用RunLoop的幾個使用場景。

1.保證線程的長時間存活

在iOS開發(fā)過程中访诱,有時候我們不希望一些花費(fèi)時間比較長的操作阻塞主線程披坏,導(dǎo)致界面卡頓,那么我們就會創(chuàng)建一個子線程盐数,然后把這些花費(fèi)時間比較長的操作放在子線程中來處理棒拂。可是當(dāng)子線程中的任務(wù)執(zhí)行完畢后玫氢,子線程就會被銷毀掉帚屉。 怎么來驗證上面這個結(jié)論呢?首先漾峡,我們創(chuàng)建一個HLThread類攻旦,繼承自NSThread,然后重寫dealloc 方法生逸。

@interface HLThread : NSThread
?
@end
?
@implementation HLThread
?
- (void)dealloc
{
  NSLog(@"%s",__func__);
}
?
@end

然后牢屋,在控制器中用HLThread創(chuàng)建一個線程,執(zhí)行一個任務(wù)槽袄,觀察任務(wù)執(zhí)行完畢后烙无,線程是否被銷毀。

- (void)viewDidLoad {
  [super viewDidLoad];
?
  // 1.測試線程的銷毀
  [self threadTest];
}
?
- (void)threadTest
{
  HLThread *subThread = [[HLThread alloc] initWithTarget:self selector:@selector(subThreadOpetion) object:nil];
  [subThread start];
}
?
- (void)subThreadOpetion
{
  @autoreleasepool {
      NSLog(@"%@----子線程任務(wù)開始",[NSThread currentThread]);
      [NSThread sleepForTimeInterval:3.0];
      NSLog(@"%@----子線程任務(wù)結(jié)束",[NSThread currentThread]);
  }
}

控制臺輸出的結(jié)果如下:

2016-12-01 16:44:25.559 RunLoopDemo[4516:352041] <HLThread: 0x608000275680>{number = 4, name = (null)}----子線程任務(wù)開始
2016-12-01 16:44:28.633 RunLoopDemo[4516:352041] <HLThread: 0x608000275680>{number = 4, name = (null)}----子線程任務(wù)結(jié)束
2016-12-01 16:44:28.633 RunLoopDemo[4516:352041] -[HLThread dealloc]

當(dāng)子線程中的任務(wù)執(zhí)行完畢后遍尺,線程就被立刻銷毀了截酷。如果程序中,需要經(jīng)常在子線程中執(zhí)行任務(wù)乾戏,頻繁的創(chuàng)建和銷毀線程迂苛,會造成資源的浪費(fèi)。這時候我們就可以使用RunLoop來讓該線程長時間存活而不被銷毀鼓择。

我們將上面的示例代碼修改一下三幻,修改后的代碼過程為,創(chuàng)建一個子線程呐能,當(dāng)子線程啟動后念搬,啟動runloop,點擊視圖催跪,會在子線程中執(zhí)行一個耗時3秒的任務(wù)(其實就是讓線程睡眠3秒)锁蠕。

修改后的代碼如下:

@implementation ViewController
?
- (void)viewDidLoad {
  [super viewDidLoad];
?
  // 1.測試線程的銷毀
  [self threadTest];
}
?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
  [self performSelector:@selector(subThreadOpetion) onThread:self.subThread withObject:nil waitUntilDone:NO];
}
?
- (void)threadTest
{
  HLThread *subThread = [[HLThread alloc] initWithTarget:self selector:@selector(subThreadEntryPoint) object:nil];
  [subThread setName:@"HLThread"];
  [subThread start];
  self.subThread = subThread;
}
?
/**
子線程啟動后,啟動runloop
*/
- (void)subThreadEntryPoint
{
  @autoreleasepool {
      NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
      //如果注釋了下面這一行懊蒸,子線程中的任務(wù)并不能正常執(zhí)行
      [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
      NSLog(@"啟動RunLoop前--%@",runLoop.currentMode);
      [runLoop run];
  }
}
?
/**
子線程任務(wù)
*/
- (void)subThreadOpetion
{
  NSLog(@"啟動RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
  NSLog(@"%@----子線程任務(wù)開始",[NSThread currentThread]);
  [NSThread sleepForTimeInterval:3.0];
  NSLog(@"%@----子線程任務(wù)結(jié)束",[NSThread currentThread]);
}
?
@end

先看控制臺輸出結(jié)果:

2016-12-01 17:22:44.396 RunLoopDemo[4733:369202] 啟動RunLoop前--(null)
2016-12-01 17:22:49.285 RunLoopDemo[4733:369202] 啟動RunLoop后--kCFRunLoopDefaultMode
2016-12-01 17:22:49.285 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子線程任務(wù)開始
2016-12-01 17:22:52.359 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子線程任務(wù)結(jié)束
2016-12-01 17:22:55.244 RunLoopDemo[4733:369202] 啟動RunLoop后--kCFRunLoopDefaultMode
2016-12-01 17:22:55.245 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子線程任務(wù)開始
2016-12-01 17:22:58.319 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子線程任務(wù)結(jié)束

有幾點需要注意: 1.獲取RunLoop只能使用 [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop]; 2.即使RunLoop開始運(yùn)行荣倾,如果RunLoop 中的 modes為空,或者要執(zhí)行的mode里沒有item骑丸,那么RunLoop會直接在當(dāng)前l(fā)oop中返回舌仍,并進(jìn)入睡眠狀態(tài)妒貌。 3.自己創(chuàng)建的Thread中的任務(wù)是在kCFRunLoopDefaultMode這個mode中執(zhí)行的。 4.在子線程創(chuàng)建好后铸豁,最好所有的任務(wù)都放在AutoreleasePool中灌曙。

注意點一解釋 RunLoop官方文檔中的第二段中就已經(jīng)說明了,我們的應(yīng)用程序并不需要自己創(chuàng)建RunLoop节芥,而是要在合適的時間啟動runloop在刺。 CF框架源碼中有CFRunLoopGetCurrent(void)CFRunLoopGetMain(void),查看源碼可知,這兩個API中头镊,都是先從全局字典中取蚣驼,如果沒有與該線程對應(yīng)的RunLoop,那么就會幫我們創(chuàng)建一個RunLoop(創(chuàng)建RunLoop的過程在函數(shù)_CFRunLoopGet0(pthread_tt)中)相艇。

注意點二解釋 這一點颖杏,可以將示例代碼中的[runLoop addPort:[NSMachPort port]forMode:NSRunLoopCommonModes];,可以看到注釋掉后坛芽,無論我們?nèi)绾吸c擊視圖留储,控制臺都不會有任何的輸出,那是因為mode中并沒有item任務(wù)咙轩。經(jīng)過NSRunLoop封裝后获讳,只可以往mode中添加兩類item任務(wù):NSPort(對應(yīng)的是source)、NSTimer臭墨,如果使用CFRunLoopRef,則可以使用C語言API,往mode中添加source赔嚎、timer膘盖、observer胧弛。 如果不添加 [runLoop addPort:[NSMachPort port]forMode:NSRunLoopCommonModes];,我們把runloop的信息輸出侠畔,可以看到:

727768-320115694bf8d18a.png.jpeg

如果我們添加上[runLoop addPort:[NSMachPort port]forMode:NSRunLoopCommonModes];,再把RunLoop的信息輸出结缚,可以看到:

727768-5f28202d76ac5997.png.jpeg

注意點三解釋 怎么確認(rèn)自己創(chuàng)建的子線程上的任務(wù)是在kCFRunLoopDefaultMode這個mode中執(zhí)行的呢? 我們只需要在執(zhí)行任務(wù)的時候软棺,打印出該RunLoop的currentMode即可红竭。 因為RunLoop執(zhí)行任務(wù)是會在mode間切換,只執(zhí)行該mode上的任務(wù)喘落,每次切換到某個mode時茵宪,currentMode就會更新。源碼請下載:CF框架源碼 CFRunLoopRun()方法中會調(diào)用CFRunLoopRunSpecific()方法瘦棋,而CFRunLoopRunSpecific()方法中有這么兩行關(guān)鍵代碼:

CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
......這中間還有好多邏輯代碼
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
...... 這中間也有一堆的邏輯
rl->_currentMode = previousMode;

我測試后稀火,控制臺輸出的是:

2016-12-02 11:09:47.909 RunLoopDemo[5479:442560] 啟動RunLoop后--kCFRunLoopDefaultMode
2016-12-02 11:09:47.910 RunLoopDemo[5479:442560] <HLThread: 0x608000270a80>{number = 4, name = HLThread}----子線程任務(wù)開始
2016-12-02 11:09:50.984 RunLoopDemo[5479:442560] <HLThread: 0x608000270a80>{number = 4, name = HLThread}----子線程任務(wù)結(jié)束

注意點四解釋 關(guān)于AutoReleasePool的官方文檔中有提到:

If you spawn a secondary thread.You must create your own autorelease pool block as soon as the thread begins executing; otherwise, your application will leak objects. (See Autorelease Pool Blocks and Threads for details.)
Each thread in a Cocoa application maintains its own stack of autorelease pool blocks. If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.
If your application or thread is long-lived and potentially generates a lot of autoreleased objects, you should use autorelease pool blocks (like AppKit and UIKit do on the main thread); otherwise, autoreleased objects accumulate and your memory footprint grows.
If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.

AFNetworking中的RunLoop案例

在AFNetworking2.6.3之前的版本,使用的還是NSURLConnection赌朋,可以在AFURLConnectionOperation中找到使用RunLoop的源碼:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
  @autoreleasepool {
      [[NSThread currentThread] setName:@"AFNetworking"];
      NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
      [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
      [runLoop run];
  }
}
?
+ (NSThread *)networkRequestThread {
  static NSThread *_networkRequestThread = nil;
  static dispatch_once_t oncePredicate;
  dispatch_once(&oncePredicate, ^{
      _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
      [_networkRequestThread start];
  });
  return _networkRequestThread;
}

AFNetworking都是通過調(diào)用 [NSObject performSelector:onThread:..] 將這個任務(wù)扔到了后臺線程的RunLoop 中凰狞。

- (void)start {
  [self.lock lock];
  if ([self isCancelled]) {
      [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
  } else if ([self isReady]) {
      self.state = AFOperationExecutingState;
      [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
  }
  [self.lock unlock];
}

我們在使用NSURLConnection或者NSStream時篇裁,也需要考慮到RunLoop問題,因為默認(rèn)情況下這兩個類的對象生成后赡若,都是在當(dāng)前線程的NSDefaultRunLoopMode模式下執(zhí)行任務(wù)达布。如果是在主線程,那么就會出現(xiàn)滾動ScrollView以及其子視圖時逾冬,主線程的RunLoop切換到UITrackingRunLoopMode模式黍聂,那么NSURLConnection或者NSStream的回調(diào)就無法執(zhí)行了。

要解決這個問題身腻,有兩種方式: 第一種方式是創(chuàng)建出NSURLConnection對象或者NSStream對象后分冈,再調(diào)用 -(void)scheduleInRunLoop:(NSRunLoop *)aRunLoopforMode:(NSRunLoopMode)mode,設(shè)置RunLoopMode即可。需要注意的是NSURLConnection必須使用其初始化構(gòu)造方法-(nullable instancetype)initWithRequest:(NSURLRequest *)requestdelegate:(nullable id)delegatestartImmediately:(BOOL)startImmediately來創(chuàng)建對象霸株,設(shè)置Mode才會起作用雕沉。

第二種方式,就是所有的任務(wù)都在子線程中執(zhí)行去件,并保證子線程的RunLoop正常運(yùn)行即可(即上面AFNetworking的做法坡椒,因為主線程的RunLoop切換到UITrackingRunLoopMode,并不影響其他線程執(zhí)行哪個mode中的任務(wù)尤溜,計算機(jī)CPU是在每一個時間片切換到不同的線程去跑一會倔叼,呈現(xiàn)出的多線程效果)。

二宫莱、RunLoop如何保證NSTimer在視圖滑動時丈攒,依然能正常運(yùn)轉(zhuǎn)

使用場景

1.我們經(jīng)常會在應(yīng)用中看到tableView 的header上是一個橫向ScrollView,一般我們使用NSTimer授霸,每隔幾秒切換一張圖片巡验。可是當(dāng)我們滑動tableView的時候碘耳,頂部的scollView并不會切換圖片显设,這可怎么辦呢? 2.界面上除了有tableView辛辨,還有顯示倒計時的Label捕捂,當(dāng)我們在滑動tableView時,倒計時就停止了斗搞,這又該怎么辦呢指攒?

場景中的代碼實現(xiàn)

我們的定時器Timer是怎么寫的呢? 一般的做法是僻焚,在主線程(可能是某控制器的viewDidLoad方法)中允悦,創(chuàng)建Timer。 可能會有兩種寫法溅呢,但是都有上面的問題澡屡,下面先看下Timer的兩種寫法:

// 第一種寫法
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[timer fire];
// 第二種寫法
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];

上面的兩種寫法其實是等價的猿挚。第二種寫法,默認(rèn)也是將timer添加到NSDefaultRunLoopMode下的驶鹉,并且會自動fire绩蜻。。 要驗證這一結(jié)論室埋,我們只需要在timerUpdate方法中办绝,將當(dāng)前runLoop的currentMode打印出來即可。

- (void)timerUpdate
{
  NSLog(@"當(dāng)前線程:%@",[NSThread currentThread]);
  NSLog(@"啟動RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
//   NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
  dispatch_async(dispatch_get_main_queue(), ^{
      self.count ++;
      NSString *timerText = [NSString stringWithFormat:@"計時器:%ld",self.count];
      self.timerLabel.text = timerText;
  });
}
// 控制臺輸出結(jié)果:
2016-12-02 15:33:57.829 RunLoopDemo02[6698:541533] 當(dāng)前線程:<NSThread: 0x600000065500>{number = 1, name = main}
2016-12-02 15:33:57.829 RunLoopDemo02[6698:541533] 啟動RunLoop后--kCFRunLoopDefaultMode

然后姚淆,我們在滑動tableView的時候timerUpdate方法孕蝉,并不會調(diào)用。 ** 原因是啥呢腌逢?** 原因是當(dāng)我們滑動scrollView時降淮,主線程的RunLoop會切換到UITrackingRunLoopMode這個Mode,執(zhí)行的也是UITrackingRunLoopMode下的任務(wù)(Mode中的item)搏讶,而timer是添加在NSDefaultRunLoopMode下的佳鳖,所以timer任務(wù)并不會執(zhí)行,只有當(dāng)UITrackingRunLoopMode的任務(wù)執(zhí)行完畢媒惕,runloop切換到NSDefaultRunLoopMode后系吩,才會繼續(xù)執(zhí)行timer。

** 要如何解決這一問題呢妒蔚?** 解決方法很簡單穿挨,我們只需要在添加timer 時,將mode 設(shè)置為NSRunLoopCommonModes即可肴盏。

- ( void)timerTest
{
  // 第一種寫法
  NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
  [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
  [timer fire];
  // 第二種寫法科盛,因為是固定添加到defaultMode中,就不要用了
}

RunLoop官方文檔iPhonedevwiki中的CFRunLoop可以看出叁鉴,NSRunLoopCommonModes并不是一種Mode土涝,而是一種特殊的標(biāo)記,關(guān)聯(lián)的有一個set幌墓,官方文檔說:For Cocoa applications, this set includes the default, modal, andevent tracking modes bydefault.(默認(rèn)包含NSDefaultRunLoopModeNSModalPanelRunLoopMode冀泻、NSEventTrackingRunLoopMode) 添加到NSRunLoopCommonModes中的還沒有執(zhí)行的任務(wù)常侣,會在mode切換時,再次添加到當(dāng)前的mode中弹渔,這樣就能保證不管當(dāng)前runloop切換到哪一個mode胳施,任務(wù)都能正常執(zhí)行。并且被添加到NSRunLoopCommonModes中的任務(wù)會存儲在runloop的commonModeItems中肢专。

其他一些關(guān)于timer的坑

我們在子線程中使用timer舞肆,也可以解決上面的問題焦辅,但是需要注意的是把timer加入到當(dāng)前runloop后,必須讓runloop運(yùn)行起來椿胯,否則timer僅執(zhí)行一次筷登。

示例代碼:

//首先是創(chuàng)建一個子線程
- (void)createThread
{
  NSThread *subThread = [[NSThread alloc] initWithTarget:self selector:@selector(timerTest) object:nil];
  [subThread start];
  self.subThread = subThread;
}
?
// 創(chuàng)建timer,并添加到runloop的mode中
- (void)timerTest
{
  @autoreleasepool {
      NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
      NSLog(@"啟動RunLoop前--%@",runLoop.currentMode);
      NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
      // 第一種寫法,改正前
  //   NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
  //   [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
  //   [timer fire];
      // 第二種寫法
        [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
?
      [[NSRunLoop currentRunLoop] run];
  }
}
?
//更新label
- (void)timerUpdate
{
  NSLog(@"當(dāng)前線程:%@",[NSThread currentThread]);
  NSLog(@"啟動RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);
  NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
  dispatch_async(dispatch_get_main_queue(), ^{
      self.count ++;
      NSString *timerText = [NSString stringWithFormat:@"計時器:%ld",self.count];
      self.timerLabel.text = timerText;
  });
}

添加timer 前的控制臺輸出:

727768-468c6aecb685bcad-2.png

添加timer后的控制臺輸出:

727768-9ab1df48dcb01b95.png.jpeg

從控制臺輸出可以看出哩盲,timer確實被添加到NSDefaultRunLoopMode中了前方。可是添加到子線程中的NSDefaultRunLoopMode里廉油,無論如何滾動惠险,timer都能夠很正常的運(yùn)轉(zhuǎn)。這又是為啥呢抒线?

這就是多線程與runloop的關(guān)系了班巩,每一個線程都有一個與之關(guān)聯(lián)的RunLoop,而每一個RunLoop可能會有多個Mode嘶炭。CPU會在多個線程間切換來執(zhí)行任務(wù)趣竣,呈現(xiàn)出多個線程同時執(zhí)行的效果。執(zhí)行的任務(wù)其實就是RunLoop去各個Mode里執(zhí)行各個item旱物。因為RunLoop是獨立的兩個遥缕,相互不會影響,所以在子線程添加timer宵呛,滑動視圖時,timer能正常運(yùn)行户秤。

總結(jié)

1鸡号、如果是在主線程中運(yùn)行timer须鼎,想要timer在某界面有視圖滾動時,依然能正常運(yùn)轉(zhuǎn)晋控,那么將timer添加到RunLoop中時汞窗,就需要設(shè)置mode為NSRunLoopCommonModes。 2赡译、如果是在子線程中運(yùn)行timer,那么將timer添加到RunLoop中后仲吏,Mode設(shè)置為NSDefaultRunLoopModeNSRunLoopCommonModes均可,但是需要保證RunLoop在運(yùn)行,且其中有任務(wù)裹唆。

三誓斥、UITableView井厌、UICollectionView等的滑動優(yōu)化

讓UITableView矩父、UICollectionView等延遲加載圖片。下面就拿UITableView來舉例說明: UITableView 的 cell 上顯示網(wǎng)絡(luò)圖片名船,一般需要兩步舞吭,第一步下載網(wǎng)絡(luò)圖片泡垃;第二步,將網(wǎng)絡(luò)圖片設(shè)置到UIImageView上羡鸥。 為了不影響滑動蔑穴,第一步,我們一般都是放在子線程中來做捐腿,這個不做贅述。 第二步宪祥,一般是回到主線程去設(shè)置。有了前兩節(jié)文章關(guān)于Mode的切換耀找,想必你已經(jīng)知道怎么做了。 就是在為圖片視圖設(shè)置圖片時复罐,在主線程設(shè)置,并調(diào)用performSelector:withObject:afterDelay:inModes:方法乱投。最后一個參數(shù),僅設(shè)置一個NSDefaultRunLoopMode双肤。

UIImage *downloadedImage = ....;
[self.myImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

當(dāng)然,即使是讀取沙盒或者bundle內(nèi)的圖片蔑赘,我們也可以運(yùn)用這一點來改善視圖的滑動。但是如果UITableView上的圖片都是默認(rèn)圖酥馍,似乎也不是很好,你需要自己來權(quán)衡了峦失。

有一個非常好的關(guān)于設(shè)置圖片視圖的圖片,在RunLoop切換Mode時優(yōu)化的例子:RunLoopWorkDistribution 先看一下界面布局:

image

一個Cell里有兩個Label,和三個imageView,這里的圖片是非常高清的(2034 ×1525)购啄,一個界面最多有18張圖片狮含。為了表現(xiàn)出卡頓的效果,我先自己實現(xiàn)了一下Cell,主要示例代碼:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  static NSString *identifier = @"cellId";
  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
  if (cell == nil) {
      cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
  }
  for (NSInteger i = 1; i <= 5; i++) {
      [[cell.contentView viewWithTag:i] removeFromSuperview];
  }
?
  UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(5, 5, 300, 25)];
  label.backgroundColor = [UIColor clearColor];
  label.textColor = [UIColor redColor];
  label.text = [NSString stringWithFormat:@"%zd - Drawing index is top priority", indexPath.row];
  label.font = [UIFont boldSystemFontOfSize:13];
  label.tag = 1;
  [cell.contentView addSubview:label];
?
  UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(105, 20, 85, 85)];
  imageView.tag = 2;
  NSString *path = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"jpg"];
  UIImage *image = [UIImage imageWithContentsOfFile:path];
  imageView.contentMode = UIViewContentModeScaleAspectFit;
  imageView.image = image;
  NSLog(@"current:%@",[NSRunLoop currentRunLoop].currentMode);
  [cell.contentView addSubview:imageView];
?
  UIImageView *imageView2 = [[UIImageView alloc] initWithFrame:CGRectMake(200, 20, 85, 85)];
  imageView2.tag = 3;
  UIImage *image2 = [UIImage imageWithContentsOfFile:path];
  imageView2.contentMode = UIViewContentModeScaleAspectFit;
  imageView2.image = image2;
  [cell.contentView addSubview:imageView2];
?
  UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectMake(5, 99, 300, 35)];
  label2.lineBreakMode = NSLineBreakByWordWrapping;
  label2.numberOfLines = 0;
  label2.backgroundColor = [UIColor clearColor];
  label2.textColor = [UIColor colorWithRed:0 green:100.f/255.f blue:0 alpha:1];
  label2.text = [NSString stringWithFormat:@"%zd - Drawing large image is low priority. Should be distributed into different run loop passes.", indexPath.row];
  label2.font = [UIFont boldSystemFontOfSize:13];
  label2.tag = 4;
?
  UIImageView *imageView3 = [[UIImageView alloc] initWithFrame:CGRectMake(5, 20, 85, 85)];
  imageView3.tag = 5;
  UIImage *image3 = [UIImage imageWithContentsOfFile:path];
  imageView3.contentMode = UIViewContentModeScaleAspectFit;
  imageView3.image = image3;
  [cell.contentView addSubview:label2];
  [cell.contentView addSubview:imageView3];
?
  return cell;
}

然后在滑動的時候,順便打印出當(dāng)前的runloopMode坑填,打印結(jié)果是:

2016-12-08 10:34:31.450 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:31.701 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:32.184 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:36.317 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:36.601 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:37.217 TestDemo[3202:1791817] current:UITrackingRunLoopMode

可以看出,為imageView設(shè)置image,是在UITrackingRunLoopMode中進(jìn)行的,如果圖片很大忌穿,圖片解壓縮和渲染肯定會很耗時,那么卡頓就是必然的朴译。

查看實時幀率眠寿,我們可以在Xcode 中選擇真機(jī)調(diào)試,然后 Product –>Profile–>Core Animation

image

然后點擊開始監(jiān)測即可:
image

下面就是幀率:

image

這里就可以使用先使用上面的方式做一次改進(jìn)。

[imageView performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

可以保證在滑動起來順暢,可是停下來之后雀彼,渲染還未完成時仍律,繼續(xù)滑動就會變的卡頓。 在切換到NSDefaultRunLoopMode中窒盐,一個runloop循環(huán)要解壓和渲染18張大圖,耗時肯定超過50ms(1/60s)葡粒。 我們可以繼續(xù)來優(yōu)化,一次runloop循環(huán)夫壁,僅渲染一張大圖片,分18次來渲染邑茄,這樣每一次runloop耗時就比較短了,滑動起來就會非常順暢搓谆。這也是RunLoopWorkDistribution中的做法泉手。 簡單描述一下這種做法: 首先創(chuàng)建一個單例缝裤,單例中定義了幾個數(shù)組,用來存要在runloop循環(huán)中執(zhí)行的任務(wù)榛做,然后為主線程的runloop添加一個CFRunLoopObserver,當(dāng)主線程在NSDefaultRunLoopMode中執(zhí)行完任務(wù),即將睡眠前锰瘸,執(zhí)行一個單例中保存的一次圖片渲染任務(wù)。關(guān)鍵代碼看DWURunLoopWorkDistribution類即可管削。

一點UITableView滑動性能優(yōu)化擴(kuò)展

影響UITableView的滑動,有哪些因素呢茸俭? 關(guān)于這一點,人眼能識別的幀率是60左右腾窝,這也就是為什么,電腦屏幕的最佳幀率是60Hz循集。 屏幕一秒鐘會刷新60次(屏幕在一秒鐘會重新渲染60次)疆柔,那么每次刷新界面之間的處理時間,就是1/60,也就是1/60秒鞋屈。也就是說,所有會導(dǎo)致計算、渲染耗時的操作都會影響UITableView的流暢评也。下面舉例說明:

1.在主線程中做耗時操作 耗時操作,包括從網(wǎng)絡(luò)下載罚缕、從網(wǎng)絡(luò)加載、從本地數(shù)據(jù)庫讀取數(shù)據(jù)腌乡、從本地文件中讀取大量數(shù)據(jù)、往本地文件中寫入數(shù)據(jù)等急迂。(這一點,相信大家都知道听盖,要盡量避免在主線程中執(zhí)行仓坞,一般都是創(chuàng)建一個子線程來執(zhí)行,然后再回到主線程)

2.動態(tài)計算UITableViewCell的高度嫉称,時間過久 在iOS7之前,每一個Cell的高度荔棉,只會計算一次,后面再次滑到這個Cell這里壹若,都會讀取緩存的高度,也即高度計算的代理方法不會再執(zhí)行壁查。但是到了iOS8,不會再緩存Cell的高度了,也就是說每次滑到某個Cell席怪,代理方法都會執(zhí)行一次,重新計算這個Cell的高度(iOS9以后沒測試過)刻撒。 所以态贤,如果計算Cell高度的這個過程過于復(fù)雜,或者某個計算使用的算法耗時很長柿冲,可能會導(dǎo)致計算時間大于1/60,那么必然導(dǎo)致界面的卡頓慨亲,或不流暢巴刻。

關(guān)于這一點沥寥,我以前的做法是在Cell中定義一個public方法,用來計算Cell高度淮野,然后計算完高度后,將高度存儲在Cell對應(yīng)的Model中(Model里定義一個屬性來存高度)洞难,然后在渲染Cell時队贱,我們依然需要動態(tài)計算各個子視圖的高度锋恬。(可能是沒用什么太過復(fù)雜的計算或算法,時間都很短滑動也順暢)

其實癣防,更優(yōu)的做法是:再定義一個ModelFrame對象,在子線程請求服務(wù)器接口返回后级遭,轉(zhuǎn)換為對象的同時挫鸽,也把各個子視圖的frame計算好,存在ModelFrame中,ModelFrame和 Model 合并成一個Model存儲到數(shù)組中干茉。這樣在為Cell各個子控件賦值時,僅僅是取值上遥、賦值粉楚,在計算Cell高度時,也僅僅是加法運(yùn)算携狭。

3.界面中背景色透明的視圖過多 為什么界面中背景色透明的視圖過多會影響UITableView的流暢?

很多文章中都提到单默,可以使用模擬器—>Debug—>Color BlendedLayers來檢測透明背景色,把透明背景色改為與父視圖背景色一樣的顏色境蜕,這樣來提高渲染速度粱年。

image

簡單說明一下舟舒,就是屏幕上顯示的所有東西,都是通過一個個像素點呈現(xiàn)出來的夺鲜。而每一個像素點都是通過三原色(紅、綠、藍(lán))組合呈現(xiàn)出不同的顏色仅胞,最終才是我們看到的手機(jī)屏幕上的內(nèi)容。在iPhone5 的液晶顯示器上有1,136×640=727,040個像素挠将,因此有2,181,120個顏色單元。在15寸視網(wǎng)膜屏的 MacBook Pro上镶蹋,這一數(shù)字達(dá)到15.5百萬以上。所有的圖形堆棧一起工作以確保每次正確的顯示。當(dāng)你滾動整個屏幕的時候婶熬,數(shù)以百萬計的顏色單元必須以每秒60次的速度刷新,這是一個很大的工作量饺谬。

每一個像素點的顏色計算是這樣的: R = S + D * (1 - Sa) 結(jié)果的顏色 是子視圖這個像素點的顏色 + 父視圖這個像素點的顏色 * (1 - 子視圖的透明度) 當(dāng)然,如果有兩個兄弟視圖疊加拔鹰,那么上面的中文解釋可能并不貼切恰画,只是為了更容易理解。

如果兩個兄弟視圖重合,計算的是重合區(qū)域的像素點: 結(jié)果的顏色 是 上面的視圖這個像素點的顏色 + 下面這個視圖該像素點的顏色 * (1 - 上面視圖的透明度)

只有當(dāng)透明度為1時,上面的公式變?yōu)镽 = S孝偎,就簡單的多了爷抓。否則的話蓝撇,就非常復(fù)雜了果复。 每一個像素點是由三原色組成,例如父視圖的顏色和透明度是(Pr,Pg,Pb,Pa)渤昌,子視圖的顏色顏色和透明度是(Sr,Sg,Sb,Sa)虽抄,那么我們計算這個重合區(qū)域某像素點的顏色,需要先分別計算出紅独柑、綠、藍(lán)清女。 Rr = Sr + Pr * (1 - Sa)怜瞒, Rg = Sg + Pg * (1 - Sa)楞卡, Rb = Sb + Pb * (1 - Sa)。 如果父視圖的透明度,即Pa = 1,那么這個像素的顏色就是(Rr,Rg,Rb)蹈丸。 但是,如果父視圖的透明Pa 不等 1女嘲,那么我們需要將這個結(jié)果顏色當(dāng)做一個整體作為子視圖的顏色顶别,再去與父視圖組合計算顏色拴孤,如此遞推化漆。

所以設(shè)置不透明時厌衔,可以為GPU節(jié)省大量的工作,減少大量的消耗赃梧。

更加詳細(xì)的說明巷折,可以看繪制像素到屏幕上這篇文章,這是一篇關(guān)于繪制像素的非常棒

使用TableView時出現(xiàn)的問題:

平時開發(fā)中繪制 tableView 時,我們使用的 cell 可能包含很多業(yè)務(wù)邏輯叶洞,比如加載網(wǎng)絡(luò)圖片仅财、繪制內(nèi)容等等。如果我們不進(jìn)行優(yōu)化的話,在繪制cell 時這些任務(wù)將同時爭奪系統(tǒng)資源,最直接的后果就是頁面出現(xiàn)卡頓,更嚴(yán)重的則會 crash霜瘪。 我通過在 cell 上加載大的圖片(找的系統(tǒng)的壁紙徙歼,大小10M左右)并改變其大小來模擬 cell 的復(fù)雜業(yè)務(wù)邏輯考润。

cell 的繪制方法中實現(xiàn)如下:

CGFloat width = (self.view.bounds.size.width-4*kBorder_W) /3;
?
UIImageView *img1 = [[UIImageView alloc] initWithFrame:CGRectMake(kBorder_W,
                                                                kBorder_W,
                                                                width,
                                                                kCell_H-kBorder_W)];
img1.image = [UIImage imageNamed:@"Blue Pond.jpg"];
[cell addSubview:img1];
UIImageView *img2 = [[UIImageView alloc] initWithFrame:CGRectMake(width+2*kBorder_W,
                                                                kBorder_W,
                                                                width,
                                                                kCell_H-kBorder_W)];
img2.image = [UIImage imageNamed:@"El Capitan 2.jpg"];
[cell addSubview:img2];
UIImageView *img3 = [[UIImageView alloc] initWithFrame:CGRectMake(2*width+3*kBorder_W,
                                                                kBorder_W,
                                                                width,
                                                                kCell_H-kBorder_W)];
img3.image = [UIImage imageNamed:@"El Capitan.jpg"];
[cell addSubview:img3];

tableView 在繪制 cell 的時候同時處理這么多資源纤怒,會導(dǎo)致頁面滑動不流暢等問題。此處只是模擬囊榜,可能效果不明顯,但這都不是重點~

微信對cell 的優(yōu)化方案是當(dāng)監(jiān)聽到列表滾動時,停止 cell 上的動畫等方式,來提升用戶體驗营袜。

Q:那么問題來了,這個監(jiān)聽是怎么做到的呢筒饰?

  • 一種是通過 scrollViewdelegate 方法;

  • 另一種就是通過監(jiān)聽 runLoop

如果有其他方案兔仰,歡迎告知~

二、下面就分享下通過監(jiān)聽RunLoop來優(yōu)化TableView:

步驟如下:

(1).獲取當(dāng)前主線程的 runloop

CFRunLoopRef runloop = CFRunLoopGetCurrent();

(2).創(chuàng)建觀察者 CFRunLoopObserverRef 丈冬, 來監(jiān)聽 runloop 槐脏。

  • 創(chuàng)建觀察者用到的核心函數(shù)就是 CFRunLoopObserverCreate
 // allocator:該參數(shù)為對象內(nèi)存分配器侵贵,一般使用默認(rèn)的分配器kCFAllocatorDefault辽旋。
// activities:要監(jiān)聽runloop的狀態(tài)
/*typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入Loop 
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒 
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有事件 };*/
// repeats:是否重復(fù)監(jiān)聽
// order:觀察者優(yōu)先級浩嫌,當(dāng)Run Loop中有多個觀察者監(jiān)聽同一個運(yùn)行狀態(tài)時,根據(jù)該優(yōu)先級判斷补胚,0為最高優(yōu)先級別码耐。
// callout:觀察者的回調(diào)函數(shù),在Core Foundation框架中用CFRunLoopObserverCallBack重定義了回調(diào)函數(shù)的閉包溶其。
// context:觀察者的上下文骚腥。
CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
a).創(chuàng)建觀察者
// 1.定義上下文
CFRunLoopObserverContext context = {
  0,
  (__bridge void *)(self),
  &CFRetain,
  &CFRelease,
  NULL
};
// 2.定義觀察者
static CFRunLoopObserverRef defaultModeObserver;
// 3.創(chuàng)建觀察者
defaultModeObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                            kCFRunLoopBeforeWaiting,
                                            YES,
                                            0,
                                            &callBack,
                                            &context);
// 4.給當(dāng)前runloop添加觀察者
// kCFRunLoopDefaultMode: App的默認(rèn) Mode,通常主線程是在這個 Mode 下運(yùn)行的瓶逃。
// UITrackingRunLoopMode: 界面跟蹤 Mode束铭,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響厢绝。
// UIInitializationRunLoopMode: 在剛啟動 App 時進(jìn)入的第一個 Mode契沫,啟動完成后就不再使用。
// GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode昔汉,通常用不到懈万。
// kCFRunLoopCommonModes: 這是一個占位的 Mode,沒有實際作用靶病。
CFRunLoopAddObserver(runloop, defaultModeObserver, kCFRunLoopCommonModes);
// 5.內(nèi)存管理
CFRelease(defaultModeObserver);
b).實現(xiàn) callBack 函數(shù)会通,只要檢測到對應(yīng)的runloop狀態(tài),該函數(shù)就會得到響應(yīng)娄周。
static void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {

  ViewController *vc = (__bridge ViewController *)info;

  //無任務(wù) 退出
  if (vc.tasksArr.count == 0) return;

  //從數(shù)組中取出任務(wù)
  runloopBlock block = [vc.tasksArr firstObject];

  //執(zhí)行任務(wù)
  if (block) {
      block();
  }

  //執(zhí)行完任務(wù)之后移除任務(wù)
  [vc.tasksArr removeObjectAtIndex:0];

}
c).從上面的函數(shù)實現(xiàn)中我們看到了block渴语、arr等對象,下面解析下:
  • 使用 Array 來存儲需要執(zhí)行的任務(wù)昆咽;
- (NSMutableArray *)tasksArr {
    if (!_tasksArr) {
        _tasksArr = [NSMutableArray array];
    }
    return _tasksArr;
}
  • 定義參數(shù) maxTaskCount 來表示最大任務(wù)數(shù)驾凶,優(yōu)化項目牙甫;

    //最大任務(wù)數(shù)@property (nonatomic, assign) NSUInteger maxTaskCount;...// 當(dāng)超出最大任務(wù)數(shù)時,以前的老任務(wù)將從數(shù)組中移除self.maxTaskCount = 50;

  • 使用 block代碼塊 來包裝一個個將要執(zhí)行的任務(wù)调违,便于 callBack 函數(shù)中分開執(zhí)行任務(wù)窟哺,減少同時執(zhí)行對系統(tǒng)資源的消耗。

    //1. 定義一個任務(wù)blocktypedef void(^runloopBlock)();

    //2. 定義一個添加任務(wù)的方法技肩,將任務(wù)裝在數(shù)組中

// 添加任務(wù)
- (void)addTasks:(runloopBlock)task {
//    NSLog(@"%d",_maxTaskCount);
//     保存新任務(wù)
    [self.tasksArr addObject:task];
    // 如果超出最大任務(wù)數(shù) 丟棄之前的任務(wù)
    if (self.tasksArr.count > _maxTaskCount) {
        [self.tasksArr removeObjectAtIndex:0];
    }
}
//3\. 將任務(wù)添加到代碼塊中
    // 耗時操作可以放在任務(wù)中
    [self addTasks:^{
        UIImageView *img1 = [[UIImageView alloc] initWithFrame:CGRectMake(kBorder_W,
                                                                          kBorder_W,
                                                                          width,
                                                                          kCell_H-kBorder_W)];
        img1.image = [UIImage imageNamed:@"Blue Pond.jpg"];
        [cell addSubview:img1];
        CFRunLoopWakeUp(CFRunLoopGetCurrent());
    }];
    [self addTasks:^{
        UIImageView *img2 = [[UIImageView alloc] initWithFrame:CGRectMake(width+2*kBorder_W,
                                                                          kBorder_W,
                                                                          width,
                                                                          kCell_H-kBorder_W)];
        img2.image = [UIImage imageNamed:@"El Capitan 2.jpg"];
        [cell addSubview:img2];
        CFRunLoopWakeUp(CFRunLoopGetCurrent());
    }];
    [self addTasks:^{
        UIImageView *img3 = [[UIImageView alloc] initWithFrame:CGRectMake(2*width+3*kBorder_W,
                                                                          kBorder_W,
                                                                          width,
                                                                          kCell_H-kBorder_W)];
        img3.image = [UIImage imageNamed:@"El Capitan.jpg"];
        [cell addSubview:img3];
        CFRunLoopWakeUp(CFRunLoopGetCurrent());
    }];

(3).使 runloop 不進(jìn)入休眠狀態(tài)且轨。

Q:按照上面步驟實現(xiàn)的情況下:我有500行的cell,為什么才顯示這么一點點呢虚婿?
3265534-c18a92fbb80f6e7c.png
A:runloop 在加載完 cell 時沒有其他事情做了旋奢,為了節(jié)省資源消耗,就進(jìn)入了休眠狀態(tài)然痊,等待有任務(wù)時再次被喚醒至朗。在我們觀察者的

callBack 函數(shù)中任務(wù)被一個個取出執(zhí)行,還沒有執(zhí)行完剧浸,runloop 就切換狀態(tài)了(休眠了)锹引, callBack函數(shù)不再響應(yīng)。導(dǎo)致出現(xiàn)上面的情況唆香。

解決方法:
//創(chuàng)建定時器 (保證runloop回調(diào)函數(shù)一直在執(zhí)行)
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self
                                                        selector:@selector(notDoSomething)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

- (void)notDoSomething {
  // 不做事情,就是為了讓 callBack() 函數(shù)一直相應(yīng)
}
3265534-45f8cc797ee8d2c4.gif

參考資料

關(guān)于今天要介紹的使用RunLoop 監(jiān)測主線程卡頓的資料如下:

原理

官方文檔說明了RunLoop的執(zhí)行順序:

1\. Notify observers that the run loop has been entered.
2\. Notify observers that any ready timers are about to fire.
3\. Notify observers that any input sources that are not port based are about to fire.
4\. Fire any non-port-based input sources that are ready to fire.
5\. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
6\. Notify observers that the thread is about to sleep.
7\. Put the thread to sleep until one of the following events occurs:
* An event arrives for a port-based input source.
* A timer fires.
* The timeout value set for the run loop expires.
* The run loop is explicitly woken up.
8\. Notify observers that the thread just woke up.
9\. Process the pending event.
* If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.
* If an input source fired, deliver the event.
* If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
10\. Notify observers that the run loop has exited.

用偽代碼來實現(xiàn)就是這樣的:

{
  /// 1\. 通知Observers躬它,即將進(jìn)入RunLoop
  /// 此處有Observer會創(chuàng)建AutoreleasePool: _objc_autoreleasePoolPush();
  __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
  do {
?
      /// 2\. 通知 Observers: 即將觸發(fā) Timer 回調(diào)腾啥。
      __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
      /// 3\. 通知 Observers: 即將觸發(fā) Source (非基于port的,Source0) 回調(diào)。
      __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
      __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
?
      /// 4\. 觸發(fā) Source0 (非基于port的) 回調(diào)冯吓。
      __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
      __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
?
      /// 6\. 通知Observers倘待,即將進(jìn)入休眠
      /// 此處有Observer釋放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
      __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
?
      /// 7\. sleep to wait msg.
      mach_msg() -> mach_msg_trap();

    /// 8\. 通知Observers,線程被喚醒
      __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
?
      /// 9\. 如果是被Timer喚醒的桑谍,回調(diào)Timer
      __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
?
      /// 9\. 如果是被dispatch喚醒的,執(zhí)行所有調(diào)用 dispatch_async 等方法放入main queue 的 block
      __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
?
      /// 9\. 如果如果Runloop是被 Source1 (基于port的) 的事件喚醒了祸挪,處理這個事件
      __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);

 } while (...);
?
  /// 10\. 通知Observers锣披,即將退出RunLoop
  /// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
  __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

主線程的RunLoop是在應(yīng)用啟動時自動開啟的,也沒有超時時間贿条,所以正常情況下雹仿,主線程的RunLoop 只會在 2—9 之間無限循環(huán)下去。 那么整以,我們只需要在主線程的RunLoop中添加一個observer胧辽,檢測從 kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting 花費(fèi)的時間是否過長。如果花費(fèi)的時間大于某一個闕值公黑,我們就認(rèn)為有卡頓邑商,并把當(dāng)前的線程堆棧轉(zhuǎn)儲到文件中摄咆,并在以后某個合適的時間,將卡頓信息文件上傳到服務(wù)器人断。

實現(xiàn)步驟

在看了上面的兩個監(jiān)測卡頓的示例Demo后吭从,我按照上面講述的思路寫了一個Demo,應(yīng)該更容易理解吧恶迈。 第一步涩金,創(chuàng)建一個子線程,在線程啟動時暇仲,啟動其RunLoop步做。

+ (instancetype)shareMonitor
{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
      instance = [[[self class] alloc] init];
      instance.monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(monitorThreadEntryPoint) object:nil];
      [instance.monitorThread start];
  });
?
  return instance;
}
?
+ (void)monitorThreadEntryPoint
{
  @autoreleasepool {
      [[NSThread currentThread] setName:@"FluencyMonitor"];
      NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
      [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
      [runLoop run];
  }
}

第二步,在開始監(jiān)測時奈附,往主線程的RunLoop中添加一個observer全度,并往子線程中添加一個定時器,每0.5秒檢測一次耗時的時長桅狠。

- (void)start
{
  if (_observer) {
      return;
  }
?
  // 1.創(chuàng)建observer
  CFRunLoopObserverContext context = {0,(__bridge void*)self, NULL, NULL, NULL};
  _observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                            kCFRunLoopAllActivities,
                                            YES,
                                            0,
                                            &runLoopObserverCallBack,
                                            &context);
  // 2.將observer添加到主線程的RunLoop中
  CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
?
  // 3.創(chuàng)建一個timer讼载,并添加到子線程的RunLoop中
  [self performSelector:@selector(addTimerToMonitorThread) onThread:self.monitorThread withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];
}
?
- (void)addTimerToMonitorThread
{
  if (_timer) {
      return;
  }
  // 創(chuàng)建一個timer
  CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
  CFRunLoopTimerContext context = {0, (__bridge void*)self, NULL, NULL, NULL};
  _timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.01, 0, 0,
                                                  &runLoopTimerCallBack, &context);
  // 添加到子線程的RunLoop中
  CFRunLoopAddTimer(currentRunLoop, _timer, kCFRunLoopCommonModes);
}

第三步,補(bǔ)充觀察者回調(diào)處理

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
  FluencyMonitor *monitor = (__bridge FluencyMonitor*)info;
  NSLog(@"MainRunLoop---%@",[NSThread currentThread]);
  switch (activity) {
      case kCFRunLoopEntry:
          NSLog(@"kCFRunLoopEntry");
          break;
      case kCFRunLoopBeforeTimers:
          NSLog(@"kCFRunLoopBeforeTimers");
          break;
      case kCFRunLoopBeforeSources:
          NSLog(@"kCFRunLoopBeforeSources");
          monitor.startDate = [NSDate date];
          monitor.excuting = YES;
          break;
      case kCFRunLoopBeforeWaiting:
          NSLog(@"kCFRunLoopBeforeWaiting");
          monitor.excuting = NO;
          break;
      case kCFRunLoopAfterWaiting:
          NSLog(@"kCFRunLoopAfterWaiting");
          break;
      case kCFRunLoopExit:
          NSLog(@"kCFRunLoopExit");
          break;
      default:
          break;
  }
}

從打印信息來看中跌,RunLoop進(jìn)入睡眠狀態(tài)的時間可能會非常短咨堤,有時候只有1毫秒,有時候甚至1毫秒都不到漩符,靜止不動時一喘,則會長時間進(jìn)入睡覺狀態(tài)。

因為主線程中的block嗜暴、交互事件凸克、以及其他任務(wù)都是在kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting 之前執(zhí)行,所以我在即將開始執(zhí)行Sources時闷沥,記錄一下時間萎战,并把正在執(zhí)行任務(wù)的標(biāo)記置為YES,將要進(jìn)入睡眠狀態(tài)時舆逃,將正在執(zhí)行任務(wù)的標(biāo)記置為NO蚂维。

第四步,補(bǔ)充timer 的回調(diào)處理

static void runLoopTimerCallBack(CFRunLoopTimerRef timer, void *info)
{
  FluencyMonitor *monitor = (__bridge FluencyMonitor*)info;
  if (!monitor.excuting) {
      return;
  }
?
  // 如果主線程正在執(zhí)行任務(wù)路狮,并且這一次loop 執(zhí)行到 現(xiàn)在還沒執(zhí)行完虫啥,那就需要計算時間差
  NSTimeInterval excuteTime = [[NSDate date] timeIntervalSinceDate:monitor.startDate];
  NSLog(@"定時器---%@",[NSThread currentThread]);
  NSLog(@"主線程執(zhí)行了---%f秒",excuteTime);
?
  if (excuteTime >= 0.01) {
      NSLog(@"線程卡頓了%f秒",excuteTime);
      [monitor handleStackInfo];
  }
}

timer 每 0.01秒執(zhí)行一次,如果當(dāng)前正在執(zhí)行任務(wù)的狀態(tài)為YES奄妨,并且從開始執(zhí)行到現(xiàn)在的時間大于闕值涂籽,則把堆棧信息保存下來,便于后面處理砸抛。 為了能夠捕獲到堆棧信息评雌,我把timer的間隔調(diào)的很惺鞣恪(0.01),而評定為卡頓的闕值也調(diào)的很辛尽(0.01)团赏。實際使用時這兩個值應(yīng)該是比較大,timer間隔為1s耐薯,卡頓闕值為2s即可舔清。

2016-12-15 08:56:39.921 RunLoopDemo03[957:16300] lag happen, detail below: 
Incident Identifier: 68BAB24C-3224-46C8-89BF-F9AABA2E3530
CrashReporter Key:   TODO
Hardware Model:     x86_64
Process:         RunLoopDemo03 [957]
Path:           /Users/harvey/Library/Developer/CoreSimulator/Devices/6ED39DBB-9F69-4ACB-9CE3-E6EB56BBFECE/data/Containers/Bundle/Application/5A94DEFE-4E2E-4D23-9F69-7B1954B2C960/RunLoopDemo03.app/RunLoopDemo03
Identifier:     com.Haley.RunLoopDemo03
Version:         1.0 (1)
Code Type:       X86-64
Parent Process: debugserver [958]
?
Date/Time:       2016-12-15 00:56:38 +0000
OS Version:     Mac OS X 10.1 (16A323)
Report Version: 104
?
Exception Type: SIGTRAP
Exception Codes: TRAP_TRACE at 0x1063da728
Crashed Thread: 4
?
Thread 0:
0   libsystem_kernel.dylib             0x000000010a14341a mach_msg_trap + 10
1   CoreFoundation                     0x0000000106f1e7b4 __CFRunLoopServiceMachPort + 212
2   CoreFoundation                     0x0000000106f1dc31 __CFRunLoopRun + 1345
3   CoreFoundation                     0x0000000106f1d494 CFRunLoopRunSpecific + 420
4   GraphicsServices                   0x000000010ad8aa6f GSEventRunModal + 161
5   UIKit                               0x00000001073b7964 UIApplicationMain + 159
6   RunLoopDemo03                       0x00000001063dbf8f main + 111
7   libdyld.dylib                       0x0000000109d7468d start + 1
?
Thread 1:
0   libsystem_kernel.dylib             0x000000010a14be5e kevent_qos + 10
1   libdispatch.dylib                   0x0000000109d13074 _dispatch_mgr_invoke + 248
2   libdispatch.dylib                   0x0000000109d12e76 _dispatch_mgr_init + 0
?
Thread 2:
0   libsystem_kernel.dylib             0x000000010a14b4e6 __workq_kernreturn + 10
1   libsystem_pthread.dylib             0x000000010a16e221 start_wqthread + 13
?
Thread 3:
0   libsystem_kernel.dylib             0x000000010a14341a mach_msg_trap + 10
1   CoreFoundation                     0x0000000106f1e7b4 __CFRunLoopServiceMachPort + 212
2   CoreFoundation                     0x0000000106f1dc31 __CFRunLoopRun + 1345
3   CoreFoundation                     0x0000000106f1d494 CFRunLoopRunSpecific + 420
4   Foundation                         0x00000001064d7ff0 -[NSRunLoop runMode:beforeDate:] + 274
5   Foundation                         0x000000010655f991 -[NSRunLoop runUntilDate:] + 78
6   UIKit                               0x0000000107e3d539 -[UIEventFetcher threadMain] + 118
7   Foundation                         0x00000001064e7ee4 __NSThread__start__ + 1243
8   libsystem_pthread.dylib             0x000000010a16eabb _pthread_body + 180
9   libsystem_pthread.dylib             0x000000010a16ea07 _pthread_body + 0
10 libsystem_pthread.dylib             0x000000010a16e231 thread_start + 13
?
Thread 4 Crashed:
0   RunLoopDemo03                       0x00000001063dfae5 -[PLCrashReporter generateLiveReportWithThread:error:] + 632
1   RunLoopDemo03                       0x00000001063da728 -[FluencyMonitor handleStackInfo] + 152
2   RunLoopDemo03                       0x00000001063da2cf runLoopTimerCallBack + 351
3   CoreFoundation                     0x0000000106f26964 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 20
4   CoreFoundation                     0x0000000106f265f3 __CFRunLoopDoTimer + 1075
5   CoreFoundation                     0x0000000106f2617a __CFRunLoopDoTimers + 250
6   CoreFoundation                     0x0000000106f1df01 __CFRunLoopRun + 2065
7   CoreFoundation                     0x0000000106f1d494 CFRunLoopRunSpecific + 420
8   Foundation                         0x00000001064d7ff0 -[NSRunLoop runMode:beforeDate:] + 274
9   Foundation                         0x00000001064d7ecb -[NSRunLoop run] + 76
10 RunLoopDemo03                       0x00000001063d9cbd +[FluencyMonitor monitorThreadEntryPoint] + 253
11 Foundation                         0x00000001064e7ee4 __NSThread__start__ + 1243
12 libsystem_pthread.dylib             0x000000010a16eabb _pthread_body + 180
13 libsystem_pthread.dylib             0x000000010a16ea07 _pthread_body + 0
14 libsystem_pthread.dylib             0x000000010a16e231 thread_start + 13
?
Thread 4 crashed with X86-64 Thread State:
  rip: 0x00000001063dfae5   rbp: 0x000070000f53fc50   rsp: 0x000070000f53f9c0   rax: 0x000070000f53fa20 
  rbx: 0x000070000f53fb60   rcx: 0x0000000000005e0b   rdx: 0x0000000000000000   rdi: 0x00000001063dfc6a 
  rsi: 0x000070000f53f9f0     r8: 0x0000000000000014     r9: 0xffffffffffffffec   r10: 0x000000010a1433f6 
  r11: 0x0000000000000246   r12: 0x000060800016b580   r13: 0x0000000000000000   r14: 0x0000000000000006 
  r15: 0x000070000f53fa40 rflags: 0x0000000000000206     cs: 0x000000000000002b     fs: 0x0000000000000000 
  gs: 0x0000000000000000 

剩下的工作就是將字符串保存進(jìn)文件,以及上傳到服務(wù)器了曲初。

我們不能將卡頓的闕值定的太小体谒,也不能將所有的卡頓信息都上傳,原因有兩點臼婆,一抒痒,太浪費(fèi)用戶流量;二颁褂、文件太多故响,App內(nèi)存儲和上傳后服務(wù)器端保存都會占用空間。

可以參考微信的做法颁独,7天以上的文件刪除彩届,隨機(jī)抽取上傳,并且上傳前對文件進(jìn)行壓縮處理等誓酒。

Crash 收集資料

原理

iOS應(yīng)用崩潰靠柑,常見的崩潰信息有EXC_BAD_ACCESS寨辩、SIGABRTXXXXXXX,而這里分為兩種情況,一種是未被捕獲的異常歼冰,我們只需要添加一個回調(diào)函數(shù)靡狞,并在應(yīng)用啟動時調(diào)用一個 API即可;另一種是直接發(fā)送的SIGABRT XXXXXXX,這里我們也需要監(jiān)聽各種信號隔嫡,然后添加回調(diào)函數(shù)甸怕。

針對情況一,其實我們都見過畔勤。我們在收集App崩潰信息時蕾各,需要添加一個函數(shù)NSSetUncaughtExceptionHandler(&HandleException)扒磁,參數(shù)是一個回調(diào)函數(shù)庆揪,在回調(diào)函數(shù)里獲取到異常的原因,當(dāng)前的堆棧信息等保存到 dump文件妨托,然后供下次打開App時上傳到服務(wù)器缸榛。

其實吝羞,我們在HandleException回調(diào)函數(shù)中,可以獲取到當(dāng)前的RunLoop内颗,然后獲取該RunLoop中的所有Mode钧排,手動運(yùn)行一遍。

針對情況二均澳,首先針對多種要捕獲的信號恨溜,設(shè)置好回調(diào)函數(shù),然后也是在回調(diào)函數(shù)中獲取RunLoop找前,然后拿到所有的Mode糟袁,手動運(yùn)行一遍。

代碼實現(xiàn)

第一步躺盛,我創(chuàng)建了一個處理類项戴,并添加一個單例方法。(代碼見末尾的Demo)

第二步槽惫,在單例中對象實例化時周叮,添加 異常捕獲 和 signal 處理的 回調(diào)函數(shù)。

- (void)setCatchExceptionHandler
{
  // 1.捕獲一些異常導(dǎo)致的崩潰
  NSSetUncaughtExceptionHandler(&HandleException);

  // 2.捕獲非異常情況界斜,通過signal傳遞出來的崩潰
  signal(SIGABRT, SignalHandler);
  signal(SIGILL, SignalHandler);
  signal(SIGSEGV, SignalHandler);
  signal(SIGFPE, SignalHandler);
  signal(SIGBUS, SignalHandler);
  signal(SIGPIPE, SignalHandler);
}

第三步仿耽,分別實現(xiàn) 異常捕獲的回調(diào) 和 signal 的回調(diào)。

void HandleException(NSException *exception)
{
  // 獲取異常的堆棧信息
  NSArray *callStack = [exception callStackSymbols];
  NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
  [userInfo setObject:callStack forKey:kCaughtExceptionStackInfoKey];

  CrashHandler *crashObject = [CrashHandler sharedInstance];
  NSException *customException = [NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo];
  [crashObject performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
}
?
void SignalHandler(int signal)
{
  // 這種情況的崩潰信息锄蹂,就另某他法來捕獲吧
  NSArray *callStack = [CrashHandler backtrace];
  NSLog(@"信號捕獲崩潰氓仲,堆棧信息:%@",callStack);

  CrashHandler *crashObject = [CrashHandler sharedInstance];
  NSException *customException = [NSException exceptionWithName:kSignalExceptionName
                                                          reason:[NSString stringWithFormat:NSLocalizedString(@"Signal %d was raised.", nil),signal]
                                                        userInfo:@{kSignalKey:[NSNumber numberWithInt:signal]}];

  [crashObject performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
}

第四步,添加讓應(yīng)用起死回生的 RunLoop 代碼

- (void)handleException:(NSException *)exception
{
  NSString *message = [NSString stringWithFormat:@"崩潰原因如下:\n%@\n%@",
                        [exception reason],
                        [[exception userInfo] objectForKey:kCaughtExceptionStackInfoKey]];
  NSLog(@"%@",message);

  UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"程序崩潰了"
                                                  message:@"如果你能讓程序起死回生得糜,那你的決定是敬扛?"
                                                  delegate:self
                                        cancelButtonTitle:@"崩就蹦吧"
                                        otherButtonTitles:@"起死回生", nil];
  [alert show];

  CFRunLoopRef runLoop = CFRunLoopGetCurrent();
  CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);

  while (!ignore) {
      for (NSString *mode in (__bridge NSArray *)allModes) {
          CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
      }
  }

  CFRelease(allModes);

  NSSetUncaughtExceptionHandler(NULL);
  signal(SIGABRT, SIG_DFL);
  signal(SIGILL, SIG_DFL);
  signal(SIGSEGV, SIG_DFL);
  signal(SIGFPE, SIG_DFL);
  signal(SIGBUS, SIG_DFL);
  signal(SIGPIPE, SIG_DFL);

  if ([[exception name] isEqual:kSignalExceptionName]) {
      kill(getpid(), [[[exception userInfo] objectForKey:kSignalKey] intValue]);
  } else {
      [exception raise];
  }
}

因為我這里弄了一個AlertView彈窗,所以必須要回到主線程來處理朝抖。 實際上啥箭,RunLoop 相關(guān)的代碼:

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
  CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);

  while (!ignore) {
      for (NSString *mode in (__bridge NSArray *)allModes) {
          CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
      }
  }

  CFRelease(allModes);

完全可以寫在 上面的 HandleException 回調(diào) 和 SignalHandler回調(diào)中。

第五步治宣,寫一段會導(dǎo)致崩潰的代碼

我是在ViewController 中添加了一個點擊事件急侥,弄了一個數(shù)組越界的Bug:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
  NSArray *array =[NSArray array];
  NSLog(@"%@",[array objectAtIndex:1]);
}

動態(tài)效果圖:

727768-5a47f8c85afebb6d.gif

遇到數(shù)組越界,應(yīng)用依然沒崩潰

sunnyxx 稱之為回光返照侮邀,為什么呢坏怪? 我再一次點擊視圖,應(yīng)用依然還是崩潰了绊茧,只能防止第一次崩潰铝宵。 我測試了,確實是第二次應(yīng)用崩潰,未能起死回生鹏秋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末尊蚁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子侣夷,更是在濱河造成了極大的恐慌横朋,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件百拓,死亡現(xiàn)場離奇詭異琴锭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)衙传,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進(jìn)店門祠够,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人粪牲,你說我怎么就攤上這事古瓤。” “怎么了腺阳?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵落君,是天一觀的道長。 經(jīng)常有香客問我亭引,道長绎速,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任焙蚓,我火速辦了婚禮纹冤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘购公。我一直安慰自己萌京,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布宏浩。 她就那樣靜靜地躺著知残,像睡著了一般。 火紅的嫁衣襯著肌膚如雪比庄。 梳的紋絲不亂的頭發(fā)上求妹,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天,我揣著相機(jī)與錄音佳窑,去河邊找鬼制恍。 笑死,一個胖子當(dāng)著我的面吹牛神凑,可吹牛的內(nèi)容都是我干的净神。 我是一名探鬼主播,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼强挫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起薛躬,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤俯渤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后型宝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體八匠,經(jīng)...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年趴酣,在試婚紗的時候發(fā)現(xiàn)自己被綠了梨树。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,861評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡岖寞,死狀恐怖抡四,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情仗谆,我是刑警寧澤指巡,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站扯饶,受9級特大地震影響臼疫,放射性物質(zhì)發(fā)生泄漏涝动。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一勉耀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蹋偏,春花似錦便斥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至字逗,卻和暖如春京郑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背葫掉。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工些举, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人俭厚。 一個月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓户魏,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子叼丑,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,860評論 2 361

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