Android高效进阶:从数据到AI【1.5】

 根据缩放比将图片加载到内存中

主要通过使用 Bitmap 的 compress 方法对磁盘上的图片进行压缩,并且存储到内存中:

1. ByteArrayOutputStream bos = new ByteArrayOutputStream();
2. bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
3. byte[] bitmapdata = bos.toByteArray();

上面代码中的 100 表示与原图保持相同的质量,控制其大小能有效减少对内存空间的占用。但是要注意,在改变 compress 方法中的质量参数的时候,压缩格式应该是 JPEG。若压缩格式被设置为 PNG,则任何修改都是无效的。
4.列表图片很多时,快速来回滑动会卡顿

无论是用 ListView 还是用 RecyclerView 作为列表的承载,当信息流中的每个 item 项都有图片且图片比较大时,快速来回上下滑动,很多时候会发生页面掉帧厉害的现象,也就是出现了列表卡顿。究其原因是,在列表高速滑动的时候,已经在子线程中加载好的图片会在主线程中被重新绘制到 ImageView 控件上。由于上下来回高速滑动,将导致 item 项频繁重用和销毁,进而导致图片中的 Bitmap 被频繁地创建和销毁。因此,解决方案就是,在列表发生滚动的情况下,暂停加载当前 ImageView 中的图片(包括下载、解码、设置等一系列操作);在列表停止滚动后,恢复 ImageView 中图片的下载任务(也包含下载、解码、设置等一系列操作)。

 针对 ListView

1. listView.setOnScrollListener(new AbsListView.OnScrollListener() {
2. @Override
3. public void onScrollStateChanged(AbsListView view, int scrollState) {
4. switch (scrollState){
5. case SCROLL_STATE_FLING:
6. //暂停图片加载
7. break;
8. case SCROLL_STATE_IDLE:
9. //恢复图片加载
10. break;
11. }
12. }
13.
14. @Override
15. public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
16.
17. }
18. });

 针对 RecyclerView
 

1. recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
2. @Override
3. public void onScrollStateChanged(RecyclerView recyclerView, int
newState) {
4. switch (newState) {
5. case RecyclerView.SCROLL_STATE_IDLE:
6. //恢复图片加载

7. break;
8. case RecyclerView.SCROLL_STATE_SETTLING:
9. //暂停图片加载

10. break;
11. }
12. }
13. });

5.列表图片显示错位、出现闪烁问题

图片显示错位、出现闪烁问题本质上是由于列表的复用机制(异步加载及对象被复用)导致的。假设一种场景:一个列表一屏显示 5 个 item,那么在拉出第 6 个 item 的时候,事实上该 item 重复使用了第 1 个 item(即复用了第 1 个 item),也就是说在第 1 个 item 从网络中下载图片并最终要显示的时候,其实该 item 已经不在当前显示区域内了,此时显示的结果将可能是在第 6 个 item 上输出图片,这就导致了图片显示错位问题。

解决此问题的方案是,每次 getView 能给对象提供一个标识,在异步加载完成时比较标识与当前行 item 的标识是否一致,一致则显示,否则不做处理。

首先给 ImageView 设置一个 Tag,这个 Tag 中设置的是图片的 URL,然后在加载的时候将取得的这个 URL 与要加载的那个 position 中的 URL 对比,如果不相同就加载,相同就复用以前的而不加载。

6.加载图片时只显示了一部分

ImageView 的高度被设置为 WRAP_CONTENT,在加载图片时,会先设置一个 loading 的占位图,这就导致 item 在计算显示高度时开始只能计算占位图的高度,所以在图片加载完成显示图片时,图片只有占位图的高度那么高,如果真实图片高度大于占位图高度,那么这个图片只会显示上半部分。

针对此问题的解决方案有两个。

 将 ImageView 的高度属性由 WRAP_CONTENT 改为准确的高度值,比如 40dp 等。

 在图片下载成功的监听回调中,通知 ImageView 父布局做一次重绘操作,即调用requestLayout 方法。

7.加载图片变绿的问题

加载图片变绿的主要原因是图片压缩,在使用 WebP 格式的图片时出现这个问题的可能性较大。

解决方案是,将默认的 Bitmap 编码格式 RGB565 更改成 ARGB_8888,这样图片就不会因过度压缩而变绿。

2.3.4 基于信息流的图片加载设计

在信息流产品中,图片是一个非常核心的元素,因此接下来描述的就是基于信息流产品的图片加载设计,整体的图片加载设计图如图 2-4 所示。

如图 2-4 所示的整个设计图分为 3 层: API 使用层、核心层和数据缓存层。

 API 使用层主要封装了与图片加载相关的一些 API,方便用户调用,包含了图片 URL以及与图片加载相关的一些配置。

 核心层的关键在图片下载的请求分发处理上,将图片的 URL 转换成数据流的形式,

覆盖了常用的 5 个渠道 Http、 Https、 File、 APK 路径和 PKN(应用包名),并且对数据流进行解码处理。

 数据缓存层主要采用了三级缓存策略:解码后的 Bitmap 内存缓存、原始未解码的Bitmap 内存缓存以及磁盘缓存,目的是提高加载图片的速度。

那么如何实现基于信息流的图片加载呢?下面将从 API 使用层、核心 Core 层以及核心Cache 层来进行说明。

2.3.5 基于信息流的图片加载实现

下面将以基于信息流的图片加载实现为例,从 API 使用层、核心 Core 层和核心 Cache 层来进行说明。

1. API 使用层入口

下面是加载图片的核心方法:

1. void loadImage(View view,String url,String thumbnailUrl,Config config,
ImageLoaderListener loaderListener, ImageProgressListener progressListener)

view 表示需要加载图片的控件,分以下 3 种情况实现。

 view 为 null 时,直接下载并且缓存图片到内存。

 view 为普通的 view 时,通过 setBackground 的方式设置背景图。

 view 为 ImageView 时,通过 setImageBitmap 设置图片的 URL 和 thumbnailUrl 分别表示图片的地址以及图片缩略图的地址。当 thumbnailUrl 存在时,优先加载缩略图(缩略图比原图小很多,加载速度会明显变快)到控件中显示。缩略图下载完成后如果图片 URL 不为空,则继续下载原图到控件,这样就保证了信息流的大图片中间加载过程的过渡是流畅的。

2.核心 Core 层

Config 是图片加载的配置:可配置加载中占位图( loadingPlaceHolder)、加载错误占位图( errorPlaceHolder)、图片的显示大小( withSize(width,height))、加载优先级( priority)、是否进行内存缓存( memoryCache)和是否进行磁盘缓存( disCache)等。

ImageLoaderListener 主要用于回调图片加载过程中的监听状态,分成功和失败两个状态,加载成功后将会返回 Bitmap。

ImageProgressListener 表示图片网络下载过程的进度回调和当前图片加载的比例。

RequestManager 主要用于管理各个图片加载请求(包括网络下载、磁盘加载和内存加载等),所有的请求都会依照优先级( priority)存放在 RequestQueue 队列中,主要会将提供的图片地址、 APK 路径或者应用包名转换成数据流。

网络图片地址( HTTP/HTTPS):通过 HttpURLConnection 下载图片转换成数据流。

普通文件地址:通过 BitmapFactory.decodeFile 将文件转换成 Bitmap 数据流。APK 路径和应用包名在前面的内容中都有提到,这里就不赘述了。

ExcutorService 有如下 3 个线程池。

 Download 线程池:主要用于异步下载图片操作。

 Decode 线程池:主要用于异步解码图片并转换成 Bitmap 的操作。

 Cache 线程池:主要用于异步地对图片进行三级缓存操作。

3.核心 Cache 层

数据缓存层主要以三级缓存方式使用:

 Bitmap 内存缓存:在 Android 5.0 版本以上和以下操作有所不同,在 Bitmap 回收机制相关章节中有详细说明。

 未解码的 Bitmap 内存缓存:主要用于在内存中存储原始的数据流,保留原始未被处理的数据。

 磁盘缓存:将数据永久化地存储到磁盘,数据不会因为进程被杀而被清理,可以减少网络请求的次数。

2.4 进程保活

进程保活可以说是 Android 的一大特色,它承担了很多应用上层的业务,不过随着Android 的发展,尤其是自 Android 6.0 版本之后,系统也对这一块进行了收拢和管理,因此常规的保活技术越来越难,接下来就描述一些可能用到的保活经验。

2.4.1 常规的保活技术

对于进程保活,其历来就是 Android 应用开发者的关注点之一,下面就来介绍一下进程保活相关技术。

1.进程保活(提升优先级,防杀)

Android 系统在 App 退出后台时并不会真正杀掉这个进程,而是将其缓存起来以方便下次能快速启用。在系统内存不足的情况下,系统会依据一套 Low Memory Killer 机制来杀进程。

Linux 内核会为每一个进程分配一个 oom_adj 值,如图 2-5 所示,这个值代表进程的优先级,值越大,代表进程优先级越低,那么进程就越容易被回收, Low Memory Killer 就是根据这套机制来决定哪个进程被回收的。

如表 2-1 所示,表示不同 oom_adj 级别对应的含义。

2.利用系统通知管理权限提升保活能力

当 App 拥有通知栏管理权限时,在 NotificationListenerService 启动后,进程 oom_adj 可以被提升到 1。

注意: 此方案具备很强的保活能力,但需要用户授权。

3.利用系统辅助功能提升保活能力

在 App 获取到辅助功能权限后,进程 oom_adj 也可以被提升到 1。

弊端:进程被杀后需要用户重新授权。4.利用系统机制开启前台服务提升保活能力原理如下。

 对 于 API level < 18 : 调 用 startForeground(ID, new Notification()) 发 送 空 的

Notification,图标不会显示。

 对于 API level ≥18:在需要提高优先级的 service A 中启动一个 InnerService,两个服

务同时 startForeground 且绑定同一个 ID。停掉 InnerService,这样通知栏图标会被移除。

注意: 这种利用系统机制开启前台服务提升保活能力的方案在 Android API level ≥ 24 时失效。

5.伪装成输入法提升保活能力

在有些系统 ROM 中,可以将自己需要保活的进程伪装成输入法进程,这样能拥有很强的保活能力,不过这个方案一般在一些系统软件能力比较差的手机上才有效果。

6.在后台播放无声音乐

这个方案主要是将需要保活的进程伪装成播放音乐进程,播放音乐进程一般拥有较强的保活能力,不过这个方案一般也在一些系统软件能力比较差的手机上才有效果。

7.进程拉活(被杀后重启)

( 1)利用系统广播拉活

常用的系统广播如下:

 ACTION_BOOT_COMPLETED

 CONNECTIVITY_ACTION

 ACTION_USER_PRESENT

弊端:当前很多热门 ROM 都增加了自启管理,这个方案将导致无法接收广播,从而导致服务无法自启。

( 2)利用系统 Service.START_STICKY 机制拉活

将 Service 设置为 START_STICKY,利用系统机制在 Service 挂掉后将其自动拉活。在以下两种情况下无法拉活:

 Service 在第一次被异常杀死后会在 5s 内重启,第二次被杀死后会在 10s 内重启,第三次被杀死后会在 20s 内重启,一旦短时间内 Service 被杀死的次数达到 5 次,则系统不再拉活该 Service。

 进程被取得 Root 权限的管理工具或系统工具通过 force-stop 系统停掉,这时无法重启。

( 3)利用 Native 进程拉活

在 Android 系统中,所有进程和系统组件的生命周期受 ActivityManagerService 的统一管理。而通过 Linux 的 fork 机制创建的进程为纯 Linux 进程,其生命周期不受 Android 系统的管理。

在监听到主进程死亡后,可以通过 am 命令拉活主进程。

该方案主要适用于 Android 5.0 以下版本的手机, 5.0 以上版本的手机中 Native 进程照样会被杀。

( 4)利用 AlarmManager 定时拉活

开一个定时任务,在后台每隔一段时间拉起进程。

( 5)利用 JobScheduler 机制拉活

在 Android 5.0 版本推出后,系统提供 JobScheduler,允许开发者在符合某些条件时创建在后台执行的任务。利用该机制可使系统拉活进程。

该方案的适用范围: Android 5.0 及以上版本的系统,进程在被强制停止后也可进行拉活。

( 6)利用账号同步机制拉活

该方案的适用范围:该方案适用于所有的 Android 版本,包括被强制停止掉的进程也可以拉活。但一些有自启管理的手机机型可能会不支持该方案,如 Vivo 6.0 手机。

( 7)利用推送 SDK 拉活

一般第三方推送 SDK 都具备矩阵拉活能力,推送 SDK 的相关接入方通常都可以相互保活。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容