普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月10日掘金 iOS

在项目中使用CocoaPods引入OpenVPNAdapter

作者 形影相吊
2025年4月10日 14:49

在项目中使用CocoaPods引入OpenVPNAdapter

一、背景

最近公司的项目需要实现接入OpenVPN功能,去github上找了一下,TunnelKitOpenVPNAdapter是不错的选择,但是由于TunnelKit只支持ios15+,并且只能使用Swift Package Manager进行引用;加上TunnelKit依赖WireGuard,但是WireGuard没有对iOS18.2进行兼容,从GitHub上下载的TunnelKit demo运行报错。报错原因就是WireGuard导致的。我们的项目支持iOS13+,并且已经引用了WireGuard,所以TunnelKit不适合。另外,虽然OpenVPNAdapter不再更新,但是经过多年的发展,它还是非常稳定的。所以我们决定选择接入OpenVPNAdapter

二、收益

项目支持OpenVPN

三、引入OpenVPNAdapter

通过github上OpenVPNAdapter的介绍,结合项目现状,我们决定使用cocoapods引用OpenVPNAdapter。

Profile文件增加如下行:

target 'Your Target Name' do
  use_frameworks!
  pod 'OpenVPNAdapter', :git => 'https://github.com/ss-abramchuk/OpenVPNAdapter.git', :tag => '0.8.0'
end

然后执行pod install, 并选择项目

四、遇到的问题

安装好OpenVPNAdapter后选择项目,直接报错:

image.png

五、分析问题

错误原因分析:shared不能在app extension中使用。

image.png

原因说明:iOS的app extension运行在一个“沙盒”环境中,没有完整的生命周期和权限,不能访问全局单例,Apple明确限制app extension使用这个API是为了确保app extension的稳定性。

查看了一下NetworkExtension中的代码,也没有调用过shared呀,奇怪了???

去Google了一下,搜到一篇文章对这种错误给出了解决方案。其中以下这个答案让我找到了解决此次错误的方案:

image.png 于是我猜测可能是OpenVPN做了什么手脚。

导入OpenVPNAdapter前,我们的项目还能正常运行的,为什么导致OpenVPNAdapter就运行报错了呢?问题大概率出在OpenVPNAdapter身上。

github上[OpenVPNAdapter]看了一下OpenVPNAapter的podspec文件。

image.png

果然在OpenVPNAdapter.podspec文件中看到了对应的设置。

APPLICATION_EXTENSION_API_ONLY = YES是xcode的一个构建设置,它的作用是:限制使用仅可用于App Extention的API,比如UIApplication.sharedApplicationopenURL:不能在App Extension中使用。

通过在podspec中设置APPLICATION_EXTENSION_API_ONLY = YES会导致pod install后,将项目里面所有的xcconfig文件都设置成APPLICATION_EXTENSION_API_ONLY = YES

包括Pods-xxx.xcconfig文件

image.png

包括第三方库

image.png 明确了问题所在,现在就要想办法解决此问题。

六、解决问题

解决报错,在Profile文件下设置以下代码:

post_install do |installer
  #修改第三方库
  installer.pods_project.targets.each do |target|
      target.build_configurations.each do |config|
        # 只对你的主工程 target 生效
        if ['YYKeyboardManager','TPEBBannerView','TYSnapshotScroll','LEEAlert','JKCategories','HWPanModal','DCKit','CJBaseHelper'].include?(target.name)
          # 解决'sharedApplication' is unavailable: not available on iOS (App Extension) - Use view controller based solutions where appropriate instead.问题
          config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO'
        end
      end
  end
  
  #修改Pods-
  installer.pods_project.targets.each do |target|
      target.build_configurations.each do |config|
        xcconfig_file_ref = config.base_configuration_reference
        next unless xcconfig_file_ref
        path = xcconfig_file_ref.real_path
        next unless path.basename.to_s.start_with?("Pods-你的工程名")
        content = File.read(path)
        if content.include?("APPLICATION_EXTENSION_API_ONLY = YES")
          new_content = content.gsub("APPLICATION_EXTENSION_API_ONLY = YES", "APPLICATION_EXTENSION_API_ONLY = NO")
          File.write(path, new_content)
          puts "✅ Modified: #{path}"
        else
          puts "🔍 Skip (no YES found): #{path}"
        end
      end
   end
end

我们将会引发编译错误的所有第三方库进行了修改,将这些第三方库的APPLICATION_EXTENSION_API_ONLY改回为NO。并且将需要修改的Pods-xxx.xcconfig文件也进行了修改

注意:App-Extension如果依赖了pod,其实是不需要修改的。

最后重新pod install,再编译就可以Success了。

七、总结

当发生错误时,要先找到导致问题的原因。再针对原因找到对应的解决方案。

第 114 题:找出字符串中连续出现最多的字符和个数

2025年4月10日 07:36

解题思路

为了找出字符串中连续出现最多的字符及其个数,我们可以采用一次遍历的方法。通过记录当前字符及其连续出现的次数,同时更新最大字符和最大次数,最终返回结果。

具体步骤

  1. 初始化变量:我们需要一个变量来记录当前字符、当前字符出现的次数、最大字符以及最大出现次数。
  2. 遍历字符串:逐个字符检查:
    • 如果当前字符与前一个字符相同,增加当前计数。
    • 如果不同,比较当前计数与最大计数,更新最大字符和最大计数,然后重置当前字符及计数。
  3. 最后检查:遍历结束后,需再次检查以确保最后的字符计数被考虑在内。
  4. 返回结果

代码实现

下面是用 Python 实现的代码示例:

def max_consecutive_char(s: str) -> (str, int):
    if not s:  # 如果字符串为空,返回空字符和0
        return "", 0
    
    max_char = s[0]  # 初始化最大字符
    max_count = 1    # 初始化最大计数
    current_char = s[0]  # 当前字符
    current_count = 1      # 当前计数

    for i in range(1, len(s)):
        if s[i] == current_char:  # 当前字符与上一个字符相同
            current_count += 1
        else:  # 当前字符不同
            if current_count > max_count:  # 更新最大字符和计数
                max_char = current_char
                max_count = current_count
            current_char = s[i]  # 更新当前字符
            current_count = 1     # 重置当前计数

    # 最后检查一次
    if current_count > max_count:
        max_char = current_char
        max_count = current_count

    return max_char, max_count

# 示例
s = "aaabbccaaaddddd"
result = max_consecutive_char(s)
print(f"字符: {result[0]}, 个数: {result[1]}")

代码解析

  • 函数定义max_consecutive_char(s: str) -> (str, int) 定义了一个接受字符串并返回字符及其个数的函数。
  • 边界条件:首先检查字符串是否为空,若为空则直接返回空字符和0。
  • 初始化:使用 max_charmax_count 记录最大字符和最大出现次数,current_charcurrent_count 用于跟踪当前字符及其计数。
  • 遍历字符串:在循环中,根据当前字符与前一个字符是否相同来更新计数。
  • 最终检查:循环结束后,检查最后的 current_count 是否大于 max_count,以确保最后的字符统计被考虑。

复杂度分析

  • 时间复杂度:O(n),其中 n 是字符串的长度。我们只遍历了一次字符串。
  • 空间复杂度:O(1),只使用了常数大小的额外空间。

总结

通过这个方法,我们可以高效地找出字符串中连续出现最多的字符和个数。这个算法简单易懂,且性能优越,适用于处理较长的字符串。

Codable 宏让 Swift 序列化如此简单!

作者 phoenix
2025年4月9日 22:34

大家好!作为 Swift 开发者,我们每天都在与数据打交道,而 JSON 与模型之间的转换无疑是家常便饭。苹果为我们提供了 Codable 协议,它在很多情况下表现出色,但随着业务逻辑变得复杂,我们常常会陷入编写大量样板代码的泥潭:手动定义 CodingKeys、实现 init(from:)encode(to:)、处理嵌套结构、应对不同的命名风格、解析各种日期格式…… 这些繁琐的任务不仅耗时,还容易出错。

有没有更优雅、更高效的方式来处理 Swift 中的 Codable 呢?

答案是肯定的!随着 Swift 5.9+ 引入的 Swift Macros,代码生成的可能性被大大扩展。今天,我向大家介绍一款基于 Swift Macros 构建的框架 —— ReerCodable

ReerCodable (github.com/reers/ReerC…) 旨在通过声明式的注解,彻底简化 Codable 的使用体验,让你告别繁琐的样板代码,专注于业务逻辑本身。

实际应用示例

让我们通过一个实际的例子来看看 ReerCodable 如何简化开发工作。假设我们有一个复杂的 API 响应:

{
  "code": 0,
  "data": {
    "user_info": {
      "user_name": "phoenix",
      "birth_date": "1990-01-01T00:00:00Z",
      "location": {
        "city": "北京",
        "country": "中国"
      },
      "height_in_meters": 1.85,
      "is_vip": true,
      "tags": ["tech", null, "swift"],
      "last_login": 1731585275944
    }
  }
}

使用 ReerCodable,我们可以这样定义模型:

@Codable
struct ApiResponse {
    var code: Int
    
    @CodingKey("data.user_info")
    var userInfo: UserInfo
}

@Codable
@SnakeCase
struct UserInfo {
    var userName: String
    
    @DateCoding(.iso8601)
    var birthDate: Date
    
    @CodingKey("location.city")
    var city: String
    
    @CodingKey("location.country")
    var country: String
    
    @CustomCoding<Double>(
        decode: { return try $0.value(forKeys: "height_in_meters") * 100.0 },
        encode: { try $0.set($1 / 100.0, forKey: "height_in_meters") }
    )
    var heightInCentimeters: Double
    
    var isVip: Bool
    
    @CompactDecoding
    var tags: [String]
    
    @DateCoding(.millisecondsSince1970)
    var lastLogin: Date
}

// 使用方式
do {
    let response = try ApiResponse.decode(from: jsonString)
    print("用户名: (response.userInfo.userName)")
    print("出生日期: (response.userInfo.birthDate)")
    print("身高(厘米): (response.userInfo.heightInCentimeters)")
} catch {
    print("解析失败: (error)")
}

原生 Codable 的那些“痛”

在我们深入了解 ReerCodable 的魅力之前,先来回顾一下使用原生 Codable 时可能遇到的常见痛点:

  1. 手动 CodingKeys: 当 JSON key 与属性名不一致时,需要手动编写 CodingKeys 枚举,只修改一个, 其他所有属性都要写, 属性少还好,一旦多了,简直是噩梦。
  2. 嵌套 Key: 处理深层嵌套的 JSON 数据,需要定义多个中间结构体或手动编写解码逻辑。
  3. 命名风格转换: 后端返回 snake_case 或 kebab-case,而 Swift 推荐 camelCase,需要手动映射。
  4. 复杂的解码逻辑: 如需自定义解码(类型转换、数据修复等),就得实现 init(from:)
  5. 默认值处理: 非 Optional 属性在 JSON 中缺失时,即使有默认值也会抛出 keyNotFound 异常。Optional 枚举缺失也会导致解码失败。
  6. 忽略属性: 某些属性不需要参与编解码,需要手动在 CodingKeys 或实现中处理。
  7. 日期格式多样: 时间戳、ISO8601、自定义格式…… 需要为 JSONDecoder 配置不同的 dateDecodingStrategy 或手动处理。
  8. 集合中的 null: 数组或字典中包含 null 值时,若对应类型非 Optional,会导致解码失败。
  9. 继承: 父类的属性无法在子类的 Codable 实现中自动处理。
  10. 枚举处理: 关联值枚举或需要匹配多种原始值的枚举,原生 Codable 支持有限。

社区现状

Swift 社区为了解决 JSON 序列化的问题,涌现出了许多优秀的第三方框架。了解它们的设计哲学和优缺点,能帮助我们更好地理解为什么基于 Swift Macros 的方案是当下更优的选择。

1. 基于自定义协议的框架

ObjectMapper

ObjectMapper 是最早的一批 Swift JSON 解析库之一,它基于自定义的 Mappable 协议:

class User: Mappable {
    var name: String?
    var age: Int?
    
    required init?(map: Map) {}
    
    func mapping(map: Map) {
        name <- map["user_name"]
        age <- map["user_age"]
    }
}

特点:

  • 不依赖 Swift 的原生 Codable
  • 不依赖反射机制
  • 自定义操作符 <- 使映射代码简洁
  • 需要手动编写映射关系
  • 支持嵌套映射和自定义转换

ObjectMapper 的优点是代码相对简洁,且不依赖 Swift 的内部实现细节,但缺点是需要手动编写映射代码,并且与 Swift 的原生序列化机制不兼容。

2. 基于运行时反射的框架

HandyJSON 和 KakaJSON

这两个框架采用了相似的实现原理,都是通过运行时反射获取类型信息:

struct User: HandyJSON {
    var name: String?
    var age: Int?
}

// 使用
let user = User.deserialize(from: jsonString)

特点:

  • 通过底层运行时反射获取类型元数据
  • 直接操作内存进行属性赋值
  • 几乎无需编写额外代码
  • 性能较高

这类框架的主要问题是强依赖 Swift 的内部实现细节和元数据结构(Metadata),随着 Swift 版本升级,容易出现不兼容问题或崩溃。它们实现了"零代码"的理想,但牺牲了稳定性和安全性。

3. 基于属性包装器(Property Wrapper)的框架

ExCodable 和 BetterCodable

这些框架利用 Swift 5.1 引入的属性包装器特性,为 Codable 提供扩展:

struct User: Codable {
    @CustomKey("user_name")
    var name: String
    
    @DefaultValue(33)
    var age: Int
}

特点:

  • 基于 Swift 的原生 Codable
  • 使用属性包装器简化常见编解码任务
  • 无需手动编写 CodingKeys 和 Codable 实现
  • 类型安全,编译时检查

属性包装器方案相比前两类有明显的优势:它既保持了与 Swift 原生 Codable 的兼容性,又简化了代码编写。但 PropertyWrapper 能力有限, 有些复杂的封装设计做不到.

4. 基于宏(Macro)的框架

CodableWrapper、CodableWrappers 和 MetaCodable,以及本文的 ReerCodable

这些框架利用 Swift 5.9 引入的宏特性,在编译时生成 Codable 的实现代码:

@Codable
struct User {
    @CodingKey("user_name")
    var name: String
    
    var age: Int = 33
}

特点:

  • 基于 Swift 的原生 Codable
  • 声明式语法,直观易懂
  • 高度灵活,支持复杂的编解码逻辑
  • 可以在类型级别应用宏

宏方案结合了所有前述方案的优点,同时避免了它们的缺点:它基于原生 Codable,保持类型安全;它支持声明式语法,代码简洁;它在编译时生成代码,没有运行时性能损失;它能够处理复杂场景,适应性强。

为什么 Macro 是最优雅的解决方案?

在所有这些框架中,基于宏的方案(如 ReerCodable)提供了最优雅的解决方案,原因如下:

  1. 与原生 Codable 无缝集成:生成的代码与手写的 Codable 实现完全相同,可以与其他使用 Codable 的 API 完美配合。对于现代三方框架如 Alamofire, GRDB 等都与 Codable 互相兼容..
  2. 声明式语法:通过注解方式声明序列化需求,代码简洁直观,意图明确。
  3. 类型安全:所有操作都在编译时进行类型检查,避免运行时错误。
  4. 高度灵活:可以处理各种复杂场景,如嵌套结构、自定义转换、条件编解码等。
  5. 维护性好:宏生成的代码是可预测的,而且不依赖于 Swift 的内部实现细节,随着 Swift 版本更新不会出现兼容性问题。
  6. 可调试性强:可以查看宏展开后的代码,便于理解和调试。
  7. 可扩展性:可以组合使用不同的宏,构建复杂的编解码逻辑。

