可否使用 == 來(lái)判斷兩個(gè)NSString類(lèi)型的字符串是否相同?為什么?
不能。==判斷的是兩個(gè)變量的值的內(nèi)存地址是否相等。如果兩個(gè)字符串內(nèi)容相同的NSString對(duì)象的創(chuàng)建方式不一樣,那這兩個(gè)NSString對(duì)象的內(nèi)存地址是不同的。
// str1和str2的內(nèi)存地址是相同的
NSString *str1 = [NSString stringWithFormat:@"a"];
NSString *str2 = [NSString stringWithFormat:@"a"];
// str3和str4的內(nèi)存地址是相同的
NSString *str3 = @"a";
NSString *str4 = @"a";
// str5和str6的內(nèi)存地址是不同的
NSString *str5 = [NSString stringWithFormat:@"a"];
NSString *str6 = "a";
NSObject為什么要有isEqual方法?
==運(yùn)算符在比較對(duì)象時(shí),比較的是兩個(gè)對(duì)象的內(nèi)存地址是否相同。但是,當(dāng)我們需要比較兩個(gè)對(duì)象的內(nèi)容是否相同時(shí)(例如,兩個(gè)UIColor對(duì)象的顏色是否相同,兩個(gè)NSString對(duì)象的字符串內(nèi)容是否相同),就需要使用isEqual:方法了。
NSObject的isEqual:方法默認(rèn)還是比較兩個(gè)對(duì)象的內(nèi)存地址是否相同,如果想要比較兩個(gè)對(duì)象的內(nèi)容,就需要重寫(xiě)isEqual:方法來(lái)自行實(shí)現(xiàn)。
官方為 Foundation 框架中某些類(lèi)(這些類(lèi)不僅重寫(xiě)了isEqual:方法,還額外實(shí)現(xiàn)了一個(gè)isEqualToXXX:方法)重新實(shí)現(xiàn)了isEqual方法,例如:
-
NSString的-isEqualToString:方法; -
NSArray的-isEqualToArray:方法; -
NSDictionary的-isEqualToDictionary:方法; -
NSSet的-isEqualToSet:方法; -
NSNumber的-isEqualToNumber:方法; -
NSAttributedString的-isEqualToAttributedString:方法; -
NSData的-isEqualToData:方法; -
NSDate的-isEqualToDate:方法。
NSObject的-(NSUInteger)hash方法有什么用?
將對(duì)象添加到NSSet中,或者對(duì)象作為NSDictionary的 key 時(shí),會(huì)調(diào)用對(duì)象的hash方法來(lái)計(jì)算該對(duì)象的哈希值,NSSet和NSDictionary使用這個(gè)哈希值來(lái)確定存儲(chǔ)對(duì)象在哈希表中的位置。
NSObject的hash方法默認(rèn)返回的是對(duì)象的內(nèi)存地址,這非常滿足哈希值對(duì)于唯一性的要求。但在某些場(chǎng)景下,卻不能滿足需求。例如,對(duì)于兩個(gè)字符串內(nèi)容相同的NSString對(duì)象來(lái)說(shuō),如果它們的創(chuàng)建方式不相同,就會(huì)導(dǎo)致它們的內(nèi)存地址并不相同。當(dāng)使用NSString對(duì)象作為NSDictionary的 key 時(shí),如果還是使用內(nèi)存地址作為NSString對(duì)象的哈希值,就會(huì)出現(xiàn)問(wèn)題。因此,官方重寫(xiě)了NSString的hash方法。
重寫(xiě)對(duì)象的hash方法的最佳實(shí)踐,就是對(duì)其關(guān)鍵屬性的hash方法的返回值進(jìn)行按位異或運(yùn)算,然后將結(jié)果值作為該對(duì)象的哈希值。
- (NSUInteger)hash {
return [self.name hash] ^ [self.birthday hash];
}
isEqual方法和hash方法之間的關(guān)系?
相等的兩個(gè)對(duì)象,它們的 hash 值一定相等。但是,兩個(gè)對(duì)象的 hash 值相等的話,這兩個(gè)對(duì)象并不一定相等,還要比較對(duì)象的具體內(nèi)容。
iOS的進(jìn)程之間的通信方式有哪些?
-
URL Scheme:這是 iOS 應(yīng)用程序之間通信最常用的的方式。在應(yīng)用程序B的
TARGETS->info->URL Types中添加一個(gè) Scheme,在應(yīng)用程序A的info.plist文件的 URL Scheme 白名單中加入這個(gè) Scheme。之后,應(yīng)用程序A就能通過(guò)openURL:方法跳轉(zhuǎn)到應(yīng)用程序B了。 - Keychain:Keychain 是一個(gè)安全的存儲(chǔ)容器,其獨(dú)立于每個(gè)應(yīng)用程序的沙盒之外。即使應(yīng)用程序被卸載了,其在 Keychain 中存儲(chǔ)的數(shù)據(jù)也依然存在。Keychain 用于應(yīng)用程序之間通信的一個(gè)典型場(chǎng)景是統(tǒng)一賬戶登錄平臺(tái),使用同一個(gè)賬號(hào)平臺(tái)的多個(gè)應(yīng)用程序,只要用戶登錄了其中一個(gè)應(yīng)用程序,那么其他應(yīng)用程序就可以實(shí)現(xiàn)自動(dòng)登錄,而不需要用戶多次輸入賬號(hào)和密碼。
-
UIPasteboard:剪貼板功能。長(zhǎng)按
UITextView,UITextField,UIWebView控件時(shí),會(huì)彈出復(fù)制,粘貼等選項(xiàng)。每個(gè)應(yīng)用程序都可以訪問(wèn)系統(tǒng)的剪貼板,所以可以利用剪貼板來(lái)在兩個(gè)應(yīng)用程序之間傳遞信息。例如,淘寶的淘口令。 - UIDocumentInteractionController:主要是用來(lái)實(shí)現(xiàn)同一個(gè)設(shè)備上 App 之間的文檔共享,以及文檔預(yù)覽、打印、發(fā)郵件和復(fù)制等功能。
- UIActivityViewController:可以在 App 之間分享和操作數(shù)據(jù)。
assign,weak 和 __unsafe_unretained 的區(qū)別
assign不僅可以修飾基本數(shù)據(jù)類(lèi)型的屬性,還可以修飾對(duì)象類(lèi)型的屬性,MRC 和 ARC 模式下都能使用。asign在修飾對(duì)象類(lèi)型的屬性時(shí),asign指針在引用對(duì)象時(shí),不會(huì)增加對(duì)象的引用計(jì)數(shù)。當(dāng)引用對(duì)象被釋放后,asign指針還是會(huì)指向引用對(duì)象原來(lái)的內(nèi)存地址,當(dāng)繼續(xù)使用asign指針訪問(wèn)對(duì)象時(shí),就會(huì)出現(xiàn)野指針,導(dǎo)致程序運(yùn)行崩潰。
weak只能在 ARC 模式下使用,且只能修飾對(duì)象類(lèi)型。weak指針在引用對(duì)象時(shí),也不會(huì)增加對(duì)象的引用計(jì)數(shù)。但是引用對(duì)象被釋放后,weak指針會(huì)自動(dòng)指向nil。
__unsafe_unretained也只能在 ARC 模式下使用,且只能修飾對(duì)象類(lèi)型。__unsafe_unretained指針在引用對(duì)象時(shí),不會(huì)增加對(duì)象的引用計(jì)數(shù)。當(dāng)引用對(duì)象被釋放后,__unsafe_unretained指針還是會(huì)指向?qū)ο笤瓉?lái)的內(nèi)存地址,當(dāng)繼續(xù)使用__unsafe_unretained指針訪問(wèn)對(duì)象時(shí),會(huì)出現(xiàn)野指針。
weak指針的管理會(huì)消耗更多的 CPU 資源,如果我們可以明確對(duì)象的生命周期,那么使用__unsafe_unretained會(huì)更加高效。
layoutSubviews 方法、setNeedsLayout 方法和 layoutIfNeeded 方法
視圖的layoutSubviews方法的默認(rèn)實(shí)現(xiàn)不會(huì)做任何事情,當(dāng)子視圖的自動(dòng)調(diào)整大小和基于約束的行為無(wú)法滿足我們的需求時(shí),可以重寫(xiě)視圖的此方法來(lái)直接設(shè)置其子視圖的frame。如果在視圖的layoutSubviews方法中有直接設(shè)置子視圖的frame,那么就不要再給視圖添加這個(gè)子視圖的約束了。否則,會(huì)覆蓋掉在視圖的layoutSubviews方法中設(shè)置的子視圖frame。調(diào)用視圖的setNeedsLayout或layoutIfNeeded方法后,或者設(shè)置視圖的frame方法后,會(huì)觸發(fā)其layoutSubviews方法。
調(diào)用視圖的setNeedsLayout方法后,不會(huì)立即計(jì)算視圖及其子視圖的frame,只是標(biāo)記視圖及其子視圖的frame需要重新計(jì)算。等到 Core Animation 繪制圖形時(shí),會(huì)先將視圖的多次布局更新合并,然后再計(jì)算出視圖的frame。接著,會(huì)調(diào)用layoutSubviews方法來(lái)直接設(shè)置其子視圖的frame。再然后,會(huì)根據(jù)添加的子視圖約束來(lái)計(jì)算其子視圖的frame。
當(dāng)視圖需要更新布局時(shí)(設(shè)置視圖的frame或約束時(shí),會(huì)調(diào)用其setNeedsLayout方法來(lái)標(biāo)記該視圖需要更新布局),調(diào)用其layoutIfNeeded方法會(huì)立即計(jì)算該視圖的frame,并調(diào)用其layoutSubviews方法,然后再根據(jù)添加的子視圖約束來(lái)計(jì)算子視圖的frame。如果視圖不需要更新布局,調(diào)用其layoutIfNeeded方法后,layoutIfNeeded方法什么也不會(huì)做,會(huì)直接返回。
int,unsigned int,NSInteger和NSUInteger有什么區(qū)別?
#if __LP64__ || (TARGET_OS_EMBEDDED && !TARGET_OS_IPHONE) || TARGET_OS_WIN32 || NS_BUILD_32_LIKE_64
typedef long NSInteger;
typedef unsigned long NSUInteger;
#else
typedef int NSInteger;
typedef unsigned int NSUInteger;
#endif
int類(lèi)型在32位系統(tǒng)中只能是int類(lèi)型,但在64位系統(tǒng)中卻有可能為int類(lèi)型,也有可能為long類(lèi)型。
NSInteger和NSUInteger是動(dòng)態(tài)定義的類(lèi)型,在不同架構(gòu)下,它們可能是int類(lèi)型,也可能是long類(lèi)型。應(yīng)該盡可能使用NSInteger和NSUInteger,這樣就不用考慮設(shè)備是32位還是64位架構(gòu)了。
應(yīng)用程序的生命周期
iOS應(yīng)用程序有五種狀態(tài):
- 未運(yùn)行狀態(tài)(Not running):應(yīng)用程序尚未啟動(dòng)或者正在運(yùn)行但已被系統(tǒng)終止。
- 未激活狀態(tài)(Inactive):應(yīng)用程序在前臺(tái)運(yùn)行,但是目前還未開(kāi)始接收事件。(它可能正在執(zhí)行其他代碼)應(yīng)用程序通常只在切換到不同狀態(tài)的過(guò)程中,短暫保持此狀態(tài)。
- 激活狀態(tài)(Active):應(yīng)用程序在前臺(tái)運(yùn)行并且正在接收事件,這是應(yīng)用程序在前臺(tái)運(yùn)行時(shí)所處的正常狀態(tài)。
- 后臺(tái)狀態(tài)(Background):應(yīng)用程序在后臺(tái)并執(zhí)行代碼。大多數(shù)應(yīng)用程序進(jìn)入后臺(tái)后,會(huì)在這個(gè)狀態(tài)停留一會(huì)兒。之后,應(yīng)用程序會(huì)被系統(tǒng)掛起。
- 掛起狀態(tài)(Suspended):應(yīng)用程序在后臺(tái)處于睡眠狀態(tài),不會(huì)執(zhí)行代碼。當(dāng)應(yīng)用程序被掛起時(shí),應(yīng)用程序依然保留在內(nèi)存中。當(dāng)出現(xiàn)內(nèi)存不足的情況時(shí),系統(tǒng)可能會(huì)清除被掛起的應(yīng)用程序,為其他前臺(tái)應(yīng)用程序提供更多的內(nèi)存。
啟動(dòng)尚未運(yùn)行的應(yīng)用程序時(shí),應(yīng)用程序會(huì)先切換到未激活狀態(tài),在執(zhí)行didFinishLaunchingWithOptions:代理方法后,切換為激活狀態(tài)。鎖屏或者按下 Home 鍵后,應(yīng)用程序會(huì)先切換到后臺(tái)狀態(tài),一段時(shí)間后,大多數(shù)應(yīng)用程序會(huì)被系統(tǒng)掛起,應(yīng)用程序切換到掛起狀態(tài)。內(nèi)存吃緊時(shí),系統(tǒng)可能會(huì)清除被掛起的應(yīng)用程序,應(yīng)用程序切換到未運(yùn)行狀態(tài)。
Mach-O 是什么?
Mach-O 是 macOS 和 iOS 的可執(zhí)行文件的文件格式,Mach-O 文件分為以下幾類(lèi):
- Executable:應(yīng)用程序的可執(zhí)行文件;
- Dylib:動(dòng)態(tài)庫(kù);
-
Bundle:無(wú)法被 dyld 鏈接的動(dòng)態(tài)庫(kù),只能通過(guò)
dlopen()函數(shù)加載;
Image(鏡像)指的是 Executable,Dylib 或 Bundle 中的一種。Framework,指的是動(dòng)態(tài)庫(kù)(或者靜態(tài)庫(kù))、頭文件和資源文件的集合。
Mach-O 文件分為 Header,Load Commands,Data 三部分,如下圖:

/// 64位架構(gòu)下 mach header 的數(shù)據(jù)結(jié)構(gòu)
struct mach_header_64 {
uint32_t magic; // CPU 架構(gòu)(64位 or 32位)
cpu_type_t cputype; // CPU 類(lèi)型,例如:arm
cpu_subtype_t cpusubtype; // CPU 的具體型號(hào)
uint32_t filetype; // 文件類(lèi)型
uint32_t ncmds; // load commands 的數(shù)量
uint32_t sizeofcmds; // 所有 load commands 的總大小
uint32_t flags; // 標(biāo)志位
uint32_t reserved; // 保留字段
};
// load command 的數(shù)據(jù)結(jié)構(gòu),不同類(lèi)型的 load command 有它們各自的數(shù)據(jù)結(jié)構(gòu)
struct load_command {
uint32_t cmd; // 指令類(lèi)型,例如:LC_SEGMENT 是 segment 的加載指令,LC_LOAD_DYLINKER 是加載 dyld 的加載指令
uint32_t cmdsize; // 指令長(zhǎng)度
};
/// 64位架構(gòu)下 segment command 的數(shù)據(jù)結(jié)構(gòu)
struct segment_command_64 {
uint32_t cmd; // 指令類(lèi)型,這里固定為 LC_SEGMENT_64
uint32_t cmdsize; // 指令長(zhǎng)度
char segname[16]; // segment name,例如:_PAGEZERO,_TEXT,_DATA,_LINKEDIT
uint64_t vmaddr; // segment 在虛擬內(nèi)存中的起始地址
uint64_t vmsize; // segment 的虛擬內(nèi)存大小
uint64_t fileoff; // segment 在文件中的偏移量
uint64_t filesize; // segment 在文件中的大小
vm_prot_t maxprot; // maximum VM protection
vm_prot_t initprot; // initial VM protection
uint32_t nsects; // segment 中包含的 sections 數(shù)量*/
uint32_t flags; // 保留字段
};
/// 64位架構(gòu)下 section 的數(shù)據(jù)結(jié)構(gòu)
struct section_64 {
char sectname[16]; // section name
char segname[16]; // 所在segment 的 name
uint64_t addr; // section 的內(nèi)存起始地址
uint64_t size; // section 所占字節(jié)數(shù)
uint32_t offset; // section 在文件中的偏移量
uint32_t align; // section 的對(duì)齊方式
uint32_t reloff; // file offset of relocation entries
uint32_t nreloc; // number of relocation entries
uint32_t flags; // flags (section type and attributes)
uint32_t reserved1; // reserved (for offset or index)
uint32_t reserved2; // reserved (for count or sizeof)
uint32_t reserved3; // reserved
};
- Header:頭部,包含 Load commands 部分的 load command 數(shù)量和 load command 的總大小,以及 Mach-O 文件的運(yùn)行環(huán)境信息,例如:CPU 架構(gòu)、CPU 類(lèi)型等;
-
Load commands:加載命令,有多種不同類(lèi)型的 load command,系統(tǒng)內(nèi)核會(huì)根據(jù) load command 來(lái)執(zhí)行對(duì)應(yīng)的加載操作。
LC_SEGMENT描述了 Data 部分的 segment 在虛擬內(nèi)存中的布局方式,LC_LOAD_DYLINKER保存著 dyld 的加載路徑; - Data:包含可執(zhí)行的機(jī)器碼和數(shù)據(jù),其被劃分為多個(gè) segment,每個(gè) segment 又分為多個(gè) section,每個(gè) segment 的大小必須是 16KB(64位架構(gòu)下)或者 4KB(32位架構(gòu)下)對(duì)齊的。
segment 映射到內(nèi)存的過(guò)程為:從
fileoff處加載filesize大小的數(shù)據(jù)到虛擬內(nèi)存的vmaddr處,并占用大小為vmsize的虛擬內(nèi)存。
Data 部分的 segment 有以下幾種類(lèi)型(還有其他類(lèi)型這里未列出):
-
_PAGEZERO:空指針陷阱段,映射到虛擬內(nèi)存空間第一頁(yè),捕捉對(duì) NULL 指針的引用; -
__TEXT:代碼段,只讀,包含可執(zhí)行的機(jī)器碼和只讀數(shù)據(jù); -
__DATA:數(shù)據(jù)段,包含所有可讀可寫(xiě)的數(shù)據(jù); -
__LINKEDIT:鏈接編輯段,包含 dyld 鏈接時(shí)需要用到的原始數(shù)據(jù),例如:間接符號(hào)表,符號(hào)表,字符串表等;
_TEXT段的 section 類(lèi)型 |
包含內(nèi)容 |
|---|---|
| __text | 程序可執(zhí)行的機(jī)器碼 |
| __stubs | 間接符號(hào)存根,用于跳轉(zhuǎn)到懶加載外部符號(hào)指針數(shù)組 |
| __stubs_helper | 懶加載符號(hào)加載輔助函數(shù) |
| __cstring | 只讀的 C 字符串常量 |
| ...... | ...... |
_DATA段的 section 類(lèi)型 |
包含內(nèi)容 |
|---|---|
| __nl_symbol_ptr | 非懶加載外部符號(hào)指針數(shù)組,dyld 加載時(shí)立即綁定值 |
| __la_symbol_ptr | 懶加載外部符號(hào)指針數(shù)組,第一次調(diào)用時(shí)才綁定值 |
| __got | 非懶加載全局符號(hào)指針數(shù)組 |
| __mod_init_func | C++ 的靜態(tài)構(gòu)造函數(shù) |
| ...... | ...... |
有關(guān)更多 Mach-O 的信息,可以參看 iOS堆棧信息解析(Mach-O),iOS逆向之五-MACH-O文件解析,Mach-O介紹,Mach-O學(xué)習(xí)小結(jié)。
Mach-O 相關(guān)數(shù)據(jù)結(jié)構(gòu)的定義在 XNU 源碼的 xnu/EXTERNAL_HEADERS/mach-o/loader.h
應(yīng)用程序的啟動(dòng)過(guò)程
啟動(dòng)應(yīng)用程序時(shí),系統(tǒng)內(nèi)核會(huì)創(chuàng)建一個(gè)新進(jìn)程,并讀取磁盤(pán)(硬盤(pán))中的應(yīng)用程序 Mach-O 文件,然后根據(jù) Mach-O 文件的 Header 信息將磁盤(pán)中的應(yīng)用程序 Mach-O 文件加載到內(nèi)存中。
在主程序 Mach-O 文件的加載過(guò)程中,會(huì)首先創(chuàng)建一個(gè)虛擬內(nèi)存映射空間(虛擬內(nèi)存是從硬盤(pán)中劃分的一塊連續(xù)區(qū)域,32位架構(gòu)下最大為 4GB,64位架構(gòu)下最大為 64GB)。接著,為主程序計(jì)算 ASLR 隨機(jī)偏移量,以及為動(dòng)態(tài)鏈接器 dyld 計(jì)算 ASLR 隨機(jī)偏移量。然后,開(kāi)始解析主程序的 Mach-O 文件。在主程序 Mach-O 文件的解析過(guò)程中,會(huì)先遍歷所有的 load command。如果 load command 是 segment 的加載指令,則會(huì)將 segment 映射到虛擬內(nèi)存中(從fileoff處加載filesize大小的數(shù)據(jù)到虛擬內(nèi)存的vmaddr處,并占用大小為vmsize的虛擬內(nèi)存);如果 load command 是 dyld 的加載指令,則會(huì)獲取 dyld 的加載路徑;如果是其他類(lèi)型的加載指令,則會(huì)執(zhí)行對(duì)應(yīng)的加載操作。最后,會(huì)根據(jù) dyld 的加載路徑讀取磁盤(pán)中 dyld 的 Mach-O 文件,并解析 dyld 的 Mach-O 文件,將其 segment 映射到虛擬內(nèi)存中。
在主程序和 dyld 加載完成后,系統(tǒng)內(nèi)核會(huì)執(zhí)行 dyld 的入口函數(shù),dyld 的入口函數(shù)會(huì)調(diào)用其_main()函數(shù)。
在 dyld 的_main()函數(shù)內(nèi)部實(shí)現(xiàn)中,會(huì)調(diào)用mapSharedCache()函數(shù)來(lái)加載共享緩存文件,共享緩存文件中包含著所有共享系統(tǒng)動(dòng)態(tài)庫(kù)的 Mach-O 文件,如果共享緩存文件還未加載,則會(huì)將共享緩存文件從磁盤(pán)映射到共享內(nèi)存(虛擬內(nèi)存中劃分的一塊區(qū)域)中;如果已加載,則會(huì)獲取共享緩存文件的加載信息。
接著,會(huì)實(shí)例化主程序,并將主程序?qū)嵗?strong>image 對(duì)象)保存到sAllImages數(shù)組中。這一步是為磁盤(pán)中主程序 Mach-O 文件的鏡像(在內(nèi)存中的主程序 Mach-O 文件)創(chuàng)建一個(gè)ImageLoader類(lèi)型的 image 對(duì)象,以便獲取鏡像的控制權(quán)。(程序運(yùn)行時(shí),操作的是 Mach-O 文件的鏡像,而不是磁盤(pán)中的 Mach-O 文件。)
然后,加載環(huán)境變量中插入的動(dòng)態(tài)庫(kù),并實(shí)例化這些動(dòng)態(tài)庫(kù),以及將這些動(dòng)態(tài)庫(kù)實(shí)例(image 對(duì)象)保存到sAllImages數(shù)組中。
再然后,會(huì)鏈接主程序。鏈接時(shí)主要做了以下事情:
- 遞歸加載主程序的依賴庫(kù)以及這些依賴庫(kù)的依賴庫(kù),如果依賴庫(kù)是共享系統(tǒng)動(dòng)態(tài)庫(kù),則會(huì)實(shí)例化共享緩存中的共享系統(tǒng)動(dòng)態(tài)庫(kù),并將共享系統(tǒng)動(dòng)態(tài)庫(kù)的鏡像實(shí)例保存到
sAllImages數(shù)組中;如果不是,則會(huì)加載依賴庫(kù)到內(nèi)存中,并實(shí)例化依賴庫(kù)鏡像,然后將依賴庫(kù)的鏡像實(shí)例保存到sAllImages數(shù)組中。 - 由于使用了 ASLR(地址空間布局隨機(jī)化)技術(shù),Mach-O 文件的鏡像在虛擬內(nèi)存中的起始地址會(huì)被增加一個(gè)隨機(jī)偏移,所以鏡像中的內(nèi)部資源的基地址也會(huì)加上這個(gè)隨機(jī)偏移。然而鏡像中的內(nèi)部數(shù)據(jù)指針指向的地址還是原始地址,因此 dyld 會(huì)在鏈接主程序時(shí),修復(fù)主程序和其依賴庫(kù)各自的內(nèi)部數(shù)據(jù)指針的指向。
接著,會(huì)鏈接環(huán)境變量(Xcode->Product->Scheme->Edit Scheme->Run->Arguments可以設(shè)置環(huán)境變量)中插入的動(dòng)態(tài)庫(kù)。(與鏈接主程序時(shí)所做的事情相同)
然后,dyld 會(huì)將主程序和其依賴庫(kù)各自的非懶加載外部符號(hào)綁定到真實(shí)的地址。(懶加載外部符號(hào)會(huì)在運(yùn)行時(shí)動(dòng)態(tài)綁定到真實(shí)的地址)
再然后,會(huì)將環(huán)境變量中插入的動(dòng)態(tài)庫(kù)和其依賴庫(kù)的非懶加載外部符號(hào)綁定到真實(shí)的地址。(與綁定主程序時(shí)所做的事情相同)
接著,綁定主程序的弱符號(hào)。(弱符號(hào):未初始化的全局變量;強(qiáng)符號(hào):函數(shù)和已初始化的全局變量)
然后,dyld 會(huì)初始化主程序。在主程序初始化過(guò)程中,會(huì)首先調(diào)用環(huán)境變量中插入的動(dòng)態(tài)庫(kù)及其依賴庫(kù)的初始化函數(shù)和 C++ 靜態(tài)構(gòu)造函數(shù),然后再調(diào)用主程序及其依賴庫(kù)的初始化函數(shù)和 C++ 靜態(tài)構(gòu)造函數(shù)。最先調(diào)用的是 libsystem 動(dòng)態(tài)庫(kù)的初始化函數(shù),libsystem 動(dòng)態(tài)庫(kù)的初始化函數(shù)會(huì)觸發(fā) libobjc 動(dòng)態(tài)庫(kù)的_objc_init初始化函數(shù),該函數(shù)內(nèi)部會(huì)調(diào)用runtime_init和 dyld 的_dyld_objc_notify_register等函數(shù)(其他函數(shù)未列出)。
runtime_init函數(shù)會(huì)初始化用于存儲(chǔ)類(lèi)的未加載 category 的unattachedCategories哈希表,以及初始化用于存儲(chǔ)類(lèi)和元類(lèi)的allocatedClasses集合。
_dyld_objc_notify_register函數(shù)會(huì)調(diào)用registerObjcNotifiers函數(shù),該函數(shù)會(huì)保存 libobjc 傳遞過(guò)來(lái)的map_images、load_images和unmap_image函數(shù)的地址。由于此時(shí)主程序和所有依賴庫(kù)都已經(jīng)被映射到內(nèi)存中了,所以registerObjcNotifiers函數(shù)在注冊(cè)完回調(diào)之后,會(huì)立即調(diào)用 libobjc 的map_images函數(shù),并將所有使用了 libobjc 的 image 的文件路徑和 mach header 傳遞過(guò)去。又由于此時(shí)已經(jīng)有 image 完成了初始化,所以registerObjcNotifiers函數(shù)還會(huì)遍歷所有當(dāng)前已經(jīng)加載的 image ,如果當(dāng)前 image 已經(jīng)初始化了并且使用了 libobjc,則會(huì)立即調(diào)用 libobjc 的load_images函數(shù),并將這個(gè) image 的文件路徑和 mach header 傳遞過(guò)去。
之后,每當(dāng)調(diào)用一個(gè)使用了 libobjc 的 image 的初始化函數(shù)之后,dyld 就會(huì)調(diào)用一次 libobjc 的load_images回調(diào)函數(shù),并將這個(gè) image 的文件路徑和 mach header 傳遞過(guò)去。
調(diào)用map_images函數(shù)時(shí),會(huì)保存所有依賴 libobjc 動(dòng)態(tài)庫(kù)的 image 的 header 信息,注冊(cè)這些 image 中的 sel、協(xié)議、類(lèi),并實(shí)現(xiàn)這些 image 中非懶加載類(lèi)和其元類(lèi)。
調(diào)用load_images函數(shù)時(shí),如果是首次調(diào)用,則會(huì)加載所有依賴 libobjc 動(dòng)態(tài)庫(kù)的 image 中的 category。并且每次調(diào)用load_images函數(shù)時(shí),會(huì)調(diào)用本次初始化的 image 中的所有 objc 類(lèi)和其 category 的+load方法。
最后,dyld 會(huì)調(diào)用主程序的main函數(shù),main函數(shù)會(huì)調(diào)用UIApplicationMain函數(shù),該函數(shù)首先會(huì)從可用的 storyboard 文件中加載應(yīng)用程序的啟動(dòng)界面,并調(diào)用AppDelegate對(duì)象的willFinishLaunchingWithOptions:和didFinishLaunchingWithOptions:方法來(lái)執(zhí)行初始化設(shè)置,然后啟動(dòng)應(yīng)用程序主線程的 runloop 來(lái)開(kāi)始接收事件。
有關(guān) App 啟動(dòng)過(guò)程的更多信息可以參看 dyld詳解,深入理解虛擬內(nèi)存機(jī)制,啟動(dòng)優(yōu)化之Clang插樁實(shí)現(xiàn)二進(jìn)制重排,iOS啟動(dòng)時(shí)間優(yōu)化,XNU、dyld源碼分析Mach-O和動(dòng)態(tài)庫(kù)的加載過(guò)程(上),XNU、dyld源碼分析Mach-O和動(dòng)態(tài)庫(kù)的加載過(guò)程(下),dyld加載流程,深入iOS系統(tǒng)底層之程序鏡像。
XNU 源碼的
load_init_program()函數(shù)在 xnu/bsd/kern/kern_exec.c,load_machfile()函數(shù)在 xnu/bsd/kern/mach_loader.c。dyld 的入口函數(shù)_main()在 dyld/src/dyld2.cpp。
如何優(yōu)化應(yīng)用程序的啟動(dòng)時(shí)長(zhǎng)?
啟動(dòng)時(shí)長(zhǎng)檢測(cè)
- 點(diǎn)擊 Xcode 的
Product->Scheme->Edit Scheme->Run->Auguments,添加值為1的環(huán)境變量DYLD_PRINT_STATISTICS。 - 使用
Instruments中的App Launch。
main函數(shù)調(diào)用之前的優(yōu)化
- 二進(jìn)制重排;
- 刪除沒(méi)有使用的變量和方法;
- 刪除沒(méi)有使用的 Objective-C 類(lèi);
- 將類(lèi)的
+load方法中的初始化設(shè)置移到+initializer方法中去。
main函數(shù)調(diào)用之后的優(yōu)化
- 減少
willFinishLaunching和didFinishLaunching方法中的初始化設(shè)置,將不是第一時(shí)間需要執(zhí)行的操作延后執(zhí)行; -
didFinishLaunching方法執(zhí)行完畢后首次顯示的應(yīng)用界面使用代碼布局來(lái)代替 xib 和 storyboard,因?yàn)?xib 和 storyboard 文件的解析需要耗費(fèi)額外的時(shí)間。(這樣做可以加快用戶看到首界面的時(shí)間)
二進(jìn)制重排
作用
虛擬內(nèi)存和物理內(nèi)存(運(yùn)行內(nèi)存,在內(nèi)存條上)是分頁(yè)的,每頁(yè)大小為 16KB(64位架構(gòu)下,page 大小為16KB;32位架構(gòu)下,page 大小為4KB)。當(dāng)訪問(wèn)一個(gè)虛擬內(nèi)存頁(yè)時(shí),如果對(duì)應(yīng)的物理內(nèi)存頁(yè)還未分配時(shí),就會(huì)觸發(fā)一次缺頁(yè)中斷。這時(shí),系統(tǒng)會(huì)阻塞進(jìn)程,分配物理內(nèi)存頁(yè),從磁盤(pán)讀取數(shù)據(jù)緩存到物理內(nèi)存頁(yè)中。如果應(yīng)用程序是通過(guò) App Store 分發(fā)的,觸發(fā)缺頁(yè)中斷后,還會(huì)對(duì)代碼進(jìn)行簽名驗(yàn)證。所以,處理缺頁(yè)中斷是一項(xiàng)比較耗時(shí)的操作。
由于系統(tǒng)使用懶加載方式加載 Mach-O 文件,Mach-O 文件一開(kāi)始并沒(méi)有從磁盤(pán)讀入到內(nèi)存,只是和內(nèi)存有一個(gè)映射。因此,應(yīng)用程序一開(kāi)始只是分配了虛擬內(nèi)存,但還未分配物理內(nèi)存。假設(shè)應(yīng)用程序在啟動(dòng)過(guò)程中需要調(diào)用方法 A 和 方法 B,當(dāng)首次調(diào)用方法 A 時(shí),由于方法 A 所在的虛擬內(nèi)存頁(yè)對(duì)應(yīng)的物理內(nèi)存頁(yè)不存在,所以會(huì)觸發(fā)缺頁(yè)中斷。此時(shí),系統(tǒng)會(huì)從磁盤(pán)讀取方法 A 對(duì)應(yīng)的物理內(nèi)存頁(yè)所包含的所有數(shù)據(jù),并將這些數(shù)據(jù)緩存到物理內(nèi)存頁(yè)中。如果方法 A 和方法 B 是在同一個(gè)內(nèi)存頁(yè)(page)中,那么后面調(diào)用方法 B 時(shí),就不會(huì)觸發(fā)缺頁(yè)中斷了。而函數(shù)的二進(jìn)制代碼在 Mach-O 文件中的位置是根據(jù)編譯順序而不是調(diào)用順序來(lái)排列的,所以方法 A 和方法 B 有可能分布在不同的內(nèi)存頁(yè)上。可以通過(guò)重新排列應(yīng)用程序在啟動(dòng)時(shí)調(diào)用的方法的二進(jìn)制代碼在 Mach-O 文件中的位置來(lái)使它們分配在同一個(gè)內(nèi)存頁(yè)中,這樣就能減少觸發(fā)缺頁(yè)中斷的次數(shù),從而加快應(yīng)用程序的啟動(dòng)過(guò)程。
如何重排二進(jìn)制
Xcode 的Build Settings中的Linking項(xiàng)有一個(gè)Order File參數(shù),可以通過(guò)這個(gè)參數(shù)配置一個(gè) order 文件的路徑。在這個(gè) order 文件中,將應(yīng)用程序啟動(dòng)過(guò)程中調(diào)用的符號(hào)按順序?qū)懺谶@個(gè)文件中。在構(gòu)建工程的時(shí)候,Xcode 會(huì)讀取這個(gè)文件,并按照文件中的符號(hào)順序來(lái)調(diào)整對(duì)應(yīng)代碼在 Mach-O 文件中的偏移地址,將在啟動(dòng)過(guò)程加載的方法集中到 Mach-O 文件的最前面。
如何檢測(cè)應(yīng)用程序在啟動(dòng)過(guò)程中調(diào)用了哪些方法
使用Clang靜態(tài)插樁在編譯期在每一個(gè)函數(shù)內(nèi)部添加 hook 代碼。
如何查看應(yīng)用程序啟動(dòng)過(guò)程中的缺頁(yè)中斷(page fault)次數(shù)
使用Instruments中的System Trace,選擇真機(jī)設(shè)備,然后選擇調(diào)試的應(yīng)用程序,點(diǎn)擊啟動(dòng),等 app 首界面展示之后,終止調(diào)試,查看 app 的Main Thread的Virtual Memory中的File Backed Page In次數(shù)。
如何 hook 主程序所引用的系統(tǒng)動(dòng)態(tài)庫(kù)的 C 函數(shù)?
主程序 Mach-O 文件 DATA 部分的__LINKEDIT段包含一個(gè)字符串表(string table),一個(gè)符號(hào)表(symbol table),一個(gè)間接符號(hào)表(indirect symbol table)。
字符串表是一個(gè)存放著所有符號(hào)的字符串名稱(chēng)的數(shù)組。
符號(hào)表是一個(gè)存儲(chǔ)著主程序中所有符號(hào)(內(nèi)部符號(hào)和外部符號(hào))的數(shù)組。
符號(hào)是一個(gè)nlist結(jié)構(gòu)體,其中存儲(chǔ)著符號(hào)的字符串名稱(chēng)在字符串表中的索引。
// 64位架構(gòu)下符號(hào)的數(shù)據(jù)結(jié)構(gòu)為
struct nlist_64 {
union {
uint32_t n_strx; // 符號(hào)的字符串名稱(chēng)在字符串表中的索引
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
間接符號(hào)表是一個(gè)存儲(chǔ)著所有符號(hào)指針?biāo)鶎?duì)應(yīng)的符號(hào)在符號(hào)表中的索引的數(shù)組。
Mach-O 文件的__DATA段中包含一個(gè)非懶加載外部符號(hào)指針數(shù)組__nl_symbol_ptr和一個(gè)懶加載外部符號(hào)指針數(shù)組__la_symbol_ptr。
__nl_symbol_ptr和__la_symbol_ptr分別對(duì)應(yīng)著一個(gè) section,section header 數(shù)據(jù)結(jié)構(gòu)中的reserved1字段是【指針數(shù)組中第一個(gè)指針?biāo)鶎?duì)應(yīng)的外部符號(hào)在符號(hào)表中的索引】在【間接符號(hào)表】中的【偏移量】,間接符號(hào)表的地址加上reserved1偏移量就是數(shù)組中第一個(gè)指針對(duì)應(yīng)的符號(hào)索引的地址。由于__nl_symbol_ptr和__la_symbol_ptr中每個(gè)指針對(duì)應(yīng)的符號(hào)索引在間接符號(hào)表中是連續(xù)存儲(chǔ)的,所以獲取到第一個(gè)指針對(duì)應(yīng)的符號(hào)索引后,就能獲取到每個(gè)指針對(duì)應(yīng)的符號(hào)索引了。
根據(jù)符號(hào)索引,可以從符號(hào)表中讀取到外部符號(hào)指針對(duì)應(yīng)的符號(hào),再根據(jù)符號(hào)中存儲(chǔ)的字符串索引,就可以從字符串表中讀取到符號(hào)對(duì)應(yīng)的符號(hào)名稱(chēng)了。
hook 系統(tǒng) C 函數(shù)原理:以字符串名稱(chēng)相匹配的方式找到系統(tǒng)動(dòng)態(tài)庫(kù) C 函數(shù)的指針,然后保存系統(tǒng) C 函數(shù)的原始地址,再將指針指向自定義函數(shù)的地址。在自定義函數(shù)中,通過(guò)保存系統(tǒng) C 函數(shù)的原始地址直接調(diào)用系統(tǒng) C 函數(shù),并執(zhí)行其他額外操作。
更多信息,可以參看 Fishhook替換C函數(shù)的原理,iOS hook框架之——fishhook。
關(guān)于編譯鏈接的相關(guān)信息,可以參看 徹底理解鏈接器。
動(dòng)態(tài)庫(kù),靜態(tài)庫(kù)以及 framework 之間的區(qū)別
- 靜態(tài)庫(kù)的文件后綴名為
.a,動(dòng)態(tài)庫(kù)的文件后綴名為.tbd或者.dylib; - 靜態(tài)庫(kù)在編譯時(shí)會(huì)被拷貝到應(yīng)用程序的可執(zhí)行文件中去,如果有多個(gè)應(yīng)用程序使用這個(gè)靜態(tài)庫(kù),那么它會(huì)被多次加載到物理內(nèi)存中;
- 動(dòng)態(tài)庫(kù)在編譯時(shí)不會(huì)被拷貝到應(yīng)用程序的可執(zhí)行文件中去,而是在啟動(dòng)應(yīng)用程序時(shí),由系統(tǒng)動(dòng)態(tài)加載到物理內(nèi)存中,系統(tǒng)只會(huì)加載一次,多個(gè)應(yīng)用程序可以共用。(iOS 不允許使用自定義的動(dòng)態(tài)庫(kù),否則會(huì)被拒絕上架)
- 靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)只是一個(gè)純二進(jìn)制文件,而 framework 中除了二進(jìn)制文件之外,還包含資源文件和頭文件;
- 靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)不能直接使用,需要有
.h頭文件配合,framework 文件可以直接使用; - framework = 靜態(tài)庫(kù)(或者動(dòng)態(tài)庫(kù))+ 頭文件 + 資源文件;
UIViewController的生命周期
- 創(chuàng)建(
alloc)并初始化(init)視圖控制器; - push 或者 present 視圖控制器;
- 調(diào)用視圖控制器的
loadView方法來(lái)加載與其關(guān)聯(lián)的視圖; - 視圖加載完畢后,調(diào)用
viewDidLoad方法; - 在視圖將要顯示之前,會(huì)調(diào)用
viewWillAppear方法; - 將要布局視圖的子視圖之前,會(huì)調(diào)用
viewWillLayoutSubviews方法; - 在布局完視圖的子視圖之后,會(huì)調(diào)用
viewDidLayoutSubviews方法; - 在視圖已經(jīng)顯示之后,會(huì)調(diào)用
viewDidAppear方法; - pop 或者 dismiss 視圖控制器;
- 在視圖將要從屏幕上移除時(shí),會(huì)調(diào)用
viewWillDisappear方法; - 在視圖已經(jīng)從屏幕上移除后,會(huì)調(diào)用
viewDidDisappear方法。 - 當(dāng)應(yīng)用程序內(nèi)存不足時(shí),會(huì)調(diào)用
didReceiveMemoryWarning方法。
觸摸事件響應(yīng)鏈
應(yīng)用程序使用響應(yīng)者對(duì)象來(lái)接收和處理事件,屬于UIResponder類(lèi)的實(shí)例對(duì)象都是響應(yīng)者。當(dāng)應(yīng)用程序接收到一個(gè)事件時(shí),UIKit 會(huì)自動(dòng)將該事件指向最合適的響應(yīng)者對(duì)象,此響應(yīng)者稱(chēng)為第一響應(yīng)者。響應(yīng)者接收到原始事件后,必須處理該事件或者將此事件轉(zhuǎn)發(fā)給另一個(gè)響應(yīng)者。UIkit 定義了如何將事件從一個(gè)響應(yīng)者傳遞到下一個(gè)響應(yīng)者的默認(rèn)規(guī)則:
-
UIView對(duì)象:如果這個(gè)視圖是視圖控制器的根視圖,那么下一個(gè)響應(yīng)者就是這個(gè)視圖控制器;否則,下一個(gè)響應(yīng)者就是它的父視圖。 -
UIViewController對(duì)象:如果視圖控制器是window的根視圖控制器,則下一個(gè)響應(yīng)者就是window;否則,下一個(gè)響應(yīng)者是該視圖控制器的父視圖控制器。 -
UIWindow對(duì)象:window的下一個(gè)響應(yīng)者是UIApplication對(duì)象。 -
UIApplication對(duì)象:UIApplication對(duì)象的下一個(gè)響應(yīng)者就是AppDelegate對(duì)象。
可以隨時(shí)通過(guò)覆蓋響應(yīng)者對(duì)象中的nextResponder屬性來(lái)更改 UIKit 定義的默認(rèn)規(guī)則。
確定觸摸事件的第一響應(yīng)者
點(diǎn)擊屏幕后,系統(tǒng)內(nèi)核會(huì)生成一個(gè)觸摸事件,并通過(guò) mach port 將觸摸事件傳遞給當(dāng)前處于前臺(tái)運(yùn)行的應(yīng)用程序。然后,該應(yīng)用程序主線程的 runloop 所注冊(cè)的基于端口的輸入源(source1)會(huì)觸發(fā)回調(diào),并將這個(gè)觸摸事件交給 UIKit 去進(jìn)行應(yīng)用內(nèi)分發(fā)。
UIKit 會(huì)調(diào)用主window的hitTest:withEvent:方法來(lái)查找視圖層中包含觸摸點(diǎn)的最上層視圖。在hitTest:withEvent:方法內(nèi)部實(shí)現(xiàn)中,如果當(dāng)前視圖不能響應(yīng)用戶交互,或者被隱藏,或者alph小于0.01,則會(huì)忽略當(dāng)前視圖及其子視圖。否則,會(huì)調(diào)用pointInside:withEvent:方法來(lái)判斷當(dāng)前視圖是否包含觸摸點(diǎn)。
如果不包含,則會(huì)忽略當(dāng)前視圖及其子視圖;如果包含,則會(huì)倒敘遍歷(最先訪問(wèn)最后添加的子視圖)當(dāng)前視圖的子視圖,并調(diào)用每個(gè)子視圖的hitTest:withEvent:方法來(lái)查找當(dāng)前子視圖層中包含觸摸點(diǎn)的最上層視圖。
如果主window的hitTest:withEvent:方法最終返回nil,則應(yīng)用程序會(huì)忽略這個(gè)觸摸事件;否則,UIKit會(huì)將觸摸事件傳遞給主window的hitTest:withEvent:方法所返回的視圖。
如果這個(gè)視圖實(shí)現(xiàn)了touchesBegan:withEvent:、touchesMoved:withEvent:和touchesEnded:withEvent:方法中的一個(gè)或者多個(gè),并且這個(gè)視圖所在視圖層中的所有視圖都沒(méi)有添加手勢(shì)識(shí)別器,那么當(dāng)觸摸開(kāi)始發(fā)生時(shí),系統(tǒng)會(huì)調(diào)用其touchesBegan:withEvent:方法去響應(yīng)觸摸事件。當(dāng)觸摸位置移動(dòng)時(shí),會(huì)調(diào)用其touchesMoved:withEvent:方法,當(dāng)觸摸結(jié)束時(shí),會(huì)調(diào)用touchesEnded:withEvent:方法;如果這幾個(gè)方法一個(gè)都沒(méi)有被實(shí)現(xiàn),那么系統(tǒng)會(huì)沿著默認(rèn)的響應(yīng)者鏈去傳遞觸摸事件。如果響應(yīng)者鏈中有響應(yīng)者實(shí)現(xiàn)了這些方法,那么該響應(yīng)者對(duì)象就會(huì)去處理傳遞來(lái)的觸摸事件。否則,該觸摸事件就不會(huì)被處理。
如果這個(gè)視圖實(shí)現(xiàn)了touchesBegan:withEvent:、touchesMoved:withEvent:和touchesEnded:withEvent:方法中的一個(gè)或者多個(gè),并且這個(gè)視圖所在視圖層中的某些視圖有添加手勢(shì)識(shí)別器,那么當(dāng)觸摸開(kāi)始發(fā)生時(shí),系統(tǒng)會(huì)調(diào)用其touchesBegan:withEvent:方法去響應(yīng)觸摸事件。隨后,如果視圖層中的視圖所添加的手勢(shì)識(shí)別器識(shí)別手勢(shì)成功了,則會(huì)立即將觸摸事件傳遞給手勢(shì)識(shí)別器去處理,然后會(huì)調(diào)用這個(gè)視圖的touchesCancelled:withEvent:方法。
UITableView的Cell重用機(jī)制
tableView 加載 cell 時(shí),首先在重用池中查找有沒(méi)有可以重用的 cell。如果沒(méi)有可重用的 cell,則創(chuàng)建一個(gè)新的 cell 來(lái)加載,并將這個(gè) cell 添加到當(dāng)前正在顯示的 cell 數(shù)組當(dāng)中去;如果有可重用的 cell,則從重用池中取出這個(gè) cell 來(lái)加載,并將 cell 添加到當(dāng)前正在顯示的 cell 數(shù)組中去。
滑動(dòng) tableview 時(shí),會(huì)移除已經(jīng)沒(méi)有顯示的 cell,然后從當(dāng)前正在顯示的 cell 數(shù)組中取出已經(jīng)沒(méi)有顯示的 cell,并將其添加到重用池中。
刷新 tableview 時(shí),會(huì)清空當(dāng)前正在顯示的 cell 數(shù)組,并將這些 cell 添加到重用池中,然后重新加載 cell。
UICollectionView自定義布局
-
prepareLayout方法:提前計(jì)算好 cell 的布局信息和 UICollectionView 的內(nèi)容區(qū)域大小,并將它們緩存; -
collectionViewContentSize方法:返回 UICollectionView 的內(nèi)容區(qū)域大??; -
layoutAttributesForElementsInRect:方法:UICollectionView 基于當(dāng)前的滾動(dòng)位置調(diào)用此方法來(lái)查找在特定 rect 中的 cell 和補(bǔ)充視圖(headerView 和 footerView)的布局信息,然后使用這些布局信息來(lái)展示 cell 和補(bǔ)充視圖。需要在該方法中遍歷提前計(jì)算好的所有布局信息,返回所有 frame 和給定 rect 相交的 cell 和補(bǔ)充視圖的布局信息。
UIView和CALayer的區(qū)別和聯(lián)系
-
CALayer負(fù)責(zé)繪制內(nèi)容,不能傳遞和響應(yīng)事件; -
UIView負(fù)責(zé)管理CALayer需要繪制的內(nèi)容,以及負(fù)責(zé)傳遞和響應(yīng)事件; - 每個(gè)
UIView都關(guān)聯(lián)有一個(gè)CALayer,并且UIView是這個(gè)CALayer的委托對(duì)象。對(duì)UIView與顯示內(nèi)容相關(guān)的屬性進(jìn)行操作時(shí),實(shí)際上是在對(duì)其關(guān)聯(lián)的CALayer的相關(guān)屬性進(jìn)行操作。
圖像顯示原理

計(jì)算機(jī)屏幕顯示圖像時(shí),是以屏幕上的單個(gè)像素點(diǎn)來(lái)代表圖像中的某個(gè)點(diǎn)的,對(duì)一組像素點(diǎn)進(jìn)行排列和著色就能構(gòu)成圖像了。由像素點(diǎn)組成的圖像,叫做位圖。
在顯示圖像時(shí),由 CPU 計(jì)算布局信息,并將需要顯示的內(nèi)容繪制成位圖(也就是紋理),然后將這些位圖傳遞給 GPU。接著,由 GPU 進(jìn)行紋理的變換、合成和渲染,并將渲染結(jié)果提交到幀緩沖區(qū)。當(dāng)硬件時(shí)鐘發(fā)出 VSync 信號(hào)時(shí),視頻控制器會(huì)從幀緩沖區(qū)中讀取數(shù)據(jù)來(lái)傳遞給屏幕去顯示。
界面滑動(dòng)卡頓的原因

在界面滑動(dòng)過(guò)程中,如果人眼每隔 16.7ms 就能看到一幀新的畫(huà)面,那么人眼所看到的動(dòng)畫(huà)效果就是流暢的。
iOS 每隔 16.7ms 就會(huì)產(chǎn)生一個(gè) VSync 信號(hào),如果在下一個(gè) VSync 信號(hào)到來(lái)時(shí),CPU 和 GPU 沒(méi)有完成顯示內(nèi)容的提交,那么這一幀畫(huà)面就會(huì)被丟棄。而此時(shí),屏幕會(huì)保留之前的畫(huà)面不變,這樣就會(huì)導(dǎo)致人眼所看到的動(dòng)畫(huà)效果是卡頓的。
離屏渲染
什么是離屏渲染
離屏渲染,指的是 GPU 在當(dāng)前屏幕緩沖區(qū)以外新開(kāi)辟一個(gè)緩沖區(qū)進(jìn)行渲染。
哪些操作會(huì)觸發(fā)離屏渲染
啟用圖層的masksToBounds,shadow、mask屬性時(shí),會(huì)觸發(fā) GPU 的離屏渲染。
為何要避免離屏渲染
啟用圖層的這些屬性之后,Core Animation 只是在 CPU 中繪制了圖層內(nèi)容,CPU 將圖層內(nèi)容交給 GPU 后,會(huì)由 GPU 去剪切內(nèi)容區(qū)域、繪制陰影和遮罩。GPU 在繪制這些內(nèi)容時(shí),會(huì)新開(kāi)辟一個(gè)緩沖區(qū),并將上下文環(huán)境從當(dāng)前屏幕緩沖區(qū)切換到屏幕外緩沖區(qū)。繪制完成后,又會(huì)將上下文環(huán)境從屏幕外緩沖區(qū)切換回當(dāng)前屏幕緩沖區(qū),然后進(jìn)行紋理合成,并將結(jié)果渲染到當(dāng)前屏幕緩沖區(qū)。由于上下文切換的開(kāi)銷(xiāo)非常昂貴,所以要盡量避免使用離屏渲染。
光柵化
如果不可避免的要觸發(fā)離屏渲染,并且觸發(fā)離屏渲染的圖層的內(nèi)容不會(huì)頻繁的變化,則可以啟用圖層的光柵化shouldRasterize屬性。圖層啟用光柵化之后,在第一次離屏渲染完成后,會(huì)緩存這個(gè)圖層的位圖,這個(gè)位圖的緩存時(shí)效是有限制的。在緩存時(shí)效內(nèi)刷新屏幕時(shí),會(huì)直接讀取緩存來(lái)使用。
界面滑動(dòng)卡頓的優(yōu)化方案
使用 Time Profiler 來(lái)查看代碼運(yùn)行所耗費(fèi)的時(shí)間,使用 Core Animation 獲取圖形繪制情況、FPS 和離屏渲染。
xcode 9.3 之后,運(yùn)行app,勾選
debug->view debugging->rendering來(lái)查看離屏渲染。
重用Cell
如果 cell 展示的內(nèi)容比較復(fù)雜,那么視圖對(duì)象的創(chuàng)建會(huì)比較耗時(shí)。而重用 cell 就可以減少 CPU 的工作量,使 CPU 更快輸出位圖。
預(yù)排版
向服務(wù)器請(qǐng)求數(shù)據(jù)成功后,預(yù)先計(jì)算 cell 中子視圖的布局信息和 cell 的高度,并緩存下來(lái),然后再刷新 tableview。這樣,tableview 在滾動(dòng)過(guò)程中顯示 cell 時(shí),就不用再進(jìn)行布局計(jì)算了。這種方式減少了 CPU 在 tableview 滾動(dòng)過(guò)程中的工作量,讓 CPU 能夠更快地輸出位圖。但是,這樣做延遲了用戶看到新數(shù)據(jù)的時(shí)間。
預(yù)渲染
如果 cell 中有圖層啟用了masksToBounds、shadow、mask屬性,GPU 會(huì)觸發(fā)離屏渲染,而離屏渲染會(huì)增加 GPU 的工作量。當(dāng)需要 GPU 進(jìn)行離屏渲染的圖層較多時(shí),GPU 就會(huì)滿負(fù)荷運(yùn)轉(zhuǎn),導(dǎo)致不能及時(shí)輸出渲染結(jié)果??梢允褂秘惾麪柷€(UIBezierPath)來(lái)設(shè)置圓角和陰影,將圓角和陰影的繪制轉(zhuǎn)移到 CPU 中,從而減輕 GPU 的壓力(相對(duì)于 CPU 而言,GPU 繪制圓角和陰影會(huì)更加耗時(shí))。
減少視圖層級(jí)
由 CPU 輸出的多個(gè)位圖最終會(huì)被 GPU 合成為一個(gè),視圖層級(jí)越復(fù)雜,GPU 紋理合成所耗費(fèi)的時(shí)間就越長(zhǎng)。
盡量避免設(shè)置視圖透明
如果視圖不透明,GPU 在進(jìn)行紋理合成的時(shí)候,可以將其像素值直接覆蓋到父視圖上。而如果視圖包含透明度的話,GPU 必須重新計(jì)算兩個(gè)視圖重疊區(qū)域的像素值,這會(huì)增加 GPU 的工作量,所有要盡量避免設(shè)置視圖透明。
異步解碼圖片并緩存解碼結(jié)果
PNG 和 JPEG 是壓縮之后的位圖圖像格式,只有先對(duì) PNG 和 JPEG 圖片數(shù)據(jù)進(jìn)行解壓縮而得到其原始像素點(diǎn)數(shù)據(jù)后,GPU 才能合成和渲染。
UIKit 默認(rèn)是在主線程串行執(zhí)行圖片的解碼操作的,而圖片的解碼又比較耗時(shí),所以我們可以將多個(gè)圖片的解碼操作移到子線程去并行執(zhí)行,這樣也能讓 CPU 更快輸出位圖。而將解碼后的結(jié)果緩存起來(lái),在下一次顯示圖片時(shí),就可以直接使用緩存而不用再次解碼了。
異步繪制
UIKit 默認(rèn)是在主線程串行執(zhí)行文本繪制和圖形繪制的,將多個(gè)文本繪制和圖形繪制移到子線程去并發(fā)執(zhí)行,也能夠讓 CPU 更快輸出位圖。但這需要我們自行實(shí)現(xiàn)視圖的繪制,也可以使用第三方庫(kù)Texture(AsyncDisplayKit)來(lái)實(shí)現(xiàn)異步繪制。
UIView的繪制過(guò)程

Core Animation 在主線程的 Runloop 中注冊(cè)了一個(gè) Observer 來(lái)監(jiān)聽(tīng) Runloop 狀態(tài)的變化。
觸摸事件喚醒主線程的 Runloop 后,Runloop 會(huì)執(zhí)行一些操作,比如視圖的外觀調(diào)整和視圖的層級(jí)調(diào)整,每個(gè)這樣的操作都會(huì)觸發(fā)view的setNeedsDisplay方法。view的setNeedsDisplay方法不會(huì)立刻就繪制內(nèi)容,它只是調(diào)用其layer的setNeedsDisplay方法來(lái)標(biāo)記這個(gè)視圖需要重新繪制。
在主線程的 Runloop 進(jìn)入休眠狀態(tài)或者退出之前,會(huì)發(fā)送一個(gè)通知給 Core Animation。Core Animation 接收到通知后,會(huì)調(diào)用layer的display方法來(lái)繪制視圖。
在display方法的內(nèi)部實(shí)現(xiàn)中,首先會(huì)判斷layer的delegate(持有layer的view就是layer的delegate)有沒(méi)有實(shí)現(xiàn)displayLayer:代理方法。如果有實(shí)現(xiàn),就會(huì)進(jìn)入到自定義繪制流程中去。如果沒(méi)有實(shí)現(xiàn),就會(huì)進(jìn)入到系統(tǒng)繪制流程;當(dāng)繪制完成后,Core Animation 會(huì)將位圖提交給 GPU 去處理。
系統(tǒng)繪制流程

如果layer的delegate(持有layer的view就是layer的delegate)不為nil,則會(huì)調(diào)用view的drawLayer:inContext:方法來(lái)繪制內(nèi)容。在drawLayer:inContext:方法的內(nèi)部實(shí)現(xiàn)中,還會(huì)調(diào)用view的drawRect:方法來(lái)繪制自定義內(nèi)容;如果layer的delegate為nil,則會(huì)調(diào)用layer的drawInContext:方法來(lái)繪制內(nèi)容。
異步繪制原理
異步繪制的原理就是實(shí)現(xiàn)UIView的dispayLayer:代理方法來(lái)自定義繪制流程。在dispayLayer:方法實(shí)現(xiàn)中,在子線程將需要顯示的內(nèi)容繪制到圖形上下文中,然后根據(jù)這個(gè)圖形上下文創(chuàng)建一個(gè)位圖,最后在主線程將這個(gè)位圖賦值給layer的contents屬性。
如何監(jiān)控界面滑動(dòng)卡頓?
監(jiān)聽(tīng)主線程的 runloop 的狀態(tài)變化,當(dāng) runloop 處于BeforeSources(非基于端口的輸入源即將觸發(fā))或者AfterWaiting(線程剛被喚醒)狀態(tài)時(shí),就發(fā)出一個(gè)信號(hào)量。同時(shí),在子線程運(yùn)行一個(gè)While循環(huán)來(lái)不斷等待這個(gè)信號(hào)量。如果連續(xù)幾次等待信號(hào)量超時(shí),則可以判定界面滑動(dòng)時(shí)產(chǎn)生了卡頓。
如何計(jì)算 FPS
將CADisplayLink添加到主線程的 runloop 中,并與 common 模式綁定。使用某個(gè)時(shí)間點(diǎn)到當(dāng)前時(shí)間內(nèi)CADisplayLink觸發(fā)的總次數(shù)除以某個(gè)時(shí)間點(diǎn)到當(dāng)前時(shí)間的時(shí)長(zhǎng),就可以計(jì)算出 FPS 了。
正常情況下,主線程的 runloop 每隔 16.7ms 就會(huì)觸發(fā)一次
CADisplayLink回調(diào)。如果主線程執(zhí)行了一個(gè)耗時(shí) 40ms 的任務(wù),那么 runloop 就會(huì)少觸發(fā) 2 次CADisplayLink回調(diào),而此時(shí)屏幕也會(huì)少更新了 2 幀畫(huà)面。
網(wǎng)絡(luò)七層協(xié)議

OSI 七層模型由上至下分別為應(yīng)用層、表示層、會(huì)話層、傳輸層、網(wǎng)絡(luò)層、數(shù)據(jù)鏈路層、物理層。
URL是什么?
統(tǒng)一資源定位符。通過(guò)一個(gè) URL,能找到互聯(lián)網(wǎng)上唯一的一個(gè)資源。
URL 的基本格式 = 協(xié)議://主機(jī)地址/路徑。
- 協(xié)議:不同的協(xié)議代表著不同的資源查找方式、資源傳輸方式。
- 主機(jī)地址:存放資源的主機(jī)(服務(wù)器)的 IP 地址(域名)。
- 路徑:資源在主機(jī)(服務(wù)器)中的具體位置。
URL 中常見(jiàn)的協(xié)議有以下幾種:
- HTTP:超文本傳輸協(xié)議,訪問(wèn)的是遠(yuǎn)程的網(wǎng)絡(luò)資源。
- file:訪問(wèn)的是本地計(jì)算機(jī)上的資源。
- mailto:訪問(wèn)的是電子郵件地址。
- FTP:訪問(wèn)的共享主機(jī)的文件資源。
HTTP
超文本傳輸協(xié)議,是一個(gè)應(yīng)用層協(xié)議。
請(qǐng)求報(bào)文的內(nèi)容
- 請(qǐng)求行:包含了請(qǐng)求方法、請(qǐng)求資源路徑、HTTP 協(xié)議版本。(
GET,/Server/resources/images/1.jpg,HTTP/1.1。) - 請(qǐng)求頭:包含了客戶端想訪問(wèn)的服務(wù)器主機(jī)地址以及客戶端的類(lèi)型、軟件環(huán)境、語(yǔ)言環(huán)境、所能接收的數(shù)據(jù)類(lèi)型和支持?jǐn)?shù)據(jù)壓縮格式信息。(客戶端想訪問(wèn)的服務(wù)器主機(jī)地址:
Host : 192.168.1.105:8080,客戶端的類(lèi)型和軟件環(huán)境:User-Agent : Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9) Firefox/30.0,客戶端所能接收的數(shù)據(jù)類(lèi)型:Accept : text/html, */*,客戶端的語(yǔ)言環(huán)境:Accept-Language : zh-cn,客戶端支持的數(shù)據(jù)壓縮格式:Accept-Encoding : gzip。) - 請(qǐng)求體:客戶端發(fā)送給服務(wù)器的具體數(shù)據(jù)。
響應(yīng)報(bào)文的內(nèi)容
- 狀態(tài)行:包含了 HTTP 協(xié)議版本、狀態(tài)碼、狀態(tài)英文名稱(chēng)。(
HTTP/1.1,200,OK。) - 響應(yīng)頭:包含了對(duì)服務(wù)器的描述、對(duì)返回?cái)?shù)據(jù)的描述。(服務(wù)器的類(lèi)型:
Server: Apache-Coyote/1.1,返回?cái)?shù)據(jù)的類(lèi)型:Content-Type: image/jpeg,返回?cái)?shù)據(jù)的長(zhǎng)度:Content-Length: 56811,響應(yīng)的時(shí)間:Date: Mon, 23 Jun 2014 12:54:52 GMT。) - 實(shí)體內(nèi)容:服務(wù)器返回給客戶端的具體數(shù)據(jù)。
HTTP定義的請(qǐng)求方法
-
GET:獲取資源,不會(huì)對(duì)服務(wù)器中存儲(chǔ)的資源進(jìn)行修改; -
POST:獲取資源,但是可能會(huì)對(duì)服務(wù)器中存儲(chǔ)的資源進(jìn)行修改; -
PUT:上傳文件到服務(wù)器; -
DELETE:刪除服務(wù)器中的指定文件; -
HEAD:和GET方法一樣,但是服務(wù)器返回的不是資源,而是資源的頭信息 -
OPTIONS:獲取服務(wù)器支持的請(qǐng)求方法。
HTTP的優(yōu)點(diǎn)和缺點(diǎn)
- HTTP 協(xié)議比較簡(jiǎn)單,程序規(guī)模小,因而通信速度很快。
- 允許傳輸任意類(lèi)型的數(shù)據(jù),非常靈活。
- HTTP 協(xié)議是一種無(wú)狀態(tài)協(xié)議。無(wú)狀態(tài)是指協(xié)議對(duì)于事物處理沒(méi)有記憶能力,服務(wù)器不知道客戶端是什么狀態(tài)。缺少狀態(tài)意味著如果后續(xù)處理需要前面的信息,則它必須重傳,這樣可能導(dǎo)致每次連接傳送的數(shù)據(jù)量增大。
- 明文傳輸數(shù)據(jù),缺乏安全性。
HTTP協(xié)議無(wú)狀態(tài)的解決方案
- Cookie:Cookie 是一種在客戶端保存狀態(tài)信息的機(jī)制??头税l(fā)送請(qǐng)求到服務(wù)器時(shí),服務(wù)器會(huì)生成一個(gè)標(biāo)識(shí)客戶端的 Cookie,并將其與數(shù)據(jù)一起返回給客戶端,由客戶端保存在本地??蛻舳嗽俅伟l(fā)送請(qǐng)求時(shí),會(huì)將該 Cookie 發(fā)送給服務(wù)器,服務(wù)器通過(guò) Cookie 來(lái)確認(rèn)客戶端的狀態(tài)信息。如果 Cookie 不設(shè)定過(guò)期時(shí)間的話,關(guān)閉瀏覽器時(shí),Cookie 就失效了。如果設(shè)置了 Cookie 的過(guò)期時(shí)間,那么瀏覽器會(huì)把 Cookie 保存到硬盤(pán)中,再次打開(kāi)瀏覽器時(shí),會(huì)依然有效,直到超過(guò)設(shè)置的有效期。Cookie 機(jī)制將數(shù)據(jù)保存在客戶端,缺乏安全性,且數(shù)據(jù)大小有限制。
- Session:Session 是一種在服務(wù)器端保存狀態(tài)信息的機(jī)制??蛻舳讼蚍?wù)器發(fā)送請(qǐng)求時(shí),服務(wù)器會(huì)生成一個(gè) Session 并保存,同時(shí)返回一個(gè) Session id 給客戶端??蛻舳嗽诤罄m(xù)的請(qǐng)求中將 Session id 發(fā)給服務(wù)器,服務(wù)器通過(guò)該 Session id 來(lái)確認(rèn)客戶端的狀態(tài)信息。服務(wù)器會(huì)把長(zhǎng)時(shí)間沒(méi)有活動(dòng)的 Session 從服務(wù)器內(nèi)存中清除,此時(shí) Session 便失效了。Session 機(jī)制將數(shù)據(jù)保存在服務(wù)器,會(huì)占用服務(wù)器的性能。
常見(jiàn)的響應(yīng)狀態(tài)碼及其含義
狀態(tài)碼的類(lèi)別:
- 1XX:信息性狀態(tài)碼,請(qǐng)求正在處理;
- 2XX:成功性狀態(tài)碼,請(qǐng)求正常處理完畢;
- 3XX:重定向狀態(tài)碼,需要進(jìn)行附加的操作以完成請(qǐng)求;
- 4XX:客戶端錯(cuò)誤狀態(tài)碼,服務(wù)器無(wú)法處理請(qǐng)求;
- 5XX:服務(wù)器錯(cuò)誤狀態(tài)碼,服務(wù)器處理請(qǐng)求出錯(cuò)。
常見(jiàn)的狀態(tài)碼:
- 200:請(qǐng)求成功。
- 301:請(qǐng)求的資源的位置已被更改,并且是永久性的更改。
- 400:客戶端請(qǐng)求的語(yǔ)法錯(cuò)誤,服務(wù)器無(wú)法解析。
- 404:服務(wù)器無(wú)法找到客戶端請(qǐng)求的資源。
- 500:服務(wù)器內(nèi)部錯(cuò)誤,無(wú)法完成請(qǐng)求。
GET請(qǐng)求和POST請(qǐng)求的區(qū)別
- GET 請(qǐng)求的參數(shù)以
?分割拼接到 URL 上,POST 請(qǐng)求的參數(shù)是放在 HTTP Body 中; - GET 請(qǐng)求傳遞的參數(shù)是有限制的,通常不能超過(guò) 1KB,而 POST 請(qǐng)求一般是沒(méi)有限制的;
- GET 請(qǐng)求只是獲取數(shù)據(jù),不會(huì)引起服務(wù)器狀態(tài)變化。而 POST 請(qǐng)求是提交數(shù)據(jù),可能會(huì)引起服務(wù)器的狀態(tài)變化;
- 同一個(gè) GET 請(qǐng)求執(zhí)行多次和執(zhí)行一次的效果完全相同,而同一個(gè) POST 請(qǐng)求多次執(zhí)行的結(jié)果可能不是完全相同的;
- GET 請(qǐng)求可以緩存,而 POST 請(qǐng)求不能緩存。
POST請(qǐng)求的 body 使用 form-urlencoded 和使用 multipart/from-data 有什么區(qū)別?
發(fā)送純文本數(shù)據(jù)時(shí),使用form-urlencoded格式對(duì)數(shù)據(jù)進(jìn)行編碼。發(fā)送的數(shù)據(jù)包含圖片、音頻或其他二進(jìn)制數(shù)據(jù)時(shí),使用multipart/from-data格式對(duì)數(shù)據(jù)進(jìn)行編碼。
HTTPS 協(xié)議
HTTPS 協(xié)議,安全套接字層超文本傳輸協(xié)議。為了數(shù)據(jù)傳輸?shù)陌踩?,HTTPS 協(xié)議在 HTTP 協(xié)議的基礎(chǔ)上加入了 SSL/TLS 協(xié)議, SSL/TLS 協(xié)議依靠證書(shū)來(lái)驗(yàn)證服務(wù)器的身份,并為客戶端和服務(wù)器之間的通信加密。
HTTPS連接的建立流程
單向認(rèn)證,客戶端和服務(wù)器都要存放向 CA 申請(qǐng)的服務(wù)器證書(shū),其流程如下:
- 客戶端發(fā)送
TLS協(xié)議版本號(hào)、客戶端支持的加密算法以及隨機(jī)數(shù)C到服務(wù)端;
- 客戶端發(fā)送
- 服務(wù)器收到客戶端所支持的加密算法之后,會(huì)和自己支持的加密算法進(jìn)行對(duì)比。如果不符合,則斷開(kāi)連接。否則,把
確認(rèn)使用的加密算法、服務(wù)器證書(shū)和隨機(jī)數(shù)S發(fā)送給客戶端;
- 服務(wù)器收到客戶端所支持的加密算法之后,會(huì)和自己支持的加密算法進(jìn)行對(duì)比。如果不符合,則斷開(kāi)連接。否則,把
- 客戶端收到服務(wù)器證書(shū)后,會(huì)將其與本地存放的服務(wù)器證書(shū)進(jìn)行對(duì)比。如果驗(yàn)證失敗,則斷開(kāi)連接;如果驗(yàn)證成功,那么客戶端會(huì)使用服務(wù)器證書(shū)中的
公鑰生成一個(gè)前主密鑰,并使用前主密鑰、隨機(jī)數(shù)C和隨機(jī)數(shù)S生成一個(gè)會(huì)話密鑰,然后客戶端使用服務(wù)器證書(shū)中的公鑰對(duì)前主密鑰加密并發(fā)送給服務(wù)器;
- 客戶端收到服務(wù)器證書(shū)后,會(huì)將其與本地存放的服務(wù)器證書(shū)進(jìn)行對(duì)比。如果驗(yàn)證失敗,則斷開(kāi)連接;如果驗(yàn)證成功,那么客戶端會(huì)使用服務(wù)器證書(shū)中的
- 服務(wù)器接收到加密的
前主密鑰后,使用服務(wù)器證書(shū)的私鑰解密得到前主密鑰,然后使用前主密鑰、隨機(jī)數(shù)S和隨機(jī)數(shù)C生成本次會(huì)話所用的會(huì)話密鑰,握手結(jié)束。
- 服務(wù)器接收到加密的
- 客戶端和服務(wù)器開(kāi)始進(jìn)行通信,通信內(nèi)容使用
會(huì)話密鑰加密。
- 客戶端和服務(wù)器開(kāi)始進(jìn)行通信,通信內(nèi)容使用
雙向認(rèn)證,客戶端和服務(wù)器除了要存放服務(wù)器證書(shū)之外,服務(wù)器還要存放一個(gè) CA 根證書(shū),客戶端還要存放一個(gè)由 CA 根證書(shū)簽名的 p12 證書(shū)。在客服端驗(yàn)證服務(wù)器成功后,客戶端還會(huì)發(fā)送 p12 證書(shū)和一段由 p12 證書(shū)簽名的數(shù)據(jù)到服務(wù)器,服務(wù)器會(huì)使用根證書(shū)對(duì) p12 證書(shū)和由 p12 證書(shū)簽名的數(shù)據(jù)進(jìn)行驗(yàn)證。驗(yàn)證成功,就會(huì)繼續(xù)后面的流程;驗(yàn)證失敗,則會(huì)斷開(kāi)連接。
HTTPS 和 HTTP 的區(qū)別
- HTTPS協(xié)議是由 HTTP+SSL/TLS 協(xié)議構(gòu)建的可進(jìn)行加密傳輸、身份認(rèn)證的網(wǎng)絡(luò)協(xié)議,比 HTTP 協(xié)議安全。
- HTTP 和 HTTPS 使用的是完全不同的連接方式,用的端口也不一樣,前者是80,后者是443。
TCP
TCP 協(xié)議的全稱(chēng)是傳輸控制協(xié)議,它是一種面向連接的、可靠的、基于字節(jié)流的傳輸層協(xié)議。其主要解決數(shù)據(jù)如何在網(wǎng)絡(luò)中傳輸,而應(yīng)用層的 HTTP 協(xié)議主要解決如何包裝數(shù)據(jù)。
三次握手
建立起一個(gè) TCP 連接需要經(jīng)過(guò)三次握手:
- 第一次握手:客戶端發(fā)送
連接請(qǐng)求到服務(wù)器,等待服務(wù)器確認(rèn);
- 第一次握手:客戶端發(fā)送
- 第二次握手:服務(wù)器收到客戶端的
連接請(qǐng)求后,向客戶端發(fā)送一個(gè)確認(rèn)消息,并等待客戶端確認(rèn);
- 第二次握手:服務(wù)器收到客戶端的
- 第三次握手:客戶端收到服務(wù)器的
確認(rèn)消息后,也會(huì)向服務(wù)器發(fā)送一個(gè)確認(rèn)消息。
- 第三次握手:客戶端收到服務(wù)器的
三次握手完畢后,正式開(kāi)始在客戶端和服務(wù)器之間傳送數(shù)據(jù)。理想狀態(tài)下,TCP 連接一旦建立,在通信雙方中的任何一方主動(dòng)關(guān)閉連接之前,TCP 連接將被一直保持下去。
服務(wù)器向客戶端發(fā)送確認(rèn)消息后,還需要等待客戶端的確認(rèn)。這是因?yàn)槿绻蛻舳税l(fā)送給服務(wù)器的連接請(qǐng)求在超出等待時(shí)間后,客戶端還未收到服務(wù)器的確認(rèn),客戶端會(huì)將這個(gè)連接請(qǐng)求標(biāo)記為已失效,然后客戶端再次發(fā)送連接請(qǐng)求到服務(wù)器,并成功建立 TCP 連接。此時(shí),之前失效的連接請(qǐng)求突然又傳送到了服務(wù)端,如果沒(méi)有客戶端的確認(rèn)的話,服務(wù)端會(huì)又建立一個(gè) TCP 連接。
四次揮手
需要斷開(kāi) TCP 連接時(shí),服務(wù)器和客戶端均可以主動(dòng)發(fā)起斷開(kāi) TCP 連接的請(qǐng)求,斷開(kāi)過(guò)程需要經(jīng)過(guò)四次揮手:
- 第一次揮手:客戶端發(fā)送
斷開(kāi)請(qǐng)求到服務(wù)器,等待服務(wù)器確認(rèn);
- 第一次揮手:客戶端發(fā)送
- 第二次揮手:服務(wù)器收到客戶端的
斷開(kāi)請(qǐng)求后,會(huì)發(fā)送確認(rèn)消息給客戶端;
- 第二次揮手:服務(wù)器收到客戶端的
- 第三次揮手:服務(wù)器沒(méi)有數(shù)據(jù)需要發(fā)送到客戶端后,服務(wù)器會(huì)發(fā)送
斷開(kāi)請(qǐng)求給客戶端,并等待客戶端確認(rèn);
- 第三次揮手:服務(wù)器沒(méi)有數(shù)據(jù)需要發(fā)送到客戶端后,服務(wù)器會(huì)發(fā)送
- 第四次揮手:客戶端收到服務(wù)器的
斷開(kāi)請(qǐng)求后,發(fā)送確認(rèn)消息給服務(wù)器。
- 第四次揮手:客戶端收到服務(wù)器的
四次揮手完畢后,服務(wù)器和客戶端之間就都斷開(kāi)了 TCP 連接。
斷開(kāi) TCP 連接需要四次揮手而不是三次是因?yàn)殛P(guān)閉連接時(shí),當(dāng)接收方收到對(duì)方的斷開(kāi)請(qǐng)求后,僅僅表示對(duì)方?jīng)]有數(shù)據(jù)需要發(fā)送了。但是接收方可能還有數(shù)據(jù)需要發(fā)送給對(duì)方,所以不會(huì)馬上斷開(kāi) TCP 連接。
可靠數(shù)據(jù)傳輸
TCP 連接通過(guò)序號(hào)和確認(rèn)應(yīng)答來(lái)保證數(shù)據(jù)傳輸?shù)目煽啃浴?/p>
當(dāng)發(fā)送端發(fā)送數(shù)據(jù)之后需要等待接收端的確認(rèn),如果收到接收端的確認(rèn)應(yīng)答,表示數(shù)據(jù)成功發(fā)送到接收端;如果在一定時(shí)間內(nèi)沒(méi)有收到接收端的確認(rèn)應(yīng)答,則認(rèn)為數(shù)據(jù)已丟失,需要重新發(fā)送。
沒(méi)有收到接收端的確認(rèn)應(yīng)答的話,分兩種情況,一種是接收端沒(méi)收到數(shù)據(jù),另一種是接收端收到了數(shù)據(jù)但是它的確認(rèn)應(yīng)答丟失了。如果是接收端的確認(rèn)應(yīng)答丟失了,那么發(fā)送端會(huì)重新發(fā)送數(shù)據(jù),接收端就會(huì)重復(fù)接收相同的數(shù)據(jù)。為了解決接收端重復(fù)接收相同數(shù)據(jù)的問(wèn)題,可以通過(guò)為發(fā)送的數(shù)據(jù)標(biāo)上序號(hào),接收端收到數(shù)據(jù)后,根據(jù)本次接收數(shù)據(jù)的序號(hào),將下一次應(yīng)該接收的序號(hào)作為應(yīng)答返回給發(fā)送端。如果接收端接收的數(shù)據(jù)的序號(hào)是重復(fù)的,則會(huì)丟棄接收的數(shù)據(jù)。
流量控制(滑動(dòng)窗口)
TCP 連接的雙方各自為該 TCP 連接分配一個(gè)發(fā)送緩存和一個(gè)接收緩存。當(dāng)接收到數(shù)據(jù)后,會(huì)將數(shù)據(jù)存放到接收緩存中。上層的應(yīng)用進(jìn)程會(huì)從接收緩存中讀取數(shù)據(jù),但不是數(shù)據(jù)一到達(dá)接收緩存就立刻讀取,因?yàn)榇藭r(shí)上層的應(yīng)用進(jìn)程可能正在處理其他事務(wù)。如果接收方的應(yīng)用層讀取數(shù)據(jù)較慢,而發(fā)送方發(fā)送數(shù)據(jù)太多太快,那么接收方的接收緩存很可能會(huì)溢出。所以,TCP 為應(yīng)用程序提供了流量控制服務(wù),以避免出現(xiàn)緩存溢出的情況。
TCP 連接的雙方各自維護(hù)著一個(gè)發(fā)送窗口和一個(gè)接收窗口來(lái)提供流量控制,發(fā)送窗口的大小是由對(duì)方的接收窗口來(lái)決定的,接收窗口用于指示發(fā)送方該接收方的接收緩存還有多少可用空間,發(fā)送窗口決定了發(fā)送方還能發(fā)送多少數(shù)據(jù)給接收方。當(dāng)接收緩存的可用空間為0時(shí),發(fā)送端會(huì)停止發(fā)送數(shù)據(jù)。
擁塞控制
計(jì)算機(jī)網(wǎng)絡(luò)中的帶寬,交換結(jié)點(diǎn)中的緩存和處理機(jī),都是網(wǎng)絡(luò)的資源。在某段時(shí)間內(nèi),如果對(duì)網(wǎng)絡(luò)中某一資源的需求超過(guò)了該資源所能提供的可用部分,網(wǎng)絡(luò)的性能就會(huì)變壞,這種情況叫做網(wǎng)絡(luò)擁塞。
如果出現(xiàn)擁塞而不進(jìn)行控制,整個(gè)網(wǎng)絡(luò)的吞吐量將會(huì)隨輸入負(fù)荷的增大而下降。為了解決這個(gè)問(wèn)題,TCP 提供了擁塞控制機(jī)制,以便讓連接的雙方根據(jù)所感知到的網(wǎng)絡(luò)擁塞程度來(lái)限制其向?qū)Ψ桨l(fā)送流量的速率。
TCP 連接的雙方各自維護(hù)有一個(gè)擁塞窗口,擁塞窗口決定了發(fā)送方能向網(wǎng)絡(luò)中發(fā)送流量的速率,擁塞窗口的大小取決于網(wǎng)絡(luò)擁塞的程度。
TCP 發(fā)送方如何感知到發(fā)生了網(wǎng)絡(luò)擁塞?
當(dāng)接收端收到失序報(bào)文段(即該報(bào)文段的序號(hào)大于期望的按序報(bào)文段的序號(hào))時(shí),接收端不會(huì)對(duì)該失序報(bào)文段進(jìn)行確認(rèn)。由于 TCP 不使用否定確認(rèn),為了讓發(fā)送方得知這一現(xiàn)象,會(huì)對(duì)上一個(gè)按序報(bào)文段進(jìn)行重復(fù)確認(rèn),這樣就會(huì)產(chǎn)生一個(gè)冗余ACK。
因?yàn)榘l(fā)送方經(jīng)常發(fā)送大量的報(bào)文段,如果其中一個(gè)報(bào)文段丟失,那么可能在定時(shí)器過(guò)期之前,發(fā)送方就會(huì)收到大量的冗余ACK。一旦收到3個(gè)冗余ACK,就說(shuō)明已被確認(rèn)3次的報(bào)文段之后的報(bào)文段已經(jīng)丟失。這時(shí),TCP 會(huì)執(zhí)行快速重傳(即在該報(bào)文段的定時(shí)器過(guò)期之前重傳該報(bào)文段)。
當(dāng)出現(xiàn)網(wǎng)絡(luò)擁塞時(shí),路由器的緩存會(huì)溢出,從而導(dǎo)致數(shù)據(jù)報(bào)被丟棄,這會(huì)引發(fā) TCP 連接的丟包。所以,當(dāng) TCP 連接出現(xiàn)丟包時(shí),發(fā)送方就可以確定出現(xiàn)了網(wǎng)絡(luò)擁塞。
TCP如何限制發(fā)送方發(fā)送流量的速率?
當(dāng)出現(xiàn)丟包事件時(shí),降低發(fā)送方發(fā)送流量的速率;當(dāng)接收到非冗余ACK時(shí),就增大發(fā)送方發(fā)送流量的速率。
TCP 擁塞控制算法
TCP 擁塞控制算法包括以下三個(gè)主要部分:
-
慢啟動(dòng):每收到1個(gè)
非冗余ACK,擁塞窗口的大小會(huì)指數(shù)級(jí)增加。如果收到了1個(gè)冗余ACK,會(huì)重置擁塞窗口的大小,并重啟慢啟動(dòng)。當(dāng)連續(xù)收到3個(gè)冗余ACK時(shí),會(huì)進(jìn)入快速恢復(fù)狀態(tài); -
擁塞避免:當(dāng)擁塞窗口的大小超過(guò)慢啟動(dòng)閥值時(shí),每收到一個(gè)
非冗余ACK,會(huì)加法增大擁塞窗口的大小。如果收到了1個(gè)冗余ACK,會(huì)乘法減小擁塞窗口的大小。當(dāng)連續(xù)收到3個(gè)冗余ACK時(shí),會(huì)進(jìn)入快速恢復(fù)狀態(tài); -
快速恢復(fù):當(dāng)收到1個(gè)
非冗余ACK時(shí),會(huì)進(jìn)入擁塞避免狀態(tài)。
Socket
Socket(套接字)是網(wǎng)絡(luò)通信過(guò)程中端點(diǎn)的抽象表示,包含進(jìn)行網(wǎng)絡(luò)通信必須的五種信息:連接使用的協(xié)議、本地主機(jī)的 IP 地址、本地進(jìn)程的協(xié)議端口、遠(yuǎn)程主機(jī)的 IP 地址、遠(yuǎn)程進(jìn)程的協(xié)議端口。
應(yīng)用層通過(guò)傳輸層進(jìn)行數(shù)據(jù)通信時(shí),TCP 連接會(huì)遇到同時(shí)為多個(gè)應(yīng)用程序進(jìn)程提供并發(fā)服務(wù)的問(wèn)題。多個(gè) TCP 連接或多個(gè)應(yīng)用程序進(jìn)程可能需要通過(guò)同一個(gè) TCP 協(xié)議端口傳輸數(shù)據(jù)。為了區(qū)別不同的應(yīng)用程序進(jìn)程和連接,計(jì)算機(jī)操作系統(tǒng)為應(yīng)用程序與 TCP/IP 協(xié)議交互提供了套接字接口。通過(guò)套接字接口區(qū)分來(lái)自不同應(yīng)用程序進(jìn)程或網(wǎng)絡(luò)連接的通信,實(shí)現(xiàn)數(shù)據(jù)傳輸?shù)牟l(fā)服務(wù)。
建立 Socket 連接至少需要一對(duì)套接字,其中一個(gè)運(yùn)行于客戶端,另一個(gè)運(yùn)行于服務(wù)器。建立 Socket 連接時(shí),可以指定使用的傳輸層協(xié)議(TCP 或 UDP)。當(dāng)使用 TCP 協(xié)議進(jìn)行連接時(shí),該 Socket 連接就是一個(gè) TCP 連接。
UDP
UDP 協(xié)議的全稱(chēng)是用戶數(shù)據(jù)報(bào)協(xié)議,它是一種傳輸層協(xié)議。
使用 UDP 協(xié)議傳輸數(shù)據(jù)時(shí),服務(wù)器在發(fā)出數(shù)據(jù)報(bào)文之后,不會(huì)確認(rèn)對(duì)方是否已接收到數(shù)據(jù),所以不需要在客戶端和服務(wù)器之間建立連接。因此,UDP 協(xié)議是不可靠的。
UDP 協(xié)議發(fā)送的每個(gè)數(shù)據(jù)報(bào)文的大小限制在64KB之內(nèi),所以其傳輸速度非常快。其應(yīng)用場(chǎng)景包括多媒體教室、網(wǎng)絡(luò)視頻會(huì)議系統(tǒng)等。
JSON和XML兩種數(shù)據(jù)結(jié)構(gòu)的區(qū)別,JSON解析和XML解析的底層原理。
- XML 的可讀性和對(duì)數(shù)據(jù)的描述性比 JSON 要好;
- JSON 的編碼和解碼更簡(jiǎn)單,因而數(shù)據(jù)體積小,傳輸速度更快。
JSON解析的底層原理
遍歷文本中的字符,并根據(jù){}、[]、,和:進(jìn)行區(qū)分。{}代表字典,[]代表數(shù)組,,是字典的鍵值對(duì)以及數(shù)組元素的分隔符,:是鍵值對(duì)的 key 和 value 的分隔符。最終結(jié)果是將 JSON 文本轉(zhuǎn)換為一個(gè)字典或者數(shù)組。
XML解析的底層原理
- DOM 解析:根據(jù)節(jié)點(diǎn)將 XML 文本轉(zhuǎn)換為一個(gè)包含其所有內(nèi)容的樹(shù),并對(duì)樹(shù)進(jìn)行遍歷。使用 DOM 解析時(shí),需要處理整個(gè) XML 文本。
- SAX 解析:遍歷 XML 文本,當(dāng)發(fā)現(xiàn)給定的節(jié)點(diǎn)時(shí),會(huì)觸發(fā)回調(diào)來(lái)告知指定的標(biāo)簽已經(jīng)找到。當(dāng)只需要處理文本中所包含的部分?jǐn)?shù)據(jù)時(shí),使用 SAX 解析可以在找到指定的標(biāo)簽后就停止解析。