• 叙利亚避谈战中国是否摆大巴 人员严重不整花式缺阵 2018-05-11
  • 男子嫖娼后恋上卖淫女 女子外出约会被其砍杀 2018-05-11
  • 郭家耀:料指数于23200至23800点水平徘徊 2018-05-05
  • 海外热身天津遭惨败 队长:跟高水平打才能学习 2018-05-05
  • 中国首个海外基地为何选吉布提?扼守全球最重要航道 2018-05-05
  • 张稀哲再示爱新婚妻子:遇到对的人 有你就知足 2018-05-04
  • 崔龙洙称为输球会做自我反省 解拉米为何没进首发 2018-05-04
  • 特朗普打破惯例 罕见“优待”中国记者 2018-05-04
  • 英国脱欧后都柏林能否成为欧洲新金融中心 2018-05-03
  • 英国明年3月底前将启动脱欧程序 脱欧时间表一览 2018-05-03
  • 加拿大“网红”柯基因病辞世 粉丝超70万 2018-05-03
  • BFS牛汇:菲律宾政局动荡 比索面临贬值压力 2018-05-02
  • 哈勃望远镜捕捉到垂死恒星:如蝴蝶蹁跹(图) 2018-05-02
  • 揭秘:小检察官如何替人“消灾” 买通公安法院 2018-05-02
  • 爱神罕见粗口回应外界质疑:老子一点都不在乎 2018-05-01
  • 36选7玩法 >iOS开发

    RunLoop解决卡顿问题

    2018-04-20 13:46 编辑: yyuuzhu 分类:iOS开发 来源:采釆一叶秋的iOS漫步

    人生就像RunLoop,不断的循环、不断的往复。当线程被杀掉,当生命结束,RunLoop就消失了,人生也就结束了。在有限的生命里,为何不让自己像RunLoop一样优雅的活着,享受每一个循环。

    作为一个有强迫症患者,在自己的app中容不下一丝杂质。这不最近遇到一个卡顿问题,查了半天代码没问题,后来发现内存暴涨。这是一个列表页面,加载了很多图片,最后查下来是因为图片太大,导致内存暴涨然后卡顿。作为一个正常的列表,代码写的很完美??墒窃趺唇饩瞿诖姹┱堑奈侍饽??

    这里有一个完整的Demo,包含卡顿与优化后的代码:

    目录
    通过在storyboard中更改两套代码

    军棋玩法:一、卡顿分析

    36选7玩法 www.mnaki.com 最根本的原因是RunLoop转一圈的时间太长了,因为一次RunLoop循环需要解析24张大图,很卡
    既然一次RunLoop加载24张图卡,那能不能一次循环加载1张呢?

    二、分析RunLoop的运行机制

    RunLoop:运行循环
    -保证(线程)不退出
    -负责监听所有事件:时钟、触摸、网络事件,没有事件就睡眠
    -每一条线程上面都有一个RunLoop,但是子线程的RunLoop默认不运行
    RunLoop的事件处理:每当有时钟、触摸、网络事件发生的时候,RunLoop苏醒,执行一次循环,循环执行完毕马上进入睡眠状态。
    可以猜想:能不能通过NSTimer来每间隔一定时间执行一个任务,这样RunLoop每间隔一定时间就会苏醒一次。每苏醒一次就执行加载一张图片。当图片加载完成让NSTimer释放,当列表每滑动一次,让NSTimer重新执行任务。

    如下图,RunLoop 想要跑起来,必须有 Mode 对象支持,而 Mode 里面必须有
    (NSSet *)Source、 (NSArray *)Timer ,源和定时器。
    至于另外一个类(NSArray *)observer是用于监听 RunLoop 的状态,因此不会激活RunLoop。


    Runloop

    observer是用于监听 RunLoop 的状态,我们就可以通过observer来监听runloop的苏醒

    伪代码
    1、创建一个定时器:每间隔0.001s执行一个空方法来唤醒RunLoop
    2、将加载图片的方法装入block,将block加入数组
    3、监听RunLoop的苏醒,苏醒回掉就执行一次就从数组中取出一个事件,执行完的事件从数组中删除

    说干就干。

    三、代码实现

    可以先下载Demo

    1、创建一个定时器:每间隔0.001s执行一个空方法来唤醒RunLoop(这里存在质疑,后面已经回答了质疑)

    self.timer = [NSTimer scheduledTimerWithTimeInterval:0.001 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
    
    -(void)timerMethod{ //啥都不干!! }

    (这里要感谢DreamTracer大神Q提的建议)
    那这里添加定时器又是为了干什么呢?应不应该加呢?下面会讲到。

    2、将加载图片的方法装入block,将block加入数组

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        MyTableViewCell *cell = [[MyTableViewCell alloc] cellWithTableView:tableView withID:@"cell"]; NSLog(@"current:%ld",(long)indexPath.row);
        ...
        cell.myImageView.image = nil;
        cell.secondLImage.image = nil;
        cell.thirdyLImage.image = nil;
        cell.fouthLImage.image = nil; ##### //添加事件 //添加文字 [self addTask:^{
            cell.myLabel.text = [NSString stringWithFormat:@"%zd - Drawing index is top priority", indexPath.row];
        }];
        [self addTask:^{
            cell.thirdLabel.text = [NSString stringWithFormat:@"%zd - Drawing large image is low priority. Should be distributed into different run loop passes.", indexPath.row];
        }]; NSString *path1 = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"png"]; //添加图片 [self addTask:^{ UIImage *image1 = [UIImage imageWithContentsOfFile:path1];
            
            cell.myImageView.image = image1;
        }];
        
        [self addTask:^{ UIImage *image2 = [UIImage imageWithContentsOfFile:path1];
            
            cell.secondLImage.image = image2;
            
        }];
        [self addTask:^{ UIImage *image3 = [UIImage imageWithContentsOfFile:path1];
            
            cell.thirdyLImage.image = image3;
        }];
        [self addTask:^{ UIImage *image4 = [UIImage imageWithContentsOfFile:path1];
            
            cell.fouthLImage.image = image4;
        }]; return cell;
    }
    -(void)addTask:(RunloopBlock)task{ if (!self.timer) {//这是优化 self.timer = [NSTimer scheduledTimerWithTimeInterval:0.001 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
        } //添加任务到数组!! [self.tasks addObject:task];
        
        
    }

    3、监听RunLoop的苏醒,苏醒回掉就执行一次就从数组中取出一个事件,执行完的事件从数组中删除

    #pragma mark -  //添加RunLoop观察者!!  CoreFoundtion 里面 Ref (引用)指针!! -(void)addRunloopObserver{ //拿到当前的runloop CFRunLoopRef runloop = CFRunLoopGetCurrent(); //定义一个context CFRunLoopObserverContext context = { 0,
            (__bridge void *)(self),
            &CFRetain,
            &CFRelease, NULL,
        }; //定义观察 static CFRunLoopObserverRef defaultModeObserver; //创建观察者 defaultModeObserver = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &Callback, &context); //添加当前runloop的观察者!! CFRunLoopAddObserver(runloop, defaultModeObserver, kCFRunLoopCommonModes); //C 语言里面Create相关的函数!创建出来的指针!需要释放 CFRelease(defaultModeObserver);
    } static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){ NSLog(@"gemelaile "); //拿到控制器 ViewController * vc = (__bridge ViewController *)info; if (vc.tasks.count == 0) {//任务执行完成就清掉timer [vc.timer invalidate];
            vc.timer = nil; return;
        }
        RunloopBlock task = vc.tasks.firstObject;
        task(); //干掉第一个任务 [vc.tasks removeObjectAtIndex:0];
    }

    如上代码做到了监听RunLoop的苏醒,每次苏醒都会回掉Callback方法

    4 打开demo,分别引入

    demo

    ViewController(优化过的)与ViewControllerNo(未优化过的)运行,看是不是完美解决卡顿。(可以看看内存,cpu)

    敲黑板

    那么我们现在来讲讲为什么前面我们要添加定时器。
    我们把加载图片的事件放进数组中,每次runloop循环一次就执行一次事件。每次有拖动事件发生,runloop都会自动执行,runloop执行几次呢,我不知道。所以为了安全起见,这里我加了个定时器。当然这里可以添加优化,例如滑动结束后初始化定时器,事件执行完就清理定时器。

    完美解决卡顿问题,RunLoop是不是很强大。
    是不是以为这篇文章就结束了,那你就太小看我了。
    每次有拖动事件发生,runloop都会自动执行,runloop执行几次呢,我不知道。最后我进入了深入的实验了解。

    真正的重点来了

    //创建runloop的即将处理 Source的观察者 static CFRunLoopObserverRef defaultModeObserver1;
        defaultModeObserver1 = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeSources, YES, 0, & sourceTodo, &context); //添加当前runloop的观察者!! CFRunLoopAddObserver(runloop, defaultModeObserver1, kCFRunLoopCommonModes); //C 语言里面Create相关的函数!创建出来的指针!需要释放 CFRelease(defaultModeObserver1); static void sourceTodo(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){ NSLog(@"sourceTodo");
    }

    这样每次有事件就会调用sourceTodo的事件
    在callback也加上输出log,同时把[vc.tasks removeObjectAtIndex:0];注销掉,那样vc.tasks就一直有事件,看看到底callBack会走多少次。这样是不是就解决了我们的疑惑呢

    static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){ //拿到控制器 ViewController * vc = (__bridge ViewController *)info; NSLog(@"CallbackNoTask");//这里还没有执行事件 if (vc.tasks.count == 0) { return;
        }
        RunloopBlock task = vc.tasks.firstObject;
        task(); //干掉第一个任务 // [vc.tasks removeObjectAtIndex:0]; NSLog(@"CallbackHasTask");//这里执行了事件 }

    一个惊人的发现,callback一直停不下来。这样不就是说在callback里执行的

    RunloopBlock task = vc.tasks.firstObject;
    task();

    这也是一个source,只要数组里面有数据,runloop就不会停?。?!

    那这样果断去掉定时器。
    把注销的代码[vc.tasks removeObjectAtIndex:0]打开,运行看看

    事件执行完后,runloop还是会跑几次就结束了。
    功夫不负苦心人,终于算了解决了这个疑惑。再次感谢之前对我的文章提出质疑的大神们,是你们让我有了动力来解决这些疑惑。

    有没有了解的欲望?。?!
    完整的Demo
    下面是RunLoop的一些基础知识,希望对你有帮助

    RunLoop入门

    一、简介

    首先,先象征性的讲下RunLoop的概念
    从字面上看,就可以看出就是兜圈圈,就是一个死循环嘛。

    二、作用

    1.保持程序运行
    2.处理app的各种事件(比如触摸,定时器等等)
    3.节省CPU资源,提高性能。

    三、枯燥知识

    下面是关于RunLoop的一些使用简述。也许有点枯燥,但是也是必须要知道的?。ㄇ煤诎錳ng),我尽量说的通俗易懂一点。

    1.两个API

    首先要知道iOS里面有两套API可以访问和使用RunLoop:

    Foundation

    NSRunLoop

    Core Foundation

    CFRunLoopRef

    上面两套都可以使用,但是要知道CFRunLoopRef是用c语言写的,是开源的,相比于NSRunLoop更加底层,而NSRunLoop其实是对CFRunLoopRef的一个简单的封装。便于使用而已。这样说来,显然CFRunLoopRef的性能要高一点。

    2.RunLoop与线程(形象)

    1.每条线程都有唯一的与之对应的RunLoop对象。
    2.主线程的RunLoop已经创建好了,而子线程的需要手动创建。(也就是说子线程的RunLoop默认是关闭的,因为有时候开了个线程但却没有必要开一个RunLoop,不然反而浪费了资源。 )
    3.RunLoop在第一次获取时创建,在线程结束时销毁。(这就相当于 线程是一个类,RunLoop是类里的实例变量,这样便于理解)

    3.获取RunLoop对象

    Foundation

    [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象 [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

    Core Foundation

    CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象 CFRunLoopGetMain(); // 获得主线程的RunLoop对象

    4.RunLoop相关类

    在Core Foundation中有RunLoop的五个类

    CFRunLoopRef CFRunLoopModeRef CFRunLoopSourceRef CFRunLoopTimerRef CFRunLoopObserverRef

    这五个类的关系如下

    五个类的关系

    由图中可以得出以下几点:
    1.CFRunLoopModeRef代表的是RunLoop的运行模式。
    2.一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。
    3.每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。
    4.如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

    CFRunLoopModeRef

    系统默认注册了5个mode

    kCFRunLoopDefaultMode //App的默认Mode,通常主线程是在这个Mode下运行 UITrackingRunLoopMode //界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响 UIInitializationRunLoopMode // 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用 GSEventReceiveRunLoopMode // 接受系统事件的内部 Mode,通常用不到 kCFRunLoopCommonModes //这是一个占位用的Mode,不是一种真正的Mode

    至于CFRunLoopModeRef的使用我会在 下面的实验三timer的使用中 详细说到。

    四、实验讲解

    这里开始之前,希望您跟着新建一个工程。实操最清晰。
    一、main函数的实验
    再来做个试验:将main的代码添加一个输出NSLog,如下

    int main(int argc, char * argv[]) { @autoreleasepool { int res = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); NSLog(@"-----"); return res;
    
        }
    }

    你猜会输出 “-----” 吗?答案是否定的,你会发现程序始终不会到NSLog(@"-----");这一行来。这就说明了程序一直在运行着。其实这都是RunLoop的功劳,它的其中一个功能就是保持程序的持续运行。有了RunLoop,main里面相当于是这样的代码(伪代码):

    BOOL running = YES; do { // 执行各种操作 } while (running); return 0;

    程序是始终在while里面的,是一个死循环。
    说到这里你肯定又会疑惑,RunLoop是什么时候创建的。其实在UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]))这个函数的内部就已经启动了一个RunLoop,所以函数一直没有返回,这才使得程序保持运行。
    (注意:这个默认启动的RunLoop是和主线程相关的!!!)

    二、NSTimer的使用

    在项目中用的NSTimer其实也和RunLoop有关系,下面我们来做个实验

    实验一 scheduledTimer方法

    新建一个工程,在ViewController中添加一个UIButton,增加button的响应以及timerTest方法,代码如下

    - (IBAction)ButtonDidClick:(id)sender {
        [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
    }
    
    - (void)timerTest
    { NSLog(@"timerTest----");
    }

    点击button可以看到输出台每隔一秒钟就打印"timerTest----"。

    实验二 timerWithTime方法

    代码如下:

    - (IBAction)ButtonDidClick:(id)sender { NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
    }
    
    - (void)timerTest
    { NSLog(@"timerTest----");
    }

    但是实验结果是,点击button后没有反应。为什么呢?
    噢~原来是少加了一句话,添加后的代码如下:

    - (IBAction)ButtonDidClick:(id)sender { NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    }
    
    - (void)timerTest
    { NSLog(@"timerTest----");
    }

    可是,为什么实验二比实验一要多加一句话呢?解:那是因为scheduledTimer方法会自动添加到当前的runloop里面去,而且runloop的运行模式kCFRunLoopDefaultMode,也就是说实验一已经将timer自动加入到了一个运行模式为kCFRunLoopDefaultMode的runloop中。

    实验三 有scrollView的情况下使用Timer

    首先,按钮响应以及timerTest的方法如下:

    - (IBAction)ButtonDidClick:(id)sender { NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    }
    
    - (void)timerTest
    { NSLog(@"timerTest----");
    }

    然后在vc中加一个textView,run起来,

    然后点击按钮,随后滑动textView,根据打印结果可以看出滑动textView的时候是不打印的,奇怪吧。其实说到底还是RunLoop搞的鬼??梢钥吹?,我们把timer加到了NSDefaultRunLoopMode的runLoop中,而在滑动textview的时候,RunLoop就切换到UITrackingRunLoopMode模式,而上面有提到说:在每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。 所以定时器就不起作用了。
    现在可以思考一下解决方法了?。ㄇ煤诎錳ng)
    提示一下,问题出在了模式上面,是不是修改一下模式就好了呢。

    解决方法:
    上面有提到过五个mode

    kCFRunLoopDefaultMode //App的默认Mode,通常主线程是在这个Mode下运行 UITrackingRunLoopMode //界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响 UIInitializationRunLoopMode // 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用 GSEventReceiveRunLoopMode // 接受系统事件的内部 Mode,通常用不到 kCFRunLoopCommonModes //这是一个占位用的Mode,不是一种真正的Mode

    其实如果把mode改为kCFRunLoopCommonModes的话就可以既支持kCFRunLoopDefaultMode又支持UITrackingRunLoopMode了。
    修改如下:
    修改mode类型

    - (IBAction)ButtonDidClick:(id)sender { NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    }

    然后run发现就算滚动textView也不会影响打印。

    写在最后:
    希望这篇文章对您有帮助。当然如果您发现有可以优化的地方,希望您能慷慨的提出来。最后祝您工作愉快!

    作者:采釆一叶秋的iOS漫步
    链接:https://www.jianshu.com/p/ef2599f7251f

    搜索CocoaChina微信公众号:CocoaChina
    微信扫一扫
    订阅每日移动开发及APP推广热点资讯
    公众号:
    CocoaChina
    我要投稿   收藏文章
    上一篇:区块链研习 | 什么是跨链?
    下一篇:AutoLayout调试技巧集合
    我来说两句
    发表评论
    您还没有登录!请登录注册
    所有评论(0

    综合评论

    相关帖子

    sina weixin mail 回到顶部