阿里、字節:一套高效的iOS面試題及答案、iOS知識點回顧、盲點總結

疫情期間比較火的《阿里、字節:一套高效的iOS面試題
》,以下是自己在2020年3月份做了一遍的結果,和盲點總結,歡迎大家交流.
以下題目有些比較簡單的就沒有作答.部分題目需要篇幅較長的,在《另外一篇博客》中有純手寫作答.

結構模型

介紹下runtime的內存模型(isa、對象、類、metaclass、結構體的存儲信息等)

1,isa爲Object中的唯一成員變量,指向對象所屬的類
2,isa的類型爲isa_t 是後來的runtime出現的,
metaclass是元類,可以理解爲根類(object類)的類
3,class中存儲的主要內容包含:
isa_t superclass
cache_t cache
class_rw_t *data()

爲什麼要設計metaclass

因爲類也是對象,動態創建的時候object類(根類)的類就是meatclass

class_copyIvarList & class_copyPropertyList區別

class_copyIvarList 就是class_ro_t中的ivars, 成員列表,在編譯期確定,不能動態添加,

class_copyPropertyList 是屬性列表,對應 class_rw_tproperty_array_t,它的內容是property_t結構體,內容爲nameattribute,對應屬性,是成員變量的聲明,在編譯期前已經存在的屬性會在編譯期生成以_爲開頭的同名成員變量,attribute中是對成員變量的一些訪問權限控制描述

class_rw_t 和 class_ro_t 的區別

class_rw_t 是runtime 時動態添加方法的操作對象,其中沒有ivar相關的內容,在運行時,objc_class中是持有的class_rw_t,class_rw_t又引用的了class_ro_t,其中的方法列表,協議列表,屬性列表等都是複製class_ro_t的內容,並且在運行時可以更改,它也是oc作爲動態語言的一大特點

class_ro_t 是編譯期,編譯器對類文件編譯產生的,其中包含了類的方法,屬性,協議等信息,它是不能被改變的,因爲已經確定了大小

category如何被加載的,兩個category的load方法的加載順序,兩個category的同名方法的加載順序

  • 0 在runtime加載過程中,加載完所有的類到hash表中後,會去遍歷所有的類及其對應的類別列表,一次把它合併到類的class_rw_t中的信息中,如方法合併到method_list中等,這也是我們一直說類別方法是不是覆蓋類中重名方法,而是在方法調用的時候會在method_list的中從後往前找,自然是category的先被找到,
  • 1,+load方法的優先級: 父類> 子類> 分類
  • 2,dyld加載類別的順序有關,在xcode中的targets-build phases-compile sources中,由上至下依次加載
  • 3,兩個category的同名方法誰後被加載誰先被調用
  • 普通方法的優先級: 分類> 子類 > 父類, 優先級高的同名方法覆蓋優先級低的
  • +load方法不會被覆蓋
  • 同一主類的不同分類中的普通同名方法調用, 取決於編譯的順序, 後編譯的文件中的同名方法會覆蓋前面所有的,包括主類. +load方法的順序也取決於編譯順序, 但是不會覆蓋
  • 分類中的方法名和主類方法名一樣會報警告, 不會報錯
  • 聲明和實現可以寫在不同的分類中, 依然能找到實現
  • 當第一次用到類的時候, 如果重寫了+ initialize方法,會去調用
  • 當調用子類的+ initialize方法時候, 先調用父類的,如果父類有分類, 那麼分類的+ initialize會覆蓋掉父類的, 和普通方法差不多
  • 父類的+ initialize不一定會調用, 因爲有可能父類的分類重寫了它

category & extension區別,能給NSObject添加Extension嗎,結果如何

category:分類

給類添加新的方法
不能給類添加成員變量
通過@property定義的變量,只能生成對應的getter和setter的方法聲明,但是不能實現getter和setter方法,同時也不能生成帶下劃線的成員屬性
是運行期決定的
注意:爲什麼不能添加屬性,原因就是category是運行期決定的,在運行期類的內存佈局已經確定,如果添加實例變量會破壞類的內存佈局,會產生意想不到的錯誤。

extension:擴展

可以給類添加成員變量,但是是私有的
可以給類添加方法,但是是私有的
添加的屬性和方法是類的一部分,在編譯期就決定的。在編譯器和頭文件的@interface和實現文件裏的@implement一起形成了一個完整的類。
伴隨着類的產生而產生,也隨着類的消失而消失
必須有類的源碼纔可以給類添加extension,所以對於系統一些類,如nsstring,就無法添加類擴展
不能給NSObject添加Extension,因爲在extension中添加的方法或屬性必須在源類的文件的.m文件中實現纔可以,即:你必須有一個類的源碼才能添加一個類的extension。

消息轉發機制,消息轉發機制和其他語言的消息機制優劣對比

在方法調用的時候,方法查詢-> 動態解析-> 消息轉發 之前做了什麼

消息調用流程

  • 1,消息發送,轉化成objc_sendMsg(receiver,SEL)
  • 2,類的cache中查找,類的methodList中查找,父類cache中查找,父類的methodList中查找到object還找不到,進入消息轉發階段
  • 3,resolveClassMethod resolveInstanceMethod中判斷當前接受者是否可以動態添加SEL,如果不能
  • 4,forwrdingTargetForSelector 交給其他接受者處理
  • 5,methodSignatureForSelector 返回方法簽名 再到forwardInvocation處理異常

load、initialize方法的區別什麼?在繼承關係中他們有什麼區別

  • load:

  • 當類被裝載的時候被調用,只調用一次
    調用方式並不是採用runtime的objc_msgSend方式調用的,而是直接採用函數的內存地址直接調用的

  • 多個類的load調用順序,是依賴於compile sources中的文件順序決定的,根據文件從上到下的順序調用

  • 子類和父類同時實現load的方法時,父類的方法先被調用

  • 本類與category的調用順序是,優先調用本類的(注意:category是在最後被裝載的)

  • 多個category,每個load都會被調用(這也是load的調用方式不是採用objc_msgSend的方式調用的),同樣按照compile sources中的順序調用的

  • load是被動調用的,在類裝載時調用的,不需要手動觸發調用

  • initialize:

  • 當類或子類第一次收到消息時被調用(即:靜態方法或實例方法第一次被調用,也就是這個類第一次被用到的時候),只調用一次

  • 調用方式是通過runtime的objc_msgSend的方式調用的,此時所有的類都已經裝載完畢

  • 子類和父類同時實現initialize,父類的先被調用,然後調用子類的

  • 本類與category同時實現initialize,category會覆蓋本類的方法,只調用category的initialize一次(這也說明initialize的調用方式採用objc_msgSend的方式調用的)

  • initialize是主動調用的,只有當類第一次被用到的時候纔會觸發

說說消息轉發機制的優劣

優勢: 讓語言具有動態性,更加靈活,具有更多讓程序員在運行時做動作的可能性.
劣勢: 高度的動態話導致安全性降低,多層機制勢必降低語言執行效率

內存管理

@property定義的變量,默認的修飾符是什麼?

關於ARC下,不顯示指定屬性關鍵字時,默認關鍵字:
1.基本數據類型:atomic readwrite assign
2.普通OC對象: atomic readwrite strong

