安卓動態分析工具【Android】3D佈局分析工具

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

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

飛豬上的doraemon一直對過分繪製和佈局深度有監控,不合理的佈局和過深得過分繪製影響頁面渲染速度。雖然發現了很多問題,多處可見以下圖的紅紅的頁面,可是一直很難推進解決,主要有兩個緣由。xcode

  1. 讓開發找到具體的位置須要從根佈局一層層遍歷分析下去,確實麻煩,因此不想改
  2. 修改後,會不會影響到其餘控件的顯示效果,光靠大腦想象難保萬全,因此不敢改

新工具

感謝@睿牧提供的外部開源參考工具
因而doraemon裏就多了同樣新的工具,將當前頁面的佈局3D化,有點像xcode上的view ui hierarchy工具,以下圖所示。新工具能夠輔助分析頁面佈局的合理性和影響過分繪製的關鍵點:緩存

  1. 在3D化的頁面上將每一個有名字的控件的名字(id)都寫上了,便於直接看出是哪一個控件(或者控件的爸爸)致使問題,以便快速定位到具體的控件;
  2. 在3D化的頁面上經過拖拽和多點觸摸放大來直觀的看出每個控件在總體佈局裏所處的位置,和對相關控件的影響,便於下結論能不能改;
  3. 在開發寫佈局文件時,常常用到layout嵌套layout,因此沒有一個全局觀,即不知當前正在寫的佈局在總體裏的位置。在3D化的頁面上,可以清晰的看出佈局是否合理,是否有不合理的嵌套佈局存在。不合理的佈局致使過深得嵌套會引起crash

分析方法(這裏以門票首頁爲例)

1. 打開過分繪製開關

2. 將門票首頁佈局3D化

按照上面的打開方式,而後進入門票首頁,再點擊「3D」Icon,能夠看到以下圖。能夠看到全部控件的背景色都被塗上了標識過分繪製深度的顏色。
ide

3. 找出影響過渡繪製的關鍵控件

從最外層佈局向內看,致使背景色突變的控件是設置了背景色,以下圖標記。其中5和6的背景色變化是由於加載了圖片,這種狀況能夠不修改。咱們主要看下一、二、三、4這4個地方。
工具

1. 標記1位置

以下代碼,在根佈局裏刷了一次全屏的白色。不合理性:標題欄自己也是白色,所在標題欄的像素區域內,根佈局的全屏白色是多餘的。
佈局

2. 標記2位置

整個頁面的佈局能夠當作是上面一個標題欄,下面一個列表控件(listview),代碼中爲這個列表控件再一次刷了白色,以下代碼所示:
post

3. 標記3位置

list的cell單元代碼中再次刷了個白色底色,很顯然這是多餘的
性能

4. 標記4位置

又一個list的cell單元這裏也刷了個白色底色,很顯然這也是多餘的,前面的e6透明度更是畫蛇添足。
字體

4. 找到了痛點位置給出解決方案

  1. 去掉根佈局的白色底色,保留listview的白色底色
  2. 去掉listview中的cell的白色底色

5. 初步優化先後對比

過分繪製數值由原先的4.04下降到2.63,提高53.6%。下圖是初步優化先後顏色對比。

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();
    }
}
相關文章
相關標籤/搜索