iOS之面試題:阿裡-P6一面-參考思路

来源:http://www.cnblogs.com/rglmuselily/archive/2017/12/14/8038325.html
-Advertisement-
Play Games

阿裡-p6-一面 1.介紹下記憶體的幾大區域? 2.你是如何組件化解耦的? 3.runtime如何通過selector找到對應的IMP地址 4.runloop內部實現邏輯? 5.你理解的多線程? 6.GCD執行原理? 7.怎麼防止別人反編譯你的app? 8.YYAsyncLayer如何非同步繪製? 9. ...


阿裡-p6-一面 

 

1.介紹下記憶體的幾大區域?

2.你是如何組件化解耦的?

3.runtime如何通過selector找到對應的IMP地址

4.runloop內部實現邏輯?

5.你理解的多線程?

6.GCD執行原理?

7.怎麼防止別人反編譯你的app?

8.YYAsyncLayer如何非同步繪製?

9.優化你是從哪幾方面著手?

 

1.介紹下記憶體的幾大區域?

 

1.棧區(stack) 由編譯器自動分配並釋放,存放函數的參數值,局部變數等。棧是系統數據結構,對應線程/進程是唯一的。優點是快速高效,缺點時有限制,數據不靈活。[先進後出]

 

棧空間分靜態分配 和動態分配兩種。

 

 

堆區(heap) 由程式員分配和釋放,如果程式員不釋放,程式結束時,可能會由操作系統回收 ,比如在ios 中 alloc 都是存放在堆中。

 

優點是靈活方便,數據適應面廣泛,但是效率有一定降低。

 

 

雖然程式結束時所有的數據空間都會被釋放回系統,但是精確的申請記憶體,釋放記憶體匹配是良好程式的基本要素。

 

3.全局區(靜態區) (static) 全局變數和靜態變數的存儲是放在一起的,初始化的全局變數和靜態變數存放在一塊區域,未初始化的全局變數和靜態變數在相鄰的另一塊區域,程式結束後有系統釋放。

 

 

4.文字常量區 存放常量字元串,程式結束後由系統釋放;

 

5.代碼區 存放函數的二進位代碼

 

大致如圖:

 

 

例子代碼:

 

 

可能被追問的問題一:

 

1.棧區 (stack [stæk]): 由編譯器自動分配釋放

 

局部變數是保存在棧區的

 

方法調用的實參也是保存在棧區的

 

2.堆區 (heap [hiːp]): 由程式員分配釋放,若程式員不釋放,會出現記憶體泄漏,賦值語句右側 使用 new 方法創建的對象,被創建對象的所有 成員變數!

 

3.BSS 段 : 程式結束後由系統釋放

 

4.數據段 : 程式結束後由系統釋放

 

5.代碼段:程式結束後由系統釋放

 

程式編譯鏈接 後的二進位可執行代碼

 

可能被追問的問題二:

 

比如申請後的系統是如何響應的?

 

棧:存儲每一個函數在執行的時候都會向操作系統索要資源,棧區就是函數運行時的記憶體,棧區中的變數由編譯器負責分配和釋放,記憶體隨著函數的運行分配,隨著函數的結束而釋放,由系統自動完成。

 

註意:只要棧的剩餘空間大於所申請空間,系統將為程式提供記憶體,否則將報異常提示棧溢出。

 

堆:

 

1.首先應該知道操作系統有一個記錄空閑記憶體地址的鏈表。

 

2.當系統收到程式的申請時,會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閑結點鏈表中刪除,並將該結點的空間分配給程式。

 

3 .由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多餘的那部分重新放入空閑鏈表中

 

可能被追問的問題三:

 

比如:申請大小的限制是怎樣的?

 

棧:棧是向低地址擴展的數據結構,是一塊連續的記憶體的區域。是棧頂的地址和棧的最大容量是系統預先規定好的,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數 ) ,如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。

 

堆:堆是向高地址擴展的數據結構,是不連續的記憶體區域。這是由於系統是用鏈表來存儲的空閑記憶體地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於電腦系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。

 

 

棧:由系統自動分配,速度較快,不會產生記憶體碎片

 

堆:是由alloc分配的記憶體,速度比較慢,而且容易產生記憶體碎片,不過用起來最方便

 

打個比喻來說:

 

使用棧就象我們去飯館里吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。

 

使用堆就象是自己動手做喜歡吃的菜餚,比較麻煩,但是比較符合自己的口味,而且自由度大。

 

2.你是如何組件化解耦的?

 

實現代碼的高內聚低耦合,方便多人多團隊開發!

 

一般需要解耦的項目都會多多少少出現,一下幾個情況:

 

耦合比較嚴重(因為沒有明確的約束,「組件」間引用的現象會比較多)

 

2.容易出現衝突(尤其是使用 Xib,還有就是 Xcode Project,雖說有腳本可以改善)

 

3.業務方的開發效率不夠高(只關心自己的組件,卻要編譯整個項目,與其他不相干的代碼糅合在一起)

 

先來看下,組件化之後的一個大概架構

 

 

「組件化」顧名思義就是把一個大的 App 拆成一個個小的組件,相互之間不直接引用。那如何做呢?

 

組件間通信

 

以 iOS 為例,由於之前就是採用的 URL 跳轉模式,理論上頁面之間的跳轉只需 open 一個 URL 即可。所以對於一個組件來說,只要定義「支持哪些 URL」即可,比如詳情頁,大概可以這麼做的

 

 

首頁只需調用[MGJRouter openURL:@"mgj://detail?id=404"]就可以打開相應的詳情頁。

 

那問題又來了,我怎麼知道有哪些可用的 URL?為此,我們做了一個後臺專門來管理。

 

 

然後可以把這些短鏈生成不同平臺所需的文件,iOS 平臺生成 .{h,m} 文件,Android 平臺生成 .java 文件,並註入到項目中。這樣開發人員只需在項目中打開該文件就知道所有的可用 URL 了。

 

目前還有一塊沒有做,就是參數這塊,雖然描述了短鏈,但真想要生成完整的 URL,還需要知道如何傳參數,這個正在開發中。

 

還有一種情況會稍微麻煩點,就是「組件A」要調用「組件B」的某個方法,比如在商品詳情頁要展示購物車的商品數量,就涉及到向購物車組件拿數據。

 

類似這種同步調用,iOS 之前採用了比較簡單的方案,還是依托於MGJRouter,不過添加了新的方法- (id)objectForURL:,註冊時也使用新的方法進行註冊

 

 

使用時NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"]這樣就拿到了購物車裡的商品數。

 

稍微複雜但更具通用性的方法是使用「協議」 <-> 「類」綁定的方式,還是以購物車為例,購物車組件可以提供這麼個 Protocol

 

 

可以看到通過協議可以直接指定返回的數據類型。然後在購物車組件內再新建個類實現這個協議,假設這個類名為MGJCartImpl,接著就可以把它與協議關聯起來[ModuleManagerregisterClass:MGJCartImplforProtocol:@protocol(MGJCart)],對於使用方來說,要拿到這個MGJCartImpl,需要調用[ModuleManagerclassForProtocol:@protocol(MGJCart)]。拿到之後再調用+ (NSInteger)orderCount就可以了。

 

那麼,這個協議放在哪裡比較合適呢?如果跟組件放在一起,使用時還是要先引入組件,如果有多個這樣的組件就會比較麻煩了。所以我們把這些公共的協議統一放到了PublicProtocolDomain.h下,到時只依賴這一個文件就可以了。

 

Android 也是採用類似的方式。

 

組件生命周期管理

 

理想中的組件可以很方便地集成到主客中,並且有跟AppDelegate一致的回調方法。這也是ModuleManager做的事情。

 

先來看看現在的入口方法

 

 

其中[MGJApp startApp]主要負責一些 SDK 的初始化。[self trackLaunchTime]是我們打的一個點,用來監測從main方法開始到入口方法調用結束花了多長時間。其他的都由ModuleManager搞定,loadModuleFromPlist:pathForResource:方法會讀取 bundle 里的一個 plist 文件,這個文件的內容大概是這樣的

 

 

每個Module都實現了ModuleProtocol,其中有一個- (BOOL)applicaiton:didFinishLaunchingWithOptions:方法,如果實現了的話,就會被調用。

 

還有一個問題就是,系統的一些事件會有通知,比如applicationDidBecomeActive會有對應的UIApplicationDidBecomeActiveNotification,組件如果要做響應的話,只需監聽這個系統通知即可。但也有一些事件是沒有通知的,比如- application:didRegisterUserNotificationSettings:,這時組件如果也要做點事情,怎麼辦?

 

一個簡單的解決方法是在AppDelegate的各個方法里,手動調一遍組件的對應的方法,如果有就執行。

 

 

殼工程

 

既然已經拆出去了,那拆出去的組件總得有個載體,這個載體就是殼工程,殼工程主要包含一些基礎組件和業務SDK,這也是主工程包含的一些內容,所以如果在殼工程可以正常運行的話,到了主工程也沒什麼問題。不過這裡存在版本同步問題,之後會說到。

 

遇到的問題

 

組件拆分

 

由於之前的代碼都是在一個工程下的,所以要單獨拿出來作為一個組件就會遇到不少問題。首先是組件的劃分,當時在定義組件粒度時也花了些時間討論,究竟是粒度粗點好,還是細點好。粗點的話比較有利於拆分,細點的話靈活度比較高。最終還是選擇粗一點的粒度,先拆出來再說。

 

假如要把詳情頁遷出來,就會發現它依賴了一些其他部分的代碼,那最快的方式就是直接把代碼拷過來,改個名使用。比較簡單暴力。說起來比較簡單,做的時候也是挺有挑戰的,因為正常的業務並不會因為「組件化」而停止,所以開發同學們需要同時兼顧正常的業務和組件的拆分。

 

版本管理

 

我們的組件包括第三方庫都是通過 Cocoapods 來管理的,其中組件使用了私有庫。之所以選擇 Cocoapods,一個是因為它比較方便,還有就是用戶基數比較大,且社區也比較活躍(活躍到了會時不時地觸發 Github 的 rate limit,導致長時間 clone 不下來···見此),當然也有其他的管理方式,比如 submodule / subtree,在開發人員比較多的情況下,方便、靈活的方案容易占上風,雖然它也有自己的問題。主要有版本同步和更新/編譯慢的問題。

 

假如基礎組件做了個 API 介面升級,這個升級會對原有的介面做改動,自然就會升一個中位的版本號,比如原先是 1.6.19,那麼現在就變成 1.7.0 了。而我們在 Podfile 里都是用~指定的,這樣就會出現主工程的 pod 版本升上去了,但是殼工程沒有同步到,然後群里就會各種反饋編譯不過,而且這個編譯不過的長尾有時能拖上兩三天。

 

