1.1 談一談GCD和NSOperation的區別?
- 首先二者都是多線程相關的概念,當然在使用中也是根據不同情境進行不同的選擇;
- GCD是將任務添加到隊列中(串行/并發/主隊列),并且制定任務執行的函數(同步/異步),其性能最好,底層是C語言的API,也更輕量級。iOS4.0以后推出的,針對多核處理器的并發技術,只能設置某一個隊列的優先級,其高級功能有一次性執行dispatch_once,延遲操作dispatch_after,調度組等等;
- NSOperation把操作(異步)添加到隊列中(全局的并發隊列),是OC框架,更加面向對象,是對GCD的封裝,iOS2.0推出,蘋果推出GCD之后,對NSOperation的底層全部重寫,可以隨時取消已經設定準備要執行的任務,已經執行的除外,可以設置隊列中每一個操作的優先級,其高級功能可以設置最大操作并發數,繼續/暫停/全部取消,可以快隊列設置操作的依賴關系。
1.2 談談多線程的應用
通常耗時的操作都放在子線程處理,然后到主線程更新UI,如
- 我們要從數據庫提取數據還要將數據分組后顯示,那么就會開個子線程來處理,處理完成后才去刷新UI顯示。
- 拍照后,會在子線程處理圖片,完成后才回到主線程來顯示圖片。拍照出來的圖片太大了,因此要做處理。
- 音頻、視頻處理會在子線程來操作
- 文件較大時,文件操作會在子線程中處理
- 做客戶端與服務端數據同步時,會在后臺閑時自動同步
2. 線程之間是如何通信的?
通過主線程和子線程切換的時候傳遞參數
1、-(void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
2、-(void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
3、-(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array
4、-(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait
5、-(void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg
方法1和方法2提供了子線程到主線程的單向通信
方法3和方法4提供了當前線程到任意線程之間的通信,比較靈活
方法5其實是創建了一個子線程來接受selecter
3. 網絡圖片處理問題怎么解決圖片重復下載問題?(SDWebImage大概實現原理)
- 這個就需要用到字典,以圖片的下載地址url為key,下載操作為value,所有的圖片大概分成三類:已經下載好的,正在下載的和將要下載的;
- 當一張圖片將要進行下載操作的時候,先判斷緩存中是否有相同的圖片,如果有的話就返回,沒有的話就去沙盒中找,有的話就拿出來用,沒有的話再去以圖片的url為key去字典中找有沒有正在進行的任務,最后去判斷等待的下載操作任務里面的字典有無相同key,如果沒有,就自己開啟任務,記錄一下,文件保存的名稱是url的md5值
- 這里建立了兩個字典 :
1.iconCache:
保存緩存的圖片
2.blockOperation
用來保存下載任務
每當進入或退出程序時,會進行圖片文件的管理:超過一星期的文件會被清除,如果設置了最大緩存,超過這個緩存就會刪除最舊的文件,直到當前緩存文件為最大緩存文件的一半大小;
一般app中大部分緩存都是圖片的情況下,可以直接調用
clear
方法進行清除緩存,getSize()
方法獲取當前緩存大小。
4. 多線程安全的幾種解決方法?
- 1、 只有在主線程刷新訪問UI
- 2、 如果要防止資源搶奪,需要用
synchronize
進行加鎖保護 - 3、 如果是異步操作要保證線程安全等問題,盡量使用GCD(有些函數默認就是安全的)
5. 原子屬性
- 原子屬性采用的是"多讀單寫"機制的多線程策略;"多讀單寫"縮小了鎖范圍,比互斥鎖的性能好
- 規定只在主線程更新UI,就是因為如果在多線程中更新,就需要給UI對象加鎖,防止資源搶占寫入錯誤,但是這樣會降低UI交互的性能,所以ios設計讓所有UI對象都是非線程安全的(不加鎖)
6. 代理的作用、block
- 代理又叫委托,是一種設計模式(可以理解為java中回調監聽機制),代理是對象與對象之間的通信交互,代理解除了對象之間的耦合性
- 改變或傳遞控制鏈,允許一個類在某些特定時刻通知到其他類,而不需要獲取到那些類的指針,可以減少框架復雜度
- 代理的屬性常是
assign
的原因:防止循環引用,以致對象無法得到正確的釋放block
底層是根據函數指針和結構體結合實現的,block
本身就是結構體,更加簡潔,不需要定義繁瑣的協議方法,但通信事件比較多的話,建議使用Delegate
-
block
就是一個數據類型,存放一段代碼,編譯的時候不會執行,只有用到的時候才會去執行里面的代碼。聲明的時候使用copy是因為要從棧區拷貝到堆區,在棧區會受到作用域的限制,超出所在的函數就會被銷毀,就沒辦法進行傳值回調等一系列操作了。應注意循環引用,weak來修飾。如果一個變量是在block外部創建,需要在block內部修改,那么需要使用block修飾這個變量(block可以在ARC和MRC情況下使用,可以修飾對象和基本數據類型,weak只能在ARC下使用,只能修飾對象,不能修飾基本數據類型) - 最常用的是使用block作為參數傳值,不同情況下回調不同的代碼(如成功回調失敗回調)
7. 談談你對runTime
運行時機制的了解(注意哦,這個很重要的)
runtime是一套比較底層的純C語言API,屬于一個C語言庫,包含了很多底層的C語言的API
- 平時編寫的OC代碼,在程序運行過程中,其實都是轉成了
runtime
的C語言代碼,runtime
是OC的幕后工作者,底層語言,例如:
OC--> [[WPFPerson alloc] init]
runtime-->objc_msgSend(objc_msgSend("WPFPerson", "alloc"), "init")
- 利用runtime可以實現一些非常底層的操作(用OC不好實現)
- 在程序運行過程中,動態創建一個類(比如KVO底層實現:檢測isa指針,發現是新建了一個類,當然Xcode7.0以前的版本才可以監聽到isa指針)
- 遍歷一個類的所有成員變量、方法,訪問私有變量(先通過
runtime的class_getInstanceVariable
獲取成員變量,再通過class_getIvar
獲取它的值)- 在程序運行過程中,動態為某個類添加屬性\方法,修改屬性值\方法,比如產品經理需要跟蹤記錄APP中按鈕的點擊次數和頻率等數據,可以通過繼承按鈕或者類別實現,但是帶來的問題比如別人不一定去實例化你寫的子類,或者其他類別也實現了點擊方法導致不確定會調用哪一個,
runtime
可以這樣解決:在按鈕的分類里面,重寫load
方法,利用dispatch_once
保證只執行一次,新建監控按鈕點擊的方法,先用class_addMethod
方法,判斷其返回的bool
值,如果添加成功,就用class_replaceMethod
將原來的方法移除,如果添加失敗,就用method_exchangeImplementations
方法進行替換- 攔截并替換方法,比如由于某種原因,我們要改變這個方法的實現,但是又不能動它的源碼(比如一些開源庫出現問題的時候,這時候runtime就可以出場了)-->先增加一個
tool
類,然后寫一個我們自己實現的方法-change
,通過runtime的class_getInstanceMethod
獲取兩個方法,在用class_replaceMethod
方法進行替換。防止數組越界的方法:數組越界的時候報錯的方法是add_object
,做一個邏輯判斷,越界的時候通過class_replaceMethod
交換掉add_object
(相當于重寫了這個方法)
- 相關應用
1.
NSCoding
(歸檔和解檔),如果一個模型有很多個屬性,那么需要對每個屬性都實現一遍encodeObject和decodeObjectForKey
方法,十分麻煩,但是如果使用class_copyIvarList
獲取所有屬性,然后循環遍歷,使用[ivarName substringFromIndex:1]
去掉成員變量下劃線
- 字典轉模型:像幾個出名的開源庫
JSONModel、MJExtension
等都是通過這種方式實現的(利用runtime
的class_copyIvarList
獲取屬性數組,遍歷模型對象的所有成員屬性,根據屬性名找到字典中key值進行賦值,當然這種方法只能解決NSString、NSNumber等,如果含有NSArray或NSDictionary,還要進行第二步轉換,如果是字典數組,需要遍歷數組中的字典,利用objectWithDict方法將字典轉化為模型,在將模型放到數組中,最后把這個模型數組賦值給之前的字典數組)
-
Method Swizzling:
OC中調用方法事實上就是向對象發送消息,而查找消息的唯一依據就是selector
的名字,因此可以使用runtime運行時機制動態交換方法。在+load
方法里面調換,因為method swizzling
的影響范圍是全局的,所以應該放在最保險的地方來處理,+load
方法能夠保證能在類初始化的時候一定能被調用,可以保證統一性,如果是在使用的時候才去調用,可能達不到全局處理的效果;使用dispatch_once
保證只交換一次。
[objc_getClass("__NSArrayM") swizzleSelector:@selector(addObject:)
withSwizzledSelector:@selector(hyb_safeAddObject:)];
使用場景:addObject
方法添加的值為nil
的時候會崩潰。調用objectAtIndex:
時出現崩潰提示empty
數組問題
8. 談談你對Run Loop的理解
- RunLoop是多線程的一個很重要的機制,就是一個線程一次只能執行一個任務,執行完任務后就會退出線程。主線程會通過do-while死循環讓程序持續等待下一個任務不退出。通過mach_msg()讓runloop沒事時進入trap狀態,節省CPU資源。非主線程通常來說就是為了執行某個任務而創建的,執行完就會歸還資源,因此默認不開啟RunLoop
- 實質上,對于子線程的runloop是默認不存在的,因為蘋果采用了懶加載的方式,如果沒有手動調用[NSRunLoop currentRunLoop]的話,就不會去查詢當前線程的RunLoop,也不會創建、加載
- 當然如果子線程處理完某個任務后不退出,需要繼續等待接受事件,需要啟動的時候也可以手動啟動,比如說添加定時器的時候就要手動開始RunLoop
8.1 如何處理事件
- 界面刷新: 當UI改變( Frame變化、 UIView/CALayer 的繼承結構變化等)時,或手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標記為待處理。 蘋果注冊了一個用來監聽BeforeWaiting和Exit的Observer,在它的回調函數里會遍歷所有待處理的 UIView/CAlayer 以執行實際的繪制和調整,并更新 UI 界面。
- 手勢識別: 如果上一步的 _UIApplicationHandleEventQueue() 識別到是一個guesture手勢,會調用Cancel方法將當前的touchesBegin/Move/End 系列回調打斷。隨后系統將對應的 UIGestureRecognizer 標記為待處理。 蘋果注冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,其回調函數為 _UIGestureRecognizerUpdateObserver(),其內部會獲取所有剛被標記為待處理的 GestureRecognizer,并執行GestureRecognizer的回調。 當有 UIGestureRecognizer 的變化(創建/銷毀/狀態改變)時,這個回調都會進行相應處理。
- 網絡請求:最底層是CFSocket層,然后是CFNetwork將其封裝,然后是NSURLConnection對CFNetwork進行面向對象的封裝,NSURLConnection是iOS7中新增的接口。當網絡開始傳輸時,NSURLConnection創建了兩個新線程:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket線程是處理底層socket連接的。NSURLConnectionLoader這個線程內部會使用RunLoop來接受底層socket的事件,并添加到上層的Delegate
8.2 應用
- 滑動與圖片刷新:當tableView的cell上有需要從網絡獲取的圖片的時候,滾動tableView,異步線程回去加載圖片,加載完成后主線程會設置cell的圖片,但是會造成卡頓。可以設置圖片的任務在CFRunloopDefaultMode下進行,當滾動tableView的時候,Runloop切換到UITrackingRunLoopMode,不去設置圖片,而是當停止的時候,再去設置圖片。(在viewDidLoad中調用self.imageView performSelector@selector(setImage) withObject:...afterDelay:...inModes@[NSDefayltRunLoopMode])
- 常駐子線程,保持子線程一直處理事件 為了保證線程長期運轉,可以在子線程中加入RunLoop,并且給Runloop設置item,防止Runloop自動退出
9. SQLite常用的SQL語句
- 創建表:create table 表名(字段名 字段數據類型 是否為主鍵, 字段名 字段數據類型, 字段名 字段數據類型...)
- 增:insert into 表名(字段1,字段2...) values(值1,值2...)
- 刪:delete from 表名 where 字段=值
10. 關于Socket,談談TCP/IP 和 UDP的理解
- Socket是一個用于傳輸網絡數據的工具,TCP/IP 和 UDP都是傳輸協議,用于定義網絡數據傳輸的格式,屬于長連接
- TCP/IP 側重可靠傳輸,傳輸速度慢,不會丟失數據,安全,聊天和下載文件時用到
- UDP:側重快速傳輸,傳輸速度快,容易丟失數據包,不安全。局域網游戲和網絡游戲,視頻聊天的時候用到
- TCP更安全是因為有一個三次握手:第一次握手(客戶端發送syn包到服務器,并進入SYN_SEND狀態,等待服務器確認),第二次握手(服務器收到syn包,必須確認客戶的SYN包,同時自己發送一個SYN+ACK包,此時服務器進入SYN_RECV狀態),第三次握手(客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK,發送完畢后服務器和客戶端都進入ESTABLISHED狀態,完成三次握手),三次握手之后才開始正式傳輸數據。因此使用TCP協議的MSN比采用UDP的QQ傳輸速度慢,但并不代表UDP不安全,因為程序員可以手動對UDP的數據收發進行驗證(比如發送方對每個數據包進行編號然后由接收方進行驗證)
- Http:超文本傳輸協議,用于定義網絡數據傳輸的格式(短鏈接)http1.0之前不支持短連接,1.1之后默認就是長連接,只要在服務器和客戶端同時設置Connection為keep-alive即可
- 長連接是為了復用,長連接指的是TCP連接,也就是為了復用TCP連接,也就是說多個HTTP請求可以復用一個TCP連接,節省了很多TCP連接建立和斷開的消耗
- 比如請求了一個網頁,這個網頁肯定還包含了CSS、JS等一系列資源,如果是短連接的話,每次打開一個網頁,基本要建立幾個甚至幾十個TCP連接,浪費了大量資源
- 長連接不是永久連接,如果一段時間內,具體的時間長短,是可以在header當中進行設置的,也就是所謂的超時時間,這個連接沒有HTTP請求發出的話,那么這個長連接就會被斷掉
- socket連接是長連接,客戶端與服務器保持通道,雙方可以主動發送數據,一般多用于即時通訊,游戲,默認超時時間是30秒,默認大小是8k(一個數據包大小)
11. 談一談內存管理
iOS的內存管理分為 MRC 和 ARC,管理的是堆區動態產生的對象,基本數據類型就不是內存管理的范圍
內存管理的核心概念是引用計數器:當對象被alloc、copy、new的時候,引用計數器+1,當被release的時候引用計數器—1,為0的時候就會被系統回收,調用dealloc方法說道內存管理,就必須說說@property的內存管理參數:retain --> release 一次舊對象 retain 一次新對象 (適用于OC對象類型)
copy --> release 一次舊對象 拷貝一個新對象出來如何避免內存泄露 --> 使用Analyze進行代碼的靜態分析
當然使用block的時候最應該注意下循環引用,使用Leaks檢測內存泄露,顯示綠色的勾告知內存處理的不錯,實際上內存得不到釋放。一般我的方法是在控制器生命周期的viewDidAppear和dealloc方法里面打印日志[[self class] description],如果沒有打印出來,就說明沒有被釋放。使用**__weak typeof(self) weakSelf = self;解決。有一次我是直接使用成員變量,而不是屬性,_age,我以為這樣沒有使用self就可以了,但是后來測試發現還是造成循環引用了,因為_age是控制器的成員變量,也就是強引用了控制器,也要改成弱引用__block **weak __typeof(_currentModel) weakModel = _currentModel;
12. 常見的數據持久化有哪些
- 偏好設置(preference),利用NSUserDefaults用來保存應用程序設置和屬性、用戶保存的數據。用戶再次打開程序或開機后這些數據仍然存在
- NSUserDefaults可以存儲的數據類型包括:NSData、NSString、NSNumber、NSDate、NSArray、NSDictionary。如果要存儲其他類型,需要先轉化為前面的類型,才能用NSUserDefault存儲
- 偏好設置是專門用來保存應用程序的配置信息的,一般不要在偏好設置中保存其他數據
- 偏好設置會將所有數據保存到同一個文件中。即preference目錄下的一個以此應用包名來命名的plist文件。
//1.獲得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//2.向文件中寫入內容
[userDefaults setObject:@"AAA" forKey:@"a"];
[userDefaults setBool:YES forKey:@"sex"];
[userDefaults setInteger:21 forKey:@"age"];
//2.1立即同步
[userDefaults synchronize];
//3.讀取文件
NSString *name = [userDefaults objectForKey:@"a"];
BOOL sex = [userDefaults boolForKey:@"sex"];
NSInteger age = [userDefaults integerForKey:@"age"];
NSLog(@"%@, %d, %ld", name, sex, age);
- 歸檔(Archiver)、解檔(unArchiver),利用NSKeyedArchiver實現歸檔、利用NSKeyedUnarchiver反接的那個
- 歸檔及時將內存中的對象寫入到磁盤文件中,歸檔也叫序列化,解檔就是講磁盤中文件中的對象讀取出來
- 必須遵循NSCoding協議,只要遵循了NSCoding協議的對象都可以通過它實現序列化,兩個協議方法必須實現
// 反歸檔
(id)initWithCoder:(NSCoder *)aDecoder {
if ([super init]) {
self.avatar = [aDecoder decodeObjectForKey:@"avatar"];
self.name = [aDecoder decodeObjectForKey:@"name"];
self.age = [aDecoder decodeIntegerForKey:@"age"];
}
return self;
}
// 歸檔
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.avatar forKey:@"avatar"];
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInteger:self.age forKey:@"age"];
}
- 歸檔,把對象歸檔時需要調用NSKeyedArchiver的工廠方法archiveRootObject: toFile: 方法
NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.data"];
Person *person = [[Person alloc] init];
person.avatar = self.avatarView.image;
person.name = self.nameField.text;
person.age = [self.ageField.text integerValue];
[NSKeyedArchiver archiveRootObject:person toFile:file];
- 反歸檔
NSString *file = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"person.data"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:file];
if (person) {
self.avatarView.image = person.avatar;
self.nameField.text = person.name;
self.ageField.text = [NSString stringWithFormat:@"%ld", person.age];
}
屬性列表
數據庫:SQLite
Core Data點擊查看講解
屬性列表
這五種持久化操作不同點
從存儲數據大小來看,歸檔、偏好設置、屬性列表三種方法適合存儲數據量較小的數據,數據庫、CoreData方法適合存儲數據量較大的數據
從加密性來看,其中歸檔會將數據進行加密,而偏好設置是直接保存到屬性列表中,不會對數據進行加密
從存儲類型來看,屬性列表只能存放固定的七種類型(可在plist文件中看到),歸檔對存儲類型無限制
13. KVC 和 KVO
- KVC(key-value-coding鍵值編碼,跟多情況下會簡化程序代碼)的常見用法:
- 給私有變量(該變量不對外開放)賦值:[Person setValue: @"19" ForKeyPath:@"age"]
- 字典轉模型:setValuesForKeyWithDictionary
- 取出私有變量:[Person valueForKey:@"age"]
- 沒有找到對應的key會崩潰:重寫setValueForUndefinedKey
KVC缺點:一旦使用KVC,編譯器無法檢查出錯誤,即不會對設置的鍵、鍵路徑進行錯誤檢查,且執行效率低于自定義的setter和getter方法,因為使用KVC鍵值編值,必須先解析字符串,然后設置或訪問對象的實例變量
通過KVO(key-value-observing,典型的觀察者模式,被觀察的對象必須使用KVC鍵值編碼來修改它的實例變量,這樣才能被觀察者觀察到)監聽person對象中name屬性發生改變
- 給監聽的屬性設置一個觀察者:
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
- 當person的name的值發生改變時,就會執行該方法
(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
do something....
}
- 當一個類的屬性被觀察的時候,系統會通過runtime動態的創建一個該類的派生類,并且會在這個類中重寫基類被觀察的屬性的setter方法,而且系統將這個類的isa指針指向了派生類(NSNotifying_類名),從而實現了給監聽的屬性賦值時調用的是派生類的setter方法。重寫的setter方法會在調用原setter方法前后,通知觀察對象值得改變。
14. @synthesize和@dynamic區別是什么
- 這兩個關鍵字都是@property對應的詞
- @synthesize 語義是如果沒有手動實現setter和getter方法,那么編譯器會自動幫你加上這兩個方法
- @dynamic告訴編譯器,屬性的setter和getter由用戶自己實現,不自動生成(readOnly只實現getter即可),但是如果沒有自己實現,編譯的餓時候不會報錯,運行的時候就會報錯,這就是所謂的動態綁定
15. 談談時間響應鏈的一般順序
- 一般來說,第一響應者是個視圖對象或者其他子類對象,當其被觸摸以后事件便交由他處理,如果他不處理,事件就會被傳遞給它的視圖控制器對象viewController(如果存在),然后是它的父視圖對象(superView)(如果存在),以此類推,直到頂層視圖,接著沿著頂層視圖(topView)到窗口(UIWindow對象)再到程序(Application對象)。如果整個過程都沒有響應這個時間,該事件就會被拋棄。一般情況下,在響應鏈中只要有UI系那個處理事件,事件就會停止傳遞
16. post和get方式的區別
GET請求的數據會負載URL之后,即把數據放在HTTP協議頭中,以?區分URL和傳輸數據,參數之間以&相連,英文字母/數字,原樣發送,如果是空格,轉化為+,如果是中文,把字符串用BASE64加密;POST就是把提交的數據放在HTTP包的包體中
GET一般用于提交少量數據(最多提交1k,瀏覽器限制),POST用于提交大量數據(理論上無限制,收服務器限制)
GET提交的數據可以在瀏覽器歷史記錄中看到,安全性不好,別人可以拿到賬號密碼
Get是向服務器發索取數據的一種請求,而POST是向服務器發提交數據的一種請求,只是發送機制不同
GET不可以設置書簽,POST可以設置書簽
什么情況下用POST:
- 請求的結果具有持續性副作用,如數據庫添加新的數據行
若使用get方法,則表單上手機的數據可能讓URL過長
要傳送的數據不是采用7位的ASCII編碼
什么情況下用GET:
- 請求是為了查找資源,HTML表單數據僅用來幫助搜索
請求結果無持續副作用性的副作用
手機的數據及HTML表單內的輸入字段名稱的總長不超過1024個字符
17. 深復制和淺復制
- 非集合類對immutable對象進行copy操作,是指針復制,mutableCopy操作時內容復制
- 非集合類對mutable對象進行copy和mutableCopy都是內容復制。
- 在集合類對象中,對immutable對象進行copy,是指針復制,mutableCopy是內容復制
- 在集合類對象中,對mutable對象進行copy和mutableCopy都是內容復制。但是:集合對象的內容復制僅限于對象本身,對象元素仍然是指針復制。
- NSString *str = @"string"; str = @"newString"; 打印對象地址,發現是發生變化的,需要把@"newStirng"當做一個新的對象,將這段對象的內存地址賦值給str
18. 關于項目中動畫的使用
- 序列幀動畫:self.imageView.animationImages = array;
- [UIView animateWithDuration] + CGAffinetransform
- 核心動畫
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"position.y"];
anim.fromValue toValue repeatCount [btn.layer addAnimation]
- 關鍵幀動畫
CAKeyframeAnimation,anim.values = array
,添加到layer上 - 組動畫
CAAnimationGroup
,將以上動畫組合起來
轉場動畫:CATransition
,設置duration
和type
,然后添加到layer
上。利用UIView
的類方法實現轉場動畫
[UIView transitionWithView: duration: options: animations:^{ } completion:nil];
-
UIDynamicAnimator
仿真者 、UISnapBehavior
吸附行為,設置damping
來調節震動幅度 、UIPushBehavior
推動行為 、UICollisionBehavior
碰撞邊緣檢測行為 、UIAttachmentBehavior
附著行為 、UIGravityBehavior
重力行為
POPSpringAnimation
springBounciness[0,20]
越大振幅越大。springSpeed
速度
19. 談談你對MVVM的認識
- 說到MVVM,就必須先說MVC,MVC就是
Model
顯示數據,View
呈現用戶界面,Controller
調節二者之間的交互,但是這個結構存在的問題就是模型的代碼太少,而隨著功能的增加控制器的代碼會越來越多,而且不方便測試(比如辦一個飯卡,默認一百元); - 在MVVM中,
view
和view controller
正式聯系在一起,我們將它視為一個組件,這兩個都不能直接引用model
,而是引用視圖模型view model
,里面放置著用戶輸入驗證邏輯,視圖顯示邏輯,發送網絡請求等代碼,view model
再去引用model
這個的優點呢,首先就是低耦合,view
可以獨立于model
變化和修改,一個viewModel
可以綁定到不同的view
上,可重用性,也有利于獨立開發,開發人員可以專注業務邏輯和數據的開發,設計人員可以專注于頁面設計 - 實際用例
- 樓市餐廳需要充餐卡,默認是100元,在模型層中,如果想保存這個值 -->
let balanece = 100
- 但是展示給用戶的時候,我們想呈現出
“您的賬戶余額為:¥100”
,在mvc
中,這種代碼只能放到視圖控制器中,顯得很臃腫,如果放在模型當中,會更丑,因為有許多進行格式化的代碼擠在其中
如果添加視圖模型,僅僅需要映射一下原始數據
struct AccountViewModel {
let dispalyBalance: String
init(mode: cardAccount) {
let formattedBalance = model.balance.currencyValue;
displayBalance = "Your balance is \(formatterBalance)"
}
}
- 通過這種方式,視圖模型實際上會讀取數據模型,然后將其中的信息進行格式化,從而準備展現在視圖當中,很容易測試,直接把帶有賬戶信息的模型放進去,然后測試顯示就可以了,在之前是特別復雜的
- 在去年
WWDC
上,Andy(iOS4.1-8的UIKit框架維護者)
演講中有一個關于zoetrope(西洋鏡)
的例子
- 大概意思就是每一幀都是靜態值,可以通過改變任務手部抬起的距離,或者任務頭部傾斜的距離,來對字符進行編碼。每一幀都是靜態的,但是當把他們放在一起,然后一直看向一個中心的話,那么始終都有新的數據出現,這樣就可以得到一個而美麗的、生動的動畫(類比Tom貓)
- 我們可以使用相同的方式來實現值類型,視圖控制器會跟隨
zoetrope
的最后一個幀圖像--也就是最新的一塊活躍數據,然后將其展示給用戶。只要您的模型發生了更新,視圖就會根據最新的信息進行更改了
var viewModel = ViewModel(model: Account)
20.0 關于Swift與OC的不同
其實是個面試官八個不懂Swift,而且一般不懂得就問個Swift與OC的不同。題主也只是在自學Swift,等到3.0出了之后再深入研究,而且項目中可能也要開始從混編逐漸向Swift靠攏了。
Swift是一門更加現代化的語言,但是目前還在成長階段,更新改動比較大,雖然說其底層思想不變,變的是API和接口,但是每次更新完Xcode看到自己的Swift項目還是有些淡淡的憂傷,而且目前Swift開發都要轉成OC的runtime,包略大,因此題主認為成熟項目最好還是采用OC
先記住一句話:OC底層面向對象,而Swift底層更加面向協議
我們已經見識過Apple使用了大量協議,比如在
tableView
當中,我們可以通過協議來告訴Apple需要多少個表視圖單元格,而不是每時每刻都要繼承UITableViewController
在這里以MVVM作為測試用例:比如現在需要建立一個類似設置界面的
tableView
,每個cell
需要一個label
和一個switch
,自定義SwitchWithTextTableViewCell
,在其內部建立一個configure
方法中對label
的title
,titleFont
,titleColor
,switch
的switchOn
和switchColor
等進行初始化,但這種方式非常累贅,比如添加一個副標題,就需要額外添加三個屬性但是利用協議
SwitchWithTextCellProtocol
,讓視圖模型實現這個協議,然后在這里設置所有的屬性
protocol SwitchWithTextCellProtocol {
var title: String { get }
var titleFont: UIFont { get }
var titleColor: UIColor { get }
var switchOn: Bool { get }
var switchColor: UIColor { get }
func onSwitchTogglenOn(onL Bool)
}
通過swift2.0重點餓協議擴展,就可以通過默認值來做一些處理了,如果對于大多數單元格來說,可以確定某一種顏色的話,就可以對其建立擴展,然后設置顏色即可,所有實現此協議的視圖就沒有必要再去設置這個顏色了
現在,我的
configure
方法里面只要實現此協議的值就可以了
// 這個方法只需要一個參數,相比于之前的多個參數簡便了很多
class SwitchWithTextTableViewCell: UITableViewCell {
func configure(withDelegate delagate: SwitchWithTextCellProtocol) {
// 在這里配置方法
}
}
- 現在的視圖模型
struct MinionModeViewModel: SwitchWithTextCellProtocol {
var title = "excellent!!"
var switchOn = true
var switchColor: UIColor {
return .yellowColor()
}
func onSwitchToggleOn(on: Bool) {
if on {
print("The Minions are here to stay!")
} else {
print("The Minions went out to play!")
}
}
}
- 現在,cellForRowAtIndexPath()也變得非常簡明了
let cell = tableView.dequeueReuseableCellWithIdentifier("SwitchWithTextTableViewCell", forIndexPath: indexPath) as! SwitchWithTextTableViewCell
cell.configure(withDelegate: MinionModeViewModel())
return cell
再把模型放在視圖模型層級,一遍對其進行跟蹤,再視圖模型中傳遞這些信息,這樣單元格就可以生成了
但是在這個基礎上,還可以再做進一步的深化,就是建立兩個協議,一個作為實際編碼的數據源:比如標題內容之類的實際數據;一個作為單元格委托:存儲顏色、字體之類的并沒有包含實際數據的信息,也就是仿照Apple中
UITableView
等集合視圖之類的地方,按照這種思維去建立單元格存儲和單元格委托
protocol SwitchWithTextCellDataSource {
var title: String { get }
var switchOn: Bool { get }
}
protocol SwitchWithTextCellDelegate {
func onSwitchTogglenOn(on: Bool)
var switchColor: UIColor { get }
var textColor: UIColor { get }
var font: UIFont { get }
}
- 接下來,再讓
configure
方法同時接受這兩個協議。因為委托可以全部在協議擴展中使用默認值進行配置,比如說字體、顏色之類的信息,這樣在理論上就可以不用向里面傳遞任何東西進去,只需要創建一個模型就可以了
// SwitchWithTextTableViewCellfunc
configure(withDataSource dataSource: SwitchWithTextCellDataSource, delegate: SwitchWithTextCellDelegate?) {
// 在這里配置視圖
}
- 然后就要開始通過擴展來改進視圖模型了,使用一個實際數據源的代碼塊,然后給定要傳遞的視圖當中的原始信息
struct MinionModeViewModel: SwiftWithTextCellDataSource {
var title = "Minion Mode!!"
var switchOn = true
}
- 接下來會在一個單獨的視圖模型的部分使用處理字體、顏色之類的委托,然后在其中進行相關的配置
extension MinionModeViewModel: SwitchWithTextCellDelegate {
var switchColor: UIColor {
return .yellowColor()
}
func onSwitchToggleOn(on: Bool) {
if on {
print("The Minions are here to stay!")
} else {
print("The Minions went out to play!")
}
}
}
- 最終,表格視圖單元格變得非常簡單
// SettingViewControllerlet
viewModel = MinionModeViewModel()
cell.configure(withDataSource:viewModel, delegate: viewModel)
return cell
- 僅僅需要創建視圖模型,然后將其傳遞到配置方法當中,最后返回單元格,就可以了
20.1 Swift2.0中的 Minxin
和Trait
在游戲開發中通常會有一個很龐大的層級關系,以及一系列的繼承,比如各種怪,繼承在這里顯得十分有意義,但是隨著層級的擴展,這個項目就會變得凌亂起來
比如說需要設計一個可以射擊的怪物,但這時候塔防頂部的大炮也會射擊,就需要把“射擊輔助類”提取出來,但是如果一直這樣提取子類,代碼后面會一團亂麻
將這個代碼重構,不再去提取能夠射擊或者能夠加血的子類,而是將其提取為協議,通過協議擴展來實現這個功能,代碼更加簡潔,更利于理解
// 一看這個對象的類型,就知道他有哪些功能,而不是一個個去查找她的實現
class ZapMonster: GameObject, GunTraint, HealthTraint, MovementTraint { ...}
- 雖然說這種設計模式是游戲方面的,但是我們平時的代碼也可以參考這種設計模式:這樣就不需要讓實際的單元格實現這個協議了,只需要將其根更廣泛的
TextPresentable
聯系在一起就可以了,這樣,任何擁有標簽的視圖,而不僅僅是單元格,都可以實現這個協議來彎沉相關的功能。這樣就可以說這個標簽有什么樣的溫恩,什么樣的顏色,以及什么樣的字體
protocol TextPresentable {
var text: String { get }
var textColor: UIColor { get }
var font: UIFont { get }
}
protocol SwitchPresentable {
var switchOn: Bool { get }
var switchColor: UIColor { get }
func onSwitchToggleOn(on: Bool)
}
這種情況下,比如需要一個圖片框,只要一個
iamgeProtocol
就可以了,設計師要求改所有標簽的顏色的話一行代碼就可以搞定現在單元格的模樣
class SwitchWithTextTableViewCell<T where T: TextPresentable, T: SwitchPresentable>: UITableViewCell {
private var delegate: T? // T是視圖模型
func configure(withDelegate delegate: T) {
// 在這里配置視圖
}
}
- 在這種情況下,它沒有實現這些協議,但是會期待某種實現這些協議的東西傳遞進去,因此我們使用了泛型,這個單元格期待了一個實現了
TextPresentableProtocol
的委托。就我們而言,傳遞進去的將是一個實現了這些協議的東西就可以了,現在要基于這些信息在單元格當中配置所有的東西了,現在就可以基于這些信息在單元格中配置所有的東西了
extension MinionModeViewModel: TextPresentable {
var text: String {
return "Minion Mode"
}
var textColor: UIColor {
return .blackColor()
}
var font: UIFont {
return .systemFontOfsize(17.0)
}
}
我們的視圖模型將會有一個
TextPresentable
代碼,在其中可以配置文本、顏色、字體,并且由于所有的這些協議擴展中都已經有默認值了,甚至不需要視圖模型去實現這些具體的內容最后,視圖模型當中的代碼就只需要
dequeue
相應的單元格。然后通過視圖模型對其進行配置,然后返回單元格即可
20.2 Swift2.2隨著iOS9.3一同登場,講講什么新調整?
-
++
和--
被deprecate
掉了,2.2版本會有一個警告,但是3.0必然會完全移除,使用+=1
和-=1
替代; - 傳統C 風格的
for
循環被干掉,只能用for in
; -
var
參數被廢除了
- 使用
var
, 讓你在函數內部修改參數- 使用
inout
,可以讓你的改變延續到函數結束后
- 允許更多的參數作為參數標簽:
for i 1.stride(to: 9, by: 2){print(i)}
從1開始,每次加2,但結果要一直小于9,代碼的自解釋性更好; - 字符串作為
selector
也被deprecated
掉了,可以通過#selector(Swift方法名)
來實現selector
,發生拼寫錯誤也會得到提醒
還有一些沒記住。。反正我的demo中需要用到的地方這幾點比較多
21. 優化tableViewCell
高度
一種是針對所有
Cell
具有固定高度的情況,通過:self.tableView.rowHeight = 88;
指定了一個所有cell
都是 88 高度的UITableView
,對于定高需求的表格,強烈建議使用這種(而非下面的)方式保證不必要的高度計算和調用。另一種方式就是實現
UITableViewDelegate
中的:heightForRowAtIndexPath:
需要注意的是,實現了這個方法后,rowHeight
的設置將無效。所以,這個方法適用于具有多種cell
高度的UITableView
。iOS7之后出了了
estimatedRowHeight
,面對不同高度的cell
,只要給一個預估的值就可以了,先給一個預估值,然后邊滑動邊計算,但是缺點就是
- 設置估算高度以后,
tableView
的contentSize.height
是根據cell高度預估值和cell的個數來計算的,導致導航條處于很不穩定的狀態,因為contentSize.height會逐漸由預估高度變為實際高度,很多情況下肉眼是可以看到導航條跳躍的- 如果是設計不好的上拉加載或下拉刷新,有可能使表格滑動跳躍
- 估算高度設計初衷是好的,讓加載速度更快,但是損失了流暢性,與其損失流暢性,我寧愿讓用戶加載界面的時候多等那零點幾秒
- iOS8 WWDC 中推出了
self-sizing cell
的概念,旨在讓cell
自己負責自己的高度計算,使用frame layout
和auto layout
都可以享受到:
self.tableView.estimatedRowHeight = 213;self.tableView.rowHeight = UITableViewAutomaticDimension;
如果不加上估算高度的設置,自動算高就失效了- 這個自動算高在
push
到下一個頁面或者轉屏時會出現高度特別詭異的情況,不過現在的版本修復了。
- 相同的代碼在 iOS7 和 iOS8 上滑動順暢程度完全不同,iOS8 莫名奇妙的卡。很大一部分原因是 iOS8 上的算高機制大不相同,從 WWDC 也倒是能找到點解釋,
cell
被認為隨時都可能改變高度(如從設置中調整動態字體大小),所以每次滑動出來后都要重新計算高度。
dequeueReusableCellWithIdentifier:forIndexPath:
相比不帶“forIndexPath”
的版本會多調用一次高度計算- iOS7 計算高度后有”緩存“機制,不會重復計算;而 iOS8 不論何時都會重新計算
cell
高度
使用
UITableView+FDTemplateLayoutCell
(百度知道負責人孫源) 無疑是解決算高問題的最佳實踐之一,既有 iOS8 self-sizing 功能簡單的 API,又可以達到 iOS7 流暢的滑動效果,還保持了最低支持 iOS6FDTemplateLayoutCell
的高度預緩存是一個優化功能,利用RunLoop
空閑時間執行預緩存任務計算,當用戶正在滑動列表時顯然不應該執行計算任務影響滑動體驗。
- 當用戶正在滑動
UIScrollView
時,RunLoop
將切換到UITrackingRunLoopMode
接受滑動手勢和處理滑動事件(包括減速和彈簧效果),此時,其他Mode (除 NSRunLoopCommonModes 這個組合 Mode)
下的事件將全部暫停執行,來保證滑動事件的優先處理,這也是 iOS 滑動順暢的重要原因- 注冊
RunLoopObserver
可以觀測當前RunLoop
的運行狀態,并在狀態機切換時收到通知:RunLoop
開始
RunLoop
即將處理Timer
RunLoop
即將處理Source
RunLoop
即將進入休眠狀態
RunLoop
即將從休眠狀態被事件喚醒
RunLoop
退出- 分解成多個
RunLoop Source
任務,假設列表有 20 個 cell,加載后展示了前 5 個,那么開啟估算后table view
只計算了這 5 個的高度,此時剩下 15 個就是“預緩存”的任務,而我們并不希望這 15 個計算任務在同一個RunLoop
迭代中同步執行,這樣會卡頓 UI,所以應該把它們分別分解到 15 個RunLoop
迭代中執行,這時就需要手動向RunLoop
中添加Source
任務(由應用發起和處理的是Source 0
任務)
22. 列舉一下你常用的第三方庫
- Xcode插件
- Alcatraz:Xcode 插件管理工具
- ColorSense-for-Xcode:代碼生成顏色預覽,可視化編輯
- KSImageNamed-Xcode:引入圖片自動提示,預覽
- VVDocumenter-Xcode:規范化注釋
- 項目中常用的第三方庫
AFNetworking:
網絡庫,通常會在AFN上面再封裝一層,主要封裝接口邏輯SDWebImage:
下載網絡圖片,定時清除緩存Reachability:
網絡狀態判斷,AFN已經有這個功能WebViewJavaScriptBridge:
Webview和cocoa
之間消息傳遞fmdb:
SQLite的封裝,簡單易用DTCoreText:
CoreText庫,支持HTMLKissXML:
XML解析,支持讀取和修改,基于libxml
ZXingObjC:
二維碼,支持編碼解碼GTMBase64:base64
編解碼GPIImage:
圖像處理JSONKit:json
解析,性能最好Mansonry
、Snapkit(Swift)
輔助自動布局MJRefresh:
上拉加載,下拉刷新MBProgressHUD
、SVProgressHUD
進度圖,加載效果提示
23. 為什么AFN顯示圖片不如SDWebImage流暢?同樣是從網絡上下載圖片而不是從緩存取圖片?
- 因為SDWebImage有一個
decoder
-
UIImage的imageWithData
函數是每次畫圖的時候才將Data解壓成ARGB的圖像 - 所以每次畫圖的時候,會有一個解壓操作,這樣效率很低,但是只有瞬時的內存需求
- 為了提高效率通過
SDWebImageDecoder
將包裝在Data
的資源解壓,然后畫在另外一張圖片上,這樣新的圖片就不再需要重復解壓了 - 這是典型的空間換時間的做法
24. 關于新特性
iOS7新特性
- 在iOS7當中,使用麥克風也需要取得用戶同意了。如果用戶不允許app使用麥克風的話,那么需要使用麥克風的app就不能接收不到任何聲音
-
[NSArray firstObject]
的實現,iOS4之前只是一個私有的方法 -
UIImage.renderingMode
著色(Tint Color)
,可以設置一個UIImage
在渲染時是否使用當前視圖的Tint Color
。 -
UIScreenEdgePanGestureRecognizer
可以從屏幕邊界即可檢測手勢 - 使用
Core Image
來檢測眨眼以及微笑iOS給Core Image
增加了兩種人臉檢測功能:CIDetectorEyeBlink以及CIDetectorSmile
。這也就是說你現在可以在照片中檢測微笑以及眨眼。
iOS8新特性
當使用iOS8定位的時候需要請求用戶授權,且在
info.plist
里添加字段NSLocationAlwaysUsageDescription
請求用戶授權的描述size classes
是為了解決storyboard
只能訂制一種屏幕樣式的問題,它不再是具體的尺寸,而是抽象尺寸通過寬/高 的compact、any、regular
組成了九種組合包含了所有蘋果設備的尺寸。iOS8中,字體是
Helvetica
,中文的字體有點類似于“華文細黑”。只是蘋果手機自帶渲染,所以看上去可能比普通的華文細黑要美觀。iOS9中,中文系統字體變為了專為中國設計的“蘋方” 有點類似于一種word
字體“幼圓”。字體有輕微的加粗效果,并且最關鍵的是字體間隙變大了!
####### iOS9新特性
iOS9系統發送的網絡請求將統一使用
HTTPs
,將不再默認使用HTTP
等不安全的網絡協議,而默認采用TLS 1.2
。服務器因此需要更新,以解析相關數據。如不更新,可通過在info.plist
中聲明,倒退回不安全的網絡請求。將允許出現這種場景:同一
app
中多個location manager:
一些只能在前臺定位,另一些可在后臺定位bitcode
的理解應該是把程序編譯成的一種過渡代碼,然后蘋果再把這個過渡代碼編譯成可執行的程序。bitcode也允許蘋果在后期重新優化我們程序的二進制文件,有類似于App瘦身的思想。stackView
Multasking:多任務特性,三種形式
- 臨時調出的滑動覆蓋:
Slide Over
- 視頻播放的畫中畫模式
(Picture in Picture)
(AVPlayerViewController
默認支持。MPMoviePlayerViewController
被deprecated
掉了,不支持)- iPad真正同時使用兩個App
UI Test:iOS9.0之前加入異步代碼測設和性能測試,可以說Xcode自帶的測試框架已經能滿足絕大部分單元測試的需求了,但是這并不夠,因為開發一個iOS app從來都是很注重UI和用戶體驗的,之前UI測試使用
KIF,Automating
,iOS9.0的Xcode給出了自帶的XCUITest
的一系列工具,和大多數UI測試工具類似,XCUI使用Accessbility
標記來確定view
,但因為是Apple自家的東西,可以自動記錄操作流程,所以只要書寫最后的驗證部分就好了,比其他UI測試工具方便多了Swift2
APP Thinning:app為了后向兼容,都同時包含了32bit和64bit,在圖片資源2X和3X的一應俱全,下載的時候只需要當前機型對應的一套資源,但是卻要全部打包下載,現在只需要升級iOS9,就可以省很多流量
3D touch
地圖顯示實時的交通狀況
人工智能siri更加智能,幾個大城市的地鐵及火車站入口都有詳細的標識
手機電池的低功耗設置
Spootlight
,你的設備會向推薦最近通話過的聯系人,使用過的APP以及你可能感興趣的去處、信息呈現更精彩
25. 我是怎樣用兩個imageView
實現了無線輪播!
- 建立一個
scrollView
,設置contentsize
為3*kWidth
,contentOffSet
為kWidth
- 接下來使用代理方法
scrollViewDidScroll
來監聽scrollview
的滾動,定義一個枚舉變量來記錄滾動的方向 - 使用KVO來監聽
direction
屬性值的改變-->[self addObserver:self forKeyPath:@"direction" options:NSKeyValueObservingOptionNew context:nil];
- 通過
observeValueForKeyPath
判斷滾動的方向,當偏移量大于x,表示左移,則將otherImageView
加在右邊,偏移量小于x,表示右移,則將otherImageView
加在左邊。同時判斷設置對應的索引,圖片 - 通過代理方法
scrollViewDidEndDecelerating
來監聽滾動結束,結束后,scrollview
的偏移量為0或者2x,我們通過代碼再次將scrollview
的偏移量設置為x,并將currImageView
的圖片修改為otherImageView
的圖片,那么我們看到的還是currImageView
,只不過展示的是下一張圖片,如圖,又變成了最初的效果 - 然后設置自動輪播,添加計時器,利用
setContentOffset
方法里面setContentOffset:animated:
方法執行完畢后不會調用scrollview的scrollViewDidEndDecelerating
方法,但是會調用scrollViewDidEndScrollingAnimation
方法,因此我們要在該方法中調用pauseScroll
(即監聽減速結束后由otherImageView
切換到currImageView
的方法) - 添加計時器:
self.timer = [NSTimer timerWithTimeInterval:self.time target:self selector:@selector(nextPage) userInfo:nil repeats:YES];
- 在
scrollViewWillBeginDragging
中停止計時器 - 在
scrollViewDidEndDragging
中開啟計時器 - 判斷外界傳入的是圖片還是路徑,如果是圖片,直接加入圖片數組中,如果是路徑,先添加一個占位圖片,然后根據路徑去下載圖片
- 監聽圖片被點擊
- 定義一個
block
屬性暴露給外界void(^imageClickBlock)(NSInteger index)
- 設置
currImageView
的userInteractionEnabled
為YES- 給
currImageView
添加一個點擊的手勢
-在手勢方法里調用block
,并傳入圖片索引
-
NSTimer
的兩種形式
scheduledTimerWithTimeInterval
是創建一個定時器,并加入到當前運行循環[NSRunLoop currentRunLoop]
中- 其他兩個
([NSTimer timerWithTimeInterval:3 target:self selector:@selector(doSomeThing1) userInfo:nil repeats:YES];
、[[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:5] interval:3 target:self selector:@selector(doSomeThing2) userInfo:nil repeats:YES];)
只是創建定時器,并未添加到當前運行循環中,所以如果是其他兩種方式創建的定時器則需要手動添加到currentRunLoop
中
- NSTimer是普通的定時器,如果系統繁忙,刷新可能會被延遲。但是
CADisplaylink
實時刷新,跟著屏幕的刷新頻率實時刷新,60次/s,與屏幕刷新頻率相同
26. tableView
的優化
iOS平臺因為UIKit本身的特性,需要將所有的UI操作都放在主線程執行,所以有時候就習慣將一些線程安全性不確定的邏輯,以及它線程結束后的匯總工作等等放到了主線程,所以主線程包含大量計算、IO、繪制都有可能造成卡頓。
可以通過監控
runLoop
監控監控卡頓,調用方法主要就是在kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
之間,還有kCFRunLoopAfterWaiting
之后,也就是如果我們發現這兩個時間內耗時太長,那么就可以判定出此時主線程卡頓.使用到
CFRunLoopObserverRef
,通過它可以實時獲得這些狀態值的變化監控后另外再開啟一個線程,實時計算這兩個狀態區域之間的耗時是否到達某個閥值,便能揪出這些性能殺手.
監控到了卡頓現場,當然下一步便是記錄此時的函數調用信息,此處可以使用一個第三方
Crash
收集組件PLCrashReporter
,它不僅可以收集Crash
信息也可用于實時獲取各線程的調用堆棧當檢測到卡頓時,抓取堆棧信息,然后在客戶端做一些過濾處理,便可以上報到服務器,通過收集一定量的卡頓數據后經過分析便能準確定位需要優化的邏輯
設置正確的
reuseidentifer
以重用cell
盡量將
View
設置為不透明,包括cell
本身(backgroundcolor默認是透明的)
,圖層混合靠GPU去渲染,如果透明度設置為100%,那么GPU就會忽略下面所有的layer
,節約了很多不必要的運算。模擬器上點擊“Debug”
菜單,然后選擇“color Blended Layers”
,會把所有區域分成綠色和紅色,綠色的好,紅色的性能差(經過混合渲染的),當然也有一些圖片雖然是不透明的,但是也會顯示紅色,如果檢查代碼沒錯的話,一般就是圖片自身的性質問題了,直接聯系美工或后臺解決就好了。除非必須要用GPU加載的,其他最好要用CPU加載,因為CPU一般不會百分百加載,可以通過CoreGraphics
畫出圓角有時候美工失誤,圖片大小給錯了,引起不必要的圖片縮放(可以找美工去改,當然也可以異步去裁剪圖片然后緩存下來),還是使用
Instrument的Color Misaligned Images
,黃色表示圖片需要縮放,紫色表示沒有像素對齊。當然一般情況下圖片格式不會給錯,有些圖片格式是GPU不支持的,就還要勞煩CPU去進行格式轉換。還有可以通過Color Offscreen-Rendered Yellow
來檢測離屏渲染(就是把渲染結果臨時保存,等到用的時候再取出,這樣相對于普通渲染更消耗內存,使用maskToBounds
、設置shadow
,重寫drawRect
方法都會導致離屏渲染)避免漸變,cornerRadius
在默認情況下,這個屬性只會影響視圖的背景顏色和border
,但是不會離屏繪制,不影響性能。不用clipsToBounds
(過多調用GPU去離屏渲染),而是讓后臺加載圖片并處理圓角,并將處理過的圖片賦值給UIImageView
。UIImageView
的圓角通過直接截取圖片實現,圓角路徑直接用貝塞爾曲線UIBezierPath
繪制(人為指定路徑之后就不會觸發離屏渲染),UIGraphicsBeginImageContextWithOptions
。UIView
的圓角可以使用CoreGraphics
畫出圓角矩形,核心是CGContextAddArcToPoint
函數。它中間的四個參數表示曲線的起點和終點坐標,最后一個參數表示半徑。調用了四次函數后,就可以畫出圓角矩形。最后再從當前的繪圖上下文中獲取圖片并返回,最后把這個圖片插入到視圖層級的底部。“Flash updated Regions”
用于標記發生重繪的區域如果
row
的高度不相同,那么將其緩存下來如果
cell
顯示的內容來自網絡,那么確保這些內容是通過異步下載使用
shadowPath
來設置陰影,圖層最好不要使用陰影,陰影會導致離屏渲染(在進入屏幕渲染之前,還看不到的時候會再渲染一次,盡量不要產生離屏渲染)減少
subview
的數量,不要去添加或移除view
,要就顯示,不要就隱藏在
cellForRowAtIndexPath
中盡量做更少的操作,最好是在別的地方算好,這個方法里只做數據的顯示,如果需要做一些處理,那么最好做一次之后將結果儲存起來.
使用適當的數據結構來保存需要的信息,不同的結構會帶來不同的操作代價使用,
rowHeight , sectionFooterHeight
和sectionHeaderHeight
來設置一個恒定高度 , 而不是從代理(delegate)
中獲取cell
做數據綁定的時候,最好在willDisPlayCell
里面進行,其他操作在cellForRowAtIndexPath
,因為前者是第一頁有多少條就執行多少次,后者是第一次加載有多少個cell
就執行多少次,而且調用后者的時候cell
還沒顯示讀取文件,寫入文件,最好是放到子線程,或先讀取好,在讓
tableView
去顯示tableView
滾動的時候,不要去做動畫(微信的聊天界面做的就很好,在滾動的時候,動態圖就不讓他動,滾動停止的時候才動,不然可能會有點影響流暢度)。在滾動的時候加載圖片,停止拖拽后在減速過程中不加載圖片,減速停止后加載可見范圍內圖片
27. 談談內存的優化和注意事項(使用Instrument工具的CoreAnimation、GPU Driver、I/O
操作,檢查fps
數值)
重用問題:比如
UITableViewCell、UICollectionViewCell、UITableViewHeaderFooterViews
等設置正確的reuseIdentifier
,充分重用懶加載控件、頁面:對于不是立刻使用的數據,都應該使用延遲加載的方式,比如網絡連接失敗的提示界面,可能一直都用不到
使用
Autorelease Pool:
在某些循環創建臨時變量處理數據時,自動釋放池以保證能及時釋放內存不要使用太多的
xib/storyboard:
載入時會將其內部的圖片在內的所有資源載入內存,即使未來很久才會需要使用,相對于純代碼寫的延遲加載,在性能和內存上就差了很多數據緩存:對于
cell
的行高要緩存起來,使用reloadData
效率也極高,對于網絡數據,不需要每次都請求的,應該緩存起來,可以寫入數據庫,也可以通過plist
文件存儲選擇正確的數據結構:針對不同的業務場景選擇最合適的數據結構是寫出高效代碼的基礎
- 數組:有序的一組值,使用索引查詢起來很快,使用值查詢的很慢,插入/刪除 很慢
- 字典:存儲鍵值對對,用鍵查找比較快
- 集合:無序的一組值,用值來查找很快,插入/刪除很快
gzip/zip
壓縮:當從服務器下載相關附件時,可以通過zip
壓縮后再下載,使得內存更小,下載速度也更快重大開銷對象:一些
objects
的初始化很慢,比如NSDateFormatter
和NSCalendar
,但是又無可避免的需要使用,通常作為屬性存儲起來,避免反復使用避免反復處理數據:需要應用需要從服務器加載數據,常為JSON或者XML格式的數據,在服務器端或者客戶端使用相同的數據結構很重要
選擇圖片時,要對圖片進行壓縮處理,根據不同的情況選擇不同的圖片加載方式,
-imageNamed:
讀取到內存后會緩存下來,適合圖片資源較小,使用很頻繁的圖片;-initWithContentsOfFiles:
僅加載圖片而不緩存,適合較大的圖片。若是collectionView
中使用大量圖片的時候,可以用UIVIew.layer.contents=(__bridge id _Nullable)(model.clipedImage.CGImage);
這樣就更輕量級一些當然有時候也會用到一些第三方,比如在使用
UICollectionView
和UITableView
的時候,Facebook
有一個框架叫AsyncDisplayKit
,這個庫就可以很好地提升滾動時流暢性以及圖片異步下載功能(不支持sb和autoLayout,需要手動進行約束設置),AsyncDisplayKit
用相關node
類,替換了UIView
和它的子類,而且是線程安全的。它可以異步解碼圖片,調整圖片大小以及對圖片和文本進行渲染,把這些操作都放到子線程,滑動的時候就流暢許多。我認為這個庫最方便的就是實現圖片異步解碼。UIImage
顯示之前必須要先解碼完成,而且解碼還是同步的。尤其是在UICollectionView/UITableView
中使用prototype cell
顯示大圖,UIImage
的同步解碼在滾動的時候會有明顯的卡頓。另外一個很吸引人的點是AsyncDisplayKit
可以把view
層次結構轉成layer
。因為復雜的view層次結構開銷很大,如果不需要view
特有的功能(例如點擊事件),就可以使用AsyncDisplayKit
的layer backing
特性從而獲得一些額外的提升。當然這個庫還處于開發階段,還有一些地方地方有待完善,比如不支持緩存,我要使用這個庫的時候一般是結合Alamofire
和AlamofireImage
實現圖片的緩存