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 的工程师也知道并且有填坑攻略,但还是要自己去发现的。

Comments

Why Proguard

Proguard 是什么?要清楚这个概念,我们先看看 Proguard 官方是怎么定义的,再看看 Android 官方是怎么定义

Proguard 官方

ProGuard is a free Java class file shrinker, optimizer, obfuscator, and preverifier. It detects and removes unused classes, fields, methods, and attributes. It optimizes bytecode and removes unused instructions. It renames the remaining classes, fields, and methods using short meaningless names. Finally, it preverifies the processed code for Java 6 or higher, or for Java Micro Edition.

ProGuard 是一个免费的压缩、优化、混淆,预验证 Java 类的工具。它能在编译期间检测并移除没有用到的类、变量、方法和属性,也能优化字节码并且移除没有用到的指令。ProGuard会把那些类、变量、和方法用一些短小且无意义的名称去重命名。最后,对于 Java 6 或者更高的版本,或者 Java Micro Edition,它还会预校验已处理的类代码,从而利于更快加载。

看起来有点意思,再来看一下 Android 官方的定义

The ProGuard tool shrinks, optimizes, and obfuscates your code by removing unused code and renaming classes, fields, and methods with semantically obscure names. The result is a smaller sized .apk file that is more difficult to reverse engineer. Because ProGuard makes your application harder to reverse engineer, it is important that you use it when your application utilizes features that are sensitive to security like when you are Licensing Your Applications.

ProGuard 通过移除未使用的代码和使用一些语意模糊的名字来重命名类、变量、方法和属性名,从而达到压缩、优化,和混淆代码的目的。最终可以得到一个更小的 .apk 文件,这个文件会增大软件逆向工程(反编译)的难度。正因为 ProGuard 会让你的应用更加难以被逆向工程反编译,所以对于独立应用而言,如果你对你的代码安全很敏感,建议在签名阶段还是“ ProGuard 一下” 。

Read on →
Comments

很长时间以来 Mobile Lin 访问慢这个问题我是知道的,但是一直也没想着去整,主要是因为觉得真正搞技术的人肯定都知道是什么原因导致访问慢,而且一定也知道加速的办法是什么。但这其实都是在为自己的懒找借口。晚上微博上终于有哥们儿跟我说了:

毕竟手机上2G网络挂着VPN来访问你的网站,也不是一件容易的事。

好吧。既然用户有需求,那就开整呗~

Octopress 在国内访问速度的优化主要从两方面进行:

googleapis 相关

Octopress 默认使用了 google fonts 和 googleapis 的 ajax,但因为众所周知的原因它们在国内是被墙的。好在数字公司在这点上做了件好事,它们有一个这玩意儿:

360网站卫士常用前端公共库CDN服务

这么一来就好办了,打开 /Octopress/source/_includes/head.html

1
2
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<link href='http://fonts.googleapis.com/css?family=Open+Sans:400italic,400,700' rel='stylesheet' type='text/css'>

换成

1
2
<script src="//ajax.useso.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<link href='http://fonts.useso.com/css?family=Open+Sans:400italic,400,700' rel='stylesheet' type='text/css'>

gravatar 相关

gravatar 是一个全球公认的头像库,跟你的 e-mail 绑定。可惜,这么好的东西在我大天朝也是不存在的。不过,依旧好在国内有 duoshuo,他们提供了一个 gravatar 的缓存。打开 /Octopress/source/_includes/header.html

1
2
3
<script type="text/javascript">
  document.write("<img src='http://www.gravatar.com/avatar/" + MD5("YOUR_EMAIL") + "?s=160' alt='Profile Picture' style='width: 160px;' />");
</script>

换成:

1
2
3
<script type="text/javascript">
  document.write("<img src='http://gravatar.duoshuo.com/avatar/" + MD5("YOUR_EMAIL") + "?s=160' alt='Profile Picture' style='width: 160px;' />");
</script>

That’s all,我们 rake generate 一下之后本地预览一下就可以看到非常显著的效果。

参考

替换Octopress Google 字体库

Copyright © 2014 - 2016 - linshen - @ . +