為什么必須在主線程操作UI

本文借鑒于:https://mp.weixin.qq.com/s/4eniMFuwoDDDfStDJ8o5Hw
在開(kāi)發(fā)過(guò)程中裆甩,我們或多或少會(huì)不經(jīng)意在后臺(tái)線程中調(diào)用了UIKit框架的內(nèi)容身冬,可能是在網(wǎng)絡(luò)回調(diào)時(shí)直接imageView.image = anImage油宜,也有可能是不小心在后臺(tái)線程中調(diào)用了UIApplication.sharedApplication洼专。而這個(gè)時(shí)候編譯器會(huì)報(bào)出一個(gè)runtime錯(cuò)誤啊研,我們也會(huì)迅速的對(duì)其進(jìn)行修正。

但仔細(xì)去思考缴川,究竟為什么一定要在主線程操作UI呢?如果在后臺(tái)線程對(duì)UI進(jìn)行操作會(huì)發(fā)生什么描馅?在后臺(tái)線程對(duì)UI進(jìn)行操作不是可以更好的避免卡頓嗎把夸?這篇文章就是基于這樣一些疑問(wèn)而產(chǎn)生的。

太長(zhǎng)不看版:
UIKit并不是一個(gè) 線程安全 的類铭污,UI操作涉及到渲染訪問(wèn)各種View對(duì)象的屬性恋日,如果異步操作下會(huì)存在讀寫問(wèn)題,而為其加鎖則會(huì)耗費(fèi)大量資源并拖慢運(yùn)行速度况凉。另一方面因?yàn)檎麄€(gè)程序的起點(diǎn)UIApplication是在主線程進(jìn)行初始化,所有的用戶事件都是在主線程上進(jìn)行傳遞(如點(diǎn)擊各拷、拖動(dòng))刁绒,所以view只能在主線程上才能對(duì)事件進(jìn)行響應(yīng)。而在渲染方面由于圖像的渲染需要以60幀的刷新率在屏幕上 同時(shí) 更新烤黍,在非主線程異步化的情況下無(wú)法確定這個(gè)處理過(guò)程能夠?qū)崿F(xiàn)同步更新知市。

從UIKit線程不安全說(shuō)起

在UIKit中,很多類中大部分的屬性都被修飾為nonatomic速蕊,這意味著它們不能在多線程的環(huán)境下工作嫂丙,而對(duì)于UIKit這樣一個(gè)龐大的框架,將其所有屬性都設(shè)計(jì)為線程安全是不現(xiàn)實(shí)的规哲,這可不僅僅是簡(jiǎn)單的將nonatomic改成atomic或者是加鎖解鎖的操作跟啤,還涉及到很多的方面:

  • 假設(shè)能夠異步設(shè)置view的屬性,那我們究竟是希望這些改動(dòng)能夠同時(shí)生效,還是按照各自runloop的進(jìn)度去改變這個(gè)view的屬性呢隅肥?
  • 假設(shè)UITableView在其他線程去移除了一個(gè)cell竿奏,而在另一個(gè)線程卻對(duì)這個(gè)cell所在的index進(jìn)行一些操作,這時(shí)候可能就會(huì)引發(fā)crash腥放。
  • 如果在后臺(tái)線程移除了一個(gè)view泛啸,這個(gè)時(shí)候runloop周期還沒(méi)有完結(jié),用戶在主線程點(diǎn)擊了這個(gè)“將要”消失的view秃症,那么究竟該不該響應(yīng)事件候址?在哪條線程進(jìn)行響應(yīng)?

仔細(xì)思考种柑,似乎能夠多線程處理UI并沒(méi)有給我們開(kāi)發(fā)帶來(lái)更多的便利岗仑,假如你代入了這些情景進(jìn)行思考,你很容易得出一個(gè)結(jié)論: “我在一個(gè)串行隊(duì)列對(duì)這些事件進(jìn)行處理就可以了莹规∨馄眩” 蘋果也是這樣想的,所以UIKit的所有操作都要放到主線程串行執(zhí)行良漱。

在Thread-Safe Class Design一文提到:

It’s a conscious design decision from Apple’s side to not have UIKit be thread-safe. Making it thread-safe wouldn’t buy you much in terms of performance; it would in fact make many things slower. And the fact that UIKit is tied to the main thread makes it very easy to write concurrent programs and use UIKit. All you have to do is make sure that calls into UIKit are always made on the main thread.

大意為把UIKit設(shè)計(jì)成線程安全并不會(huì)帶來(lái)太多的便利舞虱,也不會(huì)提升太多的性能表現(xiàn),甚至?xí)驗(yàn)榧渔i解鎖而耗費(fèi)大量的時(shí)間母市。事實(shí)上并發(fā)編程也沒(méi)有因?yàn)閁IKit是線程不安全而變得困難矾兜,我們所需要做的只是要確保UI操作在主線程進(jìn)行就可以了。

好吧患久,那假設(shè)我們用黑魔法祝福了UIKit椅寺,這個(gè)UIKit能夠完美的解決我們上面提到的問(wèn)題,并能夠按照開(kāi)發(fā)者的想法隨意展現(xiàn)不同的形態(tài)蒋失。那這個(gè)時(shí)候我們可以在后臺(tái)線程操作UI了嘛返帕?

很可惜,還是不行篙挽。

Runloop 與繪圖循環(huán)

道理我們都懂荆萤,那這個(gè)究竟跟我們不能在后臺(tái)線程操作UI有什么關(guān)系呢?

UIApplication在主線程所初始化的Runloop我們稱為Main Runloop铣卡,它負(fù)責(zé)處理app存活期間的大部分事件链韭,如用戶交互等,它一直處于不斷處理事件和休眠的循環(huán)之中煮落,以確保能盡快的將用戶事件傳遞給GPU進(jìn)行渲染敞峭,使用戶行為能夠得到響應(yīng),畫(huà)面之所以能夠得到不斷刷新也是因?yàn)镸ain Runloop在驅(qū)動(dòng)著蝉仇。

而每一個(gè)view的變化的修改并不是立刻變化旋讹,相反的會(huì)在當(dāng)前run loop的結(jié)束的時(shí)候統(tǒng)一進(jìn)行重繪殖蚕,這樣設(shè)計(jì)的目的是為了能夠在一個(gè)runloop里面處理好所有需要變化的view,包括resize骗村、hide嫌褪、reposition等等,所有view的改變都能在同一時(shí)間生效胚股,這樣能夠更高效的處理繪制笼痛,這個(gè)機(jī)制被稱為繪圖循環(huán)(View Drawing Cycle)。

假設(shè)這個(gè)時(shí)候我們應(yīng)用了我們的魔法UIKit琅拌,并愉快的在一條后臺(tái)線程操作UI缨伊,但當(dāng)我們需要對(duì)設(shè)備進(jìn)行旋轉(zhuǎn)并重新布局的時(shí)候,問(wèn)題來(lái)了进宝,因?yàn)楦鱾€(gè)線程之間不同步刻坊,這時(shí)候各個(gè)view修改的請(qǐng)求時(shí)機(jī)是零碎的,所以所有的旋轉(zhuǎn)變化并不能在Main Runloop的一個(gè)runloop里面處理完党晋,這就導(dǎo)致設(shè)備旋轉(zhuǎn)之后還有一些view遲遲沒(méi)有旋轉(zhuǎn)谭胚。
另一方面,因?yàn)槲覀兊哪Х║IKit并不是在主線程未玻,所以Main Runloop中的事件需要跨線程進(jìn)行傳輸灾而,這樣會(huì)導(dǎo)致顯示與用戶事件并不同步。試想一下我們用我們的魔法UIKit寫了一個(gè)游戲扳剿,用戶如果在圖片還沒(méi)有加載出來(lái)的時(shí)候按下了按鈕旁趟,他們就能勝利,于是我們寫出了這樣的代碼:

game.m

