旧项目升级爬坑

最近接手了一个骨灰级项目的升级任务,上一次功能代码的提测还是16年年中。由于当时支持的最高版本仅仅是5.0,两年下来Google Play上显示该应用支持的设备是寥寥无几,刚好有客户的产品需要该应用的功能支持,所以当前的可使用状态是不能够满足需求的。那么任务目标就是把应用支持的系统版本提高一些,由于8.0系统的升级有著名的service相关问题,旧代码的功能细节所剩的印象实在有限,所以暂定支持最高版本为7.0。这里记录一下这次重拾旧项目之旅。

升级支持设备的系统版本号不是简简单单改个targetSdkVersion/compileSdkVersion就能解决的事情。随随便便就能想到一箩筐问题:

  • 两年前的项目业务还记得多少?代码实现还能看得懂么?
  • 今天用的编译/调试工具,老工程会很方便的适用起来么?
  • Android版本间的系统特性变化,涉及到最大的就是权限的动态申请问题,这一块一定是要特殊处理的,旧代码是否能便利的植入权限申请代码还是个未知数,此外各种定制ROM本身对这个功能的支持就很坑爹
  • support库与framework的升级,会不会存在兼容性问题,也是个不大不小的问号
  • ……

无论如何,该动手还是得动手。业界有句很中肯的话叫,大意是“如果一个程序员发现自己六个月前的代码还是不错的,那么说明他这段时间没什么进步”。搞开老项目代码之后,百感交集的发现自己好像进步很大的样子——看哪儿都不怎么顺眼,又不敢乱动。时间有限,围绕需求本身开始操作吧。

第一步,统一配置

发现之前的项目gradle配置十分散乱,各个module各有各的版本、support等配置,不利于统一操作。所以第一步就是整理各个module中的gradle配置,把所有共性的版本都整理到工程目录下的gradle文件的ext中去:

1
2
3
4
5
6
7
ext {
minSdkVersion = 15
targetSdkVersion = 22
compileSdkVersion = 22
buildToolsVersion = "22.0.1"
supportLibVersion = "22.2.1"
}

第二步,提高版本号,迎接编译问题

