瞎拍(1)

a6000 f5.6 32mm 1/160s ISO 100

柳绿花红,春草茵长

高楼林立之间,这一片绿到底争出了些春的颜色

乱弹琴:移动端APP所谓的闪屏

“闪屏”又叫“启动画面”,追根溯源,移动端App上“闪屏”这个词似乎还是苹果“爸爸”拿过来在它那一套“HIG”里“重新定义”的。大概是为了解决应用冷启动加载时间长的问题,而采用的一个取巧的办法:先显示一个“占位图”以示程序加载中。有在这个占位图上显示LOGO的,也有干脆放个“菊花”转半天的,也有的放个应用首页的轮廓图。真可谓八仙过海,各显神通。

本人也不是iOS开发,个中细节也不甚了了,但我似乎记得在 Android 应用的开发中 Google 官方曾是不推荐用“闪屏”这种玩意的。也许我记忆混乱了,但至少相当长的一段时间 Google 的全家桶是没有“闪屏”的,直到15年某个版本全线加上了这个玩意。当时我就“震惊”了,Google你的节操呢。。。

你想,一个承载着企业品牌的LOGO的启动画面(“闪屏”),怎么也得至少显示个1秒吧,那你的应用真正被用户所看到得干等这一小段时间,不是本末倒置吗?

说起来,随着应用复杂度提升、代码越来越庞大,程序的冷启动随之变慢,那么用户似乎怎么着都得干等一段时间。。那怎么行(#‵′)凸,怎么能让“大爷们”看着黑/白屏干等呢?!

一探究竟:Android M 如何获取 Wifi MAC地址(1)

引子

1
2
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
String macAddress = wifiManager.getConnectionInfo().getMacAddress();

早在 Android M 预览版发布时就有人发现,通过WifiInfo.getMacAddress()获取的MAC地址是一个“假”的固定值,其值为 “02:00:00:00:00:00”。对于这个,官方的说法当然不外乎“保护用户隐私数据”

大胆假设

不知是有意还是一时不查,Google却忘了Java获取设备网络设备信息的API——NetworkInterface.getNetworkInterfaces()——仍然可以间接地获取到MAC地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface iF = interfaces.nextElement();

byte[] addr = iF.getHardwareAddress();
if (addr == null || addr.length == 0) {
continue;
}

StringBuilder buf = new StringBuilder();
for (byte b : addr) {
buf.append(String.format("%02X:", b));
}
if (buf.length() > 0) {
buf.deleteCharAt(buf.length() - 1);
}
String mac = buf.toString();
Log.d("mac", "interfaceName="+iF.getName()+", mac="+mac);
}

输出如下:

1
2
3
4
interfaceName=dummy0, mac=e6:f9:44:3c:ee:da
interfaceName=p2p0, mac=ae:22:0b:3e:d4
interfaceName=wlan0, mac=ac:22:0b:3e:d4
...

顾名思义,猜想wlan0对应的mac地址应该就是我们要找的。

小心求证

既然NetworkInterface可以正常获取,那得好好看看它在 Android framework 中的实现源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public byte[] getHardwareAddress() throws SocketException {
try {
// Parse colon-separated bytes with a trailing newline: "aa:bb:cc:dd:ee:ff\n".
String s = IoUtils.readFileAsString("/sys/class/net/" + name + "/address");
byte[] result = new byte[s.length()/3];
for (int i = 0; i < result.length; ++i) {
result[i] = (byte) Integer.parseInt(s.substring(3*i, 3*i + 2), 16);
}
// We only want to return non-zero hardware addresses.
for (int i = 0; i < result.length; ++i) {
if (result[i] != 0) {
return result;
}
}
return null;
} catch (Exception ex) {
throw rethrowAsSocketException(ex);
}
}

原来MAC地址是直接从"/sys/class/net/" + name + "/address"文件中读取的!

这个name是什么呢?

RecyclerView Tips(2) SortedListAdapter

上一篇说到Tabs+ViewPager+ListView是最常见的组合,这篇就议一议如何用RecyclerView快速实现列表页面。

如一个简单的列表场景:TodoList。

分页加载现有Todo
现有数据基础上增、删、改

RecyclerView的使用在此就不赘述了,本文主要讨论RecyclerView.Adapter的实现

使用最简单的ArrayList实现,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ListAdapter extends RecyclerView.Adapter<TodoViewHolder> {
final ArrayList<Item> mData;
final LayoutInflater mLayoutInflater;
public SortedListAdapter(Context context) {
mLayoutInflater = LayoutInflater.from(context);
mData = new ArrayList<>();
}

public void addItem(Item item) {
mData.add(item); // 需要自己通知更新
}

@Override
public TodoViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) {
return new TodoViewHolder (
mLayoutInflater.inflate(R.layout.list_todo_item, parent, false));
}

@Override
public void onBindViewHolder(TodoViewHolder holder, int position) {
holder.bindTo(mData.get(position));
}

@Override
public int getItemCount() {
return mData.size();
}
}

这样的Adapter一个显而易见的问题就是,如何做数据的去重

  1. 添加一项数据:最简单的是在addItem()之前,遍历一次mData,定位后再决定是插入还是更新现有数据,并调用notifyItemInserted(pos)
  2. 添加多个数据:多次重复上面的方法…

对于少量数据来说这样做并不见得有什么问题,而且写得多了,都有自己封装好的诸如ArrayObjectAdapter之类方便使用。

这样就够了吗?

答案肯定是不。Android Support Library 悄悄给我们提供了一个叫SortedList的工具类,它默默的藏在support库的角落中,鲜为人知。

SortedList?

文档对它的定义:

  1. 是一个有序列表
  2. 数据变动会触发回调SortedList.Callback的方法,如onChanged()

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