iOS 面试题二

1.给定一个宽度任意、高度为1的 UIImageView,以宽度的80%为圆心,进行旋转,请写出实现该动画效果的关键代码。

UIImageView * mImageView = [[UIImageView alloc]initWithFrame:CGRectMake(60, 200, 200, 1)];
mImageView.backgroundColor=[UIColor greenColor];
[self.view addSubview:mImageView];
    
CABasicAnimation* rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2 ];//旋转一圈 360°
rotationAnimation.duration = 3.0;  //动画时间
rotationAnimation.cumulative = YES;  // 指定累计
rotationAnimation.repeatCount = 3.0;  //动画重复的次数

//'(0, 0)' is the bottom left corner of the bounds rect, '(1, 1)' is the top right corner. Defaults to'(0.5, 0.5)'
mImageView.layer.anchorPoint = CGPointMake(0.8, 0.5);//锚定点 X 偏移到80%,即0.8,Y 无须偏移
//这里必须设置position,否则圆心仍旧是 mImageView 的中心位置, X、Y 都是相对于 mImageView 父视图的绝对值 X =(60+200*0.8)
mImageView.layer.position = CGPointMake(220, 200);
[mImageView.layer addAnimation:rotationAnimation forKey:@"animation"];  

2.请简单描述一下在子线程中创建并 schedule 一个 NSTimer 的步骤,简要描述一下 NSTimer 在使用过程中可能出现的问题。

先看下 NSTimer 相关的方法如下:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

先说可能出现的问题:

  1. scheduled 开头的方法已经默认添加到了 NSDefaultRunLoopMode,如果放在子线程中那么必须手动启动一下;如果使用timer开头的方法,不要忘记添加到 RunLoop 中: [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
  2. 启动 RunLoop 不能直接使用 [[NSRunLoop currentRunLoop] run],会导致无法终止这个 RunLoop;
  3. 必要的时候使用 CFRunLoopStop(CFRunLoopGetCurrent())来停止RunLoop,或者调用 NSTimer 的 invalidate 方法;
  4. 无论是全局 NSTimer 还是局部 NSTimer,都可能造成循环引用问题,因为 RunLoop 对 NSTimer 有强引用,解决方案:
    ① 如果只兼容到 iOS 10 以上的话,可以使用最新的 API ,无须设置 NSTimer 的 target,也就不会循环引用了;
    ② 如果需要兼容 iOS 10 以下版本的话,那么把 target 通过代理转换一下;

终极解决方案:

① 子线程创建 NSTimer
dispatch_queue_t queue = dispatch_queue_create("com.apple.www", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(actionTime:) userInfo:nil repeats:YES];
    // 这里需要注意不要使用[[NSRunLoop currentRunLoop] run],因为会无法终止这个 RunLoop
    // 而且这里必须是 NSDefaultRunLoopMode,使用 NSRunLoopCommonModes 的话定时器不执行
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});

- (void)dealloc {
    CFRunLoopStop(CFRunLoopGetCurrent());
}

//② NSTimer Category
@interface NSTimer (test)

+ (instancetype)repeatWithInterval:(NSTimeInterval)interval block:(void(^)(NSTimer *timer))block;

@end

@implementation NSTimer (test)

+ (instancetype)repeatWithInterval:(NSTimeInterval)interval block:(void(^)(NSTimer *timer))block {
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(trigger:) userInfo:[block copy] repeats:YES];
    return timer;
}
+ (void)trigger:(NSTimer *)timer {
    void(^block)(NSTimer *timer) = [timer userInfo];
    if (block) {
        block(timer);
    }
}

@end

参考链接:NSTimer不释放问题

3. 如何实现在当前线程等待一个子线程任务执行结束,然后继续执行当前线程任务(比如,使用 NSURLSession 封装一个同步的网络请求)

如果当前线程是在一个串行队列里面,那么无论这个子线程使用同步还是异步,都可以等子线程执行完才会

① 子线程使用同步线程
dispatch_sync(queue, ^{//或者 dispatch_barrier_sync
    for (int i = 0; i<100; i++) {
       NSLog(@"dispatch_barrier_async %d",i);
    }
});

② 子线程通过 dispatch_group_t 线程组来实现,有点大材小用了
dispatch_group_t _waitGroup = dispatch_group_create();
dispatch_group_async(_waitGroup, queue, ^{
    for (int i = 0; i<10; i++) {
        NSLog(@"dispatch_barrier_async %d",i);
    }
});
dispatch_group_notify(_waitGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"I have done dispatch_barrier_async");
});
// 其中 dispatch_group_notify 可以用 dispatch_group_wait 方法替换,如下:
long result = dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER);
if (result == 0) {
    NSLog(@"dispatch_group_t have done");
}

③ 通过信号量阻塞来实现
// dispatch_semaphore_create 用于创建信号量,此处需要设置为 0 ,其他应用中可以设置10、100都行
// dispatch_semaphore_signal 用于将信号量加1
// dispatch_semaphore_wait 会一直等待直到信号量大于等于 1 才会执行后续操作
dispatch_async(queue, 0), ^{
    dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    [网络请求:^{
        //请求回调
        dispatch_semaphore_signal(sema);  
    }];
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
});

// 延伸应用:通过信号量限制线程的最大并发数,如下:
// 这里创建了一个信号量为 5 的 sema ,那么同时并发的线程也就只会有 5 个,执行完一个线程通过 dispatch_semaphore_signal 进行信号量加 1 ,才会继续执行生成新的线程。

dispatch_semaphore_t sema = dispatch_semaphore_create(5);
for (NSInteger i = 0;i<100;i++) {
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),   ^{
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); 
        // doing something
        dispatch_semaphore_signal(sema);
    });
} 

GCD 中的对象在 6.0 之前是不参与 ARC ,OS_OBJECT_USE_OBJC 在 SDK 6.0 之前是没有的
也就是说在 6.0 之前,dispatch_queue_t 创建完了是需要自己手动释放的,使用 dispatch_release。

参考:关于编译选项OS_OBJECT_USE_OBJC
参考:iOS GCD中的信号量

4. 使用 CoreText API 绘制文字“我是中国人”,要求字体大小:18,颜色:“我是”黑色,“中国人”红色。

注意:必须是写在 drawRect 方法里面,否则绘制不生效

- (void)drawRect:(CGRect)rect{
    
//1.获取当前绘图上下文
CGContextRef context = UIGraphicsGetCurrentContext();
    
//2.旋转坐标系(默认和UIKit坐标是相反的)
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
    
//3.创建绘制局域
CGMutablePathRef path = CGPathCreateMutable();
    
//4:创建一个矩形文本区域。
CGRect bounds = CGRectMake(100.0,self.bounds.size.height-150,200.0,50.0);
CGPathAddRect(path,NULL,bounds);
    
// 完成了文本区域的设置,开始准备显示的素材,这里就显示一段文字。
CFStringRef textString = CFSTR("我是中国人");
     
//创建一个多属性字段,maxlength为0;maxlength是提示系统有需要多少内部空间需要保留,0表示不用提示限制。
CFMutableAttributedStringRef attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
     
//为attrString添加内容,也可以用CFAttributedStringCreate 开头的几个方法,根据不同的需要和参数选择合适的方法。这里用替换的方式,完成写入。
CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0),textString);
//此时attrString就有内容了
    
//为多属性字符串增添一个颜色的属性,这里就是奇妙之处,富文本的特点就在这里可以自由的调整文本的属性:比如,颜色,大小,字体,下划线,斜体,字间距,行间距等
CGColorSpaceRef rgbColorSpace =CGColorSpaceCreateDeviceRGB();
    
//创建一个颜色容器对象,这里我们用RGB,当然还有很多其他颜色容器对象,可以根据需要和方便自由搭配,总有一款适合你的。
//创建一个红色的对象
CGFloat components[] = {1.0,0.0,0.0,0.8};
CGColorRef red = CGColorCreate(rgbColorSpace,components);
//创建一个黑色的对象
CGFloat components_b[] = {0.0,0.0,0.0,1.0};
CGColorRef black = CGColorCreate(rgbColorSpace,components_b);
CGColorSpaceRelease(rgbColorSpace);//不用的对象要及时回收哦

//增添颜色属性  CFAttributedStringSetAttribute(attrString,CFRangeMake(0,2),kCTForegroundColorAttributeName,black);     CFAttributedStringSetAttribute(attrString,CFRangeMake(2,3),kCTForegroundColorAttributeName,red);
    
//设置字体大小
UIFont  *uiFont = [UIFont fontWithName:@"Helvetica" size:18.0];
CTFontRef ctFont = CTFontCreateWithName((CFStringRef) uiFont.fontName, uiFont.pointSize, NULL);
CFAttributedStringSetAttribute(attrString,CFRangeMake(0,5), kCTFontAttributeName, ctFont);
    
//kCTForegroundColorAttributeName,是CT定义的表示文本字体颜色的常量,类似的常量茫茫多,组成了编辑富文本的诸多属性。
//通过多属性字符,可以得到一个文本显示范围的工厂对象,我们最后渲染文本对象是通过这个工厂对象进行的。这部分需要引入#import<CoreText/CoreText.h>
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
     
//创建一个有文本内容的范围
CTFrameRef frame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0,0),path,NULL);
//把内容显示在给定的文本范围内;
CTFrameDraw(frame,context);
    
//完成任务就把我们创建的对象回收掉,内存宝贵,不用了就回收,这是好习惯。
CFRelease(frame);
CFRelease(framesetter);
CFRelease(path);
CFRelease(ctFont);
CFRelease(attrString);
}

参考:Core Text 基本使用

5. 请实现如下定义的 UIView Category 方法,返回 self 上用于响应位于点 P 的点击事件的子视图(包含点 P 的最上层的子视图)。

@interface UIView (Additions)
- (UIView *)viewWithLocalPoint:(CGPoint)p;
@end

解析:这道题耗时实在是太长了,必须说这是我的知识盲区
首先我理解题目出了偏差,我以为要返回「包含点 P 的所有子视图」;
其次我犯了一个超级低级错误,for 循环里面直接 return 了,还百思不得其解为什么只返回了一个view;

最初解法如下:

- (UIView *)viewWithLocalPoint:(CGPoint)p{
    for (UIView *v in self.subviews) {
      // 判断坐标系点是否在视图范围内
        if ([v pointInside:p withEvent:nil]) {
             return v;
        }
    }
     return nil;
}

然后我以为是方法调用出了问题,于是又去找了各种方法来试验,把 hitTest 方法也搬出来还不好用

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

后来各种日志打印才回过味来是return出了问题,当机立断去掉了return,可是打印出来的那些view也并不完全准确,时而准确时而错误,我就有点懵逼了😂

今天又看了两篇文章才找到了问题所在,划重点「 pointInside 方法中 point 必须为调用者的坐标系」,也就是需要把 point 转换一下坐标系才能用 [self convertPoint:p toView:v],到此终于把问题解决了。

到此呢我又多想了一部,因为答案看起来有点简单,不知道还有没有别的坑,于是想到了如果是点击了其他控件呢,比如 UIButton 或者是 UILabel,事实是 UIButton 是可以响应事件的,不会走响应链这条路,而且 UIButton 是继承于 UIControl 的,跟 UIView 完全不是一条路子,而 UILabel 就比较简单了,直接继承于 UIView,所以也无须多虑。(UILabel : UIView : UIResponder),所以只有 UIResponder 系列的组件才会有响应链这个说法。

参考:一篇搞定事件传递、响应者链条、hitTest和pointInside的使用
参考:iOS 坐标系转换(convertPoint)以及点在范围内的判断(pointInside)

最终解法如下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //获取点击点的坐标
    CGPoint touchPoint = [[touches  anyObject] locationInView:self.view];
    [self.view viewWithLocalPoint:touchPoint];
}

- (UIView *)viewWithLocalPoint:(CGPoint)p{
    UIView *view;
    for (UIView *v in self.subviews) {
        //转换坐标系点
        CGPoint convertPoint = [self convertPoint:p toView:v];
        // 判断坐标系点是否在视图范围内
        if ([v pointInside:convertPoint withEvent:nil]) {
            view = v;
        }
    }
    
    if (view != nil) {
        return view;
    }else{
        return nil;
    }
}

6. 一个数如果恰好等于它的因子之和,这个数就被称为“完数”。例如 6 = 1 + 2 + 3,请找出 1000 以内的所有完数。

下面是我写的比较粗糙的解法,应该还有很大优化的空间
注意两个点:①. 从1开始循环,否则会有0为被除数的问题;
②. 计算和的时候记得等到二层循环结束再计算,否则会出来一个24的错误结果。

- (void)test10000 {
    for (int i=1; i<=1000;i++ ) {
        int temp=0;
        for (int j=1; j<i; j++) {
            if (i%j==0) {
                temp+=j;
            }
            if (j+1==i&&temp == i) {
                NSLog(@"i==%d",i);
            }
        }
    }
}

想到 i/2之后的数字都不会是 i 的因子了,所以又写了个改进版本的

改进版,可以提升一倍的速度,i/2之后的数字无须再循环,因为不会再有因子了
for (int i=1; i<=1000;i++ ) {
        int temp=0;
        for (int j=1; j<(i%2==0?i/2+1:(i+1)/2); j++) {
            if (i%j==0) {
                temp+=j;
                if ((j==i/2||j+1==(i+1)/2)&&temp == i) {
                   NSLog(@"i==%d",i);
               }
            }
           
        }
    }

整套面试题 demo 下载链接

推荐阅读更多精彩内容