https://blog.csdn.net/fancylovejava/article/details/45787729java
https://blog.csdn.net/dunqiangjiaodemogu/article/details/72956291canvas
飞猪上的doraemon一直对过分绘制和布局深度有监控,不合理的布局和过深得过分绘制影响页面渲染速度。虽然发现了很多问题,多处可见以下图的红红的页面,可是一直很难推进解决,主要有两个缘由。xcode
感谢@睿牧提供的外部开源参考工具
因而doraemon里就多了同样新的工具,将当前页面的布局3D化,有点像xcode上的view ui hierarchy工具,以下图所示。新工具能够辅助分析页面布局的合理性和影响过分绘制的关键点:缓存
按照上面的打开方式,而后进入门票首页,再点击“3D”Icon,能够看到以下图。能够看到全部控件的背景色都被涂上了标识过分绘制深度的颜色。ide
从最外层布局向内看,致使背景色突变的控件是设置了背景色,以下图标记。其中5和6的背景色变化是由于加载了图片,这种状况能够不修改。咱们主要看下一、二、三、4这4个地方。工具
以下代码,在根布局里刷了一次全屏的白色。不合理性:标题栏自己也是白色,所在标题栏的像素区域内,根布局的全屏白色是多余的。布局
整个页面的布局能够当作是上面一个标题栏,下面一个列表控件(listview),代码中为这个列表控件再一次刷了白色,以下代码所示:post
list的cell单元代码中再次刷了个白色底色,很显然这是多余的性能
又一个list的cell单元这里也刷了个白色底色,很显然这也是多余的,前面的e6透明度更是画蛇添足。字体
去掉listview中的cell的白色底色
过分绘制数值由原先的4.04下降到2.63,提高53.6%。下图是初步优化先后颜色对比。
以下黄色箭头指向的位置,4个图片控件(ImageView)并排放着,用了3层布局,对此表示质疑。
3D布局工具结合过渡绘制开关能够有效地提高定位过分绘制问题,此外还容易发现多余的不合理的布局,以提高native的性能体验。
下面是源码,欢迎讨论共建。
public class ScalpelFrameLayout extends FrameLayout { /** * 传入当前顶部的Activity */ public static void attachActivityTo3dView(Activity activity) { if (activity == null) { return; } ScalpelFrameLayout layout; ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); /** * 在ids.xml里定义一个id */ if (decorView.findViewById(R.id.id_scalpel_frame_layout) == null) { layout = new ScalpelFrameLayout(activity); layout.setId(R.id.id_scalpel_frame_layout); View rootView = decorView.getChildAt(0); decorView.removeView(rootView); layout.addView(rootView); decorView.addView(layout); } else { layout = (ScalpelFrameLayout) decorView.findViewById(R.id.id_scalpel_frame_layout); } if (!layout.isLayerInteractionEnabled()) { layout.setLayerInteractionEnabled(true); } else { layout.setLayerInteractionEnabled(false); } } /** * 标记位:当前多点触摸滑动方向未肯定 */ private final static int TRACKING_UNKNOWN = 0; /** * 标记位:当前多点触摸滑动方向是垂直方向 */ private final static int TRACKING_VERTICALLY = 1; /** * 标记位:当前多点触摸滑动方向是横向方向 */ private final static int TRACKING_HORIZONTALLY = -1; /** * 旋转的最大角度 */ private final static int ROTATION_MAX = 60; /** * 反方向旋转的最大角度 */ private final static int ROTATION_MIN = -ROTATION_MAX; /** * 默认X轴旋转角度 */ private final static int ROTATION_DEFAULT_X = -10; /** * 默认Y轴旋转角度 */ private final static int ROTATION_DEFAULT_Y = 15; /** * 默认缩放比例 */ private final static float ZOOM_DEFAULT = 0.6f; /** * 最小缩放比例 */ private final static float ZOOM_MIN = 0.33f; /** * 最大缩放比例 */ private final static float ZOOM_MAX = 2f; /** * 图层默认间距 */ private final static int SPACING_DEFAULT = 25; /** * 图层间最小距离 */ private final static int SPACING_MIN = 10; /** * 图层间最大距离 */ private final static int SPACING_MAX = 100; /** * 绘制id的文案的偏移量 */ private final static int TEXT_OFFSET_DP = 2; /** * 绘制id的文案的字体大小 */ private final static int TEXT_SIZE_DP = 10; /** * view缓存队列初始size */ private final static int CHILD_COUNT_ESTIMATION = 25; /** * 是否绘制view的内容,如TextView上的文字和ImageView上的图片 */ private boolean mIsDrawingViews = true; /** * 是否绘制view的id */ private boolean mIsDrawIds = true; /** * 打印debug log开关 */ private boolean mIsDebug = true; /** * view大小矩形 */ private Rect mViewBoundsRect = new Rect(); /** * 绘制view边框和id */ private Paint mViewBorderPaint = new Paint(ANTI_ALIAS_FLAG); private Camera mCamera = new Camera(); private Matrix mMatrix = new Matrix(); private int[] mLocation = new int[2]; /** * 用来记录可见view * 可见view须要绘制 */ private BitSet mVisibilities = new BitSet(CHILD_COUNT_ESTIMATION); /** * 对id转字符串的缓存 */ private SparseArray<String> mIdNames = new SparseArray<String>(); /** * 队列结构实现广度优先遍历 */ private ArrayDeque<LayeredView> mLayeredViewQueue = new ArrayDeque<LayeredView>(); /** * 复用LayeredView */ private Pool<LayeredView> mLayeredViewPool = new Pool<LayeredView>( CHILD_COUNT_ESTIMATION) { @Override protected LayeredView newObject() { return new LayeredView(); } }; /** * 屏幕像素密度 */ private float mDensity = 0f; /** * 对移动最小距离的合法性的判断 */ private float mSlop = 0f; /** * 绘制view id的偏移量 */ private float mTextOffset = 0f; /** * 绘制view id字体大小 */ private float mTextSize = 0f; /** * 3D视图功能是否开启 */ private boolean mIsLayerInteractionEnabled = false; /** * 第一个触摸点索引 */ private int mPointerOne = INVALID_POINTER_ID; /** * 第一个触摸点的坐标X */ private float mLastOneX = 0f; /** * 第一个触摸点的坐标Y */ private float mLastOneY = 0f; /** * 当有多点触摸时的第二个触摸点索引 */ private int mPointerTwo = INVALID_POINTER_ID; /** * 第二个触摸点的坐标X */ private float mLastTwoX = 0f; /** * 第二个触摸点的坐标Y */ private float mLastTwoY = 0f; /** * 当前多点触摸滑动方向 */ private int mMultiTouchTracking = TRACKING_UNKNOWN; /** * Y轴旋转角度 */ private float mRotationY = ROTATION_DEFAULT_Y; /** * X轴旋转角度 */ private float mRotationX = ROTATION_DEFAULT_X; /** * 缩放比例,默认是0.6 */ private float mZoom = ZOOM_DEFAULT; /** * 图层之间距离,默认是25单位 */ private float mSpacing = SPACING_DEFAULT; public ScalpelFrameLayout(Context context) { super(context, null, 0); mDensity = getResources().getDisplayMetrics().density; mSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); mTextSize = TEXT_SIZE_DP * mDensity; mTextOffset = TEXT_OFFSET_DP * mDensity; mViewBorderPaint.setStyle(STROKE); mViewBorderPaint.setTextSize(mTextSize); if (Build.VERSION.SDK_INT >= JELLY_BEAN) { mViewBorderPaint.setTypeface(Typeface.create("sans-serif-condensed", NORMAL)); } } /** * 设置是否让当前页面布局3D化 * 使用该方法前先调用attachActivityTo3dView方法 * * @param enabled */ public void setLayerInteractionEnabled(boolean enabled) { if (mIsLayerInteractionEnabled != enabled) { mIsLayerInteractionEnabled = enabled; setWillNotDraw(!enabled); invalidate(); } } /** * 当前页面布局是否已经3D化 * * @return */ public boolean isLayerInteractionEnabled() { return mIsLayerInteractionEnabled; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mIsLayerInteractionEnabled || super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { if (!mIsLayerInteractionEnabled) { return super.onTouchEvent(event); } int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: int index = action == ACTION_DOWN ? 0 : event.getActionIndex(); if (mPointerOne == INVALID_POINTER_ID) { mPointerOne = event.getPointerId(index); mLastOneX = event.getX(index); mLastOneY = event.getY(index); if (mIsDebug) { log("Got pointer 1! id: %s x: %s y: %s", mPointerOne, mLastOneY, mLastOneY); } } else if (mPointerTwo == INVALID_POINTER_ID) { mPointerTwo = event.getPointerId(index); mLastTwoX = event.getX(index); mLastTwoY = event.getY(index); if (mIsDebug) { log("Got pointer 2! id: %s x: %s y: %s", mPointerTwo, mLastTwoY, mLastTwoY); } } else { if (mIsDebug) { log("Ignoring additional pointer. id: %s", event.getPointerId(index)); } } break; case MotionEvent.ACTION_MOVE: if (mPointerTwo == INVALID_POINTER_ID) { /** * 单触点滑动是控制3D布局的旋转角度 */ int i = 0; int count = event.getPointerCount(); while (i < count) { if (mPointerOne == event.getPointerId(i)) { float eventX = event.getX(i); float eventY = event.getY(i); float dx = eventX - mLastOneX; float dy = eventY - mLastOneY; float drx = 90 * (dx / getWidth()); float dry = 90 * (-dy / getHeight()); /** * 屏幕上X的位移影响的是坐标系里Y轴的偏移角度,屏幕上Y的位移影响的是坐标系里X轴的偏移角度 * 根据实际位移结合前面定义的旋转角度区间算出应该旋转的角度 */ mRotationY = Math.min(Math.max(mRotationY + drx, ROTATION_MIN), ROTATION_MAX); mRotationX = Math.min(Math.max(mRotationX + dry, ROTATION_MIN), ROTATION_MAX); if (mIsDebug) { log("Single pointer moved (%s, %s) affecting rotation (%s, %s).", dx, dy, drx, dry); } mLastOneX = eventX; mLastOneY = eventY; invalidate(); } i++; } } else { /** * 多触点滑动是控制布局的缩放和图层间距 */ int pointerOneIndex = event.findPointerIndex(mPointerOne); int pointerTwoIndex = event.findPointerIndex(mPointerTwo); float xOne = event.getX(pointerOneIndex); float yOne = event.getY(pointerOneIndex); float xTwo = event.getX(pointerTwoIndex); float yTwo = event.getY(pointerTwoIndex); float dxOne = xOne - mLastOneX; float dyOne = yOne - mLastOneY; float dxTwo = xTwo - mLastTwoX; float dyTwo = yTwo - mLastTwoY; /** * 首先判断是垂直滑动仍是横向滑动 */ if (mMultiTouchTracking == TRACKING_UNKNOWN) { float adx = Math.abs(dxOne) + Math.abs(dxTwo); float ady = Math.abs(dyOne) + Math.abs(dyTwo); if (adx > mSlop * 2 || ady > mSlop * 2) { if (adx > ady) { mMultiTouchTracking = TRACKING_HORIZONTALLY; } else { mMultiTouchTracking = TRACKING_VERTICALLY; } } } /** * 若是是垂直滑动调整缩放比 * 若是是横向滑动调整层之间的距离 */ if (mMultiTouchTracking == TRACKING_VERTICALLY) { if (yOne >= yTwo) { mZoom += dyOne / getHeight() - dyTwo / getHeight(); } else { mZoom += dyTwo / getHeight() - dyOne / getHeight(); } /** * 算出调整后的缩放比例 */ mZoom = Math.min(Math.max(mZoom, ZOOM_MIN), ZOOM_MAX); invalidate(); } else if (mMultiTouchTracking == TRACKING_HORIZONTALLY) { if (xOne >= xTwo) { mSpacing += (dxOne / getWidth() * SPACING_MAX) - (dxTwo / getWidth() * SPACING_MAX); } else { mSpacing += (dxTwo / getWidth() * SPACING_MAX) - (dxOne / getWidth() * SPACING_MAX); } /** * 算出调整后的图层间距 */ mSpacing = Math.min(Math.max(mSpacing, SPACING_MIN), SPACING_MAX); invalidate(); } if (mMultiTouchTracking != TRACKING_UNKNOWN) { mLastOneX = xOne; mLastOneY = yOne; mLastTwoX = xTwo; mLastTwoY = yTwo; } } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: index = action != ACTION_POINTER_UP ? 0 : event.getActionIndex(); int pointerId = event.getPointerId(index); if (mPointerOne == pointerId) { /** * 多触点状态切换到单触点状态 * 即若是原先是调整缩放和图层间距的状态,放开一个手指后转为控制图层旋转状态 */ mPointerOne = mPointerTwo; mLastOneX = mLastTwoX; mLastOneY = mLastTwoY; if (mIsDebug) { log("Promoting pointer 2 (%s) to pointer 1.", mPointerTwo); } /** * reset多触点状态 */ mPointerTwo = INVALID_POINTER_ID; mMultiTouchTracking = TRACKING_UNKNOWN; } else if (mPointerTwo == pointerId) { if (mIsDebug) { log("Lost pointer 2 (%s).", mPointerTwo); } /** * reset多触点状态 */ mPointerTwo = INVALID_POINTER_ID; mMultiTouchTracking = TRACKING_UNKNOWN; } break; default: break; } return true; } @Override public void draw(Canvas canvas) { if (!mIsLayerInteractionEnabled) { super.draw(canvas); return; } getLocationInWindow(mLocation); /** * 页面左上角坐标 */ float x = mLocation[0]; float y = mLocation[1]; int saveCount = canvas.save(); /** * 页面中心坐标 */ float cx = getWidth() / 2f; float cy = getHeight() / 2f; mCamera.save(); /** * 先旋转 */ mCamera.rotate(mRotationX, mRotationY, 0F); mCamera.getMatrix(mMatrix); mCamera.restore(); mMatrix.preTranslate(-cx, -cy); mMatrix.postTranslate(cx, cy); canvas.concat(mMatrix); /** * 再缩放 */ canvas.scale(mZoom, mZoom, cx, cy); if (!mLayeredViewQueue.isEmpty()) { throw new AssertionError("View queue is not empty."); } { int i = 0; int count = getChildCount(); while (i < count) { LayeredView layeredView = mLayeredViewPool.obtain(); layeredView.set(getChildAt(i), 0); mLayeredViewQueue.add(layeredView); i++; } } /** * 广度优先进行遍历 */ while (!mLayeredViewQueue.isEmpty()) { LayeredView layeredView = mLayeredViewQueue.removeFirst(); View view = layeredView.mView; int layer = layeredView.mLayer; /** * 在draw期间尽可能避免对象的反复建立 * 回收LayeredView一会再复用 */ layeredView.clear(); mLayeredViewPool.restore(layeredView); /** * 隐藏viewgroup内可见的view */ if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; mVisibilities.clear(); int i = 0; int count = viewGroup.getChildCount(); while (i < count) { View child = viewGroup.getChildAt(i); /** * 将可见的view记录到mVisibilities中 */ if (child.getVisibility() == VISIBLE) { mVisibilities.set(i); child.setVisibility(INVISIBLE); } i++; } } int viewSaveCount = canvas.save(); /** * 移动出图层的距离 */ float translateShowX = mRotationY / ROTATION_MAX; float translateShowY = mRotationX / ROTATION_MAX; float tx = layer * mSpacing * mDensity * translateShowX; float ty = layer * mSpacing * mDensity * translateShowY; canvas.translate(tx, -ty); /** * 画view的边框 */ view.getLocationInWindow(mLocation); canvas.translate(mLocation[0] - x, mLocation[1] - y); mViewBoundsRect.set(0, 0, view.getWidth(), view.getHeight()); canvas.drawRect(mViewBoundsRect, mViewBorderPaint); /** * 画view的内容 */ if (mIsDrawingViews) { view.draw(canvas); } /** * 画view的id */ if (mIsDrawIds) { int id = view.getId(); if (id != NO_ID) { canvas.drawText(nameForId(id), mTextOffset, mTextSize, mViewBorderPaint); } } canvas.restoreToCount(viewSaveCount); /** * 把刚刚应该显示但又设置了不可见的view从队列里取出来,后面再绘制 */ if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; int i = 0; int count = viewGroup.getChildCount(); while (i < count) { if (mVisibilities.get(i)) { View child = viewGroup.getChildAt(i); child.setVisibility(VISIBLE); LayeredView childLayeredView = mLayeredViewPool.obtain(); childLayeredView.set(child, layer + 1); mLayeredViewQueue.add(childLayeredView); } i++; } } } canvas.restoreToCount(saveCount); } /** * 根据id值反算出在布局文件中定义的id名字 * * @param id * @return */ private String nameForId(int id) { String name = mIdNames.get(id); if (name == null) { try { name = getResources().getResourceEntryName(id); } catch (NotFoundException e) { name = String.format("0x%8x", id); } mIdNames.put(id, name); } return name; } private static void log(String message, Object... object) { TLog.i("Scalpel", String.format(message, object)); } private static class LayeredView { private View mView = null; /** * mView所处的层级 */ private int mLayer = 0; void set(View view, int layer) { mView = view; mLayer = layer; } void clear() { mView = null; mLayer = -1; } } private static abstract class Pool<T> { private Deque<T> mPool; Pool(int initialSize) { mPool = new ArrayDeque<T>(initialSize); for (int i = 0; i < initialSize; i++) { mPool.addLast(newObject()); } } T obtain() { return mPool.isEmpty() ? newObject() : mPool.removeLast(); } void restore(T instance) { mPool.addLast(instance); } protected abstract T newObject(); } }