普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月18日iOS

想偷懒购买现成的应用,结果一更新就遇到了4.3a!

作者 iOS研究院
2025年12月17日 20:36

背景

对于Appstore来说,除了开发者自行研发产品之外。也可以通过AppStore留下的后门,转让功能

今天就简单聊聊,一位粉丝因为产品上架不顺利,所以反手买了同类型的竞品。

交易背景

某粉丝因为自己制作的工具产品遭遇了3.2f封号的洗礼,又想快速抢回自己的市场,所以自以为聪明的购买了一套AppStore线上的产品

出发点本身没有问题,确实是解决问题最快的方式,但是忽略几个关键的信息内容:

  • 老包App上架、迭代时间
  • 账号潜在风险

老包上架时间

购买的老包上架时间为2020年06月17日,最近一次迭代时间为2021年11月17日。历时更新了13个版本,无论是从包本身还是版本迭代周期,其实产品本身肯定是健康的。毕竟活了将近5年的产品,账号的权重和产品的权重,无需多言

那为什么把这点单独提出来,是因为最近迭代更新的时间

在2021年的时候,苹果审核的算法肯定是今非昔比。这种情况不亚于,用前朝的剑斩本朝的官

直白来讲,之前更新顺利是因为苹果查的不严,竞品不多。在这种背景下,产品是健硕。无论是当时的用户交互还是UI设计,肯定是符合当下苹果开发者准则。

但时至今日,那很难说了。

账号潜在风险

交易的账号有几款产品,但是粉丝只需要其中一款。那么问题来了,之后的事情没有人可以担保不发生意外。

首先就是关联风险,这种App转让的行为是强关联。 买过来的产品继续当良民最好一些隔离措施,但是不表明卖家一直可以健健康康。如果卖家一旦出现不可控的违规行为,那么买过来的产品到底能活多久?是一个大大问号

客观来说非要转让产品,除非打包一起买,不然多增加变量。

不过话说回来,这种成本又被间接性拉高,同时未必卖家愿意。

4.3a破局

粉丝在遭遇了2次4.3a后,一时间乱了阵脚。主动在后台留言咨询,请求付费破局。在了解到整个事件来龙去脉后,耗时7天工作日,成功完成App迭代工作

主要协助解决了API接口处理,内购订阅功能,3.2f老包版本功能植入

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

相关推荐

# 苹果开发者续费大坑及成功续费方案!亲测有效

# AppStore敏感词排查手册,多维度分析Guideline 2.3.1隐藏功能,轻松过审。

# 如何主动提防苹果3.2f的进攻,自查防御手册(代码篇)

# 如何主动提防苹果3.2f的进攻,自查防御手册(ASO篇)

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

Flutter运行iOS26真机的两个问题

作者 往来凡尘
2025年12月17日 18:14

一、Debug编译崩溃

../../../flutter/third_party/dart/runtime/vm/virtual_memory_posix.cc: 428: error: mprotect failed: 13 (Permission denied)

在iOS上Dart不管是JIT运行还是进行hotload的时候,都需要涉及代码在内存从RW变成RX的调整,而这在Flutter低版本是通过mprotect完成,而这在iOS 26被禁止了

所以Flutter在Debug模式下真机运行iOS 26设备就崩溃了。如果确实需要在iOS 26上真机调试,方案如下:

  1. 目前测试最新的3.38.5版本可以正常在Debug模式运行iOS 26设备。
  2. 如果不能升级Flutter版本,只能暂时使用Release或Profile模式运行(修改iOS项目Scheme,设置run->Build Configuration)

二、Flutter适配UIScene

当我的iOS 26的设备正常运行后,发现Xcode控制台有一行打印:

UIScene lifecycle will soon be required. Failure to adopt will result in an assert in the future.

强制使用UIScene不然未来就可能触发断言,太可怕了😱那就顺便做了吧。

Flutter最低版本

environment:
  sdk: ^3.10.0
  flutter: ">=3.38.0"

方案一: 自动化迁移,适合iOS AppDelegate文件没有大量定制化修改,在项目终端运行命令:

flutter config --enable-uiscene-migration

迁移成功后,会在构建日志中看到 "Finished migration to UIScene lifecycle" 的提示,否则工具会给出警告,并提示你手动迁移。

方案二: 手动迁移

  1. AppDelegate移除window生命周期相关的方法,使用UIScene lifecycle的对应方法,保留application(_:didFinishLaunchingWithOptions:)方法,移除所有创建和设置window的代码
  2. AppDelegate类遵循协议:FlutterImplicitEngineDelegate,并在didInitializeImplicitFlutterEngine方法中注册插件,并将创建Method Channels或Platform Views的逻辑都迁移到此处,因为在didFinishLaunchingWithOptions执行时,FlutterViewController可能还不存在
import Flutter
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
    override func application(
    _ application: UIApplication, 
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
  
    func didInitializeImplicitFlutterEngine(_ engineBridge: any FlutterImplicitEngineBridge) {
        GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
        // Channel、Platform Views
    }
}
  1. 创建SceneDelegate.swift文件,基础定义如下:
import UIKit
import Flutter

class SceneDelegate: FlutterSceneDelegate {
}
  1. info.plist添加Application Scene Manifest相关key:
<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <false/>
    <key>UISceneConfigurations</key>
    <dict>
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneConfigurationName</key>
                <string>Default Configuration</string>
                <key>UISceneDelegateClassName</key>
                <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
                <key>UISceneStoryboardFile</key>
                <string>Main</string>
            </dict>
        </array>
    </dict>
</dict>

key缺失可能会导致启动黑屏或控制台打印xxxkey缺失,其中key:

  • UISceneConfigurationName:Default Configurationflutter均可
  • UISceneDelegateClassName:FlutterSceneDelegate$(PRODUCT_MODULE_NAME).SceneDelegate均可

到此基础配置就完成了,恭喜你demo项目可以正常运行了,如果你的项目其他定制化修改还需要迁移,详细可以参考下面的Flutter Docs

参考链接

flutter项目大量使用.obs会导致项目性能极度下降吗

作者 yfmingo
2025年12月17日 15:26

简短的回答是:一般情况下不会,甚至在很多场景下,大量使用 .obs 反而比传统的 setState 性能更好。

但凡事都有“但是”。如果使用姿势不对,确实会导致内存占用增加或帧率下降。

我们需要从**内存(Memory)CPU/渲染(Rendering)**两个维度来分析这个问题。

1. 内存维度:会有额外开销,但通常可忽略

  • 原理:一个普通的 int a = 0 在内存中占用极小。但是 var a = 0.obs 实际上创建了一个 RxInt 对象。这个对象内部维护了一个 Stream(流)和监听器列表。
  • 影响Rx 类型的对象确实比原始类型(Primitive types)重。
  • 结论:除非你创建了数百万个 .obs 对象(例如在一个巨大的循环中为每个数据点都创建独立的 Rx 变量),否则现代手机的内存(6GB+)完全可以忽略这点开销。对于常规 App 的几百上千个状态变量,几乎没有影响。

2. CPU/渲染维度:这是关键(双刃剑)

GetX 的 .obs + Obx 机制是细粒度更新

✅ 优势:它为什么快?

当你使用 setState 时,Flutter 会重建当前 Widget 及其子树。 当你使用 Obx 时,GetX 只会重建包裹在 Obx 内部的那个小组件

  • 例子:页面有一个复杂的列表,顶部有一个计数器。
  • setState:计数器变了 -> 整个页面 build 方法重跑 -> 列表也要做 diff(虽然 Element 树可能不重绘,但 diff 也要耗 CPU)。
  • .obs:计数器变了 -> 只有 Obx(() => Text(...)) 这一行代码重跑 -> 列表完全不受影响。
  • 结论:在这种场景下,大量使用 .obs 反而提升了性能。

❌ 陷阱:什么时候会导致性能下降?

如果你遇到了性能下降,通常是因为犯了以下错误:

1. 滥用 Obx 包裹范围过大(最常见) 错误做法:

// ❌ 错误:只要 count 变了,整个 Column 里的所有东西(包括复杂的 ChartView)都会被重建
Obx(() => Column(
  children: [
    Text("Count: ${controller.count}"),
    SuperComplexChartView(), // 极度消耗性能的组件
    AnotherHeavyWidget(),
  ],
));

正确做法:

// ✅ 正确:count 变了,只有 Text 重建,下面的复杂组件纹丝不动
Column(
  children: [
    Obx(() => Text("Count: ${controller.count}")),
    SuperComplexChartView(),
    AnotherHeavyWidget(),
  ],
);

**2. 在高频更新中使用 .obs** 如果你有一个 .obs 变量绑定在 AnimationController 的回调里,或者用于监听滚动位置(ScrollPosition),每秒更新 60-120 次。

  • 如果在 Obx 里做了复杂的逻辑运算,会导致 UI 线程掉帧。
  • 建议:对于极高频的更新,考虑使用 GetBuilder(手动 update())或者直接操作 RenderObject,不过通常 Obx 也扛得住,只要别在里面做耗时操作。

