開發小知識(一)
開發小知識(二)
前言和目錄
該文章主要整理一些小知識點,主要涉及 iOS 以及計算基礎相關知識點,某些知識點暫時只有標題,后續會持續更新。筆者最近一段時間面試過程中發現一些普遍現象,對于一些很不起眼的問題,很多開發者都只停留在知道、聽說過的層面,但是一旦問 是什么 和 為什么 ,很多應試者回答的并不理想,比如下面的幾個問題:
- 數組的下標為什么從零開始?
- 經常聽到深拷貝和淺拷貝,為什么會存在深拷貝和淺拷貝這一對概念?
- block 和 函數指針有什么區別?
- 引用的本質是什么?引用和指針有什么關系?
- UI 性能優化的時候,很多面試者會提到用
CALayer
代替視圖組件,如果某天產品改需求,要求添加觸發事件,那么CALayer
上怎么添加觸發事件? - 和 H5 交互的時候,經常會用到
userAgent
, 請問userAgent
是什么?(問過幾次,純 iOS 開發者沒幾人知道只說有印象) - 標準的 MVC 架構模式中,
View
和Model
是完全獨立開來的,很多開發者都說自己使用的是 MVC 模式,當問起:為什么實際開發中自定義視圖組件時通常都會引入 Model ,并重寫setModel
方法?這還是不是 MVC ? - 面試過程中筆者偶爾會問多線程的相關問題,印象中有兩位應試者脫口而出 自旋鎖 ,當問及什么是 互斥鎖 ?什么是 自旋鎖 ?應試者一臉懵,明明是自己給自己挖坑。此外還會問到:為什么線程會不安全?也沒幾個應試者能完整回答出。
- 很多應試者都知道,http 和 https 的區別在于多了 SSL 層,但是 SSL 層里面有什么,做了什么,位于網絡模型什么位置?
- 很多人都知道內存(堆內存)回收,但是內存(堆內存)回收后發生了什么?是把內存從堆空間清空了嗎?還是重置為 0 ?還是說做了其他什么操作?
- MD5 安全嗎?如果不安全,有什么替代的方案?MD5算是加密算法的一種嗎?如果不是,和加密算法有什么區別?
- pods 經常用吧,pods 命令后面的參數--verbose 和 --no-repo-update 是什么意思?
- 令筆者比較驚訝的是,響應鏈流程算是 iOS 入門基礎知識。筆者問了一道相關問題百分之七八十的面試者都很難回答上來。A 為父視圖,依次執行
[A addSubView:B]
、[A addSubView:C]
、C.userInteractionEnabled = NO
,其中 B 視圖和 C 視圖有重疊,請問:B 視圖添加點擊事件能否響應?多數應試者第一反應是不能,結合響應鏈流程來看,答案顯然是錯誤的。 - super 經常用,請問 super 調用方法和 self 調用方法有什么本質區別?
以上僅是部分典型小知識點,更多內容請詳看此文。
目錄
- 一、CALayer如何添加點擊事件
- 二、為什么會存在堆空間
- 三、Tagged Pointer 是什么?
- 四、iOS平臺跨域訪問漏洞
- 五、緩存 NSDateFormatter
- 六、iOS 9 以后通知不再需要手動移除
- 七、UIImage 名稱為空的警告(符號斷點解決)
- 八、NSUserDefaults 存儲字典的一個坑
- 九、performSelector:afterDelay:的坑
- 十、 @autoreleasepool
- 十一、如何對 NSMutableArray 進行 KVO
- 十二、被忽略的UIViewController兩對API
- 十三、抗壓縮優先級
- 十四、約束優先級
- 十五、設置代碼只在 Debug 下起效
- 十六、為什么會有深拷貝和淺拷貝之分
- 十七、為什么交叉方法出現"死循環"
- 十八、為什么數組下標從零開始
- 十九、copy 修飾符引發崩潰問題
- 二十、為什么量子密碼學會有取代傳統加密方法的趨勢
- 二十一、引用計數是怎么管理的
- 二十二、weak 原理
- 二十三、加鹽的意義
- 二十四、Shell 腳本
- 二十五、什么是UserAgent
- 二十六、JS和OC通信方式匯總
- 二十七、UIScrollView 原理
- 二十八、--verbose 和 --no-repo-update
- 二十九、dataSource 和 delegate 的本質區別
- 三十、變種 MVC
- 三十一、函數指針和 Block
- 三十二、內存(堆內存)回收是什么意思
- 三十三、IP 和 MAC
- 三十四、MD5 相關小知識
- 三十五、響應鏈問題
- 三十六、什么是線程不安全?線程不安全的本質原因?
- 三十七、App 啟動流程
- 三十八、包體積優化中的內聯函數
- 三十九、super 本質
- 四十、引用的本質(引用和指針的區別)
- 四十一、渲染框架分類
- 四十二、NSProxy & NSObject
- 四十三、如何給百萬數據排序
- 四十四、自旋鎖 & 互斥鎖
- 四十五、應用 Crash 時為什么對操作系統無影響?
- 四十六、硬盤重量會隨著存儲數據大小而變化嗎?
- 四十七、如何消除小數誤差
- 四十八、運行時是否是 OC 的專利?
- 四十九、線程保活
- 五十、包體積優化總結
一、CALayer如何添加點擊事件
兩種方法: convertPoint
和hitTest:
,hitTest:
返回的順序嚴格按照圖層樹的圖層順序。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CGPoint point = [[touches anyObject] locationInView:self.view];
CGPoint redPoint = [self.redLayer convertPoint:point fromLayer:self.view.layer];
if ([self.redLayer containsPoint:redPoint]) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"point red" message:@"" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CGPoint point = [[touches anyObject] locationInView:self.view];
CALayer *layer = [self.view.layer hitTest:point];
if (layer == self.redLayer) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"point red" message:@"" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}else if (layer == self.yellowLayer){
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"point yellow" message:@"" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}
}
二、為什么會存在堆空間
堆空間的存在主要是為了延長對象的生命周期,并使得對象的生命周期可控。
- 如果試圖用棧空間取代堆空間,顯然是不可行的。棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,如果申請的空間超過棧的剩余空間時,將出現棧溢出,發生未知錯誤。因此,能從棧獲得的空間較小。而堆是向高地址擴展的數據結構,是不連續的內存區域。這是由于系統是用鏈表來存儲的空閑內存地址的。堆的大小受限于計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。 但是棧空間比堆空間響應速度更快,所以一般類似int、NSInteger等占用內存比較小的通常放在棧空間,對象一般放在堆空間。
- 如果試圖用數據區(全局區)取代堆空間,顯然也是不可行的。因為全局區的生命周期會伴隨整個應用而存在,比較消耗內存,生命周期不像在堆空間那樣可控,堆空間中可以隨時創建和銷毀。
- 代碼區就不用想了,如果能夠輕易改變代碼區,一個應用就無任何安全性可言了。
三、Tagged Pointer 是什么?
從 64bit 開始,iOS 引入了Tagged Pointer技術,用于優化NSNumber、NSDate、NSString等小對象的存儲。在沒有使用Tagged Pointer之前, NSNumber等對象需要動態分配內存、維護引用計數等,NSNumber指針存儲的是堆中NSNumber對象的地址值;使用Tagged Pointer之后,NSNumber指針里面存儲的數據變成了:Tag + Data,也就是將數據直接存儲在了指針中。當指針不夠存儲數據時,會使用動態分配內存的方式來存儲數據。
四、iOS平臺跨域訪問漏洞
UIWebView
默認開啟了WebKitAllowUniversalAccessFromFileURLs
和 WebKitAllowFileAccessFromFileURLs
屬性。利用這個漏洞給某個 App 下發一個 HTML 文件,當 UIWebView
使用 file 協議
打開這個 HTML 文件, HTML 文件中含有一段竊取用戶數據的 JS 代碼,就會導致用戶數據泄露。
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
_webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
[_webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:filePath]]];
<!DOCTYPE html>
<html>
<body>
<script>
// 這個可以是手機任意一個文件地址
var localfile = "/etc/passwd"
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
alert(xhr.responseText);
}
}
try {
xhr.open("GET", localfile, true);
xhr.send();
} catch (ex) {
alert(ex.message);
}
</script>
</body>
</html>
上面代碼可以讀取出手機端 /etc/passwd
的文件。這個漏洞訪問其他應用的數據,而不必需要用戶的許可。但WKWiebView
的 WebKitAllowUniversalAccessFromFileURLs
和 WebKitAllowFileAccessFromFileURLs
默認是關閉的(可以手動控制),不會存在這樣的風險。
補充:針對 https 請求UIWebView
需要做額外處理,借助NSURLConnection
做證書驗證,而WKWebView
無需做過多額外處理。
五、緩存 NSDateFormatter
緩存原因參考蘋果官方文檔:
Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, it is typically more efficient to cache a single instance than to create and dispose of multiple instances. One approach is to use a static variable.
六、iOS 9 以后通知不再需要手動移除
通知 NSNotification
在注冊者被回收時需要手動移除,是一直以來的使用準則。原因是在 MRC 時代,通知中心持有的是注冊者的 unsafe_unretained
指針,在注冊者被回收時若不對通知進行手動移除,則指針指向被回收的內存區域,變為野指針。此時發送通知會造成 crash 。而在 iOS 9 以后,通知中心持有的是注冊者的 weak
指針,這時即使不對通知進行手動移除,指針也會在注冊者被回收后自動置空。因為向空指針發送消息是不會有問題的。
七、UIImage 名稱為空的警告(符號斷點解決)
[UIImage imageNamed:]
傳了 nil
或者傳入@"",控制臺會輸出[framework] CUICatalog: Invalid asset name supplied: '(null)'
。通過符號斷點可定位。
八、NSUserDefaults 存儲字典的一個坑
NSDictionary *dict = @{@1: @"1",
@2: @"2",
@3: @"3",
@4: @"4"};
[[NSUserDefaults standardUserDefaults] setObject:dict forKey:@"key"];
[[NSUserDefaults standardUserDefaults] synchronize];
執行上述代碼會報如下錯誤:
[User Defaults] Attempt to set a non-property-list object {
3 = "3";
2 = "3";
1 = "1";
4 = "4";
} as an NSUserDefaults/CFPreferences value for key `key`
The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.
......
And although NSDictionary and CFDictionary objects allow their keys to be objects of any type, if the keys are not string objects, the collections are not property-list objects.
蘋果官網有上述這樣一段話,能往 NSUserDefaults
里存儲的對象只能是 property list objects
,包括 NSData
,NSString
, NSNumber
, NSDate
, NSArray
, NSDictionary
,且對于 NSArray
和 NSDictionary
這兩個容器對象,它們所包含的內容也必需是 property list objects
。重點看最后一句話,雖然 NSDictionary
和 CFDictionary
對象的 Key 可以為任何類型(只要遵循 NSCopying 協議即可),但是如果當Key 不為字符串 string 對象時,此時這個字典對象就不能算是property list objects
了,所以不能往 NSUserDefaults
中存儲,不然就會報錯。
九、performSelector:afterDelay:的坑
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:self withObject:@selector(test) afterDelay:.0];
NSLog(@"3");
});
- (void)test{
NSLog(@"2");
}
上述代碼的執行結果并非 1 2 3 ,而是 1 3。原因是performSelector: withObject: afterDelay:
的本質是往 RunLoop
中添加定時器,而子線程默認是沒有啟動RunLoop
。performSelector: withObject: afterDelay:
接口雖然和performSelector:
系列接口長得很類似。但前者存在于RunLoop
相關文件,后者存在于NSObject
相關文件。
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
如果在子線程中添加上述兩行代碼,啟動RunLoop, 則代碼邏輯可以正常執行。
NSLog(@"1");
[self performSelector:self withObject:@selector(test) afterDelay:.0];
NSLog(@"3");
如果上述代碼放在主線程,是可以正常執行的。因為主線程默認開啟了 RunLoop。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
// [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
同之前的分析比較,這里同樣是在子線程 thread 上執行 performSelector: withObject: afterDelay:
方法,因為子線程沒有開啟 RunLoop,這里應該只輸出 1。 但實際上會在輸出 1 之后崩潰,原因在于執行完 thread 的block后,thread 會被釋放。打開注釋開啟子線程 thread 的 RunLoop,代碼可正常執行。
十、 @autoreleasepool
autoreleasepool 使用
每次遍歷的時候生成了很多占內存大的對象,如果交于默認的 autoreleasepool 去管理生命周期,會有因為內存飆升產生crash的風險,遍歷過程中,可在適當的位置上去使用@autoreleasepool
,一旦出了@autoreleasepool
作用域,該作用域內的變量會立馬釋放。如:
for(int i = 0; i < 10000; i++){
@autoreleasepool {
Person *p = [[Person alloc]init];
}
}
但并不是所有的遍歷方法都要加上@autoreleasepool
,比如enumerateObjectsUsingBlock:
方法,仔細閱讀蘋果官方文檔,可發現該方法內部已經添加過@autoreleasepool
處理。
autoreleasepool 底層
自動釋放池的主要底層數據結構是:__AtAutoreleasePool 和 AutoreleasePoolPage。
- __AtAutoreleasePool : autoreleasepool 底層是個C++結構體__AtAutoreleasePool,創建和銷毀的時候分別會調用構造函數和析構函數。即進入
@autoreleasepool{}
執行objc_autoreleasePoolPush
,出了@autoreleasepool{}
執行objc_autoreleasePoolPop
。
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 構造函數,在創建結構體的時候調用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析構函數,在結構體銷毀的時候調用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
- AutoreleasePoolPage: 調用了autorelease的對象最終由AutoreleasePoolPage 對象來管理。如 MRC 下
Person *p = [[[Person alloc]init]autorelease]
。每一個AutoreleasePoolPage對象占用4094字節內存,本身成員占用56字節,剩下的空間用來存放 autorelease對象 的地址和POOL_BOUNDARY
。所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起。另外,每個 AutoreleasePoolPage 有個 thread ,說明 autoreleasepool 和線程是一一對應的。如下圖:
POOL_BOUNDARY: 進入
@autoreleasepool{}
執行objc_autoreleasePoolPush
后 ,會往AutoreleasePoolPage
添加POOL_BOUNDARY
,并將POOL_BOUNDARY
的內存地址作為objc_autoreleasePoolPush
的返回值記錄下來;當出了@autoreleasepool{}
時執行objc_autoreleasePoolPop
,并將之前記錄的POOL_BOUNDARY
地址作為objc_autoreleasePoolPop
的參數,objc_autoreleasePoolPop
內部會依次調用 autorelease對象 的 release 方法銷毀對象,直到遇到POOL_BOUNDARY
內存地址為止。雙向鏈表:上述描述先進后出,實際上是棧的結構。每個AutoreleasePoolPage 的內存空間是連續的,理論上可以當做棧的形式處理,但是單個 AutoreleasePoolPage 容量有限, 所以需要借助鏈表結構去連接多個 AutoreleasePoolPage 擴容。之所以要使用雙向鏈表,是因為當執行
objc_autoreleasePoolPop
時,POOL_BOUNDARY 可能在上一個 AutoreleasePoolPage 中,此時需要找到之前的 AutoreleasePoolPage,并釋放掉中間的 autorelease 對象。
系統默認 autoreleasepool 和 RunLoop 的關系
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[[Person alloc] init] autorelease];
NSLog(@"%s",__FUNCTION__);
}
- (void)viewWillAppear{
NSLog(@"%s",__FUNCTION__);
}
-(void)viewDidAppear{
NSLog(@"%s",__FUNCTION__);
}
上述代碼, MRC 下 Person 對象會在執行完viewDidLoad
和viewWillAppear
方法之后再釋放,主要是和 Runloop 有關。iOS 中有個默認的autoreleasepool
,主線程的 Runloop 中注冊了 2 個 Observer:
- 第1個Observer監聽
kCFRunLoopEntry
事件,會調系統默認autoreleasepool
的 objc_autoreleasePoolPush() ; - 第2個Observer:
監聽kCFRunLoopBeforeWaiting
事件,會調系統默認autoreleasepool
的objc_autoreleasePoolPop()
、objc_autoreleasePoolPush()
;
監聽了kCFRunLoopBeforeExit
事件,會調系統默認autoreleasepool
的objc_autoreleasePoolPop()
;
上述代碼執行結果說明了: viewDidLoad
和viewWillAppear
在同一個運行周期內。
autorelease 和 release
內存管理中調用alloc、new、copy、mutableCopy方法返回對象,在不需要這個對象時,要調用 release 或autorelease 來釋放它,MRC 中通常會使用 release 和 autorelease。
autorelease 對象在什么時候釋放 ?
分兩種情況:
- main 函數自帶的 autoReleasePool 內: 此種情況下,和 RunLoop 有關。
- 手動創建的 autoReleasePool:此種情況下,出了 autoReleasePool 之后,autorelease 對象會依次釋放。
ARC 下,方法里的局部對象什么時候釋放?
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
}
上述代碼如果 ARC 最終轉換成Person *p = [[[Person alloc] init] autorelease];
則該對象的釋放和 RunLoop 有關;如果生成如下代碼,則出了方法內部該對象會立馬釋放,實際驗證中是出了方法立馬釋放。
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
[p release];
}
十一、如何對 NSMutableArray 進行 KVO
一般情況下只有通過調用 set 方法對值進行改變才會觸發 KVO。但是在調用NSMutableArray
的 addObject
或removeObject
系列方法時,并不會觸發它的 set 方法。所以為了實現NSMutableArray
的 KVO,官方為我們提供了如下方法:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key
在增刪元素時,使用上述方法來獲取要操作的可變數組,然后再執行添加或刪除元素的操作,便能實現 KVO 機制。如:
@property (nonatomic, strong) NSMutableArray *arr;
//添加元素操作
[[self mutableArrayValueForKey:@"arr"] addObject:item];
//移除元素操作
[[self mutableArrayValueForKey:@"arr"] removeObjectAtIndex:0];
十二、被忽略的UIViewController兩對API
如何判斷一個頁面的viewWillAppear
方法是 push 或 present 進來是調用的,還是 pop 或 dismiss 是調用的?一種比較笨拙的方法是通過添加屬性標記是進入還是返回調用viewWillAppear
方法。還有一種最簡單的方法,是直接調用蘋果提供的兩對 API 。
針對 Push 和 Pop 或 add childViewController 和 remove childViewController 的 API:
@property(nonatomic, readonly, getter=isMovingToParentViewController) BOOL movingToParentViewController NS_AVAILABLE_IOS(5_0);
@property(nonatomic, readonly, getter=isMovingFromParentViewController) BOOL movingFromParentViewController NS_AVAILABLE_IOS(5_0);
針對 Present 和 Dismiss 的 API:
@property(nonatomic, readonly, getter=isBeingPresented) BOOL beingPresented NS_AVAILABLE_IOS(5_0);
@property(nonatomic, readonly, getter=isBeingDismissed) BOOL beingDismissed NS_AVAILABLE_IOS(5_0);
十三、抗壓縮優先級
兩個水平布局的label,兩邊間隔分別是12,中間間隔為8(懂意思就行)。如果兩個label 都不設置寬度,則左邊 label 會拉長,右邊 label 自適應。
UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectZero];
label1.backgroundColor = [UIColor redColor];
label1.text = @"我是標題";
[self.view addSubview:label1];
[label1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.view);
make.left.equalTo(@(12));
}];
UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectZero];
label2.backgroundColor = [UIColor redColor];
label2.text = @"我是描述";
[self.view addSubview:label2];
[label2 mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(label1);
make.left.equalTo(label1.mas_right).offset(8);
make.right.equalTo(self.view).offset(-12);
}];
如果想讓左邊 label 自適應,右邊 label 拉升,可以設置控件拉升阻力(即抗拉升),拉升阻力越大越不容易被拉升。所以只要 label1 的拉升阻力比 label2 的大就能達到效果。
//UILayoutPriorityRequired = 1000
[label1 setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisHorizontal];
// //UILayoutPriorityDefaultLow = 250
[label2 setContentHuggingPriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisHorizontal];
- Content Hugging Priority:拉伸阻力,即抗拉伸。值越大,越不容易被拉伸。
- Content Compression Resistance Priority:壓縮阻力,即抗壓縮。值越大,越不容易被壓縮。
十四、約束優先級
從左到右依次為紅、藍、黃三個視圖三等分,藍色視圖布局依賴紅色,黃色視圖布局依賴藍色,如果突然將中間的藍色視圖移除,紅色和黃色視圖的寬度就無法計算。此種情況可以設置最后一個黃色視圖的做約束優先級,移除中間藍色視圖后,紅色和黃色視圖二等分。
//紅 left bottom height
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(self.view.mas_left).with.offset(20);
make.bottom.mas_equalTo(self.view.mas_bottom).with.offset(-80);
make.height.equalTo(@50);
}];
//藍 left bottom height width=紅色
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(redView.mas_right).with.offset(40);
make.height.width.bottom.mas_equalTo(redView);
}];
//黃 left right height width=紅色
[yellowView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(blueView.mas_right).with.offset(40);
make.right.mas_equalTo(self.view.mas_right).with.offset(-20);
make.height.width.bottom.mas_equalTo(redView);
//優先級
//必須添加這個優先級,否則blueView被移除后,redView 和 yellowView 的寬度就不能計算出來
make.left.mas_equalTo(redView.mas_right).with.offset(20).priority(250);
}];
//移除藍色
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[blueView removeFromSuperview];
[UIView animateWithDuration:3 animations:^{
//不加這行代碼就直接跳到對應的地方,加這行代碼就可以執行動畫。
//另外還要注意調用layoutIfNeeded的對象必須是執行動畫的父視圖。
//[blueView.superview layoutIfNeeded];
[self.view layoutIfNeeded];
}];
});
十五、設置代碼只在 Debug 下起效
- 源代碼中的測試代碼一般可以通過
#ifdef DEBUG ... #endif
- .a 靜態庫或 .framework 動態庫,可以通過設置
Library Search Paths
和Framework Search Paths
,分別移除Release
環境對應的路徑,Debug
環境對應的路徑保持不變。 - 對于 CocoaPods 引入的測試庫,可以配置
configurations
選項讓對應的庫只在 Debug 模式下生效,如:
pod 'RongCloudIM/IMKit', '~> 2.8.3',:configurations => ['Debug']
十六、為什么會有深拷貝和淺拷貝之分
上圖中觀察可知只有
不可變 + 不可變
組合的時候才出現淺拷貝,其他三種情況都是深拷貝。原因在于,兩個不可變對象內容一旦確定都是不可變的,所以不會彼此干擾,為了節省內容空間,兩個對象可以指向同一塊內存。而其他三種情況,都有可變對象的存在,為了避免兩個對象之間的彼此干擾,所有會開辟額外的空間。
十七、為什么交叉方法出現"死循環"
因為交換了方法的實現 IMP ,如果alert_replaceInitWithString
方法內部調用initWithString
會出現真正的死循環。下面代碼的死循環只是一個假象。
@implementation NSAttributedString (Exception)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@autoreleasepool {
[objc_getClass("NSConcreteAttributedString") swizzleMethod:@selector(initWithString:) swizzledSelector:@selector(alert_replaceInitWithString:)];
}
});
}
-(instancetype)alert_replaceInitWithString:(NSString*)aString{
if (!aString) {
NSString *string = [NSString stringWithFormat:@"[%s:%d行]",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],__LINE__];
[[[ExceptionAlert alloc]init]showAlertWithString:string];
;
return nil;
}
return [self alert_replaceInitWithString:aString];
}
@end
十八、為什么數組下標從零開始
數組下標最確切的定義應該偏移(offset),如果用 a 來表示數組的首地址,a[0] 就是偏移為 0 的位置,也就是首地址,a[k] 就表示偏移 k 個 type_size 的位置,所以計算 a[k] 的內存地址只需要用這個公式:
a[k]_address = base_address + k * type_size
但是,如果數組從 1 開始計數,那我們計算數組元素 a[k]的內存地址就會變為:
a[k]_address = base_address + (k-1)*type_size
對比兩個公式,不難發現,從 1 開始編號,每次隨機訪問數組元素都多了一次減法運算,對于 CPU 來說,就是多了一次減法指令。數組作為非常基礎的數據結構,通過下標隨機訪問數組元素又是其非常基礎的編程操作,效率的優化就要盡可能做到極致。所以為了減少一次減法操作,數組選擇了從 0 開始編號,而不是從 1 開始。同理 OC 中的 objc_msgSend
是直接基于匯編實現的,直接拋開 C 或 C++ 層面的代碼調用,極可能的提升代碼執行效率。
十九、copy修飾符引發崩潰問題
可變數組或字典經過 copy 修飾符修飾后,變成不可變數組或字典,此時再去執行添加或插入元素的時候會發生崩潰。
二十、為什么量子密碼學會有取代傳統加密方法的趨勢
傳統的加密方式存在兩個問題:
- 如在非對稱加密 RSA 體系中存在私鑰,只要對方獲取到私鑰就能破解,為此會有針對私鑰泄露相關的吊銷證書檢測機制。所以只能說相對安全不能說絕對安全。
- RSA 加密是基于互質關系實現的,不是沒有破解的可能,只是需要時間,如果并發機器足夠多,時間足夠多(幾十年或幾百年),RSA 加密依然是可以破解的。
關于互質關系
如果兩個正整數,除了1以外,沒有其他公因數,我們就稱這兩個數是互質關系(coprime)。
量子密碼學是基于量子形態做加解密,如果想破解必須要介入到量子狀態中,但是量子傳輸過程中可監聽到監聽者的介入。目前量子密碼仍處于研究階段,并沒有成熟的應用,量子很容易收到外界的干擾而改變狀態。
二十一、引用計數是怎么管理的
在arm64架構之前,isa 就是一個普通的指針,存儲著Class、Meta-Class對象的內存地址。從arm64架構開始,對isa進行了優化,變成了一個共用體(union)結構,還使用位域來存儲更多的信息。 isa 的結構如下:
-
extra_r
:里面存儲的值是引用計數器減1 -
has_sidetable_rc
表示引用計數器是否過大無法存儲在isa中,如果為1,那么引用計數會存儲在一個叫SideTable
的類的屬性中。
SideTable
結構如下,其中refcnts
是一個存放著對象引用計數的散列表,用當前對象的地址值作為 key ,對象的引用計數作為 Value。
二十二、weak 原理
- (void)viewDidLoad {
[super viewDidLoad];
__strong Person *person1;
__weak Person *person2;
Person *person3;
NSLog(@"111");
{
Person *person = [[MJPerson alloc] init];
//========第一種情況========
//如果只開啟該代碼,person在111,222 之后釋放,調用dealloc。person1 指針指向 person,person1 調用 person 的set 方法進行了retain 操作,所以 person 的生命周期同 person1。
//person1 = person;
//========第二種情況========
//如果只開啟該代碼,person會在111,222 中間釋放。此時 person2 沒有強引用(retain) person。
//所謂的 weak 指針原理是指:如何做到對象(person)被銷毀之后,指向對象的 weak 指針(person2)立馬被清空,置位 nil。
//person2 = person;
//========第三種情況========
//同第一種情況
//person3 = person;
}
NSLog(@"222");
}
@implementation Person
- (void)dealloc{
NSLog(@"%s", __func__);
}
@end
上述代碼如果開啟了person1 = person
person 會在輸出111,222 之后釋放,調用dealloc;如果開啟了person2 = person
person 會在111,222 中間釋放;如果開啟person3 = person
,效果同第一種。
-
__strong
是強引用,所以只有離開了viewDidLoad
方法后 person 對象才被釋放。 - 所謂的 weak 指針原理是指:如何做到對象(person)被銷毀之后(出了上述代碼中內嵌的{ }之后), 指向對象的 weak 指針(person2)立馬被清空,并被置位 nil。
- 默認是強引用。
weak 原理說明
- weak_table 是一個散列表,key 為對象地址,value 為一個數組,數組里面保存著指向該對象的所有弱指針。
- refcnts是一個存放著對象引用計數的散列表。
一個對象可能會被多次弱引用,當這個對象被銷毀時,我們需要找到這個對象的所有弱引用,所以我們需要將這些弱引用的地址(即指針)放在一個容器里(比如數組)。當對象不再被強引用時需要銷毀的時候,可以在 SideTable 中通過這個對象的地址找到引用值,首先清空引用值。同時, SideTable
結構中還有weak_table
,該結構也是一個散列表,key 為對象地址,value 為一個數組,里面保存著指向該對象的所有弱指針。當對象釋放的時候,先清空引用哈希表RefcountMap
對應的引用值,遍歷弱指針數組,依次將各個弱指針置為 nil。
二十三、加鹽的意義
用戶設置的密碼復雜度可能不夠高,同時不同的用戶極有可能會使用相同的密碼,那么這些用戶對應的密文也會相同,這樣,當存儲用戶密碼的數據庫泄露后,攻擊者會很容易便能找到相同密碼的用戶,從而也降低了破解密碼的難度。因此,在對用戶密碼進行加密時,需要考慮對密碼進行掩飾,即使是相同的密碼,也應該要保存為不同的密文,即使用戶輸入的是弱密碼,也需要考慮進行增強,從而增加密碼被攻破的難度,而使用帶鹽的加密hash值便能滿足該需求。比如密碼原本是由字母和數字組成,破解者僅需要在字母和數字中找答案。但是如果密碼中混淆了鹽(不僅僅只包含字母和數字),破解者僅僅從字母和數字下手,肯定是找不到答案,無疑增加了破解難度。
筆者實際項目開發中,為了網絡安全,請求參數按照一定的規則拼接成字符串,然后在字符串中加鹽,最后 MD5 簽名。后端依照同樣的規則校驗簽名,若簽名值一致則通過校驗。
二十四、Shell 腳本
二十五、什么是User Agent
User Agent中文名為用戶代理,簡稱 UA,它是一個特殊字符串頭,使得服務器能夠識別客戶使用的操作系統及版本、CPU 類型、瀏覽器及版本、瀏覽器渲染引擎、瀏覽器語言、瀏覽器插件等。網站在手機端 app 打開和直接在瀏覽器中打開看到的內容可能不一樣,是因為網頁可以根據 UA 判斷是 app 打開的還是瀏覽器打開的。
navigator
可以獲取到瀏覽器的信息:navigator.userAgent
。webView中獲取 User Agent 方式如下:
+(void)initialize{
if ([NSThread isMainThread]) {
[self getUserAgent];
}else{
dispatch_async(dispatch_get_main_queue(), ^{
[self getUserAgent];
});
}
}
+(void)getUserAgent{
UIWebView *webView = [[UIWebView alloc]initWithFrame:CGRectZero];
NSString *userAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"%@meicaiMallIOS",userAgent],@"UserAgent",nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dict];
}
二十六、JS和OC通信方式匯總
JS 調 OC
JS 調 OC ?目前主要的方式有三種:
- 通過 JSCore 中的 block
- 通過 JSCore 中的 JSExport
- 通過攔截 URL
在 JS 執?行行環境中添加?一個 _OC_catch 的 block,那么在 JS 代碼中就可以直接調?用 _OC_catch 這 個函數,當在 JS 中調?用 _OC_catch 這個函數后,我們剛才注冊的 block 就會被執行。也就是通過 JS 成功的調?了 OC 代碼。
context[@"_OC_catch"] = ^(JSValue *msg, JSValue *stack) {
};
JSExport
可以導出 Objective-C 的屬性、實例方法、類方法和初始化?方法到 JS 環境,這樣就可 以通過 JS 代碼直接調?用 Objective-C 。通過 JSExport 不僅可以導出?自定義類的方法、屬性,也可以導出已有類的?方法、屬性。在導出過程中,類的方法名會被轉換成 JS 類型命名,第二個參數的第一個字?會被大寫,比如- (void)addX:(int)x andY:(int)y;
被轉為addXAndY(x, y)
。除此,JSExport
還可以導出已有類的?方法、屬性。
通過攔截 URL,這種方式是 Web 端通過某種方式發送 URLScheme 請求,之后 Native 攔截到請求并根據URL SCHEME(包括所帶的參數)進行相關操作。類似于通過 SCHEME 喚起APP。這種方式的缺點是 url 長度有隱患,并且創建請求需要一定的耗時,比注入 API 的方式調用同樣的功能。耗時會比較長。所以還是更推薦使用注入 API 的方式。
OC 調 JS
OC 調 JS 主要有 UIWebView 、WKWebView 和 JSCore 這三種?方式。? UIWebView 的方式其實可以看作是 JSCore 的?方式。
- JSCore 方式
// 要執行的 JS 代碼,定義一個 add 函數并執?行行
NSString *addjs = @"function add(a, b) {return a + b;};add(1,3)";
// sumValue 為執?行行后的結果
JSValue *sumValue = [self.context evaluateScript:addjs];
- UIWebView 方式
這種?方式說?白了了就是使?用 JSCore ,通過 UIWebView 來獲取 JSContext ,這樣直接通過獲取到 context 來執?行行 JS 代碼。
//通過 UIWebView 獲取 context
JSContext *context = [_webView
valueForKeyPath:@"documentView.webView.mainFrame.JSContext"];
// 要執行的 JS 代碼,定義一個 add 函數并執?行行
NSString *addjs = @"function add(a, b) {return a + b;};add(1,3)";
// sumValue 為執?行行后的結果
JSValue *sumValue = [self.context evaluateScript:addjs];
- WKWebView 方式
WKWebView 沒有提供獲取 JSContext 的方法,但是它提供了執行 JS 的方法evaluateJS:
,通過下面方法來執行 JS 代碼。
[self.webView evaluateJS:@"function add(a, b) {return a + b;};add(1,3)" completionHandler:^(id _Nullable msg, NSError * _Nullable error) {
NSLog(@"evaluateJS add: %@, error: %@", msg, error);
}];
二十七、UIScrollView 原理
UIScrollView
繼承自UIView
,內部有一個 UIPanGestureRecongnizer
手勢。 frame
是相對父視圖坐標系來決定自己的位置和大小,而bounds
是相對于自身坐標系的位置和尺寸的。該視圖 bounds
的 origin
視圖本身沒有發生變化,但是它的子視圖的位置卻發生了變化,因為 bounds
的 origin
值是基于自身的坐標系,當自身坐標系的位置被改變了,里面的子視圖肯定得變化, bounds
和 panGestureRecognize
是實現 UIScrollView
滑動效果的關鍵技術點。
frame和bounds對比:
參考
- frame很簡單,它的x、y就是以當前視圖的父視圖為參照確定當前視圖的位置
- bounds的x、y則是當前視圖的坐標,并不影響當前視圖的位置,但是對當前視圖的子視圖有影響。當前視圖的坐標系統原點0,0默認為左上角,當更改了bounds.origin 坐標系統原點也會對應的被改變,由于當前試圖的子視圖都是參照當前視圖的原點進行布局,當坐標系統原點位置改變,其子視圖位置也會發生變化。
二十八、--verbose 和 --no-repo-update
-
verbose
意思為 冗長的、啰嗦的,一般在程序中表示詳細信息。此參數可以顯示命令執行過程中都發生了什么。 -
pod install
或pod update
可能會卡在Analyzing dependencies
步驟,因為這兩個命令會升級 CocoaPods 的spec 倉庫
,追加該參數可以省略此步驟,命令執行速度會提升。
二十九、dataSource 和 delegate 的本質區別
普遍開發者得理解是:一個是數據,一個是操作。如果從數據傳遞方向的角度來看,兩者的本質是數據傳遞的方向不同。dataSource
是外部將數據傳遞到視圖內,而 delegate
是將視圖內的數據和操作等傳遞到外部。實際開發封裝自定義視圖,可以參照數據傳遞方向分別設置 dataSource
和 delegate
。
三十、變種 MVC
真正的 MVC 應該是蘋果提供的經典UITableView
的使用,實際開發中經常在 Cell
中引入Model
,本質上來說不算是真正的 MVC ,只能算是 MVC 的變種。真正的 MVC 中 View 和 Model 應該是完全隔離的。蘋果的 MVC 中正是因為 View 沒有和任何 Model 綁定,所以 cell 的可沖擁堵高,但是缺點是代碼過于臃腫。
三十一、函數指針和 Block
相同點:
- 二者都可以看成是一個代碼片段。
- 函數指針類型和 Block 類型都可以作為變量和函數參數的類型(typedef定義別名之后,這個別名就是一個類型)。
不同點:
- 函數指針只能指向預先定義好的函數代碼塊,函數地址是在編譯鏈接時就已經確定好的。從內存的角度看,函數指針只不過是指向代碼區的一段可執行代碼,而 block 本質是 OC對象,是 NSObject的子類,是程序運行過程中在棧內存動態創建的對象,可以向其發送copy消息將block對象拷貝到堆內存,以延長其生命周期。
補充:指針函數和函數指針的區別
指針函數是指帶指針的函數,即本質是一個函數,函數返回類型是某一類型的指針。它是一個函數,只不過這個函數的返回值是一個地址值。
int *f(x,y);
函數指針是指向函數的指針變量,即本質是一個指針變量。
int (*f) (int x); /*聲明一個函數指針 */
f = func; /* 將func函數的首地址賦給指針f */
三十二、內存(堆內存)回收是什么意思
NSObject *obj = [[NSObject alloc] init];
代碼對應的內存布局如下,obj 指針存在于棧取,obj 對象存在于堆區。obj 指針的回收由棧區自動管理,堆區的內存需要開發者自己管理(MRC)情況。所謂的堆內存回收并不是指將 obj 對象占有的內存給挖去或是將空間數據清空為0,而是指 obj 對象原本占有的空間可以被其他人利用(即其他指針可以指向該空間)。其他指針指向該空間時,重新初始化該空間,將空間原有數據清零。
三十三、IP 和 MAC
IP 是地址,有定位功能;MAC 是身份唯一標識,無定位功能;有了 MAC 地址為什么還要有 IP 地址?舉個例子,現在我要和你通信(寫信給你),地址用你的身份證號,信能送到你手上嗎? 明顯不能!身份證號前六位能定位你出生的縣,MAC 地址前幾位也可以定位生產廠家。但是你出生后會離開這個縣(IP 地址變動),哪怕你還在這個縣,我總不能滿大街喊著你的身份證號去問路邊人是否認識這個身份證號的主人,所以此刻需要借助 IP 的定位功能。
三十四、MD5 相關小知識
具體可參考筆者之前文章 iOS 簽名機制,文章中可以找到答案。
三十五、響應鏈問題
命中測試和響應鏈問題
手勢、UIControl、UITouch系列事件關系
過程:
觸屏事件的處理被分成兩個階段:查找響應者(a)和響應者處理(b、c、d)。
- a.先將事件由上向下(從父控件向子控件)傳遞,找到最合適處理事件的控件。如果是同一級別的視圖,先調用后添加的視圖 hitTest,再調用先添加視圖的 hitTest。尋找最合適的視圖之所以從 Window 開始,是因為界面渲染本身是圖層樹的結構,遍歷從樹的根節點開始,才可以獲取到各個子節點信息。
- b.調用最合適控件的 touches 系列方法。
- c.如果調用 [super touches] 方法,就會將事件順著響應鏈條向上傳遞,傳遞給上一個響應者。
- d.接著調用上一個響應者的touches方法
hitTest 內部實現代碼還原
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"-----%@",self.nextResponder.class);
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) return nil;
//判斷點在不在這個視圖里
if ([self pointInside:point withEvent:event]) {
//在這個視圖 遍歷該視圖的子視圖
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
//轉換坐標到子視圖
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
//遞歸調用hitTest:withEvent繼續判斷
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
//在這里打印self.class可以看到遞歸返回的順序。
return hitTestView;
}
}
//這里就是該視圖沒有子視圖了 點在該視圖中,所以直接返回本身,上面的hitTestView就是這個。
NSLog(@"命中的view:%@",self.class);
return self;
}
//不在這個視圖直接返回nil
return nil;
}
響應鏈是什么?
其實響應鏈就是在命中測試中,走通的路徑。用上個章節的例子,整個命中測試的走向是:A? --> D? --> B? --> C?,我們把沒走通的?的去掉,以第一響應者 B 作為頭,依次連接,響應鏈就是:B -> A。(實際上 A 后面還有控制器等,但在該例子中沒有展示控制器等,所以就寫到 A)
默認來說,若該結點是 UIView 類型的話,這個 next 屬性是該結點的父視圖。但也有幾個例外:
- 如果是 UIViewController 的根視圖,則下一個響應者是 UIViewController。
- 如果是 UIViewController:
如果 UIViewController 的視圖是 UIWindow 的根視圖,則下一個響應者是 UIWindow 對象;如果 UIViewController 是由另一個 UIViewController 呈現的,則下一個響應者是第二個 UIViewController。 - UIWindow的下一個響應者是 UIApplication。
- UIApplication 的下一個響應者是 app delegate。但僅當該 app delegate 是 UIResponder 的實例且不是 UIView、UIViewController 或 app 對象本身時,才是下一個響應者。
下面舉個例子來說明。如下圖所示,觸摸點是,那根據命中測試,B 就成為了第一響應者。由于 C 是 B 的父視圖、A 是 C 的父視圖、同時 A 是 Controller 的根視圖,那么按照規則,響應鏈就是這樣的:視圖 B -> 視圖 C -> 根視圖 A -> UIViewController 對象 -> UIWindow 對象 -> UIApplication 對象 -> App Delegate
獲取到響應鏈后,觸摸事件首先將會由第一響應者響應,首先觸發第一響應者的touchBegin 方法。
應用1:子View超出父View的情況,子 View 依舊能響應事件。
重載父 view 的 -(UIView *)hitTest: withEvent:
方法,去掉點擊必須在父 view 內的坐標判斷邏輯,子 view 就能成為最合適的視圖,用于響應事件了。注意內部的坐標轉化:判斷點擊的點是包含子視圖,如果包含子視圖,則調用子視圖的 hitTest 方法。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
/**
* 此注釋掉的方法用來判斷點擊是否在父View Bounds內,
* 如果不在父view內,就會直接不會去其子View中尋找HitTestView,return 返回
*/
// if ([self pointInside:point withEvent:event]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
// }
return nil;
}
應用2:穿透子 View 點擊父 View。
子View覆蓋在父View上,但是要實現穿透子View去響應父View點擊事件。解決方法時,重寫子 View 的 hitTest 方法。點擊的是自身則返回nil, 此時最合適響應者轉為父類。
-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *hitView =[super hitTest:point withEvent:event];
if(hitView == self){
//自動將事件傳遞到上一層(父視圖),自身不做事件處理
return nil;
}
return hitView;
}
應用3:如何實現點擊子視圖,父視圖和子視圖同時響應?
方法一:子視圖重寫以下 touch 系列方法,通過 self.nextResponder
找到父視圖,并調用父視圖的 touch 系列方法。
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self.nextResponder touchesBegan:touches withEvent:event];
[super touchesBegan:touches withEvent:event];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[self.nextResponder touchesMoved:touches withEvent:event];
[super touchesMoved:touches withEvent:event];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self.nextResponder touchesEnded:touches withEvent:event];
[super touchesEnded:touches withEvent:event];
}
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
[self.nextResponder touchesCancelled:touches withEvent:event];
[super touchesCancelled:touches withEvent:event];
}
方法二: 如果子視圖和父視圖的收拾都是 UIGestureRecognizer 相關方法,也可以通過- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
方法,同時響應父視圖和子視圖的事件。此方法返回YES,手勢事件會一直往下傳遞,不論當前層次是否對該事件進行響應。
應用4:擴大 button 熱區
重載UIButton的-(BOOL)pointInside: withEvent:方法,讓Point即使落在Button的Frame外圍也返回YES。
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point);
}
CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) {
CGRect hitTestingBounds = bounds;
if (minimumHitTestWidth > bounds.size.width) {
hitTestingBounds.size.width = minimumHitTestWidth;
hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width)/2;
}
if (minimumHitTestHeight > bounds.size.height) {
hitTestingBounds.size.height = minimumHitTestHeight;
hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height)/2;
}
return hitTestingBounds;
}
三十六、什么是線程不安全?線程不安全的本質原因?
不能確定代碼的運行順序和結果,是線程不安全的。線程安全是相對于多線程而言的,單線程不會存在線程安全問題。因為單線程代碼的執行順序是唯一確定的,進而可以確定代碼的執行結果。
線程不安全的本質原因在于:表面展現在我們眼前的可能是一行代碼,但轉換成匯編代碼后可能對應多行。當多個線程同時去訪問代碼資源時,代碼的執行邏輯就會發生混亂。如數據的寫操作,底層實現可能是先讀取,再在原有數據的基礎上改動。如果此時有一個讀操作,原本意圖是想在寫操作完畢之后再讀取數據,但不巧的這個讀操作剛好發生在寫操作執行的中間步驟中。雖然讀操作后與寫操作執行,但數據讀取的值并不是寫操作的結果值,運氣不好時還可能發生崩潰。
- (void)viewDidLoad {
[super viewDidLoad];
int a = 100;
a += 200;
NSLog(@"%d",a);
}
如上述代碼中的int a = 100;
和a += 200;
轉換的匯編代碼,為下面中間八行匯編代碼。
0x1098e7621 <+49>: callq 0x1098e7a32 ; symbol stub for: objc_msgSendSuper2
0x1098e7626 <+54>: leaq 0x1a33(%rip), %rax ; @"%d"
0x1098e762d <+61>: movl $0x64, -0x24(%rbp)
0x1098e7634 <+68>: movl -0x24(%rbp), %ecx
0x1098e7637 <+71>: addl $0xc8, %ecx
0x1098e763d <+77>: movl %ecx, -0x24(%rbp)
-> 0x1098e7640 <+80>: movl -0x24(%rbp), %esi
0x1098e7643 <+83>: movq %rax, %rdi
0x1098e7646 <+86>: movb $0x0, %al
0x1098e7648 <+88>: callq 0x1098e7a14 ; symbol stub for: NSLog
三十七、App 啟動流程
APP 啟動分為冷啟動和熱啟動,這里主要說下冷啟動過程。冷啟動分為三階段: dyld 階段、runtime階段、main函數階段,一般啟動時間的優化也是從這三大步著手。
- dyld階段:dyld(dynamic link editor)是Apple的動態鏈接器,可以用來裝載 Mach-O 文件(可執行文件、動態庫等)。啟動APP時,dyld 首先裝載可執行文件,同時會遞歸加載所有依賴的動態庫(如果不加載動態庫,可能會報找不到符號錯誤)。
- runtime 階段:首先解析可執行文件,進行各種objc結構的初始化(注冊Objc類 、初始化類對象等等)。之后調用所有類和分類的
+load
方法,attribute((constructor)) 修飾的函數的調用、創建 C++ 靜態全局變量。到此為止,可執行文件和動態庫中所有的符號(Class、Protocol、Selector、IMP …)都已經按格式成功加載到內存中,被runtime 所管理。 - main函數階段:所有初始化工作結束后,dyld就會調用main函數。
三十八、包體積優化中的內聯函數
在關于 App 包體積優化的一些博客文章中,偶爾看到包體積的優化可以從 C++ 入手,其中有一條是減少內聯函數的使用。問題來了,什么是內聯函數?為什么要減少內聯函數的使用?它和一般函數有什么異同點?和宏相比有什么異同點?
內聯函數關鍵字是 inline ,C++ 中普通函數使用的申明或實現使用inline 修飾后,即為內聯函數。注意:遞歸函數即使被 inline 修飾后也不是內聯函數,依然是普通函數。
inline int sum(int a, int b){
return a + b;
}
普通函數調用會開辟一段棧空間執行相關代碼,函數執行完再將對應的棧空間回收。而內聯函數調用中,編譯器會將函數調用直接展開為函數代碼。如cout << sum(1, 2) << endl
會直接轉換為cout << 1 + 2<< endl
,由此可見內聯函數和一般的宏很類似,都是直接替換相關代碼。同宏相比,內聯函數只是多了一些函數特性和語法檢測功能。
OC 中可以通過關鍵字 NS_INLINE
使用內聯函數。
NS_INLINE void log(int value) {
NSLog(@"%d", value);
}
綜上,內聯函數或宏省去了參數壓棧、生成匯編語言的CALL調用、返回參數、執行return等過程,可以減少函數調用的開銷。但是會增加代碼體積,所以減少內聯函數或宏的使用一定程度上可以減少包體積。但并不是說為了減小包體積完全不去使用內聯函數,建議經常會被調用的代碼,且代碼量不是很多的時候(不超過10行),為減少函數調用的開銷,可適當使用內聯函數。
三十九、super 本質
objc_msgSend 方法參數
LGPerson *person = [LGPerson alloc];
[person sayHello];
//2.消息發送
objc_msgSend(person, sel_registerName("sayHello"));
有兩個類 Animal 和 Cat ,其中 Cat 繼承自 Animal 類,在 Cat 類實現如下代碼,試問打印結果是什么?
@implementation Cat
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@",[self class]);//Cat
NSLog(@"%@",[self superclass]);//Animal
NSLog(@"%@",[super class]);//Cat
NSLog(@"%@",[super superclass]);//Animal
}
return self;
}
@end
上述代碼打印結果一次為: Cat Animal Cat Animal,前兩個結果不足為奇,后兩個結果似乎有點費解。
super 調用底層會轉換為objc_msgSendSuper
函數的調用,objc_msgSendSuper
函數接收 2 個參數 objc_super
結構體和 SEL
,objc_super
結構如下:
struct objc_super {
__unsafe_unretained _Nonnull id receiver; // 消息接收者
__unsafe_unretained _Nonnull Class super_class; // 消息接收者的父類
};
[super class]
在調用過程中,底層轉化為 objc_msgSendSuper({self, [Animal class]}, @selector(class));
,同 objc_msgSend
函數相比相當于多了第二個參數,但消息接收者仍然是 self
,所以打印結果為 Cat
。
the superclass at which to start searching for the method implementation.
objc_msgSendSuper
方法中的第二個參數主要作用是告訴從哪里開始搜索方法實現,一般傳入的是父類。這也是實際開發中 [super superClassMethod]
直接調用父類方法的原因。但是如果按照這一點來看,依然和上述打印結果不符合。因此需要看 class 和 superClass 方法的內部實現,class 方法的內部實現返回消息接收者,上述代碼消息接收者為 Cat ,因此打印結果仍然是 Cat。superClass 方法內部實現是返回消息接受者的父類,因此打印結果是 Animal。
@interface Person : NSObject
- (void)test;
@end
@implementation Person
- (void)test{
NSLog(@"person: %@,%@,%@,%@",[self class],[self superclass],[super class],[super superclass]);
}
@end
@interface Student : Person
- (void)test;
@end
@implementation Student
- (void)test{
NSLog(@"student:%@,%@,%@,%@",[self class],[self superclass],[super class],[super superclass]);
[super test];
}
@end
調用 Student 的 test 方法,最終打印結果都是 Student,Person,Student,Person。因為實例對象一直是 Student 對應的實例對象,并非是 Person 的實例對象。
四十、引用的本質(引用和指針的區別)
待更新。。。。。。
四十一、渲染框架分類
說實在的有時會對各種渲染框架感覺混亂,一會CA、一會CG等等,于是就把這些渲染框架簡單匯總了下。
- 1、UIKit & AppKit :這個不多說。
- 2、Core Animation:UIView底下封裝了一層CALayer樹,Core Animation 層是真正的渲染層,我們之所以能在屏幕上看到內容,真正的渲染工作是在 Core Animation 層的。
- 3、Core Graphics:用于運行時繪制圖像。可以繪制路徑、顏色,當開發者需要在運行時創建圖像時,可以使用 Core Graphics 去繪制。
- 4、 Core Image:用來處理已經創建的圖像。該框架擁有一系列現成的圖像過濾器,能對已存在的圖像進行高效的處理。
- 5、SceneKit & SpriteKit:普通開發者可能會對 SceneKit 和 SpriteKit 感到陌生,SceneKit 主要用于某些 3D 場景需求,而 SpriteKit 更多的用于游戲開發。SceneKit 和 SpriteKit 都包含了粒子系統、物理引擎等,即使是非游戲應用中也可以使用它們來完成一些比較炫酷的特效和物理模擬。
- 6、Metal:Metal 存在于以上渲染框架的最底層。Core Animation、Core Image、SceneKit、SpriteKit等等渲染框架都是構建于 Metal 之上的。
四十二、耗時代碼定位
實際開發中可能會遇到嚴重線程阻塞的情況,比如筆者之前就遇到過使用 MJ 下拉刷新,刷新完畢后 MJ 復位無動畫效果,第一猜測就是有阻塞,于是借助 Product --> Profile-->TimeProfiler
工具 第一時間定位到耗時較多的代碼。結果發現在渲染 Cell 的時候動態的調用了蘋果接口中 html
轉屬性文本的方法,該方法的解析異常耗時。可按照下圖設置 Call Tree ,方便定位耗時代碼。
四十三、如何給百萬數據排序
桶排序定義
給百萬數據排序可以用"桶排序",核心思想是將數據分到幾個有序的桶,每個桶里的數據再單獨進行排序。桶內排完序之后,再把每個桶里的數據按照順序依次取出,組成的序列就是有序的了。
桶排序時間復雜度
如果要排序的數據為 n 個,均勻地劃分到 m 個桶內,每個桶里就有 k = n/m
個元素。每個桶內部使用快速排序,則每個桶內時間復雜度為 O(k * logk)
。m 個桶排序的時間復是 O(m * k * logk)
,因為 k = n/m
,所以整個桶排序的時間復雜度就是 O(n*log(n/m))
。當桶的個數 m 接近數據個數 n 時,log(n/m)
就是非常小的常量,這個時候桶排序的時間復雜度接近 O(n)
。
桶排序缺點
桶排序對要排序數據的要求是非常苛刻的。
- 1、桶與桶之間有著大小順序。這樣每個桶內的數據都排序完之后,桶與桶之間的數據不需要再進行排序。
- 2、數據在各個桶之間的分布是比較均勻的。如果數據經過桶的劃分之后,有些桶里的數據非常多,有些非常少,很不平均。時間復雜度就不是常量級了。極端情況下,如果數據都被劃分到一個桶里,那就退化為
O(nlogn)
的排序算法了。
內存不足時,如何排序?
假設有 10GB 的訂單數據需要排序,內存有限,只有幾百 MB,沒辦法一次性把 10GB 的數據都加載到內存中。先掃秒訂單知道金額最小是 1 元,最大是 10 萬元。可以將訂單劃到100個桶內,第一個桶我們存儲金額在 1 元到 1000 元之內的訂單,第二桶存儲金額在 1001 元到 2000 元之內的訂單,以此類推。但是訂單的數據分布可能并不是非常均勻,某些桶內的數據依然是大于內存空間,此時可以將該桶內的數據再次進行劃分,直到能加載到內存為止。
四十四、自旋鎖 & 互斥鎖
線程安全中為了實現線程阻塞,一般有兩種方案:一種是讓線程處于休眠狀態,此時不會消耗 CPU 資源;另一種方案是讓線程忙等或空轉,此時會消耗一定的 CPU 資源。前者屬于互斥,后者屬于自旋。
自旋鎖
自旋鎖是一種特殊的互斥鎖,自旋在線程加鎖的情況下,會一直嘗試是否解鎖,如果沒有解鎖,會一直循環判斷,如果鎖已經放開,則繼續執行,不再是空轉狀態。
優點:
- 循環檢查資源持有者是否已經釋放了資源,這樣做的好處是減少了線程從睡眠到喚醒的資源消耗,但會一直占用CPU的資源。
缺點:
-
OSSpinLock
屬于自旋鎖,Pthred 庫中相關的鎖,以及NSLock
、@synchronized
等都屬于互斥鎖。OSSpinLock
目前已經不再安全,因為會出現優先級反轉問題。 現代操作系統一般采用 時間片輪轉算法 調度進程或線程,按照線程的優先級為不同的線程分配不同的時間,優先級越高分配的時間片越多。假設有兩個線程 thread1 和 thread2,其中 thread1 的優先級高于 thread2,即thread1 分配的時間片多余 thread2。如果 thread2 正在鎖內安全執行,一段時間后 thread1 執行任務時,發現鎖未打開,于是會處于忙等狀態。由于thread1 的優先級高于 thread2,此時系統會分配更多的時間片給 thread1,thread2 時間片減少,遲遲不能完成,thread1 卻一直等待。如此就造成線程優先級反轉。 - 由于一直忙等,所以忙等的過程會消耗 CPU 資源。
自旋鎖和互斥鎖適用場景:
什么時候用自旋鎖比較劃算
- 預計線程等待鎖的時間很短 。如YYCache 中的內存緩存
- 加鎖的代碼(加鎖部分的代碼也稱為臨界區)經常被調用,但競爭情況很少發生(如果競爭比較多自旋鎖可能會發生優先級反轉問題)。
- CPU資源不緊張(如果 CPU 資源比較緊張,再加上自旋鎖會一直占用 CPU 資源,會帶來更差的體驗)
- 多核處理器
什么時候用互斥鎖比較劃算
- 預計線程等待鎖的時間較長
- 單核處理器(為單核處理器時要避免占用更多的CPU資源,避免CPU空轉輪詢 )
- 臨界區有IO操作(因為 IO 操作比較占用資源,所以要使用占用資源更少的互斥鎖)
- 臨界區代碼復雜或者循環量大
- 臨界區競爭非常激烈(使用互斥鎖可以減少資源占用)
四十五、應用 Crash 時為什么對操作系統無影響?
雙模式、I/O 保護和內存保護、定時器三者是確保操作系統能夠運行的關鍵技術,可以避免外界應用崩潰對操作系統的影響。
雙模式
為了保證操作系統不受其它故障程序的影響,進而產生系統崩潰的可能。一種常用的辦法是引入雙重模式,即用戶模式和內核模式。內核模式只能運行操作系統的程序。所有的用戶應用程序只能在用戶模式下運行。 雙模式需要CPU的支持,如果CPU有模式位,則可以在操作系統中實現雙模式,目前主流的CPU基本都有模式位。雙模式允許操作系統不受其它故障應用程序的影響。特權指令是指可能引起崩潰的指令,該指令只能運行在內核模式中。 如果用戶程序需要使用特權指令,可以通過系統提供的API調用。I/O保護和內存保護
定義所有I/O指令為特權指令,用戶應用程序無法直接訪問I/O指令,只能通過系統調用進行I/O操作,從而避免非法I/O操作。
利用基址寄存器和限長寄存器隔離不同程序的內存地址。定時器
如果用戶程序死循環或用戶程序不調用系統調用,此時操作系統將無法獲得CPU并對系統進行管理。解決方法是引入定時器,在一段時間后發生中斷,將CPU控制權返回給操作系統。
四十六、硬盤重量會隨著存儲數據大小而變化嗎?
如果是磁盤重量不變,如果是 SSD硬盤(固態硬盤)會受到影響。
磁硬盤能存儲數據靠的是里面的磁鐵的方向改變。一個長條形狀的磁鐵有南極和北極兩個端,一種端代表0,另一端代表1,然后通過 01 不同的組合代表不同的意義,只要磁鐵足夠多,就能用它們排列的順序代表所有的信息,數據就是這樣存在磁硬盤中的。所以磁硬盤重量不會收存儲數據大小的影響。
SSD 內部有上萬億個小單元,每個單元表示 0 還是 1,取決于這個單元里裝了多少個電子,比如裝進去100個電子后,這個單元就代表 1 ,低于這個數值就代表 0。所以對SSD的重量會受到內部電子的影響。一個電子是0.000000……9公斤,30個零。2TB的數據至少要用2×10^13個電子。質量大約就是0.0000000000002公斤,12個零。
四十七、如何消除小數誤差
小數誤差的原因:
計算機之所以會出現運算錯誤的原因是因為一些小數無法轉換二進制數,例如 0.1 就無法用二進制數正確表示。下圖說明了小數的二進制小數表達方式,小數的表示方式和整數表示方式類似。
消除小數誤差:
把小數擴大對應的倍數,轉成整數進行計算。計算機在進行小數計算時可能會出錯,但是在計算整數的時候,只要不超過可處理數值的范圍一定不會出現問題。
四十八、運行時是否是 OC 的專利?
runtime 并非是 Objective-C 的專利,絕大多數語言都有這個概念,runtime 就是動態庫(運行時庫)的一部分。比如 C 語言中 glibc 動態鏈接庫通常會被很多操作依賴,包括字符串處理(strlen、strcpy)、信號處理、socket、線程、IO、動態內存分配等等。由于每個程序都依賴于運行時庫,這些庫一般都是動態鏈接的。這樣一來,運行時庫可以存儲在操作系統中,很多程序共享一個動態庫,這樣就可以節省內存占用空間和應用程序大小。
補充:鏈接一般分為靜態鏈接和動態鏈接。一般說的預編譯、編譯、匯編、鏈接,其中的鏈接是指靜態鏈接。所謂的動態鏈接是指: 鏈接過程被推遲到運行時再進行。
四十九、線程保活
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test{
NSLog(@"2");
}
直接執行上述代碼,在輸出 1 之后,會直接崩潰。主要原因在于,執行完 [thread start]
后,線程立馬被殺死。此時再次在線程中調用 test
方法會直接崩潰。 解決該問題的思路主要是保證線程的生命周期,即線程保活。AFN 中,異步網絡發起請求,請求回來之后,線程依然沒有被殺死,也是利用了線程保活技術。代碼如下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test{
NSLog(@"2");
}
五十、包體積優化總結
主要從以下四個方面作總結資源文件、源代碼、編譯參數配置以及蘋果自身優化。
資源文件
- 1、檢測未使用的圖片:開源項目LSUnusedResources或腳本工具
- 2、圖片壓縮處理imageoptim。順便補充一些很多電商網站里面涉及大量的商品圖片,為了節省流量通常可以使用 webp 格式圖片,webp 格式圖片不僅僅體積小,還支持 gif 格式,可參考這里。
- 3、簡單的圖片可以使用代碼自動生成。很類似的圖片,僅僅只有顏色不同,可以通過代碼處理圖片的顏色。
- 4、啟動圖和偽啟動圖不要直接在資源文件中保留兩份,偽啟動圖容器可以通過代碼獲取啟動圖資源。一次實際優化過程中,在該點下手,包體積立馬減少了 4M 左右。
- 5、Xcode 中也會有一些圖片相關設置,Compress PNG Files 和 Remove Text Medadata From PNG Files。前者打包的時候自動對圖片進行無損壓縮,后者會移除 PNG 圖像名稱、作者、版權、創作時間、注釋等信息。
- 6、部分資源文件還可以通過后端下發方式。
- 7、iconFont 替換部分圖標和文字。可參考該Demo,iconFont 制作過程。
- 8、如果項目中包含各種動畫效果,可以使用 Lottie 減少資源文件大小。
- 9、蘋果官方Symbol資源
-(UIImage*)imageChangeColor:(UIColor*)color{
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0f);//獲取畫布
[color setFill];//畫筆沾取顏色
CGRect bounds = CGRectMake(0, 0, self.size.width, self.size.height);
UIRectFill(bounds);
[self drawInRect:bounds blendMode:kCGBlendModeOverlay alpha:1.0f];//繪制一次
[self drawInRect:bounds blendMode:kCGBlendModeDestinationIn alpha:1.0f];//再繪制一次
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();//獲取圖片
return img;
}
+ (UIImage *)getLaunchImage{
CGSize viewSize = [UIScreen mainScreen].bounds.size;
NSString *viewOr = @"Portrait";//垂直
NSString *launchImage = nil;
NSArray *launchImages = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"UILaunchImages"];
for (NSDictionary *dict in launchImages) {
CGSize imageSize = CGSizeFromString(dict[@"UILaunchImageSize"]);
if (CGSizeEqualToSize(viewSize, imageSize) && [viewOr isEqualToString:dict[@"UILaunchImageOrientation"]]) {
launchImage = dict[@"UILaunchImageName"];
}
}
return [UIImage imageNamed:launchImage];
}
源代碼
- 1、使用fui工具檢測沒用到的
import
代碼文件,因為運行時的原因,刪除類之前做一下核對。 - 2、SameCodeFinder 可以在源代碼?文件中檢測到相同的 function, 過多的類似方法,可以將其抽離出來提取為工具類。
- 3、注意控制宏和內聯函數。可看本文的第三十八個小知識點包體積優化中的內聯函數。
- 4、LinkMap 可以得出每個類或者庫所占用的空間大小(代碼段+數據段),方便開發者快速定位需要優化的類或靜態庫。
- 5、OC 中項目中 Debug 代碼即使沒有使用,也沒有導入頭文件,依然會增加包體積,因為 OC 是基于運行時機制,編譯器無法確定哪些代碼將來是否會使用。但是 Swift 不同 OC,Swift 是靜態的,在編譯階段編譯器優化就可以去除無用代碼。
- 6、使用輕量級三方庫。
編譯參數配置
1、Optional Level-->Fastest,Smallest[-OS]:含義可以參照該篇文章 2.1 小節。
2、Link-Time Optimization : 它是 LLVM 編譯器的一個特性,用于在 link 中間代碼時,對全局代碼進行優化。這個優化是自動完成的,因此不需要修改現有的代碼。蘋果使用了新的優化方式 Incremental,大大減少了鏈接的時間。筆者在實際的項目開發中開啟這個配置后,包體積減少了 4 - 5M 左右。
3、Deployment Postprocessing、Strip Linked Product、Strip Debug Symbols During Copy、Symbols hidden by default 四者設置為 YES 后可以去掉不必要的符號信息,減少可執行文件大小。但去除了符號信息之后我們就只能使用 dSYM 來進行符號化了,所以需要將 Debug Information Format 修改為 DWARF with dSYM file。
其它、Dead Code Stripping(僅對靜態語言有效):刪除靜態鏈接的可執行文件中未引用的代碼。Debug 設置為 NO, Release 設置為 YES 可減少可執行文件大小。Xcode 默認會開啟此選項,C/C++/Swift 等靜態語言編譯器會在 link 的時候移除未使用的代碼,但是對于 Objective-C 等動態語言是無效的。因為 Objective-C 是建立在運行時上面的,底層暴露給編譯器的都是 Runtime 源碼編譯結果,所有的部分應該都是會被判別為有效代碼。
其它、Generate Debug Symbols(有作用但不建議修改): 當 Generate Debug Symbol s選項設置為 YES時,每個源文件在編譯成 .o 文件時,編譯參數多了 -g 和 -gmodules 兩項。打包會生成 symbols 文件。設置為 NO 則 ipa 中不會生成 symbol 文件,可以減少 ipa 大小。但會影響到崩潰的定位。保持默認的開啟,不做修改。
蘋果自身優化
- 1、Slicing : 創建、分發不同變體以適應不同目標設備的過程,App Slicing 僅向設備傳送與之相關的資源(取決于屏幕分辨率,架構等等)。如 2x 和 3x 的圖片放在 Asset Catalog 中會自動管理僅保留合適的圖片。但 Bundle 內則會同時包含2x 和 3x 。所以資源圖片盡可能放在 Asset Catalog 中。代碼資源會對應不同的設備生成不同的執行文件。如果用心的話,還會發現 AppStrore 中同一款應用在不同設備上顯示的包體積大小不同。
注意 : 代碼架構的拆分主要是由 Slicing 完成的,并非是 Bitcode 。Bitcode 的優勢更多體現在性能、以及后續的維護上。如果開啟了 Bitcode,以后 Apple 推出了新的 CPU 架構(不是指新iPhone設備)或者以后 LLVM 推出了一系列優化,我們也不再需要為其發布新的安裝包了,Apple Store 會為我們自動完成這步。