Glide使用实例

Glide图片加载库的使用实例

在泰国举行的谷歌开发者论坛上,谷歌为我们介绍了一个名叫 Glide 的图片加载库,作者是bumptech。这个库被广泛的运用在google的开源项目中,包括2014年google I/O大会上发布的官方app。

Glide Github 地址:
https://github.com/shenjianli/glide

Glide效果图:
glide

其中代码结构和Picasso的使用实例十分相似,具体的代码逻辑与结构请参考Picasso使用实例

全面学习Glide可移步—>http://mrfu.me/2016/02/27/Glide_Getting_Started/

总体设计及流程

whole design
上面是 Glide 的总体设计图。整个库分为 RequestManager(请求管理器),Engine(数据获取引擎)、 Fetcher(数据获取器)、MemoryCache(内存缓存)、DiskLRUCache、Transformation(图片处理)、Encoder(本地缓存存储)、Registry(图片类型及解析器配置)、Target(目标) 等模块。

简单的讲就是 Glide 收到加载及显示资源的任务,创建 Request 并将它交给RequestManager,Request 启动 Engine 去数据源获取资源(通过 Fetcher ),获取到后 Transformation 处理后交给 Target。

Glide 依赖于 DiskLRUCache、GifDecoder 等开源库去完成本地缓存和 Gif 图片解码工作。

Glide 优点

(1) 图片缓存->媒体缓存
Glide 不仅是一个图片缓存,它支持 Gif、WebP、缩略图。甚至是 Video,所以更该当做一个媒体缓存。
(2) 支持优先级处理
(3) 与 Activity/Fragment 生命周期一致,支持 trimMemory
Glide 对每个 context 都保持一个 RequestManager,通过 FragmentTransaction 保持与 Activity/Fragment 生命周期一致,并且有对应的 trimMemory 接口实现可供调用。
(4) 支持 okhttp、Volley
Glide 默认通过 UrlConnection 获取数据,可以配合 okhttp 或是 Volley 使用。实际 ImageLoader、Picasso 也都支持 okhttp、Volley。
(5) 内存友好
① Glide 的内存缓存有个 active 的设计
从内存缓存中取数据时,不像一般的实现用 get,而是用 remove,再将这个缓存数据放到一个 value 为软引用的 activeResources map 中,并计数引用数,在图片加载完成后进行判断,如果引用计数为空则回收掉。
② 内存缓存更小图片
Glide 以 url、view_width、view_height、屏幕的分辨率等做为联合 key,将处理后的图片缓存在内存缓存中,而不是原始图片以节省大小
③ 与 Activity/Fragment 生命周期一致,支持 trimMemory
④ 图片默认使用默认 RGB_565 而不是 ARGB_888
虽然清晰度差些,但图片更小,也可配置到 ARGB_888。
其他:Glide 可以通过 signature 或不使用本地缓存支持 url 过期

下面来说明一下存在的问题:

1.有的图片第一次加载的时候只显示占位图,第二次才显示正常的图片呢
如果你刚好使用了这个圆形Imageview库或者其他的一些自定义的圆形Imageview,而你又刚好设置了占位的话,那么,你就会遇到第一个问题。如何解决呢?
方案一: 不设置占位;
方案二:使用Glide的Transformation API自定义圆形Bitmap的转换。这里是一个已有的例子;
方案三:使用下面的代码加载图片:

1
2
3
4
5
6
7
8
9
Glide.with(mContext)
.load(url)
.placeholder(R.drawable.loading_spinner)
.into(new SimpleTarget<Bitmap>(width, height) {
@Override
public void onResourceReady(Bitmap bitmap, GlideAnimation anim) {
// setImageBitmap(bitmap) on CircleImageView
}
};

2.我总会得到类似You cannot start a load for a destroyed activity这样的异常呢
请记住一句话:不要再非主线程里面使用Glide加载图片,如果真的使用了,请把context参数换成getApplicationContext。

3.我不能给加载的图片setTag()呢
是因为你使用的姿势不对哦。如何为ImageView设置Tag呢?且听我细细道来。
方案一:使用setTag(int,object)方法设置tag,具体用法如下:
Java代码是酱紫的:

1
2
3
4
5
6
7
8
Glide.with(context).load(urls.get(i).getUrl()).fitCenter().into(imageViewHolder.image);
imageViewHolder.image.setTag(R.id.image_tag, i);
imageViewHolder.image.setOnClickListener(new View.OnClickListener() {
@Override
int position = (int) v.getTag(R.id.image_tag);
Toast.makeText(context, urls.get(position).getWho(), Toast.LENGTH_SHORT).show();
}
});

同时在values文件夹下新建ids.xml,添加


大功告成!
方案二:从Glide的3.6.0之后,新添加了全局设置的方法。具体方法如下:
先实现GlideMoudle接口,全局设置ViewTaget的tagId:
public class MyGlideMoudle implements GlideModule{
@Override
public void applyOptions(Context context, GlideBuilder builder) {
ViewTarget.setTagId(R.id.glide_tag_id);
}

@Override
public void registerComponents(Context context, Glide glide) {

}

}
同样,也需要在ids.xml下添加id


