Comments

Google 在今年的 IO 大会重点介绍了它们最新推出的 Android Architecture Components,其中最重要的一个就是 Room。在 Ormlite、GreenDao,甚至 Realm 大行其道的今天,Google 自己也总算造了一口锅自己背上了(只求 Google 日后不要轻易弃坑)。

这篇文章没有太多深奥的源码分析,因为我下午看完官方文档之后,还是觉得有点复杂,不利于初学者学习如何使用,所以打算写一篇文章来帮助大家入门。

Room 的一些特点

  1. 编译时 sql 语句检查。相信大家都有过 app 跑起来,执行到 db 语句的时候 crash,检查之后发现原来是 sql 语句少了一个 ) 或者其它符号之类的经历。Room 会在编译阶段检查你的 DAO 中的 sql 语句,如果写错了(包括 sql 语法错误跟表名、字段名等等错误),会直接编译失败并提醒你哪里不对。
  2. sql 查询直接关联到 Java 对象。这个应该不用详细解释了,虽然很多第三方 db 库早已经实现。
  3. 耗时操作主动要求异步处理。这一点还是挺值得注意的,Room 会在执行 db 操作时判断是不是在 UI 线程,比如当你需要插入一条记录到数据库时,Room 会让你放到异步线程去做,否则会直接 crash 掉 app 来告诉你不这样做容易阻塞 UI 线程。虽说死相难看了点(个人觉得打个警告不就完了么?),但对于开发者开发出高质量的应用还是有帮助的。
  4. 基于注解编译时自动生成代码。这个应该算是 Room 工作原理的核心所在了,你要写的代码之所以这么少,说白了还不是因为 Google 给你写好了很多?希望以后有时间能写一篇源码分析出来,那个时候再讲吧。
  5. API 设计符合 Sql 标准。方便扩展进行各种 db 操作。

Room 的三大组件

  • Entity。实体,说白了就是我们最常见的一个对象
  • Database。数据库,Room 提供了一个非常方便的静态方法来供我们创建数据库
  • DAO。Data Access Object,把你 Entity 所有的 CRUD 业务代码封装在这里就好
Read on →
Comments

最近省吃俭用,打算入头戴式耳机的坑。其实一开始我对头戴式耳机是没有多大兴趣的,总觉得它又笨又傻,大夏天戴着还捂得慌。但是自从好几次在飞机跟地铁上被吵得实在是受不了之后,便开始打算买一款头戴式耳机,所谓存在即价值,肯定还是有用武之地的。

工欲善其事,必先利其器。既然准备买耳机,肯定要在它休息的时候给它找个家的。淘宝上转了一圈,实在是没有我满意的。后来一次无意之间在 YouTube 上看到了 @UrAvgConsumer这个视频,里面推荐了一些 $25 以下的小玩意儿,瞬间对 Avantree 这款耳机支架长草。

Read on →
Comments

WDMyCloud 买了将近一年时间了,本来准备只给 Mac 做 Time Machine 备份用的,奈何 3T 空间实在撑不满,多着也是浪费,再加上平时白天上班,家里带宽闲着也是闲着,于是今天折腾了一下给他添加了迅雷远程下载功能,亲测可用。

首先需要说明的是,网上大部分教程抄来抄去,几乎全是 Windows 下操作的版本,其实对于 Mac 用户而言,完全不用 PuTTY,因为 Mac 自带的终端一直都支持 ssh ,而 WinSCP 在 Mac 下有更好的替代软件 Cyberduck,俗称小黄鸭。所以对于 Mac 用户而言,理论上你只需要准备后者,剩下的完全不需要考虑。

步骤

一、降级 WDMyCloud 固件版本

迅雷官方的远程下载模块目前不支持 4.0+ 版本的 WDMyCloud 固件,所以如果你不小心被西部数据升级到了最新版本,需要先降回去。方法很简单:

万事先理清思路,我们的思路:v04.04 –> V04.00 –> V03.04

所以,先准备好这两个版本的固件:

http://download.wdc.com/nas/sq-040000-607-20140630.deb

http://download.wdc.com/nas/sq-030401-230-20140415.deb

然后照着这篇文章完成降级就好,很简单。

这一步我遇到几个问题,注意一下:

  • Mac 用户如果不知道你的 WDMyCloud 在局域网内 IP 是多少,可以通过 ping 一下 wdmycloud.local 获得。

  • 在降到 V04.00 之后,再次上传固件准备降到 V03.04 的时候,WDMyCloud 会提示你没有空间上传固件了,我初步判断是 WDMyCloud 的一个 bug 导致的,它在升完之后没把我们上一次传的 deb 文件删掉导致。解决办法就是去 设置->实用工具->系统出厂还原->仅系统 还原一下系统,然后再继续降级。如果你没找到,看下图:

  • V04.00 –> V03.04 刷完之后 WDMyCloud 会提示“升级失败”。这是正常的,因为我们本身就是在做降级操作。去“固件”里看一下,已经是 V03.04 了就算大功告成。另外记得把自动更新关掉,防止再次被升级到最新固件。

Read on →
Comments

问题

钱包 2.0.0_beta 版本上线之后,我们给新获得理财权限的用户增加了一个欢迎对话框。按照 Google 官方关于开发对话框时的指导,钱包把所有会用到的 Dialog 全部采用 DialogFragment 进行了封装,并且放到了一个包下进行管理。

DialogFragment 的用法非常简单,而且内部封装好了show() 方法,很容易就可以把对话框展现出来,就像这样

1
2
3
4
5
public void showNoticeDialog() {
        // Create an instance of the dialog fragment and show it
        DialogFragment dialog = new NoticeDialogFragment();
        dialog.show(getSupportFragmentManager(), "NoticeDialogFragment");
    }

然而上线后不到一周时间,我在后台看到了很多用户遇到了这样的奔溃:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
  at android.support.v4.app.z.v(SourceFile:1440)
  at android.support.v4.app.z.a(SourceFile:1458)
  at android.support.v4.app.k.a(SourceFile:634)
  at android.support.v4.app.k.b(SourceFile:613)
  at android.support.v4.app.q.show(SourceFile:139)
  at com.meizu.flyme.wallet.fragment.p.c(SourceFile:195)
  at com.meizu.flyme.wallet.fragment.p.c(SourceFile:390)
  at com.meizu.flyme.wallet.fragment.p.b(SourceFile:61)
  at com.meizu.flyme.wallet.fragment.p$6.a(SourceFile:469)
  at com.meizu.flyme.wallet.fragment.p$6.onResponse(SourceFile:464)
  at com.android.volley.toolbox.u.deliverResponse(SourceFile:60)
  at com.android.volley.toolbox.u.deliverResponse(SourceFile:30)
  at com.android.volley.g.run(SourceFile:99)
  at android.os.Handler.handleCallback(Handler.java:815)
  at android.os.Handler.dispatchMessage(Handler.java:104)
  at android.os.Looper.loop(Looper.java:194)
  at android.app.ActivityThread.main(ActivityThread.java:5824)
  at java.lang.reflect.Method.invoke(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:372)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1010)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:805)

这个错误堆栈正是在调用了 DialogFragmentshow()方法之后才出现的,真的是非常奇怪。一开始,我们以为是简单的 fragment 生命周期问题导致的 crash,然而等我在 show() 方法之前加上了isAdded() 判断还是没有用之后,我知道事情可能没那么简单了。

Read on →
Comments

本文由 Lin Shen 译自 Common questions on AsyncTask,原文作者 Colt McAnlis, 转载请务必注明出处!

关于 AsyncTask 的常见问题

我在 Google 工作最喜欢的一点,就是把一些比较复杂的概念,分解成一小部分一小部分,这样就可以确保每一位工程师都能清楚地理解。在我最近的《Android 性能优化典范》视频中,我提到了一个表面上看上去很直观,但是它的一些属性可能会带来一些万万没想到的负面影响的东西,我是说,毫无悬念,这货就是 关于 AsyncTask:

我在这个视频里强调了一些开发者之前在使用 AsyncTask 时可能并不会注意到的事项。多亏了我们有一些非常棒的 android 开发者交流社区,能让我们看到大家反馈的一些问题:

让我们来挖掘其中的一些问题,看看能不能深入展开讨论一下:

Read on →
Comments

Project Home

https://github.com/shawnlinboy/android-OverscrollViewPager/

Description

最近项目里接到一个需求,效果如下:

简而言之,需要在 ViewPager 滑到最左或者最右的时候,仍然支持可滑动。

具体就是根据是在 ViewPager 最右边向右滑了,还是在最左边向左滑了,做出响应。这就要求对 ViewPager 的 overscroll 行为做出监听。

网上也有一些类似的轮子,但是我感觉做得真心复杂,而且都是为了实现轮播的,所以可扩展性并不强。因此我这里要做的,就是对 ViewPager 的overscroll 行为做出简单的监听并封装。剩下的,交给开发者自己去 do whatever you want 就好。

原理很简单,对手势做出监听即可。所以也算是对ViewPager 的overscroll 行为监听的另一种实现思路吧。

使用上,只需要把 ViewPager 替换成我这个就好,adapter 不需要改,也没必要改。因为我只会告诉你,你的 ViewPager 是否到头了,然后是哪边到头向哪边滑了,剩下的是你的 //TODO

欢迎各路大神对它进行拓展并发起 pr。

Demo

Comments

今天下班前被组里的小伙伴问了一个问题:

如果一个工程需要定义两个 flavor,每个 flavor 需要用一份单独的 AndroidManifest.xml,应该怎么配置?

这个问题,熟悉 gradle 的同学应该是能轻松搞定的。我们知道,gradle 在编译 apk 的时候支持给每个不同的 flavor 指定 src、res、甚至 AndroidManifest.xml 文件都没有问题。

首先我们定义两个 flavor:

1
2
3
4
productFlavors {
        normal {}
        meizu {}
    }

为了让两个 flavor 分别取不同的 AndroidManifest.xml,我们在 src 下面建立一个叫 meizu 的文件夹,里面单独放置这个 flavor 要用的清单文件,就像这样:

然后我们配置 sorceSets 闭包:

1
2
3
4
5
6
7
8
sourceSets {
        main {
            manifest.srcFile 'src/main/AndroidManifest.xml'
        }
        meizu {
            manifest.srcFile 'src/meizu/AndroidManifest.xml'
        }
    }

src/meizu/AndroidManifest.xmlsrc/main/AndroidManifest.xml的区别在于,前者删掉了 SecondActivity 的 action,理论上,如果我们编译 meizu flavor,那么在点击按钮之后,因为采用了 action 的方式来启动 activity,会因为 action 找不到导致失效。然而结果是这样吗?大家可以试一下。

结果是我们依然可以很顺利地跳到 SecondActivity…

为什么?

这就需要大家了解 gradle 在编译时,对 manifest 采用的 merge 策略。

引用一下 Ezio Shiki知乎上的一段回答:

Manifest可以通过Merge的方式合并多个Manifest源。通常来说,有三种类型manifest文件需要被merge到最终的结果apk,下面是按照优先权排序:productFlavors和buildTypes中所指定的manifest文件应用主manifest文件库manifest文件简单来说,manifest的merge会将每个元素及其子元素的节点和属性进行合并。

例如:

1
2
3
<activity
    android:name=com.foo.bar.ActivityOne
   android:theme=@theme1/>

1
2
3
<activity
    android:name=com.foo.bar.ActivityOne
   android:screenOrientation=landscape/>

合并会成为

1
2
3
4
<activity
    android:name=com.foo.bar.ActivityOne
   android:theme=@theme1
   android:screenOrientation=landscape/>

不过

1
2
3
<activity
    android:name=com.foo.bar.ActivityOne
   android:theme=@theme1/>

1
2
3
4
<activity
   android:name=com.foo.bar.ActivityOne
   android:theme=@theme2
   android:screenOrientation=landscape/>

合并会产生一个冲突,因为都有theme,而theme的属性不同。

要了解manifest合并的更高级应用,查看Manifest Merger

所以,看明白了吗?简单来说,AndroidManifest.xml 文件在 gradle 打包编译的时候,不是你指定哪个,它就100%去用哪个的。

  • 首先,你的 main 里面必须要有一份基本的,不可以因为要分 flavor 就把 main 里面的删掉,否则会直接编不过
  • 接着,你要知道 Manifest 的 merge 关系,从你的 flavor 到 main,它是一层层合并的,合并的规则上面已经提到了。
  • 最后,如果我有一个 Activity,或者 Service,或者 Receiver,真的要用另一份 AndroiManifest.xml 里的怎么办?

