阅读视图

发现新文章,点击刷新页面。

[转载] 给世界上色——滤镜底层原理

滤镜最早应用在电视影视业,对剧和电影作品后期进行调色效果。如今拍照、修图都离不开滤镜效果。我们在微博、朋友圈、电视、影院里看到的照片视频,无一没有滤镜的效果,滤镜已经深入我们生活的方方面面。

这里浅略地揭秘一下当前图像处理中滤镜底层的原理。

在这里插入图片描述
在这里插入图片描述

RGB颜色


RGB色彩模式是工业界的一种颜色标准,是通过对红®、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是运用最广的颜色系统之一。
在这里插入图片描述
对于一张图片的每一个像素,都可以由唯一的颜色(R,G,B)表示,其中RGB的范围是0~255。0代表最暗,255代表最亮。
在这里插入图片描述

颜色查找表


滤镜的本质就是颜色的变换。我们可以把一个像素的颜色(R,G,B)看成是一个三维空间上的一个坐标点。颜色变换相当于是三维空间的 [一个坐标点][另一个坐标点] 的映射关系。也就是:

1
2
3
4
5
6
7
8
9
10
11
12
         (old R,old G,old B)  --->   (new R,new G,new B)

例如:

(23,77,134) ---> (122,34,255)

(189,65,138) ---> (5,65,21)

(0,0,2) ---> (33,0,1)

………………

在这里插入图片描述

即每一个(R,G,B)都有一个一一对应的目标颜色,那么一个完整的RGB颜色查找表总共有 256×256×256 = 2^24 条。

显然 2^24 这个数字有点太大了,存储它需要至少16Mb的空间,加载到内存也至少需要吃掉16Mb的内存
(问题1:这里可以先思考一下为什么是16Mb?后面会给出解释)

因此我们在实际查找表应用中一般使用 64×64×64 的查找表,RGB每个通道都将 [连续的4个坐标] 映射成 [1个目标坐标]

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(0,X,X)    --->   (34,X,X)

(1,X,X) ---> (34,X,X)

(2,X,X) ---> (34,X,X)

(3,X,X) ---> (34,X,X)


(4,X,X) ---> (128,X,X)

(5,X,X) ---> (128,X,X)

(6,X,X) ---> (128,X,X)

(7,X,X) ---> (128,X,X)

但显然这样会导致原始图片颜色精度的丢失(例如上面的0~3的亮度映射后都变成了无差别的34),那么就需要想办法降低这种精度丢失的问题
(问题2:这里可以先思考一下有哪些补偿精度的方法?后面会给出解答)

LUT图


有了颜色映射表,接下来需要考虑如何去表达这些映射关系了。有一个笨办法是用文本去存储这 64×64×64 这么多条 (old R,old G,old B) —> (new R,new G,new B)这样的映射关系。聪明的你肯定意识到了这个文本的大小会是一个天文数字。那么有没有更好的表达方法呢?优秀的前辈们发现可以用一个图片去表示这个映射关系,这就是LUT图:
在这里插入图片描述
这个LUT图的尺寸是 512×512 ,正好等于 64×64×64(查找表的数量),也就是刚好每一个像素可以表示一条 (old R,old G,old B) —> (new R,new G,new B)这样的映射关系!

我们可以用 [图片的像素坐标] 表示(old R,old G,old B),用这个坐标所在的 [像素的颜色] 表示(new R,new G,new B),这样就实现了映射表的一一对应关系。

我们可以先从比较简单的开始看,为什么用这个坐标所在的 [像素的颜色] 表示(new R,new G,new B)?因为图片本身每个像素就是由(R,G,B)组成,并且都是0~255,刚好可以完美表示一个(new R,new G,new B)。

再来看看比较复杂一丁点的,如何用 [图片的像素坐标] 表示(old R,old G,old B)?因为一个图片的坐标是二维坐标(x,y),要如何表示一个三维的(old R,old G,old B)呢?

可以看到这个图片每一行有8个小正方形,每一列也有8个小正方形,总共有64个小正方形。每个小正方形都是一张 64×64的图片。

我们先从一个小正方形开始看。其实每一个小正方形都代表了完整的(old R,old G) —> (new R,new G)的映射。其中像素Target坐标(x,y)代表(old R,old G),像素Target的颜色的RG值就代表(new R,new G)。

例如下面这张图片:

1.假设(old R,old G) = (100,150),那么Target的坐标就是(100 / 4,150 / 4)= (25,37)

2.如果这个图片里坐标是(25,37)的Target的像素值是(213, 88),那么(new R,new G)=(213, 88)

2.即完成了(old R,old G) = (100,150) —> (new R,new G)=(213, 88) 的映射关系!
在这里插入图片描述
至此已经完成了R通道和G通道的映射,那么如何确定B通道的映射关系呢?前面说到一个完整LUT图有8×8=64个小正方形,这64个小正方形就是用来确定B通道的映射关系的。

我们把这64个小正方形按从左到右从上到下排列,编号0~63,我们就可以把(old B)映射到其中的某个小正方形格子。

拿下面这个示例图比较能说明过程:

假如(old B) = (50),那么最终的颜色落在第(50 / 4)=(12)个格子上。

但注意,这里的(12)并不是(new B),(12)仍然只是图片的 [坐标],因此它代表的其实还是(old B),仅仅 / 4 了而已。

我们回到上面一步的步骤,确定了在是哪个小正方形,就可以在这个小正方形里根据(old R,old G)确定最终的Target。那么(new B)就等于 像素Target颜色的B值!
在这里插入图片描述

聪明的你也许已经意识到了,在这64个小正方形里,每个小正方形相同(x,y)坐标所对应Target像素颜色的(R,G)都是一样的,仅仅只有B不一样。这也就是为什么B颜色最终是根据计算 [在哪个小正方形里] 来确定的。

我们再来完整走一遍LUT图颜色映射的全过程。

1.假如一个像素点原始的颜色是 (old R,old G,old B)=(38,201,88)

2.根据(old B)确定在哪个小正方形:88 / 4 = 22

3.在第22个小正方形里,根据(old R,old G)确定最终Target的坐标:(38 / 4,201 / 4)=(9,50)

4.假如第22个小正方形中,(9,50)所对应的Target像素的颜色是(50,3,126)

5.那么最终的颜色(new R,new G,new B)=(50,3,126),至此完成一个像素点颜色的映射。

6.遍历原始图片的每一个像素,都走一遍1~5的过程,完成对整张图片的滤镜效果。

这里先解答一下上面 [问题1] 和 [问题2] 的解答:

问题1:为什么是16Mb?

因为映射关系都用LUT图表示,每个像素代表一条映射,那么64×64×64 = 2^18,一张 2^18 个像素的无损图片(一般是.png)大小至少是256Kb,而 256×256×256 = 2^24 个像素的无损图片大小至少是16Mb。

问题2:有哪些补偿精度的方法?

注意到上面精度的丢失是因为像素颜色从 256 –> 64,我们上面在做除法的时候丢失了小数点,例如(38 / 4,201 / 4)=(9,50),但其实应该是(38 / 4,201 / 4)=(9.5,50.25),在实际运用的时候我们并不会抛弃小数点。

在计算的时候,如果计算得到的坐标不是位于一个像素的正中心,那么在取色时,会对相邻的几个像素进行加权平均,更靠近的像素权重越大。直观地说就是,理谁越靠近,那么谁就对最终的颜色有更重要的影响。

例如下面这个图,在最终确定颜色时,会考虑相邻的4个像素点的颜色。这就是双线性插值法,除此之外也有单线性插值法,有兴趣的朋友欢迎交流。
在这里插入图片描述
双线性插值法示意图:
在这里插入图片描述

如何制作一个LUT图?


1.打开Photoshop,打开一个你想调色的图片

在这里插入图片描述

2.通过各种调节,达到你所期待的颜色

在这里插入图片描述

3.载入一张原始LUT图

在这里插入图片描述
什么是原始LUT图:就是经过这个LUT颜色变化之后,还是原来的颜色,也就是 [什么颜色都不变]

它的映射关系:

1
2
3
4
5
6
7
8
9
10
(0,0,0) ---> (0,0,0)

(0,0,1) ---> (0,0,1)

………………

(254,255,255) ---> (254,255,255)

(255,255,255) ---> (255,255,255)

4.对这张LUT图也应用上对刚才图片的调色效果

在这里插入图片描述
至此一张LUT滤镜图就做好了:
在这里插入图片描述

怎么理解这个过程?

我的理解是,我们对图片进行 [调色的一系列操作],再 [作用在原始LUT图上],就相当于让这张原始LUT图记录下了 [这一系列操作],记下来之后就可以拿去对任意的图片进行滤镜效果了。

最后附一个效果图

请添加图片描述

[转载] 美颜的奥秘——磨皮底层原理

原文地址

据不完全统计,全世界每隔3秒就有一个人上传自己的自拍照,甚至不少人在P图上所花的时间都超过了化妆时间。

从十多年前“美图秀秀”的横空出世,再到近年来的实时美颜。到今天,美颜功能已经嵌入到各类手机系统当中,帮助大家实现完美自拍。有玩笑说,中国的P图术、韩国的整容术和日本的化妆术瓜三分天下。此秘术自诞生以来教众不断,但受用者,可瞬间变成天仙下凡,号称“传说中的3大东方神秘力量”。由此可见,随着朋友圈、微博等自拍社交越来越盛行,拍个美美的照片已经是人们的刚需了。

其实磨皮算法最底层的本质就是一种降噪算法,也可以说是模糊算法。即通过对脸部的模糊,把各种面部瑕疵模糊掉,以达到磨皮的效果。

本文很简单地介绍几种很基础的模糊算法以及磨皮后的边缘和细节还原。
在这里插入图片描述

一、磨皮核心——模糊算法


模糊算法也可以说是降噪算法,把清晰可见的东西变得模糊。磨皮的原理就是把脸部变“模糊”,把各种瑕疵、皱纹、痘印等变模糊,使得整个脸看起来很光滑平整。模糊算法就是这样,可以隐去很多细节,也可以说是可以用更少的图像信息量去表达一幅图,因此许多细节就在模糊的过程中被抹去。

如何使一张图片变模糊呢,我们不妨从微观看起。

我们来看一张3*3的图:

在这里插入图片描述

假设上面的数字都代表当前位置的像素值。

假如正中央的像素”9”代表一个瑕疵点,那么我们如何把这个”9”模糊掉呢?

1.中值滤波

对核心及周围的像素值排序,取中间值作为新的像素值。
在这里插入图片描述

2.均值滤波

将核心及周围的像素求和取平均值,作为新的像素值。
在这里插入图片描述

3.高斯滤波

在均值滤波的基础上,对每个像素加上一个权重。这个权重是一个高斯函数。概况地说,距离中心点越近,权重越大;距离中心点越远,权重越小。

一维高斯函数可以这样表示:
在这里插入图片描述
下图分别是一维高斯图像和二维高斯图像:
在这里插入图片描述
把二维高斯函数放到我们上面的3*3的区域中,并做归一化,就得到了权重:
在这里插入图片描述
那么最终的颜色这样计算:
在这里插入图片描述

4.双边滤波

在高斯滤波的基础上,再加上一个[像素差异]的权重:与中心颜色相差越大,权重越低;与中心颜色相差越小,权重越高。

这么做是为了能够在模糊的时候,较好地保护图像的边缘细节信息。这也是磨皮常用的模糊算法,因为磨皮就是需要保留人脸的一些纹路边缘细节,使得磨皮效果看起来更加自然。

可以这么理解:

高斯核是[空间域]上的权重:距离中心的空间距离越远,权重越小。

双边滤波多了一个[值域]上的权重:距离中心的像素差别越大,权重越小。

以下两个图片可以更好理解双边滤波:
在这里插入图片描述
在这里插入图片描述
还是拿上面3*3的区域应该这样算:
在这里插入图片描述
这里的值域核仅为了表达方便,实际应用中也需要做类似归一化的操作

我们可以看一下这几种模糊算法的效果:

原图:
在这里插入图片描述
中值滤波:
在这里插入图片描述
均值滤波:
在这里插入图片描述
高斯滤波:
在这里插入图片描述
双边滤波:
在这里插入图片描述

二、锐化——边缘和细节还原


锐化可以分成2步:第一步,提取边缘;第二步:边缘还原到原图上。第二步其实就是简单的把边缘图叠加到原图上,因此这里重点介绍边缘提取算法。

在这里插入图片描述

1.USM锐化

USM锐化是最常见的锐化,其主要利用了模糊图,原理如下:
在这里插入图片描述
上文说过的模糊算法其实就是把大部分细节抹去,用原图减去模糊图,就得到了这幅图像的边缘和细节了。得到细节之后,叠加回原图,就实现了锐化的效果。

