android中性能优化需要了解的

在android开发中,每次写下一块代码的时候,性能问题是必须要着重考虑的。因为每次迭代开发中不注意,想要留着在后期进行优化,那么问题逐渐变得难以控制。所以我们需要对性能这方面进行时刻的自我提醒。那么下面就从一些常见的地方说起。

一、性能优化有哪些

  1. 绘制优化
  2. 内存优化
  3. 电量优化
  4. 启动优化
  5. IO优化
  6. 流量优化
  7. 图片优化
  8. apk优化

二、IO优化

文件
  1. 使用缓冲流读写,避免使用字节流读写
shareperferrence
  1. 轻量级的数据存储方式,能够保存简单的数据类型,比如 String、int、boolean 值等。应用场合主要是数据比较少的配置信息。其内部是以 XML 结构保存在 /data/data/包名/shared_prefs 文件夹下,数据以键值对的形式保存。操作模式有三种:MODE_PRIVATE(私有)、MODE_WORLD_READABLE(可读)、MODE_WORLD_WRITEABLE(可写)。

  2. 性能差的原因

    • io瓶颈
      • SP文件没有被加载到内存时,调用getSharedPreferences方法会初始化文件并读入内存。
      • 版本低于android_H或使用了MULTI_PROCESS标志时,每次调用getSharedPreferences方法时都会读入。
      • 多进程读写sp,推荐使用ContentProvider等其他方式,效率更高,避免sp数据不一致情况
      • sp在创建的时候会把整个文件全部加载进内存,如果你的sp文件比较大,那么会带来几个严重问题:①第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿、掉帧。②解析sp的时候会产生大量的临时对象,导致频繁GC,引起界面卡顿。③这些key和value会永远存在于内存之中,占用大量内存。
    • 数据写入磁盘也有两个场景会触发:
      • Editor的commit方法,每次执行时同步写入磁盘
      • Editor的apply方法,每次执行时在单线程池中加入写入磁盘Task,异步写入
      • commit和apply的方法区别在于同步写入和异步写入,以及是否需要返回值。
        在不需要返回值的情况下,使用apply方法可以极大的提高性能。
        同时,多个写入操作可以合并为一个commit/apply,将多个写入操作合并后也能提高IO性能。
    • 锁性能差
      • SP的get操作,会锁定SharedPreferences对象,互斥其他操作。
      • SP的put操作,getEditor及commitToMemory会锁定SharedPreferences对象,put操作会锁定Editor对象,写入磁盘更会锁定一个写入锁。
      • 由于锁的缘故,SP操作并发时,耗时会徒增。减少锁耗时,是另一个优化点。推荐操作:降低单文件访问频率,多文件均摊访问,以减少锁耗时
    • 对sp的不良封装也会导致读写性能差
bitmap
  1. 4.4 以上 decodeFile 内部没有使用缓存,效率不高。要使用 decodeStream,同时传入的文件流为 BufferedInputStream。
    decodeResource 同样存在性能问题,用 decodeResourceStream。
  2. 根据控件大小进行采样压缩
sqlite
  1. 基于sqlite的读写都是读写原始的磁盘文件,也就是说sqlite的操作完全是磁盘的IO操作。SQLite操作存在一定的异常情况,在客户端也难以监控,通常的异常情况都包含在以下情况:

    • 文件错写
    • 文件锁 :系统文件锁问题SQLite依赖于底层的文件系统对文件锁的实现。SQLite 默认锁是协同锁。假设有A、B线程同时访问数据库并写入数据。这个时候来个C线程不是使用SQLite API的方式来操作数据的话(如直接的IO操作),那么数据库锁就会被自动取消,A、B线程在没有锁保护的状态同时操作db就可能导致DB的损坏。建议的操作是:
      • 尽量使用SQLite API来操作数据库的读写。
      • 如果有IO线程直接操作数据库,代码逻辑上保证线程间的同步顺序。
    • 文件 sync 失败:批量写入比较大的数据我们可能想要更快的速度,网上很多会建议你设置PRAGMA synchronous=OFF,Android对应的设置方式就是:mDatabase.execSQL(“PRAGMA synchronous=OFF”);等同于在说:我期待更快的写入速度,磁盘驱动器为了达到目标,就耍了个小聪明,忽略开始SQLite的同步操作,先通知到系统我写入完了。实际上不一定数据完全写入了,假如这个时候出现了断电等意外,就会导致真实数据写入失败从而引发DB损坏。推荐的操作:
      • 尽量不要设置PRAGMA synchronous=OFF
      • 数据比较大的写入等操作从sql语句优化和性能的优化入手。
    • 设备损坏
    • 内存覆盖
    • 操作系统 bug
    • SQLite bug
  2. SQLiteDatabaseLockedException:DB引擎在执行job的时候发现获取不到数据库的锁就会抛出这个异常。数据库锁用于防止多线程同时写入数据从而导致DB损坏。所以一般发生在多线程的事务操作上。针对同一个事务代码段,如果一个事务尚未结束提交,另外一个线程便开始介入执行就会导致冲突抛出此异常。建议的操作是:

    • 只使用一个SQLOpenHelper来访问和操作database,单例你的SQLOpenHelper,避免多个OpenHelper多次open db并写入数据。
    • 当不再需要继续操作db的时候,关闭所有database helper实例
    • 做事务操作的时候一定要保证事务完成调用了endTransaction() 或者transaction Successful相关方法。下一个事务操作会在entransaction结束后请求锁。
  3. 多db操作:

    • 文件覆盖:DatabaseB的DBOpenHelperB正在读写DatabaseA的表,同时DatabaseA正在被IO线程拷贝写入文件覆盖。DBOpenHelperB建立连接可能依然是旧的数据库,读写自然也是到旧的数据库。部分文件系统甚至引发IO拷贝被中断。
    • 交叉读写: 一般有多个DB会对应多个DBOpenHelper进行读写,如过DBOpenHelper做了跨DB的读写,就会导致重复的DB Open或Close,影响到了其他DBOpenHelper的正常读写。直接的导致锁异常或者DB损坏等问题。
    • 推荐操作: 1. 非SQLite API线程IO数据库请保持IO完全结束后再建立与数据库的读写连接或读写操作; 2.DBHelper避免跨DB的操作,一个DB对应一个DBHelper实例
  4. sql的优化

    • AUTOINCREMENT可以保证主键的严格递增,但使用AUTOINCREMENT会增加额外的开销,INSERT数据时耗时 1倍以上,所以非必需时尽量不要用。

三、网络优化

一个网络请求可以简单分为连接服务器 -> 获取数据两个部分。
其中连接服务器前还包括 DNS 解析的过程;获取数据后可能会对数据进行缓存。

连接服务器优化策略
  1. 不用域名,用 IP 直连。 省去 DNS 解析过程,首次域名解析一般需要几百毫秒,可通过直接向 IP 而非域名请求,节省掉这部分时间,同时可以预防域名劫持等带来的风险。当然为了安全和扩展考虑,这个 IP 可能是一个动态更新的 IP 列表,并在 IP 不可用情况下通过域名访问。
  2. 服务器合理部署
    • 服务器多运营商多地部署,一般至少含三大运营商、南中北三地部署。
    • 动态 IP 列表,支持优先级,每次根据地域、网络类型等选择最优的服务器 IP 进行连接
    • 对于服务器端还可以调优服务器的 TCP 拥塞窗口大小、重传超时时间(RTO)、最大传输单元(MTU)等。
获取数据优化策略
  1. 连接复用
    • 节省连接建立时间,如开启 keep-alive。
  2. 请求合并
    • 即将多个请求合并为一个进行请求,比较常见的就是网页中的 CSS Image Script。 如果某个页面内请求过多,也可以考虑做一定的请求合并。
  3. 减少请求大小
    • 对于 POST 请求,Body 可以做 Gzip 压缩,如日志。
    • 对请求头进行压缩:这个 Http 1.1 不支持,SPDY 及 Http 2.0 支持。 Http 1.1 可以通过服务端对前一个请求的请求头进行缓存,后面相同请求头用 md5 之类的 id 来表示即可。
  4. CDN缓存静态资源
    • 缓存常见的图片、JS、CSS 等静态资源。
  5. 减少返回数据的大小
    • 压缩:一般 API 数据使用 Gzip 压缩
    • 精简数据格式
    • 对于不同的设备不同网络返回不同的内容 如不同分辨率图片大小。
    • 增量更新;需要数据更新时,可考虑增量更新。如常见的服务端进行 bsdiff,客户端进行 bspatch。
    • 大文件下载: 支持断点续传,并缓存 Http Resonse 的 ETag 标识,下次请求时带上,从而确定是否数据改变过,未改变则直接返回 304。
    • 数据缓存: 缓存获取到的数据,在一定的有效时间内再次请求可以直接从缓存读取数据。
其他优化手段
  1. 预取:预取数据,预链接
  2. 分优先级,延迟部分请求
  3. 多连接:对于大文件,大图片可以考虑多连接。 注意控制请求的最大并发量
  4. 监控:优化需要通过数据对比才能看出效果,所以监控系统必不可少,通过前后端的数据监控确定调优效果。

四、内存优化

1. java内存相关;
内存区域划分;
  1. 堆 :
    • java中运行环境用来分配给对象和JRE类的内存都在堆内存,C、C++有时候malloc或者new来申请分配内存也在这里;
    • 不连续内存区域,容易造成内存碎片,不易管理
    • 所有线程共享的内存区域
  2. 栈 :
    • 相对于线程而言,执行函数时,内部变量以及对堆中对象的引用都可以在栈中,函数执行结束后就会释放这些
    • 一块连续的内存区域,先进后出,不会产生碎片,运行效率高而稳定
    • 本地方法栈和虚拟机栈,虚拟机栈为执行java代码方法服务;本地方法栈为执行native方法服务。同样都会抛出StackOverflow和OutOfMemory
    • 虚拟机栈:java方法执行的内存模型,和线程有相同的生命周期,线程私有;其中的一些关键词汇: 栈帧,局部变量表,操作数栈,指向当前方法所属的类的运行时常量池的引用,方法返回地址和一些额外的附加信息
  3. 方法区 :
    • 存放类信息,常量,静态变量等;
    • 因为反射机制的原因,虚拟机很难推测哪个类信息不再使用,因此这块区域的回收很难。对这块区域主要是针对常量池回收
    • 所有线程共享的区域
      -当方法区无法满足内存需求时会抛出 OutOfMemoryError 异常
  4. 运行时常量池 :
    • 是方法区的一部分,用于存放编译期生成的各种字面量和符号。
    • 避免了频繁的创建和销毁对象而影响系统性能,实现了对象的共享,节省了内存空间和运行时间。
    • 值得说明的是,运行时常量池具备动态特性,常量不止在编译期可以产生,在运行期间也可以产生,比如String的intern方法。
  5. 程序计数器
    • 当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成
2. 作为gc root常见的有:
官方解释:JVM中GC Roots的大致分类
1. Class 由System Class Loader/Boot Class Loader加载的类对象,这些对象不会被回收。需要注意的是其它的Class Loader实例加载的类对象不一定是GC root,除非这个类对象恰好是其它形式的GC root;

2. Thread 线程,激活状态的线程;

3. Stack Local  栈中的对象。每个线程都会分配一个栈,栈中的局部变量或者参数都是GC root,因为它们的引用随时可能被用到;

4. JNI Local JNI中的局部变量和参数引用的对象;可能在JNI中定义的,也可能在虚拟机中定义

5. JNI Global JNI中的全局变量引用的对象;同上

6. Monitor Used 用于保证同步的对象,例如wait(),notify()中使用的对象、锁等。

Held by JVM JVM持有的对象。JVM为了特殊用途保留的对象,它与JVM的具体实现有关。比如有System Class Loader, 一些Exceptions对象,和一些其它的Class Loader。对于这些类,JVM也没有过多的信息

1. 虚拟机栈中引用的对象
2. 方法区中类的静态属性引用的对象
3. 方法区中常量引用的对象
4. 本地方法栈中JNI(即一般说的Native方法)引用的对象
总结就是,方法运行时,方法中引用的对象;类的静态变量引用的对象;类中常量引用的对象;Native方法中引用的对象
3. java中的引用划分;
  1. 强引用:
    • new出来的对象;
    • Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题
  2. 软引用:
    • 如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存 空间不足了,就会回收这些对象的内存
    • 软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联 的引用队列中。
  3. 弱引用:
    • 如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更 短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
    • 由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象
    • 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联 的引用队列中
  4. 虚引用:
    • 虚引用并不会决定对象的生命周期。如果一个对象 仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
    • 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队 列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
4. 什么是内存泄漏? 常见的泄漏场景有哪些?
  1. 内存泄漏:在安卓中内存泄露常常出现的情况是指组件生命周期已经结束,但是其引用被其他更长声明周期的对象持有,得不到释放引起的。
  2. 泄漏场景:
    • 内部类,java中非静态内部类持有外部类的引用
    • 静态变量,更长声明周期的静态变量引用短周期的对象。android开发中应该尤其注意context的引用链;保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期
    • 耗时类泄漏,比如在短生命周期中创建了更长生命周期的耗时操作对象,在未结束之前,使得一直保持对短生命周期对象的引用;解决方式:①使用弱引用;②使用解耦的方式,使得长短生命周期的对象,可以在生命周期结束时及时释放
    • Handler引起,尽量使用静态handler或者单独继承handler的类文件,然后持有上下文的弱引用;使用post系列方法是,runnable也尽量使用静态;在页面销毁的时候,需要removeCallbacks取消所有的消息;
    • 单例,由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用
    • 属性动画,及时cancel掉
    • 资源未关闭造成:对于使用了BraodcastReceiver,ContentObserver,File, Cursor,Stream,Bitmap, EventBus等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
    • 多线程相关的匿名内部类/非静态内部类(Thread,Runnable,AsyncTask等)

五、UI优化

产生卡顿的原因
  1. 布局layout过于复杂,层级过深,无法在16ms内完成渲染
  2. 同一时间动画执行的次数过多,导致cpu或gpu负载过重
  3. View过度绘制,导致某些像素在同一时间内被绘制多次
  4. 在UI线程做了耗时操作,导致在系统接收到渲染vsync信号的时候,不能及时处理,出现掉帧
  5. GC回收时暂停时间过长,或者频繁的gc产生大量的暂停时间
抽象布局标签:去除不必要的嵌套和View节点、减少不必要的infalte
  1. include:
    • include标签常用于将布局中的公共部分提取出来供其他layout共用,以实现布局模块化,这在布局编写方便提供了大大的便利
    • include标签唯一需要的属性是layout属性,指定需要包含的布局文件。可以定义android:id和android:layout_*属性来覆盖被引入布局根节点的对应属性值。注意重新定义android:id后,子布局的顶结点i就变化了。
  2. viewstub
    • viewstub标签同include标签一样可以用来引入一个外部布局,不同的是,viewstub引入的布局默认不会扩张,即既不会占用显示也不会占用位置,从而在解析layout时节省cpu和内存。
    • viewstub常用来引入那些默认不会显示,只在特殊情况下显示的布局,如进度布局、网络失败显示的刷新布局、信息出错出现的提示布局等。
  3. merge
    • 在使用了include后可能导致布局嵌套过多,多余不必要的layout节点,从而导致解析变慢,不必要的节点和嵌套可通过hierarchy viewer(下面布局调优工具中有具体介绍)或设置->开发者选项->显示布局边界查看。merge标签可用于两种典型情况
    • 1.布局顶结点是FrameLayout且不需要设置background或padding等属性,可以用merge代替,因为Activity内容试图的parent view就是个FrameLayout,所以可以用merge消除只剩一个。
    • 2.某布局作为子布局被其他布局include时,使用merge当作该布局的顶节点,这样在被引入时顶结点会自动被忽略,而将其子节点全部合并到主布局中。
去除不必要的嵌套和view节点以及inflate
  1. 首次不需要使用的节点设置为GONE或使用viewstub
  2. 使用RelativeLayout代替LinearLayout
    • 大约在Android4.0之前,新建工程的默认main.xml中顶节点是LinearLayout,而在之后已经改为RelativeLayout,因为RelativeLayout性能更优,且可以简单实现LinearLayout嵌套才能实现的布局。
  3. 对于inflate的布局可以直接缓存,用全部变量代替局部变量,避免下次需再次inflate
  4. 某些特殊场景下,用SurfaceView或TextureView代替普通View
    • SurfaceView或TextureView可以通过将绘图操作移动到另一个单独线程上提高性能。
    • 因为SurfaceView在常规视图系统之外,所以无法像常规试图一样移动、缩放或旋转一个SurfaceView。TextureView是Android4.0引入的,除了与SurfaceView一样在单独线程绘制外,还可以像常规视图一样被改变
  5. 尽量为所有分辨率创建资源:减少不必要的硬件缩放,这会降低UI的绘制速度
  6. 避免GPU过度绘制(同一个像素在同一帧的时间内被多次绘制)
    1. 原因:a. xml布局中,控件有重叠并且有设置背景;b. View的onDraw在同一区域绘制多次
    2. 避免方案: a. 移除不需要的背景; b. 在自定义view的onDraw方法中,用canvas.clipRect来指定绘制的区域,防止重叠的控件发生过度绘制
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值