關(guān)于OC 底層想聊聊

OC 作為一種經(jīng)典的運(yùn)行時(shí)動(dòng)態(tài)語言, 底層的動(dòng)態(tài)運(yùn)行時(shí)、消息機(jī)制另無數(shù)iOS開發(fā)者癡迷.

Runtime:

結(jié)構(gòu)模型
  1. 我們先來看下類的定義:
typedef struct objc_class *Class;

struct objc_class {
  Class  *isa;
  Class  superClass;
  const char* name;
  long  version;
  long  info;
  long  instance_size;
  struct objc_ivar_list *ivar;
  struct objc_method_list *methodLists;
  struct objc_cache *cache;
  struct objc_protocol_list *protocol;
}

2.對(duì)象的定義:

typedef struct objc_object *id;
3.對(duì)象的isa指針:
struct objc_object {
   Class isa;
}

4.方法定義:

typedef struct objc_method *Method;
// objc_method 結(jié)構(gòu)體定義
struct objc_method {
SEL method_name; // 函數(shù)方法(選擇器)
char* method_types ; // 參數(shù)types是一個(gè)描述傳遞給方法的參數(shù)類型的字符數(shù)組披坏,這涉及到類型編碼雾叭。
IMP method_imp; // 函數(shù)指針, 函數(shù)方法通過IMP 指針,找到對(duì)應(yīng)的函數(shù)實(shí)現(xiàn)
}

5.SEL 的定義:

typedef struct objc_selector *SEL;(函數(shù)方法)
// IMP 的定義(函數(shù)指針)
typedef id(*IMP)(id, SEL,...)

6.ivar (成員變量的定義)

typedef struct objc_ivar *ivar;
struct objc_ivar {
  char* objc_ivar;
  char* ivar_type;
  int  ivar_offset;
  int space;
}

7.屬性的定義

typedef struct objc_property  *objc_property_t;

8.分類的定義:

typedef struct objc_category *Category;
struct objc_category {
char* category_name;
char* class_name;
struct objc_method_list *instance_methods;
struct objc_method_list *class_methods;
struct objc_protocol_list *protocols
}

9.protocol 協(xié)議的定義:

typedef struct objc_object *Protocol;

runtime 的一張經(jīng)典的關(guān)系圖:


isa和superClass閉環(huán)圖

為什么要設(shè)計(jì)metaClass?
Objective-C這種借鑒Smalltalk的乎婿,更注重的是消息傳遞,是動(dòng)態(tài)響應(yīng)消息。
消息傳遞對(duì)于面向?qū)ο蟮脑O(shè)計(jì)夺刑,其實(shí)在于給出一種對(duì)消息的解決方案。而面向?qū)ο髢?yōu)點(diǎn)之一的復(fù)用,在這種設(shè)計(jì)里遍愿,更多在于復(fù)用解決方案存淫,而不是單純的類本身。這種思想就如設(shè)計(jì)組件一般沼填,關(guān)心接口桅咆,關(guān)心組合而非類本身。其實(shí)之所以有MetaClass這種設(shè)計(jì)坞笙,我的理解并不是先有MetaClass岩饼,而是在萬物都是對(duì)象的Smalltalk里,向?qū)ο蟀l(fā)送消息的基本解決方案是統(tǒng)一的薛夜,希望復(fù)用的籍茧。而實(shí)例和類之間用的這一套通過isa指針指向的Class單例中存儲(chǔ)方法列表和查詢方法的解決方案的流程,是應(yīng)該在類上復(fù)用的梯澜,而MetaClass就順理成章出現(xiàn)罷了寞冯。

class_copyIvarList()& class_copyPropertyList()區(qū)別?
1). class_copyIvarList()返回的僅僅是對(duì)象類的屬性.
2). class_copyPropertyList()返回類的所有屬性和變量.(包括在@interface大括號(hào)中聲明的變量)

  1. class_rw_t 和 class_ro_t 的區(qū)別?
struct objc_object {
    isa_t isa;
};
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() { 
        return bits.data();
    }
};
struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    Class firstSubclass;
};

class_rw_t: 讀寫, OC的 屬性 方法 協(xié)議信息都保在class_rw_t中
class_ro_t: 只讀, 在編譯期class_ro_t 的結(jié)構(gòu)體就已經(jīng)確定不能更改.

category如何被加載的,兩個(gè)category的load方法的加載順序晚伙,兩個(gè)category的同名方法的加載順序?

1).類的初始化也是動(dòng)態(tài)的吮龄,根類NSObject 的+load 和+initilize兩個(gè)方法,用于類的初始化.
2). 加載順序是父類先+load咆疗,然后子類+load螟蝙,然后分類+load,那么如果分類重寫子類方法:首先子類+load,將方法添加到類的方法列表methodLists民傻,然后分類+load胰默,將重寫的方法添加到方法列表中.
4). 實(shí)際調(diào)用時(shí),調(diào)用的是后添加的方法漓踢,即后添加的方法在方法列表methodLists的這個(gè)數(shù)組的頂部,后+load的類的方法牵署,后添加到方法列表,而這時(shí)的添加方式又是插入頂部添加喧半,即[methodLists insertObject:category_method atIndex:0]; 所以objc_msgSend遍歷方法列表查找SEL 對(duì)應(yīng)的IMP時(shí)奴迅,會(huì)先找到分類重寫的那個(gè),調(diào)用執(zhí)行挺据。然后添加到緩存列表中取具,這樣主類方法實(shí)現(xiàn)永遠(yuǎn)也不會(huì)調(diào)到。
3). 類的加載順序扁耐,決定方法的添加順序暇检,調(diào)用的時(shí)候,后添加的方法會(huì)先被找到婉称,所以調(diào)用的始終是后加載的類的方法實(shí)現(xiàn),類似于入棧出棧.

