Fresco源代码分析之二:SimpleDraweeView如何拉取图片并绘制在屏幕上?

Fresco源代码分析之二:SimpleDraweeView如何拉取图片并绘制在屏幕上?

引言

《Fresco源代码分析之1:Fresco的初始化 》文章中,仔细分析了Fresco.init(Context)方法中所做的工作。总结下来有一下几点:

  1. 初始化了 ImagePipelineFactory , 包括默认的ImagePipelineConfig(其中初始化了各种线程池、是否解码、旋转之类的设置、各种Cache的默认配置等等)。
  2. 初始化了默认的 SimpleDraweeView ,包括对应的ImagePipeline。

那么问题来了,当我们通过SimpleDraweeView的

setImageUri(Uri, Object)

方法调用的时候,Fresco到底做了哪些工作呢?图片是经过了哪些工作绘制在界面上的呢?这篇文章就来说说这些事情。

调用方法

``` class="language-none">  /**
   * Displays an image given by the uri.
   *
   * @param uri uri of the image
   * @param callerContext caller context
   */
  public void setImageURI(Uri uri, @Nullable Object callerContext) {
    DraweeController controller = mSimpleDraweeControllerBuilder
        .setCallerContext(callerContext)
        .setUri(uri)
        .setOldController(getController())
        .build();
    setController(controller);
  }
```

在想象中,图片库要将图片显示出来,至少要经过查询缓存、网络获取、图片的解码、写入缓存、图片的后处理、显示图片这几个过程。但查看Fresco的代码,发现仅仅这几行,两句话就完事了,那么这两句话后面,到底有哪些故事发生呢?

源代码分析

从上述代码出发,分析图片加载的全过程。

获取DraweeController

代码的第一行,典型的是一个获取DraweeController的方法,具体的工作是怎么样的呢?

    DraweeController controller = mSimpleDraweeControllerBuilder
        .setCallerContext(callerContext)
        .setUri(uri)
        .setOldController(getController())
        .build();

其中mSimpleDraweeControllerBuilder是一个SimpleDraweeControllerBuilder的接口,该方法以此调用了四个方法,分别设置了 调用者的上下文Uri旧的Controller。从接口上看不出来Fresco具体做了什么工组的,因此还是要看看具体的实现。

在Fresco的初始化代码中,是这样初始化的

  // 初始化Drawee
  private static void initializeDrawee(Context context) {
    sDraweeControllerBuilderSupplier = new PipelineDraweeControllerBuilderSupplier(context);
    SimpleDraweeView.initialize(sDraweeControllerBuilderSupplier);
  }

因此可以看出来,SimpleDraweeView所对应的Controller实际上是源自PipelineDraweeControllerBuilder,进一步查看,可以发现继承关系如下:

SimpleDraweeControllerBuilder
|
|
AbstractDraweeControllerBuilder
|
|
PipelineDraweeControllerBuilder

既然在SimpleDraweeView.setUri()方法中只调用了setCallerContext\setUri\setOldController,那么我们就从这四个方法入手

setCallerContext(Object callerContext)

这个方法源自 AbstractDraweeControllerBuilder

  /** Sets the caller context. */
  @Override
  public BUILDER setCallerContext(Object callerContext) {
    mCallerContext = callerContext;
    return getThis();
  }

和字面意思一样,该方法就是简单的设置了上下文,而且需要注意的是,上下文的类型为Object

setUri(Uri uri)

该方法首先调用 PipelineDraweeControllerBuilder 的对应的方法

  @Override
  public PipelineDraweeControllerBuilder setUri(Uri uri) {
    return super.setImageRequest(ImageRequest.fromUri(uri));
  }

其实本质上还是调用父类的这个方法,但需要注意的是,在这里面使用ImageRequest.fromUri(Uri)又将方法包装了一下,将Uri包装为ImageRequest的类型。ImageRequest的类的注释如下:

Immutable object encapsulating everything pipeline has to know about requested image to proceed.

可以直接看出,在Fresco中,ImageRequest为一个不可变类,其中包含了一个请求所需要的全部信息。

而调用至 AbstractDraweeControllerBuilder 中对应的方法也很简单,就是设置了一下对应的成员变量

  /** Sets the image request. */
  public BUILDER setImageRequest(REQUEST imageRequest) {
    mImageRequest = imageRequest;
    return getThis();
  }

setOldController(DraweeController)

此处也是调用的 AbstractDraweeControllerBuilder中的方法,具体代码如下:

  /** Sets the old controller to be reused if possible. */
  @Override
  public BUILDER setOldController(@Nullable DraweeController oldController) {
    mOldController = oldController;
    return getThis();
  }

工作也很简单,就是同样是设置了成员变量,那么这样看来,所有的工作应该都是放在了build()方法里面,现在去看看。

build()

build方法也是直接调用的 AbstractDraweeControllerBuilder中的方法,

  /** Builds the specified controller. */
  @Override
  public AbstractDraweeController build() {
    validate();

    // if only a low-res request is specified, treat it as a final request.
    if (mImageRequest == null && mMultiImageRequests == null && mLowResImageRequest != null) {
      mImageRequest = mLowResImageRequest;
      mLowResImageRequest = null;
    }

    return buildController();
  }

这里面有一个判断

仅仅在ImageRequest以及MultiImageRequest都为空,且LowResImagaeRequest(低质量请求)存在的时候,才将这个低质量的请求作为最终的请求来处理。

buildController() 方法做了什么呢?代码如下:

  /** Builds a regular controller. */
  protected AbstractDraweeController buildController() {
    AbstractDraweeController controller = obtainController();
    controller.setRetainImageOnFailure(getRetainImageOnFailure());
    maybeBuildAndSetRetryManager(controller);
    maybeAttachListeners(controller);
    return controller;
  }

可以看出来,分了4个步骤:

  1. 获取对应的Controller
  2. 配置Controller是否显示失败重新获取的图片
  3. 设置重试管理器(RetryManager)
  4. 设置Attach事件监听者

要想搞明白了,还是一件一件事情依次看看做了什么。

AbstractDraweeController controller = obtainController();

AbstractDraweeControllerBuilder中并没有obtainController()的默认实现,并要求子类实现,在PipelineDraweeControllerBuilder中,其实现如下:

    /**
     * 该方法会尝试复用以前的Controller,
     * 复用的办法是:
     * 1\首先判断Controller的类型,如果是PipelineDraweeController,则初始化即可
     * 2\如果不是如果是PipelineDraweeController的类型,则新建一个.
     * @return
     */

  @Override
  protected PipelineDraweeController obtainController() {
    DraweeController oldController = getOldController();
    PipelineDraweeController controller;
    if (oldController instanceof PipelineDraweeController) {
      controller = (PipelineDraweeController) oldController;
      controller.initialize(
          obtainDataSourceSupplier(),
          generateUniqueControllerId(),
          getCallerContext());
    } else {
      controller = mPipelineDraweeControllerFactory.newController(
          obtainDataSourceSupplier(),
          generateUniqueControllerId(),
          getCallerContext());
    }
    return controller;
  }

其实代码也很简单,就是检查之前通过setOldController设置的Controller是否为自身的类型,如果是,则通过设置重新使用,如果不是,则通过对应的工厂方法重新建立一个。

当然,我们不能看到这边就可以了,既然Controller可以复用,我们就看看重用Controller的初始化代码中都做了什么工作?

  /**
   * 通过新配置的Id和调用者上下文,重新初始化AbstractDraweeController
   * 这种设计允许在不需要重新实例化controller的时候复用之前的Controller.
   *
   * Initializes this controller with the new id and caller context.
   * This allows for reusing of the existing controller instead of instantiating a new one.
   * This method should be called when the controller is in detached state.
   * @param id unique id for this controller
   * @param callerContext tag and context for this controller
   */
  protected void initialize(String id, Object callerContext) {
    init(id, callerContext);
  }

  private void init(String id, Object callerContext) {
    // 记录事件:开始初始化Controller
    mEventTracker.recordEvent(Event.ON_INIT_CONTROLLER);
    // 如果之前有DeferredReleaser,则释放
    // cancel deferred release
    if (mDeferredReleaser != null) {
      mDeferredReleaser.cancelDeferredRelease(this);
    }
    // 重新初始化各种状态
    // reinitialize mutable state (fetch state)
    mIsAttached = false;
    releaseFetch();
    mRetainImageOnFailure = false;
    // 重新初始化RetryManager
    // reinitialize optional components
    if (mRetryManager != null) {
      mRetryManager.init();
    }
    // 重新初始化GestureDetector
    if (mGestureDetector != null) {
      mGestureDetector.init();
      mGestureDetector.setClickListener(this);
    }
    // 清空ControllerListener
    if (mControllerListener instanceof InternalForwardingListener) {
      ((InternalForwardingListener) mControllerListener).clearListeners();
    } else {
      mControllerListener = null;
    }
    // 清空Drawee视图
    // clear hierarchy and controller overlay
    if (mSettableDraweeHierarchy != null) {
      mSettableDraweeHierarchy.reset();
      mSettableDraweeHierarchy.setControllerOverlay(null);
      mSettableDraweeHierarchy = null;
    }
    mControllerOverlay = null;
    // reinitialize constant state
    if (FLog.isLoggable(FLog.VERBOSE)) {
      FLog.v(TAG, "controller %x %s -> %s: initialize", System.identityHashCode(this), mId, id);
    }
    mId = id;
    mCallerContext = callerContext;
  }

上面的代码就是Controller初始化或者说重新初始化过程,所调用的代码,具体都有哪些工作呢?依次来看:

  1. 记录事件:初始化Controller
  2. 如果之前调用了DeferredReleaser,则取消。
  3. 设置变量 mISAttached = false; 释放之前的网络获取。
  4. 重新配置RetryManager.
  5. 重新设置触摸事件检测
  6. 配置MControllerListener,若为内置转发Listener,则保留,反之清空。
  7. 清空对应的Drawee视图。
  8. 设置Id和上下文。

setController(DraweeController)

setController(DraweeController)的代码如下:

  /** Sets the controller. */
  public void setController(@Nullable DraweeController draweeController) {
    mDraweeHolder.setController(draweeController);
    super.setImageDrawable(mDraweeHolder.getTopLevelDrawable());
  }

这一块代码也很简单,两句话:

  1. 为mDraweeHoler设置了Controller
  2. 调用了ImageView的setImageDrawable(Drawable)方法,设置了一张图片。

其实第2步代码的作用非常容易理解:就是在网络数据拉取成功之前,先设置一张默认的图片,这样在交互的时候,界面更加友好。 那么这样看来,网络请求发出等工作都是在第1步代码当中了。

  /**
   * 配置新的Controller
   * Sets a new controller.
   */
  public void setController(@Nullable DraweeController draweeController) {
    // 首先判断该DraweeHolder是否已经与某个Controller绑定.(按照Controller方法的调用行为,此处更应该表述的是,该图像已经需要Controller加载过)
    boolean wasAttached = mIsControllerAttached;
    // 如果之前绑定过,则首先解除绑定.
    if (wasAttached) {
      detachController();
    }

    // Clear the old controller, 重置之前的Controller
    if (mController != null) {
      mEventTracker.recordEvent(Event.ON_CLEAR_OLD_CONTROLLER);
      mController.setHierarchy(null);
    }
    // 将新的Controller赋值给成员变量.
    mController = draweeController;
    // 记录事件,配置视图
    if (mController != null) {
      mEventTracker.recordEvent(Event.ON_SET_CONTROLLER);
      mController.setHierarchy(mHierarchy);
    } else {
      mEventTracker.recordEvent(Event.ON_CLEAR_CONTROLLER);
    }

    // 如果之前的Controller曾经绑定,则进一步调用attachController
    if (wasAttached) {
      attachController();
    }
  }

setController 方法着实也非常简单,可以看出其实主要就是几个工作:

  1. 首先检查该DraweeHolder是否之前绑定过Controller,如果有,则调用detachController()方法释放之前的Controller。
  2. 清空之前的Controller,并将新的controller对象赋给mController。
  3. 记录事件,为新的controller配置视图。
  4. 如果该DraweeHolder之前绑定过Controller,则调用attachController()方法直接拉起下一步进程。

需要注意的是,我们在代码分析中可以发现,其实我们在setController的时候,Fresco并不一定会直接发出网络请求,那么Fresco是什么时候进行的呢?分为两种情况:

  1. 如果DraweeHoler之前有通过Controller加载过图片,则直接通过新的controller重新加载。
  2. 如果没有,则等待时机执行。那什么时候呢?接着分析一下。

图片的加载

其实很简单,Controller中有个方法叫onAttach(),这个名字是不是很熟悉?没错,和View的onAttach()方法签名一样,同样的,则个方法也是监听DraweeHolder的onAttach()回调函数,来看看。

ViewHolder

  /**
   * Gets the controller ready to display the image.
   *
   * <p>The containing view must call this method from both {@link View#onFinishTemporaryDetach()}
   * and {@link View#onAttachedToWindow()}.
   */
  public void onAttach() {
    mEventTracker.recordEvent(Event.ON_HOLDER_ATTACH);
    mIsHolderAttached = true;
    attachOrDetachController();
  }

代码很简单,就是记录事件,然后设置变量,然后调用下面一个方法,接着看。

  private void attachOrDetachController() {
    if (mIsHolderAttached && mIsVisible && mIsActivityStarted) {
      attachController();
    } else {
      detachController();
    }
  }

这个也不用多说,就是检查DraweeHolder是否已经已经在屏幕上了,是否是显示的,Activity是否已经启动,如果是,则直接attachController()。

AbastarctDraweeController 接下来就看看Controller的onAttach()方法了

  @Override
  public void onAttach() {
    if (FLog.isLoggable(FLog.VERBOSE)) {
      FLog.v(
          TAG,
          "controller %x %s: onAttach: %s",
          System.identityHashCode(this),
          mId,
          mIsRequestSubmitted ? "request already submitted" : "request needs submit");
    }
    // 记录事件
    mEventTracker.recordEvent(Event.ON_ATTACH_CONTROLLER);
    // 检查mSettingableDraweeHierarchy是否为null
    Preconditions.checkNotNull(mSettableDraweeHierarchy);
    // 释放DeferredReleaser
    mDeferredReleaser.cancelDeferredRelease(this);
    // 设置mISAttached
    mIsAttached = true;
    // 如果任务没有提交,则提交
    if (!mIsRequestSubmitted) {
      submitRequest();
    }
  }

代码也很简单,无需多说,任务就是在这里被提交的。

  // 提交请求
  protected void submitRequest() {
    // 记录事件
    mEventTracker.recordEvent(Event.ON_DATASOURCE_SUBMIT);
    // 向Controller Listener 提交对应事件
    getControllerListener().onSubmit(mId, mCallerContext);
    // 设置进度,设置不显示进度
    mSettableDraweeHierarchy.setProgress(0, true);
    // 设置相关变量
    mIsRequestSubmitted = true;
    mHasFetchFailed = false;
    mDataSource = getDataSource();
    if (FLog.isLoggable(FLog.VERBOSE)) {
      FLog.v(
          TAG,
          "controller %x %s: submitRequest: dataSource: %x",
          System.identityHashCode(this),
          mId,
          System.identityHashCode(mDataSource));
    }
    final String id = mId;
    final boolean wasImmediate = mDataSource.hasResult();
    // 添加DataSubscriber
    final DataSubscriber<T> dataSubscriber =
        new BaseDataSubscriber<T>() {
          @Override
          public void onNewResultImpl(DataSource<T> dataSource) {
            // isFinished must be obtained before image, otherwise we might set intermediate result
            // as final image.
            boolean isFinished = dataSource.isFinished();
            float progress = dataSource.getProgress();
            T image = dataSource.getResult();
            if (image != null) {
              onNewResultInternal(id, dataSource, image, progress, isFinished, wasImmediate);
            } else if (isFinished) {
              onFailureInternal(id, dataSource, new NullPointerException(), /* isFinished */ true);
            }
          }
          @Override
          public void onFailureImpl(DataSource<T> dataSource) {
            onFailureInternal(id, dataSource, dataSource.getFailureCause(), /* isFinished */ true);
          }
          @Override
          public void onProgressUpdate(DataSource<T> dataSource) {
            boolean isFinished = dataSource.isFinished();
            float progress = dataSource.getProgress();
            onProgressUpdateInternal(id, dataSource, progress, isFinished);
          }
        };
    mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor);
  }

提交请求的代码我也加了注释,但这个地方涉及到整个数据流的分析,后面接着讲,暂时就看到这里。

Fresco源代码分析:1、Fresco的初始化

Fresco源代码分析 之一 默认初始化

Fresco被认为是现在最为好用的Android图片加载库,在之前的文章,有根据官方文档分析过Fresco的入门手册,但使用始终无法明白其具体是怎么样工作的,因为,我们还是要从源码上分析Fresco的工作原理。

Fresco的初始化

在一般情况下,我们是使用默认的ImagePipeline配置来初始化Fresco的,代码如下:

  Fresco.initialize(this);

而实际上,Fresco一共提供了两个初始化方法


  /** Initializes Fresco with the default config. */
  public static void initialize(Context context) {
    // 初始化了ImagePipelineFactory的默认配置
    ImagePipelineFactory.initialize(context);
    initializeDrawee(context);
  }
  /** Initializes Fresco with the specified config. */
  public static void initialize(Context context, ImagePipelineConfig imagePipelineConfig) {
    ImagePipelineFactory.initialize(imagePipelineConfig);
    initializeDrawee(context);
  }

可以看到,这两个初始化方法仅仅是初始化ImagePipeline的不同,接下来依次分析。

初始化ImagePipelineFactory

同样的,ImagePipelineFactory总共提供了两个初始化方法,代码分别如下:

  /** Initializes {@link ImagePipelineFactory} with default config. */
  public static void initialize(Context context) {
    initialize(ImagePipelineConfig.newBuilder(context).build());
  }
  /** Initializes {@link ImagePipelineFactory} with the specified config. */
  public static void initialize(ImagePipelineConfig imagePipelineConfig) {
    sInstance = new ImagePipelineFactory(imagePipelineConfig);
  }

其中可以看到initialize(Context context)本质上也是调用initialize(ImagePipelineConfig)的方法,因此实际上也就是设置的ImagePipelineConfig的不同,而实际上,这个参数也是从Fresco的初始化Fresco.initialize(Context, ImagePipelineFactory)传入的。因此,我们从分析ImagePipelineConfig.newBuilder(context).build()开始。

ImagePipelineConfig初始化

ImagePipelineConfig.newBuilder(context)其实是构造了ImagePipelineConfig#Builder对象,build()方法则反悔了ImagePipelineConfig对象,其中设置的属性都有如下:

  @Nullable private final AnimatedImageFactory mAnimatedImageFactory; // 动画工厂
  private final Bitmap.Config mBitmapConfig; // Bitmap配置
  private final Supplier<MemoryCacheParams> mBitmapMemoryCacheParamsSupplier; // 缓存Bitmap所对应的MemoryCacheParams的供应者
  private final CacheKeyFactory mCacheKeyFactory; // CacheKey工厂
  private final Context mContext; // Context上下文
  private final boolean mDownsampleEnabled; // 是否运行下载缩略图
  private final boolean mDecodeFileDescriptorEnabled; // 是否允许解码文件描述否
  private final boolean mDecodeMemoryFileEnabled; // 是否允许解码内存中的文件
  private final Supplier<MemoryCacheParams> mEncodedMemoryCacheParamsSupplier; // 缓存未解码的图像所对应的MemoryCacheParams的供应者
  private final ExecutorSupplier mExecutorSupplier; // 线程池供应商
  private final ImageCacheStatsTracker mImageCacheStatsTracker; // 记录ImageCache各种状态的工具
  @Nullable private final ImageDecoder mImageDecoder; // 图片解码工具类
  private final Supplier<Boolean> mIsPrefetchEnabledSupplier; // 是否允许预取的供应商
  private final DiskCacheConfig mMainDiskCacheConfig; // 磁盘缓存配置
  private final MemoryTrimmableRegistry mMemoryTrimmableRegistry; // 监听内存状态
  private final NetworkFetcher mNetworkFetcher;  // 内存
  @Nullable private final PlatformBitmapFactory mPlatformBitmapFactory; // 不同版本对应的BitmapFactory
  private final PoolFactory mPoolFactory; // Pool的工厂
  private final ProgressiveJpegConfig mProgressiveJpegConfig; // 对于渐进式Jpeg的配置
  private final Set<RequestListener> mRequestListeners; // Request的监听者
  private final boolean mResizeAndRotateEnabledForNetwork; // 是否允许网络请求的图片设置大小或是旋转
  private final DiskCacheConfig mSmallImageDiskCacheConfig; // 小图片的磁盘缓存配置

