ArrayMap-synchronization-problem

一个比较难搞的Bug,有遇到的可以参考一下
难搞的原因和以前一样, 如果一个bug出现在google issue里 又没有 给出 workaround,那基本就没救了

错误输出为 java.lang.String cannot be cast to java.lang.Object[]

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Object[]
at android.util.ArrayMap.allocArrays(ArrayMap.java:187)
at android.util.ArrayMap.put(ArrayMap.java:459)
at android.app.ActivityThread.handleCreateService(ActivityThread.java:2913)
at android.app.ActivityThread.access$1900(ActivityThread.java:165)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1459)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:150)
at android.app.ActivityThread.main(ActivityThread.java:5621)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:794)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:684)

错误可能发生在任何使用ArrayMap的任何地方,比如Fragment,Intent,Bundle
Android Framework中大量使用ArrayMap存储,所以你自己不使用也仍有可能遇到此问题

目前已经确认是Android系统本身的一个Bug,相关issue可以参考
https://issuetracker.google.com/issues/37114373

还有一篇详细分析给出了具体错误原因 和 重现路径
https://gist.github.com/LanderlYoung/579872afffc62a5f837e654a6f1eab89

不想看详细内容的话也可以简单理解为 ArrayMap 存在线程安全问题

那么理所当然的,不要主动在backgroud thread中使用ArrayMap,确保都在主线程中使用就不会有问题

但是,管得了自己的代码,管不了第三方SDK的代码,在我根本没有使用过ArrayMap的情况下,错误以极低概率发生
每天umeng都能抓到若干crash记录

首先第一步是定位问题
现在我已经知道了,很可能是线程同步问题,但是由于我自身没有使用ArrayMap,所以很可能是某个第三方库在使用造成的问题

准备一个原生系统(自己手机刷一个原生系统 或者 模拟器),然后在Android Studio中使用对应的版本的系统源码,就可以进行源码断点了
第三方ROM由于代码行数 和 源码不一致,一般是很难断点的

我这里主要是定位问题,所以在 android.util.ArrayMap.put 上进行了条件断点(在断点上点右键 输入 condition)

condition : Log.e(“######”,Thread.currentThread().toString())==0

中断不会发生,但是会输出所有 调用android.util.ArrayMap.put的线程

结果如下
img

可以看到除了主线程外 还有另外2个线程在使用ArrayMap

移除condition看了一下调用栈
可以看到其中一个是
run:124, l$1 (com.tencent.beacontsa.core.eve (广点通SDK里带的东西)

另外一个是 thead-pool 是 umeng 在使用

目前初步的解决方案是 把umeng event统计都停了(本来也没在用,实质上数据都记在GA)
广点通BannerAD 延迟 5秒加载

gradle 2.2.0 with build in assets(baidu ad sdk)

前几天给一个老项目发版时,升级gradle到2.2.2,发现baidu广告SDK一直无法正确加载.
提示 load xadsdkremotefinal.jar failed

切换回 com.android.tools.build:gradle:2.2.0-alpha4 后恢复正常.

但是使用老版本gradle编译会一直失败并提示

1
2
Error:(1, 0) Plugin is too old, please update to a more recent version, or set ANDROID_DAILY_OVERRIDE environment variable to "245e4d403bf33d23690fe32bf814235bf6949f57"
<a href="fixGradleElements">Fix plugin version and sync project</a><br><a href="openFile:/Users/dk/Documents/git/***(****/******/build.gradle">Open File</a>

虽然设置ANDROID_DAILY_OVERRIDE后暂时可用,但是下次编译时又要重新处理.

稍微看了一下,发现原因是这样
百度广告SDK中有一个build in jar
目录结构大概是这样

Baidu_MobAds_SDK.jar
    - assets
        - `__xadsdk__remote__final__.jar`
    - com.baidu........
        - 若干class
    - META-INF

在使用gradle2.2.0-alpha4时,jar包中的assets会自动merge到apk的assets中,但是升级到2.2.0~2.2.2时,编译出的apk中不再包含__xadsdk__remote__final__.jar

暂时没有看到任何文档或者log中表述 gradle 2.2.0-alpha4 -> gradle 2.2.0 修改了assets的处理方式

目前我的处理方式是,解包Baidu_MobAds_SDK.jar后将 __xadsdk__remote__final__.jar 直接拷贝到工程assets目录下,即可使用最新的gradle正常编译.
当然你也可以回退到alpha4/rc1

顺便,这事有1个月了,也不见百度的集成文档更新一下.

Android 7.0 new APK Signature Scheme v2 problem with multiply channel packer

Android 7.0 中新增了 APK Signature Scheme v2 签名方式

如果Android Studio升级到 v2.2+,构建APK时默认使用的签名方式就是APK Signature Scheme v2

目前比较流行的2套 多渠道打包脚本

  • 在APK内注入${channel}.txt 文件
  • 在APK的zip info中写入 channel 信息

实质上都会在签名后修改APK文件,目前都会造成 签名认证失败
失败信息如下:

1
2
Yuki-Android git:(feature/small-fix) adb install /Users/dk/Documents/yuki-release/v3.1.8/Yuki-base-Direct.apk
Failed to install /Users/dk/Documents/yuki-release/v3.1.8/Yuki-base-Direct.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Failed to collect certificates from /data/app/vmdl453897674.tmp/base.apk: META-INF/CERT.SF indicates /data/app/vmdl453897674.tmp/base.apk is signed using APK Signature Scheme v2, but no such signature was found. Signature stripped?]

在非7.0设备上可以正常安装,在7.0设备上则无法通过签名认证.
现在可以通过在build.gradle中手动关闭APK Signature Scheme v2来避免此问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
signingConfigs {
release {
storeFile file("xxxx")
storePassword "xxxx"
keyAlias "xxxx"
keyPassword "xxxx"
v2SigningEnabled false
}
debug {

storeFile file("xxxx")
storePassword "xxxx"
keyAlias "xxxx"
keyPassword "xxxx"
v2SigningEnabled false
}
}

目前暂时这样,考虑到以后v2签名可能会变成一个强制配置,可能需要等APK Signature Scheme v2具体文档和认证方式公布后,更新一下现在的多渠道打包脚本

Receiver not registered: android.widget.ZoomButtonsController crash

#好吧,继续整理 最近一段时间遇到的比较离奇的Bug

这个Bug log是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java.lang.IllegalArgumentException: Receiver not registered: android.widget.ZoomButtonsController$1@487a4290
at android.app.ActivityThread$PackageInfo.forgetReceiverDispatcher(ActivityThread.java:793)
at android.app.ContextImpl.unregisterReceiver(ContextImpl.java:913)
at android.content.ContextWrapper.unregisterReceiver(ContextWrapper.java:331)
at android.widget.ZoomButtonsController.setVisible(ZoomButtonsController.java:404)
at android.widget.ZoomButtonsController$2.handleMessage(ZoomButtonsController.java:178)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:123)
at android.app.ActivityThread.main(ActivityThread.java:4627)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:521)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:858)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
at dalvik.system.NativeStart.main(Native Method)

因为这个Log Trace里没有任何我的代码,所以感觉上和我的应用没什么关系,而且也看不出是在哪个界面哪个地方发生的,当时也没办法复现.
只是在Umeng的后台会不断收集到这样的crash log,不多,但是偶尔就会有那么几个.
后来花时间研究了一下,发现应用中只有Webview中有这个东西.

这样有针对性的来测试,很快就找到了重现路径. zoom controller有个默认的渐隐动画,只要在动画进行重退出Activity,就会重现这个crash

同时也找到了几个相关issue:
https://code.google.com/p/android/issues/detail?id=15694
http://stackoverflow.com/questions/5267639/how-to-safely-turn-webview-zooming-on-and-off-as-needed
http://stackoverflow.com/questions/4908794/webview-throws-receiver-not-registered-android-widget-zoombuttonscontroller

解决方案是各种hack

  1. 如果不需要直接禁用即可
  2. 重载webview,destroy的时候处理zoomButtonController
  3. 重载Activity,延迟2秒destroy webview
  4. finish之前把webview及所有子view从rootview里remove掉. (这个没试过)

java.lang.IllegalStateException: package not installed

#java.lang.IllegalStateException: package not installed

用了Android Studio的时候偶尔会遇到这个问题.

ref: http://stackoverflow.com/questions/24426635/caused-by-java-lang-illegalstateexception-package-not-installed
ref: http://stackoverflow.com/questions/35675855/android-studio-floatingactionbutton-error

解决方案很简单,gradle重新sync一下就好了.

重现步骤大概是这样,gradle sync/build中,修改lib/compile配置,或者同时在命令行clean build,会导致build过程中某些该编译进去的jar/module丢失,于是运行时会报各种奇怪的Bug.大部分那都是 package not installed / class not found 一类的

这种时候修改一下gradle文件重新sync 就能修复此问题.本质上是IDE/gradle的bug,不知道原因的话解决起来有点蛋疼.

播放网络音频时,MediaPlayer在Prepare阶段访问任何API都会造成主线程阻塞

#播放网络音频时,MediaPlayer在Prepare阶段访问任何API都会造成线程阻塞

播放本地音频时,prepare阶段很短,基本不会发生这种情况,但是播放网络音频时,无论是使用prepare() 还是 prepareAsync()
MediaPlayer都会进入lock状态,此时调用MediaPlayer的任何API,比如stop/release/reset时,都会直接阻塞调用的线程,直到Prepare完成或失败.应用会抛出ANR.

当时文件服务使用的是七牛,据观察,七牛CDN在我这里似乎有点问题,请求成功率大概只有95%左右,在糟糕的网络环境下,这个问题更容易出现

这个问题比较蛋疼,由于当时需求是需要在前台播放一段网络音频,不需要后台播放,所以MediaPlayer对象直接由前台持有,切换音频时,不销毁MediaPlayer,而是stop() & reset()以后重新Prepare().

但是发现用户快速切换音频时,有时会发生anr,抓了一些anr log以后发现,anr竟然是在stop()release()上发生的.

尝试过新建Thread去prepare,也试过prepareAsync(),都不解决问题,阻塞是在调用stop/reset时发生的.

相关的Google issue : https://code.google.com/p/android/issues/detail?id=959

最后的我的解决方案是,将MediaPlayer做一下包装,prepare完成前不允许访问其任何方法(比如 stop getDuration release reset …),需要访问的时候,比如退出界面的时候Stop/release 单独开一个Thread来call这些方法,这样也只会阻塞一个异步线程.

这样配置以后基本解决了该问题

Android中如何获取Secondary External Storage路径(android secondary sdcard setting)

前置条件

当Android同时拥有内置存储区和外置SD卡时,外置存储卡会被识别为 Secondary External Storage.
本文讨论的是该情况下 Secondart External Storage的识别和读写问题,如果你没有SD卡,或没有内置存储区的设备,只要简单使用Environment.getExternalStorageDirectory() 就好了

  • 如何获得外置SD卡路径

Environment.getExternalStorageDirectory() 获得的只是系统逻辑上的存储区,在目前的大部分手机上,返回的其实是内置存储区路径
Google也没有提供一个明确的方法来或者外置存储卡路径.

1.对于Android 4.4 及以上设备使用 Application.getExternalFilesDirs(null),然后返回的数组中就包含内置存储区路径和SD卡路径,简单判断一下就好了.
需要注意的是,返回的路径应当是 /storage/sdcard1/Android/data/your.packagename 的一个路径,在SD卡内,只有该路径下应用是拥有写权限的,其它路径下你都只有只读权限.
相关文档请参考这个:
https://developer.android.com/reference/android/content/ContextWrapper.html#getExternalFilesDirs(java.lang.String)

简单来说,4.4以后你就不能在SD卡上瞎几把放东西了,只能存储在系统给你指定的地方

  1. 对于低于4.4版本的设备,只有一些歪门邪道的方法

一种方式是可以通过mount命令的返回来搜索SD卡路径,不是特别靠谱
相关代码可以参考 http://stackoverflow.com/questions/5694933/find-an-external-sd-card-location/9406276#9406276
但是请注意,我看到过4~5种不一样的匹配方法,感觉和manufacture关系很大,国内瞎几把定制ROM不确定性更大,请配合File.canRead()验证使用.

代码贴一下:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
   import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStreamReader;

import android.util.Log;


/**
* @author ajeet
*05-Dec-2014 2014
*
*/

public class StorageUtil {

public boolean isRemovebleSDCardMounted() {
File file = new File("/sys/class/block/");
File[] files = file.listFiles(new MmcblkFilter("mmcblk\\d$"));
boolean flag = false;
for (File mmcfile : files) {
File scrfile = new File(mmcfile, "device/scr");
if (scrfile.exists()) {
flag = true;
break;
}
}
return flag;
}

public String getRemovebleSDCardPath() throws IOException {
String sdpath = null;
File file = new File("/sys/class/block/");
File[] files = file.listFiles(new MmcblkFilter("mmcblk\\d$"));
String sdcardDevfile = null;
for (File mmcfile : files) {
Log.d("SDCARD", mmcfile.getAbsolutePath());
File scrfile = new File(mmcfile, "device/scr");
if (scrfile.exists()) {
sdcardDevfile = mmcfile.getName();
Log.d("SDCARD", mmcfile.getName());
break;
}
}
if (sdcardDevfile == null) {
return null;
}
FileInputStream is;
BufferedReader reader;

files = file.listFiles(new MmcblkFilter(sdcardDevfile + "p\\d+"));
String deviceName = null;
if (files.length > 0) {
Log.d("SDCARD", files[0].getAbsolutePath());
File devfile = new File(files[0], "dev");
if (devfile.exists()) {
FileInputStream fis = new FileInputStream(devfile);
reader = new BufferedReader(new InputStreamReader(fis));
String line = reader.readLine();
deviceName = line;
}
Log.d("SDCARD", "" + deviceName);
if (deviceName == null) {
return null;
}
Log.d("SDCARD", deviceName);

final File mountFile = new File("/proc/self/mountinfo");

if (mountFile.exists()) {
is = new FileInputStream(mountFile);
reader = new BufferedReader(new InputStreamReader(is));
String line = null;
while ((line = reader.readLine()) != null) {
// Log.d("SDCARD", line);
// line = reader.readLine();
// Log.d("SDCARD", line);
String[] mPonts = line.split("\\s+");
if (mPonts.length > 6) {
if (mPonts[2].trim().equalsIgnoreCase(deviceName)) {
if (mPonts[4].contains(".android_secure")
|| mPonts[4].contains("asec")) {
continue;
}
sdpath = mPonts[4];
Log.d("SDCARD", mPonts[4]);

}
}

}
}

}

return sdpath;
}

static class MmcblkFilter implements FilenameFilter {
private String pattern;

public MmcblkFilter(String pattern) {
this.pattern = pattern;

}

@Override
public boolean accept(File dir, String filename) {
if (filename.matches(pattern)) {
return true;
}
return false;
}

}

}

