iOS知識點總結(jié)(二)

Objectives-C篇

1. 比較strong,weak,assign,copy

  • strong表示指向并擁有該對象。其修飾的對象引用計數(shù)會增加1。該對象只要引用計數(shù)不為0則不會被銷毀。當然強行將其設為nil可以銷毀它。
  • weak表示指向但不擁有該對象。其修飾的對象引用計數(shù)不會增加。無需手動設置,該對象會自行在內(nèi)存中銷毀。
  • assign主要用于修飾基本數(shù)據(jù)類型,如NSInteger和CGFloat,這些數(shù)值主要存在于棧上。
  • weak 一般用來修飾對象,assign一般用來修飾基本數(shù)據(jù)類型。原因是assign修飾的對象被釋放后,指針的地址依然存在,造成野指針,在堆上容易造成崩潰。而棧上的內(nèi)存系統(tǒng)會自動處理,不會造成野指針。
  • copy與strong類似。不同之處是strong的復制是多個指針指向同一個地址,而copy的復制每次會在內(nèi)存中拷貝一份對象,指針指向不同地址。copy一般用在修飾【有可變對應類型】的不可變對象上,如NSString, NSArray, NSDictionary。
  • Objective-C 中,基本數(shù)據(jù)類型的默認關鍵字是atomic, readwrite, assign;普通屬性的默認關鍵字是atomic, readwrite, strong。

2. 請說明并比較以下關鍵詞:__weak,__block

  • __weak與weak基本相同。前者用于修飾變量(variable),后者用于修飾屬性(property)。__weak 主要用于防止block中的循環(huán)引用。
  • __block也用于修飾變量。它是引用修飾,所以其修飾的值是動態(tài)變化的,即可以被重新賦值的。【 __block用于修飾某些block內(nèi)部將要修改的外部變量】
  • __weak和__block的使用場景幾乎與block息息相關。而所謂block,就是Objective-C對于閉包的實現(xiàn)。閉包就是沒有名字的函數(shù),或者理解為指向函數(shù)的指針。

3. 請說明并比較以下關鍵詞:atomatic, nonatomic

  • atomic修飾的對象會保證setter和getter的完整性,任何線程對其訪問都可以得到一個完整的初始化后的對象。因為要保證操作完成,所以速度慢。它比nonatomic安全,但也并不是絕對的線程安全,例如多個線程同時調(diào)用set和get就會導致獲得的對象值不一樣。絕對的線程安全就要用關鍵詞synchronized。
  • nonatomic修飾的對象不保證setter和getter的完整性,所以多個線程對它進行訪問,它可能會返回未初始化的對象。正因為如此,它比atomic快,但也是線程不安全的。

4. 什么是ARC?

ARC全稱是 Automatic Reference Counting,是Objective-C的內(nèi)存管理機制。簡單地來說,就是代碼中自動加入了retain/release,原先需要手動添加的用來處理內(nèi)存管理的引用計數(shù)的代碼可以自動地由編譯器完成了。

ARC的使用是為了解決對象retain和release匹配的問題。以前手動管理造成內(nèi)存泄漏或者重復釋放的問題將不復存在。

以前需要手動的通過retain去為對象獲取內(nèi)存,并用release釋放內(nèi)存。所以以前的操作稱為MRC (Manual Reference Counting)。

5. 什么情況下會出現(xiàn)循環(huán)引用?

循環(huán)引用是指2個或以上對象互相強引用,導致所有對象無法釋放的現(xiàn)象。這是內(nèi)存泄漏的一種情況。舉個例子:

class Father

@interface Father: NSObject
@property (strong, nonatomic) Son *son;

@end

class Son

@interface Son: NSObject
@property (strong, nonatomic) Father *father; 

@end

上述代碼有兩個類,分別為爸爸和兒子。爸爸對兒子強引用,兒子對爸爸強引用。這樣釋放兒子必須先釋放爸爸,要釋放爸爸必須先釋放兒子。如此一來,兩個對象都無法釋放。

解決方法是將Father中的Son對象屬性從strong改為weak。

內(nèi)存泄漏可以用Xcode中的Debug Memory Graph去檢查,同時Xcode也會在runtime中自動匯報內(nèi)存泄漏的問題。

6. 下面代碼中有什么bug?

