您的位置:首页 > 其它

详谈Picasso图片缓存库特点及用法

2017-07-30 21:25 190 查看
本文采用研究生论文格式编写,方便大家阅读。作者:谭东

摘要

Picasso是美国SQUARE移动支付公司开源的图片缓存加载库。可以实现图片下载和缓存功能,效率和性能都很不错。



Square公司官方博客:http://square.github.io/

Square公司Github地址:https://github.com/square

Square公司Picasso的Wiki地址:http://square.github.io/picasso/

Square公司Picasso的Github地址:https://github.com/square/picasso

1 绪论
Picasso的用法是链式调用,和Glide用法很像。当然也支持个性化设置,是目前比较流行的图片缓存库之一。

1.1 Picasso特点

Picasso是全尺寸下载图片,也就是把要加载的图片原图下载缓存下来。Picasso默认的缓存格式为ARGB_888,相对于RGB_565内存占用高了一半左右,当然图片质量显示要高一些。下面看下Picasso的主要特点:

①链式调用,使用简单;

②具有一般图片框架的基础功能;

③方便的图片转换;

④加载过程监听和错误处理;

⑤自动添加磁盘和内存二级缓存;

⑥支持多种数据源加载。

Picasso默认不支持Gif图片加载。Picasso库很小,类也很少,库仅118KB大小。最新版目前是2.5.2。

2 Picasso基本用法

2.1 引入Picasso

Jar包下载:https://search.maven.org/remote_content?g=com.squareup.picasso&a=picasso&v=LATEST

Gradle引用:

[java] view
plain copy

compile 'com.squareup.picasso:picasso:2.5.2'

Maven引用:

[java] view
plain copy

<dependency>

<groupId>com.squareup.picasso</groupId>

<artifactId>picasso</artifactId>

<version>2.5.2</version>

</dependency>

2.2 基本加载图片





图2.1 Picasso加载图片

链式调用,逻辑上比较清晰。

[java] view
plain copy

Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").into(imageView);

改变图片尺寸和填充方式:

[java] view
plain copy

Picasso.with(context)

.load(url)

.resize(50, 50)

.centerCrop()

.into(imageView)

图片个性化转换,例如把图片显示加载成正方形,那么你就要自己写具体逻辑。继承Transformation,很简单,只需要重新按照你的要求去绘制和创建个Bitmap即可:

[java] view
plain copy

public class CropSquareTransformation 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 result = Bitmap.createBitmap(source, x, y, size, size);

if (result != source) {

source.recycle();

}

return result;

}

@Override public String key() { return "square()"; }

}

加入加载中图片和加载出错图片:

[java] view
plain copy

Picasso.with(context)

.load(url)

.placeholder(R.drawable.user_placeholder)

.error(R.drawable.user_placeholder_error)

.into(imageView);

针对不同来源的图片加载,如文件、网络图片、Assets目录、Resource、Content Provider等:

[java] view
plain copy

Picasso.with(context).load(R.drawable.landing_screen).into(imageView1);

Picasso.with(context).load("file:///android_asset/DvpvklR.png").into(imageView2);

Picasso.with(context).load(new File(...)).into(imageView3);

图片的Log日志模式和显示图片的缓存来源的标识设置:

[java] view
plain copy

Picasso picasso = Picasso.with(this);

picasso.setIndicatorsEnabled(true);//蓝色:从本地缓存读取的图片,红色:从网络加载的图片,绿色:从内存缓存加载的图片

picasso.setLoggingEnabled(true);//日志调试模式



图2.2 Picasso加载图片来源标识

图片缓存颜色格式配置:

[java] view
plain copy

Picasso.with(context).load("http://square.github.io/picasso/static/sample.png").config(Bitmap.Config.RGB_565)

.into(imageView);

ARGB_8888要比RGB_565占用内存大一半左右,但图像质量差距不大一般。

3 Picasso高级用法
下面的代码是实现:

1.自定义Picasso全局线程池

2.自定义Picasso默认缓存目录

3.自定义监听Picasso加载图片进度,结合OKHTTP

[java] view
plain copy

package com.tandong.picassodemo;

import android.app.Application;

import com.tandong.picassodemo.utils.OkHttp3Downloader;

import com.tandong.picassodemo.utils.ProgressListener;

import com.squareup.picasso.Picasso;

import java.io.IOException;

import java.util.concurrent.PriorityBlockingQueue;

import java.util.concurrent.ThreadPoolExecutor;

import java.util.concurrent.TimeUnit;

import okhttp3.Cache;

import okhttp3.Interceptor;

import okhttp3.OkHttpClient;

import okhttp3.Response;

/**

* Created by Administrator on 2017/4/12.

*/

/**

* 1.自定义Picasso全局线程池

* 2.自定义Picasso默认缓存目录

* 3.自定义监听Picasso加载图片进度,结合OKHTTP

*/

public class BaseApplication extends Application {

private ThreadPoolExecutor threadPoolExecutor;

private int cpu = 0;

private Picasso picasso;

private OkHttpClient okHttpClient;

public static ProgressListener progress;

@Override

public void onCreate() {

super.onCreate();

initPicasso();

}

private void initPicasso() {

okHttpClient = new OkHttpClient.Builder().addNetworkInterceptor(new Interceptor() {

@Override

public Response intercept(Chain chain) throws IOException {

Response response = chain.proceed(chain.request());

return response.newBuilder().body(new ProgressResponseBody(progress, response.body())).build();

}

}).cache(new Cache(getExternalCacheDir(), 10 * 1024 * 1024)).build();

cpu = Runtime.getRuntime().availableProcessors();

threadPoolExecutor = new ThreadPoolExecutor(cpu + 1, cpu * 2 + 1, 1, TimeUnit.MINUTES, new PriorityBlockingQueue<Runnable>());

picasso = new Picasso.Builder(this).executor(threadPoolExecutor).downloader(new OkHttp3Downloader(okHttpClient)).build();//默认3个线程

Picasso.setSingletonInstance(picasso);

}

public static void setProgressListener(ProgressListener progressListener) {

progress = progressListener;

}

}

build.gradle引用:

[java] view
plain copy

compile 'com.squareup.picasso:picasso:2.5.2'

compile 'com.squareup.okhttp3:okhttp:3.6.0'

Okhttp3下载器Okhttp3Downloader:

[java] view
plain copy

package com.tandong.picassodemo.utils;

import android.content.Context;

import android.net.Uri;

import android.os.StatFs;

import com.squareup.picasso.Downloader;

import com.squareup.picasso.NetworkPolicy;

import java.io.File;

import java.io.IOException;

import okhttp3.Cache;

import okhttp3.CacheControl;

import okhttp3.Call;

import okhttp3.OkHttpClient;

import okhttp3.Request;

import okhttp3.ResponseBody;

/**

* Created by office on 2017/4/10.

*/

public final class OkHttp3Downloader implements Downloader {

private static final String PICASSO_CACHE = "picasso-cache";

private static final int MIN_DISK_CACHE_SIZE = 5 * 1024 * 1024; // 5MB

private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB

private static File defaultCacheDir(Context context) {

File cache = new File(context.getApplicationContext().getCacheDir(), PICASSO_CACHE);

if (!cache.exists()) {

//noinspection ResultOfMethodCallIgnored

cache.mkdirs();

}

return cache;

}

private static long calculateDiskCacheSize(File dir) {

long size = MIN_DISK_CACHE_SIZE;

try {

StatFs statFs = new StatFs(dir.getAbsolutePath());

long available = ((long) statFs.getBlockCount()) * statFs.getBlockSize();

// Target 2% of the total space.

size = available / 50;

} catch (IllegalArgumentException ignored) {

}

// Bound inside min/max size for disk cache.

return Math.max(Math.min(size, MAX_DISK_CACHE_SIZE), MIN_DISK_CACHE_SIZE);

}

/**

* Creates a {@link Cache} that would have otherwise been created by calling

* {@link #OkHttp3Downloader(Context)}. This allows you to build your own {@link OkHttpClient}

* while still getting the default disk cache.

*/

public static Cache createDefaultCache(Context context) {

File dir = defaultCacheDir(context);

return new Cache(dir, calculateDiskCacheSize(dir));

}

private static OkHttpClient createOkHttpClient(File cacheDir, long maxSize) {

return new OkHttpClient.Builder()

.cache(new Cache(cacheDir, maxSize))

.build();

}

private final Call.Factory client;

private final Cache cache;

/**

* Create new downloader that uses OkHttp. This will install an image cache into your application

* cache directory.

*/

public OkHttp3Downloader(Context context) {

this(defaultCacheDir(context));

}

/**

* Create new downloader that uses OkHttp. This will install an image cache into the specified

* directory.

*

* @param cacheDir The directory in which the cache should be stored

*/

public OkHttp3Downloader(File cacheDir) {

this(cacheDir, calculateDiskCacheSize(cacheDir));

}

/**

* Create new downloader that uses OkHttp. This will install an image cache into your application

* cache directory.

*

* @param maxSize The size limit for the cache.

*/

public OkHttp3Downloader(final Context context, final long maxSize) {

this(defaultCacheDir(context), maxSize);

}

/**

* Create new downloader that uses OkHttp. This will install an image cache into the specified

* directory.

*

* @param cacheDir The directory in which the cache should be stored

* @param maxSize The size limit for the cache.

*/

public OkHttp3Downloader(File cacheDir, long maxSize) {

this(createOkHttpClient(cacheDir, maxSize));

}

public OkHttp3Downloader(OkHttpClient client) {

this.client = client;

this.cache = client.cache();

}

public OkHttp3Downloader(Call.Factory client) {

this.client = client;

this.cache = null;

}

@Override

public Response load(Uri uri, int networkPolicy) throws IOException {

CacheControl cacheControl = null;

if (networkPolicy != 0) {

if (NetworkPolicy.isOfflineOnly(networkPolicy)) {

cacheControl = CacheControl.FORCE_CACHE;

} else {

CacheControl.Builder builder = new CacheControl.Builder();

if (!NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) {

builder.noCache();

}

if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {

builder.noStore();

}

cacheControl = builder.build();

}

}

Request.Builder builder = new Request.Builder().url(uri.toString());

if (cacheControl != null) {

builder.cacheControl(cacheControl);

}

okhttp3.Response response = client.newCall(builder.build()).execute();

int responseCode = response.code();

if (responseCode >= 300) {

response.body().close();

throw new ResponseException(responseCode + " " + response.message(), networkPolicy,

responseCode);

}

boolean fromCache = response.cacheResponse() != null;

ResponseBody responseBody = response.body();

return new Response(responseBody.byteStream(), fromCache, responseBody.contentLength());

}

@Override

public void shutdown() {

if (cache != null) {

try {

cache.close();

} catch (IOException ignored) {

}

}

}

}

官方地址:https://github.com/JakeWharton/picasso2-okhttp3-downloader/blob/master/src/main/java/com/jakewharton/picasso/OkHttp3Downloader.java

自定义ResponseBody,实现对数据下载进度的监听处理:

[java] view
plain copy

package com.tandong.picassodemo;

import android.util.Log;

import com.tandong.picassodemo.utils.ProgressListener;

import java.io.IOException;

import okhttp3.MediaType;

import okhttp3.ResponseBody;

import okio.Buffer;

import okio.BufferedSource;

import okio.ForwardingSource;

import okio.Okio;

import okio.Source;

/**

* Created by Administrator on 2017/4/14.

*/

public class ProgressResponseBody extends ResponseBody {

private ResponseBody responseBody;

private BufferedSource bufferedSource;

private ProgressListener progressListener;

public ProgressResponseBody(ProgressListener progressListener,ResponseBody responseBody) {

this.responseBody = responseBody;

this.progressListener=progressListener;

}

@Override

public MediaType contentType() {

return responseBody.contentType();

}

@Override

public long contentLength() {

return responseBody.contentLength();

}

@Override

public BufferedSource source() {

if (bufferedSource == null) {

bufferedSource = Okio.buffer(source(responseBody.source()));

}

return bufferedSource;

}

private Source source(Source source) {

ForwardingSource forwardingSource = new ForwardingSource(source) {

long totalBytesRead = 0L;

@Override

public long read(Buffer sink, long byteCount) throws IOException {

long bytesRead = super.read(sink, byteCount);

totalBytesRead += bytesRead != -1 ? bytesRead : 0;

progressListener.update((int) ((100 * totalBytesRead) / responseBody.contentLength()));

Log.i("info", "进度:" + (int) ((100 * totalBytesRead) / responseBody.contentLength()));

return bytesRead;

}

@Override

public void close() throws IOException {

super.close();

}

};

return forwardingSource;

}

}

进度接口:

[java] view
plain copy

package com.tandong.picassodemo.utils;

import android.support.annotation.IntRange;

/**

* Created by Administrator on 2017/4/14.

*/

public interface ProgressListener {

public void update(@IntRange(from = 0, to = 100) int progress);

}

自定义带进度的ImageView:

[java] view
plain copy

package com.tandong.picassodemo.view;

import android.content.Context;

import android.graphics.Canvas;

import android.graphics.Color;

import android.graphics.Paint;

import android.graphics.RectF;

import android.support.annotation.IntRange;

import android.support.v4.view.ViewCompat;

import android.util.AttributeSet;

import android.widget.ImageView;

/**

* Created by office on 2017/4/10.

*/

public class PicassoImageView extends ImageView {

private final int MAX_PROGRESS = 100;

private Paint mArcPaint;

private RectF mBound;

private Paint mCirclePaint;

private int mProgress = 0;

public PicassoImageView(Context context) {

this(context, null, 0);

}

public PicassoImageView(Context context, AttributeSet attrs) {

this(context, attrs, 0);

}

public PicassoImageView(Context context, AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

init();

}

private void init() {

mArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

mArcPaint.setStyle(Paint.Style.FILL_AND_STROKE);

mArcPaint.setStrokeWidth(dpToPixel(0.1f, getContext()));

mArcPaint.setColor(Color.argb(120, 0xff, 0xff, 0xff));

mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);

mCirclePaint.setStyle(Paint.Style.STROKE);

mCirclePaint.setStrokeWidth(dpToPixel(2, getContext()));

mCirclePaint.setColor(Color.argb(120, 0xff, 0xff, 0xff));

mBound = new RectF();

}

public void setProgress(@IntRange(from = 0, to = MAX_PROGRESS) int mProgress) {

this.mProgress = mProgress;

ViewCompat.postInvalidateOnAnimation(this);

}

@Override

protected void onSizeChanged(int w, int h, int oldw, int oldh) {

super.onSizeChanged(w, h, oldw, oldh);

int min = Math.min(w, h);

int max = w + h - min;

int r = min / 5;

//set up a square in the imageView

//设置了在图片中间的一个小圆

mBound.set(max / 2 - r, min / 2 - r, max / 2 + r, min / 2 + r);

}

@Override

protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

//画进度条, Paint是画笔,详见源码

if (mProgress != MAX_PROGRESS && mProgress != 0) {

float mAngle = mProgress * 360f / MAX_PROGRESS;

//画扇型

canvas.drawArc(mBound, 270, mAngle, true, mArcPaint);

//画圆

canvas.drawCircle(mBound.centerX(), mBound.centerY(), mBound.height() / 2, mCirclePaint);

}

}

private static float scale;

public static int dpToPixel(float dp, Context context) {

if (scale == 0) {

scale = context.getResources().getDisplayMetrics().density;

}

return (int) (dp * scale);

}

}

Picasso源码原理解析:

先看流程图:



Picasso的源码很小,类也很少,相对简单些。

我们主要关注以下标红的几个类。








不是太想写太多的文字,那就看源码分析吧。

先看构造单例模式的Piasso实例:

[java] view
plain copy

/**

* with方式

*/

Picasso.with(this).load("http://square.github.io/picasso/static/sample.png").centerCrop().into(imageView);

/**

* new with方式

*/

Picasso picasso = Picasso.with(this);

/**

* builder方式

*/

picasso = new Picasso.Builder(this).executor(threadPoolExecutor).downloader(new OkHttp3Downloader(okHttpClient)).build();//默认3个线程

Picasso.setSingletonInstance(picasso);

这几种方式通过查看,都是通过一个方式实现的构造,那就是builder方式。

我们按照这句最基本的方式进行源码分析。

[java] view
plain copy

Picasso.with(this).load("http://square.github.io/picasso/static/sample.png").centerCrop().into(imageView);

点击with方法进去。

[java] view
plain copy

public static Picasso with(Context context) {

if (singleton == null) {

synchronized (Picasso.class) {

if (singleton == null) {

singleton = new Builder(context).build();

}

}

}

return singleton;

}

/** Create the {@link Picasso} instance. */

public Picasso build() {

Context context = this.context;

//是否自定义下载器,无的话使用默认下载器

if (downloader == null) {

downloader = Utils.createDefaultDownloader(context);

}

//是否自定义缓存方式,无的话使用默认LruCache

if (cache == null) {

cache = new LruCache(context);

}

//是否自定义线程池,无的话使用默认线程池

if (service == null) {

service = new PicassoExecutorService();

}

//是否自定义转换器,无的话使用默认转换器

if (transformer == null) {

transformer = RequestTransformer.IDENTITY;

}

//统计器,统计缓存的命中率

Stats stats = new Stats(cache);

//创建Dispather,分发调度器

Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);

//创建Picasso实例

return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,

defaultBitmapConfig, indicatorsEnabled, loggingEnabled);

}

}

可以看出都是通过builder方式实例化的Picasso类。这里实例化了Dispatcher和Picasso。

接下来点击load方法进去:

[java] view
plain copy

/**

* Start an image request using the specified path. This is a convenience method for calling

* {@link #load(Uri)}.

* <p>

* This path may be a remote URL, file resource (prefixed with {@code file:}), content resource

* (prefixed with {@code content:}), or android resource (prefixed with {@code

* android.resource:}.

* <p>

* Passing {@code null} as a {@code path} will not trigger any request but will set a

* placeholder, if one is specified.

*

* @see #load(Uri)

* @see #load(File)

* @see #load(int)

* @throws IllegalArgumentException if {@code path} is empty or blank string.

*/

public RequestCreator load(String path) {

if (path == null) {

return new RequestCreator(this, null, 0);

}

if (path.trim().length() == 0) {

throw new IllegalArgumentException("Path must not be empty.");

}

return load(Uri.parse(path));

}

/**

* Start an image request using the specified URI.

* <p>

* Passing {@code null} as a {@code uri} will not trigger any request but will set a placeholder,

* if one is specified.

*

* @see #load(File)

* @see #load(String)

* @see #load(int)

*/

public RequestCreator load(Uri uri) {

return new RequestCreator(this, uri, 0);

}

RequestCreator(Picasso picasso, Uri uri, int resourceId) {

if (picasso.shutdown) {

throw new IllegalStateException(

"Picasso instance already shut down. Cannot submit new requests.");

}

this.picasso = picasso;

this.data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig);

}

可以看出,这里实例化了一个RequestCreator,我们看下这个源码类干嘛的。

[java] view
plain copy

/** Fluent API for building an image download request. */

@SuppressWarnings("UnusedDeclaration") // Public API.

public class RequestCreator {

private static final AtomicInteger nextId = new AtomicInteger();

private final Picasso picasso;

private final Request.Builder data;

private boolean noFade;

private boolean deferred;

private boolean setPlaceholder = true;

private int placeholderResId;

private int errorResId;

private int memoryPolicy;

private int networkPolicy;

private Drawable placeholderDrawable;

private Drawable errorDrawable;

private Object tag;

看下基本的声明,就是进行我们个性化配置存储的类。

我们接下来点击centerCrop方法进去:

[java] view
plain copy

/**

* Crops an image inside of the bounds specified by {@link #resize(int, int)} rather than

* distorting the aspect ratio. This cropping technique scales the image so that it fills the

* requested bounds and then crops the extra.

*/

public RequestCreator centerCrop() {

data.centerCrop();

return this;

}

这个配置也是操作我们的RequestCreator。

最后点击into方法。

[java] view
plain copy

/**

* Asynchronously fulfills the request into the specified {@link ImageView}.

* <p>

* <em>Note:</em> This method keeps a weak reference to the {@link ImageView} instance and will

* automatically support object recycling.

*/

public void into(ImageView target) {

into(target, null);

}

**

* Asynchronously fulfills the request into the specified {@link ImageView} and invokes the

* target {@link Callback} if it's not {@code null}.

* <p>

* <em>Note:</em> The {@link Callback} param is a strong reference and will prevent your

* {@link android.app.Activity} or {@link android.app.Fragment} from being garbage collected. If

* you use this method, it is <b>strongly</b> recommended you invoke an adjacent

* {@link Picasso#cancelRequest(android.widget.ImageView)} call to prevent temporary leaking.

*/

public void into(ImageView target, Callback callback) {

long started = System.nanoTime();

checkMain();

if (target == null) {

throw new IllegalArgumentException("Target must not be null.");

}

if (!data.hasImage()) {//清除取消存在请求

picasso.cancelRequest(target);

if (setPlaceholder) {

setPlaceholder(target, getPlaceholderDrawable());

}

return;

}

if (deferred) {

if (data.hasSize()) {

throw new IllegalStateException("Fit cannot be used with resize.");

}

int width = target.getWidth();

int height = target.getHeight();

if (width == 0 || height == 0) {

if (setPlaceholder) {

setPlaceholder(target, getPlaceholderDrawable());

}

picasso.defer(target, new DeferredRequestCreator(this, target, callback));

return;

}

data.resize(width, height);

}

Request request = createRequest(started);//时间戳为Key,创建Request

String requestKey = createKey(request);

if (shouldReadFromMemoryCache(memoryPolicy)) {//内存缓存检查

Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);

if (bitmap != null) {

picasso.cancelRequest(target);

setBitmap(target, picasso.context, bitmap, MEMORY, noFade, picasso.indicatorsEnabled);

if (picasso.loggingEnabled) {

log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);

}

if (callback != null) {

callback.onSuccess();

}

return;

}

}

if (setPlaceholder) {

setPlaceholder(target, getPlaceholderDrawable());

}

//关键,创建Action,这里用的ImageViewAction

Action action =

new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,

errorDrawable, requestKey, tag, callback, noFade);

//把请求封装成Action,提交分发

picasso.enqueueAndSubmit(action);

}

[java] view
plain copy

void enqueueAndSubmit(Action action) {

Object target = action.getTarget();

if (target != null && targetToAction.get(target) != action) {

// This will also check we are on the main thread.

cancelExistingRequest(target);

targetToAction.put(target, action);

}

submit(action);

}

void submit(Action action) {//提交给Dispatcher分发

dispatcher.dispatchSubmit(action);

}

[java] view
plain copy

void dispatchSubmit(Action action) {

handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));

}

private static class DispatcherHandler extends Handler {

private final Dispatcher dispatcher;

public DispatcherHandler(Looper looper, Dispatcher dispatcher) {

super(looper);

this.dispatcher = dispatcher;

}

@Override public void handleMessage(final Message msg) {

switch (msg.what) {

case REQUEST_SUBMIT: {//提交分发下载请求

Action action = (Action) msg.obj;

dispatcher.performSubmit(action);

break;

}

case REQUEST_CANCEL: {

Action action = (Action) msg.obj;

dispatcher.performCancel(action);

break;

}

case TAG_PAUSE: {

Object tag = msg.obj;

dispatcher.performPauseTag(tag);

break;

}

case TAG_RESUME: {

Object tag = msg.obj;

dispatcher.performResumeTag(tag);

break;

}

case HUNTER_COMPLETE: {

BitmapHunter hunter = (BitmapHunter) msg.obj;

dispatcher.performComplete(hunter);

break;

}

case HUNTER_RETRY: {

BitmapHunter hunter = (BitmapHunter) msg.obj;

dispatcher.performRetry(hunter);

break;

}

case HUNTER_DECODE_FAILED: {

BitmapHunter hunter = (BitmapHunter) msg.obj;

dispatcher.performError(hunter, false);

break;

}

case HUNTER_DELAY_NEXT_BATCH: {

dispatcher.performBatchComplete();

break;

}

case NETWORK_STATE_CHANGE: {

NetworkInfo info = (NetworkInfo) msg.obj;

dispatcher.performNetworkStateChange(info);

break;

}

case AIRPLANE_MODE_CHANGE: {

dispatcher.performAirplaneModeChange(msg.arg1 == AIRPLANE_MODE_ON);

break;

}

default:

Picasso.HANDLER.post(new Runnable() {

@Override public void run() {

throw new AssertionError("Unknown handler message received: " + msg.what);

}

});

}

}

}

[java] view
plain copy

void performSubmit(Action action) {

performSubmit(action, true);

}

void performSubmit(Action action, boolean dismissFailed) {

if (pausedTags.contains(action.getTag())) {

pausedActions.put(action.getTarget(), action);

if (action.getPicasso().loggingEnabled) {

log(OWNER_DISPATCHER, VERB_PAUSED, action.request.logId(),

"because tag '" + action.getTag() + "' is paused");

}

return;

}

//BitmapHunter,核心,它是个线程,执行图片下载编解码

BitmapHunter hunter = hunterMap.get(action.getKey());

if (hunter != null) {

hunter.attach(action);

return;

}

if (service.isShutdown()) {

if (action.getPicasso().loggingEnabled) {

log(OWNER_DISPATCHER, VERB_IGNORED, action.request.logId(), "because shut down");

}

return;

}

hunter = forRequest(action.getPicasso(), this, cache, stats, action);

hunter.future = service.submit(hunter);//这个service是ExecutorService线程池,提交执行下载线程

hunterMap.put(action.getKey(), hunter);

if (dismissFailed) {

failedActions.remove(action.getTarget());

}

if (action.getPicasso().loggingEnabled) {

log(OWNER_DISPATCHER, VERB_ENQUEUED, action.request.logId());

}

}

大概看下BitmapHunter源码:

[java] view
plain copy

/*

* Copyright (C) 2013 Square, Inc.

*

* Licensed under the Apache License, Version 2.0 (the "License");

* you may not use this file except in compliance with the License.

* You may obtain a copy of the License at

*

* http://www.apache.org/licenses/LICENSE-2.0
*

* Unless required by applicable law or agreed to in writing, software

* distributed under the License is distributed on an "AS IS" BASIS,

* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

* See the License for the specific language governing permissions and

* limitations under the License.

*/

package com.squareup.picasso;

import android.graphics.Bitmap;

import android.graphics.BitmapFactory;

import android.graphics.Matrix;

import android.net.NetworkInfo;

import java.io.IOException;

import java.io.InputStream;

import java.io.PrintWriter;

import java.io.StringWriter;

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.Future;

import java.util.concurrent.atomic.AtomicInteger;

import static com.squareup.picasso.MemoryPolicy.shouldReadFromMemoryCache;

import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;

import static com.squareup.picasso.Picasso.Priority;

import static com.squareup.picasso.Picasso.Priority.LOW;

import static com.squareup.picasso.Utils.OWNER_HUNTER;

import static com.squareup.picasso.Utils.VERB_DECODED;

import static com.squareup.picasso.Utils.VERB_EXECUTING;

import static com.squareup.picasso.Utils.VERB_JOINED;

import static com.squareup.picasso.Utils.VERB_REMOVED;

import static com.squareup.picasso.Utils.VERB_TRANSFORMED;

import static com.squareup.picasso.Utils.getLogIdsForHunter;

import static com.squareup.picasso.Utils.log;

class BitmapHunter implements Runnable {

/**

* Global lock for bitmap decoding to ensure that we are only are decoding one at a time. Since

* this will only ever happen in background threads we help avoid excessive memory thrashing as

* well as potential OOMs. Shamelessly stolen from Volley.

*/

private static final Object DECODE_LOCK = new Object();

private static final ThreadLocal<StringBuilder> NAME_BUILDER = new ThreadLocal<StringBuilder>() {

@Override protected StringBuilder initialValue() {

return new StringBuilder(Utils.THREAD_PREFIX);

}

};

private static final AtomicInteger SEQUENCE_GENERATOR = new AtomicInteger();

private static final RequestHandler ERRORING_HANDLER = new RequestHandler() {

@Override public boolean canHandleRequest(Request data) {

return true;

}

@Override public Result load(Request request, int networkPolicy) throws IOException {

throw new IllegalStateException("Unrecognized type of request: " + request);

}

};

final int sequence;

final Picasso picasso;

final Dispatcher dispatcher;

final Cache cache;

final Stats stats;

final String key;

final Request data;

final int memoryPolicy;

int networkPolicy;

final RequestHandler requestHandler;

Action action;

List<Action> actions;

Bitmap result;

Future<?> future;

Picasso.LoadedFrom loadedFrom;

Exception exception;

int exifRotation; // Determined during decoding of original resource.

int retryCount;

Priority priority;

BitmapHunter(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action,

RequestHandler requestHandler) {

this.sequence = SEQUENCE_GENERATOR.incrementAndGet();

this.picasso = picasso;

this.dispatcher = dispatcher;

this.cache = cache;

this.stats = stats;

this.action = action;

this.key = action.getKey();

this.data = action.getRequest();

this.priority = action.getPriority();

this.memoryPolicy = action.getMemoryPolicy();

this.networkPolicy = action.getNetworkPolicy();

this.requestHandler = requestHandler;

this.retryCount = requestHandler.getRetryCount();

}

它是实现了Runable接口的线程,再继续看Run方法。

[java] view
plain copy

@Override public void run() {

try {

updateThreadName(data);

if (picasso.loggingEnabled) {

log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this));

}

//这个result是Bitmap,调用了hunt方法获取

result = hunt();

if (result == null) {//分发图片加载失败

dispatcher.dispatchFailed(this);

} else {//分发图片加载完成

dispatcher.dispatchComplete(this);

}

} catch (Downloader.ResponseException e) {

if (!e.localCacheOnly || e.responseCode != 504) {

exception = e;

}

dispatcher.dispatchFailed(this);

} catch (NetworkRequestHandler.ContentLengthException e) {

exception = e;

dispatcher.dispatchRetry(this);

} catch (IOException e) {

exception = e;

dispatcher.dispatchRetry(this);

} catch (OutOfMemoryError e) {

StringWriter writer = new StringWriter();

stats.createSnapshot().dump(new PrintWriter(writer));

exception = new RuntimeException(writer.toString(), e);

dispatcher.dispatchFailed(this);

} catch (Exception e) {

exception = e;

dispatcher.dispatchFailed(this);

} finally {

Thread.currentThread().setName(Utils.THREAD_IDLE_NAME);

}

}