ImagePipelineConfig内部的构造方法对本身复杂的参数进行设置,具体如下:

  private ImagePipelineConfig(Builder builder) {
    // 动画工厂
    mAnimatedImageFactory = builder.mAnimatedImageFactory;
    // Bitmap 内存缓存参数 Supplier
    mBitmapMemoryCacheParamsSupplier =
        builder.mBitmapMemoryCacheParamsSupplier == null ?
            new DefaultBitmapMemoryCacheParamsSupplier(
                (ActivityManager) builder.mContext.getSystemService(Context.ACTIVITY_SERVICE)) :
            builder.mBitmapMemoryCacheParamsSupplier;
    // 图片的默认格式为 ARGB_8888
    mBitmapConfig =
        builder.mBitmapConfig == null ?
            Bitmap.Config.ARGB_8888 :
            builder.mBitmapConfig;
    mCacheKeyFactory =
        builder.mCacheKeyFactory == null ?
            DefaultCacheKeyFactory.getInstance() :
            builder.mCacheKeyFactory;
    mContext = Preconditions.checkNotNull(builder.mContext);
    mDecodeFileDescriptorEnabled = builder.mDownsampleEnabled &&
        builder.mDecodeFileDescriptorEnabled;
    mDecodeMemoryFileEnabled = builder.mDecodeMemoryFileEnabled;
    mDownsampleEnabled = builder.mDownsampleEnabled;
    mEncodedMemoryCacheParamsSupplier =
        builder.mEncodedMemoryCacheParamsSupplier == null ?
            new DefaultEncodedMemoryCacheParamsSupplier() :
            builder.mEncodedMemoryCacheParamsSupplier;
    // 默认不记录Cache的Stat
    mImageCacheStatsTracker =
        builder.mImageCacheStatsTracker == null ?
            NoOpImageCacheStatsTracker.getInstance() :
            builder.mImageCacheStatsTracker;
    mImageDecoder = builder.mImageDecoder;
    mIsPrefetchEnabledSupplier =
        builder.mIsPrefetchEnabledSupplier == null ?
            new Supplier<Boolean>() {
              @Override
              public Boolean get() {
                return true;
              }
            } :
            builder.mIsPrefetchEnabledSupplier;
    // 磁盘默认 缓存大小配置
    mMainDiskCacheConfig =
        builder.mMainDiskCacheConfig == null ?
            getDefaultMainDiskCacheConfig(builder.mContext) :
            builder.mMainDiskCacheConfig;
    // 默认磁盘整理策略,该默认配置什么也不做
    mMemoryTrimmableRegistry =
        builder.mMemoryTrimmableRegistry == null ?
            NoOpMemoryTrimmableRegistry.getInstance() :
            builder.mMemoryTrimmableRegistry;
    // 默认的网络Fetcher:HttpURLConnection
    mNetworkFetcher =
        builder.mNetworkFetcher == null ?
            new HttpUrlConnectionNetworkFetcher() :
            builder.mNetworkFetcher;
    mPlatformBitmapFactory = builder.mPlatformBitmapFactory;
    mPoolFactory =
        builder.mPoolFactory == null ?
            new PoolFactory(PoolConfig.newBuilder().build()) :
            builder.mPoolFactory;
    mProgressiveJpegConfig =
        builder.mProgressiveJpegConfig == null ?
            new SimpleProgressiveJpegConfig() :
            builder.mProgressiveJpegConfig;
    mRequestListeners =
        builder.mRequestListeners == null ?
            new HashSet<RequestListener>() :
            builder.mRequestListeners;
    mResizeAndRotateEnabledForNetwork = builder.mResizeAndRotateEnabledForNetwork;
    mSmallImageDiskCacheConfig =
        builder.mSmallImageDiskCacheConfig == null ?
            mMainDiskCacheConfig :
            builder.mSmallImageDiskCacheConfig;
    // Below this comment can't be built in alphabetical order, because of dependencies
    int numCpuBoundThreads = mPoolFactory.getFlexByteArrayPoolMaxNumThreads();
    mExecutorSupplier =
        builder.mExecutorSupplier == null ?
            new DefaultExecutorSupplier(numCpuBoundThreads) : builder.mExecutorSupplier;
  }

实际上,对于ImagePipeline默认初始化的理解,就在于对这些参数设置的理解上,因此,接下来我们因此分析每一种配置的所表示的意义是什么。

Supplier<MemoryCacheParams> mBitmapMemoryCacheParamsSupplier

该配置用来表示默认的解码之后的Bitmap的缓存策略,其默认配置如下:

    // Bitmap 内存缓存参数 Supplier
    mBitmapMemoryCacheParamsSupplier =
        builder.mBitmapMemoryCacheParamsSupplier == null ?
            new DefaultBitmapMemoryCacheParamsSupplier(
                (ActivityManager) builder.mContext.getSystemService(Context.ACTIVITY_SERVICE)) :
            builder.mBitmapMemoryCacheParamsSupplier;

可以看到,如果默认情况下没有在builder中提供mBitmapMemoryCacheParamsSupplier中提供配置的话,会通过DefaultBitmapMemoryCacheParamsSupplier创建一个,那创建的内容是什么呢?DefaultBitmapMemoryCacheParamsSupplier代码比较简单,其配置的MemoryCacheParams内容通过表格简单列举一下。 MemoryCacheParams 配置:

变量名 默认值 备注
maxCacheSize 不同内存,cache大小不同,见下表 cache的最大空间,单位kb
maxCacheEntries 256 cache中允许的有效元素的最大数量
maxEvictionQueueSize Integer.MAX_VALUE cache待回收空间队列大小,单位kb
maxEvictionQueueEntries Integer.MAX_VALUE cache待回收队列最大元素数量
maxCacheEntrySize Integer.MAX_VALUE 单个cache所能容纳的最大元素数量

不同机器上,所配置的cache大小分别为:

序号 内存 设定值 说明
1 <32MB 4MB
2 >=32MB && <64MB 6MB
3 不在1、2之列 && API < 11 8MB 之前版本使用共享内存无法获得足够理想的效果
4 不再1、2之列 && API >= 11 程序可用Heap大小/4

CacheKeyFactory mCacheKeyFactory

CacheKeyFactory的配置代码如下:

    mCacheKeyFactory =
        builder.mCacheKeyFactory == null ?
            DefaultCacheKeyFactory.getInstance() :
            builder.mCacheKeyFactory;

对于CacheKeyFactory而言,其配置主要分为两类,一类是未解码的ImageRequest,一类是已经解码的ImageRequest,分别如下:

未解码的ImageRquest

对于这种ImageRquest,通过的SimpleCacheKey来实现。

  @Override
  public CacheKey getEncodedCacheKey(ImageRequest request) {
    return new SimpleCacheKey(getCacheKeySourceUri(request.getSourceUri()).toString());
  }

而实际上,SimpleCache则是简单的对传入的String取.hashCode(),换句话说,就是根据Uri生成了hashCode

解码的ImageRequest

剩余情况下,所使用的参数有:

参数名称 参数描述
mSourceString 一般对应Uri
mResizeOptions 大小调整参数
mAutoRotated 自动旋转参数
mImageDecodeOptions 解码配置
mPostprocessorCacheKey 后处理器CacheKey
mPostprocessorName 后处理器名称

Supplier<MemoryCacheParams> mEncodedMemoryCacheParams

该变量提供未解码图片缓存配置策略,代码如下:

    mEncodedMemoryCacheParamsSupplier =
        builder.mEncodedMemoryCacheParamsSupplier == null ?
            new DefaultEncodedMemoryCacheParamsSupplier() :
            builder.mEncodedMemoryCacheParamsSupplier;

看看DefaultEncodedMemoryCacheparamsSupplier都配置了什么?其实与CacheKeyFactory mCacheKeyFactory类似。区别在于Cache大小取值不同,具体如下:

序号 内存
<16MB 1MB
<32MB 2MB
>=64MB 4MB

ImageCacheStatsTracker mImageCacheStatsTracker

该配置主要用来控制ImageCache统计信息记录,相关代码如下:

    mImageCacheStatsTracker =
        builder.mImageCacheStatsTracker == null ?
            NoOpImageCacheStatsTracker.getInstance() :
            builder.mImageCacheStatsTracker;

默认配置为:什么ImageCache的统计信息也不记录。

DiskCacheConfig mMainDiskCacheConfig

磁盘缓存策略配置

    mMainDiskCacheConfig =
        builder.mMainDiskCacheConfig == null ?
            getDefaultMainDiskCacheConfig(builder.mContext) :
            builder.mMainDiskCacheConfig;

默认配置

  private static DiskCacheConfig getDefaultMainDiskCacheConfig(final Context context) {
    return DiskCacheConfig.newBuilder()
            // 默认文件路径
        .setBaseDirectoryPathSupplier(
            new Supplier<File>() {
              @Override
              public File get() {
                return context.getApplicationContext().getCacheDir();
              }
            })
            // 目录
        .setBaseDirectoryName("image_cache")
            // 默认大小 40MB
        .setMaxCacheSize(40 * ByteConstants.MB)
            // 磁盘空间不足时候,默认大小 10MB
        .setMaxCacheSizeOnLowDiskSpace(10 * ByteConstants.MB)
            // 磁盘控件极其不足时,默认 2MB
        .setMaxCacheSizeOnVeryLowDiskSpace(2 * ByteConstants.MB)
        .build();
  }

NetworkFetcher mNetworkFetcher

NetworkFetcher,Fresco支持okHttp和Android自带的HttpURLConnection实现。

    mNetworkFetcher =
        builder.mNetworkFetcher == null ?
            new HttpUrlConnectionNetworkFetcher() :
            builder.mNetworkFetcher;

网络的获取部分是一个图片库的重要部分,在后面我们需要重点分析,此处知道在默认情况下,Fresco采用Android自带的网络库即可。

PoolFactory mPoolFactory

在图形库当中,由于需要频繁小块的内存访问,重复申请空间会花费大量的时间,因此都会采用对象池/数据池的办法重复利用以前的对象,Fresco也不例外:

    mPoolFactory =
        builder.mPoolFactory == null ?
            new PoolFactory(PoolConfig.newBuilder().build()) :
            builder.mPoolFactory;

对应的默认配置代码如下:

  private PoolConfig(Builder builder) {
    mBitmapPoolParams =
        builder.mBitmapPoolParams == null ?
            DefaultBitmapPoolParams.get() :
            builder.mBitmapPoolParams;
    mBitmapPoolStatsTracker =
        builder.mBitmapPoolStatsTracker == null ?
            NoOpPoolStatsTracker.getInstance() :
            builder.mBitmapPoolStatsTracker;
    mFlexByteArrayPoolParams =
        builder.mFlexByteArrayPoolParams == null ?
            DefaultFlexByteArrayPoolParams.get() :
            builder.mFlexByteArrayPoolParams;
    mMemoryTrimmableRegistry =
        builder.mMemoryTrimmableRegistry == null ?
            NoOpMemoryTrimmableRegistry.getInstance() :
            builder.mMemoryTrimmableRegistry;
    mNativeMemoryChunkPoolParams =
        builder.mNativeMemoryChunkPoolParams == null ?
            DefaultNativeMemoryChunkPoolParams.get() :
            builder.mNativeMemoryChunkPoolParams;
    mNativeMemoryChunkPoolStatsTracker =
        builder.mNativeMemoryChunkPoolStatsTracker == null ?
            NoOpPoolStatsTracker.getInstance() :
            builder.mNativeMemoryChunkPoolStatsTracker;
    mSmallByteArrayPoolParams =
        builder.mSmallByteArrayPoolParams == null ?
            DefaultByteArrayPoolParams.get() :
            builder.mSmallByteArrayPoolParams;
    mSmallByteArrayPoolStatsTracker =
        builder.mSmallByteArrayPoolStatsTracker == null ?
            NoOpPoolStatsTracker.getInstance() :
            builder.mSmallByteArrayPoolStatsTracker;
  }

依赖来看:

PoolParams mBitmapPoolParams

默认情况下,Fresco不缓存任何Bitmap对象,如果使用完毕,则立刻释放。 PoolParams参数

名称 含义 默认配置
maxSizeHardCap 最大实际使用空间,当pool的size达到该设定值时,再申请空间会抛出BasePool.PoolSizeViolationException异常 内存>16MB -> 0.75Max;
内存<=16MB -> 0.5Max
maxSizeSoftCap 一个pool的虚拟容量:
当pool的size达到该设定值时,pool会尝试清理空间,直至size<该设定值 或者 空闲空间=0<
0:不缓存
bucketSizes pool可以包含各种各样的「size」,一个bucket用来表示一种大小,额外的,每一中bucket包含max-length,用来表示bucket中used+free的总共的元素数量,此处的maxSize是上面的soft类型的maxSize,如果达到限制,不会抛出异常,而仅仅是开始释放空间,如果此时仍有请求过来,则是和简单的alloc+free一样,不会被pool管理。如果该参数null,那么pool会根据需求自动创建bucket 0:不缓存
minBucketSize 表示pool中最小的bucket数量,可以保证任何元素小于等于该参数的都可以保存的bucket中 0
maxBucketSize 仅有元素size小于该参数时,才会保存至bucket中,如果尺寸超过该参数,会抛出异常。 Integer.MAX_VALUE

其他的也是各种Pool的配置,此处就不再多分析,需要的时候再过来看。

ProgressiveJpegConfig

渐进式照片显示控制。

RequestListeners

保存所有的请求监听者对象

ResizeAndRotateEnabledForNetwork

是否允许从网络获取的图像,调整大小/缩放

SmallImageDiskCacheConfig

较小图片的Disk Cache配置

ExecutorSupplier

线程池对象,注意,该对象初始化代码如下:

    int numCpuBoundThreads = mPoolFactory.getFlexByteArrayPoolMaxNumThreads();
    mExecutorSupplier =
        builder.mExecutorSupplier == null ?
            new DefaultExecutorSupplier(numCpuBoundThreads) : builder.mExecutorSupplier;

也就是说,这个是依赖于FlexByteArrayPoolMaxNumThreads()的,那么这个方法默认的参数是什么呢?跟进去看一下 FlexByteArrayPool中的线程池数量为:public static final int DEFAULT_MAX_NUM_THREADS = Runtime.getRuntime().availableProcessors();

初始化Drawee

Drawee的初始化代码如下:

  // 初始化Drawee
  private static void initializeDrawee(Context context) {
    sDraweeControllerBuilderSupplier = new PipelineDraweeControllerBuilderSupplier(context);
    SimpleDraweeView.initialize(sDraweeControllerBuilderSupplier);
  }

可以看到Drawee的初始化分为两步:

  1. 实例化PipelineDraweeControllerBuilderSuppplier。
  2. 使用PipelineDraweeControllerBuilderSupplier初始化SimpleDraweeView。

PipelineDraweeControllerBuilderSupplier

该对象的实例过程如下:

  public PipelineDraweeControllerBuilderSupplier(
      Context context,
      ImagePipelineFactory imagePipelineFactory,
      Set<ControllerListener> boundControllerListeners) {
    mContext = context;
    mImagePipeline = imagePipelineFactory.getImagePipeline();
    mPipelineDraweeControllerFactory = new PipelineDraweeControllerFactory(
        context.getResources(),
        DeferredReleaser.getInstance(),
        imagePipelineFactory.getAnimatedDrawableFactory(),
        UiThreadImmediateExecutorService.getInstance());
    mBoundControllerListeners = boundControllerListeners;
  }

步骤如下:

  1. 获取ImagePipeline
  2. 实例化PipelineDraweeControllerFactory
  3. 设置Controller监听者

而PipelineDraweeController则设置了一下的属性

    // 资源所在的Resource
  private Resources mResources;
    // DeferredReleaser 用于释放任务
  private DeferredReleaser mDeferredReleaser;
    // 动画Drawable工厂
  private AnimatedDrawableFactory mAnimatedDrawableFactory;
    // 基于主线程的线程池
  private Executor mUiThreadExecutor;

其中变量的含义比较容易理解,就不再解释,需要注意的是, mUiThreadExecutor是一个封装了主线程Looper的Executor。

SimpleDraweeView的初始化

SimpleDraweeView的初始化更加简单,只有一行代码。

  public static void initialize(
      Supplier<? extends SimpleDraweeControllerBuilder> draweeControllerBuilderSupplier) {
    sDraweeControllerBuilderSupplier = draweeControllerBuilderSupplier;
  }

就是将前面初始化好的 PipelineDraweeControllerBuilderSupplier 设置给SimpleDraweeView.

好了,Fresco最基本的初始化过程就是这些,但这些还不能向我们解释,Fresco具体是将图片怎么样绘制到界面上的,在下一篇文章中,将对这些内容进行分析。

Fresco学习笔记

Fresco学习笔记

Fresco是Facebook出品的一款比较新的图片库,相比 Volley 或者 Picasso 具有很多优点,这篇文章主要是记录如何学习使用Fresco。 内容主要来自: http://fresco-cn.org/docs/getting-started.html


 

配置和使用

引用

Gradle添加lib

dependencies { compile 'com.facebook.fresco:fresco:0.5.0+'}

使用

网络权限

 

初始化Fresco

注意,初始化Fresco的代码必须位于setContentView(ResourceId)代码调用之前。

Fresco.initialize(context);

xml布局文件命名空间

 
 

使用SimpleDraweeView

 

需要注意的是,Fresco的layout_width/height必须要设置,但不能使用wrap_content来根据图片自动调整布局大小,但根据layout_weight属性均分的时候,可以使用wrap_content属性。

加载图片

Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/gh-pages/static/fresco-logo.png");
SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view);
draweeView.setImageURI(uri);

剩余的工作,Fresco完成

  • 显示占位图直到加载完成
  • 下载图片
  • 缓存图片
  • 图片不再显示时,从内存中移除

几个概念

Drawees

Drawees负责图片的显示、包括几个组件,组织上类似MVC架构。

DraweeView

继承自View,负责图片的显示。(需要注意的是,Fresco并没有使用自定义View,而使用了View来显示图片) 一般情况下,使用SimpleDraweeView即可。

DraweeHierarchy

DraweeHierarchy用于组织和维护Draweeable对象,相比于MVC中的Module,需要在Java代码中自定义图片的显示,可通过该类实现。

DraweeController

DraweeController负责和image loader交互(默认情况下使用Fresco的image pipeline),可创建一个该类的实例,用于实现对图片的更多的控制。

DraweeControllerBuilder

DraweeControllers由DraweeControllerBuilder采用Builder构建者模式创建,immutable。

Liseners

builder的作用之一就是,当图片从服务器下载时,在某些状态时刻设置需要执行的代码。

Image Pipeline

Fraesco使用image pipeline来处理图片获取的过程,包括从网络、本地文件、content provider、本地资源中获取,Image pipeline在本地磁盘中保存了一份压缩的图片缓存,在RAM中一份未压缩的图片。 Image pipeline使用特定的技术 pinned purgeables 来在Java heap之外保存图片,这也要求在图片使用完成之后要关闭 close 图片。 SimpleDraweeView自动处理这一过程,大部分情况下可以满足我们的使用需求。

支持的URI

Fresco 不支持 相对路径的 URI,因此所有的URI在使用的时候指定绝对路径,包括scheme。 Fresco支持的URI包括:

Type Scheme Fetch method used
File on network http://, https:// HttpURLConnection or network layer
File on device file:// FileInputStream
Content Provider content:// ContentResolver
Asset in app asset:// AssetManager
Resource in app res:// Resource.openRawResource

注意:image pipeline仅仅支持图片资源,比如PNG图片格式,并未支持其他的比如String或者XML Drawable等。其中可能困惑的就是使用 XML 声明的Drawable,比如ShapeDraable,需要注意的是,ShapeDrawable也并非图片。如果必须要在Fresco中使用XML Drawable,可以将XML drawable指定为 placeholder,并指定uri为 null.

Drawee

在XML中使用Drawees

xml中支持的可配置属性

 

wrap_content

需要注意的是,必须为Drawees指定layout_width和layout_height,但Drawees不支持wrap_content属性。原因是:占位图和所下载的图像可能大小并不一致,如果不一致,那么下载完成之后,若为warp_content,那么View将会重新layout,改变大小和位置,导致界面跳跃。(其他的Picasso和Volley是怎么处理的?是默认使用placeholder的大小吗?)

固定宽高比

如果需要设置固定的宽和高的比例,比如4:3,那么则在XML中,指定width,height设定为warp_content。在代码中指定比例,如下:

 
mSimpleDraweeView.setAspectRatio(1.33f);

Java中使用Drawee

更改图片大小

最简单的方法是,setImageURI即可。

mSimpleDraweeView.setImageURI(uri);

如果需要更加复杂的效果,可使用controller builder.

自定义Hierarchy

在一般情况下,XML中的配置参数可满足对hierarchy参数设置的需求,但有的时候,用户可能有更多的需求:

首先创建一个hierarchy builder的实例,然后构建hierarchy并传递给Drawee。

List  backgroundsList;
List  overlaysList;
// 创建Hierarchy Builder
GenericDraweeHierarchyBuilder builder =
    new GenericDraweeHierarchyBuilder(getResources());
// 创建Hierarchy
GenericDraweeHierarchy hierarchy = builder
    .setFadeDuration(300)
    .setPlaceholderImage(new MyCustomDrawable())
    .setBackgrounds(backgroundList)
    .setOverlays(overlaysList)
    .build();
// 设置给DraweeView
mSimpleDraweeView.setHierarchy(hierarchy);

需要注意的是,对于同一个View,不要调用超过1次的setHierarchy方法,即使该view已经被回收也不可以。 Hierarchy的创建是 耗时 的,因此要注意重复利用而不是反复创建。 需要更改显示的图片可通过调用setController或者setImageURI来实现。

修改Hierarchy的配置

Hierarchy中的配置可在运行期间进行多次修改,调用下列代码获取Hierarchy

GenericDraweeHierarchy hierarchy = mSimpleDraweeView.getHierarchy();
修改placeholder
hierarchy.setPlaceholderImage(R.drawable.placeholderId);
Drawable drawable; 
// create your drawable
hierarchy.setPlaceholderImage(drawable);
修改图片

修改图片缩放方式

hierarchy.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_INSIDE);

如果设置的缩放方式为 focusCrop , 那么需要设置焦点:

hierarchy.setActualImageFocusPoint(point);

给图片添加Color Filer

ColorFilter filter;
// create your filter
hierarchy.setActualImageColorFilter(filter);
设置Rounding
RoundingParams roundingParams = hierarchy.getRoundingParams();
roundingParams.setCornersRadius(10);
hierarchy.setRoundingParams(roundingParams);

Drawee Components

定义

Drawees中,除了真实的需要显示的图片之外,其他的属性均可通过XML进行配置,在XML中配置的属性,必须为Android drawable或者color资源。 这些属性也可通过GenericDraweeHierarchyBuilder在程序中进行配置,如果在程序中配置,那么配置的资源必须为Drawable的子类。 有些属性可以在程序运行中实时配置,这些拥有可配置的属性都在GenericDraweeHierarchy类中拥有对应的方法。

Actual

Actual即对应的需要显示的图片,使用URI指定,可为网络资源、本地文件、本地资源或者content provider。 Actual为controller的属性而不是hierarchy的属性。因此在一个Drawee中配置的Actual不会出现在其他的Drawee中。 可使用setImageURI来设置图片或者通过设置controller来设置。 除了sclale类型之外,hierarchy还公开了设置Acutal图片的其他的方法:

  • focus point,当设置scale type为focuesCrop时有效
  • color filter
  • 默认的scale type为:centerCrop

Placeholder

当Drawee出现在界面的时候,会首先显示 placeholder ,当调用 setControlelr 或者 setImageURI 设置Drawee显示的图像之后, placeholder 仍会显示直至图片加载完成。如果加载的图片为渐进式JPEG(progressive JPEG),可设置JPEG的清晰度阈值,当清晰度满足条件之后,则placeholder不再显示。

  • XML属性:placeholderImage
  • Hierarchy builder方法:setPlaceholderImage
  • Hierarchy方法:setPlaceholderImage
  • 默认值:transparent ColorDrawable
  • 默认sclale方式:centerInside

