摸鱼精选第 35 期

1. 请删掉 99%的 useMemo

一般来说,如果是基础的中后台应用,大多数交互都比较粗糙,通常不需要。如果你的应用类似图形编辑器,大多数交互是颗粒状的(比如说移动形状),那么此时 useMemo 可能会起到很大的帮助。

使用 useMemo 进行优化仅在少数情况下有价值:

  • 你明确知道这个计算非常的昂贵,而且它的依赖关系很少改变。
  • 如果当前的计算结果将作为 memo 包裹组件的 props 传递。计算结果没有改变,可以利用 useMemo 缓存结果,跳过重渲染。
  • 当前计算的结果作为某些 hook 的依赖项。比如其他的 useMemo/useEffect 依赖当前的计算结果。

这几句是不是很熟悉,就是开头我说的 useMemo 的官方文档的用法中提到的这几项。

在其他情况下,将计算过程包装在 useMemo 中没有任何好处。不过这样做也没有重大危害,所以一些团队选择不考虑具体情况,尽可能多地使用 useMemo,这种做法会降低代码可读性。此外,并不是所有 useMemo 的使用都是有效的:一个“永远是新的”的单一值就足以破坏整个组件的记忆化效果。

2. 使用 Taro 开发鸿蒙原生应用 第三篇

本指南详细介绍了鸿蒙运行环境的配置、使用 Taro 开发鸿蒙应用的步骤和注意事项。

其他两篇姊妹文章:

使用 Taro 开发鸿蒙原生应用第一篇——探秘适配鸿蒙 ArkTS 的工作原理

使用 Taro 开发鸿蒙原生应用第二篇 —— 当 Taro 遇到纯血鸿蒙

3. 深入理解经典红黑树

本文主要介绍了红黑树的经典实现,包括 2-3-4 搜索树和经典红黑树的定义、性质,以及红黑树的插入和删除节点操作。此外,还分析了红黑树的时间复杂度,并给出了相关的参考资料。

重要亮点

  • 📖 2-3-4 搜索树:在 2-3 搜索树中增加了 4-节点,新节点插入 4-节点时,需要先将 4-节点转换成 3 个 2-节点,再在其中的 2-节点中执行插入操作。
  • 🔴 经典红黑树:与 2-3-4 搜索树同构,满足节点颜色为红色或黑色、根节点是黑色的、叶子节点为黑色、红色节点的两个子节点为黑色、任意叶子节点到根节点路径上的黑色节点数量相同等性质。
  • ⚙️ 插入节点:插入 2-节点直接将节点插入 2-节点;插入 3-节点需要分左斜 3-节点和右斜 3-节点两种情况讨论;插入 4-节点需要将其分解成 3 个 2-节点,之后将 2-节点的“根节点”合并到它的父节点中。
  • 🗑️ 删除节点:删除节点是红黑树中最复杂的实现,如果删除的是 3-节点或 4-节点,那么移除该节点不会影响红黑树的平衡;如果删除的是 2-节点,需要在删除节点后向上修复红黑树的平衡。
  • 🕙 时间复杂度:含有 n 个节点的红黑树的高度为 logn,不调用 fixAfterDeletion 方法时,复杂度为 O(logn),在 fixAfterDeletion 中,情况 1,3,4 在各执行常数次的颜色改变和至多 3 次旋转后便终止,只有在情况 2 中才可能重复修复平衡,指针也至多上升 O(logn)次,且没有任何旋转操作,所以 fixAfterDeletion 复杂度为 O(logn),最多旋转 3 次,因此红黑树删除方法的时间复杂度为 O(logn)。
  • 📚 巨人的肩膀:《算法导论》:第 13 章红黑树、知乎-关于 AVL 树和红黑树的一点看法、LeetCode-红黑树从入门到看开、博客园-红黑树的删除。

4. 教不会你算我输系列 | 手把手教你 HarmonyOS 应用开发

本文介绍了鸿蒙系统的开发,包括开发简介、第一个鸿蒙版 Hello World、小试牛刀-度加首页和基础图谱入门。鸿蒙系统是一款面向万物互联的全场景分布式操作系统,支持手机、平板、智能穿戴、智慧屏和车机等多种终端设备。本文还介绍了鸿蒙系统的开发环境搭建、开发套件、第一个鸿蒙应用的创建、项目结构、运行和基础图谱入门。

5. C++常见避坑指南

这篇文章是一份全面的 C++编程避坑指南,它详细总结了在 C++开发中常见的错误和最佳实践。以下是文章的精确凝练总结:

  1. 空指针调用成员函数:静态成员函数可以被空指针调用,而非静态成员函数则会导致程序崩溃。
  2. 字符串处理:使用std::string的查找方法时要注意逻辑严谨性,避免错误的字符串匹配。同时,std::stringstd::wstring之间的转换要确保字符编码一致,避免乱码。
  3. 全局静态对象:虽然提供了良好的封装和线程安全,但可能会增加程序启动时间和资源消耗,建议使用externconstexpr来声明全局常量。
  4. 迭代器删除:在使用 STL 容器时,要注意迭代器失效问题,使用eraseremove组合可以安全地删除元素。
  5. 对象拷贝:避免不必要的对象拷贝,使用引用传递和返回值优化(RVO/NRVO)来提升性能。
  6. std::shared_ptr线程安全shared_ptr的引用计数操作是线程安全的,但修改指向时需注意线程安全问题。
  7. std::map使用:使用operator[]可能插入未定义值的元素,应确保键存在或指定默认值。
  8. sizeofstrlensizeof返回类型或变量的字节大小,strlen返回以 null 结尾的字符串长度,两者使用场景不同。
  9. std::async:可能不是真正的异步,取决于启动策略,确保使用std::launch::async以实现真正的异步执行。
  10. 内存泄漏:在使用智能指针如std::shared_ptr时,确保在异常安全的情况下创建和管理,避免内存泄漏。
  11. constconstexprconst保证变量不可修改,而constexpr用于编译时可计算的常量表达式,提高程序性能。

文章最后鼓励 C++开发者交流经验,以避免这些常见的陷阱,并提供了一些额外的学习资源链接。

6. 迈向 Android 架构师:模块化设计原则

这篇文章讨论了 Android 架构中的模块化设计原则,以实现高内聚和低耦合的代码结构。以下是文章的精确凝练总结:

  1. 组件与模块定义:组件是业务相关的文件集合,在 Android 中等同于 Gradle 模块。

  2. 模块化的好处:包括更合理的代码目录、更快的编译速度和更高的开发效率。

  3. 内聚性原则:由共同闭包原则(CCP)、共同复用原则(CRP)和复用发布等同原则(REP)组成,指导如何合理划分模块以追求高内聚。

  4. 耦合性原则:包括无环依赖原则(ADP)、稳定依赖原则(SDP)和稳定抽象原则(SAP),关注模块间的依赖关系。

  5. 模块划分:讨论了基于层级、业务和领域的划分方法,每种方法都有其优势和局限性。

  6. 封装模块:强调模块内部代码应该避免公开,以保持封装性和降低耦合。

  7. 集成模块:app 模块作为集成层,负责连接模块所需的所有“胶水代码”、初始化逻辑、导航代码和公共依赖注入。

  8. 实践建议:推荐学习《整洁架构》一书,以深入理解 Android 架构理念,并应用于实际项目中。

文章强调,模块化设计是每个初阶工程师迈向高阶过程中必须掌握的知识,整洁架构中的设计原则对于构建可维护和可扩展的 Android 应用至关重要。

7. 自研流媒体协议探索与实践