weak的實現原理?SideTable的結構是什麼樣的

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fFxRpwzu-1593760800175)(https://github.com/imwangxuesen/Blog/blob/master/Private/temp/sidetable.png?raw=true)]

SideTables, SideTable, weak_table, weak_entry_t

關聯對象的應用?系統如何實現關聯對象的

全局associatedMap , 每一個對象的disguised_ptr_t結構體爲key,對應着一個ObjectAssociationMap, ObjectAssociationMap中存儲着本對象所有的key及其對應的關聯對象(ObjectAssociation)

關聯對象實現原理

關聯對象的如何進行內存管理的?關聯對象如何實現weak屬性

1, 關聯對象的ObjectAssociation中有兩個屬性(uintptr_t _policy, id value),
_policy 包含 retain, assgin copy, 會對應的對對象進行和普通對象一樣的內存管理操作.

2 ,實現weak,用__weak修飾對象,並將其用block包裹,關聯時,關聯block對象

-(void)setWeakvalue:(NSObject *)weakvalue {
    __weak typeof(weakvalue) weakObj = weakvalue;
    typeof(weakvalue) (^block)() = ^(){
        return weakObj;
    };
    objc_setAssociatedObject(self, weakValueKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSObject *)weakvalue {
    id (^block)() = objc_getAssociatedObject(self, weakValueKey);
    return block();
}

Autoreleasepool的原理?所使用的的數據結構是什麼

雙向鏈表,編譯後,autoreleasepool是一個全局變量,每一個線程,在runtime啓動時都會準備一個autorelasepool,
主要兩個方法, push, pop
push就是在page中插入一個哨兵對象,代表這些屬於要一起release的對象,
如果page滿了,則創建新的page,併合老的page關聯起來,對象指針壓棧

pop就是從傳入哨兵對象往後,所有對象,依次執行release

Objective-C Autorelease Pool 的實現原理
自動釋放池的前世今生

ARC的實現原理?ARC下對retain & release做了哪些優化

理解ARC實現原理

ARC下哪些情況會造成內存泄漏

block中的循環引用
NSTimer的循環引用
addObserver的循環引用
delegate的強引用
大次數循環內存爆漲
非OC對象的內存處理(需手動釋放)

其他

Method Swizzle注意事項

1,只交換一次
2,獲取方法(class_getInstanceMethod/class_getClassMethod)會沿着繼承者鏈向上尋找,所以防止交換時,交換的方法實現時本類的,絕對不能是父類的

Method swizzling的正確姿勢

屬性修飾符atomic的內部實現是怎麼樣的?能保證線程安全嗎

不能,它只管setter和getter方法的原子性,如果兩個線程循環做屬性的累加操作,依舊不行

iOS 中內省的幾個方法有哪些?內部實現原理是什麼

實現內省的方法包括:

isKindOfClass:Class
isMemberOfClass:Class
respondToSelector:selector
conformsToProtocol:protocol
實現原理:以上方法的實現原理都是運用runtime的相關函數實現的。

class、objc_getClass、object_getclass 方法有什麼區別?

  • class: [類 class] 返回本身, [實例 class] = object_getClass(實例)返回isa,其實是返回所屬的類
  • Class objc_getClass(const char *aClassName) 參數是類名的字符串,返回的就是這個類的類對象
  • Class object_getclass(id obj) 返回該對象的isa指針,即obj的所屬類

NSNotification相關

NSNotification原理解析

1,實現原理(結構設計、通知如何存儲的、name&observer&SEL之間的關係等)

Observation {
block_t block;
thread_t thread;
id observer;
SEL selector;
}

NotificationCenter中有三個存儲屬性,
1.1,wildcard:鏈表,存儲沒有name沒有observer的Observation存儲在這裏

1.2,named:map 有名字,有observer的存儲在這裏
{key(name):value(Map{key(observer):value(Observation對象)})}

1.3,nameless: map 沒有名字,但是有observer的存儲在這裏
{key(observer):value(Observation(鏈表))}

2,通知的發送時同步的,還是異步的

同步

3,NSNotificationCenter接受消息和發送消息是在一個線程裏嗎?如何異步發送消息

3.1是在一個線程
3.2
讓通知的執行方法異步執行即可
通過NSNotificationQueue,將通知添加到隊列當中,立即將控制權返回給調用者,在合適的實際發送通知,從而不會阻塞當前的調用

4,NSNotificationQueue是異步還是同步發送?在哪個線程響應

NSPostingStyle爲NSPostNow是同步,
NSPostWhenIdle和NSPostASAP:異步發送

5,NSNotificationQueue和runloop的關係

NSNotificationQueue將通知添加到隊列中時,其中postringStyle參數就是定義通知調用和runloop狀態之間關係。

該參數的三個可選參數:

NSPostWhenIdle:runloop空閒的時候回調通知方法
NSPostASAP:runloop在執行timer事件或sources事件的時候回調通知方法
NSPostNow:runloop立即回調通知方法

6,如何保證通知接收的線程在主線程

6.1 發送時使用傳入主線程參數
6.2 使用machport在主線程runloop註冊,在notification回調中使用machport進行線程間通信,給主線程發送消息(注意在自線程中暫存通知消息,用machport回到主線程後再執行)

7,頁面銷燬時不移除通知會崩潰嗎

iOS9之前不行,因爲notificationcenter對觀察者的引用是unsafe_unretained,當觀察者釋放的時候,觀察者的指針值並不爲nil,出現也指針
iOS9之後可以,因爲notificationcenter對觀察者的引用是weak

8,多次添加同一個通知會是什麼結果?多次移除通知呢

會調用多次observer的action
多次移除沒有任何影響

9,下面的方式能接收到通知嗎?爲什麼

// 發送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@“TestNotification” object:@1];
// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@“TestNotification” object:nil];
答案解析
收不到,因爲有名字,有observer,在查找對應的notification的時候是在named表中找,找到的不是同一個.

Runloop & KVO
runloop
runloop對於一個標準的iOS開發來說都不陌生,應該說熟悉runloop是標配,下面就隨便列幾個典型問題吧

app如何接收到觸摸事件的

1, 硬件處理相應,封裝UIEvent發送到Application(通過註冊的machport)
2, 尋找響應者,先發給手勢集合,然後仔是從離屏幕最近的開始到離屏幕最遠的,從父視圖到子視圖,通過 hitTest:withEvent

爲什麼只有主線程的runloop是開啓的

main函數中調用UIApplicationMain,處理UI,爲了讓程序可以一直運行,所以開啓一個常駐線程

爲什麼只在主線程刷新UI

1,UIKit不是線程安全的,如果多線程操作UI會產生很多渲染順序,資源浪費等不可控情況,甚至會引起頁面渲染錯誤導致的用戶流失問題.
2,如果把UIKit設計成安全的,也並不是好的,因爲UIkit設計成線程安全的,並不能提高渲染的效率,首先,渲染的過程是

  • core animation 先構建視圖佈局(layout),重載drawrect進行實時繪製(disply),圖像解碼和格式轉換(prepare), 將layer遞歸打包發送到render server (commit)

  • render server收到後,將信息反序列化成渲染樹,根據layer屬性生成繪製指令,並在下一次VSync信號來到時調用OpenGL進行渲染.

  • GPU 會等待顯示器的VSync信號發出後才進行OpenGL渲染管線,將3D幾何數據轉換成2D的像素圖像和光柵處理,隨後進行新的一幀渲染,並將其輸出到緩衝區

  • Display, 從緩衝區中取出畫面,並輸出到屏幕

這裏會出現渲染卡幀的問題,原因是因爲在一個VSync週期內,GPU沒有完成這一幀的渲染任務,導致display過程在緩衝區中拿不到畫面,顯示器丟幀

如果線程安全的UIKit在子線程中併發大量的提交到core animation處理頁面數據,本質上,知識提升了第一步的速度和效率,但是核心的瓶頸在與GPU的處理速度和VSync的同步問題,所以,這樣不但沒有提升效率和體驗,反而提高了風險和耗損,降低了用戶體驗

PerformSelector和runloop的關係

PerformSelector會在線程的runloop中添加一個Timer,到時去發送指定的消息,
如果某一個線程的runloop沒有開啓,那麼performSelector將不會生效,因爲Timer依賴runloop,如果runloop沒有啓動,那麼timer並不能執行,進而方法無法調用

如何使線程保活

1,在線程中加入MachPort
2,在線程中開啓where(YES)的循環,優化一點就是加入信號量,模擬runloop eg:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0)

線程內 {
while(YES) {
dispatch_semaphore_wait(semaphore);

	if (退出條件滿足) {
		break;
	}
}

}
dispatch_semaphore_signal(semaphore);

KVO

同runloop一樣,這也是標配的知識點了,同樣列出幾個典型問題

實現原理

  • 繼承原來的類,實現一個單獨的子類
  • 重寫被檢測屬性的Set方法,在方法中插入兩個方法,開頭調用willchange,最後調用didchange,
  • isa swizzing, 將原來類的isa指向子類,所以,在調用被檢測者的Set方法時,runtime會沿着isa的指向找到對應的類中找方法,順其自然就找到了新創建的子類中,子類也有重寫的set方法,也就順其自然的執行了,KVO的過程也就實現了,
  • 注意,子類的class也被重寫了,返回的事父類的名字,這裏假裝是父類

KVO屬性依賴

比如有一個教 fullName 的屬性,依賴於 firstName 和 lastName,當 firstName 或者 lastName 改變時,這個 fullName 屬性需要被通知到。

  • (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
    }
    你可以重寫 keyPathsForValuesAffectingValueForKey: 方法。其中要先調父類的這個方法拿到一個set,再做接下來的操作。
  • (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {

    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];

    if ([key isEqualToString:@“fullName”]) {
    NSArray *affectingKeys = @[@“lastName”, @“firstName”];
    keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
    }
    你也可以通過實現 keyPathsForValuesAffecting 方法來達到前面同樣的效果,這裏的就是屬性名,不過第一個字母要大寫,用前面的例子來說就是這樣:

  • (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@“lastName”, @“firstName”, nil];
    }

pthread_t NSTread 的關係

如何手動關閉kvo

重寫 automaticallyNotifiesObserversForKey 方法

通過KVC修改屬性會觸發KVO麼

會,
kvc的原理就是先去調用setKey方法、_setKey 方法
找不到set方法直接設置屬性 _key key isKey內部會間聽到值的改變

哪些情況下使用kvo會崩潰,怎麼防護崩潰

  • 移除一個未註冊的keyPath
  • 觀察者已經銷燬,但是沒有被移除,當觀察者的對象發生變化的時候,kvo中的觀察者變成了也指針,導致crash

kvo的優缺點

  • 不能用block的形式獲取通知
  • 不能指定自己的selector獲取通知

Block

block的內部實現,結構體是什麼樣的

block是類嗎,有哪些類型
一個int變量被 __block 修飾與否的區別?block的變量截獲
block在修改NSMutableArray,需不需要添加__block
怎麼進行內存管理的
block可以用strong修飾嗎
解決循環引用時爲什麼要用__strong、__weak修飾
block發生copy時機
Block訪問對象類型的auto變量時,在ARC和MRC下有什麼區別
多線程
主要以GCD爲主

iOS開發中有多少類型的線程?分別對比

線程框架 描述 語言 生命週期管理
pthread 跨平臺、Unix/Linux/Windows、使用難度大 C 程序員管理
NSThread 面向對象,可操作線程對象,簡單易用 OC 程序員管理
GCD 簡單易用,API使用block,代碼集中 C 自動管理
NSOperation 基於GCD,多了一些簡單實用的功能,比如最大併發數,使其更加面向對象 OC 自動管理

GCD有哪些隊列,默認提供哪些隊列

兩大類
serial dispatch queue (串行隊列) main dispatch queue
concurrent dispatch queue (並行隊列) dispatch_global_queue

GCD有哪些方法api
dispatch_sync
dispatch_async
dispatch_once
dispatch_queue_create
dispatch_semaphore_create
dispatch_semaphore_wait
dispatch_semaphore_signal
dispatch_group_create
dispatch_group_enter
dispatch_group_leave
dispatch_set_target_queue
dispatch_barrier_sync
dispatch_barrier_async
dispatch_after
dispatch_apply
dispatch_source_t

GCD主線程 & 主隊列的關係(未掌握)

如何實現同步,有多少方式就說多少

1,穿行隊列,併發爲1
2,dispatch_group_enter dispatch_group_leave
3,dispatch_semaphore_wait dispatch_semaphore_signal
4,dispatch_barrier
5,dispatch_group_notify
6,dispatch_block_wait
7,dispatch_block_notify

dispatch_once實現原理

void dispatch_once_f(dispatch_once_t *val, void *ctxt, void (*func)(void *)){
    // val == 0
    volatile long *vval = val;
    // 第一個線程進來
    // dispatch_atomic_cmpxchg(p,l,n) 方法的功能是原子操作
    // 方法含義爲如果p==l 則 將n賦值給p, 並且返回true
    // 如果p != l 則返回 false
    // 所以第一次進來 val == 0 , val被賦值爲1 , 並且返回ture
    if (dispatch_atomic_cmpxchg(val, 0l, 1l)) {
        func(ctxt); // block真正執行
        // dispatch_atomic_barrier 是一個編譯器操作,意思爲前後指令的順序不能顛倒.這裏是防止編譯器優化將原本的指令語義破壞
        dispatch_atomic_barrier();
        // 將vval賦值爲非零
        *val = ~0l;
    } 
    else 
    {
    	// 如果在第一個線程進來後執行上邊代碼塊的同時,有其他的線程進來執行
    	// 則進入空循環,等待vval被賦值爲非零.
        do
        {
        	 // 這有助於提高性能和節省CPU耗電,延遲空等
            _dispatch_hardware_pause();
        } while (*vval != ~0l);
        dispatch_atomic_barrier();
    }
}

什麼情況下會死鎖

死鎖發生的條件

  • 互斥條件: 某一個資源被佔用後,不允許其他線程進行訪問,其他線程請求時只能等待被釋放後才能使用
  • 請求和保持條件: 線程獲得資源後,又對其他資源發出請求,但是該資源可能被其他線程佔有,此時請求阻塞,但是又對自己獲得的資源保持不放
  • 不可剝奪條件: 線程已經獲得的資源,在未完成使用之前,不可被剝奪,只能使用完成後自己釋放
  • 環路等待條件: 線程發生死鎖後,必然存在一個線程-資源之間的環形鏈.

比如:
eg1:主線程調用主線程同步任務

dispatch_sync(dispatch_get_main_queue(), ^{
   NSLog(@"發生死鎖");
});
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"hhaah");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"dkjfks");
    });
});

