qmsggg / qmsggg_BlogCollect

仅仅只是用于学习和记录使用,里面包括了自己学习android的点点滴滴,希望自己在以后的时间能把之前由于时间原因没有完成的完成了,以此自勉。
57 stars 18 forks source link

Android应用性能优化最佳实践 #61

Open qmsggg opened 7 years ago

qmsggg commented 7 years ago

FPS

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

qmsggg commented 7 years ago

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

qmsggg commented 7 years ago

Android主线程的主要职责:

qmsggg commented 7 years ago

双缓冲

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

qmsggg commented 7 years ago

GPU

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

不同颜色解释:

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

qmsggg commented 7 years ago

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视图说明

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

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

参考文章

qmsggg commented 7 years ago

SysTrace使用方法

qmsggg commented 7 years ago

布局优化工具Hierarchy Viewer

qmsggg commented 7 years ago

布局层级检测 Android Lint

qmsggg commented 7 years ago

布局优化方法

使用Merge需要注意:

qmsggg commented 7 years ago

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

背景:

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

ViewStub使用

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

ViewStub使用注意

ViewStubshying场景

qmsggg commented 7 years ago

布局优化方法之布局复用

include标签

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

qmsggg commented 7 years ago

优化自我总结:

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

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

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

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

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

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

qmsggg commented 7 years ago

避免过度绘制

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

导致过度绘制的原因

qmsggg commented 7 years ago

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

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

qmsggg commented 7 years ago

自定义View过度绘制优化

Canvas.ClipRect

qmsggg commented 7 years ago

启动优化之应用启动流程

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

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);
    }
}

注意:

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

Activity

Activity生命周期金字塔模型

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

应用启动的流程

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

qmsggg commented 7 years ago

启动优化之启动耗时检测

1.adb shell am image

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 commented 7 years ago

合理的刷新机制

减少刷新次数

qmsggg commented 7 years ago

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 commented 7 years ago

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

Java对象的生命周期

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

注意:

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

内存分配

内存回收机制

GC类型

qmsggg commented 7 years ago

内存分配工具

Memory Monitor

Heap Viewer

Allocation Tracker

qmsggg commented 7 years ago

常见内存泄漏场景

资源性对象未关闭

资源性对象(比如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不被回收。

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

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

WebView

qmsggg commented 7 years ago

内存泄漏检测工具

LeakCanary

实现监控

自定义处理结果

qmsggg commented 7 years ago

优化内存空间

对象引用

减少不必要的内存开销

AutoBoxing

内存复用

使用最优的数据类型

枚举的最大优点就是类型安全,但在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

图片内存优化(未完成)

注:图片加载方案 #70

qmsggg commented 7 years ago

提升动画性能

帧动画

补间动画

属性动画

硬件加速

qmsggg commented 7 years ago

卡顿监控方案与实现

qmsggg commented 7 years ago

存储优化

存储方式

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 commented 7 years ago

代码静态扫描工具

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

qmsggg commented 7 years ago

Crash 监控

Java层Crash监控

Native层监控

Crash上报机制

下次正常后上传

qmsggg commented 7 years ago

ANR 剖析

ANR介绍

ANR(Application Not Responding) 即应用无响应; 1.KeyDispatchTimeout (5s) 2.BroadcastTimeout(10s) 3.ServiceTimeout(20s)

原因: 自生和其它appCPU占用高。

ANR分析

AS的Analyze Stacktrace分析工具

ANR监控

qmsggg commented 7 years ago

提高后台进程存活率

应用进程优先级

利用SyncAdapter提高进程优先级

1.网络连接(长连接心跳) 2.利用系统现有机制(AlarmReceiver,BootReceiver) 3.SyncAdapter 利用Android系统提供的账号同步机制实现进程优先级提高

qmsggg commented 7 years ago

耗电优化

Doze模式

qmsggg commented 7 years ago

安装包大小优化

APK包构成

减少安装包常用的方案

代码混淆

资源优化

qmsggg commented 7 years ago

Android性能优化全方面解析

qmsggg commented 6 years ago

Android性能优化:布局优化 详细解析(含讲解 )

qmsggg commented 6 years ago

Android性能优化典范 - 第6季

qmsggg commented 6 years ago

大众点评App的短视频耗电量优化实战