B 站流媒体技术部直播组开发了自研流媒体协议 BMT(Bili Media Transport),旨在解决直播下行带宽节省和回源带宽成本问题。BMT 协议在兼容性、载荷比和握手简化方面进行了优化,支持多种编解码器和多轨道能力,减少额外开销,提高效率。通过核心库 libbmt 的开发和回源架构设计,BMT 在自建 CDN 回源带宽上取得显著节省效果,并在多音轨直播活动中展现了其优势。未来,BMT 协议将继续迭代,探索在推流端的应用并增加互动玩法,为业务赋能。

8. Flutter 鸿蒙终端一体化—鹊桥相会

文章介绍了如何在鸿蒙系统中开发 Flutter 项目,并解决 Flutter 开发者不熟悉鸿蒙代码的问题。通过使用 pigeon 工具,可以一键生成 ets 代码,实现多端代码复用。文章提供了详细的代码示例和步骤,包括如何指定引用、创建接口协议文件、执行命令生成 Channel 文件,以及鸿蒙端的代码实现。通过这种方式,Flutter 代码可以无损迁移,实现 Android、iOS、鸿蒙的 UI 多端统一。

9. Beyond Compare! Rust Vs Js

文章由 360 奇舞团前端开发工程师撰写,探讨了 Rust 与 JavaScript/TypeScript 在 Web 开发中的对比。作者在计划开发一个 SSR 渲染的 Blog 时,面临了选择 JavaScript 还是 Rust 的决策。文章从前端视角出发,寻找 Rust 与 Js/Ts 的语法相似之处,帮助加深对 Rust 语言的理解。同时,文章还讨论了 Rust 中一些有争议的新特性,如 Async,并提供了 Rust 与 JavaScript 在异步编程、类型系统、错误处理、模块系统等方面的详细对比。作者建议前端开发者通过实践和编译错误来学习 Rust,并尝试在 Rust 中实现熟悉的 JavaScript/TypeScript 功能或概念,以此加深两种语言的相似之处和不同之处的认知。

10. 五年沉淀,微信全平台终端数据库 WCDB 迎来重大升级!

微信客户端团队开源的 WCDB 数据库经过五年发展,推出重大升级。新版 WCDB 不向后兼容旧接口,增加 C++核心逻辑支持,通过桥接方法使 Swift 和 Java 能接入。它完整支持 Java 和 Kotlin ORM,覆盖更多终端平台。WCDB 重写 Winq 提高 SQL 表达能力,强化数据备份和修复方案,引入数据迁移、压缩及自动添加新列功能,并进行 FTS5 优化和支持可中断事务。这些改进让 WCDB 在性能、安全性、灵活性和扩展性方面有显著提升,支持多语言且易于集成,适用于复杂业务场景。

11. 客户端动态降级系统

文章讨论了客户端开发中如何通过动态降级系统确保在不同硬件和网络环境下提供流畅的用户体验。服务端已有的降级和熔断机制可以作为客户端设计降级系统的参考。客户端需要处理性能和网速两大类问题,进一步细分为 CPU、内存、电量和网速等方面。通过实时监控这些指标并设定阈值,系统能够计算出性能等级并在必要时通知业务方进行降级处理。

系统设计包括三个主要部分:DynamicLevelManager负责调用监控和决策模块,并通过通知告知业务方;DynamicLevelMonitor监控关键性能指标;DynamicLevelDecision对性能指标进行计算并决定性能级别。文章提供了伪代码示例,阐释了设计思路,并展示了如何在 iOS 系统中实现监控和计算逻辑。

降级处理的示例包括降低网络请求图片尺寸等,目的是减少系统性能消耗,快速恢复到正常运行状态。文章还介绍了如何通过NSNotificationCenter发送性能降级或恢复通知,并根据当前性能等级调整业务处理规则。

整体而言,动态降级系统通过实时监控和智能决策,帮助客户端在面对性能或网络问题时,依然能够提供相对流畅的用户体验,并保证应用内功能的正常使用不受影响。