ReerCodable 登场:化繁为简的魔法

ReerCodable 利用 Swift Macros 的强大能力,让你只需在类型或属性前添加简单的注解,就能自动生成高效、健壮的 Codable 实现。核心就是 @Codable 宏,它会与其他 ReerCodable 提供的宏协同工作,生成最终的编解码逻辑。框架接入支持 Cocoapods, SwiftPackageManager。

代码实现上参考了优秀的 winddpan/CodableWrapper、GottaGetSwifty/CodableWrappers 和 MetaCodable,相对它们 ReerCodable 有更丰富的 feature 或更简洁的使用。

让我们来看看 ReerCodable 如何优雅地解决上述痛点:

1. 自定义 CodingKey

通过 @CodingKey 可以为属性指定自定义 key,无需手动编写 CodingKeys 枚举:

ReerCodable Codable
@Codable
struct User {
    @CodingKey("user_name")
    var name: String
    
    @CodingKey("user_age")
    var age: Int
    
    var height: Double
}
struct User: Codable {
    var name: String
    var age: Int
    var height: Double
    
    enum CodingKeys: String, CodingKey {
        case name = "user_name"
        case age = "user_age"
        case height
    }
}

2. 嵌套 CodingKey

支持通过点语法表示嵌套的 key path:

@Codable
struct User {
    @CodingKey("other_info.weight")
    var weight: Double
    
    @CodingKey("location.city")
    var city: String
}

3. 多键解码

可以指定多个 key 用于解码,系统会按顺序尝试解码直到成功:

@Codable
struct User {
    @CodingKey("name", "username", "nick_name")
    var name: String
}

4. 命名转换

支持多种命名风格转换,可以应用在类型或单个属性上:

@Codable
@SnakeCase
struct Person {
    var firstName: String  // 从 "first_name" 解码, 或编码为 "first_name"
    
    @KebabCase
    var lastName: String   // 从 "last-name" 解码, 或编码为 "last-name"
}

5. 自定义编解码容器

使用 @CodingContainer 自定义编解码时的容器路径, 通常用于JSON嵌套较多, 但 model 声明 想直接 match 子层级结构:

ReerCodable JSON
@Codable
@CodingContainer("data.info")
struct UserInfo {
    var name: String
    var age: Int
}
{
    "code": 0,
    "data": {
        "info": {
            "name": "phoenix",
            "age": 33
        }
    }
}

6. 编码专用 key

可以为编码过程指定不同的键名, 由于 @CodingKey 可能有多个参数, 再加上可以使用 @SnakeCase, KebabCase 等, 解码可能使用多个 key, 那编码时会采用第一个 key, 也可以通过 @EncodingKey 来指定 key

@Codable
struct User {
    @CodingKey("user_name")      // 解码使用 "user_name", "name"
    @EncodingKey("name")         // 编码使用 "name"
    var name: String
}

7. 默认值支持

解码失败时可以使用默认值, 原生 Codable 针对非 Optional 属性, 会在没有解析到正确值是抛出异常, 即使已经设置了初始值, 或者即使是 Optional 类型的枚举

@Codable
struct User {
    var age: Int = 33
    var name: String = "phoenix"
    // 若 JSON 中不包含 gender, 原生 Codable 会抛出异常, ReerCodable 不会, 会设置其为 nil
    var gender: Gender?
}

enum Gender {
    case male, female
}

8. 忽略属性

使用 @CodingIgnored 在编解码过程中忽略特定属性. 在解码过程中对于非 Optional 属性要有一个默认值才能满足 Swift 初始化的要求, ReerCodable 对基本数据类型和集合类型会自动生成默认值, 如果是其他自定义类型, 则需用用户提供默认值.

@Codable
struct User {
    var name: String
    
    @CodingIgnored
    var ignore: Set<String>
}

9. Base64 编解码

自动处理 base64 字符串与 Data, [UInt8] 类型的转换:

@Codable
struct User {
    @Base64Coding
    var avatar: Data
    
    @Base64Coding
    var voice: [UInt8]
}

10. 集合类型解码优化

使用 @CompactDecoding 在解码数组时自动过滤 null 值, 与 compactMap 是相同的意思:

@Codable
struct User {
    @CompactDecoding
    var tags: [String]  // ["a", null, "b"] 将被解码为 ["a", "b"]
}

同时, DictionarySet 也支持使用 @CompactDecoding 来优化

11. 日期编解码

支持多种日期格式的编解码:

ReerCodable JSON
@Codable
class DateModel {
    @DateCoding(.timeIntervalSince2001)
    var date1: Date
    
    @DateCoding(.timeIntervalSince1970)
    var date2: Date
    
    @DateCoding(.secondsSince1970)
    var date3: Date
    
    @DateCoding(.millisecondsSince1970)
    var date4: Date
    
    @DateCoding(.iso8601)
    var date5: Date
    
    @DateCoding(.formatted(Self.formatter))
    var date6: Date
    
    static let formatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
        dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
        return dateFormatter
    }()
}
{
    "date1": 1431585275,
    "date2": 1731585275.944,
    "date3": 1731585275,
    "date4": 1731585275944,
    "date5": "2024-12-10T00:00:00Z",
    "date6": "2024-12-10T00:00:00.000"
}

12. 自定义编解码逻辑

通过 @CustomCoding 实现自定义的编解码逻辑. 自定义编解码有两种方式:

  • 通过闭包, 以 decoder: Decoder, encoder: Encoder 为参数来实现自定义逻辑:
@Codable
struct User {
    @CustomCoding<Double>(
        decode: { return try $0.value(forKeys: "height_in_meters") * 100.0 },
        encode: { try $0.set($1 / 100.0, forKey: "height_in_meters") }
    )
    var heightInCentimeters: Double
}
  • 通过一个实现 CodingCustomizable 协议的自定义类型来实现自定义逻辑:
// 1st 2nd 3rd 4th 5th  -> 1 2 3 4 5
struct RankTransformer: CodingCustomizable {
    
    typealias Value = UInt
    
    static func decode(by decoder: any Decoder, keys: [String]) throws -> UInt {
        var temp: String = try decoder.value(forKeys: keys)
        temp.removeLast(2)
        return UInt(temp) ?? 0
    }
    
    static func encode(by encoder: Encoder, key: String, value: Value) throws {
        try encoder.set(value, forKey: key)
    }
}

@Codable
struct HundredMeterRace {
    @CustomCoding(RankTransformer.self)
    var rank: UInt
}

自定义实现过程中, 框架提供的方法也可以使编解码更加方便:

public extension Decoder {
    func value<Value: Decodable>(forKeys keys: String...) throws -> Value {
        let container = try container(keyedBy: AnyCodingKey.self)
        return try container.decode(type: Value.self, keys: keys)
    }
}

public extension Encoder {
    func set<Value: Encodable>(_ value: Value, forKey key: String, treatDotAsNested: Bool = true) throws {
        var container = container(keyedBy: AnyCodingKey.self)
        try container.encode(value: value, key: key, treatDotAsNested: treatDotAsNested)
    }
}

13. 继承支持

使用 @InheritedCodable 更好地支持子类的编解码. 原生 Codable 无法解析子类属性, 即使 JSON 中存在该值, 需要手动实现 init(from decoder: Decoder) throws

@Codable
class Animal {
    var name: String
}

@InheritedCodable
class Cat: Animal {
    var color: String
}

14. 枚举支持

为枚举提供丰富的编解码能力:

  • 对基本枚举类型, 以及 RawValue 枚举支持
@Codable
struct User {
    let gender: Gender
    let rawInt: RawInt
    let rawDouble: RawDouble
    let rawDouble2: RawDouble2
    let rawString: RawString
}

@Codable
enum Gender {
    case male, female
}

@Codable
enum RawInt: Int {
    case one = 1, two, three, other = 100
}

@Codable
enum RawDouble: Double {
    case one, two, three, other = 100.0
}

@Codable
enum RawDouble2: Double {
    case one = 1.1, two = 2.2, three = 3.3, other = 4.4
}

@Codable
enum RawString: String {
    case one, two, three, other = "helloworld"
}
  • 支持使用 CodingCase(match: ....) 来匹配多个值或 range
@Codable
enum Phone: Codable {
    @CodingCase(match: .bool(true), .int(10), .string("iphone"), .intRange(22...30))
    case iPhone
    
    @CodingCase(match: .int(12), .string("MI"), .string("xiaomi"), .doubleRange(50...60))
    case xiaomi
    
    @CodingCase(match: .bool(false), .string("oppo"), .stringRange("o"..."q"))
    case oppo
}
  • 对于有关联值的枚举, 支持通用 CaseValue 来匹配关联值, 使用 .label() 来声明有标签的关联值的匹配逻辑, 使用 .index() 来声明没有标签的的关联值的匹配逻辑. ReerCodable 支持两种JSON 格式的枚举匹配
    • 第一种是也是原生 Codable 支持的, 即枚举值和其关联值是父子级的结构:
    @Codable
    enum Video: Codable {
        /// {
        ///     "YOUTUBE": {
        ///         "id": "ujOc3a7Hav0",
        ///         "_1": 44.5
        ///     }
        /// }
        @CodingCase(match: .string("youtube"), .string("YOUTUBE"))
        case youTube
        
        /// {
        ///     "vimeo": {
        ///         "ID": "234961067",
        ///         "minutes": 999999
        ///     }
        /// }
        @CodingCase(
            match: .string("vimeo"),
            values: [.label("id", keys: "ID", "Id"), .index(2, keys: "minutes")]
        )
        case vimeo(id: String, duration: TimeInterval = 33, Int)
        
        /// {
        ///     "tiktok": {
        ///         "url": "https://example.com/video.mp4",
        ///         "tag": "Art"
        ///     }
        /// }
        @CodingCase(
            match: .string("tiktok"),
            values: [.label("url", keys: "url")]
        )
        case tiktok(url: URL, tag: String?)
    }
    
    • 第二种是枚举值和其关联值同级或自定义匹配的结构, 使用 .pathValue() 进行自定义路径值的匹配
    @Codable
    enum Video1: Codable {
        /// {
        ///     "type": {
        ///         "middle": "youtube"
        ///     }
        /// }
        @CodingCase(match: .pathValue("type.middle.youtube"))
        case youTube
        
        /// {
        ///     "type": "vimeo",
        ///     "ID": "234961067",
        ///     "minutes": 999999
        /// }
        @CodingCase(
            match: .pathValue("type.vimeo"),
            values: [.label("id", keys: "ID", "Id"), .index(2, keys: "minutes")]
        )
        case vimeo(id: String, duration: TimeInterval = 33, Int)
        
        /// {
        ///     "type": "tiktok",
        ///     "media": "https://example.com/video.mp4",
        ///     "tag": "Art"
        /// }
        @CodingCase(
            match: .pathValue("type.tiktok"),
            values: [.label("url", keys: "media")]
        )
        case tiktok(url: URL, tag: String?)
    }
    

15. 生命周期回调

支持编解码的生命周期回调:

@Codable
class User {
    var age: Int
    
    func didDecode(from decoder: any Decoder) throws {
        if age < 0 {
            throw ReerCodableError(text: "Invalid age")
        }
    }
    
    func willEncode(to encoder: any Encoder) throws {
        // 在编码前进行处理
    }
}

@Codable
struct Child: Equatable {
    var name: String
    
    mutating func didDecode(from decoder: any Decoder) throws {
        name = "reer"
    }
    
    func willEncode(to encoder: any Encoder) throws {
        print(name)
    }
}

16. JSON 扩展支持

提供便捷的 JSON 字符串和字典转换方法:

let jsonString = "{\"name\": \"Tom\"}"
let user = try User.decode(from: jsonString)

let dict: [String: Any] = ["name": "Tom"]
let user2 = try User.decode(from: dict)

17. 基本类型转换

支持基本数据类型之间的自动转换:

@Codable
struct User {
    @CodingKey("is_vip")
    var isVIP: Bool    // "1" 或 1 都可以被解码为 true
    
    @CodingKey("score")
    var score: Double  // "100" 或 100 都可以被解码为 100.0
}

18. AnyCodable 支持

通过 AnyCodable 实现对 Any 类型的编解码:

@Codable
struct Response {
    var data: AnyCodable  // 可以存储任意类型的数据
    var metadata: [String: AnyCodable]  // 相当于[String: Any]类型
}

19. 生成默认实例

@Codable
@DefaultInstance
struct ImageModel {
    var url: URL
}

@Codable
@DefaultInstance
struct User5 {
    let name: String
    var age: Int = 22
    var uInt: UInt = 3
    var data: Data
    var date: Date
    var decimal: Decimal = 8
    var uuid: UUID
    var avatar: ImageModel
    var optional: String? = "123"
    var optional2: String?
}

会生成以下实例

static let `default` = User5(
    name: "",
    age: 22,
    uInt: 3,
    data: Data(),
    date: Date(),
    decimal: 8,
    uuid: UUID(),
    avatar: ImageModel.default,
    optional: "123",
    optional2: nil
)

⚠️注意: 泛型类型的属性不支持使用 @DefaultInstance

@Codable
struct NetResponse<Element: Codable> {
    let data: Element?
    let msg: String
    private(set) var code: Int = 0
}

20. 生成 copy 方法

使用 Copyable 为模型生成 copy 方法

@Codable
@Copyable
public struct Model6 {
    var name: String
    let id: Int
    var desc: String?
}

@Codable
@Copyable
class Model7<Element: Codable> {
    var name: String
    let id: Int
    var desc: String?
    var data: Element?
}

生成如下 copy 方法, 可以看到, 除了默认 copy, 还可以对部分属性进行更新

public func copy(
    name: String? = nil,
    id: Int? = nil,
    desc: String? = nil
) -> Model6 {
    return .init(
        name: name ?? self.name,
        id: id ?? self.id,
        desc: desc ?? self.desc
    )
}

func copy(
    name: String? = nil,
    id: Int? = nil,
    desc: String? = nil,
    data: Element? = nil
) -> Model7 {
    return .init(
        name: name ?? self.name,
        id: id ?? self.id,
        desc: desc ?? self.desc,
        data: data ?? self.data
    )
}

以上示例展示了 ReerCodable 的主要特性,这些特性可以帮助开发者大大简化编解码过程,提高代码的可读性和可维护性。

ReerCodable 通过一系列精心设计的 Swift Macros,极大地简化了 Codable 的使用,显著减少了样板代码,提高了开发效率和代码可读性。它不仅涵盖了原生 Codable 的大部分场景,还提供了更强大、更灵活的功能,如多 key 解码、命名转换、自定义容器、健壮的默认值处理、强大的枚举支持以及便捷的辅助工具等。

如果你还在为 Codable 的繁琐实现而烦恼,不妨试试 ReerCodable,相信它会给你带来惊喜!

GitHub 地址: github.com/reers/ReerC…

欢迎大家试用、Star、提 Issue 或 PR!让我们一起用更现代、更优雅的方式来编写 Swift 代码!

文章主要由 AI 生成, 具体以 github readme 为准

昨天 — 2025年4月9日掘金 iOS

iOS面试常见问题OC

作者 Sydney
2025年4月7日 17:17

iOS面试常见问题

runtime

Objective-C 的 Runtime 是一个强大的特性,允许开发者在运行时动态地处理类、对象、方法和属性等。通过使用 Runtime,开发者可以实现一些在编译时无法实现的功能。以下是一些常见的 Objective-C Runtime 应用场景:

1. 动态方法解析

通过 Runtime,开发者可以在运行时动态地添加或替换方法。这使得可以根据需要在运行时修改类的行为。

示例:
#import <objc/runtime.h>

@interface MyClass : NSObject
- (void)originalMethod;
@end

@implementation MyClass
- (void)originalMethod {
    NSLog(@"Original Method");
}
@end

void dynamicMethod(id self, SEL _cmd) {
    NSLog(@"Dynamic Method");
}

int main() {
    MyClass *obj = [[MyClass alloc] init];
    [obj originalMethod]; // 输出: Original Method

    // 动态添加方法
    class_addMethod([MyClass class], @selector(dynamicMethod), (IMP)dynamicMethod, "v@:");

    // 调用动态添加的方法
    if ([obj respondsToSelector:@selector(dynamicMethod)]) {
        [obj performSelector:@selector(dynamicMethod)];
    }
    return 0;
}

2. 方法交换(Method Swizzling)

方法交换允许你在运行时交换两个方法的实现。这在实现某些功能(如日志记录、性能监测等)时非常有用。

示例:
#import <objc/runtime.h>

@implementation MyClass

+ (void)load {
    Method originalMethod = class_getInstanceMethod(self, @selector(originalMethod));
    Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzledMethod));

    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)originalMethod {
    NSLog(@"Original Method");
}

- (void)swizzledMethod {
    NSLog(@"Swizzled Method");
    [self swizzledMethod]; // 实际上调用的是 originalMethod
}

@end

3. 动态创建类和对象

使用 Runtime,可以在运行时动态创建类和对象。这在需要根据条件生成不同类的情况下非常有用。

示例:
Class dynamicClass = objc_allocateClassPair([NSObject class], "DynamicClass", 0);
class_addMethod(dynamicClass, @selector(sayHello), (IMP)sayHello, "v@:");
objc_registerClassPair(dynamicClass);

id obj = [[dynamicClass alloc] init];
[obj performSelector:@selector(sayHello)];

4. 属性和协议的动态访问

Runtime 允许你在运行时动态访问和修改对象的属性和协议。这对于实现一些通用功能(如序列化、字典转模型等)非常有用。

示例:
#import <objc/runtime.h>

@interface MyClass : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation MyClass
@end

int main() {
    MyClass *obj = [[MyClass alloc] init];
    [obj setValue:@"Alice" forKey:@"name"];
    NSLog(@"%@", [obj valueForKey:@"name"]); // 输出: Alice
    return 0;
}

5. 反射(Reflection)

通过 Runtime,开发者可以实现反射机制,检查对象的类型、属性、方法等。这对于调试、日志记录和动态行为非常有用。

示例:
unsigned int count;
objc_property_t *properties = class_copyPropertyList([MyClass class], &count);
for (unsigned int i = 0; i < count; i++) {
    const char *propertyName = property_getName(properties[i]);
    NSLog(@"Property: %s", propertyName);
}
free(properties);

6. 动态方法调用

使用 Runtime,可以在运行时动态调用方法,而不需要在编译时确定方法名。这对于实现一些动态行为(如插件机制)非常有用。

示例:
SEL selector = NSSelectorFromString(@"dynamicMethod");
if ([obj respondsToSelector:selector]) {
    [obj performSelector:selector];
}

7. 监控和修改对象行为

通过 Runtime,可以监控和修改对象的行为,例如在方法调用前后添加日志记录、性能监测等功能。

总结

Objective-C 的 Runtime 提供了丰富的功能,使得开发者可以在运行时动态处理类和对象。通过动态方法解析、方法交换、动态创建类和对象、动态访问属性、反射、动态方法调用等技术,开发者可以实现许多强大的功能和灵活的设计模式。然而,使用 Runtime 也要谨慎,因为它可能会导致代码的可读性和可维护性降低。

讲一下多线程和线程安全和数据共享

在软件开发中,多线程、线程安全和数据共享是非常重要的概念,尤其是在需要处理并发操作的应用程序中。以下是对这些概念的详细解释:

1. 多线程

多线程是指在同一个进程中同时运行多个线程。每个线程可以独立执行任务,多个线程可以并发地执行,从而提高程序的效率和响应能力。

1.1. 线程的特点
  • 轻量级:线程比进程更轻量,创建和销毁的开销较小。
  • 共享资源:同一进程中的线程可以共享内存和资源,这使得线程间的通信更加高效。
  • 并发执行:多个线程可以在多核处理器上并行执行,从而提高程序的性能。
1.2. 创建线程

在 Objective-C 中,可以使用 NSThreadGCD(Grand Central Dispatch)或 NSOperation 来创建和管理线程。

使用 NSThread 创建线程:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMethod) object:nil];
[thread start];
使用 GCD 创建线程:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 执行耗时操作
});

2. 线程安全

线程安全是指在多线程环境中,多个线程同时访问共享资源时,不会导致数据不一致或程序崩溃的特性。线程安全的代码可以安全地被多个线程同时执行。

2.1. 线程安全的实现方式
  • 互斥锁(Mutex):使用互斥锁来保护共享资源,确保同一时间只有一个线程可以访问该资源。
NSLock *lock = [[NSLock alloc] init];
[lock lock];
// 访问共享资源
[lock unlock];
  • 读写锁:允许多个线程同时读取共享资源,但在写入时会阻塞其他线程。

  • 信号量(Semaphore):控制对共享资源的访问,允许一定数量的线程同时访问。

  • 原子操作:使用原子操作来确保对共享数据的安全访问。

2.2. 线程不安全的示例

如果多个线程同时修改同一个变量而没有适当的同步机制,可能会导致数据不一致。

@interface Counter : NSObject
@property (nonatomic, assign) NSInteger count;
@end

@implementation Counter
- (void)increment {
    self.count++; // 线程不安全
}
@end

3. 数据共享

数据共享是指多个线程访问同一数据或资源。在多线程环境中,数据共享需要谨慎处理,以避免数据竞争和不一致性。

3.1. 数据共享的挑战
  • 数据竞争:当多个线程同时访问和修改同一数据时,可能会导致数据不一致。
  • 死锁:当两个或多个线程相互等待对方释放资源时,会导致程序无法继续执行。
3.2. 解决数据共享问题的方法
  • 使用锁:通过互斥锁、读写锁等机制来保护共享数据,确保同一时间只有一个线程可以访问。

  • 使用线程安全的数据结构:例如,使用 NSMutableArrayNSMutableDictionary 时,确保在访问时使用锁。

  • 避免共享状态:尽量减少共享数据的使用,使用消息传递或其他设计模式(如 Actor 模型)来避免直接共享状态。

4. 总结

  • 多线程可以提高程序的性能和响应能力,但需要谨慎处理线程安全和数据共享问题。
  • 线程安全确保多个线程同时访问共享资源时不会导致数据不一致。
  • 数据共享需要小心管理,以避免数据竞争和死锁等问题。

在开发多线程应用时,理解这些概念并采取适当的措施来确保线程安全和有效的数据共享是至关重要的。

ios中有哪些锁

1. NSLock

  • 描述:基本的互斥锁,提供简单的锁定和解锁功能。
  • 特点:不支持可重入,适合简单的锁定需求。
示例:
NSLock *lock = [[NSLock alloc] init];
[lock lock];
// 访问共享资源
[lock unlock];

2. NSRecursiveLock

  • 描述:可重入锁,允许同一线程多次获得锁。
  • 特点:适合复杂的多线程场景。
示例:
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
[recursiveLock lock];
// 访问共享资源
[recursiveLock unlock];

3. @synchronized

  • 描述:Objective-C 提供的简单锁机制,使用 @synchronized 语法。
  • 特点:自动管理锁,语法简洁。
示例:
@synchronized(self) {
    // 访问共享资源
}

4. Dispatch Semaphore

  • 描述:GCD 提供的信号量,用于控制对共享资源的访问。
  • 特点:可以限制同时访问共享资源的线程数量。
示例:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 访问共享资源
dispatch_semaphore_signal(semaphore);

5. Dispatch Queue

  • 描述:GCD 的串行队列可以用于避免并发访问。
  • 特点:通过串行化任务来控制访问。
示例:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
    // 访问共享资源
});

6. OSSpinLock(已弃用)

  • 描述:自旋锁,适用于短时间的锁定。
  • 特点:在高竞争情况下可能导致性能问题,已在 iOS 10 及更高版本中被弃用。

7. pthread_mutex

  • 描述:POSIX 线程库提供的互斥锁。
  • 特点:灵活性高,支持多种类型的互斥锁。
示例:
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
// 访问共享资源
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);

8. NSCondition

  • 描述:条件锁,允许线程在某个条件满足时继续执行。
  • 特点:可以在等待条件时释放锁,适合生产者-消费者模型。
示例:
NSCondition *condition = [[NSCondition alloc] init];
[condition lock];
// 等待条件
[condition wait];
// 条件满足后继续执行
[condition unlock];

9. NSConditionLock

  • 描述:条件锁的扩展,允许线程在特定条件下进行锁定和解锁。
  • 特点:可以根据条件的值来控制锁的获取和释放。
示例:
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:1];

[conditionLock lockWhenCondition:1];
// 访问共享资源
[conditionLock unlockWithCondition:2]; // 更新条件

10. NSOperationQueue

  • 描述:基于 NSOperation 的高级并发控制,提供了任务的依赖关系和优先级管理。
  • 特点:适合复杂的任务调度和管理。
示例:
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperationWithBlock:^{
    // 执行任务
}];

总结

在 iOS 开发中,选择合适的锁机制对于确保线程安全至关重要。常用的锁包括 NSLockNSRecursiveLock@synchronized、GCD 的信号量、条件锁(NSConditionNSConditionLock)、以及 pthread_mutex 等。根据具体的应用场景和需求,选择合适的锁可以提高程序的性能和稳定性。

讲一下oc中的block和其底层原理

在 Objective-C 中,Block 是一种封装了代码和上下文的对象,允许将一段代码作为参数传递或在需要时执行。Block 是一种非常强大的特性,广泛用于异步编程、回调、集合操作等场景。以下是对 Block 的详细介绍及其底层原理。

1. Block 的基本概念

  • 定义:Block 是一段可以被调用的代码块,通常用于实现回调或异步操作。
  • 语法:Block 的基本语法如下:
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {
    // code
};
示例:
void (^simpleBlock)(void) = ^{
    NSLog(@"Hello, Block!");
};

simpleBlock(); // 调用 Block

2. Block 的特性

  • 捕获变量:Block 可以捕获并存储其上下文中的变量,包括局部变量和全局变量。
  • 自动内存管理:Block 在使用时会自动管理内存,使用 ARC(Automatic Reference Counting)时,Block 会根据需要自动保留和释放捕获的对象。
  • 可以作为参数传递:Block 可以作为方法的参数传递,方便实现回调机制。
示例:
void performOperation(void (^operation)(void)) {
    operation(); // 执行传入的 Block
}

performOperation(^{
    NSLog(@"Performing operation in Block!");
});

3. Block 的类型

Block 可以分为以下几种类型:

  • 无参数无返回值 Block:不接受参数,也不返回值。
  • 有参数无返回值 Block:接受参数,但不返回值。
  • 无参数有返回值 Block:不接受参数,但返回值。
  • 有参数有返回值 Block:接受参数并返回值。
示例:
// 有参数有返回值 Block
NSInteger (^addBlock)(NSInteger, NSInteger) = ^NSInteger(NSInteger a, NSInteger b) {
    return a + b;
};

NSInteger result = addBlock(5, 3); // result = 8

4. Block 的内存管理

Block 的内存管理与对象的内存管理类似,但有一些特殊之处:

  • 栈 Block:在栈上创建的 Block(如局部 Block)在 Block 所在的作用域结束后会被销毁。
  • 堆 Block:通过 copy 方法将 Block 复制到堆上,堆 Block 可以在作用域外使用。
示例:
void (^stackBlock)(void) = ^{
    NSLog(@"This is a stack Block.");
};

void (^heapBlock)(void) = [stackBlock copy]; // 将 stackBlock 复制到堆上

5. Block 的底层原理

Block 的底层实现涉及到 C 语言的结构体和函数指针。Block 实际上是一个结构体,包含了以下几个部分:

  • 函数指针:指向 Block 中的代码。
  • 捕获的变量:Block 捕获的上下文变量。
  • 引用计数:用于管理 Block 的内存,确保在 Block 被使用时不会被释放。
Block 的结构体示例:
typedef struct {
    void *isa; // 指向类的指针
    int flags; // 标志位
    int reserved; // 保留字段
    void (*invoke)(void *, ...); // 函数指针
    struct {
        // 捕获的变量
    } descriptor;
} Block;

6. Block 的使用场景

  • 异步编程:在网络请求、动画等异步操作中使用 Block 作为回调。
  • 集合操作:在数组、字典等集合类中使用 Block 进行过滤、映射等操作。
  • 事件处理:在 UI 事件处理(如按钮点击)中使用 Block 作为回调。
示例:使用 Block 进行数组过滤
NSArray *numbers = @[@1, @2, @3, @4, @5];
NSArray *evenNumbers = [numbers filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSNumber *evaluatedObject, NSDictionary *bindings) {
    return [evaluatedObject integerValue] % 2 == 0;
}]];

NSLog(@"Even Numbers: %@", evenNumbers); // 输出: Even Numbers: (2, 4)

7. 总结

Block 是 Objective-C 中一种强大的特性,允许将代码块作为对象进行传递和执行。它的底层实现基于结构体和函数指针,支持捕获上下文变量,并通过 ARC 进行内存管理。Block 在异步编程、集合操作和事件处理等场景中得到了广泛应用。理解 Block 的特性和底层原理,有助于更好地利用这一特性来编写高效、可维护的代码。

讲一下iOS开发中的内存优化

在 iOS 开发中,内存优化是确保应用性能和用户体验的重要方面。有效的内存管理可以减少内存使用、提高应用的响应速度,并降低崩溃的风险。以下是一些内存优化的策略和最佳实践:

1. 使用 ARC(Automatic Reference Counting)

  • 描述:ARC 是 Objective-C 和 Swift 中的内存管理机制,自动管理对象的内存。开发者不需要手动调用 retainrelease,ARC 会在对象不再需要时自动释放内存。
  • 最佳实践
    • 确保使用 ARC,而不是手动内存管理。
    • 避免强引用循环(retain cycles),特别是在使用 Block 和 delegate 时。
示例:避免强引用循环
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
    [weakSelf doSomething];
};

2. 使用合适的数据结构

  • 描述:选择合适的数据结构可以有效减少内存占用。
  • 最佳实践
    • 使用 NSArrayNSDictionary 等集合类时,考虑使用 NSMutableArrayNSMutableDictionary 以避免不必要的复制。
    • 对于大量数据,考虑使用 NSCache 来缓存数据,避免重复加载。

