【iOS】最新面試題

KVO 內(nèi)部實(shí)現(xiàn)原理

  1. KVO是基于runtime機(jī)制實(shí)現(xiàn)的。
  2. 當(dāng)某個(gè)類的對(duì)象第一次被觀察時(shí),系統(tǒng)就會(huì)在運(yùn)行期動(dòng)態(tài)地創(chuàng)建該類的一個(gè)派生類,在這個(gè)派生類中重寫(xiě)基類中任何被觀察屬性的setter 方法。
  3. 派生類在被重寫(xiě)的 setter 方法實(shí)現(xiàn)真正的通知機(jī)制(Person?? NSKVONotifying_Person)

不用中間變量,用兩種方法交換 A 和 B 的值

A=A+B;
B=A-B;
A=A-B;
或者
A = A^B; 
B = A^B; 
A = A^B;

runtime 實(shí)現(xiàn)的機(jī)制是什么

runtime,運(yùn)行時(shí)機(jī)制,它是一套C語(yǔ)言庫(kù)。
實(shí)際上我們編寫(xiě)的所有OC代碼,最終都是轉(zhuǎn)成了runtime庫(kù)的東西,

比如:
類轉(zhuǎn)成了runtime庫(kù)里面的結(jié)構(gòu)體等數(shù)據(jù)類型,
方法轉(zhuǎn)成了 runtime庫(kù)里面的C語(yǔ)言函數(shù),平時(shí)調(diào)方法都是轉(zhuǎn)成了objc_msgSend 函數(shù)(所以說(shuō)OC有個(gè)消息發(fā)送機(jī)制)

因此,可以說(shuō)runtime是OC的底層實(shí)現(xiàn),是OC的幕后執(zhí)行者 有了runtime庫(kù),能做什么事情呢?

runtime庫(kù)里面包含了跟類、成員 變量、方法相關(guān)的API,比如獲取類里面的所有成員變量,為類動(dòng)態(tài) 添加成員變量,動(dòng)態(tài)改變類的方法實(shí)現(xiàn),為類動(dòng)態(tài)添加新的方法等 因此,有了runtime,想怎么改就怎么改。

是否使用 Core Text 或者 Core Image

CoreText

  • 隨意修改文本的樣式
  • 圖文混排(純C語(yǔ)言)
  • 國(guó)外:Niumb

Core Image(濾鏡處理)

  • 能調(diào)節(jié)圖片的各種屬性(對(duì)比度, 色溫, 色差等)

NSNotification、KVO、delegate 的區(qū)別和用法是什么?

通知比較靈活:1個(gè)通知能被多個(gè)對(duì)象接收, 1個(gè)對(duì)象能接收多個(gè)通知

KVO性能不好(底層會(huì)動(dòng)態(tài)產(chǎn)生新的類),只能監(jiān)聽(tīng)某個(gè)對(duì)象屬性的改 變, 不推薦使用(1個(gè)對(duì)象的屬性能被多個(gè)對(duì)象監(jiān)聽(tīng), 1個(gè) 對(duì)象能監(jiān)聽(tīng) 多個(gè)對(duì)象的其他屬性)

代理比較規(guī)范,但是代碼多(默認(rèn)是1對(duì)1)

delegate 代理的作用?

代理的目的是改變或傳遞控制鏈。
允許一個(gè)類在某些特定時(shí)刻通知到其他類,而不 需要獲取到那些類的指針??梢詼p少框架復(fù)雜度。
另外一點(diǎn),代理可以理解為java 中的回調(diào)監(jiān)聽(tīng)機(jī)制的一種類似。

簡(jiǎn)單說(shuō)一下事件響應(yīng)的流程?

  1. 一個(gè) UIView 發(fā)出一個(gè)事件之后,首先上傳給其父視圖;
  2. 父視圖上傳給其所在的控制器;
  3. 如果其控制器對(duì)事件進(jìn)行處理,事件傳遞將終止,否則繼續(xù)上傳父視圖;
  4. 直到遇到響應(yīng)者才會(huì)停止,否則事件將一直上傳,直到 UIWindow。

用@property 聲明的 NSString(或 NSArray, NSDictionary)經(jīng)常使用 copy 關(guān)鍵字,為什么? 如果改用 strong 關(guān)鍵字,可能造成什么問(wèn)題?

