Android Service生命周期回顾

Service 经历的生命周期较少,可以分三种情况记录。

第一种,startService / stopService

1

注意点:

  1. 第一次 startService 会触发 onCreate 和 onStartCommand,以后在服务运行过程中,每次 startService 都只会触发 onStartCommand
  2. 不论 startService 多少次,stopService 一次就会停止服务

第二种,bindService / unbindService

图示同上。

注意点:

  1. 第一次 bindService 会触发 onCreate 和 onBind,以后在服务运行过程中,每次 bindService 都不会触发任何回调
  2. bindService 多少次,就对应多少次的 unbindService, bindService / unbindService 操作顺序以及次数对应的话, 就会停止当前服务

第三种,前面两种交错

1

注意点:

  1. 只有当 startService / stopService 与 bindService / unbindService 都满足条件时,才会停止服务

onBind 的作用

如果我们将 “开始有 client 发起 bindService”到“所有 client 都调用 unbindService”中间的过程视为一个“轮回”,
那么同一个服务的同一个运行生命周期(从 onCrete 到 onDestroy 之间)内,就有可能发生多次“轮回”。

onBind 主要用来产生一个 Binder,只需要知道同一个服务的 Binder 只会有一个,onBind 只会在第一个“轮回”调用一次,主要负责则 “Create Binder”。

onRebind

onBind 与 onRebind 不会在一个“轮回”中同时被调用。

如果 bindService 操作触发了 onCreate 回调,那么,同时也会触发 onBind。

onUnbind 的返回值

onUnbind 的返回值只会在服务运行期间从第二个“轮回”开始发生作用。
那如何才能出现第二个“轮回”?肯定只有在调用了 startService 的情况下,服务保持运行期间,才会有第二,第三个“轮回”。
否则,每一个“轮回”就都是第二种情况了。举个例子说明下:

第一个例子,onUnbind 返回 false

  1. 调用 startService。触发 onCreate 与 onStartCommand
  2. 调用 bindService。触发 onBind
  3. 调用 unbindService 触发 unbindService
  4. 调用 bindService。无回调
  5. 调用 unbindService 无回调
  6. 调用 stopService。触发 onDestroy

第二种例子,onUnbind 返回 true

  1. 调用 startService。触发 onCreate 与 onStartCommand
  2. 调用 bindService。触发 onBind
  3. 调用 unbindService 触发 unbindService
  4. 调用 bindService。触发 onRebind
  5. 调用 unbindService 触发 onUnbind
  6. 调用 stopService。触发 onDestroy

几个问题

bindService 与什么有关?

ServiceConnection 对象是 bind 操作的标识符。

bindService / unbindService 都需要与同一个 ServiceConnection 对象配对。
同一个 ServiceConnection 对象只会触发一次绑定。只有在 unbindService 后才能重新绑定。

如何知道绑定了多少个 client

Service 本身不能知道有多少个 client,可以在每次触发 ServiceConnection 的时候增加一次静态计数。

Service 如何指导是否有新的 client 绑定?

如果不是在“轮回”的开始,Service 无法知道有新的 client 绑定,只能 client 主动通知给 Service。

Android 前后台切换监听

Android 本身并有提供这样的监听,所以就只能走偏门了。

首先,需要定义一下,什么叫“前台”,什么叫“后台”。本文定义如下:

前台

Activity 处在 FOREGROUND 优先级

后台

App进程没有停止,除去在“前台”的所有情况

所以,退到后台的方式太多了,大致有:

  1. 按Home键
  2. 按“最近任务”键
  3. 从通知栏启动其他应用
  4. 从应用内部启动其他应用
  5. 关掉屏幕

既然是监听变化,所以肯定是在相关生命周期的回调中来进行处理。ActivityA 启动 ActivityB 的生命周期方法调用顺序如下:

ActivityA#onPause -> ActivityB#onStart -> ActivityB#onResume -> ActivityA#onStop

从 ActivityB 回退到 ActivityA 的生命周期方法调用顺序如下:

ActivityB#onPause -> ActivityA#onStart -> ActivityA#onResume -> ActivityB#onStop -> ActivityB#onDestroy

