
在动手写第一行代码之前值得先想清楚一件事为什么几乎所有 Android 入门课程的第一课都是欢迎界面而不是登录页、列表页或者详情页。博学谷这个案例也是一样的选择它的第一节让你在 Eclipse 里做一个带 logo、带版本号、几秒后自动跳转的启动页看起来简单到有点敷衍但真正把它写完、写对你会发现 Activity 的生命周期、Intent 跳转、资源目录规则、主题与全屏配置、Handler 延时任务、SharedPreferences 这几个 Android 最核心的东西全都被你摸了一遍。换句话说欢迎界面是一个体量最小的全栈练习场字符串、图像、主题、清单文件、Java 逻辑全都要照顾到任何一个环节出问题它都跑不起来。我在带新人和帮人看老项目的时候见过太多欢迎界面写成能跑就行的状态白屏闪一下、返回键能回到欢迎页、图片放大后一片模糊、首页点两下就崩。这些问题单看都不难但没人讲清楚背后的原理下次换个界面你还得再踩一遍。所以这篇不打算只给你一份粘贴就能用的代码而是把博学谷欢迎界面这一节拆开揉碎从 Eclipse 这个老工具链为什么还值得走一遍到冷启动那一秒钟系统到底干了什么再到跳转逻辑、Handler 的线程安全、以及在 Eclipse 里最容易把人逼疯的 dx 报错和 R.java 不生成全部串成一条线。不管你是完全没碰过 Android 的新手还是从 Android Studio 转过来想看懂老工程结构的人都能从里面拿到能直接用的东西。1. 为什么博学谷的第一课偏偏选了 Eclipse 做欢迎界面1.1 把博学谷欢迎界面的需求摊开来看博学谷这个案例的欢迎界面需求其实非常克制App 启动后展示一张品牌图、一行应用名、一行版本号停留大概三秒然后判断是不是第一次安装。第一次装就跳引导页不是第一次就直接进主界面。就这么点事但它牵扯到的东西一点都不少。先看界面部分。要显示 logo你就得把图片放进res/drawable-*目录于是必须理解密度目录这套规则不然图在低密度机上会被放大到糊。要显示应用名和版本号你就得用strings.xml做资源引用不能硬编码否则后面做多语言时全部返工。要去掉顶部那条碍眼的状态栏和标题栏你就得认识主题和AndroidManifest.xml里的android:theme而不是在 Java 里瞎写一行requestWindowFeature。再看逻辑部分。三秒后跳转怎么实现大多数人下意识写Thread.sleep(3000)然后界面直接卡死三秒输入事件全部无响应。正确做法是丢一个延时任务到主线程的消息队列里这时候 Handler 的机制就绕不过去了。判断首启你得存一个标记SharedPreferences 是最轻的选择于是文件存储、键值读写、commit()和apply()的区别也得过一遍。最后跳转用 Intent而且必须记得finish()掉自己不然用户按返回键又回到欢迎页体验直接崩掉。一个界面把四大组件里的 Activity 用透了把资源系统、主题系统、存储、线程消息机制全带出来了这就是它适合当第一课的原因。需求小但外延大学一节顶三节。1.2 Eclipse ADT 和 Android Studio 到底差在哪现在新项目基本都用 Android Studio那为什么还要在 Eclipse 里走一遍我自己的理由是三条老教材和老项目还在用它、有些离线环境装不了 Gradle、以及你想真正看懂 R.java 是怎么来的。两者的差别最直观的体现就是工程目录。Eclipse 的 Android 工程里有一个gen目录里面的R.java是 aapt 工具在构建时自动生成的你不写它但它必须存在所有R.layout.xxx、R.id.xxx都指向它。Android Studio 迁移到 Gradle 之后R 类的生成过程被藏在构建流程里你几乎看不到它但原理一模一样。看懂 Eclipse 的gen目录你就懂了资源 ID 的本质它就是个 int 常量由构建工具根据资源目录扫描出来。另一个明显差异是依赖管理。Eclipse 里你把 jar 丢进libs目录ADT 会自动把它加进构建路径不需要声明。Android Studio 里你得在build.gradle里写implementation还得处理仓库、版本冲突。前者省事但容易乱后者啰嗦但可控。对比项Eclipse ADTAndroid Studio Gradle构建方式ADT 插件调用 aapt、dx 工具链Gradle 任务驱动链路封装更深资源 IDgen 目录下可见 R.java构建期生成默认不可见依赖管理丢进 libs 即生效需在 build.gradle 声明编译速度小工程尚可大工程 clean 一次要等很久增量构建更成熟调试体验DDMS 面板独立堆、线程查看直观集成在 IDE 里功能更全适合场景看老工程、学构建原理、离线教学日常开发和上线交付我自己带人的时候有个固定套路先用 Eclipse 把欢迎界面从零搭一遍让学习者亲眼看到 R.java 是怎么冒出来的、dx 是怎么把 class 转成 dex 的然后再切到 Android Studio 用 Gradle 重做一遍。两遍下来构建这件事就从黑盒玄学变成了知道里面有几个环节。直接上 Android Studio 也不是不行只是很多报错你只能靠搜索猜。1.3 环境搭建里三个最容易被忽略的版本坑Eclipse 时代的 Android 开发最磨人的不是写代码是配环境。ADT 插件、SDK、JDK 三者的版本匹配关系非常敏感装错一个版本能让你卡一整天。组件推荐版本说明Eclipse4.2 到 4.4 之间版本太新 ADT 插件装不上太旧没有必要的编辑器功能ADT 插件23.0.6这是官方最后发布的 ADT 版本再往后就没有了Android SDK Tools24 以前配套 build-tools 建议 22 到 23 之间JDK1.7这是最关键的一条后面第 6 节会展开讲为什么编译目标android-19 到 android-22老教材一般用 19 或 21SDK Manager 里要提前下好对应平台第一个坑是 JDK 版本。很多人电脑上装的是 JDK 1.8然后发现编译死活过不去报一个dx unsupported class file version 52.0。原因是 dx 这个转换工具只认到 Java 7 的 class 文件格式而 JDK 1.8 编译出来的是 52.0 版本。解决办法要么换 JDK 1.7要么在项目属性里把编译合规级别降到 1.7具体操作在第 6 节详细写。第二个坑是 SDK 平台和编译目标不匹配。project.properties里写了targetandroid-21但 SDK Manager 里只下了 android-19Eclipse 会直接报找不到目标平台而且报错信息不指向根因新手很难定位。第三个坑是汉化包。很多人图省事装 Babel 汉化插件结果菜单翻译得乱七八糟报错信息也被翻得看不出原始含义搜索都搜不到。英文界面下报错关键词可以直接复制去查效率高得多。我的建议是原生英文照着用看不懂的菜单随手记一下一周就习惯了。2. 冷启动那一秒钟系统到底在忙什么2.1 从点击图标到第一帧出现的完整链路理解白屏问题的前提是搞清楚点击图标之后系统做了什么。这个过程比很多人想象的复杂。你点下图标Launcher 发出一个启动请求系统里的 Zygote 进程收到后 fork 出一个新的应用进程。新进程起来之后先初始化 Application执行attachBaseContext和onCreate然后 ActivityThread 接管通过 Binder 和系统服务通信创建 Activity 实例依次回调onCreate、onStart、onResume。但注意这些回调走完并不意味着你能看到东西真正的第一帧要等主线程完成布局测量、绘制、合成交给屏幕才算完。关键点在于Application.onCreate和Activity.onCreate里执行的每一行代码都在抢占第一帧出现前的时间。你在onCreate里读文件、解析 JSON、初始化 SDK多花五百毫秒用户就多盯着白屏五百毫秒。博学谷的欢迎界面之所以值得单独做一页就是为了把这部分耗时藏在一个有品牌信息的界面后面而不是让用户对着一个空白窗口发呆。再补一个容易被忽视的细节onCreate里 setContentView 的那份布局inflate 本身也要时间。布局层级越深、控件越多inflate 越慢。所以欢迎界面的布局一定要简单最好就是一层 RelativeLayout 或 FrameLayout 套两三个子 View不要为了好看堆一堆嵌套。2.2 白屏或者黑屏根子都在主题的 windowBackground 上很多人第一次遇到的现象是点开 App先闪一下白屏然后才出现自己的界面。有人以为是手机卡有人以为是代码问题其实根子在于启动窗口的背景。Android 系统在应用进程还没准备好、Activity 还没绘制之前会先给用户展示一个启动窗口Starting Window它用的背景就是当前 Activity 主题里android:windowBackground定义的那个 drawable。默认主题里这个值是纯白或者纯黑的所以你会看到白屏或黑屏一闪。等你的 Activity 真正绘制出来启动窗口才被替换掉。这就解释了一个反直觉的结论白屏不是 bug是系统为了防止用户看到桌面而准备的兜底。你要做的不是消灭它而是让它看起来像是你界面的一部分。常见的处理思路有两种。第一种是把android:windowBackground直接设成一张图片或者一个包含背景色和 logo 的 layer-list这样启动窗口一出现就已经是欢迎界面的样子视觉上无缝衔接。第二种是在布局最外层盖一个和背景同色的 View等数据准备好再隐藏。第一种更优雅因为它在绘制之前就生效了第二种要等布局 inflate 完才起作用慢了半拍。2.3 用 layer-list 顶住启动窗口的具体写法我一般推荐用 layer-list 而不是直接放一张大图原因是 layer-list 可以用纯色加居中小图的方式适配不同分辨率时不会拉伸变形。写法大概是这样!-- res/drawable/splash_bg.xml -- ?xml version1.0 encodingutf-8? layer-list xmlns:androidhttp://schemas.android.com/apk/res/android item color android:color#FFFFFF / /item item bitmap android:gravitycenter android:srcdrawable/logo_splash / /item /layer-list然后定义一个专门给欢迎页用的主题!-- res/values/styles.xml -- style nameSplashTheme parentandroid:style/Theme.NoTitleBar.Fullscreen item nameandroid:windowBackgrounddrawable/splash_bg/item /style在清单文件里把这个主题给欢迎页 Activityactivity android:name.ui.SplashActivity android:themestyle/SplashTheme intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity有一个细节必须注意这个主题只用来撑启动窗口等onCreate执行到 setContentView 之前最好把主题切回正常的应用主题否则你的布局里所有控件都会继承Theme.NoTitleBar.Fullscreen的属性按钮样式、字体颜色都可能和你预期的不一样。切主题的写法是Override protected void onCreate(Bundle savedInstanceState) { setTheme(R.style.AppBaseTheme); // 必须在 super.onCreate 之前 super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); }setTheme一定要放在super.onCreate(savedInstanceState)前面这是从主题系统生效时机决定的Activity 的主题在super.onCreate里被读取并应用到窗口上之后再改就晚了。这个顺序错误造成的现象是主题没生效而且不报错非常难查。3. 欢迎页的布局、图片资源和屏幕适配3.1 全屏无标题栏的两种做法与取舍要去掉标题栏有两种方式。一种是在主题里声明比如上面用的Theme.NoTitleBar.Fullscreen另一种是在 Java 代码里调用requestWindowFeature(Window.FEATURE_NO_TITLE)。我强烈推荐用主题。原因是requestWindowFeature必须在setContentView之前调用一旦顺序写反就抛异常而且它是代码级配置读代码的时候不直观。主题配置在清单文件里一眼就能看到哪个页面是全屏的维护成本低。不过要注意Fullscreen会连状态栏一起隐藏。如果你的欢迎界面是纯图全屏这没问题但如果你想保留状态栏显示时间电量就只用Theme.NoTitleBar不要加 Fullscreen。另外还有一个误区有人为了全屏在代码里加getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, ...)这行代码在主题已经声明 Fullscreen 的情况下是多余的而且它生效时机在窗口创建之后会出现先显示状态栏再消失的闪烁。既然主题能解决就别在代码里重复做。3.2 图片密度目录选错效果差得离谱这是我在老项目里见到最多的图像问题。Android 的设备屏幕密度分好几档drawable-mdpi是基准1 倍drawable-hdpi是 1.5 倍drawable-xhdpi是 2 倍drawable-xxhdpi是 3 倍。系统会根据当前设备密度从对应目录取图如果找不到就找一个最接近的目录然后做缩放。缩放的代价有两个一是内存占用变大二是画质变差。举个具体的例子。你有一张 1080x1920 的背景图只放进了drawable-mdpi。在一台 xxhdpi 的手机上跑系统认为这张图是给 1 倍密度准备的要放大到 3 倍才能铺满于是解码出来的位图变成 3240x5760。按 ARGB_8888 每像素 4 字节算内存占用是 3240 × 5760 × 4 ≈ 74.6 MB。而如果你把它正确地放进drawable-xxhdpi原尺寸解码只要 1080 × 1920 × 4 ≈ 8.3 MB。差了将近九倍中低端机直接 OOM。密度目录缩放比例同一张 1080x1920 图解码后的内存drawable-mdpi3 倍放大约 74.6 MBdrawable-hdpi2 倍放大约 33.2 MBdrawable-xhdpi1.5 倍放大约 18.6 MBdrawable-xxhdpi1 倍原样约 8.3 MB所以规则很简单按图片的设计尺寸放进对应的密度目录。如果只有一张图放drawable-xxhdpi是当下最省事的做法因为主流机型密度偏高往低密度机上缩放的画质损失比放大要小得多。3.3 scaleType 的选择决定了会不会被拉变形ImageView 的scaleType属性有七八个取值欢迎界面里最常用的是三个fitXY、centerCrop、fitCenter。fitXY是把图强行拉到和控件一样大横纵比不管结果就是人像被压扁、圆形变椭圆。除非你的图是可以随意拉伸的纯色背景或渐变否则别用。centerCrop是按比例放大到完全覆盖控件多出来的部分裁掉。适合铺满全屏的背景图视觉冲击力强代价是边缘可能被裁掉一点内容。fitCenter是按比例缩放到完整显示在控件内可能留白但内容一定不会被裁。适合 logo 这种不能丢内容的图。博学谷欢迎界面里我是这么分的整张背景用centerCrop铺满中间的 logo 用fitCenter保证完整底部的应用名和版本号用普通 TextView靠 RelativeLayout 的alignParentBottom定位这样不管屏幕多长多宽三块元素的相对位置都是对的。还有个小技巧版本号可以动态取不用写死。在onCreate里拿到 PackageManager调getPackageInfo(getPackageName(), 0).versionName拼成版本 V1.0.0塞进 TextView。这样每次改版本号不用动布局文件也避免了多处不一致。4. 三秒之后去哪首启判断和跳转逻辑4.1 SharedPreferences 存首启标记的边界条件判断是不是第一次启动最轻量的办法是 SharedPreferences 里存一个布尔值。首次启动时读不到这个键判定为首启走完引导流程后写入 true。逻辑本身没难度难的是边界条件。第一个边界是卸载重装。SharedPreferences 存在应用私有目录/data/data/包名/shared_prefs/下卸载应用时会被一起清掉所以卸载重装后又会走引导页。这通常是你想要的行为但如果产品希望记得用户装过那就得用外部存储或者服务端标记成本高很多。做课程练习的时候按默认行为就行。第二个边界是版本升级。很多产品希望在升级到大版本后重新展示一次新功能引导这就需要把是否首启和版本号结合起来判断。我的做法是存一个已展示引导的版本号每次启动拿当前 versionCode 和它比不相等就展示引导展示完把新版本号写回去。这样一个键同时覆盖了首启和升级首启两种场景。第三个边界是异常中断。用户在引导页中途杀掉进程标记没写进去下次还会再展示一遍。这属于可以接受的代价真要严格处理就得在进入引导页时就写入标记但那样用户没看完引导就退出下次反而看不到了体验更差。我的选择是在引导页的开始使用按钮点击时写入逻辑上最符合用户预期。4.2 versionCode 和 versionName 该用哪个做比较这两个值都在清单文件里但用途完全不同混用会产生很难查的问题。versionCode是整数给系统看的每次发版必须递增用于应用市场判断哪个包更新。versionName是字符串给人看的可以写成 1.2.3 这种格式用户可以随意改。做版本比较必须用versionCode因为它单调递增直接比大小就行。如果用versionName做字符串比较会出现 1.10.0 小于 1.9.0 这种荒唐结果因为字符串是按字典序比的。属性类型面向对象是否可随意修改用于比较versionCodeint系统、应用市场每次发版必须递增可以直接比大小versionNameString最终用户格式随你可读性优先不建议字典序会出错清单文件里的写法manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.boxuegu.app android:versionCode3 android:versionName1.0.2读取的时候用PackageInfo.versionCode取整数做比较用PackageInfo.versionName取字符串做展示职责分清后面就不会出乱子。4.3 跳转必须带 finish否则返回键会拆穿你跳转本身很简单两行代码Intent intent new Intent(SplashActivity.this, GuideActivity.class); startActivity(intent); finish();重点是那行finish()。没有它欢迎页会留在返回栈里用户在引导页或主界面按返回键会回到已经跳过的欢迎页然后再等三秒再跳一次。用户会觉得这个 App 有病。finish()的作用是把当前 Activity 从任务栈里移除调用后系统会走onPause、onStop、onDestroy资源被回收。这里有个细节需要注意finish()不会立刻返回它只是标记了要结束当前方法会继续执行完。所以不要在finish()后面再写需要 Activity 存活的代码会出空指针。还有一个更隐蔽的问题如果跳转是在延时任务里触发的用户可能在延时期间手动点了跳过按钮这时候两个跳转都会执行出现闪屏或者跳两次。处理办法是加一个布尔标志跳转前置为 true 并判断private boolean isJumped false; private void jumpTo(Class? target) { if (isJumped) { return; } isJumped true; startActivity(new Intent(this, target)); finish(); }这个守卫标志看起来多余但在真实项目里能挡掉相当一部分偶现的闪一下问题。公众号后台和崩溃平台上不少这类问题最后查出来都是重复跳转。5. 倒计时和那根容易出事的 Handler5.1 CountDownTimer 和 Handler.postDelayed 怎么选实现延时跳转有两条常见路线。一条是Handler.postDelayed一条是CountDownTimer。如果只是简单的三秒后跳两者都能做但适用场景不太一样。Handler.postDelayed只有一个动作就是延时执行一次代码最少也最直接。CountDownTimer适合需要在界面上显示倒计时数字的场景因为它每隔一个间隔就回调一次onTick你可以在里面更新3、2、1的显示最后在onFinish里跳转。方式回调次数适合场景取消方式Handler.postDelayed一次只跳转不显示倒计时removeCallbacksCountDownTimer多次需要显示还剩 N 秒cancel博学谷这个案例如果只是静默跳转用 Handler 就够了。但如果产品要求右上角有个跳过 3s的倒计时按钮那 CountDownTimer 更省事不用自己维护剩余秒数。不管用哪个取消都要做。用户按 Home 键切后台或者点了跳过延时任务如果还在跑等它触发时 Activity 可能已经不可见了startActivity会抛异常或者跳到奇怪的地方。所以onDestroy里必须清理Override protected void onDestroy() { super.onDestroy(); if (handler ! null) { handler.removeCallbacksAndMessages(null); } }5.2 内部类持有 Activity 引用的内存泄漏这是 Android 开发里最经典的坑之一。如果你这么写// 有问题的写法 private Handler handler new Handler() { Override public void handleMessage(Message msg) { jumpTo(GuideActivity.class); } };这个匿名 Handler 是内部类隐式持有外部 SplashActivity 的引用。你把三秒的延时任务丢进去之后如果用户在这三秒内按了返回键退出Activity 本来该被回收但因为延时消息还在 MessageQueue 里MessageQueue 引用 HandlerHandler 引用 ActivityActivity 就回收不掉。三秒虽然短但如果哪天需求改成三十秒的开屏广告这个泄漏就会很显眼。正确的写法是把 Handler 声明成静态内部类用弱引用持有 Activityprivate static class SplashHandler extends Handler { private final WeakReferenceSplashActivity ref; SplashHandler(SplashActivity activity) { this.ref new WeakReference(activity); } Override public void handleMessage(Message msg) { SplashActivity activity ref.get(); if (activity null || activity.isFinishing()) { return; } activity.jumpTo(GuideActivity.class); } }静态类不持有外部实例弱引用让 Activity 在内存紧张时能被回收。那个isFinishing()判断也要留着因为弱引用可能还没来得及被清空但 Activity 已经在关闭流程中了。有人会问用Handler(Looper.getMainLooper())配合 Runnable 是不是也一样。其实是一样的泄漏的本质是延时任务对象间接引用了 Activity跟你用 Message 还是 Runnable 没关系。所以静态加弱引用这套写法值得养成习惯。5.3 跳过按钮和生命周期打断的完整处理真实的欢迎页一定会有一个跳过按钮用户不想等那三秒。加这个按钮之后要考虑的情况就多了。用户点跳过时你要做三件事取消延时任务、执行跳转、防止重复。前两件前面都讲了第三件靠守卫标志。还有一件事容易被漏掉如果用户点跳过的瞬间延时任务刚好也在执行两个线程都在主线程上不会真正并发但事件顺序是先到先执行所以只要守卫标志拦在第一行就不会出问题。另一个场景是用户按 Home 键切到后台再切回来。这时候延时任务还在计时可能你切回来的瞬间它就触发了跳转用户莫名其妙就到了主界面。我的处理是在onStop里记录已过去的时间在onStart里补算剩余时间如果已经超时就立即跳转否则重新起一个延时。这个逻辑稍微麻烦一点但对体验的提升是明显的。private long startTime; Override protected void onStart() { super.onStart(); startTime System.currentTimeMillis(); handler.sendEmptyMessageDelayed(MSG_JUMP, SPLASH_DURATION); } Override protected void onStop() { super.onStop(); handler.removeMessages(MSG_JUMP); }如果不需要那么精确最简单的做法是onStop里取消任务onStart里重新计时。用户在后台待多久都不会影响代价是每次切回来都要重新等三秒稍微有点烦但不会出错。课程练习阶段这样处理足够了。6. 在 Eclipse 里真正把人卡住的几个报错6.1 dx unsupported class file version 52.0 的完整定位过程这个报错是我在 Eclipse 时代见得最多、也最折磨人的一个。完整信息大概是Dx unsupported class file version 52.0新手看到这个第一反应是版本不兼容然后去网上一通搜搜到的答案五花八门有人让换 SDK有人让改 dx 参数越改越乱。其实根因非常明确。52.0是 Java 8 编译出来的 class 文件版本号Java 7 是 51.0Java 6 是 50.0。ADT 里的 dx 工具负责把 class 转成 dex而它只认到 51.0。如果你项目里的 Java 编译器用的是 JDK 1.8哪怕代码一行没用 Java 8 的特性编译出来的 class 文件头也是 52.0dx 直接拒收。排查链路是这样先看报错指向哪个模块一般是你自己工程或者某个库然后在 Eclipse 里右键工程PropertiesJava Compiler看 Compiler compliance level 是不是 1.8再看 Java Build Path 里 JRE 指向的版本。两处都改成 1.7 之后右键工程 Clean 一次重新编译问题通常就没了。如果改完还是报同样错说明有第三方 jar 是用 JDK 1.8 编译的。这种情况只能换库版本或者用-target 1.7重新编译那个库。我在一个老项目里遇到过某个统计 SDK 的新版本用了 Java 8降回旧版本立刻就好。注意改 JDK 版本时不要只改 Build Pathproject.properties里的java.compiler.level和 Eclipse 的全局编译器设置都要看一眼三处不一致的话Eclipse 用哪个值是不确定的。6.2 R.java 不生成按这个顺序查R.java 消失是老工程的另一大经典。现象是满屏红色所有R.xxx都报错gen目录是空的或者干脆不存在。排查顺序我总结成五步基本能覆盖九成情况。第一步看 res 目录有没有 XML 报错。Eclipse 对资源文件的语法非常严格一个属性名写错、一个标签没闭合aapt 就直接罢工R.java 不生成。打开 Problems 视图把所有跟 res 相关的 Error 先清掉。第二步看project.properties里的 target 是不是指向一个已经安装的 SDK 平台。写了 android-22 但 SDK Manager 里没下 android-22会直接卡在这一步而且报错信息不指向这个原因。第三步确认 Project 菜单里的 Build Automatically 是勾上的。有时候不小心点掉了改了资源也不重新构建看起来就像 R.java 丢了。勾上之后右键工程 Clean 一次。第四步手动删掉 gen 目录再 Clean。Eclipse 偶尔会缓存错误的构建状态手动删掉强制重新生成比反复 Clean 有效。第五步检查包名一致性。清单文件里的package属性、源码里的包声明、以及 R 类的导入路径必须一致。移动文件到别的包之后忘了改清单也会出现 R 找不到的情况这时候通常伴随 AndroidManifest 的报错。顺手提一句如果导入的是别人的工程还要看.classpath和.project这两个文件是不是带着原作者的绝对路径这种情况 Clean 一百次也没用得手动改路径或者干脆新建工程把源码拷进去。6.3 大图导致的 OOM 与内存优化欢迎界面放了一张大背景图低端机一跑就崩日志里是java.lang.OutOfMemoryError。前面 3.2 节讲了密度目录放错会放大内存这里补充几个实战手段。第一个手段是控制图片本身的尺寸。设计给的原图如果是 2400x3200先压缩到实际需要的尺寸再放进工程。手机屏幕再大也就 1440 宽超过这个宽度的高清图除了增加内存没有任何意义。压缩这事别偷懒一张图少几十兆内存收益极大。第二个手段是如果图片不需要透明度用Bitmap.Config.RGB_565解码每像素两字节比 ARGB_8888 省一半。写法是自己用 BitmapFactory 带 Options 解码而不是直接让 ImageView 去加载资源。缺点是 RGB_565 会有轻微的色带纯色渐变图慎用。第三个手段是把背景做成 9-patch。如果是纯色或者简单渐变的底图9-patch 能把一张 1x1 的图拉伸铺满全屏内存占用可以忽略。做法是用draw9patch工具给图加黑边标记可拉伸区域放进drawable目录文件名以.9.png结尾。这个技能在老项目里非常实用很多大背景都能这么优化掉。优化手段内存收益适用条件注意点正确放置密度目录高所有位图需要针对多密度出图或选一个主密度预压缩尺寸高所有位图别压到低于实际显示尺寸RGB_565 解码中无透明度需求渐变可能出现色带转 9-patch高纯色或简单渐变拉伸区域要标对否则变形及时 recycle低手动管理位图的场景现代系统 GC 基本能处理别过度使用最后提醒一句Bitmap.recycle()在老代码里到处都是但在 Android 3.0 之后位图内存已经纳入 Java 堆管理正常场景下不调用也不会泄漏反而调早了会出现 Canvas: trying to use a recycled bitmap。除非你在做图片列表这种高频创建销毁的场景否则别手动 recycle。7. 欢迎页做完之后博学谷这个项目还能往下延展什么7.1 把欢迎页抽成一个可复用的小组件写完一个欢迎页如果就这么放着下一个项目你还得从头抄一遍。我习惯把它抽成三样东西一个SplashActivity基类、一个SplashTheme、一份配置常量。配置常量负责三件事停留时长、跳转目标、是否显示跳过按钮。把这些写成静态字段或者从strings.xml里读改的时候不用动逻辑代码。基类里封装好延时任务、守卫标志、生命周期清理这套公共逻辑子类只需要重写跳到哪里去和判断首启这两个方法。这么改的好处是显而易见的。以后做新项目代码复制过来改三行就能用而且因为基类里的生命周期处理是验证过的不会出现每个项目都要重新踩一遍 Handler 泄漏和重复跳转的问题。这就是把一次练习变成资产的过程。7.2 迁移到 Android Studio 时要注意的对应关系如果你后面打算把这个工程搬到 Android Studio有几个对应关系需要知道能省很多时间。目录结构上Eclipse 的src对应 Android Studio 的src/main/javares对应src/main/reslibs对应libs并在 build.gradle 里声明AndroidManifest.xml对应src/main/AndroidManifest.xml。gen目录直接不要Android Studio 不需要它。主题和布局文件基本可以原样拷过去因为资源格式是同一套。Java 代码如果没用 Java 8 特性可以直接用如果用了 lambda 或者接口默认方法需要在 build.gradle 里开启 Java 8 支持。清单文件里的package属性在 Android Studio 里建议改到 build.gradle 的applicationId两边不一致会有坑。还有一个实际经验迁移之后先跑一遍欢迎界面确认跳转和首启判断都正常再去动其他模块。因为欢迎页是最小可运行单元它是你验证整个工程配置是否正确的探针。这一关过了后面的事都好办。我个人的做法是保留两套工程Eclipse 版本作为看原理的参考Android Studio 版本作为干活的主力。遇到 Gradle 报错看不懂的时候回去看 Eclipse 是怎么调的往往能想明白问题出在哪一层。工具会变构建的环节不会变aapt、dx、打包、签名这些步骤在哪个 IDE 里都得走一遍只是封装深浅不同而已。把欢迎界面这一课吃透你就等于拿到了看懂整个 Android 构建链路的钥匙后面再学什么都会轻松不少。