3. 图片和资源的优化

  • 描述:图片和其他资源通常占用大量内存。
  • 最佳实践
    • 使用适当的图片格式(如 JPEG、PNG)和分辨率,避免使用过大的图片。
    • 使用 UIImageimageNamed: 方法来缓存图片,避免重复加载。
    • 使用 UIImageimageWithContentsOfFile: 方法来加载不需要缓存的图片。
示例:使用 NSCache 缓存图片
NSCache *imageCache = [[NSCache alloc] init];
UIImage *cachedImage = [imageCache objectForKey:imageKey];
if (!cachedImage) {
    cachedImage = [UIImage imageNamed:imageKey];
    [imageCache setObject:cachedImage forKey:imageKey];
}

4. 及时释放不再使用的对象

  • 描述:确保及时释放不再使用的对象,以减少内存占用。
  • 最佳实践
    • 在视图控制器的 dealloc 方法中释放资源。
    • 使用 nil 清空不再需要的对象引用。
示例:在 dealloc 中释放资源
- (void)dealloc {
    [_someResource release]; // 如果使用手动内存管理
    // ARC 下不需要手动释放
}

5. 使用 Instruments 进行内存分析

  • 描述:Instruments 是 Xcode 提供的性能分析工具,可以帮助开发者检测内存泄漏和不必要的内存使用。
  • 最佳实践
    • 定期使用 Instruments 的 “Leaks” 和 “Allocations” 工具分析应用的内存使用情况。
    • 识别并修复内存泄漏和高内存使用的代码路径。

6. 避免不必要的对象创建

  • 描述:频繁创建和销毁对象会导致内存碎片和性能下降。
  • 最佳实践
    • 重用对象,例如使用对象池(Object Pool)模式。
    • 使用懒加载(Lazy Loading)技术,只有在需要时才创建对象。
示例:懒加载
- (UIImageView *)imageView {
    if (!_imageView) {
        _imageView = [[UIImageView alloc] init];
    }
    return _imageView;
}

7. 使用轻量级的视图和控件

  • 描述:复杂的视图和控件会占用更多内存。
  • 最佳实践
    • 使用轻量级的控件,如 UILabelUIButton 等,避免使用过于复杂的自定义视图。
    • 使用 UIViewdrawRect: 方法时,尽量减少绘制的复杂度。

8. 处理大数据集时使用分页加载

  • 描述:一次性加载大量数据会导致内存占用过高。
  • 最佳实践
    • 使用分页加载(Pagination)技术,分批加载数据,减少内存占用。
    • 在 UITableView 或 UICollectionView 中实现懒加载,只有在需要时才加载可见的单元格。

9. 监控内存警告

  • 描述:iOS 系统会在内存紧张时发送内存警告。
  • 最佳实践
    • 在视图控制器中实现 didReceiveMemoryWarning 方法,释放不必要的资源。
示例:
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // 释放不必要的资源
    self.largeDataArray = nil; // 释放大数组
}

10. 使用 Swift 的内存管理特性

  • 描述:如果使用 Swift 开发,利用 Swift 的内存管理特性(如值类型和引用类型)来优化内存使用。
  • 最佳实践
    • 使用结构体(Struct)而不是类(Class)来减少内存开销。
    • 使用 weakunowned 引用来避免强引用循环。

总结

内存优化是 iOS 开发中的重要环节,通过合理使用 ARC、选择合适的数据结构、优化图片和资源、及时释放对象、使用 Instruments 进行分析等方法,可以有效减少内存使用,提高应用性能。定期监控和分析内存使用情况,及时发现和解决问题,是确保应用稳定性和用户体验的关键。

iOS中启动优化

iOS 应用的启动时间对用户体验至关重要。优化应用的启动时间可以提高用户满意度,减少用户流失。以下是一些有效的 iOS 启动优化策略和最佳实践:

1. 减少主线程的工作量

  • 描述:应用启动时,主线程需要处理 UI 渲染和事件响应。如果主线程被阻塞,启动时间会增加。
  • 最佳实践
    • application:didFinishLaunchingWithOptions: 方法中,尽量减少耗时操作,如网络请求、数据库查询等。
    • 将耗时的初始化操作放到后台线程中进行。
示例:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 耗时操作
        [self performHeavyInitialization];
    });
    return YES;
}

2. 使用懒加载(Lazy Loading)

  • 描述:懒加载是一种延迟加载资源的策略,只有在需要时才加载资源。
  • 最佳实践
    • 对于不立即需要的资源(如图片、数据等),使用懒加载技术,避免在启动时加载所有资源。
示例:
- (UIImageView *)imageView {
    if (!_imageView) {
        _imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"placeholder"]];
    }
    return _imageView;
}

3. 减少启动时的资源加载

  • 描述:应用启动时加载的资源(如图片、数据文件等)会影响启动时间。
  • 最佳实践
    • 使用小尺寸的启动图(Launch Screen)和图标,避免使用过大的图片。
    • 将大文件的加载推迟到应用启动后。

4. 优化启动图(Launch Screen)

  • 描述:启动图是用户在应用启动时看到的第一屏幕,优化启动图可以提升用户的初始体验。
  • 最佳实践
    • 使用简单的启动图,避免复杂的动画和高分辨率的图片。
    • 确保启动图的尺寸和比例与设备屏幕匹配。

5. 使用 App Thinning

  • 描述:App Thinning 是 iOS 提供的一种机制,可以根据用户设备的特性优化应用的安装包。
  • 最佳实践
    • 使用 App Slicing、On-Demand Resources 和 Bitcode 来减少应用的大小和启动时间。

6. 减少第三方库的使用

  • 描述:过多的第三方库会增加应用的启动时间。
  • 最佳实践
    • 只使用必要的第三方库,避免引入不必要的依赖。
    • 考虑使用轻量级的替代方案。

7. 预加载数据

  • 描述:在应用启动时预加载必要的数据可以减少后续操作的延迟。
  • 最佳实践
    • 在应用启动时加载必要的配置和数据,以便在用户首次使用时能够快速响应。

8. 使用 Instruments 进行性能分析

  • 描述:Instruments 是 Xcode 提供的性能分析工具,可以帮助开发者识别启动时间的瓶颈。
  • 最佳实践
    • 使用 Instruments 的 Time Profiler 和 Allocations 工具分析应用的启动过程,找出耗时的操作并进行优化。

9. 避免在主线程中执行阻塞操作

  • 描述:在主线程中执行阻塞操作会导致 UI 卡顿,影响用户体验。
  • 最佳实践
    • 将所有耗时的操作(如网络请求、数据库访问等)放到后台线程中执行。

10. 使用合适的 App Delegate 方法

  • 描述:选择合适的 App Delegate 方法来执行初始化操作可以影响启动时间。
  • 最佳实践
    • application:didFinishLaunchingWithOptions: 中执行必要的初始化,而将不必要的操作推迟到 applicationDidBecomeActive: 中。

11. 监控和优化启动时间

  • 描述:定期监控应用的启动时间,确保在每次更新后都进行优化。
  • 最佳实践
    • 使用 Xcode 的 Debug Navigator 监控启动时间,确保在开发过程中保持启动时间的优化。

总结

优化 iOS 应用的启动时间是提升用户体验的重要环节。通过减少主线程的工作量、使用懒加载、优化启动图、减少资源加载、使用 App Thinning、监控启动时间等策略,可以有效提高应用的启动速度。定期分析和优化启动过程,确保应用在每次更新后都能保持良好的性能。

iOS中wkwebview原生和JS的交互实现

1. Native 调用 JS

方式一: evaluateJavaScript
webView.evaluateJavaScript("javascript:function()") { (result, error) in
    if let error = error {
        print("Error: \(error)")
    }
    // 处理返回结果
    print("Result: \(String(describing: result))")
}
方式二: callAsyncJavaScript (iOS 14.0+)
webView.callAsyncJavaScript("functionName", 
                          arguments: ["param1": "value1"], 
                          in: nil,
                          completionHandler: { result in
    // 处理返回结果
})

2. JS 调用 Native

Native 端配置
// 配置 WKWebViewConfiguration
let config = WKWebViewConfiguration()
let userContentController = WKUserContentController()
        
// 注册 JS 调用的方法名
userContentController.add(self, name: "nativeMethod")
config.userContentController = userContentController

// 创建 WKWebView
let webView = WKWebView(frame: view.bounds, configuration: config)

// 实现 WKScriptMessageHandler 协议
extension ViewController: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, 
                             didReceive message: WKScriptMessage) {
        // 根据 message.name 区分不同的方法
        if message.name == "nativeMethod" {
            // 获取参数
            if let params = message.body as? [String: Any] {
                // 处理业务逻辑
            }
        }
    }
}
JS 端调用
// JS 调用 Native 方法
window.webkit.messageHandlers.nativeMethod.postMessage({
    "param1": "value1",
    "param2": "value2"
});

注意事项

1. 内存管理

需要在页面销毁时移除注册的方法:

deinit {
    config.userContentController.removeScriptMessageHandler(forName: "nativeMethod")
}
2. 参数类型
  • JS 传递给 Native 的参数需要是可序列化的类型
3. 最佳实践
  • 建议封装统一的交互层管理所有交互方法
  • 便于维护和扩展
4. 其他配置选项

通过 WKWebView 的 configuration 可配置更多交互选项:

  • allowsInlineMediaPlayback
  • mediaTypesRequiringUserActionForPlayback
  • allowsAirPlayForMediaPlayback

通过以上配置和实现,就可以实现 Native 和 JS 的双向通信。可以根据实际业务需求,在此基础上扩展更多的交互功能。

OC中消息转发流程

OC消息转发流程步骤如下:

  1. 消息发送与查找:当发送消息[receiver message],系统先在接收者类对象的方法列表找对应方法实现,没找到就沿继承链在父类方法列表找,直到根类NSObject
  2. 动态方法解析:若继承链里没找到,系统尝试动态解析方法。对实例方法调用+ (BOOL)resolveInstanceMethod:(SEL)sel,类方法调用+ (BOOL)resolveClassMethod:(SEL)sel。若能动态添加方法实现并返回YES,消息重新发送;返回NO则进入下一步。
  3. 备用接收者:动态解析失败后,系统调用- (id)forwardingTargetForSelector:(SEL)aSelector。若返回非nil对象,消息转发给它;返回nil则进入完整消息转发流程。
  4. 完整的消息转发
    • 获取方法签名:系统调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector获取消息方法签名。若返回nil,系统调用- (void)doesNotRecognizeSelector:,程序可能崩溃。
    • 转发消息:若得到有效方法签名,系统创建NSInvocation对象封装消息信息,然后调用- (void)forwardInvocation:(NSInvocation *)anInvocation,可在该方法里进一步处理消息,如转发给其他对象。

runloop的概念和应用

概念

在Objective - C(OC)里,RunLoop是一种事件处理的循环机制,它会让程序在没有任务需要处理时进入休眠状态,而在有任务到来时被唤醒并处理这些任务。这有助于降低CPU的占用率,提升程序的性能和响应能力。

从底层来看,RunLoop和线程是紧密关联的。每个线程都有与之对应的RunLoop对象,不过默认情况下,只有主线程的RunLoop会在程序启动时自动创建并运行,而其他线程的RunLoop需要手动创建和启动。

运行机制

RunLoop的运行机制可以概括为以下几个步骤:

  1. 通知观察者RunLoop状态改变时,会通知对应的观察者。
  2. 处理定时器事件:检查是否有已到时间需要执行的定时器,如果有则执行相应的任务。
  3. 处理Input Sources:检查是否有异步的Input Sources事件(如网络请求完成)需要处理。
  4. 进入休眠状态:如果没有需要处理的任务,RunLoop会进入休眠状态,等待新的事件到来。
  5. 被唤醒:当有新的事件(如定时器触发、异步事件完成、用户操作等)到来时,RunLoop会被唤醒。
  6. 处理唤醒时的事件:处理唤醒时接收到的事件。
  7. 再次通知观察者RunLoop完成当前一轮的任务处理后,会再次通知观察者其状态的改变。

应用场景

1. 线程保活

在一些场景下,你可能需要创建一个常驻线程来处理特定的任务。这时可以通过启动该线程的RunLoop来实现线程的保活。示例代码如下:

#import <Foundation/Foundation.h>

@interface MyThread : NSObject
@property (nonatomic, strong) NSThread *thread;
@end

@implementation MyThread

- (instancetype)init {
    self = [super init];
    if (self) {
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];
        [self.thread start];
    }
    return self;
}

- (void)runThread {
    @autoreleasepool {
        // 注册一个输入源,避免RunLoop空转退出
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    }
}

- (void)performTaskOnThread:(void (^)(void))task {
    if (task) {
        [self performSelector:@selector(executeTask:) onThread:self.thread withObject:task waitUntilDone:NO];
    }
}

- (void)executeTask:(void (^)(void))task {
    if (task) {
        task();
    }
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyThread *myThread = [[MyThread alloc] init];
        [myThread performTaskOnThread:^{
            NSLog(@"Task is being executed on the custom thread.");
        }];
        sleep(2);
    }
    return 0;
}
2. 定时器优化

在使用定时器时,如果直接使用NSTimer,可能会因为当前线程的RunLoop模式问题导致定时器不能按时触发。可以将定时器添加到特定的RunLoop模式中,保证其正常工作。例如:

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
3. 界面更新优化

在某些情况下,频繁的界面更新可能会导致界面卡顿。可以利用RunLoop的空闲时间来进行界面更新,避免在主线程的繁忙时段进行大量的UI操作。例如,使用CADisplayLink结合RunLoop,在屏幕刷新时更新界面:

CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateUI)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
4. 异步任务处理

可以在子线程的RunLoop中处理一些耗时的异步任务,当任务完成后再切换到主线程更新UI。例如网络请求,在子线程发起请求,请求完成后在主线程更新界面展示数据。

计时器类型和NSTimer为何不准确

计时器的实现方式

1. NSTimer

NSTimer 是 Foundation 框架提供的一种简单计时器,可用于周期性或一次性执行任务。它与 RunLoop 紧密关联,需要将其添加到 RunLoop 中才能正常工作。

示例代码

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
@end

@implementation MyClass

- (void)startTimer {
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                      target:self
                                                    selector:@selector(timerFired)
                                                    userInfo:nil
                                                     repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

- (void)timerFired {
    NSLog(@"Timer fired!");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc] init];
        [obj startTimer];
        [[NSRunLoop currentRunLoop] run];
    }
    return 0;
}
2. CADisplayLink

CADisplayLink 是一个与屏幕刷新率同步的计时器,通常用于需要和屏幕刷新频率保持一致的场景,如动画效果。每次屏幕刷新时,它会触发一次回调。

示例代码

#import <QuartzCore/QuartzCore.h>
#import <Foundation/Foundation.h>

@interface MyClass : NSObject
@property (nonatomic, strong) CADisplayLink *displayLink;
@end

@implementation MyClass

- (void)startDisplayLink {
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkFired)];
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)displayLinkFired {
    NSLog(@"Display link fired!");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc] init];
        [obj startDisplayLink];
        [[NSRunLoop currentRunLoop] run];
    }
    return 0;
}
3. dispatch_source_t

dispatch_source_t 是 GCD(Grand Central Dispatch)提供的一种定时器,它在后台线程中运行,不受 RunLoop 模式的影响,精度较高。