如果想要更大的锐化怎么办呢?那就使用更模糊的图,以得到更大的差值:
在这里插入图片描述

2.拉普拉斯Laplace锐化

拉普拉斯锐化方式是通过对像素进行卷积遍历,以得到边缘。
在这里插入图片描述
以4领域卷积核为例:

如果当前像素与上下左右4个像素完全相同,那么计算得的结果为0,即代表当前像素并不是边缘;

反之如果计算结果不为0,说明当前像素与上下左右像素值存在差异,那么这个像素在一定程度上是边缘的一部分。

拉普拉斯锐化效果如下:
在这里插入图片描述

3.索贝尔sobel锐化

sobel锐化也是使用对像素的卷积遍历,不同的是它区分横纵卷积核。
在这里插入图片描述
以横向卷积核为例:

如果左边一列和右边一列像素完全相同,那么计算得的结果为0,即代表当前像素并不是边缘;

如果左边一列和右边一列像素值有所差别,那么计算结果不为0,代表当前像素正处在边缘部分。

sobel边缘提取效果如下:
在这里插入图片描述

效果图:
请添加图片描述

编写游戏程序的一些启示

这个月我开了个新项目:制作 deep future 的电子版。

之所以做这个事情,是因为我真的很喜欢这个游戏。而过去一年我在构思一个独立游戏的玩法时好像进入了死胡同,我需要一些设计灵感,又需要写点代码保持一下开发状态。思来想去,我确定制作一个成熟桌游的电子版是一个不错的练习。而且这个游戏的单人玩法很接近电子游戏中的 4x 类型,那也是我喜欢的,等还原了原版桌游规则后,我应该可以以此为基础创造一些适合电子游戏特性的东西来。

另一方面,我自以为了解游戏软件从屏幕上每个像素点到最终游戏的技术原理,大部分的过程都亲身实践过。但我总感觉上层的东西,尤其是游戏玩法、交互等部分开发起来没有底层(尤其是引擎部分)顺畅。我也看到很多实际游戏项目的开发周期远大于预期,似乎开发时间被投进了黑洞。

在 GameJam 上两个晚上可以做出的游戏原型,往往又需要花掉 2,3 年时间磨练成成品。我想弄清楚到底遇到了怎样的困难,那些不明不白消耗掉的开发时间到底去了哪里。

这次我选择使用前几个月开发的 soluna 作为引擎。不使用前些年开发的 Ant Engine 的原因 在这个帖子里写得很清楚了。至于为什么不用现成的 unreal/unity/godot 等,原因是:

  1. 我明白我要做什么事,该怎么做,并不需要在黑盒引擎的基础上开发。是的,虽然很多流行引擎有源码,但在没有彻底阅读之前,我认为它们对我的大脑还是黑盒。而阅读理解这些引擎代码工程巨大。

  2. 我的项目不赶时间,可以慢慢来。我享受开发过程,希望通过开发明白多一些道理,而不是要一个结果。我希望找到答案,可能可以通过使用成熟引擎,了解它们是怎样设计的来获得;但自己做一次会更接近。

  3. 自己从更底层开发可以快速迭代:如果一个设计模式不合适,可以修改引擎尝试另一个模式。而不是去追寻某个通用引擎的最佳实践。

  4. 我会使用很多成熟的开源模块和方案。但通常都是我已经做过类似的工作,期望可以和那些成熟模块的作者/社区共建。

  5. 这个项目几乎没有性能压力。我可以更有弹性的尝试不同的玩法。成熟引擎通常为了提升某些方面的性能,花去大量的资源做优化,并做了许多妥协。这些工作几乎是不可见的。也就是说,如果使用成熟引擎开发,能利用到的部分只是九牛一毛,反而需要花大量精力去学习如何用好它们;而针对具体需求自己开发,花掉的精力反而更有限,执行过程也更为有趣。

这篇 blog 主要想记录一下这大半个月以来,我是怎样迭代引擎和游戏的。我不想讨论下面列举出来的需求的最佳方案,现在已经完成的代码肯定不是,之后大概率也会再迭代掉。我这个月的代码中一直存在这样那样的“临时方案”、“全局状态”、甚至一些复制粘贴。它们可能在下一周就重构掉,也可能到游戏成型也放在那里。

重要的是过程应该被记录下来。


在一开始,我认为以立即模式编写游戏最容易,它最符合人的直觉:即游戏是由一帧帧画面构成的,只需要组帧绘制需要的画面就可以了。立即模式可以减少状态管理的复杂度。这一帧绘制一个精灵,它就出现在屏幕上;不绘制就消失了。

大部分成熟引擎提供的则是保留模式:引擎维护着一组对象集合,使用者创建或删除对象,修改这些对象的视觉属性。这意味着开发者需要做额外的很多状态管理。如果引擎维持的对象集合并非平坦结构,而是树状容器结构,这些状态管理就更复杂了。

之所以引擎喜欢提供保留模式大概是因为这样可以让实现更高效。而且在上层通过恰当的封装,立即模式和保留模式之间也是可以互相转换的。所以开发者并不介意这点:爱用立即模式开发游戏的人做一个浅封装层就可以了。

但我一开始就选择立即模式、又不需要考虑性能的话,一个只对图形 api 做浅封装的引擎直接提供立即模式最为简单。所以一开始,soluna 只提供了把一张图片和一个单独文字显示在屏幕特定位置的 api 。当然,使用现代图形 api ,给绘制指令加上 SRT 变换是举手之劳。(在 30 年前,只有一个 framebuffer 的年代,我还需要用汇编编写大量关于旋转缩放的代码)

在第一天,我从网上找来了几张卡牌的图片,只花了 10 分钟就做好了带动画和非常简单交互的 demo 。看起来还很丝滑,这给我不错的愉悦感,我觉得是个好的开始。

想想小丑牌也是用 Love2D 这种只提供基本 2d 图片渲染 api 的引擎编写出来的,想来这些也够用了。当然,据说小丑牌做了三年。除去游戏设计方面的迭代时间外,想想程序部分怎么也不需要这么长时间,除非里面有某些我察觉不到的困难。

接下来,我考虑搭一些简单的交互界面以及绘制正式的卡牌。

