1. 程式人生 > >iOS開發各種底層實現--面試必備!

iOS開發各種底層實現--面試必備!

task 源碼 控件 改變 消息發送 釋放內存 retain select 匹配

iOS開發常用技術底層實現(精簡概述)

本章將對ios開發技術底層實現的總結,其實關於ios開發中各種底層的實現,網上相關文章多到數不過來,而不且非常不錯,我也沒有自信我能比他們做的更好,因為畢竟每個人專研的東西不一樣,本文主要正對三類用戶!

  1. 資深的ios開發者,對底層做過專門研究,但是沒有一個系統整理,或者說不能很清楚的表達。
  2. ios開發初學者,沒有專門研究過底層或者相關源碼的初學者,但是不太建議一開始就看,因為如果你沒有過一點接觸,看了也看不懂,或者看了也白看,最多就是留個印象在腦子了,對初學者來說,切記不能靠背或者了解,而且細細研究每一個技術點,再慢慢深入挖掘。
  3. ios開發待業程序員(面試)專用,不管你有沒有接觸過ios開發相關的底層,只要你是在準備找工作的程序員,我相信你看了絕對有用,但是並不能正面你就真的理解了,所以希望這對你來說只是短暫的,後續得花大量時間去專門研究才能在這條路上走得更遠,不然你永遠只是個碼農!

好了,廢話也不多說了,我們開始吧。。。。。

系統篇
  • 內存管理
  • Runtime
事件篇
  • 事件傳遞
  • 事件響應
代碼篇
  • Block
  • __Block
實戰
  • KVO
  • KVC
高級
  • GCD
全棧篇
  • JSPatch
  • React Native
必備篇
  • 多線程
  • 網絡
  • 數據持久化
通用篇
  • 數組
  • 字典
  • 集合
#寫在最後

系統篇


內存管理

  • 黃金法則

    • 如果一個對象使用了alloc,[mutable] copy,retain,那麽你必須使用相應的release或autonrelease

MRC:

 手動管理內存(retain, release, autorelease,不多說) 持有對象,retain +1 ,引用計數加1, 釋放對象:release -1, 引用計數減1,當引用計數為0時,會自動釋放內存.  autorelease對象內存的管理放到autoreleasepool中, 當pool drain時,回收內存. (這是基於 objective-c的運行時特性和垃圾回收機制)

ARC:

 手動管理內存, 這是xcode4.x版本的特性,(4.1及以前沒有,我從4.6開始的), 原理是:在編譯代碼的時候為你自動在合適的位置插入release 和 autorelease, (運行時處理垃圾回收就如何MRC一樣).

總結: ARC機制擁有和MRC一樣的效率, ARC通過在部分優化和在最合適的地方完成引用計數的維護,所以支持使用ARC.

規則

規則:

  • 1、Objective-C類中實現了引用計數器,對象知道自己當前被引用的次數

  • 2、最初對象的計數器為1

  • 3、如果需要引用對象,可以給對象發送一個retain消息,這樣對象的計數器就加1

  • 4、當不需要引用對象了,可以給對象發送release消息,這樣對象計數器就減1

  • 5、當計數器減到0,自動調用對象的dealloc函數,對象就會釋放內存

  • 6、計數器為0的對象不能再使用release和其他方法

Runtime

一套純低層的C語言庫 平時我們編寫的OC代碼都會轉成Runtime去執行

特性:

動態類型:程序直到執行時才能確定所屬的類。

動態綁定:程序直到執行時才能確定實際要調用的方法。

動態加載:根據需求加載所需要的資源

Runtime消息機制

首先通過obj的isa指針找到obj對應的class。

  • 首先檢測這個 selector 是不是要忽略。比如 Mac OS X 開發,有了垃圾回收就不理會 retain,release 這些函數。
  • 檢測這個 selector 的 target 是不是 nil,Objc 允許我們對一個 nil 對象執行任何方法不會 Crash,因為運行時會被忽略掉。
  • 如果上面兩步都通過了,那麽就開始查找這個類的實現 IMP,
  • 在Class中先去cache中 通過SEL查找對應函數method,找到就執行對應的實現。
  • 若cache中未找到,再去methodList中查找,找到就執行對應的實現。
  • 若methodlist中未找到,則取superClass中查找(重復執行以上兩個步驟),直到找到最根的類為止。
  • 若任何一部能找到,則將method加 入到cache中,以方便下次查找,並通過method中的函數指針跳轉到對應的函數中去執行。
  • 如果以上都不能找到,則會開始進行消息轉發