Failure

加载失败的时候,显示Failure

  • XML属性:failureImage
  • Hierarchy builder方法:setFailureImage
  • 默认值:*placeholder image*
  • 默认sclale方式:centerInside

Retry

当图片下载失败,且对应的controller设置 enable tap-to-retry 属性之后。 必须在 build your own Controller 中设置tap-to-retry属性: setTapToRetryEnabled(true) image piple会在用户点击之后重试,最高可重试4次。

  • XML属性:retryImage
  • Hierarchy builder方法:setRetryImage
  • 默认值:*placeholder image*
  • 默认sclale方式:centerInside

Progress Bar

如果设置该属性,则会在Drawee之上显示一层overlay,直至最终的图片加载完成。

  • XML属性:progressBarImage
  • Hierarchy builder方法:setProgressBarImage
  • 默认值:*placeholder image*
  • 默认sclale方式:centerInside

Backgrounds

Background会首先绘制,在hierarchy其余层次之下。 在XML中仅仅可以指定一个Background,但是在Java代码中可以指定多个,这种情况下,则会将第一绘制在地下,然后绘制剩下的Background。 Backgound images不支持scale-types,并且自动缩放至Drawee的尺寸。

  • XML属性:backgroundImage
  • Hierarchy builder方法:setBackground、setBackgrounds
  • 默认值:None
  • 默认sclale方式:N/A

Overlays

Overlays会最后绘制,在hierarchy其余层次之上。 在XML中仅仅可以指定一个Overlay,但是在Java代码中可以指定多个,这种情况下,则会将第一绘制在地下,然后绘制剩下的Overlay。 Overlay images不支持scale-types,并且自动缩放至Drawee的尺寸。

  • XML属性:overlayImage
  • Hierarchy builder方法:setOverlay、setOverlays
  • 默认值:None
  • 默认sclale方式:N/A

Pressed State Overlay

如果指定Pressed State Overlay,那么在用户点击Drawee区域之后,则会显示对应的Overlay。比如,Drawee在显示一个button图片,overlay可以用于在用户点击之后改变button的颜色。 不支持scale-types。

  • XML属性:pressedStateOverlayImage
  • Hierarchy builder方法:setPressedStateOverlay
  • 默认值:None
  • 默认sclale方式:N/A

Progress Bars

在应用程序中,最简单的使用progress bar的方式是在构建hierarchy的时候使用ProgressBarDrawable。

.setProgressBarImage(new ProgressBarDrawable())

通过该操作可在Drawee下面显示一个黑色蓝角的progress bar。

定义自己的progress bar

如果我们需要定义自己的progress bar,需要 注意 的是 为了当加载的时候,progress能够动态变化,需要覆写Drawable.onLevelChange方法

class CustomProgressBar extends Drawable {
   @Override
   protected void onLevelChange(int level) {
     // level is on a scale of 0-10,000
     // where 10,000 means fully downloaded

     // your app's logic to change the drawable's
     // appearance here based on progress
   }
}

Scaling

可用缩放类型

ScaleType Explanation
center Center the image in the view, but perform no scaling.
centerCrop Scales the image so that both dimensions will be greater than or equal to the corresponding dimension of the parent.
One of width or height will fit exactly.
The image is centered within parent’s bounds.
focusCrop Same as centerCrop, but based around a caller-specified focus point instead of the center.
centerInside Downscales the image so that it fits entirely inside the parent.
Unlike fitCenter, no upscaling will be performed.
Aspect ratio is preserved.
The image is centered within parent’s bounds.
fitCenter Scales the image so that it fits entirely inside the parent.
One of width or height will fit exactly.
Aspect ratio is preserved.
The image is centered within the parent’s bounds.
fitStart Scales the image so that it fits entirely inside the parent.
One of width or height will fit exactly.
Aspect ratio is preserved.
The image is aligned to the top-left corner of the parent.
fitEnd Scales the image so that it fits entirely inside the parent.
One of width or height will fit exactly.
Aspect ratio is preserved.
The image is aligned to the bottom-right corner of the parent.
fitXY Scales width and height independently, so that the image matches the parent exactly.
Aspect ratio is not preserved.
none Used for Android’s tile mode.

以上的ScleType与Android中提供的Scale Type类似,但不同的是Fresco中不提供 matrix,然而Fresco中提供 focusCrop以实现更好的效果。

如何设置Scale Type

Actual, placeholder, retry和failure图片均可在XML中配置,使用方法为:fresco:actualImageScaleType,我们同样也可以在GenericDraweeHierachyBuilder类中使用代码设置。 即使在hierarchy设置之后,我们仍然可以通过GenericDraweeHierarchy来配置。 然而, 不要使用 android:scaleType属性,同样 不要使用 .setScaleType方法,这两种方式对Drawees没有效果。

focusCrop

Android和Fresco均提供 centerCrop缩放类型,该类型很常用,但也会存在使用场景的尴尬时刻,比如图片的左上角为头像,则无法通过 centerCrop来获取。 通过设置focus point,我们可以设置获取图片的某些部分,比如我们设置focus point为图片的边上,比如(0.5f, 0f),Fresco可以保证,无论怎样扩展,该focus point均可以显示。 Focus points使用相对坐标系统,(0f, 0f)为左上角,(1f, 1f)为右下角。 如果focus point设置为(0.5f, 0.5f),那么则等于 centerCrop。 为了使用focus points,必须在XML中首先设置正确的scale type。

fresco:actualImageScaleType="focusCrop"

在Java代码中,必须程序 显式 设置正确的focus point。

PointF focusPoint;
// your app populates the focus point
mSimpleDraweeView
    .getHierarchy()
    .setActualImageFocusPoint(focusPoint);

Rounded Corners and Circles Edit on GitHub

在很多时候,APP需要图片有圆角或者是园。Drawee支持各种各样的变化,但不会引入由于复制bitmaps而引入的额外的内存开销。

图片可以以两种方式制作成圆角

  • circle,圆形,设置roundAsCircle为true
  • 如果图片是个直角,需要圆角,那么同时需要设置roundedCornerRadius。 图片是直角的情况下,支持为4个角分别设置4个不同的圆弧半径,但这种情况下只能在Java中设置而不是XML中设置。

怎么设置

图片可以通过两种方式来设置圆弧

  • BITMAP_ONLY:使用一个shader来绘制图片以及圆角。这是默认的生成圆角的办法。这种方法在针对actual image和placeholder的时候有效。针对其他的,比如failure和retry的图片,不会生成圆角。此外,这种生成圆角的方式不支持动画效果。
  • OVERLAY_COLOR :通过生成一个solid color的overlay来生成圆角,由调用者指定。在这种情况下,Drawee的背景颜色应该设置为固定的,且与solid color同样。可在XML中设置 roundWithOverlayColor或者在Java代码中调用setOverlayColr方法来设置这种效果。 ###XML 通过在XML中配置此类属性,可以将设置传递给RoundingParams。
 

Java代码

当创建hierarchy的时候,我们可以传递一个RoundingParams的实例给GenericDraweeHierarchyBuilder:

RoundingParams roundingParams = RoundingParams.fromCornersRadius(7f);
// alternatively use fromCornersRadii or asCircle
roundingParams.setOverlayColor(R.color.green);
genericDraweeHierarchyBuilder
    .setRoundingParams(roundingParams);

我们也可以在代码运行的时候修改大部分的rounding参数:

RoundingParams roundingParams = RoundingParams.fromCornersRadius(5f);
roundingParams.setBorder(R.color.red, 1.0);
roundingParams.setRoundAsCircle(true);
mSimpleDraweeView.getHierarchy().setRoundingParams(roundingParams);

注意

在使用BITMAP_ONLY(默认)情况下,有几种使用上的限制:

  • 并非所有的图片都会圆角处理,只有actual image和placeholder会被处理。当前Fresco的代码在修改支持对background进行处理。
  • 只有图片可转化为BitmapDrawable或者ColorDrawable可以被rounded处理。对NinePathDrawable、ShapeDrawable以及其他类似的Drawable不会被处理(无论是XML或者Java编程方式)
  • 动画不会被rounded。
  • 由于Android中BitmapShader的限制,如果image的大小无法覆盖住整个View,那么不会说不绘制任何东西,而是edges会被不断重复。一种变通的方法是设置不同的scale类型,比如centerCrop来保证整个View被覆盖住。

OVERLAY_COLOR模式并没有上述限制,但由于其模仿实现rounded的思路是通过在image之上覆盖一层图层的方法实现,因此仅仅在Drawee的背景设置为固定的相同的颜色的时候,具有较好的效果。

Drawee原本拥有CLIPPING模式,但已经取消。

最后,这些问题都可通过设孩子一个临时bitmap的方式解决,但这会引入额外的性能负担,并不推荐。 因此,在Android平台上,没有一个完美的解决方案来实现绘制圆角。

使用ControllerBuilder

SimaleDraweeView可以通过两种方法指定图片,最简单的方式为setImageURI。 如果希望对Drawee拥有更加详细的控制,可以使用DraweeController。

构建DraweeController

将image request传递给一个PipelineDraweeControllerBuilder,并为Controller设置更加详尽的设置。

ControllerListener listener = new BaseControllerListener() {...}

DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setUri(uri)
    .setTapToRetryEnabled(true)
    .setOldController(mSimpleDraweeView.getController())
    .setControllerListener(listener)
    .build();

mSimpleDraweeView.setController(controller);

注意 我们应当记住,当构建一个新的controller的时候,应当调用setOldController以避免不必要的内存开销。

自定义ImageRequest

在一些比较高级的应用场合,我们可能需要向pipeline传递一个ImageRequest,而不仅仅是一个URI。其中一个例子就是使用postprocessor。

Uri uri;
Postprocessor myPostprocessor = new Postprocessor() { ... }
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setPostprocessor(myPostprocessor)
    .build();

DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setImageRequest(request)
    .setOldController(mSimpleDraweeView.getController())
    // other setters as you need
    .build();

Progressive JPEGs 渐进式JPEGs

Fresco支持从网络上获取Progressive JPEGs。 渐进式JPEGs仅仅在从网络上获取的时候支持。本地的图片在一次解码中全部完成,因此并不需要这种需求。

构建image request

如果需要Fresco支持渐进式JPEG,那么必须在配置image request的时候明确指定渲染此类型的image。

Uri uri;
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setProgressiveRenderingEnabled(true)
    .build();
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setImageRequest(request)
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);

Fresco会考虑在以后的版本中,通过setImageURI支持渐进式JPEGs。

动画图像

Fresco支持 GIF 动画和 WebP 格式的动画。 Fresco 在Android 2.3+ 版本上支持 WebP 以及 WebP extended 格式的图片,而此类格式在Android原生版本中并未支持。

自动播放动画

如果希望当图片出现在屏幕上的时候立刻开始绘制,而当图片不显示的时候停止播放动画,可通过对image request进行配置实现:

Uri uri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setUri(uri)
    .setAutoPlayAnimations(true)
    . // other setters
    .build();
mSimpleDraweeView.setController(controller);

手动控制播放

如果希望手动控制播放,那么需要监听图片加载的过程。

ControllerListener controllerListener = new BaseControllerListener () {
    @Override
    public void onFinalImageSet(
        String id,
        @Nullable ImageInfo imageInfo,
        @Nullable Animatable anim) {
    if (anim != null) {
      // app-specific logic to enable animation starting
      anim.start();
    }
};

Uri uri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setUri(uri)
    .setControllerListener(controllerListener)
    // other setters
    .build();
mSimpleDraweeView.setController(controller);

此外,controller暴露Animatable的接口。如果non-null,我们可通过该接口直接控制动画播放。

Animatable animatable = mSimpleDraweeView.getController().getAnimatable();
if (animatable != null) {
  animatable.start();
  // later
  animatable.stop();
}

局限性

动画Animations不支持postprocessors功能。

Requesting Multiple Images (Multi-URI)

这种方法要求我们实现自己的 image request , 主要有以下几种使用场景:

从低分辨率-->高分辨率

有的情况下,高分辨率的图片其体积较大,这个时候直接下载的话,就会导致图片很长时间无法显示,在这种情况下,可以先设置一个低分辨率的图片,然后当高分辨率的图片下载完成之后显示。

Uri lowResUri, highResUri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setLowResImageRequest(ImageRequest.fromUri(lowResUri))
    .setImageRequest(ImageRequest.fromUri(highResUri))
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);

使用缩略预览图

本选项仅供本地URI使用,且仅供JPEG图像使用 如果JPEG图像在其EXIF元信息中保存了缩略图,那么image pioeline可以先返回该缩略图。那么我们的Drawee则会先显示缩略预览图,当完整的图片加载完成并成功解码之后,会更改为显示完整的图像。

Uri uri;
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setLocalThumbnailPreviewsEnabled(true)
    .build();

DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setImageRequest(request)
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);

加载第一张下载成功的图片

一般时候,一张图片仅仅对应一个URI,但有的场景也是例外的。

比如,我们可能拥有一张从Camera获取的已经上传过的图像。原始的图像直接上传的话,体积实在太大,因此需要首先将该图像压缩。在这种场景下,我们就可以先使用压缩的图像(local-downscaled-uri),如果加载失败之后,则加载Camera原始图像(local-original-uri),即使失败了,那么则尝试加载网络上的图像(network-uploaded-uri)。

正常情况下,image pipeline会首先从memory cache中搜索图像,然后是disk cache,再然后才是network或者其他的资源。我们可以使得pipeline首先搜索memory cache中所有的图像,而不是一次一次依次操作,只有在没有找到任何图片的时候,再从disk cache中进行搜索。如果仍然没有,则会继续发出其他的请求。

实现起来也很简单,仅仅需要创建一个image request的数组,然后传递给controller的builder。

Uri uri1, uri2;
ImageRequest request = ImageRequest.fromUri(uri1);
ImageRequest request2 = ImageRequest.fromUri(uri2);
ImageRequest[] requests = { request1, request2 };

DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setFirstAvailableImageRequests(requests)
    .setOldController(mSimpleDraweeView.getController())
    .build();
mSimpleDraweeView.setController(controller);

以上的两个请求中,仅仅有一个图像可以得到显示,主要取决于哪张图片最先加载成功,无论是memory、disk或者network。pipeline会假设requests数组的顺序为用户所期待的搜索顺序。

指定自定义DataSource Supplier

为了更加灵活的使用,我们可以在构建Drawee controller的时候,为Drawee controller指定一个自定义的DataSource Supplier。我们可以自定义一个DataSource Supplier或者是组合一些已经有的实现。可以参考 FirstAvailableDataSourceSupplier和InscreasingQualityDataSourceSupplier作为例子。参考AbastractDraweeControllerBuilder来参考怎么组合不同的DataSource Supplier。

监听Downloads Events

原因 当图片下载完成的时候,我们可能需要执行一些特殊的操作:比如,显示其他的图片,显示标题等等;或者说当网络故障,提示用户等等。

图片的加载是异步进行的,所以需要额外的操作来监听DraweeController发布的events事件,其原理就是controller listener。

注意 此处不允许对图片本上进行修改,如果需要对图片本身进行修改,使用Postprocessor即可。

使用

使用很简单,只要实现ControllerListener接口即可,我们建议集成BaseControllerListener来实现。

ControllerListener controllerListener = new BaseControllerListener () {
    @Override
    public void onFinalImageSet(
        String id,
        @Nullable ImageInfo imageInfo,
        @Nullable Animatable anim) {
      if (imageInfo == null) {
        return;
      }
      QualityInfo qualityInfo = imageInfo.getQualityInfo();
      FLog.d("Final image received! " + 
          "Size %d x %d",
          "Quality level %d, good enough: %s, full quality: %s",
          imageInfo.getWidth(),
          imageInfo.getHeight(),
          qualityInfo.getQuality(),
          qualityInfo.isOfGoodEnoughQuality(),
          qualityInfo.isOfFullQuality());
    }

    @Override 
    public void onIntermediateImageSet(String id, @Nullable ImageInfo imageInfo) {
      FLog.d("Intermediate image received");
    }

    @Override
    public void onFailure(String id, Throwable throwable) {
      FLog.e(getClass(), throwable, "Error loading %s", id)
    }
};

Uri uri;
DraweeController controller = Fresco.newDraweeControllerBuilder()
    .setControllerListener(controllerListener)
    .setUri(uri);
    // other setters
    .build();
mSimpleDraweeView.setController(controller);

onFinalImageSet or onFailure 对于所有的image加载工作都会起作用。

对于渐进式JPEG,如果允许渐进显示,那么每次解码之后,都会调用onIntermediateImageSet方法。

Resizing and Rotating

调整大小、旋转是图像显示中经常使用的功能,要实现功能,需要手工直接指定image request。

Resizing Images

Resizing VS Scaling

  • Resizing:是一种软件操作,在pipeline中执行,返回一个完整的拥有不同尺寸的新的图像。
  • Scaling:是一种绘制操作,一般采用硬件加速。图像本身大小不变。

Resize和Scaling如何选择? Resizing并不经常使用,Scaling经常使用,即使在resizing中,也会使用Scaling。

Resizing在使用上有几个局限性:

  • Resizing不会返回比原始图像更大的image,Resizing操作只会使得图像更小。
  • 当前仅仅JPEG支持resized。
  • 对于图像的Resize的尺寸控制,仅仅有个大约的数字。图像无法调制大小至非常精确的尺寸,这就意味着,图像即使经过Resized,那么在显示的时候,也需要缩放才能使用View大小。
  • 仅仅支持一下的Resizing尺寸: N/8 with 1 <= N <= 8.
  • Resize调整大小是纯软件操作,相比硬件加速的scaling,速度要慢很多。R

相比之下,Scaling没有以上的局限。Scaling使用Android内置的缩放算法。在Android4.0以上的设备上可以使用GPU硬件加速。在大多数情况下,这也是最快以及最有效的将图片调整至合适尺寸的方法。为一的缺点是,如果图片的尺寸要远远大于界面的话,内存就浪费掉了。

那为什么还要使用Resizing呢?这是一种权衡。我们仅仅在图像的大小远大约View的大小,可以通过Resizing来节约内存的使用。一个具体的例子比如说,我们需要在1280*800(大约1MP)的分辨率上显示相机拍摄的一个8MP的相片。那么这个8MP的图像在ARGB排列的时候大约占据32MB的内存,如果将尺寸Resized,那么可以最低占用4MB的内存。

如果图像是从网络上获取的,在resizing之前,应当考虑能否从网络中获取对应大小尺寸的图像。如果服务器可以返回一个较小尺寸的图像,不要尝试获取8MP的图像。在开发中,要考虑使用者的数据流量消耗。除此之外,获取较小尺寸的图像,还可以节省内存存储空间和CPU时间。如果服务器无法返回一个合适较小尺寸的图片,或者正在使用本地图库,那么应该考虑resizing。除了这些情况之外,我们应当首先考虑使用Rescaling。如果需要Scale,仅仅需要设定SimpleDraweeView的layout_width/layout_height即可。

Resizing

Resizing不会修改原先的文件,而是对内存中的图像进行解码。当前的Fresco仅仅支持JPEG调整尺寸。为ImageRequest传递ResizeOptions即可。

Uri uri = "file:///mnt/sdcard/MyApp/myfile.jpg";
int width = 50, height = 50;
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setResizeOptions(new ResizeOptions(width, height))
    .build();
PipelineDraweeController controller = Fresco.newDraweeControllerBuilder()
    .setOldController(mDraweeView.getController())
    .setImageRequest(request)
    .build();
mSimpleDraweeView.setController(controller);

Auto-rotation

自动旋转

ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setAutoRotateEnabled(true)
    .build();
// as above

对图像进行编辑

原因

有些情况下,需要对从服务器或者本地获取的图片进行修改,最好的方法是使用Postprocessor,其中组好的实现方式是集成BasePostprocessor类。 例子:在图像上添加红色网:

Uri uri;
Postprocessor redMeshPostprocessor = new BasePostprocessor() { 
  @Override
  public String getName() {
    return "redMeshPostprocessor";
  }

  @Override
  public void process(Bitmap bitmap) {
    for (int x = 0; x  < bitmap.getWidth(); x+=2) {
      for (int y = 0; y  < bitmap.getHeight(); y+=2) {
        bitmap.setPixel(x, y, Color.RED);
      }
    }
  }
}

ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setPostprocessor(redMeshPostprocessor)
    .build();

PipelineDraweeController controller = (PipelineDraweeController) 
    Fresco.newDraweeControllerBuilder()
    .setImageRequest(request)
    .setOldController(mSimpleDraweeView.getController())
    // other setters as you need
    .build();
mSimpleDraweeView.setController(controller);

需要明确

  • 图像进入postprocessor得到修改,但需要注意的是,cache中的图像不会受到此处postprocessor的影响。在Android4.x及以下版本,修改后的图像与原图像一起保存在Java堆之外。
  • 为了每次都能够展现同样的编辑效果,使用的时候必须每次都指定postprocessor。对于不同请求的同一个图像,可以使用不同的postprocessor。
  • 动态图像不支持postprocessor

复制图像

可能有些原因导致无法针对原图像直接编辑,如果这种情况下,BasePostprocessor拥有另外的方法签名已解决这个问题,此处例子展示了如何将bitmap做水平翻转。

@Override
public void process(Bitmap destBitmap, Bitmap sourceBitmap) {
  for (int x = 0; x  < destBitmap.getWidth(); x++) {
    for (int y = 0; y  < destBitmap.getHeight(); y++) {
      destBitmap.setPixel(destBitmap.getWidth() - x, y, sourceBitmap.getPixel(x, y));
    }
  }
}

原图像与目标图像大小完全一样。 注意:

  • 不要修改元sourceBitmap,在以后的版本中,如果将sourceBitmap进行修改将会抛出异常。
  • 不要保存对任一个bitmap对象的引用,此处的bitmap对象均由pipeline管理,destBitmap生命周期与Drawee或者DataSource同样。

复制图像,且图像尺寸不一致

如果postprocessed图像需要一个不一样尺寸的图像,此处有第三个方法签名:使用PlatformBitmapFacotry类来安全的创建一个保存在Java heap之外的自定义尺寸的Bitmap。 此处例子展示如何创建一个1/4尺寸的图像:

@Override
public CloseableReference  process(
    Bitmap sourceBitmap,
    PlatformBitmapFactory bitmapFactory) {
  CloseableReference  bitmapRef = bitmapFactory.createBitmap(
      sourceBitmap.getWidth() / 2,
      sourceBitmap.getHeight() / 2);
  try {
    Bitmap destBitmap = bitmapRef.get();
     for (int x = 0; x  < destBitmap.getWidth(); x+=2) {
       for (int y = 0; y  < destBitmap.getHeight(); y+=2) {
         destBitmap.setPixel(x, y, sourceBitmap.getPixel(x, y));
       }
     }
     return CloseableReference.cloneOrNull(bitmapRef);
  } finally {
    CloseableReference.closeSafely(bitmapRef);
  } 
}

注意,必须遵守closeable reference的使用规范。 不要使用 Android的Btimap.createBitmap的方法,此方法创建的图像保存在Java heap当中。

Which to override?

Do not override more than one of the three process methods. Doing so can produce unpredictable results.

Caching postprocessed images

可以将postprocess处理的图像进行保存,为了实现这个效果,自定义的postprocessor必须实现getPostprocessorCacheKey方法并且返回一个not null的值。 为了使得cache命中,在以后的request中的postprocessor必须为同样的类,并且返回同样的cache key。如果不是,那么将会覆盖之前生成的cache entry。

public class OperationPostprocessor extends BasePostprocessor {
  private int myParameter;

  public OperationPostprocessor(int param) {
    myParameter = param;
  }

  public void process(Bitmap bitmap) { 
    doSomething(myParameter);
  }

  public CacheKey getPostprocessorCacheKey() {
    return new MyCacheKey(myParameter);
  }
}

如果希望cache始终命中,则设置getPostprocessorCacheKey方法返回固定常量,如果不想使cache命中,则返回null即可。

Repeated Postprocessors

可能需要对同一张图片重复处理多次。这种情况下,仅仅需要集成BaseRepeatedPostprocessor即可。 此处的例子允许对图片上的网格在任意时间变换颜色。

public class MeshPostprocessor extends BaseRepeatedPostprocessor { 
  private int mColor = Color.TRANSPARENT;

  public void setColor(int color) {
    mColor = color;
    update();
  }

  @Override
  public String getName() {
    return "meshPostprocessor";
  }

  @Override
  public void process(Bitmap bitmap) {
    for (int x = 0; x  < bitmap.getWidth(); x+=2) {
      for (int y = 0; y  < bitmap.getHeight(); y+=2) {
        bitmap.setPixel(x, y, mColor);
      }
    }
  }
}
MeshPostprocessor meshPostprocessor = new MeshPostprocessor();

/// setPostprocessor as in above example

meshPostprocessor.setColor(Color.RED);
meshPostprocessor.setColor(Color.BLUE);

需要注意的是,每一个image request仍然需要设置Postprocessor。

Image Requests

如果简单的通过Image URI设置图像,那么使用ImageRequest.fromURI方法即可,但如果需要更多的设置,则需要ImageRequestBuilder;

Uri uri;

ImageDecodeOptions decodeOptions = ImageDecodeOptions.newBuilder()
    .setBackgroundColor(Color.GREEN)
    .build();

ImageRequest request = ImageRequestBuilder
    .newBuilderWithSource(uri)
    .setAutoRotateEnabled(true)
    .setLocalThumbnailPreviewsEnabled(true)
    .setLowestPermittedRequestLevel(RequestLevel.FULL_FETCH)
    .setProgressiveRenderingEnabled(false)
    .setResizeOptions(new ResizeOptions(width, height))
    .build();

Fields in ImageRequest

Filed Description
uri the only mandatory field. See Supported URIs.
autoRotateEnabled whether to enable auto-rotation.
progressiveEnabled whether to enable progressive loading.
postprocessor component to postprocess the decoded image.
resizeOptions desired width and height. Use with caution. See Resizing.

Lowest Permitted Request Level

image pipeline根据确定的顺序查找图像

  • Check the bitmap cache. This is nearly instant. If found, return.
  • Check the encoded memory cache. If found, decode the image and return.
  • Check the "disk" (local storage) cache. If found, load from disk, decode, and return.
  • Go to the original file on network or local file. Download, resize and/or rotate if requested, decode, and return. For network images in particular, this will be the slowest by a long shot.

setLowestPermittedRequestLevel方法允许设置pipeline可以走多远,其数值为:

  • BITMAP_MEMORY_CACHE
  • ENCODED_MEMORY_CACHE
  • DISK_CACHE
  • FULL_FETCH

自定义View

DraweeHolders

总有时候DraweeViews无法满足我们的需求。比如我们可能需要在Image所在的View上添加额外的内容,我们可能需要在一个View中显示多个图片。Fresco提供了各种各样的类用于功能扩展:

  • DraweeHolder -> 单个图片
  • MultiDraweeHolder -> 多个图片

自定义View需要做的事情

Android绘制View中的组件,且这些时间仅仅在Android系统内部调用,DraweeViews使用这些时间可以更好的管理内存,提高内存的高效使用。但我们使用holders的时候,必须手动实现其中的部分功能。

处理attach/detach事件

如果不进行该操作,我们的自定义holder可能会出现内存泄露

当Android界面不再绘制图片的时候,图片也就没有在内存中保存的意义。Drawees通过监听detach事件来及时的释放内存。当Android重新绘制的时候,这些图片也会自动恢复以供显示。 这些操作在DraweeView中是自动处理的,但在自定义View中需要我们手动处理4个系统调用,并传递给DraweeHolder,下面为例子: ``` DraweeHolder mDraweeHolder;

@Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); mDraweeHolder.onDetach(); }

@Override public void onStartTemporaryDetach() { super.onStartTemporaryDetach(); mDraweeHolder.onDetach(); }

@Override public void onAttachedToWindow() { super.onAttachedToWindow(); mDraweeHolder.onAttach(); }

@Override public void onFinishTemporaryDetach() { super.onFinishTemporaryDetach(); mDraweeHolder.onAttach(); } ```

处理touch events

如果我们在Drawee中允许tap-to-retry功能,那么需要监听屏幕的触摸事件。

@Override
public boolean onTouchEvent(MotionEvent event) {
  return mDraweeHolder.onTouchEvent(event) || super.onTouchEvent(event);
}

定制onDraw

我们必须调用以下代码Drawable drawable = mDraweeHolder.getTopLevelDrawable(); drawable.setBounds(...);否则Drawee不会显示在界面上。

  • Do not downcast this Drawable.
  • Do not translate it.

其他需要做的工作

设置Drawable.Callback

// When a holder is set to the view for the first time,
// don't forget to set the callback to its top-level drawable:
mDraweeHolder = ...
mDraweeHolder.getTopLevelDrawable().setCallback(this);

// In case the old holder is no longer needed,
// don't forget to clear the callback from its top-level drawable:
mDraweeHolder.getTopLevelDrawable().setCallback(null);
mDraweeHolder = ...
Override verifyDrawable:
@Override
protected boolean verifyDrawable(Drawable who) {
  if (who == mDraweeHolder.getTopLevelDrawable()) {
    return true;
  }
  // other logic for other Drawables in your view, if any
}

Make sure invalidateDrawable invalidates the region occupied by your Drawee.

Constructing a DraweeHolder

组织构造函数

推荐构建函数如下安排:

  • 覆写View的全部3个构造函数
  • 每个构造函数中调用父类的构造方法,然后调用私有的init方法
  • 所有的初始化操作放在 init 方法中完成 这样做的初衷是:不要在一个构造方法中调用其他的构造方法。同时保证了无论调用哪个构造方法,其初始化顺序都是正确的。我们的holder在init方法中创建。

创建Holder

如果可能,总是在自定义View创建之后立刻创建Drawee。hierarchy的创建开销较大,因此最好仅仅操作一次。

class CustomView extends View {
  DraweeHolder  mDraweeHolder;

  // constructors following above pattern

  private void init() {
    GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources());
      .set...
      .set...
      .build();
    mDraweeHolder = DraweeHolder.create(hierarchy, context);
  }
}

设置图片

使用controller build实现,但要调用holder的setController方法而不是View中的方法。

DraweeController controller = Fresco.newControllerBuilder()
    .setUri(uri)
    .setOldController(mDraweeHolder.getController())
    .build();
mDraweeHolder.setController(controller);

MultiDraweeHolder

MultiDraweeHolder  mMultiDraweeHolder;

private void init() {
  GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(getResources());
    .set...
    .build();
  mMultiDraweeHolder = new MultiDraweeHolder ();
  mMultiDraweeHolder.add(new DraweeHolder (hierarchy, context));
  // repeat for more hierarchies
}

其他与DraweeHolder类似。

一些陷阱

Don't use ScrollViews

如果想要实现滑动一列图片的功能,要使用RecyclerView, ListView, or GridView.这些可以复用View,因此Fresco可以知道什么时候Drawee显示了,什么时候不显示了,但ScrollView无法实现,ScrollView会始终保持图像直至Fragment或者Activity销毁,因此如果使用ScrollView,那么程序更有可能遇到OOM的问题。

Don't downcast

It is tempting to downcast objects returns by Fresco classes into actual objects that appear to give you greater control. At best, this will result in fragile code that gets broken next release; at worst, it will lead to very subtle bugs.

Don't use getTopLevelDrawable

DraweeHierarchy.getTopLevelDrawable() should only be used by DraweeViews. Client code should almost never interact with it.

The sole exception is custom views. Even there, the top-level drawable should never be downcast. We may change the actual type of the drawable in future releases.

Don't re-use DraweeHierarchies

Never call DraweeView.setHierarchy with the same argument on two different views. Hierarchies are made up of Drawables, and Drawables on Android cannot be shared among multiple views.

Don't use Drawables in more than one DraweeHierarchy

This is for the same reason as the above. Drawables cannot be shared in multiple views.

You are completely free, of course, to use the same resourceID in multiple hierarchies and views. Android will create a separate instance of each Drawable for each view.

Don't set images directly on a DraweeView

Currently DraweeView is a subclass of Android's ImageView. This has various methods to set an image (such as setImageBitmap, setImageDrawable)

If you set an image directly, you will completely lose your DraweeHierarchy, and will not get any results from the image pipeline.

Don't use ImageView attributes or methods with DraweeView

Any XML attribute or method of ImageView not found in View will not work on a DraweeView. Typical cases are scaleType, src, etc. Don't use those. DraweeView has its own counterparts as explained in the other sections of this documentation. Any ImageView attrribute or method will be removed in the upcoming release, so please don't use those.

Image Pipeline

Introduction

流水线主要进行以下的操作: 1. 检查bitmap cache,如果存在,则返回。 2. 切换至其他的线程。 3. 检查memory cache,如果存在,则对图像解码、变换、返回,并保存至bitmap cache。 4. 检查disk cache。如果存在,则对图像解码、变换、返回,并保存至memory cache 和 bitmap cache。 5. 检查网络或者其他位置的资源,如果存在,则对图像解码、变换、返回,并保存至disk cache、memory cache 和 bitmap cache。

Fresco Image Pipeline

上图中,'disk' cache绘制在encode memory cache中来保持流程图正解。

流水线支持从本地文件、网络中获取。支持PNG、GIF、WebP、JPEG文件。

旧设备上支持WebP

Android在3.0之后才支持WebP,Extended WebP在Android 4.1.2之后才支持。如果设备不支持WebP,image pipeline会将其转换为JPEG,这样就可以在Android2.3以上的系统中均可使用WebP。

配置Image Pipeline

大部分程序可通过 Fresco.initialize(context)来简单配置Fresco。

如果程序需要更加自定义的配置,可使用ImagePipelineConfig类。

ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context)
    .setBitmapMemoryCacheParamsSupplier(bitmapCacheParamsSupplier)
    .setCacheKeyFactory(cacheKeyFactory)
    .setEncodedMemoryCacheParamsSupplier(encodedCacheParamsSupplier)
    .setExecutorSupplier(executorSupplier)
    .setImageCacheStatsTracker(imageCacheStatsTracker)
    .setMainDiskCacheConfig(mainDiskCacheConfig)
    .setMemoryTrimmableRegistry(memoryTrimmableRegistry) 
    .setNetworkFetchProducer(networkFetchProducer)
    .setPoolFactory(poolFactory)
    .setProgressiveJpegConfig(progressiveJpegConfig)
    .setRequestListeners(requestListeners)
    .setSmallImageDiskCacheConfig(smallImageDiskCacheConfig)
    .build();
Fresco.initialize(context, config);

理解Suppliers

在上述参数配置中,很多都配置对应的Supplier的实例,而不是对应的实例。这样看起来可能会麻烦些,但会带来更多的收益,我们可以在程序运行中动态切换Fresco的行为。比如Memory caches,可以每5分钟检查其Supplier。 如果不需要动态调整这些参数,那么可设置Supplier每次返回都一个实例。

Supplier  xSupplier = new Supplier () {
  public X get() {
    return new X(xparam1, xparam2...);
  }
);
// when creating image pipeline
.setXSupplier(xSupplier);

线程池

默认情况下,Image pipeline使用3个线程池。

  • 3个线程用于下载;
  • 2个线程用于所有的disk操作:本地文件读取、disk cache相关
  • 2个线程用于CPU敏感操作:decodes、transforms、后台操作

可通过设置自己的network layer来自定义网络的行为。 对于其他的修改,可通过设置ExecutorSupplier完成。

使用MemoryTrimmableRegistry

如果我们的程序监听系统内存事件,那么可以将这些事件传递给Fresco以优化memory cache; 最简单的方法是覆写 Avtivity.onTrimMemory。也可以使用ComponmentCallbacks2的子类实现。 我们应当使用MemoryTrimableRegistry。该类用于保存MemoryTrimmable的实例-Fresco的cache也在其中。当接收到系统内存事件的时候,依次调用每个trimmable的MemoryTrimable方法。

配置memory caches

bitmap cache以及解码memory cache使用MemoryCacheParams中的Supplier配置。

配置disk cache

使用构造者模式创建DiskCacheConfig实例:

DiskCacheConfig diskCacheConfig = DiskCacheConfig.newBuilder()
   .set....
   .set....
   .build()

// when building ImagePipelineConfig
.setMainDiskCacheConfig(diskCacheConfig)

追踪Cache统计信息

If you want to keep track of metrics like the cache hit rate, you can implement the ImageCacheStatsTracker class. This provides callbacks for every cache event that you can use to keep your own statistics.

Caching

总共三种Cache

Bitmap cache

bitmap cache保存Android Bitmap对象。这里面的对象都已经解码完成,准备用于展示或者用于postprocess。

在Android 4.x或者更低的版本中,bitmap的cache数据放在ashmen heap,而不是Java heap中。这意味着这些图像并不需要运行额外的GC,避免降低app的运行速度。

Android 5.0中已经相比之前的版本对内存管理进行了很好的优化,所以将bitmap的cache数据放在Java heap中是合适的。

当app切换到背景之后,bitmap cache则会被清空。

Encoded memory cache

此处Cache保存了原始的压缩的图像格式。此处的图像在展示之前必须首先解码才行。

如果需要其他的变换,比如resizing、rotating或者转码,那么这些操作在decode解码之前进行。

当app切换到背景之后,encoded memory cache也会被清空。

Disk cache

Disk cache即保存在local storage中的cache。

与Encoded Memory Cache一样,Disk cache中保存的也是压缩的图像,与encoded memory cache一样,图像在展示之前也必须解码。

与Bitmap cache/Encoded memory cache不同,在app切换到背景之后,Disk cache中的数据并不会被清空。用户可以在Android Setting界面中将这部分数据清除。

从cache中删除数据

可以使用ImagePipeline中的方法从cache中清除单独的cache数据。

ImagePipeline imagePipeline = Fresco.getImagePipeline();
Uri uri;
imagePipeline.evictFromMemoryCache(uri);
imagePipeline.evictFromDiskCache(uri);

// combines above two lines
imagePipeline.evictFromCache(uri);

evictFromDiskCache(Uri) 默认情况下假设使用的是默认的cache key factory。如果自定义了Cache Key,那么则需要调用 evictFromDiskCache(ImageRequest)方法。

清空cache

ImagePipeline imagePipeline = Fresco.getImagePipeline();
imagePipeline.clearMemoryCaches();
imagePipeline.clearDiskCaches();

// combines above two lines
imagePipeline.clearCaches();

使用一个disk cache还是两个?

大多数应用程序仅仅使用1个disk cache。但是在某些使用场景中,我们需要将尺寸的图片放在一个单独的cache中以避免由于cache中存放了过多的较大的尺寸的图片而迅速将cache填满,进而导致这小较小的图片从cache中被清楚。

为了实现这种思路,则在配置image pipeline中调用setMainDiskCacheConfigsetSmallImageDiskCacheConfig方法即可。

怎么定义什么叫small,小的图片?我们的程序自己定义,我们在构造一个image request的时候,设置其ImageType:

ImageRequest request = ImageRequest.newBuilderWithSourceUri(uri)
    .setImageType(ImageType.SMALL)

如果我们仅仅需要1个cache,那么我们应该避免调用setSmallImageDiskCacheConfig。在这种情况下,pipeline会默认使用同一个cache,设置的ImageType并不会起作用。

调整caches - Trimming the caches

当配置image pipeline的时候,我们可以设置每一个cache的最大空间。但是在某些场景下,我们仍然希望降低cache所占的空间。

Fresco的cache实现了DiskTrimmable或者MemoryTrimmable接口。这些接口中有钩子可以使得我们在app中压缩cache。

然后我们的程序则可以调用DiskTrimmableRegistry和MemoryTrimmableRegistry接口。

These objects must keep a list of trimmables. They must use app-specific logic to determine when memory or disk space must be preserved. They then notify the trimmable objects to carry out their trims.

直接使用Image Pipeline

在大多数情况下,Image Pipeline不需要单独使用,大多数的APP上使用Drawee来处理与Fresco的交互即可。

由于内存使用的原因,在Fresco上直接使用image pipeline是比较困难的。Drawee可以自动追踪我们的图片是否需要在内存中存储。当不需要的时候,Fresco可以将图片自动转存,当需要的时候图片可以自动将图片重新加载到内存中。如果我们需要直接使用image pipeline,则需要手动处理这些逻辑。

image pipeline中返回的实例都是ClosableReference的包装类。Drawee在处理完成的时候会调用.close()方法。如果我们不是直接使用Drawee,那么也需要这样处理。

Java的GC会在图片out of scope的时候自动回收内存,但这样回收的太迟,而且GC的代价较高,而且对于较大的实例会导致较大的性能损失,尤其是对于Android 4.x即以下的版本中,Android没有单独的内存用于存储Bitmap,这会特别突出。

调用pipeline

我们必须构建一个image request,然后将这个image request传递给ImagePipeline:

ImagePipeline imagePipeline = Fresco.getImagePipeline();
DataSource > 
    dataSource = imagePipeline.fetchDecodedImage(imageRequest);

参考DataSource来看怎么从其中获取数据。

跳过解码

如果不需要解码,而是直接保持其中压缩的格式,那么仅仅使用fetchEncodedImage即可:

DataSource > 
    dataSource = imagePipeline.fetchEncodedImage(imageRequest);

直接使用bitmap cache中的数据

与其他操作不同,bitmap cache的查找工作是直接在UI线程中执行的,如果Bitmap在内存中,那么可以直接获取使用:

DataSource > dataSource =
    imagePipeline.fetchImageFromBitmapCache(imageRequest);
try {
  CloseableReference  imageReference = dataSource.getResult();
  if (imageReference != null) {
    try {
      CloseableImage image = imageReference.get();
      // do something with the image
    } finally {
      CloseableReference.closeSafely(imageReference);
    }
  }
} finally {
  dataSource.close();
}

注意,不要避免finally块中的调用dataSource.close()。

Prefetching

对图片进行预取可以降低用户等待的时间,进而优化用户体验,但需要注意的是,这也是一种trade-off。预取图片会增加用户数据流量的使用,增加CPU和内存的使用。因此,对于大多数APP来说,不建议开启图片的预取。

尽管如此,image pipeline允许我们将图片提前预取至disk或者bitmap cache。这两种方案都会增加网络数据的使用,但是区别在于disk cache并不会对图片进行解码,因此其CPU的占用率更低。

预取至disk cache:

imagePipeline.prefetchToDiskCache(imageRequest);

预取至bitmap cache中:

imagePipeline.prefetchToBitmapCache(imageRequest);

DataSources和DataSubscribers

DataSource类似于Java的Future,用于异步返回计算结果。DataSource与Future不同的是,DataSource可以返回单个命令对于的一系列运算结果,而不仅仅是一个。

当提交了一个image request之后,image pipeline会返回一个data source。为了从其中获取运算结果,因此需要使用DataSourceSubScriber。

I just want a bitmap...

如果我们向image request提交的请求仅仅decoded image - Android Bitmap,我们可以充分利用BaseBitmapDataSubscriber:

dataSource.subscribe(new BaseBitmapDataSubscriber() {
    @Override
    public void onNewResultImpl(@Nullable Bitmap bitmap) {
       // You can use the bitmap in only limited ways
      // No need to do any cleanup.
    }

    @Override
    public void onFailureImpl(DataSource dataSource) {
      // No cleanup required here.
    }
  },
  executor);

使用起来很简单,但有几个使用上需要注意的地方: * 对于animated images 动画图像无法使用subscriber * 不能将bitmap变量赋值给onNewResultImpl方法之外的任何变量。原因是,当subscriber执行完成之后,image pipeline则会复用bitmap并且释放内存。如果在其后绘制图片,则会导致应用程序崩溃并抛出IllegalStateException。 * 将Bimap传递给Android通知或者remote view是安全的。如果Android需要使用Bitmap并传递给系统调用,Fresco会将Bitmap数据在ashmem中复制一份数据。所以Fresco在这种情况下可以自动清理内存。

通用解决方案

如果需要使用bitmap,那么不能直接使用Bitmap,可以通过cloaseable reference和BaseDataSubscriber:

DataSubscriber dataSubscriber =
    new BaseDataSubscriber >() {
  @Override
  public void onNewResultImpl(
      DataSource > dataSource) {

    if (!dataSource.isFinished()) {
      FLog.v("Not yet finished - this is just another progressive scan.");
    }  

    CloseableReference  imageReference = dataSource.getResult();
    if (imageReference != null) {
      try {
        CloseableImage image = imageReference.get();
        // do something with the image
      } finally {
        imageReference.close();
      }
    }
  }
  @Override
  public void onFailureImpl(DataSource dataSource) {
    Throwable throwable = dataSource.getFailureCause();
    // handle failure
  }
};