Deep future 的卡牌和一般的卡牌游戏还不一样。它没有什么图形元素,但牌面有很多文字版面设计。固然,我可以在制图设计软件里定下这些版面的位置,然后找个美术帮我填上,如果我的团队有美术的话……这是过去在商业公司的常规做法吧?可是现在我一个人,没有团队。这是一件好事,可以让我重新思考这个任务:我需要减少这件我不擅长的事情的难度。我肯定会大量修改牌面的设计,我得有合适我自己的工作流。

在 Ant 中,我们曾经集成过 RmlUI :它可以用 css 设计界面。css 做排版倒是不错,虽然我也不那么熟悉,但似乎可以完成所有需求。但我不喜欢写 xml ,也不喜欢 css 的语法,以及很多我用不到的东西。所以,我决定保留核心:我需要一个成熟的排版用的结构化描述方案,但不需要它的外表。

所以我集成了 Yoga ,使用 Lua 和我自己设计的 datalist 语言来描述这个版面设计。如果有一天,我想把这个方案推广给其他人用,它的内在结构和 css 是一致的,写一个转换脚本也非常容易。

暂时我并不需要和 Windows 桌面一样复杂的界面功能。大致上有单个固定的界面元素布局作为 HUD (也就是主界面)就够了。当然,用 flexbox 的结构来写,自动适应了不同的分辨率。采用这种类 CSS 的排版方案,实际上又回到了保留模式:在系统中保留一系列的需要排版布局的对象。

当我反思这个问题时,我认为是这样的:如果一个整体大体是不变的,那么把这个整体看作黑盒,其状态管理被封装在内部。使用复杂度并没有提高。这里的整体就是 HUD 。考虑到游戏中分为固定的界面元素和若干可交互的卡片对象,作为卡牌游戏,那些卡牌放在 HUD 中的容器内的。如果还是用同样的方案管理卡片的细节,甚至卡片本身的构图(它也是由更细分的元素构成的)。以保留模式整个管理就又变复杂了。

所以,我在 yoga 的 api 封装层上又做了一层封装。把界面元素分为两类:不变的图片和文字部分,和需要和玩家交互的容器。容器只是由 yoga 排版的一个区域,它用 callback 的形式和开发者互动就可以了。yoga 库做的事情是:按层次结构遍历处理完整个 DOM ,把所有元素平坦成一个序列,每个元素都还原成绝对坐标和尺寸,去掉层次信息,只按序列次序保留绘制的上下层关系。在这个序列中,固定的图片和文字可以直接绘制,而遇到互动区,则调用用户提供的函数。这些函数还是以立即模式使用:每帧都调用图形 API 渲染任意内容。

用下来还是挺舒服的。虽然 callback 的形式我觉得有点芥蒂,但在没找到更好的方式前先这么用着,似乎也没踩到什么坑。


渲染模块中,一开始只提供了文字和图片的渲染。但我留出了扩展材质的余地。文字本身就是一种扩展材质,而图片是默认的基础材质。做到 UI 时,我发现增加一种新的材质“单色矩形”特别有用。

因为我可以在提供给 yoga 的布局数据中对一些 box 标注,让它们呈现出不同颜色。这可以极大的方便我调试布局。尤其是我对 flexbox 布局还不太熟练的阶段,比脑补布局结果好用得多。

另一个有用的材质是对一张图片进行单色渲染,即只保留图片的 alpha 通道,而使用单一颜色。这种 mask 可以用来生成精灵的阴影,也可以对不规则图片做简单遮罩。

在扩展材质的过程中,发现了之前预留的多材质结构有一些考虑不周全的设计,一并做了修改。


到绘制卡牌时,卡牌本身也有一个 DOM ,它本质上和 HUD 的数据结构没什么区别,所以这个数据结构还是嵌套了。一开始,我在 soluna 里只提供了平坦的绘制 api ,并没有层次管理。一开始我做的假设是:这样应该够用。显然需要打破这个假设了。

我给出的解决方案是:在立即模式下,没必要提供场景树管理,但可以给一个分层堆栈。比如将当前的图层做 SRT 变换,随后的绘图指令都会应用这套变换,直到关闭这个图层(弹出堆栈)。这样,我想移动甚至旋转缩放 HUD 中的一个区域,对于这个区域的绘制指令序列来说都是透明的:只需要在开始打开一个新图层,结束时关闭这个图层即可。

另一个需求是图文混排,和文字排版。一开始我假设引擎只提供单一文字渲染的功能就够用,显然是不成立的。Yoga 也只提供 box 的排版,如果把每个单字都作为一个 box 送去 yoga 也不是不行,但直觉告诉我这不但低效,还会增加使用负担。web 上也不是针对每个单字做排版的。用 Lua 在上层做图片和文字排版也可以,但对性能来说太奢侈了。

这是一个非常固定的需求:把一块有不同颜色和尺寸的文字放在一个 box 中排版,中间会插入少许图片。过去我也设计过不少富文本描述方案,再做一次也不难。这次我选择一半在 C 中实现,一半在 Lua 中实现。C 中的数据结构利于程序解析,但书写起来略微繁琐;Lua 部分承担易于人书写的格式到底层富文本结构的转换。Lua 部分并不需要高频运行,可以很方便的 cache 结果(这是 Lua 所擅长的),所以性能不是问题。

至于插入的少许图片,我认为把图片转换为类似表情字体更简单。我顺手在底层增加了对应的支持:用户可以把图片在运行时导入字体模块。这些图片作为单独的字体存在,codepoint 可以和 unicode 重叠。并不需要以 unicode 在文本串中编码这些图片,而将编码方式加入上述富文本的结构。


在绘制文本的环节,我同时想到了本地化模块该如何设计。这并非对需求的未雨绸缪,而是我这些年来一直在维护群星的汉化 mod 。非常青睐 Paradox 的文本方案。这不仅仅是本地化问题,还涉及游戏中的文本如何拼接。尤其是卡牌游戏,关于规则描述的句子并非 RPG 中那样的整句,而是有很多子句根据上下文拼接而来的。

拼句子和本地化其实是同一个问题:不同语言间的语法不同,会导致加入一些上下文的句子结构不同。P 社在这方面下了不少功夫,也经过了多年的迭代。我一直想做一套类似的系统,想必很有意思。这次了了心愿。

我认为代码中不应该直接编码任何会显示出来的文本,而应该统一使用点分割的 ascii 字串。这些字串在本地化模块那里做第一次查表转换。

