做 Android 这些年,经常有人拿着一个老工程来问我:用 eclipse 还能不能把一个完整的 android 项目从头跑到尾。我的答案一直是能,但前提是你得把工具链的版本关系捋顺。这次我拿手里一个练手项目"博学谷"来说事,它是一个典型的培训类 App,包含欢迎界面、引导页、主页列表、视频播放几大块,第一篇先啃最前面那两秒——欢迎界面。别看它只有一个 Logo 加一行版本号,从主题配置、延时跳转、动画到资源适配,能踩的坑一样不少,而且它会强制你搞明白 Activity 生命周期、Handler 消息机制、Intent 跳转这几个最基础也最容易夹生的知识点。零基础的朋友可以照着敲一遍把完整流程跑通,已经用惯 android studio 的同学也不妨看看,很多概念是互通的,只是工程目录长得不一样、依赖管理方式不一样。
1. 环境准备:Eclipse 加 ADT 这套老工具链怎么伺候
1.1 先定版本组合,别让环境成为第一道坎
很多人搭环境失败,不是因为不会点按钮,而是一开始就把版本关系搞乱了。Eclipse 做 Android 开发依赖 ADT 插件,ADT 依赖 Android SDK,而 SDK 里的 dx 工具又和 JDK 编译出来的 class 文件版本强相关。这四个东西任意两个对不上,你连 Hello World 都跑不起来。
我实测下来最稳的一套组合是这样的:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.7 或 1.8 | 1.8 要额外把编译级别降到 1.7,原因见第 4 节 |
| Eclipse | Kepler SR2 / Luna / Mars(4.3-4.5) | 太新的 Eclipse 装 ADT 容易报依赖缺失 |
| ADT 插件 | 23.0.6 | 官方发布的最后一个 ADT 版本 |
| Build-tools | 23.0.2 及以上 | 低于 21 的 dx 不认高版本 class 文件 |
| 编译 API | 19 到 23 之间 | 这个项目用 19 就够,兼容性更好 |
| minSdk | 14 | 覆盖绝大多数还在用的老机器 |
这里解释一下为什么强调 Build-tools 的下限。Android 的编译流程是 javac 先把源码编成 class,再由 dx 转成 dex 交给设备执行。dx 是跟着 Build-tools 走的,旧版 dx 识别不了 JDK 8 编出来的 52.0 版本 class 文件,于是直接甩一句 Dx unsupported class file version 52.0 给你。这个问题不是代码写错了,纯粹是工具版本断层,后面第 4 节我会给两种解法。
安装顺序建议是:装 JDK,配好 JAVA_HOME,解压 Eclipse(绿色版不用安装,解压即用),然后在 Eclipse 里离线装 ADT,最后通过 ADT 里的 SDK Manager 下载 SDK 和 Build-tools。顺序反了的话,SDK Manager 找不到 Java 路径,会一直提示找不到 java 命令。
1.2 ADT 离线安装,以及要不要汉化
在线装 ADT 现在基本走不通了,官方更新站点早就下线。正确姿势是先下载 ADT-23.0.6.zip 安装包,然后在 Eclipse 里走 Help > Install New Software > Add > Archive,选中那个 zip,只勾 Developer Tools 这一项。
有个细节特别容易卡人:安装向导第二步会让你勾 "Contact all update sites during install to find required software",一定要取消勾选。否则 Eclipse 会挨个去请求早就失效的更新站点,进度条卡在 Calculating requirements 十几分钟,最后给你一个超时错误。取消掉之后,两三分钟就能装完。
汉化这件事我的建议是别急着做。Eclipse 汉化一般用 Babel 语言包,下载对应版本的压缩包,把里面的 features 和 plugins 目录覆盖到 Eclipse 安装目录,重启生效,装错了想还原还得手动删文件。问题在于,汉化之后菜单项名字跟网上绝大多数教程对不上,比如 Project > Properties 变成"项目 > 属性",新手照着教程反而找不到地方,最后绕的弯更多。先英文跑通整个流程,熟悉之后再考虑汉化,这是我给自己和带过的几个新人都定过的规矩。
1.3 工程目录结构:先认清每个文件夹是干嘛的
建工程的时候选 Android Project from Existing Code 或者新建 Android Application Project,填好包名(我这里用 com.boguxuegu),Eclipse 会自动生成一套目录。很多人做了半年 Android 都没搞清楚 gen 和 bin 是用来干嘛的,出问题就抓瞎。先看结构:
BoguXueGu/ ├── src/ 自己写的 Java 源码,按包分层 │ └── com/boguxuegu/ui/ 放 Activity ├── gen/ aapt 自动生成,R.java 在这里,绝对不能手改 ├── assets/ 原样打包的原始文件,比如本地 html、音频 ├── libs/ 第三方 jar ├── res/ │ ├── drawable-xxhdpi/ 按像素密度分目录的图片 │ ├── layout/ 布局 xml,文件名必须全小写 │ ├── values/ strings、colors、dimens、styles │ └── anim/ 补间动画 xml ├── AndroidManifest.xml 清单文件,声明 Activity 和权限 ├── project.properties 编译目标 API、引用的库工程 └── bin/ 编译产物,apk 就在这里,同样不手改几个关键点值得单独拎出来说。src 和 gen 是两回事,R.java 是编译期由 aapt 根据 res 目录自动生成的,你在代码里写 R.layout.activity_splash,本质是在引用这个自动生成的类;一旦 res 里有非法文件名或者 xml 语法错误,R.java 就生成不完整,编辑器满屏红线。assets 和 res 的区别在于,assets 里的文件不会被 aapt 处理,不会生成资源 id,只能靠 AssetManager 按路径读取,所以放网页、字体、离线数据都用它。res/drawable-xxhdpi 这种带密度后缀的目录,是为了让系统按屏幕密度自动挑图,你只放一张图在 drawable 里,在高密度屏上会被放大导致发虚。
另外提一句工具:如果你习惯看类之间的关系,给 Eclipse 装一个 ObjectAid UML 插件,选中包右键就能生成类图,梳理老项目的继承结构时特别省事。
2. 欢迎界面的设计思路:它到底要解决什么问题
2.1 欢迎界面的三类职责与停留时长的取舍
欢迎界面(Splash Screen)在很多人的理解里就是"放个 Logo 等两秒",其实它承担了三件事。第一是品牌露出,用户打开 App 第一眼看到的就是它,Logo、Slogan、版本号构成了对产品的第一印象。第二是给冷启动争取缓冲时间,App 第一次启动要初始化 Application、加载配置、建立数据库连接,这些活儿放在欢迎页的这两秒里做,主界面就能开得更顺。第三是路由分发,判断这是用户第一次装这个 App 还是第 N 次打开,从而决定去引导页还是去主界面。
停留时长是门手艺。我见过设 500 毫秒的,用户眼睛还没聚焦就跳走了,等于白做;也见过设 5 秒的,用户恨不得卸载。行业里比较舒服的区间是 1500 到 2500 毫秒,这个项目我定 2000 毫秒,配合一个 1600 毫秒的渐显放大动画,观感上刚好。判断依据很简单:动画播完之后留 400 毫秒左右的静置期,让用户看清 Logo 和版本号,然后再跳转,这个节奏最稳。
还有一点很多人忽略:动画时长必须小于等于延时跳转的时长。你要是动画设了 3 秒而延时只有 2 秒,用户会看到 Logo 放大到一半画面突然被切走,非常廉价。这个细节我在早期项目里踩过,后来养成了先定延时、再倒推动画时长的习惯。
2.2 三种跳转模型,选哪种取决于产品形态
欢迎界面的跳转逻辑,常见的有三种模型,我在不同项目里都用过,分别说说适用场景。
第一种是直跳主界面,最简单,延时到了直接 startActivity 到 MainActivity。适合工具类 App,用户目的明确,别耽误时间。
第二种是首启走引导页,用 SharedPreferences 存一个布尔标记,第一次打开跳到引导页(通常是 ViewPager 加几个小圆点),引导页最后有个"立即体验"按钮进主界面;之后每次打开都直接进主界面。这个项目用的就是这种,因为"博学谷"是培训类 App,有三屏功能介绍要讲清楚。
第三种是倒计时广告页,右上角显示"跳过 3",用 CountDownTimer 每秒刷新一次文字,配合一个进度条做视觉反馈。这种一般是带运营需求的商业 App 才有,而且广告图要提前缓存到本地,不能每次都从网络拉——这里我就不展开了,因为一旦涉及网络请求,欢迎页就变得很重,容易出 ANR。
三种模型共同的收尾动作是一模一样的:跳转之后必须调用 finish() 销毁当前 Activity。不调的话,用户在主页按返回键会回到欢迎页,然后再等两秒,体验非常割裂,这也是新手最常犯的错误之一。
2.3 图片资源准备与多分辨率适配
资源这块,先说结论:欢迎页的背景图不要用普通 png,尽量做成点九图(.9.png)。原因在于 Android 设备分辨率跨度太大,从 480x800 到 1440x2560 都有,一张固定尺寸的图放在不同屏幕上,要么被拉伸变形,要么两边留黑边。点九图在图片四周加了 1 像素的黑色标记线,告诉系统哪部分可以拉伸、哪部分是内容区,这样一张图就能适配所有屏幕。
制作点九图用 SDK 自带的 draw9patch 工具,在 SDK 的 tools 目录下,运行 draw9patch.bat 把图拖进去,在边上画黑线就行。左边和上边的黑线定义可拉伸区域,右边和下边的黑线定义内容填充区。画完之后文件名必须带 .9 后缀,比如 splash_bg.9.png,否则 aapt 会报错。
Logo 这种不能变形的图,就不要用点九图了,直接按密度分目录放。我的习惯是只提供一套 xxhdpi 的图(比如 420x420 像素),放在 drawable-xxhdpi 下。听起来偷懒,但实测在同价位国产机上效果可以接受,因为现在主流机器基本都是 xhdpi 以上,系统向下缩放的质量损失比向上放大小得多。图省事可以用一套 @2x 的图配 xxhdpi 目录,但严谨的做法还是 xhdpi 和 xxhdpi 各出一套。
还有一个容易被忽略的细节:Logo 在布局里用 android:src 而不是 android:background。src 会保持图片原始比例,background 则会把图拉伸铺满整个控件区域。看到 Logo 被压扁成椭圆形的,九成是把 src 写成了 background。
3. 从布局到跳转:欢迎界面核心实现
3.1 清单文件与主题:先干掉标题栏和白屏
Android 冷启动时会先用主题里配置的 windowBackground 画一屏,再去加载布局。默认主题的 windowBackground 是纯白或者纯黑,如果你的欢迎页背景是深色的,启动瞬间就会白闪一下,业内叫"白屏一闪"。解决办法很简单:给 SplashActivity 单独配一个主题,把 windowBackground 指向欢迎页的背景图,这样系统第一帧画的就是你的图,视觉上无缝衔接。
先在 res/values/styles.xml 里加一个主题:
<style name="SplashTheme" parent="@android:style/Theme.NoTitleBar.Fullscreen"> <item name="android:windowBackground">@drawable/splash_bg</item> <item name="android:windowNoTitle">true</item> </style>Theme.NoTitleBar.Fullscreen 同时去掉了标题栏和状态栏,全屏显示。有些项目只去标题栏保留状态栏,看你想要的视觉效果,我一般欢迎页做成全屏,进主界面再恢复状态栏。
然后在 AndroidManifest.xml 里给 SplashActivity 挂上这个主题:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.boguxuegu" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="19" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme"> <activity android:name=".ui.SplashActivity" android:theme="@style/SplashTheme" android:screenOrientation="portrait"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".ui.MainActivity" /> </application> </manifest>注意 package 属性必须和 src 里的包名一致,不一致的话 R.java 生成的位置和代码里 import 的路径就对不上,满屏红线。screenOrientation 锁竖屏也是培训类 App 的常规做法,横竖屏切换会重新走一遍生命周期,欢迎页没必要支持。
注意:把大图直接设成 windowBackground 会占用额外的内存,图太大(比如超过 1MB)在低端机上有可能触发 OOM。稳妥的做法是 windowBackground 用一张纯色或者小尺寸的图,真正的欢迎页背景在布局里加载。
3.2 布局文件拆解
布局不复杂,一个 RelativeLayout 居中放 Logo,底部居中放版本号,右上角放跳过按钮。代码长这样:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/splash_bg"> <ImageView android:id="@+id/iv_logo" android:layout_width="140dp" android:layout_height="140dp" android:layout_centerInParent="true" android:src="@drawable/logo_boguxuegu" /> <TextView android:id="@+id/tv_skip" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_alignParentTop="true" android:layout_marginRight="16dp" android:layout_marginTop="16dp" android:background="@drawable/bg_skip" android:paddingBottom="4dp" android:paddingLeft="12dp" android:paddingRight="12dp" android:paddingTop="4dp" android:text="跳过" android:textColor="#FFFFFF" android:textSize="12sp" android:visibility="gone" /> <TextView android:id="@+id/tv_version" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="40dp" android:textColor="#B3FFFFFF" android:textSize="12sp" android:text="版本 v1.0" /> </RelativeLayout>几个写布局的讲究。尺寸单位一定要用 dp 和 sp,dp 用于控件尺寸和间距,sp 用于字号,因为 sp 会跟随用户在系统里设置的字体大小缩放,对视力不佳的用户更友好;你要是用 px,不同屏幕上显示大小完全不同。颜色值写成 #B3FFFFFF 这种八位格式,前两位是透明度,B3 大约是 70% 不透明,用来做版本号这种次要信息刚刚好,纯白会抢 Logo 的视觉重心。
跳过按钮我这里默认设成 visibility="gone",因为普通用户其实并不需要跳过——两秒而已。如果是带广告的版本才需要显示它,而且倒计时结束要自动隐藏。这个小设计很多人不做,导致右上角一直杵着一个"跳过",点不点都尴尬。
3.3 SplashActivity 完整实现与逐行说明
核心代码就一个 Activity,我把完整实现贴出来,再挑关键行讲。
package com.boguxuegu.ui; import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.View; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.ImageView; import android.widget.TextView; import com.boguxuegu.R; import java.lang.ref.WeakReference; public class SplashActivity extends Activity implements View.OnClickListener { private static final long SPLASH_DELAY = 2000L; private static final String SP_NAME = "app_config"; private static final String KEY_FIRST_LAUNCH = "key_first_launch"; private ImageView ivLogo; private TextView tvVersion; private TextView tvSkip; private SplashHandler mHandler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); initView(); initData(); mHandler = new SplashHandler(this); mHandler.sendEmptyMessageDelayed(SplashHandler.MSG_TIME_UP, SPLASH_DELAY); } private void initView() { ivLogo = (ImageView) findViewById(R.id.iv_logo); tvVersion = (TextView) findViewById(R.id.tv_version); tvSkip = (TextView) findViewById(R.id.tv_skip); tvSkip.setOnClickListener(this); } private void initData() { tvVersion.setText("版本 v" + getVersionName()); Animation anim = AnimationUtils.loadAnimation(this, R.anim.splash_anim); ivLogo.startAnimation(anim); } private String getVersionName() { try { PackageInfo info = getPackageManager() .getPackageInfo(getPackageName(), 0); return info.versionName; } catch (PackageManager.NameNotFoundException e) { return "1.0"; } } private boolean isFirstLaunch() { SharedPreferences sp = getSharedPreferences(SP_NAME, MODE_PRIVATE); return sp.getBoolean(KEY_FIRST_LAUNCH, true); } private void markLaunched() { getSharedPreferences(SP_NAME, MODE_PRIVATE) .edit() .putBoolean(KEY_FIRST_LAUNCH, false) .apply(); } private void jump() { if (isFinishing()) { return; } Intent intent; if (isFirstLaunch()) { intent = new Intent(this, GuideActivity.class); markLaunched(); } else { intent = new Intent(this, MainActivity.class); } startActivity(intent); finish(); } @Override public void onClick(View v) { if (v.getId() == R.id.tv_skip) { mHandler.removeCallbacksAndMessages(null); jump(); } } @Override protected void onDestroy() { mHandler.removeCallbacksAndMessages(null); super.onDestroy(); } private static class SplashHandler extends Handler { static final int MSG_TIME_UP = 1; private final WeakReference<SplashActivity> mRef; SplashHandler(SplashActivity activity) { mRef = new WeakReference<SplashActivity>(activity); } @Override public void handleMessage(Message msg) { if (msg.what == MSG_TIME_UP) { SplashActivity activity = mRef.get(); if (activity != null && !activity.isFinishing()) { activity.jump(); } } } } }先说 Handler 为什么写成 static 内部类。如果直接写new Handler()匿名内部类,这个 Handler 会隐式持有 SplashActivity 的引用,而延时消息又会被主线程的 MessageQueue 持有,两者一叠加,即使 Activity 已经 finish,它的内存也回收不掉,这就是典型的内存泄漏。改成静态内部类加 WeakReference,Handler 不再强引用 Activity,泄漏链就断了。这个改法看着多写了十几行,但在实际项目里能省掉很多莫名其妙的卡顿和崩溃。
sendEmptyMessageDelayed 和 postDelayed 的区别值得一提。前者走的是 Message 机制,配合 what 值可以做消息分支,后面如果要加"检测版本更新"这类并行任务,加个 what 值就行;后者传的是 Runnable,多个 Runnable 之间不好区分,清理的时候还得单独把 Runnable 拿出来 removeCallbacks。做欢迎页我习惯用前者。
jump() 里第一行判断 isFinishing(),是为了防止一种偶发情况:用户手快,在延时消息还没到的时候连续按返回键退出了 Activity,消息延迟触发时 Activity 已经处于销毁中,这时候再 startActivity 会抛 WindowManager$BadTokenException。加上这个判断,白屏或者闪退基本就绝迹了。
onDestroy 里那句 removeCallbacksAndMessages(null) 是必须的,把队列里所有属于这个 Handler 的消息清掉。参数传 null 表示清空全部,这里只有一个延时消息,传 null 最省事。
3.4 动画实现:alpha 加 scale 的组合拳
动画文件放在 res/anim/splash_anim.xml:
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="1600" android:fillAfter="true" android:interpolator="@android:anim/decelerate_interpolator"> <alpha android:fromAlpha="0.0" android:toAlpha="1.0" /> <scale android:fromXScale="0.75" android:toXScale="1.0" android:fromYScale="0.75" android:toYScale="1.0" android:pivotX="50%" android:pivotY="50%" /> </set>三个参数值得解释。duration 定 1600 毫秒,前面说过它必须小于 2000 毫秒的跳转延时。fillAfter="true" 表示动画结束后保持最终状态,不设的话动画播完会瞬间弹回初始状态,Logo 会闪一下。interpolator 是插值器,decelerate 表示先快后慢,越接近结束速度越慢,视觉上更柔和;默认的 accelerateDecelerate 是两头慢中间快,放在 Logo 上会显得急。
pivotX 和 pivotY 是缩放的中心点,写 50% 就是以控件自身中心为原点缩放。这两个值不写的话默认是控件左上角,Logo 会往右下角"长"出来,非常奇怪,很多人第一次做动画都被这个坑过。
如果想让版本号也参与动画,可以在布局里给版本号设 android:visibility="invisible",在代码里用 AnimationListener 的 onAnimationEnd 回调把它设成 visible 并启动一个淡入动画。我一般不这么做,版本号保持静止更稳,动效集中在 Logo 上不容易乱。
3.5 版本号与首次启动标记的两个细节
版本号通过 PackageManager 拿,这段代码几乎每个项目都要抄一遍,但有个坑得说清楚:getPackageInfo 的第二个参数叫 flags,传 0 表示只要基本信息,速度快;如果传 PackageManager.GET_ACTIVITIES 之类的,系统会把所有 Activity 信息都读出来打包给你,数据量大很多。只是为了拿版本号,传 0 就够了。另外必须包 try-catch,因为理论上存在包名查不到的情况,不捕获的话这里会直接崩。
SharedPreferences 这块,我用的是独立文件名 "app_config",而不是很多教程里的 MODE_PRIVATE 默认文件名。原因很实际:App 跑起来之后,配置项会越来越多,缓存开关、上次登录账号、主题设置都往里塞,全放一个文件里,排查问题时 adb 导出来一大坨,找半天。按用途拆成 app_config、user_info、cache 几个文件,出问题一眼就能定位。卸载 App 会清空所有 SharedPreferences,所以这个首启标记天生就是"装一次看一次引导页"的效果,卸载重装又会看到引导页,测试的时候很方便。
写入用 apply() 而不是 commit()。apply() 是异步写内存并异步落盘,不阻塞主线程;commit() 是同步写磁盘,会返回布尔值告诉你成功与否,但直接在主线程调用有可能造成几毫秒到几十毫秒的卡顿。欢迎页这种场景没有"必须立刻落盘"的强需求,用 apply() 就好。当然 apply() 的代价是你在写入之后立刻用 adb 去看文件内容,可能还没落盘,这不是 bug。
提示:如果你在代码里还需要让用户"清除缓存",记得清除 SharedPreferences 时不要把首启标记一起清了,否则用户下次打开又进引导页,反馈会很难看。写删除逻辑时用 remove(key) 精确删除,别图省事用 clear()。
4. 踩坑实录:Eclipse 下做 Android 最容易翻车的几个点
4.1 R.java 不生成,满屏红线怎么破
这个问题的出现频率稳居第一。现象是代码里所有 R.layout.xxx、R.id.xxx 全部标红,Eclipse 提示 R cannot be resolved to a variable。十个人里有八个第一反应是 Clean 一下,Clean 能解决一部分,但治标不治本。
我总结的排查顺序是这样的。先看 Console 面板,aapt 报错会打印在这里,比编辑器里的红线信息准确得多。常见的 aapt 报错有三类:一是 res 目录下有非法文件名,Android 的资源文件名只允许小写字母、数字和下划线,出现大写字母、中文、空格、横杠都会直接报错,比如 splash-Bg.png 就是错的;二是布局 xml 语法有问题,比如标签没闭合、属性名拼错、引用了不存在的资源;三是 res/values 目录下的 strings.xml 里有多余的符号或者重复的 name。
还有一个隐蔽原因:AndroidManifest.xml 里的 package 属性和 src 下的包名不一致。比如 manifest 写 com.boguxuegu,src 里是 com.boguxuegu.app,那 R.java 会生成在 gen/com/boguxuegu/R.java,代码里 import 的却是 com.boguxuegu.app.R,自然找不到。改一致就恢复正常。
最后确认 Project > Build Automatically 是勾选状态,没勾选的话改了资源不会重新生成 R.java。实在不行就把 gen 目录整个删掉,Clean 之后再 Build,Eclipse 会重新生成一遍。
4.2 Dx unsupported class file version 52.0 怎么处理
这个报错前面提过,这里给完整的两种解法。
解法一,降编译级别。右键工程 > Properties > Java Compiler,把 Compiler compliance level 从 1.8 改成 1.7,同时把下方的 Use compliance from execution environment 里的 JRE 也选成 1.7。改完 Clean 重编。这个方法的优点是改动小,缺点是如果项目里用了 Java 8 的语法(比如 lambda、接口默认方法),改完就编不过了。老项目基本没这问题。
解法二,升级构建工具。在工程根目录的 project.properties 里加一行sdk.buildtools=23.0.2,同时确保 SDK Manager 里已经装好了对应的 Build-tools。新版 dx 认识 52.0 的 class 文件,编译就过了。
# project.properties target=android-19 sdk.buildtools=23.0.2两种解法我一般选第二种,因为不用动 Java 编译级别,后续加依赖的时候不容易出岔子。但如果你所在的环境没法联网下载新的 Build-tools,解法一就是唯一出路。附带说一句,同一个报错还经常以 Conversion to Dalvik format failed with error 1 的形式出现,本质是一回事,处理方法一样。
4.3 内存泄漏与 Handler 未清理的排查手法
前面代码里已经用静态内部类规避了大部分泄漏,但还是有几种情况要防。第一种是延时消息没清,Activity 已经销毁了消息还在队列里,触发时往一个已经死掉的界面上做操作。第二种是在欢迎页里启动了子线程做耗时任务,Activity 销毁时线程还在跑,做完之后回调更新 UI 就崩。欢迎页最好不要开子线程,如果非要开,在 onDestroy 里打一个标志位,线程任务里每次都检查这个标志位。
真要排查这类问题,Eclipse MAT 是绕不开的工具。它其实是一个独立的分析工具,可以从 Eclipse 的市场里装,也可以单独下载。用法是先用 DDMS 或者 adb 抓一个 hprof 堆快照,然后用 hprof-conv 转换一下格式(SDK 的 platform-tools 里带这个脚本),再用 MAT 打开。它会给你一个 Leak Suspects 报告,直接告诉你哪个对象占了最多内存、是谁在引用它。我第一次用它抓出一个泄漏的 Activity 时,那种"原来如此"的感觉挺难忘的。
日常开发中更实用的其实是 logcat。用adb logcat -s SplashActivity只过滤自己的 TAG,把 onCreate、onDestroy、jump 三处日志打出来,一眼就能看出生命周期是不是符合预期——比如 onDestroy 有没有执行、跳转发生了没有、有没有跳两次。这个习惯养成之后,绝大多数疑难杂症都能靠日志拆解掉。
4.4 常见问题速查表
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 启动瞬间白屏一闪 | 主题 windowBackground 是白色 | 给 SplashActivity 单独配主题,windowBackground 指向背景图 |
| Logo 被拉伸变形 | 用了 background 而不是 src;图未做点九图 | 改回 android:src,背景图做点九图 |
| 欢迎页一闪而过看不清 | 延时设得太短,或动画时长超过延时 | 延时定 2000ms,动画定 1600ms |
| 主页按返回又回到欢迎页 | jump 之后没有 finish() | 跳转后立即调用 finish() |
| 版本号显示为 null | 没做异常兜底 | getPackageInfo 外面套 try-catch,返回默认值 |
| 第二次打开仍进引导页 | 首启标记没写成功或 key 写错 | 检查 SP 文件名和 key,确认调用了 apply() |
| 偶发 WindowManager$BadTokenException | Activity 已 finish 还执行 startActivity | jump 前加 isFinishing() 判断 |
| 图片在部分机型上模糊 | 只放了低密度目录的图 | 至少提供一套 xxhdpi 的图 |
| R.id 找不到 | 布局未保存、文件名非法、包名不一致 | 看 Console 里 aapt 报错行,Clean 后重建 |
这张表里的每一条我都实际遇到过,其中"图片模糊"和"返回又回到欢迎页"这两条在新人代码里出现的概率最高。前者是资源目录没建对,后者就是纯粹忘了 finish(),都是一行代码的事,但不知道原因的话能折腾一下午。
5. 一点延伸:欢迎界面之后怎么接
欢迎界面只是博学谷这个项目的第一块砖,跳转逻辑里那个 GuideActivity 是第二步,用 ViewPager 加小圆点指示器,三屏功能介绍,最后一屏放"立即体验"按钮,点完把首启标记写了再进 MainActivity。ViewPager 这块要处理两个细节,一是 PagerAdapter 的 getCount 和 instantiateItem 必须对应上,否则会崩;二是小圆点的选中态切换要跟着 onPageSelected 走,别只在初始化时设一次。
到了主界面就开始上强度了,底部 Tab 加 Fragment、顶部轮播图加列表。轮播图我早期用 ViewPager 自己拼,加个定时器自动切换,后来发现要处理手指按下暂停、抬起恢复、无限循环这些边界情况,代码越写越长,就直接用第三方库了。这里有个 Eclipse 时代的痛点:现在大多数 Android 库都只发 aar 包,Eclipse 不认,你得手动把 aar 解压,把 classes.jar 拿出来放进 libs 目录,再 Add to Build Path,如果有资源文件还得手动合并到工程里。这个过程非常磨人,也是后来整个团队都转到 android studio 的根本原因——Gradle 一行 implementation 就搞定的事,在 Eclipse 里要折腾半小时。
不过话说回来,把 Eclipse 这套流程完整走一遍,对理解 Android 的构建过程是有好处的。你能亲眼看到 aapt 怎么处理资源、dx 怎么把 class 转成 dex、R.java 是怎么生成的,这些在 android studio 里被 Gradle 藏得严严实实。带新人的时候我经常让他们先在 Eclipse 上跑通一个完整项目,再换到 android studio,理解深度完全不一样。
我个人做欢迎页有几个固定习惯,分享出来供参考。第一,欢迎页里绝对不做网络请求和数据库操作,冷启动阶段做这些很容易 ANR,需要预热的操作放到 Application 的异步初始化里;第二,首启标记单独用一个 SharedPreferences 文件,别混进用户配置里;第三,动画时长比延时短 300 到 500 毫秒,让画面有个"落定"的瞬间;第四,真机调试时开着adb logcat -s SplashActivity看生命周期,比在 Eclipse 里点 LogCat 面板快得多,也不容易卡。这几个习惯看着琐碎,但项目做到后期界面一多,能省下大量排查时间。