Android 12 - WMS 层级结构 && DisplayAreaGroup 引入

1. 简介 在 Android 窗口管理中,所有的窗口都是以树形数据结构进行组织管理的,认知这棵 WMS 的树有助于我们理解窗口的管理和显示,同时,WMS 的层级也决定了其在 SurfaceFlinger 的层级结构,这恰恰决定了它的显示规则。 2. WMS 顶层层级构建 在 Android 12 中,所有窗口树形管理都继基于 WindowContainer, 每个 WindowContainer 都有一个父节点和若干个子节点,我们先看看框架中 WindowContainer 都有哪些类型: RootWindowContainer: 最顶层的管理者,直接管理 DisplayContent DisplayContent: 代表着一个真实或者虚拟的显示设备,在普遍场景中,系统中只存在一个 DisplayContent TaskDisplayArea: 是系统中所有应用任务的父节点,用于管理 Task Task: 代表着一个任务 ActivityRecord: 代表一个 Activity 节点 WallpaperWindowToken: 代表壁纸节点 … 在开始之前大概整理了一下系统中各个节点之间的关系: 从上图可以看到,节点之间的嵌套关系还是比较复杂的( 而且这还是不包括下面章节中提到的引入 Feature 之后的层级关系),层级的最顶端就是 RootWindowContainer, 而它的子节点只能是: DisplayContent 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // RootWindowContainer.java void setWindowManager(WindowManagerService wm) { ... final Display[] displays = mDisplayManager.getDisplays(); for (int displayNdx = 0; displayNdx < displays.length; ++displayNdx) { final Display display = displays[displayNdx]; // 为每一个 Display 挂载一个 DisplayContent 节点 final DisplayContent displayContent = new DisplayContent(display, this); addChild(displayContent, POSITION_BOTTOM); if (displayContent.mDisplayId == DEFAULT_DISPLAY) { mDefaultDisplay = displayContent; } } ... } 再来看看 DisplayContent 的构造方法,核心逻辑就只有一句,依靠 DisplayAreaPolicy 进行层级初始化 1 2 3 4 5 6 7 8 9 10 11 // DisplayContent.java DisplayContent(Display display, RootWindowContainer root) { super(root.mWindowManager, "DisplayContent", FEATURE_ROOT); ... // 构造子节点层级,默认策略是使用 DisplayAreaPolicy.DefaultProvider mDisplayAreaPolicy = mWmService.getDisplayAreaPolicyProvider().instantiate( mWmService, this /* content */, this /* root */, mImeWindowsContainer); ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // DisplayAreaPolicy.java static final class DefaultProvider implements DisplayAreaPolicy.Provider { @Override public DisplayAreaPolicy instantiate(WindowManagerService wmService, DisplayContent content, RootDisplayArea root, DisplayArea.Tokens imeContainer) { // 创建 TaskDisplayArea 节点,注意,这里是允许创建多个 TaskDisplayArea 并添加的 final TaskDisplayArea defaultTaskDisplayArea = new TaskDisplayArea(content, wmService, "DefaultTaskDisplayArea", FEATURE_DEFAULT_TASK_CONTAINER); final List<TaskDisplayArea> tdaList = new ArrayList<>(); tdaList.add(defaultTaskDisplayArea); final HierarchyBuilder rootHierarchy = new HierarchyBuilder(root); rootHierarchy.setImeContainer(imeContainer).setTaskDisplayAreas(tdaList); if (content.isTrusted()) { // 配置 Feature 及它所能影响的层级 configureTrustedHierarchyBuilder(rootHierarchy, wmService, content); } // 根据配置的 Feature 生成并挂载各个节点,建造层级 return new DisplayAreaPolicyBuilder().setRootHierarchy(rootHierarchy).build(wmService); } } 在 Android 12 上,Feature 正式派上用场了,原生添加了以下 Feature: WindowedMagnification: 屏幕放大功能,通過 SystemUI mirrorSurface 该节点实现内容拷贝,详见 WindowMagnificationGestureHandler#toggleMagnification HideDisplayCutout: 隐藏刘海屏功能,开启后,该节点将不会延伸到刘海屏区域(除了状态栏导航栏等窗口,因为不在节点控制范围之内) OneHandedBackgroundPanel: 单手模式下此节点会挂载一个纯色图层(在壁纸图层之下),防止深色模式下分辨不出单手模式 OneHanded: 单手模式下相关节点都会做一个向下的位移 FullscreenMagnification: 屏幕放大功能,通过无障碍服务 FullScreenMagnificationController.SpecAnimationBridge#setMagnificationSpecLocked 最后调用 DisplayContent#applyMagnificationSpec 方法实现节点放大。不过源码中并不是通过这个 Feature 来实现相关层级放大的,改造得还不彻底 ImePlaceholder: 特殊情况下用来放置输入法的节点 我们知道,Android 系统是有 Z 轴概念的,不同的窗口有不同的高度,所有的窗口类型对应到 WMS 都会有一个 layer 值,layer 越大,显示在越上面,WMS 规定 1~36 层级,每一个 Feature 都指定了它所能影响到的 layer 层。这里用颜色对不同 Feature 能影响 layer 图层进行颜色标记: 标记完之后,就需要根据图表生成窗口层级了,首先对标记好的图表进行上移,上移规则: 如果色块上方是空白的,则可以上移,直至上方是颜色块(不知道大家有没有玩过 2048 这款游戏,上移逻辑是一样的~) 上移之后,我们得到了最终的图表,接下来用以下规则进行层级构建: 同一行相邻的同色块变成一个 Feature 节点,从左到右根据颜色不断生成节点,同一行所有节点挂在同一个父节点下 父节点就是垂直正上方一行的色块对应的节点 为最末端所有 Feature 节点再添加一个节点,根据子节点代表的 layer 不一样,最后添加的节点也不一样 除了 layer 是 2、15 和 16 外,挂载 DisplayArea.Tokens(这类节点后续只能添加 WindowToken 节点) layer = 2 (也就是 APPLICATION_LAYER)的节点,挂载 TaskDisplayArea layer 等于 15 和 16 的节点,挂载 ImeContainer 通过上述构建规则后,我们可以获得一个树形的层级,并且这棵树有以下特点: 树的最末端节点对应一个 layer 范围,同一个 layer 值只有一个末端节点与之对应 为所有 Feature 都生成了对应的父节点,用以控制其所能影响的 layer 生成了这棵树后,我们会保存两样东西: 所有 layer 值对应的最末端节点,方便我们后续根据窗口类型添加节点 以 Map<Feature, List<DisplayArea>> 形式保存的所有 Feature 节点,方便我们后取出某 Feature 对应的所有节点 现在,虽说我们的 WMS 层级是构建好了,但对于这些 Feature 有何作用还完全没有涉及,这块打算放在 WM Shell 专题里进行说明~~ 3. DisplayAreaGroup 通过上面 Feature 的说明可以知道,不同的 Feature 是父子节点的关系,那如果我想划分一个逻辑显示区域,对这块区域配置不同的 Feature 该如何呢? 这时候就可以使用 DisplayAreaGroup 了,框架允许我们添加多个 DisplayAreaGroup, 并为其配置不同的 Feature。 就像原生提供的 demo 一样,我们可以创建两个 DisplayAreaGroup 并将屏幕一分为二分别放置这两个,这两个区域都是可以作为应用容器的,和分屏不一样的是,这两块区域可以有不同的 Feature 规则以及其他特性,比如设置不同的 DisplayArea#setIgnoreOrientationRequest 值 DisplayAreaGroup 和 DisplayContent 都是 RootDisplayArea 的直接子类,DisplayAreaGroup 可以认为是一个 Display 划分出的多个逻辑 Display 吧。当然,AOSP 虽然引入了这个概念和代码,但其实并未使用,我们只能从测试代码 DualDisplayAreaGroupPolicyTest 中略窥一二了~ 4. 小结 WMS 相关的内容体系实在太多,本文也仅仅是分析 WMS 窗口层级最顶层的结构,对于具体的窗口添加移除管理这些尚未涉及,同样,原生新增的 Feature 节点使用也没有涉及(这大部分都被打包进 WM Shell 中去了)

2021/10/26
articleCard.readMore

Android 12 - 跟踪利器 WinScope

1. 简介 在开发过程中,经常会遇到各种各样的窗口问题,比如动画异常、窗口异常、闪屏、黑屏、错位显示.. 以前对于这些问题,我们可以通过添加日志,调试分析代码等手段去解决,但这些 UI 问题往往出现在一瞬间,很难把握出现的时机,录制下来的日志往往也是巨大的,从海量的日志中提取有效的信息是一个枯燥且繁琐的事情。 Android 也意识到了这个问题,WinScope 的出现有效的帮助我们跟踪窗口和显示问题。它向开发者提供一个可视化的工具,让开发者能使用工具跟踪整个界面的变化过程,让我们可以观察到细微的变化。迭代了几个版本后,Android 12 上 WinScope 变得更好用了,下面来看看大概的效果: 2. 工具获取 Android 12 平台的 WinScope 工具可以通过源码编译获得,具体也可以查阅 development/tools/winscope 目录下的 README.md 文档,这里提供一个 Ubuntu 平台的编译步骤: 1 2 3 4 5 1. cd development/tools/winscope 2. sudo apt install nodejs npm 3. npm install -g yarn 4. yarn install 5. yarn build 编译过程中遇到一个问题,看上去是在执行 kotlin 优化的时候,报了个内存不足的问题: 1 2 3 4 5 6 7 8 9 10 11 Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded at com.google.gwt.dev.js.ScopeContext.referenceFor(ScopeContext.kt:68) at com.google.gwt.dev.js.JsAstMapper.mapAsPropertyNameRef(JsAstMapper.java:247) at com.google.gwt.dev.js.JsAstMapper.mapGetProp(JsAstMapper.java:608) at com.google.gwt.dev.js.JsAstMapper.mapWithoutLocation(JsAstMapper.java:138) at com.google.gwt.dev.js.JsAstMapper.map(JsAstMapper.java:47) at com.google.gwt.dev.js.JsAstMapper.mapExpression(JsAstMapper.java:466) at com.google.gwt.dev.js.JsAstMapper.mapBinaryOperation(JsAstMapper.java:304) at com.google.gwt.dev.js.JsAstMapper.mapAssignmentVariant(JsAstMapper.java:258) at com.google.gwt.dev.js.JsAstMapper.mapWithoutLocation(JsAstMapper.java:102) at com.google.gwt.dev.js.JsAstMapper.map(JsAstMapper.java:47) 可以在执行 yarn build 前通过 export JAVA_OPTS="-XX:-UseGCOverheadLimit" 禁用掉 GC overhead limit exceeded 检测 编译完之后,在当前目录下会一个 dist 目录,再把 adb_proxy/winscope_proxy.py(一个帮我们开启 trace 抓取命令的脚本,这样我们就可以告别繁琐的命令啦),文件也拷贝进 dist 目录方便我们后面使用 3. 使用 使用比较简单了,连接手机后: 双击打开 dist 目录下的 index.html 文件 在终端执行 python3 winscope_proxy.py PS: 这里建议大家可以设置个 alias 一键使用,比如: 1 alias winscope_s="xdg-open ~/tools/winscope_s/index.html && python3 ~/tools/winscope_s/winscope_proxy.py" 输入 python 命令后,终端可能会生成一个 token ,把它复制到浏览器即可 接下来就会出现下面这个界面: 在选择 START TRACE 之后,就可以在手机端进行录制操作,操作完后结束录制即可 4. 功能改进 相比与 Android 11,新的 WinScope 工具在界面上更友好了,重要的改进如下: 时间线控制更加方便了,可以选择不同类型的时间线用于控制,否则像以前录制了 ProtoLog 后,通过箭头控制时间推移简直是太难用了 新的 Transaction 和 Log 浏览界面 支持 Diff 功能,不得不说每一帧的参数太多了,如果没有 diff 功能,实在很难一眼看出那些参数发生了变化 新增了 IME 的录制,可以录制 InputMethodService、InputMethodManagerService 和 Client 的事件 录屏界面可以以 PIP 形式显示在浏览器界面上 … 不得不说确实比以前好用一些,赶紧尝尝鲜吧~

2021/10/22
articleCard.readMore

Android 12 - Letterbox 模式

1. 简介 随着越来越多大屏和折叠屏设备出现,很多应用并未对不同尺寸的设备进行 UI 适配,这时候应用选择以特定的宽高比显示(虽然 Google 不建议这这样做,官方还是希望开发者可以对不同的屏幕尺寸进行自适应布局~),当应用的宽高比和它的容器比例不兼容的时候,就会以 Letterbox 模式打开。 Letterbox 模式下界面会以指定的比例显示,周围空白区域可以填充壁纸或者颜色。至于 Letterbox 的外观可受以下因素影响: config_letterboxActivityCornersRadius: 界面圆角大小 config_letterboxBackgroundType: 背景填充类型,分别有: LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND: 颜色受 android:colorBackground 影响 LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING: 颜色受 android:colorBackgroundFloating 影响 LETTERBOX_BACKGROUND_SOLID_COLOR: 颜色受 config_letterboxBackgroundColor 影响 LETTERBOX_BACKGROUND_WALLPAPER: 显示壁纸,此选项和 FLAG_SHOW_WALLPAPER 类似,会导致壁纸窗口显示 config_letterboxBackgroundWallpaperBlurRadius: 壁纸模糊程度 config_letterboxBackgroundWallaperDarkScrimAlpha: 壁纸变暗程度 2. 何时触发 Letterbox 的触发条件一般有: android:resizeableActivity=false 且应用声明的宽高比与容器不兼容的时候(如屏幕宽高超出 android:maxAspectRatio) setIgnoreOrientationRequest(true) 系统设置忽略屏幕方向后,以横屏模式下打开一个强制竖屏的界面 3. 实现方案 Letterbox 显示的实现并不复杂,Android 12 在 ActivityRecord 中增加了 LetterboxUiController 用以控制 Letterbox 的布局和显示,先来看看处于 Letterbox 模式时 SurfaceFlinger 状态: 可以看到,跟正常情况相比,除了界面本身的大小和位置被缩放到指定比例外,四周还多了两个 Layer,挂在 ActiviRecord 节点下面,这两个 Layer 可根据配置进行指定的颜色填充,如果背景是壁纸的话,还可以设置壁纸的 dim 值和模糊程度,这些都可以通过 SurfaceControl 接口轻松实现。 下面简单分析一下代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 // LetterboxUiController.java void updateLetterboxSurface(WindowState winHint) { final WindowState w = mActivityRecord.findMainWindow(); if (w != winHint && winHint != null && w != null) { return; } // 对界面四周需要显示的 Layer 进行位置计算 layoutLetterbox(winHint); if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) { // 对 Surface 执行创建、参数设置等操作 mLetterbox.applySurfaceChanges(mActivityRecord.getSyncTransaction()); } } void layoutLetterbox(WindowState winHint) { final WindowState w = mActivityRecord.findMainWindow(); if (w == null || winHint != null && w != winHint) { return; } updateRoundedCorners(w); updateWallpaperForLetterbox(w); // 是否进入 Letterbox 模式的关键判断 if (shouldShowLetterboxUi(w)) { if (mLetterbox == null) { // 把具体逻辑委托给 Letterbox mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null), mActivityRecord.mWmService.mTransactionFactory, mLetterboxConfiguration::isLetterboxActivityCornersRounded, this::getLetterboxBackgroundColor, this::hasWallpaperBackgroudForLetterbox, this::getLetterboxWallpaperBlurRadius, this::getLetterboxWallpaperDarkScrimAlpha); mLetterbox.attachInput(w); } mActivityRecord.getPosition(mTmpPoint); // Get the bounds of the "space-to-fill". The transformed bounds have the highest // priority because the activity is launched in a rotated environment. In multi-window // mode, the task-level represents this. In fullscreen-mode, the task container does // (since the orientation letterbox is also applied to the task). final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds(); final Rect spaceToFill = transformedBounds != null ? transformedBounds : mActivityRecord.inMultiWindowMode() ? mActivityRecord.getRootTask().getBounds() : mActivityRecord.getRootTask().getParent().getBounds(); // 位置计算 mLetterbox.layout(spaceToFill, w.getFrame(), mTmpPoint); } else if (mLetterbox != null) { mLetterbox.hide(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // Letterbox.LetterboxSurface.java public void applySurfaceChanges(SurfaceControl.Transaction t) { if (!needsApplySurfaceChanges()) { // Nothing changed. return; } mSurfaceFrameRelative.set(mLayoutFrameRelative); if (!mSurfaceFrameRelative.isEmpty()) { if (mSurface == null) { // 创建挂在 ActivityRecord 节点下的 Surface,设置为 ColorLayer 类型 createSurface(t); } // 设置颜色、位置、裁剪 mColor = mColorSupplier.get(); t.setColor(mSurface, getRgbColorArray()); t.setPosition(mSurface, mSurfaceFrameRelative.left, mSurfaceFrameRelative.top); t.setWindowCrop(mSurface, mSurfaceFrameRelative.width(), mSurfaceFrameRelative.height()); // 对壁纸背景设置透明度和模糊度 mHasWallpaperBackground = mHasWallpaperBackgroundSupplier.get(); updateAlphaAndBlur(t); t.show(mSurface); } else if (mSurface != null) { t.hide(mSurface); } if (mSurface != null && mInputInterceptor != null) { mInputInterceptor.updateTouchableRegion(mSurfaceFrameRelative); t.setInputWindowInfo(mSurface, mInputInterceptor.mWindowHandle); } } 4. 小结 本文只是简单分析了下 Letterbox 模式的触发条件和显示的大概逻辑,还有很多细节没有涉及,比如详细的触发逻辑判断可以查看 LetterboxUiController#shouldShowLetterboxUi 方法

2021/10/21
articleCard.readMore

Android 列表滚动优化之 OverScroller 揭秘

1. 简介 OverScroller 在 Android 系统中承担着为 ListView、RecyclerView、ScrollView 这些滚动控件计算实时滑动位置的任务,这些位置算法直接影响着每一次滚动的体验 众所周知,Android 的动画体验远不如 iOS,即便如今 Android 已普遍支持 120Hz 高刷,体验起来也不是非常舒服。究其原因已经不是硬件性能限制,而是其中很多动画设计本身就有问题。苹果早在很早之前就发布了 Designing Fluid Interfaces 致力于打造一个丝滑流畅的用户体验,反观 Android,对于一个日常使用中使用最多的滑动工具类 OverScroller 近几年改进竟然寥寥无几,几乎没有,实在是有点想吐槽 这个系列分为两篇,第一篇主要讲述 Android 实现滚动的核心工具类 OverScroller 的使用方法和原理,第二篇我们将探索如何进行改进,希望我们每一次探索都能给用户体验带来提升 2. 使用介绍 在使用之前,先来看看 OverScroller 能做什么: startScroll:从指定位置滚动一段指定的距离然后停下,滚动效果与设置的滚动距离、滚动时间、插值器有关,跟离手速度没有关系。一般用于控制 View 滚动到指定的位置 fling:从指定位置滑动一段位置然后停下,滚动效果只与离手速度以及滑动边界有关,不能设置滚动距离、滚动时间和插值器。一般用于触摸抬手后继续让 View 滑动一会 springBack:从指定位置回弹到指定位置,一般用于实现拖拽后的回弹效果,不能指定回弹时间和插值器 startScrollflingspringBack 在代码中使用也很简单: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 1. 启动一个滚动 mOverScroller.startScroll(0, 1600, 0, -1000, 1000); // 2. 启动定时刷新任务 post(new Runnable() { @Override public void run() { // 3. 计算当前最新位置信息 if (mOverScroller.computeScrollOffset()) { // 4. 根据最新位置更新 View 状态 Log.d("OverScroller", "x=" + mOverScroller.getCurrX() + ", y=" + mOverScroller.getCurrY()); invalidate(); // 5. 判断滚动是否停止,没有停止的话启动下一轮刷新任务 if (!mOverScroller.isFinished()) { postDelayed(this, 16); } } } }); 上面就是启动一个不断滚动并刷新 View 的最小逻辑(当然更工程化的实践也可以把 Runnable 的逻辑放在 View#computeScroll 里再通过 invalidate 触发)。fling 和 springBack 的启动方式也是一样的,这里就不再进行赘述了。 3. 深入 OverScroller 内部 在上面代码中可知,启动一个滚动任务后,是通过不断地调用 computeScrollOffset 来计算位置的,接下来看下代码实现 3.1 startScroll 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class OverScroller { public void startScroll(int startX, int startY, int dx, int dy, int duration) { // 标记当前模式为 SCROLL_MODE mMode = SCROLL_MODE; // mScrollerX 和 mScrollerY 均是 SplineOverScroller 实例 // OverScroller 把参数分别传给 mScrollerX 和 mScrollerY,在里面做真正的计算 mScrollerX.startScroll(startX, dx, duration); mScrollerY.startScroll(startY, dy, duration); } static class SplineOverScroller { void startScroll(int start, int distance, int duration) { mFinished = false; // 标记起始点和结束点 mCurrentPosition = mStart = start; mFinal = start + distance; // 标记起始时间和动画时长 mStartTime = AnimationUtils.currentAnimationTimeMillis(); mDuration = duration; // Unused mDeceleration = 0.0f; mVelocity = 0; } } } startScroll 的逻辑非常的简单,只是根据参数标记了一下开始位置、结束位置、开始时间、动画时长,还没涉及位置计算(因为位置计算是放在 computeScrollOffset 的呀) 再看看定时调用的 computeScrollOffset 逻辑: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class OverScroller { public OverScroller(Context context, Interpolator interpolator, boolean flywheel) { // 初始化插值器 if (interpolator == null) { mInterpolator = new Scroller.ViscousFluidInterpolator(); } else { mInterpolator = interpolator; } ... } public boolean computeScrollOffset() { if (isFinished()) { return false; } switch (mMode) { case SCROLL_MODE: long time = AnimationUtils.currentAnimationTimeMillis(); // 计算已过去的时间 final long elapsedTime = time - mScrollerX.mStartTime; final int duration = mScrollerX.mDuration; if (elapsedTime < duration) { // 用插值器对时间比做一个变换 final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration); mScrollerX.updateScroll(q); mScrollerY.updateScroll(q); } else { abortAnimation(); } break; break; } return true; } static class SplineOverScroller { void updateScroll(float q) { // 根据比值计算最新位置 mCurrentPosition = mStart + Math.round(q * (mFinal - mStart)); } } } 逻辑也是很简单,实在没太多可说的……就是把插值器曲线映射到位移曲线,时长如果不指定的话,默认是 250ms,插值器需要通过构造方法传入,如果不指定的话,系统默认会指定一个 ViscousFluidInterpolator,下面是这个插值器的曲线,可以看到是一个先缓后快再缓的动画 3.2 fling & springBack fling 和 springBack 为什么要一起说呢?因为 fling 的动画比较复杂,springBack 算是属于 fling 的其中一个子状态,考虑以下这个情况: 我们看到当以比较大的速度执行 fling 的时候,是很容易碰到边界的,fling 会根据预设的边界值执行越界并回弹,可以把整个动画过程分解成三个阶段: SPLINE:也就是正常滑动阶段 BALLISTIC:越界减速阶段 CUBIC:回弹阶段 springBack 后执行的动画,其实就是 fling 的 CUBIC 阶段,所以干脆就放在一起说了 这个命名其实也挺有意思,这三个分别是样条曲线、弹道曲线、三次曲线,从命名上大致也可以推断出,三个阶段采用的「时间-位置」曲线是不一样的。 当然了,Android 很多控件在 fling 的时候,都把越界回弹效果取消掉,取而代之的是显示一个 EdgeEffect。也就是说执行完 SPLINE 阶段动画后,是看不到 BALLISTIC 和 CUBIC 的,只能看到一个边缘辉光效果,列表到达顶/底部的时候,往往一下子停在那里了~ 3.2.1 SPLINE 先来看看启动 fling 的入口函数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 public class OverScroller { public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) { // 标记滚动模式,主要和 startScroll 进行区分 mMode = FLING_MODE; mScrollerX.fling(startX, velocityX, minX, maxX, overX); mScrollerY.fling(startY, velocityY, minY, maxY, overY); } static class SplineOverScroller { void fling(int start, int velocity, int min, int max, int over) { mOver = over; mFinished = false; mCurrVelocity = mVelocity = velocity; mDuration = mSplineDuration = 0; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mCurrentPosition = mStart = start; mState = SPLINE; double totalDistance = 0.0; if (velocity != 0) { // 根据速度计算滑动时长 mDuration = mSplineDuration = getSplineFlingDuration(velocity); // 根据速度计算滑动距离 totalDistance = getSplineFlingDistance(velocity); } mSplineDistance = (int) (totalDistance * Math.signum(velocity)); mFinal = start + mSplineDistance; if (mFinal < min) { // 如果计算出的滑动距离超过 min 边界,则重新计算到达 min 边界时的滑动时长 adjustDuration(mStart, mFinal, min); mFinal = min; } if (mFinal > max) { // 如果计算出的滑动距离超过 max 边界,则重新计算到达 max 边界时的滑动时长 adjustDuration(mStart, mFinal, max); mFinal = max; } } private double getSplineDeceleration(int velocity) { return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); } /** * 计算滑动距离 */ private double getSplineFlingDistance(int velocity) { final double l = getSplineDeceleration(velocity); final double decelMinusOne = DECELERATION_RATE - 1.0; return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); } /** * 计算滑动时长 */ private int getSplineFlingDuration(int velocity) { final double l = getSplineDeceleration(velocity); final double decelMinusOne = DECELERATION_RATE - 1.0; return (int) (1000.0 * Math.exp(l / decelMinusOne)); } } } 看代码前先来看看上面的图,图中说明了 start、min、max、over 等位置的意义,这里简要说明一下 fling 后最终停下来的位置必须在 min 和 max 区间之间 BALLISTIC 越界阶段,不能超过 over 的位置,即 over 是最大越界距离 start 可以位于 [min, max] 区间之外,如果处于区间之外,会根据速度方向和大小做一个决策,会有以下三种情况: 速度指向边界外:先执行 BALLISTIC 越界再执行 CUBIC 回弹回到边界 速度指向边界内:如果速度足以越过边界,则按照正常流程执行,先执行 SPLINE 速度指向边界内:如果速度不足以越过边界,直接执行 CUBIC 回到边界 fling 函数主要功能: 根据起始速度计算滑动的时长和距离 如果计算出最终点处于 [min, max] 区间之外,则重新计算到达边界的时长 标记当前状态为 SPLINE 状态 滑动距离和时长的计算公式中,可以把 mPhysicalCoeff 也看做常数,把时长-速度公式和图像列出来: $$ y=1000\cdot \exp \left( \frac{\ln \left( \frac{0.35x}{2140.47} \right)}{1.358} \right) $$ 再看看距离-速度公式: $$ y=2140.47\cdot \exp \left( 1.74\cdot \ln \left( \frac{0.35\cdot x}{2140.47} \right) \right) $$ 可以看到距离和时长都是随着速度增大而增大的,只不过时长的增长速度在后期会有一定的收敛,保证动画时长不至于太长 还有一点要注意的是,$ \frac{Distance} {Duration} $ 的比值是一个线性函数,也就是初速度越大,平均速度越大,两者是线性增长的: $$ y=\frac{2140.47\cdot \exp \left( 1.74\cdot \ln \left( \frac{0.35\cdot x}{2140.47} \right) \right)}{1000\cdot \exp \left( \frac{\ln \left( \frac{0.35x}{2140.47} \right)}{1.358} \right)} $$ 但是说实话,目前我暂时没想明白这两个公式的物理意义,有明白的大佬求告知~ 难道是利用了对数函数收敛的特性确定了时长公式,然后设定平均速度线性增长后,推导出距离公式? 目前只确定了滑动总距离和时长,那么中间过程是怎么更新位置的呢: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 public class OverScroller { public boolean computeScrollOffset() { if (isFinished()) { return false; } switch (mMode) { case FLING_MODE: ... if (!mScrollerY.mFinished) { // 更新当前阶段的速度和位置 if (!mScrollerY.update()) { // 判断是否需要进入下一动画阶段 if (!mScrollerY.continueWhenFinished()) { mScrollerY.finish(); } } } break; } return true; } static class SplineOverScroller { boolean update() { final long time = AnimationUtils.currentAnimationTimeMillis(); final long currentTime = time - mStartTime; if (currentTime == 0) { return mDuration > 0; } // 根据动画时长判断当前阶段的动画是否应该结束 if (currentTime > mDuration) { return false; } double distance = 0.0; switch (mState) { case SPLINE: { // 把当前时间映射到 100 个采样点的曲线之中 final float t = (float) currentTime / mSplineDuration; final int index = (int) (NB_SAMPLES * t); float distanceCoef = 1.f; float velocityCoef = 0.f; if (index < NB_SAMPLES) { final float t_inf = (float) index / NB_SAMPLES; final float t_sup = (float) (index + 1) / NB_SAMPLES; final float d_inf = SPLINE_POSITION[index]; final float d_sup = SPLINE_POSITION[index + 1]; // 根据映射时间计算样条曲线的当前的斜率,即速度 velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); // 根据映射时间计算样条曲线的高度,即距离 distanceCoef = d_inf + (t - t_inf) * velocityCoef; } // 把样条距离映射回真正的距离 distance = distanceCoef * mSplineDistance; // 把样条速递易映射回真正的速度 mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f; break; } case BALLISTIC: { // 根据匀减速运动公式计算位置和速度 final float t = currentTime / 1000.0f; mCurrVelocity = mVelocity + mDeceleration * t; distance = mVelocity * t + mDeceleration * t * t / 2.0f; break; } case CUBIC: { // 根据一个自定义的三次曲线计算位置和速度 final float t = (float) (currentTime) / mDuration; final float t2 = t * t; final float sign = Math.signum(mVelocity); distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2); mCurrVelocity = sign * mOver * 6.0f * (- t + t2); break; } } mCurrentPosition = mStart + (int) Math.round(distance); return true; } boolean continueWhenFinished() { switch (mState) { case SPLINE: if (mDuration < mSplineDuration) { // 如果时长小于 mSplineDuration ,说明 mDuration 被重新计算过,即上面说到的到达边界的时间。那么我们需要进入越界状态 mCurrentPosition = mStart = mFinal; mVelocity = (int) mCurrVelocity; mDeceleration = getDeceleration(mVelocity); mStartTime += mDuration; onEdgeReached(); } else { // SPLINE 停止时未到达边界,结束动画 return false; } break; case BALLISTIC: // 越界状态结束,进入回弹阶段 mStartTime += mDuration; startSpringback(mFinal, mStart, 0); break; case CUBIC: // 回弹阶段结束,结束动画 return false; } update(); return true; } } } SPLINE 阶段的位置和速度完全是由预设的 SPLINE_POSITION 样条曲线决定的,它是一个大小为 101 的数组,里面存储了一条曲线平均采样 100 次的坐标值,初始化如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private static final int NB_SAMPLES = 100; private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1]; private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1]; static { float x_min = 0.0f; float y_min = 0.0f; for (int i = 0; i < NB_SAMPLES; i++) { final float alpha = (float) i / NB_SAMPLES; float x_max = 1.0f; float x, tx, coef; while (true) { x = x_min + (x_max - x_min) / 2.0f; coef = 3.0f * x * (1.0f - x); tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x; if (Math.abs(tx - alpha) < 1E-5) break; if (tx > alpha) x_max = x; else x_min = x; } SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x; } SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; } 看代码很难想象它长什么样,直接看看它的图像吧: 也就是说,SPLINE 和 startScroll 很像,位置曲线都是由一根预置的曲线决定的,把预置曲线映射真实的距离,只是 SPLINE 没有使用插值器曲线,而是使用了一根缓停的样条曲线 3.2.2 BALLISTIC SPLINE 阶段结束后,会通过 continueWhenFinished 进入下一阶段:越界阶段(前提是此时已经到达边界)。越界阶段的原理相对简单,就是一段匀减速运动(直至速度降为 0),默认加速度 a 为 -2000.0f,到达边界进入 BALLISTIC 阶段的初始化逻辑在 onEdgeReached 中: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private void onEdgeReached() { final float velocitySquared = (float) mVelocity * mVelocity; // 计算速度降为 0 时,运动的距离 float distance = velocitySquared / (2.0f * Math.abs(mDeceleration)); final float sign = Math.signum(mVelocity); if (distance > mOver) { // 如果距离大于最大距离 over,则重新计算加速度,使运动距离恰好为 over mDeceleration = - sign * velocitySquared / (2.0f * mOver); distance = mOver; } mOver = (int) distance; // 标记动画阶段 mState = BALLISTIC; mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance); // 根据初速度和加速度计算总时长 mDuration = - (int) (1000.0f * mVelocity / mDeceleration); } 速度: $$ v_t=v_0+at $$ 距离: $$ s_t=v_0t + \frac{at^2} {2} $$ 这里有个小吐槽,加速度固定是 2000,这也太小了吧,也就是说,如果越界速度为 10000, 那么需要 5s 之后,速度才能降为 0,一个 5s 的动画童鞋们估计都知道意味着多久吧? 3.2.3 CUBIC 上一节内容知道,BALLISTIC 阶段结束时,速度已经降低为 0,我们终于来到最后一段, 从 continueWhenFinished 里会调用 startSpringback 作为 CUBIC 的初始化: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 private void startSpringback(int start, int end, int velocity) { mFinished = false; // 标记动画阶段 mState = CUBIC; mCurrentPosition = mStart = start; mFinal = end; // CUBIC 阶段运动总距离 final int delta = start - end; mDeceleration = getDeceleration(delta); mVelocity = -delta; // only sign is used mOver = Math.abs(delta); // 计算此阶段的动画时长 mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration)); } 时长计算依然使用匀加速直线运动的逻辑,想象一段初速度为 0,加速度为 a, 距离为 delta 的情况: $$ delta=\frac{(v_0 + v_t)} {2} t $$ 则: $ v_0=0,v_t=at $,那么 $ delta=\frac{at^2} {2} $,$ t=\sqrt{ \frac{2*delta} {a}}$ 在 update 方法中,更新 CUBIC 的核心逻辑是: 1 2 3 4 5 6 7 8 9 10 11 case CUBIC: { // 根据一个自定义的三次曲线计算位置和速度 final float t = (float) (currentTime) / mDuration; final float t2 = t * t; final float sign = Math.signum(mVelocity); // 计算运动位置 distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2); // 计算速度,对距离公式求导即可 mCurrVelocity = sign * mOver * 6.0f * (- t + t2); break; } 核心逻辑是这个 3.0f * t2 - 2.0f * t * t2, 这其实是一个比较常用的三次曲线: 在 [0, 1] 区间内,是一个缓入缓出的曲线。至此,CUBIC 的运动规律也摸清楚了,在固定时间内,把时间映射到 [0, 1] 的区间,再把 y 坐标映射实际的位置 4. 小结 目前为止,我们终于把 startScroll 和 fling 各阶段的曲线看了一遍,至于 springBack 和其他一些情况都大同小异,就不细述了。 很多时候 OverScroller 都是只是使用的固定的曲线映射真正的曲线,比如 startScroll、SPLINE 和 CUBIC,那如果想改变效果的话,是不是修改一下曲线形态就可以了呢?但一条曲线是否真的能在不同速度下都有比较好的表现吗?或许我们还要有很多的实践和尝试才能做出一段让用户舒服滑动,而这些探索和尝试,将放在下一篇文章中详细讨论~

2021/4/9
articleCard.readMore

使用 Ninja 提升模块编译速度

1. 简介 从 Android 7 开始,Android 源码编译时默认使用 Ninja,编译时,会先把 makefile 和 bp 转换成 ninja 再进行编译。这个转换过程非常慢(需要遍历处理所有关联的 makefile、bp 文件),即使只是通过 mm 或 mmm 编译某个模块,也会有很多因素触发 ninja 文件的重新生成,而这对基于源码开发的模块很不友好,编译好慢! 2. 初识 ninja AOSP 在源码中已经内置了一个 ninja 执行文件,路径为:./prebuilts/build-tools/linux-x86/bin/ninja 我们先看看它的 help: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ➜ ~ ./prebuilts/build-tools/linux-x86/bin/ninja -h usage: ninja [options] [targets...] if targets are unspecified, builds the 'default' target (see manual). options: --version print ninja version ("1.9.0") -v, --verbose show all command lines while building -C DIR change to DIR before doing anything else -f FILE specify input build file [default=build.ninja] -j N run N jobs in parallel (0 means infinity) [default=10 on this system] -k N keep going until N jobs fail (0 means infinity) [default=1] -l N do not start new jobs if the load average is greater than N -n dry run (don't run commands but act like they succeeded) -d MODE enable debugging (use '-d list' to list modes) -t TOOL run a subtool (use '-t list' to list subtools) terminates toplevel options; further flags are passed to the tool -w FLAG adjust warnings (use '-w list' to list warnings) 简单使用的话,我们关注它的两个参数就行了 -f:这个参数指定的就是输入文件,也就是 makefile 和 bp 转换后的 ninja 文件,一般位于 ./out 目录,后面会说 targets:目标,这个和 makefile 是类似的,就是我们最终需要的产物,例如:Launcher3QuickStep、SystemUI。那么这些 targets 名是哪里定义的呢?要知道对应模块的对应的 target 名,只需要: 若模块使用的是 Android.mk:查找 LOCAL_PACKAGE_NAME 或 LOCAL_MODULE 等对应的值 若模块使用的是 Android.bp:查找 module 中 name 对应的值 举个栗子: 1 2 3 4 5 6 7 ➜ android-10.0.0_r11 ./prebuilts/build-tools/linux-x86/bin/ninja -f out/combined-aosp_walleye.ninja Launcher3QuickStep [3/13] Target Java: out/target/common/obj/JAVA_LIBRARIES/Launcher3QuickStepLib_intermediates/classes-full-debug.jar 注: 某些输入文件使用或覆盖了已过时的 API。 注: 有关详细信息, 请使用 -Xlint:deprecation 重新编译。 注: 某些输入文件使用了未经检查或不安全的操作。 注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。 [13/13] Install: out/target/product/walleye/system/product/priv-app/Launcher3QuickStep/Launcher3QuickStep.apk 就这样,不需要通过 mm 或者 mmm 命令,目标产物同样生成了。我们看看耗时: 1 2 3 4 5 6 7 8 9 10 11 ➜ android-10.0.0_r11 time ./prebuilts/build-tools/linux-x86/bin/ninja -f out/combined-aosp_walleye.ninja Launcher3QuickStep [3/13] Target Java: out/target/common/obj/JAVA_LIBRARIES/Launcher3QuickStepLib_intermediates/classes-full-debug.jar 注: 某些输入文件使用或覆盖了已过时的 API。 注: 有关详细信息, 请使用 -Xlint:deprecation 重新编译。 注: 某些输入文件使用了未经检查或不安全的操作。 注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。 [13/13] Install: out/target/product/walleye/system/product/priv-app/Launcher3QuickStep/Launcher3QuickStep.apk real0m18.994s user1m20.548s sys0m2.872s 可以看到,整个编译在 18s 完成了,相比动辄七八分钟的 mmm,效率提升还是很可观的。 3. 注意事项 虽然 ninja 很方便,但要用它来编译单个模块,还是有一些限制和注意事项的: 使用前需把对应模块编译一遍,用于生产 ninja 文件(全编或 mmm 都可以) 全编后,生成的 ninja 文件为:./out/combined-[TARGET-PRODUCT].ninja mmm 编译后,生成的 ninja 文件为:./out/combined-[TARGET-PRODUCT]-_[path_to_your_module_makefile].ninja,比如:./out/combined-aosp_walleye-_packages_apps_Launcher3_Android.mk.ninja 如果修改了 Android.mk 或 Android.bp,需使用传统的 make 命令进行编译以重新生成包含新依赖规则的 ninja 文件 可以把 ninja 放到 PATH 环境变量中,这样就不用每次都敲 ./prebuilts/build-tools/linux-x86/bin/ninja 这个路径了 4. 最后 为 Launcher 和 SystemUI 准备一份开箱即用的指令,尽情玩耍吧~ Launcher: 1 ./prebuilts/build-tools/linux-x86/bin/ninja -f out/combined-qssi-_packages_apps_Launcher3_Android.mk.ninja Launcher3QuickStep SystemUI: 1 ./prebuilts/build-tools/linux-x86/bin/ninja -f out/combined-qssi-_frameworks_base_packages_SystemUI_Android.mk.ninja SystemUI

2019/11/20
articleCard.readMore

Android Q 黑暗模式(Dark Mode)源码解析

1. 简介 随着 Android Q 发布,「黑暗模式」或者说是「夜间模式」终于在此版本中得到了支持,官方介绍见:https://developer.android.com/guide/topics/ui/look-and-feel/darktheme,再看看效果图: 其实这个功能魅族在两年前就已支持,不得不说 Android 有点落后了,今天我们就来看看原生是怎么实现全局夜间模的吧 2. 打开与关闭 从文档上我们可以可知,打开夜间模式有三个方法: 设置 -> 显示 -> 深色主题背景 下拉通知栏中开启 Pixel 手机开启省点模式时会自动激活夜间模式 3. 如何适配 打开后,我们会发现,除原生几个应用生效外,其他应用依然没有变成深色主题,那么应用该如何适配呢?官方提供了下面两种方法: 3.1. 让应用主题继承 DayNight 主题 1 <style name="AppTheme" parent="Theme.AppCompat.DayNight"> 或者继承自 1 <style name="AppTheme" parent="Theme.MaterialComponents.DayNight"> 继承后,如果当前开启了夜间模式,系统会自动从 night-qualified 中加载资源,所以应用的颜色、图标等资源应尽量避免硬编码,而是推荐使用新增 attributes 指向不同的资源,如 1 2 ?android:attr/textColorPrimary ?attr/colorControlNormal 另外,如果应用希望主动切换夜间/日间模式,可以通过 AppCompatDelegate.setDefaultNightMode() 接口主动切换 3.2. 通过 forceDarkAllowed 启用 如果应用不想自己去适配各种颜色,图标等,可以通过在主题中添加 android:forceDarkAllowed="true" 标记,这样系统在夜间模式时,会强制改变应用颜色,自动进行适配(这个功能也是本文主要探讨的)。不过如果你的应用本身使用的就是 DayNight 或 Dark Theme,forceDarkAllowed 是不会生效的。 另外,如果你不希望某个 view 被强制夜间模式处理,则可以给 view 添加 android:forceDarkAllowed="false" 或者 view.setForceDarkAllowed(false),设置之后,即使打开了夜间模式且主题添加了 forceDarkAllowed,该 view 也不会变深色。比较重要的一点是,这个接口只能关闭夜间模式,不能开启夜间模式,也就是说,如果主题中没有显示声明 forceDarkAllowed,view.setForceDarkAllowed(true) 是没办法让 view 单独变深色的。如果 view 关闭了夜间模式,那么它的子 view 也会强制关闭夜间模式 总结如下: 主题若添加 forceDarkAllowed=false,无论 view 是否开启 forceDarkAllowed 都不会打开夜间模式 主题若添加 forceDarkAllowed=true,view 可以通过 forceDarkAllowed 关闭夜间模式,一旦关闭,子 view 的夜间模式也会被关闭 如果父 view 或主题设置了 forceDarkAllowed=false,子 view 无法通过 forceDarkAllowed=true 单独打开夜间模式为 若使用的是 DayNight 或 Dark Theme 主题,则所有 forceDarkAllowed 都不生效 4. 实现原理 通过继承主题适配夜间模式的原理本质是根据 ui mode 加载 night-qualified 下是资源,这个并非 Android Q 新增的东西,我们这里不再描述。现在主要来看看 forceDarkAllowed 是如何让系统变深色的。 既然一切的源头都是 android:forceDarkAllowed 这个属性,那我们就从它入手吧,首先我们要知道,上面我们说的 android:forceDarkAllowed 其实是分为两个用处,它们分别的定义如下: frameworks/base/core/res/res/values/attrs.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <declare-styleable name="View"> <!-- <p>Whether or not the force dark feature is allowed to be applied to this View. <p>Setting this to false will disable the auto-dark feature on this View draws including any descendants. <p>Setting this to true will allow this view to be automatically made dark, however a value of 'true' will not override any 'false' value in its parent chain nor will it prevent any 'false' in any of its children. --> <attr name="forceDarkAllowed" format="boolean" /> </declare-styleable> <declare-styleable name="Theme"> <!-- <p>Whether or not the force dark feature is allowed to be applied to this theme. <p>Setting this to false will disable the auto-dark feature on everything this theme is applied to along with anything drawn by any children of views using this theme. <p>Setting this to true will allow this view to be automatically made dark, however a value of 'true' will not override any 'false' value in its parent chain nor will it prevent any 'false' in any of its children. --> <attr name="forceDarkAllowed" format="boolean" /> </declare-styleable> 一个是 View 级别的,一个是 Theme 级别的。 4.1. Theme 级别 forceDarkAllowed 从上面的总结来看,Theme 级别的开关优先级是最高的,控制粒度也最大,我们看看源码里面使用它的地方 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // frameworks/base/core/java/android/view/ViewRootImpl.java private void updateForceDarkMode() { // 渲染线程为空,直接返回 if (mAttachInfo.mThreadedRenderer == null) return; // 系统是否打开了黑暗模式 boolean useAutoDark = getNightMode() == Configuration.UI_MODE_NIGHT_YES; if (useAutoDark) { // forceDarkAllowed 默认值,开发者模式是否打开了强制 smart dark 选项 boolean forceDarkAllowedDefault = SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false); TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme); // useAutoDark = 使用浅色主题 && 主题中声明的 forceDarkAllowed 值 useAutoDark = a.getBoolean(R.styleable.Theme_isLightTheme, true) && a.getBoolean(R.styleable.Theme_forceDarkAllowed, forceDarkAllowedDefault); a.recycle(); } // 关键代码,设置是否强制夜间模式 if (mAttachInfo.mThreadedRenderer.setForceDark(useAutoDark)) { // TODO: Don't require regenerating all display lists to apply this setting invalidateWorld(mView); } } // frameworks/base/graphics/java/android/graphics/HardwareRenderer.java public boolean setForceDark(boolean enable) { if (mForceDark != enable) { mForceDark = enable; // native 代码,mNativeProxy 其实是 RenderThread 代理类的指针 nSetForceDark(mNativeProxy, enable); return true; } return false; } 这段代码还是比较简单,判断系统: 是否打开了夜间模式 是否使用浅色主题 Theme_forceDarkAllowed 是否为 true 三者同时为 true 时才会设置夜间模式,而 updateForceDarkMode 调用的时机分别是在 ViewRootImpl#setView 和 ViewRootImpl#updateConfiguration,也就是初始化和夜间模式切换的时候都会调用,确保夜间模式能及时启用和关闭。继续跟踪 HardwareRenderer#setForceDark 发现,这是一个 native 方法,所以接下来让我们进入 native 世界,nSetForceDark 对应的实现位于 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // frameworks/base/core/jni/android_view_ThreadedRenderer.cpp static void android_view_ThreadedRenderer_setForceDark(JNIEnv* env, jobject clazz, jlong proxyPtr, jboolean enable) { RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr); proxy->setForceDark(enable); } // frameworks/base/libs/hwui/renderthread/RenderProxy.cpp void RenderProxy::setForceDark(bool enable) { mRenderThread.queue().post([this, enable]() { mContext->setForceDark(enable); }); } // frameworks/base/libs/hwui/renderthread/CanvasContext.h class CanvasContext : public IFrameCallback { public: ... void setForceDark(bool enable) { mUseForceDark = enable; } bool useForceDark() { return mUseForceDark; } ... private: ... // 默认关闭强制夜间模式 bool mUseForceDark = false; ... }; 最终就是设置了一个 CanvasContext 的变量值而已,什么都还没有做,那么这个变量值的作用是什么,什么时候生效呢?我们进一步查看使用的地方: 1 2 3 4 5 6 7 8 9 10 11 // frameworks/base/libs/hwui/TreeInfo.cpp TreeInfo::TreeInfo(TraversalMode mode, renderthread::CanvasContext& canvasContext) : mode(mode) , prepareTextures(mode == MODE_FULL) , canvasContext(canvasContext) , damageGenerationId(canvasContext.getFrameNumber()) // 初始化 TreeInfo 的 disableForceDark 变量,注意变量值意义的变化,0 代表打开夜间模式,>0 代表关闭夜间模式 , disableForceDark(canvasContext.useForceDark() ? 0 : 1) , screenSize(canvasContext.getNextFrameSize()) {} } 进一步看看 disableForceDark 使用的地方 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 // frameworks/base/libs/hwui/RenderNode.cpp /** * 这个可以说是核心方法了,handleForceDark 方法调用栈如下: * - RenderNode#prepareTreeImpl * - RenderNode#pushStagingDisplayListChanges * - RenderNode#syncDisplayList * - RenderNode#handleForceDark * * 而 RenderNode#prepareTree 是绘制的必经之路,每一个节点都会走一遍这个流程 */ void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) { // 若没打开强制夜间模式,直接退出 if (CC_LIKELY(!info || info->disableForceDark)) { return; } // 根据是否有文字、是否有子节点、子节点数量等情况,得出当前 Node 属于 Foreground 还是 Background auto usage = usageHint(); const auto& children = mDisplayList->mChildNodes; if (mDisplayList->hasText()) { usage = UsageHint::Foreground; } if (usage == UsageHint::Unknown) { if (children.size() > 1) { usage = UsageHint::Background; } else if (children.size() == 1 && children.front().getRenderNode()->usageHint() != UsageHint::Background) { usage = UsageHint::Background; } } if (children.size() > 1) { // Crude overlap check SkRect drawn = SkRect::MakeEmpty(); for (auto iter = children.rbegin(); iter != children.rend(); ++iter) { const auto& child = iter->getRenderNode(); // We use stagingProperties here because we haven't yet sync'd the children SkRect bounds = SkRect::MakeXYWH(child->stagingProperties().getX(), child->stagingProperties().getY(), child->stagingProperties().getWidth(), child->stagingProperties().getHeight()); if (bounds.contains(drawn)) { // This contains everything drawn after it, so make it a background child->setUsageHint(UsageHint::Background); } drawn.join(bounds); } } // 根据 UsageHint 设置变色策略:Dark(压暗)、Light(提亮) mDisplayList->mDisplayList.applyColorTransform( usage == UsageHint::Background ? ColorTransform::Dark : ColorTransform::Light); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 // frameworks/base/libs/hwui/RecordingCanvas.cpp void DisplayListData::applyColorTransform(ColorTransform transform) { // transform: Dark 或 Light // color_transform_fns 是一个对应所有绘制指令的函数指针数组,主要是对 op 的 paint 变色或对 bitmap 添加 colorfilter this->map(color_transform_fns, transform); } template <typename Fn, typename... Args> inline void DisplayListData::map(const Fn fns[], Args... args) const { auto end = fBytes.get() + fUsed; // 遍历当前的绘制的 op for (const uint8_t* ptr = fBytes.get(); ptr < end;) { auto op = (const Op*)ptr; auto type = op->type; auto skip = op->skip; // 根据 type 找到对应的 fn,根据调用关系,我们知道 fns 数组对应 color_transform_fns,这个数组其实是一个函数指针数组,下面看看定义 if (auto fn = fns[type]) { // We replace no-op functions with nullptrs // 执行 fn(op, args...); // to avoid the overhead of a pointless call. } ptr += skip; } } #define X(T) colorTransformForOp<T>(), static const color_transform_fn color_transform_fns[] = { X(Flush) X(Save) X(Restore) X(SaveLayer) X(SaveBehind) X(Concat) X(SetMatrix) X(Translate) X(ClipPath) X(ClipRect) X(ClipRRect) X(ClipRegion) X(DrawPaint) X(DrawBehind) X(DrawPath) X(DrawRect) X(DrawRegion) X(DrawOval) X(DrawArc) X(DrawRRect) X(DrawDRRect) X(DrawAnnotation) X(DrawDrawable) X(DrawPicture) X(DrawImage) X(DrawImageNine) X(DrawImageRect) X(DrawImageLattice) X(DrawTextBlob) X(DrawPatch) X(DrawPoints) X(DrawVertices) X(DrawAtlas) X(DrawShadowRec) X(DrawVectorDrawable) }; #undef X color_transform_fn 宏定义展开 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 template <class T> constexpr color_transform_fn colorTransformForOp() { if // op 变量中是否同时包含 paint 及 palette 属性,若同时包含,则是绘制 Image 或者 VectorDrawable 的指令 // 参考:frameworks/base/libs/hwui/RecordingCanvas.cpp 中各 Op 的定义 constexpr(has_paint<T> && has_palette<T>) { return [](const void* opRaw, ColorTransform transform) { const T* op = reinterpret_cast<const T*>(opRaw); // 关键变色方法,根据 palette 叠加 colorfilter transformPaint(transform, const_cast<SkPaint*>(&(op->paint)), op->palette); }; } else if // op 变量中是否包含 paint 属性,普通绘制指令 constexpr(has_paint<T>) { return [](const void* opRaw, ColorTransform transform) { const T* op = reinterpret_cast<const T*>(opRaw); // 关键变色方法,对 paint 颜色进行变换 transformPaint(transform, const_cast<SkPaint*>(&(op->paint))); }; } else { // op 变量不包含 paint 属性,返回空 return nullptr; } } static const color_transform_fn color_transform_fns[] = { // 根据 Flush、Save、DrawImage等不同绘制 op,返回不同的函数指针 colorTransformForOp<Flush> ... }; 让我们再一次看看 map 方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 template <typename Fn, typename... Args> inline void DisplayListData::map(const Fn fns[], Args... args) const { auto end = fBytes.get() + fUsed; for (const uint8_t* ptr = fBytes.get(); ptr < end;) { auto op = (const Op*)ptr; auto type = op->type; auto skip = op->skip; if (auto fn = fns[type]) { // We replace no-op functions with nullptrs // 对 op 的 paint 进行颜色变换或叠加 colorfilter fn(op, args...); // to avoid the overhead of a pointless call. } ptr += skip; } } 贴了一大段代码,虽然代码中已经包含了注释,但还是可能比较晕,我们先来整理下: CanvasContext.mUseForceDark 只会影响 TreeInfo.disableForceDark 的初始化 TreeInfo.disableForceDark 若大于 0,RenderNode 在执行 handleForceDark 就会直接退出 handleForceDark 方法里会根据 UsageHint 类型,对所有 op 中的 paint 颜色进行变换,如果是绘制图片,则叠加一个反转的 colorfilter。变换策略有:Dark、Light 接下来让我们来看 paint 和 colorfilter 的变色实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 bool transformPaint(ColorTransform transform, SkPaint* paint) { applyColorTransform(transform, *paint); return true; } static void applyColorTransform(ColorTransform transform, SkPaint& paint) { if (transform == ColorTransform::None) return; // 对画笔颜色进行颜色变换 SkColor newColor = transformColor(transform, paint.getColor()); paint.setColor(newColor); if (paint.getShader()) { SkShader::GradientInfo info; std::array<SkColor, 10> _colorStorage; std::array<SkScalar, _colorStorage.size()> _offsetStorage; info.fColorCount = _colorStorage.size(); info.fColors = _colorStorage.data(); info.fColorOffsets = _offsetStorage.data(); SkShader::GradientType type = paint.getShader()->asAGradient(&info); if (info.fColorCount <= 10) { switch (type) { case SkShader::kLinear_GradientType: for (int i = 0; i < info.fColorCount; i++) { // 对 shader 中的颜色进行颜色变换 info.fColors[i] = transformColor(transform, info.fColors[i]); } paint.setShader(SkGradientShader::MakeLinear(info.fPoint, info.fColors, info.fColorOffsets, info.fColorCount, info.fTileMode, info.fGradientFlags, nullptr)); break; default:break; } } } if (paint.getColorFilter()) { SkBlendMode mode; SkColor color; // TODO: LRU this or something to avoid spamming new color mode filters if (paint.getColorFilter()->asColorMode(&color, &mode)) { // 对 colorfilter 中的颜色进行颜色变换 color = transformColor(transform, color); paint.setColorFilter(SkColorFilter::MakeModeFilter(color, mode)); } } } 逻辑很简单,就是对颜色进行变换,进一步看看变色逻辑: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 // 提亮颜色 static SkColor makeLight(SkColor color) { // 转换成 Lab 模式 Lab lab = sRGBToLab(color); // 对明度进行反转,明度越高,反转后越低 float invertedL = std::min(110 - lab.L, 100.0f); if (invertedL > lab.L) { // 反转后的明度高于原明度,则使用反转后的明度 lab.L = invertedL; return LabToSRGB(lab, SkColorGetA(color)); } else { return color; } } // 压暗颜色 static SkColor makeDark(SkColor color) { // 转换成 Lab 模式 Lab lab = sRGBToLab(color); // 对明度进行反转,明度越高,反转后越低 float invertedL = std::min(110 - lab.L, 100.0f); if (invertedL < lab.L) { // 反转后的明度低于原明度,则使用反转后的明度 lab.L = invertedL; // 使用 rgb 格式返回 return LabToSRGB(lab, SkColorGetA(color)); } else { // 直接返回原颜色 return color; } } static SkColor transformColor(ColorTransform transform, SkColor color) { switch (transform) { case ColorTransform::Light: return makeLight(color); case ColorTransform::Dark: return makeDark(color); default: return color; } } 到此,对 paint 的变换结束,看来无非就是反转明度。 再来看看对图片的变换: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 bool transformPaint(ColorTransform transform, SkPaint* paint, BitmapPalette palette) { // 根据 palette 和 colorfilter 判断图片是亮还是暗的 palette = filterPalette(paint, palette); bool shouldInvert = false; if (palette == BitmapPalette::Light && transform == ColorTransform::Dark) { // 图片本身是亮的,但是要求变暗,反转 shouldInvert = true; } if (palette == BitmapPalette::Dark && transform == ColorTransform::Light) { // 图片本身是暗的,但是要求变亮,反转 shouldInvert = true; } if (shouldInvert) { SkHighContrastConfig config; config.fInvertStyle = SkHighContrastConfig::InvertStyle::kInvertLightness; // 叠加一个亮度反转的 colorfilter paint->setColorFilter(SkHighContrastFilter::Make(config)->makeComposed(paint->refColorFilter())); } return shouldInvert; } 终于,bitmap 的变换也分析完了,呼~ 4.2. View 级别 forceDarkAllowed 但是,还没完呢~还记得我们最开始说的,除了 Theme 级别,还有一个 View 级别的 forceDarkAllowed,通过 View 级别 forceDarkAllowed 可以关掉它及它的子 view 的夜间模式开关。依然从 java 层看下去哈 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // rameworks/base/core/java/android/view/View.java public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { final TypedArray a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes); final int N = a.getIndexCount(); for (int i = 0; i < N; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.View_forceDarkAllowed: // 注意,这个默认是 true 的 mRenderNode.setForceDarkAllowed(a.getBoolean(attr, true)); break; } } } } // frameworks/base/graphics/java/android/graphics/RenderNode.java public final class RenderNode { public boolean setForceDarkAllowed(boolean allow) { // 又是 native 方法 return nSetAllowForceDark(mNativeRenderNode, allow); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // frameworks/base/core/jni/android_view_RenderNode.cpp static jboolean android_view_RenderNode_setAllowForceDark(jlong renderNodePtr, jboolean allow) { return SET_AND_DIRTY(setAllowForceDark, allow, RenderNode::GENERIC); } // frameworks/base/libs/hwui/RenderProperties.h class ANDROID_API RenderProperties { public: bool setAllowForceDark(bool allow) { // 设置到 mPrimitiveFields.mAllowForceDark 变量中 return RP_SET(mPrimitiveFields.mAllowForceDark, allow); } bool getAllowForceDark() const { return mPrimitiveFields.mAllowForceDark; } } 和 Theme 级别的一样,仅仅只是设置到变量中而已,关键是要看哪里使用这个变量,经过查找,我们发现,它的使用同样在 RenderNode 的 prepareTreeImpl 中: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer) { ... // 1. 如果 view 关闭了夜间模式,会在这里让 info.disableForceDark 加 1 // 2. info.disableForceDark 正是 handleForceDark 中关键变量,还记得吗? // 3. nfo.disableForceDark 大于 0 会让此 RenderNode 跳过夜间模式处理 // 4. 如果 info.disableForceDark 本身已经大于 0 了,view.setForceDarkAllowed(true) 也毫无意义 if (!mProperties.getAllowForceDark()) { info.disableForceDark++; } prepareLayer(info, animatorDirtyMask); if (info.mode == TreeInfo::MODE_FULL) { // 这里面会调用 handleForceDark 方法处理夜间模式 pushStagingDisplayListChanges(observer, info); } if (mDisplayList) { info.out.hasFunctors |= mDisplayList->hasFunctor(); // 递归调用子 Node 的 prepareTreeImpl 方法 bool isDirty = mDisplayList->prepareListAndChildren( observer, info, childFunctorsNeedLayer, [](RenderNode* child, TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer) { child->prepareTreeImpl(observer, info, functorsNeedLayer); }); if (isDirty) { damageSelf(info); } } ... // 重要,把 info.disableForceDark 恢复回原来的值,不让它影响 Tree 中同级的其他 RenderNode // 但是本 RenderNode 的子节点还是会受影响的,这就是为什么父 view 关闭了夜间模式,子 view 也会受影响的原因 // 因为还原 info.disableForceDark 操作是在遍历子节点之后执行的 if (!mProperties.getAllowForceDark()) { info.disableForceDark--; } ... } 5. 总结 本文到目前为止,总算把 Android Q 夜间模式实现原理梳理了一遍,总的来说实现不算复杂,说白了就是把 paint 中的颜色转换一下或者叠加一个 colorfilter,虽然中间还有关联知识没有细说,如 RenderThread、DisplayList、RenderNode 等图形相关的概念,限于文章大小,请读者自行了解 另外,由于水平有限,难免文中有错漏之处,若哪里写的不对,请大家及时指出,蟹蟹啦~

2019/10/22
articleCard.readMore

如何顺滑地查看 Android Native 代码

1. 简介 使用 Android Studio 查看 Android Framework 代码体验非常好,无论是索引还是界面都让人很满意,但是当你跟踪代码,发现进入 native 逻辑时,就会发现 Android Studio 对 native 代码的支持非常不好,不能索引不支持符号搜索不能跳转等,这些让人非常抓狂。那么如何能在 IDE 愉快地查看 native 代码呢?在 Windows 上,Source Insight 的表现也很好,但苦于只有 Windows 平台支持且界面不好,经过一番折腾,还真是找到了方法,下面我们将一步一步打造丝滑的 native 代码阅读环境。 先看一下效果: 2. CMake 能让 IDE 正确地建立索引,我们需要让 IDE 能正确地知道源文件、头文件、宏定义等各种数据,庆幸的是,我们发现 AOSP 在编译过程中,可以帮我们生成这些数据,详见:http://androidxref.com/9.0.0_r3/xref/build/soong/docs/clion.md 通过文档我们可知,只需要按照以下步骤完成一次编译,即可自动生成各模块对应的 CMake 文件。至于 Cmake 文件是什么,这里就不做赘述了,大家可以自行了解。 打开以下两个开关,CMakeLists.txt 就会根据编译环境自动生成 1 2 export SOONG_GEN_CMAKEFILES=1 export SOONG_GEN_CMAKEFILES_DEBUG=1 启动编译 1 make -j16 或者只编译你需要的模块 1 make frameworks/native/service/libs/ui 生成的文件存放在 out 目录,比如刚刚编译的 libui 模块对应的路径为: 1 out/development/ide/clion/frameworks/native/libs/ui/libui-arm64-android/CMakeLists.txt 合并多个模块 生成了 CMake 后,我们发现,CMake 文件是按模块生成的。这样的话,会导致 IDE 只能单独导入一个模块,而我们平时不可能只看一个模块的代码,如果把多个模块都 include 进来呢? 我们可以在 out/development/ide/clion 路径新建一个 CMakeLists.txt 文件,并添加一下内容: 1 2 3 4 5 6 7 # 指定 CMake 最低版本 cmake_minimum_required(VERSION 3.6) # 指定工程名,随意 project(aosp) # 把你需要的模块通过 add_subdirectory 添加进来,注意子目录必须也包含 CMakeLists.txt 文件 add_subdirectory(frameworks/native) #add_subdirectory(frameworks/base/core/jni/libandroid_runtime-arm64-android) 这样,我们就把多个模块合并在一起了,用 IDE 去打开这个总的 CMake 文件即可 3. 导入 IDE 只要生成 CMake 文件后,剩下的事情就好办了,现在能识别 CMake 工程的 IDE 非常多,大家可以根据个人喜好选择,如: CLion Eclipse Visual Studio … 这里以 CLion 为例讲一下如何导入 打开 CLion 选择「New CMake Project from Sources」 指定包含 CMakeLists.txt 的目录,如我们在上一个步骤中说的 out/development/ide/clion(这个目录的 CMakeLists.txt 包含了多个模块,还记得吗?) 选择「Open Existing Project」 Enjoy your journey … 当然,CLion 也有一个缺点,收费!!如何能免费使用就看大家各显神通了 4. 遇到的一些问题 生成的 CMakeLists.txt 里指定路径可能会使用绝对路径,如: set(ANDROID_ROOT /Volumes/AndroidSource/M1882_QOF7_base),这里大家要注意,如果把 CMakeLists.txt 拷贝到别的工程使用,记得修正一下路径 Mac 用户留意,如果你的 CMakeLists.txt 是从 linux 平台生成拷贝过来的,生成的 CMakeLists.txt 里指定的 c++ 编译器 set(CMAKE_CXX_COMPILER "${ANDROID_ROOT}/prebuilts/clang/host/linux-x86/clang-3977809/bin/clang++") 这里指定的是 linux-x86 的编译器,记得替换成 darwin-x86,如果对应目录下没有 clang++,那就从 AOSP 源码拷一个吧 如果 CMake 中列出的源文件在工程中找不到,会导致 CLion 停止索引,如果出现不一致的时候,移除 CMake 中源文件的声明即可 如果使用遇到其他问题,欢迎联系告知,谢谢 5. 总结 所谓工欲善其事,必先利其器。通过这种方法建立的索引包含了 AOSP 所有模块,最重要是它还会根据编译环境,把相关 FLAGS 和宏都设置好。

2019/10/11
articleCard.readMore

AOSP 编译和烧写

1. 简介 很多 Android 开发者都会希望编译 Android 源码并刷进自己的手机里面,但网上教程很多都仅仅是告诉你 lunch、make 等等,但你手里有一台设备时却发现,你编译出的镜像由于驱动关系是不能直接烧进手机的。这里整理了一下步骤,帮助大家可以按照流程编译并烧写镜像。 本篇文章以 Pixel 2 && Android 10 为例 2. 环境准备 这块没啥说,官方教程就够了,参考:https://source.android.com/setup/build/initializing 就行了 3. 源码下载 根据 https://developers.google.com/android/drivers 选择一个设备对应 Android 版本号和驱动,比如我们选择:Android 10.0.0 (QP1A.190711.020),下载驱动,记住 Build 号 在 https://source.android.com/setup/start/build-numbers 查找 QP1A.190711.020 对应的分支:android-10.0.0_r2,记住分支名 下载 AOSP 源码 注意在下载 aosp 前要安装 repo 工具,参考:https://source.android.com/setup/build/downloading 1 2 3 4 5 mkdir Pixel2 cd Pixel2 repo init -u https://android.googlesource.com/platform/manifest -b android-10.0.0_r2 --depth=1 repo sync -j8 repo start android-10.0.0_r2 --all 把步骤1中选中的两个驱动下载到 aosp 源码根目录并解压 分别执行解压后的文件,注意,执行后要同意 License,确保正确解压到 aosp 根目录的 vendor 目录 1 2 ./extract-qcom-walleye.sh ./extract-google_devices-walleye.sh 4. 源码编译 在 aosp 源码根目录执行:source build/envsetup.sh(注意,执行前终端请选bash,不要使用zsh等,在终端键入bash回车即可) 在 aosp 源码根目录执行:lunch 选择对应的版本,比如 Pixel2 就选择:aosp_walleye-userdebug 执行:make -j8 5. 镜像烧写 编译完后,执行:export ANDROID_PRODUCT_OUT=/home/chenhang/source/Pixel2/out/target/product/walleye 执行:fastboot flashall -w 烧写完成后,执行:fastboot reboot 6. Gapps 安装 编译出来的 aosp 默认没有 google 全家桶,可以通过以下方式进行安装 在 https://opengapps.org/ 根据系统版本、芯片类型选择需要的 Gapps 全家桶,可以选 stock 版本 下载后把全家桶 push 到手机 sdcard(不用解压) 在 https://twrp.me/devices/ 搜索你的设备,如: https://twrp.me/google/googlepixel2.html 下载 twrp.img 后根据截图中的命令,把 twrp 加载到手机, 选择 install 刷入 twrp.zip (这是一个 recovery 版本),重启后,通过 adb reboot recovery 进入 twrp 的recovery 系统 在手机上选择 install, 选择步骤2中 push 到手机 sdcard 的全家桶,安装结束后选择擦除 dalvik cache 和 cache,重启即可

2019/9/12
articleCard.readMore

Protocol Buffers 手册

简介 Protocol Buffers 是 google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。 至于protobuf是什么、使用场景、有什么好处,本文不做说明,这里将会为大家介绍怎么用 protobuf 来定义我们的交互协议,包括 .proto 的语法以及如何根据proto文件生成相应的代码。本文基于proto3,读者也可以点击了解proto2 proto3语法 定义一个 Message 首先我们来定义一个 Search 请求,在这个请求里面,我们需要给服务端发送三个信息: query:查询条件 page_number:你想要哪一页数据 result_per_page:每一页有多少条数据 于是我们可以这样定义: 1 2 3 4 5 6 7 8 9 // 指定使用proto3,如果不指定的话,编译器会使用proto2去编译 syntax = "proto3"; //[proto2|proto3] message SearchRequests { // 定义SearchRequests的成员变量,需要指定:变量类型、变量名、变量Tag string query = 1; int32 page_number = 2; int32 result_per_page = 3; } 定义多个 message 类型 一个 proto 文件可以定义多个 message ,比如我们可以在刚才那个 proto 文件中把服务端返回的消息结构也一起定义: 1 2 3 4 5 6 7 8 9 message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; } message SearchResponse { repeated string result = 1; } message 可以嵌套定义,比如 message 可以定义在另一个 message 内部 1 2 3 4 5 6 7 8 message SearchResponse { message Result { string url = 1; string title = 2; repeated string snippets = 3; } repeated Result results = 1; } 定义在 message 内部的 message 可以这样使用: 1 2 3 message SomeOtherMessage { SearchResponse.Result result = 1; } 定义变量类型 在刚才的例子之中,我们使用了2个标准值类型: string 和 int32,除了这些标准类型之外,变量的类型还可以是复杂类型,比如自定义的枚举和自定义的 message 这里我们把标准类型列举一下protobuf内置的标准类型以及跟各平台对应的关系: .proto说明C++JavaPythonGoRubyC#PHP doubledoubledoublefloatfloat64Floatdoublefloat floatfloatfloatfloatfloat32Floatfloatfloat int32使用变长编码,对负数编码效率低,如果你的变量可能是负数,可以使用sint32int32intintint32Fixnum or Bignum (as required)intinteger int64使用变长编码,对负数编码效率低,如果你的变量可能是负数,可以使用sint64int64longint/longint64Bignumlonginteger/string uint32使用变长编码uint32intint/longuint32Fixnum or Bignum (as required)uintinteger uint64使用变长编码uint64longint/longuint64Bignumulonginteger/string sint32使用变长编码,带符号的int类型,对负数编码比int32高效int32intintint32Fixnum or Bignum (as required)intinteger sint64使用变长编码,带符号的int类型,对负数编码比int64高效int64longint/longint64Bignumlonginteger/string fixed324字节编码, 如果变量经常大于$ 2^{28} $ 的话,会比uint32高效uint32intintint32Fixnum or Bignum (as required)uintinteger fixed648字节编码, 如果变量经常大于$ 2^{56} $ 的话,会比uint64高效uint64longint/longuint64Bignumulonginteger/string sfixed324字节编码int32intintint32Fixnum or Bignum (as required)intinteger sfixed648字节编码int64longint/longint64Bignumlonginteger/string boolboolbooleanboolboolTrueClass/FalseClassboolboolean string必须包含utf-8编码或者7-bit ASCII textstringStringstr/unicodestringString (UTF-8)stringstring bytes任意的字节序列stringByteStringstr[]byteString (ASCII-8BIT)ByteStringstring 补充说明: In Java, unsigned 32-bit and 64-bit integers are represented using their signed counterparts, with the top bit simply being stored in the sign bit. In all cases, setting values to a field will perform type checking to make sure it is valid. 64-bit or unsigned 32-bit integers are always represented as long when decoded, but can be an int if an int is given when setting the field. In all cases, the value must fit in the type represented when set. See 2. Python strings are represented as unicode on decode but can be str if an ASCII string is given (this is subject to change). Integer is used on 64-bit machines and string is used on 32-bit machines. 关于标准值类型,还可以参考Scalar Value Types 如果你想了解这些数据是怎么序列化和反序列化的,可以点击 Protocol Buffer Encoding 了解更多关于protobuf编码内容。 分配Tag 每一个变量在message内都需要自定义一个唯一的数字Tag,protobuf会根据Tag从数据中查找变量对应的位置,具体原理跟protobuf的二进制数据格式有关。Tag一旦指定,以后更新协议的时候也不能修改,否则无法对旧版本兼容。 Tag的取值范围最小是1,最大是$ 2^{29} $-1,但 19000~19999 是 protobuf 预留的,用户不能使用。 虽然 Tag 的定义范围比较大,但不同 Tag 也会对 protobuf 编码带来一些影响: 1 ~ 15:单字节编码 16 ~ 2047:双字节编码 使用频率高的变量最好设置为1 ~ 15,这样可以减少编码后的数据大小,但由于Tag一旦指定不能修改,所以为了以后扩展,也记得为未来保留一些 1 ~ 15 的 Tag 指定变量规则 在 proto3 中,可以给变量指定以下两个规则: singular:0或者1个,但不能多于1个 repeated:任意数量(包括0) 当构建 message 的时候,build 数据的时候,会检测设置的数据跟规则是否匹配 在proto2中,规则为: required:必须有一个 optional:0或者1个 repeated:任意数量(包括0) 注释 用//表示注释开头,如 1 2 3 4 5 message SearchRequest { string query = 1; int32 page_number = 2; // Which page number do we want int32 result_per_page = 3; // Number of results to return per page } 保留变量不被使用 上面我们说到,一旦 Tag 指定后就不能变更,这就会带来一个问题,假如在版本1的协议中,我们有个变量: 1 int32 number = 1; 在版本2中,我们决定废弃对它的使用,那我们应该如何修改协议呢?注释掉它?删除掉它?如果把它删除了,后来者很可能在定义新变量的时候,使新的变量 Tag = 1 ,这样会导致协议不兼容。那有没有办法规避这个问题呢?我们可以用 reserved 关键字,当一个变量不再使用的时候,我们可以把它的变量名或 Tag 用 reserved 标注,这样,当这个 Tag 或者变量名字被重新使用的时候,编译器会报错 1 2 3 4 5 message Foo { // 注意,同一个 reserved 语句不能同时包含变量名和 Tag reserved 2, 15, 9 to 11; reserved "foo", "bar"; } 默认值 当解析 message 时,如果被编码的 message 里没有包含某些变量,那么根据类型不同,他们会有不同的默认值: string:默认是空的字符串 byte:默认是空的bytes bool:默认为false numeric:默认为0 enums:定义在第一位的枚举值,也就是0 messages:根据生成的不同语言有不同的表现,参考generated code guide 注意,收到数据后反序列化后,对于标准值类型的数据,比如bool,如果它的值是 false,那么我们无法判断这个值是对方设置的,还是对方压根就没给这个变量设置值。 定义枚举 Enumerations 在 protobuf 中,我们也可以定义枚举,并且使用该枚举类型,比如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 message SearchRequest { string query = 1; int32 page_number = 2; // Which page number do we want int32 result_per_page = 3; // Number of results to return per page enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; } Corpus corpus = 4; } 枚举定义在一个消息内部或消息外部都是可以的,如果枚举是 定义在 message 内部,而其他 message 又想使用,那么可以通过 MessageType.EnumType 的方式引用。定义枚举的时候,我们要保证第一个枚举值必须是0,枚举值不能重复,除非使用 option allow_alias = true 选项来开启别名。如: 1 2 3 4 5 6 enum EnumAllowingAlias { option allow_alias = true; UNKNOWN = 0; STARTED = 1; RUNNING = 1; } 枚举值的范围是32-bit integer,但因为枚举值使用变长编码,所以不推荐使用负数作为枚举值,因为这会带来效率问题。 如何引用其他 proto 文件 在proto语法中,有两种引用其他 proto 文件的方法: import 和 import public,这两者有什么区别呢?下面举个例子说明: 在情景1中, my.proto 不能使用 second.proto 中定义的内容 在情景2中, my.proto 可以使用 second.proto 中定义的内容 情景1和情景2中,my.proto 都可以使用 first.proto 情景1和情景2中,first.proto 都可以使用 second.proto 1 2 // my.proto import "first.proto"; 1 2 3 // first.proto //import "second.proto"; import public "second.proto"; 1 2 // second.proto ... 升级 proto 文件正确的姿势 升级更改 proto 需要遵循以下原则 不要修改任何已存在的变量的 Tag 如果你新增了变量,新生成的代码依然能解析旧的数据,但新增的变量将会变成默认值。相应的,新代码序列化的数据也能被旧的代码解析,但旧代码会自动忽略新增的变量。 废弃不用的变量用 reserved 标注 int32、 uint32、 int64、 uint64 和 bool 是相互兼容的,这意味你可以更改这些变量的类型而不会影响兼容性 sint32 和 sint64 是兼容的,但跟其他类型不兼容 string 和 bytes 可以兼容,前提是他们都是UTF-8编码的数据 fixed32 和 sfixed32 是兼容的, fixed64 和 sfixed64是兼容的 Any 的使用 Any可以让你在 proto 文件中使用未定义的类型,具体里面保存什么数据,是在上层业务代码使用的时候决定的,使用 Any 必须导入 import google/protobuf/any.proto 1 2 3 4 5 6 import "google/protobuf/any.proto"; message ErrorStatus { string message = 1; repeated google.protobuf.Any details = 2; } Oneof 的使用 Oneof 类似union,如果你的消息中有很多可选字段,而同一个时刻最多仅有其中的一个字段被设置的话,你可以使用oneof来强化这个特性并且节约存储空间,如 1 2 3 4 5 6 7 8 message LoginReply { oneof test_oneof { string name = 3; string age = 4; } required string status = 1; required string token = 2; } 这样,name 和 age 都是 LoginReply 的成员,但不能给他们同时设置值(设置一个oneof字段会自动清理其他的oneof字段)。 Maps 的使用 protobuf 支持定义 map 类型的成员,如: 1 2 map<key_type, value_type> map_field = N; // 举例:map<string, Project> projects = 3; key_type:必须是string或者int value_type:任意类型 使用 map 要注意: Map 类型不能使 repeated Map 是无序的 以文本格式展示时,Map 以 key 来排序 如果有相同的键会导致解析失败 Packages 的使用 为了防止不同消息之间的命名冲突,你可以对特定的.proto文件提指定 package 名字。在定义消息的成员的时候,可以指定包的名字: 1 2 package foo.bar; message Open { ... } 1 2 3 4 5 6 message Foo { ... // 带上包名 foo.bar.Open open = 1; ... } Options Options 分为 file-level options(只能出现在最顶层,不能在消息、枚举、服务内部使用)、 message-level options(只能在消息内部使用)、field-level options(只能在变量定义时使用) java_package (file option):指定生成类的包名,如果没有指定此选项,将由关键字package指定包名。此选项只在生成 java 代码时有效 java_multiple_files (file option):如果为 true, 定义在最外层的 message 、enum、service 将作为单独的类存在 java_outer_classname (file option):指定最外层class的类名,如果不指定,将会以文件名作为类名 optimize_for (file option):可选有 [SPEED|CODE_SIZE|LITE_RUNTIME] ,分别是效率优先、空间优先,第三个lite是兼顾效率和代码大小,但是运行时需要依赖 libprotobuf-lite cc_enable_arenas (file option):启动arena allocation,c++代码使用 objc_class_prefix (file option):Objective-C使用 deprecated (field option):提示变量已废弃、不建议使用 1 2 3 4 5 option java_package = "com.example.foo"; option java_multiple_files = true; option java_outer_classname = "Ponycopter"; option optimize_for = CODE_SIZE; int32 old_field = 6 [deprecated=true]; 定义 Services 这个其实和gRPC相关,详细可参考:gRPC, 这里做一个简单的介绍 要定义一个服务,你必须在你的 .proto 文件中指定 service 1 2 3 service RouteGuide { ... } 然后在我们的服务中定义 rpc 方法,指定它们的请求的和响应类型。gRPC 允许你定义4种类型的 service 方法 简单RPC 客户端使用 Stub 发送请求到服务器并等待响应返回,就像平常的函数调用一样,这是一个阻塞型的调用 1 2 // Obtains the feature at a given position. rpc GetFeature(Point) returns (Feature) {} 服务器端流式 RPC 客户端发送请求到服务器,拿到一个流去读取返回的消息序列。客户端读取返回的流,直到里面没有任何消息。从例子中可以看出,通过在响应类型前插入 stream 关键字,可以指定一个服务器端的流方法 1 2 3 4 5 // Obtains the Features available within the given Rectangle. Results are // streamed rather than returned at once (e.g. in a response message with a // repeated field), as the rectangle may cover a large area and contain a // huge number of features. rpc ListFeatures(Rectangle) returns (stream Feature) {} 客户端流式 RPC 客户端写入一个消息序列并将其发送到服务器,同样也是使用流。一旦客户端完成写入消息,它等待服务器完成读取返回它的响应。通过在请求类型前指定 stream 关键字来指定一个客户端的流方法 1 2 3 // Accepts a stream of Points on a route being traversed, returning a // RouteSummary when traversal is completed. rpc RecordRoute(stream Point) returns (RouteSummary) {} 双向流式 RPC 双方使用读写流去发送一个消息序列。两个流独立操作,因此客户端和服务器可以以任意喜欢的顺序读写:比如, 服务器可以在写入响应前等待接收所有的客户端消息,或者可以交替的读取和写入消息,或者其他读写的组合。每个流中的消息顺序被预留。你可以通过在请求和响应前加 stream 关键字去制定方法的类型 1 2 3 // Accepts a stream of RouteNotes sent while a route is being traversed, // while receiving other RouteNotes (e.g. from other users). rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} 代码生成 使用 protoc 工具可以把编写好的 proto 文件“编译”为Java, Python, C++, Go, Ruby, JavaNano, Objective-C,或C#代码, protoc 可以从点击这里进行下载。protoc 的使用方式如下: 1 protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto 参数说明: IMPORT_PATH:指定 proto 文件的路径,如果没有指定, protoc 会从当前目录搜索对应的 proto 文件,如果有多个路径,那么可以指定多次--proto_path 指定各语言代码的输出路径 –cpp_out:生成c++代码 java_out :生成java代码 python_out :生成python代码 go_out :生成go代码 ruby_out :生成ruby代码 javanano_out :适合运行在有资源限制的平台(如Android)的java代码 objc_out :生成 Objective-C代码 csharp_out :生成C#代码 php_out :生成PHP代码

2017/4/8
articleCard.readMore

设计模式之装饰模式

概述 装饰模式(Decorator)也叫包装器模式(Wrapper),是指动态地给一个对象添加一些额外的职责,就增加功能来说装饰模式比生成子类更为灵活。它通过创建一个包装对象,也就是装饰来包裹真实的对象 情景举例 我们先来分析这样一个画图形的需求: 它能绘制各种背景,如红色、蓝色、绿色 它能绘制形状,如三角形,正方形,圆形 它能给形状加上阴影 就先列这三个简单的需求吧,下面让我们比较下各种实现的优缺点 丑陋的实现 来看看我们用继承是如何实现的,首先,抽象出一个Shape接口我想大家都不会有意见的是不是? 1 2 3 4 5 6 7 8 9 10 /** * @author HansChen */ public interface Shape { /** * 绘制图形 */ void draw(); } 然后我们定义各种情况下的子类,结构如下,看到这么多的子类,是不是有点要爆炸的感觉?真是想想都可怕 而且如果再新增一种需求,比如现在要画椭圆,那么维护的人员估计就要爆粗了吧? 为了避免写出上面的代码,聪明的童鞋们可能会提出第二种方案: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 /** * @author HansChen */ public class ShapeImpl implements Shape { enum Type { Circle, Square, Trilatera } enum Color { Red, Green, Blue } private Type type; private Color color; private boolean shadow; public ShapeImpl() { } public Type getType() { return type; } public void setType(Type type) { this.type = type; } public Color getColor() { return color; } public void setColor(Color color) { this.color = color; } public boolean isShadow() { return shadow; } public void setShadow(boolean shadow) { this.shadow = shadow; } @Override public void draw() { // TODO: 2017/3/9 根据属性情况画出不同的图 } } 这样,根据不同的画图需求,只需要设置不同的属性就可以了,这样确实避免了类爆炸增长的问题,但这种方式违反了开放封闭原则,比如画正方形的方式变了,需要对ShapeImpl进行修改,或者如果新增需求,如画椭圆,也需要对ShapeImpl进行修改。而且这个类不方便扩展,子类将继承一些对自身并不合适的方法。 装饰模式 概念介绍 装饰模式(Decorator)也叫包装器模式(Wrapper),是指动态地给一个对象添加一些额外的职责 以下情况使用Decorator模式: 需要扩展一个类的功能,或给一个类添加附加职责。 需要动态的给一个对象添加功能,这些功能可以再动态的撤销。 需要增加由一些基本功能的排列组合而产生的非常大量的功能,从而使继承关系变的不现实。 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类 但这种灵活也会带来一些缺点,这种比继承更加灵活机动的特性,也同时意味着更加多的复杂性。装饰模式会导致设计中出现许多小类,如果过度使用,会使程序变得很复杂 下面来看看装饰模式的结构: Component抽象组件,是一个接口或者是抽象类,就是定义我们最核心的对象,也就是最原始的对象。(注:在装饰模式中,必然有一个最基本、最核心、最原始的接口或者抽象类充当Component抽象组件) ConcreteComponent具体组件,是最核心、最原始、最基本的接口或抽象类的实现,我们需要装饰的就是它 Decorator装饰角色, 一般是一个抽象类,实现接口或者抽象方法,它的属性里必然有一个private变量指向Component抽象组件。 具体装饰角色,如上图中的ConcreteDecoratorA和ConcreteDecoratorB,我们要把我们最核心的、最原始的、最基本的东西装饰成其它东西。 代码示例如下: 1 2 3 4 5 6 7 /** * @author HansChen */ public interface Component { void operation(); } 1 2 3 4 5 6 7 public class ConcreteComponent implements Component { @Override public void operation() { System.out.print("do something"); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 public class Decorator implements Component { private Component component; public Decorator(Component component) { this.component = component; } @Override public void operation() { component.operation(); } } 1 2 3 4 5 6 7 8 9 10 11 12 public class ConcreteDecoratorA extends Decorator { public ConcreteDecoratorA(Component component) { super(component); } @Override public void operation() { super.operation(); System.out.println("do something"); } } 1 2 3 4 5 6 7 8 9 10 11 12 public class ConcreteDecoratorB extends Decorator { public ConcreteDecoratorB(Component component) { super(component); } @Override public void operation() { super.operation(); System.out.println("do something"); } } 上面说了一堆结构和示例代码,但大家可能还是不太好理解,下面用装饰模式来重新实现画图的功能 用装饰模式实现需求 先上结构图 首先定义可动态扩展对象的抽象 1 2 3 4 5 6 7 public interface Shape { /** * 绘制图形 */ void draw(); } 定义具体的组件,每一个组件代表一个形状 1 2 3 4 5 6 7 public class Square implements Shape { @Override public void draw() { System.out.print("正方形"); } } 1 2 3 4 5 6 7 public class Trilateral implements Shape { @Override public void draw() { System.out.print("三角形"); } } 1 2 3 4 5 6 7 public class Circle implements Shape { @Override public void draw() { System.out.print("圆形"); } } 定义可装饰者的抽象类 1 2 3 4 5 6 7 8 9 10 11 12 13 public class ShapeDecorator implements Shape { private Shape shape; public ShapeDecorator(Shape shape) { this.shape = shape; } @Override public void draw() { shape.draw(); } } 定义具体的装饰者 1 2 3 4 5 6 7 8 9 10 11 12 public class Blue extends ShapeDecorator { public Blue(Shape shape) { super(shape); } @Override public void draw() { super.draw(); System.out.print(" 蓝色"); } } 1 2 3 4 5 6 7 8 9 10 11 12 public class Green extends ShapeDecorator { public Green(Shape shape) { super(shape); } @Override public void draw() { super.draw(); System.out.print(" 绿色"); } } 1 2 3 4 5 6 7 8 9 10 11 12 public class Red extends ShapeDecorator { public Red(Shape shape) { super(shape); } @Override public void draw() { super.draw(); System.out.print(" 红色"); } } 1 2 3 4 5 6 7 8 9 10 11 12 public class Shadow extends ShapeDecorator { public Shadow(Shape shape) { super(shape); } @Override public void draw() { super.draw(); System.out.print(" 有阴影"); } } 好了,现在让我们看看具体怎么使用: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Test { public static void main(String[] args) { //正方形 红色 有阴影 Shape shape = new Square(); shape = new Red(shape); shape = new Shadow(shape); shape.draw(); //圆形 绿色 shape = new Circle(); shape = new Green(shape); shape.draw(); //三角形 蓝色 有阴影 shape = new Trilateral(); shape = new Blue(shape); shape = new Shadow(shape); shape.draw(); } } 可以看到,装饰模式是非常灵活的,通过不同的装饰,实现不同的效果 装饰模式的应用举例 这里再列举一些用到了装饰模式的情景,童鞋们可以根据这些场景加深对装饰模式的理解 Java中IO设计 Android中Context和ContextWrapper的设计 总结 装饰模式是为已有功能动态地添加功能的一种方式,它把每个要装饰的功能放在单独的类中,并让这个类包括要装饰的对象,有效地把核心职能和装饰功能区分开了。但它带来灵活的同时,也容易导致别人不了解自己的设计方式,不知如何使用。就像Java中I/O库,人们第一次接触的时候,往往无法轻易理解它。这其中的平衡取舍,就看自己咯

2017/3/9
articleCard.readMore

设计模式之桥接模式

场景问题 发送消息 现在我们要实现这样一个功能:发送消息。从业务上看,消息又分成普通消息、加急消息和特急消息多种,不同的消息类型,业务功能处理是不一样的,比如加急消息是在消息上添加“加急”字样,而特急消息除了添加特急外,还会做一条催促的记录,多久不完成会继续催促。从发送消息的手段上看,又有系统内短消息、手机短消息、邮件等等。现在要实现这样的发送提示消息的功能,该如何实现呢? 不用模式的解决方案 实现简化版本 先实现一个简单点的版本:消息只是实现发送普通消息,发送的方式先实现系统内短消息和邮件。其它的功能,等这个版本完成过后,再继续添加,这样先把问题简单化,实现起来会容易一点。由于发送普通消息会有两种不同的实现方式,为了让外部能统一操作,因此,把消息设计成接口,然后由两个不同的实现类,分别实现系统内短消息方式和邮件发送消息的方式。此时系统结构如下: 先来看看消息的统一接口,示例代码如下: 1 2 3 4 5 6 7 8 9 10 public interface Message { /** * 发送消息 * * @param message 要发送的消息内容 * @param toUser 消息发送的目的人员 */ void send(String message, String toUser); } 再来分别看看两种实现方式,这里只是为了示意,并不会真的去发送Email和站内短消息,先看站内短消息的方式,示例代码如下: 1 2 3 4 5 6 7 public class CommonMessageSMS implements Message { @Override public void send(String message, String toUser) { System.out.println("使用站内短消息的方式,发送消息'" + message + "'给" + toUser); } } 同样的,实现以Email的方式发送普通消息,示例代码如下: 1 2 3 4 5 6 7 public class CommonMessageEmail implements Message { @Override public void send(String message, String toUser) { System.out.println("使用Email的方式,发送消息'" + message + "'给" + toUser); } } 实现发送加急消息 上面的实现,看起来很简单,对不对。接下来,添加发送加急消息的功能,也有两种发送的方式,同样是站内短消息和Email的方式。 加急消息的实现跟普通消息不同,加急消息会自动在消息上添加加急,然后再发送消息;另外加急消息会提供监控的方法,让客户端可以随时通过这个方法来了解对于加急消息处理的进度,比如:相应的人员是否接收到这个信息,相应的工作是否已经开展等等。因此加急消息需要扩展出一个新的接口,除了基本的发送消息的功能,还需要添加监控的功能,这个时候,系统的结构如图所示: 先看看扩展出来的加急消息的接口,示例代码如下: 1 2 3 4 5 6 7 8 9 10 public interface UrgencyMessage extends Message { /** * 监控某消息的处理过程 * * @param messageId 被监控的消息的编号 * @return 包含监控到的数据对象,这里示意一下,所以用了Object */ Object watch(String messageId); } 相应的实现方式还是发送站内短消息和Email两种,同样需要两个实现类来分别实现这两种方式,先看站内短消息的方式,示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class UrgencyMessageSMS implements UrgencyMessage { @Override public void send(String message, String toUser) { message = "加急:" + message; System.out.println("使用站内短消息的方式,发送消息'" + message + "'给" + toUser); } @Override public Object watch(String messageId) { //获取相应的数据,组织成监控的数据对象,然后返回 return null; } } 再看看Emai的方式,示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class UrgencyMessageEmail implements UrgencyMessage { @Override public void send(String message, String toUser) { message = "加急:" + message; System.out.println("使用Email的方式,发送消息'" + message + "'给" + toUser); } @Override public Object watch(String messageId) { //获取相应的数据,组织成监控的数据对象,然后返回 return null; } } 事实上,在实现加急消息发送的功能上,可能会使用前面发送不同消息的功能,也就是让实现加急消息处理的对象继承普通消息的相应实现,这里为了让结构简单一点,清晰一点,所以没有这样做。 有何问题 上面这样实现,好像也能满足基本的功能要求,可是这么实现好不好呢?有没有什么问题呢? 我们继续向下来添加功能实现,为了简洁,就不再去进行代码示意了,通过实现的结构示意图就可以看出实现上的问题。 继续添加特急消息的处理 特急消息不需要查看处理进程,只要没有完成,就直接催促,也就是说,对于特急消息,在普通消息的处理基础上,需要添加催促的功能。而特急消息、还有催促的发送方式,相应的实现方式还是发送站内短消息和Email两种,此时系统的结构如图所示: 仔细观察上面的系统结构示意图,会发现一个很明显的问题,那就是:通过这种继承的方式来扩展消息处理,会非常不方便。 你看,实现加急消息处理的时候,必须实现站内短消息和Email两种处理方式,因为业务处理可能不同;在实现特急消息处理的时候,又必须实现站内短消息和Email这两种处理方式。 这意味着,以后每次扩展一下消息处理,都必须要实现这两种处理方式,是不是很痛苦,这还不算完,如果要添加新的实现方式呢?继续向下看吧。 继续添加发送手机消息的处理方式 如果看到上面的实现,你还感觉问题不是很大的话,继续完成功能,添加发送手机消息的处理方式 仔细观察现在的实现,如果要添加一种新的发送消息的方式,是需要在每一种抽象的具体实现里面,都要添加发送手机消息的处理的。也就是说:发送普通消息、加急消息和特急消息的处理,都可以通过手机来发送。这就意味着,需要添加三个实现。此时系统结构如图所示: 这下能体会到这种实现方式的大问题了吧。 小结一下出现的问题 采用通过继承来扩展的实现方式,有个明显的缺点:扩展消息的种类不太容易,不同种类的消息具有不同的业务,也就是有不同的实现,在这种情况下,每个种类的消息,需要实现所有不同的消息发送方式。 更可怕的是,如果要新加入一种消息的发送方式,那么会要求所有的消息种类,都要加入这种新的发送方式的实现。 要是考虑业务功能上再扩展一下呢?比如:要求实现群发消息,也就是一次可以发送多条消息,这就意味着很多地方都得修改,太恐怖了。 那么究竟该如何实现才能既实现功能,又能灵活的扩展呢? 解决方案 桥接模式来解决 用来解决上述问题的一个合理的解决方案,就是使用桥接模式。那么什么是桥接模式呢? 桥接模式定义: 将抽象部分和实现部分分离,使它们都可以独立地变化 应用桥接模式来解决的思路 仔细分析上面的示例,根据示例的功能要求,示例的变化具有两个维度,一个维度是抽象的消息这边,包括普通消息、加急消息和特急消息,这几个抽象的消息本身就具有一定的关系,加急消息和特急消息会扩展普通消息;另一个维度在具体的消息发送方式上,包括站内短消息、Email和手机短信息,这几个方式是平等的,可被切换的方式。这两个维度一共可以组合出9种不同的可能性来。 现在出现问题的根本原因,就在于消息的抽象和实现是混杂在一起的,这就导致了,一个维度的变化,会引起另一个维度进行相应的变化,从而使得程序扩展起来非常困难。 要想解决这个问题,就必须把这两个维度分开,也就是将抽象部分和实现部分分开,让它们相互独立,这样就可以实现独立的变化,使扩展变得简单。 桥接模式通过引入实现的接口,把实现部分从系统中分离出去;那么,抽象这边如何使用具体的实现呢?肯定是面向实现的接口来编程了,为了让抽象这边能够很方便的与实现结合起来,把顶层的抽象接口改成抽象类,在里面持有一个具体的实现部分的实例。 这样一来,对于需要发送消息的客户端而言,就只需要创建相应的消息对象,然后调用这个消息对象的方法就可以了,这个消息对象会调用持有的真正的消息发送方式来把消息发送出去。也就是说客户端只是想要发送消息而已,并不想关心具体如何发送。 模式结构和说明 桥接模式的结构图: Abstraction:抽象部分的接口。通常在这个对象里面,要维护一个实现部分的对象引用,在抽象对象里面的方法,需要调用实现部分的对象来完成。这个对象里面的方法,通常都是跟具体的业务相关的方法。 RefinedAbstraction:扩展抽象部分的接口,通常在这些对象里面,定义跟实际业务相关的方法,这些方法的实现通常会使用Abstraction中定义的方法,也可能需要调用实现部分的对象来完成。 Implementor:定义实现部分的接口,这个接口不用和Abstraction里面的方法一致,通常是由Implementor接口提供基本的操作,而Abstraction里面定义的是基于这些基本操作的业务方法,也就是说Abstraction定义了基于这些基本操作的较高层次的操作。 ConcreteImplementor:真正实现Implementor接口的对象。 桥接模式示例代码 先看看Implementor接口的定义,示例代码如下: 1 2 3 4 public interface Implementor { void operationImpl(); } 再看看Abstraction接口的定义,注意一点,虽然说是接口定义,但其实是实现成为抽象类。示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public abstract class Abstraction { /** * 持有一个实现部分的对象 */ protected Implementor impl; /** * 构造方法,传入实现部分的对象 * * @param impl 实现部分的对象 */ public Abstraction(Implementor impl) { this.impl = impl; } public void operation() { impl.operationImpl(); } } 该来看看具体的实现了,示例代码如下: 1 2 3 4 5 6 public class ConcreteImplementorA implements Implementor { public void operationImpl() { //真正的实现 } } 另外一个实现,示例代码如下: 1 2 3 4 5 6 public class ConcreteImplementorB implements Implementor { public void operationImpl() { //真正的实现 } } 最后来看看扩展Abstraction接口的对象实现,示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class RefinedAbstraction extends Abstraction { public RefinedAbstraction(Implementor impl) { super(impl); } /** * 示例操作,实现一定的功能 */ public void otherOperation() { //实现一定的功能,可能会使用具体实现部分的实现方法, //但是本方法更大的可能是使用Abstraction中定义的方法, //通过组合使用Abstraction中定义的方法来完成更多的功能 } } 使用桥接模式重写示例 学习了桥接模式的基础知识过后,该来使用桥接模式重写前面的示例了。通过示例,来看看使用桥接模式来实现同样的功能,是否能解决“既能方便的实现功能,又能有很好的扩展性”的问题。 要使用桥接模式来重新实现前面的示例,首要任务就是要把抽象部分和实现部分分离出来,分析要实现的功能,抽象部分就是各个消息的类型所对应的功能,而实现部分就是各种发送消息的方式。 其次要按照桥接模式的结构,给抽象部分和实现部分分别定义接口,然后分别实现它们就可以了。 从简单功能开始 从相对简单的功能开始,先实现普通消息和加急消息的功能,发送方式先实现站内短消息和Email这两种。使用桥接模式来实现这些功能的程序结构如图所示 还是看看代码实现,会更清楚一些。先看看消息发送器接口,示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /** * 消息发送器 * * @author HansChen */ public interface MessageSender { /** * 发送消息 * * @param message 要发送的消息内容 * @param toUser 消息发送的目的人员 */ void send(String message, String toUser); } 再看看抽象部分定义的接口,示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /** * 抽象的消息对象 * * @author HansChen */ public class AbstractMessageController { /** * 持有一个实现部分的对象 */ MessageSender impl; /** * 构造方法,传入实现部分的对象 * * @param impl 实现部分的对象 */ AbstractMessageController(MessageSender impl) { this.impl = impl; } /** * 发送消息,转调实现部分的方法 * * @param message 要发送的消息内容 * @param toUser 消息发送的目的人员 */ protected void sendMessage(String message, String toUser) { impl.send(message, toUser); } } 看看如何具体的实现发送消息,先看站内短消息的实现吧,示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 /** * 以站内短消息的方式发送消息 * * @author HansChen */ public class MessageSenderSMS implements MessageSender { @Override public void send(String message, String toUser) { System.out.println("使用站内短消息的方式,发送消息'" + message + "'给" + toUser); } } 再看看Email方式的实现,示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 /** * 以Email的方式发送消息 * * @author HansChen */ public class MessageSenderEmail implements MessageSender { @Override public void send(String message, String toUser) { System.out.println("使用Email的方式,发送消息'" + message + "'给" + toUser); } } 接下来该看看如何扩展抽象的消息接口了,先看普通消息的实现,示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 public class CommonMessageController extends AbstractMessageController { public CommonMessageController(MessageSender impl) { super(impl); } @Override public void sendMessage(String message, String toUser) { //对于普通消息,什么都不干,直接调父类的方法,把消息发送出去就可以了 super.sendMessage(message, toUser); } } 再看看加急消息的实现,示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class UrgencyMessageController extends AbstractMessageController { public UrgencyMessageController(MessageSender impl) { super(impl); } @Override protected void sendMessage(String message, String toUser) { message = "加急:" + message; super.sendMessage(message, toUser); } /** * 扩展自己的新功能:监控某消息的处理过程 * * @param messageId 被监控的消息的编号 * @return 包含监控到的数据对象,这里示意一下,所以用了Object */ public Object watch(String messageId) { //获取相应的数据,组织成监控的数据对象,然后返回 return null; } } 添加功能 看了上面的实现,发现使用桥接模式来实现也不是很困难啊,关键得看是否能解决前面提出的问题,那就来添加还未实现的功能看看,添加对特急消息的处理,同时添加一个使用手机发送消息的方式。该怎么实现呢? 很简单,只需要在抽象部分再添加一个特急消息的类,扩展抽象消息就可以把特急消息的处理功能加入到系统中了;对于添加手机发送消息的方式也很简单,在实现部分新增加一个实现类,实现用手机发送消息的方式,也就可以了。 这么简单?好像看起来完全没有了前面所提到的问题。的确如此,采用桥接模式来实现过后,抽象部分和实现部分分离开了,可以相互独立的变化,而不会相互影响。因此在抽象部分添加新的消息处理,对发送消息的实现部分是没有影响的;反过来增加发送消息的方式,对消息处理部分也是没有影响的。 接着看看代码实现,先看看新的特急消息的处理类,示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class SpecialUrgencyMessageController extends AbstractMessageController { public SpecialUrgencyMessageController(MessageSender impl) { super(impl); } @Override protected void sendMessage(String message, String toUser) { message = "特急:" + message; super.sendMessage(message, toUser); } public void hurry(String messageId) { //执行催促的业务,发出催促的信息 } } 再看看使用手机短消息的方式发送消息的实现,示例代码如下: 1 2 3 4 5 6 7 public class MessageSenderMobile implements MessageSender { @Override public void send(String message, String toUser) { System.out.println("使用手机的方式,发送消息'" + message + "'给" + toUser); } } 测试一下功能 看了上面的实现,可能会感觉得到,使用桥接模式来实现前面的示例过后,添加新的消息处理,或者是新的消息发送方式是如此简单,可是这样实现,好用吗?写个客户端来测试和体会一下,示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Client { public static void main(String[] args) { //创建具体的实现对象 MessageSender impl = new MessageSenderSMS(); //创建一个普通消息对象 AbstractMessageController controller = new CommonMessageController(impl); controller.sendMessage("请喝一杯茶", "小李"); //创建一个紧急消息对象 controller = new UrgencyMessageController(impl); controller.sendMessage("请喝一杯茶", "小李"); //创建一个特急消息对象 controller = new SpecialUrgencyMessageController(impl); controller.sendMessage("请喝一杯茶", "小李"); //把实现方式切换成手机短消息,然后再实现一遍 impl = new MessageSenderMobile(); controller = new CommonMessageController(impl); controller.sendMessage("请喝一杯茶", "小李"); controller = new UrgencyMessageController(impl); controller.sendMessage("请喝一杯茶", "小李"); controller = new SpecialUrgencyMessageController(impl); controller.sendMessage("请喝一杯茶", "小李"); } } 运行结果如下: 1 2 3 4 5 6 使用站内短消息的方式,发送消息'请喝一杯茶'给小李 使用站内短消息的方式,发送消息'加急:请喝一杯茶'给小李 使用站内短消息的方式,发送消息'特急:请喝一杯茶'给小李 使用手机的方式,发送消息'请喝一杯茶'给小李 使用手机的方式,发送消息'加急:请喝一杯茶'给小李 使用手机的方式,发送消息'特急:请喝一杯茶'给小李 前面三条是使用的站内短消息,后面三条是使用的手机短消息,正确的实现了预期的功能。看来前面的实现应该是正确的,能够完成功能,且能灵活扩展。 广义桥接-Java中无处不桥接 使用Java编写程序,一个很重要的原则就是“面向接口编程”,说得准确点应该是“面向抽象编程”,由于在Java开发中,更多的使用接口而非抽象类,因此通常就说成“面向接口编程”了。接口把具体的实现和使用接口的客户程序分离开来,从而使得具体的实现和使用接口的客户程序可以分别扩展,而不会相互影响。 桥接模式中的抽象部分持有具体实现部分的接口,最终目的是什么,还不是需要通过调用具体实现部分的接口中的方法,来完成一定的功能,这跟直接使用接口没有什么不同,只是表现形式有点不一样。再说,前面那个使用接口的客户程序也可以持有相应的接口对象,这样从形式上就一样了。 也就是说,从某个角度来讲,桥接模式不过就是对“面向抽象编程”这个设计原则的扩展。正是通过具体实现的接口,把抽象部分和具体的实现分离开来,抽象部分相当于是使用实现部分接口的客户程序,这样抽象部分和实现部分就松散耦合了,从而可以实现相互独立的变化。 这样一来,几乎可以把所有面向抽象编写的程序,都视作是桥接模式的体现,至少算是简化的桥接模式,就算是广义的桥接吧。而Java编程很强调“面向抽象编程”,因此,广义的桥接,在Java中可以说是无处不在。 桥接模式在Android中的应用 如果各位童鞋看到这里仍然对桥接模式还是不太清楚,在这里给大家举个在Android中非常常用的桥接模式栗子:AbsListView与ListAdapter之间的桥接模式。童鞋们可以根据这个栗子体会一下桥接模式的好处。

2017/3/1
articleCard.readMore

设计模式之代理模式

概述 我们执行一个功能的函数时,经常需要在其中写入与功能不是直接相关但很有必要的代码,如日志记录、信息发送、安全和事务支持等,这些枝节性代码虽然是必要的,但它会带来以下麻烦: 枝节性代码游离在功能性代码之外,它下是函数的目的 枝节性代码会造成功能性代码对其它类的依赖,加深类之间的耦合 枝节性代码带来的耦合度会造成功能性代码移植困难,可重用性降低 毫无疑问,枝节性代码和功能性代码需要分开来才能降低耦合程度,我们可以使用代理模式(委托模式)完成这个要求。代理模式的作用是:为其它对象提供一种代理以控制对这个对象的访问。在某些情况下,一 个客户不想直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介作用。 代理模式一般涉及到三个角色: 抽象角色:声明真实对象和代理对象的共同接口 代理角色:代理对象内部包含有真实角色的引用,从而可以操作真实角色,同时代理对象 与真实对象有相同的接口,能在任何时候代替真实对象,代理对象可以在执行真实对 象前后加入特定的逻辑以实现功能的扩展。 真实角色:代理角色所代表的真实对象,是我们最终要引用的对象 常见的代理应用场景有: 远程代理:对一个位于不同的地址空间对象提供一个局域代表对象,如RMI中的stub 虚拟代理:根据需要将一个资源消耗很大或者比较复杂的对象,延迟加载,在真正需要的时候才创建 保护代理:控制对一个对象的访问权限 智能引用:提供比目标对象额外的服务和功能 接下来,我们用代码来说明什么是代理模式 代理模式 UML图 先看看代理模式的结构图: 代码 下面给出一个小栗子说明代理模式,先定义一个抽象角色,也就是一个公共接口,声明一些需要代理的方法,本文定义一个Subject接口,为了简单说明,只是在里面定义一个request方法: 1 2 3 4 public interface Subject { void request(); } 定义Subject的实现类RealSubject,它是一个真实角色: 1 2 3 4 5 6 7 public class RealSubject implements Subject { @Override public void request() { System.out.print("do real request"); } } 定义一个代理角色ProxySubject,跟RealSubject一样,它也继承了Subject接口: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ProxySubject implements Subject { private RealSubject mSubject; public ProxySubject() { mSubject = new RealSubject(); } @Override public void request() { System.out.print("before"); mSubject.request(); System.out.print("after"); } } 客户端调用代码 1 2 3 4 5 6 7 8 public class Client { public static void main(String[] args) { Subject subject = new ProxySubject(); subject.request(); } } 这样,一个简易的代理模式模型就建立了,客户端在使用过程中,无需关注RealSubject,只需要关注ProxySubject就行了,并且可以在ProxySubject中插入一些非功能信的代码,比如输出Log,统计执行时间等等 远程代理 远程代理,对一个位于不同的地址空间对象提供一个局域代表对象。这样说大家可能比较抽象,不太能理解,但其实童鞋们可能在就接触过了,在Android中,Binder的使用就是典型的远程代理。比如ActivityManager: 在启动Activity的时,会调用ActivityManager的startActivity方法,我们看看Activity是怎么获取的: 1 2 3 4 5 6 7 8 9 10 11 12 static public IActivityManager asInterface(IBinder obj) { if (obj == null) { return null; } IActivityManager in = (IActivityManager)obj.queryLocalInterface(descriptor); if (in != null) { return in; } // 返回代理类 return new ActivityManagerProxy(obj); } 可以看到,最终是返回了一个ActivityManager的代理类,因为真正的ActivityManager是运行在内核空间的,Android应用无法直接访问得到,那么就可以借助这个ActivityManagerProxy,通过Binder与真正的ActivityManager,也就是ActivityManagerService交互。其中ActivityManagerService和ActivityManagerProxy都实现了同一个接口:IActivityManager。这个就是Android中典型的代理模式的栗子了。至于ActivityManagerService和ActivityManagerProxy是如何通过Binder实现远程调用,这个就是另一个话题Binder的内容了,这里不再做阐述 延迟加载 根据需要将一个资源消耗很大或者比较复杂的对象,延迟加载,在真正需要的时候才创建。假设我们创建RealSubject需要耗费一定的资源,那么,我们可以把创建它延迟到实际调用的时候,优化Client初始化速度,比如,这样修改ProxySubject以达到延迟加载: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class ProxySubject implements Subject { private RealSubject mSubject; public ProxySubject() { } @Override public void request() { // 延时加载 if (mSubject == null) { mSubject = new RealSubject(); } mSubject.request(); } } Client在实例化ProxySubject的时候,不需消耗资源,而是等到真正调用request的时候,才会加载RealSubject,达到延时加载的效果 保护代理 可以在Proxy类中加入进行权限,验证是否具有执行真实代码的权限,只有权限验证通过了才进行真实对象的调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class ProxySubject implements Subject { private RealSubject mSubject; private User mUser; public ProxySubject(User user) { this.mUser = user; } @Override public void request() { // 验证权限 if (mUser.isLogin()) { mSubject.request(); } } } 额外功能 通过引入代理类,可以方便地在功能性代码前后插入扩展,如Log输出,调用统计等,实现对原代码的无侵入式代码扩展,如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ProxySubject implements Subject { private RealSubject mSubject; public ProxySubject() { mSubject = new RealSubject(); } @Override public void request() { System.out.print("Log: before"); mSubject.request(); System.out.print("Log: after"); } } 静态代理和动态代理 静态代理和动态代理的概念和使用可以参考我另一篇文章:Java动态代理:http://blog.csdn.net/shensky711/article/details/52872249

2016/12/27
articleCard.readMore

依赖注入利器 - Dagger ‡

概述 在开发过程中,为了实现解耦,我们经常使用依赖注入,常见的依赖注入方式有: 构造方法注入:在构造方法中把依赖作为参数传递进去 setter方法注入:添加setter方法,把依赖传递进去 接口注入:把注入方法抽到一个接口中,然后实现该接口,把依赖传递进去 下面用一个小栗子来说明三种方式的用法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class PersonService implements DependencyInjecter { private PersonDao personDao; // 构造方法注入 public PersonService(PersonDao personDao) { this.personDao = personDao; } // setter方法注入 public void setPersonDao(PersonDao personDao) { this.personDao = personDao; } // 接口注入:实现DependencyInjecter接口 @Override public void injectPersonDao(PersonDao personDao) { this.personDao = personDao; } ... ... } 我们来看下使用一般的依赖注入方法时,代码会是怎么样的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class MainActivity extends AppCompatActivity { private PersonService mService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 创建PersonService的依赖:personDao PersonDao personDao = new PersonDaoImpl(); // 通过构造方法注入依赖 mService = new PersonService(personDao); } } 看起来还好是吧?但现实情况下,依赖情况往往是比较复杂的,比如很可能我们的依赖关系如下图: PersonDaoImpl依赖类A,类A依赖B,B依赖C和D…在这种情况下,我们就要写出下面这样的代码了: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class MainActivity extends AppCompatActivity { private PersonService mService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 创建依赖D D d = new D(); // 创建依赖C C c = new C(); // 创建依赖B B b = new B(c, d); // 创建依赖A A a = new A(b); // 创建PersonService的依赖:personDao PersonDao personDao = new PersonDaoImpl(a); // 通过构造方法注入依赖 mService = new PersonService(personDao); } } MainActivity只是想使用PersonService而已,却不得不关注PersonService的依赖是什么、PersonDaoImpl依赖的依赖是什么,需要把整个依赖关系搞清楚才能使用PersonService。而且还有一个不好的地方,一旦依赖关系变更了,比如A不再依赖B了,那么就得修改所有创建A的地方。那么,有没有更好的方式呢?Dagger就是为此而生的,让我们看看使用Dagger后,MainActivity会变成什么模样: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class MainActivity extends AppCompatActivity { @Inject PersonService mService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Dagger注入,读者现在可先不关注里面做了什么操作 DaggerPersonServiceComponent.create().inject(MainActivity.this); // 注意,mService已经是非空了,可以正常使用 mService.update(1, "HansChen"); ...... } } 之前创建A、B、C、D、PersonDaoImpl等依赖的代码全不见了,只需要调用一个注入语句就全搞定了。调用了注入语句之后,mService就可以正常使用了,是不是挺方便呢?至于这句注入语句具体干了什么,读者现在可以先不管,后面会有详细说明,这里只是做一个使用演示而已。 我们大概猜想一下,在MainActivity使用PersonService需要做哪些? 分析生成依赖关系图,如PersonService–>PersonDaoImpl–>A–>B–>C&D 根据依赖关系图获取相关依赖,比如依次创建D、C、B、A、PersonDaoImpl、PersonService的实例 把生成的PersonService实例传递给MainActivity的mService成员变量 其实Dagger做的也就是上面这些事情了,接下来就让我们真正开始学习Dagger吧 声明需要注入的对象 首先我们应该用javax.inject.Inject去注解需要被自动注入的对象,@Inject是Java标准的依赖注入(JSR-330)注解。比如下面栗子中,需要注入的对象就是MainActivity的mService。这里有个要注意的地方,被@Inject注解的变量不能用private修饰 1 2 3 4 5 6 7 public class MainActivity extends AppCompatActivity { // 注意,不能被private修饰 @Inject PersonService mService; ...... } 如何实例化出依赖? 在执行依赖注入的时候,Dagger会查找@Inject注解的成员变量,并尝试获取该类的实例,Dagger最直接的方式就是直接new出相应的对象了。实例化对象的时候,会调用对象的构造方法,但假如有多个构造方法,具体用哪个构造方法来实例化对象?Dagger肯定是不会帮我们“擅自做主”的,用哪个构造方法来实例化对象应该是由我们做主的,所以我们需要给相应的构造方法添加@Inject注解。 当Dagger需要实例化该对象的时候,会调用@Inject注解的构造方法来实例化对象: 1 2 3 4 5 6 7 8 9 10 11 12 public class PersonService implements DependencyInjecter { private PersonDao personDao; // 用@Inject注解,相当于告诉Dagger需要实例化PersonService的时候,请调用这个构造方法 @Inject public PersonService(PersonDao personDao) { this.personDao = personDao; } ...... } 聪明的你应该发现了,调用PersonService的构造方法需要传入PersonDao实例,所以要实例化PersonService,必须先要实例化PersonDao,Dagger会帮我们自动分析出这个依赖关系,并把它添加到依赖关系图里面!Dagger会尝试先去实例化一个PersonDao,如果PersonDao又依赖于另外一个对象A,那么就先尝试去实例化A……以此类推,是不是很像递归?当所有依赖都被实例化出来之后,我们的PersonService当然也被构造出来了。 问题又来了,如果PersonDao是一个接口呢?Dagger怎么知道这个接口应该怎么实现?答案是不知道的,那么Dagger怎么实例化出一个接口出来?这个就是Module存在的意义之一了。关于Module的讲解我们会在后面详细说明,我们现在只要知道,Module里面会定义一些方法,这些方法会返回我们的依赖,就像: 1 2 3 4 5 6 7 8 9 10 11 @Module public class PersonServiceModule { /** * 提供PersonDao接口实例 */ @Provides PersonDao providePersonDao(A a) { return new PersonDaoImpl(a); } } Dagger根据需求获取一个实例的时候,并不总是通过new出来的,它会优先查找Module 中是否有返回相应实例的方法,如果有,就调用Module的方法来获取实例。 比如你用@Inject注解了一个成员变量,Dagger会查找Module中是否有用@Provides注解的,返回该类实例的方法,有的话就会调用provide方法来获得实例,然后注入,如果没有的话Dagger就会尝试new出一个实例。就像我们现在这个栗子,PersonService依赖于PersonDao接口,Dagger不能直接为我们new出一个接口,但我们可以提供一个Module,在Module中定义一个返回PersonDao接口实例的方法,这样,Dagger就可以解决实例化PersonDao的问题了。 我们再梳理一下流程,如果我们用@Inject注解了一个成员变量,并调用注入代码之后,Dagger会这样处理: 查找Module中是否有用@Provides注解的,返回该类实例的方法 如果有,就调用那个provide方法来获得实例,然后注入 如果没有,就尝试调用相应的类中被@Inject注解的构造方法new出一个实例,然后注入 如果没有一个构造方法被@Inject注解,Dagger会因不能满足依赖而出错 所以假如一个变量被@Inject注解,要么在Module中提供provide方法获取实例,要么该类提供一个被@Inject注解的构造方法,否则Dagger会出错 Module的使用 一般而言,Dagger会获取所有依赖的实例,比如当需要一个TestBean的时候,会通过new TestBean()创建实例并注入到类中。但是,以下情况会就不好处理了: 需要生成的是一个接口,而Dagger不能直接实例化接口 不能在第三方库的类中添加注解 可配置的对象必须是配置的 为了解决以上问题,我们需要定义一个被@Module注解的类,在里面定义用@Provides注解的方法。用该方法返回所需的实例。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Module public class PersonServiceModule { @Provides D provideD() { return new D(); } @Provides C provideC() { return new C(); } @Provides B provideB(C c, D d) { return new B(c, d); } @Provides A provideA(B b) { return new A(b); } /** * 提供PersonDao实例 */ @Provides PersonDao providePersonDao(A a) { return new PersonDaoImpl(a); } } 就像providePersonDao返回了PersonDao接口实例,Dagger虽然不能直接实例化出PersonDao接口,但却可以调用Module的providePersonDao方法来获得一个实例。providePersonDao方法需要传入A的实例,那么这里也构成了一个依赖关系图。Dagger会先获取A的实例,然后把实例传递给providePersonDao方法。 Component的使用 到目前为止,我们虽然知道了: Dagger怎么获取实例: 从Module的provide方法中获取 通过@Inject注解的构造方法new出新的实例 Dagger会推导provide方法和构造方法的参数,形成依赖图,并“满足”我们依赖图的需求,获取依赖的实例 看样子需要注入的依赖可以获取了,但是不是总觉得还有点“零碎”,整个流程还没连贯起来?比如,Module既然是一个类,生成依赖图的时候,怎么知道跟哪个Module挂钩?即使最后生成了需要的实例,注入的“目的地”是哪里?怎么才能把它注入到“目的地”?残缺的这部分功能,正是Component提供的,Component起到了一个桥梁的作用,贯通Module和注入目标。我们来看看最开始那个例子,我们是怎么进行依赖注入的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class MainActivity extends AppCompatActivity { @Inject PersonService mService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); PersonServiceComponent component = DaggerPersonServiceComponent.builder() .personServiceModule(new PersonServiceModule()) .build(); // 注入,所有@Inject注解的成员变量都会同时注入 component.inject(MainActivity.this); // 通过component获取实例,注意,这里只是演示用法,其实mService在component.inject的时候已经完成了注入 mService = component.getPersonService(); } } 这个DaggerPersonServiceComponent是什么鬼?DaggerPersonServiceComponent其实是Dagger为我们自动生成的类,它实现了一个Component接口(这个接口是需要我们自己写的),我们来看下它实现的接口长什么样子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 /** * 指定PersonServiceModule,当需要获取某实例的时候,会查找PersonServiceModule中是否有返回相应类型的方法,有的话就通过该方法获得实例 * * @author HansChen */ @Component(modules = PersonServiceModule.class) public interface PersonServiceComponent { /** * 查找activity中被@Inject注解的成员变量,并尝试获取相应的实例,把实例赋给activity的成员变量 * 注意函数格式:返回值为空、带有一个参数 */ void inject(MainActivity activity); /** * Dagger会尝试从Module中获取PersonService实例,如果Module中不能获取对应实例,则通过PersonService的构造方法new出一个实例 * 注意函数格式:参数为空,返回值非空 */ PersonService getPersonService(); } 这个接口被Component注解修饰,它里面可以定义3种类型的方法: 返回值为空,有一个参数:查找参数中被@Inject注解的成员变量,并尝试获取相应的实例(通过Module的provide方法或@Inject注解的构造方法new出新的实例),把实例赋给参数的成员变量 返回值非空,参数为空:获取相应实例并返回 返回值是Component,参数是Moduld,通过该方法可以创建SubComponent实例 既然获取实例的时候,有可能用到Module,那么就必须为这个Component指定使用的Module是什么。具体做法就是在@Component注解中指定modules。 定义好Component之后,Dagger会自动帮我们生成实现类,这就是Dagger强大的地方!生成的类名格式是:Dagger+Component名。 Component提供了2种方法,一个是注入式方法,一个是获取实例方法。具体用什么方法,就看个人需求了。一个Component其实也对应了一个依赖图,因为Component使用哪个Module是确定不变的,依赖关系无非也就是跟Module和类的定义有关。一旦这些都确定下来了,在这个Component范围内,依赖关系也就被确定下来了。额外再说一点,在Dagger1中,Component的功能是由ObjectGraph实现的,Component是用来代替它的。 Component定义好之后,build一下工程,Dagger就会自动为我们生成实现类了,就可以使用自动生成的实现类来进行依赖注入了。到现在为止,我们已经通过Dagger完成了依赖注入。可能看起来比正常方法麻烦得多,但是Dagger框架可以让依赖的注入和配置独立于组件之外,它帮助你专注在那些重要的功能类上。通过声明依赖关系和指定规则构建整个应用程序。 熟悉完Dagger基本的使用之后,接下来我们来讲解一些稍微高级一点的用法: Dagger的进阶使用 Components之间的关系 在Dagger中,Component之间可以有两种关系:Subcomponents和Component dependencies。他们有什么作用呢?比如在我们应用中,经常会有一些依赖我们在各个界面都使用得到,比如操作数据库、比如网络请求。假设我们有个ServerApi的接口,在页面A、B、C都使用到了,那么我们要在页面A、B、C的Component里面都能获取到ServerApi的实例,但显然,获取ServerApi实例的方法都是一样的,我们不想写重复的代码。于是我们可定义一个ApplicationComponent,在里面返回ServerApi实例,通过Component之间的关系便可以共享ApplicationComponent提供的依赖图。 下面通过Android中的一个小栗子来说明Subcomponents和Component dependencies如何使用 dependencies 先说明下各个模块之间的关系 首先,我们定义一个ApplicationComponent,它定义了一个方法,通过它来获得ServerApi实例。ApplicationComponent还关联了ApplicationModule,这个Module是ServerApi实例的提供者,注意,这个Moduld还可以返回Context实例 1 2 3 4 5 @Component(modules = ApplicationModule.class) public interface ApplicationComponent { ServerApi getServerApi(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Module public class ApplicationModule { private final Context mAppContext; ApplicationModule(Context context) { mAppContext = context.getApplicationContext(); } @Provides Context provideAppContext() { return mAppContext; } @Provides ServerApi provideServerApi(Context context) { return new ServerApiImpl(context); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class DemoApplication extends Application { private ApplicationComponent mAppComponent; @Override public void onCreate() { super.onCreate(); mAppComponent = DaggerApplicationComponent.builder().applicationModule(new ApplicationModule(this)).build(); } public ApplicationComponent getAppComponent() { return mAppComponent; } } MainActivity使用MVP模式,在MainPresenter里面需要传入一个ServerApi对象 1 2 3 4 5 6 // 注意,这里有个dependencies声明 @Component(dependencies = ApplicationComponent.class, modules = MainPresenterModule.class) public interface MainPresenterComponent { MainPresenter getMainPresenter(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Module public class MainPresenterModule { private MainView mMainView; public MainPresenterModule(MainView mainView) { this.mMainView = mainView; } @Provides MainView provideMainView() { return mMainView; } } 1 2 3 4 5 6 7 8 9 10 11 public class MainPresenter { private MainView mMainView; private ServerApi mServerApi; @Inject public MainPresenter(MainView mainView, ServerApi serverApi) { this.mMainView = mainView; this.mServerApi = serverApi; } } 先抛开dependencies,我们分析这个这个依赖树是怎么样的 Component中getMainPresenter的目的很简单,就是返回MainPresenter,而MainPresenter又依赖MainView和ServerApi,MainView还好说,在MainPresenterModule中有provide方法,但是ServerApi呢?就像上面说的那样,如果我们在这个Moduld中也添加相应的provide方法,那真是太麻烦了(当然,这样做完全是可以实现的),所以我们依赖了ApplicationComponent,通过dependencies,在被依赖的Component暴露的对象,在子Component中是可见的。这个是什么意思呢?意思有两个: 被依赖Component接口暴露的对象,可以添加到依赖者的依赖图中 Component接口没有暴露的对象,依赖者是不可见的 对于第一点应该比较好理解,就像这个栗子,MainPresenterComponent生成MainPresenter需要ServerApi,而ApplicationComponent中有接口暴露了ServerApi,所以MainPresenterComponent可以获得ServerApi 对于第二点,假设MainPresenter还需要传入一个Context对象,我们注意到,ApplicationModule是可以提供Context的,那MainPresenterComponent能不能通过ApplicationComponent获取Context实例?答案是不行的,因为ApplicationComponent没有暴露这个对象。想要获取Context,除非ApplicationComponent中再添加一个getContext的方法。 他们之间的关系可以用下图描述: Subcomponents Subcomponents 实现方法一: 先定义子 Component,使 用@Subcomponent 标注(不可同时再使用 @Component) 父 Component 中定义获得子 Component 的方法 让我们对上面的栗子改造改造: 去除MainPresenterComponent的Component注解,改为Subcomponent: 1 2 3 4 5 6 7 @Subcomponent(modules = MainPresenterModule.class) public interface MainPresenterComponent { void inject(MainActivity activity); MainPresenter getMainPresenter(); } 在ApplicationComponent中新增plus方法(名字可随意取),返回值为MainPresenterComponent,参数为MainPresenterModule: 1 2 3 4 5 @Component(modules = ApplicationModule.class) public interface ApplicationComponent { MainPresenterComponent plus(MainPresenterModule module); } 这样,就构建了一个ApplicationComponent的子图:MainPresenterComponent。子图和dependencies的区别就是,子图可以范围父图所有的依赖,也就是说,子图需要的依赖,不再需要在父Component中暴露任何对象,可以直接通过父图的Moduld提供!他们的关系变为了: 这里需要注意的是,以上代码直接在父 Component 返回子 Component 的形式,要求子 Component 依赖的 Module 必须包含一个无参构造函数,用以自动实例化。如果 Module 需要传递参数,则需要使用 @Subcomponent.builder 的方式,实现方法二实现步骤如下: 在子 Component,定义一个接口或抽象类(通常定义为 Builder),使用 @Subcomponent.Builder 标注 编写返回值为 Builder,方法的参数为需要传入参数的 Module 编写返回值为当前子 Component的 无参方法 父 Component 中定义获得子 Component.Builder 的方法 代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Module public class TestModule { public TestModule(String test) { } @Provides AuthManager provideAuthManager() { return AuthManager.getInstance(); } } @Subcomponent(modules = {TestModule.class}) public interface TestComponent { AuthManager getAuthManager(); @Subcomponent.Builder interface Builder { Builder createBuilder(TestModule module); TestComponent build(); } } @Singleton @Component(modules = ApplicationModule.class) public interface ApplicationComponent { ... TestComponent.Builder testComponentBuilder(); } // 使用 TestComponent testComponent = mApplicationComponent.testComponentBuilder().createBuilder(new TestModule("test")).build(); Binds注解 在Dagger2中,一般都是使用@provide方法注入接口。在Android 中,一般我们会这样做,创建一个接口 Presenter 命名 为 HomePresenter 1 2 3 public interface HomePresenter { Observable<List<User>> loadUsers() } 然后创建一个这个接口的实例:HomePresenterImp 1 2 3 4 5 6 7 8 public class HomePresenterImp implements HomePresenter { public HomePresenterImp(){ } @Override public Observable<List<User>> loadUsers(){ //Return user list observable } } 然后在 Module 中,提供实例化的 provide 方法: 1 2 3 4 5 6 7 @Module public class HomeModule { @Provides public HomePresenter providesHomePresenter(){ return new HomePresenterImp(); } } 但是,如果我们需要添加一个依赖到 presenter 叫 UserService,那就意味着,我们也要在 module 中添加一个 provide 方法提供这个 UserService,然后在 HomePresenterImp 类中加入一个 UserService 参数的构造方法。 有没有觉得这种方法很麻烦呢?我们还可以用 @Binds 注解,如: 1 2 3 4 5 6 7 @Module public abstract class HomeModule { // 变为 abstract 方法, 同时 Module 也必须声明为 abstract, 传入的参数必须为返回参数的实现类 // 当需要 HomePresenter 时,dagger 会自动实例化 HomePresenterImp 并返回 @Binds public abstract HomePresenter bindHomePresenter(HomePresenterImp homePresenterImp); } 除了方便,使用 @Binds 注解还可以让 dagger2 生成的代码效率更高。但是需要注意的是,由于 Module 变为抽象类,Module 不能再包含非 static 的带 @Provides 注解的方法。而且这时候,依赖此 Module 的 Component 也不需要传入此 Module 实例了(也实例化不了,因为它是抽象的)。相当于此 Module 仅仅作为描述依赖关系的一个类 Scopes Scopes可是非常的有用,Dagger2可以通过自定义注解限定注解作用域。@Singleton是被Dagger预先定义的作用域注解。 没有指定作用域的@Provides方法将会在每次注入的时候都创建新的对象 一个没有scope的component不可以依赖一个有scope的组件component 子组件和父组件的scope不能相同 Module中provide方法的scope需要与Component的scope一致 我们通常的ApplicationComponent都会使用Singleton注解,也就会是说我们如果自定义component必须有自己的scope。读者到这里,可能还不能理解Scopes的作用,我们先来看下默认提供的Singlton到底有什么作用,然后再讨论Scopes的意义: Singlton Singletons是java提供的一个scope,我们来看看Singletons能做什么事情。 为@Provides注释的方法或可注入的类添加添加注解@Singlton,构建的这个对象图表将使用唯一的对象实例,比如我们有个ServerApi 方法一:用@Singleton注解类: 1 2 3 4 5 6 7 8 9 10 11 @Singleton public class ServerApi { @Inject public ServerApi() { } public boolean login(String username, String password) { return "HansChen".equals(username) && "123456".equals(password); } } 方法二:用@Singleton注解Module的provide方法: 1 2 3 4 5 6 7 8 9 @Module public class ApplicationModule { @Singleton @Provides ServerApi provideServerApi() { return new ServerApi(); } } 然后我们有个Component: 1 2 3 4 5 6 @Singleton @Component(modules = ApplicationModule.class) public interface ApplicationComponent { ServerApi getServerApi(); } 然后执行依赖注入: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class MainActivity extends AppCompatActivity { @Inject ServerApi mService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ApplicationComponent component = DaggerApplicationComponent.create(); Log.d("Hans", component.getServerApi().toString()); Log.d("Hans", component.getServerApi().toString()); Log.d("Hans", component.getServerApi().toString()); } } 使用了以上两种方法的任意一种,我们都会发现,通过component.getServerApi()获得的实例都是同一个实例。不过要注意一点的是,如果类用@Singleton注解了,但Module中又存在一个provide方法是提供该类实例的,但provide方法没有用@Singleton注解,那么Component中获取该实例就不是单例的,因为会优先查找Module的方法。 这个单例是相对于同一个Component而言的,不同的Component获取到的实例将会是不一样的。 自定义Scope 既然一个没有scope的component不可以依赖一个有scope的组件component,那么我们必然需要自定义scope来去注解自己的Component了,定义方法如下: 1 2 3 4 5 @Documented @Scope @Retention(RetentionPolicy.RUNTIME) public @interface FragmentScoped { } 定义出来的FragmentScoped在使用上和Singleton是一样的,那它和Singleton除了是不一样的注解之外,还有什么不一样呢?答案是没有!我们自定义的scope和Singleton并没有任何不一样,不会因为Singleton是java自带的注解就会有什么区别。 那么,这个scope的设定是为了什么呢? scope的作用 scope除了修饰provide方法可以让我们获得在同一个Component实例范围内的单例之外,主要的作用就是对Component和Moduld的分层管理以及依赖逻辑的可读性。 这里借用一个网络上的图片说明: ApplicationComponent一般会用singleton注解,相对的,它的Module中provide方法也只能用singleton注解。UserComponent是用UserSCope能直接使用ApplicationModule吗?不能!因为他俩的scope不一致,这就是这个设定带来的好处,防止不同层级的组件混乱。另外,因为有了scope的存在,各种组件的作用和生命周期也变得可读起来了 Lazy注入 有时可能会需要延迟获取一个实例。对任何绑定的 T,可以构建一个 Lazy 来延迟实例化直至第一次调用 Lazy 的 get() 方法。注入之后,第一次get的时会实例化出 T,之后的调用都会获取相同的实例。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class MainActivity extends AppCompatActivity implements MainView { // 懒加载 @Inject Lazy<MainPresenter> mPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MainPresenterComponent component = DaggerMainPresenterComponent.builder() .mainPresenterModule(new MainPresenterModule(this)) .applicationComponent(((DemoApplication) getApplication()).getAppComponent()) .build(); component.inject(this); Log.d("Hans", mPresenter.get().toString()); // 实例化MainPresenter Log.d("Hans", mPresenter.get().toString()); // 跟上次获取的实例是同一个实例 } } Provider注入 跟Lazy注入不一样的是,有时候我们希望每次调用get的时候,获取到的实例都是不一样的,这时候可以用Provider注入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class MainActivity extends AppCompatActivity implements MainView { // Provider @Inject Provider<MainPresenter> mPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MainPresenterComponent component = DaggerMainPresenterComponent.builder() .mainPresenterModule(new MainPresenterModule(this)) .applicationComponent(((DemoApplication) getApplication()).getAppComponent()) .build(); component.inject(this); Log.d("Hans", mPresenter.get().toString()); // 实例化MainPresenter Log.d("Hans", mPresenter.get().toString()); // 获取新的MainPresenter实例 } } Qualifiers注入 到目前为止,我们的demo里,Moduld的provide返回的对象都是不一样的,但是下面这种情况就不好处理了: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Module public class ApplicationModule { ...... // 返回ServerApi实例 @Provides ServerApi provideServerApiA(Context context) { return new ServerApiImplA(context); } // 返回ServerApi实例 @Provides ServerApi provideServerApiB(Context context) { return new ServerApiImplB(context); } } provideServerApiA和provideServerApiB返回的都是ServerApi,Dagger是无法判断用哪个provide方法的。这时候就需要添加Qualifiers了: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Module public class ApplicationModule { ...... @Provides @Named("ServerApiImplA") ServerApi provideServerApiA(Context context) { return new ServerApiImplA(context); } @Provides @Named("ServerApiImplB") ServerApi provideServerApiB(Context context) { return new ServerApiImplB(context); } } 通过这样一个限定,就能区分出2个方法的区别了,当然,在使用过程中,也同样要指明你用哪个name的实例,Dagger会根据你的name来选取对应的provide方法: 1 2 3 4 5 6 7 8 9 10 11 public class MainPresenter { private MainView mMainView; private ServerApi mServerApi; @Inject public MainPresenter(MainView mainView, @Named("ServerApiImplA") ServerApi serverApi) { this.mMainView = mainView; this.mServerApi = serverApi; } } 除了用Named注解,你也可以创建你自己的限定注解: 1 2 3 4 5 6 @Qualifier @Documented @Retention(RUNTIME) public @interface YourQualifier { String value() default ""; } 编译时验证 Dagger 包含了一个注解处理器(annotation processor)来验证模块和注入。这个过程很严格而且会抛出错误,当有非法绑定或绑定不成功时。下面这个例子缺少了 Executor: 1 2 3 4 5 6 @Module class DripCoffeeModule { @Provides Heater provideHeater(Executor executor) { return new CpuHeater(executor); } } 当编译时,javac 会拒绝绑定缺少的部分: 1 2 [ERROR] COMPILATION ERROR : [ERROR] error: java.util.concurrent.Executor cannot be provided without an @Provides-annotated method. 可以通过给方法 Executor 添加@Provides注解来解决这个问题,或者标记这个模块是不完整的。不完整的模块允许缺少依赖关系 1 2 3 4 5 6 @Module(complete = false) class DripCoffeeModule { @Provides Heater provideHeater(Executor executor) { return new CpuHeater(executor); } } 小结 第一次接触用Dagger框架写的代码时候,如果不了解各种注解作用的时候,那真会有一脸懵逼的感觉,而且单看文章,其实还是很抽象,建议大家用Dagger写个小demo玩玩,很快就上手了,这里提供几个使用Dagger的栗子,希望可以帮助大家上手Dagger Dagger demo 谷歌官方 MVP+Dagger2 Demo

2016/12/18
articleCard.readMore

Robolectric使用教程

概述 Android的单元测试可以分为两部分: Local unit tests:运行于本地JVM Instrumented test:运行于真机或者模拟器 如果使用Local测试,需要保证测试过程中不会调用Android系统API,否则会抛出RuntimeException异常,因为Local测试是直接跑在本机JVM的,而之所以我们能使用Android系统API,是因为编译的时候,我们依赖了一个名为“android.jar”的jar包,但是jar包里所有方法都是直接抛出了一个RuntimeException,是没有任何任何实现的,这只是Android为了我们能通过编译提供的一个Stub!当APP运行在真实的Android系统的时候,由于类加载机制,会加载位于framework的具有真正实现的类。由于我们的Local是直接在PC上运行的,所以调用这些系统API便会出错。 那么问题来了,我们既要使用Local测试,但测试过程又难免遇到调用系统API那怎么办?其中一个方法就是mock objects,比如借助Mockito,另外一种方式就是使用Robolectric, Robolectric就是为解决这个问题而生的。它实现一套JVM能运行的Android代码,然后在unit test运行的时候去截取android相关的代码调用,然后转到他们的他们实现的Shadow代码去执行这个调用的过程 如何使用? 为项目添加依赖 1 testCompile "org.robolectric:robolectric:3.1.4" Robolectric在第一次运行时,会下载一些sdk依赖包,每个sdk依赖包大概50M,下载速度比较慢,用户可以直接在网上下载相应依赖包,放置在本地maven仓库地址中,默认路径为:C:\Users\username\.m2\repository\org\robolectric 指定RobolectricTestRunner为运行器 为测试用例添加注解,指定测试运行器为RobolectricTestRunner。注意,这里要通过Config指定constants = BuildConfig.class,Robolectric 会通过constants推导出输出路径,如果不进行配置,Robolectric可能不能找到你的manifest、resources和assets资源 1 2 3 4 5 @RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class) public class MainActivityTest { } 什么是Shadow类 Shadow是Robolectric的立足之本,如其名,作为影子,一定是变幻莫测,时有时无,且依存于本尊。Robolectric定义了大量模拟Android系统类行为的Shadow类,当这些系统类被创建的时候,Robolectric会查找对应的Shadow类并创建一个Shadow类与原始类关联。每当系统类的方法被调用的时候,Robolectric会保证Shadow对应的方法会调用。这些Shadow对象,丰富了本尊的行为,能更方便的对Android相关的对象进行测试。 比如,我们可以借助ShadowActivity验证页面是否正确跳转了 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /** * 验证点击事件是否触发了页面跳转,验证目标页面是否预期页面 * * @throws Exception */ @Test public void testJump() throws Exception { // 默认会调用Activity的生命周期: onCreate->onStart->onResume MainActivity activity = Robolectric.setupActivity(MainActivity.class); // 触发按钮点击 activity.findViewById(R.id.activity_main_jump).performClick(); // 获取对应的Shadow类 ShadowActivity shadowActivity = Shadows.shadowOf(activity); // 借助Shadow类获取启动下一Activity的Intent Intent nextIntent = shadowActivity.getNextStartedActivity(); // 校验Intent的正确性 assertEquals(nextIntent.getComponent().getClassName(), SecondActivity.class.getName()); } @Config配置 可以通过@Config定制Robolectric的运行时的行为。这个注解可以用来注释类和方法,如果类和方法同时使用了@Config,那么方法的设置会覆盖类的设置。你可以创建一个基类,用@Config配置测试参数,这样,其他测试用例就可以共享这个配置了 配置SDK版本 Robolectric会根据manifest文件配置的targetSdkVersion选择运行测试代码的SDK版本,如果你想指定sdk来运行测试用例,可以通过下面的方式配置 1 2 3 4 5 6 7 @Config(sdk = Build.VERSION_CODES.JELLY_BEAN) public class SandwichTest { @Config(sdk = Build.VERSION_CODES.KITKAT) public void getSandwich_shouldReturnHamSandwich() { } } 配置Application类 Robolectric会根据manifest文件配置的Application配置去实例化一个Application类,如果你想在测试用例中重新指定,可以通过下面的方式配置 1 2 3 4 5 6 7 @Config(application = CustomApplication.class) public class SandwichTest { @Config(application = CustomApplicationOverride.class) public void getSandwich_shouldReturnHamSandwich() { } } 指定Resource路径 Robolectric可以让你配置manifest、resource和assets路径,可以通过下面的方式配置 1 2 3 4 5 6 7 8 9 @Config(manifest = "some/build/path/AndroidManifest.xml", assetDir = "some/build/path/assetDir", resourceDir = "some/build/path/resourceDir") public class SandwichTest { @Config(manifest = "other/build/path/AndroidManifest.xml") public void getSandwich_shouldReturnHamSandwich() { } } 使用第三方Library Resources 当Robolectric测试的时候,会尝试加载所有应用提供的资源,但如果你需要使用第三方库中提供的资源文件,你可能需要做一些特别的配置。不过如果你使用gradle来构建Android应用,这些配置就不需要做了,因为Gradle Plugin会在build的时候自动合并第三方库的资源,但如果你使用的是Maven,那么你需要配置libraries变量: 1 2 3 4 5 6 7 @RunWith(RobolectricTestRunner.class) @Config(libraries = { "build/unpacked-libraries/library1", "build/unpacked-libraries/library2" }) public class SandwichTest { } 使用限定的资源文件 Android会在运行时加载特定的资源文件,如根据设备屏幕加载不同分辨率的图片资源、根据系统语言加载不同的string.xml,在Robolectric测试当中,你也可以进行一个限定,让测试程序加载特定资源.多个限定条件可以用破折号拼接在在一起。 1 2 3 4 5 6 7 8 9 10 11 /** * 使用qualifiers加载对应的资源文件 * * @throws Exception */ @Config(qualifiers = "zh-rCN") @Test public void testString() throws Exception { final Context context = RuntimeEnvironment.application; assertThat(context.getString(R.string.app_name), is("单元测试Demo")); } Properties文件 如果你嫌通过注解配置上面的东西麻烦,你也可以把以上配置放在一个Properties文件之中,然后通过@Config指定配置文件,比如,首先创建一个配置文件robolectric.properties: 1 2 3 4 5 # 放置Robolectric的配置选项: sdk=21 manifest=some/build/path/AndroidManifest.xml assetDir=some/build/path/assetDir resourceDir=some/build/path/resourceDir 然后把robolectric.properties文件放到src/test/resources目录下,运行的时候,会自动加载里面的配置 系统属性配置 robolectric.offline:true代表关闭运行时获取jar包 robolectric.dependency.dir:当处于offline模式的时候,指定运行时的依赖目录 robolectric.dependency.repo.id:设置运行时获取依赖的Maven仓库ID,默认是sonatype robolectric.dependency.repo.url:设置运行时依赖的Maven仓库地址,默认是https://oss.sonatype.org/content/groups/public/ robolectric.logging.enabled:设置是否打开调试开关 以上设置可以通过Gradle进行配置,如: 1 2 3 4 5 6 7 8 9 android { testOptions { unitTests.all { systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo' systemProperty 'robolectric.dependency.repo.id', 'local' } } } 驱动Activity生命周期 利用ActivityController我们可以让Activity执行相应的生命周期方法,如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Test public void testLifecycle() throws Exception { // 创建Activity控制器 ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class); MainActivity activity = controller.get(); assertNull(activity.getLifecycleState()); // 调用Activity的performCreate方法 controller.create(); assertEquals("onCreate", activity.getLifecycleState()); // 调用Activity的performStart方法 controller.start(); assertEquals("onStart", activity.getLifecycleState()); // 调用Activity的performResume方法 controller.resume(); assertEquals("onResume", activity.getLifecycleState()); // 调用Activity的performPause方法 controller.pause(); assertEquals("onPause", activity.getLifecycleState()); // 调用Activity的performStop方法 controller.stop(); assertEquals("onStop", activity.getLifecycleState()); // 调用Activity的performRestart方法 controller.restart(); // 注意此处应该是onStart,因为performRestart不仅会调用restart,还会调用onStart assertEquals("onStart", activity.getLifecycleState()); // 调用Activity的performDestroy方法 controller.destroy(); assertEquals("onDestroy", activity.getLifecycleState()); } 通过ActivityController,我们可以模拟各种生命周期的变化。但是要注意,我们虽然可以随意调用Activity的生命周期,但是Activity生命周期切换有自己的检测机制,我们要遵循Activity的生命周期规律。比如,如果当前Activity并非处于stop状态,测试代码去调用了controller.restart方法,此时Activity是不会回调onRestart和onStart的。 除了控制生命周期,还可以在启动Activity的时候传递Intent: 1 2 3 4 5 6 7 8 9 10 11 12 /** * 启动Activity的时候传递Intent * * @throws Exception */ @Test public void testStartActivityWithIntent() throws Exception { Intent intent = new Intent(); intent.putExtra("test", "HelloWorld"); Activity activity = Robolectric.buildActivity(MainActivity.class).withIntent(intent).create().get(); assertEquals("HelloWorld", activity.getIntent().getExtras().getString("test")); } onRestoreInstanceState回调中传递Bundle: 1 2 3 4 5 6 7 8 9 10 11 /** * savedInstanceState会在onRestoreInstanceState回调中传递给Activity * * @throws Exception */ @Test public void testSavedInstanceState() throws Exception { Bundle savedInstanceState = new Bundle(); Robolectric.buildActivity(MainActivity.class).create().restoreInstanceState(savedInstanceState).get(); // verify something } 在真实环境下,视图是在onCreate之后的某一时刻在attach到Window上的,在此之前,View是处于不可操作状态的,你不能点击它。在Activity的onPostResume方法调用之后,View才会attach到Window之中。但是,在Robolectric之中,我们可以用控制器的visible方法使得View变为可见,变为可见之后,就可以模拟点击事件了 1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testVisible() throws Exception { ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class); MainActivity activity = controller.get(); // 调用Activity的performCreate并且设置视图visible controller.create().visible(); // 触发点击 activity.findViewById(R.id.activity_main_button1).performClick(); // 验证 assertEquals(shadowOf(activity).getNextStartedActivity().getComponent().getClassName(), SecondActivity.class.getName()); } 追加模块 为了减少依赖包的大小,Robolectric的shadows类成了好几部分: SDK PackageRobolectric Add-On Package com.android.support.support-v4org.robolectric:shadows-support-v4 com.android.support.multidexorg.robolectric:shadows-multidex com.google.android.gms:play-servicesorg.robolectric:shadows-play-services com.google.android.maps:mapsorg.robolectric:shadows-maps org.apache.httpcomponents:httpclientorg.robolectric:shadows-httpclient 用户可以根据自身需求添加以下依赖包,如 1 2 3 4 5 6 7 8 9 dependencies { ... ... testCompile 'org.robolectric:robolectric:3.1.4' testCompile 'org.robolectric:shadows-support-v4:3.1.4' testCompile 'org.robolectric:shadows-multidex:3.1.4' testCompile 'org.robolectric:shadows-play-services:3.1.4' testCompile 'org.robolectric:shadows-maps:3.1.4' testCompile 'org.robolectric:shadows-httpclient:3.1.4' } 自定义Shadow类 Shadow类需要一个public的无参构造方法以方便Robolectric框架可以实例化它,通过@Implements注解与原始类关联在一起 若原始类有有参构造方法,在Shadow类中定义public void类型的名为__constructor__的方法,且方法参数与原始类的构造方法参数一直 定义与原始类方法签名一致的方法,在里面重写实现,Shadow方法需用@Implementation进行注解 下面我们来创建RobolectricBean的Shadow类 原始类: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class RobolectricBean { String name; int color; public RobolectricBean(String name) { this.name = name; } public String getName() { return name; } public int getColor() { return color; } public void setColor(int color) { this.color = color; } } Shadow类: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 /** * 创建{@link RobolectricBean}的影子类 * * @author HansChen */ @Implements(RobolectricBean.class) public class ShadowRobolectricBean { /** * 通过@RealObject注解可以访问原始对象,但注意,通过@RealObject注解的变量调用方法,依然会调用Shadow类的方法,而不是原始类的方法 * 只能用来访问原始类的field */ @RealObject RobolectricBean realBean; /** * 需要一个无参构造方法 */ public ShadowRobolectricBean() { } /** * 对应原始类的构造方法 * * @param name 对应原始类构造方法的传入参数 */ public void __constructor__(String name) { realBean.name = name; } /** * 原始对象的方法被调用的时候,Robolectric会根据方法签名查找对应的Shadow方法并调用 */ @Implementation public String getName() { return "Hello, I ma shadow of RobolectricBean: " + realBean.name; } @Implementation public int getColor() { return realBean.color; } @Implementation public void setColor(int color) { realBean.color = color; } } Shadow类中访问原始类的field Shadow类中可以定义一个原始类的成员变量,并用@RealObject注解,这样,Shadow类就能访问原始类的field了,但是注意,通过@RealObject注解的变量调用方法,依然会调用Shadow类的方法,而不是原始类的方法,只能用它来访问原始类的field。 1 2 3 4 5 6 7 8 9 @Implements(Point.class) public class ShadowPoint { @RealObject private Point realPoint; ... public void __constructor__(int x, int y) { realPoint.x = x; realPoint.y = y; } } 如何在测试用例中让Shadow生效 在Config注解中添加shadows参数,指定对应的Shadow生效 1 2 3 4 5 6 @RunWith(RobolectricTestRunner.class) @Config(shadows = ShadowRobolectricBean.class) public class RobolectricBeanTest { ... ... } 注意,自定义的Shadow类不能通过Shadows.shadowOf()获取,需要用ShadowExtractor.extract()来获取,获取之后进行类型转换: 1 ShadowRobolectricBean shadowBean = (ShadowRobolectricBean) ShadowExtractor.extract(bean); 常用测试场景 页面跳转验证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /** * 验证点击事件是否触发了页面跳转,验证目标页面是否预期页面 * * @throws Exception */ @Test public void testJump() throws Exception { // 默认会调用Activity的生命周期: onCreate->onStart->onResume MainActivity activity = Robolectric.setupActivity(MainActivity.class); // 触发按钮点击 activity.findViewById(R.id.activity_main_jump).performClick(); // 获取对应的Shadow类 ShadowActivity shadowActivity = Shadows.shadowOf(activity); // 借助Shadow类获取启动下一Activity的Intent Intent nextIntent = shadowActivity.getNextStartedActivity(); // 校验Intent的正确性 assertEquals(nextIntent.getComponent().getClassName(), SecondActivity.class.getName()); } UI组件状态验证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /** * 验证UI组件状态 * * @throws Exception */ @Test public void testCheckBoxState() throws Exception { MainActivity activity = Robolectric.setupActivity(MainActivity.class); CheckBox checkBox = (CheckBox) activity.findViewById(R.id.activity_main_check_box); // 验证CheckBox初始状态 assertFalse(checkBox.isChecked()); // 点击按钮反转CheckBox状态 activity.findViewById(R.id.activity_main_switch_check_box).performClick(); // 验证状态是否正确 assertTrue(checkBox.isChecked()); // 点击按钮反转CheckBox状态 activity.findViewById(R.id.activity_main_switch_check_box).performClick(); // 验证状态是否正确 assertFalse(checkBox.isChecked()); } 验证Dialog 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /** * 验证Dialog是否正确弹出 * * @throws Exception */ @Test public void testDialog() throws Exception { MainActivity activity = Robolectric.setupActivity(MainActivity.class); AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog(); // 判断Dialog尚未弹出 assertNull(dialog); activity.findViewById(R.id.activity_main_show_dialog).performClick(); dialog = ShadowAlertDialog.getLatestAlertDialog(); // 判断Dialog已经弹出 assertNotNull(dialog); // 获取Shadow类进行验证 ShadowAlertDialog shadowDialog = shadowOf(dialog); assertEquals("AlertDialog", shadowDialog.getTitle()); assertEquals("Oops, now you see me ~", shadowDialog.getMessage()); } 验证Toast 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /** * 验证Toast是否正确弹出 * * @throws Exception */ @Test public void testToast() throws Exception { MainActivity activity = Robolectric.setupActivity(MainActivity.class); Toast toast = ShadowToast.getLatestToast(); // 判断Toast尚未弹出 assertNull(toast); activity.findViewById(R.id.activity_main_show_toast).performClick(); toast = ShadowToast.getLatestToast(); // 判断Toast已经弹出 assertNotNull(toast); // 获取Shadow类进行验证 ShadowToast shadowToast = shadowOf(toast); assertEquals(Toast.LENGTH_SHORT, shadowToast.getDuration()); assertEquals("oops", ShadowToast.getTextOfLatestToast()); } 验证Fragment 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, application = CustomApplication.class) public class MyFragmentTest { private MyFragment myFragment; @Before public void setUp() throws Exception { myFragment = new MyFragment(); // 把Fragment添加到Activity中 FragmentTestUtil.startFragment(myFragment); } @Test public void testFragment() throws Exception { assertNotNull(myFragment.getView()); } } 验证BroadcastReceiver 首先看下广播接收器: 1 2 3 4 5 6 public class MyReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // do something } } 广播的测试点可以包含两个方面 验证应用程序是否注册了该广播 验证广播接收器的处理逻辑是否正确,关于逻辑是否正确,可以直接人为的触发onReceive()方法,让然后进行验证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, application = CustomApplication.class) public class MyReceiverTest { @Test public void restRegister() throws Exception { ShadowApplication shadowApplication = ShadowApplication.getInstance(); String action = "ut.cn.unittestdemo.receiver"; Intent intent = new Intent(action); // 验证是否注册了相应的Receiver assertTrue(shadowApplication.hasReceiverForIntent(intent)); } @Test public void restReceive() throws Exception { String action = "ut.cn.unittestdemo.receiver"; Intent intent = new Intent(action); intent.putExtra("EXTRA_USERNAME", "HansChen"); MyReceiver myReceiver = new MyReceiver(); myReceiver.onReceive(RuntimeEnvironment.application, intent); // verify something } } 验证Service Service和Activity一样,都有生命周期,Robolectric也提供了Service的生命周期控制器,使用方式和Activity类似,这里就不做详细解释了 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, application = CustomApplication.class) public class TestServiceTest { private ServiceController<TestService> controller; private TestService testService; @Before public void setUp() throws Exception { controller = Robolectric.buildService(TestService.class); testService = controller.get(); } /** * 控制Service生命周期进行验证 * * @throws Exception */ @Test public void testLifecycle() throws Exception { controller.create(); // verify something controller.startCommand(0, 0); // verify something controller.bind(); // verify something controller.unbind(); // verify something controller.destroy(); // verify something } }

2016/12/10
articleCard.readMore

设计模式之模板方法模式和策略模式

概述 我们知道,OOP三个基本特征是:封装、继承、多态。通过继承,我们可以基于差异编程,也就是说,对于一个满足我们大部分需求的类,可以创建它的一个子类并只改变我们不期望的那部分。但是在实际使用中,继承很容易被过度使用,并且过度使用的代价是比较高的,所以我们减少了继承的使用,使用组合或委托代替 优先使用对象组合而不是类继承 在本文中,我们会分别介绍模板方法模式和策略模式,这两个模式分别使用了继承和委托两种方式。这两种模式解决的问题是类似的,经常可以互换使用,它们都可以分离通用的算法和具体的上下文。比如我们有一个通用的算法,算法有不同的实现方式,为了遵循依赖倒置原则,我们希望算法不依赖于具体实现。 本文冒泡排序法来进行举例说明: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 /** * @author HansChen */ public class Sorter { /** * 冒泡排序 */ public int sort(int[] array) { int operations = 0; if (array.length <= 1) { return operations; } for (int i = 0; i < array.length - 1; i++) { for (int j = 0; j < array.length - i - 1; j++) { operations++; if (needSwap(array, j)) { swap(array, j); } } } return operations; } /** * @return 是否需要交换数组中 index 和 index+1 元素 */ private boolean needSwap(int[] array, int index) { return array[index] > array[index + 1]; } /** * 交换array数组中的 index 和 index+1 元素 */ private void swap(int[] array, int index) { int temp = array[index]; array[index] = array[index + 1]; array[index + 1] = temp; } } 这是我们实现的冒泡排序算法,这个sort方法可以对int数组进行排序。但我们发现,这种写法的扩展性是不强的,如果我们要实现double数组排序呢?如果我们需要排序的是一个对象数组?难道需要各自定义一个方法吗?如果它们都使用冒泡排序算法,那么sort的算法逻辑肯定是相似的,有没有一种方法能让这个算法逻辑复用呢?下面用模板方法模式和策略模式对它进行改造 模板方法模式 模板方法模式:定义一个算法的骨架,将骨架中的特定步骤延迟到子类中。模板方法模式使得子类可以不改变算法的结构即可重新定义该算法的某些特定步骤 下图是用模板方法模式对冒泡排序重构后的结构图: 首先,我们在BubbleSorter的sort方法中定义算法骨架,再定义一些延迟到子类中的抽象方法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 /** * @author HansChen */ public abstract class BubbleSorter<T> { /** * 冒泡排序 */ public int sort(T array) { setArray(array); int length = getLength(); int operations = 0; if (length <= 1) { return operations; } for (int i = 0; i < length - 1; i++) { for (int j = 0; j < length - i - 1; j++) { operations++; if (needSwap(j)) { swap(j); } } } return operations; } /** * 初始化排序数组 */ protected abstract void setArray(T array); /** * @return 返回数组长度 */ protected abstract int getLength(); /** * @return 是否需要交换数组中 index 和 index+1 元素 */ protected abstract boolean needSwap(int index); /** * 交换array数组中的 index 和 index+1 元素 */ protected abstract void swap(int index); } 有了BubbleSorter类,我们就可以创建任意不同类型的对象排序的简单派生类,比如创建IntBubbleSorter去排序整型数组: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class IntBubbleSorter extends BubbleSorter<int[]> { private int[] array; @Override protected void setArray(int[] array) { this.array = array; } @Override protected int getLength() { return array == null ? 0 : array.length; } @Override protected boolean needSwap(int index) { return array != null && (array[index] > array[index + 1]); } @Override protected void swap(int index) { int temp = array[index]; array[index] = array[index + 1]; array[index + 1] = temp; } } 再比如创建DoubleBubbleSorter去排序双精度型数组: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class DoubleBubbleSorter extends BubbleSorter<double[]> { private double[] array; @Override protected void setArray(double[] array) { this.array = array; } @Override protected int getLength() { return array == null ? 0 : array.length; } @Override protected boolean needSwap(int index) { return array != null && (array[index] > array[index + 1]); } @Override protected void swap(int index) { double temp = array[index]; array[index] = array[index + 1]; array[index + 1] = temp; } } 甚至我们不仅限于对数组排序,还可以对List集合排序,比如创建IntegerListBubbleSorter对List集合进行冒泡排序: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class IntegerListBubbleSorter extends BubbleSorter<List<Integer>> { private List<Integer> list; @Override protected void setArray(List<Integer> list) { this.list = list; } @Override protected int getLength() { return list == null ? 0 : list.size(); } @Override protected boolean needSwap(int index) { return list != null && (list.get(index) > list.get(index + 1)); } @Override protected void swap(int index) { int temp = list.get(index); list.set(index, list.get(index + 1)); list.set(index + 1, temp); } } 定义上述类之后,我们看下怎么使用上面的类: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Test { public static void main(String[] args) { //对整型数组排序 int[] intArray = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; int operations = new IntBubbleSorter().sort(intArray); System.out.println("[Template Method] operations:" + operations + ", array:" + Arrays.toString(intArray)); //对double数组排序 double[] doubleArray = {9.9, 8.8, 7.7, 6.6, 5.5, 4.4, 3.3, 2.2, 1.1, 0.0}; operations = new DoubleBubbleSorter().sort(doubleArray); System.out.println("[Template Method] operations:" + operations + ", array:" + Arrays.toString(doubleArray)); //对List集合排序 List<Integer> list = Arrays.asList(9, 8, 7, 6, 5, 4, 3, 2, 1, 0); operations = new IntegerListBubbleSorter().sort(list); System.out.println("[Template Method] operations:" + operations + ", list:" + list.toString()); } } 模板方法模式展示了经典重用的一种形式,通用算法被放在基类中,通过继承在不同的子类中实现该通用算法。我们通过定义通用类BubbleSorter,把冒泡排序的算法骨架放在基类,然后实现不同的子类分别对int数组、double数组、List集合进行排序。但这样是有代价的,因为继承是非常强的关系,派生类不可避免地与基类绑定在一起了。但如果我现在需要用快速排序而不是冒泡排序来进行排序,但快速排序却没有办法重用setArray、getLength、needSwap和swap方法了。不过,策略模式提供了另一种可选的方案 策略模式 策略模式属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换,下面用策略模式对冒泡排序进行重构 下图是用策略模式对冒泡排序重构后的结构图: 首先定义一个BubbleSorter类,它持有一个抽象策略接口: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class BubbleSorter<T> { /** * 抽象策略接口,可以有不同的实现 */ private SortHandler<T> sortHandler; public BubbleSorter(SortHandler<T> sortHandler) { this.sortHandler = sortHandler; } /** * 冒泡排序 */ public int sort(T array) { sortHandler.setArray(array); int length = sortHandler.getLength(); int operations = 0; if (length <= 1) { return operations; } for (int i = 0; i < length - 1; i++) { for (int j = 0; j < length - i - 1; j++) { operations++; if (sortHandler.needSwap(j)) { sortHandler.swap(j); } } } return operations; } } 定义抽象策略接口: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public interface SortHandler<T> { /** * 初始化排序数组 */ void setArray(T array); /** * @return 返回数组长度 */ int getLength(); /** * @return 是否需要交换数组中 index 和 index+1 元素 */ boolean needSwap(int index); /** * 交换array数组中的 index 和 index+1 元素 */ void swap(int index); } 创建具体的策略类IntSortHandler对整型数组进行操作: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class IntSortHandler implements SortHandler<int[]> { private int[] array; @Override public void setArray(int[] array) { this.array = array; } @Override public int getLength() { return array == null ? 0 : array.length; } @Override public boolean needSwap(int index) { return array != null && (array[index] > array[index + 1]); } @Override public void swap(int index) { int temp = array[index]; array[index] = array[index + 1]; array[index + 1] = temp; } } 创建具体的策略类DoubleSortHandler对双精度型数组进行操作: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class DoubleSortHandler implements SortHandler<double[]> { private double[] array; @Override public void setArray(double[] array) { this.array = array; } @Override public int getLength() { return array == null ? 0 : array.length; } @Override public boolean needSwap(int index) { return array != null && (array[index] > array[index + 1]); } @Override public void swap(int index) { double temp = array[index]; array[index] = array[index + 1]; array[index + 1] = temp; } } 创建具体的策略类IntegerListSortHandler对List集合进行操作: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class IntegerListSortHandler implements SortHandler<List<Integer>> { private List<Integer> list; @Override public void setArray(List<Integer> list) { this.list = list; } @Override public int getLength() { return list == null ? 0 : list.size(); } @Override public boolean needSwap(int index) { return list != null && (list.get(index) > list.get(index + 1)); } @Override public void swap(int index) { int temp = list.get(index); list.set(index, list.get(index + 1)); list.set(index + 1, temp); } } 定义上述类之后,我们看下怎么使用策略模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Test { public static void main(String[] args) { //对整型数组排序 int[] intArray = {9, 8, 7, 6, 5, 4, 3, 2, 1, 0}; BubbleSorter<int[]> intBubbleSorter = new BubbleSorter<>(new IntSortHandler()); int operations = intBubbleSorter.sort(intArray); System.out.println("[Strategy] operations:" + operations + ", array:" + Arrays.toString(intArray)); //对double数组排序 double[] doubleArray = {9.9, 8.8, 7.7, 6.6, 5.5, 4.4, 3.3, 2.2, 1.1, 0.0}; BubbleSorter<double[]> doubleBubbleSorter = new BubbleSorter<>(new DoubleSortHandler()); operations = doubleBubbleSorter.sort(doubleArray); System.out.println("[Strategy] operations:" + operations + ", array:" + Arrays.toString(doubleArray)); //对List集合排序 List<Integer> list = Arrays.asList(9, 8, 7, 6, 5, 4, 3, 2, 1, 0); BubbleSorter<List<Integer>> integerListBubbleSorter = new BubbleSorter<>(new IntegerListSortHandler()); operations = integerListBubbleSorter.sort(list); System.out.println("[Strategy] operations:" + operations + ", list:" + list); } } 策略模式不是将通用方法放到基类中,而是把它放进BubbleSorter的sort方法中,把排序算法中必须调用的抽象方法定义在SortHandler接口中,从这个接口中派生出不同的子类。把派生出的子类传给BubbleSorter后,sort方法就可以把具体工作委托给接口去完成。注意:SortHandler对BubbleSorter是一无所知的,它不依赖于冒泡排序的具体实现,这个和模板方法模式是不同的。如果其他排序算法也需要用到SortHandler,完全也可以在相关的排序算法中使用SortHandler 总结 模板方法模式和策略模式都可以用来分离高层的算法和低层的具体实现细节,都允许高层的算法独立于它的具体实现细节重用。但策略模式还有一个额外的好处就是允许具体实现细节独立于高层的算法重用,但这也以一些额外的复杂性、内存以及运行事件开销作为代价 文中示例代码下载:https://github.com/hanschencoder/awesome-demo/tree/master/Patterns

2016/12/1
articleCard.readMore

设计模式之工厂模式(Factory)

概述 根据依赖倒置原则,我们知道,我们应优先依赖抽象类而不是具体类。在应用开发过程中,有很多实体类都是非常易变的,依赖它们会带来问题,所以我们更应该依赖于抽象接口,已使我们免受大多数变化的影响。 工厂模式(Factory)允许我们只依赖于抽象接口就能创建出具体对象的实例,所以在开发中,如果具体类是高度易变的,那么该模式就非常有用。 接下来我们就通过代码举例说明什么是工厂模式 简单工厂模式 假设我们现在有个需求:把一段数据用Wi-Fi或者蓝牙发送出去。 需求很简单是吧?刷刷刷就写下了以下实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private String mode; //Wi-Fi|Bluetooth public void onClick() { byte[] data = {0x00, 0x01}; if ("Wi-Fi".equals(mode)) { sendDataByWiFi(data); } else { sendDataByBluetooth(data); } } private void sendDataByWiFi(byte[] data) { // send data via Wi-Fi } private void sendDataByBluetooth(byte[] data) { // send data via Bluetooth } 但是上面的代码扩展性并不高,违反了开放封闭原则。比如现在又有了个新的需求,需要用zigbee把数据发送出去,就得再新增一个sendDataByZigbee方法了,而且还得修改onClick里面的逻辑。那么比较好的方法是怎么样的呢? 定义一个数据发送器类: 1 2 3 4 5 6 7 8 9 /** * 数据发送器Sender * * @author HansChen */ public interface Sender { void sendData(byte[] data); } 实现WiFi数据发送: 1 2 3 4 5 6 7 8 9 10 11 12 /** * Sender的实现类,通过Wi-Fi发送数据 * * @author HansChen */ public class WiFiSender implements Sender { @Override public void sendData(byte[] data) { System.out.println("Send data by Wi-Fi"); } } 实现蓝牙数据发送: 1 2 3 4 5 6 7 8 9 10 11 12 /** * Sender的实现类,通过蓝牙发送数据 * * @author HansChen */ public class BluetoothSender implements Sender { @Override public void sendData(byte[] data) { System.out.println("Send data by Bluetooth"); } } 这样,原来发送数据的地方就改为了: 1 2 3 4 5 6 7 8 9 10 11 12 13 private String mode; //Wi-Fi|Bluetooth public void onClick() { byte[] data = {0x00, 0x01}; Sender sender; if ("Wi-Fi".equals(mode)) { sender = new WiFiSender(); } else { sender = new BluetoothSender(); } sender.sendData(data); } 有没有觉得代码优雅了一点?但是随着发送器Sender的实现类越来越多,每增加一个实现类,就需要在onClick里面实例化相应的实现类,能不能用一个单独的类来做这个创造实例的过程呢?这就是我们讲到的工厂。我们新增一个工厂类: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /** * 简单工厂类 * * @author HansChen */ public class SimpleFactory { public static Sender createSender(String mode) { switch (mode) { case "Wi-Fi": return new WiFiSender(); case "Bluetooth": return new BluetoothSender(); default: throw new IllegalArgumentException("illegal type: " + mode); } } } 这样一来,怎么实例化数据发送器我们也不用管了,最终代码变为: 1 2 3 4 5 6 7 8 private String mode; //Wi-Fi|Bluetooth public void onClick() { byte[] data = {0x00, 0x01}; Sender sender = SimpleFactory.createSender(mode); sender.sendData(data); } 好了,到这里我们就完成了简单工厂模式的应用了,下图就是简单工厂模式的结构图: 工厂方法模式 简单工厂模式的优点在于工厂类包含了必要的判断逻辑,根据传入的参数动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖。但是这里还是会有个问题,假设上面例子中新增了一个zigbee发送器,那么一定是需要修改简单工厂类的,也就是说,我们不但对扩展开放了,对修改也开放了,这是不好的。解决的方法是使用工厂方法模式,工厂方法模式是指定义一个用于创建对象的接口,让子类决定实例化哪一个类。下面还是通过代码来说明: 在简单工厂模式的基础上,让我们对工厂类也升级一下,首先定义一个工厂类接口: 1 2 3 4 public interface SenderFactory { Sender createSender(); } 然后为每一个发送器的实现类各创建一个具体的工厂方法去实现这个接口 定义WiFiSender的工厂类: 1 2 3 4 5 6 7 public class WiFiSenderFactory implements SenderFactory { @Override public Sender createSender() { return new WiFiSender(); } } 定义BluetoothSender的工厂类: 1 2 3 4 5 6 7 public class BluetoothSenderFactory implements SenderFactory { @Override public Sender createSender() { return new BluetoothSender(); } } 这样,即使有新的Sender实现类加进来,我们只需要新增相应的工厂类就行了,不需要修改原有的工厂,下图就是工厂方法模式的结构图: 客户端调用代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 private String mode; //Wi-Fi|Bluetooth public void onClick() { byte[] data = {0x00, 0x01}; SenderFactory factory; if ("Wi-Fi".equals(mode)) { factory = new WiFiSenderFactory(); } else { factory = new BluetoothSenderFactory(); } Sender sender = factory.createSender(); sender.sendData(data); } 细心的读者可能已经发现了,工厂方法模式实现时,客户端需要决定实例化哪一个工厂类,相比于简单工厂模式,客户端多了一个选择判断的问题,也就是说,工厂方法模式把简单工厂模式的内部逻辑判断移到了客户端!你想要加功能,本来是修改简单工厂类的,现在改为修改客户端。但是这样带来的好处是整个工厂和产品体系都没有“修改”的变化,只有“扩展”的变化,完全符合了开放封闭原则。 总结 简单工厂模式和工厂方法模式都封装了对象的创建,它们使得高层策略模块在创建类的实例时无需依赖于这些类的具体实现。但是两种工厂模式之间又有差异: 简单工厂模式:最大的优点在于工厂类包含了必要的判断逻辑,根据客户端的条件动态地实例化相关的类。但这也是它的缺点,当扩展功能的时候,需要修改工厂方法,违反了开放封闭原则 工厂方法模式:符合开放封闭原则,但这带来的代价是扩展的时候要增加相应的工厂类,增加了开发量,而且需要修改客户端代码

2016/11/26
articleCard.readMore

Fragment源码分析

概述 Fragment表示 Activity 中的行为或用户界面部分。您可以将多个 Fragment 组合在一个 Activity 中来构建多窗格 UI,以及在多个 Activity 中重复使用某个 Fragment。您可以将 Fragment 视为 Activity 的模块化组成部分,它具有自己的生命周期,能接收自己的输入事件,并且您可以在 Activity 运行时添加或移除 Fragment。 Fragment 必须始终嵌入在 Activity 中,其生命周期直接受宿主 Activity 生命周期的影响。 例如,当 Activity 暂停时,其中的所有 Fragment 也会暂停;当 Activity 被销毁时,所有 Fragment 也会被销毁。 不过,当 Activity 正在运行(处于已恢复生命周期状态)时,您可以独立操纵每个 Fragment,如添加或移除它们。 当您执行此类 Fragment 事务时,您也可以将其添加到由 Activity 管理的返回栈 — Activity 中的每个返回栈条目都是一条已发生 Fragment 事务的记录。 返回栈让用户可以通过按返回按钮撤消 Fragment 事务(后退)。 当您将 Fragment 作为 Activity 布局的一部分添加时,它存在于 Activity 视图层次结构的某个 ViewGroup 内部,并且 Fragment 会定义其自己的视图布局。您可以通过在 Activity 的布局文件中声明Fragment,将其作为 <fragment> 元素插入您的 Activity 布局中,或者通过将其添加到某个现有 ViewGroup,利用应用代码进行插入。不过,Fragment 并非必须成为 Activity 布局的一部分;您还可以将没有自己 UI 的 Fragment 用作 Activity 的不可见工作线程。 本文将通过分析源码,对 Fragment 的创建、销毁以及生命周期做一个更深入的认识。 建议读者在看这篇文章的时候,先看下Fragment事务管理源码分析,对Fragment管理类先有一个比较清楚的认识。 分析入口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /** * 构造并显示Fragment * * @param containerViewId 容器控件id * @param clz Fragment类 */ protected void showFragment(@IdRes int containerViewId, Class<? extends Fragment> clz) { FragmentManager fm = getFragmentManager(); FragmentTransaction ft = fm.beginTransaction();//开始事务管理 try { Fragment f = clz.newInstance(); ft.add(containerViewId, f, clz.getName());//添加操作 ft.commit();//提交事务 } catch (Exception e) { e.printStackTrace(); } } 上面的代码就是动态地往containerViewId里添加一个Fragment并让它显示出来,可以看到,这个涉及到Fragment的事务管理,详细可以参考Fragment事务管理源码分析,这里就不再阐述了。 代码分析 BackStackRecord#run 调用了commit之后,真正执行的地方是在BackStackRecord的run方法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 public void run() { ...... if (mManager.mCurState >= Fragment.CREATED) { SparseArray<Fragment> firstOutFragments = new SparseArray<Fragment>(); SparseArray<Fragment> lastInFragments = new SparseArray<Fragment>(); calculateFragments(firstOutFragments, lastInFragments); beginTransition(firstOutFragments, lastInFragments, false); } //遍历链表,根据cmd事务类型依次处理事务 Op op = mHead; while (op != null) { switch (op.cmd) { case OP_ADD: { //添加一个新的Fragment Fragment f = op.fragment; f.mNextAnim = op.enterAnim; mManager.addFragment(f, false); } break; case OP_REPLACE: { Fragment f = op.fragment; int containerId = f.mContainerId; if (mManager.mAdded != null) { for (int i = mManager.mAdded.size() - 1; i >= 0; i--) { Fragment old = mManager.mAdded.get(i); if (old.mContainerId == containerId) { if (old == f) { op.fragment = f = null; } else { if (op.removed == null) { op.removed = new ArrayList<Fragment>(); } op.removed.add(old); old.mNextAnim = op.exitAnim; if (mAddToBackStack) { old.mBackStackNesting += 1; } mManager.removeFragment(old, mTransition, mTransitionStyle); } } } } if (f != null) { f.mNextAnim = op.enterAnim; mManager.addFragment(f, false); } } break; case OP_REMOVE: { Fragment f = op.fragment; f.mNextAnim = op.exitAnim; mManager.removeFragment(f, mTransition, mTransitionStyle); } break; case OP_HIDE: { Fragment f = op.fragment; f.mNextAnim = op.exitAnim; mManager.hideFragment(f, mTransition, mTransitionStyle); } break; case OP_SHOW: { Fragment f = op.fragment; f.mNextAnim = op.enterAnim; mManager.showFragment(f, mTransition, mTransitionStyle); } break; case OP_DETACH: { Fragment f = op.fragment; f.mNextAnim = op.exitAnim; mManager.detachFragment(f, mTransition, mTransitionStyle); } break; case OP_ATTACH: { Fragment f = op.fragment; f.mNextAnim = op.enterAnim; mManager.attachFragment(f, mTransition, mTransitionStyle); } break; default: { throw new IllegalArgumentException("Unknown cmd: " + op.cmd); } } op = op.next; } mManager.moveToState(mManager.mCurState, mTransition, mTransitionStyle, true); if (mAddToBackStack) { mManager.addBackStackState(this); } } 因为我们调用的是add操作,所以执行的代码片段是: 1 2 3 4 5 6 case OP_ADD: { Fragment f = op.fragment; f.mNextAnim = op.enterAnim; mManager.addFragment(f, false); } break; 参数解释: op.fragment:showFragment中创建的Fragment实例,并且现在Fragment的mTag、mFragmentId、mContainerId已被初始化过了 op.enterAnim:入场动画,可以先不管 mManager:FragmentManagerImpl实例 FragmentManagerImpl#addFragment 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public void addFragment(Fragment fragment, boolean moveToStateNow) { //已添加的Fragment列表 if (mAdded == null) { mAdded = new ArrayList<Fragment>(); } //设置Fragment的mIndex,并把Fragment添加到mActive列表 makeActive(fragment); //判断是否被detach。默认为false if (!fragment.mDetached) { if (mAdded.contains(fragment)) { throw new IllegalStateException("Fragment already added: " + fragment); } //把Fragment添加到mAdded列表 mAdded.add(fragment); //设置Fragment标记位 fragment.mAdded = true; fragment.mRemoving = false; //判断是否需要刷新菜单 if (fragment.mHasMenu && fragment.mMenuVisible) { mNeedMenuInvalidate = true; } //在这次分析中moveToStateNow为false,moveToState方法在本方法外层方法中调用 if (moveToStateNow) { moveToState(fragment); } } } addFragment里面把Fragment加入mActive和mAdded列表,并且设置标记为fragment.mAdded为true,fragment.mRemoving为false。 执行完ADD操作后,执行moveToState,moveToState顾名思义,就是把Fragment变为某种状态 1 2 3 4 5 6 7 8 //mManager.mCurState的状态很重要,我们下面会分析它现在处于什么状态 mManager.moveToState(mManager.mCurState, mTransition, mTransitionStyle, true); //添加本次操作到回退栈中 if (mAddToBackStack) { mManager.addBackStackState(this); } Fragment状态 我们知道Fragment的生命周期是依赖于Activity的,比如Activity处于onResume,那么Fragment也会处于onResume状态,这里的参数mManager.mCurState对应的状态有: 1 2 3 4 5 6 7 static final int INVALID_STATE = -1; // Invalid state used as a null value. static final int INITIALIZING = 0; // Not yet created. static final int CREATED = 1; // Created. static final int ACTIVITY_CREATED = 2; // The activity has finished its creation. static final int STOPPED = 3; // Fully created, not started. static final int STARTED = 4; // Created and started, not resumed. static final int RESUMED = 5; // Created started and resumed. mCurState的初始状态是Fragment.INITIALIZING,那么在BackStackRecord中调用moveToState的时候,mCurState是什么值呢?它是会受Activity生命周期影响而变化的,我们来看下FragmentActivity的代码 1 2 3 4 5 6 7 8 9 10 11 12 @SuppressWarnings("deprecation") @Override protected void onCreate(@Nullable Bundle savedInstanceState) { //绑定FragmentManager mFragments.attachHost(null /*parent*/); super.onCreate(savedInstanceState); ... ... //分发Fragment的create事件 mFragments.dispatchCreate(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void dispatchCreate() { mHost.mFragmentManager.dispatchCreate(); } public void dispatchCreate() { mStateSaved = false; //注意这里设置了新的state moveToState(Fragment.CREATED, false); } void moveToState(int newState, boolean always) { moveToState(newState, 0, 0, always); } void moveToState(int newState, int transit, int transitStyle, boolean always) { ... ... //给mCurState赋值 mCurState = newState; ... ... } 在onCreate中把mCurState变为Fragment.CREATED状态了,Activity的其他生命周期方法回调的时候,也会改变这个状态,大致整理如下: onCreate:Fragment.CREATED onStart:Fragment.ACTIVITY_CREATED–>Fragment.STARTED (Fragment.ACTIVITY_CREATED只会在Activity创建之后触发一次,Fragment.STARTED每次onStart的时候都会触发) onResume:Fragment.RESUMED onPause:Fragment.STARTED onStop:Fragment.STOPPED onDestroy:Fragment.INITIALIZING 下面是一张状态迁移图: 所以随着Activity生命周期的推进,Activity内所有Fragment的生命周期也会跟着推进。从Activity创建到显示出来,最后会处于onResume状态,那么我们这次就直接分析当前Activity处于onResume调用之后的情形好了。所以假定现在mCurState为Fragment.RESUMED, 让我们继续跟踪FragmentManagerImpl FragmentManagerImpl#moveToState 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void moveToState(int newState, int transit, int transitStyle, boolean always) { if (mHost == null && newState != Fragment.INITIALIZING) { throw new IllegalStateException("No activity"); } if (!always && mCurState == newState) { return; } mCurState = newState; if (mActive != null) { boolean loadersRunning = false; //遍历所有Active状态的Fragment,改变所有Fragment的状态 for (int i=0; i<mActive.size(); i++) { Fragment f = mActive.get(i); if (f != null) { //关键代码 moveToState(f, newState, transit, transitStyle, false); if (f.mLoaderManager != null) { loadersRunning |= f.mLoaderManager.hasRunningLoaders(); } } } if (!loadersRunning) { startPendingDeferredFragments(); } //让Activity刷新Menu if (mNeedMenuInvalidate && mHost != null && mCurState == Fragment.RESUMED) { mHost.onInvalidateOptionsMenu(); mNeedMenuInvalidate = false; } } } 设置最新的mCurState状态,通过上面的分析,我们知道newState等于Fragment.RESUMED。遍历mActive列表中保存的Fragment,改变Fragment状态,这里又调用了一个moveToState方法,这个方法就是真正回调Fragment生命周期的地方 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 void moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive) { // Fragments被detach或Fragment没有添加到mAdded列表的话,设置目标Fragment的新状态为CREATED状态,此次分析中不会进入这个分支 if ((!f.mAdded || f.mDetached) && newState > Fragment.CREATED) { newState = Fragment.CREATED; } //此次分析中f.mRemoving为false if (f.mRemoving && newState > f.mState) { // While removing a fragment, we can't change it to a higher state. newState = f.mState; } // 是否延时启动 if (f.mDeferStart && f.mState < Fragment.STARTED && newState > Fragment.STOPPED) { newState = Fragment.STOPPED; } if (f.mState < newState) { //此次命中的分支 ...... //根据Fragment当前的状态,选择case的分支。需要注意的是,这里的switch case是没有break语句的。这种设计可以让Fragment把自身的状态依次推进到目标状态 switch (f.mState) { case Fragment.INITIALIZING: if (f.mSavedFragmentState != null) { ...... } f.mHost = mHost; //mParent是在FragmentActivity的onCreate方法中调用attachHost传进来的,传进来的是空值 f.mParentFragment = mParent; f.mFragmentManager = mParent != null ? mParent.mChildFragmentManager : mHost.getFragmentManagerImpl(); f.mCalled = false; //【Fragment生命周期】onAttach回调,里面会把mCalled设置为true f.onAttach(mHost.getContext()); if (!f.mCalled) { throw new SuperNotCalledException("Fragment " + f + " did not call through to super.onAttach()"); } if (f.mParentFragment == null) { //让Activity可以监听到Fragment的attach mHost.onAttachFragment(f); } else { f.mParentFragment.onAttachFragment(f); } //f.mRetaining默认为false if (!f.mRetaining) { //关键代码,内部会调用【Fragment生命周期】onCreate f.performCreate(f.mSavedFragmentState); } else { f.restoreChildFragmentState(f.mSavedFragmentState, true); f.mState = Fragment.CREATED; } f.mRetaining = false; //Fragment是否定义在Layout文件的<fragment>标签中的,本次栗子为代码动态添加Fragment,所以为false if (f.mFromLayout) { // For fragments that are part of the content view // layout, we need to instantiate the view immediately // and the inflater will take care of adding it. f.mView = f.performCreateView(f.getLayoutInflater( f.mSavedFragmentState), null, f.mSavedFragmentState); if (f.mView != null) { f.mView.setSaveFromParentEnabled(false); if (f.mHidden) f.mView.setVisibility(View.GONE); f.onViewCreated(f.mView, f.mSavedFragmentState); } } //注意,这里没有break case Fragment.CREATED: if (newState > Fragment.CREATED) { if (!f.mFromLayout) { //开始创建Fragment的view ViewGroup container = null; if (f.mContainerId != 0) { if (f.mContainerId == View.NO_ID) { throwException(new IllegalArgumentException("")); } //调用Activity的findViewById方法查找控件 container = (ViewGroup) mContainer.onFindViewById(f.mContainerId); if (container == null && !f.mRestored) { ...... } } f.mContainer = container; //关键代码,内部会调用【Fragment生命周期】onCreateView,并返回Fragment中new出的视图 f.mView = f.performCreateView(f.getLayoutInflater( f.mSavedFragmentState), container, f.mSavedFragmentState); if (f.mView != null) { f.mView.setSaveFromParentEnabled(false); if (container != null) { //设置入场动画 Animator anim = loadAnimator(f, transit, true, transitionStyle); if (anim != null) { anim.setTarget(f.mView); setHWLayerAnimListenerIfAlpha(f.mView, anim); anim.start(); } //把Fragment的view加入到父控件 container.addView(f.mView); } if (f.mHidden) f.mView.setVisibility(View.GONE); //【Fragment生命周期】onViewCreated回调 f.onViewCreated(f.mView, f.mSavedFragmentState); } } //关键代码,内部会调用【Fragment生命周期】onActivityCreated f.performActivityCreated(f.mSavedFragmentState); if (f.mView != null) { f.restoreViewState(f.mSavedFragmentState); } f.mSavedFragmentState = null; } case Fragment.ACTIVITY_CREATED: if (newState > Fragment.ACTIVITY_CREATED) { f.mState = Fragment.STOPPED; } case Fragment.STOPPED: if (newState > Fragment.STOPPED) { if (DEBUG) Log.v(TAG, "moveto STARTED: " + f); //关键代码,内部会调用【Fragment生命周期】onStart f.performStart(); } case Fragment.STARTED: if (newState > Fragment.STARTED) { //关键代码,内部会调用【Fragment生命周期】onResume f.performResume(); // Get rid of this in case we saved it and never needed it. f.mSavedFragmentState = null; f.mSavedViewState = null; } } } else if (f.mState > newState) { //state降级处理 ...... } if (f.mState != newState) { f.mState = newState; } } 这段代码逻辑还是比较长,我把注释写在代码里了。可以看到,这个代码写得很巧妙,通过switch case控制,可以一层一层地把Fragment的生命周期推进下去,比如当前fragnemt的state是Fragment.STARTED,那么它就只会执行performResume,如果Fragment的状态是Fragment.INITIALIZING,那么就会从switch的最开始依次执行下来,把Fragment的生命周期onAttach–>onResume依次调用。 简要说明下上面的代码: mHost是FragmentHostCallback抽象类的实例,它的实现类是Activity的HostCallbacks mParent为null mHost.getContext()获取的context就是宿主Activity实例 Fragment中创建的View会自动通过container.addView(f.mView)添加到父控件中 很多Fragment的生命周期是通过Fragment的performXxx()方法去调用的,比如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void performCreate(Bundle savedInstanceState) { ...... onCreate(savedInstanceState); ...... } View performCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ...... return onCreateView(inflater, container, savedInstanceState); } void performActivityCreated(Bundle savedInstanceState) { ...... onActivityCreated(savedInstanceState); ...... } void performStart() { ...... onStart(); ...... } void performResume() { ...... onResume(); ...... } Fragment状态的降级操作 有些童鞋们可能会有疑问,上面只分析到了onAttach->onResume生命周期的回调,那onPause、onDestroy等方法又是什么时候执行的呢?我们再看下刚才的代码 1 2 3 4 5 6 if (f.mState < newState) { ...... } else if (f.mState > newState) { //state降级处理 ...... } 答案就是在else if分支里面,比如当Acivity锁屏的时候,就Activity生命周期会自动回调onPause,从而触发dispatchPause,在里面调用moveToState(Fragment.STARTED, false); 由于Fragment当前的状态是RESUMED状态,大于newState,所以就会走else if的分支,触发相应的生命周期方法。else if分支的逻辑和state升级的差不多,这里就再进行分析了 生命周期 最后,放张官网上公布的Fragment生命周期图,通过代码分析,我们发现代码的中生命周期的调用顺序和图中确实是一致的 总结 本文大致地从源码的角度分析了Fragment创建、生命周期回调的过程,如果读者对Fragment的remove、replace、hide、detach、attach等操作有兴趣的话,可以自行分析,核心代码主要在BackStackRecord类的run方法以及FragmentManagerImpl的moveToState方法中。

2016/11/15
articleCard.readMore

Fragment事务管理源码分析

概述 在Fragment使用中,有时候需要对Fragment进行add、remove、show、hide、replace等操作来进行Fragment的显示隐藏等管理,这些管理是通过FragmentTransaction进行事务管理的。事务管理是对于一系列操作进行管理,一个事务包含一个或多个操作命令,是逻辑管理的工作单元。一个事务开始于第一次执行操作语句,结束于Commit。通俗地将,就是把多个操作缓存起来,等调用commit的时候,统一批处理。下面会对Fragmeng的事务管理做一个代码分析 分析入口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /** * 显示Fragment,如果Fragment已添加过,则直接show,否则构造一个Fragment * * @param containerViewId 容器控件id * @param clz Fragment类 */ protected void showFragment(@IdRes int containerViewId, Class<? extends Fragment> clz) { FragmentManager fm = getFragmentManager(); FragmentTransaction ft = fm.beginTransaction();//开始事务管理 Fragment f; if ((f = fm.findFragmentByTag(clz.getName())) == null) { try { f = clz.newInstance(); ft.add(containerViewId, f, clz.getName());//添加操作 } catch (Exception e) { e.printStackTrace(); } } else { ft.show(f);//添加操作 } ft.commit();//提交事务 } 上面是一个简单的显示Fragment的栗子,简单判断一下Fragment是否已添加过,添加过就直接show,否则构造一个Fragment,最后提交事务。 代码分析 FragmentManager 上图是获取FragmentManager的大体过程 要管理Fragment事务,首先是需要拿到FragmentManager,在Activity中可以通过getFragmentManager()方法获取(使用兼容包的话,通过FragmentActivity#getSupportFragmentManager()),在这里我们就不对兼容包进行分析了 1 2 3 4 5 6 7 8 9 final FragmentController mFragments = FragmentController.createController(new HostCallbacks()); /** * Return the FragmentManager for interacting with fragments associated * with this activity. */ public FragmentManager getFragmentManager() { return mFragments.getFragmentManager(); } FragmentManager是一个抽象类,它是通过mFragments.getFragmentManager()来获取的,mFragments是FragmentController对象,它通过FragmentController.createController(new HostCallbacks())生成,这是一个静态工厂方法: 1 2 3 public static final FragmentController createController(FragmentHostCallback<?> callbacks) { return new FragmentController(callbacks); } 在这里面直接new了一个FragmentController对象,注意FragmentController的构造方法需要传入一个FragmentHostCallback FragmentController构造方法 1 2 3 4 private final FragmentHostCallback<?> mHost; private FragmentController(FragmentHostCallback<?> callbacks) { mHost = callbacks; } 构造方法很简单,传入了一个FragmentHostCallback实例 FragmentController#getFragmentManager 1 2 3 public FragmentManager getFragmentManager() { return mHost.getFragmentManagerImpl(); } 这里又调用了mHost的getFragmentManagerImpl方法,希望童鞋们没有被绕晕,mHost是一个FragmentHostCallback实例,那我们回过头来看看它传进来的地方 FragmentHostCallback 这个FragmentHostCallback是一个抽象类,我们可以看到,在Activity中是传入了 Activity#HostCallbacks内部类,这个就是FragmentHostCallback的实现类 FragmentHostCallback#getFragmentManagerImpl 1 2 3 4 final FragmentManagerImpl mFragmentManager = new FragmentManagerImpl(); FragmentManagerImpl getFragmentManagerImpl() { return mFragmentManager; } 终于找到FragmentManager的真身FragmentManagerImpl了 FragmentManagerImpl#beginTransaction 1 2 3 4 @Override public FragmentTransaction beginTransaction() { return new BackStackRecord(this); } 可以看到,所谓的FragmentTransaction其实就是一个BackStackRecord。到现在,FragmentManager和FragmentTransaction我们都找到了。下图就是各个类之间的关系: 下面开始真正的事务管理分析,我们先选择一个事务add来进行分析 FragmentTransaction#add 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public FragmentTransaction add(int containerViewId, Fragment fragment, String tag) { doAddOp(containerViewId, fragment, tag, OP_ADD); return this; } private void doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd) { //设置fragment的FragmentManagerImpl,mManager其实就是Activity#HostCallbacks中的成员变量 fragment.mFragmentManager = mManager; //设置fragment的tag if (tag != null) { if (fragment.mTag != null && !tag.equals(fragment.mTag)) { throw new IllegalStateException("..."); } fragment.mTag = tag; } if (containerViewId != 0) { if (containerViewId == View.NO_ID) { throw new IllegalArgumentException("..."); } if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) { throw new IllegalStateException(""); } //设置fragment的mContainerId以及mFragmentId fragment.mContainerId = fragment.mFragmentId = containerViewId; } //新增一个操作 Op op = new Op(); op.cmd = opcmd; op.fragment = fragment; //添加操作 addOp(op); } //插入到链表的最后 void addOp(Op op) { if (mHead == null) { mHead = mTail = op; } else { op.prev = mTail; mTail.next = op; mTail = op; } op.enterAnim = mEnterAnim; op.exitAnim = mExitAnim; op.popEnterAnim = mPopEnterAnim; op.popExitAnim = mPopExitAnim; mNumOp++; } add的操作步骤为: 设置fragment的FragmentManagerImpl 设置fragment的tag 设置fragment的mContainerId以及mFragmentId 插入一个类型为OP_ADD的操作到链表最后 这里用到了一个类: 1 2 3 4 5 6 7 8 9 10 11 static final class Op { Op next;//下一操作节点 Op prev;//上一操作节点 int cmd;//操作类型,可选有:OP_NULL|OP_ADD|OP_REPLACE|OP_REMOVE|OP_HIDE|OP_SHOW|OP_DETACH|OP_ATTACH Fragment fragment;//操作的Fragment对象 int enterAnim;//入场动画 int exitAnim;//出场动画 int popEnterAnim;//弹入动画 int popExitAnim;//弹出动画 ArrayList<Fragment> removed; } 这是一个操作链表节点。所有add、remove、hide等事物最终会形成一个操作链 FragmentTransaction#commit 等所有操作都插入后,最后我们需要调用FragmentTransaction的commit方法,操作才会真正地执行。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public int commit() { return commitInternal(false); } int commitInternal(boolean allowStateLoss) { //防止重复commit if (mCommitted) { throw new IllegalStateException("commit already called"); } //DEBUG代码统统不管 if (FragmentManagerImpl.DEBUG) { Log.v(TAG, "Commit: " + this); LogWriter logw = new LogWriter(Log.VERBOSE, TAG); PrintWriter pw = new FastPrintWriter(logw, false, 1024); dump(" ", null, pw, null); pw.flush(); } mCommitted = true; //只有调用了addToBackStack方法之后,这个标记才会为true if (mAddToBackStack) { mIndex = mManager.allocBackStackIndex(this); } else { mIndex = -1; } //插入事物队列 mManager.enqueueAction(this, allowStateLoss); return mIndex; } FragmentManagerImpl#enqueueAction 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /** * Adds an action to the queue of pending actions. * * @param action the action to add * @param allowStateLoss whether to allow loss of state information * @throws IllegalStateException if the activity has been destroyed */ public void enqueueAction(Runnable action, boolean allowStateLoss) { if (!allowStateLoss) { checkStateLoss(); } synchronized (this) { if (mDestroyed || mHost == null) { throw new IllegalStateException("Activity has been destroyed"); } if (mPendingActions == null) { mPendingActions = new ArrayList<Runnable>(); } mPendingActions.add(action); if (mPendingActions.size() == 1) { mHost.getHandler().removeCallbacks(mExecCommit); mHost.getHandler().post(mExecCommit); } } } 这里把操作添加到mPendingActions列表里去。并通过mHost.getHandler()获取Handler发送执行请求。从上面的分析知道,mHost就是Activity的HostCallbacks,构造方法中把Activity的mHandler传进去了,这里执行的mHost.getHandler()获取到的也就是Activity中的mHandler,这样做是因为需要在主线程中执行 1 final Handler mHandler = new Handler(); 再看看mExecCommit中做了什么操作: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 Runnable mExecCommit = new Runnable() { @Override public void run() { execPendingActions(); } }; /** * Only call from main thread! */ public boolean execPendingActions() { if (mExecutingActions) { throw new IllegalStateException("Recursive entry to executePendingTransactions"); } //再次检测是否主线程 if (Looper.myLooper() != mHost.getHandler().getLooper()) { throw new IllegalStateException("Must be called from main thread of process"); } boolean didSomething = false; while (true) { int numActions; synchronized (this) { //参数检测 if (mPendingActions == null || mPendingActions.size() == 0) { break; } numActions = mPendingActions.size(); if (mTmpActions == null || mTmpActions.length < numActions) { mTmpActions = new Runnable[numActions]; } mPendingActions.toArray(mTmpActions); mPendingActions.clear(); mHost.getHandler().removeCallbacks(mExecCommit); } mExecutingActions = true; //遍历执行待处理的事务操作 for (int i=0; i<numActions; i++) { mTmpActions[i].run(); mTmpActions[i] = null; } mExecutingActions = false; didSomething = true; } doPendingDeferredStart(); return didSomething; } 插入了事物之后,就是在主线程中把需要处理的事务统一处理,处理事务是通过执行mTmpActions[i].run()进行的,这个mTmpActions[i]就是前面我们通过enqueueAction方法插入的BackStackRecord,童鞋们可能没注意到,它可是一个Runnable,我们来看看它的定义 1 2 3 4 5 6 final class BackStackRecord extends FragmentTransaction implements FragmentManager.BackStackEntry, Runnable { static final String TAG = FragmentManagerImpl.TAG; ... ... } 兜兜转转,我们又回到了BackStackRecord BackStackRecord#run 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 public void run() { ...... if (mManager.mCurState >= Fragment.CREATED) { SparseArray<Fragment> firstOutFragments = new SparseArray<Fragment>(); SparseArray<Fragment> lastInFragments = new SparseArray<Fragment>(); calculateFragments(firstOutFragments, lastInFragments); beginTransition(firstOutFragments, lastInFragments, false); } //遍历链表,根据cmd事务类型依次处理事务 Op op = mHead; while (op != null) { switch (op.cmd) { case OP_ADD: { Fragment f = op.fragment; f.mNextAnim = op.enterAnim; mManager.addFragment(f, false); } break; case OP_REPLACE: { Fragment f = op.fragment; int containerId = f.mContainerId; if (mManager.mAdded != null) { for (int i = mManager.mAdded.size() - 1; i >= 0; i--) { Fragment old = mManager.mAdded.get(i); if (old.mContainerId == containerId) { if (old == f) { op.fragment = f = null; } else { if (op.removed == null) { op.removed = new ArrayList<Fragment>(); } op.removed.add(old); old.mNextAnim = op.exitAnim; if (mAddToBackStack) { old.mBackStackNesting += 1; } mManager.removeFragment(old, mTransition, mTransitionStyle); } } } } if (f != null) { f.mNextAnim = op.enterAnim; mManager.addFragment(f, false); } } break; case OP_REMOVE: { Fragment f = op.fragment; f.mNextAnim = op.exitAnim; mManager.removeFragment(f, mTransition, mTransitionStyle); } break; case OP_HIDE: { Fragment f = op.fragment; f.mNextAnim = op.exitAnim; mManager.hideFragment(f, mTransition, mTransitionStyle); } break; case OP_SHOW: { Fragment f = op.fragment; f.mNextAnim = op.enterAnim; mManager.showFragment(f, mTransition, mTransitionStyle); } break; case OP_DETACH: { Fragment f = op.fragment; f.mNextAnim = op.exitAnim; mManager.detachFragment(f, mTransition, mTransitionStyle); } break; case OP_ATTACH: { Fragment f = op.fragment; f.mNextAnim = op.enterAnim; mManager.attachFragment(f, mTransition, mTransitionStyle); } break; default: { throw new IllegalArgumentException("Unknown cmd: " + op.cmd); } } op = op.next; } mManager.moveToState(mManager.mCurState, mTransition, mTransitionStyle, true); if (mAddToBackStack) { mManager.addBackStackState(this); } } 到这一步,提交的事务就被真正执行了,我们知道,即使commit了事务之后,也不是同步执行的,是通过Handler发送到主线程执行的。 所有事务的处理都是在run方法里面执行,但是我们留意到,想要搞清楚add、remove等事务背后真正做了什么,还需要深入了解FragmentManagerImpl。 本文主要讲解Fragment事务的流程,FragmentManagerImpl的分析准备放到下一篇分析文章Fragment源码分析中,相信通过分析之后,就可以对Fragment的生命周期也有一个很好的认识了

2016/11/11
articleCard.readMore

java动态代理

概述 动态代理是java的一大特性,动态代理的优势就是实现无侵入式的代码扩展。它可以增强我们原有的方法,比如常用的日志监控,添加缓存等,也可以实现方法拦截,通过代理方法修改原方法的参数和返回值等。 要了解动态代理,我们需要先看看什么是静态代理 静态代理 首先你有一个接口: 1 2 3 4 public interface Api { String doSomething(String input); } 这个接口有一个原始的实现: 1 2 3 4 5 6 7 public class ApiImpl implements Api { @Override public String doSomething(String input) { return input + "原始方法"; } } 现在问题来了,有一个新的需求,我需要在所有调用doSomething的地方都添加一个log,那怎么办呢?我们当然可以在原有代码上直接加上log,但是ApiImpl里面的log真的是那段代码需要的吗?如果不修改原有代码,能不能实现?当然可以,如,我们添加一个代理类: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class ApiProxy implements Api { private Api mBase; public ApiProxy(Api base) { mBase = base; } @Override public String doSomething(String input) { System.out.println("someone call me~"); return mBase.doSomething(input); } } 这样,通过ApiProxy我们就是实现静态代理,这里只是简单的添加了log,我们完全可以在ApiProxy的doSomething方法里面,篡改输入参数input以及返回值,从而做一些坏事~ 动态代理 在上面静态代理例子中,我们已经实现了代理的功能,那为何还需要动态代理呢?设想一下以下两种情况 如果Api接口类中有100个方法,需要为每个方法都添加log 项目中有100个类,需要为每个类的方法都添加log 对于第一种情况,如果使用静态代理,那就只能这样了: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class ApiProxy implements Api { private Api mBase; public ApiProxy(Api base) { mBase = base; } @Override public String doSomething(String input) { System.out.println("someone call me~"); return mBase.doSomething(input); } @Override public String doSomething1(String input) { System.out.println("someone call me~"); return mBase.doSomething1(input); } //为每个方法添加实现...... } 而对于第二种情况,就只能新建100个代理类了。这种处理方式肯定不是我们喜欢的,怎么优雅地去解决了?动态代理这时候终于可以上场了。 JDK提供了动态代理方式,可以简单理解为JVM可以在运行时帮我们动态生成一系列的代理类,这样我们就不需要手写每一个静态的代理类了,比如: 实现InvocationHandler 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class ApiHandler implements InvocationHandler { private Api mBase; public ApiHandler(Api base) { mBase = base; } /** * 此方法会在proxy实例调用方法的时候回调 * * @param proxy 代理对象 * @param method 被调用的方法 * @param args 调用参数 * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("someone call me~"); return method.invoke(mBase, args); } } 动态创建代理类 1 2 3 4 5 6 7 private static void proxyTest() { ClassLoader loader = Api.class.getClassLoader();//加载代理类的ClassLoader Class[] interfaces = new Class[]{Api.class};//需要代理的接口 Api proxy = (Api) Proxy.newProxyInstance(loader, interfaces, new ApiHandler(new ApiImpl()));//创建代理对象 proxy.doSomething("test");//会调用ApiHandler的invoke方法 proxy.doSomething1("test");//会调用ApiHandler的invoke方法 } 这样,一个动态代理就完成了,但这里有个需要注意的,动态代理只能代理接口,也就是说interfaces数组里面,只能放接口Class 代理Hook 代理有比原始对象更强大的能力,如果我们自己创建代理对象,然后把原始对象替换为我们的代理对象,那么就可以在这个代理对象为所欲为了;修改参数,替换返回值,我们称之为Hook。 首先我们得找到被Hook的对象,也就是Hook点;什么样的对象比较适合Hook呢?静态变量和单例;在一个进程之内,静态变量和单例变量是不容易发生变化的,所以容易定位,而普通的对象则要么无法标志,要么容易改变,我们根据这个原则找到所谓的Hook点。 一般Hook的步骤有: 寻找Hook点,如静态变量或单例对象,尽量Hook pulic的对象和方法,非public不保证每个版本都一样,需要适配。 选择合适的代理方式,如果是接口可以用动态代理;如果是类可以手动写代理也可以使用cglib 用代理对象替换原始对象,如果没有公开是geter/setter方法,可以使用反射

2016/10/20
articleCard.readMore

Android分包MultiDex源码分析

概述 Android开发者应该都遇到了64K最大方法数限制的问题,针对这个问题,google也推出了multidex分包机制,在生成apk的时候,把整个应用拆成n个dex包(classes.dex、classes2.dex、classes3.dex),每个dex不超过64k个方法。使用multidex,在5.0以前的系统,应用安装时只安装main dex(包含了应用启动需要的必要class),在应用启动之后,需在Application的attachBaseContext中调用MultiDex.install(base)方法,在这时候才加载第二、第三…个dex文件,从而规避了64k问题。 当然,在attachBaseContext方法中直接install启动second dex会有一些问题,比如install方法是一个同步方法,当在主线程中加载的dex太大的时候,耗时会比较长,可能会触发ANR。不过这是另外一个问题了,解决方法可以参考:Android最大方法数和解决方案 http://blog.csdn.net/shensky711/article/details/52329035。 本文主要分析的是MultiDex.install()到底做了什么,如何把secondary dexes中的类动态加载进来。 MultiDex使用到的路径解析 ApplicationInfo.sourceDir:apk的安装路径,如/data/app/com.hanschen.multidex-1.apk Context.getFilesDir():返回/data/data/<packagename>/files目录,一般通过openFileOutput方法输出文件到该目录 ApplicationInfo.dataDir: 返回/data/data/<packagename>目录 源码分析 代码入口 代码入口很简单,简单粗暴,就调用了一个静态方法MultiDex.install(base);,传入一个Context对象 1 2 3 4 5 @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(base); } MultiDex.install分析 下面是主要的代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 public static void install(Context context) { Log.i("MultiDex", "install"); if (IS_VM_MULTIDEX_CAPABLE) { //VM版本大于2.1时,IS_VM_MULTIDEX_CAPABLE为true,这时候MultiDex.install什么也不用做,直接返回。因为大于2.1的VM会在安装应用的时候,就把多个dex合并到一块 } else if (VERSION.SDK_INT < 4) { //Multi dex最小支持的SDK版本为4 throw new RuntimeException("Multi dex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + "."); } else { try { ApplicationInfo e = getApplicationInfo(context); if (e == null) { return; } Set var2 = installedApk; synchronized (installedApk) { String apkPath = e.sourceDir; //检测应用是否已经执行过install()了,防止重复install if (installedApk.contains(apkPath)) { return; } installedApk.add(apkPath); //获取ClassLoader,后面会用它来加载second dex DexClassLoader classLoader; ClassLoader loader; try { loader = context.getClassLoader(); } catch (RuntimeException var9) { return; } if (loader == null) { return; } //清空目录:/data/data/<packagename>/files/secondary-dexes/,其实我没搞明白这个的作用,因为从后面的代码来看,这个目录是没有使用到的 try { clearOldDexDir(context); } catch (Throwable var8) { } File dexDir = new File(e.dataDir, "code_cache/secondary-dexes"); //把dex文件缓存到/data/data/<packagename>/code_cache/secondary-dexes/目录,[后有详细分析] List files = MultiDexExtractor.load(context, e, dexDir, false); if (checkValidZipFiles(files)) { //进行安装,[后有详细分析] installSecondaryDexes(loader, dexDir, files); } else { //文件无效,从apk文件中再次解压secondary dex文件后进行安装 files = MultiDexExtractor.load(context, e, dexDir, true); if (!checkValidZipFiles(files)) { throw new RuntimeException("Zip files were not valid."); } installSecondaryDexes(loader, dexDir, files); } } } catch (Exception var11) { throw new RuntimeException("Multi dex installation failed (" + var11.getMessage() + ")."); } } } 这段代码的主要逻辑整理如下: VM版本检测,如果大于2.1就什么都不做(系统在安装应用的时候已经帮我们把dex合并了),如果系统SDK版本小于4就抛出运行时异常 把apk中的secondary dexes解压到缓存目录,并把这些缓存读取出来。应用第二次启动的时候,会尝试从缓存目录中读取,除非读取出的文件校验失败,否则不再从apk中解压dexes 根据当前的SDK版本,执行不同的安装方法 先来看看MultiDexExtractor.load(context, e, dexDir, false) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /** * 解压apk文件中的classes2.dex、classes3.dex等文件解压到dexDir目录中 * * @param dexDir 解压目录 * @param forceReload 是否需要强制从apk文件中解压,否的话会直接读取旧文件 * @return 解压后的文件列表 * @throws IOException */ static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload) throws IOException { File sourceApk = new File(applicationInfo.sourceDir); long currentCrc = getZipCrc(sourceApk); List files; if (!forceReload && !isModified(context, sourceApk, currentCrc)) { try { //从缓存目录中直接查找缓存文件,跳过解压 files = loadExistingExtractions(context, sourceApk, dexDir); } catch (IOException var9) { files = performExtractions(sourceApk, dexDir); putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1); } } else { //把apk中的secondary dex文件解压到缓存目录,并把解压后的文件返回 files = performExtractions(sourceApk, dexDir); //把解压信息保存到sharedPreferences中 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1); } return files; } 首先判断以下是否需要强制从apk文件中解压,再进行下CRC校验,如果不需要从apk重新解压,就直接从缓存目录中读取已解压的文件返回,否则解压apk中的classes文件到缓存目录,再把相应的文件返回。这个方法再往下的分析就不贴出来了,不复杂,大家可以自己去看看。读取后会把解压信息保存到sharedPreferences中,里面会保存时间戳、CRC校验和dex数量。 得到dex文件列表后,要做的就是把dex文件关联到应用,这样应用findclass的时候才能成功。这个主要是通过installSecondaryDexes方法来完成的 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /** * 安装dex文件 * * @param loader 类加载器 * @param dexDir 缓存目录,用以存放opt之后的dex文件 * @param files 需要安装的dex * @throws IllegalArgumentException * @throws IllegalAccessException * @throws NoSuchFieldException * @throws InvocationTargetException * @throws NoSuchMethodException * @throws IOException */ private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException { if (!files.isEmpty()) { //对不同版本的SDK做不同处理 if (VERSION.SDK_INT >= 19) { MultiDex.V19.install(loader, files, dexDir); } else if (VERSION.SDK_INT >= 14) { MultiDex.V14.install(loader, files, dexDir); } else { MultiDex.V4.install(loader, files); } } } 可以看到,对于不同的SDK版本,分别采用了不同的处理方法,我们主要分析SDK>=19的情况,其他情况大同小异,读者可以自己去分析。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 private static final class V19 { private V19() { } /** * 安装dex文件 * * @param loader 类加载器 * @param additionalClassPathEntries 需要安装的dex * @param optimizedDirectory 缓存目录,用以存放opt之后的dex文件 * @throws IllegalArgumentException * @throws IllegalAccessException * @throws NoSuchFieldException * @throws InvocationTargetException * @throws NoSuchMethodException */ private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException { //通过反射获取ClassLoader对象中的pathList属性,其实是ClassLoader的父类BaseDexClassLoader中的成员 Field pathListField = MultiDex.findField(loader, "pathList"); //通过属性获取该属性的值,该属性的类型是DexPathList Object dexPathList = pathListField.get(loader); ArrayList suppressedExceptions = new ArrayList(); //通过反射调用dexPathList的makeDexElements返回Element对象数组。方法里面会读取每一个输入文件,生成DexFile对象,并将其封装进Element对象 Object[] elements = makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions); //将elements数组跟dexPathList对象的dexElements数组合并,并把合并后的数组作为dexPathList新的值 MultiDex.expandFieldArray(dexPathList, "dexElements", elements); //处理异常 if (suppressedExceptions.size() > 0) { Iterator suppressedExceptionsField = suppressedExceptions.iterator(); while (suppressedExceptionsField.hasNext()) { IOException dexElementsSuppressedExceptions = (IOException) suppressedExceptionsField.next(); Log.w("MultiDex", "Exception in makeDexElement", dexElementsSuppressedExceptions); } Field suppressedExceptionsField1 = MultiDex.findField(loader, "dexElementsSuppressedExceptions"); IOException[] dexElementsSuppressedExceptions1 = (IOException[]) ((IOException[]) suppressedExceptionsField1.get(loader)); if (dexElementsSuppressedExceptions1 == null) { dexElementsSuppressedExceptions1 = (IOException[]) suppressedExceptions.toArray(new IOException[suppressedExceptions .size()]); } else { IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions1.length]; suppressedExceptions.toArray(combined); System.arraycopy(dexElementsSuppressedExceptions1, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions1.length); dexElementsSuppressedExceptions1 = combined; } suppressedExceptionsField1.set(loader, dexElementsSuppressedExceptions1); } } private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", new Class[]{ArrayList.class, File.class, ArrayList.class}); return (Object[]) ((Object[]) makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory, suppressedExceptions})); } } 在Android中,有两个ClassLoader,分别是DexPathList和PathClassLoader,它们的父类都是BaseDexClassLoader,DexPathList和PathClassLoader的实现都是在BaseDexClassLoader之中,而BaseDexClassLoader的实现又基本是通过调用DexPathList的方法完成的。DexPathList里面封装了加载dex文件为DexFile对象(调用了native方法,有兴趣的童鞋可以继续跟踪下去)的方法。 上述代码中的逻辑如下: 通过反射获取pathList对象 通过pathList把输入的dex文件输出为elements数组,elements数组中的元素封装了DexFile对象 把新输出的elements数组合并到原pathList的dexElements数组中 异常处理 当把dex文件加载到pathList的dexElements数组之后,整个multidex.install基本上就完成了。 但可能还有些童鞋还会有些疑问,仅仅只是把Element数组合并到ClassLoader就可以了吗?还是没有找到加载类的地方啊?那我们再继续看看,当用到一个类的时候,会用ClassLoader去加载一个类,加载类会调用类加载器的findClass方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override protected Class<?> findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); //调用pathList的findClass方法 Class c = pathList.findClass(name, suppressedExceptions); if (c == null) { ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList); for (Throwable t : suppressedExceptions) { cnfe.addSuppressed(t); } throw cnfe; } return c; } 于是继续跟踪: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public Class findClass(String name, List<Throwable> suppressed) { //遍历dexElements数组 for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { //继续跟踪会发现调用的是一个native方法 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz; } } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; } 到现在就清晰了,当加载一个类的时候,会遍历dexElements数组,通过native方法从Element元素中加载类名相应的类 总结 到最后,总结整个multidex.install流程,其实很简单,就做了一件事情,把apk中的secondary dex文件通过ClassLoader转换成Element数组,并把输出的数组合与ClassLoader的Element数组合并。

2016/10/18
articleCard.readMore