打磨APP(一)

Android打造高性能app

“xxx,手机越来越发热了”
“xxx,怎么手机越来越卡了”
“xxx,又崩了”


1–学会申请内存

1.1 分析内存的使用情况


系统不可能将所有的内存都分配给我们的应用程序,每个程序都会有可使用的内存上限,被称为堆大小。不同的手机堆大小不同,如下代码可以获得堆大小:
结果以MB为单位进行返回,我们开发时应用程序的内存不能超过这个限制,否则会出现OOM。

为了维持多任务的功能环境,Android为每一个app都设置了一个硬性的堆大小(heap size)限制。准确的heap size限制会因为不同设备的不同RAM大小而各有差异。如果你的app已经到了heap的限制大小并且再尝试分配内存的话,会引起OutOfMemoryError的错误。

在一些情况下,我们也许想要查询当前设备的heap size限制大小是多少,然后决定cache的大小。可以通过getMemoryClass()来查询。

ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
int heapSize = manager.getMemoryClass();

这个方法会返回一个整数,表明你的应用的heap size限制是多少Mb(megabates)。

  * the baseline Android memory class is
  * 16 (which happens to be the Java heap limit of those devices); some
  * device with more memory may return 24 or even higher numbers.

1.2 知晓内存的开支情况

  • 使用枚举通常会比使用静态常量消耗两倍以上的内存,尽可能不使用枚举
  • 任何一个Java类,包括匿名类、内部类,都要占用大概500字节的内存空间
  • 任何一个类的实例要消耗12-16字节的内存开支,因此频繁创建实例也是会在一定程序上影响内存的
  • 使用HashMap时,即使你只设置了一个基本数据类型的键,比如说int,但是也会按照对象的大小来分配内存,大概是32字节,而不是4字节,因此最好使用优化后的数据集合

1.3 谨慎使用抽象编程

在Android使用抽象编程会带来额外的内存开支,因为抽象的编程方法需要编写额外的代码,虽然这些代码根本执行不到,但是也要映射到内存中,不仅占用了更多的内存,在执行效率上也会有所降低。所以需要合理的使用抽象编程。

1.4 尽量避免使用依赖注入框架

使用依赖注入框架貌似看上去把findViewById()这一类的繁琐操作去掉了,但是这些框架为了要搜寻代码中的注解,通常都需要经历较长的初始化过程,并且将一些你用不到的对象也一并加载到内存中。这些用不到的对象会一直占用着内存空间,可能很久之后才会得到释放,所以可能多敲几行代码是更好的选择。

1.5 使用更加轻量的数据结构

Android提供了一系列优化过后的数据集合工具类,如ArrayMap、SparseArray、SparseBooleanArray、LongSparseArray,使用这些API可以让我们的程序更加高效。相比起Android专门为移动操作系统编写的ArrayMap容器,HashMap在大多数情况下,都显示效率低下,更占内存。通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效,在于他们避免了对key与value的自动装箱(Autoboxing),并且避免了装箱后的解箱。
看看SparseArray的介绍:

 * It is intended to be more memory efficient
 * than using a HashMap to map Integers to Objects, both because it avoids
 * auto-boxing keys and its data structure doesn't rely on an extra entry object
 * for each mapping.

1.6 使用多个进程

谨慎使用,多数应用程序不该在多个进程中运行的,一旦使用不当,它甚至会增加额外的内存而不是帮我们节省内存。这个技巧比较适用于哪些需要在后台去完成一项独立的任务,和前台是完全可以区分开的场景。比如音乐播放,关闭软件,已经完全由Service来控制音乐播放了,系统仍然会将许多UI方面的内存进行保留。在这种场景下就非常适合使用两个进程,一个用于UI展示,另一个用于在后台持续的播放音乐。关于实现多进程,只需要在Manifast文件的应用程序组件声明一个android:process属性就可以了。

2–学会释放内存

2.1 当界面不可见时释放内存

当用户打开了另外一个程序,我们的程序界面已经不可见的时候,我们应当将所有和界面相关的资源进行释放。重写Activity的onTrimMemory()方法,然后在这个方法中监听TRIM_MEMORY_UI_HIDDEN这个级别,一旦触发说明用户离开了程序,此时就可以进行资源释放操作了。

2.2 当内存紧张时释放内存

onTrimMemory()方法还有很多种其他类型的回调,可以在手机内存降低的时候及时通知我们,我们应该根据回调中传入的级别来去决定如何释放应用程序的资源。

2.3 避免在Bitmap上浪费内存

读取一个Bitmap图片的时候,千万不要去加载不需要的分辨率。可以压缩图片等操作。
释放Bitmap

if(bitmap != null && !bitmap.isRecycled()){
  // 回收并且置为null
  bitmap.recycle();
  bitmap = null;
}
System.gc();

ImageView回收

        Drawable drawable = imageView.getDrawable();
        if (drawable != null) {
            if (drawable instanceof BitmapDrawable) {
                BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
                Bitmap bitmap = bitmapDrawable.getBitmap();
                if (bitmap != null && !bitmap.isRecycled())
                    bitmap.recycle();
                bitmap = null;
            }
        }

3– 学会善待内存

强引用+静态变量+长生命周期=不能愉快的玩耍~

Android中的垃圾回收机制并不能防止内存泄漏的出现,导致内存泄漏最主要的原因就是某些长存对象持有了一些其它应该被回收的对象的引用,导致垃圾回收器无法去回收掉这些对象,也就是出现内存泄漏了。

3.1 场景1-内部类

像Activity这样的系统组件,它会包含很多的控件(包括自定义)甚至是图片,如果它无法被垃圾回收器回收掉的话,那就算是比较严重的内存泄漏情况了。
举个例子,在MainActivity中定义一个内部类,实例化内部类对象,在内部类新建一个线程执行死循环,因为内部类持有外部类引用,所以MainActivity退出的时候,会导致内部类资源无法释放,MainActivity的控件和资源无法释放,导致OOM。

3.2 场景2-static Context

在类中定义了静态Activity变量,把当前运行的Activity实例赋值于这个静态变量!
比如说

    public static Context context;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context = this;
    }

然后退出的时候,因为是被静态变量引用了,所以不会被系统回收,就会内存溢出@

3.3 场景3-static View

类似的情况会发生在单例模式中,如果Activity经常被用到,那么在内存中保存一个实例是很实用的。正如之前所述,强制延长Activity的生命周期是相当危险而且不必要的,无论如何都不能这样做。

特殊情况:如果一个View初始化耗费大量资源,而且在一个Activity生命周期内保持不变,那可以把它变成static,加载到视图树上(View Hierachy),像这样,当Activity被销毁时,应当释放资源。(示例代码中并没有释放内存,把这个static view置null即可,但是还是不建议用这个static view的方法)
比如

    private static View mView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mView = (View) findViewById(R.id.title_template);
    }

溢出真相

3.4 场景4-匿名类

匿名类也维护了外部类的引用,所以内存泄漏很容易发生~
比如这样,MainActivity中维护一个匿名类AsyncTask:

private void start() {
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                for (; ; ) {

                }
            }
        }.execute();
    }

3.5 场景5

BroadcastReceiver,ContentObserver,FileObserver,Cursor在Activity onDestroy或者某类生命周期结束之后一定要unregister或者close掉,否则这个Activity类会被system强引用,不会被内存回收。

3.6 场景6

如果应用程序需要使用Service来执行后台任务的话,只有当任务正在执行的时候才应该让Service运行起来。当启动一个Service时,系统会倾向于将这个Service所依赖的进程进行保留,系统可以在LRUcache当中缓存的进程数量也会减少,导致切换程序的时候耗费更多性能。我们可以使用IntentService,当后台任务执行结束后会自动停止,避免了Service的内存泄漏。

4–学会高性能编码

4.1 避免创建不必要的对象

不必要的对象我们应该避免创建:

  • 如果有需要拼接的字符串,那么可以优先考虑使用StringBuffer或者StringBuilder来进行拼接,而不是加号连接符,因为使用加号连接符会创建多余的对象,拼接的字符串越长,加号连接符的性能越低。
  • 在没有特殊原因的情况下,尽量使用基本数据类型来代替封装数据类型,int比Integer要更加有效,其它数据类型也是一样。
  • 当一个方法的返回值是String的时候,通常需要去判断一下这个String的作用是什么,如果明确知道调用方会将返回的String再进行拼接操作的话,可以考虑返回一个StringBuffer对象来代替,因为这样可以将一个对象的引用进行返回,而返回String的话就是创建了一个短生命周期的临时对象。
  • 基本数据类型的数组也要优于对象数据类型的数组。另外两个平行的数组要比一个封装好的对象数组更加高效,举个例子,Foo[]和Bar[]这样的数组,使用起来要比Custom(Foo,Bar)[]这样的一个数组高效的多。

尽可能地少创建临时对象,越少的对象意味着越少的GC操作。

4.2 静态优于抽象

如果你并不需要访问一个对系那个中的某些字段,只是想调用它的某些方法来去完成一项通用的功能,那么可以将这个方法设置成静态方法,调用速度提升15%-20%,同时也不用为了调用这个方法去专门创建对象了,也不用担心调用这个方法后是否会改变对象的状态(静态方法无法访问非静态字段)。

4.3 对常量使用static final修饰符

static int intVal = 66;  
static String strVal = "Hi, girl!";  

编译器会为上面的代码生成一个初始方法,称为方法,该方法会在定义类第一次被使用的时候调用。这个方法会将66的值赋值到intVal当中,从字符串常量表中提取一个引用赋值到strVal上。当赋值完成后,我们就可以通过字段搜寻的方式去访问具体的值了。

final进行优化:

static final int intVal = 66;  
static final String strVal = "Hi, girl!";  

这样,定义类就不需要方法了,因为所有的常量都会在dex文件的初始化器当中进行初始化。当我们调用intVal时可以直接指向66的值,而调用strVal会用一种相对轻量级的字符串常量方式,而不是字段搜寻的方式。

这种优化方式只对基本数据类型以及String类型的常量有效,对于其他数据类型的常量是无效的。

4.4 使用增强型for循环语法

增强的For循环(也被称为 for-each 循环)可以被用在实现了 Iterable 接口的 collections 以及数组上。使用collection的时候,Iterator会被分配,用于for-each调用hasNext()和next()方法。使用ArrayList时,手写的计数式for循环会快3倍(不管有没有JIT),但是对于其他collection,增强的for-each循环写法会和迭代器写法的效率一样。

static class Counter {  
    int mCount;  
}  

Counter[] mArray = ...  

public void zero() {  
    int sum = 0;  
    for (int i = 0; i < mArray.length; ++i) {  
        sum += mArray[i].mCount;  
    }  
}  

public void one() {  
    int sum = 0;  
    Counter[] localArray = mArray;  
    int len = localArray.length;  
    for (int i = 0; i < len; ++i) {  
        sum += localArray[i].mCount;  
    }  
}  

public void two() {  
    int sum = 0;  
    for (Counter a : mArray) {  
        sum += a.mCount;  
    }  
}  

zero()最慢,每次都要计算mArray的长度,one()相对快得多,two()在没有JIT(Just In Time Compiler)的设备上是运行最快的,而在有JIT的设备上运行效率和one()方法不相上下,需要注意这种写法需要JDK1.5之后才支持。

下面我亲次做了测试,有图有真相:

public void fillArray() {
        mArray = new Counter[10000];
        for (int i = 0; i < 10000; i++) {
            mArray[i] = new Counter();
            mArray[i].mCount = i;
        }
}

先装填数据,然后打印每个方法的执行时间
华为荣耀7测试

这里时间单位是纳秒,很明显的看见zero()的速度相比而言像蜗牛一样

4.5 多使用系统封装好的API

系统提供不了的Api完成不了我们需要的功能才应该自己去写,因为使用系统的Api很多时候比我们自己写的代码要快得多,它们的很多功能都是通过底层的汇编模式执行的。
举个例子,实现数组拷贝的功能,使用循环的方式来对数组中的每一个元素一一进行赋值当然可行,但是直接使用系统中提供的System.arraycopy()方法会让执行效率快9倍以上。

4.6 避免在内部调用Getters/Setters方法

面向对象中封装的思想是不要把类内部的字段暴露给外部,而是提供特定的方法来允许外部操作相应类的内部字段。但在Android中,字段搜寻比方法调用效率高得多,我们直接访问某个字段可能要比通过getters方法来去访问这个字段快3到7倍。但是编写代码还是要按照面向对象思维的,我们应该在能优化的地方进行优化,比如避免在内部调用getters/setters方法。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值