本篇文章是承接上篇文章 iOS 進階+面試(一)
八、iOS 中內省的幾個方法?
對象在運行時獲取其類型的能力稱為內省。內省可以有多種方法實現。
OC運行時內省的4個方法:
判斷對象類型:
-(BOOL) isKindOfClass: 判斷是否是這個類或者這個類的子類的實例
-(BOOL) isMemberOfClass: 判斷是否是這個類的實例
判斷對象/類是否有這個方法
-(BOOL) respondsToSelector: 判讀實例是否有這樣方法
+(BOOL) instancesRespondToSelector: 判斷類是否有這個方法
在 Objective-C 中,id類型類似于(void*) ,可以指向任何類的對象,但在運行時對象的類型不再是id,而是該對象真正所屬的類。
Person *person = [[Person alloc] init];
NSArray *arr = @[person];
id obj = arr[0]; //OC集合中取出的對象都是id類型
此時可通過
BOOL isPersonClass = [obj isKindOfClass: [Person class] ];
來判斷obj是否Person類型或其子類的對象。
在 Objective-C 中,用父類類型定義的指針,可以指向其子類的對象,但在運行時對象真實類型會是子類。
//例如 Boy是Person的子類,現定義:
Person *p = [[Boy alloc] init];
可通過 BOOL isBoy = [p isMemberOfClass: [Boy class] ];
判斷Person *類型的p是否是Boy類型。
九、class方法和objc_getClass方法有什么區別?
1.當參數obj為Object實例對象
object_getClass(obj)與[obj class]輸出結果一直,均獲得isa指針,即指向類對象的指針。
2.當參數obj為Class類對象
object_getClass(obj)返回類對象中的isa指針,即指向元類對象的指針;[obj class]返回的則是其本身。
3.當參數obj為Metaclass類對象
object_getClass(obj)返回元類對象中的isa指針,因為元類對象的isa指針指向根類,所有返回的是根類對象的地址指針;[obj class]返回的則是其本身。
4.obj為Rootclass類對象
object_getClass(obj)返回根類對象中的isa指針,因為跟類對象的isa指針指向Rootclass‘s metaclass(根元類),即返回的是根元類的地址指針;[obj class]返回的則是其本身。
總結:
經上面初步的探索得知,object_getClass(obj)返回的是obj中的isa指針;而[obj class]則分兩種情況:一是當obj為實例對象時,[obj class]中class是實例方法:- (Class)class,返回的obj對象中的isa指針;二是當obj為類對象(包括元類和根類以及根元類)時,調用的是類方法:+ (Class)class,返回的結果為其本身。
十、在運行時創建類的方法objc_allocateClassPair的方法名尾部為什么是pair(成對的意思)?
// 創建一個新類和元類
Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes );
// 銷毀一個類及其相關聯的類
void objc_disposeClassPair ( Class cls );
// 在應用中注冊由objc_allocateClassPair創建的類
void objc_registerClassPair ( Class cls );
objc_allocateClassPair 函數:如果我們要創建一個根類,則superclass指定為Nil。extraBytes通常指定為0,該參數是分配給類和元類對象尾部的索引ivars的字節數
。
十一、一個int變量被__block修飾與否的區別?
沒有修飾,被block捕獲,是值拷貝。
使用__block修飾,會生成一個結構體,復制int的引用地址。達到修改數據。
1、block截獲自動變量(局部變量)值
對于 block 外的變量引用,block 默認是將其復制到其數據結構中
來實現訪問的。也就是說block的自動變量截獲只針對block內部使用的自動變量, 不使用則不截獲, 因為截獲的自動變量會存儲于block的結構體內部, 會導致block體積變大。特別要注意的是默認情況下block只能訪問不能修改局部變量的值。
2、 __block 修飾的外部變量
對于用 __block 修飾的外部變量引用,block 是復制其引用地址
來實現訪問的。block可以修改__block 修飾的外部變量的值。
3、Block的存儲域及copy操作
先來思考一下:Block是存儲在棧上還是堆上呢?
其實,block有三種類型:
- 全局塊(_NSConcreteGlobalBlock)
- 棧塊(_NSConcreteStackBlock)
- 堆塊(_NSConcreteMallocBlock)
全局塊存在于全局內存中, 相當于單例.
棧塊存在于棧內存中, 超出其作用域則馬上被銷毀
堆塊存在于堆內存中, 是一個帶引用計數的對象, 需要自行管理其內存
簡而言之,存儲在棧中的Block就是棧塊、存儲在堆中的就是堆塊、既不在棧中也不在堆中的塊就是全局塊。
遇到一個Block,我們怎么這個Block的存儲位置呢?
(1)Block不訪問外界變量(包括棧中和堆中的變量)
Block 既不在棧又不在堆中,在代碼段中,ARC和MRC下都是如此。此時為全局塊。
(2)Block訪問外界變量
MRC 環境下:訪問外界變量的 Block 默認存儲棧中。
ARC 環境下:訪問外界變量的 Block 默認存儲在堆中(實際是放在棧區,然后ARC情況下自動又拷貝到堆區),自動釋放。
4、防止 Block 循環引用
Block 循環引用的情況:
某個類將 block 作為自己的屬性變量,然后該類在 block 的方法體里面又使用了該類本身,如下:
self.someBlock = ^(Type var){
[self dosomething];
};
解決辦法:
(1)ARC 下:使用 __weak
__weak typeof(self) weakSelf = self;
self.someBlock = ^(Type var){
[weakSelf dosomething];
};
(2)MRC 下:使用 __block
__block typeof(self) blockSelf = self;
self.someBlock = ^(Type var){
[blockSelf dosomething];
};
值得注意的是,在ARC下,使用 __block 也有可能帶來的循環引用,如下:
// 循環引用 self -> _attributBlock -> tmp -> self
typedef void (^Block)();
@interface TestObj : NSObject
{
Block _attributBlock;
}
@end
@implementation TestObj
- (id)init {
self = [super init];
__block id tmp = self;
self.attributBlock = ^{
NSLog(@"Self = %@",tmp);
tmp = nil;
};
}
- (void)execBlock {
self.attributBlock();
}
@end
// 使用類
id obj = [[TestObj alloc] init];
[obj execBlock]; // 如果不調用此方法,tmp 永遠不會置 nil,內存泄露會一直在
5、有時候我們經常也會被問到block為什么 常使用copy關鍵字?
block 使用 copy 是從 MRC遺留下來的“傳統”,在 MRC 中,方法內部的 block 是在棧區的,使用 copy 可以把它放到堆區.在 ARC 中寫不寫都行:對于 block 使用 copy 還是 strong 效果是一樣的,但寫上 copy 也無傷大雅,還能時刻提醒我們:編譯器自動對 block 進行了 copy 操作。
如果不寫 copy ,該類的調用者有可能會忘記或者根本不知道“編譯器會自動對 block 進行了 copy 操作”
十二、為什么在block外部使用__weak修飾的同時需要在內部使用__strong修飾?
ARC環境中使用weak 的修飾符來修飾一個變量,防止其在block中被循環引用,而有些特殊情況下,我們在block中又使用__strong 來修飾這個在block外剛剛用__weak修飾的變量,這是因為在block中調用self會引起循環引用,而在block中需要對weakSelf進行__strong,保證代碼在執行到block中,self不會被釋放,當block執行完后,會自動釋放該strongSelf;
_weak是為了解決循環引用問題,(如果block和對象相互持有就會形成循環引用)
而__strong在Block內部修飾的對象,會保證,在使用這個對象在block內,
這個對象都不會被釋放,strongSelf僅僅是個局部變量,存在棧中,會在block執行結束后回收,不會再造成循環引用。
__strong主要是用在多線程中,防止對象被提前釋放。
十三、什么是離屏渲染?什么情況下會觸發?該如何應對?
離屏渲染就是在當前屏幕緩沖區以外,新開辟一個緩沖區進行操作。
離屏渲染出發的場景有以下:
- 圓角 (maskToBounds并用才會觸發)
- 圖層蒙版
- 陰影
- 光柵化
為什么要有離屏渲染?
大家高中物理應該學過顯示器是如何顯示圖像的:需要顯示的圖像經過CRT電子槍以極快的速度一行一行的掃描,掃描出來就呈現了一幀畫面,隨后電子槍又會回到初始位置循環掃描,形成了我們看到的圖片或視頻。
為了讓顯示器的顯示跟視頻控制器同步,當電子槍新掃描一行的時候,準備掃描的時發送一個水平同步信號(HSync信號),顯示器的刷新頻率就是HSync信號產生的頻率。然后CPU計算好frame等屬性,將計算好的內容交給GPU去渲染,GPU渲染好之后就會放入幀緩沖區。然后視頻控制器會按照HSync信號逐行讀取幀緩沖區的數據,經過可能的數模轉換傳遞給顯示器,就顯示出來了。具體的大家自行查找資料或詢問相關專業人士,這里只參考網上資料做一個簡單的描述。
離屏渲染的代價很高,想要進行離屏渲染,首選要創建一個新的緩沖區,屏幕渲染會有一個上下文環境的一個概念,離屏渲染的整個過程需要切換上下文環境,先從當前屏幕切換到離屏,等結束后,又要將上下文環境切換回來。這也是為什么會消耗性能的原因了。
由于垂直同步的機制,如果在一個 HSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是界面卡頓的原因。
為什么要避免離屏渲染?
CPU
GPU
在繪制渲染視圖時做了大量的工作。離屏渲染發生在 GPU
層面上,會創建新的渲染緩沖區,會觸發 OpenGL
的多通道渲染管線,圖形上下文的切換會造成額外的開銷,增加 GPU
工作量。如果 CPU
GPU
累計耗時 16.67
毫秒還沒有完成,就會造成卡頓掉幀。
圓角屬性
、蒙層遮罩
都會觸發離屏渲染。指定了以上屬性,標記了它在新的圖形上下文中,在未愈合之前,不可以用于顯示的時候就出發了離屏渲染。
-
在OpenGL中,GPU有2種渲染方式
- On-Screen Rendering:當前屏幕渲染,在當前用于顯示的屏幕緩沖區進行渲染操作
- Off-Screen Rendering:離屏渲染,在當前屏幕緩沖區以外新開辟一個緩沖區進行渲染操作
-
離屏渲染消耗性能的原因
- 需要創建新的緩沖區
- 離屏渲染的整個過程,需要多次切換上下文環境,先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束以后,將離屏緩沖區的渲染結果顯示到屏幕上,又需要將上下文環境從離屏切換到當前屏幕
-
哪些操作會觸發離屏渲染?
- 光柵化,layer.shouldRasterize = YES
- 遮罩,layer.mask
- 圓角,同時設置 layer.masksToBounds = YES、layer.cornerRadius大于0
- 考慮通過 CoreGraphics 繪制裁剪圓角,或者叫美工提供圓角圖片
- 陰影,layer.shadowXXX,如果設置了 layer.shadowPath 就不會產生離屏渲染
十三、反射是什么?可以舉出幾個應用場景么?
系統Foundation框架為我們提供了一些方法反射的API,我們可以通過這些API執行將字符串轉為SEL等操作。由于OC語言的動態性,這些操作都是發生在運行時的。
// SEL和字符串轉換
FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
// Class和字符串轉換
FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
FOUNDATION_EXPORT Class __nullable NSClassFromString(NSString *aClassName);
// Protocol和字符串轉換
FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto) NS_AVAILABLE(10_5, 2_0);
FOUNDATION_EXPORT Protocol * __nullable NSProtocolFromString(NSString *namestr) NS_AVAILABLE(10_5, 2_0);
通過這些方法,我們可以在運行時選擇創建那個實例,并動態選擇調用哪個方法。這些操作甚至可以由服務器傳回來的參數來控制,我們可以將服務器傳回來的類名和方法名,實例為我們的對象。
// 假設從服務器獲取JSON串,通過這個JSON串獲取需要創建的類為ViewController,并且調用這個類的getDataList方法。
Class class = NSClassFromString(@"ViewController");
ViewController *vc = [[class alloc] init];
SEL selector = NSSelectorFromString(@"getDataList");
[vc performSelector:selector];
反射機制使用技巧
假設有一天公司產品要實現一個需求:根據后臺推送過來的數據,進行動態頁面跳轉,跳轉到頁面后根據返回到數據執行對應的操作。
遇到這樣奇葩的需求,我們當然可以問產品都有哪些情況執行哪些方法,然后寫一大堆if else判斷或switch判斷。
但是這種方法實現起來太low了,而且不夠靈活,假設后續版本需求變了,還要往其他已有頁面中跳轉,這不就傻眼了嗎....
這種情況反射機制就派上用場了,我們可以用反射機制動態的創建類并執行方法。當然也可以通過runtime來實現這個功能,但是我們當前需求反射機制已經足夠滿足需求了,如果遇到更加復雜的需求可以考慮用runtime來實現。
這時候就需要和后臺配合了,我們首先需要和后臺商量好返回的數據結構,以及數據格式、類型等,返回后我們按照和后臺約定的格式,根據后臺返回的信息,直接進行反射和調用即可。
假設和后臺約定格式如下:
@{
// 類名
@"className" : @"UserListViewController",
// 數據參數
@"propertys" : @{ @"name": @"liuxiaozhuang",
@"age": @3 },
// 調用方法名
@"method" : @"refreshUserInformation"
};
定義一個UserListViewController
類,這個類用于測試,在實際使用中可能會有多個這樣的控制器類。
#import <UIKit/UIKit.h>
// 由于使用的KVC賦值,如果不想把這兩個屬性暴露出來,把這兩個屬性寫在.m文件也可以
@interface UserListViewController : UIViewController
@property (nonatomic,strong) NSString *name;/*!< 用戶名 */
@property (nonatomic,strong) NSNumber *age;/*!< 用戶年齡 */
/** 使用反射機制反射為SEL后,調用的方法 */
- (void)refreshUserInformation;
@end
下面通過反射機制簡單實現了控制器跳轉的方法,在實際使用中再根據業務需求進行修改即可。因為這篇文章主要是講反射機制,所以沒有使用runtime代碼。
簡單封裝的頁面跳轉方法,只是做演示,代碼都是沒問題的,使用時可以根據業務需求進行修改。
- (void)remoteNotificationDictionary:(NSDictionary *)dict {
// 根據字典字段反射出我們想要的類,并初始化控制器
Class class = NSClassFromString(dict[@"className"]);
UIViewController *vc = [[class alloc] init];
// 獲取參數列表,使用枚舉的方式,對控制器屬性進行KVC賦值
NSDictionary *parameter = dict[@"propertys"];
[parameter enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
// 在屬性賦值時,做容錯處理,防止因為后臺數據導致的異常
if ([vc respondsToSelector:NSSelectorFromString(key)]) {
[vc setValue:obj forKey:key];
}
}];
[self.navigationController pushViewController:vc animated:YES];
// 從字典中獲取方法名,并調用對應的方法
SEL selector = NSSelectorFromString(dict[@"method"]);
[vc performSelector:selector];
}
十四、有哪些場景是NSOperation比GCD更容易實現的?
GCD是一套 C 語言API,執行和操作簡單高效,因此NSOperation底層也通過GCD實現,這是他們之間最本質的區別.因此如果希望
自定義任務
,建議使用NSOperation;依賴關系,NSOperation可以設置
操作之間的依賴
(可以跨隊列設置),GCD無法設置依賴關系,不過可以通過同步來實現這種效果;KVO(鍵值對觀察),NSOperation容易
判斷操作當前的狀態
(是否執行,是否取消等),對此GCD無法通過KVO進行判斷;優先級,NSOperation可以
設置自身的優先級
,但是優先級高的不一定先執行,GCD只能設置隊列的優先級,如果要區分block任務的優先級,需要很復雜的代碼才能實現;繼承,NSOperation是一個抽象類.實際開發中常用的是它的兩個子類:
NSInvocationOperation和NSBlockOperation
,同樣我們可以自定義NSOperation,GCD執行任務可以自由組裝,沒有繼承那么高的代碼復用度;效率,直接使用GCD效率確實會更高效,NSOperation會多一點開銷,但是通過NSOperation可以獲得依賴,優先級,繼承,鍵值對觀察這些優勢,相對于多的那么一點開銷確實很劃算,魚和熊掌不可得兼,取舍在于開發者自己;
可以
隨時取消準備執行的任務
(已經在執行的不能取消),GCD沒法停止已經加入queue 的 block(雖然也能實現,但是需要很復雜的代碼)
基于GCD簡單高效,更強的執行能力,操作不太復雜的時候,優先選用GCD;而比較復雜的任務可以自己通過NSOperation實現.
十五、App 啟動優化策略?最好結合啟動流程來說
App啟動過程
解析Info.plist
加載相關信息,例如如閃屏
沙箱建立、權限檢查Mach-O加載
如果是胖二進制文件,尋找合適當前CPU類別的部分
加載所有依賴的Mach-O文件(遞歸調用Mach-O加載的方法)
定位內部、外部指針引用,例如字符串、函數等
執行聲明為attribute((constructor))的C函數
加載類擴展(Category)中的方法
C++靜態對象加載、調用ObjC的 +load 函數程序執行
調用main()
調用UIApplicationMain()
調用applicationWillFinishLaunching
在各種App性能的指標中,哪一此屬于啟動性能的范疇,哪一些則于App的流暢度性能?我認為應該首先把啟動過程分為四個部分:
main()函數之前
main()函數之后至applicationWillFinishLaunching完成
App完成所有本地數據的加載并將相應的信息展示給用戶
App完成所有聯網數據的加載并將相應的信息展示給用戶
- 移除不需要用到的動態庫
- 移除不需要用到的類
- 合并功能類似的類和擴展(Category)
- 壓縮資源圖片
- 優化applicationWillFinishLaunching
- 優化rootViewController加載
十六、App 無痕埋點的思路了解么?你認為理想的無痕埋點系統應該具備哪些特點
參考:http://www.lxweimin.com/p/b32b103356ea
十七、App 網絡層有哪些優化策略?
https://casatwy.com/iosying-yong-jia-gou-tan-wang-luo-ceng-she-ji-fang-an.html
十八、你知道有哪些情況會導致app崩潰,分別可以用什么方法攔截并化解?
1、NSInvalidArgumentException 異常
向容器加入nil,引起的崩潰
。hook容器添加方法,進行判斷。
https://github.com/jasenhuang/NSObjectSafe
2、 SIGSEGV 異常
SIGSEGV是當SEGV發生的時候,讓代碼終止的標識。 當去訪問沒有被開辟的內存或者已經被釋放的內存
時,就會發生這樣的異常。另外,在低內存的時候,也可能會產生這樣的異常。
3、 NSRangeException 異常
造成這個異常,就是越界異常
了,在iOS中我們經常碰到的越界異常有兩種,一種是數組越界,一種字符串截取越界
4、SIGPIPE 異常
先解釋一下什么是SIGPIPE異常,通俗一點的描述是這樣的:對一個端已經關閉的socket調用兩次write,第二次write將會產生SIGPIPE信號,該信號默認結束進程。
SIGABRT 異常 這是一個讓程序終止的標識,會在斷言、app內部、操作系統用終止方法拋出。通常發生在異步執行系統方法的時候。如CoreData、NSUserDefaults等,還有一些其他的系統多線程操作。 注意:這并不一定意味著是系統代碼存在bug,代碼僅僅是成了無效狀態,或者異常狀態。
http://www.lxweimin.com/p/c7efbc283480
5、方法沒有找到接受者異常
十九、你知道有哪些情況會導致app卡頓,分別可以用什么方法來避免?
參考 https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/
一、卡頓原因:
- CPU 資源消耗原因和解決方案:
1、對象創建
不需要響應觸摸事件的控件,用 CALayer 顯示會更加合適、盡量推遲對象創建的時間,并把對象的創建分散到多個任務中去
2、對象調整
應該盡量減少不必要的屬性修改
3、對象銷毀
如果對象可以放到后臺線程去釋放,那就挪到后臺線程去。這里有個小 Tip:把對象捕獲到 block 中,然后扔到后臺隊列去隨便發送個消息以避免編譯器警告,就可以讓對象在后臺線程銷毀了
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});
4、布局計算
盡量提前計算好布局,在需要時一次性調整好對應屬性,而不要多次、頻繁的計算和調整這些屬性。
5、文本計算、文本渲染
可以使用 CoreText 排版對象
6、 圖像的繪制
這個最常見的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是線程安全的,所以圖像的繪制可以很容易的放到后臺線程進行。一個簡單異步繪制的過程大致如下(實際情況會比這個復雜得多,但原理基本一致):
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}