Android 经验积累
Build & Compile
gradle
指定某个 dependency library
在项目中依赖的唯一版本
- 通过配置 gradle 任务,指定唯一版本
1
2
3
4
5
6
7
8
9
10subprojects {
project.configurations.all {
resolutionStrategy.eachDependency { details ->
if (details.requested.group == 'com.android.support'
&& !details.requested.name.contains('multidex')) {
details.useVersion "28.0.0"
}
}
}
}
proxy 的配置
Android Studio
的配置与gradle
不同Android Studio
在软件设置中中配置Proxy
gralde
在用户文件夹/.gradle/gradle.properties
(如 mac 就在/Users/{username}/.gradle/gradle.properties
) 中配置gradle
配置主要影响所有gradle
相关的任务,如build
clean
assemble
等等
- 局域网地址与国内地址不应该走 proxy
- 一个
nonProxyHosts
配置示例1
localhost|127.0.0.1|192.168.*|*.aliyun.com|*.huawei.com
- 一个
- 最好使用
http
而非socks
,因为socks
不支持nonProxyHosts
配置
为 maven repository
配置获取 depenency library
的规则
includeGroupByRegex
指定一个maven repository
只获取 group 匹配的dependency library
excludeGroupByRegex
指定一个maven repository
不获取 group 匹配的dependency library
- 一个配置参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19allprojects {
repositories {
maven {
url "https://jitpack.io"
content {
includeGroupByRegex "com\\.github\\.\\w*"
}
}
maven {
url 'http://developer.huawei.com/repo/'
content {
includeGroupByRegex "com\\.huawei\\.\\w*"
}
}
mavenCentral()
google()
jcenter()
}
}这组 api 是在 gradle 5.1 中加入的,api 使用说明参考: gradle 5.1 user manual
resConfigs 对语言显示的影响
- resConfigs 的作用
- 精简 strings,移除不需要语言的 strings
- 告诉 Android OS 本 app 支持的语言类型
- 设置 resConfigs 后,App 显示语言的规则
- Android OS 先确定 app 的 Locale。Android OS 会自上而下遍历用户在系统设置中设置的语言列表,直到匹配到一个 resConfigs 支持的,则设置 app Locale 为该语言;若一个都没有匹配到,则使用系统语言设置列表的第一个语言作为 app Locale
- app 根据 Android OS 确定的 Locale 显示语言。如果 Locale 能匹配到对应的 strings 文件,则显示该语言的 strings,否则使用 default strings
如何将多个 module 打包为一个 aar?
问题描述: 可能有某个 SDK 分为多个 module 构建,其中一个最顶级 module 依赖多个 sub-module,打包时希望以最顶级 module 为入口,将它们打在一起,就像在项目中直接依赖这个 module 一样。
官方回复: Android Studio 并不提供相关的功能或命令行,参考 Android SDK 技术主管在 Stack Overflow 上的回答,这个功能在 Android Studio 的开发中具有极低的优先级,不要期待某一天会有这个功能。实际上从在这篇答案 7 年后,仍然没有这种功能。
一个可行的方案:
- 假设 module 依赖关系是一个树,先从最底层叶子 module 开始打包 aar
- 将打包后的 aar 导入叶子父节点的 module 中,对这个父节点 module 打包
- 处理好重复依赖导致的冲突,如通过 exclude 命令移除重复的 package
- 重复 2、3 两步,直到打包至根节点 module
ndk / jni
ndk 编译报错: 找不到支持 mipsel 的 ndk 版本
- 报错案例
1
No toolchains found in the NDK toolchains folder for ABI with prefix mips64el-linux-android
- 解决方案 (os x)
- 创建一个 link folder 指向可以用的 ndk toolchain
1
2ln -s arm-linux-androideabi-4.9 mipsel-linux-android
ln -s aarch64-linux-android-4.9 mips64el-linux-android
- 创建一个 link folder 指向可以用的 ndk toolchain
jni method signature
- jni 方法签名使用 jvm 的规则,具体参考 官方文档 中的 Type Signatures 章节
- 示例: java 方法签名
long f (int n, String s, int[] arr);
在 jni 中表示为(ILjava/lang/String;[I)J
,这个可能在 jni 反射调用 java 方法时用到
ndk 构建添加一个文件夹下的源码
1 | add_library( |
Grammar
java
为什么 retrofit 可以获取到泛型信息
- 概述
- 位于声明一侧的,源码里写了什么到运行时就能看到什么
- 泛型类型(泛型类与泛型接口)声明
- 带有泛型参数的方法和域的声明
- 位于使用一侧的,源码里写什么到运行时都没了
- 局部变量
- 位于声明一侧的,源码里写了什么到运行时就能看到什么
- 原因: 从
Java 5
开始class
文件规定要写入声明侧的泛型信息。 - 举例
1
2
3
4
5
6
7
8
9
10
11
12public class GenericClass<T> { // 1
private List<T> list; // 2
private Map<String, T> map; // 3
public <U> U genericMethod(Map<T, U> m) { // 4
return null;
}
public void test() {
List<String> l = new ArrayList<String>(); // 5
}
}- 上面代码的 1,2,3,4 处都可以获取到泛型信息。针对 1 的
GenericClass<T>
,运行时通过Class.getTypeParameters()
方法得到的数组可以获取那个T
。同理,2 的T
、3的java.lang.String
与T
、4 的T
与U
都可以获得。 - 但是
T
、U
等在运行时的实际类型是获取不到的,因为class
文件中只记录了声明时写的泛型信息T
、U
,但不知道实际使用时的泛型类型,因为这属于使用层信息。同样的,第 5 处也是使用层,无法获取泛型信息java.lang.String
。
- 上面代码的 1,2,3,4 处都可以获取到泛型信息。针对 1 的
- retrofit 如何获取泛型信息
- 参考上面的举例,retrofit 获取的是方法返回值的泛型信息,即
Call<Entity> getEntity();
中的Entity
信息,属于声明侧,所以可以通过Method.getGenericReturnType()
获取方法返回值的泛信息。
- 参考上面的举例,retrofit 获取的是方法返回值的泛型信息,即
kotlin
kotlin 装箱 Int 值却没有改变内存地址?
kotlin
官方实例中,提到Int
值经过装箱操作可能导致内存地址不同(即创建了一个新的Int
对象),对应代码及结果如下:然而,在我们把1
2
3
4
5
6val a = 100
val boxedA: Int? = a
val anotherBoxedA: Int? = a
println(a === a) // true
println(a === boxedA) // false
println(boxedA === anotherBoxedA) // falsea
的值改为100
后,得到的结果却变成了true
,这是因为java
会缓存(-128…127)
这些int
值,在创建这些值的Int
对象时,会直接从缓存读取,不会创建新对象
kotlin
中 data class
自动生成的 equals()
函数如何排除一个某一个单独的 property
?(以排除 rowid 属性示例)
- 方法1: 利用
copy()
方法,传入相同的rowid
value
,但有额外的性能开销,维护成本低1
oldItem.copy(rowId = 0) == newItem.copy(rowId = 0)
- 方法2: 重写 equals 方法,排除 rowId,无额外性能开销,但维护成本高,每次添加参数都需要手动修改 equals 方法
kotlin class 通过 @Parcelize 生成 parcelable 代码时,无法访问 CREATOR 对象的问题
- 问题分析: 这是一个设计上的缺陷,
@Parcelize
在 kotlin 编译时生成Parcelable
实现代码,因此在编译前引用Creator
对象是无法引用到的 - 解决方案
- 方法1: 不使用
@Parcelize
,改为手写Parcelable
实现- 优势: 和编写 java 代码一样,有很多方便的插件可以自动生成
Parcelable
代码 - 劣势: 和编写 java 代码一样,在字段更新时,需要手动重新生成
Parcelable
代码
- 优势: 和编写 java 代码一样,有很多方便的插件可以自动生成
- 方法2: 通过反射调用 CREATOR 对象
- 优势: 不需要手动维护
Parcelable
代码 - 劣势: 反射调用的额外性能开销
- 优势: 不需要手动维护
- 方法1: 不使用
Android Framework
activity
如何在一个新的返回栈中启动 Activity
- 使用
FLAG_ACTIVITY_NEW_TASK
+taskAffinity
才能真正在一个新的 activity 栈中启动 activity
自定义 WXEntryActivity
路径
- 在
AndroidManifest
中使用activity-alias
重定向WXEntryActivity
的实际路径1
2
3
4<activity-alias
android:name="${applicationId}.wxapi.WXEntryActivity"
android:exported="true"
android:targetActivity=".share.WXEntryActivity" />
自定义 menu item 布局
- 编写自定义布局文件
layout_menu_item.xml
- 为菜单项的
app:actionLayout
属性设置该布局1
2
3
4<item android:id="@+id/flavor"
android:title=""
app:showAsAction="always"
app:actionLayout="@layout/layout_menu_item" /> - 在
onCreateOptionsMenu
中引用该布局,设置自定义 view 的点击事件,使其响应onOptionsItemSelected
1
2
3
4
5
6
7
8
9
10
11
12
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_menu, menu);
final MenuItem item = menu.findItem(R.id.flavor);
item.getActionView().setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
onOptionsItemSelected(item);
}
});
return super.onCreateOptionsMenu(menu);
}参考: https://blog.csdn.net/yinzhijiezhan/article/details/80997554
fragment
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
Fragment 之间执行 SharedElementTransition,需要添加 changeTransform 与 ChangeBounds 两个 transition。
- 方法1: java/kotlin 代码中手写配置 transition
- 方法2: 添加一个 xml 文件,同时添加两个 transition
1
2
3
4
5
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeTransform/>
<changeBounds/>
</transitionSet>https://stackoverflow.com/questions/26950801/shared-elements-animating-between-fragments?rq=1
fragment 通过 setCustomAnimations
设置切换动画,结合 popBackStack
实现时,如果使用 add
方法添加 fragment,会无法触发 exitAnim
,popEnterAnim
,通过 replace
添加即可
- 原因分析: 通过
add
添加的 fragment 在执行add
操作时,原栈顶 fragment 没有执行detach
,因此没有触发exitAnim
,在执行popBackStack
操作时,即将回到栈顶的 fragment 没有执行reAttach
,因此没有触发popEnterAnim
dialog
清除 Android 4.x 上 DialogFragment 顶部的蓝色横线
dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE);
必须在setContentView
前设置
recyclerView
recyclerView 在 nestedScrollView 中使用时,某些情况下导致 scrollY 重置的问题
- recyclerView.notifyDataSetChanged()
- recyclerView 在 Fragment 中,Fragment 嵌套在 NestedScrollView 中,切换 fragment 可能导致 scrollY 重置
jetpack - ViewModel
VM 的复用
- 对于 App 主界面中
Fragment
,应通过Activity
获取 VM 以达到 VM 重用的目的,因为这会使用主界面Activity
创建的ViewModelStore
来实例化 VM - 而大部分详情/跳转界面则无所谓使用
Activity
或Fragment
来获取 VM,因为它们对应的Fragment/Activity
每次都是新创建的,一定会创建一个新的ViewModelStore
来缓存 VM
jetpack - WorkManager
WorkManager
在国产机型上可能失效 (app 结束后)
jetpack - Lifecycle
Fragment Transition
时,onCreate/onDestroy
与 View
生命周期不同步导致 observe
发生两次的问题
- 问题分析:
observe
发生多次,导致多个Observer
被绑定在同一个Livedata
上 - 解决方法: 调用
observe
时,使用ViewLifecycleOwner
代理LifecycleOwner
other
fullscreen 模式下,手指下滑导致布局整体位移,顶部空出一个 statusBar 高度的黑边的问题
- 禁用 recyclerview 获取焦点,在其父布局设置
android:descendantFocusability="blocksDescendants"
监听 sd 卡插拔,receiver
除了声明相应的 action
外,还需要添加 <data android:scheme="file" />
1 | <receiver> |
如何判断 AudioTrack 播放完成
- 概述: AudioTrack 中计算播放的 audioLength 是以音频帧(frame)为单位的,详见 AudioTrack 源码注释。
- 举例: 如果音频格式为 16bit,则 而 fileLength 是以 byte 为单位,因此
1
1frame = 16bit = 2bytes
1
totalFrames(即 audioLength) = fileLength / 2
- 判断 AudioTrack 播放完成的两种方法
- 方法1:
AudioTrack.setNotificationMarkerPosition
设置音频播放到什么进度会发起通知回调,并通过AudioTrack.setPlaybackPositionUpdateListener
监听这个回调,只需要设置音频播放完成时发起通知回调,即AudioTrack.setNotificationMarkerPosition(audioLength)
即可判断音频播放完成。(这个方法在某些情况下Listener
不会响应,原因未知) - 方法2: 通过
AudioTrack.getPlaybackHeadPosition
判断是否播放完成,即AudioTrack.getPlaybackHeadPosition() == audioLength
- 方法1:
Resources
styles 可以通过 dot(.) 继承
详见 官方文档
windowSoftInputMode="adjustResize"
与 translucent actionbar
一并设置时工作异常
这是一个 framework bug
为 view 设置 .9.png
背景时,可能会导致 background
形变
- 问题分析: 设置
.9.png
作为background
,可能会改变原来的 padding, 导致原来设置的 padding 无效 - 解决方案: 设置
.9.png
background
后,在重新手动设置一下 padding
mipmap 与 drawable 的区别
- drawable: 按照实际的 density 选择图片
- mipmap: 为了解决部分设备不使用实际 density 的场景。比如 Nexus 5 屏幕是 xxhdpi,但 Launcher 使用 xxxhdpi 的 icon
- 因此只需把 app icon 放在 mipmap 文件夹下即可,其他 icon 均放在 drawable 下
参考
https://stackoverflow.com/a/28065664/2354216
https://stackoverflow.com/a/25131495/2354216
View & Layout
清除 TextView 上下的多余边距(ascent,descent)
includeFontPadding="false"
使得 Switch
的 Parent View
响应 Switch
触摸操作的办法
Switch
设置clickable=false
禁止响应操作Switch
设置background=@null
移除 state 变化时对应的 ui 反馈Parent View
设置clickListener
并在回调方法中设置对应的Switch
checked
状态
使得 ScrollView 布局填满可用空间
- 为 ScrollView 添加属性:
android:fillViewport="true"
ViewGroup 默认不会调用 onDraw 的解决方案
- 方法1: 设置
setWillNotDraw(false)
使其调用onDraw
- 方法2: 在
dispatchDraw
中绘制
Thread
ThreadPoolExecutor
添加 task
时,创建线程/添加队列的规则
- 默认规则
- 当任务进入时,如果
corePoolSize
未满,则创建新线程执行任务 - 如果
corePoolSize
已满,则线程池会将任务添加至等待队列 - 如果等待队列已满,则创建新线程执行队列头部任务,再将任务任务添加至队尾
- 如果线程数达到 maximumPoolSize,则交给 setRejectedExecutionHandler 处理
- 当任务进入时,如果
- 如果希望优先创建线程,线程池满后再添加到等待队列,可利用
RejectedExecutionHandler
的原理,手动控制等待队列,参考实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17BlockingQueue<Runnable> queue = new LinkedTransferQueue<Runnable>() {
@Override
public boolean offer(Runnable e) {
return tryTransfer(e);
}
};
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue);
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
Open Source Library
dagger2
将 @Singleton
作为 Application
层级单例的用法是错误的
- 应该单独声明一个
@AppScope
用以标记Application
生命周期的单例对象(或者需要Application
/Context
实例参与构造的对象) @Singleton
用于声明/注释java
层的最顶级单例对象,其单例层级高于 App 声明周期,即Application
(@AppScope
)@Singleton
大多用于标记一些不依赖Application
单例类,如gson/moshi
序列化/反序列化相关的帮助类,线程池管理类及其他工具类
retrofit
当 response contentLength
为 0
时,解析失败
- 报错案例
1
java.io.EOFException: End of input at line 1 column 1
- 解决方案
- 在
GsonConverterFactory
前插入一个NullOnEmptyConverterFactory
专门处理这种情况,使其在contentLength == 0
时,直接返回null
,不进行 json 解析
- 在
gson
gson 在反序列化 Object
声明的对象时,默认将数字转为 double
类型
Other
获取系统 app 的 apk 文件
- 查看 app 列表
1
adb shell pm list packages -s
- 查看 app 安装路径
1
adb shell pm path com.example.package
- 将安装路径下的 apk 文件拉取到电脑端
1
adb pull <path_returned>