一句话的Android增量更新框架

Android应用更新要使用完整的新版本Apk安装,增量更新则是提供一个新旧版本偏差数据的patch包供应用下载,然后Android应用本地使用patch包和本地apk合成新版本apk。而patch包的体积通常都远小于新版本的apk,可以为用户节省流量和下载时间,节省时间就是延续生命,所以增量更新十分实用。

一些学习文章:
Android应用的增量更新
Android 增量更新完全解析 是增量不是热修复

资料里十分详细的介绍了如何在你自己的Android项目中部署增量更新功能,而实际上这个部署过程对新手来说是复杂而浪费时间的。它需要做配置NDK,并移植bsdiff/bspatch工具到Android系统,编写jni调用等麻烦事,这是坠痛苦的

I am Angry!!! 你们这样搞是不行的!!!

应运而生的BigNews框架(Github: ha-excited/BigNews)为你省去了麻烦的增量更新部署过程,无需添加代码配置文件以及NDK编译,你只需要:

  1. 在你项目根build.gradle添加代码:

    1
    2
    3
    4
    5
    6
    allprojects {
    repositories {
    ...
    maven { url 'https://jitpack.io' }
    }
    }
  2. 在你项目模块内的build.gradle添加代码,然后Gradle Sync:

    1
    2
    3
    dependencies {
    compile 'com.github.ha-excited:BigNews:0.1.1'
    }
  3. 下载到patch文件后,你只需要写一句话,就可以合成新版本apk了。

    1
    2
    3
    4
    5
    6
    String oldApkPath = ...;
    String newApkPath = ...;
    String patchPath = ...;
    //我就说一句话,这是坠吼的!
    BigNews.make(oldApkPath, newApkPath, patchPath);
    //已经弄出了一个大新。。安装包放在newApkPath路径下,随时准备升级!!

简直是Too simple!!!excited!!!

很惭愧,做了一点微小的工作, 谢谢大家。日-日

Android应用的增量更新

介绍

Android应用更新版本时需要下载apk文件进行安装,而现在的apk都比较大,一次下载一个完整的apk包太冤枉。因此增量更新应运而生。

原理很简单:通过linux工具bsdiff计算新老apk文件的差异,将差异记录为一个体积较小的patch包。通过bspatch工具(和bsdiff配套的)将老apk文件与这个patch包合并为新apk文件并安装,达到增量更新的目的。

由此可得方案:应用在做版本更新操作时获取到了版本号有更新,就将本地的老apk包的MD5校验值与版本号提交给服务器。服务器检验md5和版本号后,把使用bsdiff生成的patch包下载地址与最新版本apk的MD5校验值传回给应用。
手机端应用程序下载到patch后,使用内置的bspatch工具将自己本地旧的apk文件与patch包合并为新版本apk文件,计算校验值正确便开始安装。整个流程若校验值错误,则下载全量包安装。

