普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月3日首页

得物 iOS 启动优化之 Building Closure

作者 得物技术
2025年4月3日 09:50

得物一直重视用户体验,尤其是启动时长这一重要指标。在近期的启动时长跟进中,我们发现了在BuildingClosure 阶段的一个优化方式,成功的帮助我们降低了 1/5 的 BuildingClosure 阶段的启动耗时。Building Closure 并非工程的编译阶段(虽然它有一个building),Building Closure 是应用初次启动时会经历的阶段,因此它会影响应用的启动时长。

单就BuildingClosure阶段而言,我们观察到该阶段其中一个函数从 480ms 暴增到 1200ms 左右(PC 电脑端运行 dyld 调试统计耗时数据),我们通过优化,将耗时从1200ms降低到110ms。即使相比最开始的情况,也相当于从480ms降低到了110ms,由此可见Building Closure 优化是应用进行启动优化必不可少的一个重要手段。因此在这里我们也和各位读者进行分享,期望能够对各自项目有所帮助。

一、神秘的 BuildingClosure

启动优化的技术、实现方案业界有不少的文章可以参考学习,这里不再额外赘述。我们来探索下启动过程中非常神秘的 BuildingClosure。

BuildingClosure 是在 System Interface Initialization 阶段 dyld 生成的,并且我们也无法做任何的干预,另外相关的剖析文章相对较少,所以说 BuildingClosure 较为神秘,也是实至名归。

BuildingClosure 是由 dyld 在应用启动阶段执行的,所以想要了解 BuildingClosure 还是要从 dyld 开始了解。

1.1 dyld && BuildingClosure

Dyld 源码可以在 Apple GitHub 上查阅 github.com/apple-oss-d…

相信大家都应该了解过,BuildingClosure 是在 iOS 13 引入进来的,对应的 dyld 为 dyld3,目的是为了减少启动环节符号查找、Rebase、Bind 的耗时。

核心技术逻辑是将重复的启动工作只做一次,在 App 首次启动、版本更新、手机重启之后的这次启动过程中,将相关信息缓存到 Library/Caches/com.app.dyld/xx.dyld 文件中,App 在下次启动时直接使用缓存好的信息,进而优化二次启动的速度。

在 iOS 15 Dyld4 中更是引入了 SwiftConformance,进一步解决了运行时 Swift 中的类型、协议检查的耗时。

图片

以上优化,我们都无需做任何工作即可享受 dyld 带来的启动速度的优化,可以感受到 Apple 的开发人员也在关心启动速度并为之做了大量的工作。

1.2 BuildingClosure 非常耗时

我们通过 instrument 观测到 BuildingClosure 的耗时占据了启动耗时将近 1/3 的时间。

虽然说,BuildingClosure 只会在首次启动、版本更新、手机重启的第一次启动生成和耗时,但是对用户的体验影响是非常之大的。

图片

1.3 BuildingClosure 文件解析

我们通过对 dyld 的编译和搭建模拟手机环境,成功模拟器了 dyld 加载可执行文件的过程,也就成功解析了 BuildingClosure 文件。BuildingClosure 文件数据格式如下(数据格式、注释仅供参考,并非全部的数据格式):

图片

BuildingClosure 文件内部结构(数据格式、注释仅供参考)

其中占用比较大的部分主要为 Loader-selectorReferencesFixupsSize SwiftTypeConformance  objcSelector objcClass

二、离奇的启动耗时暴增事件

如上,我们已经对 BuildingClosure 有了基本的了解和对 dyld 的执行过程有了一定的了解。但是这份宁静在某一天突然被打破。

2.1 启动耗时暴增 200ms

在我们一个新版本开发过程中,例行对启动耗时进行跟踪测试,但是发现新版本启动耗时暴增 200ms,可以说是灾难级别的事情。

我们开始对最近的出包做了基本的耗时统计,方式为基于 instrument,统计出来启动各个阶段的耗时数据。经过对比,可以明显观测到,200ms 耗时的增加表现在 BuildingClosure 这个环节。

但是 BuildingClosure 耗时的增加既不是阶梯式增加,也不是线性增加,并且只在新版本有增加。在排除相关因素(动态库、工程配置、打包脚本、编译环境)之后,仍然没有定位明确的原因。

在以上定位工作之后,最终确定耗时确实在 dyld 的 BuildingClosure 阶段耗时,并且怀疑可能是某些代码触发了 Dyld 的隐藏彩蛋。所以我们开始了对 BuildingClosure 更深一步的研究。

2.2 BuildingClosure 耗时异常变化定位

通过使用 Instrument 对 System Interface Initialization 阶段进行堆栈分析,最终发现了耗时最高的函数:dyld4::PrebuiltObjC::generateHashTables(dyld4::RuntimeState&)

在对比了新老版本数据,耗时变化差异的函数也是此函数,我们简称为 generateHashTables。这样使得我们更加确定耗时为 dyld 过程中的 BuildingClosure 阶段。

图片

使用 Instrument 分析 BuildingClosure 阶段耗时

三、启动优化新秘境

在发现 BuildingClosure 生成过程中耗时占比非常大,并且有异常时,起初并没有意识到有什么问题,因为这是 dyld 内的代码,并未感觉会有什么问题。但是一切都指向了该函数,于是开始撸起袖子看代码。

从代码中可以看到,此处是为了生成 BuildingClosure 中 objcSelector objcClass objcProtocol 这三个部分的 HashTable(可以参考上面的 【BuildingClosure 文件解析】部分)。

拿起 dyld 开始对耗时异常版本的可执行文件进行调试,通过对该函数和内部实现的代码逻辑阅读,以及增加耗时信息打印。最终确定,耗时的代码在 make_perfect 这个函数中,这个函数是对【输入的字符串列表】生成一个【完美 Hash 表】。

