注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

探究 Kotlin 的隐藏性能开销与避坑指南

在 2019 年 Google I/O 大会上,Google 宣布了今后 Android 开发将优先使用 Kotlin ,即 Kotlin-first,随之在 Android 开发界兴起了一阵全民学习 Kotlin 的热潮。之后 Google 也推出了一系列用...
继续阅读 »

在 2019 年 Google I/O 大会上,Google 宣布了今后 Android 开发将优先使用 Kotlin ,即 Kotlin-first,随之在 Android 开发界兴起了一阵全民学习 Kotlin 的热潮。之后 Google 也推出了一系列用 Kotlin 实现的 ktx 扩展库,例如 activity-ktxfragment-ktxcore-ktx等,提供了各种方便的扩展方法用于简化开发者的工作,Kotlin 协程目前也是官方在 Android 上进行异步编程的推荐解决方案


Google 推荐优先使用 Kotlin,也宣称不会放弃 Java,但目前各种 ktx 扩展库还是需要由 Kotlin 代码进行使用才能最大化地享受到其便利性,Java 代码来调用显得有点不伦不类。作为 Jetpack 主要组件之一的 Paging 3.x 版本目前也已经完全用 Kotlin 实现,为 Kotlin 协程提供了一流的支持。刚出正式版本不久的 Jetpack Compose 也只支持 Kotlin,Java 无缘声明式 UI


开发者可以感受到 Kotlin 在 Android 开发中的重要性在不断提高,虽然 Google 说不会放弃 Java,但以后的事谁说得准呢?开发者还是需要尽早迁移到 Kotlin,这也是必不可挡的技术趋势


Kotlin 在设计理念上有很多和 Java 不同的地方,开发者能够直观感受到的是语法层面上的差异性,背后也包含有一系列隐藏的性能开销以及一些隐藏得很深的“坑”,本篇文章就来介绍在使用 Kotlin 过程中存在的隐藏性能开销,帮助读者避坑,希望对你有所帮助 🤣🤣


慎用 @JvmOverloads


@JvmOverloads 注解大家应该不陌生,其作用在具有默认参数的方法上,用于向 Java 代码生成多个重载方法


例如,以下的 println 方法对于 Java 代码来说就相当于两个重载方法,默认使用空字符串作为入参参数


//Kotlin
@JvmOverloads
fun println(log: String = "") {

}

//Java
public void println(String log) {

}

public void println() {
println("");
}

@JvmOverloads 很方便,减少了 Java 代码调用 Kotlin 代码时的调用成本,使得 Java 代码也可以享受到默认参数的便利,但在某些特殊场景下也会引发一个隐藏得很深的 bug


举个例子


我们知道 Android 系统的 View 类包含有多个构造函数,我们在实现自定义 View 时至少就要声明一个包含有两个参数的构造函数,参数类型必须依次是 Context 和 AttributeSet,这样该自定义 View 才能在布局文件中使用。而 View 类的构造函数最多包含有四个入参参数,最少只有一个,为了省事,我们在用 Kotlin 代码实现自定义 View 时,就可以用 @JvmOverloads 来很方便地继承 View 类,就像以下代码


open class BaseView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0, defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes)

如果我们是像 BaseView 一样直接继承于 View 的话,此时使用@JvmOverloads就不会产生任何问题,可如果我们继承的是 TextView 的话,那么问题就来了


直接继承于 TextView 不做任何修改,在布局文件中分别使用 MyTextView 和 TextView,给它们完全一样的参数,看看运行效果


open class MyTextView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0, defStyleRes: Int = 0
) : TextView(context, attrs, defStyleAttr, defStyleRes)

    <github.leavesc.demo.MyTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="业志陈"
android:textSize="42sp" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="业志陈"
android:textSize="42sp" />

此时两个 TextView 就会呈现出不一样的文本颜色了,十分神奇



这就是 @JvmOverloads 带来的一个隐藏问题。因为 TextView 的 defStyleAttr 实际上是有一个默认值的,即 R.attr.textViewStyle,当中就包含了 TextView 的默认文本颜色,而由于 MyTextView 为 defStyleAttr 指定了一个默认值 0,这就导致 MyTextView 丢失了一些默认风格属性


public TextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.textViewStyle);
}

因此,如果我们要直接继承的是 View 类的话可以直接使用@JvmOverloads,此时不会有任何问题,而如果我们要继承的是现有控件的话,就需要考虑应该如何设置默认值了


慎用 解构声明


有时我们会有把一个对象拆解成多个变量的需求,Kotlin 也提供了这类语法糖支持,称为解构声明


例如,以下代码就将 People 变量解构为了两个变量:name 和 nickname,变量名可以随意取,每个变量就按顺序对应着 People 中的字段


data class People(val name: String, val nickname: String)

private fun printInfo(people: People) {
val (name, nickname) = people
println(name)
println(nickname)
}

每个解构声明其实都会被编译成以下代码,解构操作其实就是在按照顺序获取特定方法的返回值


String name = people.component1();

String nickname = people.component2();

component1()component2() 函数是 Kotlin 为数据类自动生成的方法,People 反编译为 Java 代码后就可以看到,每个方法返回的其实都是成员变量,方法名包含的数字对应的就是成员变量在数据类中的声明顺序


public final class People {
@NotNull
private final String name;
@NotNull
private final String nickname;

@NotNull
public final String component1() {
return this.name;
}

@NotNull
public final String component2() {
return this.nickname;
}

}

解构声明和数据类配套使用时就有一个隐藏的知识点,看以下例子


假设后续我们为 People 添加了一个新字段 city,此时 printInfo 方法一样可以正常调用,但 nickname 指向的其实就变成了 people 变量内的 city 字段了,含义悄悄发生了变化,此时就会导致逻辑错误了


data class People(val name: String, val city: String, val nickname: String)

private fun printInfo(people: People) {
val (name, nickname) = people
println(name)
println(nickname)
}

数据类中的字段是可以随时增减或者变换位置的,从而使得解构结果和我们一开始预想的不一致,因此我觉得解构声明和数据类不太适合放在一起使用


慎用 toLowerCase 和 toUpperCase


当我们要以忽略大小写的方式比较两个字符串是否相等时,通常想到的是通过 toUpperCasetoLowerCase 方法将两个字符串转换为全大写或者全小写,然后再进行比较,这种方式完全可以满足需求,但当中也包含着一个隐藏开销


例如,以下的 Kotlin 代码反编译为 Java 代码后,可以看到每次调用toUpperCase方法都会创建一个新的临时变量,然后再去调用临时变量的 equals 方法进行比较


fun main() {
val name = "leavesC"
val nickname = "leavesc"
println(name.toUpperCase() == nickname.toUpperCase())
}

public static final void main() {
String name = "leavesC";
String nickname = "leavesc";
String var10000 = name.toUpperCase();
String var10001 = nickname.toUpperCase();
boolean var2 = Intrinsics.areEqual(var10000, var10001);
System.out.println(var2);
}

以上代码就多创建了两个临时变量,这样的代码无疑会比较低效


有一个更好的解决方案,就是通过 Kotlin 提供的支持忽略大小写的 equals 扩展方法来进行比较,此方法内部会调用 String 类原生的 equalsIgnoreCase来进行比较,从而避免了创建临时变量,相对来说会比较高效一些


fun main() {
val name = "leavesC"
val nickname = "leavesc"
println(name.equals(other = nickname, ignoreCase = true))
}

public static final void main() {
String name = "leavesC";
String nickname = "leavesc";
boolean var2 = StringsKt.equals(name, nickname, true);
boolean var3 = false;
System.out.println(var2);
}

慎用 arrayOf


Kotlin 中的数组类型可以分为两类:



  • IntArray、LongArray、FloatArray 形式的基本数据类型数组,通过 intArrayOf、longArrayOf、floatArrayOf 等方法来声明

  • Array<T> 形式的对象类型数组,通过 arrayOf、arrayOfNulls 等方法来声明


例如,以下的 Kotlin 代码都是用于声明整数数组,但实际上存储的数据类型并不一样


val intArray: IntArray = intArrayOf(1, 2, 3)

val integerArray: Array<Int> = arrayOf(1, 2, 3)

将以上代码反编译为 Java 代码后,就可以明确地看出一种是基本数据类型 int,一种是包装类型 Integer,arrayOf 方法会自动对入参值进行装箱


private final int[] intArray = new int[]{1, 2, 3};

private final Integer[] integerArray = new Integer[]{1, 2, 3};

为了表示基本数据类型的数组,Kotlin 为每一种基本数据类型都提供了若干相应的类并做了特殊的优化。例如,IntArray、ByteArray、BooleanArray 等类型都会被编译成普通的 Java 基本数据类型数组:int[]、byte[]、boolean[],这些数组中的值在存储时不会进行装箱操作,而是使用了可能的最高效的方式


因此,如果没有必要的话,我们在开发中要慎用 arrayOf 方法,避免不必要的装箱消耗


慎用 vararg


和 Java 一样,Kotlin 也支持可变参数,允许将任意多个参数打包到一个数组中再一并传给函数,Kotlin 通过使用 varage 关键字来声明可变参数


我们可以向 printValue 方法传递任意数量的入参参数,也可以直接传入一个数组对象,但 Kotlin 要求显式地解包数组,以便每个数组元素在函数中能够作为单独的参数来调用,这个功能被称为展开运算符,使用方式就是在数组前加一个 *


fun printValue(vararg values: Int) {
values.forEach {
println(it)
}
}

fun main() {
printValue()
printValue(1)
printValue(2, 3)
val values = intArrayOf(4, 5, 6)
printValue(*values)
}

如果我们是以直接传递若干个入参参数的形式来调用 printValue 方法的话,Kotlin 会自动将这些参数打包为一个数组进行传递,这里面就包含着创建数组的开销,这方面和 Java 保持一致。 如果我们传入的参数就已经是数组的话,Kotlin 相比 Java 就存在着一个隐藏开销,Kotlin 会复制现有数组作为参数拿来使用,相当于多分配了额外的数组空间,这可以从反编译后的 Java 代码看出来


   public static final void printValue(@NotNull int... values) {
Intrinsics.checkNotNullParameter(values, "values");
int $i$f$forEach = false;
int[] var3 = values;
int var4 = values.length;

for(int var5 = 0; var5 < var4; ++var5) {
int element$iv = var3[var5];
int var8 = false;
boolean var9 = false;
System.out.println(element$iv);
}

}

public static final void main() {
printValue();
printValue(1);
printValue(2, 3);
int[] values = new int[]{4, 5, 6};
//复制后再进行调用
printValue(Arrays.copyOf(values, values.length));
}

// $FF: synthetic method
public static void main(String[] var0) {
main();
}

可以看到 Kotlin 会通过 Arrays.copyOf 复制现有数组,将复制后的数组作为参数进行调用,这样做的好处就是可以避免 printValue 方法影响到原有数组,坏处就是会额外消耗多一份的内存空间


慎用 lazy


我们经常会使用lazy()函数来惰性加载只读属性,将加载操作延迟到需要使用的时候,适用于某些不适合立刻加载或者加载成本较高的情况


例如,以下的 lazyValue 只会等到我们调用到的时候才会被赋值


val lazyValue by lazy {
"it is lazy value"
}

而在使用lazy()函数时很容易被忽略的地方就是其包含有一个可选的 model 参数:



  • LazyThreadSafetyMode.SYNCHRONIZED。只允许由单个线程来完成初始化,且初始化操作包含有双重锁检查,从而使得所有线程都得到相同的值

  • LazyThreadSafetyMode.PUBLICATION。允许多个线程同时执行初始化操作,但只有第一个初始化成功的值会被当做最终值,最终所有线程也都会得到相同的值

  • LazyThreadSafetyMode.NONE。允许多个线程同时执行初始化操作,不进行任何线程同步,导致不同线程可能会得到不同的初始化值,因此不应该用于多线程环境


lazy()函数默认情况下使用的就是LazyThreadSafetyMode.SYNCHRONIZED,从 SynchronizedLazyImpl 可以看到,其内部就使用到了synchronized来实现多线程同步,以此避免多线程竞争


public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this

override val value: T
get() {
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}

return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}

override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

private fun writeReplace(): Any = InitializedLazyImpl(value)
}

对于 Android 开发者来说,大多数情况下我们都是在主线程中调用 lazy() 函数,此时使用 LazyThreadSafetyMode.SYNCHRONIZED 就会带来不必要的线程同步开销,因此可以根据实际情况考虑替换为LazyThreadSafetyMode.NONE


慎用 lateinit var


lateinit var 适用于某些不方便马上就初始化变量的场景,用于将初始化操作延后,同时也存在一些使用上的限制:如果在未初始化的情况下就使用该变量的话会导致 NPE


例如,如果在 name 变量还未初始化时就调用了 print 方法的话,此时就会导致 NPE。且由于 lateinit var 变量不允许为 null,因此此时我们也无法通过判空来得知 name 是否已经被初始化了,而且判空操作本身也相当于在调用 name 变量,在未初始化的时候一样会导致 NPE


lateinit var name: String

fun print() {
println(name)
}

我们可以通过另一种方式来判断 lateinit 变量是否已初始化


lateinit 实际上是通过代理机制来实现的,关联的是 KProperty0 接口,KProperty0 就提供了一个扩展属性用于判断其代理的值是否已经初始化了


@SinceKotlin("1.2")
@InlineOnly
inline val @receiver:AccessibleLateinitPropertyLiteral KProperty0<*>.isInitialized: Boolean
get() = throw NotImplementedError("Implementation is intrinsic")

因此我们可以通过以下方式来进行判断,从而避免不安全的访问操作


lateinit var name: String

fun print() {
if (this::name.isInitialized) {
println("isInitialized true")
println(name)
} else {
println("isInitialized false")
println(name) //会导致 NPE
}
}

lambda 表达式


lambda 表达式在语义上很简洁,既避免了冗长的函数声明,也解决了以前需要强类型声明函数类型的情况


例如,以下代码就通过 lambda 表达式声明了一个回调函数 callback,我们无需创建一个具体的函数类型,而只需声明需要的入参参数、入参类型、函数返回值就可以


fun requestHttp(callback: (code: Int, data: String) -> Unit) {
callback(200, "success")
}

fun main() {
requestHttp { code, data ->
println("code: $code")
println("data: $data")
}
}

lambda 表达式语法虽然方便,但也隐藏着两个性能问题:



  • 每次调用 lambda 表达式都相当于在创建一个对象

  • lambda 表达式内部隐藏了自动装箱和自动拆箱的操作


将以上代码反编译为 Java 代码后,可以看到 callback 最终的实际类型就是 Function2,每次调用requestHttp 方法就相当于是在创建一个 Function2 变量


   public static final void requestHttp(@NotNull Function2 callback) {
Intrinsics.checkNotNullParameter(callback, "callback");
callback.invoke(200, "success");
}

Function2 是 Kotlin 提供的一个的泛型接口,数字 2 即代表其包含两个入参值


public interface Function2<in P1, in P2, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2): R
}

Kotlin 会在编译阶段将开发者声明的 lambda 表达式转换为相应的 FunctionX 对象,调用 lambda 表达式就相当于在调用其 invoke 方法,以此为低版本 JVM 平台(例如 Java 6 / 7)也能提供 lambda 表达式功能。此外,我们也知道泛型类型不可能是基本数据类型,因此我们在 Kotlin 中声明的 Int 最终会被自动装箱为 Integer,lambda 表达式内部自动完成了装箱和拆箱的操作


所以说,简洁的 lambda 表达式背后就隐藏了自动创建 Function 对象进行中转调用,自动装箱和自动拆箱的过程,且最终创建的方法总数要多于表面上看到的


如果想要避免 lambda 表达式的以上开销,可以通过使用 inline 内联函数来实现


在使用 inline 关键字修饰 requestHttp 方法后,可以看到此时 requestHttp 的逻辑就相当于被直接复制到了 main 方法内部,不会创建任何多余的对象,且此时使用的也是 int 而非 Integer


inline fun requestHttp(callback: (code: Int, data: String) -> Unit) {
callback(200, "success")
}

fun main() {
requestHttp { code, data ->
println("code: $code")
println("data: $data")
}
}

   public static final void main() {
String data = "success";
int code = 200;
String var4 = "code: " + code;
System.out.println(var4);
var4 = "data: " + data;
System.out.println(var4);
}

通过内联函数,可以使得编译器直接在调用方中使用内联函数体中的代码,相当于直接把内联函数中的逻辑复制到了调用方中,完全避免了调用带来的开销。对于高阶函数,作为参数传递的 lambda 表达式的主体也将被内联,这使得:



  • 声明和调用 lambda 表达式时,不会实例化 Function 对象

  • 没有自动装箱和拆箱的操作

  • 不会导致方法数增多,但如果内联函数方法体较大且被多处调用的话,可能导致最终代码量显著增加


init 的声明顺序很重要


看以下代码,我们可以在 init 块中调用 parameter1,却无法调用 parameter2,从 IDE 的提示信息 Variable 'parameter2' must be initialized也可以看出来,对于 init 块来说 parameter2 此时还未赋值,自然就无法使用了


class KotlinMode {

private val parameter1 = "leavesC"

init {
println(parameter1)
//error: Variable 'parameter2' must be initialized
//println(parameter2)
}

private val parameter2 = "业志陈"

}

从反编译出的 Java 代码也可以看出来,由于 parameter2 是声明在 init 块之后,所以 parameter2 的赋值操作其实是放在构造函数中的最后面,因此 IDE 的语法检查器就会阻止我们在 init 块中来调用 parameter2 了


public final class KotlinMode {
private final String parameter1 = "leavesC";
private final String parameter2;

public KotlinMode() {
String var1 = this.parameter1;
System.out.println(var1);
this.parameter2 = "业志陈";
}
}

IDE 会阻止开发者去调用还未初始化的变量,防止我们写出不安全的代码,我们也可以用以下方式来绕过语法检查,但同时也写出了不安全的代码


我们可以通过在 init 块中调用 print() 方法的方式来间接访问 parameter2,此时代码是可以正常编译的,但此时 parameter2 也只会为 null


class KotlinMode {

private val parameter1 = "leavesC"

init {
println(parameter1)
print()
}

private fun print() {
println(parameter2)
}

private val parameter2 = "业志陈"

}

从反编译出的 Java 代码可以看出来,print()方法依旧是会在 parameter2 初始化之前被调用,此时print()方法访问到的 parameter2 也只会为 null,从而引发意料之外的 NPE


public final class KotlinMode {
private final String parameter1 = "leavesC";
private final String parameter2;

private final void print() {
String var1 = this.parameter2;
System.out.println(var1);
}

public KotlinMode() {
String var1 = this.parameter1;
System.out.println(var1);
this.print();
this.parameter2 = "业志陈";
}
}

所以说,init 块和成员变量之间的声明顺序决定了在构造函数中的初始化顺序,我们应该先声明成员变量再声明 init 块,否则就有可能导致 NPE


Gosn & data class


来看个小例子,猜猜其运行结果会是怎样的


UserBean 是一个 dataClass,其 userName 字段被声明为非 null 类型,而 json 字符串中 userName 对应的值明确就是 null,那用 Gson 到底能不能反序列化成功呢?程序能不能成功运行完以下三个步骤?


data class UserBean(val userName: String, val userAge: Int)

fun main() {
val json = """{"userName":null,"userAge":26}"""
val userBean = Gson().fromJson(json, UserBean::class.java) //第一步
println(userBean) //第二步
printMsg(userBean.userName) //第三步
}

fun printMsg(msg: String) {

}

实际上程序能够正常运行到第二步,但在执行第三步的时候反而直接报 NPE 异常了


UserBean(userName=null, userAge=26)
Exception in thread "main" java.lang.NullPointerException: Parameter specified as non-null is null: method temp.TestKt.printMsg, parameter msg
at temp.TestKt.printMsg(Test.kt)
at temp.TestKt.main(Test.kt:16)
at temp.TestKt.main(Test.kt)

printMsg 方法接收了参数后实际上什么也没做,为啥会抛出 NPE ?


printMsg反编译为 Java 方法,可以发现方法内部会对入参进行空校验,当发现为 null 时就会直接抛出 NPE。这个比较好理解,毕竟 Kotlin 的类型系统会严格区分 可 null不可为 null 两种类型,其区分手段之一就是会自动在我们的代码里插入一些类型校验逻辑,即自动加上了非空断言,当发现不可为 null 的参数传入了 null 的话就会马上抛出 NPE,即使我们并没有使用到该参数


   public static final void printMsg(@NotNull String msg) {
Intrinsics.checkNotNullParameter(msg, "msg");
}

那既然 UserBean 中的 userName 字段已经被声明为非 null 类型了,那么为什么还可以反序列化成功呢?按照我自己的第一直觉,应该在进行反序列的时候就直接抛出异常才对


将 UserBean 反编译为 Java 代码后,也可以看到其构造函数中是有对 userName 进行 null 检查的,当发现为 null 的话会直接抛出 NPE


public final class UserBean {
@NotNull
private final String userName;
private final int userAge;

@NotNull
public final String getUserName() {
return this.userName;
}

public final int getUserAge() {
return this.userAge;
}

public UserBean(@NotNull String userName, int userAge) {
//进行 null 检查
Intrinsics.checkNotNullParameter(userName, "userName");
super();
this.userName = userName;
this.userAge = userAge;
}

···

}

那 Gson 是怎么绕过 Kotlin 的 null 检查的呢?


其实,通过查看 Gson 内部源码,可以知道 Gson 是通过 Unsafe 包来实例化 UserBean 对象的,Unsafe 提供了一个非常规实例化对象的方法:allocateInstance,该方法提供了通过 Class 对象就可以创建出相应实例的功能,而且不需要调用其构造函数、初始化代码、JVM 安全检查等,即使构造函数是 private 的也能通过此方法进行实例化。因此 Gson 实际上并不会调用到 UserBean 的构造函数,相当于绕过了 Kotlin 的 null 检查,所以即使 userName 值为 null 最终也能够反序列化成功



此问题的出现场景大多是在移动端解析服务端传来的数据的时候,移动端将数据声明为非空类型,但服务端给过来的数据却为 null 值,此时用户看到的可能就是应用崩溃了……


一方面,我觉得移动端应该对服务端传来的数据保持不信任的态度,不能觉得对方传来的数据就一定是符合约定的,为了保证安全需要将数据均声明为可空类型。另一方面,这也无疑导致移动端需要加上很多多余的判空操作,简直有点无解 =_=


ARouter & JvmField


在 Java 中,字段和其访问器的组合被称作属性。在 Kotlin 中,属性是头等的语言特性,完全替代了字段和访问器方法。在类中声明一个属性和声明一个变量一样是使用 val 和 var 关键字,两者在使用上的差异就在于赋值后是否还允许修改,在字节码上的差异性之一就在于是否会自动生成相应的 setValue 方法


例如,以下的 Kotlin 代码在反编译为 Java 代码后,可以看到两个属性的可见性都变为了 private, name 变量会同时包含有getValuesetValue 方法,而 nickname 变量只有 getValue 方法,这也是我们在 Java 代码中只能以 kotlinMode.getName() 的方式来访问 name 变量的原因


class KotlinMode {

var name = "业志陈"

val nickname = "leavesC"

}

public final class KotlinMode {
@NotNull
private String name = "业志陈";
@NotNull
private final String nickname = "leavesC";

@NotNull
public final String getName() {
return this.name;
}

public final void setName(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.name = var1;
}

@NotNull
public final String getNickname() {
return this.nickname;
}
}

为了不让 Kotlin 的 var / val 变量自动生成 getValuesetValue 方法,达到和在 Java 代码中声明公开变量一样的效果,此时就需要为属性添加 @JvmField 注解了,添加后就会变为 public 类型的成员变量,且不包含任何 getValuesetValue 方法


class KotlinMode {

@JvmField
var name = "业志陈"

@JvmField
val nickname = "leavesC"

}

public final class KotlinMode {
@JvmField
@NotNull
public String name = "业志陈";
@JvmField
@NotNull
public final String nickname = "leavesC";
}



@JvmField 的一个使用场景就是在配套使用 ARouter 的时候。我们在使用 ARouter 进行参数自动注入时,就需要为待注入的参数添加 @JvmField注解,就像以下代码一样,不添加的话就会导致编译失败


@Route(path = RoutePath.USER_HOME)
class UserHomeActivity : AppCompatActivity() {

@Autowired(name = RoutePath.USER_HOME_PARAMETER_ID)
@JvmField
var userId: Long = 0

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_home)
ARouter.getInstance().inject(this)
}

}

那为什么不添加该注解就会导致编译失败呢?


其实,ARouter 实现参数自动注入是需要依靠注解处理器生成的辅助文件来实现的,即会生成以下的辅助代码,当中会以 substitute.userIdsubstitute.userName的形式来调用 Activity 中的两个参数值,如果不添加 @JvmField注解,辅助文件就没法以直接调用变量名的方式来完成注入,自然就会导致编译失败了


public class UserHomeActivity$$ARouter$$Autowired implements ISyringe {

private SerializationService serializationService;

@Override
public void inject(Object target) {
serializationService = ARouter.getInstance().navigation(SerializationService.class);
UserHomeActivity substitute = (UserHomeActivity)target;
substitute.userId = substitute.getIntent().getLongExtra("userHomeId", substitute.userId);
}
}

Kotlin 这套为属性自动生成 getValuesetValue 方法的机制有一个缺点,就是可能会导致方法数极速膨胀,使得 Android App 的 dex 文件很快就达到最大方法数限制,不得不进行分包处理


作者:业志陈
链接:https://juejin.cn/post/7010367024916660237
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android 弹幕的两种实现及性能对比 | 自定义 LayoutManager

引子 上一篇用“动画”方案实现了弹幕效果,自定义容器控件,每一条弹幕都作为其子控件,将子弹幕的初始位置置于容器控件右边的外侧,每条弹幕都通过从右向左的动画来实现贯穿屏幕的平移。 这个方案的性能有待改善,打开 GPU 呈现模式: 原因在于容器控件会提前构建所有...
继续阅读 »

引子


上一篇用“动画”方案实现了弹幕效果,自定义容器控件,每一条弹幕都作为其子控件,将子弹幕的初始位置置于容器控件右边的外侧,每条弹幕都通过从右向左的动画来实现贯穿屏幕的平移。


这个方案的性能有待改善,打开 GPU 呈现模式:


1629556466944.gif


原因在于容器控件会提前构建所有弹幕视图并将它们堆积在屏幕的右侧。若弹幕数据量大,则容器控件会因为子视图过多而耗费大量 measure + layout 时间。


既然是因为提前加载了不需要的弹幕才导致的性能问题,那是不是可以只预加载有限个弹幕?


只加载有限个子视图且可滚动的控件,不就是 RecyclerView 吗!它并不会把 Adapter 中所有的数据提前全部转换成 View,而是只预加载一屏的数据,然后随着滚动再持续不断地加载新数据。


为了用 RecyclerView 实现弹幕效果,就得 “自定义 LayoutManager”


自定义布局参数


自定义 LayoutManager 的第一步:继承RecyclerView.LayoutManger


class LaneLayoutManager: RecyclerView.LayoutManager() {
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {}
}

根据 AndroidStudio 的提示,必须实现一个generateDefaultLayoutParams()的方法。它用于生成一个自定义的LayoutParams对象,目的是在布局参数中携带自定义的属性。


当前场景中没有自定义布局参数的需求,遂可以这样实现这个方法:


override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(
RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT
)
}

表示沿用RecyclerView.LayoutParams


初次填充弹幕


自定义 LayoutManager 最重要的环节就是定义如何布局表项。


对于LinearLayoutManager来说,表项沿着一个方向线性铺开。当列表第一次展示时,从列表顶部到底部,表项被逐个填充,这称为“初次填充”。


对于LaneLayoutManager来说,初次填充即是“将一列弹幕填充到紧挨着列表尾部的地方(在屏幕之外,可不见)”。


关于LinearLayoutManager如何填充表项的源码分析,在之前的一篇RecyclerView 面试题 | 滚动时表项是如何被填充或回收的?中分析过,现援引结论如下:




  1. LinearLayoutManager 在onLayoutChildren()方法中布局表项。

  2. 布局表项的关键方法包括fill()layoutChunk(),前者表示列表的一次填充动作,后者表示填充单个表项。

  3. 在一次填充动作中通过一个while循环不断地填充表项,直到列表剩余空间用完。用伪代码表示这个过程如下所示:


public class LinearLayoutManager {
// 布局表项
public void onLayoutChildren() {
// 填充表项
fill() {
while(列表有剩余空间){
// 填充单个表项
layoutChunk(){
// 让表项成为子视图
addView(view)
}
}
}
}
}


  1. 为了避免每次填充新表项时都重新创建视图,需要从 RecyclerView 的缓存中获取表项视图,即调用Recycler.getViewForPosition()。关于该方法的详解可以点击RecyclerView 缓存机制 | 如何复用表项?



看过源码,理解原理后,弹幕布局就可以仿照着写:


class LaneLayoutManager : RecyclerView.LayoutManager() {
private val LAYOUT_FINISH = -1 // 标记填充结束
private var adapterIndex = 0 // 列表适配器索引

// 弹幕纵向间距
var gap = 5
get() = field.dp

// 布局孩子
override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
fill(recycler)
}
// 填充表项
private fun fill(recycler: RecyclerView.Recycler?) {
// 可供弹幕布局的高度,即列表高度
var totalSpace = height - paddingTop - paddingBottom
var remainSpace = totalSpace
// 只要空间足够,就继续填充表项
while (goOnLayout(remainSpace)) {
// 填充单个表项
val consumeSpace = layoutView(recycler)
if (consumeSpace == LAYOUT_FINISH) break
// 更新剩余空间
remainSpace -= consumeSpace
}
}

// 是否还有剩余空间用于填充 以及 是否有更多数据
private fun goOnLayout(remainSpace: Int) = remainSpace > 0 && currentIndex in 0 until itemCount

// 填充单个表项
private fun layoutView(recycler: RecyclerView.Recycler?): Int {
// 1. 从缓存池中获取表项视图
// 若缓存未命中,则会触发 onCreateViewHolder() 和 onBindViewHolder()
val view = recycler?.getViewForPosition(adapterIndex)
view ?: return LAYOUT_FINISH // 获取表项视图失败,则结束填充
// 2. 将表项视图成为列表孩子
addView(view)
// 3. 测量表项视图
measureChildWithMargins(view, 0, 0)
// 可供弹幕布局的高度,即列表高度
var totalSpace = height - paddingTop - paddingBottom
// 弹幕泳道数,即列表纵向可以容纳几条弹幕
val laneCount = (totalSpace + gap) / (view.measuredHeight + gap)
// 计算当前表项所在泳道
val index = currentIndex % laneCount
// 计算当前表项上下左右边框
val left = width // 弹幕左边位于列表右边
val top = index * (view.measuredHeight + gap)
val right = left + view.measuredWidth
val bottom = top + view.measuredHeight
// 4. 布局表项(该方法考虑到了 ItemDecoration)
layoutDecorated(view, left, top, right, bottom)
val verticalMargin = (view.layoutParams as? RecyclerView.LayoutParams)?.let { it.topMargin + it.bottomMargin } ?: 0
// 继续获取下一个表项视图
adapterIndex++
// 返回填充表项消耗像素值
return getDecoratedMeasuredHeight(view) + verticalMargin
}
}

每一条水平的,供弹幕滚动的,称之为“泳道”。


泳道是从列表顶部往底部垂直铺开的,列表高度/泳道高度 = 泳道的数量。


fill()方法中就以“列表剩余高度>0”为循环条件,不断地向泳道中填充表项,它得经历了四个步骤:



  1. 从缓存池中获取表项视图

  2. 将表项视图成为列表孩子

  3. 测量表项视图

  4. 布局表项


这四步之后,表项相对于列表的位置就确定下来,并且表项的视图已经渲染完成。


运行下 demo,果然~,什么也没看到。。。


列表滚动逻辑还未加上,所以布局在列表右边外侧的表项依然处于不可见位置。但可以利用 AndroidStudio 的Layout Inspector工具来验证初次填充代码的正确性:


微信截图_20210919225802.png


Layout Inspector中会用线框表示屏幕以外的控件,如图所示,列表右边的外侧被四个表项占满。


自动滚动弹幕


为了看到填充的表项,就得让列表自发地滚动起来。


最直接的方案就是不停地调用RecyclerView.smoothScrollBy()。为此写了一个扩展法方法用于倒计时:


fun <T> countdown(
duration: Long, // 倒计时总时长
interval: Long, // 倒计时间隔
onCountdown: suspend (Long) -> T // 倒计时回调
): Flow<T> =
flow { (duration - interval downTo 0 step interval).forEach { emit(it) } }
.onEach { delay(interval) }
.onStart { emit(duration) }
.map { onCountdown(it) }
.flowOn(Dispatchers.Default)

使用Flow构建了一个异步数据流,该流每次都会发射一个倒计时的剩余时间。关于Flow的详细解释可以点击Kotlin 异步 | Flow 应用场景及原理


然后就能像这样实现列表自动滚动:


countdown(Long.MAX_VALUE, 50) {
recyclerView.smoothScrollBy(10, 0)
}.launchIn(MainScope())

每 50 ms 向左滚动 10 像素。效果如下图所示:


1632126431947.gif


持续填充弹幕


因为只做了初次填充,即每个泳道只填充了一个表项,所以随着第一排的表项滚入屏幕后,就没有后续弹幕了。


LayoutManger.onLayoutChildren()只会在列表初次布局时调用一次,即初次填充弹幕只会执行一次。为了持续不断地展示弹幕,必须在滚动时不停地填充表项。


之前的一篇RecyclerView 面试题 | 滚动时表项是如何被填充或回收的?分析过列表滚动时持续填充表项的源码,现援引结论如下:




  1. RecyclerView 在滚动发生之前,会根据预计滚动位移大小来决定需要向列表中填充多少新的表项。

  2. 表现在源码上,即是在scrollVerticallyBy()中调用fill()填充表项:


public class LinearLayoutManager {
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
return scrollBy(dy, recycler, state);
}

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
...
// 填充表项
final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
...
}
}


对于弹幕的场景,也可以仿照着写一个类似的:


class LaneLayoutManager : RecyclerView.LayoutManager() {
override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int {
return scrollBy(dx, recycler)
}

override fun canScrollHorizontally(): Boolean {
return true // 表示列表可以横向滚动
}
}

重写canScrollHorizontally()返回 true 表示列表可横向滚动。


RecyclerView 的滚动是一段一段进行的,每一段滚动的位移都会通过scrollHorizontallyBy()传递过来。通常在该方法中根据位移大小填充新的表项,然后再触发列表的滚动。关于列表滚动的源码分析可以点击RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势


scrollBy()封装了根据滚动持续填充表项的逻辑。(稍后分析)


持续填充表项比初次填充的逻辑更复杂一点,初次填充只要将表项按照泳道从上到下依次铺开填满列表的高度即可。而持续填充得根据滚动距离计算出哪个泳道即将枯竭(没有弹幕展示的泳道),只对枯竭的泳道填充表项。


为了快速获取枯竭泳道,得抽象出一个“泳道”结构,以保存该泳道的滚动信息:


// 泳道
data class Lane(
var end: Int, // 泳道末尾弹幕横坐标
var endLayoutIndex: Int, // 泳道末尾弹幕的布局索引
var startLayoutIndex: Int // 泳道头部弹幕的布局索引
)

泳道结构包含三个数据,分别是:



  1. 泳道末尾弹幕横坐标:它是泳道中最后一个弹幕的 right 值,即它的右侧相对于 RecyclerView 左侧的距离。该值用于判断经过一段位移的滚动后,该泳道是否会枯竭。

  2. 泳道末尾弹幕的布局索引:它是泳道中最后一个弹幕的布局索引,记录它是为了方便地通过getChildAt()获取泳道中最后一个弹幕的视图。(布局索引有别于适配器索引,RecyclerView 只会持有有限个表项,所以布局索引的取值范围是[0,x],x的取值比一屏表项稍多一点,而对于弹幕来说,适配器索引的取值是[0,∞])

  3. 泳道头部弹幕的布局索引:与 2 类似,为了方便地获得泳道第一个弹幕的视图。


借助于泳道这个结构,我们得重构下初次填充表项的逻辑:


class LaneLayoutManager : RecyclerView.LayoutManager() {
// 初次填充过程中的上一个被填充的弹幕
private var lastLaneEndView: View? = null
// 所有泳道
private var lanes = mutableListOf<Lane>()
// 初次填充弹幕
override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
fillLanes(recycler, lanes)
}
// 通过循环填充弹幕
private fun fillLanes(recycler: RecyclerView.Recycler?, lanes: MutableList<Lane>) {
lastLaneEndView = null
// 如果列表垂直方向上还有空间则继续填充弹幕
while (hasMoreLane(height - lanes.bottom())) {
// 填充单个弹幕到泳道中
val consumeSpace = layoutView(recycler, lanes)
if (consumeSpace == LAYOUT_FINISH) break
}
}
// 填充单个弹幕,并记录泳道信息
private fun layoutView(recycler: RecyclerView.Recycler?, lanes: MutableList<Lane>): Int {
val view = recycler?.getViewForPosition(adapterIndex)
view ?: return LAYOUT_FINISH
measureChildWithMargins(view, 0, 0)
val verticalMargin = (view.layoutParams as? RecyclerView.LayoutParams)?.let { it.topMargin + it.bottomMargin } ?: 0
val consumed = getDecoratedMeasuredHeight(view) + if (lastLaneEndView == null) 0 else verticalGap + verticalMargin
// 若列表垂直方向还可以容纳一条新得泳道,则新建泳道,否则停止填充
if (height - lanes.bottom() - consumed > 0) {
lanes.add(emptyLane(adapterIndex))
} else return LAYOUT_FINISH

addView(view)
// 获取最新追加的泳道
val lane = lanes.last()
// 计算弹幕上下左右的边框
val left = lane.end + horizontalGap
val top = if (lastLaneEndView == null) paddingTop else lastLaneEndView!!.bottom + verticalGap
val right = left + view.measuredWidth
val bottom = top + view.measuredHeight
// 定位弹幕
layoutDecorated(view, left, top, right, bottom)
// 更新泳道末尾横坐标及布局索引
lane.apply {
end = right
endLayoutIndex = childCount - 1 // 因为是刚追加的表项,所以其索引值必然是最大的
}

adapterIndex++
lastLaneEndView = view
return consumed
}
}

初次填充弹幕也是一个不断在垂直方向上追加泳道的过程,判断是否追加的逻辑如下:列表高度 - 当前最底部泳道的 bottom 值 - 这次填充弹幕消耗的像素值 > 0,其中lanes.bottom()是一个List<Lane>的扩展方法:


fun List<Lane>.bottom() = lastOrNull()?.getEndView()?.bottom ?: 0

它获取泳道列表中的最后一个泳道,然后再获取该泳道中最后一条弹幕视图的 bottom 值。其中getEndView()被定义为Lane的扩展方法:


class LaneLayoutManager : RecyclerView.LayoutManager() {
data class Lane(var end: Int, var endLayoutIndex: Int, var startLayoutIndex: Int)
private fun Lane.getEndView(): View? = getChildAt(endLayoutIndex)
}

理论上“获取泳道中最后一条弹幕视图”应该是Lane提供的方法。但偏偏把它定义成Lane的扩展方法,并且还定义在LaneLayoutManager的内部,这是多此一举吗?


若定义在 Lane 内部,则在该上下文中无法访问到LayoutManager.getChildAt()方法,若只定义为LaneLayoutManager的私有方法,则无法访问到endLayoutIndex。所以此举是为了综合两个上下文环境。


再回头看一下滚动时持续填充弹幕的逻辑:


class LaneLayoutManager : RecyclerView.LayoutManager() {
override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int {
return scrollBy(dx, recycler)
}
// 根据位移大小决定填充多少表项
private fun scrollBy(dx: Int, recycler: RecyclerView.Recycler?): Int {
// 若列表没有孩子或未发生滚动则返回
if (childCount == 0 || dx == 0) return 0
// 在滚动还未开始前,更新泳道信息
updateLanesEnd(lanes)
// 获取滚动绝对值
val absDx = abs(dx)
// 遍历所有泳道,向其中的枯竭泳道填充弹幕
lanes.forEach { lane ->
if (lane.isDrainOut(absDx)) layoutViewByScroll(recycler, lane)
}
// 滚动列表的落脚点:将表项向手指位移的反方向平移相同的距离
offsetChildrenHorizontal(-absDx)
return dx
}
}

滚动时持续填充弹幕逻辑遵循这样的顺序:



  1. 更新泳道信息

  2. 向枯竭泳道填充弹幕

  3. 触发滚动


其中 1,2 都发生在真实的滚动之前,在滚动之前,已经拿到了滚动位移,根据位移就可以计算出滚动发生之后即将枯竭的泳道:


// 泳道是否枯竭
private fun Lane.isDrainOut(dx: Int): Boolean = getEnd(getEndView()) - dx < width
// 获取表项的 right 值
private fun getEnd(view: View?) =
if (view == null) Int.MIN_VALUE
else getDecoratedRight(view) + (view.layoutParams as RecyclerView.LayoutParams).rightMargin

泳道枯竭的判定依据是:泳道最后一个弹幕的右边向左平移 dx 后是否小于列表宽度。若小于则表示泳道中的弹幕已经全展示完了,此时就要继续填充弹幕:


// 弹幕滚动时填充新弹幕
private fun layoutViewByScroll(recycler: RecyclerView.Recycler?, lane: Lane) {
val view = recycler?.getViewForPosition(adapterIndex)
view ?: return
measureChildWithMargins(view, 0, 0)
addView(view)

val left = lane.end + horizontalGap
val top = lane.getEndView()?.top ?: paddingTop
val right = left + view.measuredWidth
val bottom = top + view.measuredHeight
layoutDecorated(view, left, top, right, bottom)
lane.apply {
end = right
endLayoutIndex = childCount - 1
}
adapterIndex++
}

填充逻辑和初次填充的几乎一样,唯一的区别是,滚动时的填充不可能因为空间不够而提前返回,因为是找准了泳道进行填充的。


为什么要在填充枯竭泳道之前更新泳道信息?


// 更新泳道信息
private fun updateLanesEnd(lanes: MutableList<Lane>) {
lanes.forEach { lane ->
lane.getEndView()?.let { lane.end = getEnd(it) }
}
}

因为 RecyclerView 的滚动是一段一段进行的,看似滚动了一丢丢距离,scrollHorizontallyBy()可能要回调十几次,每一次回调,弹幕都会前进一小段,即泳道末尾弹幕的横坐标会发生变化,这变化得同步到Lane结构中。否则泳道枯竭的计算就会出错。


无限滚动弹幕


经过初次和持续填充,弹幕已经可以流畅的滚起来了。那如何让仅有的弹幕数据无限轮播呢?


只需要在Adapter上做一个小手脚:


class LaneAdapter : RecyclerView.Adapter<ViewHolder>() {
// 数据集
private val dataList = MutableList()
override fun getItemCount(): Int {
// 设置表项为无穷大
return Int.MAX_VALUE
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val realIndex = position % dataList.size
...
}

override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
val realIndex = position % dataList.size
...
}
}

设置列表的数据量为无穷大,当创建表项视图及为其绑定数据时,对适配器索引取模。


回收弹幕


剩下的最后一个难题是,如何回收弹幕。若没有回收,也对不起RecyclerView这个名字。


LayoutManager中就定义有回收表项的入口:


public void removeAndRecycleView(View child, @NonNull Recycler recycler) {
removeView(child);
recycler.recycleView(child);
}

回收逻辑最终会委托给Recycler实现,关于回收表项的源码分析,可以点击下面的文章:



  1. RecyclerView 缓存机制 | 回收些什么?

  2. RecyclerView 缓存机制 | 回收到哪去?

  3. RecyclerView 动画原理 | 换个姿势看源码(pre-layout)

  4. RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系

  5. RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?


对于弹幕场景,什么时候回收弹幕?


当然是弹幕滚出屏幕的那一瞬间!


如何才能捕捉到这个瞬间 ?


当然是通过在每次滚动发生之前用位移计算出来的!


在滚动时除了要持续填充弹幕,还得持续回收弹幕(源码里就是这么写的,我只是抄袭一下):


private fun scrollBy(dx: Int, recycler: RecyclerView.Recycler?): Int {
if (childCount == 0 || dx == 0) return 0
updateLanesEnd(lanes)
val absDx = abs(dx)
// 持续填充弹幕
lanes.forEach { lane ->
if (lane.isDrainOut(absDx)) layoutViewByScroll(recycler, lane)
}
// 持续回收弹幕
recycleGoneView(lanes, absDx, recycler)
offsetChildrenHorizontal(-absDx)
return dx
}

这是scrollBy()的完整版,滚动时先填充,紧接着马上回收:


fun recycleGoneView(lanes: List<Lane>, dx: Int, recycler: RecyclerView.Recycler?) {
recycler ?: return
// 遍历泳道
lanes.forEach { lane ->
// 获取泳道头部弹幕
getChildAt(lane.startLayoutIndex)?.let { startView ->
// 如果泳道头部弹幕已经滚出屏幕则回收它
if (isGoneByScroll(startView, dx)) {
// 回收弹幕视图
removeAndRecycleView(startView, recycler)
// 更新泳道信息
updateLaneIndexAfterRecycle(lanes, lane.startLayoutIndex)
lane.startLayoutIndex += lanes.size - 1
}
}
}
}

回收和填充一样,也是通过遍历找到即将消失的弹幕,回收之。


判断弹幕消失的逻辑如下:


fun isGoneByScroll(view: View, dx: Int): Boolean = getEnd(view) - dx < 0

如果弹幕的 right 向左平移 dx 后小于 0 则表示弹幕已经滚出列表。


回收弹幕之后,会将其从 RecyclerView 中 detach,这个操作会影响列表中其他弹幕的布局索引值。就好像数组中某一元素被删除,其后面的所有元素的索引值都会减一:


fun updateLaneIndexAfterRecycle(lanes: List<Lane>, recycleIndex: Int) {
lanes.forEach { lane ->
if (lane.startLayoutIndex > recycleIndex) {
lane.startLayoutIndex--
}
if (lane.endLayoutIndex > recycleIndex) {
lane.endLayoutIndex--
}
}
}

遍历所有泳道,只要泳道头部弹幕的布局索引大于回收索引,则将其减一。


性能


再次打开 GPU 呈现模式:


1629555943171.gif


这次体验上就很丝滑,柱状图也没有超过警戒线。


talk is cheap, show me the code


完整代码可以点击这里,在这个repo中搜索LaneLayoutManager


总结


之前花了很多时间看源码,也产生过“看源码那么费时,到底有什么用?”这样的怀疑。这次性能优化是一次很好的回应。因为看过 RecyclerView 的源码,它解决问题的思想方法就种在脑袋里了。当遇到弹幕性能问题时,这颗种子就会发芽。解决方案是多种多样的,脑袋中有怎样的种子,就会长出怎样的芽。所以看源码是播撒种子,虽不能立刻发芽,但总有一天会结果。


作者:唐子玄
链接:https://juejin.cn/post/7010521583894659103
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

kotlin修炼指南6-Sealed到底密封了啥

在代码中,我们经常需要限定一些有限集合的状态值,例如:网络请求:成功——失败账户状态:VIP——穷逼VIP——普通工具栏:展开——半折叠——收缩等等。通常情况下,我们会使用enum class来做封装,将可见的状态值通过枚举来使用。enum class Net...
继续阅读 »

在代码中,我们经常需要限定一些有限集合的状态值,例如:

  • 网络请求:成功——失败
  • 账户状态:VIP——穷逼VIP——普通
  • 工具栏:展开——半折叠——收缩

等等。

通常情况下,我们会使用enum class来做封装,将可见的状态值通过枚举来使用。

enum class NetworkState(val value: Int) {
SUCCESS(0),
ERROR(1)
}

但枚举的缺点也很明显,首先,枚举比普通代码更占内存,同时,每个枚举只能定义一个实例,不能拓展更多信息。

除此之外,还有种方式,通过抽象类来对状态进行封装,但这种方式的缺点也很明显,它打破了枚举的限制性,所以,Kotlin给出了新的解决方案——Sealed Class(密封类)。

创建状态集

下面我们以网络请求的例子来看下具体如何使用Sealed Class来进行状态的封装。

和抽象类类似,Sealed Class可用于表示层级关系。它的子类可以是任意的类:data class、普通Kotlin对象、普通的类,甚至也可以是另一个密封类,所以,我们定义一个Result Sealed Class:

sealed class Result<out T : Any> {
data class Success<out T : Any>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}

当然,也不一定非要写在顶层类中:

sealed class Result<out T : Any> 
data class Success<out T : Any>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()

这样也是可以的,它们的区别在于引用的时候,是否包含顶层类来引用而已。

大部分场景下,还是建议第一种方式,可以比较清晰的展示调用的层级关系。

在这个例子中,我们定义了两个场景,分别是Success和Error,它表示我们假设的网络状态就这两种,分别在每种状态下,例如Success,都可以传入自定义的数据类型,因为它本身就是一个class,所以借助这一点,就可以自定义状态携带的场景值。在上面这个例子中,我们定义在Success中,传递data,而在Error时,传递Exception信息。

所以,使用Sealed Class的第一步,就是对场景进行封装,梳理具体的场景枚举,并定义需要传递的数据类型。

如果场景值不需要传递数据,那么可以简单的使用:object xxxx,定义一个变量即可。

使用

接下来,我们来看下如何使用Sealed Class。

fun main() {
// 模拟封装枚举的产生
val result = if (true) {
Result.Success("Success")
} else {
Result.Error(Exception("error"))
}

when (result) {
is Result.Success -> print(result.data)
is Result.Error -> print(result.exception)
}
}

大部分场景下,Sealed Class都会配合when一起使用,同时,如果when的参数是Sealed Class,在IDE中可以快速补全所有分支,而且不会需要你单独补充else 分支,因为Sealed Class已经是完备的了。

所以when和Sealed Class真是天作之合。

进一步简化

其实我们还可以进一步简化代码的调用,因为我们每次使用Sealed Class的时候,都需要when一下,有些时候,也会产生一些代码冗余,所以,借助拓展函数,我们进一步对代码进行简化。

inline fun Result<Any>.doSuccess(success: (Any) -> Unit) {
if (this is Result.Success) {
success(data)
}
}

inline fun Result<Any>.doError(error: (Exception?) -> Unit) {
if (this is Result.Error) {
error(exception)
}
}

这里我对Result进行了拓展,增加了doSuccess和doError两个拓展,同时接收两个高阶函数来接收处理行为,这样我们在调用的时候就更加简单了。

result.doSuccess { }
result.doError { }

所以when和Sealed Class和拓展函数,真是天作之合。

那么你一定好奇了,Sealed Class又是怎么实现的,其实反编译一下就一目了然了,实际上Sealed Class也是通过抽象类来实现的,编译器生成了一个只能编译器调用的构造函数,从而避免其它类进行修改,实现了Sealed Class的有限性。

封装?

Sealed Class与抽象类类似,可以对逻辑进行拓展,我们来看下面这个例子。

sealed class TTS {

abstract fun speak()

class BaiduTTS(val value: String) : TTS() {
override fun speak() = print(value)
}

class TencentTTS(val value: String) : TTS() {
override fun speak() = print(value)
}
}

这时候如果要进行拓展,就很方便了,代码如下所示。

class XunFeiTTS(val value: String) : TTS() {
override fun speak() = print(value)
}

所以,Sealed Class可以说是在抽象类的基础上,增加了对状态有限性的控制,拓展与抽象,比枚举更加灵活和方便了。

再例如前面网络的封装:

sealed class Result<out T : Any> {
data class Success<out T : Any>(val data: T) : Result<T>()
sealed class Error(val exception: Exception) : Result<Nothing>() {
class RecoverableError(exception: Exception) : Error(exception)
class NonRecoverableError(exception: Exception) : Error(exception)
}

object InProgress : Result<Nothing>()
}

通过Sealed Class可以很方便的对Error类型进行拓展,同时,增加新的状态也非常简单,更重要的是,通过IDE的自动补全功能,IDE可以自动生成各个条件分支,避免人工编码的遗漏。


收起阅读 »

LeakCanary源码分析

LeakCanary使用 LeakCanary是一个用于Android的内存泄漏检测库.本文从如下四点分析源码 检查哪些内存泄漏 检查内存泄漏的时机 如何判定内存泄漏 如何分析内存泄漏(只有一点点,可能跟没有一样) 内存泄漏误报 1.检查哪些内存泄漏 A...
继续阅读 »

LeakCanary使用


LeakCanary是一个用于Android的内存泄漏检测库.本文从如下四点分析源码



  • 检查哪些内存泄漏

  • 检查内存泄漏的时机

  • 如何判定内存泄漏

  • 如何分析内存泄漏(只有一点点,可能跟没有一样)

  • 内存泄漏误报


1.检查哪些内存泄漏


AddWatchers.png
AppWatcherInstaller继承于ContentProvider,调用时机是介于Application的attachBaseContext(Context)和 onCreate() 之间.通过这种方式初始化.


方法2manualInstall实现了默认参数watchersToInstall,通过这个方法我们看到Activity,FragmentAndViewModel,RootView,Service四个观察者


fun appDefaultWatchers(
application: Application,
reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List<InstallableWatcher> {
return listOf(
ActivityWatcher(application, reachabilityWatcher),
FragmentAndViewModelWatcher(application, reachabilityWatcher),
RootViewWatcher(reachabilityWatcher),
ServiceWatcher(reachabilityWatcher)
)
}

2.检查内存泄漏的时机


2.1 ActivityWatcher


activity触发OnDestory检查是否回收Activity实例


private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
reachabilityWatcher.expectWeaklyReachable(
activity, "${activity::class.java.name} received Activity#onDestroy() callback"
)
}
}

2.2 FragmentAndViewModelWatcher


fragment触发onFragmentDestroyed或onFragmentViewDestroyed检查是否可以回收Fragment实例

viewModel触发onClear检查是否可以回收ViewModel实例


123.png


2.2.1 检查哪些Fragment


由于Android现在有三种Fragment

androidx.fragment.app

android.app.fragment

android.support.v4.app.Fragment

leakCanary通过反射先去检查是否引入上面三种Fragment,如果有就反射创建对应的watcher加入到
fragmentDestroyWatchers中


private fun getWatcherIfAvailable(
fragmentClassName: String,
watcherClassName: String,
reachabilityWatcher: ReachabilityWatcher
): ((Activity) -> Unit)? {

return if (classAvailable(fragmentClassName) &&
classAvailable(watcherClassName)
) {
val watcherConstructor =
Class.forName(watcherClassName).getDeclaredConstructor(ReachabilityWatcher::class.java)
@Suppress("UNCHECKED_CAST")
watcherConstructor.newInstance(reachabilityWatcher) as (Activity) -> Unit
} else {
null
}
}

2.2.2 Fragment内存泄漏检查时机


(1)application注册activity生命周期回调

(2)当监听到ctivity被创建时,获取该activity的对应的fragmentManager创建fragment的生命周期观察者

(3)当onFragmentViewDestroyed/onFragmentDestroyed触发时,遍历集合然后检查是否可以回收Fragment实例


private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

override fun onFragmentViewDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
val view = fragment.view
if (view != null) {
reachabilityWatcher.expectWeaklyReachable(
view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
"(references to its views should be cleared to prevent leaks)"
)
}
}

override fun onFragmentDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
reachabilityWatcher.expectWeaklyReachable(
fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
)
}
}

2.2.3 检查哪些ViewModel内存泄漏


既然fragment/activity被销毁了,fragment/activity对象被回收了,那么fragment/activity绑定的所有viewmodel实例也应该销毁,所以leakCanary增加了viewmodel的内存检查

(1)监听当activity被创建时,绑定一个间谍viewmodel实例


//AndroidXFragmentDestroyWatcher
override fun invoke(activity: Activity) {
if (activity is FragmentActivity) {
val supportFragmentManager = activity.supportFragmentManager
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
ViewModelClearedWatcher.install(activity, reachabilityWatcher)
}
}

(2)监听当fragment被创建时,绑定一个间谍viewmodel实例


//AndroidXFragmentDestroyWatcher##fragmentLifecycleCallbacks
override fun onFragmentCreated(
fm: FragmentManager,
fragment: Fragment,
savedInstanceState: Bundle?
) {
ViewModelClearedWatcher.install(fragment, reachabilityWatcher)
}

2.2.4 ViewModel内存泄漏检查时机


(1)利用反射获得fragment/activity绑定的viewModel集合

(2)当leakcanary绑定的viewmodel生命周期走到onCleared时,就去检查所有viewmodel实例是否可以回收(这边就是为啥作者取名叫spy)


//ViewModelClearedWatcher
override fun onCleared() {
viewModelMap?.values?.forEach { viewModel ->
reachabilityWatcher.expectWeaklyReachable(
viewModel, "${viewModel::class.java.name} received ViewModel#onCleared() callback"
)
}
}

2.3 RootViewWatcher


view触发onViewDetachedFromWindow检查是否回收View实例

利用Curtains获得视图变化,检查所有被添加到phoneWindow上面的,windowLayoutParams.title为Toast或者是Tooltip,或者除PopupWindow之外的所有view.


//RootViewWatcher
rootView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {

val watchDetachedView = Runnable {
reachabilityWatcher.expectWeaklyReachable(
rootView, "${rootView::class.java.name} received View#onDetachedFromWindow() callback"
)
}

override fun onViewAttachedToWindow(v: View) {
WindowManager.LayoutParams.TYPE_PHONE
mainHandler.removeCallbacks(watchDetachedView)
}

override fun onViewDetachedFromWindow(v: View) {
mainHandler.post(watchDetachedView)
}
})

2.4 ServiceWatcher


service触发onDestroy检查是否回收Service实例


private fun onServiceDestroyed(token: IBinder) {
servicesToBeDestroyed.remove(token)?.also { serviceWeakReference ->
serviceWeakReference.get()?.let { service ->
reachabilityWatcher.expectWeaklyReachable(
service, "${service::class.java.name} received Service#onDestroy() callback"
)
}
}
}

3.如何判定内存泄漏


234.png
ReferenceQueue : 引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中


(1)将待检查对象加入到weakReference和watchedObjects中


@Synchronized override fun expectWeaklyReachable(
watchedObject: Any,
description: String
) {
if (!isEnabled()) {
return
}
removeWeaklyReachableObjects()
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference =
KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
SharkLog.d {
"Watching " +
(if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
(if (description.isNotEmpty()) " ($description)" else "") +
" with key $key"
}

watchedObjects[key] = reference
checkRetainedExecutor.execute {
moveToRetained(key)
}
}

(6)执行GC后,遍历ReferenceQueue,删除watchedObjects集合中保存的对象


private fun removeWeaklyReachableObjects() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}

(3)判断watchedObjects长度是否发生改变,如果改变就认为内存泄漏


private fun checkRetainedCount(
retainedKeysCount: Int,
retainedVisibleThreshold: Int,
nopeReason: String? = null
): Boolean {
val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
...
if (retainedKeysCount < retainedVisibleThreshold) {
if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
if (countChanged) {
onRetainInstanceListener.onEvent(BelowThreshold(retainedKeysCount))
}
showRetainedCountNotification(
objectCount = retainedKeysCount,
contentText = application.getString(
R.string.leak_canary_notification_retained_visible, retainedVisibleThreshold
)
)
scheduleRetainedObjectCheck(
delayMillis = WAIT_FOR_OBJECT_THRESHOLD_MILLIS
)
return true
}
}
return false
}

(10) 当检查到5次内存泄漏就会生成hprof文件


override fun dumpHeap(): DumpHeapResult {
...
val durationMillis = measureDurationMillis {
Debug.dumpHprofData(heapDumpFile.absolutePath)
}
...
}

4.如何分析内存泄漏


image.png
利用Shark分析工具分析hprof文件

(8)这里通过解析hprof文件生成heapAnalysis对象.SharkLog打印并存入数据库


override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {
SharkLog.d { "\u200B\n${LeakTraceWrapper.wrap(heapAnalysis.toString(), 120)}" }

val db = LeaksDbHelper(application).writableDatabase
val id = HeapAnalysisTable.insert(db, heapAnalysis)
db.releaseReference()
...
}

5.内存泄漏误报


Java虚拟机的主流垃圾回收器采取的是可达性分析算法,
可达性算法是通过从GC root往外遍历,如果从root节点无法遍历该节点表明该节点对应的对象处于可回收状态.
反之不会回收.


public class MainActivity2 extends FragmentActivity {
Fragment mFragmentA;
Fragment mFragmentB;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
mFragmentA = new FragmentA();
mFragmentB = new FragmentB();
findViewById(R.id.buttona).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
replaceFragment(mFragmentA);
}
});
findViewById(R.id.buttonb).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
replaceFragment(mFragmentB);
}
});
}
private void replaceFragment(Fragment fragment) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, fragment).commit();
}
}

以fragment为例,leakcanary认为fragment走onDestory了,就应该释放fragment.但是这种情况真的是内存泄漏么?


    ├─ com.example.MainActivity2 instance
│ Leaking: NO (Activity#mDestroyed is false)
│ ↓ MainActivity2.mFragmentA
│ ~~~~~~~~~~
╰→ com.example.FragmentA instance
Leaking: YES (ObjectWatcher was watching this because com.example.FragmentA
received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
key = 216c8cf8-2cdb-4509-84e9-8404afefffeb
watchDurationMillis = 3804
retainedDurationMillis = -1
key = eaa41c88-bccb-47ac-8fb7-46b27dec0356
watchDurationMillis = 6113
retainedDurationMillis = 1112
key = 77d5f271-382b-42ec-904b-1e8a6d4ab097
watchDurationMillis = 7423
retainedDurationMillis = 2423
key = 8d79952f-a300-4830-b513-62e40cda8bba
watchDurationMillis = 15771
retainedDurationMillis = 10765
13858 bytes retained by leaking objects
Signature: f1d17d3f6aa4713d4de15a4f465f97003aa7

根据堆栈信息,leakcanary认为fragmentA走了onDestory应该要回收这个fragmentA对象,但是发现还被MainActivity2对象持有无法回收,然后判定是内存泄漏. 放在我们这个逻辑里面,fragment不释放是对的.
只不过这种实现不是内存最佳罢了.


作者:no_one
链接:https://juejin.cn/post/7008425328964010020
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

内联函数产生原因和原理

为什么要内联函数因为: Kotlin为了书写简单,所以引入了lambda。但是: lambda会造成性能消耗。所以: 引入了内联函数来解决这个问题。如何证明lambda书写简单我们来实现个需求,diff一下有lambda和无lambda的代码便知。需求: 实现...
继续阅读 »

为什么要内联函数

  • 因为: Kotlin为了书写简单,所以引入了lambda。

  • 但是: lambda会造成性能消耗。

  • 所以: 引入了内联函数来解决这个问题。

如何证明lambda书写简单

我们来实现个需求,diff一下有lambda和无lambda的代码便知。

需求: 实现一个函数回调,回调一个String给我。

Java版本(无lambda):

// 首先需要定义一个回调
public interface Action {
void click(String fuck);
}

// 然后定义这个方法,参数就是回调的接口
public void func(Action action) {
String str = "hello";
action.click(str);
}

// 最后调用它
public static void main(String[] args) {
// 这里需要创建一个匿名类
func(new Action() {
@Override
public void click(String fuck) {
System.out.println(fuck);
}
});
}

然后我们来看kotlin版:

// 直接定义方法,参数是个表达式函数
fun func(action: (String) -> Unit) {
val str = "hello"
action.invoke(str)
}

// 调用
fun main() {
// 参数直接传入lambda就完事,it是lambda的默认参数
func { println(it) }
}

没有对比就没有伤害,java费了十牛三虎之力写了好几行,kotlin短短几行就实现了,这就是lambda的优点: 简洁省事。其实说白了就是:不用创建对象了。

虽然可读性差了点,管它呢,反正看不懂也是别人的事,别人读不懂才能凸显我的不可替代性。

事实证明,lambda确实大大简化了代码的书写过程,我们不用敲创建对象的代码了

那么,lambda有什么缺点呢?

lambda的缺点

lambda的最大缺点就是性能损耗!

让我们反编译上述kotlin代码来看:

// 这个参数已经被替换成Function1了,这个Function1是kotlin中定义的一个接口
public static final void func(@NotNull Function1 action) {
Intrinsics.checkNotNullParameter(action, "action");
String str = "hello";
action.invoke(str);
}

// main函数
public static final void main() {
// 这里其实是创建了一个匿名类
func((Function1)null.INSTANCE);
}

我们看到,kotlin中的lambda最终会在编译期变成一个匿名类,这跟java好像没什么区别啊,都是生成一个匿名类。为什么说kotlin的lambda效率低,因为:kotlin创建匿名类是在编译期

而java在1.7之后就引入了invokedynamic指令,java中的lambda在编译期会被替换为invokedynamic指令,在运行期,如果invokedynamic被调用,就会生成一个匿名类来替换这个指令,后续调用都是用这个匿名类来完成

说白了,对于java来说,如果lambda不被调用,就不会创建匿名类。而对于kotlin来说,不管lambda是否被调用,都会提前创建一个匿名类。这就等价于:java把创建匿名类的操作后置了,有需要才搞,这就变相节省了开销。因为创建匿名类会增加类个数和字节码大小。

那么,kotlin为什么不也这么干呢,为什么非要在编译时 就提前做 将来不一定用到的东西呢?因为kotlin需要兼容java6,java6是目前Android的主要开发语言,而invokedynamic又是在java7之后引入的...,mmp!

那么,kotlin怎么擦好这个屁股呢?使用内联函数!

内联函数的实现原理

还是上述代码,我们把func改成内联的,如下:

fun main() {
func { print(it) }
}

// 方法用inline修饰了
inline fun func(action: (String) -> Unit) {
val str = "hello"
action.invoke(str)
}

同样,我们反编译下看看:

// 这个函数没变化
public static final void func(@NotNull Function1 action) {
Intrinsics.checkNotNullParameter(action, "action");
String str = "hello";
action.invoke(str);
}

// 哦,调用方变了:直接把func函数体拷贝过来了,six six six
public static final void main() {
String str$iv = "hello";
System.out.print(str$iv);
}

我们看到,添加了inline后,kotlin会直接把被调用函数的函数体,复制到调用它的地方。

这样就不用创建匿名对象了!而且,还少一次调用过程。因为调用匿名对象的函数,本身还多一次调用呢。比如:

// 内联前
public void test(){
A a = new a();
a.hello(); // 这里调用一次hello()
}

// 内联后
public void test(){
// a.hello()的代码直接拷贝进来,不用调hello()了!
}

所以,内联牛逼,万岁万岁万万岁。

但是,内联也有缺点!比如,我现在有个内联函数test(),里面有1000行代码,如果有10个地方调用它,那么就会把它复制到这10个地方,这一下就是10000行。。。这就导致class文件变相增大,进而导致apk变大,用户看见就不想下了。

怎么办呢,那就不内联!也就是说:根据函数的大小,以及被调用次数的多少,来决定是否需要内联

这是个业务的决策问题,这里不再废话。

内联函数的其他规则

好,我们来看下内联函数的一些规则。

内联函数的局限性

内联函数作为参数,只能传递给另一个内联函数。比如:

// func2是非内联的
fun func2(action: (String) -> Unit) {

}

// func是内联的
inline fun func(action: (String) -> Unit) {
val str = "hello"
action.invoke(str)

// action此时是内联的,传递给非内联函数func2,就会报错
func2(action) // 报错
}

现在我们讲func2改为内联的:

// 将func2改为内联
inline fun func2(action: (String) -> Unit) {

}

// func是内联的
inline fun func(action: (String) -> Unit) {
val str = "hello"
action.invoke(str)

// 将action传递给另一个内联函数func2,正常
func2(action) // ok
}

如果,不希望修改func2()为内联的怎么办呢,此时可以使用noinline修饰action参数:

// func2是非内联的
fun func2(action: (String) -> Unit) {

}

// func是内联的,但是action被标记为非内联的
inline fun func(noinline action: (String) -> Unit) {
val str = "hello"
action.invoke(str)

// action此时是非内联的,可以传递给非内联函数func2
func2(action) // ok
}

内联函数引的非局部返回

局部返回

我们知道,一般函数调用的返回都是局部的,比如:

// 这里直接return,也就是返回到调用它的地方
fun tReturn() {
return
}

fun func() {
println("before")
// 调用了toRetrun()
tReturn()
println("after")
}

// 测试
fun main() {
func()
}

结果如下:

before
after

这是正常的,因为func()函数先打印before,然后调用tReturn(),tReturn()入栈,执行return,tReturn()出栈,回到func()函数,接着向下打印after。

但是,如果将func()声明为内联的,然后将tReturn()作为参数传入,那么func()方法体就变了,比如:

// func声明为内联的,然后传入action参数
inline fun func(action: () -> Unit) {
println("before")
action.invoke()
println("after")
}

fun main() {
// 参数跟tReturn一样
func { return }
}

结果:

before

原理也很简单,因为参数action会被复制到func()函数中,也就合并为一个方法了,等价于:

inline fun func() {
println("before")
return // 这就是参数action的函数体,直接返回了
println("after")
}

这个不难理解,那么,如果不加inline,只是修改参数为action可以吗,比如:

// 这里没有加inline 参数一样是action
fun func(action: () -> Unit) {
println("before")
action.invoke()
println("after")
}

fun main() {
func { return } // 报错
}

这会直接报错:

Kotlin: 'return' is not allowed here

这是不允许的,因为它不知道你要return到哪个地方,但是可以这样写:

fun main() {
// return 添加了标记,标记为返回到func这个地方
func { return@func }
}

结果:

before
after

综上,一句话: 普通函数参数的return都是局部返回的,而内联函数是全局返回的

那么,怎么防备这种风险呢,或者说: 怎么让一个函数既可以内联,又不让它的参数有全局返回的return呢?比如:

inline fun func(action: () -> Unit) {
println("before")
action() // 希望这里不要有return,有就直接报错
println("after")
}

使用crossinline即可!我们修改函数如下:

// 参数用crossinline修饰
inline fun func(crossinline action: () -> Unit) {
println("before")
action()
println("after")
}

// 调用
fun main() {
func { return } // 报错: Kotlin: 'return' is not allowed here
func { return@func } // 正常
}

可以看到,corssinline在保证函数是内联的情况下,限制了全局返回

总结

  • kotlin为了书写简洁,引入了lambda
  • 但是lambda有性能开销
  • 性能开销在java7优化了,但是kotlin兼容java6,无法享受这个优化
  • 所以kotlin引入内联来解决这个问题
  • 内联是在编译期将被调用的函数拷贝到调用方的函数体,从而避免创建内部类
  • 使用inline可以将函数声明为内联的,内联函数参数是全局返回的
  • 使用noinline可以修饰函数参数为不内联
  • 使用crossinline可以修饰函数参数为内联,而且不能全局返回


作者:奔波儿灞取经
链接:https://juejin.cn/post/7008422441026322462
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

相比 XML , Compose 性能到底怎么样?

前言 最近Compose已经正式发布了1.0版本,这说明谷歌认为Compose已经可以用于正式生产环境了 那么相比传统的XML,Compose的性能到底怎么样呢? 本文主要从构建性能与运行时两个方面来分析Compose的性能,数据主要来源于:Jetpack C...
继续阅读 »

前言


最近Compose已经正式发布了1.0版本,这说明谷歌认为Compose已经可以用于正式生产环境了

那么相比传统的XML,Compose的性能到底怎么样呢?


本文主要从构建性能与运行时两个方面来分析Compose的性能,数据主要来源于:Jetpack Compose — Before and afterMeasuring Render Performance with Jetpack Compose , 想了解更多的同学可以直接点击查看


构建性能


Compose构建性能主要以 tivi 为例来进行说明

Tivi是一个开源的电影App,原本基于FragmentXML构建,同时还使用了DataBinding等使用了注解处理器的框架

后来迁移到使用Compose构建UI,迁移过程分为两步



  1. 第一步:迁移到NavigationFragment,每个FragmentUI则由Compose构建

  2. 第二步:移除Fragment,完全基于Compose实现UI


下面我们就对Pre-Compose,Fragments + Compose,Entirely Compose三个阶段的性能进行分析对比


APK体积


包体积是我们经常关注的性能指标之一,我们一起看下3个阶段的包体积对比


p1.png

p2.png

可以看出,TiviAPK 大小缩减了 46%,从 4.49MB 缩减到 2.39MB,同时方法数也减少了17%


值得注意的是,在刚开始在应用中采用Compose时,有时您会发现APK大小反而变大了

这是因为迁移没有完成,老的依赖没有完成移除,而新的依赖已经添加了,导致APK体积变大

而在项目完全迁移到Compose后,APK 大小会减少,并且优于原始指标。


代码行数


我们知道在比较软件项目时,代码行数并不是一个特别有用的统计数据,但它确实提供了对事物如何变化的一个观察指标。

我们使用cloc工具来计算代码行数


cloc . --exclude-dir=build,.idea,schemas

结果如下图所示:


p4.png
可以看出,在迁移到Compose后,毫无意外的,XML代码行减少了76%

有趣的是kotlin代码同样减少了,可能是因为我们可以减少很多模板代码,同时也可以移除之前写的一些View Helper代码


构建速度


随着项目的不断变大,构建速度是开发人员越来越关心的一个指标。

在开始重构之前,我们知道,删除大量的注解处理器会有助于提高构建速度,但我们不确定会有多少。


我们运行以下命令5次,然后取平均值


./gradlew --profile --offline --rerun-tasks --max-workers=4 assembleDebug

结果如下


p3.png

这里考虑的是调试构建时间,您在开发期间会更关注此时间。


在迁移到Compose前,Tivi 的平均构建时间为 108.71 秒。

在完全迁移到 Compose 后,平均构建时间缩短至 76.96 秒!构建时间缩短了 29%

构建时间能缩短这么多,当然不仅仅是Compose的功劳,在很大程度上受两个因素的影响:



  1. 一个是移除了使用注解处理器的DataBindingEpoxy

  2. 另一个是HiltAGP 7.0 中的运行速度更快。


运行时性能


上面我们介绍了Compose在构建时的性能,下面来看下Compose在运行时渲染的性能怎么样


分析前的准备


使用Compose时,可能有多种影响性能的指标



  • 如果我们完全在Compose中构建UI会怎样?

  • 如果我们对复杂视图使用Compose(例如用 LazyColumn 替换 RecyclerViews),但根布局仍然添加在XML

  • 如果我们使用Compose替换页面中一个个元素,而不是整个页面,会怎么样?

  • 是否可调试和R8编译器对性能的影响有多大?


为了开始回答这些问题,我们构建了一个简单的测试程序。

在第一个版本中,我们添加了一个包含50个元素的列表(其中实际绘制了大约 12 个)。该列表包括一个单选按钮和一些随机文本。


p5.jpeg

为了测试各种选项的影响,我们添加以下4种配置,以下4种都是开启了R8同时关闭了debug



  1. 纯Compose

  2. 一个XML中,只带有一个ComposeView,具体布局写在Compose

  3. XML中只包含一个RecyclerView,但是RecyclerView的每一项是一个ComposeView

  4. XML


同时为了测试build type对性能的影响,也添加了以下3种配置



  1. Compose,关闭R8并打开debug

  2. Compose,关闭R8并关闭debug

  3. XML,关闭R8并打开debug


如何定义性能?


Compose运行时性能,我们一般理解的就是页面启动到用户看到内容的时间

因此下面几个时机对我们比较重要



  1. Activity启动时间,即onCreate

  2. Activity启动完成时间,即onResume

  3. Activity渲染绘制完成时间,即用户看到内容的时间


onCreateonResume的时机很容易掌握,重写系统方法即可,但如何获得Activity完全绘制的时间呢?

我们可以给页面根View添加一个ViewTreeObserver,然后记录最后一次onDraw调用的时间


使用Profile查看上面说的过程,如下分别为使用XML渲染与使用Compose渲染的具体过程,即从OnCreate到调用最后一次onDraw的过程


使用XML

使用Compose


渲染性能分析


知道了如何定义性能,我们就可以开始测试了



  1. 每次测试都在几台设备上运行,包括最近的旗舰、没有Google Play服务的设备和一些廉价手机。

  2. 每次测试在同一台手机上都会运行10次,因此我们不仅可以获取首次渲染时间,也可以获取二次渲染时间

  3. 测试Compose版本为1.0.0


我们根据上面定义的配置,重复跑了多次,得到了一些数据,感兴趣的同学可以直接查看所有数据


p8.png

分析结果如上图所示,我们可以得出一些结论



  • R8和是否可调试对Jetpack Compose渲染时间产生了显着影响。在每次实验中,禁用R8和启用可调试性的构建所花费的时间是没有它们的构建的两倍多。在我们最慢的设备上,R8 将渲染速度加快了半秒以上,而禁用debug又使渲染速度加快了半秒。

  • XML中只包含一个ComposeView的渲染时间,跟纯Compose的耗时差不多

  • RecyclerView中包含多个ComposeView是最慢的。这并不奇怪,在XML中使用ComposeView是有成本的,所以页面中使用的ComposeView越少越好。

  • XML在呈现方面比Compose更快。没有办法解决这个问题,在每种情况下,Compose 的渲染时间比 XML 长约 33%。

  • 第一次启动总是比后续启动花费更长的时间来渲染。如果您查看完整的数据,第一个页面的渲染时间几乎是后续的两倍。


比较让我惊讶的是,尽管Compose没有了将XML转化成ViewIO操作,测量过程也因为固有特性测量提高了测量效率,但性能仍然比XML要差

不过,根据Leland Richardson说法,当从Google Play安装应用程序时,由于捆绑的AOT编译,Compose 在启动时渲染会更快,从而进一步缩小了与XML的差距


总结


经过上面对Compose性能方面的分析,总结如下



  1. 如果完全迁移到Compose,在包体积,代码行数,编译速度等方面应该会有比较大的改善

  2. 如果处于迁移中的阶段,可能因为旧的依赖没有去除,而已经引入了新的依赖,反而导致包体积等变大

  3. 尽管没有了XML转换的IO操作,测量过程也通过固有特性测量进行了优化,Compose的渲染性能比起XML仍然有一定差距

  4. 尽管目前Compose在性能方面略有欠缺(在大多数设备上仅超过一两帧),但由于其在开发人员生产力、代码重用和声明式UI的强大特性等方面的优势,Compose仍被推荐使用

作者:RicardoMJiang
链接:https://juejin.cn/post/7008522702835154980
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android知识点之Service(四)

2、service的生命周期,两种启动模式的区别 (1)、通过startService()方法来启动服务 生命周期:onCreate() -> onStart()或者onStartCommand() -> onDestroy() onStart()...
继续阅读 »

2、service的生命周期,两种启动模式的区别


(1)、通过startService()方法来启动服务

生命周期:onCreate() -> onStart()或者onStartCommand() -> onDestroy()


onStart()方法是在android 4.1以上版本废弃,采用onStartCommand()方法代替,当服务已经启动时,调用startService()方法不会重复调用onCreate()方法(只有在启动时调用一次),但会调用onStart()或者onStartCommand()方法,这在SystemServer进程的ActiveServices的bringUpServiceLocked方法中有体现:

frameworks/base/services/core/java/com/android/server/am/ActiveServices.java


private String bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg,
boolean whileRestarting, boolean permissionsReviewRequired)
throws TransactionTooLargeException {
//如果服务已经启动,则进入此判断
if (r.app != null && r.app.thread != null) {
sendServiceArgsLocked(r, execInFg, false);
return null;
}
····
}

private final void sendServiceArgsLocked(ServiceRecord r, boolean execInFg,
boolean oomAdjusted) throws TransactionTooLargeException {
····
//通过跨进程调用应用进程的ActivityThread的ApplicationThread中的scheduleServiceArgs启动服务
//最终调用到应用服务的onStartCommand()方法
r.app.thread.scheduleServiceArgs(r, slice);
····
}

当不使用服务时,调用stopService()来关闭,此时会调用服务的onDestroy()方法


特点:



  • 服务运行与注册者无关联,注册者退出服务不会退出,除非调用stopService()方法


(2)、通过bindService()方法绑定服务

生命周期:onCreate() -> onBind() -> onUnbind() -> onDestroy()


当服务已经绑定,通过unbindService()解绑(没有最终调用spotService()退出服务情况下),此时onUnbind()返回true,再通过bindService()绑定服务,此时不会再调用onBind(),而是调用onRebind(),生命周期如下:


onCreate() -> onBind() -> 调用unbindService()解绑 -> onUnbind(true) -> 调用bindService()绑定 -> onRebind() -> onUnbind() -> onDestroy()


绑定的服务退出,绑定者退出或者调用unbindService()解绑退出


特点:



  • 绑定者可以通过服务内部自定义的Binder实现类来持有服务并且调用服务中的方法

  • 绑定者退出,那么服务也会跟着退出


3、service与activity怎么实现通信


1、通过Intent方式从Activity发送数据给Service

2、使用绑定服务的ServiceConnection通过Binder进行

3、内容提供者、存储的方式

4、广播

5、socket通信

6、全局静态变量方式

7、反射注入的方式(eventBus)


4、IntentService是什么?IntentService的原理?应用场景以及与service的区别


IntentService是一个可以执行耗时操作的服务,内部维护着HandlerThread封装的子线程消息队列来执行耗时任务,在任务执行完时调用stopSelf()方法自动退出服务


原理:

frameworks/base/core/java/android/app/IntentService.java


public abstract class IntentService extends Service {
····
//定义一个Handler对象用于接收与发送消息
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
//一个抽象方法,可以被继承者重写,运行在子线程消息队列中
onHandleIntent((Intent)msg.obj);
//任务执行完,自定关闭当前服务
stopSelf(msg.arg1);
}
}
·····

@Override
public void onCreate() {
super.onCreate();
//创建一个HandlerThread对象,HandlerThread是子线程消息循环队列
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();
mServiceLooper = thread.getLooper();
//创建一个子线程的Handler对象,发送的消息运行在子线程消息队列中
mServiceHandler = new ServiceHandler(mServiceLooper);
}

@Override
public void onStart(@android.annotation.Nullable Intent intent, int startId) {
//使用Handler向子线程消息循环队列发送一条消息
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

@Override
public int onStartCommand(@android.annotation.Nullable Intent intent, int flags, int startId) {
//发送消息运行在子线程消息队列中
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

//抽象方法,运行在子线程消息队列中
protected abstract void onHandleIntent(@android.annotation.Nullable Intent intent);
}

应用场景:



  • 后台耗时操作

  • 短暂的耗时服务,如下载资源等


IntentService与service区别:



  • service不能直接执行耗时任务否则会引起ANR,IntentService可以执行耗时任务

  • service启动后需要手动调用stopService()关闭服务,IntentService启动后在执行完任何后会自动关闭服务


5、Service的onStartCommand方法有几种返回值?各代表什么意思?


有四种返回值:




  • START_STICKY:当某个服务被系统杀死时(不是正常结束服务),如果返回值为START_STICKY,则系统会尝试重启该服务,并且调用服务的onStartCommand()方法,但是onStartCommand()方法的Intent参数为Null。




  • START_NOT_STICKY:当某个服务被系统杀死时,如果返回值为START_NOT_STICKY,则系统不会重启该服务。




  • START_REDELIVER_INTENT:当某个服务被系统杀死时,如果返回值为START_REDELIVER_INTENT,则系统会尝试重启该服务,并且调用服务的onStartCommand()方法,并且会创建之前启动服务时传入的Intent,即onStartCommand()方法的Intent参数不为Null。




  • START_STICKY_COMPATIBILITY:这是START_STICKY的兼容版本,不能保证onStartCommand()方法一定会被重新调用。




6、bindService和startService混合使用的生命周期以及怎么关闭?



  • 调用startService()


生命周期 : onCreate() -> onStart()或者onStartCommand() -> onDestroy()

通过调用stopService()方法关闭服务



  • 调用bindService()


生命周期:onCreate() -> onBind() -> onUnbind() -> onDestroy()

通过调用unbindService()方法关闭服务



  • 先调用startService()后再调用bindService()


生命周期:onCreate() -> onStart()或者onStartCommand() -> onBind() -> onUnbind() -> onDestroy()

先调用stopService()再调用unbindService()关闭服务



  • 先调用bindService()后再调用startService()


生命周期:onCreate() -> onBind() -> onStart()或者onStartCommand() -> onUnbind() -> onDestroy()

先调用unbindService()再调用stopService()关闭服务



  • 先调用bindService()后调用unbindService(),最后调用bindService()


生命周期:onCreate() -> onBind() -> 调用unbindService()解绑 -> onUnbind(true) -> 调用bindService()绑定 -> onRebind() -> onUnbind() -> onDestroy()

通过调用unbindService()方法关闭服务


作者:小狼人爱吃萝卜
链接:https://juejin.cn/post/7008699606372450341
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android知识点之Service(三)

(3)、异进程服务启动绑定流程 由(1)和(2)可以知道,无论是启动服务还是绑定服务,最终是通过SystemServer进程中的ActiveServices对象的bringUpServiceLocked方法来执行,bringUpServiceLocked方法是...
继续阅读 »
(3)、异进程服务启动绑定流程

(1)和(2)可以知道,无论是启动服务还是绑定服务,最终是通过SystemServer进程中的ActiveServices对象的bringUpServiceLocked方法来执行,bringUpServiceLocked方法是进程判断与启动新进程的入口,当在应用的AndroidManifest.xml文件中Service设置为新进程执行,那么就会在bringUpServiceLocked方法中启动一个新的进程


a、SystemServer进程与Zygote进程通信流程

android应用层次的进程都是通过Zygote进程去卵化启动的,Service进程也是一样
frameworks/base/services/core/java/com/android/server/am/ActiveServices.java


private String bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg,
boolean whileRestarting, boolean permissionsReviewRequired)
throws TransactionTooLargeException {
····
//启动一个新的进程
if (app == null && !permissionsReviewRequired) {
if ((app=mAm.startProcessLocked(procName, r.appInfo, true, intentFlags,
hostingRecord, ZYGOTE_POLICY_FLAG_EMPTY, false, isolated, false)) == null) {
·····
return msg;
}
if (isolated) {
r.isolatedProc = app;
}
}
····
return null;
}
复制代码

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java


final ProcessRecord startProcessLocked(String processName,
ApplicationInfo info, boolean knownToBeDead, int intentFlags,
HostingRecord hostingRecord, int zygotePolicyFlags, boolean allowWhileBooting,
boolean isolated, boolean keepIfLarge) {
//mProcessList是一个ProcessList对象
return mProcessList.startProcessLocked(processName, info, knownToBeDead, intentFlags,
hostingRecord, zygotePolicyFlags, allowWhileBooting, isolated, 0 /* isolatedUid */,
keepIfLarge, null /* ABI override */, null /* entryPoint */,
null /* entryPointArgs */, null /* crashHandler */);
}
复制代码

frameworks/base/services/core/java/com/android/server/am/ProcessList.java


boolean startProcessLocked(ProcessRecord app, HostingRecord hostingRecord,
int zygotePolicyFlags, boolean disableHiddenApiChecks, boolean disableTestApiChecks,
boolean mountExtStorageFull, String abiOverride) {
····
//新进程启动的入口类,即是android.app.ActivityThread类的main方法
final String entryPoint = "android.app.ActivityThread";
return startProcessLocked(hostingRecord, entryPoint, app, uid, gids,
runtimeFlags, zygotePolicyFlags, mountExternal, seInfo, requiredAbi,
instructionSet, invokeWith, startTime);
····
}

boolean startProcessLocked(HostingRecord hostingRecord, String entryPoint, ProcessRecord app,
int uid, int[] gids, int runtimeFlags, int zygotePolicyFlags, int mountExternal,
String seInfo, String requiredAbi, String instructionSet, String invokeWith,
long startTime) {
····
final Process.ProcessStartResult startResult = startProcess(hostingRecord,
entryPoint, app,
uid, gids, runtimeFlags, zygotePolicyFlags, mountExternal, seInfo,
requiredAbi, instructionSet, invokeWith, startTime);
····
}


private Process.ProcessStartResult startProcess(HostingRecord hostingRecord, String entryPoint,
ProcessRecord app, int uid, int[] gids, int runtimeFlags, int zygotePolicyFlags,
int mountExternal, String seInfo, String requiredAbi, String instructionSet,
String invokeWith, long startTime) {
····
//SystemServer进程与Zygote进程通信是通过LocalSocket和LocalServerSocket进行通信,并非是Binder
startResult = Process.start(entryPoint,
app.processName, uid, uid, gids, runtimeFlags, mountExternal,
app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
app.info.dataDir, invokeWith, app.info.packageName, zygotePolicyFlags,
isTopApp, app.mDisabledCompatChanges, pkgDataInfoMap,
whitelistedAppDataInfoMap, bindMountAppsData, bindMountAppStorageDirs,
new String[]{PROC_START_SEQ_IDENT + app.startSeq});
····
return startResult;
····
}
复制代码

SystemServer进程与Zygote进程通信是通过LocalSocket和LocalServerSocket进行通信,并非是Binder,最终Zygote进程启动好新进程则会调用android.app.ActivityThread类的main方法,此时新进程启动完成


b、Zygote进程与新启动进程通信流程

新进程启动完会调用android.app.ActivityThread类的main方法
frameworks/base/core/java/android/app/ActivityThread.java


public static void main(String[] args) {
····
Looper.prepareMainLooper();
····
//创建一个ActivityThread,同时创建一个ApplicationThread的binder对象,用于跨进程通讯
ActivityThread thread = new ActivityThread();
//启动一个Application
thread.attach(false, startSeq);
····
Looper.loop();
····
}

private void attach(boolean system, long startSeq) {
····
final IActivityManager mgr = ActivityManager.getService();
try {
//跨进程调用AMS的attachApplication方法
mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
····
}
复制代码

c、新启动进程与SystemServer进程(AMS)通信流程

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java


public final void attachApplication(IApplicationThread thread, long startSeq) {
····
synchronized (this) {
····
attachApplicationLocked(thread, callingPid, callingUid, startSeq);
····
}
}

private boolean attachApplicationLocked(@NonNull IApplicationThread thread,
int pid, int callingUid, long startSeq) {
·····
if (!badApp) {
try {
//启动服务,mServices为ActiveServices对象
didSomething |= mServices.attachApplicationLocked(app, processName);
·····
} catch (Exception e) {
·····
}
}
·····
return true;
}
复制代码

frameworks/base/services/core/java/com/android/server/am/ActiveServices.java


boolean attachApplicationLocked(ProcessRecord proc, String processName)
throws RemoteException {
····
//此时进程已经启动,启动服务
realStartServiceLocked(sr, proc, sr.createdFromFg);
····
return didSomething;
}
复制代码

realStartServiceLocked方法的执行源码流程,可以参考(1)和(2),realStartServiceLocked方法中主要是启动ANR超时任务监测,以及是启动服务还是绑定服务的生命周期调用分配,这样两种不同形式的服务独自执行各自的生命周期


(4)、服务启动超时(ANR)监测处理流程流程


Service生命周期的执行,最终通过ActiveServices对象的realStartServiceLocked方法去调配,此时也是加入了超时任务监测(ANR)


a、启动Server超时任务

frameworks/base/services/core/java/com/android/server/am/ActiveServices.java


private final void realStartServiceLocked(ServiceRecord r,
ProcessRecord app, boolean execInFg) throws RemoteException {
····
//启动超时定时ANR任务
bumpServiceExecutingLocked(r, execInFg, "create");
//接下来就是启动Server一系列操作,或者计算在Server中的onCreate()生命周期方法是否有耗时操作
····
try {
····
//进入应用进程启动,移除超时定时ANR任务操作也在此处
app.thread.scheduleCreateService(r, r.serviceInfo,
mAm.compatibilityInfoForPackage(r.serviceInfo.applicationInfo),
app.getReportedProcState());
····
//完成启动
created = true;
} catch (DeadObjectException e) {
····
}
···
}

private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {
····
//开启超时任务
scheduleServiceTimeoutLocked(r.app);
···
}

void scheduleServiceTimeoutLocked(ProcessRecord proc) {
····
//通过Handler发送一个定时任务,当时间到没有移除这个任务,则认为是超时
Message msg = mAm.mHandler.obtainMessage(
ActivityManagerService.SERVICE_TIMEOUT_MSG);
msg.obj = proc;
mAm.mHandler.sendMessageDelayed(msg,
proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
}
复制代码

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java


final class MainHandler extends Handler {
····
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
····
//当Server超时的时候,回调到此处,准备弹ANR窗口
case SERVICE_TIMEOUT_MSG: {
mServices.serviceTimeout((ProcessRecord)msg.obj);
} break;
···
//当内容提供者超时,回调到此处,准备弹ANR窗口
case CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG: {
ProcessRecord app = (ProcessRecord)msg.obj;
synchronized (ActivityManagerService.this) {
processContentProviderPublishTimedOutLocked(app);
}
} break;
····
}
}
}
复制代码

frameworks/base/services/core/java/com/android/server/am/ActiveServices.java


//前台server超时时间
static final int SERVICE_TIMEOUT = 20 * 1000 * Build.HW_TIMEOUT_MULTIPLIER;
//后台server超时时间
static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;

void serviceTimeout(ProcessRecord proc) {
String anrMessage = null;
synchronized(mAm) {
····
if (timeout != null && mAm.mProcessList.mLruProcesses.contains(proc)) {
····
//超时后来到此处anrMessage赋值
anrMessage = "executing service " + timeout.shortInstanceName;
} else {
//初始化定时,设定超时时间
Message msg = mAm.mHandler.obtainMessage(
ActivityManagerService.SERVICE_TIMEOUT_MSG);
msg.obj = proc;
mAm.mHandler.sendMessageAtTime(msg, proc.execServicesFg
? (nextTime+SERVICE_TIMEOUT) : (nextTime + SERVICE_BACKGROUND_TIMEOUT));
}
}
//当时间达到了server超时时间,则进入此处
if (anrMessage != null) {
//最终通过此处处理ANR
mAm.mAnrHelper.appNotResponding(proc, anrMessage);
}
}
复制代码

frameworks/base/services/core/java/com/android/server/am/AnrHelper.java


class AnrHelper {
private static class AnrRecord {
····
final ProcessRecord mApp;
····
void appNotResponding(boolean onlyDumpSelf) {
//通过一系列操作最终调用ProcessRecord对象的appNotResponding方法
mApp.appNotResponding(mActivityShortComponentName, mAppInfo,
mParentShortComponentName, mParentProcess, mAboveSystem, mAnnotation,
onlyDumpSelf);
}
}
}
复制代码

frameworks/base/services/core/java/com/android/server/am/ProcessRecord.java


void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,
String parentShortComponentName, WindowProcessController parentProcess,
boolean aboveSystem, String annotation, boolean onlyDumpSelf) {
····
synchronized (mService) {
····
//mService对应的是ActivityManagerService对象
if (mService.mUiHandler != null) {
// Bring up the infamous App Not Responding dialog
Message msg = Message.obtain();
msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
msg.obj = new AppNotRespondingDialog.Data(this, aInfo, aboveSystem);
//向UiHandler发送一条数据
mService.mUiHandler.sendMessage(msg);
}
}
}
复制代码

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java


final class UiHandler extends Handler {
···
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
····
case SHOW_NOT_RESPONDING_UI_MSG: {
//同Handler机制回调到此处弹起ANR窗口
mAppErrors.handleShowAnrUi(msg);
ensureBootCompleted();
} break;
·····
}
}
}
复制代码

b、移除Server超时任务

frameworks/base/core/java/android/app/ActivityThread.java


public final void scheduleCreateService(IBinder token,
ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
updateProcessState(processState, false);
CreateServiceData s = new CreateServiceData();
s.token = token;
s.info = info;
s.compatInfo = compatInfo;
//向主线程发送一条消息
sendMessage(H.CREATE_SERVICE, s);
}

class H extends Handler {
public void handleMessage(Message msg) {
····
switch (msg.what) {
····
case CREATE_SERVICE:
····
//主线程处理Server启动
handleCreateService((CreateServiceData) msg.obj);
····
break;
····
}
}
}

private void handleCreateService(CreateServiceData data) {
····
try {
····
service.attach(context, this, data.info.name, data.token, app,
ActivityManager.getService());
//调用Server生命周期方法
service.onCreate();
···
try {
//调用AMS的serviceDoneExecuting方法移除Server超时监测任务
ActivityManager.getService().serviceDoneExecuting(
data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
} catch (Exception e) {
····
}
}
复制代码

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java


public void serviceDoneExecuting(IBinder token, int type, int startId, int res) {
synchronized(this) {
//mServices对应的是ActiveServices对象,移除超时任务
mServices.serviceDoneExecutingLocked((ServiceRecord)token, type, startId, res);
}
}
复制代码

frameworks/base/services/core/java/com/android/server/am/ActiveServices.java


void serviceDoneExecutingLocked(ServiceRecord r, int type, int startId, int res) {
····
if (r != null) {
····
//移除超时任务
serviceDoneExecutingLocked(r, inDestroying, inDestroying);
····
}
···
}

private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying,
boolean finishing) {
····
if (r.executeNesting <= 0) {
if (r.app != null) {
···
if (r.app.executingServices.size() == 0) {
····
//向MainHandler移除SERVICE_TIMEOUT_MSG的超时任务
mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);
}
····
}
····
}
}
复制代码


作者:小狼人爱吃萝卜
链接:https://juejin.cn/post/7008699606372450341
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android知识点之Service(二)

(2)、绑定服务流程(同进程) a、应用内调用到AMS过程 在Activity环境下调用bindService方法 frameworks/base/core/java/android/content/ContextWrapper.java public boo...
继续阅读 »
(2)、绑定服务流程(同进程)

a、应用内调用到AMS过程

在Activity环境下调用bindService方法
frameworks/base/core/java/android/content/ContextWrapper.java


public boolean bindService(Intent service, ServiceConnection conn,
int flags) {
//mBase就是Activity在启动时在ActivityThread中创建的ContextImpl.java对象
return mBase.bindService(service, conn, flags);
}
复制代码

frameworks/base/core/java/android/app/ContextImpl.java


public boolean bindService(Intent service, ServiceConnection conn, int flags) {
····
return bindServiceCommon(service, conn, flags, null, mMainThread.getHandler(), null,
getUser());
}

private boolean bindServiceCommon(Intent service, ServiceConnection conn, int flags,
String instanceName, Handler handler, Executor executor, UserHandle user) {
IServiceConnection sd;
····
//创建一个IServiceConnection的binder实现对象,内部封装ServiceConnection对象
//该对象传输到AMS所在进程中,AMS通过IServiceConnection的binder实现对象可以调用应用中IServiceConnection实现方法
if (mPackageInfo != null) {
//当在绑定服务调用bindService方法有传入Executor对象,则executor不为null
if (executor != null) {
sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), executor, flags);
} else {
sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), handler, flags);
}
}
····
try {
····
//通过Binder机制跨进程调用AMS的bindIsolatedService方法
//sd为IServiceConnection对象,是一个Binder实现类,通过封装ServiceConnection得到
//sd的作用也是跨进程通信,当通过bindIsolatedService方法进入AMS进程处理时,
//AMS通过需要绑定的服务的`public IBinder onBind(Intent intent)`方法获取到一个自定义的Binder对象实现类,
//通过sd的connected方法将这个自定义的Binder实现类回传到应用进程ServiceConnection类的onServiceConnected方法中
//这个自定义的Binder实现类可以跨进程通信,绑定的服务是不同进程的可以通过这个参数跨进程通信
int res = ActivityManager.getService().bindIsolatedService(
mMainThread.getApplicationThread(), getActivityToken(), service,
service.resolveTypeIfNeeded(getContentResolver()),
sd, flags, instanceName, getOpPackageName(), user.getIdentifier());
····
return res != 0;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
复制代码

IServiceConnection的binder实现对象创建过程
frameworks/base/core/java/android/app/LoadedApk.java


public final IServiceConnection getServiceDispatcher(ServiceConnection c,
Context context, Handler handler, int flags) {
return getServiceDispatcherCommon(c, context, handler, null, flags);
}

public final IServiceConnection getServiceDispatcher(ServiceConnection c,
Context context, Executor executor, int flags) {
return getServiceDispatcherCommon(c, context, null, executor, flags);
}

private IServiceConnection getServiceDispatcherCommon(ServiceConnection c,
Context context, Handler handler, Executor executor, int flags) {
synchronized (mServices) {
·····
//当在绑定服务调用bindService方法有传入Executor对象,则executor不为null
if (executor != null) {
sd = new ServiceDispatcher(c, context, executor, flags);
} else {
sd = new ServiceDispatcher(c, context, handler, flags);
}
····
return sd.getIServiceConnection();
}
}

static final class ServiceDispatcher {

private final ServiceDispatcher.InnerConnection mIServiceConnection;

//创建出IServiceConnection对象
private static class InnerConnection extends IServiceConnection.Stub {
····
}

ServiceDispatcher(ServiceConnection conn,
Context context, Handler activityThread, int flags) {
mIServiceConnection = new InnerConnection(this);
····
}

ServiceDispatcher(ServiceConnection conn,
Context context, Executor activityExecutor, int flags) {
mIServiceConnection = new InnerConnection(this);
····
}

IServiceConnection getIServiceConnection() {
return mIServiceConnection;
}
}
复制代码

b、AMS处理以及调用ActivityThread过程

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java


public int bindIsolatedService(IApplicationThread caller, IBinder token, Intent service,
String resolvedType, IServiceConnection connection, int flags, String instanceName,
String callingPackage, int userId) throws TransactionTooLargeException {
····
synchronized(this) {
//mServices对应的是ActiveServices.java对象
//caller为ActivityThread中的ApplicationThread的Binder机制实现类,用于与应用进程通信
//connection为IServiceConnection的Binder机制实现类,用于与应用进程通信
return mServices.bindServiceLocked(caller, token, service,
resolvedType, connection, flags, instanceName, callingPackage, userId);
}
}
复制代码

frameworks/base/services/core/java/com/android/server/am/ActiveServices.java


int bindServiceLocked(IApplicationThread caller, IBinder token, Intent service,
String resolvedType, final IServiceConnection connection, int flags,
String instanceName, String callingPackage, final int userId)
throws TransactionTooLargeException {
····
//flags在绑定过程的时候传的是Context.BIND_AUTO_CREATE,即是1
if ((flags & Context.BIND_AUTO_CREATE) != 0) {
s.lastActivity = SystemClock.uptimeMillis();
//启动服务
if (bringUpServiceLocked(s, service.getFlags(), callerFg, false,
permissionsReviewRequired) != null) {
return 0;
}
}
····
return 1;
}

private String bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg,
boolean whileRestarting, boolean permissionsReviewRequired)
throws TransactionTooLargeException {
····
final boolean isolated = (r.serviceInfo.flags&ServiceInfo.FLAG_ISOLATED_PROCESS) != 0;
····
ProcessRecord app;
//判断启动的服务进程是否存在
if (!isolated) {
app = mAm.getProcessRecordLocked(procName, r.appInfo.uid, false);
···
if (app != null && app.thread != null) {
try {
····
//存在则直接启动服务
realStartServiceLocked(r, app, execInFg);
return null;
}
···
}
} else {
//启动的服务进程不存在,需要创建HostingRecord辅助开启创建进程
if ((r.serviceInfo.flags & ServiceInfo.FLAG_USE_APP_ZYGOTE) != 0) {
hostingRecord = HostingRecord.byAppZygote(r.instanceName, r.definingPackageName,
r.definingUid);
}
}
//启动的服务进程不存在,创建一个新的进程
if (app == null && !permissionsReviewRequired) {
//启动一个新进程
if ((app=mAm.startProcessLocked(procName, r.appInfo, true, intentFlags,
hostingRecord, ZYGOTE_POLICY_FLAG_EMPTY, false, isolated, false)) == null) {
····
}
····
}
····
return null;
}

private final void realStartServiceLocked(ServiceRecord r,
ProcessRecord app, boolean execInFg) throws RemoteException {
····
//启动ANR弹窗任务,当启动前台服务超过20s或者后台服务超过200s时,会弹ANR系统窗口提示应用无响应
bumpServiceExecutingLocked(r, execInFg, "create");
····
try {
····
//跨进程调用应用ActivityThread对象,进而调用service的onCreate生命周期方法
app.thread.scheduleCreateService(r, r.serviceInfo,
mAm.compatibilityInfoForPackage(r.serviceInfo.applicationInfo),
app.getReportedProcState());
····
created = true;
} catch (DeadObjectException e) {
····
} finally {
//启动出现异常,那么ANR弹窗任务需要移除掉
if (!created) {
····
serviceDoneExecutingLocked(r, inDestroying, inDestroying);
····
}
}
···
//如果是绑定服务,那么需要把自定义的Binder对象通过IServiceConnection回传
requestServiceBindingsLocked(r, execInFg);
···
}
复制代码

绑定服务是有自定义的Binder对象,这个需要通过AMS回传到应用进程中,也就是回传到ActivityThread里,看到requestServiceBindingsLocked方法的操作
frameworks/base/services/core/java/com/android/server/am/ActiveServices.java


private final void requestServiceBindingsLocked(ServiceRecord r, boolean execInFg)
throws TransactionTooLargeException {
for (int i=r.bindings.size()-1; i>=0; i--) {
//只有是绑定服务当前IntentBindRecord对象才有具体绑定的设置值
IntentBindRecord ibr = r.bindings.valueAt(i);
if (!requestServiceBindingLocked(r, ibr, execInFg, false)) {
break;
}
}
}

private final boolean requestServiceBindingLocked(ServiceRecord r, IntentBindRecord i,
boolean execInFg, boolean rebind) throws TransactionTooLargeException {
····
//防止重复绑定判断
if ((!i.requested || rebind) && i.apps.size() > 0) {
try {
···
//调用应用进程ActivityThread中的ApplicatonThread的scheduleBindService方法,进行数据回传
r.app.thread.scheduleBindService(r, i.intent.getIntent(), rebind,
r.app.getReportedProcState());
//防止重复绑定的限制
if (!rebind) {
i.requested = true;
}
····
} catch (TransactionTooLargeException e) {
····
//绑定出现异常,那么ANR弹窗任务需要移除掉
serviceDoneExecutingLocked(r, inDestroying, inDestroying);
throw e;
} catch (RemoteException e) {
···
//绑定出现异常,那么ANR弹窗任务需要移除掉
serviceDoneExecutingLocked(r, inDestroying, inDestroying);
return false;
}
}
return true;
}
复制代码

c、应用ActivityThread处理过程

service的onCreate生命周期方法流程
frameworks/base/core/java/android/app/ActivityThread.java


public final void scheduleCreateService(IBinder token,
ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
updateProcessState(processState, false);
CreateServiceData s = new CreateServiceData();
s.token = token;
s.info = info;
s.compatInfo = compatInfo;
//向主线程发送一条消息
sendMessage(H.CREATE_SERVICE, s);
}

class H extends Handler {
public void handleMessage(Message msg) {
····
switch (msg.what) {
····
case CREATE_SERVICE:
····
//主线程处理Server启动
handleCreateService((CreateServiceData) msg.obj);
····
break;
····
}
}
}

private void handleCreateService(CreateServiceData data) {
····
try {
····
service.attach(context, this, data.info.name, data.token, app,
ActivityManager.getService());
//调用Server生命周期方法
service.onCreate();
····
try {
//调用AMS的serviceDoneExecuting方法移除Server超时监测任务
ActivityManager.getService().serviceDoneExecuting(
data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
} catch (Exception e) {
····
}
}
复制代码

绑定service的onBind或者onRebind生命周期方法流程
frameworks/base/core/java/android/app/ActivityThread.java


public final void scheduleBindService(IBinder token, Intent intent,
boolean rebind, int processState) {
updateProcessState(processState, false);
BindServiceData s = new BindServiceData();
s.token = token;
s.intent = intent;
s.rebind = rebind;
....
//向主线程发送一条消息
sendMessage(H.BIND_SERVICE, s);
}

class H extends Handler {
public void handleMessage(Message msg) {
····
switch (msg.what) {
····
case BIND_SERVICE:
···
handleBindService((BindServiceData)msg.obj);
···
break;
····
}
}
}

private void handleBindService(BindServiceData data) {
Service s = mServices.get(data.token);
····
if (s != null) {
try {
····
try {
if (!data.rebind) {
//获取到服务自定义的Binder对象,也是调用onBind生命周期方法
IBinder binder = s.onBind(data.intent);
//调用AMS所在进程的publishService方法进一步处理
ActivityManager.getService().publishService(
data.token, data.intent, binder);
} else {
//重复绑定时执行的方法,代表已经绑定过
//调用onRebind生命周期方法
s.onRebind(data.intent);
//调用AMS所在进程的serviceDoneExecuting方法移除ANR任务
ActivityManager.getService().serviceDoneExecuting(
data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
}
}
····
} catch (Exception e) {
····
}
}
}
复制代码

d、AMS继续处理绑定服务内的Binder实现类流程

此过程的目的是将绑定的服务内部onBind方法自定义的Binder实现对象回传到应用进程ServiceConnection类的onServiceConnected方法中
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java


public void publishService(IBinder token, Intent intent, IBinder service) {
····
synchronized(this) {
····
//service是绑定服务内部onBind方法自定义的Binder实现类
//token是ActiveServices中创建的Binder实现类
mServices.publishServiceLocked((ServiceRecord)token, intent, service);
}
}
复制代码

frameworks/base/services/core/java/com/android/server/am/ActiveServices.java


void publishServiceLocked(ServiceRecord r, Intent intent, IBinder service) {
···
try {
····
if (r != null) {
····
if (b != null && !b.received) {
····
for (int conni = connections.size() - 1; conni >= 0; conni--) {
ArrayList<ConnectionRecord> clist = connections.valueAt(conni);
for (int i=0; i<clist.size(); i++) {
····
try {
//调用到应用层注册的IServiceConnection的binder实现对象
//IServiceConnection是在应用层的LoadedApk.java对象内部类ServiceDispatcher中的内部类InnerConnection实现的
//InnerConnection对象继承了IServiceConnection.Stub
c.conn.connected(r.name, service, false);
}
····
}
}
}
//移除ANR超时任务
serviceDoneExecutingLocked(r, mDestroyingServices.contains(r), false);
}
}
····
}
复制代码

e、应用进程处理绑定服务内的Binder实现类流程

frameworks/base/core/java/android/app/LoadedApk.java


static final class ServiceDispatcher {
//跨进程通信Binder实现类
private static class InnerConnection extends IServiceConnection.Stub {

····

//AMS所在的进程调用此方法把绑定服务内部onBind中实现的Binder对象回调到ServiceConnection注册者
public void connected(ComponentName name, IBinder service, boolean dead)
throws RemoteException {
LoadedApk.ServiceDispatcher sd = mDispatcher.get();
if (sd != null) {
sd.connected(name, service, dead);
}
}
}

public void connected(ComponentName name, IBinder service, boolean dead) {
//如果在应用绑定服务在bindService方法中有传Executor对象这个参数,那么mActivityExecutor就不为null
if (mActivityExecutor != null) {
mActivityExecutor.execute(new RunConnection(name, service, 0, dead));
} else if (mActivityThread != null) {
///如果在应用绑定服务在bindService方法中没有传Executor对象这个参数,
// 那么会通过mMainThread.getHandler()获取到ActivityThread中的H对象,此时mActivityThread不为null
mActivityThread.post(new RunConnection(name, service, 0, dead));
} else {
//基本不会调用到这里
doConnected(name, service, dead);
}
}

//定义一个
private final class RunConnection implements Runnable {
···
public void run() {
···
//调用此方法进一步处理
doConnected(mName, mService, mDead);
····
}
····
}

public void doConnected(ComponentName name, IBinder service, boolean dead) {
·····
//mConnection对应ServiceConnection对象,最终通过onServiceConnected将绑定服务的内部Binder对象返回给注册者
if (service != null) {
mConnection.onServiceConnected(name, service);
} else {
// The binding machinery worked, but the remote returned null from onBind().
mConnection.onNullBinding(name);
}
}
}
复制代码


作者:小狼人爱吃萝卜
链接:https://juejin.cn/post/7008699606372450341
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android知识点之Service(一)

1、service 启动流程(Android 11)(1)、启动服务流程(同进程)a、应用内调用到AMS过程在Activity环境下调用startService方法 frameworks/base/core/java/android/content/Conte...
继续阅读 »


1、service 启动流程(Android 11)

(1)、启动服务流程(同进程)

a、应用内调用到AMS过程

在Activity环境下调用startService方法 frameworks/base/core/java/android/content/ContextWrapper.java

public ComponentName startService(Intent service) {
//mBase就是Activity在启动时在ActivityThread中创建的ContextImpl.java对象
return mBase.startService(service);
}

frameworks/base/core/java/android/app/ContextImpl.java

public ComponentName startService(Intent service) {
····
return startServiceCommon(service, false, mUser);
}

private ComponentName startServiceCommon(Intent service, boolean requireForeground,
UserHandle user) {
try {
····
//通过Binder机制跨进程调用AMS的startService方法
ComponentName cn = ActivityManager.getService().startService(
mMainThread.getApplicationThread(), service,
service.resolveTypeIfNeeded(getContentResolver()), requireForeground,
getOpPackageName(), getAttributionTag(), user.getIdentifier());
····
return cn;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}

frameworks/base/core/java/android/app/ActivityManager.java

public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}

@UnsupportedAppUsage
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
//IActivityManager是Binder机制实现接口类
//ServiceManager是缓存Binder的实现对象,通过getService()可以获取到跨进程通信的Binder实现对象
//例如WMS、PMS等都可以通过这个ServiceManager对象获取到
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
};

至此,启动服务就进入了SystemServer进程继续工作

b、AMS处理以及调用ActivityThread过程

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

public ComponentName startService(IApplicationThread caller, Intent service,
String resolvedType, boolean requireForeground, String callingPackage,
String callingFeatureId, int userId)
throws TransactionTooLargeException {
····
synchronized(this) {
···
ComponentName res;
try {
//mServices对应的是ActiveServices.java对象
//caller为ActivityThread中的ApplicationThread的Binder机制实现类,用于与应用进程通信
res = mServices.startServiceLocked(caller, service,
resolvedType, callingPid, callingUid,
requireForeground, callingPackage, callingFeatureId, userId);
}
····
return res;
}
}

frameworks/base/services/core/java/com/android/server/am/ActiveServices.java

ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
int callingPid, int callingUid, boolean fgRequired, String callingPackage,
@Nullable String callingFeatureId, final int userId)
throws TransactionTooLargeException {
return startServiceLocked(caller, service, resolvedType, callingPid, callingUid, fgRequired,
callingPackage, callingFeatureId, userId, false);
}

ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
int callingPid, int callingUid, boolean fgRequired, String callingPackage,
@Nullable String callingFeatureId, final int userId,
boolean allowBackgroundActivityStarts) throws TransactionTooLargeException {
····
//一切准备就绪,调用此处来启动服务
ComponentName cmp = startServiceInnerLocked(smap, service, r, callerFg, addToStarting);
····
return cmp;
}

ComponentName startServiceInnerLocked(ServiceMap smap, Intent service, ServiceRecord r,
boolean callerFg, boolean addToStarting) throws TransactionTooLargeException {
····
//启动服务
String error = bringUpServiceLocked(r, service.getFlags(), callerFg, false, false);
····
return r.name;
}

private String bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg,
boolean whileRestarting, boolean permissionsReviewRequired)
throws TransactionTooLargeException {
····
final boolean isolated = (r.serviceInfo.flags&ServiceInfo.FLAG_ISOLATED_PROCESS) != 0;
····
ProcessRecord app;
//判断启动的服务进程是否存在
if (!isolated) {
app = mAm.getProcessRecordLocked(procName, r.appInfo.uid, false);
···
if (app != null && app.thread != null) {
try {
····
//存在则直接启动服务
realStartServiceLocked(r, app, execInFg);
return null;
}
···
}
} else {
//启动的服务进程不存在,需要创建HostingRecord辅助开启创建进程
if ((r.serviceInfo.flags & ServiceInfo.FLAG_USE_APP_ZYGOTE) != 0) {
hostingRecord = HostingRecord.byAppZygote(r.instanceName, r.definingPackageName,
r.definingUid);
}
}
//启动的服务进程不存在,创建一个新的进程
if (app == null && !permissionsReviewRequired) {
//启动一个新进程
if ((app=mAm.startProcessLocked(procName, r.appInfo, true, intentFlags,
hostingRecord, ZYGOTE_POLICY_FLAG_EMPTY, false, isolated, false)) == null) {
····
}
····
}
····
return null;
}

private final void realStartServiceLocked(ServiceRecord r,
ProcessRecord app, boolean execInFg) throws RemoteException {
····
//启动ANR弹窗任务,当启动前台服务超过20s或者后台服务超过200s时,会弹ANR系统窗口提示应用无响应
bumpServiceExecutingLocked(r, execInFg, "create");
····
try {
····
//跨进程调用应用ActivityThread对象,进而调用service的onCreate生命周期方法
app.thread.scheduleCreateService(r, r.serviceInfo,
mAm.compatibilityInfoForPackage(r.serviceInfo.applicationInfo),
app.getReportedProcState());
····
created = true;
} catch (DeadObjectException e) {
····
} finally {
//启动出现异常,那么ANR弹窗任务需要移除掉
if (!created) {
····
serviceDoneExecutingLocked(r, inDestroying, inDestroying);
····
}
}
//跨进程调用应用ActivityThread对象,进而调用service的onStartCommand生命周期方法
//绑定服务在这个方法中会限制调用,不会调用service的onStartCommand生命周期方法
sendServiceArgsLocked(r, execInFg, true);
····
}

private final void sendServiceArgsLocked(ServiceRecord r, boolean execInFg,
boolean oomAdjusted) throws TransactionTooLargeException {
····
while (r.pendingStarts.size() > 0) {
···
//启动ANR弹窗任务,当启动前台服务超过20s或者后台服务超过200s时,会弹ANR系统窗口提示应用无响应
bumpServiceExecutingLocked(r, execInFg, "start");
···
}
····
try {
//通过跨进程调用应用进程的ActivityThread的ApplicationThread中的scheduleServiceArgs启动服务
r.app.thread.scheduleServiceArgs(r, slice);
} catch (TransactionTooLargeException e) {
···
caughtException = e;
} catch (RemoteException e) {
····
caughtException = e;
} catch (Exception e) {
····
caughtException = e;
}
if (caughtException != null) {
···
for (int i = 0; i < args.size(); i++) {
//启动出现异常,那么ANR弹窗任务需要移除掉
serviceDoneExecutingLocked(r, inDestroying, inDestroying);
}
···
}
}

service启动在SystemServer进程需要判断是否需要创建新进程,还需要监测service启动的超时时间,除此之后还需要判断权限等等,最终通过Binder机制调用应用进程方法调用service生命周期方法

c、应用ActivityThread处理过程

service的onCreate生命周期方法流程 frameworks/base/core/java/android/app/ActivityThread.java

public final void scheduleCreateService(IBinder token,
ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
updateProcessState(processState, false);
CreateServiceData s = new CreateServiceData();
s.token = token;
s.info = info;
s.compatInfo = compatInfo;
//向主线程发送一条消息
sendMessage(H.CREATE_SERVICE, s);
}

class H extends Handler {
public void handleMessage(Message msg) {
····
switch (msg.what) {
····
case CREATE_SERVICE:
····
//主线程处理Server启动
handleCreateService((CreateServiceData) msg.obj);
····
break;
····
}
}
}

private void handleCreateService(CreateServiceData data) {
····
try {
····
service.attach(context, this, data.info.name, data.token, app,
ActivityManager.getService());
//调用Server生命周期方法
service.onCreate();
····
try {
//调用AMS的serviceDoneExecuting方法移除Server超时监测任务
ActivityManager.getService().serviceDoneExecuting(
data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
} catch (Exception e) {
····
}
}

service的onStartCommand生命周期方法流程 frameworks/base/core/java/android/app/ActivityThread.java

public final void scheduleServiceArgs(IBinder token, ParceledListSlice args) {
List<ServiceStartArgs> list = args.getList();
for (int i = 0; i < list.size(); i++) {
ServiceStartArgs ssa = list.get(i);
ServiceArgsData s = new ServiceArgsData();
s.token = token;
s.taskRemoved = ssa.taskRemoved;
s.startId = ssa.startId;
s.flags = ssa.flags;
s.args = ssa.args;
//向主线程发送一条消息
sendMessage(H.SERVICE_ARGS, s);
}
}

class H extends Handler {
public void handleMessage(Message msg) {
····
switch (msg.what) {
····
case SERVICE_ARGS:
····
//处理服务启动
handleServiceArgs((ServiceArgsData) msg.obj);
····
break;
····
}
}
}

private void handleServiceArgs(ServiceArgsData data) {
Service s = mServices.get(data.token);
if (s != null) {
try {
····
if (!data.taskRemoved) {
//调用service的onStartCommand生命周期方法
res = s.onStartCommand(data.args, data.flags, data.startId);
}
····
try {
//跨进程调用AMS方法将ANR任务移除
ActivityManager.getService().serviceDoneExecuting(
data.token, SERVICE_DONE_EXECUTING_START, data.startId, res);
}
···
} catch (Exception e) {
····
}
}
}

收起阅读 »

Flutter手势探索——原理与实现的背后

作者:闲鱼技术——子东在日常开发中,手势和事件无处不在,比如在 Flutter 应用中点击一个点赞按钮,长按弹出 BottomSheet 和商品列表的滑动等等都存在事件传递和手势识别,Flutter 内部是如何确定哪个控件响应了事件,事件是如何在控件之间传递的...
继续阅读 »

作者:闲鱼技术——子东

在日常开发中,手势和事件无处不在,比如在 Flutter 应用中点击一个点赞按钮,长按弹出 BottomSheet 和商品列表的滑动等等都存在事件传递和手势识别,Flutter 内部是如何确定哪个控件响应了事件,事件是如何在控件之间传递的,包括像 Tap 和 DoubleTap 等手势是如何区分的。为了回答以上的问题,我们接下来深入探索 Flutter 手势的原理。

手势原理

事件分发

Flutter 中的事件是从 Window.onPointerDataPacket 的回调中获取的,将原始事件转化成 PointerEvent 加入到待处理的事件队列中,然后逐个处理队列中的 PointerEvent。

其中 _handlePointerEvent 将生成 HitTestResult 将所有的命中测试结果存在 _path (HitTestResult 中的一个命中测试对象的集合),最后遍历 HitTestResult 的 _path 进行事件分发。

命中测试

那么 HitTestResult 是如何收集这些命中测试结果的呢,与 Native 的 HitTest 类似,Flutter 中也是不断在遍历(调用 HitTest)child 判断 point 和 child 的大小比较直到找到最深一个 child 也就是离我们最近的一个 RenderBox。如果把 Widget 的结构理解成树的结构,那么 _path 中 entry 的顺序正好是从叶子节点往根节点回溯的顺序。

手势识别

了解了 Flutter 的事件分发与命中测试,接下来我们看看手势是如何识别。在 Flutter 提供了一个封装各种手势监听的 Widget —— GestureDetector,其内部实现了各种手势识别器和其回调,然后传给 RawGestureDetector 。在 RawGestureDetector 里监听了 PointerDownEvent 事件,并遍历所有识别器并调用 addPointer 方法。

我们以最简单的识别器 TapGestureRecognizer 为例,先了解 addPointer 的实现中做了哪些事情,最终调用 startTrackingPointer 方法,在事件路由里注册 handleEvent,并将其加入到竞争场(后面会讲手势竞争)中。当事件分发时根据 pointer 调用对应的 handleEvent 方法。在 handleEvent 方法实现中判断 pointer 的移动距离是否超过阈值,这个阈值的默认大小是 18 个像素点。如果超过这个阈值将拒绝事件并停止事件追踪。反之调用 TapGestureRecognizer 识别器实现的 handlePrimaryPointer,最终处理监听的回调。

手势竞争

当我们同时使用多种手势时会产生冲突,为了解决这个问题,Flutter 引入了 GestureArena(手势竞争场)的概念。在处理多种手势时把这些手势加入到竞争场中,胜出的手势会继续响应接下来的事件。 在手势竞争场中胜出者遵循两个规律:

•在竞争场中只存在一个手势识别器时,它将胜出。•当有一个手势识别器胜出,那么其他的都将失败。

举个例子,在一个 Widget 上同时监听 Horizontal 和 Vertical 手势时,当手指按下的时候两者都会进入手势竞争场,当用户手指在水平方向上移动一定距离,Horizontal 手势将胜出并响应事件。相同的,用户手指在垂直方向上移动 Vertical 手势胜出。

小结

上面分析了在 Flutter 中从事件分发到手势识别的原理,其中以 TapGestureRecognizer 为例介绍了手势识别,除了此以外还有 ScaleGestureRecognizer,PanGestureRecognizer 等等,识别这些手势的原理基本相同,重写 handleEvent 实现各自具体手势判断。接下来具体介绍在实际项目中遇到的手势冲突问题以及解决方案。

案例分析

近期团队正在优化图片浏览器的用户体验。我们与 UED 共同梳理了实现一个图片浏览器所包含的功能点:

1.点击关闭图片2.支持左右滑动切换图片3.支持双击放大4.长按唤起更多操作 ... ...

从上面的功能点分析之后,我们采用 Flutter 的系统控件 PageView 作为图片浏览器的基础组件,在其基础之上扩展出图片放大、双击和长按等手势。所以组件的框架图如下所示:

在 PageView 的 ItemView 使用 ImageWrapper 封装之后接管 ItemView 的手势来处理自定义的手势,比如缩放 ScaleGestureRecognizer 和 TapGestureRecognizer 等等。 从上面的框架图看,基于系统控件 PageView 的框架分层比较简单,尽可能利用系统控件原有的功能,即能减少实现复杂逻辑的实现,同时也避免了在多种系统和设备上的兼容性问题。在这个过程中也遇到一些手势冲突的问题。

图片放大滚动与 PageView 滑动的冲突

分析冲突原因:在 ImageWrapper 中使用 ScaleGestureRecognizer 追踪缩放事件。PageView 是在 Scrollable 的基础上实现的,Scrollable 则是利用 HorizontalDragGestureRecognizer 追踪水平拖拽事件来实现滑动。Scale 和 HorizontalDrag 同时存在必然会发生竞争,因为在水平滑动时 HorizontalDrag 手势胜出,图片无法滚动直接滑到下一页。 通过上面的分析,我们需要解决两个问题:

•图片支持滚动•图片滚动到边界时滑到下一页

一个简单的想法是在图片放大时禁止 PageView 滑动(PageView 的 physics 设置为 NeverScrollableScrollPhysics),当放大图片滚动到边界时允许 PageView 滑动下一页。该方案在实现之后,发现滚动到边界时与 PageView 滑动到下一页两者衔接的体验并不流畅。 从上面对 PageView 的源码分析,在 ImageWrapper 中实现 HorizontalDragGestureRecognizer 手势拦截了 PageView 内部的水平拖拽手势,图片放大时通过 Scale 手势回调计算位置(图片移动),当图片移动到边界时,将手势描述(DragStartDetails)传给外部的 PageView,在回调中 PageController 的 ScrollPosition 生成一个 Drag,紧接着 DragUpdateDetails 用于 drag 对象的更新。需要注意在手势事件结束时需要调用 drag.end 保持手势事件的完整性。这种方法较完美的解决了上面冲突的问题,并且通过 Flutter 自身提供的方法实现,在 HorizontalDrag 手势结束时 PageController 会处理这部分滑动的动画。

Scale 手势与 HorizontalDrag 手势的冲突

在极端的情况下,双指不同时接触到屏幕,并且至少有一根手指是横向移动,图片缩放和位置会出现异常。通过上面的竞争分析,在其中一根手指出现横向滑动的时,HorizontalDrag 在竞争中胜出,此处图片的位置会被 HorizontalDrag 手势的回调改变(图片浏览器 ImageWrapper 实现是在 Scale 和 HorizontalDrag 手势回调中协同控制图片的缩放和位移)。 由于两个手势在以上的情况下会互相切换导致异常。首先将 Scale 和 HorizontalDrag 两个手势的职责划分清楚,HorizontalDrag 的回调处理图片滚动到边界时将 Drag 事件抛出给 PageView 的 PageController 处理;Scale 的回调只处理缩放和除边界以外的位移。划分清职责之后,让两个手势同时存在那么就不存在竞争胜出者的切换的问题,那么图片缩放和位置会也就不会出现异常。 通过继承 ScaleGestureRecognizer 重写 rejectGesture 方法强制让 Scale 手势生效。从 GestureArena 的源码分析,rejectGesture 方法只在竞争结束之后收尾处理调用的,所以不会影响竞争场的竞争。并且重写 rejectGesture 方法之后可以继续追踪事件(ScaleGestureRecognizer 中 rejectGesture 实现是停止事件追踪)。

小结

解决完上面两个比较棘手的冲突问题,图片浏览器组件的雏形也有了。由于篇幅原因,很多实现的细节没有一一列举,比如如何去计算边界,图片移动距离计算等等。在解决上面的问题也花费一定的时间,在解决问题没有思路可能要回归到问题本身,拆解问题,再逐个突破。好在 Flutter 是开源的,我们可以通过源码找到问题解决的思路和方法。希望以上的解决方案能帮助到开发者,提供解决问题的思路。

展望

图片浏览器想要更好体验接下来还需要对交互细节和临界状态处理更加细致。比如在图片放大之后滚动支持一定的加速度;图片放大之后滚动到边缘时增加阻尼等等。要想极致的用户体验,这几个内容都是我们将来可能要探索的方向。


作者:闲鱼技术
链接:https://juejin.cn/post/7007996934757548069
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

实践 | Google I/O 应用是如何适配大尺寸屏幕 UI 的?

5 月 18 日至 20 日,我们以完全线上的形式举办了 Google 每年一度的 I/O 开发者大会,其中包括 112 场会议、151 个 Codelab、79 场开发者聚会、29 场研讨会,以及众多令人兴奋的发布。尽管今年的大会没有发布新版的 Google...
继续阅读 »

5 月 18 日至 20 日,我们以完全线上的形式举办了 Google 每年一度的 I/O 开发者大会,其中包括 112 场会议、151 个 Codelab、79 场开发者聚会、29 场研讨会,以及众多令人兴奋的发布。尽管今年的大会没有发布新版的 Google I/O 应用,我们仍然更新了代码库来展示时下 Android 开发最新的一些特性和趋势。


应用在大尺寸屏幕 (平板、可折叠设备甚至是 Chrome OS 和台式个人电脑) 上的使用体验是我们的关注点之一: 在过去的一年中,大尺寸屏幕的设备越来越受欢迎,用户使用率也越来越高,如今已增长到 2.5 亿台活跃设备了。因此,让应用能充分利用额外的屏幕空间显得尤其重要。本文将展示我们为了让 Google I/O 应用在大尺寸屏幕上更好地显示而用到的一些技巧。


响应式导航


在平板电脑这类宽屏幕设备或者横屏手机上,用户们通常握持着设备的两侧,于是用户的拇指更容易触及侧边附近的区域。同时,由于有了额外的横向空间,导航元素从底部移至侧边也显得更加自然。为了实现这种符合人体工程学的改变,我们在用于 Android 平台的 Material Components 中新增了 Navigation rail


△ 左图: 竖屏模式下的底部导航。右图: 横屏模式下的 navigation rail。


△ 左图: 竖屏模式下的底部导航。右图: 横屏模式下的 navigation rail。


Google I/O 应用在主 Activity 中使用了两个不同的布局,其中包含了我们的人体工程学导航。其中在 res/layout 目录下的布局中包含了 BottomNavigationView,而在 res/layout-w720dp 目录下的布局中则包含了 NavigationRailView。在程序运行过程中,我们可以通过 Kotlin 的安全调用操作符 (?.) 来根据当前的设备配置确定呈现给用户哪一个视图。


private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)

// 根据配置不同,可能存在下面两种导航视图之一。
binding.bottomNavigation?.apply {
configureNavMenu(menu)
setupWithNavController(navController)
setOnItemReselectedListener { } // 避免导航到同一目的界面。
}
binding.navigationRail?.apply {
configureNavMenu(menu)
setupWithNavController(navController)
setOnItemReselectedListener { } // 避免导航到同一目的界面。
}
...
}

小贴士: 即使您不需要数据绑定的所有功能,您仍然可以使用 视图绑定 来为您的布局生成绑定类,这样就能避免调用 findViewById 了。


单窗格还是双窗格


在日程功能中,我们用列表-详情的模式来展示信息的层次。在宽屏幕设备上,显示区域被划分为左侧的会议列表和右侧的所选会议详细信息。这种布局方式带来的一个特别的挑战是,同一台设备在不同的配置下可能有不同的最佳显示方式,比如平板电脑竖屏对比横屏显示就有差异。由于 Google I/O 应用使用了 Jetpack Navigation 实现不同界面之间的切换,这个挑战对导航图有怎样的影响,我们又该如何记录当前屏幕上的内容呢?


△ 左图: 平板电脑的竖屏模式 (单窗格)。右图: 平板电脑的横屏模式 (双窗格)。


△ 左图: 平板电脑的竖屏模式 (单窗格)。右图: 平板电脑的横屏模式 (双窗格)。


我们采用了 SlidingPaneLayout,它为上述问题提供了一个直观的解决方案。双窗格会一直存在,但根据屏幕的尺寸,第二窗格可能不会显示在可视范围当中。只有在给定的窗格宽度下仍然有足够的空间时,SlidingPaneLayout 才会同时将两者显示出来。我们分别为会议列表和详情窗格分配了 400dp 和 600dp 的宽度。经过一些实验,我们发现即使是在大屏幕的平板上,竖屏模式同时显示出双窗格内容会使得信息的显示过于密集,所以这两个宽度值可以保证只在横屏模式下才同时展现全部窗格的内容。


至于导航图,日程的目的地页面现在是双窗格 Fragment,而每个窗格中可以展示的目的地都已经被迁移到新的导航图中了。我们可以用某窗格的 NavController 来管理该窗格内包含的各个目的页面,比如会议详情、讲师详情。不过,我们不能直接从会议列表导航到会议详情,因为两者如今已经被放到了不同的窗格中,也就是存在于不同的导航图里。


我们的替代方案是让会议列表和双窗格 Fragment 共享同一个 ViewModel,其中又包含了一个 Kotlin 数据流。每当用户从列表选中一个会议,我们会向数据流发送一个事件,随后双窗格 Fragment 就可以收集此事件,进而转发到会议详情窗格的 NavController:


val detailPaneNavController = 
(childFragmentManager.findFragmentById(R.id.detail_pane) as NavHostFragment)
.navController
scheduleTwoPaneViewModel.selectSessionEvents.collect { sessionId ->
detailPaneNavController.navigate(
ScheduleDetailNavGraphDirections.toSessionDetail(sessionId)
)
// 在窄屏幕设备上,如果会议详情窗格尚未处于最顶端时,将其滑入并遮挡在列表上方。
// 如果两个窗格都已经可见,则不会产生执行效果。
binding.slidingPaneLayout.open()
}

正如上面的代码中调用 slidingPaneLayout.open() 那样,在窄屏幕设备上,滑入显示详情窗格已经成为了导航过程中的用户可见部分。我们也必须要将详情窗格滑出,从而通过其他方式 "返回" 会议列表。由于双窗格 Fragment 中的各个目的页面已经不属于应用主导航图的一部分了,因此我们无法通过按设备上的后退按钮在窗格内自动向后导航,也就是说,我们需要实现这个功能。


上面这些情况都可以在 OnBackPressedCallback 中处理,这个回调在双窗格 Fragment 的 onViewCreated() 方法执行时会被注册 (您可以在这里了解更多关于添加 自定义导航 的内容)。这个回调会监听滑动窗格的移动以及关注各个窗格导航目的页面的变化,因此它能够评估下一次按下返回键时应该如何处理。


class ScheduleBackPressCallback(
private val slidingPaneLayout: SlidingPaneLayout,
private val listPaneNavController: NavController,
private val detailPaneNavController: NavController
) : OnBackPressedCallback(false),
SlidingPaneLayout.PanelSlideListener,
NavController.OnDestinationChangedListener {

init {
// 监听滑动窗格的移动。
slidingPaneLayout.addPanelSlideListener(this)
// 监听两个窗格内导航目的页面的变化。
listPaneNavController.addOnDestinationChangedListener(this)
detailPaneNavController.addOnDestinationChangedListener(this)
}

override fun handleOnBackPressed() {
// 按下返回有三种可能的效果,我们按顺序检查:
// 1. 当前正在详情窗格,从讲师详情返回会议详情。
val listDestination = listPaneNavController.currentDestination?.id
val detailDestination = detailPaneNavController.currentDestination?.id
var done = false
if (detailDestination == R.id.navigation_speaker_detail) {
done = detailPaneNavController.popBackStack()
}
// 2. 当前在窄屏幕设备上,如果详情页正在顶层,尝试将其滑出。
if (!done) {
done = slidingPaneLayout.closePane()
}
// 3. 当前在列表窗格,从搜索结果返回会议列表。
if (!done && listDestination == R.id.navigation_schedule_search) {
listPaneNavController.popBackStack()
}

syncEnabledState()
}

// 对于其他必要的覆写,只需要调用 syncEnabledState()。

private fun syncEnabledState() {
val listDestination = listPaneNavController.currentDestination?.id
val detailDestination = detailPaneNavController.currentDestination?.id
isEnabled = listDestination == R.id.navigation_schedule_search ||
detailDestination == R.id.navigation_speaker_detail ||
(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen)
}
}

SlidingPaneLayout 最近也针对可折叠设备进行了优化更新。更多关于使用 SlidingPaneLayout 的信息,请参阅: 创建双窗格布局


资源限定符的局限


搜索应用栏也在不同屏幕内容下显示不同内容。当您在搜索时,可以选择不同的标签来过滤需要显示的搜索结果,我们也会把当前生效的过滤标签显示在以下两个位置之一: 窄模式时位于搜索文本框下方,宽模式时位于搜索文本框的后面。可能有些反直觉的是,当平板电脑横屏时属于窄尺寸模式,而当其竖屏使用时属于宽尺寸模式。


△ 平板横屏时的搜索应用栏 (窄模式)


△ 平板横屏时的搜索应用栏 (窄模式)


△ 平板竖屏时的搜索应用栏 (宽模式)


△ 平板竖屏时的搜索应用栏 (宽模式)


此前,我们通过在搜索 Fragment 的视图层次中的应用栏部分使用 <include> 标签,并提供两种不同版本的布局来实现此功能,其中一个被限定为 layout-w720dp 这样的规格。如今此方法行不通了,因为在那种情况下,带有这些限定符的布局或是其他资源文件都会被按照整屏幕宽度解析,但事实上我们只关心那个特定窗格的宽度。


要实现这一特性,请参阅搜索 布局 的应用栏部分代码。请注意两个 ViewStub 元素 (第 27 和 28 行)。


<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
... >

<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?actionBarSize">

<!-- Toolbar 不支持 layout_weight,所以我们引入一个中间布局 LinearLayout。-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:showDividers="middle"
... >

<SearchView
android:id="@+id/searchView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
... />

<!-- 宽尺寸时过滤标签的 ViewStub。-->
<ViewStub
android:id="@+id/active_filters_wide_stub"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:layout="@layout/search_active_filters_wide"
... />
</LinearLayout>
</androidx.appcompat.widget.Toolbar>

<!-- 窄尺寸时过滤标签的 ViewStub。-->
<ViewStub
android:id="@+id/active_filters_narrow_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/search_active_filters_narrow"
... />
</com.google.android.material.appbar.AppBarLayout>

两个 ViewStub 各自指向不同的布局,但都只包含了一个 RecyclerView (虽然属性略有不同)。这些桩 (stub) 在运行时直到内容 inflate 之前都不会占据可视空间。剩下要做的就是当我们知道窗格有多宽之后,选择要 inflate 的桩。所以我们只需要使用 doOnNextLayout 扩展函数,等待 onViewCreated() 中对 AppBarLayout 进行首次布局即可。


binding.appbar.doOnNextLayout { appbar ->
if (appbar.width >= WIDE_TOOLBAR_THRESHOLD) {
binding.activeFiltersWideStub.viewStub?.apply {
setOnInflateListener { _, inflated ->
SearchActiveFiltersWideBinding.bind(inflated).apply {
viewModel = searchViewModel
lifecycleOwner = viewLifecycleOwner
}
}
inflate()
}
} else {
binding.activeFiltersNarrowStub.viewStub?.apply {
setOnInflateListener { _, inflated ->
SearchActiveFiltersNarrowBinding.bind(inflated).apply {
viewModel = searchViewModel
lifecycleOwner = viewLifecycleOwner
}
}
inflate()
}
}
}

转换空间


Android 一直都可以创建在多种屏幕尺寸上可用的布局,这都是由 match_parent 尺寸值、资源限定符和诸如 ConstraintLayout 的库来实现的。然而,这并不总是能在特定屏幕尺寸下为用户带来最佳的体验。当 UI 元素拉伸过度、相距过远或是过于密集时,往往难以传达信息,触控元素也变得难以辨识,并导致应用的可用性受到影响。


对于类似 "Settings" (设置) 这样的功能,我们的短列表项在宽屏幕上会被拉伸地很严重。由于这些列表项本身不太可能有新的布局方式,我们可以通过 ConstraintLayout 限制列表宽度来解决。


<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view"
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintWidth_percent="@dimen/content_max_width_percent">

<!-- 设置项……-->

</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

在第 10 行,@dimen/content_max_width_percent 是一个浮点数类型的尺寸值,根据不同的屏幕宽度可能有不同的值。这些值从小屏幕的 1.0 开始渐渐减少到宽屏幕的 0.6,所以当屏幕变宽,UI 元素也不会因为拉伸过度而产生割裂感。


△ 宽屏幕设备上的设置界面


△ 宽屏幕设备上的设置界面


请您阅读这则关于支持不同屏幕尺寸的 指南,获得常见尺寸分界点的参考信息。


转换内容


Codelabs 功能与设置功能有相似的结构。但我们想要充分利用额外的屏幕空间,而不是限制显示内容的宽度。在窄屏幕设备上,您会看到一列项目,它们会在点击时展开或折叠。在宽尺寸屏幕上,这些列表项会转换为一格一格的卡片,卡片上直接显示了详细的内容。


△ 左图: 窄屏幕显示 Codelabs。右图: 宽屏幕显示 Codelabs。


△ 左图: 窄屏幕显示 Codelabs。右图: 宽屏幕显示 Codelabs。


这些独立的网格卡片是定义在 res/layout-w840dp 下的 备用布局,数据绑定处理信息如何与视图绑定,以及卡片如何响应点击,所以除了不同样式下的差异之外,不需要实现太多内容。另一方面,整个 Fragment 没有备用布局,所以让我们看看在不同的配置下实现所需的样式和交互都用到了哪些技巧吧。


所有的一切都集中在这个 RecyclerView 元素上:


<androidx.recyclerview.widget.RecyclerView
android:id="@+id/codelabs_list"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingHorizontal="@dimen/codelabs_list_item_spacing"
android:paddingVertical="8dp"
app:itemSpacing="@{@dimen/codelabs_list_item_spacing}"
app:layoutManager="@string/codelabs_recyclerview_layoutmanager"
app:spanCount="2"
……其他的布局属性……/>

这里提供了两个资源文件,每一个在我们为备用布局选择的尺寸分界点上都有不同的值:






















资源文件无限定符版本 (默认)-w840dp
@string/codelabs_recyclerview_layoutmanagerLinearLayoutManagerStaggeredGridLayoutManager
@dimen/codelabs_list_item_spacing0dp8dp

我们通过在 XML 文件中把 app:layoutManager 的值设置为刚才的字符串资源,然后同时设置 android:orientationapp:spanCount 实现布局管理器的配置。注意,朝向属性 (orientation) 对两种布局管理器而言是相同的,但是横向跨度 (span count) 只适用于 StaggeredGridLayoutManager,如果被填充的布局管理器是 LinearLayoutManager,那么它会简单地忽略设定的横向跨度值。


用于 android:paddingHorizontal 的尺寸资源同时也被用于另一个属性 app:itemSpacing。它不是 RecyclerView 的标准属性,那它从何而来?这其实是由 Binding Adapter 定义的一个属性,而 Binding Adapter 是我们向数据绑定库提供自定义逻辑的方法。在应用运行时,数据绑定会调用下面的函数,并将解析自资源文件的值作为参数传进去。


@BindingAdapter("itemSpacing")
fun itemSpacing(recyclerView: RecyclerView, dimen: Float) {
val space = dimen.toInt()
if (space > 0) {
recyclerView.addItemDecoration(SpaceDecoration(space, space, space, space))
}
}

SpaceDecorationItemDecoration 的一种简单实现,它在每个元素周围保留一定空间,这也解释了为什么我们会在 840dp 或更宽的屏幕上 (需要为 @dimen/codelabs_list_item_spacing 给定一个正值) 得到始终相同的元素间隔。将 RecyclerView 自身的内边距也设置为相同的值,会使得元素同 RecyclerView 边界的距离与元素间的空隙保持相同的大小,在元素周围形成统一的留白。为了让元素能够一直滚动显示到 RecyclerView 的边缘,需要设置 android:clipToPadding="false"



屏幕越多样越好


Android 一直是个多样化的硬件生态系统。随着更多的平板和可折叠设备在用户中普及,请确保在这些不同尺寸和屏幕比例中测试您的应用,这样一些用户就不会觉得自己被 "冷落" 了。Android Studio 同时提供了 可折叠模拟器自由窗口模式 以简化这些测试过程,因此您可以通过它们来检查您的应用对于上述场景的响应情况。


我们希望这些 Google I/O 应用上的变动能启发您构建充分适配各种形状和尺寸设备的美观、高质量的应用。欢迎您从 Github 下载代码,动手试一试。


作者:Android_开发者
链接:https://juejin.cn/post/7007711228541796383
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android Camera了解一下

Camera 演进简介最近在项目中遇到 Camera相关的场景,之前对这块不是很了解,趁机补了一下盲区。Android Camera 相关也是生态碎片化较为严重的一块,Android FrameWorkt提供Camera API来实现拍照与屏幕录制的能力,目前...
继续阅读 »

Camera 演进简介

最近在项目中遇到 Camera相关的场景,之前对这块不是很了解,趁机补了一下盲区。Android Camera 相关也是生态碎片化较为严重的一块,Android FrameWorkt提供Camera API来实现拍照与屏幕录制的能力,目前Android有三类API

  • Camera (为了便于区分 下面简称 Camera1)

此类是用于控制设备相机的旧版API,在Android5.0以下使用,现已Deprecated

Android 5.0以上升级的方案,控制设备相机的API,并且开放出硬件支持级别的厂商定制

(谷歌开放出官方库CameraView 帮助解决相机兼容性问题,也有其他一些三方库)

JetPack中引入,基与Cmaera2 API封装,简化了开发流程,并增加生命周期控制

\

那么 Camera和Camera2如何使用?Camera2优秀在哪里?官方开发的的CameraView和CameraX又是为了解决什么问题,下面来一个个了解下。

Camera1和Camera2实践

相机开发核心流程如下

  1. 检查权限
  2. 检测设备摄像头,打开相机
  1. 创建预览帧 显示实时画面 (一般是通过 SurfaceView、TextureView进行实时预览),每个相机有支持预览的尺寸比如4:3或者16:9、11:9等,比如定制或者横竖屏场景 需要计算合适的预览尺寸
  2. 设置相机参数,进行拍照监听
  1. 拍照 保存图片或者操作原始数据
  2. 释放相机资源

其中步骤3在Camera1和Camera2中稍有不同,Camera1拍照前必须先开启预览,而Camera2流程做了解耦,可以无需预览直接拍照

Camera1

代码

权限声明

<uses-feature
android:name="android.hardware.camera"
android:required="true" />

<uses-permission android:name="android.permission.CAMERA" />

在Android 6.0以上需要动态申请权限

ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA_PERMISSION);

打开相机

支持传入id

Camera.open() //默认后置摄像头
Camera.open(int id)

创建预览

private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder;
...
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
...
startPreview();
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
releaseCamera();
}
});
...
private void startPreview() {
try {
//设置实时预览
mCamera.setPreviewDisplay(mSurfaceHolder);
//Orientation
setCameraDisplayOrientation();
//开始预览
mCamera.startPreview();
……
} catch (Exception e) {
e.printStackTrace();
}
}

在设置预览时候可以通过 setPreviewCallback( Camera.PreviewCallback

)监听预览数据的数据

设置相机参数

Camera.Parameters

//获取Parameters对象
mParameters = camera?.parameters

设置相机相关参数内容非常丰富,这里介绍几个常用的

  • setFocusMode 设置对焦模式
  • setPreviewSize 设置预览图片大小 (相机支持不同的预览尺寸,比如横竖屏需要计算预览尺寸d可以看下谷歌的CameraView中的CmaeraView1#chooseOptimalSize处理)
  • setPreviewFormat 设置预览格式 默认返回NV21
  • setPictureSize 设置保存图片的大小
  • setPreviewDisplay 设置实时预览 SurfaceHolder
  • setPreviewCallback 监听相机预览数据回调

拍照

mCamera.takePicture

private void takePicture() {
if (null != mCamera) {
mCamera.takePicture(new Camera.ShutterCallback() {
@Override
public void onShutter() {
//按下快门后的回调

}
}, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
//回调没压缩的 base data
}
}, new Camera.PictureCallback() {
@Override
public void onPictureTaken(final byte[] data, Camera camera) {
mCamera.startPreview();
//save data
}
});
}
}

释放相机资源

mCamera.stopPreview

mCamera.release

private void releaseCamera() {
if (null != mCamera) {
mCamera.stopPreview();
mCamera.stopFaceDetection();
mCamera.setPreviewCallback(null);
mCamera.release();
mCamera = null;
}
}

Camera2

从 Android 5.0 开始,Google 引入了一套全新的相机框架 Camera2(android.hardware.camera2)并且废弃了旧的相机框架 Camera1(android.hardware.Camera) ,相较于Camera1,Camera2架构上发生了变化,主要是将相机设备模拟成一个管道,按照顺序处理每一帧的请求并返回给调用方,API上的使用难度,本节先介绍下Camera2在核心流程上的使用

\

由于Camera2架构设计成了管道,在拍照流程中细分出了通过3个类来协同

  • CaptureRequest

相机捕获图像的设置请求,包含传感器,镜头,闪光灯等

  • CaptureRequest.Builder

CaptureRequest的构造器,使用Builder模式,设置更加方便

  • CameraCaptureSession

请求抓取相机图像帧的会话,会话的建立主要会建立起一个通道。一个CameraDevice一次只能开启一个CameraCaptureSession。

源端是相机,另一端是 Target,Target可以是Preview,也可以是ImageReader。

相机过程中处理数据也做了一些优化,抽象出了

  • ImageReader

用于从相机打开的通道中读取需要的格式的原始图像数据,可以设置多个ImageReader。

  • CameraCharacteristics

主要用于获取相机信息,内部携带大量的信息信息

  • CameraDevice

相机设备类,和Camera1中的Camera同级

  • CmaeraManager

相机系统服务,用于管理和连接相机设备

代码

\

拍摄流程重新抽象

  • 创建一个用于从Pipeline获取图片的CaptureRequest
  • 修改CaptureRequest的配置
  • 创建两个不同尺寸的Surface用于接收图片数据,并且将他们添加到CaptureRequest中
  • 发送配置好的CaptureRequest 到Pieline中等待结果

一个新的 CaptureRequest 会被放入一个被称作 Pending Request Queue 的队列中等待被执行,当 In-Flight Capture Queue 队列空闲的时候就会从 Pending Request Queue 获取若干个待处理的 CaptureRequest,并且根据每一个 CaptureRequest 的配置进行 Capture 操作。最后我们从不同尺寸的 Surface 中获取图片数据并且还会得到一个包含了很多与本次拍照相关的信息的 CaptureResult,流程结束

获取相机服务

CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);

根据相机ID获取相机信息 CameraCharateristics

private boolean chooseCameraIdByFacing() {
try {
int internalFacing = INTERNAL_FACINGS.get(mFacing);
final String[] ids = mCameraManager.getCameraIdList();
……
for (String id : ids) {
CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(id);
Integer level = characteristics.get(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
if (level == null ||
level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
continue;
}
Integer internal = characteristics.get(CameraCharacteristics.LENS_FACING);
if (internal == null) {
throw new NullPointerException("Unexpected state: LENS_FACING null");
}
if (internal == internalFacing) {
mCameraId = id;
mCameraCharacteristics = characteristics;
return true;
}
}
// Not found
……
// The operation can reach here when the only camera device is an external one.
// We treat it as facing back.
mFacing = Constants.FACING_BACK;
return true;
} catch (CameraAccessException e) {
throw new RuntimeException("Failed to get a list of camera devices", e);
}
}

初始化ImageReader

ImageReader是获取图像数据的重要途径,通过它可以获取到不同格式的图像数据,例如JPEG、YUV、RAW等。通过ImageReader.newInstance(int width, int height, int format, int maxImages)创建ImageReader对象,有4个参数:

  • width:图像数据的宽度
  • height:图像数据的高度
  • format:图像数据的格式,例如ImageFormat.JPEG,ImageFormat.YUV_420_888等
  • maxImages:最大Image个数,Image对象池的大小,指定了能从ImageReader获取Image对象的最大值,过多获取缓冲区可能导致OOM,所以最好按照最少的需要去设置这个值

ImageReader其他相关的方法和回调:

  • ImageReader.OnImageAvailableListener:有新图像数据的回调
  • acquireLatestImage():从ImageReader的队列里面,获取最新的Image,删除旧的,如果没有可用的Image,返回null
  • acquireNextImage():获取下一个最新的可用Image,没有则返回null
  • close():释放与此ImageReader关联的所有资源
  • getSurface():获取为当前ImageReader生成Image的Surface
private void prepareImageReader() {
if (mImageReader != null) {
mImageReader.close();
}
Size largest = mPictureSizes.sizes(mAspectRatio).last();
mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),
ImageFormat.JPEG, /* maxImages */ 2);
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, null);
}

private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
= new ImageReader.OnImageAvailableListener() {

@Override
public void onImageAvailable(ImageReader reader) {
try (Image image = reader.acquireNextImage()) {
Image.Plane[] planes = image.getPlanes();
if (planes.length > 0) {
ByteBuffer buffer = planes[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
mCallback.onPictureTaken(data);
}
}
}

};

打开相机设备

cameraManager.openCamera(@NonNull String cameraId,@NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)的三个参数:

  • cameraId:摄像头的唯一标识
  • callback:设备连接状态变化的回调
  • handler:回调执行的Handler对象,传入null则使用当前的主线程Handler
mCameraManager.openCamera(mCameraId, mCameraDeviceCallback, null);

private final CameraDevice.StateCallback mCameraDeviceCallback
= new CameraDevice.StateCallback() {

@Override
public void onOpened(@NonNull CameraDevice camera) {
//表示相机打开成功,可以真正开始使用相机,创建Capture会话
mCamera = camera;
mCallback.onCameraOpened();
//创建Capture会话
startCaptureSession();
}

@Override
public void onClosed(@NonNull CameraDevice camera) {
//调用Camera.close()后的回调方法
mCallback.onCameraClosed();
}

@Override
public void onDisconnected(@NonNull CameraDevice camera) {
//当相机断开连接时回调该方法,需要进行释放相机的操作
mCamera = null;
}

@Override
public void onError(@NonNull CameraDevice camera, int error) {
//当相机打开失败时,需要进行释放相机的操作
Log.e(TAG, "onError: " + camera.getId() + " (" + error + ")");
mCamera = null;
}

};

创建 CaptureRequest及其target配置

Camera2是通过管道链接 request+target建立会话,首先我们得通过CaptureRequest.Builder配置好

使用 TEMPLATE_STILL_CAPTURE 模板创建一个用于拍照的 CaptureRequest.Builder 对象,并且添加拍照的 Surface 和预览的 Surface 到其中:

captureImageRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureImageRequestBuilder.addTarget(previewDataSurface)
captureImageRequestBuilder.addTarget(jpegSurface)

通过CameraDevice.createCaptureRequest()创建CaptureRequest.Builder对象,传入一个templateType参数,templateType用于指定使用何种模板创建CaptureRequest.Builder对象,templateType的取值:

  • TEMPLATE_PREVIEW:预览模式
  • TEMPLATE_STILL_CAPTURE:拍照模式
  • TEMPLATE_RECORD:视频录制模式
  • TEMPLATE_VIDEO_SNAPSHOT:视频截图模式
  • TEMPLATE_MANUAL:手动配置参数模式

除了模式的配置,CaptureRequest还可以配置很多其他信息,例如图像格式、图像分辨率、传感器控制、闪光灯控制、3A(自动对焦-AF、自动曝光-AE和自动白平衡-AWB)控制等。在createCaptureSession的回调中可以进行设置

// Auto focus should be continuous for camera preview.
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// Flash is automatically enabled when necessary.
setAutoFlash(mPreviewRequestBuilder);

// Finally, we start displaying the camera preview.
mPreviewRequest = mPreviewRequestBuilder.build();

拍照和预览

在Camera2中拍照和预览是解耦开的,有了CaptureRequest之后,需要借助CaptureSession(Capture会话)来描述

mCameraDevice.createCaptureSession()创建Capture会话,它接受了三个参数:

  • outputs:用于接受图像数据的surface集合
  • callback:用于监听 Session 状态的CameraCaptureSession.StateCallback对象
  • handler:用于执行CameraCaptureSession.StateCallback的Handler对象,传入null则使用当前的主线程Handler

\

拍照

mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler);

该方法也有三个参数,和mCaptureSession.setRepeatingRequest一样:

  • request:CaptureRequest对象
  • listener:监听Capture 状态的回调
  • handler:用于执行CameraCaptureSession.CaptureCallback的Handler对象,传入null则使用当前的主线程Handler

这里设置了mCaptureCallback:

   PictureCaptureCallback mCaptureCallback = new PictureCaptureCallback() {

@Override
public void onPrecaptureRequired() {
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);
setState(STATE_PRECAPTURE);
try {
mCaptureSession.capture(mPreviewRequestBuilder.build(), this, null);
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);
} catch (CameraAccessException e) {
Log.e(TAG, "Failed to run precapture sequence.", e);
}
}

@Override
public void onReady() {
captureStillPicture();
}

};

\

预览

Camera2中,通过连续重复的Capture实现预览功能,每次Capture会把预览画面显示到对应的Surface上。连续重复的Capture操作通过mCaptureSession.setRepeatingRequest(mPreviewRequest,mCaptureCallback, mBackgroundHandler)实现,该方法有三个参数:

  • request:CaptureRequest对象
  • listener:监听Capture 状态的回调
  • handler:用于执行CameraCaptureSession.CaptureCallback的Handler对象,传入null则使用当前的主线程Handler

停止预览使用mCaptureSession.stopRepeating()方法。

\

释放相机资源

先后对CaptureSession,CameraDevice,ImageReader进行close操作,释放资源

void stop() {
if (mCaptureSession != null) {
mCaptureSession.close();
mCaptureSession = null;
}
if (mCamera != null) {
mCamera.close();
mCamera = null;
}
if (mImageReader != null) {
mImageReader.close();
mImageReader = null;
}
}

\

Cmaera2做了哪些优化

架构升级

参考 android 官网 source.android.com/devices/cam…

架构上,Camera2 的 API 模型被设计成一个 Pipeline(管道),它按顺序处理每一帧的请求并返回请求结果给客户端。下面这张来自官方的图展示了 Pipeline 的工作流程

\

在CaptureRequest中设置不同的Surface用于接收不同的图片数据,最后从不同的Surface中获取到图片数据和包含拍照相关信息的CaptureResult

假设我们想要同时拍摄两张不同尺寸的图片,并且在拍摄的过程中闪光灯必须亮起来。整个拍摄流程如下:

  1. 创建一个用于从 Pipeline 获取图片的 CaptureRequest。
  2. 修改 CaptureRequest 的闪光灯配置,让闪光灯在拍照过程中亮起来。
  1. 创建两个不同尺寸的 Surface 用于接收图片数据,并且将它们添加到 CaptureRequest 中。
  2. 发送配置好的 CaptureRequest 到 Pipeline 中等待它返回拍照结果。

一个新的 CaptureRequest 会被放入一个被称作 Pending Request Queue 的队列中等待被执行,当 In-Flight Capture Queue 队列空闲的时候就会从 Pending Request Queue 获取若干个待处理的 CaptureRequest,并且根据每一个 CaptureRequest 的配置进行 Capture 操作。最后我们从不同尺寸的 Surface 中获取图片数据并且还会得到一个包含了很多与本次拍照相关的信息的 CaptureResult,流程结束。

其中Caputure有以下三种工作模式

  • 单次模式 One-shot

指的是一次的Capture操作,例如设置闪光灯、对焦模式、拍一张照片,多个单次模式的Capture会进入队列按照顺序执行

  • 多次模式 Burst

指的是连续多次执行指定的Capture操作,该模式执行期间不允许插入其他Capture操作,如连续拍摄100张照片,在这100张照片拍摄期间任何新的capture都会等待

  • 重复模式 Repeating

指的是不断重复执行指定的Capture操作,当有其他模式的Capture提交会暂停改模式转而执行其他模式的Capture

\

Supported Hardware Level

相机功能的强大与否和硬件息息相关,不同厂商对 Camera2 的支持程度也不同,所以 Camera2 定义了一个叫做 Supported Hardware Level 的重要概念,其作用是将不同设备上的 Camera2 根据功能的支持情况划分成多个不同级别以便开发者能够大概了解当前设备上 Camera2 的支持情况。截止到 Android P 为止,从低到高一共有 LEGACY、LIMITED、FULL 和 LEVEL_3 四个级别:

  • LEGACY:向后兼容的级别,处于该级别的设备意味着它只支持 Camera1 的功能,不具备任何 Camera2 高级特性。
  • LIMITED:除了支持 Camera1 的基础功能之外,还支持部分 Camera2 高级特性的级别。
  • FULL:支持所有 Camera2 的高级特性。
  • LEVEL_3:新增更多 Camera2 高级特性,例如 YUV 数据的后处理等

\

新特性

  • 支持在开启相机前检查相机信息
  • 在不开启预览情况下拍照
  • 一次拍摄多张不同格式和尺寸的图片
  • 控制曝光时间
  • 连拍

\

简化开发谷歌做的努力

其他优秀三方库

github.com/CameraKit/c…

github.com/natario1/Ca…

CameraView

主要是为了解决不同版本Camer使用兼容性问题

根据官方的说明:

API LevelCamera APIPreview View
9-13Camera1SurfaceView
14-20Camera1TextureView
21-23Camera2TextureView
24Camera2SurfaceView

  • Camera 区分:Android5.0(21)以下使用 Camera1,以上使用 Camera2
  • Preview View:Android6.0(23)以上使用SurfaceView(SurfaceView在Android7.0上增加了新特性(平移、旋转等)),这里应该是 Android7.0以上(>23)使用SurfaceView,其他都使用TextureView,最新的源码sdk最低版本要求14。

类图如下

通过桥接模式+ 适配器模式,抽象出相机操作的抽象类CameraViewImpl和预览抽象类PreviewImpl,业务侧只操作接口

public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
……
// Internal setup
// 1.创建预览视图
final PreviewImpl preview = createPreviewImpl(context);
mCallbacks = new CallbackBridge();
// 2.根据 Android SDK 版本选择不同的 Camera
if (Build.VERSION.SDK_INT < 21) {
mImpl = new Camera1(mCallbacks, preview);
} else if (Build.VERSION.SDK_INT < 23) {
mImpl = new Camera2(mCallbacks, preview, context);
} else {
mImpl = new Camera2Api23(mCallbacks, preview, context);
}
……
// 设置相机 ID,如前置或者后置
setFacing(a.getInt(R.styleable.CameraView_facing, FACING_BACK));
// 设置预览界面的比例,如 4:3 或者 16:9
String aspectRatio = a.getString(R.styleable.CameraView_aspectRatio);
if (aspectRatio != null) {
setAspectRatio(AspectRatio.parse(aspectRatio));
} else {
setAspectRatio(Constants.DEFAULT_ASPECT_RATIO);
}
// 设置对焦方式
setAutoFocus(a.getBoolean(R.styleable.CameraView_autoFocus, true));
// 设置闪光灯
setFlash(a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO));
a.recycle();
// Display orientation detector
// 初始化显示设备(主要指手机屏幕)的旋转监听,主要用来设置相机的旋转方向
mDisplayOrientationDetector = new DisplayOrientationDetector(context) {
@Override
public void onDisplayOrientationChanged(int displayOrientation) {
mImpl.setDisplayOrientation(displayOrientation);
}
};
}

createPreViewImpl实现

private PreviewImpl createPreviewImpl(Context context) {
PreviewImpl preview;
if (Build.VERSION.SDK_INT >= 23) {
preview = new SurfaceViewPreview(context, this);
} else {
preview = new TextureViewPreview(context, this);
}
return preview;
}

使用起来也比较简单

<com.google.android.cameraview.CameraView
android:id="@+id/camera"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:keepScreenOn="true"
android:adjustViewBounds="true"
app:autoFocus="true"
app:aspectRatio="4:3"
app:facing="back"
app:flash="auto"/>

CameraX


CameraX 是一个 Jetpack 支持库,目的是简化Camera的开发工作,它是基于Camera2 API的基础,向后兼容至 Android 5.0(API 级别 21)。
它有以下几个特性:

  • 易用性,只需要几行代码就可以实现预览和拍照
  • 保持设备的一致性,在不同相机设备上,对宽高比、屏幕方向、旋转、预览大小和高分辨率图片大小,做到都可以正常使用
  • 相机特性的扩展,增加人像、HDR、夜间模式和美颜等功能
  • 具备生命周期的管理


作者:渡口一艘船
链接:https://juejin.cn/post/7008579597696499742
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

JetPack——ViewModel简析

简介ViewModel以生命周期的方式存储和管理界面相关的数据。让数据在发生屏幕旋转等配置更改后得以继续留存。同时,可以将数据操作从UI控制器(Activity)里分离出来,这样就只需要Activity控制UI逻辑而无需处理数据业务逻辑。在需要进行一些异步操作...
继续阅读 »

简介

ViewModel以生命周期的方式存储和管理界面相关的数据。让数据在发生屏幕旋转等配置更改后得以继续留存。同时,可以将数据操作从UI控制器(Activity)里分离出来,这样就只需要Activity控制UI逻辑而无需处理数据业务逻辑。在需要进行一些异步操作的时候,免去了在Activity里大量的维护工作,并避免了在Activity销毁时潜在的内存泄漏问题。

总结一下主要优点:

可以更容易的将数据操作逻辑与Activity分离。

ViewModel的实现和基本使用

JetPack为UI控制器提供了 ViewModel 辅助程序类,该类负责为界面准备数据。在配置更改期间会自动保留 ViewModel 对象,以便它们存储的数据立即可供下一个 activity 或 fragment 实例使用。

代码如下:

public class MyViewModel extends ViewModel {
private MutableLiveData<List<String>> users;
public LiveData<List<String>> getUsers() {
if (users == null) {
users = new MutableLiveData<List<String>>();
loadUsers();
}
return users;
}

private void loadUsers() {

}
}

使用:


public class MainActivity extends AppCompatActivity {

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

MyViewModel model = new ViewModelProvider(this).get(MyViewModel.class);

model.getUsers().observe(this, users -> {

});

}
}

ViewModel的创建

注意在上面的代码中,并没有采用new MyViewModel()的方式去创建ViewModel。而是使用:

MyViewModel model = new ViewModelProvider(this).get(MyViewModel.class);

对应的在kotlin中使用:

private val myViewModel: MyViewModel by viewModels();

先说结论:使用ViewModelProvider去创建ViewModel,确保了重新创建了相同Activity时,它接收的MyViewModel实例与第一个Activity创建的ViewModel实例相同。可以简单的理解为他是一个单例。

接下来看源码是如何实现的:

首先看ViewModelProvider的构造方法和实现:


private final Factory mFactory;

private final ViewModelStore mViewModelStore;

public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {

this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory():NewInstanceFactory.getInstance());

}

public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {

mFactory = factory;

mViewModelStore = store;

}

构造方法很简单,创建一个ViewModelStoreFactory

准备工作

ViewModelStore解析

查看ViewModelStore的代码如下:


public class ViewModelStore {

private final HashMap<String, ViewModel> mMap = new HashMap<>();

final void put(String key, ViewModel viewModel) {

ViewModel oldViewModel = mMap.put(key, viewModel);

if (oldViewModel != null) {

oldViewModel.onCleared();

}

}

final ViewModel get(String key) {

return mMap.get(key);

}

Set<String> keys() {

return new HashSet<>(mMap.keySet());
}

public final void clear() {

for (ViewModel vm : mMap.values()) {

vm.clear();
}

mMap.clear();

}

}

代码很简单,就是一个HashMap存储ViewModel的键值对(HashMap源码要点解析)。

ViewModelStore通过接口ViewModelStoreOwnergetViewModelStore()方法获取,以ComponentActivity为例,具体实现如下:


//ComponentActivity.java:

static final class NonConfigurationInstances {

Object custom;

ViewModelStore viewModelStore;

}

@NonNull

@Override

public ViewModelStore getViewModelStore() {

if (getApplication() == null) {

throw new IllegalStateException("Your activity is not yet attached to the "

+ "Application instance. You can't request ViewModel before onCreate call.");

}

ensureViewModelStore();

return mViewModelStore;

}

@SuppressWarnings("WeakerAccess") /* synthetic access */

void ensureViewModelStore() {

if (mViewModelStore == null) {

NonConfigurationInstances nc =(NonConfigurationInstances) getLastNonConfigurationInstance();

if (nc != null) {

mViewModelStore = nc.viewModelStore;
}

if (mViewModelStore == null) {

mViewModelStore = new ViewModelStore();

}

}

}

其中getLastNonConfigurationInstance方法实际上是返回一个activity,也可以理解为是一个ComponentActivity的实例,在这里不做深入讨论,其在Activity中的具体实现为:


@Nullable

public Object getLastNonConfigurationInstance() {

return mLastNonConfigurationInstances != null? mLastNonConfigurationInstances.activity : null;

}

不难发现,每个Activity都会持有一个mViewModelStoreensureViewModelStore()方法确保了所有ViewModel获取到的ViewModelStore都为同一个。也就说:Activity通过持有mViewModelStore,使用HaspMap管理ViewModel。也不难发现,activity和ViewModel是一对多的关系。

Factory解析

Factory顾名思义,就是一个工厂模式。它是一个定义在ViewModelProvider中的接口,代码很简单:


public interface Factory {

@NonNull

<T extends ViewModel> T create(@NonNull Class<T> modelClass);

}

就是用来创建ViewModel的。

AppCompatActivity为例:它通过继承ComponentActivity,实现了ViewModelStoreOwner接口,而在ViewModelProvider初始化时,结合上面的代码,Factory的具体实现为:NewInstanceFactory.getInstance();

具体代码如下:

public static class NewInstanceFactory implements Factory {

private static NewInstanceFactory sInstance;


@NonNull
static NewInstanceFactory getInstance() {
if (sInstance == null) {
sInstance = new NewInstanceFactory();
}
return sInstance;
}

@SuppressWarnings("ClassNewInstance")
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
try {
return modelClass.newInstance();
} catch (InstantiationException e) {
throw new RuntimeException("Cannot create an instance of " + modelClass, e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot create an instance of " + modelClass, e);
}
}
}

一个静态单例,返回一个用来创建ViewModel的工厂类,使用create()方法通过反射创建ViewModel实例。

真正创建

完成了准备工作, 接下来就看ViewModel具体的创建过程:


@NonNull

@MainThread

public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {

String canonicalName = modelClass.getCanonicalName();

if (canonicalName == null) {

throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");

}

//用类名作为Key值

return get(DEFAULT_KEY + ":" + canonicalName, modelClass);

}

@NonNull

@MainThread

public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {

//首先查看Map中是否有当前ViewModel的实例

ViewModel viewModel = mViewModelStore.get(key);

if (modelClass.isInstance(viewModel)) {

if (mFactory instanceof OnRequeryFactory) {

((OnRequeryFactory) mFactory).onRequery(viewModel);

}

return (T) viewModel;

} else {

if (viewModel != null) {
}

}

///没有实例就创建实例

if (mFactory instanceof KeyedFactory) {

viewModel = ((KeyedFactory) mFactory).create(key, modelClass);

} else {

viewModel = mFactory.create(modelClass);

}

mViewModelStore.put(key, viewModel);

return (T) viewModel;

}

可以看到,首先使用类名作为key值,判断Activity所持有的ViewModelStore是否包含当前ViewModel的实例,有就直接拿来使用。如果没有,就通过上文中的Factory.create创建一个实例,并添加到mViewModelStore中。

总结一下:

  1. Activity通过HashMap持有所有的实例化后的ViewModel。

  2. ViewModelProvider通过单例工厂模式和ViewModelStore,也就是HashMap实现对ViewModel的创建和获取,以此保证ViewModel在当前Activity中保持单例。

ViewModel的生命周期

ViewModel 对象存在的时间范围是获取 ViewModel 时传递给 ViewModelProvider 的 Lifecycle。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失:对于 activity,是在 activity 完成时;而对于 fragment,是在 fragment 分离时。

其生命周期如下图所示:

image.png

通常在Activity的onCreate()方法里创建/获取ViewModel。若在旋转设备屏幕时,再次调用onCreate(),结合上文中ViewModel的创建,其实并不会创建新的ViewModel实例,而是从HashMap中取出本就存在的实例。

因此:ViewModel 存在的时间范围是从您首次请求 ViewModel 直到 activity 完成并销毁。

ViewModel的销毁

销毁源码如下:

@SuppressWarnings("WeakerAccess")
protected void onCleared() {
}

@MainThread
final void clear() {

if (mBagOfTags != null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
}
onCleared();
}

其中:clear()方法在ViewModelStore中调用,其相关代码在上文中已展示:

public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}

ViewModelStore.clear()则在Activity中被调用:

getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
// Clear out the available context
mContextAwareHelper.clearAvailableContext();
// And clear the ViewModelStore
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});

通过Lifecycle管理的Activity的生命周期,在Activity销毁时,也就是onDestory时调用。

带参数的ViewModel实现

既然ViewModel不可以通过new关键字来初始化,那么如果需要再初始化时向ViewModel传参该怎么办呢。例如:

public class MyViewModel extends ViewModel {

private String tag;

public MyViewModel(String tag) {
this.tag = tag;
}

private MutableLiveData<List<String>> users;

public LiveData<List<String>> getUsers() {
if (users == null) {
users = new MutableLiveData<List<String>>();
loadUsers();
}
return users;
}

private void loadUsers() {

}
}

根据上文中的代码解析,不难发现,我们需要重写Factorycreate方法,结合ViewModelProvider的构造函数,可以创建一个工厂类:

class MyViewModelFactory implements ViewModelProvider.Factory {
private String tag;

public MyViewModelFactory(String tag) {
this.tag = tag;
}

@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return (T) new MyViewModel(tag);
}
}

具体实现:


MyViewModelFactory myViewModelFactory = new MyViewModelFactory("TAG");

MyViewModel model = new ViewModelProvider(this,myViewModelFactory).get(MyViewModel.class);

当然了,也可以使用匿名内部类的方式实现:

MyViewModel model = new ViewModelProvider(this, new ViewModelProvider.Factory(){
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return (T) new MyViewModel("tag");
}
}).get(MyViewModel.class);

在kolin中的扩展

koltin中使用扩展函数简化了ViewModel的实现:

@MainThread
public inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)

@MainThread
public fun <VM : ViewModel> Fragment.createViewModelLazy(
viewModelClass: KClass<VM>,
storeProducer: () -> ViewModelStore,
factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
}


public class ViewModelLazy<VM : ViewModel> (
private val viewModelClass: KClass<VM>,
private val storeProducer: () -> ViewModelStore,
private val factoryProducer: () -> ViewModelProvider.Factory
) : Lazy<VM> {
private var cached: VM? = null

override val value: VM
get() {
val viewModel = cached
return if (viewModel == null) {
val factory = factoryProducer()
val store = storeProducer()
ViewModelProvider(store, factory).get(viewModelClass.java).also {
cached = it
}
} else {
viewModel
}
}

override fun isInitialized(): Boolean = cached != null
}

其本质上还是用ViewModelProvider创建,只不过加了一些Kotlin的语法糖,简化了操作。

不仅如此,Kotlin还是支持使用注解,这样就可以省去创建Factory的麻烦。刚兴趣的可以自己去探索(Hilt 和 Jetpack 集成

总结

  • Activity持有ViewModel,一个Activity可以有多个类型的ViewModel;

  • 同一个类型的ViewModel在Activity中有且只有一个实例;

  • 通过HashMap和工厂模式保证了单一类型ViewModel的单例;

  • ViewModel生命周期贯穿整个Activity,且不会重复创建。

收起阅读 »

android高仿微信聊天消息列表自由复制文字,双击查看文本内容

掘金地址 github地址SelectTextHelper打造一个全网最逼近微信聊天消息自由复制,双击查看文本内容框架。 汇聚底层TextView框架、原理并加以整理得出的一个实用的Helper。 仅用两个类实现便实现如此强大的功能,用法也超级简单。...
继续阅读 »

掘金地址 github地址

SelectTextHelper打造一个全网最逼近微信聊天消息自由复制,双击查看文本内容框架。 汇聚底层TextView框架、原理并加以整理得出的一个实用的Helper。 仅用两个类实现便实现如此强大的功能,用法也超级简单。

项目演示

消息页效果查看内容效果
1631677218586.gif1631678150191.gif
 
消息页全选消息页自由复制放大镜
demo_1.jpgdemo_2.jpg
 
消息页选中文本查看内容
demo_3.jpgdemo_4.jpg

特点功能:

  • 支持自由选择文本
  • 支持自定义文本有:游标颜色、游标大小、选中文本颜色
  • 支持默认全选文字或选2个文字
  • 支持滑动依然显示弹窗
  • 支持放大镜功能
  • 支持全选情况下自定义弹窗
  • 支持操作弹窗:每行个数、图片、文字、监听回调、弹窗颜色、箭头图片
  • 支持 AndroidX

Demo

下载 APK-Demo

传送门

主要实现

通过 仿照的例子 并改进弹窗坐标位置、大小加上EventBus实现

简单用例

1.导入代码

把该项目里的selecttext Module放入你的项目里面

2.给你的 TextView 创建Helper和加监听

SelectTextHelper mSelectableTextHelper=new SelectTextHelper
.Builder(textView)// 放你的textView到这里!!
.setCursorHandleColor(0xFF1379D6/*mContext.getResources().getColor(R.color.colorAccent)*/)// 游标颜色 default 0xFF1379D6
.setCursorHandleSizeInDp(24)// 游标大小 单位dp default 24
.setSelectedColor(0xFFAFE1F4/*mContext.getResources().getColor(R.color.colorAccentTransparent)*/)// 选中文本的颜色 default 0xFFAFE1F4
.setSelectAll(true)// 初次选中是否全选 default true
.setScrollShow(true)// 滚动时是否继续显示 default true
.setSelectedAllNoPop(true)// 已经全选无弹窗,设置了true在监听会回调 onSelectAllShowCustomPop 方法 default false
.setMagnifierShow(true)// 放大镜 default true
.addItem(0/*item的图标*/,"复制"/*item的描述*/, // 操作弹窗的每个item
()->Log.i("SelectTextHelper","复制")/*item的回调*/)
.build();

mSelectableTextHelper.setSelectListener(new SelectTextHelper.OnSelectListener(){
/**
* 点击回调
*/

@Override
public void onClick(View v){
// clickTextView(textView.getText().toString().trim());
}

/**
* 长按回调
*/

@Override
public void onLongClick(View v){
// postShowCustomPop(SHOW_DELAY);
}

/**
* 选中文本回调
*/

@Override
public void onTextSelected(CharSequence content){
// selectedText = content.toString();
}

/**
* 弹窗关闭回调
*/

@Override
public void onDismiss(){
}

/**
* 点击TextView里的url回调
*/

@Override
public void onClickUrl(String url){
}

/**
* 全选显示自定义弹窗回调
*/

@Override
public void onSelectAllShowCustomPop(){
// postShowCustomPop(SHOW_DELAY);
}

/**
* 重置回调
*/

@Override
public void onReset(){
// SelectTextEventBus.getDefault().dispatch(new SelectTextEvent("dismissOperatePop"));
}

/**
* 解除自定义弹窗回调
*/

@Override
public void onDismissCustomPop(){
// SelectTextEventBus.getDefault().dispatch(new SelectTextEvent("dismissOperatePop"));
}

/**
* 是否正在滚动回调
*/

@Override
public void onScrolling(){
// removeShowSelectView();
}
});

3.demo中提供了查看文本内容的SelectTextDialog 和 消息列表自由复制MainActivity

查看文本内容方法:

  • 该方法比较简单,将textView参照步骤2放入SelectTextHelper中,在dismiss调用SelectTextHelper的reset()即可。
@Override
public void dismiss(){
mSelectableTextHelper.reset();
super.dismiss();
}

高仿微信聊天消息列表自由复制方法:

  • recycleView + adapter + 多布局的使用在这里不阐述,请看本项目demo。

  • 为adapter里text类型ViewHolder中的textView参照步骤2放入SelectTextHelper中,注册SelectTextEventBus。

  • SelectTextEventBus类特别说明、原理: SelectTextEventBus在EventBus基础上加功能。在register时记录下类和方法,方便在Activity/Fragment Destroy时unregister所有SelectTextEventBus的EventBus。

  • text类型ViewHolder 添加EventBus监听

/**
* 自定义SelectTextEvent 隐藏 光标
*/

@Subscribe(threadMode = ThreadMode.MAIN)
public void handleSelector(SelectTextEvent event){
if(null==mSelectableTextHelper){
return;
}
String type=event.getType();
if(TextUtils.isEmpty(type)){
return;
}
switch(type){
case"dismissAllPop":
mSelectableTextHelper.reset();
break;
case"dismissAllPopDelayed":
postReset(RESET_DELAY);
break;
}
}
  • 重写adapter里的onViewRecycled方法,该方法在回收View时调用
@Override
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder){
super.onViewRecycled(holder);
if(holder instanceof ViewHolderText){
// 注销
SelectTextEventBus.getDefault().unregister(holder);
}
}
  • 防抖
/**
* 延迟显示CustomPop
* 防抖
*/

private void postShowCustomPop(int duration){
textView.removeCallbacks(mShowCustomPopRunnable);
textView.postDelayed(mShowCustomPopRunnable,duration);
}

private final Runnable mShowCustomPopRunnable=
()->showCustomPop(text_rl_container,textMsgBean);

/**
* 延迟重置
* 为了支持滑动不重置
*/

private void postReset(int duration){
textView.removeCallbacks(mShowSelectViewRunnable);
textView.postDelayed(mShowSelectViewRunnable,duration);
}

private void removeShowSelectView(){
textView.removeCallbacks(mShowSelectViewRunnable);
}

private final Runnable mShowSelectViewRunnable=
()->mSelectableTextHelper.reset();

如果使用 AndroidX 先在 gradle.properties 中添加,两行都不能少噢~

android.useAndroidX=true
android.enableJetifier=true

收起阅读 »

【kotlin从摸索到探究】- delay函数实现原理

简介这片文章主要讲解kotlin中delay函数的实现原理,delay是一个挂起函数。kotlin携程使用过程中,经常使用到挂起函数,在我学习kotlin携程的时候,一些现象让我很是困惑,所以打算从源码角度来逐一分析。说明在分析delay源码实现过程中,由于对...
继续阅读 »

简介

这片文章主要讲解kotlindelay函数的实现原理,delay是一个挂起函数。kotlin携程使用过程中,经常使用到挂起函数,在我学习kotlin携程的时候,一些现象让我很是困惑,所以打算从源码角度来逐一分析

说明

在分析delay源码实现过程中,由于对kotlin有些语法还不是很熟悉,所以并不会把每一步将得很透彻,只会梳理一个大致的流程,如果讲解有误的地方,欢迎指出。

例子先行

fun main() = runBlocking {
println("${treadName()}======start")
launch {
println("${treadName()}======delay 1s start")
delay(1000)
println("${treadName()}======delay 1s end")
}

println("${treadName()}======delay 3s start")
delay(3000)
println("${treadName()}======delay 3s end")
// 延迟,保活进程
Thread.sleep(500000)
}

输出如下:

main======start
main======delay 3s start
main======delay 1s start
main======delay 1s end
main======delay 3s end

根据日志可以看出:

  1. 日志输出环境是在主线程。
  2. 执行3s延迟函数后,切换到了**launch**携程体执行。
  3. delay挂起函数恢复后执行各自的打印函数。

疑问:

如果真像打印日志输出一样,所以的操作都是在一个线程(主线程)完成,那么问题来了。**第一:按照Java线程知识,单线程执行是按照顺序的,是单条线的。那么不管delay里是何等骚操作,只要没有重新起线程,应该不能够像上面输入的那样吧,你说sleepwait,如果你这么想,那么你可以去补一补Java多线程基础知识了。猜想:**1. 难得真有什么我不知道的骚操作可以在一个线程里面同时执行delay和其它代码,真像很多人说的,携程性能很好,使用挂起函数可以不用启动新的线程,就可以异步执行,那真的就很不错。2. delay启动了新的线程,上面的现象只不过是进行了线程切换,那么如果多次调用 delay那么岂不是要创建很多线程,这性能问题和资源问题怎么解决。3. delay基于某种任务调度策略。

delay源码

public suspend inline fun <T> suspendCancellableCoroutine(
crossinline block: (CancellableContinuation<T>) -> Unit
): T =
suspendCoroutineUninterceptedOrReturn { uCont ->
val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
cancellable.initCancellability()
block(cancellable)
cancellable.getResult()
}

cancellable是一个CancellableContinuationImpl对象,执行 block(cancellable),回到下面函数。

public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
// if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
if (timeMillis < Long.MAX_VALUE) {
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}

看一下cont.context.delayget方法

internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay

如果get(ContinuationInterceptor)Delay类型对象,那么直接返回该对象,如果不是返回DefaultDelay变量,看一下DefaultDelay初始化可以知道,它是一个DefaultExecutor对象,继承了EventLoopImplBase类。

runBlocking执行过程中有这样一行代码createCoroutineUnintercepted(receiver, completion).intercepted()会被ContinuationInterceptor进行包装。所以上面cont.context.delay返回的就是被包装的携程体上下文。

查看scheduleResumeAfterDelay方法。

    public override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
val timeNanos = delayToNanos(timeMillis)
if (timeNanos < MAX_DELAY_NS) {
val now = nanoTime()
DelayedResumeTask(now + timeNanos, continuation).also { task ->
continuation.disposeOnCancellation(task)
schedule(now, task)
}
}
}

创建DelayedResumeTask对象,在also执行相关计划任务,看一下schedule方法。

    public fun schedule(now: Long, delayedTask: DelayedTask) {
when (scheduleImpl(now, delayedTask)) {
SCHEDULE_OK -> if (shouldUnpark(delayedTask)) unpark()
SCHEDULE_COMPLETED -> reschedule(now, delayedTask)
SCHEDULE_DISPOSED -> {} // do nothing -- task was already disposed
else -> error("unexpected result")
}
}

这里返回SCHEDULE_OK,执行unpark函数,这里用到了Java提供的LockSupport线程操作相关知识。

读取线程

  val thread = thread
  • 如果delay是当前携程的上下文 那么把延时任务加入到队列后,那么又是怎么达到线程延迟呢。回到runBlocking执行流程,会执行coroutine.joinBlocking()这样一行代码。

      fun joinBlocking(): T {
    registerTimeLoopThread()
    try {
    eventLoop?.incrementUseCount()
    try {
    while (true) {
    @Suppress("DEPRECATION")
    if (Thread.interrupted()) throw InterruptedException().also { cancelCoroutine(it) }
    val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE
    // note: process next even may loose unpark flag, so check if completed before parking
    if (isCompleted) break
    parkNanos(this, parkNanos)
    }
    } finally { // paranoia
    eventLoop?.decrementUseCount()
    }
    } finally { // paranoia
    unregisterTimeLoopThread()
    }
    // now return result
    val state = this.state.unboxState()
    (state as? CompletedExceptionally)?.let { throw it.cause }
    return state as T
    }

    执行:

     val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE

    看一下processNextEvent

      override fun processNextEvent(): Long {
    // unconfined events take priority
    if (processUnconfinedEvent()) return 0
    // queue all delayed tasks that are due to be executed
    val delayed = _delayed.value
    if (delayed != null && !delayed.isEmpty) {
    val now = nanoTime()
    while (true) {
    delayed.removeFirstIf {
    if (it.timeToExecute(now)) {
    enqueueImpl(it)
    } else
    false
    } ?: break // quit loop when nothing more to remove or enqueueImpl returns false on "isComplete"
    }
    }
    // then process one event from queue
    val task = dequeue()
    if (task != null) {
    task.run()
    return 0
    }
    return nextTime
    }

    从延迟队列取任务

    val delayed = _delayed.value

    挂起当前线程

    parkNanos(this, parkNanos)

    这里是一个while循环,当挂起时间到,线程唤醒,继续从任务队列中取任务执行。如果还是延迟任务,这根据当前时间点,计算线程需要挂起的时间,这也是为什么多个延迟任务好像是同时执行的。

  • 如果delay是DefaultExecutor 比如这个例子:携程上下文没有像CoroutineStart.DEFAULT那样进行包装。

    fun main() {
    GlobalScope.launch(start = CoroutineStart.UNDISPATCHED){
    println("${treadName()}======我开始执行了~")
    delay(1000)
    println("${treadName()}======全局携程~")
    }
    println("${treadName()}======我要睡觉~")
    Thread.sleep(3000)
    }

    然后调用DefaultExecutor类中thread的get方法:

      override val thread: Thread
    get() = _thread ?: createThreadSync()

    看一下createThreadSync函数

      private fun createThreadSync(): Thread {
    return _thread ?: Thread(this, THREAD_NAME).apply {
    _thread = this
    isDaemon = true
    start()
    }
    }

    创建一个叫"kotlinx.coroutines.DefaultExecutor的新线程,并且开始运行。这时候会执行DefaultExecutor中的run方法。在run方法中有这样一行代码:

    parkNanos(this, parkNanos)

    点进去看看:

    internal inline fun parkNanos(blocker: Any, nanos: Long) {
    timeSource?.parkNanos(blocker, nanos) ?: LockSupport.parkNanos(blocker, nanos)
    }

    调用Java提供的LockSupport.parkNanos(blocker, nanos)方法,阻塞当前线程,实现挂起,当达到阻塞的时间,恢复线程执行。

查看进行中线程情况方法

fun main() {
println("${treadName()}======doSuspendTwo")
Thread.sleep(500000)
}

运行main,通过命令jps找到对应Java进程(没有特别指定,进程名为文件名)号。

...
3406 KotlinCoreutinesSuspendKt
...

执行jstack 进程号查看进程对应的线程资源。


作者:Coolbreeze
链接:https://juejin.cn/post/7007769804505350152
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

【kotlin从摸索到探究】- 协程的执行流程

简介 这篇文章将从源码的角度,分析携程的执行流程,我们创建一个携程,系统是怎么进行调度的,什么时候执行的,是否需要创建新线程等等,带着这些疑问,一起往下看吧。 例子先行 fun main(): Unit = runBlocking { launch {...
继续阅读 »

简介


这篇文章将从源码的角度,分析携程的执行流程,我们创建一个携程,系统是怎么进行调度的,什么时候执行的,是否需要创建新线程等等,带着这些疑问,一起往下看吧。


例子先行


fun main(): Unit = runBlocking {
launch {
println("${treadName()}======1")
}
GlobalScope.launch {
println("${treadName()}======3")
}
launch {
println("${treadName()}======2")
}
println("${treadName()}======4")
Thread.sleep(2000)
}


输出如下:


DefaultDispatcher-worker-1======3
main======4
main======1
main======2

Process finished with exit code 0


根据打印,如果根据单线程执行流程来看,是不是感觉上面的日志打印顺序有点不好理解,下面我们就逐步来进行分解。




  • runBlocking携程体
    这里将其它代码省略到了,我这里都是按照一条简单的执行流程进行讲解。


    public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {

    val eventLoop: EventLoop?
    val newContext: CoroutineContext
    ...
    if (contextInterceptor == null) {
    eventLoop = ThreadLocalEventLoop.eventLoop
    newContext = GlobalScope.newCoroutineContext(context + eventLoop)
    }
    ...
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
    }


    看一下eventLoop的初始化,会 在当前线程(主线程)创建BlockingEventLoop对象。


    internal val eventLoop: EventLoop
    get() = ref.get() ?: createEventLoop().also { ref.set(it) }

    internal actual fun createEventLoop(): EventLoop = BlockingEventLoop(Thread.currentThread())


    看一下newContext初始化,这里会对携程上下文进行组合,返回新的上下文。最后返回的是一个BlockingEventLoop对象。


    public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
    val combined = coroutineContext + context
    val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
    return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
    debug + Dispatchers.Default else debug
    }


    开始对携程进行调度


     coroutine.start(CoroutineStart.DEFAULT, coroutine, block)


    看一下执行这句代码之前,各变量的值


    image


    而上面的代码最终调用的是CoroutineStart.DEFAULTinvoke方法。


      public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>): Unit =
    when (this) {
    DEFAULT -> block.startCoroutineCancellable(completion)
    ATOMIC -> block.startCoroutine(completion)
    UNDISPATCHED -> block.startCoroutineUndispatched(completion)
    LAZY -> Unit // will start lazily
    }


    我们使用的是DEFAULT启动模式。然后会执行resumeCancellableWith方法。


      inline fun resumeCancellableWith(
    result: Result<T>,
    noinline onCancellation: ((cause: Throwable) -> Unit)?
    ) {
    val state = result.toState(onCancellation)
    if (dispatcher.isDispatchNeeded(context)) {
    _state = state
    resumeMode = MODE_CANCELLABLE
    dispatcher.dispatch(context, this)
    } else {
    executeUnconfined(state, MODE_CANCELLABLE) {
    if (!resumeCancelled(state)) {
    resumeUndispatchedWith(result)
    }
    }
    }
    }


    dispatcherBlockingEventLoop对象,没有重写isDispatchNeeded,默认返回true。然后调用dispatch继续进行分发。BlockingEventLoop继承了EventLoopImplBase并调用其dispatch方法。把任务加入到队列中。


    public final override fun dispatch(context: CoroutineContext, block: Runnable) = enqueue(block)


    回到最开始,在coroutine.start(CoroutineStart.DEFAULT, coroutine, block)执行完,还执行了coroutine.joinBlocking()看一下实现。


        fun joinBlocking(): T {
    registerTimeLoopThread()
    try {
    eventLoop?.incrementUseCount()
    try {
    while (true) {
    @Suppress("DEPRECATION")
    if (Thread.interrupted()) throw InterruptedException().also { cancelCoroutine(it) }
    val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE
    // note: process next even may loose unpark flag, so check if completed before parking
    if (isCompleted) break
    parkNanos(this, parkNanos)
    }
    } finally { // paranoia
    eventLoop?.decrementUseCount()
    }
    } finally { // paranoia
    unregisterTimeLoopThread()
    }
    // now return result
    val state = this.state.unboxState()
    (state as? CompletedExceptionally)?.let { throw it.cause }
    return state as T
    }


    执行val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE,取出任务进行执行,也就是runBlocking携程体。




  • launch {} 执行流程


    public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
    ): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
    LazyStandaloneCoroutine(newContext, block) else
    StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
    }

    因为launch是直接在runBlocking(父携程体)里新的创建的子携程体,所以执行流程上和之前将的差不多,只不过不会像runBlocking再去创建BlockingEventLoop对象,而是直接用runBlocking(父携程体)的,然后把任务加到里面,所以通过这种方式其实就是单线程对任务的调度而已。所以在runBlocking(父携程体)内通过launch启动再多的携程体,其实都是在同一线程,按照任务队列的顺序执行的。





根据上面日志输出,并没有先执行两个launch携程体,这是为什么呢,根据上面的讲解,应用知道,runBlocking(父携程体)是第一被添加的队列的任务,其次是launch,所以是这样的顺序。那可以让launch立即执行吗?答案是可以的,这就要说携程的启动模式了。





  • CoroutineStart 是协程的启动模式,存在以下4种模式:



    1. DEFAULT 立即调度,可以在执行前被取消

    2. LAZY 需要时才启动,需要start、join等函数触发才可进行调度

    3. ATOMIC 立即调度,协程肯定会执行,执行前不可以被取消

    4. UNDISPATCHED 立即在当前线程执行,直到遇到第一个挂起点(可能切线程)



    我们使用UNDISPATCHED就可以使携程体马上在当前线程执行。看一下是怎么实现的。看一下实现:





使用这种启动模式执行UNDISPATCHED -> block.startCoroutineUndispatched(completion)方法。


internal fun <T> (suspend () -> T).startCoroutineUndispatched(completion: Continuation<T>) {
startDirect(completion) { actualCompletion ->
withCoroutineContext(completion.context, null) {
startCoroutineUninterceptedOrReturn(actualCompletion)
}
}
}

大家可以自己点击去看一下,大概就是会立即执行携程体,而不是将任务放入队列。



但是GlobalScope.launch却不是按照这样的逻辑,这是因为GlobalScope.launch启动的全局携程,是一个独立的携程体了,并不是runBlocking(父携程体)子携程。看一下通过GlobalScope.launch有什么不同。





  • GlobalScope.launch执行流程



    1. 启动全局携程


    GlobalScope.launch

    newCoroutineContext(context)返回Dispatchers.Default对象。而DefaultScheduler继承了ExperimentalCoroutineDispatcher类。看一下ExperimentalCoroutineDispatcher中的dispatch代码:


     override fun dispatch(context: CoroutineContext, block: Runnable): Unit =
    ...
    coroutineScheduler.dispatch(block)
    ...


    看一下coroutineScheduler初始化


    private fun createScheduler() = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)

    CoroutineScheduler实现了Executor接口,里面还有两个全局队列和线程池相关的参数。


    @JvmField
    val globalCpuQueue = GlobalQueue()
    @JvmField
    val globalBlockingQueue = GlobalQueue()


    继续调用CoroutineScheduler中的dispatch方法


      fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, tailDispatch: Boolean = false) {
    trackTask() // this is needed for virtual time support
    val task = createTask(block, taskContext)
    // try to submit the task to the local queue and act depending on the result
    val currentWorker = currentWorker()
    val notAdded = currentWorker.submitToLocalQueue(task, tailDispatch)
    if (notAdded != null) {
    if (!addToGlobalQueue(notAdded)) {
    // Global queue is closed in the last step of close/shutdown -- no more tasks should be accepted
    throw RejectedExecutionException("$schedulerName was terminated")
    }
    }
    val skipUnpark = tailDispatch && currentWorker != null
    // Checking 'task' instead of 'notAdded' is completely okay
    if (task.mode == TASK_NON_BLOCKING) {
    if (skipUnpark) return
    signalCpuWork()
    } else {
    // Increment blocking tasks anyway
    signalBlockingWork(skipUnpark = skipUnpark)
    }
    }




    1. val task = createTask(block, taskContext)包装成TaskImpl对象。




    2. val currentWorker = currentWorker()当前是主线程,运行程序时由进程创建,肯定不是Worker对象,Worker是一个继承了Thread的类 ,并且在初始化时都指定为守护线程


      Worker存在5种状态:
      CPU_ACQUIRED 获取到cpu权限
      BLOCKING 正在执行IO阻塞任务
      PARKING 已处理完所有任务,线程挂起
      DORMANT 初始态
      TERMINATED 终止态







  1. val notAdded = currentWorker.submitToLocalQueue(task, tailDispatch)由于currentWorker是null,直接返回task对象。

  2. addToGlobalQueue(notAdded)根据任务是否是阻塞任务,将task添加到全局任务队列中。这里被添加到globalCpuQueue中。

  3. 执行signalCpuWork()来唤醒一个线程或者启动一个新的线程。


    fun signalCpuWork() {
if (tryUnpark()) return
if (tryCreateWorker()) return
tryUnpark()
}


 private fun tryCreateWorker(state: Long = controlState.value): Boolean {  
val created = createdWorkers(state)// 创建的的线程总数
val blocking = blockingTasks(state)// 处理阻塞任务的线程数量
val cpuWorkers = (created - blocking).coerceAtLeast(0)//得到非阻塞任务的线程数量
if (cpuWorkers < corePoolSize) {// 小于核心线程数量,进行线程的创建
val newCpuWorkers = createNewWorker()
if (newCpuWorkers == 1 && corePoolSize > 1) createNewWorker()// 当前非阻塞型线程数量为1,同时核心线程数量大于1时,再进行一个线程的创建,
if (newCpuWorkers > 0) return true
}
return false
}

// 创建线程
private fun createNewWorker(): Int {
synchronized(workers) {
...
val created = createdWorkers(state)// 创建的的线程总数
val blocking = blockingTasks(state)// 阻塞的线程数量
val cpuWorkers = (created - blocking).coerceAtLeast(0) // 得到非阻塞线程数量
if (cpuWorkers >= corePoolSize) return 0//超过最大核心线程数,不能进行新线程创建
if (created >= maxPoolSize) return 0// 超过最大线程数限制,不能进行新线程创建
...
val worker = Worker(newIndex)
workers[newIndex] = worker
require(newIndex == incrementCreatedWorkers())
worker.start()// 线程启动
return cpuWorkers + 1
}
}


那么这里面的任务又是怎么调度的呢,当全局任务被执行的时候,看一下Worker中的run方法:


 override fun run() = runWorker()

执行runWorker方法,该方法会从队列中找到执行任务,然后开始执行。详细代码,可以自行翻阅。



所以GlobalScope.launch使用的就是线程池,没有所谓的性能好。




  • Dispatchers调度器
    Dispatchers是协程中提供的线程调度器,用来切换线程,指定协程所运行的线程。,上面用的是默认调度器Dispatchers.Default



Dispatchers中提供了4种类型调度器:
Default 默认调度器:适合CPU密集型任务调度器 比如逻辑计算;
Main UI调度器
Unconfined 无限制调度器:对协程执行的线程不做限制,协程恢复时可以在任意线程;
IO调度器:适合IO密集型任务调度器 比如读写文件,网络请求等。





作者:Coolbreeze
链接:https://juejin.cn/post/7008083001884016648
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

使用 Kotlin 重写 AOSP 日历应用

两年前,Android 开源项目 (AOSP) 应用 团队开始使用 Kotlin 替代 Java 重构 AOSP 应用。之所以重构主要有两个原因: 一是确保 AOSP 应用能够遵循 Android 最佳实践,另外则是提供优先使用 Kotlin 进行应用开发的良...
继续阅读 »

两年前,Android 开源项目 (AOSP) 应用 团队开始使用 Kotlin 替代 Java 重构 AOSP 应用。之所以重构主要有两个原因: 一是确保 AOSP 应用能够遵循 Android 最佳实践,另外则是提供优先使用 Kotlin 进行应用开发的良好范例。Kotlin 之所以具有强大的吸引力,原因之一是其简洁的语法,很多情况下用 Kotlin 编写的代码块的代码数量相比于功能相同的 Java 代码块要更少一些。此外,Kotlin 这种具有丰富表现力的编程语言还具有其他各种优点,例如:




  • 空安全: 这一概念可以说是根植于 Kotlin 之中,从而帮助避免破坏性的空指针异常;




  • 并发: 正如 Google I/O 2019 中关于 Android 的描述,结构化并发 (structured concurrency) 能够允许使用协程简化后台的任务管理;




  • 兼容 Java: 尤其是在这次的重构项目中,Kotlin 与 Java 语言的兼容性能够让我们一个文件一个文件地进行 Kotlin 转换。




AOSP 团队在去年夏天发表了一篇文章,详细介绍了 AOSP 桌面时钟应用的转换过程。而今年,我们将 AOSP 日历应用从 Java 转换成了 Kotlin。在这次转换之前,应用的代码行数超过 18,000 行,在转换后代码库减少了约 300 行。在这次的转换中,我们沿袭了同 AOSP 桌面时钟转换过程中类似的技术,充分利用了 Kotlin 与 Java 语言的互操作性,对代码文件一一进行了转换,并在过程中使用独立的构建目标将 Java 代码文件替换为对应的 Kotlin 代码文件。因为团队中有两个人在进行此项工作,所以我们在 Android.bp 文件中为每个人创建了一个 exclude_srcs 属性,这样两个人就可以在减少代码合并冲突的前提下,都能够同时进行重构并推送代码。此外,这样还能允许我们进行增量测试,快速定位错误出现在哪些文件。


在转换任意给定的文件时,我们一开始先使用 Android Studio Kotlin 插件中提供的 从 Java 到 Kotlin 的自动转换工具。虽然该插件成功帮助我们转换了大部份的代码,但是还是会遇到一些问题,需要开发者手动解决。需要手动更改的部分,我们将会在本文接下来的章节中列出。


在将每个文件转换为 Kotlin 之后,我们手动测试了日历应用的 UI 界面,运行了单元测试,并运行了 Compatibility Test Suite (CTS) 的子集来进行功能验证,以确保不需要再进行任何的回归测试。


自动转换之后的步骤


上面提到,在使用自动转换工具之后,有一些反复出现的问题需要手动定位解决。在 AOSP 桌面时钟文章中,详细介绍了其中遇到的一些问题以及解决方法。如下列出了一些在进行 AOSP 日历转换过程中遇到的问题。


用 open 关键词标记父类


我们遇到的问题之一是 Kotlin 父类和子类之间的相互调用。在 Kotlin 中,要将一个类标记为可继承,必须得在类的声明中添加 open 关键字,对于父类中被子类覆盖的方法也要这样做。但是在 Java 中的继承是不需要使用到 open 关键字的。由于 Kotlin 和 Java 能够相互调用,这个问题直到大部分代码文件转换到了 Kotlin 才出现。


例如,在下面的代码片段中,声明了一个继承于 SimpleWeeksAdapter 的类:


class MonthByWeekAdapter(context: Context?, params:
    HashMap<String?, Int?>) : SimpleWeeksAdapter(context as Context, params) {//方法体}

由于代码文件的转换过程是一次一个文件进行的,即使是完全将 SimpleWeeksAdapter.kt 文件转换成 Kotlin,也不会在其类的声明中出现 open 关键词,这样就会导致一个错误。所以之后需要手动进行 open 关键词的添加,以便让 SimpleWeeksAdapter 类可以被继承。这个特殊的类声明如下所示:


open class SimpleWeeksAdapter(context: Context, params: HashMap<String?, Int?>?) {//方法体}

override 修饰符


同样地,子类中覆盖父类的方法也必须使用 override 修饰符来进行标记。在 Java 中,这是通过 @Override 注解来实现的。然而,虽然在 Java 中有相应的注解实现版本,但是自动转换过程中并没有为 Kotlin 方法声明中添加 override 修饰符。解决的办法是在所有适当的地方手动添加 override 修饰符。


覆写父类中的属性


在重构过程中,我们还遇到了一个属性覆写的异常问题,当一个子类声明了一个变量,而在父类中存在一个非私有的同名变量时,我们需要添加一个 override 修饰符。然而,即使子类的变量同父类变量的类型不同,也仍然要添加 override 修饰符。在某些情况下,添加 override 仍不能解决问题,尤其是当子类的类型完全不同的时候。事实上,如果类型不匹配,在子类的变量前添加 override 修饰符,并在父类的变量前添加 open 关键字,会导致一个错误:


type of *property name* doesn’t match the type of the overridden var-property

这个报错很让人疑惑,因为在 Java 中,以下代码可以正常编译:


public class Parent {
int num = 0;
}

class Child extends Parent {
String num = "num";
}

而在 Kotlin 中相应的代码就会报上面提到的错误:


class Parent {
var num: Int = 0
}

class Child : Parent() {
var num: String = "num"
}


这个问题很有意思,目前我们通过在子类中对变量重命名来规避了这个冲突。上面的 Java 代码会被 Android Studio 目前提供的代码转换器转换为有问题的 Kotlin 代码,这甚至 被报告为是一个 bug 了。


import 语句


在我们转换的所有文件中,自动转换工具都倾向于将 Java 代码中的所有 import 语句截断为 Kotlin 文件中的第一行。最开始这导致了一些很让人抓狂的错误,编译器会在整个代码中报 "unknown references" 的错误。在意识到这个问题后,我们开始手动地将 Java 中的 import 语句粘贴到 Kotlin 代码文件中,并单独对其进行转换。


暴露成员变量


默认情况下,Kotlin 会自动地为类中的实例变量生成 getter 和 setter 方法。然而,有些时候我们希望一个变量仅仅只是一个简单的 Java 成员变量,这可以通过使用 @JvmField 注解来实现。


@JvmField 注解 的作用是 "指示 Kotlin 编译器不要为这个属性生成 getter 和 setter 方法,并将其作为一个成员变量允许其被公开访问"。这个注解在 CalendarData 类 中特别有用,它包含了两个 static final 变量。通过对使用 val 声明的只读变量使用 @JvmField 注解,我们确保了这些变量可以作为成员变量被其他类访问,从而实现了 Java 和 Kotlin 之间的兼容性。


对象中的静态方法


在 Kotlin 对象中定义的函数必须使用 @JvmStatic 进行标记,以允许在 Java 代码中通过方法名,而非实例化来对它们进行调用。也就是说,这个注解使其具有了类似 Java 的方法行为,即能够通过类名调用方法。根据 Kotlin 的文档,"编译器会为对象的外部类生成一个静态方法,而对于对象本身会生成一个实例方法。"我们在 Utils 文件 中遇到了这个问题,当完成转换后,Java 类就变成了 Kotlin 对象。随后,所有在对象中定义的方法都必须使用 @JvmStatic 标记,这样就允许在其他文件中使用 Utils.method() 这样的语法来进行调用。值得一提的是,在类名和方法名之间使用 .INSTANCE (即 Utils.INSTANCE.method()) 也是一种选择,但是这不太符合常见的 Java 语法,需要改变所有对 Java 静态方法的调用。


性能评估分析


所有的基准测试都是在一台 96 核、176 GiB 内存的机器上进行的。本项目中分析用到的主要指标有所减少的代码行数、目标 APK 的文件大小、构建时间和首屏从启动到显示的时间。在对上述每个因素进行分析的同时,我们还收集了每个参数的数据并以表格的方式进行了展示。


减少的代码行数



从 Java 完全转换到 Kotlin 后,代码行数从 18,004 减少到了 17,729。这比原来的 Java 代码量 减少了大约 1.5%。虽然减少的代码量并不可观,但对于一些大型应用来说,这种转换对于减少代码行数的效果可能更为显著,可参阅 AOSP 桌面时钟 文中所举的例子。


目标 APK 大小



使用 Kotlin 编写的应用 APK 大小是 2.7 MB,而使用 Java 编写的应用 APK 大小是 2.6 MB。可以说这个差异基本可以忽略不计了,由于包含了一些额外的 Kotlin 库,所以 APK 体积上的增加,实际上是可以预期的。这种大小的增加可以通过使用 ProguardR8 来进行优化。


编译时间



Kotlin 和 Java 应用的构建时间是通过取 10 次从零进行完整构建的时间的平均值来计算的 (不包含异常值),Kotlin 应用的平均构建时间为 13 分 27 秒,而 Java 应用的平均构建时间为 12 分 6 秒。据一些资料 (如 "Java 和 Kotlin 的区别" 以及 "Kotlin 和 Java 在编译时间上的对比") 显示,Kotlin 的编译时间事实上比 Java 要更耗时,特别是对于从零开始的构建。一些分析断言,Java 的编译速度会快 10-15%,又有一些分析称这一数据为 15-20%。拿我们的例子进行从零开始完整构建所花费的时间来说,Java 的编译速度比 Kotlin 快 11.2%,尽管这个微小的差异并不在上述范围内,但这有可能是因为 AOSP 日历是一个相对较小的应用,仅有 43 个类。尽管从零开始的完整构建比较慢,但是 Kotlin 仍然在其他方面占有优势,这些优势更应该被考虑到。例如,Kotlin 相对于 Java,更简洁的语法通常可以保证较少的代码量,这使得 Kotlin 代码库更易维护。此外,由于 Kotlin 是一种更为安全有效的编程语言,我们可以认为完整构建时间较慢的问题可以忽略不计。


首屏显示的时间



我们使用了这种 方法 来测试应用从启动到完全显示首屏所需要的时间,经过 10 次试验后我们发现,使用 Kotlin 应用的平均时间约为 197.7 毫秒,而 Java 的则为 194.9 毫秒。这些测试都是在 Pixel 3a XL 设备上进行的。从这个测试结果可以得出结论,与 Kotlin 应用相比,Java 应用可能具有微小的优势;然而,由于平均时间非常接近,这个差异几乎可以忽略不计。因此,可以说 AOSP 日历应用转换到 Kotlin,并没有对应用的初始启动时间产生负面影响。


结论


将 AOSP 日历应用转换为 Kotlin 大约花了 1.5 个月 (6 周) 的时间,由 2 名实习生负责该项目的实施。一旦我们对代码库更加熟悉并更加善于解决反复出现的编译时、运行时和语法问题时,效率肯定会变得更高。总的来说,这个特殊的项目成功地展示了 Kotlin 如何影响现有的 Android 应用,并在对 AOSP 应用进行转换的路途中迈出了坚实的一步。


欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!


作者:Android_开发者
链接:https://juejin.cn/post/7008056083331678245
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

中秋快乐!来看看满眼都是中秋气息的app页面吧~

前言:看了很久,大家是真的🐂🍺,月球绕地球都整出来了,那我也来给大家整上花活~然后送上中秋祝福:月儿圆又亮,月饼圆又甜,家家团圆相聚,人人欢心甜蜜,祝你家圆人圆事事圆,中秋愉快! 不妨点个赞啦,看到这篇文章的帅哥~ app中秋的引导界面:(完整效果截图在最后哦...
继续阅读 »

前言:看了很久,大家是真的🐂🍺,月球绕地球都整出来了,那我也来给大家整上花活~然后送上中秋祝福:月儿圆又亮,月饼圆又甜,家家团圆相聚,人人欢心甜蜜,祝你家圆人圆事事圆,中秋愉快!


不妨点个赞啦,看到这篇文章的帅哥~


app中秋的引导界面:(完整效果截图在最后哦~)


效果图.gif


功能解析:


1.状态变化:背景和展示出来的诗篇与日期有关,日期不同,背景和诗篇不同


2.文字特效:中秋祝福的诗篇会一字一字慢慢浮现


3.倒计时处理:人性化,用户不想看直接跳过


1.状态变化:


我们定义一个变量date来控制状态,获取当前的日期来进行判断:


int _date = 1; //控制状态
DateTime _dateTime = DateTime.now(); //获取当前时间

然后在初始化时进行判断:


@override
 void initState() {
   super.initState();
   if (_dateTime.day <= 19) {
     ///19号之前,人们都在回家的路上
     _date = 1;
  } else if (_dateTime.day == 20) {
     ///20号,人们回到家中,吃上团圆饭
     _date = 2;
  } else if (_dateTime.day == 21) {
     ///21号,中秋快乐
     _date = 3;
  } else {
     ///中秋过后,亲人回到忙碌的生活,期盼着下一次团聚
     _date = 4;
  }
}

关于flutter如何获取时间,我给大家列出来了(送给新人,大神看了就图一乐~)


DateTime dateTime= DateTime.now();
dateTime.day 今天是几号,int类型
dateTime.month
dateTime.year
dateTime.hour
dateTime.minute
dateTime.second
dateTime.millisecond
dateTime.millisecondsSinceEpoch

2.文字特效


就像开始的gif图显示的一样,文字一个个浮现出来,其实这个很简单,我们可以自己diy,但是,广大热心程序猿给我们提供了插件:animated_text_kit


使用起来也很简单:


AnimatedTextKit(
 animatedTexts: [
   TyperAnimatedText(
     "Test文字",
     textStyle: TextStyle(fontSize: 22),
     speed: const Duration(milliseconds: 200),
  ),
],
 isRepeatingAnimation: false,//不循环播放
)

而且还有很多很多的效果,这里给大家列了出来,需要的可以查看文章最下方的项目源码


当然,在这里也是有难点的,因为flutter的文字无法竖排,网上有改源码的(我觉得复杂了)问了下朋友,说使用RotatedBox这个widget,但是我这看个der啊,你这竖的一个妙啊!


屏幕截图 2021-09-14 190155.jpg


所以最后我选择使用给每个文字后面加上/n 我直接手动换行,求求大神来告诉我解决方法(要不我自己写个插件哈哈)


3.倒计时处理


我们搞前端的必须要做一个人性化的东西给客户是不是


手动跳转加上:


int _countdown = 5;//五秒倒计时
Timer _countdownTimer;//控制倒计时

当然我们需要一个方法来控制倒计时,以及倒计时结束跳转:


void _startRecordTime() {
 _countdownTimer = Timer.periodic(Duration(seconds: 1), (timer) {
   setState(() {
     if (_countdown <= 1) {
       ///此处编写你需要跳转的界面
        _countdownTimer.cancel();
        _countdownTimer = null;
    } else {
       _countdown -= 1;
    }
  });
});
}

当然,在倒计时结束或者跳转时,记得把界面销毁~


@override
void dispose() {
 super.dispose();
 print('启动页面结束');
 if (_countdownTimer != null && _countdownTimer.isActive) {
   _countdownTimer.cancel();
   _countdownTimer = null;
}
}

onTap: () {
 ///点击跳过,在此处可以写跳转
 print("点击跳过,在此处可以写跳转代码,记得销毁界面哦");
},

完整效果:
屏幕截图 2021-09-14 195121.jpg


屏幕截图 2021-09-14 195203.jpg


屏幕截图 2021-09-14 195320.jpg


屏幕截图 2021-09-14 195345.jpg


源码地址:gitee.com/Xiao-Ti/aut…


作者:阿Tya
链接:https://juejin.cn/post/7007756651197366303
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter自适应瀑布流

前言:在电商app经常会看到首页商品推荐的瀑布流,或者类似短视频app首页也是瀑布流,这些都是需要自适应的,才能给用户带来好的体验 话不多说先上效果图: 根据效果图可以分为四步: 1.图片自适应 2.自适应标签 3.上拉刷新和下拉加载 4.底部的点赞按钮可以...
继续阅读 »

前言:在电商app经常会看到首页商品推荐的瀑布流,或者类似短视频app首页也是瀑布流,这些都是需要自适应的,才能给用户带来好的体验


话不多说先上效果图:


在这里插入图片描述在这里插入图片描述


根据效果图可以分为四步:


1.图片自适应

2.自适应标签

3.上拉刷新和下拉加载

4.底部的点赞按钮可以去掉或者自己修改样式,我这里使用的like_button库

注:本文使用的库:为啥这么多呢,因为我把图片缓存这样东西都加上了,单纯的瀑布流就用waterfall_flow

waterfall_flow: ^3.0.1
extended_image: any
extended_sliver: any
ff_annotation_route_library: any
http_client_helper: any
intl: any
like_button: any
loading_more_list: any
pull_to_refresh_notification: any
url_launcher: any

1.图片自适应:


Widget image = Stack(
children: <Widget>[
ExtendedImage.network(
item.imageUrl,
shape: BoxShape.rectangle,
//clearMemoryCacheWhenDispose: true,
border: Border.all(color: Colors.grey.withOpacity(0.4), width: 1.0),
borderRadius: const BorderRadius.all(
Radius.circular(10.0),
),
loadStateChanged: (ExtendedImageState value) {
if (value.extendedImageLoadState == LoadState.loading) {
Widget loadingWidget = Container(
alignment: Alignment.center,
color: Colors.grey.withOpacity(0.8),
child: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor:
AlwaysStoppedAnimation<Color>(Theme.of(c).primaryColor),
),
);
if (!konwSized) {
//todo: not work in web
loadingWidget = AspectRatio(
aspectRatio: 1.0,
child: loadingWidget,
);
}
return loadingWidget;
} else if (value.extendedImageLoadState == LoadState.completed) {
item.imageRawSize = Size(
value.extendedImageInfo.image.width.toDouble(),
value.extendedImageInfo.image.height.toDouble());
}
return null;
},
),
Positioned(
top: 5.0,
right: 5.0,
child: Container(
padding: const EdgeInsets.all(3.0),
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.6),
border: Border.all(color: Colors.grey.withOpacity(0.4), width: 1.0),
borderRadius: const BorderRadius.all(
Radius.circular(5.0),
),
),
child: Text(
'${index + 1}',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: fontSize, color: Colors.white),
),
),
)
],
);
if (konwSized) {
image = AspectRatio(
aspectRatio: item.imageSize.width / item.imageSize.height,
child: image,
);
} else if (item.imageRawSize != null) {
image = AspectRatio(
aspectRatio: item.imageRawSize.width / item.imageRawSize.height,
child: image,
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
image,
const SizedBox(
height: 5.0,
),
buildTagsWidget(item),
const SizedBox(
height: 5.0,
),
buildBottomWidget(item),
],
);
}

2.自适应标签:


Widget buildTagsWidget(
TuChongItem item, {
int maxNum = 6,
}) {
const double fontSize = 12.0;
return Wrap(
runSpacing: 5.0,
spacing: 5.0,
children: item.tags.take(maxNum).map<Widget>((String tag) {
final Color color = item.tagColors[item.tags.indexOf(tag)];
return Container(
padding: const EdgeInsets.all(3.0),
decoration: BoxDecoration(
color: color,
border: Border.all(color: Colors.grey.withOpacity(0.4), width: 1.0),
borderRadius: const BorderRadius.all(
Radius.circular(5.0),
),
),
child: Text(
tag,
textAlign: TextAlign.start,
style: TextStyle(
fontSize: fontSize,
color: color.computeLuminance() < 0.5
? Colors.white
: Colors.black),
),
);
}).toList());
}

3.上拉刷新和下拉加载


class PullToRefreshHeader extends StatelessWidget {
const PullToRefreshHeader(this.info, this.lastRefreshTime, {this.color});
final PullToRefreshScrollNotificationInfo info;
final DateTime lastRefreshTime;
final Color color;
@override
Widget build(BuildContext context) {
if (info == null) {
return Container();
}
String text = '';
if (info.mode == RefreshIndicatorMode.armed) {
text = 'Release to refresh';
} else if (info.mode == RefreshIndicatorMode.refresh ||
info.mode == RefreshIndicatorMode.snap) {
text = 'Loading...';
} else if (info.mode == RefreshIndicatorMode.done) {
text = 'Refresh completed.';
} else if (info.mode == RefreshIndicatorMode.drag) {
text = 'Pull to refresh';
} else if (info.mode == RefreshIndicatorMode.canceled) {
text = 'Cancel refresh';
}

final TextStyle ts = const TextStyle(
color: Colors.grey,
).copyWith(fontSize: 13);

final double dragOffset = info?.dragOffset ?? 0.0;

final DateTime time = lastRefreshTime ?? DateTime.now();
final double top = -hideHeight + dragOffset;
return Container(
height: dragOffset,
color: color ?? Colors.transparent,
//padding: EdgeInsets.only(top: dragOffset / 3),
//padding: EdgeInsets.only(bottom: 5.0),
child: Stack(
children: <Widget>[
Positioned(
left: 0.0,
right: 0.0,
top: top,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: Container(
alignment: Alignment.centerRight,
child: RefreshImage(top),
margin: const EdgeInsets.only(right: 12.0),
),
),
Column(
children: <Widget>[
Text(
text,
style: ts,
),
Text(
'Last updated:' +
DateFormat('yyyy-MM-dd hh:mm').format(time),
style: ts.copyWith(fontSize: 12),
)
],
),
Expanded(
child: Container(),
),
],
),
)
],
),
);
}
}

class RefreshImage extends StatelessWidget {
const RefreshImage(this.top);
final double top;
@override
Widget build(BuildContext context) {
const double imageSize = 40;
return ExtendedImage.asset(
Assets.assets_fluttercandies_grey_png,
width: imageSize,
height: imageSize,
afterPaintImage: (Canvas canvas, Rect rect, ui.Image image, Paint paint) {
final double imageHeight = image.height.toDouble();
final double imageWidth = image.width.toDouble();
final Size size = rect.size;
final double y = (1 - min(top / (refreshHeight - hideHeight), 1)) *
imageHeight;

canvas.drawImageRect(
image,
Rect.fromLTWH(0.0, y, imageWidth, imageHeight - y),
Rect.fromLTWH(rect.left, rect.top + y / imageHeight * size.height,
size.width, (imageHeight - y) / imageHeight * size.height),
Paint()
..colorFilter =
const ColorFilter.mode(Color(0xFFea5504), BlendMode.srcIn)
..isAntiAlias = false
..filterQuality = FilterQuality.low);

//canvas.restore();
},
);
}
}

4.底部的点赞按钮


LikeButton(
size: 18.0,
isLiked: item.isFavorite,
likeCount: item.favorites,
countBuilder: (int count, bool isLiked, String text) {
final ColorSwatch<int> color =
isLiked ? Colors.pinkAccent : Colors.grey;
Widget result;
if (count == 0) {
result = Text(
'love',
style: TextStyle(color: color, fontSize: fontSize),
);
} else {
result = Text(
count >= 1000 ? (count / 1000.0).toStringAsFixed(1) + 'k' : text,
style: TextStyle(color: color, fontSize: fontSize),
);
}
return result;
},
likeCountAnimationType: item.favorites < 1000
? LikeCountAnimationType.part
: LikeCountAnimationType.none,
onTap: (bool isLiked) {
return onLikeButtonTap(isLiked, item);
},
)

这样自适应的瀑布流就完成了。


作者:阿Tya
链接:https://juejin.cn/post/7006876169471524901
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android 控制 ContentProvider的创建

序言 随着app隐私政策的收紧,现在不经过用户同意,就收集敏感信息的行为一旦被检测出来。很容易造成app下架。但是有些SDK的初始化是通过注册ContentProvider实现自动调用其onCreate()方法,来实现无感初始化的。如果SDK在ContentP...
继续阅读 »

序言


随着app隐私政策的收紧,现在不经过用户同意,就收集敏感信息的行为一旦被检测出来。很容易造成app下架。但是有些SDK的初始化是通过注册ContentProvider实现自动调用其onCreate()方法,来实现无感初始化的。如果SDK在ContentProvider中获取了敏感信息,又没有提供控制方法。我们就很被动。于是我花了点时间研究了怎么hook contentProvider的创建。让其在用户同意后再初始化。


方案1


声明在清单文件中的ContentProvider 会在应用启动后就创建。具体是在 ActivityThread的handleBindApplication方法中。(以下截图为Android 30的ActivityThread)
在这里插入图片描述
具体就在这一句
在这里插入图片描述


installContentProviders实现如下
在这里插入图片描述
最终是通过AppComponentFactory的instantiateProvider方法创建。
在这里插入图片描述
在这里插入图片描述


而AppComponentFactory是Android 28以后系统提供给我们的一个hook的工厂类。可以通过清单文件指定,在这里面可以hook 所有组件的初始化。
在这里插入图片描述
这么指定
在这里插入图片描述


但是在Android 28以下,比如这个截图是Android 25.没有这类,ContentProvider直接通过反射获得。无法通过该类来修改。
在这里插入图片描述


最终方案


为了兼容性,考虑如下方案。在调用installContentProviders前,如果这个data里面的providers为空岂不是不会走installContentProviders方法了吗。
在这里插入图片描述
这个data 是一个AppBindData类型,通过handleBindApplication方法的参数传入。会保存到ActivityThread的 mBoundApplication 字段中。
在这里插入图片描述


于是就可以通过获取这个mBoundApplication 字段中的providers 来保存要初始化的provider。再讲providers置为空即可。到了用户同意以后,再去通过反射调用ActivityThread的installContentProviders方法即可。
在这里插入图片描述


hook时机


这个时机只有Application的attachBaseContext方法中。该方法会比installContentProviders提前执行。


最后的代码App中


public class MyApp extends Application {

static MyApp app;

/**
*用户同意
*/
public static void agree(Action action) {
HookUtil.initProvider(app);
action.doAction();
}

public interface Action {
void doAction();
}


@Override
protected void attachBaseContext(Context base) {
app = this;
try {
HookUtil.attachContext();
} catch (Exception e) {
e.printStackTrace();
}
super.attachBaseContext(base);
}
}


HookUtil


package com.zgh.testcontentprovider;

import android.content.Context;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;


/**
* Created by zhuguohui
* Date: 2021/9/13
* Time: 11:23
* Desc:
*/
public class HookUtil {

private static Object providers;

private static Method installContentProvidersMethod;
private static Object currentActivityThread;

/*
*用户同意后调用
*/
public static void initProvider(Context context){
try {
installContentProvidersMethod.invoke(currentActivityThread,context,providers);
} catch (Exception e) {
e.printStackTrace();
}
}

public static void attachContext() throws Exception {

// 先获取到当前的ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
//currentActivityThread是一个static函数所以可以直接invoke,不需要带实例参数
currentActivityThread = currentActivityThreadMethod.invoke(null);

hookInstallContentProvider(activityThreadClass);

}

private static void hookInstallContentProvider(Class activityThreadClass) throws Exception{
Field appDataField = activityThreadClass.getDeclaredField("mBoundApplication");
appDataField.setAccessible(true);
Object appData= appDataField.get(currentActivityThread);
Field providersField= appData.getClass().getDeclaredField("providers");
providersField.setAccessible(true);
providers = providersField.get(appData);
//清空provider,避免有些sdk通过provider来初始化
providersField.set(appData,null);

installContentProvidersMethod = activityThreadClass.getDeclaredMethod("installContentProviders", Context.class, List.class);
installContentProvidersMethod.setAccessible(true);
}
}


搭配


搭配我之前写的工具,可以更完美的实现用户同意之前不初始化任何SDK的目标
通过拦截 Activity的创建 实现APP的隐私政策改造


作者:solo_99
链接:https://juejin.cn/post/7007338307075964942
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

不做跟风党,LiveData,StateFlow,SharedFlow 使用场景对比

Android 常用的分层架构 Android 中加载 UI 数据不是一件轻松的事,开发者经常需要处理各种边界情况。如各种生命周期和因为「配置更改」导致的 Activity 的销毁与重建。 「配置更改」的场景有很多:屏幕旋转,切换至多窗口模式,调整窗口大小...
继续阅读 »

Android 常用的分层架构


Android 中加载 UI 数据不是一件轻松的事,开发者经常需要处理各种边界情况。如各种生命周期和因为「配置更改」导致的 Activity 的销毁与重建。



「配置更改」的场景有很多:屏幕旋转,切换至多窗口模式,调整窗口大小,浅色模式与暗黑模式的切换,更改默认语言,更改字体大小等等



因此普遍处理方式是使用分层的架构。这样开发者就可以编写独立于 UI 的代码,而无需过多考虑生命周期,配置更改等场景。 例如,我们可以在表现层(Presentation Layer)的基础上添加一个领域层(Domain Layer) 来保存业务逻辑,使用数据层(Data Layer)对上层屏蔽数据来源(数据可能来自远程服务,可能是本地数据库)。



表现层可以分成具有不同职责的组件:



  • View:处理生命周期回调,用户事件和页面跳转,Android 中主要是 Activity 和 Fragment

  • Presenter 或 ViewModel:向 View 提供数据,并不了解 View 所处的生命周期,通常生命周期比 View 长


Presenter 和 ViewModel 向 View 提供数据的机制是不同的,简单来说:



  • Presenter 通过持有 View 的引用并直接调用操作 View,以此向 View 提供数据

  • ViewModel 通过将可观察的数据暴露给观察者来向 View 提供数据


官方提供的可观察的数据 组件是 LiveData。Kotlin 1.4.0 正式版发布之后,开发者有了新的选择:StateFlowSharedFlow


最近网上流传出「LiveData 被弃用,应该使用 Flow 替代 LiveData」的声音。


LiveData 真的有那么不堪吗?Flow 真的适合你使用吗?


不人云亦云,只求接近真相。我们今天来讨论一下这两种组件。


ViewModel + LiveData


为了实现高效地加载 UI 数据,获得最佳的用户体验,应实现以下目标:



  • 目标1:已经加载的数据无需在「配置更改」的场景下再次加载

  • 目标2:避免在非活跃状态(不是 STARTEDRESUMED)下加载数据和刷新 UI

  • 目标3:「配置更改」时不会中断的工作


Google 官方在 2017 年发布了架构组件库:使用 ViewModel + LiveData 帮助开发者实现上述目标。



相信很多人在官方文档中见过这个图,ViewModelActivity/Fragment 的生命周期更长,不受「配置更改」导致 Activity/Fragment 重建的影响。刚好满足了目标 1 和目标 3。


LiveData 是可生命周期感知的。 新值仅在生命周期处于 STARTEDRESUMED 状态时才会分配给观察者,并且观察者会自动取消注册,避免了内存泄漏。 LiveData 对实现目标 1 和 目标 2 很有用:它缓存其持有的数据的最新值,并将该值自动分派给新的观察者。


LiveData 的特性


既然有声音说「LiveData 要被弃用了」,那么我们先对 LiveData 进行一个全面的了解。聊聊它能做什么,不能做什么,以及使用过程中有哪些要注意的地方。


LiveData 是 Android Jetpack Lifecycle 组件中的内容。属于官方库的一部分,Kotlin/Java 均可使用。


一句话概括 LiveDataLiveData 是可感知生命周期的,可观察的,数据持有者


它的能力和作用很简单:更新 UI


它有一些可以被认为是优点的特性:



  • 观察者的回调永远发生在主线程

  • 仅持有单个且最新的数据

  • 自动取消订阅

  • 提供「可读可写」和「仅可读」两个版本收缩权限

  • 配合 DataBinding 实现「双向绑定」


观察者的回调永远发生在主线程


这个很好理解,LiveData 被用来更新 UI,因此 ObserveronChanged() 方法在主线程回调。



背后的原理也很简单,LiveDatasetValue() 发生在主线程(非主线程调用会抛异常,postValue() 内部会切换到主线程调用 setValue())。之后遍历所有观察者的 onChanged() 方法。


仅持有单个且最新的数据


作为数据持有者(data holder),LiveData 仅持有 单个最新 的数据。


单个且最新,意味着 LiveData 每次持有一个数据,并且新数据会覆盖上一个。


这个设计很好理解,数据决定了 UI 的展示,绘制 UI 时肯定要使用最新的数据,「过时的数据」应该被忽略。



配合 Lifecycle,观察者只会在活跃状态下(STARTEDRESUMED)接收到 LiveData 持有的最新的数据。在非活跃状态下绘制 UI 没有意义,是一种资源的浪费。



自动取消订阅


这是 LiveData 可感知生命周期的重要表现,自动取消订阅意味着开发者无需手动写那些取消订阅的模板代码,降低了内存泄漏的可能性。


背后原理是在生命周期处于 DESTROYED 时,移除观察者。



提供「可读可写」和「仅可读」两个版本




点击查看代码
public abstract class LiveData<T> {
@MainThread
protected void setValue(T value) {
// ...
}

protected void postValue(T value) {
// ...
}

@Nullable
public T getValue() {
// ...
}
}

public class MutableLiveData<T> extends LiveData<T> {
@Override
public void postValue(T value) {
super.postValue(value);
}
@Override
public void setValue(T value) {
super.setValue(value);
}
}
复制代码



抽象类 LiveDatasetValue()postValue() 是 protected,而其实现类 MutableLiveData 均为 public。



LiveData 提供了 mutable(MutableLiveData) 和 immutable(LiveData) 两个类,前者「可读可写」,后者「仅可读」。通过权限的细化,让使用者各取所需,避免由于权限泛滥导致的数据异常。




点击查看代码
class SharedViewModel : ViewModel() {
private val _user : MutableLiveData<User> = MutableLiveData()

val user : LiveData<User> = _user

fun setUser(user: User) {
_user.posetValue(user)
}
}
复制代码


配合 DataBinding 实现「双向绑定」


LiveData 配合 DataBinding 可以实现 更新数据自动驱动 UI 变化,如果使用「双向绑定」还能实现 UI 变化影响数据的变化。




以下也是 LiveData 的特性,但我不会将其归类为「设计缺陷」或「LiveData 的缺点」。作为开发者应了解这些特性并在使用过程中正确处理它们。



  • value 是 nullable 的

  • 在 fragment 订阅时需要传入正确的 lifecycleOwner

  • LiveData 持有的数据是「事件」时,可能会遇到「粘性事件

  • LiveData 是不防抖的

  • LiveDatatransformation 工作在主线程


value 是 nullable 的




点击查看代码
@Nullable
public T getValue() {
Object data = mData;
if (data != NOT_SET) {
return (T) data;
}
return null;
}
复制代码


LiveData#getValue() 是可空的,使用时应该注意判空。


使用正确的 lifecycleOwner


fragment 调用 LiveData#observe() 方法时传入 thisviewLifecycleOwner 是不一样的。


原因之前写过,此处不再赘述。感兴趣的小伙伴可以移步查看


AS 在 lint 检查时会避免开发者犯此类错误。



粘性事件


官方在 [译] 在 SnackBar,Navigation 和其他事件中使用 LiveData(SingleLiveEvent 案例) 一文中描述了一种「数据只会消费一次」的场景。如展示 Snackbar,页面跳转事件或弹出 Dialog。


由于 LiveData 会在观察者活跃时将最新的数据通知给观察者,则会产生「粘性事件」的情况。


如点击 button 弹出一个 Snackbar,在屏幕旋转时,lifecycleOwner 重建,新的观察者会再次调用 Livedata#observe(),因此 Snackbar 会再次弹出。


解决办法是:将事件作为状态的一部分,在事件被消费后,不再通知观察者。这里推荐两种解决方案:



默认不防抖


setValue()/postValue() 传入相同的值多次调用,观察者的 onChanged() 会被多次调用。


严格讲这不算一个问题,看具体的业务场景,处理也很容易,调用 setValue()/postValue() 前判断一下 vlaue 与之前是否相同即可。




点击查看代码
class MainViewModel {
private val _username = MutableLiveData<String>()
val username: LiveData<String> = _username

fun setUsername(username: String) {
if (_username.value != username)
_headerText.postValue(username)
}
}
复制代码


transformation 工作在主线程


有些时候我们从 repository 层拿到的数据需要进行处理,例如从数据库获得 User List,我们想根据 id 获取某个 User。


此时我们可以借助 MediatorLiveDataTransformatoins 来实现:





点击查看代码
class MainViewModel {
val viewModelResult = Transformations.map(repository.getDataForUser()) { data ->
convertDataToMainUIModel(data)
}
}
复制代码


mapswitchMap 内部均是使用 MediatorLiveData#addSource() 方法实现的,而该方法会在主线程调用,使用不当会有性能问题。




点击查看代码
@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
Source<S> e = new Source<>(source, onChanged);
Source<?> existing = mSources.putIfAbsent(source, e);
if (existing != null && existing.mObserver != onChanged) {
throw new IllegalArgumentException(
"This source was already added with the different observer");
}
if (existing != null) {
return;
}
if (hasActiveObservers()) {
e.plug();
}
}
复制代码


我们可以借助 Kotlin 协程和 RxJava 实现异步任务,最后在主线程上返回 LiveData。如 androidx.lifecycle:lifecycle-livedata-ktx 提供了这样的写法




点击查看代码
val result: LiveData<Result> = liveData {
val data = someSuspendingFunction() // 协程中处理
emit(data)
}
复制代码


LiveData 小结




  • LiveData 作为一个 可感知生命周期的,可观察的,数据持有者,被设计用来更新 UI




  • LiveData 很轻,功能十分克制,克制到需要配合 ViewModel 使用才能显示其价值




  • 由于 LiveData 专注单一功能,因此它的一些方法使用上是有局限性的,即通过设计来强制开发者按正确的方式编码(如观察者仅在主线程回调,避免了开发者在子线程更新 UI 的错误操作)




  • 由于 LiveData 专注单一功能,如果想在表现层之外使用它,MediatorLiveData 的操作数据的能力有限,仅有的 mapswitchMap 发生在主线程。可以在 switchMap 中使用协程或 RxJava 处理异步任务,最后在主线程返回 LiveData。如果项目中使用了 RxJavaAutoDispose,甚至可以不使用 LiveData,关于 Kotlin 协程的 Flow,我们后文介绍。




  • 笔者不喜欢将 LiveData 改造成 bus 使用,让组件做其分内的事(此条属于个人观点)




Flow


Flow 是 Kotlin 语言提供的功能,属于 Kotlin 协程的一部分,仅 Kotlin 使用。


Kotlin 协程被用来处理异步任务,而 Flow 则是处理异步数据流。


那么 suspend 方法和 Flow 的区别是什么?各自的使用场景是哪些?


一次性调用(One-shot Call)与数据流(data stream)



假如我们的 app 的某一屏里显示以下元素,其中红框部分实时性不高,不必很频繁的刷新,转发和点赞属于实时性很高的数据,需要定时刷新。



对于实时性不高的数据,我们可以使用 Kotlin 协程处理(此处数据的请求是异步任务):


suspend fun loadData(): Data

uiScope.launch {
val data = loadData()
updateUI(data)
}

而对于实时性较高的数据,挂起函数就无能为力了。有的小伙伴可能会说:「返回个 List 不就行了嘛」。其实无论返回什么类型,这种操作都是 One-shot Call,一次性的请求,有了结果就结束。


示例中的点赞和转发,需要一个 数据是异步计算的,能够 按顺序 提供 多个值 的结构,在 Kotlin 协程中我们有 Flow。


fun dataStream(): Flow<Data>

uiScope.launch {
dataStream().collect { data ->
updateUI(data)
}
}


当点赞或转发数发生变化时,updateUI() 会被执行,UI 根据最新的数据更新



Flow 的三驾马车


FLow 中有三个重要的概念:



  • 生产者(Producer)

  • 消费者(Consumer)

  • 中介(Intermediaries)


生产者提供数据流中的数据,得益于 Kotlin 协程,Flow 可以 异步地生产数据


消费者消费数据流内的数据,上面的示例中,updateUI() 方法是消费者。


中介可以对数据流中的数据进行更改,甚至可以更改数据流本身,我们可以借助官方视频中的动画来理解:



在 Android 中,数据层的 DataSource/Repository 是 UI 数据的生产者;而 view/ViewModel 是消费者;换一个角度,在表现层中,view 是用户输入事件的生产者(例如按钮的点击),其它层是消费者。


「冷流」与「热流」


你可能见过这样的描述:「流是冷的」



简单来说,冷流指数据流只有在有消费者消费时才会生产数据。


val dataFlow = flow {
// 代码块只有在有消费者 collect 后才会被调用
val data = dataSource.fetchData()
emit(data)
}

...

dataFlow.collect { ... }

有一种特殊的 Flow,如 StateFlow/SharedFlow ,它们是热流。这些流可以在没有活跃消费者的情况下存活,换句话说,数据在流之外生成然后传递到流。



BroadcastChannel 未来会在 Kotlin 1.6.0 中弃用,在 Kotlin 1.7.0 中删除。它的替代者是 StateFlowSharedFlow



StateFlow


StateFlow 也提供「可读可写」和「仅可读」两个版本。


SateFlow 实现了 SharedFlowMutableStateFlow 实现 MutableSharedFlow



StateFlowLiveData 十分像,或者说它们的定位类似。


StateFlowLiveData 有一些相同点:




  • 提供「可读可写」和「仅可读」两个版本(StateFlowMutableStateFlow




  • 它的值是唯一的




  • 它允许被多个观察者共用 (因此是共享的数据流)




  • 它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的




  • 支持 DataBinding




它们也有些不同点:



  • 必须配置初始值

  • value 空安全

  • 防抖


MutableStateFlow 构造方法强制赋值一个非空的数据,而且 value 也是非空的。这意味着 StateFlow 永远有值




StateFlow 的 emit()tryEmit() 方法内部实现是一样的,都是调用 setValue()



StateFlow 默认是防抖的,在更新数据时,会判断当前值与新值是否相同,如果相同则不更新数据。



SharedFlow


SateFlow 一样,SharedFlow 也有两个版本:SharedFlowMutableSharedFlow



那么它们有什么不同?



  • MutableSharedFlow 没有起始值

  • SharedFlow 可以保留历史数据

  • MutableSharedFlow 发射值需要调用 emit()/tryEmit() 方法,没有 setValue() 方法



MutableSharedFlow 不同,MutableSharedFlow 构造器中是不能传入默认值的,这意味着 MutableSharedFlow 没有默认值。


val mySharedFlow = MutableSharedFlow<Int>()
val myStateFlow = MutableStateFlow<Int>(0)
...
mySharedFlow.emit(1)
myStateFlow.emit(1)

SateFlowSharedFlow 还有一个区别是 SateFlow 只保留最新值,即新的订阅者只会获得最新的和之后的数据。


SharedFlow 根据配置可以保留历史数据,新的订阅者可以获取之前发射过的一系列数据。



后文会介绍背后的原理



它们被用来应对不同的场景:UI 数据是状态还是事件


状态(State)与事件(Event)


状态可以是的 UI 组件的可见性,它始终具有一个值(显示/隐藏)


而事件只有在满足一个或多个前提条件时才会触发,不需要也不应该有默认值


为了更好地理解 SateFlowSharedFlow 的使用场景,我们来看下面的示例:



  1. 用户点击登录按钮

  2. 调用服务端验证登录合法性

  3. 登录成功后跳转首页


我们先将步骤 3 视为 状态 来处理:



使用状态管理还有与 LiveData 一样的「粘性事件」问题,如果在 ViewNavigationState 中我们的操作是弹出 snackbar,而且已经弹出一次。在旋转屏幕后,snackbar 会再次弹出。



如果我们将步骤 3 作为 事件 处理:



使用 SharedFlow 不会有「粘性事件」的问题,MutableSharedFlow 构造函数里有一个 replay 的参数,它代表着可以对新订阅者重新发送多个之前已发出的值,默认值为 0。



SharedFlow 在其 replayCache 中保留特定数量的最新值。每个新订阅者首先从 replayCache 中取值,然后获取新发射的值。replayCache 的最大容量是在创建 SharedFlow 时通过 replay 参数指定的。replayCache 可以使用 MutableSharedFlow.resetReplayCache 方法重置。


replay 为 0 时,replayCache size 为 0,新的订阅者获取不到之前的数据,因此不存在「粘性事件」的问题。


StateFlowreplayCache 始终有当前最新的数据:



至此, StateFlowSharedFlow 的使用场景就很清晰了:


状态(State)用 StateFlow ;事件(Event)用 SharedFlow  


StateFlow,SharedFlow 与 LiveData 的使用对比


LiveData StateFlow SharedFlow 在 ViewModel 中的使用



上图分别展示了 LiveDataStateFlowSharedFlowViewModel 中的使用。


其中 LiveDataViewModel 中使用 LiveEventLiveData 处理「粘性事件


FlowViewModel 中使用 SharedFlow 处理「粘性事件


emit() 方法是挂起函数,也可以使用 tryEmit()



LiveData StateFlow SharedFlow 在 Fragment 中的使用



注意:Flow 的 collect 方法不能写在同一个 lifecycleScope


flowWithLifecyclelifecycle-runtime-ktx:2.4.0-alpha01 后提供的扩展方法



Flow 在 fragment 中的使用要比 LiveData 繁琐很多,我们可以封装一个扩展方法来简化:



关于 repeatOnLifecycle 的设计问题,可以移步 设计 repeatOnLifecycle API 背后的故事


使用 collect 方法时要注意一个问题。



这种写法是错误的!


viewModel.headerText.collect 在协程被取消前会一直挂起,这样后面的代码便不会执行。


Flow 与 RxJava


FlowRxJava 的定位很接近,限于篇幅原因,此处不展开讲,本节只罗列一下它们的对应关系:




  • Flow = (cold) Flowable / Observable / Single




  • Channel = Subjects




  • StateFlow = BehaviorSubjects (永远有值)




  • SharedFlow = PublishSubjects (无初始值)




  • suspend function = Single / Maybe / Completable




参考文档与推荐资源



总结




  • LiveData 的主要职责是更新 UI,要充分了解其特性,合理使用




  • Flow 可分为生产者,消费者,中介三个角色




  • 冷流和热流最大的区别是前者依赖消费者 collect 存在,而热流一直存在,直到被取消




  • StateFlowLiveData 定位相似,前者必须配置初始值,value 空安全并且默认防抖




  • StateFlowSharedFlow 的使用场景不同,前者适用于「状态」,后者适用于「事件」




回到文章开头的话题,LiveData 并没有那么不堪,由于其作用单一,功能简单,简单便意味着不易出错。所以在表现层中ViewModel 向 view 暴露 LiveData 是一个不错的选择。而在 RepositoryDataSource 中,我们可以利用 LiveData + 协程来处理数据的转换。当然,我们也可以使用功能更强大的 Flow


LiveDataStateFLowSharedFlow,它们都有着各自的使用场景。并且如果使用不当,都会或多或少地遇到一些所谓的「坑」。因此在使用某个组件时,要充分了解其设计缘由以及相关特性,否则就会掉进陷阱,收到不符合预期的行为。


作者:Flywith24
链接:https://juejin.cn/post/7007602776502960165
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

android 事件分发机制

Android 事件分发机制解析1. view的事件分发机制view的事件分发是从 dispatchTouchEvent() 开始的,直接上代码;public boolean dispatchTouchEvent(MotionEvent event) { ...
继续阅读 »

Android 事件分发机制解析

1. view的事件分发机制

view的事件分发是从 dispatchTouchEvent() 开始的,直接上代码;

public boolean dispatchTouchEvent(MotionEvent event) {  
boolean result = false;
// 1. view 是否可以点击 && setOnTouchListener 有值 并且 setOnTouchListener 返回值是true 事件分发 结束

if ( (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener != null &&
mOnTouchListener.onTouch(this, event)) {
boolean result = true;

}
// 2.如果上述条件不都成立 执行 OnTouchEvent();
if (!result && onTouchEvent(event)) {
result = true;
}


return result;
}

/**
* 分析1:onTouchEvent()
*/
public boolean onTouchEvent(MotionEvent event) {



// 若该控件可点击,则进入switch判断中
if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {

// 根据当前事件类型进行判断处理
switch (event.getAction()) {

// a. 事件类型=抬起View(主要分析)
case MotionEvent.ACTION_UP:
performClick();
// ->>分析2
break;

// b. 事件类型=按下View
case MotionEvent.ACTION_DOWN:
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;

// c. 事件类型=结束事件
case MotionEvent.ACTION_CANCEL:
refreshDrawableState();
removeTapCallback();
break;

// d. 事件类型=滑动View
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();

int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
removeLongPressCallback();
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}

// 若该控件可点击,就一定返回true
return true;
}
// 若该控件不可点击,就一定返回false
return false;
}

**
* 分析2:performClick()
*/
public boolean performClick() {

if (mOnClickListener != null) {
// 只要通过setOnClickListener()为控件View注册1个点击事件
// 那么就会给mOnClickListener变量赋值(即不为空)
// 则会往下回调onClick() & performClick()返回true
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}

总结:如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

image.png

2.ViewGroup 事件分发机制

viewGroup事件分发可以分为以下阶段:1.点击事件是down,将mFirstTarget、其他的标记、状态值清空,2.检查当前事件是否被拦截,3.如果被拦截,当前的事件不会分发给子view(firstTarget为空),会交由viewGroup父类的dispatchTouchEvent处理;4.如果不拦截,会找到一个满足条件的子view,分发此次的down事件;5.如果找不到满足条件的子view,firstTouch=null,就会调用自身的dispatchTouchEvent;6.如何当前点击事件是move、up时;6.如果找到了符合条件的子view,把down事件分发给子view,并对firstTouchTarget赋值,down事件分发结束;7.接来下就是move、up事件的分发,如果down事件分发给子view了,会再次判断是否拦截;8.如果不拦截,就会把move、up分发给mFirstTouchTarget对应的子view;9.如果拦截,会分发一个cancel事件给firstTouchTarget对应的子view。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

// 1.将firstTouchTarget置空,其他状态清空
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}

final boolean intercepted;


/***
* 2.检查是否拦截事件,如果点击事件是down、或者事件已经分发给子view,通过viewGroup的
* onInterceptTouchEvent 判断
*
*/

if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}


TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 3. 如果事件没有被拦截,会需找一个满足条件的子view分发事件
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {


if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
/***
*4. 如果有子view可以分发当前的事件,对newTouchTarget,firstTouchTarget赋值,记
* 消费本次事件的view
*/
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}

if (mFirstTouchTarget == null) {
// 5. 事件交由viewGroup父类的dispatchTouchEvent 处理
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 6.找到符合条件的子view,该事件分发结束
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}

if (cancelChild) { // 如果拦截了事件,清空 firstTouchTarget
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}

}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;


final int oldAction = event.getAction();
// 如果是子view消费了viewGroup分发的事件,后续事件被viewGroup拦截,viewGroup会发送一
cancel事件给firstTouchTarget对应的子view,该事件结束。下一个事件就不会再分发给子view了。

if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}


final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;


if (newPointerIdBits == 0) {
return false;
}

if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}

handled = child.dispatchTouchEvent(transformedEvent);
}
transformedEvent.recycle();
return handled;
}

3.滑动冲突

3.1 滑动冲突场景

方向一致:父容器和子view的滑动方向一致,如:scrollView 嵌套一个recyclewView 方向不一致:父容器和子view的滑动方向不一致,如scrollView 嵌套一个 viewPage。

3.2 外部拦截法

子view需要处理事件时,在父容器里面通过onInterceptTouchEvent返回值为false,让事件交由子view处理;当父容器需要处理事件时,让onInterceptTouchEvent返回值未true,让父容器拦截子view的事件,自己处理事件。伪代码如下:

public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted=false;
int x= (int) event.getX();
int y= (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted=false;//必须不能拦截,否则后续的ACTION_MOME和ACTION_UP事件都会拦截。
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要当前点击事件){
intercepted=true;
}else {
intercepted=false;
}
break;
case MotionEvent.ACTION_UP:
intercepted=false;
break;

default:
break;
}
mLastXIntercept=x;
mLastXIntercept=y;
return intercepted;
}

3.3 内部拦截法

if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}

内部拦截法通过子view(requestDisallowInterceptTouchEvent(disallowIntercept=false),disallowIntercept=false)改变viewGroup的disallowIntercept值,来干预viewGroup是否拦截子view。从上面代码,我们可以知道:disallowIntercept只能控制让viewGroup不拦截子view,拦截子view是通过viewGroup的 onInterceptTouchEvent方法值控制的。所以内部拦截法,就是结合viewGroup的 onInterceptTouchEvent方法和view通过viewgroup.requestDisallowInterceptTouchEvent改变 disallowIntercept值共同来完成。

// 重写 viewGroup  onInterceptTouchEvent方法,down返回值不能为false
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}

//  重写子view的 dispatchTouchEvent事件 
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
//如果是左右滑动
if (父容器) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
}
mLastXIntercept = x;
mLastYIntercept = y;
return super.dispatchTouchEvent(ev);
}


收起阅读 »

自定义可点击可滑动的通用RatingBar

介绍一个可以设置间距,设置选中未选中图标及数量,选中图标的类型(整,半,任意),可点击,可滑动选择的类似原生RatingBar的自定义View。效果图预览实现自定义属性<declare-styleable name="CommonRatingBar"&g...
继续阅读 »

介绍

一个可以设置间距,设置选中未选中图标及数量,选中图标的类型(整,半,任意),可点击,可滑动选择的类似原生RatingBar的自定义View。

效果图预览

untitled.gif

实现

自定义属性

<declare-styleable name="CommonRatingBar">
<attr name="starCount" format="integer" />
<attr name="starPadding" format="dimension" />
<!-- 默认选中时的图标,可不设置,使用纯色starColor -->
<attr name="starDrawable" format="reference" />
<!-- 默认未选中时的图标 -->
<attr name="starBgDrawable" format="reference" />
<!-- 纯色样式 -->
<attr name="starColor" format="color" />
<attr name="starClickable" format="boolean" />
<attr name="starScrollable" format="boolean" />
<attr name="starType" format="enum">
<enum name="normal" value="0" />
<enum name="half" value="1" />
<enum name="whole" value="2" />
</attr>
</declare-styleable>

测量View

将控件的高度设置为测量高度,测量宽度为星星的数量+每个星星之间的padding

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
starSize = measuredHeight
setMeasuredDimension(starCount * starSize + (starCount - 1) * starPadding.toInt(), starSize)
}

绘制ratingbar

  1. 绘制未选中的背景
/**
* 未选中Bitmap
*/

private val starBgBitmap: Bitmap by lazy {
val bitmap = Bitmap.createBitmap(starSize, starSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val starDrawable = ContextCompat.getDrawable(context, starBgDrawable)
starDrawable?.setBounds(0, 0, starSize, starSize)
starDrawable?.draw(canvas)
bitmap
}

/**
* 绘制星星默认未选中背景
*/

private fun drawStar(canvas: Canvas) {
for (i in 0 until starCount) {
val starLeft = i * (starSize + starPadding)
canvas.drawBitmap(starBgBitmap, starLeft, 0f, starBgPaint)
}
}
  1. 绘制选中图标

这里bitmap宽度使用starSize + starPadding,配合BitmapShader的repeat模式,可以方便绘制出高亮的图标

/**
* 选中icon的Bitmap
*/

private val starBitmap: Bitmap by lazy {
val bitmap = Bitmap.createBitmap(starSize + starPadding.toInt(), starSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val starDrawable = ContextCompat.getDrawable(context, starDrawable)
starDrawable?.setBounds(0, 0, starSize, starSize)
starDrawable?.draw(canvas)
bitmap
}

/**
* 绘制高亮图标
*/

private fun drawStarDrawable(canvas: Canvas) {
starDrawablePaint.shader = BitmapShader(starBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
canvas.drawRect(0f, 0f, getStarProgressWidth(), height.toFloat(), starDrawablePaint)
}
  1. 绘制纯色的选中效果

使用离屏缓冲,纯色矩形与未选中背景相交的地方进行显示。具体使用可以参考扔物线大佬的文章

/**
* 星星纯色画笔
*/

private val starPaint = Paint().apply {
isAntiAlias = true
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
}

canvas?.let {
// xfermode需要使用离屏缓存
val saved = it.saveLayer(null, null)
drawStar(it)
if (starDrawable == -1) {
drawStarBgColor(it)
} else {
drawStarDrawable(it)
}
it.restoreToCount(saved)
}

/**
* 绘制高亮纯颜色
*/

private fun drawStarBgColor(canvas: Canvas) {
canvas.drawRect(0f, 0f, getStarProgressWidth(), height.toFloat(), starPaint)
}
  1. 绘制进度

根据type更正显示效果,是取半,取整还是任意取进度。open方法,可以方便修改

/**
* 获取星星绘制宽度
*/

private fun getStarProgressWidth(): Float {
val percent = progress / 100f
val starDrawCount = percent * starCount
return when (starType) {
StarType.HALF.ordinal -> {
ceilHalf(starDrawCount) * starSize + starDrawCount.toInt() * starPadding
}
StarType.WHOLE.ordinal -> {
ceilWhole(starDrawCount) * starSize + starDrawCount.toInt() * starPadding
}
else -> {
starDrawCount * starSize + starDrawCount.toInt() * starPadding
}
}
}

/**
* 取整规则
*/

private fun ceilWhole(x: Float): Float {
return ceil(x)
}

/**
* 取半规则
*/

private fun ceilHalf(x: Float): Float {
// 四舍五入 1.3->1+0.5->1.5 1.7->2
val round = round(x)
return when {
round < x -> round + 0.5f
round > x -> round
else -> x
}
}
  1. 点击+滑动

点击+滑动就是重写onTouchEvent事件:

  • 判断点击位置是否在范围内
/**
* 点击的point是否在view范围内
*/

private fun pointInView(x: Float, y: Float): Boolean {
return Rect(0, 0, width, height).contains(x.toInt(), y.toInt())
}
  • 记录按下位置,抬起位置。
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
}
MotionEvent.ACTION_UP -> {
if (starClickable && abs(event.y - downY) <= touchSlop && abs(event.x - downX) <= touchSlop && pointInView(event.x, event.y)) {
parent.requestDisallowInterceptTouchEvent(true)
val progress = (event.x / width * 100).toInt()
setProgress(progress)
listener?.onClickProgress(progress)
} else {
parent.requestDisallowInterceptTouchEvent(false)
}
}
  • 滑动记录手指move
MotionEvent.ACTION_MOVE -> {
if (starScrollable && abs(event.x - downX) - abs(event.y - downY) >= touchSlop && pointInView(event.x, event.y)) {
parent.requestDisallowInterceptTouchEvent(true)
val progress = (event.x / width * 100).toInt()
setProgress(progress)
listener?.onScrollProgress(progress)
} else {
parent.requestDisallowInterceptTouchEvent(false)
}
}
  1. 添加监听

添加OnCommonRatingBarListener,监听点击事件以及滑动事件,返回进度

click.gif

完整实现代码

class CommonRatingBar(context: Context, attrs: AttributeSet?) : View(context, attrs) {

/**
* 星星数量
*/

private var starCount = 5

/**
* 星星间隔
*/

private var starPadding = 0f

/**
* 星星大小
*/

private var starSize = 30

/**
* 星星选中背景图
*/

private var starDrawable: Int = -1

/**
* 星星未选中背景图
*/

private var starBgDrawable: Int = -1

/**
* 星星选择类型
*/

private var starType = StarType.NORMAL.ordinal

/**
* 星星颜色
*/

private var starColor: Int = Color.parseColor("#F7B500")

/**
* 星星可点击
*/

private var starClickable = false

/**
* 星星可滑动选择
*/

private var starScrollable = false

/**
* 星星未选中画笔
*/

private val starBgPaint = Paint().apply {
isAntiAlias = true
}

/**
* 星星选中画笔
*/

private val starDrawablePaint = Paint().apply {
isAntiAlias = true
}

/**
* 星星纯色画笔
*/

private val starPaint = Paint().apply {
isAntiAlias = true
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
}

private var progress = 0

/**
* 选中icon的Bitmap
*/

private val starBitmap: Bitmap by lazy {
val bitmap = Bitmap.createBitmap(starSize + starPadding.toInt(), starSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val starDrawable = ContextCompat.getDrawable(context, starDrawable)
starDrawable?.setBounds(0, 0, starSize, starSize)
starDrawable?.draw(canvas)
bitmap
}

/**
* 未选中Bitmap
*/

private val starBgBitmap: Bitmap by lazy {
val bitmap = Bitmap.createBitmap(starSize, starSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val starDrawable = ContextCompat.getDrawable(context, starBgDrawable)
starDrawable?.setBounds(0, 0, starSize, starSize)
starDrawable?.draw(canvas)
bitmap
}

init {
initView(context, attrs)
starPaint.color = starColor
}

private fun initView(context: Context, attrs: AttributeSet?) {
val obtainStyledAttributes = context.obtainStyledAttributes(attrs, R.styleable.CommonRatingBar)
starCount = obtainStyledAttributes.getInt(R.styleable.CommonRatingBar_starCount, 5)
starPadding = obtainStyledAttributes.getDimension(R.styleable.CommonRatingBar_starPadding, 10f)
starDrawable = obtainStyledAttributes.getResourceId(R.styleable.CommonRatingBar_starDrawable, -1)
starBgDrawable = obtainStyledAttributes.getResourceId(R.styleable.CommonRatingBar_starBgDrawable, -1)
starType = obtainStyledAttributes.getInt(R.styleable.CommonRatingBar_starType, StarType.NORMAL.ordinal)
starColor = obtainStyledAttributes.getColor(R.styleable.CommonRatingBar_starColor, Color.parseColor("#F7B500"))
starClickable = obtainStyledAttributes.getBoolean(R.styleable.CommonRatingBar_starClickable, false)
starScrollable = obtainStyledAttributes.getBoolean(R.styleable.CommonRatingBar_starScrollable, false)
obtainStyledAttributes.recycle()
}

override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
// super.dispatchTouchEvent(event) -> 当前view的onTouchEvent
// false -> viewGroup的onTouchEvent
return if (starClickable || starScrollable) super.dispatchTouchEvent(event)
else false
}

/**
* 最小触摸范围
*/

private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
private var downX = 0f
private var downY = 0f

@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
}
MotionEvent.ACTION_MOVE -> {
if (starScrollable && abs(event.x - downX) - abs(event.y - downY) >= touchSlop && pointInView(event.x, event.y)) {
parent.requestDisallowInterceptTouchEvent(true)
val progress = (event.x / width * 100).toInt()
setProgress(progress)
listener?.onScrollProgress(progress)
} else {
parent.requestDisallowInterceptTouchEvent(false)
}
}
MotionEvent.ACTION_UP -> {
if (starClickable && abs(event.y - downY) <= touchSlop && abs(event.x - downX) <= touchSlop && pointInView(event.x, event.y)) {
parent.requestDisallowInterceptTouchEvent(true)
val progress = (event.x / width * 100).toInt()
setProgress(progress)
listener?.onClickProgress(progress)
} else {
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
return true
}

/**
* 点击的point是否在view范围内
*/

private fun pointInView(x: Float, y: Float): Boolean {
return Rect(0, 0, width, height).contains(x.toInt(), y.toInt())
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
starSize = measuredHeight
setMeasuredDimension(starCount * starSize + (starCount - 1) * starPadding.toInt(), starSize)
}

override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (starBgDrawable == -1) {
return
}
canvas?.let {
// xfermode需要使用离屏缓存
val saved = it.saveLayer(null, null)
drawStar(it)
if (starDrawable == -1) {
drawStarBgColor(it)
} else {
drawStarDrawable(it)
}
it.restoreToCount(saved)
}
}

/**
* 绘制星星默认未选中背景
*/

private fun drawStar(canvas: Canvas) {
for (i in 0 until starCount) {
val starLeft = i * (starSize + starPadding)
canvas.drawBitmap(starBgBitmap, starLeft, 0f, starBgPaint)
}
}

/**
* 绘制高亮纯颜色
*/

private fun drawStarBgColor(canvas: Canvas) {
canvas.drawRect(0f, 0f, getStarProgressWidth(), height.toFloat(), starPaint)
}

/**
* 绘制高亮图标
*/

private fun drawStarDrawable(canvas: Canvas) {
starDrawablePaint.shader = BitmapShader(starBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
canvas.drawRect(0f, 0f, getStarProgressWidth(), height.toFloat(), starDrawablePaint)
}

/**
* 获取星星绘制宽度
*/

private fun getStarProgressWidth(): Float {
val percent = progress / 100f
val starDrawCount = percent * starCount
return when (starType) {
StarType.HALF.ordinal -> {
ceilHalf(starDrawCount) * starSize + starDrawCount.toInt() * starPadding
}
StarType.WHOLE.ordinal -> {
ceilWhole(starDrawCount) * starSize + starDrawCount.toInt() * starPadding
}
else -> {
starDrawCount * starSize + starDrawCount.toInt() * starPadding
}
}
}

private fun ceilWhole(x: Float): Float {
return ceil(x)
}

private fun ceilHalf(x: Float): Float {
// 四舍五入 1.3->1+0.5->1.5 1.7->2
val round = round(x)
return when {
round < x -> round + 0.5f
round > x -> round
else -> x
}
}

/**
* 星星的绘制进度
*/

fun setProgress(progress: Int) {
var p = progress
if (p < 0) p = 0
if (p > 100) p = 100
this.progress = p
postInvalidate()
}

fun setProgress(currentValue: Float, totalValue: Float) {
setProgress((currentValue * 100 / totalValue).toInt())
}

fun setOnCommonRatingBarListener(listener: OnCommonRatingBarListener) {
this.listener = listener
}

private var listener: OnCommonRatingBarListener? = null

interface OnCommonRatingBarListener {
fun onClickProgress(progress: Int)
fun onScrollProgress(progress: Int)
}

enum class StarType {
NORMAL, HALF, WHOLE
}

}

image.png

拓展

  • 修改纯色方法配合LinearGradient,可以有渐变的选中效果
收起阅读 »

Android 控制 ContentProvider的创建

序言随着app隐私政策的收紧,现在不经过用户同意,就收集敏感信息的行为一旦被检测出来。很容易造成app下架。但是有些SDK的初始化是通过注册ContentProvider实现自动调用其onCreate()方法,来实现无感初始化的。如果SDK在ContentPr...
继续阅读 »

序言

随着app隐私政策的收紧,现在不经过用户同意,就收集敏感信息的行为一旦被检测出来。很容易造成app下架。但是有些SDK的初始化是通过注册ContentProvider实现自动调用其onCreate()方法,来实现无感初始化的。如果SDK在ContentProvider中获取了敏感信息,又没有提供控制方法。我们就很被动。于是我花了点时间研究了怎么hook contentProvider的创建。让其在用户同意后再初始化。

方案1

声明在清单文件中的ContentProvider 会在应用启动后就创建。具体是在 ActivityThread的handleBindApplication方法中。(以下截图为Android 30的ActivityThread) 在这里插入图片描述 具体就在这一句 在这里插入图片描述

installContentProviders实现如下 在这里插入图片描述 最终是通过AppComponentFactory的instantiateProvider方法创建。 在这里插入图片描述 在这里插入图片描述

而AppComponentFactory是Android 28以后系统提供给我们的一个hook的工厂类。可以通过清单文件指定,在这里面可以hook 所有组件的初始化。 在这里插入图片描述 这么指定 在这里插入图片描述

但是在Android 28以下,比如这个截图是Android 25.没有这类,ContentProvider直接通过反射获得。无法通过该类来修改。 在这里插入图片描述

最终方案

为了兼容性,考虑如下方案。在调用installContentProviders前,如果这个data里面的providers为空岂不是不会走installContentProviders方法了吗。 在这里插入图片描述 这个data 是一个AppBindData类型,通过handleBindApplication方法的参数传入。会保存到ActivityThread的 mBoundApplication 字段中。 在这里插入图片描述

于是就可以通过获取这个mBoundApplication 字段中的providers 来保存要初始化的provider。再讲providers置为空即可。到了用户同意以后,再去通过反射调用ActivityThread的installContentProviders方法即可。 在这里插入图片描述

hook时机

这个时机只有Application的attachBaseContext方法中。该方法会比installContentProviders提前执行。

最后的代码App中

public class MyApp extends Application {

static MyApp app;

/**
*用户同意
*/

public static void agree(Action action) {
HookUtil.initProvider(app);
action.doAction();
}

public interface Action {
void doAction();
}


@Override
protected void attachBaseContext(Context base) {
app = this;
try {
HookUtil.attachContext();
} catch (Exception e) {
e.printStackTrace();
}
super.attachBaseContext(base);
}
}

HookUtil

package com.zgh.testcontentprovider;

import android.content.Context;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;


/**
* Created by zhuguohui
* Date: 2021/9/13
* Time: 11:23
* Desc:
*/

public class HookUtil {

private static Object providers;

private static Method installContentProvidersMethod;
private static Object currentActivityThread;

/*
*用户同意后调用
*/

public static void initProvider(Context context){
try {
installContentProvidersMethod.invoke(currentActivityThread,context,providers);
} catch (Exception e) {
e.printStackTrace();
}
}

public static void attachContext() throws Exception {

// 先获取到当前的ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
//currentActivityThread是一个static函数所以可以直接invoke,不需要带实例参数
currentActivityThread = currentActivityThreadMethod.invoke(null);

hookInstallContentProvider(activityThreadClass);

}

private static void hookInstallContentProvider(Class activityThreadClass) throws Exception{
Field appDataField = activityThreadClass.getDeclaredField("mBoundApplication");
appDataField.setAccessible(true);
Object appData= appDataField.get(currentActivityThread);
Field providersField= appData.getClass().getDeclaredField("providers");
providersField.setAccessible(true);
providers = providersField.get(appData);
//清空provider,避免有些sdk通过provider来初始化
providersField.set(appData,null);

installContentProvidersMethod = activityThreadClass.getDeclaredMethod("installContentProviders", Context.class, List.class);
installContentProvidersMethod.setAccessible(true);
}
}


收起阅读 »

Jetpack Compose Banner即拿即用

Jetpack Compose目前没有官方的Banner控件,所以只能自己写,搜了些资料才完成,非常感谢之前分享过这些内容的大佬们。 效果图 accompanist组库 accompanist 旨在为Jetpack Compose提供补充功能的组库,里面有非...
继续阅读 »

Jetpack Compose目前没有官方的Banner控件,所以只能自己写,搜了些资料才完成,非常感谢之前分享过这些内容的大佬们。


效果图


gif图.gif


accompanist组库


accompanist


旨在为Jetpack Compose提供补充功能的组库,里面有非常多很好用的实验性功能,之前用过的加载网络图片的rememberImagePainter就是其中之一,而做Banner的话需要用到的是其中的Pager库。


//导入依赖 
implementation "com.google.accompanist:accompanist-pager:$accompanist_pager"

这里我用的是0.16.1,因为其他库也是这个版本,目前最新是0.18.0


关键代码


1、rememberPagerState

用于记录分页状态的变量,一共有5个参数,我们用到了4个,还有一个是initialPageOffset,可以设置偏移量


val pagerState = rememberPagerState(
//总页数
pageCount = list.size,
//预加载的个数
initialOffscreenLimit = 1,
//是否无限循环
infiniteLoop = true,
//初始页面
initialPage = 0
)

2、HorizontalPager

用于创建一个可以横向滑动的分页布局,把上面的rememberPagerState传进去,其他也没啥


HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxSize(),
) { page ->
Image(
painter = rememberImagePainter(list[page].imageUrl),
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop,
contentDescription = null
)
}

3、让HorizontalPager自己动起来

这里有两个方法可以让HorizontalPager动起来,一个是animateScrollToPage,另一个是scrollToPage,从名字上都可以看出来带animate的是有动画效果的方法,也正是我想要的东西。


//自动滚动
LaunchedEffect(pagerState.currentPage) {
if (pagerState.pageCount > 0) {
delay(timeMillis)
pagerState.animateScrollToPage((pagerState.currentPage + 1) % pagerState.pageCount)
}
}

在控件里添加这行代码就可以让控件自动起来了


但这是一段看起来没问题的代码


假设页面总数pagerState.pageCount为2,当((pagerState.currentPage + 1) % pagerState.pageCount) == 0时跳转到第1个页面,但最后的效果是这样的


gif图2.gif
轮播图往左滑了,而且还出现了轮播图中间页面的画面,页面有点闪烁的感觉。


修改后

//自动滚动
LaunchedEffect(pagerState.currentPage) {
if (pagerState.pageCount > 0) {
delay(timeMillis)
//这里直接+1就可以循环,前提是pagerState的infiniteLoop == true
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
}

只修改了animateScrollToPage参数的值,看到这里可能有人会问:pagerState.currentPage + 1不会报错吗?


确实不会!


因为当rememberPagerState中的infiniteLoop(无限循环)参数设置为true时最大页码其实为Int.MAX_VALUE,而currentPage只是当前页面的索引,并不是真实的页码。


也就是说,当Banner有4个页面,这里传个5的时候,并不会报错,而且animateScrollToPage会自动将这个"5"转换为页面索引,以保证下次使用currentPage不会出错。(菜鸟,我!啊吧啊吧看了好一阵子源码没看到这个是哪里转的)


不过有些地方值得注意:



调用pagerState.animateScrollToPage(target)的时候



  • 当target > pageCount 或 target > currentPage的时候,控件向右滑动

  • 当target < pageCount 且 target < currentPage的时候,控件向左滑动

  • 另外如果currentPage和target当两者相差页面大于4的时候只会在动画中显示(currentPage、currentPage + 1、target - 1、target)四个页面



以此类推,如果改为-1的话就是不断往左自动滑动啦


pagerState.animateScrollToPage(pagerState.currentPage - 1)

Banner中定义了几个参数,indicatorAlignment可以设置指示点的位置,默认为底部居中


/**
* 轮播图
* [timeMillis] 停留时间
* [loadImage] 加载中显示的布局
* [indicatorAlignment] 指示点的的位置,默认是轮播图下方的中间,带一点padding
* [onClick] 轮播图点击事件
*/
@ExperimentalCoilApi
@ExperimentalPagerApi
@Composable
fun Banner(
list: List<BannerData>?,
timeMillis: Long = 3000,
@DrawableRes loadImage: Int = R.mipmap.ic_web,
indicatorAlignment: Alignment = Alignment.BottomCenter,
onClick: (link: String) -> Unit = {}
)

Alignment.BottomStart

bannerLeft.png


Alignment.BottomEnd

bannerRight.png


发现了个奇怪的问题


//自动滚动
LaunchedEffect(pagerState.currentPage) {
if (pagerState.pageCount > 0) {
delay(timeMillis)
//这里直接+1就可以循环,前提是infiniteLoop == true
pagerState.animateScrollToPage(pagerState.currentPage - 1)
}
}

这段代码里,由于ReCompose时机是因为pagerState.currentPage这个值产生变化的时候;当我们触摸着HorizontalPager这个控件期间,动画会挂起取消


所以当我们滑动但是不滑动到上一页或下一页,且在本次跳转页面动画触发后才松开手指的时候,就会导致自动滚动停止的问题发生。


像这样


gif图3.gif


问题解决


问题的解决思路也不复杂,只需要在手指按下时记录当前页面索引,手指抬起时判断当前页面索引是否有所改变,如果没有改变的话就手动触发动画。


PointerInput Modifier


这是用于处理手势操作的Modifier,它为我们提供了PointerInputScope作用域,在这个作用域中我们可以使用一些有关于手势的API。


例如:detectDragGestures


我们可以在detectDragGestures中拿到拖动开始/拖动时/拖动取消/拖动结束的回调,但其中的onDrag(拖动时触发回调)是必传的参数,这会导致HorizontalPager控件拖动手势失效。


suspend fun PointerInputScope.detectDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
)

所以最后使用了更基础的API - awaitPointerEvent,我们需要在awaitPointerEventScope方法为我们提供的AwaitPointerEventScope作用域内使用它。


HorizontalPager(
state = pagerState,
modifier = Modifier.pointerInput(pagerState.currentPage) {
awaitPointerEventScope {
while (true) {
//PointerEventPass.Initial - 本控件优先处理手势,处理后再交给子组件
val event = awaitPointerEvent(PointerEventPass.Initial)
//获取到第一根按下的手指
val dragEvent = event.changes.firstOrNull()
when {
//当前移动手势是否已被消费
dragEvent!!.positionChangeConsumed() -> {
return@awaitPointerEventScope
}
//是否已经按下(忽略按下手势已消费标记)
dragEvent.changedToDownIgnoreConsumed() -> {
//记录下当前的页面索引值
currentPageIndex = pagerState.currentPage
}
//是否已经抬起(忽略按下手势已消费标记)
dragEvent.changedToUpIgnoreConsumed() -> {
//当pageCount大于1,且手指抬起时如果页面没有改变,就手动触发动画
if (currentPageIndex == pagerState.currentPage && pagerState.pageCount > 1) {
executeChangePage = !executeChangePage
}
}
}
}
}
}
...
)

另外,由于轮播图可以点击跳转到详情页面,所以还需要区分单击事件和滑动事件,需要用到pagerState.targetPage(当前页面是否有任何滚动/动画正在执行),如果没有的话就会返回null。


但只要用户拖动了Banner,松手的时候targetPage就不会为null。


//是否已经抬起(忽略按下手势已消费标记)
dragEvent.changedToUpIgnoreConsumed() -> {
//当页面没有任何滚动/动画的时候pagerState.targetPage为null,这个时候是单击事件
if (pagerState.targetPage == null) return@awaitPointerEventScope
//当pageCount大于1,且手指抬起时如果页面没有改变,就手动触发动画
if (currentPageIndex == pagerState.currentPage && pagerState.pageCount > 1) {
executeChangePage = !executeChangePage
}
}

gif图4.gif
解决!(gif图切换的时候卡了一下,真机上没问题)


即拿即用


给小林一个star


import androidx.annotation.DrawableRes
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import coil.annotation.ExperimentalCoilApi
import coil.compose.rememberImagePainter
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.delay

/**
* 轮播图
* [timeMillis] 停留时间
* [loadImage] 加载中显示的布局
* [indicatorAlignment] 指示点的的位置,默认是轮播图下方的中间,带一点padding
* [onClick] 轮播图点击事件
*/
@ExperimentalCoilApi
@ExperimentalPagerApi
@Composable
fun Banner(
list: List<BannerData>?,
timeMillis: Long = 3000,
@DrawableRes loadImage: Int = R.mipmap.ic_web,
indicatorAlignment: Alignment = Alignment.BottomCenter,
onClick: (link: String) -> Unit = {}
) {

Box(
modifier = Modifier.background(MaterialTheme.colors.background).fillMaxWidth()
.height(220.dp)
) {

if (list == null) {
//加载中的图片
Image(
painterResource(loadImage),
modifier = Modifier.fillMaxSize(),
contentDescription = null,
contentScale = ContentScale.Crop
)
} else {
val pagerState = rememberPagerState(
//总页数
pageCount = list.size,
//预加载的个数
initialOffscreenLimit = 1,
//是否无限循环
infiniteLoop = true,
//初始页面
initialPage = 0
)

//监听动画执行
var executeChangePage by remember { mutableStateOf(false) }
var currentPageIndex = 0

//自动滚动
LaunchedEffect(pagerState.currentPage, executeChangePage) {
if (pagerState.pageCount > 0) {
delay(timeMillis)
//这里直接+1就可以循环,前提是infiniteLoop == true
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
}

HorizontalPager(
state = pagerState,
modifier = Modifier.pointerInput(pagerState.currentPage) {
awaitPointerEventScope {
while (true) {
//PointerEventPass.Initial - 本控件优先处理手势,处理后再交给子组件
val event = awaitPointerEvent(PointerEventPass.Initial)
//获取到第一根按下的手指
val dragEvent = event.changes.firstOrNull()
when {
//当前移动手势是否已被消费
dragEvent!!.positionChangeConsumed() -> {
return@awaitPointerEventScope
}
//是否已经按下(忽略按下手势已消费标记)
dragEvent.changedToDownIgnoreConsumed() -> {
//记录下当前的页面索引值
currentPageIndex = pagerState.currentPage
}
//是否已经抬起(忽略按下手势已消费标记)
dragEvent.changedToUpIgnoreConsumed() -> {
//当页面没有任何滚动/动画的时候pagerState.targetPage为null,这个时候是单击事件
if (pagerState.targetPage == null) return@awaitPointerEventScope
//当pageCount大于1,且手指抬起时如果页面没有改变,就手动触发动画
if (currentPageIndex == pagerState.currentPage && pagerState.pageCount > 1) {
executeChangePage = !executeChangePage
}
}
}
}
}
}
.clickable(onClick = { onClick(list[pagerState.currentPage].linkUrl) })
.fillMaxSize(),
) { page ->
Image(
painter = rememberImagePainter(list[page].imageUrl),
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop,
contentDescription = null
)
}

Box(
modifier = Modifier.align(indicatorAlignment)
.padding(bottom = 6.dp, start = 6.dp, end = 6.dp)
) {

//指示点
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
for (i in list.indices) {
//大小
var size by remember { mutableStateOf(5.dp) }
size = if (pagerState.currentPage == i) 7.dp else 5.dp

//颜色
val color =
if (pagerState.currentPage == i) MaterialTheme.colors.primary else Color.Gray

Box(
modifier = Modifier.clip(CircleShape).background(color)
//当size改变的时候以动画的形式改变
.animateContentSize().size(size)
)
//指示点间的间隔
if (i != list.lastIndex) Spacer(
modifier = Modifier.height(0.dp).width(4.dp)
)
}
}

}
}

}

}

/**
* 轮播图数据
*/
data class BannerData(
val imageUrl: String,
val linkUrl: String
)

特别感谢


RugerMc 手势处理


apk下载链接


项目地址


欢迎Star~PlayAndroid


作者:木木沐目
链接:https://juejin.cn/post/7006230365467574302
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

三步实现一个自定义任意路径的嫦娥奔月(Flutter版)

前言 可能不少人看到这个标题,心里想的是: 要是被我发现你TM就是个标题党,三步完不成,信不信我堵在你家门口,见一次打一次,你给我去死吧 不就是个平移动画嘛,我上我也行,让我进去骂死这个水文货 要是真这么想的话,我只能说: 下面给大家整个活...
继续阅读 »

前言


可能不少人看到这个标题,心里想的是:


要是被我发现你TM就是个标题党,三步完不成,信不信我堵在你家门口,见一次打一次,你给我去死吧


不就是个平移动画嘛,我上我也行,让我进去骂死这个水文货


要是真这么想的话,我只能说:



我看你是完全不懂哦
真拿你没办法

下面给大家整个活,为大家介绍一下我们“listView是万能的”教会的唯一真主和慈父——ListView,是如何通过自定义,来实现这个需求的;


先放上效果图:


最终效果

前期准备,需要自定义并提供给ListView的部分;


1. 首先,我们需要一个又大又圆的月亮:


这里呢,就先用一个背景图替代,所以把一个背景图放到stack底层中:


Stack(
children: [
Positioned.fill(
child: Image.asset("img/bg_mid_autumn.jpg",fit: BoxFit.cover,),
),
Positioned.fill(
/// 自定义的ListView
/// 先以RecyclerView的形式命个名,毕竟思路参考自Android 的RecyclerView
child: RecyclerView.builder(...),
),
],

2. 以及主人公————嫦娥:


把它以item的形式加入到自定义ListView中


RecyclerView.builder(
...
itemBuilder: (context, index) {
return Container(
width: 100,
alignment: AlignmentDirectional.topCenter,
child: Image.asset("img/img_chang_e.png",fit: BoxFit.cover,width: 100,height: 100,),
);
}
)

3. 搞一个提供规划登月路径的Widget:


class ImageEditor extends CustomPainter {
ImageEditor();

Path? drawPath;

final Paint painter = new Paint()
..color = Colors.blue
..style = PaintingStyle.stroke
..strokeWidth = 10;

void update(Offset offset) {
if (drawPath == null) {
drawPath = Path()..moveTo(offset.dx, offset.dy);
}

drawPath?.lineTo(offset.dx, offset.dy);
}

@override
void paint(Canvas canvas, Size size) {
if (drawPath != null) {
canvas.drawPath(drawPath!, painter);
}
}

@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}

搞定~正好三步;


大家先别急着骂,先不提这个自定义的ListView以及一堆莫名其妙的东西从哪来的,就说是不是三步吧


虽然要实现这三步,需要做如下工作来实现:


关于奔月动画的实现原理、方式


这块参考自android的RecyclerView的自定义LayoutManager的部分,具体详细步骤可以看这个大佬的文章:


# Android自定义LayoutManager第十一式之飞龙在天


这块仅供提供思路,虽说Flutter中没有RecyclerView这种神器,也没有layoutManager这种东西,甚至onMeasure、onLayout这块的触发时机等方面跟android都不同;


但是下沉到onMeasure、onDraw、onLayout这个层面,其实都是一样的,并非不可参考


分析与实现,需要改造ListView哪些地方:


1. 首先,我们先从 ListView 本身开始:


ListView的结构其实并不复杂,或者嚣张点,大部分可滑动的View,也无非就在那几个类上面修修改改,换句话说:


学姐

当然我知道各位一点都不喜欢看代码(其实是因为这部分太多了……放一篇介绍文章中放不下),那我简化一下,只提一下这次涉及的部分和浅层解析,毕竟这块东西我也是简单了解一下(纯属个人理解,有错误请狠狠的打我脸):



  1. ListView、nestedScrollView、CustomScrollView等滑动View,都是直接或者间接继承自ScrollView,ScrollView这个抽线类,就是黑龙江职业学院,那几个可滑动View都是受ScrollView管控;

  2. ScrollView 中管事的就是Scrollable ,把它当成学生会就行;

  3. 在这次中,Scrollable 中有这么几个类要知道:ViewPortScrollControllScrollPostion


ViewPort负责管理提供可视范围视图(学生会生活部?负责提供我们去哪里查寝)、ScrollPostion负责记录滚动位置、最大最小距离之类的信息(学生会书记?记录一下查寝结果)、ScrollControll负责统筹滚动视图的展示、动画等部分(这个我懂,这个是主席,张美玉学姐好);


2. 打破ListView不可滚动溢出的限制,并控制初始位置:


要是嫌麻烦,直接往listView的item列表的头尾处,加个listView大小的空白页,也是可以实现同样效果的


用于装逼,了解listView逻辑思路的写法:



  1. 按照上面的分析,如果要让listView可以滚动溢出,那么需要做的事,就是去找ViewPort的麻烦;


下面我们来回忆一下,一个控件,想要显示,不可避免要经过的三个步骤是:


1、measure;2、layout;3、draw


要想获取滚动限制、明显是measure或者layout部分的东西,结合ScrollPostion的_minScrollExtent和_maxScrollExtent的来源,可以定位可以修改的位置是在 RenderViewPortperformLayout 方法中,调用 scrollPosition 的 applyContentDimensions 方法的地方;


比如说这样修改,将ListView本身大小作为滚动溢出范围:


do {
assert(offset.pixels != null);
correction = _attemptLayout(mainAxisExtent, crossAxisExtent,
offset.pixels + centerOffsetAdjustment);
if (correction != 0.0) {
offset.correctBy(correction);
} else {
if (offset.applyContentDimensions(

/// 在这里调整可溢出范围,比如说下面就把size.width 作为可溢出范围,最小范围减少Size.width,最大范围增加Size.width;
math.min(-size.width, _minScrollExtent + mainAxisExtent * anchor),
math.max(0.0,
_maxScrollExtent - mainAxisExtent * (1.0 - anchor) + size.width),
)) break;
}
count += 1;
} while (count < _maxLayoutCycles);


  1. 然后让ListView的初始展示位置,设置到-Size.width的位置;


在这里我的做法是通过 LayoutBuilder 获取约束范围,然后将约束最大值直接赋值给 ScrollController,例如下面代码:


 LayoutBuilder(builder: (_context, _constraint) {

return RecyclerView.builder(
scrollDirection: Axis.horizontal,

/// 这里将约束的最大值的负数提供到ScrollController的initialScrollOffset中
controller: ScrollController(
initialScrollOffset: -_constraint.maxWidth),
itemCount: 3,
reverse: true,
addRepaintBoundaries: false,
....
)
}

PS : 这块的源码,虽说我们只需要改这么一个小点,但是像override这种方式都会因为一堆私有变量什么的无法获取,所以直接从 RenderViewportBase 到 RenderViewPort 都完整复制出来吧



  1. 最后将自定义好的ViewPort的Render部分,传给ViewPort的Widget部分,最后放到自定义ListView的buildViewPort部分:(在这里,我将这个提供溢出滚动的ViewPort命名为OverScrollViewPort)


@override
Widget buildViewport(BuildContext context, ViewportOffset offset, AxisDirection axisDirection, List<Widget> slivers) {
if (shrinkWrap) {
return OverScrollShrinkWrappingViewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
clipBehavior: clipBehavior,
);
}

return OverScrollViewPort(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
center: center,
anchor: anchor,
clipBehavior: clipBehavior,
);
}

如果我没有遗漏部分的话,这时候运行一下代码,应该是这种效果:


最终效果

3. 修改绘制,按path要求绘制:


如果你做好了准备工作,提供了一个自定义路径出来,那么将这个path传到负责绘制的 RenderObject 中,在paint方法中获取滑动比例对应path的位置,就调整绘制位置:


@override
void paint(PaintingContext context, Offset offset) {
...

/// 在这里处理path。
Path? customPath;
PathMetric? pm;
double? length;
if (layoutManager is PathLayoutManager) {
customPath = layoutManager.path;

var pathMetrics = customPath.computeMetrics(forceClosed: false);
pm = pathMetrics.elementAt(0);
length = pm.length;
}

while (child != null) {
double mainAxisDelta = childMainAxisPosition(child);
final double crossAxisDelta = childCrossAxisPosition(child);
...

/// 关于这块去掉原先 mainAxisDelta < constraints.remainingPaintExtent 部分的原因
/// 是因为之前第一个item会在滚动到边界前就被移除绘制
/// 具体是什么地方修改导致的,忘了(๑>؂<๑)
if (mainAxisDelta + paintExtentOf(child) > 0) {
if (customPath != null) {
var percent = (childOffset.dx + child.size.width) /
(child.size.width + constraints.viewportMainAxisExtent);
var tf = pm!.getTangentForOffset(length! * percent);
print("test :${tf?.position}");

var childItemOffset = childOffset;

if (tf?.position != null) {
/// 这里的50 魔法数,是因为之前设置item的height为100,
/// 因为listView好像强制将item的高度固定为listView的高度(横向情况)
/// 这块找个时间研究下怎么搞
/// 强调下,好孩子不要学我这写法
childItemOffset = Offset(
tf!.position.dx - child.size.width / 2, tf.position.dy - 50);
}

context.pushTransform(
needsCompositing,
childItemOffset,
Matrix4.identity(),
// Pre-transform painting function.
painter,
);
} else {
context.paintChild(child, childOffset);
}
}

...
}

...
}

PS:我这里弄了个LayoutManager,其实就是新建个类,把它从widget传到 renderObject &@&%……#;path的处理这块也是有问题的,不应该放在这里搞,好孩子不要学我这么搞,我这是实验性代码…………


当然,要想做到完美复刻RecyclerView,还有不少地方要改动


比如说,你给item加个点击事件,你会发现……现在这种方式,仅仅是改变了绘制的位置,item本身并未移动:


注意看弹toast前的点击位置,明明是左上角


现存问题

我猜想:这里就要涉及到listView 的 insertAndLayout 部分了,进而涉及到整体的滑动逻辑…………或者是hitTest的部分?(或许这是part 2新篇预告?)


在现在这个基础上,还有可以拓展的方面:


除了嫦娥奔月效果,其实还可以实现一些其他效果,例如:


覆盖翻页效果
覆盖翻页效果


item变换
item变换


另外在ParentData等部分中,也有一些有点意思的东西,个人感觉都挺有用的


题外话,上面正文的做法,为什么我个人并不推荐


在我看来,现在文中的这种自定义方式是不符合flutter的推荐方式的:


在我的理解中,在做flutter的自定义的时候,有个比较重要的一句话是需要遵守的:


万物均为widget


所以,如果可以的话,尽量使用widget来代替回调、方法这种,如果无法避免,也尽量约束到一个widget、及其对应element、renderObject;


所以,现在文中的方式,在我看来,虽然能实现需求,但是是通过各种回调、耦合了各个widget的及其对应的element、renderObject,因此不是flutter的良好代码,


这段代码,应急可以,偷懒也行,用于学习思路,分析步骤也是没问题的,但是,不推荐真这么搞哈


这篇文章的主要目的,是参考Android的实现方式,来分享思路与分析flutter中的listView,以及最重要的:



作者:lwlizhe
链接:https://juejin.cn/post/7007254000307437598
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Compose 实现月亮阴晴圆缺动画

效果图 人有悲欢离合,月有阴晴圆缺,此事古难全。 但愿人长久,千里共婵娟。 恰逢中秋佳节,我们今天就使用Compose来实现一下月相变化动画吧~ 感兴趣的同学可以点个Star : Compose 实现月亮阴晴圆缺动画 主要思路 满天繁星 为了实现月相动画...
继续阅读 »

效果图




人有悲欢离合,月有阴晴圆缺,此事古难全。

但愿人长久,千里共婵娟。

恰逢中秋佳节,我们今天就使用Compose来实现一下月相变化动画吧~

感兴趣的同学可以点个Star : Compose 实现月亮阴晴圆缺动画



主要思路


满天繁星


为了实现月相动画,我们首先需要一个背景,因此我们需要一个好看的星空,最好还有闪烁的效果

为为实现星空背景,我们需要做以下几件事



  1. 绘制背景

  2. 生成几十个星星,在背景上随机分布

  3. 通过scalealpha动画,实现每个星星的闪烁效果


我们一起来看下代码


@Composable
fun Stars(starNum: Int) {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val list = remember { mutableStateListOf<Star>() }
LaunchedEffect(true) {
for (i in 0..starNum) {
delay(100L)
//添加星星,它们的位置在屏幕上随机
list.add(Star(maxWidth.value * density, maxHeight.value * density))
}
}
list.forEach {
Star(it)
}
}
}

@Composable
fun Star(star: Star) {
var progress: Float by remember { mutableStateOf(0f) }
val infiniteTransition = rememberInfiniteTransition()
....
star.updateStar(progress) // 通过动画更新progress,从而更新star的属性值
Canvas(modifier = Modifier.wrapContentSize()) {
scale(star.scale, Offset(star.x, star.y)) { // 缩放动画
drawCircle(
star.starColor,
star.radius,
center = Offset(star.x, star.y),
alpha = star.alpha // alpha动画
)
}
}
}

月相变化


月相,天文学术语。(phase of the moon)是天文学中对于地球上看到的月球被太阳照明部分的称呼。随着月亮每天在星空中自东向西移动一大段距离,它的形状也在不断地变化着,这就是月亮位相变化,叫做月相。

它的变化过程如下图所示



每个阶段都有各自的名字,如下图所示:


可以看出,月相变化过程还是有些复杂的,那我们怎么实现这个效果呢?


思路分析


为了实现月相变化,首先我们需要画一个圆,代表月亮,最终的满月其实就是这样,比较简单

有了满月,如何在它的基础上,画出其它的月相呢?我们可以通过图像混合模式来实现


图像混合模式定义的是,当两个图像合成时,图像最终的展示方式。在Androd中,有相应的API接口来支持图像混合模式,即Xfermode.

图像混合模式主要有以下16种,以下这张图片从一定程度上形象地说明了图像混合的作用,两个图形一圆一方通过一定的计算产生不同的组合效果,具体如下


我们为了实现月相动画,主要需要使用以下两种混合模式



  • DST_OUT:只在源图像和目标图像不相交的地方绘制【目标图像】,在相交的地方根据源图像的alpha进行过滤,源图像完全不透明则完全过滤,完全透明则不过滤

  • DST_OVER:将目标图像放在源图像上方


我们已经了解了图形混合模式,那么需要在满月上画什么才能实现其它效果呢?

我们可以通过在满月上放一个半圆+一个椭圆来实现



  1. 如上所示,椭圆上水平的线叫长轴,竖直的线叫短轴

  2. 短轴不变,长轴半径从0到满月半径发生变化,再加上一个半圆,就可以实现不同的月相

  3. 比如为了画上蛾眉月,可以通过左半边画半圆,再加上一个椭圆,两都都使用DST_OVER混合模式来实现,就实现了它们两的并集,然后覆盖在下层满月上,就实现了上蛾眉月

  4. 为了画渐盈凸月,则同样就左半边以DST_OVER画半圆,再以DST_OUT画椭圆,就只剩下半圆与椭圆不相交的部分,再与下层的满月混合,就实现了渐盈凸月


这样说可能还是比较抽象,感兴趣的同学可下载源码详细了解下


源码实现


//月亮动画控件
@Composable
fun Moon(modifier: Modifier) {
var progress: Float by remember { mutableStateOf(0f) }
BoxWithConstraints(modifier = modifier) {
Canvas(
modifier = Modifier
.size(canvasSize)
.align(Alignment.TopCenter)
) {
drawMoonCircle(this, progress)
drawIntoCanvas {
it.withSaveLayer(Rect(0f, 0f, size.width, size.height), paint = Paint()) {
if (progress != 1f) {
//必须先画半圆,再画椭圆
drawMoonArc(this, it, paint, progress)
drawMoonOval(this, it, paint, progress)
}
}
}
}
}
}

// 1.首先画一个满月
private fun drawMoonCircle(scope: DrawScope, progress: Float) {
//....
drawCircle(Color(0xfff9dc60))
}

// 2. 画半圆
private fun drawMoonArc(scope: DrawScope, canvas: Canvas, paint: Paint, progress: Float) {
val sweepAngle = when { //从新月到满月在一边画半圆,从满月回到新月则在另一边画半圆
progress <= 0.5f -> 180f
progress <= 1f -> 180f
progress <= 1.5f -> -180f
else -> -180f
}
paint.blendMode = BlendMode.DstOver //半圆的混合模式始终是DstOver
scope.run {
canvas.drawArc(Rect(0f, 0f, size.width, size.height), 90f, sweepAngle, false, paint)
}
}

// 3. 画椭圆
private fun drawMoonOval(scope: DrawScope, canvas: Canvas, paint: Paint, progress: Float) {
val blendMode = when { //椭圆的混合模式会发生变化,这里需要注意下
progress <= 0.5f -> BlendMode.DstOver
progress <= 1f -> BlendMode.DstOut
progress <= 1.5f -> BlendMode.DstOut
else -> BlendMode.DstOver
}
paint.blendMode = blendMode
scope.run {
canvas.drawOval(
Rect(offset = topLeft, size = Size(horizontalAxis, verticalAxis)), //椭圆的长轴会随着动画变化
paint = paint
)
}
}

如上所示:



  1. 主要就是3个步骤,画满月,再画半圆,再画椭圆

  2. 半圆的混合模式始终是DstOver,而椭圆的混合模式会发生变化,它们的颜色都是黑色。

  3. 可以看到半圆与椭圆新建了一个Layer,混合模式的变化,表示的就是最后剩下的是它们的并集,还是Dst不相交的部分,最后覆盖到满月上,所以必须先画半圆

  4. 随着动画的变化,椭圆的长轴会发生变化,这样就可以实现不同的月相


诗歌打字机效果


上面其实已经做得差不多了,我们最后再添加一些诗歌,并为它们添加打字机效果


@Composable
fun PoetryColumn(
list: List<Char>,
offsetX: Float = 0f,
offsetY: Float = 0f
) {
val targetList = remember { mutableStateListOf<Char>() }
LaunchedEffect(list) {
targetList.clear()
list.forEach {
delay(500) //通过在LaunchedEffect中delay实现动画效果
targetList.add(it)
}
}
//将 Jetpack Compose 环境的 Paint 对象转换为原生的 Paint 对象
val textPaint = Paint().asFrameworkPaint().apply {
//...
}
Canvas(modifier = Modifier.wrapContentSize()) {
drawIntoCanvas {
for (i in targetList.indices) {
it.nativeCanvas.drawText(list[i].toString(), x, y, textPaint)
y += delta // 更新文字y轴位置
}
}
}
}

如上所示,代码比较简单



  1. 通过在LaunchedEffect中调用挂起函数,来实现动画效果

  2. 为了实现竖直方向的文字,我们需要使用Paint来绘制Text,而不能使用Text组件

  3. Compose目前还不支持直接绘制Text,所以我们需要调用asFrameworkPaint将其转化为原生的Paint


总结


通过以上步骤,我们就通过Compose实现了月相阴晴圆缺+星空闪耀+诗歌打字机的动画效果

开发起来跟Android自定义绘制其实并没有多大差别,代码量因为Compose强大的API与声明式特点可能还有所减少

在我看来,Compose已经相当成熟了,而且将是Android UI的未来~


开源不易,如果项目对你有所帮助,欢迎点赞,Star,收藏~


作者:RicardoMJiang
链接:https://juejin.cn/post/7007041238293544991
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

LeetCode第一讲:哈希表相关讲解

哈希表简单说明哈希表的建立需要有哈希地址,那么哈希地址地址的生成需要一个哈希函数,什么是哈希函数呢?哈希函数就是一个精心设计好的函数,该函数可以计算出存储的数据要放在什么位置,举个例子说明:例:有4条电话数据:王二蛋 12345678985李狗蛋 115544...
继续阅读 »

哈希表简单说明

哈希表的建立需要有哈希地址,那么哈希地址地址的生成需要一个哈希函数,什么是哈希函数呢?
哈希函数就是一个精心设计好的函数,该函数可以计算出存储的数据要放在什么位置,举个例子说明:

例:有4条电话数据:
王二蛋 12345678985
李狗蛋 11554456555
赵二狗 18816848615
李桂花 15899484538

如果我想查找王二蛋的电话,我需要拿出这个列表,一个一个找。但我想要通过名字快速查找王二蛋如何做呢?

答:我构建一个哈希表,来快速查找。那么通过名字来存的话,我需要构建一套规则来定位数据的存储位置。那么我构建如下的函数 Addr = H(”姓名“的首字母 ASCII - 65 ),需要一个32大小数组来存储数据。

但是如果按照刚刚的设计,那么”李桂花“与”李狗蛋“的存储就会发生冲突,那么在设计哈希函数有如下几种方式(对于哈希表而言,冲突只能尽可能地少,无法完全避免。)

哈希表构建

1、直接定址法
例如:有一个从1到100岁的人口数字统计表,其中,年龄作为关键字,哈希函数取关键字自身。
2、数字分析法
有学生的生日数据如下:
年.月.日
75.10.03
75.11.23
76.03.02
76.07.12
75.04.21
76.02.15

经分析,第一位,第二位,第三位重复的可能性大,取这三位造成冲突的机会增加,所以尽量不取前三位,取后三位比较好。
3、平方取中法
取关键字平方后的中间几位为哈希地址。
4、折叠法
将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址,这方法称为折叠法。
例如:每一种西文图书都有一个国际标准图书编号,它是一个10位的十进制数字,若要以它作关键字建立一个哈希表,当馆藏书种类不到10,000时,可采用此法构造一个四位数的哈希函数。
5、除留余数法
取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址。
H(key)=key MOD p (p<=m)
6、随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即
H(key)=random(key),其中random为随机函数。通常用于关键字长度不等时采用此法。
若已知哈希函数及冲突处理方法,哈希表的建立步骤如下:
Step1. 取出一个数据元素的关键字key,计算其在哈希表中的存储地址D=H(key)。若存储地址为D的存储空间还没有被占用,则将该数据元素存入;否则发生冲突,执行Step2。
Step2. 根据规定的冲突处理方法,计算关键字为key的数据元素之下一个存储地址。若该存储地址的存储空间没有被占用,则存入;否则继续执行Step2,直到找出一个存储空间没有被占用的存储地址为止。

冲突处理

1、拉链法
拉出一个动态链表代替静态顺序存储结构,可以避免哈希函数的冲突,不过缺点就是链表的设计过于麻烦,增加了编程复杂度。此法可以完全避免哈希函数的冲突。
2、多哈希法
设计二种甚至多种哈希函数,可以避免冲突,但是冲突几率还是有的,函数设计的越好或越多都可以将几率降到最低(除非人品太差,否则几乎不可能冲突)。
3、开放地址法
开放地址法有一个公式:Hi=(H(key)+di) MOD m i=1,2,…,k(k<=m-1)
其中,m为哈希表的表长。di 是产生冲突的时候的增量序列。如果di值可能为1,2,3,…m-1,称线性探测再散列。
如果di取1,则每次冲突之后,向后移动1个位置.如果di取值可能为1,-1,4,-4,9,-9,16,-16,…kk,-kk(k<=m/2)
称二次探测再散列。如果di取值可能为伪随机数列。称伪随机探测再散列。
4、建域法
假设哈希函数的值域为[0,m-1],则设向量HashTable[0…m-1]为基本表,另外设立存储空间向量OverTable[0…v]用以存储发生冲突的记录。

题目1:

给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]

说明:
输出结果中的每个元素一定是唯一的。
我们可以不考虑输出结果的顺序。

题目地址:https://leetcode-cn.com/problems/intersection-of-two-arrays
来源:力扣(LeetCode)

题目解答:

public static int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set = new HashSet<>();
for(int i :nums1){
set.add(i);
}
Set<Integer> set2 = new HashSet<>();
for(int j:nums2){
if(set.contains(j)){
set2.add(j);
}
}
int[] st = new int[set2.size()];
Object[] kk = set2.toArray();
for(int i=0;i<kk.length;i++){
st[i] = (int)kk[i];
}
return st;
}

解答说明:
该题由于要算交集,数组还有重复的可能行,那么就采用java中有的HashSet来处理问题,HashSet底层源码是实现了一个hashMap,HashMap实际上就是哈希表的实现,那么可以直接用hash表的特点来去重,再由hash表的特点来查询交集。算法实际时间复杂度为O(m+n) 空间复杂度:O(m+n)

题目2:

写代码,移除未排序链表中的重复节点。保留最开始出现的节点。
示例1:
输入:[1, 2, 3, 3, 2, 1]
输出:[1, 2, 3]

示例2:
输入:[1, 1, 1, 1, 2]
输出:[1, 2]
提示:

链表长度在[0, 20000]范围内。
链表元素在[0, 20000]范围内。
题目地址:https://leetcode-cn.com/problems/remove-duplicate-node-lcci
来源:力扣(LeetCode)

题目解答:

public ListNode removeDuplicateNodes(ListNode head) {
Set<Integer> set = new HashSet<>();
ListNode root = head;
ListNode temp = head;
ListNode pre = null;
while (temp!=null){
if(set.contains(temp.val)){
temp = temp.next;
pre.next = temp;
}else{
set.add(temp.val);
pre = temp;
temp = temp.next;
}
}
return root;
}

解答说明:
采用hash表的hash唯一性来做缓冲区,时间复杂度:O(n) 空间复杂度:O(n)

收起阅读 »

面试再也不怕 Handler 了,消息传递机制全解析

一、为什么要使用 Handler众所周知,Android 不允许在子线程中更新 UI。但是我们在子线程完成耗时的操作之后,需要对界面数据进行更新,又该怎么处理呢?这时候,我们可以使用 Handler 进行 UI 更新。值得注意的是,更新 UI 我们需要把 Me...
继续阅读 »

一、为什么要使用 Handler

众所周知,Android 不允许在子线程中更新 UI。但是我们在子线程完成耗时的操作之后,需要对界面数据进行更新,又该怎么处理呢?这时候,我们可以使用 Handler 进行 UI 更新。值得注意的是,更新 UI 我们需要把 Message 发送到主线程持有的 MessageQueue ,否则程序依然就会发生奔溃。

另外,除了更新 UI,Handler 是 Android 系统的消息传递机制,它定义了一套处理消息的规则,广播、服务以及线程间的通信都需要靠它来完成。

与 Handler 相关的还有 Looper 和 MessageQueue,接下来我们就从它的使用开始分析,对这三剑客一网打尽。

二、Handler 发送消息的流程

Handler 发送消息有两种方式,一种是 sendMessage 的方式,一种是 post 的方式,通过对源码的阅读,post 的方式其实是调用到了 sendMessage 的方式。那我们就来看看 sendMessage 的流程吧。通过调用 sendMessage,最终会走到下面方法中:

image.png

这里做的事情很简单,必须满足 MessageQueue 不能为空,否则程序会抛出异常,接下来看 enqueueMessage 的流程:

image.png

在这里完成了两个重要的流程:

  • 为 msg 的 target 赋值,msg.target = this,因此这个 target 就是调用的 sendMessage 的 Handler。(记住这里的重点)
  • 调用了 MessageQueue 的 enqueueMessage 方法。

到目前为止,流程来到了 MessageQueue 中。现在看 MessageQueue 的 enqueueMessage 方法。

三、MessageQueue 的工作流程

由于 enqueueMessage 的方法比较长,我们这里不截图,直接看下面的代码:(省略部分代码)

boolean enqueueMessage(Message msg, long when) {
// 1、target 不能为空,否则直接抛出异常
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
// 2、加锁,不能有多个 Handler 同时发送消息
synchronized (this) {
msg.when = when;
Message p = mMessages; // 出队列的 msg 的下一个要出队列的 msg
boolean needWake;
// 3、下面这三种情况直接插在 head 节点上,(1)这个队列是一个空队列,
// (2)这个 msg 需要立即处理,(3)是它需要处理的时间比即将出队列的节
// 点的处理时间还要小
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
needWake = mBlocked && p.target == null && msg.isAsynchronous();

// 4、如果之前第三点的条件不满足,就会从 head 节点开始遍历,
// 插入到一个合适的时间,或者链表的尾部,这个 for 循环做的其实就是
// 链表节点的插入
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// 5、是否需要进行唤醒,在 queue.next() 方法中如果没有获取到 msg就会休眠
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}

解释其实已经在上面的代码里了,下面来做一个简单归纳:

  • MessageQueue 其本质上一个单向链表,入队列这个操作进行了加锁的处理,不能多个 msg 同时入队列。
  • 在插入队列的时候,会根据当前队列是否为空,或者处理消息的时间选择合适的插入位置。
  • 最后判断是否需要进行 wake up

到目前为止,我们看了 Handler 的发送消息的流程,以及消息是如何插入链表的,那么消息是如何处理的呢?我们知道,只有调用了 Looper 的 loop() 方法之后,才能处理消息,那接下来看 Looper 的 loop() 方法。

四、Looper 的工作流程

Looper 的 loop() 方法也是相当长,接下来看代码:(省略部分代码)

public static void loop() {
// 1、获取 Looper 对象,定进行判空处理
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}

// 2、获取了 MessageQueue 对象
final MessageQueue queue = me.mQueue;
for (;;) {
// 3、调用 MessageQueue 的 next(),返回值是 msg
Message msg = queue.next(); // might block
if (msg == null) {
return;
}
....
try {
// 4、之前说过,在 SendMessage 的时候设置了 msg 的target,这个 target 就是调用 sendMessage 的 Handler
msg.target.dispatchMessage(msg);
} catch (Exception exception) {
} finally {
}

msg.recycleUnchecked();
}
}

代码本身很长,但是其实做的事情也不多,现在简单归纳一下:

  • 在调用 Looper.loop() 之前,必须先调用 Looper.prepare(),如果没有 Looper 对象的话程序会直接抛异常。
  • 通过调用 MessageQueue 的 next 方法不断的从队列里取消息出来。
  • 最后把 msg 交给 Handler 的 dispatchMessage() 进行处理。

通过源码我们可以发现调用 queue.next() 时可能发生阻塞,那这个方法又做了什么?还有,为什么要先调用 Looper.prepare(),这个方法又做了什么处理?先来看比较简单的吧:

image.png

这个 Looper.prepare() 其实是创建了一个 Looper 对象,并且通过 ThreadLocal 实现每个线程有且仅有一个这样的 Looper 对象。为什么要创建 Looper 呢?没有就不行吗?我们来看 Handler 的构造函数:

image.png

可以看到,如果 Looper 为空的话,程序直接抛异常。这个 myLooper() 是用来获取当前线程的 Looper 对象:

image.png

从时序上说,我们调用 Looper.prepare() 的时机必须在 new Handler() 之前。那么,我们主线程使用 Handler 的时候,并没有调用 Looper.prepare() 这个方法,这又是怎么回事呢?

原来,在 ActivityThread 的 main() 方法中已经为我们进行了处理:

image.png

这个 prepareMainLooper() 在内部调用了 Looper.prepare() 。到目前为止,我们解决了 Looper 的相关问题,说明了必须存在 Looper 的原因。现在还有一个问题没有解决,queue.next() 方法做了什么事情?它为什么发生阻塞呢?

接下来看 MessageQueue 的 next() 方法:(已省略部分代码)

Message next() {

final long ptr = mPtr;
if (ptr == 0) {
return null;
}

int pendingIdleHandlerCount = -1;
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
// 1、这是一个 native 方法,如果messageQueue 没有可以处理的消息就会休眠
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// 2、同步屏障,寻找队列中的下一个异步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// 3、下一个出队列的这个 msg 还没有到时间,并计算需要阻塞的时间
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 4、得到一个能够处理的msg,并返回这个 msg
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
...
}
}
}

重要的点其实已经在上面说了,下面总结一下:

  • 获取 msg 的这个过程有可能会发生阻塞,具体调用到的是 native 的 nativePollOnce 方法
  • 获取消息的时候,有一个同步屏障,也就是对 msg 对应的 target(Handler) 为空的消息进行了过滤。
  • 如果能获取到一个 msg ,那么就返回这个 msg。

四、再看 Handler

先来梳理一下我们现在明白了什么:

  • 在创建 Handler 的时候,必须先创建 Looper 对象,之后还需要调用 Looper.loop() 方法才能让 Handler 开始工作。
  • 通过 Handler sendMessage 发送消息,其实是调用了 queue.enqueueMessage,这个 Queue 其实是一个单向链表,在调用这个方法的时候,会根据当前队列的转态以及 when 把这个 msg 插入到合适的位置。
  • queue.next() 可能会发生休眠,原因是拿到不到合适的 msg,在 queue.enqueueMessgae 的时候会判断是否需要唤醒。

之前我们说过,这个 msg 其实是交给了 Handler 的 dispatchMessage 去处理,下面来看一下 Handler 是怎么处理的:

image.png

  • msg.callback 是我们通过 post 方法传递进来的一个 Runnable 对象,如果我们没有使用 post 的话,就不会走到 handleCallback(msg) 中。
  • mCallback 是一个 CallBack 对象,如果我们在创建 Handler 的时候没有传这个参数,那么 mCallback 也是为null 的。
  • 最后才会走到 handleMessage(msg) 中。

收起阅读 »

在android中如何制作一个方向轮盘

先上效果图原理很简单,其实就是一个自定义的view通过观察,很容易发现,我们自己的轮盘就两个view需要绘制,一个是外面的圆盘,一个就随手指移动的滑块; 外面的圆盘很好绘制,内部的滑块则需要采集手指的位置,根据手指的位置计算出滑块在大圆内的位置; 最后,我们做...
继续阅读 »

先上效果图

Screenrecorder-2021-09-13-09-55-26-155.gif

原理很简单,其实就是一个自定义的view

通过观察,很容易发现,我们自己的轮盘就两个view需要绘制,一个是外面的圆盘,一个就随手指移动的滑块; 外面的圆盘很好绘制,内部的滑块则需要采集手指的位置,根据手指的位置计算出滑块在大圆内的位置; 最后,我们做的UI不是单纯做一个UI吧,肯定还是要用于实际应用中去,所以要加一个通用性很好的回调.

计算滑块位置的原理:

  • 当触摸点在大圆与小圆的半径差之内:
    那么滑块的位置就是触摸点的位置
  • 当触摸点在大圆与小圆的半径差之外:
    已知大圆圆心坐标(cx,cy),大圆半径rout,小圆半径rinside,触摸点的坐标(px,py)
    求小圆的圆心(ax,ay)?

image.png

作为经过九义的你我来说,这不就是一个简简单单的数学题嘛,很容易就求解出小圆的圆心位置了。 利用三角形相似:
\frac{ax-cx}{rout-rinside} = \frac{px-cx}{\sqrt{(px-cx)^2+(py-cy)^2}}
\frac{ay-cy}{rout-rinside} = \frac{py-cy}{\sqrt{(px-cx)^2+(py-cy)^2}}

通用性很好的接口:

滑块在圆中的位置,可以很好的用一个二位向量来表示,也可以用两个浮点的变量来表示;
xratio = \frac{ax-cx}{rout-rinside}
yratio = \frac{ay-cy}{rout-rinside}

这个接口就可以很好的表示了小圆在大圆的位置了,他们的取值范围是[-1,1]

小技巧:

为了小圆能始终在脱手后回到终点位置,我们设计了一个动画,当然,实际情况中有一种情况是,你移动到某个位置后,脱手后位置不能动,那你禁用这个动画即可。

代码部分

tips:代码部分的变量名与原理的变量名有出入

public class ControllerView extends View implements View.OnTouchListener {
private Paint borderPaint = new Paint();//大圆的画笔
private Paint fingerPaint = new Paint();//小圆的画笔
private float radius = 160;//默认大圆的半径
private float centerX = radius;//大圆中心点的位置cx
private float centerY = radius;//大圆中心点的位置cy
private float fingerX = centerX, fingerY = centerY;//小圆圆心的位置(ax,ay)
private float lastX = fingerX, lastY = fingerY;//小圆自动回归中点动画中上一点的位置
private float innerRadius = 30;//默认小圆半径
private float radiusBorder = (radius - innerRadius);//大圆减去小圆的半径
private ValueAnimator positionAnimator;//自动回中的动画
private MoveListener moveListener;//移动回调的接口

public ControllerView(Context context) {
super(context);
init(context, null, 0);
}

public ControllerView(Context context,
@Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}

public ControllerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}

//初始化
private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ControllerView);
int fingerColor = typedArray.getColor(R.styleable.ControllerView_fingerColor,
Color.parseColor("#3fffffff"));
int borderColor = typedArray.getColor(R.styleable.ControllerView_borderColor,
Color.GRAY);
radius = typedArray.getDimension(R.styleable.ControllerView_radius, 220);
innerRadius = typedArray.getDimension(R.styleable.ControllerView_fingerSize, innerRadius);
borderPaint.setColor(borderColor);
fingerPaint.setColor(fingerColor);
lastX = lastY = fingerX = fingerY = centerX = centerY = radius;
radiusBorder = radius - innerRadius;
typedArray.recycle();
}
setOnTouchListener(this);
positionAnimator = ValueAnimator.ofFloat(1);
positionAnimator.addUpdateListener(animation -> {
Float aFloat = (Float) animation.getAnimatedValue();
changeFingerPosition(lastX + (centerX - lastX) * aFloat, lastY + (centerY - lastY) * aFloat);
});
}

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(getActualSpec(widthMeasureSpec), getActualSpec(heightMeasureSpec));
}


//处理wrapcontent的测量
//默认wrapcontent,没有做matchParent,指定大小的适配
//view实际的大小是通过大圆半径确定的
public int getActualSpec(int spec) {
int mode = MeasureSpec.getMode(spec);
int len = MeasureSpec.getSize(spec);
switch (mode) {
case MeasureSpec.AT_MOST:
len = (int) (radius * 2);
break;
}
return MeasureSpec.makeMeasureSpec(len, mode);
}

//绘制
@Override protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(centerX, centerY, radius, borderPaint);
canvas.drawCircle(fingerX, fingerY, innerRadius, fingerPaint);
}

@Override public boolean onTouch(View v, MotionEvent event) {
float evx = event.getX(), evy = event.getY();
float deltaX = evx - centerX, deltaY = evy - centerY;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//圆外按压不生效
if (deltaX * deltaX + deltaY * deltaY > radius * radius) {
break;
}
case MotionEvent.ACTION_MOVE:
//如果触摸点在圆外
if (Math.abs(deltaX) > radiusBorder || Math.abs(deltaY) > radiusBorder) {
float distance = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY);
changeFingerPosition(centerX + (deltaX * radiusBorder / distance),
centerY + (deltaY * radiusBorder / distance));
} else { //如果触摸点在圆内
changeFingerPosition(evx, evy);
}
positionAnimator.cancel();
break;
case MotionEvent.ACTION_UP:
positionAnimator.setDuration(1000);
positionAnimator.start();
break;
}
return true;
}

/**
* 改变位置的回调出来
*/

private void changeFingerPosition(float fingerX, float fingerY) {
this.fingerX = fingerX;
this.fingerY = fingerY;
if (moveListener != null) {
float r = radius - innerRadius;
if (r == 0) {
invalidate();
return;
}
moveListener.move((fingerX - centerX) / r, (fingerY - centerY) / r);
}
invalidate();
}

@Override protected void finalize() throws Throwable {
super.finalize();
positionAnimator.removeAllListeners();
}

public void setMoveListener(
MoveListener moveListener)
{
this.moveListener = moveListener;
}

/**
*回调事件的接口
*
**/

public interface MoveListener {
void move(float dx, float dy);
}
}

style.xml

name="ControllerView">
name="fingerColor" format="color" />
name="borderColor" format="color" />
name="fingerSize" format="dimension" />
name="radius" format="dimension" />



原文链接:https://juejin.cn/post/7007252815672279053
收起阅读 »

Android 架构师之路 - AOP 面向切面编程

引言相信很多做过Web的同学对AspectJ都不陌生,Spring的AOP就是基于它而来的。如果说平常我们随便写写程序的时候,基本也不会用到它,需要调试的话无非就是多加一个System.out.printfln()或者Log.d()。但是由于基于面向对象的固有...
继续阅读 »


引言

相信很多做过Web的同学对AspectJ都不陌生,Spring的AOP就是基于它而来的。如果说平常我们随便写写程序的时候,基本也不会用到它,需要调试的话无非就是多加一个System.out.printfln()或者Log.d()。但是由于基于面向对象的固有缺陷,导致很多同模块、同一水平上的工作要在许多类中重复出现。比如说:输出日志,监控方法执行时间,修改程序运行时的参数等等这样的事情,其实它们的代码都是可以重用的。

如果在一个大型的项目当中,使用手动修改源码的方式来达到调试、监控的目的,第一,需要插入许多重复代码(打印日志,监控方法执行时间),代码无法复用;第二,修改的成本太高,处处需要手动修改(分分钟累死、眼花)。

  • OOP: 面向对象把所有的事物都当做对象看待,因此每一个对象都有自己的生命周期,都是一个封装的整体。每一个对象都有自己的一套垂直的系列方法和属性,使得我们使用对象的时候不需要太多的关系它的内部细节和实现过程,只需要关注输入和输出,这跟我们的思维方式非常相近,极大的降低了我们的编写代码成本(而不像C那样让人头痛!)。但在现实世界中,并不是所有问题都能完美得划分到模块中。举个最简单而又常见的例子:现在想为每个模块加上日志功能,要求模块运行时候能输出日志。在不知道AOP的情况下,一般的处理都是:先设计一个日志输出模块,这个模块提供日志输出API,比如Android中的Log类。然后,其他模块需要输出日志的时候调用Log类的几个函数,比如e(TAG,…),w(TAG,…),d(TAG,…),i(TAG,…)等。
  • AOP: OOP固然开启另一个编程时代,但是久而久之也显露了它的缺点,最明显的一点就是它无法横向切割某一类方法、属性,当我们需要了解某一类方法、某一类属性的信息时,就必须要在每一个类的方法里面(即便他们是同样的方法,只因是不同的类所以不同)添加监控代码,在代码量庞大的情况下,这是一个不可取的方法。因此,AOP编产生了,基于AOP的编程可以让我们横向的切割某一类方法和属性(不需要关心他是什么类别!),AOP并不是与OOP对立的,而是为了弥补OOP的不足,因为有了AOP我们的调试和监控就变得简单清晰。

1.AspectJ介绍

1.1 AspectJ只是一个代码编译器

AspectJ 意思就是Java的Aspect,Java的AOP。它其实不是一个新的语言,它就是一个代码编译器(ajc,后面以此代替),在Java编译器的基础上增加了一些它自己的关键字识别和编译方法。因此,ajc也可以编译Java代码。它在编译期将开发者编写的Aspect程序编织到目标程序中,对目标程序作了重构,目的就是建立目标程序与Aspect程序的连接(耦合,获得对方的引用(获得的是声明类型,不是运行时类型)和上下文信息),从而达到AOP的目的(这里在编译期还是修改了原来程序的代码,但是是ajc替我们做的)。

1.2 AspectJ是用来做AOP编程的

Cross-cutting concerns(横切关注点): 尽管面向对象模型中大多数类会实现单一特定的功能,但通常也会开放一些通用的附属功能给其他类。例如,我们希望在数据访问层中的类中添加日志,同时也希望当UI层中一个线程进入或者退出调用一个方法时添加日志。尽管每个类都有一个区别于其他类的主要功能,但在代码里,仍然经常需要添加一些相同的附属功能。

  • Advice(通知): 注入到class文件中的代码。典型的 Advice 类型有 before、after 和 around,分别表示在目标方法执行之前、执行后和完全替代目标方法执行的代码。 除了在方法中注入代码,也可能会对代码做其他修改,比如在一个class中增加字段或者接口。
  • Joint point(连接点): 程序中可能作为代码注入目标的特定的点,例如一个方法调用或者方法入口。
  • Pointcut(切入点): 告诉代码注入工具,在何处注入一段特定代码的表达式。例如,在哪些 joint points 应用一个特定的 Advice。切入点可以选择唯一一个,比如执行某一个方法,也可以有多个选择,比如,标记了一个定义成@DebguTrace 的自定义注解的所有方法。
  • Aspect(切面): Pointcut 和 Advice 的组合看做切面。例如,我们在应用中通过定义一个 pointcut 和给定恰当的advice,添加一个日志切面。
  • Weaving(织入): 注入代码(advices)到目标位置(joint points)的过程。

下面这张图简要总结了一下上述这些概念。

传统编程:逐个插入验证用户模块

AOP方案:关注点聚焦

1.3、为什么要用AspectJ?
  • 非侵入式监控: 支持编译期和加载时代码注入,可以在不修监控目标的情况下监控其运行,截获某类方法,甚至可以修改其参数和运行轨迹!
  • 易于使用: 它就是Java,只要会Java就可以用它。
  • 功能强大,可拓展性高: 它就是一个编译器+一个库,可以让开发者最大限度的发挥,实现形形色色的AOP程序!

2、下载AspectJ相关资源与build.gradle配置

2.1、下载地址

下载aspectj的地址http://www.eclipse.org/aspectj/dow…\

2.2、解压aspectj jar包得到aspectjrt.jar
2.3、build.gradle配置

参考build.gradle aspectJ 写法 fernandocejas.com/2014/08/03/…
根目录中build.gradle配置:


buildscript {

repositories {
mavenCentral()
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0'
classpath 'org.aspectj:aspectjtools:1.8.13'
classpath 'org.aspectj:aspectjweaver:1.8.13'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

allprojects {
repositories {
google()
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

modules中build.gradle配置:


apply plugin: 'com.android.application'
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main


android {
compileSdkVersion 26
defaultConfig {
applicationId "com.haocai.aopdemo"
minSdkVersion 15
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

final def log = project.logger
final def variants = project.android.applicationVariants

variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
return;
}

JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)

MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break;
case IMessage.WARNING:
log.warn message.message, message.thrown
break;
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}

dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
testImplementation 'junit:junit:4.12'
compile files('libs/aspectjrt.jar')
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
// compile 'org.aspectj:aspectjrt:1.8.+'
}
注意:

dependencies 不要忘记添加 compile files('libs/aspectjrt.jar') ,aspectjrt.jar就是上一步解压得到的文件,放到libs文件夹下

3、示例程序

创建注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface BehaviorTrace {
String value();
int type();
}
Aspect 类

/**
* Created by Xionghu on 2018/1/23.
* Desc: 切面
* 你想要切下来的部分(代码逻辑功能重复模块)
*/
@Aspect
public class BehaviorAspect {
private static final String TAG = "MainAspect";
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 根据切点 切成什么样子
*
*/
@Pointcut("execution(@com.haocai.aopdemo.BehaviorTrace * *(..))")
public void annoBehavior() {

}
/**
* 切成什么样子之后,怎么去处理
*
*/

@Around("annoBehavior()")
public Object dealPoint(ProceedingJoinPoint point) throws Throwable{
//方法执行前
MethodSignature methodSignature = (MethodSignature)point.getSignature();
BehaviorTrace behaviorTrace = methodSignature.getMethod().getAnnotation(BehaviorTrace.class);
String contentType = behaviorTrace.value();
int type = behaviorTrace.type();
Log.i(TAG,contentType+"使用时间: "+simpleDateFormat.format(new Date()));
long beagin=System.currentTimeMillis();
//方法执行时
Object object = null;
try{
object = point.proceed();
}catch (Exception e){
e.printStackTrace();
}

//方法执行完成
Log.i(TAG,"消耗时间:"+(System.currentTimeMillis()-beagin)+"ms");
return object;
}
}
调用主程序


package com.haocai.aopdemo;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "Main";
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);





}
/**
* 摇一摇的模块
*
* @param view
*/
@BehaviorTrace(value = "摇一摇",type = 1)
public void mShake(View view)
{
//摇一摇的代码逻辑
{
SystemClock.sleep(3000);
Log.i(TAG," 摇到一个红包");

}
}
/**
* 语音的模块
*
* @param view
*/
@BehaviorTrace(value = "语音:",type = 1)
public void mAudio(View view)
{
//语音代码逻辑
{
SystemClock.sleep(3000);
Log.i(TAG,"发语音:我要到一个红包啦");
}
}
/**
* 打字模块
*
* @param view
*/
@BehaviorTrace(value = "打字:",type = 1)
public void mText(View view)
{
//打字模块逻辑
{
SystemClock.sleep(3000);
Log.i(TAG,"打字逻辑,我摇到了一个大红包");

}

}


// /**
// * 摇一摇的模块
// *
// * @param view
// */
// @BehaviorTrace(value = "摇一摇",type = 1)
// public void mShake(View view)
// {
// SystemClock.sleep(3000);
// Log.i(TAG," 摇到一个嫩模: 约不约");
// }
//
// /**
// * 摇一摇的模块
// *
// * @param view
// */
// public void mShake(View view)
// {
//
// long beagin=System.currentTimeMillis();
// Log.i(TAG,"摇一摇: 使用时间: "+simpleDateFormat.format(new Date()));
// //摇一摇的代码逻辑
// {
// SystemClock.sleep(3000);
//
// Log.i(TAG," 摇到一个红包");
//
// }
// //事件统计逻辑
// Log.i(TAG,"消耗时间: "+(System.currentTimeMillis()-beagin)+"ms");
//
// }

// /**
// * 语音的模块
// *
// * @param view
// */
// public void mAudio(View view)
// {
// long beagin=System.currentTimeMillis();
// Log.i(TAG,"语音: 使用时间: "+simpleDateFormat.format(new Date()));
// //语音代码逻辑
// {
// SystemClock.sleep(3000);
//
// Log.i(TAG,"发语音:我要到一个红包啦");
//
// }
// //事件统计逻辑
// Log.i(TAG,"消耗时间: "+(System.currentTimeMillis()-beagin)+"ms");
// }
//
// /**
// * 打字模块
// *
// * @param view
// */
// public void mText(View view)
// {
// //统计用户行为 的逻辑
// Log.i(TAG,"文字: 使用时间: "+simpleDateFormat.format(new Date()));
// long beagin=System.currentTimeMillis();
//
// //打字模块逻辑
// {
// SystemClock.sleep(3000);
// Log.i(TAG,"打字逻辑,我摇到了一个大红包");
//
// }
// //事件统计逻辑
// Log.i(TAG,"消耗时间: "+(System.currentTimeMillis()-beagin)+"ms");
// }

}
注意:下面注释部分为传统写法
运行结果

01-23 19:39:09.579 13051-13051/com.haocai.aopdemo I/MainAspect: 摇一摇使用时间:   2018-01-23 19:39:09
01-23 19:39:12.589 13051-13051/com.haocai.aopdemo I/Main: 摇到一个红包
01-23 19:39:12.589 13051-13051/com.haocai.aopdemo I/MainAspect: 消耗时间:3001ms
01-23 19:39:12.589 13051-13051/com.haocai.aopdemo I/MainAspect: 语音:使用时间: 2018-01-23 19:39:12
01-23 19:39:15.599 13051-13051/com.haocai.aopdemo I/Main: 发语音:我要到一个红包啦
01-23 19:39:15.599 13051-13051/com.haocai.aopdemo I/MainAspect: 消耗时间:3000ms
01-23 19:39:15.599 13051-13051/com.haocai.aopdemo I/MainAspect: 打字:使用时间: 2018-01-23 19:39:15
01-23 19:39:18.609 13051-13051/com.haocai.aopdemo I/Main: 打字逻辑,我摇到了一个大红包
01-23 19:39:18.609 13051-13051/com.haocai.aopdemo I/MainAspect: 消耗时间:3000ms
收起阅读 »

安卓分页加载器——Paging使用指南

一、简介应用开发过程中分页加载时很普遍的需求,它能节省数据流量,提升应用的性能。 Google为了方便开发者完成分页加载而推出了分页组件—Paging。为几种常见的分页机制提供了统一的解决方案。优势分页数据的内存中缓存。该功能可确保应用在处理分页数据时高效利用...
继续阅读 »

一、简介

应用开发过程中分页加载时很普遍的需求,它能节省数据流量,提升应用的性能。 Google为了方便开发者完成分页加载而推出了分页组件—Paging。为几种常见的分页机制提供了统一的解决方案。

  • 优势
    • 分页数据的内存中缓存。该功能可确保应用在处理分页数据时高效利用系统资源。
    • 内置的请求重复信息删除功能,可确保应用高效利用网络带宽和系统资源。
    • 可配置的RecyclerView适配器,会在用户滚动到已加载数据的末尾时自动请求数据。
    • 对Kotlin协程和Flow以及LiveData和RxJava的一流支持。
    • 内置对错误处理功能的支持,包括刷新和重试功能。
  • 数据来源:Paging支持三种数据架构类型
    • 网络:对网络数据进行分页加载是最常见的需求。API接口通常不太一样,Paging提供了三种不同的方案,应对不同的分页机制。Paging不提供任务错误处理功能,发生错误后可重试网络请求。
    • 数据库:数据库进行分页加载和网络类似,推荐使用Room数据库修改和插入数据。
    • 网络+数据库:通常只采用单一数据源作为解决方案,从网络获取数据,直接缓存进数据库,列表直接从数据库中获取数据。

二、核心

2.1 核心类

Paging的工作原理主要涉及三个类:

  1. PagedListAdapter:RecyclerView.Adapter基类,用于在RecyclerView显示来自PagedList的分页数据。
  2. PagedList:PagedList负责通知DataSource何时获取数据,如加载第一页、最后一页及加载数量等。从DataSource获取的数据将存储在PagedList中。
  3. DataSource:执行具体的数据载入工作,数据载入需要在工作线程中进行

以上三个类的关系及数据加载流程如下图:

20181021221030916.gif

当一条新的item插入到数据库,DataSource会被初始化,LiveData后台线程就会创建一个新的PagedList。这个新的PagedList会被发送到UI线程的PagedListAdapter中,PagedListAdapter使用DiffUtil在对比现在的Item和新建Item的差异。当对比结束,PagedListAdapter通过调用RecycleView.Adapter.notifyItemInserted()将新的item插入到适当的位置

2.2 DataSource

根据分页机制的不同,Paing为我们提供了三种DataSource。

  1. PositionalDataSource

适用于可通过任意位置加载数据,且目标数据源数量固定的情况。

  1. PageKeyedDataSource

适合数据源以“页”的方式进行请求的情况。如获取数据携带pagepageSize时。本文代码使用此DataSource

  1. ItemKeyedDataSource

适用于当目标数据的下一页需要依赖上一页数据中的最后一个对象中的某个字段作为key的情况,如评论数据的接口携带参数sincepageSize

三、使用

3.1 构建自己的DataSource

DataSource控制数据加载,包括初始化加载,加载上页数据,加载下页数据。此处我们以PageKeyedDataSource为例

//泛型参数未Key Value,Key就是每页的标志,此处为Long,Value为数据类型
class ListDataSource : PageKeyedDataSource<Long, Item>() {
//重试加载时的参数
private var lastLoadParam: Pair<LoadParams<Long>, LoadCallback<Long, Item>>? = null

}

其中的关键点在于,每次Key的选定以及loadInitialloadBeforeloadAfter三个函数的重写。PageKeyedDataSource的Key一般依赖与服务端返回的数据。

3.2 构建PagedList

companion object{

private const val TAG = "List"
const val PAGE_SIZE = 5
const val FETCH_DIS = 1

}
val ListData: LiveData<PagedList<Item>> = LivePagedListBuilder(
dataSourceFactory,
Config(
PAGE_SIZE,
FETCH_DIS,
true
)
).build()

其中PAGE_SIZE是每页的数量,FETCH_DIS是距离最后一个数据item还有多少距离就触发加载动作。

此处ListData是LiveData类型,因此可以在Activity中进行监听,当发生数据变化时,则刷新adapter:

ListViewModel.ListData.observe(this) {
adapter.submitList(it)
}

3.3 构建自己的PagedListAdapter

一定要继承PagedListAdapter<Item, RecyclerView.ViewHolder>(``POST_COMPARATOR``)POST_COMPARATOR就是DiffUtil,PagedListAdapter使用DiffUtil在对比现在的Item和新建Item的差异。

typealias ItemClickListener = (Item) -> Unit
typealias onClickListener = () -> Unit

class ListAdapter(
pri
}
}

可以看到基本写法和普通的RecyclerView.Adapter是差不多的,只是多了DiffUtil,使用起来也是一样:

adapter = ListAdapter(
this,
onItemClickListener,
headRetryClickListener,
footRetryClickListener
)
list_rv.adapter = adapter

四、Paging 3.0

Paging3与旧版Paging存在很大区别。Paging2.x运行起来的效果无限滑动还不错,不过代码写起来有点麻烦,功能也不是太完善,比如下拉刷新的方法都没有提供,我们还得自己去调用DataSource#invalidate()方法重置数据来实现。Paging3.0功能更加强大,用起来更简单。

4.1 区别

  • DataSource

Paing2中的DataSource有三种,Paging3中将它们合并到了PagingSource中,实现load()和getRefreshKey(),在Paging3中,所有加载方法参数被一个LoadParams密封类替代,该类中包含了每个加载类型所对应的子类。如果需要区分load()中的加载类型,需要检查传入了LoadParams的哪个子类

  • PagedListAdapter

Adapter不在继承PagedListAdapter,而是由PagingDataAdapter替代,其它不变。

class ArticleAdapter : PagingDataAdapter<Article,ArticleViewHolder>(POST_COMPARATOR){

companion object{

val POST_COMPARATOR = object : DiffUtil.ItemCallback<Article>() {
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean =
oldItem == newItem

override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean =
oldItem.id == newItem.id
}
}

override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
holder.tvName.text = getItem(position)?.title
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
return ArticleViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_item,parent,false))
}
}

class ArticleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val tvName: TextView = itemView.findViewById(R.id.tvname)
}

4.2 获取数据并设置给Adapter

google提倡我使用三层架构来完成数据到Adapter的设置,如下图

image.png

代码库层

代码库层中的主要 Paging 库组件是 PagingSource。每个 PagingSource 对象都定义了数据源,以及如何从该数据源检索数据。PagingSource 对象可以从任何单个数据源(包括网络来源和本地数据库)加载数据。可使用的另一个 Paging 库组件是 RemoteMediatorRemoteMediator 对象会处理来自分层数据源(例如具有本地数据库缓存的网络数据源)的分页。

ViewModel 层

Pager 组件提供了一个公共 API,基于 PagingSource 对象和 PagingConfig 配置对象来构造在响应式流中公开的 PagingData 实例。将 ViewModel 层连接到界面的组件是 PagingData。 PagingData 对象是用于存放分页数据快照的容器。它会查询 PagingSource 对象并存储结果。

界面层

界面层中的主要 Paging 库组件是 PagingDataAdapter

收起阅读 »

安卓-Glidel图片加载框架学习笔记

引用地址: muyangmin.github.io/glide-docs-… 以glide Version = '4.12.0'为例 1.Gradle配置 此处配置在子模块里(要添加到app主模块也可以),非app主模块里 //glide图片加载框架 impl...
继续阅读 »

引用地址:


muyangmin.github.io/glide-docs-…


以glide Version = '4.12.0'为例


1.Gradle配置


此处配置在子模块里(要添加到app主模块也可以),非app主模块里


//glide图片加载框架
implementation "com.github.bumptech.glide:annotations:${rootProject.glideVersion}"
api "com.github.bumptech.glide:glide:${rootProject.glideVersion}"
annotationProcessor "com.github.bumptech.glide:compiler:${rootProject.glideVersion}"

app模块只需要添加下面一句,然后app主模块引用上面的子模块


implementation project(":CommonModule")
annotationProcessor "com.github.bumptech.glide:compiler:${rootProject.glideVersion}"

2.添加权限声明


添加到子模块AndroidManifest.xml里即可(非app主模块)


<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

3.如果使用到Proguard


如果你有使用到 proguard,那么请把以下代码添加到你的 proguard.cfg 文件中:


-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}

4.@GlideModule 定义AppGlideModule子类和LibraryGlideModule子类


定义一个AppGlideModule子类,定义在app主模块里,而且只能定义在app主模块里。


package com.example.myapp;

import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;

@GlideModule
public final class MyAppGlideModule extends AppGlideModule {}

定义LibraryGlideModule,定义在非app子模块里。


import android.content.Context;
import android.util.Log;

import androidx.annotation.NonNull;

import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.LibraryGlideModule;

@GlideModule
public class MyLibraryGlideModule extends LibraryGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
super.registerComponents(context, glide, registry);
Log.d("MyLibraryGlideModule","MyLibraryGlideModule");
}
}

定义完成后,不要忘记了 Build ->Make Project,然后会生成会在app\build\generated\ap_generated_sources\debug\out的目录下生成相应的文件,如GlideApp.java,LibraryGlideModule也会自动在生成相应的文件。


5.使用 Generated API


Generated API 默认名为 GlideApp ,与 Application 模块中 AppGlideModule的子类包名相同。在 Application 模块中将 Glide.with() 替换为 GlideApp.with(),即可使用该 API 去完成加载工作:


GlideApp.with(fragment)
.load(myUrl)
.placeholder(R.drawable.placeholder)
.fitCenter()
.into(imageView);

占位符:.placeholder(placeholder) 加载中的占位符


加载失败占位符:.error(android.R.drawable.stat_notify_error)


RequestOptions在多个请求之间共享配置


RequestOptions sharedOptions = 
new RequestOptions()
.placeholder(placeholder)
.fitCenter();

Glide.with(fragment)
.load(myUrl)
.apply(sharedOptions)
.into(imageView1);

Glide.with(fragment)
.load(myUrl)
.apply(sharedOptions)
.into(imageView2);

6.在 ListView 和 RecyclerView 中的使用


在 ListView 或 RecyclerView 中加载图片的代码和在单独的 View 中加载完全一样。Glide 已经自动处理了 View 的复用和请求的取消:


View 调用 clear()into(View),表明在此之前的加载操作会被取消,并且在方法调用完成后,Glide 不会改变 view 的内容。如果你忘记调用 clear(),而又没有开启新的加载操作,那么就会出现这种情况,你已经为一个 view 设置好了一个 Drawable,但该 view 在之前的位置上使用 Glide 进行过加载图片的操作,Glide 加载完毕后可能会将这个 view 改回成原来的内容。


这里的代码以 RecyclerView 的使用为例,但规则同样适用于 ListView。


正确用法1


@Override
public void onBindViewHolder(ViewHolder holder, int position) {
String url = urls.get(position);
Glide.with(fragment)
.load(url)
.into(holder.imageView);
}

正确用法2


@Override
public void onBindViewHolder(ViewHolder holder, int position) {
if (isImagePosition(position)) {
String url = urls.get(position);
Glide.with(fragment)
.load(url)
.into(holder.imageView);
} else {
Glide.with(fragment).clear(holder.imageView);
holder.imageView.setImageDrawable(specialDrawable);
}
}

7.非 View 目标


除了将 BitmapDrawable 加载到 View 之外,你也可以开始异步加载到你的自定义 Target 中:


Glide.with(context
.load(url)
.into(new CustomTarget<Drawable>() {
@Override
public void onResourceReady(Drawable resource, Transition<Drawable> transition) {
// Do something with the Drawable here.
}

@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
// Remove the Drawable provided in onResourceReady from any Views and ensure
// no references to it remain.
}
});

8.后台线程


后台线程下载,


FutureTarget<Bitmap> futureTarget =
Glide.with(context)
.asBitmap()
.load(url)
.submit(width, height);

Bitmap bitmap = futureTarget.get();

// Do something with the Bitmap and then when you're done with it:
Glide.with(context).clear(futureTarget);

后台同步实现图片下载


 //不能直接在主线程里调用,会直接ANR
FutureTarget<File> futureTarget = Glide.with(GlideImageLoadFragment.this)
.asFile()
.load("https://dss3.bdstatic.com/iPoZeXSm1A5BphGlnYG/skin/822.jpg?2")
.addListener(new RequestListener<File>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e,
Object model, Target<File> target,
boolean isFirstResource) {
return false;
}

@Override
public boolean onResourceReady(File resource, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
return false;
}
}).submit();

//下载到的文件没有扩展名。
File file = futureTarget.get();

异常下载图片并回调 .asFile()


GlideApp.with(this)
.asFile()
.load("https://img.soogif.com/rSlMSm7msQagXhSSgIQ0LtqTusCK712l.gif")
.into(new CustomTarget<File>() {
@Override
public void onResourceReady(@NonNull File resource, @Nullable Transition<? super File> transition) {
Log.d("", "");
}

@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
Log.d("", "");
}
});

8.Glide扩展 GlideExtension


@GlideExtension 注解用于标识一个扩展 Glide API 的类。任何扩展 Glide API 的类都必须使用这个注解来标记,否则其中被注解的方法就会被忽略。


@GlideExtension 注解的类应以工具类的思维编写。这种类应该有一个私有的、空的构造方法,应为 final 类型,并且仅包含静态方法。被注解的类可以含有静态变量,可以引用其他的类或对象。


在 Application 模块中可以根据需求实现任意多个被 @GlideExtension 注解的类,在 Library 模块中同样如此。当 AppGlideModule 被发现时,所有有效的 Glide 扩展类 会被合并,所有的选项在 API 中均可以被调用。合并冲突会导致 Glide 的 Annotation Processor 抛出编译错误。


@GlideExtention 注解的类有两种扩展方式:



  1. GlideOption - 为 RequestOptions 添加一个自定义的选项。

  2. GlideType - 添加对新的资源类型的支持(GIF,SVG 等等)。


@GlideExtension


@GlideExtension只能在app模块里使用。


//@GlideExtension 注解到类上面,只能在app模块里使用。
@GlideExtension
public class MyAppExtension {
// Size of mini thumb in pixels.
private static final int MINI_THUMB_SIZE = 100;

private MyAppExtension() { } // utility class

//@GlideOption注解到方法里面。标记的方法应该为静态方法
@NonNull
@GlideOption
public static BaseRequestOptions<?> miniThumb(BaseRequestOptions<?> options) {
return options
.fitCenter()
.override(MINI_THUMB_SIZE);
}

// 你可以为方法任意添加参数,但要保证第一个参数为 RequestOptions。 标记的方法应该为静态方法
@GlideOption
public static BaseRequestOptions<?> miniThumb(BaseRequestOptions<?> options, int size) {
return options
.fitCenter()
.override(size);
}
}

添加完成后 执行 Build ->Make Project,可以看到app\build\generated\ap_generated_sources\debug\out\my\android\architecture\samples\glide\GlideOptions.java


里生成了下面的方法


@SuppressWarnings("unchecked")
@CheckResult
@NonNull
public GlideOptions miniThumb() {
return (GlideOptions) MyAppExtension.miniThumb(this);
}

/**
* @see MyAppExtension#miniThumb(BaseRequestOptions, int)
*/
@SuppressWarnings("unchecked")
@CheckResult
@NonNull
public GlideOptions miniThumb(int size) {
return (GlideOptions) MyAppExtension.miniThumb(this, size);
}

调用


GlideApp.with(fragment)
.load(url)
.miniThumb(thumbnailSize)
.into(imageView);

GlideType


@GlideType 注解的静态方法用于扩展 RequestManager 。被 @GlideType 注解的方法允许你添加对新的资源类型的支持,包括指定默认选项。


例如,为添加对 GIF 的支持,你可以添加一个被 @GlideType 注解的方法:


@GlideExtension
public class MyAppExtension {
private static final RequestOptions DECODE_TYPE_GIF = decodeTypeOf(GifDrawable.class).lock();

@NonNull
@GlideType(GifDrawable.class)
public static RequestBuilder<GifDrwable> asGif(RequestBuilder<GifDrawable> requestBuilder) {
return requestBuilder
.transition(new DrawableTransitionOptions())
.apply(DECODE_TYPE_GIF);
}
}

这样会生成一个包含对应方法的 RequestManager


public class GlideRequests extends RequesetManager {

public GlideRequest<GifDrawable> asGif() {
return (GlideRequest<GifDrawable> MyAppExtension.asGif(this.as(GifDrawable.class));
}

...
}

9.占位符


Glide允许用户指定三种不同类型的占位符,分别在三种不同场景使用:


placeholder
error
fallback


占位符(Placeholder)


占位符是当请求正在执行时被展示的 Drawable 。当请求成功完成时,占位符会被请求到的资源替换。如果被请求的资源是从内存中加载出来的,那么占位符可能根本不会被显示。如果请求失败并且没有设置 error Drawable ,则占位符将被持续展示。类似地,如果请求的url/model为 null ,并且 error Drawablefallback 都没有设置,那么占位符也会继续显示。


错误符(Error)


error Drawable 在请求永久性失败时展示。error Drawable 同样也在请求的url/model为 null ,且并没有设置 fallback Drawable 时展示。


后备回调符(Fallback)


fallback Drawable 在请求的url/model为 null 时展示。设计 fallback Drawable 的主要目的是允许用户指示 null 是否为可接受的正常情况。例如,一个 null 的个人资料 url 可能暗示这个用户没有设置头像,因此应该使用默认头像。然而,null 也可能表明这个元数据根本就是不合法的,或者取不到。 默认情况下Glide将 null 作为错误处理,所以可以接受 null 的应用应当显式地设置一个 fallback Drawable


占位符是异步加载的吗?

No。占位符是在主线程从Android Resources加载的。我们通常希望占位符比较小且容易被系统资源缓存机制缓存起来。


变换是否会被应用到占位符上?

No。Transformation仅被应用于被请求的资源,而不会对任何占位符使用。


在应用中包含必须在运行时做变换才能使用的图片资源是很不划算的。相反,在应用中包含一个确切符合尺寸和形状要求的资源版本几乎总是一个更好的办法。假如你正在加载圆形图片,你可能希望在你的应用中包含圆形的占位符。另外你也可以考虑自定义一个View来剪裁(clip)你的占位符,而达到你想要的变换效果。


在多个不同的View上使用相同的Drawable可行么?

通常可以,但不是绝对的。任何无状态(non-stateful)的 Drawable(例如 BitmapDrawable )通常都是ok的。但是有状态的 Drawable 不一样,在同一时间多个 View 上展示它们通常不是很安全,因为多个View会立刻修改(mutate) Drawable 。对于有状态的 Drawable ,建议传入一个资源ID,或者使用 newDrawable() 来给每个请求传入一个新的拷贝。


10.选项


1.请求选项


Glide中的大部分设置项都可以直接应用在 Glide.with() 返回的 RequestBuilder 对象上。


可用的选项包括(但不限于):



  • 占位符(Placeholders)

  • 转换(Transformations)

  • 缓存策略(Caching Strategies)

  • 组件特有的设置项,例如编码质量,或Bitmap的解码配置等。


例如,要应用一个 CenterCrop 转换,你可以使用以下代码:


Glide.with(fragment)
.load(url)
.centerCrop()
.into(imageView);

RequestOptions对象 apply(@NonNull BaseRequestOptions<?> options)


如果你想让你的应用的不同部分之间共享相同的加载选项,你也可以初始化一个新的 RequestOptions 对象,并在每次加载时通过 apply() 方法传入这个对象:


RequestOptions cropOptions = new RequestOptions().centerCrop(context);
...
Glide.with(fragment)
.load(url)
.apply(cropOptions)
.into(imageView);

apply() 方法可以被调用多次,因此 RequestOption 可以被组合使用。如果 RequestOptions 对象之间存在相互冲突的设置,那么只有最后一个被应用的 RequestOptions 会生效。


过渡选项 transition


不同于RequestOptionsTransitionOptions是特定资源类型独有的,你能使用的变换取决于你让Glide加载哪种类型的资源。


这样的结果是,假如你请求加载一个 Bitmap ,你需要使用 BitmapTransitionOptions ,而不是 DrawableTransitionOptions 。同样,当你请求加载 Bitmap时,你只需要做简单的淡入,而不需要做复杂的交叉淡入。


RequestBuilder


使用 RequestBuilder 可以指定:



  • 你想加载的资源类型(Bitmap, Drawable, 或其他)

  • 你要加载的资源地址(url/model)

  • 你想最终加载到的View

  • 任何你想应用的(一个或多个)RequestOption 对象

  • 任何你想应用的(一个或多个)TransitionOption 对象

  • 任何你想加载的缩略图 thumbnail()


选择资源类型


作者:缘焕
链接:https://juejin.cn/post/7006871344877060110
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

2021年,跨端是否已成趋势?Android 开发还有必要学 Flutter 吗?

由于手机APP的运行受不同操作系统的限制,目前大多数的移动APP应用开发仍然需要针对不同的系统环境进行单独的开发。不过,为了降低开发成本、提高代码复用率,减少开发者对多个平台差异适配的工作量一直是跨平台开发框架追求的目标。 但是目前,很多开发者还不不确定应该选...
继续阅读 »

由于手机APP的运行受不同操作系统的限制,目前大多数的移动APP应用开发仍然需要针对不同的系统环境进行单独的开发。不过,为了降低开发成本、提高代码复用率,减少开发者对多个平台差异适配的工作量一直是跨平台开发框架追求的目标。


但是目前,很多开发者还不不确定应该选择哪种技术来快速且低成本的开发应用程序,不过如果你熟知跨平台的发展历史,那么2021年可供大家选择的跨平台方案主选项只有两个:Flutter或者React Native



在正式进行对比之前,首先需要明确一点,即Flutter和React Native这两个框架都是构建跨平台移动应用程序的优质框架,但有时做出正确的决定取决于业务使用的角度。因此,我们选取了九个重要的参数,用于两者的比较:



  • 由谁提供技术支持?

  • 框架的市场份额占比。

  • Dart Vs JavaScript

  • 技术架构

  • 性能

  • 是否对开发者友好,便利性和社区支持

  • UI组件和定制

  • 代码的可维护性

  • 开发者的工作成本


技术支持:谷歌 VS Facebook


Flutter与React Native两大框架背后都站着科技巨头,分别是谷歌和Facebook,所以从这个角度来看两者未来会在竞争中变得更加完善,毕竟他们背后都自己的利益链。


首先,我们来看一下Flutter,Flutter是2017年由谷歌正式推出,是一个先进的应用程序软件开发工具包(SDK),包括所有的小部件和工具,理论上可以让开发者的开发过程更容易和更简单。广泛的小工具选择使开发人员能够以一种简单的方式建立和部署视觉上有吸引力的、原生编译的应用程序,用于多个平台,包括移动、网络和桌面,都使用单一的代码库。因此,Flutter应用程序开发公司有更好的机会,可以确保你更快、更快、更可靠的应用程序开发解决方案。


事实上,Flutter早再2015年Dart开发者峰会上便以“Sky”的身份亮相,Flutter具有几大买点:首先它是免费的,而且是开源的;其次,该架构基于流行的反应式编程,因为它遵循与Reactive相同的风格;最后,归功于小部件体验,Flutter应用程序有一个令人愉快的UI,整体来说转化为应用程序看起来和感觉都不错。


我们再来看一下React Native,React Native也是Facebook在2015年推出的一个跨平台原生移动应用开发框架。React Native主要使用的是JavaScript开发语言,对于使用同一代码库为iOS和Android开发应用程序来说非常方便。此外,它的代码共享功能可以更快的开发和减少开发时间。像其他跨平台技术一样,Flutter允许开发者使用相同的代码库来构建独立的应用程序,因此,相比原生应用程序更容易维护。


当然,Flutter和React Native都支持热重载功能,允许开发者直接在运行中的应用程序中添加或纠正代码,而不必保存应用程序,从而加速了开发过程。除此之外,React Native是基于一种非常流行的语言--JavaScript,开发者更易上手;React组件包裹着现有的本地代码,并通过React的声明性UI范式和JavaScript与本地API进行交互,React Native的这些特点使开发人员的工作速度大大加快。


市场份额:五五开的格局正在改变


整体上来说,这两者的市场份额是十分相近的,但Flutter在最近有后来居上之势。2019年和2020年全球软件开发公司使用的最佳跨平台移动应用开发框架时,其结果是42%的开发者更愿意留在React Native,而39%的开发者选择了Flutter。根据StackOverFlow的数据,68.8%的开发者喜欢使用Flutter进行进一步的开发项目,而57.9%的开发者对使用React Native技术进行应用开发进一步表现出兴趣。


不同的市场报告有不同的统计数字,Flutter、React Native究竟孰强孰弱或许只能从一些市场趋势中窥见一二:




  • 市场趋势一:谷歌Google Trends的统计数字显示,在过去12个月的分析中,Flutter的搜索指数已反超React Native。




  • 市场趋势二:更年轻的Flutter在Github上拥有16.8万名成员和11.8万颗星的社区,而更成熟的React Native在Github仅有20.7万名成员和9.46万颗星。


    image.png




  • 趋势三:根据Statista的数据,React Native以42%的市场份额力压Flutter,但Flutter与React Native的差距正变得越来越小,其在一年内市场份额从30%急剧跃升至39%。




image.png


语言对比:Dart Vs JavaScript


Flutter所采用的Dart开发语言是谷歌2011年在丹麦奥尔胡斯举行的GOTO大会上亮相的,Dart是一门面向对象的、类定义的、单继承的语言,它的语法类似C语言,可以转译为JavaScript,支持接口(interfaces)、混入(mixins)、抽象类(abstract classes)、具体化泛型(reified generics)、可选类型(optional typing)和sound type system,并且具有AOT与JIT编译器,Dart的最大优势在于速度,运行比JavaScript快2倍,不过Dart作为一门较新的语言,开发者还需要熟悉Java或C++的应用程序开发工作才更易上手。


而React Native则采用的为已经在IT行业广泛应用多年的Javascript语言,类似于HTML的JSX,以及CSS来开发移动应用,因此熟悉Web前端开发的技术人员只需很少的学习就可以进入移动应用开发领域,不过JavaScript线程需要时间来初始化,所以导致React Native在最初渲染之前需要花费大量时间来初始化运行,不过React Native已经发布了升级线路,并且会在最近开源升级的版本,相信随着React Native新版本的发布,性能上将会追平Flutter。


技术架构


如果单从技术上讲,Flutter绝对是一个先进的跨平台技术方案,它提供了一个分层的架构,以确保高度的定制化,而React Native依赖于其他软件来构建反应组件,并使用JavaScriptBridge来桥接原生本地模块的连接。桥接会影响性能,即使发生轻微的变化,而Flutter可以在没有桥接的情况下管理一切。


Flutter提供的分层的架构,为简单快速的UI定制铺平了道路。它被认为可以让你完全控制屏幕上的每一个像素,并允许移动应用开发公司整合叠加和动画图形、文本、视频和控件,没有任何限制。


Flutter移动平台与其他Web平台的架构略有差异,不同平台相同的公共部分就是Dart部分,即Dart Framework。Flutter的公共部分主要实现了两个逻辑:第一,开发人员可以通过Flutter Ui系统编写UI,第二使用Dart虚拟机及Dart语言可以编写跟平台资源无关的逻辑。同时这也是Flutter跨平台的核心,和Java程序可以在Linux,Window,MacOs同时运行, Web程序可以在任意平台运行类似。通过Dart虚拟机,UI及和系统无光的逻辑都可以用Dart语言编写,运行在Dart虚拟机中,是跨平台的。


而React Native依赖于其他软件来构建反应组件,其架构整体上分为三大块:Native、JavaScript 与 Bridge,其中Native 管理UI 更新及交互,JavaScript 调用 Native 能力实现业务功能,Bridge 在二者之间传递消息。React Native 中主要有 3 个线程,应用中的主线程UI Thread、进行布局计算和构造 UI 界面的线程Shadow Thread与React 等 JavaScript 代码都在这个线程执行任务的JS Thread。


正因其依赖于其他软件来构建反应组件,因此在启动上会受到以下,必须先初始化 React Native 运行时环境(即Bridge),Bridge 准备好之后开始 run JS,最后开始 Native 渲染。从架构上来看,Flutter确实性能更高,也更符合当下跨平台开发的需求。


image.png


学习成本和社区支持


当涉及到构建企业应用程序时,社区支持是必须检查的因素。而React Native和Flutter都在行业中发展了多年,并且在谷歌与Facebook两大巨头的支持下都有最新的技术更新与广泛的社区支持。而随着每一个递增的版本和技术更新,社区对该框架的兴趣和需求逐渐增加。让我们了解一下这两个框架在社区参与方面的情况。


React Native在2015年推出,其社区一直处于成长阶段,Github上对该框架的贡献者数量就是证明。但是,尽管Flutter还很年轻,也比较新,但它正在已开始显示后来居上之势。


image.png


代码的可维护性


无论你开发的应用程序多么出色,为了使其顺利运行,不断地升级和调试是必要的。与Flutter相比,用React Native维护代码真的很困难。


在React Native中,当你为了开发适配不同系统的应用程序时就需要分开编写适配代码,它会干扰框架的逻辑,从而减慢了开发过程。另外,在React Native应用程序中,大多数本地组件都有一个第三方库的依赖性,所以维护这些过时的库确实是一个具有挑战性的任务。


对于Flutter来说,由于代码逻辑相对简单,不需要适配不同的操作系统,维护代码就要容易得多,允许移动应用程序开发人员轻松发现问题,为外部工具和支持第三方库提供数据支撑。


此外,与使用React Native的热重新加载功能相比,在Flutter中发布质量更新和对应用程序进行即时更改所花费的时间也比React Native表现更好。


开发成本


无论是一个初创公司还是一个先进的互联网企业,开发成本总是大家比较关心的内容。因此,当你选择雇用反应原生开发公司或Flutter应用程序工程师时,你可能需要评估他们的费率,不同的地方有不同的开发成本。


因此,在正式启动项目之前,无论是Flutter还是React Native,都需要考虑开发人员的素质,如经验、专业知识、项目处理等开发成本问题,以评估开发人员的实际小时费用,下面是Flutter和React Native的一个开发成本的问题。


image.png


除此之外,在选择Flutter还是React Native的问题上,我们还需要考虑他们的自定义开发能力。
Flutter和React Native都有一套属于自己的UI组件和小工具。并且,Flutter就以其漂亮的UI原生型小部件而闻名,这些小部件由框架的图形引擎进行渲染和管理。


而React Native只提供了适应平台的基本工具,如按钮、滑块、加载指示灯等基础组件,如果需要开发复杂的功能,就需要使用第三方组组件。


作者:Coolbreeze
链接:https://juejin.cn/post/7006283924473118756
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

当 Adapter 遇上 Kotlin DSL,无比简单的调用方式

早在去年的时候我就提到过使用工厂的方式获取 Adapter 而不是为每个 Adapter 定义一个类文件。这样的好处是,对于不是那么复杂的 Adapter 可以节省大量的代码,提升开发效率和解放双手,同时更好的支持多类型布局效果。 1、Kotlin DSL 和...
继续阅读 »

早在去年的时候我就提到过使用工厂的方式获取 Adapter 而不是为每个 Adapter 定义一个类文件。这样的好处是,对于不是那么复杂的 Adapter 可以节省大量的代码,提升开发效率和解放双手,同时更好的支持多类型布局效果。


1、Kotlin DSL 和 Adapter 工厂方法


可以把 Kotlin DSL 当作构建者使用。这里有一篇不错的文章,想了解的可以阅读下,



http://www.ximedes.com/2020-04-21/…



Kotlin DSL 是拓展函数的延申,比如我们常用的 with 等函数就是函数的拓展,


public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}

这里是泛型 T 的拓展。这里的 T 可以类比到 Java 构建者模式中的 Builder,通过方法接收外部参数之后调用 build() 方法创建一个最终的对象即可。


对于 Adapter 工厂方法,之前我是通过如下方式使用的,


fun <T> getAdapter(
@LayoutRes itemLayout:Int,
converter: (helper: BaseViewHolder, item: T) -> Unit,
data: List<T>
): Adapter<T> = Adapter(itemLayout, converter, data)

class Adapter<T>(
@LayoutRes private val layout: Int,
private val converter: (helper: BaseViewHolder, item: T) -> Unit,
val list: List<T>
): BaseQuickAdapter<T, BaseViewHolder>(layout, list) {
override fun convert(helper: BaseViewHolder, item: T) {
converter(helper, item)
}
}

也就是每次想要得到 Adapter 的时候只要调用 getAdapter() 方法即可。这种封装方式比较简陋,支持的功能有限。后来慢慢采用了 Kotlin DSL 之后,我封装了 Kotlin DSL 风格的工厂方法。采用 Kotlin DSL 风格之后更加优雅和方便快捷,同时更好的支持多类型布局效果。


2、使用


2.1 引入依赖


首先,该项目依赖于 BRVAH,所以,你需要引入该库之后才可以使用。BRVAH 可以说是目前开源的最好用的 Adapter,我们没必要再另起炉灶自己再造轮子。这个框架设计最好地方在于通过 SpareArray 收集了 ViewHolder 控件,从而避免了自定义 ViewHolder,这是我们框架设计的基础思想。


该项目已经上传到了 MavenCentral,你需要先在项目中引入该仓库,


allprojects {
repositories {
mavenCentral()
}
}

然后在项目中添加如下依赖,


implementation "com.github.Shouheng88:xadapter:${latest_version}"

2.2 使用 Adapter 工厂方法


使用 xAdapter 之后,当你需要定义一个 Adapter 的时候,你无需单独创建一个类文件,只需要通过 createAdapter() 方法获取一个 Adapter,


adapter = createAdapter {
withType(Item::class.java, R.layout.item_eyepetizer_home) {
// Bind data with viewholder.
onBind { helper, item ->
helper.setText(R.id.tv_title, item.data.title)
helper.setText(R.id.tv_sub_title, item.data.author?.name + " | " + item.data.category)
helper.loadCover(requireContext(), R.id.iv_cover, item.data.cover?.homepage, R.drawable.recommend_summary_card_bg_unlike)
helper.loadRoundImage(requireContext(), R.id.iv_author, item.data.author?.icon, R.mipmap.eyepetizer, 20f.dp2px())
}
// Item level click and long click events.
onItemClick { _, _, position ->
adapter?.getItem(position)?.let {
toast("Clicked item: " + it.data.title)
}
}
}
}

在这种新的调用方式中,你需要通过 withType() 方法指定数据类型及其对应的布局文件,然后在 onBind() 方法中即可实现数据到 ViewHolder 的绑定操作。这里的 onBind() 方法的使用与 BRVAH 中的 convert() 方法使用一致,可以通过阅读该库了解如何使用。总之,xAapter 在 BRVAH 的基础上做了二次封装,可以说,比简单更简单。


xAdapter 支持为每个 ViewHolder 绑定点击和长按事件,同时也支持为 ViewHolder 上的某个单独的 View 添加点击和长按事件。使用方式如上所示,只需要添加 onItemClick() 方法并实现自己的逻辑即可。其他的点击事件可以参考项目的示例代码。


效果,





2.3 使用多类型 Adapter


多类型 Adapter 的使用方式非常简单,类似于上面的调用方式,只需要在 createAdapter() 内再添加一个 withType() 方法即可。下面是一个写起来可能相当复杂的 Adapter,但是采用了 xAdpater 的调用方式之后,一切变得非常简单,


private fun createAdapter() {
adapter = createAdapter {
withType(MultiTypeDataGridStyle::class.java, R.layout.item_list) {
onBind { helper, item ->
val rv = helper.getView<RecyclerView>(R.id.rv)
rv.layoutManager = GridLayoutManager(context, 3)
val adapter = createSubAdapter(R.layout.item_home_page_data_module_1, 1)
rv.adapter = adapter
adapter.setNewData(item.items)
}
}
withType(MultiTypeDataListStyle1::class.java, R.layout.item_home_page_data_module_2) {
onBind { helper, item ->
converter.invoke(helper, item)
}
onItemClick { _, _, position ->
(adapter?.getItem(position) as? MultiTypeDataListStyle1)?.let {
toast("Clicked style[2] item: " + it.item.data.title)
}
}
}
withType(MultiTypeDataListStyle2::class.java, R.layout.item_list) {
onBind { helper, item ->
val rv = helper.getView<RecyclerView>(R.id.rv)
rv.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
val adapter = createSubAdapter(R.layout.item_home_page_data_module_4, 3)
rv.adapter = adapter
adapter.setNewData(item.items)
}
}
withType(MultiTypeDataListStyle3::class.java, R.layout.item_home_page_data_module_3) {
onBind { helper, item ->
converter.invoke(helper, item)
}
onItemClick { _, _, position ->
(adapter?.getItem(position) as? MultiTypeDataListStyle3)?.let {
toast("Clicked style[4] item: " + it.item.data.title)
}
}
}
}
}

xAdapter 对多类型布局方式的支持是在 BRVAH 之上进行的改造,在这种封装方式中,数据类无需实现任何类和接口。Adpater 内部通过 Class 区分各个 ViewHolder.


效果,





总结


相对于为各种类型的数据定义 Adapter 的使用方式,以上封装方式的优势是:



  1. 借助 BRVAH 的优势,封装了大量的方法,进一步简化了 Adapter 的使用;

  2. 通过工厂和 DSL 封装,简化了调用 Adapter 的方式,你无需为数据类型定义 Adapter 文件,减少了项目中需要维护的代码和类文件数量;

  3. 通过以上封装,使用 Adapter 更加简洁,节省了大量的代码,提升开发效率和解放双手;

  4. 自由地在单一类型布局和多类型布局之间进行切换,但是少了没必要的工厂方法。


当有更加简洁的使用方式的时候,继续采用复杂的调用方式无异于抱残守缺,对于程序员而言,做这种重复而没有太大价值的工作,付出再多的汗水都不值得同情。以上是部分功能和代码的展示,可以通过阅读源码了解更多。后续我参考其他优秀的库的设计思想,支持更多 Adapter 特性的封装来实现快速调用。


项目已开源,感兴趣的可以直接阅读项目源码,源码地址:github.com/Shouheng88/…


作者:shouheng
链接:https://juejin.cn/post/7006530777995296782
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Hook AMS + APT实现集中式登录框架

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:[juejin.cn/post/700695…) 1, 背景 登录功能是App开发中一个很常见的功能,一般存在两种登录方式: 一种是进入应用...
继续阅读 »

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:[juejin.cn/post/700695…)


1, 背景


登录功能是App开发中一个很常见的功能,一般存在两种登录方式:




  • 一种是进入应用就必须先登录才能使用(如聊天类软件)




  • 另一种是以游客身份使用,需要登录的时候才会去登录(如商城类软件)




针对第二种的登录方式,一般都是在要跳转到需要登录才能访问的页面(以下简称目标页面)时通过if-else判断是否已登录,未登录则跳转到登录界面,登录成功后退回到原界面,用户继续进行操作。伪代码如下:


if (需要登录) {
// 跳转到登录页面
} else {
// 跳转到目标页面
}

这中方式存在着以下几方面问题:



  1. 当项目功能逐渐庞大以后,存在大量重复的用于判断登录的代码,且判断逻辑可能分布在不同模块,维护成本很高。

  2. 增加或删除目标页面时需要修改判断逻辑,存在耦合。

  3. 跳转到登录页面,登录成功后只能退回到原界面,用户原本的意图被打断,需要再次点击才能进入目标界面(如:用户在个人中心界面点击“我的订单”按钮想要跳转到订单界面,由于没有登录就跳转到了登录界面,登录成功后返回个人中心界面,用户需要再次点击“我的订单”按钮才能进入订单界面)。


大致流程如下图所示:


login.png


针对传统登录方案存在的问题本文提出了一种通过Hook AMS + APT实现集中式登录方案。




  1. 首先通过Hook AMS实现集中处理判断,实现了跟业务逻辑解耦。




  2. 通过注解标记需要登录的页面,然后通过APT生成需要登录页面的集合,便于Hook中的判断。




  3. 最后在Hook AMS时将原意图放入登录页面的意图中,登录页面登录成功后可以获取到原意图,实现了继续用户原意图的目的。




本方案能达到的业务流程如下:


hook_login.png


1, 集中处理


这里借鉴插件化的思路通过Hook AMS实现拦截并统一处理的目的


1.1 分析Activity启动过程

了解Activity启动过程的应该都知道Activity中的startActivity()最终会进入Instrumentation


// Activity.java
@Override
public void startActivityForResult(
String who, Intent intent, int requestCode, @Nullable Bundle options) {
...
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, who,
intent, requestCode, options);
...
}

InstrumentationexecStartActivity代码如下:


public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, String target,
Intent intent, int requestCode, Bundle options) {
...
try {
...
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target, requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}

其中调用了ActivityManagerNative.getDefault()startActivity(),那么此处getDefault()获取到的是什么?接着看代码:


/**
* Retrieve the system's default/global activity manager.
*/
static public IActivityManager getDefault() {
// step 1
return gDefault.get();
}

// step 2
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
// step 5
IBinder b = ServiceManager.getService("activity");
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
};

public abstract class Singleton<T> {
private T mInstance;

protected abstract T create();

// step 3
public final T get() {
synchronized (this) {
if (mInstance == null) {
// step 4
mInstance = create();
}
return mInstance;
}
}
}

gDefault是一个Singleton<IActivityManager>类型的静态常量,它的get()方法返回的是Singleton类中的private T mInstance;,这个mInstance的创建又是在gDefault实例化时通过create()方法实现。


这里代码有点绕,根据上面代码注释的step1 ~ 5,应该能理清楚:gDefault.get()获取到的mInstance实例就是ActivityManagerService(AMS)实例。


由于gDefault是一个静态常量,因此可以通过反射获取到它的实例,同时它是Singleton类型的,因此可以获取到其中的mInstance


到这里你应该能明白接下来要干什么了吧,没错就是Hook AMS。


1.2 Hook AMS


本文以android 6.0代码为例。注:8.0以下实现方式是相同的,8.0和9.0实现相同,10.0到12.0方式是一样的。


这里涉及到反射及动态代理的姿势,请自行了解。


1,获取gDefault实例


Class<?> activityManagerNative = Class.forName("android.app.ActivityManagerNative");
Field singletonField = activityManagerNative.getDeclaredField("gDefault");
singletonField.setAccessible(true);
// 获取gDefault实例
Object singleton = singletonField.get(null);

2,获取Singleton中的mInstance


Class<?> singletonClass = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
/* Object mInstance = mInstanceField.get(singleton); */
Method getMethod = singletonClass.getDeclaredMethod("get");
Object mInstance = getMethod.invoke(singleton);

这里本可以直接通过mInstanceField及第一步中获取的gDefault实例反射得到mInstance实例,但是实测发现在Android 10以上无法获取,不过还好可以通过Singleton中的get()方法可以获取到其实例。


3,获取要动态代理的Interface


Class<?> iActivityManagerClass = Class.forName("android.app.IActivityManager");

4,创建一个代理对象


Object proxyInstance = Proxy.newProxyInstance(context.getClassLoader(), new Class[]{iActivityManagerClass},
(proxy, method, args) -> {
if (method.getName().equals("startActivity") && !isLogin()) {
// 拦截逻辑
}
return method.invoke(mInstance, args);
});

5,用代理对象替换原mInstance对象


mInstanceField.set(singleton, proxyInstance);

6,兼容性


针对8.0以下,8.0到9.0,10.0到12.0进行适配,可以兼容各个系统版本。


至此已经实现了对AMS的Hook,只需要在代理中判断当前要启动的Activity是否需要登录,然后跳转到登录即可。


但是此时出现了一个问题,这里如何判断哪些Activity需要登录的?最简单的方式就是写死,如下:


// 获取要启动的Activity的全类名。
String intentName = xxx
if (intentName.equals("aaaActivity")
|| intentName.equals("bbbActivity")
...
|| intentName.equals("xxxActivity")){
// 去登陆
}

这样的代码存在着耦合,添加删除目标Activity都需要改这里。


接下来就是通过APT实现解耦的方案。


2, APT实现解耦


APT就不多说了,就是注解处理器,很多流行框架都在用它,如果你不了解请自行了解。


首先定义注解,然后给目标Activity加上注解就相当于打了个标记,接着通过APT找到打了这些标记的Activity,将其全类名保存起来,最后在需要使用的地方通过反射调用即可。


2.1,定义注解


// 目标页面注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface RequireLogin {
// 需要登录的Activity加上该注解
}

// 登录页面注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginActivity {
// 给登录页面加上该注解,方便在Hook中直接调用
}

// 判断是否登录方法的注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface JudgeLogin {
// 给判断是否登录的方法添加注解,需要是静态方法。
}

2.2,注解处理器


这里就不贴代码了,重点是思路:


1,获取所有添加了RequireLogin注解的Activity,存入一个集合中


2,通过JavaPoet创建一个Class


3,在其中添加方法,返回1中集合里Activity的全类名的List


最终通过APT生成的类文件如下:


package me.wsj.login.apt;

public class AndLoginUtils {
// 需要登录的Activity的全类名集合
public static List<String> getNeedLoginList() {
List<String> result = new ArrayList<>();
result.add("me.wsj.andlogin.activity.TargetActivity1");
result.add("me.wsj.andlogin.activity.TargetActivity2");
return result;
}

// 登录Activity的全类名
public static String getLoginActivity() {
return "me.wsj.andlogin.activity.LoginActivity";
}

// 判断是否登录的方法全类名
public static String getJudgeLoginMethod() {
return "me.wsj.andlogin.activity.LoginActivity#checkLogin";
}
}

2.3,反射调用


在动态代理的InvocationHandler中通过反射获取


new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("startActivity") && !isLogin()) {
// 目标Activity全类名
String intentName = xxx;
if (isRequireLogin(intentName)) {
// 该Activity需要登录,跳转到登录页面
}
}
return null;
}
}

/**
* 该activity是否需要登录
*
* @param activityName
* @return
*/
private static boolean isRequireLogin(String activityName) {
if (requireLoginNames.size() == 0) {
// 反射调用apt生成的方法
try {
Class<?> NeedLoginClazz = Class.forName(UTILS_PATH);
Method getNeedLoginListMethod = NeedLoginClazz.getDeclaredMethod("getRequireLoginList");
getNeedLoginListMethod.setAccessible(true);
requireLoginNames.addAll((List<String>) getNeedLoginListMethod.invoke(null));
Log.d("HootUtil", "size" + requireLoginNames.size());
} catch (Exception e) {
e.printStackTrace();
}
}
return requireLoginNames.contains(activityName);
}

2.4,其他


实现了判断目标页面的解耦,同样的方式也可以实现跳转登录及判断是否登录的解耦。


1,跳转登录页面


前面定义了LoginActivity()注解,APT也生成了getLoginActivity()方法,那就可以反射获取到配置的登录Activity,然后创建新的Intent,替换掉原Intent,进而实现跳转到登录页面。


if (需要跳转到登录) {
Intent intent = new Intent(context, getLoginActivity());
// 然后需要将该intent替换掉原intent接口
}

/**
* 获取登录activity
*
* @return
*/
private static Class<?> getLoginActivity() {
if (loginActivityClazz == null) {
try {
Class<?> NeedLoginClazz = Class.forName(UTILS_PATH);
Method getLoginActivityMethod = NeedLoginClazz.getDeclaredMethod("getLoginActivity");
getLoginActivityMethod.setAccessible(true);
String loginActivity = (String) getLoginActivityMethod.invoke(null);
loginActivityClazz = Class.forName(loginActivity);
} catch (Exception e) {
e.printStackTrace();
}
}
return loginActivityClazz;
}

2,判断是否登录


同理为了实现对判断是否登录的解耦,在判断是否能登录的方法上添加一个JudgeLogin注解,就可以在Hook中反射调用判断。当然这里也可以通过添加回调的方式实现。


2.5,小结


通过APT实现了对判断是否登录、判断哪些页面需要登录及跳转登录的解耦。


此时面临着最后一个问题,虽然前面已经实现了拦截并跳转到了登录页面,但是登录完成后再返回到原页面看似合理,实则不XXXX(词穷了,自行脑补😂),用户的意图被打断了。


接着就看看如何在登录成功后继续用户意图。


3, 继续用户意图


由于Intent实现了Parcelable接口,因此可以将它作为一个Intent的Extra参数传递。在Hook过程中可以获取原始Intent,因此只需在Hook中将用户的原始意图Intent作为一个附加参数存入跳转登录的Intent中,然后在登录页面获取到这个参数,登录成功后跳转到这个原始Intent即可。


1,传递原始意图


在动态代理中先拿到原始Intent,然后将它作为参数存入新的Intent中


new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("startActivity") && !isLogin()) {
// 目标Activity全类名
Intent originIntent = xxx;
String intentName = xxx;
if (isRequireLogin(intentName)) {
// 该Activity需要登录,跳转到登录页面
Intent intent = new Intent(context, getLoginActivity());
intent.putExtra(Constant.Hook_AMS_EXTRA_NAME, originIntent);
// 然后替换原Intent
...
}
}
return null;
}
}

2,获取原始意图并跳转


在登录页面,登录成功后判断其intent中是否有特定键值的附加数据,如果有则直接用它作为意图启动新页面,实现了继续用户意图的目的;


@LoginActivity
class LoginActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

...
binding.btnLogin.setOnClickListener {
// 登录成功了
var targetIntent = intent.getParcelableExtra<Intent>(AndLogin.TARGET_ACTIVITY_NAME)
// 如果存在targetIntent则启动目标intent
if (targetIntent != null) {
startActivity(targetIntent)
}
finish()
}
}

companion object {
// 该方法用于返回是否登录
@JudgeLogin
@JvmStatic
fun checkLogin(): Boolean {
return SpUtil.isLogin()
}
}
}

如上所示,如果可以在当前Intent中获取到Hook时保存的数据,则说明存在目标Intent,只需将其启动即可。


看一下最终效果:


preview.gif


4, ARouter方案


熟悉ARouter的都知道,它有一个拦截器的东西,可以在跳转前做拦截操作。如下:


@Interceptor(name = "login", priority = 1)
public class LoginInterceptorImpl implements IInterceptor {
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
...
if (isLogin) { // 已经登录不拦截
callback.onContinue(postcard);
} else { // 未登录则拦截
// callback.onInterrupt(null);
}
}

@Override
public void init(Context context) {
}
}

实现IInterceptor接口并添加Interceptor注解即可在路由跳转时实现拦截。


了解其原理的话可知:ARouter也只是在启动Activity前提供了拦截判断的时机,相当于本方案的第一步(Hook AMS)操作,后续实现解耦以及继续用户意图操作还需要自己实现。


5, 总结


本文提出了一种通过Hook AMS + APT实现集中式登录的方案,对比传统方式本方案存在以下优势:




  1. 以非侵入性的方式将分散的登录判断逻辑集中处理,减少了代码量,提高了开发效率。




  2. 增加或删除目标页面时无需修改判断逻辑,只需增加或删除其对应注解即可,符合开闭原则,降低了耦合度




  3. 在用户登录成功后直接跳转到目标界面,保证了用户操作不被中断。




本方案并没有太高深的东西,只是把常用的东西整合在一起,综合运用了一下。另外方案只是针对需要跳转页面的情况,对于判断是否登录后做其他操作的,比如弹出一个Toast这样的操作,可以通过AspectJ等来实现。


项目地址:github.com/wdsqjq/AndL…


最后,本方案提供了远程依赖,使用startup实现了无侵入初始化,使用方式如下:


1,添加依赖


allprojects {
repositories {
maven { url 'https://www.jitpack.io' }
}
}


dependencies {
implementation 'com.github.wdsqjq.AndLogin:lib:1.0.0'
kapt 'com.github.wdsqjq.AndLogin:apt_processor:1.0.0'
}

2,给需要登录的Activity添加注解


@RequireLogin
class TargetActivity1 : AppCompatActivity() {
...
}

@RequireLogin
class TargetActivity2 : AppCompatActivity() {
...
}


3,给登录Activity添加注解


@LoginActivity
class LoginActivity : AppCompatActivity() {
...
}

4,提供判断是否登录的方法


需要是一个静态方法


@LoginActivity
class LoginActivity : AppCompatActivity() {

companion object {
// 该方法用于返回是否登录
@JudgeLogin
@JvmStatic
fun checkLogin(): Boolean {
return SpUtil.isLogin()
}
}
}

作者:giswangsj
链接:https://juejin.cn/post/7006951885089292296
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Bitmap和Drawable

Bitmap:图片信息的存储工具,保存每一个像素是什么颜色image: width:640 height:400 pixel:ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000f...
继续阅读 »
  • Bitmap:图片信息的存储工具,保存每一个像素是什么颜色

image: width:640 height:400 pixel:ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000ff0000

  • Drawable是什么(Drawable在代码上是接口,BitmapDrawable、ColorDrawable等实现)?

drawable是绘制工具,重写draw进行绘制

  • view和drawable?
  1. 都是使用canvas进行绘制。
  2. drawable纯绘制工具。
  3. view会包含测量、布局、绘制。
  4. drawable绘制的时候一定要设置边界setBounds
class DrawableViewcontextContextattrAttributeSet):Viewcontextattr){
private val drawable = ColorDrawable(Color.RED)
override fun onDraw(canvas:Canvas){
super.onDraw(canvas)
drawable.setBounds(0,0,width,height)
drawable.draw(canvas)
}
}
  • Bitmap和Drawable怎么互转?(其实不是互转,是使用一个实例创建了另外一个实例)
  1. Bitmap转Drawable

java

Drawable d = new BitmapDrawable(getResource(),bitmap);

kotlin(ktx)

bitmap.toDrawable(resource)
  1. Drawable转Bitmap

java

public static Bitmap drawableToBitmap(Drawable drawable){
Bitmap bitmap = null;
if(drawable instance BitmapDrawable){//1、如果是BitmapDrawable
BitmapDrawable bitmapDrawable = (BitmapDrawable)drawable;
if(bitmapDrawable.getBitmap()!=null){
return bitmapDrawable.getBitmap();
}
}
//2、如果drawable的宽高小于等于0
if(drawable.getIntrinsicWidth()<=0||drawable.getIntrinsicHeight()<=0){
bitmap = Bitmap.createBitmap(1,1,Bitmap.ARGB_8888);
}else{
bitmap =Bitmap.createBitmap(drawable.getIntrinsicWidth(),drawable.getIntrinsicHeight(),
Bitmap.Config.ARGB_8888)
}
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0,0,canvas.getWidth(),canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}

kotlin(ktx)

drawable.toBitmap()
  • 自定义Drawable(作用:一个自定义drawable可以把多个view中的重复代码提出来,在多个view之间进行复用)
//画一个网眼
private val INTERVAL = 50.dp
class MeshDrawable:Drawable(){
private val paint = Paint(Paint.ANT_ALIAS_FLAG)
override fun draw(canvas:Canvas){
var x = bounds.lef.toFloat()
while(x<=bounds.right.toFloat()){
canvas.drawLine(x,bound.top.toFloat(),
x,bounds.bottom.toFloat(),paint)
x+=INTERVAL
}
var y = bounds.top.toFloat()
while(y<=bounds.bottom.toFloat()){
canvas.drawLine(bounds.left.toFloat(),y,
bounds.right.toFloat(),y,paint)
y+=INTERVAL
}
}
override fun setAlpha(alpha:Int){
paint.alpha = alpha
}
override fun getAlpha():Int{
return paint.alpha
}
override fun getOpacity():Int{//不透明度
return when(paint.alpha){
0->PixelFormat.TRANSPARENT
0xff ->PixelFormat.OPAQUE
else ->PixelFormat.TRANSLUCENT
}
}
override fun setColorFilter(colorFilter:ColorFilter?){
paint.colorFilter = colorFilter
}
override fun getColorFilter():ColorFilter{
return paint.colorFilter
}
}

getWidth() 是实际显示的宽度。

getMeasureWidth() 是测量宽度,在布局之前计算出来的。

getIntrinsicWidth() 是原有宽度,有时候原有宽度可能很大,但是实际上空间不够,所有效果上并没有那么大,这个方法可以获得原有宽度,可以辅助测量的时候选择合适的展示宽度。

getMinimumWidth() 是最小宽度,是XML参数定义里的 minWidth,也是一个辅助测量展示的参数。

收起阅读 »

Android高德地图踩坑记录-内存泄漏问题

1、问题现象最近做项目优化,在查找app可能存在的内存泄漏地方,项目中有用到高德地图SDK,有一个页面有展示地图,每次退出该页面的时候,LeakCanary老是提示有内存泄漏,泄漏的大概信息如下:2、排查问题看样子像是高德地图相关的内存泄漏,不过为了进一步可以...
继续阅读 »

1、问题现象

最近做项目优化,在查找app可能存在的内存泄漏地方,项目中有用到高德地图SDK,有一个页面有展示地图,每次退出该页面的时候,LeakCanary老是提示有内存泄漏,泄漏的大概信息如下:

image.png

2、排查问题

看样子像是高德地图相关的内存泄漏,不过为了进一步可以定位到问题,通常可以采用一种虽然有些笨但是可以定位到问题点的方法:控制变量法,排除到不太可能出现问题的地方,只保留可能出现的问题,具体是先注释掉和高德地图无关的代码,然后复现问题,确保问题是出在和高德地图相关的代码上

经过一系列的注释代码然后复现操作,明确内存泄漏的点是在高德地图相关的操作上,通过分析LeakCanary生成的Heap Dump(堆转储)文件,也验证了这个猜想

image.png

我在代码里有封装过一个关于地图操作的utils类,刚开始以为是在页面销毁的时候,这个utils类里有一些资源没有释放,比如当前Activity的context引用,在改为Application引用之后,发现问题还是有,然后在Activity销毁的时候,对utils里的一些资源进行了释放,发现还是不可以

后来经过在网上查找资料,查看高德地图官方demo,发现一个细节有可能是使用Butterknife的问题

image.png

因为在onDestroy方法里,我有写MapView的销毁方法,但是没有进入到if语句里面

image.png

3、问题解决方式

不使用ButterKnife的方式获取MapView控件,采用原生的findViewById的方式来获取控件对象

image.png

image.png

经过反复测试,退出页面之后,LeakCanary没有报内存泄漏的吐司

4、总结

使用高德地图SDK,地图控件MapView,使用原生的findViewById的方式来获取

收起阅读 »

如何打造一款权限请求框架

原理通过向当前Activity添加一个不可见的Fragment,从而实现权限申请流程的封装。实现不可见的Fragmentinternal class EPermissionFragment : Fragment() { private var mCal...
继续阅读 »

原理

通过向当前Activity添加一个不可见的Fragment,从而实现权限申请流程的封装。

实现

不可见的Fragment

internal class EPermissionFragment : Fragment() {
private var mCallback: EPermissionCallback? = null

fun requestPermission(callback: EPermissionCallback, vararg permissions: String) {
mCallback = callback
// 申请权限
requestPermissions(permissions, CODE_REQUEST_PERMISSION)
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (requestCode == CODE_REQUEST_PERMISSION) {
val deniedList = ArrayList<String>()
val deniedForeverList = ArrayList<String>()
grantResults.forEachIndexed { index, result ->
// 提取权限申请结果
if (result != PackageManager.PERMISSION_GRANTED) {
val permission = permissions[index]
deniedList.add(permission)
// 是否拒绝且不再显示
if (!shouldShowRequestPermissionRationale(permission)) {
deniedForeverList.add(permission)
}
}
}
if (deniedList.isEmpty()) mCallback?.onAllGranted()
if (deniedList.isNotEmpty()) mCallback?.onDenied(deniedList)
if (deniedForeverList.isNotEmpty()) mCallback?.onDeniedForever(deniedForeverList)
}
}

override fun onDestroy() {
mCallback = null
super.onDestroy()
}
}

封装权限申请

// 扩展FragmentActivity
fun FragmentActivity.runWithPermissions(
vararg permissions: String,
onDenied: (ArrayList<String>) -> Unit = { _ -> },
onDeniedForever: (ArrayList<String>) -> Unit = { _ -> },
onAllGranted: () -> Unit = {}
) {
if (checkPermissions(*permissions)) {
onAllGranted()
return
}
// 添加一个不可见的Fragment
val isFragmentExist = supportFragmentManager.findFragmentByTag(EPermissionFragment.TAG)
val fragment = if (isFragmentExist != null) {
isFragmentExist as EPermissionFragment
} else {
val invisibleFragment = EPermissionFragment()
supportFragmentManager.beginTransaction().add(invisibleFragment, EPermissionFragment.TAG).commitNowAllowingStateLoss()
invisibleFragment
}
val callback = object : EPermissionCallback {
override fun onAllGranted() {
onAllGranted()
}

override fun onDenied(deniedList: ArrayList<String>) {
onDenied(deniedList)
}

override fun onDeniedForever(deniedForeverList: ArrayList<String>) {
onDeniedForever(deniedForeverList)
}
}
// 申请权限
fragment.requestPermission(callback, *permissions)
}

使用方法

项目build.gradle添加

allprojects {
repositories {
...
maven { url 'https://www.jitpack.io' }
}
}

模块build.gradle添加

dependencies {
implementation 'com.github.RickyHal:EPermission:$latest_version'
}

在Activity或者Fragment中直接调用

// 申请存储权限
runWithPermissions(
*EPermissions.STORAGE,
onDenied = {
Toast.makeText(this, "STORAGE permission denied", Toast.LENGTH_SHORT).show()
},
onDeniedForever = {
Toast.makeText(this, "STORAGE permission denied forever", Toast.LENGTH_SHORT).show()
},
onAllGranted = {
Toast.makeText(this, "STORAGE permission granted", Toast.LENGTH_SHORT).show()
}
)

也可以用下面这个简单的方法

runWithStoragePermission(onFailed = {
Toast.makeText(this, "SMS permission denied", Toast.LENGTH_SHORT).show()
}) {
Toast.makeText(this, "SMS permission granted", Toast.LENGTH_SHORT).show()
}

一次申请多个权限

runWithPermissions(*EPermissions.CAMERA, *EPermissions.STORAGE,
onDenied = { deniedList ->
Toast.makeText(this, "permission denied $deniedList", Toast.LENGTH_SHORT).show()
},
onDeniedForever = { deniedForeverList ->
Toast.makeText(this, "permission denied forever $deniedForeverList", Toast.LENGTH_SHORT).show()
},
onAllGranted = {
Toast.makeText(this, "Permission all granted", Toast.LENGTH_SHORT).show()
})

如果不需要处理失申请权限败的情况,也可以直接这样写

runWithStoragePermission {
Toast.makeText(this, "SMS permission granted", Toast.LENGTH_SHORT).show()
}

如果某些操作执行的时候,只能有权限才去执行,则可以使用下面的方法

doWhenPermissionGranted(*EPermissions.CAMERA){
Toast.makeText(this, "Do this when camera Permission is granted", Toast.LENGTH_SHORT).show()
}

检查权限

if (checkPermissions(*EPermissions.CAMERA)) {
Toast.makeText(this, "Camera Permission is granted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Camera Permission is not granted", Toast.LENGTH_SHORT).show()
}

收起阅读 »

Dart 2.14 发布,新增语言特性和共享标准 lint

Dart 2.14 的发布对 Apple Silicon 处理器提供了更好的支持,并新增了更多提升生产力的功能,例如通过代码样式分析捕获 lint 错误、更快的发布工具、更好的级联代码格式以及一些细小的语言特性更新。 Dart SDK 对 Apple Sili...
继续阅读 »

Dart 2.14 的发布对 Apple Silicon 处理器提供了更好的支持,并新增了更多提升生产力的功能,例如通过代码样式分析捕获 lint 错误、更快的发布工具、更好的级联代码格式以及一些细小的语言特性更新。


Dart SDK 对 Apple Silicon 支持


自从在 2020 年末 Apple 发布了新的 Apple Silicon 处理器以来, Dart SDK 一直致力于增加对该处理器上的 Native 执行支持。


现在从 Dart 2.14.1 正式增加了对 Apple Silicon 的支持,当 下载 MacOS 的 Dart SDK时,一定要选择 ARM64 选项,这里需要额外注意, Flutter SDK 中的 Dart SDK 还没有绑定这一项改进


本次更新支持在 Apple Silicon 上运行 SDK/Dart VM 本身,以及对 dart compile 编译后的可执行文件在 Apple Silicon 上运行的支持,由于 Dart 命令行工具使用原生 Apple Silicon ,因此它们的启动速度会快得多


Dart 和 Flutter 共享的标准 lint


开发人员通常会需要他们的代码遵循某种风格,其中许多规则不仅仅是风格偏好(如众所周知的制表符与空格的问题),还涵盖了可能导致错误或引入错误的编码风格。


比如 Dart 风格指南要求对所有控制流结构使用花括号,例如 if-else 语句,这可以防止经典的 dangling else 问题,也就是在多个嵌套的 if-else 语句上会存在解释歧义。



另一个例子是类型推断,虽然在声明具有初始值的变量时使用类型推断没有问题,但声明未初始化的变量 时指定类型很重要,因为这可以确保类型安全



良好代码风格的通常是通过代码审查来维持,但是通过在编写代码时,运行静态分析来强制执行规则通常会更有效得多。


在 Dart 中,这种静态分析规则是高度可配置的,Dart 提供了有数百条样式规则(也称为lints),有了如此丰富的选项,选择启用这些的规则时,一开始可能会有些不知所措。



配置支持: dart.dev/guides/lang…


lint 规则: dart.dev/tools/linte…



Dart 团队维护了一个 Dart 风格指南,它描述了 Dart 团队认为编写和设计 Dart 代码的最佳方式。



风格指南: dart.dev/guides/lang…



许多开发人员以及 pub.dev 站点评分引擎都使用了一套叫 Pedantic 的 lint 规则, Pedantic 起源于 Google 内部的 Dart 风格指南,由于历史原因它不同于一般的 Dart 风格指南,此外 Flutter 框架也从未使用过 Pedantic 的规则集,而是拥有自己的一套规范规则。


这听起来可能有点混乱,但是在本次的 2.14 发布中,Dart 团队很高兴地宣布现在拥有一套全新的 lint 集合来实现代码样式指南,并且 Dart 和 Flutter SDK 默认情况下将这些规则集用于新项目:




  • package:lints/core.yaml所有 Dart 代码都应遵循的 Dart 风格指南中的主要规则,pub.dev 评分引擎已更新为 lints/core 而不是 Pedantic。




  • package:lints/recommended.yaml :核心规则之外加上推荐规则,建议将它用于所有通用 Dart 代码。




  • package:flutter_lints/flutter.yaml:核心和推荐之外的 Flutter 特定推荐规则,这个集合推荐用于所有 Flutter 代码。




如果你已经存在现有的 Dart 或者 Flutter项目,强烈建议升级到这些新规则集,从 pedantic 升级只需几步:github.com/dart-lang/l…


Dart 格式化程序和级联


Dart 2.14 对 Dart 格式化程序如何使用级联 格式化代码进行了一些优化。


以前格式化程序在某些情况下出现一些令人困惑的格式,例如 doIt() 在这个例子中调用了什么?


var result = errorState ? foo : bad..doIt();

它看起来像是被 bad 调用 ,但实际上级联适是用于整个 ? 表达式上的,因此级联是在该表达式的结果上调用的,而不仅仅是在 false 子句上,新的格式化程序清晰地描述了这一点:


 var result = errorState ? foo : bad\
..doIt();

Dart 团队还大大提高了格式化包含级联的代码的速度;在协议缓冲区生成的 Dart 代码中,可以看到格式化速度提高了 10 倍。


Pub 支持忽略文件


目前当开发者将包发布pub.dev社区时,pub 会抓取该文件夹中的所有文件,但是会跳过隐藏文件(以 . 开头的文件)和.gitignore 文件。


Dart 2.14 中更新的 pub 命令支持新 .pubignore 文件,开发者可以在其中列出不想上传到 pub.dev 的文件,此文件使用与 .gitignore 文件相同的格式。



有关详细信息,请参阅包发布文档 dart.dev/tools/pub/p…



Pub and "dart test" 性能


虽然 pub 最常用于管理代码依赖项,但它还有第二个重要的用途:驱动工具。


比如 Dart 测试工具通过 dart test 命令运行,而它实际上只是 command pub run test:test 命令的包装, package:test 在调用该 test 入口点之前,pub 首先将其编译为可以更快运行的本机代码。


在 Dart 2.14 之前对 pubspec 的任何更改(包括与 package:test 无关的更改)都会使此测试构建无效,并且还会看到一堆这样的输出,其中包含“预编译可执行文件”:


$ dart test\
Precompiling executable... (11.6s)\
Precompiled test:test.\
00:01 +1: All tests passed!

在 Dart 2.14 中,pub 在构建步骤方面更加智能,让构建仅在版本更改时发生,此外还使用并行化改进了执行构建步骤的方式,因此可以完成得更快。


新的语言功能


Dart 2.14 还包含一些语言特性变化。


首先添加了一个新的 三重移位 运算符 ( >>>),这类似于现有的移位运算符 ( >>),但 >> 执行算术移位,>>> 执行逻辑或无符号移位,其中零位移入最高有效位,而不管被移位的数字是正数还是负数。


此次还删除了对类型参数的旧限制,该限制不允许使用泛型函数类型作为类型参数,以下所有内容在 2.14 之前都是无效的,但现在是允许的:


late List<T Function<T>(T)> idFunctions;
var callback = [<T>(T value) => value];
late S Function<S extends T Function<T>(T)>(S) f;

最后对注释类型进行了小幅调整,(诸如 @Deprecated 在 Dart 代码中常用来捕获元数据的注解)以前注解不能传递类型参数,因此 @TypeHelper<int>(42, "The meaning") 不允许使用诸如此类的代码,而现在此限制现已取消。


包和核心库更改


对核心 Dart 包和库进行了许多增强修改,包括:




  • dart:core: 添加了静态方法 hashhashAllhashAllUnordered




  • dart:coreDateTime 类现在可以更好地处理本地时间。




  • package:ffi:添加了对使用 arena 分配器管理内存的支持(示例)。Arenas 是一种基于区域的内存管理形式,一旦退出 arena/region 就会自动释放资源。




  • package:ffigen:现在支持从 C 类型定义生成 Dart 类型定义。




重大变化


Dart 2.14 还包含一些重大更改,预计这些变化只会影响一些特定的用例。


#46545:取消对 ECMAScript5 的支持


所有浏览器都支持最新的 ECMAScript 版本,因此两年前 Dart 就宣布 计划弃用对 ECMAScript 5 (ES5) 的支持,这使 Dart 能够利用最新 ECMAScript 中的改进并生成更小的输出,在 Dart 2.14 中,这项工作已经完成,Dart Web 编译器不再支持 ES5。因此不再支持较旧的浏览器(例如 IE11)


#46100:弃用 stagehand、dartfmt 和 dart2native


在 2020 年 10 月的 Dart 2.10 博客文章中 宣布了将所有 Dart CLI 开发人员工具组合成一个单一的组合dart工具(类似于该flutter工具),而现在 Dart 2.14 弃用了 dartfmtdart2native 命令,并停止使用 stagehand ,这些工具在统一在 dart-tool 中都有等价的替代品。


#45451:弃用 VM Native 扩展


Dart SDK 已弃用 Dart VM 的 Native 扩展,这是从 Dart 代码调用 Native 代码的旧机制,Dart FFI(外来函数接口)是当前用于此用例的新机制,正在积极发展 以使其功能更加强大且易于使用。


作者:恋猫de小郭
链接:https://juejin.cn/post/7005770958141308935
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

如何打造一款权限请求框架

原理 通过向当前Activity添加一个不可见的Fragment,从而实现权限申请流程的封装。 实现 不可见的Fragment internal class EPermissionFragment : Fragment() { private var ...
继续阅读 »

原理


通过向当前Activity添加一个不可见的Fragment,从而实现权限申请流程的封装。


实现


不可见的Fragment


internal class EPermissionFragment : Fragment() {
private var mCallback: EPermissionCallback? = null

fun requestPermission(callback: EPermissionCallback, vararg permissions: String) {
mCallback = callback
// 申请权限
requestPermissions(permissions, CODE_REQUEST_PERMISSION)
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (requestCode == CODE_REQUEST_PERMISSION) {
val deniedList = ArrayList<String>()
val deniedForeverList = ArrayList<String>()
grantResults.forEachIndexed { index, result ->
// 提取权限申请结果
if (result != PackageManager.PERMISSION_GRANTED) {
val permission = permissions[index]
deniedList.add(permission)
// 是否拒绝且不再显示
if (!shouldShowRequestPermissionRationale(permission)) {
deniedForeverList.add(permission)
}
}
}
if (deniedList.isEmpty()) mCallback?.onAllGranted()
if (deniedList.isNotEmpty()) mCallback?.onDenied(deniedList)
if (deniedForeverList.isNotEmpty()) mCallback?.onDeniedForever(deniedForeverList)
}
}

override fun onDestroy() {
mCallback = null
super.onDestroy()
}
}

封装权限申请


// 扩展FragmentActivity
fun FragmentActivity.runWithPermissions(
vararg permissions: String,
onDenied: (ArrayList<String>) -> Unit = { _ -> },
onDeniedForever: (ArrayList<String>) -> Unit = { _ -> },
onAllGranted: () -> Unit = {}
) {
if (checkPermissions(*permissions)) {
onAllGranted()
return
}
// 添加一个不可见的Fragment
val isFragmentExist = supportFragmentManager.findFragmentByTag(EPermissionFragment.TAG)
val fragment = if (isFragmentExist != null) {
isFragmentExist as EPermissionFragment
} else {
val invisibleFragment = EPermissionFragment()
supportFragmentManager.beginTransaction().add(invisibleFragment, EPermissionFragment.TAG).commitNowAllowingStateLoss()
invisibleFragment
}
val callback = object : EPermissionCallback {
override fun onAllGranted() {
onAllGranted()
}

override fun onDenied(deniedList: ArrayList<String>) {
onDenied(deniedList)
}

override fun onDeniedForever(deniedForeverList: ArrayList<String>) {
onDeniedForever(deniedForeverList)
}
}
// 申请权限
fragment.requestPermission(callback, *permissions)
}

使用方法


项目build.gradle添加


allprojects {
repositories {
...
maven { url 'https://www.jitpack.io' }
}
}

模块build.gradle添加


dependencies {
implementation 'com.github.RickyHal:EPermission:$latest_version'
}

在Activity或者Fragment中直接调用


// 申请存储权限
runWithPermissions(
*EPermissions.STORAGE,
onDenied = {
Toast.makeText(this, "STORAGE permission denied", Toast.LENGTH_SHORT).show()
},
onDeniedForever = {
Toast.makeText(this, "STORAGE permission denied forever", Toast.LENGTH_SHORT).show()
},
onAllGranted = {
Toast.makeText(this, "STORAGE permission granted", Toast.LENGTH_SHORT).show()
}
)

也可以用下面这个简单的方法


runWithStoragePermission(onFailed = {
Toast.makeText(this, "SMS permission denied", Toast.LENGTH_SHORT).show()
}) {
Toast.makeText(this, "SMS permission granted", Toast.LENGTH_SHORT).show()
}

一次申请多个权限


runWithPermissions(*EPermissions.CAMERA, *EPermissions.STORAGE,
onDenied = { deniedList ->
Toast.makeText(this, "permission denied $deniedList", Toast.LENGTH_SHORT).show()
},
onDeniedForever = { deniedForeverList ->
Toast.makeText(this, "permission denied forever $deniedForeverList", Toast.LENGTH_SHORT).show()
},
onAllGranted = {
Toast.makeText(this, "Permission all granted", Toast.LENGTH_SHORT).show()
})

如果不需要处理失申请权限败的情况,也可以直接这样写


runWithStoragePermission {
Toast.makeText(this, "SMS permission granted", Toast.LENGTH_SHORT).show()
}

如果某些操作执行的时候,只能有权限才去执行,则可以使用下面的方法


doWhenPermissionGranted(*EPermissions.CAMERA){
Toast.makeText(this, "Do this when camera Permission is granted", Toast.LENGTH_SHORT).show()
}

检查权限


if (checkPermissions(*EPermissions.CAMERA)) {
Toast.makeText(this, "Camera Permission is granted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Camera Permission is not granted", Toast.LENGTH_SHORT).show()
}

GitHub传送门


作者:应用软件开发爱好者
链接:https://juejin.cn/post/7005913659394228232
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Frida笔记 - Android 篇 (一)

前言 相信不少小伙伴对Xposed、Cydia Substrate、Frida等hook工具都有所了解, 并且用在了自己的工作中, 本文主要分享Frida的环境配置以及基本使用, 以及相关功能在日常开发调试带来的帮助 配置Frida的环境 Frida的环境安装...
继续阅读 »

前言


相信不少小伙伴对Xposed、Cydia Substrate、Frida等hook工具都有所了解, 并且用在了自己的工作中, 本文主要分享Frida的环境配置以及基本使用, 以及相关功能在日常开发调试带来的帮助


配置Frida的环境


Frida的环境安装可以参考官方文档, 或者参考网上分享的实践, 使用较为稳定的特定版本


# 通过pip3安装Frida的CLI工具
pip3 install frida-tools
# 安装的frida版本
frida --version
# 本机目前使用的15.0.8的frida版本
# 在https://github.com/frida/frida/releases下载对应的server版本frida-server-15.0.8-android-arm64.xz
# unxz 解压缩
unxz frida-server-15.0.8-android-arm64.xz
adb root
adb push frida-server-15.0.8-android-arm64 /data/locl/tmp/
adb shell
chmod 755 /data/local/tmp/frida-server-15.0.8-android-arm64
/data/local/tmp/frida-server-15.0.8-android-arm64 &
# 打印已安装程序及包名
frida-ps -Uai

基本使用




  • Frida的开发环境, 可以参考作者在github上的exmaple


    下载完成后通过vscode打开


    git clone git://github.com/oleavr/frida-agent-example.git
    npm install




  • 配置完成后, 使用对应函数就会有相应代码提示及函数说明






  • JavaScript API可以参考官方文档说明, 了解基本使用


Hook类的构造函数


// frida -U --no-pause -f com.gio.test.three -l agent/constructor.js

function main() {
Java.perform(function () {
Java.use(
"com.growingio.android.sdk.autotrack.AutotrackConfiguration"
).$init.overload("java.lang.String", "java.lang.String").implementation =
function (projectId, urlScheme) {
// 调用原函数
var result = this.$init(projectId, urlScheme);
// 打印参数
console.log("projectId, urlScheme: ", projectId, urlScheme);
return result;
};
});
}

setImmediate(main);

Hook类的普通函数


// frida -U --no-pause -f com.gio.test.three -l agent/function.js

function main() {
Java.perform(function () {
Java.use(
"com.growingio.android.sdk.CoreConfiguration"
).setDebugEnabled.implementation = function (enabled) {
console.log("enabled: ", enabled);
// 直接返回原函数执行结果
return this.setDebugEnabled(enabled);
};
});
}

setImmediate(main);

修改类/实例参数


// 以attach的方式附加到进程, 或者使用setTimeout替换setImmediate
// frida -U -n demos -l agent/instance.js
function main() {
Java.perform(function () {
Java.choose("com.growingio.android.sdk.autotrack.AutotrackConfiguration", {
onMatch: function (instance) {
console.log("instance.mProjectId", instance.mProjectId.value);
console.log("instance.mUrlScheme", instance.mUrlScheme.value);
// 修改变量时通过赋值, 如果变量与函数同名, 需要在变量前加'_', 如: _mProjectId
instance.mProjectId.value = "t-bfc5d6a3693a110d";
instance.mUrlScheme.value = "t-growing.d80871b41ef40518";
},
onComplete: function () {},
});
});
}

setImmediate(main);

构造数组


frida -U -n demos -l agent/array.js

function main() {
Java.perform(function () {
// 构造byte数组
var byteArray = Java.array("byte", [0x46, 0x72, 0x69, 0x64, 0x61]);
// 输出为: Frida
console.log(Java.use("java.lang.String").$new(byteArray));

// 构造char数组
var charArray = Java.array("char", ["F", "r", "i", "d", "a"]);
console.log(Java.use("java.lang.String").$new(charArray));
});
}

setImmediate(main);

静态函数主动调用


// 以attach的方式附加到进程, 或者使用setTimeout替换setImmediate
// frida -U -n demos -l agent/staticFunction.js

function main() {
Java.perform(function () {
// setWebContentsDebuggingEnabled 需要在主线程调用
Java.scheduleOnMainThread(function () {
console.log("isMainThread", Java.isMainThread());
// 主动触发静态函数调用, 允许WebView调试
Java.use("android.webkit.WebView").setWebContentsDebuggingEnabled(true);
});
});
}

setImmediate(main);

动态函数主动调用


// 以attach的方式附加到进程, 或者使用setTimeout替换setImmediate
// frida -U -n demos -l agent/dynamicFunction.js

function main() {
Java.perform(function () {
Java.choose("com.growingio.android.sdk.autotrack.AutotrackConfiguration", {
onMatch: function (instance) {
// 主动触发动态函数调用
console.log("instance.isDebugEnabled: ", instance.isDebugEnabled());
console.log("instance.getChannel: ", instance.getChannel());
},
onComplete: function () {},
});
});
}

setImmediate(main);

定义一个类


// 以attach的方式附加到进程, 或者使用setTimeout替换setImmediate
// frida -U -n demos -l agent/registerClass.js

function main() {
Java.perform(function () {
var TestRunnable = Java.registerClass({
name: "com.example.TestRunnable",
// 实现接口
implements: [Java.use("java.lang.Runnable")],
// 成员变量
fields: {
testFields: "java.lang.String",
},
methods: {
// 构造函数
$init: [
{
returnType: "void",
argumentTypes: ["java.lang.String"],
implementation: function (testFields) {
// 调用父类构造函数
this.$super.$init();
// 给成员变量赋值
this.testFields.value = testFields;
console.log("$init: ", this.testFields.value);
},
},
],
// 方法
run: [
{
returnType: "void",
implementation: function () {
console.log(
"testFields: ",
this.testFields.value
);
},
},
],
},
});

TestRunnable.$new("simple test").run();
});
}

setImmediate(main);

打印函数调用堆栈


// 以attach的方式附加到进程, 或者使用setTimeout替换setImmediate
// frida -U -n demos -l agent/printStackTrace.js

function main() {
Java.perform(function () {
Java.use(
"com.growingio.android.sdk.autotrack.click.ViewClickInjector"
).viewOnClick.overload(
"android.view.View$OnClickListener",
"android.view.View"
).implementation = function (listener, view) {
// 打印当前调用堆栈信息
console.log(
Java.use("android.util.Log").getStackTraceString(
Java.use("java.lang.Throwable").$new()
)
);
return this.viewOnClick(listener, view);
};
});
}

setImmediate(main);

枚举classLoader


// 以attach的方式附加到进程, 或者使用setTimeout替换setImmediate
// frida -U -n demos -l agent/enumerateClassLoaders.js
// 适用于加固的应用, 找到对应的classloader
// 通常直接在application.attach.overload('android.content.Context').implementation获取context对应的classloader

function main() {
Java.perform(function () {
Java.enumerateClassLoaders({
onMatch: function (loader) {
try {
// 判断该loader中是否存在我们需要hook的类
if (loader.findClass("com.growingio.android.sdk.CoreConfiguration")) {
console.log("found loader:", loader);
Java.classFactory.loader = loader;
}
} catch (error) {
console.log("found error: ", error);
console.log("failed loader: ", loader);
}
},
onComplete: function () {
console.log("enum completed!");
},
});
console.log(
Java.use("com.growingio.android.sdk.CoreConfiguration").$className
);
});
}

setImmediate(main);

枚举类


// 以attach的方式附加到进程, 或者使用setTimeout替换setImmediate
// frida -U -n demos -l agent/enumerateLoadedClasses.js

function main() {
Java.perform(function () {
Java.enumerateLoadedClasses({
onMatch: function (name, handle) {
// 判断是否是我们要查找的类
if (name.toString() == "com.growingio.android.sdk.CoreConfiguration") {
console.log("name, handle", name, handle);
Java.use(name).isDebugEnabled.implementation = function () {
return true;
};
}
},
onComplete: function () {},
});
});
}

setImmediate(main);

加载外部dex并通过gson打印对象


// 以attach的方式附加到进程, 或者使用setTimeout替换setImmediate
// frida -U -n demos -l agent/printObject.js
// 通过d8将 gson.jar 转为 classes.dex
// ~/Library/Android/sdk/build-tools/30.0.3/d8 --lib ~/Library/Android/sdk/platforms/android-30/android.jar gson-2.8.8.jar
// 如果SDK中已经有了, 可以直接使用Java.use加载
// adb push classes.dex /data/local/tmp

function main() {
Java.perform(function () {
Java.choose("com.growingio.android.sdk.autotrack.AutotrackConfiguration", {
onMatch: function (instance) {
// 加载外部dex
Java.openClassFile("/data/local/tmp/classes.dex").load();
var Gson = Java.use("com.google.gson.Gson");
// JSON.stringify: "<instance: com.growingio.android.sdk.autotrack.AutotrackConfiguration>"
console.log("JSON.stringify: ", JSON.stringify(instance));
// Gson.$new().toJson: {"mImpressionScale":0.0,"mCellularDataLimit":10,"mDataCollectionEnabled":true,"mDataCollectionServerHost":"http://api.growingio.com","mDataUploadInterval":15,"mDebugEnabled":true,"mOaidEnabled":false,"mProjectId":"bfc5d6a3693a110d","mSessionInterval":30,"mUploadExceptionEnabled":false,"mUrlScheme":"growing.d80871b41ef40518"}
console.log("Gson.$new().toJson: ", Gson.$new().toJson(instance));
},
onComplete: function () {},
});
});
}

setImmediate(main);

使用场景




  1. 绕过证书绑定、校验, 进行埋点请求验证




  2. SDK开发过程中, 一般客户反馈问题都需要使用客户的app进行问题的复现及排查, 此时通过frida获取运行时特定函数的参数信息及返回信息, 能有效缩短与客户的沟通时间, 该场景使用objection最为方便




  3. 新客户在集成前, 希望看到SDK能够提供的效果, 通过frida加载dex并完成初始化, 可以提前发现兼容性问题




  4. 当碰到集成早期版本SDK的应用反馈异常, 通过类似Tinker热修复的思想替换SDK验证是否已经在当前版本修复




  5. 开放SDK相关函数远程rpc调用, 用于测试埋点的协议等场景




外链地址




  1. Frida官方文档: frida.re/docs/instal…




  2. Frida作者提供的example github地址: github.com/oleavr/frid…




  3. JavaScript API官方文档: frida.re/docs/javasc…




  4. 功能介绍中所使用demo: github.com/growingio/g…




  5. r0capture 安卓应用层通杀脚本 github地址: github.com/r0ysue/r0ca…




  6. objection github地址: github.com/sensepost/o…


作者:GrowingIO技术社区
链接:https://juejin.cn/post/7005889219595862023
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

正式版即将到来 | Android 12 Beta 5 现已发布

作者 / Dave Burke, VP of Engineering距离 Android 12 的正式发布只有几周时间了!新版 Android 系统的润色已经进入收尾阶段,今天我们带来最后一个 Beta 版更新,供大家进行测试和开发。对于开发者来说,是时候让自...
继续阅读 »

作者 / Dave Burke, VP of Engineering

距离 Android 12 的正式发布只有几周时间了!新版 Android 系统的润色已经进入收尾阶段,今天我们带来最后一个 Beta 版更新,供大家进行测试和开发。对于开发者来说,是时候让自己的应用做好准备了!

今天,您就可以在 Pixel 设备上 (包括 5G 版 Pixel 5a) 通过 OTA 更新 开始体验 Android 12 Beta 5。如果您之前已经加入了 Beta 测试,则会自动获得更新。您还可以在我们的设备制造商合作伙伴的若干指定设备上体验 Android 12 Beta 5,具体请查看 这里

有关 Android 12 的详细信息以及如何开始开发,请访问 Android 12 开发者网站

请大家关注即将发布的 Android 12 正式版的更多信息!

Beta 5 更新一览

今天的更新包含适用于 Pixel 和其他设备以及 Android 模拟器的 Android 12 发布候选版本。我们已经 在 Beta 4 抵达平台稳定性里程碑,所有面向应用的接口都已最终确定,包括 SDK 和 NDK API、面向应用的系统行为,以及非 SDK 接口限制都已确定。除此之外,Beta 5 还带来了最新的修复和优化,为您提供了完成测试所需的一切。

让您的应用做好准备

随着 Android 12 正式版的临近,我们要求所有的应用和游戏开发者完成最终兼容性测试,并在正式版到来之前发布应用和游戏的兼容性更新。对于所有 SDK、开发库、工具和游戏引擎的开发者来说,尽快发布兼容性更新更为重要: 在获得来自您的更新之前,您的下游应用和游戏开发者的工作可能会受阻。

要测试应用的兼容性,只需在运行 Android 12 Beta 5 的设备上安装您的应用,并测试应用的所有流程,找出功能或 UI 上暴露的问题。请通过 行为变更清单 (针对所有应用) 来找出可能影响应用的潜在变更,从而确定测试重点。

这里列出一些需要注意的变更:

  • 隐私中心 - 这是系统设置 (Settings) 中新加入的一个界面,可以让用户看到哪些应用在访问哪些类型的数据,以及何时访问。如果需要,用户可以对权限进行调整,并从应用获知其访问数据的详细原因。请访问 官方文档 了解详细信息。

  • 麦克风和摄像头指示标志 - 当应用正在使用摄像头或麦克风时,Android 12 会在状态栏中显示指示图标。请访问 官方文档 了解详细信息。

  • 麦克风和摄像头全局开关 - 快速设置 (Quick Settings) 中新增的全局开关功能,可以让用户立即禁用所有应用的麦克风和摄像头访问权限。请访问 官方文档 了解详细信息。

  • 剪贴板访问通知 - 当应用从剪贴板中读取数据时,系统会提醒用户。请访问 官方文档 了解详细信息。

  • 过度滚动拉伸效果 - 过度滚动时,新的 "拉伸" 效果在全系统范围内取代了以前的发光效果。请访问 官方文档 了解详细信息。

  • 应用启动画面 - Android 12 在启动应用时会使用全新的启动动画。请访问 官方文档 了解详细信息。

  • Keygen 变更 - 我们移除了一些被废弃的 BouncyCastle 加密算法,转而使用 Conscrypt 实现。如果您的应用使用 512 位的 AES 密钥,您需要将其改为 Conscrypt 支持的标准长度。请访问 官方文档 了解详细信息。

别忘了测试应用里的开发库和 SDK 的兼容性。如果您发现 SDK 的问题,请尝试更新到最新版本的 SDK ,或向其开发者寻求帮助。

一旦您发布了当前应用的兼容版本,就可以 开始着手升级 应用的 targetSdkVersion。请查阅 行为变更清单 (针对面向 Android 12 的应用),并使用 兼容性框架工具 来快速检测问题。

探索新功能和 API

Android 12 拥有大量的新功能,可以帮助您为用户构建良好的体验。请回顾我们 在 Beta 2 时所做的介绍,以及 Google I/O 上的 Android 12 演讲。要了解所有新功能和 API 的完整细节,请访问 Android 12 开发者网站

另外别忘了试用 Android Studio Arctic Fox 进行 Android 12 的开发和测试。我们已经添加了可以帮助您发现代码中可能受到 Android 12 变更影响的 lint 检查,如对启动画面的自定义声明、请求精细位置的粗略位置许可、媒体格式,以及高传感器采样率权限等。您可以 下载 并 配置 最新版本的 Android Studio 来尝试这些新功能。

即刻开始体验 Android 12

不论您是想体验 Android 12 的功能、测试应用还是 提交反馈,都可以从这次的 Beta 5 开始。只需 使用支持的 Pixel 设备注册参加测试,即可通过无线 (OTA) 方式获得更新。要开始进行开发,请先安装并设置 Android 12 SDK

您也可以在参与 Android 12 开发者预览计划的设备制造商的设备上体验 Android 12 Beta 5,请访问 developer.android.google.cn/about/versi… 查看合作伙伴的完整列表。您也可以通过 Android GSI 映像 在更多设备上进行更广泛的测试。如果您没有合适的设备,也可以在 Android 模拟器 上进行测试。Beta 5 也适用于 Android TV,您可以查看最新的功能,测试自己的应用,并尝试全新的 Google TV 体验。

下一步

Android 12 会在接下来几周内正式发布,请大家保持关注!在此期间,欢迎继续通过问题反馈页面向我们 分享您的使用反馈,包括 平台问题应用兼容性问题 以及 第三方 SDK 问题

再次感谢我们的开发者社区为打造 Android 12 做出的巨大贡献!大家分享了 数以千计的问题报告 和洞察,帮助我们调整 API、改进功能、修复重大问题,从而为用户和开发者们打造出更好的平台。

收起阅读 »

Java多线程

运行环境与工具jdk1.8.0macOS 11.4IDEA操作系统可以在同一时刻运行多个程序。例如一边播放音乐,一边下载文件和浏览网页。操作系统将cpu的时间片分配给每一个进程,给人一种并行处理的感觉。一个多线程程序可以同时执行多个任务。通常,每一个任务称为一...
继续阅读 »

运行环境与工具

  • jdk1.8.0
  • macOS 11.4
  • IDEA

操作系统可以在同一时刻运行多个程序。例如一边播放音乐,一边下载文件和浏览网页。操作系统将cpu的时间片分配给每一个进程,给人一种并行处理的感觉。

一个多线程程序可以同时执行多个任务。通常,每一个任务称为一个线程(thread),它是线程控制的简称。可以同时运行一个以上线程的程序成为多线程程序(multithreaded)。

多进程多线程有哪些区别呢?

本质区别在于进程每个进程有自己的一整套变量,而线程则共享数据。 线程比进程更轻量级,创建、销毁一个线程比启动新进程的开销要小。

实际应用中,多线程非常有用。例如应用一边处理用户的输入指令,一遍联网获取数据。

本文我们介绍Java中的Thread类。

Thread

Thread类属于java.lang包。

要创建一个线程很简单,新建一个Thread对象,并传入一个Runnable,实现run()方法。 调用start()方法启动线程。

    Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("rustfisher said: hello");
}
});
t1.start();

Java lambda

    Thread t1 = new Thread(() -> System.out.println("rustfisher said: hello"));
t1.start();

不要直接调用run()方法。 直接调用run()方法不会启动新的线程,而是直接在当前线程执行任务。

我们来看一个使用了Thread.sleep()方法的例子。

Thread t1 = new Thread(() -> {
for (String a : "rustfisher said: hello".split("")) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(a);
}
});
t1.start();

sleep(int)方法会让线程睡眠一个指定的时间(单位毫秒)。并且需要try-catch捕获InterruptedException异常。

中断线程

run()方法执行完最后一条语句后,或者return,或者出现了未捕获的异常,线程将会终止。

使用Thread的interrupt方法也可以终止线程。调用interrupt方法时,会修改线程的中断状态为true。 用isInterrupted()可以查看线程的中断状态。

但如果线程被阻塞了,就没法检测中断状态。当在一个被阻塞的线程(sleep或者wait)上调用interrupt方法,阻塞调用将会被InterruptedException中断。

被中断的线程可以决定如何响应中断。可以简单地将中断作为一个终止请求。比如我们主动捕获InterruptedException

Thread t2 = new Thread(() -> {
for (String a : "rustfisher said: hello".split("")) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("被中断 退出线程");
return;
}
System.out.print(a);
}
});
t2.start();

new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.interrupt();
}).start();

上面这个小例子展示了用interrupt()来中断线程t2。而线程t2run()方法中捕获InterruptedException后,可以进行自己的处理。

线程的状态

线程有6种状态,用枚举类State来表示:

  • NEW(新创建)
  • RUNNABLE(可运行)
  • BLOCKED(被阻塞)
  • WAITING(等待)
  • TIMED_WAITING(计时等待)
  • TERMINATED(被终止)

getState()方法可以获取到线程的状态。

新创建线程

new一个线程的时候,线程还没开始运行,此时是NEW(新创建)状态。在线程可以运行前,还有一些工作要做。

可运行线程

一旦调用start()方法,线程处于RUNNABLE(可运行)状态。调用start()后并不保证线程会立刻运行,而是要看操作系统的安排。

一个线程开始运行后,它不一定时刻处于运行状态。操作系统可以让其他线程获得运行机会。一个可运行的线程可能正在运行也可能没在运行。

被阻塞和等待

线程处于被阻塞和等待状态时,它暂时不活动。不运行代码,且只消耗最少的资源。直到线程调度器重新激活它。

  • 一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则这个线程进入阻塞状态。当这个锁被释放,并且线程调度器允许这个线程持有它,该线程变成非阻塞状态。
  • 当线程等待另一个线程通知调度器,它自己进入等待状态。例如调用Object.wait()或者Thread.join()方法。
  • 带有超时参数的方法可让线程进入超时等待状态。例如Thread.sleep()Object.wait(long)Thread.join(long)Lock.tryLock(long time, TimeUnit unit)

thread-state.png

上面这个图展示了状态之间的切换。

被终止

终止的原因:

  • run方法正常退出
  • 出现了没有捕获的异常而终止了run方法

线程属性

线程优先级,守护线程,线程组以及处理未捕获异常的处理器。

线程优先级

Java中每个线程都有一个优先级。默认情况下,线程继承它的父线程的优先级。 可用setPriority(int)方法设置优先级。优先级最大为MAX_PRIORITY = 10,最小为MIN_PRIORITY = 1,普通的是NORM_PRIORITY = 5。 线程调度器有机会选新线程是,会优先选高优先级的线程。

守护线程

调用setDaemon(true)可以切换为守护线程(daemon thread)。守护线程的用途是为其他线程提供服务。例如计时线程。 当只剩下守护线程是,虚拟机就退出了。

守护线程不应该去访问固有资源,如文件和数据库。

未捕获异常处理器

run()方法里抛出一个未捕获异常,在线程死亡前,异常被传递到一个用于未捕获异常的处理器。 要使用这个处理器,需要实现接口Thread.UncaughtExceptionHandler,并且用setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler)方法把它交给线程。

Thread t3 = new Thread(() -> {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
int x = 0, y = 3;
int z = y / x; // 故意弄一个异常
});
t3.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t + "有未捕获异常");
e.printStackTrace();
}
});
t3.start();

运行后,run()方法里抛出ArithmeticException异常

Thread[Thread-0,5,main]有未捕获异常
java.lang.ArithmeticException: / by zero
at Main.lambda$main$0(Main.java:15)
at java.lang.Thread.run(Thread.java:748)

也可以用静态方法Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler)给所有的线程安装一个默认处理器。可以在这个默认处理器里做一些工作,例如记录日志。

ThreadGroup代表着一组线程。也可以包含另外的线程组。

ThreadGroup类实现了UncaughtExceptionHandler接口。它的uncaughtException(Thread t, Throwable e)方法会有如下操作

  • 如果该线程组有父线程组,则父线程组的uncaughtException被调用。
  • 否则,如果Thread.getDefaultUncaughtExceptionHandler()返回一个非空处理器,则使用这个处理器。
  • 否则,如果抛出的ThrowableThreadDeath对象,就什么也不做。
  • 否则,线程的名字和Throwable的栈踪迹输出到System.err上。
收起阅读 »

Android compose自定义布局

开新坑了,compose自定义布局。基础知识不说了,直接上正题。我们知道,在views体系下,自定义布局需要view集成viewgroup重写onMeasure、onLayout方法,在compse中,是使用Layout的compose方法,结构如下:以一个自...
继续阅读 »

开新坑了,compose自定义布局。基础知识不说了,直接上正题。

我们知道,在views体系下,自定义布局需要view集成viewgroup重写onMeasure、onLayout方法,在compse中,是使用Layout的compose方法,结构如下:

以一个自定义Column为例:

1、首先我们定义自己的cpmpose函数

@Composablefun 
MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
)

这个方法包含最基础的两个入参,一个是修饰符modifier,一个是@composable注解的lamda表达式作为子项的内容

2、看看具体函数体的操作

@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each children
measurable.measure(constraints)
}
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Track the y co-ord we have placed children up to
var yPosition = 0
// Place children in the parent layout
placeables.forEach { placeable ->
// Position item on the screen
placeable.placeRelative(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}

代码是从官网抄的,正确性就不用说了,具体分析一下作用。

1、在函数体中使用已经定义好的Layout方法。(这个有点类似 类 的继承,compose中所有组件定义都是使用方法,没有类中的子类父类的概念,如果想要做一些统一的封装操作会比较麻烦,可以使用这种方法,函数体内去执行另一个封装好的函数,而函数最后一个参数使用@composable注解的lamda)Layout方法把修饰符和content接收,回调中发送的是 measurables和constraints.从名字就可以猜出这两个参数的作用

  • measurables:可测量元素,就是传进来的子元素
  • constraints:父类约束条件

Layout函数的lamda来自于第三个入参MeasurePolicy,这是一个接口,上面两个参数就来自于这个接口的回调:

fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult

2、使用map函数遍历measurables,每一个measurable调用measure并把constraint传入,相当于给每个子控件根据父控件的约束进行测量,类似于views体系下面的measure,得到palceables。

3、调用layout方法(注意小写,这是单独放置一个控件的方法,是单个可组合项的修饰,具体下面会再讲),layout(width,heigiht)传入布局的宽和高,在layout方法lamda中,对每一个placeable调用place方法(有几个类似的,这里使用placeRelative),传入相应坐标,完成子view的布局

到这里一个自定义布局就完成了,其实和views体系下面很像,也是相似的两步:

1、测量每个子view在父view约束下的大小

2、遍历子view,使用layout方法将每个view放在正确的位置上。

大同小异大同小异

3、关于layout(注意是小写的)

先抄一段官网的说明:

您可以使用 layout 修饰符来修改元素的测量和布局方式,layout 是一个 lambda;它的参数包括您可以测量的元素(以 measurable 的形式传递)以及该可组合项的传入约束条件(以 constraints 的形式传递)

再抄一段代码:

fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp) =
layout { measurable, constraints ->
// Measure the composable
val placeable = measurable.measure(constraints)
// Check the composable has a first baseline
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
// Height of the composable with padding - first baseline
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
val height = placeable.height + placeableY
layout(placeable.width, height) {
// Where the composable gets placed
placeable.placeRelative(0, placeableY)
}}

这个是官网上面修改text baseline top padding的方法。具体内容就不说了,可以看到,是定义了一个modifier的扩展函数,回调参数是一个measurable,内部具体还是调用layout,大同小异大同小异。

到这里自定义布局就结束了,自定义布局难点主要还是在子view在父view约束下面的布局逻辑,也可以看到其实这个移植以前views下面的自定义布局应该是比较容易的,把坐标计算逻辑抽离,然后就可以轻松完成移植了。(另外我从这里还发现了compose下面怎么实现类似以前类的继承,那就是活用fun中最后一个lamda参数,由于kotlin语法的关系,容易把lamda看作是函数体,其实在fun中只是调用了一个fun,函数具体执行都被隐藏了起来)

收起阅读 »

Android 非Root设备下调试so

准备工作手机:Google Pixel 3 Android 11, API 30工具:IDA 7.0、Android Studio电脑系统:win10写一个C++ demo稍微改动下代码,点击Hello World调用c++class MainActivity...
继续阅读 »

准备工作

  1. 手机:Google Pixel 3 Android 11, API 30
  2. 工具:IDA 7.0、Android Studio
  3. 电脑系统:win10

写一个C++ demo

image.png

稍微改动下代码,点击Hello World调用c++

class MainActivity : AppCompatActivity() {

@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// Example of a call to a native method
sample_text.setOnClickListener {
sample_text.text = stringFromJNI() + intFromJNI()
}
}

/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/

private external fun stringFromJNI(): String

private external fun intFromJNI(): Int

companion object {
// Used to load the 'native-lib' library on application startup.
init {
System.loadLibrary("native-lib")
}
}
}

native-lib.cpp代码

#include <jni.h>
#include <string>

int test_add();

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_testcpp_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++ ";
return env->NewStringUTF(hello.c_str());
}

extern "C" JNIEXPORT jint JNICALL
Java_com_example_testcpp_MainActivity_intFromJNI(JNIEnv *env, jobject thiz) {
int ret = test_add();
return (jint)ret;
}

int test_add() {
return 1 + 1;
}

运行效果(左),点击后(右)

 

将IDA目录dbgsrv下的android_server64放到Android应用目录下

这里要注意看手机是多少位的,我是64位就用64位的android_server64

image.png

通过Android Studio的Device File Explorer upload到对应的应用目录下,这个目录没有root权限通过adb是不能push文件进去 image.png

打开终端进入adb shell启动android_server

C:\Users\Administrator\Desktop\fby>adb shell
* daemon not running; starting now at tcp:5037
* daemon started successfully
blueline:/ $

这里有个关键步骤,如果直接进入到/data/data/com.example.testcpp是没有权限的,也就不能启动android_server

blueline:/ $ cd data/data/com.example.testcpp
/system/bin/sh: cd: /data/data/com.example.testcpp: Permission denied

执行run-as com.example.testcpp,进入到了应用目录,ls看下当前目录,然后启动android_server

2|blueline:/ $ run-as com.example.testcpp
blueline:/data/user/0/com.example.testcpp $ ls
android_server64 cache code_cache databases files no_backup shared_prefs
blueline:/data/user/0/com.example.testcpp $ ./android_server64
IDA Android 64-bit remote debug server(ST) v1.22. Hex-Rays (c) 2004-2017
Listening on 0.0.0.0:23946...

再打开一个终端,转发端口23946

C:\Users\Administrator>adb forward tcp:23946 tcp:23946
23946

打开IDA64 attch进程

image.png

image.png

image.png

点击ok进入到调试页面,这里已经进入断点,按F9让程序执行

image.png

在Modules窗口找到自己写的那个native-lib.so,下断点

image.png

image.png

app上点击Hello World,进入到断点

image.png


收起阅读 »

Android消息队列原理

本文章是对任玉刚前辈的《Android开发艺术》一书中的第10章“Android的消息机制”的简单理解,不足之处请多多指正。Handler是Android机制的上层接口,它的运行要依靠MessQueue和Looper。Handler的使用想必大家都很了解,一般...
继续阅读 »

本文章是对任玉刚前辈的《Android开发艺术》一书中的第10章“Android的消息机制”的简单理解,不足之处请多多指正。

Handler是Android机制的上层接口,它的运行要依靠MessQueue和Looper。Handler的使用想必大家都很了解,一般在开发中,我们会在子线程中执行耗时的操作,将操作的结果通过Handler发送给主线程,主线程用这个结果来执行UI的操作,这是我们最常见的用法,Android默认只有主线程才能更新UI,这是因为每次更新UI时都会做UI验证操作,Android在UI验证操作时首先会检查当前线程是否为主线程,如果不是就会报出异常,那为什么Android要规定只有主线程才能操作UI呢?这是因为Android的UI控件并不是线程安全的,在高并发状态下当有多个线程访问一个UI时就会出错,加锁又会让UI效率变慢。MessageQueue是消息队列,它以队列的形式对外提供插入和删除消息的工作,但其本身的数据结构并不是一个队列而是一个单向链表。Looper可以理解为消息循环处理器,它会以无限循环的方式去查找MessageQueue中的消息,如果没有消息就会一直等待。Android消息队列的Handler、MessageQueue、Looper作为一个整体,不可分割,那么接下来就对这几个模板分开探索一下。

MessageQueue

MessageQueue即消息队列,主要包含插入(enqueueMessage方法)和读取(next方法),读取一条消息的同时也会把这条消息从队列中删除,消息队列由单链表实现,因为单链表对插入和删除操作有很好的优势。

enqueueMessage方法
    boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
synchronized (this) {
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}

if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}

当新消息到来时,将消息插入链表中。

next方法
    Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}

// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}

// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}

if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;

// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}

next方法会一直阻塞,当有消息到来时,next方法会返回这条消息,并把这条消息从MessageQueue中删除。

Looper

Looper是一个消息循环处理器,在一个线程中,要打开一个Looper才能接收到其他线程发来的Message。在一个线程中,Looper本身是默认不存在的,只有子线程会初始化一个Looper,这就是为什么主线程可以默认使用Handler的原因了。 我们一般用Looper.prepare()方法给线程创建一个Looper,然后用Looper.loop()方法开启消息循环,当然Looper也可以退出,有quit()方法和quitSafely()方法,quit()方法会立即退出这个消息循环,而quitSafely()会将消息队列中的消息处理完再退出。子线程在开启消息循环处理完消息之后一定要退出,否则子线程会一直等待下去,消耗资源。

Looper.loop()方法
    public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
if (me.mInLoop) {
Slog.w(TAG, "Loop again would have the queued messages be executed"
+ " before this one completed.");
}

me.mInLoop = true;
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

// Allow overriding a threshold with a system prop. e.g.
// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
final int thresholdOverride =
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
+ ".slow", 0);

boolean slowDeliveryDetected = false;
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}

// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
// Make sure the observer won't change while processing a transaction.
final Observer observer = sObserver;
final long traceTag = me.mTraceTag;
long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
if (thresholdOverride > 0) {
slowDispatchThresholdMs = thresholdOverride;
slowDeliveryThresholdMs = thresholdOverride;
}
final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

final boolean needStartTime = logSlowDelivery || logSlowDispatch;
final boolean needEndTime = logSlowDispatch;

if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
Object token = null;
if (observer != null) {
token = observer.messageDispatchStarting();
}
long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logSlowDelivery) {
if (slowDeliveryDetected) {
if ((dispatchStart - msg.when) <= 10) {
Slog.w(TAG, "Drained");
slowDeliveryDetected = false;
}
} else {
if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
msg)) {
// Once we write a slow delivery log, suppress until the queue drains.
slowDeliveryDetected = true;
}
}
}
if (logSlowDispatch) {
showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
}

if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();
}
}

loop()方法是一个死循环,只有当消息队列next()方法返回null或者quit()方法被调用后才会跳出死循环,loop()会一直调用next()方法。

Handler

Handler的主要工作就是接收和发送消息,消息发送最终会使用send的一系列方法来实现,在平常使用中我们一般会使用sendMessage()方法发送Message,Handler会调用dispatchMessage()方法。

    public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

最后重写handleMessage方法来接收消息。

收起阅读 »

使用Flutter撸一个极简的吃月饼小游戏

先看效果 游戏规则很简单,在月饼投掷出去之后能够砸中月亮即记1分,否则该轮游戏结束,连续击中的次数则为本轮分数。 编码实现 代码上其实没有太多复杂度,大体逻辑如下: 1 月亮的移动动画,使用Tween实现一个补间动画,使用TweenSequence指定动画序...
继续阅读 »

先看效果


2021-09-08 22.17.51.gif


游戏规则很简单,在月饼投掷出去之后能够砸中月亮即记1分,否则该轮游戏结束,连续击中的次数则为本轮分数。


编码实现


代码上其实没有太多复杂度,大体逻辑如下:


1 月亮的移动动画,使用Tween实现一个补间动画,使用TweenSequence指定动画序列,监听动画的完成重复动画,从而实现月亮左右不间断移动的效果。


_animationController =
AnimationController(duration: Duration(milliseconds: 600), vsync: this);
_animationControllerCake =
AnimationController(duration: Duration(milliseconds: 900), vsync: this);
TweenSequenceItem<double> downMarginItem = TweenSequenceItem<double>(
tween: Tween(begin: 1.0, end: 50.0),
weight: 50,
);
TweenSequenceItem<double> upMarginItem = TweenSequenceItem<double>(
tween: Tween(begin: 50.0, end: 300.0),
weight: 100,
);

TweenSequence<double> tweenSequence = TweenSequence<double>([
downMarginItem,
upMarginItem,
]);

_animation = tweenSequence.animate(_animationController);

_animation.addListener(() {
if (_animation.isCompleted) {
_animationController.reverse();
}
if (_animation.isDismissed) {
_animationController.forward();
}
setState(() {});
});

2 月饼的投掷位移效果,使用StreamBuilder控件,配合Timer倒计时,在1秒内不断地Stream发送当前位置数据,从而不断的改变月饼距离底部的距离,看上去就像也是一个位移动画效果,而实际上是不断改变距离形成的视觉效果。


_countdownTimer = new Timer.periodic(new Duration(milliseconds: 1), (timer) {
if (_milliSecond > 0) {
_milliSecond = _milliSecond - 5;
} else {
_countdownTimer.cancel();
}
_cakeStreamController.sink.add(_milliSecond < 0 ? 0 : _milliSecond);
});

Container(
margin: EdgeInsets.only(bottom: distance),
child: Image.asset(
"assets/images/cake.png",
width: 60,
height: 60,
),
)

3 比较关键的一点就是,如何判断月饼投掷出去之后会和月亮发生碰撞,也就是“吃到月饼”。实现方案是:月亮的高度是已知的(屏幕高度 - 状态栏高度 - 月亮距上方距离),月饼的横坐标是固定的(屏幕宽度的一半)。在StreamBuilder中监听:当月饼高度达到月亮的高度时,判断月亮的横坐标是否和月饼一致即可,如果一致则月饼与月亮重合,记为一次有效分数。而这里为了增加游戏的可玩性,并不是很严格的判断坐标完全重合,如图所示,月饼到达月亮高度时,月亮如果在红色区域内都记为有效分数。


image.png


判断逻辑:


// 当月饼高度达到月球所处的高度时,判断月球的位置是否处于中间
if (distance < (_screenHeight - 120) &&
distance > (_screenHeight - 170 - MediaQuery.of(context).padding.top)) {
print(_animation.value);
if (_animation.value < (_screenWidth / 2 + 10) && _animation.value > (_screenWidth / 2 - 90)) {
_hintStreamController.add("太棒了");
print("撞到了");
_score++;
} else {
_hintStreamController.add("MISS");
print("MISS");
}
_milliSecond = 1000;
_cakeStreamController.sink.add(_milliSecond);
_countdownTimer.cancel();
}

引入分数排行榜


为了增加游戏的趣味性,在游戏里面增加了联机的分数排行榜机制,游戏中会将玩家的最高分数上传至服务器,在主界面的右上角可以查看自己在排行榜的位置。


Screenshot_2021-09-08-22-23-35-489_com.flutter.mo.jpg


这里云端服务器存储功能使用的是第三方平台LeanCloud,这个平台是支持Flutter的,而且在8月份刚好增加了对空安全的迭代。


扫码下载链接


目前支持安卓版本的下载,还在等什么,赶快下载登上排行榜吧~


image.png


代码地址


Github链接


由于云端服务器使用了LeanCloud,下载的老铁需要去LeanCloud平台申请AppKey填写在项目中的main.dart中的初始化位置即可。


LeanCloud.initialize(
"", "",
server: "https://zsyju4p5.lc-cn-n1-shared.com", // to use your own custom domain
queryCache: new LCQueryCache() // optinoal, enable cache
)

作者:单总不会亏待你
链接:https://juejin.cn/post/7005585014767222798
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android项目中集成Flutter,实现秒开Flutter模块

本文目标 成功在Android原生项目中集成Flutter Warning 从Flutter v1.1.7版本开始,Flutter module仅支持AndroidX应用 在release模式下Flutter仅支持以下架构:x86_64,armeabi-v7...
继续阅读 »

本文目标


成功在Android原生项目中集成Flutter


Warning



  • 从Flutter v1.1.7版本开始,Flutter module仅支持AndroidX应用

  • 在release模式下Flutter仅支持以下架构:x86_64,armeabi-v7a,arm64-v8a,不支持mips和x86,所以引入Flutter前需要选取Flutter支持的架构


android{
//...
defaultConfig {
//配置支持的动态库类型
ndk {
abiFilters 'x86_64','armeabi-v7a', 'arm64-v8a'
}
}
}

混合开发的一些适用场景



  • 在原有项目中加入Flutter页面


image



  • 原生页面中嵌入Flutter模块


image



  • 在Flutter项目中嵌入原生模块


image


主要步骤



  • 创建Flutter module

  • 为已存在的Android项目添加Flutter module依赖

  • 早Kotlin/Java中调用Flutter module

  • 编写Dart代码

  • 运行项目

  • 热重启/重新加载

  • 调试Dart代码

  • 发布应用


请把所有的项目都放在同一个文件夹内


- WorkProject
- AndroidProject
- iOSProject
- flutrter_module

WorkProject下面分别是原生Android模块,原生iOS模块,flutter模块,并且这三个模块是并列结构


创建Flutter module


在做混合开发之前我们需要创建一个Flutter module
这个时候需要


  cd xxx/WorkProject /

创建flutter_module


flutter create -t module flutter_module

如果要指定包名


flutter create -t module --org com.example flutter_module

然后就会创建成功


image



  • .android - flutter_module的Android宿主工程

  • .ios - flutter_module的iOS宿主工程

  • lib - flutter_module的Dart部分代码

  • pubspec.yaml - flutter_module的项目依赖配置文件
    因为宿主工程的存在,我们这个flutter_module在布甲额外的配置的情况下是可以独立运行的,通过安装了Flutter和Dart插件的AndroidStudio打开这个flutter_module项目,通过运行按钮可以直接运行


构建flutter aar(非必须)


可以通过如下命令构建aar


cd .android/
./gradlew flutter:assembleRelease

这会在.android/Flutter/build/outputs/aar/中生成一个flutter-release.aar归档文件


为已存在的Android用意添加Flutter module依赖


打开我们的Android项目的 settings.gradle添加如下代码


setBinding(new Binding([gradle: this]))                              
evaluate(new File(
settingsDir.parentFile,
'flutter_module/.android/include_flutter.groovy'
))

//可选,主要作用是可以在当前AS的Project下显示flutter_module以方便查看和编写Dart代码
include ':flutter_module'
project(':flutter_module').projectDir = new File('../flutter_module')

setBinding与evaluate允许Flutter模块包括它自己在内的任何Flutter插件,在setting.gradle中以类似:flutter package_info :video_player的方式存在


添加:flutter依赖


dependencies {
implementation project(':flutter')
}

添加Java8编译选项


因为Flutter的Android engine使用了Java8的特性,所有在引入Flutter时需要配置你的项目的Java8编译选项


//在app的build.gradle文件的android{}节点下添加
android {
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}

在Kotlin中调用Flutter module


支持,我们已经为我们的Android项目添加了Flutter所必须的依赖,接下来我们来看如何在项目中以Kotlin的方式在Fragment中调用Flutter模块,在这里我们能做到让Flutter优化提升加载速度,实现秒开Flutter模块


原生Kotlin端代码


/**
* flutter抽象的基类fragment,具体的业务类fragment可以继承
**/
abstract class FlutterFragment(moduleName: String) : IBaseFragment() {

private val flutterEngine: FlutterEngine?
private lateinit var flutterView: FlutterView

init {
flutterEngine =FlutterCacheManager.instance!!.getCachedFlutterEngine(AppGlobals.get(), moduleName)
}

override fun getLayoutId(): Int {
return R.layout.fragment_flutter
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(mLayoutView as ViewGroup).addView(createFlutterView(activity!!))
}

private fun createFlutterView(context: Context): FlutterView {
val flutterTextureView = FlutterTextureView(activity!!)
flutterView = FlutterView(context, flutterTextureView)
return flutterView
}

/**
* 设置标题
*/
fun setTitle(titleStr: String) {
rl_title.visibility = View.VISIBLE
title_line.visibility = View.VISIBLE
title.text = titleStr
title.setOnClickListener {

}
}

/**
* 生命周期告知flutter
*/
override fun onStart() {
flutterView.attachToFlutterEngine(flutterEngine!!)
super.onStart()
}

override fun onResume() {
super.onResume()
//for flutter >= v1.17
flutterEngine!!.lifecycleChannel.appIsResumed()
}

override fun onPause() {
super.onPause()
flutterEngine!!.lifecycleChannel.appIsInactive()
}

override fun onStop() {
super.onStop()
flutterEngine!!.lifecycleChannel.appIsPaused()
}

override fun onDetach() {
super.onDetach()
flutterEngine!!.lifecycleChannel.appIsDetached()
}

override fun onDestroy() {
super.onDestroy()
flutterView.detachFromFlutterEngine()
}
}

R.layout.fragment_flutter的布局


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<RelativeLayout
android:id="@+id/rl_title"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_45"
android:background="@color/color_white"
android:gravity="center_vertical"
android:orientation="horizontal">

<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:layout_gravity="center"
android:gravity="center"
android:textColor="@color/color_000"
android:textSize="16sp" />
</RelativeLayout>

<View
android:id="@+id/title_line"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="2px"
android:background="@color/color_eee" />
</LinearLayout>

/**
* flutter缓存管理,主要是管理多个flutter引擎
**/
class FlutterCacheManager private constructor() {

/**
* 伴生对象,保持单例
*/
companion object {

//喜欢页面,默认是flutter启动的主入口
const val MODULE_NAME_FAVORITE = "main"
//推荐页面
const val MODULE_NAME_RECOMMEND = "recommend"

@JvmStatic
@get:Synchronized
var instance: FlutterCacheManager? = null
get() {
if (field == null) {
field = FlutterCacheManager()
}
return field
}
private set
}

/**
* 空闲时候预加载Flutter
*/
fun preLoad(context: Context){
//在线程空闲时执行预加载任务
Looper.myQueue().addIdleHandler {
initFlutterEngine(context, MODULE_NAME_FAVORITE)
initFlutterEngine(context, MODULE_NAME_RECOMMEND)
false
}
}

/**
* 初始化Flutter
*/
private fun initFlutterEngine(context: Context, moduleName: String): FlutterEngine {
//flutter 引擎
val flutterLoader: FlutterLoader = FlutterInjector.instance().flutterLoader()
val flutterEngine = FlutterEngine(context,flutterLoader, FlutterJNI())
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint(
flutterLoader.findAppBundlePath(),
moduleName
)
)
//存到引擎缓存中
FlutterEngineCache.getInstance().put(moduleName,flutterEngine)
return flutterEngine
}

/**
* 获取缓存的flutterEngine
*/
fun getCachedFlutterEngine(context: Context?, moduleName: String):FlutterEngine{
var flutterEngine = FlutterEngineCache.getInstance()[moduleName]
if(flutterEngine==null && context!=null){
flutterEngine=initFlutterEngine(context,moduleName)
}
return flutterEngine!!
}

}

具体业务类使用


//在app初始化中初始一下
public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
FlutterCacheManager.getInstance().preLoad(this);
}
}

收藏页面


class FavoriteFragment : FlutterFragment(FlutterCacheManager.MODULE_NAME_FAVORITE) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setTitle(getString(R.string.title_favorite))
}
}

推荐页面


class RecommendFragment : FlutterFragment(FlutterCacheManager.MODULE_NAME_RECOMMEND) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setTitle(getString(R.string.title_recommend))
}
}

Dart端代码


import 'package:flutter/material.dart';
import 'package:flutter_module/favorite_page.dart';
import 'package:flutter_module/recommend_page.dart';

//至少要有一个入口,而且这下面的man() 和 recommend()函数名字 要和FlutterCacheManager中定义的对应上
void main() => runApp(MyApp(FavoritePage()));

//必须加注解
@pragma('vm:entry-point')
void recommend() => runApp(MyApp(RecommendPage()));

class MyApp extends StatelessWidget {
final Widget page;
const MyApp(this.page);

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: page,
),
);
}
}

Dart侧收藏页面


import 'package:flutter/material.dart';

class FavoritePage extends StatefulWidget {
@override
_FavoritePageState createState() => _FavoritePageState();
}

class _FavoritePageState extends State<FavoritePage> {
@override
Widget build(BuildContext context) {
return Container(
child: Text("收藏"),
);
}
}

Dart侧推荐页面


import 'package:flutter/material.dart';

class RecommendPage extends StatefulWidget {
@override
_RecommendPageState createState() => _RecommendPageState();
}

class _RecommendPageState extends State<RecommendPage> {
@override
Widget build(BuildContext context) {
return Container(
child: Text("推荐"),
);
}
}

最终效果


image


image


作者:Coolbreeze
链接:https://juejin.cn/post/7005562586843906085
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »