1. 1. Build & Compile
    1. 1.1. gradle
      1. 1.1.1. 指定某个 dependency library 在项目中依赖的唯一版本
      2. 1.1.2. proxy 的配置
      3. 1.1.3. 为 maven repository 配置获取 depenency library 的规则
      4. 1.1.4. resConfigs 对语言显示的影响
      5. 1.1.5. 如何将多个 module 打包为一个 aar?
    2. 1.2. ndk / jni
      1. 1.2.1. ndk 编译报错: 找不到支持 mipsel 的 ndk 版本
      2. 1.2.2. jni method signature
      3. 1.2.3. ndk 构建添加一个文件夹下的源码
  2. 2. Grammar
    1. 2.1. java
      1. 2.1.1. 为什么 retrofit 可以获取到泛型信息
    2. 2.2. kotlin
      1. 2.2.1. kotlin 装箱 Int 值却没有改变内存地址?
      2. 2.2.2. kotlin 中 data class 自动生成的 equals() 函数如何排除一个某一个单独的 property?(以排除 rowid 属性示例)
      3. 2.2.3. kotlin class 通过 @Parcelize 生成 parcelable 代码时,无法访问 CREATOR 对象的问题
  3. 3. Android Framework
    1. 3.1. activity
      1. 3.1.1. 如何在一个新的返回栈中启动 Activity
      2. 3.1.2. 自定义 WXEntryActivity 路径
      3. 3.1.3. 自定义 menu item 布局
    2. 3.2. fragment
      1. 3.2.1. java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
      2. 3.2.2. Fragment 之间执行 SharedElementTransition,需要添加 changeTransform 与 ChangeBounds 两个 transition。
      3. 3.2.3. fragment 通过 setCustomAnimations 设置切换动画,结合 popBackStack 实现时,如果使用 add 方法添加 fragment,会无法触发 exitAnim,popEnterAnim,通过 replace 添加即可
    3. 3.3. dialog
      1. 3.3.1. 清除 Android 4.x 上 DialogFragment 顶部的蓝色横线
    4. 3.4. recyclerView
      1. 3.4.1. recyclerView 在 nestedScrollView 中使用时,某些情况下导致 scrollY 重置的问题
    5. 3.5. jetpack - ViewModel
      1. 3.5.1. VM 的复用
    6. 3.6. jetpack - WorkManager
      1. 3.6.1. WorkManager 在国产机型上可能失效 (app 结束后)
    7. 3.7. jetpack - Lifecycle
      1. 3.7.1. Fragment Transition 时,onCreate/onDestroy 与 View 生命周期不同步导致 observe 发生两次的问题
    8. 3.8. other
      1. 3.8.1. fullscreen 模式下,手指下滑导致布局整体位移,顶部空出一个 statusBar 高度的黑边的问题
      2. 3.8.2. 监听 sd 卡插拔,receiver 除了声明相应的 action 外,还需要添加 <data android:scheme="file" />
      3. 3.8.3. 如何判断 AudioTrack 播放完成
    9. 3.9. Resources
      1. 3.9.1. styles 可以通过 dot(.) 继承
      2. 3.9.2. windowSoftInputMode="adjustResize" 与 translucent actionbar 一并设置时工作异常
      3. 3.9.3. 为 view 设置 .9.png 背景时,可能会导致 background 形变
      4. 3.9.4. mipmap 与 drawable 的区别
    10. 3.10. View & Layout
      1. 3.10.1. 清除 TextView 上下的多余边距(ascent,descent)
      2. 3.10.2. 使得 Switch 的 Parent View 响应 Switch 触摸操作的办法
      3. 3.10.3. 使得 ScrollView 布局填满可用空间
      4. 3.10.4. ViewGroup 默认不会调用 onDraw 的解决方案
    11. 3.11. Thread
      1. 3.11.1. ThreadPoolExecutor 添加 task 时,创建线程/添加队列的规则
  4. 4. Open Source Library
    1. 4.1. dagger2
      1. 4.1.1. 将 @Singleton 作为 Application 层级单例的用法是错误的
    2. 4.2. retrofit
      1. 4.2.1. 当 response contentLength 为 0 时,解析失败
    3. 4.3. gson
      1. 4.3.1. gson 在反序列化 Object 声明的对象时,默认将数字转为 double 类型
  5. 5. Other
    1. 5.0.1. 获取系统 app 的 apk 文件

Android 经验积累

Build & Compile

gradle

指定某个 dependency library 在项目中依赖的唯一版本

  • 通过配置 gradle 任务,指定唯一版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    subprojects {
    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
    19
    allprojects {
    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 显示语言的规则
    1. Android OS 先确定 app 的 Locale。Android OS 会自上而下遍历用户在系统设置中设置的语言列表,直到匹配到一个 resConfigs 支持的,则设置 app Locale 为该语言;若一个都没有匹配到,则使用系统语言设置列表的第一个语言作为 app Locale
    2. 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 年后,仍然没有这种功能。

一个可行的方案:

  1. 假设 module 依赖关系是一个树,先从最底层叶子 module 开始打包 aar
  2. 将打包后的 aar 导入叶子父节点的 module 中,对这个父节点 module 打包
  3. 处理好重复依赖导致的冲突,如通过 exclude 命令移除重复的 package
  4. 重复 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
      2
      ln -s arm-linux-androideabi-4.9 mipsel-linux-android
      ln -s aarch64-linux-android-4.9 mips64el-linux-android

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
2
3
4
5
6
7
8
9
10
add_library(
library
SHARED
// source files...
)

// 将 source 文件夹下的源码全部添加到 library 的构建中
target_include_directories(library
PRIVATE
./source)

Grammar

java

为什么 retrofit 可以获取到泛型信息

  • 概述
    • 位于声明一侧的,源码里写了什么到运行时就能看到什么
      • 泛型类型(泛型类与泛型接口)声明
      • 带有泛型参数的方法和域的声明
    • 位于使用一侧的,源码里写什么到运行时都没了
      • 局部变量
  • 原因:Java 5 开始 class 文件规定要写入声明侧的泛型信息。
  • 举例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public 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.StringT、4 的 TU 都可以获得。
    • 但是 TU 等在运行时的实际类型是获取不到的,因为 class 文件中只记录了声明时写的泛型信息 TU,但不知道实际使用时的泛型类型,因为这属于使用层信息。同样的,第 5 处也是使用层,无法获取泛型信息 java.lang.String
  • retrofit 如何获取泛型信息
    • 参考上面的举例,retrofit 获取的是方法返回值的泛型信息,即 Call<Entity> getEntity(); 中的 Entity 信息,属于声明侧,所以可以通过 Method.getGenericReturnType() 获取方法返回值的泛信息。

      参考链接

kotlin

kotlin 装箱 Int 值却没有改变内存地址?

  • kotlin 官方实例中,提到 Int 值经过装箱操作可能导致内存地址不同(即创建了一个新的 Int 对象),对应代码及结果如下:
    1
    2
    3
    4
    5
    6
    val a = 100
    val boxedA: Int? = a
    val anotherBoxedA: Int? = a
    println(a === a) // true
    println(a === boxedA) // false
    println(boxedA === anotherBoxedA) // false
    然而,在我们把 a 的值改为 100 后,得到的结果却变成了 true,这是因为 java 会缓存 (-128…127) 这些 int 值,在创建这些值的 Int 对象时,会直接从缓存读取,不会创建新对象

    参考链接

kotlindata 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 对象是无法引用到的
    • 有人给 jetbrain 提交了 issue,但至今仍未解决
    • 关于 @Parcelize 更详细的解释可以参考这篇 blog
  • 解决方案
    • 方法1: 不使用 @Parcelize ,改为手写 Parcelable 实现
      • 优势: 和编写 java 代码一样,有很多方便的插件可以自动生成 Parcelable 代码
      • 劣势: 和编写 java 代码一样,在字段更新时,需要手动重新生成 Parcelable 代码
    • 方法2: 通过反射调用 CREATOR 对象
      • 优势: 不需要手动维护 Parcelable 代码
      • 劣势: 反射调用的额外性能开销

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 布局

  1. 编写自定义布局文件 layout_menu_item.xml
  2. 为菜单项的 app:actionLayout 属性设置该布局
    1
    2
    3
    4
    <item android:id="@+id/flavor"
    android:title=""
    app:showAsAction="always"
    app:actionLayout="@layout/layout_menu_item" />
  3. onCreateOptionsMenu 中引用该布局,设置自定义 view 的点击事件,使其响应 onOptionsItemSelected
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Override
    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() {
    @Override
    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 Transactions & Activity State Loss

Fragment 之间执行 SharedElementTransition,需要添加 changeTransform 与 ChangeBounds 两个 transition。

fragment 通过 setCustomAnimations 设置切换动画,结合 popBackStack 实现时,如果使用 add 方法添加 fragment,会无法触发 exitAnimpopEnterAnim,通过 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
  • 而大部分详情/跳转界面则无所谓使用 ActivityFragment 来获取 VM,因为它们对应的 Fragment/Activity 每次都是新创建的,一定会创建一个新的 ViewModelStore 来缓存 VM

jetpack - WorkManager

WorkManager 在国产机型上可能失效 (app 结束后)

参考链接

jetpack - Lifecycle

Fragment Transition 时,onCreate/onDestroyView 生命周期不同步导致 observe 发生两次的问题

  • 问题分析: observe 发生多次,导致多个 Observer 被绑定在同一个 Livedata
  • 解决方法: 调用 observe 时,使用 ViewLifecycleOwner 代理 LifecycleOwner

other

fullscreen 模式下,手指下滑导致布局整体位移,顶部空出一个 statusBar 高度的黑边的问题

  • 禁用 recyclerview 获取焦点,在其父布局设置 android:descendantFocusability="blocksDescendants"

    参考链接

监听 sd 卡插拔,receiver 除了声明相应的 action 外,还需要添加 <data android:scheme="file" />

1
2
3
4
5
<receiver>
<action android:name="android.intent.action.MEDIA_MOUNTED" />
...
<data android:scheme="file" />
</receiver>

如何判断 AudioTrack 播放完成

  • 概述: AudioTrack 中计算播放的 audioLength 是以音频帧(frame)为单位的,详见 AudioTrack 源码注释。
  • 举例: 如果音频格式为 16bit,则
    1
    1frame = 16bit = 2bytes
    而 fileLength 是以 byte 为单位,因此
    1
    totalFrames(即 audioLength) = fileLength / 2
  • 判断 AudioTrack 播放完成的两种方法
    • 方法1: AudioTrack.setNotificationMarkerPosition 设置音频播放到什么进度会发起通知回调,并通过 AudioTrack.setPlaybackPositionUpdateListener 监听这个回调,只需要设置音频播放完成时发起通知回调,即 AudioTrack.setNotificationMarkerPosition(audioLength) 即可判断音频播放完成。(这个方法在某些情况下 Listener 不会响应,原因未知)

      参考: https://stackoverflow.com/a/6655260/2354216

    • 方法2: 通过 AudioTrack.getPlaybackHeadPosition 判断是否播放完成,即 AudioTrack.getPlaybackHeadPosition() == audioLength

Resources

styles 可以通过 dot(.) 继承

详见 官方文档

windowSoftInputMode="adjustResize"translucent actionbar 一并设置时工作异常

这是一个 framework bug

解决方案
issue tracker

为 view 设置 .9.png 背景时,可能会导致 background 形变

  • 问题分析: 设置 .9.png 作为 background,可能会改变原来的 padding, 导致原来设置的 padding 无效
  • 解决方案: 设置 .9.png background 后,在重新手动设置一下 padding

    参考链接

mipmap 与 drawable 的区别

View & Layout

清除 TextView 上下的多余边距(ascent,descent)

  • includeFontPadding="false"

使得 SwitchParent View 响应 Switch 触摸操作的办法

  1. Switch 设置 clickable=false 禁止响应操作
  2. Switch 设置 background=@null 移除 state 变化时对应的 ui 反馈
  3. Parent View 设置 clickListener 并在回调方法中设置对应的 Switch checked 状态

使得 ScrollView 布局填满可用空间

  • 为 ScrollView 添加属性: android:fillViewport="true"

    参考链接

ViewGroup 默认不会调用 onDraw 的解决方案

  • 方法1: 设置 setWillNotDraw(false) 使其调用 onDraw
  • 方法2: 在 dispatchDraw 中绘制

参考: https://stackoverflow.com/a/13056400/2354216

Thread

ThreadPoolExecutor 添加 task 时,创建线程/添加队列的规则

  • 默认规则
    1. 当任务进入时,如果 corePoolSize 未满,则创建新线程执行任务
    2. 如果 corePoolSize 已满,则线程池会将任务添加至等待队列
    3. 如果等待队列已满,则创建新线程执行队列头部任务,再将任务任务添加至队尾
    4. 如果线程数达到 maximumPoolSize,则交给 setRejectedExecutionHandler 处理
  • 如果希望优先创建线程,线程池满后再添加到等待队列,可利用 RejectedExecutionHandler 的原理,手动控制等待队列,参考实现如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    BlockingQueue<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 contentLength0 时,解析失败

  • 报错案例
    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 文件

  1. 查看 app 列表
    1
    adb shell pm list packages -s
  2. 查看 app 安装路径
    1
    adb shell pm path com.example.package
  3. 将安装路径下的 apk 文件拉取到电脑端
    1
    adb pull <path_returned>

    参考: https://stackoverflow.com/a/29992355/2354216