dataSource.subscribe(dataSubscriber, executor);

If you want to deviate from the example above and assign the CloseableReference to another variable somewhere else, you can. Just be sure to follow the rules.

Closeable References

大多数的应用程序进需要使用Drawee即可,而且不需要担心Drawee关闭的问题。

由于Java是支持垃圾回收的编程语言,因此大多数开发者都是任意创建对象,并且理所当然的认为这些对象最后都会从内存中释放。

但实际上,在Android 5.0以上,这种情况才算得上理想,因此在之前的版本中,这种方法并不适合处理Bitmaps。在Android 5.0以前的版本中,Bitmap会占用APP共享内存中的一大部分,由于Bitmap的存在,会大大增加系统GC的频率,降低程序的执行效果。

Bitmap的问题,使得开发者更加思念C++以及其智能指针,比如Boost。

Fresco的解决方案是CloseableReference类。为了保证能够正确的使用CloseableReference,必须遵守以下的规则:

  1. 调用者拥有CloseableReference的引用。 比如,此处我们创建了一个CloseableReference引用,但由于我们将其传递你给caller,因此该CloseableReference引用的所有权移交给该caller:
CloseableReference  foo() {
  Val val;
  return CloseableReference.of(val);
}
  1. 引用的所有者在使用完CloseableReference引用之后,必须调用close方法释放该引用。

例子中,我们创建了一个CloseableReference引用,但是没有将其给传递给调用者,因此我们必须手动关闭该引用。

void gee() {
  CloseableReference  ref = foo();
  try {
    haa(ref);
  } finally {
    ref.close();
  }
}

一般而言,finally块最适合完成该操作。

  1. 其他的,除了CloseableReference引用的所有者之外的,其他代码不能关闭引用。 例子中,我们从参数中获取到一个CloseableReference引用。但caller仍然是其所有者,因此我们不能调用对应的close方法。
void haa(CloseableReference  ref) {
  Log.println("Haa: " + ref.get());
}
  1. 在赋值之前,要记得调用clone方法复制CloseableReference引用。 如果需要保持CloseableReference引用,需要调用clone方法:
class MyClass {
  CloseableReference  myValRef;

  void mmm(CloseableReference  ref) {
    myValRef = ref.clone();
  };
  // caller can now safely close its copy as we made our own clone.

  void close() {
    CloseableReference.closeSafely(myValRef);
  }
}
// Now the caller of MyClass must close it!

如果在内部类中使用该CloseableReference引用:

void haa(CloseableReference  ref) {
  final CloseableReference  refClone = ref.clone();
  executor.submit(new Runnable() {
    public void run() {
      try {
        Log.println("Haa Async: " + refClone.get());
      } finally {
        refClone.close();
      }
    }
  });
  // caller can now safely close its copy as we made our own clone.
}

EventBus源代码解析:2、消息的发布与处理

在上一篇文章《EventBus源代码解析:1、初始化与订阅者注册》当中,我们主要分析了两个事情:

  • EventBus初始化
  • 订阅者的注册

我们通过分析,EventBus在初始化的时候,初始化了几个集合,分别用来根据EventType和Event Handler所在的类索引对应的Handler方法;并且也同时初始化了用于不同ThreadMode的Poster。订阅者在注册的时候,EventBus会解析要注册的类,分析其所有的方法,从中找出Event的Handler方法(即public修饰的以onEvent开头),然后根据EventType保存到相应的List中。

但我们一直没有分析到一个分析,那就是,EventBus到底是如何去Post消息的呢?接下来我们就去分析这个问题。按照我们的老套路,还是从最常用的代码入手:

MessageEvent event = new MessageEvent(System.currentTimeMillis(), "Message Sequence " + mSequence.getAndIncrement());
EventBus.getDefault().post(event);

这个代码主要分为两步,第一步是构建了一个需要处理的Event即MessageEvent,根据我们之前的分析,在我们调用register()方法的时候,EventBus会解析并将MessageBus作为key保存在一个HashMap中。通过调用EventBus.getDefault().post(event)方法,EventBus会自动调用我们的onEvent方法,我们这里的实现如下:

// Called in Android UI's main thread
public void onEventMainThread(MessageEvent event) {
    mMessages.add("onEventMainThread Receive : " + event);
    mAdapter.notifyDataSetChanged();
}

 public void onEvent(MessageEvent event) {
    Log.i(TAG, "Thread name : " + Thread.currentThread().getName());
    mHandler.obtainMessage(MESSAGE_WHAT_MESSAGEEVENT, event).sendToTarget();
}

那这一个过程当中,EventBus都做了哪些事情呢?我们先来看看post()方法的源代码:

post(Object event)

public void post(Object event) {
  
    // 获取当前线程(调用post方法的线程)中的一个PostingThreadState实例
    PostingThreadState postingState = currentPostingThreadState.get();
    // 获取当前线程(调用post方法的线程)中的EventQueue
    List<Object> eventQueue = postingState.eventQueue;
    // 将Event添加到队列当中
    eventQueue.add(event);

    // 如果当前线程(调用post方法的线程)没有在发布Event
    if (!postingState.isPosting) {
        // 判断调用者是否工作在主线程上
        postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
        // 设置标志,正在发布Event
        postingState.isPosting = true;
        if (postingState.canceled) {
            throw new EventBusException("Internal error. Abort state was not reset");
        }
        try {
            // 依次发送,直至eventQueue为空为止
            while (!eventQueue.isEmpty()) {
                postSingleEvent(eventQueue.remove(0), postingState);
            }
        } finally {
            postingState.isPosting = false;
            postingState.isMainThread = false;
        }
    }
}

我们依次来分析每行代码的作用:

  1. PostingThreadState postingState = currentPostingThreadState.get();
    1. 这一行代码中,有一个PostingThreadState的定义,我们首先要搞明白PostingThreadState到底是怎么回事?
      /** For ThreadLocal, much faster to set (and get multiple values). */
      final static class PostingThreadState {
          // eventQueue
          final List<Object> eventQueue = new ArrayList<Object>();
          // 标志:正在发布?
          boolean isPosting;
          // 标志:是主线程?
          boolean isMainThread;
          // 订阅者
          Subscription subscription;
          // Event
          Object event;
          // 已经取消
          boolean canceled;
      }

      我们可以看到,原来这是一个静态不可变类,按照作者的描述,该类的作用用来提高性能,用于ThreadLocal,可以更快的去设置获取读取多个值。等下ThreadLocal在哪里呢?我们继续看,

      private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {
          @Override
          protected PostingThreadState initialValue() {
              return new PostingThreadState();
          }
      };

      这是currentPostingThreadState的定义,我们可以看到,currentPostingThreadState定义为一个ThreadLocal对象,其内容是PostingThreadState对象,可以看到其中的initialValue()方法返回了一个新的PostingThreadState()对象,这是什么意思呢?换句话每当从一个线程调用currentPostingThreadState.get()方法的时候,系统会检查当前线程是否有一份PostingThreadState实例,如果没有则新建一个,再换句话说,每一个线程中都有其独一无二的一个PostingThreadState实例。那这个用来做什么呢?我们继续分析。

  2. List<Object> eventQueue = postingState.eventQueue;
    1. 这一行代码就比较有意思,post方法从postingState中获取了一个eventQueue,我们再回到刚刚去看一下PostingThreadState中关于eventQueue的定义
      final List<Object> eventQueue = new ArrayList<Object>();

      可以看到,每一个新的PostingThreadState对象中都有自己的一个eventQueue对象,并单独指向一个ArrayList,什么意思呢?我们可以明白,每个线程中都有自己的一份PostingThreadState拷贝,那么换言之,每一个线程中,同样有这样一个自己专属的eventQueue。也就是说代码List<Object> eventQueue = postingState.eventQueue;其实是获取了当前线程中的对应的eventQueue。(注意哦,EventBus中所有线程中消息的发送都是可以通过这个post方法实现的)

  3. eventQueue.add(event);
    1. 这段代码就相当简单了,将event添加到自己所在线程的eventQueue当中。
  4. 继续往下看,发现是一个判断
    if (!postingState.isPosting)

    那么我们就根据分支来分析:

    1. ture:也就是说postingState.isPosting=false;也就是说,当前的线程没有在发布event,则进入以下的流程:
      postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
      postingState.isPosting = true;
      if (postingState.canceled) {
          throw new EventBusException("Internal error. Abort state was not reset");
      }
      try {
          while (!eventQueue.isEmpty()) {
              postSingleEvent(eventQueue.remove(0), postingState);
          }
      } finally {
          postingState.isPosting = false;
          postingState.isMainThread = false;
      }

      我们来分析下,这里面都做了什么的工作呢?

      1. 首先会先判断当前的线程是否是在UI线程上,为什么要判断呢?什么这个还要问吗?UI线程不能做太多事情当然要小心处理啊!!!所以通过代码
        Looper.getMainLooper() == Looper.myLooper()

        来了解。

      2. 然后呢?判断当前的线程是否已经canceled,也就是当前的线程是否已经unregistered了,这个容易理解,不去细细分析。
      3. 然后进入一个while循环
        while (!eventQueue.isEmpty()) {
            postSingleEvent(eventQueue.remove(0), postingState);
        }

        这个代码也很明了啊,就是不断的通过postSingleEvent发送Event直至队列尾空为止。

    2. false:哦,这里应该写另外分支要做的事情,额,如果当前的线程正在发送,那么就不去做任何事情了,等待上一次的while循环处理就好。

所以,通过上面的分析,我们发现最终代码还是进入

postSingleEvent(eventQueue.remove(0), postingState);

对所有的消息进行发送处理,那我们继续分析一下这一个方法就好了。

private void postSingleEvent(Object event, PostingThreadState postingState) throws Error

首先看源代码:

// 发送单个的Event
private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
    // 获取event的类
    Class<?> eventClass = event.getClass();
    // 标记,用来表示是否已经找到event对应的订阅者
    boolean subscriptionFound = false;
    // 判断event是否开启继承?
    if (eventInheritance) {
        // 查找Event对应的所有的event类型(包括父类和接口)。
        List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
        int countTypes = eventTypes.size();
        for (int h = 0; h < countTypes; h++) {
            // 获取其中的一个Event类型(Event对应的类或者其父类或者其实现的接口的一种)
            Class<?> clazz = eventTypes.get(h);
            subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
        }
    } else {
        // 如果没有开启Event继承,则直接根据Event的类型,在指定的线程中发送Event即可。
        subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
    }
    if (!subscriptionFound) {
        if (logNoSubscriberMessages) {
            Log.d(TAG, "No subscribers registered for event " + eventClass);
        }
        if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&
                eventClass != SubscriberExceptionEvent.class) {
            post(new NoSubscriberEvent(this, event));
        }
    }
}

其实我们大体上一看就知道,这段代码也并没有执行具体的消息发送,但做了很必要的预处理工作,那都有哪些工作呢?我已经在程序里面加了很多注释了,我们可以很容易的发现,其实关键的代码在里面的那个if..else…分支语句里面,我们来依次来看看。

if (eventInheritance) {
    // 查找Event对应的所有的event类型(包括父类和接口)。
    List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
    int countTypes = eventTypes.size();
    for (int h = 0; h < countTypes; h++) {
        // 获取其中的一个Event类型(Event对应的类或者其父类或者其实现的接口的一种)
        Class<?> clazz = eventTypes.get(h);
        subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
    }
} else {
    // 如果没有开启Event继承,则直接根据Event的类型,在指定的线程中发送Event即可。
    subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
}

两个分支有什么不同呢?eventInheritance表示用户是否开启Event继承,如果不开启,则通过

postSingleEventForEventType(event, postingState, eventClass);

方法发送Event,如果开启,则首先通过

lookupAllEventTypes(eventClass);

查找event类所有的Event类型,然后依次通过代码

postSingleEventForEventType(event, postingState, eventClass);

进行消息处理,所以,我们分两步来,首先来看看lookupAllEventTypes的代码。

private List<Class<?>> lookupAllEventTypes(Class<?> eventClass)

private List<Class<?>> lookupAllEventTypes(Class<?> eventClass) {
    synchronized (eventTypesCache) {
        List<Class<?>> eventTypes = eventTypesCache.get(eventClass);
        if (eventTypes == null) {
            eventTypes = new ArrayList<Class<?>>();
            Class<?> clazz = eventClass;
            while (clazz != null) {
                eventTypes.add(clazz);
                addInterfaces(eventTypes, clazz.getInterfaces());
                clazz = clazz.getSuperclass();
            }
            eventTypesCache.put(eventClass, eventTypes);
        }
        return eventTypes;
    }
}

其实这个代码并不负责,简单的理解就是根据Event类查找其父类,然后添加到eventTypesCache里面。那另外一个函数干什么的呢?其实真正的工作都在postSingleEventForEventType(event, postingState, clazz);里面,我们去看看。

private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass)

private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
    CopyOnWriteArrayList<Subscription> subscriptions;
    synchronized (this) {
        // 根据Event类型获取其对应的订阅者。
        subscriptions = subscriptionsByEventType.get(eventClass);
    }
    // 如果存在订阅者
    if (subscriptions != null && !subscriptions.isEmpty()) {
        // 依次发送给对应的订阅者
        for (Subscription subscription : subscriptions) {
            // 设置post()方法调用线程中对应的postingState
            postingState.event = event;
            postingState.subscription = subscription;
            boolean aborted = false;
            try {
                // 将event发送给对应的订阅者。
                postToSubscription(subscription, event, postingState.isMainThread);
                aborted = postingState.canceled;
            } finally {
                postingState.event = null;
                postingState.subscription = null;
                postingState.canceled = false;
            }
            if (aborted) {
                break;
            }
        }
        return true;
    }
    return false;
}

什么?我们再一看,好吧,这个代码也没有去真正的发送消息,那我们看看这个方法到底做了什么工作呢?

  1. subscriptions = subscriptionsByEventType.get(eventClass);首先通过这行代码,从subscriptionsByEventType中,根据Event的类,获取所有的对应的subscriptions,需要注意的是,此段代码使用EventBus的实例进行同步,实际上是同步的订阅者的List
  2. 如果subscriptions==null 或者 subscriptions.size()==0,即如果不存在对应的subscriptions,那么则返回即可。
  3. 将Event依次发送给每一个Subscription
    1. 设置postingState.event = event,设置postingState.subscription = subscription;
    2. 调用方法postToSubscription(subscription, event, postingState.isMainThread);发送消息
    3. 恢复postingState默认状态为null
    4. 重复步骤3

可以看到,真正发送消息的工作还没有看到,在postToSubscription方法当中,好吧,那我们继续来学习这个方法都做了什么?

postToSubscription

源代码如下:

// 将event发送给对应的调用者
private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
    switch (subscription.subscriberMethod.threadMode) {
        case PostThread:
            // 直接调用
            invokeSubscriber(subscription, event);
            break;
        case MainThread:
            if (isMainThread) {
                // 如果post的发送线程是UI线程,那么则直接调用对应的方法即可
                invokeSubscriber(subscription, event);
            } else {
                // 否则则发送到main线程中对应的Handler中
                mainThreadPoster.enqueue(subscription, event);
            }
            break;
        case BackgroundThread:
            // 背景线程
            if (isMainThread) {
                // 如果当前工作在主线程,则直接压入背景Poster的队列
                backgroundPoster.enqueue(subscription, event);
            } else {
                // 反之则直接调用
                invokeSubscriber(subscription, event);
            }
            break;
        case Async:
            //直接压入异步Poster的队列
            asyncPoster.enqueue(subscription, event);
            break;
        default:
            throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
    }
}

可以看到,这里是最终的调用订阅者Event处理方法的地方,针对ThreadMode的不同,postToSubscription方法采取了不同的策略

  • PostThread:这个是直接在当前线程上调用处理的方法,所以直接通过invokeSubscriber(subscription, event);调用对应的方法
  • MainThread:
    • 当前在主线程上:同PostThread一样,直接通过invokeSubscriber(subscription, event);反射调用对应的方法
    • 不在主线程上,则通过mainThreadPoster.enqueue(subscription, event);将Event压入队列等待处理
  • BackgroundThread:
    • 当前在主线程上:通过backgroundPoster.enqueue(subscription, event);将Event压入队列,等待处理
    • 当前不在主线程上:通过invokeSubscriber(subscription, event);反射调用对应的方法
  • Async:直接将Event压入队列asyncPoster.enqueue(subscription, event);

好了,到这里,我们基本上明白了Event在EventBus中数据是怎么传递的了,但每一个ThreadMode不同的处理方法我们还没有看,到底是怎么样的呢?

invokeSubscriber(Subscription subscription, Object event) 源代码

    try {
        // 使用反射机制,调用对应的事件处理函数。
        subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
    } catch (InvocationTargetException e) {
        handleSubscriberException(subscription, event, e.getCause());
    } catch (IllegalAccessException e) {
        throw new IllegalStateException("Unexpected exception", e);
    }
}

这个地方的代码比较简单了,其实就是将反射的调用包装了一下,不需要多说。

我们先来看一下MainThread的时候,其Poster的处理办法:

HandlerPoster

/*
 * Copyright (C) 2012 Markus Junginger, greenrobot (http://greenrobot.de)
 *
 * 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 de.greenrobot.event;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;

/**
 * 主线程Poster,本质上为一个Handler 
 *
 */
final class HandlerPoster extends Handler {

    // 维护一个PendingPostQueue的队列
    private final PendingPostQueue queue;
    // 不太懂
    private final int maxMillisInsideHandleMessage;
    // EventBus对象
    private final EventBus eventBus;
    // 标记本Handler是否空闲:true:忙,false:空闲
    private boolean handlerActive;

    HandlerPoster(EventBus eventBus, Looper looper, int maxMillisInsideHandleMessage) {
        super(looper);
        this.eventBus = eventBus;
        this.maxMillisInsideHandleMessage = maxMillisInsideHandleMessage;
        queue = new PendingPostQueue();
    }

    // 入队列
    void enqueue(Subscription subscription, Object event) {
        // 获取一个PendingPost
        PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
        synchronized (this) {
            // 将pendingPost放入queue队列当中
            queue.enqueue(pendingPost);
            // 发送消息
            if (!handlerActive) {
                handlerActive = true;
                // 通过Handler中的MessageQueue,将通知工作在某个线程(可能是main Thread,post thread,background thread,asnyc thread)处理消息
                if (!sendMessage(obtainMessage())) {
                    throw new EventBusException("Could not send handler message");
                }
            }
        }
    }

    // 消息的处理,需要注意的是,该函数段是工作在Looper对应的线程之上的。
    // 有个问题,如果event很快处理完成,那么这个时候是不需要rescheduled的,那么如果在该event处理过程当中,已经放入其他的消息,那么这个消息会在什么时候得到处理呢?
    @Override
    public void handleMessage(Message msg) {
        boolean rescheduled = false;
        try {
            // 记录开始时间
            long started = SystemClock.uptimeMillis();
            while (true) {
                // 从等待处理的队列当中获取一个PendingPost
                PendingPost pendingPost = queue.poll();
                // 判断获取到的pendingPost是否为null,如果null则是没有需要处理的event
                if (pendingPost == null) {
                    synchronized (this) {
                        // Check again, this time in synchronized
                        // 再次处理,需要注意的是,该方法是同步的,跟谁同步的呢?是跟enqueue方法中的代码块同步,做什么用呢?
                        // 我理解的是,此处的代码主要是用于避免一种现象的发生,就是上面已经给Handler发送消息,但并未处理的时候。---> 但貌似又不是
                        // 这次是对的:就是等待前面的enqueue函数执行完成,以便于从中获取event进行处理,如果此时仍然为空,说明队列是空的,标记handlerActive为空,
                        // 这样的话,下次enqueue的时候,就可以直接通过sendMessage通知Handler立刻进行处理。
                        pendingPost = queue.poll();
                        // 如果再次从中获取数据,但为空,则说明handler不是Activie的了。
                        if (pendingPost == null) {
                            // 标记handler已经空闲
                            handlerActive = false;
                            return;
                        }
                    }
                }
                // eventBus调用订阅者的对应的方法
                eventBus.invokeSubscriber(pendingPost);
                // 工作做完,统计消耗时间
                long timeInMethod = SystemClock.uptimeMillis() - started;
                // 超时
                if (timeInMethod >= maxMillisInsideHandleMessage) {
                    // 立刻尝试处理下一个消息
                    if (!sendMessage(obtainMessage())) {
                        throw new EventBusException("Could not send handler message");
                    }
                    // 设置标记
                    rescheduled = true;
                    return;
                }
            }
        } finally {
            // 如果已经rescheduled,那么说明此时该handler已经在忙,否则则说明handler已经空闲。
            handlerActive = rescheduled;
        }
    }
}

上面是我添加过注释的源代码,我们可以发现以下的特点:

  1. 主线程的Poster本质上是一个Handler,因此关键的一点就是,看Handler到底工作在哪个Looper上,通过EventBus的默认初始化代码
    mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10);

    可以发现,其实主线程上的Poster就是一个工作在主线程上的Handler,那么剩下的就比较简单了。

  2. enqueue,入队列,其实就做了以下的事情:
    1. 同步保护,防止从多个线程同时发送消息的时候出现错误
    2. queue.enqueue(pendingPost);将需要处理的PendingPost压入队列
    3. 通过sendMessage(obtainMessage())将消息发送给Handler进行处理
  3. handleMessage(Message msg):消息处理的方法
    1. pendingPost = queue.poll();获取数据,如果为null,那么意味着没有数据可以处理,标记当前活动状态为false,那么下一次enqueue入列的时候,就可以直接通知handler进行数据处理
    2. 获取成功,则通过eventBus.invokeSubscriber(pendingPost);调用相应的方法进行处理
    3. 如果此次消息处理超时,则直接通过sendMessage(obtainMessage())进行下一次消息处理

BackgroundPoster

源代码如下:

/*
 * Copyright (C) 2012 Markus Junginger, greenrobot (http://greenrobot.de)
 *
 * 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 de.greenrobot.event;

import android.util.Log;

/**
 * Posts events in background.
 * 在后台线程当中处理events
 * @author Markus
 */
final class BackgroundPoster implements Runnable {

    // 一个保存有PendingPost的队列
    private final PendingPostQueue queue;
    // 保持对EventBus的引用
    private final EventBus eventBus;

    // 看现在的BackgroundPoster是否正在处理event
    private volatile boolean executorRunning;

    BackgroundPoster(EventBus eventBus) {
        this.eventBus = eventBus;
        queue = new PendingPostQueue();
    }

    public void enqueue(Subscription subscription, Object event) {
        // 根据subscription和event构建PendingPost
        PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
        synchronized (this) {
            // 将构建好的PendingPost加入到队列。
            queue.enqueue(pendingPost);
            if (!executorRunning) {
                // 如果当前队列空闲,则设置其为忙,并通过EventBus的线程池执行该线程
                executorRunning = true;
                eventBus.getExecutorService().execute(this);
            }
        }
    }

    @Override
    public void run() {
        try {
            try {
                while (true) {
                    // 阻塞方法,从PendignPostQueue中获取一个PendingPost
                    PendingPost pendingPost = queue.poll(1000);
                    if (pendingPost == null) {
                        synchronized (this) {
                            // Check again, this time in synchronized
                            // 原理同我们之前分析的mainPoster一样的,都是防止在加入的时候尝试取PendignPost而取不到,
                            // 代码到这里的时候,则保证如果要加入队列,工作已经完成的。
                            pendingPost = queue.poll();
                            if (pendingPost == null) {
                                executorRunning = false;
                                return;
                            }
                        }
                    }
                    // 调用对应的订阅者方法
                    eventBus.invokeSubscriber(pendingPost);
                }
            } catch (InterruptedException e) {
                Log.w("Event", Thread.currentThread().getName() + " was interruppted", e);
            }
        } finally {
            // 此时说明没有工作可做,因此释放该线程完成工作,设置标记为false。
            executorRunning = false;
        }
    }

}

通过比较和Poster代码,发现其实现的原理不一样,但大体的机制基本一直:所有的Event都是缓存在PendingPostQueue当中,当enqueue的时候入队列,然后不同ThreadMode的Poster会以不同的方式处理相应的Event。

BackgroundPoster的Event的入列方式:

  1. 将PendingPost加入到队列当中
  2. 判断如果当前的executorRunning==false,即当前BackgroundPoster没有线程在处理
    1. 设置标记executorRunning=true
    2. 将该线程提交给EventBus默认的ExecutorService进行处理

BackgroundPoster的Event的处理方式的几个特点:

  1. 当开启一个BackgroundPoster之后,会一直处理所有的PendignPost直至所有的全部处理完成。
  2. 当使用queue.poll(1000)获取PendignPost,仍然没有取回之后,会进入同步保护块(避免此时有新的PendignPost加入队列,但该线程看不到),再次尝试,如果依然没有PendingPost,说明此时没有Event通过BackgroundPoster进行处理,线程可以安全退出。

此时,我们再来回顾使用EventBus中,关于BackgroundPoster的几个说明:

  1. BackgroundPoster只会顺序对Event进行处理,因此不适合并发的情况。

对比看完了BackgroundPoster,还需要继续学习一下AsyncPoster的使用

AsyncPoster

class AsyncPoster implements Runnable {

    private final PendingPostQueue queue;
    private final EventBus eventBus;

    AsyncPoster(EventBus eventBus) {
        this.eventBus = eventBus;
        queue = new PendingPostQueue();
    }

    public void enqueue(Subscription subscription, Object event) {
        PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
        queue.enqueue(pendingPost);
        // 与mainPoster和backgroundPoster相比,直接将PendingPost执行
        eventBus.getExecutorService().execute(this);
    }

    @Override
    public void run() {
        // 获取需要处理的PendingPost
        PendingPost pendingPost = queue.poll();
        if(pendingPost == null) {
            throw new IllegalStateException("No pending post available");
        }
        // 调用相应的订阅者方法
        eventBus.invokeSubscriber(pendingPost);
    }

}

一对比,发现跟BackgroundPoster有两点有区别:

  • 入队列的时候,会直接将线程提交给ExecutorService()进行处理,不需要检查当前是否有AsyncPoster任务在执行
  • 每一个AsyncPoster任务只负责一个PendignPostQueue的处理。

 

 

EventBus源代码解析:1、初始化与订阅者注册

和以前一样,我们同样从我们最经常使用的代码入手,分析EventBus到底是如何进行工作的。所以,第一步,我们先分析EventBus的初始化代码,看看初始化代码当中,都做了哪些工作?

一:初始化

单例模式

    public static EventBus getDefault() {
        if (defaultInstance == null) {
            synchronized (EventBus.class) {
                if (defaultInstance == null) {
                    defaultInstance = new EventBus();
                }
            }
        }
        return defaultInstance;
    }

首先,我们最经常使用的EventBus的默认配置,其实就是一个我们常用的单例模式:

  • 首先尝试获取defaultInstance,如果defaultInstance为null,说明默认的EventBus还没有初始化,因此接下来的工作就是需要对EventBus进行初始化。
  • 此时,为了同一个时刻只有一个EventBus在初始化,也是为了避免多个线程同时访问的时候,各自生成了不同的EventBus对象,因此我们需要对初始化的代码块进行同步。当然同步的范围选择很重要,同步的范围必须是所有的线程都能够同时看到,因此选用了EventBus的类对象作为同步代码块的同步对象。
  • 此时,由于进入到同步代码块之后,可能是由于和其他线程竞争,但没有竞争过,那么此时的时候,很有可能其他线程当中已经将EventBus初始化完成了,所以此时需要再次检查EventBus有没有初始化好,没有的话,说明是第一次进行初始化,那进行初始化工作就可以。
  • 返回唯一的EventBus对象defaultInstance。

具体的初始化工作

构造函数:

public EventBus() {
        this(DEFAULT_BUILDER);
    }

构造函数非常简单,调用了另外的构造函数EventBus(EventBusBuilder builder)实现功能,我们还是需要看看这个构造函数中,都做了哪些工作?

    EventBus(EventBusBuilder builder) {
        // 订阅者们,根据Event类型分类:Key是订阅者的类,Value为对应的订阅者
        subscriptionsByEventType = new HashMap<Class<?>, CopyOnWriteArrayList<Subscription>>();
        // 订阅者所支持的Event类型
        typesBySubscriber = new HashMap<Object, List<Class<?>>>();
        // 保存Sticky Events,注意,这个地方用的是一个线程安全的HashMap
        stickyEvents = new ConcurrentHashMap<Class<?>, Object>();
        // 主线程的Poster
        mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10);
        // 后台线程的Poster
        backgroundPoster = new BackgroundPoster(this);
        // 异步的Poster
        asyncPoster = new AsyncPoster(this);
        // 订阅者方法寻找器
        subscriberMethodFinder = new SubscriberMethodFinder(builder.skipMethodVerificationForClasses);
        logSubscriberExceptions = builder.logSubscriberExceptions;
        logNoSubscriberMessages = builder.logNoSubscriberMessages;
        sendSubscriberExceptionEvent = builder.sendSubscriberExceptionEvent;
        sendNoSubscriberEvent = builder.sendNoSubscriberEvent;
        throwSubscriberException = builder.throwSubscriberException;
        eventInheritance = builder.eventInheritance;
        executorService = builder.executorService;
    }
  • 初始化了以下的队列
    • subscriptionsByEventType:Event的类型作为Key的HashMap,需要注意的是,其中用到了一个CopyOnWriteArrayList,当列表中的元素,其读取次数远远超过写入次数的时候,使用该集合类可以大大提高效率,且该类是线程同步保护的。
    • typesBySubscriber:同样的一个HashMap,保存了某一个订阅者当中所支持的消息类型。有个细节可以注意到,这个地方HashMap的Key是List<Class<?>>,而subscriptionsByEvent中的Key是指定了OnWriteArrayList<Subcription>>,这其中有什么技巧吗?我的理解是这个样子的:
      • 从调用的次数上来考虑,当我们POST一个event的时候,按照我们的常规思路,EventBus就应该根据Event的类型,也就是类去找到那些能够处理这个Event的Subcription,那么这些Subcriptions保存在哪里呢?–>CopyOnWriteArrayList。所以对于这一个列表来说,我们很少回去改动其中的元素,除非有新的Subcription注册。但另外一个,还没想到做什么使用,可能是注销的时候依次注销所注册的订阅者?
    • stickyEvents:保存stickey的Events,需要注意的是,这里面用到的都是ConcurrentHashMap,是线程安全的。
  • 初始化了以下的Poster(后面会依次分析几种Poster的不同实现)
    • mainThreadPoster:通过运行在main线程上的Handler实现
    • backgroundPoster:本质上为一个Runnable
    • asyncPoster:本质上同样为一个Runnable
  • 注册了一个subscriberMethodFinder:即查找订阅者类当中对应的Handler方法
  • 初始化了各种参数:需要注意的是,其中有一个executorService,其默认实现为
    private final static ExecutorService DEFAULT_EXECUTOR_SERVICE = Executors.newCachedThreadPool();

    后面我们还看到,很多消息的Post都是通过该线程池实现。

上面,我们分析完成EventBus的初始化工作,下面我们继续来分析一下我们在代码中向EventBus注册订阅者的时候,都发生了什么事情?

二、订阅者注册

1、注册:

我们一般情况下,使用的注册代码是:

register(Object subscriber)

跟踪代码,发现该代码实际上通过调用resiter(Object subscriber, boolean sticky, int priority)实现,我们来看看这段代码都做了哪些工作?

private synchronized void register(Object subscriber, boolean sticky, int priority) {
        // 查找订阅类当中的处理方法(包括其父类的方法)
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriber.getClass());
        // 根据获取的订阅者方法,将其依次订阅
        for (SubscriberMethod subscriberMethod : subscriberMethods) {
            subscribe(subscriber, subscriberMethod, sticky, priority);
        }
    }

上面的代码主要做了两件事情:

  1. 从对应的subscriber类中查找到所有的SubscriberMethod
  2. 依次订阅所有的SubscriberMethod

SubscriberMethod源代码:

    List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
        // 获取类的全名作为key
        String key = subscriberClass.getName();
        // 订阅者方法列表
        List<SubscriberMethod> subscriberMethods;
        // 尝试从methodCache中获取订阅者方法
        synchronized (methodCache) {
            subscriberMethods = methodCache.get(key);
        }
        // 如果订阅者方法列表已经存在,则直接返回
        if (subscriberMethods != null) {
            return subscriberMethods;
        }
        
        subscriberMethods = new ArrayList<SubscriberMethod>();
        Class<?> clazz = subscriberClass;
        HashSet<String> eventTypesFound = new HashSet<String>();
        StringBuilder methodKeyBuilder = new StringBuilder();
        while (clazz != null) {
            String name = clazz.getName();
            // 如果这些类是java,javax或者android的话,则直接跳过。
            if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("android.")) {
                // Skip system classes, this just degrades performance
                break;
            }

            // Starting with EventBus 2.2 we enforced methods to be public (might change with annotations again)
            // 从EventBus2.2开始,强制必须将修饰符设置为public
            Method[] methods = clazz.getDeclaredMethods();
            // 依次遍历类的全部方法
            for (Method method : methods) {
                // 获取方法名
                String methodName = method.getName();
                // 判断方法名是否以ON_EVENT_METHOD_NAME,即是否以"onEvent"开头
                if (methodName.startsWith(ON_EVENT_METHOD_NAME)) {
                    // 获取方法的修饰符
                    int modifiers = method.getModifiers();
                    // 要求方法匹配public修饰符,并且不是(Modifier.ABSTRACT | Modifier.STATIC | BRIDGE | SYNTHETIC)中任一种
                    if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
                        // 获取参数的类型
                        Class<?>[] parameterTypes = method.getParameterTypes();
                        // 如果参数的类型只有一种,也就是说,参数只有一个
                        if (parameterTypes.length == 1) {
                            // 获取方法名当中去掉onEvent剩下的部分,并根据这一部分判断其工作在哪个县城之上
                            String modifierString = methodName.substring(ON_EVENT_METHOD_NAME.length());
                            ThreadMode threadMode;
                            if (modifierString.length() == 0) {
                                // PostThread
                                threadMode = ThreadMode.PostThread;
                            } else if (modifierString.equals("MainThread")) {
                                // MainThread
                                threadMode = ThreadMode.MainThread;
                            } else if (modifierString.equals("BackgroundThread")) {
                                // BackgroundThread
                                threadMode = ThreadMode.BackgroundThread;
                            } else if (modifierString.equals("Async")) {
                                // AsyncThread
                                threadMode = ThreadMode.Async;
                            } else {
                                if (skipMethodVerificationForClasses.containsKey(clazz)) {
                                    continue;
                                } else {
                                    throw new EventBusException("Illegal onEvent method, check for typos: " + method);
                                }
                            }
                            // 获取订阅者处理方法onEvent方法的参数类型
                            Class<?> eventType = parameterTypes[0];
                            methodKeyBuilder.setLength(0);
                            // 添加方法名
                            methodKeyBuilder.append(methodName);
                            // 添加方法类型
                            methodKeyBuilder.append('>').append(eventType.getName());
                            String methodKey = methodKeyBuilder.toString();
                            // 将methodKey添加到eventTypesFound当中 true:说明该方法没有被添加过,false:说明该方法已经被添加过
                            if (eventTypesFound.add(methodKey)) {
                                // Only add if not already found in a sub class
                                // 如果methodKey在子类当中没有被添加过,则构造SubscriberMethod,并添加到subscriberMethods当中
                                subscriberMethods.add(new SubscriberMethod(method, threadMode, eventType));
                            }
                        }
                    } else if (!skipMethodVerificationForClasses.containsKey(clazz)) {
                        Log.d(EventBus.TAG, "Skipping method (not public, static or abstract): " + clazz + "."
                                + methodName);
                    }
                }
            }
            // 该类处理完成,继续处理其父类!!!
            clazz = clazz.getSuperclass();
        }
        // 如果该类或者其父类当中不包含订阅者方法,那么则抛出异常
        if (subscriberMethods.isEmpty()) {
            throw new EventBusException("Subscriber " + subscriberClass + " has no public methods called "
                    + ON_EVENT_METHOD_NAME);
        } else {
            // 该类或者其父类当中,包含订阅者方法,则将其添加到methodCache当中
            synchronized (methodCache) {
                methodCache.put(key, subscriberMethods);
            }
            return subscriberMethods;
        }
    }

SubscriberMethod方法的程序流程图:

findSubscriberMethods

 

 

简单的理解, findSubscriberMethods的工作就依次遍历指定类中的所有方法,从中找到EventBus的handler并将其添加到subscriberMethods集合当中。具体的步骤如下:

  1. 首先判断clazz类以否是java/javax/android的类,如果是,则说明这个类并不是我们的订阅者(可能是订阅者的父类,不需要处理)。
  2. 然后遍历类中所有的方法,首先挑选出其中以onEvent开头的方法,继续判断是否是我们的Event处理方法。
  3. 判断该方法的修饰符,要求方法匹配public修饰符,并且不是(Modifier.ABSTRACT | Modifier.STATIC | BRIDGE | SYNTHETIC)中任一种。
  4. 获取onEvent方法的参数类型,即要处理的Event的类型。
  5. 构建SubscriberMethod对象,并添加到subscriberMethods
  6. 获取clazz的父类,重复步骤1-6

subscribe代码如下:

    // Must be called in synchronized block
    // 必须要在同步快中执行
    private void subscribe(Object subscriber, SubscriberMethod subscriberMethod, boolean sticky, int priority) {
        
        // 获取订阅者处理方法的参数类型,即Event事件的类型
        Class<?> eventType = subscriberMethod.eventType;
        // 根据类型尝试从subscriptionsByEventType中获取参数类型,即Event事件类型所对应的订阅者列表。
        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        // 生成新的订阅者对象
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod, priority);
        // 如果订阅者列表为null,则说明是头一次添加该Event类型。
        if (subscriptions == null) {
            subscriptions = new CopyOnWriteArrayList<Subscription>();
            subscriptionsByEventType.put(eventType, subscriptions);
        } else {
            if (subscriptions.contains(newSubscription)) {
                throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
                        + eventType);
            }
        }

        // Starting with EventBus 2.2 we enforced methods to be public (might change with annotations again)
        // subscriberMethod.method.setAccessible(true);
        // 从EventBus2.2之后,设置方法必须为public
        // subscriberMethod.method.setAccessible(true);        
        int size = subscriptions.size();
        // 根据优先级,将包含订阅者处理方法的订阅者对象添加到队列的合适位置上
        for (int i = 0; i <= size; i++) {
            if (i == size || newSubscription.priority > subscriptions.get(i).priority) {
                subscriptions.add(i, newSubscription);
                break;
            }
        }

        // 保存订阅者类所能够处理的EventType
        List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
        if (subscribedEvents == null) {
            subscribedEvents = new ArrayList<Class<?>>();
            typesBySubscriber.put(subscriber, subscribedEvents);
        }
        subscribedEvents.add(eventType);

        // 如果Event设置为sticky
        if (sticky) {
            Object stickyEvent;
            synchronized (stickyEvents) {
                stickyEvent = stickyEvents.get(eventType);
            }
            if (stickyEvent != null) {
                // If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state)
                // --> Strange corner case, which we don't take care of here.
                postToSubscription(newSubscription, stickyEvent, Looper.getMainLooper() == Looper.myLooper());
            }
        }
    }

其程序架构图如下:

subscribe

 

这个其实理解起来比较简单,主要分了下面几个步骤:

  1. 根据订阅者所在的类,订阅者的方法,生成相应的订阅者对象,即Subscription对象。
  2. 获取Event的事件类型,查看此事件是否有相应的List,如果有,则说明之前有注册过其他的可以处理该事件类型的Subcription,那么则根据优先级将此次的Subcription对象插入到合适的位置,否则则新建List,并将Subcription对象插入进来。

 

自定义View:3、编写自己的ViewGroup

前面我们已经分析过两种自定义View的方法:

今天我们来继续学习第三种自定义View的方法,继承ViewGroup,实现自己的Layout。什么意思呢?其实前两种自定义View的方法,我们都是编写的具体的每一个View,然后整合到我们现有的Layout(比如LinearLayout,RelativeLayout等等)当中,但很多情况下,自定义View并不能完全满足我们的需求,或者说,我们想要使用现成的控件,但我们希望我们的界面上,我们可以完全自己来控制如何摆放这些控件。那这个时候怎么做呢?

我们先来看一下我们想要实现的效果是什么样子的?

device-2014-12-13-190320

简单分解一下上面我们要实现的需求:

  • 每一小块为自定义View(称之为磁贴),大小共有3中规格,分别是:横向占据屏幕1/4(size=one),横向占据屏幕1/2(size=two),横向占据屏幕全部(size=four)。其高度始终为屏幕1/4。颜色与文字均可以通过XML直接指定。
  • 要求在XML定义各个View之后,要求能够从左上角开始能够自动占据铺满屏幕。

好了,要求说完了,那么该怎么去实现呢?首先我们来分解任务,要实现上面的功能,其实我们总共需要两个步骤,第一步,参考自定义View:1、定制自己的饼形图,定制我们自己的磁贴。第二步,指定我们自定的ViewGroup,自动对添加的磁贴贴到合适的位置上。

一、定制磁贴

1、定制属性

首先,我们需要在attrs.xml中为磁贴定义一些属性,在这边定义之后,我们就可以直接在Layout的XML中为磁贴指定颜色,文字,大小等内容。代码如下:

    <declare-styleable name="Tile">
        <attr name="background" format="color"/>
        <attr name="title" format="string"/>
        <attr name="size" format="enum">
            <enum name="one" value="1"/>
            <enum name="two" value="2"/>
            <enum name="four" value="4"/>
        </attr>
    </declare-styleable>

如上所示,我们总共为磁贴定义了3个属性,其中包括背景颜色,文字内容,以及尺寸,one代表占据宽度的1/4,two代表占据宽度的1/2,而four则代表了占据宽度的全部。

2、定义View:Tile

接下来就是很重要的一步,定义View,即Tile,还记得我们之前的文章具体是怎么做的吗?没做,就是分3步走:

  1. 构造函数从XML中获取参数,初始化。
  2. 覆写onMeasure方法,指定View具体的大小。
  3. 覆写onDraw方法,按照参数指定的颜色,文字及大小进行绘制。

初始化

   public Tile(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.Tile, 0, 0);
        mContent = new TileContent();
        try {
            mContent.mBackgroundColor = typedArray.getColor(R.styleable.Tile_background, Color.BLUE);
            mContent.mTitle = typedArray.getString(R.styleable.Tile_title);
            mContent.mSize = typedArray.getInt(R.styleable.Tile_size, 1);
        } finally {
            typedArray.recycle();;
        }

        init();
    }

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mRect = new Rect();
        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextSize(50.0f);
        mTextSize = new Rect();
        mTextPaint.getTextBounds(mContent.mTitle, 0, mContent.mTitle.length(), mTextSize);
    }

初始化代码比较简单了:

  1. 从context.getTheme()/obtainStyeAttributes中获取包含属性的TypeArray。
  2. 从中读取设置的参数
  3. 初始化画笔,获取文字所占的边界大小。

onMeasure(init widthMeasureSpec, int heightMeasureSpec)

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width = 0;
        int height = 0;

        //如果指定了尺寸,那么就使用指定的尺寸,否则使用我们容器尺寸的一半
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
            height = width;
        } else {
            switch (mContent.mSize) {
                case SIZE_ONE :
                    width = widthSize >> 2;
                    height = width;
                    break;
                case SIZE_TWO:
                    width = widthSize >> 1;
                    height = width >> 1;
                    break;
                case SIZE_FOUR:
                    width = widthSize;
                    height = width >> 2;
                    break;
            }
        }
        mRect.set(0, 0, width, height);
        setMeasuredDimension(width, height);
    }

在这个界面设置当中,onMeasure方法是比较关键的一步,因为我们要在这个地方为每一个磁贴,根据他们配置的属性为其指定合适的尺寸,那么我们具体是怎么做的呢?

  1. 获取widthMode,如果是MeasureSpec.EACTLY,即在XML中指定了具体的大小,那么我们就应该使用指定的大小。
  2. 如果没有指定具体的大小,而是让View根据需求来自己指定的话,我们就按照原先的设计,如果尺寸为one,那么就将宽度设置为全部的宽度1/4,高度则与宽度想的呢个,如果尺寸为two,那么则宽度为全部宽度的一半,高度为对应宽度的1/2,如果为four,则宽度为全部的宽度,高度是宽度的1/4。

onDraw

onDraw方法是我们经常用到的方法了,简单的讲就是根据我们的需求画图呗,看看代码就行。

   @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(mContent.mBackgroundColor);
        canvas.drawRect(mRect, mPaint);
        mTextPaint.setColor(Color.WHITE);
        canvas.drawText(mContent.mTitle, getMeasuredWidth()/2 - mTextSize.width()/2, getMeasuredHeight()/2 + mTextSize.height()/2, mTextPaint);
    }

上面的代码中,我们首先画好北京颜色,然后设置文字的画笔颜色,为其在中间写好文字。

好了,至此,具体的Tile就准备完毕了,再来看怎么编写我们自定的ViewGroup

二、定制ViewGroup:TileLayout

1、覆写onMeasure方法

自定义View的onMeasure方法我们有写过,但是自定义Layout的onMeasure方法怎么写呢?因为ViewGroup继承自View,因此思路和View基本上差别不大,我们这里先简单考虑,我们先将整个屏幕的全部空间都占据使用。那该怎么写呢?

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

其中做了两个工作:

  • 依次通知子View们进行测量
  • 调用父类的onMeasure方法。

 

2、onLayout布局

该方法是ViewGroup的核心方法之一,简单的理解,在该方法中,我们需要设计将子元素放在合适的位置上。根据需求,我们对TileLayout的onLayout方法可以如下实现:

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        Log.i(TAG, "onLayout:" + " position:" + left + "," + top + "," + right + "," + bottom);

        int positionX = left;
        int positionY = top;

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if(! (childView instanceof Tile) ) {
                throw new IllegalArgumentException("Catch Exception not Tile!");
            }
            Tile item = (Tile) childView;

            int width = childView.getMeasuredWidth();
            int height = childView.getMeasuredHeight();

            int itemPostitionX = left;
            int itemPositionY = top;
            switch (item.getSize()) {
                case Tile.SIZE_ONE:

                    if( !isSpaceEmpty(Tile.SIZE_ONE) ) {
                        LayoutPositions params = mSizeOneSpaces.remove();
                        itemPostitionX = params.x;
                        itemPositionY = params.y;
                    } else {
                        if (positionX + width <= right) {
                            itemPostitionX = positionX;
                            itemPositionY = positionY;
                            positionX += width;
                            if (positionX >= right) {
                                positionX = left;
                                positionY = positionY + height;
                            }
                        } else {
                            itemPostitionX = left;
                            itemPostitionX = positionY + height;
                            positionX = itemPostitionX + width;
                            positionY = itemPositionY;
                        }
                    }
                    break;
                case Tile.SIZE_TWO:
                    if (positionX + width <= right) {
                        itemPostitionX = positionX;
                        itemPositionY = positionY;
                        positionX += width;
                        if (positionX >= right) {
                            positionX = left;
                            positionY = positionY + height;
                        }
                    } else {
                        for (int start=positionX; start + (width >> 1) <= right; start=start + (width >> 1)) {
                            mSizeOneSpaces.add(new LayoutPositions(start, positionY));
                        }
                        positionX = left;
                        positionY = positionY + height;
                        itemPostitionX = positionX;
                        itemPositionY = positionY;
                    }
                    break;
                case Tile.SIZE_FOUR:
                    if (positionX == left) {
                        itemPostitionX = positionX;
                        itemPositionY = positionY;
                        positionX += width;
                        positionY += height;
                    } else {
                        for (int start=positionX; start + (width >> 2) <= right; start=start + (width >> 2)) {
                            mSizeOneSpaces.add(new LayoutPositions(start, positionY));
                        }
                        positionX = left;
                        positionY = positionY + height;
                        itemPostitionX = positionX;
                        itemPositionY = positionY;
                        positionX = left;
                        positionY = positionY + height;
                    }
                    break;
            }
            Log.i(TAG, "Item:" + item.getTitle() + " position:" + itemPostitionX + "," + itemPositionY + "," + (itemPostitionX + width) + "," + (itemPositionY + height));
            childView.layout(itemPostitionX, itemPositionY, itemPostitionX + width, itemPositionY + height);
        }
    }

其实思路也很简单,首先尝试摆放,如果能将子View摆在某处,那么则摆放这里,继续摆放下面的地方,如果不能,则说明此行空间不足,那么将剩下的空间根据大小分配给合适的数量的size=“one”的元素。依次摆放就可以。

三、使用

按照我们设计的,将所有的属性设置都放在XML当中,Activity代码如下:

public class TestTileLayout extends Activity{
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.tile);
    }
}

布局代码:

<?xml version="1.0" encoding="utf-8"?>
<me.happyhls.androiddemo.view.TileLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tile="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#0000FF"
        tile:title="Title1"
        tile:size="one"
        />
    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#00FF00"
        tile:title="Title2"
        tile:size="two"
        />
    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#FF0000"
        tile:title="Title3"
        tile:size="four"
        />
    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#0000FF"
        tile:title="Title4"
        tile:size="one"
        />

    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#00FF00"
        tile:title="Title5"
        tile:size="one"
        />
    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#FF00FF"
        tile:title="Title6"
        tile:size="two"
        />
    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#FF000F"
        tile:title="Title7"
        tile:size="four"
        />
    <me.happyhls.androiddemo.view.Tile
        xmlns:tile="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tile:background="#0000FF"
        tile:title="Title8"
        tile:size="one"
        />
</me.happyhls.androiddemo.view.TileLayout>

好了,上面就是简单的对自定义ViewGroup的使用。总结来说,我们只是大体了解了一下如何自定义View,如何自定义ViewGroup,但深入的我们依然没有设计到,因此后面会有至少两篇文章,我们来分析Android原生控件TextView和Android原生Layout:LinearLayout的源代码。

自定义View:2、组合视图

昨天联系了自定义View的第一类,直接继承View自己画图。其实很多情况下,我们并不需要自己去从头开始画图,而是将现有的各种视图组合在一起,方便我们的使用。那这种情况下该怎么做呢?

我们先来看看实现的效果怎么样(下图中,我将组合视图与CardView和RecyclerView结合在一起,看起来效果还蛮不错的):

device-2014-12-09-110725

其实操作起来也很简单,与直接继承View的办法类似,一共3个步骤:

  1. 编写Layout,可以在代码中设置Layout,也可以直接通过XML配置Layout,然后在代码中直接解析就可以。
  2. 编写属性
  3. 组合在一起成为一个新的View
  4. 使用我们新的编写的View

很简单吧?我们依次来看,具体应该怎么来做。

一:编写对应的Layout

代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    <ImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:scaleType="centerInside"
        android:src="@drawable/ic_launcher"
        android:id="@+id/avator"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:layout_toRightOf="@id/avator"
        android:layout_alignTop="@id/avator"
        android:id="@+id/name"
        android:text="name"
        android:textAppearance="?android:attr/textAppearanceLarge"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/activity_vertical_margin"
        android:layout_alignLeft="@id/name"
        android:layout_below="@id/name"
        android:text="Description here"
        android:layout_alignBottom="@id/avator"
        android:id="@+id/description"/>

</RelativeLayout>

视图的定义就不需要多说了,比较简单,其界面效果就是其中的名片小卡片,包括了一张图像,名字和简单的简介。

二、编写属性

为了以后能够更加方便的使用,我们当然最好为我们新编写的视图增加属性设置,其实和上一篇文章当中介绍的基本上一样,贴上代码。

    <declare-styleable name="NameCard">
        <attr name="avatar" format="reference"/>
        <attr name="name" format="string"/>
        <attr name="description" format="string"/>
    </declare-styleable>

我们为其制定了3个属性,分别是图像,姓名,介绍。通过这些,我们就可以在XML中直接指定默认的属性。

三、构建新的View

思路:

第一步:我们需要加载我们在步骤1中编写的Layout

第二步:根据XML属性或者用户的设置更改其中图像,姓名和介绍的内容。

其他的,就交给RelativeLayout去做吧。

代码如下:

public class NameCard extends RelativeLayout{

    public static class NameCardContent {
        private Drawable mAvatarImageDrawable;
        private String mName;
        private String mDescription;

        public NameCardContent() {
        }

        public NameCardContent(Drawable avatarImageDrawable, String name, String description) {
            mAvatarImageDrawable = avatarImageDrawable;
            mName = name;
            mDescription = description;
        }

        public Drawable getAvatarImageDrawable() {
            return mAvatarImageDrawable;
        }

        public void setAvatarImageDrawable(Drawable avatarImageDrawable) {
            mAvatarImageDrawable = avatarImageDrawable;
        }

        public String getName() {
            return mName;
        }

        public void setName(String name) {
            mName = name;
        }

        public String getDescription() {
            return mDescription;
        }

        public void setDescription(String description) {
            mDescription = description;
        }
    }

    private NameCardContent mNameCardContent = new NameCardContent();

    private ImageView mImageViewAvator;
    private TextView mTextViewName;
    private TextView mTextViewDescription;

    public NameCard(Context context) {
        this(context, null);
    }

    public NameCard(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NameCard(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.NameCard, 0, 0);
        try {
            mNameCardContent.mAvatarImageDrawable = typedArray.getDrawable(R.styleable.NameCard_avatar);
            mNameCardContent.mName = typedArray.getString(R.styleable.NameCard_name);
            mNameCardContent.mDescription = typedArray.getString(R.styleable.NameCard_description);
        } finally {
            typedArray.recycle();;
        }

        inflate(context, R.layout.namecard, this);

        mImageViewAvator = (ImageView) findViewById(R.id.avator);
        mTextViewName = (TextView) findViewById(R.id.name);
        mTextViewDescription = (TextView) findViewById(R.id.description);

        mImageViewAvator.setImageDrawable(mNameCardContent.getAvatarImageDrawable());
        mTextViewName.setText(mNameCardContent.getName());
        mTextViewDescription.setText(mNameCardContent.getDescription());
    }

    public Drawable getAvatarImageDrawable() {
        return mNameCardContent.getAvatarImageDrawable();
    }

    public void setAvatarImageDrawable(Drawable avatarImageDrawable) {
        mNameCardContent.setAvatarImageDrawable(avatarImageDrawable);
        mImageViewAvator.setImageDrawable(avatarImageDrawable);
    }

    public String getName() {
        return mNameCardContent.getName();
    }

    public void setName(String name) {
        mNameCardContent.setName(name);
        mTextViewName.setText(name);
    }

    public String getDescription() {
        return mNameCardContent.getDescription();
    }

    public void setDescription(String description) {
        mNameCardContent.setDescription(description);
        mTextViewDescription.setText(description);
    }
}

 

四、使用

根据我们上面的所想要的效果,首先,为CardView编写子视图界面:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >
    <android.support.v7.widget.CardView
        xmlns:card_view="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/cardview"
        android:layout_gravity="center"
        card_view:cardCornerRadius="10dp"
        card_view:cardElevation="10dp"
        >
        <me.happyhls.androiddemo.view.NameCard
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/namecard">
        </me.happyhls.androiddemo.view.NameCard>
    </android.support.v7.widget.CardView>
</LinearLayout>

然后编写Activity对应的RecyclerView的界面

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">
    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/recycleview"
        android:layout_gravity="center"
        ></android.support.v7.widget.RecyclerView>
</LinearLayout>

最后,在Activity中初始化RecyclerView和数据,代码如下:

public class TestNameCard extends Activity {

    private static final String TAG = TestNameCard.class.getSimpleName();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.lollipop_recyclerviewandcardview);

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycleview);
        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);

        List<NameCard.NameCardContent> items = new ArrayList<NameCard.NameCardContent>();
        Drawable drawable = getResources().getDrawable(R.drawable.ic_launcher);
        for (int i = 0; i < 100; i++) {
            items.add(new NameCard.NameCardContent(drawable, "Name" + i, "Description" + i));
        }
        MyAdapter adapter = new MyAdapter(this, items);
        recyclerView.setAdapter(adapter);
    }

    static class MyAdapter extends RecyclerView.Adapter<ViewHolder> {

        private List<NameCard.NameCardContent> mItems;
        private LayoutInflater mLayoutInflater;

        public MyAdapter(Context context, List<NameCard.NameCardContent> items) {
            this.mItems = new ArrayList<NameCard.NameCardContent>(items);
            mLayoutInflater = LayoutInflater.from(context);
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int position) {
            Log.d(TAG, "Adapter creating view for " + position);
            View view = mLayoutInflater.inflate(R.layout.namecard_item, parent, false);
            ViewHolder viewHolder = new ViewHolder(view);
            viewHolder.mNameCard = (NameCard) view.findViewById(R.id.namecard);
            return viewHolder;
        }

        @Override
        public void onBindViewHolder(ViewHolder viewHolder, int position) {
            Log.d(TAG, "Adapter binding view for " + position);
            viewHolder.mNameCard.setAvatarImageDrawable(mItems.get(position).getAvatarImageDrawable());
            viewHolder.mNameCard.setName(mItems.get(position).getName());
            viewHolder.mNameCard.setDescription(mItems.get(position).getDescription());
        }

        @Override
        public int getItemCount() {
            return mItems.size();
        }
    }

    static class ViewHolder extends RecyclerView.ViewHolder {
        public ViewHolder(View view) {
            super(view);
        }

        NameCard mNameCard;
    }
}

 

思考:优化的空间?

需要注意的是,上面我们的实现中,仅仅是展示了组合视图的相关的原理和RecyclerView/CardView的使用,但实际的生产环境中不会这么简单,几个简单的点:

1、NameCard当中,我们为每一个名片都保存了Drawable,而且是强引用,同时我们观察代码,可以发现所有的DrawableActivity中的List中的NameCardContent里面,在上面的代码中,我们所有的Drawable都是指向同一个对象,因此不会占用太多的内容空间,但在实际应用当中,不同的人对应的头像必然是不同的,那这个时候就不能再这样使用了,否则会必然导致OOM。(解决办法,加入Cache,保存Drawable对应的地址或者Id)

2、关于视图层次,上面的代码中,NameCard是一个RelativeLayout,但我们注意到其中加载的namecard.xml仍然其中任然有一层RelativeLayout,其实是不需要的,多于的,由于这个视图会多次被解析,因此这样必然会严重影响加载速度,所以此处应该将namecard.xml最外层去掉RelativeLayout,设置为merge标签即可。

 

 

 

Android Studio/Intellij使用技巧

逐渐将开发环境迁移到Android Studio当中,一些使用技巧慢慢的记录下来,会始终在这个文档当中进行更新:

  1. Refactor生成的getter和setter总是把类成员变量的前缀”m”也带着。
    1. Settings->Code Style->Java->Code Generation->Naming中Filed的Name prefix设置为m即可。
  2. 将Android Studio的版本从0.8.4升级到1.0.0,结果升级完成之后提示错误:Error:(行号, 0) Gradle DSL method not found。奇怪了,后来才发现,是Grandle对应的定义改了,原来对应的行号的内容是 runProguard false,改成minifyEnabled false就可以。

自定义View:1、定制自己的饼形图

Android本身为我们提供了View,但很多情况下,仍然无法满足我们自己的需求,那么这个时候就需要自己定制View。自定义View的办法有很多,我们从最基础的开始。

device-2014-12-08-145922

上图就是我们要实现的效果,具体的我们依次来列举一下:

  • 首先要画出上面的界面
    • 饼形图,饼的面积代表其进度。
    • 在饼形图中间可以设定是否显示数字进度。
  • 要可以设置饼形图的各种参数,包括颜色,大小等等
  • 要能够通过UI中其他的控件比如SeekBar来设置饼形图的参数。
  • 要有回调函数能够使得其他的控件接收到饼形图中进度的变化。
  • 当手指按在饼形图上时,要能够不断自增进度。

好了,上面就是我们规划的需求,那么我们来依次编写代码,首先来分析我们需要做的工作有哪些?

  • 资源文件
    • attrs.xml中准备饼形图可设置的参数,这样我们可以直接在Layout中设置饼形图的各个参数
  • 程序代码
    • 覆写onMeasure方法来设置界面的大小
    • 覆写onDraw()方法来绘图
    • 设计各种回调函数(Listener)
    • 覆写onTouchEvent()设计触摸事件

资源文件

首先,我们需要在attrs.xml中为饼形图设计各种参数,我们将饼形图名称定义为ProgressPie,那我们为其设计的属性为:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ProgressPie">
        <attr name="max" format="integer"/>
        <attr name="progress" format="integer"/>
        <attr name="showText" format="boolean"/>
        <attr name="textSize" format="dimension"/>
        <attr name="textColor" format="color"/>
        <attr name="textPosition" format="enum">
            <enum name="left" value="0"/>
            <enum name="middle" value="1"/>
            <enum name="right" value="2"/>
        </attr>
        <attr name="color" format="color"/>
        <attr name="showBorder" format="boolean"/>
        <attr name="borderColor" format="color"/>
    </declare-styleable>
</resources>

我们为ProgressPie声明了一些属性,有一点需要注意的是属性的各种类型,比如整形,尺寸,布尔型,颜色,枚举类型(其中元素要在对应的View中定义)等等。

那怎么使用呢?activity.xml的界面如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" 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">

    <CheckBox
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Visiablity"
        android:id="@+id/visiablityCheckBox"
        android:checked="true" />
    <SeekBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/seekbar"
        />
    <me.happyhls.androiddemo.view.ProgressPie
        xmlns:progresspie="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="@dimen/activity_vertical_margin"
        progresspie:max="100"
        progresspie:progress="90"
        progresspie:textColor="#0000FF"
        progresspie:showText="true"
        progresspie:textSize="50sp"
        progresspie:textPosition="right"
        progresspie:color="#FF0000"
        android:id="@+id/progressspie"
        />
</LinearLayout>

重点我们来看一下我们自己定义的me.happyhls.androiddemo.view.ProgressPie:

首先我们需要申明命名空间,在AndroidStudio中的推荐写法为,将其设为res-auto,即:

xmlns:progresspie="http://schemas.android.com/apk/res-auto"

这样就可以自动找到我们的属性设置,需要注意的是,上面是当前的ADT或者AS中的推荐写法。

以前,我们一般是这样写的:

xmlns:progress="http://schemas.android.com/apk/res/me.happyhls.view.ProgressPie

但现在最好不要这样写,如果按照以前那样写上自己的包名的话,可以能出现错误,尤其是我们要作为Library提供的时候。参考:http://stackoverflow.com/questions/10398416/using-activities-from-library-projects

其他的地方则是我们比较经常使用的,不需要多说,主要来看看代码里面的内容:

ProgressPie.java

一:初始化->构造函数

构造函数是我们首先需要写好的,默认的View其中有3个构造函数:

    public View(Context context) {
    }

    public View(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public View(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context);
        TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View,defStyleAttr, 0);
        ...
    }

这里面的3个构造函数都有其不同的应用场景,为了使得我们的自定义View更加规范易用,我们同样需要书写类似的3个构造函数,在编写之前,我们需要首先搞明白,这3个构造函数分别应用在什么场景里面呢?自己思考是想不明白的,我们先去看看Button的源代码:

public class Button extends TextView {
    public Button(Context context) {
        this(context, null);
    }

    public Button(Context context, AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.buttonStyle);
    }

    public Button(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
        super.onInitializeAccessibilityEvent(event);
        event.setClassName(Button.class.getName());
    }

    @Override
    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(info);
        info.setClassName(Button.class.getName());
    }
}

从上我们可以看出:

  • 所有的构造函数最终都是通过public Button(Context context, AttributeSet attrs, int defStyle){}初始化Button。
  • 如果通过代码创建Button,那么会调用构造器public Button(Context context) {}(来源:View.java源代码注释),但本质上仍然通过public Button(Context context, AttributeSet attrs, int defStyle) {}实例化,但其中传入的参数attrs为null,defStyle为com.android.internal.R.attr.buttonStyle。
  • 如果通过XML创建Button,那么此时会调用构造器public Button(Context context, AttributeSet attrs) {},但本质上仍然通过public Button(Context context, AttributeSet attrs, int defStyle) {}实例化,但其中传入的参数attrs为attrs,defStyle为com.android.internal.R.attr.buttonStyle。
  • public Button(Context context, AttributeSet attrs, int defStyle) {}什么时候调用?不太清楚,但Button所有的初始化最终都是通过该构造器,并调用父类TextView对应的构造器实现。

综上所示,要搞明白,还需要好好看看TextView对应的构造器实现。

TextView的源代码如下,来分析一下public TextView(Context context, AttributeSet attrs, int defStyle)的源代码。 {https://github.com/happyhls/platform_frameworks_base/blob/master/core/java/android/widget/TextView.java}

1、调用父类的构造函数

super(context, attrs, defStyle);

2、为了简化逻辑设计,从系统主题中获取默认的主题,使用设置的attrs及com.android.internal.R.styleable.TextAppearance设置默认属性。

        final Resources.Theme theme = context.getTheme();        
         /*
         * Look the appearance up without checking first if it exists because
         * almost every TextView has one and it greatly simplifies the logic
         * to be able to parse the appearance first and then let specific tags
         * for this View override it.
         */
        TypedArray a = theme.obtainStyledAttributes(
                    attrs, com.android.internal.R.styleable.TextViewAppearance, defStyle, 0);
        TypedArray appearance = null;
        int ap = a.getResourceId(
                com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);
        a.recycle();
        if (ap != -1) {
            appearance = theme.obtainStyledAttributes(
                    ap, com.android.internal.R.styleable.TextAppearance);
        }

 

3、使用attrs,并从主题中获取com.android.internal.R.styleable.TextView的属性设置:

        a = theme.obtainStyledAttributes(
                    attrs, com.android.internal.R.styleable.TextView, defStyle, 0);

        int n = a.getIndexCount();
        for (int i = 0; i < n; i++) {
            int attr = a.getIndex(i);
            ...
        }

4、使用attrs,获取View中定义的focusable,clickable属性值:

a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, defStyle, 0);

可以看出,在TextView中根据用户在XML中设定的属性和系统的默认属性,依次设置TextView的各个属性值。

有一点我们需要想到的是,TextView的设计比较复杂,是因为在Android当中,TextView是大量控件的父类,换句话说,很多的控件都是基于TextView来实现的,比如说:

Known Direct Subclasses

Known Indirect Subclasses

