您的位置:首页 > 移动开发 > Android开发

如何在Android开发中让你的代码更有效率

2015-08-05 15:14 561 查看
转载自:http://greenrobot.me/devpost/doing-more-with-less-being-a-good-android-citizen/

最近看了Google IO 2012年的一个视频,名字叫做Doing
More With Less: Being a Good Android Citizen,主要是讲如何用少少的几句代码来改善Android App的性能。在这个视频里面,演讲者以一个图片app为例讲解如何应用Android中现有的东西来改善app性能问题。这个图片app的代码在这里。ppt在这里。现在我将视频里面的内容记录如下:


使用LruCache避免OOM

首先我们的图片app是用来展示手机里面保存的图片。当app里面需要展示大量的图片的时候,我们需要将这些图片从disk加载到内存当中。如果我们来回地滑动activity,系统会重复许多disk I/O;而且在一个activity里面同时加载多张图片将会占用大量内存,造成系统内存紧张,进而影响用户体验。


如何使用LruCache

这个时候我们可以引入LruCache,将我们最常用的图片缓存到内存里面,这样可以避免大量重复的disk
I/O,还可以让app加载图片时内存占用不超过设定值。具体代码如下:

123456789101112131415//这里假设通过picture的id(Long)来作为key获取对应的bitmappublic class PictureCache extends LruCache<Long, Bitmap>{ //设定最大的byte值,也就是说整个缓存所能占用的最大内存 public PictureCache(int maxByteSizes){ super(maxByteSizes); } // 计算每次添加bitmap的时候,给缓存所添加的数字,默认就是数量, //这里因为添加的是bitmap,所以每次添加都是计算bitmap对应的字节数 protected int sizeOf(Long key,Bitmap value){ return value.getByteCount(); } public void put(Long key, Bitmap value); public Bitmap get(Long key);}

如何确定Cache大小

一般我们是通过ActivityManager.getMemorySize()来确定Cache的大小。ActivityManager.getMemorySize()表明了在系统正常运行的前提下一个App所占内存的极限。所以我们可以使用它来作为Cache大小的一个衡量,比方如下的代码中,我们使用它的一半来作为Cache的大小:

1

2

3

final
ActivityManager
am
=
(ActivityManager)
getSystemService(Context.ACTIVITY_SERVICE);

final
int
memoryClassBytes
=
am.getMemoryClass()
*
1024
*
1024;

PictureCache
mCache
=
new
PictureCache(memoryClassBytes
/
2);


app跑到后台去了

当我们用这个图片app浏览完图片之后呢,我们回到Android 主界面,开始玩游戏。大家都知道,游戏很耗内存。可能我们在玩的过程中,直接用完了剩下可用的所有内存都还不够,那怎么办呢?Android会在这个时候kill一些后台的app获取相应的内存。

这里需要先说一命令。我们如何获取一个app的内存占用?很简单,使用 “adb shell procrank”命令行。这个命令行显示所有系统运行的进程所占内存大小,一般包括Vss、Rss、Pss、Uss,其中Uss对我们来说最重要,Uss表示如果这个进程被系统干掉了,那么系统可以从这个进程上面获得多少的可用内存。

好了回到之前的情景。Android会kill掉一些后台程序来供给游戏所需的内存。假设Android因为内存紧张kill掉了图片app,我在玩了一会游戏之后又打开了图片app。这个时候图片app又要重新布局,重新加载图片,整个体验对用户来说非常不好。有什么办法让Android在kill应用之前通知app一声,好让app有所准备?

这个时候我们就要用到ComponentCallbacks2,详情看文档。app在系统处于不同的内存环境时会有相应的callback,我们只需在activity里面重写这个onTrimMemory方法即可,具体示例代码如下:

12345678910111213141516public void onTrimMemory(int level) { super.onTrimMemory(level); if (level >= TRIM_MEMORY_MODERATE) { // 60 // 这个app已经进入后台有一段时间了,基本上表示用户接下来 //不会重新打开这个app,我们可以清掉所有缓存所占内存 Log.v(TAG, "evicting entire thumbnail cache"); mCache.evictAll(); } else if (level >= TRIM_MEMORY_BACKGROUND) { // 40 // 表示app刚进入后台,我们可以缩减一部分缓存所占内存 // 来保证其他前台app的内存需要 Log.v(TAG, "evicting oldest half of thumbnail cache"); mCache.trimToSize(mCache.size() / 2); } }
这个callback只是建议,不一定会被系统调用。系统在内存紧张的时候可能会直接kill掉app而不去调用这个callback。但是如果这些callback可以调用的话,这将大大地提升我们app的用户体验。

善用Android自带容器

现在我们需要为这个app添加一些新特性,我们要让这个app可以进行收藏操作(为图片添加一个是否被收藏的属性即可)。我们会一次性收藏多张图片,那么我们可以使用GridView的多选模式。按照常理来说,我们可以使用HashMap()<Long, Boolean>来存储哪些照片需要被收藏。不过这里使用HashMap有点大材小用,效率不高。我们可以使用SparseBooleanArray等Android特有的容器来代替HashMap,节省系统开销(主要是autoboxing带来的开销)

SQLite中读写操作优化

当我们获得了要收藏的图片信息(保存在SparseBooleanArray中)之后,我们需要讲这些数据保存在SQLite当中,示例代码如下:

1

2

3

4

5

6

7

8

SQLiteDatabase
db;

long[]
mPhotoIds;

ContentValues
values
=
new
ContentValues();

for
(long
photoId
:
mPhotoIds)
{

values.put(COLUMN_ID,
photoId);

values.put(COLUMN_FAVORITE,
favorite);

db.insert(TABLE_FAVORITE,
null,
values);

}

上面的代码中,每次insert都有开销。这个时候我们可以考虑使用transaction。代码如下:

1234567891011121314SQLiteDatabase db;long[] mPhotoIds;ContentValues values = new ContentValues();db.beginTransaction();try{ for (long photoId : mPhotoIds) { values.put(COLUMN_ID, photoId); values.put(COLUMN_FAVORITE, favorite); db.insert(TABLE_FAVORITE, null, values); } db.setTransactionSuccessful();}finally{ db.endTransaction();}
但是使用这个transaction的时候,db会被锁住,而碰到更重要的操作只能等待。碰到这种情况怎么办?使用db.yieldIfContendedSafely,这个方法表示我现在执行我的多次数据库操作,如果碰到其他的数据库操作,我先让别的操作完 再执行我的操作。具体代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

SQLiteDatabase
db;

long[]
mPhotoIds;

ContentValues
values
=
new
ContentValues();

db.beginTransaction();

try{

for
(long
photoId
:
mPhotoIds)
{

values.put(COLUMN_ID,
photoId);

values.put(COLUMN_FAVORITE,
favorite);

db.insert(TABLE_FAVORITE,
null,
values);

db.yieldIfContendedSafely();

}

db.setTransactionSuccessful();

}finally{

db.endTransaction();

}


使用RenderScript

如果这个时候我想让这个图片app拥有一些简单的滤镜,大家可能一下就想到使用NDK来实现相应的图片处理工作。其实Android提供RenderScript也能完成这样的任务,而且优点不少呢:

RenderScript能充分利用用户设备的硬件资源。比如设备的CPU是双核的,那么RenderScript便会相应地开辟两个worker线程来进行图片处理工作

RenderScript面向所有架构,包括ARM、X86、MIPS等等。其中原理是首先RenderScript在第一阶段编译成中间代码并打包到apk文件里面,接着在apk被安装到设备上面之后,中间代码会被再次编译成与设备平台相关的native代码

RenderScript会自动生成相应的JNI 胶水代码

以上这三点相对于NDK来说,节省了开发者许多精力,让开发者更专注于效果代码的实现。这个示例相应的RenderScript代码在这里

这个视频是专门介绍RenderScript的一些用法,也是来自于Google
IO。


善用Broadcast

如果我们需要对用户新增的照片默认添加滤镜效果,怎么实现?很好办啦,因为Android在API14的时候添加了一个新的Broadcast Intent,叫做”android.hardware.action.NEW_PICTURE”,就是在系统新增了照片时,我们截获这个intent就可以了。然后我们就在相应的BroadcastReceiver里面进行处理,相关代码如下:

1

2

3

4

5

6

7

8

9

10

public
void
onReceive(Context
context,
Intent
intent)
{

if
(isAutoApplyEnabled)
{

//
执行相应的滤镜操作

final
Intent
serviceIntent
=
new
Intent(context,
EffectService.class);

serviceIntent.setData(intent.getData());

context.startService(serviceIntent);

}
else
{

Log.d(TAG,
"Processed
no-op broadcast!");

}

}

上面的isAutoApplyEnabled变量表示系统是否开启自动对新增照片进行滤镜的操作。那如果关闭默认滤镜功能,即将isAutoApplyEnabled的值设为false呢?结果就是每次新增照片都会有intent过来,只是在onReceive方法里面没有做操作。这样造成的结果就是每次intent都会被系统传递,只是走了不同的分支,系统照样消耗资源。

因此比较好的方案就是系统中isAutoApplyEnabled变量的值在变化的时候,我们需要相应地对BroadcastReceiver进行开闭操作。具体需要用到PackageManage.setComponentEnabledSetting()方法。


最后的几句话

本文以一个图片app为背景讲述了Android开发中官方推荐的小tip。了解了这些可以让你的app性能更上一个台阶。本文涉及的所有代码可以在这里找到。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: