性能优化技巧知识梳理(2) 内存优化

1、前言

对于应用中的内存优化,和布局优化相似,也有不少的技巧,这里咱们分为如下几方面来总结:html

  • Java优化技巧
  • 避免没必要要对象的建立
  • 保证不使用对象的释放
  • 使用性能优化工具,定位内存问题

2、Java 优化技巧

首先,咱们介绍一些Java语法中的优化技巧,强烈推荐你们在编程时参考阿里巴巴编写的<<阿里巴巴Java开发手册>>,下载地址,这里简要介绍一些经常使用的知识点:android

  • 尽可能采用原始数据类型,而不是对象,例如int要比Integer占用更少的内存。
  • 若是一个方法不须要访问对象的成员变量,或者调用非静态方法,那么应当将它声明为static
  • 将常量声明为static final
  • 避免内部的getXXX()/setXXX()方法,而是直接访问变量。
  • 使用加强的for循环,而不是for(int i = 0; i < 100; i++)这样的循环。
  • 避免使用float类型,当对精度要求不高,采用int类型。

3、避免没必要要对象的建立

(1) 单例对象在须要的时候初始化

在使用单例时,咱们应当仅在使用到该单例时才去初始化它,这里咱们能够经过“静态初始化会在类被加载时触发”这一原理,来实现懒加载。数据库

public class OptSingleton {
    
    private OptSingleton() {}
    
    public static OptSingleton getInstance() {
        return Holder.INSTANCE;
    }
    
    private static class Holder {
        public static final OptSingleton INSTANCE = new OptSingleton();
    }
}
复制代码

(2) 避免进行自动装箱

自动装箱指的是将原始的数据类型转换成为引用类型,例如int转换成为Integer,这种自动装箱操做,虽然方便了咱们的使用,可是在某些场景下的不当使用有可能会致使性能问题,主要有两点:编程

  • 第一点:使用操做符时的自动装箱
public static void badAssemble() {
        Integer sum = 0;
        for (int i = 0; i < (1 << 30); i++) {
            sum = sum + i;
        }
    }
复制代码

就有自动装箱的过程,其中sum+i能够分解为下面这两句,也就是说,在循环的过程当中,咱们建立了大量的临时对象Integer,而建立完以后,它们很快又会被GC回收掉,所以,会出现内存抖动的现象。数组

int result = sum + i;
Integer sum = new Integer(result);
复制代码

咱们使用Android Studio提供的检测工具能够验证上面的结论: 浏览器

而若是咱们使用正常的写法,那么是不会出现上面的状况的:

public static void badAssemble() {
        int sum = 0;
        for (int i = 0; i < (1 << 30); i++) {
            sum = sum + i;
        }
    }
复制代码

此时的监测结果为: 安全

  • 第二点:使用容器时的自动装箱

当咱们使用例如HashMap这种容器的时候,除了要存储保存的数据,还要存储Key值,这些Key值就是由自动装箱的过程所产生的。性能优化

此时,咱们就能够考虑选用Android平台上提供的优化容器来尽量地避免装箱操做,这些容器包括SparseArraySparseBooleanArraySparseIntArraySparseLongArray,这些容器有如下特色:bash

  • key值都为原始数据类型int,避免了隐式装箱的操做,这同时也是它的局限性。
  • 其内部是经过两个数组存储数据的,一个用于key,另外一个用于value,为了优化性能,它内部对数据还采起了压缩的方式来表示稀疏数组的数据,从而节约内存空间。
  • 在查找数据时,采用的是二分查找法,相比于HashMap须要遍历Entry数组找到相等的hash值,通常来讲,咱们的数据量都不会太大,而在数据量较小时,二分查找要比遍历数组,查找速度更快。

(3) 预先指定容器的大小

当咱们使用例如HashMapArrayList这些容器时,每每不习惯给它们指定一个初始值,然而当这些容器存储空间不足时,就会去自动扩容,其扩容的大小每每是原始大小的两倍。网络

所以,当咱们须要存储额外的一个元素的时候恰好容器不够了,那么就须要扩容,可是这时候就会出现额外的浪费空间。

(4) 对于占用资源的 Activity,合理的使用 LaunchMode

对于Activity来讲,其默认的启动模式是standard,也就是说,每次启动这个Activity,都会建立一个新的实例,像相似于浏览器这种内存大户,每次外部打开一个网页,都须要建立一个Activity,而Activity又会去实例化WebView,那么是至关耗费资源的,这时,咱们就能够考虑使用singleTask或者singleInstance来实现。

(5) 处理屏幕旋转致使的重建

当屏幕发生旋转时,若是咱们没有在AndroidManifest.xml中,对其configChanges属性进行声明,那么就会致使Activity进行重建,此时,就须要从新加载Activity所须要展现的数据。

此时,咱们就能够对其进行以下的声明:

android:configChanges="keyboardHidden|orientation|screenSize"
复制代码

接着在ActivityonConfigurationChanged进行监听,对布局进行相应的改变,而不须要从新加载数据。

(6) 处理字符串拼接

在代码中,咱们常用到字符串拼接的操做,这里有两点注意:

采用高效的拼接方式

例以下面的操做,就会建立大量的临时对象:

public static void badString() {
        String result = "result";
        String append = "append";
        for (int i = 0; i < (1 << 30); i++) {
            result += append;
        }
    }
复制代码

内存检测的结果以下,能够发现,咱们出现了大量内存抖动的状况:

而若是咱们采用 StringBuilder的方式进行拼接:

public static void goodString() {
        StringBuilder result = new StringBuilder("result");
        String append = "append";
        for (int i = 0; i < (1 << 20); i++) {
            result.append(append);
        }
    }
复制代码

那么最终的结果为:

所以,在处理字符串拼接的时候,应当尽可能避免直接使用"+"号,而是使用如下两种方式的一种:

  • 使用静态方法,String.format方法进行拼接。
  • 非线程安全的StringBuilder,或者是线程安全的StringBuffer

避免没必要要的字符串拼接

当咱们须要打印Log时,通常会将它们写在一个公共类中,而后使用一个DEBUG开关,让他们在外发版本上关闭:

private static final boolean DEBUG = true;
    
    public static void LogD(String tag, String msg) {
        if (DEBUG) {
            Log.d(tag, msg);
        }
    }
复制代码

可是这种方式有一点弊端,就是,咱们在调用该方法时msg通常都是经过拼接多个字符串进行传入的,也就是说,即便没有打印该Log,也会进行字符串拼接的操做,所以,咱们应当尽可能将DEBUG开关放在字符串拼接的外部,避免没必要要拼接操做。

(7) 减小没必要要的异常

在某些时候,若是咱们能预见到某些有可能会发生异常的场景,那么提早进行判断,将能够避免因为异常所带来的代价,以启动第三方应用为例,咱们能够先判断该intent所对应的应用是否存在,再去启动它,而不是等到异常发生时再去捕获:

public static void startApp(Context context) {
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("www.qq.com"));
        intent.setComponent(new ComponentName("com.android.browser", "com.android.browser.BrowserActivity"));
        if (intent.resolveActivity(context.getPackageManager()) == null) {
            return;
        }
        try {
            context.startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
复制代码

(8) 线程复用

当执行异步操做时,不要经过new Thread的方式启动一个新的线程来执行操做,而是尽量地对已经建立的线程进行复用,通常来讲,主要有两种方式:

(9) 合理的适应对象池

例如,咱们最经常使用的Handler发送消息,当须要建立一个消息时,可使用Handler提供的obtainMessage方法,获取到Message对象,其内部,就会去Message中所维护的一个静态链表中,查找当前可用的Message对象,并将其标志位置为0,代表其正在使用。

使用对象池时,应当注意两点:

  • 将对象放回对象池时,注意初始化,防止出现脏数据。
  • 合理的控制对象池的增加,防止出现大量无用对象。

(10) 使用 inBitmap 对内存块复用

inBitmap指的是复用内存块,不须要从新给这个Bitmap申请一块新的内存,避免了一次内存的分配和回收,关于inBitmap的详细解释,能够参见这篇文章,Managing Bitmap Memory,其Demo对应的下载地址,对于inBItmap属性的使用,有如下两点限制:

  • 该属性只能在3.0以后使用,在2.3上,bitmap的数据是存储在native的内存区域中。
  • 4.4以前,inBitmap只能重用相同大小的bitmap内存区域,而在4.4以后,能够重用任何bitmap内存区域,只要这块内存比将要分配的内存大就能够。

(11) 使用注解替代枚举

public class Constant {

    public static final int FLAG_START = 0;
    public static final int FLAG_STOP = 1;
    public static final int FLAG_PAUSE = 2;

    @IntDef({FLAG_START, FLAG_STOP, FLAG_PAUSE})
    public @interface VideoState {}
}
复制代码

当咱们定义的形参时,在参数以前,加上以前定义的注解:

public static void accept(@Constant.VideoState int videoState) {
        Log.d("OptUtils", "state=" + videoState);
    }
复制代码

若是咱们传入了不属于上面的三个值,那么IDE就会警告咱们:

(12) 谨慎初始化 Application

当咱们在项目当中,引入一些第三方库,或者将一些组件放到其它进程,加入咱们自定义了Application的子类,而且在AndroidManifest.xml中进行了声明,那么在启动这些运行在其它进程中的组件时,就会调用该ApplicationonCreate()方法,此时,咱们就应当根据进程所要求的资源进行初始化。

例以下面,咱们将RemoteActivity声明在remote进程当中,而且给application指定了自定义的OptApplication

<application
        android:name=".OptApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".OptActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".RemoteActivity" android:process=":remote"/>
    </application>
复制代码

OptApplication中,判断一下调用该方法进程名,进行不一样逻辑的初始化操做:

public class OptApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        if (isMainProcess()) {
            //对主进程的资源进行初始化。
            Log.d("OptApplication", "isMainProcess=" + true);
        } else {
            //对其它进程资源进行初始化。
            Log.d("OptApplication", "isMainProcess=" + false);
        }
    }

    private boolean isMainProcess() {
        ActivityManager am = ((ActivityManager) getSystemService(Context.ACTIVITY_SERVICE));
        List<ActivityManager.RunningAppProcessInfo> process = am.getRunningAppProcesses();
        String mainProcessName = getPackageName();
        int myPid = android.os.Process.myPid();
        for (ActivityManager.RunningAppProcessInfo info : process) {
            if (info.pid == myPid && mainProcessName.equals(info.processName)) {
                return true;
            }
        }
        return false;
    }
}
复制代码

(13) 避免在 onDraw 方法中建立对象

onDraw方法中建立临时对象,不只会影响绘制的性能,并且这些临时对象在onDraw方法执行完以后又很快被回收,那么将会形成内存抖动。

(14) 合理地使用 ArrayMap 替代 HashMap

前面咱们介绍了SparseArray,它的局限性是其key值只能为原始数据类型int,而若是咱们要求它的key值为引用类型时,那么能够考虑使用ArrayMap

SparseArray同样,它会对key使用二分法进行添加、查找、删除等操做,在添加、删除、查找数据的时候都是先使用二分查找法获得相应的index,而后经过index进行添加、查找、删除操做。

若是在数据量较大的状况,那么它的性能将退化至少50%

(15) 谨慎使用抽象编程

抽象可以提高代码的灵活性与可维护性,然而,抽象会致使一个显著的额外内存开销:它们须要同等量的代码用于可执行,这些代码会被mapping到内存中。

(16) 使用 Protocol Buffers

在平时的网络数据传输时,通常用的最多的是JSON或者xml,而Protocal BuffersGoogle为序列化结构数据而设计的,相比于普通的数据传输方式,它具备如下优势:

  • 编码/解码方式简单
  • 序列化 & 反序列化 & 速度快
  • 数据压缩效果更好

关于Protocol Buffers的详细介绍,你们能够阅读 Carson_Ho 所写的一系列文章,推荐阅读:Protocol Buffer 序列化原理大揭秘 - 为何Protocol Buffer性能这么好?

(17) 谨慎使用依赖注入框架

诸如Guice或者RoboGuice这些依赖注入框架,它们能够减小大量findViewById的繁琐操做,可是这些注解的框架为了要搜寻代码中的注解,一般都须要经历较长的初始化过程,而且还可能将一些你用不到的对象也一并加载到内存当中,这些用不到的对象会一直占用内存空间,等到好久以后才释放。

(18) 谨慎使用多进程

在咱们有大量须要运行在后台的任务,例如音乐、视频、下载等业务,那么能够将它们放在独立的进程当中。可是,咱们不该当滥用它们,由于每建立一个新的进程,那么必然要分配一些内存来保存该进程的一些信息,这都将增长内存的占用。

(19) 使用 ProGurad 优化代码

经过ProGuard对代码进行优化、压缩、混淆,能够移除不须要的代码、重命名类、域与方法等,作法就是在buildTypes的指定类型下增长下面的代码:

buildTypes {
        release {
            //对于release版本采用进行混淆。
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
        }
        debug {
            //对于debug版本不混淆。
            minifyEnabled false
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
        }
复制代码

这里的混淆规文件有两份,若是有多份,那么可使用逗号分隔,第一个是Android自带的混淆文件,而第二个则是应用自定义的混淆规则文件,关于混淆文件的语法,能够参考这篇文章: ProGuard 代码混淆技术详解

(20) 谨慎使用第三方 Library

在项目中引入第三方Library时,应当注意如下几点:

  • 不要导入无用的功能:若是须要使用到定位功能,那么就只须要导入定位的Library便可,不要引入导航等Library
  • 不要导入功能重复的Library:目前存在不少开源的第三方网络框架,例如Volley/OkHttp/Retrofit等,那么在咱们引入一个新的网络框架时应当先检查代码中原有的网络框架,将以前的代码都替换成为新的框架,而不是导入多份。
  • 使用为移动平台定制的Library:不少开源项目都会针对移动平台进行项目的优化与裁剪,咱们应当首先考虑使用拥有这些版本的开源库。

(21) 使用 AnimatedVectorDrawable 替换帧动画

图片压缩知识梳理(6) - VectorDrawable 及 AnimatedVectorDrawable 使用详解 中,咱们介绍了AnimatedVectorDrawable的使用,在须要实现一些简单图形的动画时,它比帧动画效率更高、占用内存更小。

(22) 读取和屏幕分辨率匹配的图片

当咱们读取图片时,应当尽可能结合当前手机的分辨率进行处理,这里有两点建议:

  • 在图片加载到内存以前,对其进行缩放,避免加载进入过大的图片,以从资源文件中读取图片为例,咱们传入预期的宽高,先将Bitmap.ConfiginJustDecodeBounds置为true,获取到目标图片的宽高而不是将整张图片都加载到内存中,在根据预期的宽高计算出一个比例,去加载一个适合屏幕分辨率的图片,具体的操做以下面的代码块所示:
public static int calculateInSampleSize(BitmapFactory.Options options, int dstWidth, int dstHeight) {
        int srcWidth = options.outWidth;
        int srcHeight = options.outHeight;
        int inSampleSize = 1;
        if(srcHeight > dstHeight && srcWidth > dstHeight) {
            int halfWidth = srcWidth / 2;
            int halfHeight = srcHeight / 2;
            while ((halfHeight / inSampleSize) > dstHeight && (halfWidth / inSampleSize) > dstWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }

    public static Bitmap decodeResource(Resources res, @DrawableRes int resId, Bitmap.Config config, int dstWidth, int dstHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = config;
        if(dstWidth <= 0 && dstHeight <= 0) {
            return BitmapFactory.decodeResource(res, resId, options);
        }
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        options.inSampleSize = calculateInSampleSize(options, dstWidth, dstHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }
复制代码
  • 将图片放在与屏幕分辨率匹配的文件夹当中

图片基础知识梳理(2) - Bitmap 占用内存分析 一文当中,咱们分析过,在res目录下能够创建多个不一样的图片文件夹,即drawable-xhpi/drawable-xxhdpi/drawable-xxxhdpi,只有当图片放在机型对应分辨率下的文件夹时,才不会进行缩放操做,若是某张图片放在比它分辨率低的文件夹当中,那么将会进行放大操做,不只会使图片变得模糊,还要占用额外的内存。

所以,咱们应当将图片放在对应机型分辨率的文件夹当中。

3、保证不使用对象的释放

(1) 避免 Activity 泄露

Activity泄露是咱们在开发中最长碰见的内存泄露类型,下面总结几点你们比较容易犯的错误:

在 Activity 中定义非静态的 Handler 内部类

例以下面这样,咱们在Activity中定义了一个非静态的内部类LeakHandler,那么做为内部类,leakHandler默认持有外部类的实例,也就是LeakActivity

public class LeakActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LeakHandler leakHandler = new LeakHandler();
        leakHandler.sendEmptyMessageDelayed(0, 50000);
    }
    
    private class LeakHandler extends Handler {

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }
}
复制代码

在调用了sendEmptyMessageDelayed以后,那么会建立一个Message对象放到Looper的队列MessageQueue当中等待被执行,而该Message中的target会执行发送它的Handler,也就是LeakHandler,那么在该消息被处理以前,会一直存在一条从LeakActivityMessageQueue的引用链,所以,在这段时间内若是Activity被销毁,它的内存也没法释放,就是形成内存泄露。

对于这种问题,有如下几个处理的技巧:

  • Handler定义为静态内部类,这样它就不会持有外部的类的引用,若是须要在handleMessage中调用Activity中的方法,那么能够传入它做为参数,并持有它的弱引用以保证它可以回收。
private static class SafeHandler extends Handler {
        
        private WeakReference<Activity> mActHolder;
        
        public SafeHandler(Activity activity) {
            mActHolder = new WeakReference<>(activity);    
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (mActHolder != null) {
                Activity activity = mActHolder.get();
                if (activity != null && !activity.isDestroyed()) {
                    //仅在 Activity 没有被销毁时,才执行操做。
                }
            }
        }
    }
复制代码
  • ActivityonDestroy()方法中,经过removeCallbacksAndMessages(null)方法移除全部未执行的消息。

单例中的成员变量或者 static 成员变量持有了 Activity 的引用

根据持有的方式,能够简单地分为直接持有、间接持有两种类型:

  • 直接持有:在Android的不少Api中,都会使用到上下文信息Context,而Activity继承于Context类,所以咱们常常会将它传给其它类,并将它做为这些类的成员变量以便后续的操做,那么若是这个成员变量所属的类是一个单例,或者说它是该类中的一个静态成员变量,那么就会致使该Activity所占用的内存没法被释放。
  • 间接持有:某个中间对象持有了Activity,而该中间对象又做为了单例中的成员变量或者某类中的static成员变量,这些对象最多见的有如下两类: (a) Activity的非静态内部类,例如监听器,那么它就会默认持有Activity的引用。 (b) Activity中的控件,其mContext变量指向了它所属的Activity

当出现这种状况时,咱们应当注意这几点:

  • 若是可使用ApplicationContext,那么就用Activity.getApplicationContext()来替代,不要用Activity
  • 若是必须使用Activity,那么确保在ActivityonDestroy()方法执行时,将它们到Activity的引用链千方百计切断,将引用设为空,或者注销监听器。

固然不只是Activity,对于应用当中的某些大对象,例如Bitmap等,咱们也应当注意,是否出了相似于上面这种直接和间接引用的状况。

(2) 对于只执行一次的后台任务,使用 IntentService 替代 Service

当咱们须要将某些任务的生命周期和Activity分离开来,那么通常会使用Service,可是Service就须要咱们进行手动管理,若是忘记,那么将会致使额外的内存占用,而且拥有Service进程的oom_adj值通常会高于没有Service的进程,系统会更倾向于将它保留。

对于一些短时的后台任务,咱们能够考虑采用IntentService,它的onHandleIntent回调是在异步线程中执行的,而且任务执行完毕后,该Service会自动销毁,不须要手动管理。

(3) 在 onLowMemory() / onTrimMemory() 回调当中,释放没必要要的资源

为了能让各个应用知晓当前系统内存的使用状况,提供了两种类型的回调onLowMemoryonTrimMemory,在ApplicationActivityFragementServiceContentProvider这些组件中,均可以收到这两个回调,进行相应的处理。

onLowMemory

当最后一个后台应用(优先级为background的进程)被杀死以后,前台应用就会收到onLowMemory回调。

onTrimMemory(int level)

onLowMemory相比,onTrimMemory的回调更加频繁,每次计算进程优先级时,只要知足对应的条件,就会触发。level参数则代表了当前内存的占用状况,各等级的解释以下表所示,等级从上到下,进程被杀的可能性逐渐增大:

咱们应当根据当前的等级,释放掉一些没必要要的内存,以避免应用进程被杀死。

(4) 及时关闭 Cursor

不管是使用数据库,仍是ContentProvider来查询数据,在查询完毕以后,必定要记得关闭Cursor

4、使用性能优化工具,定位内存问题

关于内存的优化工具,以前一系列的文章已经介绍过了,你们能够查看下面这三篇文章:

5、特别鸣谢

以上的总结,借鉴了网上几位大神的总结,特此鸣谢:

参考的文章包括如下几篇:


更多文章,欢迎访问个人 Android 知识梳理系列:

相关文章
相关标签/搜索