本文將提供一種靜態分析的方式,用於查找可執行文件中未使用的方法,源碼連接:xuezhulian/selectorsunref。git
分析Mach-o
文件中的__DATA __objc_selrefs
段獲得使用到的方法,經過otool
找出實現的全部方法。取差集獲得未使用的方法。而後過濾setter和getter,過濾協議方法,再加上一些其它的過濾規則獲得最終的結果。github
def unref_selectors(path): ref_sels = ref_selectors(path) imp_sels = imp_selectors(path) protocol_sels = protocol_selectors(path) unref_sels = set() for sel in imp_sels: if ignore_selectors(sel): continue #protocol sels will not apppear in selrefs section if sel not in ref_sels and sel not in protocol_sels: unref_sels = unref_sels.union(filter_selectors(imp_sels[sel])) return unref_sels 複製代碼
使用otool -v -s
輸出__DATA __objc_selrefs
段的信息:bash
def ref_selectors(path): re_selrefs = re.compile('__TEXT:__objc_methname:(.+)') ref_sels = set() lines = os.popen('/usr/bin/otool -v -s __DATA __objc_selrefs %s' % path).readlines() for line in lines: results = re_selrefs.findall(line) if results: ref_sels.add(results[0]) return ref_sels 複製代碼
輸出示例:markdown
00000001030f7ce8 __TEXT:__objc_methname:getMessageRequestFromQQ:
00000001030f7cf0 __TEXT:__objc_methname:SendMessageToQQRequest:
00000001030f7cf8 __TEXT:__objc_methname:responseToGetMessageFromQQ:
00000001030f7d00 __TEXT:__objc_methname:responseToShowMessageFromQQ:
複製代碼
匹配__TEXT:__objc_methname:(.+)
獲得使用到的方法。數據結構
使用otool -oV
輸出可執行文件的詳細信息, 在__DATA,__objc_classlist
這個段裏面記錄了類實現的方法的相關信息:app
Contents of (__DATA,__objc_classlist) section 0000000102bdc190 0x103117798 _OBJC_CLASS_$_EpisodeDetailStatusCell isa 0x103117770 _OBJC_METACLASS_$_EpisodeDetailStatusCell superclass 0x103152988 _OBJC_CLASS_$_TableViewCell cache 0x0 __objc_empty_cache vtable 0x0 data 0x102be84c0 (struct class_ro_t *) flags 0x184 RO_HAS_CXX_STRUCTORS instanceStart 8 instanceSize 16 reserved 0x0 ivarLayout 0x102a2a78f layout map: 0x01 name 0x102a2a775 TTEpisodeDetailStatusCell baseMethods 0x102be83d0 (struct method_list_t *) entsize 24 count 7 name 0x1028606b7 setupConstraintsAdditional types 0x102a489fe v16@0:8 imp 0x10000c1a8 -[TTEpisodeDetailStatusCell setupConstraintsAdditional] name 0x1028606d2 setupUpdateConstraintsAdditional types 0x102a489fe v16@0:8 imp 0x10000c7b8 -[TTEpisodeDetailStatusCell setupUpdateConstraintsAdditional] name 0x1028606f3 bindDataWithEpisode:replayInfo: types 0x102a48a20 v32@0:8@16@24 imp 0x10000d014 -[TTEpisodeDetailStatusCell bindDataWithEpisode:replayInfo:] ... ... 複製代碼
經過匹配\s*imp 0x\w+ ([+|-]\[.+\s(.+)\])
獲得實現的方法,存儲的數據結構{sel:set("-[class sel]","-[class sel]")}
。oop
for line in os.popen('/usr/bin/otool -oV %s' % path).xreadlines(): results = re_sel_imp.findall(line) if results: (class_sel, sel) = results[0] if sel in imp_sels: imp_sels[sel].add(class_sel) else: imp_sels[sel] = set([class_sel]) 複製代碼
直接對ivar
賦值,不會觸發property
的setter
和getter
,這些方法即便不被調用,也不可以刪除。 otool -oV
能夠輸出類的protertieslist
:post
baseProperties 0x102be84a8 entsize 16 count 1 name 0x10293aaa5 pinkPointView attributes 0x10293aab3 T@"UIView",&,N,V_pinkPointView 複製代碼
匹配baseProperties
區間,經過\s*name 0x\w+ (.+)
匹配類的屬性,此時也就獲得了對應的setter和getter方法。spa
#delete setter and getter methods as ivar assignment will not trigger them if re_properties_start.findall(line): is_properties_area = True if re_properties_end.findall(line): is_properties_area = False if is_properties_area: property_result = re_property.findall(line) if property_result: property_name = property_result[0] if property_name and property_name in imp_sels: #properties layout in mach-o is after func imp imp_sels.pop(property_name) setter = 'set' + property_name[0].upper() + property_name[1:] + ':' if setter in imp_sels: imp_sels.pop(setter) 複製代碼
協議調用的方法不會出如今__DATA __objc_selrefs
這個段裏面,過濾協議方法採用的策略是找到相應的.h
文件,正則匹配文件中包含的協議方法。3d
def header_protocol_selectors(file_path): protocol_sels = set() file = open(file_path, 'r') is_protocol_area = False for line in file.readlines(): #delete description line = re.sub('\".*\"', '', line) #delete annotation line = re.sub('//.*', '', line) #match @protocol if re.compile('\s*@protocol\s*\w+').findall(line): is_protocol_area = True #match @end if re.compile('\s*@end').findall(line): is_protocol_area = False #match sel if is_protocol_area and re.compile('\s*[-|+]\s*\(').findall(line): sel_content_match_result = None if ':' in line: #match sel with parameters sel_content_match_result = re.compile('\w+\s*:').findall(line) else: #match sel without parameters sel_content_match_result = re.compile('\w+\s*;').findall(line) if sel_content_match_result: protocol_sels.add(''.join(sel_content_match_result).replace(';', '')) file.close() return protocol_sels 複製代碼
otool -L
能夠打印可執行文件引用到的library
,加上公共前綴/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk'
,獲得絕對路徑。使用find
命令遞歸查找該目錄下全部的.h
文件。
#get system librareis lines = os.popen('otool -L ' + path).readlines() for line in lines: line = line.strip() #delete description line = re.sub('\(.*\)', '', line).strip() if line.startswith('/System/Library/'): library_dir = system_base_dir + '/'.join(line.split('/')[0:-1]) if os.path.isdir(library_dir): header_files = header_files.union(os.popen('find %s -name \"*.h\"' % library_dir).readlines()) 複製代碼
otool -oV
的輸出來看,baseProtocols
會包含協議的方法,可是一些pod
倉庫經過.a
文件導入到宿主工程,這個時候拿不到方法的符號。最終過濾自定義協議方法的時候採用的策略和系統協議方法相同。遞歸遍歷工程目錄(腳本須要輸入的第二個參數)下的.h
文件,匹配協議方法。
header_files = header_files.union(os.popen('find %s -name \"*.h\"' % project_dir).readlines()) for header_path in header_files: header_protocol_sels = header_protocol_selectors(header_path) if header_protocol_sels: protocol_sels = protocol_sels.union(header_protocol_sels) 複製代碼
根據輸出的結果,對一些系統方法進行了過濾。
def ignore_selectors(sel): if sel == '.cxx_destruct': return True if sel == 'load': return True return False 複製代碼
爲了過濾第三方庫的方法,只保留了帶有某些前綴的類的方法,這裏須要根據實際狀況自行修改reserved_prefixs
。
def filter_selectors(sels): filter_sels = set() for sel in sels: for prefix in reserved_prefixs: if sel.startswith(prefix): filter_sels.add(sel) return filter_sels 複製代碼
最終結果保存在腳本路徑下的selectorunref.txt
文件中。和以前整理過的iOS代碼瘦身實踐:刪除無用的類 同樣,這個方式只能作靜態分析,對動態調用無效,最終是否須要刪除,還須要手動確認。