- (void)didClickButton:(UIButton *)button
{    if (self.imageView.image != nil) {        // User lose!
    } else {        // User Win!
    }
}

- (void)loadImageInBackgroundThread
{    dispatch_async(dispatch_queue_create("BackgroundQueue", NULL), ^{        self.imageView.image = [self downloadedImage];
    };
}

因?yàn)槲覀兺昝赖哪Х║IKit庇绽,在后臺(tái)執(zhí)行imageView.image = xxx并不會(huì)產(chǎn)生任何問(wèn)題锡搜。游戲上線,在你還為后臺(tái)處理UI而沾沾自喜的時(shí)候瞧掺,用戶投訴了他們明明沒(méi)有看到圖片顯示耕餐,點(diǎn)擊的時(shí)候還是告訴他們輸了,于是你的產(chǎn)品就這樣撲街了辟狈。

這是因?yàn)辄c(diǎn)擊等事件是由系統(tǒng)傳遞給UIApplication中肠缔,并在Main Runloop中進(jìn)行處理與響應(yīng),但是由于UI在后臺(tái)線程中進(jìn)行處理上陕,所以他跟事件響應(yīng)并不同步桩砰。即使在UI所在的后臺(tái)線程也自己維護(hù)了一個(gè)Runloop拓春,在Runloop結(jié)束時(shí)候進(jìn)行渲染释簿,但可能用戶已經(jīng)進(jìn)行了點(diǎn)擊操作并開(kāi)始辱罵你的游戲了。

好吧硼莽,那假設(shè)我天賦異稟庶溶,把整套UIApplication的機(jī)制全都重寫了煮纵,也用黑魔法祝福了我的新UIApplication,這個(gè)時(shí)候它能完美的解決線程同步的問(wèn)題偏螺,這個(gè)時(shí)候我可以在后臺(tái)操作UI了嗎行疏?

很可惜,還是不能套像。

理解iOS的渲染流程

要回答這個(gè)問(wèn)題酿联,我們要先從最底層的渲染說(shuō)起。
渲染系統(tǒng)框架:


image.png
  • UIKit:包含各種控件夺巩,負(fù)責(zé)對(duì)用戶操作事件的響應(yīng)贞让,本身并不提供渲染的能力
  • Core Animation:負(fù)責(zé)所有視圖的繪制、顯示與動(dòng)畫(huà)效果
  • OpenGL ES:提供2D與3D渲染服務(wù)
  • Core Graphics:提供2D渲染服務(wù)
  • Graphics Hardware:指GPU
    所以在iOS中柳譬,所有視圖的現(xiàn)實(shí)與動(dòng)畫(huà)本質(zhì)上是由 Core Animation 負(fù)責(zé)喳张,而不是UIKit。

Core Animation Pipeline 流水線:


image.png

Core Animation的繪制是通過(guò)Core Animation Pipeline實(shí)現(xiàn)美澳,它以流水線的形式進(jìn)行渲染销部,具體分為四個(gè)步驟:

  1. Commit Transaction:
    可以細(xì)分為
  • Layout:構(gòu)建視圖布局如addSubview等操作
  • Display:重載drawRect:進(jìn)行時(shí)圖繪制,該步驟使用CPU與內(nèi)存
  • Prepare:主要處理圖像的解碼與格式轉(zhuǎn)換等操作
  • Commit:將Layer遞歸打包并發(fā)送到Render Server
  1. Render Server:
  • 負(fù)責(zé)渲染工作制跟,會(huì)解析上一步Commit Transaction中提交的信息并反序列化成渲染樹(shù)(render tree)舅桩,隨后根據(jù)layer的各種屬性生成繪制指令,并在下一次VSync信號(hào)到來(lái)時(shí)調(diào)用OpenGL進(jìn)行渲染凫岖。
  1. GPU:
  • GPU會(huì)等待顯示器的VSync信號(hào)發(fā)出后才進(jìn)行OpenGL渲染管線江咳,將3D幾何數(shù)據(jù)轉(zhuǎn)化成2D的像素圖像和光柵處理,隨后進(jìn)行新的一幀的渲染哥放,并將其輸出到緩沖區(qū)歼指。
  1. Dispaly:
  • 從緩沖區(qū)中取出畫(huà)面,并輸出到屏幕上甥雕。

