一鍵清除 objc 項目中的無用方法
當項目越來越大,引入第三方庫越來越多,上架的APP體積也會越來越大,對于用戶來說體驗必定是不好的。在清理資源,編譯選項優化,清理無用類等完成后,能夠做而且效果會比較明顯的就只有清理無用函數了。現有一種方案是根據Linkmap文件取到objc的所有類方法和實例方法。再用工具逆向可執行文件里引用到的方法名,求個差集列出無用方法。這個方案有些比較麻煩的地方,因為檢索出的無用方法沒法確定能夠直接刪除,還需要挨個檢索人工判斷是否可以刪除,這樣每次要清理時都需要這樣人工排查一遍是非常耗時耗力的。
這樣就只有模擬編譯過程對代碼進行深入分析才能夠找出確定能夠刪除的方法。具體效果可以先試試看,選擇工程目錄后程序就開始檢索無用方法然后將其注釋掉。
首先遍歷目錄下所有的文件。
letfileFolderPath = self.selectFolder()
letfileFolderStringPath = fileFolderPath.replacingOccurrences(of: "file://", with: "")
letfileManager = FileManager.default;
//深度遍歷
letenumeratorAtPath = fileManager.enumerator(atPath: fileFolderStringPath)
//過濾文件后綴
letfilterPath = NSArray(array: (enumeratorAtPath?.allObjects)!).pathsMatchingExtensions(["h","m"])
然后將注釋排除在分析之外,這樣做能夠有效避免無用的解析。這里可以這樣處理。
class funcdislodgeAnnotaion(content:String) -> String {
letannotationBlockPattern = "/\\*[\\s\\S]*?\\*/" //匹配/*...*/這樣的注釋
letannotationLinePattern = "http://.*?\\n" //匹配//這樣的注釋
letregexBlock = try! NSRegularExpression(pattern: annotationBlockPattern, options: NSRegularExpression.Options(rawValue:0))
letregexLine = 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,具體方式如下
//根據代碼文件解析出一個根據標記符切分的數組
class funccreateOCTokens(conent:String) -> [String] {
var str = conent
str = self.dislodgeAnnotaion(content: str)
//開始掃描切割
letscanner = Scanner(string: str)
var tokens = [String]()
//Todo:待處理符號,.
letoperaters = [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(resultas! String)
}
}
tokens = tokens.filter {
$0 != Sb.space
}
return tokens;
}
由于objc語法中有行分割解析的,所以還要寫個行解析的方法
//根據代碼文件解析出一個根據行切分的數組
class funccreateOCLines(content:String) -> [String] {
var str = content
str = self.dislodgeAnnotaion(content: str)
letstrArr = str.components(separatedBy: CharacterSet.newlines)
return strArr
}
獲得這些數據后就可以開始檢索定義的方法了。我寫了一個類專門用來獲得所有定義的方法
class ParsingMethod: NSObject {
class funcparsingWithArray(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
}
}
這個方法大概的思路就是根據標記符設置不同的狀態,然后將獲取的信息放入定義的結構中,這個結構我是按照文件作為主體的,文件中定義那些定義方法的列表,然后定義一個方法的結構體,這個結構體里定義一些方法的信息。具體結構如下
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]() //所有方法
funcdes() {
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 aUsedMethodin aMethod.usedMethod {
showStr = ""
showStr = showStr.appending(File.desUsedMethodParams(paramArr: aUsedMethod.params))
print("\(showStr)")
}
print("------------------")
}
}
print("\n")
}
//類方法
//打印定義方法參數
class funcdesDefineMethodParams(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 funcdesUsedMethodParams(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是指針
}
有了文件里定義的方法,接下來就是需要找出所有使用過的方法,這樣才能夠通過差集得到沒有用過的方法。獲取使用過的方法,我使用了一種時間復雜度較優的方法,關鍵在于對方法中使用方法的情況做了計數的處理,這樣能夠最大的減少遍歷,達到一次遍歷獲取所有方法。具體實現如下
class ParsingMethodContent: NSObject {
class funcparsing(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
}
letprm = 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
}
}
比對后獲得無用方法后就要開始注釋掉他們了。這里用的是逐行分析,使用解析定義方法的方式通過方法結構體里定義的唯一標識符來比對是否到了無用的方法那,然后開始添加注釋將其注釋掉。實現的方法具體如下:
//刪除指定的一組方法
class funcdelete(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 = ""
}
}
lethContentArr = hContent.components(separatedBy: CharacterSet.newlines)
letmContentArr = 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 += "http://-----由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 += "http://" + 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
letmethodPnameId = 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 {
letline = mOneLine.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if mDeletingTf {
letlTokens = self.createOCTokens(conent: line)
mMtdAnnoStr += "http://" + 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 += "http://-----由SMCheckProject工具刪除-----\n//" + mOneLine + Sb.newLine
} else if psMMtdTf {
mMtdStr = mMtdStr.appending(mOneLine + Sb.newLine)
mMtdAnnoStr += "http://" + mOneLine + Sb.newLine
mMtds += self.createOCTokens(conent: line)
} else {
mContentCleaned = mContentCleaned.appending(mOneLine + Sb.newLine)
}
if line.hasSuffix(Sb.braceL) && psMMtdTf {
psMMtdTf = false
letmethodPnameId = 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)
}
}
}
基于語法層面的分析是比較有想象的,后面完善這個解析,比如說分析各個文件import的頭文件遞歸來判斷哪些類沒有使用,通過獲取的方法結合獲取類里面定義的局部變量和全局變量來分析循環引用,通過獲取的類的完整結構還能夠將其轉成JavaScriptCore能解析的js語法文件。
來自:http://ios.jobbole.com/90215/