以红米1举例,插入SD卡后,mount返回如下

rootfs / rootfs ro,relatime 0 0
tmpfs /dev tmpfs rw,seclabel,nosuid,relatime,size=437680k,nr_inodes=109420,mode=755 0 0
devpts /dev/pts devpts rw,seclabel,relatime,mode=600 0 0
proc /proc proc rw,relatime 0 0
sysfs /sys sysfs rw,seclabel,relatime 0 0
selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0
debugfs /sys/kernel/debug debugfs rw,relatime 0 0
none /acct cgroup rw,relatime,cpuacct 0 0
none /sys/fs/cgroup tmpfs rw,seclabel,relatime,size=437680k,nr_inodes=109420,mode=750,gid=1000 0 0
none /sys/fs/cgroup/memory cgroup rw,relatime,memory 0 0
tmpfs /mnt/asec tmpfs rw,seclabel,relatime,size=437680k,nr_inodes=109420,mode=755,gid=1000 0 0
tmpfs /mnt/obb tmpfs rw,seclabel,relatime,size=437680k,nr_inodes=109420,mode=755,gid=1000 0 0
none /dev/cpuctl cgroup rw,relatime,cpu 0 0
/dev/block/platform/msm_sdcc.1/by-name/system /system ext4 ro,seclabel,relatime,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/userdata /data ext4 rw,seclabel,nosuid,nodev,relatime,discard,noauto_da_alloc,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/cache /cache ext4 rw,seclabel,nosuid,nodev,relatime,discard,noauto_da_alloc,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/persist /persist ext4 rw,seclabel,nosuid,nodev,relatime,discard,noauto_da_alloc,data=ordered 0 0
/dev/block/platform/msm_sdcc.1/by-name/modem /firmware vfat ro,relatime,uid=1000,gid=1000,fmask=0337,dmask=0227,codepage=cp437,iocharset=iso8859-1,shortname=lower,errors=remount-ro 0 0
/dev/fuse /mnt/shell/emulated fuse rw,nosuid,nodev,relatime,user_id=1023,group_id=1023,default_permissions,allow_other 0 0
/dev/fuse /storage/emulated/legacy fuse rw,nosuid,nodev,relatime,user_id=1023,group_id=1023,default_permissions,allow_other 0 0
/dev/block/vold/179:65 /mnt/media_rw/sdcard1 vfat rw,dirsync,nosuid,nodev,noexec,relatime,uid=1023,gid=1023,fmask=0007,dmask=0007,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0
/dev/fuse /storage/sdcard1 fuse rw,nosuid,nodev,relatime,user_id=1023,group_id=1023,default_permissions,allow_other 0 0

可以看到,以上面的字符串匹配或者stackoverflow上其它几种验证方法(asec / vfat / void),都匹配不到最后那条记录

另一种方法:
读取 /system/etc/vold.fstab

示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try {
Scanner scanner = new Scanner(new File("/system/etc/vold.fstab"));
while (scanner.hasNext()) {
String line = scanner.nextLine();
if (line.startsWith("dev_mount")) {
String[] lineElements = line.split(" ");
String element = lineElements[2];

if (element.contains(":"))
element = element.substring(0, element.indexOf(":"));

if (element.contains("usb"))
continue;

// don't add the default vold path
// it's already in the list.
if (!out.contains(element))
//这里就是你要的SD卡路径了
out.add(element);
}
}
} catch (Exception e) {
e.printStackTrace();
}

还是以红米1为例,贴一下’/system/etc/vold.fstab’的内容

# Copyright (c) 2012, The Linux Foundation. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
#       copyright notice, this list of conditions and the following
#       disclaimer in the documentation and/or other materials provided
#       with the distribution.
#     * Neither the name of The Linux Foundation nor the names of its
#       contributors may be used to endorse or promote products derived
#       from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

dev_mount sdcard /storage/sdcard1 auto /devices/msm_sdcc.2/mmc_host

保险起见,我目前是以上2种方法同时使用,再用file.canRead()检查是否存在

Android-App-ABI-setting

先放结论:
最优性能且不考虑安装包体积
提供armeabi,armeabi-v7a,arm64-v8a,x86_64

全适配+最小体积
提供armeabi,armeabi-v7a

最小体积+99%适配
提供armeabi-v7a


首先官方的ABI说明
ref: https://developer.android.com/ndk/guides/abis.html

理想情况下,App应当同时提供armeabi,armeabi-v7a,arm64-v8a,x86,x86_64,mips,mips64的so文件
一些比较小的类库提供全部的so以保证在全平台能以最优性能运行,是没有问题的

