Android面试必读面试准备安卓面试

面试题之性能优化

2019-02-23  本文已影响21人  码字农民工

ANR

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

造成ANR的主要原因

Android中在主线程中的操作

如何解决ANR

OOM

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

如何避免OOM

缓存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卡顿解决办法

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

内存泄露

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

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显示在界面上

冷启动时间优化

其他优化


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

上一篇下一篇

猜你喜欢

热点阅读