统一之后,修改ext就足够了,根据需要把targetSdk和compileSdk改到25。之后就可以迎接一大波编译问题了。

  • apache库缺失问题

    Unable to find optional library: org.apache.http.legacy

    首先碰到的是阿帕奇库不见了的问题。Android 6.0不再支持 Apache HTTP client, 建议使用 HttpURLConnection 代替。然而并没有时间替换,所以要增加配置依赖:

    android {
    useLibrary 'org.apache.http.legacy'
    }
    
  • 接下来发现工具类中调用系统工具类FloatMath的sqrt找不到了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    Math routines similar to those found in {@link java.lang.Math}.
    *
    - <p>Historically these methods were faster than the equivalent double-based
    - {@link java.lang.Math} methods. On versions of Android with a JIT they
    - became slower and have since been re-implemented to wrap calls to
    - {@link java.lang.Math}. {@link java.lang.Math} should be used in
    - preference.
    *
    - <p>All methods were removed from the public API in version 23.
    *
    - @deprecated Use {@link java.lang.Math} instead.
    */
    @Deprecated
    public class FloatMath {

    该类deprecated ,注释建议使用Math替代。那就按建议的走,问题不大。

  • 考虑到会用到一些新的开源库和support库,原本gradle2.2.2编译的脚本配置,修改成3.0.1,并升级25对应的support库与build tool版本。 其中一些2.x版本gradle中的配置在3.x中已经不适用,需要做对应的修改。

  • 继续编译,报本地attr属性与support包属性buttonGravity冲突的错误,比较神奇

    Error:(203, 5) error: duplicate value for resource ‘attr/buttonGravity’ with config ‘’.

    Error:(203, 5) error: resource previously defined here.
    

    检查发现定义的style与attr并没有用到,可以说是无妄之灾了,干掉之后继续编译。

  • 接下来是RecyclerView的相关方法丢失

    Error:(95, 38) 错误: 找不到符号

    符号:   方法 setSupportsChangeAnimations(boolean)
    位置: 类 ItemAnimator
    

    完全记不得RecyclerView有这个接口,谷歌之,发现一片鹅厂Bugly的博文腾讯Bugly干货分享】RecyclerView 必知必会,有以下描述:

    对于RecyclerView的Item Animator,有一个常见的坑就是”闪屏问题”。这个问题的描述是:当Item视图中有图片和文字,当更新文字并调用 notifyItemChanged()时,文字改变的同时图片会闪一下。这个问题的原因是当调用 notifyItemChanged()时,会调用DefaultItemAnimator的 animateChangeImpl()执行change动画,该动画会使得Item的透明度从0变为1,从而造成闪屏。
    
    
        解决办法很简单,在 rv.setAdapter()之前调用 ((SimpleItemAnimator)rv.getItemAnimator()).setSupportsChangeAnimations(false)禁用change动画。
    

    想来新版本问题已经解决,该方法已经被干掉了,直接去掉处理。

  • 继续,发现开源库兼容问题,ButterKnife 版本冲突

    通过CSDN AndroidStudio3.0 注解报错Annotation processors must be explicitly declared now. 添加gradle配置解决

  • NDK build工具问题,如今只支持C make了,老版本的NDK不受待见了:

    好在c代码并没有更新的可能,退回老代码生成SO文件,直接引入SO库处理。

    至此,终于可以打出一个APK来了。

别急,还有各种crash

  • 首当其冲的居然还是黄油刀——

    E/AndroidRuntime: FATAL EXCEPTION: main

    Process: com.mosaic.app, PID: 18549
    java.lang.NoClassDefFoundError: Failed resolution of: Lbutterknife/ButterKnife;
        at com.mosaic.app.about.GuideActivity.onCreate(GuideActivity.java:92)
        at android.app.Activity.performCreate(Activity.java:7372)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1218)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3147)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3302)
        at android.app.ActivityThread.-wrap12(Unknown Source:0)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1891)
        at android.os.Handler.dispatchMessage(Handler.java:108)
        at android.os.Looper.loop(Looper.java:166)
        at android.app.ActivityThread.main(ActivityThread.java:7425)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:245)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:921)
     Caused by: java.lang.ClassNotFoundException: Didn't find class "butterknife.ButterKnife" on path: DexPathList[[zip file "/system/framework/com.google.android.maps.jar", zip file "/data/app/me.airtake-i_3R4RRH5SDyBLA_g5pipw==/base.apk"],nativeLibraryDirectories=[/data/app/me.airtake-i_3R4RRH5SDyBLA_g5pipw==/lib/arm, /data/app/me.airtake-i_3R4RRH5SDyBLA_g5pipw==/base.apk!/lib/armeabi-v7a, /system/lib, /vendor/lib, /product/lib]]
        at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:93)
    

    网上的说法clean/rebuild/instent run,全都是无效的。无奈之下通过升级到最新版处理,替换失效的butterknife注解,注释掉失效的方法,这一波改动比较大,好在效果还可以。

  • 接下来就是动态权限申请这个系统自带深坑了,即使是不考虑旧项目升级这个情况,正常项目中由于国内各种五花八门定制ROM的存在,该问题也一直是个深坑,加之项目时间节点的限制。斟酌再三还是选择口碑还算不错、兼容性也还可以的AndPermission。随之而来的问题就是引入一个开源库带来的编译依赖等问题的潜在风险。

    AndPermission中使用了lambada表达式,由于项目已经升级有gradle3.0.1,在app module中添加:

    compileOptions {
        targetCompatibility 1.8
        sourceCompatibility 1.8
    }
    

    解决编译问题。其他的就是基于AndPermission对用到的具体需要动态申请的权限做业务实现,这部分新增的代码改动是最大的。

  • 继续过主流程功能,有闪退中的最平凡最常见的品类出现——空指针异常。很奇怪的服务端接口返回时,调用时的context为null,有趣的是调用点是fragment中的setUserVisibleHint方法。该方法在很久以前的网文中被冠以fragment中真正的onResume/onPause的名号。似乎这货在新系统版本中找不见对应activity的context了,最快速的解决方法是:一方面对报空位置防御式判空处理,另一方面分析业务的需要,切换调用点处理。

ok,这下主流程崩溃的问题暂时没有碰到了。

事情并没有完

在测试主流程崩溃的时候,发现了一些让我很崩溃的问题——不少页面发生了一些奇怪的变化。有些阴影效果显示的很夸张,有些布局称满了整个屏幕,最无奈的是,如果你没有看过所有页面,就没办法确定是否所有页面都没有问题。

  • 阴影问题比较奇怪,由于需求上更加优先能用,并且问题位置只有两处,直接做去掉阴影处理。

  • 布局问题一般是由于升级了support库,recyclerView出现了的适配问题,很多item布局都被撑大了。原本布局文件中match_parent需要修改为wrap_content

此外调试过程中,发现在onResume中调用AndPermission会频繁回调onResume。分析源码得知该开源库的原理就是新启动一个Activity实现的,所以onResume中调用它来申请权限并不合适。源代码中需要动态申请权限的功能代码又没有封装的适合动态处理,改动源码会导致时间进度不可控。最终采用了牺牲体验的做法,部分权限统一在进入应用时提示申请权限,不影响原代码。

一些总结

捞一份两年没怎么动过的代码做系统升级适配体验上并不怎么好。虽然提前做了一些可能碰到问题的预估,但实际执行过程中会碰到更多的问题。

各种各样的兼容问题,系统/控件/开源库/编译工具等各种相互依赖纠结在一起的升级导致的兼容问题,可怕之处在于这些东西不完全可知。带来的直接问题就是时间评估失效、风险无法控制。所以第一点就是,这种情况下的项目基本是无法预知所有问题的,时间预算上要照顾好自己。

接下来是问题的解决方案,解决问题的方法可能有很多种。我们需要的是根据项目的最优先要求来选取方法,比如快速上线。那么此时完全可以不纠缠于细节,采用规避或替换的方式快速解决问题,同时要最小规模引入变化,警惕变化是否会引发连锁的新问题。

感谢您赏个荷包蛋~