Android-APP内存优化

为什么要进行内存优化

  1. APP运行内存限制,OOM导致APP崩溃
  2. APP性能:流畅性、响应速度和用户体验

查看APP内存的方法和工具

我们先获取系统服务,我们通过getSystemService传入什么参数呢?下面就先说下各个参数代表的是什么吧:

参数名 获取了什么系统服务
WINDOW_SERVICE 窗口管理器
LAYOUT_INFLATER_SERVICE 布局管理
ACTIVITY_SERVICE 活动管理
POWER_SERVICE 电源管理
ALARM_SERVICE 闹钟管理
NOTIFICATION_SERVICE 通知栏管理
KEYGUARD_SERVICE 屏幕保护管理
LOCATION_SERVICE 位置管理
SEARCH_SERVICE 搜索管理
VIBRATOR_SERVICE 手机震动
CONNECTIVITY_SERVICE 网络连接管理
WIFI_SERVICE wifi管理
WIFI_P2P_SERVICE 音频管理
MEDIA_ROUTER_SERVICE 大屏幕播放管理
TELEPHONY_SERVICE 电话管理
SubscriptionManager.from(this); 消息驱动
INPUT_METHOD_SERVICE 软键盘的输入控制
UI_MODE_SERVICE 模式管理
DOWNLOAD_SERVICE 下载管理
BATTERY_SERVICE 电池管理
JOB_SCHEDULER_SERVICE 后台服务
NETWORK_STATS_SERVICE 应用流量统计

详细的可以看我这篇博客:传送门
所以我们就要传入ACTIVITY_SERVICE这个参数,这样我们就可以通过ActivityManager中的getMemoryClass方法获取到该APP在该部手机上能获取到的最大的内存空间,单位是M,或者可以通过getLargeMemoryClass方法获取到设置了最大堆的应用的能获得到的最大的内存空间。但是这两个获取到的值一般都是相同的,因为一般的应用都不支持最大堆的申明,而且也不这么去做。

private void calculate() {
    StringBuilder str = new StringBuilder();
    ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    int memClass = activityManager.getMemoryClass();//以m为单位
    int LargememClass = activityManager.getLargeMemoryClass();//以m为单位
    str.append("memClass:"+memClass+"\n");
    str.append("LargememClass:"+LargememClass+"\n");
    mTv.setText(str.toString());
}

这么声明最大堆呢?
在AndroidManifest中application标签下定义下面的属性:

android:largeHeap="true"

Android的内存管理方式

Android系统内存分配与回收方式

我们先运行我们之前写的程序,然后再来查看一下虚拟机中运行的进程吧,我们要用adb shell这个命令进入Android底层Linux系统,然后再用ps命令查看系统里面的进程,当然有可能有人输入adb shell会返回不是内部或外部命令,也不是可运行程序和批处理文件,这就是你的环境没有配好,我有一篇博客是讲怎么解决的,这里就不啰嗦了(传送门:戳这里)。正常的我们就会获得下面这些数据,我们从中找到属于程序包名的一行数据。
Android-APP内存优化_第1张图片
然后我们来看下这个应用的进程相关信息,输入下面这个命令

dumpsys meminfo com.gin.xjh.testnc

Android-APP内存优化_第2张图片
说了怎么看系统的内存分配了,我们就来说下系统的回收方式:
GC(垃圾回收器)只有在Heap(堆)空间不够的情况下才会发出垃圾回收的命令。之后再释放空间,所以这就照成了一个很不好的情况,因为他释放空间是会让所有的线程暂停的,如果你的垃圾比较多的话,在GC触发的时候,所有的线程都会被暂停,这样就会让应用卡顿等。

APP内存限制机制

每个APP分配的最大内存限制,随不同设备而不同。使用内存最大的是图片。那为什么要堆内存进行限制呢?因为Android手机是多任务系统,有很多APP在运行,所以肯定要对内存进行限制。

切换应用时后台APP清理机制

APP在切换的时候是使用了LRU算法的(LRU算法:最近使用的排在最前面,最少可能被清理掉)
当真正要开始清理的时候,系统会发出onTrimMemort()回调,但是这个回调并不是在清理的时候回调,而是在系统内存发生变化的时候,系统会发出onTrimMemort给各个应用,然后你的APP收到了这个以后,如果系统的内存已经很少了,你就要开始把你APP中不用到的一些占用内存的东西进行释放,这样的话你的APP占用的内存就会相对的小一点,系统在查看后台APP的时候把你的APP清除的可能性就会小一点。

监控APP内存的方法

1、代码显示
这里我们就可以利用上面我们获取最大内存的代码来实现内存的查看,用代码来看呢就要在你需要看内存信息的时候调用这段代码。后面这种方法获取到的最大内存大小应该是和之前那种方法获取到的应该是一样的。

private void calculate() {
    StringBuilder str = new StringBuilder();
    ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    int memClass = activityManager.getMemoryClass();//以m为单位
    int LargememClass = activityManager.getLargeMemoryClass();//以m为单位
    str.append("memClass:"+memClass+"\n");
    str.append("LargememClass:"+LargememClass+"\n");
    Float maxMemory = Runtime.getRuntime().maxMemory()*1.0f/(1024*1024);//以字节为单位
    Float totalMemory = Runtime.getRuntime().totalMemory()*1.0f/(1024*1024);//以字节为单位
    Float freeMemory = Runtime.getRuntime().freeMemory()*1.0f/(1024*1024);//以字节为单位
    str.append("maxMemory:"+maxMemory+"\n"+"totalMemory:"+totalMemory+"\n"+"freeMemory:"+freeMemory+"\n");
    mTv.setText(str.toString());
}

Android-APP内存优化_第3张图片
当然我们也可以通过Android Studio的Android Profiler来动态的查看我们APP的内存使用情况:
Android-APP内存优化_第4张图片
当然我们也可以用DDMS来查看内存信息
DDMS打开方式:Tools->Android->Android Device Monitor。
打开DDMS以后我们在Devices中找到我们现在程序运行的模拟器或者真机,找到我们程序对应的包名,然后点击heap,再点Cause GC就可以查看详细信息了,我们查看程序是否内存泄漏主要是看data object以及class object两个的值,假如这两个值随着程序的运行数值趋于平稳就说明你的程序应该没有发生内存泄漏,如果一直在进行增长的话就是可能会发生内存泄漏。
Android-APP内存优化_第5张图片

内存优化方法

1、数据结构优化

  • 频繁使用字符串拼接用StringBuilder(字符串之间通过+的方式进行字符串拼接会产生中间字符串,这样就造成了内存浪费,并且字符串进行拼接也是比较耗时的)。
  • ArrayMap、SparseArray替换HashMap(内存使用更少,并且在数据量大的时候效率会比较高)。
  • 内存抖动(就是在短时间之内申请了很多空间,但是使用一下子就不用了,之后过了一会以后又申请很多的空间,类似的反复下去,这样就会当空间不足的时候,不断的让GC被调用,导致APP卡顿)。

我们先来人工造成内存抖动来看一下现象,按照上面的定义我们要先申请很多空间,然后弃之不用,然后再申请,这个实现应该很简单的。

private void doChurn() {
    Log.d("xjhLog","doChurn start:");
    for(int i=0;iString[] strMatrix = new String[length];
        for(int j=0;jString.valueOf(ran.nextDouble());
        }
        Log.d("xjhLog","doChurn rowStr:"+i);
    }
    Log.d("xjhLog","doChurn end.");
}

通过Android Studio的Android Profiler,我们可以看到内存的使用情况。
Android-APP内存优化_第6张图片
2、对象复用

  • 复用系统中自带的资源
  • ListView/GridView的ConvertView复用
  • 避免在onDraw方法里面执行对象的创建(因为onDraw在界面,图像或者View一有变化的化就会重新调用,如果在里面执行对象的创建的话,就会影响绘制的时间)

3、避免内存泄漏
内存泄漏:由于代码的瑕疵,导致这块内存虽然是停止不用的,但是依然还是被其他东西引用着,使得GC没法对它进行回收。

  • 内存泄漏会导致剩余可以使用的Heap(堆)越来越少,以至于频繁的触发GC。
  • 尤其是Activity泄漏。
  • 上下文对象最好是使用Application Context而不是Activity Context,因为context会经常被调用,假如我们使用了Activity Context,就导致了GC认为Activity的引用还是在的,所以不会进行回收,但是其实那时候Activity已经销毁了。
  • 注意Cursor(指针)对象是否及时关闭

OOM问题

OOM:内存溢出
OOM的必然性:Android手机对于每个APP可用的内存是有一定限制的。
OOM的可解决性:Android手机的生产厂家对于手机内存的设置是有过考量的,一般来说只要是有对内存的优化是不会出现OOM问题的。
OOM问题绝大部分发生在图片上。
(1)强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。
如下:

Object o=new Object();   //  强引用

当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。如果不使用时,要通过如下方式来弱化引用,如下:

o=null;     // 帮助垃圾收集器回收此对象

显式地设置o为null,或超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于gc的算法。
举例:

public void test(){
    Object o=new Object();
    // 省略其他操作
}

在一个方法的内部有一个强引用,这个引用保存在栈中,而真正的引用内容(Object)保存在堆中。当这个方法运行完成后就会退出方法栈,则引用内容的引用不存在,这个Object会被回收。
但是如果这个o是全局的变量时,就需要在不用这个对象时赋值为null,因为强引用不会被垃圾回收。
强引用在实际中有非常重要的用处,举个ArrayList的实现源代码:

private transient Object[] elementData;
public void clear() {
        modCount++;
        // Let gc do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
}

在ArrayList类中定义了一个私有的变量elementData数组,在调用方法清空数组时可以看到为每个数组内容赋值为null。不同于elementData=null,强引用仍然存在,避免在后续调用 add()等方法添加元素时进行重新的内存分配。使用如clear()方法中释放内存的方法对数组中存放的引用类型特别适用,这样就可以及时释放内存。
(2)软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

String str=new String("abc");                                     // 强引用
SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用

当内存不足时,等价于:

If(JVM.内存不足()) {
   str = null;  // 转换为软引用
   System.gc(); // 垃圾回收器进行回收
}

软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
这时候就可以使用软引用

Browser prev = new Browser();               // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用    
if(sr.get()!=null){ 
    rev = (Browser) sr.get();           // 还没有被回收器回收,直接获取
}
else{
    prev = new Browser();               // 由于内存吃紧,所以对软引用的对象回收了
    sr = new SoftReference(prev);       // 重新构建
}

这样就很好的解决了实际的问题。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
其实对应的还有弱引用和虚引用,这个对于内存的优化没有什么用处就不在这里说了。

优化OOM问题

  • 注意临时Bitmap对象的及时回收
  • 避免Bitmap的浪费
  • Try catch某些大内存的分配的操作
  • 加载Bitmap:缩放比例、解码格式、局部加载

我们其实可以发现上面这几点大部分和Bitmap图片操作有关系,接下来我们来说下怎么进行图片压缩,主要有三种方法一是把图片缩小,二是把图片的每个像素压缩(一个像素由4个字节压缩到2个字节),最后一种就是显示部分完整图片。
缩小图片

private void changePicOpti() {
    if (file == null) {//file图片文件
        return;
    }
    try {
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;//只获得文件的宽和高
        BitmapFactory.decodeStream(new FileInputStream(file), null, o);
        int width_tmp = o.outWidth;
        int height_tmp = o.outHeight;
        int scale = 2;
        while (true) {
            if (width_tmp / scale < SCREEN_WIDTH && height_tmp / scale < SCREEN_HEIGHT) {//SCREEN_WIDTH屏幕宽度,SCREEN_HEIGHT屏幕宽度
                break;
            }
            scale += 2;
        }
        scale /= 2;//inSampleSize=1会将原始图片放大2倍
        o.inJustDecodeBounds = false;
        o.inSampleSize = scale;
        FileInputStream fin = new FileInputStream(file);
        Bitmap bitmap = BitmapFactory.decodeStream(fin, null, o);
        img.setImageBitmap(bitmap);
    } catch (IOException e) {
        e.printStackTrace();
    }

压缩像素

private void changeRGB() {
    if (file == null) {//file图片文件
        return;
    }
    try {
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inPreferredConfig = Bitmap.Config.RGB_565;
        FileInputStream fin = new FileInputStream(file);
        Bitmap bitmap = BitmapFactory.decodeStream(fin, null, o);
        img.setImageBitmap(bitmap);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

显示部分高清图片

private void partLoad() {
    if (file == null) {//file图片文件
        return;
    }
    try {
        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;//只获得文件的宽和高
        FileInputStream fin = new FileInputStream(file);
        BitmapFactory.decodeStream(fin, null, o);
        int width_tmp = o.outWidth;
        int height_tmp = o.outHeight;
        fin = new FileInputStream(file);
        BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(fin, false);
        BitmapFactory.Options options = new BitmapFactory.Options();
        //SCREEN_WIDTH屏幕宽度,SCREEN_HEIGHT屏幕宽度,nextPx垂直方向变动的,nextPy水平方向的变动,默认中心点
        int x = width_tmp / 2 - SCREEN_WIDTH / 2 + nextPx;
        int y = height_tmp / 2 - SCREEN_HEIGHT / 2 + nextPy;
        //保证在图片范围以内
        if (x < 0) {
            x = 0;
        } else if (x > width_tmp - SCREEN_WIDTH) {
            x = width_tmp - SCREEN_WIDTH;
        }
        if (y < 0) {
            y = 0;
        } else if (y > height_tmp - SCREEN_HEIGHT) {
            y = height_tmp - SCREEN_HEIGHT;
        }
        Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(x, y, x + SCREEN_WIDTH, y + SCREEN_HEIGHT), options);
        img.setImageBitmap(bitmap);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

你可能感兴趣的:(Android小技巧,Android)