注意:框架已经迭代到2.0版本,我重新架构了整个框架,API 也得到了更好的设计,我为 2.0 版本的实现写了一篇文章 [iOS]如何重新架构 JPVideoPlayer ?。此文中的实现思路仍然是一致的,但是实现细节已经不能沿用了,具体细节请前往我的 GitHub 查看。

Tips:这次的内容分为两篇文章讲述

01、[iOS]仿微博视频边下边播之封装播放器 讲述如何封装一个实现了边下边播并且缓存的视频播放器。

02、[iOS]仿微博视频边下边播之滑动TableView自动播放 讲述如何实现在tableView中滑动播放视频,并且是流畅,不阻塞线程,没有任何卡顿的实现滑动播放视频。同时也将讲述当tableView滚动时,以什么样的策略,来确定究竟哪一个cell应该播放视频。

上篇文章讲述了封装一个边下边播,并且带有缓存功能的播放器。如果你还没有看,请点击跳转[iOS]仿微博视频边下边播之封装播放器 。接下来,讲述如何将这个播放器应用到 tableView 里。并且达到如下效果。

2.x 版本效果如下:

01、dispatch_semaphore信号量?

dispatch_semaphore 信号量基于计数器的一种多线程同步机制。在多个线程访问共有资源时候,会因为多线程的特性而引发数据出错的问题。

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);// “创建方法里会传入一个long型的参数,这个东西你可以想象是一个库存”
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);NSMutableArray *array = [NSMutableArray array];for (int index = 0; index < 10000; index++) {dispatch_async(queue, ^(){// “每运行一次,会先清一个库存,如果库存为0,那么根据传入的等待时间,决定等待增加库存的时间//如果设置为DISPATCH_TIME_FOREVER,那么意思就是永久等待增加库存,否则就永远不往下面走”dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);NSLog(@"addd :%d", index);[array addObject:[NSNumber numberWithInt:index]];// 每运行一次,增加一个库存dispatch_semaphore_signal(semaphore); });
}
复制代码

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 如果 semaphore 计数大于等于 1.计数-1,返回,程序继续运行。 如果计数为 0,则等待。 这里设置的等待时间是一直等待。

dispatch_semaphore_signal(semaphore); 计数+1. 在这两句代码中间的执行代码,每次只会允许一个线程进入,这样就有效的保证了在多线程环境下,只能有一个线程进入。

  • AVPlayer 底层是有信号量等待的特性的。具体表现在,“AVPlayer的replaceCurrentItemWithPlayerItem(用来切换视频的)方法在切换视频时底层会调用信号量等待,然后导致当前线程卡顿,如果在UITableViewCell中切换视频播放使用这个方法,会导致当前线程冻结几秒钟。” 这里说的线程是UI线程,即主线程,主线程冻结的结果就是主线程阻塞,带来卡顿和不流畅。

  • 你可能会想,那就不要在主线程切换视频,不就不卡顿主线程了吗?如果你这么做,那你就不能保证视频播放是及时响应的。同时,因为子线程你不知道什么时候调用,你也不能保证你能及时关闭不需要播放的视频。也就是说,如果基于以上思路,当你滑动 tableView 时,可能会出现多个 cell 同时播放视频,而且会出现你要播的播不了,你想掐死的掐不死。

02、切换视频解决方案?

tableView 里播放视频,当用户滑动时,肯定是频繁切换视频的。上面讲了使用 AVPlayer 自带的 replaceCurrentItemWithPlayerItem 来切换视频带来的问题,我们得出的结论是:

  • 不能使用 replaceCurrentItemWithPlayerItem 方法切换视频。
  • 不能在子线程切换视频。

解决方案一

当出现这种问题的时候,我只能跑到官方文档去找答案了。

@interface AVQueuePlayer : AVPlayer
复制代码

我在官方文档里找到 AVQueuePlayer,他是 AVPlayer 的一个子类,他会自己维护一个播放队列。并且提供方法,可以插入播放条目,也可以移除播放条目,然后切换视频。

// 创建AVQueuePlayer
NSArray *items = <#An array of player items#>;
AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] initWithItems:items];// 插入item
- (void)insertItem:(AVPlayerItem *)item afterItem:(nullable AVPlayerItem *)afterItem;// 移除item
- (void)removeItem:(AVPlayerItem *)item;// 切换视频
- (void)advanceToNextItem;
复制代码