目标: 编写一个demo,实现通过读取磁盘上的patch文件实现自我更新。

  1. 首先实验一下bsdiff/bspatch工具是否能够正常工作,达到预期效果。

    本人在Win10使用了Linux子系统功能,自带了一个Ubuntu14.04,直接用命令安装bsdiff。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    ➜ ~ sudo apt-get install bsdiff
    [sudo] password for administrator:
    Reading package lists... Done
    Building dependency tree
    Reading state information... Done
    The following NEW packages will be installed:
    bsdiff
    0 upgraded, 1 newly installed, 0 to remove and 98 not upgraded.
    Need to get 14.5 kB of archives.
    After this operation, 69.6 kB of additional disk space will be used.
    Get:1 http://archive.ubuntu.com/ubuntu/ trusty/universe bsdiff amd64 4.3-15 [14. 5 kB]
    Fetched 14.5 kB in 10s (1,335 B/s)
    Selecting previously unselected package bsdiff.
    (Reading database ... 33465 files and directories currently installed.)
    Preparing to unpack .../bsdiff_4.3-15_amd64.deb ...
    Unpacking bsdiff (4.3-15) ...
    Processing triggers for man-db (2.6.7.1-1ubuntu1) ...
    Setting up bsdiff (4.3-15) ...
    ➜ ~ bsdiff
    bsdiff: usage: bsdiff oldfile newfile patchfile
    ➜ ~ bspatch
    bspatch: usage: bspatch oldfile newfile patchfile

    我们可以看到bsdiff/bspatch都正常安装了。

    接下来,这边生成了两个Android apk,old.apk和new.apk。new.apk相对old.apk修改了一点代码。
    我们这里把它们的md5都算出来。

    1
    2
    3
    4
    5
    6
    7
    ➜ app git:(master) ✗ ll *.apk
    -rwxrwxrwx 1 root root 22M Mar 27 09:56 new.apk
    -rwxrwxrwx 1 root root 22M Mar 27 09:32 old.apk
    ➜ app git:(master) ✗ md5sum old.apk
    045382b701ce372aecd5bb87ee1e526d old.apk
    ➜ app git:(master) ✗ md5sum new.apk
    cbb1afdbc32e4d1c62c4d38674a6a3a9 new.apk

    然后用bisdiff 通过old/new.apk文件 打出一个patch

    1
    2
    3
    ➜ app git:(master) ✗ bsdiff old.apk new.apk patch
    ➜ app git:(master) ✗ ll patch
    -rwxrwxrwx 1 root root 3.5M Mar 27 13:52 patch

    打patch包过程时间稍微有点长,十几秒左右。可以看到打出来的patch包只有3.5M。

    接下来实验一下将old.apk与patch包合并,能否得到刚才的new.apk

    1
    2
    3
    ➜ app git:(master) ✗ bspatch old.apk out.apk patch
    ➜ app git:(master) ✗ md5sum out.apk
    cbb1afdbc32e4d1c62c4d38674a6a3a9 out.apk

    可以看到这里out.apk的MD5校验值和之前的new.apk是一样的。这个工具通过了实验,若能将它应用到项目,每次只用下载很少的数据就能完成版本更新。

  2. 如何将它应用到项目呢?按照刚才的实验,可以发现合包需要bspatch工具。我们需要做的是将bspatch加入到Android代码中。

    一个显而易见的方案就是将bspatch的源码(地址:bsdiff)使用ndk编译为so文件,通过jni调用。这样有点麻烦,不过我们可以方便一点:
    有个叫SmartAppUpdate(Github: SmartAppUpdate)的项目,它已经把bspatch的源码加入到jni内了。只要下载它并编译,就可以在应用内嵌入bspatch,实现增量更新了。

    配置好NDK,在SmartAppUpdates的目录内执行ndk-build:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    \ApkPatchLibrary\app\src\main\jni>ndk-build
    [arm64-v8a] Compile : ApkPatchLibrary <= com_cundong_utils_PatchUtils.c
    [arm64-v8a] SharedLibrary : libApkPatchLibrary.so
    [arm64-v8a] Install : libApkPatchLibrary.so => libs/arm64-v8a/libApkPatchLibrary.so
    [x86_64] Compile : ApkPatchLibrary <= com_cundong_utils_PatchUtils.c
    [x86_64] SharedLibrary : libApkPatchLibrary.so
    [x86_64] Install : libApkPatchLibrary.so => libs/x86_64/libApkPatchLibrary.so
    [mips64] Compile : ApkPatchLibrary <= com_cundong_utils_PatchUtils.c
    [mips64] SharedLibrary : libApkPatchLibrary.so
    [mips64] Install : libApkPatchLibrary.so => libs/mips64/libApkPatchLibrary.so
    [armeabi-v7a] Compile thumb : ApkPatchLibrary <= com_cundong_utils_PatchUtils.c
    [armeabi-v7a] SharedLibrary : libApkPatchLibrary.so
    [armeabi-v7a] Install : libApkPatchLibrary.so => libs/armeabi-v7a/libApkPatchLibrary.so
    [armeabi] Compile thumb : ApkPatchLibrary <= com_cundong_utils_PatchUtils.c
    [armeabi] SharedLibrary : libApkPatchLibrary.so
    [armeabi] Install : libApkPatchLibrary.so => libs/armeabi/libApkPatchLibrary.so
    [x86] Compile : ApkPatchLibrary <= com_cundong_utils_PatchUtils.c
    [x86] SharedLibrary : libApkPatchLibrary.so
    [x86] Install : libApkPatchLibrary.so => libs/x86/libApkPatchLibrary.so
    [mips] Compile : ApkPatchLibrary <= com_cundong_utils_PatchUtils.c
    [mips] SharedLibrary : libApkPatchLibrary.so
    [mips] Install : libApkPatchLibrary.so => libs/mips/libApkPatchLibrary.so
    \ApkPatchLibrary\app\src\main\jni>

    到这一步,我们可以得到名为libApkPatchLibrary.so的库,通过com.cundong.utils.PatchUtils模块来调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class PatchUtils {
    /**
    * native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath
    *
    * 返回:0,说明操作成功
    *
    * @param oldApkPath 示例:/sdcard/old.apk
    * @param newApkPath 示例:/sdcard/new.apk
    * @param patchPath 示例:/sdcard/xx.patch
    * @return
    */
    public static native int patch(String oldApkPath, String newApkPath,
    String patchPath);
    }

    被调用的相应jni代码为

    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
    /*
    * Class: com_cundong_utils_PatchUtils
    * Method: patch
    * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
    */
    JNIEXPORT jint JNICALL Java_com_cundong_utils_PatchUtils_patch(JNIEnv *env,
    jobject obj, jstring old, jstring new, jstring patch) {
    char * ch[4];
    ch[0] = "bspatch";
    ch[1] = (char*) ((*env)->GetStringUTFChars(env, old, 0));
    ch[2] = (char*) ((*env)->GetStringUTFChars(env, new, 0));
    ch[3] = (char*) ((*env)->GetStringUTFChars(env, patch, 0));
    __android_log_print(ANDROID_LOG_INFO, "ApkPatchLibrary", "old = %s ", ch[1]);
    __android_log_print(ANDROID_LOG_INFO, "ApkPatchLibrary", "new = %s ", ch[2]);
    __android_log_print(ANDROID_LOG_INFO, "ApkPatchLibrary", "patch = %s ", ch[3]);
    int ret = applypatch(4, ch);
    __android_log_print(ANDROID_LOG_INFO, "ApkPatchLibrary", "applypatch result = %d ", ret);
    (*env)->ReleaseStringUTFChars(env, old, ch[1]);
    (*env)->ReleaseStringUTFChars(env, new, ch[2]);
    (*env)->ReleaseStringUTFChars(env, patch, ch[3]);
    return ret;
    }

    其实applypatch函数就是bspatch代码的main函数,它这里改了个名字。

    现在,我们在SmartAppUpdates的Demo项目中测试加入App中的bspatch代码是否能够正常工作:
    把刚才的old.apk和patch包拷贝到手机的/sdcard/目录,并将如下代码加到App的onCreate()方法内。

    1
    2
    3
    PatchUtils.patch(Environment.getExternalStorageDirectory().getPath() + "/old.apk",
    Environment.getExternalStorageDirectory().getPath() + "/out.apk",
    Environment.getExternalStorageDirectory().getPath() + "/patch");

    运行APP后,按照预期/sdcard/目录会生成out.apk。通过adb shell在手机内执行命令:

    1
    2
    /sdcard $ md5sum out.apk
    cbb1afdbc32e4d1c62c4d38674a6a3a9 out.apk

    这里可以看到在app程序内嵌入的bspatch也成功的通过了老apk和patch包生成了新的apk,生成的out.apk的MD5值与new.apk是一致的。测试通过。

  3. 自我更新
    新建一个Android项目,名字叫PatchDemo。它可以通过patch包实现自我更新。代码为:

    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
    public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Button button = (Button) findViewById(R.id.button);
    button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    patch();
    }
    });
    }
    private void patch() {
    String outPath = Environment.getExternalStorageDirectory().getPath() + "/new.apk";
    if (PatchUtils.patch(this, Environment.getExternalStorageDirectory().getPath() + "/update.patch", outPath)) {
    Toast.makeText(this, "updating !", Toast.LENGTH_LONG).show();
    startActivity(new Intent(Intent.ACTION_VIEW).setDataAndType(Uri.fromFile(new File(outPath)),
    "application/vnd.android.package-archive"));
    } else {
    Toast.makeText(this, "cannot update", Toast.LENGTH_LONG).show();
    }
    }
    }

    PatchUtils代码改为:

    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
    public class PatchUtils {
    static {
    System.loadLibrary("ApkPatchLibrary");
    }
    /**
    * native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath
    * <p>
    * 返回:0,说明操作成功
    *
    * @param oldApkPath 示例:/sdcard/old.apk
    * @param newApkPath 示例:/sdcard/new.apk
    * @param patchPath 示例:/sdcard/xx.patch
    * @return
    */
    public static native int patch(String oldApkPath, String newApkPath,
    String patchPath);
    /**
    * 从patch更新自身,生成到newApkPath
    *
    * @param context
    * @param patchPath
    * @return 成功返回true
    */
    public static boolean patch(Context context, String patchPath, String newApkPath) {
    return 0 == patch(context.getPackageResourcePath(), newApkPath, patchPath) && new File(newApkPath).exists();
    }
    }

    在layout上放一个按钮,点击按钮会调用patch过程。从/sdcard/update.patch路径读取patch包,生成new.apk,然后调用安装。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.administrator.patchdemo.MainActivity">
    <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:text="Button" />
    </RelativeLayout>

    编译生成app-debug-old.apk,然后修改 android:text=”Button” 为 android:text=”NEW Button”, 再次编译生成 app-debug-new.apk。

    使用 bsdiff 生成patch包,

    1
    ➜ bsdiff app-debug-old.apk app-debug-new.apk update.patch

    将update.patch放到/sdcard路径,安装app-debug-old.apk,运行APP,点击button按钮。若一切正常,会弹出APP安装界面,安装后运行APP发现Button按钮的标题已经从Button修改成NEW Button了。

  4. 这样就结束了吗?Naive!

    你可以尝试不放入update.patch文件,会发现点击button的时候应用直接没了。因为bspatch工具是个命令行工具,它对文件未找到的异常的处理就是直接exit()把进程终结。 更新个增量包,自己先退出了,这样搞肯定是不行的,所以还需要把SmartAppUpdates项目的c代码中bspatch乱结束进程的代码给改了。至于怎么改代码就见仁见智了。