3. 大列表的 RxList 陷阱

final list = <Item>[].obs;

当你调用 list.add()list.refresh() 时,所有监听 listObx 都会重建。 如果你的 ListView 是这样写的:

Obx(() => ListView.builder(
  itemCount: controller.list.length,
  itemBuilder: (context, index) {
     // ...
  }
))

这通常没问题,因为 ListView.builder 只有视口内的 item 会渲染。但如果你在 Obx 里直接用了 Column 渲染一个长列表,那就会卡顿。

4. 滥用 Workers (ever, debounce, interval) 如果你定义了大量的 .obs 变量,并且给每一个都绑定了 ever 监听器(每次变化都执行回调),这会消耗大量的 CPU 资源,尤其是在批量更新数据的时候。

总结与建议

大量使用 .obs 不会让项目变卡,前提是你遵循“最小更新原则”。

  • 放心用:在 ViewModel (Controller) 中定义几十上百个 .obs 变量完全没问题。
  • 小心用:在 View 层写 Obx(() => ...) 时,范围越小越好。只包裹那个变化的 Text 或 Icon,不要包裹整个 Scaffold 或 Column。
  • 替代方案:如果你非常在意内存(例如在低端 IoT 设备上跑 Flutter),或者状态更新不需要响应式流的特性,可以使用 **GetBuilder + update()** 模式。它是最轻量级的,几乎等同于原生的性能,内存占用比 .obs 少,但写起来稍微麻烦一点(需要手动触发更新)。

一句话建议:继续用 .obs,但要养成把 Obx 放在 Widget 树叶子节点(最底层)的好习惯。

再次紧急修复,Flutter 针对 WebView 无法点击问题增加新的快速修复

2025年12月17日 14:42

前几天我们刚聊了 《Flutter 官方正式解决 WebView 在 iOS 26 上有点击问题》 ,这是一个完整的底层重构修复,整个修复周期审核堪比“博士论文”,但是也带来了一个问题,它只修复了 Engine 和 Framework 层面问题,那插件端还需要等升级适配修复,这链路就又再一次拉长了

所以针对这个场景,作者又提交了一个“骚操作”的快速修复,#179908 这个 PR 的修复方案非常“暴力”但也有效:找到那些特定的手势识别器,先禁用它们,然后立即重新启用, 这相当于重置了识别器的状态。

是不是又有熟悉的味道?不理解的可以看上上篇讲这个点击问题的内容。

为什么需要这个新的 PR ?因为这是一个无需任何插件更新的快速修复方案,并且也已经合并到了 master :

这个 PR 具体的代码修改就是:在 FlutterTouchInterceptingView 中添加了两个核心的辅助方法,并在 blockGesture 中调用:

  • searchAndFixWebView : 一个递归函数,它会遍历视图层级,如果遇到的视图是 WKWebView 类型,它就会调用修复手势的方法,执行 searchAndFixWebViewGestureRecognzier ,确保即使 WKWebView 被嵌套在其他 UIView 中也能被找到

  • searchAndFixWebViewGestureRecognzier : 也是一个递归函数,遍历当前视图的所有 gestureRecognizers ,检查识别器是否启用,并且类名是否用 "TouchEventsGestureRecognizer" 结尾 (通常对应 WKTouchEventsGestureRecognizer) ,然后执行 recognizer.enabled 的关闭和打开操作:

  • 修改了 blockGesture , 当手势拦截策略为 FlutterPlatformViewGestureRecognizersBlockingPolicyEager时,在 iOS 26 改为直接调用 [self searchAndFixWebView:self.embeddedView]; 来执行上述修复逻辑:

最后,方案还增加了一个 FLTDisableWebViewGestureReset ,给开发者添加了一个安全阀,通过读取 Info.plist 中的 FLTDisableWebViewGestureReset ,如果这个修复方案上线后出现严重问题,开发者可以通过配置这个 flag 来禁用这个“重置手势”的逻辑。

可以看到,这是一个快速且粗暴的改动,就是在 FlutterPlatformViews.mm 中实现了针对 WKWebView 手势识别器的递归搜索和“重启”机制,并在 blockGesture 中针对 iOS 26+ 启用了这个机制

但是好处也很明显,可以什么插件都不改就生效,当然主要是一个临时修复,为的是方便开发者快速解决问题,真正 fix 的途径还是推荐走之前的 hitTest :

参考链接

github.com/flutter/flu…

iOS逆向-哔哩哔哩增加3倍速播放(4)- 竖屏视频·全屏播放场景

作者 TouchWorld
2025年12月17日 07:49

Xnip2025-12-16_11-22-40.jpg

前言

作为哔哩哔哩的重度用户,我一直期待官方支持 3 倍速播放,但该功能迟迟未上线。于是,我利用 iOS 逆向工程知识,为 B 站 App 添加这一功能。

修改前:最高仅支持 2.0 倍速。

Screenshot 2025-12-11 at 07.26.05.png

修改后:成功添加 3.0 倍速选项

Screenshot 2025-12-11 at 07.22.57.png

本系列分为多篇,本文聚焦 竖屏视频·全屏播放 场景下的 3 倍速实现。

系列回顾

场景说明

本文分析的具体场景为:竖屏视频全屏播放

499416D4-1488-4417-89CD-E42861795807.png

开发环境

  • 哔哩哔哩版本:8.41.0

  • 逆向框架:MonkeyDev

  • 反汇编工具:IDA Professional 9.0

  • IDA插件:patching

  • UI 调试工具:Lookin

分析

1. 播放速度组件定位

通过 Lookin 分析 UI 层级可以发现,播放速度面板对应的视图组件为:

VKSettingView.TabContent

该组件内部持有一个 VKSettingView.TabModel,用于描述播放速度相关的数据模型。

157920C7-792E-4FEC-AF23-1AA783CBC33D.png

import Foundation

class VKSettingView.TabContent: VKSettingView.BaseContent {
  /* fields */
    var model: VKSettingView.TabModel ?
    var lazy selecter: VKSettingView.VKSelectControl ?
} 

2. TabModel 结构分析

Mach-O 中导出的 Swift 文件可以确认,VKSettingView.TabModel 中包含一个 items 属性,其类型为 [String],极有可能即为 播放速度数组

进一步在 IDA 中查看该类的方法实现,可以发现 items 对应的 setter 方法为:sub_10D8B5FA8

class VKSettingView.TabModel: VKSettingView.BaseModel {
  /* fields */
    var icon: String
    var itemsSize: __C.CGSize
    var items: [String]
    var selectedIndex: Int
    var dynamicSelectedString: String?
    var enableRepeatSelect: Swift.Bool
    var selectChangeCallback: ((_:))?
  /* methods */
    func sub_10d8b5bc4 // getter (instance)
    func sub_10d8b5c80 // setter (instance)
    func sub_10d8b5cdc // modify (instance)
    func sub_10d8b5d64 // getter (instance)
    func sub_10d8b5dfc // setter (instance)
    func sub_10d8b5e50 // modify (instance)
    func sub_10d8b5efc // getter (instance)
    func sub_10d8b5fa8 // setter (instance)
    func sub_10d8b5ff8 // modify (instance)
...
}

41E4F621-7F23-4445-A24D-0347CB125655.png

3. items 赋值来源追踪

  • 尝试直接对 sub_10D8B5FA8 添加符号断点并未触发,因此推断该属性可能通过 Objective-C Runtime 间接调用。

818F4D98-BD7D-4C7F-A134-A566E41E6D99.png

  • 结合 Swift / Objective-C 混编特性,对 -[TabModel setItems:] 添加断点后成功捕获调用。

C1BA5A52-C023-45FE-8436-EB592F188CF1.png

  • 通过 LLDB 打印参数内容可以确认:

    (0.5, 0.75, 1.0, 1.25, 1.5, 2.0)
    

    该数组正是当前 UI 中显示的播放速度列表。

LLDB:

(lldb) register read x2
      x2 = 0x000000028044efc0
(lldb) p (id)0x000000028044efc0
(__NSArrayI *) 0x000000028044efc0 @"6 elements"
(lldb) po (id)0x000000028044efc0
<__NSArrayI 0x28044efc0>(
0.5,
0.75,
1.0,
1.25,
1.5,
2.0
)

4. 调用栈分析

查看调用栈可以发现,items 的赋值逻辑来自:

-[BBPlayerPlaySettingWidgetV2 playbackRate:]

说明 播放速度数组是在该方法中被构造并传入 TabModel 的