因?yàn)楦割愔羔樋梢灾赶蜃宇悓?duì)象,使用 copy 的目的是為了讓本對(duì)象的屬性不受外界影響,使用 copy 無(wú)論給我傳入是一個(gè)可變對(duì)象還是不可對(duì)象,我本身持有的就是一個(gè) 不可變的副本。

如果我們使用是 strong,那么這個(gè)屬性就有可能指向一個(gè)可變對(duì)象,如果這個(gè)可變對(duì)象在外部被修改了,那么會(huì)影響該屬性。

【復(fù)制詳解】
淺復(fù)制(shallow copy):在淺復(fù)制操作時(shí),對(duì)于被復(fù)制對(duì)象的每一層都是指 針復(fù)制。
深復(fù)制(one-level-deep copy):在深復(fù)制操作時(shí),對(duì)于被復(fù)制對(duì)象,至少有 一層是深復(fù)制。
完全復(fù)制(real-deep copy):在完全復(fù)制操作時(shí),對(duì)于被復(fù)制對(duì)象的每一層都 是對(duì)象復(fù)制。
非集合類對(duì)象的 copy 與 mutableCopy [不可變對(duì)象 copy] // 淺復(fù)制 [不可變對(duì)象 mutableCopy] //深復(fù)制
[可變對(duì)象 copy] //深復(fù)制 [可變對(duì)象 mutableCopy] //深復(fù)制
集合類對(duì)象的 copy 與 mutableCopy [不可變對(duì)象 copy] // 淺復(fù)制 [不可變對(duì)象 mutableCopy] //單層深復(fù)制
[可變對(duì)象 copy] //單層深復(fù)制
[可變對(duì)象 mutableCopy] //單層深復(fù)制
這里需要注意的是集合對(duì)象的內(nèi)容復(fù)制僅限于對(duì)象本身,對(duì)象元素仍然是 指針復(fù)制

這個(gè)寫(xiě)法會(huì)出什么問(wèn)題: @property (copy) NSMutableArray *array;

因?yàn)?copy 策略拷貝出來(lái)的是一個(gè)不可變對(duì)象,然而卻把它當(dāng)成可變對(duì)象使用,很容 易造成程序奔潰。

這里還有一個(gè)問(wèn)題,該屬性使用了同步鎖,會(huì)在創(chuàng)建時(shí)生成一些額外的代碼用于幫 助編寫(xiě)多線程程序,這會(huì)帶來(lái)性能問(wèn)題,通過(guò)聲明 nonatomic 可以節(jié)省這些雖然
很小但是不必要額外開(kāi)銷,在 iOS 開(kāi)發(fā)中應(yīng)該使用 nonatomic 替代 atomic

如何讓自定義類可以用 copy 修飾符?如何重寫(xiě)帶 copy 關(guān)鍵字的 setter?

若想令自己所寫(xiě)的對(duì)象具有拷貝功能,則需實(shí)現(xiàn) NSCopying 協(xié)議。如果自定義的對(duì) 象分為可變版本與不可變版本,那么就要同時(shí)實(shí)現(xiàn) NSCopyiog 與 NSMutableCopying 協(xié)議,不過(guò)一般沒(méi)什么必要,實(shí)現(xiàn) NSCopying 協(xié)議就夠了

// 實(shí)現(xiàn)不可變版本拷貝
- (id)copyWithZone:(NSZone *)zone; // 實(shí)現(xiàn)可變版本拷貝
- (id)mutableCopyWithZone:(NSZone *)zone;
// 重寫(xiě)帶 copy 關(guān)鍵字的 setter
- (void)setName:(NSString *)name {
    _name = [name copy];
}

+(void)load; +(void)initialize;有什 么用處?

+(void)load; 當(dāng)類對(duì)象被引入項(xiàng)目時(shí), runtime 會(huì)向每一個(gè)類對(duì)象發(fā)送 load 消息。

load 方法會(huì)在每一個(gè)類甚至分類被引入時(shí)僅調(diào)用一次,調(diào)用的順序:父類優(yōu)先于子 類, 子類優(yōu)先于分類

由于 load 方法會(huì)在類被 import 時(shí)調(diào)用一次,而這時(shí)往往是改變類的行為的最佳時(shí) 機(jī),在這里可以使用例如 method swizlling 來(lái)修改原有的方法

