qmsggg_BlogCollect icon indicating copy to clipboard operation
qmsggg_BlogCollect copied to clipboard

Android应用性能优化最佳实践

Open qmsggg opened this issue 7 years ago • 38 comments

qmsggg avatar Nov 03 '17 04:11 qmsggg

FPS

FPS(Frames Per Second):表示每秒传递的帧数。在理想情况下,60FPS就感觉到不卡,这也就是说每个绘制时长应该是在16ms以内,即1000ms/60; 但是Android系统很有可能无法及时完成那些复杂的界面渲染操作。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需的60FPS,即为了实现60FPS,就意味着程序的大多数绘制操作都必须在16ms内完成。

qmsggg avatar Nov 03 '17 04:11 qmsggg

http://androidperformance.com/ 领导推荐的一个网址,里面有性能优化相关的内容

qmsggg avatar Nov 03 '17 11:11 qmsggg

Android主线程的主要职责:

  • UI生命周期控制
  • 系统事件处理
  • 消息处理
  • 界面布局
  • 界面绘制
  • 界面刷新

qmsggg avatar Nov 04 '17 05:11 qmsggg

双缓冲

双缓冲:显示内容的数据内存,为什么要用双缓冲,我们知道在LInux上通常使用FrameBuffer来做显示输出,当用户进程跟新FrameBuffer中的数据后,显示驱动会把FrameBuffer中每个像素点的值更新到屏幕,但是这样会带来一个问题,如果上一帧的数据还没显示完,FrameBuffer中的数据又更新来,就会带来残影的问题,给用户直观的感觉就会有闪烁,所以普遍采用来双缓冲技术。双缓冲意味着要使用两个缓冲区(在SharedBUfferStack中),其中一个称为Front Buffer,另外一个称为BackBuffer。UI总是先在BackBUffer中绘制,然后再和Front Buffer交换,渲染到显示设备中。即只有当另一个buffer的数据准备好后,通过io_ctrl来通知显示设备切换Buffer。

qmsggg avatar Nov 04 '17 05:11 qmsggg

GPU

应用层绘制一个页面(View),主要有三个过程:CPU准备数据->GPU从数据缓存列表获取数据->Display设备绘制。这个三个过程的耗时可以通过一个手机开发辅助工具查看:Profile GPU Rendering。PGR是从Android4.1系统开始提供的。 PGR功能特点如下:

  • 它是一个图形监测工具,能实时反应当前绘制的耗时。
  • 横轴表示时间,纵轴表示每一帧的耗时(单位为ms)。
  • 随着时间的推移,从左到右的刷新呈现。
  • 提供来一个标准的耗时,如果高于标准耗时,表示当前这一帧丢失。

不同颜色解释:

  • 每一条柱状图都由4种颜色组成:红,黄,蓝,紫,这些对线对应每一帧在不同阶段的实际耗时。
  • 蓝色代表测量绘制的时间,它代表需要多长时间区创建和更新DisplayList。在Android中,一个视图在进行渲染之前,它必须被转换称GPU熟悉的格式,简单来说就是几条绘图命令,蓝色就是记录了在屏幕上更新视图需要花费的时间,也可以理解为执行每一个View的onDraw方法,创建或者更新每一个View的Display List对象。在蓝色的线很高时,有可能因为需要重新绘制,或者自定义视图的onDraw函数处理事情太多。
  • 红色代表执行的时间,这部分是Android进行2D渲染Display List的时间,为了绘制到屏幕上,Android需要使用OpenGl ES的API接口来绘制Display List,这些API有效的将数据发送到GPU,最终在屏幕上显示出来。当红色的线非常高时,可能是由重新提交来视图而导致的。
  • 橙色部分表示处理时间,或者是CPU告诉GPU渲染一帧的地方,这是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复,如果柱状图很高,就意味着GPU太繁忙了。
  • 紫色段表示将资源转移到渲染线程的时间,只有在Android4.0及以上的版本才会提供。

在实际的开发周宏,从图形上虽然可以看到绘制的时间,但对于不便进行数据分析,比如进入某一个页面,柱状图虽然实时绘制出来,但不能封号地分析,这里可以通过:adb shell dumpsys gfxinfo com..(包名)把具体的耗时输出到日志中来分析。 任何时候超过绿线(警戒线,对应时长16ms),就由可能丢失一帧的内容,虽然对于大部分应用来说,丢失几帧确实感觉不出来卡顿。在用GPR查看了有问题的页面后就可以用Hierarchy Viewer来优化布局。

qmsggg avatar Nov 04 '17 08:11 qmsggg

TraceView:

TraceView是AndroidSDK自带的工具,用来分析函数调用过程,可以对Android的应用程序以及FrameWork层的代码进行性能分析。它是一个图形化的工具,最终会产生一个图表,用于对性能分析进行说明,可以分析到应用具体每一个方法的执行时间,使用可以非常直观简单,分析性能问题很方便。

  1. 使用方法 在使用TraceView分析之前需要得到一个*.trace的文件,然后通过TraceView来分析trace文件的信息,trace文件的获取有两种方式: (1)在DDMS中使用 (2)在代码中使用
    • 在需要开始监控的地方调用android.os.Debug.startMethodTracing().
    • 在需要结束监控的地方调用android.os.Debug.stopMethodTracing().
    • 系统会在SDK卡中创建(trace-name).trace文件
    • 使用traceview打开该文件进行分析. 注:需要使用WRITE_EXTERNAL_STORAGE权限。

2.TraceView视图说明

  • Name:所有的调用项,展开可以看到所有的Parent和Children子项,指调用和被调用
  • Inclusive:统计函数本身运行的时间 + 调用子函数运行的时间
  • Incl: inclusive时间占总时间的百分比
  • Exclusive: 同级函数本身运行的时间
  • Excl: 执行占总时间的百分比
  • Calls+Recur Calls /Total 该方法调用次数 + 递归次数
  • Cpu Time / Call 该方法耗时
  • Real Time / Call 实际时长

使用TraceView查看耗时时,主要关注Calls + Treur Galls / Total 和Cpu Time / Call这两个值,也就是关注调用次数多和耗时久的方法,然后优化这些方法的逻辑和调用次数,减少耗时。

RealTime与CpuTime区别为:因为RealTime包括来CPU的上下文切换,阻塞,GC等,所以RealTime方法的实际执行时间要比CPU Time稍微长一点。

参考文章

qmsggg avatar Nov 04 '17 08:11 qmsggg

SysTrace使用方法

qmsggg avatar Nov 04 '17 09:11 qmsggg

布局优化工具Hierarchy Viewer

qmsggg avatar Nov 04 '17 09:11 qmsggg

布局层级检测 Android Lint

qmsggg avatar Nov 04 '17 09:11 qmsggg

布局优化方法

  • 减少层级

    • 合理使用RelativeLayout和LinearLayout
    • 合理使用Merge ReativeLayout也存在性能低的问题,原因是RelativeLayout会对子View做两次测量,在RelativeLayout中View的排列方式是基于彼此的依赖关系,因为这个依赖关系可能和布局中VIew的顺序并不同,在确定每个子VIew的位置时,需要先给所有子View做一次排序。如果在RelativeLayout中允许子View横向和纵向相互依赖,就需要横向,纵向分别进行一次排序测量。但如果在LinearLayout中有weight属性,也需要进行两次测量,因为没有更多的依赖关系,所以仍然会比RelativeLayout效率高,在布局上RelativeLayout不如LinearLayout快。 但是如果布局本身层次太深,还是推荐使用RelativeLayout减少布局本身层次,相较于测量两次,虽然会增加一些计算时间,但是在体验上影响不会太大,如果优化掉两层布局仅仅是增加一次测量,还是非常值得的,布局层次深会增加内存消耗,甚至引起栈溢出等问题,即使耗点时间,也不能让应用不可用。 根据以上分析,可以总结出一下几点布局原则:
    • 尽量使用RelativeLayout和LinearLayout。
    • 在布局层级相同的情况下,使用LinearLayout。
    • 用LinearLayout有时会使嵌套层级变多,应该使用RelativeLayout,使用面尽量扁平化。
  • Merge的使用 使用Merge的场合主要有以下两种:

    • 在自定义View的使用中,父元素尽量是FrameLayout和LinearLayout。
    • 在Activity中整体的布局,根元素需要时FrameLayout。 原理就是Android布局的源码中,如果是Merge标签,那么直接将其中的子元素添加到Merage标签Parent中。 注:如果Merge 代替的布局元素为LinearLayout,在定义布局代码中将LinearLayout的属性添加到引用上,如垂直布局,背景色等。