关于这个问题,官方文档给出了我们答案:

tools:node markers

没错,我们可以使用 tools:node replace 来解决我们的问题。现在来修改一下 src/meizu/AndroidManifest.xml,在 SecondActivity 的声明里加上,如下图所示:

再编译一下这个 flavor,点击按钮,可以看到报错了。

现在已经没有对应的 Activity 来解析这个 action 了,也就是说我们为 meizu 这个 flavor 指定的 AndroidManifest.xml 总算“生效”了。

Demo 地址:https://github.com/shawnlinboy/Android-MultiFlavors

参考文章:

https://www.zhihu.com/question/22842123/answer/55675046

http://tools.android.com/tech-docs/new-build-system/user-guide/manifest-merger#TOC-tools:node-markers

http://my.oschina.net/fallenpanda/blog/373183

Comments

好久不更新博客,上来讲一下最近踩道的一个坑,顺便感觉可以普及一下在 AsyncTask 更新 UI 时的正确姿势

最近我负责的一个模块,后台数据统计总在报 Glide 加载图片的时候报错导致停止运行,堆栈大概是这个样子的:

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
java.lang.RuntimeException: Unable to destroy activity {MY PACKAGE NAME}: java.lang.IllegalStateException: Activity has been destroyed
    at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:4097)
    at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:4115)
    at android.app.ActivityThread.access$1400(ActivityThread.java:177)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1620)
    at android.os.Handler.dispatchMessage(Handler.java:111)
    at android.os.Looper.loop(Looper.java:194)
    at android.app.ActivityThread.main(ActivityThread.java:5771)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1004)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:799)
Caused by: java.lang.IllegalStateException: Activity has been destroyed
    at android.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1383)
    at android.app.BackStackRecord.commitInternal(BackStackRecord.java:745)
    at android.app.BackStackRecord.commitAllowingStateLoss(BackStackRecord.java:725)
    at com.bumptech.glide.manager.RequestManagerRetriever.getRequestManagerFragment(SourceFile:159)
    at com.bumptech.glide.manager.RequestManagerFragment.onAttach(SourceFile:117)
    at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:865)
    at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1079)
    at android.app.BackStackRecord.run(BackStackRecord.java:852)
    at android.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1485)
    at android.app.FragmentManagerImpl.dispatchDestroy(FragmentManager.java:1929)
    at android.app.Fragment.performDestroy(Fragment.java:2279)
    at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1029)
    at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1079)
    at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1061)
    at android.app.FragmentManagerImpl.dispatchDestroy(FragmentManager.java:1930)
    at android.app.Activity.performDestroy(Activity.java:6297)
    at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1151)
    at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:4084)

于是呢,本着对开源事业的满腔热血,我二话没说便到 Glide 下面给丫开了 issue。当然了,在此还是要赞扬一下 @TWiStErRob, 这位兄台看起来不像是 Glide 的官方作者,但一直很热心地回答着各路神仙给 Glide 开的 issue。于是毫无例外我的也很快得到了回复。但答案似乎并没有太多卵用,无非就是让我提供更多细节给他们排查……算了,求人不如求自己,给他们提供细节之前,不如我自己查一遍吧。

晚上花了一点点时间理了一下整个流程:首先,既然是 android.app.FragmentManagerImpl.enqueueAction 报的 Activity has been destroyed,肯定要想到是哪个 Activity 和它 attach 的 Activity,因为这个模块从头到尾都是我一个人在弄,所以这并不是很难。我追到了我在一个 Fragment 里,假设叫它 MyMainFragment,在这里里面我调用 Glide 加载了一些图片到 RecyclerView 中,这些看起来都没什么问题。但为什么会报 Activity has been destroyed ?我想到了去它 attach 的 Activity 去看 MyFragment 当时是怎么被 commit 进来的。

MyMainFragment attach 的 Activity 中,我看到了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private class FireUpTask extends AsyncTask<Void, Void,Void> {

        @Override
        protected Void doInBackground(Void... params) {
            handleIntent();
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            initFragment();
        }
    }

    private void initFragment() {
        if (!isDestroyed()) {
            FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
            fragmentTransaction.add(R.id.main_container, MyMainFragment.newInstance());
            fragmentTransaction.commitAllowingStateLoss();
        }
    }

