1. 如何有效使用 GitHub

    这是一个知乎问题下我的回答,原帖传送门。 前言 GitHub 是很多「modern」程序员使用频度很高的网站,但各人从中汲取的养分不同。有的人借助它的力量扬名立万成为一代大神;有的人迷失其中,天天在其中流连却成长有限。 而我,成长为了一名主要用 GitHub 写博客的少年,Sad story! :joy::joy::joy: 这当然不是我想要的,要知道我也是一个有大神梦的人啊。:clap: 下面给出我对自己的分析和总结,希望在以后继续使用 GitHub 的过程中能持续总结重构,形成自己高效使用它的方式。 分析 我目前使用 GitHub 的频度很高,收获一般,从使用习惯上来分析: 好习惯 将 GitHub Pages 作为写博客的工具,能使用 Markdown 专注于内容。 Follow 了一些 Android 领域的牛人,经常关注他们关注的东西。 坏习惯 刷 Dashboard 太频繁,浪费时间。 了解别人的项目不深入,往往只停留在看看「是什么」的阶段,没有了解「怎么实现」,更不用说「这里值得学习」。 没有长期维护的项目,动手写代码太少。 总结 我认为的有效使用 GitHub 的方式: Follow 你感兴趣的领域厉害的人物,持续关注他们在 GitHub 上的活动,选择其中优秀的资源学习之。 tip: 学习要深入,不要止步于泛泛地了解。 将你自己的玩具项目源码大胆发上去,不断用你学习到的优秀的模式和架构对它们进行重构,形成你个人比较固定的编程规范。 tip: 拥有自己长期维护的项目,多重构。 学习并逐渐掌握 GitHub 的工作流,使用版本控制和 Issues、Milestones 等记录和掌控自己业余项目的进度。 善用搜索,善用 awesome 系项目。 勤做笔记,使用仓库/gh-pages/Issues 写博客都是不错的方式。 参与到别人的项目中去,使用别人项目的过程中遇到问题先去 Issues 和 Pull Requests 里寻找解决方案,找不到的尝试自己去修复提 Pull Request,能力所限修复不了的提 Issues 寻求帮助。 tip: 提 Issues 和 Pull Request 先阅读一下项目里的 CONTRIBUTING,遵循项目的规范。 如果可以,发动你身边的朋友们跟你一起用起来,有适当的好友互动会更有趣。 tip: 不要把它玩成了纯社交,不停刷 timeline 那就不如玩朋友圈了。@mzlogin,欢迎互动。

    2016/04/07 GitHub

  2. 不藏拙的人生

    按我以往写博客平白直述的风格来讲,这是一个「弄玄」的标题。 但最近确实一直在思考一些「玄」的问题,比如工作的方法、人生的方向以及如何更有效地思考。 脑拙? 我偶尔会跟女朋友开玩笑讲说我有一颗单核的大脑——思考一个问题可以让我专注,思考两个问题会让我焦虑,思考一堆问题将让我凌乱。后来悲哀地发现,这好像不是玩笑。 很显然思考上面那么多问题会让我凌乱。 我也曾愤愤地怨念上天不公,没有给我一颗像牛人们那样毫不费力地处理多个复杂问题的大脑。后来学了多线程的原理,我释然了,即便给我一颗多核的大脑,如果毫无规则地丢给它一堆问题,估计也是死锁阻塞在那里了。 为今之计,只能自己在生活里打怪升级,逐渐优化进线程调度算法,做好任务划分和优先级排序,即使我是单核,也让它跑出高频高效来。 手拙? 我知道我自己有个毛病,那就是对自己没做过或者不擅长的事情很羞于在人前展示,怕被嘲笑。 比如我不会削苹果,不像我姐姐们心灵手巧能把一个苹果削到底不断皮,于是开始时我是能不吃苹果就不吃苹果,后来有人鼓吹说苹果不削皮吃更好,然后我不管它是否有科学依据就欣欣然接受了。 再比如我从小不会踢毽子,然后后来有几次参加运动会等活动里,大家围成一圈就踢起毽子来,我吃惊地发现男女老少玩这个几乎都是一把好手,好多人还会各种花式,而我一般就是尽量找个借口不参与,不然一般毽子飞到我这里的时候就是一个回合结束的时候,不管有没有人埋怨,这会让我感到沮丧。 这个毛病会妨碍我去扩展我的世界,学习和见识新的东西,我一直都知道,但是这种可恶的羞耻感一直伴随着我。 可其实我隐约是知道解决方法的。 比如我一直没打过羽毛球,但进了现在的公司之后,我决定学会这样运动技能作为长期锻炼项目,于是开始去蹭同事们的场子,不管各种接不到球的尴尬,不理会出洋相时大伙的哄笑声,在网上找一些入门教程学习……就这样坚持了下来,虽然现在也还是很菜,但好歹跟一般业余选手也能打得起来,没什么嘲笑的声音了。 只是,这样的案例在我的生活里是少数,多数情况下,我没有实现对自己的突破,不会的东西可能就一直不会了下去。 不藏拙! 以上的两「拙」困扰了我多年,为了减小其负面影响,我也总结了一些对我自己有效的步骤。 第一步,接受自己。 接受自己的不完美,接受自己不擅长很多东西的事实。每个人的成长环境或经历都不一样,别人擅长一些你不会的事情这很正常,这并不是你的错。 如果你对它并不感兴趣,那就坦然处之,告诉大家你不会,静静地旁观就好。放弃那种「如果大家不知道,就不会因为我不会这个觉得我笨」的想法,不要太在意别人的眼光(大家都很忙的),你就解脱了。 当你坦荡荡,不再试图去隐藏,那这件事又如何能再给你负面影响呢?子曾经曰过,「知之为知之,不知为不知,是知也。」 也许有人会问,当大家都在玩的时候,你玩得很菜或者单独在一旁,焦点被引到你身上的时候怎么办?这种时候,自嘲是利器,当你自己都开始贬低自己的时候,别人的善意或恶意的调侃注定收不到预期的效果,基本也就不会出口了。 而如果你对它感兴趣,想要学会它,那么接第二步。 第二步,一切皆有套路。 百度百科里对「套路」的解释是原指编成套的武术动作,现泛指成套的技巧、程式、方法等。 我们生活里的大部分事情,都有「套路」可循。 比如程序设计里的设计模式,就是编程的套路,用得好可以让程序结构更加优雅;比如遇到一个复杂的问题,使用特定的方法将其定量分析、拆分、排序后逐一解决,这是解决问题的套路,当你的大脑习惯这样的思维方式后你的解决力一定有所提升。 那我们要解决自己的某一「拙」,只需简单两步: 找对套路。 一个好教练,一本好书,一套好的视频教程,都是对的套路存在的地方。 熟能生巧。 要成为一个领域的高端玩家不容易,这可能需要非常的努力加上一定的天赋;但成为一名普通玩家并不难,找对套路之后大量的练习就可以,不要害怕学不会,毕竟「以大多数人的努力程度之低,根本轮不到拼天赋」,那么多人都能做到,你当然也能。

    2016/03/14 Blog

  3. 从 am start 的 --user 参数说到 Android 多用户

    本文的讨论围绕一个 java.lang.SecurityException 展开,异常的关键词是权限 android.permission.INTERACT_ACROSS_USERS_FULL。 java.lang.SecurityException: Permission Denial: startActivity asks to run as user -2 but is calling from user 0; this requires android.permission.INTERACT_ACROSS_USERS_FULL at android.os.Parcel.readException(Parcel.java:1425) at android.os.Parcel.readException(Parcel.java:1379) at android.app.ActivityManagerProxy.startActivityAsUser(ActivityManagerNative.java:1921) at com.android.commands.am.Am.runStart(Am.java:494) at com.android.commands.am.Am.run(Am.java:109) at com.android.commands.am.Am.main(Am.java:82) at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method) at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:235) 先就我所了解的知识说一下此异常发生的背景。 背景 Android 系统里的多用户 Android 系统是基于 Linux 内核,而 Linux 内核中用于支持多用户机制的 uid 在 Android 中被用于标识 app-specific sandbox。android.os.Process 类的 myUid() 方法 的描述里的原话是: Returns the identifier of this process’s uid. This is the kernel uid that the process is running under, which is the identity of its app-specific sandbox. It is different from myUserHandle() in that a uid identifies a specific app sandbox in a specific user. 所以注定 Android 如果要实现多用户不能直接使用 Linux 的 uid 机制了,需要另做一套机制。 在 Android API level 17 的 Features 列表里有一项是 Multiple user accounts (tablets only) 所以在 API level 17 以上的 Android 系统里其实已经内置了多用户的支持,只不过暂时只对平板启用(据说是因为多用户手机专利早已被 Symbian 雇员注册,不知真假。)。在实现上是新引入了 UserHandle 的概念,封装了 user id,在 android.os.Process 类的 myUserHandle() 方法 的描述里的原话是: Returns this process’s user handle. This is the user the process is running under. It is distinct from myUid() in that a particular user will have multiple distinct apps running under it each with their own uid. user id 与 uid 结论 user id = uid / 100000 目前 Android 手机上所有 APP 的 user id 都为 0 root 权限与 uid 是否为 0 有关,与 user id 无关 分析 /** * Representation of a user on the device. */ public final class UserHandle implements Parcelable { /** * @hide Range of uids allocated for a user. */ public static final int PER_USER_RANGE = 100000; ...... /** * @hide Enable multi-user related side effects. Set this to false if * there are problems with single user use-cases. */ public static final boolean MU_ENABLED = true; ...... /** * Returns the user id for a given uid. * @hide */ public static final int getUserId(int uid) { if (MU_ENABLED) { return uid / PER_USER_RANGE; } else { return 0; } } ...... } 这个类定义在 frameworks/base/core/java/android/os/UserHandle.java 里。 上面这段代码能得出结论 1 里的公式。 /** * Tools for managing OS processes. */ public class Process { ...... /** * Defines the root UID. * @hide */ public static final int ROOT_UID = 0; ...... /** * Defines the start of a range of UIDs (and GIDs), going from this * number to {@link #LAST_APPLICATION_UID} that are reserved for assigning * to applications. */ public static final int FIRST_APPLICATION_UID = 10000; /** * Last of application-specific UIDs starting at * {@link #FIRST_APPLICATION_UID}. */ public static final int LAST_APPLICATION_UID = 19999; ...... } 这个类定义在 frameworks/base/core/java/android/os/Process.java 里。 从 FIRST_APPLICATION_UID 与 LAST_APPLICATION_UID 的值,结合结论 1 里的公式来看,所有 APP 运行时获取自身的 user id 都为 0;而运行时 uid 为 ROOT_UID (即 0)的 APP 获取自身的 user id 也为 0,所以 user id 是否为 0 与是否获取 root 权限并无关联。 异常发生的场景 该异常发生在 API level 17 以上的机型里,在 APP 或者 APP 调用的 Native 进程里使用 am start 来启动 Activity 时。 在 Java 代码中直接 startActivity 并不会触发此异常。 好了,背景交待完毕,下面按惯例先上结论及解决方案,以便急于解决文章开始提到的异常而不想探究原理的同学可以省时地带着结论心满意足地离去。 结论及解决方案 在 API level 17 以上的 Android 设备里,通过 am start 命令来启动 Activity 时会校验调用 am 命令的进程的 user id 与 am 进程从 –user 参数获取到的 user id(默认值为 UserHandle.USER_CURRENT,即 -2)是否相等, 如果想在 APP 或者 APP 调用的 Native 进程里使用 am start 来启动 Activity,那么需要给其传递能通过校验的 –user 参数,参数值可以直接硬编码为 0,也可以使用 android.os.Process.myUserHandle().hashCode() 的值。 如果不给 am start 传递正确的 –user 参数,那调用进程对应 uid 需要拥有 INTERACT_ACROSS_USERS_FULL 权限,但是该权限的 protectionLevel 为 signature|installer,一般场景下是无法获取到的。 我做了一个 Demo APP,通过 Runtime.getRuntime().exec("am start xxxxxxx"); 来启动拔号程序界面,有两个按钮分别模拟了传递与不传递 –user 参数的情况,有兴趣的同学可以看看现象,完整源码在 AuthorityDemo。 运行截图如下: 分析 接下来我们将借助于源码对 am start 的执行过程进行分析,一点一点吹散迷雾。 am start 的执行过程 am 命令的源码在 frameworks/base/cmds/am 里,里面的 am 文件即为 am 命令主体: #!/system/bin/sh # # Script to start "am" on the device, which has a very rudimentary # shell. # base=/system export CLASSPATH=$base/framework/am.jar exec app_process $base/bin com.android.commands.am.Am "$@" 这段代码在 framworks/base/cmds/am/am 里。 am 命令是通过 app_process 最终调用到 com.android.commands.am.Am 类的 main 方法,并将所有参数传递给 main 来执行后续流程的。app_process 相关知识与 am start 执行逻辑无关,此处略去不表,放在本文最后一节附录中讲解。 am start 的关键方法调用如下: 文章开始处的异常就是在 handleIncomingUser 方法里校验 user id 和权限失败之后抛出的。下面按方法调用层级详细分析一下,如下源码所在源文件可以在上图中找到: Am.main public static void main(String[] args) { (new Am()).run(args); } 就是一个简单的 new 和 run 方法调用,而 Am 类中并无 run(String[]) 原型的方法,所以其实调用的是 Am 类的基类 BaseCommand 的 run 方法。 BaseCommand.run public void run(String[] args) { ...... mArgs = args; ...... try { onRun(); ...... } ...... } BaseCommand 类的 onRun 方法是一个抽象方法,所以其实 run 方法只是保存了参数,然后调用了 Am 的 onRun 方法。 Am.onRun @Override public void onRun() throws Exception { mAm = ActivityManagerNative.getDefault(); ...... String op = nextArgRequired(); if (op.equals("start")) { runStart(); } ...... } onRun 方法主要是对 am 命令后第一个参数进行判断并进行相应的方法调用,我们可以看到 am start 是调用了 runStart 方法。 这里可以顺便留意到的是 mAm 对象,追踪 ActivityManagerNative.getDefault() 可以知道它最终通过 binder 机制对应 ActivityManagerService 对象。 Am.runStart public class Am extends BaseCommand { ...... private boolean mWaitOption = false; ...... private int mUserId; ...... private Intent makeIntent(int defUser) throws URISyntaxException { ...... mWaitOption = false; ...... mUserId = defUser; ...... while ((opt=nextOption()) != null) { if (opt.equals("-a")) { ...... } else if (opt.equals("-W")) { mWaitOption = true; ...... } else if (opt.equals("--user")) { mUserId = parseUserArg(nextArgRequired()); } ...... } private void runStart() throws Exception { Intent intent = makeIntent(UserHandle.USER_CURRENT); ...... if (mWaitOption) { result = mAm.startActivityAndWait(null, null, intent, mimeType, null, null, 0, mStartFlags, profilerInfo, null, mUserId); res = result.result; } else { res = mAm.startActivityAsUser(null, null, intent, mimeType, null, null, 0, mStartFlags, profilerInfo, null, mUserId); } ...... switch (res) { // 启动 Activity 的结果提示 } if (mWaitOption && launched) { // 输出启动 Activity 的结果状态,起止时间等 } ...... } ...... } 由如上代码可知,一般通过 am start 启动 Activity 时若未传 -W 参数(我一般的做法),会调用 ActivityManagerService.startActivityAsUser 来启动 Activity。(ActivityManagerService.startActivityAndWait 其实与 ActivityManagerService.startActivityAsUser 类似,只是在启动 Activity 后多了一个等待过程,下面不再重复分析。) 而 mUserId 的值,若命令行中有 –user 参数,则被赋为该参数的值;若命令行中无 –user 参数,则默认为 UserHandle.USER_CURRENT 的值,在 frameworks/base/core/java/android/os/UserHandle.java 中可知此默认值为 -2。 -2 这个数字是不是有点似曾相似?没错,文首的异常信息里就有这个值。 ActivityManagerService.startActivityAsUser @Override public final int startActivityAsUser(IApplicationThread caller, String callingPackage, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo, Bundle options, int userId) { enforceNotIsolatedCaller("startActivity"); userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, false, ALLOW_FULL_ONLY, "startActivity", null); // TODO: Switch to user app stacks here. return mStackSupervisor.startActivityMayWait(caller, -1, callingPackage, intent, resolvedType, null, null, resultTo, resultWho, requestCode, startFlags, profilerInfo, null, null, options, false, userId, null, null); } Binder.getCallingUid() 是一个 native 方法,在 frameworks/base/core/java/android/os/Binder.java 中能找到,它其实就是返回了当前进程的 uid,而该 uid 是从父进程继承的。 /** * Return the Linux uid assigned to the process that sent you the * current transaction that is being processed. This uid can be used with * higher-level system services to determine its identity and check * permissions. If the current thread is not currently executing an * incoming transaction, then its own uid is returned. */ public static final native int getCallingUid(); ActivityManagerService.handleIncomingUser int handleIncomingUser(int callingPid, int callingUid, int userId, boolean allowAll, int allowMode, String name, String callerPackage) { final int callingUserId = UserHandle.getUserId(callingUid); if (callingUserId == userId) { return userId; } ...... if (callingUid != 0 && callingUid != Process.SYSTEM_UID) { final boolean allow; if (checkComponentPermission(INTERACT_ACROSS_USERS_FULL, callingPid, callingUid, -1, true) == PackageManager.PERMISSION_GRANTED) { // If the caller has this permission, they always pass go. And collect $200. allow = true; } else if (allowMode == ALLOW_FULL_ONLY) { // We require full access, sucks to be you. allow = false; } else if (......) { ...... } if (!allow) { if (userId == UserHandle.USER_CURRENT_OR_SELF) { // In this case, they would like to just execute as their // owner user instead of failing. targetUserId = callingUserId; } else { StringBuilder builder = new StringBuilder(128); builder.append("Permission Denial: "); builder.append(name); if (callerPackage != null) { builder.append(" from "); builder.append(callerPackage); } builder.append(" asks to run as user "); builder.append(userId); builder.append(" but is calling from user "); builder.append(UserHandle.getUserId(callingUid)); builder.append("; this requires "); builder.append(INTERACT_ACROSS_USERS_FULL); if (allowMode != ALLOW_FULL_ONLY) { builder.append(" or "); builder.append(INTERACT_ACROSS_USERS); } String msg = builder.toString(); Slog.w(TAG, msg); throw new SecurityException(msg); } } } ...... } 它先校验了当前进程的 user id 与参数里的 userId(即 –user 的值或默认的 -2)是否相等,如果相等则正常返回,执行后续的启动 Activity 动作; 如果不相等,普通应用程序的 callingUid 必为 0,则先进行权限校验,看当前 pid 和 uid 是否被赋予了 INTERACT_ACROSS_USERS_FULL 权限,我们在前面的「结论及解决方案」一节中已经交待过,该权限的 protectionLevel 为 signature|installer,一般场景下是无法获取到的; 如果没有 INTERACT_ACROSS_USERS_FULL 权限,allowMode 参数值又为 ALLOW_FULL_ONLY 则将抛出 SecurityException,从上一小节「ActivityManagerService.startActivityAsUser」中调用 handleIncomingUser 的参数可知 allowMode 参数就是 ALLOW_FULL_ONLY。 上方代码段里的 Permission Denial:、asks to run as user 和 but is calling from user 等字符串是不是很熟悉?这就是从文首开始困惑我们的异常抛出的地方。 至此,am start 的大概执行过程和异常发生的情景分析完成。 实现功能,避免异常 从上方的分析可知,要避免异常最直接有效的方法就是让 handleIncomingUser 方法正常返回,既然声明 INTERACT_ACROSS_USERS_FULL 权限的路不通,就只有传递 –user 参数了。 那么给这个参数传递什么值呢? 从上文的分析可知,该参数应该与 am 进程的 user id 相等,所以传递父进程的 user id 即可。(user id 由 uid 运算得来,而 uid 与父进程相同。) 由「背景」一节可知,所有 APP 进程的 user id 都为 0,所以该参数直接写 0 是可以的;如果不想硬编码,那么可以先用 Process 类的 myUserHandle() 方法获取进程的 user handle: /** * Returns this process's user handle. This is the * user the process is running under. It is distinct from * {@link #myUid()} in that a particular user will have multiple * distinct apps running under it each with their own uid. */ public static final UserHandle myUserHandle() { return new UserHandle(UserHandle.getUserId(myUid())); } 这段代码定义在 frameworks/base/core/java/android/os/Process.java 中。 继续分析 UserHandle 类里的相关方法: public final class UserHandle implements Parcelable { ...... /** @hide */ public UserHandle(int h) { mHandle = h; } /** * Returns the userId stored in this UserHandle. * @hide */ @SystemApi public int getIdentifier() { return mHandle; } ...... @Override public int hashCode() { return mHandle; } ...... 这段代码定义在 frameworks/base/core/java/android/os/UserHandle.java 中。 构造方法 UserHandle(int h) 里将 user id 保存在 mHandle 成员里,本来 UserHandle 类有一个 getIdentifier 方法可以返回 mHandle 的,但该方法被标为了 SystemApi 和 hide,无法正常调用,所以找了一个取巧的办法,使用也返回 mHandle 值的 hashCode 方法来达成目标。 所以 am start 的 –user 的参数可以直接写为 0,也可以使用 android.os.Process.myUserHandle().hashCode() 的值。 引申思考 启动 Activity 的方法并非只有在 APP 进程里使用 am start 一种,还有通过 adb 命令 adb shell am start 或在 APP 进程里使用 startActivity 等,它们为什么没有抛出此异常呢?继续探索。 为何 adb shell am start 不抛此异常 原因: shell 是拥有 INTERACT_ACROSS_USERS_FULL 权限的,所以 am start 作为其子进程继承了 shell 的 uid 和对应权限,在如上流程中 ActivityManagerService.handleIncomingUser 里通过了权限检查,不会抛出文首的异常。 在 frameworks/base/packages/Shell/AndroidManifest.xml 里有如下声明: <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.shell" coreApp="true" android:sharedUserId="android.uid.shell" > ...... <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> ...... </manifest> Java 代码里 startActivity 的执行过程 其实与 am start 一样,都是执行到了 ActivityManagerService.startActivityAsUser,区别在于参数。 @Override public final int startActivity(IApplicationThread caller, String callingPackage, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo, Bundle options) { return startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo, resultWho, requestCode, startFlags, profilerInfo, options, UserHandle.getCallingUserId()); } 在 am start 流程中的,传给 startActivityAsUser 的最后一个参数是 –user 传入的或者默认的 -2,而 Java 代码里的 startActivity 流程中传给 startActivityAsUser 的是 UserHandle.getCallingUserId(),相当于到 handleIncomingUser 中是当前进程的 user id 与当前进程的 user id 比较(必相等),如果相等则通过校验,所以必能通过校验,不会抛出文首说的异常。 附录 app_process 简要执行过程 在第 2 步,AndroidRuntime::start 中调用了 startVm 启动虚拟机,最终在第 5 步 AppRuntime::onStarted 中调用通过参数传进来的类的 main 方法,并将类名后的参数传给它。

    2016/02/10 Android

  4. 将 GitHub Pages 从 Redcarpet 切换到 kramdown

    GitHub 前不久发布了 New Features 公告,GitHub Pages now faster and simpler with Jekyll 3.0,宣布从 2016 年 5 月 1 日起,GitHub Pages 将只支持 kramdown 作为唯一的 Markdown 引擎。 这其实也算得一件好事,之前支持 Rediscount、Redcarpet 和 kramdown 等多种引擎,而它们相互之间和与标准 Markdown 之间又有一些细微却也无法忽视的差异,这让需要在多个平台使用 Markdown 的我头疼不已,早就希望 GitHub Pages 能与 GitHub 的 Issues 和 comments 等地方统一语法,本次更新虽然做不到这一点,但也算是迈出了不小的一步。 我在此前是使用 Redcarpet,配置如下: markdown: redcarpet redcarpet: extensions: - no_intra_emphasis - fenced_code_blocks - autolink - tables - with_toc_data - strikethrough pygments: true 切换到 kramdown 之后的配置如下: markdown: kramdown kramdown: input: GFM highlighter: rouge 切换过程中有若干需要处理的差异问题,现将它们及解决方法记录如下。 列表项里嵌套的代码块 嵌套在列表项中的代码块在 Redcarpet 中使用 Tab 进行缩进即可,而在 kramdown 中需要根据列表项的内容开始位置决定缩进的字符数。 关于此问题的讨论见 Embedding codeblocks in lists。 in Redcarpet: 1. list item one ```python print 'hello, world' ``` 2. list item two * unordered list item one ```python print 'hello, world' ``` * unordered list item two in kramdown: 1. list item one ```python print 'hello, world' list item two unordered list item one print 'hello, world' unordered list item two ``` 遍历 Collections 我在本博客做了一个 wiki collection,在 Redcarpet 中用 for doc in site.documents 可遍历所有 wiki。 而切换到 kramdown 后这样的写法将遍历所有的 wiki 和 posts,需要使用 for wiki in site.wiki 来遍历 wiki。 不过这点其实严格说起来应该是我在使用 Redcarpet 时的写法没有遵循 Jekyll 的文档,参考 Collections。 TOC 链接 在我之前的一篇文章 GFM 与 Redcarpet 的不同点 中,描述了 Redcarpet 与 GFM 自动生成的 TOC 链接的区别,而 kramdown 即使启用了 input: GFM 生成的链接与 GFM 也还是不同,处理 GFM 与 Redcarpet 生成的 TOC 链接的区别已经让我心累了,不想再多记一种。 不过谢天谢地,kramdown 支持自动生成 TOC,只需在想放置 TOC 的地方放置如下内容即可,非常方便。 * TOC {:toc} 删除线 在 Redcarpet 中使用如下语法能自动为文字加上删除线: ~~hello world~~ 但切换到 kramdown 后这种写法失效了,浏览了一下文档之后并没有找到 kramdown 对应的语法,这个用得也少,遂直接用 HTML 元素解决问题: <del>hello world</del> hello world update 2016/03/02: kramdown 主分支已经解决了这个问题,见 gettalong/kramdown#304,坐等 Release 后 GitHub Pages 更新就能用了。 update 2016/03/29: pages-gem 当前使用的 1.10.0 版本的 kramdown 已经包含了主分支对此的修复,已经可以愉快地使用 ~~hello world~~ 来表示 hello world 了。 表格 在 Redcarpet 中如下写法能直接显示你写的内容: READ|WRITE|SHARE 但在 kramdown 中会解析成表格: READ WRITE SHARE 所以需要将 | 转义。 READ\|WRITE\|SHARE 相关讨论见:gettalong/kramdown#151 高亮的语言名称 使用 Redcarpet + pygments 的组合时,cpp、C++ 和 c++ 都能对 C++ 代码片断进行语法高亮。 而改为 kramdown + rouge 的组合后,只能使用 cpp。 rouge 支持的语言列表可以参考如下链接: List of supported languages and lexers 图片上面空行 在 Redcarpet 中,如下写法的图片和文字之间会换行: Hello, world! ![](/img.png) 而 kramdown 中这种写法图片会直接接在文字后面显示,不换行。如果需要换行则应在图片上面空一行: Hello, world! ![](/img.png)

    2016/02/04 Markdown

  5. Build Zeal for Mac OS X

    我承认,初次遇到 Dash 的时候,我是惊艳的。 怎么会有如此方便的文档查看工具?顿时觉得被各种加载奇慢的 API 文档坑苦了好多年!于是很开心地下载了我常用的 API 文档,并且找到了它在 Windows 下的替代品 Zeal 推荐给朋友们,感觉世界从此美好了许多。 可惜好景不长。 畅快地查询几次之后就经常看到这个等待的界面了,提示 Please Purchase to Skip Waiting. The page will load in 8 seconds.,用它就是为了提升查询效率的,年轻的生命禁不起这样的等待。 购买 Dash 是 $24.99,不能算贵,不过想起了它有个免费开源、界面锉一点但是够用的兄弟 Zeal,还是决定省下这些钱去吃顿好吃的。 下载地址 我在本机编译做的 dmg 文件我上传到了百度网盘,不想折腾的同学可以直接下载拿走。 下载地址:Zeal-for-Mac-OSX.dmg 打开后将 Zeal.app 拖到「应用程序」文件夹就行了。 编译 Zeal Zeal 的源码在 zealdocs/zeal,编译方法在 README 的 How to compile 一节。 编译的步骤我参考了 Compile Zeal on Mac OS X,不过现在情况跟他那时候有了一些变化,至少从我这里编译的情况来看 Mac OS X 下可以不需要再安装 X11,而源码直接 qmake 和 make 编译通不过了。 如下是编译步骤: 安装最新版 Qt,官方文档推荐的是 v5.2.0+,我安装的是 v5.5。 下载地址:http://www.qt.io/download-open-source/ 安装 libarchive。 brew install libarchive 下载源码。 git clone git@github.com:zealdocs/zeal.git 编译。 使用 Qt Creator 打开源码下的 zeal.pro 文件,点击「项目」标签,将构建设置里的「编辑构建配置」改为 Release。 打开 src/core/core.pri 文件,在最后添加如下内容(需要将路径替换为你的机器上 libarchive 的对应完整路径): macx: { INCLUDEPATH += /usr/local/Cellar/libarchive/3.1.2/include LIBS += -L/usr/local/Cellar/libarchive/3.1.2/lib -larchive } Qt Creator 里的菜单项 「构建」——「构建所有项目」。 如果编译不报错,在你的「项目」标签里的「构建目录」里填写的目录下的 bin 子目录里应该有 Zeal.app 了。 生成安装包。 使用 Terminal 进入 Zeal.app 所在目录,运行如下命令生成 Zeal.dmg 文件: macdeployqt Zeal.app -dmg macdeployqt 命令在 Qt 安装目录下能找到,比如我的这个命令在 /Users/mazhuang/Qt5.5.0/5.5/clang_64/bin 目录下。 使用截图 遇到过的问题 编译时报错 编译过程中遇到过若干种报错,其实都是由于 libarchive 造成的,正确安装并配置 include 和 lib 目录即可。 报错 1: zeal/src/core/extractor.cpp:27: error: 'archive.h' file not found #include <archive.h> ^ 报错 2: Undefined symbols for architecture x86_64: "_archive_entry_pathname", referenced from: Zeal::Core::Extractor::extract(QString const&, QString const&, QString const&) in extractor.o "_archive_entry_set_pathname", referenced from: Zeal::Core::Extractor::extract(QString const&, QString const&, QString const&) in extractor.o "_archive_error_string", referenced from: Zeal::Core::Extractor::extract(QString const&, QString const&, QString const&) in extractor.o "_archive_filter_bytes", referenced from: Zeal::Core::Extractor::progressCallback(void*) in extractor.o "_archive_read_extract", referenced from: Zeal::Core::Extractor::extract(QString const&, QString const&, QString const&) in extractor.o "_archive_read_extract_set_progress_callback", referenced from: Zeal::Core::Extractor::extract(QString const&, QString const&, QString const&) in extractor.o "_archive_read_free", referenced from: Zeal::Core::Extractor::extract(QString const&, QString const&, QString const&) in extractor.o "_archive_read_new", referenced from: Zeal::Core::Extractor::extract(QString const&, QString const&, QString const&) in extractor.o "_archive_read_next_header", referenced from: Zeal::Core::Extractor::extract(QString const&, QString const&, QString const&) in extractor.o "_archive_read_open_filename", referenced from: Zeal::Core::Extractor::extract(QString const&, QString const&, QString const&) in extractor.o "_archive_read_support_filter_all", referenced from: Zeal::Core::Extractor::extract(QString const&, QString const&, QString const&) in extractor.o "_archive_read_support_format_all", referenced from: Zeal::Core::Extractor::extract(QString const&, QString const&, QString const&) in extractor.o ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation) make[1]: *** [../bin/Zeal.app/Contents/MacOS/Zeal] Error 1 make: *** [sub-src-make_first-ordered] Error 2 23:47:44: 进程"/usr/bin/make"退出,退出代码 2 。 Error while building/deploying project zeal (kit: Desktop Qt 5.5.0 clang 64bit) When executing step "Make" 23:47:44: Elapsed time: 00:01. 解决方法: 安装 libarchive,将根据上面编译步骤 4 里的说明修改 src/core/core.pri 文件。 关于这个问题的讨论见 zealdocs/zeal#372。 打包时报错 在打包 dmg 文件的过程中会提示 ERROR,这个貌似不影响,直接忽略就好。 ERROR: no file at "/opt/local/lib/mysql55/mysql/libmysqlclient.18.dylib" ERROR: no file at "/usr/local/lib/libpq.5.dylib" 解决方法: 忽略。 感谢 Dash,Dash 再见。

    2016/01/16 Mac

  6. Android 系统缓存扫描与清理方法分析

    本文记录的是我对 Android 的「系统缓存」及其扫描和清理方法的探索与理解。 本文讲述内容的完整代码实例见 https://github.com/mzlogin/CleanExpert。 系统缓存的定义 如下是我捏造的非官方定义: 系统缓存: Android APP 在运行过程中保存在手机内置和外置存储上的缓存文件总和。 系统缓存的组成 先说结论: 「系统缓存」由所有已安装应用的 /data/data/packagename/cache 文件夹和 /sdcard/Android/data/packagename/cache 文件夹组成。 如下是原理分析,不感兴趣的可以直接跳到下一节。 我们先来看一个熟悉的界面: 这是手机的「设置」——「应用」里的已安装应用的详情页,这里面会显示缓存的大小,而且提供了清理缓存的功能,这就是我们做「系统缓存」清理想做的事情。 这里显示的大小是如何计算出来的,它实际上的文件组成是怎么样的呢?可以从 Android 系统自带的 Settings APP 的源码中找到答案。 注:下面的分析基于我手边的 Android 4.1 源码,比较古老了,但并不妨碍理解。 探索「外部缓存」 按惯例先说结论: 「外部缓存」由所有已安装应用的 /sdcard/Android/data/packagename/cache 文件夹组成。 Settings APP 的源码在 Android 源码树的 packages/apps/Settings 目录里,在它里面能找到 InstalledAppDetails.java 文件,从名字上看它应该就是对应我们上图中的「应用详情页」,它是一个 Fragment,在它的 onResume 方法中调用了 refreshUi 方法,它里面又调用了 refreshSizeInfo 方法: private void refreshSizeInfo() { if (mAppEntry.size == ApplicationsState.SIZE_INVALID || mAppEntry.size == ApplicationsState.SIZE_UNKNOWN) { ...... } else { ...... long cacheSize = mAppEntry.cacheSize + mAppEntry.externalCacheSize; if (mLastCacheSize != cacheSize) { mLastCacheSize = cacheSize; mCacheSize.setText(getSizeStr(cacheSize)); } ...... } } 这个方法定义在文件 packages/apps/Settings/src/com/android/settings/applications/InstalledAppDetails.java 中。 很显然这里的 cacheSize 就是对应上图里的缓存大小,从这几行代码的字面意义里可以看出缓存是由「内部缓存」加「外部缓存」组成,甚至可以初步推测出本节的结论,当然我是一个严谨的人,继续深究一下其中的原理。 找到给 mAppEntry 赋值的地方: private boolean refreshUi() { ...... mAppEntry = mState.getEntry(packageName); ...... } 这个方法定义在文件 packages/apps/Settings/src/com/android/settings/applications/InstalledAppDetails.java 中。 继续看 getEntry 里做了什么: AppEntry getEntry(String packageName) { ...... for (int i=0; i<mApplications.size(); i++) { ApplicationInfo info = mApplications.get(i); if (packageName.equals(info.packageName)) { entry = getEntryLocked(info); break; } } ....... } 这个方法定义在文件 packages/apps/Settings/src/com/android/settings/applications/ApplicationsState.java 中。 找到给 mApplications 添加数据的地方: void addPackage(String pkgName) { try { synchronized (mEntriesMap) { ...... ApplicationInfo info = mPm.getApplicationInfo(pkgName, PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_DISABLED_COMPONENTS); mApplications.add(info); if (!mBackgroundHandler.hasMessages(BackgroundHandler.MSG_LOAD_ENTRIES)) { mBackgroundHandler.sendEmptyMessage(BackgroundHandler.MSG_LOAD_ENTRIES); } ...... } } catch (NameNotFoundException e) { } } 这个方法定义在文件 packages/apps/Settings/src/com/android/settings/applications/ApplicationsState.java 中。 它在 mApplications.add(info); 后顺便发了个消息,经过 MSG_LOAD_ENTRIES 到 MSG_LOAD_ICONS 到 MSG_LOAD_SIZES 的消息链,我们看到一个从名字上就看出来很重要的关键方法调用 getPackageSizeInfo: class BackgroundHandler extends Handler { ...... @Override public void handleMessage(Message msg) { ...... switch (msg.what) { ...... case MSG_LOAD_ENTRIES: { ...... if (numDone >= 6) { ...... } else { sendEmptyMessage(MSG_LOAD_ICONS); } } break; case MSG_LOAD_ICONS: { ...... if (numDone >= 2) { ...... } else { sendEmptyMessage(MSG_LOAD_SIZES); } } break; case MSG_LOAD_SIZES: { synchronized (mEntriesMap) { ...... mPm.getPackageSizeInfo(mCurComputingSizePkg, mStatsObserver); ...... } } break; } } ...... } 这个类定义在文件 packages/apps/Settings/src/com/android/settings/applications/ApplicationsState.java 中。 mPm 是 PackageManager 类型的,这是一个抽象类型,它的实现类为 ApplicationPackageManager,ApplicationPackageManager.getPackageSizeInfo 里调用了 IPackageManager.getPackageSizeInfo,IPackageManager 的实例在 ContexImpl.getPackageManager 方法里通过 ActivityThread.getPackageManager() 获得,它的方法调用最终是反映到通过 Binder 机制返回的 PackageManagerService 实例上,我们找到 getPackageSizeInfo 的最终实现: public class PackageManagerService extends IPackageManager.Stub { ...... public void getPackageSizeInfo(final String packageName, final IPackageStatsObserver observer) { ...... Message msg = mHandler.obtainMessage(INIT_COPY); msg.obj = new MeasureParams(stats, observer); mHandler.sendMessage(msg); } ...... } 这个方法定义在文件 frameworks/base/services/java/com/android/server/pm/PackageManagerService.java 中。 这里我们注意 msg.obj 的类型为 MeasureParams,INIT_COPY 消息对应的处理: class PackageHandler extends Handler { private boolean mBound = false; ...... public void handleMessage(Message msg) { ...... doHandleMessage(msg); ...... } void doHandleMessage(Message msg) { switch (msg.what) { case INIT_COPY: { ...... if (!mBound) { if (!connectToService()) { ...... } else { ...... } } else { ...... } break; } case MCS_BOUND: { ...... if (params.startCopy()) { ...... } ...... break; } ...... } } ...... } 这个类定义在文件 frameworks/base/services/java/com/android/server/pm/PackageManagerService.java 中。 mBound 默认值为 false,所以会进 connectToService 方法,里面会触发 DefaultContainerConnection.onServiceConnected 回调,发送了 MCS_BOUND 的消息,通过 params.startCopy() 来到 MeasureParams 的 handleStartCopy 方法里,可以直接看到 externalCacheSize 的计算方法: void handleStartCopy() throws RemoteException { synchronized (mInstallLock) { mSuccess = getPackageSizeInfoLI(mStats.packageName, mStats); } ...... if (mounted) { final File externalCacheDir = Environment .getExternalStorageAppCacheDirectory(mStats.packageName); final long externalCacheSize = mContainerService .calculateDirectorySize(externalCacheDir.getPath()); mStats.externalCacheSize = externalCacheSize; ...... } } 这个方法定义在文件 frameworks/base/services/java/com/android/server/pm/PackageManagerService.java 中。 externalCacheSize 实际即是 Environment.getExternalStorageAppCacheDirectory 返回的文件夹的大小,来看一下它返回的文件夹是什么: public class Environment { ...... private static final File EXTERNAL_STORAGE_ANDROID_DATA_DIRECTORY = new File(new File( getDirectory("EXTERNAL_STORAGE", "/storage/sdcard0"), "Android"), "data"); ...... public static File getExternalStorageAppCacheDirectory(String packageName) { return new File(new File(EXTERNAL_STORAGE_ANDROID_DATA_DIRECTORY, packageName), "cache"); } ...... } 这个类定义在文件 frameworks/base/core/java/android/os/Environment.java 中。 一般来讲 /storage/sdcard0 都是挂载到 /sdcard,可见 Environment.getExternalStorageAppCacheDirectory 返回的就是 /sdcard/Android/data/packagename/cache。 即有小结论一: 「外部缓存」由所有已安装应用的 /sdcard/Android/data/packagename/cache 文件夹组成。 探索「内部缓存」 先说结论: 「内部缓存」由所有已安装应用的 /data/data/packagename/cache 文件夹组成。 从上面的 handleStartCopy 方法中可知 Internal 的 cacheSize 部分在 getPackageSizeInfoLI 方法里, private boolean getPackageSizeInfoLI(String packageName, PackageStats pStats) { ...... int res = mInstaller.getSizeInfo(packageName, p.mPath, publicSrcDir, asecPath, pStats); ...... } 这个方法定义在文件 frameworks/base/services/java/com/android/server/pm/PackageManagerService.java 中。 class Installer { ...... private boolean connect() { ...... LocalSocketAddress address = new LocalSocketAddress("installd", LocalSocketAddress.Namespace.RESERVED); mSocket.connect(address); ...... } ...... private synchronized String transaction(String cmd) { if (!connect()) { ...... } if (!writeCommand(cmd)) { ...... } if (readReply()) { ...... return s; } else { ...... } } public int getSizeInfo(String pkgName, String apkPath, String fwdLockApkPath, String asecPath, PackageStats pStats) { StringBuilder builder = new StringBuilder("getsize"); builder.append(' '); builder.append(pkgName); builder.append(' '); builder.append(apkPath); ...... String s = transaction(builder.toString()); String res[] = s.split(" "); ...... try { ...... pStats.cacheSize = Long.parseLong(res[3]); ...... } catch (NumberFormatException e) { return -1; } } ...... } 这个方法定义在文件 frameworks/base/services/java/com/android/server/pm/Installer.java 中。 getSizeInfo 方法最终是通过给 /dev/socket/installd 发送 getsize packagename apkpath ... 获取的输出中解析出来。 /dev/socket/installd 的源码在 frameworks/base/cmds/installd,getsize 命令最后在 get_size 函数中处理, int get_size(const char *pkgname, const char *apkpath, const char *fwdlock_apkpath, const char *asecpath, int64_t *_codesize, int64_t *_datasize, int64_t *_cachesize, int64_t* _asecsize) { ...... if (create_pkg_path(path, pkgname, PKG_DIR_POSTFIX, 0)) { goto done; } d = opendir(path); ...... dfd = dirfd(d); ...... while ((de = readdir(d))) { const char *name = de->d_name; if (de->d_type == DT_DIR) { ...... subfd = openat(dfd, name, O_RDONLY | O_DIRECTORY); if (subfd >= 0) { int64_t size = calculate_dir_size(subfd); if (!strcmp(name,"lib")) { codesize += size; } else if(!strcmp(name,"cache")) { cachesize += size; } else { datasize += size; } } } else { ...... } } ...... } 这个函数定义在文件 frameworks/base/cmds/installd/Commands.c 中。 我们来看一下 path 是什么值: int create_pkg_path(char path[PKG_PATH_MAX], const char *pkgname, const char *postfix, uid_t persona) { size_t uid_len; char* persona_prefix; if (persona == 0) { persona_prefix = PRIMARY_USER_PREFIX; uid_len = 0; } else { ...... } const size_t prefix_len = android_data_dir.len + strlen(persona_prefix) + uid_len + 1 /*slash*/; char prefix[prefix_len + 1]; char *dst = prefix; size_t dst_size = sizeof(prefix); if (append_and_increment(&dst, android_data_dir.path, &dst_size) < 0 || append_and_increment(&dst, persona_prefix, &dst_size) < 0) { ALOGE("Error building prefix for APK path"); return -1; } ...... dir_rec_t dir; dir.path = prefix; dir.len = prefix_len; return create_pkg_path_in_dir(path, &dir, pkgname, postfix); } 这个函数定义在文件 frameworks/base/cmds/installd/utils.c 中。 可见 path 就是由 android_data_dir.path,PRIMARY_USER_PREFIX,pkgname 和 PKG_DIR_POSTFIX 四段拼接起来的,pkgname 就是包名,其它几个值分别是什么呢? ...... #define PRIMARY_USER_PREFIX "data/" ...... #define PKG_DIR_POSTFIX "" ...... 这些宏定义在 frameworks/base/cmds/installd/installd.h 中 int initialize_globals() { // Get the android data directory. if (get_path_from_env(&android_data_dir, "ANDROID_DATA") < 0) { return -1; } ...... } 这个函数定义在文件 frameworks/base/cmds/installd/installd.c 中。 android_data_dir 其实是获取的系统的 ANDROID_DATA 环境变量值,就是 /data: shell@aries:/ $ echo $ANDROID_DATA /data 所以 path 的值即为 /data/data/pkgname,而 cacheSize 即为它下面的 cache 文件夹的大小。 即有小结论二: 「内部缓存」由所有已安装应用的 /data/data/packagename/cache 文件夹组成。 以上,我们的结论得证。 系统缓存大小的计算 通过上一节我们已经知道了「系统缓存」的文件构成,在想要计算系统缓存大小的时候下意识的想法就是,用代码计算一下这两个文件夹的大小不就行了? 事实证明这个想法只是 too young, sometimes naive. /data/data/packagename/cache 文件夹每个应用访问属于自己的无压力,但其它应用是没有权限读取的,如果是做本应用内的缓存清理,那事情就简单了,直接算一算就好了。 如果是要做针对所有应用的缓存清理功能,那就得另想办法了。 这里分了两种情况:能获取 root 权限和不能获取 root 权限。我们这里先讨论非 root 权限的系统缓存计算和清理,root 权限的情况在后文会有说明。 既然直接计算文件夹大小的方法行不通了,那就仍然重复上面的故事,参考 Settings APP 的做法吧。 Settings 计算缓存大小的方法 Settings APP 使用了 PackageManager.getPackageSizeInfo 方法来做此事,难道 so easy?屁颠屁颠去查了一下 Android API,发现 PacakgeManager 的文档中压根就没有出现 getPackageSizeInfo 的身影,好吧这是一个不对外公开的 API。 但是区区困难怎么拦得住一颗改变世界的心?对付隐藏 API 我们还有反射大法。 我们回顾一下 Settings APP 里的做法: class BackgroundHandler extends Handler { ...... final IPackageStatsObserver.Stub mStatsObserver = new IPackageStatsObserver.Stub() { public void onGetStatsCompleted(PackageStats stats, boolean succeeded) { ...... entry.cacheSize = stats.cacheSize; ...... entry.externalCacheSize = stats.externalCacheSize; ...... } }; ...... @Override public void handleMessage(Message msg) { ...... switch (msg.what) { ...... case MSG_LOAD_SIZES: { synchronized (mEntriesMap) { ...... mPm.getPackageSizeInfo(mCurComputingSizePkg, mStatsObserver); ...... } } break; } } ...... } 这个类定义在文件 packages/apps/Settings/src/com/android/settings/applications/ApplicationsState.java 中。 这里有两个问题需要解决: getPackageSizeInfo 方法是一个 @hide 方法,需要通过反射来调用。 从 PackageManager.java 文件的 getPackageSizeInfo 方法定义处可知,它需要 GET_PACKAGE_SIZE 权限,幸运的是,从 frameworks/base/core/res/AndroidManifex.xml 文件里可知,该权限的 Protection level 为 normal,是可以正常声明的。 <!-- Allows an application to find out the space used by any package. --> <permission android:name="android.permission.GET_PACKAGE_SIZE" android:permissionGroup="android.permission-group.SYSTEM_TOOLS" android:protectionLevel="normal" android:label="@string/permlab_getPackageSize" android:description="@string/permdesc_getPackageSize" /> 这段代码定义在文件 frameworks/base/core/res/AndroidManifex.xml 中。 传给 getPackageSizeInfo 方法的第二个参数类型 IPackageStatsObserver 是在 android.content.pm 包下,需要自已通过 aidl 方式定义。 计算缓存大小的实现 解决步骤: 在自己的工程的 src/main 目录下创建包目录结构 aidl/android/content/pm。 注:这是使用 Android Studio 的默认做法,使用 Eclipse 默认在 src 目录下创建包目录结构 android/content/pm。 将 Android 源码 frameworks/base/core/java/android/content/pm 目录下的 IPackageStatsObserver.aidl 与其依赖的 PackageStats.aidl 拷贝到上面一步创建的目录里。 根据 frameworks/base/core/java/android/content/pm/PackageManager.java 的 getPackageSizeInfo 接口上面的注释可知,需要在 AndroidManifest.xml 里声明需要 GET_PACKAGE_SIZE 权限。 <uses-permission android:name="android.permission.GET_PACKAGE_SIZE"></uses-permission> 获取 QQ 的系统缓存大小的示例代码: public void someFunc() { IPackageStatsObserver.Stub observer = new PackageSizeObserver(); getPackageInfo("com.tencent.mobileqq", observer); } public void getPackageInfo(String packageName, IPackageStatsObserver.Stub observer) { try { PackageManager pm = ContextUtil.applicationContext.getPackageManager(); Method getPackageSizeInfo = pm.getClass() .getMethod("getPackageSizeInfo", String.class, IPackageStatsObserver.class); getPackageSizeInfo.invoke(pm, packageName, observer); } catch (NoSuchMethodException e ) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } private class PackageSizeObserver extends IPackageStatsObserver.Stub { @Override public void onGetStatsCompleted(PackageStats packageStats, boolean succeeded) throws RemoteException { if (packageStats == null || !succeeded) { } else { AppEntry entry = new AppEntry(); entry.packageName = packageStats.packagename; entry.cacheSize = packageStats.cacheSize + packageStats.externalCacheSize; // do something else,比如把 entry 通过消息发送给需要的地方,或者添加到你的列表里 } } } 获取一个应用的缓存的问题解决了,获取所有应用的系统缓存也就是遍历系统已安装应用,然后挨个调用 getPackageInfo 的事儿了。 完整的实例见 https://github.com/mzlogin/CleanExpert。 系统缓存的清理 既然借鉴 Settings APP 的做法如此好使,在做缓存清理时我们当然故伎重施。我们先来看看它是怎样清理某一个应用的缓存的。 Settings 清理缓存的方法 在 InstalledAppDetails.java 里能根据名称找到对应「清除缓存」按钮相关的代码: public class InstalledAppDetails extends Fragment implements View.OnClickListener, CompoundButton.OnCheckedChangeListener, ApplicationsState.Callbacks { ...... private Button mClearCacheButton; ...... class ClearCacheObserver extends IPackageDataObserver.Stub { public void onRemoveCompleted(final String packageName, final boolean succeeded) { final Message msg = mHandler.obtainMessage(CLEAR_CACHE); msg.arg1 = succeeded ? OP_SUCCESSFUL:OP_FAILED; mHandler.sendMessage(msg); } } ...... @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ...... mClearCacheButton = (Button) view.findViewById(R.id.clear_cache_button); ...... } ...... private void refreshSizeInfo() { ...... if (cacheSize <= 0) { mClearCacheButton.setEnabled(false); } else { mClearCacheButton.setEnabled(true); mClearCacheButton.setOnClickListener(this); } } ...... public void onClick(View v) { ...... } else if (v == mClearCacheButton) { // Lazy initialization of observer if (mClearCacheObserver == null) { mClearCacheObserver = new ClearCacheObserver(); } mPm.deleteApplicationCacheFiles(packageName, mClearCacheObserver); } ...... } 这个类定义在文件 packages/apps/Settings/src/com/android/settings/applications/InstalledAppDetails.java 中。 是不是很熟悉?是不是很激动?是不是觉得顶多再次祭出反射大法就能继续拯救世界了?先冷静一下,看看 frameworks/base/core/java/android/content/pm/PackageManager.java 文件里 deleteApplicationCacheFiles 方法上面的注释。 /** * Attempts to delete the cache files associated with an application. * Since this may take a little while, the result will * be posted back to the given observer. A deletion will fail if the calling context * lacks the {@link android.Manifest.permission#DELETE_CACHE_FILES} permission, if the * named package cannot be found, or if the named package is a "system package". * * ...... * * @hide */ 没错它又是一个 @hide 方法,关键是它需要 DELETE_CACHE_FILES 权限,而该权限的相关声明如下: <!-- Allows an application to delete cache files. --> <permission android:name="android.permission.DELETE_CACHE_FILES" android:label="@string/permlab_deleteCacheFiles" android:description="@string/permdesc_deleteCacheFiles" android:protectionLevel="signature|system" /> 这段声明定义在 frameworks/base/core/res/AndroidManifest.xml 中。 它的 protectionLevel 为 signature|system,系统应用或者与系统采用相同签名的应用才能获得此权限。 此路不通。 新的发现 那就继续想其它办法了。frameworks/base/core/java/android/content/pm/PackageManager.java 里提供了很多实用的功能,比如上面的系统缓存的大小计算以及清理都是它里面声明的方法,仔细看一下它里面声明的其它方法还真是有发现: /** * Free storage by deleting LRU sorted list of cache files across * all applications. If the currently available free storage * on the device is greater than or equal to the requested * free storage, no cache files are cleared. If the currently * available storage on the device is less than the requested * free storage, some or all of the cache files across * all applications are deleted (based on last accessed time) * to increase the free storage space on the device to * the requested value. There is no guarantee that clearing all * the cache files from all applications will clear up * enough storage to achieve the desired value. * @param freeStorageSize The number of bytes of storage to be * freed by the system. Say if freeStorageSize is XX, * and the current free storage is YY, * if XX is less than YY, just return. if not free XX-YY number * of bytes if possible. * @param observer call back used to notify when * the operation is completed * * @hide */ public abstract void freeStorageAndNotify(long freeStorageSize, IPackageDataObserver observer); 从解释来看它是用来在必要时释放所有应用的缓存所占空间的,在 Android 源码里搜索一下它被调用的地方,有一处是在 frameworks/base/services/java/com/android/server/DeviceStorageMonitorService.java 中,大致的逻辑是在系统空间不够的时候,提示用户清理系统缓存。 我们来看看这个方法实际做了什么事情: public class PackageManagerService extends IPackageManager.Stub { ...... public void freeStorageAndNotify(final long freeStorageSize, final IPackageDataObserver observer) { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.CLEAR_APP_CACHE, null); // Queue up an async operation since clearing cache may take a little while. mHandler.post(new Runnable() { public void run() { mHandler.removeCallbacks(this); int retCode = -1; retCode = mInstaller.freeCache(freeStorageSize); if (retCode < 0) { Slog.w(TAG, "Couldn't clear application caches"); } if (observer != null) { try { observer.onRemoveCompleted(null, (retCode >= 0)); } catch (RemoteException e) { Slog.w(TAG, "RemoveException when invoking call back"); } } } }); } ...... } 这个类定义在文件 frameworks/base/services/java/com/android/server/pm/PackageManagerService.java 中。 也就是说,这个方法的注释里没有提及它需要申请什么权限,但事实上它是需要 CLEAR_APP_CACHE 权限的。 该权限的相关声明: <!-- Allows an application to clear the caches of all installed applications on the device. --> <permission android:name="android.permission.CLEAR_APP_CACHE" android:permissionGroup="android.permission-group.SYSTEM_TOOLS" android:protectionLevel="dangerous" android:label="@string/permlab_clearAppCache" android:description="@string/permdesc_clearAppCache" /> 这段声明定义在 frameworks/base/core/res/AndroidManifest.xml 中。 虽然它的 protectionLevel 是 dangerous,但是好歹还是能用的。 另外,跟踪实际执行清理过程的 retCode = mInstaller.freeCache(freeStorageSize); 这一行实际是通过给 /dev/socket/installd 发送 freecache freeStorageSize 来完成清理过程,最终调用到如下函数: int free_cache(int64_t free_size) { ...... char datadir[PKG_PATH_MAX]; avail = disk_free(); ...... if (avail >= free_size) return 0; if (create_persona_path(datadir, 0)) { // /data/data ...... } d = opendir(datadir); ...... dfd = dirfd(d); while ((de = readdir(d))) { if (de->d_type != DT_DIR) continue; name = de->d_name; ...... subfd = openat(dfd, name, O_RDONLY | O_DIRECTORY); if (subfd < 0) continue; delete_dir_contents_fd(subfd, "cache"); close(subfd); avail = disk_free(); if (avail >= free_size) { closedir(d); return 0; } } ...... } 这个函数定义在文件 frameworks/base/cmds/installd/Commands.c 中。 实际就是遍历 /data/data 下的所有文件夹,依次删除它们下面的 cache 子目录,直到磁盘的可用空间大于需要的空间为止。 也就是说,freeStorageAndNotify 只是删除了「内部缓存」,扩展存储上的「外部缓存」需要我们另外处理。 清理缓存的实现 参考 frameworks/base/services/java/com/android/server/DeviceStorageMonitorService.java 中对 freeStorageAndNotify 的相关调用,最后我们的实现步骤如下: 在自己的工程的 src/main 目录下创建包目录结构 aidl/android/content/pm。 注:这是使用 Android Studio 的默认做法,使用 Eclipse 默认在 src 目录下创建包目录结构 android/content/pm。 将 Android 源码 frameworks/base/core/java/android/content/pm 目录下的 IPackageDataObserver.aidl 拷贝到上面一步创建的目录里。 在 AndroidManifest.xml 里声明 CLEAR_APP_CACHE 权限。 <uses-permission android:name="android.permission.CLEAR_APP_CACHE"></uses-permission> 通过反射调用 freeStorageAndNotify 方法,第一个参数给它一个足够大的值,它就会帮我们清理掉所有应用的缓存了。 public static void freeAllAppsCache(final Handler handler) { Context context = ContextUtil.applicationContext; File externalDir = context.getExternalCacheDir(); if (externalDir == null) { return; } PackageManager pm = context.getPackageManager(); List<ApplicationInfo> installedPackages = pm.getInstalledApplications(PackageManager.GET_GIDS); for (ApplicationInfo info : installedPackages) { String externalCacheDir = externalDir.getAbsolutePath() .replace(context.getPackageName(), info.packageName); File externalCache = new File(externalCacheDir); if (externalCache.exists() && externalCache.isDirectory()) { deleteFile(externalCache); } } try { Method freeStorageAndNotify = pm.getClass() .getMethod("freeStorageAndNotify", long.class, IPackageDataObserver.class); long freeStorageSize = Long.MAX_VALUE; freeStorageAndNotify.invoke(pm, freeStorageSize, new IPackageDataObserver.Stub() { @Override public void onRemoveCompleted(String packageName, boolean succeeded) throws RemoteException { Message msg = handler.obtainMessage(JunkCleanActivity.MSG_SYS_CACHE_CLEAN_FINISH); msg.sendToTarget(); } }); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } public static boolean deleteFile(File file) { if (file.isDirectory()) { String[] children = file.list(); for (String name : children) { boolean suc = deleteFile(new File(file, name)); if (!suc) { return false; } } } return file.delete(); } 完整的实例见 https://github.com/mzlogin/CleanExpert。 备注:经测试该方法在 Android 6.0 版本和部分 5.0+ 版本上已经失效,Android 源码里已经给 freeStorageAndNotify 方法声明添加了 @SystemApi 注释(开始添加了 @PrivateApi,后修改为 @SystemApi),见「添加」和「修改」两次提交,而且 CLEAR_APP_CACHE 方法的权限已经由 dangerous 改成了 system|signature,已经无法通过反射来正常调用,会产生 java.lang.reflect.InvocationTargetException,所以在这些版本上需要另想办法了,StackOverflow 上的一个相关讨论链接:What’s the meaning of new @SystemApi annotation, any difference from @hide?。 有 root 权限的系统缓存计算与清理 如果能获取到 root 权限,/data/data 目录的访问限制也就不再是问题,计算缓存大小和清理缓存也就不用再受上面说的方法与权限的限制了,而且能做一些没有 root 权限的情况下做不到的事情,比如: 清理单个应用的缓存。 列出应用的缓存文件列表供用户选择性清理。 实现思路很简单粗暴(如下思路未写实例验证): 思路一 通过 su 命令获取一个有 root 权限的 shell,然后通过与它交互来获取缓存文件夹的大小或清理缓存,比如让它执行命令 du -h /data/data/com.trello/cache 就能获取到 trello 的「内部缓存」大小,让它执行 rm -rf /data/data/com.trello/cache 就能删除 trello 的「内部缓存」。 注:du 命令行与参数在不同 ROM 下的不一致,所以并不推荐此做法。 思路二 或者,也可以做一个原生程序专门来负责缓存计算与清理,通过 su 命令获取有 root 权限的 shell,再用 shell 创建该原生程序进程,它继承 shell 的 root 权限,然后它就可以计算缓存大小与清理缓存,再将结果上报给 APP 进程。

    2016/01/14 Android

  7. Ubuntu 使用笔记

    使用 Ubuntu 过程中遇到的问题及解决方案。 使用 git pull 遇到问题 提示 Agent admitted failure to sign using the key. Permission denied (publickey). fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. 解决方法: ssh-add ~/.ssh/id_rsa 图形界面编辑配置文件 安装 dconf-editor。 配置 Exchange 为 ThunderBird 安装插件 ExQuilla,有时被墙。 http://mesquilla.net/exquilla-currentrelease-tb-linux.xpi 安装和配置 JDK 在 Terminal 运行: sudo add-apt-repository ppa:webupd8team/java sudo apt-get update sudo apt-get install oracle-java8-installer sudo vim /etc/profile export JAVA_HOME=/usr/lib/jvm/java-8-oracle export JRE_HOME=$JAVA_HOME/jre export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH 配置 adt 安装兼容 32 位 adb 运行的环境 sudo apt-get install lib32z1 lib32ncurses5 lib32bz2-1.0 lib32stdc++6 添加路径到 $PATH 环境变量,修改 /etc/profile 或 ~/.profile 等皆可。 export ANDROID_SDK_HOME=/home/mzlogin/android/sdk export PATH=$ANDROID_SDK_HOME/platform-tools:$ANDROID_SDK_HOME/tools:$PATH 安装 SVN 图形前端 RabbitVCS 在 Terminal 运行: sudo add-apt-repository ppa:rabbitvcs/ppa sudo apt-get update sudo apt-get install rabbitvcs-nautilus3 rabbitvcs-thunar rabbitvcs-gedit rabbitvcs-cli 创建 eclipse 快捷方式 /usr/share/applications 里新建 Eclipse.desktop,填如下内容: [Desktop Entry] Name=Eclipse Comment=Launch Eclipse Exec=/home/mzlogin/android/eclipse/eclipse Icon=/home/mzlogin/android/eclipse/icon.xpm StartupNotify=true Terminal=false Type=Application 安装 XMind 到 XMind 官网下载安装包,然后 : sudo dpkg --ignore-depends=lame,libwebkitgtk-1.0-0 -i xmind-linux-3.5.0.201410310637_amd64.deb 切换输入法 添加一个英文,一个五笔,将切换到上一个源和下一个源的快捷键分别设为左和右 Shift,这样就可以使用左右 Shift 在中英之间来回切换了。 安装 im-switch 会导致语言支持被移除,恢复用 sudo apt-get install language-selector-gnome。 消除启动 gVim 在 terminal 中的警告 安装 vim-gnome 后运行 gVim 会提示: GLib-GObject-WARNING **: Attempt to add property GnomeProgram::sm-connect after class was initialised 改为安装 vim-gtk 就好了。 解决 ibus 五笔候选词水平显示和个数的问题 修改 /usr/share/ibus-table/tables/wubi-jidian86.db 的 ime 表里的 orientation(水平 0 垂直 1)和 select_keys(有几个选择键就有几个项,从下面代码可知用 , 分隔)。 /usr/share/ibus-tables/engine/tabsqlitedb.py 中 def get_page_size (self): return len(self.get_select_keys().split(',')) 将 Caps Lock 映射为 Ctrl 安装 Gnome Tweak Tool sudo apt-get install gnome-tweak-tool 打开 tweak-tool,找到「打字」-「大写锁定键行为」,选择「将 CapsLock 作为额外的 Ctrl」 参考 http://askubuntu.com/questions/462021/how-do-i-turn-caps-lock-into-an-extra-control-key 将个人文件夹下文件夹名改为英文 ~ 目录下的「桌面」和「文档」等文件夹是中文,在 Terminal 下输入很不方便,将其改为英文的方法: 打开 ~/.config/user-dirs.dirs,将其中的中文改掉: XDG_DESKTOP_DIR="$HOME/desktop" XDG_DOWNLOAD_DIR="$HOME/downloads" XDG_TEMPLATES_DIR="$HOME/templates" XDG_PUBLICSHARE_DIR="$HOME/public" XDG_DOCUMENTS_DIR="$HOME/documents" XDG_MUSIC_DIR="$HOME/music" XDG_PICTURES_DIR="$HOME/pictures" XDG_VIDEOS_DIR="$HOME/videos" 在文件管理器中将 HOME 目录下的中文文件夹名改成与上面的配置对应。 输入「」与『』 极点五笔中文输入状态下,按 [] 即输入「」,按 {} 即输入『』。 VirtualBox 里 Ubuntu 分辨率无法调整 Ubuntu 14.04 LTS 在 VirtualBox 中刚安装完时,分辨率只有 640*480 一种选项,无法调整。 解决方法: 打开 xdiagnose 勾选 Debug 下的所有选项 重启 安装增强功能 然后: cd /media/<username>/VBOXADDITIONS_X.X.XX_XXXXX sudo ./VBoxLinuxAdditions.run (注意把 username 替换成自己的,VBOXADDITIONS 后面的 X 换成具体版本号) 与 Win7 共享 SSH key 如下步骤适用于在 Ubuntu 上使用从 Win7 拷贝的 SSH key,反之应该也一样能用。 创建 ~/.ssh 目录,确认其权限为 0700,将 Windows %userprofile%/.ssh 下的 id_rsa 和 id_rsa.pub 文件拷贝到 ~/.ssh 目录下,权限分别改为 0600 和 0644。 mzlogin@ubuntu:~$ ll ~/.ssh total 20 drwx------ 2 mzlogin mzlogin 4096 Jun 22 01:03 ./ drwxr-xr-x 20 mzlogin mzlogin 4096 Jun 22 01:02 ../ -rw------- 1 mzlogin mzlogin 1679 Jun 21 05:17 id_rsa -rw-r--r-- 1 mzlogin mzlogin 399 Jun 21 05:17 id_rsa.pub 然后 ssh-add ~/.ssh/id_rsa

    2016/01/12 Linux

  8. 为 Markdown 生成 TOC 的 Vim 插件

    因为饱受 GFM 和 Redcarpet 两种 Markdown 引擎生成 TOC 链接的差异的折磨,而我又不得不同时使用它们——博客基于 Jekyll 使用 Redcarpet(Update 2016/09/16: GitHub Pages 现在已经改为只支持 kramdown),而其它放在 GitHub 仓库里的文档使用 GFM,我决定为我常用的 Markdown 编辑器 Vim 做一款同时支持 GFM 和 Redcarpet 两种 TOC 链接风格的 Table of Contents 自动生成插件。 这算是我真正意义上完全独立开发的第一款实用 Vim 插件,当然开发过程中也参考了别人的做法。 下载地址 vim-markdown-toc 功能 为 Markdown 文件生成 Table of Contents,目前支持 GFM 和 Redcarpet 两种链接风格。 更新已经存在的 Table of Contents。 保存文件时自动更新 Table of Contents。 使用方法 生成 Table of Contents 将光标移动到想在后面插入 Table of Contents 的那一行,然后运行下面的某个命令: :GenTocGFM 生成 GFM 链接风格的 Table of Contents。 适用于 GitHub 仓库里的 Markdown 文件,比如 README.md,也适用用于生成 GitBook 的 Markdown 文件。 :GenTocRedcarpet 生成 Redcarpet 链接风格的 Table of Contents。 适用于使用 Redcarpet 作为 Markdown 引擎的 Jekyll 项目或其它地方。 更新已存在的 Table of Contents 通常不需要手动做这件事,保存文件时会自动更新已经存在的 Table of Contents。 除非是在配置里关闭了保存时自动更新,并且维持插入 Table of Contents 前后的 <!-- vim-markdown-toc -->,此时可使用 :UpdateToc 命令手动更新。 删除 Table of Contents :RemoveToc 命令可以帮你删除本插件生成的 Table of Contents。 安装方法 推荐使用 Vundle 来管理你的 Vim 插件,这样你就可以简单三步完成安装: 在你的 vimrc 文件中添加如下内容: Plugin 'mzlogin/vim-markdown-toc' :so $MYVIMRC :PluginInstall 使用 vim-plug 安装的过程的与此基本一样。 配置选项 g:vmt_auto_update_on_save 默认值:1 插件会自动更新已经存在的 Table of Contents,如果你不想要这个功能,可以在你的 vimrc 文件里加入如下内容关闭: let g:vmt_auto_update_on_save = 0 g:vmt_dont_insert_fence 默认值:0 在默认情况下,:GenTocXXX 命令会在插入的 Table of Contents 前后加上 <!-- vim-markdown-toc -->,这是为了实现自动和手动更新 Table of Contents 功能。 如果你不想看到它们,可以在 vimrc 文件里加入如下内容移除: let g:vmt_dont_insert_fence = 1 需要注意的是移除之后插件将无法再帮你保存文件时自动更新 Table of Contents 了,也无法使用 :UpdateToc 命令了。这里如果还想更新 Table of Contents,只能先手动删除已经存在的,然后重新运行 :GenTocXXX 命令。 g:vmt_cycle_list_item_markers 默认值:0 在默认情况下,所有 Table of Contents 项目前面的标记都是 *: * [Level 1](#level-1) * [Level 1-1](#level-1-1) * [Level 1-2](#level-1-2) * [Level 1-2-1](#level-1-2-1) * [Level 2](level-2) 这里提供一个选项改变这个行为,如果设置: let g:vmt_cycle_list_item_markers = 1 那标记将根据级别循环使用 *、- 和 +: * [Level 1](#level-1) - [Level 1-1](#level-1-1) - [Level 1-2](#level-1-2) + [Level 1-2-1](#level-1-2-1) * [Level 2](level-2) 这不会影响 Markdown 文档解析后的显示效果,只用于提升源文件的可读性。 屏幕截图 使用本插件生成 TOC 的英文文档在线示例 使用本插件生成 TOC 的中文文档在线示例 参考链接 GFM 与 Redcarpet 的不同点 ajorgensen/vim-markdown-toc

    2015/12/19 Vim

  9. Java 对象释放与 finalize 方法

    本文谈论的知识很浅显,只是我发现自己掌握的相关知识并不扎实,对细节并不清楚,遂将疑惑解开,并记录于此。 按惯例先上结论,对如下知识点已经清楚的选手可以省下看本文的时间了。 结论 对象的 finalize 方法不一定会被调用,即使是进程退出前。 发生 GC 时一个对象的内存是否释放取决于是否存在该对象的引用,如果该对象包含对象成员,那对象成员也遵循本条。 对象里包含的对象成员按声明顺序进行释放。 证明 假设有以下类定义: class A { public A() { System.out.println("A()"); } protected void finalize() { System.out.println("~A()"); } B b; } class B { public B() { System.out.println("B()"); } protected void finalize() { System.out.println("~B()"); } } 结论 1 证明 在 main 方法中有如下代码: A a = new A(); B b = new B(); a.b = b; a = null; 输出是什么呢? A() B() 与我想象中的有些不一样,我以为至少在进程退出前 A 类对象和 B 类对象都会被释放掉的。 我们明确一下 finalize 方法的调用时机,引用官方 API 文档的解释: Called by the garbage collector on an object when garbage collection determines that there are no more references to the object. A subclass overrides the finalize method to dispose of system resources or to perform other cleanup. 也就是说,finalize 是在 JVM 执行 GC 的时候才会执行的,而很显然,在这个例子里 main 方法退出时并没有执行 GC,而 GC 是否执行以及其执行的时机并不是我们可以精确控制的,此即证明了结论 1。 结论 2 证明 虽然我们不能精确控制 GC 的时机,但我们可以给 JVM 建议,比如我们在最后加个 System.gc() 建议 JVM 进行 GC。 A a = new A(); B b = new B(); a.b = b; a = null; System.gc(); 现在输出变成了 A() B() ~A() 可见 JVM 听从了我们的建议,执行了 GC,由于此时 A 类对象已经没有引用了,所以它被释放,而该对象的 B 类对象成员由于被局部变量 b 引用,此时不会释放。 而一个在 GC 时对象成员也会被释放的 A 类对象调用是怎么样的呢? A a = new A(); a.b = new B(); a = null; System.gc(); 此时输出为 A() B() ~B() ~A() 如上两段代码执行结果的对比证明了结论 2。 另外需要说明的是,Runtime 类里有一个 runFinalizersOnExit 方法,可以让程序在退出时执行所有对象的未被自动调用 finalize 方法,即使该对象仍被引用。但是从官方文档可以看出,该方法已经废弃,不建议使用,引用官方 API 文档如下: Deprecated. This method is inherently unsafe. It may result in finalizers being called on live objects while other threads are concurrently manipulating those objects, resulting in erratic behavior or deadlock. Enable or disable finalization on exit; doing so specifies that the finalizers of all objects that have finalizers that have not yet been automatically invoked are to be run before the Java runtime exits. By default, finalization on exit is disabled. 而同样是 Runtime 类里的 runFinalization 方法则在调用后并没有看到明显的效果,即如果不发生 GC,那即使调用了 runFinalization 方法,已经待回收的对象的 finalize 方法依然没有被调用。 结论 3 证明 我们修改一下几个类的定义: class A { public A() { System.out.println("A()"); } protected void finalize() { System.out.println("~A()"); } B b; // line a C c; // line b } class B { public B() { System.out.println("B()"); } protected void finalize() { System.out.println("~B()"); } } class C { public C() { System.out.println("C()"); } protected void finalize() { System.out.println("~C()"); } } 现在在 main 方法里有如下调用: A a = new A(); a.b = new B(); a.c = new C(); a = null; System.gc(); 输出是 A() B() C() ~B() ~C() ~A() 而如果我们互换一下 A 类声明带注释的 line a 与 line b 的位置,即变成 C c; // line b B b; // line a 输出变成 A() B() C() ~C() ~B() ~A() 此即证明了结论 3。

    2015/12/15 Java

  10. GFM 与 Redcarpet 的不同点

    GFM 即 GitHub Flavored Markdown,是 GitHub 用在 Respository、Issues、Comments 和 Pull requests 里的一种 Markdown 引擎,它与标准 Markdown 有所区别,增加了一些 GitHub 自己扩展的功能。 Redcarpet 是另一种 Markdown 引擎,我的基于 GitHub Pages 的博客采用它来解析 md 文件,_config.yml 文件里的配置如下: markdown: redcarpet redcarpet: extensions: - no_intra_emphasis - fenced_code_blocks - autolink - tables - with_toc_data - strikethrough 在 vmg/redcarpet#379 的讨论中可以得知 GFM 其实是基于 Redcarpet 的一个非开源子集开发的,Redcarpet 也支持众多自定义的扩展,本文记录的是当前 GFM 与使用如上配置的 Redcarpet 的一些差异,以备在 GitHub 不同的地方写作时参考。 目录 换行 锚点链接 列表下嵌套内容 YML 解析 GFM 独有特性 Task Lists 自动生成引用链接 Emoji 参考链接 换行 第一行(后面没有空格) 第二行 在 GFM 里会显示成跟上面一样。 而在 Redcarpet 里会显示成 第一行(后面没有空格)第二行 在 Redcarpet 里如果需要换行,要么在行尾加两个空格,要么在下面空一行新开一个段落。 锚点链接 GFM 与 Redcarpet 支持对 #、## 和 ### 这样的标题自动生成锚点链接,只不过在生成的链接 url 上会有少许差异。 当然,强烈建议在标题中不要使用奇怪的符号。 在这里做个小广告:如果你使用 Vim 编辑 Markdown,那可以试试我写的能自动生成 GFM 和 Redcarpet 这两种风格 TOC 的 Vim 插件 vim-markdown-toc。 共同点: 反引号(即 1 左边那个符号)会直接忽略掉。 字母要全小写。 空格会转换成 -。 不同点: 下面的表格列举了一些我曾经遇到过的案例,并不全,完整的实现逻辑在表格下方有说明。 字符 GFM Redcarpet " 忽略 替换成 quot,如果前后有字符,用 - 连接 ' 忽略 替换成 39,如果前后有字符,用 - 连接 & 忽略 替换成 amp,如果前后有字符,用 - 连接 / 忽略 首尾的忽略,中间的替换成 - @ 忽略 首尾的忽略,中间的替换成 - # 忽略 首尾的忽略,中间的替换成 - $ 忽略 首尾的忽略,中间的替换成 - % 忽略 首尾的忽略,中间的替换成 - ^ 忽略 首尾的忽略,中间的替换成 - + 忽略   \* 忽略 首尾的忽略,中间的替换成 - ~ 忽略 首尾的忽略,中间的替换成 - ; 忽略 首尾的忽略,中间的替换成 - . 忽略 首尾的忽略,中间的替换成 - , 忽略 首尾的忽略,中间的替换成 - ? 忽略 首尾的忽略,中间的替换成 - : 忽略 首尾的忽略,中间的替换成 - 竖线 忽略 首尾的忽略,中间的替换成 - ! 忽略 首尾的忽略,中间的替换成 - () 忽略 替换成 - ( 忽略 忽略 ) 忽略 忽略 ? 忽略 保留 , 忽略 保留 。 忽略 保留 、 忽略 保留 ! 忽略 保留 : 忽略 保留 ; 忽略 保留 “ 忽略 保留 ” 忽略 保留 《 忽略 保留 》 忽略 保留 「 忽略 保留 」 忽略 保留 『 忽略 保留 』 忽略 保留 —— 忽略 保留 总的来说就是 GFM 遇到奇怪的字符就忽略,而 Redcarpet 应用了几种不同的规则来处理。 当然这只是表面上看起来的现象,这里简单说一下它们的实现逻辑: GFM 的 TOC 链接处理实现 参考链接(by Ruby) 使用 Ruby 的正则表达式 /[^\p{Word}\- ]/u 过滤掉所有中英文标点符号、特殊符号等。 将空格替换为 -。 如果相同的链接 id 已经存在了,那在链接 id 后面添加 -{num},比如标题 hello,world 生成链接 #helloworld,而标题 hello!world 生成链接 #helloworld-1。 Redcarpet 的 TOC 链接处理实现 参考链接(by C) 将 HTML 标签,即成对的 < 与 > 及它们之间的内容删除。 进行 HTML Encode,即将 &、" 和 ' 等转换为相应 HTML 实体。 将字符 -&+$,/:;=?@"#{}|^~[]`\*()%.! 和空格替换为 -,有两个及以上 - 的地方修复成一个,将链接串首尾的 -_ 删除。 列表下嵌套内容 在 Redcarpet 中有如下规则: 如果嵌套非列表,需要缩进并且空行。 如果嵌套列表,需要缩进,但不空行。 而 GFM 则没有。 YML 解析 在 Redcarpet 中,解析头部 YML 里的内容有些需要转义: --- keywords: C\+\+ --- 而 GFM 则不需要。 GFM 独有特性 GFM 自己添加的一些特性我甚是喜欢,可惜在 GitHub Pages 里使用 Redcarpet 享受不到了。 Task Lists 这货真是个好东西,用 - [ ] 和 - [ x ] 就能做出清单列表项的显示效果来,而且如果你有编辑权限的话点选后文本能自动更新。 用法参见 Task Lists in GFM: Issues/Pulls, Comments 自动生成引用链接 Update 2015-12-06 参见 @Mentions on GitHub Pages,GitHub Pages 现在也支持使用 @username 来 at 一个 GitHub 用户了,只是用户不会收到通知。我对此功能并无需求,而且貌似会对其它使用 @ 号的地方产生非预期的解析,所以本博客当前并未启用。 对于如下格式的文本,GFM 会自动创建到对应用户对应仓库的对应链接。 * SHA: a5c3785ed8d6a35868bc169f07e40e889087fd2e * User@SHA: jlord@a5c3785ed8d6a35868bc169f07e40e889087fd2e * User/Repository@SHA: jlord/sheetsee.js@a5c3785ed8d6a35868bc169f07e40e889087fd2e * #Num: #26 * GH-Num: GH-26 * User#Num: jlord#26 * User/Repository#Num: jlord/sheetsee.js#26 Emoji Update 2015-12-06 参见 Emoji on GitHub Pages, GitHub Pages 现在也支持使用 Emoji 表情啦!:+1::+1::+1: GitHub Pages 如果能使用这个,文章一定生动不少。 参考链接 GitHub Flavored Markdown Writing on GitHub

    2015/12/05 Markdown