├── README.md ├── addDevice.rb ├── checkUnusedClassOrMethod.rb ├── copyImages.rb ├── createLanguageFile.rb ├── findXibUse.rb ├── fixImageAssetName.rb ├── fixImportHeader.rb ├── getDumpImageFiles.rb ├── languages.xlsx ├── linkmapCompare.rb └── podlockDependencyParse.rb /README.md: -------------------------------------------------------------------------------- 1 | # rubyTools iOS常用脚本 2 | ## 日常使用ruby脚本 3 | 4 | ### 1.[checkUnusedClassOrMethod.rb](https://github.com/iPermanent/rubyTools/blob/master/checkUnusedClassOrMethod.rb) 5 | 检测二进制文件内未使用到的类和方法,检测结果请人工确认再删除,出现P0事故概不负责 6 | 7 | ### 2.[copyImages.rb](https://github.com/iPermanent/rubyTools/blob/master/copyImages.rb) 8 | 在由整体项目向组件化抽离时,使用到的图片的资源统一拷贝 9 | 10 | ### 3.[createLanguageFile.rb](https://github.com/iPermanent/rubyTools/blob/master/createLanguageFile.rb) 11 | 根据excel里的多语言,自动生成iOS的多语言格式文本,请自行根据languages.xlsx 格式和自己的格式进行调整修改 12 | 13 | ### 4.[findXibUse.rb](https://github.com/iPermanent/rubyTools/blob/master/findXibUse.rb) 14 | 查找项目中未使用到的xib文件 15 | 16 | ### 5.[fixImageAssetName.rb](https://github.com/iPermanent/rubyTools/blob/master/fixImageAssetName.rb) 17 | 一键修复imageAsset里,使用的asset名和内部图片名不一致的问题,保持一致性 18 | 19 | ### 6.[fixImportHeader.rb](https://github.com/iPermanent/rubyTools/blob/master/fixImportHeader.rb) 20 | 一键修复不规范的头文件引入问题,如 #import "AFNetworking.h" / #import => #import 21 | 22 | ### 7.[getDumpImageFiles.rb](https://github.com/iPermanent/rubyTools/blob/master/getDumpImageFiles.rb) 23 | 获取项目里重复的图片(md5实际结果为维度判断) 24 | 25 | ### 8.[linkmapCompare.rb](https://github.com/iPermanent/rubyTools/blob/master/linkmapCompare.rb) 26 | 比较两个linkmap,生成一个json结果文件,内部有各库的大小比较和对应的类大小比较,方便分析app版本升级以后各组件的大小增减问题 27 | 28 | ### 9.[podlockDependencyParse.rb](https://github.com/iPermanent/rubyTools/blob/master/podlockDependencyParse.rb) 29 | 通过Podfile.lock 分析各组件的依赖关系,生成一个可视化依赖关系图 30 | 31 | ### 10.[addDevice.rb](https://github.com/iPermanent/rubyTools/blob/master/addDevice.rb) 32 | 通过本地的文件批量向苹果开发者账号里添加设备 33 | -------------------------------------------------------------------------------- /addDevice.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # -*- coding: UTF-8 -*- 3 | 4 | require "spaceship" 5 | 6 | def readDeviceList 7 | #开始登录 8 | appleAccount = nil 9 | while appleAccount == nil || appleAccount.length == 0 10 | puts("请输入开发者账号") 11 | appleAccount = gets.to_s.gsub("\n","") 12 | end 13 | 14 | applePassword = nil 15 | while applePassword == nil || applePassword.length == 0 16 | puts("请输入开发者账号的密码") 17 | applePassword = gets.to_s.gsub("\n","") 18 | end 19 | 20 | puts("开始登录中... |#{appleAccount}|#{applePassword}|") 21 | 22 | #### 如果懒得输入,注释掉以上内容,在下方填入自己的账号密码即可 23 | # appleAccount = "" 24 | # applePassword = "" 25 | puts("开始登录中... #{appleAccount}") 26 | Spaceship.login(appleAccount, applePassword) 27 | puts "登录成功,选择所属team" 28 | 29 | teamIds = Spaceship.select_team 30 | 31 | deviceListTextFile = nil 32 | while deviceListTextFile == nil or deviceListTextFile.length == 0 33 | puts "输入txt文件,可直接将文件拖至终端" 34 | txtPath = gets.to_s.gsub("\n","").gsub("\t","").gsub(" ","") 35 | if File.exist?(txtPath) 36 | deviceListTextFile = txtPath 37 | else 38 | puts "文件不存在,请重新输入" 39 | end 40 | end 41 | 42 | #本地的文件路径 43 | file = File.open(deviceListTextFile) #文本文件里录入的udid和设备名用tab分隔 44 | file.each do |line| 45 | arr = line.split(" | ") 46 | device = Spaceship.device.create!(name: arr[1], udid: arr[0]) 47 | puts "add device: #{device.name} #{device.udid} #{device.model}" 48 | end 49 | 50 | devices = Spaceship.device.all 51 | 52 | profiles = Array.new 53 | profiles += Spaceship.provisioning_profile.development.all 54 | profiles += Spaceship.provisioning_profile.ad_hoc.all 55 | 56 | puts("有以下provision,请选择需要的进行更新,输入前面的序号即可,如需多个请以英文逗号隔开,如1,2,3,4,如果需要更新所有的provision请输入all") 57 | profiles.each do |p| 58 | puts("#{profiles.find_index(p)} #{p.name}") 59 | end 60 | 61 | dealProfiles = nil 62 | while dealProfiles == nil or dealProfiles.length == 0 63 | puts "请选择需要更新的provision文件,以序号表示,用英文逗号隔开" 64 | 65 | dealProfiles = gets.to_s.gsub("\n","") 66 | end 67 | 68 | needDownloadProfileUuids = Array.new 69 | 70 | if dealProfiles == "all" 71 | #更新全部配置文件 72 | puts "开始更新全部配置文件" 73 | profiles.each do |p| 74 | needDownloadProfileUuids.append(p.name) 75 | puts "Updating #{p.name}" 76 | p.devices = devices 77 | p.update! 78 | end 79 | 80 | downloadProfiles(needDownloadProfileUuids) 81 | else 82 | puts "开始更新指定配置文件 #{dealProfiles.split(",")}" 83 | needUpdateProfiles = Array.new 84 | indexes = dealProfiles.split(",") 85 | 86 | #取需要更新的配置文件列表 87 | indexes.each do |idx| 88 | profile = profiles[idx.to_i] 89 | needUpdateProfiles.append(profile) 90 | needDownloadProfileUuids.append(profile.name) 91 | end 92 | 93 | #开始更新配置文件 94 | needUpdateProfiles.each do |p| 95 | puts "update profiles #{p.name}" 96 | p.devices = devices 97 | p.update! 98 | end 99 | 100 | downloadProfiles(needDownloadProfileUuids) 101 | end 102 | end 103 | 104 | def downloadProfiles(names) 105 | profiles = Array.new 106 | profiles += Spaceship.provisioning_profile.development.all 107 | profiles += Spaceship.provisioning_profile.ad_hoc.all 108 | 109 | puts "开始下载provision文件 #{names}" 110 | 111 | downloadFolder = __dir__ + '/Provisions/' 112 | #防止没有目录时报错,需要创建一下目录 113 | mkDirCmdStr = "mkdir -p #{downloadFolder}" 114 | system(mkDirCmdStr) 115 | profiles.each do |p| 116 | if names.include?(p.name) 117 | puts "Downloading profile #{p.name}" 118 | File.write("#{downloadFolder}#{p.name}.mobileprovision", p.download) 119 | end 120 | end 121 | 122 | puts "下载完毕,请在目录 #{downloadFolder} 里找到并双击安装provision文件" 123 | system("open #{downloadFolder}") 124 | end 125 | 126 | #读取设备列表 127 | readDeviceList 128 | 129 | -------------------------------------------------------------------------------- /checkUnusedClassOrMethod.rb: -------------------------------------------------------------------------------- 1 | ## usage: ruby checkUnusedClassOrMethod.rb 【optioal】machofilePath [-c][-s](means class or selector) 2 | ## need to install snake,please run 'brew install snake' in terminal first 3 | ## 4 | require 'json' 5 | 6 | def parseUnusedClass 7 | txtDir = __dir__ + "/" + "classUnused.txt" 8 | 9 | machOFilePath = ARGV[0] 10 | if !machOFilePath 11 | puts("please specfic a macho file path...") 12 | return 13 | end 14 | 15 | unusedClassCmd = 'snake -c -j ' + machOFilePath + ' > ' + txtDir 16 | snakeResult = system(unusedClassCmd) 17 | if !snakeResult 18 | puts("脚本调用失败,不妨试试 brew install snake ?") 19 | return 20 | else 21 | puts("执行结束,开始分析筛选") 22 | end 23 | 24 | 25 | filtArray = Array.new() 26 | 27 | text = File.read(txtDir).gsub("\"","").gsub("[","").gsub("]","") 28 | 29 | classArray = text.split(",") 30 | 31 | for className in classArray 32 | filtArray.push(className) 33 | end 34 | 35 | writeContent = filtArray.join(",\n") 36 | 37 | logFilePath = __dir__ + "/classUnusedWithFilter.txt" 38 | if !Dir.exists?(logFilePath) 39 | File.new(logFilePath,'w') 40 | end 41 | File.open(logFilePath, "w") do |f| 42 | f.write(writeContent) 43 | end 44 | File.delete(txtDir) if File.exist?(txtDir) 45 | puts("分析结束,请到:" + logFilePath + " 查看最终分析结果") 46 | end 47 | 48 | #针对单个结果分析 49 | def filterClassStructor(classMethodStructMap) 50 | #set方法排除,同样,有get的方法也要排除 51 | setSelectorsArray = Array.new() 52 | normalSelectorArray = Array.new() 53 | 54 | allSelectorsArray = classMethodStructMap["selectors"] 55 | 56 | #分出set方法和非方法 57 | for functionName in allSelectorsArray 58 | functionActureName = functionName.split(" ")[1].gsub("]","") 59 | #是set方法,当有多个:说明是多个参数,不属于set方法 60 | if functionActureName.start_with?("set") and functionActureName.end_with?(":") and functionActureName.split(":").count == 1 61 | #setAbc: => abc set向get方法转换 62 | getFunctionName = functionActureName.split(":")[0].gsub("set","").downcase 63 | setSelectorsArray.push(getFunctionName) 64 | else 65 | normalSelectorArray.push(functionActureName) 66 | end 67 | end 68 | 69 | #相减即为非get也非set方法 70 | normalSelectorArray = normalSelectorArray - setSelectorsArray 71 | 72 | finalResultMaps = Hash.new() 73 | if normalSelectorArray.count > 0 74 | className = classMethodStructMap["class"] 75 | finalResultMaps[className] = normalSelectorArray 76 | end 77 | 78 | return finalResultMaps 79 | end 80 | 81 | def parseUnusedSelector 82 | txtDir = __dir__ + "/" + "methodUnused.txt" 83 | 84 | machOFilePath = ARGV[0] 85 | if !machOFilePath 86 | puts("please specfic a macho file path...") 87 | return 88 | end 89 | 90 | unusedClassCmd = 'snake -s -j ' + machOFilePath + ' > ' + txtDir 91 | snakeResult = system(unusedClassCmd) 92 | if !snakeResult 93 | puts("脚本调用失败,不妨试试 brew install snake ?") 94 | return 95 | else 96 | puts("执行结束,开始分析筛选") 97 | end 98 | 99 | jsonText = File.read(txtDir) 100 | selectorStruct = JSON.parse(jsonText) 101 | 102 | finalFilterMap = Array.new() 103 | 104 | for methodMap in selectorStruct 105 | filterMap = filterClassStructor(methodMap) 106 | if !filterMap.empty?() 107 | className = filterMap.keys[0] 108 | finalFilterMap.push(filterMap) 109 | end 110 | end 111 | 112 | separateResult = Hash.new() 113 | separateResult["ClassUnUsedMethodResult"] = finalFilterMap 114 | 115 | jsonDir = __dir__ + "/" + "classMethodUnused.json" 116 | File.write(jsonDir,JSON.pretty_generate(separateResult)) 117 | File.delete(txtDir) if File.exist?(txtDir) 118 | puts("分析结果已输出至:" + jsonDir + " 请打开查看") 119 | end 120 | 121 | def startParseMachO 122 | machOFilePath = ARGV[0] 123 | parseWay = ARGV[1] 124 | 125 | if !machOFilePath or machOFilePath.length == 0 126 | puts("请指定machO文件路径 useage .rb xxxx/xxx/xx") 127 | return 128 | end 129 | 130 | if !parseWay or parseWay.length == 0 131 | puts("指定方式 -s为分析无用方法 -c为分析无用类") 132 | return 133 | end 134 | 135 | if parseWay == '-s' 136 | parseUnusedSelector() 137 | elsif parseWay == '-c' 138 | parseUnusedClass() 139 | puts("请谨慎删除相关类,特别是在很多类里使用反射方式,通过string拿到class时,mach-O文件内未关联此种引用造成误报") 140 | end 141 | 142 | end 143 | 144 | startParseMachO 145 | -------------------------------------------------------------------------------- /copyImages.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # -*- coding: UTF-8 -*- 3 | # author zhangheng 2019-08-21 4 | 5 | require 'fileutils' 6 | 7 | #查找代码和xib中引用到的图片名 8 | def findUsedImages 9 | projectName = File.basename(Dir.getwd) 10 | puts("使用当前脚本的路径") 11 | projectDir = Dir.getwd + "/" + projectName 12 | 13 | imageResults = Array.new() 14 | Dir.glob(projectDir + "/**/**/**/**.{h,m}").each do |name| 15 | next if Dir.exists? name 16 | text = File.read(name) 17 | 18 | pattern = /UIImage imageNamed:@"\S+\"/ 19 | results = text.scan(pattern) 20 | for result in results 21 | headerSuffix = result.gsub("UIImage imageNamed:@\"","") 22 | finalImageName = headerSuffix.gsub("\"",""); 23 | imageResults.push(finalImageName) 24 | end 25 | end 26 | 27 | # xibImageResults = Array.new() 28 | Dir.glob(projectDir + "/**/**/**/**.{xib}").each do |xibname| 29 | next if Dir.exists? xibname 30 | text = File.read(xibname) 31 | 32 | xibpattern = /image="\S+\"/ 33 | xibresults = text.scan(xibpattern) 34 | for xibresult in xibresults 35 | xibprefix = xibresult.gsub("image=\"","") 36 | xibImageName = xibprefix.gsub("\"",""); 37 | imageResults.push(xibImageName) 38 | end 39 | end 40 | uniqImages = imageResults.uniq() 41 | copyImages(uniqImages) 42 | end 43 | 44 | #通过图片名遍历主工程iamgeAssets 45 | def copyImages(needCopyImageNames) 46 | mainProjectDir = "对应imageAssets目录" 47 | Dir.glob(mainProjectDir + "/**/**.{png}").each do |assetName| 48 | next if Dir.exists? assetName 49 | dirArr = assetName.split("/") 50 | dirCount = dirArr.count() 51 | assetFolderName = dirArr[dirCount - 2] 52 | # puts("asset名为" + assetFolderName) 53 | 54 | needCopyResources = Array.new() 55 | for imageName in needCopyImageNames 56 | if assetFolderName.eql? (imageName + ".imageset") 57 | currentFolderName = File.expand_path("..", assetName) 58 | needCopyResources.push(currentFolderName) 59 | end 60 | end 61 | 62 | #因为2倍图和3倍图都会被遍历到,所以需要做去重处理 63 | finallyResouces = needCopyResources.uniq() 64 | destFolderName = Dir.getwd + "/Images.xcassets/" 65 | for resouceFolder in finallyResouces 66 | puts("执行copy asset :" + resouceFolder + " => " + destFolderName) 67 | FileUtils.cp_r(resouceFolder,destFolderName) 68 | end 69 | 70 | end 71 | end 72 | 73 | findUsedImages 74 | -------------------------------------------------------------------------------- /createLanguageFile.rb: -------------------------------------------------------------------------------- 1 | require 'roo' 2 | 3 | # 打开excel 4 | xlsx = Roo::Spreadsheet.open(Dir.getwd + '/languages.xlsx') 5 | 6 | #行数据和列数据 7 | itemColums = xlsx.sheet('Foglio1').row(2) 8 | itemRows = xlsx.sheet('Foglio1').column(2) 9 | 10 | #列数因为第一列为空所以从2开始  11 | $i = 3 12 | $num = itemColums.count() 13 | 14 | #第二列的为多语言的key字段 15 | firstColumKeys = xlsx.sheet('Foglio1').column($i - 1) 16 | 17 | fileContent = "" 18 | while $i < $num do 19 | puts("列数:" + String($i) + "数据为:\n") 20 | itemColum = xlsx.sheet('Foglio1').column($i) 21 | 22 | #第一列一般为语言名字 23 | regionName = itemColum[0] 24 | 25 | fileContent = "" 26 | for key in firstColumKeys 27 | if key && key.gsub(" ","").length > 0 28 | index = firstColumKeys.find_index(key) 29 | value = itemColum[index] 30 | if !value 31 | value = "" 32 | end 33 | value = value.gsub("\"","") 34 | rowValue = "\'" + key + "\' = \'" + value + "\'" + ";\n" 35 | fileContent = fileContent + rowValue 36 | end 37 | end 38 | 39 | puts("保存路径为:\n") 40 | puts(Dir.getwd + "/" + regionName + ".txt") 41 | puts("文件内容为:\n") 42 | puts(fileContent) 43 | File.open(Dir.getwd + "/" + regionName + ".txt", 'w') { |file| file.write(fileContent) } 44 | 45 | $i +=1 46 | end 47 | -------------------------------------------------------------------------------- /findXibUse.rb: -------------------------------------------------------------------------------- 1 | def findXibs 2 | projectName = File.basename(Dir.getwd) 3 | projectDir = Dir.getwd + "/" + projectName 4 | 5 | xibResults = Array.new() 6 | Dir.glob(projectDir + "/**/**/**/**.{xib}").each do |name| 7 | next if Dir.exists? name 8 | 9 | filePaths = name.split("/") 10 | 11 | xibFile = filePaths[filePaths.count - 1] 12 | xibName = xibFile.gsub(".xib","") 13 | 14 | xibResults.push(xibName) 15 | end 16 | # puts(xibResults) 17 | return xibResults 18 | end 19 | 20 | def findCoponentXibUsageInMainPrj(xibFileClassName) 21 | #修改路径为自己需检测的主工程目录 22 | mainProjectDir = "主工程目录" 23 | xibUsedArray = Array.new() 24 | 25 | #使用xib必须会用到xib的名字,可能有以下三种方式使用,搜索出来以后就行 26 | xibUsePattern = /\"#{xibFileClassName}\"/ 27 | xibOtherUsePattern = /NSStringFromClass(#{xibFileClassName}.class)/ 28 | xibThirdUsePattern = /NSStringFromClass(\[#{xibFileClassName} class\])/ 29 | 30 | Dir.glob(mainProjectDir + "/**/**/**.{m}").each do |ocmFile| 31 | next if Dir.exists? ocmFile 32 | text = File.read(ocmFile) 33 | 34 | xibresults = text.scan(xibUsePattern) 35 | 36 | xibresults += text.scan(xibOtherUsePattern) 37 | 38 | xibresults += text.scan(xibThirdUsePattern) 39 | 40 | if(xibresults.count > 0) 41 | puts("xib used in file" + ocmFile) 42 | puts(xibresults) 43 | end 44 | end 45 | end 46 | 47 | def startFind 48 | xibFiles = findXibs 49 | for xibFile in xibFiles do 50 | findCoponentXibUsageInMainPrj(xibFile) 51 | end 52 | end 53 | 54 | startFind 55 | -------------------------------------------------------------------------------- /fixImageAssetName.rb: -------------------------------------------------------------------------------- 1 | #Author HenryZhang 2021/09/14 2 | 3 | 4 | #通过路径判断是否为asset下的,取asset名字 5 | def getAssetImageName(imagePath) 6 | #说明是在asset目录下的 7 | splitRes = imagePath.split(".imageset/") 8 | if splitRes.count == 2 9 | prefixPath = splitRes[0] 10 | assetPaths = prefixPath.split("/") 11 | assetName = assetPaths[-1] 12 | 13 | return assetName 14 | end 15 | 16 | return "" 17 | end 18 | 19 | def getAllNeedFixImages 20 | #当前路径 21 | projectName = File.basename(__dir__) 22 | projectDir = __dir__ + "/" + projectName 23 | 24 | allImages = Array.new() 25 | 26 | #遍历所有格式图片 27 | puts("需要修复的图片名有") 28 | Dir.glob(projectDir + "/**/**/**/**/**.{jpg,png,bmp}").each do |imageFilePath| 29 | next if Dir.exists? imageFilePath 30 | 31 | #寻找在asset中的图片 32 | imageAssetName = getAssetImageName(imageFilePath) 33 | imageRealtName = imageFilePath.split("/")[-1].split("@")[0] 34 | #如果asset名和图片实际名字不同,对文件名进行修改 35 | if imageAssetName.length > 0 and imageRealtName != imageAssetName 36 | puts("realName: " + imageRealtName) 37 | puts("assetName: " + imageAssetName) 38 | 39 | #获取目录 40 | folderPath = File.dirname(imageFilePath) 41 | #对图片进行重命名 42 | oldImageFileName = imageFilePath.split("/")[-1] 43 | newFileName = oldImageFileName.gsub(imageRealtName,imageAssetName) 44 | 45 | newImageFilePath = folderPath + '/' + newFileName 46 | File.rename(imageFilePath,newImageFilePath) 47 | 48 | #别忘了还有json文件需要修改 49 | jsonFilePath = folderPath + '/Contents.json' 50 | jsonContent = File.read(jsonFilePath) 51 | 52 | #需要替换的字符串 53 | oriString = "\"filename\" : \"" + oldImageFileName + "\"" 54 | 55 | puts('原字符串 ' + oriString) 56 | 57 | imgSuffixString = oldImageFileName.include?("@") ? oldImageFileName.split("@")[1] : oldImageFileName 58 | 59 | targetString = "\"filename\" : \"" + imageAssetName + "@" + imgSuffixString + "\"" 60 | 61 | puts("需替换的字符串 " + targetString) 62 | 63 | jsonContent = jsonContent.gsub(oriString,targetString) 64 | 65 | #执行json里的图片名修复 66 | File.open(jsonFilePath, "w") { |file| file.puts jsonContent } 67 | puts("json文件内容") 68 | puts(jsonContent) 69 | 70 | puts("重命名完成") 71 | 72 | end 73 | 74 | end 75 | end 76 | 77 | getAllNeedFixImages() 78 | -------------------------------------------------------------------------------- /fixImportHeader.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # -*- coding: UTF-8 -*- 3 | # author zhangheng 4 | #功能,一次性自动替换 使用双引号导入的头文件为尖括号 5 | #用法,放入组件根目录,直接ruby fixImportHeader.rb即可, 6 | #但是注意,一定要是在组件正常引入所有的依赖并且pod install以后 7 | #因为寻找header所在的pods需要pod install 成功,才能根据结构查找到header所属模块 8 | 9 | @cache_pod_maps 10 | 11 | def replace_action 12 | #获取当前工程目录,一般主工程目录里还有一层同名的,如果需要可以自行修改 13 | projectName = File.basename(Dir.getwd) 14 | projectDir = Dir.getwd + "/" + projectName 15 | 16 | Dir.glob(projectDir + "/**/**/**/**").each do |name| 17 | next if Dir.exists? name 18 | text = File.read(name) 19 | for find, replace in @string_replacements 20 | text = text.gsub(find, replace) 21 | end 22 | 23 | File.open(name, "w") { |file| file.puts text } 24 | end 25 | end 26 | 27 | #查找引入方式不正确的header 28 | def findFixImportHeaders 29 | @cache_pod_maps = Hash.new 30 | projectName = File.basename(Dir.getwd) 31 | projectDir = Dir.getwd + "/" + projectName 32 | 33 | Dir.glob(projectDir + "/**/**/**/**.{h,m}").each do |name| 34 | next if Dir.exists? name 35 | text = File.read(name) 36 | 37 | #以import "xx.h"的引入 38 | pattern = /#import \S+\.h"/ 39 | results = text.scan(pattern) 40 | 41 | #以#import开头.h>结尾的正则 42 | pattern2 = /#import \S+\.h>/ 43 | results2 = text.scan(pattern2) 44 | 45 | hasContentNeedFix = false 46 | 47 | for result in results 48 | #取后面的xxx.h 49 | headerSuffix = result.split("\"")[1].gsub("\"","") 50 | podFolderName = getPodName(headerSuffix) 51 | if !podFolderName.nil? 52 | headerSuffix = headerSuffix.gsub("\"","") 53 | toReplaceImport = "#import <" + podFolderName + "/" + headerSuffix + ">" 54 | 55 | #执行替换 56 | text = text.gsub(result,toReplaceImport) 57 | puts("执行替换" + result + " => " + toReplaceImport) 58 | hasContentNeedFix = true 59 | end 60 | end 61 | 62 | for result2 in results2 63 | #import 此种方式 64 | if result2.split("/").count == 1 65 | #如果不包含/说明是不规范的导入需要修改 66 | headerFileName = result2.split("<")[1].gsub(">","") 67 | podName = getPodName(headerFileName) 68 | 69 | if !podName.nil? 70 | replaceImportString = "#import <" + podName + "/" + headerFileName + ">" 71 | text = text.gsub(result2,replaceImportString) 72 | 73 | puts("执行替换" + result2 + " => " + replaceImportString) 74 | hasContentNeedFix = true 75 | end 76 | end 77 | end 78 | 79 | if hasContentNeedFix 80 | puts("开始写文件=> " + name.gsub(projectDir,"")) 81 | File.open(name, "w") { |file| file.puts text } 82 | end 83 | 84 | 85 | end 86 | puts("替换完成") 87 | end 88 | 89 | def getPodName(headerFileName) 90 | if @cache_pod_maps[headerFileName] != nil 91 | return @cache_pod_maps[headerFileName] 92 | end 93 | 94 | podDir = Dir.getwd + "/Example/Pods" 95 | headerDir = podDir + "/Headers" 96 | Dir.glob(podDir + "/**/**/**/**.{h}").each do |name| 97 | next if Dir.exists? name 98 | if !name.start_with?(headerDir) && name.end_with?("/" + headerFileName) 99 | tempDir = name.gsub(podDir + "/","") #删除掉前方pod路径方便操作 100 | podFolder = tempDir.split("/")[0]#通过取到 pod的第一层目录为pod名 101 | 102 | @cache_pod_maps[headerFileName] = podFolder 103 | 104 | return podFolder 105 | end 106 | end 107 | #默认给出nil返回值 108 | return nil 109 | end 110 | 111 | findFixImportHeaders 112 | -------------------------------------------------------------------------------- /getDumpImageFiles.rb: -------------------------------------------------------------------------------- 1 | require 'digest/md5' 2 | 3 | def getOverageSizeImages 4 | projectDir = Dir.getwd 5 | imageHashs = Hash.new() 6 | 7 | Dir.glob(projectDir + "/**/**/**/**/**/**/**.{jpg,png,bmp}").each do |imageFilePath| 8 | next if Dir.exists? imageFilePath 9 | imageFileSize = File.size(imageFilePath) 10 | #输出大于50K的图片 11 | if imageFileSize > 50000 12 | puts(imageFilePath + " 此图片可能需要进行压缩,图片大小:" + String(imageFileSize)) 13 | end 14 | 15 | imagemd5 = Digest::MD5.file(imageFilePath).hexdigest 16 | imagePath = imageHashs[imagemd5] 17 | 18 | if imagePath 19 | imageHashs[imagemd5] = imagePath + ',' + imageFilePath 20 | else 21 | imageHashs[imagemd5] = imageFilePath 22 | end 23 | end 24 | 25 | for imageMd5 in imageHashs.keys 26 | imagePath = imageHashs[imageMd5] 27 | if imagePath.split(',').count > 1 28 | puts("以下图片为重复图片") 29 | puts(imagePath.split(',')) 30 | end 31 | end 32 | end 33 | 34 | getOverageSizeImages 35 | -------------------------------------------------------------------------------- /languages.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iPermanent/rubyTools/9cc034c74adb482eb4f721ae0d5d5f5abb366944/languages.xlsx -------------------------------------------------------------------------------- /linkmapCompare.rb: -------------------------------------------------------------------------------- 1 | #用法 ruby linkmapCompare.rb xxx-linkmap.txt yyy-linkmap.txt 即可以同目录下生成一个结果的json文件 2 | 3 | require 'json' 4 | 5 | class LinkMapParser 6 | def buildResultWithSymbols(symbols) 7 | result = "" 8 | for symbol in symbols 9 | result = result + symbol["size"].to_s + "\t" + symbol["file"].split("/")[-1] + "\r\n" 10 | end 11 | 12 | return result 13 | end 14 | 15 | def symbolMapFromContent(content) 16 | symbolMap = Hash.new() 17 | 18 | lines = content.split("\n") 19 | 20 | reachFiles = false 21 | reachSymbols = false 22 | reachSections = false 23 | 24 | hasPrintMap = false 25 | 26 | for line in lines 27 | if line.start_with?("#") 28 | if line.start_with?("# Object files:") 29 | reachFiles = true 30 | elsif line.start_with?("# Sections:") 31 | reachSections = true 32 | elsif line.start_with?("# Symbols:") 33 | reachSymbols = true 34 | end 35 | else 36 | if reachFiles == true && reachSections == false && reachSymbols == false 37 | range = line.rindex("]") 38 | if range != nil 39 | symModel = Hash.new() 40 | symModel["file"] = line[range+1,line.length - range-1] 41 | key = line[0,range+1] 42 | symbolMap[key] = symModel 43 | end 44 | elsif reachFiles == true && reachSections == true && reachSymbols == true 45 | if !hasPrintMap 46 | # puts(JSON.pretty_generate(symbolMap).gsub(":", " =>")) 47 | hasPrintMap = true 48 | end 49 | symbolsArray = line.split("\t") 50 | if symbolsArray.count == 3 51 | fileKeyAndName = symbolsArray[2] 52 | #16进制转换 53 | size = symbolsArray[1].to_i(16) 54 | 55 | range = fileKeyAndName.index("]") 56 | if range != nil 57 | key = fileKeyAndName[0,range+1] 58 | symbol = symbolMap[key] 59 | if symbol != nil 60 | if symbol["size"] == nil 61 | symbol["size"] = 0 62 | end 63 | symbol["size"] = (size + symbol["size"]) 64 | else 65 | puts(key + "未找到") 66 | end 67 | end 68 | end 69 | end 70 | end 71 | end 72 | 73 | return symbolMap 74 | end 75 | 76 | def openContent(linkMapPath) 77 | fileContent = File.read(linkMapPath).unpack('C*').pack('U*') 78 | 79 | puts("开始解析原始linkmap文件...") 80 | 81 | objectString = "# Object files:" 82 | if !fileContent.include?(objectString) 83 | puts("link map file 文件有误" + "Obj") 84 | return 85 | end 86 | 87 | symbolString = "# Symbols:" 88 | if !fileContent.include?(symbolString) 89 | puts("link map file 文件有误" + "Symbol") 90 | return 91 | end 92 | 93 | if !fileContent.include?("# Path:") 94 | puts("link map file 文件有误" + "Path") 95 | return 96 | end 97 | 98 | symbolMap = symbolMapFromContent(fileContent) 99 | 100 | sortedSymbols = Array.new() 101 | 102 | for key in symbolMap.keys 103 | sortedSymbols.push(symbolMap[key]) 104 | end 105 | 106 | #根据大小排序 107 | sortedSymbols = sortedSymbols.sort_by {|hashObj| hashObj["size"]}.reverse 108 | 109 | resultSymbols = buildResultWithSymbols(sortedSymbols) 110 | 111 | puts("解析原始linkmap文件完成...") 112 | 113 | return resultSymbols 114 | end 115 | end 116 | 117 | class LibrarySizeAlaysizer 118 | def prettyFormatClassResult(oriHash) 119 | for libName in oriHash.keys 120 | if libName != " DiffResult" 121 | libHash = oriHash[libName] 122 | finalArrayValue = Array.new() 123 | for clsName in libHash.keys 124 | clsHash = libHash[clsName] 125 | resultString = "" 126 | for resultKey in clsHash.keys 127 | resultString = resultString + resultKey + " => " + clsHash[resultKey] + " " 128 | end 129 | simpleHash = Hash.new 130 | simpleHash[clsName] = resultString 131 | finalArrayValue.push(simpleHash) 132 | end 133 | oriHash[libName] = finalArrayValue 134 | end 135 | end 136 | end 137 | 138 | def alyTotalSize(sizeHash,versionOld,versionNew) 139 | diffSizeArray = Array.new() 140 | for libName in sizeHash.keys 141 | libDiffSize = 0; 142 | libOldSize = 0; 143 | libNewSize = 0; 144 | for clsName in sizeHash[libName].keys 145 | diffSize = sizeHash[libName][clsName]["大小差值"] 146 | oldSize = sizeHash[libName][clsName][versionOld] 147 | newSize = sizeHash[libName][clsName][versionNew] 148 | 149 | libDiffSize += diffSize.to_i 150 | libOldSize += oldSize.to_i 151 | libNewSize += newSize.to_i 152 | end 153 | 154 | libHash = Hash.new() 155 | libHash["组件名"] = libName; 156 | libHash["原大小"] = libOldSize 157 | libHash["目前大小"] = libNewSize 158 | libHash["diffSize"] = libDiffSize 159 | 160 | diffSizeArray.push(libHash) 161 | end 162 | sizeHash[" DiffResult"] = (diffSizeArray.sort_by {|hashObj| hashObj["diffSize"] }).reverse 163 | end 164 | 165 | def parseAndCompareDatas(firsFileContent,secondFileContent,appVersionString1,appVersionString2) 166 | versionsHash1 = getAllVersionHash(firsFileContent) 167 | versionsHash2 = getAllVersionHash(secondFileContent) 168 | 169 | puts("开始比较两个版本linkmap文件各库的大小") 170 | 171 | compareResultHash = Hash.new() 172 | 173 | for libraryName in versionsHash1.keys 174 | libraryHash = versionsHash1[libraryName] 175 | for className in libraryHash.keys 176 | if compareResultHash[libraryName] == nil 177 | compareResultHash[libraryName] = Hash.new() 178 | end 179 | 180 | if compareResultHash[libraryName][className] == nil 181 | compareResultHash[libraryName][className] = Hash.new() 182 | end 183 | 184 | if versionsHash2[libraryName] == nil 185 | versionsHash2[libraryName] = Hash.new() 186 | end 187 | 188 | if versionsHash2[libraryName][className] == nil 189 | versionsHash2[libraryName][className] = "0" 190 | end 191 | 192 | libSize1 = versionsHash1[libraryName][className] 193 | 194 | libSize2 = versionsHash2[libraryName][className] ? versionsHash2[libraryName][className] : "0" 195 | 196 | tmpHash = Hash.new() 197 | if libSize1 != libSize2 198 | tmpHash[appVersionString1] = libSize1 199 | tmpHash[appVersionString2] = libSize2 200 | # puts(libSize2) 201 | 202 | #计算两者差值 203 | size1 = libSize1 204 | size2 = libSize2 205 | 206 | tmpHash["大小差值"] = (size2.to_i - size1.to_i).to_s 207 | compareResultHash[libraryName][className] = tmpHash 208 | end 209 | end 210 | end 211 | 212 | for libraryName in versionsHash2.keys 213 | libraryHash = versionsHash2[libraryName] 214 | for className in libraryHash.keys 215 | if compareResultHash[libraryName] == nil 216 | compareResultHash[libraryName] = Hash.new() 217 | end 218 | 219 | if compareResultHash[libraryName][className] == nil 220 | libSize2 = versionsHash2[libraryName][className] 221 | 222 | tmpHash = Hash.new() 223 | 224 | tmpHash[appVersionString1] = "空" 225 | tmpHash[appVersionString2] = libSize2 226 | tmpHash["大小差值"] = libSize2 227 | compareResultHash[libraryName][className] = tmpHash 228 | end 229 | end 230 | end 231 | 232 | removeEmtpyKeys(compareResultHash) 233 | alyTotalSize(compareResultHash,appVersionString1,appVersionString2) 234 | prettyFormatClassResult(compareResultHash) 235 | resultIncreamentText = JSON.pretty_generate(compareResultHash.sort.to_h) 236 | 237 | resultLogPath = __dir__ + "/libSizeCompareResult.json" 238 | if !Dir.exists?(resultLogPath) 239 | File.new(resultLogPath,'w') 240 | end 241 | File.open(resultLogPath, "w") do |f| 242 | f.write(resultIncreamentText) 243 | end 244 | 245 | puts("分析完成,结果已经保存到=>" + resultLogPath) 246 | 247 | end 248 | 249 | def removeEmtpyKeys(hashObj) 250 | for libName in hashObj.keys 251 | libHash = hashObj[libName] 252 | for clsName in libHash.keys 253 | versionsHash = hashObj[libName][clsName] 254 | if versionsHash.keys.count == 0 255 | libHash.delete(clsName) 256 | end 257 | end 258 | end 259 | 260 | for libName in hashObj.keys 261 | if hashObj[libName].keys.count == 0 262 | hashObj.delete(libName) 263 | end 264 | end 265 | end 266 | 267 | def prettyPutsHash(hashContent) 268 | putContent = JSON.pretty_generate(hashContent).gsub(":", " =>") 269 | puts(putContent) 270 | end 271 | 272 | def mainLibraryName(libName) 273 | if libName.include?("(") 274 | return libName.split("(")[0] 275 | end 276 | return libName 277 | end 278 | 279 | def libraryClassName(libName) 280 | if libName.include?("(") 281 | return libName.split("(")[1].gsub(".o)","") 282 | end 283 | return libName 284 | end 285 | 286 | def getAllVersionHash(fileContents) 287 | hashContents = Hash.new() 288 | 289 | lines = fileContents.split("\n") 290 | 291 | for line in lines 292 | libContents = line.split(" ") 293 | #前面的库名 294 | libName = libContents[1].split("(")[0] 295 | #类名 296 | className = libraryClassName(libContents[1]) 297 | libSize = libContents[0] 298 | 299 | if hashContents[libName] == nil 300 | classVersionHash = Hash.new() 301 | classVersionHash[className] = libSize 302 | hashContents[libName] = classVersionHash 303 | else 304 | hashContents[libName][className] = libSize 305 | end 306 | end 307 | return hashContents 308 | end 309 | end 310 | 311 | parser = LinkMapParser.new 312 | libCompareAly = LibrarySizeAlaysizer.new 313 | 314 | linkMapPathFirst = ARGV[0] 315 | linkMapPathSecond = ARGV[1] 316 | 317 | libUsageFirst = parser.openContent(linkMapPathFirst) 318 | libUsageSecond = parser.openContent(linkMapPathSecond) 319 | 320 | 321 | appVersionString1 = linkMapPathFirst.split("/")[-1].split("-")[0] 322 | appVersionString2 = linkMapPathSecond.split("/")[-1].split("-")[0] 323 | 324 | libCompareAly.parseAndCompareDatas(libUsageFirst,libUsageSecond,appVersionString1,appVersionString2) 325 | -------------------------------------------------------------------------------- /podlockDependencyParse.rb: -------------------------------------------------------------------------------- 1 | 2 | #用法,ruby podlockDependencyParse.rb /xx/xx/Podfile.lock 生成分析图,需要用到三方工具 graphviz,自行安装,推荐brew安装,不需要额外添加环境变量 3 | #author 张恒 2021.10.12 4 | 5 | class TyPodLib 6 | attr_accessor:pod_name 7 | attr_accessor:pod_dependencies 8 | end 9 | 10 | def parsePodfilelock 11 | podLockFilePath = ARGV[0] 12 | 13 | podArray = Array.new 14 | 15 | File.open(podLockFilePath) do |file| 16 | file.each_line do |line| 17 | #主pod 18 | if line.start_with?(" -") 19 | podlib = TyPodLib.new 20 | podlib.pod_dependencies = Array.new 21 | 22 | podName = line.split(" ")[1].gsub("\n","") 23 | podlib.pod_name = podName.split("/")[0].gsub("-","_").gsub("\"","") 24 | podArray.push(podlib) 25 | elsif line.start_with?(" -") #子pod依赖 (" -") 26 | podlib = podArray[-1] 27 | puts(line) 28 | subpodName = line.split(" ")[1].gsub("\n","") 29 | subpodName = subpodName.split("/")[0] 30 | if subpodName != podlib.pod_name 31 | podlib.pod_dependencies.push(subpodName.gsub("-","_").gsub("\"","")) 32 | end 33 | elsif line.start_with?("DEPENDENCIES:") 34 | #结束扫描,后面的不需要了 35 | break 36 | end 37 | end 38 | end 39 | 40 | generateImageData(podArray) 41 | end 42 | 43 | def generateImageData(arrayData) 44 | dotMainArray = Array.new 45 | dotRelateArray = Array.new 46 | 47 | for podlib in arrayData 48 | dotMainArray.push(podlib.pod_name + ";\n") 49 | for podDepenency in podlib.pod_dependencies 50 | dotRelateArray.push(podlib.pod_name + "->" + podDepenency + ";\n") 51 | end 52 | end 53 | 54 | #去重处理 55 | dotMainArray = dotMainArray.uniq() 56 | dotRelateArray = dotRelateArray.uniq() 57 | 58 | dotFileContent = "digraph dependencyRelation{\n" 59 | 60 | for mainContent in dotMainArray 61 | dotFileContent = dotFileContent + mainContent 62 | end 63 | 64 | for relateContent in dotRelateArray 65 | dotFileContent = dotFileContent + relateContent 66 | end 67 | 68 | dotFileContent = dotFileContent + "}" 69 | 70 | dotFilePath = __dir__ + "/" + "dependencyRelation.dot" 71 | 72 | if !Dir.exists?(dotFilePath) 73 | File.new(dotFilePath,'w') 74 | end 75 | File.open(dotFilePath, "w") do |f| 76 | f.write(dotFileContent) 77 | end 78 | 79 | #dot 默认布局方式,主要用于有向图 80 | #neato 基于spring-model(又称force-based)算法 81 | #twopi 径向布局 82 | #circo 圆环布局 83 | #fdp 用于无向图 84 | #需要修改展示样式的话,只需要把此句最前面做一下修改即可 85 | generateImageCmdString = "fdp -Tpng dependencyRelation.dot -o dependencyRelation.png" 86 | system(generateImageCmdString) 87 | end 88 | 89 | parsePodfilelock 90 | --------------------------------------------------------------------------------