有很大一部分句子是由子句构成的,因为分成子句和更细分的语素可以大大降低翻译成不同语言的工作量。这和代码中避免复制粘贴的道理是一样的:如果游戏中有一个术语出现在不同语境下,这个术语在本地化文本中只出现在唯一地方肯定最好。所以,对于文本来说,肯定是大量的交叉引用。我使用 $(key.sub.foobar) 的方式来描述这种交叉引用。注:这相当于 P 社语法中的 $key.sub.foobar$ 。我对这种分不清开闭的括号很不感冒。

另一种是对运行环境中输入的文本的引用:例如对象的名字、属性等。我使用了 ${key} 这样的语法,大致相当于 P 社的 [key] 。但我觉得统一使用 $ 前缀更好。至于图标颜色、字体等标注,在 P 社的语法中花样百出,我另可使用一致的语法:用 [] 转义。

这个文本拼接转换的模块迭代了好几次。因为我在使用中总能发现不完善的实现。估计后面还会再改动。好在有前人的经验,应该可以少走不少弯路吧。


和严肃的应用不同,游戏的交互是很活泼的。一开始我并没有打算实现元素的动画表现,因为先实现功能仿佛更重要。但做着做着,如果让画面更活泼一点似乎心情更愉悦一点。

比如发牌。当然可以直接把发好的牌画在屏幕指定区域。但我更希望有一个动态的发牌过程。这不仅仅是视觉感受,更能帮助不熟悉游戏规则的玩家尽快掌控卡牌的流向。对于 Deep Future 来说更是如此:有些牌摸出来是用来产生随机数的、有些看一眼就扔掉了、不同的牌会打在桌面不同的地方。如果缺少运动过程的表现,玩家熟悉玩法的门槛会高出不少。

但在游戏程序实现的逻辑和表现分离,我认为是一个更高原则,应尽可能遵守。这部分需要一点设计才好。为此,我并没有草率给出方案尽快试错,而是想了两天。当然,目前也不是确定方案,依旧在迭代。

css 中提供了一些关于动画的属性,我并没有照搬采用。暂时我只需要的运动轨迹,固然轨迹是对坐标这个属性的抽象,但一开始没必要做高层次的抽象。另外,我还需要保留对对象的直接控制,也就是围绕立即模式设计。所以我并没有太着急实现动画模块,而且结合另一个问题一起考虑。

游戏程序通常是一个状态机。尤其是规则复杂的卡牌游戏更是。在不同阶段,游戏中的对象遵循不同的规则互动。从上层游戏规则来看是一个状态机,从底层的动画表现来看也是,人机交互的界面部分亦然。

从教科书上搬出状态机的数据结构,来看怎么匹配这里的需求,容易走向歧途;所以我觉得应该先从基本需求入手,不去理会状态机的数据结构,先搭建一个可用的模块,再来改进。

Lua 有 first class 的 coroutine ,非常适合干这个:每个游戏状态是一个过程(相对一帧画面),有过程就有过程本身的上下文,天然适合用 coroutine 表示。而底层是基于帧的,显然就适合和游戏的过程分离开。

以发牌为例:在玩家行动阶段,需要从抽牌堆发 5 张牌到手牌中。最直接的做法是在逻辑上从牌堆取出 5 张牌,然后显示在手牌区。

我需要一个发牌的视觉表现,卡牌从抽牌堆移动到手牌区,让玩家明白这些牌是从哪里来的。同时玩家也可以自然注意到在主操作区(手牌区)之外还有一个可供交互的牌堆。

用立即模式驱动这个运动轨迹,对于单张牌来说最为简单。每帧计算牌的坐标,然后绘制它就可以了。但同时发多张牌就没那么直接了。

要么一开始就同时记录五张牌的目的地,每帧计算这五张牌的位置。这样其实是把五张牌视为整体;要么等第一张牌运动到位,然后开始发下一张牌。这样虽然比较符合现实,但作为电子游戏玩,交互又太啰嗦。

通常我们要的行为是:这五张牌连续发出,但又不是同时(同一帧)。牌的运动过程中,并非需要逐帧关注轨迹,而只需要关注开始、中途、抵达目的地三个状态。其轨迹可以一开始就确定。所以,卡牌的运动过程其实处于保留模式中,状态由系统保持(无需上层干涉),而启动的时机则交由开发者精确控制更好。至于中间状态及抵达目的地的时机,在这种对性能没太大要求的场景,以立即模式逐帧轮询应无大碍(必须采用 callback 模式)。

也就是,直观的书写回合开始的发牌流程是这样的:

for i = 1, 5 do
  draw_card() -- 发一张牌
  sleep(0.1)  -- 等待 0,1 秒
end

这段代码作为状态机逻辑的一部分天然适合放在单独的 coroutine 中。它可以和底层的界面交互以及图形渲染和并行处理。

而发牌过程,则应该是由三个步骤构成:1. 把牌设置于出发区域。2. 设定目的地,发起移动请求。3. 轮询牌是否运动到位,到位后将牌设置到目的地区域。

其中步骤 1,2 在 draw_card 函数中完成最为直观,因为它们会在同一帧完成。而步骤 3 的轮询应该放在上述循环的后续代码。采用轮询可以避免回调模式带来的难以管理的状态:同样符合直观感受,玩家需要等牌都发好了(通常在半秒之内)再做后续操作。

我以这样的模式开发了一个基于 coroutine 的简单状态机模块。用了几天觉得还挺舒适。只不过发现还是有一点点过度设计。一开始我预留了一些 api 供使用者临时切出当前状态,进入一个子状态(另一个 coroutine),完成后再返回;还有从一个过程中途跳出,不再返回等等。使用一段时间以后,发现这些功能是多余的。后续又简化掉一半。

至于动画模块,起初我认为一切都围绕卡牌来做就可以了。可以运动的基本元素就是不同的卡片。后来发现其实我还需要一些不同于卡片的对象。运动也不仅仅是位移,还包括旋转和缩放,以及颜色的渐变。

至于对象运动的起点和终点,都是针对的前面所述的“区域”这个概念。一开始“区域”只是一个回调函数;从这里开始它被重构成一个对象,有名字和更多的方法。“区域”也不再属于同一个界面对象,下面会谈到:我一开始的假设,所有界面元素在唯一 DOM 上,感觉是不够用的。我最终还是需要管理不同的 DOM ,但我依旧需要区域这个概念可以平坦化,这样可以简化对象可以在不同的 DOM 间运动的 API。