总结

增量更新这个东西原理很简单,效果也很不错,节省流量也十分方便,而且部署到项目内也不困难,只是需要用户手动点击安装(非Root),做好足够的测试就可以在项目中运用它了。

Android热修复框架: AndFix在项目内部署

介绍

AndFix Github: AndFix(当前版本0.5.0)是一个通过Native层替换Java类函数指针,达到修改程序流程,实现热修复的框架。
App使用该框架可以达到无声修复bug代码内bug的效果,用户不需要重新安装app。
AndFix经测试可以在开启MultiDex的项目内使用,但仅能新增/修改Java内的方法,新增类/修改类属性不会产生修改,可以说局限性还是比较大。在项目中应用AndFix需要谨慎使用,多测试,因为一不小心App就可能会Crash。

目标: 将AndFix框架部署到Android项目中。本文会带你处理遇到的坑。

本文使用Android Studio,非Android Studio项目请自行解决。

  1. 在build.gradle内添加

    1
    2
    3
    dependencies{
    compile 'com.alipay.euler:andfix:0.5.0@aar'
    }
  2. 这时候会遇到一个问题:如果你运行了APP,可能发现代码执行System.loadLibrary()时,会报so库找不到(如果你的项目中没有包含所有平台的so库),异常名字叫 java.lang.UnsatisfiedLinkError。此时需要在build.gradle 内添加ndk声明,指定你的项目只支持哪些平台的so库。

    1
    2
    3
    4
    5
    6
    defaultConfig {
    ndk {
    // 设置支持的 SO 库构架
    abiFilters 'armeabi', 'armeabi-v7a'//, 'arm64-v8a', 'x86', 'x86_64', 'mips', 'mips64'
    }
    ......

    现在Gradle Sync一下,完成后就成功引入了andFix。

  3. 添加一个简单的热修复工具类作为尝试。

    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
    final public class PatchHelper {
    private static PatchManager sPatchManager;
    private PatchHelper() {
    }
    public static void init(Context applicationContext, String appVersion) {
    if (null != sPatchManager) {
    throw new IllegalStateException("already inited");
    }
    sPatchManager = new PatchManager(applicationContext);
    sPatchManager.init(appVersion);
    sPatchManager.loadPatch();
    }
    public static boolean addPatch(String patchPath) {
    if (null == sPatchManager) {
    throw new IllegalStateException("need init");
    }
    try {
    sPatchManager.addPatch(patchPath);
    return true;
    } catch (IOException e) {
    e.printStackTrace();
    }
    return false;
    }
    }

    你需要在Application初始化的时候,也就是onCreate()里,执行一下PatchHelper.init(getApplicationContext());
    需要动态修复的时候,将apatch文件下载到手机存储内,执行PatchHelper.addPatch(apatch文件路径)即可完成修复。

    本文不会讲解如何使用apkpatch工具生成apatch文件并传给app的过程。请自行解决。

    自己在一个Android项目中做了一些热修复实验,在实验过程中发现一些问题:

    1) 无法修改Activity中的方法,会报出错误。说是调用父类方法出java.lang.NoSuchMethodException了。
    2) 无法修改带有System.loadLibrary()调用的方法,会抛出java.lang.UnsatisfiedLinkError。较莫名其妙。

    最后自己尝试修改了一个简单类的方法行为(里面基本没啥东西,也没有继承自某个父类),成功了。

    个人感觉目前这个热修复框架局限性还是比较大的,感觉也不太成熟,较容易崩溃。投放到生产环境前需要进行大量的测试覆盖(尤其是新的patch要在测试机上过一遍),且仅仅建议作为紧急用途使用。

  4. 上面编写的工具类对PatchManager 进行了封装。浏览一下源码很容易发现它也仅仅是为com.alipay.euler.andfix.AndFixManager类封装操作一个工具类。

    这个PatchManager 类实际上来说,写的并不太好。一个很简单的多进程APP场景就令PatchManager显得笨拙:它会在addPatch时将你的apatch文件拷贝到app的fileDir内。若多个进程都要更新这个apatch,addPatch方法将会拷贝多次该文件。
    一个简单的解决方案就是自己重新实现一个PatchManager。移除掉拷贝文件到fileDir的画蛇添足操作,将apatch直接下载到fileDir。为它添加单例/多进程更新广播/网络加载等实用功能。具体不细讲。
    另外,AndFix热修复可能直接导致用户的APP Crash掉,并且有可能是每次开启都会Crash,这就很尴尬了。除部署严格测试和发布灰度包以外,还可以考虑在Thread.setDefaultUncaughtExceptionHandler()中处理异常,根据抛出异常的情况自己清空本次更新(或者提示用户手动清除也行)。挽回一点用户体验。

