iOS R.Swift原理浅析

这篇文章是我从应用来理解R.Swift的笔记,主要想法是通过从R.Swift的引入方式,应用方式等来反向地了解R.Swift的几个方面
1.R.Swift做了什么使得我们能够以对象的方式调用资源库的数据。
2.R.generated是如何让我们通过对象来调用到真实数据的呢?
3.从1和2中我们可以了解到我们是通过R.generated来调用到对应的对象化的数据,那么,R.Swift又是如何在编辑阶段动态地根据项目中的资源库来生成以及更新R.generated文件的。
4.我们在知道R.generated的生成与更新的原理后需要更加深入地理解,R.Swift的整体结构

1.R.Swift做了什么使得我们能够以对象的方式调用资源库的数据

我们知道,当我们引入R.Swift框架时通常有数个步骤要执行。
以cocospod引入为例,第一步,将R.Swift记录在pod文件中并install
第二步,在Targers中选中Build Phases目录并通过左上方的加号按钮选择创建一个编译时运行脚本。
第三步,将编译时运行脚本拖曳到Compile Sources的上方并在Check Pods Manifest.lock的下方,然后加入相应的脚本运行命令。
第四步,将$TEMP_DIR/rswift-lastrun命令加入到Input Files中,并将$SRCROOT/R.generated.swift命令加入到Output Files中,从上面两句命令我们大致可以判断出来这是指定输入文件及输出文件的目录,分别对应第三步中的相关脚本。
第五步,拖曳生成的R.generated.swift到项目中,并且,记得不要修改其实际物理地址。

尔后,当这五步操作完成时,我们可以发现,当我们将R.Swift支持的资源或是创建或是拖曳进项目时,经过一次编译就可以正常地使用对象去调用到实际资源。
这实际上就表明了R.Swift通过每次的编译阶段运行第三步指定的脚本工具获取项目下的资源信息,将其根据自身的源码进行编译,最后输出能让我们调用对象化资源的代码到R.generated.swift中,由此,我们就可以在每次编译完成后获取R.Swift更新的对象化资源。
这也从一个侧面描述了R.Swift的作者在仓库页面所说的R.Swift倡导的对象化资源的安全性,因为,根据逻辑而言,我们的代码在运行前都要经过编译阶段,而如果在使用对象化的资源时对象化资源所指定的真实资源发生了改变,那么在编译过程中R.generated.swift产生了变化,此时Xcode本身的特性便会让编译阶段终止并报告错误,由此防止了运行阶段资源引用错误导致的不稳定状况。

2.R.generated是如何让我们通过对象来调用到真实数据的呢?

我们用两个常见的资源来看

R.image.sexygirl()
R.storyboard.main.second()

资源一是我拖曳进初始项目的一张漂亮妹子的图片,资源二是在MainStroyboard中加入的一个UIViewController,我设置其StoryboardIDsecond
R.Swift正是通过StoryboardID来分类Stroybaord中的资源数据的。
打开R.generated文件,你可以看到如下代码:


 /// This `R.image` struct is generated, and contains static references to 1 images.
  struct image {
    //R.image.sexygirl图像资源
    static let sexygirl = Rswift.ImageResource(bundle: R.hostingBundle, name: "sexygirl")
    
    /// `UIImage(named: "sexygirl", bundle: ..., traitCollection: ...)`
    static func sexygirl(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? {
      return UIKit.UIImage(resource: R.image.sexygirl, compatibleWith: traitCollection)
    }
    
    fileprivate init() {}
  }
//R.storyboard.main对象
 struct main: Rswift.StoryboardResourceWithInitialControllerType, Rswift.Validatable {
      typealias InitialController = ViewController
      
      let bundle = R.hostingBundle
      let name = "Main"
      let second = StoryboardViewControllerResource(identifier: "Second")
      
      func second(_: Void = ()) -> UIKit.UIViewController? {
        return UIKit.UIStoryboard(resource: self).instantiateViewController(withResource: second)
      }
      
      static func validate() throws {
        if #available(iOS 11.0, *) {
        }
        if _R.storyboard.main().second() == nil { throw Rswift.ValidationError(description:"[R.swift] ViewController with identifier 'second' could not be loaded from storyboard 'Main' as 'UIKit.UIViewController'.") }
      }
      
      fileprivate init() {}
    }

