普通视图

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

老司机 iOS 周报 #335 | 2025-05-19

作者 ChengzhiHuang
2025年5月18日 18:06

ios-weekly
老司机 iOS 周报,只为你呈现有价值的信息。

你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。

新闻

Apple 公布将于今年晚些时候推出的强大辅助功能

苹果在辅助功能的上的设计还是非常先进的,现有的一些辅助功能适配起来相对来说还是比较轻松的,期待苹果更多的辅助功能。

文章

🐎 UIApplication delegate deprecation coming in iOS 19 SDK

@Damien:作者通过关注 WebKit 提交记录发现 iOS 19 SDK 中 UIApplicationDelegate 协议中的部分 API 将被弃用,原因是修复因新 SDK 导致的弃用警告,作者预测“新 SDK ”指的是即将在 6 月 WWDC 发布的 iOS 19,苹果工程师们已经在用 iOS 19 SDK 编译 WebKit,所以出现了这些弃用警告。

🐕 Using Model Context Protocol in iOS apps

@含笑饮砒霜:MCP(Model Context Protocol,模型上下文协议)是一种用于大语言模型(LLM)与外部工具交互的协议。它允许模型在对话过程中调用开发者自定义的函数(称为“工具”),以访问本地或远程的数据和服务,从而增强模型的实际能力。这篇文章介绍了如何在 iOS 应用中使用 Model Context Protocol(MCP)集成大模型能力。作者通过示例展示了如何实现一个 MCP Server(用于获取 Apple Health 中的血压数据)和一个 MCP Client(通过 Anthropic Claude API 实现聊天功能)。整个流程包括定义工具、调用本地服务、解析大模型响应,并在用户请求与工具调用之间建立桥梁,实现智能对话与本地数据交互的结合。

🐕 Xtool: cross-platform Xcode replacement. Build iOS apps on Linux and more!

@Kyle-Ye: 这篇贴子介绍了作者的开发名为 Xtool 的跨平台 Xcode 替代工具。Xtool 可以在 Linux 和 Windows 上构建和部署 iOS 应用程序 , 同时也可以在 macOS 上替代 Xcode。它支持使用 SwiftPM 构建 iOS 应用程序、签名和安装应用程序。中间还提到了一些 Xtool 目前还不支持的功能 , 如 Interface Builder、资产目录和 LLDB 调试等,作者表示未来会继续完善这些功能。

🐕 Vibe Xcoding your apps

@EyreFree:本文探讨了 LLMs 驱动的氛围编程(Vibe Coding)在 Xcode 生态的应用,介绍了 Alex、CopilotForXcode 等社区开发的 AI 代码助手,提及 MCP 协议对上下文交互的作用。同时指出了 Xcode 的扩展性局限,期待苹果开放底层接口(如借鉴 VSCode 模式)优化集成,强调了上下文、运行时数据和文档对 AI 辅助的重要性。作者认为氛围编程将推动苹果革新开发者工具,建议感兴趣的朋友持续关注社区在 Xcode 与 AI 整合方面的探索。

🐎 A flowing WebGL gradient, deconstructed

@莲叔: 一篇非常棒的科普文章,通过由浅入深的例子,娓娓道来的揭开了复杂动效的神秘面纱。本质上,绝大多数动画都可以解构为 时间 + 公式 + 图形 api。本篇文章从渐变开始、过度到波浪、贝塞尔曲线等复杂动效,都以思路,公式到代码予以实现,一直到最后实现了一个非常炫酷的动态模糊效果,标题中虽然有 WebGL,但绝大多数代码其实可以非常方便的移植到其他图形 api,如 OpenGL 和 Metal 等等。对动画和图形学有兴趣的同学千万不能错过。

🐕 Unlocking the Real Power of Swift 6's Typed Throws with Error Chains

@阿权:本文围绕 Swift 6 的类型化抛出(Typed Throws)特性展开,重点解决错误处理中的「嵌套地狱」问题,核心内容如下:

  1. 类型化抛出的优势与挑战
    • 优势:编译时错误检查、类型安全、自文档化 API、IDE 支持。
    • 挑战:抛出类型只能指定一个,不支持嵌套。多层架构中错误需手动转换,导致代码冗余、类型激增和上下文丢失。
  2. Catching 协议与 ErrorKit 解决方案
    • 引入 ErrorKit,通过 Catching 协议为错误类型添加 caught(Error) case,统一包装子层错误。
    • 关键函数:
      • catch 函数:自动将闭包中抛出的错误包装为当前层错误类型,避免手动转换。
      • errorChainDescription 函数:递归构建层级化错误链,保留原始错误上下文,提升调试效率。
  3. 总结
    • 类型化抛出结合 Catching 协议,在保证类型安全的同时简化错误处理,ErrorKit 提供的工具链进一步增强了调试能力。
    • 适用于复杂多层架构,尤其适合需要清晰错误追踪和高效调试的场景。

🐕 Debug crashes in iOS using MetricKit

@Barney:文章介绍如何通过苹果 MetricKit 实现系统级崩溃诊断,捕获内存 / 系统信号等传统工具遗漏的崩溃。涵盖框架配置、调用栈分析和 iOS 版本差异化报告机制(13-14 每日汇总,15+ 即时推送),提供崩溃模拟方案,并展示与 Zoho Apptics 整合实现符号解析与数据可视化。

🐕 Fitting the Lapse experience into 15 MegaBytes

@DylanYang:本文讲述了作者在开发 App Clip 时如何将包体压缩到 15 MB 的方式。作者通过重构依赖关系,减少核心功能的依赖库大小,减少、压缩内置的图片、字体等文件,分割核心模块,较少不必要的代码,裁剪字体包,编译优化等各种手段来极致的压缩包体。虽然这是 App Clip,但是对 App 本体的包体有诉求的开发者也有不错的参考价值。

代码

ButtonKit

@Smallfly:ButtonKit 的设计背景源于 SwiftUI 对异步任务交互的不足,旨在简化开发者在处理复杂用户交互时的工作量。其主要作用是提供一个支持异步操作、进度展示和自定义样式的按钮组件,特别适合需要处理网络请求、文件操作或其他耗时任务的 SwiftUI 应用。开发者可以通过这个库减少样板代码,提升代码可读性和用户体验。

内推

重新开始更新「iOS 靠谱内推专题」,整理了最近明确在招人的岗位,供大家参考

具体信息请移步:https://www.yuque.com/iosalliance/article/bhutav 进行查看(如有招聘需求请联系 iTDriverr)

关注我们

我们是「老司机技术周报」,一个持续追求精品 iOS 内容的技术公众号,欢迎关注。

关注有礼,关注【老司机技术周报】,回复「2024」,领取 2024 及往年内参

同时也支持了 RSS 订阅:https://github.com/SwiftOldDriver/iOS-Weekly/releases.atom

说明

🚧 表示需某工具,🌟 表示编辑推荐

预计阅读时间:🐎 很快就能读完(1 - 10 mins);🐕 中等 (10 - 20 mins);🐢 慢(20+ mins)

iOS 如何找到那个方法?消息机制底层探秘

作者 布多
2025年5月18日 15:43

前言

消息机制在 iOS 开发中扮演着至关重要的角色,它为开发者提供了强大的动态性和灵活性,使得代码在运行时能够根据需要进行调整和 扩展。

本文将深入探讨 iOS 消息机制的底层实现原理,从方法调用到消息转发,揭示 Runtime 如何在运行时动态查找和执行方法。通过源码分析,帮助开发者更好地理解和运用这一核心机制。

阅读本文需要你具备以下基础知识:

  • 了解消息发送和 objc_msgSend 的关系;
  • 熟悉 ObjC 的类和对象的底层结构;
  • 熟悉类的方法列表结构;

如果你对以上知识点还不够熟悉,建议先阅读相关文章打好基础。

本文将以对象方法举例,类方法的调用流程逻辑和对象方法基本一致。

消息查找过程

在 ObjC 中,方法调用本质上是一个消息发送的过程。当我们写下 [object method] 这样的代码时,编译器会在编译期将其转换为 objc_msgSend(object, @selector(method)) 的形式。这个转换过程是 ObjC 消息机制的基础,它使得我们能够在运行时动态地查找和执行方法。

要深入理解消息发送的底层实现,我们需要从 Runtime 源码入手。在 Runtime 项目中,我们可以找到 objc_msgSend 的具体实现。下面让我们来看看这个函数的核心实现:

我在 这里 维护了一个可以直接运行调试的 Runtime 项目,方便大家直接调试源码。

MSG_ENTRY _objc_msgSend
cmpp0, #0
  // 检查接收者是否为 nil,如果是的话就执行 LNilOrTagged,其内部会返回 nil。
b.leLNilOrTagged

ldrp14, [x0]
  // 获取对象的 isa 指针。
GetClassFromIsa_p16 p14, 1, x0

LGetIsaDone:
  // 查找方法缓存,如果找到了就调用,如果没找到就调用 __objc_msgSend_uncached。
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

END_ENTRY _objc_msgSend

以上是 objc_msgSend 的汇编实现,我对其进行了精简,并添加了注释。从源码中我们可以总结出消息查找的基本流程:

  1. 首先对消息接收者进行 nil 检查,如果接收者为 nil,则调用 LNilOrTagged 函数直接返回 nil,避免后续无意义的查找;
  2. 通过对象的 isa 指针获取其类对象,这是查找方法实现的第一步,因为方法实现都存储在类对象中;
  3. 在类对象的方法缓存列表中查找目标方法,如果命中缓存则直接调用方法实现,否则调用 __objc_msgSend_uncached 进入慢速查找。

在慢速查找流程中,系统会调用 lookUpImpOrForward 函数进行更深入的方法查找(由于篇幅原因,我没有展开所有代码的调用细节,感兴趣的同学可以自行阅读源码)。以下是精简后的 lookUpImpOrForward 源码实现:

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
    
    if (!cls->isInitialized()) {
        behavior |= LOOKUP_NOCACHE;
    }
    
    checkIsKnownClass(cls);
    
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    curClass = cls;
    
    if (!cls || !cls->ISA()) {
        imp = _objc_returnNil;
        goto done;
    }
    
    for (unsigned attempts = unreasonableClassCount();;) {
        // 从类的方法列表中查找方法实现。
        method_t *meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            // 找到了方法实现并调用。
            imp = meth->imp(false);
            goto done;
        }
        
        // 获取 curClass 的父类,如果父类为 nil 的话,就跳出循环。
        if ((curClass = curClass->getSuperclass()) == nil) {
            imp = forward_imp;
            break;
        }
        
        // 检查继承链是否存在循环情况。
        if (--attempts == 0) {
            _objc_fatal("Memory corruption in class list.");
        }
        
        // 从父类的方法缓存列表中查找方法实现。
        imp = cache_getImp(curClass, sel);
        
        if (imp == forward_imp) {
            break;
        }
        
        if (imp) {
            // 从父类的方法缓存列表中找到了方法实现
            goto done;
        }
        
        /* 如果从方法缓存列表中未找到方法实现,
         则回到循环起点,从父类的方法列表中查找方法实现。*/
    }
    
    // 来到这里就说明没有找到方法实现。
    
    // 判断是否执行过方法解析?如果未执行的话就执行方法解析。
    if (behavior & LOOKUP_RESOLVER) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
    
done:
    if ((behavior & LOOKUP_NOCACHE) == 0) {
        // 将方法加入 cls(当前类) 的方法缓存列表
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
    
    if ((behavior & LOOKUP_NIL) && imp == forward_imp) {
        return nil;
    }
    
    return imp;
}

通过分析 lookUpImpOrForward 函数的源码实现,我们可以看到 Runtime 在查找方法实现时采用了多层次的查找策略,主要包括以下几个步骤:

  1. 方法查找流程:

    • 首先在类的方法缓存中快速查找,这是最高效的查找方式
    • 缓存未命中时,会遍历类的方法列表进行查找
    • 如果当前类中未找到,则沿着继承链向上查找,对每个父类重复上述两步操作
    • 找到方法实现后,会通过 log_and_fill_cache 将其缓存到当前类中,以提升后续调用性能
    • 如果遍历完整个继承链仍未找到,则进入方法动态解析阶段
  2. 方法动态解析: 当常规查找流程无法找到方法实现时,Runtime 会尝试通过动态方法解析机制来处理,这部分内容我们将在下一节详细讨论。

补充说明:

  • getMethodNoSuper_nolock 函数负责在方法列表中查找目标方法,其内部采用了二分查找(已排序)和线性查找(未排序)两种策略,以平衡查找效率和排序开销。
  • cache_getImp 函数则通过散列表实现方法缓存的快速查找,使用 SEL 作为键值,通过哈希算法将方法选择器映射到对应的实现地址。

这些优化策略共同构成了 ObjC 高效的消息查找机制,既保证了方法调用的性能,又维持了运行时的灵活性。

消息动态解析

当常规方法查找流程(包括缓存查找、方法列表查找和父类查找)都无法找到目标方法的实现时,Runtime 会进入方法动态解析阶段,调用 resolveMethod_locked 函数尝试动态添加方法实现。这个函数的核心实现如下:

static IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) {
    // 执行对象方法解析逻辑。
    if (!cls->isMetaClass()) {
        // 这行代码等同于:[cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } else {// 执行类方法解析逻辑。
        // 这行代码等同于:[cls resolveClassMethod:sel]
        resolveClassMethod(inst, sel, cls);
    }

    // 函数内部最终会调用 lookUpImpOrForward 函数。
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

通过前面的源码分析,我们已经完整地了解了 Runtime 的消息查找和动态解析机制。从 objc_msgSend 的快速查找,到 lookUpImpOrForward 的慢速查找,再到 resolveMethod_locked 的动态方法解析,我们看到了 Runtime 是如何一步步尝试找到并执行目标方法的。如果这些步骤都无法找到方法实现,Runtime 就会进入最后一道防线:消息转发机制。

接下来,让我们深入探讨消息转发机制的具体实现。

消息转发机制

消息转发机制是 ObjC Runtime 中处理未实现方法的最后一道防线,它包含快速转发和完整转发两个阶段。快速转发允许对象将消息转发给其他对象处理,而完整转发则提供了更灵活的消息处理方式。虽然消息转发的核心实现是由汇编代码完成的,但通过分析 Runtime 源码和相关资料,我们可以将其核心逻辑整理为以下伪代码实现:

int __forwarding__(void *frameStackPointer, int isStret) {
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    Class receiverClass = object_getClass(receiver);
    
    // 调用 forwardingTargetForSelector: 方法。
    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
        id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        if (forwardingTarget && forwardingTarget != receiver) {
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }
    
    // 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation。
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
        NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        if (methodSignature) {
            if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
                NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
                [receiver forwardInvocation:invocation];
                
                void *returnValue = NULL;
                [invocation getReturnValue:&returnValue];
                return returnValue;
            }
        }
    }
    
    // 如果以上两个方法都没有处理消息,则调用 doesNotRecognizeSelector 方法。
    if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
        [receiver doesNotRecognizeSelector:sel];
    }
    
    kill(getpid(), 9);
}

在消息转发机制中,有一个容易被忽视的重要细节:类方法其实也支持消息转发。虽然 Xcode 在代码提示时只会显示 - (id)forwardingTargetForSelector: 等实例方法的实现,但实际上 + (id)forwardingTargetForSelector: 等类方法同样可以用于消息转发。

