iOS底層原理學(xué)習(xí)筆記

  1. 怎樣將oc代碼反編譯成C和C++代碼?
    使用xcode內(nèi)置的LLVM的前端編譯器clang,這樣生成的代碼并不完全是底層實現(xiàn),只是一個參考
    命令:clang -rewrite-objc 文件名稱.m -o 輸出文件名稱.cpp
    指定平臺命令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc oc源文件.m -o 目標(biāo)輸出文件.cpp
    如果需要連接其他框架,-framework 框架名(UIKit)
    添加arc支持 -fobjc-arc -fobjc-runtime=ios-8.0
  2. 獲取oc源碼
    從官網(wǎng)下載,地址:https://opensource.apple.com/tarballs/objc4
  3. 在xcode實時查看內(nèi)存數(shù)據(jù)
    Debug -> Debug workflow -> View Memory
    也可以使用LLDB指令,也就是xcode的控制臺
  4. 常用的LLDB指令
    1. p <====> 打印
    2. po <===> 打印對象
    3. memory read/數(shù)據(jù)格式字節(jié)數(shù) 內(nèi)存地址 <===> 讀取內(nèi)存
    4. x/數(shù)量(幾段) 格式(進(jìn)制數(shù) x=16進(jìn)制 f=浮點 d=10進(jìn)制) 字節(jié)數(shù)(b=1 h=2 w=4 g=8) 內(nèi)存地址 <===> 讀取內(nèi)存
    5. memory write 內(nèi)存地址 數(shù)值 <===> 修改內(nèi)存中的值
    6. step 單步執(zhí)行OC代碼
    7. stepi == si 單步執(zhí)行匯編代碼
    8. continue 繼續(xù)執(zhí)行,跳到下一個斷點
    9. next 單個函數(shù)執(zhí)行
  5. 在控制臺通過函數(shù)體地址獲取函數(shù)信息
    p (IMP)地址
  6. LLVM中間代碼的生成
    OC代碼經(jīng)過LLVM經(jīng)過編譯之后會先生成跨平臺的中間代碼,然后再生成匯編代碼及二進(jìn)制
    生成中間的代碼的命令是:clang -emit-llvm -S 文件名
    這個中間代碼是一種LLVM獨有的語言,官方文檔:https://llvm.org/docs/LangRef.html
  7. 在調(diào)試過程中控制臺查看所有的調(diào)用棧
    控制臺輸入命令:bt
  8. iOS Fundation框架源碼
    GUNStep計劃將OC的庫從新實現(xiàn)一遍并且進(jìn)行開源,源碼接近于蘋果的源碼
    源碼地址:https://www.gunstep.org/resources/downloads.php
  9. GCD源碼
    https://github.com/apple/swift-corelibs-libdispatach
  1. OC對象的本質(zhì)

    • OC中類和對象都是基于C和C++的結(jié)構(gòu)體實現(xiàn)的;
    • OC中的對象分為三種:實例對象、類對象、元類對象
    實例對象的內(nèi)存分配情況
    • OC中的基類就是NSObject,這個純潔的實例對象實際上只有一個成員變量isa,isa是一個指針,在64位系統(tǒng)下占用8個字節(jié)。然而NSObject在實例化后他所分配和占用的空間是16字節(jié),在源碼中有注釋說明一個oc對象的最小占用16個字節(jié);
      其實OC在對象的內(nèi)存分配和占用上做了優(yōu)化,學(xué)名叫'字節(jié)對齊'。在占用內(nèi)存上它規(guī)定結(jié)構(gòu)體的大小必須是最大成員大小的倍數(shù)。在分配內(nèi)存上至少是16或者16的倍數(shù)。
      CPU讀取內(nèi)存的時候分為大端和小端模式,iOS是使用的小端模式,也就是從高位開始讀
    • 實例對象就是通過alloc分配內(nèi)存生成的對象,內(nèi)部結(jié)構(gòu)是一個isa指針和它的成員變量的值;
    • 類對象在內(nèi)存中有且只有一個,通過類和實例都能獲取到類對象。其中包括isa指針,superClass指針,類的屬性信息,類的對象方法信息,類的協(xié)議信息、類的成員變量信息;
    • 元類對象在每個類的內(nèi)存中也只有一個,通過runtime方法object_getClass(類對象)獲取。內(nèi)部結(jié)構(gòu)跟類對象一樣都是Class類型,只是用途不一樣。其中包括:isa指針,superclass指針,類的類方法信息;
  2. isa指針和superclass指針