示例代码

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
@end

@implementation MyClass

- (void)startDispatchTimer {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 0), 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"Dispatch timer fired!");
    });
    dispatch_resume(timer);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc] init];
        [obj startDispatchTimer];
        [[NSRunLoop currentRunLoop] run];
    }
    return 0;
}

NSTimer 不准确的原因

1. RunLoop 模式影响

NSTimer 需要被添加到 RunLoop 中才能工作,而 RunLoop 有多种运行模式。当 RunLoop 处于不同模式时,可能会忽略某些模式下的 NSTimer。例如,当用户滚动 UIScrollView 时,主线程的 RunLoop 会切换到 UITrackingRunLoopMode 模式,此时默认添加到 NSDefaultRunLoopModeNSTimer 就会暂停,直到 RunLoop 切换回 NSDefaultRunLoopMode 模式,这就导致了 NSTimer 计时不准确。

2. 任务阻塞

RunLoop 是一个事件循环,会依次处理各种任务。如果 RunLoop 中存在耗时的任务,会阻塞 RunLoop 的运行,导致 NSTimer 不能按时触发。例如,在主线程中进行大量的计算或者文件读写操作,会使 NSTimer 的触发时间延迟。

3. 系统资源竞争

在系统资源紧张的情况下,如 CPU 负载过高,操作系统可能会优先处理其他重要的任务,从而导致 NSTimer 不能精确地按照设定的时间间隔触发。

dispatch_source_t和NSTimer的区别和各自适用的场景

dispatch_source_tNSTimer 都可用于在特定时间执行任务,但它们在多个方面存在区别,适用场景也有所不同。

区别

1. 底层实现与运行机制
  • NSTimer:基于 RunLoop 实现。RunLoop 会在循环过程中检查 NSTimer 是否达到触发时间,若达到则执行相应任务。这意味着 NSTimer 的执行依赖于 RunLoop 的运行状态和模式。例如,当 RunLoop 处于某些模式(如 UITrackingRunLoopMode)时,添加到默认模式(NSDefaultRunLoopMode)的 NSTimer 可能会暂停,导致计时不准确。
  • dispatch_source_t:基于 Grand Central Dispatch(GCD)实现。GCD 是苹果提供的一种高效的异步任务处理机制,dispatch_source_t 会在指定的队列中独立运行,不受 RunLoop 模式的影响,能更精确地按照设定的时间触发任务。
2. 精度
  • NSTimer:精度相对较低。由于受 RunLoop 中其他任务的影响,当 RunLoop 被阻塞时,NSTimer 可能无法按时触发,导致计时误差。
  • dispatch_source_t:精度较高。它基于系统内核的定时器,能够更精确地控制任务的执行时间,误差较小。
3. 线程处理
  • NSTimer:通常与创建它的线程的 RunLoop 绑定,默认在主线程的 RunLoop 中运行。如果需要在子线程中使用,需要手动启动子线程的 RunLoop
  • dispatch_source_t:可以指定在任意队列(包括全局队列和自定义队列)中运行,使用起来更加灵活。可以很方便地在后台线程中执行任务,避免阻塞主线程。
4. 内存管理
  • NSTimerNSTimer 会对其目标对象(target)持有强引用,可能会导致循环引用问题。需要在适当的时候手动停止并释放 NSTimer,以避免内存泄漏。
  • dispatch_source_t:使用块(block)来处理事件,块会对捕获的变量持有强引用。需要注意块中变量的生命周期管理,避免循环引用。不过,在 GCD 的环境下,内存管理相对更加直观和容易控制。

适用场景

NSTimer 的适用场景
  • 简单的 UI 定时任务:当需要在 UI 界面上进行简单的定时更新,如定时刷新界面数据、显示倒计时等,且对时间精度要求不是非常高时,可以使用 NSTimer。例如,实现一个简单的倒计时按钮:
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, assign) NSInteger countdown;
@property (nonatomic, strong) UIButton *countdownButton;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.countdown = 10;
    self.countdownButton = [UIButton buttonWithType:UIButtonTypeSystem];
    self.countdownButton.frame = CGRectMake(100, 100, 200, 50);
    [self.countdownButton setTitle:[NSString stringWithFormat:@"倒计时: %ld", (long)self.countdown] forState:UIControlStateNormal];
    [self.view addSubview:self.countdownButton];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                  target:self
                                                selector:@selector(updateCountdown)
                                                userInfo:nil
                                                 repeats:YES];
}

- (void)updateCountdown {
    self.countdown--;
    if (self.countdown >= 0) {
        [self.countdownButton setTitle:[NSString stringWithFormat:@"倒计时: %ld", (long)self.countdown] forState:UIControlStateNormal];
    } else {
        [self.timer invalidate];
        self.timer = nil;
        [self.countdownButton setTitle:@"倒计时结束" forState:UIControlStateNormal];
    }
}

@end
  • RunLoop 紧密相关的任务:当任务需要与 RunLoop 的特定模式或事件循环结合时,NSTimer 是一个合适的选择。
dispatch_source_t 的适用场景
  • 高精度定时任务:当对时间精度要求较高,如定时同步数据、定时执行后台任务等,dispatch_source_t 能提供更精确的计时。例如,在后台定时上传日志数据:
#import <Foundation/Foundation.h>

@interface LogUploader : NSObject
- (void)startUploadingLogsPeriodically;
@end

@implementation LogUploader

- (void)startUploadingLogsPeriodically {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 0), 60 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        // 执行上传日志的操作
        NSLog(@"Uploading logs...");
    });
    dispatch_resume(timer);
}

@end
  • 后台线程任务:当需要在后台线程中执行定时任务,且不希望受到主线程 RunLoop 的影响时,dispatch_source_t 可以方便地在指定的队列中运行。
  • 复杂的异步任务调度dispatch_source_t 支持多种事件类型,除了定时器外,还可以处理文件系统变化、信号等事件,适用于更复杂的异步任务调度场景。

OC中属性关键字和其作用

在Objective - C(OC)里,属性关键字用于修饰属性,从而控制属性的内存管理、访问权限、多线程安全等特性。下面是常见的属性关键字及其作用:

内存管理相关关键字

1. assign
  • 作用:主要用于修饰基本数据类型(如intfloatdouble等)和C指针类型。它不会对所引用的对象进行内存管理,只是简单地赋值,不增加对象的引用计数。
  • 示例
@interface MyClass : NSObject
@property (assign, nonatomic) int myInt;
@end
2. retain
  • 作用:用于修饰OC对象类型。当给属性赋值时,会对新值发送retain消息,使对象的引用计数加1,同时对旧值发送release消息,使旧值的引用计数减1。
  • 示例
@interface MyClass : NSObject
@property (retain, nonatomic) NSString *myString;
@end

在ARC(自动引用计数)环境下,retain已被strong替代。

3. strong
  • 作用:这是ARC环境下使用的关键字,功能和retain类似。它表示对对象持有强引用,会增加对象的引用计数,只要有强引用指向对象,对象就不会被释放。
  • 示例
@interface MyClass : NSObject
@property (strong, nonatomic) NSArray *myArray;
@end
4. weak
  • 作用:用于修饰OC对象类型,对对象持有弱引用,不会增加对象的引用计数。当对象的引用计数变为0被释放时,弱引用会自动被置为nil,从而避免野指针问题。常用于解决循环引用问题,如在代理模式中。
  • 示例
@interface MyClass : NSObject
@property (weak, nonatomic) id<MyDelegate> delegate;
@end

访问权限相关关键字

1. readwrite
  • 作用:这是属性的默认访问权限,意味着属性同时具备 getter 和 setter 方法,可以进行读写操作。
  • 示例
@interface MyClass : NSObject
@property (readwrite, nonatomic) NSString *myProperty;
@end
2. readonly
  • 作用:表示属性只有 getter 方法,没有 setter 方法,只能读取属性的值,不能进行赋值操作。
  • 示例
@interface MyClass : NSObject
@property (readonly, nonatomic) NSString *myReadOnlyProperty;
@end

多线程安全相关关键字

1. nonatomic
  • 作用:表示属性不是线程安全的。在多线程环境下,对属性的读写操作可能会出现数据竞争问题,但它的访问速度相对较快,因为不需要进行加锁操作。在大多数情况下,如果不需要考虑线程安全问题,推荐使用nonatomic
  • 示例
@interface MyClass : NSObject
@property (nonatomic, strong) NSString *myString;
@end
2. atomic
  • 作用:表示属性是线程安全的。在多线程环境下,对属性的读写操作会进行加锁,保证同一时间只有一个线程可以访问属性,避免数据竞争问题。但加锁操作会带来一定的性能开销,所以访问速度相对较慢。
  • 示例
@interface MyClass : NSObject
@property (atomic, strong) NSString *myAtomicString;
@end

自定义访问器方法名关键字

1. getter
  • 作用:用于自定义属性的 getter 方法名。
  • 示例
@interface MyClass : NSObject
@property (nonatomic, getter=isEnabled) BOOL enabled;
@end
2. setter
  • 作用:用于自定义属性的 setter 方法名。
  • 示例
@interface MyClass : NSObject
@property (nonatomic, setter=setCustomValue:) int customValue;
@end

属性关键字copy的作用

在Objective - C(OC)里,copy也是一个重要的属性关键字,下面为你详细介绍它及其作用。

基本作用

copy关键字用于修饰属性,当给该属性赋值时,会对赋值对象进行一次复制操作,然后将复制后的对象赋值给属性。这样做的目的是保证属性所持有对象的不可变性,避免外部对原对象的修改影响到属性内部的数据。通常,copy主要用于修饰实现了NSCopying协议的对象,像NSStringNSArrayNSDictionary等。

代码示例

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
@property (copy, nonatomic) NSString *myString;
@end

@implementation MyClass
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *originalString = [NSMutableString stringWithString:@"Hello"];
        MyClass *myObject = [[MyClass alloc] init];
        myObject.myString = originalString;
        
        // 修改原始字符串
        [(NSMutableString *)originalString appendString:@" World"];
        
        NSLog(@"Original string: %@", originalString);
        NSLog(@"Copied string in MyClass: %@", myObject.myString);
    }
    return 0;
}

在上述代码中,myString属性使用了copy关键字。当把originalString赋值给myObject.myString时,会对originalString进行复制操作。之后对originalString进行修改,myObject.myString的值不会受到影响。

适用场景

1. 字符串属性

对于字符串属性,特别是当需要保证字符串内容不可变时,通常会使用copy。因为NSString是不可变的,而NSMutableString是可变的。若属性使用strong修饰,当传入一个NSMutableString对象时,外部对该对象的修改会影响到属性的值;使用copy则可避免这种情况。

@interface MyClass : NSObject
@property (copy, nonatomic) NSString *name;
@end
2. 集合属性

对于数组(NSArray)、字典(NSDictionary)等集合属性,同样可以使用copy来保证集合内容的不可变性。例如:

@interface MyClass : NSObject
@property (copy, nonatomic) NSArray *myArray;
@end

与其他关键字对比

  • strong对比strong只是对对象进行强引用,不会复制对象。当外部修改原对象时,属性所引用的对象也会受到影响;而copy会创建一个新的对象,原对象的修改不会影响到属性所引用的对象。
  • weak对比weak是弱引用,不会增加对象的引用计数,且不持有对象;copy会创建新对象并持有该对象,增加其引用计数。
  • assign对比assign主要用于基本数据类型和C指针类型,不进行内存管理;copy用于实现了NSCopying协议的对象,会进行复制操作和内存管理。

oc中block为何使用strong修饰,代理为何使用weak修饰

在Objective - C(OC)里,block常使用strong修饰,而代理(delegate)一般使用weak修饰,下面分别阐述其原因。

block 使用 strong 修饰的原因

1. 保证 block 的生命周期

block是一个对象,它在内存中有自己的生命周期。当把block作为属性存储时,若使用weak修饰,由于weak不会增加对象的引用计数,一旦block在外部的强引用都被释放,其引用计数变为 0,block就会被销毁。而使用strong修饰,属性会对block持有强引用,增加其引用计数,确保在属性持有期间block不会被提前释放,能正常使用。

示例代码

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
@property (strong, nonatomic) void (^myBlock)(void);
@end

@implementation MyClass

- (void)executeBlock {
    if (self.myBlock) {
        self.myBlock();
    }
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *obj = [[MyClass alloc] init];
        obj.myBlock = ^{
            NSLog(@"Block is executed.");
        };
        [obj executeBlock];
    }
    return 0;
}

在这个例子中,myBlock属性使用strong修饰,确保在调用executeBlock方法时,block仍然存在。

2. 避免 block 被提前释放导致的问题

block被提前释放,当尝试调用它时,就会出现崩溃或未定义行为。使用strong修饰可以有效避免这种情况,保证程序的稳定性。

代理使用 weak 修饰的原因

1. 避免循环引用

循环引用是指两个或多个对象之间相互持有强引用,导致它们的引用计数永远不会变为 0,从而无法被释放,造成内存泄漏。在代理模式中,通常是一个对象(委托者)持有另一个对象(代理)的引用,同时代理对象可能会回调委托者的方法。如果委托者对代理使用strong修饰,代理也对委托者持有强引用,就会形成循环引用。

示例代码

#import <Foundation/Foundation.h>

@protocol MyDelegate <NSObject>
- (void)delegateMethod;
@end

@interface MyClass : NSObject
@property (weak, nonatomic) id<MyDelegate> delegate;
- (void)callDelegateMethod;
@end

@implementation MyClass
- (void)callDelegateMethod {
    if ([self.delegate respondsToSelector:@selector(delegateMethod)]) {
        [self.delegate delegateMethod];
    }
}
@end

@interface AnotherClass : NSObject <MyDelegate>
@property (strong, nonatomic) MyClass *myClass;
@end

@implementation AnotherClass
- (void)delegateMethod {
    NSLog(@"Delegate method is called.");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyClass *myClass = [[MyClass alloc] init];
        AnotherClass *anotherClass = [[AnotherClass alloc] init];
        myClass.delegate = anotherClass;
        anotherClass.myClass = myClass;
        [myClass callDelegateMethod];
    }
    return 0;
}

在这个例子中,MyClassdelegate属性使用weak修饰,避免了MyClassAnotherClass之间的循环引用。当main函数结束时,myClassanotherClass可以正常被释放。

2. 确保代理对象的正确释放

使用weak修饰代理,当代理对象的其他强引用都被释放时,其引用计数变为 0,会被自动释放。同时,委托者的delegate属性会自动置为nil,避免了野指针问题,保证程序的安全性。

Masonry的block中使用self为啥不会造成循环引用

在使用Masonry布局时,在其block中使用self通常不会造成循环引用,下面从Masonry的实现原理、block的类型和内存管理等方面进行详细解释。

1. Masonry的实现原理

Masonry是一个轻量级的布局框架,它通过链式语法简化了Auto Layout的使用。当使用Masonry进行布局时,通常会调用类似如下的代码:

#import "Masonry.h"

@interface MyViewController : UIViewController
@property (nonatomic, strong) UIView *myView;
@end

@implementation MyViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.myView = [[UIView alloc] init];
    [self.view addSubview:self.myView];
    
    [self.myView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self.view);
        make.size.mas_equalTo(CGSizeMake(100, 100));
    }];
}

