百度APP iOS端包体积50M优化实践(四)代码优化
一、前言
GEEK TALK
百度APP iOS端包体积优化实践系列文章回顾:
二、Link Map文件详解
GEEK TALK
2.1 简介
2.2 生成linkMap文件
2.3 LinkMap文件结构解析
丨2.3.1 基础信息
# Path: /Users/richard/Desktop/demo/DerivedData/demo/Build/Products/Debug-iphoneos/demo.app/demo# Arch: arm64
丨2.3.2 Object文件列表
丨2.3.2 Section段表
丨2.3.4 Symbols
第一列是方法起始地址,通过这个地址我们可以查上面的段表;
第二列是大小,通过这个可以算出方法占用的大小;
第三列是归属的类(.o),值是具体编号,通过反查目标文件列表可以知道对应的类;
第四列是方法名称。
三、代码优化
GEEK TALK
3.1 无用类瘦身
丨3.1.1 静态检测获取无用类
方案介绍
第一、获取所有类地址,命令:otool -v -s __DATA __objc_classlist。
otool -v -s __DATA __objc_classlist /Users/ycx/Desktop/demo.app/demo Contents of (__DATA,__objc_classlist) section0000000100008238 00009980 00000001 000099d0 000000010000000100008248 00009a48 00000001 00009a98 000000010000000100008258 00009ac0 00000001 00009b38 00000001
第二、获取引用类的地址,命令:otool -v -s __DATA __objc_classrefs。
otool -v -s __DATA __objc_classrefs /Users/yangchengxu/Desktop/demo.app/demo Contents of (__DATA,__objc_classrefs) section000000010000990000000000 00000000 00000000 00000000000000010000991000000000 00000000 000099d0 00000001000000010000992000000000 00000000 00000000 00000000
第三、取差集,所有类的地址减去引用类的地址,拿到的就是未使用类的地址信息。
第四、符号化,遍历Linkmap和Mach-O文件可获取地址信息对应的具体类名,建立类和地址的映射关系,通过地址反解析出类名。
优缺点
对于存在引用关系但根本不会被调用的类,是无法被判断为无用类的。随着版本迭代,新老员工工作交接,很多功能的入口已经不存在了,相关的类也根本不会被调用,但是引用关系仍然保留。通过静态检测的方式,无法检测出这种情况。
静态检测无法适用于通过反射调用类及方法的场景。因为静态检测无法感知运行时的环境,无法预测哪些类或方法会被反射调用。因此,在这种情况下,静态检测将无法准确地检测出无用类或无用的方法。
丨3.1.2 动态检测获取无用类
方案介绍
// Values for class_rw_t->flags // These are not emitted by the compiler and are never used in class_ro_t.// Their presence should be considered in future ABI versions.// class_t->data is class_rw_t, not class_ro_t#define RW_REALIZED (1<<31)// class is unresolved future class#define RW_FUTURE (1<<30)// class is initialized#define RW_INITIALIZED (1<<29)// class is initializing#define RW_INITIALIZING (1<<28)// class_rw_t->ro is heap copy of class_ro_t#define RW_COPIED_RO (1<<27)// class allocated but not yet registered#define RW_CONSTRUCTING (1<<26)// class allocated and registered#define RW_CONSTRUCTED (1<<25)// available for use; was RW_FINALIZE_ON_MAIN_THREAD// #define RW_24 (1<<24)// class +load has been called#define RW_LOADED (1<<23)
#define W_INITIALIZED (1<<29)bool isinitialized() { return getMeta() -›data()-›flags & W_INITIALIZED;}对于业务线代码没有侵入性; 没有性能损耗; 可以针对线上实际运行环境做检测;
丨3.1.3 手百采用的技术方案
3.2 无用模块瘦身
def find_class(base_link_map_file):link_map_file = open(base_link_map_file, 'rb')reach_files = 0reach_sections = 0reach_symbols = 0files_map = {}while 1:line = link_map_file.readline()line = line.decode('utf-8', errors='ignore')if not line:breakif line.startswith("#"):if line.startswith("# Object files:"):reach_files = 1if line.startswith("# Sections"):reach_sections = 1if line.startswith("# Symbols"):reach_symbols = 1else:if reach_files == 1 and reach_sections == 0 and reach_symbols == 0:index = line.find("]")if index != -1:tmpfile = line[index + 2:-1]file = tmpfile.split("/")[-1]frameworkIndex = file.find("(")if frameworkIndex!= -1:frameworkName = file[0: frameworkIndex]className = file[frameworkIndex + 1:len(file)-1]if files_map:if frameworkName in files_map:files_map[frameworkName] = files_map[frameworkName] + " , " + classNameelse:files_map[frameworkName] = classNameelse:files_map[frameworkName] = classNamelink_map_file.close()return files_map
3.3 无用方法瘦身
3.4 精简重复代码
//生成的 XML 文件内容如下,根据 file 标签信息就能定位到重复代码位置。pmd cpd --files 扫描文件目录 --minimum-tokens 70 --language objectivec --encoding UTF-8 --format xml > repeat.xml检测结果如下所示,其中duplication标签中的lines表示重复内容的行数,file标签表示从那一行开始重复及具体重复文件路径,codefragment标签表示重复的代码。 <pmd-cpd><duplication lines="16" tokens="162"><file begintoken="16933" column="33" endcolumn="4" endline="28" endtoken="17094" line="13" path="path1"><file begintoken="23979" column="47" endcolumn="4" endline="26" endtoken="24140" line="11" path="path2" /><codefragment>***************************</codefragment></duplication></pmd-cpd>
3.5 工具方法瘦身
实现系统类的Category,如NSDate、UIImage、NSArray、NSDictionary的分类方法实现; 独立封装;
遍历LinkMap文件,挖掘出重复的Category,参考以下的脚本代码来实现此功能:
def get_files_map(base_link_map_file):link_map_file = open(base_link_map_file, 'rb')reach_files = 0reach_sections = 0reach_symbols = 0files_map = {}while 1:line = link_map_file.readline()line = line.decode('utf-8', errors='ignore')if not line:breakif line.startswith("#"):if line.startswith("# Object files:"):reach_files = 1if line.startswith("# Sections"):reach_sections = 1if line.startswith("# Symbols"):reach_symbols = 1else:if reach_files == 1 and reach_sections == 0 and reach_symbols == 0:# filesindex = line.find("]")if index != -1:symbol = {"file": line[index + 2:-1]}key = int(line[1: index])files_map[key] = symbolpasslink_map_file.close()return files_map
对于非Category的工具方法,进行排查和合并,最终下沉到统一工具库里面。
3.6 AB实验固话
#define kFaceverifyResourceOptimizeABTestKey @"face_verify_resource_optimize_enable"
static let verifyResourceOptimizeABTestKey: String = "face_verify_resource_optimize_enable"
四、总结
GEEK TALK
END
参考资料:
推荐阅读: