
做 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 都跑不起来。我实测下来最稳的一套组合是这样的组件推荐版本说明JDK1.7 或 1.81.8 要额外把编译级别降到 1.7原因见第 4 节EclipseKepler SR2 / Luna / Mars4.3-4.5太新的 Eclipse 装 ADT 容易报依赖缺失ADT 插件23.0.6官方发布的最后一个 ADT 版本Build-tools23.0.2 及以上低于 21 的 dx 不认高版本 class 文件编译 API19 到 23 之间这个项目用 19 就够兼容性更好minSdk14覆盖绝大多数还在用的老机器这里解释一下为什么强调 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.boguxueguEclipse 会自动生成一套目录。很多人做了半年 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 nameSplashTheme parentandroid:style/Theme.NoTitleBar.Fullscreen item nameandroid:windowBackgrounddrawable/splash_bg/item item nameandroid:windowNoTitletrue/item /styleTheme.NoTitleBar.Fullscreen 同时去掉了标题栏和状态栏全屏显示。有些项目只去标题栏保留状态栏看你想要的视觉效果我一般欢迎页做成全屏进主界面再恢复状态栏。然后在 AndroidManifest.xml 里给 SplashActivity 挂上这个主题manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.boguxuegu android:versionCode1 android:versionName1.0 uses-sdk android:minSdkVersion14 android:targetSdkVersion19 / application android:allowBackuptrue android:icondrawable/ic_launcher android:labelstring/app_name android:themestyle/AppTheme activity android:name.ui.SplashActivity android:themestyle/SplashTheme android:screenOrientationportrait intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.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 version1.0 encodingutf-8? RelativeLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:backgrounddrawable/splash_bg ImageView android:idid/iv_logo android:layout_width140dp android:layout_height140dp android:layout_centerInParenttrue android:srcdrawable/logo_boguxuegu / TextView android:idid/tv_skip android:layout_widthwrap_content android:layout_heightwrap_content android:layout_alignParentRighttrue android:layout_alignParentToptrue android:layout_marginRight16dp android:layout_marginTop16dp android:backgrounddrawable/bg_skip android:paddingBottom4dp android:paddingLeft12dp android:paddingRight12dp android:paddingTop4dp android:text跳过 android:textColor#FFFFFF android:textSize12sp android:visibilitygone / TextView android:idid/tv_version android:layout_widthwrap_content android:layout_heightwrap_content android:layout_alignParentBottomtrue android:layout_centerHorizontaltrue android:layout_marginBottom40dp android:textColor#B3FFFFFF android:textSize12sp android:text版本 v1.0 / /RelativeLayout几个写布局的讲究。尺寸单位一定要用 dp 和 spdp 用于控件尺寸和间距sp 用于字号因为 sp 会跟随用户在系统里设置的字体大小缩放对视力不佳的用户更友好你要是用 px不同屏幕上显示大小完全不同。颜色值写成 #B3FFFFFF 这种八位格式前两位是透明度B3 大约是 70% 不透明用来做版本号这种次要信息刚刚好纯白会抢 Logo 的视觉重心。跳过按钮我这里默认设成 visibilitygone因为普通用户其实并不需要跳过——两秒而已。如果是带广告的版本才需要显示它而且倒计时结束要自动隐藏。这个小设计很多人不做导致右上角一直杵着一个跳过点不点都尴尬。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 WeakReferenceSplashActivity mRef; SplashHandler(SplashActivity activity) { mRef new WeakReferenceSplashActivity(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它的内存也回收不掉这就是典型的内存泄漏。改成静态内部类加 WeakReferenceHandler 不再强引用 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 version1.0 encodingutf-8? set xmlns:androidhttp://schemas.android.com/apk/res/android android:duration1600 android:fillAftertrue android:interpolatorandroid:anim/decelerate_interpolator alpha android:fromAlpha0.0 android:toAlpha1.0 / scale android:fromXScale0.75 android:toXScale1.0 android:fromYScale0.75 android:toYScale1.0 android:pivotX50% android:pivotY50% / /set表示把多个动画组合在一起播放这里是透明度和缩放同时进行Logo 从完全透明、75% 大小渐变成不透明、原始大小。这种由远及近浮现的效果是欢迎页最经典的处理成本低、观感好。三个参数值得解释。duration 定 1600 毫秒前面说过它必须小于 2000 毫秒的跳转延时。fillAftertrue 表示动画结束后保持最终状态不设的话动画播完会瞬间弹回初始状态Logo 会闪一下。interpolator 是插值器decelerate 表示先快后慢越接近结束速度越慢视觉上更柔和默认的 accelerateDecelerate 是两头慢中间快放在 Logo 上会显得急。pivotX 和 pivotY 是缩放的中心点写 50% 就是以控件自身中心为原点缩放。这两个值不写的话默认是控件左上角Logo 会往右下角长出来非常奇怪很多人第一次做动画都被这个坑过。如果想让版本号也参与动画可以在布局里给版本号设 android:visibilityinvisible在代码里用 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.boguxuegusrc 里是 com.boguxuegu.app那 R.java 会生成在 gen/com/boguxuegu/R.java代码里 import 的却是 com.boguxuegu.app.R自然找不到。改一致就恢复正常。最后确认 Project Build Automatically 是勾选状态没勾选的话改了资源不会重新生成 R.java。实在不行就把 gen 目录整个删掉Clean 之后再 BuildEclipse 会重新生成一遍。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.buildtools23.0.2同时确保 SDK Manager 里已经装好了对应的 Build-tools。新版 dx 认识 52.0 的 class 文件编译就过了。# project.properties targetandroid-19 sdk.buildtools23.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$BadTokenExceptionActivity 已 finish 还执行 startActivityjump 前加 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 面板快得多也不容易卡。这几个习惯看着琐碎但项目做到后期界面一多能省下大量排查时间。