iOS面試題
本面試題為個(gè)人使用版本,如后續(xù)流傳出去,請轉(zhuǎn)發(fā)的朋友務(wù)必注釋一下,答案正確性有待商榷,本人的答案不代表權(quán)威,僅僅是個(gè)人理解。 文章內(nèi)部有寫混亂,將就著看吧。另外大部分圖片加載不出來,,MARKDown格式也不太統(tǒng)一(各平臺(tái)不一樣),由于博主太懶不想改,不過不影響最終效果。
更新日志
- 2020年08月17日 更新了第23條的新的引申,關(guān)于NSTimer循環(huán)引用的根本原因, 以及優(yōu)化方案
一、硬技術(shù)篇
1.對象方法和類方法的區(qū)別?
- 對象方法能個(gè)訪問成員變量。
- 類方法中不能直接調(diào)用對象方法,想要調(diào)用對象方法,必須創(chuàng)建或者傳入對象。
- 類方法可以和對象方法重名。
引伸1. 如果在類方法中調(diào)用self 會(huì)有什么問題?
- 在 實(shí)例方法中self不可以調(diào)用類方法,此時(shí)的self不是Class。
- 在類方法中self可以調(diào)用其他類方法。
- 在類方法中self不可以調(diào)用實(shí)例方法。
- 總結(jié):類方法中的self,是class/ 實(shí)例方法中self是對象的首地址。
如果你正在面試,或者正準(zhǔn)備跳槽,不妨看看我精心總結(jié)的iOS大廠面試資料:https://gitee.com/Mcci7/i-oser 來獲取一份詳細(xì)的大廠面試資料 為你的跳槽加薪多一份保障
引申2. 講一下對象,類對象,元類,跟元類結(jié)構(gòu)體的組成以及他們是如何相關(guān)聯(lián)的?
- 對象的結(jié)構(gòu)體當(dāng)中存放著isa指針和成員變量,isa指針指向類對象
- 類對象的isa指針指向元類,元類的isa指針指向NSObject的元類
- 類對象和元類的結(jié)構(gòu)體有isa,superClass,cache等等
引申3. 為什么對象方法中沒有保存對象結(jié)構(gòu)體里面,而是保存在類對象的結(jié)構(gòu)體里面?
- 方法是每個(gè)對象相互可以共用的,如果每個(gè)對象都存儲(chǔ)一份方法列表太浪費(fèi)內(nèi)存,由于對象的isa是指向類對象的,當(dāng)調(diào)用的時(shí)候, 直接去類對象中去查找就可以了,節(jié)約了很多內(nèi)存空間。
引申4. 類方法存在哪里? 為什么要有元類的存在?
- 所有的類自身也是一個(gè)對象,我們可以向這個(gè)對象發(fā)送消息(即調(diào)用類方法)。
為了調(diào)用類方法,這個(gè)類的isa指針必須指向一個(gè)包含這些類方法的一個(gè)objc_class結(jié)構(gòu)體。這就引出了meta-class的概念,元類中保存了創(chuàng)建類對象以及類方法所需的所有信息。
引申5. 什么是野指針?
- 野指針就是指向一個(gè)被釋放或者被收回的對象,但是對指向該對象的指針沒有做任何修改,以至于該指針讓指向已經(jīng)回收后的內(nèi)存地址。
- 其中訪問野指針是沒有問題的,使用野指針的時(shí)候會(huì)出現(xiàn)崩潰Crash!樣例如下
__unsafe_unretained UIView *testObj = [[UIView alloc] init];
NSLog(@"testObj 指針指向的地址:%p 指針本身的地址:%p", testObj, &testObj);
[testObj setNeedsLayout];
// 可以看到NSlog打印不會(huì)閃退,調(diào)用[testObj setNeedsLayout];會(huì)閃退
復(fù)制代碼
引申6. 如何檢測野指針?
這是網(wǎng)友總結(jié)的,有興趣的可以看下:www.lxweimin.com/p/9fd4dc046… 本人,也就是看看樂呵,其原理啥的,見仁見智吧。開發(fā)行業(yè)太j8難了!
引申7. 導(dǎo)致Crash的原因有哪些?
1、找不到方法的實(shí)現(xiàn)unrecognized selector sent to instance 2、KVC造成的crash 3、EXC_BAD_ACCESS 4、KVO引起的崩潰 5、集合類相關(guān)崩潰 6、多線程中的崩潰 7、Socket長連接,進(jìn)入后臺(tái)沒有關(guān)閉 8、Watch Dog超時(shí)造成的crash 9、后臺(tái)返回NSNull導(dǎo)致的崩潰,多見于Java做后臺(tái)服務(wù)器開發(fā)語言
引申8. 不使用第三方,如何知道已經(jīng)上線的App崩潰問題, 具體到哪一個(gè)類的哪一個(gè)方法的?
大致實(shí)現(xiàn)方式如下。
- 使用NSSetUncaughtExceptionHandler可以統(tǒng)計(jì)閃退的信息。
- 將統(tǒng)計(jì)到的信息以data的形式 利用網(wǎng)絡(luò)請求發(fā)給后臺(tái)
- 在后臺(tái)收集信息,進(jìn)行排查
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
NSSetUncaughtExceptionHandler(&my_uncaught_exception_handler);
return YES;
}
static void my_uncaught_exception_handler (NSException *exception) {
//這里可以取到 NSException 信息
NSLog(@"***********************************************");
NSLog(@"%@",exception);
NSLog(@"%@",exception.callStackReturnAddresses);
NSLog(@"%@",exception.callStackSymbols);
NSLog(@"***********************************************");
}
復(fù)制代碼
實(shí)現(xiàn)方式如: blog.csdn.net/u013896628/…
iOS中內(nèi)省的幾個(gè)方法?
- isMemberOfClass //對象是否是某個(gè)類型的對象
- isKindOfClass //對象是否是某個(gè)類型或某個(gè)類型子類的對象
- isSubclassOfClass //某個(gè)類對象是否是另一個(gè)類型的子類
- isAncestorOfObject //某個(gè)類對象是否是另一個(gè)類型的父類
- respondsToSelector //是否能響應(yīng)某個(gè)方法
- conformsToProtocol //是否遵循某個(gè)協(xié)議
引申 2. ==、 isEqualToString、isEqual區(qū)別?
- == ,比較的是兩個(gè)指針的值 (內(nèi)存地址是否相同)。
- isEqualToString, 比較的是兩個(gè)字符串是否相等。
- isEqual 判斷兩個(gè)對象在類型和值上是否都一樣。
引申 3. class方法和object_getClass方法有什么區(qū)別?
- 實(shí)例class方法直接返回object_getClass(self)
- 類class直接返回self
- 而object_getClass(類對象),則返回的是元類
3.深拷貝和淺拷貝
- 所謂深淺指的是是否創(chuàng)建了一個(gè)新的對象(開辟了新的內(nèi)存地址)還是僅僅做了指針的復(fù)制。
- copy和mutableCopy針對的是可變和不可變,凡涉及copy結(jié)果均變成不可變,mutableCopy均變成可變。
- mutableCopy均是深復(fù)制。
- copy操作不可變的是淺復(fù)制,操作可變的是深賦值。
4.NSString類型為什么要用copy修飾 ?
- 主要是防止NSString被修改,如果沒有修改的說法用Strong也行。
- 當(dāng)NSString的賦值來源是NSString時(shí),strong和copy作用相同。
- 當(dāng)NSString的賦值來源是NSMutableString,copy會(huì)做深拷貝,重新生成一個(gè)新的對象,修改賦值來源不會(huì)影響NSString的值。
5.iOS中block 捕獲外部局部變量實(shí)際上發(fā)生了什么?__block 中又做了什么?
block 捕獲的是當(dāng)前在block內(nèi)部執(zhí)行的外部局部變量的瞬時(shí)值, 為什么說瞬時(shí)值呢? 看一下C++源碼中得知, 其內(nèi)部代碼在捕獲的同時(shí)
其實(shí)block底層生成了一個(gè)和外部變量相同名稱的屬性值如果內(nèi)部修改值,其實(shí)修改的是捕獲之前的值,其捕獲的內(nèi)部的值因代碼只做了一次捕獲,并沒有做再一次的捕獲,所以block里面不可以修改值。
如果當(dāng)前捕獲的為對象類型,其block內(nèi)部可以認(rèn)為重新創(chuàng)建了一個(gè)指向當(dāng)前對象內(nèi)存地址的指針(堆),操控內(nèi)部操作的東西均為同一塊內(nèi)存地址,所以可以修改當(dāng)前內(nèi)部的對象里面的屬性,但是不能直接修改當(dāng)前的指針(無法直接修改棧中的內(nèi)容)(即重新生成一個(gè)新的內(nèi)存地址)。其原理和捕獲基本數(shù)據(jù)類型一致。
說白了, block內(nèi)部可以修改的是堆中的內(nèi)容, 但不能直接修改棧中的任何東西。
- 如果加上__block 在運(yùn)行時(shí)創(chuàng)建了一個(gè)外部變量的“副本”屬性,把棧中的內(nèi)存地址放到了堆中進(jìn)而在block內(nèi)部也能修改外部變量的值。
6.iOS Block為什么用copy修飾?
- block 是一個(gè)對象
- MRC的時(shí)候 block 在創(chuàng)建的時(shí)候,它的內(nèi)存比較奇葩,非得分配到棧上,而不是在傳統(tǒng)的堆上,它本身的作用于就屬于創(chuàng)建的時(shí)候(見光死,夭折),一旦在創(chuàng)建時(shí)候的作用于外面調(diào)用它會(huì)導(dǎo)致崩潰。
- 所以,利用copy把原本在棧上的復(fù)制到堆里面,就保住了它。
- **ARC的時(shí)候 由于ARC中已經(jīng)看不到棧中的block了。用strong和copy 一樣 隨意, 用copy是遵循其傳統(tǒng), **
7. 為什么分類中不能創(chuàng)建屬性Property(runtime除外)?
分類的實(shí)現(xiàn)原理是將category中的方法,屬性,協(xié)議數(shù)據(jù)放在category_t結(jié)構(gòu)體中,然后將結(jié)構(gòu)體內(nèi)的方法列表拷貝到類對象的方法列表中。 Category可以添加屬性,但是并不會(huì)自動(dòng)生成成員變量及set/get方法。因?yàn)閏ategory_t結(jié)構(gòu)體中并不存在成員變量。通過之前對對象的分析我們知道成員變量是存放在實(shí)例對象中的,并且編譯的那一刻就已經(jīng)決定好了。而分類是在運(yùn)行時(shí)才去加載的。那么我們就無法再程序運(yùn)行時(shí)將分類的成員變量中添加到實(shí)例對象的結(jié)構(gòu)體中。因此分類中不可以添加成員變量。
在往深一點(diǎn)的回答就是 類在內(nèi)存中的位置是編譯時(shí)期決定的, 之后再修改代碼也不會(huì)改變內(nèi)存中的位置,class_ro_t 的屬性在運(yùn)行期間就不能再改變了, 再添加方法是會(huì)修改class_rw_t 的methods 而不是class_ro_t 中的 baseMethods
引伸:關(guān)聯(lián)對象的原理?
- 關(guān)聯(lián)對象并不是存儲(chǔ)在關(guān)聯(lián)對象本身內(nèi)存中,而是存儲(chǔ)在全局統(tǒng)一的一個(gè)容器中;
- 由 AssociationsManager 管理并在它維護(hù)的一個(gè)單例 Hash 表 AssociationsHashMap 中存儲(chǔ);
- 使用 AssociationsManagerLock 自旋鎖保證了線程安全
引伸:分類可以添加那些內(nèi)容?
- 實(shí)例方法,類方法,協(xié)議,屬性
引伸:Category 的實(shí)現(xiàn)原理?
- Category 在剛剛編譯完成的時(shí)候, 和原來的類是分開的,只有在程序運(yùn)行起來的時(shí)候, 通過runtime合并在一起。
引申 使用runtime Associate方法關(guān)聯(lián)的對象,需要在主對象dealloc的時(shí)候釋放么?
- 不需要,被關(guān)聯(lián)的對象的生命周期內(nèi)要比對象本身釋放晚很多, 它們會(huì)在被 NSObject -dealloc 調(diào)用的 object_dispose() 方法中釋放。
引申 能否向編譯后得到的類中增加實(shí)例變量, 能否向運(yùn)行時(shí)創(chuàng)建的類中添加實(shí)力變量?
- 不能再編譯后得到的類中增加實(shí)例變量。因?yàn)榫幾g后的類已經(jīng)注冊在runtime中, 類結(jié)構(gòu)體中objc_ivar_list 實(shí)例變量的鏈表和objc_ivar_list 實(shí)例變量的內(nèi)存大小已經(jīng)確定,所以不能向存在的類中添加實(shí)例變量
- 能在運(yùn)行時(shí)創(chuàng)建的類中添加實(shí)力變量。調(diào)用class_addIvar 函數(shù)
引申 主類執(zhí)行了foo方法,分類也執(zhí)行了foo方法,在執(zhí)行的地方執(zhí)行了foo方法,主類的foo會(huì)被覆蓋么? 如果想只想執(zhí)行主類的foo方法,如何去做?
- 主類的方法被分類的foo覆蓋了,其實(shí)分類并沒有覆蓋主類的foo方法,只是分類的方法排在方法列表前面,主類的方法列表被擠到了后面, 調(diào)用的時(shí)候會(huì)首先找到第一次出現(xiàn)的方法。
- 如果想要只是執(zhí)行主類的方法,可逆序遍歷方法列表,第一次遍歷到的foo方法就是主類的方法
- (void)foo{
[類 invokeOriginalMethod:self selector:_cmd];
}
+ (void)invokeOriginalMethod:(id)target selector:(SEL)selector {
uint count;
Method *list = class_copyMethodList([target class], &count);
for ( int i = count - 1 ; i >= 0; i--) {
Method method = list[i];
SEL name = method_getName(method);
IMP imp = method_getImplementation(method);
if (name == selector) {
((void (*)(id, SEL))imp)(target, name);
break;
}
}
free(list);
}
復(fù)制代碼
8. load 和 initilze 的調(diào)用情況,以及子類的調(diào)用順序問題?
① 調(diào)用時(shí)刻:+load方法會(huì)在Runtime加載類、分類時(shí)調(diào)用(不管有沒有用到這些類,在程序運(yùn)行起來的時(shí)候都會(huì)加載進(jìn)內(nèi)存,并調(diào)用+load方法); 每個(gè)類、分類的+load,在程序運(yùn)行過程中只調(diào)用一次(除非開發(fā)者手動(dòng)調(diào)用)。
② 調(diào)用方式: 系統(tǒng)自動(dòng)調(diào)用+load方式為直接通過函數(shù)地址調(diào)用,開發(fā)者手動(dòng)調(diào)用+load方式為消息機(jī)制objc_msgSend函數(shù)調(diào)用。
③ 調(diào)用順序: 先調(diào)用類的+load,按照編譯先后順序調(diào)用(先編譯,先調(diào)用),調(diào)用子類的+load之前會(huì)先調(diào)用父類的+load; 再調(diào)用分類的+load,按照編譯先后順序調(diào)用(先編譯,先調(diào)用)(注意:分類的其它方法是:后編譯,優(yōu)先調(diào)用)。
① 調(diào)用時(shí)刻:+initialize方法會(huì)在類第一次接收到消息時(shí)調(diào)用。 如果子類沒有實(shí)現(xiàn)+initialize方法,會(huì)調(diào)用父類的+initialize,所以父類的+initialize方法可能會(huì)被調(diào)用多次,但不代表父類初始化多次,每個(gè)類只會(huì)初始化一次。
② 調(diào)用方式: 消息機(jī)制objc_msgSend函數(shù)調(diào)用。
③ 調(diào)用順序: 先調(diào)用父類的+initialize,再調(diào)用子類的+initialize (先初識(shí)化父類,再初始化子類)
- +initialize方法的調(diào)用方式為消息機(jī)制,而非像+load那樣直接通過函數(shù)地址調(diào)用。
9. 什么是線程安全?
- 多條線程同時(shí)訪問一段代碼,不會(huì)造成數(shù)據(jù)混亂的情況
10. 你接觸到的項(xiàng)目,哪些場景運(yùn)用到了線程安全?
答: 舉例說明,12306 同一列火車的車票, 同一時(shí)間段多人搶票! 如何解決 互斥鎖使用格式
synchronized(鎖對象) { // 需要鎖定的代碼 }
注意:鎖定1份代碼只用1把鎖,用多把鎖是無效的
Tips: 互斥鎖的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):能有效防止因多線程搶奪資源造成的數(shù)據(jù)安全問題
缺點(diǎn):需要消耗大量的CPU資源
互斥鎖的使用前提:多條線程搶奪同一塊資源
相關(guān)專業(yè)術(shù)語:線程同步,多條線程按順序地執(zhí)行任務(wù)
互斥鎖,就是使用了線程同步技術(shù)
Objective-C中的原子和非原子屬性
OC在定義屬性時(shí)有nonatomic和atomic兩種選擇
atomic:原子屬性,為setter/getter方法都加鎖(默認(rèn)就是atomic)
nonatomic:非原子屬性,不加鎖
atomic加鎖原理:
property (assign, atomic) int age;
- (void)setAge:(int)age
{
@synchronized(self) {
_age = age;
}
}
- (int)age {
int age1 = 0;
@synchronized(self) {
age1 = _age;
}
}
原子和非原子屬性的選擇
nonatomic和atomic對比
atomic:線程安全,需要消耗大量的資源
nonatomic:非線程安全,適合內(nèi)存小的移動(dòng)設(shè)備
iOS開發(fā)的建議
所有屬性都聲明為nonatomic
盡量避免多線程搶奪同一塊資源
盡量將加鎖、資源搶奪的業(yè)務(wù)邏輯交給服務(wù)器端處理,減小移動(dòng)客戶端的壓力
atomic就一定能保證線程安全么?
不能,還需要更深層的鎖定機(jī)制才可以,因?yàn)橐粋€(gè)線程在連續(xù)多次讀取某條屬性值的時(shí)候,與此同時(shí)別的線程也在改寫值,這樣還是會(huì)讀取到不同的屬性值! 或者 一個(gè)線程在獲取當(dāng)前屬性的值, 另外一個(gè)線程把這個(gè)屬性釋放調(diào)了, 有可能造成崩潰
復(fù)制代碼
11. 你實(shí)現(xiàn)過單例模式么? 你能用幾種實(shí)現(xiàn)方案?
1. 運(yùn)用GCD:
import "Manager.h"
implementation Manager
+ (Manager *)sharedManager {
static dispatch_once_t onceToken;
static Manager * sharedManager;
dispatch_once(&onceToken, ^{
sharedManager=[[Manager alloc] init];
});
return sharedManager;
}
end
注明:dispatch_once這個(gè)函數(shù),它可以保證整個(gè)應(yīng)用程序生命周期中某段代碼只被執(zhí)行一次!
2. 不使用GCD的方式:
static Manager *manager;
implementation Manager
+ (Manager *)defaultManager {
if(!manager)
manager=[[self allocWithZone:NULL] init];
return manager;
}
end
3. 正常的完整版本
+(id)shareInstance{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if(_instance == nil)
_instance = [MyClass alloc] init];
});
return _instance;
}
//重寫allocWithZone,里面實(shí)現(xiàn)跟方法一,方法二一致就行.
+(id)allocWithZone:(struct _NSZone *)zone{
return [self shareInstance];
}
//保證copy時(shí)相同
-(id)copyWithZone:(NSZone *)zone{
return _instance;
}
// 方法3創(chuàng)建的目的的是 為了方式開發(fā)者在調(diào)用單例的時(shí)候并沒有用shareInstance方法來創(chuàng)建 而是用的alloc 或者copy的形式創(chuàng)建造成單例不一致的情況
//
復(fù)制代碼
引申1. 單例是怎么銷毀的?
//必須把static dispatch_once_t onceToken; 這個(gè)拿到函數(shù)體外,成為全局的.
+ (void)attempDealloc {
onceToken = 0; // 只有置成0,GCD才會(huì)認(rèn)為它從未執(zhí)行過.它默認(rèn)為0,這樣才能保證下次再次調(diào)用shareInstance的時(shí)候,再次創(chuàng)建對象.
_sharedInstance = nil;
}
dispatch_once_t 的工作原理是,static修飾會(huì)默認(rèn)將其初始化為0, 當(dāng)且僅當(dāng)其為0的時(shí)候dispatch_once(&onceToken, ^{})這個(gè)函數(shù)才能被調(diào)用, 如果執(zhí)行了這個(gè)函數(shù) 這個(gè)dispatch_once_t 靜態(tài)變成- 1了 就永遠(yuǎn)不會(huì)被調(diào)用
復(fù)制代碼
引申2. 不使用dispatch_once 如何 實(shí)現(xiàn)單例
1.第一種方式,重寫+allocWithZone:方法;
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static id instance = nil;
@synchronized (self) { // 互斥鎖
if (instance == nil) {
instance = [super allocWithZone:zone];
}
}
return instance;
}
2.第二種方式,不用重寫+allocWithZone:方法,而是直接用@synchronized 來保證線程安全,其它與上面這個(gè)方法一樣;
+ (instancetype)sharedSingleton {
static id instance = nil;
@synchronized (self) {
if (!instance) {
instance = [[self alloc] init];
}
}
return instance;
}
復(fù)制代碼
12. 項(xiàng)目開發(fā)中,你用單例都做了什么?
答 :整個(gè)程序公用一份資源的時(shí)候 例如 :
- 設(shè)置單例類訪問應(yīng)用的配置信息
- 用戶的個(gè)人信息登錄后用的NSUserDefaults存儲(chǔ),對登錄類進(jìn)一步采用單例封裝方便全局訪問
- 防止一個(gè)單例對 應(yīng)用 多處 對同意本地?cái)?shù)據(jù)庫存進(jìn)行操作
13.APNS的基本原理
- 基本
- 第一階段:應(yīng)用程序的服務(wù)器端把要發(fā)送的消息、目的iPhone的標(biāo)識(shí)打包,發(fā)給APNS。
- 第二階段:APNS在自身的已注冊Push服務(wù)的iPhone列表中,查找有相應(yīng)標(biāo)識(shí)的iPhone,并把消息發(fā)送到iPhone。
- 第三階段:iPhone把發(fā)來的消息傳遞給相應(yīng)的應(yīng)用程序,并且按照設(shè)定彈出Push通知。
- 詳細(xì)說明
首先是注冊
- Device(設(shè)備)連接APNs服務(wù)器并攜帶設(shè)備序列號(hào)(UUID)
- 連接成功,APNs經(jīng)過打包和處理產(chǎn)生devicetoken并返回給注冊的Device(設(shè)備)
- Device(設(shè)備)攜帶獲取的devicetoken發(fā)送到我們自己的應(yīng)用服務(wù)器
- 完成需要被推送的Device(設(shè)備)在APNs服務(wù)器和我們自己的應(yīng)用服務(wù)器的注冊
推送過程
- 1、首先手機(jī)裝有當(dāng)前的app,并且保持有網(wǎng)絡(luò)的情況下,APNs服務(wù)器會(huì)驗(yàn)證devicetoken,成功那個(gè)之后會(huì)處于一個(gè)長連接。 (這里會(huì)有面試問? 如果app也注冊成功了, 也下載了,也同意了打開推送功能, 這個(gè)時(shí)候在把App刪除了, 還能接受推送了么? )
- 2、當(dāng)我們推送消息的時(shí)候,我們的服務(wù)器按照指定格式進(jìn)行打包,結(jié)合devicetoken 一起發(fā)送給APNs服務(wù)器,
- 3、APNs 服務(wù)器將新消息推送到iOS 設(shè)備上,然后在設(shè)備屏幕上顯示出推送的消息。
- 4、iOS設(shè)備收到推送消息后, 會(huì)通知給我們的應(yīng)用程序并給予提示
// 推送過程如下圖 [圖片上傳失敗...(image-2bbbef-1647873407855)]()
14. RunLoop的基礎(chǔ)知識(shí)
- RunLoop模式有哪些?
答 : iOS中有五種RunLoop模式
NSDefaultRunLoopMode (默認(rèn)模式,有事件響應(yīng)的時(shí)候,會(huì)阻塞舊事件)
NSRunLoopCommonModes (普通模式,不會(huì)影響任何事件)
UITrackingRunLoopMode (只能是有事件的時(shí)候才會(huì)響應(yīng)的模式)
還有兩種系統(tǒng)級別的模式
一個(gè)是app剛啟動(dòng)的時(shí)候會(huì)執(zhí)行一次
另外一個(gè)是系統(tǒng)檢測app各種事件的模式
復(fù)制代碼
- RunLoop的基本執(zhí)行原理
答 : 原本系統(tǒng)就有一個(gè)runloop在檢測App內(nèi)部的行為或事件,當(dāng)輸入源(用戶的直接或者間接的操作)有“執(zhí)行操作”的時(shí)候, 系統(tǒng)的runloop會(huì)監(jiān)聽輸入源的狀態(tài), 進(jìn)而在系統(tǒng)內(nèi)部做一些對應(yīng)的相應(yīng)操作。 處理完成后,會(huì)自動(dòng)回到睡眠狀態(tài), 等待下一次被喚醒,
RunLoop和線程的關(guān)系
RunLoop的作用就是用來管理線程的, 當(dāng)線程的RunLoop開啟之后,線程就會(huì)在執(zhí)行完成任務(wù)后,進(jìn)入休眠狀態(tài),隨時(shí)等待接收新的任務(wù),而不是退出。
為什么只有主線程的
runloop
是開啟的程序開啟之后,要一直運(yùn)行,不會(huì)退出。 說白了就是為了讓程序不死
如何保證一個(gè)線程永遠(yuǎn)不死(常駐線程)
// 先創(chuàng)建一個(gè)線程用于測試
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(play) object:nil];
[thread start];
// 保證一個(gè)線程永遠(yuǎn)不死
[[NSRunLoop currentRunLoop] addPort:[NSPort port] -forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 在合適的地方處理線程的事件處理
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:NO];
復(fù)制代碼
15. weak屬性?
1\. 說說你理解weak屬性?
復(fù)制代碼
1.實(shí)現(xiàn)weak后,為什么對象釋放后會(huì)自動(dòng)為nil?
runtime 對注冊的類, 會(huì)進(jìn)行布局,
對于 weak 對象會(huì)放入一個(gè) hash 表中。
用 weak 指向的對象內(nèi)存地址作為 key,
Value是weak指針的地址數(shù)組。
當(dāng)釋放的時(shí)候,其內(nèi)部會(huì)通過當(dāng)前的key找到所有的weak指針指向的數(shù)組
然后遍歷這個(gè)數(shù)組把其中的數(shù)據(jù)設(shè)置為nil。
稍微詳細(xì)的說:在內(nèi)部底層源碼也同時(shí)和當(dāng)前對象相關(guān)聯(lián)得SideTable, 其內(nèi)部有三個(gè)屬性, 一個(gè)是一把自旋鎖,一個(gè)是引用計(jì)數(shù)器相關(guān),一個(gè)是維護(hù)weak生命得屬性得表
**SideTable**這個(gè)結(jié)構(gòu)體一樣的東西,可以花半個(gè)小時(shí)看一眼。
復(fù)制代碼
延伸
- objc中向一個(gè)nil對象發(fā)送消息將會(huì)發(fā)生什么?
首先 在尋找對象化的isa指針時(shí)就是0地址返回了, 所以不會(huì)有任何錯(cuò)誤, 也不會(huì)錯(cuò)誤
- objc在向一個(gè)對象發(fā)送消息時(shí),發(fā)生了什么?
- 首先是通過obj 的isa指針找到對應(yīng)的class
- 先去操作對象中的緩存方法列表中objc_cache中去尋找 當(dāng)前方法,如果找到就直接實(shí)現(xiàn)對應(yīng)IMP
- 如果在緩存中找不到,則在class中找到對用的Method list中對用foo
- 如果class中沒有找到對應(yīng)的foo, 就會(huì)去superClass中去找
- 如果找到了對應(yīng)的foo, 就會(huì)實(shí)現(xiàn)foo對應(yīng)的IMP
緩存方法列表, 就是每次執(zhí)行這個(gè)方法的時(shí)候都會(huì)做如此繁瑣的操作這樣太過于消耗性能,所以出現(xiàn)了一個(gè)objc_cache,這個(gè)會(huì)把當(dāng)前調(diào)用過的類中的方法做一個(gè)緩存, 當(dāng)前method_name作為key, method_IMP作為Value,當(dāng)再一次接收到消息的時(shí)候,直接通過objc_cache去找到對應(yīng)的foo的IMP即可, 避免每一次都去遍歷objc_method_list
如果一直沒有找到方法, 就會(huì)專用消息轉(zhuǎn)發(fā)機(jī)制,機(jī)制如下
// 動(dòng)態(tài)方法解析和轉(zhuǎn)發(fā)
上面的例子如果foo函數(shù)一直沒有被找到,通常情況下,會(huì)出現(xiàn)報(bào)錯(cuò),但是在報(bào)錯(cuò)之前,OC的運(yùn)行時(shí)給了我們?nèi)窝a(bǔ)救的機(jī)會(huì)
- Method resolution
- Fast forwarding
- Normal forwarding
1. Runtime 會(huì)發(fā)送 +resolveInstanceMethod: 或者 +resolveClassMethod: 嘗試去 resolve(重啟) 這個(gè)消息;
2. 如果 resolve 方法返回 NO,Runtime 就發(fā)送 -forwardingTargetForSelector: 允許你把這個(gè)消息轉(zhuǎn)發(fā)給另一個(gè)對象;
3. 如果沒有新的目標(biāo)對象返回, Runtime 就會(huì)發(fā)送 -methodSignatureForSelector: 和 -forwardInvocation: 消息。你可以發(fā)送 -invokeWithTarget: 消息來手動(dòng)轉(zhuǎn)發(fā)消息或者發(fā)送 -doesNotRecognizeSelector: 拋出異常。
復(fù)制代碼
16.UIView和CALayer是什么關(guān)系?
- 兩者最明顯的區(qū)別是 View可以接受并處理事件,而 Layer 不可以;
- 每個(gè) UIView 內(nèi)部都有一個(gè) CALayer 在背后提供內(nèi)容的繪制和顯示,并且 UIView 的尺寸樣式都由內(nèi)部的 Layer 所提供。兩者都有樹狀層級結(jié)構(gòu),layer 內(nèi)部有 SubLayers,View 內(nèi)部有 SubViews.但是 Layer 比 View 多了個(gè)AnchorPoint
- 在 View顯示的時(shí)候,UIView 做為 Layer 的 CALayerDelegate,View 的顯示內(nèi)容由內(nèi)部的 CALayer 的 display
CALayer 是默認(rèn)修改屬性支持隱式動(dòng)畫的,在給 UIView 的 Layer 做動(dòng)畫的時(shí)候,View 作為 Layer 的代理,Layer 通過 actionForLayer:forKey:向 View請求相應(yīng)的 action(動(dòng)畫行為)
- layer 內(nèi)部維護(hù)著三分 layer tree,分別是 presentLayer Tree(動(dòng)畫樹),modeLayer Tree(模型樹), Render Tree (渲染樹),在做 iOS動(dòng)畫的時(shí)候,我們修改動(dòng)畫的屬性,在動(dòng)畫的其實(shí)是 Layer 的 presentLayer的屬性值,而最終展示在界面上的其實(shí)是提供 View的modelLayer
復(fù)制代碼
16. @synthesize 和 @dynamic 分別有什么作用
- @property有兩個(gè)對應(yīng)的詞,一個(gè)是 @synthesize,一個(gè)是 @dynamic。如果 @synthesize和 @dynamic都沒寫,那么默認(rèn)的就是@syntheszie var = _var;
- @synthesize 的語義是如果你沒有手動(dòng)實(shí)現(xiàn) setter 方法和 getter 方法,那么編譯器會(huì)自動(dòng)為你加上這兩個(gè)方法。
- @dynamic 告訴編譯器:屬性的 setter 與 getter 方法由用戶自己實(shí)現(xiàn),不自動(dòng)生成。(當(dāng)然對于 readonly 的屬性只需提供 getter 即可)。假如一個(gè)屬性被聲明為 @dynamic var,然后你沒有提供 @setter方法和 @getter 方法,編譯的時(shí)候沒問題,但是當(dāng)程序運(yùn)行到 instance.var = someVar,由于缺 setter 方法會(huì)導(dǎo)致程序崩潰;或者當(dāng)運(yùn)行到 someVar = var 時(shí),由于缺 getter 方法同樣會(huì)導(dǎo)致崩潰。編譯時(shí)沒問題,運(yùn)行時(shí)才執(zhí)行相應(yīng)的方法,這就是所謂的動(dòng)態(tài)綁定。
復(fù)制代碼
17. static有什么作用?
static關(guān)鍵字可以修飾函數(shù)和變量,作用如下:
**隱藏**
通過static修飾的函數(shù)或者變量,在該文件中,所有位于這條語句之后的函數(shù)都可以訪問,而其他文件中的方法和函數(shù)則不行
**靜態(tài)變量**
類方法不可以訪問實(shí)例變量(函數(shù)),通過static修飾的實(shí)例變量(函數(shù)),可以被類 方法訪問;
**持久**
static修飾的變量,能且只能被初始化一次;
**默認(rèn)初始化**
static修飾的變量,默認(rèn)初始化為0;
復(fù)制代碼
18. objc在向一個(gè)對象發(fā)送消息時(shí),發(fā)生了什么?
- objc_msgSend(recicver, selecter..)
復(fù)制代碼
19. runloop是來做什么的?runloop和線程有什么關(guān)系?主線程默認(rèn)開啟了runloop么?子線程呢?
1\. runloop與線程是一一對應(yīng)的,一個(gè)runloop對應(yīng)一個(gè)核心的線程,為什么說是核心的,是因?yàn)閞unloop是可以嵌套的,但是核心的只能有一個(gè),他們的關(guān)系保存在一個(gè)全局的字典里。
2\. runloop是來管理線程的,當(dāng)線程的runloop被開啟后,線程會(huì)在執(zhí)行完任務(wù)后進(jìn)入休眠狀態(tài),有了任務(wù)就會(huì)被喚醒去執(zhí)行任務(wù)。runloop在第一次獲取時(shí)被創(chuàng)建,在線程結(jié)束時(shí)被銷毀。
3\. 對于主線程來說,runloop在程序一啟動(dòng)就默認(rèn)創(chuàng)建好了。
4\. 對于子線程來說, runloop是懶加載的,只有當(dāng)我們使用的時(shí)候才會(huì)創(chuàng)建,所以在子線程用定時(shí)器要注意:確保子線程的runloop被開啟,不然定時(shí)器不會(huì)回調(diào)。
復(fù)制代碼
20. 如何手動(dòng)觸發(fā)一個(gè)value的KVO
鍵值觀察通知依賴于 NSObject 的兩個(gè)方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一個(gè)被觀察屬性發(fā)生改變之前, willChangeValueForKey: 一定會(huì)被調(diào)用,這就 會(huì)記錄舊的值。而當(dāng)改變發(fā)生后, didChangeValueForKey: 會(huì)被調(diào)用,繼而 observeValueForKey:ofObject:change:context: 也會(huì)被調(diào)用。如果可以手動(dòng)實(shí)現(xiàn)這些調(diào)用,就可以實(shí)現(xiàn)“手動(dòng)觸發(fā)”了。
引申 0 如何給系統(tǒng)KVO設(shè)置篩選條件?
- 舉例:取消Person類age屬性的默認(rèn)KVO,設(shè)置age大于18時(shí),手動(dòng)觸發(fā)KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
- (void)setAge:(NSInteger)age {
if (age >= 18) {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
}else {
_age = age;
}
}
復(fù)制代碼
引申 1.通過KVC修改屬性會(huì)觸發(fā)KVO么?直接修改成員變量呢 ?
- 會(huì)觸發(fā)KVO。即使沒有聲明屬性,只有成員變量,只要accessInstanceVariablesDirectly返回的是YES,允許訪問其成員變量,那么不管有沒有調(diào)用setter方法,通過KVC修改成員變量的值,都能觸發(fā)KVO。這也說明通過KVC內(nèi)部實(shí)現(xiàn)了willChangeValueForKey:方法和didChangeValueForKey:方法
- 直接修改成員變量不會(huì)觸發(fā)KVO。直接修改成員變量內(nèi)部并沒有做處理只是單純的賦值,所以不會(huì)觸發(fā)。
引申 kvc的底層實(shí)現(xiàn)?
- 賦值方法setValue:forKey:的原理
(1)首先會(huì)按照順序依次查找setKey:方法和_setKey:方法,只要找到這兩個(gè)方法當(dāng)中的任何一個(gè)就直接傳遞參數(shù),調(diào)用方法;
(2)如果沒有找到setKey:和_setKey:方法,那么這個(gè)時(shí)候會(huì)查看accessInstanceVariablesDirectly方法的返回值,如果返回的是NO(也就是不允許直接訪問成員變量),那么會(huì)調(diào)用setValue:forUndefineKey:方法,并拋出異?!癗SUnknownKeyException”;
(3)如果accessInstanceVariablesDirectly方法返回的是YES,也就是說可以訪問其成員變量,那么就會(huì)按照順序依次查找 _key、_isKey、key、isKey這四個(gè)成員變量,如果查找到了,就直接賦值;如果依然沒有查到,那么會(huì)調(diào)用setValue:forUndefineKey:方法,并拋出異常“NSUnknownKeyException”。
- 取值方法valueForKey:的原理
(1)首先會(huì)按照順序依次查找getKey:、key、isKey、_key:這四個(gè)方法,只要找到這四個(gè)方法當(dāng)中的任何一個(gè)就直接調(diào)用該方法;
(2)如果沒有找到,那么這個(gè)時(shí)候會(huì)查看accessInstanceVariablesDirectly方法的返回值,如果返回的是NO(也就是不允許直接訪問成員變量),那么會(huì)調(diào)用valueforUndefineKey:方法,并拋出異?!癗SUnknownKeyException”;
(3)如果accessInstanceVariablesDirectly方法返回的是YES,也就是說可以訪問其成員變量,那么就會(huì)按照順序依次查找 _key、_isKey、key、isKey這四個(gè)成員變量,如果找到了,就直接取值;如果依然沒有找到成員變量,那么會(huì)調(diào)用valueforUndefineKey方法,并拋出異常“NSUnknownKeyException”。
21. ViewController生命周期
按照執(zhí)行順序排列:
1. initWithCoder:通過nib文件初始化時(shí)觸發(fā)。
2. awakeFromNib:nib文件被加載的時(shí)候,會(huì)發(fā)生一個(gè)awakeFromNib的消息到nib文件中的每個(gè)對象。
3. loadView:開始加載視圖控制器自帶的view。
4. viewDidLoad:視圖控制器的view被加載完成。
5. viewWillAppear:視圖控制器的view將要顯示在window上。
6. updateViewConstraints:視圖控制器的view開始更新AutoLayout約束。
7. viewWillLayoutSubviews:視圖控制器的view將要更新內(nèi)容視圖的位置。
8. viewDidLayoutSubviews:視圖控制器的view已經(jīng)更新視圖的位置。
9. viewDidAppear:視圖控制器的view已經(jīng)展示到window上。
10. viewWillDisappear:視圖控制器的view將要從window上消失。
11. viewDidDisappear:視圖控制器的view已經(jīng)從window上消失。
復(fù)制代碼
22.網(wǎng)絡(luò)協(xié)議
- TCP三次握手和四次揮手?
三次握手
1.客戶端向服務(wù)端發(fā)起請求鏈接,首先發(fā)送SYN報(bào)文,SYN=1,seq=x,并且客戶端進(jìn)入SYN_SENT狀態(tài)
2.服務(wù)端收到請求鏈接,服務(wù)端向客戶端進(jìn)行回復(fù),并發(fā)送響應(yīng)報(bào)文,SYN=1,seq=y,ACK=1,ack=x+1,并且服務(wù)端進(jìn)入到SYN_RCVD狀態(tài) 3.客戶端收到確認(rèn)報(bào)文后,向服務(wù)端發(fā)送確認(rèn)報(bào)文,ACK=1,ack=y+1,此時(shí)客戶端進(jìn)入到ESTABLISHED,服務(wù)端收到用戶端發(fā)送過來的確認(rèn)報(bào)文后,也進(jìn)入到ESTABLISHED狀態(tài),此時(shí)鏈接創(chuàng)建成功
- 哎!
- 嗯
- 給你
復(fù)制代碼
為什么需要三次握手: 為了防止已失效的連接請求報(bào)文段突然又傳送到了服務(wù)端,因而產(chǎn)生錯(cuò)誤。假設(shè)這是一個(gè)早已失效的報(bào)文段,但server收到此失效的連接請求報(bào)文段后,就誤認(rèn)為是client再次發(fā)出的一個(gè)新的連接請求。于是就向client發(fā)出確認(rèn)報(bào)文段,同意建立連接。假設(shè)不采用“三次握手”,那么只要server發(fā)出確認(rèn),新的連接就建立了。由于現(xiàn)在client并沒有發(fā)出建立連接的請求,因此不會(huì)理睬server的確認(rèn),也不會(huì)向server發(fā)送數(shù)據(jù)。但server卻以為新的運(yùn)輸連接已經(jīng)建立,并一直等待client發(fā)來數(shù)據(jù)。這樣,server的很多資源就白白浪費(fèi)掉了。
四次揮手
1.客戶端向服務(wù)端發(fā)起關(guān)閉鏈接,并停止發(fā)送數(shù)據(jù) 2.服務(wù)端收到關(guān)閉鏈接的請求時(shí),向客戶端發(fā)送回應(yīng),我知道了,然后停止接收數(shù)據(jù) 3.當(dāng)服務(wù)端發(fā)送數(shù)據(jù)結(jié)束之后,向客戶端發(fā)起關(guān)閉鏈接,并停止發(fā)送數(shù)據(jù) 4.客戶端收到關(guān)閉鏈接的請求時(shí),向服務(wù)端發(fā)送回應(yīng),我知道了,然后停止接收數(shù)據(jù)
- 哎!
- 嗯
- 關(guān)了
- 好的
復(fù)制代碼
為什么需要四次揮手: 因?yàn)門CP是全雙工通信的,在接收到客戶端的關(guān)閉請求時(shí),還可能在向客戶端發(fā)送著數(shù)據(jù),因此不能再回應(yīng)關(guān)閉鏈接的請求時(shí),同時(shí)發(fā)送關(guān)閉鏈接的請求
引申
-
HTTP和HTTPS有什么區(qū)別?
- HTTP協(xié)議是一種使用明文數(shù)據(jù)傳輸?shù)木W(wǎng)絡(luò)協(xié)議。
- HTTPS協(xié)議可以理解為HTTP協(xié)議的升級,就是在HTTP的基礎(chǔ)上增加了數(shù)據(jù)加密。在數(shù)據(jù)進(jìn)行傳輸之前,對數(shù)據(jù)進(jìn)行加密,然后再發(fā)送到服務(wù)器。這樣,就算數(shù)據(jù)被第三者所截獲,但是由于數(shù)據(jù)是加密的,所以你的個(gè)人信息讓然是安全的。這就是HTTP和HTTPS的最大區(qū)別。
-
HTTPS的加密方式?
Https采用對稱加密和非對稱加密結(jié)合的方式來進(jìn)行通信。
-
Https不是應(yīng)用層的新協(xié)議,而是Http通信接口用SSL和TLS來加強(qiáng)加密和認(rèn)證機(jī)制。
- 對稱加密: 加密和解密都是同一個(gè)鑰匙
- 非對稱加密:密鑰承兌出現(xiàn),分為公鑰和私鑰,公鑰加密需要私鑰解密,私鑰加密需要公鑰解密
HTTP和HTTPS的建立連接的過程?
HTTP
- 建立鏈接完畢以后客戶端會(huì)發(fā)送響應(yīng)給服務(wù)器
- 服務(wù)端接受請求并且做出響應(yīng)發(fā)送給客戶端
- 客戶端收到響應(yīng)并且解析響應(yīng)給客戶
HTTPS
- 在使用HTTPS是需要保證服務(wù)端配置了正確的對應(yīng)的安全證書
- 客戶端發(fā)送請求到服務(wù)器
- 服務(wù)端返回公鑰和證書到客戶端
- 客戶端接受后,會(huì)驗(yàn)證證書的安全性,如果通過則會(huì)隨機(jī)生成一個(gè)隨機(jī)數(shù),用公鑰對其解密, 發(fā)送到服務(wù)端
- 服務(wù)端接受到這個(gè)加密后的隨機(jī)數(shù)后,會(huì)用私鑰對其進(jìn)行揭秘,得到真正的隨機(jī)數(shù),然后調(diào)用這個(gè)隨機(jī)數(shù)當(dāng)作私鑰對需要發(fā)送的數(shù)據(jù)進(jìn)行對稱加密。
- 客戶端接收到加密后的數(shù)據(jù)使用私鑰(之前生成的隨機(jī)值)對數(shù)據(jù)進(jìn)行解密,并且解析數(shù)據(jù)呈現(xiàn)給客戶
HTTP協(xié)議中GET和POST的區(qū)別
GET在特定的瀏覽器和服務(wù)器對URL的長度是有限制的。 但是理論上是沒有限制的
POST不是通過URL進(jìn)行傳值,理論上不受限制。
GET會(huì)把請求參數(shù)拼接到URL后面, 不安全,
POST把參數(shù)放到請求體里面, 會(huì)比GET相對安全一點(diǎn), 但是由于可以窺探數(shù)據(jù), 所以也不安全, 想更安全用加密。
GET比POST的請求速度快。原因:Post請求的過程, 會(huì)現(xiàn)將請求頭發(fā)送給服務(wù)器確認(rèn),然后才真正的發(fā)送數(shù)據(jù), 而Get請求 過程會(huì)在鏈接建立后會(huì)將請求頭和數(shù)據(jù)一起發(fā)送給服務(wù)器。 中間少了一步。 所以get比post 快
post的請求過程
三次握手之后 第三次會(huì)把post請求頭發(fā)送
服務(wù)器返回100 continue響應(yīng)
瀏覽器開始發(fā)送數(shù)據(jù)
服務(wù)器返回200 ok響應(yīng)
- get請求過程
- 三次握手之后 第三次會(huì)發(fā)送get請求頭和數(shù)據(jù)
- 服務(wù)器返回200 ok響應(yīng)
23. 有沒有使用過performSelector?
- 這題主要是想問的是有沒有動(dòng)態(tài)添加過方法
- 話不多說上代碼
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
// 默認(rèn)person,沒有實(shí)現(xiàn)eat方法,可以通過performSelector調(diào)用,但是會(huì)報(bào)錯(cuò)。
// 動(dòng)態(tài)添加方法就不會(huì)報(bào)錯(cuò)
[p performSelector:@selector(eat)];
}
@end
@implementation Person
// **這里真是奇葩, 實(shí)在想不到什么時(shí)候才有這種使用場景, 我再外面找不到方法, 我再當(dāng)前類里面直接在寫一個(gè)方法就好咯,干嘛要在這里寫這個(gè)玩意, 還要寫一個(gè)C語言的東西, 既然面試想問, 那咱就要會(huì)!**
// void(*)()
// 默認(rèn)方法都有兩個(gè)隱式參數(shù),
void eat(id self,SEL sel)
{
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 當(dāng)一個(gè)對象調(diào)用未實(shí)現(xiàn)的方法,會(huì)調(diào)用這個(gè)方法處理,并且會(huì)把對應(yīng)的方法列表傳過來.
// 剛好可以用來判斷,未實(shí)現(xiàn)的方法是不是我們想要?jiǎng)討B(tài)添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat)) {
// 動(dòng)態(tài)添加eat方法
// 第一個(gè)參數(shù):給哪個(gè)類添加方法
// 第二個(gè)參數(shù):添加方法的方法編號(hào)
// 第三個(gè)參數(shù):添加方法的函數(shù)實(shí)現(xiàn)(函數(shù)地址)
// 第四個(gè)參數(shù):函數(shù)的類型,(返回值+參數(shù)類型) v:void @:對象->self :表示SEL->_cmd
class_addMethod(self, @selector(eat), eat, "v@:");
}
return [super resolveInstanceMethod:sel];
}
@end
復(fù)制代碼
- 當(dāng)然面試的時(shí)候也可能問你這個(gè)
// 延時(shí)操作 和GCD的after 一個(gè)效果
[p performSelector:@selector(eat) withObject:nil afterDelay:4];
復(fù)制代碼
-
你以為完了? 錯(cuò)了,大概率面試官會(huì)問你,*** 上面這段代碼放在子線程中 是什么樣子的?為什么?**
—首先 上面這個(gè)方法其實(shí)就是內(nèi)部創(chuàng)建了一個(gè)NSTimer定時(shí)器,然后這個(gè)定時(shí)器會(huì)添加在當(dāng)前的RunLoop中所以上面代碼放到子線程中不會(huì)有任何定時(shí)器相關(guān)方法被執(zhí)行,如果想要執(zhí)行,開啟當(dāng)前線程即可 即
[[NSRunLoop currentRunLoop] run];
復(fù)制代碼
// 完整調(diào)用
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
// [[NSRunLoop currentRunLoop] run]; 放在上面執(zhí)行時(shí)不可以的,因?yàn)楫?dāng)前只是開啟了runloop 里面沒有任何事件(source,timer,observer)也是開啟失敗的
[self performSelector:@selector(test) withObject:nil afterDelay:2];
[[NSRunLoop currentRunLoop] run];
});
// 由此我自行又做了一個(gè)測試, 把
[self performSelector:@selector(test)];
在子線程調(diào)用,是沒有任何問題的。
// 我又測試了一下,
[self performSelector:@selector(test) withObject:nil afterDelay:2];
這個(gè)方法在主線程執(zhí)行 打印線程是1
在子線程中調(diào)用打印線程 非1
復(fù)制代碼
- 然后面試官開始飄了, 開始問你關(guān)于NSTimer相關(guān)問題?怎么辦? 答: 搞他!
引申 NSTimer在子線程執(zhí)行?
- NSTimer直接在在子線程是不會(huì)被調(diào)用的, 想要執(zhí)行請開啟當(dāng)前的Runloop 。具體開啟方案上面題有說,不贅述。
引申 為什么說NSTimer不準(zhǔn)確?
- NSTimer的觸發(fā)時(shí)間到的時(shí)候,runloop如果在阻塞狀態(tài),觸發(fā)時(shí)間就會(huì)推遲到下一個(gè)runloop周期 減少誤差的方法 代碼如下
// 在子線程中開啟NStimer,或者更改當(dāng)前Runloop的Mode 為NSRunLoopCommonModes
[[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
// 利用CADisplayLink (iOS設(shè)備的屏幕刷新頻率是固定的,CADisplayLink在正常情況下會(huì)在每次刷新結(jié)束都被調(diào)用,精確度相當(dāng)高)
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(logInfo)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
// 利用GCD
NSTimeInterval interval = 1.0;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
NSLog(@"GCD timer test");
});
dispatch_resume(_timer);
復(fù)制代碼
引申: NStimer的循環(huán)引用?
- 有的人會(huì)說, NSTimer本身的target會(huì)引用這self, 而self又引用這Timer就造成了循環(huán)引用, 那如果timer用weak聲明呢? 還會(huì)循環(huán)引用么? 答案:會(huì)的
- 原因是NTtimer和Runloop是一個(gè)相互存在的東西, 別的道理我就不多BB, 就是Runloop和tmier相互引用,而Runloop永遠(yuǎn)不會(huì)銷毀,造成貸方面的“牽引” 所以蘋果出來了一個(gè)invalid的方法。
- 優(yōu)化的方案還有別的, 例如利用NSProxy這個(gè)專門做消息轉(zhuǎn)發(fā)的虛類去優(yōu)化循環(huán)引用(這里也經(jīng)常會(huì)被問到。具體方案我不說, 自行百度,切記,如果兄弟你不知道這個(gè)玩意, 建議你看看,面試的時(shí)候被問到的概率還是挺大的。)
24. 為什么AFN3.0中需要設(shè)置self.operationQueue.maxConcurrentOperationCount = 1;而AF2.0卻不需要?
- 功能不一樣, 2.x是基于NSURLConnection的,其內(nèi)部實(shí)現(xiàn)要在異步并發(fā),所以不能設(shè)置1。 3.0 是基于NSURLSession其內(nèi)部是需要串行的鑒于一些多線程數(shù)據(jù)訪問的安全性考慮, 設(shè)置這個(gè)達(dá)到串行回調(diào)的效果。
AFNetworking 2.0 和3.0 的區(qū)別?
- AFN3.0剔除了所有的NSURLConnection請求的API
- AFN3.0使用NSOperationQueue代替AFN2.0的常駐線程
2.x版本常駐線程的分析
在請求完成后我們需要對數(shù)據(jù)進(jìn)行一些序列化處理,或者錯(cuò)誤處理。如果我們在主線中處理這些事情很明顯是不合理的。不僅會(huì)導(dǎo)致UI的卡頓,甚至受到默認(rèn)的RunLoopModel的影響,我們在滑動(dòng)tableview的時(shí)候,會(huì)導(dǎo)致時(shí)間的處理停止。
這里時(shí)候我們就需要一個(gè)子線程來處理事件和網(wǎng)絡(luò)請求的回調(diào)了。但是,子線程在處理完事件后就會(huì)自動(dòng)結(jié)束生命周期,這個(gè)時(shí)候后面的一些網(wǎng)絡(luò)請求得回調(diào)我們就無法接收了。所以我們就需要開啟子線程的RunLoop來保存線程的常駐。
當(dāng)然我們可以每次發(fā)起一個(gè)請求就開啟一條子線程,但是這個(gè)想一下就知道開銷有多大了。所以這個(gè)時(shí)候?;钜粭l線程來對請求得回調(diào)處理是比較好的一個(gè)方案。
3.x版本不在常駐線程的分析?
在3.x的AFN版本中使用的是NSURLSession進(jìn)行封裝。對比于NSURLConnection,NSURLSession不需要在當(dāng)前的線程等待網(wǎng)絡(luò)回調(diào),而是可以讓開發(fā)者自己設(shè)定需要回調(diào)的隊(duì)列。
所以在3.x版本中AFN使用了NSOperationQueue對網(wǎng)絡(luò)回調(diào)的管理,并且設(shè)置maxConcurrentOperationCount為1,保證了最大的并發(fā)數(shù)為1,也就是說讓網(wǎng)絡(luò)請求串行執(zhí)行。避免了多線程環(huán)境下的資源搶奪問題。
25. autoreleasePool 在何時(shí)被釋放?
- ARC中所有的新生對象都是 自動(dòng)加autorelese的, @atuorelesepool 大部分時(shí)候解決了瞬時(shí)內(nèi)存暴增的問題 。
- MRC中的情況 關(guān)鍵詞變了NSAutoreleasePool。
//來自Apple文檔,見參考
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSString stringWithContentsOfURL:urlencoding:NSUTF8StringEncoding error:&error];
}
// 如果循環(huán)次數(shù)非常多,而且循環(huán)體里面的對象都是臨時(shí)創(chuàng)建使用的,就可以用@autoreleasepool 包起來,讓每次循環(huán)結(jié)束時(shí),可以及時(shí)釋放臨時(shí)對象的內(nèi)存
// for 和 for in 里面是沒有自動(dòng)包裝@autoreleasepool著的,而下面的方法是由@autoreleasepool自動(dòng)包圍的
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 這里被一個(gè)局部@autoreleasepool包圍著
}];
復(fù)制代碼
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString* str = [[[NSString alloc] initWithString:@"666"] autorelease];
[pool drain];
// 其作用于為drain 和 init 之間
復(fù)制代碼
-
回歸正題@autoReleasePool什么時(shí)間釋放?
- 一個(gè)被autoreleasepool包裹生成得對象,都會(huì)在其創(chuàng)建生成之后自動(dòng)添加autorelease, 然后被autorelease對象得釋放時(shí)機(jī) 就是在當(dāng)前runloop循環(huán)結(jié)束的時(shí)候自動(dòng)釋放的
- 參考鏈接:blog.sunnyxx.com/2014/10/15/…
子線程中的autorelease變量什么時(shí)候釋放?
- 子線程中會(huì)默認(rèn)包裹一個(gè)autoreleasepool的, 釋放時(shí)機(jī)是當(dāng)前線程退出的時(shí)候。
autoreleasepool是如何實(shí)現(xiàn)的?
- @autoreleasepool{} 本質(zhì)上是一個(gè)結(jié)構(gòu)體:
- autoreleasepool會(huì)被轉(zhuǎn)換成__AtAutoreleasePool
- __AtAutoreleasePool 里面有兩個(gè)函數(shù)objc_autoreleasePoolPush(),objc_autoreleasePoolPop().,其實(shí)一些列下來之后實(shí)際上調(diào)用得是AutoreleasePoolPage類中得push 和 pop兩個(gè)類方法
- push就是壓棧操作,
- pop就是出棧操作于此同時(shí)對其對象發(fā)送release消息進(jìn)行釋放
26. iOS界面渲染機(jī)制? [這是很大的一個(gè)模塊,里面牽扯很多東西, 耐心看下去]
- 先簡單解釋一下渲染機(jī)制
首先iOS渲染視圖的核心是Core Animation,其渲染層次依次為:圖層樹->呈現(xiàn)樹->渲染樹
一共三個(gè)階段
CPU階段(進(jìn)行Frame布局,準(zhǔn)備視圖和圖層之間的層級關(guān)系)
OpenGL ES階段(iOS8以后改成Metal), (渲染服務(wù)把上面提供的圖層上色,生成各種幀)
GPU階段 (把上面操作的東西進(jìn)行一些列的操作,最終展示到屏幕上面)
稍微詳細(xì)說明
首先一個(gè)視圖由CPU進(jìn)行Frame布局,準(zhǔn)備視圖和圖層的層及關(guān)系。
CUP會(huì)將處理視圖和圖層的層級關(guān)系打包,通過IPC(進(jìn)程間的通信)通道提交給渲染服務(wù)(OpenGL和GPU)
渲染服務(wù)首先將圖層交給OpenGL進(jìn)行紋理生成和著色,生成前后幀緩存,再根據(jù)硬件的刷新幀率,一般以設(shè)備的VSync信號(hào)和CADisplayLink(類似一個(gè)刷新UI專用的定時(shí)器)為標(biāo)準(zhǔn),進(jìn)行前后幀緩存的切換
最后,將最終 要顯示在畫面上的后幀緩存交給GPU,進(jìn)行采集圖片和形狀,運(yùn)行變換, 應(yīng)用紋理混合,最終顯示在屏幕上。
程序卡頓的原因?
- 正常渲染流程
- CPU計(jì)算完成之后交給GPU,來個(gè)同步信號(hào)Vsync 將內(nèi)容渲染到屏幕上
- 非正常(卡頓/掉幀)的流程
- CPU計(jì)算時(shí)間正?;蛘呗?,GPU渲染時(shí)間長了, 這時(shí)候Vsync信號(hào), 由于沒有繪制完全,CUP開始計(jì)算下一幀,當(dāng)下一幀正常繪制成功之后,把當(dāng)前沒有繪制完成的幀丟棄, 顯示了下一幀,于是這樣就造成了卡頓。
需要注意的是:Vsync時(shí)間間隔是固定的, 比如60幀率大的Vsync 是每16ms就執(zhí)行一個(gè)一次,類似定時(shí)器一樣
這里會(huì)出現(xiàn)一個(gè)面試題?。。?/strong> 題目如下:
- 從第一次打開App到完全開始展現(xiàn)出UI,中間發(fā)生了什么? 或者App是怎么渲染某一個(gè)View的?
- 回答就是上面的稍微詳細(xì)說明,如果要求更詳細(xì), 可以繼續(xù)深究一下。
在科普一下 1.Core Animation Core Animation 在 RunLoop 中注冊了一個(gè) Observer,監(jiān)聽了 BeforeWaiting 和 Exit 事件。這個(gè) Observer 的優(yōu)先級是 2000000,低于常見的其他 Observer。當(dāng)一個(gè)觸摸事件到來時(shí),RunLoop 被喚醒,App 中的代碼會(huì)執(zhí)行一些操作,比如創(chuàng)建和調(diào)整視圖層級、設(shè)置 UIView 的 frame、修改 CALayer 的透明度、為視圖添加一個(gè)動(dòng)畫;這些操作最終都會(huì)被 CALayer 捕獲,并通過 CATransaction 提交到一個(gè)中間狀態(tài)去(CATransaction 的文檔略有提到這些內(nèi)容,但并不完整)。當(dāng)上面所有操作結(jié)束后,RunLoop 即將進(jìn)入休眠(或者退出)時(shí),關(guān)注該事件的 Observer 都會(huì)得到通知。這時(shí) CA 注冊的那個(gè) Observer 就會(huì)在回調(diào)中,把所有的中間狀態(tài)合并提交到 GPU 去顯示;如果此處有動(dòng)畫,CA 會(huì)通過 DisplayLink 等機(jī)制多次觸發(fā)相關(guān)流程。
2.CPU渲染職能
- 布局計(jì)算:如果視圖層級過于復(fù)雜,當(dāng)試圖呈現(xiàn)或者修改的時(shí)候,計(jì)算圖層幀率就會(huì)消耗一部分時(shí)間,
- 視圖懶加載: iOS只會(huì)當(dāng)視圖控制器的視圖顯示到屏幕上才會(huì)加載它,這對內(nèi)存使用和程序啟動(dòng)時(shí)間很有好處,但是當(dāng)呈現(xiàn)到屏幕之前,按下按鈕導(dǎo)致的許多工作都不會(huì)被及時(shí)響應(yīng)。比如,控制器從數(shù)據(jù)局中獲取數(shù)據(jù), 或者視圖從一個(gè)xib加載,或者涉及iO圖片顯示都會(huì)比CPU正常操作慢得多。
- 解壓圖片:PNG或者JPEG壓縮之后的圖片文件會(huì)比同質(zhì)量的位圖小得多。但是在圖片繪制到屏幕上之前,必須把它擴(kuò)展成完整的未解壓的尺寸(通常等同于圖片寬 x 長 x 4個(gè)字節(jié))。為了節(jié)省內(nèi)存,iOS通常直到真正繪制的時(shí)候才去解碼圖片。根據(jù)你加載圖片的方式,第一次對 圖層內(nèi)容賦值的時(shí)候(直接或者間接使用 UIImageView )或者把它繪制到 Core Graphics中,都需要對它解壓,這樣的話,對于一個(gè)較大的圖片,都會(huì)占用一定的時(shí)間。
- Core Graphics繪制:如果對視圖實(shí)現(xiàn)了drawRect:或drawLayer:inContext:方法,或者 CALayerDelegate 的 方法,那么在繪制任何東 西之前都會(huì)產(chǎn)生一個(gè)巨大的性能開銷。為了支持對圖層內(nèi)容的任意繪制,Core Animation必須創(chuàng)建一個(gè)內(nèi)存中等大小的寄宿圖片。然后一旦繪制結(jié)束之后, 必須把圖片數(shù)據(jù)通過IPC傳到渲染服務(wù)器。在此基礎(chǔ)上,Core Graphics繪制就會(huì)變得十分緩慢,所以在一個(gè)對性能十分挑剔的場景下這樣做十分不好。
- 圖層打包:當(dāng)圖層被成功打包,發(fā)送到渲染服務(wù)器之后,CPU仍然要做如下工作:為了顯示 屏幕上的圖層,Core Animation必須對渲染樹種的每個(gè)可見圖層通過OpenGL循環(huán) 轉(zhuǎn)換成紋理三角板。由于GPU并不知曉Core Animation圖層的任何結(jié)構(gòu),所以必須 要由CPU做這些事情。這里CPU涉及的工作和圖層個(gè)數(shù)成正比,所以如果在你的層 級關(guān)系中有太多的圖層,就會(huì)導(dǎo)致CPU沒一幀的渲染,即使這些事情不是你的應(yīng)用 程序可控的。
3.GPU渲染職能 GPU會(huì)根據(jù)生成的前后幀緩存數(shù)據(jù),根據(jù)實(shí)際情況進(jìn)行合成,其中造成GPU渲染負(fù)擔(dān)的一般是:離屏渲染,圖層混合,延遲加載。
這里又會(huì)出現(xiàn)一個(gè)面試題?。。?/strong> 一個(gè)UIImageView添加到視圖上以后,內(nèi)部如何渲染到手機(jī)上的?
圖片顯示分為三個(gè)步驟: 加載、解碼、渲染、 通常,我們程序員的操作只是加載,至于解碼和渲染是由UIKit內(nèi)部進(jìn)行的。 例如:UIImageView顯示在屏幕上的時(shí)候需要UIImage對象進(jìn)行數(shù)據(jù)源的賦值。而UIImage持有的數(shù)據(jù)是未解碼的壓縮數(shù)據(jù),當(dāng)賦值的時(shí)候,圖像數(shù)據(jù)會(huì)被解碼變成RGB顏色數(shù)據(jù),最終渲染到屏幕上。
看完上面的又來問題了! 關(guān)于UITableView優(yōu)化的問題?(真他媽子子孫孫無窮盡也~) 先說造成UITableView滾動(dòng)時(shí)候卡頓的的原因有哪些?
- 隱式繪制 CGContext
- 文本CATextLayer 和 UILabel
- 光柵化 shouldRasterize
- 離屏渲染
- 可伸縮圖片
- shadowPath
- 混合和過度繪制
- 減少圖層數(shù)量
- 裁切
- 對象回收
- Core Graphics繪制
- -renderInContext: 方法
在說關(guān)于UITableView的優(yōu)化問題!
基礎(chǔ)的
- 重用機(jī)制(緩存池)
- 少用有透明度的View
- 盡量避免使用xib
- 盡量避免過多的層級結(jié)構(gòu)
- iOS8以后出的預(yù)估高度
- 減少離屏渲染操作(圓角、陰影啥的)
**** 解釋一下為什么減少離屏渲染操作?****
需要?jiǎng)?chuàng)建新的緩沖區(qū)
整個(gè)過程需要多次切換上下文環(huán)境, 顯示從當(dāng)前的屏幕切換到離屏,等待離屏渲染結(jié)束后,將離屏緩沖區(qū)的渲染結(jié)果 顯示到屏幕有上, 又要將上下文環(huán)境從離屏切換到當(dāng)前屏幕,
****那些操作會(huì)觸發(fā)離屏渲染?****
光柵化 layer.shouldRasterize = YES
遮罩layer.mask
圓角layer.maskToBounds = Yes,Layer.cornerRadis 大于0
陰影l(fā)ayer.shadowXXX
進(jìn)階的
- 緩存cell的高度(提前計(jì)算好cell的高度,緩存進(jìn)當(dāng)前的模型里面)
- 異步繪制
- 滑動(dòng)的時(shí)候,按需加載
高階的
- 你想不到 竟然不推薦用UILabel。哈哈哈~ 至于為什么 看下面的鏈接吧
至于上面的那些基礎(chǔ)的,涉及到渲染級別的自己說的時(shí)候悠著點(diǎn),面試官如果想搞你的話,考一考你最上面的那些,CUP和GUP,以及openGL相關(guān), 在考一下你進(jìn)程通信IPC,以及VSync信號(hào)啥的, 這些東西太雞兒高深了,沒點(diǎn)匠心 這東西還真搞不了,要想研究可以看看YYKit的作者寫的一篇關(guān)于頁面流暢的文章:blog.ibireme.com/2015/11/12/…
卡頓檢測的方法
- 卡頓就是主線程阻塞的時(shí)間問題,可以添加Observer到主線程Runloop中,通過監(jiān)聽Runloop狀態(tài)切換的耗時(shí),以達(dá)到監(jiān)聽卡頓的目的
繼續(xù)
既然都是圖形繪制了,那就再研究一下事件響應(yīng)鏈&原理
傳統(tǒng)的問法來了:UIView和CALayer的區(qū)別? 通常我們這樣回答:UIView可以響應(yīng)用戶事件,而CALayer不能處理事件
回答這個(gè)之前, 先回顧一下另外一個(gè)經(jīng)典面試題:事件響應(yīng)鏈和事件傳遞?
基本概念:
響應(yīng)鏈: 是由鏈接在一起的響應(yīng)者(UIResponse子類)組成的,一般為第一響應(yīng)著到application對象以及中間所有響應(yīng)者一起組成的。
事件傳遞: 獲取響應(yīng)鏈之后, 將事件由第一響應(yīng)者網(wǎng)application的傳遞過程
[圖片上傳失敗...(image-74d78b-1647873407847)]
[圖片上傳失敗...(image-c53446-1647873407847)]
事件的分發(fā)和傳遞
當(dāng)程序中發(fā)生觸摸事件之后,系統(tǒng)會(huì)將事件添加到UIApplication管理的一個(gè)隊(duì)列當(dāng)中
UIApplication將處于任務(wù)隊(duì)列最前端的事件向下分發(fā) 即UIWindow
UIWindow將事件向下分發(fā),即UIView或者UIViewController
UIView首先看自己能否處理這個(gè)事件,觸摸點(diǎn)是否在自己身上,自己的透明度是否大于0,01,userInteractionEnabled 是否是YES, Hidden實(shí)際是NO,如果這些都滿足,那么繼續(xù)尋找其子視圖
遍歷子控件,重復(fù)上面步驟
如果沒有找到,那么自己就是改事件的處理者
如果自己不能處理,那么就不做任何處理 即視為沒有合適的View能接收處理當(dāng)前事件,則改事件會(huì)被廢棄。
*** 怎么尋找當(dāng)前觸摸的是哪一個(gè)View?***
下面中兩個(gè)方法
// 此方法返回的View是本次點(diǎn)擊事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
// 判斷一個(gè)點(diǎn)是否落在范圍內(nèi)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
復(fù)制代碼
事件傳遞給控件之后, 就會(huì)調(diào)用hitTest:withEvent方法去尋找更合適的View,如果當(dāng)前View存在子控件,則在子控件繼續(xù)調(diào)用hitTest:withEvent方法判斷是否是合適的View, 如果還不是就一直遍歷尋找, 找不到的話直接廢棄掉。
// 因?yàn)樗械囊晥D類都是繼承BaseView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1.判斷當(dāng)前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2\. 判斷點(diǎn)在不在當(dāng)前控件
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.從后往前遍歷自己的子控件
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[I];
// 把當(dāng)前控件上的坐標(biāo)系轉(zhuǎn)換成子控件上的坐標(biāo)系
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) { // 尋找到最合適的view
return fitView;
}
}
// 循環(huán)結(jié)束,表示沒有比自己更合適的view
return self;
}
復(fù)制代碼
- 判斷觸摸點(diǎn)是否在視圖內(nèi)?
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
復(fù)制代碼
- tableView 加一個(gè)tap的手勢, 點(diǎn)擊當(dāng)前cell的位置 哪個(gè)事件被響應(yīng) 為什么?
- tap事件被響應(yīng), 因?yàn)閠ap事件添加之后,默認(rèn)是取消當(dāng)前tap以外的所有事件的, 也就是說, tap事件處于當(dāng)前響應(yīng)者鏈的最頂端, 解決的辦法執(zhí)行tap的delagete, 實(shí)現(xiàn)
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if([touch.view isKindOfClass:[XXXXcell class]])
{
return NO;
}
return YES;
}
作者:執(zhí)筆續(xù)春秋
鏈接:https://juejin.cn/post/6854573212165111822