+(void)initialize;
也是在第一次使用這個(gè)類的時(shí)候會(huì)調(diào)用這個(gè)方法,也就是說(shuō) initialize 也是懶加載。

總結(jié):
在 Objective-C 中,runtime 會(huì)自動(dòng)調(diào)用每個(gè)類的這兩個(gè)方法
+load 會(huì)在類初始加載時(shí)調(diào)用
+initialize 會(huì)在第一次調(diào)用類的類方法或?qū)嵗椒ㄖ氨徽{(diào)用

IBOutlet 連出來(lái)的視圖屬性為什么可以被設(shè)置成 weak?

因?yàn)楦缚丶?subViews 數(shù)組已經(jīng)對(duì)它有一個(gè)強(qiáng)引用

1.首先要明確什么時(shí)候可以使用weak修飾?
一種情況是 : 在 ARC 中,在有可能出現(xiàn)循環(huán)引用的時(shí)候
另一種情況是 : 自身已經(jīng)對(duì)它進(jìn)行一次強(qiáng)引用,沒(méi)有必要再?gòu)?qiáng)引用一次

2.然后看IBOutlet連出來(lái)的視圖的引用關(guān)系
UIViewController →強(qiáng)引用→ UIView →強(qiáng)引用→ IBOutlet連出視圖


3.綜上所訴
所以問(wèn)題場(chǎng)景滿足第二種使用weak的條件, 既UIViewController不需要對(duì)IBOutlet連出來(lái)的視圖再用strong修飾, 用weak即可。

什么情況使用 weak 關(guān)鍵字

在 ARC 中,在有可能出現(xiàn)循環(huán)引用的時(shí)候,往往要通過(guò)讓其中一端使用 weak 來(lái)解決,比如: delegate 代理屬性

自身已經(jīng)對(duì)它進(jìn)行一次強(qiáng)引用,沒(méi)有必要再?gòu)?qiáng)引用一次,此時(shí)也會(huì)使用 weak,自定義 IBOutlet 控件屬性一般也使用 weak;當(dāng)然,也可以使用strong

weak修飾的屬性所指的對(duì)象遭到摧毀時(shí),屬性值也會(huì)清空(nil)

請(qǐng)簡(jiǎn)述 UITableView 的復(fù)用機(jī)制

每次創(chuàng)建 cell 的時(shí)候通過(guò) dequeueReusableCellWithIdentifier:方法創(chuàng)建 cell,
它先到 緩存池中找指定標(biāo)識(shí)的 cell,

如果沒(méi)有找到指定標(biāo)識(shí)的 cell,就直接返回 nil,且會(huì)通過(guò) initWithStyle:reuseIdentifier:創(chuàng)建一個(gè) cell

當(dāng) cell 離開(kāi)界面就會(huì)被放到緩存池中,以供下次復(fù)用

如何高性能的給 UIImageView 加個(gè)圓角?

不好的解決方案:
使用下面的方式會(huì)強(qiáng)制 Core Animation 提前渲染屏幕的離屏繪制, 而離 屏繪制就會(huì)給性能帶來(lái)負(fù)面影響,會(huì)有卡頓的現(xiàn)象出現(xiàn)

self.view.layer.cornerRadius = 5;
self.view.layer.masksToBounds = YES;

正確的解決方案:使用繪圖技術(shù)

- (UIImage *)circleImage {
  // NO 代表透明
  UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);
  // 獲得上下文
  CGContextRef ctx = UIGraphicsGetCurrentContext();
  // 添加一個(gè)圓
  CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
  CGContextAddEllipseInRect(ctx, rect);
  // 裁剪 CGContextClip(ctx);
  // 將圖片畫(huà)上去
  [self drawInRect:rect];
  UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
  // 關(guān)閉上下文 UIGraphicsEndImageContext();
  return image;
}

還有一種方案:使用了貝塞爾曲線"切割"個(gè)這個(gè)圖片, 給 UIImageView 添加了的圓 角,其實(shí)也是通過(guò)繪圖技術(shù)來(lái)實(shí)現(xiàn)的

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; 
UIImage *anotherImage = [UIImage imageNamed:@"image"];

UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:50] addClip]; 
[anotherImage drawInRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

[self.view addSubview:imageView];

描述下 SDWebImage 里面給 UIImageView 加 載圖片的邏輯

SDWebImage 中為 UIImageView 提供了一個(gè)分類 UIImageView+WebCache.h, 這個(gè)分類中有一個(gè)最常用的接口 sd_setImageWithURL:placeholderImage:,會(huì)在真實(shí)圖片出現(xiàn)前會(huì)先顯示占位圖片,當(dāng)真實(shí)圖片被加載出來(lái)后在替換占位圖片 加載圖片的過(guò)程大致如下:

首先會(huì)在 SDWebImageCache 中尋找圖片是否有對(duì)應(yīng)的緩存, 它會(huì)以 url 作為數(shù)據(jù)的索引先在內(nèi)存中尋找是否有對(duì)應(yīng)的緩存

如果緩存未找到就會(huì)利用通過(guò) MD5 處理過(guò)的 key 來(lái)繼續(xù)在磁盤中查詢對(duì)應(yīng)的數(shù)據(jù),

如果找到了, 就會(huì)把磁盤中的數(shù)據(jù)加載到內(nèi)存中,并將圖片顯示出來(lái)

如果在內(nèi)存和磁盤緩存中都沒(méi)有找到,就會(huì)向遠(yuǎn)程服務(wù)器發(fā)送請(qǐng)求,開(kāi)始下載圖片下載后的圖片會(huì)加入緩存中,并寫(xiě)入磁盤中 整個(gè)獲取圖片的過(guò)程都是在子線程中執(zhí)行,獲取到圖片后回到主線程將圖片顯示出來(lái)。

你是怎么封裝一個(gè) view 的

可以通過(guò)純代碼或者 xib 的方式來(lái)封裝子控件建立一個(gè)跟 view 相關(guān)的模型,然后將模型數(shù)據(jù)傳給 view,通過(guò)模型上的數(shù)據(jù)給 view 的子控件賦值。

/**
 * 純代碼初始化控件時(shí)一定會(huì)走這個(gè)方法
 */
- (instancetype)initWithFrame:(CGRect)frame {
  if(self = [super initWithFrame:frame]) {
      [self setup];
  }
    return self;
}

 /**
  * 通過(guò) xib 初始化控件時(shí)一定會(huì)走這個(gè)方法 
  */
- (id)initWithCoder:(NSCoder *)aDecoder {
  if(self = [super initWithCoder:aDecoder]) {
      [self setup];
  }
      return self;
}
- (void)setup {
    // 初始化代碼 
}

觸摸事件的傳遞

觸摸事件的傳遞是從父控件傳遞到子控件 如果父控件不能接收觸摸事件,那么子控件就不可能接收到觸摸事件 不能接受觸摸事件的四種情況 不接收用戶交互,即:userInteractionEnabled = NO 隱藏,即:hidden = YES
透明,即:alpha <= 0.01
未啟用,即:enabled = NO
提示:UIImageView 的 userInteractionEnabled 默認(rèn)就是 NO,因此 UIImageView 以 及它的子控件默認(rèn)是不能接收觸摸事件的
如何找到最合適處理事件的控件:
首先,判斷自己能否接收觸摸事件 可以通過(guò)重寫(xiě) hitTest:withEvent:方法驗(yàn)證 其次,判斷觸摸點(diǎn)是否在自己身上
對(duì)應(yīng)方法 pointInside:withEvent: 從后往前(先遍歷最后添加的子控件)遍歷子控件,重復(fù)前面的兩個(gè)步驟 如果沒(méi)有符合條件的子控件,那么就自己處理

事件響應(yīng)者鏈

如果當(dāng)前 view 是控制器的 view,那么就傳遞給控制器;如果控制器不存在,則將其傳遞給它的父控件。

在視圖層次結(jié)構(gòu)的最頂層視圖也不能處理接收到的事件或消息,則將事件或消息傳 遞給 UIWindow 對(duì)象進(jìn)行處理。

如果 UIWindow 對(duì)象也不處理,則將事件或消息傳遞給 UIApplication 對(duì)象

如果 UIApplication 也不能處理該事件或消息,則將其丟棄

補(bǔ)充:如何判斷上一個(gè)響應(yīng)者
如果當(dāng)前這個(gè) view 是控制器的 view,那么控制器就是上一個(gè)響應(yīng)者 如果當(dāng)前這個(gè) view 不是控制器的 view,那么父控件就是上一個(gè)響應(yīng)者

一個(gè) objc 對(duì)象的 isa 的指針指向什么?有什 么作用?

每一個(gè)對(duì)象內(nèi)部都有一個(gè) isa 指針,這個(gè)指針是指向它的真實(shí)類型 根據(jù)這個(gè)指針就能知道將來(lái)調(diào)用哪個(gè)類的方法

下面的代碼輸出什么?

@implementation Son : Father 
- (id)init {
    if (self = [super init]) {
        NSLog(@"%@", NSStringFromClass([self class])); // Son
        NSLog(@"%@", NSStringFromClass([super class]));  //Son
    }
    return self; 
}
@end

// 答案:都輸出 Son

這個(gè)題目主要是考察關(guān)于 objc 中對(duì) self 和 super 的理解:
self 是類的隱藏參數(shù),指向當(dāng)前調(diào)用方法的這個(gè)類的實(shí)例。而 super 本質(zhì)是一個(gè)編譯器標(biāo)示符,和 self 是指向的同一個(gè)消息接受者

當(dāng)使用 self 調(diào)用方法時(shí),會(huì)從當(dāng)前類的方法列表中開(kāi)始找,如果沒(méi)有,就從父類中 再找;
而當(dāng)使用 super 時(shí),則從父類的方法列表中開(kāi)始找。然后調(diào)用父類的這個(gè)方法 調(diào)用[self class] 時(shí),會(huì)轉(zhuǎn)化成 objc_msgSend 函數(shù) id objc_msgSend(id self, SEL op, ...)

調(diào) 用 [super class] 時(shí) , 會(huì) 轉(zhuǎn) 化 成 objc_msgSendSuper 函 數(shù) id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

第一個(gè)參數(shù)是 objc_super 這樣一個(gè)結(jié)構(gòu)體,其定義如下 struct objc_super { __unsafe_unretained id receiver;
__unsafe_unretained Class super_class; };

第一個(gè)成員是 receiver, 類似于上面的 objc_msgSend 函數(shù)第一個(gè)參數(shù) self 第二個(gè)成員是記錄當(dāng)前類的父類是什么,告訴程序從父類中開(kāi)始找方法,找到方法 后,最后內(nèi)部是使用 objc_msgSend(objc_super->receiver, @selector(class))去調(diào)用, 此時(shí)已經(jīng)和[self class]調(diào)用相同了,故上述輸出結(jié)果仍然返回 Son

objc Runtime 開(kāi)源代碼對(duì)- (Class)class 方法的實(shí)現(xiàn)

  • (Class)class {
    return object_getClass(self);
    }

以下代碼運(yùn)行結(jié)果如何?

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1"); 
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2"); 
    });
    NSLog(@"3"); 
}