image.png
  • 實例對象的isa指向類對象,類對象的isa指向元類對象,元類對象的isa指向基元類,基元類的isa則指向自己;
  • 實例對象中沒有superclass指針,只有類對象和元類對象中有,superclass就是指向父類,根元類對象的superclass指向根類對象,根類對象的superclass指向nil;
  • 在64位架構(gòu)以前實例對象的isa地址值直接就是類對象的地址值,在arm64架構(gòu)之后需要用實例對象的isa的地址值 & ISA_MASK(arm64 == 0x0000000ffffffff8 x_86_64 == 0x00007fffffffff8)才能獲取到真正的類對象的地址值。 類對象同樣如此;
  • 目前OC最新的版本是2.0,在源碼中的實現(xiàn)也進(jìn)行了更改,其中最大的變化是對象的結(jié)構(gòu)體實現(xiàn)使用的是c++的方式實現(xiàn)的(其中FASK_DATA_MASK == 0x00007ffffffffff8);
  • 如果想要窺探到類對象中的結(jié)構(gòu),需要仿照源碼寫一套同樣的結(jié)構(gòu)的結(jié)構(gòu)體出來(類實際上就是結(jié)構(gòu)體),然后對類對象進(jìn)行強轉(zhuǎn);


    截屏2021-07-25 上午10.09.40.png
  1. KVO

  • Key Value Observing 健值監(jiān)聽技術(shù),可以對任意對象的屬性進(jìn)行監(jiān)聽;
  • 當(dāng)一個對象A的屬性被監(jiān)聽的后,程序運行中會動態(tài)生成一個NSKVONotifying_A并且繼承自A類的子類 。同時對象A的isa指針會指向這個子類NSKVONotifying_A。在調(diào)用A對象屬性的set方法時,會通過isa指針找到類對象NSKVONotifying_A的set方法。在該set方法中時實現(xiàn)大概是先調(diào)用willChangeValue 方法,再調(diào)用super的set方法,最后再調(diào)用didChangeValue方法并且調(diào)用observer的observerValueForKeypath方法實現(xiàn)監(jiān)聽;
  • 在NSKVONotifying_A類中還實現(xiàn)了class、dealloc、isKVOA方法;
  • 如果想要手動觸發(fā)kvo,可以先調(diào)用對象的willChangeValueForKey方法,再調(diào)用didChangeValueForkey方法;
  1. KVC

  • Key Value Coding 健值編碼技術(shù),通過key獲取或設(shè)置對象的屬性的值;

  • setValue:forKey:調(diào)用流程;
    假如key == name

    1. 先去查找setName的方法實現(xiàn)進(jìn)行賦值;
    2. 如果沒有找到setName方法的實現(xiàn),就會調(diào)用對象的accessInstanceVariablesDirectly方法,查詢是否能夠直接訪問方法變量;
      • 如果是No,直接拋出異常。
      • 如果是Yes,會依次對name, isName, _name, _isName的變量進(jìn)行賦值,如果這個幾個變量都沒有也會拋出異常;
        如果賦值成功,會觸發(fā)KVO
  • getValueForKey:調(diào)用流程;

    1. 先調(diào)用getName方法獲取值;
    2. 如果沒有g(shù)etName的方法實現(xiàn),就會訪問對象的accessInstanceVariablesDirectly方法確認(rèn)是否能直接訪問變量;
      • 如果是NO,直接拋出異常。
      • 如果是YES,依次訪問成員變量_name, _isName, name, isName的值
  1. Category

    • 所有的分類在程序編譯時,會被包裝成category_t結(jié)構(gòu)體,里面存放著,類名,class指針,對象方法和類方法列表,屬性列表,協(xié)議列表;

    • 在程序運行的時候,會將所有分類的數(shù)據(jù)合并到類對象中去。比如分類中的方法列表;

      1. 倒序遍歷分類數(shù)組,取出分類中的方法列表。
      2. 類對象中的方法列表是一個二維數(shù)組進(jìn)行維護(hù)的。將二維數(shù)組擴容,再將類對象中的方法列表移到數(shù)組最后,在所有分類中的方法列表生成的二維數(shù)組,插到類對象的方法二維數(shù)組前面。
    • 所以如果分類中重寫了類對象的方法,會優(yōu)先調(diào)用分類中的方法;

    • 在程序啟動的時候,不管類或者分類有沒有被使用,都會加載compile sources列表中類和分類的load方法。調(diào)用順序會根據(jù)xcode中compile sources列表順序優(yōu)先調(diào)用所有類的load,并且先調(diào)用父類。再根據(jù)compile sources列表順序加載調(diào)用所有的分類的load。在調(diào)用load方法的時候,使用的函數(shù)地址直接調(diào)用,并沒有使用消息發(fā)送機制,所以每個類和分類的load都會調(diào)用;

    • initialize方法只有在類第一次接收消息的時候只會調(diào)用一次,也就是使用objc_messageSend方式,并且會優(yōu)先調(diào)用父類的initialize方法。如果子類中沒有實現(xiàn)initailize,就會調(diào)用父類的initialize,因為走得是消息發(fā)送機制,所以在這種情況下,父類的initialize會被調(diào)用多次。

      關(guān)聯(lián)對象
    • 為分類屬性提供儲存屬性值的地方,通過<objc/runtime.h>api的objc_setAssociatedObject和objc_getAssociatedObject保存和獲取屬性值。 其中的key最好的方案是使用該屬性的get方法,易懂,方便,唯一;
      _cmd 表示當(dāng)前方法的selector

    • 關(guān)聯(lián)對象實現(xiàn)原理:它是由AssociationsManager、AssociationsHasMap、ObjectAssociationMap、ObjectAssociation協(xié)作完成的。跟原來的類對象不發(fā)生關(guān)系。如果給A對象分類的屬性name賦值,流程如下:

      1. 通過AssociationsManager拿到AssociationsHasMap;
      2. 通過對對象A的地址哈希后的值從AssociationsHasMap里面找出ObjectAssociationMap;
      3. 通過objc_setAssociatedObjec方法中傳入的key,在ObjectAssociationMap中找出ObjectAssociation;
      4. 最后將objc_setAssociatedObjec傳入的value和policy存入ObjectAssociation;


        image.png
  2. Block

    • Block本質(zhì)上也是一個oc對象,主要封裝了函數(shù)調(diào)用及函數(shù)調(diào)用的環(huán)境。主要信息有:isa指針,函數(shù)地址,block的描述如Block的size等,捕獲的外部的auto局部變量;

    • 在引用外部變量的時候不同的情況有不同的捕獲機制,只要是auto局部變量就會被捕獲;
      auto 修飾的局部變量是指會自動銷毀的變量。C語言中定義的局部變量默認(rèn)就是auto修飾的。
      在oc函數(shù)中都會隱式的傳入self和_cmd兩個參數(shù)。所以如果block中引用了self或者是self的變量,也會被當(dāng)做局部變量進(jìn)行捕獲。

      image.png

      • block有三種類型,分別是:
        1. 全局block(NSGlobalBlock存放在數(shù)據(jù)區(qū));
        2. 堆block(NSMallocBlock存放在堆區(qū),程序員自己管理);
        3. 棧block(NSStackBlock存放在棧區(qū),自動銷毀);
          他們最終都是繼承自NSBlock,block中的isa就指向這些類對象。
      block類型 生成條件 copy操作
      NSGlobalBlock 內(nèi)部沒有訪問auto變量 什么也不做依然是global類型
      NSStackBlock 內(nèi)部訪問了auto變量在arc下依然會變成malloc類型 變成NSMallocBlock
      NSMallocBlock NSStackBlock 調(diào)用copy 引用計數(shù)增加
      • 在ARC環(huán)境時,block在某些特定的環(huán)境下,棧區(qū)block會自動進(jìn)行copy操作變成堆區(qū)block。如;
        1. block作為函數(shù)返回值時
        2. 使用強指針引用時
        3. 使用usingBlock時,比如數(shù)組的排序方法
        4. GCD的block
      • 棧區(qū)block在內(nèi)部引用了auto的對象變量時,不管該對象是strong修飾還是weak修飾都不會進(jìn)行強引用。但是當(dāng)棧區(qū)block從棧區(qū)拷貝到堆區(qū)時,在block內(nèi)部會自動根據(jù)對象的修飾符進(jìn)行copy操作,如果是strong被引用對象的引用計數(shù)+1,如果是weak,什么都不做。當(dāng)堆區(qū)block在銷毀也會被引用auto對象變量進(jìn)行dispose操作,被引用對象引用計數(shù)-1。
      • __block可以用來修飾auto變量,以達(dá)到可以在block內(nèi)部修改變量的值。
      • 假如__block修飾的auto變量A,在block內(nèi)部會被包裝成一個對象(暫且叫block_A),這個對象內(nèi)部有:isa、forwarding指針、size、變量A,如果A是個對象時,還會多一個copy和dispose的函數(shù)指針(跟外面那層的copy和dispose功能類似)。其中forwarding指針是指向它自己的,block_a中的A就是外面__block修飾的變量A。 當(dāng)這個棧block拷貝到堆區(qū)的時候,block_a中的forwarding指針就會指向堆區(qū)block的block_a,這樣就能保證不管在棧區(qū)還是堆區(qū),訪問的對象都是同一個變量A。
      • 在MRC環(huán)境下,block內(nèi)部對__block修飾的對象類型不會自動進(jìn)行強引用。
      • 解決由于block引用外部對象變量時,產(chǎn)生的循環(huán)引用,造成內(nèi)存泄漏的問題:
        1. 使用__weak修飾外部對象變量。__weak還有一個好處——weak引用的對象銷毀后,指針會設(shè)置為nil;
        2. 使用__unsafe__unretained修飾外部對象變量。這種方案可能導(dǎo)致野指針錯誤,因為對象銷毀后指針還是會指向?qū)ο笏诘牡刂罚?/li>
        3. 使用__block修飾外部對象變量,并且在block內(nèi)部將對象設(shè)置為nil,而且還必須調(diào)用這個block,才會釋放所有對象。但是在MRC環(huán)境下就是跟__unsafe__retained的效果一樣了
      • 說明一下__weak修飾了外部對象變量以后,為什么需要再block內(nèi)部再對對象變量__strong重新去修飾一下,之前我也是不太理解。首先,__strong是為了保證在整個block內(nèi)部生命周期內(nèi),對象變量不會被銷毀;再者,__weak修飾的對象變量如果直接訪問成員變量,編譯器也會報錯,因為對象可能隨時會銷毀。那調(diào)用方法為什么不報錯呢?是因為OC支持nil可以調(diào)用任何方法。
  3. Runtime

    • Object-C是一門動態(tài)性很強的語言,內(nèi)部就是基于runtime實現(xiàn)了動態(tài)性支持;
    isa指針
    • 在arm64架構(gòu)之前,isa就是一個普通的指針,直接存儲著類對象和元類的地址。在arm64之后,isa被優(yōu)化了,使用位域存儲了更多的信息,成了一個union共用體。
      image.png

      - union是共用體,顧名思義,就是內(nèi)存共用的意思。其中struct沒有實際作用,只是相當(dāng)于一個隱式注釋,表明在bits中存儲這些東西并且注明了占用的位數(shù)。 位域就是表示占用了多少位。
      1. nonpointer——地址中是否包含了其他信息
      2. has_assoc——是否包含了關(guān)聯(lián)對象
      3. has_cxx_dtor——是否有c++的析構(gòu)函數(shù)
      4. shiftclas——class指針地址
      5. magic——在調(diào)試時,對象是否未完成初始化
      6. weakly_referenced——是否有被弱引用指向過
      7. deallocating——是否正在釋放
      8. has_sidetable_rc——當(dāng)extra_rc存放的引用計數(shù)放不下時,是否將引用計數(shù)放到sidetable中
      9. extra_rc——存放對象的引用計數(shù)數(shù)值
