注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

ARouter 拦截器之多 module 独立运行

本文说明上篇文章 已分享了路由配置、跳转、原理、完整的效果演示gif以及源码,而且是多 module 项目演示的,算是路由 ARouter 的入门,还没配置使用的可以先去看看。本文的内容主要涉及如下两个:路由拦截器使用module 独立运行前者在我们...
继续阅读 »

本文说明

上篇文章 已分享了路由配置、跳转、原理、完整的效果演示gif以及源码,而且是多 module 项目演示的,算是路由 ARouter 的入门,还没配置使用的可以先去看看。

本文的内容主要涉及如下两个:

  • 路由拦截器使用
  • module 独立运行

前者在我们开发中有这样一种应用场景,默认用户不登录可以浏览一部分页面,当点击部分页面的时候就需要先去登录,也就是跳转到登录页面,普通的做法是根据需求挨个去做点击事件,这就很麻烦,如果需要跳转登录的时候传递参数啥的,那就改动超级大了;而路由ARouter的拦截器功能就很好的解决了这个问题,还支持自定义拦截器,使用起来很灵活。

后者的使用场景适合项目大,多人开发的情景,这样可以各自负责一个模块,独立调试运行,利于项目管理以及代码的维护。这块在上一篇文章的前提下还需要额外配置,本文会讲。

module 独立运行

先来看看module独立运行,然后我们在各个模块做一个模拟的跳转页面需要验证登录的示例,这样比较清晰。

第一步:配置 gradle.properties

gradle.properties 文件中添加如下代码

#是否需要单独运行某个模块 true:表示某个模块不作为依赖库使用
isSingleCircleModule=true
#isSingleCircleModule=false
isSingleHomeModule=true
#isSingleHomeModule=false

第二步:配置app下的build.gradle

在app下的build.gradle文件配置

if (!isSingleCircleModule.toBoolean()) {
implementation project(path: ':circle')
}
if (!isSingleHomeModule.toBoolean()){
implementation project(path: ':home')
}

并注释掉原来的依赖

//    implementation project(path: ':circle')
// implementation project(path: ':home')

第三步:配置各独立模块下的build.gradle

circle模块下build.gradle文件最顶部改动如下:

//plugins {
// id 'com.android.library'
//}

if (isSingleCircleModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}

home模块下build.gradle文件最顶部改动如下:

//plugins {
// id 'com.android.library'
//}

if (isSingleHomeModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}

第四步:看效果

上面的配置完成后,点击Sync Project with Gradle Files 等待编译完成,可看到如下状态:

Select Run弹窗

这个时候我们选择其中一个module运行,会发现报错如下:

Could not identify launch activity: Default Activity not found
Error while Launching activity

很明显,我们都知道Android程序的主入口是从清单文件配置的,但我们的各module都还没有做这个工作。

circle模块下的清单文件中,配置如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gs.circle">


<application
android:allowBackup="true"
android:icon="@mipmap/app_icon"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<activity android:name=".CircleActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

其中的iconlabel以及theme都可以定义在baselib中,这样我们任何 module 配置的时候就可以直接引用,而无需各自复制一份了;除此之外,values文件夹下的东西都可以移动到baselib下,方便其他模块引用,这也就是baselib模块的作用,如果你要细分,还可以j将公共资源放在一个独立的模块里,这个模块通常叫做:commonlib,具体情况而定。

配置完清单文件,运行后发现桌面会多出来一个 APP icon,打开只有一个页面,就是我们的circlemodule的主页面。home模块的清单配置就不展示了,下面看下效果:

module独立运行

这个时候再切回去运行app模块,如果发现有问题,先卸载再运行就ok了。但是会有一个问题,原来可以跳转其他模块的功能,现在跳转不了了,这其实很正常,因为在组件化开发模式下,每个 module 都是独立的app,所以肯定不能直接跳转过去。

那如何实现正常跳转呢?

需要两步,将gradle.properties中的代码修改为如下:

#isSingleCircleModule=true
isSingleCircleModule=false
#isSingleHomeModule=true
isSingleHomeModule=false

接着将circlehome模块的清单文件中的 application属性和默认启动配置项删掉,然后再运行就 ok 了。

如果想将其中一个作为依赖库使用,那么就指匠情挑设置为false即可。

关于组件之间 AndroidManifest 合并问题

其实这个可以在正式打包的时候,注释掉module中的相关代码即可,毕竟是在组件模式。那有没有办法解决每次都要注释的问题呢?答案是yes.

大致思路如下:

在可独立运行的module的res->main文件夹下新建一个文件夹(命名自定义),然后将对应的清单文件复制一份,名称不需要修改,内容的差别就是前面提到的,去掉application属性和默认启动配置项。

接着在对应 module 的 build.gradle 中指定表单的路径,代码如下:

sourceSets {
main {
if (isSingleCircleModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}

这样在不同的开发模式下就会读取到不同的 AndroidManifest.xml ,然后我们需要修改这两个表单的内容以为我们不同的开发模式服务。

单模块独立运行小结

优点:

  • 项目耦合度低,开发效率高,出现问题易排查
  • 利于项目进度管理,分工明确
  • 适合多人大项目

缺点:

  • 前期配置比较复杂,开发过程中需要修改部分配置
  • 稳定性不好把握,毕竟不是google官方出的框架,后期出问题不好处理

其实还有很多问题,实践过的应该明白,每个项目都有自己的独特之处,会有各种各样的奇怪问题,但一般网上我们都可以找到解决方案。

路由拦截器使用

首先还是需要添加几个配置,在工程下的build.gradle文件中添加下面这行代码:

classpath 'com.alibaba:arouter-register:1.0.2'

app模块的build.gradle文件下,配置改动如下:

plugins {
id 'com.android.application'
id 'com.alibaba.arouter' // 拦截器必须配置
}

配置完这两步,按照惯例,该是编译了。

为了演示,我这里在app下新建一个名为LoginActivity的页面,作业登录拦截后跳转的页面,页面内容只有一个提示文本,这里补贴代码。

然后分别在宿主模块app、功能模块circlehome中去做跳转登录页面,看看我们的拦截器是否起到了拦截作用,下面开始定义拦截器。

要独立运行某个模块,这里就不再赘述了,大家自行修改配置即可。

拦截器完整代码如下:

/**
* Description: 登录拦截器
* Date: 2021/10/9 10:42
* <p>
* 拦截器会在跳转之间执行,多个拦截器会按优先级顺序依次执行
* * <p>
* * priority 数值越小权限越高
*/

@Interceptor(priority = 2, name = "登录ARouter拦截器")
public class LoginInterceptor implements IInterceptor {

private Context mContext;

@Override
public void process(Postcard postcard, InterceptorCallback callback) {
boolean isLogin = mContext.getSharedPreferences("arouterdata", mContext.MODE_PRIVATE).getBoolean("isLogin", false);
if (isLogin) {
callback.onContinue(postcard);
} else {
switch (postcard.getPath()) {
// 需要登录的拦截下来
case ARouterPath.APP_MY_INFO:
ARouter.getInstance().build(ARouterPath.LOGIN_PAGE).with(postcard.getExtras()).navigation();
break;
default:
callback.onContinue(postcard);
break;
}
}
}

/**
* 拦截器的初始化,会在sdk初始化的时候调用该方法,仅会调用一次
*
* @param context
*/

@Override
public void init(Context context) {
mContext = context;
}

}

拦截器初始化需要重新安装才会生效,这点要注意。拦截器是不需要我们手动显示调用的,而是框架通过注解来使用的,所以我们只需要写好逻辑代码即可。

以上代码可以实现模块内和跨模块跳转拦截,本地的登录状态我这里没有处理逻辑,所以每次都会被拦截到。下面看效果:

拦截器效果

演示效果模拟进入MyInfoActivity页面时需要先登录,分别从三个模块做了跳转演示。

总结

组件化module独立运行与合并操作起来相对繁琐一点,但优点也很明显。路由框架ARouter的拦截器使用起来就很简单了,其实拦截器完全可以在学完上一篇之后,直接使用,如果组件化多模块独立运行实际项目使用不到,可以先跳过,简单了解流程即可。

Android的框架演变也很快,“三化技术”在两年前特别火,几乎大家都在讨论,但并没有持续多长时间就被新出的技术替代了,而作为一个开发者,自己需要掌握一个基本技能:从零开始搭建一个项目框架,并且这个框架尽可能的要跟上项目的持续发展

收起阅读 »

Android Jetpack系列之Lifecycle

Lifecycle介绍Lifecycle可以让某一个类变成Activity、Fragment的生命周期观察者类,监听其生命周期的变化并可以做出响应。Lifecycle使得代码更有条理性、精简、易于维护。Lifecycle中主要有两个角色:LifecycleOw...
继续阅读 »

Lifecycle介绍

Lifecycle可以让某一个类变成ActivityFragment的生命周期观察者类,监听其生命周期的变化并可以做出响应。Lifecycle使得代码更有条理性、精简、易于维护。

Lifecycle中主要有两个角色:

  • LifecycleOwner: 生命周期拥有者,如Activity/Fragment等类都实现了该接口并通过getLifecycle()获得Lifecycle,进而可通过addObserver()添加观察者。
  • LifecycleObserver: 生命周期观察者,实现该接口后就可以添加到Lifecycle中,从而在被观察者类生命周期发生改变时能马上收到通知。

实现LifecycleOwner的生命周期拥有者可与实现LifecycleObserver的观察者完美配合。

场景case

假设我们有一个在屏幕上显示设备位置的 Activity,我们可能会像下面这样实现:

internal class MyLocationListener(
private val context: Context,
private val callback: (Location) -> Unit) {

fun start() {
// connect to system location service
}

fun stop() {
// disconnect from system location service
}
}

class MyActivity : AppCompatActivity() {
private lateinit var myLocationListener: MyLocationListener

override fun onCreate(...) {
myLocationListener = MyLocationListener(this) { location ->
// update UI
}
}

public override fun onStart() {
super.onStart()
myLocationListener.start()
// manage other components that need to respond
// to the activity lifecycle
}

public override fun onStop() {
super.onStop()
myLocationListener.stop()
// manage other components that need to respond
// to the activity lifecycle
}
}

注:上面代码来自官方示例~

我们可以在Activity 或 Fragment 的生命周期方法(示例中的onStart/onStop)中直接对依赖组件进行操作。但是,这样会导致代码条理性很差且不易扩展。那么有了Lifecycle,可以将依赖组件的代码从Activity/Fragment生命周期方法中移入组件本身中。

Lifecycle使用

根目录下build.gradle:

allprojects {
repositories {
google()

// Gradle小于4.1时,使用下面的声明替换:
// maven {
// url 'https://maven.google.com'
// }
// An alternative URL is 'https://dl.google.com/dl/android/maven2/'
}
}

app下的build.gradle:

    dependencies {
def lifecycle_version = "2.3.1"
def arch_version = "2.1.0"

// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"

// Saved state module for ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"

// Annotation processor
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
// 可选 - 如果使用Java8,使用下面这个代替lifecycle-compiler
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"

// 可选 - 在Service中使用Lifecycle
implementation "androidx.lifecycle:lifecycle-service:$lifecycle_version"

// 可选 - Application中使用Lifecycle
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"

// 可选 - ReactiveStreams support for LiveData
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version"

// 可选 - Test helpers for LiveData
testImplementation "androidx.arch.core:core-testing:$arch_version"
}

Activity/Fragment中使用Lifecycle

首先先来实现LifecycleObserver观察者:

open class MyLifeCycleObserver : LifecycleObserver {

@OnLifecycleEvent(value = Lifecycle.Event.ON_START)
fun connect(owner: LifecycleOwner) {
Log.e(JConsts.LIFE_TAG, "Lifecycle.Event.ON_CREATE:connect")
}

@OnLifecycleEvent(value = Lifecycle.Event.ON_STOP)
fun disConnect() {
Log.e(JConsts.LIFE_TAG, "Lifecycle.Event.ON_DESTROY:disConnect")
}
}

在方法上,我们使用了@OnLifecycleEvent注解,并传入了一种生命周期事件,其类型可以为ON_CREATEON_STARTON_RESUMEON_PAUSEON_STOPON_DESTROYON_ANY中的一种。其中前6个对应Activity中对应生命周期的回调,最后一个ON_ANY可以匹配任何生命周期回调。 所以,上述代码中的connect()、disConnect()方法分别应该在ActivityonStart()、onStop()中触发时执行。接着来实现我们的Activity:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.e(JConsts.LIFE_TAG, "$ACTIVITY:onCreate")

//添加LifecycleObserver观察者
lifecycle.addObserver(MyLifeCycleObserver())
}

override fun onStart() {
Log.e(JConsts.LIFE_TAG, "$ACTIVITY:onStart")
super.onStart()
}

override fun onResume() {
Log.e(JConsts.LIFE_TAG, "$ACTIVITY:onResume")
super.onResume()
}

override fun onPause() {
Log.e(JConsts.LIFE_TAG, "$ACTIVITY:onPause")
super.onPause()
}

override fun onStop() {
Log.e(JConsts.LIFE_TAG, "$ACTIVITY:onStop")
super.onStop()
}

override fun onDestroy() {
Log.e(JConsts.LIFE_TAG, "$ACTIVITY:onDestroy")
super.onDestroy()
}
}

可以看到在Activity中我们只是在onCreate()中添加了这么一行代码:

lifecycle.addObserver(MyLifeCycleObserver())

其中getLifecycle()LifecycleOwner中的方法,返回的是Lifecycle对象,并通过addObserver()的方式添加了我们的生命周期观察者。接下来看执行结果,启动Activity:

2021-06-30 20:57:58.038 11257-11257/ E/Lifecycle_Study: ACTIVITY:onCreate

//onStart() 传递到 MyLifeCycleObserver: connect()
2021-06-30 20:57:58.048 11257-11257/ E/Lifecycle_Study: ACTIVITY:onStart
2021-06-30 20:57:58.049 11257-11257/ E/Lifecycle_Study: Lifecycle.Event.ON_START:connect

2021-06-30 20:57:58.057 11257-11257/ E/Lifecycle_Study: ACTIVITY:onResume

关闭Activity:

2021-06-30 20:58:02.646 11257-11257/ E/Lifecycle_Study: ACTIVITY:onPause

//onStop() 传递到 MyLifeCycleObserver: disConnect()
2021-06-30 20:58:03.149 11257-11257/ E/Lifecycle_Study: ACTIVITY:onStop
2021-06-30 20:58:03.161 11257-11257/ E/Lifecycle_Study: Lifecycle.Event.ON_STOP:disConnect

2021-06-30 20:58:03.169 11257-11257/ E/Lifecycle_Study: ACTIVITY:onDestroy

可以看到我们的MyLifeCycleObserver中的connect()/disconnect()方法的确是分别在ActivityonStart()/onStop()回调时执行的。

自定义LifecycleOwner

AndroidX中的Activity、Fragmen实现了LifecycleOwner,通过getLifecycle()能获取到Lifecycle实例(Lifecycle是抽象类,实例化的是子类LifecycleRegistry)。

public interface LifecycleOwner {
@NonNull
Lifecycle getLifecycle();
}

public class LifecycleRegistry extends Lifecycle {

}

如果我们想让一个自定义类成为LifecycleOwner,可以直接实现LifecycleOwner

class CustomLifeCycleOwner : LifecycleOwner {
private lateinit var registry: LifecycleRegistry

fun init() {
registry = LifecycleRegistry(this)
//通过setCurrentState来完成生命周期的传递
registry.currentState = Lifecycle.State.CREATED
}

fun onStart() {
registry.currentState = Lifecycle.State.STARTED
}

fun onResume() {
registry.currentState = Lifecycle.State.RESUMED
}

fun onPause() {
registry.currentState = Lifecycle.State.STARTED
}

fun onStop() {
registry.currentState = Lifecycle.State.CREATED
}

fun onDestroy() {
registry.currentState = Lifecycle.State.DESTROYED
}

override fun getLifecycle(): Lifecycle {
//返回LifecycleRegistry实例
return registry
}
}

首先我们的自定义类实现了接口LifecycleOwner,并在getLifecycle()返回LifecycleRegistry实例,接下来就可以通过LifecycleRegistry#setCurrentState来传递生命周期状态了。到目前为止,已经完成了大部分工作,最后也是需要去添加LifecycleObserver:

可以看到getLifecycle()返回的是LifecycleRegistry实例,并且在onSaveInstanceState()中分发了Lifecycle.State.CREATED状态,但是其他生命周期回调中并没有写了呀,嗯哼?再细看一下,onCreate()中有个ReportFragment.injectIfNeededIn(this),直接进去看看:

ObserverWithState#dispatchEvent()中调用了mLifecycleObserver.onStateChanged(),这个mLifecycleObserverLifecycleEventObserver类型(父类是接口LifecycleObserver ),在构造方法中通过Lifecycling.lifecycleEventObserver()创建的,最终返回的是ReflectiveGenericLifecycleObserver

//ReflectiveGenericLifecycleObserver.java
class ReflectiveGenericLifecycleObserver implements LifecycleEventObserver {
private final Object mWrapped;
private final CallbackInfo mInfo;

ReflectiveGenericLifecycleObserver(Object wrapped) {
mWrapped = wrapped;
mInfo = ClassesInfoCache.sInstance.getInfo(mWrapped.getClass());
}

@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Event event) {
mInfo.invokeCallbacks(source, event, mWrapped);
}
}

ClassesInfoCache内部存了所有观察者的回调信息,CallbackInfo是当前观察者的回调信息。getInfo()中如果从内存mCallbackMap中有对应回调信息,直接返回;否则通过createInfo()内部解析注解OnLifecycleEvent对应的方法并最终生成CallbackInfo返回。

//ClassesInfoCache.java
CallbackInfo getInfo(Class<?> klass) {

CallbackInfo existing = mCallbackMap.get(klass);
if (existing != null) {
return existing;
}
existing = createInfo(klass, null);
return existing;
}

private void verifyAndPutHandler(Map<MethodReference, Lifecycle.Event> handlers,
MethodReference newHandler, Lifecycle.Event newEvent, Class<?> klass) {
Lifecycle.Event event = handlers.get(newHandler);
if (event == null) {
handlers.put(newHandler, newEvent);
}
}

private CallbackInfo createInfo(Class<?> klass, @Nullable Method[] declaredMethods) {
Class<?> superclass = klass.getSuperclass();
Map<MethodReference, Lifecycle.Event> handlerToEvent = new HashMap<>();
if (superclass != null) {
CallbackInfo superInfo = getInfo(superclass);
if (superInfo != null) {
handlerToEvent.putAll(superInfo.mHandlerToEvent);
}
}

Class<?>[] interfaces = klass.getInterfaces();
for (Class<?> intrfc : interfaces) {
for (Map.Entry<MethodReference, Lifecycle.Event> entry : getInfo(
intrfc).mHandlerToEvent.entrySet()) {
verifyAndPutHandler(handlerToEvent, entry.getKey(), entry.getValue(), klass);
}
}

Method[] methods = declaredMethods != null ? declaredMethods : getDeclaredMethods(klass);
boolean hasLifecycleMethods = false;
//遍历寻找OnLifecycleEvent注解对应的方法
for (Method method : methods) {
OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class);
if (annotation == null) {
continue;
}
hasLifecycleMethods = true;
Class<?>[] params = method.getParameterTypes();
int callType = CALL_TYPE_NO_ARG;
if (params.length > 0) {
callType = CALL_TYPE_PROVIDER;
//第一个方法参数必须是LifecycleOwner
if (!params[0].isAssignableFrom(LifecycleOwner.class)) {
throw new IllegalArgumentException(
"invalid parameter type. Must be one and instanceof LifecycleOwner");
}
}
Lifecycle.Event event = annotation.value();

if (params.length > 1) {
callType = CALL_TYPE_PROVIDER_WITH_EVENT;
//第2个参数必须是Lifecycle.Event
if (!params[1].isAssignableFrom(Lifecycle.Event.class)) {
throw new IllegalArgumentException(
"invalid parameter type. second arg must be an event");
}
//当有2个参数时,注解必须是Lifecycle.Event.ON_ANY
if (event != Lifecycle.Event.ON_ANY) {
throw new IllegalArgumentException(
"Second arg is supported only for ON_ANY value");
}
}
if (params.length > 2) {
throw new IllegalArgumentException("cannot have more than 2 params");
}
MethodReference methodReference = new MethodReference(callType, method);
verifyAndPutHandler(handlerToEvent, methodReference, event, klass);
}
CallbackInfo info = new CallbackInfo(handlerToEvent);
mCallbackMap.put(klass, info);
mHasLifecycleMethods.put(klass, hasLifecycleMethods);
return info;
}

//CallbackInfo.java
static class CallbackInfo {
final Map<Lifecycle.Event, List<MethodReference>> mEventToHandlers;
final Map<MethodReference, Lifecycle.Event> mHandlerToEvent;

CallbackInfo(Map<MethodReference, Lifecycle.Event> handlerToEvent) {
mHandlerToEvent = handlerToEvent;
mEventToHandlers = new HashMap<>();
for (Map.Entry<MethodReference, Lifecycle.Event> entry : handlerToEvent.entrySet()) {
Lifecycle.Event event = entry.getValue();
List<MethodReference> methodReferences = mEventToHandlers.get(event);
if (methodReferences == null) {
methodReferences = new ArrayList<>();
mEventToHandlers.put(event, methodReferences);
}
methodReferences.add(entry.getKey());
}
}

void invokeCallbacks(LifecycleOwner source, Lifecycle.Event event, Object target) {
invokeMethodsForEvent(mEventToHandlers.get(event), source, event, target);
invokeMethodsForEvent(mEventToHandlers.get(Lifecycle.Event.ON_ANY), source, event,
target);
}

private static void invokeMethodsForEvent(List<MethodReference> handlers,
LifecycleOwner source, Lifecycle.Event event, Object mWrapped) {
if (handlers != null) {
for (int i = handlers.size() - 1; i >= 0; i--) {
handlers.get(i).invokeCallback(source, event, mWrapped);
}
}
}

最终调用到了MethodReference#invokeCallback()

//MethodReference.java
static class MethodReference {
final int mCallType;
final Method mMethod;

MethodReference(int callType, Method method) {
mCallType = callType;
mMethod = method;
mMethod.setAccessible(true);
}

void invokeCallback(LifecycleOwner source, Lifecycle.Event event, Object target) {
//noinspection TryWithIdenticalCatches
try {
//OnLifecycleEvent注解对应的方法入参
switch (mCallType) {
//没有参数
case CALL_TYPE_NO_ARG:
mMethod.invoke(target);
break;
//一个参数:LifecycleOwner
case CALL_TYPE_PROVIDER:
mMethod.invoke(target, source);
break;
//两个参数:LifecycleOwner,Event
case CALL_TYPE_PROVIDER_WITH_EVENT:
mMethod.invoke(target, source, event);
break;
}
} catch (InvocationTargetException e) {
throw new RuntimeException("Failed to call observer method", e.getCause());
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}

MethodReference that = (MethodReference) o;
return mCallType == that.mCallType && mMethod.getName().equals(that.mMethod.getName());
}

@Override
public int hashCode() {
return 31 * mCallType + mMethod.getName().hashCode();
}
}

根据不同入参个数通过反射来初始化并执行观察者相应方法,整个流程就从LifecycleOwner中的生命周期Event传到了LifecycleObserver中对应的方法。到这里整个流程就差不多结束了,最后是LifecycleOwner的子类LifecycleRegistry添加观察者的过程:

//LifecycleRegistry.java
@Override
public void addObserver(@NonNull LifecycleObserver observer) {
State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
//key是LifecycleObserver,value是ObserverWithState
ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);
//如果已经存在,直接返回
if (previous != null) {
return;
}
LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
if (lifecycleOwner == null) {
// it is null we should be destroyed. Fallback quickly
return;
}

boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
//目标State
State targetState = calculateTargetState(observer);
mAddingObserverCounter++;
//循环遍历,将目标State连续同步到Observer中
while ((statefulObserver.mState.compareTo(targetState) < 0
&& mObserverMap.contains(observer))) {
pushParentState(statefulObserver.mState);
statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
popParentState();
// mState / subling may have been changed recalculate
targetState = calculateTargetState(observer);
}

if (!isReentrance) {
// we do sync only on the top level.
sync();
}
mAddingObserverCounter--;
}

private State calculateTargetState(LifecycleObserver observer) {
Entry<LifecycleObserver, ObserverWithState> previous = mObserverMap.ceil(observer);

State siblingState = previous != null ? previous.getValue().mState : null;
State parentState = !mParentStates.isEmpty() ? mParentStates.get(mParentStates.size() - 1)
: null;
return min(min(mState, siblingState), parentState);
}

添加观察者,并通过while循环,将最新的State状态连续同步到Observer中,虽然可能添加ObserverLifecyleOwner分发事件晚,但是依然能收到所有事件,类似于事件总线的粘性事件。最后画一下整体的类图关系: Lifecycle.png


收起阅读 »

做一个透明的Dialog Activity

做一个透明的Dialog Activity平时在很多软件中,肯定见到过从底部的弹窗,比如分享某个文件,从底部弹出的分享平台,大部分是通过PopupWindow 底部弹出实现,这次来讲一个不一样的。1. 什么是 Dialog Activity让Acti...
继续阅读 »

做一个透明的Dialog Activity

平时在很多软件中,肯定见到过从底部的弹窗,比如分享某个文件,从底部弹出的分享平台,大部分是通过PopupWindow 底部弹出实现,这次来讲一个不一样的。

1.png

1. 什么是 Dialog Activity

让Activity拥有和Dialog一样的效果,背景虚化,悬浮效果。

2. 为什么要使用 Dialog Activity

有时候我们需要在弹窗中去做复杂的逻辑,这就导致Dialog很臃肿,而拥有Dialog样式的Activity可以像我们写UI一样,使用架构去对复杂的逻辑进行层次划分,这样在逻辑上会清洗一些,在页面的声明周期上也更方便管理一些。

3. 怎么实现 Dialog Activity

3.1 写一个样式

<style name="Theme.ActivityDialogStyle" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowCloseOnTouchOutside">false</item>
<item name="android:windowIsFloating">true</item>
</style>

其中的含义
windowIsTranslucent: 是否半透明
windowBackground: 设置dialog的背景
backgroundDimEnabled:背景是否模糊显示
windowContentOverlay:设置窗口内容不覆盖
windowCloseOnTouchOutside:
windowIsFloating:是否浮现在activity之上,这个属性很重要,设置为true之后,Activty的状态栏才会消失。

3.2 引用样式

 <activity
android:name=".MainActivity2"
android:theme="@style/Theme.ActivityDialogStyle" />

注意:activity必须要继承 AppCompatActivity。

3.3 可配置选项

如果需要设置圆角背景

在onCreate添加

getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));

如果想让该页面填满屏幕,大家知道Dialog默认是不填满的。 在onCreate添加

 Window window = getWindow();
// 把 DecorView 的默认 padding 取消,同时 DecorView 的默认大小也会取消
window.getDecorView().setPadding(0, 0, 0, 0);
WindowManager.LayoutParams layoutParams = window.getAttributes();
// 设置宽度
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
window.setAttributes(layoutParams);
// 给 DecorView 设置背景颜色,很重要,不然导致 Dialog 内容显示不全,有一部分内容会充当 padding,上面例子有举出
// window.getDecorView().setBackgroundColor(Color.GREEN);

3.4 踩坑

实际上整个的过程十分简单,但是诡异的事情发生了。我写了一个布局,是线性布局,它并没有什么问题,却一直无法正常显示。去掉 windowIsFloating 属性就好了,但windowIsFloating会造成dialog Activity存在状态栏。 最终通过修改布局解决,先看看第一个布局和效果。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity2">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:background="@color/white" />

</LinearLayout>

可以看到这个Activity是显示了,因为背景有虚化,但layout_weight 为2的白色却没有显示。

2.png

修改后的布局(使用相对布局作为根布局):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity2">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:background="@color/white" />
</LinearLayout>
</RelativeLayout>

3.png

这样才正常显示,虽然解决了,但是原因真的好迷。

收起阅读 »

Java多线程2 多个线程之间共享数据

线程范围的共享变量多个业务模块针对同一个static变量的操作 要保证在不同线程中 各模块操作的是自身对应的变量对象public class ThreadScopeSharaData { private static int data = 0 ; ...
继续阅读 »

线程范围的共享变量

多个业务模块针对同一个static变量的操作 要保证在不同线程中 各模块操作的是自身对应的变量对象


public class ThreadScopeSharaData {

private static int data = 0 ;

public static void main(String[] args) {
for(int i = 0 ;i<2 ;i++){
new Thread(new Runnable(){

@Override
public void run() {
data = new Random().nextInt();
System.out.println(Thread.currentThread().getName()+ " put random data:"+data);
new A().get() ;
new B().get() ;
}

}).start() ;
}

}

static class A {
public int get(){
System.out.println("A from " + Thread.currentThread().getName()
+ " get data :" + data);
return data ;
}
}

static class B{
public int get(){
System.out.println("B from " + Thread.currentThread().getName()
+ " get data :" + data);
return data ;
}
}
}

模块A ,B都需要访问static的变量data 在线程0中会随机生成一个data值 假设为10 那么此时模块A和模块B在线程0中得到的data的值为10 ;在线程1中 假设会为data赋值为20 那么在当前线程下

模块A和模块B得到data的值应该为20

看程序执行的结果:


Thread-0 put random data:-2009009251
Thread-1 put random data:-2009009251
A from Thread-0 get data :-2009009251
A from Thread-1 get data :-2009009251
B from Thread-0 get data :-2009009251
B from Thread-1 get data :-2009009251

Thread-0 put random data:-2045829602
Thread-1 put random data:-1842611697
A from Thread-0 get data :-1842611697
A from Thread-1 get data :-1842611697
B from Thread-0 get data :-1842611697
B from Thread-1 get data :-1842611697

会出现两种情况

1.由于线程执行速度,新的随机值将就的随机值覆盖 data 值一样
2.data 值不一样,但 A、B线程都

1.使用Map实现线程范围内数据的共享

可是将data数据和当前允许的线程绑定在一块,在模块A和模块B去获取数据data的时候 是通过当前所属的线程去取得data的结果就行了。
声明一个Map集合 集合的Key为Thread 存储当前所属线程 Value 保存data的值,代码如下:


public class ThreadScopeSharaData {


private static Map<Thread, Integer> threadData = new HashMap<>();

public static void main(String[] args) {

for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int data = new Random().nextInt();
System.out.println(Thread.currentThread().getName() + " put random data:" + data);
threadData.put(Thread.currentThread(), data);
new A().get();
new B().get();

}
}).start();

}

}

static class A {
public void get() {
int data = threadData.get(Thread.currentThread());

System.out.println("A from " + Thread.currentThread().getName() + " get data:" + data);

}
}

static class B {
public void get() {
int data = threadData.get(Thread.currentThread());
System.out.println("B from " + Thread.currentThread().getName() + " get data:" + data);

}
}
}

Thread-0 put random data:-123490895
Thread-1 put random data:-1060992440
A from Thread-0 get data:-123490895
A from Thread-1 get data:-1060992440
B from Thread-0 get data:-123490895
B from Thread-1 get data:-1060992440
2.ThreadLocal实现线程范围内数据的共享

(1)订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码分别位于不同的模块类中。\

(2)银行转账包含一系列操作: 把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同的帐户对象的方法。\

(3)例如Strut2的ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每个线程各自的状态和数据,对于不同的线程来说,getContext方法拿到的对象都不相同,对同一个线程来说,不管调用getContext方法多少次和在哪个模块中getContext方法,拿到的都是同一个。\

4.实验案例:定义一个全局共享的ThreadLocal变量,然后启动多个线程向该ThreadLocal变量中存储一个随机值,接着各个线程调用另外其他多个类的方法,这多个类的方法中读取这个ThreadLocal变量的值,就可以看到多个类在同一个线程中共享同一份数据。\

5.实现对ThreadLocal变量的封装,让外界不要直接操作ThreadLocal变量。
(1)对基本类型的数据的封装,这种应用相对很少见。
(2)对对象类型的数据的封装,比较常见,即让某个类针对不同线程分别创建一个独立的实例对象。


public class ThreadLocalTest {

private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {

for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int data = new Random().nextInt();
System.out.println(Thread.currentThread().getName() + " put random data:" + data);
threadLocal.set(data);
new A().get();
new B().get();

}
}).start();

}

}

static class A {
public void get() {
int data = threadLocal.get();

System.out.println("A from " + Thread.currentThread().getName() + " get data:" + data);

}
}

static class B {
public void get() {
int data = threadLocal.get();
System.out.println("B from " + Thread.currentThread().getName() + " get data:" + data);

}
}
}

Thread-0 put random data:-2015900409
Thread-1 put random data:-645411160
A from Thread-0 get data:-2015900409
A from Thread-1 get data:-645411160
B from Thread-0 get data:-2015900409
B from Thread-1 get data:-645411160
优化

public class ThreadLocalTest {

private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

//private static ThreadLocal<MyThreadScopeData> myThreadScopeDataThreadLocal = new ThreadLocal<>();


public static void main(String[] args) {

for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int data = new Random().nextInt();
System.out.println(Thread.currentThread().getName() + " put random data:" + data);
threadLocal.set(data);

// MyThreadScopeData myThreadScopeData = new MyThreadScopeData();
// myThreadScopeData.setName("name" + data);
// myThreadScopeData.setAge(data);
// myThreadScopeDataThreadLocal.set(myThreadScopeData);

//获取与当前线程绑定的实例并设置值
MyThreadScopeData.getThreadInstance().setName("name" + data);
MyThreadScopeData.getThreadInstance().setAge(data);
new A().get();
new B().get();

}
}).start();

}

}

static class A {
public void get() {
int data = threadLocal.get();


// MyThreadScopeData myData = myThreadScopeDataThreadLocal.get();
//
//
// System.out.println("A from " + Thread.currentThread().getName()
// + " getMyData: " + myData.getName() + "," + myData.getAge());

MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
System.out.println("A from " + Thread.currentThread().getName()
+ " getMyData: " + myData.getName() + "," + myData.getAge());
}
}

static class B {
public void get() {
int data = threadLocal.get();
//System.out.println("B from " + Thread.currentThread().getName() + " get data:" + data);

MyThreadScopeData myData = MyThreadScopeData.getThreadInstance();
System.out.println("B from " + Thread.currentThread().getName()
+ " getMyData: " + myData.getName() + "," + myData.getAge());
}
}
}

//一个绑定当前线程的类
class MyThreadScopeData {

private static ThreadLocal<MyThreadScopeData> map = new ThreadLocal<>();
private String name;
private int age;

private MyThreadScopeData() {
}

//定义一个静态方法,返回各线程自己的实例
//这里不必用同步,因为每个线程都要创建自己的实例,所以没有线程安全问题。
public static MyThreadScopeData getThreadInstance() {
//获取当前线程绑定的实例
MyThreadScopeData instance = map.get();
if (instance == null) {
instance = new MyThreadScopeData();
map.set(instance);
}
return instance;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}


}

Thread-1 put random data:-1041517189
Thread-0 put random data:-98835751
A from Thread-1 getMyData: name-1041517189,-1041517189
A from Thread-0 getMyData: name-98835751,-98835751
B from Thread-1 getMyData: name-1041517189,-1041517189
B from Thread-0 getMyData: name-98835751,-98835751
实例:

设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1,写出程序。

1、如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统就可以这么做。


public class SellTicket {
//卖票系统,多个窗口的处理逻辑是相同的
public static void main(String[] args) {
Ticket t = new Ticket();
new Thread(t).start();
new Thread(t).start();
}
}

/**
* 将属性和处理逻辑,封装在一个类中
*
* @author yang
*/

class Ticket implements Runnable {

private int ticket = 10;

public synchronized void run() {
while (ticket > 0) {
ticket--;
System.out.println("当前票数为:" + ticket);
}
}
}

2、如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,例如,设计2个线程。一个线程对j增加1,另外一个线程对j减1,银行存取款系统。


public class MultiThreadShareData {
private int j;
public static void main(String[] args) {
MultiThreadShareData multiThreadShareData = new MultiThreadShareData();
for(int i=0;i<2;i++){
new Thread(multiThreadShareData.new ShareData1()).start();//增加
new Thread(multiThreadShareData.new ShareData2()).start();//减少
}
}
//自增
private synchronized void Inc(){
j++;
System.out.println(Thread.currentThread().getName()+" inc "+j);
}
//自减
private synchronized void Dec(){
j--;
System.out.println(Thread.currentThread().getName()+" dec "+j);
}

class ShareData1 implements Runnable {
public void run() {
for(int i=0;i<5;i++){
Inc();
}
}
}
class ShareData2 implements Runnable {
public void run() {
for(int i=0;i<5;i++){
Dec();
}
}
}
}

Thread-0 inc 1
Thread-0 inc 2
Thread-0 inc 3
Thread-0 inc 4
Thread-0 inc 5
Thread-1 dec 4
Thread-1 dec 3
Thread-2 inc 4
Thread-2 inc 5
Thread-2 inc 6
Thread-2 inc 7
Thread-2 inc 8
Thread-1 dec 7
Thread-1 dec 6
Thread-1 dec 5
Thread-3 dec 4
Thread-3 dec 3
Thread-3 dec 2
Thread-3 dec 1
Thread-3 dec 0
收起阅读 »

Kotlin是如何帮助你避免内存泄漏的?

本文的代码位置在github.com/marcosholga…中的kotlin-mem-leak分支上。 我是通过创建一个会导致内存泄漏的Activity,然后观察其使用Java和Kotlin编写时的表现来进行测试的。 其中Java代码如下: public c...
继续阅读 »

本文的代码位置在github.com/marcosholga…中的kotlin-mem-leak分支上。
我是通过创建一个会导致内存泄漏的Activity,然后观察其使用JavaKotlin编写时的表现来进行测试的。
其中Java代码如下:


public class LeakActivity extends Activity {

@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak);
View button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startAsyncWork();
}
});
}

@SuppressLint("StaticFieldLeak")
void startAsyncWork() {
Runnable work = new Runnable() {
@Override public void run() {
SystemClock.sleep(20000);
}
};
new Thread(work).start();
}
}


如上述代码所示,我们的button点击之后,执行了一个耗时任务。这样如果我们在20s之内关闭LeakActivity的话就会产生内存泄漏,因为这个新开的线程持有对LeakActivity的引用。如果我们是在20s之后再关闭这个Activity的话,就不会导致内存泄漏。
然后我们把这段代码改成Kotlin版本:


class KLeakActivity : Activity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_leak)
button.setOnClickListener { startAsyncWork() }
}

private fun startAsyncWork() {
val work = Runnable { SystemClock.sleep(20000) }
Thread(work).start()
}
}

咋一看,好像就只是在Runable中使用lambda表达式替换了原来的样板代码。然后我使用leakcanary和我自己的@LeakTest注释写了一个内存泄漏测试用例。


class LeakTest {
@get:Rule
var mainActivityActivityTestRule = ActivityTestRule(KLeakActivity::class.java)

@Test
@LeakTest
fun testLeaks() {
onView(withId(R.id.button)).perform(click())
}
}


我们使用这个用例分别对Java写的LeakActivityKotlin写的KLeakActivity进行测试。测试结果是Java写的出现内存泄漏,而Kotlin写的则没有出现内存泄漏。
这个问题困扰了我很长时间,一度接近自闭。。


image


然后某天,我突然灵光一现,感觉应该和编译后字节码有关系。


分析LeakActivity.java的字节码


Java类产生的字节码如下:


.method startAsyncWork()V
.registers 3
.annotation build Landroid/annotation/SuppressLint;
value = {
"StaticFieldLeak"
}
.end annotation

.line 29
new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;

invoke-direct {v0, p0}, Lcom/marcosholgado/performancetest/LeakActivity$2;-><init>
(Lcom/marcosholgado/performancetest/LeakActivity;)V

.line 34
.local v0, "work":Ljava/lang/Runnable;
new-instance v1, Ljava/lang/Thread;

invoke-direct {v1, v0}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V

invoke-virtual {v1}, Ljava/lang/Thread;->start()V

.line 35
return-void
.end method


我们知道匿名内部类持有对外部类的引用,正是这个引用导致了内存泄漏的产生,接下来我们就在字节码中找出这个引用。


new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;

复制代码

上述字节码的含义是:
首先我们创建了一个LeakActivity$2的实例。。


奇怪的是我们没有创建这个类啊,那这个类应该是系统自动生成的,那它的作用是什么啊?
我们打开LeakActivity$2的字节码看下


.class Lcom/marcosholgado/performancetest/LeakActivity$2;
.super Ljava/lang/Object;
.source "LeakActivity.java"

# interfaces
.implements Ljava/lang/Runnable;

# instance fields
.field final synthetic this$0:Lcom/marcosholgado/performancetest/LeakActivity;

# direct methods
.method constructor <init>(Lcom/marcosholgado/performancetest/LeakActivity;)V
.registers 2
.param p1, "this$0" # Lcom/marcosholgado/performancetest/LeakActivity;

.line 29
iput-object p1, p0, Lcom/marcosholgado/performancetest/LeakActivity$2;
->this$0:Lcom/marcosholgado/performancetest/LeakActivity;

invoke-direct {p0}, Ljava/lang/Object;-><init>()V

return-void
.end method


第一个有意思的事是这个LeakActivity$2实现了Runnable接口。


这就说明LeakActivity$2就是那个持有LeakActivity对象引用的匿名内部类的对象。


# interfaces
.implements Ljava/lang/Runnable;


就像我们前面说的,这个LeakActivity$2应该持有LeakActivity的引用,那我们继续找。


# instance fields
.field final synthetic
this$0:Lcom/marcosholgado/performancetest/LeakActivity;


果然,我们发现了外部类LeakActivity的对象的引用。
那这个引用是什么时候传入的呢?只有可能是在构造器中传入的,那我们继续找它的构造器。


.method constructor 
<init>(Lcom/marcosholgado/performancetest/LeakActivity;)V

果然,在构造器中传入了LeakActivity对象的引用。
让我们回到LeakActivity的字节码中,看看这个LeakActivity$2被初始化的时候。


new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;
invoke-direct {v0, p0},
Lcom/marcosholgado/performancetest/LeakActivity$2;-><init>
(Lcom/marcosholgado/performancetest/LeakActivity;)V


可以看到,我们使用LeakActivity对象来初始化LeakActivity$2对象,这样就解释了为什么LeakActivity.java会出现内存泄漏的现象。


分析 KLeakActivity.kt的字节码


KLeakActivity.kt中我们关注startAsyncWork这个方法的字节码,因为其他部分和Java写法是一样的,只有这部分不一样。
该方法的字节码如下所示:


.method private final startAsyncWork()V
.registers 3

.line 20
sget-object v0,
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
->INSTANCE:Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;

check-cast v0, Ljava/lang/Runnable;

.line 24
.local v0, "work":Ljava/lang/Runnable;
new-instance v1, Ljava/lang/Thread;

invoke-direct {v1, v0}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V

invoke-virtual {v1}, Ljava/lang/Thread;->start()V

.line 25
return-void
.end method

可以看出,与Java字节码中初始化一个包含Activity引用的实现Runnable接口对象不同的是,这个字节码使用了静态变量来执行静态方法。


sget-object v0,         
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1; ->
INSTANCE:Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;


我们深入KLeakActivity\$startAsyncWork\$work$1的字节码看下:


.class final Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
.super Ljava/lang/Object;
.source "KLeakActivity.kt"

# interfaces
.implements Ljava/lang/Runnable;

.method static constructor <clinit>()V
.registers 1

new-instance v0,
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;

invoke-direct {v0},
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;-><init>()V

sput-object v0,
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
->INSTANCE:Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;

return-void
.end method

.method constructor <init>()V
.registers 1

invoke-direct {p0}, Ljava/lang/Object;-><init>()V

return-void
.end method

可以看出,KLeakActivity\$startAsyncWork\$work$1实现了Runnable接口,但是其拥有的是静态方法,因此不需要外部类对象的引用。
所以Kotlin不出现内存泄漏的原因出来了,在Kotlin中,我们使用lambda(实际上是一个 SAM)来代替Java中的匿名内部类。没有Activity对象的引用就不会发生内存泄漏。
当然并不是说只有Kotlin才有这个功能,如果你使用Java8中的lambda的话,一样不会发生内存泄漏。
如果你想对这部分做更深入的了解,可以参看这篇文章Translation of Lambda Expressions
如果有需要翻译的同学可以在评论里面说就行啦。


image


现在把其中比较重要的一部分说下:



上述段落中的Lamdba表达式可以被认为是静态方法。因为它们没有使用类中的实例属性,例如使用super、this或者该类中的成员变量。
我们把这种Lambda称为Non-instance-capturing lambdas(这里我感觉还是不翻译为好)。而那些需要实例属性的Lambda则称为instance-capturing lambdas




Non-instance-capturing lambdas可以被认为是private、static方法。instance-capturing lambdas可以被认为是普通的private、instance方法。



这段话放在我们这篇文章中是什么意思呢?


因为我们Kotlin中的lambda没有使用实例属性,所以其是一个non-instance-capturing lambda,可以被当成静态方法来看待,就不会产生内存泄漏。


如果我们在其中添加一个外部类对象属性的引用的话,这个lambda就转变成instance-capturing lambdas,就会产生内存泄漏。


class KLeakActivity : Activity() {

private var test: Int = 0

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_leak)
button.setOnClickListener { startAsyncWork() }
}

private fun startAsyncWork() {
val work = Runnable {
test = 1 // comment this line to pass the test
SystemClock.sleep(20000)
}
Thread(work).start()
}
}

如上述代码所示,我们使用了test这个实例属性,就会导致内存泄漏。
startAsyncWork方法的字节码如下所示:


.method private final startAsyncWork()V
.registers 3

.line 20
new-instance v0, Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;

invoke-direct {v0, p0},
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
-><init>(Lcom/marcosholgado/performancetest/KLeakActivity;)V

check-cast v0, Ljava/lang/Runnable;

.line 24
.local v0, "work":Ljava/lang/Runnable;
new-instance v1, Ljava/lang/Thread;

invoke-direct {v1, v0}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V

invoke-virtual {v1}, Ljava/lang/Thread;->start()V

.line 25
return-void
.end method

很明显,我们传入了KLeakActivity的对象,因此就会导致内存泄漏。


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

建议收藏!!Flutter状态管理插件哪家强?请看岛上码农的排行榜!

前言一路下来,Flutter 状态管理相关的文章写了有几十篇了,这是本人也没想到的结果。Flutter 的状态管理插件实在太多,感觉要深挖,可以继续写上几十篇。只是,这样写,怕是自己不累,看客都累了!😂😂😂授人以鱼不如授人以渔,本篇就专门对 Flutter 比...
继续阅读 »

前言

一路下来,Flutter 状态管理相关的文章写了有几十篇了,这是本人也没想到的结果。Flutter 的状态管理插件实在太多,感觉要深挖,可以继续写上几十篇。只是,这样写,怕是自己不累,看客都累了!😂😂😂授人以鱼不如授人以渔,本篇就专门对 Flutter 比较流行的状态管理插件做一个合集并附上对比分析和排行榜。大家可以结合对比数据和官方文档来在实际开发中选择。大家可以在评论区晒出自己用的状态管理插件和选择的理由,互相参考一下!

横向对比参数

我们横向对比以数据说话,综合了 pub 的喜欢数(Likes)、流行度(Popularity)和得分(Pub Points),Gitbub的 Star 数、贡献者数量五个维度进行比对。各个参数说明如下:

  • 喜欢数(Likes):反映的是该插件受 Flutter 开发者的喜好程度,间接反映了插件的文档完整性、可读性和插件的易用性;
  • 流行度(Popularity):反映的是该插件受 Flutter 开发者的欢迎程度和插件应用的广泛性(白话解释:使用人的人越多,意味着有更多的人提前帮你踩坑💣)。
  • 得分(Pub Points):pub 的得分满分是130分,其实是对插件的一个比较基础的全面评测,分为如下6个部分:
    • 遵循插件规范(20分)
    • 文档完整性(20分)
    • 跨平台支持(20分)
    • 通过静态分析(30分)
    • 版本兼容性(20分)
    • 支持 null safety(20分)
  • GitHub Star 数:这个大家都懂,反映的是受开发者认可的程度,实力的象征!
  • 贡献者数量:这个其实就是插件的社区号召力和参与维护的人数,贡献者越多也意味着插件的可靠性越高,不至于说更新过慢或突然中止维护(中止维护属于天坑了💣💣💣)。

为了统一对比尺度,我们统一按与本篇列出的管理插件同维度最大值进行比对,根据比值得出星级,共设置5颗星,比值与星级对应关系如下:

  • 0.9-1.0:5星
  • 0.8-0.9:4星
  • 0.6-0.8:3星
  • 0.3-0.6:2星
  • 0.3以下:1星

状态管理插件对比分析

我们先看对比数据,再来做星级评比,状态管理插件清单的五项数据如下:

插件名称喜欢数流行度(%)得分Star 数贡献者数量
Provider52071001303.9k60
Redux2459711547514
MobX696981202k61
GetX6406991204.9k140
BLoC1215991307.8k135
Event Bus257981305973
GetIt15409913078619
FlutterCommand4372130283
Binder47571201632
StateRebuilder319951203906
Stacked8509711054362
Fish Redux52921007.2k34
flutter_meedu4685130152
Riverpod1039981302k61
flutter_hooks816981301.9k31

各项参数星级评定如下:

插件名称喜欢数流行度(%)得分Star 数贡献者数量
Provider★★★★★★★★★★★★★★★★★★★
Redux★★★★★★★★★
MobX★★★★★★★★★★★★
GetX★★★★★★★★★★★★★★★★★★★★★★★
BLoC★★★★★★★★★★★★★★★★★★★★
Event Bus★★★★★★★★★★
GetIt★★★★★★★★★★
FlutterCommand★★★★★★★★
Binder★★★★★★★
StateRebuilder★★★★★★★★★★
Stacked★★★★★★★★★★★
Fish Redux★★★★★★★★★★★★★
flutter_meedu★★★★★★★★★
Riverpod★★★★★★★★★★★★
flutter_hooks★★★★★★★★★★

排行榜

基于上面的星级评定和数据,我们把每项数据的比值求和,从大大小排序,得到的分值和排行榜如下。

插件名称排名综合评分星级
GetX14.54★★★★★
BLoC24.14★★★★
Provider33.74★★★★
Fish Redux42.86★★★
Riverpod52.83★★★
MobX62.81★★★
flutter_hooks72.57★★
GetIt82.47★★
Stacked92.46★★
Event Bus102.11★★
Redux112.05★★
StateRebuilder122.02★★
flutter_meedu131.87★★
FlutterCommand141.75★★
Binder151.53★★

作者:岛上码农
链接:https://juejin.cn/post/7016485596046557198
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Flutter 入门与实战(九十三):使用 Animation 构建爱心三连动画

前言 我们开始 Flutter 动画相关篇章之旅,在应用中通过动效能够给用户带来更愉悦的体验,比较典型的例子就是一些直播平台的动效了,比如送火箭能做出来那种火箭发射的动效——感觉倍有面子,当然这是土豪的享受,我等码农只在视频里看过😂😂😂。本篇我们来介绍基于 A...
继续阅读 »

前言


我们开始 Flutter 动画相关篇章之旅,在应用中通过动效能够给用户带来更愉悦的体验,比较典型的例子就是一些直播平台的动效了,比如送火箭能做出来那种火箭发射的动效——感觉倍有面子,当然这是土豪的享受,我等码农只在视频里看过😂😂😂。本篇我们来介绍基于 Animation 类实现的基本动画构建。


Animation 简介


Animation 是一个抽象类,它并不参与屏幕的绘制,而是在设定的事件范围内对一段区间值进行插值。插值的方式可以是线性、曲线、一个阶跃函数或其他能够想到的方式。这个类的对象能够知道当前的值和状态(完成或消失)。Animation 类提供了一个监听回调方法,当它的值改变的时候,就会调用该方法:


@override
void addListener(VoidCallback listener);

因此,在监听回调里,我们可以来刷新界面,通过Animation 对象最新的值控制 UI 组件的位置、尺寸、角度,从而实现动画的效果。Animation 类通常会和 AnimationController 一起使用。


AnimationController 简介


AnimationController 是一个特殊的 Animation 类,它继承自 Animation<double>。每当硬件准备好绘制下一帧时,AnimationController就会产生一个新的值。默认情况下 AnimationController 会在给定的时间范围内产生的值是从0到1.0的线性序列值(通常60个值/秒,以达到60 fps的效果)。例如,下面的代码构建一个时长为2秒的 AnimationController


var controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);

AnimationController 具有 forwardreverse等控制动画的方法,通常用于控制动画的开始和恢复。


连接 AnimationAnimationController 的是 Animatable类,该类也是一个抽象类, 常用的的实现类包括 Tween<T>(线性插值),CurveTween(曲线插值)。Animatable 类有一个 animate 方法,该方法接收 Animation<double>类参数(通常是 AnimationController),并返回一个 Animation 对象。


Animation<T> animate(Animation<double> parent) {
return _AnimatedEvaluation<T>(parent, this);
}

animate方法使用给定的 Animation<double>对象驱动完成动效,但使用的值的范围是自身的值,从而可以构建自定义值范围的动效。比如,要构建一个2秒内从0增长100的动效值,可以使用如下的代码。


var controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
var animation = Tween<double>(begin: 0, end: 100).animate(controller);

应用 - 爱心三连


有了上面的基础,我们就可以开始牛刀小试了,我们先来一个爱心三连放大缩小的动效,如下所示,首次点击逐渐放大,再次点击逐渐缩小恢复到原样。


爱心三连.gif
界面代码很简单,三个爱心其实就是使用Stack 将三个不同颜色的爱心 Icon 组件叠加在一起,然后通过 Animtion对象的值改变 Icon 的大小。在 Animation 值变化的监听回调李使用 setState 刷新界面就好了。完整代码如下:


import 'package:flutter/material.dart';

class AnimtionDemo extends StatefulWidget {
const AnimtionDemo({Key? key}) : super(key: key);

@override
_AnimtionDemoState createState() => _AnimtionDemoState();
}

class _AnimtionDemoState extends State<AnimtionDemo>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;

@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 1), vsync: this);
animation = Tween<double>(begin: 40, end: 100).animate(controller)
..addListener(() {
setState(() {});
});
controller.addStatusListener((status) {
print(status);
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Animation 动画'),
),
body: Center(
child: Stack(
alignment: Alignment.center,
children: [
Icon(
Icons.favorite,
color: Colors.red[100],
size: animation.value * 1.5,
),
Icon(
Icons.favorite,
color: Colors.red[400],
size: animation.value,
),
Icon(
Icons.favorite,
color: Colors.red[600],
size: animation.value / 2,
),
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow, color: Colors.white),
onPressed: () {
if (controller.status == AnimationStatus.completed) {
controller.reverse();
} else {
controller.forward();
}
},
),
);
}

@override
void dispose() {
controller.dispose();
super.dispose();
}
}

这里需要提的是在_AnimtionDemoState中混入了SingleTickerProviderStateMixin,这里其实是为 AnimationController 提供了一个 TickerProivder 对象。TickerProivder对象会在每一帧刷新前触发一个 onTick回调,从而实现AnimationController的值更新。


总结


本篇介绍了Flutter 动画构建类 AnimationAnimationController 的使用,通过这两个类可以实现很多基础动画效果,例如常见的进度条、缩放、旋转、移动等。接下来我们还将介绍基于 Animation 实现动画的其他方式和其他类型的动画效果。


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

RxHttp + Flow 三步搞定任意请求

1、前言 RxHttp 在之前的版本中,已提供了RxHttp + Await协程、RxHttp + RxJava两种请求方式,这一次,RxHttp 无缝适配了 Flow , RxHttp + Flow协程配合使用,使得请求更加简单,至此,RxHttp已集齐3架...
继续阅读 »

1、前言


RxHttp 在之前的版本中,已提供了RxHttp + Await协程RxHttp + RxJava两种请求方式,这一次,RxHttp 无缝适配了 Flow , RxHttp + Flow协程配合使用,使得请求更加简单,至此,RxHttp已集齐3架马车(Flow、Await、RxJava),且每架马车皆遵循请求三部曲,掌握请求三部曲,就掌握了RxHttp的精髓。


gradle依赖


1、必选


jitpack添加到项目的build.gradle文件中,如下:


allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}

//使用kapt依赖rxhttp-compiler时必须
apply plugin: 'kotlin-kapt'

android {
//必须,java 8或更高
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.github.liujingxing.rxhttp:rxhttp:2.7.0'
kapt 'com.github.liujingxing.rxhttp:rxhttp-compiler:2.7.0' //生成RxHttp类,纯Java项目,请使用annotationProcessor代替kapt
}

2、可选


//非必须,根据自己需求选择 RxHttp默认内置了GsonConverter
implementation 'com.github.liujingxing.rxhttp:converter-fastjson:2.7.0'
implementation 'com.github.liujingxing.rxhttp:converter-jackson:2.7.0'
implementation 'com.github.liujingxing.rxhttp:converter-moshi:2.7.0'
implementation 'com.github.liujingxing.rxhttp:converter-protobuf:2.7.0'
implementation 'com.github.liujingxing.rxhttp:converter-simplexml:2.7.0'

2、RxHttp + Flow 使用


2.1、请求三部曲


用过RxHttp的同学知道,RxHttp发送任意请求皆遵循请求三部曲,如下:
rxhttp_flow_chart.jpg
代码表示


 RxHttp.get("/service/...")  //第一步,确定请求方式,可以选择postForm、postJson等方法
.add("key", "value")
.toFlow<Student>() //第二步,调用toFlow方法并输入泛型类型,拿到Flow对象
.catch {
//异常回调
val throwable = it
}.collect { //第三步,调用collect方法发起请求
//成功回调
val student = it
}

协程请求三部曲详解




  • 第一步,选择get、postForm、postJson、postBody等方法来确定请求方式,随后便可通过add、addFile、addHeader等方法来添加参数、文件、请求头等信息




  • 第二步,调用toFlow/toFlowXxx系列方法,并传入泛型类型,以获取到Flow对象,toFlow有一系列重载方法,可以实现上传/下载及进度的监听,本文后续会详细介绍,在这一步后,可以调用catchonStartonCompletion等方法去监听异常、开始及结束回调,跟平时使用Flow对象没有任何区别




  • 第三步,调用collect方法就会开始发送请求,如果一些正常的话,就会收到成功回调




以上就是RxHttp在协程中最常规的操作,掌握请求三部曲,就掌握了RxHttp的精髓


2.2、BaseUrl处理


RxHttp通过@DefaultDomain、@Domain注解来配置默认域名及非默认域名,如下:


public class Url {

@DefaultDomain //通过该注解设置默认域名
public static String BASE_URL = "https://www.wanandroid.com";

// name 参数在这会生成 setDomainToGoogleIfAbsent方法,可随意指定名称
// className 参数在这会生成RxGoogleHttp类,可随意指定名称
@Domain(name = "Google", className = "Google")
public static String GOOGLE = "https://www.google.com";
}

以上配置http://www.wanandroid.com为默认域名,http://www.google.com为非默认域名


多BaseUrl处理


//1、使用默认域名,传入相对路径即可
//此时 url 为 https://www.wanandroid.com/service/...
RxHttp.get("/service/...")
...

//2、使用google域名方式一:传入绝对路径
RxHttp.get("https://wwww.google.com/service/...")
...

//3、使用google域名方式二:调用setDomainToGoogleIfAbsent方法
//该方法是通过 @Domain 注解的 name 字段生成的,命名规则为 setDomainTo{name}IfAbsent
RxHttp.get("/service/...")
.setDomainToGoogleIfAbsent()
...

//4、使用google域名方式三:直接使用RxGoogleHttp类发送请求,
//该类是通过 @Domain 注解的 className 字段生成的,命名规则为 Rx{className}http
RxGoogleHttp.get("/service/...")
...

注:以上4种配置域名的方式,优先级别为:2 > 3 > 4 > 1


动态域名处理


//直接对url重新赋值即可,改完立即生效
Url.BASE_URL = "https://www.baidu.com";
RxHttp.get("/service/...")
...
//此时 url 为 https://www.baidu.com/service/...

2.3、业务code统一判断


我想大部分人的接口返回格式都是这样的


class BaseResponse<T> {
var code = 0
var msg : String? = null
var data : T
}

拿到该对象的第一步就是对code做判断,如果code != 200(假设200代表数据正确),就会拿到msg字段给用户一些错误提示,如果等于200,就拿到data字段去更新UI,常规的操作是这样的


RxHttp.get("/service/...")
.toFlow<BaseResponse<Student>>()
.collect {
if (response.code == 200) {
//拿到data字段(Student)刷新UI
} else {
val msg = it.msg //拿到msg字段给出错误提示
}
}

试想一下,一个项目少说也有30+个这样的接口,如果每个接口读取这么判断,就显得不够优雅,也可以说是灾难,相信也没有人会这么干。而且对于UI来说,只需要data字段即可,错误提示啥的我管不着。


那有没有什么办法,能直接拿到data字段,并且对code做出统一判断呢?有的,直接上代码


RxHttp.get("/service/...")
.toFlowResponse<Student>() //调用此方法,直接拿到data字段,也就是Student对象
.catch {
// code非200时,走异常回调,在这可拿到msg及code字段
val msg = it.msg
val code = it.code
}.collect {
//直接拿到data字段,在这就是Student对象
val student = it
}

可以看到,以上调用toFlowResponse()方法,成功回调就可直接拿到data字段,也就是Student对象。


此时,相信很多人会有疑问,




  • 业务code哪里判断的?




  • 异常回调里的it是什么对象,为啥可以拿到msg、code字段?




先来回答第一个问题,业务code哪里判断的?


其实toFlowResponse()方法并不是RxHttp内部提供的,而是通过自定义解析器,并用@Parser注解标注,最后由注解处理器rxhttp-compiler自动生成的,听不懂?没关系,直接看代码


@Parser(name = "Response")
open class ResponseParser<T> : TypeParser<T> {

//以下两个构造方法是必须的
protected constructor() : super()
constructor(type: Type) : super(type)

@Throws(IOException::class)
override fun onParse(response: okhttp3.Response): T {
val data: BaseResponse<T> = response.convertTo(BaseResponse::class, *types)
val t = data.data //获取data字段
if (data.code != 200 || t == null) { //code不等于200,说明数据不正确,抛出异常
throw ParseException(data.code.toString(), data.msg, response)
}
return t //最后返回data字段
}
}

上面代码只需要关注两点即可,


第一点,我们在类开头使用了@Parser注解,并为解析器取名为Response,此时rxhttp-compiler就会生成toFlowResponse<T>()方法,命名规则为toFlow{name}


第二点,我们在if语句里,code != 200data == null时,就抛出ParseException异常,并带上了msg、code字段,所以我们在异常回调通过强转,就可以拿到这两个字段


接着回答第二个问题,异常回调里的it是什么对象,为啥可以拿到msg、code字段?


其实it就是Throwable对象,而msg、codeThrowable的扩展字段,这需要我们自己为其扩展,代码如下:


val Throwable.code: Int
get() =
when (this) {
is HttpStatusCodeException -> this.statusCode //Http状态码异常
is ParseException -> this.errorCode.toIntOrNull() ?: -1 //业务code异常
else -> -1
}

val Throwable.msg: String
get() {
return if (this is UnknownHostException) { //网络异常
"当前无网络,请检查你的网络设置"
} else if (
this is SocketTimeoutException //okhttp全局设置超时
|| this is TimeoutException //rxjava中的timeout方法超时
|| this is TimeoutCancellationException //协程超时
) {
"连接超时,请稍后再试"
} else if (this is ConnectException) {
"网络不给力,请稍候重试!"
} else if (this is HttpStatusCodeException) { //请求失败异常
"Http状态码异常"
} else if (this is JsonSyntaxException) { //请求成功,但Json语法异常,导致解析失败
"数据解析失败,请检查数据是否正确"
} else if (this is ParseException) { // ParseException异常表明请求成功,但是数据不正确
this.message ?: errorCode //msg为空,显示code
} else {
"请求失败,请稍后再试"
}
}

到这,业务code统一判断就介绍完毕,上面的代码,大部分人只需要简单修改后,就可用到自己的项目上,如ResponseParser解析器,只需要改下if语句的判断条件即可


3、上传/下载


RxHttp对文件的优雅操作是与生俱来的,配合Flow,可以说是如虎添翼,不多说,直接上代码


3.1、文件上传


RxHttp.postForm("/service/...")  
.addFile("file", File("xxx/1.png")) //添加单个文件
.addFiles("fileList", ArrayList<File>()) //添加多个文件
.toFlow<String>()
.catch { //异常回调 }
.collect { //成功回调 }

只需要通过addFile系列方法添加File对象即可,就是这么简单粗暴,想监听上传进度,toFlow方法传入进度回调即可,如下:


RxHttp.postForm("/service/...")      
.addFile("file", File("xxx/1.png"))
.addFiles("fileList", ArrayList<File>())
.toFlow<String> { //这里还可以选择自定义解析器对应的toFlowXxx方法
val process = it.progress //已上传进度 0-100
val currentSize = it.currentSize //已上传size,单位:byte
val totalSize = it.totalSize //要上传的总size 单位:byte
}.catch { //异常回调 }
.collect { //成功回调 }

3.2、文件下载


接着再来看看下载,直接贴代码


val localPath = "sdcard//android/data/..../1.apk"  
RxHttp.get("/service/...")
.toFlow(localPath) {
//it为Progress对象
val process = it.progress //已下载进度 0-100
val currentSize = it.currentSize //已下载size,单位:byte
val totalSize = it.totalSize //要下载的总size 单位:byte
}
.catch { //异常回调 }
.collect { //成功回调,这里可以拿到本地存储路径,也就是localPath }

你没看错,下载也是调用 toFlow方法,传入本地路径及进度回调即可,当然,如果不需要监听进度,进度回调也可不传,来看看用来下载的toFlow方法签名


/**
* @param destPath 本地存储路径
* @param append 是否追加下载,即是否断点下载
* @param capacity 队列size,仅监听进度回调时生效
* @param progress 进度回调
*/
fun CallFactory.toFlow(
destPath: String,
append: Boolean = false,
capacity: Int = 1,
progress: (suspend (Progress) -> Unit)? = null
): Flow<String>

以上4个参数,只有destPath是必须的,其它3个参数,根据实际需要传递,想要断点下载,append传入true,想要监听进度就传入进度回调,


至于capacity参数,这个需要额外说明一下,它是指定队列的缓存大小,什么队列?进度回调的队列,目的就是丢弃来不及消费的事件,在现实场景中,可能会存在下游消费速度 小于 上游生产速度的情况,这就会导致事件的堆积,翻译过来就是下载很快,但你处理进度回调的地方很慢,就有可能出现你还在处理进度为10的事件,但实际下载进度可能到了50甚至更高,capacity设置为1的话,10-50之间的事件就会被丢弃,接下来下游收到的可能就是进度为50的事件,这就保证了下游收到的始终的最新的事件,也就是最及时的下载进度,当然,如果你想收到全部的进度回调事件,将capacity设置为100即可。


3.3、暂停/恢复下载


很多会有暂停/恢复下载的需求,但对于下载来说,并没有真正意义的暂停及恢复,所谓的暂停,不过就是停止下载,也就是中断请求,而恢复,就是再次发起请求从上次中断的位置继续下载,也就是断点下载,所有,只需要知道如何取消请求及断点下载即可


取消请求


Flow的取消,就是外部协程的关闭


val job = lifecycleScope.launch {
val localPath = "sdcard//android/data/..../1.apk"
RxHttp.get("/service/...")
.toFlow(localPath) {
//it为Progress对象
val process = it.progress //已下载进度 0-100
val currentSize = it.currentSize //已下载size,单位:byte
val totalSize = it.totalSize //要下载的总size 单位:byte
}
.catch { //异常回调 }
.collect { //成功回调,这里可以拿到本地存储路径,也就是localPath }
}
//在需要的时候,调用job.cancel()就是取消请求
job.cancel()

断点下载


上面介绍过,想要断点下载,只需要额外将toFlow方法的第二个参数append设置为true即可,如下:


val localPath = "sdcard//android/data/..../1.apk"  
RxHttp.get("/service/...")
.toFlow(localPath, true) {
//it为Progress对象
val process = it.progress //已下载进度 0-100
val currentSize = it.currentSize //已下载size,单位:byte
val totalSize = it.totalSize //要下载的总size 单位:byte
}
.catch { //异常回调 }
.collect { //成功回调,这里可以拿到本地存储路径,也就是localPath }

注:断点下载需要服务器接口支持


对于Android 10文件上传/下载,请点击RxHttp 完美适配Android 10/11 上传/下载/进度监听


4、转LiveData 


Flow依赖于协程环境,如果不想使用协程,又想要使用Flow,那LiveData就是一个很好的选择,在官方androidx.lifecycle:lifecycle-livedata-ktx:x.x.x库中提供了asLiveData方法,可方便的将FlowLiveData对象,有了LiveData对象,就不再需要协程环境


4.1、普通请求转LiveData


//当前在FragmentActivity环境中
RxHttp.get("/service/...")
.toFlow<Student>()
.catch { }
.asLiveData()
.observe(this) {
val student = it;
//更新UI
}

由于调用了asLiveData,所以,以上代码,不需要协程环境也可执行;


4.2、带进度上传转LiveData


RxHttp.postForm("/service/...")      
.addFile("file", File("xxx/1.png"))
.addFiles("fileList", ArrayList<File>())
.toFlow<Student> { //这里还可以选择自定义解析器对应的toFlowXxx方法
val process = it.progress //已上传进度 0-100
val currentSize = it.currentSize //已上传size,单位:byte
val totalSize = it.totalSize //要上传的总size 单位:byte
}
.catch { //异常回调 }
.asLiveData()
.observe(this) {
val student = it;
//更新UI
}

上面代码中,转LiveData后,下游observe只能收到上传完成的回调,如果你想收到包括进度回调在内的所有事件,则需要使用toFlowProgress替代toFlow方法(toFlow内部是通过toFlowProgress方法实现的,有兴趣的自己查看源码),如下:


RxHttp.postForm("/service/...")      
.addFile("file", File("xxx/1.png"))
.addFiles("fileList", ArrayList<File>())
.toFlowProgress<Student>() //该方法没有进度回调参数
.catch { //异常回调 }
.asLiveData()
.observe(this) {
//此时这里将收到所有事件,这里的it为ProgressT<Student>对象
val process = it.progress //已上传进度 0-100
val currentSize = it.currentSize //已上传size,单位:byte
val totalSize = it.totalSize //要上传的总size 单位:byte
val student = it.result //接口返回的对象
if (student != null) {
//不为null,代表上传完成,接口请求结束
}
}

4.3、带进度下载转LiveData


下载也一样,RxHttp提供了一个下载对应的toFlowProgress方法,如下:


fun CallFactory.toFlowProgress(
destPath: String,
append: Boolean = false,
capacity: Int = 1
): Flow<ProgressT<String>>

跟上面介绍下载时对应的toFlow方法相比,少了一个进度回调的参数,这里悄悄告诉你,下载的toFlow方法,内部就是通过toFlowProgress方法实现的,想了解的自己去查看源码,这里不做介绍


结合asLiveData方法,使用如下:


val localPath = "sdcard//android/data/..../1.apk"  
RxHttp.get("/service/...")
.toFlowProgress(localPath)
.catch { //异常回调 }
.asLiveData()
.observe(this) {
//此时这里将收到所有事件,这里的it为ProgressT<String>对象
val process = it.progress //已下载进度 0-100
val currentSize = it.currentSize //已下载size,单位:byte
val totalSize = it.totalSize //要下载的总size 单位:byte
val path = it.result //本地存储路径
if (path != null) {
//不为null,代表下载完成,接口请求结束
}
}

5、小结


看完本文,相信你已经领悟到了RxHttp的优雅,不管上传/下载,还是进度的监听,通通三步搞懂,掌握请求三部曲,就掌握了RxHttp的精髓。


其实,RxHttp远不止这些,本文只介绍了RxHttp + Flow的配合使用,更多功能,如:公共参数/请求头的添加、请求加解密、缓存等等,请查看


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

了解Parcelable存在的意义

Parcelable是Google团队专门为Android设计的序列化类,那在Java中已经有了Serializable序列化为什么还需要Parcelable呢?我们接下来就通过阅读Parcelable的实现类和源码来比较它们的区别,建议先对Serializa...
继续阅读 »

Parcelable是Google团队专门为Android设计的序列化类,那在Java中已经有了Serializable序列化为什么还需要Parcelable呢?我们接下来就通过阅读Parcelable的实现类和源码来比较它们的区别,建议先对Serializable序列化原理有一个了解

1.实现类

我们看一个实现了Parcelable的实体类。

public class Person implements Parcelable {

private String name;
private int sex;
private int age;
private String phone;


protected Person(Parcel in) {
name = in.readString();
sex = in.readInt();
age = in.readInt();
phone = in.readString();
}

public static final Creator<Person> CREATOR = new Creator<Person>() {
@Override
public Person createFromParcel(Parcel in) {
return new Person(in);
}

@Override
public Person[] newArray(int size) {
return new Person[size];
}
};

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(sex);
dest.writeInt(age);
dest.writeString(phone);
}
}

describeContents()方法是默认实现,特殊情况才需要返回1,newArray(int size)方法也是默认实现就行。重点看createFromParcel()和writeToParcel()方法,见名知意writeToParcel()就是序列化方法,createFromParcel()是反序列化方法。不管是启动Activity时的传递对象还是AIDL中的使用,都是通过调用这两个方法来实现数据对象的序列化和反序列化。

2.源码分析

先分析序列化writeToParcel()方法,对数据的序列化操作都是通过传入的Parcel对象。

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(sex);
dest.writeInt(age);
dest.writeString(phone);
}

我们追踪写入String数据的进去看一下。

public final void writeString(@Nullable String val) {
writeString16(val);
}

继续深入。

public final void writeString16(@Nullable String val) {
mReadWriteHelper.writeString16(this, val);
}

调用了一个帮助类的方法,传入了自己和序列化数据。

public void writeString16(Parcel p, String s) {
p.writeString16NoHelper(s);
}

最终还是调用的Parcel方法。

public void writeString16NoHelper(@Nullable String val) {
nativeWriteString16(mNativePtr, val);
}

nativeWriteString16()是一个本地方法。

@FastNative
private static native void nativeWriteString16(long nativePtr, String val);

序列化到了这里也就追踪不下去了,那我们再看反序列化createFromParcel()方法。

public Person createFromParcel(Parcel in) {
return new Person(in);
}

对数据的操作也是通过Parcel对象。直接调用了Person有参构造方法,并传入了Parcel对象。

protected Person(Parcel in) {
name = in.readString();
sex = in.readInt();
age = in.readInt();
phone = in.readString();
}

我们也看下是如何读取String数据的。

public final String readString() {
return readString16();
}

再深入。

public final @Nullable String readString16() {
return mReadWriteHelper.readString16(this);
}

同样,还是调用了帮助类的方法,传入了自己。

public String readString16(Parcel p) {
return p.readString16NoHelper();
}

还是调用了Parcel对象的方法。

public @Nullable String readString16NoHelper() {
return nativeReadString16(mNativePtr);
}

再调用了本地方法。

@FastNative
private static native String nativeReadString16(long nativePtr);

反序列化也只能追踪到这里,会发现所有的操作都是通过Parcel类实现,但是序列化和反序列化的源码流程很简单,暴露给我们的过程很少,核心的数据处理都是采用的本地方法,会疑惑数据究竟存到哪里去了呢?其实是在本地开辟了一块共享内存,通过指针指向了这块内存,把数据存入了这里面。

3.Parcelable VS Serializable

  1. Parcelable只是对内存操作,并没有序列化成正在的二进制;而Serializable会被流操作对象序列化成二进制字节数据;
  2. Serializable中使用了大量的反射和临时变量,在性能上低于Parcelable;
  3. Serializable在使用时是传入到流对象进行序列化和反序列化处理,而Parcelable都是在内部实现序列化和反序列化,Parcelable更加灵活;

4.总结

根据上述分析,得出下面结论:

  1. Parcelable只适合在Android中进行IPC通信时使用,也建议优先采用,可提高性能;但是需要注意,因为Parcelable是对内存的操作,所以大量对象数据时,可能会造成内存溢出。
  2. Serializable可以在IPC、本地存储、网络传输中都可以使用,但是因为使用了大量反射和临时变量,相对于Parcelable在性能上稍逊。
收起阅读 »

高级UI事件分发、事件冲突处理

一、MotionEvent介绍二、事件的接收流程。可根据之前的结成介绍找到入口。viewRootImpl会对事件进行处理,首先找到DecorView,然后再找到activity再在dispatchTouchEvent()里处理。setView@ViewRoot...
继续阅读 »

一、MotionEvent介绍

image.png

二、事件的接收流程。

可根据之前的结成介绍找到入口。

viewRootImpl会对事件进行处理,首先找到DecorView,然后再找到activity再在dispatchTouchEvent()里处理。

setView@ViewRootImp.java
--> mInputEventReceiver = new WindowInputEventReceiver(inputChannel,Looper.myLooper());//接收事件的
-->WindowInputEventReceiver是内部类,事件在onInputEvent(InputEvent event)方法里处理
-->enqueueInputEvent()
-->doProcessInputEvents()
-->deliverInputEvent(q)
-->stage.deliver(q)(InputStage stage;ViewPostImeStage)
-->onprocess()
-->processPointerEvent(q);
//mView是DecorView
-->mView.dispatchPointerEvent(event)//这个方法是View.java
-->dispatchTouchEvent()//这个方法在DecorView.java
-->dispatchTouchEvent@Activity.java
-->getwindow().superDispatchTouchEvent(ev);



superDispatchTouchEvent@PhoneWindow.java
-->mDecor.superDispatchTouchEvent(event)
-->最终调用的是DispatchTouchEvent@ViewGroup.java//我们处理的事件分发机制
-->onTouchEvent()

//事件处理的方法
View.dispatchTouchEvent();

DecoreView.java的dispatchTouchEvent方法。

cb == activity
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

三、父布局里有一个子view,点击子view的流程打印结果。

1.viewGroup

  • dispatchtouchevent()
  • onTouchEvent()
  • OnInterceptTouchEvent():返回true,子view被拦截

2. view

  • onclick()
  • onTouch()
  • dispatchTouchEvent()
  • onTouchEvent()

3. 打印结果

结论:每个事件都会经历父容器到子view


dispatchTouchEvent: 父容器
onInterceptTouchEvent: 父容器
dispatchTouchEvent: 子View
onTouch: 0
onTouchEvent: MotionEvent.ACTION_DOWN = 0


dispatchTouchEvent: 父容器
onInterceptTouchEvent: 父容器
dispatchTouchEvent: 子View
onTouch: 1
MotionEvent.ACTION_UP = 1
onClick

四、事件处理。

1.事件消费(没重写)

当源码中result == true的时候代表这个事件被消费了。

view#disPatchTouchEvent
-->onTouch()//此处执行onTouch方法,则后面的方法不执行。
-->onTouchEvent()//此处是执行onclick()方法的。
-->MotionEvent.Action_up
-->performClick()
-->performClickInternal()
-->onclick() //点击事件执行。
-->MotionEvent.Action_move
-->!pointInView()//当发现移动的时候移出了view,则up的时候就不会触发点击和长按的响应应。
-->MotionEvent.Action_down
-->checkForLongClick()//延时回调,长按的逻辑处理。当在500ms内触发up则取消长按。

2.viewGroup的dispatchTouchEvent的流程

  • 只有第一根手指按下会响应action_down。后续的所有手指都是action_Point_Down.

  • 最后抬起的那根手指是action_up。之前抬起的都是action_point_up

  • 最多识别32跟手指。int有多少位多少根手指。

viewGroup#dispatchTouchEvent
-->1.actionMasked == MotionEvent.ACTION_DOWN//不管是单指还是多指,会进入一次。重置状态
-->2.检测是否拦截。//OnInterceptTouchEvent
-->3.通过条件判断决定是否要分发事件。
-->进入循环判断子view里是否要处理这个事件。
-->4.当子view不处理,自己判断是否处理这个事件。
-->viewgroup进行自己处理事件是调用的view的dispatchTouchEvent()


换个角度去分析

第一块:

  • 是否拦截子view

第二块:

  • 遍历子view是否处理事件。

第三块:

  • 子view不处理,询问自己是否处理。
  • 子view处理,分情况。

大概分析一下

在第一块代码,Action_down的时候如果没有拦截子view,则会在第二个块代码遍历找到需要执行事件的view并把target.child记录下来。当后续的action_move就不会走第二块代码,之前记录的target.child去执行move事件。

五、viewgroup嵌套viewGroup事件触发分析

viewpager:横着滑动(左右滑动)。

listview:竖着滑动(上下滑动)。

1.当viewpager的oninterceptTouchEvent返回值为true。

上下不可以滑动,左右可以滑动

2.当viewpager的oninterceptTouchEvent返回值为false。

上下可以滑动,左右不可以滑动。

3.当viewpager的oninterceptTouchEvent返回值为false,当ListView的dispatchTouchEvent返回值为false。

上下不可以滑动,左右能滑动

如何实现上下可以,左右也可以滑动?

两个view叠加在一起,冲突是必然的。

冲突处理:

1.内部拦截法(子view根据条件来让事件由谁触发)要让子view拿到事件。

用此方法,必须能让子view能拿到事件。

子viewgroup

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();


switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
//让父控件不去拦截自己
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
// 这个条件由业务逻辑决定,看什么时候 子View将事件让出去
//左右滑动,就让父容器拦截。
if (Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;

}
default:
break;
}

mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}

父viewgroup


// 拦截自己的孩子
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// down事件的时候不能拦截,因为这个时候 requestDisallowInterceptTouchEvent 无效
if (event.getAction() == MotionEvent.ACTION_DOWN) {
super.onInterceptTouchEvent(event);
return false;
}
return true;
}

2.外部拦截法(由父容器来根据条件让事件由谁来触发)


// 外部拦截法:一般只需要在父容器处理,根据业务需求,返回true或者false
public boolean onInterceptTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mLastX = (int) event.getX();
mLastY = (int) event.getY();
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
return true;
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}

return super.onInterceptTouchEvent(event);
}


收起阅读 »

Android WebView H5 秒开方案总结

为了满足跨平台和动态性的要求,如今很多 App 都采用了 Hybrid 这种比较成熟的方案来满足多变的业务需求。Hybrid 也叫混合开发,即半原生半 H5 的方式,通过 WebView 来实现需要高度灵活性的业务,在需要和 Native 做交互或者是调用特定...
继续阅读 »

为了满足跨平台和动态性的要求,如今很多 App 都采用了 Hybrid 这种比较成熟的方案来满足多变的业务需求。Hybrid 也叫混合开发,即半原生半 H5 的方式,通过 WebView 来实现需要高度灵活性的业务,在需要和 Native 做交互或者是调用特定平台能力时再通过 JsBridge 来实现两端交互

采取 Hybrid 方案的理由可以有很多个:实现跨平台和动态更新、保持各端之间业务和逻辑的统一、满足快速开发的需求;而放弃 Hybrid 方案的理由只需要一个:性能相对 Native 来说要差得多。WebView 比较让人诟病的一点就是性能相对 Native 来说比较差,经常需要 load 一段时间后才能加载完成,用户体验较差。开发者在实现了基本的业务需求后,也需要来进一步优化用户体验。目前也已经有很多通用的手段来优化 WebView 展示首屏页面的时间和性能成本,而这些优化手段也不单单局限于某个平台,对于 Android 和 IOS 来说大多都是通用的,当然这也离不开前端和服务端的支持。本文就来对这些优化方案做一个总结,希望对你有所帮助 🤣🤣

一、性能瓶颈

想要优化 WebView,就需要先知道限制了 WebView 的性能瓶颈到底有哪几方面

百度 APP 曾经统计了其某一天全网用户的落地页首屏展现速度 80 分位数据,从点击到首屏展现(首图加载完成),大致需要 2600 ms

百度的开发人员将这一整个过程划分为了四个阶段,并统计出了各个阶段的平均耗时

  • 初始化 Native App 组件,花费了 260 ms。主要工作是:初始化 WebView。首次创建 WebView 的耗时均值为 500 ms,第二次创建 WebView 时会快很多
  • 初始化 Hybrid,花费了 170 ms。主要工作是:根据调起协议中传入的相关参数,校验解压下发到本地的 Hybrid 模板,大致需要 100 ms 的时间;WebView.loadUrl 执行后,触发对 Hybrid 模板头部和 Body 的解析
  • 加载正文数据和渲染页面,花费了 1400 ms。主要工作是:加载解析页面所需的 JS 文件,并通过 JS 调用端能力发起对正文数据的请求,客户端从 Server 拿到数据后,用 JsCallback 的方式回传给前端,前端需要对客户端传来的 JSON 格式的正文数据进行解析,并构造 DOM 结构,进而触发内核的渲染流程;此过程中,涉及到对 JS 的请求,加载、解析、执行等一系列步骤,并且存在端能力调用、JSON 解析、构造 DOM 等操作,较为耗时
  • 加载图片,花费了 700 ms(图片貌似标错了,此处统计的应该是从渲染正文结束首图加载完成之间的时间)。主要工作是:在上一步中,前端获取到的正文数据包含落地页的图片地址集,在完成正文的渲染后,需要前端再次执行图片请求的端能力,客户端这边接收到图片地址集后按顺序请求服务器,完成下载后,客户端会调用一次 IO 将文件写入缓存,同时将对应图片的本地地址回传给前端,最终通过内核再发起一次 IO 操作获取到图片数据流,进行渲染

可以看到,最耗时的就是 加载正文数据和渲染页面 和 加载图片 两个阶段,需要进行多次网络请求、JS 调用、IO 读写;其次是 初始化 WebView 和 加载模板文件 两个阶段,这两个阶段耗时相近,虽然基本不用进行网络请求,但涉及到对浏览器内核和模板文件的初始化操作,存在一些无法避免的时间花费

从这就可以得出最基本的优化方向:

  • 初始化的时间是否可以更快一点?例如,WebView 和模板文件的初始化时间是否可以更少一点? 能不能提前完成这些任务?
  • 完成首屏页面的前置任务是否可以更少一点?例如,网络请求、JS 调用、IO 读写的次数是否可以更少一点? 是否可以合并或者提前完成这些任务?
  • 资源文件的加载时间是否可以更快一点?例如,图片、JS、CSS 文件的请求次数是否可以更少一点? 能不能直接使用本地缓存?网络请求速度是否可以更快一点?

二、WebView 预加载

创建 WebView 属于一个比较耗时的操作,特别是在第一次创建的时候由于需要初始化浏览器内核,会耗时几百毫秒,之后再次创建 WebView 就会快很多,但也还需要几十毫秒。为了避免每次使用时都需要同步等待 WebView 创建完成,我们可以选择在合适的时机 预加载 WebView 并存入 缓存池 中,等要用到时再直接从缓存池中取,从而缩短显示首屏页面的时间

想要进行预加载,那就要思考以下两个问题该如何解决:

  • 触发时机如何选?

    既然创建 WebView 属于一个比较耗时的操作,那我们在预加载时一样可能会拖慢当前主线程,这样相当于只是把耗时操作提前了而已,我们需要保证预加载操作不会影响到当前主线程任务

  • Context 如何选?

    WebView 需要和 Context 进行绑定,且每个 WebView 应该是对应于特定的 Activity Context 实例的,不能直接使用 Application 来创建 WebView,我们需要保证预加载的 WebView Context 和最终的 Context 之间的一致性

第一个问题可以通过 IdleHandler 来解决。通过 IdleHandler 提交的任务只有在当前线程关联的 MessageQueue 为空的情况下才会被执行,因此通过 IdleHandler 来执行预创建可以保证不会影响到当前主线程任务

第二个问题可以通过 MutableContextWrapper 来解决。顾名思义,MutableContextWrapper 是系统提供的 Context 包装类,其内部包含一个 baseContext,MutableContextWrapper 所有的内部方法都会交由 baseContext 来实现,且 MutableContextWrapper 允许外部替换它的 baseContext,因此我们可以在一开始的时候使用 Application 作为 baseContext,等到 WebView 和 Activity 进行实际绑定的时候再来替换

最终预加载 WebView 的大致逻辑就如下所示。我们可以在 PageFinished 或者退出 WebViewActivity 的时候就主动调用 prepareWebView() 方法来进行预加载,需要用到的时候就从缓存池中取出来动态添加到布局文件中

/**
* @Author: leavesC
* @Date: 2021/10/4 18:57
* @Desc:
* @公众号:字节数组
*/

object WebViewCacheHolder {

private val webViewCacheStack = Stack<RobustWebView>()

private const val CACHED_WEB_VIEW_MAX_NUM = 4

private lateinit var application: Application

fun init(application: Application) {
this.application = application
prepareWebView()
}

fun prepareWebView() {
if (webViewCacheStack.size < CACHED_WEB_VIEW_MAX_NUM) {
Looper.myQueue().addIdleHandler {
log("WebViewCacheStack Size: " + webViewCacheStack.size)
if (webViewCacheStack.size < CACHED_WEB_VIEW_MAX_NUM) {
webViewCacheStack.push(createWebView(MutableContextWrapper(application)))
}
false
}
}
}

fun acquireWebViewInternal(context: Context): RobustWebView {
if (webViewCacheStack.isEmpty()) {
return createWebView(context)
}
val webView = webViewCacheStack.pop()
val contextWrapper = webView.context as MutableContextWrapper
contextWrapper.baseContext = context
return webView
}

private fun createWebView(context: Context): RobustWebView {
return RobustWebView(context)
}

}

此方案虽然无法缩减创建 WebView 所需的时间,但可以缩短完成首屏页面的时间。需要注意,对 WebView 进行缓存采取的是用空间换时间的做法,需要考虑低端机型运存较小的情况

三、渲染优化

想要优化首屏的渲染速度,首先得从整个页面访问请求的链路上看,借用阿里巴巴淘系技术的一张图,下面是常规端上 H5 页面访问链路

这一整个过程需要完成多个网络请求和 IO 操作,WebView 在加载了基本的 HTML 和 CSS 文件后,再通过 JS 从服务端获取正文数据,拿到数据后还需要完成解析 JSON、构造 DOM、应用 CSS 样式等一系列耗时操作,最终才能由内核进行渲染上屏

移动端的系统版本、处理器速度、运存大小是完全不受我们控制的,且极容易受网络波动的影响,网络链接的耗时是非常长且不可控的。如果 WebView 每次渲染都重复经历以上整个步骤,那用户的使用体验就是完全不可控的,此时可以尝试通过以下方法来进行优化

预置离线包

  • 精简并抽取公共的 JS 和 CSS 文件作为通用的页面模板,可以按业务类型来生成多套模板文件,每次打包时均预置最新的模板文件到客户端中,每套模板文件均有特定的版本号,App 在后台定时去静默更新。通过这种方式来避免每次使用都要去联网请求,从而缩短总耗时
  • 一般情况下,WebView 会在加载完主 HTML 之后才去加载 HTML 中的 JS 和 CSS 文件,先后需要进行多次 IO 操作,我们可以将 JS 和 CSS 还有一些图片都内联到一个文件中,这样加载模板时就只需要一次 IO 操作,也大大减少了因为 IO 加载冲突导致模板加载失败的问题

并行请求

  • H5 在加载模板文件的同时,由 Native 端来请求正文数据,Native 端再通过 JS 将正文数据传给 H5,以此来实现并行请求从而缩短总耗时

预加载

  • 当模板和正文数据分离之后,由于 WebView 每次使用的都是同一个模板文件,因此我们并不需要在用户进入页面的时候才去加载模板,可以直接在预加载 WebView 的同时就让其预热加载模板,这样每次使用时仅需要将正文数据传给 H5,H5 收到数据后直接进行页面渲染即可
  • 对于 Feed 流,可以通过一定策略去预加载正文数据,当用户点击查看详情时,最理想情况下就可以直接使用缓存的数据,避免受到网络的影响

延迟加载

  • 呈现首屏页面所需要的依赖项越多,就意味着用户需要的等待时间就越长,因此要尽可能地减少在首屏完成前执行的操作,对于一些非首屏必需的网络请求、 JS 调用、埋点上报等,都可以后置到首屏显示后再执行

页面静态直出

  • 并行请求正文数据虽然能够缩短总耗时,但还是需要完成解析 JSON、构造 DOM、应用 CSS 样式等一系列耗时操作,最终才能交由内核进行渲染上屏,此时 组装 HTML 这个操作就显得比较耗时了。为了进一步缩短总耗时,可以改为由后端对正文数据和前端代码进行整合,直出首屏内容,直出后的 HTML 文件已经包含了首屏展现所需的内容和样式,无需进行二次加工,内核可以直接渲染。其它动态内容可以在渲染完首屏后再进行异步加载
  • 由于客户端可能向用户提供了控制 WebView 字体大小,夜间模式的选项,为了保证首屏渲染结果的准确性,服务端直出的 HTML 就需要预留一些占位符用于后续动态回填,客户端在 loadUrl 之前先利用正则匹配的方式查找这些占位字符,按照协议映射成端信息。经过客户端回填处理后的 HTML 内容就已经具备了展现首屏的所有条件

复用 WebView

  • 更进一步的做法就是可以尝试复用 WebView。由于 WebView 使用的模板文件已经是固定的了,因此我们可以在 WebView 预加载缓存池的基础上增加复用 WebView 的逻辑,当 WebView 使用完毕后可以将其正文数据全部清空并再次存入缓存池中,等下次需要时就可以直接注入新的正文数据进行复用了,从而减少了频繁创建 WebView 和预热模板文件带来的开销

视觉优化

实现以上的优化方案后,页面的展现速度已经很快了,但在实际开发中还是会发现存在 Activity 切换过程中无法渲染 H5 页面的问题,产生视觉上的白屏现象,这可以通过开发者模式放慢动画时间来验证

从下图可以看到在 Activity 切换过程中的确是有一段明显的白屏过程

通过研究系统源码可以知道,在系统版本大于等于 4.3,小于等于 6.0 之间,ViewRootImpl 在处理 View 绘制的时候,会通过一个布尔变量 mDrawDuringWindowsAnimating 来控制 Window 在执行动画的过程中是否允许进行绘制,该字段默认为 false,我们可以利用反射的方式去手动修改这个属性,避免这个白屏效果

这个方案基本也只适用于 Android 6.0 版本了,更低的系统版本也很少进行适配了

/**
* 让 activity transition 动画过程中可以正常渲染页面
*/

fun setDrawDuringWindowsAnimating(view: View) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1
) {
//小于 4.3 和大于 6.0 时不存在此问题,无须处理
return
}
try {
val rootParent: ViewParent = view.rootView.parent
val method: Method = rootParent.javaClass
.getDeclaredMethod("setDrawDuringWindowsAnimating", Boolean::class.javaPrimitiveType)
method.isAccessible = true
method.invoke(rootParent, true)
} catch (e: Throwable) {
e.printStackTrace()
}
}

优化后的效果

四、Http 缓存策略

在上一步的渲染优化中就涉及到了对网络请求的优化,包括 减少网络请求次数、并行执行网络请求、网络请求预执行 等。对于应用来说,网络请求是不可避免的,但我们可以通过设定缓存策略来避免重复执行网络请求,或者是可以用比较低的成本来完成非首次的网络请求,这就涉及到了和 Http 缓存相关的知识点

WebView 一共支持以下四种缓存策略,默认使用的是 LOAD_DEFAULT,该策略就属于 Http 缓存策略

  • LOAD_CACHE_ONLY:只使用本地缓存,不进行网络请求
  • LOAD_NO_CACHE:不使用本地缓存,只通过网络请求
  • LOAD_CACHE_ELSE_NETWORK:只要本地有缓存就进行使用,否则就通过网络请求
  • LOAD_DEFAULT:根据 Http 协议来决定是否进行网络请求

以请求网络上一个静态文件为例,查看其响应头,当中的 Cache-Control、Expires、Etag、Last-Modified 等信息就定义了具体的缓存策略

Cache-Control、Expires

Cache-Control 是 Http 1.1 中新增加的一个用来定义资源缓存策略的报文头,它由一些定义一个响应资源应该何时被缓存、如何被缓存以及缓存多长时间的指令组成,可选值有很多种:no-cache、no-store、only-if-cached、max-age 等,比如上图所示就使用到了 max-age 来设定资源的最大有效时间,时间单位为秒

Expires 是 Http 1.0 中规定的字段,含义和 Cache-Control 类似,但由于 Expires 可能会因为客户端和服务端的时间不一致造成缓存失效,因此现在主要使用的是 Cache-Control,在优先级上也是 Cache-Control 更高

Cache-Control 也是一个通用的 Http 报文头字段,它可以分别在请求头和响应头中使用,具有不同的含义,以 max-age 为例:

  • 请求头:客户端用于告知服务端,希望接收一个有效期不大于 max-age 的资源
  • 响应头:服务端用于告知客户端,该资源在请求发起后的 max-age 时间内均是有效的,上图所示的 2592000 秒也即 30 天,客户端在第一次发起请求后的 30 天内无需再向服务端进行请求,可以直接使用本地缓存

如果在 WebView 中使用了 LOAD_DEFAULT 的话,就会遵循此 Http 缓存策略,在有效期内 WebView 会直接使用本地缓存

ETag、Last-Modified

Cache-Control 避免了 WebView 在有效期内去重复请求资源,有效期过了后 WebView 就还是需要重新去请求网络,但此时服务端的资源也许并没有发生变化,WebView 依然可以使用本地缓存,此时客户端就需要依靠 ETag 和 Last-Modified 这两个报文头来向服务器确认该资源是否可以继续使用

在第一次请求资源的时候,响应头中就包含了 ETag 和 Last-Modified,这两个报文头就用来唯一标识该资源文件

  • ETag:用于作为资源的唯一标识信息
  • Last-Modified:用于记录资源的最后一次修改时间

等客户端判断到 max-age 已过期后,就会携带这两个报文头去执行网络请求,服务端就通过这两个标识符来判断客户端的缓存资源是否可以继续使用

如下图所示,在有效期过后,客户端会在 If-None-Match 请求头中携带上第一次网络请求时拿到的 ETag 值。实际上 ETag 和 Last-Modified 可以只使用一个,以下就只使用到了 ETag;如果要传递 Last-Modified 的话,对应的请求头就是 If-Modified-Since

如果服务端判断出资源已过期,就会返回新的资源文件,此时就相当于在第一次请求资源文件,后续操作就和一开始保持一致;如果服务端判断资源还未过期,则会返回一个 304 状态码,告知客户端可以继续使用本地缓存,客户端同时更新 max-age 值,重复一开始的的缓存失效规则,这样客户端就可以用极低的成本来完成本次网络请求,这在请求的资源文件比较大的时候特别有用

但 Http 缓存策略也存在一些问题需要注意,即如何保证用户在资源更新了时能马上感知到且重新下载最新资源。假设服务端在资源有效期内更新了资源内容,此时由于客户端还处于 max-age 阶段,无法马上感知到资源已更新,从而造成更新不及时。一种比较好的解决方案就是:要求服务端在每次更新资源文件时都为其生成一个新的名字,可以用 hash 值或者随机数命名,而资源文件依托的主文件在每次发版时都引用最新的资源文件路径,从而保证客户端能够马上就感知到资源已更新,从而保证及时更新。而且,通过这种方案,既可以为资源文件设定一个非常大的 max-age 值,尽量让客户端只使用本地缓存,又可以保证每次发版时客户端都能及时更新

所以说,通过合理地设定 Http 缓存策略,一方面能够很明显地减少服务器网络带宽消耗、降低服务器的压力和开销,另一方面也可以减少客户端网络延迟的情况、避免重复请求资源文件、加快页面的打开速度,毕竟加载本地缓存文件的开销怎样都要比从网络上加载低得多

五、拦截请求与共享缓存

如今的 WebView 页面往往是图文混排的,图片是资讯类应用的重要表现形式,WebView 获取图片资源的传统方案有以下两种:

  • H5 端自己通过网络请求去下载资源。优点:实现简单,各端之间可以只专注自己的业务。缺点:两端之间的无法共享缓存,造成资源重复请求,流量浪费
  • H5 端通过调用 Native 的图片下载和缓存能力来获取资源。优点:可以实现两端之间的缓存共享。缺点:需要由 H5 端来主动触发 Native 执行,时机较为延迟,且需要通过多次 JS 调用完成资源传递,存在效率问题

以上两种方案都存在着一些缺点,要么是无法共享缓存,要么是存在效率问题,这里就再介绍一种改进方案:

实际上,WebViewClient 提供了一个 shouldInterceptRequest 方法用于支持外部去拦截请求,WebView 每次在请求网络资源时都会回调该方法,方法入参就包含了 Url,Header 等请求参数,返回值 WebResourceResponse 即代表获取到的资源对象,默认是返回 null,即由浏览器内核自己去完成网络请求

我们可以通过该方法来主动拦截并完成图片的加载操作,这样我们既可以使得两端的资源文件得以共享,也避免了多次 JS 调用带来的效率问题

大致实现就如下所示,这里我通过 OkHttp 来代理实现网络请求

/**
* @Author: leavesC
* @Date: 2021/10/4 18:56
* @Desc:
* @公众号:字节数组
*/

object WebViewInterceptRequestProxy {

private lateinit var application: Application

private val webViewResourceCacheDir by lazy {
File(application.cacheDir, "RobustWebView")
}

private val okHttpClient by lazy {
OkHttpClient.Builder().cache(Cache(webViewResourceCacheDir, 100L * 1024 * 1024))
.followRedirects(false)
.followSslRedirects(false)
.addNetworkInterceptor(
ChuckerInterceptor.Builder(application)
.collector(ChuckerCollector(application))
.maxContentLength(250000L)
.alwaysReadResponseBody(true)
.build()
)
.build()
}

fun init(application: Application) {
this.application = application
}

fun shouldInterceptRequest(webResourceRequest: WebResourceRequest?): WebResourceResponse? {
if (webResourceRequest == null || webResourceRequest.isForMainFrame) {
return null
}
val url = webResourceRequest.url ?: return null
if (isHttpUrl(url)) {
return getHttpResource(url.toString(), webResourceRequest)
}
return null
}

private fun isHttpUrl(url: Uri): Boolean {
val scheme = url.scheme
log("url: $url")
log("scheme: $scheme")
if (scheme == "http" || scheme == "https") {
return true
}
return false
}

private fun getHttpResource(
url: String,
webResourceRequest: WebResourceRequest
): WebResourceResponse? {
val method = webResourceRequest.method
if (method.equals("GET", true)) {
try {
val requestBuilder =
Request.Builder().url(url).method(webResourceRequest.method, null)
val requestHeaders = webResourceRequest.requestHeaders
if (!requestHeaders.isNullOrEmpty()) {
var requestHeadersLog = ""
requestHeaders.forEach {
requestBuilder.addHeader(it.key, it.value)
requestHeadersLog = it.key + " : " + it.value + "\n" + requestHeadersLog
}
log("requestHeaders: $requestHeadersLog")
}
val response = okHttpClient.newCall(requestBuilder.build())
.execute()
val body = response.body
if (body != null) {
val mimeType = response.header(
"content-type", body.contentType()?.type
).apply {
log(this)
}
val encoding = response.header(
"content-encoding",
"utf-8"
).apply {
log(this)
}
val responseHeaders = mutableMapOf<String, String>()
var responseHeadersLog = ""
for (header in response.headers) {
responseHeaders[header.first] = header.second
responseHeadersLog =
header.first + " : " + header.second + "\n" + responseHeadersLog
}
log("responseHeadersLog: $responseHeadersLog")
var message = response.message
val code = response.code
if (code == 200 && message.isBlank()) {
message = "OK"
}
val resourceResponse =
WebResourceResponse(mimeType, encoding, body.byteStream())
resourceResponse.responseHeaders = responseHeaders
resourceResponse.setStatusCodeAndReasonPhrase(code, message)
return resourceResponse
}
} catch (e: Throwable) {
log("Throwable: $e")
}
}
return null
}

private fun getAssetsImage(url: String): WebResourceResponse? {
if (url.contains(".jpg")) {
try {
val inputStream = application.assets.open("ic_launcher.webp")
return WebResourceResponse(
"image/webp",
"utf-8", inputStream
)
} catch (e: Throwable) {
log("Throwable: $e")
}
}
return null
}

}

采用此方案的好处有:

  • 通过 OkHttp 本身的 Cache 功能来实现资源缓存,并不局限于特定的文件类型,可以用于图片、HTML、JS、CSS 等多种类型
  • OkHttp 是完全遵循 Http 协议的,我们可以在这基础上来自由扩展 Http 缓存策略
  • 解耦了客户端和前端代码,由客户端充当 Server 的角色,对于前端来说是完全无感知的,用比较低的成本就实现了两端缓存共享
  • WebView 自带的缓存机制允许的最大缓存空间是比较小的,此方案相当于突破了 WebView 的最大缓存容量限制
  • 如果移动端已经预置了离线包,那么就可以通过此方案判断离线包是否已经包含目标文件,存在的话直接使用,否则才联网请求,参照上述的 getAssetsImage 方法

需要注意,以上只是一份示例代码,并不能直接用于生产环境,读者需要根据具体业务去进行扩展。Github 上也有一个通过此方案实现了 WebView 缓存复用的开源库,读者可以去借鉴其思路:CacheWebView

六、DNS 优化

DNS 也即域名解析,指代的是将域名转换为具体的 IP 地址的过程。DNS 会在系统级别进行缓存,如果已经解析过某域名,那么在下次使用时就可以直接去访问已知的 IP 地址,而不用先发起 DNS 再访问 IP 地址

如果 WebView 访问的主域名和客户端的不一致,那么 WebView 在首次访问线上资源时,就需要先完成域名解析才能开始资源请求,这个过程就需要多耗费几十毫秒的时间。因此最好就是保持客户端整体 API 地址、资源文件地址、WebView 线上地址的主域名都是一致的

七、CDN 加速

CDN 的全称是 Content Delivery Network,即内容分发网络。CDN 是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率

通过将 JS、CSS、图片、视频等静态类型文件托管到 CDN,当用户加载网页时,就可以从地理位置上最接近它们的服务器接收这些文件,解决了远距离访问和不同网络带宽线路访问造成的网络延迟情况

八、白屏检测

在正常情况下,完成上述的优化措施后用户基本是可以秒开 H5 页面的了。但异常情况总是会有的,用户的网络环境和系统环境千差万别,甚至 WebView 也可能发生内部崩溃。当发生问题时,用户看到的可能就直接只是一个白屏页面了,所以进一步的优化手段就是需要去检测是否发生白屏以及相应的应对措施

检测白屏最直观的方案就是对 WebView 进行截图,遍历截图的像素点的颜色值,如果非白屏颜色的颜色点超过一定的阈值,就可以认为不是白屏。字节跳动技术团队的做法是:通过 View.getDrawingCache()方法去获取包含 WebView 视图的 Bitmap 对象,然后把截图缩小到原图的 1/6,遍历检测图片的像素点,当非白色的像素点大于 5% 的时候就可以认为是非白屏的情况,可以相对高效且准确地判断出是否发生了白屏

当检测到白屏后,如果发现怎么重试也无法成功,那就只能进行降级处理了,放弃上述的优化措施,直接加载线上的详情页,优先保证用户体验


收起阅读 »

Flutter怎么样做国际化

什么是国际化 国际化是指在设计软件时,将软件与特定语言及地区脱钩的过程。当软件被移植到不同的语言地区时,软件本身不用做内部工程上的改变或修正。 本地化则是指当移植软件时,加上与特定区域设置有关的资讯和翻译文件的过程。 国际化和本地化之间的区别虽然微妙,但却很重...
继续阅读 »

什么是国际化


国际化是指在设计软件时,将软件与特定语言及地区脱钩的过程。当软件被移植到不同的语言地区时,软件本身不用做内部工程上的改变或修正。


本地化则是指当移植软件时,加上与特定区域设置有关的资讯和翻译文件的过程。 国际化和本地化之间的区别虽然微妙,但却很重要。国际化意味着产品有适用于任何地方的潜力;本地化则是为了更适合于特定地方的使用,而另外增添的特色。用一项产品来说,国际化只需做一次,但本地化则要针对不同的区域各做一次。 这两者之间是互补的,并且两者结合起来才能让一个系统适用于各地。


国际化实现中的困难


开发软件时,国际化和本地化对开发者是一个有挑战性的任务,特别是当软件当初设计时没有考虑这个问题时。通常做法是将文本和其他环境相关的资源与程序代码相分离。这样在理想的情况下,应对变化的环境时无需修改代码,只要修改资源,从而显著简化了工作。


Flutter的国际化


Flutter中的国际化包括Flutter组件的国际化和其他文本的国际化两者;


Flutter组件的国际化


Flutter给我们提供的Widget默认情况下就是支持国际化,但是在没有进行特别的设置之前,它们无论在什么环境都是以英文的方式显示的。


如果想要添加其他语言,你的应用必须指定额外的 MaterialApp 属性并且添加一个单独的 package,叫做 flutter_localizations


截至 2020 年 11 月,该软件包支持 78 种语言。


pubspec添加依赖


想要使用 flutter_localizations 的话,我们需要在 pubspec.yaml 文件中添加它作为依赖:


dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter

设置MaterialApp




  • 在localizationsDelegates中指定哪些Widget需要进行国际化



    • 用于生产本地化值集合的工厂

    • 我们这里指定了Material、Widgets、Cupertino都使用国际化




  • supportedLocales指定要支持哪些国际化



    • 我们这里指定中文和英文(也可以指定国家编码)




MaterialApp(
localizationsDelegates: [
GlobalMaterialLocalizations.delegate, // 指定本地化的字符串
GlobalCupertinoLocalizations.delegate, // 对应的Cupertino风格
GlobalWidgetsLocalizations.delegate // 指定默认的文本排列方向, 由左到右或由右到左
],
supportedLocales: [
Locale("en"),
Locale("zh")
],
)

注意:如果要指定语言代码、文字代码和国家代码,可以进行如下指定方式:


// Full Chinese support for CN, TW, and HK
supportedLocales: [
const Locale.fromSubtags(languageCode: 'zh'), // generic Chinese 'zh'
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'), // generic simplified Chinese 'zh_Hans'
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), // generic traditional Chinese 'zh_Hant'
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'), // 'zh_Hans_CN'
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'TW'), // 'zh_Hant_TW'
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'HK'), // 'zh_Hant_HK'
],

Flutter中自定义文本的国际化


创建本地化类


该类用于定义我们需要进行本地化的字符串等信息:



  • 1.我们需要一个构造器,并且传入一个Locale对象

  • 2.定义一个Map,其中存放我们不同语言对应的文本

  • 3.定义它们对应的getter方法,根据语言环境返回不同的结果


import 'package:flutter/material.dart';

class QWLocalizations {
final Locale locale;

QWLocalizations(this.locale);

static Map<String, Map<String, String>> _localizedValues = {
"fr": {"title": "Titre", "hello": "Bonjour"},
"zh": {"title": "首页", "hello": "你好"}
};

String get title {
return _localizedValues[locale.languageCode]?["title"] ?? 'title';
}

String get hello {
return _localizedValues[locale.languageCode]?["hello"] ?? 'hello';
}

static QWLocalizations of(BuildContext context) {
return Localizations.of(context, QWLocalizations);
}
}

自定义Delegate


上面的类定义好后,我们在什么位置或者说如何对它进行初始化呢?
我们可以像Flutter Widget中的国际化方式一样对它们进行初始化,也就是我们可以定义一个对象的Delegate类,并且将其传入localizationsDelegates中;


Delegate的作用就是当Locale发生改变时,调用对应的load方法,重新加载新的Locale资源


HYLocalizationsDelegate需要继承自LocalizationsDelegate,并且有三个方法必须重写:
isSupported,shouldReload,load


import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';

import 'QWLocalizations.dart';

class QWLocalizationsDelegate extends LocalizationsDelegate<QWLocalizations> {
//是否在我们支持的语言范围
@override
bool isSupported(Locale locale) {
return ["fr", "zh"].contains(locale.languageCode);
}

/*
* 当Localizations Widget重新build时,是否调用load方法重新加载Locale资源
一般情况下,Locale资源只应该在Locale切换时加载一次,不需要每次Localizations重新build时都加载一遍;
所以一般情况下返回false即可;
* */
@override
bool shouldReload(LocalizationsDelegate<QWLocalizations> old) {
return false;
}

/*
* 当Locale发生改变时(语言环境),加载对应的HYLocalizations资源
这个方法返回的是一个Future,因为有可能是异步加载的;
但是我们这里是直接定义的一个Map,因此可以直接返回一个同步的Future(SynchronousFuture)
* */
@override
Future<QWLocalizations> load(Locale locale) {
return SynchronousFuture(QWLocalizations(locale));
}

static QWLocalizationsDelegate delegate = QWLocalizationsDelegate();
}

异步加载数据


假如我们的数据是异步加载的,比如来自Json文件或者服务器,应该如何处理呢?


QWLocalizations类中如下面代码:


  static Map<String, Map<String, String>> _localizedValues = {};

Future<bool> loadJson() async {
// 1.加载json文件
String jsonString = await rootBundle.loadString("assets/json/i18n.json");

// 2.转成map类型
final Map<String, dynamic> map = json.decode(jsonString);

// 3.注意:这里是将Map<String, dynamic>转成Map<String, Map<String, String>>类型
_localizedValues = map.map((key, value) {
return MapEntry(key, value.cast<String, String>());
});
return true;
}

在QWLocalizationsDelegate中使用异步进行加载:


  @override
Future<QWLocalizations> load(Locale locale) async {
final localization = QWLocalizations(locale);
await localization.loadJson();
return localization;
}

使用本地化类


接着我们可以在代码中使用HYLocalization类。



  • 我们可以通过QWLocalizations.of(context)获取到QWLocalizations对象


Text(
QWLocalizations.of(context).hello,
)

国际化的工具---Intl


认识arb文件


目前我们已经可以通过加载对应的json文件来进行本地化了。


但是还有另外一个问题,我们在进行国际化的过程中,下面的代码依然需要根据json文件手动编写


String get title {
return _localizedValues[locale.languageCode]?["title"] ?? 'title';
}

String get hello {
return _localizedValues[locale.languageCode]?["hello"] ?? 'hello';
}

有没有一种更好的方式,让我们可以快速在本地化文件即dart代码文件直接来转换呢?答案就是arb文件



  • arb文件全称Application Resource Bundle,表示应用资源包,目前已经得到Google的支持;

  • 其本质就是一个json文件,但是可以根据该文件转成对应的语言环境;

  • arb的说明文档:github.com/google/app-…


使用IDE插件来进行arb和dart文件之间的转换




  • 初始化intl




选择工具栏Tools - Flutter Intl - Initialize for the Project



完成上面的操作之后会自动生成如下文件目录:



  • generated是自动生成的dart代码

  • I10n是对应的arb文件目录



使用intl


在localizationsDelegates中配置生成的class,名字是S



  • 1.添加对应的delegate

  • 2.supportedLocales使用S.delegate.supportedLocales


localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
S.delegate
],
supportedLocales: S.delegate.supportedLocales,

因为我们目前还没有对应的本地化字符串,所以需要在intl_en.arb文件中编写:


{
"title": "home",
"hello": "hello"
}



  • 编写后ctrl(command) + s保存即可;




之后按照如下格式在代码中使用


S.of(context).title

添加中文


如果希望添加中文支持:add local




  • 在弹出框中输入zh即可


我们会发现,会生成对应的intl_zh.arb和messages_zh.dart文件



arb其它语法


如果我们希望在使用本地化的过程中传递一些参数:



  • 比如hello kobe或hello james

  • 比如你好啊,李银河或你好啊,王小波


修改对应的arb文件:



  • {name}:表示传递的参数


{
"title": "home",
"hello": "hello",
"sayHello": "hello {name}"
}

在使用时,传入对应的参数即可:


Text(S.of(context).sayHello("李银河")),

总结


文本的国际化实质就是根据系统提供的locale信息去获取对应的文本和对UI做相应操作(指从左到右还是从右到左展示),locale信息是指国家代码、地区代码等,通常我们本地需要做的就是把文案按照{国家代码:{通用文本:本地化文案}}的格式进行组织排列。这里国家代码比如中国是zh,美国是en。通用文本一般用英文。最后就是根据locale信息和通用文本去字典获取本地化文本值的过程。


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

Flutter 绘制番外篇 - 圆中取形

前言: 对一些有趣的绘制 技能和知识, 我会通过 [番外篇] 的形式加入《Flutter 绘制指南 - 妙笔生花》小册中,一方面保证小册的“与时俱进” 和 “活力”。另一方面,是为了让一些重要的知识有个 好的归宿。 一、正 N 边形的绘制 1. 正三角形绘制...
继续阅读 »
前言:

对一些有趣的绘制 技能知识, 我会通过 [番外篇] 的形式加入《Flutter 绘制指南 - 妙笔生花》小册中,一方面保证小册的“与时俱进”“活力”。另一方面,是为了让一些重要的知识有个 好的归宿




一、正 N 边形的绘制


1. 正三角形绘制

对于正 N 形而言,绘制的本质就是对点的收集。如下图,外接圆上,平均等分三份,对应弧度的圆上坐标即为待收集的点。将这些点依次相连,即可得到期望的图形。





容易看出,对于正三角形,三个点分别位于 120°240° 的圆上。通过 三角函数更新很容易求得三个点的坐标,并用 points 列表进行记录。


@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
int count = 3;
double radius = 140 / 2;
List<Offset> points = [];
for (int i = 0; i < count; i++) {
double perRad = 2 * pi / count * i;
points.add(Offset(radius * cos(perRad), radius * sin(perRad)));
}
_drawShape(canvas, points);
}



得到点集之后,就可以形成路径进行绘制。本例全部源码位于: 01_triangle



final Paint shapePaint = Paint()
..style = PaintingStyle.stroke;

void _drawShape(Canvas canvas, List<Offset> points) {
Path shapePath = Path();
shapePath.moveTo(points[0].dx, points[0].dy);
for (int i = 1; i < points.length; i++) {
shapePath.lineTo(points[i].dx, points[i].dy);
}
shapePath.close();
canvas.drawPath(shapePath, shapePaint);
}



2. 正 N 边形

正三角形 同理,改变上面的 count 值,就可以将圆等分成 count 份,再对圆上对应点进行收集即可。























正四边形正五边形
正六边形正七边形
image-20211007132438225

可能大家会觉得上面奇数情况下,不是很。因为上面以水平方向的 为起点,是上下对称。视觉上,我们更习惯于 左右对称。想实现如下的左右对称正 N 边形,其实也很简单,在计算点位时逆时针旋转 90°即可。



double rotate = - pi / 2; 
for (int i = 0; i < count; i++) {
double perRad = 2 * pi / count * i;
points.add(Offset(
radius * cos(perRad + rotate), // 在计算时加上旋转量
radius * sin(perRad + rotate),
));
}

另外,通过圆的半径大小可以控制 正 N 边形 的大小。本例全部源码位于: 02_n_side




二、 N 角星的绘制


1、五角星的绘制

先看下思路:前面我们已经知道如何收录 正五边形 的五个点,现在再搞个小的 正五边形 。如果将两个点集进行交错合并,实现首尾相连会是什么样子呢?也就是 红0--蓝0--红1--蓝1--红2--蓝2...



这里外圆的五个点集为 outPoints,内圆的五个点集为 innerPoints 。让两个列表交错合并也非常简单,就是指定索引插入元素而已。


for(int i =0; i< count; i++){
outPoints.insert(2*i+1, innerPoints[i]);
}

这样将合并的点集形成路径,就可以得到如下的图形:





上面图形已经有点 五角星 的外貌了,可以看出只要在收集内圆上点时,顺时针偏转一下角度就行了。比如下面偏转了 15° ,看起来就更像了:



double innerRadius = 70 / 2;
List<Offset> innerPoints = [];
double offset = 15 * pi / 180;
for (int i = 0; i < count; i++) {
double perRad = 2 * pi / count * i;
innerPoints.add(Offset(
innerRadius * cos(perRad + offset),
innerRadius * sin(perRad + offset),
));
}



那这个偏角到底是多少,才符合五角星呢?也就是求下面的 α 值是多少,由于小圆上五个点是 正五边形,所以 β180°*(5-2)/5=108° ,所以 α = 180°-108°/2-90°=36°



这样就得到了一个标准的五角星,只不过是上下对称的。



要改成左右对称 很简单,上面也说过,在计算点位时,逆时针旋转 90° 即可:本例全部源码位于: 03_five_star



List<Offset> innerPoints = [];
double offset = pi / count;
for (int i = 0; i < count; i++) {
double perRad = 2 * pi / count * i;
innerPoints.add(Offset(
innerRadius * cos(perRad + rotate + offset),
innerRadius * sin(perRad + rotate + offset),
));
}

通过 外圆半径/内圆半径 可以控制五角星的 胖瘦

















70/4070/2870/15



2. N 角星的绘制

五角星完成了,其它的也就水到渠成。最重要的一步是找到角度偏移量 αn 的对应关系,不难算出:


α = 180°- 180°*(n-2)/n/2-90°
= 180°/n

注: n 边形的内角和为 180°*(n-2)

上面为了方便理解,使用了两个点集分别收集内外圆上的点,最后进行整合。理解原理后,我们可以一次性收集两个圆上的点,避免而外的合并操作。代码如下:


int count = 6;
double outRadius = 140 / 2;
double innerRadius = 70 / 2;
double offset = pi / count;
List<Offset> outPoints = [];

double rotate = -pi / 2;
for (int i = 0; i < count; i++) {
double perRad = 2 * pi / count * i;
outPoints.add(Offset(
outRadius * cos(perRad + rotate),
outRadius * sin(perRad + rotate),
));
outPoints.add(Offset(
innerRadius * cos(perRad + rotate + offset),
innerRadius * sin(perRad + rotate + offset),
));
}



这样,对于不同的 count ,就可以得到对应角数的星星。如下是 2~9 角星:





三、形状路径的使用


1、路径工具的使用

上面把所有的计算逻辑都塞在了画板中,显得非常杂乱,完全可以把这些路径形成逻辑单独抽离出来。如下 ShapePath 类,使用者只需要进行 基本参数配置 来创建对象即可,通过对象来拿到相关路径。本例全部源码位于: 04_n_star


// ShapePath型 成员变量
late ShapePath shapePath = ShapePath.star(
n: n,
outRadius: 140 / 2,
innerRadius: 80 / 2,
);

// 获取 shapePath 中的路径
canvas.drawPath(shapePath.path, shapePaint);

只需要两行代码,就可以通过ShapePath.star 构造,获得 n 角星的路径:





也通过ShapePath.polygon 构造,获得正 n 边形的路径:





2、路径工具的封装

ShapePath 中有四个成员,其中 noutRadiusinnerRadius 是路径信息的配置,_path 是路径。在获取路径时做了个判断:如果路径为空,则先通过之前的逻辑构建路径,否则,直接返回已有路径。这样可以避免同一 ShapePath 对象构建多次相同的路径。


import 'dart:math';
import 'dart:ui';

class ShapePath {

ShapePath.star({
this.n = 5,
this.outRadius = 100,
this.innerRadius = 60,
});

ShapePath.polygon({
this.n = 5,
this.outRadius = 100,
}) : innerRadius = null;

final int n;
final double outRadius;
final double? innerRadius;
Path? _path;

Path get path {
if (_path == null) {
_buildPath();
}
return _path!;
}

void _buildPath() {
int count = n;
double offset = pi / count;
List<Offset> points = [];
double rotate = -pi / 2;
for (int i = 0; i < count; i++) {
double perRad = 2 * pi / count * i;
points.add(Offset(
outRadius * cos(perRad + rotate),
outRadius * sin(perRad + rotate),
));
if (innerRadius != null) {
points.add(Offset(
innerRadius! * cos(perRad + rotate + offset),
innerRadius! * sin(perRad + rotate + offset),
));
}
}

_path = Path();
_path!.moveTo(points[0].dx, points[0].dy);
for (int i = 1; i < points.length; i++) {
_path!.lineTo(points[i].dx, points[i].dy);
}
_path!.close();
}
}



3、路径的作用

路径是绘制操作的基石,它的作用可以说非常多,可以根据路径进行合并、裁剪、描边、填充、运动等。如下是自定义 ShapeBorder 形状进行裁剪:


ClipPath(
clipper: ShapeBorderClipper(shape: MyShapeBorder()),
child: Image.asset(
'assets/images/wy_300x200.webp',
height: 200,
))


class MyShapeBorder extends ShapeBorder{

@override
EdgeInsetsGeometry get dimensions => const EdgeInsets.all(0);

@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
return Path();
}

@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
ShapePath shapePath = ShapePath.polygon(
n: 6,
outRadius: rect.shortestSide/2,
);
return shapePath.path.shift(Offset(rect.longestSide/2,rect.shortestSide/2));
}

@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
}

@override
ShapeBorder scale(double t) {
return this;
}
}

路径的使用方式在 《Flutter 绘制指南 - 妙笔生花》相关章节有具体介绍,本文主要目的是来探讨:根据圆来拾取几何图形、并形成路径的方法。到这里,本文要介绍的内容就结束了,谢谢观看~


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

Android 开发必知必会:Java 并发之三大性质、synchronized、volatile

原子性 原子(atomic) 本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation) 意为“不可被中断的一个或一系列操作”。原子性则可以表示为:一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。 有序...
继续阅读 »

原子性


原子(atomic) 本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation) 意为“不可被中断的一个或一系列操作”。原子性则可以表示为:一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。


有序性


指的是在代码顺序结构中,我们可以直观的指定代码的执行顺序, 即从上到下按序执行。但编译器和CPU处理器会根据自己的决策,对代码的执行顺序进行重新排序。优化指令的执行顺序,提升程序的性能和执行速度,使语句执行顺序发生改变,出现重排序,但最终结果看起来没什么变化(单核)。


有序性问题


指的是在多线程环境下(多核),由于执行语句重排序后,重排序的这一部分没有一起执行完,就切换到了其它线程,导致的结果与预期不符的问题。这就是编译器的编译优化给并发编程带来的程序有序性问题


指令重排序


为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入的代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,并确保这一结果和顺序执行结果是一致的,但是这个过程并不保证各个语句计算的先后顺序和输入代码中的顺序一致。这就是指令重排序。


可见性


当一个线程修改一个线程共享变量时,另外的线程能够读到这个修改的值。也就是说,被修饰的共享变量被任何线程读取的时候都能拿到最新的值。


synchronized


定义:



在多线程的环境下,多个线程同时访问共享资源会出现一些问题,而 synchronized 关键字则是用来保证线程同步的。synchronizedJava 提供的一个并发控制的关键字。主要有两种用法,分别是同步方法和同步代码块。也就是说,synchronized 既可以修饰方法也可以修饰代码块。


Java 中的每一个对象都可以作为锁,这个对象也被称为 监视器(monitor) 。具体表现为以下3种形式:



  • 对于普通同步方法,锁是当前实例对象。

  • 对于静态同步方法,琐是当前类的 Class 对象。

  • 对于同步方法块,锁是 Syschonized 括号里配置的对象。



作用:


给修饰的方法和代码块加锁,保证同时只能有一个线程访问。


特点:


有序性原子性可见性


使用:


Java:


public class SynchronizedTest {

   private final User user = new User();

   /**
    * 同步方法,监视器为当前对象
    * 此处的锁和 synchronized(this) 是同样的
    */
   public synchronized void synchronizedMethod() {}

   /**
    * 同步静态方法,监视器为当前类的 Class 对象
    * 此处的锁和 synchronized(SynchronizedTest.class) 是同样的
    */
   public synchronized static void synchronizedStaticMethod() {}

   /**
    * 同步代码块,监视器为 synchronized(object) 传入的对象
    */
   public void synchronizedCodeBlock() {

       /* 监视器为 user 对象,同时只能有一个线程拿到 user 锁 */
       synchronized (user) {
           System.out.println(user.name);
      }

       /* 监视器为当前类的实例对象,同时只能有一个线程拿到该类的实例锁 */
       synchronized (this) {
           System.out.println("SynchronizedTest");
      }

       /* 监视器为当前类的 Class 对象,同时只能有一个线程拿到当前类的 Class 对象锁 */
       synchronized (SynchronizedTest.class) {
           System.out.println("SynchronizedTest.class");
      }
  }
}

Kotlin:


class SynchronizedTestKt {

private val user = User()

/**
* 同步方法,监视器为当前对象
* 此处的锁和 synchronized(this) 是同样的
*/
@Synchronized
fun synchronizedMethod() {}

/**
* 同步代码块,监视器为 synchronized(object) 传入的对象
*/
fun synchronizedCodeBlock() {

/* 监视器为 user 对象,同时只能有一个线程拿到 user 锁 */
synchronized(user) { println(user.name) }

/* 监视器为当前类的实例对象,同时只能有一个线程拿到该类的实例锁 */
synchronized(this) { println("SynchronizedTestKt") }

/* 监视器为当前类的 Class 对象,同时只能有一个线程拿到当前类的 Class 对象锁 */
synchronized(SynchronizedTestKt::class.java) { println("SynchronizedTestKt.class") }
}

/**
* 伴生对象
*/
companion object {

/**
* 同步伴生对象方法,监视器为当前类的 Class 对象
* 此处的锁和 synchronized(SynchronizedTestKt::class.java) 是同样的
*/
@Synchronized
fun synchronizedStaticMethod() {}
}
}

volatile


定义:



Java 语言规范第3版中对 volatile 的定义如下:Java 编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。


Java 语言提供了 volatile,在某些情况下比锁要更加方便。如果一个字段被声明成 volatileJava 线程内存模型确保所有线程看到这个变量的值是一致的 。volatile 是一种轻量且在有限的条件下线程安全技术,它保证修饰的变量的可见性和有序性,但非原子性。相对于 synchronize 高效,而常常跟 synchronize 配合使用。



作用:



  1. 保证了不同线程对该变量操作的内存可见性

  2. 禁止指令重排序


特点:


有序性非原子性可见性


实现原理:


引《Java 并发编程的艺术》书中的例子:


X86 处理器下通过工具获取 JIT 编译器生成的汇编指令来查看对 volatile 进行写操作时,CPU 会做什么事。


Java 代码:


instance = new Singleton;    // instance 是被 volatile 修饰的变量

转为汇编:


0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

volatile 变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,通过查 IA-32架构软件开发者手册可知,Lock 前缀的指令在多核处理器下会引发了两件事情:



  1. 将当前处理器缓存行的数据写回到系统内存。

  2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。


为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。


volatile 的两条实现原则:



  1. Lock 前缀指令会引起处理器缓存回写到内存。

  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。


使用:


Java:


public class Main {

   private volatile int variable = 0;
}

Kotlin:


class Main {

   @Volatile
   private var variable: Int = 0
}

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

【开源项目】Compose版SmartRefreshLayout,了解一下~

下拉刷新是我们开发中的常见的需求,官方提供了SwipeRefreshLayout来实现下拉刷新,但我们常常需要定制Header或者Header与内容一起向下滚动,因此SwipeRefreshLayout往往不能满足我们的需求 在使用XML开发时,Github上...
继续阅读 »

下拉刷新是我们开发中的常见的需求,官方提供了SwipeRefreshLayout来实现下拉刷新,但我们常常需要定制Header或者Header与内容一起向下滚动,因此SwipeRefreshLayout往往不能满足我们的需求

在使用XML开发时,Github上有不少开源库如 SmartRefreshLayout 实现了下拉刷新功能,可以方便地定制化Header与滚动方式

本文主要介绍如何开发一个简单易用的ComposeSmartRefreshLayout,快速实现下拉刷新功能,如果对您有所帮助可以点个Star: Compose版SmartRefreshLayout


效果图


我们首先看下最终的效果图















基本使用自定义Header














Lottie HeaderFixedBehind(固定在背后)














FixedFront(固定在前面)FixedContent(内容固定)

特性



  1. 接入方便,使用简单,快速实现下拉刷新功能

  2. 支持自定义Header,Header可观察下拉状态并更新UI

  3. 自定义Header支持Lottie,并支持观察下拉状态开始与暂停动画

  4. 支持自定义Translate,FixedBehind,FixedFront,FixedContent等滚动方式

  5. 支持与Paging结合实现上滑加载更多功能


使用


接入


第 1 步:在工程的build.gradle中添加:


allprojects {
repositories {
...
mavenCentral()
}
}

第2步:在应用的build.gradle中添加:


dependencies {
implementation 'io.github.shenzhen2017:compose-refreshlayout:1.0.0'
}

简单使用


SwipeRefreshLayout函数主要包括以下参数:



  1. isRefreshing: 是否正在刷新

  2. onRefresh: 触发刷新回调

  3. modifier: 样式修饰符

  4. swipeStyle: 下拉刷新方式

  5. swipeEnabled: 是否允许下拉刷新

  6. refreshTriggerRate: 刷新生效高度与indicator高度的比例

  7. maxDragRate: 最大刷新距离与indicator高度的比例

  8. indicator: 自定义的indicator,有默认值


在默认情况下,我们只需要传入isRefreshing(是否正在刷新)与onRefresh触发刷新回调两个参数即可


@Composable
fun BasicSample() {
var refreshing by remember { mutableStateOf(false) }
LaunchedEffect(refreshing) {
if (refreshing) {
delay(2000)
refreshing = false
}
}
SwipeRefreshLayout(isRefreshing = refreshing, onRefresh = { refreshing = true }) {
//...
}
}

如上所示:在触发刷新回调时将refreshing设置为true,并在刷新完成后设置为false即可实现简单的下拉刷新功能


自定义Header


SwipeRefreshLayout支持传入自定义的Header,如下所示:


@Composable
fun CustomHeaderSample() {
var refreshing by remember { mutableStateOf(false) }
LaunchedEffect(refreshing) {
if (refreshing) {
delay(2000)
refreshing = false
}
}

SwipeRefreshLayout(
isRefreshing = refreshing,
onRefresh = { refreshing = true },
indicator = {
BallRefreshHeader(state = it)
}) {
//...
}
}

如上所示:BallRefreshHeader即为自定义的Header,Header中会传入SwipeRefreshState,我们通过SwipeRefreshState可获得以下参数



  1. isRefreshing: 是否正在刷新

  2. isSwipeInProgress: 是否正在滚动

  3. maxDrag: 最大下拉距离

  4. refreshTrigger: 刷新触发距离

  5. headerState: 刷新状态,包括PullDownToRefresh,Refreshing,ReleaseToRefresh三个状态

  6. indicatorOffset: Header偏移量


这些参数都是MutableState我们可以观察这些参数的变化以实现Header UI的更新


自定义Lottile Header


Compose目前已支持Lottie,我们接入Lottie依赖后,就可以很方便地实现一个Lottie Header,并且在正在刷新时播放动画,其它时间暂停动画,示例如下:


@Composable
fun LottieHeaderOne(state: SwipeRefreshState) {
var isPlaying by remember {
mutableStateOf(false)
}
val speed by remember {
mutableStateOf(1f)
}
isPlaying = state.isRefreshing
val lottieComposition by rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(R.raw.refresh_one),
)
val lottieAnimationState by animateLottieCompositionAsState(
composition = lottieComposition, // 动画资源句柄
iterations = LottieConstants.IterateForever, // 迭代次数
isPlaying = isPlaying, // 动画播放状态
speed = speed, // 动画速度状态
restartOnPlay = false // 暂停后重新播放是否从头开始
)
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(), contentAlignment = Alignment.Center
) {
LottieAnimation(
lottieComposition,
lottieAnimationState,
modifier = Modifier.size(150.dp)
)

}
}

自定义下滑方式


SwipeRefreshLayout支持以下4种下滑方式


enum class SwipeRefreshStyle {
Translate, //平移,即内容与Header一起向下滑动,Translate为默认样式
FixedBehind, //固定在背后,即内容向下滑动,Header不动
FixedFront, //固定在前面, 即Header固定在前,Header与Content都不滑动
FixedContent //内容固定,Header向下滑动,即官方样式
}

如上所示,其中默认方式为Translate,即内容与Header一起向下滑动

各位可根据需求选择相应的下滑方式,比如要实现类似官方的下滑效果,即可使用FixedContent


上拉加载更多


Compose中,上拉加载更多直接使用Paging3看起来已经足够用了,因此本库没有实现上拉加载更多相关功能

因此如果想要实现上拉加载更多,可自行结合Paging3使用


主要原理


下拉刷新功能,其实主要是嵌套滚动的问题,我们将HeaderContent放到一个父布局中统一管理,然后需要做以下事



  1. 当我们的手指向下滚动时,首先交由Content处理,如果Content滚动到顶部了,再交由父布局处理,然后父布局根据手势进行一定的偏移,增加offset

  2. 当我们松手时,判断偏移的距离,如果大于刷新触发距离则触发刷新,否则回弹到顶部(offset置为0)

  3. 当我们手指向上滚动时,首先交由父布局处理,如果父布局的offset>0则由父布局处理,减少offset,否则则由Content消费手势


NestedScrollConnection介绍


为了实现上面说的需求,我们需要对滚动进行拦截,Compose提供了NestedScrollConnection来实现嵌套滚动


interface NestedScrollConnection {
fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero

fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = Offset.Zero

suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero

suspend fun onPostFling(consumed: Velocity, available: Velocity) = return Velocity.Zero
}

如上所示,NestedScrollConnection主要提供了4个接口



  1. onPreScroll: 先拦截滑动事件,消费后再交给子布局

  2. onPostScroll: 子布局处理完滑动事件后再交给父布局,可获取当前还剩下多少可用的滑动事件偏移量

  3. onPreFling: Fling开始前回调

  4. onPostFling: Fling完成后回调



Fling含义:当我们手指在滑动列表时,如果是快速滑动并抬起,则列表会根据惯性继续飘一段距离后停下,这个行为就是 FlingonPreFling 在你手指刚抬起时便会回调,而 onPostFling 会在飘一段距离停下后回调。



具体实现


上面我们已经介绍了总体思路与NestedScrollConnection API,然后我们应该需要重写以下方法



  1. onPostScroll: 当Content滑动到顶部时,如果继续往上滑,我们就应该增加父布局的offset,因此在onPostScroll中判断available.y > 0,然后进行相应的偏移,对我们来说是个合适的时机

  2. onPreScroll: 当我们上滑时,如果offset>0,则说明父布局有偏移,因此我们应先减小父布局的offset直到0,然后将剩余的偏移量传递给Content,因此下滑时应该使用onPreScroll拦截判断

  3. onPreFling: 当我们松开手时,应判断当前的偏移量是否大于刷新触发距离,如果大于则触发刷新,否则父布局的offset置为0,这个判断在onPreFling时做比较合适


具体实现如下:


internal class SwipeRefreshNestedScrollConnection() : NestedScrollConnection {
override fun onPreScroll(
available: Offset,source: NestedScrollSource
)
: Offset = when {
// 如果用户正在上滑,需要在这里拦截处理
source == NestedScrollSource.Drag && available.y < 0 -> onScroll(available)
else -> Offset.Zero
}

override fun onPostScroll(
consumed: Offset,available: Offset,source: NestedScrollSource
)
: Offset = when {
// 如果用户正在下拉,在这里处理剩余的偏移量
source == NestedScrollSource.Drag && available.y > 0 -> onScroll(available)
else -> Offset.Zero
}

override suspend fun onPreFling(available: Velocity): Velocity {
//如果偏移量大于刷新触发距离,则触发刷新
if (!state.isRefreshing && state.indicatorOffset >= refreshTrigger) {
onRefresh()
}
//不消费速度,直接返回0
return Velocity.Zero
}
}

总结


本文主要介绍如何使用及实现一个Compose版的SmartRefreshLayout,它具有以下特性:



  1. 接入方便,使用简单,快速实现下拉刷新功能

  2. 支持自定义Header,Header可观察下拉状态并更新UI

  3. 自定义Header支持Lottie,并支持观察下拉状态开始与暂停动画

  4. 支持自定义Translate,FixedBehind,FixedFront,FixedContent等滚动方式

  5. 支持与Paging结合实现上滑加载更多功能


项目地址


Compose版SmartRefreshLayout

开源不易,如果项目对你有所帮助,欢迎点赞,Star,收藏~


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

优雅地处理运行时权限请求

前言从android 6.0(API 级别 23)开始,android引入了运行时权限,用户开始在应用运行时向其授予权限,而不是在应用安装时向其授予权限,如果应用的某项功能需要使用到受运行时权限保护的资源(例如相机、位置、麦克风等),但在运行该功能前没有动态地...
继续阅读 »

前言

从android 6.0(API 级别 23)开始,android引入了运行时权限,用户开始在应用运行时向其授予权限,而不是在应用安装时向其授予权限,如果应用的某项功能需要使用到受运行时权限保护的资源(例如相机、位置、麦克风等),但在运行该功能前没有动态地申请相应的权限,那么在调用该功能时就会抛出SecurityException异常, android 6.0已经推出了很多年了,相信大家对于运行时权限的申请过程已经非常的熟悉,但是android的运行时权限的申请过程一直都是非常的繁琐的,主要有两步:

1、在需要申请权限的地方检查该权限是否被同意,如果同意了就直接执行,如果不同意就动态申请权限;

2、重写Activity或Fragment的onRequestPermissionsResult方法,在里面根据grantResults数组判断权限是否被同意,如果同意就直接执行,如果不同意就要进行相应的提示,如果用户勾选了“don't ask again”,还要引导用户去“Settings”界面打开权限,这时还要重写onActivityResult判断权限是否被同意.

就是这简单的两步,却夹杂了大量的if else语句,不但不优雅,而且每次都要写重复的样板代码,可能android的开发者也意识到了这一点,在最新androidx中引入了activity result api,通过activity result api你可以不需要自己管理requestCode,只需要提供需要请求的权限和处理结果的回调就行,让权限请求简单了一点,但是如果在权限请求的过程中,用户点击拒绝或者拒绝并不再询问,那么我们还是需要自己处理这些情况,但是这些处理流程都是一样的,完全可以封装起来,所以我就把以前的一个使用无界面fragment代理权限申请的库重构了一下,让权限的请求流程更加简单,本文会先复习一下权限的分类,然后再介绍PermissionHelper申请权限时的设计,最后记录一下从android 6.0后随着系统的迭代跟权限申请相关的重要行为变更。

权限的分类

android中所有的预定义权限(不包括厂商自定义的)都可以在Manifest.permission这个静态类中找到定义,android把权限分为四类:普通权限、签名权限、危险权限和特殊权限,每一种类型的权限都分配一个对应的Protection Level,分别为:normal、signature、dangerous和appop,下面简单介绍一下这四种类型的权限

1、普通权限

普通权限也叫正常权限,Protection Level为normal,它不需要动态申请,你只需要在AndroidManifest.xml中静态地声明,然后系统在应用安装时就会自动的授予该应用相应的权限,当应用获得授权时,它就可以访问应用沙盒外受该普通权限保护地数据或操作,这些数据或操作不会泄漏或篡改用户的隐私,对用户或其他应用几乎没有风险。

2、签名权限

这类权限我们用得比较少,它只对拥有相同签名的应用开放,Protection Level为signature,它也不需要动态申请,例如应用A在AndroidManifest.xml中自定义了一个permission且在权限标签中加入android:protectionLevel=”signature”,表示应用A声明了一个签名权限,那么应用B想要访问应用A受该权限保护的数据时,必须要在AndroidManifest.xml中声明该权限,同时要用与应用A相同的签名打包,这样系统在应用B安装时才会自动地授予应用B该权限,应用B在获得授权后就可以访问该权限控制的数据,其他应用即使知道这个权限,也在AndroidManifest.xml中声明了该权限,但由于应用签名不同,安装时系统不会授予它该权限,这样其他应用就无法访问受该权限保护的数据。

还有一些签名权限不会供第三方应用程序使用,只会供系统预装应用使用,这种签名权限的Protection Level为signature和privileged。

3、危险权限

危险权限也叫运行时权限,Protection Level为dangerous,跟普通权限相反,一旦应用获取了该类权限,用户的隐私数据就会面临被泄露或篡改的风险,所以如果你想使用该权限保护的数据或操作,就必须在AndroidManifest.xml中静态地声明需要用到的危险权限,并在访问这些数据或操作前动态的申请权限,系统就会弹出一个权限请求弹窗征求用户的同意,除非用户同意该权限,否则你不能使用该权限保护的数据或操作。

所有的危险权限都有对应的权限组,android预定义了11个权限组(根据android 11总结),这11个权限组中包含了30个危险权限和几个普通权限,当我们动态的申请某个危险权限时,都是按权限组申请的,当用户一旦同意授权该危险权限,那么该权限所对应的权限组中的其他在AndroidManifest.xml中注册的权限也会同时被授权,android预定义的11个权限组包含的危险权限如下:

Permission GroupDangerous Permissions
CALENDAR (日历)READ_CALENDAR
WRITE_CALENDAR
CALL_LOG (通话记录,Added in android 29)READ_CALL_LOG
WRITE_CALL_LOG
PROCESS_OUTGOING_CALLS
CAMERA (相机)CAMERA
CONTACTS (通讯录)READ_CONTACTS
WRITE_CONTACTS
GET_ACCOUNTS
LOCATION (位置信息)ACCESS_COARSE_LOCATION
ACCESS_FINE_LOCATION
ACCESS_BACKGROUND_LOCATION (Added in android 10)
MICROPHONE (麦克风)RECORD_AUDIO
PHONE (电话)READ_PHONE_NUMBERS
READ_PHONE_STATE
CALL_PHONE
ANSWER_PHONE_CALLS
ADD_VOICEMAIL
USE_SIP
ACCEPT_HANDOVER (Added in android 9)
SENSORS (身体传感器)BODY_SENSORS
SMS (短信)READ_SMS
RECEIVE_WAP_PUSH
RECEIVE_SMS
RECEIVE_MMS
SEND_SMS
STORAGE (存储空间)READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGE
ACCESS_MEDIA_LOCATION (Added in android 10)
ACTIVITY_RECOGNITION (身体活动,Added in android 10)ACTIVITY_RECOGNITION (Added in android 10)

4、特殊权限

特殊权限用于保护一些特定的应用程序操作,Protection Level为appop,使用前也需要在AndroidManifest.xml中静态地声明,也需要动态的申请,但是它不同于危险权限的申请,危险权限的申请会弹出一个对话框询问你是否同意,而特殊权限的申请需要跳转到指定的设置界面,让你手动点击toggle按钮确认是否同意,截止到android 11,我了解到的常用的5个特殊权限为:

  • SYSTEM_ALERT_WINDOW:允许应用在其他应用的顶部绘制悬浮窗,当你创建的悬浮窗是TYPE_APPLICATION_OVERLAY类型时需要申请这个权限;
  • WRITE_SETTINGS:允许应用修改系统设置,当你需要修改系统参数Settings.System时需要申请该权限,例如修改系统屏幕亮度等;
  • REQUEST_INSTALL_PACKAGES: 允许应用安装未知来源应用,android 8.0以后当你在应用中安装第三方应用时需要申请这个权限,否则不会跳转到安装界面;
  • PACKAGE_USAGE_STATS:允许应用收集其他应用的使用信息,当你使用UsageStatsManager相关Api获取其他应用的信息时需要申请这个权限;
  • MANAGE_EXTERNAL_STORAGE(Added in android 11):允许应用访问作用域存储(scoped storage)中的外部存储,android 11以后强制新安装的应用使用作用域存储,但是对于文件管理器这一类的应用它们需要管理整个SD卡上的文件,所以针对这些特殊应用可以申请这个权限来获得对整个SD卡的读写权限,当应用授予这个权限后,它就可以访问文件的真实路径,注意这个权限是很危险的,声明这个权限上架应用时可能需要进行审核.

除了特殊权限,LOCATION权限组中的位置权限也有点特殊,需要注意一下,位置信息的获取不仅依赖位置权限的动态申请还依赖系统定位开关,如果你没有打开定位开关就申请了位置权限,那么就算用户同意授权位置权限,应用通过Location相关Api也无法获取到位置信息,所以申请位置权限前,最好先通过LocationManager#isProviderEnabled方法判断是否打开定位开关后再进行位置权限的申请,如果没有打开定位开关需要先跳转到设置界面打开定位开关,伪代码如下:

val locationManager = this.getSystemService(Context.LOCATION_SERVICE) as LocationManager
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) or locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
//请求位置权限
} else {
//跳转到开启定位的地方
Toast.makeText(this, "检测到未开启定位服务,请开启", Toast.LENGTH_SHORT).show()
val intent = Intent().apply {
action = Settings.ACTION_LOCATION_SOURCE_SETTINGS
}
startActivityForResult(intent, REQUEST_CODE_LOCATION_PROVIDER)
}

当然,上面危险权限和特殊权限的判断与申请,PermissionHelper都已经替你做好了封装,你只需要像平常一样在AndroidManifest.xml中静态地声明权限,然后在代码中动态地申请就行,下面我把危险权限和特殊权限都统称为动态权限,因为它们都是需要动态申请的。

动态权限申请设计

动态权限的申请依据不同的android版本和应用targetSdkVersion有着不同的行为,主要有两种处理,如下:

  • android版本 <= 5.1 或者 应用的targetSdkVersion <= 22:当用户同意安装应用时,系统会要求用户授权应用声明的所有权限,包括动态权限,如果用户不同意授权,只能拒绝安装应用,如果用户同意全部授权,他们撤销权限的唯一方式就是卸载应用;
  • android版本 >= 6.0 且 应用的targetSdkVersion >= 23:当用户同意安装应用时,系统不再强制用户必须授权动态权限,系统只会授权应用除动态权限之外的普通权限,而动态权限需要应用使用到相关功能时才动态申请,当申请动态权限时,用户可以选择授权或拒绝每项权限,即使用户同意授权权限,用户也可以随时进入应用的“Settings”中调整应用的动态权限授权,所以你每次使用到该权限的功能时,都要动态申请,因为用户有可能在“Settings”界面中把它再次关闭掉.

在android版本 <= 5.1 或者 应用的targetSdkVersion <= 22时,系统使用的是AppOps来进行权限管理,这是android在4.4推出的一套应用程序操作权限管理,AppOps所管理的是所有可能涉及用户隐私和安全的操作,例如access notification、keep weak lock、display toast 等等,而运行时权限管理是android 6.0才出现,是基于AppOps的实现,进一步做了动态请求封装和明确的规范,同时当targetSdkVersion <= 22的应用运行在 >= 6.0的android系统上时,动态权限可以在“Settings”界面中关闭,应用运行过程中使用到相关功能时就会由于没有权限而出现崩溃,这时只能使用AppOps的 checkOp方法来检测对应的权限是否已经授权,没有权限就跳转到“Settings”界面,考虑到目前android 6.0已经推出了很久,应用商店也不允许targetSdkVersion < 23的应用上架,所以为了减少框架的复杂度,动态权限申请设计就没有考虑兼容AppOps的权限管理操作,所以当你使用PermissionHelper时应用的targetSdkVersion要 >= 23

PermissionHelper支持危险权限和特殊权限的申请,只需要一行代码就可以发起权限请求,具有生命周期感应能力,只在界面可见时才发起请求和回调结果,同时当系统配置更改例如屏幕旋转后能够恢复之前权限申请流程,不会中断权限申请流程,灵活性高,可以设置请求前、拒绝后回调,在回调发生时暂停权限申请流程,然后根据用户意愿再决定是否继续权限申请流程,整个申请过程如图:

PermissionHelper可以通过设置回调在权限申请开始前和权限被拒绝后把要请求的权限和被拒绝的权限回调出去,在回调中你可以通过弹窗向用户解释要申请的权限对应用的必要性,引导用户继续授权或再次授权,PermissionHelper不定制弹窗UI,弹窗的UI由开发者自定义,开发者只需要在用户同意或拒绝后调用回调中的Process实例的相应方法就能让被暂停的权限申请流程恢复,然后在最终的结果回调中处理结果就行,整个过程都是链式的,关于向用户解释权限申请原因的弹窗,弹窗内容建议包含下面的3点:

1、包含需要授权的权限列表的描述;

2、包含确认按钮,用户可以点击确认按钮再次授权或跳转到”Settings“;

3、包含取消按钮,用户可以点击取消按钮放弃授权.

如果用户不授权这个权限,就会导致应用无法继续运行下去,可以考虑取消第3步的取消按钮,即无法取消这个弹窗,一定要用户再次授权或跳转到”Settings“去授权。

PermissionHelper整个框架的设计参考了okhttp的拦截器模式,通过责任链模式的形式把危险权限申请、特殊权限申请、申请前处理和申请后处理划分为一个个节点,然后通过Chain串联起各个节点,每个节点只负责对应的内容,如下:

val originalRequest = Request()    
val interceptors = listOf(
StartRequestNode(),
RequestLocationNode(),
RequestNormalNode(),
RequestSpecialNode(),
PostRequestNode(),
FinishRequestNode()
)
DefaultChain(originalRequest, interceptors).process(originalRequest)

通过这样的形式PermissionHelper就可以很灵活的控制权限申请流程,对于生命周期感应能力的实现PermissionHelper使用了Lifecycle+LiveData组件,这两个都是官方支持的用于实现需要响应生命周期感应的操作,可以编写更轻量级和更易于维护的代码,避免界面销毁后的内存泄漏,对于系统配置更改后的数据恢复则使用到了ViewModel组件,这是官方支持的用于保存需要在配置更改后恢复的数据,例如一些UI相关的数据,通过这三件套 + 责任链模式实现了一个简单易用的权限申请框架,更多详细使用和实现细节可以查看代码仓库

权限申请相关变更

自android 6.0推出动态权限申请之后,有一些申请行为也随着系统的迭代发生变化,目的都是更好的保护用户的隐私权,使得权限申请对用户感知:

android 8.0以后并且应用的targetSdkVersion >= 28时,应用申请某个危险权限授权,用户同意后,系统不再错误地把该危险权限对应的权限组中的其他在AndroidManifest.xml中注册的权限一并授予给应用,系统只会授予应用明确请求的权限,然而,一旦用户应用同意授权某个危险权限,则后续对该危险权限的权限组中的其他权限请求都会被自动批准,而不会提示用户,例如某个应用在AndroidManifest.xml中注册READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限,应用申请READ_EXTERNAL_STORAGE权限并且用户同意,在android 8.0之前,系统在用户同意后还会一并授予WRITE_EXTERNAL_STORAGE权限,因为它和READ_EXTERNAL_STORAGE权限是同一个权限组并且也在AndroidManifest.xml中注册,但在android 8.0之后并且应用的targetSdkVersion >= 28,系统在用户同意后只会授予READ_EXTERNAL_STORAGE权限,但是如果后来应用又申请WRITE_EXTERNAL_STORAGE权限,系统会立即授予该权限,而不会提示用户,换句话说,如果只申请了外部存储空间读取权限,在低版本下(android < 8.0)对外部存储空间使用写入操作是没有问题的,但是在高版本(android >= 8.0 && targetSdkVersion >= 28)下是会出现问题的,解决方案是将两个读和写的权限一起申请。

android 9.0增加了CALL_LOG(通话记录)权限组,并把READ_CALL_LOG、WRITE_CALL_LOG]、PROCESS_OUTGOING_CALLS权限从PHONE(电话)权限组移动到了CALL_LOG权限组,CALL_LOG权限组使得用户能够更好地控制需要访问电话通话记录敏感信息的应用程序,例如读取通话记录和识别电话号码。

android 10引入了很多隐私变更,新增了ACTIVITY_RECOGNITION(身体活动)权限和权限组,允许应用检测用户的步数或分类用户的身体活动如步行、骑自行车等;同时android 10引入了作用域存储,当应用启用作用域存储时,WRITE_EXTERNAL_STORAGE权限会失效,应用对WRITE_EXTERNAL_STORAGE权限的申请不会对应用的存储访问权限产生任何影响,并且WRITE_EXTERNAL_STORAGE会在未来被废弃,因为作用域存储的目的就是不让应用随意的修改应用沙盒外的外部存储;同时新增了ACCESS_BACKGROUND_LOCATION权限,归属于LOCATION权限组,用于后台运行的应用访问用户定位时申请,与ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION这些前台定位权限区分开,当你的应用targetSdkVersion >= 29并且运行在android 10以上时,应用在后台访问定位时需要动态的申请后台定位权限,当你把后台定位权限和前台定位权限一起申请时,弹窗授权框会有2个允许选项:始终允许仅在应用使用过程中允许,点击始终允许表示同时授权后台定位权限和前台定位权限,点击仅在应用使用过程中允许表示仅授权前台定位权限,然后下次再次申请时只会单独申请后台定位权限,并且也会有2个允许选项,并且要点击始终允许才会让后台定位权限申请通过,当你的应用targetSdkVersion < 29运行在android 10以上时,应用在申请前台定位权限时系统会把后台定位权限一并授予给应用;android 10还新增了ACCESS_MEDIA_LOCATION权限,归属于STORAGE (存储空间) 权限组,android 10以后,因为隐私问题,默认不再提供图片的地理位置信息,要获取该信息需要向用户申请ACCESS_MEDIA_LOCATION权限,并使用MediaStore.setRequireOriginal()接口更新文件Uri。

android 11也引入了很多隐私变更,android 11强制新安装的应用(targetSdkVersion >= 30)启用作用域存储,新增MANAGE_EXTERNAL_STORAGE用于代替WRITE_EXTERNAL_STORAGE权限,提供给手机管家、文件管理器这类需要管理整个SD卡上的文件的应用申请;android 11中当用户开启“安装未知来源应用”权限后返回应用,应用会被杀死重启,该行为与强制分区存储有关;从android 11后,如果应用对某个权限连续点击多次拒绝,那么下一次请求该权限时系统会直接拒绝连授权弹窗都不会弹出,该行为等同于android 11之前勾选了don‘t ask again;android 11后还新增了一次性权限(One-time permissions)和权限自动重置功能(Permissions auto-reset),这些变更只要你正确的进行运行时权限请求就不需要做额外适配;同时android 11后当targetSdkVersion < 30的应用把后台定位权限和前台定位权限一起申请时,弹窗授权框的允许选项中不再会显示始终允许选项,只有本次允许仅在应用使用过程中允许,也就说点击允许时只会授予你前台定位权限不再默认授予你后台定位权限,而android 11后targetSdkVersion >= 30的应用的ACCESS_BACKGROUND_LOCATION权限需要独立申请,不能与前台权限一起申请,如果与前台权限一起申请,系统会直接拒绝连授权弹窗都不会弹出,系统推荐增量请求权限,这样对用户更友好,同时用户必须先同意前台权限后才能进入后台定位权限的申请。

可以看到从android 10引入ACCESS_BACKGROUND_LOCATION权限以来,后台定位权限的申请一直都非常特殊,它在android 10可以和前台定位权限一起申请,而在android 11又不可以一起申请还有先后申请顺序,针对这种特殊情况,申请后台定位权限时要做到:

  • 1、先请求前台定位权限,再请求后台定位权限;
  • 2、单独请求后台定位权限,不要与其他权限一同请求.

上面这些PermissionHelper都已经做好了处理,申请时只需要把后台定位权限和前台定位权限一起传进去就行。

结语

本文主要让让大家对权限的申请流程有进一步的认识,然后可以通过对动态权限的封装,将检测动态权限,请求动态权限,权限设置跳转,监听权限设置结果等处理和业务功能隔离开来,业务以后可以非常快速的接入动态权限支持,提高开发效率。


收起阅读 »

Android混合开发快速上手入门

一 混合开发简介原生app :java/kotlin 纯原生写出的app;web app:web写出的app;hybird app:原生+web(通过webview)写出的app;当然,现在也有很多第三方混合开发框架以及简便的js桥,但是作为最基础的webvi...
继续阅读 »


一 混合开发简介

原生app :java/kotlin 纯原生写出的app;

web app:web写出的app;

hybird app:原生+web(通过webview)写出的app;

当然,现在也有很多第三方混合开发框架以及简便的js桥,但是作为最基础的webview,掌握js/android的互调等相关知识是非常必要的。

二 Android-Js互调

2.1 准备自己的html文件

安卓和html中js的互调,一是要有安卓代码,二肯定需要html网页。工程中,网页都是放在服务器,方便随时更改,用户无需再次更新自己的app,已达到hybrid开发的目的,实例方便起见,将html文件放在了本地。

首先,在自己安卓项目中的app目录下新建assets文件夹(若没有):

接着,在assets文件夹下新建自己的html文件,代码如下:

<html>
<head>
<meta http-equiv="Content-Type" charset="GB2312"/>

<script type="text/javascript">
function javacalljs(){
document.getElementById("showmsg").innerHTML = "JAVA调用了JS的无参函数";
}

function javacalljswith(arg){
document.getElementById("showmsg").innerHTML = (arg);
}

</script>

</head>

<body>
<h3 align="center">Web模块</h3>

<h3 id="showmsg" align="center">调用js显示结果</h3>

<div style="text-align:center; vertical-align:middle;">
<input type="button" value="Js调用Java代码" onclick="window.android.jsCallAndroid()"/>
</div>

<br>

<br>

<div style="text-align:center; vertical-align:middle;">
<input type="button" value="Js调用Java代码并传参数" onclick="window.android.jsCallAndroidArgs('Js传过来的参数')"/>
</div>

</body>
</html>

2.2 WebView控件的准备设置

在自己的activity活动中获得webview控件后,需要进行以下设置:

WebSettings webSettings = webview.getSettings();
//与js交互必须设置
webSettings.setJavaScriptEnabled(true);
webview.loadUrl("file:///android_asset/html.html");
webview.addJavascriptInterface(MainActivity.this,"android");
  • webSettings.setJavaScriptEnabled(true) 表示让WebView支持调用Js;
  • webview.loadUrl("file:///android_asset/html.html") 表示加载assets文件下的html.html文件(因为没有网络地址所以加载的本地文件)
  • webview.addJavascriptInterface(MainActivity.this,"android") 给webview添加Js调用接口,第一个参数为类对象,第二个参数为自定义别名,Js通过这个别名来调用Java的方法,我这里自定义为android。 html中用到:<input type="button" value="Js调用Java代码" οnclick="window.android.jsCallAndroid()"/>

2.3 Android调用Js代码

在android代码中(如按钮点击事件中),通过webview这个中介调用loadUrl来执行html代码中的Js代码:

 tvJs.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
webview.loadUrl("javascript:javacalljs()");
}
});

下为html中要被安卓调用的js函数代码,函数意图为:向id为showmsg的h3大小标题中写入字符串“JAVA调用了JS的无参函数”。

function javacalljs(){
document.getElementById("showmsg").innerHTML = "JAVA调用了JS的无参函数";
}

在上述基础上,若要在Android调用Js函数时传参数,只需要在loadUrl方法中进行字符串的拼接,将参数以字符串形式拼接进去即可。

webview.loadUrl("javascript:javacalljswith(" + "'Android传过来的参数'" + ")");

2.4 Js调用Android方法和传参数

点击html按钮,通过οnclick="window.android.jsCallAndroid()事件,通过android别名调用Java文件的jsCallAndroid()方法。曾经Js可直接调用Java代码窃取App信息,为安全起见,在Android4.4以上并且必须加入@JavascriptInterface才有响应。

@JavascriptInterface
public void jsCallAndroid(){
tvShowmsg.setText("Js调用Android方法");
}

@JavascriptInterface
public void jsCallAndroidArgs(String args){
tvShowmsg.setText(args);
}

所有的activity代码如下:

package com.lucas.autils.autils;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.TextView;

/**
* 原生webview js与安卓互调
*/

public class JsJavaActivity extends Activity {

private WebView webview;
private TextView tvJs;
private TextView tvJsArgs;
private TextView tvShowmsg;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_jsjava);

setWebview();
initView();
}

private void initView() {
tvJs = (TextView) findViewById(R.id.tv_androidcalljs);
tvJsArgs = (TextView) findViewById(R.id.tv_androidcalljsargs);
tvShowmsg = (TextView) findViewById(R.id.tv_showmsg);

tvJs.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
webview.loadUrl("javascript:javacalljs()");
}
});

tvJsArgs.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
webview.loadUrl("javascript:javacalljswith(" + "'Android传过来的参数'" + ")");
}
});
}

private void setWebview() {
webview = (WebView) findViewById(R.id.webview);
WebSettings webSettings = webview.getSettings();
webSettings.setBuiltInZoomControls(true);
webSettings.setSupportZoom(true);
//与js交互必须设置
webSettings.setJavaScriptEnabled(true);
webview.loadUrl("file:///android_asset/html.html");
webview.addJavascriptInterface(JsJavaActivity.this,"android");
}

@JavascriptInterface
public void jsCallAndroid(){
tvShowmsg.setText("Js调用Android方法");
}

@JavascriptInterface
public void jsCallAndroidArgs(String args){
tvShowmsg.setText(args);
}

}

三 常用的几个方法和注意点

3.1 WebViewClient中的shouldOverrideUrlLoading拦截url

安卓webview中setWebViewClient方法中需要一个WebViewClient对象,而WebViewClient中有个方法为shouldOverrideUrlLoading,通过此方法可以进行我们需要跳转的url地址的拦截,并根据我们需要进行自定义化的一些操作,解析url做相应的事情。

3.2 WebViewClient中的onPageStarted

onPageStarted会在webview加载相应的url开始之前进行调用,常用来处理需要在加载相应url之前的一些操作。

3.3 WebViewClient中的onPageFinished

onPageStarted会在webview加载相应的url结束之后进行调用,常用来处理需要在加载相应url之后的一些操作,比如加载后更加网页标题填充原生页面最上方的活动标题。

3.4 webview的evaluateJavascript方法

该方法的执行不会使页面刷新,而方法(loadUrl )的执行则会使页面刷新。此Android 4.4 后才可使用。

//拦截url
webview.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.indexOf("jump")>-1){
Toast.makeText(JsJavaActivity.this,"拦截到了相应url",Toast.LENGTH_LONG).show();
return true;
}else if (url.startsWith("http")){
view.loadUrl(url);
return true;
}
return false;
}


@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
// 开始加载页面时
}



@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// 加载结束

//因为该方法的执行不会使页面刷新,而方法(loadUrl )的执行则会使页面刷新。
//Android 4.4 后才可使用
webview.evaluateJavascript("javascript:changename()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此处为 js 返回的结果
Log.v("Native",value);
}
});


}

});
收起阅读 »

Glide源码解析

本次源码解析基于4.12.0,如有描述错误,请大佬们评论指出。一、Glide的用法 // RecyclerView中加载图片 @Override public void onBindViewHolder(PhotoViewHolder holder, int ...
继续阅读 »

本次源码解析基于4.12.0,如有描述错误,请大佬们评论指出。

一、Glide的用法

 // RecyclerView中加载图片
@Override
public void onBindViewHolder(PhotoViewHolder holder, int position) {
GlideApp.with(holder.itemView).load(list.get(position))
.transform(new RoundedCorners(40))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.placeholder(R.drawable.ic_launcher)
.error(R.drawable.ic_launcher)
.into(holder.imageView);
}

二、Glide一些面试常考点

  • 2.1、 Glide如何感知Application、Activity、Fragment的生命周期?

Q:先问下如果你想感知application的那几个内存不足的方法,你会怎么做。
ComponentCallbacks2是系统提供的类。 image.png image.png

Application类管理这些订阅者,方法回调时,遍历通知。 image.png

Glide中的trimMemory收到事件通知后的已做处理,不需要我们自己再去清理Glide的资源占用。 image.png

Q:页面如果有ImageView,加载图片时立马页面关闭/返回,此时应该停止加载,Glide如何感知Activity/Fragment的onDestroy呢?

当然是在对应的Act或者Fragment中插入空白SupportRequestManagerFragment实现。

GlideApp.with(activity).load("https://t7.baidu.com/it/u=3652245443,3894439772&fm=193&f=GIF").into(view);
GlideApp.with(fragment).load("https://t7.baidu.com/it/u=3652245443,3894439772&fm=193&f=GIF").into(view);

image.png

image.png 如果说我们的Activity有个ImageView,它里面有两个Fragment也加载ImageView,按照规范,with方法应该是基于ImageView所处的context来决定,该传Act就传Act,该传Fragment就传Framgent没错,这样一来,Glide就嵌入3个SupportRequestManagerFragment进入了我们的页面。有点厉害哦。

但是如果Fragment里面不小心写成了下面这样

//fragment中的ImageView
GlideApp.with(view).load("https://t7.baidu.com/it/u=3652245443,3894439772&fm=193&f=GIF").into(view);

用一个简单的demo模拟这种场景,Glide从View去找Fragment竟然找不到, \color{#ff0000}{既然找不到View所在的Fragment容器,那就只能用跟Activity保持一致了} 所以小伙子们写with方法的时候要注意啊。(记得之前我用过kotlin中Fragment拓展方法,可以通过View找到其所在的Fragment)

image.png image.png

  • 2.2、 Glide的MemoryCache(LruResourceCache)和LruBitmapPool以及DiskLruCache默认size多大呢?

    final int MEMORY_CACHE_TARGET_SCREENS = 2;
final int BITMAP_POOL_TARGET_SCREENS =Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 4 : 1;
int widthPixels =context.getResources().getDisplayMetrics().widthPixels;
int heightPixels =context.getResources().getDisplayMetrics().heightPixels;
int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;
//8及其8以上4张图图片的size
int targetBitmapPoolSize = Math.round(screenSize * BITMAP_POOL_TARGET_SCREENS);
//2张屏幕大小的size
int targetMemoryCacheSize = Math.round(screenSize * MEMORY_CACHE_TARGET_SCREENS);

int DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024;
String DEFAULT_DISK_CACHE_DIR = "image_manager_disk_cache";
File cacheDirectory = context.getCacheDir();

LruResourceCache默认: 只有2张屏幕大小的图片size
LruBitmapPool默认: 只有1 or 4张的屏幕大小的Bitmap可以复用;
DiskLruCache默认: 在内置SD卡且占用空间250M

image.png

  • 2.3、 三级缓存的添加和移除发生在什么时机?

    弱引用缓存(ResourceWeakReference)   内存缓存(LruResourceCache)   磁盘缓存(DiskLruCache)

    同一个Bitmap一旦从LruResourceCache取出(remove)了,那它就只有弱引用缓存了,如果弱引用清除时,就是LruCache加入缓存时,看样子二者不能共存?

    目前测试的结果:一旦图片加载ok了,先加入弱引用缓存,如果是recyclerView列表,item复用,ImageView会被into很多次,该Bitmap对应的弱引用早就没了,此时Bitmap会加入LruResourceCache。如果不是列表是页面加载的ImageView,当页面关闭时,Bitmap的弱引用清除,此时会加入LruResourceCache。如果LruResourceCache内存不够,那就进行trim。

    DiskLruCache下文讲。

  • 2.4、 Glide如何区分一个Url的内容是png,还是jpg,还是gif的呢?

先让大家看一下内容,后面会贯通讲下。

image.png

  • 2.5、  设置BitmapFactory.Options.inBitmap作用

BitmapFactory.Options.inBitmap = lruBitmapPool.getDirty(width, height, expectedConfig)
inBitmap表示要复用Bitmap,该Bitmap就来自LruBitmapPool,由于这个池本身容纳的bitmap数量有限,能提供还好,不能提供它还是直接createBitmap(新创建)返回Bitmap。

Q:如果BitmapPool中的有Bitmap存在,是不是一定可以复用?有没有啥限制?

@Override
public void reschedule() {
runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
getActiveSourceExecutor().execute(job);
}

image.png

第二次调用SourceGenerator的startNext就准备缓存到磁盘,这个缓存的就是源数据。

private void cacheData(Object dataToCache) {
try {
Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
DataCacheWriter<Object> writer =
new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
helper.getDiskCache().put(originalKey, writer);
} finally {
loadData.fetcher.cleanup();
}
sourceCacheGenerator =
new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
}

存了之后,就用NIO的方式读取刚刚缓存在磁盘里面的文件,这一套操作,是不是有点慢了,先存到本地再读取file获取ByteBuffer。

image.png

  • 3.2.3、根据DirectByteBuffer解码出Resource(Bitmap)

我们这里load进去的url就是一张图片,对应三条解码路径:

  • DirectByteBuffer->GifDrawable->Drawable
  • DirectByteBuffer->Bitmap->Drawable
  • DirectByteBuffer->BitmapDrawable->Drawable

但是不确定是哪一条,那就都试试,发现每次都从gif类型(ByteBufferGifDecoder)开始,不知是不是特意为之,如果类型不匹配就换下一个。

private Resource<ResourceType> decodeResourceWithList(DataRewinder<DataType> rewinder....)
throws GlideException {
Resource<ResourceType> result = null;
for (int i = 0, size = decoders.size(); i < size; i++) {
ResourceDecoder<DataType, ResourceType> decoder = decoders.get(i);
try {
DataType data = rewinder.rewindAndGet();
if (decoder.handles(data, options)) { //gif类型需要通过获取文件类型判断,bitmap则直接true
data = rewinder.rewindAndGet(); //重置buffer读取的位置到起始位置
result = decoder.decode(data, width, height, options);
}
} catch (IOException | RuntimeException | OutOfMemoryError e) {
exceptions.add(e);
}
if (result != null) {
break;
}
}
if (result == null) {
throw new GlideException(failureMessage, new ArrayList<>(exceptions));
}
return result;
}

image.png

解析ByteBuffer的文件类型关键代码来了:

// DefaultImageHeaderParser
@NonNull
private ImageType getType(Reader reader) throws IOException {
try {
final int firstTwoBytes = reader.getUInt16();
// JPEG.类型读取两个字节就可以判断了
if (firstTwoBytes == EXIF_MAGIC_NUMBER) {
return JPEG;
}
//gif要读3字节
final int firstThreeBytes = (firstTwoBytes << 8) | reader.getUInt8();
if (firstThreeBytes == GIF_HEADER) {
return GIF;
}
//png要读4字节
final int firstFourBytes = (firstThreeBytes << 8) | reader.getUInt8();
if (firstFourBytes == PNG_HEADER) {
reader.skip(25 - 4);
try {
int alpha = reader.getUInt8();
return alpha >= 3 ? PNG_A : PNG;
} catch (Reader.EndOfFileException e) {
return PNG;
}
}
//更多其他类型不列举了
// WebP (reads up to 21 bytes).
......
return UNKNOWN;
}
}

每一次尝试,缓冲区都会读一些字节,下次尝试还是要从头开始,此时就需要重置位置为0,所以搞了个ByteBufferRewinder(rewind--倒带)来干这事。 image.png

很明显,我们这个不是Gif图,那就换下一个试试ByteBufferBitmapDecoder。 image.png

先将ByteBuffer转换InputStream,看到InputStream,是不是跟Bitmap很近了,它先获取流中Bitmap的宽高和是否有旋转角度,以及是否配置Target.SIZE_ORIGINAL来调整目标宽高,一般来说,图片无旋转,且图片没有显式配置是Target.SIZE_ORIGINAL,那么目标宽高就是我们之前获取的宽高(不记得了就看上面的)。

然后再次检测文件类型(不明白之前已经尝试gif类型判断时,已经得出了图片类型,但是它没保存,此时还要再获取一次,差评!),基于scaleType综合考虑采样率,代码太多了,就不贴了。在流保存Bitmap之前,设置Bitmap走复用。

private static void setInBitmap(
BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height)
{
.....
options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
.....
}
.......//一系列配置整完后 bitmap操作开始了
Bitmap downsampled = BitmapFactory.decodeStream(dataRewinder.rewindAndGet(), null, options);
callbacks.onDecodeComplete(bitmapPool, downsampled);
Bitmap rotated = null;
if (downsampled != null) {
downsampled.setDensity(displayMetrics.densityDpi);
//开始旋转Bitmap了,又是很好的可以抄袭的地方,以后有旋转bitmap的场景也这么干
rotated = TransformationUtils.rotateImageExif(bitmapPool, downsampled, orientation);
if (!downsampled.equals(rotated)) {
bitmapPool.put(downsampled);
}
}
return rotated;

image.png

  • 3.2.4、目标bitmap获取到,还要transform下,就是我们设置的什么圆角操作等啦。

public Resource<Transcode> decode(....){
//Resource<ResourceType> 就是 Resource<Bitmap>--->相当于拿到bitmap
Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options);
//对bitmap做转换
Resource<ResourceType> transformed = callback.onResourceDecoded(decoded);
return transcoder.transcode(transformed, options);
}

callback.onResourceDecoded(decoded)很关键

<Z> Resource<Z> onResourceDecoded(DataSource dataSource, @NonNull Resource<Z> decoded) {
Class<Z> resourceSubClass = (Class<Z>) decoded.get().getClass();
Transformation<Z> appliedTransformation = null;
Resource<Z> transformed = decoded;
//磁盘缓存策略在这里发挥作用
if (dataSource != DataSource.RESOURCE_DISK_CACHE) {
//选取其中一个跟Bitmap匹配的Transformation操作
appliedTransformation = decodeHelper.getTransformation(resourceSubClass);
//应用操作
transformed = appliedTransformation.transform(glideContext, decoded, width, height);
}
//应用完之后,旧的bitmap直接让其回收
if (!decoded.equals(transformed)) {
decoded.recycle();
}
......
//DiskCacheStrategy.DATA的isResourceCacheable默认就是false了
//DiskCacheStrategy.AUTOMATIC经过了几重不明所以的判断,isFromAlternateCacheKey=false,导致也是false
//但是不影响,因为之前已经在本地缓存过一次源数据了
//所以这里专门为DiskCacheStrategy.RESOURCE和DiskCacheStrategy.ALL使用
if (diskCacheStrategy.isResourceCacheable(isFromAlternateCacheKey, dataSource, encodeStrategy)) {
.....
final Key key;
switch (encodeStrategy) {
case SOURCE: //源数据,,不太可能会走这个逻辑
key = new DataCacheKey(currentSourceKey, signature);
break;
case TRANSFORMED: //转换后的bitmap对应的key
key = new ResourceCacheKey(decodeHelper.getArrayPool(), currentSourceKey, signature,
width, height,appliedTransformation, resourceSubClass, options);
break;
.....
}
LockedResource<Z> lockedResult = LockedResource.obtain(transformed);
//拿到key,但是没有做缓存操作,因为defer是延迟处理的,后面会很快存转换后的数据到磁盘
deferredEncodeManager.init(key, encoder, lockedResult);
result = lockedResult;
}
return result;
}

image.png image.png 圆角的处理,以后有这种需求,也这么干。

  • 3.2.5、通知bitmap就绪了且按需保存转换的数据到磁盘。

 private void decodeFromRetrievedData() {
Resource<Bitmap> nresource = decodeFromData(currentFetcher, currentData, currentDataSource);
notifyEncodeAndRelease(resource, currentDataSource, isLoadingFromAlternateCacheKey);
}

//resource就是bitmap
private void notifyEncodeAndRelease(Resource<Bitmap> resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) {
if (resource instanceof Initializable) {
// bitmap.prepareToDraw(); 预先将bitmap加载到gpu上
((Initializable) resource).get().prepareToDraw();
}
....
//通知engine以及回调给用户onResourceReady
....
//这里真正开始写入转换的后的数据
if (deferredEncodeManager.hasResourceToEncode()) {
deferredEncodeManager.encode(diskCacheProvider, options);
}
.....
}

//deferredEncodeManager //这里真正开始写入转换的后的数据
void encode(DiskCacheProvider diskCacheProvider, Options options) {
GlideTrace.beginSection("DecodeJob.encode");
try {
//bitmap缓存为file
diskCacheProvider.getDiskCache().put(key, new DataCacheWriter<>(encoder, toEncode, options));
} finally {
toEncode.unlock();
GlideTrace.endSection();
}
}
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Z> transition) {
if (transition == null || !transition.transition(resource, this)) {
//终于看到ImageView显示图片了
imageView.setImageBitmap(resource);
} else {
maybeUpdateAnimatable(resource);
}
}
//DiskCache进行put时,就会调用DataCacheWriter的wirte方法
//wirte方法就调用encoder的encode方法,将bitmap缓存到文件
public boolean encode( Resource<Bitmap> resource, File file, Options options) {
final Bitmap bitmap = resource.get();
Bitmap.CompressFormat format = getFormat(bitmap, options);
try {
int quality = options.get(COMPRESSION_QUALITY);
boolean success = false;
OutputStream os = null;
try {
os = new FileOutputStream(file);
if (arrayPool != null) {
os = new BufferedOutputStream(os, arrayPool);
}
bitmap.compress(format, quality, os);
os.close();
success = true;
} catch (IOException e) {
....
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
// Do nothing.
}
}
}
return success;
} finally {
GlideTrace.endSection();
}
}

大家仔细看下上面代码的注释。 当看到imageView.setImageBitmap(bitmap) 后,整个逻辑就走完了。

四、从以上加载流程来提出问题

  • 4.1、 DiskCacheStrategy.RESOURCE、DiskCacheStrategy.DATA、DiskCacheStrategy.AUTOMATIC有啥区别?

尽管DiskCacheStrategy.AUTOMATIC是默认,听说很智能,智能个鬼,从简单加载url显示bitmap来看,我暂时看不出它跟DiskCacheStrategy.DATA有啥子区别。
DiskCacheStrategy.RESOURCE:只缓存bitmap转换后的数据到磁盘,在SourceGenerator的startNextLoad去加载网络资源,下载回调返回的是流数据,直接拿着数据流,去解析,解析ok,最后转成Bitmap,bitmap根据用户设置的transform或者默认transform做一次转换,最后将转换后的bitmap缓存到磁盘。
DiskCacheStrategy.DATA:只缓存源数据到磁盘。在SourceGenerator的startNextLoad去加载网络资源,下载回调返回的是流数据,然后将流数据缓存以源数据缓存到磁盘,然后将本地的磁盘缓存的源数据file使用NIO读取为DirectByteBuffer,然后对这个byteBuffer进行一系列的解析处理:可以解析,就将bytebuffer转成inputStream,最后转成bitmap,后面流程差不多一样了。

如果让我选择磁盘缓存策略,我会优先选DiskCacheStrategy.RESOURCE,至少在我看来从默认的设置AUTOMATIC没看到优点。不知道有没有啥副作用啊。

  • 4.2、 实际场景中弱引用、MemoryCache添加移除时机?

首次从网络加载图片,当bitmap一切就绪,在ImageView上设置Bitmap时会发通知完成回调,此时资源bitmap的弱引用会被添加,,,此时LruCache中没有Bitmap哦,不要以为bitmap此时也加入到LruCache中了。

public synchronized void onEngineJobComplete(....) {
if (resource != null && resource.isMemoryCacheable()) {
//此时加入弱引用缓存中
activeResources.activate(key, resource);
}
.....
}

那LruCache的添加操作在何时呢?当资源释放的时候,比如我们的页面(含有Glide加载ImageVeiw)关闭,或者recyclerView的Item列表滑动复用item时,会触发弱引用的清除和LruCache对资源的添加。

@Override
public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
//资源释放的时候,就清空弱引用先将它放入队列里面的
activeResources.deactivate(cacheKey);
if (resource.isMemoryCacheable()) {
//资源释放的时候,弱引用清楚,此时Lru缓存加入进去
cache.put(cacheKey, resource);
}
.....
}
synchronized void deactivate(Key key) {
ResourceWeakReference removed = activeEngineResources.remove(key);
if (removed != null) {
removed.reset();
}
}

页面关闭/返回 image.png

recyclerView列表滑动 image.png

那MemoryCache缓存中何时取出,又是何时添加的
其实就是在发起请求前,Engine先从内存缓存中取,有就直接通知回调,没有就走后面一系列流程。

private EngineResource<?> loadFromMemory(EngineKey key...) {
if (!isMemoryCacheable) { //跳过缓存
return null;
}
//从弱引用ResourceWeakReference中查找
EngineResource<?> active = = activeResources.get(key);
if (active != null) {
active.acquire(); //资源被使用,引用++
return active;
}
//从MemoryCache中找,找出来就是从LruCache中移除,remove的返回值就是啊
EngineResource<?> cached = cache.remove(key);
final EngineResource<?> result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
result = (EngineResource<?>) cached;
} else {
result = new EngineResource<>( cached, true,true, key,this);
}
if (result != null) {
//资源被使用,引用++ 且 添加到弱引用中
result.acquire();
activeResources.activate(key, result);
return result;
}
return null;
}

看样子,资源弱引用存在,那LruResourceCache就不可能存在这个资源,二者属于不同阶段的一个相互补充,没得交集。

五、后续

本期只是针对load(url)做了一个简单的操作流转的记录,这个记录贯穿了一系列的知识点,对Glide的了解还是比较浅,后续对其ModelLoader、Gif、video加载这块,也做个补充吧。

收起阅读 »

Android 架构之OkHttp源码解读(上)

前言在我们编写Android程序时,OkHttp已经成为了我们必不可少的部分,但我们往往知道OkHttp怎么用,不知其原理。在本篇中,我将通过如下方式带你深入其原理。OkHttp 介绍OkHttp 调用流程socket 连接池复用机制高并发请求队列:任务分发责...
继续阅读 »

前言

在我们编写Android程序时,OkHttp已经成为了我们必不可少的部分,但我们往往知道OkHttp怎么用,不知其原理。在本篇中,我将通过如下方式带你深入其原理。

  • OkHttp 介绍

  • OkHttp 调用流程

  • socket 连接池复用机制

  • 高并发请求队列:任务分发

  • 责任链模式拦截器设计

1.0 OkHttp 介绍

由Square公司贡献的一个处理网络请求的开源项目,是目前Android使用最广泛的网络框架。从Android4.4开始HttpURLConnection的底层实现采用的是OkHttp。

谷歌官方在6.0以后再android sdk已经移除了httpclient,加入了okhttp。

很多知名网络框架,比如 Retrofit 底层也是基于OkHttp实现的。

1.1 OkHttp 调用流程

图片1.png

如图所示:

OkHttp请求过程中最少只需要接触OkHttpClient、Request、Call、Response,但是框架内部进行大量的逻辑处理。

所有的逻辑大部分集中在拦截器中,但是在进入拦截器之前还需要依靠分发器来调配请求任务。

  • . 分发器:内部维护队列与线程池,完成请求调配;
  • . 拦截器:五大默认拦截器完成整个请求过程。

1.2 socket 连接池复用机制

在了解socket 的复用连接池之前,我们首先要了解几个概念。

  1. TCP 三次握手
  2. TCP 四次挥手

1.2.1 TCP三次握手

2.png

如图所示

我们把客户端比喻成男生,服务器比喻成女生。男生在追求女生的时候,男生发送了求偶的信号,女生在接受到求偶信号后,表示愿意接受男生,于是向男生发送了我愿意,但你要给我彩礼钱的信号。男生收到女生愿意信号后,表示也愿意给彩礼钱,向女生递交了彩礼钱。

整个过程双方必须秉持着相互了解对方的意图并且相互同意的情况下,才能相互连接。连接成功后,将会保持一段时间的长连接,就好如男女朋友在一起的一段时间,当发现彼此不合时,就迎来了TCP四次挥手(分手)

1.2.2 TCP四次挥手

3.png

如图所示

我们依然将客户端比喻成男生,服务器比喻成女生。当男生发现女生太做作了,不合适时,就向女生提出了分手,女生第一时间给男生反应,你为什么要分手?过后女生也想明白了,就再次问男生是不是确定要分手?男生实在不想继续下去了,于是就向女生表明了确定要分手。

在整个TCP四次挥手过程中,只要有一方提出了断开连接,另一方在收了到断开连接信息后,先是表明已经收到了断开连接提示,然后再次提出方发送是否确认断开的提示,当收到确认断开信息时,双方才能断开整个TCP连接。

所以为什么会有连接复用?或者说连接复用为什么会提高性能?

通常我们在发起http请求的时候首先要完成tcp的三次握手,然后传输数据,最后再释放连接。三次握手的过程可以参考这里 TCP三次握手详解及释放连接过程。 一次Http响应的过程 在这里插入图片描述

如图所示:

在高并发的请求连接情况下或者同个客户端多次频繁的请求操作,无限制的创建会导致性能低下。 因此http有一种叫做keepalive connections的机制,它可以在传输数据后仍然保持连接,当客户端需要再次获取数据时,直接使用刚刚空闲下来的连接而不需要再次握手。

在这里插入图片描述

Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间)。

1.2.3 连接池(ConnectionPool)分析

public final class ConnectionPool {
/**
* Background threads are used to cleanup expired connections. There will be at most a single
* thread running per connection pool. The thread pool executor permits the pool itself to be
* garbage collected.
*/

private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));

/** The maximum number of idle connections for each address. */
//每个地址的最大空闲连接数。
private final int maxIdleConnections;
//每个地址的最长保持时间
private final long keepAliveDurationNs;
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
// 双向队列
private final Deque<RealConnection> connections = new ArrayDeque<>();
final RouteDatabase routeDatabase = new RouteDatabase();
boolean cleanupRunning;
....

源码解析

  • Executor executor:线程池,用来检测闲置socket并对其进行清理。
  • Deque connections:缓存池。Deque 是一个双端列表,支持在头尾插入元素,这里用作LIFO(后进先出)堆栈,多用于缓存数据。
  • RouteDatabase routeDatabase:用来记录连接失败的router。

1、缓存操作

ConnectionPool提供对Deque进行操作的方法分别对put、get、connectionBecameIdle、evictAll几个操作。分别对应放入连接、获取连接、移除连接、移除所有连接操作。这里举例put和get操作。

put操作

  void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
//下文重点讲解
executor.execute(cleanupRunnable);
}
connections.add(connection);
}

源码解析

可以看到在新的connection 放进列表之前执行清理闲置连接的线程。 既然是复用,那么看下他获取连接的方式。

get操作

  /**
* Returns a recycled connection to {@code address}, or null if no such connection exists. The
* route is null if the address has not yet been routed.
*/

@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}

源码解析

遍历connections缓存列表,当某个连接计数的次数小于限制的大小以及request的地址和缓存列表中此连接的地址完全匹配。则直接复用缓存列表中的connection作为request的连接。

2、连接池清理和回收

上文我们讲到 Executor 线程池,用来清理闲置socket连接的。我们在put新连接到队列的时候会先执行清理闲置线程连接的线程,调用的是: executor.execute(cleanupRunnable),接下来我们就来分析:cleanupRunnable。

  private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};

源码解析

线程中不停调用Cleanup 清理的动作并立即返回下次清理的间隔时间。继而进入wait 等待之后释放锁,继续执行下一次的清理。所以可能理解成他是个监测时间并释放连接的后台线程。 所以在这只要了解cleanup动作的过程,就清楚了这个线程池是如何回收的了

3、总结

到这,整个socket连接池复用机制讲完了。连接池复用的核心就是用Deque来存储连接,通过put、get、connectionBecameIdle、evictAll几个操作。另外通过判断连接中的计数对象StreamAllocation来进行自动回收连接。

1.3 高并发请求队列:任务分发

图片4.png

如图所示

当我们进行多网络接口请求时,将会通过对应任务分发器分派对应的任务。在解读源码之前,将会先手写一份直播任务分发的小demo,先理解何为分发器,方便后面更容易理解OkHttp是如何进行分发的。

1.3.1、手写直播分发demo

需求整理:

当用户进入直播界面的时候,用户首先能看到主播流所展示的页面,其次红包流、购物车流、以及其他流所展示的界面布局。而且这些附加流可动态控制,每个模块也必须单独做自己模块的事。

先定义 直播任务分发器

  • LivePart
public abstract class LivePart {
public abstract void dispatch(BaseEvent event);
}
  • BaseEvent
public abstract class BaseEvent {

}
  • LiveEvent
//用于通知开播事件类
public class LiveEvent extends BaseEvent{
}

定义对应直播流

  • 主播流 SmallVideoPart
//事件分发机制
public class SmallVideoPart extends LivePart {
@Override
public void dispatch(BaseEvent event) {
if(event instanceof LiveEvent){
System.out.println("主播流来了,其他小视频窗口流要渲染出来了");
//可在这执行直播流相关的逻辑
}

}
}
  • 红包流 RedPackPart
//红包部件干他自己的事情
public class RedPackPart extends LivePart {
@Override
public void dispatch(BaseEvent event) {
if(event instanceof LiveEvent) {
System.out.println("直播流来了,红包准备开始");
//可在这执行红包相关的逻辑
}
}
}

  • 购物车流 GouwuchePart

哈哈哈,看到这是不是游刃有余呢?不过这里与同步请求不同的是,这里有俩个队列,一个正在执行的队列,一个为等待队列。 从这段代码里可知,什么时候进正在执行队列,什么时候进等待队列。 那么问题来了,已经进入等待队列里面的请求,什么时候迁移到执行队列里面来呢? 答案就在于这个方法的请求参 AsyncCall ,其实它就是一个Runnable ,进去寻找答案。

final class AsyncCall extends NamedRunnable {
private final Callback responseCallback;

String host() {
return originalRequest.url().host();
}

Request request() {
return originalRequest;
}

RealCall get() {
return RealCall.this;
}

AsyncCall(Callback responseCallback) {
super("OkHttp %s", redactedUrl());
this.responseCallback = responseCallback;
}

@Override protected void execute() {
boolean signalledCallback = false;
try {
//后面会重点讲解这getResponseWithInterceptorChain 方法
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
eventListener.callFailed(RealCall.this, e);
responseCallback.onFailure(RealCall.this, e);
}
} finally {
// 当请求执行完成调用了Dispatcher的finished方法
client.dispatcher().finished(this);
}
}
}

源码分析

这里就是网络请求的核心类,不过在这不用看那么多,只需要看最后 finally 调用了 finished 方法。也就是说每个网络请求结束时,都会调用该方法,这还没完全找到答案,继续追进dispatcher的 finished方法。

  /** Used by {@code AsyncCall#run} to signal completion. */
void finished(AsyncCall call) {
finished(runningAsyncCalls, call, true);
}

继续深入

  private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
// promoteCalls这里是true, 执行promoteCalls()
if (promoteCalls) promoteCalls();
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}

if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}

这里第三个变量为true,也就是 promoteCalls 这个方法是必然执行的,那么进这个方法看看。

  private void promoteCalls() {
// 如果执行的队列请求数量超过64个,直接return
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
// 如果等待的队列请求数量为空,直接return
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
// 遍历等待队列
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
// 检查一下正在执行的同一host的请求数量是不是不满5个
if (runningCallsForHost(call) < maxRequestsPerHost) {
// 满足条件,当前等待任务移出等待队列
i.remove();
//当前被移除等待队列的任务加入正在执行队列
runningAsyncCalls.add(call);
//直接执行请求任务!
executorService().execute(call);
}
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}

1.3.3、总结

哈哈哈哈,相信能看到这,上面提的问题直接迎刃而解。是不是很简单?再来一张图总结一下。

图片5.png

1.4 责任链模式拦截器设计

在上文讲解 Dispatcher 分发器的时候,里面讲解了异步请求,并且贴出了 AsyncCall 代码段,再次在这里贴一次。

final class AsyncCall extends NamedRunnable {
...略
@Override protected void execute() {
boolean signalledCallback = false;
try {
Response response = getResponseWithInterceptorChain();
...略
} catch (IOException e) {
...略
} finally {
client.dispatcher().finished(this);
}
}
}

同步调用

  @Override public Response execute() throws IOException {
...略
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
eventListener.callFailed(this, e);
throw e;
} finally {
client.dispatcher().finished(this);
}
}

源码分析

这里可以看出 同步、异步调用 代码段里面,都调用了 getResponseWithInterceptorChain 方法。既然都调用了这方法,那我们进入一探究竟。

  Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
//开发者自定义拦截器
interceptors.addAll(client.interceptors());
// RetryAndFollowUpInterceptor (重定向拦截器)
interceptors.add(retryAndFollowUpInterceptor);
// BridgeInterceptor (桥接拦截器)
interceptors.add(new BridgeInterceptor(client.cookieJar()));
//CacheInterceptor (缓存拦截器)
interceptors.add(new CacheInterceptor(client.internalCache()));
// ConnectInterceptor (连接拦截器)
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
//开发者自定义拦截器
interceptors.addAll(client.networkInterceptors());
}
//CallServerInterceptor(读写拦截器)
interceptors.add(new CallServerInterceptor(forWebSocket));

Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());

return chain.proceed(originalRequest);
}

源码分析

这段代码,我们可以理解成添加了一系列责任链拦截器。那么问题又来了。 何为责任链?何为拦截器?它们有什么作用? 在本篇里,先让你理解这些,然后在下一篇里具体详解每一个拦截器。 如果理解什么是责任链拦截器的读者也可以选择跳过下面内容,直接看下一篇 Android 架构之OkHttp源码解读(中)

在本篇里,我准备了俩个小demo,相信看完后应该能有所收获。

1.4.1 模拟公司员工报销Demo

需求整理

现在要写一个报销系统,其中组长报销额度为1000;主管报销额度为5000;经理报销额度为10000;boos报销额度为10000+。

代码如下

1、Leader

public abstract class Leader {
//上级领导
public Leader nextHandler;
/**
* 处理报账请求
* @param money 能批复的报账额度
*/

public final void handleRequest(int money){
System.out.println(getLeader());
if(money <=limit()){
handle(money);
}else{
System.out.println("报账额度不足,提交领导");
if(null != nextHandler){
nextHandler.handleRequest(money);
}
}
}
/**
* 自身能批复的额度权限
* @return 额度
*/

public abstract int limit();
/**
* 处理报账行为
* @param money 具体金额
*/

public abstract void handle(int money);
/**
* 获取处理者
* @return 处理者
*/

public abstract String getLeader();
}

代码分析

该类可看作为 领导基类,具有报销功能的人。

2、组长

//组长(额度1000)
public class GroupLeader extends Leader {
@Override
public int limit() {
return 1000;
}
@Override
public void handle(int money) {
System.out.println("组长批复报销"+ money +"元");
}
@Override
public String getLeader() {
return "当前是组长";
}
}

3、主管

//主管(额度5000):
public class Director extends Leader {
@Override
public int limit() {
return 5000;
}
@Override
public void handle(int money) {
System.out.println("主管批复报销"+ money +"元");
}
@Override
public String getLeader() {
return "当前是主管";
}
}

4、经理

//经理(额度10000)
public class Manager extends Leader {
@Override
public int limit() {
return 10000;
}
@Override
public void handle(int money) {
System.out.println("经理批复报销"+ money +"元");
}
@Override
public String getLeader() {
return "当前是经理";
}
}

5、boos

//老板
public class Boss extends Leader {
@Override
public int limit() {
return Integer.MAX_VALUE;
}
@Override
public void handle(int money) {
System.out.println("老板批复报销"+ money +"元");
}
@Override
public String getLeader() {
return "当前是老板";
}
}

6、开始报销

    //员工要报销  员工-》组长-》主管-》经理-》老板
//员工报销8000块
private void bxMoney(){
GroupLeader groupLeader = new GroupLeader();
Director director = new Director();
Manager manager = new Manager();
Boss boss = new Boss();
//设置上级领导处理者对象,组长的上级为主管
groupLeader.nextHandler = director;
//设置主管上级为经理
director.nextHandler = manager;
//设置经理上级为boos
manager.nextHandler = boss;
//这种责任链不好,还需要指定下一个处理对象
//发起报账申请
groupLeader.handleRequest(8000);
}

7、运行效果

 I/System.out: 当前是组长
I/System.out: 报账额度不足,提交领导
I/System.out: 当前是主管
I/System.out: 报账额度不足,提交领导
I/System.out: 当前是经理
I/System.out: 经理批复报销8000

8、总结

到这,相信你对责任链有了一个初步的认知,上一级做不好的交给下一级,但是这种责任链并不好,因为要通过代码手动指定责任链下一级到底是谁,而我们看到的OkHttp框架里面并不是用的这种模式。所以就迎来了第二个demo。

1.4.2 模拟支付场景Demo

需求整理

小明去超市里面买东西,结账的时候发现微信和支付宝的余额都不足,但是支付宝和微信里面余额加起来能够付款,于是小明 选择了微信、支付宝混合支付;小白也去超市买东西,但他的支付宝、微信金额都远远大于结账金额,于是他可以任选其一支付。

代码如下

1、定义一个具有支付能力的基类

public abstract class AbstractPay {

/**
* 支付宝支付
*/

public static int ALI_PAY = 1;

/**
* 微信支付
*/

public static int WX_PAY = 2;
/**
* 两者支付方式
*/

public static int ALL_PAY = 3;

/**
* 条码支付
*
* @param payRequest
* @param abstractPay
*/

abstract protected void barCode(PayRequest payRequest, AbstractPay abstractPay);
}

2、支付宝支付

public class AliPay extends AbstractPay {
@Override
public void barCode(PayRequest payRequest, AbstractPay abstractPay) {
if (payRequest.getPayCode() == ALI_PAY) {
System.out.println("支付宝扫码支付");
} else if(payRequest.getPayCode() == ALL_PAY){
System.out.println("支付宝扫码支付完成,等待下一步");
abstractPay.barCode(payRequest, abstractPay);
}else {
abstractPay.barCode(payRequest, abstractPay);
}
}
}

3、微信支付

public class WxPay extends AbstractPay {
@Override
public void barCode(PayRequest payRequest, AbstractPay abstractPay) {
if (payRequest.getPayCode() == WX_PAY) {
System.out.println("微信扫码支付");
} else if(payRequest.getPayCode() == ALL_PAY){
System.out.println("微信扫码支付完成,等待下一步");
abstractPay.barCode(payRequest, abstractPay);
}else {
abstractPay.barCode(payRequest, abstractPay);
}
}
}

4、待支付的商品

/**
* 待支付商品
*/

public class PayRequest {
//待选择的支付方式
private int payCode=0;

public int getPayCode() {
return payCode;
}

public void setPayCode(int payCode) {
this.payCode = payCode;
}
}

5、支付操作类

public class PayChain extends AbstractPay {
/**
* 完整责任链列表
*/

private List<AbstractPay> list = new ArrayList<>();

/**
* 索引
*/

private int index = 0;

/**
* 添加责任对象
*
* @param abstractPay
* @return
*/

public PayChain add(AbstractPay abstractPay) {
list.add(abstractPay);
return this;
}

@Override
public void barCode(PayRequest payRequest, AbstractPay abstractPay) {
// 所有遍历完了,直接返回
if (index == list.size()) {
System.out.println("支付全部完成,请取商品");
return;
}
// 获取当前责任对象
AbstractPay current = list.get(index);
// 修改索引值,以便下次回调获取下个节点,达到遍历效果
index++;
// 调用当前责任对象处理方法
current.barCode(payRequest, this);
}
}

6、开始支付

    private void scanMoney() {
PayRequest payRequest = new PayRequest();
//1、支付宝支付;2、微信支付;3、两者支付方式
payRequest.setPayCode(3);
PayChain chain = new PayChain();
chain.add(new AliPay());
chain.add(new WxPay());
chain.barCode(payRequest, chain);
}

7、运行效果

 I/System.out: 支付宝扫码支付完成,等待下一步
I/System.out: 微信扫码支付完成,等待下一步
I/System.out: 支付全部完成,请取商品

看这段代码结构是否似曾相识?这不就是OkHttp添加拦截器的格式么? 那么是不是可以假设一下,OkHttp添加的拦截器,是否也按照demo的方式执行的? 在这里再次贴一下OkHttp添加拦截器的代码段。

  Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
//开发者自定义拦截器
interceptors.addAll(client.interceptors());
// RetryAndFollowUpInterceptor (重定向拦截器)
interceptors.add(retryAndFollowUpInterceptor);
// BridgeInterceptor (桥接拦截器)
interceptors.add(new BridgeInterceptor(client.cookieJar()));
//CacheInterceptor (缓存拦截器)
interceptors.add(new CacheInterceptor(client.internalCache()));
// ConnectInterceptor (连接拦截器)
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
//开发者自定义拦截器
interceptors.addAll(client.networkInterceptors());
}
//CallServerInterceptor(读写拦截器)
interceptors.add(new CallServerInterceptor(forWebSocket));

Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());

return chain.proceed(originalRequest);
}

源码解读

这里看添加方式几乎和demo一样,那么使用呢?源码最后一句调用了RealInterceptorChain.proceed方法,我们进去看看。

 @Override public Response proceed(Request request) throws IOException {
return proceed(request, streamAllocation, httpCodec, connection);
}

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size())
throw new AssertionError();
...略
calls++;
...略
// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
...略
return response;
}

源码解读

看这,具体使用也和demo如出一辙,拦截器使用完了,demo选择的return结束,okHttp选择抛异常结束;每当一个拦截器使用完了,就会继续切换下一个拦截器。好了,本篇文章就到这差不多结束了,最后再来个总结。

8、总结

相信看到这里的小伙伴,你应该理解了OkHttp的重要性、调用流程、连接池复用、任务分发、以及这样添加责任链拦截器的原因。


收起阅读 »

Retrofit解析

本次源码解析基于2.9.0,如有描述错误,请大佬们评论指出。一、Retrofit的作用Retrofit基于okhttp,简化了okhttp请求接口的操作,而且适配Rxjava和kotlin的协程,但目前还没有适配kotlin的Flow,如果要适配,自己封装也是...
继续阅读 »

本次源码解析基于2.9.0,如有描述错误,请大佬们评论指出。

一、Retrofit的作用

Retrofit基于okhttp,简化了okhttp请求接口的操作,而且适配Rxjava和kotlin的协程,但目前还没有适配kotlin的Flow,如果要适配,自己封装也是可以的。

先看看早期直接使用okhttp请求 image.png 构造请求+解析响应+使用okhttp的线程池执行(当然okhttp也有同步调用),一堆操作很是麻烦,如果加上loading显示/隐藏、线程切换代码会更加复杂,retrofit+rxjava的经典搭配适应潮流就出现了。

retrofit适配的返回值 image.png 支持协程的话,小伙伴可能会懵逼了,注解啥的都好说,这个都好处理,它怎么拿到方法上的suspend,其实retrofit不需要拿suspend这个修饰符,因为java压根没有suspend,编译之后显真身,suspend在kotlin看来就只是一个挂起函数标志,在编译成java字节码后偷偷摸摸多了个用于回调的接口Continuation。 image.png 先来看看retrofit用法

//就是创建我们的retrofit客户端
public class HttpManager {
private Retrofit mRetrofit;
private Map<Class<?>, Object> mMap = new HashMap<>();
private static class SingletonInstance {
private static final HttpManager INSTANCE = new HttpManager();
}
public static HttpManager getInstance() {
return SingletonInstance.INSTANCE;
}
private HttpManager() {
mRetrofit = new Retrofit.Builder()
.client(自定义的okhttpClient) //不写的话,retrofit也会默认创建
.baseUrl("https://xxxx.xxxx.cn")
.addConverterFactory(ScalarsConverterFactory.create())//转换为String对象
.addConverterFactory(GsonConverterFactory.create())//转换为Gson对象
//接口返回值适配
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
}
public <T> T create(Class<T> cls) {
if (!mMap.containsKey(cls)) {
T t = mRetrofit.create(cls);
mMap.put(cls, t);
}
return (T) mMap.get(cls);
}
}

image.png image.png image.png

Q: 那个Call直接可以enqueue,那个observeable在subscribe后数据就可以接收,协程挂起恢复后就直接返回了,有点厉害,咋实现?
拿简单的observable来说,我们会使用create方法创建一个Observable,然后需要自己管理数据的发射,retrofit操作的Observable估计也要自行处理数据的发射???是这样么?后文解释。 image.png

Q: 方法上有注解,请求参数也有注解,返参还有泛型,这个怎么处理?
方法上有注解,请求参数也有注解,拿到method后解析注解,这个不难,拿到这些注解后,构建Request,如果是post的话,还要构造RequestBody,要注明MediaType,返回值是Call、Observable等决定是走默认的还是rxjava,或者协程,返回值上的泛型也很关键,在okhttp的rawResponse拿到后,要解析响应,需要预先选择合适的解析器解析数据。

二、从Retrofit+Observable请求post接口熟悉流程

  • 2.1、post请求编写

image.png 这里没有为协程专门搞个什么CallAdapterFactory哦,因为协程走默认的DefaultCallAdapterFactory。 这个默认的在创建Retrofit对象时添加进去的。 image.png image.png

  • 2.2、 retrofit的创建--->Retrofit的build方法

public Retrofit build() {
.....
okhttp3.Call.Factory callFactory = this.callFactory;
if (callFactory == null) { //没有就默认可以给你创建OkhttpClient
callFactory = new OkHttpClient();
}
Executor callbackExecutor = this.callbackExecutor;
if (callbackExecutor == null) { //回调的线程池,安卓默认就是主线程切换
callbackExecutor = new MainThreadExecutor();
}
//添加默认的 new DefaultCallAdapterFactory(callbackExecutor)
List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
callAdapterFactories.addAll(new DefaultCallAdapterFactory(callbackExecutor)
List<Converter.Factory> converterFactories = new ArrayList<>( 1 + this.converterFactories.size() + platform.defaultConverterFactoriesSize());
//添加一些默认的转换器
converterFactories.add(new BuiltInConverters());
converterFactories.addAll(this.converterFactories);
converterFactories.addAll(new OptionalConverterFactory());
return new Retrofit(....);
}
//android的回调的主线程池
static final class MainThreadExecutor implements Executor {
private final Handler handler = new Handler(Looper.getMainLooper());
@Override
public void execute(Runnable r) {
handler.post(r);
}
}

如果没看retrofit的build方法,在debug时,发现我们明明只添加了两个转换器和一个RxjavaCallAdapter,为啥会多出来一些不认识的转换器,那是因为retrofit在创建时,偷偷摸摸给你添加了一些默认的。多贴心呐。 记住:callFactory就是OkHttpClient

  • 2.3、 经典retrofit的动态代理---->create方法

image.png create方法的返回值是我们自定义的Api接口对象,所以可以直接调用Api的方法---废话。
InvocationHandler的invoke方法的返回值是Object,Api接口类里面方法的返回值可能是Call、Observable、Object,采用Object做返回值就都可以支持了。

来看看loadServiceMethod

private final Map<Method, ServiceMethod<?>> serviceMethodCache = new ConcurrentHashMap<>();
//用一个支持并发安全的Map缓存Method了
ServiceMethod<?> loadServiceMethod(Method method) {
ServiceMethod<?> result = serviceMethodCache.get(method);
if (result != null) return result;
synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) { //首次请求,肯定都会先去解析注解
result = ServiceMethod.parseAnnotations(this, method);
serviceMethodCache.put(method, result);
}
}
return result;
}

返回值是个ServiceMethod,他的子类有好几个。 image.png 其中以Rxjava和Call方式请求都是返回的CallAdapted.
协程返回的是SuspendForBody或者SuspendForResponse.

来看看parseAnnotations

static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
//先创建RequestFactory,然后创建HttpServiceMethod
RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);
........
返回值是Void以及不能解析的返回值类型判断
.....
return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
}
  • 2.4、 RequestFactory创建---->解析方法注解以及方法参数

很关键的RequestFactory.parseAnnotations(...)返回RequestFactory,它里面含有太多信息

static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
return new Builder(retrofit, method).build();
}
//方法参数处理器数组
ParameterHandler<?>[] parameterHandlers
RequestFactory build() {
for (Annotation annotation : methodAnnotations) {
//解析方法上的注解
parseMethodAnnotation(annotation);
}
//hasBody isFormEncoded relativeUrl isFormEncoded isMultipart gotPart
//那我们的post这里肯定是有body的哈,不是表单提交,这里不是很重要的细节,不写
......
int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = new ParameterHandler<?>[parameterCount];
for (int p = 0, lastParameter = parameterCount - 1; p < parameterCount; p++) {
parameterHandlers[p] =
//解析参数
parseParameter(p, parameterTypes[p], parameterAnnotationsArray[p], p == lastParameter);
}
.....
return new RequestFactory(this);
}

来看看解析方法上的注解parseMethodAnnotation(annotation)
这里就以例子@POST("app/courses/behaviour")参考,解析方法上的注解获取请求方式以及短路径

private void parseMethodAnnotation(Annotation annotation) {
//判断方法上注解的类型 DELETE GET HEAD PATCH PUT OPTIONS HTTP POST retrofit2.http.Headers
//Multipart FormUrlEncoded)
//这里只保留POST
if (annotation instanceof DELETE) {
....
} else if (annotation instanceof POST) {
//这里就是处理这个 @POST("app/courses/behaviour")
this.httpMethod ="POST";
this.hasBody = true;
String value=((POST) annotation).value();
if (value.isEmpty()) {
return;
}
.......
//保存短路径
this.relativeUrl = value;
this.relativeUrlParamNames = parsePathParameters(value);
}
......
}

来看解析方法参数--->parseParameter
先讲讲我们的Body类
fun getCourse(@Body info: CourseInfo): Observable 参数用@Body注解,设计了ParameterHandler这个类 image.png 这里拎出Body这个类,它要构造RequestBody,需选择xxxRequestBodyConverter才能构建成功。 image.png

private @Nullable ParameterHandler<?> parseParameter(....) {
ParameterHandler<?> result = null;
if (annotations != null) {
for (Annotation annotation : annotations) {
ParameterHandler<?> annotationAction =
//解析方法参数的注解
parseParameterAnnotation(p, parameterType, annotations, annotation);
.......
result = annotationAction;
}
}
if (result == null) {
if (allowContinuation) {
try { //判断是不是走协程,使用Continuation.class类判断
//这个判断有点粗糙
if (Utils.getRawType(parameterType) == Continuation.class) {
isKotlinSuspendFunction = true;
return null;
}
} catch (NoClassDefFoundError ignored) {
}
}
.....
}
return result;
}

上面👆就解释了,Retrofit怎么判断是走协程的,是通过判断参数里面有没有一个Continuation类型,是的话,就走协程。下面在参数里面添加了Continuation,但是我希望他走Rxjava,但不幸的事,它认为应该走协程,那就奔溃了。后文解释。 方来了,这个parseParameterAnnotation方法有400+行,一个方法400+行呐。但我们只看需要的 这里以注解是Field和Body为例。

@Nullable
private ParameterHandler<?> parseParameterAnnotation(
int p, Type type, Annotation[] annotations, Annotation annotation
) {
//annotation instanceof Url/Path/Query/QueryName/QueryMap/Header/HeaderMap等太多,删除
//以Field和Body类讲解
if (annotation instanceof Url) {
.....
} else if (annotation instanceof Field) {
.....
//省略部分逻辑
Converter<?, String> converter = retrofit.stringConverter(type, annotations);
return new ParameterHandler.Field<>(name, converter, encoded);
} else if (annotation instanceof Body) {
//body类型 肯定就不是表单类型了
......
Converter<?, RequestBody> converter;
converter = retrofit.requestBodyConverter(type, annotations, methodAnnotations);
//requestBody转换器存在于请求参数处理器中
return new ParameterHandler.Body<>(method, p, converter);
}
return null; // Not a Retrofit annotation.
}

Field注解: image.png 来看看retrofit.stringConverter(type, annotations)方法 image.png image.png image.png 请求url(baseUrl+relativeUrl拼接)、头字段的处理

Request.Builder get() {
HttpUrl url;
HttpUrl.Builder urlBuilder = this.urlBuilder;
if (urlBuilder != null) {
url = urlBuilder.build();
} else {
url = baseUrl.resolve(relativeUrl);
}
RequestBody body = this.body;
......
MediaType contentType = this.contentType;
if (contentType != null) {
if (body != null) {
body = new ContentTypeOverridingRequestBody(body, contentType);
} else {
headersBuilder.add("Content-Type", contentType.toString());
}
}
return requestBuilder.url(url).headers(headersBuilder.build()).method(method, body);
}

解析okhttp3的rawResponse为Retrofit的Response

 Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
ResponseBody rawBody = rawResponse.body();
rawResponse = rawResponse.newBuilder()
.body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
.build();
int code = rawResponse.code();
if (code < 200 || code >= 300) {
try {
// 使用okio读取的
ResponseBody bufferedBody = Utils.buffer(rawBody);
return Response.error(bufferedBody, rawResponse);
} finally {
rawBody.close();
}
}
if (code == 204 || code == 205) {
rawBody.close();
return Response.success(null, rawResponse);
}
ExceptionCatchingResponseBody catchingBody = new ExceptionCatchingResponseBody(rawBody);
try {
//之前保存的响应体转换器去转换ResponseBody
T body = responseConverter.convert(catchingBody);
return Response.success(body, rawResponse);
} catch (RuntimeException e) {
catchingBody.throwIfCaught();
throw e;
}
}

image.png

Q: 协程请求的接口方法声明上没有Call,它是怎么选择DefaultCallAdapterFactory的呢?

static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method)
{
//先创建RequestFactory
RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);
boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction; //true
......
Annotation[] annotations = method.getAnnotations();
Type adapterType;
if (isKotlinSuspendFunction) {
Type[] parameterTypes = method.getGenericParameterTypes();
//找那个Continuation<?super ExtendItem>参数,取的是ExtendItem的下界
Type responseType = Utils.getParameterLowerBound(0, (ParameterizedType) parameterTypes[parameterTypes.length - 1]);
.....
//获取返回值的类型,,这里偷偷摸摸加东西了,Call.class-->看来要走DefaultCallAdapterFactory
adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
//而且给方法注解加多一个SkipCallbackExecutor类型的注解
annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
} else {
adapterType = method.getGenericReturnType();
}
//创建CallAdapter 区分是走默认的还是Rxjava那一套
CallAdapter<ResponseT, ReturnT> callAdapter = createCallAdapter(retrofit, method, adapterType, annotations);
//校验响应类型是不是okhttp的Response,是的话直接throw Exception
//检验响应类型是不是retrofit的Response,是的话,没带泛型,直接throw Exception
//校验请求如果是head请求且返回值是Void类型,不满足的就直接throw Exception
.....
//创建转换器
Converter<ResponseBody, ResponseT> responseConverter = createResponseConverter(retrofit, method, responseType);
okhttp3.Call.Factory callFactory = retrofit.callFactory;
if (!isKotlinSuspendFunction) {
//非协程的部分
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
} else if (continuationWantsResponse) {
//协程的返回值是Response<XXX>类型
return (HttpServiceMethod<ResponseT, ReturnT>)
new SuspendForResponse<>(requestFactory, callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
} else {
//协程的返回值是xxx类型-->我们这里就是ExtendItem类型,所以走这里
return (HttpServiceMethod<ResponseT, ReturnT>)
new SuspendForBody<>(requestFactory, callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter, continuationBodyNullable);
}
}

requestFactory.isKotlinSuspendFunction在创建RequestFactory时,走协程的话会设置为true。
上面依次创建requestFactory、callAdapter,响应转换器responseConverter,最后创建SuspendForBody(HttpServiceMethod的子类)。--->callFactory就是OkhttpClient。
这个地方细节有点多,suspend编译之后,参数加了个Continuation,但是CallAdapter只有2种,协程走的是默认的DefaultCallAdapterFactory,而且为了不跟retrofit的Call请求方式起冲突了,偷偷摸摸的在我们代码里面下毒了。

    Type adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
Annotation[] annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);

Utils.ParameterizedTypeImpl就是ParameterizedType的子类,这里rawType就是Call,responseType就是ExtendItem

static final class ParameterizedTypeImpl implements ParameterizedType { 
.....
ParameterizedTypeImpl(@Nullable Type ownerType, Type rawType, Type... typeArguments) {
this.ownerType = ownerType;
this.rawType = rawType;
this.typeArguments = typeArguments.clone();
}
@Override
public Type getRawType() {
return rawType;
}
.......
}

从Contiuation参数上获取到响应类型是ExtendItem类型,然后再经过封装,将响应类型的getRawType返回Call,这点很关键,这下就决定能走DefaultCallAdapterFactory了。
再来看看ensurePrensent方法:偷偷摸摸给方法加上一个新的注解SkipCallbackExecutor返回,然后给CallAdapter创建使用。贴心呐。
SkipCallbackExecutor 表示跳过线程切换到主线程,协程才不用Retrofit的主线程切换MainThreadExecutor,是用户通过协程调度器实现。 image.png

最后,我们看看suspend实际处理的样子: image.png 创建callAdapter 之前也讲过的
image.png

看看DefaultCallAdapterFactory的get方法,有用到SkipCallbackExecutor注解。 image.png 看那个getRawType(returnType),只要是Call.class就能返回非null的CallAdapter,所以Retrofit封装响应类型的rawType为Call是有用的。
同时基于SkipCallbackExecutor注解的判断,导致CallAdapter的adapt方法直接将入参的call原样返回了。

响应体的转换器准备

private final List<Converter.Factory> converterFactories = new ArrayList<>();
private final List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>();

前面讲过的,他会尝试converFactories,我们这里是ExtendItem类型,那么它的转换器,只可能是GsonResponseBodyConverter了 image.png

Retrofit的java代码跟协程代码融合的地方-->SuspendForBody的invoke方法

final @Nullable Object invoke(Object[] args) {
Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
//之前SuspendForBody的CallAdapter啥事没干,入参是Retrofit中的Call,因没有处理线程切换的操作,返参还是Retrofit中的Call,看后面的图哈。
Call<ResponseT> call = callAdapter.adapt(call, args)
Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
try {
return isNullable ? KotlinExtensions.awaitNullable(call, continuation)
: KotlinExtensions.await(call, continuation);
} catch (Exception e) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}

}

image.png Retrofit的call创建好后,因我们的返回值是ExtendItem,不是可空的,所以就丢给KotlinExtensions.await方法,开始协程处理。

suspend fun <T : Any> Call<T>.await(): T {
//支持可取消的协程
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
//取消接口请求
cancel()
}
//走okhttp的异步接口请求
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
val invocation = call.request().tag(Invocation::class.java)!!
val method = invocation.method()
val e = KotlinNullPointerException("Response from " +method.declaringClass.name + '.' +method.name +" was null but response body type was declared as non-null")
continuation.resumeWithException(e)
} else {
continuation.resume(body)
}
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}

java调用转到kotlin调用,java的第一个参数是Call,这里是Call的拓展方法await,然后java第二个参数,给suspendCancellableCoroutine接收,这个await方法就是回调转成挂起函数的经典模板。这个模板代码一行都没精简过哈。

Q: 那协程请求完后切换到主线程在哪里执行的呢? image.png

看代码,关注下lifecycleScope的launch是否有主线程调度器,

val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
if (existing != null) {
return existing
}
val newScope = LifecycleCoroutineScopeImpl( this,
//协程上下文在这里
SupervisorJob() + Dispatchers.Main.immediate)
if (mInternalScopeRef.compareAndSet(null, newScope)) {
newScope.register()
return newScope
}
}
}

协程上下文:SupervisorJob() + Dispatchers.Main.immediate,从这里看出他是个Supervisor,主从作用域,它取消了,不会影响父协程,非常的可以。
调度器是这个Dispatchers.Main.immediate,还能说啥,协程的调度通过协程拦截器拦截Continuation实现。

image.png image.png kotlin的代码debug不好整,大家大致看下。

image.png

五、后续

我们看到用retrofit+协程写个接口请求,还要显式的try catch,真的是,有点,哎,看后续Retrofit支持Kotlin的Flow不,不支持的话,可以考虑自己整个试试。

收起阅读 »

Android 系统启动流程Init、Zygote、SystemService、ServiceManager

Android系统启动流程操作系统本身也是一个程序,只是这个程序是用来管理我们 App 应用程序的。 从系统的角度上来讲,Android系统的启动过程可以分为 bootloader 引导,装载和启动 linux内核,启动Android系统。Android 系统...
继续阅读 »

Android系统启动流程

操作系统本身也是一个程序,只是这个程序是用来管理我们 App 应用程序的。 从系统的角度上来讲,Android系统的启动过程可以分为 bootloader 引导,装载和启动 linux内核,启动Android系统。
Android 系统虽然也是基于 Linux 系统的,但是由于 Android 属于移动设备,并没有像 PC 那样的 BIOS 程序, 取而代之的是 BootLoader (系统启动加载器)。 Bootloader 相当于电脑上的Bios 他的主要作用就是初始化基本的硬件设备,建立内存空间映射,为装载linux内核准备好运行环境,当linux内核加载完毕之后,bootloder就会从内存中清除。在 Android 里没有硬盘,而是 ROM,它类似于硬盘存放操作系统,用户程序等。 ROM 跟硬盘一样也会划分为不同的区域,用于放置不同的程序。当 Linux 内核启动后会初始化各种软硬件环境,加载驱动程序,挂载根文件系统,Linux 内核加载的准备完毕后就开始加载一些特定的程序(进程)了。
具体流程,可以参考下流程图:

android_start.png
对于纯Android应用层开发来讲,了解一些Android的启动流程的知识并不会直接提高自己的代码质量。但是作为整个Android系统的开端,这部分的流程时刻影响着应用层的方方面面。这些知识也是作为Android开发进阶必须要了解的一部分。对于前面的bootloader引导也好,装载启动linux文件都是很底层的东西,感兴趣的可以自行了解一下,我们从启动androng系统开始分析,第一个加载的就是 init 进程。

一:init进程

我们应该都知道不管是 Java 还是 C/C++ 去运行某一个程序(进程)都是 XXX.xxx 的 main 方法作为入口,相信有很多大佬都跟我一样,App 开发做久了渐渐就忘记了还有个 main 方法。因此我们找到 /system/core/init/Init.cpp 的 main() 方法:

int main(int argc,char ** argv){

...
if(is_first_stage){
//创建和挂在启动所需要的文件目录
mount("tmpfs","/dev","tmpfs",MS_NOSUID,"mode=0755");
mkdir("/dev/pts",0755);
//创建和挂在很多...
...
}

...
//对属性服务进行初始化
property_init();

...
//用于设置子进程信号处理函数(如Zygote),如果子进程异常退出,init进程会调用该函数中设定的信号处理函数来处理
signal_handler_init();

...

//启动属性服务
start_property_service();

...

//解析init.rc配置文件
parser.ParseConfig("/init.rc");

}

main 方法里面有 148 行代码(不包括子函数代码)具体分为四个步骤:1.创建目录,挂载分区,2.解析启动脚本,3.启动解析的服务,4.守护解析的服务。init.rc 文件是 Android 系统的重要配置文件,位于 /system/core/rootdir/init.rc

import /init.environ.rc
import /init.usb.rc
// 当前硬件版本的脚本
import /init.${ro.hardware}.rc
import /init.${ro.zygote}.rc
import /init.trace.rc

on early-init
...
on init
...
// 服务 服务名称 执行文件路径 执行参数
// 有几个重要的服务
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server
service servicemanager /system/bin/servicemanager
service surfaceflinger /system/bin/surfaceflinger
service media /system/bin/mediaserver
service installd /system/bin/installd

在处理了繁多的任务后,init进程会进行最关键的一部操作: 启动Zygote
至此 init 进程已全部分析完毕,有四个步骤:1. 创建目录,挂载分区,2. 解析启动脚本,3. 启动解析的服务,4. 守护解析的服务。最需要注意的是 init 创建了 zygote(创建 App 应用的服务)、servicemanager (client 与 service 通信管理的服务)、surfaceflinger(显示渲染服务) 和 media(多媒体服务) 等 service 进程。

二:Zygote

Zygote 进程是由 init 进程通过解析 init.rc 文件而创建的。

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
socket zygote stream 666
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd

对应找到 /frameworks/base/cmds/app_process/app_main.cpp 源码文件中的 main 方法

int main(int argc, char* const argv[])
{
// AppRuntime 继承 AndoirdRuntime
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
// 过滤第一个参数
argc--;
argv++;
...
bool zygote = false;
bool startSystemServer = false;
bool application = false;
String8 niceName;
String8 className;

++i; // Skip unused "parent dir" argument.
// 解析参数
while (i < argc) {
const char* arg = argv[i++];
if (strcmp(arg, "--zygote") == 0) {
zygote = true;
niceName = ZYGOTE_NICE_NAME;
} else if (strcmp(arg, "--start-system-server") == 0) {
startSystemServer = true;
} else if (strcmp(arg, "--application") == 0) {
application = true;
} else if (strncmp(arg, "--nice-name=", 12) == 0) {
niceName.setTo(arg + 12);
} else if (strncmp(arg, "--", 2) != 0) {
className.setTo(arg);
break;
} else {
--i;
break;
}
}

...
//设置进程名
if (!niceName.isEmpty()) {
runtime.setArgv0(niceName.string());
set_process_name(niceName.string());
}
// 如果 zygote ,AndroidRuntime 执行 com.android.internal.os.ZygoteInit
// 看上面解析的脚本参数执行的是这里。
if (zygote) {
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied.\n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
return 10;
}
}

上面首先是解析参数,然后来到 /frameworks/base/core/jni/AndroidRuntime.cpp 中的 start 方法:

/*
* Start the Android runtime. This involves starting the virtual machi

相信很多人都跟我一样,刚开始看这样的代码有一定的难度,毕竟都是大学时期学的,不涉及底层开发的,看起来有些吃力,但是仔细阅读之后,或者通过命名,都可以大致看得懂流程。

Zygote 进程是由 init 进程解析 init.rc 脚本创建的,其具体的执行源码是在 App_main.main 方法,首先会创建一个虚拟机实例,然后注册 JNI 方法,最后通过 JNI 调用进入 Java 世界来到 ZygoteInit.main 方法。在 Java 世界中我们会为 Zygote 注册 socket 用于进程间通信,预加载一些通用的类和资源,启动 system_server 进程,循环等待孵化创建新的进程。总结一下ZygoteInit的main方法都做了哪些事情:

1.创建了一个Server端的Socket

2.预加载类和资源

3.启动了SystemServer进程

4.等待AMS请求创建新的应用程序进程

Zygote进程启动后,总共做了哪几件事:

1.创建AndroidRuntime并调用其start方法,启动Zygote进程。

2.创建Java虚拟机并为Java虚拟机注册JNI方法。

3.通过JNI调用ZygoteInit的main函数进入Zygote的java框架层。

4.通过registerZygoteSocket方法创建服务端Socket,并通过runSelectLoop方法等待AMS的请求来创建新的应用程序进程。

5.启动SystemServer。

三:SystemService

Zygote 进程的启动过程中会调用 startSystemServer 方法来启动 SystemServer 进程:

private static boolean startSystemServer(String abiList, String socketName) throws MethodAndArgsCaller, RuntimeException {
...
// 设置一些参数
String args[] = {
"--setuid=1000",
"--setgid=1000",
"--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1032,3001,3002,3003,3006,3007",
"--capabilities=" + capabilities + "," + capabilities,
"--nice-name=system_server",
"--runtime-args",
"com.android.server.SystemServer",
};

ZygoteConnection.Arguments parsedArgs = null;
int pid;
try {
...
// fork 创建 system_server 进程,后面会具体分析
pid = Zygote.forkSystemServer(
parsedArgs.uid, parsedArgs.gid,
parsedArgs.gids,
parsedArgs.debugFlags,
null,
parsedArgs.permittedCapabilities,
parsedArgs.effectiveCapabilities);
} catch (IllegalArgumentException ex) {
throw new RuntimeException(ex);
}

// pid == 0 代表子进程,也就是 system_server 进程
if (pid == 0) {
// 执行初始化 system_server 进程
handleSystemServerProcess(parsedArgs);
}
return true;
}

1. 启动 SystemServer

public static int forkSystemServer(int uid, int gid, int[] gids, int debugFlags,
int[][] rlimits, long permittedCapabilities, long effectiveCapabilities) {
VM_HOOKS.preFork();
int pid = nativeForkSystemServer(uid, gid, gids, debugFlags, rlimits, permittedCapabilities, effectiveCapabilities);
// Enable tracing as soon as we enter the system_server.
if (pid == 0) {
Trace.setTracingEnabled(true);
}
VM_HOOKS.postForkCommon();
return pid;
}

// 调用的 native 方法去创建的,nativeForkSystemServer() 方法在 AndroidRuntime.cpp 中注册的,调用 com_android_internal_os_Zygote.cpp 中的 com_android_internal_os_Zygote_nativeForkSystemServer() 方法
native private static int nativeForkSystemServer(int uid, int gid, int[] gids, int debugFlags,int[][] rlimits, long permittedCapabilities, long effectiveCapabilities);

static jint com_android_internal_os_Zygote_nativeForkSystemServer(
JNIEnv* env, jclass, uid_t uid, gid_t gid, jintArray gids,
jint debug_flags, jobjectArray rlimits, jlong permittedCapabilities,
jlong effectiveCapabilities) {
// fork 创建 systemserver 进程
pid_t pid = ForkAndSpecializeCommon(env, uid, gid, gids,
debug_flags, rlimits, permittedCapabilities, effectiveCapabilities,
MOUNT_EXTERNAL_DEFAULT, NULL, NULL, true, NULL,NULL, NULL);
// pid > 0 是父进程执行的逻辑
if (pid > 0) {
// waitpid 等待 SystemServer 进程的退出,如果退出了重启 zygote 进程
if (waitpid(pid, &status, WNOHANG) == pid) {
RuntimeAbort(env);
}
}
return pid;
}

static pid_t ForkAndSpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray javaGids, jint debug_flags, jobjectArray javaRlimits, jlong permittedCapabilities, jlong effectiveCapabilities, jint mount_external, jstring java_se_info, jstring java_se_name, bool is_system_server, jintArray fdsToClose, jstring instructionSet, jstring dataDir) {
//设置子进程的 signal 信号处理函数
SetSigChldHandler();
// fork 子进程(SystemServer)
pid_t pid = fork();
if (pid == 0) {
// 进入子进程
...
// gZygoteClass = com/android/internal/os/Zygote
// gCallPostForkChildHooks = GetStaticMethodIDOrDie(env, gZygoteClass, "callPostForkChildHooks", "(ILjava/lang/String;)V");
// 等价于调用 Zygote.callPostForkChildHooks()
env->CallStaticVoidMethod(gZygoteClass, gCallPostForkChildHooks, debug_flags,is_system_server ? NULL : instructionSet);
...
} else if (pid > 0) {
// the parent process
}
return pid;
}

private static void handleSystemServerProcess( ZygoteConnection.Arguments parsedArgs) throws ZygoteInit.MethodAndArgsCaller {
...
if (parsedArgs.invokeWith != null) {
...
} else {
ClassLoader cl = null;
if (systemServerClasspath != null) {
// 创建类加载器,并赋予当前线程
cl = new PathClassLoader(systemServerClasspath, ClassLoader.getSystemClassLoader());
Thread.currentThread().setContextClassLoader(cl);
}
// RuntimeInit.zygoteInit
RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs, cl);
}
}

public static final void zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) throws ZygoteInit.MethodAndArgsCaller {
...
// 通用的一些初始化
commonInit();
// 这个方法是 native 方法,主要是打开 binder 驱动,启动 binder 线程,后面分析 binder 驱动的时候再详解。
nativeZygoteInit();
// 应用初始化
applicationInit(targetSdkVersion, argv, classLoader);
}

private static void applicationInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) throws ZygoteInit.MethodAndArgsCaller {
...
final Arguments args;
try {
// 解析参数 Arguments
args = new Arguments(argv);
} catch (IllegalArgumentException ex) {
return;
}
...
invokeStaticMain(args.startClass, args.startArgs, classLoader);
}

private static void invokeStaticMain(String className, String[] argv, ClassLoader classLoader) throws ZygoteInit.MethodAndArgsCaller {
Class<?> cl = Class.forName(className, true, classLoader);
...

Method m;
try {
m = cl.getMethod("main", new Class[] { String[].class });
} catch (NoSuchMethodException ex) {
...
} catch (SecurityException ex) {
...
}
...
// 通过抛出异常,回到了 ZygoteInit.main()
// try{} catch (MethodAndArgsCaller caller) {caller.run();}
throw new ZygoteInit.MethodAndArgsCaller(m, argv);
}

绕了一大圈我们发现是通过抛异常回到了 ZygoteInit.main() 方法中的 try…catch(){MethodAndArgsCaller.run() }

2. 创建 SystemServer

public static class MethodAndArgsCaller extends Exception implements Runnable {
...
public void run() {
try {
// 根据传递过来的参数可知,此处通过反射机制调用的是 SystemServer.main() 方法
mMethod.invoke(null, new Object[] { mArgs });
} catch (IllegalAccessException ex) {
...
}
}

public final class SystemServer {
...
public static void main(String[] args) {
new SystemServer().run();
}

private void run() {
// 主线程 looper
Looper.prepareMainLooper();

// 初始化系统上下文
createSystemContext();

// 创建系统服务管理
mSystemServiceManager = new SystemServiceManager(mSystemContext);
// 将 mSystemServiceManager 添加到本地服务的成员 sLocalServiceObjects,sLocalServiceObjects 里面是一个静态的 map 集合
LocalServices.addService(SystemServiceManager.class, mSystemServiceManager);

//启动各种系统服务
try {
// 启动引导服务
startBootstrapServices();
// 启动核心服务
startCoreServices();
// 启动其他服务
startOtherServices();
} catch (Throwable ex) {
Slog.e("System", "************ Failure starting system services", ex);
throw ex;
}

// 一直循环执行
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}

private void createSystemContext() {
// 创建系统进程的上下文信息,这个在进程启动再详解
ActivityThread activityThread = ActivityThread.systemMain();
mSystemContext = activityThread.getSystemContext();
...
}

private void startBootstrapServices() {
// 阻塞等待与 installd 建立 socket 通道
Installer installer = mSystemServiceManager.startService(Installer.class);

// 启动服务 ActivityManagerService
mActivityManagerService = mSystemServiceManager.startService(ActivityManagerService.Lifecycle.class).getService();
mActivityManagerService.setSystemServiceManager(mSystemServiceManager);

// 启动服务 PackageManagerService
mPackageManagerService = PackageManagerService.main(mSystemContext, installer, mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore);
mPackageManager = mSystemContext.getPackageManager();

// 设置 AMS , 把自己交给 ServiceManager. addService 去管理
mActivityManagerService.setSystemProcess();

...
}

private void startCoreServices() {
...
}

private void startOtherServices() {
// 启动闹钟服务
mSystemServiceManager.startService(AlarmManagerService.class);
// 初始化 Watchdog
final Watchdog watchdog = Watchdog.getInstance();
watchdog.init(context, mActivityManagerService);
// 输入管理的 service
inputManager = new InputManagerService(context);
// WindowManagerService
wm = WindowManagerService.main(...);
// InputManagerService 和 WindowManagerService 都交给 ServiceManager 管理
ServiceManager.addService(Context.WINDOW_SERVICE, wm);
ServiceManager.addService(Context.INPUT_SERVICE, inputManager);
// 启动input
inputManager.start();
// 显示启动界面
ActivityManagerNative.getDefault().showBootMessage(...);
// 状态栏管理
statusBar = new StatusBarManagerService(context, wm);
// JobSchedulerService
mSystemServiceManager.startService(JobSchedulerService.class);
...
// 准备好了 wms, pms, ams 服务
wm.systemReady();
mPackageManagerService.systemReady();
mActivityManagerService.systemReady();
}

...
}

我们可以看到SystemServer在启动后,陆续启动了各项服务,包括ActivityManagerService,PowerManagerService,PackageManagerService等等,而这些服务的父类都是SystemService。

最后总结一下SystemServer进程:

1.启动Binder线程池

2.创建了SystemServiceManager(用于对系统服务进行创建、启动和生命周期管理)

3.启动了各种服务

3. 管理 SystemServer

系统服务启动后都会交给 ServiceManager 来管理,无论是 mSystemServiceManager.startService 还是 ServiceManager.addService 都是走的 ServiceManager.addService() 方法:

public static void addService(String name, IBinder service) {
try {
getIServiceManager().addService(name, service, false);
} catch (RemoteException e) {
Log.e(TAG, "error in addService", e);
}
}

private static IServiceManager getIServiceManager() {
if (sServiceManager != null) {
return sServiceManager;
}
sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());
return sServiceManager;
}

public abstract class ServiceManagerNative extends Binder implements IServiceManager {
static public IServiceManager asInterface(IBinder obj) {
if (obj == null) {
return null;
}
IServiceManager in =(IServiceManager)obj.queryLocalInterface(descriptor);
if (in != null) {
return in;
}
// 创建 ServiceManagerProxy 对象
return new ServiceManagerProxy(obj);
}
}

class ServiceManagerProxy implements IServiceManager {
private IBinder mRemote;

public ServiceManagerProxy(IBinder remote) {
mRemote = remote;
}
...
// IPC binder 驱动
public void addService(String name, IBinder service, boolean allowIsolated)
throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IServiceManager.descriptor);
data.writeString(name);
data.writeStrongBinder(service);
data.writeInt(allowIsolated ? 1 : 0);
// mRemote 是 IBinder 对象
mRemote.transact(ADD_SERVICE_TRANSACTION, data, reply, 0);
reply.recycle();
data.recycle();
}
}

最后我们再来总结一下:SystemServer 进程是由 Zygote 进程 fork 创建的,SystemServer 进程创建后会创建启动引导服务、核心服务和其他服务,并且将所创建的服务,通过跨进程通信交给 ServiceManager 进程来管理。

哈哈,看完这么多,是不是感觉有点了解,然后又不是特别了解,有一定的了解,就会想去深入了解,花点时间,慢慢去消化,主体流程了解,具体的底层实现,有兴趣可以多了解。


收起阅读 »

国庆渐变头像

国庆五星红旗渐变色头像五星红旗半透明头像教程国旗渐变头像国庆头像 国旗渐变 制作设置教程此生不悔入华夏 祝祖国繁荣昌盛!效果展示缘起群聊的时候, 有人说这个国旗渐变的效果, 我看了一下, 有点帅呢, 就研究了一下环境雷电模拟器: 4.0.63 Android版...
继续阅读 »

国庆五星红旗渐变色头像

五星红旗半透明头像教程

国旗渐变头像

国庆头像 国旗渐变 制作设置教程

此生不悔入华夏 祝祖国繁荣昌盛!

效果展示

01.jpg02.jpg03.jpg04.jpg05.jpg06.jpg07.jpg08.jpg09.jpg10.jpg11.jpg12.jpg13.jpg14.jpg15.jpg

缘起

群聊的时候, 有人说这个国旗渐变的效果, 我看了一下, 有点帅呢, 就研究了一下

环境

雷电模拟器: 4.0.63 Android版本: 7.1.2 Autojs版本: 8.8.20

思路

  1. 准备国旗和头像
  2. 国旗修改透明渐变
  3. 合并两张图片

你将学到以下知识点

  • 判断图片类型, 如果用户传入了jpg, 就把他变成png
  • 修改图片大小, 把国旗和头像宽高改为一致
  • 修改图片为透明渐变
  • 保存mat为文件
  • byte数组转为16进制字符串
  • mat转bitmap
  • 打印mat的属性
  • 合并图片

代码讲解

1. 国旗渐变我做成了模块, 方便调用
let formatImg = require("./formatImg");

let dir = files.path("./img");
var arr = files.listDir(dir);
let filePathList = arr.map((item) => {
return files.join(dir, item);
});

filePathList.map((filePath) => {
formatImg(filePath);
});
2. 导入类
runtime.images.initOpenCvIfNeeded();
importClass(org.opencv.core.MatOfByte);
importClass(org.opencv.core.Scalar);
importClass(org.opencv.core.Point);
importClass(org.opencv.core.CvType);
importClass(java.util.List);
importClass(java.util.ArrayList);
importClass(java.util.LinkedList);
importClass(org.opencv.imgproc.Imgproc);
importClass(org.opencv.imgcodecs.Imgcodecs);
importClass(org.opencv.core.Core);
importClass(org.opencv.core.Mat);
importClass(org.opencv.core.MatOfDMatch);
importClass(org.opencv.core.MatOfKeyPoint);
importClass(org.opencv.core.MatOfRect);
importClass(org.opencv.core.Size);
importClass(org.opencv.features2d.DescriptorMatcher);
importClass(org.opencv.features2d.Features2d);
importClass(org.opencv.core.MatOfPoint2f);
importClass(org.opencv.android.Utils);
importClass(android.graphics.Bitmap);
importClass(java.lang.StringBuilder);
importClass(java.io.FileInputStream);
importClass(java.io.File);
3. 定义图片类型, 用于判断图片类型
const TYPE_JPG = "jpg";
const TYPE_GIF = "gif";
const TYPE_PNG = "png";
const TYPE_BMP = "bmp";
const TYPE_UNKNOWN = "unknown";
4. 归一化图片, 都改为png格式
function formatImg(imgPath) {
let type = getPicType(new FileInputStream(new File(files.path(imgPath))));
if (type === "png") {
return imgPath;
} else if (type === "jpg") {
var img = images.read(imgPath);
images.save(img, "/sdcard/tempImg001.png");
return "/sdcard/tempImg001.png";
} else {
toastLog("只支持jpg或者png");
return false;
}
}
5. 读取图片
var img = images.read(imgPath);
var img2 = images.read(imgPath2);
6. 修改图片大小
function normalize(img, img2) {
let imgWidth = img.getWidth(); // 200
let imgHeight = img.getHeight(); // 200
return images.resize(img2, [imgWidth, imgHeight]);
}
7. 修改图片透明渐变
function transparentGradient(mat) {
let width = mat.width();
let height = mat.height();
let unit = 256 / width;
let wLimit = (width / 5) * 3;
for (var i = 0; i < height; i++) {
for (var j = 0; j < width; j++) {
let item = mat.get(i, j);
if (j > wLimit) {
item[3] = 0;
} else {
item[3] = 180 - unit * j;
}
mat.put(i, j, item);
}
}
return mat;
}
8. 合并图片
let img4 = merge(mat, mat3);
9. 预览效果
let tempDir = files.join("/sdcard/Pictures/img");
let tempFilePath2 = files.join(tempDir, files.getName(oImgPath2));
files.createWithDirs(tempFilePath2);
images.save(img4, tempFilePath2);
app.viewFile("/sdcard/1.png");
10. 释放资源
img.recycle();
img2.recycle();
img3.recycle();
img4.recycle();
mat.release();
mat3.release();

名人名言

思路是最重要的, 其他的百度, bing, stackoverflow, 安卓文档, autojs文档, 最后才是群里问问 --- 牙叔教程


收起阅读 »

Kotlin系列八:静态方法、infix函数、高阶函数的常见应用举例

一 静态方法 java中定义静态方法只需要在方法前添加static即可; kotlin中有四种方式:object的单例类模式、companion object(可以局部写静态方法)、JvmStatic注解模式、顶层函数模式。 1.1 object 用objec...
继续阅读 »

一 静态方法


java中定义静态方法只需要在方法前添加static即可;


kotlin中有四种方式:object的单例类模式、companion object(可以局部写静态方法)、JvmStatic注解模式、顶层函数模式。


1.1 object


用object修饰的类,实际上是单例类,在Kotlin中调用时是类名加方法直接使用。


object Util {
fun doAction(){
Log.v("TAG","doAction")
}
}

//kotlin中调用
Util.doAction()

//java中调用 INSTANCE是Util的单例类
Util.INSTANCE.doAction();

1.2 companion object


用companion object修饰的方法也能通过类名加.直接调用,但是这时通过伴生对象实现的。它的原理是在原有类中生成一个伴生类,Kotlin会保证这个伴生类只有一个对象。


class Util{
//此处是单例类可以调用的地方
companion object {
fun doAction(){
Log.v("TAG","doAction")
}
}
//此处是普通方法
fun doAction2(){
Log.v("TAG","doAction")
}
}

//kotlin调用
Util.doAction()

//java调用,Companion是单例类
Util.Companion.doAction();

1.3 @JvmStatic注解


给单例类(object)和伴生对象的方法加@JvmStatic注解,这时编译器会将这些方法编译成真正的静态方法。


注意:JvmStatic只能注释在单例类或companion object中的方法上。


class Util{
//此处是单例类可以调用的地方
companion object {
@JvmStatic
fun doAction(){
Log.v("TAG","doAction")
}
}

//此处是普通方法
fun doAction2(){
Log.v("TAG","doAction")
}

}

//kotlin中的调用
Util.doAction()

//java中的调用,成为了真正的单例类
Util.doAction();

1.4 顶层方法


顶层函数是指那些没有定义在任何类中的函数,写在任何类的外层即可,kotlin会将所有顶层函数编译成静态方法,可以在任何位置被直接调用。


//在类的外部
fun doAction(){
}
class Util {
}

//kotlin调用方式
doAction()

//java调用方式,真正的静态方法
UtilKt.doAction();

二 infix函数


infix函数作用:将函数调用的语法修改了一下。


比如:A to B 等于 A.to(B)。


实现方式:在函数前面加上infix即可。


限制条件:1.不能是顶层函数;2.参数只能有一个。


例子:


infix fun String.beginsWith(p:String) = startsWith(p)

三 利用高阶函数简化常用API


apply函数简化intent.putExtra():


 fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
val editor = edit()
editor.block()
editor.apply()
}


使用:
getSharedPreferences("user", Context.MODE_PRIVATE).open {
putString("username", "Lucas")
putBoolean("graduated", false)
}

简化SharedPreferences:


 fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
val editor = edit()
editor.block()
editor.apply()
}


使用:
getSharedPreferences("user", Context.MODE_PRIVATE).open {
putString("username", "Lucas")
putBoolean("graduated", false)
}

简化ContentValues:


fun cvOf(vararg pairs: Pair<String, Any?>) =  ContentValues().apply {
for(pair in pairs){
val key = pair.first
val value = pair.second
when(value){
is Int -> put(key, value)
is Long -> put(key, value)
is Short -> put(key, value)
is Float -> put(key, value)
is Double -> put(key, value)
is Boolean -> put(key, value)
is String -> put(key, value)
is Byte -> put(key, value)
is ByteArray -> put(key, value)
null -> putNull(key)
}
}
}

懒加载技术实现lazy():


  fun <T> later(block: () -> T) = Later(block)

class Later<T>(val block: () -> T){
var value: Any? = null

operator fun getValue(any: Any?, prop: KProperty<*>): T{
if (value == null){
value = block
}
return value as T
}
}

使用:
val haha:String by later {
"hhaa"
}

泛型实化简化startActivity()并附带intent传值:


    inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit){
val intent = Intent(context, T::class.java)
intent.block()
context.startActivity(intent)
}
使用:
startActivity<MainActivity>(this){
putExtra("haha","1111")
putExtra("hahaaaa","1111")
}

简化N个数的最大值最小值:


   fun <T : Comparator<T>> MyMax(vararg nums: T): T{
if (nums.isEmpty()) throw RuntimeException("params can not be empty")
var maxNum = nums[0]
for (num in nums){
if (num > maxNum){
maxNum = num
}
}
return maxNum
}

使用:
val a = 1
val b = 3
val c = 2
val largest = MyMax(a,b,c)

简化Toast:


    fun String.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT){
Toast.makeText(context, this, duration).show()
}
fun Int.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT){
Toast.makeText(context, this, duration).show()
}

用法:
"ahah".showToast(this, Toast.LENGTH_LONG)

简化Snackbar:


fun View.showSnackbar(text: String, actionText: String?=null, duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? =null){
val snackbar = Snackbar.make(this, text, duration)
if (actionText != null && block != null){
snackbar.setAction(actionText){
block()
}
}
snackbar.show()
}

fun View.showSnackbar(text: String, actionResId: Int?=null, duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? =null){
val snackbar = Snackbar.make(this, text, duration)
if (actionResId != null && block != null){
snackbar.setAction(actionResId){
block()
}
}
snackbar.show()
}

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

反射解决FragmentDialog内存泄露??‍♂️

怎么引发内存泄露的 这个DialogFragment的内存泄露几年前我就遇到了,但当时也稀里糊涂的,在网上搜索各种办法,看的我也是云里雾里,迷迷糊糊。在查阅大量资料之后,终于明白为什么会导致内存泄露了。 归根到底就是DialogFragment在给Dialog...
继续阅读 »

怎么引发内存泄露的


这个DialogFragment的内存泄露几年前我就遇到了,但当时也稀里糊涂的,在网上搜索各种办法,看的我也是云里雾里,迷迷糊糊。在查阅大量资料之后,终于明白为什么会导致内存泄露了。


归根到底就是DialogFragment在给Dialog设置setOnCancelListenersetOnDismissListener的时候将当前的DialogFragment引用传给了Message。在一些复杂项目中,各种各样的第三方库都有自己的消息处理,是根HandleThread有关系,这玩意一多就容易有问题。(最后一句话我搬的,其实我也不清楚🤣)


Looper.loop()中用MessageQueue.next()去取消息,如果之后没有消息,next()会处于一个挂起状态,MessageQueue会一直检测最后一条消息链是否有next消息被添加,于是最后的消息会被一直索引,直到下一条Message出现。


我就不展示这些源码了,因为可能看不懂,所以我根据自己的理解写了个简单的差不多的测试:


我先创建一个自己的Looper->MyLooper,模拟Looper的运作


object MyLooper {
//处理消息队列的类
val myQueue = MyMessageQueue()
///添加一条消息
fun addMessage(msg: Message) {
println("添加消息: ${msg.obj}")
myQueue.addMessage(msg)
}
//开始吧
fun lopper() {
while (true) {
val next = myQueue.next()
println("处理消息---->${next?.obj}")
if (next == null) {
return
}
}
}
}

创建消息Message和队列MessageQueue,我不写那么复杂了,差不多一个意思,一个是消息载体,一个是处理消息队列的。



class Message(var obj: Any? = null, var next: Message? = null)

class MyMessageQueue {
//初始消息
private var message: Message = Message("线程启动")
//将新来的消息添加到当前消息的屁股后面
fun addMessage(msg: Message?) {
//我的下一个消息就是你
message.next = msg
}
//检索下一个Message,如果没有下一个message,我就等下一条消息出现。
fun next(): Message {
while (true) {
if (message.next == null) {
println("重新检查消息 当前被卡住的消息-${message.obj}")
Thread.sleep(100)
continue
}
val next = message.next
message = next!!
return message
}
}
}

写一个测试类试试


    @Test
fun test() {
println("消息测试开始")
Thread {
MyLooper.lopper()
}.start()
Thread.sleep(100)
MyLooper.addMessage(Message("One Message"))//发送第一个消息
Thread.sleep(100)
MyLooper.addMessage(Message("Two Message"))//发送第二个消息
Thread.sleep(100)
while (true) {
continue
}
}

运行结果也不负众望,最后一条消息一直被索引。


myLooper.png


这差不多就是我理解的意思。


如何处理


DialogFragment要通过消息机制来通知自己关闭了,这个逻辑没办法更改。我们只能通过弱引用当前的DialogFragment让系统GG的时候帮我们回收掉,我的最终解决是通过反射替换父类的变量。


重写DialogFragment设置的两个监听器

    private DialogInterface.OnCancelListener mOnCancelListener =
new DialogInterface.OnCancelListener() {
@SuppressLint("SyntheticAccessor")
@Override
public void onCancel(@Nullable DialogInterface dialog) {
if (mDialog != null) {
DialogFragment.this.onCancel(mDialog);
}
}
};

private DialogInterface.OnDismissListener mOnDismissListener =
new DialogInterface.OnDismissListener() {
@SuppressLint("SyntheticAccessor")
@Override
public void onDismiss(@Nullable DialogInterface dialog) {
if (mDialog != null) {
DialogFragment.this.onDismiss(mDialog);
}
}
};

上面两个是DialogFragment源码的两个监听器,不管他怎么写,最后都是要把当前的this放进去。


所以我们重写两个监听器。


因为两个监听器的操作流程差不多一样,我就写了个接口,等会你就明白了。


interface IDialogFragmentReferenceClear {
//弱引用对象
val fragmentWeakReference: WeakReference<DialogFragment>
//清理弱引用
fun clear()
}

重写取消监听器:


class OnCancelListenerImp(dialogFragment: DialogFragment) :
DialogInterface.OnCancelListener, IDialogFragmentReferenceClear {

override val fragmentWeakReference: WeakReference<DialogFragment> =
WeakReference(dialogFragment)

override fun onCancel(dialog: DialogInterface) {
fragmentWeakReference.get()?.onCancel(dialog)
}

override fun clear() {
fragmentWeakReference.clear()
}
}

重写关闭监听器:


class OnDismissListenerImp(dialogFragment: DialogFragment) :
DialogInterface.OnDismissListener, IDialogFragmentReferenceClear {

override val fragmentWeakReference: WeakReference<DialogFragment> =
WeakReference(dialogFragment)

override fun onDismiss(dialog: DialogInterface) {
fragmentWeakReference.get()?.onDismiss(dialog)
}

override fun clear() {
fragmentWeakReference.clear()
}
}

很简单是吧。


然后就是替换了。


替换父类的监听器

我这里的替换是直接替换的DialogFragment这两个变量。


我们在替换父类的监听器的时候,一定要在父类使用这两个监听器之前替换。因为在我测试过程中,在之后替换,还是有极小的概率造成内存泄露,很无语,但我也不知道为什么。


我们先捋一下Dialog的创建流程:


onCreateDialog(@Nullable Bundle savedInstanceState)出发,会依次找到这几个方法。



  1. public LayoutInflater onGetLayoutInflater

  2. private void prepareDialog

  3. public Dialog onCreateDialog


上面是按1.2.3顺序执行的。触发Dialog设置监听器是在onGetLayoutInflater,所以我们重写这个方法。在父类执行之前进行替换,使用反射替换~


    override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater {
//先尝试反射替换
val isReplaceSuccess = replaceCallBackByReflexSuper()
//现在可以执行父类的操作了
val layoutInflater = super.onGetLayoutInflater(savedInstanceState)
if (!isReplaceSuccess) {
Log.d("Dboy", "反射设置DialogFragment 失败!尝试设置Dialog监听")
replaceDialogCallBack()
} else {
Log.d("Dboy", "反射设置DialogFragment 成功!")
}

return layoutInflater
}

这里是核心的替换操作。我们找到要替换的类和字段,然后反射修改它的值。


    private fun replaceCallBackByReflexSuper(): Boolean {
try {
val superclass: Class<*> =
findSuperclass(javaClass, DialogFragment::class.java) ?: return false
//重新给取消接口赋值
val mOnCancelListener = superclass.getDeclaredField("mOnCancelListener")
mOnCancelListener.isAccessible = true
mOnCancelListener.set(this, OnCancelListenerImp(this))
//重新给关闭接口赋值
val mOnDismissListener = superclass.getDeclaredField("mOnDismissListener")
mOnDismissListener.isAccessible = true
mOnDismissListener.set(this, OnDismissListenerImp(this))
return true
} catch (e: NoSuchFieldException) {
Log.e("Dboy", "dialog 反射替换失败:未找到变量")
} catch (e: IllegalAccessException) {
Log.e("Dboy", "dialog 反射替换失败:不允许访问")
}
return false
}

我们在反射获取失败之后,在手动进行一次设置,看上面的调用时机。


    private fun replaceDialogCallBack() {
if (mOnCancelListenerImp == null) {
mOnCancelListenerImp = OnCancelListenerImp(this)
}
dialog?.setOnCancelListener(mOnCancelListenerImp)
if (mOnDismissListenerImp == null) {
mOnDismissListenerImp = OnDismissListenerImp(this)
}
dialog?.setOnDismissListener(mOnDismissListenerImp)
}

replaceDialogCallBack替换回调接口,可以减少内存泄露,但不能完全解决内存泄露。在没有特殊情况下,反射都是会成功的,只要反射替换成功,给内存泄露说拜拜。


然后再onDestroyView清空一下我们的弱引用。


    override fun onDestroyView() {
super.onDestroyView()
//手动清理一下弱引用
mOnCancelListenerImp?.clear()
mOnCancelListenerImp = null

mOnDismissListenerImp?.clear()
mOnDismissListenerImp = null
}

为什么你的解决方法不管用


我刚接触DialogFragment的时候,这个内存泄露就一直伴随着我。


我当时菜鸟,在网上找各种解决方法,有的说重写onCreateDialog替换一个自己的Dialog,重写两个监听器设置方法,然后不让DialogFragment设置这两个监听器就解决了...我去,我现在想想感觉这个是最弱智的解决办法了,完全是为了解决而解决,直接掐断源头。


之后还有一个比较靠谱的方法,和我这个一样,也是重写这两个接口弱引用对象,不过那个方法是在onActivityCreated中对Dialog的这两个接口进行的重新赋值。这个方法是可行了。但是后来,我发现又不行了。就是因为是在父类先设置一次监听器之后还是有机会造成内存泄露。


还有就是说,等你去翻阅自己AndroidStudio的DialogFragment源码之后你会发现你根本没有看到父类有这两个变量mOnCancelListenermOnDismissListener。其实我也发现了。


这是为什么?


DialogFragment的源码包是依赖在appcompat中的,它的版本有好几个.


appcompat_versions.png


当你引用低于1.3.0的版本是不适用于我这个解决办法的。当你高于1.3.0版本是可以使用的,当然你也可以单独引Fragment的依赖只要高于1.3.0就行。


appcompat:1.2.0 的源码

Snipaste_2021-09-27_18-04-50.png


在1.2.0,只能在onActivityCreated中重新设置两个监听器来减少内存泄露出现的概率


 override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (isLowVersion) {
Log.d("Dboy", "低版本中重新替换覆盖")
if (mOnCancelListenerImp == null) {
mOnCancelListenerImp = OnCancelListenerImp(this)
}
dialog?.setOnCancelListener(mOnCancelListenerImp)

if (mOnDismissListenerImp == null) {
mOnDismissListenerImp = OnDismissListenerImp(this)
}
dialog?.setOnDismissListener(mOnDismissListenerImp)
}
}

appcompat:1.3.0 的源码:

dialogFragment_1.3.4_listener.png
dialogFragment_1.3.4_listener_set.png


这两个版本的差异还是比较大的。所以你直接搜的解决办法,放到你的项目里,可能因为版本不对,导致没有效果。不过我也做了替代方案。当反射失败提示找不到变量的时候,做一下标记,认为是低版本,然后再到onActivityCreated中进行一次设置。


当你引用的第三方库或者其他模块中存在不同appcompat版本的时候,打包时会使用你项目里最高版本的,所以要多注意检查是否存在依赖冲突,版本内容差异过大会直接报错的。


加一下混淆

差点忘了最重要的,既然是反射,当然少不了混淆文件了。我们只需要保证在混淆编译的时候,DialogFragment中这两个变量mOnCancelListenermOnCancelListener不被混淆就可以了。


在你项目的proguard-rules.pro中加入这个规则:


-keepnames class androidx.fragment.app.DialogFragment{
private ** mOnCancelListener;
private ** mOnDismissListener;
}

后言


在我解决这个内存泄露的时候,当时真的是烦死我了,在网上搜索的帖子,不是复制粘贴别人的就是复制粘贴别人的。我看到某个帖子不错之后就会去找原文,我找到一篇使用弱引用解决内存泄露的文章DialogFragment引起的内存泄露 来自隔壁的。我看这位老哥最早发布的,不知道老哥是不是原创作者,如果是还是很厉害的。我也是从中学习到了。虽然我的解决办法是从他那里学到的,但是我不会复制粘贴别人的文章,不能做技术的盗窃者。我也不会使用别人的代码,我喜欢自己动手写,这样能在写代码中学到更多东西。


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

图解 ArrayDeque 比 LinkedList 快

接口 Deque 的子类 ArrayDeque ,作为栈使用时比 Stack 快,因为原来的 Java 的 Stack 继承自 Vector,而 Vector 在每个方法中都加了锁,而 Deque 的子类 ArrayDeque 并没有锁的开销。 接口 Dequ...
继续阅读 »

接口 Deque 的子类 ArrayDeque ,作为栈使用时比 Stack 快,因为原来的 Java 的 Stack 继承自 Vector,而 Vector 在每个方法中都加了锁,而 Deque 的子类 ArrayDeque 并没有锁的开销。


接口 Deque 还有另外一个子类 LinkedListLinkedList 基于双向链表实现的双端队列,ArrayDeque 作为队列使用时可能比 LinkedList 快。


而这篇文章主要来分析,为什么 ArrayDequeLinkedList 快。在开始分析之前,我们需要简单的了解一下它们的数据结构的特点。


接口 Deque


接口 Deque 继承自 Queue 即队列, 在 Java 中队列有两种形式,单向队列( AbstractQueue ) 和 双端队列( Deque ),单向队列效果如下所示,只能从一端进入,另外一端出去。



而今天主要介绍双端队列( Deque ), Deque 是双端队列的线性数据结构, 可以在两端进行插入和删除操作,效果如下所示。



双端队列( Deque )的子类分别是 ArrayDequeLinkedListArrayDeque 基于数组实现的双端队列,而 LinkedList 基于双向链表实现的双端队列,它们的继承关系如下图所示。



接口 DequeQueue 提供了两套 API ,存在两种形式,分别为抛出异常,和不抛出异常,返回一个特殊值 null 或者布尔值 ( true | false )。



























操作类型抛出异常返回特殊值
插入addXXX(e)offerXXX(e)
移除removeXXX()pollXXX()
查找element()peekXXX()

ArrayDeque


ArrayDeque 是基于(循环)数组的方式实现双端队列,数组初始化容量为 16(JDK 8),结构图如下所示。



ArrayDeque 具有以下特点:



  • 因为双端队列只能在头部和尾部插入或者删除元素,所以时间复杂度为 O(1),但是在扩容的时候需要批量移动元素,其时间复杂度为 O(n)

  • 扩容的时候,将数组长度扩容为原来的 2 倍,即 n << 1

  • 数组采用连续的内存地址空间,所以查询的时候,时间复杂度为 O(1)

  • 它是非线程安全的集合


LinkedList


LinkedList 基于双向链表实现的双端队列,它的结构图如下所示。



LinkedList 具有以下特点:



  • LinkedList 是基于双向链表的结构来存储元素,所以长度没有限制,因此不存在扩容机制

  • 由于链表的内存地址是非连续的,所以只能从头部或者尾部查找元素,查询的时间复杂为 O(n),但是 JDK 对 LinkedList 做了查找优化,当我们查找某个元素时,若 index < (size / 2),则从 head 往后查找,否则从 tail 开始往前查找 , 但是我们在计算时间复杂度的时候,常数项可以省略,故时间复杂度 O(n)


Node<E> node(int index) {
// size >> 1 等价于 size / 2
if (index < (size >> 1)) {
// form head to tail
} else {
// form tail to head
}
}


  • 链表通过指针去访问各个元素,所以插入、删除元素只需要更改指针指向即可,因此插入、删除的时间复杂度 O(1)

  • 它是非线程安全的集合


最后汇总一下 ArrayDequeLinkedList 的特点如下所示:































集合类型数据结构初始化及扩容插入/删除时间复杂度查询时间复杂度是否是线程安全
ArrqyDeque循环数组初始化:16
扩容:2 倍
0(n)0(1)
LinkedList双向链表0(1)0(n)

为什么 ArrayDeque 比 LinkedList 快


了解完数据结构特点之后,接下来我们从两个方面分析为什么 ArrayDeque 作为队列使用时可能比 LinkedList 快。




  • 从速度的角度:ArrayDeque 基于数组实现双端队列,而 LinkedList 基于双向链表实现双端队列,数组采用连续的内存地址空间,通过下标索引访问,链表是非连续的内存地址空间,通过指针访问,所以在寻址方面数组的效率高于链表。




  • 从内存的角度:虽然 LinkedList 没有扩容的问题,但是插入元素的时候,需要创建一个 Node 对象, 换句话说每次都要执行 new 操作,当执行 new 操作的时候,其过程是非常慢的,会经历两个过程:类加载过程 、对象创建过程。




    • 类加载过程



      • 会先判断这个类是否已经初始化,如果没有初始化,会执行类的加载过程

      • 类的加载过程:加载、验证、准备、解析、初始化等等阶段,之后会执行 <clinit>() 方法,初始化静态变量,执行静态代码块等等




    • 对象创建过程



      • 如果类已经初始化了,直接执行对象的创建过程

      • 对象的创建过程:在堆内存中开辟一块空间,给开辟空间分配一个地址,之后执行初始化,会执行 <init>() 方法,初始化普通变量,调用普通代码块






接下来我们通过 算法动画图解 | 被 "废弃" 的 Java 栈,为什么还在用 文章中 LeetCode 算法题:有效的括号,来验证它们的执行速度,以及在内存方面的开销,代码如下所示:


class Solution {
public boolean isValid(String s) {

// LinkedList VS ArrayDeque

// Deque<Character> stack = new LinkedList<Character>();
Deque<Character> stack = new ArrayDeque<Character>();

// 开始遍历字符串
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 遇到左括号,则将其对应的右括号压入栈中
if (c == '(') {
stack.push(')');
} else if (c == '[') {
stack.push(']');
} else if (c == '{') {
stack.push('}');
} else {
// 遇到右括号,判断当前元素是否和栈顶元素相等,不相等提前返回,结束循环
if (stack.isEmpty() || stack.poll() != c) {
return false;
}
}
}
// 通过判断栈是否为空,来检查是否是有效的括号
return stack.isEmpty();
}
}

正如你所看到的,核心算法都是一样的,通过接口 Deque 来访问,只是初始化接口 Deque 代码不一样。


// 通过 LinkedList 初始化     
Deque<Character> stack = new LinkedList<Character>();

// 通过 ArrayDeque 初始化
Deque<Character> stack = new ArrayDeque<Character>();


结果如上所示,无论是在执行速度、还是在内存开销上 ArrayDeque 的性能都比 LinkedList 要好。



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

Android -activity的布局加载流程

Activity 布局加载的流程首先在onCreate通过setContentView设置布局protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstan...
继续阅读 »

Activity 布局加载的流程

首先在onCreate通过setContentView设置布局

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

进入Activity中

public void setContentView(@LayoutRes int layoutResID) {
//实际调用的是PhoneWindow.setContentView , PhoneWindow是window的唯一实现类
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public Window getWindow() {
return mWindow;
}

image.png

找到PhoneWindow中的setContentView方法

public void setContentView(int layoutResID) {
//因为我们当前是窗体初始化,所以mContentParent肯定为空
if (mContentParent == null) {
//初始化顶层布局
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//去加载我们自定义的layout
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

PhoneWindow里面有两个比较重要的参数:

DecorView:
这是窗口的顶层试图,它可以包含所有的窗口装饰
ViewGroup:
布局容器。放置窗口内容的视图。要么放置DecorView本生,要么防止内容所在的
DecorView的子级

初始化顶层布局:

private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//初始化 mDecor
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
//初始化mContentParent
mContentParent = generateLayout(mDecor);
....
}
}

进入generateDecor方法其实啥事都没有干只是new了一个DecorView出来


protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}

进入generateLayout方法

protected ViewGroup generateLayout(DecorView decor) {
//做一些窗体样式的判断

//给窗体进行装饰
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
//加载系统布局 判断到底是加载那个布局
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
}

mDecor.startChanging();
//将加载到的基础布局添加到mDecor中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//通过系统的content的资源ID去进行实例化这个控件
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}


}

找一个比较简单的布局screen_simple.xml看一下

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

进入到DecorView里面

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
if (mBackdropFrameRenderer != null) {
loadBackgroundDrawablesIfNeeded();
mBackdropFrameRenderer.onResourcesLoaded(
this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
getCurrentColor(mNavigationColorViewState));
}

mDecorCaptionView = createDecorCaptionView(inflater);
//把传进来的layoutResource布局进行解析并渲染
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {

// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}

加载用户的资源文件


if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//加载用户的xml文件
mLayoutInflater.inflate(layoutResID, mContentParent);
}

进入LayoutInflater类中的inflate方法

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}

调用inflate方法

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: "" + res.getResourceName(resource) + "" ("
+ Integer.toHexString(resource) + ")");
}
//解析xml文件
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
if (root != null && attachToRoot) {
//把自定义的布局解析完之后添加到mContentParent中
root.addView(temp, params);
}

至此Activity加载布局的大致流程已经分析完成。 最后上一下流程图:

Android-activity的UI绘制流程.jpg

收起阅读 »

设计模式-代理模式(Proxy Pattern)

定义为其他对象提供一种代理以控制对这个对象的访问按照代理的创建时期,代理类可以分为两种: 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。动态代理:在程序运行时运用反射机制动态创建而成...
继续阅读 »

定义

为其他对象提供一种代理以控制对这个对象的访问

按照代理的创建时期,代理类可以分为两种: 

  • 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。

  • 动态代理:在程序运行时运用反射机制动态创建而成。

使用场景

主要作用:控制对象访问

  • 扩展目标对象的功能:例如演员(目标对象),有演戏的功能,找一个经纪人(代理),会额外提供收费的功能,实际上是代理的功能,而不是演员的功能。
  • 限制目标对象的功能:例如经纪人对收费不满意,只让演员演一场戏,对演员的功能进行了部分限制。

类图

  • Subject:抽象主题角色,主要是声明代理类和被代理类共同的接口方法
  • RealSubject:具体主题角色(被代理角色),执行具体的业务逻辑
  • Proxy:代理类,持有一个被代理对象的引用,负责在被代理对象方法调用的前后做一些额外操作

4、优点

  • 职责清晰,被代理角色只实现实际的业务逻辑,代理对象实现附加的处理逻辑
  • 扩展性高,可以更换不同的代理类,实现不同的代理逻辑

静态代理

编译时期就已经存在,一般首先需要定义接口,而被代理的对象和代理对象一起实现相同的接口。

1、接口定义:

public interface Play {
//唱歌
void sing(int count);
//演出
void show();
}

2、演员(被代理对象):

public class Actor implements Play {
@Override
public void sing(int count) {
System.out.print("唱了" + count + "首歌");
}

@Override
public void show() {
System.out.print("进行演出");
}
}

被代理对象提供了几个具体方法实现

3、经纪人(代理对象):

public class Agent implements Play {
//被代理对象
private Play player;
private long money;

public void setMoney(long money){
this.money = money;
}

/**
* @param player
* @param money 收费
*/

public Agent(Play player, long money) {
this.player = player;
this.money = money;
}

@Override
public void sing(int count) {
player.sing(count);
}
//控制了被代理对象的访问
@Override
public void show() {
if (money > 100) {
player.show();
} else {
System.out.println("baibai...");
}
}
}

4、使用

public class PlayTest {
public static void main(String[] args){
Actor actor = new Actor();
Agent agent = new Agent(actor, 50);
agent.sing(2);
agent.show();
agent.setMoney(200);
agent.show();
}
}

代理对象通过自身的逻辑处理对目标对象的功能进行控制。

动态代理

动态一般指的是在运行时的状态,是相对编译时的静态来区分,就是在运行时生成一个代理对象帮我们做一些逻辑处理。主要使用反射技术获得类的加载器并且创建实例。
动态代理可以在运行时动态创建一个类,实现一个或多个接口,可以在不修改原有类的基础上动态为通过该类获取的对象添加方法、修改行为。

1、生成动态代理类:

InvocationHandler是动态代理接口,动态代理类需要实现该接口,并在invoke方法中对代理类的方法进行处理

public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

参数说明:

  • Object proxy:被代理的对象
  • Object[] args:要调用的方法
  • Object[] args:方法调用所需要的参数

2、创建动态代理类

Proxy类可以通过newProxyInstance创建一个代理对象

public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)

throws IllegalArgumentException {
if (h == null) {
throw new NullPointerException();
}
Class<?> cl = getProxyClass0(loader, interfaces);
try {
//通过反射完成了代理对象的创建
final Constructor<?> cons = cl.getConstructor(constructorParams);
return newInstance(cons, h);
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString());
}
}

参数说明:

  • ClassLoader loader:类加载器
  • Class<?>[] interfaces:所有的接口
  • InvocationHandler h:实现InvocationHandler接口的子类

3、动态代理demo:

(1)定义动态代理类

public class ActorProxy implements InvocationHandler {
private Play player;

public ActorProxy(Play player) {
this.player = player;
}

/**
* 获取动态代理对象
*/

public Object getDynamicProxy() {
return Proxy.newProxyInstance(player.getClass().getClassLoader(), player.getClass().getInterfaces(), this);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//处理被代理对象的方法实现
if ("show".equals(method.getName())) {
System.out.println("代理处理show....");
return method.invoke(player, null);
} else if ("sing".equals(method.getName())) {
System.out.println("代理处理sing....");
return method.invoke(player, 2);
}
return null;
}
}

代理类实现InvocationHandler接口,在invoke方法中对player(被代理对象)做相应的逻辑处理。

(2)使用

public class ProxyTest {

public static void main(String[] args) {

ActorProxy actorProxy = new ActorProxy(new Actor());
//通过调用Proxy.newProxyInstance方法生成代理对象
Play proxy = (Play) actorProxy.getDynamicProxy();
//调用代理类相关方法
proxy.show();
proxy.sing(3);
}
}

四、Android中的代理模式

Retrofit代理模式

(1)Retrofit使用: 定义接口

public interface MyService {
@GET("users/{user}/list")
Call<String> getMyList(@Path("user") String user);
}

新建retrofit对象,然后产生一个接口对象,然后调用具体方法去完成请求。

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://xxx.com")
.build();
MyService myService = retrofit.create(MyService.class);
Call<String> myList = myService.getMyList("my");

retrofit.create方法就是通过动态代理的方式传入一个接口,返回了一个对象

(2)动态代理分析:

public <T> T create(final Class<T> service) {
//判断是否为接口
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
//创建请求接口的动态代理对象
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
private final Object[] emptyArgs = new Object[0];

@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
//将接口中方法传入返回了ServiceMethod
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}

通过Proxy.newProxyInstance,该动态代理对象可以拿到请求接口实例上所有注解,然后通过代理对象进行网络请求。

收起阅读 »

Compose 仅50行代码轻松定制下滑刷新

目前有一个正在进行的 Jetpack Compose中文手册 项目,旨在帮助开发者更好的理解和掌握Compose框架,目前仍还在开荒中,欢迎大家进行关注与加入! 这篇文章由本人撰写,目前文章已经发布到该手册中,欢迎进行查阅。 下滑刷新效果展...
继续阅读 »

目前有一个正在进行的 Jetpack Compose中文手册 项目,旨在帮助开发者更好的理解和掌握Compose框架,目前仍还在开荒中,欢迎大家进行关注与加入! 这篇文章由本人撰写,目前文章已经发布到该手册中,欢迎进行查阅。


下滑刷新效果展示


像下滑刷新这样涉及到嵌套滑动的手势行为就需要使用 nestedScroll 修饰符来完成。接下来,就让我们先来介绍一下 nestedScroll 修饰符是什么,并且该怎么用。





nestedScroll 修饰符


nestedScroll 修饰符主要用于处理嵌套滑动的场景,为父布局劫持消费子布局滑动手势提供了可能。


使用 nestedScroll 参数列表中有一个必选参数 connection 和一个可选参数 dispatcher


connection: 嵌套滑动手势处理的核心逻辑,内部回调可以在子布局获得滑动事件前预先消费掉部分或全部手势偏移量,也可以获取子布局消费后剩下的手势偏移量。


dispatcher:调度器,内部包含用于父布局的 NestedScrollConnection , 可以调用 dispatch* 方法来通知父布局发生滑动


fun Modifier.nestedScroll(
connection: NestedScrollConnection,
dispatcher: NestedScrollDispatcher? = null
)

NestedScrollConnection


NestedScrollConnection 提供了四个回调方法。


interface NestedScrollConnection {
fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero

fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset = Offset.Zero

suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero

suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
return Velocity.Zero
}
}

onPreScroll


方法描述:预先劫持滑动事件,消费后再交由子布局。


参数列表:



  • available:当前可用的滑动事件偏移量

  • source:滑动事件的类型


返回值:当前组件消费的滑动事件偏移量,如果不想消费可返回Offset.Zero




onPostScroll


方法描述:获取子布局处理后的滑动事件


参数列表:



  • consumed:之前消费的所有滑动事件偏移量

  • available:当前剩下还可用的滑动事件偏移量

  • source:滑动事件的类型


返回值:当前组件消费的滑动事件偏移量,如果不想消费可返回 Offset.Zero ,则剩下偏移量会继续交由当前布局的父布局进行处理




onPreFling


方法描述:获取 Fling 开始时的速度。


参数列表:



  • available:Fling 开始时的速度


返回值:当前组件消费的速度,如果不想消费可返回 Velocity.Zero




onPostFling


方法描述:获取 Fling 结束时的速度信息。


参数列表:




  • consumed:之前消费的所有速度




  • available:当前剩下还可用的速度




返回值:当前组件消费的速度,如果不想消费可返回Velocity.Zero,剩下速度会继续交由当前布局的父布局进行处理。



Fling含义:当我们手指在滑动列表时,如果是快速滑动并抬起,则列表会根据惯性继续飘一段距离后停下,这个行为就是 Fling onPreFling 在你手指刚抬起时便会回调,而 onPostFling 会在飘一段距离停下后回调。



实现下滑刷新


像下滑刷新这样涉及到嵌套滑动的手势行为就可以使用 nestedScroll 修饰符来完成。


示例介绍


在这个示例中存在着加载动画和列表数据。当我们手指向下滑时,此时如果列表顶部没有数据则会逐渐出现加载动画。与之相反,当我们手指向上滑时,此时如果加载动画还在,则加载动画逐渐向上消失,直到加载动画完全消失后,列表才会被向下滑动。


设计实现方案


为实现这个滑动刷新的需求,我们可以设计如下方案。我们首先需要将加载动画和列表数据放到一个父布局中统一管理。




  1. 当我们手指向下滑时,我们希望滑动手势首先交给子布局中的列表进行处理,如果列表已经滑到顶部说明此时滑动手势事件没有被消费,此时再交由父布局进行消费。父布局可以消费列表消费剩下的滑动手势事件(为加载动画增加偏移)。




  2. 当我们手指向上滑时,我们希望滑动手势首先被父布局消费(为加载动画减小偏移),如果加载动画本身仍未出现时,则不进行消费。然后将剩下的滑动手势交给子布局列表进行消费。




NestedScrollConnection 实现


使用 nestedScroll 修饰符最重要的就是根据自己的业务场景来定制 NestedScrollConnection 的实现,接下来我们就逐个分析 NestedScrollConnection 重的借口该如何进行实现。


实现 onPostScroll


向我们之前设计的实现方案一样,当我们手指向下滑时,我们希望滑动手势首先交给子布局中的列表进行处理,如果列表已经滑到顶部说明此时滑动手势事件没有被消费,此时再交由父布局进行消费。 onPostScroll 回调时机是符合我们的需求的。


我们首先需要判断该滑动事件是不是拖动事件,通过 available.y > 0 判断是否是下滑手势,如果都没问题时,通知加载动画增加偏移量。返回值 Offset(x = 0f, y = available.y) 意味着将剩下的所有偏移量全部消费调,不再向外层父布局继续传播了。


override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
if (source == NestedScrollSource.Drag && available.y > 0) {
state.updateOffsetDelta(available.y)
return Offset(x = 0f, y = available.y)
} else {
return Offset.Zero
}
}

实现 onPreScroll


与上面相反,此时我们希望下滑收回加载动画,当我们手指向上滑时,我们希望滑动手势首先被父布局消费(为加载动画减小偏移),如果加载动画本身仍未出现时,则不进行消费。然后将剩下的滑动手势交给子布局列表进行消费。onPreScroll 回调时机是符合这个需求的。


我们首先需要判断该滑动事件是不是拖动事件,通过 available.y < 0 判断是否是上滑手势。此时可能加载动画本身未出现,所以需要额外进行判断。如果未出现则返回 Offset.Zero 不消费,如果出现了则返回 Offset(x = 0f, y = available.y) 进行消费。


override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (source == NestedScrollSource.Drag && available.y < 0) {
state.updateOffsetDelta(available.y)
return if (state.isSwipeInProgress) Offset(x = 0f, y = available.y) else Offset.Zero
} else {
return Offset.Zero
}
}

实现 onPreFling


接下来,我们需要一个松手时的吸附效果。如果拉过加载动画高度的一般则进行加载,否则就收缩回初始状态。前问我提到了 onPreFling 在松手时回调,即符合我们当前这个的场景。



即使松手时速度很慢或静止,onPreFlingonPostFling都会回调,只是速度数值很小。



这里我们只需要吸引效果,并不希望消费速度,所以返回 Velocity.Zero 即可


override suspend fun onPreFling(available: Velocity): Velocity {
if (state.indicatorOffset > height / 2) {
state.animateToOffset(height)
state.isRefreshing = true
} else {
state.animateToOffset(0.dp)
}
return Velocity.Zero
}

实现 onPreFling


由于我们的下滑刷新手势处理不涉及 onPreFling 回调时机,所以不进行额外的实现。


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

Flutter ListView懒加载(滑动不加载,停止滑动加载)

前言:为了更好的减小网络的带宽,使得列表更加流畅,我们需要了解懒加载,也称延迟加载。 面试真题:flutter如何实现懒加载? 关于上一章的登录界面,各位属实难为我了,我也在求ui小姐姐,各位点点赞给我点动力吧~ 懒加载也叫延迟加载,指的是在长网页中延迟...
继续阅读 »

前言:为了更好的减小网络的带宽,使得列表更加流畅,我们需要了解懒加载,也称延迟加载。 面试真题:flutter如何实现懒加载?


关于上一章的登录界面,各位属实难为我了,我也在求ui小姐姐,各位点点赞给我点动力吧~


5e3c9f11dc5c8d53c46907cf16e2b5e.jpg
ca.png


image.png


懒加载也叫延迟加载,指的是在长网页中延迟加载图像,是一种很好优化网页性能的方式。用户滚动到它们之前,可视区域外的图像不会加载。这与图像预加载相反,在长网页上使用延迟加载将使网页加载更快。在某些情况下,它还可以帮助减少服务器负载。常适用图片很多,页面很长的电商网站场景中。


对ListView优化就那么几点:(后面都会写出来):


1.Flutter ListView加载图片优化(懒加载)


2.Flutter ListView加载时使用图片使用缩略图,对图片进行缓存


3.Flutter 减少build()的耗时


本章,我们会实现wechat朋友圈的优化功能,即当页面在滑动时不加载图片,在界面停止滑动时加载图片。

效果图:

tt0.top-288153.gif


1.了解widget通知监听:NotificationListener


NotificationListener属性:




  • child:widget



  • onNotification:NotificationListenerCallback<Notification>

    返回值true表示消费掉当前通知不再向上一级NotificationListener传递通知,false则会再向上一级NotificationListener传递通知;这里需要注意的是通知是由下而上去传递的,所以才会称作冒泡通知!




2.需要一个bool来控制是否加载


///加载图片的标识
bool isLoadingImage = true;

3.编写传递通知的方法,使其作用于NotificationListener


bool notificationFunction(Notification notification) {
 ///通知类型
 switch (notification.runtimeType) {
   case ScrollStartNotification:
     print("开始滚动");

     ///在这里更新标识 刷新页面 不加载图片
     isLoadingImage = false;
     break;
   case ScrollUpdateNotification:
     print("正在滚动");
     break;
   case ScrollEndNotification:
     print("滚动停止");

     ///在这里更新标识 刷新页面 加载图片
     setState(() {
       isLoadingImage = true;
    });
     break;
   case OverscrollNotification:
     print("滚动到边界");
     break;
}
 return true;
}

4.根据bool值加载不同的组件


ListView buildListView() {
 return ListView.separated(
   itemCount: 1000, //子条目个数
   ///构建每个条目
   itemBuilder: (BuildContext context, int index) {
     if (isLoadingImage) {
       ///这时将子条目单独封装在了一个StatefulWidget中
       return Image.network(
         netImageUrl,
         width: 100,
         height: 100,
         fit: BoxFit.fitHeight,
      );
    } else {
       return Container(
         height: 100,
         width: 100,
         child: Text("加载中..."),
      ); //占位
    }
  },

   ///构建每个子Item之间的间隔Widget
   separatorBuilder: (BuildContext context, int index) {
     return new Divider();
  },
);
}

完整代码:


class ScrollHomePageState extends State {
 ///加载图片的标识
 bool isLoadingImage = true;

 ///网络图片地址
 String netImageUrl =
     "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0a4ce25d48b8405cbf5444b6195928d4~tplv-k3u1fbpfcp-no-mark:0:0:0:0.awebp";

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: new AppBar(
       title: Text("详情"),
    ),
     ///列表
     body: NotificationListener(
       ///子Widget中的滚动组件滑动时就会分发滚动通知
       child: buildListView(),
       ///每当有滑动通知时就会回调此方法
       onNotification: notificationFunction,
    ),
  );
}

 bool notificationFunction(Notification notification) {
   ///通知类型
   switch (notification.runtimeType) {
     case ScrollStartNotification:
       print("开始滚动");

       ///在这里更新标识 刷新页面 不加载图片
       isLoadingImage = false;
       break;
     case ScrollUpdateNotification:
       print("正在滚动");
       break;
     case ScrollEndNotification:
       print("滚动停止");

       ///在这里更新标识 刷新页面 加载图片
       setState(() {
         isLoadingImage = true;
      });
       break;
     case OverscrollNotification:
       print("滚动到边界");
       break;
  }
   return true;
}

 ListView buildListView() {
   return ListView.separated(
     itemCount: 1000, //子条目个数
     ///构建每个条目
     itemBuilder: (BuildContext context, int index) {
       if (isLoadingImage) {
         ///这时将子条目单独封装在了一个StatefulWidget中
         return Image.network(
           netImageUrl,
           width: 100,
           height: 100,
           fit: BoxFit.fitHeight,
        );
      } else {
         return Container(
           height: 100,
           width: 100,
           child: Text("加载中..."),
        ); //占位
      }
    },

     ///构建每个子Item之间的间隔Widget
     separatorBuilder: (BuildContext context, int index) {
       return new Divider();
    },
  );
}
}

是不是很简单,但是懒加载确实是面试真题,你了解了吗?


ad.png


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

为什么 Compose 没有布局嵌套问题?

前言 做过布局性能优化的同学都知道,为了优化界面加载速度,要尽可能的减少布局的层级。这主要是因为布局层级的增加,可能会导致测量时间呈指数级增长。 而Compose却没有这个问题,它从根本上解决了布局层级对布局性能的影响: Compose界面只允许一次测量。这意...
继续阅读 »

前言


做过布局性能优化的同学都知道,为了优化界面加载速度,要尽可能的减少布局的层级。这主要是因为布局层级的增加,可能会导致测量时间呈指数级增长。

Compose却没有这个问题,它从根本上解决了布局层级对布局性能的影响: Compose界面只允许一次测量。这意味着随着布局层级的加深,测量时间也只是线性增长的.

下面我们就一起来看看Compose到底是怎么只测量一次就把活给干了的,本文主要包括以下内容:



  1. 布局层级过深为什么影响性能?

  2. Compose为什么没有布局嵌套问题?

  3. Compose测量过程源码分析


1. 布局层级过深为什么影响性能?


我们总说布局层级过深会影响性能,那么到底是怎么影响的呢?主要是因为在某些情况下ViewGroup会对子View进行多次测量

举个例子


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">

<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@android:color/holo_red_dark" />

<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/black" />
</LinearLayout>


  1. LinearLayout宽度为wrap_content,因此它将选择子View的最大宽度为其最后的宽度

  2. 但是有个子View的宽度为match_parent,意思它将以LinearLayout的宽度为宽度,这就陷入死循环了

  3. 因此这时候, LinearLayout 就会先以0为强制宽度测量一下子View,并正常地测量剩下的其他子View,然后再用其他子View里最宽的那个的宽度,二次测量这个match_parent的子 View,最终得出它的尺寸,并把这个宽度作为自己最终的宽度。

  4. 这是对单个子View的二次测量,如果有多个子View写了match_parent ,那就需要对它们每一个都进行二次测量。

  5. 除此之外,如果在LinearLayout中使用了weight会导致测量3次甚至更多,重复测量在Android中是很常见的


上面介绍了为什么会出现重复测量,那么会有什么影响呢?不过是多测量了几次,会对性能有什么大的影响吗?

之所以需要避免布局层级过深是因为它对性能的影响是指数级的



  1. 如果我们的布局有两层,其中父View会对每个子View做二次测量,那它的每个子View一共需要被测量 2 次

  2. 如果增加到三层,并且每个父View依然都做二次测量,这时候最下面的子View被测量的次数就直接翻倍了,变成 4 次

  3. 同理,增加到 4 层的话会再次翻倍,子 View 需要被测量 8 次



也就是说,对于会做二次测量的系统,层级加深对测量时间的影响是指数级的,这就是Android官方文档建议我们减少布局层级的原因


2. Compose为什么没有布局嵌套问题?


我们知道,Compose只允许测量一次,不允许重复测量。

如果每个父组件对每个子组件只测量一次,那就直接意味着界面中的每个组件只会被测量一次



这样即使布局层级加深,测量时间却没有增加,把组件加载的时间复杂度从O(2ⁿ) 降到了 O(n)


那么问题就来了,上面我们已经知道,多次测量有时是必要的,但是为什么Compose不需要呢?

Compose中引入了固有特性测量(Intrinsic Measurement)


固有特性测量即Compose允许父组件在对子组件进行测量之前,先测量一下子组件的「固有尺寸」

我们上面说的,ViewGroup的二次测量,也是先进行这种「粗略测量」再进行最终的「正式测量」,使用固有特性测量可以产生同样的效果


而使用固有特性测量之所以有性能优势,主要是因为其不会随着层级的加深而加倍,固有特性测量也只进行一次

Compose会先对整个组件树进行一次Intrinsic测量,然后再对整体进行正式的测量。这样开辟两个平行的测量过程,就可以避免因为层级增加而对同一个子组件反复测量所导致的测量时间的不断加倍了。



总结成一句话就是,在Compose里疯狂嵌套地写界面,和把所有组件全都写进同一层里面,性能是一样的!所以Compose没有布局嵌套问题


2.1 固有特性测量使用


假设我们需要创建一个可组合项,该可组合项在屏幕上显示两个用分隔线隔开的文本,如下所示:


p14.png


为了实现分隔线与最高的文本一样高,我们可以怎么做呢?


@Composable
fun TwoTexts(
text1: String,
text2: String,
modifier: Modifier = Modifier
) {
Row(modifier = modifier.height(IntrinsicSize.Min)) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp)
.wrapContentWidth(Alignment.Start),
text = text1
)
Divider(
color = Color.Black,
modifier = Modifier
.fillMaxHeight()
.width(1.dp)
)
Text(
modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
.wrapContentWidth(Alignment.End),
text = text2
)
}
}

注意,这里给Rowheight设置为了IntrinsicSize.Min,IntrinsicSize.Min会递归查询它子项的最小高度,其中两个Text的最小高度即文本的宽度,而Divider的最小高度为0
因此最后Row的高度即为最长的文本的高度,而Divider的高度为fillMaxHeight,也就跟最高的文本一样高了

如果我们这里不设置高度为IntrinsicSize.Min的话,Divider的高度是占满屏幕的,如下所示


p13.png


3. Compose测量过程源码分析


上面我们介绍了固有特性测量是什么,及固有特性测量的使用,下面我们来看看Compose的测量究竟是怎么实现的


3.1 测量入口


我们知道,在Compose中自定义Layout是通过Layout方法实现的


@Composable inline fun Layout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
)

主要传入3个参数



  1. content:自定义布局的子项,我们后续需要对它们测量和定位

  2. modifier: 对Layout添加的一些修饰modifier

  3. measurePolicy: 即测量规则,这个是我们主要需要处理的地方


measurePolicy中主要有五个接口


fun interface MeasurePolicy {
fun MeasureScope.measure(measurables: List<Measurable>,constraints: Constraints): MeasureResult

fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>,height: Int): Int

fun IntrinsicMeasureScope.minIntrinsicHeight(measurables: List<IntrinsicMeasurable>,width: Int): Int

fun IntrinsicMeasureScope.maxIntrinsicWidth(measurables: List<IntrinsicMeasurable>,height: Int): Int

fun IntrinsicMeasureScope.maxIntrinsicHeight(measurables: List<IntrinsicMeasurable>,width: Int): Int
}

可以看出:



  1. 使用固有特性测量的时候,会调用对应的IntrinsicMeasureScope方法,如使用Modifier.height(IntrinsicSize.Min),就会调用minIntrinsicHeight方法

  2. 父项测量子项时,就是在MeasureScope.measure方法中调用measure.meausre(constraints),但是具体是怎么实现的呢?我们来看个例子


@Composable
fun MeasureTest() {
Row() {
Layout(content = { }, measurePolicy = { measurables, constraints ->
measurables.forEach {
it.measure(constraints)
}
layout(100, 100) {

}
})
}
}

一个简单的例子,我们在measure方法中打个断点,如下图所示:



  1. 如下图所示,是由RowMeasurePolicy中开始测量子项,rowColumnMeasurePolicy我们定义为ParentPolicy

  2. 然后调用到LayoutNode,OuterMeasurablePlaceable,InnerPlaceablemeasure方法

  3. 最后再由InnerPlaceable中调用到子项的MeasurePolicy,即我们自定义Layout实现的部分,我们定义它为ChildPolicy

  4. 子项中也可能会测量它的子项,在这种情况下它就变成了一个ParentPolicy,然后继续后续的测量



综上所述,父项在测量子项时,子项的测量入口就是LayoutNode.measure,然后经过一系列调用到子项自己的MeasurePolicy,也就是我们自定义Layout中自定义的部分


3.2 LayoutNodeWrapper链构建


上面我们说了,测量入口是LayoutNode,后续还要经过OuterMeasurablePlaceable,InnerPlaceablemeasure方法,那么问题来了,这些东西是怎么来的呢?

首先给出结论



  1. 子项都是以LayoutNode的形式,存在于Parentchildren中的

  2. Layout的设置的modifier会以LayoutNodeWrapper链的形式存储在LayoutNode中,然后后续做相应变换


由于篇幅原因,关于第一点就不在这里详述了,有兴趣的同学可以参考:Jetpack Compose 测量流程源码分析

我们这里主要看下LayoutNodeWrapper链是怎么构建的


  internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)
private val outerMeasurablePlaceable = OuterMeasurablePlaceable(this, innerLayoutNodeWrapper)
override fun measure(constraints: Constraints) = outerMeasurablePlaceable.measure(constraints)
override var modifier: Modifier = Modifier
set(value) {
// …… code
field = value
// …… code


// 创建新的 LayoutNodeWrappers 链
// foldOut 相当于遍历 modifier
val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod /*📍 modifier*/ , toWrap ->
var wrapper = toWrap
if (mod is OnGloballyPositionedModifier) {
onPositionedCallbacks += mod
}
if (mod is RemeasurementModifier) {
mod.onRemeasurementAvailable(this)
}

val delegate = reuseLayoutNodeWrapper(mod, toWrap)
if (delegate != null) {
wrapper = delegate
} else {
// …… 省略了一些 Modifier判断
if (mod is KeyInputModifier) {
wrapper = ModifiedKeyInputNode(wrapper, mod).assignChained(toWrap)
}
if (mod is PointerInputModifier) {
wrapper = PointerInputDelegatingWrapper(wrapper, mod).assignChained(toWrap)
}
if (mod is NestedScrollModifier) {
wrapper = NestedScrollDelegatingWrapper(wrapper, mod).assignChained(toWrap)
}
// 布局相关的 Modifier
if (mod is LayoutModifier) {
wrapper = ModifiedLayoutNode(wrapper, mod).assignChained(toWrap)
}
if (mod is ParentDataModifier) {
wrapper = ModifiedParentDataNode(wrapper, mod).assignChained(toWrap)
}

}
wrapper
}

outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
outerMeasurablePlaceable.outerWrapper = outerWrapper

……
}

如上所示:



  1. 默认的LayoutNodeWrapper链即由LayoutNode , OuterMeasurablePlaceable, InnerPlaceable 组成

  2. 当添加了modifier时,LayoutNodeWrapper链会更新,modifier会作为一个结点插入到其中


举个例子,如果我们给Layout设置一些modifier:


Modifier.size(100.dp).padding(10.dp).background(Color.Blue)

那么对应的LayoutNodeWrapper链如下图所示



这样一个接一个链式调用下一个的measure,直到最后一个结点InnerPlaceable

那么InnerPlaceable又会调用到哪儿呢?



InnerPlaceable最终调用到了我们自定义Layout时写的measure方法


3.3 固有特性测量是怎样实现的?


上面我们介绍了固有特性测量的使用,也介绍了LayoutNodeWrapper链的构建,那么固有特性测量是怎么实现的呢?

其实固有特性测量就是往LayoutNodeWrapper链中插入了一个Modifier


@Stable
fun Modifier.height(intrinsicSize: IntrinsicSize) = when (intrinsicSize) {
IntrinsicSize.Min -> this.then(MinIntrinsicHeightModifier)
IntrinsicSize.Max -> this.then(MaxIntrinsicHeightModifier)
}

private object MinIntrinsicHeightModifier : IntrinsicSizeModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
//正式测量前先根据固有特性测量获得一个约束
val contentConstraints = calculateContentConstraints(measurable, constraints)
//正式测量
val placeable = measurable.measure(
if (enforceIncoming) constraints.constrain(contentConstraints) else contentConstraints
)
return layout(placeable.width, placeable.height) {
placeable.placeRelative(IntOffset.Zero)
}
}

override fun MeasureScope.calculateContentConstraints(
measurable: Measurable,
constraints: Constraints
): Constraints {
val height = measurable.minIntrinsicHeight(constraints.maxWidth)
return Constraints.fixedHeight(height)
}

override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
) = measurable.minIntrinsicHeight(width)
}

如上所示:



  1. IntrinsicSize.Min其实也是个Modifier

  2. MinIntrinsicHeightModifier会在测量之间,先调用calculateContentConstraints计算约束

  3. calculateContentConstraints中则会递归地调用子项的minIntrinsicHeight,并找出最大值,这样父项的高度就确定了

  4. 固有特性测量完成后,再调用measurable.measure,开始真正的递归测量


3.4 测量过程小结


准备阶段

子项在声明时,会生成LayoutNode添加到父项的chindredn中,同时子项的modifier也将构建成LayoutNodeWrapper链,保存在LayoutNode

值得注意的是,如果使用了固有特性测量,将会添加一个IntrinsicSizeModifierLayoutNodeWrapper链中


测量阶段

父容器在其测量策略MeasurePolicymeasure函数中会执行childmeasure函数。

childmeasure方法按照构建好的LayoutNodeWrapper链一步步的执行各个节点的measure函数,最终走到InnerPlaceablemeasure函数,在这里又会继续它的children进行测量,此时它的children 就会和它一样进行执行上述流程,一直到所有children测量完成。


用下面这张图总结一下上述流程。


总结


本文主要介绍了以下内容



  1. Android中布局层级过深为什么会对性能有影响?

  2. Compose中为什么没有布局嵌套问题?

  3. 什么是固有特性测量及固有特性测量的使用

  4. Compose测量过程源码分析及固有特性测量到底是怎样实现的?


如果本文对你有所帮助,欢迎点赞收藏~


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

Android-activity的启动流程

需要结合Application的启动流程。 juejin.cn/post/701209…//查看栈顶可见activity是否正等待 if (normalMode) { try { if (mStackSupervisor.at...
继续阅读 »


需要结合Application的启动流程。 juejin.cn/post/701209…

//查看栈顶可见activity是否正等待
if (normalMode) {
try {
if (mStackSupervisor.attachApplicationLocked(app)) {
didSomething = true;
}
} catch (Exception e) {
Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
badApp = true;
}
}

进入ActivityStackSupervisor中的

boolean attachApplicationLocked(ProcessRecord app) throws RemoteException {
final String processName = app.processName;
boolean didSomething = false;
for (int displayNdx = mActivityDisplays.size() - 1; displayNdx >= 0; --displayNdx) {
final ActivityDisplay display = mActivityDisplays.valueAt(displayNdx);
for (int stackNdx = display.getChildCount() - 1; stackNdx >= 0; --stackNdx) {
final ActivityStack stack = display.getChildAt(stackNdx);
if (!isFocusedStack(stack)) {
continue;
}
//从activityStack(Activity栈)把所有的activity添加给mTmpActivityList
stack.getAllRunningVisibleActivitiesLocked(mTmpActivityList);
//返回当前应用最顶端的activity
final ActivityRecord top = stack.topRunningActivityLocked();
final int size = mTmpActivityList.size();
//遍历所有的activity
for (int i = 0; i < size; i++) {
final ActivityRecord activity = mTmpActivityList.get(i);
f (activity.app == null && app.uid == activity.info.applicationInfo.uid
&& processName.equals(activity.processName)) {
try {
if (realStartActivityLocked(activity, app,
top == activity /* andResume */, true /* checkConfig */)) {
didSomething = true;
}
} catch (RemoteException e) {

throw e;
}
}
}
}
}
if (!didSomething) {
ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
}
return didSomething;
}

进入realStartActivityLocked方法

final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,
boolean andResume, boolean checkConfig) throws RemoteException {
...
//创建activity启动事务
final ClientTransaction clientTransaction = ClientTransaction.obtain(app.thread,
r.appToken);
//添加回调
clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
System.identityHashCode(r), r.info,
mergedConfiguration.getGlobalConfiguration(),
mergedConfiguration.getOverrideConfiguration(), r.compat,
r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle,
r.persistentState, results, newIntents, mService.isNextTransitionForward(),
profilerInfo));

final ActivityLifecycleItem lifecycleItem;
if (andResume) {
lifecycleItem = ResumeActivityItem.obtain(mService.isNextTransitionForward());
} else {
lifecycleItem = PauseActivityItem.obtain();
}
clientTransaction.setLifecycleStateRequest(lifecycleItem);
//提交事务
mService.getLifecycleManager().scheduleTransaction(clientTransaction);
...
}

进入ClientLifecycleManager


void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
final IApplicationThread client = transaction.getClient();
transaction.schedule();
if (!(client instanceof Binder)) {
// If client is not an instance of Binder - it's a remote call and at this point it is
// safe to recycle the object. All objects used for local calls will be recycled after
// the transaction is executed on client in ActivityThread.
transaction.recycle();
}
}

进入ClientTransaction这个类的schedule方法

public void schedule() throws RemoteException {
mClient.scheduleTransaction(this);
}

mClient其实就是IApplicationThread

public static ClientTransaction obtain(IApplicationThread client, IBinder activityToken) {
ClientTransaction instance = ObjectPool.obtain(ClientTransaction.class);
if (instance == null) {
instance = new ClientTransaction();
}
instance.mClient = client;
instance.mActivityToken = activityToken;

return instance;
}

回到ActivityThread中的scheduleTransaction方法中

public void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
ActivityThread.this.scheduleTransaction(transaction);
}

进入activitythread父类ClientTransactionHandler中

void scheduleTransaction(ClientTransaction transaction) {
transaction.preExecute(this);
sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);
}

回到activitythread中handlerMessage中处理

case EXECUTE_TRANSACTION:
final ClientTransaction transaction = (ClientTransaction) msg.obj;
mTransactionExecutor.execute(transaction);
if (isSystem()) {
// Client transactions inside system process are recycled on the client side
// instead of ClientLifecycleManager to avoid being cleared before this
// message is handled.
transaction.recycle();
}
// TODO(lifecycler): Recycle locally scheduled transactions.
break;

进入TransactionExecutor类中执行execute方法

public void execute(ClientTransaction transaction) {
final IBinder token = transaction.getActivityToken();
log("Start resolving transaction for client: " + mTransactionHandler + ", token: " + token);

executeCallbacks(transaction);

executeLifecycleState(transaction);
mPendingActions.clear();
log("End resolving transaction");
}

到executeCallbacks方法中

public void executeCallbacks(ClientTransaction transaction) {
final List<ClientTransactionItem> callbacks = transaction.getCallbacks();
if (callbacks == null) {
// No callbacks to execute, return early.
return;
}
log("Resolving callbacks");

final IBinder token = transaction.getActivityToken();
ActivityClientRecord r = mTransactionHandler.getActivityClient(token);

// In case when post-execution state of the last callback matches the final state requested
// for the activity in this transaction, we won't do the last transition here and do it when
// moving to final state instead (because it may contain additional parameters from server).
final ActivityLifecycleItem finalStateRequest = transaction.getLifecycleStateRequest();
final int finalState = finalStateRequest != null ? finalStateRequest.getTargetState()
: UNDEFINED;
// Index of the last callback that requests some post-execution state.
final int lastCallbackRequestingState = lastCallbackRequestingState(transaction);
//遍历事务管理器中的所有窗体请求对象
final int size = callbacks.size();
for (int i = 0; i < size; ++i) {
//获得的是LaunchActivityItem
final ClientTransactionItem item = callbacks.get(i);
log("Resolving callback: " + item);
final int postExecutionState = item.getPostExecutionState();
final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
item.getPostExecutionState());
if (closestPreExecutionState != UNDEFINED) {
cycleToPath(r, closestPreExecutionState);
}
//进行窗体创建请求
item.execute(mTransactionHandler, token, mPendingActions);
item.postExecute(mTransactionHandler, token, mPendingActions);
if (r == null) {
// Launch activity request will create an activity record.
r = mTransactionHandler.getActivityClient(token);
}

if (postExecutionState != UNDEFINED && r != null) {
// Skip the very last transition and perform it by explicit state request instead.
final boolean shouldExcludeLastTransition =
i == lastCallbackRequestingState && finalState == postExecutionState;
cycleToPath(r, postExecutionState, shouldExcludeLastTransition);
}
}
}

进入到LaunchActivityItem中的execute方法

@Override
public void execute(ClientTransactionHandler client, IBinder token,
PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
//创建一个ActivityClientRecord对象,用于activity的实例化
ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
mPendingResults, mPendingNewIntents, mIsForward,
mProfilerInfo, client);
//回调给activityThread
client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}

回到activityThread类中的handleLaunchActivity方法

public Activity handleLaunchActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions, Intent customIntent) {
...
//根据传递过来的activityClientRecord创建一个activity
final Activity a = performLaunchActivity(r, customIntent);
...
}

进入performLaunchActivity方法

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
try {
//通过反射创建activity对象
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
...
}

进入Instrumentation类中的newActivity方法

public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
String pkg = intent != null && intent.getComponent() != null
? intent.getComponent().getPackageName() : null;
return getFactory(pkg).instantiateActivity(cl, className, intent);
}

进入AppComponentFactory类中

public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className,
@Nullable Intent intent)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return (Activity) cl.loadClass(className).newInstance();
}

到此activity就被创建出来了。 继续回到activityThread类中

activity.mCalled = false;
if (r.isPersistable()) {
//通过mInstrumentation调用activity的生命周期方法
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}

进入Instrumentation类中的callActivityOnCreate方法

public void callActivityOnCreate(Activity activity, Bundle icicle,
PersistableBundle persistentState) {
prePerformCreate(activity);
activity.performCreate(icicle, persistentState);
postPerformCreate(activity);
}

进入activity中的performCreate方法中

final void performCreate(Bundle icicle, PersistableBundle persistentState) {
mCanEnterPictureInPicture = true;
restoreHasCurrentPermissionRequest(icicle);
if (persistentState != null) {
onCreate(icicle, persistentState);
} else {
onCreate(icicle);
}
writeEventLog(LOG_AM_ON_CREATE_CALLED, "performCreate");
mActivityTransitionState.readState(icicle);

mVisibleFromClient = !mWindow.getWindowStyle().getBoolean(
com.android.internal.R.styleable.Window_windowNoDisplay, false);
mFragments.dispatchActivityCreated();
mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions());


收起阅读 »

Kotlin系列三:空指针检查

Android系统上崩溃率最高的异常类型就是空指针异常(NullPointerException)。public void doStudy(Study study) { if (study != null) { study.readBo...
继续阅读 »

Android系统上崩溃率最高的异常类型就是空指针异常(NullPointerException)。

public void doStudy(Study study) {
if (study != null) {
study.readBooks();
study.doHomework();
}
}

这种java里常见的判空检查容易陷入判空地狱的灾难。Kotlin提供了很好的解决思路。

1 可空类型(?)

Kotlin在编译时就进行判空检查,这会导致代码变得相对难写些,因为你得实时考虑到对象的为空与否。

一个判空举例:

fun doStudy(study: Study) {
study.readBooks()
study.doHomework()
}

如果你尝试向doStudy()函数传入一个null参数,则会提示错误:

那么可为空的类型系统是什么样的呢?很简单,就是在类名的后面加上一个问号:

为什么会出现红色报错:由于我们将参数改成了可为空的Study?类型,此时调用参数的readBooks()和doHomework()方法都可能造成空指针异常过。如何解决呢:

fun doStudy(study: Study?) {
if (study != null) {
study.readBooks()
study.doHomework()
}
}

2 判空辅助工具

2.1 ?.操作符

?.操作符:当对象不为空时正常调用相应的方法,当对象为空时则什么都不做(相当于外部包裹了 !=null 的一个判断了):

fun doStudy(study: Study?) {
study?.readBooks()
study?.doHomework()
}

2.1 ?:操作符

?:操作符:操作符的左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果。

val c = if (a ! = null) {
a
} else {
b
}

这段代码的逻辑使用?:操作符就可以简化成:

val c = a ?: b

比如现在我们要编写一个函数用来获得一段文本的长度,使用传统的写法就可以这样写:

fun getTextLength(text: String?): Int {
if (text != null) {
return text.length
}
return 0
}

改进:

fun getTextLength(text: String?) = text?.length ?: 0

2.2 !!操作符

不过Kotlin的空指针检查机制也并非总是那么智能,有的时候我们可能从逻辑上已经将空指针异常处理了,但是Kotlin的编译器并不知道,这个时候它还是会编译失败。

观察如下的代码示例:

var content: String? = "hello"

fun main() {
if (content != null) {
printUpperCase()
}
}

fun printUpperCase() {
val upperCase = content.toUpperCase()
println(upperCase)
}

看上去好像逻辑没什么问题,但这段代码一定是无法运行的。因为printUpperCase()函数并不知道外部已经对content变量进行了非空检查,在调用toUpperCase()方法时,还认为这里存在空指针风险,从而无法编译通过。在这种情况下,如果我们想要强行通过编译,可以使用非空断言工具,写法是在对象的后面加上!!,如下所示:

fun printUpperCase() {
val upperCase = content!!.toUpperCase()
println(upperCase)
}

这种写法意在告诉Kotlin,我非常确信这里的对象不会为空,所以不用你来帮我做空指针检查了,如果出现问题,你可以直接抛出空指针异常,后果由我自己承担。

2.3 let函数

let函数属于Kotlin中的标准函数,这个函数提供了函数式API的编程接口,并将原始调用对象作为参数传递到Lambda表达式中。示例代码如下:

obj.let { obj2 ->
// 编写具体的业务逻辑
}
结合doStudy()函数:

fun doStudy(study: Study?) {
study?.readBooks()
study?.doHomework()
}

虽然这段代码我们通过?.操作符优化之后可以正常编译通过,但其实这种表达方式是有点啰嗦的,如果将这段代码准确翻译成使用if判断语句的写法,对应的代码如下:

fun doStudy(study: Study?) {
if (study != null) {
study.readBooks()
}
if (study != null) {
study.doHomework()
}
}

也就是说,本来我们进行一次if判断就能随意调用study对象的任何方法,但受制于?.操作符的限制,现在变成了每次调用study对象的方法时都要进行一次if判断。

这个时候就可以结合使用?.操作符和let函数来对代码进行优化了,如下所示:

fun doStudy(study: Study?) {
study?.let { stu ->
stu.readBooks()
stu.doHomework()
}
}

我来简单解释一下上述代码,?.操作符表示对象为空时什么都不做,对象不为空时就调用let函数,而let函数会将study对象本身作为参数传递到Lambda表达式中,此时的study对象肯定不为空了,我们就能放心地调用它的任意方法了。

另外还记得Lambda表达式的语法特性吗?当Lambda表达式的参数列表中只有一个参数时,可以不用声明参数名,直接使用it关键字来代替即可,那么代码就可以进一步简化成:

fun doStudy(study: Study?) {
study?.let {
it.readBooks()
it.doHomework()
}
}
收起阅读 »

Flutter 入门与实战(八十):使用GetX构建更优雅的页面结构

前言 App 的大部分页面都会涉及到数据加载、错误、无数据和正常几个状态,在一开始的时候我们可能数据获取的状态枚举用 if...else 或者 switch 来显示不同的 Widget,这种方式会显得代码很丑陋,譬如下面这样的代码: if (PersonalC...
继续阅读 »

前言


App 的大部分页面都会涉及到数据加载、错误、无数据和正常几个状态,在一开始的时候我们可能数据获取的状态枚举用 if...else 或者 switch 来显示不同的 Widget,这种方式会显得代码很丑陋,譬如下面这样的代码:


if (PersonalController.to.loadingStatus == LoadingStatus.loading) {
return Center(
child: Text('加载中...'),
);
}
if (PersonalController.to.loadingStatus == LoadingStatus.failed) {
return Center(
child: Text('请求失败'),
);
}
// 正常状态
PersonalEntity personalProfile = PersonalController.to.personalProfile;
return Stack(
...
);

这种情况实在是不够优雅,在 GetX 中提供了一种 StateMixin 的方式来解决这个问题。


StateMixin


StateMixin 是 GetX 定义的一个 mixin,可以在状态数据中混入页面数据加载状态,包括了如下状态:



  • RxStatus.loading():加载中;

  • RxStatus.success():加载成功;

  • RxStatus.error([String? message]):加载失败,可以携带一个错误信息 message

  • RxStatus.empty():无数据。


StateMixin 的用法如下:


class XXXController extends GetxController
with StateMixin<T> {
}

其中 T 为实际的状态类,比如我们之前一篇 PersonalEntity,可以定义为:


class PersonalMixinController extends GetxController
with StateMixin<PersonalEntity> {
}

然后StateMixin 提供了一个 change 方法用于传递状态数据和状态给页面。


void change(T? newState, {RxStatus? status})

其中 newState 是新的状态数据,status 就是上面我们说的4种状态。这个方法会通知 Widget 刷新。


GetView


GetX 提供了一个快捷的 Widget 用来访问容器中的 controller,即 GetViewGetView是一个继承 StatelessWidget的抽象类,实现很简单,只是定义了一个获取 controllerget 属性。


abstract class GetView<T> extends StatelessWidget {
const GetView({Key? key}) : super(key: key);

final String? tag = null;

T get controller => GetInstance().find<T>(tag: tag)!;

@override
Widget build(BuildContext context);
}

通过继承 GetView,就可以直接使用controller.obx构建界面,而 controller.obx 最大的特点是针对 RxStatus 的4个状态分别定义了四个属性:


Widget obx(
NotifierBuilder<T?> widget, {
Widget Function(String? error)? onError,
Widget? onLoading,
Widget? onEmpty,
})


  • NotifierBuilder<T?> widget:实际就是一个携带状态变量,返回正常状态界面的函数,NotifierBuilder<T?>的定义如下。通过这个方法可以使用状态变量构建正常界面。


typedef NotifierBuilder<T> = Widget Function(T state);


  • onError:错误时对应的 Widget构建函数,可以使用错误信息 error

  • onLoading:加载时对应的 Widget

  • onEmpty:数据为空时的 Widget



通过这种方式可以自动根据 change方法指定的 RxStatus 来构建不同状态的 UI 界面,从而避免了丑陋的 if...elseswitch 语句。例如我们的个人主页,可以按下面的方式来写,是不是感觉更清晰和清爽了?


class PersonalHomePageMixin extends GetView<PersonalMixinController> {
PersonalHomePageMixin({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return controller.obx(
(personalEntity) => _PersonalHomePage(personalProfile: personalEntity!),
onLoading: Center(
child: CircularProgressIndicator(),
),
onError: (error) => Center(
child: Text(error!),
),
onEmpty: Center(
child: Text('暂无数据'),
),
);
}
}

对应的PersonalMixinController的代码如下:


class PersonalMixinController extends GetxController
with StateMixin<PersonalEntity> {
final String userId;
PersonalMixinController({required this.userId});

@override
void onReady() {
getPersonalProfile(userId);
super.onReady();
}

void getPersonalProfile(String userId) async {
change(null, status: RxStatus.loading());
var personalProfile = await JuejinService().getPersonalProfile(userId);
if (personalProfile != null) {
change(personalProfile, status: RxStatus.success());
} else {
change(null, status: RxStatus.error('获取个人信息失败'));
}
}
}

Controller 的构建


从 GetView 的源码可以看到,Controller 是从容器中获取的,这就需要使用 GetX 的容器,在使用 Controller 前注册到 GetX 容器中。


Get.lazyPut<PersonalMixinController>(
() => PersonalMixinController(userId: '70787819648695'),
);

总结


本篇介绍了使用GetXStateMixin方式构建更优雅的页面结构,通过controller.obx 的参数配置不同状态对应不同的组件。可以根据 RxStatus 状态自动切换组件,而无需写丑陋的 if...elseswitch 语句。当然,使用这种方式的前提是需要在 GetX 的容器中构建 controller 对象,本篇源码已上传至:GetX 状态管理源码。实际上使用容器能够带来其他的好处,典型的应用就是依赖注入(Dependency Injection,简称DI),接下来我们会使用两篇来介绍依赖注入的概念和具体应用。


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

落地西瓜视频埋点方案,埋点从未如此简单

前言 目前,几乎每个商用应用都有数据埋点的需求。你的 App 是怎么做埋点的呢,有遇到让你 “难顶” 的问题吗? 在这篇文章里,我将带你建立数据埋点的基本认识,还会介绍西瓜视频团队的前端埋点方案,最后为你带来我的落地实现 EasyTrack。如果能帮上忙,请...
继续阅读 »

前言



  • 目前,几乎每个商用应用都有数据埋点的需求。你的 App 是怎么做埋点的呢,有遇到让你 “难顶” 的问题吗?

  • 在这篇文章里,我将带你建立数据埋点的基本认识,还会介绍西瓜视频团队的前端埋点方案,最后为你带来我的落地实现 EasyTrack。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。




目录





1. 数据埋点概述


1.1 为什么要埋点?


“除了上帝,任何人都必须用数据说话”,在数据时代,使用数据驱动产品迭代已经称为行业共识。在分析应用数据之前,首先需要获得数据,这就需要前端或服务端进行数据埋点。


1.2 数据需求的工作流程


首先,你需要了解数据需求的工作流程,需求是如何产生,又是如何流转的,主要分为以下几个环节:



  • 1、需求产生: 产品需求引起产品形态变化,产生新的数据需求;

  • 2、事件设计: 数据产品设计埋点事件并更新数据字典文档,提出埋点评审;

  • 3、埋点开发: 开发进行数据埋点开发;

  • 4、埋点测试: 测试进行数据埋点测试,确保数据质量;

  • 5、数据消费: 数据分析师进行数据分析,推荐系统工程师进行模型训练,赋能产品运营决策。



1.3 数据消费的经典场景



























消费场景需求描述技术需求
渗透率分析统计 DAU/PV/UV/VV 等准确的上报时机
归因分析分析前因后果准确上报上下文 (如场景、会话、来源页面)
1. A / B 测试
2. 个性化推荐
分析用户特征、产品特征等准确上报事件属性

可以看到,在归因分析中,除了需要上报事件本身的属性之外,还需要上报事件产生时的上下文信息,例如当前页面、来源页面、会话等。


1.4 埋点数据采集的基本模型


数据采集是指在前端或服务端收集需要上报的事件属性的过程。为了满足复杂、高效的数据消费需求,需要科学合理地设计端侧的数据采集逻辑,基本可以总结为 “4W + 1H” 模型:





































模型描述举例
1、WHAT什么行为事件名
2、WHEN行为产生的时间时间戳
3、WHO行为产生的对象对象唯一标识 (例如用户 ID、设备 ID)
4、WHERE行为产生的环境设备所处的环境 (例如 IP、操作系统、网络)
5、HOW行为的特征上下文信息 (例如当前页面、来源页面、会话)



2. 如何实现数据埋点?


2.1 埋点方案总结


目前,业界已经存在多种埋点方案,主要分为全埋点、前端代码埋点和服务端代码埋点三种,优缺点和适用场景总结如下:































全埋点前端埋点服务端埋点
优势开发成本低完整采集上下文信息不依赖于前端版本
劣势数据量大,无法获取上下文数据,数据质量低前端开发成本较高服务端开发成本较高、获取上下文信息依赖于接口传值
适用场景通用基础事件(如启动/退出、浏览、点击)核心业务流程(如登录、注册、收藏、购买)核心业务结果事件(如支付成功)



  • 1、全埋点: 指通过编译时插桩、运行时动态代理等 AOP 手段实现自动埋点和上报,无须开发者手动进行埋点,因此也称为 “无埋点”;




  • 2、前端埋点: 指前端 (包括客户端) 开发者手动编码实现埋点,虽然可以通过埋点工具或者脚本简化埋点开发工作,但总体上还是需要手动操作;




  • 3、服务端埋点: 指服务端手动编码实现埋点,缺点是需要客户端需要侵入接口来保留上下文参数。




2.2 全埋点方案的局限性


表面上看,全埋点方案的优势很明显:客户端和服务端只需要一次开发,就能实现所有页面、所有路径的曝光和点击事件埋点,节省了研发人力,也不用担心埋点逻辑会侵入正常业务逻辑。然而,不可能存在完美的解决方案,全埋点方案还是存在一些局限性:




  • 1、资源消耗较大: 全场景上报会产生大量无用数据,网络传输、数据存储和数据计算需要消耗大量资源;




  • 2、页面稳定性要求较高: 需要保持页面视图结构相对稳定,一旦页面视图结果变化,历史录入的埋点数据就会失效;




  • 3、无法采集上下文信息: 无法采集事件产生时的上下文信息,也就无法满足复杂的数据消费需求。




2.3 埋点设计的整体方案


考虑的不同方案都存在优缺点,单纯采用一种埋点方案是不切实际的,需要根据不同业务场景和不同数据消费需要而采用不同的埋点方案:




  • 1、全埋点: 作为全局兜底方案,可以满足粗粒度的统计需求;




  • 2、前端埋点: 作为全埋点的补充方案,可以自定义埋点参数,主要处理核心业务流程事件,例如(如登录、注册、收藏、购买);




  • 3、服务端埋点: 核心业务结果事件,例如订单支付成功。






3. 前端埋点中的困难


3.1 一个简单的埋点场景


现在,我们通过一个具体的埋点场景,试着发现在做埋点需求时会遇到的困难或痛点。我直接使用西瓜视频中的一个埋点场景:



—— 图片引用自西瓜视频技术博客


这个产品场景很简单,左边是西瓜视频的推荐流列表,点击 “电影卡片” 会进入右边的 “电影详情页” 。两个页面中都有 “收藏按钮”,现在的数据需求是采集不同页面中 “收藏按钮” 的点击事件,以便分析用户收藏影片的行为,优化影片的推荐模型。



  • 1、在推荐列表页中上报点击事件:


“event_name" : "click_favorite", // 事件名
"cur_page" : "feed", // 当前页面
"video_id" : "123", // 影片 ID
"video_name" : "影片名", // 影片名
"video_type" : "1", // 影片类型
"$user_id" : "10000", // 用户 ID
"$device_id" : "abc" // 设备 ID
... // 其他预置属性


  • 2、在电影详情页中上报点击事件:


“event_name" : "click_favorite", // 事件名
"from_page" : "feed"
"cur_page" : "video_detail", // 当前页面
"video_id" : "123", // 影片 ID
"video_name" : "影片名", // 影片名
"video_type" : "1", // 影片类型
"$user_id" : "10000", // 用户 ID
"$device_id" : "abc" // 设备 ID
... // 其他预置属性

3.2 现状分析


理解了这个埋点场景之后,我们先梳理出目前遇到的困难:




  • 1、埋点参数分散: 需要上报的埋点参数位于不同 UI 容器或不同业务模块,代码跨度很大(例如:Activity、Fragment、ViewHolder、自定义 View);




  • 2、组件复用: 组件抽象复用后在多个页面使用(例如通用的 ViewHolder 或自定义 View);




  • 3、数据模型不一致: 不同场景 / 页面下描述状态的数据模型不一致,需要额外的转换适配过程(例如有的模型用 video_type 表示影片类型,另一些模型用 videoType 表示影片类型)。




3.3 评估标准


理解了问题和现状,现在我们开始尝试找到解决方案。为此,我们需要想清楚理想中的解决方案,应该满足什么标准:



  • 1、准确性: 这是核心目标,能够在保证不同场景 / 页面下准确收集埋点数据;

  • 2、简洁性: 使用方法尽可能简单,收敛模板代码;

  • 3、可用性: 尽可能高效稳定,不容易出错,性能开销小。


3.4 常规解决方案


1、逐级传递 —— 通过面向对象的关系逐级传递埋点参数:


通过 Android 框架支持的 Activity / Fragment 参数传递方式和面向对象程序设计,逐级将埋点参数传递到最深层的收藏按钮。例如:




  • 列表页: Activity -> ViewModel -> FeedFragment (推荐) -> Adapter -> ViewHolder (电影卡片) -> CollectButton (收藏按钮)




  • 详情页: Activity -> ViewModel -> DetailBottomFragment(底部功能区) -> CollectButton (收藏按钮)




缺点 (参数传递困难) :传递数据需要编写大量重复模板代码,工程代码膨胀,增大维护难度。再叠加上组件复用的情况,逐级传递会让代码复杂度非常高,很明显不是一个合理的解决方案。


2、Bean 传递 —— 在 Java Bean 中增加字段来收集埋点参数:


缺点 (违背单一职责原则):Java Bean 中侵入了与业务无关的埋点参数,同时会造成 Java Bean 数据冗余,增大维护难度。


3、全局单例 —— 通过全局单例对象来收集埋点参数:


这个方案与 “Bean 传递 ” 类似,区别在于埋点参数从 Java Bean 中移动到全局单例中,但缺点还是很明显:


缺点 (写入和清理时机):单例会被多个位置写入,一旦被覆盖就无法被恢复,容易导致上报错误;另外清理的时机也难以把握,清理过早会导致埋点参数丢失,清理过晚会污染后面的埋点事件。




4. 西瓜视频方案


理解了数据埋点开发中的困难,有没有什么方案可以简化埋点过程中的复杂度呢?我们来讨论下西瓜视频团队分享的一个思路:基于视图树收集埋点参数。




—— 图片引用自西瓜视频技术博客


通过分析数据与视图节点的关系可以发现,事件的埋点数据正好分布在视图树的不同节点中。当 “收藏按钮” 触发事件时,只需要沿着视图树逐级向上查找 (通过 View#getParent()) 就可以收集到所有数据。


并且,树的分支天然地支持为参数设置不同的值。例如 “推荐 Fragment” 需要上报 “channel : recomment”,而 “电影 Fragment” 需要上报 “channel : film”。因为 Fragment 的根布局对应有视图树中的不同节点,所以在不同 Fragment 中触发的事件最终收集到的 “channel” 参数值也就不同了。Nice~




5. EasyTrack 埋点框架


思路 Get 到了,现在我们来讨论如何应用这个思路来解决问题。贴心的我已经帮你实现为一个框架 EasyTrack。源码地址:github.com/pengxurui/E…


5.1 添加依赖



  • 1、依赖 JitPack 仓库


在项目级 build.gradle 声明远程仓库:


allprojects {
repositories {
google()
mavenCentral()
// JitPack 仓库
maven { url "https://jitpack.io" }
}
}


  • 2、依赖 EasyTrack 框架


在模块级 build.gradle 中依赖类库:


dependencies {
...
// 依赖 EasyTrack 框架
implementation 'com.github.pengxurui:EasyTrack:v1.0.1'
// 依赖 Kotlin 工具(非必须)
implementation 'com.github.pengxurui:KotlinUtil:1.0.1'
}

5.2 依附埋点参数到视图树


ITrackModel接口定义了一个数据填充能力,你可以创建它的实现类来定义一个数据节点,并在 fillTrackParams() 方法中声明参数。例如:MyGoodsViewHolder 实现了 ITrackMode 接口,在 fillTrackParams() 方法中声明参数(goods_id / goods_name)。


随后,通过 View 的扩展函数View.trackModel()将其依附到视图节点上。扩展函数 View.trackModel() 内部基于 View#setTag() 实现。


MyGoodsViewHolder.kt


class MyGoodsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), ITrackModel {

private var mItem: GoodsItem? = null

init {
// Java:EasyTrackUtilsKt.setTrackModel(itemView, this);
itemView.trackModel = this
}

override fun fillTrackParams(params: TrackParams) {
mItem?.let {
params.setIfNull("goods_id", it.id)
params.setIfNull("goods_name", it.goods_name)
}
}
}

EasyTrackUtils.kt


/**
* Attach track model on the view.
*/
var View.trackModel: ITrackModel?
get() = this.getTag(R.id.tag_id_track_model) as? ITrackModel
set(value) {
this.setTag(R.id.tag_id_track_model, value)
}

ITrackModel.kt


/**
* 定义数据填充能力
*/
interface ITrackModel : Serializable {
/**
* 数据填充
*/
fun fillTrackParams(params: TrackParams)
}

5.3 触发事件埋点


在需要埋点的地方,直接通过定义在 View 上的扩展函数 trackEvent(事件名)触发埋点事件,它会以该扩展函数的接收者对象为起点,逐级向上层视图节点收集参数。另外,它还有多个定义在 Activity、Fragment、ViewHolder 上的扩展函数,但最终都会调用到 View.trackEvent。


class MyGoodsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(item: GoodsItem) {
...
trackEvent(GOODS_EXPOSE)
}
}

EasyTrackUtils.kt


@JvmOverloads
fun Activity?.trackEvent(eventName: String, params: TrackParams? = null) =
findRootView(this)?.doTrackEvent(eventName, params)

@JvmOverloads
fun Fragment?.trackEvent(eventName: String, params: TrackParams? = null) =
this?.requireView()?.doTrackEvent(eventName, params)

@JvmOverloads
fun RecyclerView.ViewHolder?.trackEvent(eventName: String, params: TrackParams? = null) {
this?.itemView?.let {
if (null == it.parent) {
it.post { it.doTrackEvent(eventName, params) }
} else {
it.doTrackEvent(eventName, params)
}
}
}

@JvmOverloads
fun View?.trackEvent(eventName: String, params: TrackParams? = null): TrackParams? =
this?.doTrackEvent(eventName, params)

查看 logcat 日志,可以看到以下日志,显示埋点并没有生效。这是因为没有为 EasyTrack 配置埋点数据上报和统计分析的能力。


logcat 日志


EasyTrackLib: Try track event goods_expose, but the providers is Empty.

5.4 实现 ITrackProvider 接口


EasyTrack 的职责在于收集分散的埋点数据,本身没有提供埋点数据上报和统计分析的能力。因此,你需要实现 ITrackProvider 接口进行依赖注入。例如,这里模拟实现友盟数据埋点提供器,在 onInit() 方法中进行初始化,在 onEvent() 方法中调用友盟 SDK 事件上报方法。


MockUmengProvider.kt


/**
* 模拟友盟数据上报
*/
class MockUmengProvider : ITrackProvider() {

companion object {
const val TAG = "Umeng"
}

/**
* 是否启用
*/
override var enabled = true

/**
* 名称
*/
override var name = TAG

/**
* 初始化
*/
override fun onInit() {
Log.d(TAG, "Init Umeng provider.")
}

/**
* 执行事件上报
*/
override fun onEvent(eventName: String, params: TrackParams) {
Log.d(TAG, params.toString())
}
}

5.5 配置 EasyTrack


在应用初始化时,进行 EasyTrack 的初始化配置。我们可以将相关的初始化代码单独封装起来,例如:


StatisticsUtils.kt


// 模拟友盟数据统计提供器
val umengProvider by lazy {
MockUmengProvider()
}

// 模拟神策数据统计提供器
val sensorProvider by lazy {
MockSensorProvider()
}

/**
* 初始化 EasyTrack,在 Application 初始化时调用
*/
fun init(context: Context) {
configStatistics(context)
registerProviders(context)
}

/**
* 配置
*/
private fun configStatistics(context: Context) {
// 调试开关
EasyTrack.debug = BuildConfig.DEBUG
// 页面间参数映射
EasyTrack.referrerKeyMap = mapOf(
CUR_PAGE to FROM_PAGE,
CUR_TAB to FROM_TAB
)
}

/**
* 注册提供器
*/
private fun registerProviders(context: Context) {
EasyTrack.registerProvider(umengProvider)
EasyTrack.registerProvider(sensorProvider)
}

EventConstants.java


public static final String FROM_PAGE = "from_page";
public static final String CUR_PAGE = "cur_page";
public static final String FROM_TAB = "from_tab";
public static final String CUR_TAB = "cur_tab";


























配置类型描述
debugBoolean调试开关
referrerKeyMapMap<String,String>全局页面间参数映射
registerProvider()ITrackProvider底层数据埋点能力

以上步骤是 EasyTrack 的必选步骤,完成后重新执行 trackEvent() 后可以看到以下日志:


logcat 日志


/EasyTrackLib:  
onEvent:goods_expose
goods_id= 10000
goods_name = 商品名
Try track event goods_expose with provider Umeng.
Try track event goods_expose with provider Sensor.
------------------------------------------------------

5.6 页面间参数映射


上一节中有一个referrerKeyMap配置项,定义了全局的页面间参数映射。 举个例子,在分析不同入口的转化率时,不仅仅需要上报当前页面的数据,还需要上报来源页面的信息。这样我们才能分析用户经过怎样的路径来到当前页面,并最终触发了某个行为。


需要注意的是,来源页面的参数往往不能直接添加到当前页面的埋点参数中,这里一般会有一定的转换规则 / 映射关系。例如:来源页面的 cur_page 参数,在当前页面应该映射为 from_page 参数。 在这个例子里,我们配置的映射关系是:



  • 来源页面的 cur_page 映射为当前页面的 from_page;

  • 来源页面的 cur_tab 映射为当前页面的 from_tab。


因此,假设来源页面传递给当前页面的参数是 A,则当前页面在触发事件时的收集参数是 B:


A (来源页面):
{
"cur_page" : "list"
...
}

B (当前页面):
{
"cur_page" : "detail",
"from_page" : "list",
...
}

BaseTrackActivity 实现了页面间参数映射,你可以创建 BaseActivity 类并继承于 BaseTrackActivity,或者将其内部的逻辑迁移到你的 BaseActivity 中。这一步是可选的,如果你不使用页面间参数映射的特性,你那大可不必使用 BaseTrackActivity。



















操作描述
定义映射关系1、EasyTrack.referrerKeyMap 配置项
2、重写 BaseTrackActivity #referrerKeyMap() 方法
传递页面间参数Intent.referrerSnapshot(TrackParams) 扩展函数

MyGoodsDetailActivity.java


public class MyGoodsDetailActivity extends MyBaseActivity {

private static final String EXTRA_GOODS = "extra_goods";

public static void start(Context context, GoodsItem item, TrackParams params) {
Intent intent = new Intent(context, GoodsDetailActivity.class);
intent.putExtra(EXTRA_GOODS, item);
EasyTrackUtilsKt.setReferrerSnapshot(intent, params);
context.startActivity(intent);
}

@Nullable
@Override
protected String getCurPage() {
return GOODS_DETAIL_NAME;
}

@Nullable
@Override
public Map<String, String> referrerKeyMap() {
Map<String, String> map = new HashMap<>();
map.put(STORE_ID, STORE_ID);
map.put(STORE_NAME, STORE_NAME);
return map;
}
}

需要注意的是,BaseTrackActivity 不会将来源页面的全部参数都添加到当前页面的参数中,只有在全局 referrerKeyMap 配置项或 referrerKeyMap() 方法中定义了映射关系的参数,才会添加到当前页面。 例如:MyGoodsDetailActivity 继承于 BaseActivity,并重写 referrerKeyMap() 定义了感兴趣的参数(STORE_ID、STORE_NAME)。最终触发埋点时的日志如下:


logcat 日志


/EasyTrackLib:  
onEvent:goods_detail_expose
goods_id= 10000
goods_name = 商品名
store_id = 10000
store_name = 商店名
from_page = Recommend
cur_page = goods_detail
Try track event goods_expose with provider Umeng.
Try track event goods_expose with provider Sensor.
------------------------------------------------------

在一般的埋点模型中,每个 Activity (页面) 都有对应一个唯一的 page_id,因此你可以重写 fillTrackParams() 方法追加这些固定的参数。例如:MyBaseActivity 定义了 getCurPage() 方法,子类可以通过重写 getCurPage() 来设置 page_id。


MyBaseActivity.java


abstract class MyBaseActivity : BaseTrackActivity() {

@CallSuper
override fun fillTrackParams(params: TrackParams) {
super.fillTrackParams(params)
// 填充页面统一参数
getCurPage()?.also {
params.setIfNull(CUR_PAGE, it)
}
}

protected open fun getCurPage(): String? = null
}

5.7 TrackParams 参数容器


TrackParams 是 EasyTrack 收集参数的中间容器,最终会分发给 ITrackProvider 使用。



























方法描述
set(key: String, value: Any?)设置参数,无论无何都覆盖
setIfNull(key: String, value: Any?)设置参数,如果已经存在该参数则丢弃
get(key: String): String?获取参数值,参数不存在则返回 null
get(key: String, default: String?)获取参数值,参数不存在则返回默认值 default

5.8 使用 Kotlin 委托依附参数


如果你觉得每次定义 ITrackModel 数据节点后都需要调用 View.trackModel,你可以使用我定义的 Kotlin 委托 “跳过” 这个步骤,例如:


MyFragment.kt


private val trackNode by track()

EasyTrackUtils.kt


fun <F : Fragment> F.track(): TrackNodeProperty<F> = FragmentTrackNodeProperty()

fun RecyclerView.ViewHolder.track(): TrackNodeProperty<RecyclerView.ViewHolder> =
LazyTrackNodeProperty() viewFactory@{
return@viewFactory itemView
}

fun View.track(): TrackNodeProperty<View> = LazyTrackNodeProperty() viewFactory@{
return@viewFactory it
}

如果你还不了解委托属性,可以看下我之前写过的一篇文章,这里不解释其原理了:Android | ViewBinding 与 Kotlin 委托双剑合璧




6. EasyTrack 核心源码


这一节,我简单介绍下 EasyTrack 的核心源码,最核心的部分在入口类 EasyTrack 中:


6.1 doTrackEvent()


doTrackEvent() 是触发埋点的主方法,主要流程是调用 fillTrackParams() 收集埋点参数,再将参数分发给有效的 ITrackProvider。


internal fun Any.doTrackEvent(eventName: String, otherParams: TrackParams? = null): TrackParams? {
1. 检查是否有有效的 ITrackProvider
2. 基于视图树递归收集埋点参数(fillTrackParams)
3. 日志
4. 将收集到的埋点参数分发给有效的 ITrackProvider
}

6.2 fillTrackParams()


-> 基于视图树递归收集埋点参数
internal fun fillTrackParams(node: Any?, params: TrackParams? = null): TrackParams {
val result = params ?: TrackParams()
var curNode = node
while (null != curNode) {
when (curNode) {
is View -> {
// 1. 视图节点
if (android.R.id.content == curNode.id) {
// 1.1 Activity 节点
val activity = getActivityFromView(curNode)
if (activity is IPageTrackNode) {
// 1.1.1 IPageTrackNode节点(处理页面间参数映射)
activity.fillTrackParams(result)
curNode = activity.referrerSnapshot()
} else {
// 1.1.2 终止
curNode = null
}
} else {
// 1.2 Activity 视图子节点
curNode.trackModel?.fillTrackParams(result)
curNode = curNode.parent
}
}
is ITrackNode -> {
// 2. 非视图节点
curNode.fillTrackParams(result)
curNode = curNode.parent
}
else -> {
// 3. 终止
curNode = null
}
}
}
return result
}

主要逻辑:从入参 node 为起点,循环获取依附在视图节点上的 ITrackModel 数据节点并调用 fillTrackParams() 方法收集参数,并将循环指针指向 parent。




7. 总结


EasyTrack 框架的源码我已经放在 Github 上了,源码地址:github.com/pengxurui/E… 我也写了一个简单的 Sample Demo,你可以直接运行体验下。欢迎批评,欢迎 Issue~


说说目前遇到的问题,在处理页面间参数传递时,我们需要依赖 Intent extras 参数。这就导致我们需要在大量创建 Intent 的地方都加入来源页面的埋点参数(注意:即使你不使用 EasyTrack,你也要这么做)。目前我还没有想到比较好的方法,你觉得呢?说说你的看法吧。


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

【Flutter 状态管理】第一论: 对状态管理的看法与理解

前言 由 编程技术交流圣地[-Flutter群-] 发起的 状态管理研究小组,将就 状态管理 相关相关话题进行为期 两个月 的讨论。小组将于两个月后解散,并发布相关讨论成果。 目前只有内定的 5 个人参与讨论,如果你对状态管理有什么独特的见解,或想参与其中。...
继续阅读 »
前言

编程技术交流圣地[-Flutter群-] 发起的 状态管理研究小组,将就 状态管理 相关相关话题进行为期 两个月 的讨论。小组将于两个月后解散,并发布相关讨论成果。



目前只有内定的 5 个人参与讨论,如果你对状态管理有什么独特的见解,或想参与其中。可以发表一篇自己对状态管理的认知文章,作为入群的“门票”,欢迎和我们共同交流。




前两周进行第一个话题的探讨 :


你对状态管理的看法与理解



状态管理,状态管理。顾名思义是状态+管理,那问题来了,到底什么是状态?为什么要管理呢?


一、何谓状态


1. 对状态概念的思考

其实要说明一个东西是什么,是非常困难的。这并不像数学中能给出具体的定义,比如


平行四边形: 是在同一个二维平面内,由两组平行线段组成的闭合图形
三角形: 是由同一平面内不在同一直线上的三条线段首尾顺次连接所组成的封闭图形

如果具有明确定义的概念,我们可以很容易理解它的特性和作用。但对于 状态 这种含义比较笼统的词汇,那就仁者见仁,智者见智 了。我查了一下,对于状态而言有如下解释:


状态是人或事物表现出来的形态。是指现实(或虚拟)事物处于生成、生存、发展、消亡时期
或各转化临界点时的形态或事物态势。

如果影射到编程上,状态就是界面各个时期的表现,状态的改变,通过刷新后会导致界面的变化。那 界面状态 有什么区别和联系呢?


比如说一颗种子发芽、长大、开花、结果、枯萎,这是外在的表征,是外界所看到的形态变化。但从根本上来说,这些变化是种子与外界的资源交换,导致的内部数据变化,而产生的结果。也就是一个是 面子 ,一个是 里子


看花人并不会在意种子的内部的变化逻辑,他们只需满足看花的需求就行了。 也就是说 界面是表现 ,是用来给用户看的;状态是本质 ,是需要编程者去维护的。如果一个开发者只能看到 面子 ,而忽略我们本身就是那颗种子,还谈什么状态,想什么管理?。




2.状态、交互与界面

对一个应用而言,最根本的目的在于: 用户 通过操作界面, 可以进行正确的逻辑处理,并得到一定的响应反馈





从用户的角度来看,应用内部运作机制是个 黑盒,用户不需要、也没必要了解细节。但这个黑盒内部逻辑处理需要编程者进行实现,我们是无法逃避的。



拿我们最熟悉的计数器而言,点击按钮,修改状态信息,重新构建后,实现界面上数字变化的效果。





二、为什么需要管理


说到 管理 一词,你觉得什么情况下需要管理?是 复杂,只有 复杂 才有管理的必要。那管理有什么好处?


比如张三开了一家餐馆,雇了四个人,他们各干各的,都要同时进行招乎食客、烧菜、送快递、清洁等任务,那效率将非常低下。如果菜里吃出了不明生物 (bug),也不容易定位问题根源。这很像什么东西都塞在一个 XXXState 里去完成,其中不仅需要处理组件构建逻辑,还掺杂着大量的业务逻辑


如果将复杂的事务,分层次地交由不同人进行处理,各司其职,要比四个人各干各的要高效。而管理的目的就是分层级提高地 处理任务。




1.状态的作用范围

首先来思考一个问题:是不是所有的状态都需要管理?比如说下面的 FloatingActionButton ,在点击时会有水波纹的效果,界面的变化就意味着存在着状态的变化



FloatingActionButton 组件继承自 StatelessWidget,也就是说它并没有改变自身状态的能力。那点击时,为什么状态会发生变化呢?因为它在 build 中使用了 RawMaterialButton 组件,RawMaterialButton 中使用了 InkWell ,而 InkWell 继承自 InkResponseInkResponsebuild 中使用了_InkResponseStateWidget ,这个组件中维护了水波纹在手势中的状态变化逻辑。


class FloatingActionButton extends StatelessWidget{

---->[FloatingActionButton#build]----
Widget result = RawMaterialButton(
onPressed: onPressed,
mouseCursor: mouseCursor,
elevation: elevation,
focusElevation: focusElevation,
hoverElevation: hoverElevation,
highlightElevation: highlightElevation,
disabledElevation: disabledElevation,
constraints: sizeConstraints,
materialTapTargetSize: materialTapTargetSize,
fillColor: backgroundColor,
focusColor: focusColor,
hoverColor: hoverColor,
splashColor: splashColor,
textStyle: extendedTextStyle,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
autofocus: autofocus,
enableFeedback: enableFeedback,
child: resolvedChild,
);



也就是说:点击时,水波纹的变化效果,被封装在 _InkResponseStateWidget 组件状态中。像这种私有的状态,我们并不需要进行管理,因为它能够独立完成自己任务,而且外界并不需要了解这些状态。比如水波纹的圆心半径等会变化的状态信息,在外界是不关心的。

Flutter 中的 State 本身就是一种状态管理的手段。因为:


1. State 具有根据状态信息,构建组件的能力
2. State 具有重新构建组件的能力

所有的 StatefulWidget 都是这样,变化逻辑及状态量都会被封装在对应的 XXXState 类中。是局部的,私有的,外界无需了解内部状态的信息变化,也没有可以直接访问的途径。这一般用于对组件的封装,将复杂且相对独立的状态变化,封装起来,简化用户使用。




2.状态的共享及修改同步

上面说的 State 管理状态虽然非常小巧,方便。但同时也会存在不足之处,因为状态量被维护在 XXXState 内部,外界很难访问修改。比如下面 page1 中,C 是数字信息,跳转到 page2 时,也要显示这个数值,且按下 R 按钮能要让 page1page2 的数字都重置为 0。这就存在着状态存在共享及修改同步更新,该如何实现呢?





我们先来写个如下的设置界面:



class SettingPage extends StatelessWidget {
const SettingPage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('设置界面'),),
body: Container(
height: 54,
color: Colors.white,
child: Row(
children: [
const SizedBox(width: 10,),
Text('当前计数为:'),
Spacer(),
ElevatedButton(child: Text('重置'),onPressed: (){} ),
const SizedBox(width: 10,)
],
),
),
);
}
}

那如何知道当前的数值,以及如何将 重置 操作点击时,影响 page1 的数字状态呢?其实 构造入参回调函数 可以解决一切的数据共享和修改同步问题。




3.代码实现 - setState 版:源码位置

在点击重置时 ,由于 page2 的计数也要清空,这就说明其状态量需要变化,要用 StatefulWidget 维护状态。在构造时,通过构造方法传入 initialCounter ,让 page2 的数字可以与 page1 一致。通过 onReset 回调函数来监听重置按钮的触发,以此来重置 page1 的数字状态,让 page1 的数字可以与 page2 一致。这就是让两个界面的同一状态量保持一致。如下图:


class SettingPage extends StatefulWidget {
final int initialCounter;
final VoidCallback onReset;

const SettingPage({
Key? key,
required this.initialCounter,
required this.onReset,
}) : super(key: key);

@override
State<SettingPage> createState() => _SettingPageState();
}














跳转到设置页设置页重置

class _SettingPageState extends State<SettingPage> {
int _counter = 0;

@override
void initState() {
super.initState();
_counter = widget.initialCounter;
}

//构建同上, 略...

void _onReset() {
widget.onReset();
setState(() {
_counter = 0;
});
}

_SettingPageState 中维护 _counter 状态量,在点击 重置 时执行 _onReset 方法,触发 onReset 回调。在 界面1 中监听 onReset ,来重置 界面1 的数字状态。这样通过 构造入参回调函数 ,就能保证两个界面 数字状态信息 的同步。


---->[界面1 跳转代码]-----
Navigator.push(context,
MaterialPageRoute(builder: (context) => SettingPage(
initialCounter: _counter,
onReset: (){
setState(() {
_counter=0;
});
},
)));

但这样,确定也很明显,数据传来传去,调来调去,非常麻烦,乱就容易出错。如果再多几个需要共享的信息,或者在其他界面里还需要共享这个状态,那代码里将会更加混乱。




4.代码实现 - ValueListenableBuilder 版:源码位置

上面的 setState 版实现 数据共享和修改同步,除了代码混乱之外,还有一些其他的缺点。首先,在 SettingPage 中我们又维护了一个状态信息,两个界面的信息虽然相同,却是两份一样的。如果状态信息是比较大的对象,这未免会造成不必要的内存浪费。





其次,就是深为大家诟病的 setState 重构范围。State#setState 执行后,会触发 build 方法重新构建组件。比如在 page1 中,_MyHomePageState#build 构建的是 Scaffold ,当状态变化时触发 setState ,其下的所有组件都会被构建一遍,重新构建的范围过大。

大家可以想一下,这里为什么不把 Scaffold 提到外面去?原因是:FloatingActionButton 组件需要修改状态量 _counter 并执行重新构建,所以不得不扩大构建的范围,来包含住 FloatingActionButton





其实 Flutter 中有个组件可以解决上面两个问题,那就是 ValueListenableBuilder 。使用方式很简单,先创建一个 ValueNotifier 的可监听对象 _counter


class _MyHomePageState extends State<MyHomePage> {

final ValueNotifier<int> _counter = ValueNotifier(0);

@override
void dispose() {
super.dispose();
_counter.dispose();
}

void _incrementCounter() {
_counter.value++;
}

如下使用 ValueListenableBuilder 组件,监听 _counter 对象,当该可监听对象的数值变化时,会可以通知监听者,重新构建 builder 方法里的组件。这样最大的好处在于:不需要 通过 _MyHomePageState#setState 对内部整体进行构建,仅对需要改变的局部 进行重新构建。


ValueListenableBuilder(
valueListenable: _counter,
builder: (ctx, int value, __) => Text(
'$value',
style: Theme.of(context).textTheme.headline4,
),
),



可以将 对于_counter 可见听对象传入 page2 中,同样通过 ValueListenableBuilder 监听 counter。这就相当于观察者模式中,两个订阅者 同时监听一个发布者 。在 page2 中让发布者信息变化,也会通知两个订阅者,比如执行 counter.value =0 ,两处的 ValueListenableBuilder 都会触发局部重建。



这样就能达到和 setState 版 一样的效果,通过 ValueListenableBuilder 简化了入参和回调通知,并具有局部重构组件的能力。可以说 State状态的共享及修改同步 方面是被 ValueListenableBuilder 完胜的。但话说回来, State 本来就不是做这种事的,它更注重于私有状态的处理。比如ValueListenableBuilder 的本质,就是一个通过 State 实现的私有状态封装 ,所以没有什么好不好,只有适合或不适合。





三、使用状态管理工具


1. 状态管理工具的必要性

其实前面的 ValueListenableBuilder 的效果以及不错了,但是在某些场合仍存在不足。因为 _counter 需要通过构造方法进行传递,如果状态量过多,或共享场合变多、传递层级过深,也会使代码处理比较复杂。最致命的一点是:业务逻辑处理界面组件都耦合在 _MyHomePageState 中,这对于拓展维护而言并不是件好事。所以 管理 对于 复杂逻辑性下的状态的共享及修改同步 是有必要的。





2.通过 flutter_bloc 实现状态管理: 源码位置

我们前面说过,状态管理的目的在于:让状态可以共享及在更新状态时可以同步更新相关组件显示,且将状态变化逻辑界面构建进行分离。flutter_bloc 是实现状态管理的工具之一,它的核心是:通过 BlocEvent 操作转化成 State;同时通过 BlocBuilder 监听状态的变化,进行局部组件构建。


通过这种方式,编程者可以将 状态变化逻辑 集中在 Bloc 中处理。当事件触发时,通过发送 Event 指令,让 Bloc 驱动 State 进行变化。就这个小案例而言,主要有两个事件: 自加重置 。像这样不需要参数的 Event , 通过枚举进行区分即可,比如定义事件:


enum CountEvent {
add, // 自加
reset, // 重置
}



状态,就是界面构建需要依赖的信息。这里定义 CountState ,持有 value 数值。


class CountState {
final int value;
const CountState({this.value = 0});
}



最后是 Bloc ,新版的 flutter_bloc 通过 on 监听事件,通过 emit 产出新状态。如下在构造中通过 on 来监听 CountEvent 事件,通过 _onCountEvent 方法进行处理,进行 CountState 的变化。当 event == CountEvent.add 时,会产出一个原状态 +1 的新 CountState 对象。


class CountBloc extends Bloc<CountEvent, CountState> {
CountBloc() : super(const CountState()){
on<CountEvent>(_onCountEvent);
}

void _onCountEvent(CountEvent event, Emitter<CountState> emit) {
if (event == CountEvent.add) {
emit(CountState(value: state.value + 1));
}

if (event == CountEvent.reset) {
emit (const CountState(value: 0));
}
}
}

画一个简单的示意图,如下:点击 _incrementCounter 时,只需要触发 CountEvent.add 指令即可。核心的状态处理逻辑会在 CountBloc 中进行,并生成新的状态,且通过 BlocBuilder 组件 触发局部更新 。这样,状态变化的逻辑界面构建的逻辑就能够很好地分离。



// 发送自加事件指定
void _incrementCounter() {
BlocProvider.of<CountBloc>(context).add(CountEvent.add);
}

//构建数字 Text 处使用 BlocBuilder 局部更新:
BlocBuilder<CountBloc, CountState>(
builder: _buildCounterByState,
),

Widget _buildCounterByState(BuildContext context, CountState state) {
return Text(
'${state.value}',
style: Theme.of(context).textTheme.headline4,
);
}



这样,设置界面的 重置 按钮也是类似,只需要发出 CountEvent.reset 指令即可,核心的状态处理逻辑会在 CountBloc 中进行,并生成新的状态,且通过 BlocBuilder 组件 触发局部更新





由于 BlocProvider.of<CountBloc>(context) 获取 Bloc 对象,需要上级的上下文存在该 BlocProvider ,可以在最顶层进行提供。这样在任何界面中都可以获取该 Bloc 及对其状态进行共享。



这是个比较小的案例,可能无法体现 Bloc 的精髓,但作为一个入门级的体验还是挺不错的。你需要自己体会一下:


[1]. 状态的 [共享] 及 [修改状态] 时同步更新。
[2]. [状态变化逻辑] 和 [界面构建逻辑] 的分离。

个人认为,这两点是状态管理的核心。也许每个人都会有各自的认识,但至少你不能在不知道自己要管理什么的情况下,做着表面上认为是状态管理的事。最后总结一下我的观点:状态就是界面构建需要依赖的信息;而管理,就是通过分工,让这些状态信息可以更容易维护更便于共享更好同步变化更'高效'地运转flutter_bloc 只是 状态管理 的工具之一,而其他的工具,也不会脱离这个核心。




四、官方案例 - github_search 解读


1. 案例介绍:源码位置

为了让大家对 flutter_bloc 在逻辑分层上有更深的认识,这里选取了 flutter_bloc 官方的一个案例进行解读。下面先简单看一下界面效果:


[1] 输入字符进行搜索,界面显示 github 项目
[2] 在不同的状态下显示不同的界面,如未输入、搜索中、搜索成功、无数据。
[3] 输入时防抖 debounce。避免每输入一个字符都请求接口。

注: debounce : 当调用动作 n 毫秒后,才会执行该动作,若在这 n 毫秒内又调用此动作则将重新计算执行时间。















搜索状态变化无数据时状态显示

项目结构


├── bloc         # 处理状态变化逻辑
├── view # 处理视图构建
├── repository # 处理数据获取逻辑
└── main.dart # 程序入口



2.仓储层 repository

我们先来看一下仓储层 repository ,这是将数据获取逻辑单独抽离出来,其中包含model 包下相关数据实体类 ,和 api 包下数据获取操作。



有人可能会问,业务逻辑都放在 Bloc 里处理不就行了吗,为什么非要搞个 repository 层。其实很任意理解,Bloc 核心是处理状态的变化,如果接口请求代码都放在 Bloc 里就显得非常臃肿。更重要的有点是: repository 层是相对独立的,你完全可以单独对进行测试,保证数据获取逻辑的正确性。


这样能带来另一个好处,当数据模型确定后。repository 层和界面层完全可以同步进行开发,最后通过 Bloc 层将 repository界面 进行整合。分层是进行管理的一种手段,就像不同部门来处理不同的事务,一旦出错,就很容易定位是哪个环节出了问题。当一个部门的进行拓展升级,也能尽可能不波及其他部门。



repository 层也是通用的,不管是 Bloc 也好、Provider 也好,都只是管理的一种手段。repository 层作为数据的获取方式是完全独立的,比如 todo 的案例,Bloc 版和 Provider 可以共用一个 repository 层,因为即使框架的使用方式有差异,但数据的获取方式是不变的。




下面来简单看一下repository 层的逻辑,GithubRepository 依赖两个对象,只有一个 search 方法。其中 GithubCache 类型 cache 对象用于记录缓存,在查询时首先从缓存中查看,如果已存在,则返回缓存数据。否则使用 GithubClient 类型的 client 对象进行搜索。





GithubClient 主要通过 http 获取网络数据。





GithubClient 就是通过一个 Map 维护搜索字符搜索结果的映射。这了处理的比较简单,完全可以基于此进行拓展:比如设置一个缓存数量上限,不然随着搜索缓存会一直加入;或将缓存加入数据库,支持离线缓存。将 repository 层独立出来后,这些功能的拓展就能和界面层解耦。因为界面只关心数据本身,并不关心数据如何缓存、如何获取。





3. bloc 层

首先来看事件,整个搜索功能只有一个事件:文字输入时的TextChanged,事件触发时需要附带搜索的信息字符串。


abstract class GithubSearchEvent extends Equatable {
const GithubSearchEvent();
}

class TextChanged extends GithubSearchEvent {
const TextChanged({required this.text});

final String text;

@override
List<Object> get props => [text];

@override
String toString() => 'TextChanged { text: $text }';
}



至于状态,整个过程中有四类状态:



  • [1]. SearchStateEmpty : 输入字符为空时的状态,无维护数据。

  • [2]. SearchStateLoading : 从请求开始到响应中的等待状态,无维护数据。

  • [3]. SearchStateSuccess: 请求成功的状态,维护 SearchResultItem 条目列表。

  • [4]. SearchStateError:失败状态,维护错误信息字符串。





最后是 Bloc,用于整合状态变化的逻辑。在 构造方法 中通过 onTextChanged 事件进行监听,触发 _onTextChanged 产出状态。比如 searchTerm.isEmpty 说明无字符输入,产出 SearchStateEmpty 状态。在 githubRepository.search 获取数据前,产出 SearchStateLoading 表示等待状态。请求成功则产出 SearchStateSuccess 状态,且内含结果数据,失败则产出 SearchStateError 状态。


class GithubSearchBloc extends Bloc<GithubSearchEvent, GithubSearchState> {
GithubSearchBloc({required this.githubRepository})
: super(SearchStateEmpty()) {
on<TextChanged>(_onTextChanged);
}

final GithubRepository githubRepository;

void _onTextChanged(
TextChanged event,
Emitter<GithubSearchState> emit,
) async {
final searchTerm = event.text;

if (searchTerm.isEmpty) return emit(SearchStateEmpty());

emit(SearchStateLoading());

try {
final results = await githubRepository.search(searchTerm);
emit(SearchStateSuccess(results.items));
} catch (error) {
emit(error is SearchResultError
? SearchStateError(error.message)
: const SearchStateError('something went wrong'));
}
}
}

到这里,整个业务逻辑就完成了,不同时刻的状态变化也已经完成,接下来只需要通过 BlocBuilder 监听状态变化,构建组件即可。另外说明一下 debounce 的作用:如果不进行防抖处理,每次输入字符都会触发请求获取数据,这样会造成请求非常频繁,而且过程中的输入大多数是无用的。这种情况,就可以使用 debounce 进行处理,比如,输入 300 ms 后才进行请求操作,如果在此期间有新的输入,就重新计时。
其本质是对流的转换操作,在 stream_transform 插件中有相关处理,在 pubspec.yaml 中添加依赖


stream_transform: ^2.0.0



on<TextChanged>transformer 参数中可以指定事件流转换器,这样就能完成防抖效果:


const Duration _duration = Duration(milliseconds: 300);

EventTransformer<Event> debounce<Event>(Duration duration) {
return (events, mapper) => events.debounce(duration).switchMap(mapper);
}

class GithubSearchBloc extends Bloc<GithubSearchEvent, GithubSearchState> {
GithubSearchBloc({required this.githubRepository})
: super(SearchStateEmpty()) {
// 使用 debounce 进行转换
on<TextChanged>(_onTextChanged, transformer: debounce(_duration));
}



4.界面层

界面层的处理非常简单,通过 BlocBuilder 监听状态变化,根据不同的状态构建不同的界面元素即可。





事件的触发,是在文字输入时。输入框被单独封装成 SearchBar 组件,在 TextFieldonChanged 方法中,触发 _githubSearchBlocTextChanged 方法,这样驱动点,让整个状态变化的“齿轮组”运转了起来。


---->[search_bar.dart]----
@override
void initState() {
super.initState();
_githubSearchBloc = context.read<GithubSearchBloc>();
}

return TextField(
//....
onChanged: (text) {
_githubSearchBloc.add(TextChanged(text: text));
},

这样一个简单的搜索需求就完成了,flutter_bloc 还通过了非常多的实例、文档,有兴趣的可以自己多研究研究。




五、小结


这里小结一下我对状态管理的理解:


[1]. [状态] 是界面构建需要依赖的信息。
[2]. [管理] 是对复杂场景的分层处理,使[状态变化逻辑]独立于[视图构建逻辑]。

再回到那个最初的问题,是所有的状态都需要管理吗?如何区分哪些状态需要管理?就像前端 redux 状态管理,在 You Might Not Need Redux (可自行百度译文) 中说到:人们常常在正真需要 Redux 之前,就选择使用它 。对于状态管理,其实都是这样,往往初学者 "趋之若鹜" ,不明白为什么要状态管理,为什么一个很简单的功能,非要弯弯绕绕一大圈来实现。就是看到别用了,使用我也要用,这是不理智的。


我们在使用前应该明白:


[1]. 状态是否需要被共享和修改同步。如果否,也许通过 [State] 封装为内部状态是更好的选择。
[2]. [业务逻辑] 和[界面状态变化] 是否复杂到有分层的必要。如果不是非常复杂,
FutureBuilder、ValueListenableBuilder 这种小巧的局部构建组件也许是更好的选择。

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

Android 高级UI5 画笔Paint的基本用法

1.setStyle(Paint.Style style)设置画笔样式,取值有Paint.Style.FILL :填充内部Paint.Style.FILL_AND_STROKE :填充内部和描边Paint.Style.STROKE :仅描边代码实例:publi...
继续阅读 »

1.setStyle(Paint.Style style)

设置画笔样式,取值有
Paint.Style.FILL :填充内部
Paint.Style.FILL_AND_STROKE :填充内部和描边
Paint.Style.STROKE :仅描边

代码实例:


public class PaintViewBasic extends View {
private Paint mPaint;

public PaintViewBasic(Context context) {
super(context);
mPaint = new Paint();
}

public PaintViewBasic(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawStyle(canvas);
}

private void drawStyle( Canvas canvas ) {

mPaint.setColor(Color.RED);//设置画笔的颜色
mPaint.setTextSize(60);//设置文字大小
mPaint.setStrokeWidth(5);//设置画笔的宽度
mPaint.setAntiAlias(true);//设置抗锯齿功能 true表示抗锯齿 false则表示不需要这功能

mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(200,200,160,mPaint);

mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(200,600,160,mPaint);

mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
canvas.drawCircle(200,1000,160,mPaint);

}

}

2.setStrokeCap(Paint.Cap cap)

设置线冒样式,取值有
Paint.Cap.BUTT(无线冒)
Paint.Cap.ROUND(圆形线冒)
Paint.Cap.SQUARE(方形线冒)
注意:冒多出来的那块区域就是线帽!就相当于给原来的直线加上一个帽子一样,所以叫线帽


    private void drawStrokeCap(Canvas canvas) {
Paint paint = new Paint();

paint.setAntiAlias(true);
paint.setStrokeWidth(200);
paint.setColor(Color.parseColor("#00ff00"));
paint.setStrokeCap(Paint.Cap.BUTT); // 线帽,即画的线条两端是否带有圆角,butt,无圆角
canvas.drawLine(200, 200, 500, 200, paint);

paint.setColor(Color.parseColor("#ff0000"));
paint.setStrokeCap(Paint.Cap.ROUND); // 线帽,即画的线条两端是否带有圆角,ROUND,圆角
canvas.drawLine(200, 500, 500, 500, paint);

paint.setColor(Color.parseColor("#0000ff"));
paint.setStrokeCap(Paint.Cap.SQUARE); // 线帽,即画的线条两端是否带有圆角,SQUARE,矩形
canvas.drawLine(200, 800, 500, 800, paint);
}

3.setStrokeJoin(Paint.Join join)

设置线段连接处样式,取值有:
Paint.Join.MITER(结合处为锐角)
Paint.Join.Round (结合处为圆弧)
Paint.Join.BEVEL (结合处为直线)


  private void drawStrokeJoin( Canvas canvas ) {
Paint paint = new Paint();

paint.setAntiAlias( true );
paint.setStrokeWidth( 80 );
paint.setStyle(Paint.Style.STROKE ); // 默认是填充 Paint.Style.FILL
paint.setColor( Color.parseColor("#0000ff") );

Path path = new Path();
path.moveTo(100, 100);
path.lineTo(400, 100);
path.lineTo(100, 300);
paint.setStrokeJoin(Paint.Join.MITER);
canvas.drawPath(path, paint);

path.moveTo(100, 500);
path.lineTo(400, 500);
path.lineTo(100, 700);
paint.setStrokeJoin(Paint.Join.ROUND);
canvas.drawPath(path, paint);

path.moveTo(100, 900);
path.lineTo(400, 900);
path.lineTo(100, 1100);
paint.setStrokeJoin(Paint.Join.BEVEL);
canvas.drawPath(path, paint);
}

}

4.setPathEffect(PathEffect effect)

设置绘制路径的效果,如点画线等

CornerPathEffect:

这个类的作用就是将Path的各个连接线段之间的夹角用一种更平滑的方式连接,类似于圆弧与切线的效果。
一般的,通过CornerPathEffect(float radius)指定一个具体的圆弧半径来实例化一个CornerPathEffect。

DashPathEffect:

这个类的作用就是将Path的线段虚线化。
构造函数为DashPathEffect(float[] intervals, float offset),其中intervals为虚线的ON和OFF数组,该数组的length必须大于等于2,phase为绘制时的偏移量。

DiscretePathEffect:

这个类的作用是打散Path的线段,使得在原来路径的基础上发生打散效果。
一般的,通过构造DiscretePathEffect(float segmentLength,float deviation)来构造一个实例,其中,segmentLength指定最大的段长,deviation指定偏离量。

PathDashPathEffect:

这个类的作用是使用Path图形来填充当前的路径,其构造函数为PathDashPathEffect (Path shape, float advance, float phase,PathDashPathEffect.Stylestyle)。
shape则是指填充图形,advance指每个图形间的间距,phase为绘制时的偏移量,style为该类自由的枚举值,有三种情况:Style.ROTATE、Style.MORPH和
Style.TRANSLATE。其中ROTATE的情况下,线段连接处的图形转换以旋转到与下一段移动方向相一致的角度进行转转,MORPH时图形会以发生拉伸或压缩等变形的情况与下一段相连接,TRANSLATE时,图形会以位置平移的方式与下一段相连接。

ComposePathEffect:

组合效果,这个类需要两个PathEffect参数来构造一个实例,ComposePathEffect (PathEffect outerpe,PathEffect innerpe),表现时,会首先将innerpe表现出来,然后再在innerpe的基础上去增加outerpe的效果。

SumPathEffect:

叠加效果,这个类也需要两个PathEffect作为参数SumPathEffect(PathEffect first,PathEffect second),但与ComposePathEffect不同的是,在表现时,会分别对两个参数的效果各自独立进行表现,然后将两个效果简单的重叠在一起显示出来。

关于参数phase

在存在phase参数的两个类里,如果phase参数的值不停发生改变,那么所绘制的图形也会随着偏移量而不断的发生变动,这个时候,看起来这条线就像动起来了一样。


private float phase;
private PathEffect[] effects;
private int[] colors;

private void drawPathEffect(Canvas canvas) {
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4);
// 创建,并初始化Path
Path path = new Path();
path.moveTo(0, 0);
for (int i = 1; i <= 35; i++) {
// 生成15个点,随机生成它们的坐标,并将它们连成一条Path
path.lineTo(i * 20, (float) Math.random() * 60);
}
// 初始化七个颜色
colors = new int[]{Color.BLACK, Color.BLUE, Color.CYAN, Color.GREEN, Color.MAGENTA, Color.RED,
Color.GRAY};


// 将背景填充成白色
canvas.drawColor(Color.WHITE);
effects = new PathEffect[7];
// -------下面开始初始化7中路径的效果
// 使用路径效果
effects[0] = null;
// 使用CornerPathEffect路径效果
effects[1] = new CornerPathEffect(10);
// 初始化DiscretePathEffect
effects[2] = new DiscretePathEffect(3.0f, 5.0f);
// 初始化DashPathEffect
effects[3] = new DashPathEffect(new float[]{20, 10, 5, 10}, phase);
// 初始化PathDashPathEffect
Path p = new Path();
p.addRect(0, 0, 8, 8, Path.Direction.CCW);
effects[4] = new PathDashPathEffect(p, 12, phase, PathDashPathEffect.Style.ROTATE);
// 初始化PathDashPathEffect
effects[5] = new ComposePathEffect(effects[2], effects[4]);
effects[6] = new SumPathEffect(effects[4], effects[3]);
// 将画布移到8,8处开始绘制
canvas.translate(8, 8);
// 依次使用7中不同路径效果,7种不同的颜色来绘制路径
for (int i = 0; i < effects.length; i++) {
mPaint.setPathEffect(effects[i]);
mPaint.setColor(colors[i]);
canvas.drawPath(path, mPaint);
canvas.translate(0, 200);
}
// 改变phase值,形成动画效果
phase += 1;
invalidate();
}

5.setShadowLayer(float radius, float dx, float dy, int shadowColor)

阴影制作:包括各种形状(矩形,圆形等等),以及文字等等都能设置阴影。


    private void drawShadowLayer(Canvas canvas) {
// 建立Paint 物件
Paint paint1 = new Paint();
paint1.setTextSize(100);
// 设定颜色
paint1.setColor(Color.BLACK);
// 设定阴影(柔边, X 轴位移, Y 轴位移, 阴影颜色)
paint1.setShadowLayer(10, 5, 5, Color.GRAY);
// 实心矩形& 其阴影
canvas.drawText("我爱你", 20,100,paint1);
Paint paint2 = new Paint();
paint2.setTextSize(100);
paint2.setColor(Color.GREEN);
paint2.setShadowLayer(10, 6, 6, Color.GRAY);
canvas.drawText("你真傻", 20,200,paint2);

//cx和cy为圆点的坐标
int radius = 80;
int offest = 40;
int startX = radius + offest;
int startY = radius + offest + 200;

Paint paint3 = new Paint();
//如果不关闭硬件加速,setShadowLayer无效
setLayerType(LAYER_TYPE_SOFTWARE, null);
paint3.setShadowLayer(20, -20, 10, Color.DKGRAY);
canvas.drawCircle(startX, startY, radius, paint3);
paint3.setStyle(Paint.Style.STROKE);
paint3.setStrokeWidth(5);
canvas.drawCircle(startX + radius * 2 + offest, startY, radius, paint3);
}

6.setXfermode(Xfermode xfermode)

Xfermode国外有大神称之为过渡模式,这种翻译比较贴切但恐怕不易理解,大家也可以直接称之为图像混合模式,因为所谓的“过渡”其实就是图像混合的一种,这个方法跟我们上面讲到的setColorFilter蛮相似的。查看API文档发现其果然有三个子类:AvoidXfermode, PixelXorXfermode和PorterDuffXfermode,这三个子类实现的功能要比setColorFilter的三个子类复杂得多。

由于AvoidXfermode, PixelXorXfermode都已经被标注为过时了,所以这次主要研究的是仍然在使用的PorterDuffXfermode:

PorterDuffXfermode

该类同样有且只有一个含参的构造方法PorterDuffXfermode(PorterDuff.Mode mode),虽说构造方法的签名列表里只有一个PorterDuff.Mode的参数,但是它可以实现很多酷毙的图形效果!!而PorterDuffXfermode就是图形混合模式的意思,其概念最早来自于SIGGRAPH的Tomas Proter和Tom Duff,混合图形的概念极大地推动了图形图像学的发展,延伸到计算机图形图像学像Adobe和AutoDesk公司著名的多款设计软件都可以说一定程度上受到影响,而我们PorterDuffXfermode的名字也来源于这俩人的人名组合PorterDuff,那PorterDuffXfermode能做些什么呢?我们先来看一张API DEMO里的图片:

这张图片从一定程度上形象地说明了图形混合的作用,两个图形一圆一方通过一定的计算产生不同的组合效果,在API中Android为我们提供了18种(比上图多了两种ADD和OVERLAY)模式:

ADD:饱和相加,对图像饱和度进行相加,不常用

CLEAR:清除图像

DARKEN:变暗,较深的颜色覆盖较浅的颜色,若两者深浅程度相同则混合

DST:只显示目标图像

DST_ATOP:在源图像和目标图像相交的地方绘制【目标图像】,在不相交的地方绘制【源图像】,相交处的效果受到源图像和目标图像alpha的影响

DST_IN:只在源图像和目标图像相交的地方绘制【目标图像】,绘制效果受到源图像对应地方透明度影响

DST_OUT:只在源图像和目标图像不相交的地方绘制【目标图像】,在相交的地方根据源图像的alpha进行过滤,源图像完全不透明则完全过滤,完全透明则不过滤

DST_OVER:将目标图像放在源图像上方

LIGHTEN:变亮,与DARKEN相反,DARKEN和LIGHTEN生成的图像结果与Android对颜色值深浅的定义有关

MULTIPLY:正片叠底,源图像素颜色值乘以目标图像素颜色值除以255得到混合后图像像素颜色值

OVERLAY:叠加

SCREEN:滤色,色调均和,保留两个图层中较白的部分,较暗的部分被遮盖

SRC:只显示源图像

SRC_ATOP:在源图像和目标图像相交的地方绘制【源图像】,在不相交的地方绘制【目标图像】,相交处的效果受到源图像和目标图像alpha的影响

SRC_IN:只在源图像和目标图像相交的地方绘制【源图像】

SRC_OUT:只在源图像和目标图像不相交的地方绘制【源图像】,相交的地方根据目标图像的对应地方的alpha进行过滤,目标图像完全不透明则完全过滤,完全透明则不过滤

SRC_OVER:将源图像放在目标图像上方

XOR:在源图像和目标图像相交的地方之外绘制它们,在相交的地方受到对应alpha和色值影响,如果完全不透明则相交处完全不绘制


public class PorterDuffView extends View {

Paint mPaint;
Context mContext;
int BlueColor;
int PinkColor;
int mWith;
int mHeight;
public PorterDuffView(Context context) {
super(context);
init(context);
}
public PorterDuffView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}

public PorterDuffView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mHeight = getMeasuredHeight();
mWith = getMeasuredWidth();
}

private void init(Context context) {
mContext = context;
BlueColor = ContextCompat.getColor(mContext, R.color.colorPrimary);
PinkColor = ContextCompat.getColor(mContext, R.color.colorAccent);
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
}
private Bitmap drawRectBm(){
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(BlueColor);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
Bitmap bm = Bitmap.createBitmap(200,200, Bitmap.Config.ARGB_8888);
Canvas cavas = new Canvas(bm);
cavas.drawRect(new RectF(0,0,70,70),paint);
return bm;
}
private Bitmap drawCircleBm(){
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(PinkColor);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
Bitmap bm = Bitmap.createBitmap(200,200, Bitmap.Config.ARGB_8888);
Canvas cavas = new Canvas(bm);
cavas.drawCircle(70,70,35,paint);
return bm;
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setFilterBitmap(false);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextSize(20);
RectF recf = new RectF(20,20,60,60);
mPaint.setColor(BlueColor);
canvas.drawRect(recf,mPaint);
mPaint.setColor(PinkColor);
canvas.drawCircle(100,40,20,mPaint);
@SuppressLint("WrongConstant") int sc = canvas.saveLayer(0, 0,mWith,mHeight, null, Canvas.MATRIX_SAVE_FLAG |
Canvas.CLIP_SAVE_FLAG |
Canvas.HAS_ALPHA_LAYER_SAVE_FLAG |
Canvas.FULL_COLOR_LAYER_SAVE_FLAG |
Canvas.CLIP_TO_LAYER_SAVE_FLAG);
int y = 180;
int x = 50;
for(PorterDuff.Mode mode : PorterDuff.Mode.values()){
if(y >= 900){
y = 180;
x += 200;
}
mPaint.setXfermode(null);
canvas.drawText(mode.name(),x + 100,y,mPaint);
canvas.drawBitmap(drawRectBm(),x,y,mPaint);
mPaint.setXfermode(new PorterDuffXfermode(mode));
canvas.drawBitmap(drawCircleBm(),x,y,mPaint);
y += 120;
}
mPaint.setXfermode(null);
// 还原画布
canvas.restoreToCount(sc);
}
}

收起阅读 »

android音视频基础

一、编码目的编码的目的:压缩,各种音视频的编码方式就是为了让视频体积更小,有利于存储和传输。编码的核心四想就是去除冗余信息。二、编码思路1.空间冗余图像内部相邻元素之间存在较强的相关性,造成信息的冗余。(一块区域颜色一样)2.时间冗余相邻视频帧具有较大的相关性...
继续阅读 »

一、编码目的

编码的目的:压缩,各种音视频的编码方式就是为了让视频体积更小,有利于存储和传输。编码的核心四想就是去除冗余信息。

二、编码思路

1.空间冗余

图像内部相邻元素之间存在较强的相关性,造成信息的冗余。(一块区域颜色一样)

2.时间冗余

相邻视频帧具有较大的相关性,造成信息的冗余。(第一帧和第二帧绝大多数数据一样)

3. 视觉冗余

人类不敏感的信息可以去除。(红色偏点橘色)

4.信息熵冗余 == 熵编码-哈夫曼算法

也称编码冗余,人们用于表达某一信息所需要的比特数总比理论上表示该信息所需要的最少比特数要大,它们之间的差距就是信息熵冗余,或称编码冗余。

5.知识冗余 == 人类(头 身体 腿),汽车,房子 不需要记录

是指在有些图像中还包含与某些验证知识有关的信息。

6.I帧、P帧、B帧压缩思路

I帧:帧内编码帧,关键帧,I帧可以看作一个图像经过压缩之后的产物,可以单独解码出一个完整的图像;(压缩率最低)

P帧:前向预测/参考 编码帧,记录了本帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面。 (压缩率比I帧高,比B帧低 属于 适中情况)

B帧:双向预测/参考 编码帧,记录了本帧与前后帧的差别,解码需要参考前面一个I帧或者P帧,同时也需要后面的P帧才能解码一张完整的图像。 (参考前后的预测得到的,压缩率是最高,但是耗时)

image.png

三、编码标准

1.组织

  • 国际电信联盟:H.264、H.265
  • MPEG系列标准:MPEG1、MPEG2、MPEG4、AVC

AVC == H.264

HEVC == H.265

2.视频编码概念

通过指定的压缩技术,把某一种视频格式文件,转换成另一种视频文件格式文件的方式。

3. H.264分层结构(VCL和NAL)

  • VCL

    VCL(viedo coding layer,视频编码层):负责高效的视频内容展示。

    VCL数据:编码处理的输出,被压缩编码后的视频数据序列。

  • NAL

    NAL(Network Abstraction Layer,网络提取层):以网络所要求的恰当方式对数据进行打包传送,是传输层。不管是网络还是本地都需要通过这一层来传输。

NAL = 一个字节的片头 + 若干的片数据

image.png

传输的是NAL

4. H.264的输出结构

H.264编码器默认的输出为:起始码+NALU。

起始码:0x00000001和0x000001

0x00000001:NALU里有狠多片

0x000001:NALU里只有一片。

5.举例分析H.264文件格式。

image.png

SPS 序列参数集(记录有多少I帧,多少B帧,多少P帧,帧是如何排列) == 7
00 00 00 01 670x67 ---> 2进制01100111 ---> 取低五位 00000111 ---> 十六进制 0x07


PPS 图像参数集(图像宽高信息等) == 8
00 00 00 01 68, 0x68 ---> 2进制01101000---> 取低五位 00001000 ---> 十六进制 0x08


SEI补充信息单元(可以记录坐标信息,人员信息, 后面解码的时候,可以通过代码获取此信息)https://blog.csdn.net/y601500359/article/details/80943990
00 00 01 06 , 0x06 ---> 2进制00000110---> 取低五位00000110 ---> 十六进制 0x06

I帧
00 00 00 65, 0x65 ---> 2进制01100101---> 取低五位00000101 ---> 十六进制 0x05
最终是 5 I帧完整画面出来

P帧
61 -->0x01 重要P帧
41 -->0x01 非重要P帧

B帧
01 -->0x01 B帧

image.png

6.PTS和DTS

DTS:解码时间戳,在什么时候解码这一帧的数据。

PTS:显示时间戳,在什么时候显示这一帧数据。

在没有B帧的时候,DTS和PTS是一样的顺序。

因为B帧的解码需要靠前一帧和后一帧,只要有B帧DTS和PTS就一定会乱。

image.png

GOP:I帧+ 下一个I帧之前的所有B帧和P帧。

i帧=GOP是什么理解的?
SPS PPS I P B P B P B P B I 一组   SPS PPS I P B P B P B P B I 二组
收起阅读 »

Android 是怎么捕捉 java 异常的

val default = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { t, e ->    //...
继续阅读 »
 val default = Thread.getDefaultUncaughtExceptionHandler()

Thread.setDefaultUncaughtExceptionHandler { t, e ->
   // 处理异常
   Log.e("Uncaught", "exception message : "+ e.message)
   // 将异常回执给原注册的 handler
   default.uncaughtException(t, e)
}

以上是很简单的一段代码,经常被用于 java 异常全局捕捉,但我的疑问是,他是怎么实现全局捕捉的,带着这样的疑问,我们来扒一下代码看看。


顺藤摸瓜,我们看看静态方法 getDefaultUncaughtExceptionHandler 是被谁调用的,看了下所有的类调用的类,唯有 ThreadGroup 最靠谱:


image.png


在 parent 为空的情况下,就会调用 getDefaultUncaughtExceptionHandler 来回调异常,然后继续顺藤摸瓜,看看 ThreadGroup 的 uncaughtException 是被谁触发的,搜了一个圈,没有一个靠谱的。在我踌躇时,顺带瞄了一眼注释,奇迹发现:


-   Called by the Java Virtual Machine when a thread in this
- thread group stops because of an uncaught exception, and the thread
- does not have a specific {[@link ](/link%20)Thread.UncaughtExceptionHandler}
- installed.

意思是:当一个未捕获的异常导致线程组中的线程停止时,JVM 会调用该方法。那我们就去搜搜 jvm 的源码,看看是怎么触发这个方法的。


在 Hotspot 虚拟机源码的 thread.cpp 中的 JavaThread::exit 方法发现了这样的一段代码,并且还给出了注释:


image.png


在线程调用 exit 退出时,如果有未捕获的异常,则会调用 Thread.dispatchUncaughtException 方法,然后我们继续跟踪该方法:


image.png


然后调用当前线程的 uncaughtException 分发异常:


image.png


有意思的来了,如果我们没有给当前线程设置 UncaughtExceptionHandler ,则会将这个异常交给当前线程的 ThreadGroup 处理。如果我们给当前线程设置了 UncaughtExceptionHandler,则当前线程发生了异常,永远也不会抛给 getDefaultUncaughtExceptionHandler,该功能适合捕捉当前线程异常来用。


终于回到了我们起初看到的 ThreadGroup.UncaughtExceptionHandler 方法,贴回原来的图继续分析:


image.png


这个地方会继续判断 parent 是否为空,parent 是个 ThreadGroup,ThreadGroup 实现了 Thread.UncaughtExceptionHandler 接口。这里我就直接说答案了,后面再说 ThreadGroup 和 Thread 的关系,最终会走到 system 的 ThreadGroup,system 的 parent 是个空,这时候走 else 分支,获取 Thread 中的 getDefaultUncaughtExceptionHandler 静态变量,触发 uncaughtException 方法,由于我们在 Activity 中设置了这个静态变量,所以,我们收到了这个异常通知。


小知识


1、如何捕获异常不退出


val default = Thread.getDefaultUncaughtExceptionHandler()

Log.e("Uncaught", "Uncaught handler: "+ default)
// Uncaught handler: com.android.internal.os.RuntimeInit$KillApplicationHandler@21f02a3

Thread.setDefaultUncaughtExceptionHandler { t, e ->
   // 将异常回执给原注册的 handler
   // default.uncaughtException(t, e)
}

捕获异常后,什么都不处理。但这样做显得非常不地道,这样会导致其他框架无法通过之前设置的静态变量捕获到异常上报。我打印了一下 default 是 RuntimeInit,该类在捕获到异常后,会做 killProcess。


2、如何捕获指定线程异常:


val thread = Thread {
     val a = 1/0
}
thread.setUncaughtExceptionHandler { t, e ->
       Log.e("Uncaught", "Uncaught trace: "+ e.message)
}
thread.start()

3、ThreadGroup 和 Thread 的关系结构


image.png



  • Thread 的 parent 是在 new Thread 的时候指定的,构造可传自定义的 ThreadGroup,默认是使用创建当前线程的 ThreadGroup

  • Thread 添加进 ThreadGroup 的 Thread[] 数组时机是在调用 start 启动线程的时候做的

  • ThreadGroup 的 parent 是在 new ThreadGroup 的时候指定的,构造可传自定义的 ThreadGroup,默认是使用当前线程的 ThreadGroup

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

Kotlin协程实现原理概述

协程的顶层实现-CPS 现有如下代码: fun test(a: Int, b: Int) { // 求和 var result = a + b // 乘以2 result = result shl 1 // 加2 ...
继续阅读 »

协程的顶层实现-CPS


现有如下代码:


fun test(a: Int, b: Int) {
// 求和
var result = a + b
// 乘以2
result = result shl 1
// 加2
result += 2
// 打印结果
println(result)
}

我们来将代码SRP一下(单一职责):


// 加法
fun sum(a: Int,b: Int) = a + b
// x乘以2
fun double(x: Int) = x shl 1
// x加2
fun add2(x: Int) = x + 2

// 最终的test
fun test(a: Int, b: Int) {
// 从内层依次调用,最终打印
println(add2(double(sum(a,b))))
}

可以看到,我们将原来一坨的方法,抽离成了好几个方法,每个方法干一件事,虽然提高了可读性和可维护性,但是代码复杂了,我们来让它更复杂一点。


上述代码是 让内层方法的返回值 作为参数 传递给外层方法,现在我们 把外层方法作为接口回调 传递给 内层方法:


// 加法,next是加法做完的回调,会传入相加的结果
fun sum(a: Int, b: Int, next: (Int) -> Unit) = a + b
// x乘以2
fun double(x: Int, next: (Int) -> Unit) = x shl 1
// x加2
fun add2(x: Int, next: (Int) -> Unit) = x + 2

// 最终的test
fun test2(a: Int, b: Int) {
// 执行加法
sum(a, b) { sum ->
// 加完执行乘法
double(sum) { double ->
// 乘完就加2
add2(double) { result ->
// 最后打印
println(result)
}
}
}
}

这就是CPS的代码风格:通过接口回调的方式来实现的


假设: 我们上述的几个方法: sum()/double()/add2()都是挂起函数,那么最终也会编译为CPS风格的回调函数方式,也就是:原来看起来同步的代码,经过编译器的"修改",变成了异步的方法,也就是:CPS化了,这就是kotlin协程的顶层实现逻辑。


现在,让我们来验证一下,我们定义一个suspend函数,反编译看下是否真的CPS化了。


// 定义挂起函数
suspend fun test(id: String): String = "hello"

反编译结果如下:


// 参数添加了一个Continuation参数
public final Object test(@NotNull String id, @NotNull Continuation $completion) {
return "hello";
}

可以看到,多了个Continuation参数,这是个接口,是在本次函数执行完毕后执行的回调,内容如下:


public interface Continuation<in T> {
// 保存上下文(比如变量状态)
public val context: CoroutineContext

// 方法执行结束的回调,参数是个范型,用来传递方法执行的结果
public fun resumeWith(result: Result<T>)
}

好,现在我们知道了suspend函数 是通过添加Continuation来实现的,我们来看个具体的业务:


// 根据id获取token
suspend fun getToken(id: String): String = "token"

// 根据token获取info
suspend fun getInfo(token: String): String = "info"

// 测试
suspend fun test() {
// 先获取token,这是耗时请求
val token = getToken("123")
// 再根据token获取info,这也是个耗时请求
val info = getInfo(token)
// 打印
println(info)
}

上述的业务代码很简单,但是前两步都是耗时操作,线程会卡在那里wait吗?显然不会,既然是suspend函数,那么就可以CPS化,等价的CPS代码如下:


// 跟上述相同,传递了Continuation回调
fun getToken(id: String, callback: Continuation<String>): String = "token"

// 跟上述相同,传递了Continuation回调
fun getInfo(token: String, callback: Continuation<String>): String = "info"

// 测试(只写了主线代码)
fun test() {
// 先获取token,传入回调
getToken("123", object : Continuation<String> {
override fun resumeWith(result: Result<String>) {
// 用token获取info,传入回调
val token = result.getOrNull()
getInfo(token!!, object : Continuation<String> {
override fun resumeWith(result: Result<String>) {
// 打印结果
val info = result.getOrNull()
println(info)
}
})
}
})
}

上述就是无suspend的CPS风格代码,通过传入接口回调来实现协程的同步代码风格。


接下来我们来反编译suspend风格代码,看下它里面是怎么调度的。


协程的底层实现-状态机


我们先来简单修改下suspend test函数:


// 没变化
suspend fun getToken(id: String): String = "token"
// 没变化
suspend fun getInfo(token: String): String = "info"

// 添加了局部变量a,看下suspend怎么保存a这个变量
suspend fun test() {
val token = getToken("123") // 挂起点1
var a = 10 // 这里是10
val info = getInfo(token) // 挂起点2,需要将前面的数据保存(比如a),在挂起点之后恢复
println(info)
println(a
}

每个suspend函数调用点,都会生成一个挂起点,在挂起点我们要保存当前的运行状态,比如局部变量等。


反编译后的代码大致如下:


public final Object getToken(String id, Continuation completion) {
return "token";
}

public final Object getInfo(String token, Continuation completion) {
return "info";
}

// 重点函数(伪代码)
public final Object test(Continuation<String>: continuation) {
Continuation cont = new ContinuationImpl(continuation) {
int label; // 保存状态
Object result; // 保存中间结果,还记得那个Result<T>吗,是个泛型,因为泛型擦除,所以为Object,用到就强转
int tempA; // 保存上下文a的值,这个是根据具体代码产生的
};
switch(cont.label) {
case 0 : {
cont.label = 1; //更新label

getToken("123",cont) // 执行对应的操作,注意cont,就是传入的回调
break;
}

case 1 : {
cont.label = 2; // 更新label

// 这是一个挂起点,我们要保存上下文数据,这里就保存a的值
int a = 10;
cont.tempA = a; // 保存a的值

// 获取上一步的结果,因为泛型擦除,需要强转
String token = (Object)cont.result;
getInfo(token, cont); // 执行对应的操作
break;
}

case 2 : {
String info = (Object)cont.result; // 获取上一步的结果
println(info); // 执行对应的操作

// 在挂起点之后,恢复a的值
int a = cont.tempA;
println(a);

return;
}
}
}

我们可以将每个case理解为一个状态,每个case分支对应的语句,理解为一个Continuation实现。


上述伪代码大致描述了协程的调度流程:



  • 1 调用test函数时,需要传入一个Continuation接口,我们会对它进行二次装饰。

  • 2 装饰就是根据函数具体逻辑,在内部添加额外的上下文数据和状态信息(也就是label)。

  • 3 每个状态对应一个Continuation接口,里面会执行对应的业务逻辑。

  • 4 每个状态都会: 保存上下文信息 -> 获取上一个状态的结果 -> 执行本状态业务逻辑 -> 恢复上下文信息。

  • 5 直到最后一个状态对应的逻辑执行完毕。


总结


综上,我们可以归纳以下几点:



  • 1 Kotlin协程没有很"频繁"的切换线程,它是在顶层通过调度方式实现的,所以效率是比较高的。

  • 2 Kotlin中,每个suspend方法,都需要一个Continuation接口实现,用来执行下一个状态的操作;并且,每个suspend方法的调用点都会产生一个挂起点。

  • 3 每个挂起点,都会产生一个label,对应于状态机的一个状态,不同的状态之间,通过Continuation来切换。

  • 4 Kotlin协程会在每个挂起点保存当前的上下文数据,并且在挂起点之后进行恢复。这样,每个状态之间就是相互独立的,可以独立调度。

  • 5 协程的切换,只不过是从一种状态切换到另一种状态,因为不同状态是相互独立的,所以在合适的时机,再切换回来也不会对结果造成影响。

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

Flutter跨进程混合栈渲染的实践——子进程WebView

前言 首先祝大家中秋节快乐,而明天又要上班啦~ 哈哈哈。不过,立此之处,国庆可期矣~ 好了,书归正传,在此我想分享一下关于我在Flutter 安卓端的跨进程渲染所做的一些实践。 起因 随着项目不断的迭代,功能日益复杂,内存占用也与日俱增。在压测过程中,app的...
继续阅读 »

前言


首先祝大家中秋节快乐,而明天又要上班啦~ 哈哈哈。不过,立此之处,国庆可期矣~


好了,书归正传,在此我想分享一下关于我在Flutter 安卓端跨进程渲染所做的一些实践。


起因


随着项目不断的迭代,功能日益复杂,内存占用也与日俱增。在压测过程中,app的崩溃也多是因为各种原因的内存泄漏异常抖动并最终引发OOM而被系统杀死。按技术栈划分主要集中以下两端:




  1. 原生端本身的代码质量(不当设计、图片加载、对象未释放等)所造成,这点通过回溯及找到组内对应同学修复便可快速解决。




  2. 前端的代码质量(亦如上)所引起,这点则需要找到前端组的同学进行修复,但是跨组/部门的无力感我想大家或多或少都会有一些。




不管原因几何,结果都是App崩了,我们一方面找到负责的同学抓紧修复外,另一方面也在思考如何从原生解决(至少隔绝)H5导致的App崩溃问题。


分析


有一定原生开发经验的我们,便想到了子进程。而通过子进程去分担主进程的内存压力,在各大厂也均有应用,可证明它是一个比较成熟的方案,而就单进程Web-View来说,市面上也有不少成功的Android框架及技术方案的分享。


纯原生(Android)应用来讲,因为栈的统一,接入一个子进程web-view,还是比较方便的,大致开启一个子进程,然后startActivity即可,无需关心栈的管理。但是Flutter应用则分为两种栈:


1 Android栈 (管理activity)

2 Flutter栈 (管理flutter的route)

在实际应用中,H5与原生均有复杂的交互,这里不仅体现在功能上的,还包括UI上的。就算不考虑跳转动画的问题,Flutter栈内的叠加(Flutter和H5)就需要一个单独的栈管理器来处理(如Flutter Boost)。


在考虑到投入产出成本以及问题的本质并非栈管理器可以解决的情况下(如 Flutter页面部分是H5等情况),我决定用Flutter自带的Texture Widget进行H5的显示,这样统一了栈的管理,同时Texture Widget可以自由调整大小,做到任意Flutter页面的(部分)嵌入。Texture Widget需要一个Surface,而Surface又具有天然的跨进程属性这无疑大大方便了开发。


实践及结果


经过一段时间的研究和设计,最终有了一个Alpha版的框架,在此我对架构做一下简单的介绍:


flutter_remote_view_framework.png


按进程划分


主要分为两部分:


1. 主进程包含Flutter及相应的平台部分,承担surface的创建、展示、交互等的发起方。

2. 子进程主要包含zygote activity , webview 等。

进程之间通过Binder进行通信。


按流程划分


主要分为三部分:


1. Flutter侧,主要发起创建指令并最终消费子进程的渲染数据。

2. 平台侧,主要承担Flutter与子进程的web-view的通信转发功能,同时承担surface的创建功能,
也是真正与子进程通信的模块。

3. 子进程,主要负责webview的创建,并使用主进程所提供的surface进行H5内容的输出。

所遇到的一些难点


系统弹窗的权限问题


在子进程中使用web-view,并渲染在指定surface 上需要借助virtual displaypresentation,但是如果presentation的创建不是基于activity context,那么则需要一个系统权限才可以正常工作,这对于我们的需求来说,是不可接受的。


为此便创建了一个Zygote activity,它工作于后台,主要责任就是提供一个context和部分presentation的创建工作。同时借助内存泄漏以尽可能长的保留它的存活时间。


交互


由于系统事件(如 触摸)是分发到当前(前台)activity stack的栈顶activity,那么当Zygote activity工作于后台的时候,我们的触摸事件是分发到了Main activity,h5则无法响应任何交互。因此我们需要在主进程做事件的分拣并通过binder转发到子进程,以此来让H5消费到属于它的事件。


触摸事件的分发及错位问题


上面的问题细分后,可以明确我们需要解决Flutter端的H5页面在非栈顶的情况下不能消费事件,因为Flutter所接受的事件由Main Activity提供,所以事件的分发也在此处处理,为此我增加了一个栈协调器(相对于栈管理要简单一些),以获取当前Flutter端的栈情况,并做出正确的分发。


经过实际实践,效果还是不错的,但也发现一个问题:点击坐标错位。经过研究发现,这主要是Flutter端布局和web view端布局不一致导致的,换言之需要计算在Flutter点击时的position相对于那个Texture widget内的相对位置,并做转换再进行分发。


通信


客观的说,这里并没有什么难点,但比较,因为操作涉及到UI,所以不仅要考虑到进程间的通信、线程切换还有各进程的主、子线程的切换。并且按领域进行划分话,又分为共有和私有通信,为此增加了communicate hub以区分各领域的通信。


结果


在一些主要问题解决后,得到了最终的效果图(debug mode):


small.gif


这个Demo并不满足生产,但是验证了它的可行性,而就真正的上线来说,还是有一部分工作要做的,如坐标转换器优化(下一个版本要做的)、协调器、垃圾回收、兜底策略等等。


到此我的分享就结束了,希望对大家有所帮助,同时也殷切希望有大佬能指出设计的不足,谢谢大家的阅读。


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

Android性能优化—StrictMode的使用

概述StrictMode是Android开发过程中一个必不可缺的性能检测工具,他能帮助开发检测出一些不合理的代码块。策略分类StrictMode分为线程策略(ThreadPolicy)和虚拟机策略(VmPolicy)线程策略(ThreadPolicy)线程策略...
继续阅读 »

概述

StrictMode是Android开发过程中一个必不可缺的性能检测工具,他能帮助开发检测出一些不合理的代码块。

策略分类

StrictMode分为线程策略(ThreadPolicy)和虚拟机策略(VmPolicy)

线程策略(ThreadPolicy)

线程策略主要包含了以下几个方面

  • detectNetwork:监测主线程使用网络(重要)
  • detectCustomSlowCalls:监测自定义运行缓慢函数
  • penaltyLog:输出日志
  • penaltyDialog:监测情况时弹出对话框
  • detectDiskReads:检测在UI线程读磁盘操作 (重要)
  • detectDiskWrites:检测在UI线程写磁盘操作(重要)
  • detectResourceMismatches:检测发现资源不匹配 (api>22)
  • detectAll:检测所有支持检测等项目(如果太懒,不想一一列出来,可以通过这个方式)
  • permitDiskReads:允许UI线程在磁盘上读操作

虚拟机策略(VmPolicy)

虚拟机策略主要包含了以下几个方面

  • detectActivityLeaks:检测Activity 的内存泄露情况(重要)(api>10)
  • detectCleartextNetwork:检测明文的网络 (api>22)
  • detectFileUriExposure:检测file://或者是content:// (api>17)
  • detectLeakedClosableObjects:检测资源没有正确关闭(重要)(api>10)
  • detectLeakedRegistrationObjects:检测BroadcastReceiver、ServiceConnection是否被释放 (重要)(api>15)
  • detectLeakedSqlLiteObjects:检测数据库资源是否没有正确关闭(重要)(api>8)
  • setClassInstanceLimit:设置某个类的同时处于内存中的实例上限,可以协助检查内存泄露(重要)
  • penaltyLog:输出日志
  • penaltyDeath:一旦检测到应用就会崩溃

代码

    private void enabledStrictMode() {
//开启Thread策略模式
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectNetwork()//监测主线程使用网络io
.detectCustomSlowCalls()//监测自定义运行缓慢函数
.detectDiskReads() // 检测在UI线程读磁盘操作
.detectDiskWrites() // 检测在UI线程写磁盘操作
.penaltyLog() //写入日志
.penaltyDialog()//监测到上述状况时弹出对话框
.build());
//开启VM策略模式
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects()//监测sqlite泄露
.detectLeakedClosableObjects()//监测没有关闭IO对象
.setClassInstanceLimit(MainActivity.class, 1) // 设置某个类的同时处于内存中的实例上限,可以协助检查内存泄露
.detectActivityLeaks()
.penaltyLog()//写入日志
.penaltyDeath()//出现上述情况异常终止
.build());
}

案例1

public class MainActivity extends Activity {

private Handler mHandler = new Handler();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (BuildConfig.DEBUG) {
enabledStrictMode();
}
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
Log.d("MainActivity", "我来了");
}
}, 10 * 1000);
TextView tv = new TextView(this);
tv.setText("不错啊");
}

private void enabledStrictMode() {
//开启Thread策略模式
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectNetwork()//监测主线程使用网络io
.detectCustomSlowCalls()//监测自定义运行缓慢函数
.detectDiskReads() // 检测在UI线程读磁盘操作
.detectDiskWrites() // 检测在UI线程写磁盘操作
.penaltyLog() //写入日志
.penaltyDialog()//监测到上述状况时弹出对话框
.build());
//开启VM策略模式
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects()//监测sqlite泄露
.detectLeakedClosableObjects()//监测没有关闭IO对象
.setClassInstanceLimit(MainActivity.class, 1) // 设置某个类的同时处于内存中的实例上限,可以协助检查内存泄露
.detectActivityLeaks()
.penaltyLog()//写入日志
.penaltyDeath()//出现上述情况异常终止
.build());
}
}

如代码所示,我在MainActivity(启动模式为singleTask且为app的启动Activity)中创建一个Handler(非静态),然后执行一个delay了10s的任务。
现在我不断的启动和退出MainActivity,结果发现如下图所示

可以看出MainActivity创建了多份实例(此图使用了MAT中的OQL,以后的章节会详细的讲解),我们的预期是只能有一个这样的MainActivity实例。将其中某个对象实例引用路径列出来,见下图。

通过上图我们可以发现,是Handler持有了此MainActivity实例,导致这个MainActivity无法被释放。

改造

public class MainActivity extends Activity {

private static class InnerHandler extends Handler {
private final WeakReference<MainActivity> mWeakreference;

InnerHandler(MainActivity activity) {
mWeakreference = new WeakReference<>(activity);
}

@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
final MainActivity activity = mWeakreference.get();
if (activity == null) {
return;
}
Log.d("MainActivity","执行msg");
}
}

private Handler mHandler = new InnerHandler(this);

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (BuildConfig.DEBUG) {
enabledStrictMode();
}
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
Log.d("MainActivity", "我来了");
}
}, 10 * 1000);
TextView tv = new TextView(this);
tv.setText("我来了");
setContentView(tv);
}

@Override
protected void onDestroy() {
mHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}

private void enabledStrictMode() {
//开启Thread策略模式
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectNetwork()//监测主线程使用网络io
.detectCustomSlowCalls()//监测自定义运行缓慢函数
.detectDiskReads() // 检测在UI线程读磁盘操作
.detectDiskWrites() // 检测在UI线程写磁盘操作
.penaltyLog() //写入日志
.penaltyDialog()//监测到上述状况时弹出对话框
.build());
//开启VM策略模式
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects()//监测sqlite泄露
.detectLeakedClosableObjects()//监测没有关闭IO对象
.setClassInstanceLimit(MainActivity.class, 1) // 设置某个类的同时处于内存中的实例上限,可以协助检查内存泄露
.detectActivityLeaks()
.penaltyLog()//写入日志
.build());
}
}

将Handler实现为静态内部类,且通过弱引用的方式将当前Activity持有,在onDestory出调用removeCallbacksAndMessages(null)方法,此处填null,表示将Handler中所有的消息都清空掉。
运行代码后,通过MAT分析见下图

由图可见,当前有且仅有一个MainActivity,达到代码设计预期。

备注

这个案例在我们分析过程中,会爆出android instances=2; limit=1字样的StrictMode信息,原因是由于我们在启动退出MainActivity的过程中,系统正在回收MainActivity的实例(回收是需要时间的),即此对象正在被FinalizerReference引用,而我们正在启动另外一项MainActivity,故报两个实例。

案例2

public class MainActivity extends Activity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (BuildConfig.DEBUG) {
enabledStrictMode();
}
TextView tv = new TextView(this);
tv.setText("我来了");
setContentView(tv);
newThread();
takeTime();
}

private void newThread() {
for (int i = 0; i < 50; i++) {
new Thread(new Runnable() {
@Override
public void run() {
takeTime();
}
}).start();
}
}

private void takeTime() {
try {
File file = new File(getCacheDir(), "test");
if (file.exists()) {
file.delete();
}
file.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(file);
final String content = "hello 我来了";
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < 100; i++) {
buffer.append(content);
}
fileOutputStream.write(buffer.toString().getBytes());
fileOutputStream.flush();
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
protected void onDestroy() {
super.onDestroy();
}

private void enabledStrictMode() {
//开启Thread策略模式
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectNetwork()//监测主线程使用网络io
.detectCustomSlowCalls()//监测自定义运行缓慢函数
.detectDiskReads() // 检测在UI线程读磁盘操作
.detectDiskWrites() // 检测在UI线程写磁盘操作
.penaltyLog() //写入日志
.penaltyDialog()//监测到上述状况时弹出对话框
.build());
//开启VM策略模式
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects()//监测sqlite泄露
.detectLeakedClosableObjects()//监测没有关闭IO对象
.setClassInstanceLimit(MainActivity.class, 1) // 设置某个类的同时处于内存中的实例上限,可以协助检查内存泄露
.detectActivityLeaks()
.penaltyLog()//写入日志
.build());
}
}

运行以上代码,弹出警告对话框

点击确定后,查看StrictMode日志见附图

从日志信息我们可以得到,程序在createNewFile、openFile、writeFile都花了75ms时间,这对于程序来说是一个较为耗时的操作。接着继续我们的日志

从字面上意思我们知道,是文件流没有关闭,通过日志我们能很快的定位问题点:

      fileOutputStream.write(buffer.toString().getBytes());
fileOutputStream.flush();

文件流flush后,没有执行close方法,这样会导致这个文件资源一直被此对象持有,资源得不到释放,造成内存及资源浪费。

总结

StrictMode除了上面的案例情况,还可以检测对IO、网络、数据库等相关操作,而这些操作恰恰是Android开发过程中影响App性能最常见因素(都比较耗时、CPU占用时间、占用大量内存),所以在开发过程中时刻关注StrictMode变化是一个很好的习惯——一方面可以检测项目组员代码质量,另一方面也可以让自己在Android开发过程中形成一些良好的写代码的思维方式。在StrictMode检测过程中,我们要时刻关注日志的变换(如方法执行时间长短),尤其要对那些红色的日志引起注意,因为这些方法引发的问题是巨大的。

收起阅读 »

写个图片加载框架

假如让你自己写个图片加载框架,你会考虑哪些问题?首先,梳理一下必要的图片加载框架的需求:异步加载:线程池切换线程:Handler,没有争议吧缓存:LruCache、DiskLruCache防止OOM:软引用、LruCache、图片压缩、Bitmap像素存储位置...
继续阅读 »

    假如让你自己写个图片加载框架,你会考虑哪些问题?

    首先,梳理一下必要的图片加载框架的需求:

    • 异步加载:线程池
    • 切换线程:Handler,没有争议吧
    • 缓存:LruCache、DiskLruCache
    • 防止OOM:软引用、LruCache、图片压缩、Bitmap像素存储位置
    • 内存泄露:注意ImageView的正确引用,生命周期管理
    • 列表滑动加载的问题:加载错乱、队满任务过多问题

    当然,还有一些不是必要的需求,例如加载动画等。

    2.1 异步加载:

    线程池,多少个?

    缓存一般有三级,内存缓存、硬盘、网络。

    由于网络会阻塞,所以读内存和硬盘可以放在一个线程池,网络需要另外一个线程池,网络也可以采用Okhttp内置的线程池。

    读硬盘和读网络需要放在不同的线程池中处理,所以用两个线程池比较合适。

    Glide 必然也需要多个线程池,看下源码是不是这样

    public final class GlideBuilder {
    ...
    private GlideExecutor sourceExecutor; //加载源文件的线程池,包括网络加载
    private GlideExecutor diskCacheExecutor; //加载硬盘缓存的线程池
    ...
    private GlideExecutor animationExecutor; //动画线程池

    Glide使用了三个线程池,不考虑动画的话就是两个。

    2.2 切换线程:

    图片异步加载成功,需要在主线程去更新ImageView,

    无论是RxJava、EventBus,还是Glide,只要是想从子线程切换到Android主线程,都离不开Handler。

    看下Glide 相关源码:

        class EngineJob<R> implements DecodeJob.Callback<R>,Poolable {
    private static final EngineResourceFactory DEFAULT_FACTORY = new EngineResourceFactory();
    //创建Handler
    private static final Handler MAIN_THREAD_HANDLER =
    new Handler(Looper.getMainLooper(), new MainThreadCallback());

    问RxJava是完全用Java语言写的,那怎么实现从子线程切换到Android主线程的? 依然有很多3-6年的开发答不上来这个很基础的问题,而且只要是这个问题回答不出来的,接下来有关于原理的问题,基本都答不上来。

    有不少工作了很多年的Android开发不知道鸿洋、郭霖、玉刚说,不知道掘金是个啥玩意,内心估计会想是不是还有叫掘银掘铁的(我不知道有没有)。

    我想表达的是,干这一行,真的是需要有对技术的热情,不断学习,不怕别人比你优秀,就怕比你优秀的人比你还努力,而你却不知道

    2.3 缓存

    我们常说的图片三级缓存:内存缓存、硬盘缓存、网络。

    2.3.1 内存缓存

    一般都是用LruCache

    Glide 默认内存缓存用的也是LruCache,只不过并没有用Android SDK中的LruCache,不过内部同样是基于LinkHashMap,所以原理是一样的。

    // -> GlideBuilder#build
    if (memoryCache == null) {
    memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
    }

    既然说到LruCache ,必须要了解一下LruCache的特点和源码:

    为什么用LruCache?

    LruCache 采用最近最少使用算法,设定一个缓存大小,当缓存达到这个大小之后,会将最老的数据移除,避免图片占用内存过大导致OOM。

    LruCache 源码分析
        public class LruCache<K, V> {
    // 数据最终存在 LinkedHashMap 中
    private final LinkedHashMap<K, V> map;
    ...
    public LruCache(int maxSize) {
    if (maxSize <= 0) {
    throw new IllegalArgumentException("maxSize <= 0");
    }
    this.maxSize = maxSize;
    // 创建一个LinkedHashMap,accessOrder 传true
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
    ...

    LruCache 构造方法里创建一个LinkedHashMap,accessOrder 参数传true,表示按照访问顺序排序,数据存储基于LinkedHashMap。

    先看看LinkedHashMap 的原理吧

    LinkedHashMap 继承 HashMap,在 HashMap 的基础上进行扩展,put 方法并没有重写,说明LinkedHashMap遵循HashMap的数组加链表的结构

    LinkedHashMap重写了 createEntry 方法。

    看下HashMap 的 createEntry 方法

    void createEntry(int hash, K key, V value, int bucketIndex) {
    HashMapEntry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
    size++;
    }

    HashMap的数组里面放的是HashMapEntry 对象

    看下LinkedHashMap 的 createEntry方法

    void createEntry(int hash, K key, V value, int bucketIndex) {
    HashMapEntry<K,V> old = table[bucketIndex];
    LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
    table[bucketIndex] = e; //数组的添加
    e.addBefore(header); //处理链表
    size++;
    }

    LinkedHashMap的数组里面放的是LinkedHashMapEntry对象

    LinkedHashMapEntry

    private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
    // These fields comprise the doubly linked list used for iteration.
    LinkedHashMapEntry<K,V> before, after; //双向链表

    private void remove() {
    before.after = after;
    after.before = before;
    }

    private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
    after = existingEntry;
    before = existingEntry.before;
    before.after = this;
    after.before = this;
    }

    LinkedHashMapEntry继承 HashMapEntry,添加before和after变量,所以是一个双向链表结构,还添加了addBeforeremove 方法,用于新增和删除链表节点。

    LinkedHashMapEntry#addBefore
    将一个数据添加到Header的前面

    private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
    after = existingEntry;
    before = existingEntry.before;
    before.after = this;
    after.before = this;
    }

    existingEntry 传的都是链表头header,将一个节点添加到header节点前面,只需要移动链表指针即可,添加新数据都是放在链表头header 的before位置,链表头节点header的before是最新访问的数据,header的after则是最旧的数据。

    再看下LinkedHashMapEntry#remov

    在Bitmap构造方法创建了一个 BitmapFinalizer类,重写finalize 方法,在java层Bitmap被回收的时候,BitmapFinalizer 对象也会被回收,finalize 方法肯定会被调用,在里面释放native层Bitmap对象。

    6.0 之后做了一些变化,BitmapFinalizer 没有了,被NativeAllocationRegistry取代。

    例如 8.0 Bitmap构造方法

        Bitmap(long nativeBitmap, int width, int height, int density,
    boolean isMutable, boolean requestPremultiplied,
    byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {

    ...
    mNativePtr = nativeBitmap;
    long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
    // 创建NativeAllocationRegistry这个类,调用registerNativeAllocation 方法
    NativeAllocationRegistry registry = new NativeAllocationRegistry(
    Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
    registry.registerNativeAllocation(this, nativeBitmap);
    }

    NativeAllocationRegistry 就不分析了, 不管是BitmapFinalizer 还是NativeAllocationRegistry,目的都是在java层Bitmap被回收的时候,将native层Bitmap对象也回收掉。 一般情况下我们无需手动调用recycle方法,由GC去盘它即可。

    上面分析了Bitmap像素存储位置,我们知道,Android 8.0 之后Bitmap像素内存放在native堆,Bitmap导致OOM的问题基本不会在8.0以上设备出现了(没有内存泄漏的情况下),那8.0 以下设备怎么办?赶紧升级或换手机吧~

    我们换手机当然没问题,但是并不是所有人都能跟上Android系统更新的步伐,所以,问题还是要解决~

    Fresco 之所以能跟Glide 正面交锋,必然有其独特之处,文中开头列出 Fresco 的优点是:“在5.0以下(最低2.3)系统,Fresco将图片放到一个特别的内存区域(Ashmem区)” 这个Ashmem区是一块匿名共享内存,Fresco 将Bitmap像素放到共享内存去了,共享内存是属于native堆内存。

    Fresco 关键源码在 PlatformDecoderFactory 这个类

    8.0 先不看了,看一下 4.4 以下是怎么得到Bitmap的,看下GingerbreadPurgeableDecoder这个类有个获取Bitmap的方法

    //GingerbreadPurgeableDecoder
    private Bitmap decodeFileDescriptorAsPurgeable(
    CloseableReference<PooledByteBuffer> bytesRef,
    int inputLength,
    byte[] suffix,
    BitmapFactory.Options options) {
    // MemoryFile :匿名共享内存
    MemoryFile memoryFile = null;
    try {
    //将图片数据拷贝到匿名共享内存
    memoryFile = copyToMemoryFile(bytesRef, inputLength, suffix);
    FileDescriptor fd = getMemoryFileDescriptor(memoryFile);
    if (mWebpBitmapFactory != null) {
    // 创建Bitmap,Fresco自己写了一套创建Bitmap方法
    Bitmap bitmap = mWebpBitmapFactory.decodeFileDescriptor(fd, null, options);
    return Preconditions.checkNotNull(bitmap, "BitmapFactory returned null");
    } else {
    throw new IllegalStateException("WebpBitmapFactory is null");
    }
    }
    }

    捋一捋,4.4以下,Fresco 使用匿名共享内存来保存Bitmap数据,首先将图片数据拷贝到匿名共享内存中,然后使用Fresco自己写的加载Bitmap的方法。

    Fresco对不同Android版本使用不同的方式去加载Bitmap,至于4.4-5.0,5.0-8.0,8.0 以上,对应另外三个解码器,大家可以从PlatformDecoderFactory 这个类入手,自己去分析,思考为什么不同平台要分这么多个解码器,8.0 以下都用匿名共享内存不好吗?期待你在评论区跟大家分享~

    2.5 ImageView 内存泄露

    曾经在Vivo驻场开发,带有头像功能的页面被测出内存泄漏,原因是SDK中有个加载网络头像的方法,持有ImageView引用导致的。

    当然,修改也比较简单粗暴,将ImageView用WeakReference修饰就完事了。

    事实上,这种方式虽然解决了内存泄露问题,但是并不完美,例如在界面退出的时候,我们除了希望ImageView被回收,同时希望加载图片的任务可以取消,队未执行的任务可以移除。

    Glide的做法是监听生命周期回调,看 RequestManager 这个类

    public void onDestroy() {
    targetTracker.onDestroy();
    for (Target<?> target : targetTracker.getAll()) {
    //清理任务
    clear(target);
    }
    targetTracker.clear();
    requestTracker.clearRequests();
    lifecycle.removeListener(this);
    lifecycle.removeListener(connectivityMonitor);
    mainHandler.removeCallbacks(addSelfToLifecycle);
    glide.unregisterRequestManager(this);
    }

    在Activity/fragment 销毁的时候,取消图片加载任务,细节大家可以自己去看源码。

    2.6 列表加载问题

    图片错乱

    由于RecyclerView或者LIstView的复用机制,网络加载图片开始的时候ImageView是第一个item的,加载成功之后ImageView由于复用可能跑到第10个item去了,在第10个item显示第一个item的图片肯定是错的。

    常规的做法是给ImageView设置tag,tag一般是图片地址,更新ImageView之前判断tag是否跟url一致。

    当然,可以在item从列表消失的时候,取消对应的图片加载任务。要考虑放在图片加载框架做还是放在UI做比较合适。

    线程池任务过多

    列表滑动,会有很多图片请求,如果是第一次进入,没有缓存,那么队列会有很多任务在等待。所以在请求网络图片之前,需要判断队列中是否已经存在该任务,存在则不加到队列去。

    总结

    本文通过Glide开题,分析一个图片加载框架必要的需求,以及各个需求涉及到哪些技术和原理。

    • 异步加载:最少两个线程池
    • 切换到主线程:Handler
    • 缓存:LruCache、DiskLruCache,涉及到LinkHashMap原理
    • 防止OOM:软引用、LruCache、图片压缩没展开讲、Bitmap像素存储位置源码分析、Fresco部分源码分析
    • 内存泄露:注意ImageView的正确引用,生命周期管理

收起阅读 »

Android 高级UI 事件传递机制

1.View的事件分发流程dispatchTouchEvent():onTouchListener--->onTouch方法onTouchEventonClickListener--->onClick方法ListenerInfo static...
继续阅读 »

1.View的事件分发

流程
  1. dispatchTouchEvent():
  2. onTouchListener--->onTouch方法
  3. onTouchEvent
  4. onClickListener--->onClick方法

ListenerInfo


    static class ListenerInfo {
/**
* Listener used to dispatch focus change events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/

protected OnFocusChangeListener mOnFocusChangeListener;

/**
* Listeners for layout change events.
*/

private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;

protected OnScrollChangeListener mOnScrollChangeListener;

/**
* Listeners for attach events.
*/

private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;

/**
* Listener used to dispatch click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/

public OnClickListener mOnClickListener;

/**
* Listener used to dispatch long click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/

protected OnLongClickListener mOnLongClickListener;

/**
* Listener used to dispatch context click events. This field should be made private, so it
* is hidden from the SDK.
* {@hide}
*/

protected OnContextClickListener mOnContextClickListener;

/**
* Listener used to build the context menu.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/

protected OnCreateContextMenuListener mOnCreateContextMenuListener;

private OnKeyListener mOnKeyListener;

private OnTouchListener mOnTouchListener;

private OnHoverListener mOnHoverListener;

private OnGenericMotionListener mOnGenericMotionListener;

private OnDragListener mOnDragListener;

private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;

OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;

OnCapturedPointerListener mOnCapturedPointerListener;
}

dispatchTouchEvent


    public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}

boolean result = false;

if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}

if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

if (!result && onTouchEvent(event)) {
result = true;
}
}

if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}

// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}

return result;
}

onTouchEvent

结论:
  1. 控件的Listener事件触发的顺序是onTouch,再onClick
  2. 控件的onTouch返回true,将会使onClick的事件没有了---阻止了事件的传递。返回false,才会传递onClick事件 。
  3. 如果onTouchListener的onTouch方法返回了true,那么view里面的onTouchEvent就不会被调用了。顺序dispatchTouchEvent-->onTouchListener---return false-->onTouchEvent
  4. 如果view为disenable,则:onTouchListener里面不会执行,但是会执行onTouchEvent(event)方法
  5. onTouchEvent方法中的ACTION_UP分支中触发onclick事件监听
    onTouchListener-->onTouch方法返回true,消耗次事件。down,但是up事件是无法到达onClickListener.
    onTouchListener-->onTouch方法返回false,不会消耗此事件

2.ViewGroup+View的事件分发

ViewGroup继承View

  1. dispatchTouchEvent()
  2. onInterceptTouchEvent() (拦截触摸,ViewGroup独有)
  3. onTouchEvent()
dispatchTouchEvent

  @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
onInterceptTouchEvent

  public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
示例

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.RelativeLayout;

/**
* Created by Xionghu on 2018/6/6.
* Desc:
*/


public class MyRelativeLayout extends RelativeLayout {
public MyRelativeLayout(Context context) {
super(context);
}

public MyRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("kpioneer", "dispatchTouchEvent:action--"+ev.getAction()+"---view:MyRelativeLayout");
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("kpioneer", "onInterceptTouchEvent:action--"+ev.getAction()+"---view:MyRelativeLayout");
return super.onInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("kpioneer", "onTouchEvent:action--"+event.getAction()+"---view:MyRelativeLayout");
return super.onTouchEvent(event);
}
}

点击Button


06-06 11:05:18.340 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--0---view:MyRelativeLayout
06-06 11:05:18.340 27438-27438/com.haocai.eventdemo I/kpioneer: onInterceptTouchEvent:action--0---view:MyRelativeLayout
06-06 11:05:18.340 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--0
06-06 11:05:18.340 27438-27438/com.haocai.eventdemo I/kpioneer: OnTouchListener:acton--0----view:com.haocai.eventdemo.MyButton{8e32527 VFED..C.. ........ 0,42-264,186 #7f070022 app:id/button1}
06-06 11:05:18.340 27438-27438/com.haocai.eventdemo I/kpioneer: onTouchEvent:action--0
06-06 11:05:18.370 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--2---view:MyRelativeLayout
06-06 11:05:18.370 27438-27438/com.haocai.eventdemo I/kpioneer: onInterceptTouchEvent:action--2---view:MyRelativeLayout
06-06 11:05:18.370 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--2
06-06 11:05:18.370 27438-27438/com.haocai.eventdemo I/kpioneer: OnTouchListener:acton--2----view:com.haocai.eventdemo.MyButton{8e32527 VFED..C.. ...P.... 0,42-264,186 #7f070022 app:id/button1}
06-06 11:05:18.380 27438-27438/com.haocai.eventdemo I/kpioneer: onTouchEvent:action--2
06-06 11:05:18.390 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--2---view:MyRelativeLayout
06-06 11:05:18.390 27438-27438/com.haocai.eventdemo I/kpioneer: onInterceptTouchEvent:action--2---view:MyRelativeLayout
06-06 11:05:18.390 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--2
06-06 11:05:18.390 27438-27438/com.haocai.eventdemo I/kpioneer: OnTouchListener:acton--2----view:com.haocai.eventdemo.MyButton{8e32527 VFED..C.. ...P.... 0,42-264,186 #7f070022 app:id/button1}
06-06 11:05:18.390 27438-27438/com.haocai.eventdemo I/kpioneer: onTouchEvent:action--2
06-06 11:05:18.400 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--2---view:MyRelativeLayout
06-06 11:05:18.400 27438-27438/com.haocai.eventdemo I/kpioneer: onInterceptTouchEvent:action--2---view:MyRelativeLayout
06-06 11:05:18.400 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--2
06-06 11:05:18.400 27438-27438/com.haocai.eventdemo I/kpioneer: OnTouchListener:acton--2----view:com.haocai.eventdemo.MyButton{8e32527 VFED..C.. ...P.... 0,42-264,186 #7f070022 app:id/button1}
06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: onTouchEvent:action--2
06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--1---view:MyRelativeLayout
06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: onInterceptTouchEvent:action--1---view:MyRelativeLayout
06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: dispatchTouchEvent:action--1
06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: OnTouchListener:acton--1----view:com.haocai.eventdemo.MyButton{8e32527 VFED..C.. ...P.... 0,42-264,186 #7f070022 app:id/button1}
06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: onTouchEvent:action--1
06-06 11:05:18.410 27438-27438/com.haocai.eventdemo I/kpioneer: OnClickListener----view:com.haocai.eventdemo.MyButton{8e32527 VFED..C.. ...P.... 0,42-264,186 #7f070022 app:id/button1}
该例子中Button事件点击:
  1. 先接触到事件的是父容器
  2. ViewGroup顺序:dispatchTouchEvent--->onInterceptTouchevent-->dispatchTouchEvent(Button)-->OnTouchListener(Button) --->return false---> onTouchEvent(Button)(消耗事件) ----- onTouchevent(该示例父布局并没调用)
收起阅读 »

Android View post 方法

解析View.post方法。分析一下这个方法的流程。 说起post方法,我们很容易联想到Handler的post方法,都是接收一个Runnable对象。那么这两个方法有啥不同呢? Handler的post方法 先来简单看一下Handler的post(Runna...
继续阅读 »

解析View.post方法。分析一下这个方法的流程。


说起post方法,我们很容易联想到Handlerpost方法,都是接收一个Runnable对象。那么这两个方法有啥不同呢?


Handler的post方法


先来简单看一下Handlerpost(Runnable)方法。这个方法是将一个Runnable加到消息队列中,并且会在这个handler关联的线程里执行。


下面是关联的部分源码。可以看到传入的Runnable对象,装入Message后,被添加进了queue队列中。


Handler 有关的部分源码


    // android.os Handler 有关的部分源码
public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage(r), 0);
}

private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();

if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

具体流程,可以看handler介绍


View的post方法


我们直接跟着post的源码走。


public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}

// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}

private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}

可以看到一开始就查询是否有attachInfo,如果有,则用attachInfo.mHandler来执行这个任务。


如果没有attachInfo,则添加到View自己的mRunQueue中。确定运行的线程后,再执行任务。


post(Runnable action)的返回boolean值,如果为true,表示任务被添加到消息队列中了。
如果是false,通常表示消息队列关联的looper正在退出。


那么我们需要了解AttachInfoHandlerActionQueue


AttachInfo


AttachInfoView的静态内部类。View关联到父window后,用这个类来存储一些信息。


AttachInfo存储的一部分信息如下:



  • WindowId mWindowId window的标志

  • View mRootView 最顶部的view

  • Handler mHandler 这个handler可以用来处理任务


HandlerActionQueue


View还没有handler的时候,拿HandlerActionQueue来缓存任务。HandlerAction是它的静态内部类,存储Runnable与延时信息。


public class HandlerActionQueue {
private HandlerAction[] mActions;

public void post(Runnable action)
public void executeActions(Handler handler)
// ...

private static class HandlerAction {
final Runnable action;
final long delay;
// ...
}
}

View的mRunQueue


将任务(runnable)排成队。当View关联上窗口并且有handler后,再执行这些任务。


/**
* Queue of pending runnables. Used to postpone calls to post() until this
* view is attached and has a handler.
*/
private HandlerActionQueue mRunQueue;

这个mRunQueue里存储的任务啥时候被执行?我们关注dispatchAttachedToWindow方法。


void dispatchAttachedToWindow(AttachInfo info, int visibility) {
// ...
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
// ...
}

这个方法里调用了mRunQueue.executeActions


executeActions(Handler handler)方法实际上是用传入的handler处理队列中的任务。


而这个dispatchAttachedToWindow会被ViewGroup中被调用。


或者是ViewRootImpl中调用


host.dispatchAttachedToWindow(mAttachInfo, 0);

小结


View的post方法,实际上是使用了AttachInfohandler


如果View当前还没有AttachInfo,则把任务添加到了View自己的HandlerActionQueue队列中,然后在dispatchAttachedToWindow中把任务交给传入的AttachInfohandler。也可以这样认为,View.post用的就是handler.post


我们在获取View的宽高时,会利用View的post方法,就是等View真的关联到window再拿宽高信息。


流程图归纳如下


post-flow1.png


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

【开源项目】简单易用的Compose版StateLayout,了解一下~

前言 在页面中常常需要展示网络请求状态,以带来更好的用户体验,具体来说通常有加载中,加载失败,加载为空,加载成功等状态. 在XML中我们通常用一个ViewGroup封装各种状态来实现,那么使用Compose该如何实现这种效果呢? 本文主要介绍Compose如何...
继续阅读 »

前言


在页面中常常需要展示网络请求状态,以带来更好的用户体验,具体来说通常有加载中加载失败加载为空加载成功等状态.

XML中我们通常用一个ViewGroup封装各种状态来实现,那么使用Compose该如何实现这种效果呢?

本文主要介绍Compose如何封装一个简单易用的StateLayout,有兴趣的同学可以点个Star : Compose版StateLayout


效果图


首先看下最终的效果图


特性



  1. 支持配置全局默认布局,如默认加载中,默认成功失败等

  2. 支持自定义默认样式文案,图片等细节

  3. 支持完全自定义样式,如自定义加载中样式

  4. 支持自定义处理点击重试事件

  5. 完全使用数据驱动,使用简单,接入方便


使用


接入


第 1 步:在工程的build.gradle中添加:


allprojects {
repositories {
...
mavenCentral()
}
}

第2步:在应用的build.gradle中添加:


dependencies {
implementation 'io.github.shenzhen2017:compose-statelayout:1.0.0'
}

简单使用


定义全局样式


在框架中没有指定任何默认样式,因此你需要自定义自己的默认加载中,加载失败等页面样式

同时需要自定义传给自定义样式的数据结构类型,方便数据驱动


data class StateData(
val tipTex: String? = null,
val tipImg: Int? = null,
val btnText: String? = null
)

@Composable
fun DefaultStateLayout(
modifier: Modifier = Modifier,
pageStateData: PageStateData,
onRetry: OnRetry = { },
loading: @Composable (StateLayoutData) -> Unit = { DefaultLoadingLayout(it) },
empty: @Composable (StateLayoutData) -> Unit = { DefaultEmptyLayout(it) },
error: @Composable (StateLayoutData) -> Unit = { DefaultErrorLayout(it) },
content: @Composable () -> Unit = { }
) {
ComposeStateLayout(
modifier = modifier,
pageStateData = pageStateData,
onRetry = onRetry,
loading = { loading(it) },
empty = { empty(it) },
error = { error(it) },
content = content
)
}

如上所示,初始化时我们主要需要做以下事



  1. 自定义默认加载中,加载失败,加载为空等样式

  2. 自定义StateData,即传给默认样式的数据结构,比如文案,图片等,这样后续需要修改的时候只需修改StateData即可


直接使用


如果我们直接使用默认样式,直接如下使用即可


@Composable
fun StateDemo() {
var pageStateData by remember {
mutableStateOf(PageState.CONTENT.bindData())
}
DefaultStateLayout(
modifier = Modifier.fillMaxSize(),
pageStateData = pageStateData,
onRetry = {
pageStateData = PageState.LOADING.bindData()
}
) {
//Content
}
}

如上所示,可以直接使用,如果需要修改状态,修改pageStateData即可


自定义文案


如果我们需要自定义文案或者图片等细节,可简单直接修改StateData即可


fun StateDemo() {
var pageStateData by remember {
mutableStateOf(PageState.CONTENT.bindData())
}
//....
pageStateData = PageState.LOADING.bindData(StateData(tipTex = "自定义加载中文案"))
}

自定义布局


有时页面的加载中样式与全局的并不一样,这就需要自定义布局样式了


@Composable
fun StateDemo() {
var pageStateData by remember {
mutableStateOf(PageState.CONTENT.bindData())
}
DefaultStateLayout(
modifier = Modifier.fillMaxSize(),
pageStateData = pageStateData,
loading = { CustomLoadingLayout(it) },
onRetry = {
pageStateData = PageState.LOADING.bindData()
}
) {
//Content
}
}

主要原理


其实Compose要实现不同的状态非常简单,传入不同的数据即可,如下所示:


    Box(modifier = modifier) {
when (pageStateData.status) {
PageState.LOADING -> loading()
PageState.EMPTY -> empty()
PageState.ERROR -> error()
PageState.CONTENT -> content()
}
}

其实代码非常简单,但是这段代码是个通用逻辑,如果每个页面都要写这一段代码可能也挺烦的

所以这段代码其实是模板代码,我们想到Scaffold脚手架,提供了组合各个组件的API,包括标题栏、底部栏、SnackBar(类似吐司功能)、浮动按钮、抽屉组件、剩余内容布局等,让我们可以快速定义一个基本的页面结构。


仿照Scaffold,我们也可以定义一个模板组件,用户可以传入自定义的looading,empty,error,content等组件,再将它们组合起来,这样就形成了ComposeStateLayout


data class PageStateData(val status: PageState, val tag: Any? = null)

data class StateLayoutData(val pageStateData: PageStateData, val retry: OnRetry = {})

typealias OnRetry = (PageStateData) -> Unit

@Composable
fun ComposeStateLayout(
modifier: Modifier = Modifier,
pageStateData: PageStateData,
onRetry: OnRetry = { },
loading: @Composable (StateLayoutData) -> Unit = {},
empty: @Composable (StateLayoutData) -> Unit = {},
error: @Composable (StateLayoutData) -> Unit = {},
content: @Composable () -> Unit = { }
) {
val stateLayoutData = StateLayoutData(pageStateData, onRetry)
Box(modifier = modifier) {
when (pageStateData.status) {
PageState.LOADING -> loading(stateLayoutData)
PageState.EMPTY -> empty(stateLayoutData)
PageState.ERROR -> error(stateLayoutData)
PageState.CONTENT -> content()
}
}
}

如上所示,代码很简单,主要需要注意以下几点:



  1. PageStateDatatag即传递给自定义loading等页面的信息,为Any类型,没有任何限制,用户可灵活处理

  2. 自定义loading等页面也传入了OnRetry,因此我们也可以处理自定义点击事件


总结


本文主要实现了一个Compose版的StateLayout,它具有以下特性



  1. 支持配置全局默认布局,如默认加载中,默认成功失败等

  2. 支持自定义默认样式文案,图片等细节

  3. 支持完全自定义样式,如自定义加载中样式

  4. 支持自定义处理点击重试事件

  5. 完全使用数据驱动,使用简单,接入方便


项目地址


简单易用的Compose版StateLayout

开源不易,如果项目对你有所帮助,欢迎点赞,Star,收藏~


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

ViewPager2&TabLayout:拓展出一个文本选中放大效果

ViewPager2正式推出已经一年多了,虽然不如3那样新潮,但是也不如老前辈ViewPager那样有众多开源库拥簇,比如它的灵魂伴侣TabLayout明显后援不足,好在TabLayout自身够硬! ViewPager2灵魂伴侣是官方提供的: com.goog...
继续阅读 »

ViewPager2正式推出已经一年多了,虽然不如3那样新潮,但是也不如老前辈ViewPager那样有众多开源库拥簇,比如它的灵魂伴侣TabLayout明显后援不足,好在TabLayout自身够硬!


ViewPager2灵魂伴侣是官方提供的:


com.google.android.material.tabs.TabLayout

TabLayout 利用其良好的设计,使得自定义非常容易。


像匹配ViewPager的优秀开源库FlycoTabLayout的效果,使用TabLayout都能比较容易的实现:


FlycoTabLayout 演示


image.png


实现上图中的几个常用效果TabLayout 仅需在xml重配置即可


tablayout.gif


不过稍微不同的是,上图中第二第三栏选中后的字体是有放大效果的。


这是利用TabLayout.TabcustomView属性达到的。下文便是实现的思路与过程记录。


正文


思路拆解:



  • 介于此功能耦合点仅仅是TabLayoutMediator,选择使用拓展包装TabLayoutMediator,轻量且无侵入性,API还便捷

  • 自定义TabLayoutMediator,设置customView,放入自己的TextView

  • 内部自动添加一个addOnTabSelectedListener,在选中后使用动画渐进式的改变字体大小,同理取消选中时还原


解决过的坑:



  • TextView的文本在Size改变时,宽度动态变化,调用requestLayout()。Tab栏会因此触发重新测量与重绘,出现短促闪烁。塞两个TextView,一个作为最大边界并且设置INVISIBLE

  • 同样是重测问题,导致TabLayout额外多从头绘制一次Indicator时,直观表现就是每次切换Indicator时,会出现闪现消失。采用自定义了一个ScaleTexViewTabView,动态控制是否触发super.requestLayout


(因为已经准备了两个View,负责展示效果的View最大范围是明确无法超过既定范围的,所以这个办法不算“黑”)




  • 核心API:





fun <T : View> TabLayout.createMediatorByCustomTabView(
vp: ViewPager2,
config: CustomTabViewConfig<T>
): TabLayoutMediator {
return TabLayoutMediator(this, vp) { tab, pos ->
val tabView = config.getCustomView(tab.view.context)
tab.customView = tabView
config.onTabInit(tabView, pos)
}
}

fun TabLayout.createTextScaleMediatorByTextView(
vp: ViewPager2,
config: TextScaleTabViewConfig
): TabLayoutMediator {

val mediator = createMediatorByCustomTabView(vp, config)
...
...
return mediator
}



  • 使用:




val mediator = tabLayout.createTextScaleMediatorByTextView(viewPager2,
object : TextScaleTabViewConfig(scaleConfig) {
override fun onBoundTextViewInit(boundSizeTextView: TextView, position: Int) {
boundSizeTextView.textSizePx = scaleConfig.onSelectTextSize
boundSizeTextView.text = tabs[position]
}
override fun onVisibleTextViewInit(dynamicSizeTextView: TextView, position: Int) {
dynamicSizeTextView.setTextColor(Color.WHITE)
dynamicSizeTextView.text = tabs[position]
}
})
mediator.attach()

整个代码去除通用拓展不过100行左右,不过鉴于其独立性还是要单独发布到基础组件库中。这样组件库中就有两个是单文件的组件了哈哈~


点击直达完整源码~,拷贝即用


END


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

Dialog 按照顺序弹窗

背景: 产品需求,在同一个页面弹窗需要按照顺序实现: 利用PriorityQueue现实,支持相同优先级,按插入时间排序,目前仅支持Activity,不支持Fragment代码: DialogPriorityUtil 实现优先级弹窗/** ...
继续阅读 »

背景: 产品需求,在同一个页面弹窗需要按照顺序

实现: 利用PriorityQueue现实,支持相同优先级,按插入时间排序,目前仅支持Activity,不支持Fragment

代码: DialogPriorityUtil 实现优先级弹窗

/**
* ClassName: DialogPriorityUtil
* Description: show dialog by priority
* author Neo
* since 2021-09-15 20:15
* version 1.0
*/
object DialogPriorityUtil : LifecycleObserver {

private val dialogPriorityQueue = PriorityQueue<PriorityDialogWrapper>()

private var hasDialogShowing = false

@MainThread
fun bindLifeCycle(appCompatActivity: AppCompatActivity) {
appCompatActivity.lifecycle.addObserver(this)
}

@MainThread
fun showDialogByPriority(dialogWrapper: PriorityDialogWrapper? = null) {
if (dialogWrapper != null) {
dialogPriorityQueue.offer(dialogWrapper)
}
if (hasDialogShowing) return
val maxPriority: PriorityDialogWrapper = dialogPriorityQueue.poll() ?: return
if (!maxPriority.isShowing()) {
hasDialogShowing = true
maxPriority.showDialog()
}
maxPriority.setDismissListener {
hasDialogShowing = false
showDialogByPriority()
}
}

@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
dialogPriorityQueue.clear()
}
}
/**
* 定义dialog优先级
* @property priority Int
* @constructor
*/
sealed class DialogPriority(open val priority: Int) {
sealed class HomeMapFragment(override val priority: Int) : DialogPriority(priority) {
/**
* App更新
*/
object UpdateDialog : HomeMapFragment(0)

/**
* 等级提升
*/
object LevelUpDialog : HomeMapFragment(1)

/**
* 金币打卡
*/
object CoinClockInDialog : HomeMapFragment(2)
}
}

/**
* ClassName: PriorityDialogWrapper
* Description: 优先级弹窗包装类
* author Neo
* since 2021-09-15 20:20
* version 1.0
*/
class PriorityDialogWrapper(private val dialog: Dialog, private val dialogPriority: DialogPriority) : Comparable<PriorityDialogWrapper> {

private var dismissCallback: (() -> Unit)? = null

private val timestamp = SystemClock.elapsedRealtimeNanos()

init {
dialog.setOnDismissListener {
dismissCallback?.invoke()
}
}

fun isShowing(): Boolean = dialog.isShowing

fun setDismissListener(callback: () -> Unit) {
this.dismissCallback = callback
}

fun showDialog() {
dialog.show()
}

override fun compareTo(other: PriorityDialogWrapper): Int {
return when {
dialogPriority.priority > other.dialogPriority.priority -> {
// 当前对象比目标对象大,则返回 1
1
}
dialogPriority.priority < other.dialogPriority.priority -> {
// 当前对象比目标对象小,则返回 -1
-1
}
else -> {
// 若是两个对象相等,则返回 0
when {
timestamp > other.timestamp -> {
1
}
timestamp < other.timestamp -> {
-1
}
else -> {
0
}
}
}
}
}
}

使用:

AppCompatActivity

DialogPriorityUtil.bindLifeCycle(this)
DialogPriorityUtil.showDialogByPriority(...)
收起阅读 »

kotlin的协程异步,并发(同步)

一:协程的异步任务private fun task(){ println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, start"...
继续阅读 »

一:协程的异步

任务

private fun task(){
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, start")
Thread.sleep(1000)
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, end")
}

下面使用协程异步的方式,让任务task()在子线程中处理。

方式1:launch()+Dispatchers.IO

launch创建协程;

Dispatchers.IO调度,在子线程处理网络耗时

fun testNotSync() {
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法start")
// 重复执行3次,模拟点击3次
repeat(3) {
CoroutineScope(Dispatchers.IO).launch {
task()
}
}
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法end")

// 防止main函数执行结束,就不管其他线程的打印工作了,哈哈
Thread.sleep(10000)
}

结果:

currentThread:main, time:1631949431058, 方法start
currentThread:main, time:1631949431166, 方法end

currentThread:DefaultDispatcher-worker-3 @coroutine#3, time:1631949431176, start
currentThread:DefaultDispatcher-worker-1 @coroutine#2, time:1631949431182, start
currentThread:DefaultDispatcher-worker-2 @coroutine#1, time:1631949431182, start

currentThread:DefaultDispatcher-worker-3 @coroutine#3, time:1631949432176, end
currentThread:DefaultDispatcher-worker-1 @coroutine#2, time:1631949432182, end
currentThread:DefaultDispatcher-worker-2 @coroutine#1, time:1631949432183, end

显示:主线程内容先执行,然后会在3个子线程异步的执行

方式2:async()+Dispatchers.IO

fun testByCoroutineAsync() {
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法start")
repeat(3){
CoroutineScope(Dispatchers.IO).async {
task()
}
}
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法end")
// 防止main函数执行结束,就不管其他线程的打印工作了,哈哈
Thread.sleep(10000)
}

结果:

currentThread:main @coroutine#1, time:1631957324981, 方法start
currentThread:main @coroutine#1, time:1631957325007, 方法end
currentThread:DefaultDispatcher-worker-1 @coroutine#3, time:1631957325007, start
currentThread:DefaultDispatcher-worker-2 @coroutine#2, time:1631957325007, start
currentThread:DefaultDispatcher-worker-4 @coroutine#4, time:1631957325007, start
currentThread:DefaultDispatcher-worker-1 @coroutine#3, time:1631957326007, end
currentThread:DefaultDispatcher-worker-4 @coroutine#4, time:1631957326007, end
currentThread:DefaultDispatcher-worker-2 @coroutine#2, time:1631957326007, end

显示:主线程内容先执行,然后会在3个子线程异步的执行

看源码发现CoroutineScope.async 等同 CoroutineScope.launch,不同是返回值。

方式3:withContext+Dispatchers.IO

/**
* 单个withContext的异步任务
*/
fun testByWithContext() = runBlocking {
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法start")

withContext(Dispatchers.IO) {
task()
}

println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法end")

// 防止main函数执行结束,就不管其他线程的打印工作了,哈哈
Thread.sleep(10 *1000)
}

结果:

currentThread:main @coroutine#1, time:1631958195591, 方法start
currentThread:DefaultDispatcher-worker-1 @coroutine#1, time:1631958195669, start
currentThread:DefaultDispatcher-worker-1 @coroutine#1, time:1631958196669, end
currentThread:main @coroutine#1, time:1631958196671, 方法end

发现:withContext的task是在子线程中执行,但是也阻塞了main线程,最后执行了"方法end"

因为withContext切io线程后,还挂起了外部的协程(可以理解线程),需要等withCotext执行完成,才会回到原来的协程,也直接可以理解为阻塞了当前的线程。

上面是单个withCotext的异步执行,看多个withContext是怎么样的

fun testByWithContext() = runBlocking {
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法start")
// 重复3次,模拟点击3次
repeat(3) {
println("repeat it = $it")
withContext(Dispatchers.IO) {
task()
}
}

println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法end")

// 防止main函数执行结束,就不管其他线程的打印工作了,哈哈
Thread.sleep(10 *1000)
}

结果:

currentThread:main @coroutine#1, time:1631958027834, 方法start
repeat it = 0
currentThread:DefaultDispatcher-worker-1 @coroutine#1, time:1631958027870, start
currentThread:DefaultDispatcher-worker-1 @coroutine#1, time:1631958028870, end
repeat it = 1
currentThread:DefaultDispatcher-worker-1 @coroutine#1, time:1631958028873, start
currentThread:DefaultDispatcher-worker-1 @coroutine#1, time:1631958029873, end
repeat it = 2
currentThread:DefaultDispatcher-worker-1 @coroutine#1, time:1631958029874, start
currentThread:DefaultDispatcher-worker-1 @coroutine#1, time:1631958030874, end
currentThread:main @coroutine#1, time:1631958030874, 方法end

发现:先main线程执行,然后一个withcontext异步执行完成,才能执行下一个withcontext的异步

实现了多个异步任务的同步,当我们有多个接口请求,需要按顺序执行时,可以使用

二:协程的并发(同步)

Java中并发concurrent的处理,基本使用同步synchronized,Lock,join等来处理。下面我们看看协程怎麽处理的。

1:@Synchronized 注解

我们将上面的任务task修改一下,方法上面加个注解@Synchronized,然后执行launch的异步看能不能同步任务?

使用

/**
* @Synchronized 修改普通函数ok,可以同步
*/
@Synchronized
private fun taskSynchronize(){
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, start")
Thread.sleep(1000)
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, end")
}

测试:aunch异步同时访问taskSynchronize()任务

fun testCoroutineWithSync() {
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法start")
repeat(3){
CoroutineScope(Dispatchers.IO).launch {
taskSynchronize()
}
}
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法end")

// 防止main函数执行结束,就不管其他线程的打印工作了,哈哈
Thread.sleep(10000)
}

结果:

currentThread:main, time:1631959341585, 方法start
currentThread:main, time:1631959341657, 方法end
currentThread:DefaultDispatcher-worker-4 @coroutine#3, time:1631959341657, start
currentThread:DefaultDispatcher-worker-4 @coroutine#3, time:1631959342658, end
currentThread:DefaultDispatcher-worker-1 @coroutine#1, time:1631959342658, start
currentThread:DefaultDispatcher-worker-1 @coroutine#1, time:1631959343658, end
currentThread:DefaultDispatcher-worker-2 @coroutine#2, time:1631959343658, start
currentThread:DefaultDispatcher-worker-2 @coroutine#2, time:1631959344658, end

发现:先main先执行完成,然后每个线程任务,同步执行完成了

问题

当@Synchronized 注解的方法中,有挂起函数且是阻塞的,就不行了

修改一下任务,其中的Thread.sleep(1000)改为delay(1000),看看如何?

/**
* 和方法taskSynchronize(), 不同的是内部使用了delay的挂起函数,而其它会阻塞,需要等它完成后面的才能开始
*
* @Synchronized 关键字不要修饰方法中有suspend挂起函数,因为内部又挂起了,就不会同步了
*/
@Synchronized
suspend fun taskSynchronizeByDelay(){
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, start")
delay(1000)
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, end")
}
/**
* 执行体是taskSynchronizeByDelay(), 内部会使用delay函数,导致他外部的线程挂起,其他线程可以访问执行体,
*
* 所以:@Synchronized 同步注解,尽量不用修饰suspend的函数
*/
fun testCoroutineWithSync2() {
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法start")
repeat(3){
CoroutineScope(Dispatchers.IO).launch {
taskSynchronizeByDelay()
}
}
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法end")

// 防止main函数执行结束,就不管其他线程的打印工作了,哈哈
Thread.sleep(10000)
}

结果:

currentThread:main, time:1631961179390, 方法start
currentThread:main, time:1631961179451, 方法end
currentThread:DefaultDispatcher-worker-1 @coroutine#1, time:1631961179456, start
currentThread:DefaultDispatcher-worker-4 @coroutine#3, time:1631961179464, start
currentThread:DefaultDispatcher-worker-3 @coroutine#2, time:1631961179464, start
currentThread:DefaultDispatcher-worker-3 @coroutine#1, time:1631961180462, end
currentThread:DefaultDispatcher-worker-3 @coroutine#3, time:1631961180464, end
currentThread:DefaultDispatcher-worker-4 @coroutine#2, time:1631961180464, end

发现:加了@Synchronized注解,还是异步的执行,因为task中有delay这个挂起函数,它会挂起外部协程,直到执行完成才会执行其他的。

2:Mutex()

使用:

var mutex = Mutex()
mutex.withLock {
// TODO
}

测试:

fun testSyncByMutex() = runBlocking {
var mutex = Mutex()
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法start")
repeat(3){
CoroutineScope(Dispatchers.IO).launch {
mutex.withLock {
task()
}
}
}
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法end")

// 防止main函数执行结束,就不管其他线程的打印工作了,哈哈
Thread.sleep(10000)
}

结果:

currentThread:main @coroutine#1, time:1631951230155, 方法start
currentThread:main @coroutine#1, time:1631951230178, 方法end
currentThread:DefaultDispatcher-worker-1 @coroutine#2, time:1631951230178, start
currentThread:DefaultDispatcher-worker-1 @coroutine#2, time:1631951231178, end
currentThread:DefaultDispatcher-worker-2 @coroutine#3, time:1631951231183, start
currentThread:DefaultDispatcher-worker-2 @coroutine#3, time:1631951232183, end
currentThread:DefaultDispatcher-worker-1 @coroutine#4, time:1631951232183, start
currentThread:DefaultDispatcher-worker-1 @coroutine#4, time:1631951233184, end

发现:多个异步任务同步完成了。

3:Job.join()

Job创建协程返回的句柄,它支持join()操作,类是java线程的join功能,可以等待任务执行完成,实现同步

测试:

fun testSyncByJob() = runBlocking{
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法start")
repeat(3){
var job = CoroutineScope(Dispatchers.IO).launch {
task()
}
job.start()
job.join()
}
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法end")

// 防止main函数执行结束,就不管其他线程的打印工作了,哈哈
Thread.sleep(10000)
}

结果:

currentThread:main @coroutine#1, time:1631959997427, 方法start
currentThread:DefaultDispatcher-worker-1 @coroutine#2, time:1631959997507, start
currentThread:DefaultDispatcher-worker-1 @coroutine#2, time:1631959998507, end
currentThread:DefaultDispatcher-worker-1 @coroutine#3, time:1631959998509, start
currentThread:DefaultDispatcher-worker-1 @coroutine#3, time:1631959999509, end
currentThread:DefaultDispatcher-worker-1 @coroutine#4, time:1631959999510, start
currentThread:DefaultDispatcher-worker-1 @coroutine#4, time:1631960000510, end
currentThread:main @coroutine#1, time:1631960000510, 方法end

发现:多个任务可以同步一个个完成,并且阻塞了main线程,和withContext的效果一样哦。

4:ReentrantLock

使用:

val lock = ReentrantLock()
lock.lock()
task()
lock.unlock()

测试:

fun testReentrantLock2() = runBlocking {

val lock = ReentrantLock()
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法start")
repeat(3){
CoroutineScope(Dispatchers.IO).launch {
lock.lock()
task()
lock.unlock()
}
}
println("currentThread:${Thread.currentThread().name}, time:${System.currentTimeMillis()}, 方法end")

// 防止main函数执行结束,就不管其他线程的打印工作了,哈哈
Thread.sleep(10000)
}

结果:

currentThread:main @coroutine#1, time:1631960884403, 方法start
currentThread:main @coroutine#1, time:1631960884445, 方法end
currentThread:DefaultDispatcher-worker-1 @coroutine#2, time:1631960884445, start
currentThread:DefaultDispatcher-worker-1 @coroutine#2, time:1631960885446, end
currentThread:DefaultDispatcher-worker-5 @coroutine#4, time:1631960885446, start
currentThread:DefaultDispatcher-worker-5 @coroutine#4, time:1631960886446, end
currentThread:DefaultDispatcher-worker-2 @coroutine#3, time:1631960886446, start
currentThread:DefaultDispatcher-worker-2 @coroutine#3, time:1631960887447, end

发现:同步完成。

收起阅读 »

Kotlin中的高阶函数,匿名函数、Lambda表达式

高阶函数、匿名函数与lambda 表达式 Kotlin 函数都是头等的,这意味着它们可以存储在变量与数据结构中、作为参数传递给其他高阶函数以及从其他高阶函数返回。可以像操作任何其他非函数值一样操作函数。 头等函数:头等函数(first-class functi...
继续阅读 »

高阶函数、匿名函数与lambda 表达式

 Kotlin 函数都是头等的,这意味着它们可以存储在变量与数据结构中、作为参数传递给其他高阶函数以及从其他高阶函数返回。可以像操作任何其他非函数值一样操作函数。

 头等函数:头等函数(first-class function)是指在程序设计语言中,函数被当作头等公民。这意味着,函数可以作为别的函数的参数、函数的返回值,赋值给变量或存储在数据结构中

高阶函数

高阶函数是将函数用作参数或返回值的函数。

 //learnHighFun是一个高阶函数,因为他有一个函数类型的参数funParam,注意这里有一个新的名词,函数类型,函数在kotlin中也是一种类型。那他是什么类型的函数呢?注意(Int)->Int,这里表示这个函数是一个,接收一个Int,并返回一个Int类型的参数。
 fun learnHighFun(funParam:(Int)->Int,param:Int){}

 以上就是一个最简单的高阶函数了。了解高阶函数之前,显然,我们有必要去了解一下上面的新名词,函数类型

函数类型

如何声明一个函数类型的参数

 在kotlin中,声明一个函数类型的格式很简单,在kotlin中我们是通过->符号来组织参数类型和返回值类型,左右是函数的参数,右边是函数的返回值,函数的参数,必须在()中,多个参数的时候,用,将参数分开。如下:

 //表示该函数类型,接收一个Int类型的参数,并且返回值为Int类型
 (Int)->Int
 
 //表示该函数类型,接收两个参数,一个Int类型的参数,一个String类型的参数,并且返回值为Int类型
 (Int,Stirng)->Int

 那没有函数参数,和无返回值函数怎么声明?如下:

 //声明一个没有参数,返回值是Int的函数类型,函数类型中,函数没有参数的时候,()不可以省略
 ()->Int
 
 //明一个没有参数,没有返回值的函数类型,函数类型中,函数没有返回值的时候,Unit不可以省略
 ()->Unit
 

 以上就是简单的函数类型的声明了。那么如果是一个高阶函数,它的参数类型也是一个高阶函数,那要怎么声明?比如以下的式子表示什么含义:

 private fun learnHigh(funParams:((Int)->Int)->Int){}
 //这里表示的是一个高阶函数learnHigh,他有一个函数类型的参数funParams。而这个funParams的类型也是一个高阶函数的类型。funParams这个函数类型表示,它接受一个普通函数类(Int)->Int的参数,并返回一个Int类型。这段话读起来确实很绕,但是你明白了这个复杂的例子之后,基本所有的高阶函数你都能看懂什么意思了。
 
 //这里这个highParam的类型,就符合上面learnHigh函数所要接收的函数类型
 fun highParam(param: (Int)->Int):Int{
     return  1
 }

 讲了参数为函数类型的高阶函数,返回值类型为函数的高阶函数也基本参照上面的这些看就可以了。那么下一个问题来了,我是讲了这么多高阶函数,这么多函数类型的知识点。那么这些函数类型的参数要怎么传?换句话说,应该怎么样把这些函数类型的参数,传给的高阶函数?直接使用函数名可以吗?显然是不行的,因为函数名并不是一个表达式,不具备类型信息。那么我们这时候就需要一个单纯的方法引用表达式

函数引用

 在kotlin中,使用两个冒号的来实现对某个类的方法进行引用。 这句话包含了哪些信息呢?第一,既然是引用,那么说明是对象。也就是使用双冒号实现的引用也是一个对象。 它是一个函数类型的对象。第二,既然对象,那么他就需要被创建,也就是说,这里创建了一个函数类型的对象,这个对象是具有和这个函数功能相同的对象。还是举例子来说明一下上面两句话是什么意思:

 fun testFunReference(){
     funReference(1)  //普通函数,直接通过函数名然后附带参数来调用。
     val funObject = ::funReference //函数的引用,他本质上已经是一个对象了
     testHighFun(funObject) //通过一个函数引用,将这个函数类型的对象,传递给高阶函数。所以高阶函数里面接收的参数本质上还是对象。
 
     funObject.invoke(1) //等同于funReference(1)
     funObject(1) //等同于funReference(1),等同于funObject.invoke(1)
 }
 
 fun funReference(param:Int){
     //doSomeThing
 }
 
 fun testHighFun(funParam:(Int)->Unit){
     //doSomeThing
 }
 //这是反编译出来的java代码
 public final void testFunReference() {
     this.funReference(1);
     //val funObject = ::funReference 这句代码反编译出来就是这样的,可以看出这里是新创建了一个对象
     KFunction funObject = new Function1((TestFun)this) {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object var1) {
             this.invoke(((Number)var1).intValue());
             return Unit.INSTANCE;
        }
 
         public final void invoke(int p1) {
            ((TestFun)this.receiver).funReference(p1);
        }
    };
     this.testHighFun((Function1)funObject);
    ((Function1)funObject).invoke(1);
    ((Function1)funObject).invoke(1);//funObject(1)最终是调用的funObject.invoke(1)
 }
 
 public final void funReference(int param) {
 }
 //可以看出这个testHighFun接收的是一个Function1类型的对象
 public final void testHighFun(@NotNull Function1 funParam) {
     Intrinsics.checkNotNullParameter(funParam, "funParam");
 }

以上就是关于函数引用的知识点了。

 理解了以上的用法,但是这种写法好像每次都需要去声明一个函数,那么有没有其他不需要重新声明函数的方法去调用高阶函数呢?那肯定还是有的,如果这都不支持那Kotlin的这个高阶函数这个特性不就有点鸡肋了吗?接下来就讲解另外两个知识点,kotlin中的匿名函数Lambda表达式

匿名函数

 来讲匿名函数,看定义就知道这是一个没有名字的'函数',注意这里的'函数'这两个字是带有引号的。首先来看看怎么在高阶函数中使用吧。

 //接着上面的例子讲
 //除了这种通过引用对象调用testHighFun(funObject)的方法,还可以直接把一个函数当做这个高阶函数的参数。
 val param = fun (param:Int){ //注意这里是没有函数名的,所以是匿名'函数'
     //doSomeThing
 }
 testHighFun(param)

 注意:通过之前的分析,我们可以知道,这个高阶函数testHighFun接收的参数是一个函数对象的引用,也就是说我们定义的val param是一个函数对象的引用,那么可以得出这个匿名'函数' fun(param:Int){},他的本质是一个函数对象。他并不是'函数'。我们可以看一下反编译出来的java代码

 //param是一个Function1类型的对象的引用
 Function1 param = (Function1)null.INSTANCE;
 this.testHighFun(param);

所以记住一点,Kotlin中的匿名函数,它的本质不是函数。而是对象。它和函数不是一个东西,它是一个函数类型的对象。对象和函数,它们是两个东西。

Lambda表达式

Lambda 表达式的完整语法形式如下:

 val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }

 Lambda 表达式总是括在花括号中, 完整语法形式的参数声明放在花括号内,并有可选的类型标注, 函数体跟在一个 -> 符号之后。如果推断出的该Lambda 的返回类型不是 Unit,那么该 Lambda 主体中的最后一个(或可能是单个) 表达式会视为返回值。

由于Kotlin中是支持类型推到的,所以以上的写法可以简化成如下两个格式:

 val sum= { x: Int, y: Int -> x + y }
 val sum: (Int, Int) -> Int = { x, y -> x + y }

 在kotlin中还支持,如果函数的最后一个参数是函数,那么作为相应参数传入的 Lambda 表达式可以放在圆括号之外:

 //比如我们上面的那个例子testHighFun,可以将lambda放到原括号之外
 testHighFun(){
 //doSomeThing
 }
 
 //如果该 lambda 表达式是调用时唯一的参数,那么圆括号可以完全省略:如下
 testHighFun{
 //doSomeThing
 }
 
 //一个 lambda 表达式只有一个参数是很常见的。
 //如果编译器自己可以识别出签名,也可以不用声明唯一的参数并忽略 ->。 该参数会隐式声明为 it: 如下
 testHighFun{
     //doSomeThing
     it.toString(it)
 }

从 lambda 表达式中返回一个值

 我们可以使用限定的返回语法从 lambda 显式返回一个值。 否则,将隐式返回最后一个表达式的值。参考官网的例子如下

 ints.filter {
     val shouldFilter = it > 0
     shouldFilter
 }
 
 ints.filter {
     val shouldFilter = it > 0
     return@filter shouldFilter
 }

 好了,以上就是Lambda的基本用法了。

 讲了这么多,我们只是讲解了Lambda怎么使用,那么它的本质是什么?其实仔细思考一下上面的testHighFun可以传入一个Lambda表达式就可以大概知道,Lambda的本质也是一个函数类型的对象。这一点也可以通过发编译的java代码去看。

匿名函数与Lambda表达式的总结:

  1. 两者都能作为高阶函数的参数进行传递。
  2. 两者的本质都是函数类型的对象。

备注:以上就是我个人对高阶函数,匿名函数,Lambda表达式的理解,有什么不对的地方,还请各位大佬指正。

收起阅读 »

高仿小米加载动画效果

前言 首先看一下小米中的加载动画是怎么样的,恩恩~~~~虽然只是张图片,因为录制不上全部,很多都是刚一加载就成功了,一点机会都不提供给我,所以就截了一张图,他这个加载动画特点就是左面圆圈会一直转。 仿照的效果如下: 实现过程 这个没有难度,只是学会一个公式...
继续阅读 »

前言


首先看一下小米中的加载动画是怎么样的,恩恩~~~~虽然只是张图片,因为录制不上全部,很多都是刚一加载就成功了,一点机会都不提供给我,所以就截了一张图,他这个加载动画特点就是左面圆圈会一直转。


image.png


仿照的效果如下:


录屏_选择区域_20210917141950.gif


实现过程


这个没有难度,只是学会一个公式就可以,也就是已知圆心,半径,角度,求圆上的点坐标,算出来的结果在这个点绘制一个实心圆即可,下面是自定义Dialog,让其在底部现实,其中的View也是自定义的一个。



class MiuiLoadingDialog(context: Context) : Dialog(context) {
private var miuiLoadingView : MiuiLoadingView= MiuiLoadingView(context);
init {
setContentView(miuiLoadingView)
setCancelable(false)
}

override fun show() {
super.show()
val window: Window? = getWindow();
val wlp = window!!.attributes

wlp.gravity = Gravity.BOTTOM
window.setBackgroundDrawable( ColorDrawable(Color.TRANSPARENT));
wlp.width=WindowManager.LayoutParams.MATCH_PARENT;
window.attributes = wlp
}
}

下面是主要的逻辑,在里面,首先通过clipPath方法裁剪出一个上边是圆角的形状,然后绘制一个外圆,这是固定的。


中间的圆需要一个公式,如下。


x1   =   x0   +   r   *   cos(a   *   PI   /180   ) 
y1   =   y0   +   r   *   sin(a   *   PI  /180   ) 

x0、y0就是外边大圆的中心点,r是中间小圆大小,a是角度,只需要一直变化这个角度,得出的x1、y1通过drawCircle绘制出来即可。


image.png



class MiuiLoadingView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

//Dialog上面圆角大小
val CIRCULAR: Float = 60f;

//中心移动圆位置
var rx: Float = 0f;
var ry: Float = 0f;

//左边距离
var MARGIN_LEFT: Int = 100;

//中心圆大小
var centerRadiusSize: Float = 7f;

var textPaint: Paint = Paint().apply {
textSize = 50f
color = Color.BLACK
}

var circlePaint: Paint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 8f
isAntiAlias = true
color = Color.BLACK
}

var centerCirclePaint: Paint = Paint().apply {
style = Paint.Style.FILL
isAntiAlias = true
color = Color.BLACK
}

var degrees = 360;

val TEXT = "正在加载中,请稍等";
var textHeight = 0;

init {

var runnable = object : Runnable {
override fun run() {
val r = 12;
rx = MARGIN_LEFT + r * Math.cos(degrees.toDouble() * Math.PI / 180).toFloat()
ry =
((measuredHeight.toFloat() / 2) + r * Math.sin(degrees.toDouble() * Math.PI / 180)).toFloat();
invalidate()
degrees += 5
if (degrees > 360) degrees = 0
postDelayed(this, 1)
}
}
postDelayed(runnable, 0)


var rect = Rect()
textPaint.getTextBounds(TEXT, 0, TEXT.length, rect)
textHeight = rect.height()
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(widthMeasureSpec, 220);
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

var path = Path()
path.addRoundRect(
RectF(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat()),
floatArrayOf(CIRCULAR, CIRCULAR, CIRCULAR, CIRCULAR, 0f, 0f, 0f, 0f), Path.Direction.CW
);
canvas.clipPath(path)
canvas.drawColor(Color.WHITE)


canvas.drawCircle(
MARGIN_LEFT.toFloat(), measuredHeight.toFloat() / 2,
35f, circlePaint
)

canvas.drawCircle(
rx, ry,
centerRadiusSize, centerCirclePaint
)


canvas.drawText(TEXT, (MARGIN_LEFT + 80).toFloat(), ((measuredHeight / 2)+(textHeight/2)).toFloat(), textPaint)
}
}

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