一个思路就是通过统计当前活动的 Activity 数目来计算。

在 Activity#onStart 中来检测前一个状态是否是“后台”,如果是,则触发“切换到前台”事件,并将活动 Activity 数目加1。
在 Activity#onStop 中并将活动 Activity 数目减1。如果活动的 Activity 数目等于0,
就认为当前应用处于“后台”状态, 并触发“切换到后台”事件。

所以,一个初步方案大致是,实现一个基类 BaseActivity,并重写以下 onStart 和 onStop 回调方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static int compatStartCount = 0;
private static boolean isCompatForeground = true;

@Override
public void onStart() {
super.onStart();
compatStartCount++;
if (!isCompatForeground) {
isCompatForeground = true;
onBackgroundToForeground(activity);
}
}

@Override
public void onStop() {
super.onStop();
compatStartCount--;
if (compatStartCount == 0) {
isCompatForeground = false;
onForegroundToBackground(activity);
}
}

开始趟坑

  1. 在关掉/点亮屏幕的情况下,android3 之前不会触发 onStart 和 onStop 回调。只会触发 onPause 和 onStop。所以以上代码失效。
    按理来说,这应该是 Android 的含糊之处,onStop 的触发时机定义如下:

     Called when you are no longer visible to the user
    

按理来说,屏幕关闭的时候也符合条件,但是 android3 之前并未按照如此定义而来。所以需要 hack 一下:

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
32
33
34
35
36
37
38
39
40
private static boolean isCompatLockStop = false;

private static boolean isStandard() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
}

public static boolean isInteractive(Context context) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (Build.VERSION.SDK_INT >= VERSION_CODES.KITKAT_WATCH) {
return pm.isInteractive();
} else {
return pm.isScreenOn();
}
}

@Override
protected void onResume() {
super.onResume();
if (isStandard()) {
//np
} else {
if (isCompatLockStop) {
isCompatLockStop = false;
onStart();
}
}
}

@Override
protected void onPause() {
super.onPause();
if (isStandard()) {
//np
} else {
if (!isInteractive(activity)) { //锁屏触发
isCompatLockStop = true;
onStop();
}
}
}

对于 android3 之前的系统,我们在 onPause 中处理 锁屏问题,如果 onPause 被触发的时候,手机处于 “非交互” 状态,就认为是按下了电源键(或者其他锁屏方式),触发“后台”状态。
在 onResume 中判断如果是从锁屏界面恢复,则回到“前台”状态。

存在问题:点亮屏幕的时候就被认为回到“前台”状态,这个暂未找到好的方法避免。

  1. 快速锁屏/点亮的情况下,会多次触发生命周期回调。

这个问题在 nexus 5 (Android 6.0) 上稳定重现,重现步骤:关掉屏幕后快速点亮再快速关闭。本来预期的表现应该是:

onPause
onStop

但是实际的表现如下:

onPause
onStop
onStart
onResume
onPause
onStop

所以,最终结果就是:

前台 -> 后台
后台 -> 前台 
前台 -> 后台

多了一个周期。

这个问题我还没有解决。尝试过解决办法有:

1.在 onStart 中判断 inKeyguardRestrictedInputMode 状态:

1
2
3
4
public static boolean inKeyguardRestrictedInputMode(Context context) {
KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
return keyguardManager.inKeyguardRestrictedInputMode();
}

在锁屏状态下就不触发计数。但是这个判断根本不可信,在快速切换的情况下,在非锁屏情况下返回 true 的几率也比较大。

  1. 网上有一种判断应用是否在前台的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean isAppOnForeground() {

ActivityManager activityManager = (ActivityManager) getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
String packageName = getApplicationContext().getPackageName();

List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager
.getRunningAppProcesses();
if (appProcesses == null)
return false;

for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
// The name of the process that this object is associated with.
if (appProcess.processName.equals(packageName)
&& appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
return true;
}
}
return false;
}

但是这种测试方法并不靠谱,比如在魅族手机上,锁屏状态下也是返回 true。

  1. 监听 ACTION_USER_PRESENT, ACTION_SCREEN_ON, ACTION_SCREEN_OFF 等事件。

