回顾 2016

今年的 GitHub 提交记录相当惨淡,学东西也基本是蜻蜓点水:

  • cURL 库:

之前项目中涉及过 C++ 的大文件上传,不过是基于原生的 socket;而第三方网络库 cURL 之大名, 我素有耳闻;又发现一个数千赞的 Swift 服务端框架内部也是基于 cURL,遂决定学习一个,主要是 HTTP 协议的 get/post、以及文件下载;[代码]

  • Open GL ES:

同样是项目中有涉及 GPU 图像处理,通过《OpenGL ES 3.0 编程指南》初步了解了大致流程,具体原理后续还得复习计算机图形学相关理论,GLSL 语法也需要进一步深入学习;[代码]

  • Unity:

都说今年是 VR 元年,而 Unity 目前算是对 VR 支持比较好的平台; 又发现隔壁有个 VR 团队,他们在内部论坛发的文章也是 Unity 相关;所以果断重拾 Unity,目前仅学了基本的组件控制和 NGUI;[代码]

  • 汇编:

说实话,之前在学校无论是学《微机原理》还是《嵌入式系统》,都对汇编比较头疼的,各种指令让人崩溃;前段时间偶然在知乎看到有人写的汇编文章很生动,所以又重燃了兴趣;[代码]

FileChannel 高速拷贝文件的秘密

最近在做性能优化,发现 Java NIO 中的 FileChannel 类的 transfer 相关方法,能显著提升文件拷贝速度(自测减少耗时 30-70% 左右)。

官方的说法是,利用了文件系统的缓存:

This method is potentially much more efficient than a simple loop that reads from the source channel and writes to this channel.
Many operating systems can transfer bytes directly from the source channel into the filesystem cache without actually copying them.

民间则有人提到,利用了 DMA (Direct Memory Access):

The key advantage here is that the JVM uses the OS’s access to DMA (Direct Memory Access), if present.
What happens is the data goes straight to/from disc, to the bus, and then to the destination, by passing any circuit through RAM or the CPU.

《计算机组成原理》当年学到的几乎已恢复出厂设置,通过 回 (chá) 忆 (yuè) 还是发现 DMA 和 NIO 确实有不少类似的地方:

  • DMA 不会一直占用 CPU 时间去执行 IO,而且是执行完通过中断去通知;而 NIO 也是异步非阻塞的;
  • DMA 和 NIO 都有 Channel 的概念;

下面还是先通过跟踪源代码来看看。

SQLite 并发读写问题

Android 中对 SQLiteDatabase 多线程并发读写时,很容易抛出以下异常:

1
2
3
4
5
java.lang.IllegalStateException: Cannot perform this operation because the connection pool has been closed.
at android.database.sqlite.SQLiteConnectionPool.throwIfClosedLocked(SQLiteConnectionPool.java:962)
at android.database.sqlite.SQLiteConnectionPool.waitForConnection(SQLiteConnectionPool.java:599)
at android.database.sqlite.SQLiteConnectionPool.acquireConnection(SQLiteConnectionPool.java:348)
at android.database.sqlite.SQLiteSession.acquireConnection(SQLiteSession.java:894)

一般首先想到的肯定是 Singleton 或 sychronized 解决方式,但这样其实是 block 的,并不能解决性能问题。
最近在 SQLite 官网搜索 concurrency 找到的一篇文章中发现,开启 WAL(Write-Ahead Logging) 即可解决并发读写问题。

回顾 2015

毕业工作三年,一直埋头应用层开发,专业理论知识有恢复出厂设置的危险,而且深感各方面提升都遇到了瓶颈;
而去年疯狂地尝试各种新技术,导致战线拉得太长;所以今年自然是回归主旋律、填坑为主:代码写得少,书看的多,总结归纳的多。
通过对基础理论的复习巩固和专业领域的深挖,不仅重新(初步)完善了知识体系,还顺利拿到鹅厂 offer,也算弥补了四年前校招的缺憾。

JNI 引用问题梳理

最近项目中有个视频文件分块上传的模块,核心逻辑是 C/C++ 实现的,Android 上层调用自然又要写 JNI。

其中有个需求是 Native 层上传进度更新时需要回调 Java 代码,这里我用了 C++11 的 Lambda 表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::function<void(unsigned)> cxx_progress_callback;

if (jcallback) {
cxx_progress_callback = [&env, &jcallback]
(unsigned progress) -> void {
jclass jcallback_class = env->GetObjectClass(jcallback);
if (jcallback_class) {
jmethodID methodId = env->GetMethodID(jcallback_class, "onProgressChanged", "(I)V");
if (methodId) {
env->CallVoidMethod(jcallback, methodId, progress);
}
}
};
}

一开始跑起来也没发现问题,但当我把 C/C++ 代码定义的 BLOCK_SIZE 从 512K 改为 1K 时,不幸踩了 JNI 引用的坑:

1
JNI ERROR (app bug): local reference table overflow (max=512)

为什么说是?之前也踩过类似的坑;只不过上次是全局引用,这次是局部引用;

两次踩坑,所以决心这次在解决问题之余,也详细梳理下 JNI 引用相关问题。

Java 内存分配与垃圾回收机制

程序计数器:

  • 用于指示当前线程执行的指令行号,字节码解释器通过改变它的值选取下一条待执行的指令;

  • 分支、循环、跳转、异常处理、线程恢复都需要依赖它;

  • 它是线程私有的;

:

  • 存储和方法执行相关的信息:栈帧(Stack Frame);

  • 栈帧包含: 局部变量表(基本数据类型和引用)、操作栈、动态链接、方法出口等信息;

  • 每一个方法从被调用到运行结束都对应着栈帧从入栈到出栈的过程;

  • 根据方法类型,可分为虚拟机栈和本地方法栈;

  • 它也是线程私有的;