使用Merge需要注意:

  • Merge只能用在布局XML文件的根元素
  • 使用merge来加载一个布局时,必须指定一个ViewGroup作为其父元素,并且要设置加载的attachToRoot参数为true(参照inflate(int, ViewGroup, boolean))。
  • 不能在ViewStub中使用Merge标签。原因就是ViewStub的inflate方法中没有attachToRoot的设置。

qmsggg avatar Nov 04 '17 09:11 qmsggg

布局优化方法之提高显示速度

背景:

使用VIEW.GONE等来隐藏View效率非常低,因为它还在布局当中。仍然会测试和解析这些布局。

ViewStub使用

ViewStub是一个轻量级的View,它是一个看不见的,并且不占用布局位置,占用资源非常消的视图对象,可以为ViewStub指定一个布局,加载布局是,只有ViewStub会被初始化,然后当ViewStub被设置为可见时,或者调用了ViewStub.inflate()时,Viewstub所指向的布局会被加载和实例化,然后ViewStub的布局属性都会传给它指向的布局

ViewStub使用注意

  • ViewStub只能加载一次,之后ViewStub对象会被设置为空,换句话说,某个被ViewStub指定的布局被加载后,就不能在通过ViewStub来控制它来,所以它不适用于需要按需来显示隐藏的情况。
  • ViewStub只能用来加载一个布局文件,而不是某个具体的View,当然也可以把View写在某个布局文件中,如果想操作一个具体的View,还是使用visibility属性。
  • ViewStub不能使用嵌套的Merge标签。

ViewStubshying场景

  • 在程序运行期间,某个布局在加载后,就不会有变化,除非销毁该页面再重新绘制加载。
  • 想要控制显示于隐藏一个布局文件,而非某个VIew

qmsggg avatar Nov 04 '17 13:11 qmsggg

布局优化方法之布局复用

include标签

Android的布局复用通过标签来实现,可以把相同的提取出来。

qmsggg avatar Nov 04 '17 14:11 qmsggg

优化自我总结:

1.尽量使用RelativeLayout和LinearLayout,不要使用绝对布局AbsoluteLayout

2.将可复用的组件抽取出来并通过标签使用

3.使用<ViewStub/>标签加载一些不常用的布局

4.使用<merge/>标签减少布局的嵌套层次

5.尽可能少用wrap_content,wrap_content会增加布局measure时的计算成本,已知宽高为固定值时,不用wrap_content

6.删除控件中的无用属性

qmsggg avatar Nov 04 '17 14:11 qmsggg

避免过度绘制

过度绘制(Overdraw)是在在屏幕上的某个像素在同一帧的时间内被绘制来多次。在多层次重叠的UI结构(如带背景的TextView)中,如果不可见的UI也在做绘制的操作,就会导致某些像素区被绘制来多次。

导致过度绘制的原因

  • XML布局 --- 控件有重叠且都有设置背景
  • View自绘 --- View.onDraw里面同一个区域被绘制多次

过度绘制检测工具Show GPU OverDraw

  • 无色: 没有过度绘制,每个像素绘制来1次
  • 蓝色:每个像素多绘制来1次,
  • 绿色:每个像素多绘制来2次
  • 淡红:3次,这个区域不超过屏幕的1/4是可以接受的
  • 深红:4次及以上,需要优化

qmsggg avatar Nov 04 '17 14:11 qmsggg

如何避免过度绘制之布局上的优化

  • 移除XML中非必需的背景,或根据条件设置
  • 移除Window默认的背景
  • 按需显示占位背景图片 使用Android自带的一些主题时,activity往往会被设置一个默认的背景,这个背景由DecorView持有,当自定义布局有一个全屏的背景时,比如设置来这个界面的全屏黑色背景,DecorView的背景此时对我们来说是无用的,但是它会产生一次OverDraw,因此没有必要的话,也可以移除。
protected void onCreate(Bundle savedInstancestate) {
    super.onCreate(savedInstanceState)
    this.getWindow().setBackgroundDrawable(null);
}

针对ListView中的Avatar ImageView的设置,在getView的代码中,判断是否获取对应的Bitmap,获取Avatar的图像之后,把ImageView的Background设置为Transparent,只有当图像没有获取到时,才设置对应的Background占位图片。

qmsggg avatar Nov 04 '17 14:11 qmsggg

自定义View过度绘制优化

Canvas.ClipRect

qmsggg avatar Nov 04 '17 14:11 qmsggg

启动优化之应用启动流程

Android 应用程序的载体是APK文件,其中包括来组件和资源,APK文件可能运行在一个独立的进程中,也有可能产生多个进程,还可以多个APK运行在同一个进程中,可以通过不同的方式来实现。 但需要注意两点:

  • 第一:每个应用只对应一个Application对象,并且启动应用一定会产生一个Application对象;
  • 第二:应用程序可视化组件Activity是应用的基本组成之一;

Application

Application 是Android系统框架中的一个系统组件,Android程序启动时,系统会创建一个Application对象,用来存储系统的一些信息。Android系统会自动在每个程序运行时创建一个Application类的对象,并且只创建一个,可以理解为Application是一个单例类。 应用可以不指定一个具体的Application,系统会自动创建,但一般在开发中都会创建一个继承于系统Applicaton的类实现一些功能,比如一些数据库的创建,模块的初始化等。但这个派生类必须在AndroidManifest.xml中定义好,在application标签增加name属性,并添加自己的Application的类名。 启动Application时,系统会创建一个PID,即进程ID,所有的Activity都会在此进程上运行。在Application创建初始化全局变量,同一个应用的所有Activity都可以读取到这些全局变量的值,Application的生命周期是整个应用程序中最长的,它的生命周期等于这个应用程序的生命周期,因为它是全局的单例的,所以在不同的Activity或者Service中获得的对象都是同一个对象。因此在安卓中要避免使用静态变量来存储长久保存的值,可以用Application,但并不建议使用太多的全局变量。

public class LibApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public void onTerminate() {
        super.onTerminate();
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
    }
}
  • attachBaseContext(Context base):得到应用上下文的Context,在应用创建时首先调用。
  • onCreate():应用创建是调用,晚于attachBaseContext(Context base)方法。
  • onTerminate():应用结束时调用。
  • onConfigurationChanged(Configuration newConfig):系统配置发生变化时调用。
  • onLowMemory(): 系统低内存时调用。
  • onTrimMemory(int level): 系统要求应用释放内存时调用,level为级别。

注意:

在开发过程中,尽量使用Application中的Context实例,因为使用Activity中的Context可能会导致内存泄漏。也可以使用Activity的getApplicationContext方法。

Activity

Activity生命周期金字塔模型

onCreate方法包括一个savedInstanceState参数,在有关重新创建Activity中非常有用。

应用启动的流程

  • 冷启动: 因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,在创建和初始化MainActivity类,最后显示界面
  • 热启动: 因为会从已有的进程中启动,所以热启动不会在创建和初始化Application,而是直接创建和初始化MainActivity,即Application只会初始化一次,只包含Activity中的生命周期。

启动->Applicaiton->attachBaseContext()->onCreate()->Activity生命周期。

qmsggg avatar Nov 06 '17 16:11 qmsggg

启动优化之启动耗时检测

1.adb shell am image

  • ThsTime:一般和TotalTime时间一样,如果在应用启动时开了一个过度的全通明的页面(Activity)预先处理一些事,再显示出主页面(Activity),这样将比TotalTime小。
  • TotalTime:应用的启动时间,包括创建进程+Applicaton初始化+Activity初始化到界面显示。
  • WaitTime:一般比TotalTime大些,包括系统影响的耗时。