回调事件比较晚,在生命周期中已经跑完好几圈之后,广播才接收到。而且在各个生命周期内无法判断是否因为锁屏导致的周期变化。

顺便说一下, ACTION_USER_PRESENT 和 ACTION_SCREEN_ON 存在下面几种情况:

  1. 只触发 ACTION_SCREEN_ON
  2. 先触发 ACTION_SCREEN_ON,再触发 ACTION_USER_PRESENT (电源键,然后解锁)
  3. 先触发 ACTION_USER_PRESENT,再触发 ACTION_SCREEN_ON (指纹解锁)

这样的话,无法通过简单的计数来判断,需要加入 hashCode 来处理。

所以最终这种快速切换的情况被我忽略了,因为无非就是多了一个周期而已。

不完美实现

对于 API>=14 的系统上,可以直接增加 ActivityLifecycleCallbacks 监听,这样可以省去定义 BaseActivity 的步骤,减少侵入性。

所以在 API>=14 的系统上,直接使用 ActivityLifecycleCallbacks, 如果需要兼容 2.3,那么还是需要抽象出 BaseActivity。

演示代码地址 : https://github.com/xesam/AppMonitor

Python 包小结

如何导入本地库

为了能够导入本地库,我们需要将本地库所在的目录添加到PYTHONPATH环境变量中。PYTHONPATH 是一个包含多个目录的列表,Pyton解释器会在这些目录中查找要导入的模块。

设置临时PYTHONPATH

通过在Python脚本中设置PYTHONPATH环境变量来临时添加本地库的路径。

1
2
import sys
sys.path.append('/path/to/mylib')

设置永久PYTHONPATH

在Linux或Mac系统上,我们可以通过在编辑 ~/.bashrc (具体的文件需要参照具体的系统)文件中添加如下行来设置永久的PYTHONPATH:

1
export PYTHONPATH="/path/to/mylib:$PYTHONPATH"

在Windows系统上,我们可以通过以下步骤设置永久的PYTHONPATH:

  1. 右键点击“我的电脑”(注意不是磁盘驱动符),选择”属性”;
  2. 点击”高级系统设置”;
  3. 点击“环境变量”;
  4. 在”系统变量”中,找到名为”PYTHONPATH”的变量,如果不存在则创建;
  5. 编辑”PYTHONPATH”变量,将本地库的路径添加到其中,多个路径之间用分号(;)分隔。
  6. 点击“确定”关闭窗口。
  7. 如果没生效,可能需要重启你的命令终端。

数字货币与现金战争

最近有两个新闻总是引人关注:

第一个事件:Apple pay 入华,支付大战愈演愈烈。

不过是Apple pay入场,媒体就叫嚣着“狼来了”,替支付宝们着急。没错,他们都是各种 pay:

银联是 union pay
支付宝是 ali pay
微信支付是 tencent pay
applepay就更露骨了,apple pay

在支付占有率上他们是竞争对手。不过,这对民众影响能有多大呢?无非是多了一种选择而已,需要关心这件事本身吗?

或许,我们更应该关注背后的变化。因为在更大的框架内,所有的 pay 都是同一阵营的队友,他们都有一个共同任务:普及货币的数字化。

第二个事件:央行放信号:数字货币不远了

注意区分“货币的数字化”和”数字货币”。先来看看什么是数字货币,“货币”本身的概念就很难理解,“数字货币”就更难理解了。结合实际的例子来理解比较直观 —— 比特币就是一种数字货币。

央行2014年就开始研究“区块链”技术,“区块链”技术起源于比特币,所谓无利不起早,干这事肯定有大利可图。

Read More

pip 记录

常用的命令

常用命令

install {package name} #安装包
download {package name} #下载包   
uninstall {package name} #卸载包                 
freeze #按照 requirements 格式导出已安装的包               
list #查看已安装包列表                    
show {package name} #查看已安装的包信息                     
check {package name} #检查已安装的包依赖                
config                      Manage local and global configuration.
search                      Search PyPI for packages.
wheel                       Build wheels from your requirements.

指定镜像

1
install {package name} -i {mirror site}  

常用镜像:

清华:https://pypi.tuna.tsinghua.edu.cn/simple
阿里云:http://mirrors.aliyun.com/pypi/simple/
中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/
华中理工大学:http://pypi.hustunique.com/
山东理工大学:http://pypi.sdutlinux.org/
豆瓣:http://pypi.douban.com/simple/

升级 pip

1
pip install --upgrade pip

升级库

1
pip install --upgrade {package name}

打印线程状态

其实这是一个面试题。

线程状态

按照 Thread.State 的定义,一个线程可能处在以下六种状态之一 :

  1. NEW

    线程对象被创建,但是还未开始。即还没有调用过 Thread.start

  2. RUNNABLE

    线程正在运行,或者等待分配运行资源

  3. BLOCKED

    等待进入同步块(monitor lock),注意与Lock对象的区别。
    或者在调用 Object.wait 之后再次进入同步的块/方法(意思就是再次进入的这个时候依旧没有获取到锁)。

  4. WAITING

    等待其他线程的操作,触发进入此状态是操作:

     Object.wait with no timeout
     Thread.join with no timeout
     LockSupport.park //比如condition.await()
    
  5. TIMED_WAITING

    等待一定的时间

     Thread.sleep
     Object.wait with timeout
     Thread.join with timeout
     LockSupport.parkNanos
     LockSupport.parkUntil
    
  6. TERMINATED

    线程已经执行完成

Read More

venv 常用

venv 主要用来管理 Python 的执行环境,简单说来就是管理包的版本,是无法控制 Phthon 的版本的。比如想在 Python3.6 与 Python3.8之间切换,靠现阶段的 venv 是做不到的,可能需要借助 conda、pyenv 之类的工具。

从 Python 3.4 开始,pip 与 venv 模块会在安装 Python 时一起默认安装。

创建虚拟环境

1
python -m venv {project name}

如果是当前目录,直接使用

1
python -m venv .

就行了。此时在 {project name} 文件夹下创建下述文件夹:

Include
Lib
Scripts(也有可能是 bin 之类的名称)
pyvenv.cfg

用 venv 工具创建出的虚拟环境,初始只装有 pip 与 setuptools 模块,除此之外,没有预装其他的软件包。

激活虚拟环境

1
./Scripts/activate

不同的操作系统脚本文件可能不一致,比如 windows 下面是 activate.bat 。
如果在 linux 环境下,可以使用:

1
source ./Scripts/activate

执行脚本之后,就会激活虚拟环境,此时执行:

1
which python 

可以查看相关的 python 或者 pip 信息。

安装依赖

在激活的终端里面,可以使用 pip 安装对应的模块。比如:

1
pip install json5

通过

1
pip show json5

可以验证 json5 的安装路径以及其他信息。

退出虚拟环境

通过与 activate 相反的 deactivate 操作,即可回到默认环境。

重建依赖

使用 pip freeze 命令把当前环境所依赖的包明确地保存到一份外部文件中(按照惯例,这个文件命名为 requirements.txt)。

1
python3 -m pip freeze > requirements.txt

或者:

1
pip freeze > requirements.txt

就会在执行目录生成一份 requirements.txt 文件,内容如下:

1
json5==0.9.6

如果对版本规则比较熟悉,也可以手写这份文件。

假设要用 {project name} 环境之中的配置来构建另外一套相似虚拟环境。我们先用venv工具把那套环境创建出来,然后用activate激活它。具体命令可以参见 上文。

我们可以执行python3 -m pip install命令,把刚才用 python3 -m pip freeze 所保存的 requirements.txt 文件通过 -r 选项传给它,这样就能够将那份文件所记录的软件包安装到这套环境里面了。

1
python3 -m pip install -r /a/b/c/d/requirements.txt

或者:

1
pip install -r requirements.txt

哈希值检查

要实现哈希检查模式,只需在需求文件中写入带有包名的摘要:

1
json5==0.9.6 --hash=sha256:{hash digest}

支持的哈希算法包括 md5、sha1、sha224、sha224、sha384、sha256 和 sha512。

需要注意的问题

  1. Python本身的版本并不包含在requirements.txt之中,所以必须单独管理。
  2. 虚拟环境有个很容易出错的地方,就是不能直接把它移动到其他路径下面,因为它里面的一些命令(例如python3)所指向的位置都是固定写好的,其中用到了这套环境的安装路径,假如移动到别处,那么这些路径就会失效。

参考《Effective Python》,纯做记录用。

conda 常用

conda是一款软件管理软件,相当于应用商店,在使用 Python 的过程中,可以用来管理 Python 版本。

常用的命令

1. 查看安装了哪些包

1
conda list

2. 查看当前存在哪些虚拟环境

1
2
conda env list 
conda info -e

3. 检查更新当前conda

1
conda update conda

4. 创建虚拟环境

1
2
conda create -n your_env_name
conda create -n your_env_name python==x.x #指定 python 版本

create 命令创建 python 版本为 x.x,名字为 your_env_name 的虚拟环境。your_env_name 文件可以在 conda 安装目录的 envs 文件下找到。

5. 激活或者切换虚拟环境

打开命令行,输入 python –version 检查当前 python 版本。

1
2
3
Linux:  source activate your_env_nam
Windows: activate your_env_name
Mac: conda activate your_env_name

6. 在虚拟环境中安装额外的包

1
2
conda install -n your_env_name [package]
conda install -n your_env_name [package=x.y.z] # 指定 package 版本

7. 关闭虚拟环境(即从当前环境退出返回使用PATH环境中的默认python版本)

1
deactivate env_name

或者activate root切回root环境,Linux下:source deactivate

8. 删除虚拟环境

1
conda env remove -n your_env_name

9. 删除环境中的某个包

1
conda remove --name $your_env_name  $package_name

10. 重命名环境名称

在 Conda 4.14 版本之前,是没有直接内置命令的,只能删除原环境然后重新创建。从 Conda 4.14 版本开始,你可以使用以下命令来改变环境名称:

1
conda rename -n old_env_name new_env_name

虽然这个命令在内部的实现方式有点尴尬,但是起码算是直接支持了。

镜像配置

查看 conda 的配置:

1
conda config --show

查看或者直接查看 channels:

1
conda config --show channels

添加镜像

比如添加中科大的源:

1
2
conda config --add channels https://mirrors.ustc.edu.cn/anaconda/pkgs/free/
conda config --set show_channel_urls yes

conda config –set show_channel_urls yes的意思是从channel中安装包时显示channel的 url,这样在搜索或者安装的时候可知来源

移除镜像

1
2
conda config --remove channels defaults
conda config --remove channels https://mirrors.ustc.edu.cn/anaconda/pkgs/free/

Jupyter

如果 conda 环境不出现在 jupyter 选项中,可以按照如下方法处理:

1
nb_conda_kernels

 should be installed in the environment from which you run Jupyter Notebook or JupyterLab. This might be your base conda environment, but it need not be. For instance, if the environment notebook_env contains the notebook package, then you would run

1
conda install -n notebook_env nb_conda_kernels

Any other environments you wish to access in your notebooks must have an appropriate kernel package installed. For instance, to access a Python environment, it must have the ipykernel package; e.g.

1
conda install -n python_env ipykernel

To utilize an R environment, it must have the r-irkernel package; e.g.

1
conda install -n r_env r-irkernel

For other languages, their corresponding kernels must be installed.

参考

  1. https://stackoverflow.com/questions/39604271/conda-environments-not-showing-up-in-jupyter-notebook

linux 汇编错误记录

insight 安装

1
variable 'errcount' set but not used

解决办法:

1
./configure --disable-werror

参考阅读: http://www.tuicool.com/articles/VRJf6v

32/64 位问题

编译一个简单的汇编文件,然后链接出现的时候这个问题。

1
2
$nasm -f elf -g -F stabs easy.asm
$ld -o easy easy.o

错误结果:

ld: i386 architecture of input file `eatsyscall.o' is incompatible with i386:x86-64 output

解决方案:

指定 64 位

1
2
$nasm -f elf64 -g -F stabs easy.asm
$ld -o easy easy.o