回顧一下位運算中的與、或、反碼,左移的用法;
1. 與  符號 = & 兩位同時為“1”,結(jié)果才為“1”,否則為0
#假如想取出一個二進(jìn)制數(shù)中的第3位數(shù)值
int a = 1;
#如果result > 0 就代表第三位是1
int result = a & (1 << 3)
2. 或   符號 = | 兩位只要有一個為1,其值為1,否則為0
#如果想改變某一位的值 就用或
a = a | (1 << 3);
3. 反碼 符號 = ~ 取相反的值 如果0b0100  結(jié)果:0b1011
4. 左移 符號 = <<  將數(shù)值以二進(jìn)制的方式向左移,如0001<<4,結(jié)果:1000 
  • 類對象中的屬性協(xié)議等信息是由class_rw_t管理的,但是在類初始化的時候會將類的初始化信息放到class_ro_t中保存的,并且其中存放的屬性方法協(xié)議等信息都是只讀的。類信息初始化完之后再將class_ro_t交給class_rw_t管理。不同的是class_rw_t的是信息是可以修改的。

  • method_t是函數(shù)咋runtime的封裝,里面包含了函數(shù)名SEL、返回值類型和方法類型types、函數(shù)體指針I(yè)MP。
    SEL其實是跟char*差不多的東西,可以通過Selector和sel_registerName獲??;
    types存放了函數(shù)返回值和參數(shù)的類型編碼字符串??梢允褂聾encode查看具體類型的具體編碼。

    char *types = ‘i24@0:8i16f20’
    1. 第一個i 表示返回值是一個int類型, 占用4個字節(jié);
    2. 24 表示函數(shù)的參數(shù)總大小;
    3. @ 表示id類型,占用8個字節(jié),每個函數(shù)都會默認(rèn)傳入兩個參數(shù)slef和_cmd;
    4. 0 表示self是從0位開始;
    5. : 表示方法_cmd,占用8個字節(jié);
    6. 8 代表_cmd從第8位開始;
    7. i 表示int類型形參,占用4個字節(jié);
    8. f 表示float類型的形參,占用4個字節(jié);
    
  • 在類對象內(nèi)部也維護(hù)了一個方法緩存列表cache_t,當(dāng)對象第一次調(diào)用一個方法的時候,為了提高效率,該方法會被緩存到這個列表中,實際上維護(hù)的是一個散列表。


    image.png
  • 那么cache_t是怎樣做到方法緩存優(yōu)化的呢?
    其核心就是維護(hù)了一張散列表(默認(rèn)散列表的長度是4),從算法的角度來講就是以空間換時間。當(dāng)緩存一個名為test的方法時,先使用test & _mask得出散列表的下標(biāo),這個下標(biāo)的值必定是小于_mask的值,然后在將方法名和函數(shù)體指針封裝成bucket_t根據(jù)下標(biāo)放到散列表_buckets中。
    值得注意的是:方法名 & _mask得到的下標(biāo)值index會有重復(fù)的情況,這時候發(fā)現(xiàn)散列表中的下標(biāo)位置是有值的那么就會index會進(jìn)行減1操作直到找到空位置存儲。(當(dāng)然也會判斷下標(biāo)位置中的key是否一致,如果一致直接覆蓋)
    還有存滿的情況,也就是上面那種情況,直到下標(biāo)值到0 還是沒有空位置,就從最大的下標(biāo)繼續(xù)查找直到begain的位置。當(dāng)散列表存滿的時候,就對散列表擴容至原基礎(chǔ)上的兩倍大,再清除緩存,重新存儲。

  • 當(dāng)一個對象調(diào)用一個方法的時候,最終會轉(zhuǎn)成objc_messageSend給class發(fā)送消息,如果最后查到superclass為nil,就會進(jìn)入動態(tài)方法解析流程,如果動態(tài)解析還沒處理就會進(jìn)入消息轉(zhuǎn)發(fā)流程;

  1. 消息發(fā)送流程如下圖:
大致主要流程是:
1. 通過對像的isa找到類對象或元類對象;
2. 從類對象中的緩存cache_t查找,這里假裝沒有;
3. 從類對象中class_rw_t的方法列表中查找,這里又假裝沒有;(如果查到了,會存到對象的cache_t里面去,流程結(jié)束)
4. 從父類中查找,直到superclass為nil,進(jìn)入動態(tài)方法解析流程;(如果父類找到了,回到第3步)
image.png
  1. 動態(tài)方法解析流程:
    動態(tài)方法解析流程只會進(jìn)入一次
1. 不管對象有沒有實現(xiàn)類方法resolveInstanceMethod或resolveClassMethod,都會嘗試調(diào)用并再一次進(jìn)入消息發(fā)送流程。這里假裝實現(xiàn)了;
2. 在方法內(nèi)部可以通過class_addMethod為該方法動態(tài)添加一個實現(xiàn),這里假裝沒有實現(xiàn);
3. 進(jìn)入消息轉(zhuǎn)發(fā)流程(動態(tài)添加實現(xiàn)了 沒有這一步了);
image.png
  1. 消息轉(zhuǎn)發(fā)流程:
    如果是類方法,以下幾種涉及的方法就改成類方法
1. 調(diào)用forwardingTargetForSelector:方法,
返回能處理這個方法的對象。這里假裝返回nil;
2. 調(diào)用methodSignatureForSelector:方法,
返回方法簽名(即返回值、參數(shù)類型編碼)。這里假裝返回了;
3. 調(diào)用forwardingInovacation:方法,
invocation中包含了方法接收者、方法名、方法簽名。執(zhí)行invoke就會重新發(fā)送消息。其實到了這一步就標(biāo)志著流程結(jié)束了;
image.png
  • super的理解:使用super調(diào)用方法,意思就是從父類開始查找這個方法并調(diào)用,但實際接收者還是self。super最終會轉(zhuǎn)成objc_messageSendSuper方法發(fā)送消息,參數(shù)只是比bjc_messageSend多傳了一個父類。
  1. Runloop

  • Runloop就是保證程序不被退出,并且在運行過程中持續(xù)做一些事情。iOS程序中的觸摸事件、定時器、GCD、網(wǎng)絡(luò)等事件都依賴于runloop。runloop能夠做到有事情就馬上處理,沒事情就掛起,不消耗cpu,性能高的特點;

  • Runloop與線程是一一對應(yīng)的,每一個線程都有一個runloop,但是runloop并不是自動創(chuàng)建的,只有在獲取的時候才會去創(chuàng)建runloop。在iOS中主線程的runloop是自動創(chuàng)建的。在程序中有一個全局的字典以線程為key保存著所有的runloop。runloop也會隨著線程銷毀而消失;

  • runloop可以存在多個運行模式model,但是一次只會運行一個模式,在mode進(jìn)行切換的時候,只能先退出(這里的退出并不是循環(huán)),再重新進(jìn)入。每個model中包含了多個source0和source1(觸摸事件等)、timer(定時器事件)、observer(監(jiān)聽事件),沒有這幾個都是空的runloop會立即退出;


    image.png
    1. source0:處理觸摸事件處理和performSelector:onThread:
    2. source1: 處理基于port的線程間通信 和系統(tǒng)事件的捕捉(屏幕觸摸事件先是經(jīng)過source1包裝再交給source0處理的)
    3. Timers: 處理NSTimer 和performSelector:withObject:afterDelay:
    4. Observers: 監(jiān)聽runloop的狀態(tài),處理UI刷新和AutoreleasePool,都是在runloop即將進(jìn)入睡眠之前操作的。
  • 常用的runloop模式有default和tricking,前者是默認(rèn)的模式,后者則是scrollview滾動時的model。commonModels則是包含了這兩種模式;

    • runloop運行邏輯


      image.png
  • runloop的休眠實現(xiàn)原理,runloop的休眠能夠真正做到cpu不做任何事情。這里主要涉及到狀態(tài)的切換,需要休眠就會切換到內(nèi)核態(tài),如果有消息要要處理就從內(nèi)核態(tài)切換到用戶態(tài);

  • runloop控制線程?;?/p>

