如何使用swift高效自动配置Xcode工程

Posted 郑郑同学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何使用swift高效自动配置Xcode工程相关的知识,希望对你有一定的参考价值。

导语: 在ios自动化测试或是iOS自动化管理生成工程的过程中,会涉及到修改工程的配置和添加新的文件(Copy Bundle Resources),静态库,framework(Link Binary With Libraries),修改BundleID,修改Project Name,修改各类Search Path等…

今天要给大家介绍一个新造的"轮子": XcodeManager

  • Language: Swift(4.0)

  • Github: https://github.com/ZhengShouDong/XcodeManage (欢迎star)


1. 介绍基础知识

  • Xcode工程文件指的是xcodeproj,就是平时经常打开的这个工程文件(本质是个文件夹,可通过右键显示包内容打开查看内部),xcodeproj内包含了project.pbxproj文件,xcuserdata文件夹,xcworkspace文件夹.

  • 其中project.pbxproj文件内部存放了我们工程的很多关键性信息,例如Build Settings,Identity,Signing,Build Phases,我们打断点在这里也有对应的配置(是按电脑用户来隔离的,有时间以后分析一下).

  • 想要管理我们想要管理的信息,例如Identity,Build Settings,Signing..就是通过改写project.pbxproj文件来操作的.

2. 为什么会有XcodeManager

  • 前段时间在使用swift写后端来完成一些任务,这其中就遇到了需要自定义Xcode工程来编译ipa并签名.

为什么不用别人写好的轮子呢?我也想用啊..不好意思没有能达到我需求的..
此处介绍几个其他语言编写的轮子:

  • cordova-node-xcode:使用javascript编写,node.js运行环境.

  • Xcodeproj:使用Ruby编写.

  • pbxplorer:使用Ruby编写.

  • mod-pbxproj:使用Python编写.

曾使用swift间接调用node库cordova-node-xcode来实现需求,坎坷之路不多说.说说不方便的地方吧,虽然是Apache的孵化项目下的子模块,但是Bug是真心多..由于是我使用node.js封装了一层将库内部的部分API转换为了可以使用shell来调用的node命令行程序,每一次的更改配置都会有一定的IO操作文件(大概总共几百个配置需要更改吧..)效率超级低,平均3个/s配置完成.我不是说node.js慢,在有v8的加持下它运行起来很快,但是在我的这种情况下,无法发挥最大效能.另外我不能开多线程来同时并发执行node.js,因为最终都会操作(删除,追加,修改)同一个文件.OK,就这样写入173项配置它需要60s左右的操作时间..成为了整个后端系统性能上的瓶颈.
只有一个想法,尽快用swift实现一个适合我们使用的库,把效率提起来!由此,XcodeManager"蛋"生了!!同样改动的配置数量下,两个库的时间差了近10倍.使用XcodeManager仅需要6s左右就可以完成同样的任务.

3. 介绍project.pbxproj文件

project.pbxproj这个文件.它本质是个plist文件,只不过更老一代.官方名称:Property List.据说历史可追溯到乔布斯时代的NeXT的OpenStep.
关于文件内部的详细介绍可看这个http://www.monobjc.net/xcode-project-file-format.html

  • 那平时我们怎么操作这个文件的呢?

  • 由于存在Xcode这个强大的IDE,所以我们不需要直接操作这个文件,在Xcode很多面板上的操作直接就关联到了这个文件内部.我们仅需要轻轻点几下General,Build Settungs,Info等面板的按钮即可完成一些配置;

  • 瞧瞧这个文件的内部(篇幅有限,部分截取):

/* Begin PBXBuildFile section */
        C1AC41E620C11E8D007A53CC /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = C1AC41E520C11E8D007A53CC /* AppDelegate.m */; };
        C1AC41E920C11E8D007A53CC /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C1AC41E820C11E8D007A53CC /* ViewController.m */; };
        C1AC41EC20C11E8D007A53CC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C1AC41EA20C11E8D007A53CC /* Main.storyboard */; };
        C1AC41EE20C11E8E007A53CC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C1AC41ED20C11E8E007A53CC /* Assets.xcassets */; };
        C1AC41F120C11E8E007A53CC /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C1AC41EF20C11E8E007A53CC /* LaunchScreen.storyboard */; };
        C1AC41F420C11E8E007A53CC /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C1AC41F320C11E8E007A53CC /* main.m */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
        C1AC41E120C11E8D007A53CC /* iOS_Project.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOS_Project.app; sourceTree = BUILT_PRODUCTS_DIR; };
        C1AC41E420C11E8D007A53CC /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
        C1AC41E520C11E8D007A53CC /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
        C1AC41E720C11E8D007A53CC /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = "<group>"; };
        C1AC41E820C11E8D007A53CC /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = "<group>"; };
        C1AC41EB20C11E8D007A53CC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
        C1AC41ED20C11E8E007A53CC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
        C1AC41F020C11E8E007A53CC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
        C1AC41F220C11E8E007A53CC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
        C1AC41F320C11E8E007A53CC /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
        C1AC41DE20C11E8D007A53CC /* Frameworks */ = {
            isa = PBXFrameworksBuildPhase;
            buildActionMask = 2147483647;
            files = (
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
/* End PBXFrameworksBuildPhase section */

4. 实现原理是什么

操作以上文件内的内容并回写即可完成配置.前提一个地方都不要出现错误,否则Xcode就会"不认得"这个文件(文件损坏)

由于需要操作pbxproj文件的内容,所以一定会发生硬盘上的IO操作.为了高效,减少硬盘的IO.将pbxproj文件读取后反序列化再存入内存中缓存起来,所有的IO操作均在内存中,这样一来会比在硬盘中快速高效地多,最终在所有的操作完毕后将缓存在内存中的数据序列化再写回硬盘中.

整体流程大概是这样了,还有几处细节:

  • 初始化XcodeManager对象后多次调用库去操作同一个工程文件,如何最大的提升效率?在这里我将上次使用了的pbxproj文件路径和内容(二进制)的hash值存储在内存中,当再次调用解析工程文件的时候如果还是同一个文件,直接返回在内存中缓存的数据即可,加速数据解析.这里还有个不太严谨的问题,只使用Data的hash去对比容易碰撞.将来的版本中将会解决这个问题.关键代码如下:

let fileData = try Data(contentsOf: fileUrl)
let hashValue = fileData.hashValue
if (self._hashTag == hashValue && !self._cacheProjet.isEmpty) {
    return self._cacheProjet
}
self._filePath = filePath
self._hashTag = hashValue
let data = try PropertyListSerialization.propertyList(from: fileData, options: .mutableContainersAndLeaves, format: nil)
self._cacheProjet = JSON(data)
  • 文件的内部机制,需要对所有的对象生成新的UUID,我们平时在Xcode中看到的每一个文件列表,每一个配置文件的Value,都有对应的UUID.在文件内部,所有的UUID不得重复,长度:24,全大写…,关键代码如下:

/// generate a new uuid
fileprivate func generateUuid() -> String {
    if (self._cacheProjet.isEmpty) {
        return String()
    }
    let uuid = UUID().uuidString.replacingOccurrences(of"-"with"").suffix(24).uppercased()
    let array = self.allUuids(self._cacheProjet)
    if (array.index(of: uuid) ?? -1 >= 0) {
        return generateUuid()
    }
    return uuid
}
  • 文件保存时,看似序列化写回磁盘是件很简单的事情.但还有编码问题需要处理.直接写回磁盘会有中文乱码问题,需要把中文内容的Unicode标量值拿出来转换为Numeric character reference,这里写入了两次,目前我还没有好的解决方法.第一次把内存中数据序列化写入到磁盘上,第二次处理中文乱码读取后再次写入.好在这两次写入只是在调用save()函数时候发生.主要代码:

fileprivate func saveProject(fileURL: URL, withPropertyList list: Any) -> Bool {
        let url = fileURL
        func handleEncode(fileURL: URL) -> Bool 
{
            func encodeString(_ str: String) -> String {
                var result = String()
                for scalar in str.unicodeScalars {
                    if (scalar.value > 0x4e00 && scalar.value < 0x9fff) {
                        result += String(format: "&#%04d;", scalar.value)
                    } else {
                        result += scalar.description
                    }
                }
                return result
            }
            do {
                var txt = try String(contentsOf: fileURL, encoding: .utf8)
                txt 
= encodeString(txt)
                try txt.write(to: fileURL, atomically: true, encoding: .utf8)
                return true
            } catch {
                xcodeManagerPrintLog("Translate chinese characters to mathematical symbols error: \(error.localizedDescription)", type: .error)
                return false
            }
        }
        do {
            let data = try PropertyListSerialization.data(fromPropertyList: list, format: .xml, options: 0)
            try data.write(to: url, options: .atomic)
            return handleEncode(fileURL: url)
        } catch {
            xcodeManagerPrintLog("Save project file failed: \(error.localizedDescription)", type: .error)
            return false
        }
    }

5. 如何使用

你需要在你的swift项目中的Package.swift文件中写入:

import PackageDescription
let package = Package(
    name: "YourProjectName",
    dependencies: [.package(url: "https://github.com/ZhengShouDong/XcodeManager.git", from: "0.1.1")],
    targets: .target(name: "YourProjectName", dependencies: ["XcodeManager"]))

然后使用swift package update命令来拉取XcodeManager作为依赖使用.
具体使用方法和API调用方式请看项目的README(
https://github.com/ZhengShouDong/XcodeManager/blob/master/README.md).



以上是关于如何使用swift高效自动配置Xcode工程的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Xcode for Swift 中使用代码格式化程序?

带有 Swift 超慢输入和自动完成功能的 Xcode 6

如何使用 swift 将 google 地方自动完成功能添加到 xcode(教程)

如何让链接在 iOS 中使用 Xcode/Swift 自动打开 safari 应用程序(不是本地)?

Swift - Xcode 8.3 自动完成未显示

如何使用 Swift 在 XCode UI 自动化中选择随机表格视图单元格