阅读视图

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

从开放平台到受控生态:谷歌宣布 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)的时间复杂度。

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.jsresolve.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插件包,然后将渲染模式属性设置为单通道实例化。


docs.unity3d.com/Packages/co…


【从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}]$ 中的所有正整数的执行次数之和的方法如下。

  1. 用 $\textit{currReductions}$ 表示当前执行次数,用 $\textit{start}$ 表示执行次数是 $\textit{currReductions}$ 的最小正整数,初始时 $\textit{currReductions} = 1$,$\textit{start} = 1$。

  2. 对于每个 $\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$。理由如下。

  1. 对于正整数 $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$)。

  2. 根据 $r(x)$ 的性质,新数组 $\textit{reductionsArr}$ 为单调递增数组且任意两个相邻元素之差等于 $0$ 或 $1$。每次从新数组 $\textit{reductionsArr}$ 中选择最大的两个元素分别减少 $1$,则可以经过若干次操作将所有的最大元素都减少 $1$ 且最多有一个次大元素减少 $1$。

  3. 经过若干次操作之后,一定可以将新数组 $\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 image.png

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版本 image.png

4.解决方案

方案一:修改编译配置

在构建工具中定义这个变量,让其存在并被设置为 false(即在生产环境禁用 hydration 不匹配详情)。

  1. 使用vue-cli image.png
  2. 使用vite image.png

方案二:升级 Vue