再继续看hunt方法:

[java] view
plain copy

Bitmap hunt() throws IOException {

Bitmap bitmap = null;

//内存缓存检查,检查了很多次内存缓存了

if (shouldReadFromMemoryCache(memoryPolicy)) {

bitmap = cache.get(key);

if (bitmap != null) {

stats.dispatchCacheHit();

loadedFrom = MEMORY;

if (picasso.loggingEnabled) {

log(OWNER_HUNTER, VERB_DECODED, data.logId(), "from cache");

}

return bitmap;

}

}

data.networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy;

RequestHandler.Result result = requestHandler.load(data, networkPolicy);

if (result != null) {

loadedFrom = result.getLoadedFrom();

exifRotation = result.getExifOrientation();

bitmap = result.getBitmap();

// If there was no Bitmap then we need to decode it from the stream.

if (bitmap == null) {

InputStream is = result.getStream();

try {//inputstream解码

bitmap = decodeStream(is, data);

} finally {

Utils.closeQuietly(is);

}

}

}

if (bitmap != null) {

if (picasso.loggingEnabled) {

log(OWNER_HUNTER, VERB_DECODED, data.logId());

}

stats.dispatchBitmapDecoded(bitmap);//解码命中率统计

if (data.needsTransformation() || exifRotation != 0) {

synchronized (DECODE_LOCK) {

if (data.needsMatrixTransform() || exifRotation != 0) {

bitmap = transformResult(data, bitmap, exifRotation);

if (picasso.loggingEnabled) {

log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId());

}

}

if (data.hasCustomTransformations()) {

bitmap = applyCustomTransformations(data.transformations, bitmap);

if (picasso.loggingEnabled) {

log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations");

}

}

}

if (bitmap != null) {

stats.dispatchBitmapTransformed(bitmap);

}

}

}

return bitmap;

}

看decodeStream方法里的逻辑:

[java] view
plain copy

/**

* Decode a byte stream into a Bitmap. This method will take into account additional information

* about the supplied request in order to do the decoding efficiently (such as through leveraging

* {@code inSampleSize}).

*/

static Bitmap decodeStream(InputStream stream, Request request) throws IOException {

MarkableInputStream markStream = new MarkableInputStream(stream);

stream = markStream;

long mark = markStream.savePosition(65536); // TODO fix this crap.

final BitmapFactory.Options options = RequestHandler.createBitmapOptions(request);

final boolean calculateSize = RequestHandler.requiresInSampleSize(options);

boolean isWebPFile = Utils.isWebPFile(stream);

markStream.reset(mark);

// When decode WebP network stream, BitmapFactory throw JNI Exception and make app crash.

// Decode byte array instead

if (isWebPFile) {

byte[] bytes = Utils.toByteArray(stream);

if (calculateSize) {

BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);

RequestHandler.calculateInSampleSize(request.targetWidth, request.targetHeight, options,

request);

}

return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);

} else {

if (calculateSize) {

BitmapFactory.decodeStream(stream, null, options);

RequestHandler.calculateInSampleSize(request.targetWidth, request.targetHeight, options,

request);

markStream.reset(mark);

}

Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options);

if (bitmap == null) {

// Treat null as an IO exception, we will eventually retry.

throw new IOException("Failed to decode stream.");

}

return bitmap;

}

}

就是对inputSteam转为Bimap操作。

接下来,看dispatcher派发加载成功消息的逻辑:

[java] view
plain copy

void dispatchComplete(BitmapHunter hunter) {

handler.sendMessage(handler.obtainMessage(HUNTER_COMPLETE, hunter));

}

[java] view
plain copy

case HUNTER_COMPLETE: {//加载完成

BitmapHunter hunter = (BitmapHunter) msg.obj;

dispatcher.performComplete(hunter);

break;

}

[java] view
plain copy

void performComplete(BitmapHunter hunter) {

if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {

cache.set(hunter.getKey(), hunter.getResult());

}

hunterMap.remove(hunter.getKey());

batch(hunter);

if (hunter.getPicasso().loggingEnabled) {

log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter), "for completion");

}

}

private void batch(BitmapHunter hunter) {

if (hunter.isCancelled()) {

return;

}

batch.add(hunter);

if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) {

handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY);

}

}

[java] view
plain copy

case HUNTER_DELAY_NEXT_BATCH: {//继续派发next_batch消息

dispatcher.performBatchComplete();

break;

}

[java] view
plain copy

void performBatchComplete() {

List<BitmapHunter> copy = new ArrayList<BitmapHunter>(batch);

batch.clear();

mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy));//注意这里的mainThreadHandler是Picasso的Handler

logBatch(copy);

}

注意这里的mainThreadHander是Picasso里的Handler,我们可以看下Dispatcher的实例化。

[java] view
plain copy

Dispatcher(Context context, ExecutorService service, Handler mainThreadHandler,

Downloader downloader, Cache cache, Stats stats) {

this.dispatcherThread = new DispatcherThread();

this.dispatcherThread.start();

Utils.flushStackLocalLeaks(dispatcherThread.getLooper());

this.context = context;

this.service = service;

this.hunterMap = new LinkedHashMap<String, BitmapHunter>();

this.failedActions = new WeakHashMap<Object, Action>();

this.pausedActions = new WeakHashMap<Object, Action>();

this.pausedTags = new HashSet<Object>();

this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);

