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 字体库

Comments

不得不承认,长久以来,对于大部分 Android 工程师,分析内存泄露这一问题多少还是显得有些苦巴巴。因为自己去 dump HPROF 文件,再用 MAT 这类工具分析,对于之前没有接触过这方面工作的还是要一定学习成本的。而且因为这些代(da)码(keng)真的是你一行行写(wa)出来的,每个人在查自己代码的内存泄露问题时候多少都会想着“卧槽这里怎么可能有问题?这可是我亲手写的啊!!!”,这往往就让问题更加难以被发现。

今天,哦不,凌晨了。。。昨天!昨天,Android 开源界最伟(jian)大(zhi)高(kai)效(gua)的公司 Square 又向业界投下一颗重磅炸弹。推出了一个叫 LeakCanary 的玩意儿,可以通过简单粗暴的方式来让开发者获取自己应用的内存泄露情况。而且得益于 gradle 强大的可配置性,可以确保只在编译 debug 版本时才会检查内存泄露,而编译 release 等版本的时候则会自动跳过检查,避免影响性能。当然,理论上在 debug 阶段所有发现的问题也都该在 release 之前解决掉,否则就没有办法显得逼(ku)格(bi)满满了。

这货真的有这么好用?机智的我还是决定写个 demo 跑一下试试:

接入步骤

build.gradle

因为不想让这样的检查在正式给用户的 release 版本中也进行,所以在 dependencies 里添加

1
2
3
4
5
6
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:support-v13:+'
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}

接下来,在你的应用里写一个自定义 Application ,并在其中“安装” RefWatcher

1
2
3
4
5
6
7
8
9
10
public class AppApplication extends Application {

    private RefWatcher mRefWatcher;

    @Override
    public void onCreate() {
        super.onCreate();
        mRefWatcher = LeakCanary.install(this);
    }
}

记得把它作为 android:name 配到 AndroidManifest.xmlApplication 节点下。

大功告成,就是这么简单。。。

Read on →
Comments

本文由 林申 译自 Getting Java Event Notification Right,并由 唐尤华 校稿,首次发布在 ImportNew转载请务必注明出处

很多情况下你会定义一类事件,然后对其进行管理。然而,处理不当就会遇到 ConcurrentModificationException。本文通过示例介绍了使用 Java 事件通知(Event Notification)需要注意的一些细节。

通过实现观察者模式来提供 Java 事件通知(Java event notification)似乎不是件什么难事儿,但这过程中也很容易就掉进一些陷阱。本文介绍了我自己在各种情形下,不小心制造的一些常见错误。

Java 事件通知

让我们从一个最简单的 Java Bean 开始,它叫 StateHolder,里面封装了一个私有的 int 型属性 state 和常见的访问方法:

1
2
3
4
5
6
7
8
9
10
11
12
public class StateHolder {

  private int state;

  public int getState() {
    return state;
  }

  public void setState( int state ) {
    this.state = state;
  }
}

现在假设我们决定要 Java bean 给已注册的观察者广播一条 状态已改变 事件。小菜一碟!!!定义一个最简单的事件和监听器简直撸起袖子就来……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// change event to broadcast
public class StateEvent {

  public final int oldState;
  public final int newState;

  StateEvent( int oldState, int newState ) {
    this.oldState = oldState;
    this.newState = newState;
  }
}

// observer interface
public interface StateListener {
  void stateChanged( StateEvent event );
}

…接下来我们需要在 StateHolder 的实例里注册 StatListeners

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StateHolder {

  private final Set<StateListener> listeners = new HashSet<>();

  [...]

  public void addStateListener( StateListener listener ) {
    listeners.add( listener );
  }

  public void removeStateListener( StateListener listener ) {
    listeners.remove( listener );
  }
}

…最后一个要点,需要调整一下 StateHolder#setState 这个方法,来确保每次状态有变时发出的通知,都代表这个状态真的相对于上次产生变化了:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void setState( int state ) {
  int oldState = this.state;
  this.state = state;
  if( oldState != state ) {
    broadcast( new StateEvent( oldState, state ) );
  }
}

private void broadcast( StateEvent stateEvent ) {
  for( StateListener listener : listeners ) {
    listener.stateChanged( stateEvent );
  }
}

搞定了!要的就是这些。为了显得专(zhuang)业(bi)一点,我们可能还甚至为此实现了测试驱动,并为严密的代码覆盖率和那根表示测试通过的小绿条而洋洋自得。而且不管怎么样,这不就是我从网上那些教程里面学来的写法吗?

那么问题来了:这个解决办法是有缺陷的。。。

Read on →
Comments

明天回公司了,关电脑之前写点东西。博客似乎是好久没更新了,正好也总结一下。

