标签归档:Android

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具体是将图片怎么样绘制到界面上的,在下一篇文章中,将对这些内容进行分析。

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:的。

Android中Activity四种启动模式和taskAffinity属性详解 (转载)

转自http://blog.csdn.net/zhangjg_blog/article/details/10923643 感谢原作者。

在android应用开发中,打造良好的用户体验是非常重要的。而在用户体验中,界面的引导和跳转是值得深入研究的重要内容。在开发中,与界面跳转联系比较紧密的概念是Task(任务)和Back Stack(回退栈)。activity的启动模式会影响Task和Back Stack的状态,进而影响用户体验。除了启动模式之外,Intent类中定义的一些标志(以FLAG_ACTIVITY_开头)也会影响Task和Back Stack的状态。在这篇文章中主要对四种启动模式进行分析和验证,其中涉及到activity的一个重要属性taskAffinity和Intent中的标志之一FLAG_ACTIVITY_NEW_TASK。关于Intent中其他标志位的具体用法会在另一篇文章中介绍。

Task是一个存在于Framework层的概念,容易与它混淆的有Application(应用)和Process(进程)。在开始介绍Activity的启动模式的使用之前,首先对这些概念做一个简单的说明和区分。

一 Application,Task和Process的区别与联系。

application翻译成中文时一般称为“应用”或“应用程序”,在android中,总体来说一个应用就是一组组件的集合。众所周知,android是在应用层组件化程度非常高的系统,android开发的第一课就是学习android的四大组件。当我们写完了多个组件,并且在manifest文件中注册了这些组件之后,把这些组件和组件使用到的资源打包成apk,我们就可以说完成了一个application。application和组件的关系可以在manifest文件中清晰地体现出来。如下所示:

<?xml version="1.0" encoding="utf-8"?>  
<manifest android:versionCode="1"  
        android:versionName="1"  
        xmlns:android="http://schemas.android.com/apk/res/android"  
        package="com.example.android.myapp">  
  
    <application android:label="@string/app_name">  
        <activity android:name=".MyActivity" android:label="@string/app_nam">  
            <intent-filter>  
                <action android:name="android.intent.action.MAIN" />  
                <category android:name="android.intent.category.LAUNCHER" />  
            </intent-filter>  
        </activity>  
    <receiver android:name=".MyReceiver"/>  
    <provider android:name=".MyProvider"/>  
    <service android:name=".MyService"/>  
    </application>  
</manifest>

由此可见,application是由四大组件组成的。在app安装时,系统会读取manifest的信息,将所有的组件解析出来,以便在运行时对组件进行实例化和调度。

而task是在程序运行时,只针对activity的概念。说白了,task是一组相互关联的activity的集合,它是存在于framework层的一个概念,控制界面的跳转和返回。这个task存在于一个称为back stack的数据结构中,也就是说,framework是以栈的形式管理用户开启的activity。这个栈的基本行为是,当用户在多个activity之间跳转时,执行压栈操作,当用户按返回键时,执行出栈操作。举例来说,如果应用程序中存在A,B,C三个activity,当用户在Launcher或Home Screen点击应用程序图标时,启动主Activity A,接着A开启B,B开启C,这时栈中有三个Activity,并且这三个Activity默认在同一个任务(task)中,当用户按返回时,弹出C,栈中只剩A和B,再按返回键,弹出B,栈中只剩A,再继续按返回键,弹出A,任务被移除。如下图所示:

laucnhMode-1

task是可以跨应用的,这正是task存在的一个重要原因。有的Activity,虽然不在同一个app中,但为了保持用户操作的连贯性,把他们放在同一个任务中。例如,在我们的应用中的一个Activity A中点击发送邮件,会启动邮件程序的一个Activity B来发送邮件,这两个activity是存在于不同app中的,但是被系统放在一个任务中,这样当发送完邮件后,用户按back键返回,可以返回到原来的Activity A中,这样就确保了用户体验。

说完了application和task,最后介绍process。process一般翻译成进程,进程是操作系统内核中的一个概念,表示直接受内核调度的执行单位。在应用程序的角度看,我们用java编写的应用程序,运行在dalvik虚拟机中,可以认为一个运行中的dalvik虚拟机实例占有一个进程,所以,在默认情况下,一个应用程序的所有组件运行在同一个进程中。但是这种情况也有例外,即,应用程序中的不同组件可以运行在不同的进程中。只需要在manifest中用process属性指定组件所运行的进程的名字。如下所示:

<activity android:name=".MyActivity" android:label="@string/app_nam"  
    android:process=":remote">  
</activity>

这样的话这个activity会运行在一个独立的进程中。

二 Activity四种启动模式详解

activity有四种启动模式,分别为standard,singleTop,singleTask,singleInstance。如果要使用这四种启动模式,必须在manifest文件中<activity>标签中的launchMode属性中配置,如:

<activity android:name=".app.InterstitialMessageActivity"  
          android:label="@string/interstitial_label"  
          android:theme="@style/Theme.Dialog"  
          android:launchMode="singleTask"  
</activity>

同样,在Intent类中定义了很多与Activity启动或调度有关的标志,<activity>标签中有一些属性,这些标志,属性和四种启动模式联合使用,会在很大程度上改变activity的行为,进而会改变task和back stask的状态。关于Intent中的标志和<activity>标签中有一些属性会在本文后面介绍,在这一节中,先介绍activity的四种启动模式。

  • standard
    • 标准启动模式,也是activity的默认启动模式。在这种模式下启动的activity可以被多次实例化,即在同一个任务中可以存在多个activity的实例,每个实例都会处理一个Intent对象。如果Activity A的启动模式为standard,并且A已经启动,在A中再次启动Activity A,即调用startActivity(new Intent(this,A.class)),会在A的上面再次启动一个A的实例,即当前的桟中的状态为A–>A。
  • singleTop
    • 如果一个以singleTop模式启动的activity的实例已经存在于任务桟的桟顶,那么再启动这个Activity时,不会创建新的实例,而是重用位于栈顶的那个实例,并且会调用该实例的onNewIntent()方法将Intent对象传递到这个实例中。举例来说,如果A的启动模式为singleTop,并且A的一个实例已经存在于栈顶中,那么再调用startActivity(new Intent(this,A.class))启动A时,不会再次创建A的实例,而是重用原来的实例,并且调用原来实例的onNewIntent()方法。这是任务桟中还是这有一个A的实例。
    • 如果以singleTop模式启动的activity的一个实例已经存在与任务桟中,但是不在桟顶,那么它的行为和standard模式相同,也会创建多个实例。
  • singleTask
    • 谷歌的官方文档上称,如果一个activity的启动模式为singleTask,那么系统总会在一个新任务的最底部(root)启动这个activity,并且被这个activity启动的其他activity会和该activity同时存在于这个新任务中。如果系统中已经存在这样的一个activity则会重用这个实例,并且调用他的onNewIntent()方法。即,这样的一个activity在系统中只会存在一个实例。
    • 其实官方文档中的这种说法并不准确,启动模式为singleTask的activity并不会总是开启一个新的任务。详情请参考 解开Android应用程序组件Activity的”singleTask”之谜,在本文后面也会通过示例来进行验证。
  • singleInstance
    • 总是在新的任务中开启,并且这个新的任务中有且只有这一个实例,也就是说被该实例启动的其他activity会自动运行于另一个任务中。当再次启动该activity的实例时,会重用已存在的任务和实例。并且会调用这个实例的onNewIntent()方法,将Intent实例传递到该实例中。和singleTask相同,同一时刻在系统中只会存在一个这样的Activity实例。

三 实例验证singleTask启动模式

上面将activity的四种启动模式就基本介绍完了。为了加深对启动模式的了解,下面会通过一个简单的例子进行验证。由以上的介绍可知,standard和singleTop这两种启动模式行为比较简单,所以在下面的例子中,会对singleTask和singleInstance着重介绍。

验证启动singleTask模式的activity时是否会创建新的任务

以下为验证示例AndroidTaskTest。这个实例中有三个Activity,分别为:MainActivity,SecondActivity和ThirdActivity。以下为这个示例的manifest文件。

<?xml version="1.0" encoding="utf-8"?>  
<manifest xmlns:android="http://schemas.android.com/apk/res/android"  
    package="com.jg.zhang.androidtasktest"  
    android:versionCode="1"  
    android:versionName="1.0" >  
  
    <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="17" />  
  
    <application android:icon="@drawable/ic_launcher" android:label="@string/app_name">  
        <activity  android:label="@string/app_name"  
            android:name="com.jg.zhang.androidtasktest.MainActivity">  
            <intent-filter>  
                <action android:name="android.intent.action.MAIN" />  
                <category android:name="android.intent.category.LAUNCHER" />  
            </intent-filter>  
        </activity>  
          
        <!--android:taskAffinity="com.jg.zhang.androidtasktest.second"   
            android:alwaysRetainTaskState="true"  
            android:allowBackup="true" -->  
              
         <activity android:name="com.jg.zhang.androidtasktest.SecondActivity"  
             android:launchMode="singleTask">  
            <intent-filter >  
                <action android:name="com.jg.zhang.androidtasktest.SecondActivity"/>  
                <category android:name="android.intent.category.DEFAULT"/>  
            </intent-filter>  
        </activity>  
          
         <activity android:name="com.jg.zhang.androidtasktest.ThirdActivity"  
            android:label="@string/app_name" >  
        </activity>  
    </application>  
      
</manifest>

由此可见,MainActivity和ThirdActivity都是标准的启动模式,而SecondActivity的启动模式为singleTask。

以下为这三个Activity的界面,很简单,在MainActivity中点击按钮启动SecondActivity,在SecondActivity中点击按钮启动ThirdActivity。

 

以下为这三个activity的主要代码:

MainActivity
laucnhMode-2

MainActivity

public class MainActivity extends Activity {  
  
    private static final String ACTIVITY_NAME = "MainActivity";  
    private static final String LOG_TAG = "xxxx";  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
          
        findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {  
            @Override  
            public void onClick(View v) {  
                Intent intent = new Intent(MainActivity.this, SecondActivity.class);  
  
                startActivity(intent);  
            }  
        });  
          
        int taskId = getTaskId();  
        Log.i(LOG_TAG, ACTIVITY_NAME +"所在的任务的id为: " +  taskId);  
    }

SecondActivity

public class SecondActivity extends Activity {  
    private static final String ACTIVITY_NAME = "SecondActivity";  
    private static final String LOG_TAG = "xxxx";  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_second);  
          
        findViewById(R.id.button2).setOnClickListener(new OnClickListener() {  
            @Override  
            public void onClick(View v) {  
                    Intent intent = new Intent(SecondActivity.this, ThirdActivity.class);  
                    startActivity(intent);  
            }  
        });  
          
        int taskId = getTaskId();  
        Log.i(LOG_TAG, ACTIVITY_NAME +"所在的任务的id为: " +  taskId);  
          
    }

ThirdActivity

public class ThirdActivity extends Activity {  

      
    private static final String ACTIVITY_NAME = "ThirdActivity";  
    private static final String LOG_TAG = "xxxx";  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_third);  
        int taskId = getTaskId();  
        Log.i(LOG_TAG, ACTIVITY_NAME +"所在的任务的id为: " +  taskId);  
    }

以上三个activity只列出了onCreate()方法中的内容,实现的逻辑为在MainActivity中点击按钮启动SecondActivity,在SecondActivity中点击按钮启动ThirdActivity。并且在onCreate方法中会以log的形式打印出当前activity所属的任务(Task)的Id。

现在执行以下操作,运行该示例,并且点击MainActivity界面中的按钮,开启SecondActivity。在该示例中SecondActivity的启动模式为singleTask。按照官方文档的说法,SecondActivity会在一个新的任务中开启。但是查看打印出的log,发现MainActivity和SecondActivity所在的任务的Id相同。

laucnhMode-3

在命令行中执行以下命令 adb shell dumpsys activity , 有以下输出:

TaskRecord{412ded08 #8 A com.jg.zhang.androidtasktest}
    Run #2: ActivityRecord{412c91e8 com.jg.zhang.androidtasktest/.SecondActivity}
    Run #1: ActivityRecord{412c08a0 com.jg.zhang.androidtasktest/.MainActivity}

所以,和官方文档表述的不同,MainActivity和SecondActivity是启动在同一个任务中的。其实,把启动模式设置为singleTask,framework在启动该activity时只会把它标示为可在一个新任务中启动,至于是否在一个新任务中启动,还要受其他条件的限制。现在在SecondActivity增加一个taskAffinity属性,如下所示:

<activity android:name="com.jg.zhang.androidtasktest.SecondActivity"  
    android:launchMode="singleTask"  
    android:taskAffinity="com.jg.zhang.androidtasktest.second">  
   <intent-filter >  
       <action android:name="com.jg.zhang.androidtasktest.SecondActivity"/>  
       <category android:name="android.intent.category.DEFAULT"/>  
   </intent-filter>  
</activity>

重新运行该示例,执行相同的操作,即:点击MainActivity界面中的按钮,开启SecondActivity,并且点击SecondActivity中的按钮,启动ThirdActivity,log中输出的内容为:

laucnhMode-4

在命令行中执行adb shell dumpsys activity命令,有以下输出:

TaskRecord{411e6a88 #6 A com.jg.zhang.androidtasktest.second}
    Run #3: ActivityRecord{411c8ea0 com.jg.zhang.androidtasktest/.ThirdActivity}
    Run #2: ActivityRecord{412bc870 com.jg.zhang.androidtasktest/.SecondActivity}
TaskRecord{412ece18 #5 A com.jg.zhang.androidtasktest}
    Run #1: ActivityRecord{412924c0 com.jg.zhang.androidtasktest/.MainActivity}

由此可见,MainActivity和SecondActivity运行在不同的任务中了,并且被SecondActivity启动的ThirdActivity和SecondActivity运行在同一个任务中。这种现象的具体解释可以参考解开Android应用程序组件Activity的”singleTask”之谜

在这里便引出了manifest文件中<activity>的一个重要属性,taskAffinity。在官方文档中可以得到关于taskAffinity的以下信息

  1. taskAffinity表示当前activity具有亲和力的一个任务(翻译不是很准确,原句为The task that the activity has an affinity for.),大致可以这样理解,这个 taskAffinity表示一个任务,这个任务就是当前activity所在的任务。
  2. 在概念上,具有相同的affinity的activity(即设置了相同taskAffinity属性的activity)属于同一个任务。
  3. 一个任务的affinity决定于这个任务的根activity(root activity)的taskAffinity。
  4. 这个属性决定两件事:当activity被re-parent时,它可以被re-paren哪个任务中;当activity以FLAG_ACTIVITY_NEW_TASK标志启动时,它会被启动到哪个任务中。(这个比较    难以理解,请结合<activity>中的属性allowTaskReparenting和Intent中的标志       FLAG_ACTIVITY_NEW_TASK加以理解)
  5. 默认情况下,一个应用中的所有activity具有相同的taskAffinity,即应用程序的包名。我们可以通过设置不同的taskAffinity属性给应用中的activity分组,也可以把不同的       应用中的activity的taskAffinity设置成相同的值。
  6. 为一个activity的taskAffinity设置一个空字符串,表明这个activity不属于任何task。

这就可以解释上面示例中的现象了,由第5条可知,MainActivity和SecondActivity具有不同的taskAffinity,MainActivity的taskAffinity为com.jg.zhang.androidtasktest,SecondActivity的taskAffinity为com.jg.zhang.androidtasktest.second,根据上面第4条,taskAffinity可以影响当activity以FLAG_ACTIVITY_NEW_TASK标志启动时,它会被启动到哪个任务中。这句话的意思是,当新启动的activity(SecondActivity)是以FLAG_ACTIVITY_NEW_TASK标志启动时(可以认为FLAG_ACTIVITY_NEW_TASK和singleTask作用相同,当启动模式为singleTask时,framework会将它的启动标志设为FLAG_ACTIVITY_NEW_TASK),framework会检索是否已经存在了一个affinity为com.jg.zhang.androidtasktest.second的任务(即一个TaskRecord对象)

  • 如果存在这样的一个任务,则检查在这个任务中是否已经有了一个SecondActivity的实例
    • 如果已经存在一个SecondActivity的实例,则会重用这个任务和任务中的SecondActivity实例,将这个任务调到前台,清除位于SecondActivity上面的所有Activity,显示SecondActivity,并调用SecondActivity的onNewIntent();
    • 如果不存在一个SecondActivity的实例,会在这个任务中创建SecondActivity的实例,并调用onCreate()方法
  • 如果不存在这样的一个任务,会创建一个新的affinity为com.jg.zhang.androidtasktest.second的任务,并且将SecondActivity启动到这个新的任务中

上面讨论的是设置taskAffinity属性的情况,如果SecondActivity只设置启动模式为singleTask,而不设置taskAffinity,即三个Activity的taskAffinity相同,都为应用的包名,那么SecondActivity是不会开启一个新任务的,framework中的判定过程如下:

  1. 在MainActivity启动SecondActivity时,发现启动模式为singleTask,那么设定他的启动标志为FLAG_ACTIVITY_NEW_TASK
  2. 然后获得SecondActivity的taskAffinity,即为包名com.jg.zhang.androidtasktest
  3. 检查是否已经存在一个affinity为com.jg.zhang.androidtasktest的任务,这个任务是存在的,就是MainActivity所在的任务,这个任务是在启动MainActivity时开启的
  4. 既然已经存在这个任务,就检索在这个任务中是否存在一个SecondActivity的实例,发现不存在
  5. 在这个已有的任务中启动一个SecondActivity的实例

为了作一个清楚的比较,列出SecondActivity启动模式设为singleTask,并且taskAffinity设为com.jg.zhang.androidtasktest.second时的启动过程

  1. 在MainActivity启动SecondActivity时,发现启动模式为singleTask,那么设定他的启动标志为FLAG_ACTIVITY_NEW_TASK
  2. 然后获得SecondActivity的taskAffinity,即com.jg.zhang.androidtasktest.second
  3. 检查是否已经存在一个affinity为com.jg.zhang.androidtasktest.second的任务,这个任务是不存在的
  4. 创建一个新的affinity为com.jg.zhang.androidtasktest.second的任务,并且将SecondActivity启动到这个新的任务中

其实framework中对任务和activity‘的调度是很复杂的,尤其是把启动模式设为singleTask或者以FLAG_ACTIVITY_NEW_TASK标志启动时。所以,在使用singleTask和FLAG_ACTIVITY_NEW_TASK时,要仔细测试应用程序。这也是官方文档上的建议。

实例验证将两个不同app中的不同的singleTask模式的Activity的taskAffinity设成相同

官方文档中提到,可以把不同的 应用中的activity的taskAffinity设置成相同的值,这样的话这两个activity虽然不在同一应用中,却会在运行时分配到同一任务中,下面对此进行验证,在这里,会使用上面的示例AndroidTaskTest,并创建一个新的示例AndroidTaskTest1。AndroidTaskTest1由两个activity组成,分别为MianActivity和OtherActivity,在MianActivity中点击按钮会启动OtherActivity,该程序的界面和上一个类似,代码也类似,再此仅列出清单文件。

<?xml version="1.0" encoding="utf-8"?>  

<manifest xmlns:android="http://schemas.android.com/apk/res/android"  
    package="com.jg.zhang.androidtasktest1"  
    android:versionCode="1"  android:versionName="1.0" >  
  
    <uses-sdk android:minSdkVersion="9"  android:targetSdkVersion="17" />  
  
    <application  
        android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name"  
        android:theme="@style/AppTheme" >  
        <activity  
            android:name="com.jg.zhang.androidtasktest1.MainActivity"  
            android:label="com.jg.zhang.androidtasktest1.MainActivity" >  
            <intent-filter>  
                <action android:name="android.intent.action.MAIN" />  
                <category android:name="android.intent.category.LAUNCHER" />  
            </intent-filter>  
        </activity>  
          
        <activity  
            android:name="com.jg.zhang.androidtasktest1.OtherActivity"  
            android:label="com.jg.zhang.androidtasktest1.OtherActivity"  
            android:taskAffinity="com.jg.zhang.androidtasktest.second"  
            android:launchMode="singleTask">  
        </activity>  
    </application>  
  
</manifest>

可以看到OtherActivity的启动模式被设置为singleTask,并且taskAffinity属性被设置为com.jg.zhang.androidtasktest.second,这和AndroidTaskTest应用中的SecondActivity相同。现在将这两个应用安装在设备上。执行以下操作:

启动AndroidTaskTest应用,在它的MianActivity中点击按钮开启SecondActivity,由上面的介绍可知secondActivity是运行在一个新任务中的,这个任务就是com.jg.zhang.androidtasktest.second。

然后按Home键回到Launcher,启动AndroidTaskTest1,在启动AndroidTaskTest1的入口Activity(MianActivity)时,会自动启动新的任务,那么现在一共有三个任务,AndroidTaskTest的MianActivity和SecondActivity分别占用一个任务,AndroidTaskTest1的MianActivity也占用一个任务。

在AndroidTaskTest1的MianActivity中点击按钮启动OtherActivity,那么这个OtherActivity是在哪个任务中呢?

下面执行adb shell dumpsys activity命令,发现有以下输出:

TaskRecord{412370c0 #4 A com.jg.zhang.androidtasktest.second} 
Intent { cmp=com.jg.zhang.androidtasktest/.SecondActivity }
      Hist #4: ActivityRecord{412f5ba0 com.jg.zhang.androidtasktest1/.OtherActivity}
          Intent { flg=0x400000 cmp=com.jg.zhang.androidtasktest1/.OtherActivity }
          ProcessRecord{412adb28 479:com.jg.zhang.androidtasktest1/10044}
      Hist #3: ActivityRecord{4125c880 com.jg.zhang.androidtasktest/.SecondActivity}
          Intent { cmp=com.jg.zhang.androidtasktest/.SecondActivity }
          ProcessRecord{41218e48 463:com.jg.zhang.androidtasktest/10043}

TaskRecord{412f0f60 #5 A com.jg.zhang.androidtasktest1} 
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.jg.zhang.androidtasktest1/.MainActivity }
      Hist #2: ActivityRecord{413045a8 com.jg.zhang.androidtasktest1/.MainActivity}
          Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.jg.zhang.androidtasktest1/.MainActivity }
          ProcessRecord{412adb28 479:com.jg.zhang.androidtasktest1/10044}
   
TaskRecord{412c5928 #3 A com.jg.zhang.androidtasktest}
 Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.jg.zhang.androidtasktest/.MainActivity }
      Hist #0: ActivityRecord{41250850 com.jg.zhang.androidtasktest/.MainActivity}
          Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.jg.zhang.androidtasktest/.MainActivity }
          ProcessRecord{41218e48 463:com.jg.zhang.androidtasktest/10043}

在执行上述操作时,打印出的Log为:

laucnhMode-5

所以由此可见,AndroidTaskTest的SecondActivity和AndroidTaskTest1的OtherActivity是在同一任务中的。由上面adb shell dumpsys activity命令的输出结果(蓝色字体)还可以看出,AndroidTaskTest和AndroidTaskTest1这两个应用程序会开启两个进程,他们的所有组件分别运行在独立的进程中,其中AndroidTaskTest所在进程的进程号为10043,AndroidTaskTest1所在进程的进程号为10044。com.jg.zhang.androidtasktest.second任务中的两个activity属于不同的应用,并且运行在不同的进程中,这也说明了一个问题:任务(Task)不仅可以跨应用(Application),还可以跨进程(Process)。

实例验证singleTask的另一意义:在同一个任务中具有唯一性

谷歌官方文档中提到,singleTask模式的activity总会在一个新的任务中开启。上面已经验证了这种说法不确切,singleTask模式只意味着“可以在一个新的任务中开启”,至于是不是真的会在新任务中开启,在framework中还有其他条件的限制。由上面的介绍可知,这个条件为:是否已经存在了一个由他的taskAffinity属性指定的任务。这一点具有迷惑性,我们在看到singleTask这个单词的时候,会直观的想到它的本意:single in task。即,在同一个任务中,只会有一个该activity的实例。现在让我们进行验证:

为了验证这种情况,需要修改一下上面用到的AndroidTaskTest示例。增加一个FourthActivity,并且MianActivity,SecondActivity,ThirdActivity和FourthActivity这四个activity都不设置taskAffinity属性,并且将SecondActivity启动模式设为singleTask,这样这四个activity会在同一个任务中开启。他们的开启流程是这样的:MianActivity开启SecondActivity,SecondActivity开启ThirdActivity,ThirdActivity开启FourthActivity,FourthActivity开启SecondActivity。代码和软件界面就不列出了,只列出清单文件

<?xml version="1.0" encoding="utf-8"?>  
<manifest xmlns:android="http://schemas.android.com/apk/res/android"  
    package="com.jg.zhang.androidtasktest"  
    android:versionCode="1"  
    android:versionName="1.0" >  
  
    <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="17" />  
  
    <application android:allowBackup="true"  
        android:icon="@drawable/ic_launcher" android:label="androidtasktest">  
          
        <activity  android:name="com.jg.zhang.androidtasktest.MainActivity">  
            <intent-filter>  
                <action android:name="android.intent.action.MAIN" />  
                <category android:name="android.intent.category.LAUNCHER" />  
            </intent-filter>  
        </activity>  
              
         <activity android:name="com.jg.zhang.androidtasktest.SecondActivity"  
             android:launchMode="singleTask"/>  
          
         <activity android:name="com.jg.zhang.androidtasktest.ThirdActivity"/>  
           
         <activity android:name="com.jg.zhang.androidtasktest.FourthActivity"/>  
           
    </application>  
      
</manifest>

现在从MianActivity一直启动到FourthActivity,打印出的系统Log为:

由此可见这四个activity都是在同一个任务中的。再次执行adb shell dumpsys activity命令加以验证:

TaskRecord{412e9458 #6 A com.jg.zhang.androidtasktest}
    Run #4: ActivityRecord{412e12e8 com.jg.zhang.androidtasktest/.FourthActivity}
    Run #3: ActivityRecord{412a9e30 com.jg.zhang.androidtasktest/.ThirdActivity}
    Run #2: ActivityRecord{412a4dd8 com.jg.zhang.androidtasktest/.SecondActivity}
    Run #1: ActivityRecord{4122fae0 com.jg.zhang.androidtasktest/.MainActivity}

同样可以说明目前这四个activity都运行在affinity为com.jg.zhang.androidtasktest的任务中,即栈中的状态为MainActivity –>  SecondActivity –> ThirdActivity –> FourthActivity。

下面执行在FourthActivity中点击按钮启动SecondActivity的操作,注意,SecondActivity的启动模式为singleTask,那么现在栈中的情况如何呢?再次执行adb shell dumpsys activity命令,有以下输出:

TaskRecord{412e9458 #6 A com.jg.zhang.androidtasktest}
    Run #2: ActivityRecord{412a4dd8 com.jg.zhang.androidtasktest/.SecondActivity}
    Run #1: ActivityRecord{4122fae0 com.jg.zhang.androidtasktest/.MainActivity}

这时栈中的状态为MainActivity –>  SecondActivity。确实确保了在任务中是唯一的,并且清除了同一任务中它上面的所有Activity。 那么这个SecondActivity的实例是重用的上次已有的实例还是重新启动了一个实例呢?可以观察系统Log, 发现系统Log没有改变,还是上面的四条Log。打印Log的语句是在各个Activity中的onCreate方法中执行的,没有打印出新的Log,说明SecondActivity的onCreate的方法没有重新执行,也就是说是重用的上次已经启动的实例,而不是销毁重建。

经过上面的验证,可以得出如下的结论:在启动一个singleTask的Activity实例时,如果系统中已经存在这样一个实例,就会将这个实例调度到任务栈的栈顶,并清除它当前所在任务中位于它上面的所有的activity。

四 实例验证singleInstance的行为

根据上面的讲解,并且参考谷歌官方文档,singleInstance的特点可以归结为以下三条:

  • 以singleInstance模式启动的Activity具有全局唯一性,即整个系统中只会存在一个这样的实例
  • 以singleInstance模式启动的Activity具有独占性,即它会独自占用一个任务,被他开启的任何activity都会运行在其他任务中(官方文档上的描述为,singleInstance模式的Activity不允许其他Activity和它共存在一个任务中)
  • 被singleInstance模式的Activity开启的其他activity,能够开启一个新任务,但不一定开启新的任务,也可能在已有的一个任务中开启

下面对这三个特点分别验证,所使用的示例同样为AndroidTaskTest,只不过会进行一些修改,下面列出它的清单文件:

<?xml version="1.0" encoding="utf-8"?>  
<manifest xmlns:android="http://schemas.android.com/apk/res/android"  
    package="com.jg.zhang.androidtasktest"  
    android:versionCode="1"  
    android:versionName="1.0" >  
  
    <uses-sdk android:minSdkVersion="10" android:targetSdkVersion="17" />  
  
    <application android:allowBackup="true"  
        android:icon="@drawable/ic_launcher" android:label="androidtasktest">  
          
        <activity  android:name="com.jg.zhang.androidtasktest.MainActivity">  
            <intent-filter>  
                <action android:name="android.intent.action.MAIN" />  
                <category android:name="android.intent.category.LAUNCHER" />  
            </intent-filter>  
        </activity>  
          
         <activity android:name="com.jg.zhang.androidtasktest.SecondActivity"  
             android:launchMode="singleInstance">  
             <intent-filter>  
                 <action android:name="com.jg.zhang.androidtasktest.ACTION_MY"/>  
                 <category android:name="android.intent.category.DEFAULT"/>  
             </intent-filter>  
         </activity>  
          
         <activity android:name="com.jg.zhang.androidtasktest.ThirdActivity"/>  
           
    </application>  
      
</manifest>

由上面的清单文件可以知道,该应用包括三个activity,分别为MianActivity,SecondActivity,ThirdActivity,其中SecondActivity启动模式设置为singleInstance。MianActivity可以开启SecondActivity,SecondActivity可以开启ThirdActivity。 并且为了可以在其他应用中开启SecondActivity,为SecondActivity设置了一个IntentFilter,这样就可以在其他应用中使用隐式Intent开启SecondActivity。

为了更好的验证singleInstance的全局唯一性,还需要其他一个应用,对上面的AndroidTaskTest1进行一些修改即可。AndroidTaskTest1只需要一个MianActivity,在MainActivity中点击按钮会开启AndroidTaskTest应用中的SecondActivity。开启AndroidTaskTest应用中的SecondActivity的代码如下:

/** 
 * 该方法在布局中按钮的android:onClick属性中指定 
 * android:onClick="launchOtherActivity" 
 * @param v 
 */  
public void launchOtherActivity(View v){  
    Intent intent = new Intent();  
      
    //以下Action为"com.jg.zhang.androidtasktest.ACTION_MY"  
    //即AndroidTaskTest应用中SecondActivity的action  
    intent.setAction("com.jg.zhang.androidtasktest.ACTION_MY");  
      
    startActivity(intent);  
}

下面开始验证第一个特点:以singleInstance模式启动的Activity具有全局唯一性,即整个系统中只会存在一个这样的实例

执行如下操作:安装AndroidTaskTest应用,点击MainActivity中的按钮,开启SecondActivity,可以看到如下log输出:

laucnhMode-6

执行adb shell dumpsys activity命令,有以下输出:

TaskRecord{411189e0 #9 A com.jg.zhang.androidtasktest}
    Run #2: ActivityRecord{4129af80 com.jg.zhang.androidtasktest/.SecondActivity}
TaskRecord{41305528 #8 A com.jg.zhang.androidtasktest}
    Run #1: ActivityRecord{41296e60 com.jg.zhang.androidtasktest/.MainActivity}

以上可以说明,singleInstance模式的Activity总是会在新的任务中运行(前提是系统中还不存在这样的一个实例) 。

下面验证它的全局唯一性,执行以下操作:安装另一个应用AndroidTaskTest1,在开启的MainActivity中点击按钮开启AndroidTaskTest应用中的SecondActivity。看到打印出一条新的日志:

laucnhMode-7

执行adb shell dumpsys activity命令,有以下输出:

TaskRecord{411189e0 #9 A com.jg.zhang.androidtasktest}
    Run #3: ActivityRecord{4129af80 com.jg.zhang.androidtasktest/.SecondActivity}
TaskRecord{412dc788 #12 A com.jg.zhang.androidtasktest1}
    Run #2: ActivityRecord{4121c628 com.jg.zhang.androidtasktest1/.MainActivity}
TaskRecord{41305528 #8 A com.jg.zhang.androidtasktest}
    Run #1: ActivityRecord{41296e com.jg.zhang.androidtasktest/.MainActivity}

由红色字体可以得知,开启的SecondActivity就是上次创建的编号为4129af80的SecondActivity,并且Log中没有再次输出关于SecondActivity的信息,说明SecondActivity并没有重新创建。由此可以得出结论:以singleInstance模式启动的Activity在整个系统中是单例的,如果在启动这样的Activiyt时,已经存在了一个实例,那么会把它所在的任务调度到前台,重用这个实例。

下面开始验证第二个特点:以singleInstance模式启动的Activity具有独占性,即它会独自占用一个任务,被他开启的任何activity都会运行在其他任务中

重新安装AndroidTaskTest应用,点击MainActivity中的按钮,开启SecondActivity,在SecondActivity中点击按钮,开启ThirdActivity。可以看到有如下Log输出:

laucnhMode-8

执行adb shell dumpsys activity命令,有以下输出:

TaskRecord{412a95b8 #15 A com.jg.zhang.androidtasktest}
    Run #3: ActivityRecord{411f9318 com.jg.zhang.androidtasktest/.ThirdActivity}
TaskRecord{41353a68 #16 A com.jg.zhang.androidtasktest}
    Run #2: ActivityRecord{413537c8 com.jg.zhang.androidtasktest/.SecondActivity}
TaskRecord{412a95b8 #15 A com.jg.zhang.androidtasktest}
    Run #1: ActivityRecord{4123a0c8 com.jg.zhang.androidtasktest/.MainActivity}

SecondActivity所在的任务为16,被SecondActivity启动的ThirdActivity所在的任务为15,这就说明以singleInstance模式启动的Activity具有独占性,即它会独自占用一个任务,被他开启的任何activity都会运行在其他任务中。

下面开始验证第三个特点:被singleInstance模式的Activity开启的其他activity,能够在新的任务中启动,但不一定开启新的任务,也可能在已有的一个任务中开启

有上面对第二个特点的验证可以看到,被SecondActivity启动的ThirdActivity并没有运行在一个新开启的任务中,而是和MainActivity运行在了同一个已有的任务中,那么在什么情况下ThirdActivity才会启动一个新的任务呢?

现在对程序的清单文件做以下修改,为ThirdActivity增加一个属性taskAffinity:

<activity android:name="com.jg.zhang.androidtasktest.ThirdActivity"  
    android:taskAffinity="com.jg.zhang.androidtasktest.second"/>

重新安装AndroidTaskTest应用,执行和上一步中同样的操作:点击MainActivity中的按钮,开启SecondActivity,在SecondActivity中点击按钮,开启ThirdActivity。可以看到有如下输出:

laucnhMode-9

执行adb shell dumpsys activity命令,有以下输出:

TaskRecord{413551b0 #20 A com.jg.zhang.androidtasktest.second}
    Run #3: ActivityRecord{412de9c0 com.jg.zhang.androidtasktest/.ThirdActivity}
TaskRecord{4134b268 #19 A com.jg.zhang.androidtasktest}
    Run #2: ActivityRecord{412a36a0 com.jg.zhang.androidtasktest/.SecondActivity}
TaskRecord{413131e8 #18 A com.jg.zhang.androidtasktest}
    Run #1: ActivityRecord{41271e10 com.jg.zhang.androidtasktest/.MainActivity}

可见,被SecondActivity启动的ThirdActivity启动在了一个新的任务中,即在启动ThirdActivity时创建了一个新任务。这就说明被singleInstance模式的Activity A在开启另一activity B时,能够开启一个新任务,但是是不是真的开启新任务,还要受其他条件的限制,这个条件是:当前系统中是不是已经有了一个activity B的taskAffinity属性指定的任务。

其实这种行为和singleTask启动时的情况相同。在Activity的启动模式设置为singleTask时,启动时系统会为它加上FLAG_ACTIVITY_NEW_TASK标志,而被singleInstance模式的Activity开启的activity,启动时系统也会为它加上FLAG_ACTIVITY_NEW_TASK标志,所以他们启动时的情况是相同的,上面再验证singleTask时已经阐述过,现在重新说明一下:

由于ThirdActivity是被启动模式为singleInstance类型的Activity(即SecondActivity)启动的,framework会为它它加上FLAG_ACTIVITY_NEW_TASK标志,这时  framework会检索是否已经存在了一个affinity为com.jg.zhang.androidtasktest.second(即ThirdActivity的taskAffinity属性)的任务,

  • 如果存在这样的一个任务,则检查在这个任务中是否已经有了一个ThirdActivity的实例,
    1. 如果已经存在一个ThirdActivity的实例,则会重用这个任务和任务中的ThirdActivity实例,将这个任务调到前台,清除位于ThirdActivity上面的所有Activity,显示ThirdActivity,并调用ThirdActivity的onNewIntent()。
    2. 如果不存在一个ThirdActivity的实例,会在这个任务中创建ThirdActivity的实例,并调用onCreate()方法
  • 如果不存在这样的一个任务,会创建一个新的affinity为com.jg.zhang.androidtasktest.second的任务,并且将ThirdActivity启动到这个新的任务中

如果ThirdActivity不设置taskAffinity,即ThirdActivity和MainActivity的taskAffinity相同,都为应用的包名,那么ThirdActivity是不会开启一个新任务的,framework中的判定过程如下:

  1. 在SecondActivity启动ThirdActivity时,因为SecondActivity是singleInstance的,所以设定ThirdActivity的启动标志为FLAG_ACTIVITY_NEW_TASK
  2. 然后获得ThirdActivity的taskAffinity,即为包名com.jg.zhang.androidtasktest
  3. 检查是否已经存在一个affinity为com.jg.zhang.androidtasktest的任务,这个任务是存在的,就是MainActivity所在的任务,这个任务是在启动MainActivity时开启的
  4. 既然已经存在这个任务,就检索在这个任务中是否存在一个ThirdActivity的实例,发现不存在
  5. 在这个已有的任务中启动一个SecondActivity的实例

为了作一个清楚的比较,列出ThirdActivity的taskAffinity属性设为com.jg.zhang.androidtasktest.second时的启动过程

  1. 在SecondActivity启动ThirdActivity时,因为SecondActivity是singleInstance的,那么设定ThirdActivity的启动标志为FLAG_ACTIVITY_NEW_TASK
  2. 然后获得ThirdActivity的taskAffinity,即为com.jg.zhang.androidtasktest.second
  3. 检查是否已经存在一个affinity为com.jg.zhang.androidtasktest.second的任务,这个任务是不存在的
  4. 创建一个新的affinity为com.jg.zhang.androidtasktest.second的任务,并且将ThirdActivity启动到这个新的任务

到此singleInstance也介绍完了。

五 本文小结

由上述可知,Task是Android Framework中的一个概念,Task是由一系列相关的Activity组成的,是一组相关Activity的集合。Task是以栈的形式来管理的。

我们在操作软件的过程中,一定会涉及界面的跳转。其实在对界面进行跳转时,Android Framework既能在同一个任务中对Activity进行调度,也能以Task为单位进行整体调度。在启动模式为standard或singleTop时,一般是在同一个任务中对Activity进行调度,而在启动模式为singleTask或singleInstance是,一般会对Task进行整体调度。

对Task进行整体调度包括以下操作:

  1. 按Home键,将之前的任务切换到后台
  2. 长按Home键,会显示出最近执行过的任务列表
  3. 在Launcher或HomeScreen点击app图标,开启一个新任务,或者是将已有的任务调度到前台
  4. 启动singleTask模式的Activity时,会在系统中搜寻是否已经存在一个合适的任务,若存在,则会将这个任务调度到前台以重用这个任务。如果这个任务中已经存在一个要启动的Activity的实例,则清除这个实例之上的所有Activity,将这个实例显示给用户。如果这个已存在的任务中不存在一个要启动的Activity的实例,则在这个任务的顶端启动一个实例。若这个任务不存在,则会启动一个新的任务,在这个新的任务中启动这个singleTask模式的Activity的一个实例。
  5. 启动singleInstance的Activity时,会在系统中搜寻是否已经存在一个这个Activity的实例,如果存在,会将这个实例所在的任务调度到前台,重用这个Activity的实例(该任务中只有这一个Activity),如果不存在,会开启一个新任务,并在这个新任务中启动这个singleInstance模式的Activity的一个实例。