iOS面试常见问题OC

iOS面试常见问题

runtime

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

1. 动态方法解析

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

示例:
objc 复制代码
#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)

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

示例:
objc 复制代码
#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,可以在运行时动态创建类和对象。这在需要根据条件生成不同类的情况下非常有用。

示例:
objc 复制代码
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 允许你在运行时动态访问和修改对象的属性和协议。这对于实现一些通用功能(如序列化、字典转模型等)非常有用。

示例:
objc 复制代码
#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,开发者可以实现反射机制,检查对象的类型、属性、方法等。这对于调试、日志记录和动态行为非常有用。

示例:
objc 复制代码
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,可以在运行时动态调用方法,而不需要在编译时确定方法名。这对于实现一些动态行为(如插件机制)非常有用。

示例:
objc 复制代码
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 创建线程:
objc 复制代码
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMethod) object:nil];
[thread start];
使用 GCD 创建线程:
objc 复制代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 执行耗时操作
});

2. 线程安全

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

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

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

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

2.2. 线程不安全的示例

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

objc 复制代码
@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

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

2. NSRecursiveLock

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

3. @synchronized

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

4. Dispatch Semaphore

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

5. Dispatch Queue

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

6. OSSpinLock(已弃用)

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

7. pthread_mutex

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

8. NSCondition

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

9. NSConditionLock

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

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

10. NSOperationQueue

  • 描述 :基于 NSOperation 的高级并发控制,提供了任务的依赖关系和优先级管理。
  • 特点:适合复杂的任务调度和管理。
示例:
objc 复制代码
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 的基本语法如下:
objc 复制代码
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {
    // code
};
示例:
objc 复制代码
void (^simpleBlock)(void) = ^{
    NSLog(@"Hello, Block!");
};

simpleBlock(); // 调用 Block

2. Block 的特性

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

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

3. Block 的类型

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

  • 无参数无返回值 Block:不接受参数,也不返回值。
  • 有参数无返回值 Block:接受参数,但不返回值。
  • 无参数有返回值 Block:不接受参数,但返回值。
  • 有参数有返回值 Block:接受参数并返回值。
示例:
objc 复制代码
// 有参数有返回值 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 可以在作用域外使用。
示例:
objc 复制代码
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 的结构体示例:
objc 复制代码
typedef struct {
    void *isa; // 指向类的指针
    int flags; // 标志位
    int reserved; // 保留字段
    void (*invoke)(void *, ...); // 函数指针
    struct {
        // 捕获的变量
    } descriptor;
} Block;

6. Block 的使用场景

  • 异步编程:在网络请求、动画等异步操作中使用 Block 作为回调。
  • 集合操作:在数组、字典等集合类中使用 Block 进行过滤、映射等操作。
  • 事件处理:在 UI 事件处理(如按钮点击)中使用 Block 作为回调。
示例:使用 Block 进行数组过滤
objc 复制代码
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 时。
示例:避免强引用循环
objc 复制代码
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
    [weakSelf doSomething];
};

2. 使用合适的数据结构

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

3. 图片和资源的优化

  • 描述:图片和其他资源通常占用大量内存。
  • 最佳实践
    • 使用适当的图片格式(如 JPEG、PNG)和分辨率,避免使用过大的图片。
    • 使用 UIImageimageNamed: 方法来缓存图片,避免重复加载。
    • 使用 UIImageimageWithContentsOfFile: 方法来加载不需要缓存的图片。
示例:使用 NSCache 缓存图片
objc 复制代码
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 中释放资源
objc 复制代码
- (void)dealloc {
    [_someResource release]; // 如果使用手动内存管理
    // ARC 下不需要手动释放
}

5. 使用 Instruments 进行内存分析

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

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

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

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

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

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

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

9. 监控内存警告

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

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

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

总结

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

iOS中启动优化

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

1. 减少主线程的工作量

  • 描述:应用启动时,主线程需要处理 UI 渲染和事件响应。如果主线程被阻塞,启动时间会增加。
  • 最佳实践
    • application:didFinishLaunchingWithOptions: 方法中,尽量减少耗时操作,如网络请求、数据库查询等。
    • 将耗时的初始化操作放到后台线程中进行。
示例:
objc 复制代码
- (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)

  • 描述:懒加载是一种延迟加载资源的策略,只有在需要时才加载资源。
  • 最佳实践
    • 对于不立即需要的资源(如图片、数据等),使用懒加载技术,避免在启动时加载所有资源。
示例:
objc 复制代码
- (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
swift 复制代码
webView.evaluateJavaScript("javascript:function()") { (result, error) in
    if let error = error {
        print("Error: \(error)")
    }
    // 处理返回结果
    print("Result: \(String(describing: result))")
}
方式二: callAsyncJavaScript (iOS 14.0+)
swift 复制代码
webView.callAsyncJavaScript("functionName", 
                          arguments: ["param1": "value1"], 
                          in: nil,
                          completionHandler: { result in
    // 处理返回结果
})

2. JS 调用 Native

Native 端配置
swift 复制代码
// 配置 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 端调用
javascript 复制代码
// JS 调用 Native 方法
window.webkit.messageHandlers.nativeMethod.postMessage({
    "param1": "value1",
    "param2": "value2"
});

注意事项

1. 内存管理

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

swift 复制代码
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来实现线程的保活。示例代码如下:

objc 复制代码
#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模式中,保证其正常工作。例如:

objc 复制代码
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,在屏幕刷新时更新界面:

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

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

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

计时器的实现方式

1. NSTimer

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

示例代码

objc 复制代码
#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;
}

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

示例代码

objc 复制代码
#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 模式的影响,精度较高。

示例代码

objc 复制代码
#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。例如,实现一个简单的倒计时按钮:
objc 复制代码
#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 能提供更精确的计时。例如,在后台定时上传日志数据:
objc 复制代码
#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指针类型。它不会对所引用的对象进行内存管理,只是简单地赋值,不增加对象的引用计数。
  • 示例
objc 复制代码
@interface MyClass : NSObject
@property (assign, nonatomic) int myInt;
@end
2. retain
  • 作用 :用于修饰OC对象类型。当给属性赋值时,会对新值发送retain消息,使对象的引用计数加1,同时对旧值发送release消息,使旧值的引用计数减1。
  • 示例
objc 复制代码
@interface MyClass : NSObject
@property (retain, nonatomic) NSString *myString;
@end

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

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

访问权限相关关键字

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

多线程安全相关关键字

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

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

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

属性关键字copy的作用

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

基本作用

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

代码示例

objc 复制代码
#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则可避免这种情况。

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

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

objc 复制代码
@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不会被提前释放,能正常使用。

示例代码

objc 复制代码
#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修饰,代理也对委托者持有强引用,就会形成循环引用。

示例代码

objc 复制代码
#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进行布局时,通常会调用类似如下的代码:

objc 复制代码
#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 循环中多次请求同一图片的情况:

objc 复制代码
#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 和通知负载。
示例负载
json 复制代码
{
  "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)

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

objc 复制代码
@interface 类名 (分类名)
// 方法声明
@end

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

示例代码:

objc 复制代码
@interface NSString (MyCategory)
- (BOOL)myIsEmpty;
@end

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

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

objc 复制代码
@interface 类名 ()
// 属性、方法、成员变量声明
@end

示例代码:

objc 复制代码
@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)
  • 隐藏类的内部实现:将类的私有属性、方法和成员变量放在扩展中,避免暴露给外部,提高代码的安全性和可维护性。

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

相关推荐
独行soc6 分钟前
2025年常见渗透测试面试题-红队面试宝典下(题目+回答)
linux·运维·服务器·前端·面试·职场和发展·csrf
uhakadotcom21 分钟前
Google Earth Engine 机器学习入门:基础知识与实用示例详解
前端·javascript·面试
uhakadotcom1 小时前
Amazon GameLift 入门指南:六大核心组件详解与实用示例
后端·面试·github
_一条咸鱼_2 小时前
深入解析 Vue API 模块原理:从基础到源码的全方位探究(八)
前端·javascript·面试
_一条咸鱼_3 小时前
深入剖析 Vue 状态管理模块原理(七)
前端·javascript·面试
uhakadotcom3 小时前
一文读懂DSP(需求方平台):程序化广告投放的核心基础与实战案例
后端·面试·github
uhakadotcom4 小时前
拟牛顿算法入门:用简单方法快速找到函数最优解
算法·面试·github
小黑屋的黑小子4 小时前
【数据结构】反射、枚举以及lambda表达式
数据结构·面试·枚举·lambda表达式·反射机制
JiangJiang4 小时前
🚀 Vue人看React useRef:它不只是替代 ref
javascript·react.js·面试
卫崽5 小时前
JavaScript 中的 ?? 与 || 运算符详解
javascript·面试