淺談 iOS 應用啟動過程

萌面大道發表於2017-03-27

由於種種原因,掘金等第三方平臺部落格不再保證能夠同步更新,歡迎移步 GitHub:github.com/kingcos/Per…。謝謝!

Create an iOS single view application manually in Swift.

Date Notes Swift Xcode
2017-05-26 CS193p UIApplication 3.1 8.3.2
2017-03-28 首次提交 3.0 8.2.1

Preface

首先要感謝沒故事的卓同學大大送的泊學會員,在泊學學了幾節課,瞭解了很多不同角度的 iOS 開發知識。這篇文章就啟發自其 iOS 101 中的一個純手工的 Single View Application 一文。但本文將更加深入的敘述了啟動過程,且實現均為 Swift 3.0。

本文對應的 Demo 可以在:github.com/kingcos/Sin… 檢視、下載。

Manually or Storyboard

main.m in Objective-C Single View Application

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
複製程式碼
  • 自從 Storyboard 誕生以來,關於純程式碼、Xib、以及 Storyboard 的選擇就在 iOS 開發圈中炸開了鍋。這裡不會探討各種姿勢的優劣,只是可能很多和我一樣的初學者,從一開始就被 Storyboard 先入為主。加上方便靈活的拖控制元件,自然而然就可能沒有機會去思考一個 iOS 應用是如何啟動起來的。加上 Swift 的誕生,使得整個專案的初始結構得到了更大的簡化(少了 main.m 以及很多 .h 標頭檔案)。
  • 為了便於研究 iOS 應用的啟動過程,我們刪除 Xcode 自動建立的 Main.storyboard,並把 Main Interface 清空。

Main Interface

AppDelegate.swift

  • AppDelegate.swift 中是 AppDelegate 類。
  • AppDelegate 將會建立 App 內容繪製的視窗,並提供應用內響應狀態改變(state transitions)的地方。
  • AppDelegate 將會建立 App 的入口和 Run Loop(執行迴圈),並將輸入事件傳送到 App(由 @UIApplicationMain 完成)。

Run Loop: An event processing loop that you use to schedule work and coordinate the receipt of incoming events in your app. (From Start Developing iOS Apps (Swift)) Run Loop 是一個事件處理迴圈,可以用來在應用中安排任務並定位收到的即將到來的事件。

AppDelegate.swift

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        window = UIWindow()
        window?.backgroundColor = UIColor.red
        window?.rootViewController = UIViewController()
        window?.makeKeyAndVisible()

        return true
    }
}
複製程式碼
  • 因為我們刪除了 Main.storyboard,我們需要以上程式碼初始化 UIWindow(根 UIView),並使得其可見,關於 UIWindow 可以參考文末的連結。
  • AppDelegate 中的方法將應用程式物件和代理聯絡起來,當應用在不同狀態會自動呼叫相應的代理方法,我們可以自定義相應的實現,抑或留空或刪除即使用預設的實現。
  • application(_:​did​Finish​Launching​With​Options:​):該方法在應用啟動程式幾乎完成且將要執行之際呼叫,因此在其中初始化 window,設定根控制器,並使得其可見。

@UIApplicationMain

main.swift

import UIKit

UIApplicationMain(
    CommandLine.argc,
    UnsafeMutableRawPointer(CommandLine.unsafeArgv)
        .bindMemory(
            to: UnsafeMutablePointer<Int8>.self,
            capacity: Int(CommandLine.argc)),
    nil,
    NSStringFromClass(AppDelegate.self)
)
複製程式碼
  • 在 AppDelegate.swift 檔案中 AppDelegate 類宣告之上的一行便是 @UIApplicationMain。
  • 我們可以嘗試將該行註釋,連結器將直接報錯「entry point (_main) undefined.」,即入口 main 未定義,因此可以得知 @UIApplicationMain 標籤將會根據其下方的 AppDelegate 建立一個 UIApplicationMain 入口並啟動程式。
  • 手動實現 @UIApplicationMain:
    • 如果不使用 @UIApplicationMain 標籤,需要自己建立一個 main.swift 檔案,但我們不需要自己建立方法,Xcode 可以直接將該檔案中的程式碼當作 main() 函式。
    • 在 main.swift 中,我們新增以上的程式碼。

Code written at global scope is used as the entry point for the program, so you don’t need a main() function. (From The Swift Programming Language (Swift 3.0.1)) 全域性範圍書寫的程式碼將被當作程式入口,所以不需要 main() 函式。

UIApplication​Main()

  • 在 main.swift 中,呼叫了 int UIApplicationMain(int argc, char * _Nonnull argv[], NSString *principalClassName, NSString *delegateClassName); 方法。
  • 該方法在建立了應用程式物件、應用程式代理、以及設定了事件迴圈。
  • UIApplication​Main() 的前兩個引數是命令列引數。
  • principalClassName: 該引數為 UIApplication 類名或其子類名的字串,nil 是預設為 UIApplication。
  • delegateClassName: 該引數為要初始化的應用程式代理(AppDelegate)類,也可指定為 nil 但需要從應用程式的主 nib 檔案載入代理物件。
  • 雖然該函式有返回值,但從不返回。

UIApplication

MyApp.swift

class MyApp: UIApplication {
    override func sendEvent(_ event: UIEvent) {
        print("\(#function) - Event: \(event)")

        super.sendEvent(event)
    }
}
複製程式碼
  • 由上文得知,main.swift 中 UIApplication​Main()的第三個引數可以為 UIApplication 類名或其子類名的字串。
  • 新建一個 MyApp.swift 在其中定義一個 UIApplication 子類,我們便可以在這裡做一些針對應用全域性的事情,比如重寫 sendEvent(:) 方法便可以監聽到整個應用傳送事件。

Update for CS193p

let myApp = UIApplication.shared
複製程式碼
  • UIApplication 在 App 中是單例的。
  • UIApplication 管理所有全域性行為。
  • UIApplication 不需要子類化。
// 在其他 App 中開啟 URL
open func open(_ url: URL, options: [String : Any] = [:], completionHandler completion: ((Bool) -> Swift.Void)? = nil)

@available(iOS 3.0, *)
open func canOpenURL(_ url: URL) -> Bool

// 註冊接收推送通知
@available(iOS 8.0, *)
open func registerForRemoteNotifications()

@available(iOS 3.0, *)
open func unregisterForRemoteNotifications()
// 本地或推送的通知由 UNNotification 處理

// 設定後臺取回間隔
@available(iOS 7.0, *)
open func setMinimumBackgroundFetchInterval(_ minimumBackgroundFetchInterval: TimeInterval)
// 通常將其設定為:
UIApplicationBackgroundFetchIntervalMinimum

// 後臺時請求更長時間
@available(iOS 4.0, *)
open func beginBackgroundTask(expirationHandler handler: (() -> Swift.Void)? = nil) -> UIBackgroundTaskIdentifier
// 不要忘記結束時呼叫 endBackgroundTask(UIBackgroundTaskIdentifier)

// 狀態來網路使用 Spinner 顯示開關
var isNetworkActivityIndicatorVisible: Bool

var backgroundTimeRemaining: TimeInterval { get } // 直到暫停
var preferredContentSizeCategory: UIContentSizeCategory { get } // 大字型或小字型
var applicationState: UIApplicationState { get } // 前臺,後臺,已啟用
複製程式碼

Reference

也歡迎您關注我的微博 @萌面大道V & 簡書

相關文章