iOS打包(重签名的方式)--用Mac客户端来实现
2017-08-24 10:53
696 查看
iOS打包(重签名的方式)--用Mac客户端来实现
公司最近出了个需求,要求迅速给客户打一些马甲包,就是替换里面的plist和一些资源文件(icon和launchImage),于是找了很多资料,发现这一部分很多内容都已过期或者说讲的不全面,遂收集了一个全套的ipa重签名内容,分享给大家。代码是用swift写的,版本3.0
常量定义
struct PathDefine { static let UnzipPath = NSTemporaryDirectory().appending("unzip") static let TargetPath = "/Users/apple/Desktop" static let outputPath : String = "/Users/apple/Desktop/autoPackage" } struct OtherStringDefine { static let appid = "application-identifier" static let kTeamIdentifier = "com.apple.developer.team-identifier" static let kKeychainAccessGroups = "keychain-access-groups" static let codeSignature = "_CodeSignature" static let dbVersionKey = "dbVersion" }
这些常量在以下内容中会用到
1解包
//解压之前先创建解压目录 fileprivate func createWorkingPath() { let manager = FileManager.default if manager.fileExists(atPath: PathDefine.UnzipPath) == false { //不存在目录时创建一个 do { try manager.createDirectory(atPath: PathDefine.UnzipPath, withIntermediateDirectories: true, attributes: nil) } catch { print(error.localizedDescription) } } else { // 存在目录时清空目录 do { try manager.removeItem(atPath: PathDefine.UnzipPath) } catch { print(error.localizedDescription) } //再创建一个 do { try manager.createDirectory(atPath: PathDefine.UnzipPath, withIntermediateDirectories: true, attributes: nil) } catch { print(error.localizedDescription) } } } //这两个方法也是会多次被调用的,作用是监听task运行结果 fileprivate func checkComplete(task : Process, complete : @escaping (Bool) -> Void) { Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(timerComplete(sender:)), userInfo: ["task" : task, "complete" : complete], repeats: true) } @objc private func timerComplete(sender : Timer) { let task : Process = (sender.userInfo as! Dictionary<String>)["task"] as! Process let complete : (Bool) -> Void = (sender.userInfo as! Dictionary<String>)["complete"] as! ((Bool) -> Void) if task.isRunning == false { sender.invalidate() if task.terminationStatus == 1 { complete(false) } else { complete(true) } } } //开始解包 fileprivate func unzip(complete :@escaping (Bool) -> Void) { let task = Process.init() task.launchPath = "/usr/bin/unzip" //这个record.ipaInputPath是我模型中的变量,ipa母包的绝对路径 大家可以把ipa丢到终端里面,就可以看到了 task.arguments = [ "-q", record.ipaInputPath, "-d", PathDefine.UnzipPath] let pi = Pipe.init() task.standardOutput = pi let file = pi.fileHandleForReading task.launch() let data = file.readDataToEndOfFile() print(String.init(data: data, encoding: .utf8) ?? "") self.checkComplete(task: task) { [unowned self] (result) in self.removeCodeSignature() //移除签名,内容在下面 complete(result) } }
2移除签名文件
为什么要移除签名文件,因为我们这个是二次签名,以前的签名文件肯定不能用了啊,而且如果不移除了会影响签名过程fileprivate func removeCodeSignature() { var appName = self.record.ipaInputPath appName = appName.substring(to: appName.index(appName.endIndex, offsetBy: -4)) //这里要说一下,解包后的目录Payload/下.app文件的文件名默认是你xcode里面被打包那个target的名字。因为我的工程名和target名不一样,所有要替换一下。 targetName变量就是我的target名字 if targetName.characters.count > 0 { appName = targetName } let appPath = PathDefine.UnzipPath + "/Payload/" + URL.init(fileURLWithPath: appName).lastPathComponent + ".app" let codeSignPath = appPath + "/" + OtherStringDefine.codeSignature let manager = FileManager.default if manager.fileExists(atPath: codeSignPath) { do { try manager.removeItem(atPath: codeSignPath) } catch { print(error.localizedDescription) } } }
3 编辑plist文件
fileprivate func editPlist(complete : (Bool) -> Void) { var appName = record.ipaInputPath appName = appName.substring(to: appName.index(appName.endIndex, offsetBy: -4)) if targetName.characters.count > 0 { appName = targetName } let plistPath = PathDefine.UnzipPath + "/Payload/" + URL.init(fileURLWithPath: appName).lastPathComponent + ".app/Info.plist" let plistDic = NSMutableDictionary.init(contentsOfFile: plistPath) if plistDic == nil { complete(false) return } //定义了一个delegate变量,因为我要打多个不同项目的包,所以他的delegate来实现不同的编辑逻辑,如果不知道的plist文件中每个key的名字,可以在这里把plistDic打印到控制台上 self.delegate?.editPlist(plistDic: plistDic!) //移除之前的plist文件 let manager = FileManager.default do { try manager.removeItem(atPath: plistPath) } catch { print(error.localizedDescription) } //把plist文件按照XML格式重新写入到目录下 do { let xmlData = try PropertyListSerialization.data(fromPropertyList: plistDic ?? "", format: .xml, options: 0) as NSData if xmlData.write(toFile: plistPath, atomically: true) == false { complete(false) return } } catch { print(error.localizedDescription) } complete(true) }
4替换icon和launchImage
fileprivate func replaceIconAndLaunchImage(complete :@escaping (Bool) -> Void) { //这里每个被替换的图片的名字都是你原来工程里面配置的名字,比如AppIcon29x29@2x.png,AppIcon就是我在Xcode-General-App icons And Launch Images-App Icons Source中配置的名字,launchImage同理 let destinationNameArray = ["AppIcon29x29@2x.png", "AppIcon29x29@3x.png", "AppIcon40x40@2x.png", "AppIcon40x40@3x.png", "AppIcon60x60@2x.png", "AppIcon60x60@3x.png", "LaunchImage-700@2x.png", "LaunchImage-700-568h@2x.png", "LaunchImage-800-667h@2x.png", "LaunchImage-800-Portrait-736h@3x.png"] let sourcePathArray = [record.icon58Path, record.icon87Path, record.icon80Path, record.icon120Path, record.icon120_2Path, record.icon180Path, record.launch4Path, record.launch5Path, record.launch6Path, record.launch6pPath] for index in 0..<sourcePathArray xss=removed xss=removed xss=removed> 0 { appName = targetName } let payloadPath = PathDefine.UnzipPath + "/Payload/" + URL.init(fileURLWithPath: appName).lastPathComponent + ".app" let destinationPath = payloadPath + "/" + destinationNameArray[index] if manager.fileExists(atPath: destinationPath) { do { try manager.removeItem(atPath: destinationPath) } catch { complete(false) print(error.localizedDescription) } do { try manager.copyItem(atPath: sourcePathArray[index], toPath: destinationPath) } catch { complete(false) print(error.localizedDescription) } } } complete(true) }
5编辑entitlement文件
entitlement文件文件里面存放了和签名有关的内容,是通过当前这个provisionFile文件和plist中的相关内容生成的private func createEntitlements(_ complete: @escaping (Bool) -> Void) { //删除entitlement文件 let entitlmentPath = PathDefine.UnzipPath + "/Entitlements.plist" let manager = FileManager.default if manager.fileExists(atPath: entitlmentPath) { do { try manager.removeItem(atPath: entitlmentPath) } catch { print(error.localizedDescription) } } let task = Process.init() task.launchPath = "/usr/bin/security" //record.provisionPath是我模型中ProvisonFile的路径,你可以替换你的ProvisionFile的绝对路径。但是,但是,但是,你plist中Bundle Identifier的值必须和你ProvisionFile指定的id相同,比如你ProvisionFile指定的id是com.366EC.test,那么你的plist中Bundle Identifier也必须是这个值,否则通不过security过程。另外ProvisionFile类型最好是发布类型,开发类型的我没测试过 task.arguments = ["cms", "-D", "-i", record.provisionPath] task.currentDirectoryPath = PathDefine.UnzipPath let pi = Pipe.init() task.standardOutput = pi let file = pi.fileHandleForReading task.launch() self.checkComplete(task: task) { [unowned self] (result) in if result == false { let errorString = String.init(data: file.readDataToEndOfFile(), encoding: .utf8) ?? "" complete(false) } else { //这个地方会得到一个security过的字符串,但是在Mac OS 10.10以上会报出“SecPolicySetValue”打头的一句话,这个必须去掉。此时调试台也会打印“security: SecPolicySetValue: One or more parameters passed to a function were not valid.”那是正常的,不管它 let data = file.readDataToEndOfFile() var entitlementsResult = String.init(data: data, encoding: .ascii) ?? "" if entitlementsResult.contains("SecPolicySetValue") { var inOutput : Array<String> = entitlementsResult.components(separatedBy: "\n") inOutput.remove(at: 0) entitlementsResult = inOutput.joined(separator: "\n") } let entitlementData = entitlementsResult.data(using: .utf8)! do { var entitlementDic = try PropertyListSerialization.propertyList(from: entitlementData, options: PropertyListSerialization.ReadOptions.mutableContainers, format: nil) as! Dictionary<String> entitlementDic = entitlementDic["Entitlements"] as! Dictionary<String> //entitlementDic中有一个key为OtherStringDefine.appid的字段必须改为"teamid.bundlId"这种格式,teamID就是你证书后面括号中那一串,没有括号的去你的开发者帐号里面查,每个证书都有一个teamID entitlementDic.updateValue(String.init(format: "%@.%@", self.record.teamId, self.record.bid), forKey: OtherStringDefine.appid) //移除无用的字段 entitlementDic.removeValue(forKey: OtherStringDefine.kTeamIdentifier) entitlementDic.removeValue(forKey: OtherStringDefine.kKeychainAccessGroups) //和plist文件一样,转换为XML文件重新写入原来的目录中 do { let xmlData = try PropertyListSerialization.data(fromPropertyList: entitlementDic, format: .xml, options: 0) as NSData if xmlData.write(toFile: entitlmentPath, atomically: true) == false { complete(false) return } } catch { print(error.localizedDescription) } } catch { print(error.localizedDescription) } complete(true) } } }
6替换Provision文件
这里要注意一下,放入打包目录里面的provision文件的名字必须是embedded.mobileprovisionprivate func editProvisionFile(_ complete: @escaping (Bool) -> Void) { //替换provisioning let manager = FileManager.default var appName = record.ipaInputPath appName = appName.substring(to: appName.index(appName.endIndex, offsetBy: -4)) if targetName.characters.count > 0 { appName = targetName } let payloadPath = PathDefine.UnzipPath + "/Payload/" + URL.init(fileURLWithPath: appName).lastPathComponent + ".app" let appProfilePath = payloadPath + "/embedded.mobileprovision" if manager.fileExists(atPath: appProfilePath) { do { try manager.removeItem(atPath: appProfilePath) } catch { print(error.localizedDescription) } } //这里用了Process来做拷贝,其实用FileManager也是一样的,我之前遇到一些问题,总以为是因为使用FileManager造成的权限问题,但最后通过对比实验,发现不是这里造成的 let task = Process.init() task.launchPath = "/bin/cp" task.arguments = [record.provisionPath, appProfilePath] let pi = Pipe.init() task.standardOutput = pi let file = pi.fileHandleForReading task.launch() let data = file.readDataToEndOfFile() print(String.init(data: data, encoding: .utf8) ?? "") self.checkComplete(task: task) { (result) in complete(result) } }
7重签名
// 在.app目录会有一些图片缓存,必须清理一下,否则不能通过签名 private func removeUnrealizePath(_ complete: @escaping (Bool) -> Void) { let task = Process.init() task.launchPath = "/usr/bin/xattr" var appName = record.ipaInputPath appName = appName.substring(to: appName.index(appName.endIndex, offsetBy: -4)) if targetName.characters.count > 0 { appName = targetName } let appPath = PathDefine.UnzipPath + "/Payload/" + URL.init(fileURLWithPath: appName).lastPathComponent + ".app" task.arguments = [ "-cr", appPath] // task.currentDirectoryPath = appPath let pi = Pipe.init() task.standardOutput = pi let file = pi.fileHandleForReading task.launch() let data = file.readDataToEndOfFile() // print(String.init(data: data, encoding: .utf8)) self.checkComplete(task: task) { (result) in complete(result) } } private func doCodeSign(_ complete: @escaping (Bool) -> Void) { self.removeUnrealizePath { [unowned self] (result) in var appName = self.record.ipaInputPath appName = appName.substring(to: appName.index(appName.endIndex, offsetBy: -4)) if self.targetName.characters.count > 0 { appName = self.targetName } let appPath = PathDefine.UnzipPath + "/Payload/" + URL.init(fileURLWithPath: appName).lastPathComponent + ".app" var task = Process.init() task.launchPath = "/bin/sh" task.currentDirectoryPath = PathDefine.UnzipPath //这里是在解压目录下寻找需要加签的文件,并放入directories.txt中。这里用;\n来分隔连续执行的命令,用\n来分隔一条命令中需要换行的部分 var cmdString = "find -d " + PathDefine.UnzipPath + " \\( -name \"*.app\" -o -name \"*.appex\" -o -name \"*.framework\" -o -name \"*.dylib\" \\) > directories.txt;\n" //把刚才那个directories.txt中得到的文件名字取出来,依次签名。self.record.certificate是你证书的名字,不是证书路径,在钥匙串里面选中证书后点显示简介就能看到 cmdString = cmdString.appendingFormat("while IFS='' read -r line || [[ -n \"$line\" ]]; do \n /usr/bin/codesign --continue -f -s \"%@\" --entitlements \"Entitlements.plist\" \"$line\" \n done < directories xss=removed xss=removed xss=removed xss=removed xss=removed xss=removed xss=removed> Void) { let task = Process.init() let ipaOutputPath = PathDefine.outputPath + "/" + record.appName + ".ipa" task.launchPath = "/usr/bin/zip" task.arguments = ["-qry", ipaOutputPath, "Payload/"] task.currentDirectoryPath = PathDefine.UnzipPath let pi = Pipe.init() task.standardOutput = pi let file = pi.fileHandleForReading task.launch() let data = file.readDataToEndOfFile() print(String.init(data: data, encoding: .utf8) ?? "") self.checkComplete(task: task) { [unowned self] (result) in if result == false { complete(false) } else { //这里就是把解包的目录删了 self.clear({ (result) in if result == false { print("清理缓存失败") } complete(true) }) } } }
如果所有的命令都执行完了,并且都成功了,那么可把find调出来并打开输出的目录
NSWorkspace.shared().openFile(PathDefine.outputPath, withApplication: "Finder")
写在最后,整理这个重新打包的教程花了我一个周的时间,其中有的时间花在了制作Mac客户端的界面和学习shell命令上,而且在使用的Process的时候也遇到很多问题。此外,重签名的步骤不能乱,目前这个顺序就是经过我反复验证的了,文中出现了一些外面传进来的变量,比如record变量,它的属性名的字面意思已经再清楚不过了,这里就不再一一说明
相关文章推荐
- 使用shell脚本实现客户端应用自动化打包——mac
- ios无线方式安装应用程序-苹果企业证书打包的客户端如何使用plist下载
- ios 在window和mac上另类打包方式
- ios 在window和mac上另类打包方式
- ios中发短信功能实现的几种方式
- iOS学习笔记02——以编码的方式实现Auto Layout自动布局(一)
- iOS开发之第三方登录微信-- 史上最全最新第三方登录微信方式实现
- 【HELLO WAKA】WAKA iOS客户端 之二 架构设计与实现篇
- 最简洁的方式(最少的代码)在Android上实现IOS的switch button
- Android Studio使用Gradle实现自动打包,签名,自定义apk文件名,多渠道打包,集成系统签名证书【附效果图附源码】
- IOS客户端实现RSA加密
- iOS KVO 观察者模式实现方式
- ios 实现动画的几种方式
- Unity中针对Android Apk的签名验证(C#实现),防止二次打包
- Mac下maven项目打包方式
- Java 实现 SSH 协议的客户端登录认证方式
- ios 安卓 打包 (mac&&window) 含 Quick 打包
- Ad_Hoc方式打包iOS应用程序
- iOS监听耳机插拔的不使用系统通知实现的一种方式
- 推送通知iOS客户端编写实现及推送服务器端编写