Universal-Image-Loader源码解解析---display过程 + 获取bitmap过程
Universal-Image-Loader在github上的地址:https://github.com/nostra13/Android-Universal-Image-Loader
它的基本使用请参考我的另一篇博客 http://www.cnblogs.com/yuan1225/p/8426900.html,下面我从源码角度研究它。
ImageLoader使用的是双重判断的懒汉试单例模式。
/** Returns singleton class instance */
public static ImageLoader getInstance() {
if (instance == null) {
synchronized (ImageLoader.class) {
if (instance == null) {
instance = new ImageLoader();
}
}
}
return instance;
}
先看ImageLoader的初始化过程:
/**
* Initializes ImageLoader instance with configuration.<br />
* If configurations was set before ( {@link #isInited()} == true) then this method does nothing.<br />
* To force initialization with new configuration you should {@linkplain #destroy() destroy ImageLoader} at first.
*
* @param configuration {@linkplain ImageLoaderConfiguration ImageLoader configuration}
* @throws IllegalArgumentException if <b>configuration</b> parameter is null
*/
public synchronized void init(ImageLoaderConfiguration configuration) {
if (configuration == null) {
throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL);
}
if (this.configuration == null) {
L.d(LOG_INIT_CONFIG);
engine = new ImageLoaderEngine(configuration);
this.configuration = configuration;
} else {
L.w(WARNING_RE_INIT_CONFIG);
}
}
以上是在Application中调用 ImageLoader.getInstance().init(config.build());的初始化过程。接下来是ImageLoader的使用过程分析
ImageLoader.getInstance().displayImage(url, imageView, options);
displayImage方法有很大重载的方法,最终都会辗转调用到最复杂的这个重载方法:
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
checkConfiguration();
if (imageAware == null) {
throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
}
if (listener == null) {
listener = defaultListener;
}
if (options == null) {
options = configuration.defaultDisplayImageOptions;
} if (TextUtils.isEmpty(uri)) {
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingStarted(uri, imageAware.getWrappedView());
if (options.shouldShowImageForEmptyUri()) {
imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
} else {
imageAware.setImageDrawable(null);
}
listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
return;
} if (targetSize == null) {
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
engine.prepareDisplayTaskFor(imageAware, memoryCacheKey); listener.onLoadingStarted(uri, imageAware.getWrappedView()); Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {
L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey); if (options.shouldPostProcess()) {
ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
} else {
options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
}
} else {
if (options.shouldShowImageOnLoading()) {
imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
} else if (options.isResetViewBeforeLoading()) {
imageAware.setImageDrawable(null);
} ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
options, listener, progressListener, engine.getLockForUri(uri));
LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
defineHandler(options));
if (options.isSyncLoading()) {
displayTask.run();
} else {
engine.submit(displayTask);
}
}
}
参数的意义:
1.URI uri : Image URI;可用的几种URI:
"http://site.com/image.png" // from Web
"file:///mnt/sdcard/image.png" // from SD card
"file:///mnt/sdcard/video.mp4" // from SD card (video thumbnail)
"content://media/external/images/media/13" // from content provider
"content://media/external/video/media/13" // from content provider (video thumbnail)
"assets://image.png" // from assets
"drawable://" + R.drawable.img // from drawables (non-9patch images)
2.ImageAware imageAware : 显示图像的视图,是androidimageview的包装类,参数来源是new ImageViewAware(imageView).不能为空
3.DisplayImageOptions options :用于图像解码和显示,如果该参数为空则使用默认的option。
4.ImageSize targetSize:图像目标大小。 如果为空 - 大小将取决于视图。
5.ImageLoadingListener listener:用于图像加载过程监听。 如果在UI线程上调用此方法,则监听器在UI线程上触发事件
6.ImageLoadingProgressListener progressListener:图像加载进度监听。 如果在UI线程上调用此方法,则监听器在UI线程上触发事件。 应在{option选项}中启用缓存磁盘以使此侦听器正常工作。
这个方法比较长,它的逻辑比较清晰,主要做了下面的方法:
1.判断各个参数是否合法,是否需要默认值
判断配置参数和显示图片的控件是否为空,如果为空直接抛出了异常
判断listener options targetsize是否为null,如果为空则使用默认值
判断uri是否为空,如果uri为空,则在ImageLoaderEngine中取消该视图的显示任务,如果在options中设置了showImageForEmptyUri(R.drawable.ic_empty)则为该视图显示一个默认的空uri时的图片,直接返回。
2.开始监听下载任务。先从缓存中读取图片的bitmap,如果缓存中有则直接使用,否则需要从磁盘或者从网络下载图片。
下面就来看如何从缓存中读取,如何下载。
当从memoryCache读取的bitmap不为null 并且没有被回收时,就直接展示缓存中的这个bitmap。默认情况下options.shouldPostProcess()是false。除非在初始化options选项时设置了postProcesser。
所以我们之间看49行。点开display方法,咦,它是一个接口。
它有几个实现类分别实现不同的图片显示方法。如果在初始化options选项没有设置displayer()选项则默认使用SimpleBitmapDisplayer()正常显示一张图片。如果设置了如下
.displayer(new CircleBitmapDisplayer(Color.WHITE,5))则显示圆角图片。
下面以SimpleBitmapDisplayer为例,分析如何实现display的。
public final class SimpleBitmapDisplayer implements BitmapDisplayer {
@Override
public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {
imageAware.setImageBitmap(bitmap);
}
}
这个实现类比较简单,只有一个方法一句话,可以把它理解成我们在android中给imageView设置图片的一种方式:imageview.setImageBitmap(bitmap);
ImageViewAware是Android imageView的包装类,保持ImageView的弱引用以防止内存泄漏。如何使用imageview的弱引用这一步暂时忽略,先回到第34行。
以上是缓存中有bitmap,下面分析如果从缓存中获取的bitmap为空,则需要加载。因为Androidx.x之后不容许在UI线程中做网络加载的操作,所以我们只分析异步加载的方式,就是第66行engine.submit(displayTask);
在这里涉及到了一个非常重要的类:ImageLoaderEngine,它负责{LoadAndDisplayImageTask显示任务}的执行。下面重点解析一下ImageLoaderEngine这个类。
engine这个对象是什么时候初始化的呢?请回到Imageloader对象的初始化方法init中,第15行:engine = new ImageLoaderEngine(configuration);
看一下ImageLoaderEngine的构造方法:
ImageLoaderEngine(ImageLoaderConfiguration configuration) {
this.configuration = configuration; taskExecutor = configuration.taskExecutor;
taskExecutorForCachedImages = configuration.taskExecutorForCachedImages; taskDistributor = DefaultConfigurationFactory.createTaskDistributor();
}
以taskExecutorForCachedImages 为例,taskExecutorForCachedImages 是一个线程池,异步显示memory cache里面的bitmap。
进入submit方法:
/** Submits task to execution pool */
void submit(final LoadAndDisplayImageTask task) {
taskDistributor.execute(new Runnable() {
@Override
public void run() {
File image = configuration.diskCache.get(task.getLoadingUri());
boolean isImageCachedOnDisk = image != null && image.exists();
initExecutorsIfNeed();
if (isImageCachedOnDisk) {
taskExecutorForCachedImages.execute(task);
} else {
taskExecutor.execute(task);
}
}
15 });
}
当执行到第10行时,就会调用LoadAndDisplayImageTask的run方法,接下来看这个类。这个类的对象封装了engine和Imageloadinginfo,所以包含了所有的configuration 和options选项。
来看它的run方法
@Override
public void run() {
if (waitIfPaused()) return;
if (delayIfNeed()) return; ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
if (loadFromUriLock.isLocked()) {
L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
} loadFromUriLock.lock();
Bitmap bmp;
try {
checkTaskNotActual(); bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp == null || bmp.isRecycled()) {
bmp = tryLoadBitmap();
if (bmp == null) return; // listener callback already was fired checkTaskNotActual();
checkTaskInterrupted(); if (options.shouldPreProcess()) {
L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPreProcessor().process(bmp);
if (bmp == null) {
L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
}
} if (bmp != null && options.isCacheInMemory()) {
L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
configuration.memoryCache.put(memoryCacheKey, bmp);
}
} else {
loadedFrom = LoadedFrom.MEMORY_CACHE;
L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey);
} if (bmp != null && options.shouldPostProcess()) {
L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey);
bmp = options.getPostProcessor().process(bmp);
if (bmp == null) {
L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
}
}
checkTaskNotActual();
checkTaskInterrupted();
} catch (TaskCancelledException e) {
fireCancelEvent();
return;
} finally {
loadFromUriLock.unlock();
} DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
runTask(displayBitmapTask, syncLoading, handler, engine);
}
在这里使用了可重入锁机制来保证并发操作时数据的完整性。先从缓存中获取到的bitmap==null,看19行,来看这个方法。
private Bitmap tryLoadBitmap() throws TaskCancelledException {
Bitmap bitmap = null;
try {
File imageFile = configuration.diskCache.get(uri);
if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
loadedFrom = LoadedFrom.DISC_CACHE; checkTaskNotActual();
bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
}
if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
loadedFrom = LoadedFrom.NETWORK; String imageUriForDecoding = uri;
if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
imageFile = configuration.diskCache.get(uri);
if (imageFile != null) {
imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
}
} checkTaskNotActual();
bitmap = decodeImage(imageUriForDecoding); if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
fireFailEvent(FailType.DECODING_ERROR, null);
}
}
}
.... return bitmap;
}
在这个方法返回了一个解码后的bitmap,是从磁盘读取文件或者网络中获取的。获取到bitmap后就回到run方法的58行。这里将bmp, imageLoadingInfo, engine等封装了一个runnable的实现类DisplayBitmapTask中。
同样来看一下这个runnable的run方法。
@Override
public void run() {
if (imageAware.isCollected()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else if (isViewWasReused()) {
L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
} else {
L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);
displayer.display(bitmap, imageAware, loadedFrom);
engine.cancelDisplayTaskFor(imageAware);
listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
}
}
第11行就回到了display方法,去给我们的ImageView设置bitmap。到此就完成了图片在控件中的展示过程。取消这次任务,并回调监听器的onLoadingComplete方法。
接下来我们来分析这个框架是如何加载图片的bitmap的。回到
tryLoadBitmap()方法的第10行或者25行,看decodeImage(imageUriForDecoding);这个方法
private Bitmap decodeImage(String imageUri) throws IOException {
ViewScaleType viewScaleType = imageAware.getScaleType();
ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
getDownloader(), options);
return decoder.decode(decodingInfo);
}
图片的uri等信息被封装到了ImageDecodingInfo对象当中。其中getDownloader()返回了ImageDownloader这个接口实现类对象的引用。 那这个ImageDownloader的对象是从哪里来的呢?我们看LoadAndDisplayImageTask的构造方式中,
downloader = configuration.downloader;
networkDeniedDownloader = configuration.networkDeniedDownloader;
slowNetworkDownloader = configuration.slowNetworkDownloader;
而configuration 是最初在Application类中初始化 ImageLoader 的时候用户传递过来的参数。初始化ImageLoaderConfiguration.Builder时为其设置imageDownloader,如果不设置该变量,则在build()的时候在initEmptyFieldsWithDefaultValues方法中为其初始化一个默认值:
if (downloader == null) {
downloader = DefaultConfigurationFactory.createImageDownloader(context);
}
其实也就是ImageDownloader的一个实现类的对象BaseImageDownloader对象
public static ImageDownloader createImageDownloader(Context context) {
return new BaseImageDownloader(context);
}
而ImageDownloader对外提供了getStream方法,根据uri获取输入流信息。
InputStream getStream(String imageUri, Object extra)
然后我们来看decodeImage方法中的decode做了什么。ImageDecoder是一个接口,里面只有一个decode方法,它的主要作用就是将获取到的InputStream转换成Bitmap。看它的实现类BaseImageDecoder
public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
Bitmap decodedBitmap;
ImageFileInfo imageInfo; InputStream imageStream = getImageStream(decodingInfo);
if (imageStream == null) {
L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
return null;
}
try {
imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
imageStream = resetStream(imageStream, decodingInfo);
Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
} finally {
IoUtils.closeSilently(imageStream);
} if (decodedBitmap == null) {
L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
} else {
//将Bitmap缩放和旋转成满足需求的Bitmap
decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
imageInfo.exif.flipHorizontal);
}
return decodedBitmap;
}
第5行,这个方法调用的就是ImageDownloader的 getStream,并根据uri的Scheme信息判断这个图片是在哪里,从这里真正去获取bitmap。
@Override
public InputStream getStream(String imageUri, Object extra) throws IOException {
switch (Scheme.ofUri(imageUri)) {
case HTTP:
case HTTPS:
return getStreamFromNetwork(imageUri, extra);
case FILE:
return getStreamFromFile(imageUri, extra);
case CONTENT:
return getStreamFromContent(imageUri, extra);
case ASSETS:
return getStreamFromAssets(imageUri, extra);
case DRAWABLE:
return getStreamFromDrawable(imageUri, extra);
case UNKNOWN:
default:
return getStreamFromOtherSource(imageUri, extra);
}
}
我们以网络获取为例进行分析:
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
HttpURLConnection conn = createConnection(imageUri, extra); int redirectCount = 0;
while (conn.getResponseCode() / 100 == 3 && redirectCount < MAX_REDIRECT_COUNT) {
conn = createConnection(conn.getHeaderField("Location"), extra);
redirectCount++;
} InputStream imageStream;
try {
imageStream = conn.getInputStream();
} catch (IOException e) {
// Read all data to allow reuse connection (http://bit.ly/1ad35PY)
IoUtils.readAndCloseStream(conn.getErrorStream());
throw e;
}
if (!shouldBeProcessed(conn)) {
IoUtils.closeSilently(imageStream);
throw new IOException("Image request failed with response code " + conn.getResponseCode());
} return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());
}
通过HttpURLConnection从服务器获取一个InputStream,封装在了一个自定义的ContentLengthInputStream中。获取到InputStream后返回到decode方法的11行.
protected ImageFileInfo defineImageSizeAndRotation(InputStream imageStream, ImageDecodingInfo decodingInfo)
throws IOException {
Options options = new Options();
options.inJustDecodeBounds = true; //true那么将不返回实际的bitmap对象,不给其分配内存空间但是可以得到一些解码边界信息即图片大小等信息
BitmapFactory.decodeStream(imageStream, null, options); ExifInfo exif;
String imageUri = decodingInfo.getImageUri();
if (decodingInfo.shouldConsiderExifParams() && canDefineExifParams(imageUri, options.outMimeType)) {
exif = defineExifOrientation(imageUri);
} else {
exif = new ExifInfo();
}
return new ImageFileInfo(new ImageSize(options.outWidth, options.outHeight, exif.rotation), exif);
}
在这个方法中第一次调用 BitmapFactory.decodeStream(imageStream, null, options); 获取图片的大小解码边界等信息
decode方法的第12行:是重新获取bitmap,这个我也没有搞清楚它为什么又获取一遍,可能是因为调用过来一次decodeStream,但是第一次调用只是为了得到图片的一些信息,并未得到bitmap,所以需要重新对一个新的inputStream进行decode,
只是这个时候已经知道了图片的大小 格式等信息了。就可以直接返回bitmap。 至此,过去bitmap的过程已经完毕,这就是display时调用的imageAware.setImageBitmap(bitmap);中的bitmap的由来。
Universal-Image-Loader源码解解析---display过程 + 获取bitmap过程的更多相关文章
- netty源码解解析(4.0)-11 Channel NIO实现-概览
结构设计 Channel的NIO实现位于io.netty.channel.nio包和io.netty.channel.socket.nio包中,其中io.netty.channel.nio是抽象实 ...
- netty源码解解析(4.0)-10 ChannelPipleline的默认实现--事件传递及处理
事件触发.传递.处理是DefaultChannelPipleline实现的另一个核心能力.在前面在章节中粗略地讲过了事件的处理流程,本章将会详细地分析其中的所有关键细节.这些关键点包括: 事件触发接口 ...
- netty源码解解析(4.0)-17 ChannelHandler: IdleStateHandler实现
io.netty.handler.timeout.IdleStateHandler功能是监测Channel上read, write或者这两者的空闲状态.当Channel超过了指定的空闲时间时,这个Ha ...
- netty源码解解析(4.0)-18 ChannelHandler: codec--编解码框架
编解码框架和一些常用的实现位于io.netty.handler.codec包中. 编解码框架包含两部分:Byte流和特定类型数据之间的编解码,也叫序列化和反序列化.不类型数据之间的转换. 下图是编解码 ...
- netty源码解解析(4.0)-20 ChannelHandler: 自己实现一个自定义协议的服务器和客户端
本章不会直接分析Netty源码,而是通过使用Netty的能力实现一个自定义协议的服务器和客户端.通过这样的实践,可以更深刻地理解Netty的相关代码,同时可以了解,在设计实现自定义协议的过程中需要解决 ...
- netty源码解解析(4.0)-4 线程模型-概览
netty线程体系概览 netty的高并发能力很大程度上由它的线程模型决定的,netty定义了两种类型的线程: I/O线程: EventLoop, EventLoopGroup.一个EventLoop ...
- netty源码解解析(4.0)-14 Channel NIO实现:读取数据
本章分析Nio Channel的数据读取功能的实现. Channel读取数据需要Channel和ChannelHandler配合使用,netty设计数据读取功能包括三个要素:Channel, Eve ...
- netty源码解解析(4.0)-15 Channel NIO实现:写数据
写数据是NIO Channel实现的另一个比较复杂的功能.每一个channel都有一个outboundBuffer,这是一个输出缓冲区.当调用channel的write方法写数据时,这个数据被一系列C ...
- netty源码解解析(4.0)-13 Channel NIO实现: 关闭和清理
Channel提供了3个方法用来实现关闭清理功能:disconnect,close,deregister.本章重点分析这个3个方法的功能的NIO实现. disconnect实现: 断开连接 disco ...
随机推荐
- 【读书笔记】C++Primer---第一章
1.标准库的头文件用尖括号<>括起来,非标准库的头文件用双引号“”括起来:
- hadoop配置文件详解系列(二)-hdfs-site.xml篇
上一篇介绍了core-site.xml的配置,本篇继续介绍hdfs-site.xml的配置. 属性名称 属性值 描述 hadoop.hdfs.configuration.version 1 配置文件的 ...
- php面向对象中的魔术方法
原创,转载请注明出处 在 PHP 中以两个下划线开头的方法,__construct(), __destruct (), __call(), __callStatic(),__get(), __set( ...
- 批处理(Batch)---批处理脚本。
批处理(Batch),也称为批处理脚本.顾名思义,批处理就是对某对象进行批量的处理,通常被认为是一种简化的脚本语言,它应用于DOS和Windows系统中.批处理文件的扩展名为bat .目前比较常见的批 ...
- windows系统命令行
使用 命令+/?就可显示命令的详细说明. 比如 ping/?就可知道ping命令的详细使用说明 netstat /?就可知道ping命令的使用说明
- ResultSet只返回一行数据的原因
写之前,先告戒一下自己......写代码一定要细心,自己写的即使是非常简单的地方也要细心,不能自我感觉太良好,那往往可能会有些bug在等着你...... 注意事项: 1.当你为了查看数据库中是否存在某 ...
- 如何利用Python网络爬虫爬取微信朋友圈动态--附代码(下)
前天给大家分享了如何利用Python网络爬虫爬取微信朋友圈数据的上篇(理论篇),今天给大家分享一下代码实现(实战篇),接着上篇往下继续深入. 一.代码实现 1.修改Scrapy项目中的items.py ...
- Vector简单介绍
/*枚举就是Vector特有的取出方式发现枚举和迭代器很像其实枚举和迭代是一样的因为枚举的名称以及方法的名称都过长了.所以被迭代器取代了枚举郁郁而终了. */ import java.util.*;c ...
- 系列博文-Three.js入门指南(张雯莉)-静态demo和three.js功能概览
一:一个最简单的静态DEMO //body加载完后触发init() //WebGL的渲染是需要HTML5 Canvas元素的,你可以手动在HTML的<body>部分中定义Canvas元素, ...
- Extjs 上传文件 IE不兼容的问题[提示下载保存]
我最不喜欢的浏览器的是IE,但无奈很多项目的客户使用的是IE. 在使用Extjs做文件上传时,其他浏览器没有问题,但IE却一个劲提示保存文件,看服务端运行,它其实是运行成功了已经,但客户端的进度条却一 ...