总结

AndFix部署到项目的过程虽有一些坑,总体较为简单。AndFix能火线救急,但是局限性也较大,杀伤力也很大(本来不崩溃的应用可以用AndFix强行弄崩溃了)。在不同Android适配上据说也有不小的坑(未验证过)。两个字:慎用

Android热修复框架: AndFix的初步接触

介绍

AndFix Github: AndFix(当前版本0.5.0)是阿里推出的Android热修复方案。
市面上方案较多,选择它是因为它的效果比目前市面上其他的修复方案要好(修复方案比较不是本文重点,本文仅做实践记录)。
AndFix方案只能对代码进行热修复实现修复代码bug,而无法热更新资源。

目标:在AndFix提供的Demo中使用AndFix,试验热修复Demo中的方法。

Demo配置

  1. 从Github下载AndFix源码。

  2. 打开Android Studio 2.3,File->New->Import Project导入AndFix项目。

  3. File->New->Import Project导入AndFixDemo项目。(在AndFix项目中sample文件夹中)。

  4. AndFixDemo导入完成后,项目应该会报错。因为AndFixDemo中还没有添加AndFix库。

  5. 切换到AndFix项目,打开命令行,输入gradle assembleRelease编译AndFix。编译完成后在项目的build/output/aar目录找到andfix-release.aar。

  6. 将andfix-release.aar拷贝到AndFixDemo项目内的app/libs文件夹(若没有该文件夹,请新建)。

  7. 打开AndFixDemo项目的app/build.gradle文件,添加以下代码,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    repositories {
    flatDir {
    dirs 'libs'
    }
    }
    dependencies {
    compile(name: 'andfix-release', ext: 'aar')
    }

然后Sync一下项目。

这时候报了个错:

1
2
3
Error:Execution failed for task ':app:processDebugManifest'.
> Manifest merger failed : uses-sdk:minSdkVersion 8 cannot be smaller than version 9 declared in library [:andfix-release:] C:\Users\anonymous\.android\build-cache\5887af3b2772a1bf81b83033920436ff6841c8de\output\AndroidManifest.xml
Suggestion: use tools:overrideLibrary="com.alipay.euler.andfix" to force usage

把app/build.gradle中 android { defaultConfig { minSdkVersion从8 改为9,重新Sync。 错误消失,项目Demo配置完成。

上面说的是本地编译&添加方法,也可以通过在build.gradle中加入以下代码引入andfix库:

1
2
3
dependencies {
compile 'com.alipay.euler:andfix:0.5.0@aar'
}

热修复使用

打开Android设备,运行Android Studio编译AndFixDemo安装App并运行。

App运行后,会执行

1
2
3
Log.e(TAG, A.a("good"));
Log.e(TAG, "" + new A().b("s1", "s2"));
Log.e(TAG, "" + new A().getI());

相关类:

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
public class O {
public String s = "s";
public O(String s) {
this.s = s;
}
@Override
public String toString() {
return s;
}
}
.......
.......
public class A {
static int i = 10;
private static O o = new O("a");
String s = "s";
public static String a(String str) {
Log.i("euler", "fix error");
return "a";
}
public int b(String s1, String s2) {
Log.i("euler", "fix error");
Log.i("euler", o.s);
return 0;
}
public int getI() {
return i;
}
}

可以看到log输出为:

1
2
3
4
5
6
7
8
03-25 08:07:39.193 20868-20868/com.euler.andfix D/euler: inited.
03-25 08:07:39.194 20868-20868/com.euler.andfix D/euler: apatch loaded.
03-25 08:07:39.206 20868-20868/com.euler.andfix I/euler: fix error
03-25 08:07:39.206 20868-20868/com.euler.andfix E/euler: a
03-25 08:07:39.206 20868-20868/com.euler.andfix I/euler: fix error
03-25 08:07:39.206 20868-20868/com.euler.andfix I/euler: a
03-25 08:07:39.206 20868-20868/com.euler.andfix E/euler: 0
03-25 08:07:39.206 20868-20868/com.euler.andfix E/euler: 10

现在开始!!!
目标:尝试修改类A的行为。

1. 在AndFix项目根目录找到tools文件夹,解压里面的apkpatch-1.0.3.zip,得到文件夹apkpatch-1.0.3,执行一下apkpatch.bat。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
X:\apkpatch-1.0.3>apkpatch.bat
ApkPatch v1.0.3 - a tool for build/merge Android Patch file (.apatch).
Copyright 2015 supern lee <sanping.li@alipay.com>
usage: apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
-a,--alias <alias> alias.
-e,--epassword <***> entry password.
-f,--from <loc> new Apk file path.
-k,--keystore <loc> keystore path.
-n,--name <name> patch name.
-o,--out <dir> output dir.
-p,--kpassword <***> keystore password.
-t,--to <loc> old Apk file path.
usage: apkpatch -m <apatch_path...> -k <keystore> -p <***> -a <alias> -e <***>
-a,--alias <alias> alias.
-e,--epassword <***> entry password.
-k,--keystore <loc> keystore path.
-m,--merge <loc...> path of .apatch files.
-n,--name <name> patch name.
-o,--out <dir> output dir.
-p,--kpassword <***> keystore password.
X:\apkpatch-1.0.3>

Demo的MainApplication模块实现了载入patch并应用的功能:

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
public class MainApplication extends Application {
private static final String TAG = "euler";
private static final String APATCH_PATH = "/out.apatch";
/**
* patch manager
*/
private PatchManager mPatchManager;
@Override
public void onCreate() {
super.onCreate();
// initialize
mPatchManager = new PatchManager(this);
mPatchManager.init("1.0");
Log.d(TAG, "inited.");
// load patch
mPatchManager.loadPatch();
Log.d(TAG, "apatch loaded.");
// add patch at runtime
try {
// .apatch file path
String patchFileString = Environment.getExternalStorageDirectory()
.getAbsolutePath() + APATCH_PATH;
mPatchManager.addPatch(patchFileString);
Log.d(TAG, "apatch:" + patchFileString + " added.");
} catch (IOException e) {
Log.e(TAG, "", e);
}
}
}

以上代码是AndFix的使用方法!!!

apkpatch工具可以为你生成一个patch文件,按照代码中描述,将apkpatch生成的patch文件放到/sdcard/out.apatch目录就可以了。
App启动后会将patch载入,并更新app。

2. 生成keystore,并为apk签名。

1) 切到AndFixDemo项目,在Android Studio 中点击Build->Generated Signed Apk, 生成一个keystore命名为key.keystore保存在AndFixDemo项目根目录。
我这里密码是123456,alias是key,信息全部是乱写的。

2) 使用Build->Generated Signed Apk编译出一个apk文件,重命名为old.apk。就放在app目录。

