前言
在iOS應用開發中,啟動速度是影響用戶體驗的重要因素之一。研究表明,啟動時間每增加1秒,用戶留存率就會下降約7%。本文將深入探討iOS啟動優化的各個方面,從底層原理到具體實踐,幫助開發者打造更快的應用啟動體驗。
一、iOS啟動流程解析
1.1 冷啟動與熱啟動
冷啟動(Cold Launch)
冷啟動是指應用完全未運行,需要從磁盤加載所有資源的過程。這是最完整的啟動過程,包含了所有初始化步驟。
熱啟動(Warm Launch)
熱啟動是指應用已存在于內存中,只需恢復運行狀態的過程。相比冷啟動,熱啟動跳過了部分初始化步驟,啟動速度更快。
1.2 Mach-O文件結構
在深入討論啟動流程之前,我們需要先了解iOS應用的可執行文件格式 - Mach-O。Mach-O(Mach Object)是macOS和iOS系統使用的可執行文件格式,其結構設計精巧且高效。
1.2.1 文件結構概述
由三個主要部分組成:
Header(文件頭) Load Commands(加載命令)Data(數據段)
Header(文件頭)
- 包含文件的基本信息,如CPU架構、文件類型等
- 定義了Load Commands的數量和大小
struct mach_header_64 {uint32_t magic; // 魔數,標識文件類型uint32_t cputype; // CPU類型uint32_t cpusubtype; // CPU子類型uint32_t filetype; // 文件類型uint32_t ncmds; // Load Commands數量uint32_t sizeofcmds; // Load Commands總大小uint32_t flags; // 標志位uint32_t reserved; // 保留字段
};
Load Commands(加載命令)
- 描述了如何加載文件內容
- 定義了段的位置、大小、權限等
- 主要命令類型:
LC_SEGMENT_64
:定義段的位置和屬性LC_DYLD_INFO
:動態鏈接信息LC_SYMTAB
:符號表信息LC_LOAD_DYLIB
:依賴的動態庫LC_CODE_SIGNATURE
:代碼簽名信息
Data(數據段)
- 包含實際的代碼和數據
- 按功能分為多個段(Segment)
1.2.2 段(Segment)與節(Section)詳解
在Mach-O文件中,數據部分被組織成多個段(Segment),每個段又包含多個節(Section)。這種層次結構的設計使得不同類型的代碼和數據可以被合理地組織和管理。
-
段(Segment)的基本概念
- 段是Mach-O文件中的主要數據組織單位
- 每個段都有特定的內存保護屬性(如可讀、可寫、可執行)
- 段通過Load Commands中的
LC_SEGMENT_64
命令定義 - 段的大小必須是頁大小的整數倍(通常為4KB或16KB)
-
主要段及其作用
-
__TEXT
段:包含可執行代碼和只讀數據- 內存屬性:只讀、可執行
- 主要用途:存儲程序代碼和常量數據
- 優化建議:將頻繁執行的代碼放在一起,提高緩存命中率
-
__DATA
段:包含可讀寫數據- 內存屬性:可讀、可寫
- 主要用途:存儲全局變量、靜態變量等
- 優化建議:減少全局變量使用,降低內存占用
-
__LINKEDIT
段:包含鏈接器使用的信息- 內存屬性:只讀
- 主要用途:存儲符號表、字符串表等鏈接信息
- 優化建議:減少符號數量,降低鏈接時間
-
-
節(Section)
- 節是段內的更小組織單位
- 每個節都有特定的用途和屬性
-
段與節的關系
- 段是內存管理的基本單位,定義了內存保護屬性
- 節是邏輯組織單位,定義了具體的數據類型和用途
- 一個段可以包含多個節,但所有節共享段的內存屬性
- 節的布局會影響程序的性能和內存使用
1.3 啟動時間線
pre-main階段
Mach-O加載(冷啟動特有)
- 內核首先加載應用可執行文件(Mach-O),這個過程涉及虛擬內存映射和代碼簽名驗證。
- Mach-O文件包含多個段(Segment),如__TEXT(代碼段)、__DATA(數據段)等,每個段都有特定的內存保護屬性。
- 創建進程和主線程時,系統會分配虛擬內存空間,設置ASLR(地址空間布局隨機化)以增強安全性。
- 系統會初始化進程的虛擬內存管理結構,包括頁表、內存區域描述符等。
動態鏈接階段(冷啟動特有)
- dyld(動態鏈接器)開始工作,它首先解析Mach-O文件的LC_LOAD_DYLIB命令,獲取所有依賴的動態庫。
- 對于每個動態庫,dyld會遞歸加載其依賴項,這個過程可能涉及磁盤I/O和內存映射。
- 符號解析階段,dyld需要處理大量的符號引用,包括函數調用、全局變量訪問等。
- 重定位階段,dyld需要修改代碼中的地址引用,使其指向正確的內存位置。
運行時初始化
- Objective-C運行時環境被初始化,系統會掃描所有類,構建類繼承關系圖。
- 方法注冊階段,系統會為每個方法創建IMP(Implementation)指針,并建立方法選擇器(SEL)到IMP的映射。
- +load方法的執行是同步的,且執行順序不確定,這可能導致死鎖或性能問題。
- C++靜態初始化會觸發全局對象的構造函數調用,這些調用可能涉及復雜的初始化邏輯。
main階段
UIApplicationMain(冷啟動特有)
- main函數執行時,系統會創建UIApplication實例,這個過程涉及大量的Objective-C消息發送。
- UIApplicationMain會創建主線程RunLoop,設置事件源和觀察者。
- AppDelegate的初始化可能涉及復雜的業務邏輯,如網絡請求、數據庫操作等。
應用生命周期
- application:didFinishLaunchingWithOptions:方法中可能包含大量的初始化代碼。
- 視圖控制器的創建和配置可能涉及復雜的依賴關系。
- 數據預加載可能觸發大量的I/O操作。
首屏渲染階段
視圖層級構建
- 視圖的創建涉及大量的內存分配和對象初始化。
- 自動布局計算使用Cassowary算法,時間復雜度是O(n3)。
- 視圖的繪制涉及Core Animation的圖層樹構建。
數據加載
- 網絡請求可能受到DNS解析、TCP連接建立等因素的影響。
- 本地數據讀取涉及文件I/O和數據庫操作。
- 圖片解碼可能占用大量的CPU和內存資源。
UI狀態恢復(熱啟動特有)
- 視圖層級的重建需要處理大量的自動布局約束。
- 系統會重新計算視圖的frame和bounds,這個過程可能觸發多次布局計算。
- 用戶界面狀態的恢復可能涉及大量的狀態同步操作。
二、啟動優化方案
2.1 pre-main階段優化
2.1.1 減少動態庫數量
優化動態庫加載是提升啟動速度的關鍵,可以通過以下方式實現:
-
使用靜態庫替代動態庫
- 靜態庫在編譯時被鏈接到可執行文件中,這可以完全消除動態庫加載的開銷。
- 在Build Settings中設置"Mach-O Type"為"Static Library",編譯器會將靜態庫的代碼和數據直接合并到主二進制文件中。
- 使用靜態庫可以減少約30-50%的啟動時間,具體取決于動態庫的數量和大小。
// 在Build Settings中設置 MACH_O_TYPE = staticlib
-
合并多個動態庫為一個
- 使用lipo工具合并多個架構的庫,可以減少dyld的加載次數。
- 合并庫時需要處理符號沖突,可以使用-fvisibility=hidden來控制符號的可見性。
- 合并后的庫大小會增加,但啟動性能會提升約20-30%。
# 合并多個架構的庫 lipo -create lib1.a lib2.a -output libCombined.a# 設置符號可見性 OTHER_CFLAGS = -fvisibility=hidden
-
使用弱引用動態庫
- 在Other Linker Flags中添加-weak_framework可以實現弱引用動態庫。
- 弱引用動態庫會在首次使用時才加載,這可以延遲非必需庫的加載時間。
- 這種方式可以減少約10-15%的啟動時間,但會增加首次使用時的延遲。
// 在Other Linker Flags中設置 OTHER_LDFLAGS = -weak_framework FrameworkName
2.1.2 優化+load方法
+load方法的優化對啟動性能有顯著影響:
-
避免在+load中執行耗時操作
- +load方法在main函數前執行,且執行順序不確定,應該避免在這里執行耗時操作。
- 使用dispatch_once可以確保線程安全,但要注意避免死鎖。
- 在+load中執行耗時操作可能導致啟動時間增加50-100ms。
class MyClass {static func load() {// 使用dispatch_once確保線程安全DispatchQueue.once(token: "MyClass.load") {setupEssentialComponents()}}private static func setupEssentialComponents() {// 只進行必要的初始化,避免耗時操作} }
-
使用initialize替代load
- initialize方法在類第一次使用時才會調用,這可以延遲非必需初始化。
- initialize方法是線程安全的,且可以被子類覆蓋,這提供了更大的靈活性。
- 使用initialize替代load可以減少約20-30ms的啟動時間。
class MyClass {static func initialize() {if self == MyClass.self {DispatchQueue.global(qos: .default).async {setupComponents()}}}private static func setupComponents() {// 確保只對當前類執行初始化} }
2.1.3 控制C++靜態初始化
C++靜態初始化的優化可以顯著提升啟動性能:
-
減少全局變量使用
- 使用單例模式替代全局變量,可以避免靜態初始化的不確定性。
- 通過靜態局部變量實現線程安全的延遲初始化,這可以避免啟動時的性能開銷。
- 禁止拷貝和賦值操作可以防止意外的對象復制。
class MyManager {static let shared = MyManager()private init() {// 私有構造函數,防止外部創建實例}// 禁止拷貝和賦值操作private func copy() -> MyManager {return self} }
-
延遲初始化
- 使用靜態局部變量實現延遲初始化,可以避免啟動時的性能開銷。
- 這種方式可以減少約10-20ms的啟動時間,具體取決于初始化操作的復雜度。
class LazyInitializer {static var data: String {// 使用靜態局部變量實現延遲加載struct Static {static let instance = loadData()}return Static.instance}private static func loadData() -> String {// 實現數據加載邏輯return ""} }
2.2 main階段優化
2.2.1 延遲初始化
延遲初始化是提升啟動性能的有效手段:
-
懶加載模式
- 通過檢查屬性是否為空來決定是否加載數據,可以減少啟動時的資源占用。
- 這種方式可以減少約30-50ms的啟動時間,具體取決于數據的大小和復雜度。
class DataManager {private var _dataArray: [Any]?private let dataQueue = DispatchQueue(label: "com.app.dataQueue")var dataArray: [Any] {if _dataArray == nil {dataQueue.async {self._dataArray = self.loadData()}}return _dataArray ?? []}private func loadData() -> [Any] {// 實現數據加載邏輯return []} }
-
線程安全的單例
- 使用dispatch_once確保線程安全,可以避免競態條件。
- 這種方式可以減少約10-20ms的啟動時間,具體取決于初始化操作的復雜度。
class SharedData {static let shared = SharedData()private var _data: [Any]?var data: [Any] {if _data == nil {DispatchQueue.once(token: "SharedData.data") {_data = loadData()}}return _data ?? []}private func loadData() -> [Any] {// 實現數據加載邏輯return []} }
2.2.2 異步初始化
異步初始化可以顯著提升啟動響應性:
-
后臺線程初始化
- 將非關鍵初始化操作放到后臺線程,可以避免阻塞主線程。
- 這種方式可以減少約50-100ms的主線程阻塞時間。
class AppDelegate: UIResponder, UIApplicationDelegate {func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {// 在后臺線程執行非關鍵初始化DispatchQueue.global(qos: .default).async {self.setupNonCriticalComponents()}return true}private func setupNonCriticalComponents() {// 實現非關鍵組件的初始化} }
-
并發控制
- 使用OperationQueue控制并發數量,可以平衡性能和資源利用。
- 這種方式可以減少約20-30%的初始化時間,具體取決于任務的并行度。
class ComponentManager {private let operationQueue: OperationQueue = {let queue = OperationQueue()queue.maxConcurrentOperationCount = 2return queue}()func setupComponents() {operationQueue.addOperation {self.setupComponentA()}operationQueue.addOperation {self.setupComponentB()}}private func setupComponentA() {// 實現組件A的初始化}private func setupComponentB() {// 實現組件B的初始化} }
2.3 首屏渲染優化
2.3.1 視圖層級優化
視圖層級的優化對渲染性能有顯著影響:
-
減少視圖層級
- 使用扁平化結構,可以顯著提升渲染性能。
- 每減少一層視圖嵌套,可以提升約5-10%的渲染性能。
-
使用CALayer替代UIView
- CALayer比UIView更輕量級,具有更好的性能。
- 使用CALayer可以減少約30-50%的內存占用和20-30%的渲染時間。
2.3.2 圖片資源優化
圖片資源的優化對內存使用和渲染性能有重要影響:
-
圖片格式選擇
- 選擇合適的圖片格式可以顯著減少內存占用和加載時間。
- WebP格式比PNG小約30-50%,比JPEG小約20-30%。
- HEIC格式在iOS設備上有硬件加速支持,解碼性能更好。
-
懶加載和緩存實現
- 在后臺線程加載圖片,可以避免阻塞主線程。
- 使用NSCache實現圖片緩存,可以避免重復加載。
- 這種方式可以減少約50-100ms的圖片加載時間。
- 設置合適的緩存大小限制,可以平衡內存使用和性能。
- 這種方式可以減少約30-50%的圖片加載時間。
實現示例:
class ImageManager {private let imageCache = NSCache<NSString, UIImage>()private let imageQueue = DispatchQueue(label: "com.app.imageQueue")func setupImageCache() {imageCache.countLimit = 100}func loadImageIfNeeded(for imageView: UIImageView, path: String) {guard imageView.image == nil else { return }if let cachedImage = imageCache.object(forKey: path as NSString) {imageView.image = cachedImage} else {imageQueue.async {if let image = UIImage(contentsOfFile: path) {self.imageCache.setObject(image, forKey: path as NSString)DispatchQueue.main.async {imageView.image = image}}}}} }
三、進階優化技巧
3.1 二進制重排
二進制重排是提升啟動性能的高級技巧,通過優化代碼在內存中的布局來減少缺頁中斷:
3.1.1 原理與優勢
- 通過修改代碼段的物理布局,使啟動時需要的代碼盡可能連續存放
- 減少缺頁中斷(Page Fault)次數,每次缺頁中斷約消耗10ms
- 提高CPU緩存命中率,減少內存訪問延遲
- 可提升啟動速度約20-40%
- 優化后代碼布局更符合實際執行順序,提高指令緩存效率
3.1.2 實現步驟詳解
1. 生成Link Map文件
// 在Build Settings中設置
OTHER_LDFLAGS = -Wl,-map,$(BUILT_PRODUCTS_DIR)/$(PRODUCT_NAME).linkmap
//或者使用 Xcode默認提供了生成Linkmap的選項
Write Link Map File 設為YES
//這兩種方法選擇其一即可
// 分析Link Map文件結構
# Path: /Users/xxx/Library/Developer/Xcode/DerivedData/xxx/Build/Products/Debug-iphonesimulator/xxx.linkmap
//使用Write Link Map File 時,路徑不同
#Path:/Users/xxx/Library/Developer/Xcode/DerivedData/<YourProject>/Build/Intermediates.noindex/<YourTarget>.build/<Configuration>-<Platform>/<YourTarget>.build/XXX-LinkMap-normal-XXX.txt
# Arch: x86_64
# Object files:
[ 0] linker synthesized
[ 1] /Users/xxx/xxx.o
# Sections:
# Address Size Segment Section
0x100000000 0x00000000 __TEXT __text
0x100000000 0x00000000 __TEXT __stubs
2. 收集函數調用順序
1. 使用Instruments的Time Profiler
在Xcode中選擇 Product -> Profile -> Time Profiler
記錄啟動過程中的函數調用順序
2. 自定義插樁實現
class FunctionTracer {private static var callStack: [String] = []private static let queue = DispatchQueue(label: "com.app.functionTracer")static func traceFunction(_ function: String) {queue.async {callStack.append(function)if callStack.count > 1000 {saveCallStack()}}}private static func saveCallStack() {let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]let filePath = (path as NSString).appendingPathComponent("function_trace.txt")let trace = callStack.joined(separator: "\n")try? trace.write(toFile: filePath, atomically: true, encoding: .utf8)callStack.removeAll()}}
3. 使用LLDB命令收集
在Xcode控制臺輸入:
// (lldb) breakpoint set -n main
// (lldb) breakpoint command add 1
// > bt
// > continue
// > DONE
3. 生成Order文件
// Order文件格式示例
/*
# 啟動關鍵路徑
_main
_UIApplicationMain
_application:didFinishLaunchingWithOptions:# 核心初始化函數
_setupCoreComponents
_initializeNetwork
_setupDatabase# 視圖控制器初始化
_RootViewController.init
_HomeViewController.init
_setupUI# 數據加載
_loadInitialData
_fetchUserProfile
_loadCachedData
*/// 自動生成Order文件的腳本
class OrderFileGenerator {static func generateOrderFile(from trace: [String]) -> String {var orderFile = "# Generated Order File\n\n"// 按調用頻率排序let frequency = Dictionary(grouping: trace, by: { $0 }).mapValues { $0.count }.sorted { $0.value > $1.value }// 生成Order文件內容for (function, _) in frequency {orderFile += "\(function)\n"}return orderFile}
}
4. 應用重排配置
// 在Build Settings中設置ORDER_FILE = $(SRCROOT)/order.txt
3.1.3 函數重排策略
- 優先重排啟動關鍵路徑上的函數
- 將相關功能模塊的代碼放在一起
- 考慮函數調用頻率和依賴關系
- 避免過度重排導致代碼段過大
3.1.4 注意事項
- 重排可能影響調試體驗
- 需要定期驗證重排效果
- 考慮不同設備架構的差異
- 保持代碼的可維護性
3.2 預加載優化
預加載優化通過提前加載資源來提升用戶體驗:
-
后臺預加載策略
class ResourcePreloader {private let preloadQueue = DispatchQueue(label: "com.app.preloadQueue", qos: .utility,attributes: .concurrent)private let semaphore = DispatchSemaphore(value: 3) // 控制并發數func preloadResources() {preloadQueue.async {self.preloadImages()self.preloadData()self.preloadWebViews()}}private func preloadImages() {let imagePaths = ["image1", "image2", "image3"]for path in imagePaths {semaphore.wait()preloadQueue.async {defer { self.semaphore.signal() }// 實現圖片預加載if let image = UIImage(contentsOfFile: path) {ImageCache.shared.cache(image, forKey: path)}}}}private func preloadData() {// 實現數據預加載}private func preloadWebViews() {// 實現WebView預加載} }
-
智能預加載
class SmartPreloader {private let predictionModel = UserBehaviorModel()private let preloadQueue = DispatchQueue(label: "com.app.smartPreload")func predictAndPreload(for user: User) {let predictions = predictionModel.predictNextActions(for: user)for prediction in predictions {switch prediction.type {case .image:preloadImages(for: prediction)case .data:preloadData(for: prediction)case .web:preloadWebContent(for: prediction)}}}private func preloadImages(for prediction: Prediction) {// 實現智能圖片預加載}private func preloadData(for prediction: Prediction) {// 實現智能數據預加載}private func preloadWebContent(for prediction: Prediction) {// 實現智能Web內容預加載} }
-
預加載優化建議
- 根據設備性能和網絡狀況動態調整預加載策略
- 實現預加載優先級機制
- 監控預加載效果,動態調整預加載內容
- 在低電量模式下減少預加載
3.3 啟動圖優化
啟動圖優化對提升用戶體驗至關重要:
-
輕量級啟動圖實現
class LaunchScreenManager {static func setupLightweightLaunchScreen() {let window = UIApplication.shared.windows.firstlet launchView = UIView(frame: window?.bounds ?? .zero)// 使用漸變色背景let gradientLayer = CAGradientLayer()gradientLayer.frame = launchView.boundsgradientLayer.colors = [UIColor.white.cgColor, UIColor.lightGray.cgColor]launchView.layer.addSublayer(gradientLayer)// 添加簡單的品牌標識let logoImageView = UIImageView(image: UIImage(named: "logo"))logoImageView.center = launchView.centerlaunchView.addSubview(logoImageView)window?.addSubview(launchView)// 動畫過渡到主界面UIView.animate(withDuration: 0.3, animations: {launchView.alpha = 0}) { _ inlaunchView.removeFromSuperview()}} }
-
動態啟動圖優化
class DynamicLaunchScreen {static func generateDynamicLaunchScreen() -> UIImage? {let size = UIScreen.main.bounds.sizelet scale = UIScreen.main.scaleUIGraphicsBeginImageContextWithOptions(size, false, scale)guard let context = UIGraphicsGetCurrentContext() else { return nil }// 繪制動態背景drawDynamicBackground(in: context, size: size)// 添加設備特定的元素if UIDevice.current.userInterfaceIdiom == .pad {drawiPadSpecificElements(in: context, size: size)} else {drawiPhoneSpecificElements(in: context, size: size)}let image = UIGraphicsGetImageFromCurrentImageContext()UIGraphicsEndImageContext()return image}private static func drawDynamicBackground(in context: CGContext, size: CGSize) {// 實現動態背景繪制}private static func drawiPadSpecificElements(in context: CGContext, size: CGSize) {// 實現iPad特定元素繪制}private static func drawiPhoneSpecificElements(in context: CGContext, size: CGSize) {// 實現iPhone特定元素繪制} }
-
啟動圖優化建議
- 使用矢量圖形替代位圖
- 根據設備特性優化啟動圖尺寸
- 實現平滑的過渡動畫
- 考慮深色模式適配
四、性能監控與測量
4.1 啟動時間測量
準確的性能測量是優化的基礎:
-
Time Profiler使用
- Time Profiler可以分析函數調用耗時,幫助識別性能瓶頸。
- 使用Instruments的Time Profiler模板,可以獲取詳細的性能數據。
- 分析結果包括CPU使用率、函數調用棧、線程狀態等信息。
-
自定義時間點標記
- 使用高精度計時器記錄關鍵時間點,可以準確測量各個階段的耗時。
- 這種方式可以提供約1ms的測量精度。
class LaunchTimeTracker {private static var eventTimes: [String: TimeInterval] = [:]private static var eventOrder: [String] = []static func markTime(_ eventName: String) {let currentTime = ProcessInfo.processInfo.systemUptimeeventTimes[eventName] = currentTimeeventOrder.append(eventName)}static func printAllEvents() {guard let startTime = eventTimes[eventOrder.first ?? ""] else { return }for eventName in eventOrder {if let eventTime = eventTimes[eventName] {let duration = (eventTime - startTime) * 1000print("\(eventName): \(String(format: "%.2f", duration))ms")}}} }
4.2 內存使用監控
內存使用監控對性能優化至關重要:
-
Allocations工具使用
- Allocations工具可以分析內存分配,幫助檢測內存泄漏。
- 使用Instruments的Allocations模板,可以獲取詳細的內存使用數據。
- 分析結果包括內存分配大小、分配位置、內存泄漏等信息。
-
內存監控實現
- 使用task_info獲取進程的內存使用情況,可以監控內存峰值。
- 這種方式可以提供約1MB的測量精度。
class MemoryMonitor {static func monitorMemoryUsage() {var info = task_vm_info_data_t()var count = mach_msg_type_number_t(MemoryLayout<task_vm_info>.size) / 4let result = withUnsafeMutablePointer(to: &info) { infoPtr ininfoPtr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { intPtr intask_info(mach_task_self_,task_flavor_t(TASK_VM_INFO),intPtr,&count)}}if result == KERN_SUCCESS {let usedMemory = info.phys_footprintprint("Memory usage: \(usedMemory / 1024 / 1024) MB")}} }
五、最佳實踐建議
- 保持簡單:只加載必需數據,延遲非關鍵功能,使用輕量級組件。
- 異步處理:使用GCD/OperationQueue,控制并發數量,注意線程安全。
- 延遲加載:按需加載資源,使用緩存機制,實現預加載策略。
- 監控性能:使用Instruments,添加監控點,定期分析。
- 漸進式加載:先顯示框架,逐步加載數據,優化用戶體驗。
六、總結
iOS啟動優化是一個系統工程,需要從多個維度進行考慮和優化。通過理解啟動流程、合理使用優化技巧,并持續監控性能,我們可以顯著提升應用的啟動速度,為用戶提供更好的使用體驗。
優化原則
- 測量優先:使用工具量化性能,建立性能基準,持續監控改進。
- 漸進優化:從關鍵路徑開始,逐步優化次要部分,避免過度優化。
- 平衡考慮:性能與可維護性,速度與資源占用,用戶體驗與開發效率。
如果覺得本文對你有幫助,歡迎點贊、收藏、關注我,后續會持續分享更多 iOS 優化方案。