这个类还是很好使的,可以在不卡顿主线程的情况下流畅切换视频。但是他有他的坑,是我多次试验以后发现的,就是重播视频的时候,如果你放在那里不动,他大概会重复播放十次左右,然后播放器就莫名其妙的死掉了,这个时候你没有办法重新唤醒他。至于什么原因导致的,我暂时还没有找到。如果你知道,请你务必在下面留言,让更多人看到。

解决方案二

上面的那个方案被我否了,接下来,我采取的是尝试每次切换视频的时候都重新创建播放器,重新建立网络请求。总之,就是所有的配置都重新创建。刚开始的时候,我担心这样会造成处理器负担,但是实际使用起来,发现并没有任何性能问题。但是前提是,在重新创建之前,把所有的请求释放掉,同时把之前的播放器也释放,还有预览图层也释放。

03、重播?

先来看一下 AVFoundation 下用来表示时间的结构体 CMTimeAVFoundation 下的时间刻度是以最精准的分数形式来表示的。他有两个重要的值,value 表示分子,timescale 表示分母。

typedef struct {CMTimeValue value;  // 分子CMTimeScale timescale;  // 分母CMTimeFlags flags;  CMTimeEpoch epoch;
} CMTime;
复制代码

比如说我们要表示视频的起点,就是 0 秒,那就可以写成 CMTimeMake(0, 1)

我们的播放器是支持自动重播的,所以我们要在系统的通知中心监听播放器播放完成的通知,在接收到通知后,进行对应的处理。

// 监听播放结束通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidPlayToEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];- (void)playerItemDidPlayToEnd:(NSNotification *)notification{// 重复播放, 从起点开始重播, 没有内存暴涨__weak typeof(self) weak_self = self;[self.player seekToTime:CMTimeMake(0, 1) completionHandler:^(BOOL finished) {__strong typeof(weak_self) strong_self = weak_self;if (!strong_self) return;[strong_self.player play];}];
}
复制代码

04、滑动 tableView 自动播放?

首先是一启动,应该自动去可见 cell 中查找第一个需要播放视频的 cell,如果找到就开始播放。

-(void)playVideoInVisiableCells{NSArray *visiableCells = [self.tableView visibleCells];// 在可见cell中找到第一个有视频的cellJPVideoPlayerCell *videoCell = nil;for (JPVideoPlayerCell *cell in visiableCells) {if (cell.videoPath.length > 0) {videoCell = cell;break;}}// 如果找到了, 就开始播放视频if (videoCell) {self.playingCell = videoCell;self.currentVideoPath = videoCell.videoPath;JPVideoPlayer *player = [JPVideoPlayer sharedInstance];[player playWithUrl:[NSURL URLWithString:videoCell.videoPath] showView:videoCell.containerView];player.mute = YES;}
}
复制代码

接下来就是滚动 tableView 的时候,播放视频。在做之前,首先,我们要先制定一个规则来确定,滚动的时候究竟哪一个 cell 应该播放视频。我画了一张图,一起来看一下。

我的规则是:当 tableView 滑动的时候,我会播放可见 cell 中,最靠近屏幕中心的那个 cell 的视频。如果最靠近屏幕中心的那个 cell 没有视频,就会按照这个规则去其他可见 cell 中找,如果都没有找到,就不播放视频。规则有了,接下来就是去实现。其实,实现的时候,我们应该换一个思路,就是,只有那个 cell 需要播放视频,才会参与筛选是否是最靠近屏幕中心的 cell。这样就避免了递归查找。

-(void)handleScroll{// 找到下一个要播放的cell(最在屏幕中心的)JPVideoPlayerCell *finnalCell = nil;NSArray *visiableCells = [self.tableView visibleCells];NSMutableArray *indexPaths = [NSMutableArray array];CGFloat gap = MAXFLOAT;for (JPVideoPlayerCell *cell in visiableCells) {[indexPaths addObject:cell.indexPath];if (cell.videoPath.length > 0) { // 如果这个cell有视频CGPoint coorCentre = [cell.superview convertPoint:cell.center toView:nil];CGFloat delta = fabs(coorCentre.y-[UIScreen mainScreen].bounds.size.height*0.5);if (delta < gap) {gap = delta;finnalCell = cell;}}}// 注意, 如果正在播放的cell和finnalCell是同一个cell, 不应该在播放if (finnalCell != nil && self.playingCell != finnalCell)  {[[JPVideoPlayer sharedInstance]stop];[[JPVideoPlayer sharedInstance]playWithUrl:[NSURL URLWithString:finnalCell.videoPath] showView:finnalCell.containerView];self.playingCell = finnalCell;self.currentVideoPath = finnalCell.videoPath;[JPVideoPlayer sharedInstance].mute = YES;return;}// 再看正在播放视频的那个cell移出视野, 则停止播放BOOL isPlayingCellVisiable = YES;if (![indexPaths containsObject:self.playingCell.indexPath]) {isPlayingCellVisiable = NO;}if (!isPlayingCellVisiable && self.playingCell) {[self stopPlay];}
}
复制代码

这里没有难点,唯一可以讲一下的就是坐标之间的转换。我们拿到的 cell 的中心点的坐标是 tableView 的坐标,但是我们计算各个中心点离屏幕中心点之间的距离,用的是屏幕 Window 的坐标,所以要先将这个中心点的坐标转换为屏幕的坐标,再进行计算。但是,你也可以不转换坐标,因为他们是同一个坐标系的(tableView 的坐标系)。可是,我个人的编程习惯是先转换,再计算。因为我觉得会比较清晰一点。尤其是当我们做复杂的过渡动画的时候,有这个意识,你会发现条理会很清晰。

05、什么时候播?

你肯定告诉我,滑动的时候播。这个回答是正确的,但是也是不正确的,因为我们尝试用编程的思想来思考这个问题。tableView 的滚动过程分为两种情况:

  • 将要开始拖动 --> 开始拖动 --> 滚动 --> 松手 --> 静止 --> 结束
  • 将要开始拖动 --> 开始拖动 --> 滚动 --> 松手 --> 开始减速 --> 减速完成 --> 静止 --> 结束

首先要肯定的是,不能在滚动的时候调用视频播放的逻辑。这一点应该没有异议。原因是,第一,这个方法会来很多很多次,而且从实现上来说,不可能一调用滚动的代理就实现播放。第二从用户角度来说,在滑动的时候,他自己也没有决定要看哪一个 cell。所以在滚动时,我们不做反应。

其次是开始拖动时,也不要作反应,因为,这个时候作反应没有意义。松手的时候,因为有松手时静止和松手时滚动两种情况,所以我不做处理。

逐渐排除下来,最后,适合调用播放逻辑的,只剩下“静止”这个动作了。我们来看一下静止对应的代理方法:

// 松手时已经静止,只会调用scrollViewDidEndDragging
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{if (decelerate == NO) { // scrollView已经完全静止[self handleScroll];}
}// 松手时还在运动, 先调用scrollViewDidEndDragging,在调用scrollViewDidEndDecelerating
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{// scrollView已经完全静止[self handleScroll];
}
复制代码

做到这里,我们已经能够实现 tableView 在滑动的时候,流畅的播放视频了。

06、现有的问题(bug)?

很多有经验的老司机,应该已经看出来问题了。问题就是,我们现有的规则是:

  • 在可见 cell 中,播放最靠近屏幕中心的 cell 的视频。
  • tableView 滑动静止的时候调用视频播放逻辑。

还记得这张图吗?仔细想一下,按照我们上面的规则,cell 01 是永远不可能在静止的时候成为离屏幕中心最近那个 cell 的(也有例外,那就是其他三个 cell 都没有要播放视频。但是,我们的程序不能有设计缺陷)。同样的,底部也有一个这样的 cell,不能滚动到屏幕中心。我把这样的 cell 叫做“滑动不可及 cell”,下文都会以这个词来称呼归类这一类 cell

所以,我在上面的两点规则上加了一条:

  • 如果“滑动不可及 cell”完整出现在视野,那么优先播放“滑动不可及 cell ”的视频,注意,这里说得是“滑动不可及 cell 完整出现在视野”,注意点是“完整出现”。

有了规则就依照这个规则来解决。首先,我们面对的第一个问题是,我怎么知道我的列表里有几个这样的 cell?不知道,没关系,我们可以实际测量。我以 iPhone 6s 为样机,进行了测量,我这里的测量前提是,我们的 cell 上的视频尺寸和 cell 的尺寸是一致的。我的测量结果如下:

每屏cell个数        4  3  2
滑动不可及cell个数   1  1  0
复制代码

我这里需要说明的是,我不可能知道你项目的具体需求,但是,如果你的实际需求和我文章中的不一样,那你根据我现有思路进行更改就可以了。

接下来继续,根据以上的分析,我首先把测量结果保存到一个字典中,以每屏可见 cell 的最大个数为 key, 对应的不能播放视频的 cellvalue。以后,你只要根据行高和屏幕高度这两个值算出每屏有多少 cell,就能取出有多少个 cell 是滑动不可及 cell

-(NSDictionary *)dictOfVisiableAndNotPlayCells{// 以每屏可见cell的最大个数为key, 对应的不能播放视频的cell为value// 只有每屏可见cell数在3以上时,才会出现滑动时有cell的视频永远播放不到// 以下值都是实际测量得到if (!_dictOfVisiableAndNotPlayCells) {_dictOfVisiableAndNotPlayCells = @{@"4" : @"1",@"3" : @"1",};}return _dictOfVisiableAndNotPlayCells;
}
复制代码

其次,我把 cell 归为三个类型,我用一个枚举来表示:

// 播放滑动不可及cell的类型
typedef NS_ENUM(NSUInteger, PlayUnreachCellStyle) {PlayUnreachCellStyleUp = 1, // 顶部不可及PlayUnreachCellStyleDown = 2, // 底部不可及PlayUnreachCellStyleNone = 3 // 播放滑动可及cell
};
复制代码

当每个 cell 来到 cellForRowAtIndexPath 方法的时候,我就根据 cellrow 数和最大不可及 cell 数,给每个 cell 打一个标签。

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{JPVideoPlayerCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseID forIndexPath:indexPath];if (self.maxNumCannotPlayVideoCells > 0) {if (indexPath.row <= self.maxNumCannotPlayVideoCells-1) {cell.cellStyle = PlayUnreachCellStyleUp;}else if (indexPath.row >= self.listArr.count-self.maxNumCannotPlayVideoCells){cell.cellStyle = PlayUnreachCellStyleDown;}else{cell.cellStyle = PlayUnreachCellStyleNone;}}return cell;
}
复制代码

在播放逻辑里加入这些代码,用来维护我上面加的那条规则:

// 优先查找滑动不可及cell
if (cell.cellStyle != PlayUnreachCellStyleNone) {// 并且不可及cell要全部露出if (cell.cellStyle == PlayUnreachCellStyleUp) {CGPoint cellLeftUpPoint = cell.frame.origin;// 不要在边界上cellLeftUpPoint.y += 1;CGPoint coorPoint = [cell.superview convertPoint:cellLeftUpPoint toView:nil];CGRect windowRect = self.view.window.bounds;BOOL isContain = CGRectContainsPoint(windowRect, coorPoint);if (isContain) {finnalCell = cell;}}else if (cell.cellStyle == PlayUnreachCellStyleDown){CGPoint cellLeftUpPoint = cell.frame.origin;cellLeftUpPoint.y += cell.bounds.size.height;// 不要在边界上cellLeftUpPoint.y -= 1;CGPoint coorPoint = [cell.superview convertPoint:cellLeftUpPoint toView:nil];CGRect windowRect = self.view.window.bounds;BOOL isContain = CGRectContainsPoint(windowRect, coorPoint);if (isContain) {finnalCell = cell;}}
}
复制代码

到这里为止,我们的播放逻辑基本上没有问题了。

07、真的没有问题了?

其实还是有问题的,就问题就是,当你快速滑动的时候,会出现 cell 循环利用的图像错误。可以想象,在快速滑动的时候,我们没有做任何处理,上个 cell 的视频图像在 cell 移出视野时没有清除,那当这个 cell 被循环利用的时候,就会把上个 cell 的图像带到下一个 cell,这样就会有显示问题。

其实解决方案很简单,就是当 cell 移出视野,把对应的图层去掉,播放器释放。

// 快速滑动循环利用问题
-(void)handleQuickScroll{if (!self.playingCell) return;NSArray *visiableCells = [self.tableView visibleCells];NSMutableArray *indexPaths = [NSMutableArray array];for (JPVideoPlayerCell *cell in visiableCells) {[indexPaths addObject:cell.indexPath];}BOOL isPlayingCellVisiable = YES;if (![indexPaths containsObject:self.playingCell.indexPath]) {isPlayingCellVisiable = NO;}// 当前播放视频的cell移出视线, 或者cell被快速的循环利用了, 都要移除播放器if (!isPlayingCellVisiable || ![self.playingCell.videoPath isEqualToString:self.currentVideoPath]) {[self stopPlay];}
}
复制代码

好的,真的没有问题了?,谢谢你的阅读,Github 源码点击这里 JPVideoPlayer。

08、更新

  • 2016.10.09 : 处理在切换视频的短暂时间内, 当前播放视频的 cell 吸收了滑动事件, 如果滑动当前播放视频的 cell, 会导致 tableView 无法接收到滑动事件, 造成 tableView 假死。 感谢提供 bug 的朋友@大墙66370 具体见我的 Github JPVideoPlayer。

  • 2016.11.04: 简书朋友@菜先生提交了一个关于单例里重复添加监听的问题, 具体是播放工具单例在每次调用 init 方法时总会重复添加监听播放完成等的通知, 会导致通知方法重复调用, 这个问题可能带来卡顿. 最新的版本已经修复了这个问题, 具体见我的 Github JPVideoPlayer。

  • 2016.11.08 感谢简书作者 @老孟(http://www.jianshu.com/users/9f6960a40be6/timeline), 他帮我测试了多数的真机设备, 包括iPhone 5s 国行 系统9.3.5  iPhone 6plus 港行 系统10.0.2 iPhone 6s 国行 系统9.3.2  iPhone 6s plus 港行 系统10.0.0 iPhone 7plus 国行 系统10.1.1, 我之前由于手上设备有限, 只测试了 iPhone 6s 和 iPhone 6s plus, 但是 @老孟发 现在较旧设备上有卡顿的现象, 具体表现为播放本地已经缓存的视频的时候会出现2-3秒的假死, 其实是阻塞了主线程. 现在经过修改过后的版本修复了这个问题, 并且以上设备都测试通过, 没有出现卡顿情况.

  • 2016.11.10 关闭播放器以后, 视频还在后台播放的bug已经修复提交, 详见 JPVideoPlayer。感谢简书朋友@花无缺_提交的 bug.

  • 2016.11.18 1.修复了可能出现, 当播放一些特别小的视频文件时, 会出现播放不了的情况. 2.添加了缓存管理的工具类, 你可以调用 -getSize: 方法异步获取缓存大小. 也可以使用 -clearVideoCacheForUrl: 或者 -clearAllVideoCache 方法清除缓存.

  • 2017.05.02 更新. 有些朋友反应有些视频无法边下边播, 具体解决思路请参考 这篇博文 。

注意:框架已经迭代到2.0版本,我重新架构了整个框架,API 也得到了更好的设计,我为 2.0 版本的实现写了一篇文章 [iOS]如何重新架构 JPVideoPlayer ?。此文中的实现思路仍然是一致的,但是实现细节已经不能沿用了,具体细节请前往我的 GitHub 查看。

NewPan 的文章集合

下面这个链接是我所有文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。

NewPan 的文章集合索引

如果你有问题,除了在文章最后留言,还可以在微博 @盼盼_HKbuy 上给我留言,以及访问我的 Github。

[iOS]仿微博视频边下边播之滑动 TableView 自动播放相关推荐

  1. [iOS]仿微博视频边下边播之滑动TableView自动播放

    Tips:这次的内容分为两篇文章讲述 01.[iOS]仿微博视频边下边播之封装播放器 讲述如何封装一个实现了边下边播并且缓存的视频播放器. 02.[iOS]仿微博视频边下边播之滑动TableView自 ...

  2. iOS开发之仿微博视频边下边播之自定义AVPlayer播放器, 边下边播解剖。视频处理流程,建立连接-请求数据-统筹数据-解码数据-视频呈现

    Tips:这次的内容分为两篇文章讲述 01.[iOS]仿微博视频边下边播之封装播放器 讲述如何封装一个实现了边下边播并且缓存的视频播放器. 02.[iOS]仿微博视频边下边播之滑动TableView自 ...

  3. 仿微博视频边下边播之封装播放器

    来源:NewPan(@盼盼_HKbuy) 链接:http://www.jianshu.com/p/0d4588a7540f Tips:这次的内容分为两篇文章讲述 01.[iOS]仿微博视频边下边播之封 ...

  4. 0930 视频边下边播/蓝牙库/阿里博客/afnetworking详细/小程序工具

    iOS视频边下边播–缓存播放数据流 简书: http://www.jianshu.com/p/990ee3db0563 简单易用的蓝牙库,基于CoreBluetooth的封装,并兼容ios和mac o ...

  5. html视频播放完自动跳转,在html5视频中跳转到currentTime后自动播放

    我试图创建一个视频,该视频在点击按钮后跳转到视频后从视频中的某个点自动播放.我拥有它,以便视频跳到现场,但我无法弄清楚如何从那里自动播放.我是新来的JavaScript,我想可能有一个简单的解决方案, ...

  6. php音视频边下边播,封装bilibili播放器,自定义边下边播和缓存功能

    image 本项目使用播放器是ijkplay, 并且进行封装和修改主要功能: 1.重新编辑ijkplay的so库, 使其更精简和支持https协议 2.自定义MediaDataSource, 使用ok ...

  7. Android 视频边下边播,MP4头信息在后调整头信息

    mp4视频有两种格式,一种视频头信息在前,这种直接可以先缓存头信息,然后直接边下边播,还有一种是头信息在最后,这种情况下则需要处理mp4的头信息,并调整mp4的格式. mp4文件的格式如下图 图1 从 ...

  8. 实现视频边下边播(视频MOOV信息前置)

    (由于时间久,忘记原链接,仅把自己现在实现方式写出以供参考:) public class QtFastStart {public static boolean sDEBUG = false; priv ...

  9. RecyclerView实现仿微博视频自动播放

    近期遇到一个需求是做类似微博中视频在WiFi状态下自动播放,故写了一个简单的demo.重点代码都会贴出来,一些细节,项目中加了demo上并没有添加(例如:判断是否为WiFi状态)在这里说明一下. 需求 ...

最新文章

  1. 新春祝福必杀计之发送短信攻略
  2. 利用Python制作简单的小程序:IP查看器
  3. 赠书 | 详解 4 种爬虫技术
  4. SSIM与PSNR的计算方式
  5. 爱吃苹果的与喜欢篮球的没必要非得达成一致~
  6. 【电子信息复试】考研复试常考问题——数据库
  7. Linux进程和计划任务管理(详细图例)
  8. Buffers, windows, and tabs
  9. oracle数据库恢复参数文件位置,Oracle数据库的参数文件备份与恢复
  10. 新iPhone XR全配色曝光:苹果你清醒一点 还在搞配色?
  11. 程序员为3万福利放弃30万年薪:贪小便宜的人,都把自己坑惨了
  12. Linux/Windows下查看同一网段下的所有活动IP
  13. 数据库系统教程第三版施伯乐
  14. 微信app支付 服务器接口,iOS微信支付——APP调用微信支付接口
  15. C语言 pow函数 undefined reference to `pow‘ 已解决
  16. [VS2010]逸雨清风 永久稳定音乐外链生成软件V0.1
  17. 矩池云 | Tony老师解读Kaggle Twitter情感分析案例
  18. FTP与TFTP介绍
  19. 三极管基础分类, 参数选择及常见型号对比
  20. 【华东师范大学自然科学版】一种面向双中台双链架构的内生性 数据安全交互协议研究——CSCD

热门文章

  1. Linkedin领英如何添加或更改账号的邮箱地址
  2. 项目名前有个红色感叹号的解决办法
  3. (每日更新)《虚拟现实VR资讯》(Yanlz+Unity+SteamVR+云技术+5G+AI=VR云游戏=云渲染+人机交互+立钻哥哥+==)
  4. 【渝粤教育】广东开放大学 公共经济学 形成性考核 (33)
  5. html设置ie9兼容性视图,ie9浏览器设置兼容性视图在哪里设置
  6. Webtoos 仿Q+云桌面框架
  7. CEF 最新版本自己编译加上支持播放MP4视频
  8. 中国电子束抗蚀剂市场深度研究分析报告
  9. Android微信页面缓存清理,安卓微信浏览器缓存如何清理
  10. 项目产品化的版本管控实践方案