0%

前言

如果应用耗电量过大,就会消耗用户手机过多的电量,甚至发热,现在一般手机都会有耗电量过大的提醒,当用户看到后,也可能会把你的应用卸载掉,因此需要对其进行优化,让应用的耗电量尽可能小。

检测工具

Battery Historian:https://github.com/google/battery-historian

Read more »

前言

Android中使用SQLite做为数据库,当数据量很大的情况下,就可能会出现性能问题,比如查询速度过慢,更新速度过慢等,因此需要对其进行优化。

优化方法

  • 使用事务

    每次事务都会修改rollback journal文件,因此比较耗时,对于批量操作,应该合并到同一个事务中。

  • 建立索引

    对查询多,更新少的字段建立索引,但索引会占用空间,而且会降低更新速度。

  • 使用SQLiteStatement

    对于循环执行数据库操作,应该在循环外预编译好sql语句得到SQLiteStatement对象,然后循环内bindXXX,而不是直接在循环内调用execSQL方法,这样会导致创建大量的SQLiteStatement临时对象,而且每次都会重新编译sql语句。

  • 缓存getColumnIndex返回结果

    对于循环执行数据库操作,getColumnIndex方法应该放到循环前调用,而不是每次循环都调用一次,虽然该方法的本质是从Map中查找index,但对性能还是有一定的影响的,放在循环前调用会稍微快一些。

  • Cursor优化

    正如微信的WCDB所说,在合适的场景下,直接把Cursor的共享内存缓存去掉,会提高数据查询的速度。

  • 分表、分库

    SQLite的同步锁是表级别的,因此为了避免等待,可以按照具体场景拆分为多个表,或者分库。

  • SQL语句拼接

    不要用+进行连接,用StringBuilder,有循环的最后放到循环外部,不要每次循环都去生成sql语句字符串。

  • SQL语句优化

    适用所有数据库优化,不仅仅只是SQLite,见下部分。

Read more »

前言

APK包过大,可能会导致用户不想下载,或者消耗用户更多的流量,需要更多的下载等待时间,因此需要对APK进行瘦身,使得APK包尽量精简。

瘦身方法

  • so优化

    1. so裁剪:避免为了使用一个小功能引入一个大的so,删除用不到的so代码和依赖。

    2. 只保留armeabi或者armeabi-v7a下的so文件:armeabi属于比较老的CPU架构,不支持硬件浮点运算,所以速度较慢,但兼容设备更多,而现在大部分首页都是支持armeabi-v7a架构的。

    3. 分渠道打包so:不同渠道的用户手机CPU架构不同,为了避免同时加入多种so,可以对不同渠道放入对应的so。

  • 依赖库

    这里主要指第三方的jar,aar等依赖库,引入的库应该尽可能精简。

  • res优化

    1. 只保留一套图片: 对于用到的图片,一般放到xxhdpi目录就可以,具体放哪里,要看目标用户手机主要的分辨率是多少。

    2. 动态加载:不是很重要的图,或者非首屏显示的图,改为动态加载。

    3. 图片压缩:tinypng,用webp代替png。

    4. 无用资源:用AS删除无用资源。

    5. 矢量图:用矢量图代替真实图片,矢量图代码占用空间小,而且不会失真。

    6. 多语言:对国内用户只打中文包,resConfigs指定为zh-rCN。

    7. 使用AndResGuard开源库:微信出的工具,用于压缩资源路径,同时也起到混淆资源文件名的作用。

    8. 精简字体文件:字体文件只包含需要用到的文字、数字或者符合等,而不是直接引入整个字体文件。

  • assets优化

    跟res优化差不多,删除无用资源,压缩资源,动态下载。

  • 代码优化

    1. 打开minifyEnabled:代码压缩,编译时会自动移除没有被使用到的代码,可以通过keep来强制指定不移除某些代码。

    2. 打开shrinkResources:资源压缩,必须在minifyEnabled为true的前提下使用,编译时会自动移除没有被使用到的资源,也可以自定义指定哪些资源不移除。

    3. proguard代码混淆:这个正式包都有,代码混淆能够减少类名、方法名等长度,移除没有被使用到的代码。

      注意minifyEnabled和proguard的区别:minifyEnabled只能移除无用代码,速度较快;而proguard除了能移除无用代码,还可以进行名称混淆,优化代码,速度较慢。参考文章:https://stackoverflow.com/questions/37007485/whats-the-difference-between-minifyenabled-and-useproguard-in-the-android-p

    4. 代码动态化:组件化,代码动态下载。

    5. 单个枚举大概占用1~1.4KB,用常量代替枚举。

为什么需要MultiDex

在Android5.0以下,虚拟机执行的是Dalvik指令,Dalvik指令集的方法索引参数至占用两个字节,也就是最多能区分65536个不同的方法,这个是65536方法数限制的根本原因,当然dx工具在打包时会直接判断方法数是否超过65536,超过就直接抛异常,这个只是表面原因。ART本身就对MultiDex做了支持,因此无需开发者对其做特殊处理。

Read more »

如果APP启动速度很慢,会导致用户体验很差,甚至用户流失。所以我们需要持续地对启动速度进行优化,不断减少启动等待时间,把优化做到极致。

启动类型

启动类型可以分为三种:

  • 冷启动

    进程不存在,启动时需要先创建进程,然后再创建并打开页面。

  • 温启动

    进程存在,但页面不存在,只需要创建并打开页面。

  • 热启动

    进程存在,页面也存在,直接把页面拉到前台即可。

Read more »

我们知道,Cursor使用结束后,如果没有调用Cursor的close方法,就可能会导致内存泄露,但其原因是什么呢?究竟是什么对象泄露了?下面通过源码分析其原因。

SQLiteDatabase的query过程

  1. SQLiteDatabase执行查询操作很简单,直接调用query方法:

    CustomSQLiteDatabase helper = new CustomSQLiteDatabase(this);
    SQLiteDatabase db = helper.getWritableDatabase();
    Cursor cursor = db.query("COMPANY", null, null, null, null, null, null);
  2. 内部会创建一个SQLiteDirectCursorDriver对象,然后调用其query方法:

    public Cursor query(CursorFactory factory, String[] selectionArgs) {
        final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal);
        final Cursor cursor;
        try {
            // 建立index->列名映射,最终放在Object数组中
            query.bindAllArgsAsStrings(selectionArgs);
    
            if (factory == null) {
                cursor = new SQLiteCursor(this, mEditTable, query);
            } else {
                cursor = factory.newCursor(mDatabase, this, mEditTable, query);
            }
        } catch (RuntimeException ex) {
            query.close();
            throw ex;
        }
    
        mQuery = query;
        return cursor;
    }

    一般我们不会指定CursorFactory,所以factory为null,这种情况下创建的是一个SQLiteCursor对象,也就是外部的query方法返回的其实就是这个SQLiteCursor对象。

    另外注意到这里调用query方法时,只是做了一些初始化操作,简单返回一个SQLiteCursor对象而且,并不会真的去执行数据库查询,因此这个query执行起来不耗时。

    接着我们拿到这个SQLiteCursor对象后,调用moveToPosition(0)方法把游标移动到第一条记录,这个操作就比较耗时了,SQLiteCursor继承自AbstractWindowedCursor,看一下AbstractWindowedCursor的moveToPosition方法:

    @Override
    public final boolean moveToPosition(int position) {
        // Make sure position isn't past the end of the cursor
        final int count = getCount();
        if (position >= count) {
            mPos = count;
            return false;
        }
    
        // Make sure position isn't before the beginning of the cursor
        if (position < 0) {
            mPos = -1;
            return false;
        }
    
        // Check for no-op moves, and skip the rest of the work for them
        if (position == mPos) {
            return true;
        }
    
        boolean result = onMove(mPos, position);
        if (result == false) {
            mPos = -1;
        } else {
            mPos = position;
        }
    
        return result;
    }

    这里的getCount方法和onMove方法,都可能会触发执行fillWindow方法,而fillWindow方法就是真正去查询数据库,然后把数据填充到共享内存中。对于这块共享内存,默认大小为2MB,只有当真正查询数据库时才会被创建,然后填充查询结果数据,假如结果数据过大填充不下,就只填充一部分,如果下次读取到填充之外的数据,就需要重新执行数据库查询,清空原有共享内存缓存数据,然后把新数据填充到共享内存中。

  3. 继续看fillWindow方法:

    private void fillWindow(int requiredPos) {
        clearOrCreateWindow(getDatabase().getPath());
        try {
            Preconditions.checkArgumentNonnegative(requiredPos,
                    "requiredPos cannot be negative, but was " + requiredPos);
    
            if (mCount == NO_COUNT) {
                mCount = mQuery.fillWindow(mWindow, requiredPos, requiredPos, true);
                mCursorWindowCapacity = mWindow.getNumRows();
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "received count(*) from native_fill_window: " + mCount);
                }
            } else {
                int startPos = mFillWindowForwardOnly ? requiredPos : DatabaseUtils
                        .cursorPickFillWindowStartPosition(requiredPos, mCursorWindowCapacity);
                mQuery.fillWindow(mWindow, startPos, requiredPos, false);
            }
        } catch (RuntimeException ex) {
            // Close the cursor window if the query failed and therefore will
            // not produce any results.  This helps to avoid accidentally leaking
            // the cursor window if the client does not correctly handle exceptions
            // and fails to close the cursor.
            closeWindow();
            throw ex;
        }
    }

    一开始就调用clearOrCreateWindow方法,如果共享内存已经存在就清空,没有就创建,这里其实就是创建一个CursorWindow对象,当然它只是一个壳而已,最终对共享内存的操作都是通过native层实现的。

    共享内存准备好之后,就可以调用mQuery的fillWindow方法查询数据库填充数据了,mQuery的类型为SQLiteQuery,fillWindow方法会连接到数据库查询数据,然后填充到共享内存中。

Read more »

LeakCanary是什么

LeakCanary是square推出的用于检测Android内存泄漏的开源工具,使用起来十分简单,接入成本低,项目地址:https://github.com/square/leakcanary

使用方法

// module的build.gradle添加依赖
implementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'

public class App extends Application {

@Override
public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
}
Read more »

为什么需要优化

Android中内存优化是一个比较重要的话题,因为内存会直接影响到程序的运行性能,在开发中经常会出现一些内存相关的问题,包括内存泄露,内存抖动,占用过多内存等,因此优化的目的,就是让减少这几类问题的出现,或者彻底解决,让程序运行起来更加流畅,性能更高效。

常用检测工具

  • LeakCanary

    用于检测内存泄露,如果发生内存泄露,会给出泄露对象的引用链,其内部是通过WeakReferences的回收队列实现内存泄露检测的。

  • Android Studio的Profiler工具

    可查看对象占用内存情况,只能查看debuggable应用。

  • adb命令

    执行命令adb shell dumpsys meminfo,可以查看所有应用的内存信息。常用的参数有–package,用于指定要查看的包名的所有进程的内存信息,比如我们要查看微信的内存信息,执行下面的命令:

    adb shell dumpsys meminfo --package com.tencent.mm

    这里就会微信所有进程的内存信息,如果我们只需要看主进程的信息,可以直接通过pid指定,所以先执行命令ps获取到pid:

    adb shell ps -A | grep com.tencent.mm

    打印结果如下:

    u0_a170       3525   633 2068856  21644 0                   0 S com.tencent.mm:appbrand1
    u0_a170      19179   633 2577364 112404 0                   0 S com.tencent.mm
    u0_a170      19404   633 2240664  11740 0                   0 S com.tencent.mm:appbrand0
    u0_a170      26971   633 1965968   5312 0                   0 S com.tencent.mm:exdevice
    u0_a170      28292   633 2003436   5296 0                   0 S com.tencent.mm:push

    看到主进程的pid为19179,然后执行命令:

    adb shell dumpsys meminfo 19179

    打印结果如下:

    Applications Memory Usage (in Kilobytes):
    Uptime: 138531332 Realtime: 241333585
    
    ** MEMINFO in pid 19179 [com.tencent.mm] **
                       Pss  Private  Private  SwapPss     Heap     Heap     Heap
                     Total    Dirty    Clean    Dirty     Size    Alloc     Free
                    ------   ------   ------   ------   ------   ------   ------
      Native Heap    40613    40564        0    72101   164480   132995    31484
      Dalvik Heap    19065    19016        0     8873    52069    27493    24576
     Dalvik Other    11200    11196        0      893                           
            Stack      100      100        0       16                           
           Ashmem        4        4        0        0                           
          Gfx dev     6432     6432        0        0                           
        Other dev       28        0       28        0                           
         .so mmap     6138      440     4328     3370                           
        .jar mmap       28       28        0      336                           
        .apk mmap     1157       88      672      160                           
        .ttf mmap       60        0        0        0                           
        .dex mmap    22720        0    19372       28                           
        .oat mmap     2520        0        4        0                           
        .art mmap    10700     9644      260     3067                           
       Other mmap      336        0      324        4                           
       EGL mtrack     9248     9248        0        0                           
        GL mtrack     5436     5436        0        0                           
          Unknown     2666     2664        0     4830                           
            TOTAL   232129   104860    24988    93678   216549   160488    56060
    
     App Summary
                           Pss(KB)
                            ------
               Java Heap:    28920
             Native Heap:    40564
                    Code:    24932
                   Stack:      100
                Graphics:    21116
           Private Other:    14216
                  System:   102281
    
                   TOTAL:   232129       TOTAL SWAP PSS:    93678
    
     Objects
                   Views:     1388         ViewRootImpl:        1
             AppContexts:        7           Activities:        1
                  Assets:        7        AssetManagers:        0
           Local Binders:      157        Proxy Binders:       98
           Parcel memory:       82         Parcel count:      327
        Death Recipients:        8      OpenSSL Sockets:        0
                WebViews:        0
    
     SQL
             MEMORY_USED:      528
      PAGECACHE_OVERFLOW:       64          MALLOC_SIZE:      117
    
     DATABASES
          pgsz     dbsz   Lookaside(b)          cache  Dbname
             4      436             97       25/40/10  /data/user/0/com.tencent.mm/databases/beacon_tbs_db
             4      108            109       27/42/16  /data/user/0/com.tencent.mm/databases/google_app_measurement.db
             4       36             99         4/34/6  /data/user/0/com.tencent.mm/databases/tes_db
    
     Asset Allocations
        : 132K

    对于第一部分,一般只需要关注前两列就行,Pss Total表示该进程占用内存大小,包括了按比例计算进程间共享内存,而Private Dirty就只是该进程占用内存大小。然后对应到具体的行,Native Heap为本地堆占用内存大小,Dalvik Heap为Java堆占用内存大小,Dalvik Other为JIT和垃圾回收记录占用内存大小,.so mmap为映射的so代码占用的内存大小,.dex mmap为映射的dex代码占用的内存大小。

    对于第三部分,表示在当前进程中,某些重要类型的实例对象数量,比如ViewRootImpl就是窗口数量,AppContexts就是Context数量,而Activities为Activity的数量。

Read more »

为什么需要优化

Android布局可能存在的主要问题有两个:过度绘制和卡顿,过度绘制会浪费资源,卡顿会影响用户体验,甚至导致ANR,因此需要进行优化,优化都是围绕着这两个问题展开的,最终目的就是减少过度绘制,减少卡顿。

常见检测工具

有时通过直觉无法明显发现过度绘制和卡顿,或者无法准确量化其严重程度,因此需要借助检测工具来帮助我们排查问题,常用的工具主要有两个:开发者选项中的调试GPU过度绘制和GPU呈现模式分析。

Read more »

Protobuf是什么

全称为Protocol Buffers,Google推出的序列化框架,用于将自定义数据结构序列化成字节流,和将字节流反序列化为数据结构,该框架不依赖开发语言,也不依赖运行平台,扩展性好,目前支持的语言比较多,包括Java,C++,Python,Ruby等。

使用Protobuf

在这里使用Windows和Java进行实例演示:

Read more »