然後我們就想了個辦法,如果不在殼工程里指定基礎庫的版本,只在主工程里指定呢,理論上應該可行,只要不出現某個基礎庫要同時維護多個版本的情況。但實踐中發現,殼工程有時會莫名其妙地升不上去,在 podfile 里指定最新的版本又可以升上去,所以此路不通。

 

還有一個問題是pod update時間過長,經常會在Analyzing Dependency上卡 10 多分鐘,非常影響效率。後來排查下來是跟組件的 Podspec 有關,配置了 subspec,且依賴比較多。

 

然後就是 pod update 之後的編譯,由於是源碼編譯,所以這塊的時間花費也不少,接下去會考慮 framework 的方式。

 

持續集成

 

在剛開始,持續集成還不是很完善,業務方升級組件,直接把 podspec 扔到 private repo 里就完事了。這樣最簡單,但也經常會帶來編譯通不過的問題。而且這種隨意的版本升級也不太能保證質量。於是我們就搭建了一套持續集成系統,大概如此

 

 

每個組件升級之前都需要先通過編譯,然後再決定是否升級。這套體系看起來不複雜,但在實施過程中經常會遇到後端的併發問題,導致業務方要麼集成失敗,要麼要等不少時間。而且也沒有一個地方可以呈現當前版本的組件版本信息。還有就是業務方對於這種命令行的升級方式接受度也不是很高。

 

 

基於此,在經過了幾輪討論之後,有了新版的持續集成平臺,升級操作通過網頁端來完成。

 

大致思路是,業務方如果要升級組件,假設現在的版本是 0.1.7,添加了一些 feature 之後,殼工程測試通過,想集成到主工程里看看效果,或者其他組件也想引用這個最新的,就可以在後臺手動把版本升到 0.1.8-rc.1,這樣的話,原先依賴~> 0.1.7的組件,不會升到 0.1.8,同時想要測試這個組件的話,只要手動把版本調到 0.1.8-rc.1 就可以了。這個過程不會觸發 CI 的編譯檢查。

 

當測試通過後,就可以把尾部的-rc.n去掉,然後點擊「集成」,就會走 CI 編譯檢查,通過的話,會在主工程的 podfile 里寫上固定的版本號 0.1.8。也就是說,podfile 里所有的組件版本號都是固定的。

 

 

周邊設施

 

基礎組件及組件的文檔 / Demo / 單元測試

 

無線基礎的職能是為集團提供解決方案,只是在蘑菇街 App 里能 work 是遠遠不夠的,所以就需要提供入口,知道有哪些可用組件,並且如何使用,就像這樣(目前還未實現)

 

 

這就要求組件的負責人需要及時地更新 README / CHANGELOG / API,並且當發生 API 變更時,能夠快速通知到使用方。

 

公共 UI 組件

 

組件化之後還有一個問題就是資源的重覆性,以前在一個工程里的時候,資源都可以很方便地拿到,現在獨立出去了,也不知道哪些是公用的,哪些是獨有的,索性都放到自己的組件里,這樣就會導致包變大。還有一個問題是每個組件可能是不同的產品經理在跟,而他們很可能只關註於自己關心的頁面長什麼樣,而忽略了整體的樣式。公共

 

UI 組件就是用來解決這些問題的,這些組件甚至可以跨 App 使用。(目前還未實現)

 

 

參考答案一:http://blog.csdn.net/GGGHub/article/details/52713642

 

參考答案二:http://limboy.me/tech/2016/03/10/mgj-components.html

 

3.runtime如何通過selector找到對應的IMP地址?

 

概述

 

類對象中有類方法和實例方法的列表,列表中記錄著方法的名詞、參數和實現,而selector本質就是方法名稱,runtime通過這個方法名稱就可以在列表中找到該方法對應的實現。

 

這裡聲明瞭一個指向struct objc_method_list指針的指針,可以包含類方法列表和實例方法列表

 

具體實現

 

在尋找IMP的地址時,runtime提供了兩種方法

 

IMP class_getMethodImplementation(Class cls, SEL name);IMP method_getImplementation(Method m)

 

而根據官方描述,第一種方法可能會更快一些

 

@note c class_getMethodImplementation may be faster than c method_getImplementation(class_getInstanceMethod(cls, name)).

 

對於第一種方法而言,類方法和實例方法實際上都是通過調用class_getMethodImplementation()來尋找IMP地址的,不同之處在於傳入的第一個參數不同

 

類方法(假設有一個類A)

 

class_getMethodImplementation(objc_getMetaClass("A"),@selector(methodName));

 

實例方法

 

class_getMethodImplementation([A class],@selector(methodName));

 

通過該傳入的參數不同,找到不同的方法列表,方法列表中保存著下麵方法的結構體,結構體中包含這方法的實現,selector本質就是方法的名稱,通過該方法名稱,即可在結構體中找到相應的實現。

 

struct objc_method {SEL method_namechar *method_typesIMP method_imp}

 

而對於第二種方法而言,傳入的參數只有method,區分類方法和實例方法在於封裝method的函數

 

類方法

 

Method class_getClassMethod(Class cls, SEL name)

 

實例方法

 

Method class_getInstanceMethod(Class cls, SEL name)

 

最後調用IMP method_getImplementation(Method m)獲取IMP地址

 

實驗

 

 

這裡有一個叫Test的類,在初始化方法里,調用了兩次getIMPFromSelector:方法,第一個aaa方法是不存在的,test1和test2分別為實例方法和類方法

 

 

然後我同時實例化了兩個Test的對象,列印信息如下

 

 

大家註意圖中紅色標註的地址出現了8次:0x1102db280,這個是在調用class_getMethodImplementation()方法時,無法找到對應實現時返回的相同的一個地址,無論該方法是在實例方法或類方法,無論是否對一個實例調用該方法,返回的地址都是相同的,但是每次運行該程式時返回的地址並不相同,而對於另一種方法,如果找不到對應的實現,則返回0,在圖中我做了藍色標記。

 

還有一點有趣的是class_getClassMethod()的第一個參數無論傳入objc_getClass()還是objc_getMetaClass(),最終調用method_getImplementation()都可以成功的找到類方法的實現。

 

而class_getInstanceMethod()的第一個參數如果傳入objc_getMetaClass(),再調用method_getImplementation()時無法找到實例方法的實現卻可以找到類方法的實現。

 

4.runloop內部實現邏輯?

 

 

蘋果在文檔里的說明,RunLoop 內部的邏輯大致如下:

 

 

其內部代碼整理如下 :

 

可以看到,實際上 RunLoop 就是這樣一個函數,其內部是一個 do-while 迴圈。當你調用 CFRunLoopRun() 時,線程就會一直停留在這個迴圈里;直到超時或被手動停止,該函數才會返回。

 

RunLoop 的底層實現

 

從上面代碼可以看到,RunLoop 的核心是基於 mach port 的,其進入休眠時調用的函數是 mach_msg()。為瞭解釋這個邏輯,下麵稍微介紹一下 OSX/iOS 的系統架構。

 

 

蘋果官方將整個系統大致劃分為上述4個層次:

 

應用層包括用戶能接觸到的圖形應用,例如 Spotlight、Aqua、SpringBoard 等。

 

應用框架層即開發人員接觸到的 Cocoa 等框架。

 

核心框架層包括各種核心框架、OpenGL 等內容。

 

Darwin 即操作系統的核心,包括系統內核、驅動、Shell 等內容,這一層是開源的,其所有源碼都可以在opensource.apple.com里找到。

 

我們在深入看一下 Darwin 這個核心的架構:

 

 

其中,在硬體層上面的三個組成部分:Mach、BSD、IOKit (還包括一些上面沒標註的內容),共同組成了 XNU 內核。

 

XNU 內核的內環被稱作 Mach,其作為一個微內核,僅提供了諸如處理器調度、IPC (進程間通信)等非常少量的基礎服務。

 

BSD 層可以看作圍繞 Mach 層的一個外環,其提供了諸如進程管理、文件系統和網路等功能。

 

IOKit 層是為設備驅動提供了一個面向對象(C++)的一個框架。

 

Mach

 

本身提供的 API 非常有限,而且蘋果也不鼓勵使用 Mach 的

 

API,但是這些API非常基礎,如果沒有這些API的話,其他任何工作都無法實施。在 Mach

 

中,所有的東西都是通過自己的對象實現的,進程、線程和虛擬記憶體都被稱為"對象"。和其他架構不同, Mach

 

的對象間不能直接調用,只能通過消息傳遞的方式實現對象間的通信。"消息"是 Mach 中最基礎的概念,消息在兩個埠 (port)

 

之間傳遞,這就是 Mach 的 IPC (進程間通信) 的核心。

 

Mach 的消息定義是在頭文件的,很簡單:

 

typedef struct {

mach_msg_header_t header;

mach_msg_body_t body;

} mach_msg_base_t;

typedef struct {

mach_msg_bits_t msgh_bits;

mach_msg_size_t msgh_size;

mach_port_t msgh_remote_port;

mach_port_t msgh_local_port;

mach_port_name_t msgh_voucher_port;

mach_msg_id_t msgh_id;

} mach_msg_header_t;

 

一條 Mach 消息實際上就是一個二進位數據包 (BLOB),其頭部定義了當前埠 local_port 和目標埠 remote_port,

 

發送和接受消息是通過同一個 API 進行的,其 option 標記了消息傳遞的方向:

 

mach_msg_return_t mach_msg(

mach_msg_header_t *msg,

mach_msg_option_t option,

mach_msg_size_t send_size,

mach_msg_size_t rcv_size,

mach_port_name_t rcv_name,

mach_msg_timeout_t timeout,

mach_port_name_t notify);

 

為了實現消息的發送和接收,mach_msg()

 

函數實際上是調用了一個 Mach 陷阱 (trap),即函數mach_msg_trap(),陷阱這個概念在 Mach

 

中等同於系統調用。當你在用戶態調用 mach_msg_trap() 時會觸發陷阱機制,切換到內核態;內核態中內核實現的 mach_msg()

 

函數會完成實際的工作,如下圖:

 

 

這些概念可以參考維基百科:System_call、Trap_(computing)。

 

RunLoop

 

的核心就是一個 mach_msg() (見上面代碼的第7步),RunLoop 調用這個函數去接收消息,如果沒有別人發送 port

 

消息過來,內核會將線程置於等待狀態。例如你在模擬器里跑起一個 iOS 的 App,然後在 App 靜止時點擊暫停,你會看到主線程調用棧是停留在

 

mach_msg_trap() 這個地方。

 

關於具體的如何利用 mach port 發送信息,可以看看NSHipster 這一篇文章,或者這裡的中文翻譯 。

 

