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

概述

    利用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。本文如有不对之处,望多多指正,谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容