这个 FireUpTask 在 Activity 的 onCreate() 方法中被创建并执行,其实要做的很简单,无非就是想在后台异步把跳转进来的 Intent 处理完,然后再切到 MyMainFragment,看起来是没什么问题,而且为了防止有时候操作很快或者 Monkey 测试的时候如果切到 MyMainFragment 时 Activity 已经被销毁,我还特地加多了 if (!isDestroyed()) 判断,可为什么还是出问题?

先看一下 isDestroyed()) 方法:

Returns true if the final onDestroy() call has been made on the Activity, so this instance is now dead.

看来,SDK 告诉我们,如果这个方法返回 true ,那证明 Activity 的最后 onDestroy() 已经被调用,Activity 实例现在已经挂掉了。对啊!!!确实是这样啊,我都加了这个判断了啊,可为什么还是报错?

别着急,就在 isDestroyed()) 下面,还有一个方法,isFinishing()),于是赶紧看了一下文档:

Check to see whether this activity is in the process of finishing, either because you called finish() on it or someone else has requested that it finished. This is often used in onPause() to determine whether the activity is simply pausing or completely finishing.

这下子一目了然了吧,如果你的 Activity 正在结束,或者因为你主动调了 finish() (我在这个 Activity 里确实有一处会主动调 finish(),又或者因为其它别的什么鬼导致了 Activity 被请求销毁,这个时候可能 isDestroyed()) 可能还没有来得及返回 true,但是 isFinishing()) 就会返回 true 告诉你 Activity 确实正在被停止。

为了证实这一点,我尝试搜索了 Android 源码对这两个方法的使用。发现 Google 官方在拨号应用里就尝试做出了这样的判断:

http://androidxref.com/6.0.0_r1/xref/packages/apps/Dialer/src/com/android/dialer/calllog/ClearCallLogDialog.java#74

在“电话”应用的清除通话记录对话框中,Google 也是简单粗暴地 new 了一个 AsyncTask 来在后台清掉通话记录,然后在 onPostExecute(Void result) 中去更新 UI。亮点在于,大家可以看 Google 的工程师写了什么:

1
2
3
4
5
6
7
final Activity activity = progressDialog.getOwnerActivity();
if (activity == null || activity.isDestroyed() || activity.isFinishing()) {
  return;
}
if (progressDialog != null && progressDialog.isShowing()) {
  progressDialog.dismiss();
}

它拿了这个 dialogFragment 的宿主 Activity,然后对当前 Activity 的“死活”做出了判断,在 activity == null || activity.isDestroyed() || activity.isFinishing() 这三个都不会发生的时候,才会继续后面的操作。

这是至关重要的!虽然我们都知道,Dialog 这种东西是必须 attach 在一个带合法 Window Token 的组件,比如 Activity 或者 Frgament 上,理论上只要 Dialog 显示着,这个组件都不会被销毁。但是,我们却无法考虑到一些极端情况,比如有的用户手速确实很快,或者有些机器性能确实比较好反应很快,或者,你的应用在跑 Monkey 测试的时候,更加有可能出现这种 Activity 提前挂掉的情况。这个时候,如果应用内部不 handle 这个问题,那么呵呵呵,停止运行就来了。

回到我自己的这个问题,既然可以从源码里读到 Google 给出的答案,下面就是修自己的锅了,很简单,我们也可以仿照加上类似的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private class FireUpTask extends AsyncTask<Void, Void,Void> {

        @Override
        protected Void doInBackground(Void... params) {
            handleIntent();
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            if (isDestroyed() || isFinishing()) {
                return;
            }
            initFragment();
        }
    }

    private void initFragment() {
            FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
            fragmentTransaction.add(R.id.main_container, MyMainFragment.newInstance());
            fragmentTransaction.commitAllowingStateLoss();
    }

这样一来,如果 AsyncTask 跑完,准备去切 Fragment 的时候,Activity 已经挂了,这个时候就不会再进到 transaction 里面,自然 Fragment 也不会被 attach 进来,自然 Glide 去加载图片那些问题就都不会有了。

看来平时还是要多看看 Android 源码,感觉很多坑 Google 的工程师也知道并且有填坑攻略,但还是要自己去发现的。

Copyright © 2014 - 2017 - linshen - @ . +