有哪些類型的線程鎖,分別介紹下作用和使用場景

https://blog.csdn.net/deft_mkjing/article/details/79513500

自旋鎖: 線程反覆檢查所變量是否可用,線程這一過程中保持執行,因此是一種忙等狀態,一旦獲取了,線程一直保持,直到顯示的釋放,自旋鎖避免了進程上下文切換的開銷,因此,對於線程只阻塞很短的場合是有效的.

  • OSSpinLock

互斥鎖: 避免兩個線程同時對某一變量進行讀寫,通過將代碼切片成一個個臨界區而達成

  • NSLock
  • pthread_mutex
  • @sysnchronized
  • os_unfair_lock

信號量:

  • dispatch_semaphore

條件鎖:

  • NSCondition

讀寫鎖

  • pthread_rwlock
  • pthread_mutex(recursive)
  • NSRecursiveLock
  • NSConditionLock
  • os_unfair_lock(ios 10)之後

OSSpinLock已經被棄用,因爲會出現低優先級線程持有鎖,然後高優先級去獲取鎖的時候,spin lock會進入忙等狀態,佔用大量cpu時間,這時低優先級隊列無法爭奪cpu時間片,從而導致任務無法完成,無法釋放lock

NSOperationQueue中的maxConcurrentOperationCount默認值