实现类方法的消息转发非常简单:

  1. 将转发方法声明为类方法(使用 + 号);
  2. 在转发方法中使用类对象而不是实例对象。

总结

本文深入分析了 Runtime 中消息发送的核心实现,包括 objc_msgSend 的汇编实现以及 loopUpImpOrForward 函数的工作原理。但要完全理解 ObjC 的消息机制,还需要了解以下几个关键点:

  1. 消息查找过程:类是如何从方法列表中定位目标方法的?getMethodNoSuper_nolock 函数在其中扮演什么角色?
  2. 方法缓存机制:类是如何通过 cache_getImp 函数从缓存中快速获取方法实现的?
  3. 对象内存结构:包括 isa 指针、类指针、属性列表、方法列表等底层数据结构。

这些知识点涉及 ObjC 对象的内存布局,建议读者结合 Runtime 源码深入学习。

另外,在实际开发中,我们经常使用 respondsToSelector: 来检查对象是否实现了某个方法。但这个方法存在一个局限性:它无法检测到通过消息转发机制实现的方法。为此,我实现了一个支持消息转发检测的 respondsToSelector 方法,代码如下:

@interface NSObject (WXL)
- (BOOL)wxl_respondsToSelectorIncludingForwarding:(SEL)aSel;
@end

@implementation NSObject (WXL)
- (BOOL)wxl_respondsToSelectorIncludingForwarding:(SEL)aSel {
    if ([self respondsToSelector:aSel]) {
        return YES;
    }
    
    // 检查消息转发是否能处理消息。
    if ([self respondsToSelector:@selector(forwardingTargetForSelector:)]) {
        id forwardingTarget = [self forwardingTargetForSelector:aSel];
        if (forwardingTarget && forwardingTarget != self) {
            return YES;
        }
    }
    
    if ([self respondsToSelector:@selector(methodSignatureForSelector:)]) {
        NSMethodSignature *signature = [self methodSignatureForSelector:aSel];
        if (signature && [self respondsToSelector:@selector(forwardInvocation:)]) {
            return YES;
        }
    }
    
    return NO;
}
@end

React Hooks 的优势和使用场景

作者 Riesenzahn
2025年5月18日 07:37

React Hooks 的优势和使用场景

核心优势

  1. 简化组件逻辑
    • 告别 class 组件的繁琐生命周期
    • 将相关逻辑聚合到单个 Hook 中
    • 消除 this 绑定问题
// 类组件
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return <button onClick={this.handleClick}>点击 {this.state.count} 次</button>;
  }
}

// 函数组件 + Hooks
function Example() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>点击 {count} 次</button>;
}
  1. 逻辑复用
    • 自定义 Hook 实现跨组件逻辑复用
    • 替代高阶组件和 render props 模式
// 自定义 Hook
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => setSize({
      width: window.innerWidth,
      height: window.innerHeight
    });
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

// 在组件中使用
function MyComponent() {
  const { width } = useWindowSize();
  return <div>窗口宽度: {width}px</div>;
}
  1. 性能优化
    • 细粒度的状态更新控制
    • 避免不必要的渲染
function ExpensiveComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // 只有 count 变化时才重新计算
  const computedValue = useMemo(() => {
    return expensiveCalculation(count);
  }, [count]);

  // 只有在组件挂载时执行
  useEffect(() => {
    fetchInitialData();
  }, []);

  return (
    <div>
      <p>{computedValue}</p>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

主要使用场景

  1. 状态管理
    • useState: 基础状态管理
    • useReducer: 复杂状态逻辑
function TodoApp() {
  const [todos, dispatch] = useReducer(todoReducer, []);

  function handleAddTodo(text) {
    dispatch({ type: 'ADD_TODO', text });
  }

  // ...
}
  1. 副作用处理
    • useEffect: 数据获取、订阅、手动 DOM 操作
    • useLayoutEffect: DOM 变更后同步执行
function DataFetcher({ id }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    let ignore = false;
    
    async function fetchData() {
      const result = await fetch(`/api/data/${id}`);
      if (!ignore) {
        setData(await result.json());
      }
    }

    fetchData();
    
    return () => { ignore = true; };
  }, [id]); // id 变化时重新获取

  // ...
}
  1. 性能优化
    • useMemo: 缓存计算结果
    • useCallback: 缓存函数引用
function Parent() {
  const [count, setCount] = useState(0);
  
  // 避免子组件不必要的重渲染
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <>
      <Child onClick={handleClick} />
      <div>计数: {count}</div>
    </>
  );
}
  1. 访问 DOM 元素
    • useRef: 获取 DOM 引用或保存可变值
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  
  const onButtonClick = () => {
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>聚焦输入框</button>
    </>
  );
}
  1. 上下文访问
    • useContext: 简化上下文使用
const ThemeContext = React.createContext('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button style={{ background: theme === 'dark' ? '#333' : '#eee' }}>按钮</button>;
}

最佳实践

  1. Hook 使用规则

    • 只在 React 函数组件顶层调用 Hook
    • 不要在循环、条件或嵌套函数中调用 Hook
    • 自定义 Hook 必须以 "use" 开头
  2. 组织代码

    • 将复杂逻辑拆分为多个小 Hook
    • 相关逻辑组织在一起
  3. 性能考量

    • 合理使用依赖数组
    • 避免不必要的 effect 执行
    • 大型列表考虑虚拟化
  4. 测试策略

    • 使用 @testing-library/react-hooks 测试自定义 Hook
    • 模拟依赖项进行隔离测试

常见问题解决方案

  1. 无限循环

    // 错误示例
    useEffect(() => {
      setCount(count + 1); // 会导致无限循环
    }, [count]);
    
    // 正确方式
    useEffect(() => {
      setCount(c => c + 1); // 使用函数式更新
    }, []); // 空依赖数组
    
  2. 过时闭包

    function Counter() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        const id = setInterval(() => {
          setCount(count + 1); // 总是使用初始值
        }, 1000);
        return () => clearInterval(id);
      }, []); // 缺少 count 依赖
    
      // 正确方式
      useEffect(() => {
        const id = setInterval(() => {
          setCount(c => c + 1); // 使用函数式更新
        }, 1000);
        return () => clearInterval(id);
      }, []);
    }
    
  3. 条件执行

    // 错误示例
    if (condition) {
      useEffect(() => { ... }); // 违反 Hook 规则
    }
    
    // 正确方式
    useEffect(() => {
      if (condition) {
        // 在 effect 内部进行条件判断
      }
    }, [condition]);
    

React Hooks 通过简化组件逻辑、提高代码复用性和优化性能,已经成为现代 React 开发的标准方式。合理运用各种 Hook 可以显著提升开发效率和代码可维护性。

如何提高前端应用的性能?

作者 Riesenzahn
2025年5月18日 07:37

如何提高前端应用的性能

1. 代码优化

1.1 减少不必要的DOM操作

// 差: 频繁操作DOM
for(let i=0; i<100; i++) {
  document.getElementById('list').innerHTML += `<li>${i}</li>`;
}

// 好: 使用文档片段批量操作
const fragment = document.createDocumentFragment();
for(let i=0; i<100; i++) {
  const li = document.createElement('li');
  li.textContent = i;
  fragment.appendChild(li);
}
document.getElementById('list').appendChild(fragment);

1.2 使用事件委托

// 差: 为每个元素绑定事件
document.querySelectorAll('.btn').forEach(btn => {
  btn.addEventListener('click', handleClick);
});

// 好: 使用事件委托
document.getElementById('container').addEventListener('click', (e) => {
  if(e.target.classList.contains('btn')) {
    handleClick(e);
  }
});