關於Mach的歷史可以看看這篇很有趣的文章:Mac OS X 背後的故事(三)Mach 之父 Avie Tevanian。

 

蘋果用 RunLoop 實現的功能

 

首先我們可以看一下 App 啟動後 RunLoop 的狀態:

 

可以看到,系統預設註冊了5個Mode:

 

1. kCFRunLoopDefaultMode: App的預設 Mode,通常主線程是在這個 Mode 下運行的。

 

2. UITrackingRunLoopMode: 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響。

 

3. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用。

 

4: GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到。

 

5: kCFRunLoopCommonModes: 這是一個占位的 Mode,沒有實際作用。

 

你可以在這裡看到更多的蘋果內部的 Mode,但那些 Mode 在開發中就很難遇到了。

 

5.你理解的多線程?

 

  1. 可能會追問,每種多線程基於什麼語言?

  2. 生命周期是如何管理?

  3. 你更傾向於哪種?追問至現在常用的兩種你的看法是?

 

第一種:pthread

 

.特點:

 

  1. 一套通用的多線程API

  2. 適用於UnixLinuxWindows等系統

  3. 跨平臺可移植

  4. 使用難度大

 

b.使用語言:c語言

c.使用頻率:幾乎不用

d.線程生命周期:由程式員進行管理

 

第二種:NSThread

 

a.特點:

 

1)使用更加面向對象

 

2)簡單易用,可直接操作線程對象

 

b.使用語言:OC語言

 

c.使用頻率:偶爾使用

 

d.線程生命周期:由程式員進行管理

 

第三種:GCD

 

a.特點:

 

1)旨在替代NSThread等線程技術

 

2)充分利用設備的多核(自動)

 

b.使用語言:C語言

 

c.使用頻率:經常使用

 

d.線程生命周期:自動管理

 

第四種:NSOperation

 

a.特點:

  1. 基於GCD(底層是GCD)

  2. 比GCD多了一些更簡單實用的功能

  3. 使用更加面向對象

 

  1. 使用語言:OC語言

  2. 使用頻率:經常使用

  3. 線程生命周期:自動管理

 

多線程的原理

 

同一時間,CPU只能處理1條線程,只有1條線程在工作(執行)

 

多線程併發(同時)執行,其實是CPU快速地在多條線程之間調度(切換)

 

如果CPU調度線程的時間足夠快,就造成了多線程併發執行的假象

 

思考:如果線程非常非常多,會發生什麼情況?

 

CPU會在N多線程之間調度,CPU會累死,消耗大量的CPU資源

 

每條線程被調度執行的頻次會降低(線程的執行效率降低)

 

多線程的優點

 

能適當提高程式的執行效率

 

能適當提高資源利用率(CPU、記憶體利用率)

 

多線程的缺點

 

開啟線程需要占用一定的記憶體空間(預設情況下,主線程占用1M,子線程占用512KB),如果開啟大量的線程,會占用大量的記憶體空間,降低程式的性能

 

線程越多,CPU在調度線程上的開銷就越大

 

程式設計更加複雜:比如線程之間的通信、多線程的數據共用

 

你更傾向於哪一種?

 

傾向於GCD:

 

GCD

 

技術是一個輕量的,底層實現隱藏的神奇技術,我們能夠通過GCD和block輕鬆實現多線程編程,有時候,GCD相比其他系統提供的多線程方法更加有效,當然,有時候GCD不是最佳選擇,另一個多線程編程的技術

 

NSOprationQueue 讓我們能夠將後臺線程以隊列方式依序執行,並提供更多操作的入口,這和 GCD 的實現有些類似。

 

這種類似不是一個巧合,在早期,MacOX

 

與 iOS 的程式都普遍採用Operation

 

Queue來進行編寫後臺線程代碼,而之後出現的GCD技術大體是依照前者的原則來實現的,而隨著GCD的普及,在iOS 4 與 MacOS X

 

10.6以後,Operation Queue的底層實現都是用GCD來實現的。

 

那這兩者直接有什麼區別呢?

 

  • 1.    GCD是底層的C語言構成的API,而NSOperationQueue及相關對象是Objc的對象。在GCD中,在隊列中執行的是由block構成的任務,這是一個輕量級的數據結構;而Operation作為一個對象,為我們提供了更多的選擇;

  • 2.    在NSOperationQueue中,我們可以隨時取消已經設定要準備執行的任務(當然,已經開始的任務就無法阻止了),而GCD沒法停止已經加入queue的block(其實是有的,但需要許多複雜的代碼);

  • 3.    NSOperation能夠方便地設置依賴關係,我們可以讓一個Operation依賴於另一個Operation,這樣的話儘管兩個Operation處於同一個並行隊列中,但前者會直到後者執行完畢後再執行;

  • 4.    我們能將KVO應用在NSOperation中,可以監聽一個Operation是否完成或取消,這樣子能比GCD更加有效地掌控我們執行的後臺任務;

  • 5.    在NSOperation中,我們能夠設置NSOperation的priority優先順序,能夠使同一個並行隊列中的任務區分先後地執行,而在GCD中,我們只能區分不同任務隊列的優先順序,如果要區分block任務的優先順序,也需要大量的複雜代碼;

  • 6.    我們能夠對NSOperation進行繼承,在這之上添加成員變數與成員方法,提高整個代碼的復用度,這比簡單地將block任務排入執行隊列更有自由度,能夠在其之上添加更多自定製的功能。

 