這個志向了一個靜態變量,值爲-1,是根據系統情況去分配他的數值.

NSTimer、CADisplayLink、dispatch_source_t 的優劣

視圖&圖像相關

定時器都是依賴runloop運行的,
NSTimer是最見的定時器,注意在使用的時候在滑動的時候不會被觸發,需要加入到runloop commonmode 中,在沒有runloop運行的子線程,也不會執行
有內存泄漏問題,注意invalied.提供了比較多的初始化方法.比較方便易用

CADisplaylink 是根據屏幕刷新頻率處罰的定時器,蘋果目前的屏幕刷新頻率爲60HZ,華爲剛出了一個90HZ的以後可能這需要注意下改動,在不同的手機上是不同的頻率,他比較準確,在沒有什麼耗時任務卡住runloop的情況下,很穩定,但是不靈活,適合做UI刷新,添加進runloop即刻啓動

dispatch_source_t 是基於gcd 給runloop添加source而實現的定時任務.更加精確,可以設置冗餘時間,可以放到子線程

AutoLayout的原理,性能如何

自動佈局 Auto Layout (原理篇)

UIView & CALayer的區別

1,UIView是CAlayer的外殼,用來處理響應,包裝Layer,繼承自UIResponder
2,Layer是實際渲染層,用來處理視圖屬性,他的代理是UIView,view的frame、bounds等屬性實際都是來自layer
3,Layer維護着邏輯樹(這裏是代碼可以操作的),動畫樹(中間層,系統在此層修改屬性,進行渲染相關的操作),顯示樹(其內容就是現在正被顯示在屏幕上的內容)
4,座標系統:layer比view多了anchorPoint,是各種圖形變換的座標原點,同時會更改layer的position的位置,缺省值爲「0.5,0.5」,即layer中央
UIView & CALayer的區別

事件響應鏈

1,手指點擊屏幕,硬件將響應消息轉化爲UIEvent傳入正在活躍的應用時間隊列
2,發給手勢系統,看看是否可以響應,如果不能,則進入應用
2,第一層是UIWindow響應,調用pointInside:withEvent 檢查點是否在該視圖內.如果是,通過hitTest:withEvent 調用所有子視圖的pointInside:withEvent,和hitTest:withEvent
規則
1,pointInside:withEvent 返回NO,什麼都不做
2,pointInside:withEvent 返回YES, 調用對應的hitTest:withEvent,遞歸檢查子視圖是否是可以響應事件.
3,pointInside:withEvent 返回YES,並且沒有子視圖,hitTest:withEvent返回自己,並逐層翻上去.
至此便找到了響應者.

這時響應者鏈,事件傳遞鏈就是一層層翻上去,看看誰可以響應事件.

drawrect & layoutsubviews調用時機

1,drawrect用來拿到上下文繪製視圖內容,在UIView的子類中
2,drawrect調用時機:
2.1:loadview之後
2.2:待用setNeedsDisplay或者setNeedsDisplayInRect:出發drawrect,前提條件爲view必須有rect
2.3 該方法在調用sizeThatFits後被調用,所以可以先調用sizeToFit計算出size。然後系統自動調用drawRect:方法。
這裏簡單說一下sizeToFit和sizeThatFit:
sizeToFit:會計算出最優的 size 而且會改變自己的size
sizeThatFits:會計算出最優的 size 但是不會改變 自己的 size

