高温与奇怪的天象 - 肘子的 Swift 周报 #92
从 6 月中开始,我所在的城市也迎来了罕见的高温天气。尽管相较于动辄 35-40 多摄氏度的其他地区,30-31 摄氏度在数字上看起来并不夸张,但对于习惯了 20 几度的我来说,这种温度已经很难熬了。
从 6 月中开始,我所在的城市也迎来了罕见的高温天气。尽管相较于动辄 35-40 多摄氏度的其他地区,30-31 摄氏度在数字上看起来并不夸张,但对于习惯了 20 几度的我来说,这种温度已经很难熬了。
SDWebImage 是iOS 开发最流行的异步图片加载框架, 其中的缓存模块 SDImageCache
有一个很精巧的设计弱引用缓存, 源码参考
1 |
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; // strong-weak cache |
在我们的项目中引入了这个小修改, 整体的缓存命中率有1%左右的提升,
是否开启 | 内存缓存命中率 | 磁盘缓存命中率 | 下载命中率 |
---|---|---|---|
开启弱引用缓存 | 67.99% | 21.36% | 10.67% |
不开启弱引用缓存 | 66.76% | 21.99% | 11.25% |
这里每天分享一个 iOS 的新知识,快来关注我吧
作为 App 开发来说,字体在应用中扮演着至关重要的角色。一个精心选择的字体能够让你的应用在众多竞争者中脱颖而出。
但是,无论你选择哪种字体,都必须确保它的核心功能——可读性。
在以前,只有苹果自带的系统字体支持动态调整大小,而自定义字体则不支持。但自从 iOS 11 以来,这种情况已经改变。现在,你可以轻松地在动态字体中使用你的自定义字体。
今天就来讲讲如何在动态字体中使用自定义字体。
苹果早在 iOS 7 时就引入了动态字体,旨在让用户选择他们偏好的文本大小以满足自身需求。
在较大的文本尺寸下,各种文本样式(如 .headline
, .subheadline
, .body
, .footnote
, .caption1
, .caption2
, .largeTitle
, .title1
, .title2
, .title3
和 .callout
)的权重、大小和行距值可以参考苹果的人机界面指南 - 动态字体尺寸[1]。
动态字体与文本样式一起起作用,文本样式用于为每种文本大小设定缩放因子。例如,.caption2
是最小的文本样式,不会缩小到小于 11 号的大小,因为那样会很难阅读。在最小、小、中和大文本大小下,.caption2
文本样式的大小将保持在 11pt。
要获取动态字体,我们可以使用 UIFont 类方法 preferredFont(forTextStyle:)
来初始化字体。
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true
设置 adjustsFontForContentSizeCategory
为 true,可以在设备的内容大小类别更改时自动更新字体。
上面的代码将返回一个苹果 San Francisco 常规字体,大小为 17(大文本尺寸下的 body 样式),以下是大文本大小上的所有文本样式的示例。
可以通过以下方式更改字体大小:
打开系统设置 - 显示与亮度 - 文字大小。
通过拖动滑块调整字体大小。
通过上边的方法调整字体到一定大小就不能再大了,其实还有办法可以调到更大:
打开设置 - 辅助功能 - 显示与文字大小 - 更大字体。
打开更大字体开关。
通过拖动滑块调整字体大小。
在开发阶段,还可以直接从 Xcode 调整字体。
点击调试栏中的图标 Environment Overrides 按钮.
打开 Dynamic Type 开关.
通过拖动滑块调整字体大小。
在 iOS 11 中,苹果引入了 UIFontMetrics,使我们的代码更简单。通过它,我们可以创建指定文本样式的 UIFontMetrics,然后将自定义字体传递给 scaledFont(for:)
方法,以获得基于自定义字体的字体对象,具有适当的样式信息,并自动缩放以匹配当前动态字体设置。
let customFont = UIFont(name: "Merriweather-Regular", size: 17)! // <1>
let label = UILabel()
label.adjustsFontForContentSizeCategory = true
label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont) // <2>
<1> 我们初始化了自定义字体。在这个例子中,我使用了 Google 字体的 Merriweather 字体。
<2> 我们定义了 UIFontMetrics 的 body 文本样式,然后用它来缩放我们的自定义字体。
虽然 UIFontMetrics 可以减少我们在支持动态类型的自定义字体上的工作量,但它并不是万能的。有时候我们仍然需要做一些额外的工作。
scaledFont(for:)
方法会根据大文本尺寸的基础字体大小应用缩放因子。苹果在 iOS 人机界面指南中说明了系统字体的字体度量标准。你可以用它作为为每种文本样式定义自定义字体的起始。
以下是我基于苹果度量的简单实现:
let customFonts: [UIFont.TextStyle: UIFont] = [
.largeTitle: UIFont(name: "Merriweather-Regular", size: 34)!,
.title1: UIFont(name: "Merriweather-Regular", size: 28)!,
.title2: UIFont(name: "Merriweather-Regular", size: 22)!,
.title3: UIFont(name: "Merriweather-Regular", size: 20)!,
.headline: UIFont(name: "Merriweather-Bold", size: 17)!,
.body: UIFont(name: "Merriweather-Regular", size: 17)!,
.callout: UIFont(name: "Merriweather-Regular", size: 16)!,
.subheadline: UIFont(name: "Merriweather-Regular", size: 15)!,
.footnote: UIFont(name: "Merriweather-Regular", size: 13)!,
.caption1: UIFont(name: "Merriweather-Regular", size: 12)!,
.caption2: UIFont(name: "Merriweather-Regular", size: 11)!
]
extension UIFont {
class func customFont(forTextStyle style: UIFont.TextStyle) -> UIFont {
let customFont = customFonts[style]!
let metrics = UIFontMetrics(forTextStyle: style)
let scaledFont = metrics.scaledFont(for: customFont)
return scaledFont
}
}
将 UIFontMetrics(forTextStyle: style).scaledFont(for: customFont)
替换为 UIFont.customFont(forTextStyle: style)
并再次运行即可。
let styles: [UIFont.TextStyle] = [.largeTitle, .title1, .title2, .title3, .headline, .subheadline, .body, .callout, .footnote, .caption1, .caption2]
for style in styles {
...
let label = UILabel()
label.adjustsFontForContentSizeCategory = true
label.text = String(describing: style)
label.font = UIFont.customFont(forTextStyle: style)
...
}
最后看下效果:
UIFontMetrics 可以减少我们在让自定义字体支持动态类型时所需的工作量。另外我们可能需要花一些时间来微调基础字体以确保其在所有变体中都适合,在 UIFontMetrics 的帮助下,这个过程不算负责。
希望这能帮助你更好地在应用中运用自定义字体。关于自定义动态字体,你有什么看法吗?欢迎在评论区中留言交流。
[1]
苹果的人机界面指南: developer.apple.com/design/huma…
这里每天分享一个 iOS 的新知识,快来关注我吧
本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!
OpenGL(Open Graphics Library)是一个跨语言、跨平台的图形渲染 API,用于渲染 2D 和 3D 图形。它提供了一套硬件无关的接口,让开发者能够利用 GPU 进行高效的图形渲染。
硬件抽象层:屏蔽不同 GPU 的差异
状态机:通过设置状态来控制渲染行为
立即模式 vs 保留模式:现代 OpenGL 使用保留模式
可编程管线:通过着色器程序控制渲染过程
// OpenGL 使用右手坐标系
// X轴:向右为正
// Y轴:向上为正
// Z轴:向外为正(屏幕外)
// 顶点坐标通常在 [-1, 1] 范围内
let vertices: [Float] = [
-1.0, -1.0, 0.0, // 左下
1.0, -1.0, 0.0, // 右下
0.0, 1.0, 0.0 // 顶部
]
// 顶点着色器
attribute vec4 position;
attribute vec2 texCoord;
varying vec2 vTexCoord;
void main() {
gl_Position = position;
vTexCoord = texCoord;
}
将顶点连接成图元(点、线、三角形)
进行视锥体裁剪
背面剔除
将图元转换为像素
插值计算片段的属性
// 片段着色器
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
gl_FragColor = color;
}
深度测试
模板测试
混合
// 基础顶点着色器
attribute vec4 aPosition;
attribute vec2 aTexCoord;
uniform mat4 uModelViewProjectionMatrix;
varying vec2 vTexCoord;
void main() {
gl_Position = uModelViewProjectionMatrix * aPosition;
vTexCoord = aTexCoord;
}
// 基础片段着色器
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
uniform float uAlpha;
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
gl_FragColor = vec4(color.rgb, color.a * uAlpha);
}
gl_Position
:顶点位置(顶点着色器输出)
gl_FragColor
:片段颜色(片段着色器输出)
gl_PointSize
:点大小
gl_FragCoord
:片段坐标
// 标量类型
float, int, bool
// 向量类型
vec2, vec3, vec4
ivec2, ivec3, ivec4
bvec2, bvec3, bvec4
// 矩阵类型
mat2, mat3, mat4
// 采样器类型
sampler2D, samplerCube
func createTexture(from image: UIImage) -> GLuint {
guard let cgImage = image.cgImage else { return 0 }
var textureName: GLuint = 0
glGenTextures(1, &textureName)
glBindTexture(GLenum(GL_TEXTURE_2D), textureName)
// 设置纹理参数
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)
// 上传纹理数据
let width = cgImage.width
let height = cgImage.height
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
let context = CGContext(data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: colorSpace,
bitmapInfo: bitmapInfo.rawValue)!
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
let data = context.data!
glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(width), GLsizei(height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), data)
return textureName
}
// 纹理坐标 (0,0) 在左下角,(1,1) 在右上角
let texCoords: [Float] = [
0.0, 0.0, // 左下
1.0, 0.0, // 右下
0.0, 1.0, // 左上
1.0, 1.0 // 右上
]
// 最近邻过滤
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_NEAREST)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_NEAREST)
// 线性过滤
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
// 多级纹理过滤
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR_MIPMAP_LINEAR)
glGenerateMipmap(GLenum(GL_TEXTURE_2D))
import GLKit
class OpenGLView: GLKView {
private var context: EAGLContext!
private var program: GLuint = 0
override init(frame: CGRect) {
// 创建 OpenGL ES 2.0 上下文
context = EAGLContext(api: .openGLES2)!
super.init(frame: frame, context: context)
// 设置代理
self.delegate = self
// 设置像素格式
self.drawableColorFormat = .RGBA8888
self.drawableDepthFormat = .format24
// 设置内容缩放因子
self.contentScaleFactor = UIScreen.main.scale
// 设置当前上下文
EAGLContext.setCurrent(context)
// 初始化 OpenGL 状态
setupOpenGL()
}
private func setupOpenGL() {
// 启用深度测试
glEnable(GLenum(GL_DEPTH_TEST))
// 设置清除颜色
glClearColor(0.0, 0.0, 0.0, 1.0)
// 创建着色器程序
program = createShaderProgram()
}
}
extension OpenGLView: GLKViewDelegate {
func glkView(_ view: GLKView, drawIn rect: CGRect) {
// 清除缓冲区
glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT))
// 使用着色器程序
glUseProgram(program)
// 设置顶点数据
setupVertexData()
// 设置纹理
setupTexture()
// 绘制
glDrawArrays(GLenum(GL_TRIANGLES), 0, 6)
}
}
func createShaderProgram() -> GLuint {
let vertexShaderSource = """
attribute vec4 position;
attribute vec2 texCoord;
varying vec2 vTexCoord;
void main() {
gl_Position = position;
vTexCoord = texCoord;
}
"""
let fragmentShaderSource = """
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
void main() {
gl_FragColor = texture2D(uTexture, vTexCoord);
}
"""
// 编译顶点着色器
let vertexShader = compileShader(type: GLenum(GL_VERTEX_SHADER), source: vertexShaderSource)
// 编译片段着色器
let fragmentShader = compileShader(type: GLenum(GL_FRAGMENT_SHADER), source: fragmentShaderSource)
// 创建程序
let program = glCreateProgram()
glAttachShader(program, vertexShader)
glAttachShader(program, fragmentShader)
glLinkProgram(program)
// 检查链接状态
var linkStatus: GLint = 0
glGetProgramiv(program, GLenum(GL_LINK_STATUS), &linkStatus)
if linkStatus == GL_FALSE {
print("Program link failed")
glDeleteProgram(program)
return 0
}
// 清理着色器
glDeleteShader(vertexShader)
glDeleteShader(fragmentShader)
return program
}
func compileShader(type: GLenum, source: String) -> GLuint {
let shader = glCreateShader(type)
var cSource = (source as NSString).utf8String
var length = GLint(source.utf8.count)
glShaderSource(shader, 1, &cSource, &length)
glCompileShader(shader)
// 检查编译状态
var compileStatus: GLint = 0
glGetShaderiv(shader, GLenum(GL_COMPILE_STATUS), &compileStatus)
if compileStatus == GL_FALSE {
var infoLength: GLint = 0
glGetShaderiv(shader, GLenum(GL_INFO_LOG_LENGTH), &infoLength)
var infoLog = [GLchar](repeating: 0, count: Int(infoLength))
glGetShaderInfoLog(shader, infoLength, nil, &infoLog)
print("Shader compilation failed: \(String(cString: infoLog))")
glDeleteShader(shader)
return 0
}
return shader
}
class ImageRenderer {
private var program: GLuint = 0
private var vertexBuffer: GLuint = 0
private var texture: GLuint = 0
func setup() {
// 创建顶点缓冲区
let vertices: [Float] = [
// 位置 // 纹理坐标
-1.0, -1.0, 0.0, 0.0, 0.0,
1.0, -1.0, 0.0, 1.0, 0.0,
-1.0, 1.0, 0.0, 0.0, 1.0,
1.0, -1.0, 0.0, 1.0, 0.0,
1.0, 1.0, 0.0, 1.0, 1.0,
-1.0, 1.0, 0.0, 0.0, 1.0
]
glGenBuffers(1, &vertexBuffer)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), vertexBuffer)
glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout<Float>.size * vertices.count, vertices, GLenum(GL_STATIC_DRAW))
}
func render() {
glUseProgram(program)
// 设置顶点属性
glBindBuffer(GLenum(GL_ARRAY_BUFFER), vertexBuffer)
let positionLocation = glGetAttribLocation(program, "position")
let texCoordLocation = glGetAttribLocation(program, "texCoord")
glEnableVertexAttribArray(GLuint(positionLocation))
glEnableVertexAttribArray(GLuint(texCoordLocation))
let stride = GLsizei(MemoryLayout<Float>.size * 5)
glVertexAttribPointer(GLuint(positionLocation), 3, GLenum(GL_FLOAT), GLboolean(GL_FALSE), stride, nil)
glVertexAttribPointer(GLuint(texCoordLocation), 2, GLenum(GL_FLOAT), GLboolean(GL_FALSE), stride, UnsafeRawPointer(bitPattern: 3 * MemoryLayout<Float>.size))
// 设置纹理
glActiveTexture(GLenum(GL_TEXTURE0))
glBindTexture(GLenum(GL_TEXTURE_2D), texture)
glUniform1i(glGetUniformLocation(program, "uTexture"), 0)
// 绘制
glDrawArrays(GLenum(GL_TRIANGLES), 0, 6)
glDisableVertexAttribArray(GLuint(positionLocation))
glDisableVertexAttribArray(GLuint(texCoordLocation))
}
}
// 灰度滤镜
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(vec3(gray), color.a);
}
// 反色滤镜
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
gl_FragColor = vec4(1.0 - color.rgb, color.a);
}
// 模糊滤镜
void main() {
vec2 texelSize = 1.0 / textureSize(uTexture, 0);
vec4 sum = vec4(0.0);
for(int i = -2; i <= 2; i++) {
for(int j = -2; j <= 2; j++) {
vec2 offset = vec2(float(i), float(j)) * texelSize;
sum += texture2D(uTexture, vTexCoord + offset);
}
}
gl_FragColor = sum / 25.0;
}
// 合并多个绘制调用
func batchRender(objects: [GameObject]) {
// 按材质分组
let groupedObjects = Dictionary(grouping: objects) { $0.material }
for (material, objects) in groupedObjects {
// 绑定材质
bindMaterial(material)
// 批量绘制
for object in objects {
updateTransform(object.transform)
drawObject(object)
}
}
}
// 使用 VBO 存储顶点数据
func createVertexBuffer() {
let vertices: [Float] = [/* 顶点数据 */]
glGenBuffers(1, &vertexBuffer)
glBindBuffer(GLenum(GL_ARRAY_BUFFER), vertexBuffer)
glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout<Float>.size * vertices.count, vertices, GLenum(GL_STATIC_DRAW))
}
// 使用纹理图集
func createTextureAtlas() {
// 将多个小纹理合并到一个大纹理中
// 减少纹理切换次数
}
// 使用压缩纹理
func loadCompressedTexture() {
// 使用 PVRTC 或 ASTC 格式
// 减少内存占用和带宽
}
// 避免分支语句
// 不好的做法
if (condition) {
color = texture2D(tex1, coord);
} else {
color = texture2D(tex2, coord);
}
// 好的做法
color = mix(texture2D(tex1, coord), texture2D(tex2, coord), condition ? 1.0 : 0.0);
// 使用内置函数
// 不好的做法
float length = sqrt(x * x + y * y);
// 好的做法
float length = length(vec2(x, y));
问题:纹理显示为黑色或白色
原因:
纹理数据格式不匹配
纹理坐标错误
采样器设置问题
解决:
// 检查纹理格式
glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, width, height, 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), data)
// 检查纹理坐标
let texCoords: [Float] = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0]
// 设置正确的采样器
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
问题:物体渲染顺序错误
解决:
// 启用深度测试
glEnable(GLenum(GL_DEPTH_TEST))
glDepthFunc(GLenum(GL_LESS))
// 清除深度缓冲区
glClear(GLbitfield(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT))
问题:OpenGL 资源未正确释放
解决:
deinit {
// 释放纹理
if texture != 0 {
glDeleteTextures(1, &texture)
}
// 释放缓冲区
if vertexBuffer != 0 {
glDeleteBuffers(1, &vertexBuffer)
}
// 释放着色器程序
if program != 0 {
glDeleteProgram(program)
}
}
问题:渲染性能低下
解决:
减少绘制调用次数
使用批处理
优化着色器
使用 LOD(细节层次)
启用背面剔除
// 在片段着色器中输出调试信息
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
// 输出红色通道作为调试
gl_FragColor = vec4(color.r, 0.0, 0.0, 1.0);
}
func checkOpenGLState() {
// 检查帧缓冲区状态
let status = glCheckFramebufferStatus(GLenum(GL_FRAMEBUFFER))
if status != GLenum(GL_FRAMEBUFFER_COMPLETE) {
print("Framebuffer not complete: \(status)")
}
// 检查着色器编译状态
var compileStatus: GLint = 0
glGetShaderiv(shader, GLenum(GL_COMPILE_STATUS), &compileStatus)
if compileStatus == GL_FALSE {
// 获取错误信息
var infoLength: GLint = 0
glGetShaderiv(shader, GLenum(GL_INFO_LOG_LENGTH), &infoLength)
var infoLog = [GLchar](repeating: 0, count: Int(infoLength))
glGetShaderInfoLog(shader, infoLength, nil, &infoLog)
print("Shader compilation failed: \(String(cString: infoLog))")
}
}
// 使用 Instruments 进行性能分析
// 关注以下指标:
// - GPU 使用率
// - 绘制调用次数
// - 纹理内存使用
// - 顶点处理数量
OpenGL 是一个强大的图形渲染 API,掌握它需要:
理解渲染管线:从顶点到像素的完整流程
掌握着色器编程:GLSL 语言和 GPU 编程
熟悉纹理系统:纹理创建、采样和过滤
学会性能优化:批处理、内存管理、算法优化
掌握调试技巧:状态检查、错误处理、性能分析
通过持续学习和实践,OpenGL 将成为你图形编程的强大工具。记住:
从简单开始,逐步增加复杂度
重视性能优化
养成良好的调试习惯
关注最新的 OpenGL 特性和最佳实践
书接上文AppStore的肃清了没有备案的产品,后台很多留言询问怎么样可以不备案?
好好好,想卡Bug,又不想花钱,还不想有风险?
行吧,参考了很多资料和证明咨询了AppStore,终于找出来一条免备案的骚操作。
尊敬的审核团队:
你好,非常感谢您给我提供这样一个回复的机会。
兹证明Apple ID:xxxxxx@qq.com,持有人为xxx,我申请的免备案AppleID为:xxxxxx,
Bundle ID:com.xxxxxx.xxxx.xxx
身份证号码为:xxxxxxxxxxx,居住地址为:xxx省xxxx市xxx区xxx路xxx号xxx小区xxx栋xxxx-xxx。
具体参考资料,请查看身份证正面和背面的照片。为了证明我是账号持有人,我还将额外提供户籍信息、居住地缴费清单,来确保我的身份真实有效。
正面:【附图】
背面:【附图】
户口本:【附图】
水费、电费:【附图】
本人郑重承诺,以上所有信息真实有效,如有任何欺瞒审核团队或虚假资料本人愿意承担任何法律责任,承担一切法律后果。
持有签字:xxxx 【手印】
签字日期:xxxx年xx月xx日
因为对于国内开发者信上海是苹果的话事人,所以不需要担心语言沟通问题。
⚠️关于免责声明的内容,仅供参考。总之,要尽可能多得向苹果提供有效资料。提交之后,就是耐心等待结果。
如果开发者邮箱收到苹果新消息,那么恭喜你已经成功跳过来备案要求。
因为本文示例产品为单机应用,类目属于工具类。只是用了AppStore内购相关的API,其他不需要任何网络请求。所以,需要联网的产品未必适用本文内容。
遵守规则,方得长治久安
,最后祝大家大吉大利,今晚过审!
# Pingpong和连连的平替,让AppStore收款无需新增持有人。
# 有幸和Appstore审核人员进行了一场视频会议特此记录。
更多Appstore咨询问题,请关注知识星球。「提供1v1上架指导,帮助开发者解决Appstore的疑难杂症,助力每一位开发者!」
。
在开发一个 Flutter 应用时,通常需要考虑以下几个方面:
本文将以一个 Instagram 登录页为例,从 0 开始搭建一个 Flutter 项目,逐步实现上述功能,构建一个可持续开发的项目架构。本文完整代码:github.com/wutao23yzd/… 中的Demo5,效果如下:
创建flutter项目,采用very_good_cli,(pub.dev/packages/ve… 创建 Flutter 项目的好处在于,它提供了规范化的项目结构、严格的代码分析规则、内建测试和 CI 支持,帮助开发者快速搭建高质量、可维护的应用,特别适合团队协作和企业级项目开发。通过如下指令,可以创建一个指定组织名和包名的flutter应用
very_good create flutter_app flutter_instagram_clone_app --org "com.flutter” --application-id "com.flutter.futter_instagram_clone"
l10n 是 “localization” 的缩写(l + 10个字母 + n),即“本地化/国际化”。very_good_cli创建好项目后,会自动生成国际化相关文件,但没有中文,可以这样子添加:
# 添加中文支持
preferred-supported-locales:
- en
- zh
路由使用go_routter (pub.dev/packages/go… 是 Flutter 官方推荐的路由管理库,,支持嵌套路由、参数传递、URL 同步、重定向和导航守卫等高级功能。相比传统的 Navigator,它结构更清晰、代码更简洁,而且支持基于用户登录状态的路由重定向逻辑,比如登录、登出,可以通过如下代码跳转:
redirect: (context, state) {
final authenticated = appBloc.state.status == AppStatus.authenticated;
final authenticating = state.matchedLocation == AppRoutes.auth.route;
if (!authenticated) return AppRoutes.auth.route;
if (authenticating && authenticated) return AppRoutes.home.route;
return null;
}
在 Flutter 项目中,采用 Cubit + Bloc + Repository 的分层模型 是一种清晰、可维护性强的架构设计。它将业务逻辑、状态管理和数据访问进行职责分离(pub.dev/packages/bl…
在提供的Demo中,AuthRepository 统一产出 用户身份流,任何需要身份信息的层(AppBloc)都只订阅这一个来源。
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return RepositoryProvider(
// 全局唯一的数据层
create: (_) => AuthRepository(),
child: BlocProvider(
create: (context) => AppBloc(
authRepository: context.read<AuthRepository>(),
),
child: const AppView(),
),
);
}
}
LoginCubit
只负责表单状态与调用 AuthRepository.login
AppBloc
只订阅 AuthRepository.user
,再映射成 authenticated / unauthenticated
。这样 UI ↔︎ 业务 ↔︎ 数据 的依赖方向清晰且单向。
使用BlocListener<LoginCubit, LoginState>
监听当前状态,如果出现错误,则顶部弹出提示;使用clearIfQueue
清除旧消息 。
BlocListener<LoginCubit, LoginState>(
listenWhen: (p, c) => p.status != c.status && c.status.isError,
listener: (_, s) => openSnackbar(
SnackbarMessage.error(title: '错误', description: s.message!),
clearIfQueue: true,
),
child: const LoginForm(),
);
首先要要安装flutter_gen
依赖,同时在dev_dependencies
中,需要安装
build_runner: ^2.5.4
flutter_gen_runner: ^5.10.0
对于svg的文件,还需要安装依赖flutter_svg
;然后按照demo中所示,在pubspec.yaml
中提供资源所示路径;最后,执行dart run build_runner build
自动生成assets.gen.dart
文件,引用资源文件如下所示:
Image.asset(Assets.images.logoPng.path);
FontFamily(Assets.fonts.montserrat); // 类型安全 + IDE 自动补全
class AppBlocObserver extends BlocObserver {
@override
void onChange(BlocBase bloc, Change change) =>
log('[Bloc] ${bloc.runtimeType} $change');
}
写在最后
Demo大量代码使用了www.youtube.com/watch?v=xr5…
你也可以为这个项目出一份力,如果发现有价值的信息、文章、工具等可以到 Issues 里提给我们,我们会尽快处理。记得写上推荐的理由哦。有建议和意见也欢迎到 Issues 提出。
@阿权:iOS 26 的液体玻璃效果让大家眼前一亮,大家可能已经在重新设计自己的 App,恨不得给所有 UI 都加上液态玻璃效果。文章提到液态玻璃效果容易滥用,导致界面的不和谐。使用液态玻璃的场景应该是用于突出按钮下方的内容,例如地图预览上方的操作按钮。换句话说,液态玻璃效果的控件会让控件自身与下方内容悬浮出来,形成两个解耦的交互层级,如果控件本身与内容是嵌合的二维依赖关系,则并不适合添加液态玻璃效果。
@Cooper Chen:本文深度剖析 AI 编程助手 Cursor 的进阶使用方法,为开发者提供一套可落地的效率提升方案:
1.Prompt 设计黄金法则
2.Rules 规范引擎
3.工具链整合方案
本文提供的技术方案设计模板和开发规范 Rules 可直接复用,帮助开发者快速建立 AI 辅助编程工作流。文中揭示的"代码生成 + 架构决策"分层协作模式,为现代软件开发提供了高效的智能解决方案。
@david-clang:Flutter 界面中出现的诡异细线,本质原因是:
解决方案是:
@Smallfly: 这是一篇非常详实的 AI 辅助开发实践分享。作者用 Claude Code 将一个 12 年前的 Objective-C 应用 Vinylogue 重写为 Swift + SwiftUI,仅用 7 天时间就完成了从架构升级到 App Store 上架的全流程。
文章的价值在于:
--quiet
标志、合理管理上下文窗口、创建反馈循环等对于想要尝试 AI 辅助开发的 iOS 开发者来说,这篇文章提供了一个很好的参考框架。特别是文章中提到的"保持在宏观层面评估代码库,让 AI 处理微观层面的工作"这一理念,对提高开发效率很有启发意义。
@AidenRao:Airbnb 的 SwiftUI 性能优化分享:通过为视图自定义 Equatable
协议实现,仅在实际数据变化时触发重绘,避免不必要的视图更新。将大型视图分解为小型可差异化组件,配合复杂度检测工具(如 SwiftLint 规则)预警重构时机,减少单次渲染计算量。
@老驴:Apple 最近发布了一个新的开源项目叫 Container,本质上是一个运行在 Linux 上,基于 Swift 和 Virtualization framework 的容器库。它的重点是更好地支持 Apple Silicon 芯片跑容器。 个人猜测,这可能是 Apple 为将来在自家服务器上使用 Apple Silicon 做准备的一步。毕竟一直有传言说 Apple 想让自家数据中心的服务器用上自研芯片,而要做到这一点,一个完善的容器方案是少不了的。
重新开始更新「iOS 靠谱内推专题」,整理了最近明确在招人的岗位,供大家参考
具体信息请移步:https://www.yuque.com/iosalliance/article/bhutav 进行查看(如有招聘需求请联系 iTDriverr)
我们是「老司机技术周报」,一个持续追求精品 iOS 内容的技术公众号,欢迎关注。
关注有礼,关注【老司机技术周报】,回复「2024」,领取 2024 及往年内参
同时也支持了 RSS 订阅:https://github.com/SwiftOldDriver/iOS-Weekly/releases.atom 。
🚧 表示需某工具,🌟 表示编辑推荐
预计阅读时间:🐎 很快就能读完(1 - 10 mins);🐕 中等 (10 - 20 mins);🐢 慢(20+ mins)
有两个observer会监听runloop两个事件,一个observer监听runloop要进入的是时候entry,会调用pool push方法创建一个autorelease pool
另一个observer监听runloop的状态,当runloop要进入休眠状态时beforewaiting,会pop一个自动释放池,同时push创建一个新的自动释放池。
AutoreleasePoolPage 结构
class AutoreleasePoolPage
{
const magic
id *next 指向下一个可以存放被释放对象的地址
pthread_t const thread 当前所在的线程
AutoreleasePoolPage *const parent 当前page的父节点
AutoreleasePoolPage *child
}
每个page占4096个字节也就是4kb,自身成员变量只占56个字节,也就是7个成员变量,每个成员变量占8个字节。其他四千多个字节都是用来存放被autorelease修饰的对象内存地址。
pool_boundary的作用是区分不同自动释放,调用push时,会传入一个pool_boundary并返回一个地址,这个地址不存储@autorelease对象的地址,起到一个标识作用,用来分隔不同的autoreleasepool
调用pop的时候,会传入end地址,从后到前调用对象的release方法,直到pool_boundary为止。
如果存在多个page,会从child的page最末尾开始调用,直到pool_boundary
page是一个栈结构,释放是从栈顶开始
多层嵌套会共用一个page,通过pool_boundary来分隔,优先释放在里层的pool,因为最里层的pool中的对象被放倒了栈顶,优先释放栈顶对象。
@autoreleasepool {
NSObject *p1 = [[NSObject alloc] init]
NSObject *p2 = [[NSObject alloc] init]
@autoreleasepool {
NSObject *p3 = [[NSObject alloc] init]
@autoreleasepool{
NSObject *p4 = [[NSObject alloc] init]
}
}
}
SDK does not contain 'libarclite' at the path '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a'; try increasing the minimum deployment target
libarclite_iphonesimulator.a
文件,那就command + G
打开这个路径看一下,结果发现这个目录下边没有arc
这个文件夹。如下图所示:/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/
command + C
+ command + V
复制粘贴arc
这个目录下,不能拖拽,拖拽的是快捷方式不是真实的文件。下载地址我已经放到github了,需要的可以自行下载。
iOS 线程管理是一个老生常谈的话题,也是新人绕不过去的一道难关。用好多线程不容易,下面我简单谈一谈我的体会,希望对屏幕前的你有所帮助。
首先,要知道线程不是越多越好,创建线程和切换线程都有一定的开销,线程使用的不当也容易造成崩溃。那么什么时候需要使用多线程呢?一个主要的衡量标准是这个操作是否耗时,比如读写文件、网络请求、加解密等。特别是IO密集的操作,一定是要多线程的,否则会阻塞当前线程。
其次,线程和队列有着紧密的联系(ios里面特指GCD队列),如果某些操作需要按照一定的时序来执行并且对执行的时间不是那么敏感的话,那么最好就是放在一个串行队列里,比如写缓存。如果这些操作对执行时间敏感,且不是很讲究顺序的话,那么放在并行队列里比较合适,比如从分批下载视频片段(例如dash和hls)。如果是对执行时间敏感,并且又有一定的执行顺序,那么可以考虑NSOperationQueue,或者用dispatch_group、dispatch_semaphore来管理多个线程及其依赖关系。如果对这些都不讲究,那就用不着多线程了。
一般情况下,能用异步还是用异步,除非是需要等待结果返回的才用同步。这主要是因为同步操作会阻塞线程,弄的不好还会导致死锁。编写同步代码的话,主要是用在同步读取某些属性这种场景,比如以下这个方法
- (BOOL)hasSubscribeTopic:(NSString*)topic {
__block BOOL subscribed = NO;
dispatch_sync(self.syncQueue, ^{
subscribed = [self.subscribedTopics containsObject:topic];
});
return subscribed;
}
但是这样写有一个问题,就是如果别的方法在syncQueue对应的线程上调用了hasSubscribeTopic这个方法,就会导致死锁,所以正确的方式应该是这样
static const void * kDispatchQueueSpecificKey = &kDispatchQueueSpecificKey;
//init方法中调用
dispatch_queue_set_specific(_syncQueue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
- (BOOL)hasSubscribeTopic:(NSString*)topic {
void* value = dispatch_get_specific(kDispatchQueueSpecificKey);
if (value == (__bridge void *)(self)) {
return [self.subscribedTopics containsObject:topic];
}else{
__block BOOL subscribed = NO;
dispatch_sync(self.syncQueue, ^{
subscribed = [self.subscribedTopics containsObject:topic];
});
return subscribed;
}
}
有些第三方库没有注意这方面,比如SDWebImage的SDImageCache,使用的时候就需要尤其注意
- (void)storeImageDataToDisk:(nullable NSData *)imageData
forKey:(nullable NSString *)key {
if (!imageData || !key) {
return;
}
dispatch_sync(self.ioQueue, ^{
[self _storeImageDataToDisk:imageData forKey:key];
});
}
这个如前所述,主要看对执行时间的敏感程度和有无顺序要求。一般使用dispatch_create创建的队列以串行为主(swift的dispatchQueue默认就是串行的)。并行队列使用global_queue就可以了,但是有一个需要特别注意的是,不管是dispatch_get_global_queue还是dispatch_create分配的线程都是有上限的,如果超出上限,系统要么就是等待前面的线程执行完成(iOS模拟器),要么就会因为分配资源过多而导致崩溃(iOS真机)。通过下面这段代码,可以测试出系统最多能分配多少个线程,在iphone 15的模拟器上我测试得到的是global_queue能分配64个左右线程,而dispatch_create相对多一点,100多不到200个。
dispatch_queue_t syncQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
for (int i=0;i<1000;i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
pthread_t tid = pthread_self();
printf("1 tid=%lu\n",(unsigned long)tid);
dispatch_sync(syncQueue, ^{
NSLog(@"2");
});
NSLog(@"3");
});
}
还有一个问题就是,使用dispatch_get_global_queue创建的线程看似64个也够用了,但如果在这些线程里面使用了同步操作等待串行队列执行完成的话,就会造成阻塞,最终超出线程数量上限而崩溃。比如将上面代码中的NSLog(@"2") 改为一个写缓存之类的耗时操作。
由于线程数量是有上限的,并且线程切换比较耗时,所以对于性能要求较高的程序需要有线程池来管理多线程。iOS是没有系统自带的线程池的,一般都是自己实现(推荐使用dispatch_semaphore或NSOperationQueue,具体实现可以参考java的executor相关代码。需要注意的是,什么时候切换到线程池是有讲究的,一般规则是逻辑层的代码尽早切换到线程池,特别是有些逻辑可能会创建多个线程的时候,比如多个图片的下载和缓存。
线程的同步也是一个比较经典的话题了,我在这里就不想赘述了,大家可以在网上随便搜一搜,我只提一下,一般线程间同步就几种方式:
个人比较推荐的是加锁(性能要求没那么高)和条件变量(性能要求较高,逻辑相对简单的场景)。串行队列如果管理不当可能会创建多个线程,因此不做推荐。内存屏障和CAS原子操作比较底层,使用起来也没那么方便,除非是对时序和性能要求极高。
除了使用C语言的pthrea_create和pthread_join来进行线程创建和销毁时的通信外,iOS还可以使用NSMachPort和NSThread的performSelectorOnThread来做线程间通信。前者跟runloop结合,在runloop的生命周期内注册一个特定的事件来定期检查并执行,后者类似于pthread_create,在创建线程时传递一个参数。 除了这种系统提供的方法外,还有一种通用的方式,就是在线程内维护一个事件队列,外部需要给这个线程发消息时,就往队列插入一个事件,然后该线程在一个循环内定时去取事件执行。有点类似runloop的感觉,如果要跨平台的话可以考虑使用libevent(一般用来做网络通信)来实现。
不管是在iOS还是其他的平台上,多线程管理都是一个复杂的话题。要用好多线程,除了要掌握一些常见的方法外,最主要还是平时编程的时候多思考,什么时候应该用多线程,以及怎么样做好线程同步和队列的选择,在追求高性能的同时保证安全性。
断点下载:是指在下载一个较大文件过程中因网络中断等主动暂停下载,当重启任下载任务时,能够从上次停止的位置继续下载,而不用重新下载。
URLSessionDownloadTask:是实现断点下载的核心类,专门用于下载文件到临时位置,并原生支持断点续传:
相关代码:
let configuration = URLSessionConfiguration.default
var downloadTask : URLSessionDownloadTask?
let fileURL = URL(string: "http://vjs.zencdn.net/v/oceans.mp4")
任务下载
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
downloadTask = session.downloadTask(with: URLRequest(url: fileURL!))
downloadTask?.resume()
继续下载
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
downloadTask = session.downloadTask(withResumeData: data)
downloadTask?.resume()
取消下载
downloadTask?.cancel(byProducingResumeData: { [weak self] resumeData in
self?.downloadTask = nil
// 其他操作
}
下载的过程本身是不处理相关数据的存储的,需要我们自己来实现。数据持久化的方式很多但支持断点下载功能的多半都是比较大型的文件。因此选择沙盒(SandBox)来存储下载的文件是十分合适的。
获取文件目录:一般都是把文件存储到documentDirectory,userDomainMask目录
let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
创建写入文件路径:这里表示把文件写入MyVideos文件,文件名为:oceans.mp4。
let folderName = documentDaiectory.appendingPathComponent("MyVideos")
let videoURL = folderName.appendingPathComponent("oceans.mp4")
在上一步获取文件目录已经指定了一个根目录这个会沙盒系统的根目录下再创建一个MyVideos文件
// 创建需要的文件目录
do {
try fileManager.createDirectory(at: folderName, withIntermediateDirectories: true, attributes: nil)
// 写入文件
} catch {
print("创建目录失败:\(error)")
}
写入文件
do {
try data.write(to: videoURL)
print("写入成功")
} catch {
print("写入失败:\(error)")
}
下次继续下载时要去做一个判断,查看是否已经存储之前下载的内容,返回TRUE则是进行继续下载,返回FALSE则是重新开始下载
let fileManager = FileManager.default
guard let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
return false
}
let documentsFileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")
if fileManager.fileExists(atPath: documentsFileURL.path) {
do {
// 存在
// 同时获取当一已下载文件的Data
self.currentDownloadData = try Data(contentsOf: documentsFileURL)
return true
} catch {
return false
}
} else {
return true
}
在对返回的状态做相应的处理
if isFileExist() == true {
// 继续下载
} else {
// 重新下载
}
除了相关下载存储操作外还要实现 URLSessionDownloadDelegate 相关代理方法
下载完成:通过URLSessionDownloadTask下载完成的文件并不会存储到指定的文件夹,而是存储在sandbox的tmp目录下的临时文件夹内。该文件夹内的数据随时都会被系统清理,因此要在适当的时候把文件转移到我们需要的文件下。
这里我们把文件存储到""MyVideos"文件下并使用"oceans.mp4"为文件名
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let fileManager = FileManager.default
let documentDirectory = FileManager.default.urls(for:.documentDirectory, in: .userDomainMask).first!
let fileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")
do {
if isFileExist() == true {
// 还是对文件是否存在做一个判断并做一个删除处理,因为沙盒系统本身不会自动覆盖同名文件的处理
try fileManager.removeItem(at: fileURL)
}
// 移动到指定目录
try fileManager.moveItem(at: location, to: fileURL)
} catch {
print("删除文件出错:\(error)")
}
}
下载过程中方法:可以从该方法获取到下载的进度
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
self.currentProgressValue = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
}
import UIKit
import Foundation
typealias DownloadProgressBlock = (_ progreee : Float) -> ()
typealias DownloadFileBlock = (_ fileURL : URL) -> ()
class WZGVideoDownloader : NSObject {
static var shard = WZGVideoDownloader()
var progressBlock : DownloadProgressBlock?
var fileBlock : DownloadFileBlock?
let configuration = URLSessionConfiguration.default
var downloadTask : URLSessionDownloadTask?
let fileURL = URL(string: "http://vjs.zencdn.net/v/oceans.mp4")
// 存储已下载data
var currentDownloadData : Data?
// 当前文件大小
var currentProgressValue : Float = 0.0
func startDownload(_ fileSize : Data) {
let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let documentFileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
// 判断是继续下载还是重新下载
if isFileExist() == true {
if let data = self.currentDownloadData {
if data == fileSize {
self.progressBlock?(1)
self.fileBlock?(documentFileURL)
return
}
self.progressBlock?(self.currentProgressValue)
// 继续下载
print("继续下载")
downloadTask = session.downloadTask(withResumeData: data)
downloadTask?.resume()
}
} else {
// 重新下载
print("重新下载")
downloadTask = session.downloadTask(with: URLRequest(url: fileURL!))
downloadTask?.resume()
}
}
func stopDownload() {
downloadTask?.cancel(byProducingResumeData: { [weak self] resumeData in
guard let resumeData = resumeData else {
return
}
self?.writeSandBox(resumeData)
self?.downloadTask = nil
})
}
// 判断是否有下载的文件
func isFileExist() -> Bool {
let fileManager = FileManager.default
guard let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
return false
}
let documentsFileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")
if fileManager.fileExists(atPath: documentsFileURL.path) {
do {
self.currentDownloadData = try Data(contentsOf: documentsFileURL)
print("currentDownloadData:\(currentDownloadData)")
return true
} catch {
return false
}
} else {
return false
}
}
// 写入sandbox
func writeSandBox(_ data : Data) {
let fileManager = FileManager.default
let documentDaiectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
//创建目录几写入文件名
let folderName = documentDaiectory.appendingPathComponent("MyVideos")
//设置写入文件名称
let videoURL = folderName.appendingPathComponent("oceans.mp4")
// 创建目录
do {
try fileManager.createDirectory(at: folderName, withIntermediateDirectories: true, attributes: nil)
// 写入文件
do {
try data.write(to: videoURL)
print("写入成功")
} catch {
print("写入失败:\(error)")
}
} catch {
print("创建目录失败:\(error)")
}
}
}
extension WZGVideoDownloader : URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let fileManager = FileManager.default
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileURL = documentDirectory.appendingPathComponent("MyVideos/oceans.mp4")
do {
if isFileExist() == true {
// 文件存在则删除
try fileManager.removeItem(at: fileURL)
}
// 下载完会保存在temp零食文具目录 转移至需要的目录
try fileManager.moveItem(at: location, to: fileURL)
self.fileBlock?(fileURL)
} catch {
print("删除文件出错:\(error)")
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
self.currentProgressValue = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
DispatchQueue.main.async {
self.progressBlock?(self.currentProgressValue)
}
}
}
myClosure = { [weak self] in
guard let self = self else { return }
}
// ✅ 只需要在最外层写一次
myClosure1 = { [weak self] in [weak self]
// 在顶部进行一次安全解包
guard let self = self else { return }
// 在这个作用域里,`self` 是一个临时的强引用,可以安全使用
// 不用担心它被提前释放
myClosure2 = {
// 它会捕获上面 guard let 创建好的、非可选的强引用 self
myClosure3 = {
// 你可以直接、安全地调用 self 的方法或属性
}
}
}
OC 的核心问题在于:
@strongify(self)
创建的那个临时强引用 strongSelf
的生命周期会持续到包含它的那个 block 执行完毕。如果这个 block 内部又启动了一个长时间的异步任务(第二个 block),那么 strongSelf
会被第二个 block 捕获,导致 self
实例的生命周期被不必要地延长。因此,为了追求最精细的内存管理,开发者才会在每一层异步调用前都重复进行“弱化 -> 强化”的操作。
Swift 的优势在于: guard let self = self
创建的强引用 self
是一个全新的、仅存在于外层闭包作用域内的局部变量。当 anotherAsyncTask
的闭包(内层闭包)创建时,它捕获的是这个局部的、新的 self
。一旦外层闭包执行完毕(someAsyncTask
的回调结束),这个局部的 self
变量就会被销毁。anotherAsyncTask
的闭包对它的持有也就自然解除了,完全不会影响到原始 self
实例的生命周期。
特性 / 行为 | Objective-C (block ) |
Swift (closure ) |
---|---|---|
弱引用声明 |
@weakify(self) 或 __weak typeof(self) weakSelf = self;
|
在捕获列表 [weak self]
|
临时强引用 |
@strongify(self) 或 __strong typeof(weakSelf) strongSelf = weakSelf;
|
guard let self = self else { return } |
嵌套捕获 | 内层 block 捕获由 @strongify 创建的 strongSelf ,其生命周期可能过长,导致需要“再次弱化”。 |
内层闭包捕获由 guard let 创建的局部强引用 self 。该局部变量生命周期很短,因此无需再次弱化。 |
开发者操作 | 需要警惕并可能在每一层异步调用前都重复“弱化-强化”的模式。 | 只需要在最外层做一次“弱化-强化” ,内部可以完全放心使用。 |
someClosure = { [self] in }
那是因为Xcode 的首要任务是解决编译错误,而不是帮你分析内存管理。 而 [self] in
正是解决这个特定编译错误的“最直接”的语法。它解决了语法问题,但它没有解决循环引用的问题。它只是把一个隐式的强引用,变成了一个显式的强引用。所以解决循环引用问题还是需要[weak self]
。
[self]
对于内存泄露来说是错误的呗?这个说法不完全准确,但您的警惕性非常对!更精确的说法是:
在【会】产生循环引用的场景下使用 [self]
,是绝对错误的,它会直接导致内存泄漏。
但是,在【不会】产生循环引用的场景下,[self]
则是安全、甚至是被推荐的写法。
所以,[self]
本身不是“错误”,它只是一个工具。错误的是在不合适的场景下使用了这个工具。
比如下列两种情况
// 这个闭包被传递给 UIView.animate,执行完动画后就会被销毁。
// self 并没有一个属性来持有这个闭包。
// 所以 self -> 闭包 这条强引用链不存在。
UIView.animate(withDuration: 0.5) { [self] in
// 在这里使用 [self] in 是【完全正确】的。
// 它明确地告诉编译器:“我知道我在强引用 self,且我确定这是安全的。”
}
// DispatchQueue.main.asyncAfter 的闭包同样是执行完就销毁。
DispatchQueue.main.asyncAfter(deadline: .now() +1.0) { [self] in
// 这里使用 [self] in 也是【完全正确】的。
}
简单的可以理解为“一次性”的工作 (用 [self]
是安全的),“长期”的规则 (必须用 [weak self]
)。
答案是不需要的。为什么呢?因为Alamofire临时持有了您的闭包,由于 ViewController
没有持有任何东西,所以闭包无论如何强引用 ViewController
,都构不成一个闭环。因此,这里使用 [self]
来显式强引用是完全安全的。
无论是在 OC 的 AFNetworking 还是 Swift 的 Alamofire,它们【内部都没有,也不可能】自动处理您在闭包中捕获 self
导致的循环引用。防止循环引用的责任始终在调用者(也就是您)身上。
原因和我们上面分析的完全一样:因为您在 OC 中调用 AFN 封装的场景,很可能也属于“一次性”的调用,本身就不会产生循环引用。由于AFHTTPSessionManager的实例 manager
是一个局部变量,方法执行完就释放了,success
block 被 manager
临时持有,执行完也就释放了。所以,当时您在 OC 里不写弱引用是正确的,不是因为 AFNetworking 内部处理了,而是因为您的【用法】决定了它根本没有循环引用!
[MyOCNetworkManager requestWithURL:@"" parameters:nil success:^(id responseObject) {
// 您在这里直接使用 self,比如 [self.tableView reloadData];
// 并没有写 __weak typeof(self) weakSelf = self;
} failure:^(NSError *error) {
// ...
}];
完全没有问题,这样做在内存上是绝对安全的,但同样,它也是不必要的,并且有轻微的副作用。
在那个场景下,编写弱引用代码(即 weak/strong dance
)是“安全但多余的”。
我们来对比一下两种写法:
ViewController
持有,因此不存在循环引用。[MyOCNetworkManager requestWithURL:@""
parameters:nil
success:^(id responseObject) {
// 直接使用 self,闭包会强引用 self。
// 因为没有循环引用,self 会在闭包执行完后被正常释放,这是安全的。
[self.tableView reloadData];
}
failure:nil];
这种写法简洁、清晰,并且正确地表达了意图:“这是一个一次性的任务,我需要 self
在任务执行时是存在的。”
__weak typeof(self) weakSelf = self;
[MyOCNetworkManager requestWithURL:@"/some/path"
parameters:nil
success:^(id responseObject) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// 使用 strongSelf 来确保在闭包执行期间 self 不会被释放
[strongSelf.tableView reloadData];
}
failure:nil];
是的,100%安全。弱引用永远不会增加引用计数,所以它绝不会“创造”出一个循环引用。从这个角度看,它没有任何“问题”。
有,和 Swift 的情况完全相同:
代码变得冗余:为了一个没有必要的安全措施,您多写了三行样板代码 (__weak
, __strong
, if
)。这降低了代码的简洁性。
意图变得模糊:当其他开发者读到这段代码时,他们会看到 weak/strong dance
,这通常是一个强烈的信号,表示“这里有循环引用的风险”。他们可能会因此花时间去寻找一个实际并不存在的风险点,增加了维护成本。
极端情况下的功能差异:如果 self
(比如一个 UIViewController
) 在网络请求发出后、但在回调执行前被释放了(例如用户快速返回上一个页面),那么:
写法一(强引用) :self
会被闭包“续命”,直到闭包执行完毕。[self.tableView reloadData]
会被执行。
写法二(弱引用) :self
会被立即释放。当回调执行时,strongSelf
会是 nil
,代码会直接 return
,[strongSelf.tableView reloadData]
不会被执行。
[MyOCNetworkManager requestWithURL:@""
parameters:nil
success:^(id responseObject) {
// 直接使用 self,闭包会强引用 self。
// 因为没有循环引用,self 会在闭包执行完后被正常释放,这是安全的。
[self.tableView reloadData];
}
failure:nil];
__weak typeof(self) weakSelf = self;
[MyOCNetworkManager requestWithURL:@""
parameters:nil
success:^(id responseObject) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// 使用 strongSelf 来确保在闭包执行期间 self 不会被释放
[strongSelf.tableView reloadData];
}
failure:nil];
我们回到最常见的场景:网络请求回来后,更新界面。
Objective-C
// 弱引用写法
__weak typeof(self) weakSelf = self;
[MyNetworkManager request:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return; // 如果 self 销毁了,就直接返回
}
[strongSelf.tableView reloadData]; // 更新界面
}];
设想一下用户快速返回,self
(也就是 ViewController
) 被销毁了。这时网络回调回来了,因为 strongSelf
是 nil
,所以 [strongSelf.tableView reloadData]
这行代码没有被执行**。
这对用户来说是问题吗?完全不是。 因为界面都已经消失了,tableView
也不存在了,去刷新一个不存在的界面本来就是一件没有意义的事情。代码不执行,反而更干净利落。
所以,在 99% 的 UI 更新场景中,弱引用导致的“代码不执行”这个行为差异,不仅不是问题,反而是我们期望的、最合理的结果。
要触发这个“功能差异”,需要满足一个条件:self
的销毁发生在“网络请求发出后”和“回调执行前”这个短暂的时间窗口内。
对于大多数响应速度很快的 API 来说,这个窗口可能只有几百毫秒。用户需要操作得非常快才能正好卡在这个时间点上。因此,在日常测试和使用中,这个情况本身就不容易遇到。
假设回调里做的事情是 [self hideLoadingIndicator]
。如果用户已经返回了上一个页面,那个加载指示器 loadingIndicator
本来就已经随着页面消失了,所以 hideLoadingIndicator
这行代码执不执行,用户根本感知不到任何区别。
在 GitHub 上确实有不少优秀且实用的 iOS 图片编辑相关的开源项目和 Demo。这些项目覆盖了基础编辑(裁剪、旋转、调整)、滤镜应用、涂鸦、贴纸添加、高级特效等功能。以下是一些值得关注的项目,适合学习和集成:
TOCropViewController
YPImagePicker
SignatureView
PencilKitExample
TOCropViewController
PhotoEditDebug
MetalPetal
PencilKit
或 SignatureView
这些项目大多持续维护,代码质量较高。建议根据需求先尝试 Demo,再选择性集成模块到项目中!如果遇到具体实现问题,可以深入查看其 Issues 或源码实现逻辑。