this.downloader = downloader;

this.mainThreadHandler = mainThreadHandler;//这个就是Picasso传进来的mainThreadHandler

this.cache = cache;

this.stats = stats;

this.batch = new ArrayList<BitmapHunter>(4);

this.airplaneMode = Utils.isAirplaneModeOn(this.context);

this.scansNetworkChanges = hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE);

this.receiver = new NetworkBroadcastReceiver(this);

receiver.register();

}

那我们看下Picasso里的HUNTER_BATCH_COMPLETE消息处理:

[java] view
plain copy

static final String TAG = "Picasso";

static final Handler HANDLER = new Handler(Looper.getMainLooper()) {

@Override public void handleMessage(Message msg) {

switch (msg.what) {

case HUNTER_BATCH_COMPLETE: {

@SuppressWarnings("unchecked") List<BitmapHunter> batch = (List<BitmapHunter>) msg.obj;

//noinspection ForLoopReplaceableByForEach

for (int i = 0, n = batch.size(); i < n; i++) {

BitmapHunter hunter = batch.get(i);

hunter.picasso.complete(hunter);//回调了complete方法

}

break;

}

case REQUEST_GCED: {

Action action = (Action) msg.obj;

if (action.getPicasso().loggingEnabled) {

log(OWNER_MAIN, VERB_CANCELED, action.request.logId(), "target got garbage collected");

}

action.picasso.cancelExistingRequest(action.getTarget());

break;

}

case REQUEST_BATCH_RESUME:

@SuppressWarnings("unchecked") List<Action> batch = (List<Action>) msg.obj;

//noinspection ForLoopReplaceableByForEach

for (int i = 0, n = batch.size(); i < n; i++) {

Action action = batch.get(i);

action.picasso.resumeAction(action);

}

break;

default:

throw new AssertionError("Unknown handler message received: " + msg.what);

}

}

};

我们看下Picasso的complete方法:

[java] view
plain copy

void complete(BitmapHunter hunter) {

Action single = hunter.getAction();//拿到了派发过来的Action

List<Action> joined = hunter.getActions();

boolean hasMultiple = joined != null && !joined.isEmpty();

boolean shouldDeliver = single != null || hasMultiple;

if (!shouldDeliver) {

return;

}

Uri uri = hunter.getData().uri;

Exception exception = hunter.getException();

Bitmap result = hunter.getResult();//拿到了Bitmap

LoadedFrom from = hunter.getLoadedFrom();

if (single != null) {

deliverAction(result, from, single);//执行deliverAction方法

}

if (hasMultiple) {

//noinspection ForLoopReplaceableByForEach

for (int i = 0, n = joined.size(); i < n; i++) {

Action join = joined.get(i);

deliverAction(result, from, join);

}

}

if (listener != null && exception != null) {

listener.onImageLoadFailed(this, uri, exception);

}

}

我们看下deliverAction方法:

[java] view
plain copy

private void deliverAction(Bitmap result, LoadedFrom from, Action action) {

if (action.isCancelled()) {

return;

}

if (!action.willReplay()) {

targetToAction.remove(action.getTarget());

}

if (result != null) {

if (from == null) {

throw new AssertionError("LoadedFrom cannot be null.");

}

action.complete(result, from);//回调了action的complete方法

if (loggingEnabled) {

log(OWNER_MAIN, VERB_COMPLETED, action.request.logId(), "from " + from);

}

} else {

action.error();

if (loggingEnabled) {

log(OWNER_MAIN, VERB_ERRORED, action.request.logId());

}

}

}

这里注意下,我们之前用的是Action里的ImageViewAction类,我们看下ImageViewAction源码和它的complete类里的处理逻辑:

[java] view
plain copy

/*

* Copyright (C) 2013 Square, Inc.

*

* Licensed under the Apache License, Version 2.0 (the "License");

* you may not use this file except in compliance with the License.

* You may obtain a copy of the License at

*

* http://www.apache.org/licenses/LICENSE-2.0
*

* Unless required by applicable law or agreed to in writing, software

* distributed under the License is distributed on an "AS IS" BASIS,

* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

* See the License for the specific language governing permissions and

* limitations under the License.

*/

package com.squareup.picasso;

import android.content.Context;

import android.graphics.Bitmap;

import android.graphics.drawable.Drawable;

import android.widget.ImageView;

class ImageViewAction extends Action<ImageView> {

Callback callback;

ImageViewAction(Picasso picasso, ImageView imageView, Request data, int memoryPolicy,

int networkPolicy, int errorResId, Drawable errorDrawable, String key, Object tag,

Callback callback, boolean noFade) {

super(picasso, imageView, data, memoryPolicy, networkPolicy, errorResId, errorDrawable, key,

tag, noFade);

this.callback = callback;

}

//图片加载完成回调

@Override public void complete(Bitmap result, Picasso.LoadedFrom from) {

if (result == null) {

throw new AssertionError(

String.format("Attempted to complete action with no result!\n%s", this));

}

ImageView target = this.target.get();

if (target == null) {

return;

}

Context context = picasso.context;

boolean indicatorsEnabled = picasso.indicatorsEnabled;

PicassoDrawable.setBitmap(target, context, result, from, noFade, indicatorsEnabled);//设置bitmap给PicassoDrawable

if (callback != null) {

callback.onSuccess();

}

}

@Override public void error() {

ImageView target = this.target.get();

if (target == null) {

return;

}

if (errorResId != 0) {

target.setImageResource(errorResId);

} else if (errorDrawable != null) {

target.setImageDrawable(errorDrawable);

}

if (callback != null) {

callback.onError();

}

}

@Override void cancel() {

super.cancel();

if (callback != null) {

callback = null;

}

}

}

我们简单看下PicassoDrawable源码和它的setBitmap方法:

[java] view
plain copy