// 答案:主線程死鎖

使用 block 時(shí)什么情況會(huì)發(fā)生引用循環(huán),如 何解決?

系統(tǒng)的某些 block api 中,UIView 的 block 版本寫(xiě)動(dòng)畫(huà)時(shí)不需要考慮,但也有一 些 api 需要考慮

以下這些使用方式不會(huì)引起循環(huán)引用的問(wèn)題

[UIView animateWithDuration:duration animations:^ {
    [self.superview layoutIfNeeded]; 
}];

[[NSOperationQueue mainQueue] addOperationWithBlock:^ {
    self.someProperty = xyz; 
}];

[[NSNotificationCenter defaultCenter] addObserverForName:@"someNotification" object:nil
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * notification)
{ 
    self.someProperty = xyz; 
}];

但如果方法中的一些參數(shù)是成員變量,那么可以造成循環(huán)引用,如:GCD 、 NSNotificationCenter 調(diào)用就要小心一點(diǎn),比如 GCD 內(nèi)部如果引用了 self,而且 GCD 的參數(shù)是 成員變量,則要考慮到循環(huán)引用,舉例如下:

// 【GCD】分析:self-->_operationsQueue-->block-->self 形成閉環(huán),就造成了循環(huán)引用
__weak typeof(self) weakSelf = self;
dispatch_group_async(_operationsGroup, _operationsQueue, ^{
    [weakSelf doSomething];
    [weakSelf doSomethingElse]; 
});
// 【NSNotificationCenter】分析:self-->_observer-->block-->self 形成閉環(huán),就造成了循環(huán)引用
__weak typeof(self) weakSelf = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey" object:nil queue:nil usingBlock:^(NSNotification *note) {
    [weakSelf dismissModalViewControllerAnimated:YES];
}];
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容