Android 性能优化总结

作者: dooze | 来源:发表于2016-10-25 01:28 被阅读593次

    将从以下几个方面总结Android的应用性能优化

    性能

    • 框架API
    • UI 性能
    • I/O性能
    • 屏幕滚动性能

    内存

    • Android 如何管理内存
      • OOM终结 & 低内存终结
      • 应用内存使用监测
    • 识别内存泄露
    • 最佳实践

    糟糕的用户体验

    • Activity 启动时间过长
    • 应用无反应(ANR)
    • 帧速率差

    关于帧

    帧速率

    • 为了保证能达到60fps,最多只有16ms去处理每一帧
    • 而保证能达到24fps,最多只有41ms去处理每一帧

    常见操作耗时

    • Binder RPC 调用大约花费 0.12ms
    • 从闪存读取一个字节大约花费 0.0x ~ 5ms(一个文件只读一个字节有可能大于1ms)
    • 写内容到闪存大约花费 1-100ms(一个文件只写一个字节有可能小于1ms)
    • TCP 初始化加上HTTP提取通常花费秒级的时间(此处指的是建立链接并获取链接返回的数据的时间)

    读写操作在性能差一些的机子上可能时间会有出入

    往磁盘写内容的时候,会随着磁盘的剩余空间的较少而导致写速率不断减低

    永远不要做阻塞UI线程的事情,用一个新的线程去做可能会影响UI体验的事情

    四种可以异步的实现:

    1. Runnable
    2. Thread
    3. Future
    4. ExecutorService
    • 使用Thread
    new Thread(new Runnable() {
      @Override
      public void run() {
        // do some heavy work
      }
    }).start();
    
    
    • 使用内置AsyncTask
    new AsyncTask<URL, Integer, Integer>() {
      protected Long doInBackground(URL... urls) {
        final int count = urls.length;
          for ( int i = 0; i < count; i++ ) {
            Downloader.download(url);
            publishProgress(i);
        }
      return count;
      }
      protected void onProgressUpdate(Integer... progress) {
        setProgress(progress[0]);
      }
      protected void onPostExecute(Integer result) {
        showDialog(“Downloaded “ + result + “ files”);
      }
    }
    
    • 使用HandlerThread
    HandlerThread mHandlerThread = new HandlerThread("WorkerThread");
    Handler handler = new Handler(mHandlerThread.getLooper()) {
      @Override
      public void handleMessage(Message msg) {
        switch (msg.what) {
          case JOB_1:
            // do job #1
          break;
          case JOB_2:
            // do job #2
          break;
        }
      }
    };
    
    
    handler.sendEmptyMessage(JOB_1);
    handler.sendEmptyMessage(JOB_2);
    
    
    handler.post(new Runnable() {
      @Override
      public void run() {
        // do more work
      }
    });
    
    
    @Override
    protected void onDestroy() {
      mHandlerThread.quit();
      super.onDestroy();
    }
        
    
    • 使用AsyncQueryHandler
    new AsyncQueryHandler(getContentResolver()) {
      @Override
      protected void onQueryComplete(int token, Object cookie,
        Cursor cursor) {
          if (token == 0) {
            // get data from cursor
          }
        }
        }.startQuery(0, // token
            null, // cookie
            RawContacts.CONTENT_URI, null, // projection
            RawContacts.CONTACT_ID + "<?", // selection
            new String[] { "888" }, // selectionArgs
            RawContacts.DISPLAY_NAME_PRIMARY + " ASC" // orderby
            );
    
    • 使用IntentService
    public class WorkerService extends IntentService {
      public WorkerService() {
        super("WorkerThread");
      }
      @Override
      protected void onHandleIntent(Intent intent) {
        String action = intent.getAction();
        if ("com.test.DO_JOB_1".equals(action)) {
            // do job #1
        }
      }
    }
    
    
    
    startService(new Intent("com.test.DO_JOB_1"));
    

    UI线程性能总结

    • Activity or Fragment
      • AsyncTask
      • Handler,HandlerThread
      • AsyncTaskLoader
    • ContentProvider
      • AsyncQueryHandler
      • CursorLoader
    • Service
      • IntentService
      • Parcel.writeStrongBinder(IBinder binder)

    View Hierarchy

    • Measure
    • Layout
    • Draw
    • Key Events
    • Trackball Events
    • Touch Evnets

    Tips:

    • 降低布局层次结构的复杂性

    • 使用层次结构查看器来检查是否存在瓶颈

    • 使用RelativeLayout或者GridLayout来简化复杂布局的层次嵌套

    • 使用<merge />标签来较少布局层次

    • 使用<ViewStub />标签来延迟该标签下的布局的渲染

      <ViewStub
      android:id="@+id/stub_import"
      android:inflatedId="@+id/panel_import"
      android:layout="@layout/progress_overlay"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:layout_gravity="bottom" />
      
      ((ViewStub)
      findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
      // or
      View importPanel = ((ViewStub)
      findViewById(R.id.stub_import)).inflate();
      
    • 使用layoutopt检测常见问题

    I/O性能优化

    • 异步写SharedPreferences

      SharedPreferences.Editor.apply(); // 异步
      SharedPreferences.Editor.commit(); // 同步
      
    • 数据库query查询语句中的 * 替换成具体的列值

    • 使用TraceView配置您的数据库查询

    • 使用LIMIT子句减少选择行

    • 最小化完整窗口时间

    • 使用索引优化数据库查询

    • 预编译常用的SQL语句

    String sql = “INSERT INTO table VALUES (?, ?)”;
    SQLiteStatement stmt = mDatabase.compileStatement(sql);
    DatabaseUtils.bindObjectToProgram(stmt, 1, 1);
    DatabaseUtils.bindObjectToProgram(stmt, 2, 2);
    stmt.execute();
    stmt.close();
    
    //或者使用 PreparaStatement
    
    • 推迟ContentObserver.onChange()中的自动重新检查
    getContentResolver().registerContentObserver(uri, true,
      new ContentObserver(new Handler()) {
        @Override
        public void onChange(boolean selfChange) {
          mDirty = true;
        }
      });
    
      @Override
      protected void onResume() {
        super.onResume();
        if (mDirty) {
          // start query again
          mDirty = false;
        }
      }
    
    • 在事务中使用批量操作

      • ContentProviderOperation!
      • ContentProviderOperation.Builder!
      • ContentResolver.applyBatch()
    • 在一个比较长的事务中允许偶尔的事务提前

      SQLiteDatabase.yieldIfContendedSafely()
      
    • 使用事件日志调试

      adb logcat -b events content_query_sample:I *:S
      adb logcat -b events content_update_sample:I *:S
      adb logcat -b events db_sample:I *:S

    滑动性能优化(List)

    • ListView : 通过复用view来避免不必要的inflate操作

      @Override
      public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.main, parent, false);
        }
        // ....
      }
      
    • 通过ViewHolder缓存v试图,而避免不必要的findViewByI'd

      @Override
      public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
          convertView = mInflater.inflate(R.layout.main, parent, false);
          ViewHolder holder = new ViewHolder();
          holder.img = (ImageView) convertView.findViewById(R.id.image);
          holder.txt = (TextView) convertView.findViewById(R.id.text);
          convertView.setTag(holder);
        }
        ViewHolder holder = (ViewHolder) convertView.getTag();
        holder.img.setImageResource(R.drawable.icon);
        holder.txt.setText(R.string.hello);
        return convertView;
      }
      private static class ViewHolder {
        ImageView img;
        TextView txt;
      }
      
      
    • 避免view的不必要绘制(例如背景的重复绘制)

      Android 中 会绘制每一个父view即使它被覆盖在一个不透明的子view之下

      当你有一个父view并且是永远不可见的,那么不要绘制它(包括他的背景)

    • 大多数的情况下你不需要绘制window的背景

      //Activity中
      getWindow().setBackgroundDrawable(null);
      
      //style中
      android:windowBackground="@null"
      
    • 避免在运行时进行图片缩放(特殊业务需求除外)

    • 避免在视图(ListView等)滚动的时候进行动画,如果业务要求使用动画,那么请关闭绘制缓存

      ListView.setDrawableCacheEnabled(false)
      
    • 使用Allocation Tracker(内存分配追踪器)检测并避免频繁的垃圾回收

    • 考虑使用Object Pool ,StringBuilder等封装类型

    • 缓存的时候考虑使用SoftReference

    • 在调试模式的时候启用StrictMode(可以检查大部分不规范,不安全操作)

      public void onCreate() {
        if (DEVELOPER_MODE) {
              StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectDiskReads()
                .detectDiskWrites()
                .detectNetwork()
                .penaltyLog()
                .build());
              StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectLeakedSqlLiteObjects()
                .detectLeakedClosableObjects()
                .penaltyLog()
                .penaltyDeath()
                .build());
          }
        super.onCreate();
      }
      
    • 检查主线程Looper是否有不必要的活动

      Looper.setMessageLogging();
      

    Memory

    在系统级别,Android使用低内存驱动程序运行修改过的OOM Killer

    包括:

    • Linux OOM killer
    • OOM_ADJ
    • Android Low Memory Killer

    Android中的低内存阈值(init.rc中)

    # Define the memory thresholds at which the above process classes will
    # be killed. These numbers are in pages (4k).
    
    setprop ro.FOREGROUND_APP_MEM 2048
    setprop ro.VISIBLE_APP_MEM 3072
    setprop ro.PERCEPTIBLE_APP_MEM 4096
    setprop ro.HEAVY_WEIGHT_APP_MEM 4096
    setprop ro.SECONDARY_SERVER_MEM 6144
    setprop ro.BACKUP_APP_MEM 6144
    setprop ro.HOME_APP_MEM 6144
    setprop ro.HIDDEN_APP_MEM 7168
    setprop ro.EMPTY_APP_MEM 8192
    

    OOM_ADJ基于重要性级别(init.rc中)

    # Define the oom_adj values for the classes of processes that can be 
    # killed by the kernel. These are used in ActivityManagerService. 
    
    setprop ro.FOREGROUND_APP_ADJ 0 
    setprop ro.VISIBLE_APP_ADJ 1 
    setprop ro.PERCEPTIBLE_APP_ADJ 2 
    setprop ro.HEAVY_WEIGHT_APP_ADJ 3 
    setprop ro.SECONDARY_SERVER_ADJ 4 
    setprop ro.BACKUP_APP_ADJ 5 
    setprop ro.HOME_APP_ADJ 6 
    setprop ro.HIDDEN_APP_MIN_ADJ 7 
    setprop ro.EMPTY_APP_ADJ 15 
    

    进程重要性级别

    • Persistent(持续存在)
      • OOM_ADJ < 0
      • system_server (-16) , com.android.phone (-12)
    • Foreground(前台进程)
      • FOREGROUND_APP_ADJ = 0
      • 运行前台Activity
      • 运行一个Service,执行onCreate(),onStartCommand(),onDestroy()
      • 托管由前台Activity或前台进程绑定的Service
      • 运行一个BroadcastReceiver,执行onReceive()
      • 托管由持续或前台进程使用的ContentProvider
    • Visible(可见进程)
      • VISIBLE_APP_ADJ = 1
      • 运行可见的Activity(不在前台,也就是,不是正在和用户交互的)
      • 运行由startService()启动的Service,Service使用startForeground()
        使自己处于前台状态
    • Service(服务进程)
      • SECONDARY_SERVER_ADJ = 4
      • 运行由startService()启动Service,并且不是可见进程
    • Background(后台进程)
      • HIDDEN_APP_MIN_ADJ (7) .. EMPTY_APP_ADJ (15)
      • 不包含任何活动应用程序组件的进程

    Low Memory 回调

    Activity.onLowMemory()
    Fragment.onLowMemory()
    Activity.onSaveInstanceState(Bundle)
    Service.onLowMemory()
    ContentProvider.onLowMemory()
    

    在应用程序级别,Android限制了多少内存可以分配给每个应用程序。

    Android为每个应用程序定义了一个堆限制,并指示何时抛出OutOfMemoryError

    Android Studio 中 Heap窗口中的相关术语

    术语 解释
    Heap limit 应用在Dalvik堆中的最大允许占用空间
    Heap size 当前Dalvik堆的大小
    Allocated 应用在Dalvik堆上分配的字节总数
    Free Heap size – Allocated
    % Used Allocated / Heap size * 100%
    External allocation (3.0之前) Bitmap byte[]

    ActivityManager.getMemoryClass() 可以查看当前应用Heap size limit

    OOM 发生的情形

    • 2.3之前

    Heap size + external allocation + new allocation request >= Heap limit

    • 2.3(包括)之后

    Heap size + new allocation request >= Heap limit

    new allocation request : 新的内存开辟请求大小

    不代表进程内存使用的情形

    • 每个进程从zygote fork出来后,都会有2mb以上的开销
    • 在使用native的时候会开辟更多的内存:
      • Android应用程序运行在Dalvik VM中,同时通过JNI加载本地库
      • 由应用程序调用的Dalvik级API可以代表申请人使用本机库。
    • 如果你启用了硬件加速(4.0中默认开启),那么会多有8mb的内存去使用OpenGL

    查看内存使用情况

    • 根据进程内存使用情况排序:

    adb shell procrank -p

    PID Vss Rss Pss Uss cmdline!
    3156 80272K 80220K 59228K 57624K com.htc.launcher
    1455 94540K 58728K 37488K 36060K system_server
    9000 55224K 55200K 33900K 32412K com.roguso.plurk
    6713 47912K 47880K 27719K 26788K tw.anddev.aplurk
    1624 44804K 44760K 24954K 24200K android.process.acore
    2081 44992K 44960K 23205K 21628K com.htc.android.mail
    1604 41288K 41248K 22393K 21752K com.htc.android.htcime
    1594 40912K 40844K 21588K 20284K com.htc.weatheridlescreen
    1622 39904K 39872K 21297K 20696K com.android.phone
    

    VSS(Virtual Set Size):进程可以访问的页面总数

    RSS(Resident Set Size): RAM中进程可以访问的页总数

    PSS(Proportion Set Size):进程在RAM中使用的页面总数,其中每个页面的大小是页面总数除以共享它的进程数

    USS(Unique Set Size):进程可以访问的非共享页面的数量

    • 列出进程的虚拟内存区域

      adb shell procmem -p <pid>

    Vss Rss Pss Uss ShCl ShDi PrCl PrDi Name
    ------- ------- ------- ------- ------- ------- ------- ------- 
    4K 4K 0K 0K 4K 0K 0K 0K /system/bin/app_process
    4K 4K 0K 0K 4K 0K 0K 0K /system/bin/app_process
    13908K 13908K 11571K 11508K 2400K 0K 11508K 0K [heap]
    0K 0K 0K 0K 0K 0K 0K 0K [heap]
    4K 4K 4K 4K 0K 0K 4K 0K [heap]
    36K 36K 0K 0K 0K 36K 0K 0K /dev/__properties__
    .......
    

    adb shell dumpsys meminfo <pid>

    Applications Memory Usage (kB):
    Uptime: 89133197 Realtime: 106110266
    
    ** MEMINFO in pid 11961 [com.htc.friendstream] **
                    native dalvik other total limit bitmap nativeBmp
              size: 15032  8535   N/A   23567 32768 N/A    N/A
         allocated: 14565  5697   N/A   20262 N/A   4669   1918
              free: 162    2838   N/A   3000  N/A   N/A    N/A
             (Pss): 4105   2550   13952 20607 N/A   N/A    N/A
    (shared dirty): 2440   1928   5532  9900  N/A   N/A    N/A
      (priv dirty): 4044   708    12716 17468 N/A   N/A    N/A
    
    Objects
               Views: 0 ViewRoots: 0
         AppContexts: 0 Activities: 0
              Assets: 7 AssetManagers: 7
       Local Binders: 11 Proxy Binders: 15
    Death Recipients: 1
     OpenSSL Sockets: 0!
    

    Private Dirty = USS

    无法分页到磁盘并且不与任何其他进程共享的进程内部RAM量

    当进程消失时,系统可以使用的RAM

    • 一些重要的虚拟内存区域

    /dev/ashmem/dalvik-heap : 在Dalvik级别为堆分配的匿名页面

    [heap], [anonymous] : 由malloc()在本机级别分配的匿名页面

    /system/framework/*.odex (release build)

    /data/dalvik-cache/*.odex (debug build) : 文件支持的mmap页面

    Garbage collection(垃圾收集)

    • 2.3之前的GC

      收集垃圾的时候会停止其他所有的工作

      对整个堆进行收集

      造成的暂停时间一般都大于100ms

    • 2.3及其之后

      不会暂停其他工作,而是与其他工作同时进行(绝大部分是这样的)

      一次垃圾收集只是对堆的一部分而已

      造成的暂停时间一般小于5ms

    Memory leaks(内存泄露)

    • GC并不能避免内存泄露
    • 有一个指向长期存在的且未使用的对象的应用,导致这个不被使用的对象不能被回收
    • 在Android中,通常发生内存泄露的是对Context或者Activity的引用

    常见的因为Context 或着 Activity造成的内存泄露

    1. 在Activity中存在长期存在的指向非静态内部类实例对象的引用

      public class TestActivity extends Activity{
        static LeakyTest leaky = null;
        class LeakyTest{
          void doSoming(){
            //doing
          }
        }
        
        @Override
        protected void onCreate(Bundle saveInstanceStates){
          super.onCreate(saveInstanceStates);
          if(leaky==null)
            leaky = new LeakyTest();
          //....
        }
        //.....
      }
      
    2. 在Activity中有超出Activity生命周期且长期存活的线程

         new Thread(new Runnable(){
           @Override
           public void run(){
             //do long-live works
           }
         }).start();
      

    有用的方法

    • 使用logcat检查是否有内存随着时间的推移而不断增加(尤其注意某些方法的执行步骤!)

    例如得到的日志信息:

    D/dalvikvm(9050):GC_CONCURRENT free 2049k, 65% free  3571k/9991k, external 4703k/5261k, paused 2ms+2ms
    

    D/dalvikvm(9050): <u>GC_CONCURRENT</u> free 2049k, 65% free 3571k/9991k, external 4703k/5261k, paused 2ms+2ms

    下划线处GC的原因:

    GC_CONCURRENT

    GC_FOR_MALLOC

    GC_EXTERNAL_ALLOC

    GC_HPROF_DUMP_HEAP

    GC_EXPLICIT

    D/dalvikvm(9050): GC_CONCURRENT <u>free 2049k</u>, 65% free 3571k/9991k, external 4703k/5261k, paused 2ms+2ms

    下划线处GC的原因:

    内存释放

    D/dalvikvm(9050): GC_CONCURRENT free 2049k, <u>65% free 3571k/9991k</u>, external 4703k/5261k, paused 2ms+2ms

    下划线处GC的原因:

    内存释放

    堆进行信息统计

    D/dalvikvm(9050): GC_CONCURRENT free 2049k, 65% free 3571k/9991k, <u>external 4703k/5261k,</u> paused 2ms+2ms

    下划线处GC的原因:

    内存释放

    堆进行信息统计

    内部内存进行信息统计

    D/dalvikvm(9050): GC_CONCURRENT free 2049k, 65% free 3571k/9991k, external 4703k/5261k, <u>paused 2ms+2ms</u>

    下划线处GC的原因:

    内存释放

    堆进行信息统计

    内部内存进行信息统计

    时间暂停

    • 使用分配跟踪器查看是否有随着时间分配未预料的对象(Android Studio中的logcat窗口中有对应的按钮)
    • 使用(Histogram view)直方图视图查看活动实例的数量。 有多于一个Activity的一个实例,那么这是一个强烈的Activity / Context泄露的迹象。
    • 按保留大小排序的Dominator Tree视图有助于识别保留了大量的内存且不能被释放的对象。 他们通常是找到内存泄漏的好起点。

    强烈推荐郭神关于内存泄露分析的文章:

    Android最佳性能实践(二)——分析内存的使用情况

    其他优化建议

    • 造成OutOfMemoryError的原因通常是Bitmap或者对象进行了太多的内存分配

      加载图片的时候尽可能不要加载原尺寸的大图,可以使用缩略图

      回收已经不使用的Bitmap资源bitmap.recycle().

      2.3(包括)之前,Bitmap的引用是放在堆中的,而Bitmap的数据部分是放在栈中的,需要用户调用recycle方法手动进行内存回收 ,2.3之后,整个Bitmap,包括数据和引用,都放在了堆中,这样,整个Bitmap的回收就全部交给GC了,这个recycle方法就再也不需要使用了。

      列表中加载图片注意使用小图,以及做好缓存工作

      尽可能的避免碎片化

      减少Java在应用堆空间堆快满的时候再堆分配

      缓存中使用SoftReference

      使用WeakReference避免堆存泄露

    //缩放图片
    BitmapFactory.Options opts = new BitmapFactory.Options();
    opts.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(path, opts);
    final int originalWidth = opts.outWidth;
    final int originalHeight = opts.outHeight;
    final int originalDim = Math.max(originalWidth, originalHeight);
    opts = new BitmapFactory.Options();
    opts.inSampleSize = 1;
    while ( originalDim > MAX_IMAGE_DIM ) {
      opts.inSampleSize *= 2;
      originalDim /= 2;
    }
    return BitmapFactory.decodeFile(path, opts);
    
    //对比之间的例子,这里改成了静态内部类
    public class MainActivity extends Activity {
      static Leaky leak = null;
      static class Leaky {
        void doSomething() {
            //doing
        }
      }
      @Override
      public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (leak == null) {
          leak = new Leaky();
        }
      }
    }
    
    public class MainActivity extends Activity {
    static Leaky leak = null;
    static class Leaky {
    private final Context mContext; //final修饰
      public Leaky(Context context) {
        super();
        mContext = context;
        doSomethingWithOuterInstance();
      }
      void doSomethingWithOuterInstance() {
        String text = mContext.getString(R.string.hello);
        System.out.println(text);
      }
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
      if (leak == null) {
          leak = new Leaky(this);
      }
    }
    }
    
    public class MainActivity extends Activity {
    static Leaky leak = null;
    static class Leaky {
    private final WeakReference<Context> mContext;//使用了弱引用
    public Leaky(Context context) {
      super();
      mContext = new WeakReference<Context>(context);
      doSomethingWithOuterInstance();
    }
    void doSomethingWithOuterInstance() {
      Context context = mContext.get();
      if (context != null) {
        String text = context.getString(R.string.hello);
        System.out.println(text);
      }
      }
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      if (leak == null) {
        leak = new Leaky(this);
      }
    }
    }
    

    更多优化建议,请移步郭神博客

    Android最佳性能实践

    相关文章

      网友评论

      • shinjiko:时间量级似乎错了诶
        dooze:@shinjiko 我重新在真机上做了测试,单纯的写一个字节的话会大概在0.0x ~5ms,虚拟会稍差一点。还有就是剩余内存小的时候也相对慢一些。很感谢指正!
        shinjiko: @dooze 从闪存读取字节的时间从第一感上不太对
        dooze:@shinjiko 首先很感谢!但是不知道你是指哪里的时间量级?

      本文标题:Android 性能优化总结

      本文链接:https://www.haomeiwen.com/subject/vccruttx.html