美文网首页ios runtime专题iOS Developerswfit收集
从runtime组件化方案聊到Swift与OC混编

从runtime组件化方案聊到Swift与OC混编

作者: Mr杰杰 | 来源:发表于2017-05-14 13:08 被阅读168次

    概述

        利用runtime特性实现iOS项目的组件化开发,是由@casatwy大神提出来的,在他的博客中具体介绍了该方案的可行性与开发流程,并较之蘑菇街的组件化方案做了比对,今年开始公司部门内部开始采用runtime组件化方案来对项目进行重构,笔者虽然没有参与,但是在听过几位大神同事的分享之后,加之对Swift的学习兴趣,将原来纯Objective-C语言的实现“翻译”为Swift与OC混编,甚至是纯Swift的实现。在整个“翻译”过程中,笔者使用Swift逐步替换runtime组件化方案的每一个步骤,并且为了实现组件化方案的最终形态,将每个组件上传到CocoaPods库,通过pod install就可以随时添加/删除组件,且不会影响程序的实际运行。在这个过程中,可以说是跋山涉水,踩坑无数。在此,跟各位看官聊一聊Swift与OC混编,而runtime的学习不在文本的讨论范围内,只讨论具体的应用。


    runtime组件化方案

        Objective-C是一门动态语言,runtime是其最神奇的特性之一,只要一个OC类继承自NSObject,就可以在运行时被调用或修改,runtime组件化方案的灵感来来源于此。runtime组件化方案基于Mediator和Target-Action模式,下面用一个案例来“举个栗子”。一个项目中有两个业务A和B,原项目代码中A和B相互调用,耦合性高,为了达到解耦的目的,引入中间件Mediator,同时为了保证业务A或业务B不与中间件Mediator产生耦合,只允许由Mediator单向调用业务A或业务B,业务A和业务B没有调用关系。有一天因业务调整需要撤下业务B,假如是原项目结构,业务A势必会受到牵连,但是在新的项目结构下,撤下业务B只会导致Mediator调用业务B的返回结果全为空,不会影响其他业务运行。Objective-C的函数调用采用的是运行时的消息发送机制,向一个对象发送消息时,runtime库会该对象所属的类(Target),然后在该类中的方法列表以及其父类方法列表中寻找方法(Action)运行。且Objective-C支持对nil对象发送消息,返回结果也为nil,不会引发崩溃。综合Mediator和Target-Action的优势,利用runtime来实现项目组件化的方案也便油然而生。


    Swift与Objective-C混编

        Objective-C的runtime特性完美契合了这种组件化方案,那么问题来了,Swift可以做到吗?在学习Swift语言之初,我了解到的都是Swift语言的属性和函数在编译期就已经确定下来,Swift是一门强安全性语言,不再支持runtime这种动态性诸如此类的内容。的确,编译期的确定性在安全性上要优于运行时的动态性,Swift也是这么做的。但是Objective-C作为C语言的一种延伸,已经在历史的长河中见证了其优越性,也根深蒂固到每个项目当中,Swift即使想推翻重来,也不得不给这个老大哥一个面子。为了使得Swift与Objective-C能够相互兼容,苹果公司也是使出了十八般武艺,其中一项便是允许Swift类继承NSObject。

        先来看一下"纯"Swift类的声明:

    public class SwiftClass1 {

    }

        SwiftClass1的“纯”在于它没有继承任何父类,属于Swift原生类。Swift原生类是无法使用runtime打印属性和函数的信息。

        再来看一个“不纯”Swift类的声明:

    public class SwiftClass2: NSObject {

    }

        SwiftClass2的“不纯”在与它继承自NSObject。继承自NSObject的类依然具有Objective-C的动态特性,可以通过runtime获取属性和函数,运行时方法依赖TypeEncoding,但是依然无法获取Swift特有的属性(例如元组tuple)。

        假如你的项目是Swift与Objective-C混编的,那么千万别使用Swift原生类和Swift特有的属性,你会发现死活都永不了。可这是为什么呢?这地从两个神奇的文件说起。在OC环境的项目下新建一个Swift文件,系统会提示创建一个名为ProductName-Bridging-Header.h的桥接文件,ProductName是项目名称,创建成功后会显示项目目录下,同时系统还会隐式地创建一个名为ProductName-Swift.h,ProductName也是项目名称,之所以说隐式,是因为不存在于项目目录下,只有在导入该头文件后点击,才能进入查看里面的内容。ProductName-Bridging-Header.h文件用于导入OC的头文件,供Swift调用:

    #import "XXX.h"  // 在ProductName-Bridging-Header.h导入

        ProductName-Swift.h文件用于将Swift代码转化为OC类型的头文件,所有Swift代码都由这个文件统一管理,在OC文件中导入这个.h文件,就可以调用Swift:

    #import "ProductName-Bridging-Header.h"  // 在OC文件中导入

        Swift原生类或特有属性无法被编译进ProductName-Swift.h中,也就无法被OC代码调用。只有将Swift类继承NSObject,才可以被正常使用。


    Swift“翻译”之路

        正是由于继承自NSObject的Swift类可以在runtime环境中被调用,也就催生了我“翻译”runtime组件化方案的念头。runtime组件化方案在整体结构上可以分为四层:Mediator,Mediator扩展,Target-Action,Module。Module是业务层代码,实现一个具体的功能模块;Target-Action封装了Module的代码,提供接口给Mediator调用,在运行时runtime需要选择指定的Target(类名)和Action(函数名)来执行,Target-Action需要导入Module的头文件,两者有耦合性;Mediator扩展是Mediator是扩展(分类),负责将Target和Action传递给Mediator,Mediator扩展和Target-Action一一对应,通过字符串调用Target-Action,提供接口给外部调用,两者无耦合性;Mediator在runtime环境下寻找并实现具体的方法,整个项目的Target-Action方法最终都由该类执行,调用失败将返回nil。使用runtime传递参数时需要注意,runtime方法只接受字典类型的参数,所以Mediator扩展需要将参数转化为字典传递给Mediator,在Target-Action中再将字典转化为参数传递给Module。

        一个项目需要新建一个组件化模块,推荐的代码实现逻辑是Mediator扩展->Module->Target-Action,Mediator是一个固定的文件,不需要重写。接着是Mediator扩展,提供接口给外部调用,即使调用失败,返回结果nil,不会对项目产生影响。然后是Module,实现具体的业务逻辑,最后是Target-Action,封装Module的业务逻辑代码。

    以下是我在“翻译”过程中总结出来的经验教训:


    1.在Objective-C函数中返回一个基本类型或对象类型的值,在Swift中需要转化为对应的Optional类型值,Optional在Swift世界中是一个极为重要的概念。Objective-C中Foundation对象,允许出现nil值,在Swift中要转换为Optional对象。

    Optional

    (1)nil。在Objective-C中,nil是一个指向空对象的指针,只能赋值给对象。而在Swift中,nil不再是一个指针,是一个确定的值,表示缺失值。任何可选类型都可以赋值为nil(对象类型和基础类型)。

    (2)optional的定义。optional(可选类型)的官方定义:A type that can represent either a wrapped value or nil, the absence of a value.optional的表示方法很简单,就是在变量类型后面加上一个?。定义为non-optional类型的变量必须在定义的时候或在init方法中初始化,否则会发生编译错误。

    (3)optional的由来。苹果希望通过引入optional类型来提高Swift语言的安全性,期望在编译时及时检测到问题,而不是留到运行时才出现。用OC编写的代码会经常出现编译通过,但是运行崩溃的情况,因为OC是一门运行时语言,在编译期的安全检查不严格。为了尽量的减少运行时Crash的情况,就要在编译期间对代码进行严格的检查,optional的引入可以很好的完成这个任务。optional源码地址

    (4)optional的使用。一个optional类型的值可以看做是一个盒子,可以将任何东西放进这个盒子(对象类型或基础类型),也可以不放(nil),这个过程叫做包装(wrap)。一旦放进去以后,如果想要使用这个东西,必须先打开盒子,打开的过程称为拆包(unwrap),optional类型的值只有拆包后才可以使用,拆包方法有两种:可选绑定(if let .... {})和隐式拆包(在optional类型的值后面加!)。


    2.组件化方案中,runtime方法调用Target-Action返回的结果类型为id。id类型是Objective-C中最常见的一种对象类型,但不能表示基本类型,需要借助NSNumber转化。而在Swift世界中,id类型早已是过眼云烟,但苹果为了兼容Objective-C,使用AnyObject类型来代替id,同时了还增加了Any类型。

    id,AnyObject,Any,AnyClass

    (1)NSObject是Objective-C中绝大数类的基类,id是一个指针,可以指向任何一个继承自NSObject类的对象,编译器不会对向声明为 id 的对象进行类型检查,这体现了Objective-C的动态特性,但是id不能指向基本类型。

    (2)Swift是一门强安全性的语言,在创建一个对象的时候就应该声明该对象的类型,系统也会根据初始值自动判定对象类型,Swift不提倡一个数组中可以存放多个类型的对象的做法。但是为了兼容Objective-C,为了兼容id类型,AnyObject便应运而生。官方对AnyObject的解释是:“The protocol to which all classes implicitly conform”,意思就是所有的类都隐式遵守了这个协议,头文件中的代码如下:@objc public protocol AnyObject {}。在 Swift 中,使用@objc修饰的类型,可以直接供 Objective-C 调用。虽然Swift使用AnyObject来代替id,但还是有区别的。在Swift环境下,编译器不仅不会对向声明为AnyObject的对象进行类型检查,而且返回AnyObject的对象都是Optional类型。在Objective-C中,id是可以指向nil的,假如某个方法的返回值是id,那么在Swift中的返回值就是AnyObject?,在使用时建议通过可选绑定来获得具体的对象。

    (3)AnyObject可以代表任意class类型的实例,而Any可以代表任意类型,包括方法(基础类型,结构体和函数等)类型,而不仅限于class类型。AnyClass在头文件中的代码如下:public typealias AnyClass = AnyObject.Type。AnyObject.Type的返回值是一个元类型(Meta)。例如ClassA.Type表示ClassA这个类的类型,可以声明一个元类型来表示ClassA这个类型本身。在Swift中,.self可以放在类后面来取得该类的类型(let classType:ClassA.Type = ClassA.self),也可以将这个变量声明为AnyClass类型来表示该类型(let classType:AnyClass = ClassA.self)。说白了AnyClass表示的意思就是任意类型。

    // --------------------AnyObject跟Any的差别--------------------

    letstringValue:String="string"// String为Swift的struct类型

    vararrayValue:Array = [1,2,3]// Array为Swift的struct类型

    vardictionaryValue:Dictionary = ["key1":"value1","key2":"value2"]//Dictionary为Swift的struct类型

    // Array不接受多类型数据,但是将数组中元素的类型声明为Any,可以解决这个问题

    letmixedAnyArray: [Any] = [stringValue,arrayValue,dictionaryValue]

    // AnyObject只能表示任意Class类型的实例,使用下面的数组会报错

    //let mixedAnyObjectArray: [AnyObject] = [stringValue, arrayValue, dictionaryValue]

    (4)在Swift中,假如函数不需要返回值,则可以忽略返回值的定义。但是严格上来说,虽然没有返回值被定义,swift函数依然会返回值。没有定义返回类型的函数会返回一 个特殊的 Void 值,它其实是一个空的元组(tuple),没有任何元素,也可以写成()。所以,-> Void,->(),不写,这三者是等价的。在Objective-C中没有找到具体的官方解释,只在某本书上找到一段话来解释没有返回值的情况:”如果方法不返回值,可用void类型声明“,至于void类型表示什么,这块知识我是空白缺失的。在使用runtime进行组件化开发时,Objective-C的perform函数返回值为id类型的对象,即使Target-Action类中的函数没有返回值,也必须在函数声明时返回id类型的对象,在函数实现的末尾添加return nil。Swift的perform函数返回值为AnyObject?类型的对象,假如Target_Action类中的函数没有返回值,可以不返回值。


    3.在使用Objective-C进行项目开发时,我们往往会忽略访问控制符这个看似无足轻重的关键词。通常通过.h和.m文件来控制,想开放给外部使用的属性和方法,在.h文件中声明,否则就是在.m文件中声明并实现。但在Swift世界中,访问控制符是一个非常重要的概念,而且推荐在使用过程中手动管理。

    访问控制符

    Objective-C提供了四类访问控制符,权限从小到大依次是:

    @private(当前类访问权限):成员只能在当前类内部可以访问,在类实现部分定义的成员变量相当于默认使用了这种访问权限。

    @package(同映像访问权限):成员可以在当前类或和当前类实现的同一映像中使用。同一映像就是编译后生成的同一框架或同一个执行文件。

    @protected(子类访问权限,默认访问级别):成员可以在当前类和当前类的子类中访问。在类接口部分定义的成员变量默认是这种访问权限。

    @public(公共访问权限):成员可以在任意地方访问。

    在3.0之前,Swift提供了三类访问控制符,Swift3.0开始又新增了两类,权限从小到大依次是:

    1.private(当前类访问权限):private访问级别所修饰的属性或方法只能在当前类访问。

    2.fileprivate(当前文件访问权限,Swift3.0新增):fileprivate访问级别所修饰的属性或者方法在当前的Swift源文件里可以访问。

    3.internal(默认访问级别,internal修饰符可写可不写):

    1)internal访问级别所修饰的属性或方法在文件所在的整个module中都可以访问。

    2)如果是框架或者库代码,则在整个框架内部都可以访问,框架由外部代码所引用时,则不可以访问。

    3)如果是App代码,也是在整个App代码,也是在整个App内部可以访问。

    4.public(公开访问权限):可以在任何地方访问。但其他module中不可以被重写和继承,而在module内可以被重写和继承。

    5.open(完全公开访问权限,Swift3.0新增):可以在任何地方访问,包括重写和继承。

    命名空间

        在Objective-C中没有命名空间的概念,为了避免冲突,Objective-C的类名通常会加上两三个字母的作为前缀,例如Apple保留的NS和UI前缀,系统框架Core Foundation的CF前缀,第三方类库AFNetworking的AF前缀等。一个Swift项目其实是由一个或多个module组成的,一个module代表一个命名空间。例如一个Target就是一个module,一个.framwork文件也是一个module,每个module相互独立,即使两个不同的module中存在同名的属性或函数,也不会产生冲突,因为在Swift中,一个属性或函数的实际访问方式是命名空间+属性或函数(用.符号连接)。获取命名空间的方法参考地址

        由于采用组件化的方法开发项目,所以每个组件的最终形态是一个私有Pod,由CocoaPods统一管理,在需要使用某个module时,通过pod install安装使用。通过pod来引入module,必然会引发访问控制权限的问题,哪些属性或函数是允许其他module访问,哪些不允许,都需要合理控制。将Objective-C代码封装成module,即使代码中没有访问控制符,只要在.h文件中添加供外部访问的属性或函数,就可以提供给其他module访问。但是将Swift代码封装成module,代码中没有添加访问控制符(默认是internal),则其他module是无法正常访问的,需要显式声明为public或open。


    总结

        笔者文章功底有限,洋洋洒洒的几千字几乎清一色全是文字,曾经想过画一些图或引用几段代码来配合文字更好的解释说明,但时间精力有限,请各位看官链接。能坚持看到这里的不容易,也请最后坚持一会,容笔者做一下总结。

        本文从runtime组件化方案聊起,大致描述了组件化方案的四个层级:Mediator,Mediator扩展,Target-Action,Module。接着分享了Swift”翻译“过程中的一些心得体验:Optional,id与AnyObject的联系,访问控制符和命名空间。还有好多小小的细节在开发过程遇到,只是粗心大易忘记记录。最后附上runtime组件化方案的Demo。本文如有不对之处,望多多指正,谢谢!

    相关文章

      网友评论

      • 如意神王:swift mediator 里面的 target action 不执行,oc的可以,楼主有没有办法?
      • XIAODAO:String、Array、Dictionary为Swift的值类型,底层是用struct实现的

      本文标题:从runtime组件化方案聊到Swift与OC混编

      本文链接:https://www.haomeiwen.com/subject/zlcxxxtx.html