3) 改类A为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class A {
static int i = 20; //从10修改到20
private static O o = new O("b"); //从"s" 修改为 "b"
String s = "s";
public static String a(String str) {
Log.i("euler", "fix success"); //从 "error" 修改为 "success"
return str; //从"a" 改为 str
}
public int b(String s1, String s2) {
Log.i("euler", "fix success"); //从 "error" 修改为 "success"
Log.i("euler", o.s);
return 0;
}
public int getI() {
return i;
}
}

4) 再使用Build->Generated Signed Apk编译出一个apk文件,重命名为new.apk。就放在app目录。

5) 把

1
2
@set /p APK_PATCH=请输入apkpatch.bat路径名:
%APK_PATCH% -f app/new.apk -t app/old.apk -o out.apatch -k key.keystore -p 123456 -a key -e 123456

存到文件patch.bat,放在AndFixDemo项目根目录。

6) 执行patch.bat

1
2
3
4
5
6
7
8
X:\project\AndFixDemo>patch
请输入apkpatch.bat路径名:X:\apkpatch-1.0.3\apkpatch.bat
X:\project\AndFixDemo>X:\apkpatch-1.0.3\apkpatch.bat -f app/new.apk -t app/old.apk -o out.apatch -k key.keystore -p 123456 -a key -e 123456
add modified Method:Ljava/lang/String; a(Ljava/lang/String;) in Class:Lcom/euler/test/A;
add modified Method:I b(Ljava/lang/String;Ljava/lang/String;) in Class:Lcom/euler/test/A;
X:\project\AndFixDemo>

工具告诉我们已经做好两个方法的patch了。
这里apkpatch为我们生成了一个文件夹,里面放了几个文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
X:\project\AndFixDemo>dir out.apatch
驱动器 X 中的卷是 SSD
卷的序列号是 A64D-1BE1
X:\project\AndFixDemo\out.apatch 的目录
2017/03/25 19:54 <DIR> .
2017/03/25 19:54 <DIR> ..
2017/03/25 19:54 1,316 diff.dex
2017/03/25 19:54 2,846 new-28ef61a629487761bedf0f2f0c4ac17f.apatch
2017/03/25 19:54 <DIR> smali
2 个文件 4,162 字节
3 个目录 66,200,182,784 可用字节
X:\project\AndFixDemo>

其中new-28ef61a629487761bedf0f2f0c4ac17f.apatch这个文件就是起作用的。

我们先安装old.apk,然后运行。 日志如下:

1
2
3
4
5
6
7
8
03-25 08:07:39.193 20868-20868/com.euler.andfix D/euler: inited.
03-25 08:07:39.194 20868-20868/com.euler.andfix D/euler: apatch loaded.
03-25 08:07:39.206 20868-20868/com.euler.andfix I/euler: fix error
03-25 08:07:39.206 20868-20868/com.euler.andfix E/euler: a
03-25 08:07:39.206 20868-20868/com.euler.andfix I/euler: fix error
03-25 08:07:39.206 20868-20868/com.euler.andfix I/euler: a
03-25 08:07:39.206 20868-20868/com.euler.andfix E/euler: 0
03-25 08:07:39.206 20868-20868/com.euler.andfix E/euler: 10

他现在是这么个提示。

我们把new-28ef61a629487761bedf0f2f0c4ac17f.apatch上传到手机的/sdcard/out.apatch路径里

1
2
3
4
X:\project\AndFixDemo>adb push out.apatch\new-28ef61a629487761bedf0f2f0c4ac17f.apatch /sdcard/out.apatch
[100%] /sdcard/out.apatch
X:\project\AndFixDemo>