运动过程本身,藏在较低的层次。它是一个独立模块,本质上是以保留模式管理的。在运动管理模块中,保留的状态仅仅是时间轴。也就是逐帧驱动每个运动对象的时间轴(一个数字)。逐帧处理部分还是立即模式的,传入对象的起点和终点,通过时间进度立即计算出当前的状态,并渲染出来。


从状态管理的角度看,每帧的画面和动画管理其实并不是难题。和输入相关的交互管理更难一些,尤其是鼠标操作。对于键盘或手柄,可以使用比较直观的方式处理:每帧检查当下的输入内容和输入状态,根据它们做出反应即可。而鼠标操作天生就是事件驱动的,直到鼠标移动到特定位置,这个位置关联到一个可交互对象,鼠标的点击等操作才有了特别的含义。

ImGUI 用了一种立即模式的直观写法解决这个问题。从使用者角度看,它每帧轮询了所有可交互对象,在绘制这些对象的同时,也依次检查了这些对象是否有交互事件。我比较青睐这样的用法,但依然需要做一些改变。毕竟 ImGUI 模式不关注界面的外观布局,也不擅长处理运动的元素。

我单独实现了一个焦点管理模块。它内部以保留模式驱动界面模块的焦点响应。和渲染部分一样,处理焦点的 API 也使用了一些 callback 注入。这个模块仅管理哪个区域接收到了鼠标焦点,每个区域通过 callback 函数再以立即模式(轮询的方式)查询焦点落在区域内部的哪个对象上。

在使用层面,开发者依然用立即模式,通过轮询获取当前的鼠标焦点再哪个区域哪个对象上;并可查询当前帧在焦点对象上是否发生了交互事件(通常是点击)。这可以避免用 callback 方式接收交互事件,对于复杂的状态机,事件的 callback 要难管理的多。

一开始我认为,单一 HUD 控制所有界面元素就够了。只需要通过隐藏部分暂时不用的界面元素就可以实现不同游戏状态下不同的功能。在这个约束条件下,代码可以实现的非常简单。但这几天发现不太够用。比如,我希望用鼠标右键点击任何一处界面元素,都会对它的功能做一番解说。这个解说界面明显是可以和主界面分离的。我也有很大意愿把两块隔离开,可以分别独立开发测试。解说界面是帮助玩家理解游戏规则和交互的,和游戏的主流程关系不大。把它和游戏主流程放在一起增加了整体的管理难度。但分离又有悖于我希望尽可能将对象管理平坦化的初衷,我并不希望引入树状的对象层次结构。

最近的设计灵感和前面绘制模块的图层设计类似,我给界面也加入了图层的概念。永远只有一个操作层,但层次之间用栈管理。在每个状态看到的当下,界面的 DOM 都是唯一的。状态切换时则可以将界面压栈和出栈。如果后续不出现像桌面操作系统那样复杂的多窗口结构的话,我想这种栈结构分层的界面模式还可以继续用下去。

另一个变动是关于“区域”。之前我认为需要参与交互的界面元素仅有“区域”,“区域”以立即模式自理,逐帧渲染自身、轮询焦点状态处理焦点事件。最近发现,额外提供一种叫“按钮”的对象会更方便一些。“按钮”固然可以通过“区域”来实现,但实践中,处理“按钮”的不是“按钮”本身,而是容纳“按钮”的容器,通常也是最外层的游戏过程。给“按钮”加上类似 onclick 的 callback 是很不直观的;更直观的做法是在游戏过程中,根据对应的上下文,检查是否有关心的按钮被点击。

所有的按钮的交互管理可以放在一个平坦的集合中,给它们起上名字。查询时用 buttons.click() == "我关心的按钮名字" 做查询条件,比用 button_object.click() 做查询条件要舒服一点。


以上便是最近一个月的部分开发记录。虽然,代码依旧在不断修改,方案也无法确定,下个月可能还会推翻目前的想法。但我感觉找到了让自己舒适的节奏。

不需要太着急去尽快试错。每天动手之前多想想,少做一点,可以节省很多实作耗掉的精力;也不要过于执著于先想清楚再动手,毕竟把代码敲出带来的情绪价值也很大。虽然知道流畅的画面背后有不少草率的实现决定,但离可以玩的游戏更进一步的心理感受还是很愉悦的。

日拱一卒,功不唐捐。

Swift 中 let 与 var 的真正区别:不仅关乎“可变”与否

原文:Swift Basics: The Real Difference Between let and var Explained with Examples

很多初学 Swift 的同学会把 letvar 的区别简单记忆成“常量 vs 变量”。

但在实际工程中,这条规则只是起点。选择 let 还是 var 会直接影响代码的安全性、可读性,甚至运行时性能。

基础语义:可变与不可变

  • var:可变变量。值在生命周期内可以被重新赋值或修改。
  • let:不可变绑定。一旦赋值,就不能再指向别的值。
// var 可以改
var score = 10
score += 5          // ✅ 11

// let 不能改
let pi = 3.14
pi = 3.1415         // ❌ Cannot assign to value: 'pi' is a 'let' constant

何时用 let,何时用 var?

官方社区的最佳实践:“先写 let,必要时再改成 var。”

这条规则背后的逻辑是:

  1. 不可变数据天然线程安全,减少副作用;
  2. 编译器可以做更多优化(如栈上分配、内联);
  3. 阅读代码的人无需担心值被中途篡改。
场景 推荐关键字
用户 ID、出生日期、API 返回的只读模型 let
计分器、计时器、用户输入框内容 var
SwiftUI 的 @State包装属性 var(因为框架会重新赋值)

示例:

let identityNumber = "12345678900"   // 一辈子不会变
var currentCity      = "Erzurum"     // 用户可能搬家

let 真的“绝对不变”吗?

答案是:取决于类型是值类型(Value Type)还是引用类型(Reference Type)。

引用类型(class)

class Person {
    var name: String
    init(name: String) { self.name = name }
}

let person = Person(name: "Turabi")
person.name = "Muhammed"   // ✅ 合法!
  • person 这个“变量名”不能指向别的对象;
  • 但对象内部的属性仍可以变动。

值类型(struct / enum)

struct Book {
    let title: String
}

let book = Book(title: "1984")
book.title = "The Art of War"   // ❌ Cannot assign to property: 'book' is a 'let' constant
  • 值类型实例被 let 修饰后,整个实例及其所有属性都不可变;
  • 如果想改属性,需要把实例声明为 var,或者把属性声明为 var