消息轉發

  • 1.動態方法解析:向當前類發送 resolveInstanceMethod: 信號,檢查是否動態向該類添加了方法。(迷茫請搜索:@dynamic)
  • 2.快速消息轉發:檢查該類是否實現了 forwardingTargetForSelector: 方法,若實現了則調用這個方法。若該方法返回值對象非nil或非self,則向該返回對象重新發送消息。
  • 3.標準消息轉發:runtime發送methodSignatureForSelector:消息獲取Selector對應的方法簽名。返回值非空則通過forwardInvocation:轉發消息,返回值為空則向當前對象發送doesNotRecognizeSelector:消息,程序崩潰退出

總結就是: 在一個函數找不到時,OC提供了三種方式去補救:

  • 1、調用resolveInstanceMethod給個機會讓類添加這個實現這個函數
  • 2、調用forwardingTargetForSelector讓別的對象去執行這個函數
  • 3、調用forwardInvocation(函數執行器)靈活的將目標函數以其他形式執行。 如果都不中,調用doesNotRecognizeSelector拋出異常。

事件篇

應用如何找到最合適的控件來處理事件?

1.首先判斷主窗口(keyWindow)自己是否能接受觸摸事件
2.判斷觸摸點是否在自己身上
3.子控件數組中從後往前遍歷子控件,重復前面的兩個步驟(所謂從後往前遍歷子控件,就是首先查找子控件數組中最後一個元素,然後執行1、2步驟)
4.view,比如叫做fitView,那麽會把這個事件交給這個fitView,再遍歷這個fitView的子控件,直至沒有更合適的view為止。
5.如果沒有符合條件的子控件,那麽就認為自己最合適處理這個事件,也就是自己是最合適的view

