瞎拍(4)

老家门口的油菜花开的正盛,花丛中许多蜜蜂穿梭,忍不住追着瞎拍了几张 (´・ω・`)

✿

✿

✿

✿

✿

一个小插件解决组件化引发的DEX字段数爆炸的问题

TL;DR

插件名:shrinker

项目地址: https://github.com/yrom/shrinker(其实很早之前就已经发布到github上了,不过无人问津→_→)

插件效果:与removeUnusedCode 同用可以起到最佳效果

这里有一个简单的测试项目,大部分类来自于依赖的support库,结果如下:

选项methodsfieldsclasses
原始项目22164143672563
应用shrinker 插件2197978052392
应用shrinker 并开启 removeUnusedCode1133533021274

如果应用于依赖众多的大型项目则效果惊人。

ps. 其实已经在 b站的APP 上使用很久了,插件稳定、可靠且无副作用。

原理

不论组件化或者说模块化,都有个核心思想:拆分,拆成一个又一个独立的Library。

拆分 Library 引入的问题

举个例子

现一个 APP,它为了实践组件/模块化,拆分出了 common-ui ,business-a, business-b… 依赖关系如下图所示:

R 文件生成的大致流程如下图:

其中processReleaseResources 实际是调用的 aapt工具来给每个依赖的Library都生成一个最终确定的R.java

可想而知,第一个问题:** 拆分的Android Library越多,R 文件越多! **

然而,Library 的 R 文件只会在最终编译成 APK 时确定字段常量值,输出 aar 时只有一个R.txt用于记录声明的资源。

假设 common-ui 声明了15个公共drawable资源,则生成的 R 文件中将有 15个相关的用于记录的字段,而且每个依赖于它的上层的library 生成的R都会有这15个同名的字段,如下图:

由此可得,第二个问题:** 越底层的依赖所声明资源越多,最终生成的 R 文件越庞大 ! ** 因为这些字段没有得到有效内联,最终生成的DEX字段数就会严重超标。

为了解决组件/模块化进程中出现的上述两个问题,shrinker 应运而生。

《Android Kotlin 指南》中译版

虽然很可惜没有在Kotlin 刚发布的时候跟进学习,现在也还不晚~

官方很早在 Github 上发布了一个名为 android kotlin guides 的网站供 Android 开发者学习使用,但似乎没有中文版(至今没找到╮( ̄▽ ̄)╭)。

于是乎,自己动手丰衣足食。也就几个页面,将它们都翻译了一下~

链接:https://yrom.github.io/kotlin-guides/
项目:https://github.com/yrom/kotlin-guides/

又试着将一个小工具用kotlin 重写了一遍,用以实践kotlin:https://github.com/Bilibili/xpref

That’s ALL.

瞎拍(3)

又闲来无事到植物园瞎拍了几张,也不枉这秋高气爽ヽ(゚∀゚*)ノ

✿

✿

✿

✿

Re: 用Go实现渠道打包工具walle

Walle,是美团点评技术团队针对 Android Signature V2 Scheme 签名过的APK的一个多渠道打包工具

近来恰巧在学习Go,就想着把它“翻译”成Go,权当作练手,同时还能给“包管理后台”用用。

Walle 的工作原理

想要把一个工具用另一种语言实现,首先得明白它的原理。

其实原理官方已经解释的很详细了,这里就不再详细缀述,主要列出一些关键的点:

APK 签名的本质

APK 文件其实是一个 ZIP(Jar包),所谓签名其实就是对包或者包中的文件做签名

V1 其实就是 Jar 签名,“签名”要保护的是包中的文件不被修改,并不保护包本身(如ZIP Comment);V2 则是针对V1的缺陷,在V1的基础上将包本身视为Blob再做一次签名。

这就要求 V2 需要在不破坏、不影响 ZIP 文件格式(能被普通的ZIP工具解压),不污染V1签名正确性(APK能被老Andriod系统识别)的前提下写入到包中。
APK 签名验证过程
(图来自https://source.android.com/security/apksigning/)
故而需要针对性的在不伤害 ZIP 文件格式的前提下做一些“魔改”,将 V2 签名数据块插入到 ZIP 的 Central Directory 之前。
APK V2 签名之前与之后

V2 签名之后的APK

APK 被分为 4 部分:

  1. Contents of ZIP entries (从文件头开始直到 APK Signing Block)
  2. APK Signing Block
  3. ZIP Central Directory
  4. ZIP End of Central Directory

1、3、4的内容受到APK Signing Block 保护,准确的说是受 APK Signing Block 中的签名信息保护

APK Signing Block 格式

  • Signing Block 字节数 (不含自身计数) (uint64)
  • “ID-value” 对序列:
    • 此“ID-value” 长度 (uint64)
    • ID (uint32)
    • value (可变长度: “ID-value” 长度 - 4个字节)
  • Signing Block 字节数 (与前面的数值相同) (uint64)
  • magic “APK Sig Block 42” (16 个字节)

为向后兼容考虑,ID-value对设计成了可以存在多个。

V2 签名是数据块中的一个ID-value对,其ID0x7109871a,值即为“签名信息”(signed data)。

在验证 V2 签名过程中,会跳过验证器不认识的 ID

插入渠道信息

从上面的原理可以得到结论APK Signing Block中可以增加一个ID-value来存储渠道,且不会破坏原有签名信息,即毋须重签名做到插入渠道

并发与并行之我见(concurrency vs parallelism)

引子

近来把玩了Java 8 的一些特性,如Lambda 表达式Stream,实在过瘾,有初学编程之快感,相见恨晚之情且按下不表。

其中 Stream 有一个parallelStream()的API则着实勾起了我的好奇心,parallel(译过来是“并行”)这个词于我而言可谓陌生,在不算深入的使用和品味一番后,记以本文。本人才疏学浅,如有谬误之处,请不吝指教。

咬文嚼字

在 Android 的 Java 代码世界里并发(concurrency)这个词比较常见,而并行(parallelism)这个词则少之又少,且常常被人与前者混为一谈。实质上,这两个词是完全不同的概念。

援引Java官方的文档中定义(翻译可能不够准确,建议看原文):

parallelism 译为并行,指至少两个线程同一时间在运行

concurrency 译为并发,指存在至少两个线程在执行着任务。更为一般性的并行,以时间分片作为执行单元的虚拟的并行。

我的理解:

首先得跟官方文档一样明确一个词义范围,否则就没有一点“可比性”了,提到这两个词多用其程序上执行“任务”层面的含义。

并行,就是字面意思,同一时刻执行多个任务。指执行程序代码的机器的物理上的一种能力,各个任务如同平行世界般的,形式上可以是多线程乃至多进程,被多个CPU同时运作。任务之间无任何依赖关系,不互斥、不交叉。

并发,指程序逻辑上的一种能力,支持多个任务同时存在一起被处理的能力。一个任务通常被划分为一个个的时间分片,比肩接踵般轮流的、可交替切换的被一个或多个CPU所执行,即不同任务的时间分片之间会有互斥、等待,一个任务可能因为CPU资源被抢走而被中断执行。

举个简单例子,单核CPU的机器,执行多线程化处理过的Java程序,一样称该程序支持并发,但它囿于机器性能永远做不到并行。

所提的并发和并行的概念都是为了最大化的利用多核CPU的能力,从而加速程序的执行。从这点来讲,那些将两个词混为一谈的人多半为一知半解的(看到这里的你已经不是了~)。

瞎拍(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小时内提示一次,如果你点过了确定。

下面为废话时间。