小结与实战 Checklist

  1. 默认用 let,除非编译器报错提示你需要可变性。
  2. API 模型全部用 let,除非后端明确会推送增量更新。
  3. UI 状态(如 @State 属性)用 var
  4. 多线程或并发场景,优先把数据设计成不可变,减少锁竞争。
  5. 如果想让对象内部也不可变,考虑:
    • 把 class 改成 struct;
    • 或者把内部属性全部设为 let

附:完整示例

// 1. 值类型:struct
struct Point {
    let x: Int
    let y: Int
}

let p = Point(x: 0, y: 0)
// p.x = 10   // ❌

// 2. 引用类型:class
class Counter {
    var value: Int = 0
}

let counter = Counter()
counter.value += 1   // ✅
// counter = Counter()   // ❌

选择 letvar 不仅是语法风格问题,更是设计决策。

当你写下 let 的那一刻,就向未来的维护者传递了“这里不会被意外修改”的承诺。

git hooks配置

背景 用cocoapods管理的iOS项目中需要限制 release 分支直接push 经过一番Google,找到了git hooks限制的方法,但是看了很多文章,发现废话一堆,不能快速的解决我的问题

零一开源|前沿技术周刊 #12

前沿技术周刊 是一份专注于技术生态的周刊,每周更新。本周刊深入挖掘高质量技术内容,为开发者提供持续的知识更新与技术洞察。

订阅渠道:【零一开源】、 【掘金】、 【RSS


大厂在做什么

美团智能头盔作为专为外卖骑手打造的智能安全装备,具备蓝牙通话、戴盔识别、智能语音助手、碰撞摔倒监控等功能,核心软件功能围绕如何通过主动安全和被动安全相结合的方式有效保护骑手。 本期分享主要介绍智能头盔骑行通话质量、智能语音助手、碰撞摔倒监控三项软件能力。其中“骑行通话质量和智能语音助手”降低骑手操作手机导致的“分心”,帮助骑手“防患于未然”。“碰撞摔倒监控”最大限度的保护骑手、快速的感知事故和触发救治。
在数字内容井喷的时代,移动端已成为视频创作的重要阵地,而视频编辑页作为创作工具的核心场景,不仅为创作者提供了丰富的表达手段和创意平台,更是提升视频制作的效率。通过直观的操作界面和丰富的功能集成,用户可以轻松地将素材、音频、特效及文字等进行融合,创造出独具风格、彰显个性的作品。
如今,AI 编程工具正在重塑软件开发,其核心目标直指“开发民主化”。它们不再仅仅是补全代码片段的助手,而是能理解自然语言需求、生成可运行代码框架、甚至参与系统设计的“协作者”。这一背景下,越来越多的企业开始对外发布相关产品,美团便是其中之一。
兄弟们,刚点开这篇《2025 Google 开发者大会主旨演讲精华汇总》,结果微信提示“环境异常”,得验证才能看… 估计是链接被拦截了?暂时没法扒拉具体内容,等能进去了再瞅瞅。不过按往年套路,大概率是AI开发工具更新、云原生新特性、Android/iOS跨端方案这些硬货,可能还有TensorFlow或Flutter的新版本?回头内容正常了再补个详细的,现在只能说——等我验证完再给你们同步干货!
高德终端技术团队进行开源项目仓库代码升级期间,由于主版本跨度大,代码量更新变化也很大,过往在低版本上的经验知识不足以支持升级,如果依赖个人读懂整体仓库代码耗时过长。为研发提效,使用了阿里内部代码平台工具,发现暂不能满足一些定制化的知识问答,同时使用上也存在一些限制,外部类似deepwiki工具又存在代码安全问题,因此,基于code RAG和code Agent技术开发了研发提效工具,一定程度上满足了对仓库代码的定制理解,查询和修改需求。
从最初仅支持面向编译时的小程序端解决方案,到如今拥有支持多种前端框架和 UI 库的强大能力;从单一的构建工具,到通过开放生态为开发者提供 Webpack、Vite、ESBuild 等丰富的工具选择,让团队能够定制专属的研发流程;从专注小程序开发,到覆盖各大小程序平台以及 Web、iOS、Android、HarmonyOS 等移动端场景——Taro 的每一步成长都离不开社区的力量。
最近,我们上线了一个新能力:支持将部分中文视频翻译为外语的原声风格配音。也就是说,观众现在可以听到“这个人用另一种语言在说话”,但他的声音、语气、节奏,甚至个性表达都和原片几乎一致,不再是那种传统配音里千篇一律的“代言人声线”,而是像本人亲自讲外语一样自然。这背后,其实是一整套跨模态、多语言协同生成系统的能力升级。
在现代播放器架构中,音频后处理已不仅是锦上添花的功能,而是构建差异化听觉体验的关键组件。尤其在多样化的播放场景(手机外放、耳机、电视音响等)下,通过定制化的音效增强手段,有效提升听感表现已成为基础能力之一。

码圈新闻

这两天在上海世博展览馆举行的 2025 世界人工智能大会(WAIC)热度相当高,上到央媒下到朋友圈不断看到,甚至总理李强、双奖(诺贝尔/图灵)得主辛顿都在开幕式出现,影响力爆表。 周末去逛了一天,AI 的落地场景之多令人咋舌,看完以后我给之前的好几个点子都划上了删除线。还是得多出来看看大厂/新秀公司都在做什么,避免做类似的事情。 这篇文章按照类别记录一下印象比较深刻的产品。
刚刷完2025 Google开发者大会的客户端内容,给咱3年+的老哥们捋捋重点。 Android 15是重头戏:后台任务管理收紧了,得注意`WorkManager`新的电量阈值限制,不然应用可能被系统强杀;UI渲染加了硬件加速新接口,复杂列表滑动能再提10-15帧,对电商、社交类应用挺香。 开发工具方面,Android Studio Hedgehog直接集成了AI代码诊断,写`Compose`时会自动提示重组优化点,试了下比之前手动查省事儿多了。Flutter 4.0也放了大招,原生代码互调延迟降了40%,混编项目终于不用再纠结性能损耗了。 哦对了,跨平台布局`Jetpack Multiwindow`支持更完善了,平板/折叠屏适配能少写一半适配代码。暂时就这些干货,后台优化和Flutter新特性建议优先上手,其他的可以先放收藏夹吃灰~
今日,亚马逊云科技首次上线 OpenAI 开放权重模型,向数百万亚马逊云科技客户开放。客户现可通过 Amazon Bedrock 和 Amazon SageMaker AI 使用 OpenAI 开放权重模型,实现将先进的开放权重模型与全球最广泛云服务的深度集成。
世界机器人大会已经走过10年,回看以前的新闻和产品,此刻站在场馆里大概只有一个感慨:机器人发展太迅速了!
北京时间8月8日凌晨1时,OpenAI举行了长达1个多小时的线上发布会,正式推出了GPT-5。与此前的模型更新直播时间短且主要由研发人员发布相比,GPT-5的发布明显规格更高,不仅发布时间长、细节多,而且OpenAI首席执行官山姆·奥特曼也现身发布会现场。

深度技术

这篇文章我瞅着是讲Android底层的,主要扒了ART虚拟机加载Dex的整个流程,从Dex文件解析到内存映射、类加载这些关键步骤都拆得挺细。重点是结合脱壳场景,分析了加载过程里哪些节点能当通用脱壳点——比如某个钩子函数的调用时机、内存中Dex原始数据的暴露时刻。对咱们这种搞Android逆向或底层开发的来说,理清ART Dex加载逻辑,找脱壳点就有章法了,实操性挺强,值得细品。
在AI技术迅猛发展的今天,如何与大型语言模型高效“对话”已成为释放其潜力的关键。本文深入探讨了提示词工程(Prompt Engineering)这一新兴领域,系统解析了从基础概念到高级技巧的完整知识体系,并结合“淘宝XX业务数科Agent”和科研论文深度学习两大实战案例,揭示了高质量提示词如何将AI从“工具”升级为“智能协作者”。无论你是初学者还是实践者,都能从中掌握让AI真正为你所用的核心方法论。
Cursor 是近来大火的 coding agent 工具,凭借其深度集成的智能代码生成、上下文感知和对话式编程体验,极大地提升了开发效率,成为众多工程师日常开发的得力帮手。作为 Cursor 的付费用户,我已将其作为主力编码工具,每天在实际项目中频繁使用。只有真正深入使用,才能切身感受到它所带来的编程体验的神奇之处。在这个过程中,我也对其背后的技术实现产生了浓厚兴趣,本文试图通过一系列实验,深入分析 Cursor 在后台与大模型之间的通信机制,探寻 Cursor 智能能力背后的底层思想与设计原理。
多模态大语言模型(Multimodal Large Language Model)是指能够处理和融合多种不同类型数据(如文本、图像、音频、视频等)的大型人工智能模型。此类模型通常基于深度学习技术,能够理解和生成多种模态的数据,从而在各种复杂的应用场景中表现出强大的能力。
在构建RAG(检索增强生成)系统时,文本分块质量直接影响知识检索精度与LLM输出效果。本文将深入解析五种分块策略的工程实现与优化方案。文中还会放一些技术文档,方便大家更好的理解RAG中常见的技术点。

新技术介绍

迄今为止最大的Compose更新带来了原生自动填充, 智能动画以及让构建Android用户界面如同魔法般轻松的功能
兄弟,你发的这篇Flutter 3.35更新的文章内容好像有点小状况啊——页面显示“环境异常”,得先验证才能看具体内容。我这刷了半天,也没瞅见更新了啥新特性、优化了哪些性能。要不你先去把验证搞定,把正经的更新内容放出来?等内容齐了,我再帮你扒拉扒拉这版3.35到底香不香~
TheRouter 是由货拉拉技术开源的,可同时用于 Android/iOS/HarmonyOS 模块化开发的一整套解决方案框架。Android 支持 KSP、支持 AGP8,iOS 支持 OC/Swift,不仅能对常规的模块依赖解耦、页面跳转,同时提供了模块化过程中常见问题的解决办法。例如:完美解决了模块化开发后由于组件内无法获取 Application 生命周期与业务流程,造成每次初始化与关联依赖调用都需要跨模块修改代码的问题,是目前业界最领先的移动端路由框架。
随着AI时代的到来,各类AI工具层出不穷,业界都在探索一套完整的AI加成的提效方案,我们团队基于自身特色,利用起团队沉淀好的历史知识库,落地了一套深度结合AI的工作流,用AI武装研发团队,实现研发效率的提升。

博客推荐

兄弟,你给的这篇文章内容好像有点问题啊。标题写着《适配 16KB 页面大小:提升应用性能并为用户提供更流畅的应用体验》,但正文全是微信环境异常的提示,什么“完成验证后继续访问”“小程序赞”“在看”之类的,根本瞅不见正经内容。这样我没法帮你总结摘要啊,估计是复制的时候出岔子了?要不你检查下内容是不是漏了,或者重新发下正文?等你弄好我再帮你扒拉扒拉~
兄弟们,刚瞅了眼你发的《深入浅出Android的Context机制》,内容咋全是微信验证、点赞那些玩意儿?正文好像没显示出来啊。不过Context这东西咱老安卓开发肯定熟,简单说就是个“万能管家”——访问资源、启动Activity/Fragment、调系统服务(比如LayoutInflater、NotificationManager)都得靠它。最容易踩坑的就是Context的生命周期:Application Context全局单例,跟着应用走;Activity Context跟页面生命周期绑定,用完就没。要是拿Activity Context搞个静态单例,页面关了还被占着,内存泄漏妥妥的。平时记着:长生命周期的对象(比如单例、Handler)别用Activity Context,能用Application Context就用,准没错。等你文章内容正常了再细扒,先记住这几点避坑~
一般来说ArkWeb作为鸿蒙的Web容器,性能是够用的。但是针对网页的前置处理条件较多,例如涉及到DNS,大量的资源下载,网页和动画渲染等。作为重度依赖资源链的容器,当某个资源还没ok,就会很容易出现白屏,卡端,长时间loading这些影响用户体验的问题。

GitHub 一周推荐

阿里开源最新文生图模型

关于我们

零一开源】 是一个 文章开源项目 的分享站,有写博客开源项目的也欢迎来提供投递。 每周会搜集、整理当前的新技术、新文章,欢迎大家订阅。

[奸笑]

GCD源码剖析

由一个问题想到的:现象是UI操作在主线程但不在主队列,仍不安全,这个问题需要深入gcd源码才能搞清楚真正原因

RunLoop 实现原理

当 APP 没有任何任务的时候,RunLoop 会进入休眠,RunLoop 就告诉 CPU,现在没有事情做,我要去休息,这时 CPU 就会将其资源释放出来去做其他的事情。当下次有任务的时候,例如用户点
❌