3, layoutsubviews是用來重新佈局,(比如改變子視圖位置)默認方法中什麼都不幹,開發者可以自己重寫
4, 調用時機:
4.1, setNeedLayout標記,在下一輪runloop時調用layoutsubviews
4.2, 如果在setNeedLayout後立即調用layoutIfNeed會立即出發layoutsubviews

系統調用時機:
1, init不會觸發
2, addsubviews會觸發layoutSubviews (前提爲frame有值)
3, 改變view的frame會觸發
4, 滾動srollview
5, 旋轉會觸發父視圖的layoutsubviews
6, 直接調用layoutsubviews

layoutsubviews早於drawrect調用. 前者佈局相關,後者繪製相關

UI的刷新原理


一 圖形渲染過程

視圖渲染

1,我們創建的View,設置視圖相關的屬性,比如frame,backgroundcolor,textcolor等,背後實際體現在CALayer上

2,Core Animation將layer解析成圖層樹,每個節點存儲着layer相關的frame,bounds,transform等屬性,圖層數還能表達各個圖層之間的關係.

3,CoreAnimation 依賴於OpenGL ES或者Metal做GPU渲染,Core Graphics做CPU渲染.如上圖所示,在屏幕上顯示視圖,CPU和GPU要寫作,一部分數據通過CoreGraphics、CoreImage由CPU預處理,最終由OpenGL ES或者metal將數據傳送到GPU,最終顯示到屏幕

3.1,CoreGraphic基於Quartz高級繪畫引擎,主要用於運行時圖像繪製,開發者可以用來處理基於路徑的繪圖,轉換,顏色管理,離屏渲染,圖案,漸變和陰影,圖像數據管理,圖像創建和圖像遮照以及PDF文檔創建、顯示和分析

3.2,CoreImage用於運行前創建的圖像,大部分情況下CoreImage在GPU中完成工作,如果GPU忙,會使用CPU進行處理.支持兩種處理模式

顯示邏輯

1,CoreAnimation提交會話(包括自己和子樹的layout狀態等)
2,RenderServer解析提交的子樹狀態,生成繪製指令
3,CPU執行繪製指令
4,顯示渲染後的數據

Core Animation提交流程

在CoreAnimation流水線中,App調用Render Server前的最後一步,Commit Transaciton分爲四個步驟

  • Layout:視圖構建,layoutSubviews方法重載,addSubview方法填充子視圖,主要就是進行layer的創建
  • Display:視圖繪製,設置要成像的圖元數據.重載視圖的drawRect:方法可以自定義UIView的顯示,原理是drawrect方法內部繪製寄宿圖,該過程使用CPU和內存,繪製完成放入layer的contents屬性指向的backing store
  • Prepare: 準備階段,該階段一般處理圖像解碼和轉換工作.
  • Commit: 將圖層進行打包(序列化),併發送至Render Server,該過程遞歸執行,因爲圖層和視圖都是以樹形存在.
    • 當runloop進入休眠(BeforeWaiting)和退出(Exit)會通知Observer調用_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv進行打包,併發送至Render Server
    • 這裏CoreAnimatino會創建一個OpenGL ES紋理,並確保位圖被上傳到對應紋理中.
渲染服務(Render Server)
  • 反序列化CoreAnimation提交的數據.
  • 過濾圖層間遮擋部分
  • 將圖層書轉化爲渲染樹(對應每個圖層的信息,比如頂點座標、顏色等信息的抽離出來,形成樹狀結構)
  • 將渲染樹遞歸提交給OpenGL ES/Metal
  • OpenGL/Metal生成繪製命令,結合固定的渲染管線.提交到命令緩衝區(CommandBuffer)供GPU讀取使用
圖形渲染管線(Graphics Rendering Pipeline)

OpenGL/Metal的作用是在CPU上生成GPU可以理解的一系列指令,並提交給GPU
圖形渲染管線,實際上指的是一堆原始圖形數據經過一個輸送管道,期間經過各種變化處理最終出現在屏幕的過程,通常情況下,渲染管線可以描述成vertices(頂點)到pixels(像素)的過程
如圖:

關於圖中詳細解析,看鏈接內

屏幕顯示

CPU計算顯示內容–>GPU渲染–>渲染結果放到幀緩衝區(iOS是雙緩衝)–>視頻控制器按照VSync信號逐行讀取幀緩衝區數據,傳遞給顯示器顯示

雙緩衝區原理

隱式動畫 & 顯示動畫區別

什麼是離屏渲染

imageName & imageWithContentsOfFile區別,多個相同的圖片,會重複加載嗎?

imageName:
加載後會由系統管理內存,內存佔用比較小,對象釋放,內存不會,多次加載相同圖片不會重複加載.
適合小圖片,頻繁重複加載的場景

imageWithContentsOfFile:
內存佔用小,相同圖片會重複加載,重複佔用內存,對象釋放,對應的內存也會釋放.
適合大圖片,不頻繁重複加載的場景

圖片是什麼時候解碼的,如何優化

解碼:png,jpeg這種都是壓縮格式,解碼就是解壓縮的過程,
圖片解碼需要大量計算,耗時長,iOS創建UIImage 或者 CGImageSource的時候並不會立即解碼,圖片設置到UIImageView或者CALayer.contents中的時候,並且CALayer被提交到GPU前,CGImage中的數據纔會解碼,並且是在主線程.不可避免.

優化方案:
後臺線程把圖片會知道CGBitmapContext中,然後從Bitmap直接創建圖片,

圖片渲染怎麼優化

圖片渲染怎麼優化

1,下載圖片
2,圖片處理(裁剪,邊框等)
3,寫入磁盤
4,從磁盤讀取數據到內核緩衝區
5,從內核緩衝區複製到用戶空間(內存級別拷貝)
6,解壓縮爲位圖(耗cpu較高)
7,如果位圖數據不是字節對齊的,CoreAnimation會copy一份位圖數據並進行字節對齊
8,CoreAnimation渲染解壓縮過的位圖
以上4,5,6,7,8步是在UIImageView的setImage時進行的,所以默認在主線程進行(iOS UI操作必須在主線程執行)。

如果GPU的刷新率超過了iOS屏幕60Hz刷新率是什麼現象,怎麼解決

1,GPU處理完後會放入幀緩衝區,視頻控制器定時讀取緩衝區內容,給屏幕顯示
2,如果緩衝區允許覆蓋,那麼就會產生丟幀,或者和麪斷層.不允許覆蓋,就會丟幀

可以用雙緩衝區,或者加大緩衝區的方案解決

性能優化

App啓動過程
精準計算啓動時間

如何做啓動優化,如何監控

XCode中有debug工具:
對於如何測試啓動時間,Xcode 提供了一個很讚的方法,只需要在 Edit scheme -> Run -> Arguments 中將環境變量 DYLD_PRINT_STATISTICS 設爲 1,就可以看到 main 之前各個階段的時間消耗

冷啓動,熱啓動
慢的原因是因爲主線程的工作量大,比如IO,大量計算

從啓動週期每一步分析,優化
main執行之前
main執行之後
首屏渲染之後

main執行之前

  • 加載可執行文件(App的.o文件合集)
  • 加載動態鏈接庫,進行rebase和bind符號綁定
  • Objc運行時預備處理,類註冊,category註冊,selector唯一性檢查等
  • 初始化,load方法執行,attribute((constructor))修飾的函數調用,創建C++靜態全局變量.

可優化方案:

  • 減少動態庫加載
  • load方法中的內容儘量放到首屏渲染後在執行或換成在initialize中執行
  • 控制C++全局變量的數量
  • 減少不用的類(類別)和方法

main函數執行之後

這個階段一般是從AppDelegate的applicationDidFinishLaunching的方法到首屏渲染,展示歡迎頁這個階段
一般包含的動作有:

  • 首屏初始化所有配置文件的讀寫操作
  • 首頁數據讀取
  • 首屏渲染計算

可優化方案:

  • 只放和首屏渲染相關的業務代碼,其他非首屏業務的初始化監聽註冊,配置文件讀取放到首屏渲染後
  • 使用time profiler觀察方法耗時,優化對應的方法

監控方案:
1,定時任務獲取主隊列的調用棧,0.002獲取一次,記錄每次獲取的方法內容,對應方法時間累加
2,hook objcMsgSend方法 獲取每個方法前後時間,記錄時間,需要彙編,難度大
3,使用XCode工具 Timer Profiler 觀察方法耗時

如何做卡頓優化,如何監控

卡頓原因:
在一個VSync內GPU和CPU的協作,未能將渲染任務完成放入到幀緩衝區,視頻控制器去緩衝區拿數據的時候是空的,所以卡幀

卡頓優化

  • 圖片等大文件IO緩存
  • 耗時操作放入子線程
  • 提高代碼執行效率(JSON to Model的方案,鎖的使用等,減少循環,UI佈局frame子線程預計算)
  • UI減少全局刷新,儘量使用局部刷新

監控卡幀

  • CADisplayLink 監控,結合子線程和信號量,兩次事件觸發時間間隔超過一個VSync的時長,上報調用棧

  • 在runloop中添加監聽,如果kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting中間的耗時超過VSync的時間,那麼就是卡幀了,然後這個時候拿到線程調用棧,看看,那個部分耗時長即可.

如何做耗電優化,如何監控

使用定時器,每隔一段時間獲取一次電量,並上報

+ (float)getBatteryLevel {
    [UIDevice currentDevice].batteryMonitoringEnabled = YES;
    return [UIDevice currentDevice].batteryLevel;
}

電量優化(原則儘量少的減少計算)

  • 降低地理位置的刷新頻次,藍牙和定位按需獲取.不用關掉
  • 整合UI刷新,降低刷新頻次
  • 避免使用透明元素,image和imageview大小設置相同降低計算
  • 使用懶加載,不用的東西不要提前創建
  • 選擇合適的數據結構存儲數據,
    NSArray,使用index來查找很快(插入和刪除很慢)
    字典,使用鍵來查找很快
    NSSets,是無序的,用鍵查找很快,插入/刪除很快
  • timer時間不宜太短,設置合理的leeway
  • 控制線程個數
  • 優化算法,減少不必要的循環

如何做網絡優化,如何監控

網絡優化分爲,提速,節流,安全,選擇合理網絡協議處理針對的業務(比如聊天的,用socket)

提速

  • 增加緩存,比如圖片緩存,H5緩存,列表頁數據放入數據庫緩存
  • 降低請求次數,多個藉口合併,這裏需要服務端配合
  • 壓縮傳輸內容,減少不必要數據傳輸