但是有时候会有一些较大的类库,比如ffmpeg,opencv之类,一个so文件就有3~5mb,全部提供会让APK体积过于膨胀,这个时候就需要进行适当的权衡.


不考虑最优性能的话:

其中 x86(x86_32) 的真实设备实际上并不存在,只有模拟器可以调成x86_32,实际存在的CPU都是x86_64的
并且,所有x86_64的设备都支持Secondary ABI, 大部分配置都是 armeabi/armeabi-v7a , 不考虑性能最优的话,是可以不提供的

mips略过,印象中只有早期起一些android laptop 是这种架构.


这样我们需要实际考虑的ABI只有armeabi,armeabi-v7a,arm64-v8a

考虑最小体积,其实只提供一个armeabi-v7a对于99%的机型就足够了
目前就测试结果看
只有 MX4 / MX4 PRO 的5.1/5.1.1 会无法找到so文件,猜测可能Primary ABI是 arm64-v8a 并且 Secondary ABI是 armeabi
完美的避过了v7a ,当然也有可能是别的原因.目前观察到的只有这款机型有问题


之后因为armeabi(实际上是V5)的设备现在市面上几乎已经没有了,考虑兼容,当时尝试使用了v7a+v8a的so,没有提供armeabi版本的so
大约也能覆盖98%的设备
上线后发现OPPO R7(全系列几乎都有),魅蓝Metal,魅蓝Note,魅族 PRO 5出现大量crash,马上做了hotfix.
从数据看,只发生在 Oppo和魅族上,其它机型完全没有.
我也很郁闷,这几款机型CPU都是 arm64的,反而找不到v8a的so,具体是个什么情况我不是很理解

就最终结果来看,使用v7a就能覆盖99%的设备(除了魅族 MX4 / MX 4 PRO)
为了支持这一设备,再配上个armeabi的so就好了


主要观察的数据是应用在umeng后台的错误统计
以下是一些测试数据

OPPO R7 V5 OK

OPPO R7 v7 OK

OPPO R7 V5+v7 OK

OPPO R7 V5+v7+V8 OK

OPPO R7 v7+V8 CRASH

OPPO R7 5.0 V7+V8 OK

魅蓝note2 V5 CRASH

魅蓝note2 v7 OK

魅蓝note2 v7+v8 OK

魅蓝note2 V5+v7+v8 OK

乐视 V5 CRASH

乐视 v7 OK

乐视 v7+v8 OK

乐视 V5+v7+v8 OK

NEXUS 5X V5 CRASH

NEXUS 5X V7 OK

NEXUS 5X V7+V8 OK

PRO 5 V7 CRASH

PRO 5 V7+V8 CRASH

PRO 5 V5 OK

PRO 5 V5+V7+V8 OK

GALAXY S6 V7 OK

GALAXY S6 V5+V7 OK

最终方案使用 v5+v7,v8a以兼容模式运行32位库
以下设备测试没问题.

OPPO R7 OK

三星 S6 OK

华为 mate S OK

魅蓝note2 OK

魅族 PRO 5 OK

nexus 5X OK

小米4C OK

乐视手机 OK

nexus 5 OK

Mediaplayer ConfigPriority(0x6f800002)) ERROR on Marshmallow

#Mediaplayer ConfigPriority(0x6f800002)) ERROR on Marshmallow

Android 6.0 使用源生MediaPlayer播放小于15s的音频文件时,有可能会抛出此错误 ConfigPriority(0x6f800002))
不会crash,但是没有声音

log:

04-21 16:42:11.787 745-21846/? I/QComExtractorFactory: Sniff: Set key to use qti parser
04-21 16:42:11.787 745-21846/? I/FFmpegExtractor: android-source:0xee977200
04-21 16:42:11.790 745-21846/? E/FFmpegExtractor: android-source:0xee977200: avformat_open_input failed, err:Invalid data found when processing input
04-21 16:42:11.790 745-21846/? W/FFmpegExtractor: sniff through BetterSniffFFMPEG failed, try LegacySniffFFMPEG
04-21 16:42:11.793 745-21846/? E/FFmpegExtractor: android-source:0xee977200|file:: avformat_open_input failed, err:Invalid data found when processing input
04-21 16:42:11.793 745-21846/? D/FFmpegExtractor: SniffFFMPEG failed to sniff this source
04-21 16:42:11.793 745-21846/? I/ExtendedExtractor: QTIParser is prefered
04-21 16:42:11.793 745-21846/? D/MMParserExtractor: setExtraFlags called with flags 3 for paser instance ee977340
04-21 16:42:11.793 745-21846/? D/MMParserExtractor: using bigger parser buffers
04-21 16:42:11.793 745-21846/? I/ExtendedExtractor: ExtendedExtractor::create 0xee977340
04-21 16:42:11.793 745-21846/? I/ExtendedExtractor: ExtendedExtractor::updateExtractor: Use default QTI parser 
04-21 16:42:11.793 745-21846/? D/MMParserExtractor: Overiding Parser out buffer size  = 262144 
04-21 16:42:11.794 745-21845/? D/NuPlayerDriver: notifyListener_l(0xefee20a0), (1, 0, 0)
04-21 16:42:11.796 745-3258/? D/NuPlayerDriver: start(0xefee20a0), state is 4, eos is 0
04-21 16:42:11.796 745-21845/? I/GenericSource: start
04-21 16:42:11.797 745-21845/? I/AudioPolicyManagerCustom: Offload min duration is 15s
04-21 16:42:11.797 745-21845/? I/AudioPolicyManagerCustom: PCM offload property is enabled
04-21 16:42:11.797 745-21845/? I/AudioPolicyManagerCustom: Offload min duration is 15s
04-21 16:42:11.798 745-21845/? I/AudioPolicyManagerCustom: Offload min duration is 15s
04-21 16:42:11.798 745-21845/? I/AudioPolicyManagerCustom: PCM offload property is enabled
04-21 16:42:11.798 745-21845/? I/AudioPolicyManagerCustom: Offload min duration is 15s
04-21 16:42:11.812 745-21850/? E/OMXNodeInstance: setConfig(21f:google.aac.decoder, ConfigPriority(0x6f800002)) ERROR: Undefined(0x80001001)
04-21 16:42:11.812 745-21850/? I/ACodec: codec does not support config priority (err -2147483648)
04-21 16:42:11.813 745-21850/? I/MediaCodec: MediaCodec will operate in async mode
04-21 16:42:11.814 745-21851/? I/SoftAAC2: Reconfiguring decoder: 0->44100 Hz, 0->0 channels
04-21 16:42:11.815 745-21851/? W/SoftAAC2: aacDecoder_DecodeFrame decoderErr = 0x4007
04-21 16:42:11.815 745-21851/? W/SoftAAC2: AAC decoder returned error 0x4007, substituting silence
04-21 16:42:11.816 745-21849/? I/NuPlayerDecoder: [audio] saw output EOS
04-21 16:42:11.816 745-21845/? I/ExtendedNuPlayer: notifyListener 2 0 0
04-21 16:42:11.816 745-21845/? D/NuPlayerDriver: notifyListener_l(0xefee20a0), (2, 0, 0)
04-21 16:42:11.816 745-21848/? W/NuPlayerRenderer: onDrainAudioQueue(): audio sink is not ready

google issue已经确认了此Bug,目前处在assign状态,不过在6.0.1上测试时已经正常.

ref google issue [需翻墙]

从log和更新记录上看,猜测似乎是offload的影响

av.offload.enable=true 
av.streaming.offload.enable=true 
audio.offload.buffer.size.kb=64 
audio.offload.min.duration.secs=30 
audio.offload.gapless.enabled=true 
audio.offload.pcm.16bit.enable=true 
audio.offload.pcm.24bit.enable=true

从源码上看 似乎针对不同长度的音频文件 ,根据offload.enable和offload.min.duration,会有不同的处理方式

Audio Offload 音频分载, 具体代码逻辑没有细看,也不大清楚具体错误原因

目前唯一已知解决方案是,降级到5.0或升级到6.0.1+

CoordinatorLayout中的坑

踩完了坑记录一下,CoordinatorLayout使用介绍请看chrisbanes的cheesesquare

如果你只打算学习一下CoordinatorLayout然后写2个Demo试试,那么本文并没有什么卵用,但是如果你打算在生产环境使用CoordinatorLayout,那么强烈推荐阅读一下本文,可以减少很多弯路,这个东西看起来很好,但是实际上坑也很多。

###前言
很多应用主页常见的构造模式

一个包含ActionBar和Banner的header+ViewPager的组合模式
比如这样:

img

然后需要滚动的时候能够将 ActionBar和Banner滚出界面,但是又需要ViewPager的TabLayout能固定在屏幕顶部
比如滚动后是这样:

img

Github上有很多sticky-viewpager xx-header-viewpager之类的项目提供类似的功能,但是大部分都会存在各种事件冲突问题,比如滑动不流畅,卡顿,弹跳之类的。

好在Android新的SupportLibrary提供了CoordinatorLayout能直接实现类似功能,具体如何使用参考 SupportLibrary中CoordinatorLayout的使用文档即可,例子也可以直接看cheesesquare

我就谈下使用过程中里面的坑:

不支持ListView,不支持ScrollView,低版本不兼容ViewCompat.setNestedScrollingEnabled

PS.更新

support 23.1+新增了

ViewCompat.setNestedScrollingEnabled(listView/gridview,true); 

可以给ListView提供NestedScrolling支持,但是只在LOLIPOP+版本上生效,下面的方案也一样。

低版本只能使用RecyclerView


一些文档/博客文章上说 可以使用一个 可滚动的组件
还有些错误的文章说 支持ListView

实际上该组件必须实现了 NestedScrollingChild接口

你可以使用RecyclerView,RecyclerView自身就实现了该接口,如果你要使用其他组件,那么可能需要继承原来的View并实现NestedScrollingChild接口

一个实现了NestedScrollingChild的Listview demo如下:

public class NestedScrollingListView extends ListView implements NestedScrollingChild {

    private final NestedScrollingChildHelper mScrollingChildHelper;

    public NestedScrollingListView(Context context) {
        super(context);
        mScrollingChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);
    }

    public NestedScrollingListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScrollingChildHelper = new NestedScrollingChildHelper(this);
        setNestedScrollingEnabled(true);
    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mScrollingChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mScrollingChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return mScrollingChildHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        mScrollingChildHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return mScrollingChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                        int dyUnconsumed, int[] offsetInWindow) {
        return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }
}

其他View也可以使用类似的方式实现。

只对 5.0+ 生效,低版本无效。

RecyclerView滑动的时候不流畅

目前已知 r23.1 & r 23.2 都存在此问题,Google code的提交代码里倒是有相关修复记录,但是目前还没有发布。

直接原因是Fling Direction错误,导致Fling事件被吃掉了,ACTION_UP/ACTION_CANCEL事件一旦发生,scroll动作直接停止

StackOverFlow上一个高票解决方案是重写一个FlingBehavior,并配置给AppBar.

注意,这个Behavior是给AppBar的,不是给下面的可滚动组件的。

<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="your.package.FlingBehavior">
<!--your views here-->
 </android.support.design.widget.AppBarLayout>

public class FlingBehavior extends AppBarLayout.Behavior {

    private static final int TOP_CHILD_FLING_THRESHOLD = 3;
    private boolean isPositive;

    public FlingBehavior() {
    }

    public FlingBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
        if (velocityY > 0 && !isPositive || velocityY < 0 && isPositive) {
            velocityY = velocityY * -1;
        }
        if (target instanceof RecyclerView && velocityY < 0) {
            final RecyclerView recyclerView = (RecyclerView) target;
            final View firstChild = recyclerView.getChildAt(0);
            final int childAdapterPosition = recyclerView.getChildAdapterPosition(firstChild);
            consumed = childAdapterPosition > TOP_CHILD_FLING_THRESHOLD;
        }
        consumed=false;
        return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
        isPositive = dy > 0;
    }

注意consumed计算这部分,会影响滚动形式

请参考父类滚动的实现代码:

 @Override
public boolean onNestedFling(final CoordinatorLayout coordinatorLayout,
            final AppBarLayout child, View target, float velocityX, float velocityY,
            boolean consumed) {
        boolean flung = false;

        if (!consumed) {
            // It has been consumed so let's fling ourselves
            flung = fling(coordinatorLayout, child, -child.getTotalScrollRange(),
                    0, -velocityY);
        } else {
            // If we're scrolling up and the child also consumed the fling. We'll fake scroll
            // upto our 'collapsed' offset
            if (velocityY < 0) {
                // We're scrolling down
                final int targetScroll = -child.getTotalScrollRange()
                        + child.getDownNestedPreScrollRange();
                if (getTopBottomOffsetForScrollingSibling() < targetScroll) {
                    // If we're currently not expanded more than the target scroll, we'll
                    // animate a fling
                    animateOffsetTo(coordinatorLayout, child, targetScroll);
                    flung = true;
                }
            } else {
                // We're scrolling up
                final int targetScroll = -child.getUpNestedPreScrollRange();
                if (getTopBottomOffsetForScrollingSibling() > targetScroll) {
                    // If we're currently not expanded less than the target scroll, we'll
                    // animate a fling
                    animateOffsetTo(coordinatorLayout, child, targetScroll);
                    flung = true;
                }
            }
        }

        mWasNestedFlung = flung;
        return flung;
    }

确认你需要滚动哪一步分,你可以简单根据RecyclerView展示的first item的position来判断Recyclerview是否在top位置,也有另一种代码如下:

@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, float velocityX, float velocityY, boolean consumed) {
    if (target instanceof RecyclerView) {
        final RecyclerView recyclerView = (RecyclerView) target;
        consumed = velocityY > 0 || recyclerView.computeVerticalScrollOffset() > 0;
    }
    return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}

另一个解决方案是使用 smooth-app-bar-layout

具体如何使用请参考它自己的文档

当header是Toolbar时,demo运作良好,当headerview高度较大时,滑动会发生剧烈抖动,弹跳,暂时还没去看它源代码,可能是我使用不正确

不支持adjustResize

可以参考这个问题
http://stackoverflow.com/questions/35599125/adjustresize-does-not-work-with-coordinatorlayout

解决方案是可以通过ViewTreeObserver.OnGlobalLayoutListener监听界面改变,然后人工处理padding bottom

public class KeyboardUtil {
    private View decorView;
    private View contentView;

    public KeyboardUtil(Activity act, View contentView) {
        this.decorView = act.getWindow().getDecorView();
        this.contentView = contentView;

        //only required on newer android versions. it was working on API level 19
        if (Build.VERSION.SDK_INT >= 19) {
            decorView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
        }
    }