void PrebuiltObjC::generateHashTables(RuntimeState& state)
{
    // Write out the class table
    writeObjCDataStructHashTable(state, PrebuiltObjC::ObjCStructKind::classes, objcImages, classesHashTable, duplicateSharedCacheClassMap, classMap);
    // Write out the protocol table
    writeObjCDataStructHashTable(state, PrebuiltObjC::ObjCStructKind::protocols, objcImages, protocolsHashTable, duplicateSharedCacheClassMap, protocolMap);
    // If we have closure selectors, we need to make a hash table for them.
    if ( !closureSelectorStrings.empty() ) {
        objc::PerfectHash phash;
        objc::PerfectHash::make_perfect(closureSelectorStrings, phash);
        size_t size = ObjCStringTable::size(phash);
        selectorsHashTable.resize(size);
        //printf("Selector table size: %lld\n", size);
        selectorStringTable = (ObjCStringTable*)selectorsHashTable.begin();
        selectorStringTable->write(phash, closureSelectorMap.array());
    }
}

继续深入了解 make_perfect 这个函数的实现。

3.1 Perfect Hash

通过对研读代码逻辑和耗时分析,最终定位到耗时代码部分为PerfectHash.cpp 中 findhash 函数,这个函数也是 完美散列函数 的核心逻辑。

这里涉及到了一个概念PerfectHash,PerfectHash 的核心是完美散列函数,我们看下维基百科的解释:

zh.wikipedia.org/wiki/%E5%AE…

对集合S的完美散列函数是一个将S的每个元素映射到一系列无冲突的整数的哈希函数

简单来讲 完美散列函数 是【对输入的字符串列表】【为每个字符串生成一个唯一整数】。

for (si=1; ; ++si)
    {
        ub4 rslinit;
        /* Try to find distinct (A,B) for all keys */
        *salt = si * 0x9e3779b97f4a7c13LL; /* golden ratio (arbitrary value) */
        initnorm(keys, *alen, blen, smax, *salt);
        rslinit = inittab(tabb, keys, FALSE);
        if (rslinit == 0)
        {
            /* didn't find distinct (a,b) */
            if (++bad_initkey >= RETRY_INITKEY)
            {
                /* Try to put more bits in (A,B) to make distinct (A,B) more likely */
                if (*alen < maxalen)
                {
                    *alen *= 2;
                }
                else if (blen < smax)
                {
                    blen *= 2;
                    tabb.resize(blen);
                    tabq.resize(blen+1);
                }
                bad_initkey0;
                bad_perfect0;
            }
            continue;                             /* two keys have same (a,b) pair */
        }
        /* Given distinct (A,B) for all keys, build a perfect hash */
        if (!perfect(tabb, tabh, tabq, smax, scramble, (ub4)keys.count()))
        {
            if (++bad_perfect >= RETRY_PERFECT)
            {
                if (blen < smax)
                {
                    blen *= 2;
                    tabb.resize(blen);
                    tabq.resize(blen+1);
                    --si;               /* we know this salt got distinct (A,B) */
                }
                else
                {
                    return false;
                }
                bad_perfect0;
            }
            continue;
        }
        break;
    }

此时通过对比新老版本的数据(使用 dyld 分别运行新老版本的可执行文件对比打印的日志),发现:

  • 老版本循环了 31 次成功生成 HashTable

  • 新版本循环了 92 次成功生成 HashTable

至此,我们距离成功已经非常接近了,于是进一步研读 dyld 源码和增加了更多打印信息代码,最终找到了相互冲突的函数字符串名称。

/*
 * put keys in tabb according to key->b_k
 * check if the initial hash might work
 */
static int inittab_ts(dyld3::OverflowSafeArray<bstuff>& tabb, dyld3::OverflowSafeArray<key>& keys, int complete, int si)
// bstuff   *tabb;                     /* output, list of keys with b for (a,b) */
// ub4       blen;                                            /* length of tabb */
// key      *keys;                               /* list of keys already hashed */
// int       complete;        /* TRUE means to complete init despite collisions */
{
  int  nocollision = TRUE;
  ub4 i;
  memset((void *)tabb.begin(), 0, (size_t)(sizeof(bstuff)*tabb.maxCount()));
  /* Two keys with the same (a,b) guarantees a collision */
  for (i0; i < keys.count(); i++) {
    key *mykey = &keys[i];
    key *otherkey;
    for (otherkey=tabb[mykey->b_k].list_b;
     otherkey;
     otherkey=otherkey->nextb_k)
    {
      if (mykey->a_k == otherkey->a_k)
      {
          // 打印冲突的字符串
        std::cout << mykey->name_k << " and " << otherkey->name_k << " has the same ak " << otherkey->a_k << " si is " << si << std::endl;
        nocollision = FALSE;
          /* 屏蔽此处代码,有冲突的情况下,继续执行,便于打印所有的冲突
    if (!complete)
      return FALSE;
           */
      }
    }
    ++tabb[mykey->b_k].listlen_b;
    mykey->nextb_k = tabb[mykey->b_k].list_b;
    tabb[mykey->b_k].list_b = mykey;
  }
  /* no two keys have the same (a,b) pair */
  return nocollision;
}

根据以上信息,我们已经了解到在Building Closure阶段中,可能存在字符串的 Hash 碰撞 引发循环次数大幅增加,进而引发了启动耗时暴增。

在经过 dyld 调试的耗时数据、构建出包后验证的数据验证后,通过避免 Hash 碰撞,我们完成了启动时长的优化。

3.2 向前一步

其实从打印的冲突函数名称来看,历史代码中已经存在了 Hash 碰撞 的现象。

猜想,如果我们解决了所有的字符串的 Hash 碰撞,岂不是不仅可以修复启动耗时异常上升的问题,还可以进一步降低启动耗时,提高启动速度?

于是我们对每个有碰撞的函数名称进行修改,经过出包验证,结果与我们猜测的一致,启动耗时有明显的下降。

图片

数据为 PC 电脑端运行 dyld 生成 BuildingClosure 的耗时数据,非手机端数据

四、总结

我们探索了 BuildingClosure 的生成过程,发现在Building Closure阶段中,可能存在字符串的 Hash 碰撞 引发循环次数大幅增加,进而引发了启动耗时暴增,进而导致启动耗时的大幅增加。

我们也发现,Building Closure Hash碰撞相关的启动耗时,其实与项目配置、编译环境、打包脚本等均无任何关系,就只是存在了字符串的Hash 碰撞 ,才引发循环次数大幅增加,进而导致启动时长增加。

往期回顾

1.分布式数据一致性场景与方案处理分析|得物技术

2.从对话到自主行动:AI应用如何从 Chat 进化为 Agent?开源项目源码深度揭秘|得物技术

3.得物技术部算法项目管理实践分享