2.代码打点

package com.errarehest.android.androidlib.utils.timemonitor;

import android.util.Log;

import java.util.HashMap;
import java.util.Iterator;

/**
 * Created by luohao on 2017/11/7.
 * Created date 2017/11/7
 * 统计耗时的数据结构
 */

public class TimeMonitor {
    private final String TAG = TimeMonitor.class.getSimpleName();
    private int mMonitorId = -1;
    // 保存一个耗时统计模块的各种耗时,tag对应某一个阶段的时间
    private HashMap<String, Long> mTimeTag = new HashMap<>();
    private long mStartTime = 0;

    public TimeMonitor(int id) {
        mMonitorId = id;
    }

    public int getMonitorId() {
        return mMonitorId;
    }

    public void startMoniter() {
        // 每次重新启动,都需要把前面的数据结构清除,避免统计到错误数据
        if (mTimeTag.size() > 0) {
            mTimeTag.clear();
        }
        mStartTime = System.currentTimeMillis();
    }

    // 打一次点,tag交线需要统计的上层自定义
    public void recodingTimeTag(String tag) {
        // 检查是否保存过相同的tag
        if (mTimeTag.get(tag) != null) {
            mTimeTag.remove(tag);
        }

        long time = System.currentTimeMillis() - mStartTime;
        mTimeTag.put(tag, time);
    }

    public void end(String tag, boolean writeLog) {
        recodingTimeTag(tag);
        end(writeLog);
    }

    public void end(boolean writeLog) {
        if (writeLog) {
            // 写入文件
        }
        testShowData();
    }

    public void testShowData() {
        if (mTimeTag.size() <= 0) {
            return;
        }

        Iterator iterator = mTimeTag.keySet().iterator();
        while (iterator.hasNext()) {
            String tag = (String) iterator.next();
            Log.d(TAG, tag + ":" + mTimeTag.get(tag));
        }
    }

    public HashMap<String, Long> getTimeTag() {
        return mTimeTag;
    }
}

package com.errarehest.android.androidlib.utils.timemonitor;

/**
 * Created by luohao on 2017/11/7.
 * Created date 2017/11/7
 */

public class TimeMonitorConfig {
    public static final int TIME_MONITOR_ID_APPLICATION_START = 1;
    public static final int TIME_MONITOR_ID_RV_TEST = 2;
}
package com.errarehest.android.androidlib.utils.timemonitor;

import android.content.Context;

import java.util.HashMap;

/**
 * Created by luohao on 2017/11/7.
 * Created date 2017/11/7
 */

public class TimeMonitorManager {
    private static TimeMonitorManager sTimeMonitorManager = null;
    private static Context sContext = null;

    private HashMap<Integer, TimeMonitor> mTimeMonitorList = null;
    public synchronized static TimeMonitorManager getInstance() {
        if (sTimeMonitorManager == null) {
            sTimeMonitorManager = new TimeMonitorManager();
        }
        return sTimeMonitorManager;
    }

    public TimeMonitorManager() {
        mTimeMonitorList = new HashMap<>();
    }

    public void resetTimeMonitor(int id) {

        // Debug模式不需要计数

        if (mTimeMonitorList.get(id) != null) {
            mTimeMonitorList.remove(id);
        }

        getTimeMonitor(id);
    }

    public TimeMonitor getTimeMonitor(int id) {
        TimeMonitor monitor = mTimeMonitorList.get(id);
        if (monitor == null) {
            monitor = new TimeMonitor(id);
            mTimeMonitorList.put(id, monitor);
        }
        return monitor;
    }
}

public class RvTest extends AppCompatActivity {

    RecyclerView mRvTest;

    Button mBtSet;
    Button mBtAdd;
    Button mBtClear;

    TestAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TimeMonitorManager.getInstance().resetTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_RV_TEST);
        TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_RV_TEST).startMoniter();
        setContentView(R.layout.activity_rv_test);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });

        mAdapter = new TestAdapter();

        mRvTest = (RecyclerView) findViewById(R.id.rvTest);
        mBtSet = (Button) findViewById(R.id.btSetData);
        mBtAdd = (Button) findViewById(R.id.btAddData);
        mBtClear = (Button) findViewById(R.id.btClearData);

        mBtSet.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mAdapter.set(getData());
            }
        });

        mBtAdd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mAdapter.add(getData());
            }
        });

        mBtClear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mAdapter.notifyData(getData2());
            }
        });

        mRvTest.setAdapter(mAdapter);
        mRvTest.setLayoutManager(new MyLinearLayoutManager(this));
    }

    @Override
    protected void onResume() {
        super.onResume();
        TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_RV_TEST).recodingTimeTag("onResume:");
    }

    @Override
    protected void onDestroy() {
        TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_RV_TEST).end(false);

        super.onDestroy();

    }
}

qmsggg avatar Nov 06 '17 17:11 qmsggg

合理的刷新机制

减少刷新次数

  • 控制刷新频率
  • 避免没有必要的刷新 首先需要判断是否需要刷新,比如数据没有变化,需要刷新的控件(View)不在可见区域,就没有必要刷新。如果一个View从不可见到可见,一定要刷新一次。

避免后台线程影响

在列表滚动的时候停止图片加载, example:ImageLoader加载图片

缩小刷新区域

在以下两个场景下可以采用局部刷新的方法来节省更多的资源:

  • 自定义VIew中: 自定义View一般采用invalidata方法刷新。如果需要更新的数据只是在某一个区域内改变,在调用invalidata方法更新这个区域的时,也会更新整个视图,这就浪费了不需要更新的区域资源。Android系统提供了两个局部更新数据的方法:
    • invalidate(Rect dirty);
    • invalidate(int left, int top, int right, int bottom);
  • 第二种就是容器中的某个Item发生了变化,只需要更新这个Item即可。比如在ListView中,如果是单条操作,就必须调用Adatper的notifyDataSetChanged()刷新。

qmsggg avatar Nov 07 '17 05:11 qmsggg

Android tools

一开始不明白,后来删掉这个属性之后发现会出现一个提示:

pick preview layout from the "Fragment Layout" context menu

原来tools:layout仅仅是告诉编辑器,Fragment在程序预览的时候该显示成什么样,并不会对apk产生实际作用,是为开发者设计的。

一般来说被xmlns:tools="http://schemas.android.com/tools" 声明的tools作为前缀的属性都不会被编译进去。这个跟上面代码中tools:context是一样的。

qmsggg avatar Nov 07 '17 07:11 qmsggg

内存优化之Android内存管理机制

Java对象的生命周期

  • 创建阶段(Created) 为对象分配存储空间 开始构造对象 从父类到子类对static成员进行初始化 父类成员变量按照顺序初始化,递归调用父类的构造方法 子类成员变量按照顺序初始化,子类构造方法调用 一旦对象被创建,并有某个引用指向它,这个对象的状态就切换到了应用阶段(In Use)

  • 应用阶段(In Use) 对象至少被一个强引用持有并且对象在作用域内

  • 不可见阶段(Invisible) 程序本身不再持有该对象的任何强引用,但是这些引用可能还存在着; 一般具体是指程序的执行已经超过该对象的作用域了

  • 不可达阶段(Unreachable) 该对象不再被任何强引用所持有; 可能仍被JVM等系统下的某些已经装载的惊天变灵或者线程或JNI所持有,这些特殊的强引用被称为GC root,这种情况容易导致内存泄露,无法被回收

  • 收集阶段(Collected) 对象不可达,并且GC已经准备好对该对象占用的内存空间重新分配的时候,处于手机阶段。 如果重写了finazlie()方法,则会去执行该方法。

尽量不要重写finazlie()方法,因为有可能影响JVM的对象分配与回收速度或者可能造成该对象的再次复活

  • 终结阶段 当对象执行完finalize()方法之后,仍然处于不可达状态时,则该对象进入终结阶段。在这个阶段,内存空间等待GC进行回收

  • 对象空间的重新分配 GC对该对象占有的内存空间进行回收或者再分配,该对象彻底消失