    public void enable() {
        if (Build.VERSION.SDK_INT >= 19) {
            decorView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
        }
    }

    public void disable() {
        if (Build.VERSION.SDK_INT >= 19) {
            decorView.getViewTreeObserver().removeOnGlobalLayoutListener(onGlobalLayoutListener);
        }
    }


    //a small helper to allow showing the editText focus
    ViewTreeObserver.OnGlobalLayoutListener onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            Rect r = new Rect();
            //r will be populated with the coordinates of your view that area still visible.
            decorView.getWindowVisibleDisplayFrame(r);

            //get screen height and calculate the difference with the useable area from the r
            int height = decorView.getContext().getResources().getDisplayMetrics().heightPixels;
            int diff = height - r.bottom;

            //if it could be a keyboard add the padding to the view
            if (diff != 0) {
                // if the use-able screen height differs from the total screen height we assume that it shows a keyboard now
                //check if the padding is 0 (if yes set the padding for the keyboard)
                if (contentView.getPaddingBottom() != diff) {
                    //set the padding of the contentView for the keyboard
                    contentView.setPadding(0, 0, 0, diff);
                }
            } else {
                //check if the padding is != 0 (if yes reset the padding)
                if (contentView.getPaddingBottom() != 0) {
                    //reset the padding of the contentView
                    contentView.setPadding(0, 0, 0, 0);
                }
            }
        }
    };


    /**
     * Helper to hide the keyboard
     *
     * @param act
     */
    public static void hideKeyboard(Activity act) {
        if (act != null && act.getCurrentFocus() != null) {
            InputMethodManager inputMethodManager = (InputMethodManager) act.getSystemService(Activity.INPUT_METHOD_SERVICE);
            inputMethodManager.hideSoftInputFromWindow(act.getCurrentFocus().getWindowToken(), 0);
        }
    }
}

KeyboardUtil keyboardUtil = new KeyboardUtil(this, findViewById(android.R.id.content));

//enable it
keyboardUtil.enable();

ps.
fitSystemWindow 和 adjustResize 和 FLAG_TRANSLUCENT_STATUS 一起使用 一样也会有冲突,造成界面缩放不正常。 当时也是用的这个解决方案。

CoordinatorLayout的onScrollListener只支持21+

解决方案:
在Behavior里监听滚动,并把数据传出来

代码和后一个问题贴在一起

注意 我这里只监听了 onDependentViewChanged
并在这里调用onScroll接口,然后外部实际使用的数据是 headerview.getTop , 并不能覆盖全部情况,但是我本身只用来做状态栏渐变色,有其它使用场景请自行修改

当顶部容器不使用Toolbar时,Measure会有问题

解决方案:
自定义一个 Behavior,继承AppBarLayout.ScrollingViewBehavior

并重载onMeasureChild方法

public class PatchedScrollingViewBehavior extends AppBarLayout.ScrollingViewBehavior {

    private OnScrollListener onScrollListener;

    public PatchedScrollingViewBehavior() {
        super();
    }

    public PatchedScrollingViewBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {

        Log.e("####", "onMeasureChild");


        if (child.getLayoutParams().height == -1) {
            List dependencies = parent.getDependencies(child);
            if (dependencies.isEmpty()) {
                return false;
            }

            AppBarLayout appBar = findFirstAppBarLayout(dependencies);
            if (appBar != null && ViewCompat.isLaidOut(appBar)) {
                if (ViewCompat.getFitsSystemWindows(appBar)) {
                    ViewCompat.setFitsSystemWindows(child, true);
                }

                int scrollRange = appBar.getTotalScrollRange();
//                int height = parent.getHeight() - appBar.getMeasuredHeight() + scrollRange;
                int parentHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
                int height = parentHeight - appBar.getMeasuredHeight() + scrollRange;
                int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
                parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
                return true;
            }
        }

        return false;
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
    }

    private static AppBarLayout findFirstAppBarLayout(List<View> views) {
        int i = 0;
        for (int z = views.size(); i < z; ++i) {
            View view = (View) views.get(i);
            if (view instanceof AppBarLayout) {
                return (AppBarLayout) view;
            }
        }
        return null;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
                                          View dependency) {
        if (onScrollListener != null) {
            onScrollListener.onScroll(parent, child, dependency);
        }
        return super.onDependentViewChanged(parent, child, dependency);
    }

    public interface OnScrollListener {
        void onScroll(CoordinatorLayout parent, View child,
                      View dependency);

    }

    public OnScrollListener getOnScrollListener() {
        return onScrollListener;
    }

    public void setOnScrollListener(OnScrollListener onScrollListener) {
        this.onScrollListener = onScrollListener;
    }
}

REF:
http://stackoverflow.com/questions/30923889/flinging-with-recyclerview-appbarlayout
https://github.com/henrytao-me/smooth-app-bar-layout
https://code.google.com/p/android/issues/detail?id=177729