市面上的iOS卡顿分析方案有三种:监控FPS、监控RunLoop、ping主线程

前面2个都比较熟悉,第三个是最近才了解到的。

方案一:监控FPS

一般来说,我们约定60FPS即为流畅。那么反过来,如果App在运行期间出现了掉帧,即可认为出现了卡顿。

监控FPS的方案几乎都是基于CADisplayLink实现的。简单介绍一下CADisplayLink:CADisplayLink是一个和屏幕刷新率保持一致的定时器,一但 CADisplayLink 以特定的模式注册到runloop之后,每当屏幕需要刷新的时候,runloop就会调用CADisplayLink绑定的target上的selector。
可以通过向RunLoop中添加CADisplayLink,根据其回调来计算出当前画面的帧数。

#import "FPSMonitor.h"
#import <UIKit/UIKit.h>@interface FPSMonitor ()
@property (nonatomic, strong) CADisplayLink* link;
@property (nonatomic, assign) NSInteger count;
@property (nonatomic, assign) NSTimeInterval lastTime;
@end@implementation FPSMonitor- (void)beginMonitor {_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsInfoCaculate:)];[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];}- (void)fpsInfoCaculate:(CADisplayLink *)sender {if (_lastTime == 0) {_lastTime = sender.timestamp;return;}_count++;double deltaTime = sender.timestamp - _lastTime;if (deltaTime >= 1) {NSInteger FPS = _count / deltaTime;_lastTime = sender.timestamp;_count = 0;NSLog(@"FPS: %li", (NSInteger)ceill(FPS + 0.5));}
}@end

FPS的好处就是直观,小手一划后FPS下降了,说明页面的某处有性能问题。坏处就是只知道这是页面的某处,不能准确定位到具体的堆栈。


方案二:监控RunLoop

首先来介绍下什么是RunLoop。RunLoop是维护其内部事件循环的一个对象,它在程序运行过程中重复的做着一些事情,例如接收消息、处理消息、休眠等等。

所谓的事件循环,就是对事件/消息进行管理,没有消息时,休眠线程以避免资源消耗,从用户态切换到内核态。

有事件/消息需要进行处理时,立即唤醒线程,回到用户态进行处理。

#import <UIKit/UIKit.h>
#import "AppDelegate.h"int main(int argc, char * argv[]) {NSString * appDelegateClassName;@autoreleasepool {appDelegateClassName = NSStringFromClass([AppDelegate class]);}return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

UIApplicationMain函数内部会启动主线程的RunLoop,使得iOS程序持续运行。

iOS系统中有两套API来使用RunLoop,NSRunLoop(CFRunLoopRef的封装)和CFRunLoopRef。Foundation框架是不开源的,可以通过开源的CoreFoundation来分析RunLoop内部实现。

点此下载CoreFoundation。

RunLoop对象底层就是一个CFRunLoopRef结构体,内部数据如下:

struct __CFRunLoop {pthread_t _pthread;               // 与RunLoop一一对应的线程CFMutableSetRef _commonModes;     // 存储着NSString(mode名称)的集合CFMutableSetRef _commonModeItems; // 存储着被标记为commonMode的Source0/Source1/Timer/ObserverCFRunLoopModeRef _currentMode;    // RunLoop当前的运行模式CFMutableSetRef _modes;           // 存储着RunLoop所有的 Mode(CFRunLoopModeRef)模式// 其他属性略
};
struct __CFRunLoopMode {CFStringRef _name;            // mode 类型,如:NSDefaultRunLoopModeCFMutableSetRef _sources0;    // 事件源 sources0CFMutableSetRef _sources1;    // 事件源 sources1CFMutableArrayRef _observers; // 观察者CFMutableArrayRef _timers;    // 定时器// 其他属性略
};

Source0被添加到RunLoop上时并不会主动唤醒线程,需要手动去唤醒。Source0负责对触摸事件的处理以及performSeletor:onThread:

Source1具备唤醒线程的能力,使用的是基于Port的线程间通信。Source1负责捕获系统事件,并将事件交由Source0处理。

struct __CFRunLoopSource {CFRuntimeBase _base;uint32_t _bits;pthread_mutex_t _lock;CFIndex _order;         /* immutable */CFMutableBagRef _runLoops;union {CFRunLoopSourceContext version0;      // 表示 sources0CFRunLoopSourceContext1 version1;     // 表示 sources1} _context;
};

__CFRunLoopTimer和NSTimer是免费桥接toll-free bridged的。
performSelector:WithObject:afterDelay:方法会创建timer并添加到RunLoop中。

struct __CFRunLoopTimer {CFRuntimeBase _base;uint16_t _bits;pthread_mutex_t _lock;CFRunLoopRef _runLoop;CFMutableSetRef _rlModes;CFAbsoluteTime _nextFireDate;CFTimeInterval _interval;       /* immutable */CFTimeInterval _tolerance;          /* mutable */uint64_t _fireTSR;          /* TSR units */CFIndex _order;         /* immutable */CFRunLoopTimerCallBack _callout;    /* immutable */CFRunLoopTimerContext _context; /* immutable, except invalidation */
};

RunLoopObserver用于监听RunLoop的六种状态。CFRunLoopObserver中的_activities用于保存RunLoop的活动状态,当状态发生改变时,通过回调函数_callout函数通知所有observer。

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry = (1UL << 0),          // 即将进入 RunLoopkCFRunLoopBeforeTimers = (1UL << 1),   // 即将处理 TimerskCFRunLoopBeforeSources = (1UL << 2),  // 即将处理 SourceskCFRunLoopBeforeWaiting = (1UL << 5),  // 即将进入休眠kCFRunLoopAfterWaiting = (1UL << 6),   // 刚从休眠中唤醒kCFRunLoopExit = (1UL << 7),           // 即将退出 RunLoopkCFRunLoopAllActivities = 0x0FFFFFFFU  // 以上所有状态
};
struct __CFRunLoopObserver {CFRuntimeBase _base;pthread_mutex_t _lock;CFRunLoopRef _runLoop;CFIndex _rlCount;CFOptionFlags _activities;      /* immutable */CFIndex _order;         /* immutable */CFRunLoopObserverCallBack _callout; /* immutable */CFRunLoopObserverContext _context;  /* immutable, except invalidation */
};

简单过一下RunLoop的源码。

void CFRunLoopRun(void) {   /* DOES CALLOUT */int32_t result;do {result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);CHECK_FOR_FORK();} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

简单来看RunLoop是个 do..while循环,下面来看看循环中具体干了哪些事情。

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */CHECK_FOR_FORK();if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;__CFRunLoopLock(rl);//根据modeName来查找本次运行的modeCFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);// 如果没找到mode 或者 mode里没有任何的事件,就此停止,不再循环if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {Boolean did = false;if (currentMode) __CFRunLoopModeUnlock(currentMode);__CFRunLoopUnlock(rl);return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;}CFRunLoopModeRef previousMode = rl->_currentMode;rl->_currentMode = currentMode;int32_t result = kCFRunLoopRunFinished;// 通知 observers 即将进入RunLoopif (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);// RunLoop具体要做的事情result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);// 通知 observers 即将退出RunLoopif (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);__CFRunLoopModeUnlock(currentMode);__CFRunLoopPopPerRunData(rl, previousPerRun);rl->_currentMode = previousMode;__CFRunLoopUnlock(rl);return result;
}