我们先来看sexygirl资源,这个图像资源被分配image结构体中,其实现了一个属性及一个函数

/// Image `sexygirl`.
    static let sexygirl = Rswift.ImageResource(bundle: R.hostingBundle, name: "sexygirl")
    
    /// `UIImage(named: "sexygirl", bundle: ..., traitCollection: ...)`
    static func sexygirl(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? {
      return UIKit.UIImage(resource: R.image.sexygirl, compatibleWith: traitCollection)
    }

我们可以看到其在函数中调用了UIImage函数,入参是上面构造的属性R.image.sexygirl,而点击这个关于UIImage的初始化函数我们会跳转到R.swift.library的相关文件中,表明这是一个UIImage的扩展,在扩展中实现的初始化函数内部调用了UIImage的真实初始化函数,通过imageNameBoundle获取到指定的资源。
而对应的imageNameBoundle是怎么来的呢?我们可以看到sexygirl属性,其本身在定义时就指定了imageNameBoundle,也就是说,真实资源的指向信息在编译阶段由脚本向R.generated完成插入。
而再看R.storyboard.main.second()我们可以发现,R.storyboard结构体中只实现了指向真实main资源的函数,而其用来获取真实资源的R.storyboard.main实际上是一个_R.storyboard.main的对象,而当我们跳转到_R.storyboard.main时才能看到关于second的实现部分。
而同样的,我们可以发现,在second的实现函数中同样是使用了预先获取好的identifier调用UIStoryboard获取对应标记控制器的系统接口来获取到真实资源。
所以到这一步,我们就明白了,``R.generated的工作主要就是在编译脚本运行完成后提供自动生成的对象化资源的相关接口,通过将接口以及早已在编译阶段通过脚本获取到的关于真实资源的信息相结合,形成了我们所看到的R.Swift的表象。而R.swift.library的作用正是预先为R.generated提供所需的扩展,在R.generated新建或更新完成时使用。 这就是R.Swift的表象,之所以说是表象,是因为我们真正想见到的关于R.Swift的如何利用脚本去获取一个项目下的所有资源信息以及脚本是如何编写整个R.generated的代码并最终生成R.generated的文件的,这些R.Swift的内核都藏在R.Swift`的脚本中。

3.从1和2中我们可以了解到我们是通过R.generated来调用到对应的对象化的数据,那么,R.Swift又是如何在编辑阶段动态地根据项目中的资源库来生成以及更新R.generated文件的。

我们可以从R.Swift的git仓库直接下载zip包来获取源码,需要注意的是,git上的源码需要使用Swift Package 包管理系统进行配置,而配置完成后我们可以通过生成xocdeproj文件来查看代码,关于如何通过Swift Page Manager进行配置,git仓库有如下说明:

Building from source

R.swift is built using Swift Package Manager (SPM).

Check out the code
Run swift build -c release from the root directory
Follow the manual installation steps with the binary you now have
For developing on R.swift in Xcode, run swift package generate-xcodeproj --xcconfig-overrides RswiftConfig.xcconfig.

在配置完成后我们也可以通过在Build Phases加入编译时脚本来使用源码生成R.generated.swift文件,我试了几次,成功生成了文件,其中需要注意的有两点:
一、脚本不能直接拷贝git仓库的脚本命令,比如"$SRCROOT/rswift" generate "$SRCROOT/R.generated.swift",因为我们下载下来的源码的rswift不在根目录下,所以要修改地址到对应的目录下。
二、源码包中没有Project Bundle Identifier,需要在Build Setting中配置。
其他的我在生成R.generated.swift中没有碰到什么困难。

在源码中生成R.generated.swift仅是让人理解一下R.Swift使用源码生成的过程,实际要对源码有更多更深的理解我们还是直接看main.swift文件吧。

在进入main.swit文件我们会看到百多行的代码,可能直接从上往下看会让人不清不楚,所以我们先对头部的import文件和尾部的运行代码先进行粗浅的理解。

...head
import Foundation
import Commander
import RswiftCore
import XcodeEdit
...
...end
let group = Group()
group.addCommand("generate", "Generates R.generated.swift file", generate)
group.run(Rswift.version)
...

我们从头部引用文件里可以看到有两个特别的引用文件,一个是Commander,另一个是XcodeEdit,这两个框架便是R.Swift源码通过SPM引入的框架,简而言之,Commander的作用是让我们在Swift中以其提供的相关接口快速方便地构造命令行接口。
XcodeEdit的功能对Xcode project.pbxproj进行读写,而该框架在R.Swift中的使用绝大多数的应用便是获取一个项目下的资源信息为R.Swift自动化构造提供相应的助力。

接着看尾部代码。
GroupCommander中的类,其主要功能就是作为命令行接口的容器。
第二行代码,它往group中插入了一个命令,命令的key是generate,备注是Generates R.generated.swift file,实际上塞入了一个对象generate
我们可以拿R.swift的编译脚本进行对比:
"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/R.generated.swift"

所以我们大约能猜出编译脚本中的generate实际上估摸就是在执行这个源码的相关命令。
我们接着往下看,第三行代码的run函数很明显就是在执行第二行的代码。

这个时候我们可以回头去找第二行的代码中的generate对象了。
往回回滚代码找到generate

最外壳的代码大致就是这个样子的:

let generate = command(
  CommanderOptions.uiTest,
  CommanderOptions.importModules,
  CommanderOptions.accessLevel,
  CommanderOptions.rswiftIgnore,
  CommanderOptions.inputOutputFilesValidation,
  CommanderArguments.outputPath
) { uiTestOutputPath, importModules, accessLevel, rswiftIgnore, inputOutputFilesValidation, outputPath in

......

}

最外壳的代码没什么深究的意义,如果你对Commander这个框架有所理解,实际上CommanderOptions.uiTest都是包装的命令可选参数,而在R.Swift中这里使用到的参数都带有默认值,在我们仅使用generate命令而不带可选参数时默认实现。
具体的这些参数的意义因为个人认为不牵扯到核心代码所以暂且略过。
在略过外壳代码后我们看到的是generate对象的内部回调,在这里我们首先看到的是诸如

 let processInfo = ProcessInfo()

  // Touch last run file
  do {
    let tempDirPath = try ProcessInfo().environmentVariable(name: EnvironmentKeys.tempDir)
    let lastRunFile = URL(fileURLWithPath: tempDirPath).appendingPathComponent(Rswift.lastRunFile)
    try Date().description.write(to: lastRunFile, atomically: true, encoding: .utf8)
  } catch {
    warn("Failed to write out to '\(Rswift.lastRunFile)', this might cause Xcode to not run the R.swift build phase: \(error)")
  }

 let xcodeprojPath = try processInfo.environmentVariable(name: EnvironmentKeys.xcodeproj)
  let targetName = try processInfo.environmentVariable(name: EnvironmentKeys.target)
  let bundleIdentifier = try processInfo.environmentVariable(name: EnvironmentKeys.bundleIdentifier)
  let productModuleName = try processInfo.environmentVariable(name: EnvironmentKeys.productModuleName)

这样的代码,直到try RswiftCore(callInformation).run()之前都属于同一性质的代码,即预先为R.Swift的核心代码配置前提数据。
我们同样可以忽略这部分代码,只要知道几个核心要素就行了,
其一是,这部分代码我们可以看到有相当数量的局部不可变对象都是通过ProcessInfo这个获取当前进程相关信息的对象进行配置的,因此ProcessInfo可以说是R.Swift获取资源信息列表的前提。
其二是,另一部分预先配置对象都是通过我们之前看到的外壳入参的相关对象进行配置,这也说明在我们直接调用generate命令时,外壳入参的可选参数的默认配置对内部的逻辑也起到一定作用。
但也正如我们之前所说的,这部分代码在为R.Swift需求的资源列表信息提供预先准备的前提信息配置,就逻辑而言也是十分重要的,毕竟没有这部分信息,R.Swift真实需求的资源列表信息可能就无从谈起了,但其可以说都没有涉及到R.Swift的核心要素,因此,我们继续往下看。

  try RswiftCore(callInformation).run()

callInformation对象正是R.Swift在进行自动化生产时需要的最终关于所有前提信息的封装容器。
进入到RswiftCore的内部代码我们可以看到,在初始化时RswiftCore并没有再对callInformation进行数据再重构,所有的逻辑代码都存在于run函数中。

在研究run函数的内部结构时我是这么对其进行分割的,首先,我注意到了fileContents这个对象以及紧随其中的writeIfChanged(contents: fileContents, toURL: callInformation.outputURL),这段代码很明显是在向一个URL地址里写入fileContents对象,而通过options查看fileContents,我们可以发现这就是一个字符串,在看到这个字符串的时候我们就明白了,fileContents已经代表了R.Swift的最终成果,它将所有的资源信息都转译成我们在R.generate.swift中看到的结构体,并将结构体转译成了带有格式的字符串,然后将字符串直接写入到命名为R.generate.swift的文件中,使其按照字符串的格式排列。
那么用来生成fileContentsgenerateRegularFileContents函数自然就带有将所有资源信息生成的结构体进行拼装的职责。

先暂且不查看generateRegularFileContents的内部结构,我们继续往上看,既然fileContents已经是最终结果了,那上面寥寥数个局部不可变对象具有什么样的职责呢?
第一个xcodeproj是通过url地址进行初始化,查看内部的初始化代码,我们很简单就可以理解其实际功能是利用XcodeEdit框架获取到XCProjectFile的实例对象。
接着看ignoreFile,忽略掉它,顾名思义,就是忽略文件,在熟悉.gitnore的我们眼里,它不过是另一种规则文件让R.Swift在编译过程中忽略掉一些资源。

然后是resourceURLs,文件名已经很明确地告诉我们了,这个对象数组就是我们需要的资源信息列表,而通过option查看,我们也很直观就能看到这是一个URL类型的数组。
那这个数组是如何生成的呢,我们首先进入xcodeproj.resourcePathsForTarget中,它的第一部分代码通过传入的targetName来确定一个projectFile下哪个是最终所需要的target。而这个targetName正是我们在往一个项目中的targetBuild phases时确定下来的。
接着找到targetbuildPhases,通过buildPhases来获取资源信息列表,因为这部分的逻辑都是存在于XcodeEdit这个框架中,如果要深入了解的话必须对XcodeEdit的源码做出分析,因此这里只是简单的说明一下,一个项目中的Build Phases本身就存在资源信息列表的展示,你可以打开手头上随意一个项目进行,在Build Phases下的Copy Bundle Resources下就是项目的资源列表,我们可以简单地认为XcodeEdit就是获取到了这个条目下的资源列表转化为相应的信息交给我们,当然,关于如何获取到资源信息实际上牵扯到很多东西,要深入了解这块东西就必须要对XcodeEdit的源码有所了解了。
总而言之,这一步都是在调用XcodeEdit的函数,最终获取到所需的资源信息列表,返传[Path]对象,并最终重组为[URL]

现在,我们持有所有资源的URL地址了,但是只有URL地址是不够的,R.Swift所支持的文件类型有nib、storyboard、图片、字体等等,我们需要从URL地址中识别该URL地址是哪一类的资源并将其分类到正确的资源数组中,有时,例如nib和storyboard类型的资源文件,我们往往还要分离出更多所需信息,比如nib中存在多少个view的nib文件,storyboard中有多少个控制器的nib,以及它们指向真实控制器的唯一标示符等等等等。

要想完成这一步,就要看resources实例对象的初始化了。
Resources的初始化正是将前一行代码获得的resourceURLs进行系统化地分类。
进入到Resources的初始化函数,我们可以看到其核心代码是遍历URL地址,并通过try特性尝试性地将url地址塞入对应的结构体初始化函数中,塞入成功则将其纳入对应的资源数组中。
这么说就太简单了,我们实际地来看使用URL地址对对应的结构体进行初始化都做了什么吧。

因为R.Swift解析的类型数据有近十种,限于篇幅,这里主要是归纳了Nib、Storybaord、AssetFolder这三种我们平时使用R.Swift时最长接触的类型数据。

首先是Nib结构体的尝试初始化:

 init(url: URL) throws {
    try Nib.throwIfUnsupportedExtension(url.pathExtension)

    guard let filename = url.filename else {
      throw ResourceParsingError.parsingFailed("Couldn't extract filename from URL: \(url)")
    }
    name = filename

    guard let parser = XMLParser(contentsOf: url) else {
      throw ResourceParsingError.parsingFailed("Couldn't load file at: '\(url)'")
    }

    let parserDelegate = NibParserDelegate()
    parser.delegate = parserDelegate

    guard parser.parse() else {
        throw ResourceParsingError.parsingFailed("Invalid XML in file at: '\(url)'")
    }

    rootViews = parserDelegate.rootViews
    reusables = parserDelegate.reusables
    usedImageIdentifiers = parserDelegate.usedImageIdentifiers
    usedColorResources = parserDelegate.usedColorReferences
    usedAccessibilityIdentifiers = parserDelegate.usedAccessibilityIdentifiers
  }

这部分的结构体初始化函数的前几行的代码逻辑都大致相同,查看是否能获取到文件名,如果是需要进行结构解析的对象的话尝试性根据URL地址初始化XML解析器。

Nib结构体在解析器初始化后将解析器的代理交给NibParserDelegate对象开始解析,然后NibParserDelegate在解析成功后再将相应的解析完成数据交给Nib
我们具体来看下NibParserDelegate如何完成XML解析的。

要仔细了解R.Swift对Nib文件的解析,我们自然需要创建一个Nib文件来对照参考,你可以随意地在一个新项目中创建一个Nib文件,设置其中的View以及更多的视图并右键Nib文件选择Open as Source Code来查看其真实的XML格式数据。
例如:

 
        
        
        
            
            
            
                
                    
                    
                    
                        
                        
                    
                
            
            
            
                
                
            
            
        
    

因为一个XML文件的代码过于繁多,我这里只截取了其中的Objects元素中的相关代码,该XML代码描述了一个属于TestView类的视图,其中,有一个居中的子View。

我们再来看看NibParserDelegate实现的XML解析代理中做了什么。

我们可以直接阅览NibParserDelegate关于XMLParserDelegate@objc func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String])
的相关实现。

//第一部分
    if isObjectsTagOpened {
      levelSinceObjectsTagOpened += 1
    }
    if elementName == "objects" {
      isObjectsTagOpened = true
    }

若已经检测到元素名为Objects的元素,则设置isObjectsTagOpenedtrue,代表需要解析的核心内容已经出现了,而当isObjectsTagOpenedtrue时,每次解析到元素都将levelSinceObjectsTagOpened进行自增,这个值代表目前解析到的元素的深度,也可称作层级。

而与didStartElement代理回调相对应的didEndElement回调,会对上面的相关布尔值和层级值进行反置代表Objects元素解析完成。

接着,对每一个元素名进行解析,如果是image、color、accessibility、userDefinedRuntimeAttribute这数种类型,则直接从元素的内容字典中获取需要的value填入归纳数组中。

 case "image":
      if let imageIdentifier = attributeDict["name"] {
        usedImageIdentifiers.append(imageIdentifier)
      }

    case "color":
      if let colorName = attributeDict["name"] {
        usedColorReferences.append(colorName)
      }

    case "accessibility":
      if let accessibilityIdentifier = attributeDict["identifier"] {
        usedAccessibilityIdentifiers.append(accessibilityIdentifier)
      }

    case "userDefinedRuntimeAttribute":
      if let accessibilityIdentifier = attributeDict["value"], "accessibilityIdentifier" == attributeDict["keyPath"] && "string" == attributeDict["type"] {
        usedAccessibilityIdentifiers.append(accessibilityIdentifier)
      }

而除此之外的元素都通过viewWithAttributes函数尝试性地进行解析,以找到Nib文件中的rootView

viewWithAttributes内部实现了对attributeDict的应用并尝试性获取customModuleProvider、customModule、customClass三个字符串,若能获取到则根据规则将customClass转化为SwiftIdentifier对象,该对象负责将customClass字符串转为为规定格式的字符并存储。之后SwiftIdentifier作为参数被纳入Type对象中,与之相同的还有被存储到Module中的customModule字符串,最终Type对象作为最终存储单元输出。
这一步主要的工作就是找到一个当我们在调用R.nib.xxx时应该被调到的对象的原始文本形态并将其转化为R.Swift在输出R.generated.Swift时需要的数据格式。

在看完Nib的解析后,我们接下来看同样重要的Storyboard

跳过解析前与Nib解析相类似的步骤,我们直接来看didStartElement回调函数。

case "document":
      if let initialViewController = attributeDict["initialViewController"] {
        initialViewControllerIdentifier = initialViewController
      }

首先,当元素为document时记录初始控制器的标记符,这个属性会在之后获取到控制器时逐一对照来找到初始控制器。

接着,我们先跳过segue属性,先看控制器的序列化。
控制器的序列化同样在swicthdefault中进行,控制器通过viewControllerFromAttributes函数进行序列化,大部分逻辑都与Nib相同,但控制器的XML比Nib要复杂的多,这里在进行序列化的时候也多了几个关键参数。
其一是idid参数是这个控制器在Storyboard中的唯一标识符,我们上面有提到,在解析到document时会记录一个初始控制器标识符的参数initialViewControllerIdentifier,这个参数就是解析控制器XML时获得的id
其二是storyboardIdentifier,回想一下使用R.Swift的过程,我们是不是需要在Storyboard中设置控制器的Storyboard ID属性才能通过R.Swift调到该对象,storyboardIdentifier正是Storyboard ID,记录这个属性至关重要。

最后,连带上述两个参数和Type属性,初始化Storyboard.ViewController并返传,一个控制器的解析也就完成了。
但这里的任务还未结束。
回到解析的代理回调,我们可以看到Storyboard的解析与Nib不同的地方是,没有直接将解析完成的数据传入全局数组中保存,而是用一个全局属性currentViewController引用它。
因为,我们还需要这个解析完成的结构体去容纳其他数据。
现在,我们回到Segue的解析。
在查看Segue的解析代码,我们很容易就发现除了例行的Type转化外还提取了segueIdentifier、destination、kind三个属性,即使不常用Segue也能理解这三个参数分别代表Segue的唯一标示符、跳转目标的唯一标示符(即上面解析的id)和跳转效果。
这三个参数会和Type一起作为参数初始化Storyboard.Segue,并将初始化的对象加入到currentViewControllersegue数组中存储。
这样就完成了一个控制器XML的全部解析,并将XML中本来就位于控制器的XML代码内部的Segue代码正确地插入到控制器结构体中。

最终,所有归纳完成的控制器都存储到Storyboard结构体的属性数组中,返还Storyboard结构体。

最后,我们来看一下存储了一般项目大部分资源的AssetFolder的序列化。
首先回忆一下我们日常对Assets的使用,我们通常会存储大量的图片资金到该资源夹下,iOS11增加了一些特性使得我们也可以存储颜色资源到资源夹下。并且,为了防止中、大型项目图片资源过多而Assets资源夹难以阅读的问题,我们同样可以在Assets中创建子文件夹。

R.swift的序列化同样也对标了三种URL数组

var imageAssetURLs = [URL]()
    var colorAssetURLs = [URL]()
    var namespaces = [URL]()

在通过FileManagerAssets进行解析时首先将数据进行分类。
但需要注意的是,R.Swift需求的数据可不是URL类型,即使是将URL转化为字符串也无法满足,因为,我们仅仅需要图片的名字,它前面一长串的路径地址是不需要的,就如我们调用UIImage.name()时也不需要路径一样。

在对URL地址进行归纳时我们可以注意一下:

 if imageExtensions.contains(pathExtension) {
          imageAssetURLs.append(fileURL)
        }
        if colorExtensions.contains(pathExtension) {
          colorAssetURLs.append(fileURL)
        }

这几行代码。
通过查看imageExtensions我们可以发现它是存储字符串的Set,但Set存储的却不是我们下意识以为的png、jpg,因为实际上我们放入资源文件夹的资源都会被创建一个imageExtensions中的后缀名的文件夹,例如图片资源的2x、3x图会被存储到同一名字的.imageset文件夹下方便管理。

在完成URL分类后开始进行资源夹解析。

首先,进行子资源夹的解析,将URL类型的路径数组转化为NamespacedAssetSubfolder对象数组,遍历调用populateSubfolders函数。
该函数的(似乎)实际功能是为了将URL的路径层级转化成数据的层级,并且,对资源文件夹pathresourcePath,利用NamespacedAssetSubfolder初始化时通过URL获取的相关数据进行字符串组合。(这段代码我不是看的很清楚,主要原因是这里的代码看似是在进行层级划分,但之后的生成Swift代码文本环节没有看到有根据层级划分做什么,也可能是我看漏了。)
在完成资源夹的层级划分,使用同样的手段对图片资源和颜色资源进行解析,就如我们调用UIImage.name()函数时无需后缀名一样,在解析图片资源时也不需要深入到.imageset内部对2x、3x图片进行解析,只要直接将.imageset去掉后缀名就可以了。解析完成后将资源全部存储到统一的资源数组中。

Assets的解析工作就完成了。

其他的类型也通过类似的手段进行解析。
当所有的URL都解析完成时,接下来就是如何将所有的解析数据转化为Swift代码格式的字符串了。

let fileContents = generateRegularFileContents(resources: resources, generators: [
        ImageStructGenerator(assetFolders: resources.assetFolders, images: resources.images),
        ColorStructGenerator(assetFolders: resources.assetFolders),
        FontStructGenerator(fonts: resources.fonts),
        SegueStructGenerator(storyboards: resources.storyboards),
        StoryboardStructGenerator(storyboards: resources.storyboards),
        NibStructGenerator(nibs: resources.nibs),
        ReuseIdentifierStructGenerator(reusables: resources.reusables),
        ResourceFileStructGenerator(resourceFiles: resources.resourceFiles),
        StringsStructGenerator(localizableStrings: resources.localizableStrings),
        AccessibilityIdentifierStructGenerator(nibs: resources.nibs, storyboards: resources.storyboards),
      ])

我们首先来看看generateRegularFileContents函数的入参,其第一入参是上一步解析完成的总resources,这个参数基本是备用性质,函数内部实现我都没见哪里用到……
接着是一个[StructGenerator]类型的数组,StructGenerator是一个协议,只要实现这个协议的对象都可以插入数组,所以我们看到了上面一长串的类似ImageStructGenerator初始化函数的大量StructGenerator初始化函数,其统一特征都是将上一步解析完成,填入Resources内部属性数组的数据分别利用,就像ImageStructGenerator的初始化就是利用resources.assetFoldersresources.images,前者通过解析Assets获得,后者则是通过解析分散在项目各处的没有被添加入Assets资源夹中的图片文件得到。

ImageStructGenerator内部结构相对清晰明了,其在初始化的时候没有仅将入参作为全局属性保存,另一个至关重要的函数是generatedStruct,该函数作为协议声明的函数,在generateRegularFileContents函数被统一调用。

在阅览每个遵循StructGenerator协议的结构体的generatedStruct函数内部实现时,我建议利用R.generated.swift达到更好理解的作用,例如,在查看ImageStructGenerator的相关实现时,拉几张图片到项目中,编译后查看R.generated.swiftImage结构体相关代码,基本上就能很清楚ImageStructGenerator的每一步做了什么了。
ImageStructGenerator的该函数被调用时,首先设置了文本名为image,这个文本名对应了R.generated.swiftimage结构体,接着,从初始化时存储的AssetFolderImage数组中获取到所有的图片名并重组成同一的字符串数组。

    let structName: SwiftIdentifier = "image"
    let qualifiedName = prefix + structName
    let assetFolderImageNames = assetFolders
      .flatMap { $0.imageAssets }

    let imagesNames = images
      .grouped { $0.name }
      .values
      .compactMap { $0.first?.name }

    let allFunctions = assetFolderImageNames + imagesNames

接着,浏览R.generated.swift,我们可以发现一个image结构内部组成总的来说有两部分,一部分是真实资源的静态全局变量

    static let sexygirl = Rswift.ImageResource(bundle: R.hostingBundle, name: "sexygirl")

另一部分是我们拿到真实数据的静态函数,通过遍历项目获得的图片名来调用对应的真实图片资源。

    /// `UIImage(named: "sexygirl", bundle: ..., traitCollection: ...)`
    static func sexygirl(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? {
      return UIKit.UIImage(resource: R.image.sexygirl, compatibleWith: traitCollection)
    }

以此推论,ImageStructGenerator内部应该有负责将这两块内容转化成Swift代码文本的相关模块。

这两块的主体实现就是imageLetsgroupedFunctions.uniques.map { imageFunction(for: $0, at: externalAccessLevel, prefix: qualifiedName)两块数据,前者生成了一个[Let]结构体数组,后者生成了一个Function结构体数组。
两者的数据来源都来自于groupedFunctions.unique这个[String]数组,而groupedFunctions则是通过第一步我们获取的所有图片资源的字符串数组allFunctions组装而来。

Let、Function、Struct三种结构体的定义区别其实不大,前两者更为简单一些,而后者更复杂,因为通常情况后者是作为前两者的容器存在。
三种结构体都遵循并实现了SwiftCodeConverible协议,该协议仅提供一个swiftCode属性,结构体在实现该属性时即将自身所有的属性都转化为文本格式作为当外界调用swiftCode时的输出字符。

我们可以对照来看一个Let结构体的swiftCode实现及其在R.generated.swift中的代码。

  Let(
          comments: ["Image `\(name)`."],
          accessModifier: externalAccessLevel,
          isStatic: true,
          name: SwiftIdentifier(name: name),
          typeDefinition: .inferred(Type.ImageResource),
          value: "Rswift.ImageResource(bundle: R.hostingBundle, name: \"\(name)\")"
        )
-————————————————————————————————————————————————————
var swiftCode: String {
    let commentsString = comments.map { "/// \($0)\n" }.joined(separator: "")
    let accessModifierString = accessModifier.swiftCode
    let staticString = isStatic ? "static " : ""

    let typeString: String
    switch typeDefinition {
    case let .specified(type): typeString = ": \(type)"
    case .inferred: typeString = ""
    }

    return "\(commentsString)\(accessModifierString)\(staticString)let \(name)\(typeString) = \(value)"
  }
  /// Image `sexygirl`.  
    static let sexygirl = Rswift.ImageResource(bundle: R.hostingBundle, name: "sexygirl")

注释对应commentsstatic对应isStatic布尔值,sexygirl对应name,调用Rswift.ImageResource的代码对应了value,这就是一个Let结构体的全貌了。
FunctionStruct也是大致如此,只不过更加复杂一些。

现在我们知道ImageStructGenerator通过所有的图片资源最终输出了一个已经包含R.image所有实现文本的Struct结构体了,接下来我们来看看generateRegularFileContents如何利用各个结构体输出文本。

进入generateRegularFileContents函数内部,第一行就是

    let aggregatedResult = AggregatedStructGenerator(subgenerators: generators)
      .generatedStructs(at: callInformation.accessLevel, prefix: "")

aggregatedResult是一个包装过的元组,其一称作externalStruct,其二称作internalStruct,暂且记下名称的意义,我们进入AggregatedStructGenerator内部。

这个类的意义是输出包裹所有我们之前看到被传入函数内部的结构体的结构体,generatedStructs函数开头三行的R_R两个静态文本很清晰地向我们揭示了这一点。

let structName: SwiftIdentifier = "R"
    let qualifiedName = structName
    let internalStructName: SwiftIdentifier = "_R"

而这两个字符分别定义了外部结构体和内部结构体的名称,即externalStructinternalStruct

那么如何划分内部结构体和外部结构体呢?

 let collectedResult = subgenerators
      .compactMap {
        let result = $0.generatedStructs(at: externalAccessLevel, prefix: qualifiedName)
        if result.externalStruct.isEmpty { return nil }
        if let internalStruct = result.internalStruct, internalStruct.isEmpty { return nil }

        return result
      }
      .reduce(StructGeneratorResultCollector()) { collector, result in collector.appending(result) }

首先,我们对subgenerators进行遍历重组,在compactMap高阶函数中调用每个StructGeneratorgeneratedStructs函数,通过该函数获得每个子类的Struct输出,而我们之前浏览的ImageStructGenerator中并没有该函数的实现,那是因为ImageStructGenerator声明的协议是ExternalOnlyStructGenerator,代表其不存在内部结构体,而该协议重实现了StructGenerator协议的generatedStructs函数,使其调用遵循该协议的对象的generatedStruct函数(注意,没有s)。

这个时候,如果是StoryBoard类型的StructGenerator则会输出内外两个结构体,而Image类型的则只输出外结构体。

在拿到所有的子类的Struct输出时,再对元组result使用reduce高阶函数,将所有结构体存储到StructGeneratorResultCollector结构体中,由此,完成整体的分类。最终,再定义内外两个结构体,分别容纳StructGeneratorResultCollector中的相关数据并输出。(这一步为什么要再使用StructGeneratorResultCollector对元组数据进行分类我并不是很理解)

 let codeConvertibles: [SwiftCodeConverible?] = [
      HeaderPrinter(),
      ImportPrinter(
        modules: callInformation.imports,
        extractFrom: [externalStruct, internalStruct],
        exclude: [Module.custom(name: callInformation.productModuleName)]
      ),
      externalStruct,
      internalStruct
    ]

最后,将所有数据转为[SwiftCodeConverible]数组(该数组如果还有印象的话就知道SwiftCodeConverible声明了swiftCode属性,而所有的Let、Function、Struct都实现了该属性输出文本),拼装成唯一的字符串,输出成为R.generated.swift文件。

由此,整个R.swift的脚本任务就完成。

你可能感兴趣的:(iOS R.Swift原理浅析)