category & extension區(qū)別块仆,能給NSObject添加Extension嗎构蹬,結(jié)果如何?
category: 分類,有名字, 屬性不實(shí)現(xiàn)setter getter 方法.
extension: 擴(kuò)展,無名字, 可擴(kuò)展 方法 屬性 成員變量.

消息轉(zhuǎn)發(fā)機(jī)制,消息轉(zhuǎn)發(fā)機(jī)制和其他語言的消息機(jī)制優(yōu)劣對(duì)比?
1).消息(傳遞)機(jī)制
RunTime簡稱運(yùn)行時(shí)悔据。就是系統(tǒng)在運(yùn)行的時(shí)候的一些機(jī)制庄敛,其中最主要的是消息機(jī)制。
對(duì)于C語言科汗,函數(shù)的調(diào)用在編譯的時(shí)候會(huì)決定調(diào)用哪個(gè)函數(shù)藻烤。編譯完成之后直接順序執(zhí)行,無任何二義性头滔。OC的函數(shù)調(diào)用稱為消息發(fā)送隐绵。屬于動(dòng)態(tài)調(diào)用過程。在編譯的時(shí)候并不能決定真正調(diào)用哪個(gè)函數(shù)(也就是說拙毫,在編譯階段依许,OC可以調(diào)用任何函數(shù),即使這個(gè)函數(shù)并未實(shí)現(xiàn)缀蹄,只要申明過就不會(huì)報(bào)錯(cuò)峭跳。而C語言在編譯階段就會(huì)報(bào)錯(cuò))。只有在真正運(yùn)行的時(shí)候才會(huì)根據(jù)函數(shù)的名稱找 到對(duì)應(yīng)的函數(shù)來調(diào)用缺前。
來看 這個(gè)函數(shù)是怎么調(diào)用的[obj makeText];

首先蛀醉,編譯器將代碼[obj makeText]轉(zhuǎn)化為objc_msgSend(obj, @selector (makeText)),在objc_msgSend函數(shù)中衅码。首先通過obj的isa指針找到obj對(duì)應(yīng)的Class拯刁。在Class中先去cache中通過SEL查找對(duì)應(yīng)函數(shù)method,若 cache中未找到逝段。再去methodList中查找垛玻,若methodlist中未找到,則取superClass中查找奶躯。若能找到帚桩,則將method加入到cache中,以方便下次查找嘹黔,并通過method中的函數(shù)指針跳轉(zhuǎn)到對(duì)應(yīng)的函數(shù)中去執(zhí)行账嚎。

2).消息轉(zhuǎn)發(fā)機(jī)制(可以間接實(shí)現(xiàn)多重繼承)

在方法調(diào)用的時(shí)候,方法查詢-> 動(dòng)態(tài)解析-> 消息轉(zhuǎn)發(fā) 之前做了什么?

IMP儡蔓、SEL郭蕉、Method的區(qū)別和使用場(chǎng)景?

+(void)load、+(void)initialize方法的區(qū)別什么喂江?在繼承關(guān)系中他們有什么區(qū)別.
+(void)load程序剛啟動(dòng)就會(huì)調(diào)用這個(gè)方法.
在外部初始化本類的時(shí)候,就會(huì)馬上調(diào)用+(void)initialize方法.
程序啟動(dòng) -> load(自動(dòng)調(diào)用) -> [外部初始化本類方法] ->initialize(自動(dòng)調(diào)用) ->調(diào)用本類方法

  1. 說說消息轉(zhuǎn)發(fā)機(jī)制的優(yōu)劣.
內(nèi)存管理
  1. weak的實(shí)現(xiàn)原理召锈?SideTable的結(jié)構(gòu)是什么樣的?
  2. 關(guān)聯(lián)對(duì)象的應(yīng)用?系統(tǒng)如何實(shí)現(xiàn)關(guān)聯(lián)對(duì)象的?
  3. 關(guān)聯(lián)對(duì)象的如何進(jìn)行內(nèi)存管理的开呐?關(guān)聯(lián)對(duì)象如何實(shí)現(xiàn)weak屬性?
  4. Autoreleasepool的原理烟勋?所使用的的數(shù)據(jù)結(jié)構(gòu)是什么?
  5. ARC的實(shí)現(xiàn)原理?ARC下對(duì)retain & release做了哪些優(yōu)化?
  6. ARC下哪些情況會(huì)造成內(nèi)存泄漏?
其他
  1. Method Swizzle注意事項(xiàng)?
  2. 屬性修飾符atomic的內(nèi)部實(shí)現(xiàn)是怎么樣的?能保證線程安全嗎?
-(void)setName:(NSString *)name {
@synchronizaed(self) {
if(_name = name) {
[_name release];
[_name retain];
       }
    }
}
-(*NSString)name {
@synchronizaed(self) {
return _name;
   }
}

原子屬性只能保證內(nèi)部set get 的完整性不受外部影響,但是不能保證線程的絕對(duì)安全, 線程安全還需要線程鎖.

  1. iOS 中內(nèi)省的幾個(gè)方法有哪些筐付?內(nèi)部實(shí)現(xiàn)原理是什么?
    內(nèi)省是(對(duì)象)在(運(yùn)行時(shí))獲取其(類型)的能力;
-(BOOL)isKindOfClass:
-(BOOL)isMemberOfClass:
-(BOOL)respondsToSelector:
+(BOOL)instanceRespondsToSelector:
  1. class卵惦、objc_getClass、object_getclass 方法有什么區(qū)別?

NSNotification相關(guān)

蘋果并沒有開源相關(guān)代碼瓦戚,但是可以讀下GNUStep的源碼沮尿,基本上實(shí)現(xiàn)方式很具有參考性

  1. 實(shí)現(xiàn)原理(結(jié)構(gòu)設(shè)計(jì)、通知如何存儲(chǔ)的较解、name&observer&SEL之間的關(guān)系等)
  2. 通知的發(fā)送時(shí)同步的畜疾,還是異步的
  3. NSNotificationCenter接受消息和發(fā)送消息是在一個(gè)線程里嗎?如何異步發(fā)送消息
  4. NSNotificationQueue是異步還是同步發(fā)送印衔?在哪個(gè)線程響應(yīng)
  5. NSNotificationQueuerunloop的關(guān)系
  6. 如何保證通知接收的線程在主線程
  7. 頁面銷毀時(shí)不移除通知會(huì)崩潰嗎
  8. 多次添加同一個(gè)通知會(huì)是什么結(jié)果啡捶?多次移除通知呢

Runloop & KVO

runloop
  1. app如何接收到觸摸事件的?

  2. 為什么只有主線程的runloop是開啟的?
    1).因?yàn)锳PP啟動(dòng)以后就開啟了主線程,主線程中需要接受各種的用戶操作, 如果不開啟主線程的runLoop 那主線程在執(zhí)行一次任務(wù)后就不是保活狀態(tài), 無法工作.
    2). 大多數(shù)的子線程創(chuàng)建其實(shí)只處理了一件事情,并不需要奔楸海活, 節(jié)省CPU的資源,如果需要毕故睿活,那就手動(dòng)開啟runloop,也保證了runloop 的靈活性.

  3. 為什么只在主線程刷新UI?
    1). 整個(gè)程序的啟點(diǎn)UIApplication 是在主線程初始化的, 所有的用戶事件都在主線程完成.
    2). 而在渲染方面由于圖像的渲染需要以60幀的刷新率在屏幕上 同時(shí)更新,在非主線程異步化的情況下無法確定這個(gè)處理過程能夠?qū)崿F(xiàn)同步更新与帆。
    3).如果Main RunLoop 中的事件需要跨線程進(jìn)行傳輸,就會(huì)導(dǎo)致顯示和用戶事件不能同步.

  4. performSelector和runloop的關(guān)系?

1). 使用performSelector的話一定是在運(yùn)行時(shí)候才能發(fā)現(xiàn),如果此方法不存在就會(huì)crash.
2). 在子線程中無法使用performSelector 的afterDelay 方法,因?yàn)閍fterDelay的方式是使用當(dāng)前線程的runloop 根據(jù)afterDelay參數(shù)創(chuàng)建一個(gè)Timer定時(shí)器后調(diào)用SEL, 無afterDelay的方式是直接調(diào)用SEL, 所以需要在子線程創(chuàng)建runloop.
// 第一種, 子線程創(chuàng)建runloop

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self performSelector:@selector(delayMethod) withObject:nil afterDelay:0];
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"調(diào)用方法==開始");
    sleep(5);
    NSLog(@"調(diào)用方法==結(jié)束");
});

// 第二種,其實(shí)是GCD內(nèi)部已經(jīng)創(chuàng)建好了runloop

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        if ([self respondsToSelector:@selector(delayMethod)]) {
            [self performSelector:@selector(delayMethod) withObject:nil];
        }
});
    
NSLog(@"調(diào)用方法==開始");
sleep(5);
NSLog(@"調(diào)用方法==結(jié)束");
  1. 如何使線程绷硕模活?
    在線程中開啟一個(gè)runloop
KVO

1.實(shí)現(xiàn)原理?
KVO 是基于強(qiáng)大的runtime
當(dāng)一個(gè)A類的屬性發(fā)生改變時(shí), 通過runtime會(huì)創(chuàng)建A的派生類B,B繼承于A.
通過runtime 的(isa-swapping)將A 的isa 指向B.
當(dāng)B的屬性發(fā)生改變后, 就會(huì)重寫A的屬性setter 方法,通知observer發(fā)生改變.

2.如何手動(dòng)關(guān)閉KVO?

  1. 通過KVC修改屬性會(huì)觸發(fā)KVO么?
    會(huì)的, 修改屬性,會(huì)調(diào)用屬性的set 方法, 那么就會(huì)啟動(dòng)KVO的observer.
  2. 哪些情況下使用KVO會(huì)崩潰,怎么預(yù)防崩潰?
    KVO的添加次數(shù)與移出次數(shù)不匹配.
    被觀察者提前釋放.(被觀察者在 dealloc 時(shí)仍然注冊(cè)著 KVO)
    添加了觀察者, 但是未實(shí)現(xiàn)監(jiān)聽方法.
    添加或者移出時(shí),keyPath 為空.
  1. kvo的優(yōu)缺點(diǎn)?
    優(yōu)點(diǎn):能夠提供一種簡單的方法實(shí)現(xiàn)兩個(gè)對(duì)象間的同步.
    用key paths來觀察屬性玄糟,因此也可以觀察嵌套對(duì)象
    完成了對(duì)觀察對(duì)象的抽象勿她,因?yàn)椴恍枰~外的代碼來允許觀察值能夠被觀察.

缺點(diǎn):
我們觀察的屬性必須使用strings來定義.
對(duì)屬性重構(gòu)將導(dǎo)致我們的觀察代碼不再可用.

