compile
早就已经被弃用了,取而代之的则是 implementation
跟 api
。之前一直以为两者的区别在于是否可以传递依赖。implementation
的禁止“传递依赖”只是指编译时的,而被传递的依赖项仍然会被打到最终的包里,在运行时提供。这么做主要还是为了提升编译效率:进行依赖隔离,当单个模块发生变更之后,对其没有直接依赖的模块可以不用重新编译。Gradle 里真正禁止”传递依赖“(编译时 + 运行时):
针对单个依赖 ModuleDependency.setTransitive(boolean):
1 | dependencies { |
针对 configuration Configuration.setTransitive(boolean):
1 | configurations.all { |
参考
]]>Java 中 lambda 的变量捕获是值的捕获,也就是直接把捕获的变量拷贝一份,这意味着对于栈上的变量,
所以 Java 干脆禁止这样做,要求它们必须是 final 或者 effectively final(即未加 final 修饰,但在 lambda 声明之后无赋值操作)的。
当然如果真的需要改变原始的值的话,IDE 会提示你可以用 Atomic 类或者一个单元素的数组包装一下,曲线救国。这样捕获时是将 Atomic 对象或者数组变量拷贝一份,而他们指向的数据都是堆上的同一份数据,变相实现引用捕获。
随便写个 demo 反编译下可以发现如果你在 lambda 中对捕获的变量有赋值操作,在 JVM 平台上编译器就会自动帮你把变量包装成一个 Ref 对象,原理跟上面提到的 Java 中的解决方式本质上是一样的。参考 Ref.java。
]]>Apply Changes
功能。cmd package install-create
创建 session,install-write
写入 instructions & patch 数据。installer
执行 swapinstaller
进程开启一个 socket servercmd activity attach-agent <PROCESS> <FILE>
attach agent 到指定进程installer
读取数据callbacks.ClassFileLoadHook = Agent_ClassFileLoadHook
RetransformClasses
,触发前面设置的 ClassFileLoadHook
:Hook ActivityThread.handleDispatchPackageBroadcast()
,更新资源。1 | agent/ JVMTI agent 实现,编译成 so 文件,拷贝到 app 的 data 目录下,通过 JVMTI attach 到对应的进程上。 |
核心逻辑可以参考 deployer/src/main/java/com/android/tools/deployer/Deployer.java 的 swap
方法,十分清晰。
1 | // Get the list of files from the local apks |
char, byte, short, int, Character, Byte, Short, Integer, String
类型。今天读 flutter 代码的时候无意间发现 switch 语句对 String 的支持是通过 hashCode
配合 equals
实现的 😂。
鉴于 Blog 好久没更新了,特地来水一篇。不多说了,看下代码。
这是源码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
switch (method.getName()) {
case "addView":
addView(args);
return null;
case "removeView":
removeView(args);
return null;
case "updateViewLayout":
updateViewLayout(args);
return null;
}
try {
return method.invoke(mDelegate, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
这是反编译过的。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
38public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String var4 = method.getName();
byte var5 = -1;
switch(var4.hashCode()) {
case -1148522778:
if (var4.equals("addView")) {
var5 = 0;
}
break;
case 931413976:
if (var4.equals("updateViewLayout")) {
var5 = 2;
}
break;
case 1098630473:
if (var4.equals("removeView")) {
var5 = 1;
}
}
switch(var5) {
case 0:
this.addView(args);
return null;
case 1:
this.removeView(args);
return null;
case 2:
this.updateViewLayout(args);
return null;
default:
try {
return method.invoke(this.mDelegate, args);
} catch (InvocationTargetException var6) {
throw var6.getCause();
}
}
}
View 有四个主要的构造方法,第四个需要在 API 21 以上,一般而言我们都会实现前三个。
public View(Context context)
public View(Context context, @Nullable AttributeSet attrs)
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
public View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
下面来看一种常见的写法。
1 | public Circle(Context context) { |
在前面几个构造方法中调用 this, 再在后面的构造方法中统一调用 super。这种写法的确很常见,在各种开源库甚至 Android 自己的一些 View 都采用了这种写法。然而如果你对此不假思索地照抄下来,还是可能会遇到坑的。
先来看下这些构造方法的用途。
new View(context)
)用的。View 的构造方法是用来初始化 View 属性(比如背景,边距,id,文字大小,文字颜色等等)的,那么这些初始值从哪里来以及如何获取就是我们首先要关心的。根据我们的经验,这些值通常是在布局文件,或者 Style 中指定。通过查看 View 的源码我们可以发现这些值的查找是通过 Context 的 public final TypedArray obtainStyledAttributes(AttributeSet set, @StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)
方法完成的,这个方法中的参数对应于与 View 构造方法中的参数。
让我们来看一下这几个参数。
AttributeSet attrs
是我们在布局文件里面为该 View 定义的属性集,比如为一个 TextView 指定 android:textSize="16sp" android:textColor: "#666"
。值得注意的是我们可以在布局文件中为 View 指定 style
属性,引用一个 Style 资源,这样如果相关属性没有在布局文件中没有直接指定的话,就会去 Style 中查找。
以 def 命名的参数是指 default。int defStyleAttr
其实是一个 attr 资源,它对应于当前 Theme 中的一项属性,其值是对一个 Style 的引用。那么有了这个参数,如果要查找的属性通过 AttributeSet attrs
找不到,我们就可以去当前 Theme 中由 defStyleAttr 引用的 Style 中查找。
举个例子。
1 | <style name="AppTheme" parent="@style/Theme.AppCompat.Light.NoActionBar"> |
我们可以通过在 Theme 中定义 buttonStyle
属性来更改 Button 的默认样式。这里的 buttonStyle
就是一个 attr 资源 <attr name="buttonStyle" format="reference" />
,它的值就是对我们自己定义的 Style MyButtonStyle
的引用。这样,如果我们没有在布局文件里给 Button 定义 android:textSize
的话,它就会去 MyButtonStyle 中查找。
那么如果我们没有指定 defStyleAttr 参数(传 0 进去),或者我们的 Theme 并没有定义 defStyleAttr 呢? 那就要看最后一个参数了。
最后一个参数 int defStyleRes
显然是一个 Style 资源了,在前面的地方都相关属性都找不到的情况下,就可以从这个指定的 Style 中去查找相关属性。
当然,还有最后一个查找位置就是我们直接在 Theme 中定义的属性了。
那么了解了这些基础之后,让我们再来回顾下上文提到的写法。
考虑这样一种情况,我们要继承的父类 View 同样实现了以上几个构造方法,而且用到了 defStyleAttr
和 defStyleRes
属性。
比如 Button 的构造方法是这样写的。
1 | public Button(Context context) { |
它指定了会从当前 Theme 中的 buttonStyle
中去查找默认样式,而如果按照上文中的写法,我们继承了 Button 类又刚好在构造方法中去调用 this(…) 构造方法,最后才调用一下 super,那么 Button 的构造方法接收到的 defStyleAttr
也就不是 com.android.internal.R.attr.buttonStyle
,而是 0,这样就导致了我们的 Button 无法从 Theme 中获取默认样式。这种情况显然不是我们所期望的。
其实后面两个构造方法要传 int defStyleAttr
与 int defStyleRes
进去,也就意味着会改变默认属性的查找位置,所以只有我们需要覆盖父类的默认样式时才应该去调用这两个构造方法,否则我们最好调用第二个构造方法 super(context, attrs)
。
本文将解析胶水 React Redux 背后的 magic。
Redux 官方文档给出了使用 React Redux 来给 React 集成 Redux 的用法。
根据文档,整体思想是由一个顶级的容器组件来持有和管理 state,子组件作为展示型组件,接受父组件传递的 props 并 render 出来。当然,这些 props 最终来源于容器组件所持有的的全局 state。
首先看官方的示例代码
1 | render( |
Provider 的角色就是上文中的容器组件。这里将 Provider 作为顶级组件,并将 store 作为 props 传递给它。
再来看对子组件的包装
1 | import { connect } from 'react-redux' |
对子组件的包装通过 connect
函数来实现,先传递 mapStateToProps
,mapDispatchToProps
给 connect
得到一个新的函数,再把子组件传进去得到包装后的组件。这个包装出来的组件就会在合适的时机通过某种魔法调用 mapStateToProps
,mapDispatchToProps
并将结果作为 props 传递给子组件,这样子组件就可以访问全局 state,或者 dispatch action 了。
其实这里 connect
相关的部分就是高阶组件(Higher Order Component)的角色,它是一个函数,接受组件作为参数,并返回包装后的组件。我们通过 ReactDevTools 可以探查到生成的组件是直接包在我们原来的组件外面的,比如我们有一个叫 Home 的组件,返回的组件树如下。
1 | <Connect(Home)> |
那么根据 React Redux 的用法,我们主要的关注点就是两个:Provider
组件与 connect
函数。
Provider
组件由 createProvider
函数创建。
1 | // Provider.js |
那么这里的重点就是 getChildContext
方法,它的返回值包含了对 store 的引用。Provider
就是通过 getChildContext
来向组件树提供 context(store)的。
关于 context,它的作用是向下传递数据,与 props 相比,context 不需要一级一级地手动传下去,而下面的任何组件都能够通过 this.context
直接访问 context。
官方文档对 context 有更加详细的解释(Context)。
1 | // createConnect with default args builds the 'official' connect behavior. Calling it with |
connect
通过 createConnect()
创建出来,connect
的核心实现在 connectHOC
(即connectAdvanced
)里。
connectAdvanced
返回了函数 function wrapWithConnect(WrappedComponent)
,也就是说这个 WrappedComponent
就是我们自己的组件,而 wrapWithConnect
返回了一个 Connect
组件,它就是最终包装得到的组件。
在 Redux 中,我们要拿到 store 的 state,就应该先去 subscribe 这个 store,然后在 state 更新之后得到通知。我们的组件要想在 state 更新时得到通知,就得靠 Connect
先订阅 store,然后传 props 过来。所以这个 Connect
的实现也是八九不离十了,它应该是先订阅了 store,在 state 更新后再调用 mapStateToProps
和 mapDispatchToProps
,组合之后作为 props 传给子组件。
1 | addExtraProps(props) { |
1 | function makeSelectorStateful(sourceSelector, store) { |
可以看到,Connect
订阅了 store,并在 onStateChange
中令 selector 重新计算 props,之后 setState 触发组件的 render,使子组件的 props 得到更新。
写个论文格式什么的要求还是挺多的,标题/正文用什么字体,字号都有规定。要方便设置格式就要善用 Style。
首先是选择一套与你的需求比较接近的基本主题,可以尽量降低自定义样式的工作量。
然后你就可以按论文要求自定义 Style 了。打开 Styles Pane,就可以看到预定义的各种样式,可以直接将其应用到文中。针对某一个样式,可以直接进行修改,应用该样式的文本都可以随之更新,是不是很像写 CSS?样式还可以继承已有样式,颇有点 OO 的感觉。
一篇小论文还用不着 Citation 这么高级的功能,而且我大清自有国情在此,Word 的这个功能也不够本土化。要添加参考文献,只需要用 Cross-reference 就可以了。首先在知网或其他地方导出参考文献格式,复制到文中,然后使用 Word 给它们添加编号,再在原文引用的地方插入 Cross-reference 时直接选择对应的参考文献条目就好了。
]]>普通权限不会影响到用户的隐私,只要像以往那样在 manifest 中声明即可使用。
危险权限不仅要在 manifest 中声明,而且需要获得用户的批准。虽然可以临时将 target API 设置在 23 以下规避,但一来用户仍然可以在系统设置中强制拒绝授予权限造成应用崩溃,二来也并非长久之计。
系统分组处理危险权限的授予,如果应用已经获得某个组内的某项权限,那么也就默认获得了该组内的其他权限。比如我们已经获得了 READ_CONTACTS
权限,那么相当于 CONTACTS
组内的 WRITE_CONTACTS
和 GET_ACCOUNTS
权限也获得了。
Permission Group | Permissions |
---|---|
CALENDAR | READ_CALENDAR WRITE_CALENDAR |
CAMERA | CAMERA |
CONTACTS | READ_CONTACTS WRITE_CONTACTS GET_ACCOUNTS |
LOCATION | ACCESS_FINE_LOCATION ACCESS_COARSE_LOCATION |
MICROPHONE | RECORD_AUDIO |
PHONE | READ_PHONE_STATE CALL_PHONE READ_CALL_LOG WRITE_CALL_LOG ADD_VOICEMAIL USE_SIP PROCESS_OUTGOING_CALLS |
SENSORS | BODY_SENSORS |
SMS | SEND_SMS RECEIVE_SMS READ_SMS RECEIVE_WAP_PUSH RECEIVE_MMS |
STORAGE | READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE |
与运行时权限相关的 API 主要有以下几个方法。
1 | // Context/ContextCompat |
checkSelfPermission
方法用来检测是否已经获得某项权限,返回结果为PackageManager.PERMISSION_GRANTED
或 PackageManager.PERMISSION_DENIED
。
shouldShowRequestPermissionRationale
表示应用是否应该向用户解释为何需要权限。未请求过该权限时返回 false。
requestPermissions
执行请求权限的实际操作,在调用该方法时,系统弹出对话框让用户选择是否允许获取所请求的权限。
如果用户选择了 Never ask again 并拒绝授予,那么之后调用 requestPermissions
请求该权限直接会被拒绝,shouldShowRequestPermissionRationale
都会返回 false。
onRequestPermissionsResult
方法是 Activity 的一个回调方法,类似于 onActivityResult
,返回权限请求的结果。
1 | if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { |
{}
:闭包1 | // app/build.gradle |
自动装箱的主要作用是方便在值类型和包装类型之间切换,比如可以直接在源代码里将值类型赋值给包装类型或反之,使用 == 来比较值类型和包装类型的值。这里提到是源代码里这样做是因为自动装箱/拆箱特性主要由编译器帮我们完成。
当我们编写以下一段程序时
1 | int a = new Integer(5); |
int a = new Integer(5).intValue();
的代码Integer b = Integer.valueOf(6);
assert(a.intValue() == b);
关于包装类型的缓存
根据 Java 语言规范(JLS8,§5.1.7),包装类型会对部分值做缓存,在像 Integer.valueOf(int)
这样的方法中起作用,所以会出现 Integer a = 5; Integer b = 5; assert(a == b);
的情况。
]]>If the value p being boxed is an integer literal of type int between -128 and 127 inclusive (§3.10.1), or the boolean literal true or false (§3.10.3), or a character literal between ‘\u0000’ and ‘\u007f’ inclusive (§3.10.4), then let a and b be the results of any two boxing conversions of p. It is always the case that a == b.
这次 Android Studio 不但支持传统 NDK 项目的集成,同时也支持使用 Cmake 来组织 NDK 代码。Cmake 项目可以在 Gradle 做如下配置
1 | externalNativeBuild{ |
对于传统的 NDK 项目现在可以在 Gradle 里添加如下配置块
1 | externalNativeBuild{ |
path
Android.mk 文件的路径,可以使用相对路径
targets
用来过滤需要打包进 apk 的库,可以针对不同的 product flavor 做配置。只有这里指定的 targets 才会被打包,若未配置则全部打包
abiFilters
这里最好还是再指定一次,Application.mk 里指定的似乎会被忽略。如果 NDK 项目依赖一些平台相关的库或代码并且只有部分平台的依赖,这时会报找不到文件的错误,其实是没有设置 abiFilters 导致它会去找所有平台的文件
完成配置并同步 Gradle 之后,在该模块的目录下会生成一个 .externalNativeBuild
目录,目录结构为 .externalNativeBuild/ndkBuild/${ProductFlavor}/${abi}/
。
里面是生成的构建脚本,包含三个文件。指定了 Android.mk 及 Application.mk 的位置,ndk-build 命令,编译参数及 toolchains 配置。
并且从生成的脚本中也可以看出这里已经可以根据 Gradle 中的 Debug/Release 模式生成相应的编译命令了。
如此看来 Gradle 采取的是根据我们的配置生成对应的脚本,在外部执行编译脚本,再将结果进行打包的策略,这样便使原来的项目无缝迁移,同时也为 Cmake 这样的组织方式提供了扩展。
另外现在可以针对不同渠道做定制了。可以通过在编译器参数中定义宏的方式将参数传进 JNI 中,再在 JNI 代码中做对应处理。
JNI 开发中 Java 层向下传字符串比较常用的是 JNIEnv 的 GetStringUTFChars 方法将 jstring
转为 const char *
,用完后使用 ReleaseStringUTFChars 方法释放。
1 | const jchar * GetStringChars(JNIEnv *env, jstring string, |
然而使用该方法返回的字符串却并非采用标准 UTF-8 编码,而是Modified UTF-8 Strings,即一种修改过的 UTF-8 编码。
标准的 UTF-8 编码以 8-bit 即一个字节为基本单位,一个字符可以由一到六个字节编码表示,只要留出每个字节的高几位作为标志位就可以表示出该字节的类型,这样就可以判断出其后有几个字节与该字节合在一起表示一个字符。比如最高位为 0
表示该字节单独表示一个字符;最高两位为 10
表示该字节是跟在其他字节后面的,仅包含数据;最高三位为 110
则表示它将与后一个字节共同表示同一字符。
在 Modified UTF-8 Strings 中,U+FFFF
以上编码的字符(比如 Emoji 字符)并没有继续遵循 UTF-8 编码的规则,而是将其拆分为两个部分,分别使用三个字节存放,共六个字节。但这样也就导致了使用 UTF-8 编码解析的话便将会其识别为两个字符。
1 | 1 | 1 | 0 | 1 | 1 | 0 | 1 |
1 | 0 | 1 | 0 | ||||
1 | 0 | ||||||
1 | 1 | 1 | 0 | 1 | 1 | 0 | 1 |
1 | 0 | 1 | 1 | ||||
1 | 0 |
主要有的解决思路有两种,一种是使用 UTF-16 编码,系统提供了对应的 GetStringChars 和 ReleaseStringChars 方法。
1 | const jchar * GetStringChars(JNIEnv *env, jstring string, |
另一种是先在 Java 层拿到 UTF-8 编码的字符串 byte[] 数据,再以 jbyteArray 的形式传入。
1 | byte[] data = str.getBytes("UTF-8"); |
1 | // jbyteArray _bytes; |
1 | sudo apt update && sudo apt install samba |
直接在 Ubuntu 的文件管理器 GUI 右键菜单里选择 属性 -> 本地文件共享,并勾选共享此目录
以及允许其他人来创建和删除这个文件夹里的文件
就可以了,系统会为我们自动设置目录的权限及共享配置。
我们可以单独创建一个用户用于登录 Samba 服务。
1 | sudo useradd smb |
允许使用该用户登录 Samba 并重启 Samba 服务。
1 | sudo smbpasswd -a smb && sudo service smbd restart |
现在只要在 Finder 左侧边栏选择服务器,点击 Connect as
并使用创建的账户登录就可以直接访问共享目录啦。
Touch 事件被封装成 MotionEvent 对象来传递。
由 ACTION_DOWN 开始,经过若干次 ACTION_MOVE 并以 ACTION_UP 结束的一个事件序列称为一个 gesture。
涉及 Touch 事件处理的角色有四种:
事件在 activity 中的分发起始于 dispatchTouchEvent()
方法。从源码可以看出 activity 调用了 Window 的 superDispatchTouchEvent()
方法来处理,若返回值为 true,则结束事件分发,否则调用 onTouchEvent()
方法。
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
Window 的实现类为 PhoneWindow,PhoneWindow 实际调用了 DecorView 的 superDispatchTouchEvent()
。
而 DecorView 是 activity 中的顶级 View,它其实是一个 FrameLayout,它的 superDispatchTouchEvent()
直接调用了 ViewGroup 的 dispatchTouchEvent()
。
由此,事件流入 View 层级。
1 | // PhoneWindow |
dispatchTouchEvent()
流程事件流入 View 层级后会逐级分发,ViewGroup 处在事件分发的中间层,是事件分发机制中最复杂的部分。此处源码非常长,就不贴出来了。
ACTION_DOWN 事件作为一次 gesture 的开始,可以影响后续事件的传递流程。为了方便理解,这里将它与其他类型的事件分开分析。
1 |
|
1 |
|
dispatchTouchEvent()
流程View 位于事件分发的最下层,不再向下分发。View 的事件分发逻辑大致等效于 return mOnTouchListener.onTouch(this, e) || onTouchEvent(e);
。
View 的 OnClickListener 是在 onTouchEvent()
方法里回调的,所以如果我们为 View 设置了 onTouchListener 并在 onTouch()
里返回 true,那么它的 OnClick 等系统预设的事件会失效。
1 | // 简化的 View dispatchTouchEvent 代码 |
java.text.DateFormat
这个类来专门处理日期格式,它的实例仅能通过三个工厂方法获取,然后就可以调用 format()
和 parse()
方法来格式化和解析了。DateFormat.getDateInstance([int style[, Locale aLocale]])
DateFormat.getDateTimeInstance([int dateStyle, int timeStyle[, Locale aLocale]])
DateFormat.getTimeInstance([int style[, Locale aLocale]])
其中的 style 可以是 SHORT, MEDIUM, LONG 或者 FULL。
我们常用的是其子类 SimpleDateFormat
,它可以直接传入一个字符串 pattern 来定义格式,详细文档可以看这里 http://developer.android.com/intl/zh-cn/reference/java/text/SimpleDateFormat.html。
坑出没注意
如果要解析的日期字符串不是纯数字(比如含有用文字表示的的月份或者周几)的话,一定要记得传入 Locale。
另外一种日期的格式化问题就是弄成那种像 2小时前
,1天前
这样的格式。Android 其实自带了一个 DataUtils
类来处理这种格式。
formatDateRange()
可以格式化时间段,比如 3:00pm - 4:00pm
,或者 Dec 31, 2007 - Jan 1, 2008
。formatElapsedTime()
可以传入秒数,格式化成 MM:SS
或 H:MM:SS
。formatSameDayTime
需要传入 now 参数,对在同一天的时间显示时间,不是同一天则仅显示日期。getRelativeDateTimeString()
就是我们常见的 2小时前
格式了。
下面我们一步一步来分析这个机制的实现(以下源码均基于 SDK 23)。
我们都知道 Java 中一个线程执行的入口点是它的 run()
方法(或者对应 Runable 对象的 run()
方法),我们要为目标线程建立消息循环就需要在此处进行操作,否则线程代码一下子就执行完了,结束掉了。
这时我们就需要 Looper 来完成这个工作了,从其命名我们就可以看出来它是作为一个“循环器”来使用,它可以方便地为我们的线程建立消息循环。我们在使用 Looper 的时候都是先调用 Looper.prepare()
,然后创建 Handler 作为消息处理器,最后再调用 Looper.loop()
来启动消息循环。
那么 Looper.prepare()
干了些什么事情呢,我们来看一下其源码。
1 | /** Initialize the current thread as a looper. |
其中的 sThreadLocal
是 Looper 类的静态 ThreadLocal 类对象。这里先说明一下 ThreadLocal 的作用,ThreadLocal 就是用来从当前线程对象里存取数据的一个工具类。
ThreadLocal 使用 Thread.currentThread()
来获得当前代码所在的线程对象,然后把数据存到线程对象中,get 也是从线程对象里取出来。
我们从源码可以看出来 Looper.prepare()
会向当前线程对象存放一个 Looper 对象,另外 Looper 对象在创建的时候创建一个 MessageQueue,将其以及当前线程对象作为对象成员。
再来看 Handler 的创建。
1 | /** |
Handler 的构造函数可以传入一个 Looper 对象,否则将会调用 Looper.myLooper()
从当前线程对象里取出前面 Looper.prepare()
存的 Looper 对象,之后 Handler 将 Looper 和 MessageQueue 作为对象成员。
最后我们就可以调用 Looper.loop()
来启动消息循环了。
1 | /** |
Looper.loop()
会从当前线程对象中把前面存的 Looper 对象以及该对象的 mMessageQueue 取出来(这里取消息是通过调用 MessageQueue 的 next()
方法来取得下一条消息),然后启动消息循环,不停地从 mMessageQueue 里取消息并分派处理,也就是会调用消息对应的 target Handler 的 dispathMessage()
方法,当然这个消息处理过程就与 Looper.loop()
在同一个线程里了。
在 Android 中我们使用 Handler 来完成的。我们从前面就知道 Handler 在创建的时候就会持有一个 MessageQueue 的引用,其实这个就是用来往 MessageQueue 里发消息用的。
当我们使用 Handler 发消息时,最后都会调用到 Handler 的 enqueueMessage()
方法。
1 | private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { |
这里 Handler 将 Message 的 target 设置为自己,也就是指定了这条消息将会由自己来处理,然后将 Message 放进 MessageQueue 里面。
因为 MessageQueue 的 enqueueMessage()
方法的权限是 package level 的,我们不能直接调用,就需要通过 Handler 来把消息发到 MessageQueue 里。
所以 Handler 的作用主要有两个,一是处理消息,二是作为发消息的一个工具。
那么根据以上分析,我们可以得出结论:
一个 Looper 对象一定对应于一个线程,一个线程最多有一个 Looper 对象
一个 Message 最终由它的 target Handler 来处理
一个 Message 会在持有它所被加入的 MessageQueue 的 Looper 对象对应的线程中被处理
所以如果我们在子线程中完成了耗时操作想要切换到主线程更新 UI 的时候,思路应该是:
主线程 Looper 的 MessageQueue -> 主线程 Looper -> 构造持有主线程 Looper 的 Handler(new Handler(Looper.getMainLooper())) -> 使用该 Handler 发送消息
standard
,singleTop
,singleTask
,singleInstance
),网上和一些书上也有各种讲解,但这些文章大多是针对四种启动模式进行介绍,总是在看过一段时间后就忘掉了。本文希望带你重新理解 activity 启动模式,并理解一些其他的相关概念,而不是仅仅针对四种启动模式。Activity 启动模式是与 task 密切相关的,所以我们先从 task 说起。
A task is a collection of activities that users interact with when performing a certain job. The activities are arranged in a stack (the back stack), in the order in which each activity is opened.
上面这句话是官方文档里的,解释了 Android 中 task 的概念,task 就是若干 activity 组成的一个集合,以栈的形式来管理。在 Android 中,每个 activity 都是运行在 task 中的,前台 task 的栈顶 activity 位于前台,可以直接与用户交互。
那么一个 activity 运行在哪个 task 中是由谁来决定的呢?我们首先来看 taskAffinity,taskAffinity 是影响 Activity 运行在哪个 task 中的第一个因素。
一个 activity 的 taskAffinity 是我们在 manifests.xml
里定义的,每个 Activity 都可以设置一个 android:taskAffinity
属性,它的值是一个字符串。application
元素也有这个属性,它的默认值是 manifest
元素中指定的包名(package
属性)。对于 Activity 的这个属性,当我们不显式设置的时候它就会从 application
元素继承。
那么在 taskAffinity 属性起作用的情况下,除了 taskAffinity 值为空字符串的 Activity,具有相同 taskAffinity 的 Activity 会运行在同一个 task 中,即使它们处于不同的应用中。taskAffinity 为空字符串表示这个 Activity 不会与其他任何 task 关联。
manifests.xml
中的 launchMode我们可以在 manifests.xml
中为 activity 指定 android:launchMode
属性,可以的取值有 standard
,singleTop
,singleTask
,singleInstance
,默认值是 standard
。
那么我们不设置 Intent flags(下文会讲到)的情况下启动一个 activity 的时候,系统就会根据 manifests.xml
里设置的 launchMode 来决定 activity 启动模式。下面是四种启动模式的处理说明。
standard
,singleTop
:在当前 task 中启动,忽略 taskAffinity 属性。
两种模式的区别是 standard
模式每次启动都会创建新的实例,而 singleTop
模式则是若已经有该 activity 的实例存在于 task 栈顶的情况下不再创建新实例,而是回调该实例的 onNewIntent()
方法。
singleTask
:这时该 activity 要在哪个 task 中运行就由我们前面指定的 taskAffinity 来决定了(该 activity 只能运行在 taskAffinity 与自己的 taskAffinity 相同的 task 中)。并且在这个 task 中该 activity 的实例只允许存在一个,若已经有一个实例在栈顶,就会回调它的 onNewIntent()
方法;若有实例且不在栈顶,系统则会将该 task 中该位于该实例之上的 activity 出栈销毁,使该实例回到前台,并回调其 onNewIntent()
方法。
singleInstance
:与 singleTask
类似,区别在于 singleInstance
的 task 中只能有一个 activity 实例存在,即该 activity 只能单独运行于一个 task 中。
影响 activity 启动模式的另一个地方就是 Intent flag 了。我们可以在使用 Intent 启动一个 activity 的时候可以调用 Intent 的 setFlag()
方法来为它设置标志位,且 Intent flag 具有更高的优先级。影响 activity 启动模式的 Intent flag 有下面几个。
FLAG_ACTIVITY_NEW_TASK
:在新 task 中启动 activity,效果与 singleTask
模式类似,但不会清除目标 activity 之上的 activity。使用这个 flag 可以实现应用间切换的效果。
FLAG_ACTIVITY_SINGLE_TOP
:与 singleTop
模式效果相同。
FLAG_ACTIVITY_CLEAR_TOP
:若目标 activity 在其 task 中已经有实例存在,则将 task 中其上的 activity 出栈销毁,使该实例回到前台,并回调其 onNewIntent()
方法。与 FLAG_ACTIVITY_NEW_TASK
结合使用可以实现 singleTask
模式的效果。
本文主要介绍 Eclipse 下和 Android Studio 下 NDK 开发环境的搭建。
http://developer.android.com/ndk/downloads/index.html
在这里根据平台选择相应的文件下载。
Windows 下面可以直接双击下载的 exe 文件解压。
Linux 和 OS X 用户需要进入下载的文件目录,执行下面的命令解压。
1 | chmod a+x android-ndk-r10c-darwin-x86_64.bin |
只要把 NDK 目录加入到系统 PATH 变量就好了,这步是为了方便从命令行执行 NDK 脚本。
Android > NDK > NDK Location
native 方法在方法签名前加 native
关键字,无方法体。
1 | public native String yo(); |
在左侧项目视图上点击右键,选择 Android Tools -> Add Native Support…,然后输入 Library Name:libxxx.so,Finish。
之后 Eclipse 会为我们生成 jni 目录以及 Android.mk
,xxx.cpp
两个文件。
在 Eclipse 中执行 build 命令,然后进入到项目的 bin\classes
目录,执行 javah -cp 完整类名 android.jar
命令,其中完整的类名是指包含包名,android.jar
是 SDK 目录下对应的 compile SDK 版本目录下面的 android.jar 的路径,比如我的 compile SDK 版本是 23,那我的路径就是 SDK目录\platforms\android-23\android.jar
。此时会 javah 会为我们在 classes 目录下生成 jni 头文件。
此处需要注意的是如果使用 C++ 编写,需要定义 extern "C"
宏。
关于 extern "C"
的详细信息可以参考这篇文章 extern “c”用法解析。
1 |
|
进入项目根目录,使用 ndk-build
命令执行编译,编译完成后会在项目的 libs
目录下生成相应的 so 文件。
在类中添加初始化代码段加载 jni 库
1 | static { |
build.gradle (Module: app)
添加 NDK 模块在 android > defaultConfig 下新增如下片段,执行 build,会提示需要在 gradle.properties 添加 android.useDeprecatedNdk=true
,根据提示添加完后重新 build 一下。
1 | ndk { |
在 https://bintray.com/android/android-tools/com.android.tools.build.gradle-experimental/view 查看 gradle-experimental 插件的最新版,并在 Project 的 buildscript>dependencies classpath 中替换原有的插件。
启用 gralde-experimental 需要修改 module 的配置。
1 | apply plugin: 'com.android.model.application' |
同 Eclipse
在 native 方法名上,按 Option + Enter
键根据提示选择第一项 Create function …,Android Studio 会自动为我们生成相应的 c 文件,及方法签名等信息,方便我们编写 native 代码。
同 Eclipse,Android Studio 会自动帮我们编译 native 代码,无需手动编译
]]>Android 中多线程通信基本的方式是使用 Handler 机制,基本使用方式如下。
1 | //init handler on the original thread |
系统还内置了AsyncQueryHandler 辅助子类,方便使用 ContentResolver 进行异步查询操作。
关于 Handler,需要注意的是,当 Handler 被声明为 Activity 的非静态内部类时, Handler 会持有外部 Activity 实例的引用,Handler 生命周期比 Activity 长时会导致 Activity 实例不能被正常释放,从而引起内存泄漏。一种解决方式是将 Handler 声明为 Activity 的静态内部类或者单独的类,在 Handler 内部使用 WeakReference/SoftReference 保存对 Activity的引用,既能访问 Activity 的 View 更新 UI,又可以避免内存泄漏。
AsyncTask 方式是官方提供的用来简化手动写 Handler 的一种异步机制,其内部仍使用 Handler 实现。
需要继承AsyncTask<Params, Progress, Result>类,重写doInBackground,onPostExecute等回调方法,使用的时候调用 execute 方法(只能在 UI 线程调用)传入参数就可以方便的执行 IO 或网络等耗时操作并在操作完成时更新 UI。
使用 AsyncTask 同样需要注意内存泄漏问题。
算是 Handler 方式的一种语法糖吧,使用了 Activity 自身维护的 一个 mHandler 实例,便于在 UI 线程执行操作,比如异步获取数据后更新 UI。
Loader 是在 Android 3.0 引入的用于在 Activity 和 Fragment 中简化异步加载数据的方式。
系统提供了 AsyncTaskLoader
使用 RxJava 可以方便地进行多线程调度,通过调用 Observable.subscribeOn 和 Observable.observeOn,并使用 Schedulers.io() 和 Schedulers.mainThread() 等工厂方法传入参数即可自由切换线程。
使用 EventBus 可以在任意线程发布数据,并通过订阅方法的命名约定规定在何种线程执行订阅的回调方法。
1 | // Called in the same thread (default) |
1 | $ hexo new "My New Post" |
More info: Writing
1 | $ hexo server |
More info: Server
1 | $ hexo generate |
More info: Generating
1 | $ hexo deploy |
More info: Deployment
]]>