从上面可以看到RunLoop除了通知observers即将进入/退出外,其他具体要做的事情都写在了__CFRunLoopRun中。

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {uint64_t startTSR = mach_absolute_time();// 状态判断if (__CFRunLoopIsStopped(rl)) {__CFRunLoopUnsetStopped(rl);return kCFRunLoopRunStopped;} else if (rlm->_stopped) {rlm->_stopped = false;return kCFRunLoopRunStopped;}// 初始化timeout_timer代码 略int32_t retVal = 0;do {__CFPortSet waitSet = rlm->_portSet;__CFRunLoopUnsetIgnoreWakeUps(rl);// 通知 observers 即将处理Timerif (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);// 通知 observers 即将处理Sourcesif (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);// 处理主队列异步的block__CFRunLoopDoBlocks(rl, rlm);// 处理Source0Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);if (sourceHandledThisLoop) {// 处理block__CFRunLoopDoBlocks(rl, rlm);}Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);didDispatchPortLastTime = false;if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {// 判断有无Source1if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {// 有Source1就跳转到handle_msggoto handle_msg;}}// 通知 observers 即将进入休眠if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);__CFRunLoopSetSleeping(rl);__CFPortSetInsert(dispatchPort, waitSet);__CFRunLoopModeUnlock(rlm);__CFRunLoopUnlock(rl);CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();if (kCFUseCollectableAllocator) {memset(msg_buffer, 0, sizeof(msg_buffer));}msg = (mach_msg_header_t *)msg_buffer;// 休眠,等待消息来唤醒线程__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);__CFRunLoopLock(rl);__CFRunLoopModeLock(rlm);rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));__CFPortSetRemove(dispatchPort, waitSet);__CFRunLoopSetIgnoreWakeUps(rl);__CFRunLoopUnsetSleeping(rl);//通知 observers RunLoop刚从休眠中唤醒if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting))  __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);// 跳转标志 handle_msghandle_msg:;__CFRunLoopSetIgnoreWakeUps(rl);if (MACH_PORT_NULL == livePort) {CFRUNLOOP_WAKEUP_FOR_NOTHING();// handle nothing} else if (livePort == rl->_wakeUpPort) {CFRUNLOOP_WAKEUP_FOR_WAKEUP();}#if USE_MK_TIMER_TOO// 被Timer唤醒else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {CFRUNLOOP_WAKEUP_FOR_TIMER();//处理Timerif (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {// Re-arm the next timer__CFArmNextTimerInMode(rlm, rl);}}
#endif// 被GCD唤醒else if (livePort == dispatchPort) {CFRUNLOOP_WAKEUP_FOR_DISPATCH();__CFRunLoopModeUnlock(rlm);__CFRunLoopUnlock(rl);_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);// 处理GCD__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);__CFRunLoopLock(rl);__CFRunLoopModeLock(rlm);sourceHandledThisLoop = true;didDispatchPortLastTime = true;} else {// 被Source1唤醒CFRUNLOOP_WAKEUP_FOR_SOURCE();voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);_CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);} // 处理Block    __CFRunLoopDoBlocks(rl, rlm);// 处理返回值if (sourceHandledThisLoop && stopAfterHandle) {// 进入loop时参数标记为处理完事件就返回retVal = kCFRunLoopRunHandledSource;} else if (timeout_context->termTSR < mach_absolute_time()) {// 超出传入参数标记的超时时间retVal = kCFRunLoopRunTimedOut;} else if (__CFRunLoopIsStopped(rl)) {// 被外部调用者强行停止__CFRunLoopUnsetStopped(rl);retVal = kCFRunLoopRunStopped;} else if (rlm->_stopped) {// 自动停止rlm->_stopped = false;retVal = kCFRunLoopRunStopped;} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {// mode为空,没有source0、source1、timer、observersretVal = kCFRunLoopRunFinished;}} while (0 == retVal);if (timeout_timer) {dispatch_source_cancel(timeout_timer);dispatch_release(timeout_timer);} else {free(timeout_context);}return retVal;
}

整体流程如下图所示。

事件循环机制

根据这张图可以看出:RunLoop在BeforeSources和AfterWaiting后会进行任务的处理。可以在此时阻塞监控线程并设置超时时间,若超时后RunLoop的状态仍为RunLoop在BeforeSources或AfterWaiting,表明此时RunLoop仍然在处理任务,主线程发生了卡顿。