4.商家域稳定性建设之原理探索|得物技术

5.得物 Android Crash 治理实践

文 / 道隐

关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

得物 iOS 启动优化之 Building Closure

作者 得物技术
2025年4月3日 09:50

得物一直重视用户体验,尤其是启动时长这一重要指标。在近期的启动时长跟进中,我们发现了在BuildingClosure 阶段的一个优化方式,成功的帮助我们降低了 1/5 的 BuildingClosure 阶段的启动耗时。Building Closure 并非工程的编译阶段(虽然它有一个building),Building Closure 是应用初次启动时会经历的阶段,因此它会影响应用的启动时长。

单就BuildingClosure阶段而言,我们观察到该阶段其中一个函数从 480ms 暴增到 1200ms 左右(PC 电脑端运行 dyld 调试统计耗时数据),我们通过优化,将耗时从1200ms降低到110ms。即使相比最开始的情况,也相当于从480ms降低到了110ms,由此可见Building Closure 优化是应用进行启动优化必不可少的一个重要手段。因此在这里我们也和各位读者进行分享,期望能够对各自项目有所帮助。

一、神秘的 BuildingClosure

启动优化的技术、实现方案业界有不少的文章可以参考学习,这里不再额外赘述。我们来探索下启动过程中非常神秘的 BuildingClosure。

BuildingClosure 是在 System Interface Initialization 阶段 dyld 生成的,并且我们也无法做任何的干预,另外相关的剖析文章相对较少,所以说 BuildingClosure 较为神秘,也是实至名归。

BuildingClosure 是由 dyld 在应用启动阶段执行的,所以想要了解 BuildingClosure 还是要从 dyld 开始了解。

1.1 dyld && BuildingClosure

Dyld 源码可以在 Apple GitHub 上查阅 github.com/apple-oss-d…

相信大家都应该了解过,BuildingClosure 是在 iOS 13 引入进来的,对应的 dyld 为 dyld3,目的是为了减少启动环节符号查找、Rebase、Bind 的耗时。

核心技术逻辑是将重复的启动工作只做一次,在 App 首次启动、版本更新、手机重启之后的这次启动过程中,将相关信息缓存到 Library/Caches/com.app.dyld/xx.dyld 文件中,App 在下次启动时直接使用缓存好的信息,进而优化二次启动的速度。

在 iOS 15 Dyld4 中更是引入了 SwiftConformance,进一步解决了运行时 Swift 中的类型、协议检查的耗时。

图片

以上优化,我们都无需做任何工作即可享受 dyld 带来的启动速度的优化,可以感受到 Apple 的开发人员也在关心启动速度并为之做了大量的工作。

1.2 BuildingClosure 非常耗时

我们通过 instrument 观测到 BuildingClosure 的耗时占据了启动耗时将近 1/3 的时间。

虽然说,BuildingClosure 只会在首次启动、版本更新、手机重启的第一次启动生成和耗时,但是对用户的体验影响是非常之大的。

图片

1.3 BuildingClosure 文件解析

我们通过对 dyld 的编译和搭建模拟手机环境,成功模拟器了 dyld 加载可执行文件的过程,也就成功解析了 BuildingClosure 文件。BuildingClosure 文件数据格式如下(数据格式、注释仅供参考,并非全部的数据格式):

图片

BuildingClosure 文件内部结构(数据格式、注释仅供参考)

其中占用比较大的部分主要为 Loader-selectorReferencesFixupsSize SwiftTypeConformance  objcSelector objcClass

二、离奇的启动耗时暴增事件

如上,我们已经对 BuildingClosure 有了基本的了解和对 dyld 的执行过程有了一定的了解。但是这份宁静在某一天突然被打破。

2.1 启动耗时暴增 200ms

在我们一个新版本开发过程中,例行对启动耗时进行跟踪测试,但是发现新版本启动耗时暴增 200ms,可以说是灾难级别的事情。

我们开始对最近的出包做了基本的耗时统计,方式为基于 instrument,统计出来启动各个阶段的耗时数据。经过对比,可以明显观测到,200ms 耗时的增加表现在 BuildingClosure 这个环节。

但是 BuildingClosure 耗时的增加既不是阶梯式增加,也不是线性增加,并且只在新版本有增加。在排除相关因素(动态库、工程配置、打包脚本、编译环境)之后,仍然没有定位明确的原因。

在以上定位工作之后,最终确定耗时确实在 dyld 的 BuildingClosure 阶段耗时,并且怀疑可能是某些代码触发了 Dyld 的隐藏彩蛋。所以我们开始了对 BuildingClosure 更深一步的研究。

2.2 BuildingClosure 耗时异常变化定位

通过使用 Instrument 对 System Interface Initialization 阶段进行堆栈分析,最终发现了耗时最高的函数:dyld4::PrebuiltObjC::generateHashTables(dyld4::RuntimeState&)

在对比了新老版本数据,耗时变化差异的函数也是此函数,我们简称为 generateHashTables。这样使得我们更加确定耗时为 dyld 过程中的 BuildingClosure 阶段。

图片

使用 Instrument 分析 BuildingClosure 阶段耗时

三、启动优化新秘境

在发现 BuildingClosure 生成过程中耗时占比非常大,并且有异常时,起初并没有意识到有什么问题,因为这是 dyld 内的代码,并未感觉会有什么问题。但是一切都指向了该函数,于是开始撸起袖子看代码。

从代码中可以看到,此处是为了生成 BuildingClosure 中 objcSelector objcClass objcProtocol 这三个部分的 HashTable(可以参考上面的 【BuildingClosure 文件解析】部分)。

拿起 dyld 开始对耗时异常版本的可执行文件进行调试,通过对该函数和内部实现的代码逻辑阅读,以及增加耗时信息打印。最终确定,耗时的代码在 make_perfect 这个函数中,这个函数是对【输入的字符串列表】生成一个【完美 Hash 表】。

void PrebuiltObjC::generateHashTables(RuntimeState& state)
{
    // Write out the class table
    writeObjCDataStructHashTable(state, PrebuiltObjC::ObjCStructKind::classes, objcImages, classesHashTable, duplicateSharedCacheClassMap, classMap);
    // Write out the protocol table
    writeObjCDataStructHashTable(state, PrebuiltObjC::ObjCStructKind::protocols, objcImages, protocolsHashTable, duplicateSharedCacheClassMap, protocolMap);
    // If we have closure selectors, we need to make a hash table for them.
    if ( !closureSelectorStrings.empty() ) {
        objc::PerfectHash phash;
        objc::PerfectHash::make_perfect(closureSelectorStrings, phash);
        size_t size = ObjCStringTable::size(phash);
        selectorsHashTable.resize(size);
        //printf("Selector table size: %lld\n", size);
        selectorStringTable = (ObjCStringTable*)selectorsHashTable.begin();
        selectorStringTable->write(phash, closureSelectorMap.array());
    }
}

继续深入了解 make_perfect 这个函数的实现。

3.1 Perfect Hash

通过对研读代码逻辑和耗时分析,最终定位到耗时代码部分为PerfectHash.cpp 中 findhash 函数,这个函数也是 完美散列函数 的核心逻辑。

这里涉及到了一个概念PerfectHash,PerfectHash 的核心是完美散列函数,我们看下维基百科的解释:

zh.wikipedia.org/wiki/%E5%AE…

对集合S的完美散列函数是一个将S的每个元素映射到一系列无冲突的整数的哈希函数

简单来讲 完美散列函数 是【对输入的字符串列表】【为每个字符串生成一个唯一整数】。

for (si=1; ; ++si)
    {
        ub4 rslinit;
        /* Try to find distinct (A,B) for all keys */
        *salt = si * 0x9e3779b97f4a7c13LL; /* golden ratio (arbitrary value) */
        initnorm(keys, *alen, blen, smax, *salt);
        rslinit = inittab(tabb, keys, FALSE);
        if (rslinit == 0)
        {
            /* didn't find distinct (a,b) */
            if (++bad_initkey >= RETRY_INITKEY)
            {
                /* Try to put more bits in (A,B) to make distinct (A,B) more likely */
                if (*alen < maxalen)
                {
                    *alen *= 2;
                }
                else if (blen < smax)
                {
                    blen *= 2;
                    tabb.resize(blen);
                    tabq.resize(blen+1);
                }
                bad_initkey0;
                bad_perfect0;
            }
            continue;                             /* two keys have same (a,b) pair */
        }
        /* Given distinct (A,B) for all keys, build a perfect hash */
        if (!perfect(tabb, tabh, tabq, smax, scramble, (ub4)keys.count()))
        {
            if (++bad_perfect >= RETRY_PERFECT)
            {
                if (blen < smax)
                {
                    blen *= 2;
                    tabb.resize(blen);
                    tabq.resize(blen+1);
                    --si;               /* we know this salt got distinct (A,B) */
                }
                else
                {
                    return false;
                }
                bad_perfect0;
            }
            continue;
        }
        break;
    }

此时通过对比新老版本的数据(使用 dyld 分别运行新老版本的可执行文件对比打印的日志),发现:

  • 老版本循环了 31 次成功生成 HashTable

  • 新版本循环了 92 次成功生成 HashTable

至此,我们距离成功已经非常接近了,于是进一步研读 dyld 源码和增加了更多打印信息代码,最终找到了相互冲突的函数字符串名称。

/*
 * put keys in tabb according to key->b_k
 * check if the initial hash might work
 */
static int inittab_ts(dyld3::OverflowSafeArray<bstuff>& tabb, dyld3::OverflowSafeArray<key>& keys, int complete, int si)
// bstuff   *tabb;                     /* output, list of keys with b for (a,b) */
// ub4       blen;                                            /* length of tabb */
// key      *keys;                               /* list of keys already hashed */
// int       complete;        /* TRUE means to complete init despite collisions */
{
  int  nocollision = TRUE;
  ub4 i;
  memset((void *)tabb.begin(), 0, (size_t)(sizeof(bstuff)*tabb.maxCount()));
  /* Two keys with the same (a,b) guarantees a collision */
  for (i0; i < keys.count(); i++) {
    key *mykey = &keys[i];
    key *otherkey;
    for (otherkey=tabb[mykey->b_k].list_b;
     otherkey;
     otherkey=otherkey->nextb_k)
    {
      if (mykey->a_k == otherkey->a_k)
      {
          // 打印冲突的字符串
        std::cout << mykey->name_k << " and " << otherkey->name_k << " has the same ak " << otherkey->a_k << " si is " << si << std::endl;
        nocollision = FALSE;
          /* 屏蔽此处代码,有冲突的情况下,继续执行,便于打印所有的冲突
    if (!complete)
      return FALSE;
           */
      }
    }
    ++tabb[mykey->b_k].listlen_b;
    mykey->nextb_k = tabb[mykey->b_k].list_b;
    tabb[mykey->b_k].list_b = mykey;
  }
  /* no two keys have the same (a,b) pair */
  return nocollision;
}

根据以上信息,我们已经了解到在Building Closure阶段中,可能存在字符串的 Hash 碰撞 引发循环次数大幅增加,进而引发了启动耗时暴增。

在经过 dyld 调试的耗时数据、构建出包后验证的数据验证后,通过避免 Hash 碰撞,我们完成了启动时长的优化。

3.2 向前一步

其实从打印的冲突函数名称来看,历史代码中已经存在了 Hash 碰撞 的现象。

猜想,如果我们解决了所有的字符串的 Hash 碰撞,岂不是不仅可以修复启动耗时异常上升的问题,还可以进一步降低启动耗时,提高启动速度?

于是我们对每个有碰撞的函数名称进行修改,经过出包验证,结果与我们猜测的一致,启动耗时有明显的下降。

图片

数据为 PC 电脑端运行 dyld 生成 BuildingClosure 的耗时数据,非手机端数据

四、总结

我们探索了 BuildingClosure 的生成过程,发现在Building Closure阶段中,可能存在字符串的 Hash 碰撞 引发循环次数大幅增加,进而引发了启动耗时暴增,进而导致启动耗时的大幅增加。

我们也发现,Building Closure Hash碰撞相关的启动耗时,其实与项目配置、编译环境、打包脚本等均无任何关系,就只是存在了字符串的Hash 碰撞 ,才引发循环次数大幅增加,进而导致启动时长增加。

往期回顾

1.分布式数据一致性场景与方案处理分析|得物技术