- (void)viewDidLoad {
  UILabel *alertLabel = [[UILabel alloc] initWithFrame:CGRectMake(100,100,100,100)];
  alertLabel.text = @"Wait 4 seconds...";
  [self.view addSubview:alertLabel];

  NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
  [backgroundQueue addOperationWithBlock:^{
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:4]];
    alertLabel.text = @"Ready to go!”
  }];
}

Bug在于,在等了4秒之后,alertLabel并不會更新為Ready to Go。

原因是,所有UI的相關操作應該在主線程進行。當我們可以在一個后臺線程中等待4秒,但是一定要在主線程中更新alertLabel。

最簡單的修正如下:

- (void)viewDidLoad {
  UILabel *alertLabel = [[UILabel alloc] initWithFrame:CGRectMake(100,100,100,100)];
  alertLabel.text = @"Wait 4 seconds...";
  [self.view addSubview:alertLabel];

  NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
  [backgroundQueue addOperationWithBlock:^{
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:4]];
      [[NSOperationQueue mainQueue] addOperationWithBlock:^{
         alertLabel.text = @"Ready to go!”
      }];
  }];
}

7. 以scheduledTimerWithTimeInterval的方式觸發(fā)的timer,在滑動頁面上的列表時,timer會暫停,為什么?該如何解決?

原因在于滑動時當前線程的runloop切換了mode用于列表滑動,導致timer暫停。

runloop中的mode主要用來指定事件在runloop中的優(yōu)先級,有以下幾種:
  • Default(NSDefaultRunLoopMode):默認,一般情況下使用;
  • Connection(NSConnectionReplyMode):一般系統(tǒng)用來處理NSConnection相關事件,開發(fā)者一般用不到;
  • Modal(NSModalPanelRunLoopMode):處理modal panels事件;
  • Event Tracking(NSEventTrackingRunLoopMode):用于處理拖拽和用戶交互的模式。
  • Common(NSRunloopCommonModes):模式合集。默認包括Default,Modal,Event Tracking三大模式,可以處理幾乎所有事件。

回到題中的情境?;瑒恿斜頃r,runloop的mode由原來的Default模式切換到了Event Tracking模式,timer原來好好的運行在Default模式中,被關閉后自然就停止工作了。

解決方法其一是將timer加入到NSRunloopCommonModes中。其二是將timer放到另一個線程中,然后開啟另一個線程的runloop,這樣可以保證與主線程互不干擾,而現(xiàn)在主線程正在處理頁面滑動。示例代碼如下:

// 方法1
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

// 方法2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(repeat:) userInfo:nil repeats:true];
    [[NSRunLoop currentRunLoop] run];
});

Swift篇

8. 類(class)和結(jié)構(gòu)體(struct)有什么區(qū)別?

Swift 中,類是引用類型,結(jié)構(gòu)體是值類型。值類型在傳遞和賦值時將進行復制,而引用類型則只會使用引用對象的一個"指向"。所以他們兩者之間的區(qū)別就是兩個類型的區(qū)別。

舉個簡單的例子,代碼如下

class Temperature {
  var value: Float = 37.0
}

class Person {
  var temp: Temperature?

  func sick() {
    temp?.value = 41.0
  }
}

let A = Person()
let B = Person()
let temp = Temperature()

A.temp = temp
B.temp = temp

A.sick()

上面這段代碼,由于 Temperature 是 class ,為引用類型,故 A 的 temp 和 B 的 temp指向同一個對象。A 的 temp修改了,B 的 temp 也隨之修改。這樣 A 和 B 的 temp 的值都被改成了41.0。如果將 Temperature 改為 struct,為值類型,則 A 的 temp 修改不影響 B 的 temp。

內(nèi)存中,引用類型諸如類是在堆(heap)上,而值類型諸如結(jié)構(gòu)體實在棧(stack)上進行存儲和操作。相比于棧上的操作,堆上的操作更加復雜耗時,所以蘋果官方推薦使用結(jié)構(gòu)體,這樣可以提高 App 運行的效率。

class有這幾個功能struct沒有的:
  • class可以繼承,這樣子類可以使用父類的特性和方法
  • 類型轉(zhuǎn)換可以在runtime的時候檢查和解釋一個實例的類型
  • 可以用deinit來釋放資源
  • 一個類可以被多次引用
struct也有這樣幾個優(yōu)勢:
  • 結(jié)構(gòu)較小,適用于復制操作,相比于一個class的實例被多次引用更加安全。
  • 無須擔心內(nèi)存memory leak或者多線程沖突問題

9. Swift 是面向?qū)ο筮€是函數(shù)式的編程語言?

Swift 既是面向?qū)ο蟮?,又是函?shù)式的編程語言。

說 Swift 是面向?qū)ο蟮恼Z言,是因為 Swift 支持類的封裝、繼承、和多態(tài),從這點上來看與 Java 這類純面向?qū)ο蟮恼Z言幾乎毫無差別。

說 Swift 是函數(shù)式編程語言,是因為 Swift 支持 map, reduce, filter, flatmap 這類去除中間狀態(tài)、數(shù)學函數(shù)式的方法,更加強調(diào)運算結(jié)果而不是中間過程。

10. 請說明并比較以下關鍵詞:Open, Public, Internal, File-private, Private

Swift 有五個級別的訪問控制權(quán)限,從高到底依次為比如 Open, Public, Internal, File-private, Private。

他們遵循的基本原則是:高級別的變量不允許被定義為低級別變量的成員變量。比如一個 private 的 class 中不能含有 public 的 String。反之,低級別的變量卻可以定義在高級別的變量中。比如 public 的 class 中可以含有 private 的 Int。

  • Open 具備最高的訪問權(quán)限。其修飾的類和方法可以在任意 Module 中被訪問和重寫;它是 Swift 3 中新添加的訪問權(quán)限。
  • Public 的權(quán)限僅次于 Open。與 Open 唯一的區(qū)別在于它修飾的對象可以在任意 Module 中被訪問,但不能重寫。
  • Internal 是默認的權(quán)限。它表示只能在當前定義的 Module 中訪問和重寫,它可以被一個 Module 中的多個文件訪問,但不可以被其他的 Module 中被訪問。
  • File-private 也是 Swift 3 新添加的權(quán)限。其被修飾的對象只能在當前文件中被使用。例如它可以被一個文件中的 class,extension,struct 共同使用。
  • Private 是最低的訪問權(quán)限。它的對象只能在定義的作用域內(nèi)使用。離開了這個作用域,即使是同一個文件中的其他作用域,也無法訪問。

11. 請說明并比較以下關鍵詞:strong, weak, unowned

Swift 的內(nèi)存管理機制與 Objective-C一樣為 ARC(Automatic Reference Counting)。它的基本原理是,一個對象在沒有任何強引用指向它時,其占用的內(nèi)存會被回收。反之,只要有任何一個強引用指向該對象,它就會一直存在于內(nèi)存中。
  • strong 代表著強引用,是默認屬性。當一個對象被聲明為 strong 時,就表示父層級對該對象有一個強引用的指向。此時該對象的引用計數(shù)會增加1。
  • weak 代表著弱引用。當對象被聲明為 weak 時,父層級對此對象沒有指向,該對象的引用計數(shù)不會增加1。它在對象釋放后弱引用也隨即消失。繼續(xù)訪問該對象,程序會得到 nil,不虧崩潰
  • unowned 與弱引用本質(zhì)上一樣。唯一不同的是,對象在釋放后,依然有一個無效的引用指向?qū)ο螅皇?Optional 也不指向 nil。如果繼續(xù)訪問該對象,程序就會崩潰。
加分回答:

weak 和 unowned 的引入是為了解決由 strong 帶來的循環(huán)引用問題。簡單來說,就是當兩個對象互相有一個強指向去指向?qū)Ψ剑@樣導致兩個對象在內(nèi)存中無法釋放。

weak 和 unowned 的使用場景有如下差別:
  • 當訪問對象時該對象可能已經(jīng)被釋放了,則用 weak。比如 delegate 的修飾。
  • 當訪問對象確定不可能被釋放,則用 unowned。比如 self 的引用。
  • 實際上為了安全起見,很多公司規(guī)定任何時候都使用 weak 去修飾。

13. 用 Swift 實現(xiàn)或(||)操作

這題解法很多,下面給出一種最直接的解法:

func ||(left: Bool, right: Bool) –> Bool {
  if left {
    return true
  } else {
    return right
  }
}