然后再开app,发现log变了

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
03-25 08:14:54.119 27167-27167/com.euler.andfix D/euler: inited.
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: modify com.euler.test.A.s flag:
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: setFieldFlag_5_1: 1
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: modify com.euler.test.A.i flag:
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: setFieldFlag_5_1: 9
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: modify com.euler.test.A.o flag:
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: setFieldFlag_5_1: 9
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: replace_5_1: -1354706580 , -1354706580
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: modify com.euler.test.A_CF.s flag:
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: setFieldFlag_5_1: 1
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: modify com.euler.test.A_CF.i flag:
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: setFieldFlag_5_1: 9
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: modify com.euler.test.A_CF.o flag:
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: setFieldFlag_5_1: 9
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: replace_5_1: 1927159936 , 1927159936
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: modify com.euler.test.A_CF.s flag:
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: setFieldFlag_5_1: 1
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: modify com.euler.test.A_CF.i flag:
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: setFieldFlag_5_1: 9
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: modify com.euler.test.A_CF.o flag:
03-25 08:14:54.128 27167-27167/com.euler.andfix D/AndFix: setFieldFlag_5_1: 9
03-25 08:14:54.128 27167-27167/com.euler.andfix D/euler: apatch loaded.
03-25 08:14:54.139 27167-27167/com.euler.andfix I/euler: fix success
03-25 08:14:54.139 27167-27167/com.euler.andfix E/euler: good
03-25 08:14:54.139 27167-27167/com.euler.andfix I/euler: fix success
03-25 08:14:54.139 27167-27167/com.euler.andfix I/euler: a
03-25 08:14:54.139 27167-27167/com.euler.andfix E/euler: 0
03-25 08:14:54.139 27167-27167/com.euler.andfix E/euler: 10

可以看到里面的两个方法调用的确修改成功了,但是改动的两个静态变量的影响没有到andFix里面。

通过观察日志可以做出猜测:里面有两个replace,应该代表修改了两个方法名字。

用命令把/sdcard/out.apatch给删掉,发现log依旧是修复过的。框架应该已经把修改应用到app里了。

总结

AndFix项目提供了一个Demo,Demo已经实现了patch的代码流程。
本文的实验修改了Demo中的类,并用工具通过比较新老app生成更新patch。
可以看到AndFix框架读取到更新patch后成功的修复了类内的一个类方法和对象方法,而对类变量修改却无效。
若在项目内部署了这样一个热更新框架,的确能够实现远程无声修bug。

若想应用AndFix到生产环境,还需要

1) 大概了解内部原理机制
2) 编写测试代码测试它的修改生效范围
3) MultiDex对它的影响
4) 代码混淆

不过这篇文章的目标已经达成了。

Java语言的新特性

官方文档上学习了一下从Java5到Java8各个版本的语言新特性, 在此总结.

Java8

*lambda表达式

可以把一个函数式接口写作lambda表达式

1
2
3
4
5
6
7
8
9
10
//常规写法
new Thread(new Runnable() {
@Override
public void run() {
//do sth
}
}).start();
//lambda表达式写法
new Thread(()->{/*do sth*/}).start();

lambda表达式也支持以下特性

  • 方法引用: 已有名称的方法可以用紧凑的,容易阅读的lambda表达式来表现

    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
    public class Person {
    ......
    public static int compareByAge(Person a, Person b) {
    return a.birthday.compareTo(b.birthday);
    }}
    }
    Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);
    //常规写法
    class PersonAgeComparator implements Comparator<Person> {
    public int compare(Person a, Person b) {
    return a.getBirthday().compareTo(b.getBirthday());
    }
    }
    Arrays.sort(rosterAsArray, new PersonAgeComparator());
    //lambda表达式写法
    Arrays.sort(rosterAsArray,
    (Person a, Person b) -> {
    return a.getBirthday().compareTo(b.getBirthday());
    }
    );
    //lambda表达式写法
    Arrays.sort(rosterAsArray,
    (a, b) -> Person.compareByAge(a, b)
    );
    //方法引用写法
    Arrays.sort(rosterAsArray, Person::compareByAge);
  • 默认方法: 可以在interface内添加使用default关键字修饰的具有实现的方法, 它和旧版本的interface是兼容的;
    并且可以在interface中定义静态(static)方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    interface I {
    void a();
    //静态方法
    static void b() {
    //do sth
    }
    //默认方法
    default void c() {
    //do sth
    }
    }
  • 新api的增强: 加入一些支持lambda表达式的包如java.util.function, java.util.stream.
    另外在java.util.Collection接口中增加了一个default方法

    1
    default Stream<E> stream();

为集合提供流操作

*改进的类型推断

现在类型推断可以通过目标类型推断出泛型方法调用的类型参数. Java7可以对赋值语句使用类型推断, 而在Java8中会对更多的上下文中的目标类型进行类型推断.

1
2
3
List<String> stringList = new ArrayList<>(); //Java7特性: 对泛型实例创建进行类型推断
stringList.add("A");
stringList.addAll(Arrays.asList()); //Java8 特性: 可以从方法参数的类型参数来进行类型推断

*类型注解

现在可以在任何使用类型的地方应用注解

1
2
3
4
5
6
7
8
9
10
11
@NonNull String str;
//实例创建
new @Interned MyObject();
//类型转换
myString = (@NonNull String) str;
//接口实现
class UnmodifiableList<T> implements
@Readonly List<@Readonly T> { ... }
//异常声明
void monitorTemperature() throws
@Critical TemperatureException { ... }

*重复注解

可以在一个类型定义上重复的使用多次注解(加入了@Repeatable注解类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public @interface Schedules {
Schedule[] value();
}
....
@Repeatable(Schedules.class)
public @interface Schedule {
String dayOfMonth() default "first";
String dayOfWeek() default "Mon";
int hour() default 12;
}
...
@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... }

*方法参数反射

现在可以使用java.lang.reflect.Executable.getParameters得到构造器,和方法的参数表
(Executable是构造器和方法的父类)

1
2
3
4
5
Method method = ...;
method.getParameters();
Constructor constructor = ...;
constructor.getParameters();

Java7

*二进制字面常量

可以使用二进制字面常量来表达整形类型(byte, short, int, long), 以0b(0B)作为前缀

1
2
System.out.println(0b11); // 3
System.out.println(0B111); // 7

*字面常量数字的下划线

在数字常量中可以使用下划线来提高数字的可读性, 不能将下划线添加在数字头尾部

1
2
3
4
System.out.println(11_1); // 111
System.out.println(011_1); // 73
System.out.println(0x11_1); // 273
System.out.println(0B11_1); // 7