事件的傳遞和響應的區別:

  • 事件的傳遞是從上到下(父控件到子控件),事件的響應是從下到上(順著響應者鏈條向上傳遞:子控件到父控件。

事件響應

響應者鏈的事件傳遞過程:
1>如果當前view是控制器的view,那麽控制器就是上一個響應者,事件就傳遞給控制器;如果當前view不是控制器的view,那麽父視圖就是當前view的上一個響應者,事件就傳遞給它的父視圖
2>在視圖層次結構的最頂級視圖,如果也不能處理收到的事件或消息,則其將事件或消息傳遞給window對象進行處理
3>如果window對象也不處理,則其將事件或消息傳遞給UIApplication對象
4>如果UIApplication也不能處理該事件或消息,則將其丟棄

事件傳遞

事件處理的整個流程總結:
  1.觸摸屏幕產生觸摸事件後,觸摸事件會被添加到由UIApplication管理的事件隊列中(即,首先接收到事件的是UIApplication)。
  2.UIApplication會從事件隊列中取出最前面的事件,把事件傳遞給應用程序的主窗口(keyWindow)。
  3.主窗口會在視圖層次結構中找到一個最合適的視圖來處理觸摸事件。(至此,第一步已完成)
  4.最合適的view會調用自己的touches方法處理事件
  5.touches默認做法是把事件順著響應者鏈條向上拋。

代碼篇


Block的底層實現

一句話:

棧地址和對地址值的拷貝

block就是一個裏面存儲了指向函數體中包含定義block時的代碼塊的函數指針,以及block外部上下文變量等信息的結構體。

Block結構體中含有isa指針,這就證明了Block其實就是對象,並具有一般對象的所有功能。這個isa指針被初始化為 NSConcreteStackBlock 或者 NSConcreteGlobalBlock 類的地址。在沒有開啟ARC的情況下,如果Block中包含有局部變量則isa被初始化為前者,否則就被初始化為後者。而當ARC開啟後,如果Block中包含有局部變量則isa被初始化為 NSConcreteMallocBlock ,否則就被初始化為 NSConcreteGlobalBlock 。invoke是一個函數指針,它指向的是Block被轉換成函數的地址。最後的imported variables部分是Block需要訪問的外部的局部變量,他們在編譯就會被拷貝到Block中,這樣一來Block就是成為一個閉包了。

__block底層實現

一句話:傳值 和傳址

block打印C++源碼可以看到Block_byref_a_0結構體,這個結構體中含有isa指針,所以也是一個對象,它是用來包裝局部變量a的。當block被copy到堆中時,Persontest_block_impl_0的拷貝輔助函數Persontest_block_copy_0會將__Block_byref_a_0拷貝至堆中,所以即使局部變量所在堆被銷毀,block依然能對堆中的局部變量進行操作

這樣做是為了保證操作的值始終是堆中的拷貝,而不是棧中的值。(處理在局部變量所在棧還沒銷毀,就調用block來改變局部變量值的情況,如果沒有__forwarding指針,則修改無效)

實戰篇


KVO/KVC

VC/KVO是觀察者模式的一種實現,在Cocoa中是以被萬物之源NSObject類實現的NSKeyValueCoding/NSKeyValueObserving非正式協議的形式被定義為基礎框架的一部分。從協議的角度來說,KVC/KVO本質上是定義了一套讓我們去遵守和實現的方法。 當然,KVC/KVO實現的根本是Objective-C的動態性和runtime,這在後文的原理部分會有詳述。 另外,KVC/KVO機制離不開訪問器方法的實現,這在後文中也有解釋。

1、KVC簡介

全稱是Key-value coding,翻譯成鍵值編碼。顧名思義,在某種程度上跟map的關系匪淺。它提供了一種使用字符串而不是訪問器方法去訪問一個對象實例變量的機制。

2、KVO簡介

全稱是Key-value observing,翻譯成鍵值觀察。提供了一種當其它對象屬性被修改的時候能通知當前對象的機制。再MVC大行其道的Cocoa中,KVO機制很適合實現model和controller類之間的通訊。

KVO的底層實現(基於KVC-》運行時)

當某個類的對象第一次被觀察時,系統就會在運行期動態地創建該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的 setter 方法。

派生類在被重寫的 setter 方法實現真正的通知機制,就如前面手動實現鍵值觀察那樣。這麽做是基於設置屬性會調用 setter 方法,而通過重寫就獲得了 KVO 需要的通知機制。當然前提是要通過遵循 KVO 的屬性設置方式來變更屬性值,如果僅是直接修改屬性對應的成員變量,是無法實現 KVO 的。

同時派生類還重寫了 class 方法以“欺騙”外部調用者它就是起初的那個類。然後系統將這個對象的 isa 指針指向這個新誕生的派生類,因此這個對象就成為該派生類的對象了,因而在該對象上對 setter 的調用就會調用重寫的 setter,從而激活鍵值通知機制。此外,派生類還重寫了 dealloc 方法來釋放資源

isa指針指向的其實是類的元類,如果之前的類名為:Person,那麽被runtime更改以後的類名會變成:NSKVONotifying_Person。 新的NSKVONotifying_Person類會重寫以下方法: 增加了監聽的屬性對應的set方法,class,dealloc,_isKVOA

KVC的底層實現

KVC運用了一個isa-swizzling技術。isa-swizzling就是類型混合指針機制。KVC主要通過isa-swizzling,來實現其內部查找定位的。isa指針,如其名稱所指,(就是is a kind of的意思),指向維護分發表的對象的類。該分發表實際上包含了指向實現類中的方法的指針,和其它數據。

一個對象在調用setValue的時候,

  • (1)首先根據方法名找到運行方法的時候所需要的環境參數。
  • (2)他會從自己isa指針結合環境參數,找到具體的方法實現的接口。
  • (3)再直接查找得來的具體的方法實現。

高級


GCD底層實現

GCD內部是怎麽實現的

  • 1 IOS和OS X的核心是XNU內核,GCD是基於XUN內核實現的
  • 2 GCD的API全部在libdispatch庫中
  • 3 GCD的底層實現主要有Dispatch Queue 和Dispatch Source

    • Dispatch Queue :管理block操作
    • Dispatch Source :處理事件(比如線程間通信)

NSOperationQueue 和GCD的區別和類似的地方

  • 1 GCD是純C語言的API, NSOperationQueue是基於GCD的OC版本封裝
  • 2 GCD只支持FIFO的隊列,NSOperationQueue可以很方便地調整執行順序 設置最大並發數量
  • 3 NSOperationQueue 可以輕松地在operation 間設置依賴關系,而GCD需要寫很多的代碼
  • 4 NSOperationQueue支持KVO,可以監測operation是否正在執行(is Executed),是否結束(is finished),是否取消( is canceld);
  • 5 GCD的執行速度比NSOperationQueue快

全棧篇


JSpatch底層實現

JSPatch 能做到通過 JS 調用和改寫 OC 方法最根本的原因是 Objective-C 是動態語言,OC 上所有方法的調用/類的生成都通過 Objective-C Runtime 在運行時進行,我們可以通過類名/方法名反射得到相應的類和方法:

Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];

