當(dāng)項目越來越大族购,引入第三方庫越來越多壳贪,上架的 APP 體積也會越來越大,對于用戶來說體驗必定是不好的寝杖。在清理資源违施,編譯選項優(yōu)化,清理無用類等完成后朝墩,能夠做而且效果會比較明顯的就只有清理無用函數(shù)了醉拓。
一種方案是我們滴滴的王康基于clang插件這樣一個源碼級別的分析工具來分析代碼間的調(diào)用關(guān)系達(dá)到分析出無用代碼的目的,文章在這里: 基于clang插件的一種iOS包大小瘦身方案 文章里對objc方法的定義收苏,調(diào)用亿卤,實現(xiàn)的全面說明達(dá)到了極致,非常值得一看鹿霸。
另一種方案是根據(jù) Linkmap 文件取到objc的所有類方法和實例方法排吴。再用工具比如 otool 命令逆向出可執(zhí)行文件里引用到的方法名然后通過求差集得到無用函數(shù),由于API的回調(diào)也會被認(rèn)為是無用函數(shù)懦鼠,所以這個方案還需要將這些回調(diào)函數(shù)加到白名單里過濾钻哩。具體說明,可以看看微信團隊的這篇文章: iOS微信安裝包瘦身
還有一種使用了 * machoview * 從 Mach-O 里獲取信息進行無用方法和文件的處理肛冶。阿里有篇文章對 Mach-O 的處理做了詳細(xì)的說明: 減小ipa體積之刪除frameWork中無用mach-O文件
這幾個現(xiàn)有方案有些比較麻煩的地方街氢,因為檢索出的無用方法沒法確定能夠直接刪除,還需要挨個檢索人工判斷是否可以刪除睦袖,這樣每次要清理時都需要這樣人工排查一遍是非常耗時耗力的珊肃。
這樣就只有模擬編譯過程對代碼進行深入分析才能夠找出確定能夠刪除的方法。具體效果可以先試試看馅笙,程序代碼在:https://github.com/ming1016/SMCheckProject 選擇工程目錄后程序就開始檢索無用方法然后將其注釋掉伦乔。
設(shè)置結(jié)構(gòu)體 ??
首先確定結(jié)構(gòu),類似先把 OC 文件根據(jù)語法畫出整體結(jié)構(gòu)董习。先看看 OC Runtime 里是如何設(shè)計的結(jié)構(gòu)體烈和。
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
/*類*/
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
#endif
};
/*成員變量列表*/
struct objc_ivar_list {
int ivar_count
#ifdef __LP64__
int space
#endif
/* variable length structure */
struct objc_ivar ivar_list[1]
}
/*成員變量結(jié)構(gòu)體*/
struct objc_ivar {
char *ivar_name
char *ivar_type
int ivar_offset
#ifdef __LP64__
int space
#endif
}
/*方法列表*/
struct objc_method_list {
struct objc_method_list *obsolete;
int method_count;
#ifdef __LP64__
int space;
#endif
/* variable length structure */
struct objc_method method_list[1];
};
/*方法結(jié)構(gòu)體*/
struct objc_method {
SEL method_name;
char *method_types; /* a string representing argument/return types */
IMP method_imp;
};
一個 class 只有少量函數(shù)會被調(diào)用,為了減少較大的遍歷所以創(chuàng)建一個 objc_cache 皿淋,在找到一個方法后將 method_name 作為 key招刹,將 method_imp 做值,再次發(fā)起時就可以直接在 cache 里找窝趣。
使用 swift 創(chuàng)建類似的結(jié)構(gòu)體疯暑,做些修改
//文件
class File: NSObject {
//文件
public var type = FileType.FileH
public var name = ""
public var content = ""
public var methods = [Method]() //所有方法
public var imports = [Import]() //引入類
}
//引入
struct Import {
public var fileName = ""
}
//對象
class Object {
public var name = ""
public var superObject = ""
public var properties = [Property]()
public var methods = [Method]()
}
//成員變量
struct Property {
public var name = ""
public var type = ""
}
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 = "" //唯一標(biāo)識高帖,便于快速比較
}
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是指針
}
```swift
## 開始語法解析 ??
首先遍歷目錄下所有的文件。
```swift
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"])
然后將注釋排除在分析之外畦粮,這樣做能夠有效避免無用的解析散址。
分析是否需要按照行來切割乖阵,在 @interface , @end 和 @ implementation 预麸, @end 里面不需要換行瞪浸,按照;符號,外部需要按行來吏祸。所以兩種切割都需要对蒲。
先定義語法標(biāo)識符
class Sb: NSObject {
public static let add = "+"
public static let minus = "-"
public static let rBktL = "("
public static let rBktR = ")"
public static let asterisk = "*"
public static let colon = ":"
public static let semicolon = ";"
public static let divide = "/"
public static let agBktL = "<"
public static let agBktR = ">"
public static let quotM = "\""
public static let pSign = "#"
public static let braceL = "{"
public static let braceR = "}"
public static let bktL = "["
public static let bktR = "]"
public static let qM = "?"
public static let upArrow = "^"
public static let inteface = "@interface"
public static let implementation = "@implementation"
public static let end = "@end"
public static let selector = "@selector"
public static let space = " "
public static let newLine = "\n"
}
接下來就要開始根據(jù)標(biāo)記符號來進行切割分組了,使用 Scanner 贡翘,具體方式如下
//根據(jù)代碼文件解析出一個根據(jù)標(biāo)記符切分的數(shù)組
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;
}
行解析的方法
//根據(jù)代碼文件解析出一個根據(jù)行切分的數(shù)組
class func createOCLines(content:String) -> [String] {
var str = content
str = self.dislodgeAnnotaion(content: str)
let strArr = str.components(separatedBy: CharacterSet.newlines)
return strArr
}
根據(jù)結(jié)構(gòu)將定義的方法取出 ??
- (id)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(NSString *)path cacheTime:(NSInteger)cacheTime subDirectory:(NSString*)subDirectory;
這里按照語法規(guī)則順序取出即可蹈矮,將方法名,返回類型鸣驱,參數(shù)名泛鸟,參數(shù)類型記錄。這里需要注意 Block 類型的參數(shù)
- (STMPartMaker *(^)(STMPartColorType))colorTypeIs;
這種類型中還帶有括號的語法的解析踊东,這里用到的方法是對括號進行計數(shù)北滥,左括號加一右括號減一的方式取得完整方法。
獲得這些數(shù)據(jù)后就可以開始檢索定義的方法了闸翅。我寫了一個類專門用來獲得所有定義的方法
class func parsingWithArray(arr:Array<String>) -> Method {
var mtd = Method()
var returnTypeTf = false //是否取得返回類型
var parsingTf = false //解析中
var bracketCount = 0 //括弧計數(shù)
var step = 0 //1獲取參數(shù)名再芋,2獲取參數(shù)類型,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 {
var shouldAdd = false
if mtd.params.count > 1 {
//處理這種- (void)initWithC:(type)m m2:(type2)i, ... NS_REQUIRES_NIL_TERMINATION;入?yún)槎鄥?shù)情況
if methodParam.type.characters.count > 0 {
shouldAdd = true
}
} else {
shouldAdd = true
}
if shouldAdd {
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
}
}
//括弧結(jié)束后的重置工作
parsingTf = false
types = []
}
} else if parsingTf {
types.append(tk)
//todo:返回block類型會使用.設(shè)置值的方式坚冀,目前獲取用過方法方式?jīng)]有.這種的解析济赎,暫時作為
if tk == Sb.upArrow {
mtd.returnTypeBlockTf = true
}
} else if tk == Sb.colon {
step = 2
} else if step == 1 {
if tk == "initWithCoordinate" {
//
}
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
}
這個方法大概的思路就是根據(jù)標(biāo)記符設(shè)置不同的狀態(tài),然后將獲取的信息放入定義的結(jié)構(gòu)中遗菠。
使用過的方法的解析 ??
進行使用過的方法解析前需要處理的事情
- @“…” 里面的數(shù)據(jù)联喘,因為這里面是允許我們定義的標(biāo)識符出現(xiàn)的。
- 遞歸出文件中 import 所有的類辙纬,根據(jù)對類的使用可以清除無用的 import
- 繼承鏈的獲取豁遭。
- 解析獲取實例化了的成員變量列表。在解析時需要依賴列表里的成員變量名和變量的類進行方法的完整獲取贺拣。
簡單的方法
[view update:status animation:YES];
從左到右按照 : 符號獲取
方法嵌套調(diào)用蓖谢,下面這種情況如何解析出
@weakify(self);
[[[[[[SMNetManager shareInstance] fetchAllFeedWithModelArray:self.feeds] map:^id(NSNumber *value) {
@strongify(self);
NSUInteger index = [value integerValue];
self.feeds[index] = [SMNetManager shareInstance].feeds[index];
return self.feeds[index];
}] doCompleted:^{
//抓完所有的feeds
@strongify(self);
NSLog(@"fetch complete");
//完成置為默認(rèn)狀態(tài)
self.tbHeaderLabel.text = @"";
self.tableView.tableHeaderView = [[UIView alloc] init];
self.fetchingCount = 0;
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
//下拉刷新關(guān)閉
[self.tableView.mj_header endRefreshing];
//更新列表
[self.tableView reloadData];
//檢查是否需要增加源
if ([SMFeedStore defaultFeeds].count > self.feeds.count) {
self.feeds = [SMFeedStore defaultFeeds];
[self fetchAllFeeds];
}
}] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(SMFeedModel *feedModel) {
//抓完一個
@strongify(self);
self.tableView.tableHeaderView = self.tbHeaderView;
//顯示抓取狀態(tài)
self.fetchingCount += 1;
self.tbHeaderLabel.text = [NSString stringWithFormat:@"正在獲取%@...(%lu/%lu)",feedModel.title,(unsigned long)self.fetchingCount,(unsigned long)self.feeds.count];
[self.tableView reloadData];
}];
一開始會想到使用遞歸譬涡,以前我做 STMAssembleView 時就是使用的遞歸闪幽,這樣時間復(fù)雜度就會是 O(nlogn) ,這次我換了個思路涡匀,將復(fù)雜度降低到了 n 盯腌,思路大概是 創(chuàng)建一個字典,鍵值就是深度陨瘩,從左到右深度的增加根據(jù) [ 符號腕够,減少根據(jù) ] 符號级乍,值會在 [ 時創(chuàng)建一個 Method 結(jié)構(gòu)體,根據(jù)]來完成結(jié)構(gòu)體帚湘,將其添加到 methods 數(shù)組中 玫荣。
具體實現(xiàn)如下
class func parsing(contentArr:Array<String>, inMethod:Method) -> Method {
var mtdIn = inMethod
//處理用過的方法
//todo:還要過濾@""這種情況
var psBrcStep = 0
var uMtdDic = [Int:Method]()
var preTk = ""
//處理?:這種條件判斷簡寫方式
var psCdtTf = false
var psCdtStep = 0
//判斷selector
var psSelectorTf = false
var preSelectorTk = ""
var selectorMtd = Method()
var selectorMtdPar = MethodParam()
uMtdDic[psBrcStep] = Method() //初始時就實例化一個method,避免在define里定義只定義]符號
for var tk in contentArr {
//selector處理
if psSelectorTf {
if tk == Sb.colon {
selectorMtdPar.name = preSelectorTk
selectorMtd.params.append(selectorMtdPar)
selectorMtd.pnameId += "\(selectorMtdPar.name):"
} else if tk == Sb.rBktR {
mtdIn.usedMethod.append(selectorMtd)
psSelectorTf = false
selectorMtd = Method()
selectorMtdPar = MethodParam()
} else {
preSelectorTk = tk
}
continue
}
if tk == Sb.selector {
psSelectorTf = true
selectorMtd = Method()
selectorMtdPar = MethodParam()
continue
}
//通常處理
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
//[]不配對的容錯處理
if psBrcStep < 0 {
psBrcStep = 0
}
} 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
}
在設(shè)置 Method 結(jié)構(gòu)體時將參數(shù)名拼接起來成為 Method 的識別符用于后面處理時的快速比對大诸。
解析使用過的方法時有幾個問題需要注意下
1.在方法內(nèi)使用的方法捅厂,會有 respondsToSelector , @selector 還有條件簡寫語法的情況需要單獨處理下资柔。
2.在 #define 里定義使用了方法
#define CLASS_VALUE(x) [NSValue valueWithNonretainedObject:(x)]
找出無用方法 ??
獲取到所有使用方法后進行去重焙贷,和定義方法進行匹對求出差集,即全部未使用的方法建邓。
去除無用方法 ??
比對后獲得無用方法后就要開始注釋掉他們了盈厘。遍歷未使用的方法,根據(jù)先前 Method 結(jié)構(gòu)體中定義了方法所在文件路徑官边,根據(jù)文件集結(jié)構(gòu)和File的結(jié)構(gòu)體沸手,可以避免 IO ,直接獲取方法對應(yīng)的文件內(nèi)容和路徑注簿。
對文件內(nèi)容進行行切割契吉,逐行檢測方法名和參數(shù),匹對時開始對行加上注釋诡渴, h 文件已;符號為結(jié)束捐晶, m 文件會對大括號進行計數(shù),逐行注釋妄辩。實現(xiàn)的方法具體如下:
//刪除指定的一組方法
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 += "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
let methodPnameId = ParsingMethod.parsingWithArray(arr: hMtds).pnameId
if aMethod.pnameId == methodPnameId {
hContentCleaned += hMtdAnnoStr
} else {
hContentCleaned += hMtdStr
}
hMtdAnnoStr = ""
hMtdStr = ""
hMtds = []
}
}
//刪除無用函數(shù)
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 += "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
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文件
//刪除無用函數(shù)
if mContent.characters.count > 0 {
try! mContentCleaned.write(to: URL(string:mFilePath)!, atomically: false, encoding: String.Encoding.utf8)
}
}
}
完整代碼在:https://github.com/ming1016/SMCheckProject 這里惑灵。
后記 ??
有了這樣的結(jié)構(gòu)數(shù)據(jù)就可以模擬更多人工檢測的方式來檢測項目。
通過獲取的方法結(jié)合獲取類里面定義的局部變量和全局變量眼耀,在解析過程中模擬引用的計數(shù)來分析循環(huán)引用等等類似這樣的檢測英支。
通過獲取的類的完整結(jié)構(gòu)還能夠?qū)⑵滢D(zhuǎn)成JavaScriptCore能解析的js語法文件等等。
對于APP瘦身的一些想法 ??
瘦身應(yīng)該從平時開發(fā)時就需要注意哮伟。除了功能和組件上的復(fù)用外還需要對堆棧邏輯進行封裝以達(dá)到代碼壓縮的效果干花。
比如使用ReactiveCocoa和RxSwift這樣的函數(shù)響應(yīng)式編程庫提供的方法和編程模式進行
對于UI的視圖邏輯可以使用一套統(tǒng)一邏輯壓縮代碼使用DSL來簡化寫法等。