Block
  1. block的內(nèi)部實(shí)現(xiàn),結(jié)構(gòu)體是什么樣的?
  2. block是類嗎阵翎,有哪些類型?
  3. 一個(gè)int變量被 __block 修飾與否的區(qū)別逢并?block的變量截獲?
  4. block在修改NSMutableArray,需不需要添加__block?
  5. 怎么進(jìn)行內(nèi)存管理的?
  6. block可以用strong修飾嗎?
  7. 解決循環(huán)引用時(shí)為什么要用__strong郭卫、__weak修飾?
  8. block發(fā)生copy時(shí)機(jī)?
  9. Block訪問對(duì)象類型的auto變量時(shí)筒狠,在ARC和MRC下有什么區(qū)別?
多線程
  1. iOS開發(fā)中有多少類型的線程?分別對(duì)比

  2. GCD有哪些隊(duì)列箱沦,默認(rèn)提供哪些隊(duì)列

  3. GCD有哪些方法api

  4. GCD主線程 & 主隊(duì)列的關(guān)系

  5. 如何實(shí)現(xiàn)同步辩恼,有多少方式就說多少

  6. dispatch_once實(shí)現(xiàn)原理

  7. 什么情況下會(huì)死鎖?
    串行隊(duì)列中執(zhí)行同步線程的任務(wù).

  8. 有哪些類型的線程鎖,分別介紹下作用和使用場(chǎng)景

  9. NSOperationQueue中的maxConcurrentOperationCount默認(rèn)值

maxConcurrentOperationCount代表隊(duì)列同一時(shí)間允許執(zhí)行的最多的任務(wù)數(shù)谓形≡钜粒或者理解為同一時(shí)間允許執(zhí)行的最多線程數(shù)。

maxConcurrentOperationCount默認(rèn)為-1寒跳,代表不限制聘萨。

maxConcurrentOperationCount 必須要提前設(shè)置,如果隊(duì)列中添加了操作再設(shè)置maxConcurrentOperationCount就無效了童太。

  1. NSTimer米辐、CADisplayLink胸完、dispatch_source_t 的優(yōu)劣
視圖&圖像相關(guān)
  1. AutoLayout的原理,性能如何?
    背景:
    AutoLayout本質(zhì)就是一個(gè)線性方程解析Engine.

NSLayoutConstraint翘贮,本質(zhì)上是表示兩個(gè)視圖之間布局關(guān)系的一個(gè)線性方程赊窥,該方程可以是線性等式、也可以是線性不等式狸页。
多個(gè)約束對(duì)象組成是一個(gè)約束集合锨能,本質(zhì)上是表示某個(gè)界面上多個(gè)視圖之間布局關(guān)系的線性方程組。方程組中的多個(gè)線性方程芍耘,以數(shù)字標(biāo)識(shí)的優(yōu)先級(jí)進(jìn)行排序(UILayoutPriority址遇,本質(zhì)上是浮點(diǎn)型float.


Auto Layout 基本原理
  1. UIView & CALayer的區(qū)別?

  2. 事件響應(yīng)者鏈?
    把Event事件加入到UIApplication隊(duì)列中, UIResponser ->UIApplication-> UIWindow ->UIView

  3. drawrect & layoutsubviews調(diào)用時(shí)機(jī)?

drawRect調(diào)用時(shí)機(jī):

如果在UIView初始化時(shí)沒有設(shè)置frame,會(huì)導(dǎo)致drawRect不被自動(dòng)調(diào)用.
sizeToFit后會(huì)調(diào)用斋竞。這時(shí)候可以先用sizeToFit中計(jì)算出size倔约,然后系統(tǒng)自動(dòng)調(diào)用drawRect方法.
通過設(shè)置contentMode為.redraw時(shí),那么在每次設(shè)置或更改frame的時(shí)候自動(dòng)調(diào)用drawRect.
直接調(diào)用setNeedsDisplay坝初,或者setNeedsDisplayInRect會(huì)觸發(fā)drawRect.

layoutSubViews調(diào)用時(shí)機(jī):

init初始化不會(huì)調(diào)用layoutSubviews方法.

addSubview時(shí)會(huì)調(diào)用.
改變一個(gè)UIView的frame時(shí)會(huì)調(diào)用.
(滾動(dòng)一個(gè)UIScrollView導(dǎo)致UIView重新布局時(shí)會(huì)調(diào)用.
旋轉(zhuǎn)Screen會(huì)觸發(fā)父UIView上的事件.
手動(dòng)調(diào)用setNeedsLayout或者layoutIfNeeded.)

  1. UI的刷新原理?
    當(dāng)在操作 UI 時(shí)跺株,比如改變了 Frame、更新了 UIView/CALayer 的層次時(shí)脖卖,或者手動(dòng)調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后乒省,這個(gè) UIView/CALayer 就被標(biāo)記為待處理,并被提交到一個(gè)全局的容器去畦木。
    RunLoop的 Observer會(huì)監(jiān)聽 BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件袖扛,回調(diào)去執(zhí)行一個(gè)很長的函數(shù):
    _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個(gè)函數(shù)里會(huì)遍歷所有待處理的 UIView/CAlayer 以執(zhí)行實(shí)際的繪制和調(diào)整十籍,并更新 UI 界面蛆封。

  2. 隱式動(dòng)畫 & 顯示動(dòng)畫區(qū)別?
    顯式動(dòng)畫是指用戶自己通過beginAnimations:context:和commitAnimations創(chuàng)建的動(dòng)畫。
    隱式動(dòng)畫是指通過UIView的animateWithDuration:animations:方法創(chuàng)建的動(dòng)畫勾栗。
    隱式動(dòng)畫是ios4之后引入sdk的惨篱,之前只有顯式動(dòng)畫。從官方的介紹來看围俘,兩者并沒有什么差別砸讳,甚至蘋果還推薦使用隱式 動(dòng)畫,但是這里面有一個(gè)問題界牡,就是使用隱式動(dòng)畫后簿寂,View會(huì)暫時(shí)不能接收用戶的觸摸、滑動(dòng)等手勢(shì)宿亡。
    這就造成了當(dāng)一個(gè)列表滾動(dòng)時(shí)常遂,如果對(duì)其中的view使用了隱式動(dòng)畫,就會(huì)感覺滾動(dòng)無法主動(dòng)停止下來挽荠,必須等動(dòng)畫結(jié)束了才能停止克胳。

  3. 什么是離屏渲染?
    GPU在當(dāng)前顯示緩存區(qū)之外又開辟新的緩存區(qū),就是離屏渲染.

  4. imageName & imageWithContentsOfFile區(qū)別

  5. 多個(gè)相同的圖片平绩,會(huì)重復(fù)加載嗎?

  6. 圖片是什么時(shí)候解碼的,如何優(yōu)化?
    一般我們使用的圖像是JPEG/PNG漠另,這些圖像數(shù)據(jù)不是位圖捏雌,而是是經(jīng)過編碼壓縮后的數(shù)據(jù),需要線將它解碼轉(zhuǎn)成位圖數(shù)據(jù)酗钞,然后才能把位圖渲染到屏幕上腹忽。

當(dāng)你用 UIImage 或 CGImageSource 的那幾個(gè)方法創(chuàng)建圖片時(shí)来累,圖片數(shù)據(jù)并不會(huì)立刻解碼砚作。圖片設(shè)置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前嘹锁,CGImage 中的數(shù)據(jù)才會(huì)得到解碼葫录。這一步是發(fā)生在主線程的,并且不可避免领猾。

圖片加載的工作流

概括來說米同,從磁盤中加載一張圖片,并將它顯示到屏幕上摔竿,中間的主要工作流如下:

  1. 假設(shè)我們使用 +imageWithContentsOfFile: 方法從磁盤中加載一張圖片面粮,這個(gè)時(shí)候的圖片并沒有解壓縮;
  2. 然后將生成的 UIImage 賦值給 UIImageView 继低;
  3. 接著一個(gè)隱式的 CATransaction 捕獲到了 UIImageView 圖層樹的變化熬苍;
  4. 在主線程的下一個(gè) run loop 到來時(shí),Core Animation 提交了這個(gè)隱式的 transaction 袁翁,這個(gè)過程可能會(huì)對(duì)圖片進(jìn)行 copy 操作柴底,而受圖片是否字節(jié)對(duì)齊等因素的影響,這個(gè) copy 操作可能會(huì)涉及以下部分或全部步驟:
    1. 分配內(nèi)存緩沖區(qū)用于管理文件 IO 和解壓縮操作粱胜;
    2. 將文件數(shù)據(jù)從磁盤讀到內(nèi)存中柄驻;
    3. 將壓縮的圖片數(shù)據(jù)解碼成未壓縮的位圖形式,這是一個(gè)非常耗時(shí)的 CPU 操作焙压;
    4. 最后 Core Animation 使用未壓縮的位圖數(shù)據(jù)渲染 UIImageView 的圖層鸿脓。

在上面的步驟中,我們提到了圖片的解壓縮是一個(gè)非常耗時(shí)的 CPU 操作涯曲,并且它默認(rèn)是在主線程中執(zhí)行的答憔。那么當(dāng)需要加載的圖片比較多時(shí),就會(huì)對(duì)我們應(yīng)用的響應(yīng)性造成嚴(yán)重的影響掀抹,尤其是在快速滑動(dòng)的列表上虐拓,這個(gè)問題會(huì)表現(xiàn)得更加突出。

圖像的解碼
解碼操作是比較耗時(shí)的傲武,并且沒有GPU硬解碼蓉驹,只能通過CPU城榛,iOS默認(rèn)會(huì)在主線程對(duì)圖像進(jìn)行解碼。解碼過程是一個(gè)相當(dāng)復(fù)雜的任務(wù)态兴,需要消耗非常長的時(shí)間狠持。60FPS ≈ 0.01666s per frame = 16.7ms per frame,這意味著在主線程超過16.7ms的任務(wù)都會(huì)引起掉幀瞻润。很多庫都解決了圖像解碼的問題喘垂,不過由于解碼后的圖像太大,一般不會(huì)緩存到磁盤绍撞,SDWebImage的做法是把解碼操作從主線程移到子線程正勒,讓耗時(shí)的解碼操作不占用主線程的時(shí)間。

對(duì)于PNG圖片來說傻铣,因?yàn)槲募赡芨笳抡辏约虞d會(huì)比JPEG更長,但是解碼會(huì)相對(duì)較快非洲,而且Xcode會(huì)把PNG圖片進(jìn)行解碼優(yōu)化之后引入工程鸭限。JPEG圖片更小,加載更快两踏,但是解壓的步驟要消耗更長的時(shí)間败京,因?yàn)镴PEG解壓算法比基于zip的PNG算法更加復(fù)雜。
當(dāng)加載圖片的時(shí)候梦染,iOS通常會(huì)延遲解壓圖片的時(shí)間赡麦,直到加載到內(nèi)存之后。因?yàn)樾枰诶L制之前進(jìn)行解壓弓坞,這就會(huì)在準(zhǔn)備繪制圖片的時(shí)候影響性能隧甚。
iOS通常會(huì)延時(shí)解壓圖片,等到圖片在屏幕上顯示的時(shí)候解壓圖片渡冻。解壓圖片是非常耗時(shí)的操作戚扳。

圖像解碼的核心方法CGBitmapContextCreate

CGContextRef CGBitmapContextCreate(
void * data,
size_t width,
size_t height,
size_t bitsPerComponent,
size_t bytesPerRow,
CGColorSpaceRef _Nullable space,
uint32_t bitmapInfo)

  1. 圖片渲染怎么優(yōu)化?
    圖片的顯示分為三步:加載、解碼族吻、渲染帽借。
    通常,我們操作的只有加載超歌,解碼和渲染是由UIKit進(jìn)行砍艾。

什么是解碼?
以UIImageView為例巍举。當(dāng)其顯示在屏幕上時(shí)脆荷,需要UIImage作為數(shù)據(jù)源。
UIImage持有的數(shù)據(jù)是未解碼的壓縮數(shù)據(jù),能節(jié)省較多的內(nèi)存和加快存儲(chǔ)蜓谋。
解碼是一個(gè)計(jì)算量較大的任務(wù)梦皮,且需要CPU來執(zhí)行;并且解碼出來的圖片體積與圖片的寬高有關(guān)系桃焕,而與圖片原來的體積無關(guān)剑肯。
其體積大小可簡單描述為:寬 * 高 * 每個(gè)像素點(diǎn)的大小 = width * height * 4bytes。

解碼原理

圖像解碼操作會(huì)造成什么問題观堂?
以我們常見的UITableView和UICollectionView為例让网,假如我們?cè)谑褂靡粋€(gè)多圖片顯示的功能:
在上下滑動(dòng)顯示圖片的過程中,我們會(huì)在cellFor的方法加載UIImage圖片师痕、賦值給UIImageView溃睹,相當(dāng)于在主線程同時(shí)進(jìn)行IO操作、解碼操作等七兜,會(huì)造成內(nèi)存迅速增長和CPU負(fù)載瞬間提升丸凭。
并且內(nèi)存的迅速增加會(huì)觸發(fā)系統(tǒng)的內(nèi)存回收機(jī)制福扬,嘗試回收其他后臺(tái)進(jìn)程的內(nèi)存腕铸,增加CPU的工作量。如果系統(tǒng)無法提供足夠的內(nèi)存铛碑,則會(huì)先結(jié)束其他后臺(tái)進(jìn)程狠裹,最終無法滿足的話會(huì)結(jié)束當(dāng)前進(jìn)程。

那么如何對(duì)這種情況進(jìn)行優(yōu)化 汽烦?

優(yōu)化1:降采樣

在滑動(dòng)顯示的過程中涛菠,圖片顯示的寬高遠(yuǎn)比真實(shí)圖片要小,我們可以采用加載縮略圖的方式減少圖片的占用內(nèi)存撇吞。
如下圖所示:


截屏2021-03-02 下午9.28.38.png

我們加載jpeg的圖片俗冻,然后進(jìn)行相關(guān)設(shè)置,解碼后根據(jù)設(shè)置生成CGImage縮略圖牍颈,最后包裝成UIImage迄薄,最終傳遞給UIImageView渲染。
思考:這里的解碼步驟為何不是上文提到的imageView.image=image時(shí)機(jī)煮岁?

func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let downsampleOptions = [kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary
let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
return UIImage(cgImage: downsampledImage)
}

我的理解:正常的UIImage加載是從APP本地讀取讥蔽,或者從網(wǎng)絡(luò)下載圖片,此時(shí)不涉及圖片內(nèi)容相關(guān)的操作画机,并不需要解碼冶伞;當(dāng)圖片被賦值給UIImageView時(shí),CALayer讀取圖片內(nèi)容進(jìn)行渲染步氏,所以需要對(duì)圖片進(jìn)行解碼响禽;
而上文的縮略圖生成過程中,已經(jīng)對(duì)圖片進(jìn)行解碼操作,此時(shí)的UIImage只是一個(gè)CGImage的封裝芋类,所以當(dāng)UIImage賦值給UIImageView時(shí)瀑焦,CALayer可以直接使用CGImage所持有的圖像數(shù)據(jù)。

優(yōu)化2:異步處理

從用戶的體驗(yàn)來分析梗肝,滑動(dòng)的操作往往是間斷性觸發(fā)榛瓮,在滑動(dòng)的瞬間有較大的工作量,而且由于都是在主線程進(jìn)行操作無法進(jìn)行任務(wù)分配巫击,CPU 2處于閑置禀晓。由此引申出兩種優(yōu)化手段:Prefetching(預(yù)處理)和
Background decoding/downsampling(子線程解碼和降采樣)。綜合起來坝锰,可以在Prefetching的時(shí)候把降采樣放到子線程進(jìn)行處理粹懒,因?yàn)榻挡蓸舆^程就包括解碼操作。

Prefetching回調(diào)中顷级,把降采樣的操作放到同步隊(duì)列serialQueue中凫乖,處理完畢之后拋給主線程進(jìn)行update操作。
需要特別注意弓颈,此處不能是并發(fā)隊(duì)列帽芽,否則會(huì)造成線程爆炸,原因見總結(jié)部分翔冀。

截屏2021-03-02 下午10.05.07.png

優(yōu)化3:使用Image Asset Catalogs

Apple推薦的圖片資源管理工具导街,壓縮效率更高,在iOS 12的機(jī)器上有10~20%的空間節(jié)約纤子,并且每個(gè)版本Apple都會(huì)持續(xù)對(duì)其進(jìn)行優(yōu)化搬瑰。
內(nèi)容較多,詳細(xì)可點(diǎn)Session控硼。

總結(jié)

應(yīng)用上述的優(yōu)化策略泽论,已經(jīng)能對(duì)圖片加載有比較好的優(yōu)化。

原文如下:
Thread Explosion(線程爆炸)
More images to decode than available CPUs(解碼圖像數(shù)量大于CPU數(shù)量)
GCD continues creating threads as new work is enqueued(GCD創(chuàng)建新線程處理新的任務(wù))
Each thread gets less time to actually decode images(每個(gè)線程獲得很少的時(shí)間解碼圖像)

從這個(gè)案例我們學(xué)習(xí)到如何避免圖像解碼的線程爆炸卡乾,但還能擴(kuò)散思維:

我們分析蘋果工程師的邏輯:
原因(解碼任務(wù)過多)==> 過程(GCD開啟更多線程) ==> 結(jié)果( 每個(gè)線程獲得更少的時(shí)間)
延伸出來的問題有:
GCD是如何處理并發(fā)隊(duì)列翼悴?為何會(huì)啟動(dòng)多個(gè)線程處理?
多少的線程數(shù)量是合適的说订?線程的cpu時(shí)間分配和切換代價(jià)如何抄瓦?
...
舉一反三,類似的問題太多陶冷。但是這樣的思考稍顯混亂钙姊,仍有優(yōu)化的空間。

把腦海關(guān)于GCD的認(rèn)知提煉出來:
1埂伦、GCD是用來處理一系列任務(wù)的同步和異步執(zhí)行煞额,隊(duì)列有串行和并發(fā)兩種,與線程的關(guān)系只有主線程和非主線程的區(qū)別;
2膊毁、串行隊(duì)列是執(zhí)行完當(dāng)前的任務(wù)胀莹,才會(huì)執(zhí)行下一個(gè)block任務(wù);并行隊(duì)列是多個(gè)block任務(wù)并行執(zhí)行婚温,GCD會(huì)根據(jù)任務(wù)的執(zhí)行情況分配線程描焰,原則是盡快完成所有任務(wù);

接下來的表現(xiàn)是操作系統(tǒng)相關(guān)的知識(shí):
1栅螟、iOS系統(tǒng)中進(jìn)程和線程的關(guān)聯(lián)荆秦,每個(gè)啟動(dòng)的APP都是一個(gè)進(jìn)程,其中有多個(gè)線程力图;
2步绸、cpu的時(shí)間是分為多個(gè)時(shí)間片,每個(gè)線程輪詢執(zhí)行吃媒;
3瓤介、線程切換執(zhí)行有代價(jià),但比進(jìn)程切換小得多赘那;
4刑桑、每個(gè)cpu核心在同一時(shí)刻只能執(zhí)行一個(gè)線程;

至此我們可以結(jié)合操作系統(tǒng)和GCD的知識(shí)漓概,猜測(cè)底層GCD的實(shí)現(xiàn)思路和線程爆炸情況下的表現(xiàn):
主線程把多個(gè)任務(wù)block放到并發(fā)隊(duì)列漾月,GCD先啟動(dòng)一個(gè)線程處理解碼任務(wù)病梢,線程執(zhí)行過程中遇到耗時(shí)操作時(shí)(IO等待胃珍、大量CPU計(jì)算),短時(shí)間內(nèi)無法完成蜓陌,為了不阻塞后續(xù)任務(wù)的執(zhí)行觅彰,GCD啟動(dòng)新的線程處理新的任務(wù)。

集合此案例钮热,我們能回答相關(guān)問題:
1填抬、現(xiàn)在有一個(gè)很復(fù)雜的計(jì)算任務(wù),例如是統(tǒng)計(jì)一個(gè)5000x5000圖片中像素點(diǎn)的RGB顏色通道隧期,如果用分為25個(gè)任務(wù)放到GCD并發(fā)隊(duì)列飒责,把大圖切分成25個(gè)1000x1000小圖分別統(tǒng)計(jì),是否會(huì)速度的提升仆潮?
2宏蛉、GCD的串行隊(duì)列和并發(fā)隊(duì)列的應(yīng)用場(chǎng)景有何不同?

  1. 如果GPU的刷新率超過了iOS屏幕60Hz刷新率是什么現(xiàn)象性置,怎么解決?
    畫面撕裂
    降低GPU刷新頻率為60Hz
    GPU每秒鐘刷新屏幕60次拾并,這每刷新一次就是一幀frame,每一幀大概在1/60 = 16.67ms畫面最佳.

性能優(yōu)化

  1. 如何做啟動(dòng)優(yōu)化,如何監(jiān)控
  2. 如何做卡頓優(yōu)化嗅义,如何監(jiān)控
  3. 如何做耗電優(yōu)化屏歹,如何監(jiān)控
  4. 如何做網(wǎng)絡(luò)優(yōu)化,如何監(jiān)控

開發(fā)證書

  1. 蘋果使用證書的目的是什么
  2. AppStore安裝app時(shí)的認(rèn)證流程
  3. 開發(fā)者怎么在debug模式下把a(bǔ)pp安裝到設(shè)備呢

架構(gòu)設(shè)計(jì)

典型源碼的學(xué)習(xí)

  1. AFNetWorking
  2. SDWebImage
  3. JSPatch之碗、Aspects(雖然一個(gè)不可用蝙眶、另一個(gè)不維護(hù),但是這兩個(gè)庫都很精煉巧妙褪那,很適合學(xué)習(xí))
  4. Weex/RN, 筆者認(rèn)為這種前端和客戶端緊密聯(lián)系的庫是必須要知道其原理的
  5. CTMediator械馆、其他router庫,這些都是常見的路由庫武通,開發(fā)中基本上都會(huì)用到

