疫情期間比較火的《阿里、字節:一套高效的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_t
的property_array_t
,它的內容是property_t
結構體,內容爲name
和attribute
,對應屬性,是成員變量的聲明,在編譯期前已經存在的屬性會在編譯期生成以_
爲開頭的同名成員變量,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下哪些情況會造成內存泄漏
block中的循環引用
NSTimer的循環引用
addObserver的循環引用
delegate的強引用
大次數循環內存爆漲
非OC對象的內存處理(需手動釋放)
其他
Method Swizzle注意事項
1,只交換一次
2,獲取方法(class_getInstanceMethod/class_getClassMethod)會沿着繼承者鏈向上尋找,所以防止交換時,交換的方法實現時本類的,絕對不能是父類的
屬性修飾符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相關
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的原理,性能如何
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紋理,並確保位圖被上傳到對應紋理中.
- 當runloop進入休眠(BeforeWaiting)和退出(Exit)會通知Observer調用
渲染服務(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,如果緩衝區允許覆蓋,那麼就會產生丟幀,或者和麪斷層.不允許覆蓋,就會丟幀
可以用雙緩衝區,或者加大緩衝區的方案解決
性能優化
如何做啓動優化,如何監控
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時的認證流程
開發者怎麼在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、觀察者,代理,裝飾模式
單例的弊端
- 內存一直佔用,濫用導致內存浪費
- 線程安全問題多地方使用其中的數據,線程安全需要注意
- 內容數據如果影響多個地方,代碼耦合度很高.
常見的路由方案,以及優缺點對比
如果保證項目的穩定性
設計一個圖片緩存框架(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單向認證和雙向認證