開發(fā)小知識(一)
開發(fā)小知識(二)
前言和目錄
該文章主要整理一些小知識點判导,主要涉及 iOS 以及計算基礎(chǔ)相關(guān)知識點,某些知識點暫時只有標(biāo)題,后續(xù)會持續(xù)更新。筆者最近一段時間面試過程中發(fā)現(xiàn)一些普遍現(xiàn)象礁蔗,對于一些很不起眼的問題醋寝,很多開發(fā)者都只停留在知道、聽說過的層面育谬,但是一旦問 是什么 和 為什么 含蓉,很多應(yīng)試者回答的并不理想频敛,比如下面的幾個問題:
- 數(shù)組的下標(biāo)為什么從零開始?
- 經(jīng)常聽到深拷貝和淺拷貝馅扣,為什么會存在深拷貝和淺拷貝這一對概念斟赚?
- block 和 函數(shù)指針有什么區(qū)別?
- 引用的本質(zhì)是什么差油?引用和指針有什么關(guān)系拗军?
- UI 性能優(yōu)化的時候,很多面試者會提到用
CALayer
代替視圖組件蓄喇,如果某天產(chǎn)品改需求发侵,要求添加觸發(fā)事件,那么CALayer
上怎么添加觸發(fā)事件妆偏? - 和 H5 交互的時候刃鳄,經(jīng)常會用到
userAgent
, 請問userAgent
是什么?(問過幾次钱骂,純 iOS 開發(fā)者沒幾人知道只說有印象) - 標(biāo)準(zhǔn)的 MVC 架構(gòu)模式中叔锐,
View
和Model
是完全獨立開來的挪鹏,很多開發(fā)者都說自己使用的是 MVC 模式,當(dāng)問起:為什么實際開發(fā)中自定義視圖組件時通常都會引入 Model 愉烙,并重寫setModel
方法讨盒?這還是不是 MVC ? - 面試過程中筆者偶爾會問多線程的相關(guān)問題步责,印象中有兩位應(yīng)試者脫口而出 自旋鎖 返顺,當(dāng)問及什么是 互斥鎖 ?什么是 自旋鎖 勺择?應(yīng)試者一臉懵创南,明明是自己給自己挖坑伦忠。此外還會問到:為什么線程會不安全省核?也沒幾個應(yīng)試者能完整回答出。
- 很多應(yīng)試者都知道昆码,http 和 https 的區(qū)別在于多了 SSL 層气忠,但是 SSL 層里面有什么,做了什么赋咽,位于網(wǎng)絡(luò)模型什么位置旧噪?
- 很多人都知道內(nèi)存(堆內(nèi)存)回收,但是內(nèi)存(堆內(nèi)存)回收后發(fā)生了什么脓匿?是把內(nèi)存從堆空間清空了嗎淘钟?還是重置為 0 ?還是說做了其他什么操作陪毡?
- MD5 安全嗎米母?如果不安全,有什么替代的方案毡琉?MD5算是加密算法的一種嗎铁瞒?如果不是,和加密算法有什么區(qū)別桅滋?
- pods 經(jīng)常用吧慧耍,pods 命令后面的參數(shù)--verbose 和 --no-repo-update 是什么意思?
- 令筆者比較驚訝的是丐谋,響應(yīng)鏈流程算是 iOS 入門基礎(chǔ)知識芍碧。筆者問了一道相關(guān)問題百分之七八十的面試者都很難回答上來。A 為父視圖号俐,依次執(zhí)行
[A addSubView:B]
泌豆、[A addSubView:C]
、C.userInteractionEnabled = NO
萧落,其中 B 視圖和 C 視圖有重疊践美,請問:B 視圖添加點擊事件能否響應(yīng)洗贰?多數(shù)應(yīng)試者第一反應(yīng)是不能,結(jié)合響應(yīng)鏈流程來看陨倡,答案顯然是錯誤的敛滋。 - super 經(jīng)常用,請問 super 調(diào)用方法和 self 調(diào)用方法有什么本質(zhì)區(qū)別兴革?
以上僅是部分典型小知識點绎晃,更多內(nèi)容請詳看此文。
目錄
- 一杂曲、CALayer如何添加點擊事件
- 二庶艾、為什么會存在堆空間
- 三、Tagged Pointer 是什么擎勘?
- 四咱揍、iOS平臺跨域訪問漏洞
- 五、緩存 NSDateFormatter
- 六棚饵、iOS 9 以后通知不再需要手動移除
- 七煤裙、UIImage 名稱為空的警告(符號斷點解決)
- 八、NSUserDefaults 存儲字典的一個坑
- 九噪漾、performSelector:afterDelay:的坑
- 十硼砰、 @autoreleasepool
- 十一、如何對 NSMutableArray 進行 KVO
- 十二欣硼、被忽略的UIViewController兩對API
- 十三题翰、抗壓縮優(yōu)先級
- 十四、約束優(yōu)先級
- 十五诈胜、設(shè)置代碼只在 Debug 下起效
- 十六豹障、為什么會有深拷貝和淺拷貝之分
- 十七、為什么交叉方法出現(xiàn)"死循環(huán)"
- 十八耘斩、為什么數(shù)組下標(biāo)從零開始
- 十九沼填、copy 修飾符引發(fā)崩潰問題
- 二十、為什么量子密碼學(xué)會有取代傳統(tǒng)加密方法的趨勢
- 二十一括授、引用計數(shù)是怎么管理的
- 二十二坞笙、weak 原理
- 二十三、加鹽的意義
- 二十四荚虚、Shell 腳本
- 二十五薛夜、什么是UserAgent
- 二十六、JS和OC通信方式匯總
- 二十七版述、UIScrollView 原理
- 二十八梯澜、--verbose 和 --no-repo-update
- 二十九、dataSource 和 delegate 的本質(zhì)區(qū)別
- 三十渴析、變種 MVC
- 三十一晚伙、函數(shù)指針和 Block
- 三十二吮龄、內(nèi)存(堆內(nèi)存)回收是什么意思
- 三十三、IP 和 MAC
- 三十四咆疗、MD5 相關(guān)小知識
- 三十五漓帚、響應(yīng)鏈問題
- 三十六、什么是線程不安全午磁?線程不安全的本質(zhì)原因尝抖?
- 三十七、App 啟動流程
- 三十八迅皇、包體積優(yōu)化中的內(nèi)聯(lián)函數(shù)
- 三十九昧辽、super 本質(zhì)
- 四十、引用的本質(zhì)(引用和指針的區(qū)別)
- 四十一登颓、渲染框架分類
- 四十二搅荞、NSProxy & NSObject
- 四十三、如何給百萬數(shù)據(jù)排序
- 四十四挺据、自旋鎖 & 互斥鎖
- 四十五取具、應(yīng)用 Crash 時為什么對操作系統(tǒng)無影響?
- 四十六扁耐、硬盤重量會隨著存儲數(shù)據(jù)大小而變化嗎?
- 四十七产阱、如何消除小數(shù)誤差
- 四十八婉称、運行時是否是 OC 的專利?
- 四十九构蹬、線程蓖醢担活
- 五十、包體積優(yōu)化總結(jié)
一庄敛、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];
}
}
二藻烤、為什么會存在堆空間
堆空間的存在主要是為了延長對象的生命周期绷雏,并使得對象的生命周期可控。
- 如果試圖用棽劳ぃ空間取代堆空間涎显,顯然是不可行的。棧是向低地址擴展的數(shù)據(jù)結(jié)構(gòu)兴猩,是一塊連續(xù)的內(nèi)存的區(qū)域期吓。這句話的意思是棧頂?shù)牡刂泛蜅5淖畲笕萘渴窍到y(tǒng)預(yù)先規(guī)定好的,如果申請的空間超過棧的剩余空間時倾芝,將出現(xiàn)棧溢出讨勤,發(fā)生未知錯誤箭跳。因此,能從棧獲得的空間較小潭千。而堆是向高地址擴展的數(shù)據(jù)結(jié)構(gòu)衅码,是不連續(xù)的內(nèi)存區(qū)域。這是由于系統(tǒng)是用鏈表來存儲的空閑內(nèi)存地址的脊岳。堆的大小受限于計算機系統(tǒng)中有效的虛擬內(nèi)存逝段。由此可見,堆獲得的空間比較靈活割捅,也比較大奶躯。 但是棧空間比堆空間響應(yīng)速度更快亿驾,所以一般類似int嘹黔、NSInteger等占用內(nèi)存比較小的通常放在棧空間莫瞬,對象一般放在堆空間儡蔓。
- 如果試圖用數(shù)據(jù)區(qū)(全局區(qū))取代堆空間,顯然也是不可行的疼邀。因為全局區(qū)的生命周期會伴隨整個應(yīng)用而存在喂江,比較消耗內(nèi)存,生命周期不像在堆空間那樣可控旁振,堆空間中可以隨時創(chuàng)建和銷毀获询。
- 代碼區(qū)就不用想了,如果能夠輕易改變代碼區(qū)拐袜,一個應(yīng)用就無任何安全性可言了吉嚣。
三、Tagged Pointer 是什么蹬铺?
從 64bit 開始尝哆,iOS 引入了Tagged Pointer技術(shù),用于優(yōu)化NSNumber甜攀、NSDate秋泄、NSString等小對象的存儲。在沒有使用Tagged Pointer之前赴邻, NSNumber等對象需要動態(tài)分配內(nèi)存印衔、維護引用計數(shù)等,NSNumber指針存儲的是堆中NSNumber對象的地址值姥敛;使用Tagged Pointer之后奸焙,NSNumber指針里面存儲的數(shù)據(jù)變成了:Tag + Data,也就是將數(shù)據(jù)直接存儲在了指針中。當(dāng)指針不夠存儲數(shù)據(jù)時与帆,會使用動態(tài)分配內(nèi)存的方式來存儲數(shù)據(jù)了赌。
四、iOS平臺跨域訪問漏洞
UIWebView
默認開啟了WebKitAllowUniversalAccessFromFileURLs
和 WebKitAllowFileAccessFromFileURLs
屬性玄糟。利用這個漏洞給某個 App 下發(fā)一個 HTML 文件勿她,當(dāng) UIWebView
使用 file 協(xié)議
打開這個 HTML 文件, HTML 文件中含有一段竊取用戶數(shù)據(jù)的 JS 代碼,就會導(dǎo)致用戶數(shù)據(jù)泄露阵翎。
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
的文件逢并。這個漏洞訪問其他應(yīng)用的數(shù)據(jù),而不必需要用戶的許可郭卫。但WKWiebView
的 WebKitAllowUniversalAccessFromFileURLs
和 WebKitAllowFileAccessFromFileURLs
默認是關(guān)閉的(可以手動控制)砍聊,不會存在這樣的風(fēng)險。
補充:針對 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
在注冊者被回收時需要手動移除,是一直以來的使用準(zhǔn)則贰盗。原因是在 MRC 時代许饿,通知中心持有的是注冊者的 unsafe_unretained
指針,在注冊者被回收時若不對通知進行手動移除童太,則指針指向被回收的內(nèi)存區(qū)域米辐,變?yōu)橐爸羔槨4藭r發(fā)送通知會造成 crash 书释。而在 iOS 9 以后,通知中心持有的是注冊者的 weak
指針赊窥,這時即使不對通知進行手動移除爆惧,指針也會在注冊者被回收后自動置空。因為向空指針發(fā)送消息是不會有問題的锨能。
七扯再、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];
執(zhí)行上述代碼會報如下錯誤:
[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.
蘋果官網(wǎng)有上述這樣一段話唯咬,能往 NSUserDefaults
里存儲的對象只能是 property list objects
应狱,包括 NSData
,NSString
, NSNumber
, NSDate
, NSArray
, NSDictionary
免绿,且對于 NSArray
和 NSDictionary
這兩個容器對象可帽,它們所包含的內(nèi)容也必需是 property list objects
钾军。重點看最后一句話鳄袍,雖然 NSDictionary
和 CFDictionary
對象的 Key 可以為任何類型(只要遵循 NSCopying 協(xié)議即可),但是如果當(dāng)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");
}
上述代碼的執(zhí)行結(jié)果并非 1 2 3 ,而是 1 3搅幅。原因是performSelector: withObject: afterDelay:
的本質(zhì)是往 RunLoop
中添加定時器阅束,而子線程默認是沒有啟動RunLoop
。performSelector: withObject: afterDelay:
接口雖然和performSelector:
系列接口長得很類似盏筐。但前者存在于RunLoop
相關(guān)文件围俘,后者存在于NSObject
相關(guān)文件。
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
如果在子線程中添加上述兩行代碼琢融,啟動RunLoop界牡, 則代碼邏輯可以正常執(zhí)行。
NSLog(@"1");
[self performSelector:self withObject:@selector(test) afterDelay:.0];
NSLog(@"3");
如果上述代碼放在主線程漾抬,是可以正常執(zhí)行的宿亡。因為主線程默認開啟了 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 上執(zhí)行 performSelector: withObject: afterDelay:
方法挽荠,因為子線程沒有開啟 RunLoop,這里應(yīng)該只輸出 1平绩。 但實際上會在輸出 1 之后崩潰圈匆,原因在于執(zhí)行完 thread 的block后,thread 會被釋放捏雌。打開注釋開啟子線程 thread 的 RunLoop跃赚,代碼可正常執(zhí)行。
十性湿、 @autoreleasepool
autoreleasepool 使用
每次遍歷的時候生成了很多占內(nèi)存大的對象纬傲,如果交于默認的 autoreleasepool 去管理生命周期,會有因為內(nèi)存飆升產(chǎn)生crash的風(fēng)險肤频,遍歷過程中叹括,可在適當(dāng)?shù)奈恢蒙先ナ褂?code>@autoreleasepool,一旦出了@autoreleasepool
作用域宵荒,該作用域內(nèi)的變量會立馬釋放汁雷。如:
for(int i = 0; i < 10000; i++){
@autoreleasepool {
Person *p = [[Person alloc]init];
}
}
但并不是所有的遍歷方法都要加上@autoreleasepool
,比如enumerateObjectsUsingBlock:
方法净嘀,仔細閱讀蘋果官方文檔,可發(fā)現(xiàn)該方法內(nèi)部已經(jīng)添加過@autoreleasepool
處理摔竿。
autoreleasepool 底層
自動釋放池的主要底層數(shù)據(jù)結(jié)構(gòu)是:__AtAutoreleasePool 和 AutoreleasePoolPage面粮。
- __AtAutoreleasePool : autoreleasepool 底層是個C++結(jié)構(gòu)體__AtAutoreleasePool,創(chuàng)建和銷毀的時候分別會調(diào)用構(gòu)造函數(shù)和析構(gòu)函數(shù)继低。即進入
@autoreleasepool{}
執(zhí)行objc_autoreleasePoolPush
,出了@autoreleasepool{}
執(zhí)行objc_autoreleasePoolPop
熬苍。
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 構(gòu)造函數(shù),在創(chuàng)建結(jié)構(gòu)體的時候調(diào)用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析構(gòu)函數(shù)袁翁,在結(jié)構(gòu)體銷毀的時候調(diào)用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
- AutoreleasePoolPage: 調(diào)用了autorelease的對象最終由AutoreleasePoolPage 對象來管理柴底。如 MRC 下
Person *p = [[[Person alloc]init]autorelease]
。每一個AutoreleasePoolPage對象占用4094字節(jié)內(nèi)存粱胜,本身成員占用56字節(jié)柄驻,剩下的空間用來存放 autorelease對象 的地址和POOL_BOUNDARY
。所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起焙压。另外鸿脓,每個 AutoreleasePoolPage 有個 thread ,說明 autoreleasepool 和線程是一一對應(yīng)的涯曲。如下圖:
POOL_BOUNDARY: 進入
@autoreleasepool{}
執(zhí)行objc_autoreleasePoolPush
后 ,會往AutoreleasePoolPage
添加POOL_BOUNDARY
野哭,并將POOL_BOUNDARY
的內(nèi)存地址作為objc_autoreleasePoolPush
的返回值記錄下來;當(dāng)出了@autoreleasepool{}
時執(zhí)行objc_autoreleasePoolPop
幻件,并將之前記錄的POOL_BOUNDARY
地址作為objc_autoreleasePoolPop
的參數(shù)拨黔,objc_autoreleasePoolPop
內(nèi)部會依次調(diào)用 autorelease對象 的 release 方法銷毀對象,直到遇到POOL_BOUNDARY
內(nèi)存地址為止绰沥。雙向鏈表:上述描述先進后出篱蝇,實際上是棧的結(jié)構(gòu)。每個AutoreleasePoolPage 的內(nèi)存空間是連續(xù)的徽曲,理論上可以當(dāng)做棧的形式處理零截,但是單個 AutoreleasePoolPage 容量有限, 所以需要借助鏈表結(jié)構(gòu)去連接多個 AutoreleasePoolPage 擴容秃臣。之所以要使用雙向鏈表瞻润,是因為當(dāng)執(zhí)行
objc_autoreleasePoolPop
時,POOL_BOUNDARY 可能在上一個 AutoreleasePoolPage 中甜刻,此時需要找到之前的 AutoreleasePoolPage,并釋放掉中間的 autorelease 對象正勒。
系統(tǒng)默認 autoreleasepool 和 RunLoop 的關(guān)系
- (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 對象會在執(zhí)行完viewDidLoad
和viewWillAppear
方法之后再釋放得院,主要是和 Runloop 有關(guān)。iOS 中有個默認的autoreleasepool
章贞,主線程的 Runloop 中注冊了 2 個 Observer:
- 第1個Observer監(jiān)聽
kCFRunLoopEntry
事件祥绞,會調(diào)系統(tǒng)默認autoreleasepool
的 objc_autoreleasePoolPush() ; - 第2個Observer:
監(jiān)聽kCFRunLoopBeforeWaiting
事件,會調(diào)系統(tǒng)默認autoreleasepool
的objc_autoreleasePoolPop()
、objc_autoreleasePoolPush()
;
監(jiān)聽了kCFRunLoopBeforeExit
事件蜕径,會調(diào)系統(tǒng)默認autoreleasepool
的objc_autoreleasePoolPop()
;
上述代碼執(zhí)行結(jié)果說明了: viewDidLoad
和viewWillAppear
在同一個運行周期內(nèi)两踏。
autorelease 和 release
內(nèi)存管理中調(diào)用alloc、new兜喻、copy梦染、mutableCopy方法返回對象,在不需要這個對象時朴皆,要調(diào)用 release 或autorelease 來釋放它帕识,MRC 中通常會使用 release 和 autorelease。
autorelease 對象在什么時候釋放 遂铡?
分兩種情況:
- main 函數(shù)自帶的 autoReleasePool 內(nèi): 此種情況下肮疗,和 RunLoop 有關(guān)。
- 手動創(chuàng)建的 autoReleasePool:此種情況下扒接,出了 autoReleasePool 之后伪货,autorelease 對象會依次釋放。
ARC 下钾怔,方法里的局部對象什么時候釋放碱呼?
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
}
上述代碼如果 ARC 最終轉(zhuǎn)換成Person *p = [[[Person alloc] init] autorelease];
則該對象的釋放和 RunLoop 有關(guān);如果生成如下代碼,則出了方法內(nèi)部該對象會立馬釋放蒂教,實際驗證中是出了方法立馬釋放巍举。
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
[p release];
}
十一、如何對 NSMutableArray 進行 KVO
一般情況下只有通過調(diào)用 set 方法對值進行改變才會觸發(fā) KVO凝垛。但是在調(diào)用NSMutableArray
的 addObject
或removeObject
系列方法時懊悯,并不會觸發(fā)它的 set 方法。所以為了實現(xiàn)NSMutableArray
的 KVO梦皮,官方為我們提供了如下方法:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key
在增刪元素時炭分,使用上述方法來獲取要操作的可變數(shù)組,然后再執(zhí)行添加或刪除元素的操作剑肯,便能實現(xiàn) KVO 機制捧毛。如:
@property (nonatomic, strong) NSMutableArray *arr;
//添加元素操作
[[self mutableArrayValueForKey:@"arr"] addObject:item];
//移除元素操作
[[self mutableArrayValueForKey:@"arr"] removeObjectAtIndex:0];
十二、被忽略的UIViewController兩對API
如何判斷一個頁面的viewWillAppear
方法是 push 或 present 進來是調(diào)用的让网,還是 pop 或 dismiss 是調(diào)用的呀忧?一種比較笨拙的方法是通過添加屬性標(biāo)記是進入還是返回調(diào)用viewWillAppear
方法。還有一種最簡單的方法溃睹,是直接調(diào)用蘋果提供的兩對 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);
十三、抗壓縮優(yōu)先級
兩個水平布局的label因篇,兩邊間隔分別是12泞辐,中間間隔為8(懂意思就行)笔横。如果兩個label 都不設(shè)置寬度,則左邊 label 會拉長咐吼,右邊 label 自適應(yīng)吹缔。
UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectZero];
label1.backgroundColor = [UIColor redColor];
label1.text = @"我是標(biāo)題";
[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 自適應(yīng),右邊 label 拉升锯茄,可以設(shè)置控件拉升阻力(即抗拉升)厢塘,拉升阻力越大越不容易被拉升。所以只要 label1 的拉升阻力比 label2 的大就能達到效果撇吞。
//UILayoutPriorityRequired = 1000
[label1 setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisHorizontal];
// //UILayoutPriorityDefaultLow = 250
[label2 setContentHuggingPriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisHorizontal];
- Content Hugging Priority:拉伸阻力俗冻,即抗拉伸。值越大牍颈,越不容易被拉伸迄薄。
- Content Compression Resistance Priority:壓縮阻力,即抗壓縮煮岁。值越大讥蔽,越不容易被壓縮。
十四画机、約束優(yōu)先級
從左到右依次為紅冶伞、藍、黃三個視圖三等分步氏,藍色視圖布局依賴紅色响禽,黃色視圖布局依賴藍色,如果突然將中間的藍色視圖移除荚醒,紅色和黃色視圖的寬度就無法計算芋类。此種情況可以設(shè)置最后一個黃色視圖的做約束優(yōu)先級,移除中間藍色視圖后界阁,紅色和黃色視圖二等分侯繁。
//紅 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);
//優(yōu)先級
//必須添加這個優(yōu)先級,否則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:^{
//不加這行代碼就直接跳到對應(yīng)的地方贮竟,加這行代碼就可以執(zhí)行動畫。
//另外還要注意調(diào)用layoutIfNeeded的對象必須是執(zhí)行動畫的父視圖较剃。
//[blueView.superview layoutIfNeeded];
[self.view layoutIfNeeded];
}];
});
十五咕别、設(shè)置代碼只在 Debug 下起效
- 源代碼中的測試代碼一般可以通過
#ifdef DEBUG ... #endif
- .a 靜態(tài)庫或 .framework 動態(tài)庫,可以通過設(shè)置
Library Search Paths
和Framework Search Paths
写穴,分別移除Release
環(huán)境對應(yīng)的路徑顷级,Debug
環(huán)境對應(yīng)的路徑保持不變。 - 對于 CocoaPods 引入的測試庫确垫,可以配置
configurations
選項讓對應(yīng)的庫只在 Debug 模式下生效弓颈,如:
pod 'RongCloudIM/IMKit', '~> 2.8.3',:configurations => ['Debug']
十六、為什么會有深拷貝和淺拷貝之分
上圖中觀察可知只有
不可變 + 不可變
組合的時候才出現(xiàn)淺拷貝删掀,其他三種情況都是深拷貝翔冀。原因在于,兩個不可變對象內(nèi)容一旦確定都是不可變的披泪,所以不會彼此干擾纤子,為了節(jié)省內(nèi)容空間,兩個對象可以指向同一塊內(nèi)存款票。而其他三種情況控硼,都有可變對象的存在,為了避免兩個對象之間的彼此干擾艾少,所有會開辟額外的空間卡乾。
十七、為什么交叉方法出現(xiàn)"死循環(huán)"
因為交換了方法的實現(xiàn) IMP 缚够,如果alert_replaceInitWithString
方法內(nèi)部調(diào)用initWithString
會出現(xiàn)真正的死循環(huán)幔妨。下面代碼的死循環(huán)只是一個假象。
@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
十八谍椅、為什么數(shù)組下標(biāo)從零開始
數(shù)組下標(biāo)最確切的定義應(yīng)該偏移(offset)误堡,如果用 a 來表示數(shù)組的首地址,a[0] 就是偏移為 0 的位置雏吭,也就是首地址锁施,a[k] 就表示偏移 k 個 type_size 的位置,所以計算 a[k] 的內(nèi)存地址只需要用這個公式:
a[k]_address = base_address + k * type_size
但是杖们,如果數(shù)組從 1 開始計數(shù)悉抵,那我們計算數(shù)組元素 a[k]的內(nèi)存地址就會變?yōu)椋?/p>
a[k]_address = base_address + (k-1)*type_size
對比兩個公式,不難發(fā)現(xiàn)胀莹,從 1 開始編號基跑,每次隨機訪問數(shù)組元素都多了一次減法運算,對于 CPU 來說描焰,就是多了一次減法指令媳否。數(shù)組作為非常基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)荆秦,通過下標(biāo)隨機訪問數(shù)組元素又是其非忱榻撸基礎(chǔ)的編程操作,效率的優(yōu)化就要盡可能做到極致步绸。所以為了減少一次減法操作掺逼,數(shù)組選擇了從 0 開始編號,而不是從 1 開始瓤介。同理 OC 中的 objc_msgSend
是直接基于匯編實現(xiàn)的吕喘,直接拋開 C 或 C++ 層面的代碼調(diào)用赘那,極可能的提升代碼執(zhí)行效率。
十九氯质、copy修飾符引發(fā)崩潰問題
可變數(shù)組或字典經(jīng)過 copy 修飾符修飾后募舟,變成不可變數(shù)組或字典,此時再去執(zhí)行添加或插入元素的時候會發(fā)生崩潰闻察。
二十拱礁、為什么量子密碼學(xué)會有取代傳統(tǒng)加密方法的趨勢
傳統(tǒng)的加密方式存在兩個問題:
- 如在非對稱加密 RSA 體系中存在私鑰,只要對方獲取到私鑰就能破解辕漂,為此會有針對私鑰泄露相關(guān)的吊銷證書檢測機制呢灶。所以只能說相對安全不能說絕對安全。
- RSA 加密是基于互質(zhì)關(guān)系實現(xiàn)的钉嘹,不是沒有破解的可能鸯乃,只是需要時間,如果并發(fā)機器足夠多隧期,時間足夠多(幾十年或幾百年)飒责,RSA 加密依然是可以破解的。
關(guān)于互質(zhì)關(guān)系
如果兩個正整數(shù)仆潮,除了1以外宏蛉,沒有其他公因數(shù),我們就稱這兩個數(shù)是互質(zhì)關(guān)系(coprime)性置。
量子密碼學(xué)是基于量子形態(tài)做加解密拾并,如果想破解必須要介入到量子狀態(tài)中,但是量子傳輸過程中可監(jiān)聽到監(jiān)聽者的介入鹏浅。目前量子密碼仍處于研究階段嗅义,并沒有成熟的應(yīng)用,量子很容易收到外界的干擾而改變狀態(tài)隐砸。
二十一之碗、引用計數(shù)是怎么管理的
在arm64架構(gòu)之前,isa 就是一個普通的指針季希,存儲著Class褪那、Meta-Class對象的內(nèi)存地址。從arm64架構(gòu)開始式塌,對isa進行了優(yōu)化博敬,變成了一個共用體(union)結(jié)構(gòu),還使用位域來存儲更多的信息峰尝。 isa 的結(jié)構(gòu)如下:
-
extra_r
:里面存儲的值是引用計數(shù)器減1 -
has_sidetable_rc
表示引用計數(shù)器是否過大無法存儲在isa中偏窝,如果為1,那么引用計數(shù)會存儲在一個叫SideTable
的類的屬性中。
SideTable
結(jié)構(gòu)如下祭往,其中refcnts
是一個存放著對象引用計數(shù)的散列表伦意,用當(dāng)前對象的地址值作為 key ,對象的引用計數(shù)作為 Value链沼。
二十二默赂、weak 原理
- (void)viewDidLoad {
[super viewDidLoad];
__strong Person *person1;
__weak Person *person2;
Person *person3;
NSLog(@"111");
{
Person *person = [[MJPerson alloc] init];
//========第一種情況========
//如果只開啟該代碼,person在111括勺,222 之后釋放,調(diào)用dealloc曲掰。person1 指針指向 person疾捍,person1 調(diào)用 person 的set 方法進行了retain 操作,所以 person 的生命周期同 person1栏妖。
//person1 = person;
//========第二種情況========
//如果只開啟該代碼乱豆,person會在111,222 中間釋放吊趾。此時 person2 沒有強引用(retain) person宛裕。
//所謂的 weak 指針原理是指:如何做到對象(person)被銷毀之后,指向?qū)ο蟮?weak 指針(person2)立馬被清空论泛,置位 nil揩尸。
//person2 = person;
//========第三種情況========
//同第一種情況
//person3 = person;
}
NSLog(@"222");
}
@implementation Person
- (void)dealloc{
NSLog(@"%s", __func__);
}
@end
上述代碼如果開啟了person1 = person
person 會在輸出111,222 之后釋放屁奏,調(diào)用dealloc岩榆;如果開啟了person2 = person
person 會在111,222 中間釋放坟瓢;如果開啟person3 = person
,效果同第一種勇边。
-
__strong
是強引用,所以只有離開了viewDidLoad
方法后 person 對象才被釋放折联。 - 所謂的 weak 指針原理是指:如何做到對象(person)被銷毀之后(出了上述代碼中內(nèi)嵌的{ }之后)粒褒, 指向?qū)ο蟮?weak 指針(person2)立馬被清空,并被置位 nil诚镰。
- 默認是強引用奕坟。
weak 原理說明
- weak_table 是一個散列表,key 為對象地址怕享,value 為一個數(shù)組执赡,數(shù)組里面保存著指向該對象的所有弱指針。
- refcnts是一個存放著對象引用計數(shù)的散列表函筋。
一個對象可能會被多次弱引用沙合,當(dāng)這個對象被銷毀時,我們需要找到這個對象的所有弱引用跌帐,所以我們需要將這些弱引用的地址(即指針)放在一個容器里(比如數(shù)組)首懈。當(dāng)對象不再被強引用時需要銷毀的時候绊率,可以在 SideTable 中通過這個對象的地址找到引用值,首先清空引用值究履。同時滤否, SideTable
結(jié)構(gòu)中還有weak_table
,該結(jié)構(gòu)也是一個散列表最仑,key 為對象地址藐俺,value 為一個數(shù)組,里面保存著指向該對象的所有弱指針泥彤。當(dāng)對象釋放的時候欲芹,先清空引用哈希表RefcountMap
對應(yīng)的引用值,遍歷弱指針數(shù)組吟吝,依次將各個弱指針置為 nil菱父。
二十三、加鹽的意義
用戶設(shè)置的密碼復(fù)雜度可能不夠高剑逃,同時不同的用戶極有可能會使用相同的密碼浙宜,那么這些用戶對應(yīng)的密文也會相同,這樣蛹磺,當(dāng)存儲用戶密碼的數(shù)據(jù)庫泄露后粟瞬,攻擊者會很容易便能找到相同密碼的用戶,從而也降低了破解密碼的難度称开。因此亩钟,在對用戶密碼進行加密時,需要考慮對密碼進行掩飾鳖轰,即使是相同的密碼清酥,也應(yīng)該要保存為不同的密文,即使用戶輸入的是弱密碼蕴侣,也需要考慮進行增強焰轻,從而增加密碼被攻破的難度,而使用帶鹽的加密hash值便能滿足該需求昆雀。比如密碼原本是由字母和數(shù)字組成辱志,破解者僅需要在字母和數(shù)字中找答案。但是如果密碼中混淆了鹽(不僅僅只包含字母和數(shù)字)狞膘,破解者僅僅從字母和數(shù)字下手揩懒,肯定是找不到答案,無疑增加了破解難度。
筆者實際項目開發(fā)中,為了網(wǎng)絡(luò)安全鸥滨,請求參數(shù)按照一定的規(guī)則拼接成字符串嚷节,然后在字符串中加鹽,最后 MD5 簽名酌呆。后端依照同樣的規(guī)則校驗簽名继谚,若簽名值一致則通過校驗奏路。
二十四阔蛉、Shell 腳本
二十五弃舒、什么是User Agent
User Agent中文名為用戶代理,簡稱 UA状原,它是一個特殊字符串頭聋呢,使得服務(wù)器能夠識別客戶使用的操作系統(tǒng)及版本、CPU 類型颠区、瀏覽器及版本坝冕、瀏覽器渲染引擎、瀏覽器語言瓦呼、瀏覽器插件等。網(wǎng)站在手機端 app 打開和直接在瀏覽器中打開看到的內(nèi)容可能不一樣测暗,是因為網(wǎng)頁可以根據(jù) 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 調(diào) OC
JS 調(diào) OC ?目前主要的方式有三種:
- 通過 JSCore 中的 block
- 通過 JSCore 中的 JSExport
- 通過攔截 URL
在 JS 執(zhí)?行行環(huán)境中添加?一個 _OC_catch 的 block质和,那么在 JS 代碼中就可以直接調(diào)?用 _OC_catch 這 個函數(shù),當(dāng)在 JS 中調(diào)?用 _OC_catch 這個函數(shù)后稚字,我們剛才注冊的 block 就會被執(zhí)行饲宿。也就是通過 JS 成功的調(diào)?了 OC 代碼。
context[@"_OC_catch"] = ^(JSValue *msg, JSValue *stack) {
};
JSExport
可以導(dǎo)出 Objective-C 的屬性胆描、實例方法瘫想、類方法和初始化?方法到 JS 環(huán)境,這樣就可 以通過 JS 代碼直接調(diào)?用 Objective-C 昌讲。通過 JSExport 不僅可以導(dǎo)出?自定義類的方法国夜、屬性,也可以導(dǎo)出已有類的?方法短绸、屬性车吹。在導(dǎo)出過程中,類的方法名會被轉(zhuǎn)換成 JS 類型命名醋闭,第二個參數(shù)的第一個字?會被大寫窄驹,比如- (void)addX:(int)x andY:(int)y;
被轉(zhuǎn)為addXAndY(x, y)
。除此证逻,JSExport
還可以導(dǎo)出已有類的?方法乐埠、屬性。
通過攔截 URL,這種方式是 Web 端通過某種方式發(fā)送 URLScheme 請求饮戳,之后 Native 攔截到請求并根據(jù)URL SCHEME(包括所帶的參數(shù))進行相關(guān)操作豪治。類似于通過 SCHEME 喚起APP。這種方式的缺點是 url 長度有隱患扯罐,并且創(chuàng)建請求需要一定的耗時负拟,比注入 API 的方式調(diào)用同樣的功能。耗時會比較長歹河。所以還是更推薦使用注入 API 的方式掩浙。
OC 調(diào) JS
OC 調(diào) JS 主要有 UIWebView 、WKWebView 和 JSCore 這三種?方式秸歧。? UIWebView 的方式其實可以看作是 JSCore 的?方式厨姚。
- JSCore 方式
// 要執(zhí)行的 JS 代碼,定義一個 add 函數(shù)并執(zhí)?行行
NSString *addjs = @"function add(a, b) {return a + b;};add(1,3)";
// sumValue 為執(zhí)?行行后的結(jié)果
JSValue *sumValue = [self.context evaluateScript:addjs];
- UIWebView 方式
這種?方式說?白了了就是使?用 JSCore 键菱,通過 UIWebView 來獲取 JSContext 谬墙,這樣直接通過獲取到 context 來執(zhí)?行行 JS 代碼。
//通過 UIWebView 獲取 context
JSContext *context = [_webView
valueForKeyPath:@"documentView.webView.mainFrame.JSContext"];
// 要執(zhí)行的 JS 代碼经备,定義一個 add 函數(shù)并執(zhí)?行行
NSString *addjs = @"function add(a, b) {return a + b;};add(1,3)";
// sumValue 為執(zhí)?行行后的結(jié)果
JSValue *sumValue = [self.context evaluateScript:addjs];
- WKWebView 方式
WKWebView 沒有提供獲取 JSContext 的方法拭抬,但是它提供了執(zhí)行 JS 的方法evaluateJS:
,通過下面方法來執(zhí)行 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
,內(nèi)部有一個 UIPanGestureRecongnizer
手勢纷闺。 frame
是相對父視圖坐標(biāo)系來決定自己的位置和大小算凿,而bounds
是相對于自身坐標(biāo)系的位置和尺寸的。該視圖 bounds
的 origin
視圖本身沒有發(fā)生變化犁功,但是它的子視圖的位置卻發(fā)生了變化氓轰,因為 bounds
的 origin
值是基于自身的坐標(biāo)系,當(dāng)自身坐標(biāo)系的位置被改變了波桩,里面的子視圖肯定得變化戒努, bounds
和 panGestureRecognize
是實現(xiàn) UIScrollView
滑動效果的關(guān)鍵技術(shù)點。
frame和bounds對比:
參考
- frame很簡單镐躲,它的x储玫、y就是以當(dāng)前視圖的父視圖為參照確定當(dāng)前視圖的位置
- bounds的x、y則是當(dāng)前視圖的坐標(biāo)萤皂,并不影響當(dāng)前視圖的位置撒穷,但是對當(dāng)前視圖的子視圖有影響。當(dāng)前視圖的坐標(biāo)系統(tǒng)原點0,0默認為左上角裆熙,當(dāng)更改了bounds.origin 坐標(biāo)系統(tǒng)原點也會對應(yīng)的被改變端礼,由于當(dāng)前試圖的子視圖都是參照當(dāng)前視圖的原點進行布局禽笑,當(dāng)坐標(biāo)系統(tǒng)原點位置改變,其子視圖位置也會發(fā)生變化蛤奥。
二十八佳镜、--verbose 和 --no-repo-update
-
verbose
意思為 冗長的、啰嗦的凡桥,一般在程序中表示詳細信息蟀伸。此參數(shù)可以顯示命令執(zhí)行過程中都發(fā)生了什么。 -
pod install
或pod update
可能會卡在Analyzing dependencies
步驟缅刽,因為這兩個命令會升級 CocoaPods 的spec 倉庫
啊掏,追加該參數(shù)可以省略此步驟,命令執(zhí)行速度會提升衰猛。
二十九迟蜜、dataSource 和 delegate 的本質(zhì)區(qū)別
普遍開發(fā)者得理解是:一個是數(shù)據(jù),一個是操作啡省。如果從數(shù)據(jù)傳遞方向的角度來看娜睛,兩者的本質(zhì)是數(shù)據(jù)傳遞的方向不同。dataSource
是外部將數(shù)據(jù)傳遞到視圖內(nèi)卦睹,而 delegate
是將視圖內(nèi)的數(shù)據(jù)和操作等傳遞到外部微姊。實際開發(fā)封裝自定義視圖,可以參照數(shù)據(jù)傳遞方向分別設(shè)置 dataSource
和 delegate
分预。
三十、變種 MVC
真正的 MVC 應(yīng)該是蘋果提供的經(jīng)典UITableView
的使用薪捍,實際開發(fā)中經(jīng)常在 Cell
中引入Model
笼痹,本質(zhì)上來說不算是真正的 MVC ,只能算是 MVC 的變種酪穿。真正的 MVC 中 View 和 Model 應(yīng)該是完全隔離的凳干。蘋果的 MVC 中正是因為 View 沒有和任何 Model 綁定,所以 cell 的可沖擁堵高被济,但是缺點是代碼過于臃腫救赐。
三十一、函數(shù)指針和 Block
相同點:
- 二者都可以看成是一個代碼片段只磷。
- 函數(shù)指針類型和 Block 類型都可以作為變量和函數(shù)參數(shù)的類型(typedef定義別名之后经磅,這個別名就是一個類型)。
不同點:
- 函數(shù)指針只能指向預(yù)先定義好的函數(shù)代碼塊钮追,函數(shù)地址是在編譯鏈接時就已經(jīng)確定好的预厌。從內(nèi)存的角度看,函數(shù)指針只不過是指向代碼區(qū)的一段可執(zhí)行代碼元媚,而 block 本質(zhì)是 OC對象轧叽,是 NSObject的子類苗沧,是程序運行過程中在棧內(nèi)存動態(tài)創(chuàng)建的對象,可以向其發(fā)送copy消息將block對象拷貝到堆內(nèi)存炭晒,以延長其生命周期待逞。
補充:指針函數(shù)和函數(shù)指針的區(qū)別
指針函數(shù)是指帶指針的函數(shù),即本質(zhì)是一個函數(shù)网严,函數(shù)返回類型是某一類型的指針识樱。它是一個函數(shù),只不過這個函數(shù)的返回值是一個地址值屿笼。
int *f(x牺荠,y);
函數(shù)指針是指向函數(shù)的指針變量,即本質(zhì)是一個指針變量驴一。
int (*f) (int x); /*聲明一個函數(shù)指針 */
f = func; /* 將func函數(shù)的首地址賦給指針f */
三十二休雌、內(nèi)存(堆內(nèi)存)回收是什么意思
NSObject *obj = [[NSObject alloc] init];
代碼對應(yīng)的內(nèi)存布局如下,obj 指針存在于棧取肝断,obj 對象存在于堆區(qū)杈曲。obj 指針的回收由棧區(qū)自動管理,堆區(qū)的內(nèi)存需要開發(fā)者自己管理(MRC)情況胸懈。所謂的堆內(nèi)存回收并不是指將 obj 對象占有的內(nèi)存給挖去或是將空間數(shù)據(jù)清空為0担扑,而是指 obj 對象原本占有的空間可以被其他人利用(即其他指針可以指向該空間)。其他指針指向該空間時趣钱,重新初始化該空間涌献,將空間原有數(shù)據(jù)清零。
三十三首有、IP 和 MAC
IP 是地址燕垃,有定位功能;MAC 是身份唯一標(biāo)識井联,無定位功能卜壕;有了 MAC 地址為什么還要有 IP 地址?舉個例子烙常,現(xiàn)在我要和你通信(寫信給你)轴捎,地址用你的身份證號,信能送到你手上嗎蚕脏? 明顯不能侦副!身份證號前六位能定位你出生的縣,MAC 地址前幾位也可以定位生產(chǎn)廠家驼鞭。但是你出生后會離開這個縣(IP 地址變動)跃洛,哪怕你還在這個縣,我總不能滿大街喊著你的身份證號去問路邊人是否認識這個身份證號的主人终议,所以此刻需要借助 IP 的定位功能汇竭。
三十四葱蝗、MD5 相關(guān)小知識
具體可參考筆者之前文章 iOS 簽名機制,文章中可以找到答案细燎。
三十五两曼、響應(yīng)鏈問題
命中測試和響應(yīng)鏈問題
手勢、UIControl玻驻、UITouch系列事件關(guān)系
過程:
觸屏事件的處理被分成兩個階段:查找響應(yīng)者(a)和響應(yīng)者處理(b悼凑、c、d)璧瞬。
- a.先將事件由上向下(從父控件向子控件)傳遞户辫,找到最合適處理事件的控件。如果是同一級別的視圖嗤锉,先調(diào)用后添加的視圖 hitTest渔欢,再調(diào)用先添加視圖的 hitTest。尋找最合適的視圖之所以從 Window 開始瘟忱,是因為界面渲染本身是圖層樹的結(jié)構(gòu)奥额,遍歷從樹的根節(jié)點開始,才可以獲取到各個子節(jié)點信息访诱。
- b.調(diào)用最合適控件的 touches 系列方法垫挨。
- c.如果調(diào)用 [super touches] 方法,就會將事件順著響應(yīng)鏈條向上傳遞触菜,傳遞給上一個響應(yīng)者九榔。
- d.接著調(diào)用上一個響應(yīng)者的touches方法
hitTest 內(nèi)部實現(xiàn)代碼還原
- (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]) {
//轉(zhuǎn)換坐標(biāo)到子視圖
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
//遞歸調(diào)用hitTest:withEvent繼續(xù)判斷
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
//在這里打印self.class可以看到遞歸返回的順序。
return hitTestView;
}
}
//這里就是該視圖沒有子視圖了 點在該視圖中涡相,所以直接返回本身帚屉,上面的hitTestView就是這個。
NSLog(@"命中的view:%@",self.class);
return self;
}
//不在這個視圖直接返回nil
return nil;
}
響應(yīng)鏈?zhǔn)鞘裁矗?/h5>
其實響應(yīng)鏈就是在命中測試中漾峡,走通的路徑。用上個章節(jié)的例子喻旷,整個命中測試的走向是:A? --> D? --> B? --> C?生逸,我們把沒走通的?的去掉,以第一響應(yīng)者 B 作為頭且预,依次連接槽袄,響應(yīng)鏈就是:B -> A。(實際上 A 后面還有控制器等锋谐,但在該例子中沒有展示控制器等遍尺,所以就寫到 A)
默認來說,若該結(jié)點是 UIView 類型的話涮拗,這個 next 屬性是該結(jié)點的父視圖乾戏。但也有幾個例外:
- 如果是 UIViewController 的根視圖迂苛,則下一個響應(yīng)者是 UIViewController。
- 如果是 UIViewController:
如果 UIViewController 的視圖是 UIWindow 的根視圖鼓择,則下一個響應(yīng)者是 UIWindow 對象三幻;如果 UIViewController 是由另一個 UIViewController 呈現(xiàn)的,則下一個響應(yīng)者是第二個 UIViewController呐能。 - UIWindow的下一個響應(yīng)者是 UIApplication念搬。
- UIApplication 的下一個響應(yīng)者是 app delegate。但僅當(dāng)該 app delegate 是 UIResponder 的實例且不是 UIView摆出、UIViewController 或 app 對象本身時朗徊,才是下一個響應(yīng)者。
下面舉個例子來說明偎漫。如下圖所示爷恳,觸摸點是,那根據(jù)命中測試骑丸,B 就成為了第一響應(yīng)者舌仍。由于 C 是 B 的父視圖、A 是 C 的父視圖通危、同時 A 是 Controller 的根視圖铸豁,那么按照規(guī)則,響應(yīng)鏈就是這樣的:視圖 B -> 視圖 C -> 根視圖 A -> UIViewController 對象 -> UIWindow 對象 -> UIApplication 對象 -> App Delegate
獲取到響應(yīng)鏈后菊碟,觸摸事件首先將會由第一響應(yīng)者響應(yīng)节芥,首先觸發(fā)第一響應(yīng)者的touchBegin 方法。
應(yīng)用1:子View超出父View的情況逆害,子 View 依舊能響應(yīng)事件头镊。
重載父 view 的 -(UIView *)hitTest: withEvent:
方法,去掉點擊必須在父 view 內(nèi)的坐標(biāo)判斷邏輯魄幕,子 view 就能成為最合適的視圖相艇,用于響應(yīng)事件了。注意內(nèi)部的坐標(biāo)轉(zhuǎn)化:判斷點擊的點是包含子視圖纯陨,如果包含子視圖坛芽,則調(diào)用子視圖的 hitTest 方法。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
/**
* 此注釋掉的方法用來判斷點擊是否在父View Bounds內(nèi)翼抠,
* 如果不在父view內(nèi)咙轩,就會直接不會去其子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;
}
應(yīng)用2:穿透子 View 點擊父 View阴颖。
子View覆蓋在父View上活喊,但是要實現(xiàn)穿透子View去響應(yīng)父View點擊事件。解決方法時量愧,重寫子 View 的 hitTest 方法钾菊。點擊的是自身則返回nil, 此時最合適響應(yīng)者轉(zhuǎn)為父類帅矗。
-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *hitView =[super hitTest:point withEvent:event];
if(hitView == self){
//自動將事件傳遞到上一層(父視圖),自身不做事件處理
return nil;
}
return hitView;
}
應(yīng)用3:如何實現(xiàn)點擊子視圖结缚,父視圖和子視圖同時響應(yīng)损晤?
方法一:子視圖重寫以下 touch 系列方法,通過 self.nextResponder
找到父視圖红竭,并調(diào)用父視圖的 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 相關(guān)方法,也可以通過- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
方法茵宪,同時響應(yīng)父視圖和子視圖的事件最冰。此方法返回YES,手勢事件會一直往下傳遞,不論當(dāng)前層次是否對該事件進行響應(yīng)稀火。
應(yīng)用4:擴大 button 熱區(qū)
重載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;
}
三十六凰狞、什么是線程不安全篇裁?線程不安全的本質(zhì)原因?
不能確定代碼的運行順序和結(jié)果赡若,是線程不安全的达布。線程安全是相對于多線程而言的,單線程不會存在線程安全問題逾冬。因為單線程代碼的執(zhí)行順序是唯一確定的黍聂,進而可以確定代碼的執(zhí)行結(jié)果。
線程不安全的本質(zhì)原因在于:表面展現(xiàn)在我們眼前的可能是一行代碼身腻,但轉(zhuǎn)換成匯編代碼后可能對應(yīng)多行产还。當(dāng)多個線程同時去訪問代碼資源時,代碼的執(zhí)行邏輯就會發(fā)生混亂嘀趟。如數(shù)據(jù)的寫操作脐区,底層實現(xiàn)可能是先讀取,再在原有數(shù)據(jù)的基礎(chǔ)上改動她按。如果此時有一個讀操作牛隅,原本意圖是想在寫操作完畢之后再讀取數(shù)據(jù),但不巧的這個讀操作剛好發(fā)生在寫操作執(zhí)行的中間步驟中尤溜。雖然讀操作后與寫操作執(zhí)行,但數(shù)據(jù)讀取的值并不是寫操作的結(jié)果值汗唱,運氣不好時還可能發(fā)生崩潰宫莱。
- (void)viewDidLoad {
[super viewDidLoad];
int a = 100;
a += 200;
NSLog(@"%d",a);
}
如上述代碼中的int a = 100;
和a += 200;
轉(zhuǎn)換的匯編代碼,為下面中間八行匯編代碼哩罪。
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函數(shù)階段,一般啟動時間的優(yōu)化也是從這三大步著手辛辨。
- dyld階段:dyld(dynamic link editor)是Apple的動態(tài)鏈接器捕捂,可以用來裝載 Mach-O 文件(可執(zhí)行文件、動態(tài)庫等)斗搞。啟動APP時指攒,dyld 首先裝載可執(zhí)行文件,同時會遞歸加載所有依賴的動態(tài)庫(如果不加載動態(tài)庫僻焚,可能會報找不到符號錯誤)允悦。
- runtime 階段:首先解析可執(zhí)行文件,進行各種objc結(jié)構(gòu)的初始化(注冊O(shè)bjc類 虑啤、初始化類對象等等)隙弛。之后調(diào)用所有類和分類的
+load
方法,attribute((constructor)) 修飾的函數(shù)的調(diào)用狞山、創(chuàng)建 C++ 靜態(tài)全局變量全闷。到此為止,可執(zhí)行文件和動態(tài)庫中所有的符號(Class铣墨、Protocol室埋、Selector、IMP …)都已經(jīng)按格式成功加載到內(nèi)存中伊约,被runtime 所管理姚淆。 - main函數(shù)階段:所有初始化工作結(jié)束后,dyld就會調(diào)用main函數(shù)屡律。
三十八腌逢、包體積優(yōu)化中的內(nèi)聯(lián)函數(shù)
在關(guān)于 App 包體積優(yōu)化的一些博客文章中,偶爾看到包體積的優(yōu)化可以從 C++ 入手超埋,其中有一條是減少內(nèi)聯(lián)函數(shù)的使用搏讶。問題來了,什么是內(nèi)聯(lián)函數(shù)霍殴?為什么要減少內(nèi)聯(lián)函數(shù)的使用媒惕?它和一般函數(shù)有什么異同點?和宏相比有什么異同點来庭?
內(nèi)聯(lián)函數(shù)關(guān)鍵字是 inline 妒蔚,C++ 中普通函數(shù)使用的申明或?qū)崿F(xiàn)使用inline 修飾后,即為內(nèi)聯(lián)函數(shù)。注意:遞歸函數(shù)即使被 inline 修飾后也不是內(nèi)聯(lián)函數(shù)肴盏,依然是普通函數(shù)科盛。
inline int sum(int a, int b){
return a + b;
}
普通函數(shù)調(diào)用會開辟一段棧空間執(zhí)行相關(guān)代碼菜皂,函數(shù)執(zhí)行完再將對應(yīng)的椪昝啵空間回收。而內(nèi)聯(lián)函數(shù)調(diào)用中恍飘,編譯器會將函數(shù)調(diào)用直接展開為函數(shù)代碼榨崩。如cout << sum(1, 2) << endl
會直接轉(zhuǎn)換為cout << 1 + 2<< endl
,由此可見內(nèi)聯(lián)函數(shù)和一般的宏很類似常侣,都是直接替換相關(guān)代碼蜡饵。同宏相比,內(nèi)聯(lián)函數(shù)只是多了一些函數(shù)特性和語法檢測功能胳施。
OC 中可以通過關(guān)鍵字 NS_INLINE
使用內(nèi)聯(lián)函數(shù)溯祸。
NS_INLINE void log(int value) {
NSLog(@"%d", value);
}
綜上,內(nèi)聯(lián)函數(shù)或宏省去了參數(shù)壓棧舞肆、生成匯編語言的CALL調(diào)用焦辅、返回參數(shù)、執(zhí)行return等過程椿胯,可以減少函數(shù)調(diào)用的開銷筷登。但是會增加代碼體積,所以減少內(nèi)聯(lián)函數(shù)或宏的使用一定程度上可以減少包體積哩盲。但并不是說為了減小包體積完全不去使用內(nèi)聯(lián)函數(shù)前方,建議經(jīng)常會被調(diào)用的代碼,且代碼量不是很多的時候(不超過10行)廉油,為減少函數(shù)調(diào)用的開銷惠险,可適當(dāng)使用內(nèi)聯(lián)函數(shù)。
三十九抒线、super 本質(zhì)
objc_msgSend 方法參數(shù)
LGPerson *person = [LGPerson alloc];
[person sayHello];
//2.消息發(fā)送
objc_msgSend(person, sel_registerName("sayHello"));
有兩個類 Animal 和 Cat 班巩,其中 Cat 繼承自 Animal 類,在 Cat 類實現(xiàn)如下代碼嘶炭,試問打印結(jié)果是什么抱慌?
@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
上述代碼打印結(jié)果一次為: Cat Animal Cat Animal,前兩個結(jié)果不足為奇眨猎,后兩個結(jié)果似乎有點費解抑进。
super 調(diào)用底層會轉(zhuǎn)換為objc_msgSendSuper
函數(shù)的調(diào)用,objc_msgSendSuper
函數(shù)接收 2 個參數(shù) objc_super
結(jié)構(gòu)體和 SEL
睡陪,objc_super
結(jié)構(gòu)如下:
struct objc_super {
__unsafe_unretained _Nonnull id receiver; // 消息接收者
__unsafe_unretained _Nonnull Class super_class; // 消息接收者的父類
};
[super class]
在調(diào)用過程中弦悉,底層轉(zhuǎn)化為 objc_msgSendSuper({self, [Animal class]}, @selector(class));
,同 objc_msgSend
函數(shù)相比相當(dāng)于多了第二個參數(shù)柠偶,但消息接收者仍然是 self
螺捐,所以打印結(jié)果為 Cat
。
the superclass at which to start searching for the method implementation.
objc_msgSendSuper
方法中的第二個參數(shù)主要作用是告訴從哪里開始搜索方法實現(xiàn)逮矛,一般傳入的是父類鸡号。這也是實際開發(fā)中 [super superClassMethod]
直接調(diào)用父類方法的原因。但是如果按照這一點來看须鼎,依然和上述打印結(jié)果不符合鲸伴。因此需要看 class 和 superClass 方法的內(nèi)部實現(xiàn),class 方法的內(nèi)部實現(xiàn)返回消息接收者晋控,上述代碼消息接收者為 Cat 汞窗,因此打印結(jié)果仍然是 Cat。superClass 方法內(nèi)部實現(xiàn)是返回消息接受者的父類赡译,因此打印結(jié)果是 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
調(diào)用 Student 的 test 方法,最終打印結(jié)果都是 Student,Person,Student,Person蝌焚。因為實例對象一直是 Student 對應(yīng)的實例對象裹唆,并非是 Person 的實例對象。
四十只洒、引用的本質(zhì)(引用和指針的區(qū)別)
待更新许帐。。毕谴。成畦。。涝开。
四十一循帐、渲染框架分類
說實在的有時會對各種渲染框架感覺混亂,一會CA忠寻、一會CG等等惧浴,于是就把這些渲染框架簡單匯總了下。
- 1奕剃、UIKit & AppKit :這個不多說衷旅。
- 2、Core Animation:UIView底下封裝了一層CALayer樹纵朋,Core Animation 層是真正的渲染層柿顶,我們之所以能在屏幕上看到內(nèi)容,真正的渲染工作是在 Core Animation 層的操软。
- 3嘁锯、Core Graphics:用于運行時繪制圖像。可以繪制路徑家乘、顏色蝗羊,當(dāng)開發(fā)者需要在運行時創(chuàng)建圖像時,可以使用 Core Graphics 去繪制仁锯。
- 4耀找、 Core Image:用來處理已經(jīng)創(chuàng)建的圖像。該框架擁有一系列現(xiàn)成的圖像過濾器业崖,能對已存在的圖像進行高效的處理野芒。
- 5、SceneKit & SpriteKit:普通開發(fā)者可能會對 SceneKit 和 SpriteKit 感到陌生双炕,SceneKit 主要用于某些 3D 場景需求狞悲,而 SpriteKit 更多的用于游戲開發(fā)。SceneKit 和 SpriteKit 都包含了粒子系統(tǒng)妇斤、物理引擎等摇锋,即使是非游戲應(yīng)用中也可以使用它們來完成一些比較炫酷的特效和物理模擬。
- 6站超、Metal:Metal 存在于以上渲染框架的最底層乱投。Core Animation、Core Image顷编、SceneKit戚炫、SpriteKit等等渲染框架都是構(gòu)建于 Metal 之上的。
四十二媳纬、耗時代碼定位
實際開發(fā)中可能會遇到嚴重線程阻塞的情況双肤,比如筆者之前就遇到過使用 MJ 下拉刷新,刷新完畢后 MJ 復(fù)位無動畫效果钮惠,第一猜測就是有阻塞茅糜,于是借助 Product --> Profile-->TimeProfiler
工具 第一時間定位到耗時較多的代碼。結(jié)果發(fā)現(xiàn)在渲染 Cell 的時候動態(tài)的調(diào)用了蘋果接口中 html
轉(zhuǎn)屬性文本的方法素挽,該方法的解析異常耗時蔑赘。可按照下圖設(shè)置 Call Tree 预明,方便定位耗時代碼缩赛。
四十三、如何給百萬數(shù)據(jù)排序
桶排序定義
給百萬數(shù)據(jù)排序可以用"桶排序"撰糠,核心思想是將數(shù)據(jù)分到幾個有序的桶酥馍,每個桶里的數(shù)據(jù)再單獨進行排序。桶內(nèi)排完序之后阅酪,再把每個桶里的數(shù)據(jù)按照順序依次取出旨袒,組成的序列就是有序的了汁针。
桶排序時間復(fù)雜度
如果要排序的數(shù)據(jù)為 n 個,均勻地劃分到 m 個桶內(nèi)砚尽,每個桶里就有 k = n/m
個元素施无。每個桶內(nèi)部使用快速排序,則每個桶內(nèi)時間復(fù)雜度為 O(k * logk)
必孤。m 個桶排序的時間復(fù)是 O(m * k * logk)
帆精,因為 k = n/m
,所以整個桶排序的時間復(fù)雜度就是 O(n*log(n/m))
隧魄。當(dāng)桶的個數(shù) m 接近數(shù)據(jù)個數(shù) n 時,log(n/m)
就是非常小的常量隘蝎,這個時候桶排序的時間復(fù)雜度接近 O(n)
购啄。
桶排序缺點
桶排序?qū)σ判驍?shù)據(jù)的要求是非常苛刻的嘱么。
- 1狮含、桶與桶之間有著大小順序。這樣每個桶內(nèi)的數(shù)據(jù)都排序完之后曼振,桶與桶之間的數(shù)據(jù)不需要再進行排序几迄。
- 2、數(shù)據(jù)在各個桶之間的分布是比較均勻的冰评。如果數(shù)據(jù)經(jīng)過桶的劃分之后映胁,有些桶里的數(shù)據(jù)非常多,有些非常少甲雅,很不平均解孙。時間復(fù)雜度就不是常量級了。極端情況下抛人,如果數(shù)據(jù)都被劃分到一個桶里弛姜,那就退化為
O(nlogn)
的排序算法了。
內(nèi)存不足時妖枚,如何排序廷臼?
假設(shè)有 10GB 的訂單數(shù)據(jù)需要排序,內(nèi)存有限绝页,只有幾百 MB荠商,沒辦法一次性把 10GB 的數(shù)據(jù)都加載到內(nèi)存中。先掃秒訂單知道金額最小是 1 元续誉,最大是 10 萬元结啼。可以將訂單劃到100個桶內(nèi)屈芜,第一個桶我們存儲金額在 1 元到 1000 元之內(nèi)的訂單郊愧,第二桶存儲金額在 1001 元到 2000 元之內(nèi)的訂單朴译,以此類推。但是訂單的數(shù)據(jù)分布可能并不是非常均勻属铁,某些桶內(nèi)的數(shù)據(jù)依然是大于內(nèi)存空間眠寿,此時可以將該桶內(nèi)的數(shù)據(jù)再次進行劃分,直到能加載到內(nèi)存為止焦蘑。
四十四盯拱、自旋鎖 & 互斥鎖
線程安全中為了實現(xiàn)線程阻塞,一般有兩種方案:一種是讓線程處于休眠狀態(tài)例嘱,此時不會消耗 CPU 資源狡逢;另一種方案是讓線程忙等或空轉(zhuǎn),此時會消耗一定的 CPU 資源拼卵。前者屬于互斥奢浑,后者屬于自旋。
自旋鎖
自旋鎖是一種特殊的互斥鎖腋腮,自旋在線程加鎖的情況下雀彼,會一直嘗試是否解鎖,如果沒有解鎖即寡,會一直循環(huán)判斷徊哑,如果鎖已經(jīng)放開,則繼續(xù)執(zhí)行聪富,不再是空轉(zhuǎn)狀態(tài)莺丑。
優(yōu)點:
- 循環(huán)檢查資源持有者是否已經(jīng)釋放了資源,這樣做的好處是減少了線程從睡眠到喚醒的資源消耗墩蔓,但會一直占用CPU的資源窒盐。
缺點:
-
OSSpinLock
屬于自旋鎖,Pthred 庫中相關(guān)的鎖钢拧,以及NSLock
蟹漓、@synchronized
等都屬于互斥鎖。OSSpinLock
目前已經(jīng)不再安全源内,因為會出現(xiàn)優(yōu)先級反轉(zhuǎn)問題葡粒。 現(xiàn)代操作系統(tǒng)一般采用 時間片輪轉(zhuǎn)算法 調(diào)度進程或線程,按照線程的優(yōu)先級為不同的線程分配不同的時間膜钓,優(yōu)先級越高分配的時間片越多嗽交。假設(shè)有兩個線程 thread1 和 thread2,其中 thread1 的優(yōu)先級高于 thread2颂斜,即thread1 分配的時間片多余 thread2夫壁。如果 thread2 正在鎖內(nèi)安全執(zhí)行,一段時間后 thread1 執(zhí)行任務(wù)時沃疮,發(fā)現(xiàn)鎖未打開盒让,于是會處于忙等狀態(tài)梅肤。由于thread1 的優(yōu)先級高于 thread2,此時系統(tǒng)會分配更多的時間片給 thread1邑茄,thread2 時間片減少姨蝴,遲遲不能完成,thread1 卻一直等待肺缕。如此就造成線程優(yōu)先級反轉(zhuǎn)左医。 - 由于一直忙等,所以忙等的過程會消耗 CPU 資源同木。
自旋鎖和互斥鎖適用場景:
什么時候用自旋鎖比較劃算
- 預(yù)計線程等待鎖的時間很短 浮梢。如YYCache 中的內(nèi)存緩存
- 加鎖的代碼(加鎖部分的代碼也稱為臨界區(qū))經(jīng)常被調(diào)用,但競爭情況很少發(fā)生(如果競爭比較多自旋鎖可能會發(fā)生優(yōu)先級反轉(zhuǎn)問題)彤路。
- CPU資源不緊張(如果 CPU 資源比較緊張秕硝,再加上自旋鎖會一直占用 CPU 資源,會帶來更差的體驗)
- 多核處理器
什么時候用互斥鎖比較劃算
- 預(yù)計線程等待鎖的時間較長
- 單核處理器(為單核處理器時要避免占用更多的CPU資源斩萌,避免CPU空轉(zhuǎn)輪詢 )
- 臨界區(qū)有IO操作(因為 IO 操作比較占用資源,所以要使用占用資源更少的互斥鎖)
- 臨界區(qū)代碼復(fù)雜或者循環(huán)量大
- 臨界區(qū)競爭非常激烈(使用互斥鎖可以減少資源占用)
四十五屏轰、應(yīng)用 Crash 時為什么對操作系統(tǒng)無影響颊郎?
雙模式、I/O 保護和內(nèi)存保護霎苗、定時器三者是確保操作系統(tǒng)能夠運行的關(guān)鍵技術(shù)姆吭,可以避免外界應(yīng)用崩潰對操作系統(tǒng)的影響。
雙模式
為了保證操作系統(tǒng)不受其它故障程序的影響唁盏,進而產(chǎn)生系統(tǒng)崩潰的可能内狸。一種常用的辦法是引入雙重模式,即用戶模式和內(nèi)核模式厘擂。內(nèi)核模式只能運行操作系統(tǒng)的程序昆淡。所有的用戶應(yīng)用程序只能在用戶模式下運行。 雙模式需要CPU的支持刽严,如果CPU有模式位昂灵,則可以在操作系統(tǒng)中實現(xiàn)雙模式,目前主流的CPU基本都有模式位舞萄。雙模式允許操作系統(tǒng)不受其它故障應(yīng)用程序的影響眨补。特權(quán)指令是指可能引起崩潰的指令,該指令只能運行在內(nèi)核模式中倒脓。 如果用戶程序需要使用特權(quán)指令撑螺,可以通過系統(tǒng)提供的API調(diào)用。I/O保護和內(nèi)存保護
定義所有I/O指令為特權(quán)指令崎弃,用戶應(yīng)用程序無法直接訪問I/O指令甘晤,只能通過系統(tǒng)調(diào)用進行I/O操作含潘,從而避免非法I/O操作。
利用基址寄存器和限長寄存器隔離不同程序的內(nèi)存地址安皱。定時器
如果用戶程序死循環(huán)或用戶程序不調(diào)用系統(tǒng)調(diào)用调鬓,此時操作系統(tǒng)將無法獲得CPU并對系統(tǒng)進行管理。解決方法是引入定時器酌伊,在一段時間后發(fā)生中斷腾窝,將CPU控制權(quán)返回給操作系統(tǒng)。
四十六居砖、硬盤重量會隨著存儲數(shù)據(jù)大小而變化嗎虹脯?
如果是磁盤重量不變,如果是 SSD硬盤(固態(tài)硬盤)會受到影響奏候。
磁硬盤能存儲數(shù)據(jù)靠的是里面的磁鐵的方向改變循集。一個長條形狀的磁鐵有南極和北極兩個端,一種端代表0蔗草,另一端代表1咒彤,然后通過 01 不同的組合代表不同的意義,只要磁鐵足夠多咒精,就能用它們排列的順序代表所有的信息镶柱,數(shù)據(jù)就是這樣存在磁硬盤中的。所以磁硬盤重量不會收存儲數(shù)據(jù)大小的影響模叙。
SSD 內(nèi)部有上萬億個小單元歇拆,每個單元表示 0 還是 1,取決于這個單元里裝了多少個電子范咨,比如裝進去100個電子后故觅,這個單元就代表 1 ,低于這個數(shù)值就代表 0渠啊。所以對SSD的重量會受到內(nèi)部電子的影響输吏。一個電子是0.000000……9公斤,30個零替蛉。2TB的數(shù)據(jù)至少要用2×10^13個電子评也。質(zhì)量大約就是0.0000000000002公斤,12個零灭返。
四十七盗迟、如何消除小數(shù)誤差
小數(shù)誤差的原因:
計算機之所以會出現(xiàn)運算錯誤的原因是因為一些小數(shù)無法轉(zhuǎn)換二進制數(shù),例如 0.1 就無法用二進制數(shù)正確表示熙含。下圖說明了小數(shù)的二進制小數(shù)表達方式罚缕,小數(shù)的表示方式和整數(shù)表示方式類似。
消除小數(shù)誤差:
把小數(shù)擴大對應(yīng)的倍數(shù)怎静,轉(zhuǎn)成整數(shù)進行計算邮弹。計算機在進行小數(shù)計算時可能會出錯黔衡,但是在計算整數(shù)的時候,只要不超過可處理數(shù)值的范圍一定不會出現(xiàn)問題腌乡。
四十八盟劫、運行時是否是 OC 的專利?
runtime 并非是 Objective-C 的專利与纽,絕大多數(shù)語言都有這個概念侣签,runtime 就是動態(tài)庫(運行時庫)的一部分。比如 C 語言中 glibc 動態(tài)鏈接庫通常會被很多操作依賴急迂,包括字符串處理(strlen影所、strcpy)、信號處理僚碎、socket猴娩、線程、IO勺阐、動態(tài)內(nèi)存分配等等卷中。由于每個程序都依賴于運行時庫,這些庫一般都是動態(tài)鏈接的渊抽。這樣一來蟆豫,運行時庫可以存儲在操作系統(tǒng)中,很多程序共享一個動態(tài)庫腰吟,這樣就可以節(jié)省內(nèi)存占用空間和應(yīng)用程序大小无埃。
補充:鏈接一般分為靜態(tài)鏈接和動態(tài)鏈接徙瓶。一般說的預(yù)編譯毛雇、編譯、匯編侦镇、鏈接灵疮,其中的鏈接是指靜態(tài)鏈接。所謂的動態(tài)鏈接是指: 鏈接過程被推遲到運行時再進行壳繁。
四十九震捣、線程保活
- (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");
}
直接執(zhí)行上述代碼闹炉,在輸出 1 之后蒿赢,會直接崩潰。主要原因在于渣触,執(zhí)行完 [thread start]
后羡棵,線程立馬被殺死。此時再次在線程中調(diào)用 test
方法會直接崩潰嗅钻。 解決該問題的思路主要是保證線程的生命周期皂冰,即線程钡暾梗活。AFN 中秃流,異步網(wǎng)絡(luò)發(fā)起請求赂蕴,請求回來之后,線程依然沒有被殺死舶胀,也是利用了線程备潘担活技術(shù)。代碼如下:
- (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");
}
五十峻贮、包體積優(yōu)化總結(jié)
主要從以下四個方面作總結(jié)資源文件席怪、源代碼、編譯參數(shù)配置以及蘋果自身優(yōu)化纤控。
資源文件
- 1挂捻、檢測未使用的圖片:開源項目LSUnusedResources或腳本工具
- 2、圖片壓縮處理imageoptim船万。順便補充一些很多電商網(wǎng)站里面涉及大量的商品圖片刻撒,為了節(jié)省流量通常可以使用 webp 格式圖片耿导,webp 格式圖片不僅僅體積小声怔,還支持 gif 格式,可參考這里舱呻。
- 3醋火、簡單的圖片可以使用代碼自動生成。很類似的圖片箱吕,僅僅只有顏色不同芥驳,可以通過代碼處理圖片的顏色。
- 4茬高、啟動圖和偽啟動圖不要直接在資源文件中保留兩份兆旬,偽啟動圖容器可以通過代碼獲取啟動圖資源。一次實際優(yōu)化過程中怎栽,在該點下手丽猬,包體積立馬減少了 4M 左右。
- 5熏瞄、Xcode 中也會有一些圖片相關(guān)設(shè)置脚祟,Compress PNG Files 和 Remove Text Medadata From PNG Files。前者打包的時候自動對圖片進行無損壓縮强饮,后者會移除 PNG 圖像名稱由桌、作者、版權(quán)、創(chuàng)作時間沥寥、注釋等信息碍舍。
- 6、部分資源文件還可以通過后端下發(fā)方式邑雅。
- 7片橡、iconFont 替換部分圖標(biāo)和文字』匆埃可參考該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、注意控制宏和內(nèi)聯(lián)函數(shù)编丘∮胙В可看本文的第三十八個小知識點包體積優(yōu)化中的內(nèi)聯(lián)函數(shù)。
- 4嘉抓、LinkMap 可以得出每個類或者庫所占用的空間大兴魇亍(代碼段+數(shù)據(jù)段),方便開發(fā)者快速定位需要優(yōu)化的類或靜態(tài)庫掌眠。
- 5蕾盯、OC 中項目中 Debug 代碼即使沒有使用幕屹,也沒有導(dǎo)入頭文件蓝丙,依然會增加包體積,因為 OC 是基于運行時機制望拖,編譯器無法確定哪些代碼將來是否會使用渺尘。但是 Swift 不同 OC,Swift 是靜態(tài)的说敏,在編譯階段編譯器優(yōu)化就可以去除無用代碼鸥跟。
- 6、使用輕量級三方庫。
編譯參數(shù)配置
1医咨、Optional Level-->Fastest,Smallest[-OS]:含義可以參照該篇文章 2.1 小節(jié)枫匾。
2、Link-Time Optimization : 它是 LLVM 編譯器的一個特性拟淮,用于在 link 中間代碼時干茉,對全局代碼進行優(yōu)化。這個優(yōu)化是自動完成的很泊,因此不需要修改現(xiàn)有的代碼角虫。蘋果使用了新的優(yōu)化方式 Incremental,大大減少了鏈接的時間委造。筆者在實際的項目開發(fā)中開啟這個配置后戳鹅,包體積減少了 4 - 5M 左右。
3昏兆、Deployment Postprocessing枫虏、Strip Linked Product、Strip Debug Symbols During Copy爬虱、Symbols hidden by default 四者設(shè)置為 YES 后可以去掉不必要的符號信息模软,減少可執(zhí)行文件大小。但去除了符號信息之后我們就只能使用 dSYM 來進行符號化了饮潦,所以需要將 Debug Information Format 修改為 DWARF with dSYM file燃异。
其它、Dead Code Stripping(僅對靜態(tài)語言有效):刪除靜態(tài)鏈接的可執(zhí)行文件中未引用的代碼继蜡。Debug 設(shè)置為 NO回俐, Release 設(shè)置為 YES 可減少可執(zhí)行文件大小。Xcode 默認會開啟此選項稀并,C/C++/Swift 等靜態(tài)語言編譯器會在 link 的時候移除未使用的代碼仅颇,但是對于 Objective-C 等動態(tài)語言是無效的。因為 Objective-C 是建立在運行時上面的碘举,底層暴露給編譯器的都是 Runtime 源碼編譯結(jié)果忘瓦,所有的部分應(yīng)該都是會被判別為有效代碼。
其它引颈、Generate Debug Symbols(有作用但不建議修改): 當(dāng) Generate Debug Symbol s選項設(shè)置為 YES時耕皮,每個源文件在編譯成 .o 文件時,編譯參數(shù)多了 -g 和 -gmodules 兩項蝙场。打包會生成 symbols 文件凌停。設(shè)置為 NO 則 ipa 中不會生成 symbol 文件,可以減少 ipa 大小售滤。但會影響到崩潰的定位罚拟。保持默認的開啟台诗,不做修改。
蘋果自身優(yōu)化
- 1赐俗、Slicing : 創(chuàng)建拉队、分發(fā)不同變體以適應(yīng)不同目標(biāo)設(shè)備的過程,App Slicing 僅向設(shè)備傳送與之相關(guān)的資源(取決于屏幕分辨率阻逮,架構(gòu)等等)氏仗。如 2x 和 3x 的圖片放在 Asset Catalog 中會自動管理僅保留合適的圖片。但 Bundle 內(nèi)則會同時包含2x 和 3x 夺鲜。所以資源圖片盡可能放在 Asset Catalog 中皆尔。代碼資源會對應(yīng)不同的設(shè)備生成不同的執(zhí)行文件挨措。如果用心的話绵脯,還會發(fā)現(xiàn) AppStrore 中同一款應(yīng)用在不同設(shè)備上顯示的包體積大小不同拇舀。
注意 : 代碼架構(gòu)的拆分主要是由 Slicing 完成的毁渗,并非是 Bitcode 演痒。Bitcode 的優(yōu)勢更多體現(xiàn)在性能售担、以及后續(xù)的維護上嘶摊。如果開啟了 Bitcode铅匹,以后 Apple 推出了新的 CPU 架構(gòu)(不是指新iPhone設(shè)備)或者以后 LLVM 推出了一系列優(yōu)化仅胞,我們也不再需要為其發(fā)布新的安裝包了每辟,Apple Store 會為我們自動完成這步。