起底 iOS 包瘦身

概述

本文会从图片和代码两个维度,来进行包瘦身实践。

图片层面,可以优化的点包括:

  1. 压缩图片
  2. 修改图片格式
  3. 删除无用图片
  4. 删除重复图片

代码层面,介绍查找并删除 Objective-C 或 Swift 代码的方法。

1 图片优化

1-1 压缩图片

图片压缩可分为有损压损和无损压损。

  1. 有损压缩一般是以牺牲图片的质量为代价来进行的。
  2. 无损压缩则通过去除图片中的无用数据

比如在 png 格式中,有两种类型的数据块:

  1. 必要的关键数据块
  2. 非必要的辅助数据块(相机信息、内嵌缩略图)

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 工程。运行代码后会出现下面界面:

LSUnusedResources 界面

允许设置的参数包括:

  1. Project Path:工程目录的路径
  2. Exclude Folder:设置工程目录的查找黑名单(比如 Pods 文件夹下内容)
  3. Resource Suffix:想要查找的资源后缀
  4. 正则表达式设置:比如在 Objective-C 语言的 .m 文件中查找 @".?",Swift 文件里的 ".?"
  5. 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 包含三部分:

  1. Object File 包含了.o 目标文件和库文件
  2. Section 包含了代码段和数据段在 Mach-O 文件的偏移位置以及大小
  3. Symbols 包含了所有的方法、类、block

我们想要找的方法和类就在 Symbols 中。

Mach-O

Mach-O 文件是 Xcode 编译成功后的产物,使用 Mach-OView 查看信息。

  1. __objc_selrefs 中列出了所有调用过的方法
  2. __objc_classrefs 中列出所有使用过的的类
  3. __objc_superrefs 中列出所有使用的父类

缺陷

  1. 无法找到 performSelector 方法调用的方法
  2. 需要人工进行比对、查找,工作量比较大

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 之间头文件生成等。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270