最后在AndroidManifest.xml文件里面添加


又可以愉快的玩耍了,嘻嘻`(∩_∩)′。
方案三:写一个继承自ImageViewTaget的类,复写它的get/setRequest方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Glide.with(context).load(urls.get(i).getUrl()).fitCenter().into(new ImageViewTarget<GlideDrawable>(imageViewHolder.image) {
@Override
protected void setResource(GlideDrawable resource) {
imageViewHolder.image.setImageDrawable(resource);
}
@Override
public void setRequest(Request request) {
imageViewHolder.image.setTag(i);
imageViewHolder.image.setTag(R.id.glide_tag_id,request);
}
@Override
public Request getRequest() {
return (Request) imageViewHolder.image.getTag(R.id.glide_tag_id);
}
});
imageViewHolder.image.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = (int) v.getTag();
Toast.makeText(context, urls.get(position).getWho(), Toast.LENGTH_SHORT).show();
}
});

Glide使用技巧

1.Glide.with(context).resumeRequests()和 Glide.with(context).pauseRequests()
当列表在滑动的时候,调用pauseRequests()取消请求,滑动停止时,调用resumeRequests()恢复请求。这样是不是会好些呢?
2.Glide.clear()
当你想清除掉所有的图片加载请求时,这个方法可以帮助到你。
3.ListPreloader
如果你想让列表预加载的话,不妨试一下ListPreloader这个类。

一些基于Glide的库

1.glide-transformations(https://github.com/wasabeef/glide-transformations
一个基于Glide的transformation库,拥有裁剪,着色,模糊,滤镜等多种转换效果,赞的不行不行的~~
2.GlidePalette https://github.com/florent37/GlidePalette
一个可以在Glide加载时很方便使用Palette的库。

源码下载

Glide源码

ES6 Promise

Promise的含义
所谓Promise,就是一个对象,用来传递异步操作的消息。它代表了**某个未来才知道结果的事件(通常是一个异步操作),并且这个事件提供统一的API,可供进一步处理。

Promise对象有以下两个特点:
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中),Resolved(已完成,又称Fulfiled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,这的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直操持这个结果。当改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

缺点:

1.无法取消Promise,一旦新建它就会立即执行,无法中途取消
2.不设置回调函数,Promise内部抛出错误,不会反应到外部。
3.当处于Pending状态时,无法得知目前进展到哪个阶段(刚刚开始还是完成)。

如果某些事件不断地反复发生,一般来说,使用stream模式是比部署Promise更好的选择。

基本用法

Promise对象是一个构造函数,用来生成Promise实例,创建Promise实例:

1
2
3
4
5
6
7
8
9
10
var promise = new Promise(function(resolve,reject){
//some code
if(异步操作成功){
resolve(value);
}
else{
reject(error);
}
}
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,
由JavaScript引擎提供,不用自己部署。
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从Pending变为
Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,
将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操作失败时调
用,并将异步操作报出的错误,作为参数传递出去。
Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。

1
2
3
4
5
promise.then(function(value) {
// success
}, function(value) {
// failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为Resolved时调用,
第二个回调函数是Promise对象的状态变为Reject时调用。其中,第二个函数是可选的,不一定要提供。
这两个函数都接受Promise对象传出的值作为参数。
下面是一个Promise对象的简单例子。

1
2
3
4
5
6
7
8
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value);
});

上面代码中,timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间
(ms参数)以后,Promise实例的状态变为Resolved,就会触发then方法绑定的回调函数。

如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject函数的参数
通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个
Promise实例,表示异步操作的结果有可能是一个值,也有可能是另一个异步操作,比如像下面这样。

1
2
3
4
5
6
7
var p1 = new Promise(function(resolve, reject){
// ...
});
var p2 = new Promise(function(resolve, reject){
// ...
resolve(p1);
})

上面代码中,p1和p2都是Promise的实例,但是p2的resolve方法将p1作为参数,即一个异步操作的结果
是返回另一个异步操作。
注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是Pending,
那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是Resolved或者Rejected,那么p2的回调
函数将会立刻执行。

Fresco使用实例

Fresco实例有效果图:

Fresco Example

Fresco项目的结构:

Fresco struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.shen.frescotest.MainActivity" >
<ListView
android:id="@+id/listview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:dividerHeight="1dip"
/>
</RelativeLayout>

listview中条目的布局代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_vertical"
android:padding="5dip" >
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/my_image_view"
android:layout_width="120dp"
android:layout_height="wrap_content"
fresco:viewAspectRatio="1"
fresco:fadeDuration="300"
fresco:actualImageScaleType="fitCenter"
fresco:placeholderImage="@drawable/ic_launcher"
fresco:failureImage="@drawable/ic_launcher"
fresco:roundAsCircle="true"
fresco:roundedCornerRadius="10dp"
fresco:roundTopLeft="true"
fresco:roundTopRight="true"
fresco:roundBottomLeft="true"
fresco:roundBottomRight="true"
fresco:roundingBorderWidth="1dp"
fresco:roundingBorderColor="#00ffff"
/>
<ImageView
android:id="@+id/imageView1"
android:layout_width="60dip"
android:layout_height="60dip"
android:src="@drawable/ic_launcher"
android:visibility="gone" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginLeft="15dip"
>
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="20sp"
android:layout_marginTop="5dip"/>
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="14sp"
android:layout_marginTop="2dip"/>
</LinearLayout>
</LinearLayout>

SimpleDraweeView的xml属性:

1.强制性的宽高

你必须声明 android:layout_width 和 android:layout_height。如果没有在XML中声明这两个属性,将无法正确加载图像。

wrap_content
Drawees 不支持 wrap_content 属性。

所下载的图像可能和占位图尺寸不一致,如果设置出错图或者重试图的话,这些图的尺寸也可能和所下载的图尺寸不一致。

如果大小不一致,假设使用的是 wrap_content,图像下载完之后,View将会重新layout,改变大小和位置。这将会导致界面跳跃。

固定宽高比
只有希望显示固定的宽高比时,可以使用wrap_content。

2.如果希望图片以特定的宽高比例显示,例如 4:3,可以在XML中指定fresco:viewAspectRatio属性

1
2
3
4
5
6
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/my_image_view"
android:layout_width="20dp"
android:layout_height="wrap_content"
fresco:viewAspectRatio="1.33"
<!-- other attributes -->

也可以在代码中指定显示比例:

mSimpleDraweeView.setAspectRatio(1.33f);
项目中设置的1:1也就是说宽高都是120dp

3.fresco:actualImageScaleType表求缩放类型,其值如下所示:
center 居中,无缩放。
centerCrop 保持宽高比缩小或放大,使得两边都大于或等于显示边界,且宽或高契合显示边界。居中显示。
focusCrop 同centerCrop, 但居中点不是中点,而是指定的某个点。
centerInside 缩放图片使两边都在显示边界内,居中显示。和 fitCenter 不同,不会对图片进行放大。
如果图尺寸大于显示边界,则保持长宽比缩小图片。
fitCenter 保持宽高比,缩小或者放大,使得图片完全显示在显示边界内,且宽或高契合显示边界。居中显示。
fitStart 同上。但不居中,和显示边界左上对齐。
fitEnd 同fitCenter, 但不居中,和显示边界右下对齐。
fitXY 不保存宽高比,填充满显示边界。
none 如要使用tile mode显示, 需要设置为none

4.圆角
圆角实际有2种呈现方式:

圆圈 - 设置roundAsCircle为true
圆角 - 设置roundedCornerRadius
设置圆角时,支持4个角不同的半径。XML中无法配置,但可在Java代码中配置。

设置圆角
可使用以下两种方式:

默认使用一个 shader 绘制圆角,但是仅仅占位图和所要显示的图有圆角效果。失败示意图和重下载示意图无圆角效果,且这种圆角方式不支持动画。
叠加一个solid color来绘制圆角。但是背景需要固定成指定的颜色。 在XML中指定 roundWithOverlayColor, 或者通过调用setOverlayColor来完成此设定。

5.其他
fresco:placeholderImage=”@drawable/ic_launcher” 表示点位图片
fresco:failureImage=”@drawable/ic_launcher”表示失败后显示的图片
fresco:roundingBorderWidth=”1dp” 表示图片圆的边线宽度
fresco:roundingBorderColor=”#00ffff”表示圆线的颜色

主MainActivity的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package com.shen.frescotest;
import java.util.ArrayList;
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.SimpleDraweeView;
public class MainActivity extends Activity {
//设置图片请求的基础地址
private static final String BASE_URL = "http://img1.3lian.com/img2011/w1/106/85/";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Fresco.initialize(this);
setContentView(R.layout.activity_main);
//创建一组数据用来填充ListView时在界面显示数据
ArrayList<Product> dishList = new ArrayList<Product>();
dishList.add(new Product(BASE_URL + "42.jpg", "水煮鱼片", "38.00"));
dishList.add(new Product(BASE_URL + "34.jpg", "小炒肉", "18.00"));
dishList.add(new Product(BASE_URL + "37.jpg", "清炒时蔬", "15.00"));
dishList.add(new Product(BASE_URL + "11.jpg", "金牌烤鸭", "36.00"));
dishList.add(new Product(BASE_URL + "12.jpg", "粉丝肉煲", "20.00"));
dishList.add(new Product(BASE_URL + "42.jpg", "水煮鱼片", "38.00"));
dishList.add(new Product(BASE_URL + "34.jpg", "小炒肉", "18.00"));
dishList.add(new Product(BASE_URL + "37.jpg", "清炒时蔬", "15.00"));
dishList.add(new Product(BASE_URL + "11.jpg", "金牌烤鸭", "36.00"));
dishList.add(new Product(BASE_URL + "12.jpg", "粉丝肉煲", "20.00"));
//获取ListView组件并设置数据适配器
ListView mListView = (ListView) this.findViewById(R.id.listview);
ProductListViewAdapter adapter = new ProductListViewAdapter(dishList);
mListView.setAdapter(adapter);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
// ListView适配器
private class ProductListViewAdapter extends BaseAdapter {
private ArrayList<Product> dataList;
public ProductListViewAdapter(ArrayList<Product> list) {
this.dataList = list;
}
@Override
public int getCount() {
return dataList.size();
}
@Override
public Object getItem(int position) {
return dataList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ListViewItemHolder item = null;
if (convertView == null) {
convertView = LayoutInflater.from(MainActivity.this).inflate(
R.layout.main_listview_item, null);
item = new ListViewItemHolder();
item.img_iv = (SimpleDraweeView) convertView
.findViewById(R.id.my_image_view);
item.name_textview = (TextView) convertView
.findViewById(R.id.textView1);
item.price_textview = (TextView) convertView
.findViewById(R.id.textView2);
convertView.setTag(item);
} else {
item = (ListViewItemHolder) convertView.getTag();
}
Product product = dataList.get(position);
Uri uri = Uri.parse(product.getImgUrl());
// SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view);
// draweeView.setImageURI(uri);
item.img_iv.setImageURI(uri);
DraweeController draweeController = Fresco.newDraweeControllerBuilder().setUri(product.getImgUrl()).build();
// item.img_iv.setController(new DraweeController(){
//
//
//
// });
item.name_textview.setText(product.getName());
item.price_textview.setText(product.getPrice() + "元");
return convertView;
}
}
// ListView的Item组件类
private class ListViewItemHolder {
SimpleDraweeView img_iv;
TextView name_textview;
TextView price_textview;
}
}

源代码

扩展阅读:

http://frescolib.org/

http://www.fresco-cn.org/

https://github.com/facebook/fresco

http://www.codeceo.com/article/android-fresco-usage.html

Fresco 使用实例

Fresco解决的问题

在Android设备上面,快速高效的显示图片是极为重要的。过去的几年里,我们在如何高效的存储图像这方面遇到了很多问题。图片太大,但是手机的 内存却很小。每一个像素的R、G、B和alpha通道总共要占用4byte的空间。如果手机的屏幕是480*800,那么一张屏幕大小的图片就要占用 1.5M的内存。手机的内存通常很小,特别是Android设备还要给各个应用分配内存。在某些设备上,分给Facebook App的内存仅仅有16MB。一张图片就要占据其内存的十分之一。

当你的App内存溢出会发生什么呢?它当然会崩溃!Fresco就是用来解决这个问题。它可以管理使用到的图片和内存,从此App不再崩溃。

Fresco内存区

1.Android中每个App的 Java堆内存大小都是被严格的限制的。每个对象都是使用Java的new在堆内存实例化

Fresco是由Facebook开发的,为了理解Facebook到底做了什么工作,在此之前我们需要了解在Android可以使用的堆内存之间的区别。
Android中每个App的 Java堆内存大小都是被严格的限制的。每个对象都是使用Java的new在堆内存实例化,这是内存中相对安全的一块区域。内存有垃圾回收机制,所以当 App不在使用内存的时候,系统就会自动把这块内存回收。

不幸的是,内存进行垃圾回收的过程正是问题所在。当内存进行垃圾回收时,内存不仅仅进行了垃圾回收,还把 Android 应用完全终止了。这也是用户在使用 App 时最常见的卡顿或短暂假死的原因之一。这会让正在使用 App 的用户非常郁闷,然后他们可能会焦躁地滑动屏幕或者点击按钮,但 App 唯一的响应就是:在 App 恢复正常之前,请求用户耐心等待

2.Native堆是由C++程序的new进行分配的

相比之下,Native堆是由C++程序的new进行分配的。在Native堆里面有更多可用内存,App只被设备的物理可用内存限制,而且没有垃圾回收机制或其他东西拖后腿。但是c++程序员必须自己回收所分配的每一块内存,否则就会造成内存泄露,最终导致程序崩溃。

3.Android有另外一种内存区域,叫做Ashmem

Android有另外一种内存区域,叫做Ashmem,它操作起来更像Native堆,但是也有额外的系统调用。Android 在操作 Ashmem 堆时,会把该堆中存有数据的内存区域从 Ashmem 堆中抽取出来,而不是把它释放掉,这是一种弱内存释放模式;被抽取出来的这部分内存只有当系统真正需要更多的内存时(系统内存不够用)才会被释放。当 Android 把被抽取出来的这部分内存放回 Ashmem 堆,只要被抽取的内存空间没有被释放,之前的数据就会恢复到相应的位置。

4.可消除的Bitmap

Ashmem不能被Java应用直接处理,但是也有一些例外,图片就是其中之一。当你创建一张没有经过压缩的Bitmap的时候,Android的API允许你指定是否是可清除的。

1
2
3
BitmapFactory.Options = new BitmapFactory.Options();
options.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);

经过上面的代码处理后,### 可清除的Bitmap会驻留在 Ashmem 堆中。
不管发生什么,垃圾回收器都不会自动回收这些 Bitmap。当 Android 绘制系统在渲染这些图片,Android 的系统库就会把这些 Bitmap 从 Ashmem 堆中抽取出来,而当渲染结束后,这些 Bitmap 又会被放回到原来的位置。如果一个被抽取的图片需要再绘制一次,系统仅仅需要把它再解码一次,这个操作非常迅速。

这听起来像一个完美的解决方案,但是问题是Bitmap解码的操作是运行在UI线程的。Bitmap解码是非常消耗CPU资源的,当消耗过大时会引起UI阻塞。因为这个原因,所以Google不推荐使用这个特性。 现在它们推荐使用另外一个特性——inBitmap。但是这个特性直到Android3.0之后才被支持。即使是这样,这个特性也不是非常有用,除非 App 里的所有图片大小都相同,这对Fackbook来说显然是不适用的。一直到4.4版本,这个限制才被移除了。但我们需要的是能够运行在 Android 2.3-最新版本中的通用解决方案。

自力更生

提到的“解码操作致使 UI 假死”的问题,我们找到了一种同时使 UI 显示和内存管理都表现良好的解决方法。如果我们在 UI 线程进行渲染之前把被抽取的内存区域放回到原来的位置,并确保它再也不会被抽取,那我们就可以把这些图片放在 Ashmem 里,同时不会出现 UI 假死的问题。幸运的是,Android 的 NDK 中有一个函数可以完美地实现这个需求,名字叫做 AndroidBitmap_lockPixels。这个函数最初的目的就是:在调用 unlockPixels 再次抽取内存区域后被执行。
当我们意识到我们没有必要这样做的时候,我们取得了突破。如果我们只调用lockPixels而不调用对应的unlockPixels,那么我们就 可以在Java的堆内存里面创建一个内存安全的图像,并且不会导致UI线程加载缓慢。只需要几行c++代码,我们就完美的解决了这个问题。

用C++的思想写Java代码就像《蜘蛛侠》里面说的:“能力越强,责任越大。”可清除的 Bitmap 既不会被垃圾回收器回收,也不会被 Ashmem 内置的清除机制处理,这使得使用它们可能会造成内存泄露。所以我们只能靠自己啦。
在c++中,通常的解决方案是建立智能指针类,实现引用计数。这些需要利用到c++的语言特性——拷贝构造函数、赋值操作符和确定的析构函数。这种语法在Java之中不存在,因为垃圾回收器能够处理这一切。所以我们必须以某种方式在Java中实现C++的这些保证机制。
我们创建了两个类去完成这件事。其中一个叫做“SharedReference”,它有addReference和deleteReference 两个方法,调用者调用时必须采取基类对象或让它在范围之外。一旦引用计数器归零,资源处理(Bitmap.recycle)就会发生。
然而,很显然,让Java开发者去调用这些方法是很容易出错的。Java语言就是为了避免做这样的事情的!所以SharedReference之 上,我们构建了CloseableReference类。它不仅实现了Java的Closeable接口,而且也实现了Cloneable接口。它的构造 器和clone()方法会调用addReference(),而close()方法会调用deleteReference()。所以Java开发者需要遵 守下面两条简单的的规则:

  1. 在分配CloseableReference新对象的时候,调用.clone()。

  2. 在超出作用域范围的时候,调用.close(),这通常是在finally代码块中。

这些规则可以有效地防止内存泄漏,并让我们在像Fackbook的Android客户端这种大型的Java程序中享受Native内存管理和通信。

不仅仅是加载程序,它是一个管道

在移动设备上显示图片需要很多的步骤:
image cache

几个优秀的开源库都是按照这个顺序执行的,比如 Picasso,Universal Image Loader,Glide和 Volley等等。上面这些开源库为Android的发展做出了非常重要的贡献。我们相信Fresco在几个重要方面会表现的更好。
我们的不同之处在于把上面的这些步骤看作是管道,而不仅仅是加载器。每一个步骤和其他方面应该是尽可能独立的,把数据和参数传递进去,然后产生一个 输出,就这么简单。它应该可以做一些操作,不管是并行还是串行。一些操作只能在特性条件下才能执行。一些有特殊要求的在线程上执行。除此之外,当我们考虑 改进图像的时候,所有的图片就会变得非常复杂。很多人在低网速情况下使用Facebook,我们想要这些人能够尽快的看到图片,甚至经常是在图片没有完全 下载完之前。

不要烦恼,拥抱stream

在Java中,异步代码历来都是通过Future机制来执行的。在另外的线程里面代码被提交执行,然后一个类似Future的对象可以检查执行的结 果是不是已经完成了。但是,这只在假设只有一种结果的情况下行得通。在处理渐进的图像的时候,我们希望可以完整而且连续的显示结果。
我们的解决方式是定义一个更广义的Future版本,叫做DataSource。它提供了一个订阅方法,调用者必须传入一个 DataSubscriber和Executor。DataSubscriber可以从DataSource获取到处理中和处理完毕的结果,并且提供了很 简单的方法来区分。因为我们需要非常频繁的处理这些对象,所以必须有一个明确的close调用,幸运的是,DataSource本身就是 Closeable。
在后台,每一个箱子上面都实现了一个叫做“生产者/消费者”的新框架。在这个问题是,我们是从ReactiveX获取的灵感。我们的系统拥有和RxJava相似的接口,但是更加适合移动设备,并且有内置的对Closeables的支持。
保持简单的接口。

动画全覆盖

使用Facebook的人都非常喜欢Stickers,因为它可以以动画形式存储GIF和Web格式。如果支持这些格式,就需要面临新的挑战。因为 每一个动画都是由不止一张图片组成的,你需要解码每一张图片,存储在内存里,然后显示出来。对于大一点的动画,把每一帧图片放在内存是不可行的。
我们建立了AnimatedDrawable,一个强大的可以呈现动画的Drawable,同时支持GIF和WebP格式。 AnimatedDrawable实现标准的Android Animatable接口,所以调用者可以随意的启动或者停止动画。为了优化内存使用,如果图片足够小的时候,我们就在内存里面缓存这些图片,但是如果太 大,我们可以迅速的解码这些图片。这些行为调用者是完全可控的。
所有的后台都用c++代码实现。我们保持一份解码数据和元数据解析,如宽度和高度。我们引用技术数据,它允许多个Java端的Drawables同时访问一个WebP图像。

如何去爱你?

我来告诉你…当一张图片从网络上下载下来之后,我们想显示一张占位图。如果下载失败了,我们就会显示一个错误标志。当图片加载完之后,我们有一个渐变动画。通过 使用硬件加速,我们可以按比例放缩,或者是矩阵变换成我们想要的大小然后渲染。我们不总是按照图片的中心进行放缩,那么我们可以自己定义放缩的聚焦点。有 些时候,我们想显示圆角甚至是圆形的图片。所有的这些操作都应该是迅速而平滑的。
我们之前的实现是使用Android的View对象——时机到了,可以使用ImageView替换出占位的View。这个操作是非常慢的。改变 View会让Android强制刷新整个布局,当用户滑动的时候,这绝对不是你想看到的效果。比较明智的做法是使用Android的Drawables, 它可以迅速的被替换。
所以我们创建了Drawee。这是一个像MVC架构的图片显示框架。该模型被称为DraweeHierarchy。它被实现为Drawables的一个层,对于底层的图像而言,每一个曾都有特定的功能——成像、层叠、渐变或者是放缩。
DraweeControllers通过管道的方式连接到图像上——或者是其他的图片加载库——并且处理后台的图片操作。他们从管道接收事件并决定如何处理他们。他们控制DraweeHierarchy实际上的操作——无论是占位图片,错误条件或是完成的图片。
DraweeViews 的功能不多,但都是至关重要的。他们监听Android的View不再显示在屏幕上的系统事件。当图片离开屏幕的时候,DraweeView可以告诉 DraweeController关闭使用的图像资源。这可以避免内存泄露。此外,如果它已经不在屏幕范围内的话,控制器会告诉图片管道取消网络请求。因 此,像Fackbook那样滚动一长串的图片的时候,不会频繁的网络请求。
通过这些努力,显示图片的辛苦操作一去不复返了。调用代码只需要实例化一个DraweeView,然后指定一个URI和其他可选的参数就可以了。剩下的一切都会自动完成。开发人员不需要担心管理图像内存,或更新图像流。Fresco为他们把一切都做了。

Picasso使用实例

Picasso使用实例

实现的例子效果图如下:

picasso
图片右上角红色三角表示这张图片是从网络上下载显示出来的,绿色三角表示是从内存缓存中加载的图片

picasso test
这张效果图中的黄色三角表示图片是从本地缓存中读取出来的

这个例子的工程结构图如下:

picasso struct

主布局文件

1
2
3
4
5
6
7
8
9
10
11
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="${relativePackage}.${activityClass}" >
<ListView
android:id="@+id/listview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:dividerHeight="1dip" />
</RelativeLayout>

这个布局文件就是全屏显示一个列表ListView视图控件,没有什么要说的!

ListView中Item布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_vertical"
android:padding="5dip" >
<ImageView
android:id="@+id/imageView1"
android:layout_width="60dip"
android:layout_height="60dip"
android:src="@drawable/ic_launcher" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginLeft="15dip"
>
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="20sp"
android:layout_marginTop="5dip"/>
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:textSize="14sp"
android:layout_marginTop="2dip"/>
</LinearLayout>
</LinearLayout>

这个布局文件是用来设置ListView中每项需要显示的控件,左边ImageView显示一张图片,右边坚排两个TextView来显示两段文字!

MainActivity和Listview适配器代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package com.shen.picasso;
import java.util.ArrayList;
import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import com.squareup.picasso.Picasso;
public class MainActivity extends Activity {
//设置图片请求的基础地址
private static final String BASE_URL = "http://img1.3lian.com/img2011/w1/106/85/";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//创建一组数据用来填充ListView时在界面显示数据
ArrayList<Product> dishList = new ArrayList<Product>();
dishList.add(new Product(BASE_URL + "42.jpg", "水煮鱼片", "38.00"));
dishList.add(new Product(BASE_URL + "34.jpg", "小炒肉", "18.00"));
dishList.add(new Product(BASE_URL + "37.jpg", "清炒时蔬", "15.00"));
dishList.add(new Product(BASE_URL + "11.jpg", "金牌烤鸭", "36.00"));
dishList.add(new Product(BASE_URL + "12.jpg", "粉丝肉煲", "20.00"));
dishList.add(new Product(BASE_URL + "42.jpg", "水煮鱼片", "38.00"));
dishList.add(new Product(BASE_URL + "34.jpg", "小炒肉", "18.00"));
dishList.add(new Product(BASE_URL + "37.jpg", "清炒时蔬", "15.00"));
dishList.add(new Product(BASE_URL + "11.jpg", "金牌烤鸭", "36.00"));
dishList.add(new Product(BASE_URL + "12.jpg", "粉丝肉煲", "20.00"));
dishList.add(new Product(BASE_URL + "42.jpg", "水煮鱼片", "38.00"));
//获取ListView组件并设置数据适配器
ListView mListView = (ListView) this.findViewById(R.id.listview);
ProductListViewAdapter adapter = new ProductListViewAdapter(dishList);
mListView.setAdapter(adapter);
}
// ListView适配器
private class ProductListViewAdapter extends BaseAdapter {
private ArrayList<Product> dataList;
public ProductListViewAdapter(ArrayList<Product> list) {
this.dataList = list;
}
@Override
public int getCount() {
return dataList.size();
}
@Override
public Object getItem(int position) {
return dataList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ListViewItemHolder item = null;
//加载ListView中每项的布局文件
if (convertView == null) {
convertView = LayoutInflater.from(MainActivity.this).inflate(
R.layout.product_listview_item, null);
item = new ListViewItemHolder();
item.imgIv = (ImageView) convertView
.findViewById(R.id.imageView1);
item.nameTv = (TextView) convertView
.findViewById(R.id.textView1);
item.priceTv = (TextView) convertView
.findViewById(R.id.textView2);
convertView.setTag(item);
} else {
item = (ListViewItemHolder) convertView.getTag();
}
Product product = dataList.get(position);
//设置是否在图片右上角显示三角形来说明图片来源(网络,本地,内存)
Picasso.with(MainActivity.this).setIndicatorsEnabled(true);
//这里就是异步加载网络图片的地方
Picasso.with(MainActivity.this).load(product.getImgUrl()).transform(new CircleTransform())
.into(item.imgIv);
item.nameTv.setText(product.getName());
item.priceTv.setText(product.getPrice() + "元");
return convertView;
}
}
// ListView的Item组件类
private class ListViewItemHolder {
ImageView imgIv;
TextView nameTv;
TextView priceTv;
}
}

上面代码过长,我就不一一说明了,并且代码中关键点我也写了注释,想了解的可以看一下,在这里我重点说一下源码中 Picasso.with(MainActivity.this).setIndicatorsEnabled(true);这句代码,这句代码是用来在图片右上角来显示标志一个三角标志的,有人会问这个标志有什么用途了?这个标志充分体验上Google程序员高大上的地方(代码给使用者良好的体验),这里的标志用来说明图片是从网络,本地,或是内存中加载出来的,形象的来说明图片的缓存机制!红色三角表示从网络上下载显示的,黄色三角表求图片是从本地文件硬盘读取的,若三色为绿色表求图片是直接从内存缓存中加载的,这也更加形象的说明了Picasso的三级图片缓存机制!

注:这个三角标志可以从上面的效果图片中清晰的看出来

细心的读者可能发现上面Picasso.with(MainActivity.this).load(product.getImgUrl()).transform(new CircleTransform()).into(item.imgIv);代码调用了transform(new CircleTransform())这个方法,这个在这里起什么作用了?Picasso这个库支持进行图片变换,这里的作用就是把图片变换成圆形显示!

进行图片变换为圆形的转换器代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.shen.picasso;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import com.squareup.picasso.Transformation;
public class CircleTransform implements Transformation {
@Override
public Bitmap transform(Bitmap source) {
int size = Math.min(source.getWidth(), source.getHeight());
int x = (source.getWidth() - size) / 2;
int y = (source.getHeight() - size) / 2;
Bitmap squaredBitmap = Bitmap.createBitmap(source, x, y, size, size);
if (squaredBitmap != source) {
source.recycle();
}
Bitmap bitmap = Bitmap.createBitmap(size, size, source.getConfig());
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
BitmapShader shader = new BitmapShader(squaredBitmap,
BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
paint.setShader(shader);
paint.setAntiAlias(true);
float r = size / 2f;
canvas.drawCircle(r, r, r, paint);
squaredBitmap.recycle();
return bitmap;
}
@Override
public String key() {
return "circle";
}
}

Listview中填充的数据对象类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.shen.picasso;
//表示菜类(经过烹调的蔬菜、蛋品、肉类等)
public class Product {
private String imgUrl; // 图片地址
private String name; // 菜名
private String price; // 菜价
public Product(String imgUrl, String name, String price) {
this.imgUrl = imgUrl;
this.name = name;
this.price = price;
}
public String getImgUrl() {
return imgUrl;
}
public void setImgUrl(String imgUrl) {
this.imgUrl = imgUrl;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPrice() {
return price;
}
public void setPrice(String price) {
this.price = price;
}
}

picasso是Square公司开源的一个Android图形缓存库,地址,可以实现图片下载和缓存功能。仅仅只需要一行代码就能完全实现图片的异步加载!

如果项目中使用了OkHttp库的话,默认会使用OkHttp来下载图片。否则使用HttpUrlConnection来下载图片。

1
2
3
4
5
6
7
8
static Downloader createDefaultDownloader(Context context) {
try {
Class.forName("com.squareup.okhttp.OkHttpClient");
return OkHttpLoaderCreator.create(context);
} catch (ClassNotFoundException ignored) {
}
return new UrlConnectionDownloader(context);
}

Picasso的一些基本策略缓存策略MemoryCache+DiskCache+Net
1.MemoryCache采用的是Lru策略,持有一定数量处理过的图(譬如经过resize/rotate处理,可直接设置到view中)。
2.DiskCache是网络图片在本地的缓存,缓存的是原图,可能需要经过处理才能设置到view中。
3.Net是图片服务器,当MemoryCache和DiskCache均取不到图片时,网络拉取,成本最高。

Picasso最大缓存为50MB,最小的缓存为:5MB

1
2
private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB
private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB

Picasso进行缓存:

1
2
3
4
5
6
7
8
try {
File httpCacheDir = new File(context.getCacheDir(), "http");
long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
Class.forName("android.net.http.HttpResponseCache")
.getMethod("install", File.class, long.class)
.invoke(null, httpCacheDir, httpCacheSize);
} catch (Exception httpResponseCacheNotAvailable) {
}}

源码下载地址:

PicassoTest 源码

官方地址
Picasso Api

缓存机制

Android 客户端缓存机制

Android客户端的缓存机制能很好的提高客户端的显示速度,节省不必要的流量请求,为用户提供良好的体验等优点。虽然Android缓存有上述所述的好处,但并不是所有的网络请求,所有的业务逻辑都适合使用缓存机制!如,电商中常有的秒杀功能要求数据实时性特别强,几毫秒的缓存延迟都会严重影响数据的同步,给用户错误的库存及倒计时的体验!那什么情况下适合运用缓存机制了,其实也好理解,就是对于数据实时性要求不高,对数据买保鲜性要求较低的业务请求,我们可以考虑使用缓存机制来进行数据缓存,充分利用缓存带给我们的好处!

下面争对我开发的电商Android客户端的业务,来说明一下请求是否适合使用缓存:

业务功能 是否适用缓存 缓存内容 说明
轮播图 Json数据 数据不会经常改动
快速入口 Json数据 数据及跳转的地址不会经常变动
秒杀 时实性较强,需要与后台数据保持高度的实时性
专题 Json数据 实时性要求不高
行业精选 Json数据 实时性要求不高
大数据推荐 实时高,根据用户的浏览历史推出不同的商品
普通商品详情 现有基础上不能缓存,若是能把请求进行细分,可以考虑部分缓存
秒杀商品详情 实时性较强,需要与后台数据保持高度的实时性
显示广告 Json数据 这个修改不会过于频繁,若要修改频繁可以设置一个较短的失效时间

##缓存方案##

###缓存方案的流程图如下所示:###
cache uml

###缓存方案实现简单介绍:###
(1)在手机内存中进行缓存,初步设想为Map结构,缓存是传入关键字(key)和要缓存的数据(例如为Object),使用缓存时直接根据关键字(key)来获取缓存中的数据(Object),再根据Object中的属性更新界面显示。

(2)若在(1)中取出的Object为空,说明内存中没有缓存key所对应的数据对象,我可以从二级缓存数据库中进行读取,并把缓存更新内存缓存中。

(3)若在(2)中也没有读取到Key所对应的数据,则发送网络请求从后台服务器端获取数据,更新界面及手机内存缓存和数据库缓存,为下次使用缓存提供数据支撑!