知識(shí)補(bǔ)充:iOS的VSync與雙緩沖機(jī)制
VSync:

VSync(vertical sync)是指垂直同步踩身,在玩游戲的時(shí)候在設(shè)置的時(shí)候應(yīng)該會(huì)看見(jiàn)過(guò)這個(gè)選項(xiàng),這個(gè)機(jī)制能夠讓顯卡和顯示器保持在一個(gè)相同的刷新率從而避免畫(huà)面撕裂社露。在iOS中挟阻,屏幕具有60Hz的刷新率,這意味著它每秒需要顯示60張不同的圖片(幀)峭弟,但GPU并沒(méi)有一個(gè)確定的刷新率附鸽,在某些時(shí)候GPU可能被要求更強(qiáng)力的數(shù)據(jù)輸出來(lái)確保渲染能力,這時(shí)候他們可能比屏幕刷新率(60Hz)更快瞒瘸,就會(huì)導(dǎo)致屏幕不能完整的渲染所有GPU給他的數(shù)據(jù)坷备,因?yàn)樗粔蚩欤聊坏纳弦粠€沒(méi)渲染完情臭,下一幀就已經(jīng)到來(lái)了省撑,這就導(dǎo)致畫(huà)面的撕裂赌蔑。
這個(gè)時(shí)候我們就要引入VSync了,簡(jiǎn)單來(lái)說(shuō)它就是讓顯卡保持他的輸出速率不高于屏幕的刷新率竟秫,啟用了VSync后娃惯,GPU不再會(huì)給你可憐的60Hz屏幕每秒發(fā)送100幀了,它會(huì)增加每一幀的發(fā)送間隔肥败,確保顯示器能夠有充足的時(shí)間去處理每一幀趾浅。

雙緩沖機(jī)制:

雙緩沖機(jī)制是用于避免或減少畫(huà)面閃爍的問(wèn)題,在單緩沖的情況下馒稍,GPU輸出了一幀畫(huà)面潮孽,緩沖區(qū)就需要馬上獲取這個(gè)畫(huà)面,并交給顯示屏去顯示筷黔,而這段時(shí)間GPU輸出的畫(huà)面就全都丟失了往史,因?yàn)闆](méi)有緩沖區(qū)去承載這些畫(huà)面,就會(huì)造成畫(huà)面的閃爍佛舱。
而在雙緩沖機(jī)制下有一個(gè)Back Frame Buffer和一個(gè)Front Frame Buffer椎例,在GPU繪制完成后,它會(huì)將圖像先保存到Back Frame Buffer中请祖,操作完畢后订歪,會(huì)調(diào)用一個(gè)交換函數(shù),讓繪制完成的Back Frame Buffer上的圖像交換到Front Frame Buffer上肆捕。由于雙緩沖利用了更多顯存與CPU消耗時(shí)間刷晋,從而避免了畫(huà)面的閃爍。

So慎陵?
相信大家都會(huì)遇到過(guò)應(yīng)用卡頓眼虱,卡頓的原因就是因?yàn)閮蓭乃⑿聲r(shí)間間隔大于60幀每秒(約16.67ms),導(dǎo)致用戶感覺(jué)點(diǎn)擊或者滑動(dòng)時(shí)席纽,界面沒(méi)有及時(shí)的響應(yīng)捏悬。

前面提到Core Animation Pipeline是以流水線的形式工作的,在理想的狀況下我們希望它能夠在1/60s內(nèi)完成圖層樹(shù)的準(zhǔn)備工作并提交給渲染進(jìn)程润梯,而渲染進(jìn)程在下一次VSync信號(hào)到來(lái)的時(shí)候提交給GPU進(jìn)行渲染过牙,并在1/60s內(nèi)完成渲染,這樣就不會(huì)產(chǎn)生任何的卡頓纺铭。

但是由于我們使用了我們的魔法UIKit寇钉,所以我們?cè)谠S多后臺(tái)線程進(jìn)行了UI操作,在runloop的結(jié)尾準(zhǔn)備進(jìn)行渲染的時(shí)候舶赔,不同線程提交了不同的渲染信息扫倡,于是我們就擁有了更多的繪制事務(wù),這個(gè)時(shí)候Core Animation Pipeline會(huì)不斷將信息提交顿痪,讓GPU進(jìn)行渲染镊辕,由于繪制事件的不同步導(dǎo)致了GPU渲染的不同步,可能在上一幀是需要渲染一個(gè)label消失的畫(huà)面蚁袭,下一幀卻又需要渲染這個(gè)label改變了文字征懈,最終導(dǎo)致的是界面的不同步。

(如果你真的想要這樣的效果揩悄,可以嘗試一下使用我的DWAnimatedLabel)

另一方面卖哎,在VSync和雙緩沖機(jī)制我們可以看出渲染其實(shí)是一個(gè)十分消耗系統(tǒng)資源的操作(占用顯存與CPU),所以可能會(huì)因?yàn)榇罅康氖聞?wù)和線程之間頻繁的上下文切換導(dǎo)致了GPU無(wú)法處理删性,反而影響了性能亏娜,從而導(dǎo)致在1/60s中無(wú)法完成圖層樹(shù)的提交,導(dǎo)致了嚴(yán)重的卡頓蹬挺。

但我真的很想在后臺(tái)線程操作UI维贺,我能再用黑魔法嗎?

好吧巴帮,其實(shí)是有辦法的溯泣。

Texture or ComponentKit
AsyncDisplayKit(現(xiàn)命名為Texture) 是Facebook開(kāi)源的一個(gè)用于保持iOS界面流暢的框架。

ComponentKit是Facebook開(kāi)源的一個(gè)基于React思想的iOS原生UI開(kāi)發(fā)框架榕茧。它通過(guò)函數(shù)式和聲明的方式構(gòu)建UI垃沦。

讓我們撤銷掉我們對(duì)UIKit施展的各種魔法,回到這個(gè)UI只能在主線程進(jìn)行操作的世界吧用押。這兩個(gè)框架其實(shí)并不是真正的在后臺(tái)線程操作UI肢簿,而是用了更巧妙的方法將一些耗時(shí)的操作異步執(zhí)行,從而繞開(kāi)了UIKit只能在主線程操作的限制蜻拨。

比如Texture創(chuàng)建了各類Node池充,在node中包含了UIView,而Node本身是線程安全的缎讼,所以允許在后臺(tái)線程對(duì)Node進(jìn)行修改纵菌,隨后在第一次主線程訪問(wèn)View的時(shí)候它才會(huì)在內(nèi)部生成對(duì)應(yīng)的View,當(dāng)node的屬性發(fā)生改變的時(shí)候休涤,他也不會(huì)馬上進(jìn)行修改咱圆,而是在適當(dāng)?shù)臅r(shí)機(jī)一次性的在主線程為內(nèi)部的View進(jìn)行設(shè)置。(有點(diǎn)類似于繪圖循環(huán))
而ComponentKit則是通過(guò)創(chuàng)建Component來(lái)描述UI功氨,它也是一個(gè)線程安全的類序苏。可以將Component認(rèn)為是一個(gè)刻板捷凄,而UIView是刻板下的一張紙忱详,渲染則是噴墨的過(guò)程。當(dāng)我們生成了一個(gè)Component的時(shí)候跺涤,就等于生成了一個(gè)View的模版匈睁,在進(jìn)行渲染的時(shí)候只要按照模版進(jìn)行繪制就可以了监透。復(fù)雜的界面可以通過(guò)各種簡(jiǎn)單的Component來(lái)組成。(類似于Flutter的widget)

但是我……

總結(jié)