调用堆栈:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 10.1
  * frame #0: 0x000000011084df44 bili-universal`-[TabModel setItems:]
    frame #1: 0x0000000128d2ba78 BiliBiliTweak.dylib`_logos_method$App$VKSettingViewTabModel$setItems$(self=0x00000002821d6220, _cmd="setItems:", items=6 elements) at NJDetailPlayerAd.xm:418:5
    frame #2: 0x000000011062db8c bili-universal`-[BBPlayerPlaySettingWidgetV2 playbackRate:] + 488
    frame #3: 0x000000011062c17c bili-universal`sub_10D69410C + 112
    frame #4: 0x000000018ff78ce0 CoreFoundation`__NSARRAY_IS_CALLING_OUT_TO_A_BLOCK__ + 16
    frame #5: 0x000000018fe7cc0c CoreFoundation`-[__NSArrayM enumerateObjectsWithOptions:usingBlock:] + 192
...

5. 伪代码验证

IDA 中分析 -[BBPlayerPlaySettingWidgetV2 playbackRate:] 的伪代码,可以清晰看到播放速度数组是通过如下方式写死创建的:

v25[0] = CFSTR("0.5");
v25[1] = CFSTR("0.75");
v25[2] = CFSTR("1.0");
v25[3] = CFSTR("1.25");
v25[4] = CFSTR("1.5");
v25[5] = CFSTR("2.0");

至此可以确认:竖屏全屏场景下的播放速度选项并非动态配置,而是硬编码在该方法中

伪代码:

id __cdecl -[BBPlayerPlaySettingWidgetV2 playbackRate:](BBPlayerPlaySettingWidgetV2 *self, SEL a2, id a3)
{
...
  v4 = objc_retain(a3);
  v5 = objc_retainAutoreleasedReturnValue(-[BBPlayerObject context](self, "context"));
  v6 = objc_retainAutoreleasedReturnValue(-[BBPlayerContext status](v5, "status"));
  v7 = -[BBPlayerStatus isVerticalScreen](v6, "isVerticalScreen");
  objc_release(v6);
  objc_release(v5);
  if ( v7 )
  {
    v25[0] = CFSTR("0.5");
    v25[1] = CFSTR("0.75");
    v25[2] = CFSTR("1.0");
    v25[3] = CFSTR("1.25");
    v25[4] = CFSTR("1.5");
    v25[5] = CFSTR("2.0");
    v8 = objc_retainAutoreleasedReturnValue(+[NSArray arrayWithObjects:count:](&OBJC_CLASS___NSArray, "arrayWithObjects:count:", v25, 6LL));
    v21 = 0LL;
    v22 = &v21;
...

通用解决方案

由于播放速度数组是通过 +[NSArray arrayWithObjects:count:] 构造的,因此可以采用 全局 Hook 的方式,对该方法进行拦截与替换。

核心思路如下:

  1. 判断 count == 6
  2. 校验原始数组内容是否与默认倍速数组一致
  3. 在满足条件时返回包含 3.0 的新数组

Hook 实现代码

%hook NSArray

+ (instancetype)arrayWithObjects:(id *)objects count:(NSUInteger)cnt {
    if (cnt != 6) {
        return %orig;
    }
    NSArray *origArr = %orig(objects, cnt);
    // 用 __autoreleasing 修饰数组元素
    __autoreleasing id oldRates[] = {
        @"0.5",
        @"0.75",
        @"1.0",
        @"1.25",
        @"1.5",
        @"2.0"
    };
    NSUInteger oldRatesCount = sizeof(oldRates) / sizeof(oldRates[0]);
    // 传数组名即可,数组名会退化为指针类型 __autoreleasing id *
    NSArray *oldRatesArr = %orig(oldRates, oldRatesCount);
    if (cnt == 6 && [origArr isEqualToArray:oldRatesArr]) {
        __autoreleasing id newRates[] = {
            @"0.5",
            @"1.0",
            @"1.25",
            @"1.5",
            @"2.0",
            @"3.0"
        };
        NSUInteger newRatesCount = sizeof(newRates) / sizeof(newRates[0]);
        NSArray *newRatesArr = %orig(newRates, newRatesCount);
        return newRatesArr;
    }
    return origArr;
}

%end

最终效果

竖屏视频全屏播放场景下,播放速度列表成功新增 3.0 倍速,且不影响其他播放场景与现有功能逻辑。

Screenshot 2025-12-16 at 10.18.43.png

总结

本文通过 UI 分析、调用栈追踪与伪代码验证,完整定位了 竖屏视频全屏播放场景 下播放速度数组的生成位置,并给出了一个 稳定、通用且侵入性较低Hook 方案。

该思路同样适用于其他存在硬编码配置的功能修改场景。

代码

BiliBiliMApp-无广告版哔哩哔哩

昨天 — 2025年12月17日iOS

我和 CloudKit 的这八年:从开源 IceCream 到商业应用实战

作者 Fatbobman
2025年12月17日 22:12

IceCream 作者 Cai Yue 分享他与 CloudKit 八年的开发历程:从 2017 年开源 IceCream 并获得 Apple 官方认可,到将 CloudKit 应用于 Music Mate 和 Setlists 等商业项目的实战经验。文章深入探讨了 CloudKit 的核心优势、关键局限以及进阶玩法。通过真实案例展示如何在生产环境中高效使用 CloudKit,为苹果生态开发者提供避坑指南和最佳实践。

Use 'git mv' to record filename case changes in Git

作者 Ole Begemann
2025年12月17日 01:11

After my previous post Tracking renamed files in Git, here’s another entry in my ongoing series “I thought git mv was useless but I was wrong”.

This one’s especially relevant to users on macOS and Windows, where the file system is case-insensitive by default. More precisely, APFS on macOS is case-insensitive but case-preserving by default. That is, A.TXT and a.txt refer to the same file (and these two cannot coexist in the same directory), but the file system records the filename exactly as you entered it.

If you’re on a such a file system and change the case of a filename, Git will not record the new name — unless you use git mv to perform the renaming.

Demo

1. Without git mv (bad)

Note: I tested this on macOS with the default APFS (case-insensitive) file system. You’ll get different results if your file system is case-sensitive.

Let’s create a fresh repository and commit a single file named A.txt:

mkdir testrepo
cd testrepo
git init
echo "Hello" > A.txt
git add .
git commit -m "Create A"
[main (root-commit) 3d73aea] Create A
 1 file changed, 1 insertion(+)
 create mode 100644 A.txt

Now we rename the file from A.txt to a.txt:

# Rename the file (change case)
# Note: not using `git mv`
mv A.txt a.txt
git status
nothing to commit, working tree clean

That’s interesting. git status says “nothing to commit” because nothing has changed from its perspective. Git is still tracking a file named A.txt, whose contents haven’t changed.

If we now make edits to the file a.txt (aka A.txt; both names refer to the same file), Git tracks this as a change of the existing file, which is still named A.txt in Git’s datastore:

echo "World" > a.txt
git status
Changes not staged for commit:
	modified:   A.txt

Let’s commit the change:

git add .
git commit -m "Edit A"
[main e86bcb2] Edit A
 1 file changed, 1 insertion(+), 1 deletion(-)

Now we’re in a situation where the recorded filenames on the file system and in Git have diverged. A fresh clone of the repository will create the file with its original name A.txt because that’s the spelling Git has recorded:

cd ..
git clone testrepo testrepo-clone
cd testrepo-clone
ls
A.txt

I think this is a real problem. You might assume it’s not an issue as long as all people working with this repo are on case-insensitive file systems, but can you guarantee that? And even if you can, you cannot guarantee that the software you’re writing will only ever be used on case-insensitive file systems.

For instance, if your code loads the file named a.txt from the app’s bundle but the CI step that packages your app for release checked the file out as A.txt, your app will fail for users on case-sensitive file systems. And the reason is that Git has stored a different filename than what you’re using.

You can avoid this by using git mv for renaming, as shown in the second demo below.

2. With git mv (good)

Same setup as above: a fresh repository with a single file named A.txt:

mkdir testrepo2
cd testrepo2
git init
echo "Hello" > A.txt
git add .
git commit -m "Create A"
[main (root-commit) abc2bba] Create A
 1 file changed, 1 insertion(+)
 create mode 100644 A.txt

We now rename A.txt to a.txt again, but this time we use git mv:

git mv A.txt a.txt
git status
Changes to be committed:
	renamed:    A.txt -> a.txt

Aha, Git recognizes the rename. This is exactly what we want! We can commit this to record the new filename:

git commit -m "Rename A.txt to a.txt"
[main 42d1974] Rename A.txt to a.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename A.txt => a.txt (100%)

That’s it. Git and the file system use the same filename, and so will any new clone of the repository. Future bugs avoided.

Workaround: 2-stage commit

I said in my previous post that it’s not always practical to use git mv for renaming. What to do in this case?

My workaround is to split the rename operation into 2 renamings and commit each separately:

  1. Rename A.txt to an arbitrary temporary name, e.g. a_.txt. This filename must differ from the original filename in more than just case. Commit this as “Rename A.txt to a.txt (step 1/2)”.
  2. Rename a_.txt to the final name a.txt. Commit this as “Rename A.txt to a.txt (step 2/2)”.

By using an intermediate filename that differs in more than just case, we force Git to record the renamings. It looks a little clunky in the commit log, but I’ll take that over introducing a hidden bug.

SwiftUI 状态管理详解

作者 如此风景
2025年12月16日 14:57

SwiftUI 状态管理详解

SwiftUI 的状态管理系统是其核心特性,理解它是构建复杂应用的基础。以下是 SwiftUI 状态管理的全面详解:

1. 状态管理的基本概念

什么是状态?

状态是随时间变化的数据,这些变化会触发视图的更新。

// 基本状态声明
@State private var count = 0  // 简单值
@State private var user = User()  // 引用类型(不推荐)
@State private var items: [String] = []  // 集合

2. @State

特性

  • 私有状态:只能在当前视图内部访问
  • 值类型专用:最适合简单值类型(Int、String、Bool等)
  • 视图拥有:当视图被销毁时,状态也被销毁
  • 自动更新:状态改变时,视图自动重新计算
struct CounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

底层原理

// @State 的简化实现概念
struct State<Value> {
    var wrappedValue: Value
    var projectedValue: Binding<Value>
    
    // SwiftUI 内部管理:
    // 1. 在视图之外存储实际值
    // 2. 监听变化
    // 3. 触发视图更新
}

3. @Binding

特性

  • 双向连接:在视图之间创建双向数据流
  • 不拥有数据:只是对现有状态的引用
  • 使用 $ 前缀:获取绑定
// 父视图
struct ParentView: View {
    @State private var isOn = false
    
    var body: some View {
        ChildView(isOn: $isOn)  // 传递绑定
    }
}

// 子视图
struct ChildView: View {
    @Binding var isOn: Bool
    
    var body: some View {
        Toggle("Switch", isOn: $isOn)
    }
}

4. @StateObject vs @ObservedObject

对比

特性 @StateObject @ObservedObject
生命周期 视图拥有并管理 外部管理,视图只观察
创建时机 视图初始化时创建 外部传入
数据丢失 视图更新时保留 视图更新时可能丢失
使用场景 创建并拥有 ViewModel 接收父视图传递的 ViewModel

@StateObject 示例

class UserViewModel: ObservableObject {
    @Published var name = "John"
    @Published var age = 30
}

struct UserView: View {
    @StateObject var viewModel = UserViewModel()  // 视图创建并拥有
    
    var body: some View {
        VStack {
            Text("Name: \(viewModel.name)")
            Text("Age: \(viewModel.age)")
            Button("Increase Age") {
                viewModel.age += 1
            }
        }
    }
}

@ObservedObject 示例

struct ParentView: View {
    @StateObject var sharedViewModel = SharedViewModel()
    
    var body: some View {
        VStack {
            ChildView(viewModel: sharedViewModel)  // 传递引用
        }
    }
}

struct ChildView: View {
    @ObservedObject var viewModel: SharedViewModel  // 观察外部对象
    
    var body: some View {
        Text("Data: \(viewModel.data)")
    }
}

5. @EnvironmentObject

特性

  • 全局共享:在视图层次中隐式共享
  • 避免传递链:不需要逐层传递
  • 必须提供:使用前必须通过 .environmentObject() 提供
// 1. 创建 ObservableObject
class AppSettings: ObservableObject {
    @Published var theme = "Light"
    @Published var fontSize = 16.0
}

// 2. 在根视图提供
@main
struct MyApp: App {
    @StateObject var settings = AppSettings()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(settings)  // 注入环境
        }
    }
}