DexClassLoader 实现 Android 插件加载

Java 中的 ClassLoader:

Java 中 ClassLoader 用于动态加载 Class 到 JVM,包含 BootstrapClassLoader(C++ 编写,用于加载系统核心类)、ExtClassLoader(用于加载 lib/ext/ 目录的扩展 API)、AppClassLoader(加载 CLASSPATH 目录下的类)。

双亲委托机制:

  • 任何自定义 ClassLoader 都必须继承 ClassLoader 抽象类,并指定其 parent 加载器,默认为 BootstrapClassLoader;

  • 任何自定义 ClassLoader 在加载一个类之前都会先委托其 parent 去加载,只有 parent 加载失败才会自己加载;

    • 这样既可以防止重复加载,又可以排除安全隐患(防止用户替换系统核心类);

    • 所以一般只需要重写 findClass()方法即可(在 parent 加载失败时调用);

  • 双亲委托机制是在 loadClass() 方法实现的,要想避开(自己验证安全性,比如 Tomcat 的 WebAppClassLoader),必须重写 loadClass() 方法;

自定义 ClassLoader 用途:

  • 在执行非置信代码前先做签名认证等;

  • 从网络、数据库等动态加载类;

类的卸载:

  • 只有当类的实例被回收,才会被 unload,但被 BootstrapClassLoader 加载的系统类除外;

  • 重复加载类会报异常,只能重新定义新的 ClassLoader 再次加载;

深入理解 Android:ActivityManagerService

Linux 设置进程优先级函数:

1
int setpriority(int which, int who, int prio);
  • 参数 whichwho 联合使用:

    • whichPRIO_PROGRESS 时,who 代表进程;

    • whichPRIO_PGROUP 时, who 代表进程组;

    • whichPRIO_USER 时,who 代表 uid

  • 参数 prio 为优先级,范围 [-20,19],值越大,被调用几率越小;

Linux 设置进程调度策略函数:

1
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);
  • 参数 pid 为进程 id;

  • 参数 policy 为调度策略,目前 Android 只支持前三种(非实时调度策略):

    • SCHED_OTHER: 默认值,标准的分时共享策略;

    • SCHED_BATCH: 针对批处理进程的调度策略;

    • SCHED_IDLE: 针对优先级非常低的适合在后台运行的策略;

    • SCHED_FIFO: 队列调度策略;

    • SCHED_RR: round-robin 循环调度策略;

  • 由于 Android 不支持实时调度策略,参数 param 必须为 NULL

深入理解 Android:PowerManagerService、BatteryService/BatteryStatsService

PowerManagerService 简介:

  • PowerManagerServiceIPowerManager.Stub 类派生,并实现了 Watchdog.MonitorLocalPowerManager 接口;

  • 客户端使用 PowerManager 类,其内部通过代表 BinderProxy 端的 mService 成员变量与 PowerManagerService 进行 Binder 通信;

  • PowerManagerServiceBatteryServiceBatteryStatsServiceLightServiceSensorManagerActivityManagerServiceWindowManagerService 等许多系统服务均有交互;

PowerManagerService 的创建:

  • PowerManagerServiceSystemServerServerThread 线程中创建;

  • 调用构造方法:

    1. 获取当前进程的 uid 和 pid;

    2. 调用 Power.setLastUserActivityTimeout() 设置超时为一周,Power 类封装了同 Linux 内核交互的接口;

    3. 初始化 mUserStatemPowerState 变量为 0

    4. 调用 Watchdog.getInstance().addMonitor() 将自己添加到 Watchdog 监控队列;

深入理解 Android:PackageManagerService

SystemServer 创建 PackageManagerService:

  1. 调用 PackageManagerServicemain() 方法,得到 IPackageManager 对象;

  2. 调用 IPackageManager.isFirstBoot() 判断是否开机后第一次启动(ZygoteSystemServer 退出后,init 会再次启动它们);

  3. 调用 IPackageManager.performBootDexOpt() 执行 dex 优化;

  4. 调用 IPackageManager.systemReady() 通知系统已进入就绪状态;

PackageManagerService 构造方法:

  1. 创建 Settings 对象,并调用其 addSharedUserLPw() 方法,保存 SharedUserSetting 信息;

  2. 创建 Installer 对象,用于和 Native 进程 installd 交互;

  3. 创建 ThreadHandler 线程,并以其 Looper 为参数创建 PackageHandler 对象,用于程序的安装和卸载;

  4. 根据 Installer 对象和 /data/user 文件对象创建 UserManager 对象,用于多用户管理;

  5. 调用 readPermissions() 方法,从 /system/etc/permissions 目录下的 XML 文件读取权限信息;

  6. 调用 Settings 对象的 readLPw() 方法解析 /data/system 目录下的文件;

  7. 扫描 /system/frameworks 目录以及 BOOTCLASSPATH 和 platform.xml 定义的系统库目录下的 jar 和 APK 文件是否需要 dex 优化,如果需要则调用 Installer.dexopt() 方法发消息给 installd 让它优化;如果任意一个文件执行了 dex 优化操作,删除 /data/dalvik-cache 目录下的缓存文件;

  8. 创建 AppDirObserver 对象监听 /system/frameworks、/system/app、/vendor/app (厂商定制)、/data/app、/data/app-private 5 个目录,并调用 scanDirLI() 方法扫描其中的 APK 文件;

  9. 汇总上面扫描 XML 和 APK 得到的信息,并写入文件;