检查 package.json,将 Vue 及其相关依赖(@vue/compiler-sfcvue-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 在特定场景下的妙用。

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

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

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


大厂在做什么

美团智能头盔作为专为外卖骑手打造的智能安全装备,具备蓝牙通话、戴盔识别、智能语音助手、碰撞摔倒监控等功能,核心软件功能围绕如何通过主动安全和被动安全相结合的方式有效保护骑手。 本期分享主要介绍智能头盔骑行通话质量、智能语音助手、碰撞摔倒监控三项软件能力。其中“骑行通话质量和智能语音助手”降低骑手操作手机导致的“分心”,帮助骑手“防患于未然”。“碰撞摔倒监控”最大限度的保护骑手、快速的感知事故和触发救治。
在数字内容井喷的时代,移动端已成为视频创作的重要阵地,而视频编辑页作为创作工具的核心场景,不仅为创作者提供了丰富的表达手段和创意平台,更是提升视频制作的效率。通过直观的操作界面和丰富的功能集成,用户可以轻松地将素材、音频、特效及文字等进行融合,创造出独具风格、彰显个性的作品。
如今,AI 编程工具正在重塑软件开发,其核心目标直指“开发民主化”。它们不再仅仅是补全代码片段的助手,而是能理解自然语言需求、生成可运行代码框架、甚至参与系统设计的“协作者”。这一背景下,越来越多的企业开始对外发布相关产品,美团便是其中之一。

新技术介绍

迄今为止最大的Compose更新带来了原生自动填充, 智能动画以及让构建Android用户界面如同魔法般轻松的功能
兄弟,你发的这篇Flutter 3.35更新的文章内容好像有点小状况啊——页面显示“环境异常”,得先验证才能看具体内容。我这刷了半天,也没瞅见更新了啥新特性、优化了哪些性能。要不你先去把验证搞定,把正经的更新内容放出来?等内容齐了,我再帮你扒拉扒拉这版3.35到底香不香~

深度技术

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

码圈新闻

这两天在上海世博展览馆举行的 2025 世界人工智能大会(WAIC)热度相当高,上到央媒下到朋友圈不断看到,甚至总理李强、双奖(诺贝尔/图灵)得主辛顿都在开幕式出现,影响力爆表。 周末去逛了一天,AI 的落地场景之多令人咋舌,看完以后我给之前的好几个点子都划上了删除线。还是得多出来看看大厂/新秀公司都在做什么,避免做类似的事情。 这篇文章按照类别记录一下印象比较深刻的产品。
刚刷完2025 Google开发者大会的客户端内容,给咱3年+的老哥们捋捋重点。 Android 15是重头戏:后台任务管理收紧了,得注意`WorkManager`新的电量阈值限制,不然应用可能被系统强杀;UI渲染加了硬件加速新接口,复杂列表滑动能再提10-15帧,对电商、社交类应用挺香。 开发工具方面,Android Studio Hedgehog直接集成了AI代码诊断,写`Compose`时会自动提示重组优化点,试了下比之前手动查省事儿多了。Flutter 4.0也放了大招,原生代码互调延迟降了40%,混编项目终于不用再纠结性能损耗了。 哦对了,跨平台布局`Jetpack Multiwindow`支持更完善了,平板/折叠屏适配能少写一半适配代码。暂时就这些干货,后台优化和Flutter新特性建议优先上手,其他的可以先放收藏夹吃灰~

博客推荐

兄弟,你给的这篇文章内容好像有点问题啊。标题写着《适配 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 一周推荐


关于我们

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

[奸笑]

未来将至:人形机器人运动会 - 肘子的 Swift 周报 #99

不久前在北京举办的世界人形机器人运动会上,出现了许多令人忍俊不禁的场景:机器人对着空气挥拳、跑步时左摇右摆、踢球时相互碰撞后集体倒地。尽管这些画面看起来颇为滑稽,但回顾过去几年人形机器人的发展历程就会发现,即便当前的产品仍存在诸多不足,其进步却是惊人的。按照这样的发展速度,也许在十年甚至更短的时间内,人形机器人就将走进我们的日常生活,满足各种实际需求。

(多图预警!!!) 边统计边压缩,给你超详细的解答

image.png

写在文首

看了很多题解,大家大致的思路基本一致,时间复杂度为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. 当有连续三个1的时候,用now分别递增为1,2,3(第一个1可以形成一个矩形,第二个1和第一个1可以形成1个矩形加上其自身一共是两个,第三个1分别可以和前面连续的1形成两个长度分别为3,2的矩形,加上自身一共三个),并加到ans中
  2. 这一步也可以记录连续的个数,使用等差数列求和公式计算连续1处可以形成的矩形数目,具体代码不展示
  3. 当遍历到mat[j][k] == 0时,即1不连续了,置now为0,以便后面遇到连续1时进行统计

(2)压缩

  1. 理论上,纵向的矩形也可以像横向那样进行统计,但是我们注意到我们不仅要求横向和纵向的矩形,还要求诸如2x2之类的
  2. 在(1)中我们得到所有大小为1的矩形和横向的各种大小的矩形的情况,所以后面我们只需要求纵向的大小大于1的矩形的情况即可
  3. 所以在这我们选择向下"压缩"题目所给二维数组,以便继续统计,这里"压缩"的思路理解好下次遇到类似的题能想出来就行,来看图吧

起始的二维数组
image.png

对其使用“按位与”进行压缩
image.png

第二次统计的结果
image.png

思考:这里我们可以发现第二次的统计是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

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

订阅渠道:【零一开源】、 【掘金】、 【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 一周推荐

阿里开源最新文生图模型

关于我们

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

[奸笑]

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 中 kCFRunLoopDefaultModeUITrackingRunLoopMode 都已经被标记为“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 是事件源(输入源)。其分为 source0source1

  • 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。 可以使用 CoreFoundationCFRunLoopSourceRef 相关的函数来创建自定义输入源,可以使用多个回调函数配置自定义输入源,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/CALayersetNeedsLayout/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

当调用 NSObjectperformSelecter: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.privatecom.apple.NSURLConnectionLoader。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 DelegateNSURLConnectionLoader 中的 RunLoop 接收来自底层 CFSocketSource1 通知。当收到通知后,在合适的时机向 Delegate 线程 RunLoop 发送 CFMultiplexerSourceSource0 通知,同时唤醒 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 对我们业务逻辑的处理时间在两个阶段:

  • kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting 之间
  • kCFRunLoopAfterWaiting 之后

所以,如果主线程 Runloop 处在 kCFRunLoopBeforeSources 时间过长,也就是迟迟无法将任务处理完成,顺利到达 kCFRunLoopBeforeWaiting 阶段,说明发生了卡顿。

同样的,如果 Runloop 处在 kCFRunLoopAfterWaiting 时间过长,也是发生了卡顿。

所以,如果我们要利用 Runloop 来监控卡顿的话,就要关注 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting 两个阶段,一般卡顿时间超过 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 实现原理 - 李峰峰博客
📢 作者声明:
转载请注明来源

我差点失去了巴顿(我的狗狗) - 肘子的 Swift 周报 #98

巴顿已经 13 岁了。尽管大多数时候他都表现出远超同龄狗狗的活力和状态,但随着年龄增长,各种健康问题也随之而来。不久前,巴顿被检查出肺动脉高压,医生给出了针对性的治疗方案。就在我为治疗似乎初见成效而欣慰时,上周一下午,巴顿突然无法站立,大量流口水,表现出明显的心脏不适。

❌