2. 资源优化

2.1 图片优化

  • 使用WebP格式替代JPEG/PNG
  • 实现懒加载(Lazy Loading)
  • 使用响应式图片(srcset)
  • 压缩图片(TinyPNG等工具)

2.2 代码分割

// 动态导入实现代码分割
const module = await import('./module.js');

2.3 缓存策略

  • 设置合理的Cache-Control头
  • 使用Service Worker实现离线缓存
  • 资源文件使用内容哈希命名

3. 网络优化

3.1 使用CDN

  • 将静态资源部署到CDN
  • 选择离用户最近的CDN节点

3.2 启用HTTP/2

  • 多路复用减少连接数
  • 头部压缩减少传输量
  • 服务器推送预加载资源

3.3 预加载关键资源

<link rel="preload" href="critical.css" as="style">
<link rel="prefetch" href="next-page.js" as="script">

4. 渲染优化

4.1 减少重排和重绘

// 获取布局信息前进行批量修改
const width = element.offsetWidth; // 触发重排
element.style.width = width + 10 + 'px'; 

// 使用requestAnimationFrame优化动画
function animate() {
  // 动画逻辑
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

4.2 使用CSS硬件加速

.transform-element {
  transform: translateZ(0);
  will-change: transform;
}

5. 监控与分析

5.1 性能指标

  • First Contentful Paint (FCP)
  • Largest Contentful Paint (LCP)
  • Cumulative Layout Shift (CLS)
  • Time to Interactive (TTI)

5.2 性能工具

  • Lighthouse
  • WebPageTest
  • Chrome DevTools Performance面板
  • 真实用户监控(RUM)

6. 框架优化

6.1 React优化

// 使用React.memo避免不必要渲染
const MemoComponent = React.memo(MyComponent);

// 使用useCallback/useMemo缓存计算结果
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

6.2 Vue优化

// 使用v-once处理静态内容
<div v-once>{{ staticContent }}</div>

// 合理使用计算属性
computed: {
  filteredList() {
    return this.list.filter(item => item.active);
  }
}

7. 构建优化

7.1 Tree Shaking

// package.json配置sideEffects
{
  "sideEffects": ["*.css", "*.scss"]
}

7.2 压缩代码

  • 使用Terser压缩JavaScript
  • 使用CSSNano压缩CSS
  • 使用HTMLMinifier压缩HTML

8. 移动端优化

8.1 减少首屏资源

  • 关键CSS内联
  • 非关键JS延迟加载
  • 使用骨架屏提升感知性能

8.2 优化触摸事件

/* 禁用触摸高亮 */
button {
  -webkit-tap-highlight-color: transparent;
}

/* 优化滚动性能 */
.scroll-container {
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;
}

最佳实践总结

  1. 测量优先:使用性能工具找出瓶颈
  2. 渐进增强:确保核心功能在低端设备可用
  3. 按需加载:只加载当前需要的资源
  4. 持续监控:建立性能基准和报警机制
  5. 团队协作:将性能指标纳入开发流程

通过综合应用以上技术,可以显著提升前端应用的加载速度、交互流畅度和整体用户体验。性能优化是一个持续的过程,需要定期评估和调整策略。

Mac 显示器相关

作者 Augustine
2025年5月17日 21:49

使用Mac系电脑,并且是外接显示器使用时。 Macbook retina屏幕是5K的,通过HIDPI渲染成 2K 布局。 用MacMini 外接27寸4K屏幕,通过HIDPI 渲染成 1K 布局。

外接显示器如何调出更多【分辨率】和【刷新率】

打开【系统设置】=>【显示器】
可以手动设置屏幕的【分辨率】。
最右侧的选项(更多空间)就是不做 HIDPI 的显示效果。

此时,只提供了几个分辨率选项供我们选择。 其实还有很多选项被隐藏起来了。
按住 alt键盘/option 键 点击缩放 可以展示这台显示器所能支持的所有分辨率选项(不需要第三方软件,这是Mac自己的技巧)。
下面还有个选项框【显示低分辨率模式】
低分辨率就是不做HIDPI

选 2560 * 1440 =》显示器用4K的性能渲染出2K的画面,也就是开了HIDPI。 选 2560 * 1440(低分辨率)=》显示器真的只使用这个分辨率。

查看显示器【物理分辨率】以及当前采用的【逻辑分辨率】

打开【System Report】=> 【Graphics/Displays】

macMini-displays.png 红色是显示器的【物理分辨率】。
蓝色则是常说的【逻辑分辨率】。
即,此时 Mac 将4K的屏幕通过 HIDPI 把UI视图渲染为 1K 屏幕的显示布局。
视觉上看起来就是,拥有4K级清晰度的超高清1K屏幕


Swift 中可选链(Optional Chaining)存在的意义是什么?

作者 90后晨仔
2025年5月18日 11:42

在 Swift 中,可选链(Optional Chaining) 的存在意义在于提供一种安全、简洁且优雅的方式,用于访问可能为 nil 的属性、方法或下标,同时避免运行时错误。它是 Swift 语言对空值安全(Null Safety)的核心设计之一,以下是其核心意义和优势的详细总结:


1. 安全访问空值(避免崩溃)

  • 核心意义
    在 Swift 中,如果直接访问一个可能为 nil 的属性或方法(如 john.residence!.numberOfRooms),若 residencenil,会触发运行时错误(fatal error: unexpectedly found nil)。
    可选链通过 ? 操作符替代 ! 强制解包,当链中的某个环节为 nil 时,整个表达式会安全地返回 nil,而不是崩溃。

  • 示例对比

    // 强制解包(危险):若 john.residence 为 nil,会崩溃
    let roomCount = john.residence!.numberOfRooms
    
    // 可选链(安全):若 john.residence 为 nil,roomCount 为 nil
    let roomCount = john.residence?.numberOfRooms
    

2. 简化嵌套访问的复杂性

  • 核心意义
    在复杂的嵌套对象链中(如 person.address?.building?.name),可选链允许开发者用单行代码安全地访问多层属性,而无需多次嵌套 if letguard let

  • 示例

    class Person {
        var address: Address?
    }
    
    class Address {
        var building: Building?
    }
    
    class Building {
        var name: String?
    }
    
    let person = Person()
    if let name = person.address?.building?.name {
        print("Building name: $name)")
    } else {
        print("无法获取建筑名称")
    }
    

    如果没有可选链,需要多层嵌套解包,代码会变得冗长且难以阅读:

    if let address = person.address {
        if let building = address.building {
            if let name = building.name {
                print("Building name: $name)")
            } else {
                print("无法获取建筑名称")
            }
        } else {
            print("无法获取建筑名称")
        }
    } else {
        print("无法获取建筑名称")
    }
    

3. 统一处理可选值的返回类型

  • 核心意义
    无论目标属性是否为可选类型,可选链的返回值始终是可选类型(例如,原本返回 Int 的属性会变为 Int?)。这种设计使得开发者可以统一处理结果(如使用 if letguard let 解包),无需额外判断原始属性是否为可选。

  • 示例

    class Residence {
        var numberOfRooms = 1 // 非可选属性
    }
    
    class Person {
        var residence: Residence?
    }
    
    let john = Person()
    let roomCount: Int? = john.residence?.numberOfRooms // 返回 Int?
    

4. 替代 Objective-C 中的 nil 消息传递

  • 核心意义
    在 Objective-C 中,向 nil 发送消息是合法的,但不会有任何效果(返回 nil0)。Swift 的可选链扩展了这一特性,使其适用于所有类型(包括值类型),并明确通过返回 nil 表示失败。

  • 示例

    // Swift 可选链
    let result = someOptionalValue?.doSomething() // 若 someOptionalValue 为 nil,result 为 nil
    
    // Objective-C 中类似行为
    [someOptionalValue doSomething]; // 若 someOptionalValue 为 nil,不会崩溃但无效果
    

5. 支持方法调用和下标访问

  • 核心意义
    可选链不仅适用于属性访问,还支持方法调用下标访问,进一步扩展了其适用范围。

  • 示例

    class Toy {
        func flying() { print("飞盘在飞---") }
    }
    
    class Dog {
        var toy: Toy?
    }
    
    class Person {
        var dog: Dog?
    }
    
    let person = Person()
    person.dog?.toy?.flying() // 安全调用方法
    
    let array: [Int]? = [1, 2, 3]
    let value = array?[1] // 安全访问下标
    

6. 提高代码可读性和可维护性

  • 核心意义
    通过减少冗余的 if letguard let 嵌套,可选链使代码更简洁、直观,开发者可以快速定位潜在的 nil 风险点。

  • 对比示例

    // 使用可选链
    let name = user?.profile?.address?.city
    
    // 不使用可选链
    if let user = user, let profile = user.profile, let address = profile.address {
        let name = address.city
    }
    

7. 与 if let/guard let 的协同使用

  • 核心意义
    可选链通常与 if letguard let 结合使用,实现对结果的进一步解包和处理。

  • 示例

    if let roomCount = john.residence?.numberOfRooms {
        print("房间数: $roomCount)")
    } else {
        print("无法获取房间数")
    }
    

总结

Swift 的可选链通过以下方式显著提升了开发体验:

  1. 安全性:避免因 nil 导致的崩溃。
  2. 简洁性:减少嵌套代码,提升可读性。
  3. 灵活性:支持属性、方法、下标的统一访问。
  4. 一致性:所有类型均适用,无需区分类或值类型。

核心意义
可选链是 Swift 对空值安全的优雅解决方案,它让开发者在面对复杂对象链时,既能保持代码的简洁性,又能确保程序的健壮性。

昨天 — 2025年5月17日iOS

iOS Cursor 使用心得

作者 CodePower
2025年5月17日 15:18

最近在使用 Cursor 过程中,业务需求基本上 80% 代码由 AI 开发。

本文把过程中的一些心得记录分享一下。

基础配置

先介绍一下基本的配置。

个人使用的 Rules:

使用中文回复
每次生成代码前,先概述你的方案,等我确认后再生成代码
仅做最小改动,不要动任何我别的代码逻辑和注释,不调整注释的标点符号
不添加 log,只保留最关键的注释,注释使用中文

由于不同地方使用的风格不同,具体代码风格在 prompt 再单独指定。

自己记录在笔记中,方便快速粘贴,如:

public class SomeView: UIView {
    // 定义 UI 子控件,如
    let textLabel = UILabel()
    let imageView = UIImageView()

    override init(frame: CGRect) {
        super.init(frame: frame)

        // 对 UI 子控件进行布局
        textLabel.sk_added(to: self).then({ cur in
            // 此处设置 UI 展示属性
            cur.font = .systemFont(ofSize: 17, weight: .regular)
        }).sk_layout { make in
            // 此处设置布局代码,使用 SnapKit 语法
            make.left.equalTo(12)
            make.height.equalTo(24)
            make.top.equalTo(16)
            make.bottom.equalTo(-14)
        }
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Vibe Coding

图片展示了整个 AI 辅助编程的个人流程。

image.png

下面针对这个流程逐项再展开说明下。

0、前提掌握

上图是市面上常用的 AI 编程工具,按使用者的可控制程度排列。

虽然目前市面上已经有很多 0 基础 AI 编程工具,但是对于有准确性要求的商业项目开发,或者希望代码能够由人工交替维护,还是需要把控整体的架构。

所以哪部分知识是 Vibe Coding 下比较重要的部分?个人认为有以下几点:

  1. 某个领域的核心概念:拿 iOS 举例的话可能有 Swift 语法、UIKit 的使用方法、如何调试程序

  2. 最佳实践的把握(代码品味):SOP、模板、规范

而在 AI 的帮助下,我们也可以更快速地入门某个专门领域。

而代码品味则需要一定时间的工作沉淀,但在不同的领域里是可以共享的。

1、领域驱动,数据设计先行

image.png

在开发前,先确认好数据,包含以下维度

  • UI 需要用到的 ViewModel
  • 存储或与后台交互的 Model

当然,数据的设计也可以让 AI 进行,我们做一个 review 和调整。

这样做的好处是,后续 AI 将围绕固定的数据处理来进行编码,相当于提供了上下文和约束,会更容易得到想要的结果。

而对于已有的项目,则可以让 AI 先生成数据处理流程的文档,也方便自己快速了解逻辑。

这里的思路就是将步骤拆解成 SOP,每一步进行人工的确认,确认后作为下一步的前提

下面在逻辑开发过程中,也会遵照这一思路进行。

2、逻辑开发,Feature 维度

在 rules 中,有这样一句:“每次生成代码前,先概述你的方案,等我确认后再生成代码”。

这样做有两方面的好处:

  1. 对话中,AI 会将前文都带上,先输出方案作为后续代码生成的上下文,有利于代码准确
  2. 可以检查方案的可行性,如果不合适可以先进行调整

另外一点是以 Feature 的维度进行开发,特别是对于复杂的 controller。

具体含义是:实现某个功能的时候,先将相关的功能拆分出来,放入一个单独的 Feature 类,后续的开发限定在这个 Feature 类内进行。

这样做的好处主要是:

  1. 本身也是一种防止 Massive Controller 的实践,有利于代码的高内聚、低耦合
  2. 防止 AI 对其它无关代码进行修改(虽然 rules 说明了不要这样做,但 AI 还是会忍不住,特别是对于把注释的中文引号改成英文引号,claude 模型似乎有一种执念 😂)

3、Feature 完成,实时重构

在功能开发过程中,由于是通过多次和多轮对话进行的,AI 生成的代码难免会存在冗余的毛病。

在功能完成后,我们可以要求 AI 对 Feature 进行一次重构,让其优化代码风格、抽离重复的部分。

实际效果来看,AI 能出色地完成该任务。

这也体现了我们按 Feature 维度进行开发的好处,可以有效限制重构的范围。

Git Tips

勤提交,完成一个功能点后提交一下,防止 AI 后续改乱

但对于功能点过程中的多轮生成,每次都提交比较繁琐,提交也会过多而且不完整

个人比较习惯通过暂存区来阶段性提交,将已接受的和新生成的分离开

image.png

完成一轮之后,把接受的改动放入暂存区,此时 Cursor 也会自动算作 accept

再继续进行下一轮生成时,新的改动会在未暂存区,单独看这部分即可

参考

氛围编程 Vibe coding - 维基百科,自由的百科全书

AI 工具对比 I ranked every AI Coder: Bolt vs. Cursor vs. Replit vs Lovable

领域驱动 Domain Modeling: What you need to know before coding | Thoughtworks

Cursor 技巧 CursorHub教程

Cursor iOS 开发(供参考,个人还是 Cursor 生成、Xcode 运行比较顺手) How to use VSCode/Cursor for iOS development | by Thomas Ricouard | Medium

Podfile主要指令和参数

2025年5月17日 11:59

Podfile 中,主要用于定义 CocoaPods 配置的指令和参数。它是一个用 Ruby 编写的文件,包含了如何将依赖项(Pods)集成到 iOS 或 macOS 项目的详细信息。以下是 Podfile 中常见参数的详细解释:

1. platform

platform :ios, '10.0'
  • 含义:指定项目支持的最小 iOS 版本或其他平台版本。
  • 例子
    • platform :ios, '10.0' 表示项目最低支持 iOS 10.0 版本。
    • platform :osx, '10.13' 表示项目最低支持 macOS 10.13 版本。

2. target

target 'YourAppName' do
  # Pods for YourAppName
end
  • 含义:指定一个目标应用(Target),在该目标中集成相应的依赖。
  • 例子target 'MyApp' 指定了将要包含依赖的目标应用或目标框架。

3. pod

pod 'Alamofire', '~> 5.0'
  • 含义:指定依赖的具体库或框架及其版本。
  • 参数
    • 'Alamofire':库的名称。
    • ~> 5.0:表示 Alamofire 版本范围为 5.x(例如 5.1、5.2 等)。

还有以下配置含义

  • >0.1 表示大于 0.1 的任何版本,这样可以包含 0.2 或者 1.0
  • >=0.1 表示大于等于 0.1 的任何版本
  • <0.1 表示少于 0.1 的任何版本
  • <=0.1 表示少于等于 0.1 的任何版本

4. podspec

pod 'LibraryName', :podspec => 'https://example.com/LibraryName.podspec'
  • 含义:通过指定 .podspec 文件来集成自定义的 Pod 库,而不是从公共的 CocoaPods 仓库中获取。
  • 例子pod 'MyLibrary', :podspec => 'https://example.com/myLibrary.podspec'

5. use_frameworks!

use_frameworks!
  • 含义:启用动态框架(Frameworks)。如果不设置,CocoaPods 默认使用静态库(Static Libraries)。
  • 详细:设置 use_frameworks! 后,所有的 Pod 都会作为动态框架来集成,这对某些 Swift 项目尤为重要,因为 Swift 代码必须使用动态框架才能与其他 Swift 代码互相调用。

6. inherit!

inherit! :search_paths
  • 含义:控制如何继承父级 target 中的设置。常用于继承父目标的配置或路径。
  • 常见选项
    • :search_paths:继承父目标的搜索路径设置。
    • :complete:完全继承父目标的所有设置。

7. post_install

post_install do |installer|
  installer.pods_project.targets.each do |target|
    # 修改构建设置
  end
end
  • 含义:在安装完所有 Pods 后执行的脚本。通常用于对生成的 Xcode 项目进行自定义设置。
  • 例子:你可以在这里设置一些编译选项,或者在安装后做一些额外的配置,如自定义 Info.plist 或设置 Build Settings

8. pre_install

pre_install do |installer|
  # 安装之前做一些事情
end
  • 含义:在开始安装 Pods 之前执行的脚本,通常用于对安装前的环境进行调整。
  • 例子:可以用来检查是否满足某些条件,或者做一些清理工作。

9. target 中的 dependency

target 'App' do
  pod 'Alamofire'
  target 'AppTests' do
    pod 'Quick'
  end
end
  • 含义:在目标中嵌套定义其他子目标或依赖项。例如,如果你的应用有一个测试目标,你可以在 AppTests 中指定它的依赖。

10. configurations

configurations = ['Debug', 'Release']
  • 含义:指定哪些构建配置需要应用此 Podfile 中的配置。

11. source

source 'https://github.com/CocoaPods/Specs.git'
  • 含义:指定 Pod 依赖库的源仓库。默认情况下,CocoaPods 使用官方的 Specs 仓库,但你可以添加或更改为其他源。
  • 例子:如果使用私有的 Pod 库,你可以设置 source 指向你自己的仓库地址。

12. deployment_target

deployment_target = '10.0'
  • 含义:设置每个 Pod 的部署目标版本。它可以覆盖 platform 指定的版本,适用于某些特定库。
  • 例子deployment_target = '10.0' 表示设置该 Pod 的最低支持版本为 iOS 10.0。

13. :git:path

pod 'MyLibrary', :git => 'https://github.com/MyOrg/MyLibrary.git'
  • 含义:指定从 Git 仓库安装 Pod 或从本地路径安装 Pod。
  • 例子
    • :git 用于从 Git 仓库拉取代码。
    • :path 用于指定本地路径中的 Pod。

14. :branch, :tag, :commit

pod 'MyLibrary', :git => 'https://github.com/MyOrg/MyLibrary.git', :branch => 'develop'
  • 含义:当使用 :git 时,可以指定从哪个分支、标签或者提交记录拉取 Pod。
  • 例子
    • :branch => 'develop' 指定拉取 develop 分支。
    • :tag => 'v1.0.0' 指定拉取某个标签。
    • :commit => 'abcdef' 指定拉取某个特定提交。

15. :subspec

pod 'Alamofire/NetworkReachability'
  • 含义:如果 Pod 提供多个子模块,你可以只集成其中的一部分。子模块通常被称为 "subspecs"。
  • 例子pod 'Alamofire/NetworkReachability' 仅引入 Alamofire 中的 NetworkReachability 模块。

CocoaPods常见问题的处理方法,文章链接:

Xcode运行项目时安装cocoapods报错问题修复Couldn't determine repo type for URL:https://cdn.cocoapods.org/:

juejin.cn/post/725297…

CocoaPods常见问题:

nativesupport.dcloud.net.cn/UniMPDocs/F…

Swift中的析构函数deinit

作者 原鸣清
2025年5月17日 07:02

析构函数

在 Swift 中,deinit(析构函数)只适用于 class 类型。因为只有类是引用类型,而不像结构体 struct 或枚举 enum 是值类型。

虽然 Swift 自动管理内存(ARC,Automatic Reference Counting),你不需要经常写 deinit,但在某些关键场景中非常有用。以下是最常见且实用的使用场景:

常用场景一:移除通知监听者

使用 NotificationCenter 添加观察者时,如果不手动移除,会导致内存泄漏或重复响应事件

class MyObserver {

    init() {
        NotificationCenter.default.addObserver(self, selector: #selector(handleEvent), name: .someEvent, object: nil)
    }

    @objc func handleEvent() {
        print("Event received")
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
        print("Observer deinitialized")
    }
}

常用场景二:取消定时器、任务、订阅等异步资源

比如 Timer、DispatchSourceTimer、Combine 的 AnyCancellable、URLSession task 等等。

class TimerHandler {
    var timer: Timer?

    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            print("Tick")
        }
    }

    deinit {
        timer?.invalidate()
        print("Timer invalidated")
    }
}

常用场景三:断开委托(Delegate)或弱引用检查

有时候你需要在 deinit 中把自己从其他类的 delegate 中移除,以避免无用引用。

class Child: SomeDelegate {
    init(parent: Parent) {
        parent.delegate = self
    }

    deinit {
        print("Child released")
        // 这里通常不强制清空 delegate,但有时你想显式处理
    }
}

常用场景四:清理自定义资源或临时文件

当类负责写入临时文件或打开文件句柄等,需要在析构时清理。

class TempFileManager {
    let filePath: String

    init() {
        filePath = NSTemporaryDirectory() + UUID().uuidString
        FileManager.default.createFile(atPath: filePath, contents: nil)
    }

    deinit {
        try? FileManager.default.removeItem(atPath: filePath)
        print("Temporary file deleted")
    }
}

常用场景五:调试或资源释放追踪

打印 deinit 是常见的调试手段,能够帮助判断是否某对象已经被释放(尤其是在处理循环引用、Retain Cycle 的时候)。

deinit {
    print("MyViewController has been released!")
}

总结:deinit 适用的典型场景

场景 用途说明
通知监听移除 防止内存泄漏
定时器/任务取消 释放异步资源
文件或自定义资源清理 避免残留资源
delegate/数据源解绑(可选) 规范化资源管理
Combine/任务订阅取消 清理绑定或订阅
打印调试(检查是否释放) 非常实用的调试手段

在 SwiftUI 或 iOS 架构设计中,有些类如 ViewModel 或自定义对象,还是推荐善用 deinit 来做资源释放和问题定位



构造链

Swift 中 enum、struct 和 class 都可以拥有构造函数(initializer),但三者在构造行为和使用方式上有 明显差异,尤其是在以下几个地方:

• 默认构造器是否自动生成

• 成员变量初始化规则

• 继承与重写支持

• 可变性

• 引用与值语义

这里主要讨论下构造链。构造在 Swift 中,class 是引用类型,支持继承。而子类初始化时需要 保证父类的属性也被正确初始化,因此构造函数需要遵循一定的调用顺序,也就是所谓的“构造链”:

• 子类的 init 必须在某一阶段调用 super.init(...),以确保父类部分正确构造。

• 所有 stored property 初始化完后,才能调用 super.init()。

• Swift 编译器会严格检查构造顺序,避免未初始化状态下访问属性。

class Animal {
    var name: String
    init(name: String) {
        self.name = name
        print("Animal init")
    }
}

class Dog: Animal {
    var breed: String
    init(name: String, breed: String) {
        self.breed = breed              // 1️⃣ 先初始化子类属性
        super.init(name: name)         // 2️⃣ 调用父类构造器
        print("Dog init")
    }
}

// 调用
let dog = Dog(name: "Buddy", breed: "Labrador")

// 输出
# Animal init
# Dog init

对于 class类型:

1. 所有存储属性必须初始化。

2. 子类构造器必须在合适时机调用 super.init(...)。

3. 若父类没有默认构造器,子类必须手动调用父类指定构造器。

4. convenience init 只能调用本类的 designated init,不能直接调用 super.init。

5. 父类的 required init 必须被子类实现。

"struct 和 enum 没有继承, 它们只有构造器,不存在构造链,甚至构造器都可以用默认自动生成的

昨天以前iOS

苹果3.2f倒计时结束后“死亡邮件”是啥样的?

作者 iOS研究院
2025年5月16日 18:39

前言

经历了苹果审核几轮清洗,有不少来询问3.2f破局以及被封号之后的情况,所以单独拿出来简单做个回复。

3.2f终止时间

从收到3.2f邮件到苹果正式封号,大概是30个自然日左右。正常情况下,未做任何解释以及回复一般就是足月嘎了。

如果是积极回复苹果,主动向苹果解释说明情况的大概率会比30天长一些。如果说也是30天到时候嘎了,那么就证明解释的内容完全没有被苹果认可,直接维持原判

3.2f死亡邮件

下面的图就是30天后收到死亡通牒,如图可见。

wechat_2025-05-16_174421_144.png

其中最关键的两点:

第一点:死刑立即执行!

本函作为您与Apple之间的《Apple开发者计划许可协议》(以下简称“ADP协议”)和《Apple开发者协议》(以下简称“开发者协议”)的终止通知,立即生效。

第二点:没救了不用挣扎了。

本函并非对此事事实的完整陈述,本函中的任何内容均不应被解释为放弃苹果可能拥有的任何权利或补救措施,所有这些权利或补救措施均在此保留

后续处理

最基本是测试设备不用想了,要贴标签做好设备隔离。避免重蹈覆辙!开发设备如果条件允许的情况下建议也处理掉了,以绝后患。

其次是将绑定的内购账号存档记录,不再使用。

最后就是代码了能不用就不能,能不从里面粘贴就别粘贴。苹果的算法已经不像之前那么简单了,别因为偷懒因小失大!也恳请各位管理层能多宽限些时间,避免技术编码时间不够不得不偷懒。

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

JSON Schema 表单规范

2025年5月16日 16:16

🧾 JSON Schema 表单规范 v1.0

该规范用于描述一个表单页面的 UI、字段规则、交互逻辑和校验要求。支持分组、动态显隐、联动、正则等高级能力。


JSON示例

{
  "title": "注册信息",
  "sections": [
    {
      "title": "基础信息",
      "fields": [
        {
          "key": "name",
          "label": "姓名",
          "type": "text",
          "required": true,
          "placeholder": "请输入姓名"
        },
        {
          "key": "gender",
          "label": "性别",
          "type": "radio",
          "required": true,
          "options": [
            { "label": "男", "value": "male" },
            { "label": "女", "value": "female" }
          ]
        },
        {
          "key": "militaryStatus",
          "label": "服兵役情况",
          "type": "text",
          "placeholder": "如已服役/未服役",
          "visibleIf": {
            "gender": "male"
          }
        }
      ]
    },
    {
      "title": "联系方式",
      "fields": [
        {
          "key": "phone",
          "label": "手机号码",
          "type": "text",
          "validators": [
            {
              "type": "regex",
              "pattern": "^1[0-9]{10}$",
              "message": "请输入有效手机号"
            }
          ]
        }
      ]
    }
  ]
}

Swift Demo

chatgpt.com/canvas/shar…

📌 顶层结构

{
  "title": "表单标题",
  "sections": [ FormSection, ... ]
}
字段 类型 含义
title string 整个表单的标题
sections array 分区数组,每个分区有一个标题与若干字段

📦 FormSection

{
  "title": "分区标题",
  "fields": [ FormField, ... ]
}
字段 类型 含义
title string 分区标题
fields array 表单字段列表

🧩 FormField 字段属性说明

{
  "key": "字段标识",
  "label": "显示名称",
  "type": "字段类型",
  "required": true,
  "placeholder": "提示文本",
  "editable": true,
  "hidden": false,
  "options": [ Option ],
  "visibleIf": { "gender": "male" },
  "validators": [ Validator ]
}
字段 类型 说明
key string 字段唯一标识(存值取值)
label string 显示名
type string 字段类型(见下表)
required boolean 是否必填
placeholder string 输入框提示文字
editable boolean 是否可编辑
hidden boolean 是否默认隐藏(不可见)
options array of Option 可选项,仅限 select/radio
visibleIf object {k: v} 显示条件(当 key==value 时显示)
validators array of Validator 校验规则

支持的字段类型(type):

类型 说明
text 普通文本输入框
textarea 多行文本
number 数字输入
select 下拉菜单
radio 单选组
checkbox 多选组(待扩展)
date 日期选择(可扩展)

🎛️ Option 配置(用于 select / radio)

{ "label": "男", "value": "male" }
字段 类型 含义
label string 用户可见文字
value string 实际提交值

✅ 校验 Validator

{
  "type": "regex",
  "pattern": "^1[0-9]{10}$",
  "message": "请输入有效手机号"
}
字段 类型 说明
type string 当前仅支持 "regex"
pattern string 正则表达式
message string 错误提示文本

🎯 visibleIf 条件控制说明

用于控制字段是否显示,格式如下:

"visibleIf": {
  "gender": "male"
}
  • 当 gender == "male" 时,该字段显示
  • 支持多个条件(与关系)
  • 可扩展支持数组值(如某字段 ∈ 多个值)

🧪 示例片段

{
  "key": "militaryStatus",
  "label": "服兵役情况",
  "type": "text",
  "visibleIf": {
    "gender": "male"
  }
}

🏁 约定建议(团队协作)

建议 说明
key 唯一 必须唯一,用于表单存储
所有文本请支持 i18n 协议 label / placeholder / message
支持 UI 端自定义字段类型扩展 可在 type 扩展为 imageUploader, mapPicker
使用 camelCase 命名字段 推荐风格一致

LazyViewContainer-iOS中节省内存的UI创建方式

作者 songgeb
2025年5月16日 14:48

背景

iOS工程中,创建自定义UI组件时有一个常见的写法:

  1. UI组件初始化时,创建所有子组件实例;并将子组件添加到自定义组件层级中,再使用Autolayout做好约束布局
  2. 后续有数据时,直接为组件的每个子组件填充数据

这样的写法是会导致一定的内存浪费,比如

  • 有的场景可能仅在特定情况下才会拿到数据显示UI组件,如果没有数据,若仍然初始化了某个UI组件,则导致该组件占用了一部分长期不会用到的内存
  • 这样的浪费在像直播房等需要大量UI、非UI组件的场景下比较常见

当然,根本的解决方案也是很容易想到的:

  • 延迟加载:仅在需要某个UI组件时才去创建和使用

没错,这无疑可以从根本上解决该问题,但是,延迟加载写起来其实要更麻烦一些,所以为了方便快捷,最开始提到的写法其实更普遍

下面会提到一种折中的方案,能部分降低内存的浪费

LazyViewContainer

  • 红框为普通UI组件直接创建和内存分配过程;绿框中是LazyViewContainer工作原理
  • 普通UI组件初始化完后,内存中就有了该组件完整的对象
  • LazyViewContainer只是一个有很少轻量属性(3个)的普通类,所以LazyViewContainer初始化后,此时内存中的占用较小(约48 bytes)。仅当执行ensureView方法时,才会尝试创建实际的UI组件
  • 最右侧紫色框展示了不同情况下内存占用情况
  • 简单总结一下LazyViewContainer的工作原理,就是在实际UI组件创建之前添加了一个中间层(使用泛型和closure来做到这一点),尽可能延迟实际UI组件及子组件的创建

内存占用测试

使用了几个最常见的系统UI组件进行了一下内存占用的测试

  • 测试环境:
    • iPhone 13 mini/iOS 18.1.1, Release环境
  • 测试方法:
    • 使用不同UI组件,使用不同创建方式,验证对应情况下,看单个UI组件实例实际的内存占用

不同UI组件的创建逻辑代码如下所示:

// UILabel
let label = UILabel()
label.text = "Test Label"
label.font = .systemFont(ofSize: 14)
label.textColor = .red
label.backgroundColor = .blue
label.frame = CGRect(x: 100, y: 100, width: 100, height: 20)
// UIButton
let button = UIButton(type: .system)
button.setTitle("Test Button", for: .normal)
button.backgroundColor = .green
button.frame = CGRect(x: 100, y: 200, width: 100, height: 40)
// UIImageView
let imageView = UIImageView()
imageView.backgroundColor = .yellow
imageView.frame = CGRect(x: 100, y: 300, width: 100, height: 100)
组件类型 创建方式 单个实例内存(bytes) 节省内存(bytes) 节省比例
UILabel 直接创建 896 - -
Lazy创建 48 768 94.64%
Lazy Ensure 944 -48 -5.36%
UIButton 直接创建 768 - -
Lazy创建 48 1792 93.75%
Lazy Ensure 816 -48 -6.25%
UIImageView 直接创建 512 - -
Lazy创建 48 384 90.62%
Lazy Ensure 560 -48 -9.38%

根据测试结果,总结一下结论:

  1. LazyViewContainer单个实例(在没有通过ensureView触发实际UI组件创建时)占用的内存约为 48 bytes
  2. 基于结论1,所以当LazyViewContainer真的要创建实际UI组件时,会比直接创建组件占用的内存更多,但也仅多48 bytes,至于精确的内存占比可以参考表格中“节省比例”列的绿字数据
  3. 同时也能预见到,当自定义UI组件越复杂时,“仅Lazy创建”的方式下,带来的内存节省比例更显著

LazyViewContainer使用场景

首先要明确两点:

  1. 这并不是创建UI组件的最节省内存的方式,只是一种在编码便捷性和内存性能之间的一种折中手段
  2. 不是所有情况下都推荐使用LazyViewContainer

那么,什么场景下推荐使用?

  • 用户可能长时间进行操作的、复杂UI层级的页面(如语音、视频、直播房间)
    • 这样的页面支持的功能比较多,但又不是需要同时运行这些功能,当多数用户、多数情况下仅使用一小部分功能时,那么如果仍然在进入页面时创建所有功能的业务或UI组件,就是会造成比较严重的内存浪费
  • 现有代码中业务逻辑太复杂,不宜短时间做大重构时,适合这样的折中方案
  • 复杂的自定义UI组件
    • 子组件越多,UI层级越复杂,当该UI组件使用率低的时候,所带来的内存节省效果越明显

什么场景下不推荐使用?

  • 列表页面
    • 列表滚动时,伴随多个cell的复用,虽然每个cell的数据不尽相同,有的UI元素可能不需要显示。但可能会被复用,所以此情况下,cell初始化时将所有子UI组件都创建,合理性更强,这样会更方便滚动时的复用

源码与使用方式

源码:

//
//  LazyViewContainer.swift
//
//  Created by songgeb on 2025/1/1.
//  Copyright © 2025 songgeb. All rights reserved.
//

import UIKit
/// 将创建自定义UIView逻辑延迟到数据到来时,同时避免当数据无效时仍创建自定义UIView,导致无意义的内存占用问题
final class LazyViewContainer<T: UIView> {
    private(set) var view: T?
    private let createView: () -> T
    private weak var parentView: UIView?

    init(createView: @escaping () -> T) {
        self.createView = createView
    }

    init() {
        createView = { T() }
    }

    /// 获取或创建视图,交给调用方处理布局
    /// - Parameters:
    ///   - parent: 父视图
    ///   - customAddition: 自定义添加视图逻辑
    ///   - onFirstAddition: 首次创建、同时添加到parentView的时机
    /// - Returns: 自定义视图
    @discardableResult
    func ensureView(
        in parent: UIView,
        customAddition: ((T) -> Void)? = nil,
        onFirstAddition: ((T) -> Void)? = nil) -> T
    {
        if let existingView = view {
            return existingView
        }

        let newView = Thread.isMainThread ? createView() : DispatchQueue.main.sync { createView() }

        // 使用自定义添加方式或默认 addSubview
        if let customAddition {
            customAddition(newView)
        } else {
            parent.addSubview(newView)
        }

        onFirstAddition?(newView)
        view = newView
        parentView = parent
        return newView
    }

    /// 移除视图
    /// - Parameter customRemoval: 自定义移除视图逻辑
    func removeView(using customRemoval: ((T) -> Void)? = nil) {
        guard let view = view, view.superview != nil else { return }
        func doRemoving() {
            if let customRemoval {
                customRemoval(view)
            } else {
                view.removeFromSuperview()
            }
        }
        if Thread.isMainThread {
            doRemoving()
        } else {
            DispatchQueue.main.async { doRemoving() }
        }
        self.view = nil
        parentView = nil
    }

    // 检查视图是否创建
    var isCreated: Bool {
        view != nil
    }

    deinit {
        removeView()
    }
}

使用方式:

class TestViewController: UIViewController {
    private lazy var leftVerticalStack: UIStackView = {
        let vstack = UIStackView(frame: .zero)
        vstack.axis = .vertical
        vstack.alignment = .leading
        vstack.distribution = .equalSpacing
        return vstack
    }()
    
    private lazy var detailLabelLoader = LazyViewContainer<UILabel> {
        let label = UILabel()
        label.font = .systemFont(withSize: 14)
        label.textColor = .bSecondary
        return label
    }

    private func updateDetailLabel(_ shouldShowDetail: Bool) {
        if shouldShowDetail {
            detailLabelLoader.ensureView(in: leftVerticalStack, customAddition: { [weak leftVerticalStack] label in
                guard let leftVerticalStack else { return }
                leftVerticalStack.addArrangedSubview(label)
            })
            detailLabelLoader.view?.text = "abc"

        } else {
            detailLabelLoader.removeView { [weak self] label in
                guard let self else { return }
                leftVerticalStack.removeArrangedSubview(label)
            }
        }
    }
}
❌
❌