也可以替換某個類的方法為新的實現:

static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, selector, newViewDidLoad, @"");

還可以新註冊一個類,為類添加方法:

Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
objc_registerClassPair(cls);
class_addMethod(cls, selector, implement, typedesc);

對於 Objective-C 對象模型和動態消息發送的原理已有很多文章闡述得很詳細,這裏就不詳細闡述了。理論上你可以在運行時通過類名/方法名調用到任何 OC 方法,替換任何類的實現以及新增任意類。所以 JSPatch 的基本原理就是:JS 傳遞字符串給 OC,OC 通過 Runtime 接口調用和替換 OC 方法。這是最基礎的原理,實際實現過程還有很多怪要打,接下來看看具體是怎樣實現的。

總結:

使用JS利用OC的動態特性,執行我們想要執行的代碼

React Native

RN主要的通信在於java與js之間,平常我們寫的jsx代碼最終會調用到原生的View。上一篇博客我們也了解到了要新建一個原生模塊需要在java層和js層分別寫一個Module

特點:

  • 可以基於 React Native使用 JavaScript 編寫應用邏輯,UI 則可以保持全是原生的。這樣的話就沒有必要就 HTML5 的 UI 做出常見的妥協;

  • React 引入了一種與眾不同的、略顯激進但具備高可用性的方案來構建用戶界面。長話短說,應用的 UI 簡單通過一個基於應用目前狀態的函數來表達。

RN總共分為三層,java層,C++層,js層

  • Java層:java層就是app原生代碼,它通過啟動C++層的javascript解析器javascriptCore來執行js代碼,從而構建原生UI等。java層依賴於眾多優秀開源庫,在圖片處理使用的是Fresco,網絡通信使用的是okhttp,Json解析工具用jackson,動畫庫用NineOldAndroids等,在java層原生的功能均封裝為Module,如Toast和Log等。
  • C++層:c++層最主要是封裝了JavaScriptCore,它是一個全新的支持ES6的webKit。Bridge連接了java與js之間的通信。解析js文件是通過JSCExectutor進行的。
  • Js層:主要處理事件分發及UI Layout,平常開發最常用的。通用jsx來寫業務代碼,通過flexbox來實現布局。不依賴DOM。由於react有 DOM diff這個算法,所以它的效率非常高。 通信機制

在Java層與Js層的bridge分別存有相同一份模塊配置表,Java與Js互相通信時,通過將裏配置表中的moduleID,methodID轉換成json數據的形式傳遞給到C++層,C++層傳送到js層,通過js層的的模塊配置表找到對應的方法進行執行,如果有callback,則回傳給java層。這裏只是大概介紹。

總結:

  • 在程序啟動的時候,首先會調用ReactActivity的onCreate函數中,我們會去創建一個ReactInstanceManagerImpl對象。通過ReactRootView的startReactApplication方法開啟整個RN世界的大門。
  • 在這個方法中,我們會通過一個AsyncTask去創建ReactContext
  • 在創建ReactContext中,我們把我們自己註入和CoreModulesPackage通過processPackage方法將其中的各個modules註入到了對應的Registry中。最後通過CatalystInstanceImpl中的ReactBridge將NativeModule和JSModule註冊表通過jni傳輸到了JS層。
  • java調用js時,會在ReactApplicationContext創建的時候存入註冊表類JavaScriptModuleRegistry中,同時通過動態代理生成代理實例,並在代理攔截類JavaScriptModuleInvocationHandler中統一處理發向Javascript的所有通信請求。
  • JSCExecutor將所有來自Java層的通信請求封裝成Javascript執行語句。
  • 接著在js層中的MessageQueue裏匹配ModuleId和MethodId。找到調用模塊。
  • 如果是js層調用java層,js最終都會調用_nativeCall方法,通過flushedQueue將this.queue返回給Bridger。
  • C++層調用PlatformBridgeCallback對象的onCallNativeModules方法,執行makeJavaCall方法,裏面最終通過env->CallVoidMethod調用了Java層的方法。
  • 調用Java層NativeModulesReactCallback的call方法,通過moduleID從保存在其內部的NativeModule映射表,匹配到需要被執行的NativeModule對象,再通過methodID匹配到所要調用的方法。通過invoke反射方式執行NativeModule的方法。

必備篇


多線程
網絡
數據持久化


通用篇


數組
字典
集合

iOS開發各種底層實現--面試必備!