Android开发中优化分析及总结笔记

一、奔溃的原因及优化

       1、Android的奔溃分为Java奔溃和Native奔溃。

            Java奔溃就是在Java代码中,出现了未捕获异常,导致程序异常退出。Native奔溃是因为Native代码中访问非法地址,也可能是地址对齐出现了问题,或者发生了程序主动abort,这些都会产生响应的signal信号,导致程序异常退出。

       2、Native奔溃的捕获流程:

            编译端,编译C/C++代码时,需要将带符号信息的文件保留下来;客户端,捕获到奔溃时,将收集到尽可能多的有用信息写入日志文件,然后选择合适的时机上传服务器;服务端,读取客户端上报的日志文件,寻找适合的符号文件,生成可读的C/C++调用栈。Breakpad是一个跨平台的开源项目,可以集成用来捕获Native奔溃。

       3、选择奔溃服务:

            奔溃的服务系统有腾讯的Bugly、阿里巴巴的啄木鸟平台等。

二、内存优化:

   内存分配:

       静态:内存在程序编译的时候已经分配好了,这块内存在程序运行期间一直存在;主要放静态数据,全局static数据和一些常亮。

       栈:在执行函数或方法时,函数内部变量存储都放在栈中,函数存储单元自动释放;栈运行速度快,但数量有限。

       堆:也叫动态内存分配,是通过对象new出来的对象实例。

       区别:堆是不连续的区域,空间大;栈是一块连续的内存区域,大小由操作系统决定,队列的实现方式,先进后出。

       使用:成员变量全部都存在堆中(包括基本类型、引用及引用的对象实体)——类的对象最终是被new出来的;局部变量数据类型和引用存储在栈中,引用的实体对象在堆中——它们属于方法的变量。

  1、内存泄露说明:

        内存泄露简单说就是对象由于编码错误或者系统原因,仍然存在着对其直接或间接的引用,导致系统无法进行回收。内存泄露容易留下逻辑隐患,并增加了应用内存峰值与发生OOM的概率。

 2、造成的常见原因:

     静态对象、this$0、系统、监听器、线程、Textline、广播、定时器、输入法、Web View、Handler、音频等对象被持有导致无法释放或不能按照对象正常的生命周期进行释放。

3、内存泄露的监控方案:

   Square的开源库leakcanry通过弱引用方式侦查Activity或对象的生命周期,若发现内存泄露自动dump Hprof文件,最终能展现出来内存泄露发生的具体位置。

4、兜底回收内存:

   Activity泄露会导致改Activity应用到的Bitmap、DrawingCache等无法释放,对内存造成大的压力,兜底回收是指已泄露Acivity,尝试回收持有的资源,泄露的仅仅是一个Activity空壳,从而降低对内存的压力。做法是在Activity onDestory时候从view的rootview开始,递归释放所有子view涉及的图片,背景,DrawingCache,监听器等等资源,让Acivity成为一个不占资源的空壳,泄露了也不会导致图片资源被持有。

   …
   …
   Drawable d = iv.getDrawable();
   if (d != null) {
       d.setCallback(null);
   }        
   iv.setImageDrawable(null);
   ...
   ...

降低运行时内存的一些方法:

 1、减少bitmap占用的内存:

     1)、防止bitmap占用资源多大导致OOM

     2)、图片按需加载:图片的大小不应超过view的大小。在把图片载入内存之前,先计算出一个合适的inSampleSize缩放比例,避免不必要的大图载入。对此,可重载drawable与ImageView,例如在Acivity ondestroy时,检测图片大小与View的代销,若超过,可以上报或提示。

    3)、统一的bitmap加载器:Picasso、Fresco、ImageLoader加载库有了统一的bitmap加载器,我们可以在加载bitmap时,若发生OOM(try catch方式),可以通过清除cache,降低bitmap format(ARGB8888/RBG565/ARGB4444/ALPHA8)等方式,重新尝试。

   4)、图片存在像素浪费:对于.9图,美工可能在出图时在拉伸与非拉伸区域都有大量的像素重复。通过获取图片的像素ARGB值,计算连续相同的像素区域,自定义算法判定这些区域是否可以缩放。关键也是需要将这些工作做到系统化,可及时发现问题,解决问题。

  2、使用多进程:

     对于webview,图库等;由于存在内存系统泄露或者占用内存过多的问题,我们可以采用单独的进程。

  3、上报OOM详细信息:当系统发生OOM的crash时,我们应当上传更加详细的内存相关信息,方便我们定位当时内存的具体情况。其他例如使用large heap、inBitmap、SparseArray、Protobuf等不再一一细述,对代码采用优化--埋坑--优化--埋坑的方式并不推荐。我们应该着力于建立一套合理的框架与监控体系,能及时的发现诸如bitmap过大、像素浪费、内存占用过大、应用OOM等问题。

 

三、卡顿优化:

   卡顿排查工具:

       traceview性能分析工具,它利用Android Runtime函数调用的event事件,讲函数运行的耗时和调用关系写入trace中。

       systrace是Android4.1新增的心梗分析工具。通常使用systrace跟踪系统的I/O操作、CPU负载、Surface渲染、GC等事件。

   卡顿处理:

       过于复杂的布局:界面性能取决于UI渲染性能,CPU负责UI布局元素的Measure,Layout、Draw等相关运算执行。可以借助Hierarchy Viewer工具帮助我们分析布局,Hierarchy Viewer可以以图形化树状结构形式展示出UI层级,还对每个节点给出了三个小圆点,以指示改元素Measure,Layout、Draw的耗时及性能。

       过度绘制(Overdraw):用来描述一个像素在屏幕上多少次被重绘在一帧上。Android系统提供了可视化的方案让我们很方便查看overdraw的现象:在“系统设置”——>“开发者选项”——>“调试GPU过度绘制”中开启调试。

       UI线程的复杂运算:UI线程的复杂运算会造成UI无响应,使用traceview工具分析。

       频繁的GC:在执行GC操作的时候,任何线程的操作都需要暂停,等待GC操作完成之后,其他操作才能继续运行,故而频繁GC会导致界面卡顿。频繁GC的原因:一、内存抖动:大量的对象被创建又在短时间内马上被释放;瞬间产生大量的对象会严重占据内存区域,从而触发更多的GC。解决方案:一般瞬间大量产生对象是因为在代码循环中new对象,或是在Ondraw方法中创建对象等;尽量不要在循环中大量的使用局部变量。

 

四、启动优化:

        1、启动的过程分析:点击响应应用解释——>预览窗口显示——>Application创建——>闪屏Activity创建界面准备——>闪屏显示——>主页显示——>其他工作——>窗口可操作。

        2、启动问题分析:

            问题1、点击图标很久都不响应;问题2、首页显示过慢;问题3、首页显示后无法操作。

        3、启动过程避免进行大量的字符串操作,特别是序列化跟反序列化过程,一些频繁创建的对象,例如网络库和图片库的Byte数组、Buffer可以复用。如一些模块实在需要频繁创建对象,可以考虑移到Native实现。

    

五、I/O优化:

         1、Linux I/O的概念:文件I/O操作由应用程序、文件系统和磁盘共同完成。首先应用程序将I/O命令发送给文件系统,然后文件系统会在合适的时机把I/O操作发给磁盘。

            文件系统I/O:应用程序调用read()方法,系统会通过中断从用户空间进入内核处理,然后经过VFS(Virtual File System,虚拟文件系统)、具体文件系统、页缓存Page Cache 。

            磁盘:是指系统的存储设备,应用程序要read()的数据没有在页缓存中,这时候就需要真正向磁盘发起I/O请求。这份过程要先经过内核的通用块层、I/O调度层、设备驱动层,最后才会交给具体的硬件设备处理。

        2、Android I/O:手机使用的存储设备,用闪存作为存储设备,也就是我们常说的ROM。

        3、I/O的三种方式

             标准I/O:应用程序平时用到read/write操作都属于标准I/O,也就是缓存I/O(Buffered I/O);特性是对于读操作——当应用程序读取某块数据时,如存在页缓存中则立即返回给应用程序,不需实际的物理读盘操作;对于写操作——数据先写到页缓存中,数据是否立即写到磁盘取决于写操作的机制。 

             直接I/O:访问文件方式减少了一次数据拷贝和一些系统调用的耗时,很大程度降低了CPU的使用率及内存的占用。

             mmap:Android系统启动加载DEX时,不会把整个文件一次性读到内存中,而是采用mmap的方式。通过把文件映射到进程的地址空间、最终映射的物理内存依然在页缓存中;带来的好处,减少系统调用,一次mmap()系统调用后所有的调用会像操作内存一样不会出现戴亮的read/write系统调用;减少数据拷贝,mmap只需从磁盘拷贝一次就可以;可靠性高。

         4、多线程阻塞I/O和非阻塞NIO

               多线程阻塞I/O,读写收到I/O性能瓶颈的影响,在到达一定速度后整体性能就会收到明显的影响,过多的线程反而会导致应用整体性能的明显下降。实际开发中大部分都是读一些比较小的文件,使用单独的I/O线程还是专门新开一个线程,其实差别不大。 

               非阻塞的NIO是以事件的方式通知,的确可以减少线程切换的开销。Chrome网络库是一个使用NIO提升性能很好的例子,特别在系统非常繁忙时。但是NIO的缺点也非常明显,应用程序的实现会变得更复杂,有的时候异步改造并不容易。使用NIO的最大作用不是减少读取文件的耗时,而是最大化提升应用整体的CPU利用率。

         5、监控线上的I/O操作:分为有Java Hook和Native Hook

            Hook的四个接口可以采集到的信息:open——文件名、fd、文件原始大小、堆栈、线程;read、write——类型、读写次数、读写总大小、使用buffer大小、读写总耗时;close——打开问价总耗时、最大的连续读写时间。

 

六、存储优化:

       1、Android的存储基础:

            Android的分区;分区一般来说就是讲设备的存储划分为一些互不重叠的部分,每个部分都可以单独格式化,用作不同的目的。这样系统就可以灵活的针对单独分区做到不同的操作。

           /system分区:存放所有Google提供的Android组件的地方,以只读方式mount。

           /data分区:存放用户数据的地方。

           /vendor分区:存放厂商特殊系统修改的地方。

           /cache分区:系统升级过程使用的分区或recovery。

          /storge分区:外置或内置sdcard。

      2、Android存储安全:

          权限控制:Android的每一个应用都在自己的应用沙盒内运行,沙盒使用了标准Linux的保护机制,通过为每个应用创建独一无二的Linux UID来定义。

          数据加密:Android的有两种设备加密方式——全盘加密和文件级加密。

     3、常见的数据存储方法:

          存储就是把特定的数据结构转化成可以被记录和还原的格式,这个数据格式可以是二进制、XML、JSON、protocol Buffer等。

         SharedPreferences存储方式:

              跨进程不安全——由于没有使用跨进程的锁,SharedPreferences在跨进程频繁读写有可能导致数据全部丢失。

              加载缓慢——SharedPreferences文件的加载使用异步线程,加载线程并没有甚至线程优先级,如主线程读取数据就需要文件加载线程的结束。

              全量写入——无论是调用commit()还是apply(),即使值改动其中的一个条目,都会把整个内容全部写入到文件。

              卡顿——由于提供了异步落盘的apply机制,在奔溃或其他异常情况可能会导致数据丢失。

            系统提供SharedPreferences的应用场景是用来存储一些非常简单、轻量的数据。我们不要使用它来存储过于复杂的数据。

       ContentProvider存储方式;

              ContentProvider的生命周期默认在Application onCreat()之前,而且都是在主线程创建的,在自定义ContentProvider类的构造函数、静态代码块、onCreat函数都尽量不要做耗时的操作,会拖慢启动速度。

              ContentProvider在进行跨进程数据传递时,利用了Android的Binder和匿名共享内存机制。简单说就是通过Binder传递CursorWindow对象内部的匿名共享内存的文件描述符。这样在跨进程传输中,结果数据并不需要跨进程传输,而是在不同进程中传递匿名内存文件描述符来操作同一块匿名内存,这样来实现不同进程访问相同数据的目的。

      对象的序列化:

              应用程序中的对象存储在内存中,如想把对象存储下来或网络传输,这个时候就需要用到对象的序列化和反序列化,对象序列化就是把一个Object对象所有信息表示成一个字节序列,这个包括Class信息、继承关系信息、访问权限、变量类型以及数值信息等。

              Serialzable序列化;

                  是Java原生的序列化机制,可通过Serializable将对象持久化存储,也可通过Bundle传递Serializable的序列化数据。

                 Serializable的原理是通过ObjectInputStream和ObjectOutStream来实现的。从源码上看,整个序列化过程使用了大量的反射和临时变量,而且在序列化对象时,不仅会序列化当前对象本身,还需要递归序列化对象应用的其他对象。

                WriteObject和readObject方法。Serializable序列化支持替代默认流程,它会先反射判断是否存在自己实现的序列化方法WriteObject或反序列化readObject。通过这两个方法,我们可以对某些字段做一些特殊修改,也可以实现序列化的加密功能。

                writeReplace和readResolve方法。这两个方法代理序列化的对象,可以实现自定义返回的序列化实例。     

                Serializable不被序列化的字段,类的static变量以及被声明为transient的字段,默认的序列化机制都会忽略的字段,不会进行序列化存储;也可以使用writeReplace和readResolve方法做自定义的序列化存储。

                 serialVersionUID,在类实现了Serializable接口后,需添加一个Serial Version ID,类似与类的版本号。

                 构造方法,Serializable的反序列默认是不会执行构造函数的,它是根据数据流中对Object的描述信息创建对象的。

              

            Parcelable序列化:

                 Parcelable的序列化只会在内存中进行操作,不会将数据存储到磁盘里。

                 Parcelable的写入和读取的时候都需要手动添加自定义代码,使用起来相比Serializable会复杂;Parcelable不需要采用反射的方式去实现序列化和反序列化。

        

        数据的序列化;

              JSON数据:

                     JSON是一种轻量级的数据交互格式,它被广泛使用于网络传输中,很多应用的服务端的通信都是使用JSON格式进行交互。

                     JSON的优势:相比对象序列化方案,速度更快,体积更小‘相比二进制的序列化方案,结构可读,易于排查问题;使用方便,支持跨平台、跨语言。支持嵌套引用。

              Protocol Buffers:

                      数据量大Protocol Buffer是一个好选择。

     4、数据库SQLite的使用及优化:

              SQLiteDatabaseLockedException的异常的原因是并发导致的。

              多进程并发:多进程可以同时获取SHARED锁来读取数据,但是只有一个进程可以获取EXCLUSIVE锁来写数据库。EXCLUSIVE模式下,数据库连接在断开前都不会释放SQLite文件的锁,从而避免不必要的冲突。提高数据库访问的速度。

              多线程并发:SQLite支持多线程并发模式,需要开启多线程配置。系统SQLite会默认的开启多线程Multi-thread模式,SQLite所的粒度都是数据库文件级别,并没有实现表级甚至级的锁;同一个句柄一时间只有一个线程在操作,这时需要打开连接池Connection Pool。

              在写之间是不能并发的,如出现多个并情况,依然可能会出现SQLiteDatabaseLockedException。这时可以让应用中捕获这个异常,然后等待一段时间再重试。

             查询优化:

               索引优化:建立索引是有代价的,需要一直维护索引表的更新;比如对于一个很小的表来说就是没必要建立索引;如一个表经常是执行插入更新操作,那么也需要节制的建立索引。索引优化是SQLite优化中最简单同时也是最有效的,但是它并不是简单的建一个索引就可以了,有时需要进一步调整查询语句甚至是表的结构,这样才能达到最好的效果。

            页大小与缓存大小:

            其他优化:通过引进ORM,可以大大的提升开发效率。通过WAL模式和连接池,可以提高SQLite的并发性能。通过正确的建立索引,可以提升SQLite的查询速度。通过调整默认页大小和缓存大小,可以提升SQLite的整体性能。

            SQLite的监控:

                    本地测试——SQL语句都应该先在本地测试,通过EXPLAIN QUERY PLAN测试SQL语句的查询计划,是全表扫描还是使用了索引,以及具体使用了哪个索引。

                    耗时监控——

                    智能监控——

             

五、网络优化:

           网络性能评估:延迟——数据从信心源发送到目的地所需的时间;带宽——逻辑或物理通信路径最大的吞吐量。

           网络数据包的发送过程:数据包从手机出发要经过无线网络、核心网络以及外部网络,才能到达我们的服务器。

           网络优化的核心内容

                  速度:在网络正常或者良好的时候,这样更好地利用宽带。进一步提升网络请求速度;

                  弱网络:移动端网络复杂多变,再出现网络连接不稳定时,怎样最大保证网络的连通性;

                  安全:网络安全不容忽视。怎样有效防止被第三方劫持、窃听甚至篡改;

           网络请求的整个过程

                  DNS解析:通过DNS服务器,拿到对应域名的IP地址,有DNS解析耗时情况、运营商LocalDNS的劫持、DNS调度;

                  创建连接:与服务器简历连接,包括TCP三次握手、TLS秘钥协商。有多个IP端口该如何选择、是否要使用HTTPS、能否减少省下创建连接的时间;

                  发送/接收数据:在成功建立连接之后,可以跟服务器交互,进行组装数据、发送数据、接收数据、解析数据。关注问题是,如何根据网络状况将宽带利用好、怎样快速地侦测到网络延时、在弱网络下如何调整包大小;

                  关闭连接:关注主动关闭和被动关闭两种情况,一般希望客户端可以主动关闭连接。

           

六、UI优化:

        1、屏幕与适配:

             px:像素点;

             ppi:像素密度,没英寸所包含的像素数目。这是屏幕物理参数;

             dpi:像素密度,在系统软件上指定的单位尺寸的像素数量。与ppi不同的是,dpi可能会被人为的调整;

             dp:基于屏幕物理分辨率一个抽象的单位,用月说明与密度无关的尺寸和位置;

             density:密度,屏幕上每平方英寸中含有的像素点数量。

通过dp加上自适应布局可以基本解决屏幕的碎片化的问题,也是Android推荐使用的屏幕兼容适配方案。适配方案的博客有——《Android目前稳定高效的UI适配方案》、《smallestWidth限定符适配方案

       2、UI优化的常用手段:

            尽量使用硬件加速:硬件加速绘制的性能是远远高于软件加速的,所以UI优化的第一手段就是保证渲染尽量使用硬件加速。有些情况不能使用硬件加速,是因为硬件加速不能支持所有的Canvas API,具体API兼容列表可以见drawing-support文档。如使用了不支持的API,系统需要通过CPU软件模拟绘制,这也是渐变、磨砂、圆角等小狗渲染性能比较低的原因。

             Creat View优化

                  View的创建是在UI线程中,创建时会包括各种XML的随机读的I/O时间、解析XML的时间、生成对象那个的时间(Framework会大量使用到反射)。

                 使用代码创建:使用XML进行编写可以Android Studio中实时预览界面,如果对一个界面进行极致优化,就可以使用代码进行编写界面。

                 异步创建:在线程提前创建View,实现UI预览。如这样会抛出异常。解决方法是在使用线程创建UI的时候,先把线程Looper的MessagaQueue替换成UI线程Looper的Queue。在创建万View后需要吧线程的Looper恢复成原来的。

                 View重用:正常来说,View会随着Activity的销毁而同时销毁。ListView、RecycleView通过View的缓存与重用大大地提升渲染性能。因此可以参考这些思想,实现一套可以在不同Activity或者Fragment使用的View缓存机制。

                 measure/layout优化:减少UI布局层次,尽量扁平化,使用<ViewStub><Merge>等优化;优化layout的开销,尽量不适用RelativeLayout或者几月weighted LinearLayout,这两者layout的开销非常巨大。推荐使用ConstrainLayout替代。

                 背景优化:尽量不要重复去设置背景,主题背景(theme),theme默认会是一个纯色背景,如自定义界面背景,主题背景就是无用的。主题背景是设置在DecorView中,所以会带来重复绘制。2018发布的PrecomputeredText集成在jetpack中,可以异步进行measure和layout,不必在主线程中执行。

              UI优化的进阶手段

                   Litho异步布局

                       是Facebook开源的声明式Android UI渲染框架,一般来说Android所有的控件绘制都要遵守measure->layout->draw的流水线并且都发生在主线程中。和PrecomputeredTex一样,把measure和layout都放到了后台线程,只留下了必须要在主线程完成的draw,这样可以大大降低UI线程的负载;

               Litho还优化了RecyclerView中UI组件的缓存和回收方法。原生的RecyclerView或ListView是按照viewType来进行缓存和回收,但RecyclerView中出现viewType过多,会使缓存形同虚设,但Litho是按照text、image和video独立回收的,这可以提高缓存命中率、降低内存使用率、提高滚动帧率。

                 Flutter自己的布局+渲染引擎

                      Flutter是Google推出并开源的移动应用开发框架,开发着可以通多Dart语言开发APP,一套代码可以同时在IOS和Android平台。 在Android上Flutter完全没有基于系统的渲染引擎,而是吧Skia引擎直接集成进了APP中,这使得Flutter APP就像一个游戏App。并且直接使用了Dart虚拟机,可以说是一套跳脱出Android的方案,所以Flutter也可以很容易实现跨平台。 

                 RenderThread与RenderScript

                       在Android5.0,系统增加了RenderThread,对于ViewPropertyAnimator和CircularReveal动画,我们可以使用RenderThread实现动画的异步渲染。但主线程阻塞的时候,普通动画会出现明显的丢帧卡顿,而是使用RenderThread渲染的动画即使阻塞了主线程仍然不受影响。

                      图片的变换涉及大量的计算任务,可以通过RenderScript,他是Android操作系统上的一套API。基于异构计算思想,专门用于密集计算。

                 

七、包体积的优化

          安装包的文件结构

             res/:存放编译后的资源文件,如Drawable、Layout等;

             assets/:应用程序的资源,应用程序可以使用AssetManager来检索该资源;

             META-INF/:该文件夹一般存放于已经签名的apk中,包含了apk中所有文件的签名摘要等信息;

             resources.arsc:编译后的二进制资源文件;

             AndroidManifest.xml:Android的清淡文件,用于面熟应用程序员的名称、版本、所需权限、注册的四大组件。

  

            

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章