# 啟動線程永駐
self.thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"begain a  thread ---------------");
        // 添加一個port任務(wù) 不至于讓thread因為沒有任務(wù)導(dǎo)致退出
        [[NSRunLoop currentRunLoop] addPort:NSPort.new forMode:NSDefaultRunLoopMode];
        //每執(zhí)行完一個任務(wù) 這個循環(huán)就會重新執(zhí)行一次
        while (!weakself.isStopThread && weakself != nil) {
            //關(guān)鍵點:runloop內(nèi)部會開一個無限循環(huán)  有任務(wù)做事  無任務(wù)休眠; 默認(rèn)在每執(zhí)行一次任務(wù)就跳出內(nèi)部循環(huán)
            bool result = [NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantFuture];
            NSLog(@"%@",result ? @"runloop 運行成功" : @"runloop 運行失敗");
        }
        NSLog(@"end a thread  -----------------");
    }];
[self.thread start];

#執(zhí)行任務(wù)
if (self.thread) {
        //waitUntilDone: 等到任務(wù)執(zhí)行完才往下走
        [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:true];
}

# 停止線程
// 使用thread執(zhí)行這個方法
 - (void)stopThread {
    
    NSLog(@"%s  %@", __func__, NSThread.currentThread);
    CFRunLoopStop(CFRunLoopGetCurrent());
 
    _stopThread = true;
    self.thread = nil;
}
  1. 多線程

  • 多線程的使用方式有四種:


    image.png
  • 線程的隊列執(zhí)行的方式分為同步和異步兩種,同步執(zhí)行不會重新創(chuàng)建線程,就在當(dāng)前線程執(zhí)行。異步執(zhí)行在新的線程中執(zhí)行,可能會創(chuàng)建新的線程(在主隊列就不會創(chuàng)建新的線程);同步異步主要的區(qū)別是會不會創(chuàng)建新的線程;

  • 隊列的類型分為串行和并發(fā)隊列,串行隊列就是一個個去執(zhí)行任務(wù),異步隊列就會有多個線程同時去執(zhí)行任務(wù);


    image.png
  • 死鎖的情況:如果在串行隊列queue_A中的任務(wù)S1中使用sync添加同步任務(wù)S2并且該任務(wù)的執(zhí)行隊列依然是queue_A,就會產(chǎn)生死鎖。本質(zhì)就是因為S1要執(zhí)行完任務(wù)必須得執(zhí)行完任務(wù)S2,但是由于S2是在S1中執(zhí)行,由于S1跟S2在同一個同步隊列中,所以S2要想執(zhí)行,必須等S1執(zhí)行完。因此就產(chǎn)生了相互等待,就死鎖了;

同步隊列queue_A
S1任務(wù) S2屬于S1中要執(zhí)行的任務(wù)
S2任務(wù) S2也是一個任務(wù),但是它必須排在S1后面執(zhí)行,同時S1又必須得等S2執(zhí)行完
S3任務(wù)
S4任務(wù)
......
  • GCD中g(shù)roup的使用
如果想在一個并發(fā)隊列中先執(zhí)行S1和S1任務(wù),等這個任務(wù)執(zhí)行完之后再執(zhí)行S3。
1.  創(chuàng)建一個group和一個并發(fā)隊列queue;
2. 將S1和S2分別添加到queue和group中;
3. 在使用group的notify執(zhí)行S3;
  • 線程同步方案:
  1. OSSpinLock——自旋鎖,是一種忙等的鎖,相當(dāng)于寫了一個while循環(huán),會一直占用cpu資源。;
  • 初始化:OS_SPINKLOCK_INIT
  • 嘗試加鎖:OSSpinLockTry
  • 加鎖:OSSpinkLockLock
  • 解鎖:OSSpinkLockUnlock

目前這把鎖并不是安全的,它會出現(xiàn)線程優(yōu)先級反轉(zhuǎn)的問題。因為多線程在并發(fā)執(zhí)行的時候,是cpu每一個線程輪流分配一點時間,只是這個時間分配的非常的短,感覺像是同時執(zhí)行的。然而在CPU在分配給線程的時間依賴于線程的優(yōu)先級,如果優(yōu)先級高CPU分配給該線程執(zhí)行任務(wù)的時間會更長。所以講線程A和線程B在同時執(zhí)行同一個任務(wù)的時候,如果線程A的優(yōu)先級高于線程B,在線程B加鎖后,線程A此時進(jìn)來發(fā)現(xiàn)被加鎖了,會在原地一直等待(會一直占用CPU資源)。這是由于線程A的優(yōu)先級更高,所以cpu會一直分配資源給線程A執(zhí)行任務(wù)。這個時候線程B由于優(yōu)先級低導(dǎo)致沒有資源科執(zhí)行任務(wù),也就導(dǎo)致當(dāng)前的任務(wù)執(zhí)行不完,線程B也無法釋放鎖。最終可能形成死鎖;

  1. os_unfair_lock - 為了替代OSSpinkLock,在iOS10.0以后出現(xiàn)的一種鎖,它解決了OSSpinkLock優(yōu)先級反轉(zhuǎn)的問題,在碰到加鎖的時候,不會去忙等,而是睡眠;
  • 初始化:os_unfair_lock_init
  • 嘗試加鎖:os_unfair_lock_try
  • 加鎖:os_unfair_lock_lock
  • 解鎖:os_unfair_lock_unlock
  1. pthread_mutex——從名字來看叫做互斥鎖,在線程等待的時候是睡眠處理?;コ怄i還有另外一種類型PTHREAD_MUTEX_RECURSIVE是遞歸鎖,它的特性是同一個線程可以重復(fù)的加鎖開鎖,如果不是同一線程就會產(chǎn)生互
    斥。
  • 引用庫:pthread.h

  • 初始化:pthread_mutex_init(*, null) 或者PTHREAD_MUTEX_INITIALIZER, 后者是一個結(jié)構(gòu)體宏定義

  • 嘗試加鎖:pthread_mutex_trylock

  • 加鎖:pthread_mutex_lock

  • 解鎖:pthread_mutex_unlock

  • 條件:pthread_cond_t,可以做到解鎖一個線程然后開始睡眠,直到在另外一個線程中通知pthread_cond_t控制睡眠的線程醒來繼續(xù)加鎖并執(zhí)行。
    加鎖睡眠:pthread_cond_wait 通知解鎖睡眠: pthread_cond_signal(通知單個線程) / pthread_cond_broadcast(通知多個線程)。
    最后還需要pthread_cond_destroy銷毀;

  • 銷毀:pthread_mutex_destroy

  1. disptach_semaphore——信號量,用來控制線程最大并發(fā)數(shù)量
  • 創(chuàng)建信號量:dispatch_semephore_create(最大并發(fā)數(shù)量);
  • 開始等待:dispatch_semaphore_wait 就是讓信號值減1,如果信號值等于0的時候就會讓線程休眠等待,直到信號量的值大于0;
  • 信號記錄:dispatch_semaphore_signal 讓信號值加1,并且通知等待的地方;
  1. 串行隊列同步執(zhí)行——將多個線程任務(wù)放到同一個隊列中執(zhí)行;
  2. NSLock 是對mutext普通鎖的封裝
  3. NSRecursiveLock 是對mutext遞歸鎖的封裝
  4. NSCondition 是對pthread_cond和mutex的封裝
  5. NSConditionLock 是對NSCondition進(jìn)一步的封裝
  6. synchronized——是對mutex的封裝,@synchornized(以某個對象為加鎖對象){}。在底層實際上是使用傳進(jìn)去的對象生成另外一個對象,該對象維護(hù)的就是一個遞歸鎖;
  • 這些鎖的性能由高到低排列是:
同步方案的性能表現(xiàn)
us_unfair_lock
OSSPinkLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutext(recursive)
NSRecursiveLock
NSConditionlock
@synchronized
  • 修飾屬性關(guān)鍵字atomic
    如果使用@property(atomic)定義一個屬性A,那么amotic,會在setter和getter方法內(nèi)部加一個鎖,以保證設(shè)置屬性和獲取屬性是線程安全的;
  • 多讀單寫的解決方案, 多讀單寫的要求是多個線程可以并發(fā)進(jìn)行讀操作,只允許同時一個線程進(jìn)行寫操作,并且在寫操作的時候,不能進(jìn)行讀操作;
    1. 讀寫鎖:pthread_rwlock,讀的時候使用pthread_rwlock_rdlock加鎖,寫的時候使用pthread_rwlock_wrlock加鎖;
    2. 在寫操作的時候設(shè)立屏障,使用dispatch_barrier_sync(queue),在讀操作的時候使用dispatch_async(queue),讀寫任務(wù)必須在同一個隊列,而且該隊列必須是自己創(chuàng)建的。如果是全局隊列或者是串行隊列,設(shè)立屏障沒有效果;
  1. 內(nèi)存管理

  • iOS程序的內(nèi)存分布:內(nèi)存被一段一段的分隔開來,從低到高可分為:保留區(qū)域,代碼區(qū)域、數(shù)據(jù)區(qū)域、堆區(qū)、棧區(qū)、內(nèi)核區(qū);

    1. 保留區(qū)域取決于硬件配置,該區(qū)域不能使用;
    2. 代碼區(qū)域就是存放編譯的代碼,所以iOS項目代碼大小是有限制的;
    3. 數(shù)據(jù)區(qū)域存放著字符串常量,已初始化和未初始化的全局變量和靜態(tài)變量;
    4. 堆區(qū)就是可以有程序員控制的區(qū)域,那些通過alloc、malloc、calloc動態(tài)分配的空間。分配地址是從低到高;
    5. 棧區(qū)主要是函數(shù)中產(chǎn)生的開銷,比如局部變量。分配的地址從高到低;
    6. 內(nèi)核區(qū)系統(tǒng)底層用的區(qū)域;
  • Tagged Pointer 標(biāo)記指針。在64位架構(gòu)下,對NSNumber、NSString、NSDate小對象進(jìn)行了優(yōu)化。將這些類型的數(shù)據(jù)和類型直接存儲到了指針中,不需要動態(tài)分配內(nèi)存,維護(hù)引用計數(shù)等。當(dāng)數(shù)據(jù)在指針中放不下時,才會動態(tài)分配內(nèi)存。如果調(diào)用方法,也是調(diào)用objc_sendMessage,顯然標(biāo)記指針對象沒有isa,所以是直接從指針地址中取出對象的數(shù)據(jù),大大的提高了效率。
    如果對象指針地址中的二進(jìn)制的最低或高位(Mac平臺是最低位1,iOS平臺是最高位1 << 63)是1時,就證明該指針是標(biāo)記指針

  • OC對象的內(nèi)存管理

    • 根據(jù)對象的引用計數(shù)器進(jìn)行內(nèi)存管理,當(dāng)對象的計數(shù)器為0的時候就釋放對象
    • xCode支持MRC和ARC兩種管理內(nèi)存模式,MRC就是手動管理內(nèi)存,需要自己對對象進(jìn)行retain、release、autorelease操作。而ARC就是自動管理內(nèi)存,在xcode編譯的時候會在對象需要的地方自動添加retain、release和autorelease代碼;retain對象引用計數(shù)器+1,release對象引用計數(shù)器-1,autorelease在適當(dāng)?shù)臅r候引用計數(shù)器自動-1。當(dāng)調(diào)用方法alloc、new、copy、mutableCopy返回一個對象時,對象的引用計數(shù)器就是1;
    • OC屬性中的關(guān)鍵字,assign == 直接賦值、retain/strong == 在賦值的時候?qū)π轮祌etain對舊值release、copy == 在賦值的時候?qū)εf值release對新值copy;
  • @autoreleasepool自動釋放池實現(xiàn)原理:


    image.png
    • autoreleasepool實際是會轉(zhuǎn)成AutoReleasePoolPage對象,每一個autoreleasepool的大小是4096個字節(jié),其中56個字節(jié)是存放成員變量的,剩余的就是存放調(diào)用了autorelease的對象地址。如果存滿就會創(chuàng)建下一個AutoReleasePoolPage對象,并且上一個page對象的child會指向新創(chuàng)建的page,新創(chuàng)建的page就指向上一個page對象。所以它的整體設(shè)計是一個雙向鏈表的結(jié)構(gòu);
    • @autoreleasepool是可以嵌套的,每個@autoreleasepool一開始就會調(diào)用objc_autoreleasePoolPush,,如果沒有page對象就創(chuàng)建一個,并且往棧內(nèi)放一個POOL_BOUNDARY(其實就是個0),并且返回這個棧頂?shù)牡刂?。每個@autoreleasepool最后都會調(diào)用objc_autoreleasePoolPop(),將一開始放進(jìn)去的POOL_BOUNDARY的地址傳進(jìn)去,表示釋放到這個位置,然后就開始釋放棧內(nèi)的地址,直到POOL_BOUNDARY。
      查看autoreleasepool內(nèi)的情況,通過聲明C函數(shù)extern void _objc_autoreleasePoolPrint()
    • autorelease的對象在什么時候釋放呢?在主線程的runloop中有兩個observer,一個是監(jiān)聽runloop睡眠之前的狀態(tài),還有一個是runloop的退出狀態(tài),這兩個observer都會調(diào)用一次objc_autoreleasePoolPop釋放對象。在睡眠之前的狀態(tài)則還會調(diào)用一次objc_autoreleasePoolPush;
  1. 性能優(yōu)化

  • 在屏幕成像的過程中,CPU和GPU起著至關(guān)重要的作用。
    CPU主要負(fù)責(zé)對象的創(chuàng)建銷毀、對象屬性的調(diào)整計算、布局的計算、文本的計算和排版、圖片的格式轉(zhuǎn)換和解碼、圖像的繪制等;
    GPU主要負(fù)責(zé)紋理的渲染,在CPU計算完的數(shù)據(jù)會交給GPU去處理成渲染數(shù)據(jù),并且放到一個緩沖區(qū)內(nèi)(在iOS中有兩個緩沖區(qū)),每收到一次垂直同步信號,就會從緩沖區(qū)內(nèi)取一幀的數(shù)據(jù)進(jìn)行渲染;
  • 屏幕卡頓的原因:

iOS的屏幕渲染是每秒60幀,也就是每過16毫秒就會收到一次垂直同步信號。在每一次收到垂直信號的時候,如果幀緩沖區(qū)內(nèi)沒有新的數(shù)據(jù),就會顯示上一次的數(shù)據(jù),這就導(dǎo)致掉幀了;

  • 針對CPU的優(yōu)化:
  1. 盡量用輕量級的對象,比如不需要處理事件的圖層,能用CALayer就不用UIView;
  2. 不要頻繁的調(diào)用UIView的相關(guān)屬性,比如frame、bounds、transform等,盡量減少不必要的修改;
  3. 盡量提前計算好布局。在需要時一次性調(diào)整對象的屬性,不要多次修改屬性;
  4. Autolayout布局比frame更耗性能;
  5. 圖片的size最好跟UIImageView的size保持一致,避免重繪帶來的性能損耗;
  6. 控制線程的最大并發(fā)數(shù);
  7. 盡量把耗時的操作放到子線程,比如文本的尺寸計算,繪制,圖片的解碼和繪制;
  • 針對GPU的優(yōu)化:
  1. 盡量避免短時間內(nèi)顯示大量的圖片,盡可能的多張圖片合成一張圖片進(jìn)行展示;
  2. GPU能處理的最大紋理尺寸是4096*4096,一旦超過這個尺寸,就會占用CPU的資源來處理,所以紋理盡量不要超過這個尺寸;
  3. 盡量減少視圖的數(shù)量和層次;
  4. 減少使用透明的視圖,不透明的就設(shè)置opaque= true;
  5. 盡量避免離屏渲染;
  • 離屏渲染
  1. 在openGL中,GPU有兩種渲染方式:
    1. 當(dāng)前屏幕渲染:在當(dāng)前用于顯示的屏幕緩沖區(qū)進(jìn)行渲染操作;
    2. 離屏渲染:在當(dāng)前屏幕緩沖區(qū)意外另外開辟一個緩沖區(qū)進(jìn)行渲染操作;
  2. 離屏渲染因為需要創(chuàng)建新的緩沖區(qū),并且在屏幕渲染的過程中,需要要多次切換上下文環(huán)境,先是從當(dāng)前屏幕切換到離屏緩沖區(qū),等到離屏結(jié)束后,將離屏緩沖區(qū)的渲染結(jié)果渲染到屏幕上,又需要切回當(dāng)前屏幕緩沖區(qū),這樣的操作比較消耗性能;
  3. 觸發(fā)離屏渲染的操作有:
    1. 光柵化,layer.shouldRasterize = true;
    2. 遮罩,layer.mask;
    3. 圓角,同時設(shè)置layer.masksToBounds = true, layer.cornerRadius > 0;
    4. layer.shadowXXX, 如果設(shè)置layer.shadowPath就不會產(chǎn)生離屏渲染;
  • 耗電優(yōu)化
  1. 少用定時器;
  2. 優(yōu)化I/O操作,不要頻繁寫入小數(shù)據(jù),最好批量一次性寫入。讀寫大量重要數(shù)據(jù)時,可以考慮用dispatch_io,其提供了基于GCD的異步操作文件I/O的API,用dispatch_io系統(tǒng)干回優(yōu)化磁盤訪問。數(shù)據(jù)量比較大的,應(yīng)該使用數(shù)據(jù)庫;
  • 網(wǎng)絡(luò)優(yōu)化:
    1. 減少、壓縮網(wǎng)絡(luò)數(shù)據(jù)(json、protobuffer);
    2. 如果多次請求的結(jié)果是相同的,盡量使用緩存;
    3. 使用斷點續(xù)傳,否則網(wǎng)絡(luò)不穩(wěn)定時,可能多次傳輸相同的內(nèi)容;
    4. 網(wǎng)絡(luò)不可用時,不要嘗試執(zhí)行網(wǎng)絡(luò)請求;
    5. 讓用戶可以取消長時間運行或者速度很慢的網(wǎng)絡(luò)操作,設(shè)置合適的超時時間;
    6. 批量傳輸:比如下載視頻流量時,不要傳輸很小的數(shù)據(jù)包,直接下載整個文件或者一大塊一大塊的下載;
  • 定位優(yōu)化:
    1. 如果只需要快速定位確定用戶的位置,最好用CLLOcationManager的requestLocation方法。因為定位完成后,會自動讓定位硬件斷電;
    2. 如果不是導(dǎo)航應(yīng)用,盡量不要實時更新位置,定位完畢后就關(guān)閉定位服務(wù);
    3. 盡量降低定位精度,比如金蓮更不要使用精度最高的KCLLocationAccuracyBest;
    4. 需要后臺定位時,盡量設(shè)置pauseLocationUpdatesAutomatically為true,如果用戶不太可能移動的時候系統(tǒng)會自動暫停位置更新;
  • App啟動優(yōu)化
    • App的啟動分為兩種,一種是冷啟動,就是從零開始啟動app,第二種是熱啟動,App在后臺的時候再次啟動App。 啟動優(yōu)化主要針對前者;
    • 可以通過設(shè)置環(huán)境變量可以打印出app的啟動時間分析,Edit scheme —> Run —> Arguments —> arguments passed on launch,下面添加變量DYLD_PRINT_STATISTICS并設(shè)置為1,如果需要更詳細(xì)的信息DYLD_PRINT_STATISTICS_DETAIL;
    • App啟動分為三個階段,先后分為:dyld、runtime、main函數(shù)
      1. Dyld,Apple的動態(tài)鏈接器,可以用來裝載Mach-O文件(可執(zhí)行文件、動態(tài)庫等);
        1. Dyld會裝載App的可執(zhí)行文件,同時會遞歸連接所有依賴的動態(tài)庫。當(dāng)dyld把可執(zhí)行文件、動態(tài)庫都裝載完畢后,會通知runtime進(jìn)行下一步的處理;
        2. 優(yōu)化方案:
          1. 減少動態(tài)庫。合并一些動態(tài)庫,定期清理不必要的動態(tài)庫;
          2. 減少objc類和分類的數(shù)量、減少selector數(shù)量,定期清理不必要的類和分類;
          3. 減少C++虛函數(shù)的數(shù)量;
          4. swift盡量使用struct;
      2. Runtime會調(diào)用map_images進(jìn)行可執(zhí)行文件內(nèi)容的解析和處理,在load_images中調(diào)用call_load_methods,調(diào)用所有class和category的load方進(jìn)行各種objc結(jié)構(gòu)的初始化(注冊objc類、初始化類對象等等)。還會調(diào)用C++靜態(tài)初始化器和atrribute修飾的函數(shù)。 這樣可執(zhí)行文件和動態(tài)庫中所有的class、protocol、selector、、IMP都已經(jīng)按格式成功加載到內(nèi)存中,被runtime所管理;
        優(yōu)化方案:
        用+initialize方法配合dispatch_once替代attribute((constructor))、C++靜態(tài)構(gòu)造器、Objc的load方法
      3. Main,App的啟動由dyld主導(dǎo),將可執(zhí)行文件加載到內(nèi)存,順便加載所有依賴的動態(tài)庫,并有runtime負(fù)責(zé)加載成objc定義的結(jié)構(gòu)。所有的初始化結(jié)束后,dyld就會調(diào)用main函數(shù),再調(diào)用UIApplicationMain函數(shù),APPDelegate的application:didFinishedLaunchingWithOptions方法。
        優(yōu)化方案:
        在不影響用戶的前提下,盡量將一些耗時操作延遲,不要全部放到finishLoading方法中;
  • 安裝包瘦身:
  • 安裝包主要有可執(zhí)行文件、資源(圖片、音頻、視頻等)組成;
  • 資源可以采用無損壓縮;
  • 去除沒有用到的資源,使用開源項目:https://github.com/tinymind/LSUnusedResources
  • 編譯器優(yōu)化(瘦身可執(zhí)行文件)
  • 將工程的Strip lInked Product 、Make Strinfs read-only、Symbols Hidden by Default 設(shè)置為true(新的Xcode項目這些值默認(rèn)就是true),可以去除不必要的調(diào)試符號;
  • 去掉異常支持,Enable C++ Exception、Enable Objective-Exception設(shè)置為false、Other C Flags添加-fno-exceptions(實際驗證這一步并沒有什么卵用)
  • 利用AppCode(IDE)(https://www.jetbrains.com/objc/)檢測未使用的代碼:菜單欄 -> Code -> Inspect Code
  • LinkMap- 查看每個函數(shù)占用的大小
image.png
  1. 架構(gòu)設(shè)計

  • 架構(gòu)屬于軟件設(shè)計方案,具體可到類與類之間的關(guān)系、模塊與模塊之間的關(guān)系、客戶端與服務(wù)端之間的關(guān)系。在開發(fā)中常用的有MVC、MVP、MVVM,這三種構(gòu)架都是view和model相互解耦,可獨立重復(fù)利用。并且都是以controller、presenter、viewModel為媒介處理view層的需要;

  • 一般來說架構(gòu)可以分成:三層架構(gòu)和四層架構(gòu)。
    三層架構(gòu):界面層、業(yè)務(wù)層、數(shù)據(jù)層;
    四層架構(gòu):界面層、業(yè)務(wù)層、網(wǎng)絡(luò)層、數(shù)據(jù)層;
    前面的MVC、MVP、MVVM,通產(chǎn)常用于界面層的設(shè)計使用;
    優(yōu)秀博客地址:https://github.com/skyming/Trip-to-iOS-Design-Patterns

  • 設(shè)計模式

    image.png

    優(yōu)秀博客地址:https://design-patterns.readthedoc.io/zh_CN/latest

  1. 數(shù)據(jù)結(jié)構(gòu)和算法

推薦書籍:
嚴(yán)蔚敏的《數(shù)據(jù)結(jié)構(gòu)》
《大話數(shù)據(jù)結(jié)構(gòu)和算法》

  1. 網(wǎng)絡(luò)

推薦書籍:
《HTTP權(quán)威指南》
《TCP/IP詳解卷1:協(xié)議》

持續(xù)更新 學(xué)無止境

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容