Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

37 Commits

Repository files navigation

使用Swift3开发了个MacOS的程序可以检测出objc项目中无用方法,然后一键全部清理

当项目越来越大,引入第三方库越来越多,上架的 APP 体积也会越来越大,对于用户来说体验必定是不好的。在清理资源,编译选项优化,清理无用类等完成后,能够做而且效果会比较明显的就只有清理无用函数了。

一种方案是我们滴滴的王康基于clang插件这样一个源码级别的分析工具来分析代码间的调用关系达到分析出无用代码的目的,文章在这里: 基于clang插件的一种iOS包大小瘦身方案 文章里对objc方法的定义,调用,实现的全面说明达到了极致,非常值得一看。

另一种方案是根据 Linkmap 文件取到objc的所有类方法和实例方法。再用工具比如 otool 命令逆向出可执行文件里引用到的方法名然后通过求差集得到无用函数,由于API的回调也会被认为是无用函数,所以这个方案还需要将这些回调函数加到白名单里过滤。具体说明,可以看看微信团队的这篇文章: iOS微信安装包瘦身

还有一种使用了 * machoview * 从 Mach-O 里获取信息进行无用方法和文件的处理。阿里有篇文章对 Mach-O 的处理做了详细的说明: 减小ipa体积之删除frameWork中无用mach-O文件

这几个现有方案有些比较麻烦的地方,因为检索出的无用方法没法确定能够直接删除,还需要挨个检索人工判断是否可以删除,这样每次要清理时都需要这样人工排查一遍是非常耗时耗力的。

这样就只有模拟编译过程对代码进行深入分析才能够找出确定能够删除的方法。具体效果可以先试试看,程序代码在:https://github.com/ming1016/SMCheckProject 选择工程目录后程序就开始检索无用方法然后将其注释掉。

设置结构体 😚

首先确定结构,类似先把 OC 文件根据语法画出整体结构。先看看 OC Runtime 里是如何设计的结构体。

structobjc_object { ClassisaOBJC_ISA_AVAILABILITY;
};
/*类*/structobjc_class { ClassisaOBJC_ISA_AVAILABILITY;
#if !__OBJC2__Classsuper_class;
constchar*name;
longversion;
longinfo;
longinstance_size;
structobjc_ivar_list*ivars;
structobjc_method_list**methodLists;
structobjc_cache*cache;
structobjc_protocol_list*protocols;
#endif
};
/*成员变量列表*/structobjc_ivar_list {
intivar_count#ifdef__LP64__intspace#endif/* variable length structure */structobjc_ivarivar_list[1]
} /*成员变量结构体*/structobjc_ivar {
char*ivar_namechar*ivar_typeintivar_offset#ifdef__LP64__intspace#endif
} /*方法列表*/structobjc_method_list { structobjc_method_list*obsolete;
intmethod_count;
#ifdef__LP64__intspace;
#endif/* variable length structure */structobjc_methodmethod_list[1];
};
/*方法结构体*/structobjc_method { SELmethod_name;
char*method_types; /* a string representing argument/return types */IMPmethod_imp;
};

一个 class 只有少量函数会被调用,为了减少较大的遍历所以创建一个 objc_cache ,在找到一个方法后将 method_name 作为 key,将 method_imp 做值,再次发起时就可以直接在 cache 里找。

使用 swift 创建类似的结构体,做些修改

//文件
classFile:NSObject{
//文件
publicvartype=FileType.FileH
publicvarname=""publicvarcontent=""publicvarmethods=[Method]() //所有方法
publicvarimports=[Import]() //引入类
}
//引入
structImport{publicvarfileName=""}
//对象
classObject{publicvarname=""publicvarsuperObject=""publicvarproperties=[Property]()publicvarmethods=[Method]()}
//成员变量
structProperty{publicvarname=""publicvartype=""}structMethod{publicvarclassMethodTf=false //+ or -
publicvarreturnType=""publicvarreturnTypePointTf=falsepublicvarreturnTypeBlockTf=falsepublicvarparams=[MethodParam]()publicvarusedMethod=[Method]()publicvarfilePath="" //定义方法的文件路径,方便修改文件使用
publicvarpnameId="" //唯一标识,便于快速比较
}classMethodParam:NSObject{publicvarname=""publicvartype=""publicvartypePointTf=falsepublicvariName=""}classType:NSObject{
//todo:更多类型
publicvarname=""publicvartype=0 //0是值类型 1是指针
}
```swift
## 开始语法解析 😈
首先遍历目录下所有的文件
```swift
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"])

然后将注释排除在分析之外,这样做能够有效避免无用的解析。

分析是否需要按照行来切割,在 @interface@end@ implementation@end 里面不需要换行,按照;符号,外部需要按行来。所以两种切割都需要。

先定义语法标识符

classSb:NSObject{publicstaticletadd="+"publicstaticletminus="-"publicstaticletrBktL="("publicstaticletrBktR=")"publicstaticletasterisk="*"publicstaticletcolon=":"publicstaticletsemicolon=";"publicstaticletdivide="/"publicstaticletagBktL="<"publicstaticletagBktR=">"publicstaticletquotM="\""publicstaticletpSign="#"publicstaticletbraceL="{"publicstaticletbraceR="}"publicstaticletbktL="["publicstaticletbktR="]"publicstaticletqM="?"publicstaticletupArrow="^"publicstaticletinteface="@interface"publicstaticletimplementation="@implementation"publicstaticletend="@end"publicstaticletselector="@selector"publicstaticletspace=""publicstaticletnewLine="\n"}

接下来就要开始根据标记符号来进行切割分组了,使用 Scanner ,具体方式如下

//根据代码文件解析出一个根据标记符切分的数组
classfunc createOCTokens(conent:String)->[String]{varstr= conent
str =self.dislodgeAnnotaion(content: str)
//开始扫描切割
letscanner=Scanner(string: str)vartokens=[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]varoperatersString=""foropin operaters {
operatersString = operatersString.appending(op)}varset=CharacterSet()
set.insert(charactersIn: operatersString)
set.formUnion(CharacterSet.whitespacesAndNewlines)while !scanner.isAtEnd {foroperaterin operaters {if(scanner.scanString(operater, into:nil)){
tokens.append(operater)}}varresult:NSString?
result =nil;
if scanner.scanUpToCharacters(from: set, into:&result){
tokens.append(result as!String)}}
tokens = tokens.filter{
$0 !=Sb.space
}return tokens;
}

行解析的方法

//根据代码文件解析出一个根据行切分的数组
classfunc createOCLines(content:String)->[String]{varstr= content
str =self.dislodgeAnnotaion(content: str)letstrArr= str.components(separatedBy:CharacterSet.newlines)return strArr
}

根据结构将定义的方法取出 🤖

- (id)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(NSString *)path cacheTime:(NSInteger)cacheTime subDirectory:(NSString*)subDirectory;

这里按照语法规则顺序取出即可,将方法名,返回类型,参数名,参数类型记录。这里需要注意 Block 类型的参数

- (STMPartMaker *(^)(STMPartColorType))colorTypeIs;

这种类型中还带有括号的语法的解析,这里用到的方法是对括号进行计数,左括号加一右括号减一的方式取得完整方法。

获得这些数据后就可以开始检索定义的方法了。我写了一个类专门用来获得所有定义的方法