@end

在上述代码里,mas_makeConstraints方法接收一个block作为参数,在这个block中使用了self.view

2. block的类型与内存管理

OC中的block有三种类型:NSGlobalBlockNSStackBlockNSMallocBlock。在Masonry的场景中,传递给mas_makeConstraints方法的block属于NSMallocBlock,即堆上的block。

当block捕获外部变量时,会对捕获的对象产生引用。对于self,如果block持有self的强引用,同时self又持有block的强引用,就会形成循环引用。但在Masonry中,Masonry框架内部并不会对传入的block进行强引用。

Masonry的mas_makeConstraints方法只是在方法执行过程中使用传入的block来配置约束,方法执行完毕后,block的使命就完成了,不会被Masonry框架长期持有。也就是说,Masonry框架不会让self和block之间形成相互的强引用关系,所以不会造成循环引用。

3. 总结

Masonry的block中使用self不会造成循环引用的原因在于:Masonry框架内部不会对传入的block进行强引用,block在完成约束配置的使命后就会被释放,不会和self形成相互的强引用,进而避免了循环引用的问题。不过,在其他场景下,如果block被某个对象强引用,同时block又捕获了该对象的self,就需要使用弱引用(如__weak typeof(self) weakSelf = self;)来避免循环引用。

说一些sdwebimage的缓存机制

SDWebImage是一个广泛应用于iOS和macOS开发的开源库,它为开发者提供了便捷的图片异步下载和缓存功能。下面详细介绍SDWebImage的缓存机制。

缓存类型

SDWebImage有两种主要的缓存类型,分别是内存缓存(NSCache)和磁盘缓存(文件系统)。

1. 内存缓存(NSCache
  • 原理:使用NSCache对象来存储图片,它是苹果提供的一个类似于字典的容器,不过具备自动清理机制。当系统内存不足时,NSCache会自动释放一些对象以节省内存。
  • 优点:访问速度极快,因为数据存储在内存中,无需进行磁盘I/O操作,能够快速地为视图提供图片,从而提升用户体验。
  • 缺点:存储容量有限,因为内存资源本身就是有限的,并且当应用程序进入后台或者系统内存紧张时,部分缓存可能会被清除。
2. 磁盘缓存(文件系统)
  • 原理:将图片数据以文件的形式存储在设备的磁盘上。SDWebImage会根据图片的URL生成一个唯一的键,然后将图片数据存储在以该键命名的文件中。
  • 优点:存储容量大,能够缓存大量的图片数据,适合长期保存。即使应用程序被关闭或者设备重启,缓存的图片仍然存在。
  • 缺点:访问速度相对较慢,因为涉及到磁盘I/O操作,需要一定的时间来读取和写入文件。

缓存流程

当请求一张图片时,SDWebImage会按照以下流程进行缓存查找和处理:

1. 内存缓存查找
  • 首先,SDWebImage会根据图片的URL生成一个唯一的键,然后在内存缓存中查找是否存在对应的图片。
  • 如果在内存缓存中找到了图片,就直接返回该图片,无需进行后续的磁盘查找和网络请求操作,这样可以极大地提高图片的加载速度。
2. 磁盘缓存查找
  • 若在内存缓存中未找到图片,SDWebImage会接着在磁盘缓存中查找。它会根据生成的键找到对应的文件,并读取文件中的图片数据。
  • 如果在磁盘缓存中找到了图片,会将图片数据解码并存储到内存缓存中,方便后续的快速访问,然后返回该图片。
3. 网络请求
  • 如果在内存缓存和磁盘缓存中都没有找到图片,SDWebImage会发起网络请求,从远程服务器下载图片。
  • 下载完成后,会将图片数据存储到磁盘缓存中,同时解码并存储到内存缓存中,最后返回该图片。

缓存策略

SDWebImage提供了多种缓存策略,开发者可以根据具体需求进行选择:

1. SDWebImageCacheTypeNone
  • 表示不使用任何缓存,每次请求图片时都会直接发起网络请求,不会从内存缓存或磁盘缓存中查找。
2. SDWebImageCacheTypeDisk
  • 优先从磁盘缓存中查找图片,如果磁盘缓存中没有,则发起网络请求。下载完成后,会将图片存储到磁盘缓存中,但不会存储到内存缓存中。
3. SDWebImageCacheTypeMemory
  • 优先从内存缓存中查找图片,如果内存缓存中没有,则发起网络请求。下载完成后,会将图片存储到内存缓存和磁盘缓存中。
4. SDWebImageCacheTypeAll
  • 这是默认的缓存策略,会先从内存缓存中查找图片,如果没有则从磁盘缓存中查找,最后才发起网络请求。下载完成后,会将图片存储到内存缓存和磁盘缓存中。

缓存管理

SDWebImage还提供了一些方法来管理缓存,例如:

1. 清除缓存
  • 可以使用clearMemory方法清除内存缓存,使用clearDisk方法清除磁盘缓存。
  • 还可以使用clearDiskOnCompletion方法在后台线程中异步清除磁盘缓存,并在完成后执行回调。
2. 计算缓存大小
  • 可以使用getSize方法计算磁盘缓存的大小,使用getDiskCount方法计算磁盘缓存中图片的数量。
3. 设置缓存时间
  • 可以使用maxDiskAge属性设置磁盘缓存的最大有效期,超过该时间的缓存文件会被自动清除。

通过这些缓存机制和管理方法,SDWebImage能够有效地减少网络请求,提高图片加载速度,节省用户的流量和设备资源。

在一个for循环中调用SDWebImage加载同一个url的图片,为何实际的图片加载请求只会走一次

即便在 for 循环第二次执行时第一次请求的图片还没加载出来,SDWebImage 也不会再次发起新的网络请求,这得益于其内部的请求管理和去重机制,下面详细解释:

1. 请求队列与任务去重

SDWebImage 内部维护了一个请求队列,当发起一个图片加载请求时,会先检查该请求是否已经存在于队列中。

  • 请求唯一标识:每个图片请求都会根据图片的 URL 生成一个唯一的标识。在 for 循环中多次请求同一 URL 的图片时,这些请求的标识是相同的。
  • 任务去重逻辑:当新的请求到来时,SDWebImage 会检查请求队列中是否已经存在相同标识的请求。如果存在,说明该图片的加载任务已经在进行中,就不会再发起新的网络请求,而是将新的请求添加到已存在请求的回调列表中,等图片加载完成后统一通知所有的回调。

2. 示例代码及原理说明

以下是一段简单的示例代码,模拟在 for 循环中多次请求同一图片的情况:

#import <SDWebImage/SDWebImage.h>
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController
@property (nonatomic, strong) UIImageView *imageView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
    [self.view addSubview:self.imageView];

    NSString *imageURLString = @"https://example.com/image.jpg";
    NSURL *imageURL = [NSURL URLWithString:imageURLString];

    for (int i = 0; i < 5; i++) {
        [self.imageView sd_setImageWithURL:imageURL
                      placeholderImage:nil
                               options:SDWebImageRetryFailed
                             completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
            if (image) {
                NSLog(@"Image loaded successfully!");
            } else {
                NSLog(@"Error loading image: %@", error.localizedDescription);
            }
        }];
    }
}

@end

在上述代码中,for 循环执行了 5 次,每次都请求同一 URL 的图片。当第一次请求发起后,后续的 4 次请求在进入 SDWebImage 内部时,会发现队列中已经存在相同 URL 的请求,于是不会再次发起网络请求,而是将这 4 次请求对应的回调添加到第一次请求的回调列表中。等图片加载完成后,SDWebImage 会依次调用这些回调,通知所有请求方图片已加载完成。

3. 避免资源浪费

这种机制有效地避免了对同一资源的重复请求,减少了网络带宽的浪费和服务器的负载。同时,由于多个请求共享同一个加载任务,也提高了资源的利用效率,使得图片加载的整体性能得到提升。

综上所述,SDWebImage 通过请求队列和任务去重机制,确保了在 for 循环中多次请求同一 URL 的图片时,即使第一次请求的图片还未加载完成,也不会发起新的网络请求。

iOS远程通知流程

iOS 远程通知(Push Notification)的流程涉及设备、应用、苹果服务器(APNs)和第三方服务器之间的协作,以下是其核心流程的详细说明:

1. 设备注册与 Token 获取

步骤
  • 应用请求权限
    应用首次启动时,通过 UNUserNotificationCenter 请求用户授权通知权限(alert、badge、sound 等)。
  • 生成 Device Token
    若用户授权,iOS 会为设备生成一个唯一的 device token(基于设备和应用的组合标识)。
  • 回调处理
    系统通过 didRegisterForRemoteNotificationsWithDeviceToken 回调将 device token 返回给应用。
关键点
  • Token 的唯一性:每个应用在设备上的 Token 不同,且定期更新(需通过回调监听更新)。
  • 证书依赖:应用需配置 Apple 推送证书(开发/生产环境),用于与 APNs 通信。

2. 应用将 Token 上传至 Provider 服务器

步骤
  • 应用发送 Token
    应用将获取的 device token 发送到开发者的服务器(Provider 服务器)。
  • 存储 Token
    Provider 服务器将 Token 与用户账户关联,以便后续发送通知。
关键点
  • Token 有效性:若 Token 过期或设备重新安装应用,需重新获取并更新服务器存储。

3. Provider 服务器向 APNs 发送通知

步骤
  • 构建通知负载
    Provider 服务器根据业务需求,构建符合 APNs 格式的 JSON 通知负载(Payload),包含:
    • aps 字典:必填字段(如 alertbadgesound)。
    • 自定义字段:业务数据(如通知类型、消息 ID)。
  • 建立 SSL 连接
    Provider 服务器通过 SSL 连接到 APNs(沙盒环境或生产环境)。
  • 发送通知请求
    使用 HTTP/2 协议发送 POST 请求,包含目标 device token 和通知负载。
示例负载
{
  "aps": {
    "alert": "新消息!",
    "badge": 1,
    "sound": "default"
  },
  "custom_data": {
    "message_id": "12345"
  }
}

4. APNs 处理与分发通知

步骤
  • 验证请求
    APNs 验证 Provider 服务器的证书和请求合法性。
  • 查找目标设备
    根据 device token 定位目标设备。
  • 推送通知
    通过 Apple 的私有网络将通知发送到设备。
关键机制
  • 静默通知(Silent Push):设置 content-available: 1 可触发后台下载(需配置后台模式)。
  • 通知优先级:通过 priority 字段控制实时性(10 为高优先级,5 为低优先级)。

5. 设备接收与处理通知

步骤
  • 通知到达设备
    设备收到通知后,根据应用状态执行不同操作:
    • 应用在前台:通过 userNotificationCenter:willPresentNotification:withCompletionHandler: 回调处理(可自定义显示逻辑)。
    • 应用在后台/关闭:系统自动显示横幅、声音或更新角标。
  • 用户交互
    用户点击通知时,系统通过 userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler: 回调唤醒应用。
关键点
  • 静默通知处理:应用在后台时,静默通知触发 didReceiveRemoteNotification 回调(需开启后台模式)。
  • 通知分组:iOS 支持按线程或主题分组通知(通过 threadIdentifier 字段)。

流程示意图

应用 → 设备注册 → APNs 获取 Token → 应用上传 Token → Provider 服务器 → APNs → 设备

常见问题与注意事项

  1. Token 失效:设备系统更新、应用卸载重装或 Token 过期时需重新获取。
  2. 证书配置:确保正确配置推送证书(开发/生产环境),避免因证书问题导致通知失败。
  3. 通知延迟:APNs 可能因网络或负载延迟推送,需设计重试机制。
  4. 用户隐私:需明确告知用户通知用途,并提供关闭选项(通过系统设置)。

通过以上流程,开发者可实现高效、可靠的 iOS 远程通知功能,提升用户体验和应用活跃度。

iOS中动态库和静态库的区别

在iOS开发里,动态库和静态库是两种重要的代码复用方式,它们存在诸多区别,下面从多个方面进行详细介绍:

1. 定义与概念

  • 静态库:静态库是一种包含已编译代码的文件,在编译时会被完整地复制到应用程序的可执行文件中。也就是说,应用程序在编译阶段就将静态库的代码整合进来,成为自身的一部分。常见的静态库文件扩展名有 .a(针对C、C++、Objective - C代码)和 .framework(苹果提供的一种包含头文件、库文件、资源文件等的集合形式)。
  • 动态库:动态库同样是包含已编译代码的文件,但在编译时,应用程序不会将动态库的代码复制到自身的可执行文件中,而是在运行时动态地加载。动态库在系统中通常只有一份副本,多个应用程序可以共享使用。iOS中的动态库文件扩展名一般是 .dylib 或者 .framework

2. 编译和链接过程

  • 静态库
    • 编译:静态库的代码会被单独编译成目标文件,然后打包成静态库文件。
    • 链接:在应用程序编译时,链接器会将静态库中的代码复制到应用程序的可执行文件中。这意味着最终生成的可执行文件包含了静态库的所有代码,体积会相应增大。
  • 动态库
    • 编译:动态库的代码也是先编译成目标文件,然后打包成动态库文件。
    • 链接:应用程序在编译时,只是记录下对动态库的引用信息,而不会将动态库的代码复制到可执行文件中。在应用程序运行时,系统会动态地加载所需的动态库到内存中,并将应用程序和动态库进行链接。

3. 内存使用

  • 静态库:由于静态库的代码在编译时被复制到应用程序的可执行文件中,每个使用该静态库的应用程序都会包含一份静态库的副本。这会导致多个应用程序占用更多的磁盘空间,并且在运行时,每个应用程序都会在内存中加载一份静态库的代码,造成内存资源的浪费。
  • 动态库:动态库在系统中通常只有一份副本,多个应用程序可以共享使用。当应用程序需要使用动态库时,系统会将动态库加载到内存中,多个应用程序可以共享这一份动态库,从而节省了内存空间。

4. 更新和维护

  • 静态库:如果静态库的代码发生了更新,开发者需要重新编译包含该静态库的应用程序,并将新的可执行文件发布给用户。这对于开发者来说比较繁琐,而且用户需要重新下载整个应用程序才能使用更新后的静态库。
  • 动态库:动态库的更新相对比较方便。当动态库的代码发生更新时,开发者只需要更新系统中的动态库文件,而不需要重新编译和发布应用程序。用户在下次运行应用程序时,系统会自动加载更新后的动态库。

5. 兼容性

  • 静态库:静态库的代码在编译时就被整合到应用程序中,因此与应用程序的兼容性较好。只要应用程序的编译环境和静态库的编译环境兼容,就可以正常使用。
  • 动态库:动态库的兼容性相对较差。由于动态库是在运行时加载的,如果系统中没有安装相应的动态库,或者动态库的版本不兼容,应用程序可能会出现运行时错误。因此,在使用动态库时,需要确保系统中安装了正确版本的动态库。

6. 应用场景

  • 静态库:适用于一些功能相对稳定、不需要频繁更新的代码模块,如一些基础的工具类库、数学计算库等。使用静态库可以确保应用程序的独立性,不受外部环境的影响。
  • 动态库:适用于一些需要频繁更新、多个应用程序共享的代码模块,如系统框架、第三方SDK等。使用动态库可以节省磁盘空间和内存资源,并且方便代码的更新和维护。