總的來說,Operation

 

queue

 

提供了更多你在編寫多線程程式時需要的功能,並隱藏了許多線程調度,線程取消與線程優先順序的複雜代碼,為我們提供簡單的API入口。從編程原則來說,一般我們需要儘可能的使用高等級、封裝完美的API,在必須時才使用底層API。但是我認為當我們的需求能夠以更簡單的底層代碼完成的時候,簡潔的GCD或許是個更好的選擇,而Operation

 

queue 為我們提供能更多的選擇。

 

傾向於:NSOperation

 

NSOperation相對於GCD:

 

1,NSOperation擁有更多的函數可用,具體查看api。NSOperationQueue 是在GCD基礎上實現的,只不過是GCD更高一層的抽象。

 

2,在NSOperationQueue中,可以建立各個NSOperation之間的依賴關係。

 

3,NSOperationQueue支持KVO。可以監測operation是否正在執行(isExecuted)、是否結束(isFinished),是否取消(isCanceld)

 

4,GCD 只支持FIFO 的隊列,而NSOperationQueue可以調整隊列的執行順序(通過調整權重)。NSOperationQueue可以方便的管理併發、NSOperation之間的優先順序。

 

使用NSOperation的情況:各個操作之間有依賴關係、操作需要取消暫停、併發管理、控制操作之間優先順序,限制同時能執行的線程數量.讓線程在某時刻停止/繼續等。

 

使用GCD的情況:一般的需求很簡單的多線程操作,用GCD都可以了,簡單高效。

 

從編程原則來說,一般我們需要儘可能的使用高等級、封裝完美的API,在必須時才使用底層API。

 

當需求簡單,簡潔的GCD或許是個更好的選擇,而Operation queue 為我們提供能更多的選擇。

 

6.GCD執行原理?

 

GCD有一個底層線程池,這個池中存放的是一個個的線程。之所以稱為“池”,很容易理解出這個“池”中的線程是可以重用的,當一段時間後這個線程沒有被調用胡話,這個線程就會被銷毀。註意:開多少條線程是由底層線程池決定的(線程建議控制再3~5條),池是系統自動來維護,不需要我們程式員來維護(看到這句話是不是很開心?)

 

而我們程式員需要關心的是什麼呢?我們只關心的是向隊列中添加任務,隊列調度即可。

 

  • 如果隊列中存放的是同步任務,則任務出隊後,底層線程池中會提供一條線程供這個任務執行,任務執行完畢後這條線程再回到線程池。這樣隊列中的任務反覆調度,因為是同步的,所以當我們用currentThread列印的時候,就是同一條線程。

 

  • 如果隊列中存放的是非同步的任務,(註意非同步可以開線程),當任務出隊後,底層線程池會提供一個線程供任務執行,因為是非同步執行,隊列中的任務不需等待當前任務執行完畢就可以調度下一個任務,這時底層線程池中會再次提供一個線程供第二個任務執行,執行完畢後再回到底層線程池中。

 

  • 這樣就對線程完成一個復用,而不需要每一個任務執行都開啟新的線程,也就從而節約的系統的開銷,提高了效率。在iOS7.0的時候,使用GCD系統通常只能開5~8條線程,iOS8.0以後,系統可以開啟很多條線程,但是實在開發應用中,建議開啟線程條數:3~5條最為合理。

 

通過案例明白GCD的執行原理

 

案例一:

 

 

分析:

 

首先執行任務1,這是肯定沒問題的,只是接下來,程式遇到了同步線程,那麼它會進入等待,等待任務2執行完,然後執行任務3。但這是隊列,有任務來,當然會將任務加到隊尾,然後遵循FIFO原則執行任務。那麼,現在任務2就會被加到最後,任務3排在了任務2前面,問題來了:

 

任務3要等任務2執行完才能執行,任務2又排在任務3後面,意味著任務2要在任務3執行完才能執行,所以他們進入了互相等待的局面。【既然這樣,那乾脆就卡在這裡吧】這就是死鎖。

 

 

案例二:

 

 

分析:

 

首先執行任務1,接下來會遇到一個同步線程,程式會進入等待。等待任務2執行完成以後,才能繼續執行任務3。從dispatch_get_global_queue可以看出,任務2被加入到了全局的並行隊列中,當並行隊列執行完任務2以後,返回到主隊列,繼續執行任務3。

 

 

案例三:

 

 

案例四:

 

 

分析:

 

首先,將【任務1、非同步線程、任務5】加入Main

 

Queue中,非同步線程中的任務是:【任務2、同步線程、任務4】。所以,先執行任務1,然後將非同步線程中的任務加入到Global

 

Queue中,因為非同步線程,所以任務5不用等待,結果就是2和5的輸出順序不一定。然後再看非同步線程中的任務執行順序。任務2執行完以後,遇到同步線程。將同步線程中的任務加入到Main

 

Queue中,這時加入的任務3在任務5的後面。當任務3執行完以後,沒有了阻塞,程式繼續執行任務4。

 

 

案例五:

 

 

分析:

 

和上面幾個案例的分析類似,先來看看都有哪些任務加入了Main Queue:

 

【非同步線程、任務4、死迴圈、任務5】。

 

在加入到Global Queue非同步線程中的任務有:

 

【任務1、同步線程、任務3】。第一個就是非同步線程,任務4不用等待,

 

所以結果任務1和任務4順序不一定。任務4完成後,程式進入死迴圈,

 

Main Queue阻塞。但是加入到Global Queue的非同步線程不受影響,

 

繼續執行任務1後面的同步線程。同步線程中,將任務2加入到了主線程,

 

並且,任務3等待任務2完成以後才能執行。這時的主線程,已經被死迴圈阻塞了。

 

所以任務2無法執行,當然任務3也無法執行,在死迴圈後的任務5也不會執行。

 

 

7.怎麼防止別人動態在你程式生成代碼?

 

(這題是聽錯了面試官的意思)

 

面試官意思是怎麼防止別人反編譯你的app?

 

1.本地數據加密

 

iOS應用防反編譯加密技術之一:對NSUserDefaults,sqlite存儲文件數據加密,保護帳號和關鍵信息

 

2.URL編碼加密

 

iOS應用防反編譯加密技術之二:對程式中出現的URL進行編碼加密,防止URL被靜態分析

 

3.網路傳輸數據加密

 

iOS應用防反編譯加密技術之三:對客戶端傳輸數據提供加密方案,有效防止通過網路介面的攔截獲取數據

 

4.方法體,方法名高級混淆

 

iOS應用防反編譯加密技術之四:對應用程式的方法名和方法體進行混淆,保證源碼被逆向後無法解析代碼

 

5.程式結構混排加密

 

iOS應用防反編譯加密技術之五:對應用程式邏輯結構進行打亂混排,保證源碼可讀性降到最低

 

6.藉助第三方APP加固,例如:網易雲易盾

 

8.YYAsyncLayer如何非同步繪製?

 

YYAsyncLayer是非同步繪製與顯示的工具。為了保證列表滾動流暢,將視圖繪製、以及圖片解碼等任務放到後臺線程,

 

YYKitDemo

 

對於列表主要對兩個代理方法的優化,一個與繪製顯示有關,另一個與計算佈局有關:

 

Objective-C

 

1-(CGFloat)tableView:(UITableView*)tableViewheightForRowAtIndexPath:(NSIndexPath*)indexPath;

 

2-(UITableViewCell*)tableView:(UITableView*)tableViewcellForRowAtIndexPath:(NSIndexPath*)indexPath;

 

常規邏輯可能覺得應該先調用tableView : cellForRowAtIndexPath :返回UITableViewCell對象,事實上調用順序是先返回UITableViewCell的高度,是因為UITableView繼承自UIScrollView,滑動範圍由屬性contentSize來確定,UITableView的滑動範圍需要通過每一行的UITableViewCell的高度計算確定,複雜cell如果在列表滾動過程中計算可能會造成一定程度的卡頓。

 

假設有20條數據,當前屏幕顯示5條,tableView : heightForRowAtIndexPath :方法會先執行20次返回所有高度並計算出滑動範圍,tableView : cellForRowAtIndexPath :執行5次返回當前屏幕顯示的cell個數。

 

 

從圖中簡單看下流程,從網路請求返回JSON數據,將Cell的高度以及內部視圖的佈局封裝為Layout對象,Cell顯示之前在非同步線程計算好所有佈局對象,並存入數組,每次調用tableView: heightForRowAtIndexPath :只需要從數組中取出,可避免重覆的佈局計算。同時在調用tableView: cellForRowAtIndexPath :對Cell內部視圖非同步繪製佈局,以及圖片的非同步繪製解碼,這裡就要說到今天的主角YYAsyncLayer。

 

YYAsyncLayer

 

首先介紹裡面幾個類:

 

YYAsyncLayer:繼承自CALayer,繪製、創建繪製線程的部分都在這個類。

 

YYTransaction:用於創建RunloopObserver監聽MainRunloop的空閑時間,並將YYTranaction對象存放到集合中。

 

YYSentinel:提供獲取當前值的value(只讀)屬性,以及- (int32_t)increase自增加的方法返回一個新的value值,用於判斷非同步繪製任務是否被取消的工具。

 

 

AsyncDisplay.png

 

上圖是整體非同步繪製的實現思路,後面一步步說明。現在假設需要繪製Label,其實是繼承自UIView,重寫+ (Class)layerClass,在需要重新繪製的地方調用下麵方法,比如setter,layoutSubviews。

 

Objective-C

 

+(Class)layerClass{

returnYYAsyncLayer.class;

}

-(void)setText:(NSString*)text{

_text=text.copy;

[[YYTransactiontransactionWithTarget:selfselector:@selector(contentsNeedUpdated)]commit];

}

-(void)layoutSubviews{

[superlayoutSubviews];

[[YYTransactiontransactionWithTarget:selfselector:@selector(contentsNeedUpdated)]commit];

}

 

YYTransaction有selector、target的屬性,selector其實就是contentsNeedUpdated方法,此時並不會立即在後臺線程去更新顯示,而是將YYTransaction對象本身提交保存在transactionSet的集合中,上圖中所示。

 

Objective-C

+(YYTransaction*)transactionWithTarget:(id)targetselector:(SEL)selector{

if(!target||!selector)returnnil;

YYTransaction*t=[YYTransactionnew];

t.target=target;

t.selector=selector;

returnt;

}

-(void)commit{

if(!_target||!_selector)return;

YYTransactionSetup();

[transactionSetaddObject:self];

}

 

同時在YYTransaction.m中註冊一個RunloopObserver,監聽MainRunloop在kCFRunLoopCommonModes(包含kCFRunLoopDefaultMode、UITrackingRunLoopMode)下的kCFRunLoopBeforeWaiting和kCFRunLoopExit的狀態,也就是說在一次Runloop空閑時去執行更新顯示的操作。

 