classfunc parsingWithArray(arr:Array<String>)->Method{varmtd=Method()varreturnTypeTf=false //是否取得返回类型
varparsingTf=false //解析中
varbracketCount=0 //括弧计数
varstep=0 //1获取参数名,2获取参数类型,3获取iName
vartypes=[String]()varmethodParam=MethodParam()
//print("\(arr)")
forvar tkin arr {
tk = tk.replacingOccurrences(of:Sb.newLine, with:"")if(tk ==Sb.semicolon || tk ==Sb.braceL) && step !=1{varshouldAdd=falseif mtd.params.count >1{
//处理这种- (void)initWithC:(type)m m2:(type2)i, ... NS_REQUIRES_NIL_TERMINATION;入参为多参数情况
if methodParam.type.characters.count >0{
shouldAdd =true}}else{
shouldAdd =true}if shouldAdd {
mtd.params.append(methodParam)
mtd.pnameId = mtd.pnameId.appending("\(methodParam.name):")}}elseif tk ==Sb.rBktL {
bracketCount +=1
parsingTf =true}elseif tk ==Sb.rBktR {
bracketCount -=1if bracketCount ==0{vartypeString=""fortypeTkin 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 =[]}}elseif parsingTf {
types.append(tk)
//todo:返回block类型会使用.设置值的方式,目前获取用过方法方式没有.这种的解析,暂时作为
if tk ==Sb.upArrow {
mtd.returnTypeBlockTf =true}}elseif tk ==Sb.colon {
step =2}elseif step ==1{if tk =="initWithCoordinate"{
//
}
methodParam.name = tk
step =0}elseif step ==3{
methodParam.iName = tk
step =1
mtd.params.append(methodParam)
mtd.pnameId = mtd.pnameId.appending("\(methodParam.name):")
methodParam =MethodParam()}elseif tk !=Sb.minus && tk !=Sb.add {
methodParam.name = tk
}}//遍历
return mtd
}

这个方法大概的思路就是根据标记符设置不同的状态,然后将获取的信息放入定义的结构中。

使用过的方法的解析 😱

进行使用过的方法解析前需要处理的事情

  • @“…” 里面的数据,因为这里面是允许我们定义的标识符出现的。
  • 递归出文件中 import 所有的类,根据对类的使用可以清除无用的 import
  • 继承链的获取。
  • 解析获取实例化了的成员变量列表。在解析时需要依赖列表里的成员变量名和变量的类进行方法的完整获取。

简单的方法

[view update:status animation:YES];

从左到右按照 : 符号获取

方法嵌套调用,下面这种情况如何解析出

@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");
//完成置为默认状态
self.tbHeaderLabel.text = @"";
self.tableView.tableHeaderView = [[UIView alloc] init];
self.fetchingCount = 0;
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
//下拉刷新关闭
[self.tableView.mj_header endRefreshing];
//更新列表
[self.tableView reloadData];
//检查是否需要增加源if ([SMFeedStore defaultFeeds].count > self.feeds.count) {
self.feeds = [SMFeedStore defaultFeeds];
[selffetchAllFeeds];
}
}] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(SMFeedModel *feedModel) {
//抓完一个
@strongify(self);
self.tableView.tableHeaderView = self.tbHeaderView;
//显示抓取状态
self.fetchingCount += 1;
self.tbHeaderLabel.text = [NSStringstringWithFormat:@"正在获取%@...(%lu/%lu)",feedModel.title,(unsignedlong)self.fetchingCount,(unsignedlong)self.feeds.count];
[self.tableView reloadData];
}];

一开始会想到使用递归,以前我做 STMAssembleView 时就是使用的递归,这样时间复杂度就会是 O(nlogn) ,这次我换了个思路,将复杂度降低到了 n ,思路大概是 创建一个字典,键值就是深度,从左到右深度的增加根据 [ 符号,减少根据 ] 符号,值会在 [ 时创建一个 Method 结构体,根据]来完成结构体,将其添加到 methods 数组中 。

具体实现如下

