面试题之性能优化

作者: 码字农民工 | 来源:发表于2019-02-23 10:58 被阅读21次

    ANR

    Application Not Responding(界面无反应,一般的相应时间是五秒)。

    造成ANR的主要原因

    • 主线程被IO操作阻塞
    • 主线程中存在耗时的计算

    Android中在主线程中的操作

    • Activity的所有生命周期回调都是执行在主线程中的
    • Service默认是执行在主线程中的
    • BroadcastReceiveronReceive回调是执行在主线程中的
    • 没有在子线程中使用LooperHandlerhandleMessage,post(Runnable)是执行在主线程中的
    • AsyncTask的回调中除了doInBackground,其他都是执行在主线程中的

    如何解决ANR

    • 使用AsyncTask处理耗时的IO操作
    • 使用handler来处理工作线程的耗时任务
    • ActivityonCreateonResume回调中尽量避免耗时的代码

    OOM

    当前占用的内存加上我们申请的内存资源超过了Dalvik虚拟机的最大内存限制时就会抛出Out of memory异常

    如何避免OOM

    • Bitmap

      • 图片显示(比如说如果加载网络图片的时候,我们可以优先加载缩略图,还有比如在列表中,我们可以在滚动的时候不去加载图片)
      • 及时释放内存(当我们去创建Bitmap的时候,它会通过jni调用nativeCreate()方法,这样不仅会在java中开辟一块内存,同时会在底层c也开辟一块内存空间,当我们确认不用该图片时,可以调用recycle()方法去释放内存,主要还是调用了nativeRecycle()去释放C中的内存,不过我们即使不去调用recycle()释放内存,当我们程序的进程被杀死时,也会去释放该内存)
      • 图片压缩
      • 捕获异常(创建bitmap时,我们可以使用try/catch来捕获oom异常)
    • ListView/RecyclerView的优化

    • 避免在自定义ViewonDraw()方法中执行对象的创建

    • 谨慎使用多进程

    缓存LruCache

    LruCache内部维护了一个LinkedHashMap(双链表数据结构),在put数据的时候会判断指定的内存大小是否已满。若已满,则会使用最近最少使用算法进行清理,LinkedHashMap内部是一个数组加双向链表的形式来存储数据,也就是说当我们通过get方法获取数据的时候,数据会从队列跑到队头来。反反复复,队尾的数据自然是最少使用到的数据。

    具体使用

    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    LruCache<String, Bitmap> cache = new LruCache<String, Bitmap>(maxMemory / 8) {
                @Override
                protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
                    return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
                }
    };
    

    我们在使用LruCache的时候需要复写sizeOf()方法,具体我们就从源码分析一波吧。

    public class LruCache<K, V> {
        private final LinkedHashMap<K, V> map;
        //构造方法
        public LruCache(int maxSize) {
            if (maxSize <= 0) {
                throw new IllegalArgumentException("maxSize <= 0");
            }
            this.maxSize = maxSize;
            this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
        }
        
        //测量元素大小
        private int safeSizeOf(K key, V value) {
            int result = sizeOf(key, value);
            if (result < 0) {
                throw new IllegalStateException("Negative size: " + key + "=" + value);
            }
            return result;
        }
    
        public final V get(K key) {
            if (key == null) {
                throw new NullPointerException("key == null");
            }
    
            V mapValue;
            synchronized (this) {
                //这里的map.get()方法就会进行数据排序
                mapValue = map.get(key);
                if (mapValue != null) {
                    //命中次数+1,并且返回mapValue
                    hitCount++;
                    return mapValue;
                }
                //未命中次数+1
                missCount++;
            }
            //如果未命中,会尝试利用create方法创建对象
            //create需要自己实现,若未实现则返回null
            V createdValue = create(key);
            if (createdValue == null) {
                return null;
            }
    
            synchronized (this) {
                //创建了新对象之后,再将其添加进map中,与之前put方法逻辑基本相同
                createCount++;
                mapValue = map.put(key, createdValue);
                if (mapValue != null) {
                    map.put(key, mapValue);
                } else {
                    size += safeSizeOf(key, createdValue);
                }
            }
    
            if (mapValue != null) {
                entryRemoved(false, key, createdValue, mapValue);
                return mapValue;
            } else {
                //每次加入数据时,都需要判断一下是否溢出
                trimToSize(maxSize);
                return createdValue;
            }
        }
    
        public final V put(K key, V value) {
            if (key == null || value == null) {
                throw new NullPointerException("key == null || value == null");
            }
    
            V previous;
            synchronized (this) {
                //count为LruCahe的缓存个数,这里加一
                putCount++;
                //加上这个value的大小
                size += safeSizeOf(key, value);
                //存进LinkedHashMap中
                previous = map.put(key, value);
                if (previous != null) {
                    //如果之前存过这个key,则减掉之前value的大小
                    size -= safeSizeOf(key, previous);
                }
            }
    
            if (previous != null) {
                entryRemoved(false, key, previous, value);
            }
            //进行内存判断
            trimToSize(maxSize);
            return previous;
        }
        //判断是否内存溢出
        private void trimToSize(int maxSize) {
            while(true) {
                //这是一个无限循环,目的是为了移除value直到内存空间不溢出
                Object key;
                Object value;
                synchronized(this) {
                    if (this.size < 0 || this.map.isEmpty() && this.size != 0) {
                        //如果没有分配内存空间,抛出异常
                        throw new IllegalStateException(this.getClass().getName() + ".sizeOf() is reporting inconsistent results!");
                    }
                    if (this.size <= maxSize || this.map.isEmpty()) {
                        //如果小于内存空间
                        return;
                    }
                    //否则将使用Lru算法进行移除(找到LinkedHashMap的头节点进行移除)
                    Entry<K, V> toEvict = (Entry)this.map.entrySet().iterator().next();
                    key = toEvict.getKey();
                    value = toEvict.getValue();
                    this.map.remove(key);
                    this.size -= this.safeSizeOf(key, value);
                    //回收次数+1
                    ++this.evictionCount;
                }
    
                this.entryRemoved(true, key, value, (Object)null);
            }
        }
    
        public final V remove(K key) {
            //判空
            if (key == null) {
                throw new NullPointerException("key == null");
            }
    
            V previous;
            synchronized (this) {
                //根据key移除value
                previous = map.remove(key);
                if (previous != null) {
                    //减掉size的大小
                    size -= safeSizeOf(key, previous);
                }
            }
    
            if (previous != null) {
                entryRemoved(false, key, previous, null);
            }
    
            return previous;
        }
    }
    

    从上面的代码中我们可以分析出来其重点的代码就在trimToSize()方法当中,每次LruCacheput(value)都调用该方法,在trimToSize()中对大于了存储空间的值找到LinkedHashMap的头节点进行移除(最少使用的值),这里我们需要记住的是LinkedHashMapget方法(每次get对节点进行顺序排列,将使用的数据重新排列到节点尾部)。

        public V get(Object key) {
            Node<K,V> e;
            if ((e = getNode(hash(key), key)) == null)
                return null;
            if (accessOrder)
                afterNodeAccess(e);
            return e.value;
        }
    
        void afterNodeAccess(Node<K,V> e) { // move node to last
            LinkedHashMapEntry<K,V> last;
                //accessOrder为true且当前节点不是尾节点则进行访问顺序排序
            if (accessOrder && (last = tail) != e) {
                LinkedHashMapEntry<K,V> p =
                    (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
                //下面是排序过程(就是将当前的数值放置节点尾部)
                p.after = null;
                if (b == null)
                    head = a;
                else
                    b.after = a;
                if (a != null)
                    a.before = b;
                else
                    last = b;
                if (last == null)
                    head = p;
                else {
                    p.before = last;
                    last.after = p;
                }
                tail = p;
                ++modCount;
            }
        }
    

    原来如此!LinkedHashMap在这个方法中实现了按访问顺序排序,这也就是为什么我们的LruCache底层是使用的LinkedHashMap作为数据结构。

    UI卡顿

    Android中通常流畅的动画保持在16ms绘制一帧,也就是我们常说的60fps(1000ms/16ms约等于60),如果绘制时间超过了16ms,就会给人一种卡顿的现象。

    UI卡顿原因分析

    • UI线程做轻微的耗时操作,导致UI线程卡顿
    • 布局layout过于复杂,无法在16ms内完成渲染
    • 同一时间内动画执行次数过多,导致CPUGPU负载过重
    • View的过度绘制,导致某些像素在同一帧时间内被绘制多次,从而导致CPUGPU负载过重
    • View频繁的触发measure,layout导致累计耗时过多,整个View频繁的重新渲染
    • 内存频繁的触发GC操作,导致GC暂时阻塞渲染操作
    • 冗余资源及逻辑等导致加载和执行过慢
    • ANR

    UI卡顿解决办法

    1. 布局优化
      减少布局嵌套,可以结合实际使用include标签,merge标签
    2. 列表及Adapter的优化
      比如说在列表滚动时候不要进行图片加载操作
    3. 背景和图片等内存分配优化
      背景最好不要过度绘制,图片最好压缩
    4. 避免ANR(不要在主线程中进行耗时操作)

    内存泄露

    某个不再使用的对象被其他实例所引用,导致其该被回收而无法被回收。

    Android中常见的内存泄露

    • 单例
      长生命周期类持有短生命周期类的引用,比如单例的构造方法中传入了ActivityContext,我们需要传入的是ApplicationContext
    • Handler
      非静态内部类持有外部类的引用,比如在Activity中直接申明了一个非静态的Handler,解决办法:1.将申明的Handler变成static 2.创建一个静态的内部类Handler持有Activity的弱引用 3.在ActivityonDestroy()方法中removemessage.
    • 开启线程
      匿名内部类持有外部类的引用,比如说new Thread, new AsyncTask等,解决办法: 将其写成静态的非匿名内部类
    • WebView

    内存管理

    内存管理机制的特点

    • 更少的占用内存
    • 在合适的时候,合理的释放系统资源
    • 在系统内存紧张的情况下,能释放掉大部分不重要的资源,来为Android系统提供可用的内存
    • 能够很合理的在特殊生命周期中,保存或者还原重要数据,以至于系统能够正确的重新恢复该应用

    内存优化方法

    1. Service完成任务后,尽量停止它(可以使用IntentService,在IntentService内有一个工作线程来处理耗时操作,启动IntentService的方式和启动传统的Service一样,同时,当任务执行完后,IntentService会自动停止)
    2. UI不可见的时候,释放掉一些只有UI使用的资源
    3. 在系统内存紧张的时候,尽可能多的释放掉一些非重要的资源
    4. 避免滥用Bitmap导致的内存浪费
    5. 使用针对内存优化过的数据容器(少用枚举常量,它消耗的资源是常量的两倍多)
    6. 避免使用依赖注入框架
    7. 使用ZIP对齐的Apk
    8. 使用多进程(比如定位,推送,WebView可以单独开启一个进程)

    冷启动优化

    冷启动的定义

    冷启动就是在启动应用前,当前系统中没有该应用的任何进程信息。

    热启动的定义

    热启动:用户使用返回键退出应用,然后马上又重新启动应用。其实就是重新启动应用的时候,当前系统后台中拥有该应用的进程。

    冷启动时间的计算

    这个时间值是从应用启动(创建进程)开始计算,到完成视图的第一次绘制(当第一个Activity内容对用户可见)为止

    冷启动的流程

    Application的构造器方法->attachBaseContext()->onCreate()->Activity的构造方法->onCreate()->配置主题中背景等属性->onStart()->onResume()->测量布局绘制显示在界面上

    1. Zygote进程中fork创建出一个新的进程
    2. 创建和初始化Application类,创建MainActivity
    3. inflate布局,当onCreate/onStart/onResume方法都执行完成
    4. contentViewmeasure/layout/draw显示在界面上

    冷启动时间优化

    • 减少onCreate()方法的工作量
    • 不要让Application参与业务操作和进行耗时操作
    • 不要以静态变量的方式在Application中保存数据
    • 减少第一个Activity的布局嵌套

    其他优化

    • Android不用静态变量存储重要数据
      • 静态变量等数据可能会由于进程已经被杀死而被重新的初始化
      • 建议使用其他的方式传输数据:文件/sp/contentProvider
    • SharePreference
      • 不能跨进程同步数据(SharePreference在多进程读写的时候,不能跨进程读写数据,因为每个进程都会维护一套SharePreference的副本,每次修改和读取都是读取该进程的副本,只有在应用结束的时候,所有进程的副本才会同步至文件中)
      • 存储SharePreference的文件过大问题(内容都是通过key/value的形式进行存储的,如果文件过大,特别消耗内存,可能造成主界面卡顿,还很有可能导致创建大量的临时变量,导致内存泄漏)
    • 内存对象序列化

      将对象的状态信息转化为可以存储或者传输的形式的过程

      • Serializeble(Java的序列化方式,在序列化的时候会产生大量的临时变量,从而引起频繁的GC)
      • Parcelable(Android特有的序列化方式,不能序列化磁盘数据)
    • 避免在UI线程中进行繁重的操作

    以上就是对Android中的性能优化的一些个人见解和总结。

    相关文章

      网友评论

        本文标题:面试题之性能优化

        本文链接:https://www.haomeiwen.com/subject/vyieyqtx.html