概述
本文会从图片和代码两个维度,来进行包瘦身实践。
图片层面,可以优化的点包括:
- 压缩图片
- 修改图片格式
- 删除无用图片
- 删除重复图片
代码层面,介绍查找并删除 Objective-C 或 Swift 代码的方法。
1 图片优化
1-1 压缩图片
图片压缩可分为有损压损和无损压损。
- 有损压缩一般是以牺牲图片的质量为代价来进行的。
- 无损压缩则通过去除图片中的无用数据
比如在 png 格式中,有两种类型的数据块:
- 必要的
关键数据块
- 非必要的
辅助数据块
(相机信息、内嵌缩略图)
png 的无损压缩,就是通过去除非必要的辅助数据块
来实现的。
这里推荐两种压缩方式:
tinypng
tinypng 属于有损压缩,压缩率最高能达到 70% 以上。提供的是网站压缩服务。
ImageOptim
ImageOptim 同时提供无损压缩和有损选项,可以最大程度保证图片的原始清晰度和细节。提供的是 Mac 软件。
1-2 更改图片格式(ing)
除了传统使用的 png 图片格式,我们还可以考虑选择 WebP。
WebP 是一款由 Google 开源的图片格式,其主要优势是压缩率高。在无损压缩模式下,大小比 png 格式少 26%。有损模式模式下,比 JPEG 图片小 25-34%。
通过工具 cwebp 可以将其它格式的图片转为 WebP 格式。
// 通用写法
cwebp [options] input_file -o output_file.webp
// 有损压缩
cwebp -lossless original.png -o new.webp
// 无损压缩
cwebp -lossless original.png -o new.webp
关于 WebP 在 iOS 中的应用,我自己建了一个直接可编译的 demo 工程,供参考,GitHub 链接。
如果想要对 WebP 有更多了解,请参看移动端图片格式调研。
1-3 删除无用图片
这里推荐使用 GitHub 上的开源库 LSUnusedResources,查找代码工程中未使用的资源(包括图片、mp3、mp4)。
下载下来的是一个 Objective-C 的 Mac 工程。运行代码后会出现下面界面:
允许设置的参数包括:
- Project Path:工程目录的路径
- Exclude Folder:设置工程目录的查找黑名单(比如 Pods 文件夹下内容)
- Resource Suffix:想要查找的资源后缀
- 正则表达式设置:比如在 Objective-C 语言的 .m 文件中查找 @".?",Swift 文件里的 ".?"
- Ignore similar name:是否忽略名字类似的图片,勾选后,即代表在代码中出现 tag_%d,tag_1.png 即被认为使用过
其源代码核心代码如下:
// 将工程文件夹下,符合该后缀要求的文件全部找出来,并存在一个字典中
NSDictionary *dic = [[ResourceFileSearcher sharedObject] startWithProjectPath:projectPath excludeFolders:excludeFolders resourceSuffixs:resourceSuffixs];
// 查找源代码文件中的所有字符串,并存储到一个集合中
NSSet *set = [[ResourceStringSearcher sharedObject] startWithProjectPath:projectPath excludeFolders:excludeFolders resourceSuffixs:resourceSuffixs resourcePatterns:[self resourcePatterns]];
NSMutableArray *unusedResult = [NSMutableArray array];
for (NSString *name in resNames) {
// 如果集合 set 中,则说明该资源文件未被使用
if (![set containsObject:name]) {
[unusedResult append:dic[name]];
}
}
// unusedResult 即为所有未使用的资源文件信息
1-4 删除重复图片
重复图片的查找问题,这里我们转换为比对不同图片 MD5 值的问题。
如果两张图片数据的 MD5 值相同,可以判定为相同图片。
具体代码如下:
import os
import sys
import hashlib
# 调用方式
# python repeat_image.py 目录路径
def find_repeat_image(sourceDir):
result = []
# 遍历文件夹
for dirpath, _, filenames in os.walk(sourceDir):
md5list = {}
for filename in filenames:
path = os.path.join(dirpath, filename)
# 判断是否是目录
if os.path.isdir(path):
continue
# init md5
md5obj = hashlib.md5()
# open rb是读取二进制文件
fd = open(path, 'rb')
buff = fd.read()
md5obj.update(buff)
fd.close()
# 获取小写 md5 字符串
filemd5 = str(md5obj.hexdigest()).lower()
# 检查该 md5 是否已经存在
if filemd5 in md5list:
md5list[filemd5].add(path)
else:
md5list[filemd5] = set([path])
for key in md5list:
list = md5list[key]
# 超过 1,则说明有存在重复
if len(list) > 1:
result.append(list)
return result
# 调用方式
arr = find_repeat_image(sys.argv[1])
if len(arr) == 0:
print("无重复图片")
else:
for repeat in arr:
print("-----------重复图片有------------")
for item in repeat:
print(item)
2 代码优化
2-1 LinkMap & Mach-O
注:该方法只适用于 Objective-C
大致思路是从 LinkMap 获取工程文件中所有类、方法信息,从 Mach-O 找到所有使用过的类、方法,两者的差值即为未使用的代码。
LinkMap
在 Xcode - Build Settings 中设置 Write Link Map File 为 YES,Path to Link Map File 设置为需要输出的 txt 文件。
这里说一个小技巧,在 $(SRCROOT)
可以代表当前工程的根目录。
具体位置如下图:
LinkMap 包含三部分:
- Object File 包含了.o 目标文件和库文件
- Section 包含了代码段和数据段在 Mach-O 文件的偏移位置以及大小
- Symbols 包含了所有的方法、类、block
我们想要找的方法和类就在 Symbols 中。
Mach-O
Mach-O 文件是 Xcode 编译成功后的产物,使用 Mach-OView 查看信息。
-
__objc_selrefs
中列出了所有调用过的方法 -
__objc_classrefs
中列出所有使用过的的类 -
__objc_superrefs
中列出所有使用的父类
缺陷
- 无法找到 performSelector 方法调用的方法
- 需要人工进行比对、查找,工作量比较大
2-2 运行时检查类是否被使用过
注:该方法只适用于 Objective-C
思路:
我们知道在 objc 中类的 initialize 方法执行时机是在首次向该类发送消息时。那么是不是就意味着在 objc 源码会记录是否初始化呢?
答案是有的,在 objc 源码中,可以找到以下代码:
#define RW_INITIALIZED (1<<29)
struct objc_class : objc_object {
bool isInitialized() {
return getMeta()->data()->flags & RW_INITIALIZED;
}
}
因此在运行时尽可能跑完 App 所有场景后,如果某个类对象的 isInitialized 属性仍然返回 NO,则可以判定该类没有被使用到。
接下来遗留的问题是,objc 源码的实现细节是对开发者屏蔽的,在代码工程中如何访问到 isInitialized 属性呢?
解决办法是参照 objc 源码,创建对应的数据结构。然后进行强转访问。
// 比如在源码中有一个 objc_class,就仿照创建一个 zyy_objc_class
struct zyy_objc_class : zyy_objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;
public:
class_rw_t* data() {
return bits.data();
}
zyy_objc_class* metaClass() {
return (zyy_objc_class *)((long long)isa & ISA_MASK);
}
};
检查方法:
+ (BOOL)isObjInitialize {
zyy_objc_class *cls = (__bridge zyy_objc_class *)([UsedCodeClass class]);
class_rw_t *metaClassData = cls->metaClass()->data();
bool isInitialized = (metaClassData->flags) & RW_INITIALIZED;
return isInitialized;
}
2-3 查找 Swift 中未使用方法和类
推荐一个三方库 periphery 可以查找 Swift 中未使用的类和方法。
不过要注意的是,因为 OC 的动态性,所以当 Swift 代码暴露给 OC 时便被认定为已使用的代码。
periphery 是基于 SourceKit 实现的,做 Swift 开发的应该也会熟悉另一款基于 SourceKit 实现的 Swiftlint。
SourceKit 提供的功能包括:源代码转换
、语法高亮
、排版
、代码自动补全
、Swift OC 之间头文件生成
等。