UIKit不能在主線程進(jìn)行操作航唆,這一個(gè)鐵律只要是熟悉iOS開(kāi)發(fā)的都會(huì)有所耳聞胀蛮,但是往深一層其實(shí)這個(gè)涉及到很多的東西,包括軟件糯钙、整體UIKit框架的實(shí)現(xiàn)粪狼、硬件等等,很多細(xì)節(jié)的東西往往是我們?cè)谄匠S兴雎缘娜伟丁再榄?赡芪覀冎啦荒茉谥骶€程操作,卻不知道其內(nèi)在原因享潜;可能我們知道怎么排查困鸥、處理卡頓,卻不知道其真正的成因剑按;可能我們知道drawRect:方法會(huì)導(dǎo)致CPU飆升窝革,卻不知道原因是上下文的切換導(dǎo)致……

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市吕座,隨后出現(xiàn)的幾起案子虐译,更是在濱河造成了極大的恐慌,老刑警劉巖吴趴,帶你破解...
    沈念sama閱讀 221,430評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漆诽,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡锣枝,警方通過(guò)查閱死者的電腦和手機(jī)厢拭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)撇叁,“玉大人供鸠,你說(shuō)我怎么就攤上這事≡赡郑” “怎么了楞捂?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,834評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)趋厉。 經(jīng)常有香客問(wèn)我寨闹,道長(zhǎng),這世上最難降的妖魔是什么君账? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,543評(píng)論 1 296
  • 正文 為了忘掉前任繁堡,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘椭蹄。我一直安慰自己闻牡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,547評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布绳矩。 她就那樣靜靜地躺著罩润,像睡著了一般。 火紅的嫁衣襯著肌膚如雪埋酬。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,196評(píng)論 1 308
  • 那天烧栋,我揣著相機(jī)與錄音写妥,去河邊找鬼。 笑死审姓,一個(gè)胖子當(dāng)著我的面吹牛珍特,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播魔吐,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼扎筒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了酬姆?” 一聲冷哼從身側(cè)響起嗜桌,我...
    開(kāi)封第一講書(shū)人閱讀 39,671評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辞色,沒(méi)想到半個(gè)月后骨宠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,221評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡相满,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,303評(píng)論 3 340
  • 正文 我和宋清朗相戀三年层亿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片立美。...
    茶點(diǎn)故事閱讀 40,444評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡匿又,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出建蹄,到底是詐尸還是另有隱情碌更,我是刑警寧澤,帶...
    沈念sama閱讀 36,134評(píng)論 5 350
  • 正文 年R本政府宣布洞慎,位于F島的核電站针贬,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏拢蛋。R本人自食惡果不足惜桦他,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,810評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧快压,春花似錦圆仔、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,285評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至脉幢,卻和暖如春歪沃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嫌松。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,399評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工沪曙, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人萎羔。 一個(gè)月前我還...
    沈念sama閱讀 48,837評(píng)論 3 376
  • 正文 我出身青樓液走,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親贾陷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子缘眶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,455評(píng)論 2 359

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

  • 在開(kāi)發(fā)過(guò)程中,我們或多或少會(huì)不經(jīng)意在后臺(tái)線程中調(diào)用了UIKit框架的內(nèi)容髓废,可能是在網(wǎng)絡(luò)回調(diào)時(shí)直接imageView...
    Dywane閱讀 7,539評(píng)論 15 101
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,111評(píng)論 1 32
  • 面向?qū)ο蟮娜筇匦裕悍庋b巷懈、繼承、多態(tài) OC內(nèi)存管理 _strong 引用計(jì)數(shù)器來(lái)控制對(duì)象的生命周期慌洪。 _weak...
    運(yùn)氣不夠技術(shù)湊閱讀 1,108評(píng)論 0 10
  • 你曾說(shuō)過(guò)砸喻,最美好的憧憬即是知道了無(wú)結(jié)果,卻還是執(zhí)迷不悟的去相信只因?yàn)橛谀阄襾?lái)說(shuō)蒋譬,于最美好的年華里割岛,你我邂逅了青春。...
    伊人斕珊閱讀 230評(píng)論 0 1
  • yire閱讀 57評(píng)論 0 0