12. Ollama:本地大模型运行指南

Ollama 是一个开源框架,允许用户在本地运行大型语言模型,如 Llama 3、Mistral、Gemma 等,同时支持自定义和创建新模型。该框架使用 Go 语言开发,适用于 macOS、Linux 和 Windows 系统。

13. Java 线程池的实现原理及其在业务中的最佳实践

本文深入探讨了 Java 线程池的实现原理、源码分析,并提供了在业务场景中使用线程池的最佳实践。

线程池简介

  • 定义:线程池是一种管理并复用线程的机制,通过预先创建并维护一定数量的线程来执行任务,减少线程创建和销毁的性能开销。
  • 好处:减少线程创建和销毁的开销,控制和优化系统资源利用,提高响应速度和并发性能。

Java 线程池的实现原理

  • 类继承关系:核心类为ThreadPoolExecutor,继承自AbstractExecutorService
  • 核心方法:包括execute()submit()shutdown()等。
  • 线程池状态:包括 RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。
  • 执行流程:任务提交后,根据当前线程数和队列状态决定是直接执行、入队列还是创建新线程。
  • 问题思考:分析了线程池的核心线程是否可回收,以及是否可以在提交任务前创建线程。

源码分析

  • execute(Runnable command):任务提交入口,根据线程池状态和工作线程数决定任务去向。
  • addWorker(Runnable firstTask, boolean core):创建新工作线程并添加到线程池。
  • runWorker(Worker w):执行任务,支持线程复用。
  • getTask():从阻塞队列中获取任务。
  • processWorkerExit(w, completedAbruptly):处理线程退出。

线程池在业务中的最佳实践

  • 选择合适参数:根据任务类型(CPU 密集型或 I/O 密集型)和线程池用途(快速响应或批量处理)选择合适的参数。
  • 正确创建线程池对象:推荐使用饿汉式的单例模式创建线程池,支持灵活参数配置。
  • 避免相互依赖的子任务使用同一线程池:防止线程饥饿和死锁。
  • 合理选择 submit()和 execute()方法:根据任务是否需要返回结果选择合适的方法。
  • 捕获线程池中子任务的异常:防止线程异常导致资源浪费。

文章强调了在业务开发中合理利用线程池的重要性,并提供了实际代码示例和问题分析,帮助开发者更高效地使用线程池。

14. JVM 的基础入门

文章全面介绍了 Java 虚拟机(JVM)的基础知识,涵盖了 JVM 内存划分、堆内存分配策略、对象创建步骤、引用类型、类加载机制、垃圾回收(GC)及其算法、垃圾收集器配置、性能调优工具和故障排查策略。

JVM 内存划分:包括堆、方法区(元空间)、虚拟机栈、本地方法栈和程序计数器。堆是对象实例存储的共享区域,方法区存储类信息和静态变量,JDK1.8 后改为元空间。

堆内存分配策略:对象优先在 Eden 区分配,大对象直接进入老年代,长期存活的对象也会进入老年代。动态对象年龄判定和空间分配担保是确保 GC 安全进行的机制。

创建对象步骤:类加载检查、分配内存、初始化零值、设置对象头和执行 init 方法。

对象引用:包括强引用、软引用、弱引用和虚引用,影响垃圾回收行为。

JVM 类加载过程:包括加载、验证、准备、解析和初始化五个阶段。双亲委派机制保证了核心类的安全加载。

垃圾回收:介绍了引用计数法和可达性分析法,以及两次标记过程。垃圾回收算法包括复制算法、标记清除、标记整理和分代收集。

垃圾收集器:从 Serial、Parnew 到 CMS、G1、ZGC 等,各有特点,适用于不同的应用场景。

性能调优:使用 jps、jinfo、jstat 等工具监控和调优 JVM 性能。

故障排查:包括硬件故障、内存泄漏、CPU 飙高、死循环等问题的排查方法。

