Android dynamic analysis tool [Android] 3D layout analysis tool

https://blog.csdn.net/fancylovejava/article/details/45787729

https://blog.csdn.net/dunqiangjiaodemogu/article/details/72956291

Doraemon on Fliggy has always monitored over-drawing and layout depth. Unreasonable layout and over-drawing will affect page rendering speed. Although a lot of problems have been found, many of which can be seen on the red page as shown below, but it has been difficult to promote solutions, mainly for two reasons.

  1. It is really troublesome to let the development find a specific location, which is really troublesome, so I don't want to change it ;
  2. After the modification, will it affect the display effect of other controls? It is difficult to rely on the imagination of the brain alone, so I dare not change it ;

new tool

Thanks for the external open source reference tool provided by @ Rui Mu,
so there is a new tool in doraemon, which makes the layout of the current page 3D, a bit like the view ui hierarchy tool on xcode, as shown in the figure below. New tools can help analyze the plausibility of page layout and the key points that affect overdraw:

  1. Write the name (id) of each named control on the 3D page, so as to directly see which control (or the control's father) caused the problem, so as to quickly locate the specific control;
  2. On the 3D page, you can intuitively see the position of each control in the overall layout by dragging and multi-touch zooming in, and the impact on the related controls, so as to draw conclusions whether it can be changed or not;
  3. When developing and writing layout files, layout nested layouts are often used, so there is no global view, that is, the position of the layout currently being written is unknown in the whole. On the 3D page, you can clearly see whether the layout is reasonable and whether there is an unreasonable nested layout. Unreasonable layout leads to too deep nesting will cause crash ;

Analysis method (take the ticket home page as an example)

1. Turn on the overdraw switch

2. Make the ticket homepage layout 3D

Follow the opening method above, then enter the ticket home page, and then click the "3D" Icon, you can see the following picture. You can see that the background color of all controls is colored to indicate the depth of overdraw.

3. Identify the key controls that affect transition drawing

Looking inward from the outermost layout, the control that causes the background color mutation is set with the background color, as marked in the following figure. The background color changes of 5 and 6 are due to the loading of pictures, which can be left unmodified. We mainly look at the four places 1, 2, 3, and 4.

1. Mark 1 position

The following code brushes a full-screen white in the root layout. Irrationality: The title bar itself is also white, and within the pixel area of ​​the title bar, the full-screen white color of the root layout is redundant.

2. Mark the 2 position

The layout of the entire page can be seen as a title bar above and a list control (listview) below, which is painted in white again in the code, as shown in the following code:

3. Mark the 3 position

A white background is painted again in the cell unit code of the list, which is obviously redundant

4. Mark the 4 position

Another list cell unit is also painted with a white background, which is obviously redundant, and the e6 transparency in front is even more unnecessary.

4. Find the pain point and give a solution

  1. Remove the white background of the root layout and keep the white background of the listview
  2. Remove the white background of the cell in the listview

5. Comparison before and after preliminary optimization

The overdraw value was reduced from 4.04 to 2.63, an increase of 53.6%. The picture below is the color comparison before and after the initial optimization.

6. Layout rationality analysis

At the position pointed to by the yellow arrow below, 4 picture controls (ImageView) are placed side by side, using a 3-layer layout, which is questioned.

finally

The 3D layout tool combined with the transition drawing switch can effectively improve the positioning and overdrawing problem, and it is also easy to find redundant and unreasonable layouts to improve the native performance experience.

The following is the source code, welcome to discuss and build.

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();
    }
}

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325092209&siteId=291194637