单独加载android的view库 - CSDN
  • http://www.jianshu.com/p/254dc5ddffea
    展开全文
  • 由于最近在Android TV的项目开发中遇到一个问题,需要对某个view进行多次的按键监听,而我们都知道,通过调用setOnKeyListener实现了OnKeyListener接口之后我们就可以实现对按键进行监听,但是却不能够多次注册通过...

      由于最近在Android TV的项目开发中遇到一个问题,需要对某个view进行多次的按键监听,而我们都知道,通过调用setOnKeyListener实现了OnKeyListener接口之后我们就可以实现对按键进行监听了。
    比如有如下代码:

    view.setOnKeyListener(new OnKeyListener() {
                @Override
                public boolean onKey(View v, int keyCode, KeyEvent event) {
                    // TODO Auto-generated method stub
                    Log.i(TAG, "keyCode:" + keyCode);
                    if (event.getAction() == KeyEvent.ACTION_DOWN) {
                        switch (keyCode) {
                        case KeyEvent.KEYCODE_DPAD_LEFT:
                            //TO DO
                            return true;
                        case KeyEvent.KEYCODE_DPAD_RIGHT:
                            //TO DO
                            return false;
                        default:
                            break;
                        }
                    }
                    return false;
                }
            });

      我们同过实现这几个view的按键监听事件,通过keyCode来判断是哪个按键,在按下的时候做出某些响应。看上去也没什么毛病。

      但是问题来了,在我前文中监听了这个view的左右按键之后,我又想监听关于他的上下键那该怎么办?总不能我再一次调用setOnKeyListener实现了OnKeyListener接口,然后实现这个按键的监听事件吧?那这样子前文中实现的左右按键的监听就被覆盖了。

      通过源码我们也可以看得出来:

      /**
         * Register a callback to be invoked when a hardware key is pressed in this view.
         * Key presses in software input methods will generally not trigger the methods of
         * this listener.
         * @param l the key listener to attach to this view
         */
        public void setOnKeyListener(OnKeyListener l) {
            getListenerInfo().mOnKeyListener = l;
        }

      实际上我们实现的OnKeyListener接口就是赋值给了getListenerInfo().mOnKeyListener,而再调用一次setOnKeyListener那么上一个实现的监听就被覆盖了,那怎么办呢?

    问题的根本原因在于:
      对于按键监听来说,View只是提供对按键的监听,并没有提供对单个按键的监听(当然有些键除外,比如确定按钮).

      那有没有可能自定义一个View,实现只监听某个按键的事件呢,答案肯定是有的。
    我们先来说一下思路,然后再具体写出代码。思路如下:

    1、我们要实现对某个按键进行监听,就应该要自定义View提供一个addKeyListener()的方法可以来加入一个按键监听;

    2、addKeyListener方法应该是接收一个接口实现对象,接口的定义必须至少包含有得到按键码和按键事件这两个方法。我们才能通过实现这个接口从而编写这个按键码的事件;

    3、由于要注册的按键有若干个,我们应该要选择一个集合来存储他们,这里可以通过一个Map集合,把按键码作为key,接口实现作为value,从而得到某个key的监听实现;

    4、自定义View应该是默认实现了setOnKeyListener,然后我们在OnkeyListener的接口实现中,来响应我们在Map集合中注册的某个按键监听,为了防止外界调用setOnKeyListener而被覆盖,我们要把setOnKeyListener的原本实现给屏蔽掉。

    基于以上的分析过程,让我们来试试吧
    首先先自定义一个View,自定义View需要继承View的构造方法:

    public class MyView extends ImageView {
        public MyView(Context context) {
            super(context);
        }
        public MyView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
        public MyView(Context context,AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
    }

      然后我们在定义一个接口,这里为了区分OnkeyListener接口,我就把它命名为OnKeyLogicListener吧,定义如下:

        /**
         * 某个按键的监听类
         */
        public interface OnKeyLogicListener {
            /**
             *  按键的处理逻辑
             */
            boolean onKeyLogic(android.view.View v);
            /**
             * 指定按键,比如 KeyEvent.KEYCODE_DPAD_DOWN
             * @return 按键码
             */
            int getKeyCode();
        }

      接口很清晰,就是通过实现这个接口,在getKeyCode方法中返回按键码,在onKeyLogic中编写事件逻辑,

      然后,我们选则一个Map集合来存储这个按键码到接口的映射关系,这里采用HashMap的话,编译器会警告我们说,使用SparseArray来代替HashMap,什么是SparseArray呢,它比HashMap好在哪里呢,在这里不是重点就不多说了。因为我们这里的key是整数型,而SparseArray里面是提供了整数型的key到value的映射关系(只能是int,比如key是String的话那就不能用SparseArray)

    好了,我们在自定义View里面来定义一个成员变量:

    SparseArray<OnKeyLogicListener> keyMapListener;

      接下来我们在自定义View里面来编写一个内部的OnkeyListener实现类,如下:

    class OnMyKeyListener implements android.view.View.OnKeyListener {
    
    
            @Override
            public boolean onKey(android.view.View v, int keyCode, KeyEvent event) {
                // TODO Auto-generated method stub
                Log.i(TAG, "OnMyKeyListener keyCode="+keyCode+",event="+event);
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    OnKeyLogicListener listener =keyMapListener==null?null: keyMapListener.get(keyCode);
                    return listener==null?false:listener.onKeyLogic(v);
    
            }
                return false;
            }
        }

      看到这里的实现,读者应该就比较明白了吧。正是因为我们自己来实现onKeyListener,然而onKeyListener只是对集合里面注册的按键事件进行反馈,比如我集合里面有个上键,那么通过查找集合里面的记录,如果有对上键的注册则会执行listener.onKeyLogic(v),没有的话则直接返回false。

      接下来在自定义View里面编写添加按键监听的方法:addKeyLogicListener,方法实现如下:

    public void addKeyLogicListener(OnKeyLogicListener listener) {
            if (onMyKeyListener == null) {
                onMyKeyListener = new OnMyKeyListener();
                super.setOnKeyListener(onMyKeyListener);
            }
            if(keyMapListener==null){
                keyMapListener = new SparseArray<MyView.OnKeyLogicListener>();
            }
            keyMapListener.put(listener.getKeyCode(), listener);
        }

      该方法首先判断onMyKeyListener 是否为空,onMyKeyListener 就是上面我们写的OnMyKeyListener接口实现类。用的时候才加载监听,所以会先判断一下为空则先设置本身的按键监听,。同时也要判断一下集合是否为空则new出来一个,然后把按键码作为key,接口作为value保存在集合中。

      最后为了不被外界调用setOnkeyListenr后覆盖了我们本来做的接口实现,那么可以重写setOnkeyListenr方法,把方法的实现屏蔽掉即可。

      到这里我们就基本完成了对任意单个按键的注册了。比如我在Activity中,要注册上键的监听,那么很简单。只需要调用addKeyLogicListener方法,做法如下:

        view.addKeyLogicListener(new OnKeyLogicListener() {
                @Override
                public boolean onKeyLogic(View v) {
                    // TODO Auto-generated method stub
                    return false;
                }
                //返回上键的按键码
                @Override
                public int getKeyCode() {
                    // TODO Auto-generated method stub
                    return KeyEvent.KEYCODE_DPAD_UP;
                }
            });

      这样就可以实现单个按键的监听了,如需再注册下键、左右键则再一次调用即可。
    说到这里,我们确实是已经实现了标题说的对某个按键的监听,但是我为什么要说是“基本完成呢”。

      不知道读者有没有注意到,在上面的代码中,我们为了实现某个监听,而屏蔽掉了原本的按键监听,这会带来什么影响呢?

      假设,我的项目现在来了新需求,需要对这个view单独处理上下左右的按键,然后对其他按键实现统一监听处理。

      这如何是好啊?坑爹的需求这不是让我尴尬么?

      那,能不能做到外界既可以调用setOnKeyListener来处理按键,又可以调用addKeyLogicListener来单独处理某个按键呢? 答案是肯定的。

      那我们前面的分析思路就应该做出一些改变了。

      关键在于我们内部实现的OnMyKeyListener的处理逻辑,我们可以在定义一个OnKeyListener变量,来存放外界通过调用setOnKeyListener而传进来的OnKeyListener接口实现类,然后在OnMyKeyListener类中的实现逻辑可以是:

      如果,集合中有这个按键,那么处理OnKeyLogicListener实现的按键事件,如果没有,那么把这个按键的响应转发到外界实现的OnKeyListener去(如果外界调用了setOnkeyListener方法的话)。

      考虑一种情况,如果集合中有这个按键,而外界通过调用setOnkeyListener后也实现了这个按键事件,那么怎么处理呢,当然这里事件分发规则都是我们自己来定义的,这里我们可以定义成优先处理OnMyKeyListener,再通过onKeyLogic方法的返回值来决定要不要把事件也传给OnKeyListener。

      因此,基于以上的分析,我们把原本的OnMyKeyListener类的方法修改一下,变成如下:

    /***
     * OnKeyListener实现类
     * 会优先响应{@link #OnKeyLogicListener}接口的按键处理
     * 再根据返回值决定要不要响应{@link #setOnKeyListener()}接口
     *
     */
        class OnMyKeyListener implements android.view.View.OnKeyListener {
    
    
            @Override
            public boolean onKey(android.view.View v, int keyCode, KeyEvent event) {
                // TODO Auto-generated method stub
                Log.i(TAG, "OnMyKeyListener keyCode="+keyCode+",event="+event);
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    OnKeyLogicListener listener =keyMapListener==null?null: keyMapListener.get(keyCode);
    
                    Log.i(TAG, "OnMyKeyListener listener is null? "+(listener==null));
    
                    //如果OnKeyLogicListener为空则响应原始的按键监听
                    if (listener == null ) {
                        return OnOriginalKey(v, keyCode, event);
                    }
    
                    //OnKeyLogicListener不为空则优先被响应,其后再决定是否传递事件到原始的按键监听
                    if(!listener.onKeyLogic(v)){
                        return OnOriginalKey(v, keyCode, event);
                    }else{
                        return true;
                    }
                }else{
                    //ACTION_UP动作还是被原来的监听
                    return OnOriginalKey(v, keyCode, event);
                }
            }
        }
        /**
         * 原本的按键响应
         */
        private boolean OnOriginalKey(android.view.View v, int keyCode, KeyEvent event) {
            return onKeyListener != null ? onKeyListener.onKey(v,
                    keyCode, event) : false;
        }

      在上面我也写了注释,相信不难理解,这个处理逻辑就是:
      如果集合里面没有这个按键,那么就传递到“原本的按键监听“(指的就是我们原本通过setOnkeyListener设置的监听).

      如果集合有这个按键,那么优先处理这个按键的事件,其次如果事件返回了false,那么才会再传递到“原本的按键监听“,

      下面的这个OnOriginalKey这是对onKeyListener 的处理逻辑做一个封装,也就是如果onKeyListener 不为空则把请求传递到onKeyListener上。

      当然这个onKeyListener我们也要定义在我们的自定义View里面的成员变量,最后我们还得修改一下setOnkeyListener的逻辑:

        /**
         * 还是可以用原始的按键监听接口
         */
        @Override
        public void setOnKeyListener(android.view.View.OnKeyListener l) {
            // TODO Auto-generated method stub
            if (l != null) {
                onKeyListener = l;
            }
            //实际上注册的是OnMyKeyListener实现类
            if(onMyKeyListener==null){
                onMyKeyListener = new OnMyKeyListener();
                super.setOnKeyListener(onMyKeyListener);
            }
            // super.setOnKeyListener(l);
        }

      可以看到,其实外界传入进来的OnKeyListener接口实现类只是做一个赋值,并没有真正转到super.setOnKeyListener(l)去,而是传了onMyKeyListener。

      所以对于View的机制来说,本质上监听的还是原来的那个 getListenerInfo().mOnKeyListener。只不过我们做了一层事件分发。

    最后给出这个自定义view的代码:

    public class HomeImgView extends ImageView {
        private static final String TAG = HomeImgView.class.getSimpleName();
        OnMyKeyListener onMyKeyListener;
        OnKeyListener onKeyListener;
        SparseArray<OnKeyLogicListener> keyMapListener;
        public HomeImgView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    
        public HomeImgView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public HomeImgView(Context context) {
            super(context);
        }
    
        /**
         * 还是可以用原始的按键监听接口
         */
        @Override
        public void setOnKeyListener(android.view.View.OnKeyListener l) {
            // TODO Auto-generated method stub
            if (l != null) {
                onKeyListener = l;
            }
            //实际上注册的是OnMyKeyListener实现类
            if(onMyKeyListener==null){
                onMyKeyListener = new OnMyKeyListener();
                super.setOnKeyListener(onMyKeyListener);
            }
            // super.setOnKeyListener(l);
        }
    
        */
        /**
         * 使用该方法添加的按键监听,会比{@link #setOnKeyListener()}被优先响应
         * @param listener
         */
        public void addKeyLogicListener(OnKeyLogicListener listener) {
            if (onMyKeyListener == null) {
                onMyKeyListener = new OnMyKeyListener();
                super.setOnKeyListener(onMyKeyListener);
            }
            if(keyMapListener==null){
                keyMapListener = new SparseArray<HomeImgView.OnKeyLogicListener>();
            }
            keyMapListener.put(listener.getKeyCode(), listener);
        }
        /**
         * 某个按键的监听类
         */
        public interface OnKeyLogicListener {
            /**
             *  按键的处理逻辑
             * @return  返回true则不再响应该按键在{@link #setOnKeyListener()}的处理
             */
            boolean onKeyLogic(android.view.View v);
            /**
             * 指定按键,比如 KeyEvent.KEYCODE_DPAD_DOWN
             * @return 按键码
             */
            int getKeyCode();
        }
    /***
     * OnKeyListener实现类
     * 会优先响应{@link #OnKeyLogicListener}接口的按键处理
     * 再根据返回值决定要不要响应{@link #setOnKeyListener()}接口
     *
     */
        class OnMyKeyListener implements android.view.View.OnKeyListener {
    
    
            @Override
            public boolean onKey(android.view.View v, int keyCode, KeyEvent event) {
                // TODO Auto-generated method stub
                Log.i(TAG, "OnMyKeyListener keyCode="+keyCode+",event="+event);
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    OnKeyLogicListener listener =keyMapListener==null?null: keyMapListener.get(keyCode);
    
                    Log.i(TAG, "OnMyKeyListener listener is null? "+(listener==null));
    
                    //如果OnKeyLogicListener为空则响应原始的按键监听
                    if (listener == null ) {
                        return OnOriginalKey(v, keyCode, event);
                    }
    
                    //OnKeyLogicListener不为空则优先被响应,其后再决定是否传递事件到原始的按键监听
                    if(!listener.onKeyLogic(v)){
                        return OnOriginalKey(v, keyCode, event);
                    }else{
                        return true;
                    }
                }else{
                    //ACTION_UP动作还是被原来的监听
                    return OnOriginalKey(v, keyCode, event);
                }
            }
        }
        /**
         * 原本的按键响应
         */
        private boolean OnOriginalKey(android.view.View v, int keyCode, KeyEvent event) {
            return onKeyListener != null ? onKeyListener.onKey(v,
                    keyCode, event) : false;
        }
    }
    
    展开全文
  • Android View性能优化

    千次阅读 2018-04-15 16:32:32
    最近被公司外派到兄弟公司支援某个app的重构,业务重构过程中,有测试反馈说重构后的版本相比之前的版本出现了严重的卡顿问题,于是开始了View层级的性能优化。本篇文章主要是讲述View性能的发现、调试,不详细介绍...

          最近被公司外派到兄弟公司支援某个app的重构,业务重构过程中,有测试反馈说重构后的版本相比之前的版本出现了严重的卡顿问题,于是开始了View层级的性能优化。本篇文章主要是讲述View性能的发现、调试,不详细介绍View渲染的原理、调试工具如何使用等。不过本文会给出相应知识点的相关链接,大家如需详细了解,可点击进入对应文章。

           本文从简略讲述View的渲染原理和三种常用ViewGroup性能分析出发,然后介绍发现APP运行过程中各种View性能检测的工具,并详细介绍如何在过度渲染、频繁渲染、渲染范围、布局层级、UI工作量等方面进行优化。

           每个View的渲染都包括Measure、Layout和Draw三个过程,其中Measure用来计算View的大小,Layout用来计算View的位置,Draw用来绘制View,每个步骤的具体流程可参见链接:http://www.cnblogs.com/jycboy/p/6066654.html

    View的渲染原理

    单个View的渲染

         每个View的渲染都包括Measure、Layout和Draw三个过程,其中Measure用来计算View的大小,Layout用来计算View的位置,Draw用来绘制View,每个步骤的具体流程可参见链接:http://www.cnblogs.com/jycboy/p/6066654.html    

         Measure和Layout过程我们在这不做描述,简单说下Draw的内容:
    1. 绘制自身背景
    2. 绘制自身内容
    3. 绘制子控件
    4. 控制滚动条、阴影等部分
        为了提高View渲染效率,应该尽可能减少View的渲染内容,比如自身背景。

    View的渲染流程

        单独为一个View执行刷新操作,并不会仅仅更新该View,而是会逐级上报,一直到rootview,并通过各种条件判断是否需要重新绘制其它View,有此可以看出,稍微使用不当,会很容易导致全屏渲染,而我们发现的性能问题,也即是该原因引起的。为了能更好的了解优化方案,掌握了解View渲染的流程是非常有必要的。


        每次对View的刷新都会执行以上流程,详细情况请参见https://www.cnblogs.com/jycboy/p/6219915.htmlhttp://www.cnblogs.com/jycboy/p/6066654.html

    ViewGroup的性能分析

        我们知道View通常都是依附ViewGroup存在,ViewGroup中存放View的个数以及相互依赖也是影响性能的一个重要原因。我们常用的ViewGroup为FrameLayout、LinearLayout和RelativeLayout,这三种Layout在布局时处理方式不同,所带来的效率也肯定存在差异。本文不详细介绍三种Layout的区别以及在渲染过程中的处理流程,这些大家应该都有深刻的理解,或者参考链接:https://blog.csdn.net/hejjunlin/article/details/51159419,在这我们仅仅描述一下三种Layout的性能情况,并给出使用建议,帮助在开发过程中使用最适合的Layout。下表是三种Layout的性能比较。

    LayoutMeasure次数计算效率t特点
    RelativeLayout2需要计算水平和垂直两个方向的依赖关系,效率较差布局灵活,能够使用较少层级实现复杂布局,减少遍历的深度,但单层级计算效率低
    LinearLayout1,存在weight或measureWithLargestChild属性时计算2次仅计算水平或垂直单个方向的依赖关系,效率较好计算效率高,但实现复杂布局,必须多层嵌套
    FrameLayout2第一次计算最大子控件大小,第二次调整其它match_parent属性的子控件布局简单,计算效率高,但无法实现复杂布局

    通过上面的比较分析,本文建议:

    1. 在三种布局在相同层次下能够实现同样效果时,使用优先级为FrameLayout>=LinearLayout>RelativeLayout
    2. 对复杂的UI,建议使用RelativeLayout,用来减少层级,提高遍历效率
    3. 使用LinearLayout时,慎用weight属性和measureWithLargestChild属性,会引起2次渲染
    4. 尽量减少RelativeLayout中包含太多的无依赖的控件数量。有的时候,RelativeLayout中分为上下部分控件,但由于每部分控件数量都较多,在使用时非常有可能因为上部分某个控件的改变导致下半部分的控件也进行无效刷新。这种情况下,建议将这些无依赖的控件依附相同层级的Layout,降低代码风险。

    View性能调试

        前章节简单讲述了View的渲染原理和流程,希望能够帮助大家掌握View的基础知识,这部分也基本上是面试必考题。解决某个问题之前,流程通常为:发现问题->分析问题->确定问题->解决问题->验证问题。那我们就先从发现问题开始,如何来发现性能中存在的问题。

    发现问题

        研发开发过程中,由于关注点用于开发时的单个业务上,所以很容易忽略性能上的问题,即使在开发过程中发现了卡顿问题,但由于业务紧张,也不会放下当前的工作去处理性能方面的问题。所以性能问题通常是由QA,甚至是真实用户提出。

    分析问题

        问题发生了,接下来就得分析。为了分析性能上的问题,本文整理了性能分析的各种工具,帮助我们确定问题。

        引起卡顿的原因从细节上分包括:过度绘制,频繁渲染,布局层级深,刷新范围大,UI线程工作量大,内存抖动等原因。接下来,我们对每一项都进行测试分析。

    过度绘制(OverDraw)

        过度绘制是指屏幕中某个范围被多次渲染,比如父控件设置了背景色,子控件设置了图片显示或者文本显示,这样在子控件的对应区域,就会渲染两次。每次渲染都会带来性能消耗,同一区域渲染的次数越多,那么带来的消耗就越大。检测OverDraw,可通过打开手机“开发者选项”->"调试GPU过度绘制开关",就可以在手机屏幕上查看绘制情况。如下图所示。


        颜色越深代表绘制的次数越大,但一个屏幕大部分都被粉丝或红色占据时,我们就必须考虑优化了。

    刷新频率和刷新范围

        手机的帧率为60fps,那么每帧渲染的周期为16ms。当屏幕不存在UI更新时,每一帧并不需要处理,但如果UI发生改变了,就会触发View的整个刷新流程。频繁的界面刷新,会导致手机电量的消耗,如果存在UI滑动的操作,也有可能导致滑动时的卡顿。所以在优化过程中,应该尽量降低View的刷新频率。检查刷新频率,可通过打开手机“开发者选项”中的“显示GPU视图更新”和“GPU呈现模式分析”两个开关进行分析。前者开关打开后,当屏幕中某个区域发生UI刷新时,会闪烁红色,通过该工具就能知道UI的刷新范围。

        “GPU呈现模式分析”开启选择“柱状视图”后,会在屏幕底部显示每一帧性能的详细情况。如下图所示。


        左上角的红色区域因为wifi信号的刷新而导致闪烁,底部的柱状表示每一帧的性能情况,其中每种颜色表示渲染的每一步骤的消耗时间,水平方向的绿线表示16ms,超过水平线的帧是导致卡顿的关键位置,是值得优化的地方。但从实际执行情况看,想确保每一帧都在16ms以下是不现实的,只能尽量减少。柱状图中每种颜色的含义如下:

     

        优化过程中,我们重点优化蓝绿占比比较高的帧。

    布局层级

        前面文章提到,View的渲染过程会遍历ViewTree,如果tree的层级越深,那么带来的消耗就越大,所以在实现UI布局的时候,我们尽量减少删除不必要的ViewGroup,减少布局层级。层级的检测工具可以使用Android Studio中的Layout Inspector和Hierarchy View。Layout Inspector的工具如下:


        Hierarchy View不仅能够展示布局的层次,而且能够计算每个节点的耗时情况,更能帮助找到耗时的关键View。但是该工具只能用于root的手机或者模拟器,如果想在非root手机上使用,需要自己增加ViewServer,具体使用方法可参加:https://www.cnblogs.com/hoolay/p/6248514.html


    UI线程工作量大

        Android中UI更新必须要在主线程中执行(SurfaceView外),如果主线程执行了耗时操作,比如IO操作,大量for循环,或者和子线程共享一把锁,都有可能引起应用卡顿,甚至出现anr。可使用Systrace或TraceView进行跟踪分析。

        Systrace可以收集系统和应用的数据信息,生成的HTML报告中能够提示异常帧,并通过放大后,看到每一帧的执行细节和时间消耗,通过该步骤可以分析出耗时所在。之前我们通过这个工具分析出一个200ms的字符集初始耗时,大幅度提高首屏的启动时间。使用方法可参考https://blog.csdn.net/hfreeman2008/article/details/53538155


        TraceView和Systrace相似,但是该工具能够提供更加详细的执行信息,不仅仅包含本方法的执行情况,也包括调用方法和被调用发的执行情况,使用方法可参考https://blog.csdn.net/u011240877/article/details/54347396


    内存抖动

        内存检测工具我们可以通过Android Studio的Monitor工具以及System Log检测。Monitor工具能够实时给出app占用内存的情况,当内存曲线呈现频繁波浪状时,就存在内存抖动的情况,另外,log中一直提示gc的有关信息,也有可能出现了内存抖动。内存的分析在本文中不做详细介绍,大家记住不要在for循环或者getView, onBindViewHolder等方法中频繁new内存,尽量做到内存复用。

    确定问题

    UI中控件较多,每个控件都有可能引起效率问题,仅通过分析工具,虽然能够看到性能展示,但通常无法直接确定问题原因,我们还需要一些其他的技术手段来辅助我们确定问题,可以参考以下几个手段进行:

    1. 单一View调试。UI布局中View较多,很难确定渲染频繁、渲染范围大是哪个控件更新引起的,这个时候可以通过删除其它View的加载,只保留目标View,跟踪该View在更新时带来的渲染影响,实现各个击破。有的时候TextView的text更新就会引起渲染问题。
    2. 单一业务调试。某个View或者某组View的更新可能跟多个业务相关,可通过注释其它业务代码,仅保留单一业务代码进行调试,寻找业务优化方案。比如当某个业务存在频繁更新UI的情况,我们可以通过比对前后数据是否发生改变,或者延迟刷新等手段来优化UI刷新频率。
    3. 自定义关键ViewGroup或View,尤其是根View。在自定义类中关键方法(onMeasure、onLayout、onDraw)中增加Log,打印关键方法的耗时情况和执行次数,能够方便统计出一次行为所带来的刷新次数,帮助发现不合理的UI刷新。
    4. 当确定某次刷新是因为View的某个API引起的,可以通过查看该View的源码,重点分析执行requestLayout或invalidate方法的判断条件,从条件中寻求优化方案。后面我们会通过RecyleView.notifyDataSetChanged方法来详细讲述。

    解决问题

        通过各种手段将问题确定后,我们就要真正解决问题了。

    过度渲染解决

        第一节我们提到了,View的渲染内容包括自身背景、自身内容、子控件、滚动条或阴影。优化过度渲染,就是尽可能减少每个View减少自身内容。建议从以下几个方面考虑:

    1. 不要设置无效的背景色。如果父控件被子控件完全遮盖,那么父控件即使设置了背景,那么在最终的UI中也不会被展示出来。但是不被展示和不会被渲染是两回事,真实情况是父控件的背景会依然被绘制,带来不必要的渲染消耗。由于业务的不断迭代和开发人员的更替,该问题非常普遍,另外,测试过程中通常不使用OverDraw检测工具,对于这种隐藏的性能问题,通过肉眼也无法发现。
    2. 尽量不要对背景使用Alpha值。在对带有Alpha属性的View渲染时,会先对View做一次rgb的渲染,然后在对第一次渲染的结果做Alpha处理,如此会对该View渲染两次。如下图左边所示的alpha设置。通过Hierarchy View分析会查出over_layer_fourth_section这个控件消耗了8ms用于draw,但是其它平级的view都是1ms以下。该控件虽然设置了alpha为0,但该属性和Visible:Gone是两种不同的属性,依赖会导致两次Draw。

        对Alpha的处理建议采用以下方式:

    • 尽量不要在初始化View进行设置,而是在需要使用该view时才真正设置;
    • 对于alpha变化,要使用View动画或者属性动画。
    • 尽量使用合适的alpha,对于alpha较低,并不影响用户体验时,去掉alpha,而是使用透明色。

    3. 如果界面自定义了背景色,可以去掉系统自带的背景色,设置android:windowbackground="null"

    4. 绘制时,可以考虑使用ClipRect或QuickReject方法限制渲染区域,减小绘制区域覆盖的概率。

    5. 善用.9图,对于wrap_content属性的控件,如要设置边框,可考虑使用.9图,其中图中设置透明色,这样在渲染控件是会忽略透明区域的绘制。

    6. 从设计上避免“OverDesigin”。

    频繁渲染解决

        频繁渲染是影响性能的主要愿意之一。在优化过程中,我们需要确定哪些刷新是必须的,哪些是可以避免的。影响界面刷新的两个重要API:invalidate和requestLayout。其中invalidate会通知view及其祖先view重新渲染,requestLayout会通知view和祖先view进行onMeasure、onLayout和onMeasure,效率会更低。因此,为解决频繁渲染,主要注意以下几个方面:

    1. 尽量不要主动调用invalidate,postInvalidate和requestLayout,尤其是requestLayout;
    2. 各种View的很多API都会触发上述三个方法,比如setVisibility, addView, setRequestLayoutParams, setText等方法。在优化过程中,也要尽量少的减少这些方法的调用。以setText为例,如果本次更新的内容和上次显示的内容一致,那么就没有必要执行,执行一次内容比对的代价通常比刷新一次View的代价要小很多。可参考下面处理案例:

    3. 优先使用固定大小或者match_parent属性。View的很多属性更改时,都会对非wrap_content属性做优化,以提高UI刷新效率。还是以TextView的setText方法为例,在该API中,会执行checkForReLayout方法来执行UI刷新。从代码中可以很容易看出对于wrap_content属性一定会执行requestLayout和invalidate方法,而对于其它属性,会根据高度是否变化而决定是否执行request。


    4.当需要同时执行invalidate、requestLayout时,建议先执行requestLayout,后执行invalidate。原因是并不是每次请求刷新UI的事件都会得到响应,当目前正在执行UI刷新时,新的刷新事件会被忽略。由于invalidate仅仅是更改PFLAG_DIRTY属性,不会影响到下一次requestLayout属性,但是requestLayout更改的是PFLAG_FORCE_LAYOUT属性,会同时影响draw方法,所以有可能会忽略后面的invalidate。

    5.当业务数据更新非常频繁时,可通过延迟更新策略降低刷新频率。比如当新数据来时,postDelay 5s进行刷新,在delay期间,新来的数据更改下一次显示的新值,直到5s结束后显示出最后一次更新的值。这样能够在不影响用户体验的基础上,降低UI刷新频率。

    渲染范围优化

        评价一个布局是好是坏,不仅仅在于展示效果和层级,还包括是否能够将刷新影响范围降到最小。一个很差的布局中,一个简单控件的变化都有可能导致整屏界面的刷新,这个性能带来灾难性的损失。所以在设计布局中,需要考虑以下几个因素:

    1. 尽量设置控件的大小。固定大小的控件不会导致其它View也跟随其改变,因此在刷新时范围能够有效的得到限制。

    2. 尽量不要将所有的控件放在一个较大的RelativeLayout布局中,尤其是当子view不存在相互依赖的情况,因为一个控件的改变可能会引起该布局中所有View的刷新。可以适当的将部分子控件依附另外一个和父View同级的View。这样即没有增加层级,也能够将子View分组存放,降低了View依赖的概率。

    3. 本文重点讲一下RecycleView的优化。RecycleView是频繁刷新的典型控件,处理不当,其带来的问题会非常严重。在优化RecycleView中,重点采用以下几个方式:

    • 相同于其它View,尽量控制RecycleView的大小;
    • RecycleView如果固定大小,可以使用setHasFixedSize(true)来优化。该属性表示当RecycleView大小固定时,子控件的增加、删除和刷新不会引起父控件的更改,因此在刷新时只处理子控件就够了。

    • 尽量少的调用notifyDataSetChanged方法,而是优先使用notifyItemXXXXX方法。原因是notifyDateSetChanged会一定执行requestLayout方法,但是notifyItemXXXXX会有条件的执行requestLayout,而这个条件就是sethasFixedSIze的属性。

    • 避免多次连续调用notify事件,每次更新应做到一次性通知。

    4. 自定义onDraw方法。该方案适用于大小位置固定的动画控件。可参考美团的一个案例:https://tech.meituan.com/Dianping_Shortvideo_Battery_TestCase.html

    5. 优先使用View动画或者属性动画来执行控件的动画效果。因为这两种动画通过矩阵变化来完成动画展示,而不会更改View的Layout属性,这样不会给其它控件带来刷新的影响。

    布局层级优化

    1. 优先使用RelativeLayout或ConstanLayout来实现复杂布局
    2. 尽量使用单个View来完成UI效果,比如使用TextView的compound drawable和text来替代用LinearLayout实现;
    3. 善用merge、include、viewStub标签
    4. 利用第三方工具查找可以优化的布局,比如Android Lint。

    UI线程工作和内存优化

    1. 将业务逻辑放于子线程,UI线程仅仅处理刷新操作;
    2. 将IO操作和图片转换操作都放于子线程中;
    3. 列表item中复用重复资源,避免每次bind都重新生成,减少内存抖动
    4. 慎用for循环,注意处理效率
    5. 避免主线程和子线程共享一把锁,尤其是耗时锁

    展开全文
  • Android自己的ImageView或者View不能直接加载运行Gif图片,如果要在一个Android的ImageView中加载一个gif图片资源,则需要通过其他途径实现,我之前写了一些关于如何在Android加载gif图片的文章: 文章1,《基于...
    

    Android加载Gif和ImageView的通用解决方案:android-gif-drawable(1)

    Android自己的ImageView或者View不能直接加载运行Gif图片,如果要在一个Android的ImageView中加载一个gif图片资源,则需要通过其他途径实现,我之前写了一些关于如何在Android中加载gif图片的文章:
    文章1,《基于开源框架Glide加载Gif资源图到Android ImageView中》链接地址:http://blog.csdn.net/zhangphil/article/details/45561983
    文章2,《Android加载Gif图片的一般方法:Movie实现》链接地址:http://blog.csdn.net/zhangphil/article/details/50441944

    文章1,2虽然解决了如何加载gif图片资源的问题,但仍存在一个问题:需要事先知道该资源是何图片资源,并假定该资源就是gif图片资源。
    这在有些需求开发中或许不恰当,因为有些时候,仅仅一个View容器,需要它呈现和装载多种图片类型不管是gif或者png,而不需要事先知道它是何种图片类型。
    android-gif-drawable就是这样一种通用的gif加载解决方案。android-gif-drawable在github上的官方主页地址:

    https://github.com/koral--/android-gif-drawable ,该地址上的库及代码是针对Android Studio的。

    针对Eclipse,android-gif-drawable提供了专门的包(包里含有需要的库资源和demo代码)。页面链接地址:https://github.com/koral--/android-gif-drawable-eclipse-sample ,将该代码整体全部下载,下载后是一个完整的eclipse项目,编译器如果报错则可能需要导入相关的support-v4包,已经把jdk切换到1.7及以上。然后就可以直接运行这个demo项目工程了。
    android-gif-drawable使用简单,把GifImageView当作普通的View写进布局文件中,然后加载gif动图资源或者普通的png、jpeg图资源装载进去即可。
    简单给出一个用GifImageView加载gif动图以及加载普通的png图片的例子。
    先写布局文件activity_main.xml:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"
        android:orientation="vertical" >
    
        <pl.droidsonroids.gif.GifImageView
            android:id="@+id/gif1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    
        <pl.droidsonroids.gif.GifImageView
            android:id="@+id/gif2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    
    </LinearLayout>


    上层Java代码:

    package zhangphil.gif;
    
    import android.app.Activity;
    import android.os.Bundle;
    import pl.droidsonroids.gif.GifDrawable;
    import pl.droidsonroids.gif.GifImageView;
    
    public class MainActivity extends Activity {
    
    	@Override
    	protected void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    
    		setContentView(R.layout.activity_main);
    
    		GifImageView gifImageView1 = (GifImageView) findViewById(R.id.gif1);
    
    		GifImageView gifImageView2 = (GifImageView) findViewById(R.id.gif2);
    
    		try {
    			// 如果加载的是gif动图,第一步需要先将gif动图资源转化为GifDrawable
    			// 将gif图资源转化为GifDrawable
    			GifDrawable gifDrawable = new GifDrawable(getResources(), R.drawable.loading);
    
    			// gif1加载一个动态图gif
    			gifImageView1.setImageDrawable(gifDrawable);
    
    			
    			// 如果是普通的图片资源,就像Android的ImageView set图片资源一样简单设置进去即可。
    			// gif2加载一个普通的图片(如png,bmp,jpeg等等)
    			gifImageView2.setImageResource(R.drawable.ic_launcher);
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }



    运行结果如图所示(上面是一个不停旋转的加载gif动图,下面是一个普通的png图片,即小机器人):



    由于https://github.com/koral--/android-gif-drawable-eclipse-sample这个页面下载到的代码针对eclipse推出的项目包中是一个混在一起的项目,实际开发过程中,最好的做法是把依赖的核心代码及资源分离出来,这样达到复用和工程代码结构清晰,我把android-gif-drawable for Eclipse的关键代码抽取分离出来,单独作成一个lib,需要的时候直接导入然后引用即可。
    android-gif-drawable for Eclipse在github上的lib包页面地址:
    https://github.com/zhangphil/android-gif-drawable-for-Eclipse
    使用时候,从这个页面下载完整的lib项目,作为lib导入到eclipse里面,在自己需要的项目中加以引用即可。


    附上loading.gif动图:

    展开全文
  • 效果图实现思路可以看出该View可分为三个部分来实现 最外围的圆,该部分需要区分进度圆和底部的刻度圆,进度部分的刻度需要和底色刻度区分开来 中间显示的文字进度,需要让文字在View中居中显示 旋转的小圆点,...
  • Android6.0源码分析之View(一)

    千次阅读 2016-12-29 11:05:08
    目前对于view还处于学习阶段,本来打算学习结束之后再写一篇进行总结,但是发现自己自制力太差,学习效率太低,所以在此,边学边写博客,不仅督促自己完成对view的学习,而且还可以看看大家对于view有什么想知道的,...
  • Android自定义控件之刷新与加载(一)

    千次阅读 2018-01-24 16:08:47
    本文主要介绍下拉刷新与上拉加载的主要实现逻辑,效果如下图所示: 实现逻辑 主要是将listview添加一头一脚方式来实现,将ListView封装成一个控件PullToRefresh形式: PullToRefresh主要代码: public ...
  • Android 超高仿微信图片选择器 图片该这么加载

    万次阅读 多人点赞 2016-08-10 19:48:46
    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/39943731,本文出自:【张鸿洋的博客】1、概述关于手机图片加载器,在当今像素随随便便破千万的时代,一张图片占据的内存都相当可观,作为高大...
  • 比如说在ListView中加载图片,如果是同步加载图片倒还好,但是一旦使用异步加载图片那么问题就来了,这个问题我相信很多Android开发者都曾经遇到过,就是异步加载图片会出现错位乱序的情况。遇到这个问题时,不少人...
  • ViewPager加载不出View的解决方法

    千次阅读 2017-08-06 23:29:11
    ViewPager加载不出View的解决方法 今天在写App的时候遇到了一个问题,我用ViewPager加载三个View(里面只放了一个全屏的ImageView)的时候,View的背景颜色设置的是黑色,但是加载出来的View是全屏的白色,并且没有...
  • 今天分享一篇关于混合开发的文章: 如何在现有的 Android 项目中集成 React Native,以及如何在原生 Activity中加载 RN 视图。 原生项目 集成 React Native 1.创建Android工程:ReactNativeApp 关于如何创建...
  • 你需要知道的Android View的创建

    千次阅读 2017-02-23 15:04:33
    View的创建与绘制一向是很多人望而止步的问题。然而我们在平常的应用开发中是最经常运用到的setContentView(),我们都会用在Activity的onCreate()的时候调用setContentView()来加载编辑好的XML布局。但是实际上创建...
  • Android动画之View animation(视图动画)

    千次阅读 2017-08-14 19:46:57
    Android动画可以分为以下3种: View Animation 视图动画,通多对整个视图不断做图像的变换(平移、缩放、旋转、透明度)产生的动画效果,是一种渐进式动画。 Drawable Animation 图片动画,其实也是逐帧动画,是...
  • Android图片加载库的封装实战

    万次阅读 2017-05-09 12:07:47
    图片加载Android开发中最最基础的功能,为了降低开发周期和难度,我们经常会选用一些图片加载的开源
  • 1 RequireMent 加载网络pdf文件: http://file.chmsp.com.cn/colligate/file/00100000224821.pdf 2 scheme ...(1) webView加载 ,谷歌服务有限制 ...(4) 三方依赖 3 Core Code (1) webView加载 ...
  • Android自己的ImageView或者View不能直接加载运行Gif图片,如果要在一个Android的ImageView中加载一个gif图片资源,则需要通过其他途径实现,我之前写了一些关于如何在Android加载gif图片的文章: 文章1,《基于...
  • 大家好,今天我们继续学习Glide。在上一篇文章当中,我带着大家一起深入探究了Glide的缓存机制,我们不光掌握了Glide缓存的使用方法,还通过源码分析对缓存的工作原理进行了了解。今天是这个Glide系列的第四篇文章,...
  • Glide是一个快速高效的Android图片加载库,注重于平滑的滚动。Glide提供了易用的API,高性能、可扩展的图片解码管道(decode pipeline),以及自动的资源池技术。 Glide 支持拉取,解码和展示视频快照,图片,和GIF...
  • 转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38476887 ,本文出自【张鸿洋的博客】上一篇博客介绍了Android异步消息处理机制,如果你还不了解,可以看:Android 异步消息处理机制 让你深入...
  • Android面试题View

    千次阅读 2018-11-28 11:49:57
    Android面试题View篇,由本人整理汇总,后续将推出系列篇,如果喜欢请持续关注和推荐。 系列文章目录: Android面试题ViewAndroid面试题进程篇 Android面试题线程篇 Activity生命周期? onCreate() -&gt...
1 2 3 4 5 ... 20
收藏数 23,858
精华内容 9,543
热门标签
关键字:

单独加载android的view库