// 3. 在任何子视图中访问
struct ContentView: View {
    @EnvironmentObject var settings: AppSettings
    
    var body: some View {
        VStack {
            Text("Theme: \(settings.theme)")
            Button("Toggle Theme") {
                settings.theme = settings.theme == "Light" ? "Dark" : "Light"
            }
        }
    }
}

6. @Environment

特性

  • 系统值:访问系统提供的环境值
  • 自定义环境:也可以定义自己的环境值
  • 只读:通常是只读的(除非使用 Binding)
// 使用系统环境值
struct SystemInfoView: View {
    @Environment(\.colorScheme) var colorScheme
    @Environment(\.sizeCategory) var sizeCategory
    @Environment(\.locale) var locale
    
    var body: some View {
        VStack {
            Text("Color Scheme: \(colorScheme == .dark ? "Dark" : "Light")")
            Text("Locale: \(locale.identifier)")
        }
    }
}

// 自定义环境值
// 1. 定义环境键
private struct UserThemeKey: EnvironmentKey {
    static let defaultValue = "Light"
}

extension EnvironmentValues {
    var userTheme: String {
        get { self[UserThemeKey.self] }
        set { self[UserThemeKey.self] = newValue }
    }
}

// 2. 使用自定义环境
struct ThemedView: View {
    @Environment(\.userTheme) var theme
    
    var body: some View {
        Text("Current theme: \(theme)")
    }
}

// 3. 设置自定义环境
ParentView()
    .environment(\.userTheme, "Dark")

7. @Published 与 ObservableObject

工作机制

class UserData: ObservableObject {
    // 自动发布变化
    @Published var name = "Alice"
    @Published var score = 100
    
    // 手动控制发布
    var manualProperty = "Test" {
        willSet {
            objectWillChange.send()  // 手动触发更新
        }
    }
    
    // 计算属性需要手动触发
    var displayName: String {
        "User: \(name)"
        // 注意:计算属性变化不会自动触发,除非依赖的 @Published 属性变化
    }
}

8. 状态管理的最佳实践

1. 选择合适的工具

// 决策树:
// 1. 是否只在当前视图使用? → @State
// 2. 是否需要在子视图中修改? → @Binding
// 3. 是否在多个视图共享? → ObservableObject
// 4. 是否全局共享? → @EnvironmentObject
// 5. 是否访问系统设置? → @Environment

2. 避免在 body 中创建状态

// ❌ 错误:每次都会创建新实例
var body: some View {
    let viewModel = ViewModel()  // 错误!
    // ...
}

// ✅ 正确:使用 @StateObject
struct MyView: View {
    @StateObject var viewModel = ViewModel()  // 只创建一次
    
    var body: some View {
        // ...
    }
}

3. 状态提升(State Hoisting)

// 将状态提升到最近的共同祖先
struct ParentView: View {
    @State private var text = ""  // 状态提升
    
    var body: some View {
        VStack {
            ChildAView(text: $text)
            ChildBView(text: $text)
        }
    }
}

struct ChildAView: View {
    @Binding var text: String
    
    var body: some View {
        TextField("Enter text", text: $text)
    }
}

struct ChildBView: View {
    @Binding var text: String
    
    var body: some View {
        Text("You typed: \(text)")
    }
}

4. 使用 ViewModel 管理复杂状态

class LoginViewModel: ObservableObject {
    @Published var username = ""
    @Published var password = ""
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    var isFormValid: Bool {
        !username.isEmpty && !password.isEmpty
    }
    
    func login() async {
        isLoading = true
        errorMessage = nil
        
        // 模拟网络请求
        do {
            try await Task.sleep(nanoseconds: 1_000_000_000)
            // 处理登录逻辑
        } catch {
            errorMessage = "Login failed"
        }
        
        isLoading = false
    }
}

struct LoginView: View {
    @StateObject var viewModel = LoginViewModel()
    
    var body: some View {
        Form {
            TextField("Username", text: $viewModel.username)
            SecureField("Password", text: $viewModel.password)
            
            if let error = viewModel.errorMessage {
                Text(error).foregroundColor(.red)
            }
            
            Button("Login") {
                Task {
                    await viewModel.login()
                }
            }
            .disabled(!viewModel.isFormValid || viewModel.isLoading)
        }
    }
}

9. 状态管理的性能优化

1. 使用 Equatable 避免不必要的更新

struct UserView: View, Equatable {
    let user: User
    
    var body: some View {
        Text(user.name)
            .background(Color.random())
    }
    
    // 实现 Equatable,只有 user.id 变化时才更新
    static func == (lhs: UserView, rhs: UserView) -> Bool {
        lhs.user.id == rhs.user.id
    }
}

// 在父视图中使用
List(users) { user in
    UserView(user: user)
        .equatable()  // 启用自定义相等性检查
}

2. 使用 .id() 修饰符强制更新

struct DynamicView: View {
    @State private var version = 0
    
    var body: some View {
        VStack {
            // 当 version 变化时,整个视图会重新创建
            ComplexView()
                .id(version)
            
            Button("Refresh") {
                version += 1  // 强制重新创建
            }
        }
    }
}

3. 避免在 body 中创建闭包

// ❌ 错误:每次都会创建新闭包
Button(action: {
    self.doSomething()  // 每次 body 计算都创建新闭包
}) {
    Text("Click me")
}

// ✅ 正确:使用私有方法
Button(action: doSomething) {
    Text("Click me")
}

private func doSomething() {
    // 处理逻辑
}

10. 状态管理的常见陷阱

陷阱1:在子视图中修改 @State

// ❌ 错误:不能直接在子视图中修改父视图的 @State
struct ChildView: View {
    var count: Int
    
    var body: some View {
        Button("Increment") {
            // 错误!不能修改
        }
    }
}

// ✅ 正确:使用 @Binding
struct ChildView: View {
    @Binding var count: Int
    
    var body: some View {
        Button("Increment") {
            count += 1  // 正确!
        }
    }
}

陷阱2:@State 与引用类型

class User {
    var name = "John"
}

struct UserView: View {
    @State private var user = User()  // 不推荐!
    
    var body: some View {
        Button("Change Name") {
            user.name = "Alice"  // ❌ 不会触发视图更新!
        }
    }
}

// ✅ 正确:使用 @StateObject 或 @ObservedObject
class UserViewModel: ObservableObject {
    @Published var name = "John"
}

struct UserView: View {
    @StateObject var viewModel = UserViewModel()
    
    var body: some View {
        Button("Change Name") {
            viewModel.name = "Alice"  // ✅ 会触发更新
        }
    }
}

陷阱3:@EnvironmentObject 未提供

struct MyView: View {
    @EnvironmentObject var settings: AppSettings  // 运行时崩溃如果未提供!
    
    var body: some View {
        Text(settings.theme)
    }
}

// ✅ 安全使用
struct MyView: View {
    @EnvironmentObject var settings: AppSettings
    
    var body: some View {
        if let settings = settings {  // 实际上 @EnvironmentObject 是 non-optional
            Text(settings.theme)
        } else {
            Text("Settings not available")
        }
    }
}

11. 状态管理与 Combine 集成

import Combine

class DataService: ObservableObject {
    @Published var data: [String] = []
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // 使用 Combine 处理复杂数据流
        $data
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .sink { newData in
                print("Data updated: \(newData)")
            }
            .store(in: &cancellables)
    }
    
    func fetchData() {
        // 模拟网络请求
        URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com")!)
            .map(\.data)
            .decode(type: [String].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { _ in },
                  receiveValue: { [weak self] newData in
                self?.data = newData
            })
            .store(in: &cancellables)
    }
}

12. 总结:状态管理选择指南

场景 推荐方案 说明
简单视图内部状态 @State 计数器、开关状态等
父子视图双向绑定 @Binding 表单输入、设置传递
视图自己的 ViewModel @StateObject 创建并拥有 ViewModel
接收外部 ViewModel @ObservedObject 父视图传递的 ViewModel
跨多级视图共享 @EnvironmentObject 主题、用户设置等
访问系统设置 @Environment 深色模式、区域设置
用户默认设置 @AppStorage 持久化简单设置
场景状态恢复 @SceneStorage 多窗口应用状态恢复

记住关键原则:状态应该存储在能覆盖所有需要访问该状态的视图的最高层级中,但不要更高

这种设计使得 SwiftUI 应用既灵活又高效,能够自动优化视图更新,提供流畅的用户体验。

《Flutter全栈开发实战指南:从零到高级》- 25 -性能优化

2025年12月16日 14:46

引言

当用户说某个App用起来很卡时,他们真正抱怨的是什么? 不是CPU使用率,不是内存占用,甚至不是帧率数字。用户感受到的是响应延迟界面跳帧操作不跟手。这就是为什么我们要做性能优化——不是为了让数字好看,而是为了让用户感知流畅。

根据Google的用户体验研究:

  • 100ms内响应:用户感觉瞬间完成;
  • 1秒内响应:用户感觉流畅自然;
  • 1-3秒响应:用户开始注意到延迟;
  • 3秒以上响应:用户感到不耐烦,面临卸载的可能;

今天,我们将从多个维度,深入理解Flutter性能优化的是什么为什么怎么做

一、优化构建性能

是什么导致了构建性能问题?

让我们先来看一张构建流程图:

graph TD
    A[setState调用] --> B[Widget树重建]
    B --> C[Element树更新]
    C --> D[RenderObject更新]
    D --> E[布局计算]
    E --> F[绘制执行]
    
    G[性能瓶颈来源] --> H[过度重建]
    G --> I[复杂计算]
    G --> J[深度嵌套]
    G --> K[不当的Key使用]

优化手段1:const构造函数 - 编译期优化

是什么:const构造函数创建的Widget在编译时确定,运行时不会重复构建。

为什么重要

  • 避免不必要的Widget实例创建
  • 减少垃圾回收压力
  • 提高热重载性能

怎么做

// 不推荐:每次build都创建新对象
Widget build(BuildContext context) {
  return Container(
    padding: EdgeInsets.all(16), // 每次创建新EdgeInsets
    child: Text('标题', style: TextStyle(fontSize: 18)), // 每次创建新TextStyle
  );
}

// 优化写法:使用const
Widget build(BuildContext context) {
  return const Padding(
    padding: EdgeInsets.all(16), // const EdgeInsets
    child: Text(
      '标题',
      style: TextStyle(fontSize: 18),
    ),
  );
}

// 推荐:提取常量
class OptimizedWidget extends StatelessWidget {
  static const _padding = EdgeInsets.all(16);
  static const _textStyle = TextStyle(fontSize: 18);
  
  @override
  Widget build(BuildContext context) {
    return const Padding(
      padding: _padding,
      child: Text('标题', style: _textStyle),
    );
  }
}

注意点

  • const只能用于参数在编译时可确定的Widget
  • 带回调函数的Widget不能使用const
  • 列表中的item使用const效果最明显

优化手段2:Key的正确使用 - 控制Element复用

是什么:Key帮助Flutter识别Widget的身份,决定是否复用Element。

为什么重要

  • 错误的Key导致不必要的Element重建
  • 正确的Key保持状态在Widget移动时不被丢失

如何选择Key类型?

Key类型 适用场景 注意点
ValueKey 基于值的唯一标识 值变化时状态重置
ObjectKey 基于对象的唯一标识 适合对象列表
UniqueKey 绝对唯一标识 每次重建都不同
GlobalKey 全局唯一标识 谨慎使用,性能开销大
PageStorageKey 保持滚动位置 用于可滚动列表

怎么做

class KeyOptimizationDemo extends StatefulWidget {
  @override
  _KeyOptimizationDemoState createState() => _KeyOptimizationDemoState();
}

class _KeyOptimizationDemoState extends State<KeyOptimizationDemo> {
  List<String> items = ['A', 'B', 'C'];
  
  void _reverseList() {
    setState(() {
      items = items.reversed.toList();
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ElevatedButton(
          onPressed: _reverseList,
          child: const Text('反转列表'),
        ),
        Expanded(
          child: ListView.builder(
            itemCount: items.length,
            itemBuilder: (context, index) {
              final item = items[index];
              
              // 情况1:没有Key - 反转时状态会错乱
              // return StatefulListItem(text: item);
              
              // 情况2:使用index作为Key - 反转时状态错乱
              // return StatefulListItem(key: ValueKey(index), text: item);
              
              // 情况3:使用ValueKey - 反转时状态保持正确
              return StatefulListItem(key: ValueKey(item), text: item);
            },
          ),
        ),
      ],
    );
  }
}

class StatefulListItem extends StatefulWidget {
  final String text;
  
  const StatefulListItem({Key? key, required this.text}) : super(key: key);
  
  @override
  _StatefulListItemState createState() => _StatefulListItemState();
}

class _StatefulListItemState extends State<StatefulListItem> {
  int _counter = 0;
  
  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text('${widget.text} - 点击次数: $_counter'),
      onTap: () => setState(() => _counter++),
    );
  }
}

优化手段3:RepaintBoundary - 隔离重绘区域

是什么:创建一个独立的绘制图层,避免不必要的重绘。

为什么重要

  • 频繁变化的组件不影响静态区域
  • 减少整体重绘范围
  • 提高渲染效率

适用场景

  • 频繁动画的组件
  • 独立滚动的列表
  • 视频播放器
  • 游戏画布

怎么做

class RepaintBoundaryDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          // 静态区域,不需要重绘边界
          const StaticHeader(),
          
          // 频繁动画区域,需要隔离
          RepaintBoundary(
            child: AnimatedClock(),
          ),
          
          // 静态区域
          const StaticContent(),
          
          // 独立滚动的列表
          Expanded(
            child: RepaintBoundary(
              child: ProductList(),
            ),
          ),
        ],
      ),
    );
  }
}

注意点

  • 每个RepaintBoundary都有内存和性能开销
  • 不要过度使用,特别是在深度嵌套中
  • 优先隔离频繁变化的小区域

优化手段4:didUpdateWidget - 严格控制更新

是什么:StatefulWidget更新时的回调,可以精确控制哪些变化需要重建。

为什么重要

  • 避免不必要的setState调用
  • 可以只更新真正变化的部分
  • 减少重建范围

怎么做

class SmartWidget extends StatefulWidget {
  final String title;
  final List<int> data;
  
  const SmartWidget({Key? key, required this.title, required this.data})
      : super(key: key);
  
  @override
  _SmartWidgetState createState() => _SmartWidgetState();
}

class _SmartWidgetState extends State<SmartWidget> {
  late String _currentTitle;
  late List<int> _currentData;
  String? _computedResult;
  
  @override
  void initState() {
    super.initState();
    _currentTitle = widget.title;
    _currentData = widget.data;
    _computeResult();
  }
  
  @override
  void didUpdateWidget(SmartWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    
    // 只有title变化时才重建UI
    if (widget.title != _currentTitle) {
      _currentTitle = widget.title;
      setState(() {});
    }
    
    // data变化时重新计算,但不一定需要重建UI
    if (!_listEquals(widget.data, _currentData)) {
      _currentData = widget.data;
      _computeResult();
      // 这里不调用setState,因为_computedResult可能被其他Widget使用
    }
  }
  
  void _computeResult() {
    _computedResult = '计算结果: ${widget.data.length}';
  }
  
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: [
          Text(_currentTitle),
          if (_computedResult != null) Text(_computedResult!),
        ],
      ),
    );
  }
}

二、优化内存

Flutter内存泄漏的根源?

graph LR
    A[内存泄漏] --> B[未释放的订阅]
    A --> C[未取消的Timer]
    A --> D[未dispose的Controller]
    A --> E[循环引用]
    A --> F[大对象未及时释放]
    
    G[泄漏影响] --> H[内存持续增长]
    G --> I[OOM崩溃]
    G --> J[应用被系统终止]
    G --> K[用户体验差]

优化手段1:管理资源生命周期

是什么:确保所有需要手动释放的资源都被正确释放。

为什么重要

  • Dart有垃圾回收,但某些资源需要手动管理
  • 未释放的资源会导致内存泄漏
  • 订阅和监听器可能持有Widget引用

需要管理的资源类型

  1. StreamSubscription
  2. Timer
  3. ScrollController/TextEditingController
  4. AnimationController
  5. FocusNode
  6. ImageStream

怎么做

// 典型的内存泄漏
class LeakyWidget extends StatefulWidget {
  @override
  _LeakyWidgetState createState() => _LeakyWidgetState();
}

class _LeakyWidgetState extends State<LeakyWidget> {
  StreamSubscription? _subscription;
  Timer? _timer;
  ScrollController _controller = ScrollController();
  
  @override
  void initState() {
    super.initState();
    
    // 订阅Stream
    _subscription = Stream.periodic(Duration(seconds: 1))
        .listen((_) => print('tick'));
    
    // 启动Timer
    _timer = Timer.periodic(Duration(seconds: 2), (_) => print('timer'));
    
    // 添加监听器
    _controller.addListener(() => print('scrolling'));
  }
  
  @override
  void dispose() {
    // 忘记取消和释放
    // _subscription?.cancel();
    // _timer?.cancel();
    // _controller.dispose();
    
    super.dispose();
  }
}

// 正确做法:
class SafeWidget extends StatefulWidget {
  @override
  _SafeWidgetState createState() => _SafeWidgetState();
}

class _SafeWidgetState extends State<SafeWidget> {
  late final StreamSubscription _subscription;
  late final Timer _timer;
  late final ScrollController _controller;
  
  @override
  void initState() {
    super.initState();
    
    _controller = ScrollController();
    _controller.addListener(_onScroll);
    
    _subscription = Stream.periodic(Duration(seconds: 1))
        .listen(_onTick);
    
    _timer = Timer.periodic(Duration(seconds: 2), _onTimer);
  }
  
  void _onTick(_) => print('tick');
  void _onTimer(_) => print('timer');
  void _onScroll() => print('scrolling');
  
  @override
  void dispose() {
    // 按创建顺序的逆序释放
    _timer.cancel();
    _subscription.cancel();
    _controller.dispose();
    
    super.dispose();
  }
}

优化手段2:大对象管理

是什么:针对图像、列表等大内存对象进行特殊管理。

为什么重要

  • 图像是移动应用内存的最大占用者
  • 不当的图像加载会导致OOM
  • 列表数据可能占用大量内存

优化策略

  1. 图像压缩和缓存
  2. 列表分页加载
  3. 对象池复用
  4. 懒加载和预加载平衡

怎么做

// 优化图像内存
class ImageMemoryOptimizer {
  // 1. 使用正确的图像尺寸
  static Widget buildOptimizedImage(String url) {
    return Image.network(
      url,
      width: 100,  
      height: 100,
      fit: BoxFit.cover,
      cacheWidth: 200,  
      cacheHeight: 200,
    );
  }
  
  // 2. 使用缓存策略
  static Widget buildCachedImage(String url) {
    return CachedNetworkImage(
      imageUrl: url,
      placeholder: (context, url) => CircularProgressIndicator(),
      errorWidget: (context, url, error) => Icon(Icons.error),
      width: 100,
      height: 100,
      fit: BoxFit.cover,
      memCacheWidth: 200,  
      memCacheHeight: 200,
      maxWidthDiskCache: 400,  
      maxHeightDiskCache: 400,
    );
  }
  
  // 3. 管理图片预加载
  static final Map<String, ImageProvider> _preloadedImages = {};
  
  static Future<void> preloadImage(String url) async {
    if (_preloadedImages.containsKey(url)) return;
    
    final completer = Completer<void>();
    final imageProvider = NetworkImage(url);
    
    // 预加载到缓存
    final stream = imageProvider.resolve(ImageConfiguration.empty);
    final listener = ImageStreamListener((info, sync) {
      _preloadedImages[url] = imageProvider;
      completer.complete();
    });
    
    stream.addListener(listener);
    await completer.future;
    stream.removeListener(listener);
  }
}

// 优化列表内存
class ListMemoryOptimizer {
  // 1. 分页加载
  static Future<List<Item>> loadItems(int page, int pageSize) async {
    final start = page * pageSize;
    final end = start + pageSize;
    return await _fetchItems(start, end);
  }
  
  // 2. 重用列表项
  static Widget buildReusableListItem(Item item) {
    return const ReusableListItem(item: item);
  }
}

优化手段3:WeakReference使用

是什么:弱引用允许对象被垃圾回收,即使还有引用指向它。

为什么重要

  • 打破循环引用
  • 避免因监听器导致的内存泄漏
  • 允许缓存被自动清理

适用场景

  • 事件监听器
  • 缓存实现
  • 观察者模式

怎么做

// 使用弱引用的事件管理器
import 'dart:weak';

class EventManager {
  final List<WeakReference<EventListener>> _listeners = [];
  
  void addListener(EventListener listener) {
    _listeners.add(WeakReference(listener));
  }
  
  void notify(String event) {
    // 清理被回收的监听器
    _listeners.removeWhere((ref) => ref.target == null);
    
    // 通知存活的监听器
    for (final ref in _listeners) {
      ref.target?.onEvent(event);
    }
  }
}

abstract class EventListener {
  void onEvent(String event);
}

// 具体使用
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> implements EventListener {
  late final EventManager _eventManager;
  
  @override
  void initState() {
    super.initState();
    _eventManager = EventManager();
    _eventManager.addListener(this); 
  }
  
  @override
  void onEvent(String event) {
    if (mounted) {
      setState(() {
        // 处理事件...
      });
    }
  }
  
  @override
  void dispose() {
    // 不需要手动移除监听器
    super.dispose();
  }
}

三、优化包体积

包体积为什么重要?

graph TD
    A[包体积过大的影响] --> B[下载量下降]
    A --> C[存储空间占用]
    A --> D[更新频率降低]
    A --> E[低端设备体验差]
    
    F[体积构成分析] --> G[第一位:资源文件]
    F --> H[第二位:Flutter引擎]
    F --> I[第三位:Dart代码]
    F --> J[第四位:三方库]

优化手段1:优化资源文件

是什么:减少图片、字体等资源文件的大小。

为什么重要

  • 资源文件通常占用最大体积
  • 未使用的资源白白占用空间
  • 未优化的资源加载慢

优化手段

  1. 删除未使用资源
  2. 压缩图片格式
  3. 字体子集化
  4. 按需加载大资源

怎么做

# pubspec.yaml优化示例
flutter:
  assets:
    # 不要导入整个目录
    # - assets/images/
    
    # 精确指定需要的文件
    - assets/images/icon.png
    - assets/images/logo.png
    - assets/images/splash.png
    
    # 使用WebP格式
    - assets/images/background.webp
    
  fonts:
    - family: NotoSans
      fonts:
        - asset: assets/fonts/NotoSans-Regular.ttf
          # 只包含需要的字体
          # - asset: assets/fonts/NotoSans-Bold.ttf
          #   weight: 700
# 资源优化常用的终端命令:
# 1. 查找大文件
find . -name "*.png" -size +100k -exec ls -lh {} \;

# 2. 转换为WebP
cwebp -q 80 input.png -o output.webp

# 3. 字体子集化
pyftsubset font.ttf --text-file=chinese_chars.txt

优化手段2:代码混淆

是什么:通过混淆、压缩和优化减少代码体积。

为什么重要

  • 移除未使用的代码
  • 缩短标识符名称
  • 优化控制流

优化工具

  1. R8/ProGuard(Android)
  2. Dart编译优化
  3. Tree Shaking

怎么做

// android/app/build.gradle
android {
    buildTypes {
        release {
            // 启用代码优化
            minifyEnabled true
            shrinkResources true
            
            // 使用R8
            useProguard true
            
            proguardFiles getDefaultProguardFile(
                'proguard-android-optimize.txt'
            ), 'proguard-rules.pro'
        }
    }
}
# proguard-rules.pro
# 保留Flutter必要类
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }

# 移除日志
-assumenosideeffects class android.util.Log {
    public static *** d(...);
    public static *** i(...);
    public static *** w(...);
    public static *** e(...);
}

优化手段3:按需加载与延迟导入

是什么:将应用拆分为多个模块,按需加载。

为什么重要

  • 减少初始加载体积
  • 节省用户流量,提高启动速度

实现方式

  1. deferred import
  2. 动态特性模块
  3. 代码分割

怎么做

// 延迟导入
import 'package:heavy_library/heavy_library.dart' deferred as heavy;

class LazyLoadWidget extends StatefulWidget {
  @override
  _LazyLoadWidgetState createState() => _LazyLoadWidgetState();
}

class _LazyLoadWidgetState extends State<LazyLoadWidget> {
  bool _isLoaded = false;
  
  Future<void> _loadLibrary() async {
    await heavy.loadLibrary();
    setState(() => _isLoaded = true);
  }
  
  @override
  Widget build(BuildContext context) {
    if (!_isLoaded) {
      return ElevatedButton(
        onPressed: _loadLibrary,
        child: const Text('加载功能模块'),
      );
    }
    
    return heavy.HeavyWidget();
  }
}

// 动态特性加载
class FeatureManager {
  static final Map<String, dynamic> _loadedFeatures = {};
  
  static Future<dynamic> loadFeature(String featureName) async {
    if (_loadedFeatures.containsKey(featureName)) {
      return _loadedFeatures[featureName];
    }
    
    switch (featureName) {
      case 'payment':
        final payment = await import('package:app/payment.dart')
            .deferred as dynamic;
        _loadedFeatures[featureName] = payment;
        return payment;
        
      case 'analytics':
        // 按需加载分析模块
        final analytics = await import('package:app/analytics.dart')
            .deferred as dynamic;
        _loadedFeatures[featureName] = analytics;
        return analytics;
    }
    
    return null;
  }
}

优化手段4:优化应用Bundle

是什么:利用平台特性进行智能分发。

为什么重要

  • Android App Bundle减少下载体积
  • iOS App Thinning按设备分发
  • 支持动态功能模块

实现方式

  1. Android App Bundle
  2. iOS App Thinning
  3. 动态功能交付

怎么做

// 启用Android App Bundle
android {
    bundle {
        language {
            enableSplit = true  // 按语言拆分
        }
        density {
            enableSplit = true  // 按屏幕密度拆分
        }
        abi {
            enableSplit = true  // 按CPU架构拆分
        }
    }
}
# 构建命令
# 1. 构建Android App Bundle
flutter build appbundle

# 2. 分析Bundle
flutter build appbundle --target-platform android-arm64 --analyze-size

# 3. 测试Bundle
bundletool build-apks --bundle=app.aab --output=app.apks
bundletool install-apks --apks=app.apks

四、优化渲染性能

什么导致了渲染卡顿?

graph TB
    A[渲染卡顿原因] --> B[复杂布局计算]
    A --> C[频繁绘制操作]
    A --> D[图层合成开销]
    A --> E[GPU负载过高]
    
    B --> F[嵌套过深]
    B --> G[约束传递频繁]
    
    C --> H[过度使用阴影]
    C --> I[透明度处理不当]
    C --> J[频繁裁剪操作]
    
    D --> K[RepaintBoundary过多]
    D --> L[图层混合模式复杂]

优化手段1:优化布局性能

是什么:减少布局计算的时间和复杂度。

为什么重要

  • 布局是渲染管线中最耗时的阶段之一
  • 复杂的布局导致界面卡顿
  • 不当的约束传递引发连锁重排

优化手段

  1. 减少Widget嵌套深度
  2. 使用合适的布局Widget
  3. 避免过度使用Flexible/Expanded
  4. 使用CustomSingleChildLayout/CustomMultiChildLayout

怎么做

// 不推荐:深度嵌套
Widget buildBadLayout() {
  return Container(
    child: Column(
      children: [
        Container(
          child: Row(
            children: [
              Container(child: Text('A')),
              Container(child: Text('B')),
              Container(child: Text('C')),
            ],
          ),
        ),
        // 更多嵌套...
      ],
    ),
  );
}

// 优化的布局
Widget buildGoodLayout() {
  return Column(
    children: [
      Row(
        children: const [
          Text('A'),
          SizedBox(width: 8),
          Text('B'),
          SizedBox(width: 8),
          Text('C'),
        ],
      ),
      // 使用间隔Widget替代多余的Container
      const SizedBox(height: 16),
    ],
  );
}

// 使用CustomMultiChildLayout优化复杂布局
class OptimizedLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomMultiChildLayout(
      delegate: _LayoutDelegate(),
      children: [
        LayoutId(
          id: _LayoutItem.header,
          child: const HeaderWidget(),
        ),
        LayoutId(
          id: _LayoutItem.content,
          child: const ContentWidget(),
        ),
        LayoutId(
          id: _LayoutItem.footer,
          child: const FooterWidget(),
        ),
      ],
    );
  }
}

class _LayoutDelegate extends MultiChildLayoutDelegate {
  @override
  void performLayout(Size size) {
    // 一次性计算所有子项的位置和大小
    final headerSize = layoutChild(
      _LayoutItem.header,
      BoxConstraints.loose(size),
    );
    
    final footerSize = layoutChild(
      _LayoutItem.footer,
      BoxConstraints.loose(size),
    );
    
    final contentConstraints = BoxConstraints(
      maxWidth: size.width,
      maxHeight: size.height - headerSize.height - footerSize.height,
    );
    
    layoutChild(_LayoutItem.content, contentConstraints);
    
    // 定位子项
    positionChild(_LayoutItem.header, Offset.zero);
    positionChild(_LayoutItem.content, Offset(0, headerSize.height));
    positionChild(_LayoutItem.footer, 
        Offset(0, size.height - footerSize.height));
  }
  
  @override
  bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) {
    return false;
  }
}

enum _LayoutItem { header, content, footer }

优化手段2:优化绘制性能

是什么:减少复杂绘制操作频率。

为什么重要

  • 复杂的绘制操作消耗GPU资源
  • 过度绘制浪费渲染时间
  • 不当的效果使用导致性能变差

优化策略

  1. 避免过度使用阴影
  2. 谨慎使用透明度
  3. 减少裁剪操作
  4. 使用缓存图片

怎么做

// 优化绘制
class PaintOptimization {
  // 1. 优化阴影
  static BoxDecoration optimizedShadow() {
    return BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(8),
      // 过度使用阴影
      // boxShadow: [
      //   BoxShadow(color: Colors.black12, blurRadius: 16),
      //   BoxShadow(color: Colors.black12, blurRadius: 8),
      //   BoxShadow(color: Colors.black12, blurRadius: 4),
      // ],
      
      // 优化后
      boxShadow: const [
        BoxShadow(
          color: Colors.black12,
          blurRadius: 4,
          spreadRadius: 0,
          offset: Offset(0, 2),
        ),
      ],
    );
  }
  
  // 2. 优化透明度
  static Widget optimizedOpacity() {
    // 使用Opacity Widget
    // return Opacity(
    //   opacity: 0.5,
    //   child: Container(color: Colors.blue),
    // );
    
    // 使用颜色透明度
    return Container(
      color: Colors.blue.withOpacity(0.5),
    );
    
    // 对于静态半透明,使用ColorFiltered
    // return ColorFiltered(
    //   colorFilter: ColorFilter.mode(
    //     Colors.white.withOpacity(0.5),
    //     BlendMode.modulate,
    //   ),
    //   child: Container(color: Colors.blue),
    // );
  }
  
  // 3. 优化裁剪
  static Widget optimizedClip() {
    // 复杂裁剪
    // return ClipPath(
    //   clipper: ComplexClipper(),
    //   child: Container(color: Colors.blue),
    // );
    
    // 简单裁剪
    return ClipRRect(
      borderRadius: BorderRadius.circular(8),
      child: Container(color: Colors.blue),
    );
    
    // 使用装饰而不是裁剪
    // return Container(
    //   decoration: BoxDecoration(
    //     color: Colors.blue,
    //     borderRadius: BorderRadius.circular(8),
    //   ),
    // );
  }
}