注意:

1.软引用可以加速虚拟机对垃圾内存的回收速度,更可靠地维护系统的运行安全,防止内存益出(Out Of Memory)等问题产生。 2.创建对象以后,在确定不需要使用该对象时,使对象置空,这样更符合垃圾回收标准,比如Object = null,可以提高内存使用效率,并且不要采用过深的继承层次。访问本地变量优于访问类中的变量。

内存分配

内存回收机制

GC类型

qmsggg avatar Nov 07 '17 09:11 qmsggg

内存分配工具

Memory Monitor

Heap Viewer

Allocation Tracker

qmsggg avatar Nov 09 '17 14:11 qmsggg

常见内存泄漏场景

资源性对象未关闭

资源性对象(比如Cursor,File文件等)往往都使用类一些缓冲,在不使用的时候,应该及时关闭它们,以便它们的缓存数据能够及时回收。它们的缓存不只是存在于Java虚拟机内,还存在于Java虚拟机外。如果仅仅是把它们引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如SQLite Curosr(在析构函数finalize()中,如果没有关闭它,它自己会调用close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样效率太低了。所以应该调用colse函数,将其关闭,然后设置为null。

注册对象未反注册(比如广播等)

类的静态变量持有大数据对象

非静态内部类的静态实例

Handler 临时性内存泄漏

Handler通过发送Message与主线程交互,Message发出之后存储在MessageQueue中,有些Message也不是马上就被处理到,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler非静态的,则会导致Activity或者Service不被回收。

  • 1.改成静态
  • 2.内部弱引用外部的
  • 3.外部销毁时Handler调用removeCallbacksAndMessage(null) 注:AsyncTask内部也是Handler机制,所以要注意,但是这种一般都是临时的。

容器中的对象没清理造成的内存泄漏

如果对象的集合是一个static的,情况就严重了。

WebView

qmsggg avatar Nov 09 '17 15:11 qmsggg

内存泄漏检测工具

LeakCanary

实现监控

自定义处理结果

qmsggg avatar Nov 09 '17 15:11 qmsggg

优化内存空间

对象引用

  • 强引用
  • 软引用
  • 弱引用
  • 虚引用 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 可能需要运行多次GC,才能找到并释放弱引用对象。 问题:之前遇到函数参数是一个接口的匿名实现,在函数内部采用弱引用,几乎每次弱引用取出来的值都为null,终于找到原因了,要用也该用软引用。

减少不必要的内存开销

AutoBoxing

内存复用

使用最优的数据类型

  • 1.HashMap与ArrayMap HashMap是一个散列表,想HashMap中put元素时,先根据key的HashCode重新计算hash值,根据hash值得到这个元素在数组中的位置,如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置。HashMap会配置一个大的数组来减少潜在的冲突,并且会有其他的逻辑防止链接算法和一些冲突的发生。 ArrayMap 总结:以下情况优先考虑ArrayMap:
    • 当对象的数目非常小(1000以内),但是访问特别多或者删除和插入频率不高时。
    • 当有映射容器,有映射发生,并且所有映射的容器也是ArrayMap时。?????
  • 2.枚举类型 Android系统启动应用后,会在应用单独分配一块内存,应用的dex code, Heap 以及运行时的内存都会在此分配。枚举类型的dex size是普通常量定义的13倍以上(dex code增加);同时运行时,一个enum值的声明会消耗至少20byte,这还没有算上其中的对象数组需要保持对enum值的引用。每个枚举项都会被声明成一个静态变量,并被赋值。

枚举的最大优点就是类型安全,但在Android平台上,枚举的内存开销是直接定义常量的3倍以上,所以尽量不使用枚举,目前提供了int型和String型两种注解方式:IntDef和StringDef,

public static final int UI_PERF_LEVEL_0 = 0;
public static final int UI_PERF_LEVEL_1 = 1;
@IntDef({UI_PERF_LEVEL_0, UI_PERF_LEVEL_1})
@Retention(RetentionPolicy.SOURCE)
public @interface PER_LEVEL {

}

public static int getLevel(@PER_LEVEL int level) {
...
}

使用IntDef和StringDef需要在Gradle配置中引入:compilecom.android.support:support-annotations:22.0.0

  • 3.LruCache

图片内存优化(未完成)

注:图片加载方案 #70

qmsggg avatar Nov 09 '17 16:11 qmsggg

提升动画性能

帧动画

补间动画

属性动画

硬件加速

qmsggg avatar Nov 09 '17 19:11 qmsggg

卡顿监控方案与实现

qmsggg avatar Nov 09 '17 19:11 qmsggg

存储优化

存储方式

  • SharedPreference 一个轻量级数据存储类,使用于保存软件配置参数。使用SharedPreferences保存参数,最终是使用xml格式文件存放数据,文件存放在/data/data/<pagename_name>/shared_prefs目录下。 (1)特点 用Map数据结构来存储,以键值(key-value)的方式存储。 (2) 存储路径 (3) 创建模式 Activity.MODE_APPEND:如果文件存在,在末尾增加; Activity.MODE_PRIVATE:默认模式,只能由创建该文件的应用程序调用,即为私有的; Activity.MODE_WORLD_PEADABLE:所有读取和创建 Activity.MODE_WORLD_WRITEABLE:所有写入,访问和创建的文件权限;
  • SQLite
  • File
  • ContentProvider

序列化

  • Serializable 在序列化的时候会产生大量的临时变量,在序列化的过程中会消耗更多的内存, 从而引起频繁的GC。
  • Parcelable 不能使用在要将数据存储在磁盘上的情况。Parcel本质为了更好的实现对象在IPC之间的传递,并不是一个通用的序列化机制,改变Parcel中任何数据的底层实现都可能导致之前的数据不可读取。
  • Gson
  • fastJason
  • Nano Proto Buffers
  • FlatBuffers

SharedPreferences优化

SharedPreferences实际上是对一个XML文件存储key-value键值对,每次的commit和apply操作都是一次I/O写操作。大家都知道I/O操作是最慢的操作之一,在主线程操作会导致主线程慢。SharedPreferences性能优化的主要两个方面: 1.IO性能 2.同步锁问题

A. 当SharedPreferences文件还没有被加载到内存的时候,调用getSharedPreferences方法会初始化文件并读入内存,这容易导致耗时增加。 B.Editor的commit或者apply方法每次执行时,同步写入磁盘耗时较长。

需要说明的是,editor的commit和apply方法的区别在于同步写入和异步写入,以及是否需要返回值。在不需要返回值的情况下,使用apply方法可以极大提高性能。

另一个方面就是同步锁的问题,SharedPreferences类中的commitToMemory()方法会锁定SharedPreferences对象,Put()和getEditor()方法会锁定Editor对象,在写入磁盘是更会锁定一个写入锁。 因此最好的优化方法就是避免频繁地读写SharedPreferences,减少无谓的调用,如下,为代码在同一生命周期内,SharedPreferences读一次即可。 if(开关未读 & 没有发生过变化) { int state = sp.getUserId(); } 而对于批量操作,最好先获取一个editor,进行批量操作,然后调用apply方法。

注:跨进程读写SharedPreferences需要用到ContentProvider方案支持,对所有SP操作套上了ContentProvider进行访问,耗时增加3倍左右,因此尽量避免跨进程进程操作。

数据库使用及优化

1.SQLiteStatement

SQLiteStatement只能插入具体的一个表中的数据,在插入之前记得先清除上一次的数据;

2.使用事务

3.使用索引

4.异步线程,写数据库统一管理

双缓冲机制:常用的放内存。

5.提高查询性能

查询数据量的大小

查询数据的列数(查询需要的,需要避免不需要的数据)

排序的复杂度

qmsggg avatar Nov 09 '17 20:11 qmsggg

代码静态扫描工具

1.checkstyle 2.FindBugs 3.PMD 4.Android Lintn

qmsggg avatar Nov 10 '17 10:11 qmsggg

Crash 监控

Java层Crash监控

Native层监控

Crash上报机制

下次正常后上传

qmsggg avatar Nov 10 '17 10:11 qmsggg