kCFRunLoopBeforeWaiting:Runloop將要進入休眠。

 

kCFRunLoopExit:即將退出本次Runloop。

 

Objective-C

 

staticvoidYYTransactionSetup(){

staticdispatch_once_tonceToken;

dispatch_once(&onceToken,^{

transactionSet=[NSMutableSetnew];

CFRunLoopRefrunloop=CFRunLoopGetMain();

CFRunLoopObserverRefobserver;

observer=CFRunLoopObserverCreate(CFAllocatorGetDefault()

kCFRunLoopBeforeWaiting|kCFRunLoopExit,

true,// repeat

0xFFFFFF,// after CATransaction(2000000)

YYRunLoopObserverCallBack,NULL);

CFRunLoopAddObserver(runloop,observer,kCFRunLoopCommonModes);

CFRelease(observer);

});

}

 

下麵是RunloopObserver的回調方法,從transactionSet取出transaction對象執行SEL的方法,分發到每一次Runloop執行,避免一次Runloop執行時間太長。

 

Objective-C

 

staticvoidYYRunLoopObserverCallBack(CFRunLoopObserverRefobserver,CFRunLoopActivityactivity,void*info){

if(transactionSet.count==0)return;

NSSet*currentSet=transactionSet;

transactionSet=[NSMutableSetnew];

[currentSetenumerateObjectsUsingBlock:^(YYTransaction*transaction,BOOL*stop){

#pragma clang diagnostic push

#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

[transaction.targetperformSelector:transaction.selector];

#pragma clang diagnostic pop

}];

}

 

接下來是非同步繪製,這裡用了一個比較巧妙的方法處理,當使用GCD時提交大量併發任務到後臺線程導致線程被鎖住、休眠的情況,創建與程式當前激活CPU數量(activeProcessorCount)相同的串列隊列,並限制MAX_QUEUE_COUNT,將隊列存放在數組中。

 

YYAsyncLayer.m有一個方法YYAsyncLayerGetDisplayQueue來獲取這個隊列用於繪製(這部分YYKit中有獨立的工具YYDispatchQueuePool)。創建隊列中有一個參數是告訴隊列執行任務的服務質量quality of service,在iOS8+之後相比之前系統有所不同。

 

iOS8之前隊列優先順序:

 

  • DISPATCH_QUEUE_PRIORITY_HIGH 2高優先順序

  • DISPATCH_QUEUE_PRIORITY_DEFAULT 0預設優先順序

  • DISPATCH_QUEUE_PRIORITY_LOW (-2)低優先順序

  • DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN後臺優先順序

 

iOS8+之後:

 

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 工作中需要創建SQL Job對資料庫進行定期備份,現把腳本記錄如下。 1. 完整備份: 2. 差異備份: 以上兩段可以分別創建兩個SQL Job,比如完整備份的作業可以定在每周天凌晨運行,執行成功後刪掉之前的備份文件(代碼中有刪除的語句),差異備份的作業定在周一到周六每天凌晨運行。 ...
  • 1什麼會增加主從延遲? 1 網路不好 2 從庫硬體差 3 索引沒做好,從庫執行慢 4 從庫鎖等待,多見於myisam 5 主庫寫頻繁,從庫單線程執行慢 6 使用row複製,或mix使用行複製 2如何優化,減少延遲時間? 1 如何寫頻繁,水平拆分,減少單片寫數量 2 避免複雜DML操作 3幾個因為主從 ...
  • 1. 配置Yum源及關閉SeLinux 2. 安裝桌面環境(如果未安裝) 3. 安裝軟體包 4. 建立用戶 5. 建立文件夾 6. 配置系統核心參數,Oracle用戶資源限制,Oracle用戶環境變數 7. 編輯主機名 8. 解壓安裝文件 9. 開始安裝 [oracle@localhost ~]$ ...
  • 目錄: 前言 SQL Server基礎準備 1.新建資料庫 2.在數據中添加表 3.向表中添加數據 SQL Server與C#基礎準備 實例解析 1.C#連接資料庫 2.查詢特定列數據 3.使用列別名 4.在列上加入計算 5.使用比較設置條件 總結 1.新建資料庫 2.在數據中添加表 3.向表中添加 ...
  • 最近公司的泛微OA無法訪問,Oracle資料庫也無法正常啟動,嘗試了好多方法,終於解決了,先說說基本情況,希望能給碰到同樣問題的朋友帶來一點幫助。 ...
  • 當出現這種問題是網上的解答方案都是一模一樣的! 我這邊先給個地址講的是解決方案http://blog.csdn.net/qq_25827845/article/details/52974991 但是我踩的坑是 我電腦連的是WiFi跟測試真機是一樣的地址!但是我在真機上設置伺服器地址的時候我的ip地址 ...
  • 可以看到QQ上的ToolBar其實就是一個自定義的view,可以看到不同的界面就是簡單地修改了文字而已,在第二張與第三張尤其的明顯,我們就仿QQ的這個Toolbar設置一個自定義控制項 在開始之前,首先瞭解一下官方是如何實現一個控制項的,比如說一個Linearlayout 它不是有layout_widt ...
  • 使用了第三方庫, 有看他們是怎麼實現的嗎? 例:SD、YY、AFN、MJ等! <1>.SD為例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...