節流

  • 壓縮思路同上
  • 在用戶角度上,在視頻等大流量場景要判斷是否爲wifi網絡,並提示用戶

安全

  • https
  • 數據加密,防止中間人竊聽
  • 加入簽名,防止中間人篡改
  • 加入https證書校驗,防止抓包

開發證書

蘋果使用證書的目的是什麼

爲了防止開發者的應用隨意安裝在手機上,用證書控制,只有經過蘋果允許的應用纔可以安裝上,防止盜版什麼的
這麼做的目的是爲了整個應用生態的體驗着想,當然也有安全因素在裏邊.

AppStore安裝app時的認證流程

AppStore安裝app時的認證流程

開發者怎麼在debug模式下把app安裝到設備呢(這個題不會)

架構設計
典型源碼的學習
只是列出一些iOS比較核心的開源庫,這些庫包含了很多高質量的思想,源碼學習的時候一定要關注每個框架解決的核心問題是什麼,還有它們的優缺點,這樣才能算真正理解和吸收

AFN

AFN3.0解析
AFN2.0解析
SDWebImage
JSPatch、Aspects(雖然一個不可用、另一個不維護,但是這兩個庫都很精煉巧妙,很適合學習)
Weex/RN, 筆者認爲這種前端和客戶端緊密聯繫的庫是必須要知道其原理的
CTMediator、其他router庫,這些都是常見的路由庫,開發中基本上都會用到
請圈友們在評論下面補充吧
架構設計

手動埋點、自動化埋點、可視化埋點

  • 手動埋點:代碼侵入,不好維護,無法線上動態,但是靈活

  • 自動化埋點:全量獲取,自定義程度低,靈活性差,效率低.好處是容錯高
    技術原理:hook關鍵方法,一類事件的統一入口或者出口,比如統計PV用viewcontroller的viewdidload.統計點擊事件用UIControl的sendAction:to:forEvent等

  • 可視化買點:開發代價大,自定義程度低,靈活性差.好處就是的運營人員上手快.對業務發展由好處.

MVC、MVP、MVVM設計模式

MVC/MVP/MVVM 三種設計模式簡介附demo

常見的設計模式

單例,工廠,MVC、MVP、MVVM、觀察者,代理,裝飾模式

單例的弊端

  • 內存一直佔用,濫用導致內存浪費
  • 線程安全問題多地方使用其中的數據,線程安全需要注意
  • 內容數據如果影響多個地方,代碼耦合度很高.

常見的路由方案,以及優缺點對比

如果保證項目的穩定性
設計一個圖片緩存框架(LRU)
如何設計一個git diff
設計一個線程池?畫出你的架構圖
你的app架構是什麼,有什麼優缺點、爲什麼這麼做、怎麼改進
其他問題
PerformSelector & NSInvocation優劣對比
oc怎麼實現多繼承?怎麼面向切面(可以參考Aspects深度解析-iOS面向切面編程)
哪些bug會導致崩潰,如何防護崩潰
怎麼監控崩潰
app的啓動過程(考察LLVM編譯過程、靜態鏈接、動態鏈接、runtime初始化)
沙盒目錄的每個文件夾劃分的作用
簡述下match-o文件結構
系統基礎知識
進程和線程的區別
HTTPS的握手過程
什麼是中間人攻擊?怎麼預防
TCP的握手過程?爲什麼進行三次握手,四次揮手

堆和棧區的區別?誰的佔用內存空間大

棧由編譯器申請空間大小,是固定的,連續的,對應線程是唯一的,快速高效,缺點是有限制,先入後出不靈活

堆區是通過alloc分配的,它的內存空間是動態的,由一個空閒內存空間指針鏈表維護,內存空間不連續,可以很大,但是容易內存碎片化.好處就是查詢快.

###加密算法:對稱加密算法和非對稱加密算法區別
對稱加密:

  • 加解密一個key
  • 安全性低
  • 速度快
  • 無法確認來源

非對稱加密:

  • 公鑰 私鑰
  • 安全性高,
  • 速度慢
  • 可以確定來源

常見的對稱加密和非對稱加密算法有哪些

對稱加密:
DES 3DES AES
非對稱加密:
RSA DSA

MD5、Sha1、Sha256區別

MD5 輸出128位
SHA1 輸出160位
SHA256 輸出256位

charles抓包過程?不使用charles,4G網絡如何抓包
數據結構與算法
對於移動開發者來說,一般不會遇到非常難的算法,大多以數據結構爲主,筆者列出一些必會的算法,當然有時間了可以去LeetCode上刷刷題

八大排序算法
棧&隊列
字符串處理
鏈表
二叉樹相關操作
深搜廣搜
基本的動態規劃題、貪心算法、二分查找

必看

直擊2020
神經病院objc runtime入院考試
靠譜的 iOS

還沒有熟悉的點

Objc的底層併發API
NSURLCache
1, GCD的源碼分析
相關文檔:
iOS主線程和主隊列的區別
多線程-奇怪的GCD

被遺棄的線程
dispatch_async與dispatch_sync區別
GCD容易讓人迷惑的幾個小問題
深入理解gcd
深入理解GCD之dispatch_queue

Objective-C runtime機制(5)——iOS 內存管理
Https單向認證和雙向認證

Flutter安裝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章