Android 性能优化(二)

2021-01-22 18:08:33 浏览数 (1)

上一篇 Android性能优化(一)

  • 卡顿优化
    • 卡顿
    • 帧率
    • 卡顿原因
    • 卡顿检测
      • 使用dumpsys gfxinfo
      • 使用systrace
      • 使用BlockCanary
      • 使用Choreographer
    • 优化
  • 存储优化
    • 交换数据格式
    • SharePreferences 优化
    • Bitmap 解码
    • 数据库优化
      • 事务
      • SQLiteStatement
      • 索引
    • 其它通用优化
  • 网络优化
    • 网络连接对用户的影响
    • 分析网络连接的工具
      • Network Monitor
      • 网络代理工具
    • 从哪些方面优化网络连接
      • 接口设计
      • 网络缓存
      • 弱网测试&优化
  • 耗电优化
    • 耗电监控
      • Android Vitals
    • 耗电监控都监控什么
    • 如何监控耗电
      • Java Hook
      • 插桩
  • 多线程并发优化
    • Thread 使用
      • Thread 中断
      • 同步
    • Android Threading
      • AsyncTask
      • HandlerThread
      • IntentService
      • Loader
      • ThreadPool
    • 线程优先级
  • 安装包优化
    • 常用的优化方式
      • 清理无用资源
      • 图片资源优化
      • 资源动态加载
      • lib库优化
      • 7zip压缩资源
      • 代码混淆
      • 资源(res)混淆
      • 使用微信AndResGuard
      • Facebook的redex优化字节码

卡顿优化

卡顿

在应用开发中如果留意到log的话有时候可能会发下下面的log信息:

代码语言:txt复制
I/Choreographer(1200): Skipped 60 frames!  The application may be doing too much work on its main thread.

在大部分Android平台的设备上,Android系统是16ms刷新一次,也就是一秒钟60帧。要达到这种刷新速度就要求在ui线程中处理的任务时间必须要小于16ms,如果ui线程中处理时间长,就会导致跳过帧的渲染,也就是导致界面看起来不流畅,卡顿。如果用户点击事件5s中没反应就会导致ANR。

帧率

即 Frame Rate,单位 fps,是指 gpu 生成帧的速率,60fps,Android中更帧率相关的类是SurfaceFlinger。

SurfaceFlinger

surfaceflinger作用是接受多个来源的图形显示数据,将他们合成,然后发送到显示设备。比如打开应用,常见的有三层显示,顶部的statusbar底部或者侧面的导航栏以及应用的界面,每个层是单独更新和渲染,这些界面都是有surfaceflinger合成一个刷新到硬件显示。

在这里插入图片描述在这里插入图片描述

在显示过程中使用到了bufferqueue,surfaceflinger作为consumer方,比如windowmanager管理的surface作为生产方产生页面,交由surfaceflinger进行合成。

在这里插入图片描述在这里插入图片描述

VSync

Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,VSync是Vertical Synchronization(垂直同步)的缩写,是一种在PC上很早就广泛使用的技术,可以简单的把它认为是一种定时中断。而在Android 4.1(JB)中已经开始引入VSync机制,用来同步渲染,让UI和SurfaceFlinger可以按硬件产生的VSync节奏进行工作。

安卓系统中有 2 种 VSync 信号:

1、屏幕产生的硬件 VSync: 硬件 VSync 是一个脉冲信号,起到开关或触发某种操作的作用。

2、由 SurfaceFlinger 将其转成的软件 Vsync 信号:经由 Binder 传递给 Choreographer。

除了Vsync的机制,Android还使用了多级缓冲的手段以优化UI流程度,例如双缓冲(A B),在显示buffer A的数据时,CPU/GPU就开始在buffer B中准备下一帧数据:但是不能保证每一帧CPU、GPU都运行状态良好,可能由于资源抢占等性能问题导致某一帧GPU掉链子,vsync信号到来时buffer B的数据还没准备好,而此时Display又在显示buffer A的数据,导致后面CPU/GPU没有新的buffer着手准备数据,导致卡顿(jank)。

在这里插入图片描述在这里插入图片描述

卡顿原因

从系统层面上看主要以下几个方面的原因会导致卡顿:

1. SurfaceFlinger 主线程耗时

SurfaceFlinger 负责 Surface 的合成 , 一旦 SurfaceFlinger 主线程调用超时 , 就会产生掉帧 .

SurfaceFlinger 主线程耗时会也会导致 hwc service 和 crtc 不能及时完成, 也会阻塞应用的 binder 调用, 如 dequeueBuffer queueBuffer 等.

2. 后台活动进程太多导致系统繁忙

后台进程活动太多,会导致系统非常繁忙, cpu io memory 等资源都会被占用, 这时候很容易出现卡顿问题 , 这也是系统这边经常会碰到的问题。

dumpsys cpuinfo 可以查看一段时间内 cpu 的使用情况:

在这里插入图片描述在这里插入图片描述

3.主线程调度不到 , 处于 Runnable 状态

当线程为 Runnable 状态的时候 , 调度器如果迟迟不能对齐进行调度 , 那么就会产生长时间的 Runnable 线程状态 , 导致错过 Vsync 而产生流畅性问题。

4、System 锁

system_server 的 AMS 锁和 WMS 锁 , 在系统异常的情况下 , 会变得非常严重 , 如下图所示 , 许多系统的关键任务都被阻塞 , 等待锁的释放 , 这时候如果有 App 发来的 Binder 请求带锁 , 那么也会进入等待状态 , 这时候 App 就会产生性能问题 ; 如果此时做 Window 动画 , 那么 system_server 的这些锁也会导致窗口动画卡顿

在这里插入图片描述在这里插入图片描述

5、Layer过多导致 SurfaceFlinger Layer Compute 耗时

Android P 修改了 Layer 的计算方法 , 把这部分放到了 SurfaceFlinger 主线程去执行, 如果后台 Layer 过多, 就会导致 SurfaceFlinger 在执行 rebuildLayerStacks 的时候耗时 , 导致 SurfaceFlinger 主线程执行时间过长。

在这里插入图片描述在这里插入图片描述

从应用层来看以下会导致卡顿:

1、主线程执行时间长

主线程执行 Input Animation Measure Layout Draw decodeBitmap 等操作超时都会导致卡顿 。

  • 1、Measure Layout 耗时超时
在这里插入图片描述在这里插入图片描述
  • 2、draw耗时
在这里插入图片描述在这里插入图片描述
  • 3、Animation回调耗时
在这里插入图片描述在这里插入图片描述
  • 4、View 初始化耗时
在这里插入图片描述在这里插入图片描述
  • 5、List Item 初始化耗时
在这里插入图片描述在这里插入图片描述
  • 6、主线程操作数据库

2、主线程 Binder 耗时

Activity resume 的时候, 与 AMS 通信要持有 AMS 锁, 这时候如果碰到后台比较繁忙的时候, 等锁操作就会比较耗时, 导致部分场景因为这个卡顿, 比如多任务手势操作。

在这里插入图片描述在这里插入图片描述

3、WebView 性能不足

应用里面涉及到 WebView 的时候, 如果页面比较复杂, WebView 的性能就会比较差, 从而造成卡顿

在这里插入图片描述在这里插入图片描述

4、帧率与刷新率不匹配

如果屏幕帧率和系统的 fps 不相符 , 那么有可能会导致画面不是那么顺畅. 比如使用 90 Hz 的屏幕搭配 60 fps 的动画。

在这里插入图片描述在这里插入图片描述

卡顿检测

卡顿检测可以使用以下多种方法同时进行:

1、使用dumpsys gfxinfo

2、使用Systrace获取相关信息

3、使用LayoutInspect 检测布局层次

4、使用BlockCanary

5、利用Choreographer。

6、使用严格模式(StrictMode )。

使用dumpsys gfxinfo

在开发过程中发现有卡顿发生时可以使用下面的命令来获取卡顿相关的信息:

代码语言:txt复制
adb shell dumpsys gfxinfo [PACKAGE_NAME]

输入这个命令后可能会打印下面的信息:

代码语言:txt复制
Applications Graphics Acceleration Info:
Uptime: 102809662 Realtime: 196891968
** Graphics info for pid 31148 [com.android.settings] **
Stats since: 524615985046231ns
Total frames rendered: 8325
Janky frames: 729 (8.76%)
90th percentile: 13ms
95th percentile: 20ms
99th percentile: 73ms
Number Missed Vsync: 294
Number High input latency: 47
Number Slow UI thread: 502
Number Slow bitmap uploads: 44
Number Slow issue draw commands: 135

上面参数说明:

Graphics info for pid 31148 com.android.settings: 表明当前dump的为设置界面的帧信息,pid为31148

Total frames rendered: 8325 本次dump搜集了8325帧的信息

Janky frames :729 (8.76%)出现卡顿的帧数有729帧,占8.76%

Number Missed Vsync: 294 垂直同步失败的帧

Number Slow UI thread: 502 因UI线程上的工作导致超时的帧数

Number Slow bitmap uploads: 44 因bitmap的加载耗时的帧数

Number Slow issue draw commands: 135 因绘制导致耗时的帧数

使用systrace

上面使用的dumpsys是能发现问题或者判断问题的严重性,但无法定位真正的原因。如果要定位原因,应当配合systrace工具使用。

systrace使用

Systrace可以帮助分析应用是如何设备上运行起来的,它将系统和应用程序线程集中在一个共同的时间轴上,分析systrace的第一步需要在程序运行的时间段中抓取trace log,在抓取到的trace文件中,包含了这段时间中想要的关键信息,交互情况。

在这里插入图片描述在这里插入图片描述

图1显示的是当一个app在滑动时出现了卡顿的现象,默认的界面下,横轴是时间,纵向为trace event,trace event 先按进程分组,然后再按线程分组.从上到下的信息分别为Kernel,SurfaceFlinger,应用包名。通过配置trace的分类,可以根据配置情况记录每个应用程序的所有线程信息以及trace event的层次结构信息。

Android studio中使用systrace

1、在android设备的 设置 – 开发者选项 – 监控 – 开启traces。

2、选择要追中的类别,并且点击确定。

完成以上配置后,开始抓trace文件

代码语言:txt复制
$ python systrace.py --cpu-freq --cpu-load --time=10 -o mytracefile.html

分析trace文件

抓到trace.html文件后,通过web浏览器打开

检查Frames

每个应用程序都有一排代表渲染帧的圆圈,通常为绿色,如果绘制的时间超过16.6毫秒则显示黄色或红色。通过“W”键查看帧。

在这里插入图片描述在这里插入图片描述

trace应用程序代码

在framework中的trace marker并没有覆盖到所有代码,因此有些时候需要自己去定义trace marker。在Android4.3之后,可以通过Trace类在代码中添加标记,这样将能够看到在指定时间内应用的线程在做哪些工作,当然,trace 的begin和end操作也会增加一些额外的开销,但都只有几微秒左右。

通过下面的例子来说明Trace类的 用法。

代码语言:txt复制
public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {

    ...

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Trace.beginSection("MyAdapter.onCreateViewHolder");
        MyViewHolder myViewHolder;
        try {
            myViewHolder = MyViewHolder.newInstance(parent);
        } finally {
            Trace.endSection();
        }
        return myViewHolder;
    }

   @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        Trace.beginSection("MyAdapter.onBindViewHolder");
        try {
            try {
                Trace.beginSection("MyAdapter.queryDatabase");
                RowItem rowItem = queryDatabase(position);
                mDataset.add(rowItem);
            } finally {
                Trace.endSection();
            }
            holder.bind(mDataset.get(position));
        } finally {
            Trace.endSection();
        }
    }

…

}

使用BlockCanary

BlockCanary是国内开发者MarkZhai开发的一套性能监控组件,它对主线程操作进行了完全透明的监控,并能输出有效的信息,帮助开发分析、定位到问题所在,迅速优化应用。

其特点有:

1、非侵入式,简单的两行就打开监控,不需要到处打点,破坏代码优雅性。

2、精准,输出的信息可以帮助定位到问题所在(精确到行),不需要像Logcat一样,慢慢去找。

3、目前包括了核心监控输出文件,以及UI显示卡顿信息功能

BlockCanary基本原理

android应用程序只有一个主线程ActivityThread,这个主线程会创建一个Looper(Looper.prepare),而Looper又会关联一个MessageQueue,主线程Looper会在应用的生命周期内不断轮询(Looper.loop),从MessageQueue取出Message 更新UI。

代码语言:txt复制
public static void loop() {
    ...
    for (;;) {
        ...
        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to "   msg.target   " "  
                    msg.callback   ": "   msg.what);
        }
        msg.target.dispatchMessage(msg);
        if (logging != null) {
            logging.println("<<<<< Finished to "   msg.target   " "   msg.callback);
        }
        ...
    }
}

BlockCanary主要是检测msg.target.dispatchMessage(msg);之前的>>>>> Dispatching to 和之后的<<<<< Finished to的间隔时间。

应用发生卡顿,一定是在dispatchMessage中执行了耗时操作。通过给主线程的Looper设置一个Printer,打点统计dispatchMessage方法执行的时间,如果超出阀值,表示发生卡顿,则dump出各种信息,提供开发者分析性能瓶颈。

使用Choreographer

Android 主线程运行的本质,其实就是 Message 的处理过程,我们的各种操作,包括每一帧的渲染操作 ,都是通过 Message 的形式发给主线程的 MessageQueue ,MessageQueue 处理完消息继续等下一个消息。

在这里插入图片描述在这里插入图片描述

Choreographer 的引入,主要是配合 Vsync ,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机. 目前大部分手机都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每个 16.6 ms , Vsync 信号唤醒 Choreographer 来做 App 的绘制操作 ,这就是引入 Choreographer 的主要作用。

Choreographer 两个主要作用

1、承上:负责接收和处理 App 的各种更新消息和回调,等到 Vsync 到来的时候统一处理。比如集中处理 Input(主要是 Input 事件的处理) 、Animation(动画相关)、Traversal(包括 measure、layout、draw 等操作) ,判断卡顿掉帧情况,记录 CallBack 耗时等。

2、启下:负责请求和接收 Vsync 信号。接收 Vsync 事件回调(通过 FrameDisplayEventReceiver.onVsync );请求 Vsync(FrameDisplayEventReceiver.scheduleVsync) .

使用Choreographer 计算帧率

Choreographer 处理绘制的逻辑核心在 Choreographer.doFrame 函数中,从下图可以看到,FrameDisplayEventReceiver.onVsync post 了自己,其 run 方法直接调用了 doFrame 开始一帧的逻辑处理:

在这里插入图片描述在这里插入图片描述

Choreographer周期性的在UI重绘时候触发,在代码中记录上一次和下一次绘制的时间间隔,如果超过16ms,就意味着一次UI线程重绘的“丢帧”。丢帧的数量为间隔时间除以16,如果超过3,就开始有卡顿的感知。

使用Choreographer检测帧的代码如下:

代码语言:txt复制
public class MyFrameCallback implements Choreographer.FrameCallback {
        private String TAG = "性能检测";
        private long lastTime = 0;

        @Override
        public void doFrame(long frameTimeNanos) {
            if (lastTime == 0) {
                //代码第一次初始化。不做检测统计。
                lastTime = frameTimeNanos;
            } else {
                long times = (frameTimeNanos - lastTime) / 1000000;
                int frames = (int) (times / 16);

                if (times > 16) {
                    Log.w(TAG, "UI线程超时(超过16ms):"   times   "ms"   " , 丢帧:"   frames);
                }

                lastTime = frameTimeNanos;
            }

            Choreographer.getInstance().postFrameCallback(mFrameCallback);
        }
    }

优化

由上面的分析可知对象分配垃圾回收(GC)、线程调度以及Binder调用 是Android系统中常见的卡顿原因,因此卡顿优化主要以下几种方法,更多的要结合具体的应用来进行:

1、布局优化

  • 通过减少冗余或者嵌套布局来降低视图层次结构。比如使用约束布局代替线性布局和相对布局。
  • 用 ViewStub 替代在启动过程中不需要显示的 UI 控件。
  • 使用自定义 View 替代复杂的 View 叠加。

2、减少主线程耗时操作

  • 主线程中不要直接操作数据库,数据库的操作应该放在数据库线程中完成。
  • sharepreference尽量使用apply,少使用commit,可以使用MMKV框架来代替sharepreference。
  • 网络请求回来的数据解析尽量放在子线程中,不要在主线程中进行复制的数据解析操作。
  • 不要在activity的onResume和onCreate中进行耗时操作,比如大量的计算等。

3、减少过度绘制

过度绘制是同一个像素点上被多次绘制,减少过度绘制一般减少布局背景叠加等方式,如下图所示右边是过度绘制的图片。

在这里插入图片描述在这里插入图片描述

4、列表优化

  • RecyclerView使用优化,使用DiffUtil和notifyItemDataSetChanged进行局部更新等。

5、对象分配和回收优化

自从Android引入 ART 并且在Android 5.0上成为默认的运行时之后,对象分配和垃圾回收(GC)造成的卡顿已经显著降低了,但是由于对象分配和GC有额外的开销,它依然又可能使线程负载过重。 在一个调用不频繁的地方(比如按钮点击)分配对象是没有问题的,但如果在在一个被频繁调用的紧密的循环里,就需要避免对象分配来降低GC的压力。

  • 减少小对象的频繁分配和回收操作。

存储优化

交换数据格式

Google 推出的 Protocal Buffers 是一种更轻便高效的存储结构,但消耗内存较大。

FlatBuffers 同样由 Google 推出,专注性能,适合移动端。占用存储比 Protocal 要大。

SharePreferences 优化

  • 当 SharedPreferences 文件还没有被加载到内存时,调用 getSharedPreferences 方法会初始化文件并读入内存,这容易导致 耗时更长。
  • Editor 的 commit 或者 apply 方法的区别在于同步写入和异步 写入,以及是否需要返回值。在不需要返回值的情况下,使用 apply 方法可以极大提高性能
  • SharedPreferences 类 中的 commitToMemory() 会锁定 SharedPreference 对象,put() 和 getEditor() 方法会锁定 Editor 对象,在写入磁盘时更会锁定一个写入锁。因此,最好的优化方法就是避免频繁地读写 SharedPreferences,减少无谓的调用。对于 SharedPreferences 的批量操作,最好先获取一个 editor 进行批量操作,然后调用 apply 方法。

Bitmap 解码

  • 4.4 以上 decodeFile 内部没有使用缓存,效率不高。要使用 decodeStream,同时传入的文件流为 BufferedInputStream。
  • decodeResource 同样存在性能问题,用 decodeResourceStream。

数据库优化

  1. 使用 StringBuilder 代替 String
  2. 查询时返回更少的结果集及更少的字段

查询时只取需要的字段和结果集,更多的结果集会消耗更多的时间及内存,更多的字段会导致更多的内存消耗。

  1. 少用 cursor.getColumnIndex

根据性能调优过程中的观察 cursor.getColumnIndex 的时间消耗跟 cursor.getInt 相差无几。可以在建表的时候用 static 变量记住某列的 index,直接调用相应 index 而不是每次查询。

  1. 异步线程

Android 中数据不多时表查询可能耗时不多,不会导致 ANR,不过大于 100ms 时同样会让用户感觉到延时和卡顿,可以放在线程中运行,但 sqlite 在并发方面存在局限,多线程控制较麻烦,这时候可使用单线程池,在任务中执行 db 操作,通过 handler 返回结果和 UI 线程交互,既不会影响 UI 线程,同时也能防止并发带来的异常。

  1. SQLiteOpenHelper 维持一个单例

因为 SQLite 对多线程的支持并不是很完善,如果两个线程同时操作数据库,因为数据库被另一个线程占用, 这种情况下会报“Database is locked” 的异常。所以在数据库管理类中使用单例模式,就可以保证无论在哪个线程中获取数据库对象,都是同一个。

最好的方法是所有的数据库操作统一到同一个线程队列管理,而业务层使用缓存同步,这样可以完全避免多线程操作数据库导致的不同步和死锁问题。

  1. Application 中初始化
  • 使用 Application 的 Context 创建数据库,在 Application 生命周期结束时再关闭。
  • 在应用启动过程中最先初始化完数据库,避免进入应用后再初始化导致相关操作时间变长。
  1. 少用 AUTOINCREMENT

主键加上 AUTOINCREMENT 后,可以保证主键严格递增,但并不能保证每次都加 1,因为在插入失败后,失败的行号不会被复用,会造成主键有间隔,继而使 INSERT 耗时 1 倍以上。

这个 AUTOINCREMENT 关键词会增加 CPU,内存,磁盘空间和磁盘 I/O 的负担,所以 尽量不要用,除非必需。通常情况下都不是必需的。

事务

使用事务的两大好处是原子提交和更优性能:

  • 原子提交:意味着同一事务内的所有修改要么都完成要么都不做,如果某个修改失败,会自动回滚使得所有修改不生效。
  • 更优性能:Sqlite 默认会为每个插入、更新操作创建一个事务,并且在每次插入、更新后立即提交。这样如果连续插入 100 次数据实际是创建事务、执行语句、提交这个过程被重复执行了 100 次。如果显式的创建事务,这个过程只做一次,通过这种一次性事务可以使得性能大幅提升。尤其当数据库位于 sd 卡时,时间上能节省两个数量级左右。

主要三个方法:beginTransaction,setTransactionSuccessful,endTransaction。

SQLiteStatement

使用 Android 系统提供的 SQLiteStatement 来插入数据,在性能上有一定的提高,并且也解决了 SQL 注入的问题。

代码语言:txt复制
SQLiteStatement statement = dbOpenHelper.getWritableDatabase().compileStatement("INSERT INTO EMPERORS(name, dynasty, start_year) values(?,?,?)"); 
statement.clearBindings();
statement.bindString(1, "Max"); 
statement.bindString(2, "Luk"); 
statement.bindString(3, "1998"); 
statement.executeInsert();

SQLiteStatement 只能插入一个表中的数据,在插入前要清除上一次的数据。

索引

索引就像书本的目录,目录可以快速找到所在页数,数据库中索引可以帮助快速找到数据,而不用全表扫描,合适的索引可以大大提高数据库查询的效率。

优点:大大加快了数据库检索的速度,包括对单表查询、连表查询、分组查询、排序查询。经常是一到两个数量级的性能提升,且随着数据数量级增长。

缺点:

  • 索引的创建和维护存在消耗,索引会占用物理空间,且随着数据量的增加而增加。
  • 在对数据库进行增删改时需要维护索引,所以会对增删改的性能存在影响。

分类

  1. 直接创建索引和间接创建索引
    • 直接创建: 使用 sql 语句创建,Android 中可以在 SQLiteOpenHelper 的 onCreate 或是 onUpgrade 中直接 excuSql 创建语句,如 CREATE INDEX mycolumn_index ON mytable (myclumn)
    • 间接创建: 定义主键约束或者唯一性键约束,可以间接创建索引,主键默认为唯一索引。
  2. 普通索引和唯一性索引
    • 普通索引:CREATEINDEXmycolumn_indexONmytable(myclumn)
    • 唯一性索引:保证在索引列中的全部数据是唯一的,对聚簇索引和非聚簇索引都可以使用,语句为 CREATE UNIQUE COUSTERED INDEX myclumn_cindex ON mytable(mycolumn)
  3. 单个索引和复合索引
    • 单个索引:索引建立语句中仅包含单个字段,如上面的普通索引和唯一性索引创建示例。
    • 复合索引:又叫组合索引,在索引建立语句中同时包含多个字段,如 CREATEINDEXname_indexONusername(firstname,lastname),其中 firstname 为前导列。
  4. 聚簇索引和非聚簇索引 (聚集索引,群集索引)
    • 聚簇索引:物理索引,与基表的物理顺序相同,数据值的顺序总是按照顺序排列,如 CREATE CLUSTERED INDEX mycolumn_cindex ON mytable(mycolumn) WITH ALLOW_DUP_ROW,其中 WITH ALLOW_DUP_ROW 表示允许有重复记录的聚簇索引
    • 非聚簇索引:CREATEUNCLUSTEREDINDEXmycolumn_cindexONmytable(mycolumn),索引默认为非聚簇索引

使用场景

  1. 当某字段数据更新频率较低,查询频率较高,经常有范围查询 (>, <, =,>=, <=)order bygroup by 发生时建议使用索引。并且选择度(一个字段中唯一值的数量 / 总的数量)越大,建索引越有优势
  2. 经常同时存取多列,且每列都含有重复值可考虑建立复合索引

使用规则

  1. 对于复合索引,把使用最频繁的列做为前导列 (索引中第一个字段)。如果查询时前导列不在查询条件中则该复合索引不会被使用。如 create unique index PK_GRADE_CLASS on student (grade, class)select * from student where class = 2 未使用到索引,select * from dept where grade = 3 使用到了索引
  2. 避免对索引列进行计算,对 where 子句列的任何计算如果不能被编译优化,都会导致查询时索引失效 select * from student where tochar(grade)=’2
  3. 比较值避免使用 NULL
  4. 多表查询时要注意是选择合适的表做为内表。连接条件要充份考虑带有索引的表、行数多的表,内外表的选择可由公式:外层表中的匹配行数 * 内层表中每一次查找的次数确定,乘积最小为最佳方案。实际多表操作在被实际执行前,查询优化器会根据连接条件,列出几组可能的连接方案并从中找出系统开销最小的最佳方案
  5. 查询列与索引列次序一致
  6. 用多表连接代替 EXISTS 子句
  7. 把过滤记录数最多的条件放在最前面
  8. 善于使用存储过程,它使 sql 变得更加灵活和高效 (Sqlite 不支持存储过程)

其它通用优化

  • 经常用的数据读取后缓存起来,以免多次重复读写造成“写入放大”
  • 子线程读写数据
  • ObjectOutputStream 在序列化磁盘时,会把内存中的每个对象保存到磁盘,在保存对象的 时候,每个数据成员会带来一次 I/O 操作。在 ObjectOutputStream 上面再封装一个输出流 ByteArrayOutputStream 或 BufferedOutputStream,先将对象序列化后的信息写到缓存区中,然后再一次性地写到磁盘上;相应的,用 ByteArrayInputStream 或 BufferedInputStream 替代 ObjectInputStream。
  • 合理选择缓冲区 Buffer 的大小。太小导致 I/O 操作次数增多,太大导致申请时间变长。比如 4-8 KB。

网络优化

互联网时代, App作为于用户交互的端, 可以说实际上是一个界面, 产品的业务, 服务都是由Server提供的. 而App与Server的交互依赖于网络, 故而网络优化, 也是我们的App优化中不可缺少的一个优化项。除了客户端, 接口的优化外, 很多一部分优化还依赖于服务器端, 包括服务器端的代码开发, 部署方式等。

网络连接对用户的影响

App的网络连接对于用户来说, 影响很多, 且多数情况下都很直观, 直接影响用户对这个App的使用体验. 其中较为重要的几点:

  • 流量 App的流量消耗对用户来说是比较敏感的, 毕竟流量是花钱的嘛. 现在大部分人的手机上都有安装流量监控的工具App, 用来监控App的流量使用. 如果我们的App这方面没有控制好, 会给用户不好的使用体验.
  • 电量 电量相对于用户来说, 没有那么明显. 一般用户可能不会太注意. 但是如电量优化中的那样, 网络连接(radio)是对电量影响很大的一个因素. 所以我们也要加以注意.
  • 用户等待 也就是用户体验, 良好的用户体验, 才是我们留住用户的第一步. 如果App请求等待时间长, 会给用户网络卡, 应用反应慢的感觉, 如果有对比, 有替代品, 我们的App很可能就会被用户无情抛弃.

分析网络连接的工具

Network Monitor

Android Studio内置的Monitor工具中就有一个Network Monitor:

imgimg

其中:

  • Rx --- R(ecive) 表示下行流量, 即下载接收.
  • Tx --- T(ransmit) 表示上行流量, 即上传发送.

怎么使用Network Monitor?

Network monitor实时跟踪选定应用的数据请求情况. 我们可以连上手机, 选定调试应用进程, 然后在App上操作我们需要分析的页面请求.

例如, 上图就是以CoderPub为例, 针对从repo列表界面进入repo详情界面的监控数据.

可以看到从10s到30s之间, 20s时间内发生了多次数据请求, 且22s到27s之间的请求数据量还很大.

分析代码可以看到, 在请求repo详情的时候是打包了很多请求的:

代码语言:txt复制
@Override
public Observable<RepoDetail> getRepoDetail(String owner, String name) {
    return Observable.zip(mRepoService.get(owner, name),
            mRepoService.contributors(owner, name),
            mRepoService.listForks(owner, name, "newest"),
            mRepoService.readme(owner, name),
            isStarred(owner, name),
            new Func5<Repo, ArrayList<User>, ArrayList<Repo>, Content, Boolean, RepoDetail>() {
                @Override
                public RepoDetail call(Repo repo, ArrayList<User> users, ArrayList<Repo> forks, Content readme, Boolean isStarred) {
                    RepoDetail detail = new RepoDetail();
 
                    repo.setStarred(isStarred);
                    detail.setBaseRepo(repo);
                    detail.setForks(forks);
 
                    // because the readme content is encode with Base64 by github.
                    readme.content = StringUtil.base64Decode(readme.content);
                    detail.setReadme(readme);
 
                    detail.setContributors(users);
                    return detail;
                }
            });
}

这也验证了14s到20s间的四次数据请求, 另外由于repo详情界面会显示作者以及贡献者的图片, 而图片的数据量相对大, 故而23s到27s间有多次数据量很大的请求发生.

网络代理工具

一般来说, 网络代理工具有两个作用:

  1. 截获网络请求响应包, 分析网络请求
  2. 设置代理网络, 移动App开发中一般用来做不同网络环境的测试, 例如Wifi/4G/3G/弱网等.

代理工具很多, 诸如Wireshark, Fiddler, Charles等, 在此不一一细说了, 使用方法自行问谷歌度娘. :)

从哪些方面优化网络连接

简单来说, 两个方面:

  • 减少Radio活跃时间
    • 也就是减少网络数据获取的频次.
    • 这就减少了radio的电量消耗, 控制电量使用.
  • 减少获取数据包的大小
    • 可以减少流量消耗
    • 也可以让每次请求更快, 在网络情况不好的情况下也有良好表现, 提升用户体验.

接口设计

API设计

App与Server之间的API设计要考虑网络请求的频次, 资源的状态等. 以便App可以以较少的请求来完成业务需求和界面的展示.

例如, 注册登录. 正常会有两个API, 注册和登录, 但是设计API时我们应该给注册接口包含一个隐式的登录. 来避免App在注册后还得请求一次登录接口(有可能失败, 从而导致业务流程失败).

例如, 之前提到的获取repo详情, 实际上请求了4个接口, 请求了repo的信息, forks列表, contributors列表, readme, 这是因为github提供的接口是尽量单一职责的. 然而在我们的实际开发中, 我们的Server除了提供这些单一职责的小接口外, 最好还能组合一个满足客户端业务需求的repo详情接口出来.

Gzip压缩

使用Gzip来压缩request和response, 减少传输数据量, 从而减少流量消耗.

考虑使用Protocol Buffer代替JSON

以前我们传输数据使用XML, 后来使用JSON代替了XML, 很大程度上也是为了可读性和减少数据量(当然还有映射成POJO的方便程度).

Protocol Buffer是Google推出的一种数据交换格式.

如果我们的接口每次传输的数据量很大的话, 可以考虑下protobuf, 会比JSON数据量小很多.

当然相比来说, JSON也有其优势, 可读性更高.

本节以网络流量优化的角度推荐protobuf作为一个选择, 具体还需更具实际情况考虑.

图片的Size

上面Network Monitor中看到的22s到27s之间的有多次请求, 且数据量还很大. 就是在获取图片资源.

图片相对于接口请求来说, 数据量要大得多. 故而也是我们需要优化的一个点.

我们可以在获取图片时告知服务器需要的图片的宽高, 以便服务器给出合适的图片, 避免浪费.

我们现在很多公司的图片资源都是使用第三方的云存储服务的(七牛, 阿里云存储之类的).

以七牛为例, 可以在请求图片的url中添加诸如质量, 格式, width, height等path来获取合适的图片资源:

代码语言:txt复制
imageView2/<mode>/w/<LongEdge>
                 /h/<ShortEdge>
                 /format/<Format>
                 /interlace/<Interlace>
                 /q/<Quality>
                 /ignore-error/<ignoreError>

网络缓存

适当的缓存, 既可以让我们的应用看起来更快, 也能避免一些不必要的流量消耗.

打包网络请求

当接口设计不能满足我们的业务需求时. 例如可能一个界面需要请求多个接口, 或是网络良好, 处于Wifi状态下时我们想获取更多的数据等.

这时就可以打包一些网络请求, 例如请求列表的同时, 获取Header点击率较高的的item项的详情数据.

可以通过一些统计数据来帮助我们定位用户接下来的操作是高概率的, 提前获取这部分的数据.

监听相关状态

通过监听设备的状态:

  • 休眠状态
  • 充电状态
  • 网络状态

结合JobScheduler来根据实际情况做网络请求. 比方说Splash闪屏广告图片, 我们可以在连接到Wifi时下载缓存到本地; 新闻类的App可以在充电, Wifi状态下做离线缓存.

弱网测试&优化

除了正常的网络优化, 我们还需考虑到弱网情况下, App的表现.

弱网测试

有几种方式来模拟弱网进行测试.

Android Emulator

创建和启动Android模拟器可以设置网络速度和延迟:

创建时:

imgimg

启动时, 使用emulator命令:

代码语言:txt复制
$emulator -netdelay gprs -netspeed gsm -avd Nexus_5_API_22

具体参数参考这里和这里, 需要翻墙.

使用网络代理工具

以Charles为例:

保持手机和PC处于同一个局域网, 在手机端wifi设置高级设置中设置代理方式为手动, 代理ip填写PC端ip地址, 端口号默认8888.

imgimg
imgimg

其他模拟弱网方式

如果你恰好也是iOS的开发者, Apple提供了Network Link Conditioner, 非常好用.

可以模拟的网络情况与上述类似:

代码语言:txt复制
                        ![img](https://img-blog.csdn.net/20180910062859260?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2F1Z2Z1bg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

如果你使用Linux环境开发, 还可以试下facebook出的ATC.

弱网优化

利用上述工具模拟弱网, 在弱网情况下体验我们的App. 一般来说, 网络延迟在60ms内, 是OK的, 超过200ms就比较糟糕了. 我们需要做的是在比较糟糕的网络环境下还能给用户较好的体验.

弱网优化, 本质上是在弱网的情况下能让用户流畅的使用我们的App. 我们要做的就是结合上述的优化项:

  • 压缩/减少数据传输量
  • 利用缓存减少网络传输
  • 针对弱网(移动网络), 不自动加载图片
  • 界面先反馈, 请求延迟提交 例如, 用户点赞操作, 可以直接给出界面的点赞成功的反馈, 使用JobScheduler在网络情况较好的时候打包请求.

耗电优化

实践中,如果我们的应用需要播放视频、需要获取 GPS 信息、需要拍照,这些耗电看起来是无法避免的。

如何判断哪些耗电是可以避免,或者是需要去优化的呢?可以看下面这张图,当用户去看耗电排行榜的时候,发现“王者荣耀”使用了 7 个多小时,这时用户对“王者荣耀”的耗电是有预期的。

imageimage

假设这个时候发现某个应用他根本没怎么使用(前台时间很少),但是耗电却非常多。这种情况会跟用户的预期差别很大,他可能就会想去投诉。

所以耗电优化的第一个方向是优化应用的后台耗电。

知道了系统是如何计算耗电的,那反过来看,我们也就可以知道应用在后台不应该做什么,例如长时间获取 WakeLock、WiFi 和蓝牙的扫描等。为什么说耗电优化第一个方向就是优化应用后台耗电,因为大部分厂商预装项目要求最严格的正是应用后台待机耗电。

imageimage

当然前台耗电我们不会完全不管,但是标准会放松很多。再来看看下面这张图,如果系统对你的应用弹出这个对话框,可能对于微信来说,用户还可以忍受,但是对其他大多数的应用来说,可能很多用户就直接把你加入到后台限制的名单中了

imageimage

耗电优化的第二个方向是符合系统的规则,让系统认为你耗电是正常的。

而 Android P 是通过 Android Vitals 监控后台耗电,所以我们需要符合 Android Vitals 的规则,目前它的具体规则如下:

imageimage

虽然上面的标准可能随时会改变,但是可以看到,Android 系统目前比较关心后台 Alarm 唤醒、后台网络、后台 WiFi 扫描以及部分长时间 WakeLock 阻止系统后台休眠。

耗电监控

对于耗电监控也是如此,我们首先需要抽象出具体的规则,然后收集尽量多的辅助信息,帮助问题的排查。

Android Vitals

Android Vitals 的几个关于电量的监控方案与规则:

  • Alarm Manager wakeup 唤醒过多
  • 频繁使用局部唤醒锁
  • 后台网络使用量过高
  • 后台 WiFi scans 过多

在使用了一段时间之后,我发现它并不是那么好用。以 Alarm wakeup 为例,Vitals 以每小时超过 10 次作为规则。由于这个规则无法做修改,很多时候我们可能希望针对不同的系统版本做更加细致的区分。

其次跟 Battery Historian 一样,我们只能拿到 wakeup 的标记的组件,拿不到申请的堆栈,也拿不到当时手机是否在充电、剩余电量等信息。

imageimage

对于网络、WiFi scans 以及 WakeLock 也是如此。虽然 Vitals 帮助我们缩小了排查的范围,但是依然需要在茫茫的代码中寻找对应的可疑代码。

耗电监控都监控什么

Android Vitals并不是那么好用,而且对于国内的应用来说其实也根本无法使用。不管怎样,我们还是需要搭建自己的耗电监控系统。

那我们的耗电监控系统应该监控哪些内容,怎么样才能比 Android Vitals 做得更好呢?

  • 监控信息。简单来说系统关心什么,我们就监控什么,而且应该以后台耗电监控为主。类似 Alarm wakeup、WakeLock、WiFi scans、Network 都是必须的,其他的可以根据应用的实际情况。如果是地图应用,后台获取 GPS 是被允许的;如果是计步器应用,后台获取 Sensor 也没有太大问题。
  • 现场信息。监控系统希望可以获得完整的堆栈信息,比如哪一行代码发起了 WiFi scans、哪一行代码申请了 WakeLock 等。还有当时手机是否在充电、手机的电量水平、应用前台和后台时间、CPU 状态等一些信息也可以帮助我们排查某些问题。
  • 提炼规则。最后我们需要将监控的内容抽象成规则,当然不同应用监控的事项或者参数都不太一样。 由于每个应用的具体情况都不太一样,下面是一些可以用来参考的简单规则。
imageimage

在安卓绿色联盟的会议中,华为公开过他们后台资源使用的“红线”,你也可以参考里面的一些规则:

imageimage

如何监控耗电

明确了我们需要监控什么以及具体的规则之后,就可以来到实现这个环节了。跟 I/O 监控、网络监控一样,我首先想到的还是 Hook 方案。

Java Hook

Hook 方案的好处在于使用者接入非常简单,不需要去修改自己的代码。下面我以几个比较常用的规则为例,看看如果使用 Java Hook 达到监控的目的。

  • WakeLock。WakeLock 用来阻止 CPU、屏幕甚至是键盘的休眠。类似 Alarm、JobService 也会申请 WakeLock 来完成后台 CPU 操作。WakeLock 的核心控制代码都在PowerManagerService中,实现的方法非常简单。
代码语言:txt复制
// 代理 PowerManagerService
ProxyHook().proxyHook(context.getSystemService(Context.POWER_SERVICE), "mService", this);

@Override
public void beforeInvoke(Method method, Object[] args) {
    // 申请 Wakelock
    if (method.getName().equals("acquireWakeLock")) {
        if (isAppBackground()) {
            // 应用后台逻辑,获取应用堆栈等等     
         } else {
            // 应用前台逻辑,获取应用堆栈等等
         }
    // 释放 Wakelock
    } else if (method.getName().equals("releaseWakeLock")) {
       // 释放的逻辑    
    }
}
  • Alarm。Alarm 用来做一些定时的重复任务,它一共有四个类型,其中ELAPSED_REALTIME_WAKEUP和RTC_WAKEUP类型都会唤醒设备。同样,Alarm 的核心控制逻辑都在AlarmManagerService中,实现如下:
代码语言:txt复制
// 代理 AlarmManagerService
new ProxyHook().proxyHook(context.getSystemService
(Context.ALARM_SERVICE), "mService", this);

public void beforeInvoke(Method method, Object[] args) {
    // 设置 Alarm
    if (method.getName().equals("set")) {
        // 不同版本参数类型的适配,获取应用堆栈等等
    // 清除 Alarm
    } else if (method.getName().equals("remove")) {
        // 清除的逻辑
    }
}
  • 其他。对于后台 CPU,我们可以使用卡顿监控相关的方法。对于后台网络,同样我们可以通过网络监控相关的方法。对于 GPS 监控,我们可以通过 Hook 代理LOCATION_SERVICE。对于 Sensor,我们通过 Hook SENSOR_SERVICE中的“mSensorListeners”,可以拿到部分信息。
  • 通过 Hook,我们可以在申请资源的时候将堆栈信息保存起来。当我们触发某个规则上报问题的时候,可以将收集到的堆栈信息、电池是否充电、CPU 信息、应用前后台时间等辅助信息也一起带上。

插桩

虽然使用 Hook 非常简单,但是某些规则可能不太容易找到合适的 Hook 点。而且在 Android P 之后,很多的 Hook 点都不支持了。

出于兼容性考虑,我首先想到的是写一个基础类,然后在统一的调用接口中增加监控逻辑。以 WakeLock 为例:

代码语言:txt复制
public class WakelockMetrics {
    // Wakelock 申请
    public void acquire(PowerManager.WakeLock wakelock) {
        wakeLock.acquire();
        // 在这里增加 Wakelock 申请监控逻辑
    }
    // Wakelock 释放
    public void release(PowerManager.WakeLock wakelock, int flags) {
        wakelock.release();
        // 在这里增加 Wakelock 释放监控逻辑
    }
}

Facebook 也有一个耗电监控的开源库Battery-Metrics,它监控的数据非常全,包括 Alarm、WakeLock、Camera、CPU、Network 等,而且也有收集电量充电状态、电量水平等信息。

Battery-Metrics 只是提供了一系列的基础类,在实际使用中,接入者可能需要修改大量的源码。但对于一些第三方 SDK 或者后续增加的代码,我们可能就不太能保证可以监控到了。这些场景也就无法监控了,所以 Facebook 内部是使用插桩来动态替换。

遗憾的是,Facebook 并没有开源它们内部的插桩具体实现方案。不过这实现起来其实并不困难,事实上在 Sample 中,已经使用过 ASM、Aspectj 这两种插桩方案了。

插桩方案使用起来兼容性非常好,并且使用者也没有太大的接入成本。但是它并不是完美无缺的,对于系统的代码插桩方案是无法替换的,例如 JobService 申请 PARTIAL_WAKE_LOCK 的场景。

多线程并发优化

在程序开发的实践当中,为了让程序表现得更加流畅,我们肯定会需要使用到多线程来提升程序的并发执行性能。但是编写多线程并发的代码一直以来都是一个相对棘手的问题,所以想要获得更佳的程序性能,非常有必要掌握多线程并发编程的基础技能。

Thread 使用

Thread使用需要注意的点:

Thread 中断

常用的有两种方式:

(1).通过抛出InterruptedException来中断线程

代码语言:txt复制
    public  static  class  MyThread extends Thread{
        private  int count=0;
        @Override
        public void run() {
            super.run();
            try{
                while(true){
                        count  ;
                        System.out.println("count value:" count);
                        if (this.interrupted() || this.isInterrupted()){
                            System.out.println("check interrupted show!");
                            throw new InterruptedException();
                        }
                }
            }catch ( InterruptedException e) {
                System.out.println("thread is stop!");
                e.printStackTrace();
            }
        }
        
    } 

(2).通过变量来中断(常用)

代码语言:txt复制
    public  static  class  CustomThread extends Thread{
        private  int count=0;
        private boolean isCancel = false;
        @Override
        public void run() {
            super.run();
            while(!isCancel){
                    count  ;
                    System.out.println("count value:" count);
            }
        }
        
        public synchronized void cancel(){
            isCancel = true;
        }
    } 

同步

分变量同步和代码块同步两个方面来讲解

(1).变量同步

使用volatile关键字

代码语言:txt复制
    /**
     * 主内存和线程内存缓存进行同步
     */
    volatile int val = 5;
    public int getVal() {
        return val;
    }
    public void setVal(int val) {
        this.val = val;
    }

使用synchronized关键字

代码语言:txt复制
    int val2 = 5;
    /**
     * 使用一个motinor来监听(实现资源由一个线程进行操作)
     * 主内存和线程内存缓存进行同步
     * @return
     */
    public synchronized int getVal2() {
        return val2;
    }
    public synchronized int setVal2(int val) {
        this.val2 = val;
    }

使用关键字AtomicXXXXX

代码语言:txt复制
    AtomicInteger mAtomicValue = new  AtomicInteger(0);
    public void setAtomicValue(int value){
        mAtomicValue.getAndSet(value);
    }
    public int getAtomicValue(){
        return mAtomicValue.get();
    }

(2).代码块同步

代码块同步分乐观锁和悲观锁来讲解

使用悲观锁时,其他线程等待,进入睡眠,频繁切换任务,消耗cpu资源

代码语言:txt复制
    synchronized (this) {
        .....   
    }

使用乐观锁时,失败重试,避免任务重复切换,减少cpu消耗

代码语言:txt复制
    ReentrantLock lock = new  ReentrantLock();
    lock.lock();
    ......
    lock.unlock();

Android Threading

android中很多操作需要在主线程中执行,比如UI的操作,点击事件等等,但是如果主线程操作太多,占有的执行时间过长就会出现前面我们说的卡顿现象:

为了减轻主线程操作过多,避免出现卡顿的现象,我们把一些操作复杂的消耗时间长的任务放到线程池中去执行。下面我们就来介绍android中几种线程的类。

AsyncTask

为UI线程与工作线程之间进行快速的切换提供一种简单便捷的机制。适用于当下立即需要启动,但是异步执行的生命周期短暂的使用场景。

它提供了一种简便的异步处理机制,但是它又同时引入了一些令人厌恶的麻烦。一旦对AsyncTask使用不当,很可能对程序的性能带来负面影响,同时还可能导致内存泄露。(关于内存泄漏在上面已经讲过)

使用AsyncTask需要注意的问题?

(1).在AsyncTask中所有的任务都是被线性调度执行的,他们处在同一个任务队列当中,按顺序逐个执行。一旦有任务执行时间过长,队列中其他任务就会阻塞。

对于上面的问题,我们可以使用AsyncTask.executeOnExecutor()让AsyncTask变成并发调度。

(2).AsyncTask对正在执行的任务不具备取消的功能,所以我们要在任务代码中添加取消的逻辑(和上面Thread类似)

(3).AsyncTask使用不当会导致内存泄漏(可以参考内存泄漏一章)

HandlerThread

为某些回调方法或者等待某些任务的执行设置一个专属的线程,并提供线程任务的调度机制。

先来了解下Looper,Handler,MessageQueue

Looper: 能够确保线程持续存活并且可以不断的从任务队列中获取任务并进行执行。

Handler: 能够帮助实现队列任务的管理,不仅仅能够把任务插入到队列的头部,尾部,还可以按照一定的时间延迟来确保任务从队列中能够来得及被取消掉。

MessageQueue: 使用Intent,Message,Runnable作为任务的载体在不同的线程之间进行传递。

把上面三个组件打包到一起进行协作,这就是HandlerThread

我们先来看下源码:

代码语言:txt复制
    public class HandlerThread extends Thread {
        public HandlerThread(String name, int priority) {
            super(name);
            mPriority = priority;
        }

        @Override
        public void run() {
            mTid = Process.myTid();
            Looper.prepare();
            synchronized (this) {
                mLooper = Looper.myLooper();
                notifyAll();
            }
            Process.setThreadPriority(mPriority);
            onLooperPrepared();
            Looper.loop();
            mTid = -1;
        }

        public Looper getLooper() {
            if (!isAlive()) {
                return null;
            }
            // If the thread has been started, wait until the looper has been created.
            synchronized (this) {
                while (isAlive() && mLooper == null) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                    }
                }
            }
            return mLooper;
        }
    }

从上面的源码发现,HandlerThread其实就是在线程中维持一个消息循环队列。下面我们看下使用:

代码语言:txt复制
    HandlerThread mHanderThread = new HandlerThread("hanlderThreadTest", Process.THREAD_PRIORITY_BACKGROUND);
    mHanderThread.run();
    Looper mHanderThreadLooper = mHanderThread.getLooper();

    Handler mHandler = new Handler(mHanderThreadLooper){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //子线程中执行
            ...
        }
    };
    //发送消息
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            ...
        }
    });  

IntentService

适合于执行由UI触发的后台Service任务,并可以把后台任务执行的情况通过一定的机制反馈给UI。

默认的Service是执行在主线程的,可是通常情况下,这很容易影响到程序的绘制性能(抢占了主线程的资源)。除了前面介绍过的AsyncTask与HandlerThread,我们还可以选择使用IntentService来实现异步操作。IntentService继承自普通Service同时又在内部创建了一个HandlerThread,在onHandlerIntent()的回调里面处理扔到IntentService的任务。所以IntentService就不仅仅具备了异步线程的特性,还同时保留了Service不受主页面生命周期影响的特点。

使用IntentService需要特别注意的点:

(1).因为IntentService内置的是HandlerThread作为异步线程,所以每一个交给IntentService的任务都将以队列的方式逐个被执行到,一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。

(2).通常使用到IntentService的时候,我们会结合使用BroadcastReceiver把工作线程的任务执行结果返回给主UI线程。使用广播容易引起性能问题,我们可以使用LocalBroadcastManager来发送只在程序内部传递的广播,从而提升广播的性能。我们也可以使用runOnUiThread()快速回调到主UI线程。

(3).包含正在运行的IntentService的程序相比起纯粹的后台程序更不容易被系统杀死,该程序的优先级是介于前台程序与纯后台程序之间的。

Loader

对于3.0后ContentProvider中的耗时操作,推荐使用Loader异步加载数据机制。相对其他加载机制,Loader有那些优点呢?

  • 提供异步加载数据机制
  • 对数据源变化进行监听,实时更新数据
  • 在Activity配置发生变化(如横竖屏切换)时不用重复加载数据
  • 适用于任何Activity和Fragment

下面我们来看下Loader的具体使用:

我们以获得手机中所有的图片为例:

代码语言:txt复制
    getLoaderManager().initLoader(LOADER_TYPE, null, mLoaderCallback);
    LoaderManager.LoaderCallbacks<Cursor> mLoaderCallback = new LoaderManager.LoaderCallbacks<Cursor>() {
        private final String[] IMAGE_COLUMNS={
                MediaStore.Images.Media.DATA,//图片路径
                MediaStore.Images.Media.DISPLAY_NAME,//显示的名字
                MediaStore.Images.Media.DATE_ADDED,//添加时间
                MediaStore.Images.Media.MIME_TYPE,//图片扩展类型
                MediaStore.Images.Media.SIZE,//图片大小
                MediaStore.Images.Media._ID,//图片id
        };

        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            toggleShowLoading(true,getString(R.string.common_loading));

            CursorLoader cursorLoader = new CursorLoader(ImageSelectActivity.this,                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,IMAGE_COLUMNS,
                    IMAGE_COLUMNS[4]   " > 0 AND " IMAGE_COLUMNS[3]   " =? OR "  IMAGE_COLUMNS[3]   " =? ",
                    new String[]{"image/jpeg","image/png"},IMAGE_COLUMNS[2]   " DESC");
            return cursorLoader;
        }

        @Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
            if(data != null && data.getCount() > 0){
                ArrayList<String> imageList = new ArrayList<>();

                if(mShowCamera){
                    imageList.add("");
                }
                while (data.moveToNext()){
                    String path = data.getString(data.getColumnIndexOrThrow(IMAGE_COLUMNS[0]));
                    imageList.add(path);
                    Log.e("ImageSelect", "IIIIIIIIIIIIIIIIIIII=====>" path);
                }
                //显示数据
                showListData(imageList);
                toggleShowLoading(false,getString(R.string.common_loading));
            }
        }

        @Override
        public void onLoaderReset(Loader<Cursor> loader) {  
        }   

onCreateLoader() 实例化并返回一个新创建给定ID的Loader对象

onLoadFinished() 当创建好的Loader完成了数据的load之后回调此方法

onLoaderReset() 当创建好的Loader被reset时调用此方法,这样保证它的数据无效

LoaderManager会对查询的操作进行缓存,只要对应Cursor上的数据源没有发生变化,在配置信息发生改变的时候(例如屏幕的旋转),Loader可以直接把缓存的数据回调到onLoadFinished(),从而避免重新查询数据。另外系统会在Loader不再需要使用到的时候(例如使用Back按钮退出当前页面)回调onLoaderReset()方法,我们可以在这里做数据的清除等等操作。

ThreadPool

把任务分解成不同的单元,分发到各个不同的线程上,进行同时并发处理。

线程池适合用在把任务进行分解,并发进行执行的场景。

系统提供ThreadPoolExecutor帮助类来帮助我们简化实现线程池。

使用线程池需要特别注意同时并发线程数量的控制,理论上来说,我们可以设置任意你想要的并发数量,但是这样做非常的不好。因为CPU只能同时执行固定数量的线程数,一旦同时并发的线程数量超过CPU能够同时执行的阈值,CPU就需要花费精力来判断到底哪些线程的优先级比较高,需要在不同的线程之间进行调度切换。

一旦同时并发的线程数量达到一定的量级,这个时候CPU在不同线程之间进行调度的时间就可能过长,反而导致性能严重下降。另外需要关注的一点是,每开一个新的线程,都会耗费至少64K 的内存。为了能够方便的对线程数量进行控制,ThreadPoolExecutor为我们提供了初始化的并发线程数量,以及最大的并发数量进行设置。

代码语言:txt复制
    /**
     * 核心线程数
     * 最大线程数
     * 保活时间
     * 时间单位
     * 任务队列
     * 线程工厂
     */
    threadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            linkedBlockingQueue, sThreadFactory);
    threadPoolExecutor.execute(runnable);

我们知道系统还提供了Executors类中几种线程池,下面我们来看下这些线程池的缺点:

newFixedThreadPool 和 newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。

newCachedThreadPool 和 newScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM

我们看到这些线程池但是有缺点的,所以具体使用那种方式实现要根据我们的需求来选择。

如果想要避开上面的问题,可以参考OKHttp中线程池的实现,OKHttp中队线程调度又封装了一层,使用安全且方便,有兴趣的可以去看看源码。

线程优先级

Android系统会根据当前运行的可见的程序和不可见的后台程序对线程进行归类,划分为forground的那部分线程会大致占用掉CPU的90%左右的时间片,background的那部分线程就总共只能分享到5%-10%左右的时间片。之所以设计成这样是因为forground的程序本身的优先级就更高,理应得到更多的执行时间。

默认情况下,新创建的线程的优先级默认和创建它的母线程保持一致。如果主UI线程创建出了几十个工作线程,这些工作线程的优先级就默认和主线程保持一致了,为了不让新创建的工作线程和主线程抢占CPU资源,需要把这些线程的优先级进行降低处理,这样才能给帮组CPU识别主次,提高主线程所能得到的系统资源。

在Android系统里面,我们可以通过android.os.Process.setThreadPriority(int)设置线程的优先级,参数范围从-20到24,数值越小优先级越高。Android系统还为我们提供了以下的一些预设值,我们可以通过给不同的工作线程设置不同数值的优先级来达到更细粒度的控制。

大多数情况下,新创建的线程优先级会被设置为默认的0,主线程设置为0的时候,新创建的线程还可以利用THREAD_PRIORITY_LESS_FAVORABLE或者THREAD_PRIORITY_MORE_FAVORABLE来控制线程的优先级。

安装包优化

安装包优化的主要方向就是APP瘦身,那么App瘦身带来哪些好处呢?

(1).下载时省流量

(2).用户好的体验,下载更快,安装更快

常用的优化方式

清理无用资源

在我们应用版本的迭代中,肯定有废弃的代码和资源,我们要及时地清理,来减小App体积。

清理的方法:

(1).使用Refactor->Remove unused Resource

这个一键清除的小功能不是特别的又用,因为资源是经过反射或字符拼接等方式获取,所以检查不完全,需要我们不断的实验。

(2).使用Lint工具

lint工具还是很有用的,它给我们需要优化的点

需要注意的点:

  • 检测没有用的布局并且删除
  • 把未使用到的资源删除
  • 建议String.xml有一些没有用到的字符也删除掉

(3).开启shrinkResources去除无用资源

在build.gradle 里面配置shrinkResources true,在打包的时候会自动清除掉无用的资源,但经过实验发现打出的包并不会,而是会把部分无用资源用更小的东西代替掉。注意,这里的“无用”是指调用图片的所有父级函数最终是废弃代码,而shrinkResources true 只能去除没有任何父函数调用的情况.

代码语言:txt复制
    android {
        buildTypes {
            release {
                shrinkResources true
            }
        }
    }

(4).删除无用的语言资源

大部分应用其实并不需要支持几十种语言的国际化支持。比如我们只是保存中文支持:

代码语言:txt复制
    android {
        defaultConfig {
            resConfigs "zh"
        }
    }

(5).清理第三方库中冗余代码

对于第三方库,可能我们只是用到库中的一个功能,那么我们就可以导入源码,并且删除无关的代码,来减小体积。

图片资源优化

图片是占用空间比较大的资源,这是我们要重点优化的地方。

(1).使用压缩过的图片

这个点在这里就不再累赘。

(2).只用一套图片

对于绝大对数APP来说,只需要取一套设计图就足够了。从内存占用和适配的角度考虑,这一套图建议放在xhdpi文件夹下;

(3).使用不带alpha值的jpg图片

对于非透明的大图,jpg将会比png的大小有显著的优势,虽然不是绝对的,但是通常会减小到一半都不止。

(4).使用tinypng有损压缩

支持上传PNG图片到官网上压缩,然后下载保存,在保持alpha通道的情况下对PNG的压缩可以达到1/3之内,而且用肉眼基本上分辨不出压缩的损失.

(5).使用webp格式

webp支持透明度,压缩比比jpg更高但显示效果却不输于jpg,从Android 4.0 开始原生支持,但是不支持包含透明度,直到Android 4.2.1 才支持显示含透明度的webp,使用的时候要特别注意。

(6).使用svg

矢量图是由点与线组成,和位图不一样,它再放大也能保持清晰度,而且使用矢量图比位图设计方案能节约30~40%的空间,现在谷歌一直在强调扁平化方式,矢量图可很好的契合该设计理念。

  • 占用存储空间小
  • 无极拉伸不会出现锯齿,可以照顾不同尺寸的机型
  • Android Studio自带很多资源

(7).使用shape

特别是在扁平化盛行的当下,很多纯色的渐变的圆角的图片都可以用shape实现,代码灵活可控,省去了大量的背景图片。

(8).使用着色方案

相信你的工程里也有很多selector文件,也有很多相似的图片只是颜色不同,通过着色方案我们能大大减轻这样的工作量,减少这样的文件。

(9).对打包后的图片进行压缩

使用7zip压缩方式对图片进行压缩,建议使用微信的AndResGuard

资源动态加载

资源可以动态加载,减小apk体积。

(1).在线化素材库

如果你的APP支持素材库(比如聊天表情库)的话,考虑在线加载模式,因为往往素材库都有不小的体积

(2).皮肤加载

有的app用到皮肤库,这是就可以使用动态加载。

(3).模块插件化

如果模块过多,apk体积过大,可以考虑插件化,来减少体积。

lib库优化

只提供对主流架构的支持,比如arm,对于mips和x86架构可以考虑不支持,这样可以大大减小APK的体积.

7zip压缩资源

对于assets或者raw文件夹中的资源,可以使用7zip压缩,使用时进行解压。

代码混淆

在gradle使用minifyEnabled进行Proguard混淆的配置.

代码语言:txt复制
    android {
        buildTypes {
            release {
                minifyEnabled true
            }
        }
    }
  • 为什么代码混淆可以让apk变小?
  1. 可以删除注释和不用的代码。
  2. 将java文件名改成短名
  3. 将方法名改成短名

资源(res)混淆

资源混淆简单来说希望实现将res/drawable/icon,png变成res/drawable/a.png,或我们甚至可以将文件路径也同时混淆,改成r/s/a.png。

建议使用微信的AndResGuard

使用微信AndResGuard

使用微信AndResGuard对资源混淆并且压缩图片res等资源

代码语言:txt复制
    apply plugin: 'AndResGuard'
    buildscript {
        dependencies {
            classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.1.7'
        }
    }
    andResGuard {
        mappingFile = null
        use7zip = true
        useSign = true
        keepRoot = false
        // add <your_application_id>.R.drawable.icon into whitelist.
        // because the launcher will get thgge icon with his name
        def packageName = <your_application_id>
                whiteList = [
        //for your icon
        packageName   ".R.drawable.icon",
                //for fabric
                packageName   ".R.string.com.crashlytics.*",
                //for umeng update
                packageName   ".R.string.umeng*",
                packageName   ".R.string.UM*",
                packageName   ".R.string.tb_*",
                packageName   ".R.layout.umeng*",
                packageName   ".R.layout.tb_*",
                packageName   ".R.drawable.umeng*",
                packageName   ".R.drawable.tb_*",
                packageName   ".R.anim.umeng*",
                packageName   ".R.color.umeng*",
                packageName   ".R.color.tb_*",
                packageName   ".R.style.*UM*",
                packageName   ".R.style.umeng*",
                packageName   ".R.id.umeng*"
        ]
        compressFilePattern = [
        "*.png",
                "*.jpg",
                "*.jpeg",
                "*.gif",
                "resources.arsc"
        ]
        sevenzip {
            artifact = 'com.tencent.mm:SevenZip:1.1.7'
            //path = "/usr/local/bin/7za"
        }
    }

Facebook的redex优化字节码

redex是facebook发布的一款android字节码的优化工具.

redex

0 人点赞