classfunc parsing(contentArr:Array<String>, inMethod:Method)->Method{varmtdIn= inMethod
//处理用过的方法
//todo:还要过滤@""这种情况
varpsBrcStep=0varuMtdDic=[Int:Method]()varpreTk=""
//处理?:这种条件判断简写方式
varpsCdtTf=falsevarpsCdtStep=0
//判断selector
varpsSelectorTf=falsevarpreSelectorTk=""varselectorMtd=Method()varselectorMtdPar=MethodParam()uMtdDic[psBrcStep]=Method() //初始时就实例化一个method,避免在define里定义只定义]符号
forvar tkin contentArr {
//selector处理
if psSelectorTf {if tk ==Sb.colon {
selectorMtdPar.name = preSelectorTk
selectorMtd.params.append(selectorMtdPar)
selectorMtd.pnameId +="\(selectorMtdPar.name):"}elseif 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 +=1uMtdDic[psBrcStep]=Method()}elseif 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}}elseif tk ==Sb.colon {
//条件简写情况处理
if psCdtTf && psCdtStep ==0{
psCdtTf =falsecontinue}
//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):"))!
}}elseif tk ==Sb.qM {
psCdtTf =true}else{
tk = tk.replacingOccurrences(of:Sb.newLine, with:"")
preTk = tk
}}return mtdIn
}

在设置 Method 结构体时将参数名拼接起来成为 Method 的识别符用于后面处理时的快速比对。

解析使用过的方法时有几个问题需要注意下 1.在方法内使用的方法,会有 respondsToSelector@selector 还有条件简写语法的情况需要单独处理下。 2.在 #define 里定义使用了方法

#defineCLASS_VALUE(x) [NSValuevalueWithNonretainedObject:(x)]

找出无用方法 😄

获取到所有使用方法后进行去重,和定义方法进行匹对求出差集,即全部未使用的方法。

去除无用方法 😎

比对后获得无用方法后就要开始注释掉他们了。遍历未使用的方法,根据先前 Method 结构体中定义了方法所在文件路径,根据文件集结构和File的结构体,可以避免 IO ,直接获取方法对应的文件内容和路径。 对文件内容进行行切割,逐行检测方法名和参数,匹对时开始对行加上注释, h 文件已;符号为结束, m 文件会对大括号进行计数,逐行注释。实现的方法具体如下:

//删除指定的一组方法
classfunc delete(methods:[Method]){print("无用方法")foraMethodin methods {print("\(File.desDefineMethodParams(paramArr: aMethod.params))")
//开始删除
//continue
varhContent=""varmContent=""varmFilePath= 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 =tryString(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文件------------------
varpsHMtdTf=falsevarhMtds=[String]()varhMtdStr=""varhMtdAnnoStr=""varhContentCleaned=""forhOneLinein hContentArr {varline= 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)}elseif 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 =falseletmethodPnameId=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文件----------------
varmDeletingTf=falsevarmBraceCount=0varmContentCleaned=""varmMtdStr=""varmMtdAnnoStr=""varmMtds=[String]()varpsMMtdTf=falseformOneLinein mContentArr {letline= mOneLine.trimmingCharacters(in:CharacterSet.whitespacesAndNewlines)if mDeletingTf {letlTokens=self.createOCTokens(conent: line)
mMtdAnnoStr +="//"+ mOneLine + Sb.newLine
fortkin lTokens {if tk ==Sb.braceL {
mBraceCount +=1}if tk ==Sb.braceR {
mBraceCount -=1if 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
}elseif 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 =falseletmethodPnameId=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 这里。

后记 🦁

有了这样的结构数据就可以模拟更多人工检测的方式来检测项目。

通过获取的方法结合获取类里面定义的局部变量和全局变量,在解析过程中模拟引用的计数来分析循环引用等等类似这样的检测。 通过获取的类的完整结构还能够将其转成JavaScriptCore能解析的js语法文件等等。

对于APP瘦身的一些想法 👽

瘦身应该从平时开发时就需要注意。除了功能和组件上的复用外还需要对堆栈逻辑进行封装以达到代码压缩的效果。

比如使用ReactiveCocoa和RxSwift这样的函数响应式编程库提供的方法和编程模式进行

对于UI的视图逻辑可以使用一套统一逻辑压缩代码使用DSL来简化写法等。

About

使用Swift3开发了个MacOS的程序可以检测出objc项目中无用方法,然后一键全部清理

Resources

Stars

553 stars

Watchers

15 watching

Forks

Releases

Packages

Used by

Contributors

Languages