综上所述,静态库和动态库各有优缺点,开发者需要根据具体的需求和场景选择合适的库类型。

讲一下OC中的分类和扩展的区别

在Objective - C(OC)里,分类(Category)和扩展(Extension)都是为类添加功能的重要手段,但它们存在明显区别,下面从多个方面详细介绍。

定义与语法

分类(Category)

分类允许在不修改原类代码的基础上,为已有的类添加新的方法。其语法格式如下:

@interface 类名 (分类名)
// 方法声明
@end

@implementation 类名 (分类名)
// 方法实现
@end

示例代码:

@interface NSString (MyCategory)
- (BOOL)myIsEmpty;
@end

@implementation NSString (MyCategory)
- (BOOL)myIsEmpty {
    return [self length] == 0;
}
@end
扩展(Extension)

扩展也被叫做匿名分类,主要用于为类添加私有属性、方法和成员变量。它通常定义在 .m 文件中,语法格式如下:

@interface 类名 ()
// 属性、方法、成员变量声明
@end

示例代码:

@interface MyClass ()
@property (nonatomic, strong) NSString *privateString;
- (void)privateMethod;
@end

@implementation MyClass
- (void)privateMethod {
    NSLog(@"Private method called.");
}
@end

功能特点

分类(Category)
  • 添加方法:主要用途是为类添加新的方法,这些方法可以在不修改原类代码的情况下使用。
  • 代码组织:有助于将一个大的类按照功能模块进行拆分,提高代码的可读性和可维护性。例如,将 UIViewController 的不同功能(如网络请求、数据处理、界面布局)分别放在不同的分类中。
  • 运行时决议:分类的方法是在运行时被添加到类中的,这意味着可以在运行时动态地为类添加新的行为。
扩展(Extension)
  • 添加私有成员:可以为类添加私有属性、方法和成员变量,这些成员只能在当前类的 .m 文件中访问,起到信息隐藏的作用。
  • 编译时决议:扩展的成员是在编译时就确定的,编译器会将扩展的成员视为类的一部分。

访问权限

分类(Category)
  • 分类中声明的方法是公开的,任何地方都可以调用这些方法。例如,在上述 NSString 分类的例子中,其他类可以直接调用 myIsEmpty 方法。
扩展(Extension)
  • 扩展中声明的属性、方法和成员变量通常是私有的,只能在当前类的 .m 文件中访问。这样可以将类的内部实现细节隐藏起来,提高代码的安全性和可维护性。

局限性

分类(Category)
  • 无法添加属性:分类中不能直接添加属性,因为分类没有足够的空间来存储实例变量。不过,可以通过关联对象(Associated Objects)技术间接实现属性的添加。
  • 方法覆盖问题:如果分类中定义的方法和原类或其他分类中的方法同名,会出现方法覆盖的情况,调用时会优先调用最后编译的分类中的方法,这可能会导致一些难以调试的问题。
扩展(Extension)
  • 依赖原类:扩展必须依赖于原类的实现,不能独立存在。通常在 .m 文件中定义,用于补充原类的私有成员。

使用场景

分类(Category)
  • 为系统类添加功能:例如为 NSStringNSArray 等系统类添加自定义的方法,方便在项目中使用。
  • 代码模块化:将一个大的类按照功能模块拆分成多个分类,使代码结构更加清晰。
扩展(Extension)
  • 隐藏类的内部实现:将类的私有属性、方法和成员变量放在扩展中,避免暴露给外部,提高代码的安全性和可维护性。

  • 临时添加方法:在开发过程中,临时为类添加一些只在当前类中使用的方法。

2025年第一季度3.2f求助排名第一,你还敢违规操作么?

作者 iOS阿玮
2025年4月8日 14:46

前言

从认证微信公众号也有一段时间,发现文章阅读量也不再先之前那么惨淡。甚至还从后台看到有些已经有一些铁粉用户了。

在这里感谢各位读者和广大iOSer同行的支持,抱拳了老铁们~

为了感谢各位的信任与支持,我决定公开第一季度咨询最高的话题。噩梦封号3.2f

fba0d3ffaec3f0f7fb8841ba61a91615.jpg

3.2f榜首

坦白来讲,在2025年第一季度咨询最高,首当其冲的3.2f占比最高的原文是:

App submissions from your account have engaged in concept or feature switch schemes to 
evade the review process, such as dynamically populating different app content after 
review, submitting apps with hidden features, repeatedly submitting misleading apps, 
and/or submitting apps with concrete references to content that you are not authorized 
to provide or is otherwise not appropriate for the App Store. 

翻译如下:

你的账户提交的应用采用了概念或功能切换方案来逃避审核过程,例如在审核后动态填充不同的应用内容,提交隐藏功
能的应用,反复提交误导性的应用,以及/或提交的应用引用了你无权提供的内容或不适合应用商店的内容。

很不幸,今年这种原文的开发者,目前救回来的几率是0%。因为大环境不好,所以这类型的3.2f,都会跟咨询的团队确认,是否属实。如果苹果判定属实,建议就不要付费,避免钱花了,依旧无法救回来。大家都不容易,只能说互相体谅。

基本上已经3.2f咨询从侧重申诉,转型到3.2f问题分析,以及新产品上架规避的方向。

通常违规有以下几个维度:

  • 违规支付,采用第三方通道变现
  • AB面,不包含违规支付,但是功能模块有所隐瞒
  • 马甲包代码,祖传代码重复使用

特别强调:社交类产品在今年是最大重灾区,从本月1号到现在已经有9个3.2f社交产品咨询(从2025年1月1号,截至目前3.2f社交累计咨询人数40+),所以也请各位开发者爱惜自己羽毛,珍重Appstore在线的每一天。

否极泰来

为什么说是否极泰来呢?那些合规的开发者其实迎来了自己的春天!

因为大量违规的竞品自己的一顿骚操作,最后成功作死,把坑位腾出来了。那么Appstore的排名,相对之前会迎来用户喷涌式的自增长红利期

毕竟在市场上少一个竞品,就多一个曝光的机会,运气好还有几率承接前辈的用户流量

所以,违规一时爽,被封火葬场!

应对方案

保守派

如果是产品是远程下发的违规支付,那么建议把App功能调整到Appstore审核版本。(AB面的应用肯定是不适合的,自求多福~)保持着以不变应万变的心态,坚持少做少做的原则

维护派

如果现在的产品可以正常的迭代,并且处于一个健康的审核周期。那么建议,可以尝试考虑去除违规的内容。当然这里就是看是想风浪越大鱼越贵?还是猥琐发育别浪?

毕竟风险和收益是对等的,高额的收益也对应着重头来过的几率++

在成年人的世界里,确实没有绝对的对与错,也尊重每个大环境里艰难前行的同行们~

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

在swiftui中使用实现类似微信右上角的功能菜单

作者 1024小神
2025年4月8日 11:15

因为要开发iOS版本的pakeplus安装包,所以现在需要添加一些实用的功能,比如加载网页可以复制当前网址,可以使用外部safari打开,或者重新加载这样的功能,所以需要模仿实现右上角的功能菜单。实现逻辑就是使用position来控制菜单位置和三角形位置:

实现的代码如下:

import SwiftUI
import WebKit

struct BottomMenuView: View {
    @State private var selectedTab = 0
    @State private var isShowingDrawer = false
    @State private var isShowingMenu = false
    
    // Define your URLs here
    private let urls = [
        "https://www.baidu.com/",
        "https://juejin.cn/",
        "https://chat.deepseek.com/"
    ]
    
    var body: some View {
        ZStack {
            VStack(spacing: 0) {
                // Top Bar with Menu Button
                HStack {
                    Button(action: {
                        isShowingDrawer = true
                    }) {
                        Image(systemName: "line.3.horizontal")
                            .font(.title2)
                            .foregroundColor(.primary)
                    }
                    
                    Spacer()
                    
                    Text("PakePlus")
                    
                    Spacer()
                    
                    // 自定义菜单按钮
                    Button(action: {
                        isShowingMenu.toggle()
                    }) {
                        Image(systemName: "plus.circle")
                            .font(.title2)
                            .foregroundColor(.primary)
                    }
                }
                .background(Color(.systemBackground))
                .padding(.horizontal)
                .padding(.vertical, 6)
                
                // WebView for the selected URL
                WebView(url: URL(string: urls[selectedTab])!)
                    .edgesIgnoringSafeArea(.top)
                
                // Bottom Tab Bar
                HStack(spacing: 0) {
                    ForEach(0 ..< urls.count, id: \.self) { index in
                        Button(action: {
                            selectedTab = index
                        }) {
                            VStack {
                                Image(systemName: tabIcon(for: index))
                                    .font(.system(size: 20))
                                Text(tabTitle(for: index))
                                    .font(.caption)
                            }
                            .foregroundColor(selectedTab == index ? .blue : .gray)
                            .frame(maxWidth: .infinity)
                            .padding(.vertical, 8)
                        }
                    }
                }
                .background(Color(.systemBackground))
                .overlay(
                    Rectangle()
                        .frame(height: 0.5)
                        .foregroundColor(Color(.systemGray4)),
                    alignment: .top
                )
            }
            
            // 自定义菜单
            if isShowingMenu {
                Image(systemName: "arrowtriangle.up.fill")
                    .resizable()
                    .frame(width: 30, height: 30)
                    .position(x: UIScreen.main.bounds.width - 30, y: 50)
                    .foregroundStyle(Color(.systemGray6))
                VStack(alignment: .trailing, spacing: 0) {
                    Button(action: {
                        // 复制网址动作
                        isShowingMenu = false
                    }) {
                        Text("复制网址")
                            .padding(.horizontal, 16)
                            .padding(.vertical, 8)
                            .frame(width: 100)
                    }
                    .background(Color(.systemGray6))
                    .cornerRadius(8, corners: [.topLeft, .topRight])
                    
                    Button(action: {
                        // 外部打开动作
                        isShowingMenu = false
                    }) {
                        Text("外部打开")
                            .padding(.horizontal, 16)
                            .padding(.vertical, 8)
                            .frame(width: 100)
                    }
                    .background(Color(.systemGray6))
                    
                    Button(action: {
                        // 重新加载动作
                        isShowingMenu = false
                    }) {
                        Text("重新加载")
                            .padding(.horizontal, 16)
                            .padding(.vertical, 8)
                            .frame(width: 100)
                    }
                    .background(Color(.systemGray6))
                    .cornerRadius(8, corners: [.bottomLeft, .bottomRight])
                }
                .position(x: UIScreen.main.bounds.width - 60, y: 100)
                .transition(.opacity)
                .background(Color.white.opacity(0.0001))
            }
            
            // Side Drawer
            SideDrawerView(isShowing: $isShowingDrawer)
        }
        .onTapGesture {
            if isShowingMenu {
                isShowingMenu = false
            }
        }
    }
    
    private func tabIcon(for index: Int) -> String {
        switch index {
        case 0:
            return "house.fill"
        case 1:
            return "star.fill"
        case 2:
            return "play.fill"
        default:
            return "circle.fill"
        }
    }
    
    private func tabTitle(for index: Int) -> String {
        switch index {
        case 0:
            return "Home"
        case 1:
            return "Favorites"
        case 2:
            return "Videos"
        default:
            return "Tab"
        }
    }
}

#Preview {
    BottomMenuView()
}

// Add RoundedCorner extension
struct RoundedCorner: Shape {
    var radius: CGFloat = .infinity
    var corners: UIRectCorner = .allCorners

    func path(in rect: CGRect) -> Path {
        let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners,
                                cornerRadii: CGSize(width: radius, height: radius))
        return Path(path.cgPath)
    }
}

extension View {
    func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
        clipShape(RoundedCorner(radius: radius, corners: corners))
    }
}

 

iOS分类中的关联对象:如何用运行时突破Category的存储限制

作者 布多
2025年4月7日 23:43

前言

在 iOS 开发中,关联对象(Associated Objects)是一种强大的运行时特性,它允许我们在 Category 中为已有的类动态添加"属性"。众所周知,Category 的一个重要限制是无法直接添加实例变量,这是因为 Category 是在运行时动态加载的,而类的内存布局(包括实例变量的大小和偏移量)必须在编译时就确定下来。关联对象通过 Runtime 机制巧妙地绕过了这一限制,让我们能够在运行时为对象动态关联任意值,从而实现类似实例变量的效果。

虽然网上关于如何使用关联对象的教程很多,但大多数都停留在使用层面,对其内部实现原理鲜有深入探讨。本文将从 Runtime 源码的角度,和大家一起深入剖析关联对象的底层实现原理,包括其内部数据结构、内存管理机制、线程安全性以及性能考量等关键细节。通过本文,你将全面了解关联对象的工作原理,从而能够更好地在实际开发中运用这一技术。为了能够更好地理解本文内容,建议读者已经对关联对象的基本使用有所了解。

关联对象的底层原理

要理解关联对象的底层原理,我们需要从 Runtime 提供的三个核心 API 开始:

  1. objc_setAssociatedObject - 用于设置关联对象
  2. objc_getAssociatedObject - 用于获取关联对象
  3. objc_removeAssociatedObjects - 用于移除对象的所有关联对象

这三个 API 共同构成了关联对象技术的基础,它们分别负责关联对象的创建、访问和清理工作。让我们先从 objc_setAssociatedObject 的实现开始,深入了解关联对象是如何被存储和管理的。

📝 本文使用的 Runtime 版本是 objc4-906。为了方便阅读,我对代码样式和排版略作了修改,并删减了一些不影响主逻辑的冗余代码。

🔧 我在 这里 维护了一个可以直接运行调试的 Runtime 项目,欢迎大家下载调试源码。

如何添加关联对象

当我们调用 objc_setAssociatedObject 函数来设置关联对象时,Runtime 系统会执行一系列复杂的操作来确保关联对象被正确地存储和管理。这个过程涉及到内存管理、线程安全以及哈希表操作等多个关键环节。让我们通过分析源码,一步步揭开关联对象添加过程的神秘面纱。相关源码整理后如下所示(如果觉得代码太长可以先跳过,后面会有详细解释):

void 
objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, key, value, policy);
}

