探索 pointerIndex out of range

最近 App 里报了一个奇怪的 Exception。为什么说奇怪呢,因为它发生的概率不是很大,但每天总有那么几个。这种非必现的问题,解决起来最是麻烦。查了下代码,发现是 event.getY() 报错,我刚看到的反应是:纳尼,这里都能报错?网上查找了下解决方案,大致都是在 onTouchEvent 里面或外面包一层 try…catch。能解决问题,可惜不够优雅。 于是花了些时间研究下。

1
2
3
4
5
6
7
java.lang.IllegalArgumentException: pointerIndex out of range
at android.view.MotionEvent.nativeGetAxisValue(Native Method)
at android.view.MotionEvent.getY(MotionEvent.java:1994)
at com.kay.example.DemoView.onTouchEvent(HomeView.java:184)
at android.view.View.dispatchTouchEvent(View.java:7714)
at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2210)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:1945)

探索

这种非必现的问题,第一步要找到能重现问题的场景。于是我就拼命地戳那个报错的 View,在不懈努力之下,还真重现了几次。异常的栈跟上面是一致的,看来 event.getY() 真能出错。
如果 event.getY() 这种基础的方法都能出错,那么系统的控件是如何防止这个错误的呢。
查看了 NestedScrollView 的源码,发现它的 onTouchEvent 的处理果然是有门道的。

如何在非 UI 线程截图

截图是一个很常见的需求,但网上常见的截图方法都是在主线程运行的。而这有一个隐患,就是卡。因为截图是通过调用 view.draw(canvas),而这就会阻塞主线程的绘制流程引起卡顿。

我司的 App,为了解决卡顿的问题,是在后台线程进行截图的,同样也是调用 view.draw(canvas) 方法,如下方代码所示。第一次看到时挺讶异的,draw 还能在后台线程运行而不报错?介于使用该方法时一直很安全,从未报错,也就默默收起这个疑问。

Hexo 博客加速实践

最近打开博客的速度特别特别慢。慢到怎样的程度呢?打开篇文章要花 60s 以上,还经常死机。虽然博客没什么人看,但作为本博客最忠实的用户,实在不能忍。就花了点时间排查原因,优化速度。

排查

由于博客使用 Hexo 托管在 Github Pages 上,都是些静态文件的显示,速度慢的原因大致就是资源的加载速度问题。使用 Chrome Console 的 Network 工具,发现有一个发送到 fonts.useso.com 请求持续了几十秒。
然后在 Hexo 相应的 Theme 文件下使用grep -rI fonts.useso.com . 搜索相关字符串,发现了相关的 css 文件。

1
2
font-title = "Lato", Helvetica Neue, Helvetica, Arial, sans-serif
@import url("//fonts.useso.com/css?family=Lato:400,400italic")

当初搭建博客的时候,为了提升加载速度将字体源从 google fonts 替换为 360 提供的前端公共库 CDN,也就是 fonts.useso.com。没想到现在变得这么不稳定。搜了下国内没有好的替换源,再加上 Lato 字体也就一处使用,索性删了。
再次打开 kaywu.github.io,发送到 fonts.useso.com 的请求没有了,速度果然快了不少。
但是,这还不够。

从代码创建 Shape Drawable

平时一直是用 xml 来写 Shape Drawable,但由于这次的背景是会根据状态变化不同的颜色,xml 就不能满足要求了。得从代码来构造相关的 Drawable 对象。

相关 xml 代码如下:

1
2
3
4
5
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<corners android:radius="10dp"/>
<stroke android:width="2px" android:color="#ff000000"/>
</shape>

查看有个相关类叫 ShapeDrawable,根据里面的 Api 实现了类似的效果。

1
2
3
4
5
6
7
8
float radius = DensityUtil.dip2px(this, 10);
float[] outerR = new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
RoundRectShape rr = new RoundRectShape(outerR, null, null);
ShapeDrawable drawable = new ShapeDrawable(rr);
drawable.getPaint().setColor(0xff333333);
drawable.getPaint().setStyle(Paint.Style.STROKE);
drawable.getPaint().setStrokeWidth(2);
code.setBackgroundDrawable(drawable);

除了代码上相对繁琐之外,还有一点,ShapeDrawable 对 StrokeWidth 的处理不够完善,在边缘处的 Stroke 只显示了一半。

Single Number

Single Number

Given an array of integers, every element appears twice except for one. Find that single one.

Note:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

题目很简单,但有难度的是不使用额外空间。也就是说要使用一些比较 trick 的方法。
这让我想到以前碰到的一题,交换 Array 中两元素的位置,同样不使用额外空间,最后解法是使用 bitwise operation。
按照这思路去解题的话,很快就想到 xor 运算,相同得 0,不同得 1,题目就引刃而解了。

Counting Bits

Counting Bits
Given a non negative integer number num. For every numbers i in the range 0 ≤ i ≤ num calculate the number of 1’s in their binary representation and return them as an array. For num = 5 you should return [0,1,1,2,1,2].
给定一个非负整数 num,返回一个数组,数组中的值为 0 到 num 间相对应整数其二进制表示中 1 的个数。举例来说,若 num 为 5,则返回的数组为 [0,1,1,2,1,2]。

Android 性能优化实践小记

说来惭愧,看过那么多 Android 性能优化的文章,但这块实战经验微乎其微。抱着已经看过猪跑了,怎么着也得吃上猪肉的心态,尝试去解决某页面的卡顿。实践下来觉得是一次不错的经验,写文章记录下。

发现卡顿

在解决卡顿之前,你得发现哪里卡顿。这里多亏了 BlockCanary,它是 Android 上一个性能监控组件,能很方便地发现卡顿的原因。
下面是 BlockCanary 显示卡顿信息的截图。

可以看出,卡顿是由于 DateHelper.parseString 而引起的。
那接下来让我们看看 parseString 的具体代码。

1
2
3
4
5
6
7
8
9
public static Date parseString(String dateString) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());

try {
return dateFormat.parse(dateString);
} catch (ParseException e) {
return null;
}
}

很简单的一段代码,将规定格式的 String 转换为 Date 对象,简单到貌似没有可以出错及优化的空间。
至此我们陷入了一个死胡同。我们查找到了疑似卡顿的原因,但是无法确认也不知如何优化。

Android 存储笔记

一直搞不清 Android 常用文件夹的位置,做笔记整理下。顺便记录下自己在存储文件时踩的坑。
以包名为 com.kay.example 为例。

内部储存

1
2
3
context.getCacheDir /data/data/com.kay.example/cache
context.getFileDir /data/data/com.kay.example/files
context.getDir /data/data/com.kay.example

外部存储

1
2
3
4
context.getExternalCacheDir /sdcard/Android/data/com.example.kay/cache
context.getExternalFilesDir /sdcard/Android/data/com.example.kay/files
Environment.getExternalStorageDirectory /sdcard/
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES) /sdcard/Movies

Fragment 源码笔记

说来惭愧,用了 Fragment 这么久却没怎么看过它的源码。原因么,一个字懒。每次碰到疑难杂症,Copy and Paste from StackOverflow。前几天看了通过源码解析 Fragment 启动过程,跟着流程走了遍,颇有体会,下面就简单记录下几个要点。

Transaction commit 是异步操作

1
getSupportFragmentManager().beginTransaction().add(R.id.container, fragment).commit();

上面是常常使用的一段代码。beginTransacton() 返回的是 BackStackRecord。BackStackRecord 的 commit() 最终调用的是 mHandler.post()post() 中的操作不是同步执行的,而是在 UI 线程的 Handler 队列的尾部被提交。

Fragment 的状态同步

依旧是上面那句代码,beginTransaction.add(R.id.container, fragment) 最终会调用 mManager.addFragment(f, false)。第二个参数含义为 moveToStateNow,表示是否立即变更 fragment 的状态。由于该参数为 false,因此在 addFragment 时,fragment 不会立即更变其状态,也就意味着 fragment 的回调函数,如 onAttach、onCreate 不会被调用。但 addFragment 会将 fragment 添加到 FragmentManagerImpl 的 mAdded、mActive 队列中。
当下一次 FragmentManagerImpl 的 moveToState(int, boolean) 被调用时,一般发生在 Activity 调用 FragmentManagerImpl 相关的方法时,如 Activity onCreate 时会调用 FragmentManagerImpl 的 dispatchCreate 方法,FragmentManagerImpl 会遍历 mActive 队列并调用 moveToState(f, newState, transit, transitStyle, false),使 fragment 的状态和最新的状态同步。

参考