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 看起来跟旋转屏幕之前的没什么两样,但它们是两个实例)

Android 音频焦点(Audio Focus)

引子

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

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

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

音频焦点

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

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

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

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

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

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

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

TextView Get Line Count return 0?

想给TextView加上个“展开/收起”的功能,思路是这样的,给TextView限制maxLine为4行,当getLineCount() >=4时,显示“展开”按钮。
但是无论怎么在textView#setText 之前还是之后去getLineCount()都返回的“0”…..
仔细“领会”了一番doc:

Return the number of lines of text, or 0 if the internal Layout has not been built.

原因应该是内部的布局还没有绘制完毕,我就去getLineCount()了,当然会返回0咯。

正确的get姿势应该如下

1
2
3
4
5
6
7
8
9
10
11
mTextView.setText("large text");
mTextView.post(new Runnable() {

@Override
public void run() {

int lineCount = mTextView.getLineCount();
if(lineCount >=4)
showDetailButton();
}
});

包名修改老用户迁移记

不知名的原因,原来的包被下架了,只能改包名卷土重来了。
改包名容易,但原来的2k+活跃用户咋办。。。

后面想到个辙,做一个原包名的空壳子APK,他做下列动作:

  1. 备份/data/data/<pkg>/ 下的数据到SD卡
  2. 安装新包名版本,该apk会检测SD卡备份的数据,将其收入囊中
  3. 安装完毕(无论用户是否取消,因为这时,数据一定是备份了的),卸载自己
  4. 成功迁移。

但是在付诸行动的时候,在第一步犯了个致命错误!

ViewPager: Can't change tag of fragment

问题描述:

在应用里用到了ViewPagerFragmentPagerAdapter 实现多页(>20页)滑动,每一页都是一个Fragment,给ViewPager注册一个OnPageChangeListener,当知道某一个Fragment被选中时,开始调用其刷新数据的方法。

并有一目录,点击某个条目时,调用 ViewPagersetCurrentItem(position) ,页面跳转到指定页,但是此时奇怪的是注册的 OnPageChangeListener#onPageSelected 居然没有被调用,表现为选中的页面没有刷新数据。

这时候往右侧滑动几页没问题,再往左侧滑动几页就一定会报出异常:

java.lang.IllegalStateException: Can’t change tag of fragment PageFragment{42ef29a8 #14 id=0x7f060052
android:switcher:2131099730:22}: was android:switcher:2131099730:22 now android:switcher:2131099730:17

分析原因:

经过一番排查,发现问题出现的原因在于我的FragmentPagerAdapter 内部维护的List集合造成的,因为我想自己来控制一下Fragment内部数据的销毁,并且使Fragment得到重用。

但是,在往List集合中,add(fragment)的时候,没有注意FragmentPagerAdapter#getItem(position)传入的位置和实例化fragment所存放在List中的位置!