面试题之性能优化

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

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

public class LruCache {
    private final LinkedHashMap map;
    //构造方法
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap(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 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 e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

    void afterNodeAccess(Node e) { // move node to last
        LinkedHashMapEntry last;
            //accessOrder为true且当前节点不是尾节点则进行访问顺序排序
        if (accessOrder && (last = tail) != e) {
            LinkedHashMapEntry p =
                (LinkedHashMapEntry)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中的性能优化的一些个人见解和总结。

你可能感兴趣的:(面试题之性能优化)