文章适合作为 JVM 入门和日常调优工作的参考。

15. Netty 的基础入门

本文详细介绍了 Netty 框架的核心组件、逻辑架构、IO 模型、Reactor 多线程模型、拆包粘包问题解决方案、自定义协议、WriteAndFlush 操作、内存管理、高性能数据结构和定时器原理。

核心组件

  • Core 核心层:提供底层网络通信的通用抽象和实现。
  • Protocol Support 协议支持层:覆盖主流协议的编解码实现。
  • Transport Service 传输服务层:定义和实现网络传输能力。

逻辑架构

  • 网络通信层:执行网络 I/O 操作,包括 BootStrap、ServerBootStrap 和 Channel。
  • 事件调度层:通过 Reactor 线程模型对事件进行聚合处理,核心组件为 EventLoopGroup 和 EventLoop。
  • 服务编排层:负责组装服务,核心组件包括 ChannelPipeline、ChannelHandler 和 ChannelHandlerContext。

五种 IO 模型

  • 阻塞 I/O (BIO)
  • 同步非阻塞 I/O (NIO)
  • 多路复用 I/O (select/poll)
  • 信号驱动 I/O (SIGIO)
  • 异步 I/O (aio_系列函数)

Reactor 多线程模型

  • Netty 基于非阻塞 I/O,依赖 NIO 框架的多路复用器 Selector。
  • 事件分发器有两种设计模式:Reactor 和 Proactor。

拆包粘包问题

  • TCP 是面向流的协议,无数据包界限,可能导致拆包现象。
  • 解决方案包括固定消息长度、特定分隔符和消息长度+消息内容。

自定义协议

  • 介绍了 Netty 常用编码器和解码器类型。
  • 通过魔数、协议版本号、序列化算法等设计自定义协议。

WriteAndFlush

  • 出站操作,从 Pipeline 的 Tail 节点开始,直到 Head 节点。
  • write 方法将数据写入 ChannelOutboundBuffer 缓存。
  • flush 方法将数据写入 Socket 缓冲区。

内存管理

  • 讨论了堆外内存的使用和优点。
  • 介绍了 DirectByteBuffer 对象和 Cleaner 对象的作用。

数据载体 ByteBuf

  • 与 JDK NIO 的 ByteBuffer 相比,Netty 的 ByteBuf 支持动态扩缩容,简化读写状态切换。

内存分配算法

  • 讨论了动态内存分配、伙伴算法和 Slab 算法。

高性能数据结构

  • FastThreadLocal:使用 Object 数组替代 Entry 数组,提高查找效率和安全性。
  • HashedTimerWheel:用于定时任务的执行,通过时间轮原理优化任务调度。

零拷贝技术

  • 描述了传统数据拷贝过程和零拷贝技术的优势。
  • Netty 中的零拷贝技术体现在堆外内存、CompositeByteBuf、Unpooled.wrappedBuffer、ByteBuf.slice 和 FileRegion 等方面。

select、poll、epoll 的区别

  • select 和 poll 查询每个 fd 的状态,epoll 支持水平触发和边缘触发,使用事件的就绪通知方式。

文章适合作为 Netty 入门和日常开发的参考资料。

16. ChatGPT 的工作原理

本文主要介绍了 ChatGPT 的工作原理,包括其基本概念、概率来源、模型构建、训练过程、嵌入概念、内部结构、训练方法以及实际应用等方面。作者通过深入浅出的方式,解释了 ChatGPT 如何通过大量的文本数据进行训练,从而能够生成自然流畅的语言文本。同时,作者还探讨了 ChatGPT 在实际应用中可能存在的问题和挑战,并提出了一些未来的研究方向和建议。