void
_object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    /*
     将 object 对象包装成 DisguisedPtr,
     这么做有如下目的:
     1. 避免被 ARC 错误处理;
     2. 原生指针值的哈希分布可能不均匀,导致哈希冲突率高;
     3. 防逆向分析。
     */
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    
    // 将 value 和 policy(内存管理策略) 包装成一个 ObjcAssociation 对象。
    ObjcAssociation association{policy, value};

    association.acquireValue() = {
      if (_value) {
             // 根据内存管理策略对 value 执行 retain 或 copy 操作。
             switch (_policy & 0xFF) {
             case OBJC_ASSOCIATION_SETTER_RETAIN:
                 _value = objc_retain(_value);
                 break;
             case OBJC_ASSOCIATION_SETTER_COPY:
                 _value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
                 break;
             }
         }
    }

    bool isFirstAssociation = false;
    {
        // 获取全局锁(在构造函数中加锁,在析构函数中解锁,这项技术被称为 RAII)
        AssociationsManager manager;
        // 获取全局关联对象的哈希表。
        AssociationsHashMap &associations(manager.get());

        if (value) {// 设置关联值
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            // 标记是否为首次关联
            if (refs_result.second) {
                isFirstAssociation = true;
            }

            // 获取 object 对应的关联表(内层哈希表)
            auto &refs = refs_result.first->second;
            // 插入或替换 key 对应的关联值
            auto result = refs.try_emplace(key, std::move(association));
            
            // 若 key 已存在,交换旧值用于后续释放
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {// 删除关联值
            auto refs_it = associations.find(disguised);
            
            // 检查关联表是否为空
            if (refs_it != associations.end()) {
                // 获取 object 对应的关联表(内层哈希表)
                auto &refs = refs_it->second;
                // 获取 key 对应的数据。
                auto it = refs.find(key);
                
                if (it != refs.end()) {
                    // 获取旧值用于释放。
                    association.swap(it->second);
                    // 从内部表上删除键值对
                    refs.erase(it);
                    // 若内层表为空,从全局关联表中移除这个内层表。
                    if (refs.size() == 0) {
                        associations.erase(refs_it);
                    }
                }
            }
        }
    }// 这里会释放局部变量 manager,并执行它的析构函数然后释放掉全局锁。
    
    // 如果是首次设置关联对象,修改 isa 上的 has_assoc 字段为 true。
    if (isFirstAssociation) {
        object->setHasAssociatedObjects();
    }

    // 释放旧值
    association.releaseHeldValue() = {
        if (_value && (_policy & OBJC_ASSOCIATION_SETTER_RETAIN)) {
            objc_release(_value);
        }
    }
}

通过分析上述源码实现,我们可以清晰地看到关联对象的核心存储结构,一个精心设计的双层哈希表系统:

  1. 第一层哈希表(AssociationsHashMap):以对象的内存地址(即 self)为键,映射到该对象的所有关联对象存储表;
  2. 第二层哈希表(ObjectAssociationMap):以传入的key为键,存储具体的关联对象信息。

值得注意的是,系统并不是直接存储传入的 value 对象,而是将其封装在一 个ObjcAssociation 类型中。这个结构体包含两个关键信息:

  • value: 实际存储的关联对象;
  • policy: 内存管理策略(如retain、copy等)。

这种设计既保证了关联对象的快速存取,又实现了灵活的内存管理。

我绘制了一张结构示意图如下所示:

structure.png

让我们以一个具体的例子来说明这个存储结构:

objc_setAssociatedObject(self, @selector(name), @"budo", OBJC_ASSOCIATION_COPY_NONATOMIC) 这行代码为例。

  • 第一个参数 self 对应第一层哈希表(AssociationsHashMap)的键;
  • 第二个参数 @selector(name) 对应第二层哈希表(ObjectAssociationMap)的键;
  • 第三个参数 @"budo" 和第四个参数 OBJC_ASSOCIATION_COPY_NONATOMIC 则被包装成一个 ObjcAssociation 对象。

另外,关于内存管理修饰符的一个重要发现:OBJC_ASSOCIATION_RETAIN_NONATOMIC 和 OBJC_ASSOCIATION_RETAIN 在实际运行时的行为是完全一致的,从上面的源码中也可以发现这一点,我写了一个测试,代码在 这里,结果显示 OBJC_ASSOCIATION_RETAIN_NONATOMIC 和 OBJC_ASSOCIATION_RETAIN 在实际运行时的行为是完全一致的。这是因为关联对象的所有操作都会获取全局锁来确保线程安全,所以无论是否指定 NONATOMIC,都会得到相同级别的同步保护。这一点与属性修饰符 atomic/nonatomic 的行为有所不同。

如何获取关联对象

获取关联对象的值,是通过 objc_getAssociatedObject 函数实现的。这个函数内部会调用 _object_get_associative_reference 来完成实际的获取操作。整个过程也是围绕着双层哈希表进行,相关的源码整理后如下所示:

id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, key);
}

id
_object_get_associative_reference(id object, const void *key) {
    ObjcAssociation association{};

    {
        // 获取全局锁(在构造函数中加锁,在析构函数中解锁)
        AssociationsManager manager;
        // 获取全局关联对象哈希表
        AssociationsHashMap &associations(manager.get());
        // 获取 object 对应的关联表(内层哈希表)
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        // 判断内层表是否为空
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            // 通过 key 找到对应数据。
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                // 获取对象并执行 retain 操作。
                association.retainReturnedValue();
            }
        }
    }// 这里会执行 manager 的析构函数并释放全局锁。

    // 将对象加入自动释放池并返回。
    return association.autoreleaseReturnedValue();
}

从以上代码可以看出,获取关联对象的过程相对简单,主要包含以下几个步骤:

  1. 首先获取全局锁以确保线程安全;
  2. 通过 object 作为键在第一层哈希表(AssociationsHashMap)中查找对应的内层哈希表;
  3. 如果找到内层表,则使用 key 作为键在第二层哈希表(ObjectAssociationMap)中查找 ObjcAssociation 对象;
  4. 如果找到 ObjcAssociation 对象,则根据其内存管理策略对 value 执行 retain 操作;
  5. 最后将 value 加入自动释放池并返回。

整个过程中的关键是双层哈希表的设计,这种设计让我们能够高效地通过对象和键的组合来存取关联值。同时,通过 RAII 技术和自动释放池的使用,保证了内存管理的安全性。

如何移除所有关联对象

关联对象的移除有两种方式:移除单个关联值和移除所有关联值。

对于单个关联值的移除,我们可以通过调用 objc_setAssociatedObject 函数并将 value 参数设置为 nil 来实现。这种方式实际上是复用了关联对象的设置逻辑,当 value 为 nil 时,Runtime 系统会自动清理对应的关联值。具体的实现细节可以参考上面的章节:如何添加关联对象

而本节我们将重点关注如何通过 objc_removeAssociatedObjects 函数一次性移除对象的所有关联值。这个函数通常在对象被释放时由 Runtime 系统自动调用,当然我们也可以根据需要手动调用它。让我们一起来分析这个函数的具体实现:

void objc_removeAssociatedObjects(id object)  {
    if (object && object->hasAssociatedObjects()) {
        _object_remove_associations(object, /*deallocating*/false);
    }
}

void
_object_remove_associations(id object, bool deallocating) {
    ObjectAssociationMap refs{};

    {
        // 获取全局锁(在构造函数中加锁,在析构函数中解锁)
        AssociationsManager manager;
        // 获取全局关联对象哈希表
        AssociationsHashMap &associations(manager.get());
        // 获取 object 对应的关联表(内层哈希表)
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            // 获取旧值用于释放
            refs.swap(i->second);
            
            bool didReInsert = false;
            
            /*
             如果是由 dealloc 触发的,会检查关联对象的内存管理策略是否为 OBJC_ASSOCIATION_SYSTEM_OBJECT。
             这是一个系统内部使用的特殊策略,它要求相关对象必须在其它所有普通关联对象都释放完成后才能被释放。
             猜测这种设计可能是为了确保某些系统级对象能在整个释放流程中保持可用状态。
             */
            if (!deallocating) {
                for (auto &ref: refs) {
                    if (ref.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
                        i->second.insert(ref);
                        didReInsert = true;
                    }
                }
            }
            
            if (!didReInsert) {
                // 从全局关联表中移除这个内部表
                associations.erase(i);
            }
        }
    }// 这里会执行 manager 的析构函数并释放全局锁。
    
    SmallVector<ObjcAssociation *, 4> laterRefs;
    
    // 遍历并释放所有的关联对象。
    for (auto &i: refs) {
        if (i.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
            if (deallocating)
                laterRefs.append(&i.second);
        } else {
            // 释放目标对象。
            i.second.releaseHeldValue();
        }
    }
    
    // 遍历并释放 OBJC_ASSOCIATION_SYSTEM_OBJECT 对象。
    for (auto *later: laterRefs) {
        later->releaseHeldValue();
    }
}

释放所有关联对象的逻辑比较简单,但需要注意一些细节,它的主要流程如下:

  1. 获取全局关联表(AssociationsHashMap),然后根据参数 object(即 self) 获取对应的内层表(ObjectAssociationMap);
  2. 把内层表中的关联对象存储到一个临时变量 refs 中,这样做是为了避免在遍历过程中修改哈希表。
  3. 检查是否存在 OBJC_ASSOCIATION_SYSTEM_OBJECT 类型的关联对象。如果存在且不是由 dealloc 触发的,则需要将这些对象重新插入到内层表中,以确保它们能在最后被释放。
  4. 如果内层表中没有重新插入的对象,则从全局关联表中移除 object 对应的内层表。
  5. 分两步释放关联对象:先释放普通关联对象,再释放 OBJC_ASSOCIATION_SYSTEM_OBJECT 类型的关联对象。这种设计可能是为了确保系统对象在整个释放流程中保持可用状态。

整个过程都是在全局锁的保护下进行的,这保证了线程安全,但也意味着大量使用关联对象可能会影响性能。

相关疑问与注意事项

如何在关联对象中使用 weak 属性

关联对象默认是不支持 weak 属性的。这是因为 weak 特性的实现需要能够获取到属性指针的地址,以便在对象释放时将指针置为 nil。而关联对象是通过哈希表存储的,我们无法获取到存储值的内存地址,故而不能直接使用 weak 特性。

如果你确实需要在关联对象中实现类似 weak 的效果,有以下几种替代方案:

  1. 使用中间对象:创建一个中间对象,将 weak 属性存储在这个对象中,然后通过这个中间对象来间接实现 weak 效果;
  2. 使用 NSHashTable 等弱引用容器;
  3. 手动实现引用计数管理。

关于“中间对象”的具体实现方案和 weak 指针的更多详细内容,可以参考我的另一篇文章:揭开 iOS 中 weak 指针的神秘面纱:从原理到实践

关联对象为什么不能直接添加成员变量

这个问题需要从两个角度来理解:技术实现层面和设计目的层面。

从技术实现层面来说,类的内存布局(包括实例变量的大小和偏移量)必须在编译时就确定下来。一旦类被编译完成,其内存布局就已经固定,无法再动态地添加实例变量。因为分类不仅适用于我们自己项目中的类,更重要的是还包括系统框架和第三方库中的类。

从设计目的层面来说,关联对象的设计初衷就是为了在不改变类内存布局的情况下,实现动态地为对象添加存储能力。这使得我们可以:

  1. 在 Category 中添加"属性",突破 Category 的存储限制;
  2. 为系统类添加自定义属性,而无需继承;
  3. 在运行时动态地为任意对象关联数据。

如果关联对象的实现被限制为只能给当前项目中的类添加属性,那么理论上确实可以通过编译器在编译期间修改类的内存布局来实现。但这样就失去了关联对象最重要的特性 - 运行时动态性,也无法实现对系统和三方库中的类进行扩展。

如果你想了解更多 Category 方面的知识,可以参考我的另一篇文章:2024 再探ObjC-Category:动态特性与运行时实现的极致之美

关联对象和真正的属性有什么区别

虽然关联对象在使用上与普通属性非常相似,但它们在实现机制和性能特征上存在显著差异:

  1. 实现机制:

    • 属性是在编译时就确定的实例变量,直接存储在对象的内存布局中。
    • 关联对象是运行时动态添加的,通过全局哈希表来存储和管理。
  2. 性能开销:

    • 属性访问只需要一次简单的内存偏移计算。
    • 关联对象需要哈希表查找、加解锁等多个操作步骤,性能开销较大,我在 iPhone 8 Plus(iOS15.8.3) 设备上做了一个测试,代码在 这里,结果显示属性比关联对象快了 4 倍左右,这里只测试了单线程的情况,不能反映多线程下的实际性能,结果只当作一个参考。 performance.png
  3. 线程安全:

    • iOS 中的属性一般是非原子的(nonatomic),多线程访问需要手动加锁。
    • 关联对象通过全局锁保证了操作的原子性。
  4. 内存管理:

    • 属性的内存管理由编译器自动处理,通过 strong、weak 等属性修饰符指定。
    • 关联对象需要手动指定内存管理策略,且不支持 weak 引用。
  5. 使用场景:

    • 属性适用于类的核心功能实现。
    • 关联对象主要用于运行时动态扩展功能,尤其是在分类中添加存储能力。

因此,虽然关联对象为我们提供了强大的动态扩展能力,但在性能要求较高的场景下,应优先考虑使用属性来实现。

关联对象的释放时机

关联对象的释放时机与对象本身的释放时机完全一致。当一个对象被释放时,Runtime 系统会自动调用 _object_remove_associations 函数来移除该对象的所有关联对象(和手动调用 objc_removeAssociatedObjects 函数效果类似,除了第二个参数值不一样)。整个调用链路如下所示:

dealloc
└── _objc_rootDealloc
    └── rootDealloc
        └── object_dispose
            └── objc_destructInstance
                └── _object_remove_associations

使用关联对象时的注意事项

虽然关联对象为我们提供了强大的动态扩展能力,但由于其特殊的实现机制,在实际使用过程中我们仍需要注意以下几个关键点,以避免潜在的问题:

  1. 内存管理策略的选择

    • 谨慎使用 OBJC_ASSOCIATION_ASSIGN:这种策略不会持有对象,容易造成野指针崩溃。仅在以下特殊场景考虑使用:
      • 关联对象是 Tagged Pointer(如小整数的 NSNumber);
      • 关联对象与被关联对象具有相同的生命周期;
      • 需要打破循环引用的场景。
    • 即使是 Tagged Pointer 对象,也建议使用带有 RETAIN 或 COPY 的内存管理策略,因为 Runtime 系统内部会自动处理 Tagged Pointer 的内存管理。
  2. 性能考量:关联对象的存取涉及全局哈希表操作和加锁,性能开销比直接访问实例变量大;所以不要在性能敏感的代码路径中频繁操作关联对象;可以考虑使用缓存机制减少访问频率。

  3. 线程安全:关联对象只保证了读写操作是原子的,但多线程访问时仍需注意数据一致性(例如多线程对可变对象的修改);复杂操作时建议额外加锁保护。

  4. 内存管理

    • 在对象释放时,其关联对象会被自动移除;
    • 使用 RETAIN/COPY 策略时要注意避免循环引用;
    • 如果确实需要 weak 引用效果,可以考虑使用 “中间对象” 或 NSHashTable 等方案。
  5. 调试与维护

    • 关联对象不会出现在类的属性列表中,调试时需要特别注意;
    • 建议统一管理关联对象的 key,避免冲突;
    • 适当添加注释说明关联对象的用途和生命周期管理方式。

技术总结

通过对关联对象底层实现原理的深入分析,我们可以得出以下关键结论:

  1. 存储结构

    • 关联对象的底层采用双层哈希表结构实现,保证了快速的存取效率;
    • 第一层以对象地址为键,映射到该对象的所有关联对象;
    • 第二层以关联键为索引,存储具体的关联值和内存管理策略。
  2. 线程安全

    • 关联对象内部通过全局锁保证了存取操作的原子性;
    • 采用 RAII 技术自动管理锁的获取和释放;
    • 但是复杂操作时仍需考虑额外的同步机制。
  3. 内存管理

    • 支持 retain/copy/assign 多种内存管理策略,但不支持 weak 引用;
    • 自动处理关联对象的生命周期,与对象的生命周期一致;
    • 通过 ObjcAssociation 封装实现统一的内存管理。

关联对象技术为 Category 突破了存储限制,但也带来了额外的复杂性和性能开销。在实际开发中,应当根据具体场景权衡使用,合理使用这把 “双刃剑”。

❌
❌