扣丁書屋

得物 iOS 工程演進之路

正文

大家好,我是來自得物 iOS 架構組的 Casa。今天想和大家分享一下得物這兩年以來的 iOS 的工程演進,希望能給處在演進階段的工程師帶來一些新的啟發。

工程演進規劃

首先需要說明的是,我認為,有架構師的團隊,和沒有架構師的團隊,工程演進方向是不一樣的。

  • 有架構師的團隊:會對工程有規劃,當遇到演進階段的“分叉口”時,會有一個比較清晰的目標,決定接下來該往哪走。
  • 沒有架構師的團隊:野蠻生長,業務需要什么添加什么。那么我首先來給大家分享一下,得物的工程規劃一般是怎么做的?目前得物將工程演進分為了三個階段:工程化、組件化以及容器化。

首先,需要明確這三個步驟分別是什么,以及分別想要解決的問題

  1. 工程化:
  • 定義:為項目搭建一系列的周邊工具,打通 CI / CD 流程。
  • 解決的問題:解決標準操作流程(SOP) 的問題。

2 . 組件化:

  • 定義:將項目打碎并拆分成若干個組件組成的項目,以面向組件的方式進行開發。
  • 解決的問題:解決工程業務復雜的問題。

3 . 容器化:

  • 定義:利用拆分的組件,在快速滿足業務需求的同時,盡量少地提升項目復雜度,同時以面向構件的方式進行開發。
  • 解決的問題:解決組件復用性的問題。
  • 需要注意的是,此處定義的“容器化”,是站在組件化的基礎上實現的,與 Flutter、React Native、Weex、WebView 容器的概念有所區分。集成了上述容器技術的的應用,是具備容器能力的,但這并不代表工程是容器化的。

同時,工程健康是需要貫穿整個演進階段的。接下來我會先來分享工程健康上,得物所做的優化,再來探討項目工程化、組件化、容器化等演進階段。

工程健康

工程健康主要包含:包體積治理、Crash 治理、啟動流程治理等三項。對于部分資源充裕的項目,可能還會關注電量損耗、圖片加載時長、API 請求時長等。

包體積大小治理

隨著業務擴展,得物用了 2 - 3 個版本從 250M 上升到 350M,為了減少包大小,得物做了如下實踐。

包體積大小主要由資源大小與代碼大小組成,所以治理的方向就是想辦法減少這兩塊內容所占的大小。

資源的治理可以分為幾個方向:

  • 資源壓縮:
  • 第一階段:嘗試使用 ImageOptim 進行無損壓縮,但是發現毫無收益,原因是因為 Xcode 已經幫我們進行了無損壓縮。
  • 第二階段:使用了 80% 的有損壓縮,對于部分特殊圖片,還可以選擇更高的有損壓縮。(此處得物工程獲得了 10M+ 的收益)。

  • 重復資源合并:由于組件化的存在,每個組件可能會存在自己使用到的圖片,導致大量相似、甚至重復圖片存在。這里得物分幾個階段進行了優化。
  • 第一階段:使用腳本掃描出重復資源,然后手工對資源進行合并。
  • 第二階段:實現了腳本合并。在編譯結束時,針對資源計算 MD5,然后確認相同 MD5 資源的數量,若存在多個,則移除相同的冗余資源。同時為了讓大家都使用相同的資源,我們對 imageNamed: 等方法進行封裝 / Hook,將傳入的資源名字處理成 MD5,然后再進行資源的搜索。這樣可以解決多組件使用相同資源的問題。
  • 第三階段:使用機器學習找出相同相似資源。除了 MD5 一樣的資源,還有很多相似的資源是可以進行優化的。我們通過機器學習腳本找出所有的相似圖片,對高相似度的資源進行腳本合并,相似度較低的交由業務線進行確認后再進行合并。

  • 資源下發:
  • 第一階段:使用蘋果 SKDownload 功能,但該功能更適合關卡類的游戲。對于得物的電商場景,由于沒有關卡概念,也沒有辦法預測用戶執行上下級頁面的路徑,所以這里只能將所有的下級資源全都下載,沒有起到特別大效果 。
  • 第二階段:自建資源下發平臺。每個版本對應一個資源包。eg:1.0.0 的版本,對應 1.0.0 的資源包;2.0.0 的版本,對應 2.0.0 的資源包??蛻舳送ㄟ^“資源管理器”訪問資源,如果資源沒下載好,則訪問 CDN 進行下載(非重要圖片資源處理)。對于比較大的動畫 / AR 資源,若未下載好,則使用圖片資源進行業務邏輯的降級兜底。
  • 第三階段:資源增量下發,只下載新版本新增資源。(對于資源下發平臺,得物此塊獲得了大概 3 - 4 倍的提升)

代碼的治理可以分為以下幾個方向:

  • 三方庫裁剪:

針對只需要三方庫一小部分功能的場景,我們對三方庫進行定制化縮減,如移除不需要的功能;針對為解決同類需求,引入多個相似第三方庫的場景,限定只用某一個,如 Kingfisher、SDWebImage 等同功能組件僅保留一個,其他業務需要進行遷移。同時,為了確認當前工程中用到的第三方庫,我們對 LinkMap 進行了解析,從而找到使用的第三方庫及具體使用的業務場景。

  • 無用代碼移除:

我們花了大量時間定義了需要做無用代碼掃描的集合 S1,同時掃描出當前使用的集合 S2,然后求 S1、S2 的差集獲得無用代碼的集合。

  • 重復代碼合并

找由于復制粘貼產生的重復代碼,然后抽出中間組件,各組件對中間組件進行依賴實現。

經過上述資源層面及代碼層面的優化,得物工程包體積從 350M 下降至 250M,包體積下降 28.5%。

工程代碼治理

  • 長依賴鏈組件:部分項目中會存在組件依賴鏈過長的情況,如 A -> B -> C -> D(此處 "->" 代表依賴狀態)。這樣會導致使用 A 組件的時候,必須同時引入 B、C、D 組件。我們認為長依賴組件的出現是不合理的,是沒有合理區分縱向依賴橫向依賴導致的。針對這部分內容的處理,需要調整依賴方式。

  • 縱向依賴:直接依賴代碼,通過 Pod Dependency 進行依賴。

  • 橫向依賴:通過組件調度的方式進行間接依賴,無需直接依賴代碼。

  • 長編譯耗時組件:當新組件僅需使用大組件的小功能時,直接依賴了大組件,會導致新組件開發時,開發、編譯成本變大。針對這部分內容,需要將大組件再次進行拆分成若干小組件進行處理。

  • 無用、重復代碼:此塊內容已在包體積大小治理中闡明,不再贅述。

Crash 治理

  • 純人工階段:使用 Bugly Crash 管理系統,手工確認 Crash 后分給具體的業務線,同時人工跟進問題修復情況。

  • 半自動化階段:使用腳本爬 Bugly 信息(包括 Crash Stack 信息),然后自動識別業務線并自動落表。

  • 全自動化階段:由于 Bugly 是每小時統計一次 Crash 數據,所以有可能出現數據滯后的問題。為此我們重新搭建了 Crash 平臺,去做自動化收集、解析、告警的事情,同時實現自動跟進修復情況。

啟動流程治理

  • 啟動流程分組:將代碼按業務線分塊并標注,用以確定每條業務線所占的時長,隨后可根據該指標治理部分業務線大幅增長的不合理情況。
  • 代碼插樁 diff:編譯時,進行代碼插樁。然后將新舊包代碼插樁數據對比,當新增代碼調用時,能夠及時發現調用鏈的變化,用于確保調用鏈必要且合理。
  • 安全模式:假設某個組件沒有正常完成工作(如 DB 遷移、A / B Test 數據獲?。?,可以進入安全模式,通過 Hook RunLoop 并嘗試重啟,以避免應用崩潰。

工程化

得物工程化主要圍繞三項開展,分別是:圍繞組件化的基礎設施、包分發平臺以及持續交付。

圍繞組件化的基礎設施

需要注意的是,工程化的基礎設施,需要隨著組件化、容器化階段不停調整。并不是意味進入組件化后,工程化就不用繼續,而是進入組件化后,工作重點主要放在組件化,但仍需投入精力調整優化相關的基礎設施。

結合得物場景,主要圍繞組件化實現了以下命令行工具,用以提供業務同學的開發效率:

  • 組件創建腳本、組件工具:解決創建、管理組件的問題。
  • 組件發版工具:解決組件發版的問題。
  • 二進制調試工具:解決二進制組件調試問題。
  • 組件上游依賴查詢工具:解決組件依賴查詢問題。
  • 編譯成功節點切換工具:解決“出包難”的問題。
  • 裙帶源碼組件切換工具:解決 ARC 不對齊的問題。

后文會結合具體場景說明工具作用。

組件管理(包含組件創建、發版等組件工具)

區別于其他公司,得物沒有使用在 Podfile 中定義版本號的方式。主要因為得物工程中組件較多(組件數量超 1430+),平均每隔 4.8 分鐘發版一次(日均發版 100+)。如果使用 Podfile,會意味著每當組件發版,Podfile 都需要修改版本號,繼而會產生大量的 Podfile 文件沖突。同時,得物也經常存在多組件聯合發版的問題,這樣依賴上游需要同時調整多個組件版本號,存在較大的溝通成本。

基于上述考慮,得物目前主要使用索引做組件管理,且固定以下命名規范(下列假設版本號為 X.Y.Z):

  • 開發環境:使用 dev 索引庫,規定組件發布新版本時,僅變更版本號的第一位,就像 A,從 1.0.0 變為 2.0.0。
  • 沙盒環境:使用 test 索引庫,規定組件發布新版本時,僅變更版本號的第二位,就像 B,從 1.0.0 變為 1.1.0。
  • 灰度環境:使用 gray 索引庫,規定組件發布新版本時,僅變更版本號的第三位,就像 C,從 1.0.0變為 1.0.1。
  • 現網環境:使用 release 索引庫,規定組件不能發布新版本。

不同環境使用不同的 Git 倉庫,且 Podfile 中固定了每個組件當前環境的倉庫地址(所以切換環境時,需要調整 Podfile 中的 Source,并處理不同環境 git upstream 的合并)。倉庫環境的拆分,避免了新版本開發影響到已有版本邏輯。

二進制(包含二進制調試工具、組件上游依賴查詢工具、裙帶源碼組件切換工具)

  • 單工程獨立編譯制作:

  • 優點:組件發版時完成二進制制作;可靠性高。

  • 缺點:組件制作二進制時長取決于組件規模,大組件制作仍舊耗時;存在大量組件需要做二進制,單光算編譯時長,需要 5 天制作;出現測試包體積變大。

  • 全工程編譯產物制作:利用編譯緩存,從 Xcode 編譯緩存 DerivedData 中取出組件。

  • 優點:每 50min 完成 1400+ 二進制制作。

  • 缺點:只能分批制作(1h / 次);存在 ARC 不對齊的情況。

  • ARC 不對齊:A 組件已經是二進制(在編譯時加入 ARC),B 組件使用源碼編譯,此時再次編譯(編譯器會為 B 會添加 ARC,但不會對 A 組件進行處理,有可能導致 ARC 不對齊引發內存被提前釋放,導致 EXC_BAD_ACCESS Crash)。為了解決該問題,在做二進制時,需要對新發版的組件做裙帶組件源碼切換:將其上游一級、下游一級的組件變成源碼依賴而非二進制依賴。

  • 基于 Oolong 的增量制作,Oolong 是得物開發的腳本(即上文中提到的裙帶組件源碼切換工具),能夠將一個組件的上一級和下一級依賴的組件變為源碼參與編譯:

  • 優點:徹底解決 ARC 不對齊;每 20 min 可以完成 1400+ 二進制制作。

  • 缺點:只能分批制作(20 min / 次)。

最終,得物完成基于 Oolong 的增量制作。需要說明的是,得物不追求所有工程都是二進制,原因是開發工作一般以小時計,20min 的二進制編譯時長已經能相對滿足需求。

持續交付(包含編譯成功節點切換工具)

得物工程較大,存在多人(100+)同時開發、多組件同時發布的情況。并且,為追求效率,我們沒對組件進行編譯驗證,導致有時候,出一個“可用”的包比較困難。為了解決該問題,我們進行了如下方案。

  • Xcode Server 定時打包:CI 機定時打包。

  • 優點:能夠避免編譯錯誤長時間不被解決的問題。

  • 缺點:不保證編譯成功,且每輪編譯時長較長,相隔 1h 才能獲得編譯結果;但 CI 機編譯成功不代表開發端編譯一定能成功,對新人不友善。

  • 記錄編譯成功節點并提供腳本(“Boom”)用以切換:執行腳本后,將代碼切換至當前分支最后一次編譯成功節點。

  • 優點:能夠避免編譯錯誤長時間不被解決的問題;能夠最大限度保證開發端代碼編譯成功。

  • 缺點:編譯成功節點落后近 1h;由于開發的組件一定是源碼,若使用二進制編譯,會造成 ARC 不對齊引起的各種偶發問題。

  • 多機車輪打包:使用大量機器持續構建不同環境的包(如上圖示)。
  • 優點:能夠及時發現編譯錯誤,每輪 10 min;能夠最大限度保證開發端代碼編譯成功。
  • 缺點:需要較多機器資源。

除此之外,我們還利用了“飛書卡片”功能,定期輸出構建信息。

  • 若出現構建失敗,則自動提醒引發失敗的工程師(一次構建可能包含多個不同的工程師,此時提醒該組工程師)進行處理。
  • 若工程師開始排查,可點擊“我來處理”,“飛書卡片”狀態會發生變化,變為“修復中”狀態。
  • 若修復完成,會展示“飛書卡片”會展示“已修復”狀態。

組件化

IPO 模型

在開始組件化相關的內容前,我們首先要明確,組件化想要達到的效果是:化整為零,各自獨立;確保每一部分是正確的,整體就是正確的。解決代碼復用并不是組件化要解決的主要問題,組件化要解決的問題是將復雜的大工程拆分成很多簡單的小工程,且小工程之間能夠互相協作;工程使用了 Cocoapods 也不意味著已完成組件化。組件化的是指代碼可以獨立編譯,可以獨立測試。

在講組件化之前,需要強調一個概念:“IPO”模型。

**“IPO”模型是指:輸入、處理、輸出三者中,只要其中兩者正確,剩余一者必定正確。**該模型的指導意義在于指導我們如何做組件化拆解和組件設計。

  1. 我們在做組件化拆解時,需要定義清楚這個組件的 Input。
  2. 我們在做組件化拆解時,需要確保組件的代碼是正確的,也就是說 Process 是正確的。
  3. 在確定 Input 已經定義清楚,且 Process 是正確的情況下,我們可以不必關心 Output,因為根據 “IPO”模型,只要 Input 和 Process 是正確的,Output 就一定是正確的。

“IPO”模型指導了我們做組件拆分的重點:將 Input 定義清楚,且確保 Process 代碼正確。

在 CTMediator 方案體系下,OC Category / Swift Extension 的目的就是要保證 Input 是定義清楚的;剩余只需保證組件能夠正確執行,即 Process 是正確的。那么 Output 就一定正確。如此一來,一個組件就是完整正確的。一個工程中只要每個組件都是完整正確的,那么這個工程就是完整正確的。

為什么不使用基于注冊的組件化方案

目前組件化方案主要分為兩大類:

  • 使用注冊:又可分為 URL 注冊 / Protocol 注冊。
  • 不使用注冊:基于 Target-Action 及 Runtime:類 CTMediator 方案。

目前注冊類方案存在管理注冊時序、大量注冊實例造成無用內存消耗、大量注冊代碼造成的時間損耗等問題,對于這些問題,我們可以使用各種“補丁邏輯”(例如直接將注冊 URL 注入 mach_O 文件等)來解決。但若使用類 CTMediator 方案,則無需考慮類似問題,也就不需要去做“補丁邏輯”。

組件拆分粒度

業界針對組件拆分粒度有不同的認知,因此也就會存在不同的討論。這些討論如果脫離了當前的業務階和團隊發展階段,是沒有意義的。在不同的階段下,組件拆分粒度是不一樣的。

小規模業務和團隊

小規模業務和團隊(5人以內):此時應以業務線為維度拆分組件,拆分出幾個組件即可,大多數情況下是 3 - 5 個。如果此時不做組件化、或拆分過多組件,工作效率都會降低。

在這樣的業務規模和團隊下,每次迭代時的迭代狀態是這樣的:

黃色的圓圈表示參與迭代并修改的組件。我們能夠看到這三次迭代中,大家只需要關心各自業務的組件,迭代無關的組件可以不必修改。在這種情況下,組件化為工程帶來了降低迭代復雜度的優勢。

中等規模業務和團隊

中等規模業務和團隊一般在 10 - 20 人左右,此時應以流程為維度拆分組件,組件數量大約在十幾個到數十個不等。如果還保留之前小規模階段的粒度或者拆得太細,工作效率就都不太理想。

在這樣的業務規模和團隊下,每次迭代時的狀態是這樣的:

我們能夠看到,在中等規模和團隊的情況下,工程的組件化拆解就已經引入分層的概念了。工程分層沒有絕對的標準,合理就行。絕大多數工程分層就是三層:業務實現層、膠水層、工具 SDK 層。

大規模業務和團隊

在大規模業務和團隊下,工程師規??赡芙偕踔翈装?,業務線越來越大。產品經理的數量也變得很多,需求越來越細??赡芤粋€流程就是一個產品經理在提需求,一個業務線中有好幾個產品經理去提需求是十分常見的情況。

那么,此時對應到我們的工程中,組件的拆解粒度也要更細,可能就要細到一個頁面就是一個組件,一個工具就是一個組件。這樣才能達到理想的工程效率。在這樣的工程規模和組件粒度下,注冊類的組件化方案可能就無法做到很好的支持。一方面注冊會很消耗時間,另一方面,每拆一個組件就要對應要注冊一個內容,提高了組件拆分的成本。

我們在進行架構設計的時候,要充分的考慮未來團隊及工程的成長速度及成長規模,需要為工程設計一個合理的組件化方案。如果隨著團隊規模擴張,小問題再次變成大問題,沒有真正的實現化整為零,則該組件化方案的價值就不大了。

在大規模業務和團隊下,每次迭代時的狀態是這樣的:

通過這樣的迭代狀態,我們能夠看到:

  1. 由于產品需求很細,當我們做到組件的顆粒度也很細時,每次迭代需求中的修改范圍就能夠做到很小,修改范圍小了,需要考慮的連帶關系就少了,工作效率就能得到提高。
  2. 在這種規模的業務和團隊情況下,我們會發現,整個工程的分層概念已經沒有意義了。我們轉而需要關注的是組件的依賴關系是否合理,需要做好縱向依賴和橫向依賴的區分和管理;需要做到某一個條線或者某一個功能,都能夠在自己的工程里進行獨立編譯、測試。
  3. 雖然分層概念在整個工程的范圍中已經失去了意義,但分層概念其實已經下沉到了某個條線或者某個功能中。我們把某個條線或者某個功能理解為一個“組件群”,在這個組件群中,是可以落實分層的概念的。

好的架構沒有 Common 沒有 Core,也不應該有大組件的存在

為什么不允許存在公共模塊

有 Common / Core 時,意味著有那么一部分代碼,其職責是不明確的。不明確職責的代碼會造成代碼未來難以維護,因此不是一個好的架構。

另外每人對 Common / Core 的認知不同,很可能會使得 Common / Core 變成一個公共垃圾堆,出現 “既然放哪里都不合適,那我就放 Common / Core 吧” 的情況。該垃圾堆持續增長,成為一座垃圾山,導致 Common / Core 未來無法維護。

在得物中,規定每個組件實現了什么功能,就是什么組件,不存在 “有關部門” 這一說。

為什么不允許存在大組件

原因是大部分功能的開發,其實僅需要大組件的其中一個功能,但為了實現該功能,卻需要引入一個大組件。這種情況不僅導致代碼維護復雜,還會導致這個小組件編譯時長不合理地增多。此時更合理的做法應該是將大組件打散成若干個小組件,其他組件需要啥,就只依賴它需要的那部分。我們認為一個大組件應該是由若干個小組件組成,而不是一個大組件由很多功能的代碼拼成。

Argument List Too Long

Argument List Too Long 指的是 Xcode 代碼編譯基本完成后,在執行工程 Build Phases 中的 Run Script 時 / 編譯時突然停止。

原因:Cocoapods 為 Pod 生成編譯參數后,會寫入到環境變量。此時若使用 Pod 構造的組件達到一定量級,會導致寫入環境變量過多,繼而導致環境變量總長度超過操作系統限制(即 26w 個字符),最終導致命令停止。

解決方案:環境變量中主要包含三個長度較長的內容,即 Header Search Path、Library Search Path 及 ModuleMap Path,所以關鍵是對這些信息進行合并。

  • 合并 Header Search Path、Library Search Path 到一個文件夾:建立專用文件目錄,將相關數據遷移至該專用目錄,然后在 Xcode 中設定該目錄。
  • 合并 ModuleMap 到一個文件:需要注意的是,Xcode 要求我們提供 ModuleMap 文件路徑(而非文件夾路徑),所以這里關鍵在于如何合并 ModuleMap 文件。由于 ModuleMap 編譯時才產生,所以得物在編譯 Pre Action 時,將其他 ModuleMap 內容合并至當前 DerivedData 路徑下的 ModuleMap,同時設定 Xcode 讀取的 ModuleMap 文件路徑。

容器化

隨著工程的成長和業務復雜度的提升,我們一路從工程化、組件化走來,最后走向容器化。在這個過程中,容器化在一定程度上引入了動態性,但動態性并不是工程的容器化階段真正要解決的問題。

容器化真正解決的問題是改變工程的開發模式:容器化將工程師從業務、頁面的開發,轉而變成頁面上某個小卡片小功能的開發,再由容器根據規則,將這些小卡片組裝成為頁面。

從組件化走向容器化是一個自然而然的過程,容器化并不是憑空出現的,容器化是基于上一個階段組件化的組件積累,逐步整合出來的。在得物,容器化還是基于 CTMediator 實現的,這意味著很多我們在組件化階段已積累的組件,是可以在容器化過程中直接使用的。

視圖渲染

針對容器化處理,得物會下發一個協議,然后容器會對該協議進行解析,隨后遞交至渲染器進行渲染,渲染過程中,其會通過 CTMediator 獲取視圖組件。該步驟中,容器實際上就是一個利用現有組件的手段,力求達到 “活字印刷” 的效果。

事件通信

除了視圖組件外,還存在事件需要進行通信。組件往容器傳遞事件,如果此時容器沒有處理事件,則事件通過 CTMediator 繼續進行分發。需要注意的是,事件可以由其他組件處理,也可以由容器自身直接處理(但如埋點等事件,更適合放在容器進行處理)。

除此之外,我們認為以命令模式來描述一個事件更為合理。因為在該模式下,我們告訴了開發者實現事件所有的充要條件。由于命令模式與 CTMediator 的 Target-Action 設計是一樣的體系,因此容器化使用 CTMediator 進行實現則顯得十分自然。

最終得物落地后的容器化可以支持上述功能,基本與 React 類似,但使用的是原生組件構造的容器,相比而言,省去了通信時耗,理論性能更優。

此處再次重申,使用 WebView、Flutter、React、Weex 等容器技術搭建的工程,是具備了容器能力的工程。但在我個人看來,這并不意味著這個工程進入了容器化階段。容器化階段畢竟還是要從組件化逐步成長而來,利用組件化已有的體系和組件,針對已有的頁面進行容器化的升級。

應用

目前容器化已在得物首頁、Tab 頁、活動頁完成部署,由于其用到的均是原生技術,所以無需額外招聘 Flutter、React 等技術棧的人才,也無需搭建額外的基礎設施,過去的組件也基本全部能復用。

總結

工程化、組件化一直是 iOS 業界日久不衰的話題之一。本次分享中,Casa 為我們分享了得物 App 如何在工程演進的過程中,逐漸落地工程化、組件化,并復盤了在落地過程中的遇到的困難以及后續的方案迭代。除此之外,Casa 還提出了更深層次的“容器化”思想,能夠幫助我們在落地組件化之后,更好的面向構件開發。


https://mp.weixin.qq.com/s/Lr6tDxacQKGZ19cKdmNg1w

最多閱讀

iOS 性能檢測新方式?——AnimationHitches 8月以前  |  18043次閱讀
快速配置 Sign In with Apple 2年以前  |  5484次閱讀
APP適配iOS11 3年以前  |  4436次閱讀
App Store 審核指南[2017年最新版本] 3年以前  |  4262次閱讀
所有iPhone設備尺寸匯總 3年以前  |  4184次閱讀
使用 GPUImage 實現一個簡單相機 3年以前  |  3915次閱讀
開篇 關于iOS越獄開發 3年以前  |  3794次閱讀
在越獄的iPhone設置上使用lldb調試 3年以前  |  3719次閱讀
給數組NSMutableArray排序 3年以前  |  3642次閱讀
使用ssh訪問越獄iPhone的兩種方式 3年以前  |  3346次閱讀
UITableViewCell高亮效果實現 3年以前  |  3344次閱讀
關于Xcode不能打印崩潰日志 3年以前  |  3242次閱讀
使用ssh 訪問越獄iPhone的兩種方式 3年以前  |  3083次閱讀
為對象添加一個釋放時觸發的block 3年以前  |  2857次閱讀
使用最高權限操作iPhone手機 3年以前  |  2828次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>