*String类可以用于switch语句

这里case语句中的statement只能使用常量字符串

1
2
3
4
5
6
7
8
9
10
11
String str = new String("aaa");
switch (str) {
case "aaa":
System.out.println(true);
break;
case "bbb":
System.out.println(false);
break;
default:
break;
}

*泛型实例创建的类型推断

只要编译器可以根据上下文推断出泛型实例的类型参数, 在调用泛型类型的构造器时可以用<>(学名叫做菱形”diamond”)来替代指定类型参数.

1
List<Integer> integerList = new ArrayList<>();

*优化变长参数的方法调用

在变长参数中使用不可具体化的类型(例如List)时, 编译器会报出警告, 该警告可以被

1
@SafeVarargs


1
@SuppressWarnings({"unchecked", "varargs"})

抑制.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Main.java
...
public static void main(String[] args) {
List<String> a = new ArrayList<String>();
fun(a, a);
}
public static <T> T fun(T... args) {
for (T t : args) {
System.out.println(t);
}
return args[0];
}
...

使用java6 编译Main.java 加上 -Xlint:unchecked参数

1
2
3
4
5
jdk1.6.0_45\bin\javac.exe -Xlint:unchecked Main.java
Main.java:38: 警告:[unchecked] 对于 varargs 参数,类型 java.util.List<java.lang.String>[] 的泛型数组创建未经检查
fun(a, a);
^
1 警告

使用java7 编译Main.java 加上 -Xlint:unchecked参数

1
2
3
4
5
6
7
8
9
10
jdk1.7.0_80\bin\javac.exe -Xlint:unchecked Main.java
Main.java:32: 警告: [unchecked] 对于类型为List<String>[]的 varargs 参数, 泛型数组创建未经过检查
fun(a, a);
^
Main.java:35: 警告: [unchecked] 参数化 vararg 类型T的堆可能已受污染
public static <T> T fun(T... args) {
^
其中, T是类型变量:
T扩展已在方法 <T>fun(T...)中声明的Object
2 个警告

添加 @SafeVarargs

1
2
3
4
5
6
7
@SafeVarargs
public static <T> T fun(T... args) {
for (T t : args) {
System.out.println(t);
}
return args[0];
}

编译器警告消失

添加 @SuppressWarnings({“unchecked”, “varargs”}),

1
2
3
4
5
jdk1.7.0_80\bin\javac.exe -Xlint:unchecked Main.java
Main.java:32: 警告: [unchecked] 对于类型为List<String>[]的 varargs 参数, 泛型数组创建未经过检查
fun(a, a);
^
1 个警告

*try_with_resource

使用try_with_resource语句, 可以对实现了AutoCloseable(java7 加入的接口, 原有的Closeable成为它的子类) 的资源进行自动关闭, 而不用手动在finally语句块中关闭.

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
//常规写法
static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
if (br != null) br.close();
}
}
//try_with_resource
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
//try_with_resource
public static void writeToFileZipFileContents(String zipFileName, String outputFileName)
throws java.io.IOException {
java.nio.charset.Charset charset = java.nio.charset.StandardCharsets.US_ASCII;
java.nio.file.Path outputFilePath = java.nio.file.Paths.get(outputFileName);
// Open zip file and create output file with try-with-resources statement
try (
java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFileName);
java.io.BufferedWriter writer = java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
) {
// Enumerate each entry
for (java.util.Enumeration entries = zf.entries(); entries.hasMoreElements(); ) {
// Get the entry name and write it to the output file
String newLine = System.getProperty("line.separator");
String zipEntryName = ((java.util.zip.ZipEntry) entries.nextElement()).getName() + newLine;
writer.write(zipEntryName, 0, zipEntryName.length());
}
}
}

这里需要注意的是:

  • 若try语句块抛出异常, 则在try_with_resource关闭资源调用AutoCloseable.close()时抛出的IOException会被抑制, catch到的异常将是try语句块抛出的异常. 这里可以调用Throwable.getSuppressed()获取被抑制的异常.
  • 在使用finally语句块中关闭资源时, 若try语句块抛出了异常, 则在finally语句块中抛出的IOException异常会覆盖掉try语句块中抛出的异常.
  • Closeable 是java5加入的关闭资源的方法, 多次调用Closeable.close()关闭资源不会引发错误
  • AutoCloseable 是java7加入的关闭资源的方法, 仅允许调用一次AutoCloseable.close();

* 多重异常捕获&优化对重抛出异常的类型检查

多重异常捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//之前的写法
catch (IOException ex) {
logger.log(ex);
throw ex;
} catch (SQLException ex) {
logger.log(ex);
throw ex;
}
//现在可以这样写
catch (IOException|SQLException ex) {
logger.log(ex);
throw ex;
}

重抛出更精确描述的异常, 在java7之前的版本不允许这样写

1
2
3
4
5
6
7
8
9
10
public void rethrowException(String exceptionName)
throws IOException, NullPointerException {
try {
}
catch (Exception e) {
//在这里重抛出类型只能为IOException, NullPointerException,
//若有其他类型, 必须在throw子句中声明
throw e;
}
}

Java6

java6 在语言方面没有改动, 主要改动在新增了一些包之类的.

Java5

*泛型

这种期待已久的对类型系统的增强允许类型或方法对各种类型的对象进行操作,同时提供编译时类型的安全性。它将编译器类型安全添加到集合框架中, 消除了类型转换的苦差事.
Java中的泛型和C++的模板看上去很相似, 但这相似性是肤浅的: Java的泛型的实现机制是类型擦除, 并不支持泛型对象实例化以及模板元编程.

Java5之前的集合操作写法

1
2
3
4
5
6
// Removes 4-letter words from c. Elements must be strings
static void expurgate(Collection c) {
for (Iterator i = c.iterator(); i.hasNext(); )
if (((String) i.next()).length() == 4)
i.remove();
}

使用泛型的集合操作

1
2
3
4
5
6
// Removes the 4-letter words from c
static void expurgate(Collection<String> c) {
for (Iterator<String> i = c.iterator(); i.hasNext(); )
if (i.next().length() == 4)
i.remove();
}

更多的泛型方面资料需参考官方文档和书籍学习.

*For-each

这个循环的新特性能让你更方便的迭代遍历数组和集合
Java5之前的写法

1
2
3
4
5
6
7
8
9
void cancelAll(Collection<TimerTask> c) {
for (Iterator<TimerTask> i = c.iterator(); i.hasNext(); )
i.next().cancel();
}
for (Iterator i = suits.iterator(); i.hasNext(); ) {
Suit suit = (Suit) i.next();
for (Iterator j = ranks.iterator(); j.hasNext(); )
sortedDeck.add(new Card(suit, j.next()));
}

ForEach写法

1
2
3
4
5
6
7
void cancelAll(Collection<TimerTask> c) {
for (TimerTask t : c)
t.cancel();
}
for (Suit suit : suits)
for (Rank rank : ranks)
sortedDeck.add(new Card(suit, rank));

数组的ForEach操作

1
2
3
4
5
6
int sum(int[] a) {
int result = 0;
for (int i : a)
result += i;
return result;
}

*自动装箱/拆箱

Java5会在需要的时候自动在基本类型和相应的包装类型之间进行转换

1
2
3
4
5
6
7
8
9
10
11
List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2)
li.add(i);
public static int sumEven(List<Integer> li) {
int sum = 0;
for (Integer i: li)
if (i % 2 == 0)
sum += i;
return sum;
}

其实是

1
2
3
4
5
6
7
8
9
10
11
List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2)
li.add(Integer.valueOf(i));
public static int sumEven(List<Integer> li) {
int sum = 0;
for (Integer i : li)
if (i.intValue() % 2 == 0)
sum += i.intValue();
return sum;
}

这个转换过程被编译器自动完成了

*类型安全枚举

在java的以往版本中实现枚举模式通常使用整形常数, 这会带来很多问题:

  • 类型不安全: 它可以当做数字使用
  • 无名字空间: 必须在名字上添加前缀以同其他枚举区分
  • 较脆弱: 如果改变了枚举的顺序或者增加新的枚举, 就需要重新编译所有代码以保证不会出现未定义行为
  • 不直观: 输出枚举值的Log打印出来的仅仅只是int, 不能直观的表达任何信息

新增的关键字enum可以定义一个无法被实例化和继承(final)的类, 在这个类里可以定义一系列作为枚举值的常量供用户使用. 类型安全枚举的好处:

  • 枚举常量是枚举类的实例, 他是类型安全且不会和其他枚举类型发生冲突的.
  • 增加了新的枚举常量也不会影响到原有代码的正常工作
  • 枚举常量使用toString()可以输出直接常量字面值, 更加直观.
  • 用户可以在枚举类中定义一些枚举变量可以使用的方法, 让程序逻辑更加清晰.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum Operation {
PLUS { double eval(double x, double y) { return x + y; } },
MINUS { double eval(double x, double y) { return x - y; } },
TIMES { double eval(double x, double y) { return x * y; } },
DIVIDE { double eval(double x, double y) { return x / y; } };
// Do arithmetic op represented by this constant
abstract double eval(double x, double y);
}
public static void main(String args[]) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n", x, op, y, op.eval(x, y));
}
1
2
3
4
5
6
$ java Operation 4 2
4.000000 PLUS 2.000000 = 6.000000
4.000000 MINUS 2.000000 = 2.000000
4.000000 TIMES 2.000000 = 8.000000
4.000000 DIVIDE 2.000000 = 2.000000

在Effective Java的第21条提到用类代替enum结构, 其内容讲的就是实现一种”类型安全枚举”的模型, 用这种更安全的枚举模型来替代原有的类C语言的枚举.而Java5直接在语言层面实现了这种类型安全的枚举模型.在没有极其苛刻的性能要求的情况下(如果你的JVM不是运行在蜂窝电话或者烤面包机上),类型安全枚举带来的额外开销(装载枚举类以及构造常量对象)不会引起用户注意.

*变长参数

方法调用的最后一个参数允许使用变长参数, 消除了用户手动创建数组打包参数传入方法的痛苦.

Java5以前想要格式化字符串只能这样写

1
2
3
4
5
6
7
8
9
Object[] arguments = {
new Integer(7),
new Date(),
"a disturbance in the Force"
};
String result = MessageFormat.format(
"At {1,time} on {1,date}, there was {2} on planet "
+ "{0,number,integer}.", arguments);

而现在

1
2
3
4
String result = MessageFormat.format(
"At {1,time} on {1,date}, there was {2} on planet "
+ "{0,number,integer}.",
7, new Date(), "a disturbance in the Force");

在Java5里String类基于变长参数的特性提供了新的api, format(). 如果阅读一下源码会发现它直接调用MessageFormat.format()做格式串生成.

1
2
public static String format(String pattern,
Object... arguments);

*静态导入

导入类内所有静态成员到当前名字空间, 不需要添加类前缀即可引用被导入的静态成员. 官方建议谨慎和适当的使用静态导入以免污染名字空间以及破坏程序的可读性和可维护性.

1
2
3
4
double r = Math.cos(Math.PI * theta);
import static java.lang.Math.*;
double r = cos(PI * theta);

*注解

这个新特性可以让程序员在代码中使用声明式编程: 在代码中添加注解, 用工具从注解中生成代码. 这种特性可以极大的减少程序员的工作量. 参考库ButterKnife. 此外程序本身也可以通过反射获取属性和方法的注解信息.
定义注解以及使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public @interface RequestForEnhancement {
int id();
String synopsis();
String engineer() default "[unassigned]";
String date() default "[unimplemented]";
}
@RequestForEnhancement(
id = 2868724,
synopsis = "Enable time-travel",
engineer = "Mr. Peabody",
date = "4/1/3007"
)
public static void travelThroughTime(Date destination) { ... }

更多的注解方面资料需参考官方文档和书籍学习.


感想: Java5之前的Java程序员真辛苦