/*

* Copyright (C) 2013 Square, Inc.

*

* Licensed under the Apache License, Version 2.0 (the "License");

* you may not use this file except in compliance with the License.

* You may obtain a copy of the License at

*

* http://www.apache.org/licenses/LICENSE-2.0
*

* Unless required by applicable law or agreed to in writing, software

* distributed under the License is distributed on an "AS IS" BASIS,

* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

* See the License for the specific language governing permissions and

* limitations under the License.

*/

package com.squareup.picasso;

import android.content.Context;

import android.graphics.Bitmap;

import android.graphics.Canvas;

import android.graphics.ColorFilter;

import android.graphics.Paint;

import android.graphics.Path;

import android.graphics.Point;

import android.graphics.Rect;

import android.graphics.drawable.AnimationDrawable;

import android.graphics.drawable.BitmapDrawable;

import android.graphics.drawable.Drawable;

import android.os.Build;

import android.os.SystemClock;

import android.widget.ImageView;

import static android.graphics.Color.WHITE;

import static com.squareup.picasso.Picasso.LoadedFrom.MEMORY;

final class PicassoDrawable extends BitmapDrawable {//继承自BitmapDrawable

// Only accessed from main thread.

private static final Paint DEBUG_PAINT = new Paint();

private static final float FADE_DURATION = 200f; //ms

/**

* Create or update the drawable on the target {@link ImageView} to display the supplied bitmap

* image.

*/

static void setBitmap(ImageView target, Context context, Bitmap bitmap,

Picasso.LoadedFrom loadedFrom, boolean noFade, boolean debugging) {

Drawable placeholder = target.getDrawable();

if (placeholder instanceof AnimationDrawable) {

((AnimationDrawable) placeholder).stop();

}

PicassoDrawable drawable =

new PicassoDrawable(context, bitmap, placeholder, loadedFrom, noFade, debugging);

target.setImageDrawable(drawable);//调用ImageView的setImageDrawable方法将转换后的drawable加载进去显示

}

/**

* Create or update the drawable on the target {@link ImageView} to display the supplied

* placeholder image.

*/

static void setPlaceholder(ImageView target, Drawable placeholderDrawable) {

target.setImageDrawable(placeholderDrawable);

if (target.getDrawable() instanceof AnimationDrawable) {

((AnimationDrawable) target.getDrawable()).start();

}

}

private final boolean debugging;

private final float density;

private final Picasso.LoadedFrom loadedFrom;

Drawable placeholder;

long startTimeMillis;

boolean animating;

int alpha = 0xFF;

PicassoDrawable(Context context, Bitmap bitmap, Drawable placeholder,

Picasso.LoadedFrom loadedFrom, boolean noFade, boolean debugging) {

super(context.getResources(), bitmap);

this.debugging = debugging;

this.density = context.getResources().getDisplayMetrics().density;

this.loadedFrom = loadedFrom;

boolean fade = loadedFrom != MEMORY && !noFade;

if (fade) {

this.placeholder = placeholder;

animating = true;

startTimeMillis = SystemClock.uptimeMillis();

}

}

@Override public void draw(Canvas canvas) {

if (!animating) {

super.draw(canvas);

} else {

float normalized = (SystemClock.uptimeMillis() - startTimeMillis) / FADE_DURATION;

if (normalized >= 1f) {

animating = false;

placeholder = null;

super.draw(canvas);

} else {

if (placeholder != null) {

placeholder.draw(canvas);

}

int partialAlpha = (int) (alpha * normalized);

super.setAlpha(partialAlpha);

super.draw(canvas);

super.setAlpha(alpha);

if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {

invalidateSelf();

}

}

}

if (debugging) {

drawDebugIndicator(canvas);

}

}

@Override public void setAlpha(int alpha) {

this.alpha = alpha;

if (placeholder != null) {

placeholder.setAlpha(alpha);

}

super.setAlpha(alpha);

}

@Override public void setColorFilter(ColorFilter cf) {

if (placeholder != null) {

placeholder.setColorFilter(cf);

}

super.setColorFilter(cf);

}

@Override protected void onBoundsChange(Rect bounds) {

if (placeholder != null) {

placeholder.setBounds(bounds);

}

super.onBoundsChange(bounds);

}

private void drawDebugIndicator(Canvas canvas) {

DEBUG_PAINT.setColor(WHITE);

Path path = getTrianglePath(new Point(0, 0), (int) (16 * density));

canvas.drawPath(path, DEBUG_PAINT);

DEBUG_PAINT.setColor(loadedFrom.debugColor);

path = getTrianglePath(new Point(0, 0), (int) (15 * density));

canvas.drawPath(path, DEBUG_PAINT);

}

private static Path getTrianglePath(Point p1, int width) {

Point p2 = new Point(p1.x + width, p1.y);

Point p3 = new Point(p1.x, p1.y + width);

Path path = new Path();

path.moveTo(p1.x, p1.y);

path.lineTo(p2.x, p2.y);

path.lineTo(p3.x, p3.y);

return path;

}

}

这样,整个Picasso就实现了我们的图片请求加载显示。

如果你觉得本文太长,可以单独查看Picasso源码原理分析一文:

http://blog.csdn.net/jay100500/article/details/71055229

参考文献

[1]Picasso官方简易Wiki.Picasso[OL].2013.http://square.github.io/picasso/

独创性声明

本人声明所呈交的学术研究文章是本人自己和在他人指导下进行的研究工作及取得的研究成果。除了文中特别加以标注和致谢的地方外,文章中不包含他人已经发表或撰写过的研究成果,也不包含为获得其他教育机构的学位或证书而使用过的材料。与我一同工作的同志对本研究所做的任何贡献均已在论文中作了明确的说明。

学术文章作者签名:谭东 签字日期: 2017年 4月 25日

学术文章版权使用授权书

本学术文章作者完全了解有关保留、使用学术文章的规定,有权保留并向国家有关部门或机构送交文章的复印件和磁盘,允许文章被查阅和借阅。本人授权后可以将学术文章的全部或部分内容编入有关数据库进行检索,可以采用影印、缩印或扫描等复制手段保存、汇编学术文章。





遵循:BY-SA

署名-相同方式共享 4.0协议
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: