1. 程式人生 > IOS開發 >打造完備的 iOS 元件化方案:如何面向介面進行模組解耦?(二)

打造完備的 iOS 元件化方案:如何面向介面進行模組解耦?(二)

繼續上一篇的內容:打造完備的 iOS 元件化方案:如何面向介面進行模組解耦?(一)

功能擴充套件

總結完使用介面進行模組解耦和依賴管理的方法,我們可以進一步對 router 進行擴充套件了。上面使用 makeDestination 建立模組是最基本的功能,使用 router 子類後,我們可以進行許多有用的功能擴充套件,這裡給出一些示範。

自動註冊

編寫 router 程式碼時,需要註冊 router 和 protocol 。在 OC 中可以在 +load 方法中註冊,但是 Swift 裡已經不能使用 +load 方法,而且分散在 +load 中的註冊程式碼也不好管理。BeeHive 中通過巨集定義和__attribute((used,section("__DATA,""BeehiveServices""")))

,把註冊資訊新增到了 mach-O 中的自定義區域,然後在啟動時讀取並自動註冊,可惜這種方式在 Swift 中也無法使用了。

我們可以把註冊程式碼寫在 router 的+registerRoutableDestination方法裡,然後逐個呼叫每個 router 類的+registerRoutableDestination方法即可。還可以更進一步,用 runtime 技術遍歷 mach-O 中的__DATA,__objc_classlist區域的類列表,獲取所有的 router 類,自動呼叫所有的+registerRoutableDestination方法。

把註冊程式碼統一管理之後,如果不想使用自動註冊,也能隨時切換為手動註冊。

// editor 模組的 router
class EditorViewRouter: ZIKViewRouter {
  
    override class func registerRoutableDestination() {
        registerView(EditorViewController.self)
        register(RoutableView<EditorViewProtocol>())
    }

}

複製程式碼

<details><summary>Objective-C Sample</summary>

@interface EditorViewRouter : ZIKViewRouter
@end

@implementation EditorViewRouter

+ (void)registerRoutableDestination {
    [self registerView:[EditorViewController class]];
    [self registerViewProtocol:ZIKRoutable(EditorViewProtocol)];
}

@end

複製程式碼

</details>

封裝介面跳轉

iOS 中模組間耦合的原因之一,就是介面跳轉的邏輯是通過 UIViewController 進行的,跳轉功能被限制在了 view controller 上,導致資料流常常都繞不開 view 層。要想更好地管理跳轉邏輯,就需要進行封裝。

封裝介面跳轉可以遮蔽 UIKit 的細節,此時介面跳轉的程式碼就可以放在非 view 層(例如 presenter、view model、interactor、service),並且能夠跨平臺,也能輕易地通過配置切換跳轉方式。

如果是普通的模組,就用ZIKServiceRouter,而如果是介面模組,例如 UIViewControllerUIView,就可以用ZIKViewRouter,在其中封裝了介面跳轉功能。

封裝介面跳轉後,使用方式如下:

class TestViewController: UIViewController {

    //直接跳轉到 editor 介面
    func showEditor() {
        Router.perform(to: RoutableView<EditorViewProtocol>(),path: .push(from: self))
    }
  
    //跳轉到 editor 介面,跳轉前用 protocol 配置介面
    func prepareAndShowEditor() {
        Router.perform(
            to: RoutableView<EditorViewProtocol>(),path: .push(from: self),preparation: { destination in
                // 跳轉前進行配置
                // destination 自動推斷為 EditorViewProtocol
            })
    }
}

複製程式碼

<details><summary>Objective-C Sample</summary>

@implementation TestViewController

- (void)showEditor {
    //直接跳轉到 editor 介面
    [ZIKRouterToView(EditorViewProtocol) performPath:ZIKViewRoutePath.pushFrom(self)];
}

- (void)prepareAndShowEditor {
    //跳轉到 editor 介面,跳轉前用 protocol 配置介面
    [ZIKRouterToView(EditorViewProtocol) 
        performPath:ZIKViewRoutePath.pushFrom(self)
        preparation:^(id<EditorViewProtocol> destination) {
            // 跳轉前進行配置
            // destination 自動推斷為 EditorViewProtocol
    }];
}

@end

複製程式碼

</details>

可以用 ViewRoutePath 一鍵切換不同的跳轉方式:

enum ViewRoutePath {
    case push(from: UIViewController)
    case presentModally(from: UIViewController)
    case presentAsPopover(from: UIViewController,configure: ZIKViewRoutePopoverConfigure)
    case performSegue(from: UIViewController,identifier: String,sender: Any?)
    case show(from: UIViewController)
    case showDetail(from: UIViewController)
    case addAsChildViewController(from: UIViewController,addingChildViewHandler: (UIViewController,@escaping () -> Void) -> Void)
    case addAsSubview(from: UIView)
    case custom(from: ZIKViewRouteSource?)
    case makeDestination
    case extensible(path: ZIKViewRoutePath)
}

複製程式碼

而且在介面跳轉後,還可以根據跳轉時的跳轉方式,一鍵回退介面,無需再手動區分 dismiss、pop 等各種情況:

class TestViewController: UIViewController {
    var router: DestinationViewRouter<EditorViewProtocol>?

    func showEditor() {
        // 持有 router
        router = Router.perform(to: RoutableView<EditorViewProtocol>(),path: .push(from: self))
    }
    
    // Router 會對 editor view controller 執行 pop 操作,移除介面
    func removeEditor() {
        guard let router = router,router.canRemove else {
            return
        }
        router.removeRoute()
        router = nil
    }
}

複製程式碼

<details><summary>Objective-C Sample</summary>

@interface TestViewController()
@property (nonatomic,strong) ZIKDestinationViewRouter(id<EditorViewProtocol>) *router;
@end
@implementation TestViewController

- (void)showEditor {
    // 持有 router
    self.router = [ZIKRouterToView(EditorViewProtocol) performPath:ZIKViewRoutePath.pushFrom(self)];
}

// Router 會對 editor view controller 執行 pop 操作,移除介面
- (void)removeEditor {
    if (![self.router canRemove]) {
        return;
    }
    [self.router removeRoute];
    self.router = nil;
}

@end

複製程式碼

</details>

自定義跳轉

有些介面的跳轉方式很特殊,例如 tabbar 上的介面,需要通過切換 tabbar item 來進行。也有的介面有自定義的跳轉動畫,此時可以在 router 子類中重寫對應方法,進行自定義跳轉。

class EditorViewRouter: ZIKViewRouter<EditorViewController,ViewRouteConfig> {

    override func destination(with configuration: ViewRouteConfig) -> Any? {
        return EditorViewController()
    }

    override func canPerformCustomRoute() -> Bool {
        return true
    }
    
    override func performCustomRoute(onDestination destination: EditorViewController,fromSource source: Any?,configuration: ViewRouteConfig) {
        beginPerformRoute()
        // 自定義跳轉
        CustomAnimator.transition(from: source,to: destination) {
            self.endPerformRouteWithSuccess()
        }
    }
    
    override func canRemoveCustomRoute() -> Bool {
        return true
    }
    
    override func removeCustomRoute(onDestination destination: EditorViewController,removeConfiguration: ViewRemoveConfig,configuration: ViewRouteConfig) {
        beginRemoveRoute(fromSource: source)
        // 移除自定義跳轉
        CustomAnimator.dismiss(destination) {
            self.endRemoveRouteWithSuccess(onDestination: destination,fromSource: source)
        }
    }
    
    override class func supportedRouteTypes() -> ZIKViewRouteTypeMask {
        return [.custom,.viewControllerDefault]
    }
}

複製程式碼

<details><summary>Objective-C Sample</summary>

@interface EditorViewRouter : ZIKViewRouter
@end

@implementation EditorViewRouter

- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
    return [[EditorViewController alloc] init];
}

- (BOOL)canPerformCustomRoute {
    return YES;
}

- (void)performCustomRouteOnDestination:(id)destination fromSource:(UIViewController *)source configuration:(ZIKViewRouteConfiguration *)configuration {
    [self beginPerformRoute];
    // 自定義跳轉
    [CustomAnimator transitionFrom:source to:destination completion:^{
        [self endPerformRouteWithSuccess];
    }];
}

- (BOOL)canRemoveCustomRoute {
    return YES;
}

- (void)removeCustomRouteOnDestination:(id)destination fromSource:(UIViewController *)source removeConfiguration:(ZIKViewRemoveConfiguration *)removeConfiguration configuration:(__kindof ZIKViewRouteConfiguration *)configuration {
    [self beginRemoveRouteFromSource:source];
    // 移除自定義跳轉
    [CustomAnimator dismiss:destination completion:^{
        [self endRemoveRouteWithSuccessOnDestination:destination fromSource:source];
    }];
}

+ (ZIKViewRouteTypeMask)supportedRouteTypes {
    return ZIKViewRouteTypeMaskCustom|ZIKViewRouteTypeMaskViewControllerDefault;
}

@end

複製程式碼

</details>

支援 storyboard

很多專案使用了 storyboard,在進行模組化時,肯定不能要求所有使用 storyboard 的模組都改為使用程式碼。因此我們可以 hook 一些 storyboard 相關的方法,例如-prepareSegue:sender:,在其中呼叫prepareDestination:configuring:即可。

URL 路由

雖然之前列出了 URL 路由的許多缺點,但是如果你的模組需要從 h5 介面呼叫,例如電商 app 需要實現跨平臺的動態路由規則,那麼 URL 路由就是最佳的方案。

但是我們並不想為了實現 URL 路由,使用另一套框架再重新封裝一次模組。只需要在 router 上擴充套件 URL 路由的功能,即可同時用介面和 URL 管理模組。

你可以給 router 註冊 url:

class EditorViewRouter: ZIKViewRouter<EditorViewProtocol,ViewRouteConfig> {
    override class func registerRoutableDestination() {
        // 註冊 url
        registerURLPattern("app://editor/:title")
    }
}

複製程式碼

<details><summary>Objective-C Sample</summary>

@implementation EditorViewRouter

+ (void)registerRoutableDestination {
    // 註冊 url
    [self registerURLPattern:@"app://editor/:title"];
}

@end

複製程式碼

</details>

之後就可以用相應的 url 獲取 router:

ZIKAnyViewRouter.performURL("app://editor/test_note",path: .push(from: self))

複製程式碼

<details><summary>Objective-C Sample</summary>

[ZIKAnyViewRouter performURL:@"app://editor/test_note" path:ZIKViewRoutePath.pushFrom(self)];

複製程式碼

</details>

以及處理 URL Scheme:

public func application(_ app: UIApplication,open url: URL,options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
    let urlString = url.absoluteString
    if let _ = ZIKAnyViewRouter.performURL(urlString,fromSource: self.rootViewController) {
        return true
    } else if let _ = ZIKAnyServiceRouter.performURL(urlString) {
        return true
    }
    return false
}

複製程式碼

<details><summary>Objective-C Sample</summary>

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
    if ([ZIKAnyViewRouter performURL:urlString fromSource:self.rootViewController]) {
        return YES;
    } else if ([ZIKAnyServiceRouter performURL:urlString]) {
        return YES;
    }
    return NO;
}

複製程式碼

</details>

每個 router 子類還能各自對 url 進行進一步處理,例如處理 url 中的引數、通過 url 執行對應方法、執行路由後傳送返回值給呼叫者等。

每個專案對 URL 路由的需求都不一樣,基於 ZIKRouter 強大的可擴充套件性,你也可以按照專案需求實現自己的 URL 路由規則。

用 router 物件代替 router 子類

除了建立 router 子類,也可以使用通用的 router 例項物件,在每個物件的 block 屬性中提供和 router 子類一樣的功能,因此不必擔心類過多的問題。原理就和用泛型 configuration 代替 configuration 子類一樣。

ZIKViewRoute 物件通過 block 屬性實現子類重寫的效果,程式碼可以用鏈式呼叫:

ZIKViewRoute<EditorViewController,ViewRouteConfig>
.make(withDestination: EditorViewController.self,makeDestination: ({ (config,router) -> EditorViewController? in
    return EditorViewController()
}))
.prepareDestination({ (destination,config,router) in

}).didFinishPrepareDestination({ (destination,router) in

})
.register(RoutableView<EditorViewProtocol>())

複製程式碼

<details><summary>Objective-C Sample</summary>

[ZIKDestinationViewRoute(id<EditorViewProtocol>) 
 makeRouteWithDestination:[ZIKInfoViewController class] 
 makeDestination:^id<EditorViewProtocol> _Nullable(ZIKViewRouteConfig *config,ZIKRouter *router) {
    return [[EditorViewController alloc] init];
}]
.prepareDestination(^(id<EditorViewProtocol> destination,ZIKViewRouteConfig *config,ZIKViewRouter *router) {

})
.didFinishPrepareDestination(^(id<EditorViewProtocol> destination,ZIKViewRouter *router) {

})
.registerDestinationProtocol(ZIKRoutable(EditorViewProtocol));

複製程式碼

</details>

簡化 router 實現

基於 ZIKViewRoute 物件實現的 router,可以進一步簡化 router 的實現程式碼。

如果你的類很簡單,並不需要用到 router 子類,直接一行程式碼註冊類即可:

ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(),forMakingView: EditorViewController.self)

複製程式碼

<details><summary>Objective-C Sample</summary>

[ZIKViewRouter registerViewProtocol:ZIKRoutable(EditorViewProtocol) forMakingView:[EditorViewController class]];

複製程式碼

</details>

或者用 block 自定義建立物件的方式:

ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(),forMakingView: EditorViewController.self) { (config,router) -> EditorViewProtocol? in
                     return EditorViewController()
        }


複製程式碼

<details><summary>Objective-C Sample</summary>

[ZIKViewRouter
    registerViewProtocol:ZIKRoutable(EditorViewProtocol)
    forMakingView:[EditorViewController class]
    making:^id _Nullable(ZIKViewRouteConfiguration *config,ZIKViewRouter *router) {
        return [[EditorViewController alloc] init];
 }];

複製程式碼

</details>

或者指定用 C 函式建立物件:

function makeEditorViewController(config: ViewRouteConfig) -> EditorViewController? {
    return EditorViewController()
}

ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(),forMakingView: EditorViewController.self,making: makeEditorViewController)

複製程式碼

<details><summary>Objective-C Sample</summary>

id<EditorViewController> makeEditorViewController(ZIKViewRouteConfiguration *config) {
    return [[EditorViewController alloc] init];
}

[ZIKViewRouter
    registerViewProtocol:ZIKRoutable(EditorViewProtocol)
    forMakingView:[EditorViewController class]
    factory:makeEditorViewController];

複製程式碼

</details>

事件處理

有時候模組需要處理一些系統事件或者 app 的自定義事件,此時可以讓 router 子類實現,再進行遍歷分發。

class SomeServiceRouter: ZIKServiceRouter {
    @objc class func applicationDidEnterBackground(_ application: UIApplication) {
        // handle applicationDidEnterBackground event
    }
}

複製程式碼
class AppDelegate: NSObject,NSApplicationDelegate {

    func applicationDidEnterBackground(_ application: UIApplication) {
        
        Router.enumerateAllViewRouters { (routerType) in
            if routerType.responds(to: #selector(applicationDidEnterBackground(_:))) {
                routerType.perform(#selector(applicationDidEnterBackground(_:)),with: application)
            }
        }
        Router.enumerateAllServiceRouters { (routerType) in
            if routerType.responds(to: #selector(applicationDidEnterBackground(_:))) {
                routerType.perform(#selector(applicationDidEnterBackground(_:)),with: application)
            }
        }
    }

}

複製程式碼

<details><summary>Objective-C Sample</summary>

@interface SomeServiceRouter : ZIKServiceRouter
@end
@implementation SomeServiceRouter

+ (void)applicationDidEnterBackground:(UIApplication *)application {
    // handle applicationDidEnterBackground event
}

@end

複製程式碼
@interface AppDelegate ()
@end
@implementation AppDelegate

- (void)applicationDidEnterBackground:(UIApplication *)application {
    
    [ZIKAnyViewRouter enumerateAllViewRouters:^(Class routerClass) {
        if ([routerClass respondsToSelector:@selector(applicationDidEnterBackground:)]) {
            [routerClass applicationDidEnterBackground:application];
        }
    }];
    [ZIKAnyServiceRouter enumerateAllServiceRouters:^(Class routerClass) {
        if ([routerClass respondsToSelector:@selector(applicationDidEnterBackground:)]) {
            [routerClass applicationDidEnterBackground:application];
        }
    }];
}

@end

複製程式碼

</details>

單元測試

藉助於使用介面管理依賴的方案,我們在對模組進行單元測試時,可以自由配置 mock 依賴,而且無需 hook 模組內部的程式碼。

例如這樣一個依賴於網路模組的登陸模組:

// 登入模組
class LoginService {

    func login(account: String,password: String,completion: (Result<LoginError>) -> Void) {
        // 內部使用 RequiredNetServiceInput 進行網路訪問
        let netService = Router.makeDestination(to: RoutableService<RequiredNetServiceInput
        >())
        let request = makeLoginRequest(account: account,password: password)
        netService?.POST(request: request,completion: completion)
    }
}

// 宣告依賴
extension RoutableService where Protocol == RequiredNetServiceInput {
    init() {}
}

複製程式碼

<details><summary>Objective-C Sample</summary>

// 登入模組
@interface LoginService : NSObject
@end
@implementation LoginService

- (void)loginWithAccount:(NSString *)account password:(NSString *)password  completion:(void(^)(Result *result))completion {
    // 內部使用 RequiredNetServiceInput 進行網路訪問
    id<RequiredNetServiceInput> netService = [ZIKRouterToService(RequiredNetServiceInput) makeDestination];
    Request *request = makeLoginRequest(account,password);
    [netService POSTRequest:request completion: completion];
}

@end
  
// 宣告依賴
@protocol RequiredNetServiceInput <ZIKServiceRoutable>
- (void)POSTRequest:(Request *)request completion:(void(^)(Result *result))completion;
@end

複製程式碼

</details>

在編寫單元測試時,不需要引入真實的網路模組,可以提供一個自定義的 mock 網路模組:

class MockNetService: RequiredNetServiceInput {
    func POST(request: Request,completion: (Result<NetError>) {
        completion(.success)
    }
}

複製程式碼
// 註冊 mock 依賴
ZIKAnyServiceRouter.register(RoutableService<RequiredNetServiceInput>(),forMakingService: MockNetService.self) { (config,router) -> EditorViewProtocol? in
                     return MockNetService()
        }

複製程式碼

<details><summary>Objective-C Sample</summary>

@interface MockNetService : NSObject <RequiredNetServiceInput>
@end
@implementation MockNetService

- (void)POSTRequest:(Request *)request completion:(void(^)(Result *result))completion {
    completion([Result success]);
}
  
@end

複製程式碼
// 註冊 mock 依賴
[ZIKServiceRouter registerServiceProtocol:ZIKRoutable(EditorViewInput) forMakingService:[MockNetService class]];

複製程式碼

</details>

對於那些沒有介面互動的外部依賴,例如只是簡單的跳轉到對應介面,則只需註冊一個空白的 proxy。

單元測試程式碼:

class LoginServiceTests: XCTestCase {
    
    func testLoginSuccess() {
        let expectation = expectation(description: "end login")
        
        let loginService = LoginService()
        loginService.login(account: "account",password: "pwd") { result in
            expectation.fulfill()
        }
        
        waitForExpectations(timeout: 5,handler: { if let error = $0 {print(error)}})
    }
    
}

複製程式碼

<details><summary>Objective-C Sample</summary>

@interface LoginServiceTests : XCTestCase
@end
@implementation LoginServiceTests

- (void)testLoginSuccess {
    XCTestExpectation *expectation = [self expectationWithDescription:@"end login"];
    
    [[LoginService new] loginWithAccount:@"" password:@"" completion:^(Result *result) {
        [expectation fulfill];
    }];
    
    [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) {
        !error? : NSLog(@"%@",error);
    }];
}
@end

複製程式碼

</details>

使用介面管理依賴,可以更容易 mock,剝除外部依賴對測試的影響,讓單元測試更穩定。

介面版本管理

使用介面管理模組時,還有一個問題需要注意。介面是會隨著模組更新而變化的,這個介面已經被很多外部使用了,要如何減少介面變化產生的影響?

此時需要區分新介面和舊介面,區分版本,推出新介面的同時,保留舊介面,並將舊介面標記為廢棄。這樣使用者就可以暫時使用舊介面,漸進式地修改程式碼。

這部分可以參考 Swift 和 OC 中的版本管理巨集。

介面廢棄,可以暫時使用,建議儘快使用新介面代替:

// Swift
@available(iOS,deprecated: 8.0,message: "Use new interface instead")

複製程式碼
// Objective-C
API_DEPRECATED_WITH_REPLACEMENT("performPath:configuring:",ios(7.0,7.0));

複製程式碼

介面已經無效:

// Swift
@available(iOS,unavailable)

複製程式碼
// Objective-C
NS_UNAVAILABLE

複製程式碼

最終形態

最後,一個 router 的最終形態就是下面這樣:

// editor 模組的 router
class EditorViewRouter: ZIKViewRouter<EditorViewController,ViewRouteConfig> {

    override class func registerRoutableDestination() {
        registerView(EditorViewController.self)
        register(RoutableView<EditorViewProtocol>())
        registerURLPattern("app://editor/:title")
    }

    override func processUserInfo(_ userInfo: [AnyHashable : Any] = [:],from url: URL) {
        let title = userInfo["title"]
        // 處理 url 中的引數
    }

    // 子類重寫,建立模組
    override func destination(with configuration: ViewRouteConfig) -> Any? {
        let destination = EditorViewController()
        return destination
    }

    // 配置模組,注入靜態依賴
    override func prepareDestination(_ destination: EditorViewController,configuration: ViewRouteConfig) {
        // 注入 service 依賴
        destination.storageService = Router.makeDestination(to: RoutableService<EditorStorageServiceInput>())
        // 其他配置
        // 處理來自 url 的引數
        if let title = configuration.userInfo["title"] as? String {
            destination.title = title
        } else {
            destination.title = "預設標題"
        }        
    }
  
    // 事件處理
    @objc class func applicationDidEnterBackground(_ application: UIApplication) {
        // handle applicationDidEnterBackground event
    }
}

複製程式碼

<details><summary>Objective-C Sample</summary>

// editor 模組的 router
@interface EditorViewRouter : ZIKViewRouter
@end

@implementation EditorViewRouter

+ (void)registerRoutableDestination {
    [self registerView:[EditorViewController class]];
    [self registerViewProtocol:ZIKRoutable(EditorViewProtocol)];
    [self registerURLPattern:@"app://editor/:title"];
}

- (void)processUserInfo:(NSDictionary *)userInfo fromURL:(NSURL *)url {
    NSString *title = userInfo[@"title"];
    // 處理 url 中的引數
}

// 子類重寫,建立模組
- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
    EditorViewController *destination = [[EditorViewController alloc] init];
    return destination;
}

// 配置模組,注入靜態依賴
- (void)prepareDestination:(EditorViewController *)destination configuration:(ZIKViewRouteConfiguration *)configuration {
    // 注入 service 依賴
    destination.storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination];
    // 其他配置
    // 處理來自 url 的引數
    NSString *title = configuration.userInfo[@"title"];
    if (title) {
        destination.title = title;
    } else {
        destination.title = @"預設標題";
    }
}

// 事件處理
+ (void)applicationDidEnterBackground:(UIApplication *)application {
    // handle applicationDidEnterBackground event
}

@end

複製程式碼

</details>

基於介面進行解耦的優勢

我們可以看到基於介面管理模組的優勢:

  • 依賴編譯檢查,實現嚴格的型別安全
  • 依賴編譯檢查,減少重構時的成本
  • 通過介面明確宣告模組所需的依賴,允許外部進行依賴注入
  • 保持動態特性的同時,進行路由檢查,避免使用不存在的路由模組
  • 利用介面,區分 required protocol 和 provided protocol,進行明確的模組適配,實現徹底解耦

回過頭看之前的 8 個解耦指標,ZIKRouter 已經完全滿足。而 router 提供的多種模組管理方式(makeDestination、prepareDestination、依賴注入、頁面跳轉、storyboard 支援),能夠覆蓋大多數現有的場景,從而實現漸進式的模組化,減輕重構現有程式碼的成本。

書籍目錄——獲取地址加小編微信拉你進iOS開發群:17512010526


作者:黑超熊貓zuik

來源:www.jianshu.com/p/3aab83626…