Android性能优化

时间:2021-7-21 作者:qvyue

1.如何避免过度绘制(Overdraw)

应用可能会在单个帧内多次绘制同一个像素,这种情况称为“过度绘制”。过度绘制通常是不必要的,最好避免。它会浪费 GPU 时间来渲染与用户在屏幕上所见内容无关的像素,进而导致性能问题。

Android性能优化
过度绘制.png

地址

解决
A.合理选择控件容器

首先想到的是解决布局问题,推荐使用ConstraintLayout,减少层级。

B.去掉Window的默认背景

当我们使用了Android自带的一些主题时,Window会被默认添加一个纯色的背景,这个背景是被DecorView持有的。
当我们的自定义布局时又添加了一张背景图或者设置背景色,那么DecorView此时的background对我们来说是无用的,但是它会产生一次Overdraw,带来绘制性能损耗。
去掉Window的背景可以在onCreate()中setContentView()之后调用

getWindow().setBackgroundDrawable(null); 

或者在主题中设置

@null
C.去掉其他不必要的背景

有时候为了方便会先给Layout设置一个整体的背景,再给子View设置背景,这里也会造成重叠,如果子View宽度mach_parent,可以看到完全覆盖了Layout的一部分,这里就可以通过分别设置背景来减少重绘。
再比如如果采用的是selector的背景,将normal状态的color设置为”@android:color/transparent”,也同样可以解决问题。

D.clipRect()和quickReject()

为了解决Overdraw的问题,Android系统会通过避免绘制那些完全不可见的组件来尽量减少消耗。
但是不幸的是,对于那些过于复杂的自定义的View(通常重写了onDraw方法),Android系统无法检测在onDraw里面具体会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。
但是我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。
这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。
这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。
同时clipRect()还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。
除了clipRect()之外,我们还可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。

E.ViewStub

ViewStub是个什么东西?一句话总结:高效占位符。
我们经常会遇到这样的情况,运行时动态根据条件来决定显示哪个View或布局。
常用的做法是把View都写在上面,先把它们的可见性都设为View.GONE,然后在代码中动态的更改它的可见性。
这样的做法的优点是逻辑简单而且控制起来比较灵活。但是它的缺点就是,耗费资源。
虽然把View的初始可见View.GONE但是在inflate布局的时候View仍然会被inflate,也就是说仍然会创建对象,会被实例化,会被设置属性。也就是说,会耗费内存等资源。
推荐的做法是使用android.view.ViewStub,ViewStub是一个轻量级的View,它一个看不见的,不占布局位置,占用资源非常小的控件。
可以为ViewStub指定一个布局,在inflate布局的时候,只有ViewStub会被初始化,然后当ViewStub被设置为可见的时候,或是调用了ViewStub.inflate()的时候,ViewStub所向的布局就会被Inflate和实例化,然后ViewStub的布局属性都会传给它所指向的布局。
这样,就可以使用ViewStub来方便的在运行时,要还是不要显示某个布局。

public class MainActivity extends AppCompatActivity {

    private ViewStub vs;
    private AppCompatTextView tv;

    private boolean isInflate;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        vs = (ViewStub) findViewById(R.id.view_stub_tv);
    }

    public void showOrHideViewStub(View view) {
        if (tv != null && tv.getVisibility() == View.VISIBLE) {
            tv.setVisibility(View.GONE);
        } else {
            if (isInflate) {
                tv.setVisibility(View.VISIBLE);
            } else {
                tv = (AppCompatTextView) vs.inflate();
                isInflate = true;
            }
        }
    }

}
F.Merge

Merge标签有什么用呢?简单粗暴点回答:干掉一个view层级。
Merge的作用很明显,但是也有一些使用条件的限制。
有两种情况下我们可以使用Merge标签来做容器控件。
第一种子视图不需要指定任何针对父视图的布局属性,就是说父容器仅仅是个容器,子视图只需要直接添加到父视图上用于显示就行。
另外一种是假如需要在LinearLayout里面嵌入一个布局(或者视图),而恰恰这个布局(或者视图)的根节点也是LinearLayout,这样就多了一层没有用的嵌套,无疑这样只会拖慢程序速度。而这个时候如果我们使用merge根标签就可以避免那样的问题。
另外Merge只能作为XML布局的根标签使用,当inflate以开头的布局文件时,必须指定一个父ViewGroup,并且必须设定attachToRoot为true.

G.善用.9

给ImageView加一个边框,你肯定遇到过这种需求,通常在ImageView后面设置一张背景图,露出边框便完美解决问题,此时这个ImageView,设置了两层drawable,底下一层仅仅是为了作为图片的边框而已。
但是两层drawable的重叠区域去绘制了两次,导致overdraw。
所以可以将背景drawable制作成.9,并且将和前景重叠的部分设置为透明。
由于Android的2D渲染器会优化.9中的透明区域,从而优化了这次overdraw。
但是背景图片必须制作成.9才行,因为Android 2D渲染器只对.9有这个优化,否则,一张普通的Png,就算你把中间的部分设置成透明,也不会减少这次overdraw。

H.慎用Alpha

假如对一个View做Alpha转化,需要先将View绘制出来,然后做Alpha转化,最后将转换后的效果绘制在界面上。
通俗点说,做Alpha转化就需要对当前View绘制两遍,可想而知,绘制效率会大打折扣,耗时会翻倍,所以Alpha还是慎用。
如果一定做Alpha转化的话,可以采用缓存的方式。

view.setLayerType(LAYER_TYPE_HARDWARE);
XXXXX
view.setLayerType(LAYER_TYPE_NONE);

通过setLayerType方式可以将当前界面缓存在GPU中,这样不需要每次绘制原始界面,但是GPU内存是相当宝贵的,所以用完要马上释放掉。

2.布局优化

A.在LinearLayout和RelativeLayout都可以完成布局的情况下优先选择 RelativeLayout,可以减少View的层级(现在推荐使用ConstraintLayout)。
B.将常用的布局组件抽取出来使用标签。
C.通过标签来加载不常用的布局。
D.使用标签来减少布局的嵌套层次。

3.网络优化

A.尽量减少网络请求,能够合并的就尽量合并。
B.避免DNS解析,根据域名查询可能会耗费上百毫秒的时间,也可能存在DNS劫持的风险。可以根据业务需求采用增加动态更新 IP 的方式,或者在 IP 方式访问失败时切换到域名访问方式。
C.大量数据的加载采用分页的方式。
D.网络数据传输采用GZIP压缩。
E.加入网络数据的缓存,避免频繁请求网络。
F.上传图片时,在必要的时候压缩图片。

5.安装包优化

A.使用混淆,可以在一定程度上减少apk体积。
B.减少应用中不必要的资源文件,比如图片,在不影响APP效果的情况下尽量压缩图片,有一定的效果。
C.在使用了SO库的时候优先保留v7版本的SO库,删掉其他版本的SO库。v7 版本的SO库可以满足市面上绝大多数的要求。

4.内存优化

为什么会产生内存泄漏?

当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

内存泄漏对程序的影响?

内存泄漏是造成应用程序OOM的主要原因之一!我们知道Android系统为每个应用程序分配的内存有限,而当一个应用中产生的内存泄漏比较多时,这就难免会导致应用所需要的内存超过这个系统分配的内存限额,这就造成了内存溢出而导致应用Crash。

常见的内存泄漏汇总
单例造成的内存泄漏

由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。
当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:
A.如果此时传入的是Application的Context,因为Application的生命周期就是整个应用的生命周期,所以这将没有任何问题。
B.如果此时传入的是Activity的Context,当这个Context所对应的Activity退出时,由于该Context的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前Activity退出时它的内存并不会被回收,这就造成泄漏了。
所以可以这么写:

public class Test {

    private static Test instance;
    
    private Context context;

    private Test(Context context) {
        this.context = context.getApplicationContext();//使用Application的Context
    }

    public static Test getInstance(Context context) {
        if (instance == null) {
            instance = new Test(context);
        }
        return instance;
    }

}
静态变量导致的内存泄漏

例如Context对象为静态的,那么当前Activity就无法正常销毁,会常驻内存。

public class TestActivity extends AppCompatActivity {

    public static Context context;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        context = this;
        startActivity(new Intent(this, XXXXXActivity.class));
        finish();
    }

}
Android性能优化
静态变量内存泄漏.png

可以使用Android Studio自带的Profiler工具来查看内存泄漏的对象,可以看到在当前Activity销毁之后,TestActivity仍然存在于内存之中,并没有销毁掉。解决方法也是推荐使用Application的Context。

非静态内部类创建静态实例造成的内存泄漏

有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:

public class TestActivity extends AppCompatActivity {

    private static Test test;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        if (test == null) {
            test = new Test();
        }
    }

    class Test {

    }

}

这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,
不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,
该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。
正确的做法为将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application的Context。
所以可以这么写:

public class Test {

    private static Test instance;
    
    private Context context;

    private Test(Context context) {
        this.context = context.getApplicationContext();//使用Application的Context
    }

    public static Test getInstance(Context context) {
        if (instance == null) {
            instance = new Test(context);
        }
        return instance;
    }

}
public class TestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        Test.getInstance(this);
    }

}
匿名内部类

Android开发经常会继承实现Activity、Fragment或者View,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄露。

public class TestActivity extends AppCompatActivity {

    private Runnable runnable1 = new MyRunnable();
    private Runnable runnable2 = new Runnable() {
        @Override
        public void run() {

        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        runnable1.run();
        runnable2.run();
    }

    private static class MyRunnable implements Runnable {

        @Override
        public void run() {

        }

    }

}

runnable1和runnable2的区别是,runnable2使用了匿名内部类。我们来看看运行时这两个引用的内存:

Android性能优化
匿名内部类.png

可以看到,runnable1没什么特别的,但runnable2这个匿名类的实现对象里面多了一个this$0的引用,
这个引用指向TestActivity.this,也就是说当前的MainActivity实例会被ref2持有,如果将这个引用再传入一个异步线程,此线程和此Acitivity生命周期不一致的时候,就造成了Activity的泄露。

Handler造成的内存泄漏

Handler的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免ANR而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等api都借助Handler来处理,
但Handler不是万能的,对于Handler的使用代码编写一不规范即有可能造成内存泄漏。
另外,我们知道Handler、Message和MessageQueue都是相互关联在一起的,万一Handler发送的Message尚未被处理,则该Message及发送它的Handler对象将被线程MessageQueue一直持有。
由于Handler属于TLS(Thread Local Storage) 变量,生命周期和Activity是不一致的。
因此这种实现方式一般很难保证跟View或者Activity的生命周期保持一致,故很容易导致无法正确释放。

public class TestActivity extends AppCompatActivity {

    private Handler handler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }

    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {

            }
        }, 1000 * 60);
        finish();
    }


}

在该TestActivity中声明了一个延迟1分钟执行的消息Message,Handler将其push进了消息队列MessageQueue里。
当该Activity被finish()掉时,延迟执行任务的Message还会继续存在于主线程中,它持有该Activity的Handler引用,所以此时finish()掉的Activity就不会被回收了从而造成内存泄漏(因Handler为非静态内部类,它会持有外部类的引用)。
修复方法:在Activity中避免使用非静态内部类,比如上面我们将Handler声明为静态的,则其存活期跟Activity的生命周期就无关了。
同时通过弱引用的方式引入Activity,避免直接将Activity作为Context传进去,见下面代码:

public class TestActivity extends AppCompatActivity {

    private final MyHandler handler = new MyHandler(this);

    //静态内部类的实例不包含对其外部类的隐式引用
    private static class MyHandler extends Handler {

        private final WeakReference activity;

        public MyHandler(TestActivity activity) {
            this.activity = new WeakReference(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            TestActivity activity = this.activity.get();
            if (activity != null) {

            }
        }

    }

    //静态匿名类的实例不包含对其外部类的隐式引用
    private static final Runnable runnable = new Runnable() {

        @Override
        public void run() {

        }

    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        handler.postDelayed(runnable, 1000 * 60);
        finish();
    }


}
资源未关闭造成的内存泄漏

对于使用了BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

属性动画导致的内存泄漏

常见的例子就是在属性动画执行的过程中退出了Activity,这时View对象依然持有Activity的引用从而导致了内存泄漏。解决办法就是在onDestroy()中调用动画的cancel()取消属性动画。

WebView导致的内存泄漏

WebView比较特殊,即使是调用了它的destroy()方法,依然会导致内存泄漏。其实避免WebView导致内存泄漏的最好方法就是让WebView所在的Activity处于另一个进程中,当这个Activity结束时杀死当前WebView所处的进程即可。

        //如何避免WebView内存泄露1:不在xml中定义WebView ,而是在需要的时创建,并且Context使用getApplicationContext()
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        WebView wv = new WebView(context);
        wv.setLayoutParams(lp);
        layout.addView(wv);
    protected void onDestroy() {
        //如何避免WebView内存泄露2:销毁时先让WebView加载null内容,然后移除WebView,再销毁WebView,最后置空
        if (wv != null) {
            wv.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
            wv.clearHistory();
            ((ViewGroup) wv.getParent()).removeView(wv);
            wv.destroy();
            wv = null;
        }
        super.onDestroy();
    }

6.

7.

8.

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。