當專案越來越大,引入第三方庫越來越多,上架的APP體積也會越來越大,對於使用者來說體驗必定是不好的。在清理資源,編譯選項優化,清理無用類等完成後,能夠做而且效果會比較明顯的就只有清理無用函式了。現有一種方案是根據Linkmap檔案取到objc的所有類方法和例項方法。再用工具逆向可執行檔案裡引用到的方法名,求個差集列出無用方法。這個方案有些比較麻煩的地方,因為檢索出的無用方法沒法確定能夠直接刪除,還需要挨個檢索人工判斷是否可以刪除,這樣每次要清理時都需要這樣人工排查一遍是非常耗時耗力的。
這樣就只有模擬編譯過程對程式碼進行深入分析才能夠找出確定能夠刪除的方法。具體效果可以先試試看,程式程式碼在:https://github.com/ming1016/SMCheckProject 選擇工程目錄後程式就開始檢索無用方法然後將其註釋掉。
首先遍歷目錄下所有的檔案。
1 2 3 4 5 6 7 |
let fileFolderPath = self.selectFolder() let fileFolderStringPath = fileFolderPath.replacingOccurrences(of: "file://", with: "") let fileManager = FileManager.default; //深度遍歷 let enumeratorAtPath = fileManager.enumerator(atPath: fileFolderStringPath) //過濾檔案字尾 let filterPath = NSArray(array: (enumeratorAtPath?.allObjects)!).pathsMatchingExtensions(["h","m"]) |
然後將註釋排除在分析之外,這樣做能夠有效避免無用的解析。這裡可以這樣處理。
1 2 3 4 5 6 7 8 9 10 11 12 |
class func dislodgeAnnotaion(content:String) -> String { let annotationBlockPattern = "/\\*[\\s\\S]*?\\*/" //匹配/*...*/這樣的註釋 let annotationLinePattern = "//.*?\\n" //匹配//這樣的註釋 let regexBlock = try! NSRegularExpression(pattern: annotationBlockPattern, options: NSRegularExpression.Options(rawValue:0)) let regexLine = try! NSRegularExpression(pattern: annotationLinePattern, options: NSRegularExpression.Options(rawValue:0)) var newStr = "" newStr = regexLine.stringByReplacingMatches(in: content, options: NSRegularExpression.MatchingOptions(rawValue:0), range: NSMakeRange(0, content.characters.count), withTemplate: Sb.space) newStr = regexBlock.stringByReplacingMatches(in: newStr, options: NSRegularExpression.MatchingOptions(rawValue:0), range: NSMakeRange(0, newStr.characters.count), withTemplate: Sb.space) return newStr } |
這裡/…/這種註釋是允許換行的,所以使用.*的方式會有問題,因為.是指非空和換行的字元。那麼就需要用到[\s\S]這樣的方法來包含所有字元,\s是匹配任意的空白符,\S是匹配任意不是空白符的字元,這樣的或組合就能夠包含全部字元。
接下來就要開始根據標記符號來進行切割分組了,使用Scanner,具體方式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
//根據程式碼檔案解析出一個根據標記符切分的陣列 class func createOCTokens(conent:String) -> [String] { var str = conent str = self.dislodgeAnnotaion(content: str) //開始掃描切割 let scanner = Scanner(string: str) var tokens = [String]() //Todo:待處理符號,. let operaters = [Sb.add,Sb.minus,Sb.rBktL,Sb.rBktR,Sb.asterisk,Sb.colon,Sb.semicolon,Sb.divide,Sb.agBktL,Sb.agBktR,Sb.quotM,Sb.pSign,Sb.braceL,Sb.braceR,Sb.bktL,Sb.bktR,Sb.qM] var operatersString = "" for op in operaters { operatersString = operatersString.appending(op) } var set = CharacterSet() set.insert(charactersIn: operatersString) set.formUnion(CharacterSet.whitespacesAndNewlines) while !scanner.isAtEnd { for operater in operaters { if (scanner.scanString(operater, into: nil)) { tokens.append(operater) } } var result:NSString? result = nil; if scanner.scanUpToCharacters(from: set, into: &result) { tokens.append(result as! String) } } tokens = tokens.filter { $0 != Sb.space } return tokens; } |
由於objc語法中有行分割解析的,所以還要寫個行解析的方法
1 2 3 4 5 6 7 |
//根據程式碼檔案解析出一個根據行切分的陣列 class func createOCLines(content:String) -> [String] { var str = content str = self.dislodgeAnnotaion(content: str) let strArr = str.components(separatedBy: CharacterSet.newlines) return strArr } |
獲得這些資料後就可以開始檢索定義的方法了。我寫了一個類專門用來獲得所有定義的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
class ParsingMethod: NSObject { class func parsingWithArray(arr:Array) -> Method { var mtd = Method() var returnTypeTf = false //是否取得返回型別 var parsingTf = false //解析中 var bracketCount = 0 //括弧計數 var step = 0 //1獲取引數名,2獲取引數型別,3獲取iName var types = [String]() var methodParam = MethodParam() //print("\(arr)") for var tk in arr { tk = tk.replacingOccurrences(of: Sb.newLine, with: "") if (tk == Sb.semicolon || tk == Sb.braceL) && step != 1 { mtd.params.append(methodParam) mtd.pnameId = mtd.pnameId.appending("\(methodParam.name):") } else if tk == Sb.rBktL { bracketCount += 1 parsingTf = true } else if tk == Sb.rBktR { bracketCount -= 1 if bracketCount == 0 { var typeString = "" for typeTk in types { typeString = typeString.appending(typeTk) } if !returnTypeTf { //完成獲取返回 mtd.returnType = typeString step = 1 returnTypeTf = true } else { if step == 2 { methodParam.type = typeString step = 3 } } //括弧結束後的重置工作 parsingTf = false types = [] } } else if parsingTf { types.append(tk) //todo:返回block型別會使用.設定值的方式,目前獲取用過方法方式沒有.這種的解析,暫時作為 if tk == Sb.upArrow { mtd.returnTypeBlockTf = true } } else if tk == Sb.colon { step = 2 } else if step == 1 { methodParam.name = tk step = 0 } else if step == 3 { methodParam.iName = tk step = 1 mtd.params.append(methodParam) mtd.pnameId = mtd.pnameId.appending("\(methodParam.name):") methodParam = MethodParam() } else if tk != Sb.minus && tk != Sb.add { methodParam.name = tk } }//遍歷 return mtd } } |
這個方法大概的思路就是根據標記符設定不同的狀態,然後將獲取的資訊放入定義的結構中,這個結構我是按照檔案作為主體的,檔案中定義那些定義方法的列表,然後定義一個方法的結構體,這個結構體裡定義一些方法的資訊。具體結構如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
enum FileType { case fileH case fileM case fileSwift } class File: NSObject { public var path = "" { didSet { if path.hasSuffix(".h") { type = FileType.fileH } else if path.hasSuffix(".m") { type = FileType.fileM } else if path.hasSuffix(".swift") { type = FileType.fileSwift } name = (path.components(separatedBy: "/").last?.components(separatedBy: ".").first)! } } public var type = FileType.fileH public var name = "" public var methods = [Method]() //所有方法 func des() { print("檔案路徑:\(path)\n") print("檔名:\(name)\n") print("方法數量:\(methods.count)\n") print("方法列表:") for aMethod in methods { var showStr = "- (\(aMethod.returnType)) " showStr = showStr.appending(File.desDefineMethodParams(paramArr: aMethod.params)) print("\n\(showStr)") if aMethod.usedMethod.count > 0 { print("用過的方法----------") showStr = "" for aUsedMethod in aMethod.usedMethod { showStr = "" showStr = showStr.appending(File.desUsedMethodParams(paramArr: aUsedMethod.params)) print("\(showStr)") } print("------------------") } } print("\n") } //類方法 //列印定義方法引數 class func desDefineMethodParams(paramArr:[MethodParam]) -> String { var showStr = "" for aParam in paramArr { if aParam.type == "" { showStr = showStr.appending("\(aParam.name);") } else { showStr = showStr.appending("\(aParam.name):(\(aParam.type))\(aParam.iName);") } } return showStr } class func desUsedMethodParams(paramArr:[MethodParam]) -> String { var showStr = "" for aUParam in paramArr { showStr = showStr.appending("\(aUParam.name):") } return showStr } } struct Method { public var classMethodTf = false //+ or - public var returnType = "" public var returnTypePointTf = false public var returnTypeBlockTf = false public var params = [MethodParam]() public var usedMethod = [Method]() public var filePath = "" //定義方法的檔案路徑,方便修改檔案使用 public var pnameId = "" //唯一標識,便於快速比較 } class MethodParam: NSObject { public var name = "" public var type = "" public var typePointTf = false public var iName = "" } class Type: NSObject { //todo:更多型別 public var name = "" public var type = 0 //0是值型別 1是指標 } |
有了檔案裡定義的方法,接下來就是需要找出所有使用過的方法,這樣才能夠通過差集得到沒有用過的方法。獲取使用過的方法,我使用了一種時間複雜度較優的方法,關鍵在於對方法中使用方法的情況做了計數的處理,這樣能夠最大的減少遍歷,達到一次遍歷獲取所有方法。具體實現如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
class ParsingMethodContent: NSObject { class func parsing(contentArr:Array, inMethod:Method) -> Method { var mtdIn = inMethod //處理用過的方法 //todo:還要過濾@""這種情況 var psBrcStep = 0 var uMtdDic = [Int:Method]() var preTk = "" //處理?:這種條件判斷簡寫方式 var psCdtTf = false var psCdtStep = 0 for var tk in contentArr { if tk == Sb.bktL { if psCdtTf { psCdtStep += 1 } psBrcStep += 1 uMtdDic[psBrcStep] = Method() } else if tk == Sb.bktR { if psCdtTf { psCdtStep -= 1 } if (uMtdDic[psBrcStep]?.params.count)! > 0 { mtdIn.usedMethod.append(uMtdDic[psBrcStep]!) } psBrcStep -= 1 } else if tk == Sb.colon { //條件簡寫情況處理 if psCdtTf && psCdtStep == 0 { psCdtTf = false continue } //dictionary情況處理@"key":@"value" if preTk == Sb.quotM || preTk == "respondsToSelector" { continue } let prm = MethodParam() prm.name = preTk if prm.name != "" { uMtdDic[psBrcStep]?.params.append(prm) uMtdDic[psBrcStep]?.pnameId = (uMtdDic[psBrcStep]?.pnameId.appending("\(prm.name):"))! } } else if tk == Sb.qM { psCdtTf = true } else { tk = tk.replacingOccurrences(of: Sb.newLine, with: "") preTk = tk } } return mtdIn } } |
比對後獲得無用方法後就要開始註釋掉他們了。這裡用的是逐行分析,使用解析定義方法的方式通過方法結構體裡定義的唯一識別符號來比對是否到了無用的方法那,然後開始新增註釋將其註釋掉。實現的方法具體如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
//刪除指定的一組方法 class func delete(methods:[Method]) { print("無用方法") for aMethod in methods { print("\(File.desDefineMethodParams(paramArr: aMethod.params))") //開始刪除 //continue var hContent = "" var mContent = "" var mFilePath = aMethod.filePath if aMethod.filePath.hasSuffix(".h") { hContent = try! String(contentsOf: URL(string:aMethod.filePath)!, encoding: String.Encoding.utf8) //todo:因為先處理了h檔案的情況 mFilePath = aMethod.filePath.trimmingCharacters(in: CharacterSet(charactersIn: "h")) //去除頭尾字符集 mFilePath = mFilePath.appending("m") } if mFilePath.hasSuffix(".m") { do { mContent = try String(contentsOf: URL(string:mFilePath)!, encoding: String.Encoding.utf8) } catch { mContent = "" } } let hContentArr = hContent.components(separatedBy: CharacterSet.newlines) let mContentArr = mContent.components(separatedBy: CharacterSet.newlines) //print(mContentArr) //----------------h檔案------------------ var psHMtdTf = false var hMtds = [String]() var hMtdStr = "" var hMtdAnnoStr = "" var hContentCleaned = "" for hOneLine in hContentArr { var line = hOneLine.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) if line.hasPrefix(Sb.minus) || line.hasPrefix(Sb.add) { psHMtdTf = true hMtds += self.createOCTokens(conent: line) hMtdStr = hMtdStr.appending(hOneLine + Sb.newLine) hMtdAnnoStr += "//-----由SMCheckProject工具刪除-----\n//" hMtdAnnoStr += hOneLine + Sb.newLine line = self.dislodgeAnnotaionInOneLine(content: line) line = line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) } else if psHMtdTf { hMtds += self.createOCTokens(conent: line) hMtdStr = hMtdStr.appending(hOneLine + Sb.newLine) hMtdAnnoStr += "//" + hOneLine + Sb.newLine line = self.dislodgeAnnotaionInOneLine(content: line) line = line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) } else { hContentCleaned += hOneLine + Sb.newLine } if line.hasSuffix(Sb.semicolon) && psHMtdTf{ psHMtdTf = false let methodPnameId = ParsingMethod.parsingWithArray(arr: hMtds).pnameId if aMethod.pnameId == methodPnameId { hContentCleaned += hMtdAnnoStr } else { hContentCleaned += hMtdStr } hMtdAnnoStr = "" hMtdStr = "" hMtds = [] } } //刪除無用函式 try! hContentCleaned.write(to: URL(string:aMethod.filePath)!, atomically: false, encoding: String.Encoding.utf8) //----------------m檔案---------------- var mDeletingTf = false var mBraceCount = 0 var mContentCleaned = "" var mMtdStr = "" var mMtdAnnoStr = "" var mMtds = [String]() var psMMtdTf = false for mOneLine in mContentArr { let line = mOneLine.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) if mDeletingTf { let lTokens = self.createOCTokens(conent: line) mMtdAnnoStr += "//" + mOneLine + Sb.newLine for tk in lTokens { if tk == Sb.braceL { mBraceCount += 1 } if tk == Sb.braceR { mBraceCount -= 1 if mBraceCount == 0 { mContentCleaned = mContentCleaned.appending(mMtdAnnoStr) mMtdAnnoStr = "" mDeletingTf = false } } } continue } if line.hasPrefix(Sb.minus) || line.hasPrefix(Sb.add) { psMMtdTf = true mMtds += self.createOCTokens(conent: line) mMtdStr = mMtdStr.appending(mOneLine + Sb.newLine) mMtdAnnoStr += "//-----由SMCheckProject工具刪除-----\n//" + mOneLine + Sb.newLine } else if psMMtdTf { mMtdStr = mMtdStr.appending(mOneLine + Sb.newLine) mMtdAnnoStr += "//" + mOneLine + Sb.newLine mMtds += self.createOCTokens(conent: line) } else { mContentCleaned = mContentCleaned.appending(mOneLine + Sb.newLine) } if line.hasSuffix(Sb.braceL) && psMMtdTf { psMMtdTf = false let methodPnameId = ParsingMethod.parsingWithArray(arr: mMtds).pnameId if aMethod.pnameId == methodPnameId { mDeletingTf = true mBraceCount += 1 mContentCleaned = mContentCleaned.appending(mMtdAnnoStr) } else { mContentCleaned = mContentCleaned.appending(mMtdStr) } mMtdStr = "" mMtdAnnoStr = "" mMtds = [] } } //m檔案 //刪除無用函式 if mContent.characters.count > 0 { try! mContentCleaned.write(to: URL(string:mFilePath)!, atomically: false, encoding: String.Encoding.utf8) } } } |
完整程式碼在:https://github.com/ming1016/SMCheckProject 這裡。基於語法層面的分析是比較有想象的,後面完善這個解析,比如說分析各個檔案import的標頭檔案遞迴來判斷哪些類沒有使用,通過獲取的方法結合獲取類裡面定義的區域性變數和全域性變數來分析迴圈引用,通過獲取的類的完整結構還能夠將其轉成JavaScriptCore能解析的js語法檔案。