上面這種解法勉強正確,但是并不高效?;颍▅|)操作的本質(zhì)是當左邊為真的時候,我們無需計算右邊。而上面這種事先,是將右邊默認值預先準備好,再傳入進行操作。當右邊值的計算十分復雜時會 造成了性能上的浪費。所以,上面這種做法違反了或(||)操作的本質(zhì)。正確的實現(xiàn)方法如下:

func ||(left: Bool, right: @autoclosure () -> Bool) –> Bool {
  if left {
    return true
  } else {
    return right()
  }
}

autoclosure 可以將右邊值的計算推遲到判定left 為 false 的時候,這樣就可以避免第一種方法帶來的不必要開銷了。

14. 實現(xiàn)一個函數(shù)。求一個整型二維數(shù)組中所有元素之和

func sumPairs(_ nums: [[Int]]) -> Int {
  return nums.flatMap { $0 }.reduce(0) { $0 + $1 }
}

Swift 有函數(shù)式編程的思想。其中 flatMap, map, reduce, filter 是其代表的方法。本題中考察了 flatMap 的降維思路,以及 reduce 的基本使用。相比于一般的 for 循環(huán),這樣的寫法要更加得簡潔漂亮。


比較OC和Swift

15. 說說Swift為什么將String,Array,Dictionary設計成值類型?

要解答這個問題,就要和Objective-C中相同的數(shù)據(jù)結(jié)構(gòu)設計進行比較。Objective-C中,字符串,數(shù)組,字典,皆被設計為引用類型。

  • 值類型相比引用類型,最大的優(yōu)勢在于內(nèi)存使用的高效。值類型在棧上操作,引用類型在堆上操作。棧上的操作僅僅是單個指針的上下移動,而堆上的操作則牽涉到合并、移位、重新鏈接等。也就是說Swift這樣設計,大幅減少了堆上的內(nèi)存分配和回收的次數(shù)。同時copy-on-write又將值傳遞和復制的開銷降到了最低。
  • String,Array,Dictionary設計成值類型,也是為了線程安全考慮。通過Swift的let設置,使得這些數(shù)據(jù)達到了真正意義上的“不變”,它也從根本上解決了多線程中內(nèi)存訪問和操作順序的問題。
  • 設計成值類型還可以提升API的靈活度。例如通過實現(xiàn)Collection這樣的協(xié)議,我們可以遍歷String,使得整個開發(fā)更加靈活高效。

16. 在Swift和Objective-C的混編項目中,如何在Swift文件中調(diào)用Objective-C文件中已經(jīng)定義的方法?如何在Objective-C文件中調(diào)用Swift文件中定義的方法?

  • Swift中若要使用Objective-C代碼,可以在ProjectName-Bridging-Header.h里添加Objective-C的頭文件名稱,Swift文件中即可調(diào)用相應的Objective-C代碼。一般情況Xcode會在Swift項目中第一次創(chuàng)建Objective-C文件時自動創(chuàng)建ProjectName-Bridging-Header.h文件。
  • Objective-C中若要調(diào)用Swift代碼,可以導入Swift生成的頭函數(shù)ProjectName-Swift.h來實現(xiàn)。
  • Swift文件中若要規(guī)定固定的方法或?qū)傩员┞督oObjective-C使用,可以在方法或?qū)傩郧凹由螥objc來聲明。如果該類是NSObject子類,那么Swift會在非private的方法或?qū)傩郧白詣蛹由螥objc。

17. 用Swift 將協(xié)議(protocol)中的部分方法設計成可選(optional),該怎樣實現(xiàn)?

@optional@required是 Objective-C 中特有的關鍵字。
Swift中,默認所有方法在協(xié)議中都是必須實現(xiàn)的。而且,協(xié)議里方法不可以直接定義 optional。先給出兩種解決方案:

  1. 在協(xié)議和方法前都加上 @objc 關鍵字,然后再在方法前加上 optional 關鍵字。該方法實際上是把協(xié)議轉(zhuǎn)化為Objective-C的方式然后進行可選定義。示例如下:
@objc protocol SomeProtocol {
  func requiredFunc()
  @objc optional func optionalFunc()
}
  1. 用擴展(extension)來規(guī)定可選方法。Swift中,協(xié)議擴展(protocol extension)可以定義部分方法的默認實現(xiàn),這樣這些方法在實際調(diào)用中就是可選實現(xiàn)的了。示例如下:
protocol SomeProtocol {
  func requiredFunc()
  func optionalFunc()
}

extension SomeProtocol {
  func optionalFunc() {
    print(“Dumb Implementation”)
  }
}

Class SomeClass: SomeProtocol {
  func requiredFunc() {
    print(“Only need to implement the required”)
  }
}

18. 要給一個UIButton增加一個點擊后抖動的效果,該怎樣實現(xiàn)?

解決方案有三種。個人推薦用protocol來解決。

  • 實現(xiàn)一個自定義的UIButton類,在其中添加點擊抖動效果的方法(shake方法)
  • 寫一個UIButton或者UIView的拓展(extension),然后在其中增加shake方法
  • 定義一個protocol,然后在協(xié)議擴展(protocol extension)中添加shake方法

分析這三種方法:

  • 在自定義的類中添加shake方法擴展性不好。如果shake方法被用在其他地方,又要在其他類中再添加一遍- shake方法,這樣代碼復用性差。
  • 在extension中實現(xiàn)雖然解決了代碼復用性問題,但是可讀性比較差。團隊開發(fā)中并不是所有人都知道這個extension中存在shake方法,同時隨著功能的擴展,extension中新增的方法會層出不窮,它們很難歸類管理。
  • 用協(xié)議定義解決了復用性、可讀性、維護性三個難題。協(xié)議的命名(例如Shakeable)直接可以確定其實現(xiàn)的UIButton擁有相應shake功能;通過協(xié)議擴展,可以針對不同類實現(xiàn)特定的方法,可維護性也大大提高;因為協(xié)議擴展通用于所有實現(xiàn)對象,所以代碼復用性也很高。

19. 試比較Swift和Objective-C中的初始化方法(init)有什么異同?

一言以蔽之,Swift中的初始化方法更加嚴格和準確。

  • Objective-C中,初始化方法無法保證所有成員變量都完成初始化;編譯器對屬性設置并無警告,但是實際操作中會出現(xiàn)初始化不完全的問題;初始化方法與普通方法并無實際差別,可以多次調(diào)用。
  • Swift中,初始化方法必須保證所有optional的成員變量都完成初始化。同時新增convenience和required兩個修飾初始化方法的關鍵詞。convenience只是提供一種方便的初始化方法,必須通過調(diào)用同一個類中designated初始化方法來完成。required是強制子類重寫父類中所修飾的初始化方法。

20. 談談對Objective-C和Swift 動態(tài)特性的理解

runtime其實就是Objective-C的動態(tài)機制。runtime執(zhí)行的是編譯后的代碼,這時它可以動態(tài)加載對象、添加方法、修改屬性、傳遞信息等等。具體過程是在Objective-C中對象調(diào)用方法時,如 [self.tableview reload] ,發(fā)生了兩件事。

  • 編譯階段,編譯器(compiler)會把這句話翻譯成 objc_msgSend(self.tableview, [@selector](reload)),把消息發(fā)送給self.tableview。
  • 運行階段,接收者self.tableview會響應這個消息,期間可能會直接執(zhí)行、轉(zhuǎn)發(fā)消息,也可能會找不到方法崩潰。

所以整個流程是編譯器翻譯 –> 給接收者發(fā)送消息 –> 接收者響應消息三個流程。

如[self.tableview reload]中,self.tableview就是接收者,reload就是消息,所以方法調(diào)用的格式在編譯器看來是[receiver message]。

其中接收者如何響應代碼,就發(fā)生在運行時(runtime)。runtime執(zhí)行的是編譯后的代碼,這時它可以動態(tài)加載對象、添加方法、修改屬性、傳遞信息等等,runtime的運行機制就是Objective-C的動態(tài)特性。

Swift目前被公認為是一門靜態(tài)語言。它的動態(tài)特性都是通過橋接OC來實現(xiàn)。如果要把動態(tài)特性寫得更Swift一點,可以用protocol來處理,比如OC中的reflection這樣寫:

if ([someImage respondsToSelector:@selector(shake)]) {
  [someImage performSelector:shake];
}

Swift 中可以這樣寫:

if let shakeableImage = someImage as? Shakeable {
  shakeableImage.shake()
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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