架構(gòu)設(shè)計(jì)

  1. 手動(dòng)埋點(diǎn)霹崎、自動(dòng)化埋點(diǎn)、可視化埋點(diǎn)

  2. MVC冶忱、MVP尾菇、MVVM設(shè)計(jì)模式

  3. 常見的設(shè)計(jì)模式
    單利 工廠 門面 觀察者 通知

  4. 單例的弊端

  5. 常見的路由方案,以及優(yōu)缺點(diǎn)對(duì)比

  6. 如果保證項(xiàng)目的穩(wěn)定性

  7. 設(shè)計(jì)一個(gè)圖片緩存框架(LRU)

  8. 如何設(shè)計(jì)一個(gè)git diff

  9. 設(shè)計(jì)一個(gè)線程池囚枪?畫出你的架構(gòu)圖

  10. 你的app架構(gòu)是什么派诬,有什么優(yōu)缺點(diǎn)、為什么這么做链沼、怎么改進(jìn)

其他問題

  1. PerformSelector & NSInvocation優(yōu)劣對(duì)比
  2. oc怎么實(shí)現(xiàn)多繼承默赂?怎么面向切面(可以參考Aspects深度解析-iOS面向切面編程
  3. 哪些bug會(huì)導(dǎo)致崩潰,如何防護(hù)崩潰
  4. 怎么監(jiān)控崩潰
  5. app的啟動(dòng)過程(考察LLVM編譯過程括勺、靜態(tài)鏈接缆八、動(dòng)態(tài)鏈接、runtime初始化)
  6. 沙盒目錄的每個(gè)文件夾劃分的作用
  7. 簡述下match-o文件結(jié)構(gòu)

系統(tǒng)基礎(chǔ)知識(shí)

  1. 進(jìn)程和線程的區(qū)別?
    進(jìn)程是app 啟動(dòng)市創(chuàng)建的,一個(gè)app 只能有一個(gè)進(jìn)程.
    線程是在進(jìn)程中的, 一個(gè)進(jìn)程可以有多個(gè)線程.

  2. HTTPS的握手過程?
    client發(fā)送url 請(qǐng)求.
    server端收到請(qǐng)求,并且配置好數(shù)字證書(公鑰),并返回給client.
    client驗(yàn)證數(shù)字證書的有效性,并生成隨機(jī)數(shù).
    client 打包數(shù)字證書 + 隨機(jī)數(shù)發(fā)送給server.
    server端解析.
    server 返回信息給client.
    client解鎖返回信息.
    ps: server發(fā)送的簽名證書屬于非對(duì)稱加密, client 與server 通訊使用的是對(duì)稱加密.

  3. 什么是中間人攻擊疾捍?怎么預(yù)防
    就是第三方偽裝成client盜走了數(shù)字簽名證書.

  4. TCP的握手過程奈辰?為什么進(jìn)行三次握手,四次揮手?

  5. 堆和棧區(qū)的區(qū)別乱豆?誰的占用內(nèi)存空間大?

  6. 加密算法:對(duì)稱加密算法和非對(duì)稱加密算法區(qū)別?

  7. 常見的對(duì)稱加密和非對(duì)稱加密算法有哪些?
    非對(duì)稱加密算法:RSA奖恰,DSA/DSS
    對(duì)稱加密算法:DES,RC4宛裕,3DES

  8. MD5瑟啃、Sha1、Sha256區(qū)別?

  9. charles抓包過程揩尸?不使用charles蛹屿,4G網(wǎng)絡(luò)如何抓包?

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

LeetCode

  1. 八大排序算法
  2. 棧&隊(duì)列
  3. 字符串處理
  4. 鏈表
  5. 二叉樹相關(guān)操作
  6. 深搜廣搜
  7. 基本的動(dòng)態(tài)規(guī)劃題、貪心算法疲酌、二分查找
    [原文章] (http://www.reibang.com/p/e87e0be2281f)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蜡峰,一起剝皮案震驚了整個(gè)濱河市了袁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌湿颅,老刑警劉巖载绿,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異油航,居然都是意外死亡沈自,警方通過查閱死者的電腦和手機(jī)徐矩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人脆诉,你說我怎么就攤上這事绽昼∧盍矗” “怎么了蒂培?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長奠伪。 經(jīng)常有香客問我跌帐,道長,這世上最難降的妖魔是什么绊率? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任谨敛,我火速辦了婚禮,結(jié)果婚禮上滤否,老公的妹妹穿的比我還像新娘脸狸。我一直安慰自己,他們只是感情好藐俺,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布炊甲。 她就那樣靜靜地躺著,像睡著了一般紊搪。 火紅的嫁衣襯著肌膚如雪蜜葱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天耀石,我揣著相機(jī)與錄音,去河邊找鬼爸黄。 笑死滞伟,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的炕贵。 我是一名探鬼主播梆奈,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼称开!你這毒婦竟也來了亩钟?” 一聲冷哼從身側(cè)響起乓梨,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎清酥,沒想到半個(gè)月后扶镀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡焰轻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年臭觉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辱志。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蝠筑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出揩懒,到底是詐尸還是另有隱情什乙,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布已球,位于F島的核電站稳强,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏和悦。R本人自食惡果不足惜退疫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鸽素。 院中可真熱鬧褒繁,春花似錦、人聲如沸馍忽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽遭笋。三九已至坝冕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瓦呼,已是汗流浹背喂窟。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留央串,地道東北人磨澡。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像质和,于是被迫代替她去往敵國和親稳摄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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