重要亮点

  • 🤖 ChatGPT 的工作原理:ChatGPT 从根本上说总是试图对它目前得到的任何文本进行“合理的延续”,它不看字面文本;它寻找在某种意义上“意义匹配”的东西。
  • 📄 语言模型:ChatGPT 的核心是一个所谓的“大型语言模型”(LLM),它的建立可以很好地估计这些概率。
  • 🧠 神经网络:神经网络的发明形式与今天的使用非常接近,它可以被认为是大脑似乎工作方式的简单理想化。
  • 📊 概率从何而来:通过取一个英语文本的样本,然后计算不同字母在其中出现的频率,从而得到每个字母的概率。
  • 📈 什么是模型:模型是一种给出某种计算答案的程序,而不是仅仅测量和记住每个案例。
  • 👀 类人的任务模型:我们通常试图让神经网络做的任务是“类似人类”的,而神经网络可以捕获相当普遍的“类似人类的过程”。
  • 🔍 神经网路:神经网络是一个理想化的“神经元”的连接集合,通常按层排列,一个简单的例子是:每个“神经元”都被有效地设置为评估一个简单的数字函数。
  • 📝 机器学习和神经网络的训练:训练有素的网络从它所展示的特定例子中“概括”出来,然后依靠神经网络以“合理”的方式在这些例子之间进行“插值”(或“概括”)。
  • 💻 神经网络训练的实践与理论:在训练神经网络的艺术方面取得了许多进展,有几个关键部分,包括架构的选择、数据的获取和处理、超参数的设置等。
  • 📦 嵌入的概念:嵌入是一种尝试用数字阵列来表示事物“本质”的方式,其特性是“附近的事物”由附近的数字来表示。
  • 🔧 ChatGPT 内部:ChatGPT 是一个巨大的神经网络,它的操作分为三个基本阶段:首先,它获取与迄今为止的文本相对应的标记序列,并找到代表这些标记的嵌入(即一个数字阵列);其次,它以“标准的神经网络方式”对这一嵌入进行操作,数值“通过”网络中的连续层,产生一个新的嵌入(即一个新的数字阵列);然后,它从这个数组的最后一部分,生成一个大约 50,000 个值的数组,这些值变成了不同的可能的下一个标记的概率。
  • 📝 ChatGPT 的训练:ChatGPT 的训练是基于一个巨大的文本语料库,通过调整网络中的权重,使网络在这些例子上的误差(“损失”)最小。
  • 📖 基本训练之上:训练 ChatGPT 的大部分工作是向它“展示”大量来自网络、书籍等的现有文本,但事实证明,还有一个明显相当重要的部分,即让实际的人类主动与 ChatGPT 互动,看看它产生了什么,并在实际上给它反馈“如何成为一个好的聊天机器人”。
  • 🧐 是什么真正让 ChatGPT 工作:语言在根本层面上比它看起来要简单得多,这意味着 ChatGPT 能够成功地“捕捉”人类语言的本质和背后的思维。此外,在其训练中,ChatGPT 以某种方式“隐含地发现”了语言(和思维)中的任何规律性,使其成为可能。
  • 📚 意义空间和语义运动法则:在 ChatGPT 中,任何一段文本都有效地由一个数字阵列来表示,我们可以将其视为某种“语言特征空间”中的一个点的坐标。因此,当 ChatGPT 继续一个文本时,这相当于在语言特征空间中追踪一个轨迹。
  • 📝 语义语法和计算语言的力量:产生“有意义的人类语言”需要什么?在过去,我们可能会认为这不可能是一个人的大脑。但现在我们知道,ChatGPT 的神经网络可以很好地完成这一任务。不过,也许这已经是我们能走的最远的路了,没有什么比这更简单——或者更容易被人类理解——的东西会起作用。
  • 🤔 那么 ChatGPT 在做什么,为什么它能发挥作用:ChatGPT 的基本概念在某种程度上相当简单。从网络、书籍等人类创造的大量文本样本开始。然后训练一个神经网络来生成“像这样”的文本。特别是,让它能够从一个“提示”开始,然后继续生成“像它被训练过的那样”的文本。