瞎拍(2)

闲来无事到植物园瞎拍了几张,也不枉这春光无限好

✿

✿

✿

✿

又走了一遭步行街,远远的观摩了一下大都市的夜

南京路

Android 不想和你说话,抛了个 java.lang.VerifyError

一个奇怪的崩溃

E/AndroidRuntime(22035): FATAL EXCEPTION: main
E/AndroidRuntime(22035): java.lang.VerifyError: com/sample/FileUtils
E/AndroidRuntime(22035): at com.sample.App.onCreate(App.java:16)
E/AndroidRuntime(22035): at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:999)
E/AndroidRuntime(22035): at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4220)
E/AndroidRuntime(22035): at android.app.ActivityThread.access$1300(ActivityThread.java:137)
E/AndroidRuntime(22035): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1262)
E/AndroidRuntime(22035): at android.os.Handler.dispatchMessage(Handler.java:99)
E/AndroidRuntime(22035): at android.os.Looper.loop(Looper.java:137)
E/AndroidRuntime(22035): at android.app.ActivityThread.main(ActivityThread.java:4819)
E/AndroidRuntime(22035): at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime(22035): at java.lang.reflect.Method.invoke(Method.java:511)
E/AndroidRuntime(22035): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
E/AndroidRuntime(22035): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
E/AndroidRuntime(22035): at dalvik.system.NativeStart.main(Native Method)

为什么说奇怪?一般地,java.lang.VerifyError 是说 JVM 在加载一个类时,会去校验类的正确性,只有类文件不合法才会报这个Error。
比如,一个类试图extends一个标记为final的类,或者试图override final方法(发生在外部依赖类改变声明且应用没有完整重新编译的情况下)。
Android 中会发生这种情况的,一般是需要兼容API的时候,比如用到了高版本SDK中有的类,低版本没有,或者使用高版本API中有低版本没有的方法。

然而这个FileUtils类在com.sample.App中使用时候并没有用到与Android 版本相关的兼容性方法。

百思不得其解

Debug,有时候看堆栈是不够的,还需要查看Logcat中一些有用的上下文

W/dalvikvm(22035): VFY: unable to resolve static method 13457: Landroid/system/Os;.stat (Ljava/lang/String;)Landroid/system/StructStat;
W/dalvikvm(22035): VFY: unable to resolve exception class 1594 (Landroid/system/ErrnoException;)
W/dalvikvm(22035): VFY: unable to find exception handler at addr 0xe
W/dalvikvm(22035): VFY: rejected Lcom/sample/FileUtils;.getUid (Ljava/lang/String;)I
W/dalvikvm(22035): VFY: rejecting opcode 0x0d at 0x000e
W/dalvikvm(22035): VFY: rejected Lcom/sample/FileUtils;.getUid (Ljava/lang/String;)I
W/dalvikvm(22035): Verifier rejected class Lcom/sample/FileUtils;

Log也似乎与平常使用高版本SDK类时的兼容性警告类似:

W/dalvikvm(22524): VFY: unable to resolve virtual method 684: Landroid/content/res/Resources;.getColor (ILandroid/content/res/Resources$Theme;)I
W/dalvikvm(22524): VFY: unable to resolve virtual method 686: Landroid/content/res/Resources;.getColorStateList (ILandroid/content/res/Resources$Theme;)Landroid/content/res/ColorStateList;
W/dalvikvm(22524): VFY: unable to resolve virtual method 693: Landroid/content/res/Resources;.getDrawable (ILandroid/content/res/Resources$Theme;)Landroid/graphics/drawable/Drawable;

回到崩溃开始的警告,”unable to resolve static method” 这条日志应该不会是导致VerifyError的元凶。(注:出现这个警告意味着你如果运行时用到了这个方法,运行时将会报错,如InstantiationError、NoSuchMethodError之类)

那么应该是关键的一句:”unable to find exception handler at addr 0xe“,导致后面的”rejected Lcom/sample/FileUtils;.getUid (Ljava/lang/String;)I” 并最终导致”Verifier rejected class Lcom/sample/FileUtils;”

仔细查看 FileUtils 这个类里的方法getUid()是否有try-catch代码块:

1
2
3
4
5
6
7
8
9
10
11
@TargetApi(21)
public static int getUid(String path) {
if (Build.VERSION.SDK_INT >= 21) {
try {
return Os.stat(path).st_uid;
} catch (android.system.ErrnoException e) {
return -1;
}
}
return -1;
}

确实有尝试catch一个低版本不存在的Exception,但问题在于这个方法并没有使用到!!
而且看起来也十分的正常,一般兼容老版本SDK不都是这样的写法吗?为何单单这里会导致FileUtils类“不合法”?

为了证明是这个在低版本不存在的Exception导致的,对该方法里的try-catch做了简单的处理:

1
2
3
4
5
try {
return Os.stat(path).st_uid;
} catch (Exception e) {
return -1;
}

不出所料,警告只剩下了VFY: unable to resolve static method 13457: Landroid/system/Os;.stat (Ljava/lang/String;)Landroid/system/StructStat;而且没有导致VerifyError

what ??????

想必看到现在的你也是一脸问号……

在启动zsh时自动加载node

折腾之心不死

如果你关注本人 blog 比较久的话,你会发现界面又变了~~ 那是因为从用了许久的 octopress 迁移到 hexo 了。

其实很早就把玩过 node.js,当时用的 nvm 来安装的node
瞎折腾,竟是安装到了一个非推荐目录(推荐的目录是~/.nvm),导致使用 hexo 时发生了一些奇异的事:比如,重启 zsh 后执行hexo就提示 zsh: command not found: hexo,又或是执行npm zsh: command not found: npm

nvm如何下载使用就不多说了,本文主要讲述一下如何解决“启动zsh时自动加载node”。

如果你了解 zsh 的话,你应该知道它可以在启动时加载插件 (plugins)。

简而言之就是在 ~/.zshrc 中的 plugins=(foo) 一行,添加上你需要加载的插件。

插件目录一般是: ~/.oh-my-zsh/plugins/<foo>
插件文件名一般是:~/.oh-my-zsh/plugins/<foo>/<foo>.plugin.zsh
如 git ~/.oh-my-zsh/plugins/git/git.plugin.zsh

我们要做的就是在 ~/.oh-my-zsh/plugins 目录下添加一个自己的插件

添加插件 nvm_auto_load

到插件目录下

1
cd ~/.oh-my-zsh/plugins/

创建 nvm_auto_load 目录

1
mkdir nvm_auto_load

将初始化 nvm 写入 nvm_auto_load.plugin.zsh

1
2
3
4
if [ -z "$NVM_DIR" ]; then
NVM_DIR="$HOME/.nvm"
fi
[ -s "$NVM_DIR/nvm.sh" ] && source $NVM_DIR/nvm.sh

一点小小的经验: Android 上的「安全音量」

一个小问题

在做Android MediaPlayer相关的开发时,可能都会遇到一个需求:自定义音量控制条,如触摸屏幕某块区域则随之调整媒体播放音量并显示自定义的提示条。

设置音量的代码一般形如下:

1
2
AudioManager audioManager = (AudioManager)context.getSystemService(AUDIO_SERVICE);
audioManager.setStreamVolume(STREAM_MUSIC, newIndex, 0); // flags 为0因为我们要自己做提示

但当 Android 设备插上耳机,为了避免音量过高伤害用户听力,会触发其“安全音量”(Safe Media Volume)机制,如果在未经用户确认允许使用大音量时,且这时设置音量newIndex超过其推荐阈值,则这段代码执行完你会发现毫无反应,播放的声音依然不会很大。

如何处理?

其实很简单,关键在于往往被人忽略的最后一个参数 flags

只要在设置音量后,复查一次当前值是否相当,如果比较小,则交由系统来显示音量提示对话框。而此时因欲设定的值超过推荐值,一般会触发「音量过高警告」提示用户,用户确认后即可设置成功。

代码如下:

1
2
3
4
audioManager.setStreamVolume(STREAM_MUSIC, newIndex, 0);
if (Build.VERSION.SDK_INT >= 18 && audioManager.getStreamVolume(STREAM_MUSIC) < newIndex) {
audioManager.setStreamVolume(STREAM_MUSIC, newIndex, FLAG_SHOW_UI);
}

效果如图:(用哔哩哔哩Android 客户端播放视频时滑动屏幕调整音量)

Safe media volume warning dialog

ps:这个对话框实现各个ROM厂商不一定一致。

pps:这个对话框原生Android M 只会在20小时内提示一次,如果你点过了确定。

下面为废话时间。

瞎拍(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’…

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