普通视图
从开放平台到受控生态:谷歌宣布 Android 开发者验证政策 - 肘子的 Swift 周报 #101
谷歌宣布从 2026 年 9 月起,将 Play 商店的开发者验证要求扩展到所有 Android 应用安装方式,这从根本上改变了该平台的开放分发模式。这项政策要求所有在 Google Play 之外分发应用的开发者必须向谷歌注册、提供政府身份证明并支付费用。该政策将首先在巴西、印度尼西亚、新加坡和泰国实施,2027 年扩展至全球。这代表着 Android 自诞生以来对其开放生态系统原则的最重大背离。
[python3] 5行随机算法(20ms)
随机大法好!
(大雾)
思路:当正确答案比错误答案还多时,不妨随便蒙一个。
class Solution:
def getNoZeroIntegers(self, n: int) -> List[int]:
while(True):
L = random.randint(1,n)
R = n-L
if '0' not in str(L) and '0' not in str(R):
return [L,R]
时间复杂度:O(n^0.046 * lg(n))
,两个部分:
· While循环:O(n^0.046)
。
平均循环次数 == 命中无零整数的期望。生成数字每增加一位,就会有1/10的几率命中0,使得命中期望变为原来的10/9。
因此,平均循环次数为 (10/9) ^ lg(n)
,整理得n ^ lg(10/9)
,约为n的0.046次幂。
考虑到2147483647 ^ 0.046 = 2.673
,在Int范围和O(1)几乎没啥区别。
· If校验:O(lg(n))
'0' not in dec(int)
需要lg(n)的时间复杂度。
日本浮生录 09:睡魔花灯列阵,穿越夏夜青森
Elpis 开发框架搭建第二期 - Webpack5 实现工程化建设
一、目标
使 业务文件 通过 解析引擎 转换成能够 供Koa进行页面渲染 的 产物文件
二、解析引擎的作用
- 解析编译
- 依赖分析
- 编译
- 输出
- 模块分包
- 模块分析
- 模块拆分
- 输出
- 压缩优化与分流
三、分步实现
1. 完成 Webpack 5 基础打包配置
1.1 目录结构
在原有/app
文件夹中新增webpack
文件夹,并添加文件使结构如下
/app
|----原有其他文件夹...
|----/webpack
|----build.js
|----/confg
|----webpack.base.js
1.2 Webpack 相关配置
build.js
配置内容
const webpack = require('webpack');
const webBaseConfig = require('./config/webpack.base.js');
console.log('\nbuilding... \n');
webpack(webBaseConfig, (err, stats) => {
if (err) {
throw err;
}
process.stdout.write(`${stats.toString({
colors: true, // 在控制台输出色彩信息
modules: false, // 不显示每个模块的打包信息
children: false, // 不显示子模块的打包信息
chunks: false, // 不显示每个代码块的信息
chunkModules: true // 显示代码块中模块的信息
})}\n`)
})
webpack.base.js
配置内容
const path = require('path');
const webpack = require('webpack')
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
/**
* webpack 基础配置
*/
module.exports = {
// 入口配置
entry: {
'entry.page1': './app/pages/page1/entry.page1.js',
'entry.page2': './app/pages/page2/entry.page2.js'
},
// 模块解析配置(决定了要加载解析哪些模块以及用什么方式去解释)
module: {
rules: [
{
test: /\.vue$/,
use: {
loader: 'vue-loader'
}
},
{
test: /\.js$/,
include: [
// 只对业务代码进行 babel,加快 webpack 打包速度
path.resolve(process.cwd(), './app/pages')
],
use: {
loader: 'babel-loader'
}
},
{
test: /\.(png|jpe?g|gif)(\?.+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 300,
esModule: false
}
}
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.less$/,
use: [ 'style-loader', 'css-loader', 'less-loader' ]
},
{
test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
use: 'file-loader'
}
]
},
// 产物输出路径
output: {
filename: 'js/[name]_[chunkhash:8].bundle.js',
path: path.join(process.cwd(), './app/public/dist/prod'),
publicPath: '/dist/prod',
crossOriginLoading: 'anonymous'
},
// 配置模块解析的具体行为(定义 webpack 在打包时,如何找到并解析具体模块的路径)
resolve: {
extensions: ['.js', '.vue', '.less', '.css'],
alias: {
}
},
// 配置 webpack 插件
plugins: [
// 处理 .vue 文件,这个插件是必须的
// 它的职能是将你定义过的其他规则复制并应用到 .vue 文件里
// 例如,如果只有一条匹配规则 /\.js$/ 的规则,那么它会应用到 .vue 文件中的 <script> 板块中
new VueLoaderPlugin(),
// 把第三方库暴露到 window context 下
new webpack.ProvidePlugin({
Vue: 'vue'
}),
// 定义全局常量
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: 'true', // 支持 vue 解析 optionsApi
__VUE_PROD_DEVTOOLS: 'false', // 禁用 Vue 调试工具
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false' // 禁用生产环境显示 “水合” 信息
}),
// 构造最终渲染的页面模版
new HtmlWebpackPlugin({
// 产物 (最终模版) 输出路径
filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page1.tpl'),
// 指定要使用的模版文件
template: path.resolve(process.cwd(), './app/view/entry.tpl'),
// 要注入的代码块
chunks: [ 'entry.page1']
}),
new HtmlWebpackPlugin({
// 产物 (最终模版) 输出路径
filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page2.tpl'),
// 指定要使用的模版文件
template: path.resolve(process.cwd(), './app/view/entry.tpl'),
// 要注入的代码块
chunks: [ 'entry.page2']
})
],
// 配置打包输出优化(代码分割、模块合并、缓存、TreeShaing、压缩等优化策略)
optimization: {}
}
1.3 测试文件与模板文件配置
新增一些文件,用于后续验证打包效果
app/pages/page1/entry.page1.js
import { createApp } from 'vue';
import page1 from './page1.vue';
const app = createApp(page1);
app.mount('#root')
app/pages/page1/page1.vue
<template>
<h1>page1</h1>
<input v-model="content" />
</template>
<script setup>
import { ref } from 'vue';
const content = ref('');
console.log('page1 init')
</script>
<style lang="less" scoped>
h1{
color: red;
}
</style>
app/pages/page2/entry.page2.js
import { createApp } from 'vue';
import page2 from './page2.vue';
const app = createApp(page2);
app.mount('#root')
app/pages/page2/page2.vue
<template>
<h1>page2</h1>
<input v-model="content" />
</template>
<script setup>
import { ref } from 'vue';
const content = ref('');
console.log('page2 init')
</script>
<style lang="less" scoped>
h1{
color: blue;
}
</style>
app/view/entry.tpl
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{ name }}</title>
<link href="/static/normalize.css" rel="stylesheet">
<link href="/static/icon.png" rel="icon" type="image/x-icon">
</head>
<body style="color: red">
<div id="root"></div>
<input id="env" value="{{ env }}" style="display: none">
<input id="options" value="{{ options }}" style="display: none">
</body>
<script type="text/javascript">
try {
window.env = document.getElementById('env').value;
const options = document.getElementById('options').value;
window.options = JSON.parse(options);
} catch (e) {
console.error(e)
}
</script>
</html>
1.4 验证结果
node ./app/webpack/build.js
运行上述命令后,能够在 app/public/dist
文件看到打包后的产物
1.5 Controller 修改
由于我们将用于给 Koa 进行渲染的文件放在了 app/public/dist
文件夹中,所以我们需要修改之前的 app/controller/view.js
,对渲染路径进行修改
await ctx.render(`output${sep}entry.${ctx.params.page}`, {
修改为
await ctx.render(`dist${sep}entry.${ctx.params.page}`, {
2. Webpack 打包优化
2.1 实现动态构造
在基础配置中,pligun
的位置我们使用了多个 new HtmlWebpackPlugin
指定最终渲染的页面模版,但是这不利于后续更改与维护,所以我们将要采用动态构造的方式来提高可维护性。
在 app/webpack/config/webpack.base.js
引入所需新依赖
const glob = require("glob");
在 引入依赖后、webpack 基础配置前 实现动态构造
...原有引入依赖代码不变...
// 动态构造 pageEntries htmlWebpackPluginList
const pageEntries = {};
const htmlWebpackPluginList = [];
// 获取 app/pages 目录下所有入口文件 (entry.xxx.js)
const entryList = path.resolve(process.cwd(), "./app/pages/**/entry.*.js");
glob.sync(entryList).forEach((file) => {
const entryName = path.basename(file, ".js");
// 构造 entry
pageEntries[entryName] = file;
// 构造最终渲染的页面文件
htmlWebpackPluginList.push(
// html-webpack-plugin 辅助注入打包后的 bundle 文件到 tpl 文件中
new HtmlWebpackPlugin({
// 产物 (最终模版) 输出路径
filename: path.resolve(
process.cwd(),
"./app/public/dist/",
`${entryName}.tpl`
),
// 指定要使用的模版文件
template: path.resolve(process.cwd(), "./app/view/entry.tpl"),
// 要注入的代码块
chunks: [entryName],
})
);
});
module.exports = { ...已有内容不变... }
动态构造会将多个 HtmlWebpackPlugin
合并为一个 HtmlWebpackPluginList
,我们需要修改代码,将原有的多个 HtmlWebpackPlugin
替换为 HtmlWebpackPluginList
webpack.base.js
文件内,将 plugins 中的
new HtmlWebpackPlugin({
// 产物 (最终模版) 输出路径
filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page1.tpl'),
// 指定要使用的模版文件
template: path.resolve(process.cwd(), './app/view/entry.tpl'),
// 要注入的代码块
chunks: [ 'entry.page1']
}),
new HtmlWebpackPlugin({
// 产物 (最终模版) 输出路径
filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page2.tpl'),
// 指定要使用的模版文件
template: path.resolve(process.cwd(), './app/view/entry.tpl'),
// 要注入的代码块
chunks: [ 'entry.page2']
})
替换为
...htmlWebpackPluginList,
至此,我们成功完成了动态构造的配置
2.2 打包输出优化配置
除了一些基础配置之外,我们还可以在 optimization
自定义一些打包优化规则
// 配置打包输出优化(代码分割、模块合并、缓存、TreeShaing、压缩等优化策略)
optimization: {
/**
* 把 js 文件打包成3种类型
* 1. vendor: 第三方 lib 库,基本不会改动,除非依赖版本升级
* 2. common: 业务组件代码的公共部分抽取出来,改动较少
* 3. entry.{page}: 不用页面 entry 里的业务组件代码的差异部分,会经常改动
* 目的: 把改动和引用频率不一样的 js 区分出来,以达到更好利用浏览器缓存的效果
*/
splitChunks: {
chunks: 'all', // 对同步和异步模块都进行分割
maxAsyncRequests: 10, // 每次异步加载的最大并行请求数
maxInitialRequests: 10, // 入口点的最大并行请求数
cacheGroups: {
vendor: { // 第三方依赖库
test: /[\\/]node_modules[\\/]/,
name: 'vendor', // 模块名称
priority: 20, // 优先级,数字越大,优先级越高
enforce: true, // 强制执行
reuseExistingChunk: true, // 复用已有的公共 chunk
},
common: { // 公共模块
name: 'common', // 模块名称
minChunks: 2, // 被 2 处引用即被归为公共模块
minSize: 1, // 最小分割文件大小 (1 byte)
priority: 10, // 优先级
reuseExistingChunk: true, // 复用已有的公共 chunk
}
}
}
},
2.3 其他配置
在 webpack.base.js
的 resolve.alias
中,我们可以定义一些变量以便后续在 require
时能够更便捷
alias: {
$pages: path.resolve(process.cwd(), "./app/pages"),
$common: path.resolve(process.cwd(), "./app/common"),
$widgets: path.resolve(process.cwd(), "./app/widgets"),
$store: path.resolve(process.cwd(), "./app/store"),
}
3. Webpack 环境分流配置
未完待续
elpis 源于 抖音“哲玄前端”《大前端全栈实践》
【URP】Unity 插入自定义RenderPass
自定义渲染通道是一种改变通用渲染管道(URP)如何渲染场景或场景中的对象的方法。自定义呈现通道(RenderPass)包含自己的Render代码,可以在注入点将其添加到RenderPass中。
【从UnityURP开始探索游戏渲染】专栏-直达
添加自定义呈现通道(RenderPass):
- 使用Scriptable render pass API创建自定义render pass的代码。
- 将自定的render pass注入到URP管线中的指定注入点中,有两种方式:
- 用RenderPipelineManager API注入自定义渲染通道
- 或者通过创建一个可脚本化的RendererFeature添加到URP渲染器中。
使用Scriptable render pass API创建自定义render pass
-
Example custom render pass
using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; internal class ColorBlitPass : ScriptableRenderPass { ProfilingSampler m_ProfilingSampler = new ProfilingSampler("ColorBlit"); Material m_Material; RTHandle m_CameraColorTarget; float m_Intensity; public ColorBlitPass(Material material) { m_Material = material; renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing; } public void SetTarget(RTHandle colorHandle, float intensity) { m_CameraColorTarget = colorHandle; m_Intensity = intensity; } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { ConfigureTarget(m_CameraColorTarget); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { var cameraData = renderingData.cameraData; if (cameraData.camera.cameraType != CameraType.Game) return; if (m_Material == null) return; CommandBuffer cmd = CommandBufferPool.Get(); using (new ProfilingScope(cmd, m_ProfilingSampler)) { m_Material.SetFloat("_Intensity", m_Intensity); Blitter.BlitCameraTexture(cmd, m_CameraColorTarget, m_CameraColorTarget, m_Material, 0); } context.ExecuteCommandBuffer(cmd); cmd.Clear(); CommandBufferPool.Release(cmd); } }
将自定的render pass注入到URP管线中的指定注入点中
用RenderPipelineManager API注入自定义渲染通道
-
通过RenderPipelineManager的注入点委托提供执行时机,加上Camera的EnqueuePass方法注入自定义RenderPass。
public class EnqueuePass : MonoBehaviour { [SerializeField] private BlurSettings settings; private BlurRenderPass blurRenderPass; private void OnEnable() { ... blurRenderPass = new BlurRenderPass(settings); // Subscribe the OnBeginCamera method to the beginCameraRendering event. RenderPipelineManager.beginCameraRendering += OnBeginCamera; } private void OnDisable() { RenderPipelineManager.beginCameraRendering -= OnBeginCamera; blurRenderPass.Dispose(); ... } private void OnBeginCamera(ScriptableRenderContext context, Camera cam) { ... // Use the EnqueuePass method to inject a custom render pass cam.GetUniversalAdditionalCameraData() .scriptableRenderer.EnqueuePass(blurRenderPass); } }
创建一个可脚本化的RendererFeature
此示例执行将屏幕染成绿色的全屏blit。
- 要创建自定义渲染通道,创建一个名为ColorBlitPass.cs的新c#脚本,然后从示例自定义渲染通道部分粘贴代码。
- 注意:这个例子使用了Blitter API。不要使用CommandBuffer。URP中的Blit API。更多信息请参考Blit。
- 使用上面定义好的定制Render Pass
- 要创建Scriptable RendererFeature,将自定义渲染通道添加到渲染循环中,请创建一个名为ColorBlitRendererFeature.cs的新c#脚本,然后将示例Scriptable RendererFeature部分中的代码粘贴进来。
-
Example Scriptable Renderer Feature Scriptable Renderer Feature 添加 render pass 到渲染循环.
using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; internal class ColorBlitRendererFeature : ScriptableRendererFeature { public Shader m_Shader; public float m_Intensity; Material m_Material; ColorBlitPass m_RenderPass = null; public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (renderingData.cameraData.cameraType == CameraType.Game) renderer.EnqueuePass(m_RenderPass); } public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData) { if (renderingData.cameraData.cameraType == CameraType.Game) { // Calling ConfigureInput with the ScriptableRenderPassInput.Color argument // ensures that the opaque texture is available to the Render Pass. m_RenderPass.ConfigureInput(ScriptableRenderPassInput.Color); m_RenderPass.SetTarget(renderer.cameraColorTargetHandle, m_Intensity); } } public override void Create() { m_Material = CoreUtils.CreateEngineMaterial(m_Shader); m_RenderPass = new ColorBlitPass(m_Material); } protected override void Dispose(bool disposing) { CoreUtils.Destroy(m_Material); } }
-
- 要创建将像素染成绿色的着色器代码,请创建一个着色器文件,然后从示例着色器部分粘贴代码。
-
Example shader
-
着色器执行渲染的GPU端。它从相机中采样颜色纹理,然后输出绿色值设置为所选强度的颜色。 注意:与Blitter API一起使用的着色器必须是手工编码的着色器。图形着色器与Blitter API不兼容。
Shader "ColorBlit" { SubShader { Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"} LOD 100 ZWrite Off Cull Off Pass { Name "ColorBlitPass" HLSLPROGRAM #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" // The Blit.hlsl file provides the vertex shader (Vert), // the input structure (Attributes) and the output structure (Varyings) #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl" #pragma vertex Vert #pragma fragment frag // Set the color texture from the camera as the input texture TEXTURE2D_X(_CameraOpaqueTexture); SAMPLER(sampler_CameraOpaqueTexture); // Set up an intensity parameter float _Intensity; half4 frag (Varyings input) : SV_Target { UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); // Sample the color from the input texture float4 color = SAMPLE_TEXTURE2D_X(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, input.texcoord); // Output the color from the texture, with the green value set to the chosen intensity return color * float4(0, _Intensity, 0, 1); } ENDHLSL } } }
-
- 将ColorBlitRendererFeature添加到当前URP Renderer资源中。有关更多信息,请参阅向URP渲染器添加渲染器功能。
- 要更改亮度,请调整Color Blit Renderer Feature组件中的Intensity属性。
注意:如果项目使用XR,为了使示例可视化,在项目中安装MockHMD XR插件包,然后将渲染模式属性设置为单通道实例化。
【从UnityURP开始探索游戏渲染】专栏-直达
(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)
3495. 使数组元素都变为零的最少操作次数
解法
思路和算法
对正整数 $x$ 执行除以 $4$ 向下取整,将 $x$ 变成 $0$ 的执行次数与 $x$ 的值的关系如下:当 $1 \le x < 4$ 时,需要执行 $1$ 次;当 $4 \le x < 16$ 时,需要执行 $2$ 次;当 $16 \le x < 64$ 时,需要执行 $3$ 次;以此类推,当存在正整数 $p$ 满足 $4^{p - 1} \le x < 4^p$ 时,需要执行 $p$ 次。因此将 $x$ 变成 $0$ 的执行次数是 $\lfloor \log_4 x \rfloor + 1$。
对于二维数组 $\textit{queries}$ 中的每个查询 $[\textit{left}, \textit{right}]$,可以分别计算从 $\textit{left}$ 到 $\textit{right}$ 的每个正整数的执行次数,并得到区间 $[\textit{left}, \textit{right}]$ 中的所有正整数的执行次数之和。
由于 $\textit{left}$ 和 $\textit{right}$ 的取值范围是 $1 \le \textit{left} < \textit{right} \le 10^9$,因此如果直接遍历区间 $[\textit{left}, \textit{right}]$ 中的每个正整数计算执行次数,则时间复杂度过高,需要优化。
为了计算区间 $[\textit{left}, \textit{right}]$ 中的所有正整数的执行次数之和,可以分别计算区间 $[1, \textit{right}]$ 中的所有正整数的执行次数之和与区间 $[1, \textit{left} - 1]$ 中的所有正整数的执行次数之和,两项之差即为区间 $[\textit{left}, \textit{right}]$ 中的所有正整数的执行次数之和。
对于非负整数 $\textit{num}$,计算区间 $[1, \textit{num}]$ 中的所有正整数的执行次数之和的方法如下。
-
用 $\textit{currReductions}$ 表示当前执行次数,用 $\textit{start}$ 表示执行次数是 $\textit{currReductions}$ 的最小正整数,初始时 $\textit{currReductions} = 1$,$\textit{start} = 1$。
-
对于每个 $\textit{start}$ 计算 $\textit{end} = \min(\textit{start} \times 4 - 1, \textit{num})$,则区间 $[\textit{start}, \textit{end}]$ 为执行次数是 $\textit{currReductions}$ 的所有正整数的区间,该区间中的正整数个数是 $\textit{end} - \textit{start} + 1$,因此将区间 $[1, \textit{num}]$ 中的所有正整数的执行次数之和增加 $\textit{currReductions} \times (\textit{end} - \textit{start} + 1)$。然后将 $\textit{start}$ 的值乘以 $4$,将 $\textit{currReductions}$ 的值增加 $1$,重复上述操作。当 $\textit{start} > \textit{num}$ 时,结束操作,得到区间 $[1, \textit{num}]$ 中的所有正整数的执行次数之和。特别地,当 $\textit{num} = 0$ 时,上述做法也适用。
将区间 $[\textit{left}, \textit{right}]$ 中的所有正整数的执行次数之和记为 $\textit{reductions}$。当每次操作对两个正整数执行除以 $4$ 向下取整时,区间 $[\textit{left}, \textit{right}]$ 的最少操作次数等于 $\Big\lceil \dfrac{\textit{reductions}}{2} \Big\rceil$。理由如下。
-
对于正整数 $x$,用 $r(x)$ 表示对正整数 $x$ 执行除以 $4$ 向下取整,将 $x$ 变成 $0$ 的执行次数,则 $r(x) = \lfloor \log_4 x \rfloor + 1$。将区间 $[\textit{left}, \textit{right}]$ 中的每个正整数 $x$ 都替换成 $r(x)$,则得到从 $r(\textit{left})$ 到 $r(\textit{right})$ 的 $\textit{right} - \textit{left} + 1$ 个正整数组成的新数组,将新数组记为 $\textit{reductionsArr}$,则问题转换成:每次从新数组 $\textit{reductionsArr}$ 中选择两个元素分别减少 $1$,计算将新数组 $\textit{reductionsArr}$ 中的所有元素都变成零或负数的最少操作次数(由于 $0$ 除以 $4$ 仍等于 $0$,因此新数组中的元素变成负数也符合原数组中的元素变成 $0$)。
-
根据 $r(x)$ 的性质,新数组 $\textit{reductionsArr}$ 为单调递增数组且任意两个相邻元素之差等于 $0$ 或 $1$。每次从新数组 $\textit{reductionsArr}$ 中选择最大的两个元素分别减少 $1$,则可以经过若干次操作将所有的最大元素都减少 $1$ 且最多有一个次大元素减少 $1$。
-
经过若干次操作之后,一定可以将新数组 $\textit{reductionsArr}$ 变成所有元素值都相等(例如全部是 $r$)或其中一个元素值等于其余每个元素值加 $1$(例如只有一个元素是 $r + 1$,其余元素都是 $r$)。对于两种情况,都可以将元素值同步减少,直到所有元素变成 $0$ 或其中一个元素值是 $1$ 且其余每个元素值都是 $0$,当剩余一个元素值是 $1$ 时还需要额外操作一次才能将新数组 $\textit{reductionsArr}$ 中的所有元素都变成零或负数。因此在操作结束之后,新数组 $\textit{reductionsArr}$ 中最多有一个元素是 $-1$,其余元素都是 $0$,最少操作次数等于 $\Big\lceil \dfrac{\textit{reductions}}{2} \Big\rceil$。
分别计算二维数组 $\textit{queries}$ 中的每个查询的最少操作次数,计算所有查询结果的总和,即为答案。
代码
###Java
class Solution {
public long minOperations(int[][] queries) {
long operations = 0;
for (int[] query : queries) {
long reductions = countReductions(query[1]) - countReductions(query[0] - 1);
operations += (reductions + 1) / 2;
}
return operations;
}
public long countReductions(int num) {
long reductions = 0;
int currReductions = 1;
long start = 1;
while (start <= num) {
long end = Math.min(start * 4 - 1, num);
reductions += currReductions * (end - start + 1);
start *= 4;
currReductions++;
}
return reductions;
}
}
###C#
public class Solution {
public long MinOperations(int[][] queries) {
long operations = 0;
foreach (int[] query in queries) {
long reductions = CountReductions(query[1]) - CountReductions(query[0] - 1);
operations += (reductions + 1) / 2;
}
return operations;
}
public long CountReductions(int num) {
long reductions = 0;
int currReductions = 1;
long start = 1;
while (start <= num) {
long end = Math.Min(start * 4 - 1, num);
reductions += currReductions * (end - start + 1);
start *= 4;
currReductions++;
}
return reductions;
}
}
复杂度分析
-
时间复杂度:$O(n \log m)$,其中 $n$ 是数组 $\textit{queries}$ 的长度,$m$ 是数组 $\textit{queries}$ 中的最大值。需要计算 $n$ 个查询的结果,每个查询的计算时间是 $O(\log m)$,因此时间复杂度是 $O(n \log m)$。
-
空间复杂度:$O(1)$。
Uncaught ReferenceError: __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ is not defined
vue项目部署后访问页面空白,控制台报错:Uncaught ReferenceError: VUE_PROD_HYDRATION_MISMATCH_DETAILS is not defined
1.问题现象
vue项目编译后通过nginx部署,浏览器访问前端出现
- 1.页面空白
- 2.控制台报错:
Uncaught ReferenceError: __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ is not defined
2.问题解释
此错误意为: 未捕获的引用错误:__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ 变量未定义
- 从 Vue
3.4
开始,引入了__VUE_PROD_HYDRATION_MISMATCH_DETAILS__
这个编译时特性标志,用于控制生产环境中是否显示 hydration 不匹配的详细错误信息。 - 当您的项目(或其某个依赖)尝试使用此新特性时,但如果项目运行的 Vue 版本低于
3.4
(此例中为3.2.47
),运行时环境中不存在这个全局变量,从而导致此引用错误,并使得应用崩溃,呈现白屏。
3.问题解决
通过官方文档找到如下说明:cn.vuejs.org/api/compile…
- VUE_PROD_HYDRATION_MISMATCH_DETAILS****仅在3.4+中可用
- 查看当前项目vue使用的是
3.2.47
版本
4.解决方案
方案一:修改编译配置
在构建工具中定义这个变量,让其存在并被设置为 false
(即在生产环境禁用 hydration 不匹配详情)。
- 使用
vue-cli
- 使用
vite
方案二:升级 Vue
检查 package.json
,将 Vue 及其相关依赖(@vue/compiler-sfc
, vue-router
等)升级到 3.4
或更高版本。这是最根本的解决方法,因为它确保了代码和运行时环境的一致性。
npm install vue@^3.4.0
# 或
yarn add vue@^3.4.0
5. 总结
- 错误
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ is not defined
是由于高版本特性在低版本环境中运行导致的。 -
快速修复:在构建配置中手动定义该变量为
false
。 - 升级修复:将 Vue 升级到 3.4+ 版本。
3516. 找到最近的人
解法
思路和算法
由于第 $1$ 个人和第 $2$ 个人的移动速度相同,因此与第 $3$ 个人的距离更近的人会先到达第 $3$ 个人的位置。
第 $1$ 个人到第 $3$ 个人的距离是 $\textit{distance}_1 = |x - z|$,第 $2$ 个人到第 $3$ 个人的距离是 $\textit{distance}_2 = |y - z|$。结果如下。
-
如果 $\textit{distance}_1 < \textit{distance}_2$,则第 $1$ 个人先到达第 $3$ 个人的位置,返回 $1$。
-
如果 $\textit{distance}_1 > \textit{distance}_2$,则第 $2$ 个人先到达第 $3$ 个人的位置,返回 $2$。
-
如果 $\textit{distance}_1 = \textit{distance}_2$,则两个人同时到达第 $3$ 个人的位置,返回 $0$。
代码
###Java
class Solution {
public int findClosest(int x, int y, int z) {
int distance1 = Math.abs(x - z), distance2 = Math.abs(y - z);
if (distance1 < distance2) {
return 1;
} else if (distance1 > distance2) {
return 2;
} else {
return 0;
}
}
}
###C#
public class Solution {
public int FindClosest(int x, int y, int z) {
int distance1 = Math.Abs(x - z), distance2 = Math.Abs(y - z);
if (distance1 < distance2) {
return 1;
} else if (distance1 > distance2) {
return 2;
} else {
return 0;
}
}
}
复杂度分析
-
时间复杂度:$O(1)$。
-
空间复杂度:$O(1)$。
用 MainActor.assumeIsolated 解决旧 API 与 Swift 6 适配问题
尽管 Swift 提供严格并发检查已有一段时间,但许多苹果官方 API 仍未对此进行充分适配,这种情况可能还会持续相当长的时间。随着 Swift 6 的逐步普及,这个问题变得愈发突出:开发者一方面希望享受 Swift 编译器带来的并发安全保障,另一方面又对如何让代码满足编译要求感到困惑。本文将通过一个 NSTextAttachmentViewProvider 的实现案例,介绍 MainActor.assumeIsolated 在特定场景下的妙用。
写给这段旅程,也写给未来的自己 - 肘子的 Swift 周报 #100
一转眼,周报已经来到了第 100 期。回想 2023 年 10 月第一期时,我并没有把握自己能坚持这么久。但过去两年,通过持续创作,我收获了许多。
x+y必须是奇数,一行代码,0ms双百
Problem: 3021. Alice 和 Bob 玩鲜花游戏
[TOC]
思路
按题意,x + y 要是奇数。因此,推导下,结果就是:
$$n 内偶数的个数 * m 内奇数的个数 + m 内偶数的个数 * n 内奇数的个数$$
$$= (n >> 1) * ((m + 1) >> 1) + (m >> 1) * ((n + 1) >> 1)$$
Code
执行用时分布0ms击败100.00%;消耗内存分布5.55MB击败100.00%
###C
long long flowerGame(int n, int m) {
return (long)(n >> 1) * ((m + 1) >> 1) + (long)(m >> 1) * ((n + 1) >> 1);
}
###Python3
class Solution:
def flowerGame(self, n: int, m: int) -> int:
return (n >> 1) * ((m + 1) >> 1) + (m >> 1) * ((n + 1) >> 1)
您若还有不同方法,欢迎贴在评论区,一起交流探讨! ^_^
↓ 点个赞,点收藏,留个言,再划走,感谢您支持作者! ^_^
动态规划
零一开源|前沿技术周刊 #13
前沿技术周刊 是一份专注于技术生态的周刊,每周更新。本周刊深入挖掘高质量技术内容,为开发者提供持续的知识更新与技术洞察。
大厂在做什么
新技术介绍
深度技术
码圈新闻
博客推荐
- Android: 适配 16KB 页面大小:提升应用性能并为用户提供更流畅的应用体验
- Android: 深入浅出Android的Context机制
- HarmonyOS: 鸿蒙ArkWeb加载优化方案详解
GitHub 一周推荐
关于我们
【零一开源】 是一个 文章 和 开源项目 的分享站,有写博客或开源项目的也欢迎来提供投递。 每周会搜集、整理当前的新技术、新文章,欢迎大家订阅。
未来将至:人形机器人运动会 - 肘子的 Swift 周报 #99
不久前在北京举办的世界人形机器人运动会上,出现了许多令人忍俊不禁的场景:机器人对着空气挥拳、跑步时左摇右摆、踢球时相互碰撞后集体倒地。尽管这些画面看起来颇为滑稽,但回顾过去几年人形机器人的发展历程就会发现,即便当前的产品仍存在诸多不足,其进步却是惊人的。按照这样的发展速度,也许在十年甚至更短的时间内,人形机器人就将走进我们的日常生活,满足各种实际需求。
(多图预警!!!) 边统计边压缩,给你超详细的解答
写在文首
看了很多题解,大家大致的思路基本一致,时间复杂度为O(n·n·m),但大多题解没有把过程表述的很完整,作为一个刷题的小白,我想在这写写小白也能看懂的思路,和做这道题的一些细节和收获
解题思路
题目要求既要考虑1x2, 2x1的情况, 也要考虑2x2的情况:
(1)先考虑1xn的情况,即在一行中(横向)可以找出多少个矩形**(这步我称之为"统计")**,代码如下:统计行中的矩形数目 int now = 0; for (int k = 0; k < col; k++){ if (mat[j][k] == 0) now = 0; else now = k == 0 ? mat[j][0] : now + 1; ans += now; }
- 当有连续三个1的时候,用now分别递增为1,2,3(第一个1可以形成一个矩形,第二个1和第一个1可以形成1个矩形加上其自身一共是两个,第三个1分别可以和前面连续的1形成两个长度分别为3,2的矩形,加上自身一共三个),并加到ans中
- 这一步也可以记录连续的个数,使用等差数列求和公式计算连续1处可以形成的矩形数目,具体代码不展示
- 当遍历到mat[j][k] == 0时,即1不连续了,置now为0,以便后面遇到连续1时进行统计
(2)压缩:
- 理论上,纵向的矩形也可以像横向那样进行统计,但是我们注意到我们不仅要求横向和纵向的矩形,还要求诸如2x2之类的
- 在(1)中我们得到所有大小为1的矩形和横向的各种大小的矩形的情况,所以后面我们只需要求纵向的大小大于1的矩形的情况即可
- 所以在这我们选择向下"压缩"题目所给二维数组,以便继续统计,这里"压缩"的思路理解好下次遇到类似的题能想出来就行,来看图吧
起始的二维数组
对其使用“按位与”进行压缩
第二次统计的结果
思考:这里我们可以发现第二次的统计是2xn的,既包含了2X1的情况也包含了2x2的情况,接下来的步骤继续如上循环"压缩"即可
代码
###java
class Solution {
public int numSubmat(int[][] mat) {
int row = mat.length, col = mat[0].length, ans = 0;
for (int i = 0; i < row; i++){
//统计
for (int j = i; j < row; j++){
int now = 0;
for (int k = 0; k < col; k++){
if (mat[j][k] == 0) now = 0;
else now = k == 0 ? mat[j][0] : now + 1;
ans += now;
}
}
//压缩
for (int j = row - 1; j > i; j--){
for (int k = 0; k < col; k++){
mat[j][k] = mat[j][k] & mat[j - 1][k];
}
}
}
return ans;
}
}
希望这篇题解能让你看懂,喜欢的留下你宝贵的会心一赞吧,刷题之路,我们绝不言败!ヾ(◍°∇°◍)ノ゙
5454. 统计全 1 子矩形
解题思路
矩阵里每个点(i.j)统计他这行左边到他这个位置最多有几个连续的1,存为left[i][j]。然后对于每个点(i.j),我们固定子矩形的右下角为(i.j),利用left从该行i向上寻找子矩阵左上角为第k行的矩阵个数。每次将子矩阵个数加到答案中即可。
时间复杂度O(nnm),空间复杂度O(nm)。
代码
###cpp
class Solution {
public:
int numSubmat(vector<vector<int>>& mat) {
int n = mat.size();
int m = mat[0].size();
vector<vector<int> > left(n,vector<int>(m));
int now = 0;
for(int i=0;i<n;i++){
now = 0;
for(int j=0;j<m;j++){
if(mat[i][j] == 1) now ++;
else now = 0;
left[i][j] = now;
}
}
int ans = 0,minx;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
minx = 0x3f3f3f3f;
for(int k=i;k>=0;k--){
minx = min(left[k][j],minx);
ans += minx;
}
}
}
return ans;
}
};
零一开源|前沿技术周刊 #12
前沿技术周刊 是一份专注于技术生态的周刊,每周更新。本周刊深入挖掘高质量技术内容,为开发者提供持续的知识更新与技术洞察。
大厂在做什么
码圈新闻
深度技术
新技术介绍
博客推荐
- Android: 适配 16KB 页面大小:提升应用性能并为用户提供更流畅的应用体验
- Android: 深入浅出Android的Context机制
- HarmonyOS: 鸿蒙ArkWeb加载优化方案详解
GitHub 一周推荐
- 其他: Qwen-Image
关于我们
【零一开源】 是一个 文章 和 开源项目 的分享站,有写博客或开源项目的也欢迎来提供投递。 每周会搜集、整理当前的新技术、新文章,欢迎大家订阅。
RunLoop 实现原理
🔗 原文:RunLoop 实现原理 - 李峰峰博客
1、RunLoop 的概念
我们都知道,APP 运行过程中有一个很重要的线程,就是主线程。但是,一般线程执行完任务后就会退出,而 APP 需要持续运行,所以就需要一个机制使主线程持续运行并随时处理用户事件,在 iOS 里,程序的持续运行就是通过 RunLoop 实现的。
RunLoop 的作用:
-
保持程序持续运行; 程序一启动就会开启一个主线程,主线程开启之后会自动运行一个主线程对应的 RunLoop,RunLoop 保证主线程不会被销毁,也就保证了程序的持续运行;
-
处理 App 中的各种事件 比如:触摸事件,定时器事件等;
-
节省 CPU 资源,提高程序性能 程序运行起来时,当什么操作都没有做的时候,RunLoop 就告诉 CPU,现在没有事情做,我要去休息,这时 CPU 就会将其资源释放出来去做其他的事情,当有事情做的时候 RunLoop 就会立马起来去做事情;
在 iOS 中,NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,CFRunLoopRef 这些 API 都是线程安全的,Apple 在其文档上对 NSRunLoop 非线程安全的提示:
通常不将 RunLoop 类视为线程安全的,并且只能在当前线程的上下文中调用其方法。永远不要尝试调用在不同线程中运行的 RunLoop 对象的方法,因为这样做可能会导致意外的结果。
CFRunLoopRef 是开源的,源码下载地址:opensource.apple.com/tarballs/CF…
为了源码阅读更容易,便于理解 RunLoop 代码关键逻辑,本文所贴出的源码部分有删减,只留下了关键部分。
2、RunLoop 的数据结构
在 CoreFoundation 中,RunLoop 主要有 5 个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
(1) CFRunLoopRef
NSRunLoop 是基于 CFRunLoopRef
封装的,提供了面向对象的 API,接下来看下 NSRunLoop(即 CFRunLoopRef
)的数据结构:
typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop
{
// ......
// runloop 所对应线程
pthread_t _pthread;
// 存放所有标记为 common 的 mode
CFMutableSetRef _commonModes;
// 存放 common mode item 的集合(source、timer、observer)
CFMutableSetRef _commonModeItems;
// 当前所在 mode
CFRunLoopModeRef _currentMode;
// 存放 mode 的集合
CFMutableSetRef _modes;
// ......
};
根据以上源码可知,RunLoop 也是一个结构体,即 __CFRunLoop
,并且可以看到其中几个关键的成员变量:
-
_commonModes RunLoop 的内容发生变化时,RunLoop 会自动将
_commonModeItems
里的 Source/Observer/Timer 同步到_commonModes
中所有Mode
里。主线程的 RunLoop 中kCFRunLoopDefaultMode
和UITrackingRunLoopMode
都已经被标记为“Common”。 我们可以通过下面方法把一个Mode
标记为 “Common”:CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
-
_commonModeItems 添加 mode item 的时候,如果
modeName
传入NSRunLoopCommonModes
,则该 mode item 会被保存到 RunLoop 的_commonModeItems
中,例如:[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
-
_currentMode runloop 当前所在
mode
-
_modes 存放
mode
的集合
也就是说,RunLoop 可以有多个 mode(CFRunLoopModeRef) 对象,但是同一时间只能运行某一种特定的 Mode。
CFRunLoop 对外暴露的管理 Mode 接口只有下面 2 个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
Mode 暴露的管理 mode item 的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop 会自动创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。
(2) CFRunLoopModeRef
CFRunLoopModeRef
其实是指向 __CFRunLoopMode
结构体的指针,其源码如下:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode
{
// ...
// mode 的名称
CFStringRef _name;
// mode 是否停止
Boolean _stopped;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
// ...
};
从以上源码可知,每个 mode 对象中,可以存储多个 source、observer、timer(source/observer/timer 被统称为 mode item)。
系统默认注册了 5 个 Mode:
-
kCFRunLoopDefaultMode App的默认 Mode,通常主线程是在这个 Mode 下运行的,是大多数操作使用的模式。一般情况下,使用此模式来启动运行循环并配置输入源。
-
UIInitializationRunLoopMode 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
-
UITrackingRunLoopMode 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
-
GSEventReceiveRunLoopMode 接受系统事件的内部 Mode。
-
kCFRunLoopCommonModes 这是一个占位用的 Mode,作为标记
kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
用,并不是一种真正的 Mode
(3) CFRunLoopSourceRef
CFRunLoopSourceRef
是事件源(输入源)。其分为 source0
和 source1
:
-
source0 非基于 port 的,接收点击事件,触摸事件等 APP 内部事件,也就是用户触发的事件。这种 source 是不能主动唤醒 RunLoop 的。 使用时,需要先调用 :
CFRunLoopSourceSignal(source)
将这个 Source 标记为待处理,然后再调用:CFRunLoopWakeUp(runloop)
来主动唤醒 RunLoop,让其处理这个事件。 -
source1 基于 Port 的,能主动唤醒 RunLoop,通过内核和其他线程通信,接收分发系统事件;触摸硬件,通过 Source1 接收和分发系统事件到 Source0 处理。
关于 Port 内容后文会进行总结。
CFRunLoopSourceRef
源码如下:
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
struct __CFRunLoopSource
{
// ...
// source 的优先级,值为小,优先级越高
CFIndex _order; /* immutable */
// runloop 集合
CFMutableBagRef _runLoops;
// 联合体,表示 source 是 source0 还是 source1
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
// ...
};
(4) CFRunLoopObserverRef
CFRunLoopObserverRef
是观察者,每个 Observer
都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
CFRunLoopObserverRef 源码如下:
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver
{
// ...
// observer 对应的 runloop
CFRunLoopRef _runLoop;
// observer 观察了多少个 runloop
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
// observer 优先级
CFIndex _order; /* immutable */
// observer 回调函数
CFRunLoopObserverCallBack _callout; /* immutable */
// ...
};
typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
例如,监听 RunLoop 的状态:
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
});
// 把 observer 添加到 RunLoop 中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
(5) CFRunLoopTimerRef
CFRunLoopTimerRef
是基于时间的触发器,我们常用的 NSTimer
其实就是 CFRunLoopTimerRef
,他们之间是 toll-free bridged 的,可以相互转换。其包含一个时间长度和一个回调(函数指针)。
当其加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行那个回调。
总结:
一个 RunLoop 中,只能对应一个线程,但是可以包含多个 Mode,每个 mode,可以包含多个 source、observer、timer,其关系如下:
- RunLoop 启动时只能选择其中一个 Mode,作为 currentMode。
- 如果需要切换 Mode,只能退出当前 Loop,再重新选择一个 Mode 进入。
- 不同 Mode 的 Source0/Source1/Timer/Observer 能分隔开来,互不影响。
- 如果 Mode 里没有任何 Source0/Source1/Timer/Observer,RunLoop 会立马退出。
3、RunLoop 的执行流程
当 APP 没有任何任务的时候,RunLoop 会进入休眠,RunLoop 就告诉 CPU,现在没有事情做,我要去休息,这时 CPU 就会将其资源释放出来去做其他的事情。当下次有任务的时候,例如用户点击了屏幕,RunLoop 就会结束休眠开始处理用户的点击事件。所以,为了看到 RunLoop 执行流程,可以在点击事件里加个断点,查看 RunLoop 相关的方法调用栈:
根据上图发现,分析 RunLoop 执行流程,可以从 CFRunLoopRunSpecific
、__CFRunLoopRun
函数入手,而对 CFRunLoopRunSpecific
函数的调用,可以在源码中找到,是在 CFRunLoopRun
函数中,源码如下:
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
由以上源码可知:
- 默认底层是通过
CFRunLoopRun
开启 RunLoop 的,并且超时时间设置的非常大:1.0e10,可以理解为不超时。 - 我们也可以通过
CFRunLoopRunInMode
函数设置自定义启动方式,可以自定义超时时间、mode。
然后进入 CFRunLoopRunSpecific
函数,这是 RunLoop 的核心逻辑:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
/// __CFRunLoopRun中 具体要做的事情
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
/// 11. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 4. 处理block
__CFRunLoopDoBlocks(rl, rlm);
// 5. 处理Source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
// 如果处理Source0的结果是true
if (sourceHandledThisLoop) {
// 再次处理block
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 6. 如果有Source1 (基于port) 处于ready状态,直接处理这个Source1然后跳转去处理消息。
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
// 如果有Source1, 就跳转到handle_msg
goto handle_msg;
}
// 7. 通知 Observers: RunLoop 的线程即将进入休眠(sleep)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
// 调用mach_msg等待接受mach_port的消息。线程将进入休眠, 等待别的消息来唤醒当前线程:
// 一个基于 port 的Source 的事件。
// 一个 Timer 到时间了
// RunLoop 自身的超时时间到了
// 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
__CFRunLoopUnsetSleeping(rl);
// 8. 通知Observers: 结束休眠, RunLoop的线程刚刚被唤醒了
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 收到消息,处理消息。
handle_msg:;
if (/* 被timer唤醒 */) {
// 01. 处理Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
} else if (/* 被gcd唤醒 */) {
// 02. 处理gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被Source1唤醒
// 处理Source1
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
}
// 9. 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 10. 设置返回值, 根据不同的结果, 处理不同操作
if (sourceHandledThisLoop && stopAfterHandle) {
// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (0 == retVal);
return retVal;
}
由以上源码可知,RunLoop 内部是一个 do-while 循环;当调用 CFRunLoopRun()
时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
RunLoop 执行流程可用下面这张图概括:
通过上面的执行流程可以发现,RunLoop 处理了很多次 Block,即调用了很多次
__CFRunLoopDoBlocks
,那这里处理的 Block 到底是什么 Block 呢?
前面提到了 __CFRunLoop
结构体中的一些常见成员,其实还有两个和 Block 相关的成员:
struct __CFRunLoop {
// ...
struct _block_item *_blocks_head; // 存放 CFRunLoopPerformBlock 函数添加的 Block 的双向链表的头指针
struct _block_item *_blocks_tail; // 存放 CFRunLoopPerformBlock 函数添加的 Block 的双向链表的尾指针
// ...
};
_blocks_head
和 _blocks_tail
就是用于存放 CFRunLoopPerformBlock
函数添加的 Block 的,可见 RunLoop 是将添加的 Block 任务保存在双向链表中的。
我们可以通过 CFRunLoopPerformBlock
将一个 Block 任务加入到 RunLoop:
void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void(block)( void));
可以看出添加 Block 任务的时候,是绑定到某个 runloop mode 的。调用上面的 api 之后,runloop 在执行的时候,会通过如下 API 执行对应 mode 中所有的 block:
__CFRunLoopDoBlocks(rl, rlm);
需要注意的是,CFRunLoopPerformBlock
不会主动唤醒 RunLoop,添加完 Block 之后可以使用 CFRunLoopWakeUp
来主动唤醒 RunLoop。
4、RunLoop 与线程的关系
CFRunLoop
是基于 pthread
来管理线程的,苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
。 这两个函数内部的逻辑大致如下:
// 获得当前线程的 RunLoop 对象,内部调用 _CFRunLoopGet0 函数
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
// 查看_CFRunLoopGet0方法
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
// 如果为空则t设置为主线程
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
// 如果不存在 RunLoop,则创建
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 根据传入的主线程获取主线程对应的 RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主线程 将主线程-key 和 RunLoop-Value 保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 从字典里面取 RunLoop:将线程作为 key 从字典里获取 RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果 RunLoop 为空,则创建一个新的 RunLoop,所以 RunLoop 会在第一次获取的时候创建
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 创建好之后,以线程为 key,RunLoop 为 value,一对一存储在字典中,下次获取的时候,则直接返回字典内的 RunLoop
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// do not release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
// 线程结束是销毁 loop
CFRelease(newLoop);
}
// 如果传入线程和当前线程相同
if (pthread_equal(t, pthread_self())) {
// 注册一个回调,当线程销毁时,顺便也销毁对应的 RunLoop
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
通过源码分析可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个 Dictionary 字典里。
所以我们创建子线程 RunLoop 时,只需在子线程中获取当前线程的 RunLoop 对象即可 [NSRunLoop currentRunLoop]
。如果不获取,那子线程就不会创建与之相关联的 RunLoop,并且只能在一个线程的内部获取其 RunLoop。
当通过调用 [NSRunLoop currentRunLoop]
方法获取 RunLoop 时,会先看一下字典里有没有子线程对应的 RunLoop,如果有则直接返回 RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。当线程结束时,RunLoop 会被销毁。
Runloop 与线程的关系总结:
- 每条线程都有唯一的一个与之对应的 RunLoop 对象;
- RunLoop 保存在一个全局的 Dictionary 里,线程作为 key,RunLoop 作为 value
- 调用
[NSRunLoop currentRunLoop]
方法获取 RunLoop 时,会先看一下字典里有没有子线程对应的 RunLoop,如果有则直接返回 RunLoop,如果没有则会创建一个,并将对应关系保存到字典里。 - 主线程的 RunLoop 已经自动创建好了,子线程的 RunLoop 需要主动创建;
- RunLoop 在第一次获取时创建,在线程结束时销毁;
5、Runloop 的启动与退出
(1) 创建 Runloop
无法直接创建 RunLoop,但是 RunLoop 在第一次获取时自动创建,获取 RunLoop:
Foundation
// 获得当前线程的 RunLoop 对象
[NSRunLoop currentRunLoop];
// 获得主线程的 RunLoop 对象
[NSRunLoop mainRunLoop];
Core Foundation
// 获得当前线程的 RunLoop 对象
CFRunLoopGetCurrent();
// 获得主线程的 RunLoop 对象
CFRunLoopGetMain();
(2) 启动 Runloop
Apple 把 Runloop 启动方式分成了三种:
- 无条件地(Unconditionally)
- 有时间限制(With a set time limit)
- 指定 Mode(In a particular mode)
这三种方式分别对应下面三个方法:
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
- 第 1 种方式,本质就是在
NSDefaultRunLoopMode
模式下无限循环调用runMode:beforeDate:
方法,在此期间会处理来自输入源的数据; - 第 2 种方式,本质也是在
NSDefaultRunLoopMode
模式下无限循环调用runMode:beforeDate:
方法,区别在于它达到指定的超时时间后就不会再调用,在此期间会处理来自输入源的数据。 - 第 3 种方式,Runloop 只会运行一次,达到指定超时时间或者第一个 input source 被处理,则 Runloop 就会退出,这个方法会阻塞当前线程,直到返回结果(YES:输入源被处理或者达到指定的超时值,NO:没有启动成功)。
(3) 退出 Runloop
相较于 Runloop 的启动,它的退出就比较简单了,只有两种方法:
- 设置超时时间
- 手动结束
针对前面提到的第 2、3 中启动方式,可以直接设置超时时间控制退出。如果想要手动退出,可以使用下面函数,其参数就是 Runloop 对象:
void CFRunLoopStop(CFRunLoopRef rl)
但是 Apple 文档中在介绍利用 CFRunLoopStop()
手动退出时提到:
The difference is that you can use this technique on run loops you started unconditionally.
这里的解释非常容易产生误会,如果在阅读时没有注意到 exit 和 terminate 的微小差异就很容易掉进坑里,因为在 run 方法的文档中还有这句话:
If you want the run loop to terminate, you shouldn't use this method
也就是说,前面三种 Runloop 启动方式,对应退出方式如下:
-
run 无法退出
-
runUntilDate: 只能通过设置超时时间进行退出
-
runMode:beforeDate: 可以通过设置超时时间或者使用
CFRunLoopStop
方法来退出
CFRunLoopStop()
函数只会结束当前的 runMode:beforeDate:
调用,而不会结束后续的调用,这也就是为什么 Runloop 的文档中说 CFRunLoopStop()
可以 exit(退出) 一个 Runloop,而在 run 等方法的文档中又说这样会导致 Runloop 无法 terminate(终结)。
如果既让 Runloop 长时间运行,又要在必要时刻手动退出 Runloop,Apple 官方文档提供了推荐方式:
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
在对应线程中通过如下逻辑退出 Runloop:
shouldKeepRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
6、RunLoop 的底层实现
(1) RunLoop 与 mach port
Apple 将 iOS 系统大致划分为下面 4 个层次:
Darwin 的架构如下:
Darwin 是 macOS 和 iOS 操作环境的操作系统部分,Darwin 是一种类 Unix 操作系统(即 Unix 衍生出的系统,在一定程度上继承了原始 Unix 特性),Darwin 的内核是 XNU,XNU 是 Apple 开发的用于 macOS、iOS、tvOS、watchOS 操作系统的内核,XNU 是 X is Not Unix 的缩写。它是一个宏内核 BSD 与微内核 Mach 混合内核,以期将两者的特性兼收并蓄,同时拥有两种内核的优点。
关于 iOS 系统架构相关更多内容,可以看下我的这篇博客:《深入解析 iOS 系统架构》
Mach: Mach 是一个由卡内基梅隆大学开发的计算机操作系统微内核,Mach 核心之上可平行运行多个操作系统,XNU 内核以一个被深度定制的 Mach 内核作为基础。Mach 提供了诸如处理器调度、IPC (进程间通信)等少量且不可或缺的基础 API。在 Mach 中,所有东西都是“对象”,进程(在 Mach 中称为任务)、线程和虚拟内存都是对象。但是,在 Mach 架构中,对象间不能相互调用,对象间通信只能通过消息传递。“消息”是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。
BSD: XNU 中的 BSD 代码来自 FreeBSD 内核,FreeBSD 是一种开放源代码的类 Unix 的操作系统,基于 BSD Unix 的源代码派生发展而来。BSD 层确保了 Darwin 系统的 UNIX 特性,真正的内核是 Mach,但是对外部隐藏。BSD 提供了更高层次的抽象 API,例如:基于 Mach 的任务之上的 Unix 进程模型、文件系统、网络协议栈等相关 API。
I/O Kit: I/O Kit 为设备驱动提供了一个面向对象(C++)的一个框架,框架提供每种设备驱动需要的常见特性,以使驱动程序可以用更少的时间和代码完成。
用户态与内核态: 内核控制着操作系统最核心的部分,为了防止应用程序崩溃而导致的内核崩溃,内核与应用程序之间需要进行严格的分离。基于软件的分离会产生巨大的开销,因此现代的操作系统都是依靠硬件来分离。分离的结果就是用户态与内核态。
用户态和内核态的切换有两种类型:
- 自愿转换:比如系统调用;
- 非自愿转换:当发生异常、中断或处理器陷阱的时候,代码的执行会被挂起,并且保留发生错误时候的完整状态。控制权被转交给预定义的内核态错误处理程序或中断服务程序。
在 XNU 中,系统调用有四种类别:
- BSD 系统调用
- Mach 陷阱
- 机器相关调用
- 诊断调用
Mach 消息的发送和接收都是通过同一个 API 函数 mach_msg()
进行的,这个函数在用户态和内核态都有实现。mach_msg()
函数调用了一个 Mach 陷阱(trap),在用户态调用 mach_msg_trap()
会引发陷阱机制,切换到内核态,在内核态中,内核实现的 mach_msg()
会完成实际的工作,如下图:
RunLoop 的核心就是基于 mach port 的,其进入休眠时调用的函数是
mach_msg()
,RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。
前面提到的 source1
就是基于 mach port 的,它用来接收系统事件。当对应系统事件发生后(例如用户点击了屏幕),最终会通过 mach port 将事件转发给需要的 App 进程。随后苹果注册的那个 source1
就会触发回调,RunLoop 被唤醒,APP 开始处理对应事件。
(2) RunLoop 输入源
Runloop 作为线程的入口用来响应传入事件,Runloop 从两种不同类型的源接收事件:
-
输入源(Input Source) 用于传输异步事件,通常是来自另一个线程或者其他程序的消息。输入源将异步事件传递给相应的处理程序,并调用 runUntilDate: 方法(在线程的关联 NSRunLoop 对象上调用)退出。
-
定时器源(Timer Source) 提供同步事件,预定的时间或者固定的时间间隔重复执行,计时器源将事件传递给其处理程序,但不会导致 Runloop 退出。
输入源(Input Source) 创建输入源时,可以将其分配给 Runloop 的一种或多种 mode。一般情况下应该在默认模式下运行 Runloop,但也可以指定自定义 mode。如果输入源不在当前监视的 mode 下,则它生成的任何事件都将保留,直到 Runloop 以正确的 mode 运行,输入源主要有:基于的端口的输入源、自定义输入源、Perform Selector 源。
基于的端口的输入源(Port-based Source)
监听应用程序的 Mach 端口,由内核自动发出信号,对应源码中的 source1
。
Cocoa 和 Core Foundation 都提供了创建基于的端口输入源相关的对象和函数,如果使用 Cocoa 提供的相关方法,不需要直接创建输入源,可以使用 NSPort
相关的方法来创建一个 Port
对象,并将该对象添加到 Runloop 中,该 Port 对象会负责创建和配置输入源。使用 Core Foundation 函数实现稍微复杂些,我们需要手动的创建 Port 和它的 Runloop 源。使用 CFMachPortRef
, CFMessagePortRef
, 或者 CFSocketRef
函数来创建适当地对象。
例如:
- (void)testsource1 {
// 声明两个端口
NSPort *mainPort = [NSMachPort port];
NSPort *threadPort = [NSMachPort port];
// 设置线程的端口的代理回调为自己
threadPort.delegate = self;
// 给主线程 Runloop 加一个端口
[[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 子线程
// 给子线程添加一个 Port,并运行子线程中的 Runloop
[[NSRunLoop currentRunLoop] addPort:threadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
// 2 秒后,从主线程向子线程发送一条消息
NSString *s1 = @"hello";
NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
// 发送一条消息
// 参数:date(发送时间)、msgid(消息标识)、components(发送消息附带参数)、reserved(预留参数,暂时用不到)
[threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];
});
}
// 这个 NSMachPort 收到消息的回调,注意这个参数,可以先给一个id
- (void)handlePortMessage:(id)message
{
NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);
NSArray *array = [message valueForKeyPath:@"components"];
NSData *data = array[1];
NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",s1);
}
打印结果:
RunLoopTest[10368:5612468] 收到消息了,线程为:<NSThread: 0x6000015acf80>{number = 6, name = (null)}
RunLoopTest[10368:5612468] hello
自定义输入源(Custom input Source)
监听自定义事件源,必须从另一个线程手动发信号通知自定义源,对应源码中的 source0
。
可以使用 CoreFoundation
中 CFRunLoopSourceRef
相关的函数来创建自定义输入源,可以使用多个回调函数配置自定义输入源,CoreFoundation 在必要时候调用这些函数来配置 source
,处理传入的事件,并在从 Runloop 中移除 source
时将其移除。
除此之外,还需要定义事件的传递机制,这部分是运行在单独的线程上,负责向输入源提供数据并在适当的时候发出信号,事件的传递机制可自行定义。
例如:
@implementation ViewController{
CFRunLoopRef _runLoopRef;
CFRunLoopSourceRef _source;
CFRunLoopSourceContext _source_context;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self testsource0];
}
- (void)testsource0 {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"starting thread.......");
_runLoopRef = CFRunLoopGetCurrent();
// 初始化_source_context。
bzero(&_source_context, sizeof(_source_context));
// 这里创建了一个基于事件的源,绑定了一个函数
_source_context.perform = fire;
//参数
_source_context.info = "hello";
// 创建一个source
_source = CFRunLoopSourceCreate(NULL, 0, &_source_context);
// 将 source 添加到当前 RunLoop 中去
CFRunLoopAddSource(_runLoopRef, _source, kCFRunLoopDefaultMode);
// 开启 RunLoop 第三个参数设置为 YES,执行完一次事件后返回
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 9999999, YES);
NSLog(@"end thread.......");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (CFRunLoopIsWaiting(_runLoopRef)) {
NSLog(@"RunLoop 正在等待事件输入");
// 添加输入事件
CFRunLoopSourceSignal(_source);
// 唤醒线程,线程唤醒后发现由事件需要处理,于是立即处理事件
CFRunLoopWakeUp(_runLoopRef);
}else {
NSLog(@"RunLoop 正在处理事件");
// 添加输入事件,当前正在处理一个事件,当前事件处理完成后,立即处理当前新输入的事件
CFRunLoopSourceSignal(_source);
}
});
}
static void fire(void* info) {
NSLog(@"我现在正在处理后台任务");
printf("%s",info);
}
@end
打印结果:
RunLoopTest[10630:5649707] starting thread.......
RunLoopTest[10630:5649457] RunLoop 正在等待事件输入
RunLoopTest[10630:5649707] 我现在正在处理后台任务
RunLoopTest[10630:5649707] end thread.......
Perform Selector 源(Cocoa Perform Selector Source) 除了基于端口的源外,Cocoa 还定义了一个自定义输入源,允许在任何线程上 Perfrom Selector。与基于端口的源一样,Perfrom Selector 请求在目标线程上序列化,缓解了在一个线程上运行多个方法时可能出现的许多同步问题。与基于端口的源不同的是,Perform Selector 源在 Perfrom Selector 后会从 Runloop 中删除自己。
在另一个线程上 Perfrom Selector 时,目标线程必须具有活动的 Runloop。这意味对于我们创建的子线程,需要显式创建 Runloop。由于主线程的 Runloop 是自动创建的,所以可以在 applicationDidFinishLaunching:
方法后随时 Perfrom Selector。Runloop 每次循环时,Runloop 都会处理所有的 Perfrom Selector 调用,而不是在每次循环时都只处理一个。
在其他线程上 Perfrom Selector 的相关方法:
// 在主线程的下一个 Runloop 周期中,执行指定的选择器。这些方法允许您选择阻塞当前线程,直到执行选择器结束。
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
// 对拥有 NSThread 对象的任何线程执行指定的选择器。这些方法允许您选择阻塞当前线程,直到执行选择器结束。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
// 在下一个 Runloop 周期和指定时长延迟(可选)后,在当前线程上执行指定的选择器。由于它要等到下一个 Runloop 周期来执行选择器,所以这些方法提供了一个来自当前执行代码的自动最小延迟,多个选择器时会按照顺序依次执行。
// performSelector:afterDelay: 方法内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
// 取消使用 performSelector:withObject:afterDelay: 或 performSelector:withObject:afterDelay:inModes: 发送的到当前线程的消息
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
计时器源(Timer Source) 定时器源在预定时间内同步地将事件传递给线程,定时器可以让线程在对应时刻通知自己执行一些事务,尽管定时器是基于时间的通知方式,但是并不是真的时间机制。就像输入源,定时器在 Runloop 中也是和特定 Mode 相关联的。如果定时器没有处在 Runloop 正在监视的 Mode 中的话,该定时器是不会触发的。必须要等到 Runloop 在定时器支持的 Mode 中运行时,该定时器才会正常运行。如果定时器被触发时机正好是在 Runloop 执行任务中,那么这个定时器源的相关事件只有在 Runloop 下一次运行循环时才能被执行。如果 Runloop 停止运行,那么该定时器源的事件将永远没办法执行。
反复执行的定时器会根据它的触发时间自动配置,并不是真实的触发时间。例如,一个定时器设置的是每 5 秒触发一次,在真实时间上可能是有点延迟的,如果真实时间的延迟大于定时器触发时间的话,那么这次触发时机将被错过。
创建定时器源,可以使用下面两个方法:
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:
上面这两个方法创建了定时器并添加到当前线程的默认 mode (NSDefaultRunLoopMode
)中,但是也可以通过 NSRunLoop
的下面的实例方法来将 NSTimer
对象添加到其他 mode 中:
addTimer:forMode:
例如,下面两种实现方式效果是一样的:
/// 分开处理,我们可以通过更多的自定义方式来处理timer,比如添加到不同的NSDefaultRunLoopMode。
NSDate *futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer *myTimer = [[NSTimer alloc] initWithFireDate:futureDate interval:0.1 target:self selector:@selector(timedothing:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode];
/// 将创建和调度同时进行
[NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(timedothing:) userInfo:nil repeats:YES];
7、RunLoop 目前的应用
(1) AutoreleasePool
- RunLoop 的进入的时候会调用
objc_autoreleasePoolPush()
创建新的自动释放池。 - RunLoop 的进入休眠的时候会调用
objc_autoreleasePoolPop()
和objc_autoreleasePoolPush()
销毁自动释放池、创建一个新的自动释放池。 - RunLoop 即将退出时会调用
objc_autoreleasePoolPop()
释放自动自动释放池内对象。
(2) 事件响应
苹果注册了一个 Source1
(基于 mach port 的) 用来接收系统事件,当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent
事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等)、触摸、加速、接近传感器等几种 Event,随后用 mach port 转发给需要的 App 进程。随后苹果注册的那个 Source1
就会触发回调,并调用 _UIApplicationHandleEventQueue()
进行应用内部的分发。
_UIApplicationHandleEventQueue()
会把 IOHIDEvent
处理并包装成 UIEvent
进行处理或分发,其中包括识别 UIGesture
/处理屏幕旋转/发送给 UIWindow
等。通常事件比如 UIButton
点击、touchesBegin
/Move
/End
/Cancel
事件都是在这个回调中完成的。
(3) 手势识别
当上面的 _UIApplicationHandleEventQueue()
识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin
/Move
/End
系列回调打断。随后系统将对应的 UIGestureRecognizer
标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting
(Loop 即将进入休眠) 事件,这个 Observe r的回调函数是 _UIGestureRecognizerUpdateObserver()
,其内部会获取所有刚被标记为待处理的 GestureRecognizer
,并执行 GestureRecognizer
的回调。
当有 UIGestureRecognizer
的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
(4) 界面更新
当在操作 UI 时,比如改变了 Frame、更新了 UIView
/CALayer
的层次时,或者手动调用了 UIView/CALayer
的 setNeedsLayout
/setNeedsDisplay
方法后,这个 UIView
/CALayer
就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting
(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
。这个函数里会遍历所有待处理的 UIView
/CAlayer
以执行实际的绘制和调整,并更新 UI 界面。
这个函数内部的调用栈大概是这样的:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
(5) 定时器
NSTimer
其实就是 CFRunLoopTimerRef
,他们之间是 toll-free bridged 的。一个 NSTimer
注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop 为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
(6) PerformSelector
当调用 NSObject
的 performSelecter:afterDelay:
后,实际上其内部会创建一个 Timer
并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread:
时,实际上其会创建一个 Timer
加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
performSelector:withObject:
只是发消息,不会有 Timer ,所以不会有上面的问题,在子线程调用,不需要开启 Runloop
(7) 关于 GCD
根据前面 RunLoop 的执行流程可以知道,GCD 也是可以唤醒 RunLoop 的,GCD 由子线程返回到 主线程,只有在这种情况下才会触发 RunLoop,会触发 RunLoop 的 Source 1
事件:
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"main queue");
});
当调用 dispatch_async(dispatch_get_main_queue(), block)
时,libDispatch
会向主线程的 RunLoop 发送消息,RunLoop 会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE()
里执行这个 block。但这个逻辑仅限于 dispatch
到主线程,dispatch
到其他线程仍然是由 libDispatch
处理的。
(8) 关于网络请求
iOS 中,关于网络请求的接口有如下几层: CFSocket
是最底层的接口,只负责 socket 通信。
-
CFNetwork
是基于CFSocket
等接口的上层封装,ASIHttpRequest
工作于这一层。 -
NSURLConnection
是基于CFNetwork
的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
+ NSURLSession
是 iOS7 中新增的接口,表面上是和 NSURLConnection
并列的,但底层仍然用到了 NSURLConnection
的部分功能 (比如 com.apple.NSURLConnectionLoader
线程),AFNetworking 2 和 Alamofire 工作于这一层。
下面主要介绍下 NSURLConnection
的工作过程。
通常使用 NSURLConnection
时,你会传入一个 Delegate
,当调用了 [connection start]
后,这个 Delegate
就会不停收到事件回调。实际上,start
这个函数的内部会会获取 CurrentRunLoop
,然后在其中的 DefaultMode
添加了 4 个 Source0
(即需要手动触发的 Source)。其中 CFMultiplexerSource
是负责各种 Delegate
回调的,CFHTTPCookieStorage
是处理各种 Cookie
的。
当开始网络传输时,我们可以看到 NSURLConnection
创建了两个新线程:com.apple.CFSocket.private
和 com.apple.NSURLConnectionLoader
。其中 CFSocket
线程是处理底层 socket 连接的。NSURLConnectionLoader
这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0
通知到上层的 Delegate
。
NSURLConnectionLoader
中的 RunLoop 接收来自底层 CFSocket
的 Source1
通知。当收到通知后,在合适的时机向 Delegate
线程 RunLoop 发送 CFMultiplexerSource
等 Source0
通知,同时唤醒 Delegate
线程的 RunLoop 来让其处理这些通知。接收到 CFMultiplexerSource
通知后,Delegate 线程的 RunLoop 执行对应 Delegate
回调。
8、Runloop 开发中的使用场景
唯一需要显式运行 Runloop 是在创建子线程时。主线程的 Runloop 已自动创建并运行。对于子线程,需要自行判断是否需要 Runloop,如果需要,则开发者自行创建。例如,下列操作需要启动 Runloop:
- 使用端口或自定义输入源与其他线程通信。
- 在子线程上使用计时器。
- 调用 performSelector… 相关方法。
- 线程保活,以执行周期性任务。
以下是在实际开发是,Runloop 的一些使用场景:
(1) 线程保活
当子线程中的任务执行完毕后,线程就被会被立刻销毁。如果 APP 中需要经常在子线程中执行任务,频繁的创建和销毁线程,会造成资源的浪费,这时候我们就可以使用 Runloop 来让该线程长时间存活而不被销毁,实现如下:
KeepAliveThread.h
typedef void (^KeepAliveThreadTask)(void);
@interface KeepAliveThread : NSObject
// 在子线程执行任务
- (void)executeTask:(KeepAliveThreadTask)task;
// 结束线程
- (void)stop;
@end
KeepAliveThread.m
@interface KeepAliveThread()
@property (nonatomic, strong) NSThread *thread;
@property (nonatomic, assign) BOOL shouldKeepRunning;
@end
@implementation KeepAliveThread
#pragma mark - Public methods
- (instancetype)init {
if (self = [super init]) {
self.shouldKeepRunning = YES;
__weak typeof(self) weakSelf = self;
self.thread = [[NSThread alloc] initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && weakSelf.shouldKeepRunning) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];
[self.thread start];
}
return self;
}
- (void)executeTask:(KeepAliveThreadTask)task {
if (!self.thread || !task) return;
[self performSelector:@selector(p_executeTask:) onThread:self.thread withObject:task waitUntilDone:NO];
}
- (void)stop {
if (!self.thread) return;
[self performSelector:@selector(p_stop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)dealloc {
[self stop];
}
#pragma mark - Private methods
- (void)p_stop {
self.shouldKeepRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
}
- (void)p_executeTask:(KeepAliveThreadTask)task {
task();
}
@end
(2) 保证 Timer 正常运行
创建 Timer 有下面两种方式,两种实现方式是等价的:
// 方式 1
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[timer fire];
// 方式 2
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
当滑动 UIScrollView
时,主线程的 RunLoop 会切换到 UITrackingRunLoopMode
这个 Mode,执行的也是 UITrackingRunLoopMode
下的任务(Mode 中的 item),而 Timer 是添加在 NSDefaultRunLoopMode
下的,所以 Timer 任务并不会执行,只有当 UITrackingRunLoopMode
的任务执行完毕,Runloop 切换到 NSDefaultRunLoopMode
后,才会继续执行 Timer。解决方法很简单,我们只需要在添加 Timer 时,将 mode 设置为 NSRunLoopCommonModes
即可:
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[timer fire];
如果是在子线程中使用 Timer,由于子线程的 Runloop 并不会自动创建,所以必须在子线程中创建并启动 Runloop,否则 Timer 无法正常运行,创建并启动 Runloop 方法:
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
由于子线程中不会涉及到 UI 更新,所以无需再主动将 Timer 添加到 NSRunLoopCommonModes
下。
(3) 利用 Runloop 优化 UITableView 加载图片时滑动卡顿问题
UITableView
滚动时,主线程的 Runloop 会切换到 UITrackingRunLoopMode
这个 Mode,我们可以在 NSDefaultRunLoopMode
中设置图片,避免一边滑动一边设置 image 引起的卡顿问题:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = ...
// ......
// 在 NSDefaultRunLoopMode 下设置图片
[self performSelector:@selector(p_loadImgeWithIndexPath:)
withObject:indexPath
afterDelay:0.0
inModes:@[NSDefaultRunLoopMode]];
// ......
return cell;
}
- (void)p_loadImgeWithIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
// 子线程下载图片
[ImageDownload loadImageWithUrl:@"xxxx" success:^(UIImage *image) {
// 回到主线程刷新UI
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = image;
});
}];
}
(4) 利用 Runloop 监控卡顿
根据 Runloop 的执行流程可以发现,Runloop 对我们业务逻辑的处理时间在两个阶段:
-
kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
之间 -
kCFRunLoopAfterWaiting
之后
所以,如果主线程 Runloop 处在 kCFRunLoopBeforeSources
时间过长,也就是迟迟无法将任务处理完成,顺利到达 kCFRunLoopBeforeWaiting
阶段,说明发生了卡顿。
同样的,如果 Runloop 处在 kCFRunLoopAfterWaiting
时间过长,也是发生了卡顿。
所以,如果我们要利用 Runloop 来监控卡顿的话,就要关注 kCFRunLoopBeforeSources
和 kCFRunLoopAfterWaiting
两个阶段,一般卡顿时间超过 250ms 会被明显感知,所以,可以以连续 5 次卡顿时长超过 50ms 可以认为发生卡顿,或者根据需要调整统计阀值。以下是通过 Runloop 监听卡顿的一个例子:
@interface LagMonitor() {
int timeoutCount;
CFRunLoopObserverRef runLoopObserver;
@public
dispatch_semaphore_t dispatchSemaphore;
CFRunLoopActivity runLoopActivity;
}
@end
@implementation LagMonitor
#pragma mark - Interface
+ (instancetype)shareInstance {
static id instance = nil;
static dispatch_once_t dispatchOnce;
dispatch_once(&dispatchOnce, ^{
instance = [[self alloc] init];
});
return instance;
}
// 开始监控
- (void)beginMonitor {
NSLog(@"开始监控");
if (runLoopObserver) {
return;
}
// 创建信号量,注意这里信号量为 0
dispatchSemaphore = dispatch_semaphore_create(0);
// 创建 Observer
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
runLoopObserverCallBack,
&context);
// 将 Observer 添加到主线程的 RunLoop
CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
// 在子线程持续监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES) {
/**
信号量 P 操作,成功的话信号量会 - 1,这里超时时间为 50ms,即等待 50ms 后还没成功就返回失败
操作成功,信号量 - 1,返回值为 0;操作失败,返回值非 0
由于初始信号量为 0,这里会阻塞,直到 runLoopObserverCallBack 函数中对信号量做了 V 操作,即 RunLoop 状态发生改变的时候。
*/
long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC));
if (semaphoreWait != 0) {
// 发生超时,说明 RunLoop 保持在一个状态的时间超过了 50ms
if (!self->runLoopObserver) {
self->timeoutCount = 0;
self->dispatchSemaphore = 0;
self->runLoopActivity = 0;
return;
}
// 如果是在 BeforeSources 或 AfterWaiting 这两个状态持续时间达到超时时间,就代表发生了卡顿
if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
// 出现五次出结果
if (++self->timeoutCount < 5) {
continue;
}
// 发生了卡顿,可以使用 PLCrashReporter 等收集堆栈并上报
NSLog(@"发生了卡顿,");
}
}
self->timeoutCount = 0;
}
});
}
// 结束监控
- (void)endMonitor {
NSLog(@"结束监控");
if (!runLoopObserver) {
return;
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
// 释放 Observer
CFRelease(runLoopObserver);
runLoopObserver = NULL;
}
#pragma mark - Private
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
LagMonitor *lagMonitor = (__bridge LagMonitor*)info;
lagMonitor->runLoopActivity = activity;
dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
// 对信号量进行 V 操作,信号量 + 1
dispatch_semaphore_signal(semaphore);
}
@end
上面只是统计卡顿的基础版本,如果真的使用到项目中上面逻辑还有不少需要优化的地方,例如:
- 避免多次重复上报同一个卡顿堆栈
- 可以先将堆栈保存到内存中,以堆栈栈顶函数为特征,如果相同认为整个堆栈是同一个,不重复上报。
- 准确定位真正卡顿的堆栈
- 假如主线程有三个任务,只有第一个是引起卡顿的任务,当开始上报卡顿时获取到的堆栈可能是后两个不耗时的任务的堆栈。这种情况可以每 50ms 甚至更短时间获取一次堆栈,只保留最近一定数量(例如最近 20 个)堆栈信息,当发生卡顿时相同堆栈数量最多的堆栈就是真正引起卡顿的堆栈。
目前也有一些比较成熟的卡顿监控方案,例如:matrix。
🔗 原文链接:
RunLoop 实现原理 - 李峰峰博客
📢 作者声明:
转载请注明来源