2.从对话到自主行动:AI应用如何从 Chat 进化为 Agent?开源项目源码深度揭秘|得物技术

3.得物技术部算法项目管理实践分享

4.商家域稳定性建设之原理探索|得物技术

5.得物 Android Crash 治理实践

文 / 道隐

关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

前端图片技术深度解析:格式选择、渲染原理与性能优化

作者 Process
2025年4月3日 08:24

前言

在Web开发中,图片处理是影响用户体验和网站性能的关键因素。前端开发者每天都要面对一系列图片相关的技术决策:

  • 静态图片格式:选择PNG还是JPG追求更小体积?
  • 动态图像选择:GIF还是APNG?
  • 压缩策略:如何在保持视觉质量的前提下减小文件大小?
  • 格式应用:web端图片是否要转化成 WebP 还是 AVIF?

这些决策直接影响着首屏加载时间、带宽消耗和用户交互体验等核心指标,甚至将影响业务的商业转化率。图片处理其实也考验着一个前端的基本功,本文将深入解析常见图片格式的编码原理,结合浏览器渲染机制与现代Web标准,提供一套科学、可落地的图片优化实践方案。


一、图片格式

在选择图片格式之前首先要清楚这些图片格式的特点和区别

1. 类型

图片格式主要分为两种类型:位图和矢量图。我们常见的图片除了 SVG 都是位图

位图

位图是由像素点组成的,适合照片和复杂图像,其清晰度由分辨率和像素总量共同决定。

矢量图

矢量图是通过代码或者说是用数学公式描述的

  1. 优点是在任何缩放比例下都可以展示得很清晰而不会失真
  2. 缺点是细节的展示效果不够丰富,对于复杂图像来说,比如要达到照片的效果,若通过SVG进行矢量图绘制,需要大量的代码描述,生成文件就会非常大,并且浏览器解析和渲染对很耗性能,所以svg不适合细节丰富的图像,只适合图标和简单图形。

2. 压缩方式

图片格式不同的压缩方法由图像编码标准决定:

  1. JPG(有损压缩)

    • 原理:通过丢弃人眼不敏感的细节(如高频色彩信息)来减少文件体积。
    • 特点
      • 压缩率极高,适合照片类图像(色彩丰富、渐变多)。
      • 不支持透明度,多次保存会累积质量损失(“代际损失”)。
  2. PNG(无损压缩)

    • 原理:使用 DEFLATE 算法无损压缩像素数据,保留所有原始信息。
    • 特点
      • 适合图标、线条图等需要保留锐利边缘的图像。
      • 支持透明度,文件体积通常比 JPEG 大。
  3. WebP:集 JPEG 和 PNG 优点于一身。无损压缩比 PNG 小 26%,有损压缩比 JPEG 小 25-34%,并且支持透明通道。

  4. AVIF:更先进的压缩算法,压缩率比 WebP 还高 30%。但兼容性较差,目前浏览器支持率只有 86%。

而我们开发过程中经常对图片进行二次压缩(同格式),比如借助 TinyPng 工具,实际上是对图片进行有损压缩

  1. 量化减少颜色数量:将 1600 万色(24-bit)缩减到 256 色(8-bit)。
  2. 丢弃元数据:删除 EXIF、ICC 配置等非必要数据。
  3. 优化压缩算法:使用改进的 DEFLATE 算法更高效压缩数据。

虽然 PNG 本身是无损格式,但 TinyPNG 通过有损预处理+无损压缩的组合,实现了“视觉无损”的高压缩率。

3. 图片格式总结

格式 类型 压缩方式 透明度支持 动画支持 特点 适用场景
JPEG 位图 有损 高压缩率,适合照片,但压缩过度会失真 照片、复杂色彩图像
PNG 位图 无损 ✅ (Alpha) 支持透明,无损压缩,文件较大 透明图标、精确图形(如截图)
GIF 位图 无损(256色) ✅ (1-bit) 支持简单动画,但色彩有限 表情包、简单动画
WebP 位图 有损/无损 ✅ (Alpha) 现代格式,压缩效率高,支持动画和透明度 全能替代(JPEG/PNG/GIF)
AVIF 位图 有损/无损 ✅ (Alpha) 下一代压缩,比WebP更高效,但兼容性差 高质量图片、未来项目
APNG 位图 无损 ✅ (Alpha) PNG的动画扩展,支持全透明动画 高质量动画(替代GIF)
SVG 矢量图 无损 无限缩放,通过代码描述图形,支持CSS/JS控制 图标、LOGO、UI元素

二、浏览器渲染机制与性能影响

1. 关键渲染路径中的图片处理

graph TD
    A[HTML解析] --> B[发现<img>标签]
    B --> C{是否可见视口?}
    C -->|是| D[立即加载]
    C -->|否| E[延迟加载]
    D --> F[解码线程处理]
    F --> G[主线程布局计算]
    G --> H[绘制合成]

浏览器的图片渲染流程可以分为以下阶段:

1. 解析与加载

  • HTML 解析:浏览器解析 HTML 时遇到 <img> 标签,立即发起图片资源的网络请求(除非使用 loading="lazy" 延迟加载)。
  • 优先级控制:图片的加载优先级通常低于关键资源(如 CSS、JS),但可通过 fetchpriority="high" 调整。
  • 预加载优化:使用 <link rel="preload"> 提前加载关键图片。
2. 解码(Decoding)
  • 主线程阻塞:图片解码(将二进制数据转换为像素)默认在主线程执行,大图片可能导致主线程卡顿。

  • 异步解码:通过 img.decode() API 异步解码图片,避免阻塞主线程。

  • 格式影响

    • JPEG/PNG:解码复杂度较高。
    • WebP/AVIF:现代格式解码效率更高。
3. 布局(Layout)与绘制(Paint)
  • 布局计算:图片尺寸变化(未指定 width/height)会导致布局重排(Reflow)。
  • 图层分离:使用 will-change: transform 或 transform: translateZ(0) 将图片提升到独立图层,减少绘制区域。
4. 合成(Composite)
  • GPU 加速:某些 CSS 属性(如 transformopacity)触发 GPU 合成,跳过主线程直接由合成器处理。
  • 减少重绘:避免频繁修改图片尺寸或位置,触发不必要的重绘(Repaint)。

2. 性能瓶颈分析

  • 解码耗时:高分辨率图片消耗CPU资源(尤其是AVIF/WebP)
  • 布局抖动:未设置尺寸的图片引发回流(CLS问题)
  • 内存占用:4096x4096的RGBA图片占用67MB内存
  • 网络竞争:过多图片请求阻塞关键资源加载

实测数据

  • 未优化的2MB JPEG:加载时间≈800ms(4G网络)
  • 优化后的200KB WebP:加载时间≈150ms
  • LCP(最大内容绘制)提升300ms可使转化率提高5%

三、性能优化全方案

1. 格式选择策略

前面可以得出结论,avif压缩率最高,性能最好,但要考虑兼容性问题,而webp是当前最佳平衡选择,最佳实践我们可以通过代码检测图片格式兼容性。

服务端:浏览器端在发起图片请求时会带上当前浏览器支持的图片格式,可以在服务端判断后返回对应的图片格式。

image.png

// 动态格式检测
const acceptHeader = req.headers.accept || '';
const supportsWebP = acceptHeader.includes('webp');
const supportsAvif = acceptHeader.includes('avif');

function getBestFormat() {
  if (supportsAvif) return 'avif';
  if (supportsWebP) return 'webp';
  return 'jpg';
}

决策树

graph TD
    A[选择图片] --> B{是否需要动画?}
    B -->|是| C[GIF/WebP/AVIF]
    B -->|否| D{是否为复杂图像?}
    D -->|是| E[是否需要透明度]
    D -->|否| F[SVG]
    E -->|是| G[PNG/WebP/AVIF]
    E -->|否| H[JPEG/WebP/AVIF]
    C --> I[检测浏览器兼容性]
    F --> I
    G --> I
    H --> I

2. 响应式图片技术

响应式图片是现代Web开发的必备技术,可以根据设备屏幕大小和分辨率提供最合适的图片资源。

srcset 和 sizes 属性

<img
  src="image-400w.jpg"
  srcset="image-400w.jpg 400w, image-800w.jpg 800w, image-1200w.jpg 1200w"
  sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
  alt="响应式图片示例" />

picture 元素实现格式回退

<picture>
  <source srcset="image.avif" type="image/avif" />
  <source srcset="image.webp" type="image/webp" />
  <img src="image.jpg" alt="图片描述" />
</picture>

3. 懒加载策略

原生懒加载

<img src="image.jpg" loading="lazy" alt="懒加载图片" />

交叉观察器实现

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach((img) => {
  observer.observe(img);
});

4. CDN与缓存优化

  • 使用CDN分发图片资源,减少网络延迟
  • 设置合理的缓存策略,常见图片可设置长期缓存
  • 使用内容哈希命名,便于缓存更新
# Nginx缓存配置示例
location ~* \.(jpg|jpeg|png|gif|webp|avif)$ {
    expires 30d;
    add_header Cache-Control "public, no-transform";
}

5. 图片预处理与自动化

构建时优化

使用webpack、gulp等工具在构建阶段自动处理图片:

// webpack配置示例
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: {
                progressive: true,
                quality: 65,
              },
              optipng: {
                enabled: false,
              },
              pngquant: {
                quality: [0.65, 0.9],
                speed: 4,
              },
              webp: {
                quality: 75,
              },
            },
          },
        ],
      },
    ],
  },
};

图片服务API

使用图片处理服务动态调整图片大小和格式: 例如通过阿里云OSS服务通过x-oss-process图片处理参数,将图片进行裁剪、质量变化、格式转化等操作

<!-- 使用OSS等服务 -->
<img src="https://oss-console-img-demo-cn-hangzhou-3az.oss-cn-hangzhou.aliyuncs.com/example.gif?x-oss-process=image/format,png" alt="优化图片" />

四、未来趋势展望

1. HTTP/3多路复用

HTTP/3基于QUIC协议,采用UDP传输,解决了HTTP/2中的队头阻塞问题。对于图片密集型应用,多路复用能显著提升并行下载效率,减少等待时间。

graph LR
    A[HTTP/2] --> B[单TCP连接]
    B --> C[队头阻塞]
    D[HTTP/3] --> E[多UDP连接]
    E --> F[避免队头阻塞]

2. 机器学习驱动的图像压缩

Google RAISR技术

RAISR (Rapid and Accurate Image Super Resolution) 使用机器学习算法,能在低分辨率图像基础上重建高质量图像,大幅减少传输数据量。

神经网络压缩

基于神经网络的压缩算法如Facebook的DLVC (Deep Learning Video Coding),可将压缩率提高30-50%,同时保持视觉质量。

3. WebCodecs API与GPU加速

WebCodecs API允许Web应用直接访问底层媒体编解码器,实现硬件加速:

// WebCodecs API示例
async function decodeImage(imageData) {
  const decoder = new ImageDecoder({
    data: imageData,
    type: 'image/avif',
  });

  const result = await decoder.decode();
  return result.image;
}

4. 新一代图像格式

JPEG XL

JPEG XL旨在替代JPEG,提供更高压缩率和更丰富的功能:

  • 比JPEG小60%,同时提高视觉质量
  • 支持无损转换现有JPEG
  • 支持动画、透明度和HDR

WebP 2

Google正在开发WebP的下一代版本,预计将进一步提高压缩效率,并增强动画支持。

结语

图片优化是性能工程的艺术,开发需要平衡视觉质量、加载速度与兼容性需求。通过深入理解格式特性、浏览器工作原理,并结合本文提供的全方位优化策略,可以显著提升Web应用的用户体验和业务指标。

随着新技术的不断涌现,图片优化的方法也在持续演进。作为前端开发者,我们需要不断学习和实践,将最佳图片处理方案融入日常开发流程中。

昨天 — 2025年4月2日首页

【万字总结】前端全方位性能优化指南(八)——Webpack 6调优、模块联邦升级、Tree Shaking突破

2025年4月1日 09:17
构建工具深度优化——从机械配置到智能工程革命 当Webpack配置项突破2000行、Node进程内存耗尽告警时,传统构建优化已触及工具链的物理极限:Babel转译耗时占比超60%、跨项目模块复用催生冗
昨天以前首页

Flutter 性能优化:实战指南

2025年4月1日 09:46

主要的优化方面,如首屏、列表、渲染、内存、数据加载、工具等。

一、首屏渲染优化

1. 骨架屏与占位符

  • 问题:首屏加载时白屏或长时间等待数据导致用户体验差。

  • 解决方案

    • 骨架屏:使用 SkeletonLoader 等组件模拟页面结构,替代空白屏4。
    • 占位符:数据未加载时返回轻量组件如 SizedBox.shrink(),降低首帧渲染时间(如减少 200ms)。
  • 代码示例

    Widget build(BuildContext context) {
      if (_isLoading) return const SizedBox.shrink(); // 轻量占位
      // 真实内容
    }
    

2. 资源预加载

  • 问题:首屏图片加载延迟。

  • 解决方案:在页面初始化前预加载关键资源(如轮播图、Banner)。

  • 代码示例

    Future<void> _precacheImages() async {
      await Future.wait([
        precacheImage(AssetImage('assets/banner.png'), context),
      ]);
    }
    

3. 数据预取与FFI优化

  • 问题:Flutter 通过 Channel 请求数据时线程切换和编解码耗时。

  • 解决方案

    • Native侧发起请求:在页面路由阶段由 Native 提前请求数据,避免 Flutter 线程切换。
    • FFI(Foreign Function Interface) :通过 FFI 直接读取 Native 缓存数据,减少序列化开销。
  • 效果:详情页启动时间优化 100ms 以上9。


二、长列表与滚动性能优化

1. 懒加载与按需构建

  • 问题:一次性渲染大量列表项导致卡顿。

  • 解决方案

    • ListView.builder/GridView.builder:仅构建可见项,避免内存溢出。
    • 分帧渲染(分帧加载) :将复杂子组件分帧渲染,避免单帧耗时过长。
  • 代码示例

    ListView.builder(
      itemCount: 1000,
      itemBuilder: (context, index) => ListItem(data[index]),
    )
    

2. 复用与缓存

  • 问题:频繁切换 Tab 导致重复加载数据。

  • 解决方案

    • 状态缓存:使用 PageStorageKey 或 AutomaticKeepAliveClientMixin 缓存页面状态。
    • 数据分级管理:通过 RxMap 维护不同分类的数据状态,减少重复请求。

三、UI渲染优化

1. 减少Widget重建

  • 问题:频繁调用 setState 导致不必要的 Widget 重建。

  • 解决方案

    • const Widget:使用 const 构造函数创建静态组件,避免重复构建。
    • Provider 选择性刷新:通过 Selector 或 Consumer 局部刷新,而非全量更新。
  • 代码示例

    const MyWidget(); // 编译时确定,避免重建
    

2. 动画优化

  • 问题:复杂动画导致帧率下降。

  • 解决方案

    • AnimatedBuilder:分离动画逻辑与静态组件,仅重绘动画部分。
    • 复用Child:将不变部分作为 child 参数传入,避免重复构建。
  • 代码示例

    AnimatedBuilder(
      animation: _controller,
      child: const StaticWidget(),
      builder: (context, child) => Transform.rotate(
        angle: _controller.value,
        child: child,
      ),
    )
    

四、内存与GPU优化

1. 内存泄漏检测

  • 问题:未释放资源导致内存持续增长。

  • 解决方案

    • 及时销毁监听:在 dispose 方法中取消订阅和释放资源。
    • 工具检测:使用 DevTools 的 Memory 视图分析内存泄漏。

2. GPU渲染优化

  • 问题:多图层叠加(如半透明蒙层)导致 GPU 负载高。

  • 解决方案

    • 避免 saveLayer:使用 ClipRRect 替代复杂蒙层,减少离屏渲染。
    • 缓存静态图像:通过 checkerboardRasterCacheImages 检测未缓存的图像。

五、工具与工程化实践

1. 性能分析工具

  • DevTools:通过 Timeline 视图分析帧耗时,定位构建、布局、绘制阶段的瓶颈。
  • Profile模式:打包 Profile 版本,获取接近真实环境的性能数据。

2. 模块级混合开发

  • 问题:全 Flutter 页面启动性能差(如动态库加载耗时)。

  • 解决方案

    • Native与Flutter混合:核心模块用 Native 实现,非核心模块保留 Flutter,降低启动时间。
    • FlutterBoost 扩展:支持模块级混合容器,解决生命周期和布局问题。

六、其他高级优化

1. Isolate 并行计算

  • 问题:Dart 单线程阻塞 UI。

  • 解决方案:将耗时计算(如 JSON 解析)放入 Isolate 执行。

  • 代码示例

    final result = await compute(heavyTask, data);
    

2. 资源压缩与预置

  • 问题:资源加载慢(如图片、字体)。

  • 解决方案

    • 图片压缩:使用 flutter_image_compress 优化图片体积。
    • 本地预置:关键资源(如 JSON 模板)预置到本地,减少网络依赖。

总结

  • 核心原则:减少 Widget 重建、按需加载、复用资源、分离耗时操作。

  • 实战案例

    • 携程酒店通过分帧渲染优化;
    • 淘宝特价版使用 FFI 和 Native 数据预取提升首屏速度;
    • 长列表懒加载和状态缓存避免卡顿。

iOS性能优化:OC和Swift实战指南

2025年4月1日 09:29

常见的iOS性能优化点,比如内存管理、UI优化、多线程处理、网络请求优化、启动优化、I/O操作、图像处理、算法优化、工具使用等。

一、内存优化

1. 循环引用处理

  • 原理:对象之间的强引用导致无法释放,内存泄漏。

  • Objective-C

    • 使用 __weak 或 __unsafe_unretained(需谨慎)打破循环:

      __weak typeof(self) weakSelf = self;
      self.block = ^{
          // 弱引用避免循环
          [weakSelf doSomething];
      };
      
    • 对 NSTimer 使用 NSProxy 或 weak 委托模式。

  • Swift

    • 使用 [weak self] 或 [unowned self] 捕获列表:

      self.block = { [weak self] in
          guard let self = self else { return }
          self.doSomething()
      }
      
    • unowned 适用于生命周期相同或更短的场景(如父子关系)。


2. 自动释放池(Autorelease Pool)

  • 原理:批量创建临时对象时,及时释放内存。

  • Objective-C

    @autoreleasepool {
        for (int i = 0; i < 100000; i++) {
            NSString *temp = [NSString stringWithFormat:@"%d", i];
            // 临时对象会被及时释放
        }
    }
    
  • Swift

    autoreleasepool {
        for i in 0..<100000 {
            let temp = "(i)"
            // 临时对象在块结束时释放
        }
    }
    
  • 适用场景:大量临时对象生成(如解析 JSON 数组、图像处理)。


二、UI 性能优化

1. 避免离屏渲染(Offscreen Rendering)

  • 原理:离屏渲染(如圆角、阴影)触发 GPU 额外绘制,导致卡顿。

  • 优化方法

    • 预渲染圆角

      • Objective-C

        // 使用 Core Graphics 提前绘制圆角
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0);
        [[UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:10] addClip];
        [image drawInRect:view.bounds];
        UIImage *roundedImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        view.layer.contents = (id)roundedImage.CGImage;
        
      • Swift

        let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
        let roundedImage = renderer.image { context in
            UIBezierPath(roundedRect: view.bounds, cornerRadius: 10).addClip()
            image.draw(in: view.bounds)
        }
        view.layer.contents = roundedImage.cgImage
        
    • 避免 shouldRasterize:除非复用图层,否则会触发离屏渲染。


2. Cell 复用与轻量化

  • 原理:避免频繁创建/销毁 Cell,减少 CPU 和内存压力。

  • Objective-C

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        // 复用 Cell
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CellID"];
        if (!cell) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"CellID"];
        }
        // 轻量配置(避免复杂计算)
        cell.textLabel.text = [self.dataArray[indexPath.row] title];
        return cell;
    }
    
  • Swift

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CellID", for: indexPath)
        cell.textLabel?.text = dataArray[indexPath.row].title
        return cell
    }
    
  • 优化点

    • 避免在 cellForRow 中执行耗时操作(如网络请求)。
    • 使用 prepareForReuse 清理旧数据。

三、多线程优化

1. 主线程任务最小化

  • 原理:主线程阻塞导致 UI 卡顿(16ms 内未完成一帧绘制)。

  • Objective-C

    // 将耗时操作放到后台线程
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSData *data = [NSData dataWithContentsOfURL:url];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = [UIImage imageWithData:data];
        });
    });
    
  • Swift

    DispatchQueue.global(qos: .userInitiated).async {
        let data = try? Data(contentsOf: url)
        DispatchQueue.main.async {
            self.imageView.image = UIImage(data: data!)
        }
    }
    

2. 线程安全与锁优化

  • Objective-C

    • @synchronized 实现简单但性能较低:

      objc

      @synchronized(self) {
          // 临界区
      }
      
    • 高性能场景使用 os_unfair_lock(替代已废弃的 OSSpinLock):

      #include <os/lock.h>
      os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
      os_unfair_lock_lock(&lock);
      // 临界区
      os_unfair_lock_unlock(&lock);
      
  • Swift

    • 使用 NSLock 或 DispatchQueue 屏障:

      let queue = DispatchQueue(label: "com.example.threadSafe", attributes: .concurrent)
      queue.async(flags: .barrier) {
          // 写操作(独占访问)
      }
      

四、网络优化

1. 请求合并与缓存

  • Objective-C

    // 使用 NSURLSession 的缓存策略
    NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10];
    
  • Swift

    let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 10)
    
  • 优化点

    • 减少重复请求(如短时间内多次刷新)。
    • 使用 HTTP/2 多路复用降低连接开销。

五、启动时间优化

1. 冷启动阶段优化

  • 原理:减少 main() 函数之前的加载时间(T1)和首帧渲染时间(T2)。

  • Objective-C

    • 减少动态库数量,合并 Category。
    • 避免在 +load 方法中执行代码。
  • Swift

    • 使用 @UIApplicationMain 减少启动代码。

    • 延迟非必要初始化:

      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
          // 延迟初始化第三方 SDK
      }
      

六、I/O 优化

1. 文件读写异步化

  • Objective-C

    dispatch_io_t channel = dispatch_io_create_with_path(DISPATCH_IO_STREAM, [path UTF8String], O_RDONLY, 0, queue, ^(int error) {});
    dispatch_io_read(channel, 0, SIZE_MAX, queue, ^(bool done, dispatch_data_t data, int error) {});
    
  • Swift

    let queue = DispatchQueue.global(qos: .background)
    DispatchIO.read(fromFileDescriptor: fd, queue: queue) { data, _ in
        // 处理数据
    }
    

七、图像处理优化

1. 异步解码与降采样

  • Objective-C

    // 使用 ImageIO 进行降采样
    CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
    CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)@{(id)kCGImageSourceThumbnailMaxPixelSize: @(300)});
    UIImage *image = [UIImage imageWithCGImage:imageRef];
    
  • Swift

    let source = CGImageSourceCreateWithURL(url as CFURL, nil)!
    let options: [CFString: Any] = [kCGImageSourceThumbnailMaxPixelSize: 300]
    let imageRef = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)!
    let image = UIImage(cgImage: imageRef)
    

八、算法与数据结构

1. 高效遍历与查询

  • Objective-C

    • 使用 NSDictionary 替代 NSArray 快速查找(O(1) vs O(n))。
  • Swift

    • 使用 lazy 延迟计算集合:

      let filteredData = data.lazy.filter { $0.isValid }.map { $0.value }
      
    • 使用 ContiguousArray 提升性能(连续内存布局)。


九、工具使用

1. Instruments 分析

  • Time Profiler:定位 CPU 热点函数。
  • Allocations:分析内存分配类型和泄漏。
  • Core Animation:检测离屏渲染和帧率。

十、Swift 特有优化

1. 减少动态派发

  • 使用 final 和 private 修饰类或方法:

    final class NetworkManager {
        private func fetchData() { ... }
    }
    

2. 值类型优先

  • 使用结构体(struct)替代类(class)减少引用计数开销:

    struct Point {
        var x: Double
        var y: Double
    }
    

总结

  • Objective-C 优化重点:手动内存管理、@autoreleasepool、避免 performSelector 潜在泄漏。
  • Swift 优化重点:值类型、协议扩展、DispatchQueue 和现代语法(如 Result 类型)。
  • 通用原则:减少主线程阻塞、复用资源、异步化耗时操作、利用工具分析瓶颈。
❌
❌