RecyclerView Tips(1) RecycledViewPool

想必Tabs+ViewPager+ListView 结合使用的场景在你的Android手机中的各大应用里并不少见,比如最为典型的网易新闻。

众所周知,用RecyclerView可以非常简单的替代掉ListView。可仅仅就为了将ListView换成RecyclerView,这换汤不换药的做法显然不足以让人心动。

如果我说,再用上RecycledViewPool,可以使你的布局渲染速度、ViewPager滑动流畅度上升一个档次,你会心动并行动吗?

RecycledViewPool是什么?

关于RecycledViewPool,官方文档是这样说的:

Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views. This can be useful if you have multiple RecyclerViews with adapters that use the same view types, for example if you have several data sets with the same kinds of item views displayed by a ViewPager.

RecyclerView automatically creates a pool for itself if you don’t provide one.

简言之就是,你可以给RecyclerView设置一个ViewHolder的对象池,这个池称为RecycledViewPool,这个对象池可以节省你创建ViewHolder的开销,更能避免GC。即便你不给它设置,它也会自己创建一个。

如此说来,如果多个RecylerView间共用一个RecycledViewPool是不是能让你的UI更加的“顺滑”?

使用RecycledViewPool

RecycledViewPool使用起来也是非常的简单:先从某个RecyclerView对象中获得它创建的RecycledViewPool对象,或者是自己实现一个RecycledViewPool对象,然后设置个接下来创建的每一个RecyclerView即可。

需要注意的是,如果你使用的LayoutManager是LinearLayoutManager或其子类(如GridLayoutManager),需要手动开启这个特性:layout.setRecycleChildrenOnDetach(true)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RecyclerView view1 = new RecyclerView(context);
LinearLayoutManager layout = new LinearLayoutManager(context);
layout.setRecycleChildrenOnDetach(true);
view1.setLayoutManager(layout);
RecycledViewPool pool = view1.getRecycledViewPool();
//...
RecyclerView view2 = new RecyclerView(context);
//... (set layout manager)
view2.setRecycledViewPool(pool);
//...
RecyclerView view3 = new RecyclerView(context);
//...(set layout manager)
view3.setRecycledViewPool(pool);

ViewPager中使用原理同上,只是你得通过ViewPagerAdapter传递个下一个RecyclerView。

另辟蹊径实现Android多渠道打包

要先说明的是本文说的“渠道”单指在AndroidManifest.xml<meta-data>定义的一个标识字符串(如友盟统计)。在代码或者通过其他文件定义的方式殊途同归。

说起 Android 多渠道打包,真是八仙过海各显神通:有手动一个个耐心打包的,有用AntMaven重复跑编译任务的,有用apktool解包后再修改重打包的,有在build.gradle定义一堆flavor的,乃至有通过apk里META-INF/下的空文件来定义渠道的

上述方法各有优劣,在这里就不一一赘述了。

本文要介绍的是另一种方法:直接修改APK中的AndroidManifest.xml
上述种种,说白了都是围绕着如何修改AndroidManifest.xml,如何重打包或是重编译。介绍的这个方法也不外如是,只是无需重打包重编译而已。

首先得知道一点,APK中的AndroidMainfest.xml,解压出来用文本编辑器可是不能直接打开的,它是aapt生成的一个二进制的xml格式(被称为AXML),得用其他工具(如apktool)先解析出来。所以问题来了,如何直接修改这个 AXML 文件?

如何修改AXML中渠道名

想要修改一个文件,你得先了解它的格式。AXML文件格式其实早已有人研究,如:《发布C语言的Android binary XML(AXML)解析代码》
《AndroidManifest Ambiguity方案原理及代码》

这里就直接引用结论了:string在AXML中是存放在StringChunk中的;string都是UTF-16编码的;如果需要往AXML中新增string是比较麻烦的(牵一发而动全身…);为了4字节对齐string数据块末尾可能被填充数个0x00…

综上结论,可知,<meta-data>定义的渠道值是UTF-16编码的string,并且可能被填充数个0x00

那么为了方便后期修改,我们可以先编译的一个特殊的“占位渠道包”,这个包的渠道名是一个占位字符串,而这个字符串在AXML占的数据块长度能适应所有渠道名的长度。假设一个占位字符串长度16,那么它自然可以被个数小于16的任意字符串所替代,如占位字符串’abcdefghijklmnop’,渠道有’xxxx’,’abcdef789’…

通过这个特殊的渠道包,我们就能够生成所有渠道包。

Gradle 添加上传 APK 到FIR.im支持

!DEPRECATED 由于FIR网站的更新,这篇文章的脚本已经失效了!新的API的利用脚本正在捣鼓中…

关于FIR.im就不做过多介绍了,本文主要是为了解决通过 Android Studio 或者说 Android Gradle 构建工具快速上传 APK 到 FIR 上。

FIR提供了上传APK的API,下面就是使用这个API的脚本:

Gradle 修改 Maven 仓库地址

近来迁移了一些项目到Android Studio,采用Gradle构建确实比原来的Ant方便许多。但是编译时下载依赖的网速又着实令人蛋疼不已。

如果能切换到国内的Maven镜像仓库,如开源中国的Maven库(已停止维护),或阿里云的Maven服务,又或者是换成自建的Maven私服,那想必是极好的。

一个简单的办法,修改项目根目录下的build.gradle,将jcenter()或者mavenCentral()替换掉即可:

1
2
3
4
5
allprojects {
repositories {
maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
}
}

但是架不住项目多,难不成每个都改一遍么?

自然是有省事的办法,将下面这段Copy到名为init.gradle文件中,并保存到 USER_HOME/.gradle/文件夹下即可。

Apktool 解包资源引用偏移的问题

Apktool(v2.0.0rc3) 解由最新的build-tools(v21.x) 所编译的Apk,如果其引用了Android Lollipop的Material主题,会出现资源引用偏移的情况。
values-v21文件夹下的styles.xml中的主题原来是:

<style name="AppBaseTheme" parent="@android:style/Theme.Material.Light">

解包后却变成

<style name="AppBaseTheme" parent="@android:style/Widget.Holo.DateTimePicker">

##原因
Apktool decode 某个Apk首先需要加载 packageandroid (其package id一般为0x01[1]) 的资源映射表(Resource Table),这就是第一次运行Apktool会安装内置资源包resources.arscxxx/apktool/framework/1.apk的原因[2]

可想而知,如果这个资源包没有及时升级,自然就会导致资源引用偏移甚至解包失败:

Exception in thread "main" brut.androlib.err.UndefinedResObject:resource spec: 0x01...

DialogFragment的Listener在屏幕旋转后被重置的问题

问题

DialogFragment 在 Activity 中创建后通过 show(fm, tag),弹出 Dialog,如下面的代码所示:

1
2
3
MyDialogFragment dialog = new MyDialogFragment(); // 一个自定义的 DialogFragment
dialog.setButtonListener(aListener); // 一个自定义方法,设置一个监听(如某个按钮的回调)
dialog.show(getFragmentManager(), "tag"); // 弹出对话框

点击按钮可以回调 Activity 的 aListener,但是,如果在对话框存在的情况下旋转屏幕,再怎么点按钮aLisenter也不回调了?!

原因

只要理解了Fragment 和 Activity 生命周期就会知道原因其实很简单:

  1. 旋转屏幕时,Activity将会被重新创建。
  2. Activity“临终”前会在onSaveInstanceState()中保存 DialogFragment的状态(FragmentManagerState);
  3. “复活”后的Activity,在onCreate()中会根据savedInstanceState所给予的FragmentManagerState自动重新实例化DialogFragment,并且 show()出 dialog

流程为:

旋转屏幕-->-Activity.onSaveInstanceState()-->-Activity.onCreate()-->- DialogFragment.show()--|

这个时候的DialogFragment所持有的aListener引用当然也不复存在,再点按钮自然也不会收到回调(虽然这个 dialog 看起来跟旋转屏幕之前的没什么两样,但它们是两个实例)

简述用 MAT 分析 Android 应用OOM

OOM(OutOfMemoryError) 相信是所有 Android 开发者遇到的最多的 Error,因此找出个中缘由是非常重要。

这里用到的分析工具主要是 Eclipse MAT 插件http://www.eclipse.org/mat/

Eclipse MAT(Memory Analyzer Tool)主要用于分析Java程序OOM 时 JVM 的 Heap dump文件(HPROF格式的二进制文件)。

一般的,Java 程序要生成这样的dump文件,需设置 JVM 启动参数-XX:-HeapDumpOnOutOfMemoryError(同样适用于Android应用)。

Android应用生成 dump 文件可以使用 DDMS,点击Devices工具”Dump HPROF file”,按提示框保存文件即可(如果安装了 MAT则会自动打开文件)。

具体就不再赘述…
相关阅读:

这里简单介绍一下用 MAT分析 Android 应用的内存泄露。

  1. 首先安装 MAT
    http://www.eclipse.org/mat/downloads.php下载或者用Update Sitehttp://download.eclipse.org/mat/1.4/update-site/安装
  2. 在 DDMS 中选择需要调试的设备及你的应用进程
  3. 点击“Dump HPROF file”按钮提取 dump 文件(需稍许等待)
  4. 自动用MAT 打开文件,选择“Leak Suspects Report”
  5. 睁大你的双眼揪出最有可能导致 OOM 的因素吧!

Eclipse打开后挂在Android SDK Loader 0%

如题,近来更新ADT后就重新开启Eclipse这个问题就会莫名发生。只能强制退出Eclipse,再次打开也如故。
目前解决办法是强退后手动清除cache:

1
2
3
cd ~/.android
rm -rf ./cache
rm -r ./ddms.cfg

然后重启Eclipse

另外,stackoverflow上也有人讨论这个问题:http://stackoverflow.com/questions/13489141/eclipse-hangs-at-the-android-sdk-content-loader,提出了许多办法,大多是清除用户数据之类,一一尝试总有一款适合你~~

Android 音频焦点(Audio Focus)

引子

说 Audio Focus 前先说个很简单需求:来电暂停正在播放的音乐,电话结束时恢复播放。

看到这个需求,第一反应肯定是:监听用户来电状态,作相应操作。这里不多做介绍,这样做有个不好的地方就是需要隐私权限!这样做一点也不优雅

后来搜索时看到一篇分析文章:Android来电时停止音乐播放的流程(顺便说一嘴,这篇转载居然不注明出处!!)。文章里的分析很明确的指出,系统在框架层就很好的帮我们处理了这个需求,问题是如何将音乐交给系统框架来处理呢?

音频焦点

问题的解决方法就是:请求系统的音频焦点Request the Audio Focus)。

如果英文还行,强烈建议请看官方的原文:Managing Audio Playback,里面介绍的很清楚。以下为简单概述。

官方文档指出Android 在处理音频播放是分了多个“音频流”的,如音乐流、音效流、电话声音流等,使控制音量时可以互不干涉。多数情况下我们播放音乐都是使用 STREAM_MUSIC 音频流。

另外,系统中可能会有多个应用程序会播放音频,所以需要考虑他们之间该如何协调,为了避免同时播放音乐,Android 系统使用音频焦点来进行统一管理,即只有获得了音频焦点的应用程序才可以播放音乐。

那么,播放音频应该这样来做:

  1. 获取音频焦点 requestAudioFocus
  2. 获取成功后,开始播放音频
  3. 处理音频焦点的丢失和“DUCK”
  4. 播放完毕后取消焦点

如此便可以完美的解决引子里的需求。