ios 音视频实现边播边缓存的思路和解决方案 (转)
本片为转载内容,主要是以后自己看起来方便一些
原文地址:iOS音视频实现边下载边播放
其实音视频本地缓存的思想都差不多,都需要一个中间对象来连接播放器和服务器。
近段时间制作视频播放社区的功能,期间查找了不少资料,做过很多尝试,现在来整理一下其中遇到的一些坑.由于考虑到AVPlayer对视频有更高自由度的控制,而且能够使用它自定义视频播放界面,iOS中所使用的视频播放控件为AVPlayer,而抛弃了高层次的MediaPlayer框架,现在想想挺庆幸当初使用了AVPlayer。
AVPlayer的基本知识
AVPlayer本身并不能显示视频,而且它也不像MPMoviePlayerController有一个view属性。如果AVPlayer要显示必须创建一个播放器层AVPlayerLayer用于展示,播放器层继承于CALayer,有了AVPlayerLayer之添加到控制器视图的layer中即可。要使用AVPlayer首先了解一下几个常用的类:
AVAsset:主要用于获取多媒体信息,是一个抽象类,不能直接使用。
AVURLAsset:AVAsset的子类,可以根据一个URL路径创建一个包含媒体信息的AVURLAsset对象。
AVPlayerItem:一个媒体资源管理对象,管理者视频的一些基本信息和状态,一个AVPlayerItem对应着一个视频资源。
iOS视频实现边下载边播放的几种实现
1.本地实现http server
在iOS本地开启Local Server服务,然后使用播放控件请求本地Local Server服务,本地的服务再不断请求视频地址获取视频流,本地服务请求的过程中把视频缓存到本地,这种方法在网上有很多例子,有兴趣了解的人可自己下载例子查看。
2.使用AVPlayer的方法开启下载服务
.AVURLAsset *urlAsset = [[AVURLAsset alloc]initWithURL:url options:nil];
.AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset];
.[self.avPlayer replaceCurrentItemWithPlayerItem:item];
.[self addObserverToPlayerItem:item];
但由于AVPlayer是没有提供方法给我们直接获取它下载下来的数据,所以我们只能在视频下载完之后自己去寻找缓存视频数据的办法,AVFoundation框架中有一种从多媒体信息类AVAsset中提取视频数据的类AVMutableComposition和AVAssetExportSession。
其中AVMutableComposition的作用是能够从现有的asset实例中创建出一个新的AVComposition(它也是AVAsset的字类),使用者能够从别的asset中提取他们的音频轨道或视频轨道,并且把它们添加到新建的Composition中。
AVAssetExportSession的作用是把现有的自己创建的asset输出到本地文件中。
为什么需要把原先的AVAsset(AVURLAsset)实现的数据提取出来后拼接成另一个AVAsset(AVComposition)的数据后输出呢,由于通过网络url下载下来的视频没有保存视频的原始数据(或者苹果没有暴露接口给我们获取),下载后播放的avasset不能使用AVAssetExportSession输出到本地文件,要曲线地把下载下来的视频通过重构成另外一个AVAsset实例才能输出。代码例子如下:
NSString *documentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[];
NSString *myPathDocument = [documentDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.mp4",[_source.videoUrl MD5]]]; NSURL *fileUrl = [NSURL fileURLWithPath:myPathDocument]; if (asset != nil) {
AVMutableComposition *mixComposition = [[AVMutableComposition alloc]init];
AVMutableCompositionTrack *firstTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
[firstTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:[[asset tracksWithMediaType:AVMediaTypeVideo]objectAtIndex:] atTime:kCMTimeZero error:nil]; AVMutableCompositionTrack *audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
[audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:[[asset tracksWithMediaType:AVMediaTypeAudio]objectAtIndex:] atTime:kCMTimeZero error:nil]; AVAssetExportSession *exporter = [[AVAssetExportSession alloc]initWithAsset:mixComposition presetName:AVAssetExportPresetHighestQuality];
exporter.outputURL = fileUrl;
if (exporter.supportedFileTypes) {
exporter.outputFileType = [exporter.supportedFileTypes objectAtIndex:] ;
exporter.shouldOptimizeForNetworkUse = YES;
[exporter exportAsynchronouslyWithCompletionHandler:^{ }]; }
}
3.使用AVAssetResourceLoader回调下载,也是最终决定使用的技术
AVAssetResourceLoader通过你提供的委托对象去调节AVURLAsset所需要的加载资源。而很重要的一点是,AVAssetResourceLoader仅在AVURLAsset不知道如何去加载这个URL资源时才会被调用,就是说你提供的委托对象在AVURLAsset不知道如何加载资源时才会得到调用。所以我们又要通过一些方法来曲线解决这个问题,把我们目标视频URL地址的scheme替换为系统不能识别的scheme,然后在我们调用网络请求去处理这个URL时把scheme切换为原来的scheme。
实现边下边播功能AVResourceLoader的委托对象必须要实现AVAssetResourceLoaderDelegate下五个协议的其中两个:
//在系统不知道如何处理URLAsset资源时回调
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest NS_AVAILABLE(10_9, 6_0);
//在取消加载资源后回调
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest NS_AVAILABLE(10_9, 7_0);
以下来说说具体要怎么做处理
第一步,创建一个AVURLAsset,并且用它来初始化一个AVPlayerItem
#define kCustomVideoScheme @"yourScheme"
NSURL *currentURL = [NSURL URLWithString:@"http://***.***.***"];
NSURLComponents *components = [[NSURLComponents alloc]initWithURL:currentURL resolvingAgainstBaseURL:NO];
////注意,不加这一句不能执行到回调操作
components.scheme = kCustomVideoScheme;
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:components.URL
options:nil];
//_resourceManager在接下来讲述
[urlAsset.resourceLoader setDelegate:_resourceManager queue:dispatch_get_main_queue()];
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset];
_playerItem = item; if (IOS9_OR_LATER) {
item.canUseNetworkResourcesForLiveStreamingWhilePaused = YES;
}
[self.avPlayer replaceCurrentItemWithPlayerItem:item];
self.playerLayer.player = self.avPlayer;
[self addObserverToPlayerItem:item];**
第二步,创建AVResourceManager实现AVResourceLoader协议
@interface AVAResourceLoaderManager : NSObject < AVAssetResourceLoaderDelegate >
第三步,实现两个必须的回调协议,实现中有几件需要做的事情
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
//获取系统中不能处理的URL
NSURL *resourceURL = [loadingRequest.request URL];
//判断这个URL是否遵守URL规范和其是否是我们所设定的URL
if ([self checkIsLegalURL:resourceURL] && [resourceURL.scheme isEqualToString:kCustomVideoScheme]){
//判断当前的URL网络请求是否已经被加载过了,如果缓存中里面有URL对应的网络加载器(自己封装,也可以直接使用NSURLRequest),则取出来添加请求,每一个URL对应一个网络加载器,loader的实现接下来会说明
AVResourceLoaderForASI *loader = [self asiresourceLoaderForRequest:loadingRequest];
if (loader == nil){
loader = [[AVResourceLoaderForASI alloc] initWithResourceURL:resourceURL];
loader.delegate = self;
//缓存网络加载器
[self.resourceLoaders setObject:loader forKey:[self keyForResourceLoaderWithURL:resourceURL]];
}
//加载器添加请求
[loader addRequest:loadingRequest];
//返回YES则表明使用我们的代码对AVAsset中请求网络资源做处理
return YES;
}else{
return NO;
} }
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
//如果用户在下载的过程中调用者取消了获取视频,则从缓存中取消这个请求
NSURL *resourceURL = [loadingRequest.request URL];
NSString *actualURLString = [self actualURLStringWithURL:resourceURL];
AVResourceLoaderForASI *loader = [_resourceLoaders objectForKey:actualURLString];
[loader removeRequest:loadingRequest];
}
第四步,判断缓存中是否已下载完视频
- (void)addRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
//1判断自身是否已经取消加载
if(self.isCancelled==NO){
//2判断本地中是否已经有文件的缓存,如果有,则直接从缓存中读取数据,文件保存和读取这里不做详述,使用者可根据自身情况创建文件系统
AVAResourceFile *resourceFile = [self.resourceFileManager resourceFileWithURL:self.resourceURL];
if (resourceFile) {
//3若本地文件存在,则从文件中获取以下属性
loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
//3.1contentType
loadingRequest.contentInformationRequest.contentType = resourceFile.contentType;
//3.2数据长度
loadingRequest.contentInformationRequest.contentLength = resourceFile.contentLength;
//3.3请求的偏移量
long long requestedOffset = loadingRequest.dataRequest.requestedOffset;
//3.4请求总长度
NSInteger requestedLength = loadingRequest.dataRequest.requestedLength;
//3.5取出本地文件中从偏移量到请求长度的数据
NSData *subData = [resourceFile.data subdataWithRange:NSMakeRange(@(requestedOffset).unsignedIntegerValue, requestedLength)];
//3.6返回数据给请求
[loadingRequest.dataRequest respondWithData:subData];
[loadingRequest finishLoading];
}else{
//4如果没有本地文件,则开启网络请求,从网络中获取 ,见第五步
[self startWithRequest:loadingRequest];
}
}
else{
//5如果已经取消请求,并且请求没有完成,则封装错误给请求,可自己实现
if(loadingRequest.isFinished==NO){
[loadingRequest finishLoadingWithError:[self loaderCancelledError]];
}
}
}
第五步,添加loadingRequest到网络文件加载器,这部分的操作比较长
- (void)startWithRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
//判断当前请求是否已经开启,由于苹果系统原因,会有两次回调到AVResourceLoaderDelegate,我们对其进行判断,只开启一次请求
if (self.dataTask == nil){
//根据loadingRequest中的URL创建NSURLRequest,注意在此把URL中的scheme修改为原先的scheme
NSURLRequest *request = [self requestWithLoadingRequest:loadingRequest];
__weak __typeof(self)weakSelf = self;
//获取url的绝对路径,并使用ASIHttpRequest进行网络请求,下面的请求方法经过封装,就不详说如何对ASI进行封装了,但是每一步需要做的事情能以block的形式更好说明
NSString *urlString = request.URL.absoluteString;
self.dataTask = [self GET:urlString requestBlock:^(Request *req) {
NSLog(@"### %s %@ ###", __func__, req);
//在接受到请求头部信息时,说明链接成功,数据开始传输
if (req.recvingHeader//意思是请求接受到头部信息状态){
NSLog(@"### %s recvingHeader ###", __func__);
__strong __typeof(weakSelf)strongSelf = weakSelf;
if ([urlString isEqualToString:req.originalURL.absoluteString]) {
4.1//,创建临时数据保存网络下载下来的视频信息
strongSelf.tempData = [NSMutableData data];
}
4.2//把头部信息内容写入到AVAssetResourceLoadingRequest,即loadingRequest中
[strongSelf processPendingRequests];
}
else if (req.recving//请求接受中状态){
NSLog(@"### %s recving ###", __func__);
__strong __typeof(weakSelf)strongSelf = weakSelf;
//此处需多次调用把请求的信息写入到loadingRequest的步骤,实现下载的过程中数据能输出到loadingRequest播放
if (urlString == req.originalURL.absoluteString) {
5.1//这个处理是判断此时返回的头部信息是重定向还是实际视频的头部信息,如果是重定向信息,则不作处理
if (!_contentInformation && req.responseHeaders) {
if ([req.responseHeaders objectForKey:@"Location"] ) {
NSLog(@" ### %s redirection URL ###", __func__);
}else{
//5.2如果不是重定向信息,则把需要用到的信息提取出来
_contentInformation = [[RLContentInformationForASI alloc]init];
long long numer = [[req.responseHeaders objectForKey:@"Content-Length"]longLongValue];
_contentInformation.contentLength = numer;
_contentInformation.byteRangeAccessSupported = YES;
_contentInformation.contentType = [req.responseHeaders objectForKey:@"Content-type"];
}
} //5.3开始从请求中获取返回数据
NSLog(@"### %s before tempData length = %lu ###", __FUNCTION__, (unsigned long)self.tempData.length);
strongSelf.tempData = [NSMutableData dataWithData:req.rawResponseData];
NSLog(@"### %s after tempData length = %lu ###",__FUNCTION__, (unsigned long)self.tempData.length);
//5.4把返回数据输出到loadingRequest中
[strongSelf processPendingRequests];
}
}else if (req.succeed){
//请求返回成功,在这里做最后一次把数据输出到loadingRequest,且做一些成功后的事情
NSLog(@"### %s succeed ###", __func__);
NSLog(@"### %s tempData length = %lu ###", __func__, (unsigned long)self.tempData.length);
__strong __typeof(weakSelf)strongSelf = weakSelf;
if (strongSelf) {
[strongSelf processPendingRequests]; //保存缓存文件,我在保存文件这里做了一次偷懒,如果有人参考我写的文件可对保存文件作改进,在每次返回数据时把数据追加写到文件,而不是下载成功之后才保存,这请求时也可以使用这个来实现断点重输的功能
AVAResourceFile *resourceFile = [[AVAResourceFile alloc]initWithContentType:strongSelf.contentInformation.contentType date:strongSelf.tempData];
[strongSelf.resourceFileManager saveResourceFile:resourceFile withURL:self.resourceURL];
//在此做一些清理缓存、释放对象和回调到上层的操作
[strongSelf complete];
if (strongSelf.delegate && [strongSelf.delegate respondsToSelector:@selector(resourceLoader:didLoadResource:)]) {
[strongSelf.delegate resourceLoader:strongSelf didLoadResource:strongSelf.resourceURL];
}
}
}else if (req.failed){
//9如果请求返回失败,则向上层抛出错误,且清理缓存等操作
NSLog(@"### %s failed ###" , __func__);
[self completeWithError:req.error];
}
}];
}
[self.pendingRequests addObject:loadingRequest];
}
第六步,把请求返回数据输出到loadingRequest的操作
- (void)processPendingRequests
{
__weak __typeof(self)weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong __typeof(weakSelf)strongSelf = weakSelf;
NSMutableArray *requestsCompleted = [NSMutableArray array];
//从缓存信息中找出当前正在请求中的loadingRequest
for (AVAssetResourceLoadingRequest *loadingRequest in strongSelf.pendingRequests){
//把头部信息输出到loadingRequest中
[strongSelf fillInContentInformation:loadingRequest.contentInformationRequest];
//把视频数据输出到loadingRequest中
BOOL didRespondCompletely = [strongSelf respondWithDataForRequest:loadingRequest.dataRequest];
//在success状态中做最后一次调用的时候,检测到请求已经完成,则从缓存信息中清除loadingRequest,并且把loadingRequest标志为完成处理状态
if (didRespondCompletely){
[requestsCompleted addObject:loadingRequest];
[loadingRequest finishLoading];
}
}
//清理缓存
[strongSelf.pendingRequests removeObjectsInArray:requestsCompleted];
});
}
、 //把提取出来的头部信息输出到loadingRequest中,可以优化
- (void)fillInContentInformation:(AVAssetResourceLoadingContentInformationRequest *)contentInformationRequest
{
if (contentInformationRequest == nil || self.contentInformation == nil){
return;
}
contentInformationRequest.byteRangeAccessSupported = self.contentInformation.byteRangeAccessSupported;
contentInformationRequest.contentType = self.contentInformation.contentType;
contentInformationRequest.contentLength = self.contentInformation.contentLength;
} //把缓存数据输出到loadingRequest中
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest
{
long long startOffset = dataRequest.requestedOffset;
if (dataRequest.currentOffset != ){
startOffset = dataRequest.currentOffset;
} // Don't have any data at all for this request
if (self.tempData.length < startOffset){
return NO;
} // This is the total data we have from startOffset to whatever has been downloaded so far
NSUInteger unreadBytes = self.tempData.length - (NSUInteger)startOffset; // Respond with whatever is available if we can't satisfy the request fully yet
NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes); [dataRequest respondWithData:[self.tempData subdataWithRange:NSMakeRange((NSUInteger)startOffset, numberOfBytesToRespondWith)]]; long long endOffset = startOffset + dataRequest.requestedLength;
BOOL didRespondFully = self.tempData.length >= endOffset; return didRespondFully;
}
视频边下边播的流程大致上已经描述完毕,本博文中没有说到的代码有错误处理方式、缓存文件的读写和保存格式、部分内存缓存使用说明、
参考链接:
http://www.codeproject.com/Articles/875105/Audio-streaming-and-caching-in-iOS-using
http://www.cnblogs.com/kenshincui/p/4186022.html#mpMoviePlayerController
补充:
在开发过程中遇到的一些坑在这里补充一下
1.在iOS9后,AVPlayer的replaceCurrentItemWithPlayerItem方法在切换视频时底层会调用信号量等待然后导致当前线程卡顿,如果在UITableViewCell中切换视频播放使用这个方法,会导致当前线程冻结几秒钟。遇到这个坑还真不好在系统层面对它做什么,后来找到的解决方法是在每次需要切换视频时,需重新创建AVPlayer和AVPlayerItem。
2.iOS9后,AVFoundation框架还做了几点修改,如果需要切换视频播放的时间,或需要控制视频从头播放调用seekToDate方法,需要保持视频的播放rate大于0才能修改,还有canUseNetworkResourcesForLiveStreamingWhilePaused这个属性,在iOS9前默认为YES,之后默认为NO。
3.AVPlayer的replaceCurrentItemWithPlayerItem方法正常是会引用住参数AVPlayerItem的,但在某些情况下导致视频播放失败,它会马上释放对这个对象的持有,假如你对AVPlayerItem的实例对象添加了监听,但是自己没有对item的计数进行管理,不知道什么时候释放这个监听,则会导致程序崩溃。
4.为什么我选择第三种方法实现边下边播,第一种方法需要程序引入LocalServer库,需增加大量app包大小,且需要开启本地服务,从性能方面考虑也是不合适。第二种方式存在的缺陷很多,一来只能播放网络上返回格式contentType为public/mpeg4等视频格式的url视频地址,若保存下来之后,文件的格式也需要保存为.mp4或.mov等格式的本地文件才能从本地中读取,三来使用AVMutableComposition对视频进行重构后保存,经过检验会对视频源数据产生变化,对于程序开发人员来说,需要保证各端存在的视频数据一致。第三种边下边播的方法其实是对第二种方法的扩展,能够解决上面所说的三种问题,可操控的自由度更高。
ios 音视频实现边播边缓存的思路和解决方案 (转)的更多相关文章
- Android设备广告投放解决方案——大量网络图片、多个网络视频的轮播、缓存与更新
转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/7742996.html 一:业务场景 基于Android系统的设备上投放广告,诸如:地铁广告屏.自助服务机器上的 ...
- iOS 音视频播放
播放控制切换为: ijkplayer wiki: https://github.com/changsanjiang/SJVideoPlayer/wiki/Use-ijkplayer 播放控制切换为: ...
- ios上视频与音乐合成后出现播放兼容问题的解决方法
近期EasyDarwin开源流媒体团队EasyVideoRecorder小组同学Carl在支持一款短视频应用上线时,遇到一个问题:我们在IOS上合成"图片+音乐"成为视频之后,在P ...
- iOS视频边下边播--缓存播放数据流
实现视频边下边播,这里的边下边播不是单独开一个子线程去下载,而是把视频播放的数据给保存到本地.简而言之,就是使用一遍的流量,既播放了视频,也保存了视频. 用到的框架:<AVFoundation/ ...
- iOS ksyhttpcache音视频缓存
pod 'ksyhttpcache' 桥接文件 引入 #import <KSYHTTPCache/KSYHTTPProxyService.h> 带appdelegate里初始化 KSYHT ...
- ios ktvhttpcache 音视频缓存插件使用
1.PodFile 文件增加 pod 'KTVHTTPCache', '~> 2.0.0' 2.在终端 需要先cd到podfile文件所在目录 执行pod install 3.在header ...
- Android IOS WebRTC 音视频开发总结(五七)-- 网络传输上的一种QoS方案
本文主要介绍一种QoS的解决方案,文章来自博客园RTC.Blacker,欢迎关注微信公众号blacker,更多详见www.rtc.help QoS出现的背景: 而当网络发生拥塞的时候,所有的数据流都有 ...
- 视频边下边播--缓存播放数据流-b
google搜索“iOS视频变下边播”,有好几篇博客写到了实现方法,其实只有一篇,其他都是copy的,不过他们都是使用的本地代理服务器的方式. 原理很简单,但是缺点也很明显,需要自己写一个本地代理服务 ...
- 转:Android IOS WebRTC 音视频开发总结 (系列文章集合)
随笔分类 - webrtc Android IOS WebRTC 音视频开发总结(七八)-- 为什么WebRTC端到端监控很关键? 摘要: 本文主要介绍WebRTC端到端监控(我们翻译和整理的,译 ...
随机推荐
- iOS总结_UI层自我复习总结
UI层复习笔记 在main文件中,UIApplicationMain函数一共做了三件事 根据第三个参数创建了一个应用程序对象 默认写nil,即创建的是UIApplication类型的对象,此对象看成是 ...
- 分布式锁1 Java常用技术方案
前言: 由于在平时的工作中,线上服务器是分布式多台部署的,经常会面临解决分布式场景下数据一致性的问题,那么就要利用分布式锁来解决这些问题.所以自己结合实际工作中的一些经验和网上看到的一些资 ...
- Electron使用与学习--(页面间的通信)
目录结构: index.js是主进程js. const electron = require('electron') const app = electron.app const BrowserWin ...
- 比Mysqli操作数据库更简便的方式 。PDO
下面来说一下PDO 先画一张图来了解一下 mysqli是针对mysql这个数据库扩展的一个类 PDO是为了能访问更多数据库 如果出现程序需要访问其他数据库的话就可以用PDO来做 PDO数据访问抽象层1 ...
- .net 分布式架构之业务消息队列
开源QQ群: .net 开源基础服务 238543768 开源地址: http://git.oschina.net/chejiangyi/Dyd.BusinessMQ ## 业务消息队列 ##业务消 ...
- EF上下文对象线程内唯一性与优化
在一次请求中,即一个线程内,若是用到EF数据上下文对象,就创建一个,这也加是很多人的代码中习惯在使用上下文对象时,习惯将对象建立在using中,也是为了尽早释放上下文对象, 但是如果有一个业务逻辑调用 ...
- AutoMapper的介绍与使用(一)
软件环境 vs2015 asp.net mvc 5 .NET Framework 4.5.2 AutoMapper 5.2.0.0 AutoMapper安装 新建asp.net mvc 项目 Auto ...
- StatePattern(状态模式)
/** * 状态模式 * @author TMAC-J * 状态模式和策略模式很像,其实仔细研究发现完全不一样 * 策略模式各策略之间没有任何关系,独立的 * 状态模式各状态之间接口方法都是一样的 * ...
- JAVA的内存模型(变量的同步)
一个线程中变量的修改可能不会立即对其他线程可见,事实上也许永远不可见. 在代码一中,如果一个线程调用了MyClass.loop(),将来的某个时间点,另一个线程调用了MyClass.setValue( ...
- Android listview和gridview以及view的区别
GridView 可以指定显示的条目的列数. listview一般显示的条目的列数都是一列 如果是列表(单列多行形式)的使用ListView,如果是多行多列网状形式的优先使用GridView andr ...