优化手段3:优化列表性能

是什么:针对ListView、GridView等可滚动组件进行优化。

为什么重要

  • 几乎所有的应用都会用到列表组件,他的性能好坏直接影响用户体验
  • 不当的列表实现导致滚动卡顿

优化策略

  1. 使用正确的ListView构造函数
  2. 设置合理的缓存范围
  3. 优化列表项构建
  4. 使用Sliver系列组件

怎么做

class ListOptimization {
  // 1. 选择合适的ListView构造函数
  static Widget buildOptimizedList(List<Item> items) {
    return ListView.builder(
      itemCount: items.length,
      
      // 优化参数
      addAutomaticKeepAlives: false, // 手动控制状态保持
      addRepaintBoundaries: true,   
      
      // 缓存范围,预渲染区域
      cacheExtent: 1000, // 默认250,增大可减少滚动卡顿
      
      // 如果item高度固定,使用itemExtent提高性能
      // itemExtent: 100,
      
      // 或者使用prototypeItem
      // prototypeItem: const ListTile(title: Text('原型')),
      
      itemBuilder: (context, index) {
        return _buildOptimizedItem(items[index]);
      },
    );
  }
  
  // 2. 优化列表项构建
  static Widget _buildOptimizedItem(Item item) {
    return RepaintBoundary(
      child: KeepAlive(
        keepAlive: _shouldKeepAlive(item),
        child: const OptimizedListItem(item: item),
      ),
    );
  }
  
  static bool _shouldKeepAlive(Item item) {
    return item.isImportant;
  }
  
  // 3. 使用CustomScrollView和Slivers
  static Widget buildCustomScrollView() {
    return CustomScrollView(
      slivers: [
        // 固定AppBar
        const SliverAppBar(pinned: true, expandedHeight: 200),
        
        // 固定Header
        const SliverToBoxAdapter(
          child: Padding(
            padding: EdgeInsets.all(16),
            child: Text('商品列表'),
          ),
        ),
        
        // 使用SliverFixedExtentList
        SliverFixedExtentList(
          itemExtent: 100, 
          delegate: SliverChildBuilderDelegate(
            (context, index) => _buildOptimizedItem(_getItem(index)),
            childCount: 1000,
          ),
        ),
      ],
    );
  }
}

优化手段4:优化动画性能

是什么:优化动画的流畅度和性能。

为什么重要

  • 动画流畅度是用户体验的最直观感受,复杂的动画会消耗大量资源,也会导致卡顿

优化策略

  1. 使用AnimatedWidget/AnimatedBuilder
  2. 避免在动画中调用setState
  3. 使用TweenAnimationBuilder
  4. 合理使用物理动画

怎么做

class AnimationOptimization {
  // 1. 使用AnimatedBuilder避免不必要的重建
  static Widget buildOptimizedAnimation() {
    return AnimatedBuilder(
      animation: _animationController,
      builder: (context, child) {
        return Transform.rotate(
          angle: _animationController.value * 2 * pi,
          child: child,
        );
      },
      child: const Icon(Icons.refresh), 
    );
  }
  
  // 2. 使用TweenAnimationBuilder动画
  static Widget buildTweenAnimation() {
    return TweenAnimationBuilder<double>(
      tween: Tween(begin: 0, end: 1),
      duration: Duration(seconds: 1),
      builder: (context, value, child) {
        return Opacity(
          opacity: value,
          child: Transform.scale(scale: value, child: child),
        );
      },
      child: const Text('淡入放大动画'),
    );
  }
  
  // 3. 优化物理动画
  static Widget buildPhysicsAnimation() {
    return Draggable(
      feedback: Material(
        child: Container(
          width: 100,
          height: 100,
          color: Colors.blue.withOpacity(0.5),
        ),
      ),
      childWhenDragging: Container(),
      child: Container(
        width: 100,
        height: 100,
        color: Colors.blue,
      ),
      feedbackOffset: Offset.zero,
    );
  }
}

五、性能监控

工具链

graph LR
    A[性能监控工具链] --> B[开发阶段]
    A --> C[测试阶段]
    A --> D[生产阶段]
    
    B --> E[Flutter DevTools]
    B --> F[Dart Observatory]
    B --> G[性能覆盖层]
    
    C --> H[自动化测试]
    C --> I[性能测试]
    C --> J[内存泄漏测试]
    
    D --> K[APM集成]
    D --> L[崩溃监控]
    D --> M[用户行为分析]

工具使用

// 1. 性能覆盖层
void enablePerformanceOverlay() {
  runApp(
    MaterialApp(
      home: const MyApp(),
      showPerformanceOverlay: true, 
      
      // 其他调试选项
      checkerboardRasterCacheImages: true,
      checkerboardOffscreenLayers: true,
      showSemanticsDebugger: true,
    ),
  );
}

// 2. 自定义性能监控
class PerformanceMonitor extends StatefulWidget {
  final Widget child;
  
  const PerformanceMonitor({Key? key, required this.child}) : super(key: key);
  
  @override
  _PerformanceMonitorState createState() => _PerformanceMonitorState();
}

class _PerformanceMonitorState extends State<PerformanceMonitor> 
    with WidgetsBindingObserver {
  final List<double> _frameTimes = [];
  double _fps = 0;
  int _droppedFrames = 0;
  
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance!.addObserver(this);
  }
  
  @override
  void didChangeMetrics() {
    // 监控帧率
    SchedulerBinding.instance!.addPostFrameCallback((timeStamp) {
      _recordFrameTime(timeStamp);
    });
  }
  
  void _recordFrameTime(Duration timeStamp) {
    final now = timeStamp.inMicroseconds / 1000;
    _frameTimes.add(now);
    
    // 保留最近1秒的数据
    final oneSecondAgo = now - 1000;
    _frameTimes.removeWhere((time) => time < oneSecondAgo);
    
    // 计算FPS
    if (_frameTimes.length >= 2) {
      final fps = (_frameTimes.length - 1) * 1000 / 
                 (_frameTimes.last - _frameTimes.first);
      
      // 计算掉帧
      final frameInterval = _frameTimes.last - 
                           _frameTimes[_frameTimes.length - 2];
      if (frameInterval > 33.34) { 
        _droppedFrames++;
      }
      
      setState(() {
        _fps = fps;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        widget.child,
        if (kDebugMode) // 只在调试模式显示
          Positioned(
            top: 40,
            right: 10,
            child: Container(
              padding: const EdgeInsets.all(8),
              color: _getFPSColor(),
              child: Text(
                'FPS: ${_fps.toStringAsFixed(1)}\n'
                '掉帧: $_droppedFrames',
                style: const TextStyle(color: Colors.white),
              ),
            ),
          ),
      ],
    );
  }
  
  Color _getFPSColor() {
    if (_fps >= 55) return Colors.green;
    if (_fps >= 30) return Colors.orange;
    return Colors.red;
  }
}

六、性能优化优先级

基于影响范围和实施难度等多重因素,建议性能优化的优先级顺序如下:

优先级 优化方向 影响范围 实施难度 推荐工具
P0 修复内存泄漏 整个应用 DevTools, LeakCanary
P0 优化启动时间 首次体验 Flutter Performance
P1 优化列表滚动 核心功能 Performance Overlay
P1 优化包体积 下载率 Analyze Size
P2 优化动画流畅度 用户体验 Flutter Inspector
P2 优化构建性能 开发体验 Dart Analyzer
P3 优化渲染管线 特定场景 Custom RenderObject

总结

至此性能优化的知识点就全部介绍完了,其核心:

  • 需要从架构设计阶段开始考虑
  • 不同的应用场景需要不同的优化策略
  • 数据驱动
  • 所有优化都要服务于用户体验
  • 性能优化是持续的过程

过早优化是万恶之源,但不优化是用户体验的灾难。 优化在于正确的时间,用正确的方法,优化正确的地方。


如果觉得这篇文章有帮助,别忘了一键三连支持一下!有任何问题或建议,欢迎在评论区交流讨论! 转载请注明出处~~~

昨天以前iOS
❌
❌