那我们的代码改怎么写呢?如果不会,那么就模仿,所以,我们模仿TextView,同样声明3个构造函数,在含有3个参数的构造函数中具体的初试话所有需要初始化的属性,代码如下:

    public ProgressPie(Context context) {
        this(context, null);
    }

    public ProgressPie(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ProgressPie(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ProgressPie, 0, 0);
        try {
            mMax = typedArray.getInteger(R.styleable.ProgressPie_max, 100);
            mProgress = typedArray.getInteger(R.styleable.ProgressPie_progress, 0);
            if (mProgress > mMax) {
                mProgress = mMax;
            } else if (mProgress < 0) {
                mProgress = 0;
            }
            mShowText = typedArray.getBoolean(R.styleable.ProgressPie_showText, false);
            if (mShowText) {
                mTextSize = typedArray.getDimensionPixelSize(R.styleable.ProgressPie_textSize, 20);
                mTextColor = typedArray.getColor(R.styleable.ProgressPie_textColor, Color.BLACK);
                mTextPosition = typedArray.getInteger(R.styleable.ProgressPie_textPosition, TEXT_POSITION_MIDDLE);
            }
            mColor = typedArray.getColor(R.styleable.ProgressPie_color, Color.BLUE);
            mShowBorder = typedArray.getBoolean(R.styleable.ProgressPie_showBorder, true);
            if (mShowBorder) {
                mBorderColor = typedArray.getColor(R.styleable.ProgressPie_borderColor, Color.GRAY);
            }
        } finally {
            typedArray.recycle();
        }
        init();
    }

    private void init() {
        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(mTextSize);


        mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPiePaint.setColor(mColor);
    }

其实整体来说,并不复杂,简单的讲,首先获取XML中的配置的属性,并进行设置,最后初始化了我们所需要的Paint画笔。需要注意的是TypeArray使用完成之后,记得要回收。

直接在XML中定义>style定义>由defStyleAttr和defStyleRes指定的默认值>直接在Theme中指定的值 //参考自http://www.cnblogs.com/angeldevil/p/3479431.html

二:测量->onMeasure

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.i(TAG, "onMeasure");

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width, height;

        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            CharSequence charSequence = String.valueOf(mMax);
            width = getTextWidth(charSequence, mTextPaint) << 1;
        }

        height = width;
        if (width > 0) {
           this.width = width;
            this.height = height;
            rectF.set(0f, 0f, width, height);
        }
        setMeasuredDimension(height, width);
    }

在我们的饼形图当中,我们要设定的饼是一个圆形,那么其大小该怎么确定呢?

  • 最小情况(wrap_content):首先是一个圆形,如果要显示数字的话,那么最小的饼只要能够覆盖数字的空间就可以。同时,因为我们设计了3种数字的显示方式:靠左,靠右,居中,因此我们可以保证圆的半径为数字所占空间的大小,那么就可以保证在所有的情况下,都能够正常显示
  • 最大的情况(match_parent)/XML指定控件大小:其实这两种情况下,我们都可以因为是使用到该饼形图的地方已经为我们指定好了大小:如果是XML指定大小的情况,那不用多说,如果是match_parent,那么这个时候其实大小是由容器剩下的空间决定好了的。因此在这些情况下,我们只要采用给定的尺寸就可以。

首先说明一下MeasureSpec这个类:

该类封装了从Layout parent传递给child的Layout参数。MeasureSpec都包含了width和height属性,由size和mode构成。

对于Mode,总共有3中类型,由MeasureSpec的最高两位来表示(32位):

  • UNSPECIFIED(0b00):没有指定
  • EXACTLY(0b01):Layout parent中已经定义了child元素的具体尺寸,不管child所需要的或者设置的尺寸是多少,其最终都使用父控件所指定的大小。
  • AT_MOST(0b11):子元素可以任意获得所需要的大小。

setMeasuredDimension

我们可以注意到,在onMeasure()方法的最后,我们调用了View中的setMeasuredDimension方法,需要注意的是,该方法是onMeasure中必须调用的,用来保存我们设置的尺寸大小。如果没有调用该方法的话,会抛出异常。

还有,其中我们使用getTextWidth()方法来获取数字所占用的屏幕大小。我暂时知道的有两种思路

  • (int)FloatMath.ceil(Layout.getDesiredWidth(charSequence, textPaint)); //Return how wide a layout must be in order to display the specified text slice with one line per paragraph.
  • mTextPaint.getTextBounds(text, 0, text.length(), mBounds); //Return in bounds (allocated by the caller) the smallest rectangle that encloses all of the characters, with an implied origin at (0,0).
    int textWidth = mBounds.width();

两种办法都可以。

三、画图->onDraw

前面我们已经看完了,一个自定义View如何进行初始化和确定尺寸的,现在则看看到底是如何来画图的。

    protected void onDraw(Canvas canvas) {
        Log.i(TAG, "onDraw");
        super.onDraw(canvas);
        mPiePaint.setColor(mColor);
        mPiePaint.setStyle(Paint.Style.FILL);
        canvas.drawArc(rectF, 180, 360*getProgress()/getMax(), true, mPiePaint );
        if (mShowBorder) {
            mPiePaint.setColor(mBorderColor);
            mPiePaint.setStyle(Paint.Style.STROKE);
            canvas.drawArc(rectF, 180, 360*getProgress()/getMax(), true, mPiePaint );
        }
        if ( mShowText ) {
            String text = String.valueOf(getProgress());
            mTextPaint.getTextBounds(text, 0, text.length(), mBounds);
            int textWidth = mBounds.width();
            int textHeight = mBounds.height();
            switch (mTextPosition) {
                case TEXT_POSITION_LEFT:
                    canvas.drawText(text, width/2 - textWidth, height/2 + textHeight/2, mTextPaint);
                    break;
                case TEXT_POSITION_MIDDLE:
                    canvas.drawText(text, width/2 - textWidth/2, height/2 + textHeight/2, mTextPaint);
                    break;
                case TEXT_POSITION_RIGHT:
                    canvas.drawText(text, width/2, height/2 + textHeight/2, mTextPaint);
                    break;
            }
        }
    }

onDraw方法是我们必须要实现的一个方法,说简单一点,该方法就是用来画图的,怎么画呢?

在onDraw方法中有一个参数Canvas,Canvas即画板,在该画板上画图,则会直接显示在界面上。我们来看看我们具体是怎么画的图。

  1. 我们首先调用了super.onDraw(canvas):其实分析到这里,我们可以知道,View的onDraw(Canvas canvas)方法中,什么工作也没有做,因此这段代码可以省去。
  2. 在canvas画扇形,需要注意的是,我们同时利用mPiePaint来画扇形和边界,因此此时我们应该先将画笔设置的Style设置为Paint.Style.FILL。
  3. 判断是否需要画边界,则画出边界
  4. 判断是否需要画文字,如果需要,则画出文字。

四、触摸事件->onTouchEvent

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mUpdatingThread = new UpdatingThread();
                mUpdatingThread.start();;
                break;
            case MotionEvent.ACTION_UP:
                if(mUpdatingThread!=null && mUpdatingThread.isAlive()) {
                    mUpdatingThread.finish();;
                    mUpdatingThread.interrupt();;
                }
                break;

            default:
                return true;
        }
        return true;
    }

触摸事件,我们只需要实现onTouchEvent就可以,需要注意的是,其中的返回值,如果返回true,那么说明此事件已经被View处理,不需要再分发,如果为false,则说明此事件还需要被其他的View处理。

在View的文档中提到,如果说我们要处理点击事件的话,最好的方法不是覆写onTouchEvent方法,而是覆写performClick()方法,可以获得以下的好处:

  • obeying click sound preferences
  • dispatching OnClickListener calls
  • handling ACTION_CLICK when accessibility features are enabled

我们这里要处理一直按下的状态,当按下的时候,数字每1s加一,当松手的时候停止,因此我们设计的逻辑是,当按下的时候,启动一个线程处理,松手的时候,停止该线程即可。

    private class UpdatingThread extends Thread {
        private volatile boolean isRunning = true;

        @Override
        public void run() {
            while (!Thread.interrupted() &&isRunning) {
                if (mProgress < mMax) {
                    ProgressPie.this.post(new Runnable() {
                        @Override
                        public void run() {
                            ProgressPie.this.setProgress(++mProgress);
                        }
                    });
                }
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    //e.printStackTrace();;
                }
            }
        }

        public void finish() {
            isRunning = false;
        }
    }

需要注意的是,为了访问UI线程的空间,我们使用了View的post(Runnable runnable)方法。

好了,各个部分就是这样,具体的详细代码可以参考:

https://github.com/happyhls/AndroidDemo/blob/master/app/src/main/java/me/happyhls/androiddemo/view/ProgressPie.java

 

App Components : Intents and Intent Filters (译)

API Guides:http://developer.android.com/guide/components/intents-filters.html

Intent是一种消息组件,用来从一个app component向另外一个app component请求一个动作。在系统内部Intent的传递流程如下:

虽然在components中有着灵活变化的通信方式,但有3种最基本的用法:

  1. 启动activity
  2. 启动service
  3. 发布broadcast

Intent的类型主要有两种:

  • 显式的Intent(Explicit intents):显式Intent会通过名字(完整的类名)指定要启动的组建。因为在在自己的程序中,我们知道具体的要启动的activity或者service的类名,所以一般程序内部会使用这种方式来启动。
  • 隐式的Intent(Implicit intents):不会指定具体的要启动的类名,但会定义一种相对通用的action请求以允许其他的程序来处理。

如果使用显示的Intent的话,那么系统会立刻启用Intent指定的系统组件;但如果使用隐式的Intent,Android系统会从系统中查找程序的manifest文件中定义的与intent filters相匹配的intent。如果系统找到一个,那么就会启动相应的component,并传入相应的Intent;如果同时匹配了多个intent,系统会弹出一个dialog,让用户选择一个执行。

Intent filter是在应用程序的manifest文件中的一段表达式,用来表示该系统组件所接收消息的类型。比如说,可以我们可以为activity声明一个intent-filter来允许其他的应用程序直接调用该的activity。因此,如果说一个activity没有设置任何的intent-filter,那么该acitivy只会接收显示intent.

注意:为了保证我们的应用程序的安全,要使用显式intent来启动Service,并且不要为Service声明任何的intent-filter对象。如果我们使用隐士intent来启动service的话是无法保证其安全性的,因为我们无法确定service该如何相应这个intent,而且用户也无法知道哪个service被启动了。从Android5.0(API 21)开始,如果我们通过隐士的intent来调用bindService()的时候,系统会抛出一个异常。

构建一个Intent

Intent包含了必要的信息来让Android系统判断需要启动哪个系统组件(比如一个准确的系统组件的name或者是组件的category),还有一个额外的信息用来保证启动的系统组件可以优雅的处理好接收到的请求(比如action或者data)。

 

Intent一般包含以下的信息:

  • Component name
    • 要启动的组件名称,该项可选,但如果需要使用显式Intent除外。如果没有Component name,那么该intent就是一个隐式intent,由系统来根据附加在该intent上的其他信息,比如action,data,category等,决定哪个系统组件来接收这个intent。如果所以,如果说我们需要在应用程序内部启动特定的系统组件,那么我们就应该指定Component name。
    • 注意:当启动Service的时候,我们必须总是指定component name。要不然的话,我们无法确定会由哪个Service来处理这个intent,并且用户这不会知道哪个Service被启动了。
    • Intent中的该部分内容是一个ComponentName对象,我们可以使用完整名称的类名(包括包名)来制定目标组件。比如:com.example.ExampleActivity。我们可以通过 setComponent(), setClass(),setClassName(), 或者 Intent的构造器来设置组件名称。
  • Action
    • 一个用于指定所要执行的通用动作的字符串(比如view或者pick)。在broadcast的使用情景中,这对应着正在发生并且需要报告的action。Action很大部分定下了intent后面的结构是如何构成的,尤其是在data和extras中包含了什么样的信息。
    • 我们可以在我们的应用程序内部指定我们自己的intents需要使用的actions(或者供其他的程序调用我们应用程序内部的系统组件),但是我们一般情况下还是使用Intent类或者其他的framework类定义的Action常量。这里有一些常用的用来启动activity的actions:
      • ACTION_VIEW:如果我们要启动一应用程序来展示信息,比如要展示一个照片可以启动gallery应用程序,或者在map的应用程序中展示地址信息。
      • ACTION_SEND:还有另外一个名称“share” intent,我们如果有一些消息要在应用程序之间共享。
      • 可以通过查看Intent类来查看共多的通用actions的常量。其他的一些actions可能在Android框架层的其他部分定义,比如Settings中定义了一些启动Settings中特定界面的actions。
    • 我们可以使用setAction()或者Intent的构造函数来指定action。
    • 如果我们要定义我们自己的action,那么一定不要忘记包含自己的包名作为前缀,比如:
      static final String ACTION_TIMETRAVEL = "com.example.action.TIMETRAVEL";
  • Data:
    • 我们所需要处理的数据的MIME格式所对应的URI(一个Uri对象)。数据的类型由intent中的Action属性确定。比如如果一个action是 ACTION_EDIT,那么数据应该包含我们要编辑的文档的URI。
    • 当创建一个intent的时候,除了指定URI之外,指定数据的格式(MIME类型)也是特别重要的。比如,即使URI格式是一样的,但一个能够显示照片的程序,并不能播放一段媒体音乐。所以,如果为我们的数据指定MIME类型,就能够帮助Android系统更好的找到接收处理该Intent的系统组件。然后,有的时候,MIME的类型是从URI中继承来的-尤其是当数据是content:URI,这意味着数据是保存在设备当中,并且由ContentProvider控制的,这时候数据的MIME类型系统是可以探测到的。
    • 如果仅仅需要设置数据的URI,调用setData()。仅仅设置MIME类型,调用setType(),如果两者都要设定,调用setDataAndType()
      • 注意:如果我们要同时设置URI和MIME类型的话,不要调用setData()以及setType(),因此调用setData()或者setType()的时候会将另一个设置为null。这个时候一定要调用setDataAndType().
  • Category:
    • 一个包含了额外的其他用来指定处理此intent的系统组件类型的字符串。一个intent可以包含任意数量的itnent。但大多数intent并不需要包含一个category。这里展示一些常用的CATEGORY。
      • CATEGORY_BROWSABLE:目标Activity允许被web浏览器调用来通过一个链接,比如image或者e-mail来展示数据。
      • CATEGORY_LAUNCHER:该Activity为应用程序的初始化activity,并且会在系统的Launcher中展示出来。
      • 其他的可以参考Intent的类来看下其他的categories。
      • 我们可以通过addCategory()来添加Category。

上面所描述的属性,包括Component name,Action,data(Uri,MIME type),Category定义了一个intent的典型特征。通过上面的信息,Android系统能够找到要启动的系统组件。

然而,一个intent可以携带的信息并不仅仅只有上面所列出的,还有其他的信息,但并不会对查找对应的component产生影响。

  • Extras:Key-Value对。可以承载一些额外的信息,比如data URIs。
    • 通过putExtra()方法来添加。或者也可以创建一个Bundle对象来包含这些信息,然后把这个Bundle放进Intent中去。
    • Intent类为标准数据类型指定了一些经常使用的EXTRA_*常量。但我们可以为自己的程序制定声明自己的数据。没用过
      static final String EXTRA_GIGAWATTS = "com.example.EXTRA_GIGAWATTS";

       

  • Flags:可以用来展示如何启动一个activity(比如activity应该输入哪个task),对应的activity启动之后,如何处理(比如是否要让其在最近的activities中显示)。详细的可以参考setFlag()方法。

如何使用显示Intent

使用显示的Intent,必须要指定Component Name,其他的属性都可以不设置,比如:

// Executed in an Activity, so 'this' is the Context
// The fileUrl is a string URL, such as "http://www.example.com/image.png"
Intent downloadIntent = new Intent(this, DownloadService.class);
downloadIntent.setData(Uri.parse(fileUrl));
startService(downloadIntent);

如何使用隐式Intent

// Create the text message with a string
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage);
sendIntent.setType(HTTP.PLAIN_TEXT_TYPE); // "text/plain" MIME type

// Verify that the intent will resolve to an activity
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(sendIntent);
}

强制显示应用程序选择器

有的时候,我们需要应用程序必须弹出应用程序的选择界面来供用户选择,可以按照下面的方法:

Intent sendIntent = new Intent(Intent.ACTION_SEND);
...

// Always use string resources for UI text.
// This says something like "Share this photo with"
String title = getResources().getString(R.string.chooser_title);
// Create intent to show the chooser dialog
Intent chooser = Intent.createChooser(sendIntent, title);

// Verify the original intent will resolve to at least one activity
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(chooser);
}

区别在于,这里使用Intent.createChoose(Intent,String)的方法包装了原有的Inent。

接收隐式Intent

如果需要接收一个隐式的Intent,那么需要在程序的menifest file中配置对应的<intent-filter>,每个intent-filter都需要指定接收的intent的action,data,category。如果一个Intent的数据与我们的intent-filter相匹配的话,那么系统就会调用相应的Intent
系统会根据此配置将其发送到相应的位置。
注意,显式intent不会关心intent-filter中设置的内容,因为其本身已经制定Component Name,不需要额外的信息来查找要启动的系统组件。
每一个系统组件都应该指定与其所能做的工作相对应的intent-filter。比如,在gallery的一个activity可能会有两个filters:一个用来查看图片,一个用来编辑图片。当该activity启动的时候,会根据intent的内容来决定要如何处理。
每一个intent filter都是通过应用程序中的manifest中,对应的系统组件之内的<intent-filter>元素来定义的。在其中我们可以为其指定action元素,或者更多。
  • <action>:定义了intent接收到 action类型,在name属性中。该属性必须是一个action字符串,而不是类的常量。
  • <data>:定义了接收的数据类型,使用一个或者更多的属性来识别data URI的不同片段,比如scheme,host,port,path以及MIME类型。
  • <category>:定义了intent接收的category类型,在name属性中。同action一样,比如为字符串。
    • 需要注意的是:为了能够使得隐式Intent能够通过 startActivity() and startActivityForResult() 启动组件,比如要在其中的cagegory中添加CATEGORTY_DEFAULT,否则的话,该组件不会被系统识别。
<activity android:name="ShareActivity">
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
</activity>

当然,如果我们需要,可以在一个intent-filter当中指定多个<action>, <data>, or <category>,但如果我们这样做的话,只有在这里面的信息全部匹配的时候,对应的component才会被系统调用。

如果我们我们需要处理多种类型的intents,但仅仅是指定的action,data,category组合,那么我们需要创建多个intent-filter。

一个隐式的inent要看和filter是否匹配,会一次匹配上面的3个元素。

注意:为了避免不小心运行了其他的应用程序的Service,我们始终要记住使用显式Intent启动Service,并不要为Service设置任何intent-filter。

笔记:对于activities,intent-filter必须在manifest文件中声明。但broadcast receivers所使用的intent-filter可以通过调用 registerReceiver()unregisterReceiver()来实现。

限制component的访问权限

使用intent filter并不是一个安全的方法来避免其他的应用程序来启动我们的系统组件。虽然intent filters限制了一个系统组件仅仅会相应设置的一种或者几种对应的隐式intent,但其他的应用程序如果开发者知道你的系统组件的component names的话,仍然可以启动。因此为了保证仅仅我们自己的程序能够启动相应的组件,我们应该为相应的component设置exported标签为false。

例子

<activity android:name="MainActivity">
    <!-- This activity is the main entry, should appear in app launcher -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

<activity android:name="ShareActivity">
    <!-- This activity handles "SEND" actions with text data -->
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
    <!-- This activity also handles "SEND" and "SEND_MULTIPLE" with media data -->
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <action android:name="android.intent.action.SEND_MULTIPLE"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="application/vnd.google.panorama360+jpg"/>
        <data android:mimeType="image/*"/>
        <data android:mimeType="video/*"/>
    </intent-filter>
</activity>

 

Using a Pending Intent

PendingIntent是对Intent的封装。使用PendingIntent的主要目的之一在于保证其他的应用程序能够获得对应的权限,就好像这个intent是从我们自己的程序中自己的进程中调用的一样。
PendingIntent主要的使用于:
  • Notification中,用户点击Notification,然后系统的NotificationManager运行这个Intent
  • App Widget。当用户点击桌面上的小控件的时候,桌面程序执行的Intent。
  • Alarm。以后在某个特定时间去执行的Intent,比如Android的AlarmManager在固定时间去执行的这个Intent。
由于在系统中,大量的PendingIntent在设计的时候其目的是相同的,因此必须为了不同的PendingIntent中的不同的组件类型设置不同的PendingIntent。有以下3中调用。
根据API Guide的文档,除非应用程序是在接受来自其他的app的PendingIntents,那么以上的方法应该是使用的所有的方法。

Intent Resolution

注意下data的识别原则:
每个intent-filter可以包含0个或者多个data元素,比如说:
<intent-filter>
<data android:mimeType="video/mpeg" android:scheme="http" ... />
<data android:mimeType="audio/mpeg" android:scheme="http" ... />
...
</intent-filter>
每一个<data>元素都可以确定一个URI结构和数据类型(MIME media type)。有几个分开的属性,scheme,host,port,path。
<scheme>://<host>:<port>/<path>
content://com.example.project:200/folder/subfolder/etc
每一个属性都是可选的,但是遵循以下的线性规则:
  • 如果scheme没有确定,那么host会被忽略。
  • 如果host没有确定,那么port会被忽略。
  • 如果scheme和host都没有被设定,path会被忽略。
在对URI验证的时候,是按照以下的思路进行:
  • 如果filter仅仅确定了scheme,那么所有有用同样的scheme的URIs会被match
  • 如果确定了scheme和authority,但是没有path;那么所有的URIs会比对schme和authority,忽略path
  • 如果scheme,authority,path都确定了,那么只有所有的全部一样的时候才会被确定。
Note: A path specification can contain a wildcard asterisk (*) to require only a partial match of the path name.
如果在intent-filter中同时设定了URI和MIME类型,那么会按照以下的规则判断:
  1. 如果一个intent既不包含URI,也不包含MIME,那么仅仅会通过没有指定URI或者MIME的filter。
  2. 如果一个intent包含URI,但不包含MIME(既不单独指定,也无法从URI中获取),那么仅仅会通过指定了URI并且match,而MIME又没有配置的filter。
  3. 如果一个intent包含了MIME类型,但是不包含URI,那么仅仅会通过包含了MIME并且match,而且不包含URI的filter
  4. 如果一个intent即包含URI,也包含MIME(可以单独设定,也可以是从URI中获取),那么MIME部分会判断是不是符合,对于URI部分,如果URI match 或者 在filter不识别URI的时候,URI包含content:或者file:,那么会认为match。换句话说,一个filter仅支持MIME类型的时候,组件是默认支持content:以及file:的。