- (void)beginMonitor {self.dispatchSemaphore = dispatch_semaphore_create(0);// 第一个监控,监控是否处于 运行状态CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};self.runLoopBeginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,LONG_MIN,&myRunLoopBeginCallback,&context);//  第二个监控,监控是否处于 睡眠状态self.runLoopEndObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,LONG_MAX,&myRunLoopEndCallback,&context);CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopBeginObserver, kCFRunLoopCommonModes);CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopEndObserver, kCFRunLoopCommonModes);// 创建子线程监控dispatch_async(dispatch_get_global_queue(0, 0), ^{//子线程开启一个持续的loop用来进行监控while (YES) {long semaphoreWait = dispatch_semaphore_wait(self.dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 17 * NSEC_PER_MSEC));if (semaphoreWait != 0) {if (!self.runLoopBeginObserver || !self.runLoopEndObserver) {self.timeoutCount = 0;self.dispatchSemaphore = 0;self.runLoopBeginActivity = 0;self.runLoopEndActivity = 0;return;}// 两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿if ((self.runLoopBeginActivity == kCFRunLoopBeforeSources || self.runLoopBeginActivity == kCFRunLoopAfterWaiting) ||(self.runLoopEndActivity == kCFRunLoopBeforeSources || self.runLoopEndActivity == kCFRunLoopAfterWaiting)) {// 出现三次出结果if (++self.timeoutCount < 2) {continue;}NSLog(@"调试:监测到卡顿");} // end activity}// end semaphore waitself.timeoutCount = 0;}// end while});
}// 第一个监控,监控是否处于 运行状态
void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {RunLoopMonitor2* lagMonitor = (__bridge RunLoopMonitor2 *)info;lagMonitor.runLoopBeginActivity = activity;dispatch_semaphore_t semaphore = lagMonitor.dispatchSemaphore;dispatch_semaphore_signal(semaphore);
}//  第二个监控,监控是否处于 睡眠状态
void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {RunLoopMonitor2* lagMonitor = (__bridge RunLoopMonitor2 *)info;lagMonitor.runLoopEndActivity = activity;dispatch_semaphore_t semaphore = lagMonitor.dispatchSemaphore;dispatch_semaphore_signal(semaphore);
}

方案三:Ping主线程

Ping主线程的核心思想是向主线程发送一个信号,一定时间内收到了主线程的回复,即表示当前主线程流畅运行。没有收到主线程的回复,即表示当前主线程在做耗时运算,发生了卡顿。

目前昆虫线上使用的就是这套方案。

self.semaphore = dispatch_semaphore_create(0);
- (void)main {//判断是否需要上报__weak typeof(self) weakSelf = self;void (^ verifyReport)(void) = ^() {__strong typeof(weakSelf) strongSelf = weakSelf;if (strongSelf.reportInfo.length > 0) {if (strongSelf.handler) {double responseTimeValue = floor([[NSDate date] timeIntervalSince1970] * 1000);double duration = responseTimeValue - strongSelf.startTimeValue;if (DEBUG) {NSLog(@"卡了%f,堆栈为--%@", duration, strongSelf.reportInfo);}strongSelf.handler(@{@"title": [InsectUtil dateFormatNow].length > 0 ? [InsectUtil dateFormatNow] : @"",@"duration": [NSString stringWithFormat:@"%.2f",duration],@"content": strongSelf.reportInfo});}strongSelf.reportInfo = @"";}};while (!self.cancelled) {if (_isApplicationInActive) {self.mainThreadBlock = YES;self.reportInfo = @"";self.startTimeValue = floor([[NSDate date] timeIntervalSince1970] * 1000);dispatch_async(dispatch_get_main_queue(), ^{self.mainThreadBlock = NO;dispatch_semaphore_signal(self.semaphore);});[NSThread sleepForTimeInterval:(self.threshold/1000)];if (self.isMainThreadBlock) {self.reportInfo = [InsectBacktraceLogger insect_backtraceOfMainThread];}dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);//卡顿超时情况;verifyReport();} else {[NSThread sleepForTimeInterval:(self.threshold/1000)];}}
}

总结

方案 优点 缺点 实现复杂性
FPS 直观 无法准确定位卡顿堆栈 简单
RunLoop Observer 能定位卡顿堆栈 不能记录卡顿时间,定义卡顿的阈值不好控制 复杂
Ping Main Thread 能定位卡顿堆栈,能记录卡顿时间 一直ping主线程,费电 中等

iOS中的3种卡顿检测相关推荐

  1. ios实时卡顿检测和优化方案

    在移动设备上开发软件,性能一直是我们最为关心的话题之一,我们作为程序员除了需要努力提高代码质量之外,及时发现和监控软件中那些造成性能低下的"罪魁祸首"也是我们神圣的职责.友盟+U- ...

  2. iOS性能优化-UI卡顿检测

    前言 在实现需求的同时,能写出既优雅性能又高效的代码是每个开发者都在追求的目标,但是在实际开发中,随着每个版本需求的迭代,功能变得越来越复杂,加上开发者的意识不够或者一时疏忽,日渐复杂的工程很容易产生 ...

  3. iOS界面优化 ——卡顿检测和优化方案

    iOS界面优化 --卡顿检测和优化方案 1. 卡顿原理 2. 卡顿检测 3. 界面优化 1. 卡顿原理 卡顿,也就是掉帧.当UIView被绘制时,cpu执行drawrect,通过context将数据写 ...

  4. 关于ios 卡顿检测分析

    很多iOS 开发,开发过程中都会面临到解决App卡顿问题,从而也衍生出很多的方法去解决卡顿,这篇文章来描述下iOS卡顿产生的原因,以及如何进行iOS卡顿检测分析. 了解iOS卡顿原理 像素是如何显示到 ...

  5. iOS开发-App应用崩溃卡顿分析

    文章目录 App崩溃问题 可捕获的崩溃信息收集 PLCrashReporter实现 系统接口 不可捕获的崩溃 iOS后台模式 Runloop 卡顿 Watch Dog 内存达到单个App上限被杀死 J ...

  6. Android卡顿检测及优化

    前言 之前在项目中做过一些Android卡顿以及性能优化的工作,但是一直没时间总结,趁着这段时间把这部分总结一下. 卡顿 在应用开发中如果留意到log的话有时候可能会发下下面的log信息: I/Cho ...

  7. AndroidAPP卡顿检测必备工具友盟+U-Apm

    移动互联网行业的暴热,促进了各大公司在做APP,随着大家越来越追求精神享受,卡顿.不流畅的app被人们不断的放弃.如何做到上线后不被吐槽,不再背锅上线,成为大家越来越急切的需求. 但是,要想发现这些问 ...

  8. 深入解析:Android卡顿检测及优化项目实战经验总结,任君白嫖

    前言 之前在项目中做过一些Android卡顿以及性能优化的工作,但是一直没时间总结,趁着这段时间把这部分总结一下. GitHub系统教程学习地址:https://github.com/Timdk857 ...

  9. bootsrtap h5 移动版页面 在苹果手机ios滑动上下拉动滚动卡顿问题解决方法

    bootsrtap h5 移动版页面 在苹果手机ios滑动上下拉动滚动卡顿问题解决方法 bootsrtap框架做的h5页面,在android手机下没有卡顿问题,在苹果手机就一直存在这问题,开始毫无头绪 ...

最新文章

  1. Magent搭建Memcached集群
  2. 干货丨区块链如何改变人工智能(AI)?
  3. java中全大写字符_Java 大写字符串中的字符
  4. python爬虫,爬取猫眼电影1(正则表达式)
  5. 机器学习:防止模型过拟合的方法
  6. Microsoft Build 2021大会开始后,Develop Blog一系列更新
  7. 使用 Vue.js 和 Chart.js 制作绚丽多彩的图表
  8. 2021年NBA季后赛第三轮晋级预测
  9. 浙大计算机学院绩点,浙江大学学分制管理暂行规定-浙江大学本科生院.doc
  10. linux atom编码设置,Ubuntu中Atom编辑器显示中文乱码的处理方法
  11. 微软开放技术热烈祝贺开源社成立!
  12. 《剑指offer》-中序遍历下一个节点
  13. lua luarocks luacheck linux安装
  14. 【《离散数学》试卷(A)】离散数学期末考试题——分享篇——题目完善、答案详尽【推荐学习】
  15. 西门子PLC怎么调试?
  16. 天大18年c语言离线作业,2018春 Python语言程序设计(天津大学仁爱学院)-中国大学mooc-题库零氪...
  17. Java聊天室系统(三):图形界面窗口展示
  18. b站视频下载日记新篇2021年4月3日
  19. Linux(四)——CROND和磁盘分区与挂载
  20. IE6下图片的浏览剪裁与上传

热门文章

  1. IOS – OpenGL ES 图像鱼眼扩散效果 GPUImageBulgeDistortionFilter
  2. Simulink如何添加模块到Library Browser
  3. JS中如何让某个动作延迟几秒执行(☆)
  4. 如何给无光驱无软驱无USB启动的老笔记本装系统
  5. 七月行情有盼头——技术派=基础,资金流派和基本面派是工具,思维流是集大成者_96
  6. 文件服务器角色提供多种服务 其中,Win2008实战:配置双节点打印服务器故障转移群集...
  7. 惠普暗影精灵Plus 3代 (OMEN 17-an014TX)参数
  8. 词云 文字云 标签云 教程19年最新版
  9. 电子招投标系统源码之了解电子招标投标全流程
  10. 计算机知识及保密培训目的,二勘院举办保密知识和计算机网络安全专题培训会...