//EVENT 2014

  • 2014.01 考研失败 (意料之中,虽然想来觉得浪费了时间,但是小妞劝我还是当经历吧,要是没有这段经历倒也真的可能会后悔)
  • 2014.02 ~ 2014.03 苦逼做东西找实习找工作
  • 2014.04 很幸运像魅族投出的人生第一份简历有了答复,当然也顺利拿到了实习 Offer
  • 2014.04.13 扬中 –> 珠海,开始了我的魅族工程师生涯
  • 2014.05 半个月多一点时间,在 Flyme 里面留下了第一款产品——系统升级
  • 2014.06 四年大学时光就此告别,我毕业了
  • 2014.06.09 班级聚餐,一班人喝得伶仃大醉,第一次向她说出了藏在心里好久的话
  • 2014.06.10 毕业回家,整理了全部情绪,准备 11 号回公司
  • 2014.06.13 谢谢小妞,好巧,我也喜欢你
  • 2014.06.23 毕业典礼,我在珠海,缺席了学生时代最后这场毕业典礼
  • 2014.07 ~ 2014.09 忙碌+收获最多的 3 个月,第一次完完整整看着一个原生的 Android 4.4.2 系统在大家的努力下,一步步演变成了如今的 Flyme 4。很高兴再次负责了 Flyme 4 中的“系统升级”项目,全新的一级界面和动画效果,最后完成的时候自己也不免感慨
  • 2014.09.02 MX4 正式上市。第一次感受到要把自己做的一个东西交到千万级的用户群手中,就像一个等待检验成果的孩子一般,既紧张又激动
  • 2014.10 ~ 2014.12 系统升级大致进入稳定维护期,抽身进了应用中心 & 游戏中心项目组,开发了其中的部分功能

这一年无论是在技术能力还是在工作交流能力上,自我感觉都有了很大的提升。技术方面本来一直准备抽空用一篇文章总结一下的,没想到也就这么一天天压到了现在都没弄。 还是感觉不管怎么样一定要注重技术的积累与总结,加上我有这么好的一个博客平台,所以今年我会努力把博客重新运作起来。

//TODO 2015

Wish List

  1. 买一部微单,好好学习摄影。之所以不买单反是因为真的觉得单反好重又累赘。另外,把女朋友拍漂亮似乎才是正经事!
  2. 6 月过后重新换一处地方住。最好要能看到海,因为小妞以后来的话会喜欢。要有一面可以用来布置的墙,可以挂一个电视下班以后看看电影。
  3. 如果预算充足,想买一套好一点的桌面音箱,还是好喜欢音乐,没办法。

Tech

  1. 学习更多 Android Lollipop 中新增的 API 用法
  2. 每月分析至少一款开源项目的源代码,或者翻译一篇国外网站的技术文章,后期我打算也开始向一些 IT 技术网站投稿做译者
  3. 好好学习设计模式, 尝试将它们的思想渗透进我的代码
  4. 让我的 Apps 更流畅,更顺滑
  5. 有可能的话学习一门新语言。比如 QML 或者 HTML 5,可以用来开发 Ubuntu Phone 的 App。
  6. 保持对新技术的热情,尽可能多去看一些“轮子”的实现方式,取人之长,补己之短

Life

  1. 把爸妈接过来看我一次
  2. 和小妞去海南旅游
  3. 我知道这一年会有很多操蛋的事情等着我,其实准确说每年都会有。所以但愿我能心平气和地一件件应付过来吧
Mac, OS, X
Comments

折腾了好几天总算搞定了 Mac OS 下 Octopress 的搭建。我使用的版本是 Yosemite。

要在 Mac OS 下搭建 Octopress ,需要以下几个步骤。当前,建议读者还是要有一点 Linux 基础,最好之前用过一款 Linux 发行版。

1.包管理软件

熟悉 Ubuntu 的同学可能知道,在 Ubuntu 下面你可以很轻松地通过 apt-get install 来得到你想要的包,如果没有的话还可以通过新立得来查找。而对于 Mac OS X 而言,苹果默认是不带这样一个包管理软件的,不过没关系,我们有 Homebrew 神器,你可以很简单地通过 ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 命令来获取它。安装完成后请记得 vim 一下你的 bash.profile ,增加 export PATH=/usr/local/bin:$PATH,再 source 一下让它生效。

成功安装后,你甚至就可以把它想象成 apt-get 命令,比如你可以执行 brew install wget 来完成你想要的包安装。

2.Gcc 编译环境

gcc 编译环境是为了安装 ruby 时编译作准备的,高大上的 XCode 里面已经带了一套很好的编译环境,所以这一步你需要去装好 Xcode,并运行一次,确保安装了对应的组件。

3. Ruby 1.9.3

Octopress 强制要求了必须用 Ruby 1.9.3 构建,而 Yosemite 自带的 Ruby 版本已经达到了 2.0.0。那么问题就来了——“如何在不破坏系统自带的 ruby 环境的前提下,切换到 Ruby 1.9.3 ?”方法就是使用 rbenv,执行如下命令即可:

1
2
3
4
5
6
7
8
9
*安装 rbenv
brew update
brew install rbenv
brew install ruby-build

*使用 rbenv 安装 1.9.3
rbenv install 1.9.3-p0
rbenv local 1.9.3-p0
rbenv rehash

回过头去看一下,其实正是 rbenv local 1.9.3-p0 命令保证了你在安装 1.9.3 的 ruby 同时不会破坏原有系统变量。此时执行 ruby -v,如果看到版本已经切到了 1.9.3,就证明大功告成。否则请重新执行:

1
2
rbenv local 1.9.3-p0
rbenv rehash`

如果执行完之后还是不行,可能是因为你的 rbenv 压根没生效。请vim一下你的bash.profile,增加export RBENV_ROOT=/usr/local/var/rbenv,再source一下让它生效。

4.最后几步

可能看到这里你已经醉了,但很遗憾,一切已经结束,接下来的照着官方教程走吧,Good Luck!

Copyright © 2014 - 2018 - linshen - @ . +