注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

写给vue转react的同志们(5)

写给vue转react的同志们(4)我们知道 React 中使用高阶组件(下面简称HOC)来复用一些组件的逻辑。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件...
继续阅读 »

写给vue转react的同志们(4)
我们知道 React 中使用高阶组件(下面简称HOC)来复用一些组件的逻辑。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。


组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。


const EnhancedComponent = higherOrderComponent(WrappedComponent);

上面出自 React 官方文档。


那在 Vue 中 复用组件逻辑实际上比较简单,利用 Mixins 混入复用组件逻辑,当 Mixins 中的逻辑过多时(比如方法和属性),在项目当中使用时追述源代码会比较麻烦,因为他在混入后没有明确告诉你哪个方法是被复用的。


//mixins.js(Vue 2 举例)
export defalut {
data() {
return {
text: 'hello'
}
}
}
// a.vue
import mixins from './mixins.js'
export defalut {
mixins: [mixins]
computed: {
acitveText() {
return `来自mixins的数据:${this.text}`
}
}
}
复制代码

可以看到除了在开头引入并挂载混入,并没有看到this.text是从哪里来的,混入虽然好用,但当逻辑复杂时,其阅读起来是有一定困难的。


那你想在 Vue 中强行使用像 React 那样的高阶组件呢?那当然可以。只是 Vue 官方不怎么推崇 HOC,且 Mixins 本身可以实现 HOC 相关功能。


简单举个例子:


// hoc.js
import Vue from 'Vue'

export default const HOC = (component, text) => {
return Vue.component('HOC', {
render(createElement) {
return createElement(component, {
on: { ...this.$listeners },
props: {
text: this.text
}
})
},
data() {
return {
text: text,
hocText: 'HOC'
}
},
mounted() {
// do something ...
console.log(this.text)
console.log(this.hocText)
}
})
}

使用高阶组件:


// user.vue
<template>
<userInfo/>
</template>

<script>
import HOC from './hoc.js'
// 引入某个组件
import xxx from './xxx'

const userInfo = HOC(xxx, 'hello')

export default {
name: 'user',
components: {
userInfo
}
}
</script>



是不是相比 Mixins 更加复杂一点了?在 Vue 中使用高阶组件所带来的收益相对于 Mixins 并没有质的变化。不过话又说回来,起初 React 也是使用 Mixins 来完成代码复用的,比如为了避免组件的非必要的重复渲染可以在组件中混入 PureRenderMixin


const PureRenderMixin = require('react-addons-pure-render-mixin')
const component = React.createClass({
mixins: [PureRenderMixin]
})


后来 React 使用shallowCompare 来 替代 PureRenderMixin


const shallowCompare = require('react-addons-shallow-compare')
const component = React.createClass({
shouldComponentUpdate: (nextProps, nextState) => {
return shallowCompare(nextProps, nextState)
}
})


这需要你自己在组件中实现 shouldComponentUpdate 方法,只不过这个方法具体的工作由 shallowCompare 帮你完成,我们只需调用即可。


再后来 React 为了避免总是要重复调用这段代码,React.PureComponent 应运而生,总之 React 在慢慢将 Mixins 脱离开来,这对他们的生态系统并不是特别的契合。当然每种方案都各有千秋,只是是否适合自己的框架。


那我们回归 HOC,在 React 中如何封装 HOC 呢?


实际上我在往期篇幅有提到过:
点击传送


但是我还是简单举个例子:


封装 HOC:


// hoc.js
export default const HOC = (WrappedComponent) => {
return Class newComponent extends WrappedComponent {
constructor(props) {
super(props)
// do something ...
this.state = {
text: 'hello'
}
}
componentDidMount() {
super.componentDidMount()
// do something ...
console.log('this.state.text')
}
render() {
// init render
return super.render()
}
}
}



使用 HOC:


// user.js
import HOC from './hoc.js'
class user extends React.Component {
// do something ...
}
export defalut HOC(user)


装饰器写法更为简洁:


import HOC from './hoc.js'
@HOC
class user extends React.Component {
// do something ...
}
export defalut user


可以看到无论 Vue 还是 React 亦或是 HOC 或 Mixins 他们都是为了解决组件逻辑复用应运而生的,具体使用哪一种方案还要看你的项目契合度等其他因素。


技术本身并无好坏,只是会随着时间推移被其他更适合的方案取代,技术迭代也是必然的,相信作为一个优秀的程序员也不会去讨论一个技术的好或坏,只有适合与否。


作者:饼干_
链接:https://juejin.cn/post/7020215941422137381

收起阅读 »

Android学习指南 — Android进阶篇

ARTART 代表 Android Runtime,其处理应用程序执行的方式完全不同于 Dalvik,Dalvik 是依靠一个 Just-In-Time (JIT) 编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高...
继续阅读 »

ART

ART 代表 Android Runtime,其处理应用程序执行的方式完全不同于 Dalvik,Dalvik 是依靠一个 Just-In-Time (JIT) 编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运 行。ART 则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫 Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。

ART 功能

预先 (AOT) 编译

ART 引入了预先编译机制,可提高应用的性能。ART 还具有比 Dalvik 更严格的安装时验证。在安装时,ART 使用设备自带的 dex2oat 工具来编译应用。该实用工具接受 DEX 文件作为输入,并为目标设备生成经过编译的应用可执行文件。该工具应能够顺利编译所有有效的 DEX 文件。

垃圾回收优化

垃圾回收 (GC) 可能有损于应用性能,从而导致显示不稳定、界面响应速度缓慢以及其他问题。ART 通过以下几种方式对垃圾回收做了优化:

  • 只有一次(而非两次)GC 暂停
  • 在 GC 保持暂停状态期间并行处理
  • 在清理最近分配的短时对象这种特殊情况中,回收器的总 GC 时间更短
  • 优化了垃圾回收的工效,能够更加及时地进行并行垃圾回收,这使得 GC_FOR_ALLOC 事件在典型用例中极为罕见
  • 压缩 GC 以减少后台内存使用和碎片

开发和调试方面的优化

  • 支持采样分析器

一直以来,开发者都使用 Traceview 工具(用于跟踪应用执行情况)作为分析器。虽然 Traceview 可提供有用的信息,但每次方法调用产生的开销会导致 Dalvik 分析结果出现偏差,而且使用该工具明显会影响运行时性能

ART 添加了对没有这些限制的专用采样分析器的支持,因而可更准确地了解应用执行情况,而不会明显减慢速度。KitKat 版本为 Dalvik 的 Traceview 添加了采样支持。

  • 支持更多调试功能

ART 支持许多新的调试选项,特别是与监控和垃圾回收相关的功能。例如,查看堆栈跟踪中保留了哪些锁,然后跳转到持有锁的线程;询问指定类的当前活动的实例数、请求查看实例,以及查看使对象保持有效状态的参考;过滤特定实例的事件(如断点)等。

  • 优化了异常和崩溃报告中的诊断详细信息

当发生运行时异常时,ART 会为您提供尽可能多的上下文和详细信息。ART 会提供 java.lang.ClassCastExceptionjava.lang.ClassNotFoundException 和 java.lang.NullPointerException 的更多异常详细信息(较高版本的 Dalvik 会提供 java.lang.ArrayIndexOutOfBoundsException 和 java.lang.ArrayStoreException 的更多异常详细信息,这些信息现在包括数组大小和越界偏移量;ART 也提供这类信息)。

ART GC

ART 有多个不同的 GC 方案,这些方案包括运行不同垃圾回收器。默认方案是 CMS(并发标记清除)方案,主要使用粘性 CMS 和部分 CMS。粘性 CMS 是 ART 的不移动分代垃圾回收器。它仅扫描堆中自上次 GC 后修改的部分,并且只能回收自上次 GC 后分配的对象。除 CMS 方案外,当应用将进程状态更改为察觉不到卡顿的进程状态(例如,后台或缓存)时,ART 将执行堆压缩。

除了新的垃圾回收器之外,ART 还引入了一种基于位图的新内存分配程序,称为 RosAlloc(插槽运行分配器)。此新分配器具有分片锁,当分配规模较小时可添加线程的本地缓冲区,因而性能优于 DlMalloc。

与 Dalvik 相比,ART CMS 垃圾回收计划在很多方面都有一定的改善:

  • 与 Dalvik 相比,暂停次数从 2 次减少到 1 次。Dalvik 的第一次暂停主要是为了进行根标记,即在 ART 中进行并发标记,让线程标记自己的根,然后马上恢复运行。
  • 与 Dalvik 类似,ART GC 在清除过程开始之前也会暂停 1 次。两者在这方面的主要差异在于:在此暂停期间,某些 Dalvik 环节在 ART 中并发进行。这些环节包括 java.lang.ref.Reference 处理、系统弱清除(例如,jni 弱全局等)、重新标记非线程根和卡片预清理。在 ART 暂停期间仍进行的阶段包括扫描脏卡片以及重新标记线程根,这些操作有助于缩短暂停时间。
  • 相对于 Dalvik,ART GC 改进的最后一个方面是粘性 CMS 回收器增加了 GC 吞吐量。不同于普通的分代 GC,粘性 CMS 不移动。系统会将年轻对象保存在一个分配堆栈(基本上是 java.lang.Object 数组)中,而非为其设置一个专属区域。这样可以避免移动所需的对象以维持低暂停次数,但缺点是容易在堆栈中加入大量复杂对象图像而使堆栈变长。

ART GC 与 Dalvik 的另一个主要区别在于 ART GC 引入了移动垃圾回收器。使用移动 GC 的目的在于通过堆压缩来减少后台应用使用的内存。目前,触发堆压缩的事件是 ActivityManager 进程状态的改变。当应用转到后台运行时,它会通知 ART 已进入不再“感知”卡顿的进程状态。此时 ART 会进行一些操作(例如,压缩和监视器压缩),从而导致应用线程长时间暂停。目前正在使用的两个移动 GC 是同构空间压缩和半空间压缩。

  • 半空间压缩将对象在两个紧密排列的碰撞指针空间之间进行移动。这种移动 GC 适用于小内存设备,因为它可以比同构空间压缩稍微多节省一点内存。额外节省出的空间主要来自紧密排列的对象,这样可以避免 RosAlloc/DlMalloc 分配器占用开销。由于 CMS 仍在前台使用,且不能从碰撞指针空间中进行收集,因此当应用在前台使用时,半空间还要再进行一次转换。这种情况并不理想,因为它可能引起较长时间的暂停。
  • 同构空间压缩通过将对象从一个 RosAlloc 空间复制到另一个 RosAlloc 空间来实现。这有助于通过减少堆碎片来减少内存使用量。这是目前非低内存设备的默认压缩模式。相比半空间压缩,同构空间压缩的主要优势在于应用从后台切换到前台时无需进行堆转换。

Hook

基本流程

1、根据需求确定 要 hook 的对象
2、寻找要hook的对象的持有者,拿到要 hook 的对象
3、定义“要 hook 的对象”的代理类,并且创建该类的对象
4、使用上一步创建出来的对象,替换掉要 hook 的对象

使用示例

/**
* hook的核心代码
* 这个方法的唯一目的:用自己的点击事件,替换掉 View 原来的点击事件
*
* @param view hook的范围仅限于这个view
*/
@SuppressLint({"DiscouragedPrivateApi", "PrivateApi"})
public static void hook(Context context, final View view) {//
try {
// 反射执行View类的getListenerInfo()方法,拿到v的mListenerInfo对象,这个对象就是点击事件的持有者
Method method = View.class.getDeclaredMethod("getListenerInfo");
method.setAccessible(true);//由于getListenerInfo()方法并不是public的,所以要加这个代码来保证访问权限
Object mListenerInfo = method.invoke(view);//这里拿到的就是mListenerInfo对象,也就是点击事件的持有者

// 要从这里面拿到当前的点击事件对象
Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");// 这是内部类的表示方法
Field field = listenerInfoClz.getDeclaredField("mOnClickListener");
final View.OnClickListener onClickListenerInstance = (View.OnClickListener) field.get(mListenerInfo);//取得真实的mOnClickListener对象

// 2. 创建我们自己的点击事件代理类
// 方式1:自己创建代理类
// ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
// 方式2:由于View.OnClickListener是一个接口,所以可以直接用动态代理模式
// Proxy.newProxyInstance的3个参数依次分别是:
// 本地的类加载器;
// 代理类的对象所继承的接口(用Class数组表示,支持多个接口)
// 代理类的实际逻辑,封装在new出来的InvocationHandler内
Object proxyOnClickListener = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d("HookSetOnClickListener", "点击事件被hook到了");//加入自己的逻辑
return method.invoke(onClickListenerInstance, args);//执行被代理的对象的逻辑
}
});
// 3. 用我们自己的点击事件代理类,设置到"持有者"中
field.set(mListenerInfo, proxyOnClickListener);
} catch (Exception e) {
e.printStackTrace();
}
}

// 自定义代理类
static class ProxyOnClickListener implements View.OnClickListener {
View.OnClickListener oriLis;

public ProxyOnClickListener(View.OnClickListener oriLis) {
this.oriLis = oriLis;
}

@Override
public void onClick(View v) {
Log.d("HookSetOnClickListener", "点击事件被hook到了");
if (oriLis != null) {
oriLis.onClick(v);
}
}
}

Proguard

Proguard 具有以下三个功能:

  • 压缩(Shrink): 检测和删除没有使用的类,字段,方法和特性
  • 优化(Optimize) : 分析和优化Java字节码
  • 混淆(Obfuscate): 使用简短的无意义的名称,对类,字段和方法进行重命名

规则

  • 关键字
关键字描述
keep保留类和类中的成员,防止被混淆或移除
keepnames保留类和类中的成员,防止被混淆,成员没有被引用会被移除
keepclassmembers只保留类中的成员,防止被混淆或移除
keepclassmembernames只保留类中的成员,防止被混淆,成员没有引用会被移除
keepclasseswithmembers保留类和类中的成员,防止被混淆或移除,保留指明的成员
keepclasseswithmembernames保留类和类中的成员,防止被混淆,保留指明的成员,成员没有引用会被移除
  • 通配符
通配符描述
匹配类中的所有字段
匹配类中所有的方法
匹配类中所有的构造函数
*匹配任意长度字符,不包含包名分隔符(.)
**匹配任意长度字符,包含包名分隔符(.)
***匹配任意参数类型
  • 指定混淆时可使用字典
-applymapping filename 指定重用一个已经写好了的map文件作为新旧元素名的映射。
-obfuscationdictionary filename 指定一个文本文件用来生成混淆后的名字。
-classobfuscationdictionary filename 指定一个混淆类名的字典
-packageobfuscationdictionary filename 指定一个混淆包名的字典
-overloadaggressively 混淆的时候大量使用重载,多个方法名使用同一个混淆名(慎用)

公共模板

#############################################
#
# 对于一些基本指令的添加
#
#############################################
# 代码混淆压缩比,在 0~7 之间,默认为 5,一般不做修改
-optimizationpasses 5

# 混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames

# 指定不去忽略非公共库的类
-dontskipnonpubliclibraryclasses

# 这句话能够使我们的项目混淆后产生映射文件
# 包含有类名->混淆后类名的映射关系
-verbose

# 指定不去忽略非公共库的类成员
-dontskipnonpubliclibraryclassmembers

# 不做预校验,preverify 是 proguard 的四个步骤之一,Android 不需要 preverify,去掉这一步能够加快混淆速度。
-dontpreverify

# 保留 Annotation 不混淆
-keepattributes *Annotation*,InnerClasses

# 避免混淆泛型
-keepattributes Signature

# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable

# 指定混淆是采用的算法,后面的参数是一个过滤器
# 这个过滤器是谷歌推荐的算法,一般不做更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*


#############################################
#
# Android开发中一些需要保留的公共部分
#
#############################################

# 保留我们使用的四大组件,自定义的 Application 等等这些类不被混淆
# 因为这些子类都有可能被外部调用
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService


# 保留 support 下的所有类及其内部类
-keep class android.support.** { *; }

# 保留继承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**

# 保留 R 下面的资源
-keep class **.R$* { *; }

# 保留本地 native 方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}

# 保留在 Activity 中的方法参数是view的方法,
# 这样以来我们在 layout 中写的 onClick 就不会被影响
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}

# 保留枚举类不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}

# 保留我们自定义控件(继承自 View)不被混淆
-keep public class * extends android.view.View {
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}

# 保留 Parcelable 序列化类不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}

# 保留 Serializable 序列化的类不被混淆
-keepnames class * implements java.io.Serializable
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
!private <fields>;
!private <methods>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}

# 对于带有回调函数的 onXXEvent、**On*Listener 的,不能被混淆
-keepclassmembers class * {
void *(**On*Event);
void *(**On*Listener);
}

# webView 处理,项目中没有使用到 webView 忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.webView, java.lang.String);
}

# js
-keepattributes JavascriptInterface
-keep class android.webkit.JavascriptInterface { *; }
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}

# @Keep
-keep,allowobfuscation @interface android.support.annotation.Keep
-keep @android.support.annotation.Keep class *
-keepclassmembers class * {
@android.support.annotation.Keep *;
}

常用的自定义混淆规则

# 通配符*,匹配任意长度字符,但不含包名分隔符(.)
# 通配符**,匹配任意长度字符,并且包含包名分隔符(.)

# 不混淆某个类
-keep public class com.jasonwu.demo.Test { *; }

# 不混淆某个包所有的类
-keep class com.jasonwu.demo.test.** { *; }

# 不混淆某个类的子类
-keep public class * com.jasonwu.demo.Test { *; }

# 不混淆所有类名中包含了 ``model`` 的类及其成员
-keep public class **.*model*.** {*;}

# 不混淆某个接口的实现
-keep class * implements com.jasonwu.demo.TestInterface { *; }

# 不混淆某个类的构造方法
-keepclassmembers class com.jasonwu.demo.Test {
public <init>();
}

# 不混淆某个类的特定的方法
-keepclassmembers class com.jasonwu.demo.Test {
public void test(java.lang.String);
}

aar中增加独立的混淆配置

build.gralde

android {
···
defaultConfig {
···
consumerProguardFile 'proguard-rules.pro'
}
···
}

检查混淆和追踪异常

开启 Proguard 功能,则每次构建时 ProGuard 都会输出下列文件:

  • dump.txt
    说明 APK 中所有类文件的内部结构。
  • mapping.txt
    提供原始与混淆过的类、方法和字段名称之间的转换。
  • seeds.txt
    列出未进行混淆的类和成员。
  • usage.txt
    列出从 APK 移除的代码。

这些文件保存在 /build/outputs/mapping/release/ 中。我们可以查看 seeds.txt 里面是否是我们需要保留的,以及 usage.txt 里查看是否有误删除的代码。 mapping.txt 文件很重要,由于我们的部分代码是经过重命名的,如果该部分出现 bug,对应的异常堆栈信息里的类或成员也是经过重命名的,难以定位问题。我们可以用 retrace 脚本(在 Windows 上为 retrace.bat;在 Mac/Linux 上为 retrace.sh)。它位于 /tools/proguard/ 目录中。该脚本利用 mapping.txt 文件和你的异常堆栈文件生成没有经过混淆的异常堆栈文件,这样就可以看清是哪里出问题了。使用 retrace 工具的语法如下:

retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

架构

MVC

在 Android 中,三者的关系如下:

由于在 Android 中 xml 布局的功能性太弱,所以 Activity 承担了绝大部分的工作,所以在 Android 中 mvc 更像:

总结:

  • 具有一定的分层,model 解耦,controller 和 view 并没有解耦
  • controller 和 view 在 Android 中无法做到彻底分离,Controller 变得臃肿不堪
  • 易于理解、开发速度快、可维护性高

MVP

通过引入接口 BaseView,让相应的视图组件如 Activity,Fragment去实现 BaseView,把业务逻辑放在 presenter 层中,弱化 Model 只有跟 view 相关的操作都由 View 层去完成。

总结:

  • 彻底解决了 MVC 中 View 和 Controller 傻傻分不清楚的问题
  • 但是随着业务逻辑的增加,一个页面可能会非常复杂,UI 的改变是非常多,会有非常多的 case,这样就会造成 View 的接口会很庞大
  • 更容易单元测试

MVVM

在 MVP 中 View 和 Presenter 要相互持有,方便调用对方,而在 MVP 中 View 和 ViewModel 通过 Binding 进行关联,他们之前的关联处理通过 DataBinding 完成。

总结:

  • 很好的解决了 MVC 和 MVP 的问题
  • 视图状态较多,ViewModel 的构建和维护的成本都会比较高
  • 但是由于数据和视图的双向绑定,导致出现问题时不太好定位来源

Jetpack

架构

CMake 构建 NDK 项目

CMake 是一个开源的跨平台工具系列,旨在构建,测试和打包软件,从 Android Studio 2.2 开始,Android Sudio 默认地使用 CMake 与 Gradle 搭配使用来构建原生库。

启动方式只需要在 app/build.gradle 中添加相关:

android {
···
defaultConfig {
···
externalNativeBuild {
cmake {
cppFlags ""
}
}

ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a'
}
}
···
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}

然后在对应目录新建一个 CMakeLists.txt 文件:

# 定义了所需 CMake 的最低版本
cmake_minimum_required(VERSION 3.4.1)

# add_library() 命令用来添加库
# native-lib 对应着生成的库的名字
# SHARED 代表为分享库
# src/main/cpp/native-lib.cpp 则是指明了源文件的路径。
add_library( # Sets the name of the library.
native-lib

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp)

# find_library 命令添加到 CMake 构建脚本中以定位 NDK 库,并将其路径存储为一个变量。
# 可以使用此变量在构建脚本的其他部分引用 NDK 库
find_library( # Sets the name of the path variable.
log-lib

# Specifies the name of the NDK library that
# you want CMake to locate.
log)

# 预构建的 NDK 库已经存在于 Android 平台上,因此,无需再构建或将其打包到 APK 中。
# 由于 NDK 库已经是 CMake 搜索路径的一部分,只需要向 CMake 提供希望使用的库的名称,并将其关联到自己的原生库中

# 要将预构建库关联到自己的原生库
target_link_libraries( # Specifies the target library.
native-lib

# Links the target library to the log library
# included in the NDK.
${log-lib})
···

常用的 Android NDK 原生 API

支持 NDK 的 API 级别关键原生 API包括
3Java 原生接口#include <jni.h>
3Android 日志记录 API#include <android/log.h>
5OpenGL ES 2.0#include <GLES2/gl2.h> #include <GLES2/gl2ext.h>
8Android 位图 API#include <android/bitmap.h>
9OpenSL ES#include <SLES/OpenSLES.h> #include <SLES/OpenSLES_Platform.h> #include <SLES/OpenSLES_Android.h> #include <SLES/OpenSLES_AndroidConfiguration.h>
9原生应用 API#include <android/rect.h> #include <android/window.h> #include<android/native_activity.h> ···
18OpenGL ES 3.0#include <GLES3/gl3.h> #include <GLES3/gl3ext.h>
21原生媒体 API#include <media/NdkMediaCodec.h> #include <media/NdkMediaCrypto.h> ···
24原生相机 API#include <camera/NdkCameraCaptureSession.h> #include <camera/NdkCameraDevice.h> ···
···

类加载器

双亲委托模式

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子 ClassLoader 再加载一次。如果不使用这种委托模式,那我们就可以随时使用自定义的类来动态替代一些核心的类,存在非常大的安全隐患。

DexPathList

DexClassLoader 重载了 findClass 方法,在加载类时会调用其内部的 DexPathList 去加载。DexPathList 是在构造 DexClassLoader 时生成的,其内部包含了 DexFile。

DexPathList.java
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
收起阅读 »

Android自定义控件六边形

Android自定义六边形控件一.效果图原文地址: https://blog.csdn.net/oMengHui/article/details/45540645二.核心算法平面内一个坐标点是否在多边形内判断,使用射线法判断。从目标点出发引一条射线,...
继续阅读 »



Android自定义六边形控件

一.效果图

原文地址: https://blog.csdn.net/oMengHui/article/details/45540645

20150506195536825.gif

二.核心算法
平面内一个坐标点是否在多边形内判断,使用射线法判断。从目标点出发引一条射线,看这条射线和多边形所有边的交点数目。如果是奇数个交点,则说明点在多边形内部;如果是偶数个交点,则说明在外部。

20150506195740393.jpeg

算法图解:

20150506195748111.jpeg

参考代码:

int pnpoly(int nvert, float *vertx, float *verty, float testx, float testy)
{
int i, j, c = 0;
for (i = 0, j = nvert-1; i < nvert; j = i++)
{
if ( ((verty[i]>testy) != (verty[j]>testy)) &&
(testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) )
c = !c;
}
return c;
}
复制代码

更多参考信息

三.知识点
1.控件属性自定义和使用 在values->attrs->declare-styleable中定义属性;在布局中引入(格式xmls:sec=”schemas.android.com/apk/res/程序包…;

2.Paint画笔使用 class继承View后重写onDraw方法,Paint paint=new Paint().setStyle(Style.FILL); canvas.drawText(“Hello”,x,y,paint);

3.Path路径使用 Path path=new Path(); path.moveTo(x1,y1); path.lineTo(x2,y2); path.close(); canvas.drawPath(path,paint);

4.图片缩放平铺居中 六边形视图显示为正方形,如属性设置图片宽高不相等直接使用图片会被拉伸变形。通过逻辑处理以图片宽高较小值居中裁剪图片。

 /**
* 按宽/高缩放图片到指定大小并进行裁剪得到中间部分图片
*
* @param bitmap 源bitmap
* @param w 缩放后指定的宽度
* @param h 缩放后指定的高度
* @return 缩放后的中间部分图片
*/
public static Bitmap zoomBitmap(Bitmap bitmap, int w, int h) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
float scaleWidht, scaleHeight, x, y;
Bitmap newbmp;
Matrix matrix = new Matrix();
if (width > height) {
scaleWidht = ((float) h / height);
scaleHeight = ((float) h / height);
x = (width - w * height / h) / 2;// 获取bitmap源文件中x做表需要偏移的像数大小
y = 0;
} else if (width < height) {
scaleWidht = ((float) w / width);
scaleHeight = ((float) w / width);
x = 0;
y = (height - h * width / w) / 2;// 获取bitmap源文件中y做表需要偏移的像数大小
} else {
scaleWidht = ((float) w / width);
scaleHeight = ((float) w / width);
x = 0;
y = 0;
}
matrix.postScale(scaleWidht, scaleHeight);
try {
newbmp = Bitmap.createBitmap(bitmap, (int) x, (int) y,
(int) (width - x), (int) (height - y), matrix, true);// createBitmap()方法中定义的参数x+width要小于或等于bitmap.getWidth(),y+height要小于或等于bitmap.getHeight()
} catch (Exception e) {
e.printStackTrace();
return null;
}
return newbmp;
}
复制代码

5.动画(ScaleAnimation)

Animation scaleAnimation = new ScaleAnimation(start, end, start, end,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
scaleAnimation.setDuration(30);
scaleAnimation.setFillAfter(true);
this.startAnimation(endAnimation);
复制代码

6.监听实现

HexagonView.java ->
public interface OnHexagonViewClickListener {
public void onClick(View view);
}
public void setOnHexagonClickListener(OnHexagonViewClickListener listener) {
this.listener = listener;
}
OnHexagonViewClickListener hexagonListener=new OnHexagonViewClickListener();//实例化
/**
*系统onTouchEvent
*/
public boolean onTouchEvent(MotionEvent event){
if(!isOn){//未点中六边形
break;
}
switch(event.getAction()){
case MotionEvent.ACTION_UP:
if(hexagonListener!=null){
hexagonListener.click(this);
}
break;
}
}

MainActivity->
public class MainActivity extends Activity implements HexagonView.OnHexagonViewClickListener{
HexagonView hexagonViewHello;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
hexagonViewHello=(HexagonView)this.findViewById(R.id.hexagonviewhello);
hexagonViewHello.setOnHexagonClickListener(this);
}
/**
* 事件监听
*/
public void onClick(View view){
Log.d(TAG,"onClick()");
switch (view.getId()){
case R.id.hexagonviewhello:
Toast.makeText(this,"Hello",Toast.LENGTH_SHORT).show();
break;
}
}

}
收起阅读 »

Kotlin协程的取消和异常传播机制

1.协程核心概念回顾结构化并发(Structured Concurrency)作用域(CoroutineScope /SupervisorScope)作业(Job/SupervisorJob)开启协程(launch/async)2.协程的取消2.1 协程的取消...
继续阅读 »

1.协程核心概念回顾

结构化并发(Structured Concurrency)

作用域(CoroutineScope /SupervisorScope)

作业(Job/SupervisorJob)

开启协程(launch/async)

2.协程的取消

2.1 协程的取消操作

Job生命周期

  • 作用域或作业的取消

示例代码

   suspend fun c01_cancle() {
val scope = CoroutineScope(Job())
val job1 = scope.launch { }
val job2 = scope.launch { }
//取消作业
job1.cancel()
job2.cancel()
//取消作用域
scope.cancel()

}

注意:不能在已取消的作用域中再开启协程

2.2确保协程可以被取消

  • 协程的取消只是标记了协程的取消状态,并未真正取消协程

示例代码:

  val job = launch(Dispatchers.Default) {
var i = 0
while (i < 5) {
println("Hello ${i++}")
Thread.sleep(100)
}
}
delay(200)
println("Cancel!")
job.cancel()

打印结果://未真正取消,直接检查

Hello 0
Hello 1
Hello 2
Cancel!
Hello 3
Hello 4

  • 可以用 isActive ensureActive() yield来在关键位置做检查,确保协程可以正常关闭
   val job = launch(Dispatchers.Default) {
var i = 0
while (i < 5 && isActive) {//方法1
ensureActive()//方法2
yield()//方法3
println("Hello ${i++}")
Thread.sleep(100)
}
}
delay(200)
println("Cancel!")
job.cancel()

2.3 协程取消后的资源关闭

  • try/finally可以关闭资源
 launch {
try {
openIo()//开启文件io
delay(100)
throw ArithmeticException()
} finally {
println("协程结束")
closeIo()//关闭文件io
}
}
  • 注意:finally中不能调用挂起函数(如果一定要调用,需要用withContext(NonCancellable),不推荐使用)
   launch {
try {
work()
} finally {
//withContext(NonCancellable)可以执行,不然不会再被执行
withContext(NonCancellable) {
delay(1000L) // 挂起方法
println("Cleanup done!")
}
}
}

2.4 CancellationException 会被忽略

  val job = launch {
try {
delay(Long.MAX_VALUE)
} catch (e: Exception) {
println("捕获到一个异常$e")
//打印:捕获到一个异常java.util.concurrent.CancellationException: 我是一个取消异常
}
}
yield()
job.cancel(CancellationException("我是一个取消异常"))
job.join()

3.协程的异常传播机制

3.1 捕捉协程异常

3.1.1 try/catch

  • try/catch业务代码
 launch {
try {
throw ArithmeticException("计算错误")
} catch (e: Exception) {
println("捕获到一个异常$e")
}
}
//打印:捕获到一个异常java.lang.ArithmeticException: 计算错误
  • try/catch协程
  try {
launch {
throw ArithmeticException("计算错误")
}
} catch (e: Exception) {
println("捕获到一个异常$e")
}

//无法捕捉到 error日志
Exception in thread "main" java.lang.ArithmeticException: 计算错误
at com.jinbo.kotlin.coroutine.C05_Exception$testDemo$2$1.invokeSuspend(C05_Exception.kt:65)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:84)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.jinbo.kotlin.coroutine.C05_Exception.main(C05_Exception.kt:17)
  • 无法通过外部try-catch语句来捕获协程异常

3.1.2 CoroutineExceptionHandler 捕捉异常

  supervisorScope {
val exceptionHandler = CoroutineExceptionHandler { _, e ->
println("捕获到一个异常$e")
}
launch(exceptionHandler) {
throw ArithmeticException("计算错误")
}
}
//捕获到一个异常java.lang.ArithmeticException: 计算错误

3.1.3 runCatching 捕捉异常

  val catching = kotlin.runCatching {
"hello"
throw ArithmeticException("我是一个异常")
}
if (catching.isSuccess) {
println("正常结果是${catching.getOrNull()}")
} else {
println("失败了,原因是:${catching.exceptionOrNull()}")
}

这时,就要介绍协程的异常传播机制

3.2 协同作用域的传播机制

3.2.1 特性

  • 双向传播,取消子协程,取消自己,向父协程传播

[协同作用域传播特性] 示意图

  coroutineScope {
launch {
launch {
//子协程的异常,会向上传播
throw ArithmeticException() }
}
launch {
launch { }
}
}

3.2.2 子协程无法捕获自己的异常,只有父协程才可以


val scope = CoroutineScope(Job())
//父协程(根协程)才可以捕获异常
scope.launch(exceptionHandler) {
launch {
throw ArithmeticException("我是一个子异常")
}
//这时不会捕获到,会向上传播
// launch(exceptionHandler) {
// throw ArithmeticException("我是另外一个子异常")
// }
}

3.2.3 当父协程的所有子协程都结束后,原始的异常才会被父协程处理

val handler = CoroutineExceptionHandler { _, exception ->
println("捕捉到异常: $exception")
}
val job = GlobalScope.launch(handler) {
launch { // 第一个子协程
try {
delay(Long.MAX_VALUE)
} finally {
withContext(NonCancellable) {
println("第一个子协程还在运行,所以暂时不会处理异常")
delay(100)
println("现在子协程处理完成了")
}
}
}
launch { // 第二个子协程
delay(10)
println("第二个子协程出异常了")
throw ArithmeticException()
}
}
job.join()

//打印结棍:

第二个子协程出异常了
第一个子协程还在运行,所以暂时不会处理异常
现在子协程处理完成了
捕捉到异常: java.lang.ArithmeticException

3.2.4 异常聚合

第 1 个发生的异常会被优先y处理,在此之后发生的所有其他异常会被添加到最先发生的异常上, 作为被压制(suppressed)的异常

  val handler = CoroutineExceptionHandler { _, exception ->
println("捕捉到异常: $exception ${exception.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
delay(100)
throw IOException() // 第一个异常
}
launch {
try {
delay(Long.MAX_VALUE) // 当另一个同级的协程因 IOException 失败时,它将被取消
} finally {
throw ArithmeticException() // 同时抛出第二个异常
}
}

delay(Long.MAX_VALUE)
}
job.join()

输出:
捕捉到异常: java.io.IOException [java.lang.ArithmeticException]

3.2.5 launch 和 async异常处理

  • launch 直接抛出异常,无等待
  launch {
throw ArithmeticException("launch异常")
}

//打印
Exception in thread "main" java.lang.ArithmeticException: launch异常
  • async预期会在用户调用await()时,再反馈异常

直接在根协程(GlobalScope) 或 supervisor子协程时,async会在await()时抛出异常

  supervisorScope {
val deferred = async {
throw ArithmeticException("异常")
}
}
//打印结果:空
  • 在await()时才抛出异常
  supervisorScope {
val deferred = async {
throw ArithmeticException("异常")
}
try {
deferred.await()
} catch (e: Exception) {
println("捕获到一个异常$e")
}
}
//打印结果:
捕获到一个异常java.lang.ArithmeticException: 异常
  • tips: 如果不是直接在根协程(GlobalScope) 或 supervisor子协程时,async 和 launch表现一致,直接抛出异常,不会在await()时,再抛出异常
  supervisorScope {
launch {
val deferred = async {
throw ArithmeticException("异常")
}
}
}

3.2.6 coroutineScope外部可以用try-catch捕获(supervisor不可以)

 try {
coroutineScope {
launch {
throw ArithmeticException("异常")
}
}
} catch (e: Exception) {
println("捕捉到异常:$e")
}

//打印结果:
捕捉到异常:java.lang.ArithmeticException: 异常

3.3 监督作用域的传播机制

3.3.1 特性 单向向下传播

  • 监督作用域的传播机制 (独立决策的权利?)

示意图

- supervisor的示例代码

3.3.2 子协程可以单独设置CoroutineExceptionHandler

 supervisorScope {
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}

打印结果:
发现了异常java.lang.ArithmeticException: 异常出现了

3.3.3 监督作业只对它直接的子协程有用

  supervisorScope {
//监督作业只对它直接的子协程有用
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}
-无效示例代码
supervisorScope {
launch {
//监督作业的子子协程无法独立处理异常,向上抛异常
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}
}

//打印结果:
Exception in thread "main" java.lang.ArithmeticException: 异常出现了
at com.jinbo.kotlin.coroutine.C05_Exception$testDemo$2$1$1$1.invokeSuspend(C05_Exception.kt:1039)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:84)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.jinbo.kotlin.coroutine.C05_Exception.main(C05_Exception.kt:17)

3.4 正确使用coroutineExceptionHandler

3.4.1 根协程(GlobalScope)//TODO 确认

  GlobalScope.launch(exceptionHandler) {  }

3.4.2 supervisorScope 直接子级

 supervisorScope {
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}

3.4.3 手动创建的Scope(Job()/SupervisorJob())

 val scope = CoroutineScope(Job())
scope.launch(exceptionHandler) {
throw ArithmeticException("异常")
}

4 思考

4.1 android 的协同

  • viewmodelScope lifecycleScope

收起阅读 »

DiffUtil 让 RecyclerView 更好用

DiffUtil 让 RecyclerView 更好用前几天在写局部刷新RecyclerView时,评论区有掘友提到了DiffUtil,说实话,确实没有在项目中用到过,查了资料,DiffUtil帮我们做了很多刷新很多工作,真香。DiffUtil是什么DiffU...
继续阅读 »

DiffUtil 让 RecyclerView 更好用

前几天在写局部刷新RecyclerView时,评论区有掘友提到了DiffUtil,说实话,确实没有在项目中用到过,查了资料,DiffUtil帮我们做了很多刷新很多工作,真香。

DiffUtil是什么

DiffUtil 是来自recycleview-v7下的工具类,Diff 直接翻译过来是 差异、对比,所以这个工具类主要帮助我们对比两个数据集,寻找出最小的变化量。那么它和RecyclerView有什么关系呢,实际上我们只要把新旧数据集给到DiffUtil,那么它就会自动帮我们对比数据,并且刷新适配器,而不用我们判断,是增加了删除了等等,DiffUtil对比之后自动帮我们搞定,这就是它非常好用的地方了

常规的适配器

我们先用RecyclerView写一个常规的列表。
它拥有刷新item和item局部刷新的功能。
代码如下:

MainActivity: 主界面

public class MainActivity extends AppCompatActivity {

private RecyclerView recyclerView;

private List<PersonInfo> mDatas;
private PersonAdapter personAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

recyclerView = findViewById(R.id.recyclerView);
initData();
recyclerView.setLayoutManager(new LinearLayoutManager(this));
personAdapter = new PersonAdapter(this, mDatas);
recyclerView.setAdapter(personAdapter);
}

private void initData() {
mDatas = new ArrayList<>();
mDatas.add(new PersonInfo(1, "姓名1"));
mDatas.add(new PersonInfo(2, "姓名2"));
mDatas.add(new PersonInfo(3, "姓名3"));
mDatas.add(new PersonInfo(4, "姓名4"));
mDatas.add(new PersonInfo(5, "姓名5"));
mDatas.add(new PersonInfo(6, "姓名6"));
mDatas.add(new PersonInfo(7, "姓名7"));
mDatas.add(new PersonInfo(8, "姓名8"));
mDatas.add(new PersonInfo(9, "姓名9"));
mDatas.add(new PersonInfo(10, "姓名10"));
mDatas.add(new PersonInfo(11, "姓名11"));
}

public void ADD(View view) {
int position = mDatas.size();
List<PersonInfo> tempData = new ArrayList<>();

tempData.add(new PersonInfo(12, "姓名12"));
tempData.add(new PersonInfo(13, "姓名13"));
tempData.add(new PersonInfo(14, "姓名114"));

mDatas.addAll(tempData);
personAdapter.notifyItemRangeInserted(position, tempData.size());
}

public void DELETE(View view) {
mDatas.remove(1);
personAdapter.notifyItemRemoved(1);
}

public void UPDATE(View view) {
mDatas.get(1).setName("姓名:我被更新了");
personAdapter.notifyItemChanged(1);
}

public void UPDATE2(View view) {
mDatas.get(1).setName("姓名:我被更新了");

Bundle payload = new Bundle();
payload.putString("KEY_NAME", mDatas.get(1).getName());
personAdapter.notifyItemChanged(1, payload);
}
}
复制代码

PersonAdapter: 适配器

public class PersonAdapter extends RecyclerView.Adapter<PersonAdapter.DiffVH> {
private List<PersonInfo> mDatas;
private LayoutInflater mInflater;

public PersonAdapter(Context context, List<PersonInfo> mDatas) {
this.mDatas = mDatas;
mInflater = LayoutInflater.from(context);
}

public void setDatas(List<PersonInfo> mDatas) {
this.mDatas = mDatas;
}

@Override
public DiffVH onCreateViewHolder(ViewGroup parent, int viewType) {
return new DiffVH(mInflater.inflate(R.layout.item_person, parent, false));
}

@Override
public void onBindViewHolder(final DiffVH holder, final int position) {
PersonInfo personInfo = mDatas.get(position);
holder.tv_index.setText(String.valueOf(personInfo.getIndex()));
holder.tv_name.setText(String.valueOf(personInfo.getName()));
}

@Override
public void onBindViewHolder(DiffVH holder, int position, List<Object> payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
} else {
Bundle payload = (Bundle) payloads.get(0);
PersonInfo bean = mDatas.get(position);
for (String key : payload.keySet()) {
switch (key) {
case "KEY_INDEX":
holder.tv_index.setText(String.valueOf(bean.getIndex()));
break;
case "KEY_NAME":
holder.tv_name.setText(String.valueOf(bean.getName()));
break;
default:
break;
}
}
}
}

@Override
public int getItemCount() {
return mDatas != null ? mDatas.size() : 0;
}

class DiffVH extends RecyclerView.ViewHolder {
TextView tv_index;
TextView tv_name;

public DiffVH(View view) {
super(view);
tv_index = view.findViewById(R.id.tv_index);
tv_name = view.findViewById(R.id.tv_name);
}
}
}

复制代码

PersonInfo: 实体类

public class PersonInfo {
private int index;
private String name;

public PersonInfo(int index, String name) {
this.index = index;
this.name = name;
}

public int getIndex() {
return index;
}

public void setIndex(int index) {
this.index = index;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
复制代码

activity_main

<?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=".MainActivity">

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="增加"
android:onClick="ADD"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除"
android:onClick="DELETE"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="修改"
android:onClick="UPDATE"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="局部更新"
android:onClick="UPDATE2"/>

</LinearLayout>


<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />


</LinearLayout>

复制代码

item_person

<?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="wrap_content"
android:orientation="vertical"
tools:context=".MainActivity">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@color/purple_200"
android:orientation="horizontal"
android:padding="5dp">

<TextView
android:id="@+id/tv_index"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />

<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/tv_index" />

</RelativeLayout>


</LinearLayout>
复制代码

引入DiffUtil

我们创建一个DiffUtil类,在需要更新的时候,用DiffUtil中的方法去代替原本的刷新方法。

用新增举例,这样就能达到更新的目的

public void ADD(View view) {
List<PersonInfo> newData = new ArrayList<>();
newData.addAll(mDatas);
newData.add(new PersonInfo(12, "姓名12"));
newData.add(new PersonInfo(13, "姓名13"));
newData.add(new PersonInfo(14, "姓名114"));

DiffUtil.calculateDiff(new DiffUtilCallBack(newData,mDatas), true).dispatchUpdatesTo(personAdapter);
mDatas = newData;
personAdapter.setDatas(mDatas);
}

复制代码

DiffUtilCallBack

public class DiffUtilCallBack extends DiffUtil.Callback {
private List<PersonInfo> newlist;
private List<PersonInfo> oldlist;

public DiffUtilCallBack(List<PersonInfo> newlist, List<PersonInfo> oldlist) {
this.newlist = newlist;
this.oldlist = oldlist;
}

@Override
public int getOldListSize() {
return oldlist.size();
}

@Override
public int getNewListSize() {
return newlist.size();
}

@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
//判断是否是同一个item,可以在这里处理 判断是否是相同item的逻辑,比如id之类的
return newlist.get(newItemPosition).getIndex() == oldlist.get(oldItemPosition).getIndex();
}

@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
//判断数据是否发生改变,这个 方法会在上面的方法返回true时调用, 因为虽然item是同一个,但有可能item的数据发生了改变
return newlist.get(newItemPosition).getName().equals(oldlist.get(oldItemPosition).getName());
}
}
复制代码

万能适配器中的DiffUtil

配合RecyclerView,我一直在使用万能适配器(BaseRecyclerViewAdapterHelper),如果你也习惯了使用万能适配器,在它的3.0方法中引入了对DiffUtil的支持。

直接看官方文档吧。

代码下载:https://github.com/CymChad/BaseRecyclerViewAdapterHelper/archive/refs/heads/master.zip

收起阅读 »

iOS 上的 WebSocket 框架 Starscream

iOS
Starscream实现Websocket通讯1.Starscream 简介2.Starscream 使用2.1 Starscream基本使用2.2 Starscream高阶使用2.2.1 判断是否连接2.2.2 自定义头文件2.2.3 自定义HTTP方法2....
继续阅读 »

Starscream实现Websocket通讯
1.Starscream 简介
2.Starscream 使用
2.1 Starscream基本使用
2.2 Starscream高阶使用
2.2.1 判断是否连接
2.2.2 自定义头文件
2.2.3 自定义HTTP方法
2.2.4 协议
2.2.5 自签名 SSL
2.2.5.1 SSL引脚
2.2.5.2 SSL密码套件
2.2.6 压缩扩展
2.2.7 自定义队列
2.2.8 高级代理
3.Starscream 使用Demo
1.Starscream 简介

Starscream的特征:

Conforms to all of the base Autobahn test suite.
Nonblocking. Everything happens in the background, thanks to GCD.
TLS/WSS support.
Compression Extensions support (RFC 7692)
Simple concise codebase at just a few hundred LOC.
什么是websocket:
WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。
在 WebSocket API,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

HTTP 第一次出现是 1991 年,它设计为一种请求/响应式的通讯机制。Web 浏览器用这种机制工作良好,用户请求 web 页,服务器返回内容。但某些时候,需要有新数据时不经过用户请求就通知用户——也就是,服务器推。
HTTP 协议无法很好地解决推模型。在 websocket 出现前,web 服务通过一系列浏览器刷新机制来实现推模型,但效率无法让人满意。
webSocket 实现了服务端推机制。新的 web 浏览器全都支持 WebSocket,这使得它的使用超级简单。通过 WebSocket 能够打开持久连接,大部分网络都能轻松处理 WebSocket 连接。
WebSocket 通常应用在某些数据经常性或频繁改变的场景。例如 Facebook 中的 web 通知、Slack 中的实时聊天、交易系统中的变化的股票价格
socket通讯过程:


集成Websocket:

开发中推荐使用Starscream框架。通过pod 方式导入:

pod 'Starscream'
1
Starscream 使用swift版本为4.2
2.Starscream 使用
2.1 Starscream基本使用
import UIKit
import Starscream
@objc public protocol DSWebSocketDelegate: NSObjectProtocol{
/**websocket 连接成功*/
optional func websocketDidConnect(sock: DSWebSocket)
/**websocket 连接失败*/
optional func websocketDidDisconnect(socket: DSWebSocket, error: NSError?)
/**websocket 接受文字信息*/
func websocketDidReceiveMessage(socket: DSWebSocket, text: String)
/ **websocket 接受二进制信息*/
optional func websocketDidReceiveData(socket: DSWebSocket, data: NSData)
}
public class DSWebSocket: NSObject,WebSocketDelegate {
var socket:WebSocket!
weak var webSocketDelegate: DSWebSocketDelegate?
//单例
class func sharedInstance() -> DSWebSocket
{
return manger
}
static let manger: DSWebSocket = {
return DSWebSocket()
}()

//MARK:- 链接服务器
func connectSever(){
socket = WebSocket(url: NSURL(string: 你的URL网址如:ws://192.168.3.209:8080/shop))
socket.delegate = self
socket.connect()
}

//发送文字消息
func sendBrandStr(brandID:String){
socket.writeString(brandID))
}
//MARK:- 关闭消息
func disconnect(){
socket.disconnect()
}

//MARK: - WebSocketDelegate
//客户端连接到服务器时,websocketDidConnect将被调用。
public func websocketDidConnect(socket: WebSocket){
debugPrint("连接成功了: \(error?.localizedDescription)")
webSocketDelegate?.websocketDidConnect!(self)
}
//客户端与服务器断开连接后,将立即调用 websocketDidDisconnect。
public func websocketDidDisconnect(socket: WebSocket, error: NSError?){
debugPrint("连接失败了: \(error?.localizedDescription)")
webSocketDelegate?.websocketDidDisconnect!(self, error: error)
}
//当客户端从连接获取一个文本框时,调用 websocketDidReceiveMessage。
//注:一般返回的都是字符串
public func websocketDidReceiveMessage(socket: WebSocket, text: String){
debugPrint("接受到消息了: \(error?.localizedDescription)")
webSocketDelegate?.websocketDidReceiveMessage!(self, text: text)
}
public func websocketDidReceiveData(socket: WebSocket, data: NSData){
debugPrint("data数据")
webSocketDelegate?.websocketDidReceiveData!(self, data: data)
}
}

编写一个pong框架
writePong方法与writePing相同,但发送一个pong控制帧。
socket.write(pong: Data()) //example on how to write a pong control frame over the socket!
1
Starscream会自动响应传入的ping 控制帧,这样你就不需要手动发送 pong。

但是,如果出于某些原因需要控制这个 prosses,你可以通过禁用 respondToPingWithPong 来关闭自动 ping 响应。

socket.respondToPingWithPong=false//Do not automaticaly respond to incoming pings with pongs.
1
当客户端从连接获得一个pong响应时,调用 websocketDidReceivePong。 你需要实现WebSocketPongDelegate协议并设置一个额外的委托,例如: socket.pongDelegate = self

funcwebsocketDidReceivePong(socket: WebSocketClient, data: Data?) {
print("Got pong! Maybe some data: (data?.count)")
}

2.2 Starscream高阶使用
2.2.1 判断是否连接
if socket.isConnected {
// do cool stuff.
}

2.2.2 自定义头文件
你可以使用自己自定义的web socket标头覆盖默认的web socket标头,如下所示:
var request = URLRequest(url: URL(string: "ws://localhost:8080/")!)
request.timeoutInterval = 5
request.setValue("someother protocols", forHTTPHeaderField: "Sec-WebSocket-Protocol")
request.setValue("14", forHTTPHeaderField: "Sec-WebSocket-Version")
request.setValue("Everything is Awesome!", forHTTPHeaderField: "My-Awesome-Header")
let socket = WebSocket(request: request)

2.2.3 自定义HTTP方法
你的服务器在连接到 web socket时可能会使用不同的HTTP方法:
var request = URLRequest(url: URL(string: "ws://localhost:8080/")!)
request.httpMethod = "POST"
request.timeoutInterval = 5
let socket = WebSocket(request: request)

2.2.4 协议
如果需要指定协议,简单地将它的添加到 init:
//chat and superchat are the example protocols here
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
socket.delegate = self
socket.connect()

2.2.5 自签名 SSL
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])

//set this if you want to ignore SSL cert validation, so a self signed SSL certificate can be used.
socket.disableSSLCertValidation = true

2.2.5.1 SSL引脚
Starscream还支持SSL固定。
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
let data = ... //load your certificate from disk
socket.security = SSLSecurity(certs: [SSLCert(data: data)], usePublicKeys: true)
//socket.security = SSLSecurity() //uses the .cer files in your app's bundle

你可以加载证书的Data 小区,否则你可以使用 SecKeyRef,如果你想要使用 public 键。 usePublicKeys bool是使用证书进行验证还是使用 public 键。 如果选择 usePublicKeys,将自动从证书中提取 public 密钥。

2.2.5.2 SSL密码套件
要使用SSL加密连接,你需要告诉小红你的服务器支持的密码套件。
socket = WebSocket(url: URL(string: "wss://localhost:8080/")!, protocols: ["chat","superchat"])

// Set enabled cipher suites to AES 256 and AES 128
socket.enabledSSLCipherSuites = [TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]

如果你不知道服务器支持哪些密码套件可以查看:SSL Labs

2.2.6 压缩扩展
Starscream支持压缩扩展( RFC 7692 )。 默认情况下,压缩是启用的,但是只有当服务器支持压缩时才会使用压缩。 你可以通过 .enableCompression 属性启用或者禁用压缩:
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!)
socket.enableCompression = false

如果应用程序正在传输已经压缩。随机或者其他uncompressable数据,则应禁用压缩。
2.2.7 自定义队列
调用委托方法时可以指定自定义队列。 默认使用 DispatchQueue.main,因此使所有委托方法调用都在主线程上运行。 重要的是要注意,所有 web socket处理都是在后台线程上完成的,只有修改队列时才更改委托方法。 实际的处理总是在后台线程上,不会暂停你的应用程序。
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
//create a custom queue
socket.callbackQueue = DispatchQueue(label: "com.vluxe.starscream.myapp")

2.2.8 高级代理
socket.advancedDelegate = self

websocketDidReceiveMessage
func websocketDidReceiveMessage(socket: WebSocketClient, text: String, response: WebSocket.WSResponse) {
print("got some text: \(text)")
print("First frame for this message arrived on \(response.firstFrame)")
}

websocketDidReceiveData
func websocketDidReceiveData(socket: WebSocketClient, data: Date, response: WebSocket.WSResponse) {
print("got some data it long: \(data.count)")
print("A total of \(response.frameCount) frames were used to send this data")
}

websocketHttpUpgrade
当发送HTTP升级请求后,会返回下面回调

func websocketHttpUpgrade(socket: WebSocketClient, request: CFHTTPMessage) {
print("the http request was sent we can check the raw http if we need to")
}

func websocketHttpUpgrade(socket: WebSocketClient, response: CFHTTPMessage) {
print("the http response has returned.")
————————————————

原文链接:https://blog.csdn.net/kyl282889543/article/details/100655005

收起阅读 »

iOS 15-适配要点

iOS
增加UISheetPresentationController,通过它可以控制 Modal 出来的 UIViewController 的显示大小,且可以通过拖拽手势在不同大小之间进行切换。只需要在跳转的目标 UIViewController 做如下处理:if ...
继续阅读 »

  1. 增加UISheetPresentationController,通过它可以控制 Modal 出来的 UIViewController 的显示大小,且可以通过拖拽手势在不同大小之间进行切换。只需要在跳转的目标 UIViewController 做如下处理:

    if let presentationController = presentationController as? UISheetPresentationController {
    // 显示时支持的尺寸
    presentationController.detents = [.medium(), .large()]
    // 显示一个指示器表示可以拖拽调整大小
    presentationController.prefersGrabberVisible = true
    }
  2. UIButton支持更多配置。UIButton.Configuration是一个新的结构体,它指定按钮及其内容的外观和行为。它有许多与按钮外观和内容相关的属性,如cornerStyle、baseForegroundColor、baseBackgroundColor、buttonSize、title、image、subtitle、titlePadding、imagePadding、contentInsets、imagePlacement等。

    // Plain
    let plain = UIButton(configuration: .plain(), primaryAction: nil)
    plain.setTitle("Plain", for: .normal)
    // Gray
    let gray = UIButton(configuration: .gray(), primaryAction: nil)
    gray.setTitle("Gray", for: .normal)
    // Tinted
    let tinted = UIButton(configuration: .tinted(), primaryAction: nil)
    tinted.setTitle("Tinted", for: .normal)
    // Filled
    let filled = UIButton(configuration: .filled(), primaryAction: nil)
    filled.setTitle("Filled", for: .normal)

    Snipaste_2021-07-11_15-26-55.png16259886795922.png

  3. 推出CLLocationButton用于一次性定位授权,该内容内置于CoreLocationUI模块,但如果需要获取定位的详细信息仍然需要借助于CoreLocation

    let locationButton = CLLocationButton()
    // 文字
    locationButton.label = .currentLocation
    locationButton.fontSize = 20
    // 图标
    locationButton.icon = .arrowFilled
    // 圆角
    locationButton.cornerRadius = 10
    // tint
    locationButton.tintColor = UIColor.systemPink
    // 背景色
    locationButton.backgroundColor = UIColor.systemGreen
    // 点击事件,应该在在其中发起定位请求
    locationButton.addTarget(self, action: #selector(getCurrentLocation), for: .touchUpInside)
  4. URLSession 推出支持 async/await 的 API,包括获取数据、上传与下载。

    // 加载数据
    let (data, response) = try await URLSession.shared.data(from: url)
    // 下载
    let (localURL, _) = try await session.download(from: url)
    // 上传
    let (_, response) = try await session.upload(for: request, from: data)

  5. 系统图片支持多个层,支持多种渲染模式。

    // hierarchicalColor:多层渲染,透明度不同
    let config = UIImage.SymbolConfiguration(hierarchicalColor: .systemRed)
    let image = UIImage(systemName: "square.stack.3d.down.right.fill", withConfiguration: config)
    // paletteColors:多层渲染,设置不同风格
    let config2 = UIImage.SymbolConfiguration(paletteColors: [.systemRed, .systemGreen, .systemBlue])
    let image2 = UIImage(systemName: "person.3.sequence.fill", withConfiguration: config2)
  6. UINavigationBar、UIToolbar 和 UITabBar 设置颜色,需要使用 UIBarAppearance APIs。

    // UINavigationBar
    let navigationBarAppearance = UINavigationBarAppearance()
    navigationBarAppearance.backgroundColor = .red
    navigationController?.navigationBar.scrollEdgeAppearance = navigationBarAppearance
    navigationController?.navigationBar.standardAppearance = navigationBarAppearance
    // UIToolbar
    let toolBarAppearance = UIToolbarAppearance()
    toolBarAppearance.backgroundColor = .blue
    navigationController?.toolbar.scrollEdgeAppearance = toolBarAppearance
    navigationController?.toolbar.standardAppearance = toolBarAppearance
    // UITabBar
    let tabBarAppearance = UITabBarAppearance()
    toolBarAppearance.backgroundColor = .purple
    tabBarController?.tabBar.scrollEdgeAppearance = tabBarAppearance
    tabBarController?.tabBar.standardAppearance = tabBarAppearance
  7. UITableView 新增了属性 sectionHeaderTopPadding,会给每一个section 的 header 增加一个默认高度。

    tableView.sectionHeaderTopPadding = 0
  8. UIImage 新增了几个调整尺寸的方法。

    // preparingThumbnail
    UIImage(named: "sv.png")?.preparingThumbnail(of: CGSize(width: 200, height: 100))
    // prepareThumbnail,闭包中直接获取调整后的UIImage
    UIImage(named: "sv.png")?.prepareThumbnail(of: CGSize(width: 200, height: 100)) { image in
    // 需要回到主线程更新UI
    }
    // byPreparingThumbnail
    await UIImage(named: "sv.png")?.byPreparingThumbnail(ofSize: CGSize(width: 100, height: 100))

文中代码已在 Xcode 13 Beta3 中测试通过, 案例源代码下载地址

收起阅读 »

iOS Runtime (四)Runtime的消息机制

iOS
引言 iOS的消息转发机制,在我们开发中有时候忘记实现某个声明的方法,从而在运行过程中调用该方法出现崩溃, 当然这类问题是可以解决的,在当前对象或者父类对象中添加对象的方法实现,再重新运行,调用该方法就能解决这个问题,又或者在我们运行的时候动态的去添加接收者中...
继续阅读 »

引言


iOS的消息转发机制,在我们开发中有时候忘记实现某个声明的方法,从而在运行过程中调用该方法出现崩溃,


当然这类问题是可以解决的,在当前对象或者父类对象中添加对象的方法实现,再重新运行,调用该方法就能解决这个问题,又或者在我们运行的时候动态的去添加接收者中未知方法实现,这就是这篇重点要学习的内容。


错误异常实例


创建Game对象代码

#import <Foundation/Foundation.h>
@interface Game : NSObject
- (void)Play;
- (void)DoThings:(NSString *)Str Num:(NSInteger)num;
@end

#import "Game.h"
@implementation Game
- (void)Play{
NSLog(@"the game is play");
}
@end

调用该对象中没有实现的方法 DoThings: Num:


Game *game = [[Game alloc]init];
[game DoThings:@"wodenidetade" Num:10];

当我们运行的时候会报当调用的对象的方法不存在,即便是消息转发过后还是不存在的时候,就会抛出这个异常


解决方案


第一种方式

遇到这种情况通常第一个想法就是在该对象或继承树中的实现文件中添加该方法并实现,这种形式,就是你必须要去实现方法,需要开发者主动去写代码。


第二种方式

消息转发在运行时:


1.动态方法解析

+(BOOL)resolveInstanceMethod:(SEL)sel 实例方法解析
+(BOOL)resolveClassMethod:(SEL)sel 类方法解析

当运用消息转发运行时,根据调用的方法类型调用这两个方法其中一个,返回值BOOL类型,告诉系统该消息是否被处理,YES处理 NO 未处理



  • resolveInstanceMethod实例方法调用

  • resolveClassMethod类方法调用


这样的作用是: 当接受者接受到的消息方法并没有找到的情况下,系统会调用该函数,给予这个对象一次动态添加该消息方法实现的机会,如果该对象动态的添加了这个方法的实现,就返回YES,告诉系统这个消息我已经处理完毕。再次运行该方法。


注意:

当这个对象在实现了resolveInstanceMethod,resolveClassMethod两个方法,并没有对该对象消息进行处理,那么该方法会被调用两次:


一次是没有找到该方法需要对象解析处理;第二次是告诉系统我处理完成需要再次调用该方法但实际上并没有处理完成,所以会调用第二次该方法崩溃


2.后备接收者对象

-(id)forwardingTargetForSelector:(SEL)aSelector

在消息转发第一次方法解析中没有处理方法,并告诉系统本对象无法处理,需另寻办法,那么系统会给予另外一个办法,就是让别的对象B来处理该问题,如果对象B能够处理该消息,那么该消息转发结束。


将未知SEL作为参数传入,寻找另外对象处理,如果可以处理,返回该对象


3.以其他形式实现该消息方法

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
-(void)forwardInvocation:(NSInvocation *)anInvocation

当我们在前面两个步骤都没有处理该未知SEL时,就会到第三个步骤,上述两个方法是最后寻找IML的机会



  • 将未知SEL作为参数传入methodSignatureForSelector,在该方法中处理该消息,一旦能够处理,返回方法签名(自由的修改方法签名,apple签名),让后续forwardInvocation来进行处理



  • forwardInvocation中我们可以做很多的操作,这个方法比forwardingTargetForSelector更灵活



    • 也可以做到像forwardingTargetForSelector一样的结果,不同的是一个是让别的对象去处理,后者是直接切换调用目标,也就是该方法的Target

    • 我们也可以修改该方法的SEL,就是重新替换一个新的SEL

    • ....




4.直到最后未处理,抛出异常

-(void)doesNotRecognizeSelector:(SEL)aSelector

作为找不到函数实现的最后一步,NSObject实现这个函数只有一个功能,就是抛出异常。


虽然理论上可以重载这个函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。


流程图


代码实现

/*
* 第一步 实例方法专用 方法解析
**/
+ (BOOL)resolveInstanceMethod:(SEL)sel{

NSLog(@"%@",NSStringFromSelector(sel));

if(sel == @selector(DoThings:Num:)){
class_addMethod([self class], sel, (IMP)MyMethodIMP, "v@:");
return YES;
}

return [super resolveInstanceMethod:sel];
}

/*
* 第二步 如果第一步未处理,那么让别的对象去处理这个方法
**/
-(id)forwardingTargetForSelector:(SEL)aSelector{
if([NSStringFromSelector(aSelector) isEqualToString:@"DoThings:Num:"]){
return [[Tools alloc]init];
}
return [super forwardingTargetForSelector:aSelector];
}

/*
* 第三步 如果前两步未处理,这是最后处理的机会将目标函数以其他形式执行
**/
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSString *SelStr = NSStringFromSelector(aSelector);
if([SelStr isEqualToString:@"DoThings:Num:"]){
[NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
//改变消息接受者对象
[anInvocation invokeWithTarget:[[Tools alloc]init]];

//改变消息的SEL
anInvocation.selector = @selector(flyGame);
[anInvocation invokeWithTarget:self];
}

- (void)flyGame{
NSLog(@"我要飞翔追逐梦想!");
}

/*
* 作为找不到函数实现的最后一步,NSObject实现这个函数只有一个功能,就是抛出异常。
* 虽然理论上可以重载这个函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。
*
***/
- (void)doesNotRecognizeSelector:(SEL)aSelector{

}


作者:响彻天堂
链接:https://www.jianshu.com/p/019bce1e6253

上一篇链接:https://www.imgeek.org/article/825358865

收起阅读 »

Java正则表达式语法大全

在我们日常开发项目中经常用到正则表达式/比如邮箱/电话手机号/域名/ip等)都会经常用到其实一个字符串就是一个简单的正则表达式,例如 Hello World 正则表达式匹配 "Hello World" 字符串。.(点号)也是一个正则表达式,...
继续阅读 »

在我们日常开发项目中经常用到正则表达式/比如邮箱/电话手机号/域名/ip等)都会经常用到

其实一个字符串就是一个简单的正则表达式,例如 Hello World 正则表达式匹配 "Hello World" 字符串。

.(点号)也是一个正则表达式,它匹配任何一个字符如:"a" 或 "1"。

下表列出了一些正则表达式的实例及描述:

正则表达式描述
this is text匹配字符串 "this is text"
this\s+is\s+text注意字符串中的 \s+ 。匹配单词 "this" 后面的 \s+  可以匹配多个空格,之后匹配 is 字符串,再之后 \s+  匹配多个空格然后再跟上 text 字符串。可以匹配这个实例:this is text
^\d+(.\d+)?^ 定义了以什么开始\d+ 匹配一个或多个数字? 设置括号内的选项是可选的. 匹配 "."可以匹配的实例:"5", "1.5" 和 "2.21"。

java 正则表达式和 Perl 的是最为相似的。

java.util.regex 包主要包括以下三个类:

  • Pattern 类:

    pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。

  • Matcher 类:

    Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。

  • PatternSyntaxException:

    PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。 以下实例中使用了正则表达式  .runoob.  用于查找字符串中是否包了 runoob 子串:

import java.util.regex.*;
class RegexExample1{
public static void main(String[] args){
String content = "I am noob " + "from runoob.com.";
String pattern = ".*runoob.*";
boolean isMatch = Pattern.matches(pattern, content);
System.out.println("字符串中是否包含了 'runoob' 子字符串? " + isMatch);
}
}

最终打印字符串中是否包含了 'runoob' 子字符串? true

正则表达式语法大全

在其他语言中,\ 表示:我想要在正则表达式中插入一个普通的(字面上的)反斜杠,请不要给它任何特殊的意义。

在 Java 中,\ 表示:我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。 所以,在其他的语言中(如 Perl),一个反斜杠 \ 就足以具有转义的作用,而在 Java 中正则表达式中则需要有两个反斜杠才能被解析为其他语言中的转义作用。也可以简单的理解在 Java 的正则表达式中,两个 \ 代表其他语言中的一个 \,这也就是为什么表示一位数字的正则表达式是 \d,而表示一个普通的反斜杠是 \。

System.out.print("\");    // 输出为 \
System.out.print("\\"); // 输出为 \
字符说明
\将下一字符标记为特殊字符、文本、反向引用或八进制转义符。例如, n匹配字符 n。\n 匹配换行符。序列 \\ 匹配 \ ,\( 匹配 (。
^匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与"\n"或"\r"之后的位置匹配。
$匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与"\n"或"\r"之前的位置匹配。
*零次或多次匹配前面的字符或子表达式。例如,zo* 匹配"z"和"zoo"。* 等效于 {0,}。
+一次或多次匹配前面的字符或子表达式。例如,"zo+"与"zo"和"zoo"匹配,但与"z"不匹配。+ 等效于 {1,}。
?零次或一次匹配前面的字符或子表达式。例如,"do(es)?"匹配"do"或"does"中的"do"。? 等效于 {0,1}。
{n}n 是非负整数。正好匹配 n 次。例如,"o{2}"与"Bob"中的"o"不匹配,但与"food"中的两个"o"匹配。
{n,}n 是非负整数。至少匹配 n 次。例如,"o{2,}"不匹配"Bob"中的"o",而匹配"foooood"中的所有 o。"o{1,}"等效于"o+"。"o{0,}"等效于"o*"。
{n,m}m 和 n 是非负整数,其中 n <= m。匹配至少 n 次,至多 m 次。例如,"o{1,3}"匹配"fooooood"中的头三个 o。'o{0,1}' 等效于 'o?'。注意:您不能将空格插入逗号和数字之间。
?当此字符紧随任何其他限定符(*、+、?、{n}、{n,}、{n,m})之后时,匹配模式是"非贪心的"。"非贪心的"模式匹配搜索到的、尽可能短的字符串,而默认的"贪心的"模式匹配搜索到的、尽可能长的字符串。例如,在字符串"oooo"中,"o+?"只匹配单个"o",而"o+"匹配所有"o"。
.匹配除"\r\n"之外的任何单个字符。若要匹配包括"\r\n"在内的任意字符,请使用诸如"[\s\S]"之类的模式。
(pattern)匹配 pattern 并捕获该匹配的子表达式。可以使用  0…9 属性从结果"匹配"集合中检索捕获的匹配。若要匹配括号字符 ( ),请使用"("或者")"。
(?:pattern)匹配 pattern 但不捕获该匹配的子表达式,即它是一个非捕获匹配,不存储供以后使用的匹配。这对于用"or"字符 (
(?=pattern)执行正向预测先行搜索的子表达式,该表达式匹配处于匹配 pattern 的字符串的起始点的字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,'Windows (?=95
(?!pattern)执行反向预测先行搜索的子表达式,该表达式匹配不处于匹配 pattern 的字符串的起始点的搜索字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,'Windows (?!95
xy
[xyz]字符集。匹配包含的任一字符。例如,"[abc]"匹配"plain"中的"a"。
[^xyz]反向字符集。匹配未包含的任何字符。例如,"[^abc]"匹配"plain"中"p","l","i","n"。
[a-z]字符范围。匹配指定范围内的任何字符。例如,"[a-z]"匹配"a"到"z"范围内的任何小写字母。
[^a-z]反向范围字符。匹配不在指定的范围内的任何字符。例如,"[^a-z]"匹配任何不在"a"到"z"范围内的任何字符。
\b匹配一个字边界,即字与空格间的位置。例如,"er\b"匹配"never"中的"er",但不匹配"verb"中的"er"。
\B非字边界匹配。"er\B"匹配"verb"中的"er",但不匹配"never"中的"er"。
\cx匹配 x 指示的控制字符。例如,\cM 匹配 Control-M 或回车符。x 的值必须在 A-Z 或 a-z 之间。如果不是这样,则假定 c 就是"c"字符本身。
\d数字字符匹配。等效于 [0-9]。
\D非数字字符匹配。等效于 [^0-9]。
\f换页符匹配。等效于 \x0c 和 \cL。
\n换行符匹配。等效于 \x0a 和 \cJ。
\r匹配一个回车符。等效于 \x0d 和 \cM。
\s匹配任何空白字符,包括空格、制表符、换页符等。与 [ \f\n\r\t\v] 等效。
\S匹配任何非空白字符。与 [^ \f\n\r\t\v] 等效。
\t制表符匹配。与 \x09 和 \cI 等效。
\v垂直制表符匹配。与 \x0b 和 \cK 等效。
\w匹配任何字类字符,包括下划线。与"[A-Za-z0-9_]"等效。
\W与任何非单词字符匹配。与"[^A-Za-z0-9_]"等效。
\xn匹配 n,此处的 n 是一个十六进制转义码。十六进制转义码必须正好是两位数长。例如,"\x41"匹配"A"。"\x041"与"\x04"&"1"等效。允许在正则表达式中使用 ASCII 代码。
*num*匹配 num,此处的 num 是一个正整数。到捕获匹配的反向引用。例如,"(.)\1"匹配两个连续的相同字符。
*n*标识一个八进制转义码或反向引用。如果 *n* 前面至少有 n 个捕获子表达式,那么 n 是反向引用。否则,如果 n 是八进制数 (0-7),那么 n 是八进制转义码。
*nm*标识一个八进制转义码或反向引用。如果 *nm* 前面至少有 nm 个捕获子表达式,那么 nm 是反向引用。如果 *nm* 前面至少有 n 个捕获,则 n 是反向引用,后面跟有字符 m。如果两种前面的情况都不存在,则 *nm* 匹配八进制值 nm,其中 n 和 m 是八进制数字 (0-7)。
\nml当 n 是八进制数 (0-3),m 和 l 是八进制数 (0-7) 时,匹配八进制转义码 nml。
\un匹配 n,其中 n 是以四位十六进制数表示的 Unicode 字符。例如,\u00A9 匹配版权符号 (©)。

注意:根据 Java Language Specification 的要求,Java 源代码的字符串中的反斜线被解释为 Unicode 转义或其他字符转义。因此必须在字符串字面值中使用两个反斜线,表示正则表达式受到保护,不被 Java 字节码编译器解释。例如,当解释为正则表达式时,字符串字面值 "\b" 与单个退格字符匹配,而 "\b" 与单词边界匹配。字符串字面值 "(hello)" 是非法的,将导致编译时错误;要与字符串 (hello) 匹配,必须使用字符串字面值 "\(hello\)"。


作者:java李杨勇
链接:https://juejin.cn/post/7020303920966664222
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

熬夜再战Android之修炼Kotlin-【Get和Set】、【继承】、【抽象类/嵌套类/内部类】篇

前提 当前环境 2021年10月8日最新下载2020.3.1 Patch 2 版本 👉实践过程 😜Get和Set 其实Kotlin声明实体类之后,里面的变量默认就带有set和get的属性功能了。除非想要特殊业务内容。 比如set需要结合项目进行其他业务处理,g...
继续阅读 »

前提


当前环境


2021年10月8日最新下载2020.3.1 Patch 2 版本


👉实践过程


😜Get和Set


其实Kotlin声明实体类之后,里面的变量默认就带有set和get的属性功能了。除非想要特殊业务内容。


比如set需要结合项目进行其他业务处理,get也是同样的道理。


【filed】是系统内置的一个关键字,算是中间变量


除了这些


var name: String? = null
        set(value) { //value随意起名
            field = value  //这个field是系统内置的 用在get
        }
        get() {
            return field + "这是返回"
        }
var urlJUEJIIN: String? = null
        get() =field+"这是只有get"
var urlCSDN: String? = null
var urlList: List<String>? = null

😜继承


在Java中可以说所有的类都继承自Object,而在Kotlin中可以说所有的是继承自Any类。


在Java中继承使用关键字【extends】,而在Kotlin中使用【:】(英文冒号)


除此之外,不管是方法重写还是属性变量重写,前面都加上【override】关键字,这一点和Java一样


class EntityTwo : Entity {
    constructor() {

    }

    constructor(name: String) : this(name, 0) {

    }

    //不同参数的次要构造函数
    constructor(name: String, age: Int) : super(name, age) {
        Log.e("TAG,", "执行了子类构造器$name===$age")
    }
}

😜接口


这点也和Java类似,使用【interface】定义,使用上也没差距


修饰类的关键字有



  • abstract    // 说明该类为抽象类 

  • final       // 说明该类为类不可继承,默认属性

  • enum        // 说明该类为枚举类

  • open        // 说明该类为类可继承,类默认是final的

  • annotation  // 说明该类为注解类


访问权限的修饰符有:



  • private    // 访问权限-仅在同一个文件中可见

  • protected  // 访问权限-同一个文件中或子类可见

  • public     // 访问权限-所有调用的地方都可见

  • internal   // 访问权限-同一个模块中可见


经过学习和试验验证,小空决定还是用Java的实体类吧,反正他们有互操作性。


😜抽象类/嵌套类/内部类


小空带大家直接用实例来看明白


abstract class EntityThree {
    abstract fun methonOne()
}

//嵌套类实例
class One {                  // 这是外部类
    private val age: Int = 1
    class Two {             // 这是在类里面的类,叫做嵌套类
        fun hello() {

        }

        fun hi() = 3
    }
}

//内部类使用关键字inner
class Three {
    inner class Four { //这个Four类是内部类
        fun hello() {

        }
        fun hi() = 3
    }
}
//这是引用示例
var one = Three().Four().hello()

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

使用BlackHook(黑钩) 可以Hook一切java或者kotlin方法

前言 之前做内存优化的时候,为了实现对线程的使用监控,借助了一个第三方的hook框架(epic),这个框架可以hook一切java方法,使用也简单,但是最大的问题是它有较严重的兼容性问题,部分机型会出现闪退的现象,这就导致它不能被带到线上使用,只能在线下使用,...
继续阅读 »

前言


之前做内存优化的时候,为了实现对线程的使用监控,借助了一个第三方的hook框架(epic),这个框架可以hook一切java方法,使用也简单,但是最大的问题是它有较严重的兼容性问题,部分机型会出现闪退的现象,这就导致它不能被带到线上使用,只能在线下使用,为了实现在线上监控线程的使用,于是我便开发了BlackHook插件,也可以hook一切java方法,而且很稳定,没有兼容性问题,真是十足的黑科技


简介


BlackHook 是一个实现编译时插桩的gradle插件,基于ASM+Tranfrom实现,理论上可以hook任意一个java方法或者kotlin方法,只要代码对应的字节码可以在编译阶段被Tranfrom扫描到,就可以使用ASM在代码对应的字节码处插入特定字节码,从而hook该方法


优点



  1. 用DSL(领域特定语言)使用该插件,使用简单,配置灵活,而且插入的字节码可以使用
    ASM Bytecode Viewer Support Kotlin 插件自动生成,上手难度低

  2. 理论上可以hook任意一个java方法,只要代码对应的字节码可以在编译阶段被Tranfrom扫描到

  3. 基于ASM+Tranfrom实现,在编译阶段直接修改字节码,效率高,没有兼容性问题


使用


在app下面的build.gradle文件添加如下代码


apply plugin: 'com.blackHook'

/**
* 返回hook线程构造函数的字节码,Hook 线程的构造函数,让每次在调用Thread的构造函数的时候就会调用
* ThreadCheck类的 printThread方法,从而在控制台打印线程的构造函数的调用堆栈,这些代码可以借助
* ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一个类,用于修改字节码
*/
void createHookThreadByteCode(MethodVisitor mv, String className) {
mv.visitTypeInsn(Opcodes.NEW, "com/quwan/tt/asmdemoapp/ThreadCheck")
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "<init>", "()V", false)
mv.visitLdcInsn(className)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "printThread", "(Ljava/lang/String;)V", false)
}

/**
* 返回需要被hook的方法,需要被hook的方法是Thread的构造函数
*/
List<HookMethod> getHookMethods() {
List<HookMethod> hookMethodList = new ArrayList<>()
hookMethodList.add(new HookMethod("java/lang/Thread", "<init>", "()V", { MethodVisitor mv -> createHookThreadByteCode(mv, "java/lang/Thread") }))
return hookMethodList
}

blackHook {
//表示要处理的数据类型是什么,CLASSES 表示要处理编译后的字节码(可能是 jar 包也可能是目录),RESOURCES 表示要处理的是标准的 java 资源
inputTypes BlackHook.CONTENT_CLASS
//表示Transform 的作用域,这里设置的SCOPE_FULL_PROJECT代表作用域是全工程
scopes BlackHook.SCOPE_FULL_PROJECT
//表示是否支持增量编译,false不支持
isIncremental false
//表示hook的方法
hookMethodList = getHookMethods()
}

以上的代码其实是hook的Thread的构造函数,将ThreadCheck的printThread方法hook到了Thread的构造函数中,每次调用线程的构造函数的时候就会调用ThreadCheck的printThread方法,这个方法会打印出Thread的构造函数的调用堆栈,从而可以在控制台知道哪个页面的哪行代码实例化了Thread,ThreadCheck的代码如下


class ThreadCheck {

var isCanAppendLog = false
private val tag = "====>ThreadCheck"

fun printThread(name : String){

println("====>printThread:${name}")

val es = Thread.currentThread().stackTrace

val normalInfo = StringBuilder(" \nThreadTrace:")
.append("\nthreadName:${name}")
.append("\n====================================threadTraceStart=======================================")

for (e in es) {

if (e.className == "dalvik.system.VMStack" && e.methodName == "getThreadStackTrace") {
isCanAppendLog = false
}

if (e.className.contains("ThreadCheck") && e.methodName == "printThread") {
isCanAppendLog = true
} else {
if (isCanAppendLog) {
normalInfo.append("\n${e.className}(lineNumber:${e.lineNumber})")
}
}
}
normalInfo.append("\n=====================================threadTraceEnd=======================================")

Log.i(tag, normalInfo.toString())
}

}

上面的代码获取了调用堆栈,并且打印到控制台


实现原理


首先它是一个gradle 的自定义Plugin,其次它是通过在编译阶段修改字节码实现Hook,在编译阶段通过Tranfrom扫描所有的字节码,然后根据在使用插件的时候设置的需要被Hook的方法,插入需要被插入的字节码,
需要被插入的字节码也是在使用的时候设置的,例如下面的代码


/**
* 返回hook线程构造函数的字节码,Hook 线程的构造函数,让每次在调用Thread的构造函数的时候就会调用
* ThreadCheck的 printThread方法,从而在控制台打印线程的构造函数的调用堆栈,这些代码可以借助
* ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一个类,用于修改字节码
*/
void createHookThreadByteCode(MethodVisitor mv, String className) {
mv.visitTypeInsn(Opcodes.NEW, "com/quwan/tt/asmdemoapp/ThreadCheck")
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "<init>", "()V", false)
mv.visitLdcInsn(className)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "printThread", "(Ljava/lang/String;)V", false)
}

准备过程


实现这个gradle插件需要我们有足够的预备知识,如下:



实现过程


1.自定义gradle plugin


因为这是一个gradle插件,所以需要我们自定义一个gradle的plugin


1. 新建一个模块


在工程中新建一个模块,命名为"buildSrc",注意,一定要命名为buildSrc,否则在工程中必须要将代码发布到本地或者远程maven仓库中才能正常使用,这样调试不方便,如下所示:


image.png


2. 然后配置gradle脚本,代码如下所示:


plugins {
id 'java-library'
id 'maven'
id 'groovy'
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

dependencies {
implementation gradleApi()//gradle sdk
implementation localGroovy()
implementation "com.android.tools.build:gradle:3.4.1"
implementation 'org.ow2.asm:asm:9.1'
implementation 'org.ow2.asm:asm-commons:9.1'
}

3. 实现Plugin类


新建groovy文件夹,新建BlackHookPlugin类,继承Transform类,实现Plugin接口


image.png


BlackHookPlugin代码如下所示:


package com.blackHook.plugin

class BlackHookPlugin extends Transform implements Plugin<Project> {

....此处省略了很多代码

@Override
void apply(Project target) {
println("注册了")
project = target
target.extensions.getByType(BaseExtension).registerTransform(this)
target.extensions.create("blackHook", BlackHook.class)
}

....此处省略了很多代码
}

新建resources文件夹,新建com.blackHook.properties文件,如下所示


image.png


com.blackHook.properties文件的代码如下:


implementation-class=com.blackHook.plugin.BlackHookPlugin

implementation-class的值即是BlackHookPlugin的完整路径,另外,com.blackHook.properties文件的文件名既是使用插件的时候的插件名,如下代码:


apply plugin: 'com.blackHook'

2. 实现BlackHook扩展类


新建BlackHook类,代码如下


public class BlackHook {

Closure methodHooker;

List<HookMethod> hookMethodList = new ArrayList<>();

public static final String CONTENT_CLASS = "CONTENT_CLASS";
public static final String CONTENT_JARS = "CONTENT_JARS";
public static final String CONTENT_RESOURCES = "CONTENT_RESOURCES";

public static final String SCOPE_FULL_PROJECT = "SCOPE_FULL_PROJECT";
public static final String PROJECT_ONLY = "PROJECT_ONLY";

String inputTypes = CONTENT_CLASS;

String scopes = SCOPE_FULL_PROJECT;

boolean isNeedLog = false;

boolean isIncremental = false;

public Closure getMethodHooker() {
return methodHooker;
}

public void setMethodHooker(Closure methodHooker) {
this.methodHooker = methodHooker;
}

public List<HookMethod> getHookMethodList() {
return hookMethodList;
}

public void setHookMethodList(List<HookMethod> hookMethodList) {
this.hookMethodList = hookMethodList;
}

public String getInputTypes() {
return inputTypes;
}

public void setInputTypes(String inputTypes) {
this.inputTypes = inputTypes;
}

public String getScopes() {
return scopes;
}

public void setScopes(String scopes) {
this.scopes = scopes;
}

public boolean getIsIncremental() {
return isIncremental;
}

public void setIsIncremental(boolean incremental) {
isIncremental = incremental;
}

public boolean getIsNeedLog() {
return isNeedLog;
}

public void setIsNeedLog(boolean needLog) {
isNeedLog = needLog;
}
}

这个类用于接收开发人员使用插件的时候设置的参数和需要被Hook的方法以及参与Hook的字节码,我们在使用blackHook插件的时候可以使用DSL的方式来使用,如下代码所示:


blackHook {
//表示要处理的数据类型是什么,CLASSES 表示要处理编译后的字节码(可能是 jar 包也可能是目录), RESOURCES 表示要处理的是标准的 java 资源
inputTypes BlackHook.CONTENT_CLASS
//表示Transform 的作用域,这里设置的SCOPE_FULL_PROJECT代表作用域是全工程
scopes BlackHook.SCOPE_FULL_PROJECT
//表示是否支持增量编译,false不支持
isIncremental false
//表示hook的方法
hookMethodList = getHookMethods()
}

之所以可以这么做是因为我们在BlackHookPlugin将BlackHook类添加到了target.extensions(扩展属性)中,
如下代码:


class BlackHookPlugin extends Transform implements Plugin<Project> {
@Override
void apply(Project target) {
target.extensions.create("blackHook", BlackHook.class)
}
}

3.开始实现扫描


需要在BlackHookPlugin的transform()方法中扫描全局代码,代码如下:


  @Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
if (outputProvider != null) {
outputProvider.deleteAll()
}
if (blackHook == null) {
blackHook = new BlackHook()
blackHook.methodHooker = project.extensions.blackHook.methodHooker
blackHook.isNeedLog = project.extensions.blackHook.isNeedLog
for (int i = 0; i < project.extensions.blackHook.hookMethodList.size(); i++) {
HookMethod hookMethod = new HookMethod()
hookMethod.className = project.extensions.blackHook.hookMethodList.get(i).className
hookMethod.methodName = project.extensions.blackHook.hookMethodList.get(i).methodName
hookMethod.descriptor = project.extensions.blackHook.hookMethodList.get(i).descriptor
hookMethod.createBytecode = project.extensions.blackHook.hookMethodList.get(i).createBytecode
blackHook.hookMethodList.add(hookMethod)
}
}
inputs.each { input ->
input.directoryInputs.each { directoryInput ->
handleDirectoryInput(directoryInput, outputProvider)
}
//遍历jarInputs
input.jarInputs.each { JarInput jarInput ->
//处理jarInputs
handleJarInputs(jarInput, outputProvider)
}
}
super.transform(transformInvocation)
}

void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
if (directoryInput.file.isDirectory()) {
directoryInput.file.eachFileRecurse { file ->
String name = file.name
if (name.endsWith(".class") && !name.startsWith("R$drawable")
&& !"R.class".equals(name) && !"BuildConfig.class".equals(name)) {
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor = new AllClassVisitor(classWriter, blackHook)
classReader.accept(classVisitor, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}
}
}

//处理完输入文件之后,要把输出给下一个任务
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}

void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
//重名名输出文件,因为可能同名,会覆盖
def jarName = jarInput.name

def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
JarFile jarFile = new JarFile(jarInput.file)
Enumeration enumeration = jarFile.entries()
File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
//避免上次的缓存被重复插入
if (tmpFile.exists()) {
tmpFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
//用于保存
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(jarEntry)
//插桩class
if (entryName.endsWith(".class") && !entryName.startsWith("R$")
&& !"R.class".equals(entryName) && !"BuildConfig.class".equals(entryName)) {
//class文件处理
jarOutputStream.putNextEntry(zipEntry)
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new AllClassVisitor(classWriter, blackHook)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
jarOutputStream.write(code)
} else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
//结束
jarOutputStream.close()
jarFile.close()
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}

扫描的过程中会将扫描到的所有类的信息(包含类名,父类名,方法名等)交给AllClassVisitor类,AllClassVisitor类代码如下所示:


public class AllClassVisitor extends ClassVisitor {
private String className;
private BlackHook blackHook;
private String superClassName;

public AllClassVisitor(ClassVisitor classVisitor, BlackHook blackHook) {
super(ASM6, classVisitor);
this.blackHook = blackHook;
}

@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
className = name;
superClassName = superName;
}

// 扫描到每个类中的方法的时候会回调到这个方法
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
// 新建AllMethodVisitor类,将扫描到类和方法的信息以及BlackHook类存储的参数交给 AllMethodVisitor对象,由AllMethodVisitor来判断是否需要Hook指定的方法
return new AllMethodVisitor(blackHook, mv, access, name, descriptor, className, superClassName);
}

然后在AllClassVisitor类中会将将扫描到的类和方法的信息以及BlackHook扩展类存储的参数交给AllMethodVisitor对象,由AllMethodVisitor来判断是否需要Hook指定的方法,AllMethodVisitor代码如下:


class AllMethodVisitor extends AdviceAdapter {
private final String methodName;
private final String className;
private BlackHook blackHook;
private String superClassName;

protected AllMethodVisitor(BlackHook blackHook, org.objectweb.asm.MethodVisitor methodVisitor, int access, String name, String descriptor, String className, String superClassName) {
super(ASM5, methodVisitor, access, name, descriptor);
this.blackHook = blackHook;
this.methodName = name;
this.className = className;
this.superClassName = superClassName;
}

@Override
protected void onMethodEnter() {
super.onMethodEnter();
}

@Override
public void visitMethodInsn(int opcode, String owner, String methodName, String descriptor, boolean isInterface) {
super.visitMethodInsn(opcode, owner, methodName, descriptor, isInterface);
if (blackHook.isNeedLog) {
System.out.println("====>methodInfo:" + "className:" + owner + ",methodName:" + methodName + ",descriptor:" + descriptor);
}
if (blackHook != null && blackHook.hookMethodList != null && blackHook.hookMethodList.size() > 0) {
for (int i = 0; i < blackHook.hookMethodList.size(); i++) {
HookMethod hookMethod = blackHook.hookMethodList.get(i);
//这里根据开发人员设置的需要hook的方法以及扫描到的方法来判断是否需要hook
if ((owner.equals(hookMethod.className) || superClassName.equals(hookMethod.className) || className.equals(hookMethod.className)) && methodName.equals(hookMethod.methodName) && descriptor.equals(hookMethod.descriptor)) {
hookMethod.createBytecode.call(mv);
break;
}
}
}
}
}

在这个类中根据开发人员调用插件的时候设置的需要hook的方法以及扫描到的方法来判断是否需要hook


4.源码


github.com/18824863285…


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

扒一扒Android的.9图

前言相信大家对.9图都不陌生,我们在开发当中当有控件的背景需要对内容的大小做自适应的时候,可能就需要用到.9图。如下图所示,就是一张.9图。官方是这么定义的:NinePatchDrawable 图形是一种可拉伸的位图,可用作视图的背景。Android...
继续阅读 »

前言

相信大家对.9图都不陌生,我们在开发当中当有控件的背景需要对内容的大小做自适应的时候,可能就需要用到.9图。如下图所示,就是一张.9图。官方是这么定义的:

NinePatchDrawable 图形是一种可拉伸的位图,可用作视图的背景。Android 会自动调整图形的大小以适应视图的内容。NinePatch 图形是标准 PNG 图片,包含一个额外的 1 像素边框。必须使用 9.png 扩展名将其保存在项目的 res/drawable/ 目录下。

ninepatch_raw.png

那么有人可能会说,这有什么好讲的,从做Andorid开始,我就一直用到现在了。但是,往往越简单的东西,我们越容易忽略它。下面我们就带着这几个问题,一步步来看:

  1. Android是怎么识别一张.9图的?
  2. .9图片一定要放在res/drawable目录下吗,Android是怎么处理它的,为什么在手机上显示出来这个黑色边线却不见了?
  3. 一定要用.9图才能达到自适应的效果吗,普通图片行不行?

PNG

定义

从官方介绍可以得知,.9图是一张标准的PNG图片,只不过是加了一些额外的像素而已,那么首先我们得了解一下什么是PNG。

便携式网络图形(英语:Portable Network Graphics,PNG)是一种支持无损压缩的位图图形格式,支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性。PNG的开发目标是改善并取代GIF作为适合网络传输的格式而不需专利许可,所以被广泛应用于互联网及其他方面上。

文件结构

文件跟协议一样,都是用数据来呈现的。那么既然协议有协议头来标识是什么协议,文件也一样。PNG的文件标识(file signature)是由8个字节组成(89 50 4E 47 0D 0A 1A 0A, 十六进制),系统就是根据这8个自己来识别出PNG文件。

在文件头之后,紧跟着的是数据块。PNG的数据块分为两类,一类是PNG文件必须包含、读写软件也必须要支持的关键块(critical chunk);另一种叫做辅助块(ancillary chunks),PNG允许软件忽略它不认识的附加块。这种基于数据块的设计,允许PNG格式在扩展时仍能保持与旧版本兼容。

数据块的格式:

名称字节数说明
Length4字节指定数据块中数据域的长度,其长度不超过(2^{31}-1)字节
Chunk Type Code(数据块类型码)4字节数据块类型码由ASCII字母(A-Z和a-z)组成
Chunk Data(数据块实际内容)实际内容长度存储按照Chunk Type Code指定的数据
CRC(循环冗余检测)4字节存储用来检测是否有错误的循环冗余码

关键块中有4个标准的数据块:

  • 文件头数据块IHDR(header chunk):包含有图像基本信息,作为第一个数据块出现并只出现一次。
  • 调色板数据块PLTE(palette chunk):必须放在图像数据块之前。
  • 图像数据块IDAT(image data chunk):存储实际图像数据。PNG数据允许包含多个连续的图像数据块。
  • 图像结束数据IEND(image trailer chunk):放在文件尾部,表示PNG数据流结束。

当然关于PNG的信息不止这些,有兴趣了解更多的话,可以去阅读RFC 2083,这里不做过多赘述。

所以不难猜出,.9图是在PNG的辅助块加了自己可以识别的数据块,然后显示的时候对图片做特殊的处理

Android是怎么加载一张.9图的

在Android中,一张图片对应的是一个Bitmap,我们可以看看从怎么从文件读取一张Bitmap入手

//BitmapFactory.java

public static Bitmap decodeFile(String pathName) {
   return decodeFile(pathName, null);
}

private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
           Rect padding, Options opts, long inBitmapHandle, long colorSpaceHandle);

我们根据一个文件路径读取一张图片的话,需要调用BitmapFactorydecodeFile方法,这里我省略了一些过程,但最终都会调用到nativeDecodeStream这个方法,它是一个native方法,接着看C++那边是怎么实现的

//BitmapFactory.cpp

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
       jobject padding, jobject options, jlong inBitmapHandle, jlong colorSpaceHandle) {
...

   if (stream.get()) {
      ...
       bitmap = doDecode(env, std::move(bufferedStream), padding, options, inBitmapHandle,
                         colorSpaceHandle);
  }
   return bitmap;
}

static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
                       jobject padding, jobject options, jlong inBitmapHandle,
                       jlong colorSpaceHandle) {
...
   NinePatchPeeker peeker;
   std::unique_ptr<SkAndroidCodec> codec;
  {
      ...
       std::unique_ptr<SkCodec> c = SkCodec::MakeFromStream(std::move(stream), &result, &peeker);
      ...
  }
 
...
jbyteArray ninePatchChunk = NULL;
   if (peeker.mPatch != NULL) {
       size_t ninePatchArraySize = peeker.mPatch->serializedSize();
       ninePatchChunk = env->NewByteArray(ninePatchArraySize);
       jbyte* array = (jbyte*) env->GetPrimitiveArrayCritical(ninePatchChunk, NULL);
       memcpy(array, peeker.mPatch, peeker.mPatchSize);
       env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0);
  }
 
// now create the java bitmap
   return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
           bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}

doDecode方法很长,这里只提取关键部分。我们看到了关键的NinePatchPeeker,然后把它的指针传给MakeFromStream这个方法。接着copy出NinePatchPeekermPatchBitmap作为构造参数。我们接着往下看:

// SkCodec.cpp
// 刚才NinePatchPeeker传给了这个方法的第三个参数,NinePatchPeeker实际上是实现了SkPngChunkReader
std::unique_ptr<SkCodec> SkCodec::MakeFromStream(
       std::unique_ptr<SkStream> stream, Result* outResult,
       SkPngChunkReader* chunkReader, SelectionPolicy selectionPolicy) {
 
  ...
#ifdef SK_HAS_PNG_LIBRARY
   if (SkPngCodec::IsPng(buffer, bytesRead)) {
       return SkPngCodec::MakeFromStream(std::move(stream), outResult, chunkReader);
  } else
#endif
  ...
}

// SkPngCodec.cpp
std::unique_ptr<SkCodec> SkPngCodec::MakeFromStream(std::unique_ptr<SkStream> stream,
                                                   Result* result, SkPngChunkReader* chunkReader) {
   SkCodec* outCodec = nullptr;
   *result = read_header(stream.get(), chunkReader, &outCodec, nullptr, nullptr);
   if (kSuccess == *result) {
       // Codec has taken ownership of the stream.
       SkASSERT(outCodec);
       stream.release();
  }
   return std::unique_ptr<SkCodec>(outCodec);
}

static SkCodec::Result read_header(SkStream* stream, SkPngChunkReader* chunkReader,
                                  SkCodec** outCodec,
                                  png_structp* png_ptrp, png_infop* info_ptrp) {
...
#ifdef PNG_READ_UNKNOWN_CHUNKS_SUPPORTED
   // Hookup our chunkReader so we can see any user-chunks the caller may be interested in.
   // This needs to be installed before we read the png header. Android may store ninepatch
   // chunks in the header.
   if (chunkReader) {
       png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS, (png_byte*)"", 0);
       png_set_read_user_chunk_fn(png_ptr, (png_voidp) chunkReader, sk_read_user_chunk);
  }
#endif
...
}

这里重点看下png_set_read_user_chunk_fn这个方法,传了chunkReadersk_read_user_chunk方法进去

#ifdef PNG_READ_USER_CHUNKS_SUPPORTED
void PNGAPI
png_set_read_user_chunk_fn(png_structrp png_ptr, png_voidp user_chunk_ptr,
   png_user_chunk_ptr read_user_chunk_fn) {
  ...
  png_ptr->read_user_chunk_fn = read_user_chunk_fn;
  png_ptr->user_chunk_ptr = user_chunk_ptr;
}
#endif

这个方法主要是对png_ptr的两个变量进行赋值,png_ptr是一个PNG结构体的指针。之后read_user_chunk_fn这个方法会在pngrutil.c中被调用

// pngrutil.c
void png_handle_unknown(png_structrp png_ptr, png_inforp info_ptr,
   png_uint_32 length, int keep) {
...
# ifdef PNG_READ_USER_CHUNKS_SUPPORTED
  if (png_ptr->read_user_chunk_fn != NULL) {
     if (png_cache_unknown_chunk(png_ptr, length) != 0) {
        /* Callback to user unknown chunk handler */
        int ret = (*(png_ptr->read_user_chunk_fn))(png_ptr,
            &png_ptr->unknown_chunk);
    }
  }
...
}

这里看方法名就知道是libpng这个库在读取未知的数据块,调用了read_user_chunk_fn方法读取用户自己定义的数据块。而read_user_chunk_fn就是上面的sk_read_user_chunk

// SkPngCodec.cpp
#ifdef PNG_READ_UNKNOWN_CHUNKS_SUPPORTED
static int sk_read_user_chunk(png_structp png_ptr, png_unknown_chunkp chunk) {
   SkPngChunkReader* chunkReader = (SkPngChunkReader*)png_get_user_chunk_ptr(png_ptr);
   // readChunk() returning true means continue decoding
   return chunkReader->readChunk((const char*)chunk->name, chunk->data, chunk->size) ? 1 : -1;
}
#endif

// pngget.c
#ifdef PNG_USER_CHUNKS_SUPPORTED
png_voidp PNGAPI
png_get_user_chunk_ptr(png_const_structrp png_ptr) {
  return (png_ptr ? png_ptr->user_chunk_ptr : NULL);
}
#endif

拿到一个SkPngChunkReader,而它的具体实现上面有说到,就是NinePatchPeeker

// NinePatchPeeker.cpp
bool NinePatchPeeker::readChunk(const char tag[], const void* data, size_t length) {
   if (!strcmp("npTc", tag) && length >= sizeof(Res_png_9patch)) {
       Res_png_9patch* patch = (Res_png_9patch*) data;
       size_t patchSize = patch->serializedSize();
       if (length != patchSize) {
           return false;
      }
       // You have to copy the data because it is owned by the png reader
       Res_png_9patch* patchNew = (Res_png_9patch*) malloc(patchSize);
       memcpy(patchNew, patch, patchSize);
       Res_png_9patch::deserialize(patchNew);
       patchNew->fileToDevice();
       free(mPatch);
       mPatch = patchNew;
       mPatchSize = patchSize;
  } else if (!strcmp("npLb", tag) && length == sizeof(int32_t) * 4) {
       mHasInsets = true;
       memcpy(&mOpticalInsets, data, sizeof(int32_t) * 4);
  } else if (!strcmp("npOl", tag) && length == 24) { // 4 int32_ts, 1 float, 1 int32_t sized byte
       mHasInsets = true;
       memcpy(&mOutlineInsets, data, sizeof(int32_t) * 4);
       mOutlineRadius = ((const float*)data)[4];
       mOutlineAlpha = ((const int32_t*)data)[5] & 0xff;
  }
   return true;
}

找了这么久,我们的目的地终于找到了。可以看到.9图对应的数据块有三个:npTcnpLbnpOl,负责图片图片拉伸的是npTc这个数据块。它在这里用一个Res_png_9patch的结构体封装,我们可以从这个结构体的注释就可以知道很多事情了,懒得看注释的话可以跳过,直接看我下面的解释:

/**
* This chunk specifies how to split an image into segments for
* scaling.
*
* There are J horizontal and K vertical segments. These segments divide
* the image into J*K regions as follows (where J=4 and K=3):
*
*     F0   S0   F1     S1
*   +-----+----+------+-------+
* S2| 0 | 1 | 2   |   3   |
*   +-----+----+------+-------+
*   |     |   |     |       |
*   |     |   |     |       |
* F2| 4 | 5 | 6   |   7   |
*   |     |   |     |       |
*   |     |   |     |       |
*   +-----+----+------+-------+
* S3| 8 | 9 | 10 |   11 |
*   +-----+----+------+-------+
*
* Each horizontal and vertical segment is considered to by either
* stretchable (marked by the Sx labels) or fixed (marked by the Fy
* labels), in the horizontal or vertical axis, respectively. In the
* above example, the first is horizontal segment (F0) is fixed, the
* next is stretchable and then they continue to alternate. Note that
* the segment list for each axis can begin or end with a stretchable
* or fixed segment.
*
* ...
*
* The colors array contains hints for each of the regions. They are
* ordered according left-to-right and top-to-bottom as indicated above.
* For each segment that is a solid color the array entry will contain
* that color value; otherwise it will contain NO_COLOR. Segments that
* are completely transparent will always have the value TRANSPARENT_COLOR.
*
* The PNG chunk type is "npTc".
*/
struct alignas(uintptr_t) Res_png_9patch
{
int8_t wasDeserialized;
   uint8_t numXDivs, numYDivs, numColors;

   uint32_t xDivsOffset, yDivsOffset, colorsOffset;

// .9图右边和下边黑线描述的方位
   int32_t paddingLeft, paddingRight, paddingTop, paddingBottom;

   enum {
       // The 9 patch segment is not a solid color.
       NO_COLOR = 0x00000001,

       // The 9 patch segment is completely transparent.
       TRANSPARENT_COLOR = 0x00000000
  };

...
     
   inline int32_t* getXDivs() const {
       return reinterpret_cast<int32_t*>(reinterpret_cast<uintptr_t>(this) + xDivsOffset);
  }
   inline int32_t* getYDivs() const {
       return reinterpret_cast<int32_t*>(reinterpret_cast<uintptr_t>(this) + yDivsOffset);
  }
   inline uint32_t* getColors() const {
       return reinterpret_cast<uint32_t*>(reinterpret_cast<uintptr_t>(this) + colorsOffset);
  }
}

注释告诉我们几个信息:

  • 一张图片被分为几个区块,支持拉伸的区块坐标分别存储在xDivs和yDivs两个数组。

  • S开头的表示可以拉伸(其实就是做.9图时,旁边1像素的黑线标记的范围),F表示不能拉伸。

    按照注释中的例子,图片被分为12块,例如S0,它表示编号为1、5、9在横轴方向 上是可以拉伸的,S1则表示标号3、7、11是支持拉伸的。所以xDivs和yDivs存储的数据长下面这样:

    xDivs = [S0.start, S0.end, S1.start, S1.end]

    yDivs = [S2.start, S2.end, S3.start, S3.end]

  • colors 描述了各个区块的颜色,按照从左到右从上到下表示。通常情况下,赋值为源码中定义的NO_COLOR = 0x00000001就行了

    colors = [c1, c2, c3, .... c11]

  • 横向(或者纵向)有多个拉伸块的时候,他们的拉伸长度是按照他们标识的范围比例来算的。加入S0是1像素,S1是3像素,则他们拉伸长度按照1:3去拉伸

数据结构

那么,从Res_png_9patch的序列化方法,我们可以推断出这个chunk的数据结构

void Res_png_9patch::serialize(const Res_png_9patch& patch, const int32_t* xDivs,
                              const int32_t* yDivs, const uint32_t* colors, void* outData) {
   uint8_t* data = (uint8_t*) outData;
   memcpy(data, &patch.wasDeserialized, 4);     // copy wasDeserialized, numXDivs, numYDivs, numColors
   memcpy(data + 12, &patch.paddingLeft, 16);   // copy paddingXXXX
   data += 32;

   memcpy(data, xDivs, patch.numXDivs * sizeof(int32_t));
   data +=  patch.numXDivs * sizeof(int32_t);
   memcpy(data, yDivs, patch.numYDivs * sizeof(int32_t));
   data +=  patch.numYDivs * sizeof(int32_t);
   memcpy(data, colors, patch.numColors * sizeof(uint32_t));
}
名称字节长度说明
wasDeserialized1这个值为-1的话表示这个区块不是.9图
numXDivs1xDivs 数组长度
numYDivs1yDivs 数组长度
numColors1colors 数组长度
--4无意义
--4无意义
paddingLeft4横向内容区域的左边
paddingRight4横向内容区域的右边
paddingTop4纵向内容区域的顶部
paddingBottom4纵向内容区域的底部
--无意义
xDivsnumXDivs * 4横向拉伸区域(图片上方黑线)
yDivsnumYDivs * 4纵向拉伸区域(图片左边黑线)
colorsnumColors * 4各个区块颜色

小结

那么,到这里Android把一个.9图加载成Bitmap给理清楚了。先通过读取PNG到header信息,发现有npTc数据块到时候,把它到chunk数据读取出来,用来做Bitmap的构造参数。接下来我们看看绘制

绘制

.9图是用NinePatchDrawable做绘制的,使用方式是这样的:

val bitmap = BitmapFactory.decodeFile(absolutePath)
// 检查bitmap的ninePatchChunk是不是属于.9图的格式,其实就是判断这个chunk的wasDeserialized(第一个字节)是不是等于-1,
val isNinePatch = NinePatch.isNinePatchChunk(bitmap.ninePatchChunk)
if (isNinePatch) {
// 用bitmap以及bitmap.ninePatchChunk构造NinePatchDrawable
val background = NinePatchDrawable(context.resources, bitmap, bitmap.ninePatchChunk, Rect(), null)
imageView.background = background
}

NinePatchDrawable的绘制方法里,又会调用到native方法,由于篇幅原因,这里简单的列下调用栈,大家感兴趣的话可以去看源码:

NinePatchDrawable.java -> draw()
NinePatch.java -> draw()
Canvas.java -> drawPatch()
BaseCanvas.java -> drawPatch()
-> nDrawNinePatch() // 这里是一个native方法,从这里开始就都是native逻辑了

SkiaCanvas.cpp -> drawNinePatch() // Canvas所有的native方法都对应的native层的SkiaCanvas。这里会根据xDivs和yDivs的数据把图片分为N个格子
SkCanvas.cpp -> drawImageLattice()
SkDevice.cpp -> onDrawImageLattice() // 这里循环绘制每个格子
-> drawImageRect()
SkBitmapDevice.cpp -> drawBitmapRect() // 这里给Paint设置了BitmapShader去绘制图片,模式用的是CLAMP(拉伸模式)

到这里,从加载到绘制的过程都已经讲完了,但是还漏了一块,那就是编译。

编译

大家有没有疑问,.9图header里面,npTc这个数据块哪里来?官方介绍为什么叫我们要保存到res/drawable/里面?

其实在编译的时候,aapt会对res/drawable/的图片进行编译,发现是.9图,就把图片四周的黑色像素提取出来,整理成npTc数据块,放到PNG的header里面。

我们可以用Vim打开一张未编译的.9图看看

1.png

这里我们可以看到一些基本的数据块,例如IHDR以及IEND。接着我们用aapt编译一下这张.9图,具体命令如下:

./aapt s -i xxx_in.png -o xxx_out.png

2.png 用Vim打开之后,可以看到多了很多信息,也可以看到.9图对应的npTc数据块,打开图片也可以发现那些四周的黑线不见了。

最后

回答一下文章开头的几个问题:

.9图片一定要放在res/drawable目录下吗

这个不一定,如果你需要从assets目录、sdcard、或者网络读取.9图,也可以实现。只不过需要手动用aapt对图片做处理

一定要用.9图才能达到自适应的效果吗,普通图片行不行

通过了解.9图的原理之后,答案是肯定行的。我们可以自己手动构造ninePatchChunk, 然后传给NinePatchDrawable就可以了,这里就不写代码演示了。


收起阅读 »

在Android中使用Netty进行通讯,附带服务端代码

NettyNetty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。 Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke's Choice Award...
继续阅读 »

Netty

Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。 Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke's Choice Award,见http://www.java.net/dukeschoice… Facebook 和 Instagram 以及流行 开源项目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其强大的对于网络抽象的核心代码。

依赖引入

由于使用最新版本的话,发现有个类找不到,后面查了下是因为jdk版本,在android中的话,太高的jdk版本肯定不支持,所以我找了19年的发行版,测试ok。

implementation 'io.netty:netty-all:4.1.42.Final'

服务端代码实现

NettyServer

@Slf4j
public class NettyServer {

public void start(InetSocketAddress socketAddress) {
//new 一个主线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//new 一个工作线程组
EventLoopGroup workGroup = new NioEventLoopGroup(200);
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ServerChannelInitializer())
.localAddress(socketAddress)
//设置队列大小
.option(ChannelOption.SO_BACKLOG, 1024)
// 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
.childOption(ChannelOption.SO_KEEPALIVE, true);
//绑定端口,开始接收进来的连接
try {
ChannelFuture future = bootstrap.bind(socketAddress).sync();
log.info("服务器启动开始监听端口: {}", socketAddress.getPort());
// future.channel().writeAndFlush("你好啊");
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//关闭主线程组
bossGroup.shutdownGracefully();
//关闭工作线程组
workGroup.shutdownGracefully();
}
}
}

ServerChannelInitializer

public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//添加编解码
socketChannel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast(new NettyServerHandler());
}
}

NettyServerHandler

@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 客户端连接会触发
*/

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("Channel active......");
}

@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("Channel Inactive......");
}

/**
* 客户端发消息会触发
*/

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//----------- 只改了这里 -----------
log.info("服务器收到消息1111: {}", msg.toString());

ctx.write("{\"data\":{\"taskData\":{\"collectionRule\":{\"id\":1,\"name\":\"IP主播直播室互动用户\",\"rule\":\"[{\\\"label\\\":\\\"抖音号\\\",\\\"key\\\":\\\"dyId\\\",\\\"type\\\":\\\"string\\\"},{\\\"key\\\":\\\"count\\\",\\\"label\\\":\\\"数量\\\",\\\"type\\\":\\\"string\\\"}]\",\"ruleType\":\"collect\",\"source\":\"collectLiveAudience\"},\"description\":\"粉丝列表-付鹏的财经世界3\",\"ruleId\":\"1\",\"ruleParam\":\"{\\\"dyId\\\":\\\"ghsys\\\",\\\"count\\\":\\\"140000\\\"}\"},\"taskId\":\"64\",\"taskType\":\"collection\"},\"devicesId\":\"5011bbdcd5006a93\",\"type\":\"task\"}");
ctx.flush();
}

/**
* 发生异常触发
*/

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

启动服务

@SpringBootApplication
public class ServerApplication {

public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
//启动服务端
NettyServer nettyServer = new NettyServer();
nettyServer.start(new InetSocketAddress("192.18.52.95", 8091));
}
}

客户端代码实现

其实netty的使用客户端和服务器端整体上是差不多的,所以这里只列出来核心代码。

初始化操作

abstract class McnNettyTask : Runnable {

private var socketChannel: SocketChannel? = null
private var isConnected = false

override fun run() {
createConnection()
}

private fun createConnection() {
val nioEventLoopGroup = NioEventLoopGroup()
val bootstrap = Bootstrap()
bootstrap
.group(nioEventLoopGroup)
.option(ChannelOption.TCP_NODELAY, true) //无阻塞
.channel(NioSocketChannel::class.java)
.option(ChannelOption.SO_KEEPALIVE, true) //长连接
.option(ChannelOption.SO_TIMEOUT, 30_000) //收发超时
.handler(McnClientInitializer(object : McnClientListener {
override fun disConnected() {
isConnected = false
}

override fun connected() {
isConnected = true
}
}, object : McnEventListener {
override fun onReceiverMessage(messageRequest: MessageRequest) {
dispatchMessage(messageRequest)
}
}))
try {
val channelFuture = bootstrap.connect(McnNettyConfig.ip, McnNettyConfig.port)
.addListener(object : ChannelFutureListener {
override fun operationComplete(future: ChannelFuture) {
if (future.isSuccess) {
socketChannel = future.channel() as SocketChannel;
isConnected = true
CommonConsole.log("netty connect success (ip: ${McnNettyConfig.ip}, port: ${McnNettyConfig.port})")

sendMsg(MessageRequest.createDevicesState(0))
} else {
CommonConsole.log("netty connect failure (ip: ${McnNettyConfig.ip}, port: ${McnNettyConfig.port})")
isConnected = false
future.channel().close()
nioEventLoopGroup.shutdownGracefully()
}
}
}).sync()//阻塞,直到连接完成
channelFuture.channel().closeFuture().sync()
} catch (ex: Exception) {
ex.printStackTrace()
} finally {
//释放所有资源和创建的线程
nioEventLoopGroup.shutdownGracefully()
}
}

fun isConnected(): Boolean {
return isConnected
}

fun disConnected() {
socketChannel?.close()
}

abstract fun dispatchMessage(messageRequest: MessageRequest)

fun sendMsg(msg: String, nettyMessageListener: McnMessageListener? = null) {
if (!isConnected()) {
nettyMessageListener?.sendFailure()
return
}
socketChannel?.run {
writeAndFlush(msg + "###").addListener { future ->
if (future.isSuccess) {
//消息发送成功
CommonConsole.log("netty send message success (message: $msg")
nettyMessageListener?.sendSuccess()
} else {
//消息发送失败
CommonConsole.log("netty send message failure (message: $msg")
nettyMessageListener?.sendFailure()
}
}
}
}
}

加载handler和Initializer

class McnClientInitializer(
private val nettyClientListener: McnClientListener,
private val nettyEventListener: McnEventListener
) :
ChannelInitializer<SocketChannel>() {

override fun initChannel(socketChannel: SocketChannel) {
val pipeline = socketChannel.pipeline()
// pipeline.addLast("decoder", McnStringDecoder())
// pipeline.addLast("encoder", McnStringEncoder())
// pipeline.addLast(LineBasedFrameDecoder(1024))
pipeline.addLast("decoder", StringDecoder())
// pipeline.addLast("encoder", StringEncoder())
pipeline.addLast(DelimiterBasedFrameEncoder("###"))
pipeline.addLast(McnClientHandler(nettyClientListener, nettyEventListener))
}
}

核心数据接收处理handler

class McnClientHandler(
private val nettyClientListener: McnClientListener,
private val nettyEventListener: McnEventListener
) :
SimpleChannelInboundHandler<String>() {

override fun channelActive(ctx: ChannelHandlerContext?) {
super.channelActive(ctx)
CommonConsole.log("Netty channelActive.........")
nettyClientListener.connected()
}

override fun channelInactive(ctx: ChannelHandlerContext?) {
super.channelInactive(ctx)
nettyClientListener.disConnected()
}

override fun channelReadComplete(ctx: ChannelHandlerContext?) {
super.channelReadComplete(ctx)
CommonConsole.log("Netty channelReadComplete.........")
}

override fun exceptionCaught(ctx: ChannelHandlerContext?, cause: Throwable?) {
super.exceptionCaught(ctx, cause)
CommonConsole.log("Netty exceptionCaught.........${cause?.message}")
cause?.printStackTrace()
ctx?.close()
}

override fun channelRead0(ctx: ChannelHandlerContext?, msg: String?) {
CommonConsole.log("Netty channelRead.........${msg}")
msg?.run {
try {
val messageRequest =
Gson().fromJson<MessageRequest>(msg, MessageRequest::class.java)
nettyEventListener.onReceiverMessage(messageRequest)
} catch (ex: Exception) {
ex.printStackTrace()
}
ReferenceCountUtil.release(msg)
}
}
}

处理数据粘包 & 数据分包

如果使用netty,你肯定会碰到数据粘包和数据分包的问题的。所谓数据粘包就是当数据量比较小的情况下,相近时间内的多个发送数据会被作为一个数据包接收解析。而数据分包就是会将一个比较大的数据包分成为很多个小的数据包。不管是数据的分包还是粘包,都会导致我们使用的时候不能简单使用数据,所以我们要对粘包和分包数据做处理,让每次发送的数据都是独立且完整的。

对于数据编码,使用自定义的解析器处理,相当于是对数据使用特定字符串做拼接和反取操作。

服务端:

ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,
Unpooled.wrappedBuffer(delimiter.getBytes())));
// 将分隔之后的字节数据转换为字符串数据
ch.pipeline().addLast(new StringDecoder());
// 这是我们自定义的一个编码器,主要作用是在返回的响应数据最后添加分隔符
ch.pipeline().addLast(new DelimiterBasedFrameEncoder("###"));
// 最终处理数据并且返回响应的handler
ch.pipeline().addLast(new EchoServerHandler());

客户端:

/ 对服务端返回的消息通过_$进行分隔,并且每次查找的最大大小为1024字节
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,
Unpooled.wrappedBuffer(delimiter.getBytes())));
// 将分隔之后的字节数据转换为字符串
ch.pipeline().addLast(new StringDecoder());
// 对客户端发送的数据进行编码,这里主要是在客户端发送的数据最后添加分隔符
ch.pipeline().addLast(new DelimiterBasedFrameEncoder("###"));
// 客户端发送数据给服务端,并且处理从服务端响应的数据
ch.pipeline().addLast(new EchoClientHandler());

DelimiterBasedFrameEncoder

public class DelimiterBasedFrameEncoder extends MessageToByteEncoder<String> {

private String delimiter;

public DelimiterBasedFrameEncoder(String delimiter) {
this.delimiter = delimiter;
}

@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out)
throws Exception {
// 在响应的数据后面添加分隔符
ctx.writeAndFlush(Unpooled.wrappedBuffer((msg + delimiter).getBytes()));
}
}

如果是

对客户端 & 服务端通讯加入[暗号]

在客户端和服务端数据通讯的时候,为了确保数据的完整性和安全性,通常会加入一段暗号,作为安全校验。其实两端真实的通信结构就变成了如下:

完整数据 = 暗号字节 + 真实数据内容

客户端和服务端在获取到数据之后,按照约定将暗号数据移除之后,剩下的就是正式的数据。

对于暗号的处理,可以通过MessageToMessageEncoder来实现,通过对获取到的通讯字节码编解码来对数据处理。

McnStringEncoder

/**
* copy自 StringEncoder源码,进行修改,增加了业务处理暗号
*/

class McnStringEncoder : MessageToMessageEncoder<CharSequence> {

var charset: Charset? = null

constructor(charset: Charset?) {
if (charset == null) {
throw NullPointerException("charset")
} else {
this.charset = charset
}
}

constructor() : this(Charset.defaultCharset())

override fun encode(ctx: ChannelHandlerContext?, msg: CharSequence?, out: MutableList<Any>?) {
if (msg?.isNotEmpty() == true) {
out?.add(
ByteBufUtil.encodeString(
ctx!!.alloc(),
CharBuffer.wrap(McnNettyConfig.private_key + msg),
charset
)
)
}
}
}

McnStringDecoder

/**
* copy自 StringDecoder源码,进行修改,增加了业务处理暗号
*/

class McnStringDecoder : MessageToMessageDecoder<ByteBuf> {
var charset: Charset? = null

constructor(charset: Charset?) {
if (charset == null) {
throw NullPointerException("charset")
} else {
this.charset = charset
}
}

constructor() : this(Charset.defaultCharset())

override fun decode(ctx: ChannelHandlerContext?, msg: ByteBuf?, out: MutableList<Any>?) {
msg?.run {
Log.e("info", "decoder结果====>${msg.toString(charset)}")
//校验报文长度是否合法
if (msg.readableBytes() <= McnNettyConfig.keyLength) {
out?.add(ErrorData.creator(ErrorData.LENGTH_ERROR, "报文长度校验失败"))
return
}
val privateKey = this.readBytes(McnNettyConfig.keyLength)
//校验报文暗号是否匹配
if (privateKey.toString(charset) != McnNettyConfig.private_key) {
out?.add(ErrorData.creator(ErrorData.PRIVATE_KEY_ERROR, "报文暗号校验失败"))
return
}
//获取真实报文内容
out?.add(this.toString(charset))
}
}

data class ErrorData(
var errorCode: Int,
var errorMsg: String
) {

companion object {

//长度异常
const val LENGTH_ERROR = -10001

//报文校验失败
const val PRIVATE_KEY_ERROR = -10002

@JvmStatic
fun creator(errorCode: Int, message: String): String {
val errorData = ErrorData(errorCode, message)
return JSON.toJSONString(errorData)
}
}
}
}

最后的使用就很简单了。只需要将我们的处理加到处理链就行了。

val pipeline = socketChannel.pipeline()
pipeline.addLast("decoder", McnStringDecoder())
pipeline.addLast("encoder", McnStringEncoder())

收起阅读 »

再谈协程之第三者Flow基础档案

该来的还是来了,LiveData提供了响应式编程的基础,搭建了一套数据观察者的使用框架,但是,它相当于RxJava这类的异步框架来说,有点略显单薄了,这也是经常被人诟病的问题,因此,Flow这个小三就顺应而生了。Flow作为一套异步数据流框架,几乎可以约等于R...
继续阅读 »

该来的还是来了,LiveData提供了响应式编程的基础,搭建了一套数据观察者的使用框架,但是,它相当于RxJava这类的异步框架来说,有点略显单薄了,这也是经常被人诟病的问题,因此,Flow这个小三就顺应而生了。

Flow作为一套异步数据流框架,几乎可以约等于RxJava,但借助Kotlin语法糖和协程,以及Kotlin的DSL语法,可以让Flow的写法变得异常简洁,让你直面人性最善良的地方,一切的黑暗和丑陋,都被编译器消化了。而且,Flow作为LiveData的进化版本,可以很好的和JetPack结合起来,作为全家桶的一员,为统一架构添砖加瓦。

要理解FLow,首先需要了解Flow的各种操作符和基础功能,如果不理解这些,那么很难将Flow灵活运用,所以,本节主要来梳理Flow的基础。

Flow前言

首先,我们来看一个新的概念——冷流和热流,如果你看网上的Flow相关的文章,十有八九都会提到这个很冷门的名词。

Flow是早上冷的,到Channel才热起来。

一个异步数据流,通常包含三部分:

  • 上游
  • 操作符
  • 下游

所谓冷流,即下游无消费行为时,上游不会产生数据,只有下游开始消费,上游才从开始产生数据。

而所谓热流,即无论下游是否有消费行为,上游都会自己产生数据。

Flow操作符

Flow和RxJava一样,用各种操作符撑起了异步数据流框架的半边天。Flow默认为冷流,即下游有消费时,才执行生产操作。

所以,操作符也被分为两类——中间操作符和末端操作符,中间操作符不会产生消费行为,返回依然为Flow,而末端操作符,会产生消费行为,即触发流的生产。

Flow的创建

仅仅创建Flow,是不会执行Flow中的任何代码的,但我们首先,还是要看下如何创建Flow。

  • flow

通过flow{}构造器,可以快速创建Flow,在flow中,可以使用emit来生产数据(或者emitAll生产批量数据),示例如下。

flow {
for (i in 0..3) {
emit(i.toString())
}
}
  • flowOf

与listOf类似,Flow可以通过flowOf来产生有限的已知数据。

flowOf(1, 2, 3)
  • asFlow

asFlow用于将List转换为Flow。

listOf(1,2,3).asFlow()
  • emptyFlow

如题,创建一个空流。

末端操作符

末端操作符在调用之后,创建Flow的代码才会执行,这点和Sequence非常类似。

  • collect

collect是最常用的末端操作符,示例如下。

末端操作符都是suspend函数,所以需要运行在协程作用域中。

MainScope().launch {
val time = measureTimeMillis {
flow {
for (i in 0..3) {
Log.d("xys", "emit value---$i")
emit(i.toString())
}
}.collect {
Log.d("xys", "Result---$it")
}
}
Log.d("xys", "Time---$time")
}
  • collectIndexed

带下标的collect,下标是Flow中的emit顺序。

MainScope().launch {
val time = measureTimeMillis {
flow {
for (i in 0..3) {
Log.d("xys", "emit value---$i")
emit(i.toString())
}
}.collectIndexed { index, value ->
Log.d("xys", "Result in $index --- $value")
}
}
Log.d("xys", "Time---$time")
}
  • collectLatest

collectLatest用于在collect中取消未来得及处理的数据,只保留当前最新的生产数据。

flowOf(1, 2, 3).collectLatest {
delay(1)
Log.d("xys", "Result---$it")
}
  • toCollection、toSet、toList

这些操作符用于将Flow转换为Collection、Set和List。

  • launchIn

在指定的协程作用域中直接执行Flow。

flow {
for (i in 0..3) {
Log.d("xys", "emit value---$i")
emit(i.toString())
}
}.launchIn(MainScope())
  • last、lastOrNull、first、firstOrNull

返回Flow的最后一个值(第一个值),区别是last为空的话,last会抛出异常,而lastOrNull可空。

flow {
for (i in 0..3) {
emit(i.toString())
}
}.last()

状态操作符

状态操作符不做任何修改,只是在合适的节点返回状态。

  • onStart:在上游生产数据前调用
  • onCompletion:在流完成或者取消时调用
  • onEach:在上游每次emit前调用
  • onEmpty:流中未产生任何数据时调用
  • catch:对上游中的异常进行捕获
  • retry、retryWhen:在发生异常时进行重试,retryWhen中可以拿到异常和当前重试的次数
MainScope().launch {
Log.d("xys", "Coroutine in ${Thread.currentThread().name}")
val time = measureTimeMillis {
flow {
for (i in 0..3) {
emit(i.toString())
}
throw Exception("Test")
}.retryWhen { _, retryCount ->
retryCount <= 3
}.onStart {
Log.d("xys", "Start Flow in ${Thread.currentThread().name}")
}.onEach {
Log.d("xys", "emit value---$it")
}.onCompletion {
Log.d("xys", "Flow Complete")
}.catch { error ->
Log.d("xys", "Flow Error $error")
}.collect {
Log.d("xys", "Result---$it")
}
}
Log.d("xys", "Time---$time")
}

另外,onCompletion也可以监听异常,代码如下所示。

.onCompletion { exception ->
Log.d("xys", "Result---$exception")
}

Transform操作符

与RxJava一样,在数据流中,我们可以利用操作符对数据进行各种变换,以满足操作流的不同需求。

  • map、mapLatest、mapNotNull

map操作符将Flow的输入通过block转换为新的输出。

flow {
for (i in 0..3) {
emit(i)
}
}.map {
it * it
}
  • transform、transformLatest

transform操作符与map操作符有点一样,但又不完全一样,map是一对一的变换,而transform则可以完全控制流的数据,进行过滤、 重组等等操作都可以。

flow {
for (i in 0..3) {
emit(i)
}
}.transform { value ->
if (value == 1) {
emit("!!!$value!!!")
}
}.collect {
Log.d("xys", "Result---$it")
}
  • transformWhile

transformWhile的返回值是一个bool类型,用来控制流的截断,如果返回true,则流继续执行,如果false,则流截断。

flow {
for (i in 0..3) {
emit(i)
}
}.transformWhile { value ->
emit(value)
value == 1
}.collect {
Log.d("xys", "Result---$it")
}

过滤操作符

如题,过滤操作符用于过滤流中的数据。

  • filter、filterInstance、filterNot、filterNotNull

过滤操作符可以按条件、类型或者对过滤取反、取非空等条件进行操作。

flow {
for (i in 0..3) {
emit(i)
}
}.filter { value ->
value == 1
}.collect {
Log.d("xys", "Result---$it")
}
  • drop、dropWhile、take、takeWhile

这类操作符可以丢弃前n个数据,或者是只拿前n个数据。带while后缀的,则表示按条件进行判断。

  • debounce

debounce操作符用于防抖,指定时间内的值只接收最新的一个。

  • sample

sample操作符与debounce操作符有点像,但是却限制了一个周期性时间,sample操作符获取的是一个周期内的最新的数据,可以理解为debounce操作符增加了周期的限制。

  • distinctUntilChangedBy

去重操作符,可以按照指定类型的参数进行去重。

组合操作符

组合操作符用于将多个Flow的数据进行组合。

  • combine、combineTransform

combine操作符可以连接两个不同的Flow。

val flow1 = flowOf(1, 2).onEach { delay(10) }
val flow2 = flowOf("a", "b", "c").onEach { delay(20) }
flow1.combine(flow2) { i, s -> i.toString() + s }.collect {
Log.d("xys", "Flow combine: $it")
}

输出为:

D/xys: Flow combine: 1a
D/xys: Flow combine: 2a
D/xys: Flow combine: 2b
D/xys: Flow combine: 2c

可以发现,当两个Flow数量不同时,始终由Flow1开始,用其最新的元素,与Flow2的最新的元素进行组合,形成新的元素。

  • merge

merge操作符用于将多个流合并。

val flow1 = flowOf(1, 2).onEach { delay(10) }
val flow2 = flowOf("a", "b", "c").onEach { delay(20) }
listOf(flow1, flow2).merge().collect {
Log.d("xys", "Flow merge: $it")
}

输出为:

D/xys: Flow merge: 1
D/xys: Flow merge: 2
D/xys: Flow merge: a
D/xys: Flow merge: b
D/xys: Flow merge: c

merge的输出结果是按照时间顺序,将多个流依次发射出来。

  • zip

zip操作符会分别从两个流中取值,当一个流中的数据取完,zip过程就完成了。

val flow1 = flowOf(1, 2).onEach { delay(10) }
val flow2 = flowOf("a", "b", "c").onEach { delay(20) }
flow1.zip(flow2) { i, s -> i.toString() + s }.collect {
Log.d("xys", "Flow zip: $it")
}

输出为:

D/xys: Flow zip: 1a
D/xys: Flow zip: 2b

线程切换

在Flow中,可以简单的使用flowOn来指定线程的切换,flowOn会对上游,以及flowOn之前的所有操作符生效。

flow {
for (i in 0..3) {
Log.d("xys", "Emit Flow in ${Thread.currentThread().name}")
emit(i)
}
}.map {
Log.d("xys", "Map Flow in ${Thread.currentThread().name}")
it * it
}.flowOn(Dispatchers.IO).collect {
Log.d("xys", "Collect Flow in ${Thread.currentThread().name}")
Log.d("xys", "Result---$it")
}

这种情况下,flow和map的操作都将在子线程中执行。

而如果是这样:

flow {
for (i in 0..3) {
Log.d("xys", "Emit Flow in ${Thread.currentThread().name}")
emit(i)
}
}.flowOn(Dispatchers.IO).map {
Log.d("xys", "Map Flow in ${Thread.currentThread().name}")
it * it
}.collect {
Log.d("xys", "Collect Flow in ${Thread.currentThread().name}")
Log.d("xys", "Result---$it")
}

这样map就会执行在主线程了。

同时,你也可以多次调用flowOn来不断的切换线程,让前面的操作符执行在不同的线程中。

取消Flow

Flow也是可以被取消的,最常用的方式就是通过withTimeoutOrNull来取消,代码如下所示。

MainScope().launch {
withTimeoutOrNull(2500) {
flow {
for (i in 1..5) {
delay(1000)
emit(i)
}
}.collect {
Log.d("xys", "Flow: $it")
}
}
}

这样当输出1、2之后,Flow就被取消了。

Flow的取消,实际上就是依赖于协程的取消。

Flow的同步非阻塞模型

首先,我们要理解下,什么叫同步非阻塞,默认场景下,Flow在没有切换线程的时候,运行在协程作用域指定的线程,这就是同步,那么非阻塞又是什么呢?我们知道emit和collect都是suspend函数,所谓suspend函数,就是会挂起,将CPU资源让出去,这就是非阻塞,因为suspend了就可以让一让,让给谁呢?让给其它需要执行的函数,执行完毕后,再把资源还给我。

所以,我们来看下面这个例子。

flow {
for (i in 0..3) {
emit(i)
}
}.onStart {
Log.d("xys", "Start Flow in ${Thread.currentThread().name}")
}.onEach {
Log.d("xys", "emit value---$it")
}.collect {
Log.d("xys", "Result---$it")
}

输出为:

D/xys: Start Flow in main
D/xys: emit value---0
D/xys: Result---0
D/xys: emit value---1
D/xys: Result---1
D/xys: emit value---2
D/xys: Result---2
D/xys: emit value---3
D/xys: Result---3

可以发现,emit一个,collect拿一个,这就是同步非阻塞,互相谦让,这样谁都可以执行,看上去flow中的代码和collect中的代码,就是同步执行的。

异步非阻塞模型

假如我们给Flow增加一个线程切换,让Flow执行在子线程,同样是上面的代码,我们再来看下执行情况。

flow {
for (i in 0..3) {
emit(i)
}
}.onStart {
Log.d("xys", "Start Flow in ${Thread.currentThread().name}")
}.onEach {
Log.d("xys", "emit value---$it")
}.flowOn(Dispatchers.IO).collect {
Log.d("xys", "Collect Flow in ${Thread.currentThread().name}")
Log.d("xys", "Result---$it")
}

输出为:

D/xys: Start Flow in DefaultDispatcher-worker-1
D/xys: emit value---0
D/xys: emit value---1
D/xys: emit value---2
D/xys: emit value---3
D/xys: Collect Flow in main
D/xys: Result---0
D/xys: Collect Flow in main
D/xys: Result---1
D/xys: Collect Flow in main
D/xys: Result---2
D/xys: Collect Flow in main
D/xys: Result---3

这个时候,Flow就变成了异步非阻塞模型,异步呢,就更好理解了,因为在不同线程,而此时的非阻塞,就没什么意义了,由于flow代码先执行,而这里的代码由于没有delay,所以是同步执行的,执行的同时,collect在主线程进行监听。

除了使用flowOn来切换线程,使用channelFlow也可以实现异步非阻塞模型。


收起阅读 »

Hilt 扩展 | MAD Skills

案例: WorkManager 扩展Hilt 扩展是一个生成代码的库,常通过注解处理器实现。生成的代码作为构成 Hilt 依赖项注入关系图的模块或入口点。Jetpack 中 WorkManager 的集成库就是一个扩展的例子。WorkManager ...
继续阅读 »

案例: WorkManager 扩展

Hilt 扩展是一个生成代码的库,常通过注解处理器实现。生成的代码作为构成 Hilt 依赖项注入关系图的模块或入口点。

Jetpack 中 WorkManager 的集成库就是一个扩展的例子。WorkManager 扩展帮助我们减少向 worker 提供依赖项时所需的模板代码及配置。该库由两部分组成,分别为 androidx.hilt:hilt-work 和 androidx.hilt:hilt-compiler。第一部分包含 HiltWorker 注解以及一些运行时的辅助类,第二部分是一个注解处理器,根据第一部分中注解提供的信息生成模块。

扩展的使用非常简单,仅需在您的 worker 上添加 @HiltWorker 注解:

@HiltWorker
public class ExampleWorker extends Worker {
// ...
}

扩展编译器会生成一个添加了 @Module 注解的类:

@Generated("androidx.hilt.AndroidXHiltProcessor")
@Module
@InstallIn(SingletonComponent.class)
@OriginatingElement(
topLevelClass = ExampleWorker.class
)
public interface ExampleWorker_HiltModule {
@Binds
@IntoMap
@StringKey("my.app.ExmapleWorker")
WorkerAssistedFactory<? extends ListenableWorker> bind(
ExampleWorker_AssistedFactory factory);
}

该模块为 worker 定义了一个可以访问 HiltWorkerFactory 的绑定。然后,配置 WorkerManager 使用该 factory,从而使 worker 的依赖项注入可用。

Hilt 聚合

启用扩展的一个关键机制是 Hilt 能够从类路径中发现模块和入口点。这被称为聚合,因为模块和入口点被聚合到带有 @HiltAndroidApp 注解的 Application 中。

由于 Hilt 具有聚合能力,任何通过添加 @InstallIn 注解生成 @Module 及 @EntryPoint 的工具都可以被 Hilt 发现,并在编译期成为 Hilt DI 图中的一部分。这使得扩展可以轻松地以插件形式集成到 Hilt,无需开发者处理任何额外工作。

注解处理器

生成代码的常规途径是使用注解处理器。源文件转换为 class 文件之前,注解处理器会在编译器中运行。当资源带有处理器所声明的已支持的注解时,处理器会进行处理。处理器可以生成进一步需要被处理的方法,因此编译器会不断循环运行注解处理器,直到没有新的内容产生。一旦所有的环节都完成,编译器才会将源文件转换为 class 文件。

△ 注解处理示意图

△ 注解处理示意图

由于循环机制,处理器可以相互作用。这非常重要,因为这使得 Hilt 的注解处理器可以处理由其他处理器生成的 @Module 或 @EntryPoint 类。这也意味着您的扩展也可以建立在其他人编写的扩展之上!

WorkManager extension processor 根据带有 @HiltWorker 注解的类生成代码,同时验证注解用法并使用 JavaPoet 等库生成代码。

Hilt 扩展注解

Hilt API 中有两个重要的注解: @GeneratesRootInput 和 @OriginatingElement。扩展应该使用这些注解才能与 Hilt 正确集成。

扩展应该使用 @GeneratesRootInput 来启用代码生成的注解。这让 Hilt 注解处理器知道它应该在生成组件之前完成扩展注解处理器的工作。例如,@HiltWorker 注解本身是被 @GeneratesRootInput 注解修饰的:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@GeneratesRootInput
public @interface HiltWorker {
}

所生成的带有 @Module、@EntryPoint 以及 @InstallIn 注解的类都需要添加 @OriginatingElement 注解,该注解的输入参数是触发模块或入口点生成的顶层类。这就是 Hilt 判断生成的模块和入口点是否在本地测试的依据。例如,在 Hilt 测试中定义了一个添加 @HiltWorker 注解的内部类,模块的初始元素就是测试值。

测试案例如下:

@HiltAndroidTest
class SampleTest {
@HiltWorker
class TestWorker extends Worker {
// …
}
}

生成的模块包含 @OriginatingElement 注解:

@Module
@InstallIn(SingletonComponent.class)
@OriginatingElement(
topLevelClass = SampleTest.class
)
public interface SampleTest_TestWorker__HiltModule {
// …
}

心得

Hilt 扩展支持多种可能性,以下是创建扩展的一些心得:

项目中的通用模式

如果您的项目中有创建模块或入口点的通用模式,那么它们很大概率可以通过使用 Hilt 扩展实现自动化。举个例子,如果每一个实现特定接口的类都必须创建一个具有多绑定的模块,那么可以创建一个扩展,只需在实现类上添加注解即可生成多重绑定模块。

支持非标准成员注入

对于那些 Framework 中已经支持带有实例化能力的成员注入类型,我们需要创建一个 @EntryPoint。如果有多种类型需要被成员注入,那么自动创建入口点的扩展会很有用。例如,需要通过 ServiceLoader 发现服务实现的库负责实例化发现的服务。为了将依赖项注入到服务实现中,必须创建一个 @EntryPoint。通过使用 Hilt 扩展,可以使用在实现类上添加注解完成自动生成入口点。扩展可以进一步生成代码以使用入口点,例如由服务实现扩展的基类。这类似于 @AndroidEntryPoint 为 Activity 创建 @EntryPoint,并创建使用生成的入口点在 Activity 中执行成员注入的基类。

镜像绑定

有时需要使用不同的限定符来镜像或重新声明绑定。当存在自定义组件时,这可能更常见。为了避免丢失重新声明的绑定,可以创建 Hilt 扩展以自动生成其他镜像绑定的模块。例如,考虑包含不同依赖项实现的应用中 "付费" 和 "免费" 订阅的情况。然后,每一层都有两个不同的自定义组件,这样您就可以确定依赖关系的作用域。当添加一个通用的未限定作用域的绑定时,定义绑定的模块可以在其 @InstallIn 中包含两个组件,也可以加载在父组件中,通常是单例组件。但是当绑定被限定作用域时,模块必须被复制,因为需要不同的限定符。实现一个扩展就可以生成两个模块,可以避免样板代码并确保不会遗漏通用绑定。

总结

Hilt 的扩展可以进一步增强代码库中的依赖项注入能力,因为它们可以实现与 Hilt 尚不支持的其他库集成。总而言之,扩展通常由两部分组成,包含扩展注解的运行时部分,以及生成 @Module 或 @EntryPoint 的代码生成器 (通常是注解处理器)。扩展的运行时部分可能有额外的辅助类,这些辅助类使用声明在生成的模块或入口点中绑定。代码生成器还可能生成与扩展相关的附加代码,它们无需专门生成模块和入口点。

扩展必须使用两个注解才能与 Hilt 正确交互:

  • @GeneratesRootInput 添加在扩展注解上。
  • @OriginatingElement 由扩展添加在生成的模块或入口点上。

最后,您可以查看 hilt-install-binding 项目,这是一个简单扩展的示例,它展示了本文中提到的概念。

以上便是 MAD Skills 系列关于 Hilt 的全部内容,如需观看视频全集,请移步到 Hilt - MAD Skills 播放列表。感谢阅读本文!

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!

收起阅读 »

面试官:Java从编译到执行,发生了什么?

面试官:今天从基础先问起吧,你是怎么理解Java是一门「跨平台」的语言,也就是「一次编译,到处运行的」?候选者:很好理解啊,因为我们有JVM。候选者:Java源代码会被编译为class文件,class文件是运行在JVM之上的。候选者:当我们日常开发安装JDK的...
继续阅读 »

面试官:今天从基础先问起吧,你是怎么理解Java是一门「跨平台」的语言,也就是「一次编译,到处运行的」?

候选者:很好理解啊,因为我们有JVM。

候选者:Java源代码会被编译为class文件,class文件是运行在JVM之上的。

候选者:当我们日常开发安装JDK的时候,可以发现JDK是分「不同的操作系统」,JDK里是包含JVM的,所以Java依赖着JVM实现了『跨平台』

候选者:JVM是面向操作系统的,它负责把Class字节码解释成系统所能识别的指令并执行,同时也负责程序运行时内存的管理。

面试官那要不你来聊聊从源码文件(.java)到代码执行的过程呗?

候选者:嗯,没问题的

候选者:简单总结的话,我认为就4个步骤:编译->加载->解释->执行

候选者:编译:将源码文件编译成JVM可以解释的class文件。

候选者:编译过程会对源代码程序做 「语法分析」「语义分析」「注解处理」等等处理,最后才生成字节码文件。

候选者:比如对泛型的擦除和我们经常用的Lombok就是在编译阶段干的。

候选者:加载:将编译后的class文件加载到JVM中。

候选者:在加载阶段又可以细化几个步骤:装载->连接->初始化

候选者:下面我对这些步骤又细说下哈。

候选者:【装载时机】为了节省内存的开销,并不会一次性把所有的类都装载至JVM,而是等到「有需要」的时候才进行装载(比如new和反射等等)

候选者:【装载发生】class文件是通过「类加载器」装载到jvm中的,为了防止内存中出现多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上)

候选者:【装载规则】JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现装载,而程序中的类文件则由系统加载器(AppClassLoader )实现装载。

候选者:装载这个阶段它做的事情可以总结为:查找并加载类的二进制数据,在JVM「堆」中创建一个java.lang.Class类的对象,并将类相关的信息存储在JVM「方法区」中

面试官:嗯…

候选者:通过「装载」这个步骤后,现在已经把class文件装载到JVM中了,并创建出对应的Class对象以及类信息存储至方法区了。

候选者:「连接」这个阶段它做的事情可以总结为:对class的信息进行验证、为「类变量」分配内存空间并对其赋默认值。

候选者:连接又可以细化为几个步骤:验证->准备->解析

候选者:1. 验证:验证类是否符合 Java 规范和 JVM 规范

候选者:2. 准备:为类的静态变量分配内存,初始化为系统的初始值

候选者:3. 解析:将符号引用转为直接引用的过程

面试官:嗯…

候选者:通过「连接」这个步骤后,现在已经对class信息做校验并分配了内存空间和默认值了。

候选者:接下来就是「初始化」阶段了,这个阶段可以总结为:为类的静态变量赋予正确的初始值。

候选者:过程大概就是收集class的静态变量、静态代码块、静态方法至()方法,随后从上往下开始执行。

候选者:如果「实例化对象」则会调用方法对实例变量进行初始化,并执行对应的构造方法内的代码。

候选者:扯了这么多,现在其实才完成至(编译->加载->解释->执行)中的加载阶段,下面就来说下【解释阶段】做了什么

候选者:初始化完成之后,当我们尝试执行一个类的方法时,会找到对应方法的字节码的信息,然后解释器会把字节码信息解释成系统能识别的指令码。

候选者:「解释」这个阶段它做的事情可以总结为:把字节码转换为操作系统识别的指令

候选者:在解释阶段会有两种方式把字节码信息解释成机器指令码,一个是字节码解释器、一个是即时编译器(JIT)。

候选者:JVM会对「热点代码」做编译,非热点代码直接进行解释。当JVM发现某个方法或代码块的运行特别频繁的时候,就有可能把这部分代码认定为「热点代码」

候选者:使用「热点探测」来检测是否为热点代码。「热点探测」一般有两种方式,计数器和抽样。HotSpot使用的是「计数器」的方式进行探测,为每个方法准备了两类计数器:方法调用计数器和回边计数器

候选者:这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。

候选者:即时编译器把热点方法的指令码保存起来,下次执行的时候就无需重复的进行解释,直接执行缓存的机器语言

面试官:嗯…

候选者:解释阶段结束后,最后就到了执行阶段。

候选者:「执行」这个阶段它做的事情可以总结为:操作系统把解释器解析出来的指令码,调用系统的硬件执行最终的程序指令。

候选者:上面就是我对从源码文件(.java)到代码执行的过程的理解了。

面试官:嗯…我还想问下你刚才提到的双亲委派模型…

候选者:下次一定!

本文总结:

  • Java跨平台因为有JVM屏蔽了底层操作系统

  • Java源码到执行的过程,从JVM的角度看可以总结为四个步骤:编译->加载->解释->执行

    • 「编译」经过 语法分析、语义分析、注解处理 最后才生成会class文件
    • 「加载」又可以细分步骤为:装载->连接->初始化。装载则把class文件装载至JVM,连接则校验class信息、分配内存空间及赋默认值,初始化则为变量赋值为正确的初始值。连接里又可以细化为:验证、准备、解析
    • 「解释」则是把字节码转换成操作系统可识别的执行指令,在JVM中会有字节码解释器和即时编译器。在解释时会对代码进行分析,查看是否为「热点代码」,如果为「热点代码」则触发JIT编译,下次执行时就无需重复进行解释,提高解释速度
    • 「执行」调用系统的硬件执行最终的程序指令


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

收起阅读 »

面试官:双亲委派模型你了解吗?

面试官:要不你今天来详细讲讲双亲委派机制? 候选者:嗯,好的。 候选者:上次提到了:class文件是通过「类加载器」装载至JVM中的 候选者:为了防止内存中存在多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载类,而是把请求委托给父加载器去完成,依次向...
继续阅读 »

面试官要不你今天来详细讲讲双亲委派机制?


候选者:嗯,好的。


候选者:上次提到了:class文件是通过「类加载器」装载至JVM中的


候选者:为了防止内存中存在多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载类,而是把请求委托给父加载器去完成,依次向上)


候选者:JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现装载,而程序中的类文件则由系统加载器(AppClassLoader )实现装载。



候选者:这应该很好理解吧?


面试官:雀食(确实)!


面试官顺着话题,我想问问,打破双亲委派机制是什么意思?


候选者:很好理解啊,意思就是:只要我加载类的时候,不是从APPClassLoader->Ext ClassLoader->BootStrap ClassLoader 这个顺序找,那就算是打破了啊


候选者:因为加载class核心的方法在LoaderClass类的loadClass方法上(双亲委派机制的核心实现)


候选者:那只要我自定义个ClassLoader,重写loadClass方法(不依照往上开始寻找类加载器),那就算是打破双亲委派机制了。


面试官:这么简单?


候选者:嗯,就是这么简单


面试官那你知道有哪个场景破坏了双亲委派机制吗?


候选者:最明显的就Tomcat啊


面试官:详细说说?


候选者:在初学时部署项目,我们是把war包放到tomcat的webapp下,这意味着一个tomcat可以运行多个Web应用程序(:


候选者:是吧?


面试官:嗯..


候选者:那假设我现在有两个Web应用程序,它们都有一个类,叫做User,并且它们的类全限定名都一样,比如都是com.yyy.User。但是他们的具体实现是不一样的


候选者:那么Tomcat是如何保证它们是不会冲突的呢?


候选者:答案就是,Tomcat给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找(:


候选者:那这样就做到了Web应用层级的隔离



面试官嗯,那你还知道Tomcat还有别的类加载器吗?


候选者:嗯,知道的


候选者:并不是Web应用程序下的所有依赖都需要隔离的,比如Redis就可以Web应用程序之间共享(如果有需要的话),因为如果版本相同,没必要每个Web应用程序都独自加载一份啊。


候选者:做法也很简单,Tomcat就在WebAppClassLoader上加了个父类加载器(SharedClassLoader),如果WebAppClassLoader自身没有加载到某个类,那就委托SharedClassLoader去加载。


候选者:(无非就是把需要应用程序之间需要共享的类放到一个共享目录下嘛)


面试官:嗯..


候选者:为了隔绝Web应用程序与Tomcat本身的类,又有类加载器(CatalinaClassLoader)来装载Tomcat本身的依赖


候选者:如果Tomcat本身的依赖和Web应用还需要共享,那么还有类加载器(CommonClassLoader)来装载进而达到共享


候选者:各个类加载器的加载目录可以到tomcat的catalina.properties配置文件上查看


候选者:我稍微画下Tomcat的类加载结构图吧,不然有点抽象



面试官:嗯,还可以,我听懂了,有点意思。


面试官顺便,我想问下,JDBC你不是知道吗,听说它也是破坏了双亲委派模型的,你怎么理解的。


候选者:Eumm,这个有没有破坏,见仁见智吧。


候选者:JDBC定义了接口,具体实现类由各个厂商进行实现嘛(比如MySQL)


候选者:类加载有个规则:如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。


候选者:我们用JDBC的时候,是使用DriverManager进而获取Connection,DriverManager在java.sql包下,显然是由BootStrap类加载器进行装载


候选者:当我们使用DriverManager.getConnection()时,得到的一定是厂商实现的类。


候选者:但BootStrap ClassLoader会能加载到各个厂商实现的类吗?


候选者:显然不可以啊,这些实现类又没在java包中,怎么可能加载得到呢


面试官:嗯..


候选者:DriverManager的解决方案就是,在DriverManager初始化的时候,得到「线程上下文加载器」


候选者:去获取Connection的时候,是使用「线程上下文加载器」去加载Connection的,而这里的线程上下文加载器实际上还是App ClassLoader


候选者:所以在获取Connection的时候,还是先找Ext ClassLoader和BootStrap ClassLoader,只不过这俩加载器肯定是加载不到的,最终会由App ClassLoader进行加载



面试官:嗯..


候选者:那这种情况,有的人觉得破坏了双亲委派机制,因为本来明明应该是由BootStrap ClassLoader进行加载的,结果你来了一手「线程上下文加载器」,改掉了「类加载器」


候选者:有的人觉得没破坏双亲委派机制,只是改成由「线程上下文加载器」进行类加载,但还是遵守着:「依次往上找父类加载器进行加载,都找不到时才由自身加载」。认为”原则”上是没变的。


面试官:那我了解了


本文总结




  • 前置知识: JDK中默认类加载器有三个:AppClassLoader、Ext ClassLoader、BootStrap ClassLoader。AppClassLoader的父加载器为Ext ClassLoader、Ext ClassLoader的父加载器为BootStrap ClassLoader。这里的父子关系并不是通过继承实现的,而是组合。




  • 什么是双亲委派机制: 加载器在加载过程中,先把类交由父类加载器进行加载,父类加载器没找到才由自身加载。




  • 双亲委派机制目的: 为了防止内存中存在多份同样的字节码(安全)




  • 类加载规则: 如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。




  • 如何打破双亲委派机制: 自定义ClassLoader,重写loadClass方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)




  • 打破双亲委派机制案例: Tomcat



    • 为了Web应用程序类之间隔离,为每个应用程序创建WebAppClassLoader类加载器

    • 为了Web应用程序类之间共享,把ShareClassLoader作为WebAppClassLoader的父类加载器,如果WebAppClassLoader加载器找不到,则尝试用ShareClassLoader进行加载

    • 为了Tomcat本身与Web应用程序类隔离,用CatalinaClassLoader类加载器进行隔离,CatalinaClassLoader加载Tomcat本身的类

    • 为了Tomcat与Web应用程序类共享,用CommonClassLoader作为CatalinaClassLoader和ShareClassLoader的父类加载器

    • ShareClassLoader、CatalinaClassLoader、CommonClassLoader的目录可以在Tomcat的catalina.properties进行配置




  • 线程上下文加载器: 由于类加载的规则,很可能导致父加载器加载时依赖子加载器的类,导致无法加载成功(BootStrap ClassLoader无法加载第三方库的类),所以存在「线程上下文加载器」来进行加载。


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

Flutter 快速开发框架

项目简介 此框架旨在将常规的Flutter项目中使用到的通用(与业务无关)的功能从剥离出来,构成Flutter开发项目的框架,在开发新的Flutter项目时,可以直接引用本项目 import 'package:framework/framework.dart'...
继续阅读 »

项目简介


此框架旨在将常规的Flutter项目中使用到的通用(与业务无关)的功能从剥离出来,构成Flutter开发项目的框架,在开发新的Flutter项目时,可以直接引用本项目 import 'package:framework/framework.dart'来使用框架中相关的功能,提升开发效率。


Github项目地址:可以看我主页找我拿


此框架目前包含以下功能模块:接口请求API模块、消息提示模块、路由模块、统一错误处理、日志模块、屏幕适配测试、自定义UI组件库、本地存储模块构成


框架使用说明


引用


import 'package:framework/framework.dart';

使用


参考 Example 中使用的例子

框架首页



网络HTTP模块



消息提示模块



路由模块



统一错误处理介绍



日志模块



屏幕适配测试



自定义UI组件库



本地存储模块



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

都 2021 年了,还有人在研究 Handler?

我们经常使用和提及 Android 中特有的线程间通信方式即 Handler 机制,缘于该机制特别好用、极为重要! 初尝 Handler 机制的时候,原以为 Handler 类发挥了很大的作用。当你深入了解它的原理之后,会发现 Handler 只是该机制的调用...
继续阅读 »

我们经常使用和提及 Android 中特有的线程间通信方式即 Handler 机制,缘于该机制特别好用、极为重要!


初尝 Handler 机制的时候,原以为 Handler 类发挥了很大的作用。当你深入了解它的原理之后,会发现 Handler 只是该机制的调用入口和回调而已,最重要的东西是 LooperMessagQueue,以及不断流转的 Message


本次针对 Handler 机制常被提及和容易困扰的 20 个问题进行整理和回答,供大家解惑和回顾~


问题前瞻:



  1. 简述下 Handler 机制的总体原理?

  2. Looper 存在哪?如何可以保证线程独有?

  3. 如何理解 ThreadLocal 的作用?

  4. 主线程 Main Looper 和一般 Looper 的异同?

  5. Handler 或者说 Looper 如何切换线程?

  6. Looper 的 loop() 死循环为什么不卡死?

  7. Looper 的等待是如何能够准确唤醒的?

  8. Message 如何获取?为什么这么设计?

  9. MessageQueue 如何管理 Message?

  10. 理解 Message 和 MessageQueue 的异同?

  11. Message 的执行时刻如何管理?

  12. Handler、Mesage 和 Runnable 的关系如何理解?

  13. IdleHandler 空闲 Message 了解过吗?有什么用?

  14. 异步 Message 或同步屏障了解过吗?怎么用?什么原理?

  15. Looper 和 MessageQueue、Message 及 Handler 的关系?

  16. Native 侧的 NativeMessageQueue 和 Looper 的作用是?

  17. Native 侧如何使用 Looper?

  18. Handler 为什么可能导致内存泄露?如何避免?

  19. Handler 在系统当中的应用

  20. Android 为什么不允许并发访问 UI?


1. 简述下 Handler 机制的总体原理?




  1. Looper 准备和开启轮循:



    • Looper#prepare() 初始化线程独有的 Looper 以及 MessageQueue

    • Looper#loop() 开启死循环读取 MessageQueue 中下一个满足执行时间的 Message

      • 尚无 Message 的话,调用 Native 侧的 pollOnce() 进入无限等待

      • 存在 Message,但执行时间 when 尚未满足的话,调用 pollOnce() 时传入剩余时长参数进入有限等待






  2. Message 发送、入队和出队:



    • Native 侧如果处于无限等待的话:任意线程向 Handler 发送 MessageRunnable 后,Message 将按照 when 条件的先后,被插入 Handler 持有的 Looper 实例所对应的 MessageQueue 中适当的位置。 MessageQueue 发现有合适的 Message 插入后将调用 Native 侧的 wake() 唤醒无限等待的线程。这将促使 MessageQueue 的读取继续进入下一次循环,此刻 Queue 中已有满足条件的 Message 则出队返回给 Looper

    • Native 侧如果处于有限等待的话:在等待指定时长后 epoll_wait 将返回。线程继续读取 MessageQueue,此刻因为时长条件将满足将其出队




  3. Looper 处理 Message 的实现:


    Looper 得到 Message 后回调 Message 的 callback 属性即 Runnable,或依据 target 属性即 Handler,去执行 Handler 的回调。



    • 存在 mCallback 属性的话回调 Handler$Callback

    • 反之,回调 handleMessage()




2. Looper 存在哪?如何可以保证线程独有?



  • Looper 实例被管理在静态属性 sThreadLocal

  • ThreadLocal 内部通过 ThreadLocalMap 持有 Looper,key 为 ThreadLocal 实例本身,value 即为 Looper 实例

  • 每个 Thread 都有一个自己的 ThreadLocalMap,这样可以保证每个线程对应一个独立的 Looper 实例,进而保证 myLooper() 可以获得线程独有的 Looper


彩蛋:一个 App 拥有几个 Looper 实例?几个 ThreadLocal 实例?几个 MessageQueue 实例?几个 Message 实例?几个 Handler 实例



  • 一个线程只有一个 Looper 实例

  • 一个 Looper 实例只对应着一个 MessageQueue 实例

  • 一个 MessageQueue 实例可对应多个 Message 实例,其从 Message 静态池里获取,存在 50 的上限

  • 一个线程可以拥有多个 Handler 实例,其Handler 只是发送和执行任务逻辑的入口和出口

  • ThreadLocal 实例是静态的,整个进程共用一个实例。每个 Looper 存放的 ThreadLocalMap 均弱引用它作为 key


3. 如何理解 ThreadLocal 的作用?



  • 首先要明确并非不是用来切换线程的,只是为了让每个线程方便程获取自己的 Looper 实例,见 Looper#myLooper()

    • 后续可供 Handler 初始化时指定其所属的 Looper 线程

    • 也可用来线程判断自己是否是主线程




4. 主线程 Main Looper 和一般 Looper 的异同?




  • 区别:



    1. Main Looper 不可 quit


    主线程需要不断读取系统消息和用书输入,是进程的入口,只可被系统直接终止。进而其 Looper 在创建的时候设置了不可 quit 的标志,而其他线程的 Looper 则可以也必须手动 quit



    1. Main Looper 实例还被静态缓存


    为了便于每个线程获得主线程 Looper 实例,见 Looper#getMainLooper(),Main Looper 实例还作为 sMainLooper 属性缓存到了 Looper 类中。




  • 相同点:



    1. 都是通过 Looper#prepare() 间接调用 Looper 构造函数创建的实例

    2. 都被静态实例 ThreadLocal 管理,方便每个线程获取自己的 Looper 实例




彩蛋:主线程为什么不用初始化 Looper?


App 的入口并非 MainActivity,也不是 Application,而是 ActivityThread。


其为了 Application、ContentProvider、Activity 等组件的运行,必须事先启动不停接受输入的 Looper 机制,所以在 main() 执行的最后将调用 prepareMainLooper() 创建 Looper 并调用 loop() 轮循。


不需要我们调用,也不可能有我们调用。


可以说如果主线程没有创建 Looper 的话,我们的组件也不可能运行得到!


5. Handler 或者说 Looper 如何切换线程?




  1. Handler 创建的时候指定了其所属线程的 Looper,进而持有了 Looper 独有的 MessageQueue




  2. Looper#loop() 会持续读取 MessageQueue 中合适的 Message,没有 Message 的时候进入等待




  3. 当向 Handler 发送 Message 或 Runnable 后,会向持有的 MessageQueue 中插入 Message




  4. Message 抵达并满足条件后会唤醒 MessageQueue 所属的线程,并将 Message 返回给 Looper




  5. Looper 接着回调 Message 所指向的 Handler Callback 或 Runnable,达到线程切换的目的




简言之,向 Handler 发送 Message 其实是向 Handler 所属线程的独有 MessageQueue 插入 Message。而线程独有的 Looper 又会持续读取该 MessageQueue。所以向其他线程的 Handler 发送完 Message,该线程的 Looper 将自动响应。


6. Looper 的 loop() 死循环为什么不卡死?


为了让主线程持续处理用户的输入,loop() 是死循环,持续调用 MessageQueue#next() 读取合适的 Message。


但当没有 Message 的时候,会调用 pollOnce() 并通过 Linux 的 epoll 机制进入等待并释放资源。同时 eventFd 会监听 Message 抵达的写入事件并进行唤醒。


这样可以空闲时释放资源、不卡死线程,同时能持续接收输入的目的


彩蛋1:loop() 后的处理为什么不可执行


因为 loop() 是死循环,直到 quit 前后面的处理无法得到执行,所以避免将处理放在 loop() 的后面。


**彩蛋2:Looper 等待的时候线程到底是什么状态? **


调用 Linux 的 epoll 机制进入等待,事实上 Java 侧打印该线程的状态,你会发现线程处于 Runnable 状态,只不过 CPU 资源被暂时释放。


7. Looper 的等待是如何能够准确唤醒的?


读取合适 Message 的 MessageQueue#next() 会因为 Message 尚无或执行条件尚未满足进行两种等的等待:




  • 无限等待


    尚无 Message(队列中没有 Message 或建立了同步屏障但尚无异步 Message)的时候,调用 Natvie 侧的 pollOnce() 会传入参数 -1


    Linux 执行 epoll_wait() 将进入无限等待,其等待合适的 Message 插入后调用 Native 侧的 wake() 向唤醒 fd 写入事件触发唤醒 MessageQueue 读取的下一次循环




  • 有限等待


    有限等待的场合将下一个 Message 剩余时长作为参数交给 epoll_wait(),epoll 将等待一段时间之后自动返回,接着回到 MessageQueue 读取的下一次循环




8. Message 如何获取?为什么这么设计?




  • 享元设计模式:通过 Message 的静态方法 obatin() 获取,因为该方法不是无脑地 new,而是从单链表池子里获取实例,并在 recycle() 后将其放回池子




  • 好处在于复用 Message 实例,满足频繁使用 Message 的场景,更加高效




  • 当然,缓存池存在上限 50,因为没必要无限制地缓存,这本身也是一种浪费




  • 需要留意缓存池是静态的,也就是整个进程共用一个缓存池




9. MessageQueue 如何管理 Message?



  • MessageQueue 通过单链表管理 Message,不同于进程共用的 Message Pool,其是线程独有的

  • 通过 Message 的执行时刻 when 对 Message 进行排队和出队

  • MessageQueue 除了管理 Message,还要管理空闲 Handler 和 同步屏障


10. 理解 Message 和 MessageQueue 的异同?




  • 相同点:都是通过单链表来管理 Message 实例;




    • Message 通过 obtain() 和 recycle() 向单链表获取插入节点




    • MessageQueue 通过 enqueueMessage() 和 next() 向单链表获取和插入节点






  • 区别:



    • Message 单链表是静态的,供进程使用的缓存池




  • MessageQueue 单链表非静态,只供 Looper 线程使用




11. Message 的执行时刻如何管理?



  • 发送的 Message 都是按照执行时刻 when 属性的先后管理在 MessageQueue 里

    • 延时 Message 的 when 等于调用的当前时刻delay 之和

    • 非延时 Message 的 when 等于当前时刻(delay 为 0

    • 插队 Message 的 when 固定为 0,便于插入队列的 head



  • 之后 MessageQueue 会根据读取的时刻和 when 进行比较

    • 将 when 已抵达的出队,

    • 尚未抵达的计算出当前时刻和目标 when 的插值,交由 Native 等待对应的时长,时间到了自动唤醒继续进行 Message 的读取




事实上,无论上述哪种 Message 都不能保证在其对应的 when 时刻执行,往往都会延迟一些!因为必须等当前执行的 Message 处理完了才有机会读取队列的下一个 Message。


比如发送了非延时 Message,when 即为发送的时刻,可它们不会立即执行。都要等主线程现有的任务(Message)走完才能有机会出队,而当这些任务执行完 when 的时刻已经过了。假使队列的前面还有其他 Message 的话,延迟会更加明显!


彩蛋:. onCreate() 里向 Handler 发送大量 Message 会导致主线程卡顿吗?


不会,发送的大量 Message 并非立即执行,只是先放到队列当中而已。


onCreate() 以及之后同步调用的 onStart() 和 onResume() 处理,本质上也是 Message。等这个 Message 执行完之后,才会进行读取 Message 的下一次循环,这时候才能回调 onCreate 里发送的 Message。


需要说明的是,如果发送的是 FrontOfQueue 将 Message 插入队首也不会立即先执行,因为 onStart 和 onResume 是 onCreate 之后同步调用的,本质上是同一个 Message 的作业周期


12. Handler、Mesage 和 Runnable 的关系如何理解?



  • 作为使用 Handler 机制的入口,Handler 是发送 Message 或 Runnable 的起点

  • 发送的 Runnable 本质上也是 Message,只不过作为 callback 属性被持有

  • Handler 作为 target 属性被持有在 Mesage 中,在 Message 执行条件满足的时候供 Looper 回调


事实上,Handler 只是供 App 使用 Handler 机制的 API,实质来说,Message 是更为重要的载体。


13. IdleHandler 空闲 Message 了解过吗?有什么用?




  • 适用于期望空闲时候执行,但不影响主线程操作的任务




  • 系统应用:



    1. Activity destroy 回调就放在了 IdleHandler

    2. ActivityThreadGCHandler 使用了 IdleHandler,在空闲的时候执行 GC 操作




  • App 应用:



    1. 发送一个返回 true 的 IdleHandler,在里面让某个 View 不停闪烁,这样当用户发呆时就可以诱导用户点击这个 View

    2. 将某部分初始化放在 IdleHandler 里不影响 Activity 的启动




  • 彩蛋问题:



    1. add/remove IdleHandler 的方法,是否需要成对使用?


    不需要,回调返回 false 也可以移除




    1. mIdleHanders 一直不为空时,为什么不会进入死循环?




    执行过 IdleHandler 之后会将计数重置为 0,确保下一次循环不重复执行



    1. 是否可以将一些不重要的启动服务,搬移到 IdleHandler 中去处理?


    最好不要,回调时机不太可控,需要搭配 remove 谨慎使用



    1. IdleHandle 的 queueIdle() 运行在那个线程?


    取决于 IdleHandler add 到的 MessageQueue 所处的线程




14. 异步 Message 或同步屏障了解过吗?怎么用?什么原理?




  • 异步 Message:设置了 isAsync 属性的 Message 实例



    • 可以用异步 Handler 发送

    • 也可以调用 Message#setAsynchronous() 直接设置为异步 Message




  • 同步屏障:在 MessageQueue 的某个位置放一个 target 属性为 null 的 Message,确保此后的非异步 Message 无法执行,只能执行异步 Message




  • 原理:当 MessageQueue 轮循 Message 时候发现建立了同步屏障的时候,会去跳过其他 Message,读取下个 async 的 Message 并执行,屏障移除之前同步 Message 都会被阻塞




  • 应用:比如屏幕刷新 Choreographer 就使用到了同步屏障,确保屏幕刷新事件不会因为队列负荷影响屏幕及时刷新。




  • 注意:同步屏障的添加或移除 API 并未对外公开,App 需要使用的话需要依赖反射机制




15. Looper 和 MessageQueue、Message 及 Handler 的关系?



  • Message 是承载任务的载体,在 Handler 机制中贯穿始终

  • Handler 则是对外公开的 API,负责发送 Message 和处理任务的回调,是 Message 的生产者

  • MessagQueue 负责管理待处理 Message 的入队和出队,是 Message 的容器

  • Looper 负责轮循 MessageQueue,保持线程持续运行任务,是 Message 的消费者


彩蛋:如何保证 MessageQueue 并发访问安全?


任何线程都可以通过 Handler 生产 Message 并放入 MessageQueue 中,可 Queue 所属的 Looper 在持续地读取并尝试消费 Message。如何保证两者不产生死锁?


Looper 在消费 Message 之前要先拿到 MessageQueue 的锁,**只不过没有 Message 或 Message 尚未满足条件的进行等待前会事先释放锁,**具体在于 nativePollOnce() 的调用在 synchronized 方法块的外侧。


Message 入队前也需先拿到 MessageQueue 的锁,而这时 Looper 线程正在等待且不持有锁,可以确保 Message 的成功入队。入队后执行唤醒后释放锁,Native 收到 event 写入后恢复 MessagQueue 的读取并可以拿到锁,成功出队。


这样一种在没有 Message 可以消费时执行等待同时不占着锁的机制,避免了生产和消费的死锁。


16. Native 侧的 NativeMessageQueue 和 Looper 的作用是?




  • NativeMessageQueue 负责连接 Java 侧的 MessageQueue,进行后续的 waitwake,后续将创建 wake 的FD,并通过 epoll 机制等待或唤醒。但并不参与管理 Java 的 Message




  • Native 侧也需要 Looper 机制,等待和唤醒的需求是同样的,所以将这部分实现都封装到了 JNI 的NativeMessageQueue 和 Native 的 Looper 中,供 Java 和 Native 一起使用




17. Native 侧如何使用 Looper?




  • Looper Native 部分承担了 Java 侧 Looper 的等待和唤醒,除此之外其还提供了 Message、MessageHandlerWeakMessageHandlerLooperCallbackSimpleLooperCallback 等 API




  • 这些部分可供 Looper 被 Native 侧直接调用,比如 InputFlinger 广泛使用了 Looper




  • 主要方法是调用 Looper 构造函数或 prepare 创建 Looper,然后通过 poll 开始轮询,接着 sendMessageaddEventFd,等待 Looper 的唤醒。使用过程和 Java 的调用思路类似




18. Handler 为什么可能导致内存泄露?如何避免?



  • 持有 Activity 实例的内名内部类或内部类的生命周期应当和 Activity 保持一致,否则产生内存泄露的风险。

  • 如果 Handler 使用不当,将造成不一致,表现为:匿名内部类或内部类写法的 Handler、Handler$Callback、Runnable,或者Activity 结束时仍有活跃的 Thread 线程或 Looper 子线程

  • 具体在于:异步任务仍然活跃或通过发送的 Message 尚未处理完毕,将使得内部类实例的生命周期被错误地延长。造成本该回收的 Activity 实例被别的 ThreadMain Looper 占据而无法及时回收(活跃的 Thread 或 静态属性 sMainLooper 是 GC Root 对象)

  • 建议的做法:

    • 无论是 Handler、Handler$Callback 还是 Runnable,尽量采用静态内部类 + 弱引用的写法,确保尽管发生不当引用的时候也可以因为弱引用能清楚持有关系

    • 另外在 Activity 销毁的时候及时地终止 Thread、停止子线程的 Looper 或清空 Message,确保彻底切断 Activity 经由 Message 抵达 GC Root 的引用源头(Message 清空后会其与 Handler 的引用关系,Thread 的终止将结束其 GC Root 的源头)




注意:静态的 sThreadLocal 实例不持有存放 Looper 实例的 ThreadLocalMap,而是由 Thread 持有。从这个角度上来讲,Looper 会被活跃的 GC Root Thread 持有,进而也可能导致内存泄露。


彩蛋:网传的 Handler$Callback 方案能否解决内存泄露?


不能。


Callback 采用内部类或匿名内部类写法的话,默认持有 Activity 的引用,而 Callback 被 Handler 持有。这最终将导致 Message -> Handler -> Callback -> Activity 的链条仍然存在。


19. Handler 在系统当中的应用


特别广泛,比如:



  • Activity 生命周期的管理

  • 屏幕刷新

  • HandlerThread、IntentService

  • AsyncTask 等。


主要利用 Handler 的切换线程、主线程异步 Message 的重要特性。注意:Binder 线程非主线程,但很多操作比如生命周期的管理都要回到主线程,所以很多 Binder 调用过来后都要通过 Handler 切换回主线程执行后续任务,比如 ActviityThread$H 就是 extends Handler。


20. Android 为什么不允许并发访问 UI?


Android 中 UI 非线程安全,并发访问的话会造成数据和显示错乱。


但此限制的检查始于ViewRootImpl#checkThread(),其会在刷新等多个访问 UI 的时机被调用,去检查当前线程,非主线程的话抛出异常。


而 ViewRootImpl 的创建在 onResume() 之后,也就是说如果在 onResume() 执行前启动线程访问 UI 的话是不会报错的,这点需要留意!


彩蛋:onCreate() 里子线程更新 UI 有问题吗?为什么?


不会。


因为异常的检测处理在 ViewRootImpl 中,该实例的创建和检测在 onResume() 之后进行。


结语


能力和精力有限,如果出现遗漏、错误或细节不明的地方,欢迎不吝赐教。


让我们共同维护这些个问题,彻底吃透 Handler 机制!


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

【知识点】OkHttp 原理 8 连问

前言 OkHttp可以说是Android开发中最常见的网络请求框架,OkHttp使用方便,扩展性强,功能强大,OKHttp源码与原理也是面试中的常客 但是OKHttp的源码内容比较多,想要学习它的源码往往千头万绪,一时抓不住重点. 本文从几个问题出发梳理OKH...
继续阅读 »

前言


OkHttp可以说是Android开发中最常见的网络请求框架,OkHttp使用方便,扩展性强,功能强大,OKHttp源码与原理也是面试中的常客

但是OKHttp的源码内容比较多,想要学习它的源码往往千头万绪,一时抓不住重点.

本文从几个问题出发梳理OKHttp相关知识点,以便快速构建OKHttp知识体系,如果对你有用,欢迎点赞~


本文主要包括以下内容



  1. OKHttp请求的整体流程是怎样的?

  2. OKHttp分发器是怎样工作的?

  3. OKHttp拦截器是如何工作的?

  4. 应用拦截器和网络拦截器有什么区别?

  5. OKHttp如何复用TCP连接?

  6. OKHttp空闲连接如何清除?

  7. OKHttp有哪些优点?

  8. OKHttp框架中用到了哪些设计模式?


1. OKHttp请求整体流程介绍


首先来看一个最简单的Http请求是如何发送的。


   val okHttpClient = OkHttpClient()
val request: Request = Request.Builder()
.url("https://www.google.com/")
.build()

okHttpClient.newCall(request).enqueue(object :Callback{
override fun onFailure(call: Call, e: IOException) {
}

override fun onResponse(call: Call, response: Response) {
}
})

这段代码看起来比较简单,OkHttp请求过程中最少只需要接触OkHttpClientRequestCallResponse,但是框架内部会进行大量的逻辑处理。

所有网络请求的逻辑大部分集中在拦截器中,但是在进入拦截器之前还需要依靠分发器来调配请求任务。

关于分发器与拦截器,我们在这里先简单介绍下,后续会有更加详细的讲解



  • 分发器:内部维护队列与线程池,完成请求调配;

  • 拦截器:五大默认拦截器完成整个请求过程。




整个网络请求过程大致如上所示



  1. 通过建造者模式构建OKHttpClientRequest

  2. OKHttpClient通过newCall发起一个新的请求

  3. 通过分发器维护请求队列与线程池,完成请求调配

  4. 通过五大默认拦截器完成请求重试,缓存处理,建立连接等一系列操作

  5. 得到网络请求结果


2. OKHttp分发器是怎样工作的?


分发器的主要作用是维护请求队列与线程池,比如我们有100个异步请求,肯定不能把它们同时请求,而是应该把它们排队分个类,分为正在请求中的列表和正在等待的列表,
等请求完成后,即可从等待中的列表中取出等待的请求,从而完成所有的请求


而这里同步请求各异步请求又略有不同


同步请求


synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}

因为同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。后续按照加入队列的顺序同步请求即可


异步请求


synchronized void enqueue(AsyncCall call) {
//请求数最大不超过64,同一Host请求不能超过5个
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}

当正在执行的任务未超过最大限制64,同时同一Host的请求不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。

每个任务完成后,都会调用分发器的finished方法,这里面会取出等待队列中的任务继续执行


3. OKHttp拦截器是怎样工作的?


经过上面分发器的任务分发,下面就要利用拦截器开始一系列配置了


# RealCall
override fun execute(): Response {
try {
client.dispatcher.executed(this)
return getResponseWithInterceptorChain()
} finally {
client.dispatcher.finished(this)
}
}

我们再来看下RealCallexecute方法,可以看出,最后返回了getResponseWithInterceptorChain,责任链的构建与处理其实就是在这个方法里面


internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)

val chain = RealInterceptorChain(
call = this,interceptors = interceptors,index = 0
)
val response = chain.proceed(originalRequest)
}

如上所示,构建了一个OkHttp拦截器的责任链

责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。

如上所示责任链添加的顺序及作用如下表所示:







































拦截器作用
应用拦截器拿到的是原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。
RetryAndFollowUpInterceptor处理错误重试和重定向
BridgeInterceptor应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。
CacheInterceptor缓存拦截器,如果命中缓存则不会发起网络请求。
ConnectInterceptor连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。
networkInterceptors(网络拦截器)用户自定义拦截器,通常用于监控网络层的数据传输。
CallServerInterceptor请求拦截器,在前置准备工作完成后,真正发起了网络请求。

我们的网络请求就是这样经过责任链一级一级的递推下去,最终会执行到CallServerInterceptorintercept方法,此方法会将网络响应的结果封装成一个Response对象并return。之后沿着责任链一级一级的回溯,最终就回到getResponseWithInterceptorChain方法的返回,如下图所示:


4. 应用拦截器和网络拦截器有什么区别?


从整个责任链路来看,应用拦截器是最先执行的拦截器,也就是用户自己设置request属性后的原始请求,而网络拦截器位于ConnectInterceptorCallServerInterceptor之间,此时网络链路已经准备好,只等待发送请求数据。它们主要有以下区别



  1. 首先,应用拦截器在RetryAndFollowUpInterceptorCacheInterceptor之前,所以一旦发生错误重试或者网络重定向,网络拦截器可能执行多次,因为相当于进行了二次请求,但是应用拦截器永远只会触发一次。另外如果在CacheInterceptor中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。

  2. 其次,除了CallServerInterceptor之外,每个拦截器都应该至少调用一次realChain.proceed方法。实际上在应用拦截器这层可以多次调用proceed方法(本地异常重试)或者不调用proceed方法(中断),但是网络拦截器这层连接已经准备好,可且仅可调用一次proceed方法。

  3. 最后,从使用场景看,应用拦截器因为只会调用一次,通常用于统计客户端的网络请求发起情况;而网络拦截器一次调用代表了一定会发起一次网络通信,因此通常可用于统计网络链路上传输的数据。


5. OKHttp如何复用TCP连接?


ConnectInterceptor的主要工作就是负责建立TCP连接,建立TCP连接需要经历三次握手四次挥手等操作,如果每个HTTP请求都要新建一个TCP消耗资源比较多

Http1.1已经支持keep-alive,即多个Http请求复用一个TCP连接,OKHttp也做了相应的优化,下面我们来看下OKHttp是怎么复用TCP连接的


ConnectInterceptor中查找连接的代码会最终会调用到ExchangeFinder.findConnection方法,具体如下:


# ExchangeFinder
//为承载新的数据流 寻找 连接。寻找顺序是 已分配的连接、连接池、新建连接
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
synchronized (connectionPool) {
// 1.尝试使用 已给数据流分配的连接.(例如重定向请求时,可以复用上次请求的连接)
releasedConnection = transmitter.connection;
result = transmitter.connection;

if (result == null) {
// 2. 没有已分配的可用连接,就尝试从连接池获取。(连接池稍后详细讲解)
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
result = transmitter.connection;
}
}
}

synchronized (connectionPool) {
if (newRouteSelection) {
//3. 现在有了IP地址,再次尝试从连接池获取。可能会因为连接合并而匹配。(这里传入了routes,上面的传的null)
routes = routeSelection.getAll();
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
foundPooledConnection = true;
result = transmitter.connection;
}
}

// 4.第二次没成功,就把新建的连接,进行TCP + TLS 握手,与服务端建立连接. 是阻塞操作
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);

synchronized (connectionPool) {
// 5. 最后一次尝试从连接池获取,注意最后一个参数为true,即要求 多路复用(http2.0)
//意思是,如果本次是http2.0,那么为了保证 多路复用性,(因为上面的握手操作不是线程安全)会再次确认连接池中此时是否已有同样连接
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
// 如果获取到,就关闭我们创建里的连接,返回获取的连接
result = transmitter.connection;
} else {
//最后一次尝试也没有的话,就把刚刚新建的连接存入连接池
connectionPool.put(result);
}
}

return result;
}

上面精简了部分代码,可以看出,连接拦截器使用了5种方法查找连接



  1. 首先会尝试使用 已给请求分配的连接。(已分配连接的情况例如重定向时的再次请求,说明上次已经有了连接)

  2. 若没有 已分配的可用连接,就尝试从连接池中 匹配获取。因为此时没有路由信息,所以匹配条件:address一致——hostport、代理等一致,且匹配的连接可以接受新的请求。

  3. 若从连接池没有获取到,则传入routes再次尝试获取,这主要是针对Http2.0的一个操作,Http2.0可以复用square.comsquare.ca的连接

  4. 若第二次也没有获取到,就创建RealConnection实例,进行TCP + TLS握手,与服务端建立连接。

  5. 此时为了确保Http2.0连接的多路复用性,会第三次从连接池匹配。因为新建立的连接的握手过程是非线程安全的,所以此时可能连接池新存入了相同的连接。

  6. 第三次若匹配到,就使用已有连接,释放刚刚新建的连接;若未匹配到,则把新连接存入连接池并返回。


以上就是连接拦截器尝试复用连接的操作,流程图如下:


6. OKHttp空闲连接如何清除?


上面说到我们会建立一个TCP连接池,但如果没有任务了,空闲的连接也应该及时清除,OKHttp是如何做到的呢?


  # RealConnectionPool
private val cleanupQueue: TaskQueue = taskRunner.newQueue()
private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
override fun runOnce(): Long = cleanup(System.nanoTime())
}

long cleanup(long now) {
int inUseConnectionCount = 0;//正在使用的连接数
int idleConnectionCount = 0;//空闲连接数
RealConnection longestIdleConnection = null;//空闲时间最长的连接
long longestIdleDurationNs = Long.MIN_VALUE;//最长的空闲时间

//遍历连接:找到待清理的连接, 找到下一次要清理的时间(还未到最大空闲时间)
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();

//若连接正在使用,continue,正在使用连接数+1
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
//空闲连接数+1
idleConnectionCount++;

// 赋值最长的空闲时间和对应连接
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
//若最长的空闲时间大于5分钟 或 空闲数 大于5,就移除并关闭这个连接
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// else,就返回 还剩多久到达5分钟,然后wait这个时间再来清理
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
//连接没有空闲的,就5分钟后再尝试清理.
return keepAliveDurationNs;
} else {
// 没有连接,不清理
cleanupRunning = false;
return -1;
}
}
//关闭移除的连接
closeQuietly(longestIdleConnection.socket());

//关闭移除后 立刻 进行下一次的 尝试清理
return 0;
}

思路还是很清晰的:



  1. 在将连接加入连接池时就会启动定时任务

  2. 有空闲连接的话,如果最长的空闲时间大于5分钟 或 空闲数 大于5,就移除关闭这个最长空闲连接;如果 空闲数 不大于5 且 最长的空闲时间不大于5分钟,就返回到5分钟的剩余时间,然后等待这个时间再来清理。

  3. 没有空闲连接就等5分钟后再尝试清理。

  4. 没有连接不清理。


流程如下图所示:


7. OKHttp有哪些优点?



  1. 使用简单,在设计时使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。

  2. 扩展性强,可以通过自定义应用拦截器与网络拦截器,完成用户各种自定义的需求

  3. 功能强大,支持SpdyHttp1.XHttp2、以及WebSocket等多种协议

  4. 通过连接池复用底层TCP(Socket),减少请求延时

  5. 无缝的支持GZIP减少数据流量

  6. 支持数据缓存,减少重复的网络请求

  7. 支持请求失败自动重试主机的其他ip,自动重定向


8. OKHttp框架中用到了哪些设计模式?



  1. 构建者模式:OkHttpClientRequest的构建都用到了构建者模式

  2. 外观模式: OkHttp使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。

  3. 责任链模式: OKHttp的核心就是责任链模式,通过5个默认拦截器构成的责任链完成请求的配置

  4. 享元模式: 享元模式的核心即池中复用,OKHttp复用TCP连接时用到了连接池,同时在异步请求中也用到了线程池


总结


本文主要梳理了OKHttp原理相关知识点,并回答了以下问题:



  1. OKHttp请求的整体流程是怎样的?

  2. OKHttp分发器是怎样工作的?

  3. OKHttp拦截器是如何工作的?

  4. 应用拦截器和网络拦截器有什么区别?

  5. OKHttp如何复用TCP连接?

  6. OKHttp空闲连接如何清除?

  7. OKHttp有哪些优点?

  8. OKHttp框架中用到了哪些设计模式?

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

写给vue转react的同志们(4)

下一篇应各位老爷要求,这篇文章开始拥抱hooks,本文将从vue3与react 17.x(hooks)对比来感受两大框架的同工异曲之处。 今天的主题:vue3与react 定义与修改数据vue3与react 计算属性vue3与react 实现监听 vue3与r...
继续阅读 »

下一篇

应各位老爷要求,这篇文章开始拥抱hooks,本文将从vue3react 17.x(hooks)对比来感受两大框架的同工异曲之处。


今天的主题:

vue3与react 定义与修改数据

vue3与react 计算属性

vue3与react 实现监听


vue3与react hooks 定义与修改数据


实际上两者都是偏hooks的写法,这样的高灵活性的组合,相信大部分人还是觉得香的,无论是以前的vue options或是react class的写法都是比较臃肿且复用性较差的(相较于hooks)。下面举个例子对比一下。


vue3





react


import { useState } from 'react';
function App() {
const [todos, setTodos] = useState({
age: 25,
sex: 'man'
})
const setObj = () => {
setTodos({
...todos,
age: todos.age + 1
})
}
return (

{todos.age}


{todos.sex}



);
}



通过比较上述代码可以看到vue3react hooks基本写法是差不多的,只是vue提倡template写法,react提倡jsx写法,模板的写法并不影响你js逻辑的使用,所以不论框架再怎么变化,js也是我们前端的铁饭碗,请各位务必掌握好!


vue3与react 计算属性


计算属性这一块是为了不让我们在模板处写上太过复杂的运算,这是计算属性存在的意义。vue3中提供了computed方法,react hook提供了useMemo让我们实现计算属性(没有类写法中可以使用get来实现计算属性具体可看往期文章)


vue3






react


import { useMemo, useState } from 'react'
function App() {
const [obj, setObj] = useState({
age: 25,
sex: 'man'
})
const people = useMemo(() => {
return `this people age is ${obj.age} and sex is ${obj.sex}`
}, [obj])
return (

age: {obj.age}


sex: {obj.sex}


info: {people}



)
}


可以看到对比两大框架的计算属性,除了模板书写略有不同其他基本神似,都是hooks写法,通过框架内部暴露的某个方法去实现某个操作,这样一来追述和定位错误时也更加方便,hooks大概率就是现代框架的趋势,它不仅让开发者的代码可以更加灵活的组合复用,数据和方法来源也更加容易定位清晰。


vue3与react 实现监听


vue3watch被暴露成一个方法通过传入对应监听的参数以及回调函数实现,react中也有类似的功能useEffect,实际上他和componentDidMountcomponentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。看例子:


vue3



import { ref, watch } from 'vue'
export default {
setup() {
const count = ref(0)
watch(count,
(val) => {
console.log(val)
},
{ immediate: true, deep: true }
)
function setCount() {
count.value ++
}
return {
count,
setCount
}
}

}

react


import { useState, useEffect } from 'react'
function App() {
const [count, setCount] = useState(0)
const setCount = () => {
setCount(count + 1)
}
useEffect(() => {
console.log(count)
})
return (

count: {count}



)
}

可以看到,vue3整体趋势是往hooks靠,不难看出来未来不论哪种框架大概率最终都会往hooks靠,react hooks无疑是给了我们巨大的启发,函数式编程会越来越普及,从远古时期的传统三大金刚html、css、script就能产出一个页面到现在的组件化,一个js即可是一个页面。


总结


函数式编程是趋势,但其实有很多老项目都是基于vue2.xoptions写法或者react class的写法还是居多,把这些项目迁移迭代到最新才是头疼的事情,当然选择适合现有项目的技术体系才是最重要的。


作者:饼干_
链接:https://juejin.cn/post/6991765115150270478

收起阅读 »

写给vue转react的同志们(3)

下一篇我们都知道vue上手比较容易是因为他的三标签写法以及对指令的封装,他更像一个做好的包子你直接吃。 相比react他的纯js写法,相对来说自由度更高,这也意味着很多东西你需要自己手动封装,所以对新手没那么友好,所以他更像面粉,但可以制作更多花样的食物。 今...
继续阅读 »

下一篇

我们都知道vue上手比较容易是因为他的三标签写法以及对指令的封装,他更像一个做好的包子你直接吃。


相比react他的纯js写法,相对来说自由度更高,这也意味着很多东西你需要自己手动封装,所以对新手没那么友好,所以他更像面粉,但可以制作更多花样的食物。


今天的主题

react 计算属性

react ref


react 计算属性


我们知道vue中有提供computed让我们来实现计算属性,只要依赖改变就会发生变化,那么react中是没有提供的,这里我们需要自己手动实现计算属性。简单举例一下:


vue 计算属性






react 计算属性(类写法)


class App extends React.Component {
constructor(props) {
super(props)
this.state = {
msg: 'hello react'
}
}
get react_computed() {
return this.state.msg
}
componentDidMount() {
setTimeout(() => {
this.setState({
msg: 'hello react change'
})
}, 2000)
}
render() {
return (

{ this.react_computed }

)
}

}


可以看到react中我们手动定义了get来让他获取msg的值,从而实现了计算属性,实际上vue中的computed也是基于get和set实现的,get中收集依赖,在set中派发更新。


react ref


vue中的ref使用起来也是非常简单在对应组件上标记即可获取组件的引用,那么react中呢?
react中当然也可以像vue一样使用,但官方并不推荐字符串的形式来使用ref,并且在react16.x后的版本移除了。


看一段大佬描述:



  • 它要求 React 跟踪当前呈现的组件(因为它无法猜测this)。这让 React 变慢了一点。

  • 它不像大多数人所期望的那样使用“渲染回调”模式(例如),因为 ref 会因为DataGrid上述原因而被放置。

  • 它不是可组合的,即如果一个库在传递的子组件上放置了一个引用,用户不能在它上面放置另一个引用。回调引用是完全可组合的。


举例:


vue ref






react ref


class App extends React.Component {
myRef = React.createRef()
constructor(props) {
super(props)
}
render() {
return (

// 正常使用

// 回调使用(可组合)
this['' + index]} />
// 调用api(react16.x)


)
}

}

vue中的ref我们不必多言,看看react的,官方更推荐第三种用法(react16.x),第二种用法在更新过程中会被执行两次,通过在外部定义箭头函数使用即可,但是大多情况都是无关紧要。第一种用法在react16.x后的版本被废弃了。


总结


都到这篇了,相信你转型react上手业务基本没问题了,后续将慢慢深入两大框架的对比,重点叙述react,vue辅之。


我是饼干,让我们一起成长。最后别忘记点赞关注收藏三连击🌟


作者:饼干_
链接:https://juejin.cn/post/6979061382415122462

收起阅读 »

写给vue转react的同志们(2)

下一篇react中想实现类似vue中的插槽 首先,我个人感觉jsx的写法比模板写法要灵活些,虽然没有像vue那样有指令,这就是为啥vue会上手简单点,因为他就像教科书一样教你怎么使用,而react纯靠你手写表达式来实现。 如果你想实现类似插槽的功能,其实大部分...
继续阅读 »

下一篇

react中想实现类似vue中的插槽


首先,我个人感觉jsx的写法比模板写法要灵活些,虽然没有像vue那样有指令,这就是为啥vue会上手简单点,因为他就像教科书一样教你怎么使用,而react纯靠你手写表达式来实现。


如果你想实现类似插槽的功能,其实大部分UI框架也可以是你自己定义的组件,例如ant desgin的组件,他的某些属性是可以传jsx来实现类似插槽的功能的,比如:


import React from 'react'
import { Popover } from 'antd'

class App extends React.Component {
constructor(props) {
super(props)
this.state = {
content: (

你好,这里是react插槽



)
}
}
render() {
const { content } = this.state
return (


悬浮


)
}
}

上面这样就可以实现类似插槽的功能,这点上确实是比vue灵活些,不需要在结构里在加入特定的插槽占位。
如果是vue的话就可能是这样:




大家可以自己写写demo去体会一下。


单向数据流与双向绑定


我们知道vue中通过发布订阅模式实现了响应式,把inputchange封装成v-model实现双向绑定,react则没有,需要你自己通过this.setState去实现,这点上我还是比较喜欢v-model能省不少事。


虽说单向数据流更清晰,但实际大部分人在开发中出现bug依旧要逐个去寻找某个属性值在哪些地方使用过,尤其是当表单项很多且校验多的时候,代码会比vue多不少,所以大家自行衡量,挑取合适自己或团队的技术栈才是最关键的,不要盲目追求所谓的新技术。


举个例子(简写):


react


import React from 'react'
import { Form, Input, Button } from 'antd'

const FormItem = Form.Item

class App extends React.Component {
constructor(props) {
super(props)
}

onChange(key, e) {
this.setState({
[key] : e
})
}

onClick = () => {
console.log('拿到了:',this.state.username)
}

render() {
return (





vue(简写)





其实乍一看也差不了多少,vue这种options的写法其实会比较清晰一点,react则需要你自己去划分功能区域。


css污染


vue中可以使用scoped来防止样式污染,react没有,需要用到.module.css,原理都是一样的,通过给类名添加一个唯一hash值来标识。


举个例子:


react(简写):


xxx.module.css

.xxx {
background-color: red;
}

xxx.css

.xxx {
background-color: blue;
}

xxx.jsx
import React from 'react'
import style form './xxx.module.css'
import './xxx.css'
class App extends React.Component {
render(){
return (


blue


red


)
}
}


vue


xxx.css
.xxx {
background-color: red;
}

xxx.vue

export default {
methods: {
click() {
this.$router.push('yyy')
}
}
}



yyy.vue

export default {
methods: {
click() {
this.$router.push('xxx')
}
}
}




上面只是简单举个例子,页面之间的样式污染主要是因为css默认是全局生效的,所以无论scoped也好或是module.css也好,都会解析成ast语法树,通过添加hash值生成唯一样式。


总结


无论vue也好,react也好不变的都是js,把js基础打牢才是硬道理。


作者:饼干_
链接:https://juejin.cn/post/6972099403213438984

收起阅读 »

写给vue转react的同志们(1)

学习一个框架最好的办法就是从业务做起。首先我们要弄清做业务需要什么知识点去支持 今天的主题:react 是怎么样传输数据的react 怎么封装组件react 的生命周期 实际上vue熟练的同学们,我觉得转react还是比较好上手的,就是要适应他的纯js的写法以...
继续阅读 »

学习一个框架最好的办法就是从业务做起。首先我们要弄清做业务需要什么知识点去支持


今天的主题:

react 是怎么样传输数据的

react 怎么封装组件

react 的生命周期


实际上vue熟练的同学们,我觉得转react还是比较好上手的,就是要适应他的纯js的写法以及jsx等,个人认为还是比较好接受的,其实基本上都一样,只要弄清楚数据怎么传输怎么处理,那剩下的jsx大家都会写了吧。


react 组件通讯


这里我们来跟vue对比一下。比如
在vue中父子组件传值(简写):


// 父组件
data: {
testText:'这是父值'
}
methods:{
receive(val) {
console.log('这是子值:', val)
}
}
<child :testText="testText" @childCallBack="receive"/>
// 子组件
props: {
testText: {
type: String,
default: ''
}
}
methods:{
handleOn(){
this.$emit('childCallBack', '我是子组件')
}
}
<template>
<div @click="handleOn">{{testText}}</div>
</template>

在react中父子组件传值:


// 父组件
export default class Father extends React.Component {
constructor(props) {
super(props)
this.state = {
testText: '这是父值'
}
receive = (val) => {
console.log('这是子值:', val)
}
render(){
return(
<div>
<Son childCallBack={this.receive} testText={testText}/>
</div>
)
}
}
}
// 子组件
export default class Son extends React.Component {
constructor(props) {
super(props)
}
render() {
const { testText } = this.props
return (
<div>
父组件传过来的testText:{testText}
<div onClick={this.receiveFather}>
点我从子传父
</div>
</div>
)
}
receiveFather = () => {
this.props.childCallBack('我是子组件')
}
}

可以看到react 和 vue 其实相差不大,都是通过props去进行父传子的通讯,然后通过一个事件把子组件的数据传给父组件。聪明的同学肯定注意到react里我用了箭头函数赋给了一个变量了。如果不这样写,this的指向是不确定的,也可以在标签上这样写this.receiveFather.bind(this),不过这样写的坏处就是对性能有影响,可以在constructor中一次绑定即可。但还是推荐箭头函数的写法。(封装组件其实跟这个八九不离十了,就不再叙述)


react 单向数据流


我们都知道vue里直接v-model 然后通过this.属性名就可以访问和修改属性了,这是vue劫持了get和set做了依赖收集和派发更新,但是react里没有这种东西,你不能直接通过this.state.属性名去修改值,需要通过this.setState({"属性名":"属性值"}, callback(回调函数)),你在同一地方修改属性是没办法立刻拿到修改后的属性值,需要通过setState的回调拿到。我还是比较喜欢vue的双向绑定(手动狗头)。


react 的生命周期


我们都知道vue的生命周期(create、mounted、update、destory),其实react也差不多,他们都是要把某个html的div替换并挂载渲染的。
列举比较常用的:


constructor()
constructor()中完成了React数据的初始化,它接受两个参数:props和context,当想在函数内部使用这两个参数时,需使用super()传入这两个参数。这个就当于定义初始数据,熟悉vue的同学你可以把他当成诸如data、methods等。
注意:只要使用了constructor()就必须写super(),否则会导致this指向错误。


componentWillMount()
componentWillMount()一般用的比较少,它更多的是在服务端渲染时使用。它代表的过程是组件已经经历了constructor()初始化数据后,但是还未渲染DOM时。这个相当于vue的created啦,vue中可以通过在这个阶段用$nextTick去操作dom(不建议),不知道react有没有类似的api呢?


componentDidMount()
组件第一次渲染完成,此时dom节点已经生成,可以在这里调用ajax请求,返回数据setState后组件会重新渲染,这个就相当于vue的mounted阶段啦。


componentWillUnmount ()
在此处完成组件的卸载和数据的销毁。这个相当于vue中的beforeDestory啦,clear你在组件中所有的setTimeout,setInterval,移除所有组建中的监听 removeEventListener。


componentWillUpdate (nextProps,nextState)
组件进入重新渲染的流程,这里可以拿到改变后的数据值(相当于vue中updated)。


componentDidUpdate(prevProps,prevState)
组件更新完毕后,react只会在第一次初始化成功会进入componentDidmount,之后每次重新渲染后都会进入这个生命周期,这里可以拿到prevProps和prevState,即更新前的props和state,(相当于vue中的beforeupdated)。


render()
render函数会插入jsx生成的dom结构,react会生成一份虚拟dom树,在每一次组件更新时,在此react会通过其diff算法比较更新前后的新旧DOM树,比较以后,找到最小的有差异的DOM节点,并重新渲染。这里就是你写页面的地方。


总结


小细节

react 中使用组件第一个字母需大写

react 万物皆可 props

mobx 很香🐶

react中没有指令(如v-if、v-for等)需自己写三目运算符或so on~



总结一下,从vue转react还是比较好上手的(react中还有函数式写法我没有说,感兴趣可以看看),个人认为弄清楚数据通讯以及生命周期对应的钩子使用场景等,其实基本就差不多啦。但是还有很多细节需要同学们在实际业务中去发现和解决。react只是框架,大家js基础还是要打好的。祝各位工作顺利,准时发薪。🐶

收起阅读 »

iOS Runtime (三)Runtime的消息机制

iOS
消息发送 消息机制就是向接收者发送消息,并带有参数,根据接收者对象的数据结构,找到相关发放实现,最后达到这个消息的目的。 objc_msgSend是Runtime的核心,Objective-C中调用对象方法就是消息传递。 objc_msgSend并不是直接调用...
继续阅读 »

消息发送


消息机制就是向接收者发送消息,并带有参数,根据接收者对象的数据结构,找到相关发放实现,最后达到这个消息的目的。


objc_msgSendRuntime的核心,Objective-C中调用对象方法就是消息传递。

objc_msgSend并不是直接调用方法实现(IMP)而是发送消息,让类的结构体去动态查到方法实现,所以在为查找到方法实现之前我们可以动态的去修改这个方法的实现


在Object-C中,我们其实可以直接调用C的代码也就是Runtime的C语言代码,需要添加message.h头文件。

#import 
#import

编写Runtime的时候会遇到没有提示的尴尬,那是因为在Xcode5.0以后的版本,Apple不建议我们写比较底层的代码,So,在target->info搜索msgYES改成NO,然后可以尽情的使用Runtime代码


Objc-msgSend所做的事情



1,找到方法的实现,由于通过单独的类以不同方式创建相同的方法,因此这个方法的实现的确定取决于接收消息的类对象,也即是说多个实例类对戏那个可以创建同样的方法,每个实例对象中的该方法都是独立存在的。

2,调用该方法实现,将接收消息类指针,以及该方法的参数传递给这个类。

3,最后将过程的返回值作为自己的返回值传递



消息传递的发送过程和关键要素



1,指向superclass的指针。消息发送给对象时,消息传递函数遵循对象的isa指针指向类结构的指针,在该结构中它查询结构体变量methodLists中的方法SEL(方法选择器).

2,会有一个SEL方法实现的地址(这个地址是基于独立的类)关联的表

    当创建一个新的对象时,分配内存,初始化变量,对象变量中的第一个是指向该类结构的指针,这个名字为isa的指针能让对象可以访问它的类,并通过该类访问它继承的所有类

    isa指针是对象使用Objective-C运行时系统所必需的,在结构中定义的任何字段中,对象需要与结构体objc_object(objc/objc.h中的定义)"等效",日常开发中很少有创见自己的根对象的这种情况,一般从NSObject或者NSProxy继承的对象会自动拥有isa变量

    如在isa指向的类结构中找不到SEL(方法选择器),Objc_msgSend会跟随指向Supercalss(父类)指针并再次尝试查找该SEL。如连续失败直到NSObject类,它的superclass也就是它自己本身。一旦找到SEL,该函数就会调用methodLists的方法并将接收对象的指针传给它。



加速消息发送




  • 1,有的时候在一个类会有继承关系,Objective-C中大部分对象都是继承于NSObject、自己自定义类,在这种继承体系当中有很多的方法,这些方法有可能不会用到,在向类发送消息的时候,去methodLists中查找无疑会拖慢程序的运行速度,所以Apple在开发的时候加入了缓存cache的概念,也就是缓存。

  • 2,在每个类中都会有一个单独的缓存cache,它可以包含继承过来的方法SEL以及自定义的SEL,在搜索methodLists之前,消息传递程序会检查接受者对象的告诉缓存cache,如果找到,就不会在去搜索庞大的methodLists列表,一旦在缓存当中存在你需要的SEL,这样以后也就比函数调用稍微慢一点。

  • 3,理论上cache缓存的是一些会再次调用的SEL,当写的程序预热足够时间,那么所有发送过的SEL都会在cache中找到

  • 4,cache会动态增长,容纳新的消息,知道程序中所有调用的SEL运行一遍为止

  • 5,原理时:好比是通常小圈子找人总比大圈子找人要快



Runtime的发送消息隐藏的参数


每次当我们向一个对象发送消息时,也就是Objective-C调用方法的时候,传递的所有参数,还包括两个隐藏的参数:



接收者对象

调用的方法SEL _cmd



这两个参数没有在定义中声明,而是在编译代码时插入方法实现的。

/*
* _cmd 就是你调用的方法的SEL
**/
NSLog(@"%@",NSStringFromSelector(_cmd));

规避动态绑定的方法,获取方法地址


代码正常编译的时候,需要使用消息传递Objc-msgSend才能找到方法的IMP中间就有了这个消息传递的过程。

有时候我们不希望调用消息传递的,或者节省消息传递的开销,就需要我们拿到方法的IMP,代码直接使用IMP中的方法。

下面的示例显示了如何调用实现setFilled:方法的过程:

@interface ViewController (){
NSInteger num;
}
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
void (*setter)(id, SEL, BOOL);
int i;

setter = (void (*)(id, SEL, BOOL))[self methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(self, @selector(setFilled:), YES);
}

- (void)setFilled:(NSInteger)number{
NSLog(@"%ld",++num);
}

传递给方法实现的前两个参数是: 接收对象(self)方法选择器对象(SEL),这些参数隐藏在方法的语法中,方法作为函数调用时必须使它显式化。


使用methodForSelector绕过动态绑定可以节省消息传递的大部分时间,在特定的消息多次重复的情况下才会节省的更加显著


methodForSelector是由Cocoa运行时系统提供,它并不是Objective-C语言本身的一个特性


3人点赞


链接:https://www.jianshu.com/p/04760fc66276
收起阅读 »

iOS Runtime (二) Runtime底层详解

iOS
Runtime的定义? 为了更好的认识类是怎么工作的,我们将要将一段Object-C的代码用clang看下底层的C/C++的写法。 在Object-C中的NSObject对象中@interface NSObject <NSObject> { ...
继续阅读 »

Runtime的定义?


为了更好的认识类是怎么工作的,我们将要将一段Object-C的代码用clang看下底层的C/C++的写法


在Object-C中的NSObject对象中

@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}

Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

由此可见可以看到id是指向objc_object的一个指针

objc_class结构体中的定义如下:

struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

runtime使用当中,我们经常需要用到的字段,它们的定义


isaClass对象,指向objc_class结构体的指针,也就是这个Class的MetaClass(元类)

类的实例对象的 isa 指向该类;该类的 isa 指向该类的MetaClassMetaCalssisa对象指向RootMetaCalss


super_class Class对象指向父类对象



  • 如果该类的对象已经是RootClass,那么这个super_class指向nil


  • MetaCalssSuperClass指向父类的MetaCalss


  • MetaCalssRootMetaCalss,那么该MetaClassSuperClass指向该对象的RootClass


ivars 类中所有 属性的列表,使用场景:我们在字典转换成模型的时候需要用到这个列表找到属性的名称,去取字典中的值,KVC赋值,或者直接Runtime赋值


methodLists 类中 所有的方法的列表,使用场景:如在程序中写好方法,通过外部获取到方法名称字符串,然后通过这个字符串得到方法,从而达到外部控制App已知方法。


cache 主要用于 缓存常用方法列表,每个类中有很多方法,我平时不用的方法也会在里面,每次运行一个方法,都要去 methodLists遍历得到方法,如果类的方法不多还行,但是基本的类中都会有很多方法,这样势必会影响程序的运行效率,所以 cache在这里就会被用上,当我们使用这个类的方法时先判断 cache是否为空,为空从 methodLists找到调用,并保存到 cache,不为空先从 cache中找方法,如果找不到在去 methodLists,这样提高了程序方法的运行效率。


protocols故名思义,这个类中都遵守了 哪些协议,使用场景:判断类是否遵守了某个协议上


在介绍runtime的时候,需要了解下类的本质。


类底层代码、类的本质?


为了更好的认识类是怎么工作的,我们将要将一段Object-C的代码用clang看下底层的C/C++的写法

typedef enum : NSUInteger {
ThisRPGGame = 0,
ThisActionGame = 1,
ThisBattleFlagGame = 2,
} ThisGameType;

@interface Game : NSObject
@property (copy,nonatomic)NSString *Name;
@property (assign,nonatomic)ThisGameType Type;
@end

@implementation Game
@synthesize Name,Type;

- (void)GiveThisGameName:(NSString *)name{
Name = name;
}

- (void)GiveThisGameType:(ThisGameType)type{
Type = type;
}
@end

使用命令,在当前文件夹中会出现Game.cpp的文件

# clang -rewrite-objc Game.m

由于生成的文件很庞大,可以仔细去研读,受益匪浅

/*
* 顾名思义存放property的结构体
* 当我们使用perproty的时候,会生成这样一个结构体
* 具体存储的数据为
* 实际内容:"Name","T@\"NSString\",C,N,VName"
* 原型:@property (copy,nonatomic)NSString *Name;
* 这个具体是怎么实现的,我会在后面继续深入研究,本文主要来理解runtime的理解
**/
struct _prop_t {
const char *name; //名字
const char *attributes; //属性
};

/*
*类中方法的结构体,cmd和imp的关系是一一对应的关系
*创建对象生成isa指针,指向这个对象的结构体时
*同时生成了一个表"Dispatch table"通过这个_cmd的编号找到对应方法
*使用场景:
*例如方法交换,方法判断。。。
**/
struct _objc_method {
struct objc_selector * _cmd; //SEL 对应着OC中的@selector()
const char *method_type; //方法的类型
void *_imp; //方法的地址
};


/*
* method_list_t 结构体:
* 原型:
* - (void)GiveThisGameName:(NSString *)name;
* 实际存储的方式:
* {(struct objc_selector *)"GiveThisGameName:", "v24@0:8@16", (void *)_I_Game_GiveThisGameName_}
* 其主要目的是存储一个数组,基本的数据类型是 _objc_method
* 扩展:当然这其中有你的属性,自动生成的setter、getter方法
**/

static struct _method_list_t {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[6];
}

/*
* 表示这个类中所遵守的协议对象
* 使用场景:
* 判断类是否遵守这个协议,从而动态添加、重写、交换某些方法,来达到某些目的
*
**/

struct _protocol_t {
void * isa; // NULL
const char *protocol_name;
const struct _protocol_list_t * protocol_list; // super protocols
const struct method_list_t *instance_methods; // 实例方法
const struct method_list_t *class_methods; //类方法
const struct method_list_t *optionalInstanceMethods; //可选的实例方法
const struct method_list_t *optionalClassMethods; //可选的类方法
const struct _prop_list_t * properties; //属性列表
const unsigned int size; // sizeof(struct _protocol_t)
const unsigned int flags; // = 0
const char ** extendedMethodTypes; //扩展的方法类型
};

/*
* 类的变量的结构体
* 原型:
* NSString *Name;
* 存储内容:
* {(unsigned long int *)&OBJC_IVAR_$_Game$Name, "Name", "@\"NSString\"", 3, 8}
* 根据存储内容可以大概了解这些属性的工作内容
**/
struct _ivar_t {
unsigned long int *offset; // pointer to ivar offset location
const char *name; //名字
const char *type; //属于什么变量
unsigned int alignment; //未知
unsigned int size; //大小
};


/*
* 这个就是类中的各种方法、属性、等等信息
* 底层也是一个结构体
* 名称、方法列表、协议列表、变量列表、layout、properties。。
*
**/
struct _class_ro_t {
unsigned int flags;
unsigned int instanceStart;
unsigned int instanceSize;
unsigned int reserved;
const unsigned char *ivarLayout; //布局
const char *name; //名字
const struct _method_list_t *baseMethods;//方法列表
const struct _objc_protocol_list *baseProtocols; //协议列表
const struct _ivar_list_t *ivars; //变量列表
const unsigned char *weakIvarLayout; //弱引用布局
const struct _prop_list_t *properties; //属性列表
};

/*
* 类本身
* oc在创建类的时候都会创建一个 _class_t的结构体
* 我的理解是在runtime中的object-class结构体在底层就会变成_class_t结构体
**/
struct _class_t {
struct _class_t *isa; //元类的指针
struct _class_t *superclass; //父类的指针
void *cache; //缓存
void *vtable; //表信息、未知
struct _class_ro_t *ro; //这个就是类中的各种方法、属性、等等信息
};


/*
* 类扩展的结构体
* 在OC中写的分类
**/
struct _category_t {
const char *name; //名称
struct _class_t *cls; //这个是哪个类的扩展
const struct _method_list_t *instance_methods; //实例方法列表
const struct _method_list_t *class_methods; //类方法列表
const struct _protocol_list_t *protocols; //协议列表
const struct _prop_list_t *properties; //属性列表
};

类就是多个结构体组合的一个集合体,类中的行为、习惯、属性抽象,按照机器能懂的数据存储到我们底层的结构体当中,在我们需要使用的时候直接获取使用。


那么就开始研究一下,类是如何使用,类的基本使用过程以及过程中runtime所做的事情。


类底层是如何调用方法?


了解了类的组成,那么类是通过什么样的形式去获取方法属性并得到应用?

在Object-C开发中我们经常会说到,对象调用方法,其本质就是想这个对象发送消息,为什么会有这么一说?下面我们来验证一下。

Object-C代码

int main(int argc, char * argv[]) {

Game *game = [Game alloc];
[game init];
[game Play];
return 0;
}

底层代码的实现

int main(int argc, char * argv[]) {

Game *game = ((Game *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Game"), sel_registerName("alloc"));
game = ((Game *(*)(id, SEL))(void *)objc_msgSend)((id)game, sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)game, sel_registerName("Play"));
return 0;
}

代码中使用了



objc_msgSend 消息发送

objc_getClass 获取对象

sel_registerName 获取方法的SEL



因为目前重点是objc_msgSend,其他的Runtime的方法会在后面继续一一道来, So 一个对象调用其方法,在`=Object-C中就是向这个对象发送一条消息,消息的格式

objc_msgSend("对象","SEL","参数"...)
objc_msgSend( id self, SEL op, ... )


收起阅读 »

iOS Runtime (一) 什么是Runtime?

iOS
一:Runtime是什么? 1,运行时(Runtime)是指将数据类型的确定由编译时推迟到了运行时。 2,Runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。 3,平时编写的OC代码,在程序运行过程中,其实最终会...
继续阅读 »

一:Runtime是什么?



1,运行时(Runtime)是指将数据类型的确定由编译时推迟到了运行时

2,Runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。

3,平时编写的OC代码,在程序运行过程中,其实最终会转换成Runtime的C语言代码,Runtime是Object-C的幕后工作者

4,Object-C需要Runtime来创建对象,进行消息发送转发



二:Runtime用在哪些地方?



1,在程序运行过程中,动态的创建类,动态添加、修改这个类的属性方法

2,遍历一个类中所有的成员变量、属性、以及所有方法

2,消息传递、转发



三:Runtime具体应用?



1,创建类,给类添加属性、方法

2,方法交换

3,获取对象的属性、私有属性

4,字典转换模型

5,KVC、KVO

6,归档(编码、解码)

7,NSClassFromString class<->字符串

8,block



常见用法


1,使用objc_allocateClassPair可在运行时创建新的类

2,使用class_addMethodclass_addIvar可向类中增加方法实例变量

3,最后使用objc_registerClassPair注册后,就可以使用此类了。

这体现了OC作为运行时语言的强大之一:在代码运行中动态创建并添加方法变量


a.使用objc_allocateClassPair创建一个类Class

const char * className = "Calculator";
Class kclass = objc_getClass(className);
if (!kclass)
{
Class superClass = [NSObject class];
kclass = objc_allocateClassPair(superClass, className, 0);
}


b.使用class_addIvar添加一个成员变量

  NSUInteger size;
NSUInteger alignment;
NSGetSizeAndAlignment("*", &size, &alignment);
class_addIvar(kclass, "expression", size, alignment, "*");

注:



1.type定义参考

2."*"星号代表字符( )iOS字符为4位,并采用4位对齐kclass



c.使用class_addMethod添加成员方法

    class_addMethod(kclass, @selector(setExpressionFormula:), (IMP)setExpressionFormula, "v@:@");
class_addMethod(kclass, @selector(getExpressionFormula), (IMP)getExpressionFormula, "@@:");

static void setExpressionFormula(id self, SEL cmd, id value)
{
NSLog(@"call setExpressionFormula");
}

static void getExpressionFormula(id self, SEL cmd)
{
NSLog(@"call getExpressionFormula");
}

注:



1.type定义参考

2."v@:@",解释v-返回值void类型,@-self指针id类型,:-SEL指针SEL类型,@-函数第一个参数为id类型。

3."@@:",解释@-返回值id类型,@-self指针id类型,:-SEL指针SEL类型。



d.注册到运行时环境

objc_registerClassPair(kclass);

e.实例化类

id instance = [[kclass alloc] init];

f.给变量赋值

object_setInstanceVariable(instance, "expression", "1+1"); 

g.获取变量值

void * value = NULL;
object_getInstanceVariable(instance, "expression", &value);

h.调用函数

[instance performSelector:@selector(getExpressionFormula)];

说明:objc_allocateClassPair函数的作用是创建一个新类newClass及其元类,三个参数依次为newClass的父类newClass的名称,第三个参数通常为0。然后可向newClass中添加变量及方法,注意若要添加类方法,需用objc_getClass(newClass)获取元类,然后向元类中添加类方法。接下来必须把newClass注册到运行时系统,否则系统是不能识别这个的。



链接:https://www.jianshu.com/p/e7586587ccf7
收起阅读 »

Android中Window 和 WindowManager

Window 是一个抽象类,具体实现是 PhoneWindow,通过 WindowManager 创建。WindowManager是外界访问Window的入口,Window 的具体实现位于 WindowManagerService 中WindowManager...
继续阅读 »

Window 是一个抽象类,具体实现是 PhoneWindow,通过 WindowManager 创建。

WindowManager是外界访问Window的入口,Window 的具体实现位于 WindowManagerService 中

WindowManager 和 WindowManagerService 的交互是一个 IPC 的过程

Andorid 中所有的视图都是通过 Window 来呈现的

不管是 Activity 、Dialog 还是 Taost

因此, Window 实际是 View 的直接管理者 !!!

1. Window 和 WindowManager

WindowManager 添加 Window 的过程

将一个 Button 添加到屏幕坐标为(100,,300)的位置上。其中,Flags 和 type 这两个参数比较重要。

mFloatingButton = new Button(this);
mFloatingButton.setText("button");
mLayoutParams = new WindowManager.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0 ,0 , PixelFormat.TRANSPARENT);
mLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL
   | LayoutParams.FLAG_NOT_FOCUSABLE
   | LayoutParams.FLAG_SHOW_WHEN_LOCKED
   
mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mLayoutParams.x = 100;
mLayoutParams.y =300;
mWindowManager.addView (mFloatignButton, mLayoutParams);

Flags 参数表示 Window 的属性

  • FLAG_NOT_FOCUSABLE

    表示Window 不需要获取焦点,也不需要接收各种输入事件,此标记会同时启用 FLAG_NOT_TOUCH_MODAL ,最终事件会直接传递给下层的具体焦点的 Window

  • FLAG_NOT_TOUCH_MODAL

    在此模式下,系统会将当前 Window 区域以外的单击事件传递给底层的 Window ,当前 Window 区域以内的单击事件则自己处理。这个标记很重要,一般来说都需要开启此标记,否则其他 Window 将无法收到单击事件。

  • FLAG_SHOW_WHEN_LOCKED

    开启此模式可以让 Window 显示在锁屏的界面上。

Type 参数表示 Window 的类型, 应用 Window 、子 Window 和 系统 Window

  • 应用 Window 对应着一个 Activity , 层级 1-99
  • 子Window 不能单独存在,需要依附在特定的父Window之中 , 层级 1000-1999
  • 系统Window 是需要声明权限在能创建的 Window, 比如 Toast 和 系统状态栏这些都是系统 Window , 2000-2999

    一般选用 TYPE_SYSTEM_OVERLAY 或者 TYPE_SYSTEM_ERROR

注: Window 是分层的,每个Window 都有对应的 z-ordered , 层级大的会覆盖在层级小的 Window 的上面。

WindowManager 所提供的功能很简单,常用方只有三个方法:

  • 添加 View
  • 更新 View
  • 删除View

这三个方法定义在 ViewManager 中,WindowManager 继承了 ViewManager

public interface ViewManager
{
public void addView(View view , ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}

拖动 Window 的效果

根据手指的位置来设定 LayoutParams 中 的 x 和 y 的值即可改变 Window 的位置。

首先给 View 设置 onTouchListener : mFloatingButton.setOnTouchListener(this)

然后在onTouch 方法中不断更新View 的位置即可。

public boolean onTouch(View v, MotionEvent envet){
int rawX = (int) event.getRawX();
int rawY = (int) event.getRwaY();
switch(event.getAction()){
case MotionEvent.ACTION_MOVE:{
mLayoutParams.x = rawX;
mLayoutParams.y = rawY;
mWindowManager.updateViewLayout(mFloatingButton, mLayoutParams);
break;
}
default:
break;
}
return false;
}

2. Window 的内部机制

Window 是一个抽象的概念,每一个Window 都对应着一个 View 和一个 ViewRootImpl 。

Window 和 View 通过 ViewRootImpl 来建立联系,因此Window 并不是实际存在的,它是以View 的形式存在。

View 才是 Window 存在的实体。

1. Window 的添加过程

Window 的添加过程需要通过 WindowManager 的 addView 来实现, WindowManager 是一个接口,它的真正实现是 WindowManagerImpl 类。


@Override
public void addView(View view, ViewGroup.LayoutParams params){
   mGlobal.addView(view, params , mDisplay, mParentWindow);
}

@Override
public void updateViewLayout(View view , ViewGroup.LayoutParams params){
   mGlobal.updateViewLayout(view ,params);
}

@Override
public void removeView(View view){
   mGlobal.removeView(view, false);
}

交给 WindowManagerGlobal 来处理

WindowManagerGlobal 以工厂的形式向外提供自己的实例。

WindowManagerGlobal 的 addView 方法主要分为如下步

  1. 检查参数是否合法,如果是子 Window 那么还需要调整一些布局参数

    if (view == null){
    throw new IllegalArgumentException("view must not be null");
    }
    if (display == null){
       throw new IllegalArgumentException("display must be null");
    }
    if(!(params instanceof WindowManager.LayoutParams)){
       throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
    }

    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
    if (parentWindow != null){
       parentWindow.adjustLayoutParamsForSubWindow(wparams);
    }
  2. 创建 ViewRootImpl 并将 View 添加到列表中

在 WindowManagerGlobal 内部有如下几个列表比较重要:

private final ArrayList<Viwe>mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingVies = new ArraySet<View>();
  • mViews 存储的是所有 Window 所对应的 View
  • mRoots 存储的是所有 Window 所对应的 ViewRootImpl
  • mParams 存储的是所有 Window 所对应的布局参数
  • mDyingViews 则存储了那些正在被删除的 View 对象。或者是那些已经调用 removeView 方法但是删除操作还未完成的 Window 对象。

在 addView 中通过如下方式将 Window 的一系列对象添加到列表中

root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);

mViews.add(view);
mRoot.add(root);
mParams.add(wparams);
  1. 通过 ViewRootImpl 来更新界面并完成 Window 的添加过程

    由ViewRootImpl 的 setView 方法来完成。View 的绘制过程是由 ViewRootImpl 来完成的。

    setView 内部通过 requestLayout 来完成异步刷新请求。

public void requestLayout(){
if (!mHandlingLayoutInLayoutRequest){
checkThread();
mLayoutRequested = true;
// 实际是VIEW 绘制的入口
scheduleTraversals();
}
}

接着会通过 WindowSession 最终来完成 Window 的添加过程。

在下面的代码中 mWindowSession 的类型是 IWindowSession , 它是一个 Binder 对象,真正实现类是 Session, 也就是 Window 的添加过程是一次IPC 调用

try{
   mOrigWindowType = mWindowAttributes.type;
   mAttachInfo.mRecomputeGlobalAttributes = true;
   collectViewAttributes();
   res =  mWindsSeesion.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(),mAttachInfo.mContentInsets, mInputChannel);
}catch (RemoteException e){
   mAdded = false;
   mView = null;
   mAttachInfo.mRootView = null;
   mInputChannel = null;
   mFallbackEventHandler.setView(null);
   unscheduleTraversals();
   setAccessibilityFocus(null, null);
   throw new RuntimeException("Adding window failed", e);
}

在 Session 内部会通过 WindowManagerService 来实现 Window 的添加

public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
                      int viewVisibility, int displayId, Rect outCotentInsets,
                      InputChannel outInputChannel){
   return mService.addWindow(this, window, seq, attrs, viewVisibility, disalayId, outContentInsets, outInputChannel);
}

如此一来,Window 的添加请求就给 WindowManagerService 去处理了。在WindowManagerService 内部会为每一个应用保留一个单独的 Session.

2. Window 的删除过程

先通过 WindowManagerImpl后,再进一步通过 WindowManagerGlobal 来实现的。

public void removeView(View view , boolean immediate){
if (view == null){
throw new IllegalArgumentException("view must not be null");
}

synchronized(mLock){
       // 先通过 findViewLocked 来查找待删除的 View 的索引,建立数组遍历
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
       // 调用removeViewLocked 来做进一步的删除
removeViewLocked(index, immediate);
if (curView == view){
return;
}
throw new IllegalStateExceotion("Calling with view " + view + "but the
ViewAncestor is attached to " + curView);
}
}
private void removeViewLocked(int index, boolean immediate){
   ViewRootImpl root = mRoots.get(index);
   View view = root.getView();
   
   if (view != null){
       InputMethodManager imm = InputMethodManager.getInstance();
       if ( imm != null){
           imm.windowDismissed(mViews.get(index).getWindowToken());
      }
  }
   boolean deferred = root.die(immediate);
   if(view != null){
       view.assignParent(null);
       if (deferred){
           mDyingViews.add(view);
      }
  }
}

removeViewLocked 是通过 ViewRootImpl 来完成删除操作。

WindowManager 中提供了两种删除接口 removeView 和 removeImmdiate

分别代表 异步删除和 同步删除

3. Window 的更新过程

由WindowManagerGlobal 的 updateViewLayout 方法实现。

4. Window 的创建过程

Activity 的 Window 是通过 PolicyManager 的一个工厂方法来创建的。

Activity 的视图是如何附属在 Window 上,由于Activity 的视图由setContentView 方法提供。

public void setContentView(int layoutResID){
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

这里可以看出,Activity 将具体实现交给了 Window 处理,而Window 的具体实现是 PhoneWindow .

这里主要看 PhoneWindow

  1. 如果没有 DecorCView ,那么就创建它

    DecorView 是一个 FrameLayout , 是Activity 中的顶级 View , 一般来说它的内部包含标题栏和内部栏。

    不管怎样,内容栏是一定要存在的,并且内容来具体固定的 id ,那就是 “ content" .完整的 id 是 android.R.content.

    DecorView 的创建过程由 installDecor 方法来完成,在方法内部会通过 generateDecor 来直接创建 DecorView ,这时其还是一个空白的FrameLayout:

    protected DecorView generateDecor(){
       return new DecorView(getContext(), -1);
    }

    为了初始化 DecorView 的结构,PhoneWindow 还需要通过 generateLayout 方法来加载具体的布局文件到 DecorView 中。

    View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in , new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) in;
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
// 该id 对应的ViewGroup 就是 mContentParent
public static final inT ID_ANDROID_CONTENT = com.android.internal.R.id.content
  1. 将View 添加到 DecorView 的 mContentParent 中
// 直接将 Activity 的视图添加到 Decorview 的 mContentParent 中
mLayoutInflater.inflate(layoutResID, mContentParent)
   
// Activity 的布局文件被添加到 DecorView 的 mContentParent中, setContentView
  1. 回调Activity 的 onContentChanged 方法通知 Activity 视图已经改变
final Callback cb =  getCallback();
if (cb != null && !isDestroyed()){
cb.onContentChanged();
}

在 makeVisible 方法中,DecorView 真正地完成了添加和显示这两个过程,此时 Activity 的视图才能被用户看到。

void makeVisible(){
if (!mWindowAdded){
ViewManager wm = getWindiowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisihility(View.VISIBLE);
}


在 makeVisible 方法中,DecorView 真正地完成了添加和显示这两个过程,此时 Activity 的视图才能被用户看到。

void makeVisible(){
if (!mWindowAdded){
ViewManager wm = getWindiowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisihility(View.VISIBLE);
}
收起阅读 »

一篇文章带你走近Android自定义view

前言从专科到本科,目前本科大四,已经是学习Android的第四个年头了,本打算积累一下冲23考研,但是最近被大佬洗脑后准备冲一冲22的考研,所以后续出文章的几率会很小,但是在前不久答应粉丝整理一个较为详细的Android自定义view教程,恰巧最近报名被华为选...
继续阅读 »

前言

从专科到本科,目前本科大四,已经是学习Android的第四个年头了,本打算积累一下冲23考研,但是最近被大佬洗脑后准备冲一冲22的考研,所以后续出文章的几率会很小,但是在前不久答应粉丝整理一个较为详细的Android自定义view教程,恰巧最近报名被华为选入2021年鸿蒙公开课的学生代表之一,在学校为请假条奔波的路上,所以抽出一下午写一篇文章。(有点小感冒,如发现错误请见谅,感谢指正!!!)。


下文为正文内容,所有链接案例注解都比较详细

一、为什么要自定义view

随着各大产品经理的内卷,Android系统内置的View早已无法满足我们的需求,我们需要针对自己的业务来定制我们需要的view,以达到更好的用户体验感,从而增加用户的黏性。

二、先看看一个超级简单的自定义view(三个构造函数)

需求:一个界面两个跑马灯(在xml中实现) 出现的问题:Textview在xml文件中实现跑马灯,如果有两个跑马灯,则会出现抢焦点的现象,只会跑一个。 解决方式:自定义一个Textview,设置其自动获得焦点: isFocused();

public class MyTextView extends TextView {
//在用代码创建的时候调用
public MyTextView(Context context) {
this(context, null);
}

//在识别XML的时候会调用此方法创建Textview,底层会用反射去AttribestSet去取属性值
public MyTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

//给第一个构造函数和第二个使用
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

//解决一个问题,需要Textview天生获取焦点
@Override
public boolean isFocused() {
return true;
}
}

从以上代码中,本人已将函数的作用写入到备注中。

三、了解手机的坐标系

4bdd208ec73144d1be51e84291c3651f_tplv-k3u1fbpfcp-watermark.webp

具体案例文章:Android用Canvas画一个真正能跑的跑马灯

四、使用Canvas画一个折线图(重写onDraw()方法)

此文章案例主要为canvas.drawLine(),drawText()的简单使用。

具体案例文章:Android用Canvas画一个折线图,并加以简单封装

五、如何自定义属性,且在view中获取到属性的值(小提,在六中会有案例)

以颜色为例。

//attrs文件
<attr name="leftcolor" format="reference|color"/>
<attr name="rightcolor" format="reference|color"/>
//java文件 ---TaiJiView为自定义view名称
//获取自定义属性。
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TaiJiView);
//获取颜色
int leftcolor = ta.getColor(R.styleable.TaiJiView_leftcolor, Color.BLACK);
int rightcolor=ta.getColor(R.styleable.TaiJiView_rightcolor, Color.WHITE);
//回收
ta.recycle();
//布局中
app:leftcolor="@color/colorPrimary"
app:rightcolor="#ff0000"

六、绘制图案以及加入设置简单的动画(案例讲解很详细)

canvas.drawCircle ,旋转动画

具体案例文章:Android自定义view之太极图

七、自定义view的实现分类以及自定义组合控件的案例

  • 自定义组合控件:将多个控件组合成为一个新的控件。(本案例)
  • 继承系统控件:如标题二的案例
  • 继承View:如标题六的案例
  • 继承ViewGroup:继承自LinearLayout等系统控件,在系统控件的基础功能上进行扩展。

具体案例文章:Android自定义view之模仿登录界面文本输入框(华为云APP)

八、简单测量以及自定义接口实例来控制动画的更新计算表达式(onMeasure,TypeEvaluator)

项目源码贴在链接文章末尾

具体案例文章:Android自定义view之围棋动画

九 、通过改变变量的值达到动画效果

Android自定义view之利用drawArc方法实现动态效果

Android自定义view之围棋动画(化繁为简)

Android自定义view之利用PathEffect实现动态效果

Android自定义view之线条等待动画(灵感来源:金铲铲之战)

小提:把绘制点移动到中间。代码看起来会简洁点

十、当界面更新频繁(SurfaceView)

讲讲Android为自定义view提供的SurfaceView

十一、GLSurfaceView(继承自SurfaceView,3D效果)

Android自定义view之3D正方体

如需继续深入还请了解openGL相关内容。

十二 、关于SVG

Android利用SVG实现动画效果 Android SVG动画详细例子

十三 、上一个简单github案例

Android线条等待动画JMWorkProgress(可添加依赖直接使用)

十四 、还没来得及具体写的(关键词)

贝塞尔曲线,事件分发机制。枚举(可在框架中用于确定动画状态)

十五 、两道面试相关八股(根据本人面试大厂整理)

1.View绘制流程

View的绘制是从 ViewRootImpl的 performTraversals()方法开始,从最顶层的 View(ViewGroup)开始逐层对每个 View进行绘制操作 。

View 绘制中主要流程分为measure,layout, draw 三个阶段。

measure :根据父 view 传递的 MeasureSpec 进行计算大小, 自定义View的过程中都会在onMeasure中进行宽高的测量,这个方法会从父布局中接收两个参数 widthMeasureSpac和 heightMeasureSpac,所以子布局的宽高大小需要受限于父布局。

layout :根据 measure 子 View 所得到的布局大小和布局参数,将子View放在合适的位置上, 结合源码可知 layout()会将四个位置参数传递给 setOpticalFrame()或者 setFrame(),而 setOpticalFrame()内部会调用 setFrame(),所以最终通过 setFrame()确定 View在 ViewGroup中的位置。位置确定完毕会调用 onLayout(l,t,r,b)对子View进行摆放。

draw :把 View 对象绘制到屏幕上。

  • Canvas:画布,不管是文字,图形,图片都要通过画布绘制而成
  • Paint:画笔,可设置颜色,粗细,大小,阴影等等等等,一般配合画布使用
  • Path:路径,用于形成一些不规则图形。
  • Matrix:矩阵,可实现对画布的几何变换。

2.View 的事件分发机制

触摸事件的类型

触摸事件对应的是 MotionEvent 类,事件的类型主要有如下三种:

  • ACTION_DOWN
  • ACTION_MOVE(移动的距离超过一定的阈值会被判定为 ACTION_MOVE 操作)
  • ACTION_UP

View 事件分发本质就是对 MotionEvent 事件分发的过程。即当一个 MotionEvent 发生后,系统将这个点击事件传递到一个具体的 View 上。

事件分发流程

事件分发过程由三个方法共同完成:

dispatchTouchEvent: 方法返回值为 true 表示事件被当前视图消费掉;返回为 super.dispatchTouchEvent 表示继续分发该事件,返回为 false 表示交给父类的 onTouchEvent 处理。

onInterceptTouchEvent: 方法返回值为 true 表示拦截这个事件并交由自身的 onTouchEvent 方法进行消费;返回 false 表示不拦截,需要继续传递给子视图。 如果 return super.onInterceptTouchEvent(ev), 事件拦截分两种情况:

  • 1.如果该View存在子View且点击到了该子View, 则不拦截, 继续分发 给 子 View 处理, 此时相当于 return false。

  • 2.如果该 View 没有子 View 或者有子 View 但是没有点击中子 View(此时 ViewGroup 相当于普通 View), 则交由该 View 的 onTouchEvent 响应,此时相当于 return true。

注意:一般的 LinearLayout、 RelativeLayout、FrameLayout 等 ViewGroup 默认不拦截, 而 ScrollView,ListView 等 ViewGroup 则可能拦截,得看具体情况。

onTouchEvent: 方法返回值为 true 表示当前视图可以处理对应的事件;返回值 为 false 表示当前视图不处理这个事件,它会被传递给父视图的 onTouchEvent 方法进行处理。如果 return super.onTouchEvent(ev),事件处理分为两种情况:

  • 1.如果该 View 是 clickable 或者 longclickable 的,则会返回 true, 表示消费 了该事件, 与返回 true 一样;

  • 2.如果该 View 不是 clickable 或者 longclickable 的,则会返回 false, 表示不 消费该事件,将会向上传递,与返回 false 一样。

注意:在 Android 系统中,拥有事件传递处理能力的类有以下三种:

  • Activity:拥有分发和消费两个方法。

  • ViewGroup:拥有分发、拦截和消费三个方法。

  • View:拥有分发、消费两个方法。


收起阅读 »

Retrofit流程极简解析

Retrofit流程极简解析以SandwichDemo为例子来解析。github地址创建Retrofitprivate val retrofit: Retrofit = Retrofit.Builder() .client(okHttpClient) .bas...
继续阅读 »

Retrofit流程极简解析

以SandwichDemo为例子来解析。github地址

创建Retrofit

  • private val retrofit: Retrofit = Retrofit.Builder()
    .client(okHttpClient)
    .baseUrl(
    "https://gist.githubusercontent.com/skydoves/aa3bbbf495b0fa91db8a9e89f34e4873/raw/a1a13d37027e8920412da5f00f6a89c5a3dbfb9a/"
    )
    .addConverterFactory(GsonConverterFactory.create())

    /* asynchronous supports */
    // .addCallAdapterFactory(DataSourceCallAdapterFactory.create())

    /* coroutines supports */
    .addCallAdapterFactory(CoroutinesResponseCallAdapterFactory.create())
    //.addCallAdapterFactory(CoroutinesDataSourceCallAdapterFactory.create())
    .build()

    创建接口类

    val disneyService: DisneyCoroutinesService = retrofit.create(DisneyCoroutinesService::class.java)

    获取接口返回的数据

    val apiResponse:ApiResponse<List<Poster>> = disneyService.fetchDisneyPosterList()

    就是这么简单,数据获取完成


细分流程解析

    1. 创建Retrofit。这里使用了创建者模式,通过Retrofit.Builder来创建Retrfofit实例,一般项目里都会做成单例
    2. Builder().client(OkHttpClient client)设置网络请求的最终调用者,这里和OkHttp是绝配
    3. baseUrl(Url baseUrl)设置baseUrl链接
    4. addConverterFactory(Converter.Factory factory)添加网络参数和返回类的转换器,例如Gson,Moshi
    5. addCallAdapterFactory(CallAdapter.Factory factory)添加接口请求结果的转换器
    6. build()方法中,会通过platform.defaultCallAdapterFactories(callbackExecutor)来添加默认的CallAdapter.Factory转换器和我们自定义的转换器。而ConvertorFactory转换器,默认加入new BuiltInConverters()和平台默认转换器platform.defaultConverterFactories()以及我们自定义的转换器
  • 通过Retrofit创建接口类 1.调用create(Class<T> 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 @Nullable 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);
    }
    args = args != null ? args : emptyArgs;
    return platform.isDefaultMethod(method)
    ? platform.invokeDefaultMethod(method, service, proxy, args)
    : loadServiceMethod(method).invoke(args);
    }
    });

    这里就是通过动态代理来。动态代理的理论网上很多,可以自己搜索;简单说下,就是比如代理的接口类,调用它的方法时候,会进入到动态代理类里InvocationHandlerinvoke()中,这里参数有method提供Method的各种方法,args参数提供方法的各个参数。这里就是完全代理了接口方法,来自己实现,这里思想多大,舞台就有多大。

  • invoke方法解析

    1. 解析loadServiceMethod(method).invoke(args)loadServiceMethod()方法返回ServiceMethod抽象类,实际是HttpServiceMethod类。
    2. 核心方法HttpServiceMethod.parseAnnotations方法调用并返回HttpServiceMethod类,这里是核心解析方法;上面的invoke(args)方法最终是调用了HttpServiceMethod类的invoke方法,最终是调用如下:
     @Override
    final @Nullable ReturnT invoke(Object[] args) {
    Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    return adapt(call, args);
    }

    这里记住这个类OkHttpCall 3. 解析HttpServiceMethod<ResponseT, ReturnT>.parseAnnotations()方法: 这里会通过RequestFactory来解析参数和返回值,其中 java if (Utils.getRawType(parameterType) == Continuation.class) { isKotlinSuspendFunction = true; return null; } 这个解析判断是否是suspend函数。 这里会根据是否挂起函数来确定不同的返回值。 继续:根据是否是挂起函数,来获取对应的adapterType,即类似Call<UserData>里的UserData类型,或者suspendUserData返回值类型。

    CallAdapter<ResponseT, ReturnT> callAdapter =
    createCallAdapter(retrofit, method, adapterType, annotations);
    Type responseType = callAdapter.responseType();

    这里通过返回类型,来匹配我们加入的CallAdapter来进行返回的Response的包装或者逻辑处理

    Converter<ResponseBody, ResponseT> responseConverter =
    createResponseConverter(retrofit, method, responseType);

    这里通过responseType来获取我们添加的返回结果转换器,比如GsonFactory,MothiFactory来 4.

        if (!isKotlinSuspendFunction) {
    return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
    } else if (continuationWantsResponse) {
    //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
    return (HttpServiceMethod<ResponseT, ReturnT>)
    new SuspendForResponse<>(
    requestFactory,
    callFactory,
    responseConverter,
    (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
    } else {
    //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
    return (HttpServiceMethod<ResponseT, ReturnT>)
    new SuspendForBody<>(
    requestFactory,
    callFactory,
    responseConverter,
    (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
    continuationBodyNullable);
    }

    这里会返回最终的各式各样的HttpMethod的实现类 如果非suspend函数,则直接返回CallAdapter这里,Java代码非协程一般都是这种情况; 如果是suspend函数且返回值为Response类型的,则返回SuspendForResponse 其余的suspend函数情况,则返回SuspendForBodykotlin+协程里一般是这种情况 5. 分析CallAdapterSuspendForBody的区别,最大区别,就是Suspend会再adapt里自动调用OkHttp的请求接口方法并返回对应的Response,而CallAdapter则不会,而是需要使用者自己去调用。

    至此,简略版的Retrofit流程已经梳理完毕

    我们自己可以自定义的部分:ConverterFactoryCallFactory这里官方都给了默认的和常用的,例如Converter转换类就有gson,guava,jackson,moshi,jaxb....;而默认的CallFactory,除了库里自带的默认的DefualtCallFactory,还有官方写的库:guava,java8,rxjava,rxjava2,rxjava3,scala,这里常用的是rxjava2,rxjava3,还有例如我现在用的Sandwich库里封装的CoroutinesResponseCallAdapterFactorykotlin协程配合起来非常好用

收起阅读 »

FLutter即时通讯

1. 即时通讯简述 即时通讯是端开发工作中常见的需求,本篇文章以作者工作中使用FLutter开发社交软件即时通讯需求为背景,描述一下即时通讯功能设计的要点。 2. 重要概念 即时通讯需要前后端配合,约定消息格式与消息内容。本次IM客户端需求开发使用了公司已有的...
继续阅读 »

1. 即时通讯简述


即时通讯是端开发工作中常见的需求,本篇文章以作者工作中使用FLutter开发社交软件即时通讯需求为背景,描述一下即时通讯功能设计的要点。


2. 重要概念


即时通讯需要前后端配合,约定消息格式与消息内容。本次IM客户端需求开发使用了公司已有的基于Socket.io搭建的后台,下文描述涉及到的一些概念。


2.1 WebSocket协议


WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket协议与传统的HTTP协议的主要区别为,WebSocket协议允许服务端主动向客户端推送数据,而传统的HTTP协议服务器只有在客户端主动请求之后才能向客户端发送数据。在没有WebSocket之前,即时通讯大部分采用长轮询方式。


2.2 Socket.io和WebSocket的区别


Socket.io不是WebSocket,它只是将WebSocket和轮询 (Polling)机制以及其它的实时通信方式封装成了通用的接口,并且在服务端实现了这些实时机制的相应代码。也就是说,WebSocket仅仅是Socket.io实现即时通信的一个子集。因此WebSocket客户端连接不上Socket.io服务端,当然Socket.io客户端也连接不上WebSocket服务端。


2.3 服务端socket消息


理解了服务端socket消息也就理解了服务器端的即时通讯逻辑,服务器发出的socket消息可以分为两种:




  1. 服务器主动发出的消息:


    例如,社交软件中的A用户给B用户发出了消息,服务器在收到A用户的消息后,通过socket链接,将A用户的消息转发给B用户,B用户客户端接收到的消息就属于服务器主动发出的。其他比较常见的场景例如直播软件中,全平台用户都会收到的礼物消息广播。




  2. 服务器在接收到客户端消息后的返回消息:


    例如,长链接心跳机制,客户端向服务器发送ping消息,服务器在成功接受客户端的ping消息后返回的pong消息就属于服务器的返回消息。其他常见的场景如社交软件中A用户给B用户发出了消息,服务器在收到A用户的消息后,给A客户端返回一条消息,供A客户端了解消息的发送状态,判断发送是否成功。大部分场景,服务器在接收到客户端主动发出的消息之后都需要返回一条消息。




3. 客户端实现流程


几个设计客户端即时通讯的重点。


3.1 心跳机制


所谓心跳就是客户端发出ping消息,服务器成功收到后返回pong消息。当客户端一段时间内不在发送ping消息,视为客户端断开,服务器就会主动关闭socket链接。当客户端发送ping消息,服务器一段时间内没有返回pong消息,视为服务器断开,客户端就会启动重连机制。


启动流程


3.2 重连机制


重连机制为客户端重新发起连接,常见的重连条件如下:



  • 客户端发送ping消息,服务器一段时间内没有返回pong。

  • 客户端网络断开。

  • 服务器主动断开连接。

  • 客户端主动连接失败。


当出现极端情况(客户端断网)时,频繁的重连可能会导致资源的浪费,可以设置一段时间内的最大重连次数,当重连超过一定次数时,休眠一段时间。


3.3 消息发送流程



  1. 将消息存储到本地数据库,发送状态设为等待。

  2. 发送socket消息。

  3. 接收到服务器返回的socket消息后,将本地数据库等待状态的消息改为成功。


注意事项:


将消息存储到本地数据库时需要生成一个id存入数据库,同时传给服务器,当收到消息时根据id判断更新本地数据库的哪一条消息。


3.4 消息接收流程



3.5 其他相关



  • 聊天页消息的排序:在查询本地数据库时使用order by按时间排序。

  • 消息列表:也推荐做本地存储,当收到消息的时候需要先判断本地消息列表是否有当前消息用户的对话框,如果没有就先插入,有就更新。消息列表的维护就不展开说了,感兴趣可以看代码。

  • 图片语音消息:将图片和语言先上传到专门的服务器上(各种专门的云存储服务器),sokcet消息和本地存储传递的是云服务器上的URL。

  • 多人聊天(群聊):与单人聊天逻辑基本一致,区别位本地数据库需要添加一个会话ID字段,打开一个群就查询对应会话ID的数据。聊天消息不再是谁发给谁,而是在哪个群聊下。


4. 客户端Flutter代码


把部分代码贴上来,完整项目在作者的github上。


4.1 心跳机制


  heart() {
pingTimer = Timer.periodic(Duration(seconds: 30), (data) {
if (pingWaitTime >= 60) {
socket.connect();
pingWaitTime = 0;
pingWaitTimer!.cancel();
ping();
}
if (!pingWaitFlag) ping();
});
}

ping() {
debugPrint("ping");
String pingData =
'{"type":"ping","payload":{"front":true},"msg_id":${DateTime.now().millisecondsSinceEpoch}}';
socket.emit("message", pingData);
pingWaitFlag = true;
pingWaitTime = 0;
pingWaitTimer = Timer.periodic(Duration(seconds: 1), (data) {
pingWaitTime++;
print(data.hashCode);
if (pingWaitTime % 10 == 0) debugPrint(pingWaitTime.toString());
});
}
//pong
if (socketMessage.type == PONG && socketMessage.code == 1000) {
pingWaitFlag = false;
pingWaitTimer!.cancel();
pingWaitTime = 0;
}

4.2 本地数据库设计


数据库表的设计是比较重要的,理解了数据库设计,读代码也就无压力了。


      //消息表
CREATE TABLE chatDetail (
chat_id TEXT PRIMARY KEY,//主键
from_id TEXT,//发送人
to_id TEXT,//接收人
created_at TEXT,
content TEXT,//消息内容
image TEXT,//UI展示用,用户头像
name TEXT,//UI展示用,用户名
sex TEXT,//UI展示用,用户性别
status TEXT,//消息状态
type INTEGER,//消息类型,图片/文字/语音等
chat_object_id TEXT//聊天对象ID,对当前用户而言的聊天对象,是一系列本地操作的核心
)
//消息列表表
CREATE TABLE chatList (
cov_id TEXT,
unread_count INTEGER,
last_msg_text TEXT,
last_msg_at TEXT,
image TEXT,
name TEXT,
sex TEXT,
chat_object_id TEXT PRIMARY KEY)

5. 总结


无论是Flutter技术,或是IOS/Android/Web。只要掌握了即时通讯的核心开发流程,不同的技术只是API有些变化。API往往看文档就能解决,大前端或是特定平台的工程师还是要掌握核心开发流程,会几种做同样事情的API意义不大。


demo写的比较简单,有问题可以评论。


项目github地址


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

看动画学算法之:平衡二叉搜索树AVL Tree

简介 平衡二叉搜索树是一种特殊的二叉搜索树。为什么会有平衡二叉搜索树呢? 考虑一下二叉搜索树的特殊情况,如果一个二叉搜索树所有的节点都是右节点,那么这个二叉搜索树将会退化成为链表。从而导致搜索的时间复杂度变为O(n),其中n是二叉搜索树的节点个数。 而平衡二叉...
继续阅读 »

简介


平衡二叉搜索树是一种特殊的二叉搜索树。为什么会有平衡二叉搜索树呢?


考虑一下二叉搜索树的特殊情况,如果一个二叉搜索树所有的节点都是右节点,那么这个二叉搜索树将会退化成为链表。从而导致搜索的时间复杂度变为O(n),其中n是二叉搜索树的节点个数。


而平衡二叉搜索树正是为了解决这个问题而产生的,它通过限制树的高度,从而将时间复杂度降低为O(logn)。


AVL的特性


在讨论AVL的特性之前,我们先介绍一个概念叫做平衡因子,平衡因子表示的是左子树和右子树的高度差。


如果平衡因子=0,表示这是一个完全平衡二叉树。


如果平衡因子=1,那么这棵树就是平衡二叉树AVL。


也就是是说AVL的平衡因子不能够大于1。


先看一个AVL的例子:



总结一下,AVL首先是一个二叉搜索树,然后又是一个二叉平衡树。


AVL的构建


有了AVL的特性之后,我们看下AVL是怎么构建的。


public class AVLTree {

//根节点
Node root;

class Node {
int data; //节点的数据
int height; //节点的高度
Node left;
Node right;

public Node(int data) {
this.data = data;
left = right = null;
}
}

同样的,AVL也是由各个节点构成的,每个节点拥有data,left和right几个属性。


因为是二叉平衡树,节点是否平衡还跟节点的高度有关,所以我们还需要定义一个height作为节点的高度。


在来两个辅助的方法,一个是获取给定的节点高度:


//获取给定节点的高度
int height(Node node) {
if (node == null)
return 0;
return node.height;
}

和获取平衡因子:


//获取平衡因子
int getBalance(Node node) {
if (node == null)
return 0;
return height(node.left) - height(node.right);
}

AVL的搜索


AVL的搜索和二叉搜索树的搜索方式是一致的。


先看一个直观的例子,怎么在AVL中搜索到7这个节点:



搜索的基本步骤是:



  1. 从根节点15出发,比较根节点和搜索值的大小

  2. 如果搜索值小于节点值,那么递归搜索左侧树

  3. 如果搜索值大于节点值,那么递归搜索右侧树

  4. 如果节点匹配,则直接返回即可。


相应的java代码如下:


//搜索方法,默认从根节点搜索
public Node search(int data){
return search(root,data);
}

//递归搜索节点
private Node search(Node node, int data)
{
// 如果节点匹配,则返回节点
if (node==null || node.data==data)
return node;

// 节点数据大于要搜索的数据,则继续搜索左边节点
if (node.data > data)
return search(node.left, data);

// 如果节点数据小于要搜素的数据,则继续搜索右边节点
return search(node.right, data);
}

AVL的插入


AVL的插入和BST的插入是一样的,不过插入之后有可能会导致树不再平衡,所以我们需要做一个再平衡的步骤。


看一个直观的动画:



插入的逻辑是这样的:



  1. 从根节点出发,比较节点数据和要插入的数据

  2. 如果要插入的数据小于节点数据,则递归左子树插入

  3. 如果要插入的数据大于节点数据,则递归右子树插入

  4. 如果根节点为空,则插入当前数据作为根节点


插入数据之后,我们需要做再平衡。


再平衡的逻辑是这样的:



  1. 从插入的节点向上找出第一个未平衡的节点,这个节点我们记为z

  2. 对z为根节点的子树进行旋转,得到一个平衡树。


根据以z为根节点的树的不同,我们有四种旋转方式:



  • left-left:



如果是left left的树,那么进行一次右旋就够了。


右旋的步骤是怎么样的呢?



  1. 找到z节点的左节点y

  2. 将y作为旋转后的根节点

  3. z作为y的右节点

  4. y的右节点作为z的左节点

  5. 更新z的高度


相应的代码如下:


Node rightRotate(Node node) {
Node x = node.left;
Node y = x.right;

// 右旋
x.right = node;
node.left = y;

// 更新node和x的高度
node.height = max(height(node.left), height(node.right)) + 1;
x.height = max(height(x.left), height(x.right)) + 1;

// 返回新的x节点
return x;
}


  • right-right:


如果是right-right形式的树,需要经过一次左旋:



左旋的步骤正好和右旋的步骤相反:



  1. 找到z节点的右节点y

  2. 将y作为旋转后的根节点

  3. z作为y的左节点

  4. y的左节点作为z的右节点

  5. 更新z的高度


相应的代码如下:


//左旋
Node leftRotate(Node node) {
Node x = node.right;
Node y = x.left;

//左旋操作
x.left = node;
node.right = y;

// 更新node和x的高度
node.height = max(height(node.left), height(node.right)) + 1;
x.height = max(height(x.left), height(x.right)) + 1;

// 返回新的x节点
return x;
}


  • left-right:



如果是left right的情况,需要先进行一次左旋将树转变成left left格式,然后再进行一次右旋,得到最终结果。



  • right-left:



如果是right left格式,需要先进行一次右旋,转换成为right right格式,然后再进行一次左旋即可。


现在问题来了,怎么判断一个树到底是哪种格式呢?我们可以通过获取平衡因子和新插入的数据比较来判断:




  1. 如果balance>1,那么我们在Left Left或者left Right的情况,这时候我们需要比较新插入的data和node.left.data的大小


    如果data < node.left.data,表示是left left的情况,只需要一次右旋即可


    如果data > node.left.data,表示是left right的情况,则需要将node.left进行一次左旋,然后将node进行一次右旋




  2. 如果balance<-1,那么我们在Right Right或者Right Left的情况,这时候我们需要比较新插入的data和node.right.data的大小
    如果data > node.right.data,表示是Right Right的情况,只需要一次左旋即可


    如果data < node.left.data,表示是Right left的情况,则需要将node.right进行一次右旋,然后将node进行一次左旋




插入节点的最终代码如下:


//插入新节点,从root开始
public void insert(int data){
root=insert(root, data);
}

//遍历插入新节点
Node insert(Node node, int data) {

//先按照普通的BST方法插入节点
if (node == null)
return (new Node(data));

if (data < node.data)
node.left = insert(node.left, data);
else if (data > node.data)
node.right = insert(node.right, data);
else
return node;

//更新节点的高度
node.height = max(height(node.left), height(node.right)) + 1;

//判断节点是否平衡
int balance = getBalance(node);

//节点不平衡有四种情况
//1.如果balance>1,那么我们在Left Left或者left Right的情况,这时候我们需要比较新插入的data和node.left.data的大小
//如果data < node.left.data,表示是left left的情况,只需要一次右旋即可
//如果data > node.left.data,表示是left right的情况,则需要将node.left进行一次左旋,然后将node进行一次右旋
//2.如果balance<-1,那么我们在Right Right或者Right Left的情况,这时候我们需要比较新插入的data和node.right.data的大小
//如果data > node.right.data,表示是Right Right的情况,只需要一次左旋即可
//如果data < node.left.data,表示是Right left的情况,则需要将node.right进行一次右旋,然后将node进行一次左旋

//left left
if (balance > 1 && data < node.left.data)
return rightRotate(node);

// Right Right
if (balance < -1 && data > node.right.data)
return leftRotate(node);

// Left Right
if (balance > 1 && data > node.left.data) {
node.left = leftRotate(node.left);
return rightRotate(node);
}

// Right Left
if (balance < -1 && data < node.right.data) {
node.right = rightRotate(node.right);
return leftRotate(node);
}

//返回插入后的节点
return node;
}

AVL的删除


AVL的删除和插入类似。


首先按照普通的BST删除,然后也需要做再平衡。


看一个直观的动画:



删除之后,节点再平衡也有4种情况:




  1. 如果balance>1,那么我们在Left Left或者left Right的情况,这时候我们需要比较左节点的平衡因子


    如果左节点的平衡因子>=0,表示是left left的情况,只需要一次右旋即可


    如果左节点的平衡因<0,表示是left right的情况,则需要将node.left进行一次左旋,然后将node进行一次右旋




  2. 如果balance<-1,那么我们在Right Right或者Right Left的情况,这时候我们需要比较右节点的平衡因子


    如果右节点的平衡因子<=0,表示是Right Right的情况,只需要一次左旋即可


    如果右节点的平衡因子>0,表示是Right left的情况,则需要将node.right进行一次右旋,然后将node进行一次左旋




相应的代码如下:


Node delete(Node node, int data)
{
//Step 1. 普通BST节点删除
// 如果节点为空,直接返回
if (node == null)
return node;

// 如果值小于当前节点,那么继续左节点删除
if (data < node.data)
node.left = delete(node.left, data);

//如果值大于当前节点,那么继续右节点删除
else if (data > node.data)
node.right = delete(node.right, data);

//如果值相同,那么就是要删除的节点
else
{
// 如果是单边节点的情况
if ((node.left == null) || (node.right == null))
{
Node temp = null;
if (temp == node.left)
temp = node.right;
else
temp = node.left;

//没有子节点的情况
if (temp == null)
{
node = null;
}
else // 单边节点的情况
node = temp;
}
else
{ //非单边节点的情况
//拿到右侧节点的最小值
Node temp = minValueNode(node.right);
//将最小值作为当前的节点值
node.data = temp.data;
// 将该值从右侧节点删除
node.right = delete(node.right, temp.data);
}
}

// 如果节点为空,直接返回
if (node == null)
return node;

// step 2: 更新当前节点的高度
node.height = max(height(node.left), height(node.right)) + 1;

// step 3: 获取当前节点的平衡因子
int balance = getBalance(node);

// 如果节点不再平衡,那么有4种情况
//1.如果balance>1,那么我们在Left Left或者left Right的情况,这时候我们需要比较左节点的平衡因子
//如果左节点的平衡因子>=0,表示是left left的情况,只需要一次右旋即可
//如果左节点的平衡因<0,表示是left right的情况,则需要将node.left进行一次左旋,然后将node进行一次右旋
//2.如果balance<-1,那么我们在Right Right或者Right Left的情况,这时候我们需要比较右节点的平衡因子
//如果右节点的平衡因子<=0,表示是Right Right的情况,只需要一次左旋即可
//如果右节点的平衡因子>0,表示是Right left的情况,则需要将node.right进行一次右旋,然后将node进行一次左旋
// Left Left Case
if (balance > 1 && getBalance(node.left) >= 0)
return rightRotate(node);

// Left Right Case
if (balance > 1 && getBalance(node.left) < 0)
{
node.left = leftRotate(node.left);
return rightRotate(node);
}

// Right Right Case
if (balance < -1 && getBalance(node.right) <= 0)
return leftRotate(node);

// Right Left Case
if (balance < -1 && getBalance(node.right) > 0)
{
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}

本文的代码地址:


learn-algorithm


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

密码学系列之:加密货币中的scrypt算法

简介 为了抵御密码破解,科学家们想出了很多种方法,比如对密码进行混淆加盐操作,对密码进行模式变换和组合。但是这些算法逐渐被一些特制的ASIC处理器打败,这些ASIC处理器不做别的,就是专门来破解你的密码或者进行hash运算。 最有名的当然是比特币了,它使用的是...
继续阅读 »

简介


为了抵御密码破解,科学家们想出了很多种方法,比如对密码进行混淆加盐操作,对密码进行模式变换和组合。但是这些算法逐渐被一些特制的ASIC处理器打败,这些ASIC处理器不做别的,就是专门来破解你的密码或者进行hash运算。


最有名的当然是比特币了,它使用的是为人诟病的POW算法,谁的算力高,谁就可以挖矿,这样就导致了大量无意义的矿机的产生,这些矿机什么都不能干,就算是用来算hash值。结果浪费了大量的电力。


普通人更是别想加入这个只有巨头才能拥有的赛道,如果你想用一个普通的PC机来挖矿,那么我估计你挖到矿的几率可能跟被陨石砸中差不多。


为了抵御这种CPU为主的密码加密方式,科学家们发明了很多其他的算法,比如需要占用大量内存的算法,因为内存不像CPU可以疯狂提速,所以限制了很多暴力破解的场景,今天要将的scrypt算法就是其中一种,该算法被应用到很多新的加密货币挖矿体系中,用以表示他们挖矿程序的公平性。


scrypt算法


scrypt是一种密码衍生算法,它是由Colin Percival创建的。使用scrypt算法来生成衍生key,需要用到大量的内存。scrypt算法在2016年作为RFC 7914标准发布。


密码衍生算法主要作用就是根据初始化的主密码来生成系列的衍生密码。这种算法主要是为了抵御暴力破解的攻击。通过增加密码生成的复杂度,同时也增加了暴力破解的难度。


但是和上面提到的原因一样,之前的password-based KDF,比如PBKDF2虽然提高了密码生成的遍历次数,但是它使用了很少的内存空间。所以很容易被简单的ASIC机器破解。scrypt算法就是为了解决这样的问题出现的。


scrypt算法详解


scrypt算法会生成非常大的伪随机数序列,这个随机数序列会被用在后续的key生成过程中,所以一般来说需要一个RAM来进行存储。这就是scrypt算法需要大内存的原因。


接下我们详细分析一下scrypt算法,标准的Scrypt算法需要输入8个参数,如下所示:



  • Passphrase: 要被hash的输入密码

  • Salt: 对密码保护的盐,防止彩虹表攻击

  • CostFactor (N): CPU/memory cost 参数,必须是2的指数(比如: 1024)

  • BlockSizeFactor (r): blocksize 参数

  • ParallelizationFactor (p): 并行参数

  • DesiredKeyLen (dkLen): 输出的衍生的key的长度

  • hLen: hash函数的输出长度

  • MFlen: Mix函数的输出长度


这个函数的输出就是DerivedKey。


首先我们需要生成一个expensiveSalt。首先得到blockSize:


blockSize = 128*BlockSizeFactor 

然后使用PBKDF2生成p个blockSize,将这p个block组合成一个数组:


[B0...Bp−1] = PBKDF2HMAC-SHA256(Passphrase, Salt, 1, blockSize*ParallelizationFactor)

使用ROMix对得到的block进行混合:


   for i ← 0 to p-1 do
Bi ← ROMix(Bi, CostFactor)

将B组合成新的expensiveSalt:


expensiveSalt ← B0∥B1∥B2∥ ... ∥Bp-1

接下来使用PBKDF2和新的salt生成最终的衍生key:


return PBKDF2HMAC-SHA256(Passphrase, expensiveSalt, 1, DesiredKeyLen);

下面是ROMix函数的伪代码:


Function ROMix(Block, Iterations)

Create Iterations copies of X
X ← Block
for i ← 0 to Iterations−1 do
Vi ← X
X ← BlockMix(X)

for i ← 0 to Iterations−1 do
j ← Integerify(X) mod Iterations
X ← BlockMix(X xor Vj)

return X

其中BlockMix的伪代码如下:


Function BlockMix(B):

The block B is r 128-byte chunks (which is equivalent of 2r 64-byte chunks)
r ← Length(B) / 128;

Treat B as an array of 2r 64-byte chunks
[B0...B2r-1] ← B

X ← B2r−1
for i ← 0 to 2r−1 do
X ← Salsa20/8(X xor Bi) // Salsa20/8 hashes from 64-bytes to 64-bytes
Yi ← X

return ← Y0∥Y2∥...∥Y2r−2 ∥ Y1∥Y3∥...∥Y2r−1

scrypt的使用


Scrypt被用在很多新的POW的虚拟货币中,比如Tenebrix、 Litecoin 和 Dogecoin。感兴趣的朋友可以关注一下。


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

java流太太太..............好用了

情景:一个集合对象list,现在想获取这个集合中每个对象的id,并将这些id值存放在另一个集合中,方便我去查询数据。如果是你来实现这个需求,你会用什么方法去实现呢。 我猜会有许多人会选择循环变量这个集合对象,取出id存放在集合里面,代码是这样的: List&...
继续阅读 »
  • 情景:一个集合对象list,现在想获取这个集合中每个对象的id,并将这些id值存放在另一个集合中,方便我去查询数据。如果是你来实现这个需求,你会用什么方法去实现呢。


我猜会有许多人会选择循环变量这个集合对象,取出id存放在集合里面,代码是这样的:


List<Clazz> list = clazzes;
List<Long> ids = new ArrayList();
for (Clazz clazz : list) {
ids.add(clazz.getId());
}

但是!实际上,这个需求可以只用一行代码就可以解决,那是用的什么呢?“流”请看代码:


List<Clazz> list = clazzes;
List<Long> collect = list.stream().map(Clazz::getId).collect(Collectors.toList());

使用流一行代码就可以解决关键看着清晰明了。
上面list.stream().map(Clazz::getId).collect(Collectors.toList())这一行代码用了JAVA8 的两个新特性



  • 双冒号 双冒号就是把方法当作参数传递给需要的方法,或者是传递到stream()中去。在这里就是将其传到stream中去其语法格式 类名::方法名

  • stream 流 通过Collectors 类将流转换成集合元素 流的操作还有许多,可以参考搜索网络


再分享一下 最近根据echart图来查询数据,我在写查询语句筛选条件使用了大量的stream流,发现使用stream流是真的舒服。


我先描述我最近的一个接口:这个接口需要展示四个饼图。而四个饼图是:1.男女教师占比;2.各年龄段占比 3.学历占比,4.职称统计
我想在一个接口中完成这个四个的查询 我的思路有几个:


1.是写多个查询语句 需要一个查询一个(但是各种筛选条件下来 很麻烦)


2.利用视图 可以用来多次调用(但是在查询中会存在in操作 觉得麻烦)


3.利用stream流 根据筛选条件查出符合的教师信息 对每一个操作进行筛选


 通过各种筛选条件查出的结果: teacherList (集合类型)
Long count1 = teacherList.stream().filter(e -> e.getGender().equals(0)).count(); //男生数量
Long count2 = teacherList.stream().filter(e -> e.getGender().equals(1)).count(); //女生数量

通过这样可以直接算出数量 而不用去便利算数据


而更多详细的stream流的信息可以去网上搜索学习


我对stream流的学习还在表面 还有许多灵活的用法我还需要继续学习 欢迎大佬指导!


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

为什么需要Java内存模型?

面试官:今天想跟你聊聊Java内存模型,这块你了解过吗? 候选者:嗯,我简单说下我的理解吧。那我就从为什么要有Java内存模型开始讲起吧 面试官:开始你的表演吧。 候选者:那我先说下背景吧 候选者:1. 现有计算机往往是多核的,每个核心下会有高速缓存。高速缓存...
继续阅读 »

面试官今天想跟你聊聊Java内存模型,这块你了解过吗?


候选者:嗯,我简单说下我的理解吧。那我就从为什么要有Java内存模型开始讲起吧


面试官:开始你的表演吧。


候选者:那我先说下背景吧


候选者:1. 现有计算机往往是多核的,每个核心下会有高速缓存。高速缓存的诞生是由于「CPU与内存(主存)的速度存在差异」,L1和L2缓存一般是「每个核心独占」一份的。


候选者:2. 为了让CPU提高运算效率,处理器可能会对输入的代码进行「乱序执行」,也就是所谓的「指令重排序」


候选者:3. 一次对数值的修改操作往往是非原子性的(比如i++实际上在计算机执行时就会分成多个指令)


候选者:在永远单线程下,上面所讲的均不会存在什么问题,因为单线程意味着无并发。并且在单线程下,编译器/runtime/处理器都必须遵守as-if-serial语义,遵守as-if-serial意味着它们不会对「数据依赖关系的操作」做重排序。



候选者:CPU为了效率,有了高速缓存、有了指令重排序等等,整块架构都变得复杂了。我们写的程序肯定也想要「充分」利用CPU的资源啊!于是乎,我们使用起了多线程


候选者:多线程在意味着并发,并发就意味着我们需要考虑线程安全问题


候选者:1. 缓存数据不一致:多个线程同时修改「共享变量」,CPU核心下的高速缓存是「不共享」的,那多个cache与内存之间的数据同步该怎么做?


候选者:2. CPU指令重排序在多线程下会导致代码在非预期下执行,最终会导致结果存在错误的情况。



候选者:针对于「缓存不一致」问题,CPU也有其解决办法,常被大家所认识的有两种:


候选者:1.使用「总线锁」:某个核心在修改数据的过程中,其他核心均无法修改内存中的数据。(类似于独占内存的概念,只要有CPU在修改,那别的CPU就得等待当前CPU释放)


候选者:2.缓存一致性协议(MESI协议,其实协议有很多,只是举个大家都可能见过的)。MESI拆开英文是(Modified (修改状态)、Exclusive (独占状态)、Share(共享状态)、Invalid(无效状态))


候选者:缓存一致性协议我认为可以理解为「缓存锁」,它针对的是「缓存行」(Cache line) 进行”加锁”,所谓「缓存行」其实就是 高速缓存 存储的最小单位。



面试官:嗯…


候选者:MESI协议的原理大概就是:当每个CPU读取共享变量之前,会先识别数据的「对象状态」(是修改、还是共享、还是独占、还是无效)。


候选者:如果是独占,说明当前CPU将要得到的变量数据是最新的,没有被其他CPU所同时读取


候选者:如果是共享,说明当前CPU将要得到的变量数据还是最新的,有其他的CPU在同时读取,但还没被修改


候选者:如果是修改,说明当前CPU正在修改该变量的值,同时会向其他CPU发送该数据状态为invalid(无效)的通知,得到其他CPU响应后(其他CPU将数据状态从共享(share)变成invalid(无效)),会当前CPU将高速缓存的数据写到主存,并把自己的状态从modify(修改)变成exclusive(独占)


候选者:如果是无效,说明当前数据是被改过了,需要从主存重新读取最新的数据。



候选者:其实MESI协议做的就是判断「对象状态」,根据「对象状态」做不同的策略。关键就在于某个CPU在对数据进行修改时,需要「同步」通知其他CPU,表示这个数据被我修改了,你们不能用了。


候选者:比较于「总线锁」,MESI协议的”锁粒度”更小了,性能那肯定会更高咯


面试官但据我了解,CPU还有优化,你还知道吗?


候选者:嗯,还是了解那么一点点的。


候选者:从前面讲到的,可以发现的是:当CPU修改数据时,需要「同步」告诉其他的CPU,等待其他CPU响应接收到invalid(无效)后,它才能将高速缓存数据写到主存。


候选者:同步,意味着等待,等待意味着什么都干不了。CPU肯定不乐意啊,所以又优化了一把。


候选者:优化思路就是从「同步」变成「异步」。


候选者:在修改时会「同步」告诉其他CPU,而现在则把最新修改的值写到「store buffer」中,并通知其他CPU记得要改状态,随后CPU就直接返回干其他事了。等到收到其它CPU发过来的响应消息,再将数据更新到高速缓存中。


候选者:其他CPU接收到invalid(无效)通知时,也会把接收到的消息放入「invalid queue」中,只要写到「invalid queue」就会直接返回告诉修改数据的CPU已经将状态置为「invalid」



候选者:而异步又会带来新问题:那我现在CPU修改完A值,写到「store buffer」了,CPU就可以干其他事了。那如果该CPU又接收指令需要修改A值,但上一次修改的值还在「store buffer」中呢,没修改至高速缓存呢。


候选者:所以CPU在读取的时候,需要去「store buffer」看看存不存在,存在则直接取,不存在才读主存的数据。【Store Forwarding】


候选者:好了,解决掉第一个异步带来的问题了。(相同的核心对数据进行读写,由于异步,很可能会导致第二次读取的还是旧值,所以首先读「store buffer」。


面试官还有其他?


候选者:那当然啊,那「异步化」会导致相同核心读写共享变量有问题,那当然也会导致「不同」核心读写共享变量有问题啊


候选者:CPU1修改了A值,已把修改后值写到「store buffer」并通知CPU2对该值进行invalid(无效)操作,而CPU2可能还没收到invalid(无效)通知,就去做了其他的操作,导致CPU2读到的还是旧值。


候选者:即便CPU2收到了invalid(无效)通知,但CPU1的值还没写到主存,那CPU2再次向主存读取的时候,还是旧值…


候选者:变量之间很多时候是具有「相关性」(a=1;b=0;b=a),这对于CPU又是无感知的…


候选者:总体而言,由于CPU对「缓存一致性协议」进行的异步优化「store buffer」「invalid queue」,很可能导致后面的指令很可能查不到前面指令的执行结果(各个指令的执行顺序非代码执行顺序),这种现象很多时候被称作「CPU乱序执行」


候选者:为了解决乱序问题(也可以理解为可见性问题,修改完没有及时同步到其他的CPU),又引出了「内存屏障」的概念。



面试官:嗯…


候选者:「内存屏障」其实就是为了解决「异步优化」导致「CPU乱序执行」/「缓存不及时可见」的问题,那怎么解决的呢?嗯,就是把「异步优化」给”禁用“掉(:


候选者:内存屏障可以分为三种类型:写屏障,读屏障以及全能屏障(包含了读写屏障),屏障可以简单理解为:在操作数据的时候,往数据插入一条”特殊的指令”。只要遇到这条指令,那前面的操作都得「完成」。


候选者:那写屏障就可以这样理解:CPU当发现写屏障的指令时,会把该指令「之前」存在于「store Buffer」所有写指令刷入高速缓存。


候选者:通过这种方式就可以让CPU修改的数据可以马上暴露给其他CPU,达到「写操作」可见性的效果。


候选者:那读屏障也是类似的:CPU当发现读屏障的指令时,会把该指令「之前」存在于「invalid queue」所有的指令都处理掉


候选者:通过这种方式就可以确保当前CPU的缓存状态是准确的,达到「读操作」一定是读取最新的效果。



候选者:由于不同CPU架构的缓存体系不一样、缓存一致性协议不一样、重排序的策略不一样、所提供的内存屏障指令也有差异,为了简化Java开发人员的工作。Java封装了一套规范,这套规范就是「Java内存模型」


候选者:再详细地说,「Java内存模型」希望 屏蔽各种硬件和操作系统的访问差异,保证了Java程序在各种平台下对内存的访问都能得到一致效果。目的是解决多线程存在的原子性、可见性(缓存一致性)以及有序性问题。



面试官那要不简单聊聊Java内存模型的规范和内容吧?


候选者:不了,怕一聊就是一个下午,下次吧?


本文总结




  • 并发问题产生的三大根源是「可见性」「有序性」「原子性」




  • 可见性:CPU架构下存在高速缓存,每个核心下的L1/L2高速缓存不共享(不可见)




  • 有序性:主要有三方面可能导致打破



    • 编译器优化导致重排序(编译器可以在不改变单线程程序语义的情况下,可以对代码语句顺序进行调整重新排序)

    • 指令集并行重排序(CPU原生就有可能将指令进行重排)

    • 内存系统重排序(CPU架构下很可能有store buffer /invalid queue 缓冲区,这种「异步」很可能会导致指令重排)




  • 原子性:Java的一条语句往往需要多条 CPU 指令完成(i++),由于操作系统的线程切换很可能导致 i++ 操作未完成,其他线程“中途”操作了共享变量 i ,导致最终结果并非我们所期待的。




  • 在CPU层级下,为了解决「缓存一致性」问题,有相关的“锁”来保证,比如“总线锁”和“缓存锁”。



    • 总线锁是锁总线,对共享变量的修改在相同的时刻只允许一个CPU操作。

    • 缓存锁是锁缓存行(cache line),其中比较出名的是MESI协议,对缓存行标记状态,通过“同步通知”的方式,来实现(缓存行)数据的可见性和有序性

    • 但“同步通知”会影响性能,所以会有内存缓冲区(store buffer/invalid queue)来实现「异步」进而提高CPU的工作效率

    • 引入了内存缓冲区后,又会存在「可见性」和「有序性」的问题,平日大多数情况下是可以享受「异步」带来的好处的,但少数情况下,需要强「可见性」和「有序性」,只能”禁用”缓存的优化。

    • “禁用”缓存优化在CPU层面下有「内存屏障」,读屏障/写屏障/全能屏障,本质上是插入一条”屏障指令”,使得缓冲区(store buffer/invalid queue)在屏障指令之前的操作均已被处理,进而达到 读写 在CPU层面上是可见和有序的。




  • 不同的CPU实现的架构和优化均不一样,Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,保证了Java程序在各种平台下对内存的访问都能得到一致效果


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

手把手教你利用XSS攻击

前两天我收到安全部门的一个通知:高风险XSS攻击漏洞。 我们部门首先确定风险来源,并给出了解决方案。前端部分由我解决,并紧急修复上线。 一:那么什么是XSS攻击呢? 人们经常将跨站脚本攻击(Cross Site Scripting)缩写为CSS,但...
继续阅读 »

前两天我收到安全部门的一个通知:高风险XSS攻击漏洞。


906501ADEAF08AD26A3F225744EA44BB.jpg





我们部门首先确定风险来源,并给出了解决方案。前端部分由我解决,并紧急修复上线。


5C92478016448CBE2BB5650DAEB40955.jpg



一:那么什么是XSS攻击呢?


人们经常将跨站脚本攻击(Cross Site Scripting)缩写为CSS,但这会与层叠样式表(Cascading Style Sheets,CSS)的缩写混淆。因此,有人将跨站脚本攻击缩写为XSS。恶意攻击者往Web页面里插入恶意html代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意用户的特殊目的。主要指的自己构造XSS跨站漏洞网页或者寻找非目标机以外的有跨站漏洞的网页。XSS是web安全最为常见的攻击方式,在近年来,常居web安全漏洞榜首。


光看这个定义,很多同学一定不理解是什么意思,下面我会模拟XSS攻击,同学们应该就知道怎么回事了。
在模拟XSS攻击之前,我们先来看看XSS攻击的分类。


二:XSS攻击有几种类型呢?


①反射型XSS攻击(非持久性XSS攻击)

②存储型XSS攻击(持久型XSS攻击)

③DOM-based型XSS攻击


三:接下来我们将模拟这几种XSS攻击


第一种:反射型XSS攻击(非持久性XSS攻击)


反射型XSS攻击一般是攻击者通过特定手法,诱使用户去访问一个包含恶意代码的URL,当受害者点击这些专门设计的链接的时候,恶意代码会直接在受害者主机上的浏览器执行。此类XSS攻击通常出现在网站的搜索栏、用户登录口等地方,常用来窃取客户端Cookies或进行钓鱼欺骗。


下面我们来看一个例子:


image.png


这是一个普通的点击事件,当用户点击之后,就执行了js脚本,弹窗了警告。


image.png


你会说,这能代表啥,那如果这段脚本是这样的呢?


image.png


当浏览器执行这段脚本,就盗用了用户的cookie信息,发送到了自己指定的服务器。你想想他接下来会干什么呢?


第二种:存储型XSS攻击(持久型XSS攻击)


攻击者事先将恶意代码上传或者储存到漏洞服务器中,只要受害者浏览包含此恶意代码的页面就会执行恶意代码。这意味着只要访问了这个页面的访客,都有可能会执行这段恶意脚本,因此存储型XSS攻击的危害会更大。此类攻击一般出现在网站留言、评论、博客日志等交互处,恶意脚本存储到客户端或者服务端的数据库中。


增删改查在web管理系统中中很常见,我们找到一个新增功能页面,这以一个富文本输入框为例,输入以下语句,点击保存,再去查看详情,你觉得会发生什么?


image.png


没错,如果是前端的同学或许已经猜到了,h是浏览器的标签,这样传给服务器,服务器再返回给前端,浏览器渲染的时候,会把第二行当成h1标签来渲染,就会出现以下效果,第二行文字被加粗加大了。


image.png


这里我只是输入了普通的文本,而近几年随着互联网的发展,出现了很多h5多媒体标签,那要是我利用它们呢?
不清楚的同学,可自行打开W3cschool网站查看:


image.png


黑客是怎么攻击我们的呢?黑客会自己写一些脚本,来获取我们的cookies敏感等信息,然后他发送到他自己的服务器,当他拿到我们这些信息后,就能绕过前端,直接调后端的接口,比如提现接口,想想是不是很恐怖!!!


image.png


这里我利用一个在线远程网站来模拟XSS攻击。地址如下:
svg.digi.ninja/xss.svg**
目前网站还能访问,同学们可以自己体验一下,如果后期链接失效不可访问了,同学们可以重新找一个,或者自己手写一个脚本,然后伪装成svg上传到自己的服务器。
我们在地址栏输入上面这个地址,来看看实际效果,提示你已经触发了XSS攻击。
image.png


当我们点击确定,出现了一个黑人,哈哈哈,恭喜你,你银行卡里的钱已经全被黑客取走了。这就是黑客得逞后的样子,他得逞后还在嘲讽你。


image.png


接下来,我们利用多媒体标签和这个脚本来攻击我们实际的的网站。


这里记得在地址前面加上//表示跨越,如图:


image.png
当我们点击保存之后,再去查看详情页面发现。


image.png


哦豁,刚刚那个网站的场景在我们的web管理系统里面触发了,点击确定,那个小黑人又来嘲讽你了。


image.png


这脚本在我们的管理系统成功运行,并获取了我们的敏感信息,就可以直接绕过前端,去直接掉我们后端银行卡提现接口了。并且这类脚本由于保存在服务器中,并存着一些公共区域,网站留言、评论、博客日志等交互处,因此存储型XSS攻击的危害会更大。


第三种:DOM-based型XSS攻击


客户端的脚本程序可以动态地检查和修改页面内容,而不依赖于服务器端的数据。例如客户端如从URL中提取数据并在本地执行,如果用户在客户端输入的数据包含了恶意的JavaScript脚本,而这些脚本没有经过适当的过滤或者消毒,那么应用程序就可能受到DOM-based型XSS攻击。


下面我们来看一个例子


image.png


这段代码的意思是点击提交之后,将输入框中的内容渲染到页面。效果如下面两张图。


①在输入框中输入内容


image.png


②点击确定,输入框中的内容渲染到页面


image.png


那如何我们输内容是不是普通文本,而是恶意的脚本呢?


image.png


没错,恶意的脚本在渲染到页面的时候,没有被当成普通的文本,而是被当成脚本执行了。
image.png


总结:XSS就是利用浏览器不能识别是普通的文本还是恶意代码,那么我们要做的就是阻止恶意代码执行,比如前端的提交和渲染,后端接口的请求和返回都要对此类特殊标签做转义和过滤处理,防止他执行脚本,泄露敏感的数据。感兴趣的同学可以根据我上面的步骤,自己去模拟一个XSS攻击,让自己也体验一次当黑客的感觉。



收起阅读 »

产品经理又开始为难我了???我。。。。

最近做项目的时候,就是产品经理给的图总是很大,不压缩。每天要处理这些图片真的很累哇。于是一怒之下写下了这个**「vscode 插件」。「插件核心功能是压缩,然后上传图片」。 压缩的网站其实就是「tinypng」** 这个网站然后图片压缩后,然后再上传到cdn上...
继续阅读 »

最近做项目的时候,就是产品经理给的图总是很大,不压缩。每天要处理这些图片真的很累哇。于是一怒之下写下了这个**「vscode 插件」「插件核心功能是压缩,然后上传图片」。 压缩的网站其实就是「tinypng」** 这个网站然后图片压缩后,然后再上传到cdn上,然后然后这个压缩过的url 直接放到我们的粘贴板上。下面跟着我的步伐一步一步来写实现它。 先看效果图:


演示gif 图


演示gif 图


效率对比


开发这个主要是提高团队开发效率, 绝不是为了炫技。 看图:


image-20211017224316386


image-20211017224316386


需求分析



  1. 可在vscodde的setting中配置上传所需的参数,可以根据个人的需求单独进行配置;

  2. 2.在开发过程中可在编辑器中直接选择图片并上传到阿里云将图片链接填写到光标位置;


中文文档




一个好的文档可以帮助我们更容易的开发:如果英文比较好的同学可以直接看Vscode英文文档,这里api会比较全,可以找到更简洁的方案实现功能; 不过我的话,还是花很久时间找了这篇比较全的中文文档




搭建项目


vscode 插件的开发需要全局安装脚手架:


 npm install -g yo generator-code

安装成功后,直接使用对应命令 「yo code」 来生成一个插件工程:


vscode开始这个页面


vscode开始这个页面


这就开始脚手架页面了,可以选择自己习惯的配置。输入对应的配置 然后 就创建了对应的项目了。


我们看下项目结构:


插件结构


插件结构


插件运行


这时候我们先要去测试下我的这个插件到底是不是能够成功运行。在项目根目录按住F5 然后运行 「vscode extension」 ,这时候会出现一个新的vscode 窗口,但是我这里遇到的一个问题就是这个:


插件


插件


我大概理解了下就是vscode 插件的依赖版本比较低:


目前是:


插件


插件


这上面说的很清楚 vscode扩展指定 与其兼容的 vscode 版本兼容 很显然我这里太高了, 给他降级。然后给他换成1.60.2 完美解决


插件运行——成功演示


ok, 怎么查看自己查看插件有没有成功运行呢, 分为3步



  1. F5 开始调试 —— 产生一个新的调试窗口

  2. 在新的窗口—— command + shift + P 找到 hello word

  3. 点击运行看见弹窗 显示 表示弹窗运行成功


直接看下面的gif 图吧:


gif 演示


gif 演示


插件开发——配置参数


配置插件的属性面板, 这个主要是要在package.json 配置一些参数


配置参数


配置参数


第一个参数我们稍后再讲其实就是对应你注册的自定义command, 下面的配置 其实就是对应插件属性面板一些参数,然后你可以通过vscode 的一些api 可以获得你配置的这些参数


下面我是我配置的参数,你可以会根据插件自定义去调整


"properties": {
    "upload_image.domain": {
      "type": "string",
      "default": "",
      "description": "设置上传域名"
    },
    "upload_image.accessKey": {
      "type": "string",
      "default": "",
      "description": "设置oss上传accessKey"
    },
    "upload_image.secretKey": {
      "type": "string",
      "default": "",
      "description": "设置oss上传secretKey"
    },
    "upload_image.scope": {
      "type": "string",
      "default": "",
      "description": "设置oss上传上传空间"
    },
    "upload_image.gzip": {
      "type": "boolean",
      "default": "true",
      "description": "是否启用图片压缩"
    }
  }

大概就是这几个参数, 然后我们测试下同样打开f5 然后在新窗口 找到设置然后找到扩展, 设置项其实就是对应我们的 上面的**「title」**


压缩图片。


我们看下效果:


效果


效果


插件开发——配置右键菜单


这个功能描述大概就是,你在写的时候突然要上传,直接点击鼠标右键,然后直接选择图片。 对就是这个简单的东西,做东西需要从用户的角度考虑,一定要爽,能省一步是一步。呵呵哈哈哈


这个配置其实就是在 还是在刚才的**「package.json」** 上继续配置:


"menus": {
    "editor/context": [
      {
        "when": "editorFocus",
        "command": "extension.choosedImage",
        "group": "navigation"
      }
    ]
  }

when:就是你鼠标在编辑的时候


command: 就是自定义的事件,我叫他选择图片, 这个其实就是在extension.js 注册的事件名字 tips: 就是对应的事件名称


let texteditor = vscode.commands.registerTextEditorCommand(
  'extension.choosedImage', ... )

这个其实就是在extension .js 注册对应的事件名,这里的**「事件名」** 一定要和 「package.json」 中文件对应不然会出不来的。 给大家演示下:


图片


图片


重启插件 按下f5 然后按下右键就有我们自定义的右键菜单了。但是问题来了我们按住右键 是不是得弹出一个选择图片的框哇,不然怎么上传对吧?


打开图片上传 弹框


强大的vscode支持了内置的api, 支持打开:


const uri = await vscode.window.showOpenDialog({
    canSelectFolders: false,
    canSelectMany: false,
    filters: {
      images: ['png', 'jpg','apng','jpeg','gif','webp'],
    },
  }); 

就是这个 api, 你可以过滤出想要的图片, 在filters 里面,然后呢 吐出给我们的是对应图片的路径。


我们看下效果:图片选择


读取图片数据


其实这个时候我们我们已经有了图片的路径,这时候就要利用 **「node.js」**的fs 模块 去读取 这个图片的数据 buffer ,这个其实为了方便我们将图片上传到oss 上。 代码如下:


const uri = await vscode.window.showOpenDialog({
    canSelectFolders: false,
    canSelectMany: false,
    filters: {
      images: ['png', 'jpg','apng','jpeg','gif','webp'],
    },
  }); 
let imgBuffer =  await fs.readFile(uri[0].path);

这里还涉及到一个就是说: 本地图片的名字 进行加密, 不能上传到oss 各种中文啥的, 显示的我们很不专业哇


所以这里写了一个MD5的转换


function md5Name(name) {
 const index = name.lastIndexOf('.')
 const sourceFileName = name.substring(0, index)
 const suffix = name.substring(index)
 const fileName = md5(sourceFileName + Date.now()) + suffix
 return fileName.toLowerCase()
}

就是将名字搞成花里胡哨的样子,呵呵哈哈哈!


图片压缩


我们得到图片的buffer 数据后其实要对图片要支持压缩, 其实社区里面有很多方案, 这里的话我调研的很多还是决定使用tinfiy, 他也有对应的**「node.js」** 使用的他主要理由主要是看下面这张图:


apng


apng


对的这家伙支持**「apng」, 其他的不是很清楚。 但是他不是免费的一个人一个月免费「500」** 次, 思考了下还行,我们也用不到辣么多次最终还是考虑用它去实现。


安装


安装npm包并添加到您应用的依赖中,您就可以使用Node.js客户端:


npm install --save tinify

认证


您必须提供您的API密钥来使用API。您可以通过注册您的姓名和Email地址来获取API密钥。 请秘密保存API密钥。


const tinify = require("tinify");
tinify.key = "YOUR_API_KEY";

这个的话其实就是你的邮箱去注册一下,然后把你对应的**「key」** 去激活其实就可以了


如图


如图


其实就是下面这个你的key 设置激活就好了


tinify压缩图片


您可以上传任何JPEG或PNG图片到Tinify API来进行压缩。我们将自动检测图片类型并相应的使用TinyPNG或TinyJPG引擎进行优化。 只要上传文件或提供图片URL,就会开始压缩。


您可以选择一个本地文件作为源并写入到另一个文件中。


const source = tinify.fromFile("unoptimized.webp");
source.toFile("optimized.webp");

您还可以从缓冲区(buffer)(二进制字符串)上传图片并获取压缩后图片的数据。


const fs = require("fs");
fs.readFile("unoptimized.jpg", function(err, sourceData) {
  if (err) throw err;
  tinify.fromBuffer(sourceData).toBuffer(function(err, resultData) {
    if (err) throw err;
    // ...
  });
});


代码实现


function compressBuffer(sourceData, key = 'xxx') {
 return new Promise((resolve,reject) => {
  tinify.key = key;
  tinify.fromBuffer(sourceData).toBuffer(function(err, resultData) {
   if(resultData) {
    resolve(resultData)
   }
   if (err) {
    reject(err);
   }
   // ...
  });
 })
}

基于他这个封装了一个promise, 这个**「fromBuffer」** , 到 「toBuffer」 是真的好用。 哈哈哈哈很香,记得一定要设置key 不然promise 直接会报错的, 设置key的方法 就在上面👆🏻, 然后这样其实我们就获得了压缩的图片数据了。


上传图片到oss


这里的话其实有的使用七牛云、 有的使用阿里云。去上传图片,或者是ajax 去上传其实都可以


一般都是要获取token 啥的以及各种签名信息,然后直接上传就好了, 然后呢你就可以获得一张图片地址了。代码我就不展示了, 都是前端应该都懂。这里我说下我遇到的一些问题



  1. 第一个就是js 跑的 是node js 的环境, 如果使用**「FormData」** 这个类的话 他直接会报找不到, 这个方法是 undefined, 还有**「fetch」**, 所以说要去安装对应node js 包 ,我这里使用的是 「cross-fetch」「form-data」


这里我说一下配置的问题就是你在扩展中如何获得的你配置的参数:


"configuration": [
   {
    "title": "压缩图片",
    "properties": {
     "upload_image.secretKey": {
      "type": "string",
      "default": "",
      "description": "设置tinify的ApIKey"
     },
     "upload_image.secretTokenUrl": {
      "type": "string",
      "default": "",
      "description": "设置得物的tokenUrl"
     }
    }
   }
  ]

每个属性前面对应的 upload_image 其实你在扩展中你可以通过:


const imageConfig =  vscode.workspace.getConfiguration('upload_image')

然后你就可以拿到配置了,upload_image 后面的属性 其实对应的就是对象中的key 然后呢你就可以对吧操作了


这个东西还是具体项目, 具体分析,你们自己 可以针对自己的项目去配置


插件开发——图片链接写入编辑器中


通过上面的方法已经可以获得图片上传后的链接,接下来就是将链接写入编辑器中: 首先判断编辑器选择位置,editor.selection中可以获得光标位置、光标选择首尾位置。若光标有选中内容则editBuilder.replace替换选中内容,否则editBuilder.insert在光标位置插入图片链接:


// 将图片链接写入编辑器
function addImageUrlToEditor(url) {
 let editor = vscode.window.activeTextEditor
 if (!editor) {
   return
 }
 const { start, end, active } = editor.selection
 if (start.line === end.line && start.character === end.character) {
   // 在光标位置插入内容
   const activePosition = active
   editor.edit((editBuilder) => {
  editBuilder.insert(activePosition, url)
   })
 } else {
   // 替换内容
   const selection = editor.selection
   editor.edit((editBuilder) => {
  editBuilder.replace(selection, url)
   })
 }
}

插件发布


到这里,其实一整个vscode插件 其实已经可以开发完成了, 然后我们要把他进行打包发布到vscode 的应用市场


创建账号


我是直接github 登录创建, 首先我们进入文档中提到的主页,完成验证登录后创建一个组织。


创建一个组织


创建一个组织


创建发布者


进入下面这个页面 marketplace.visualstudio.com/manage/publ…** 插件发布者, 给大家看下我的:


发布者


发布者


打包发布


首先全局 安装脚手架


npm install -g vsce

然后 cd 到当前插件目录 使用下面命令


$ cd myExtension
$ vsce package
# myExtension.vsix generated

这里的打包会报一些error:


第一个就是插件的package.json 增加发布者


"publisher": "Fly",

如果给插件加图标: 其实在项目中创建一个文件夹: image 然后把图片放进去: 同时也要在package.json 中配置


"icon": "images/dewu.jpeg",

这里可能有⚠️,不过没什么关系,继续跑就完事了


warn


warn


最后的话其实就是要写readme ,不然 不让你发布。


打包上传


一切准备就绪: 命令行 输入


vsce package 

然后项目中就会出现:


照片


照片


然后可以把这个东西拖到页面这个页面


marketplace.visualstudio.com/manage/publ…


上传


上传


然后点击上传就好了,你就可以在vscode 插件商场可以看到自己写的插件了


插件


作者:Fly
链接:https://juejin.cn/post/7020052159999770632

收起阅读 »

TypeScript 想更深入一层?我推荐自定义 transformer 的 compiler api

现在 JS 的很多库都用 typescript 写了,面试也几乎必问 typescript,可能你对 ts 的各种语法和内置高级类型都挺熟悉了,对 ts 的配置、命令行的使用也没啥问题,但总感觉对 ts 的理解没那么深,苦于没有很好的继续提升的方式。这时候我推...
继续阅读 »

现在 JS 的很多库都用 typescript 写了,面试也几乎必问 typescript,可能你对 ts 的各种语法和内置高级类型都挺熟悉了,对 ts 的配置、命令行的使用也没啥问题,但总感觉对 ts 的理解没那么深,苦于没有很好的继续提升的方式。这时候我推荐你研究下 typescript compiler api


typescript 会把 ts 源码 parse 成 AST,然后对 AST 进行各种转换,之后生成 js 代码,在这个过程中会对 AST 进行类型检查。typescript 把这整个流程封装到了 tsc 的命令行工具里,平时我们一般也是通过 tsc 来编译 ts 代码和进行类型检查的。


但其实 ts 除了提供 tsc 的命令行工具外,也暴露了很多 api,同时也能自定义 transformer。这就像 babel 可以编译 esnext、ts 语法到 js,可以写 babel 插件来转换代码,也暴露了各种 api 一样。只不过 typescript transformer 的生态远远比不上 babel 插件,知道的人也比较少。


其实 typescript transformer 能做到一些 babel 插件做不到的事情:

babel 是从 ts、exnext 等转 js,生成的 js 代码里会丢失类型信息,不能生成 ts 代码。

babel 只是转换 ts 代码,并不会进行类型检查。


这两个 babel 插件做不到的事情,通过 typescript transformer 都可以做到。


而且,学会 typescript compiler 的 api 能够帮助你深入 typescript 的编译流程,更好的掌握 typescript。


说了这么多,我们通过一个例子来入门下 typescript transformer 吧。


案例描述


这样一段 ts 代码:


type IsString<T> = T extends string ? 'Yes' : 'No';

type res = IsString<true>;
type res2 = IsString<'aaa'>;

我们希望能把 res 和 res2 的类型的值算出来,通过注释加在后面。


像这样:


type IsString<T> = T extends string ? 'Yes' : 'No';

type res = IsString<true> //No;
type res2 = IsString<'aaa'> //Yes;

这个案例既用到了 transformer api,又用到了类型检查的 api。


下面我们来分析下思路:


思路分析


我们首先要把 ts 代码 parse 成 AST,然后通过 AST 找到要转换的节点,这里是 TypeReference 节点。


可以用 astexplorer.net 看一下:



IsString 是一个 TypeReference,也就是引用了别的类型,然后有 typeName 是 IsString 和类型参数 typeArguments,这里的类型参数就是 true。


是不是很像一个函数调用,这就是高级类型的本质,通过把类型参数传到引用的高级类型里求出最终的类型。


然后我们找到 TypeReference 的节点之后就可以通过 type checker 的 api 来求出类型值,之后创建一个注释节点添加到后面就行了。


转换完 AST,再把它打印成 ts 代码字符串。


思路就是这样,接下来我们具体来实现下,也熟悉下 ts 的 api。


代码实现


parse 代码成 AST 需要先指定要编译的文件和编译参数(createProgram 的 api),然后就可以拿到不同文件的 AST 了(getSourceFile 的 api)。


const ts = require("typescript");

const filename = "./input.ts";
const program = ts.createProgram([filename], {}); // 第二个参数是 compiler options,就是配置文件里的那些

const sourceFile = program.getSourceFile(filename);

这里的 sourceFile 就是 AST 的根结点。


接下来我们要对 AST 进行转换,使用 transform 的 api:


const  { transformed } = ts.transform(sourceFile, [
function (context) {
return function (node) {
return ts.visitNode(node, visit);

function visit(node) {
if (ts.isTypeReferenceNode(node)) {
// ...
}
return ts.visitEachChild(node, visit, context)
}
};
}
]);

transform 要传入遍历的 AST 以及 transfomerFactory。

AST 就是上面 parse 出的 sourceFile。

transformerFactory 可以拿到 context 中的很多 api 来用,它的返回值就是转换函数 transformer。


transformer 参数是 node,返回值是修改后的 node。


要修改 node 就要遍历 node,使用 visit api 和 vistEachChild 的 api,过程中根据类型过滤出 TypeReference 的节点。


之后对 TypeReference 节点做如下转换:


if (ts.isTypeReferenceNode(node)) {
const type = typeChecker.getTypeFromTypeNode(node);

if (type.value){
ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, type.value);
}
}

也就是通过 typeCheker 来拿到 IsString 这个类型的最终类型值,然后通过 addSyntheticTrailingComment 的 api 在后面加一个注释。


其中用到的 typeChecker 是通过 getTypeChecker 的 api 拿到的:


const typeChecker = program.getTypeChecker();

这样就完成了我们的转换 ts AST 的目的。


然后通过 printer 把 AST 打印成 ts 代码。


const printer =ts.createPrinter();

const code = printer.printNode(false, transformed[0], transformed[0]);

console.log(code);

这样就可以了,我们来测试下。


测试之前,全部代码放这里了:


const ts = require("typescript");

const filename = "./input.ts";
const program = ts.createProgram([filename], {}); // 第二个参数是 compiler options,就是配置文件里的那些

const sourceFile = program.getSourceFile(filename);

const typeChecker = program.getTypeChecker();

const { transformed } = ts.transform(sourceFile, [
function (context) {
return function (node) {
return ts.visitNode(node, visit);
function visit(node) {
if (ts.isTypeReferenceNode(node)) {
const type = typeChecker.getTypeFromTypeNode(node);

if (type.value){
ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, type.value);
}
}
return ts.visitEachChild(node, visit, context)
}
};
}
]);

const printer =ts.createPrinter();

const code = printer.printNode(false, transformed[0], transformed[0]);

console.log(code);

测试效果


经测试,我们达到了求出类型添加到后面的注释里的目的



复盘


激不激动,这是我们第一个 ts transformer 的例子,虽然功能比较简单,但是我们也学会了如何对 ts 代码做 parse、 transform,print,以及 type check。


其实 babel 也有 parse、transform、generate 这 3 步,但没有 type check 的过程,也不能打印成 ts 代码。


用 compiler api 的过程中你会发现原来高级类型就是一个 typeReference,需要传入 typeArguments 来求值的,从而对高级类型的理解更深了。


总结


对 typescript 语法和配置比较熟悉后,想更进一步的话,可以学习下 compiler 的 api 来深入 ts 的编译流程。它包括 transfomer、type checker 等 api,可以达到像 babel 插件一样的转换 ts 代码的目的,而且还能做类型检查。


我们通过一个例子来熟悉了下 typescript 的编译流程和 transformer 的写法。


当你需要修改 ts 代码然后生成 ts 代码的时候,babel 是做不到的,它只能生成 js 代码,这时候可以考虑下 typescript 的自定义 transformer。


而且用 typescript compiler api 能够加深你对 ts 编译流程和类型检查的理解。


ts compiler api 尤其是其中的自定义 transformer 是 typescript 更进一层的不错的方向。



收起阅读 »

JavaScript之彻底理解EventLoop

在正式学习Event Loop之前,先需要解决几个问题:什么是同步与异步?JavaScript是一门单线程语言,那如何实现异步?同步任务和异步任务的执行顺序如何?异步任务是否存在优先级? 同步与异步 计算机领域中的同步与异步和我们现实社会的同步和异步正好相反。...
继续阅读 »

在正式学习Event Loop之前,先需要解决几个问题:

什么是同步与异步?

JavaScript是一门单线程语言,那如何实现异步?

同步任务和异步任务的执行顺序如何?

异步任务是否存在优先级?


同步与异步


计算机领域中的同步与异步和我们现实社会的同步和异步正好相反。现实中的同步,就是同时进行,突出的是"同",比如看足球比赛的时候吃着零食,两件事情同时发生;异步就是不同时。但计算机中与现实存在一定差异。


举个栗子


天气冷了,早上刚醒来想喝点热水暖暖身子,但这每天起早贪黑996,晚上回来太累躺下就睡,没开水啊,没法子,只好急急忙忙去烧水。


现在早上太冷了啊,不由得在被窝里面多躺了一会,收拾的时间紧紧巴巴,不能空等水开,于是我便趁此去洗漱,收拾自己。
洗漱完,水开了,喝到暖暖的热水,舒服啊!


舒服完,开启新的996之日,打工人出发!


烧水和洗漱是在同时间进行的,这就是计算机中的异步


计算机中的同步是连续性的动作,上一步未完成前,下一步会发生堵塞,直至上一步完成后,下一步才可以继续执行。例如:只有等水开,才能喝到暖暖的热水。


单线程却可以异步?


JavaScript的确是一门单线程语言,但是浏览器UI是多线程的,异步任务借助浏览器的线程和JavaScript的执行机制实现。
例如,setTimeout就借助浏览器定时器触发线程的计时功能来实现。


浏览器线程



  1. GUI渲染线程

    • 绘制页面,解析HTML、CSS,构建DOM树等

    • 页面的重绘和重排

    • 与JS引擎互斥(JS引擎阻塞页面刷新)



  2. JS引擎线程

    • js脚本代码执行

    • 负责执行准备好的事件,例如定时器计时结束或异步请求成功且正确返回

    • 与GUI渲染线程互斥



  3. 事件触发线程

    • 当对应的事件满足触发条件,将事件添加到js的任务队列末尾

    • 多个事件加入任务队列需要排队等待



  4. 定时器触发线程

    • 负责执行异步的定时器类事件:setTimeout、setInterval等

    • 浏览器定时计时由该线程完成,计时完毕后将事件添加至任务队列队尾



  5. HTTP请求线程

    • 负责异步请求

    • 当监听到异步请求状态变更时,如果存在回调函数,该线程会将回调函数加入到任务队列队尾




同步与异步执行顺序



  1. JavaScript将任务分为同步任务和异步任务,同步任务进入主线中中,异步任务首先到Event Table进行回调函数注册。

  2. 当异步任务的触发条件满足,将回调函数从Event Table压入Event Queue中。

  3. 主线程里面的同步任务执行完毕,系统会去Event Queue中读取异步的回调函数。

  4. 只要主线程空了,就会去Event Queue读取回调函数,这个过程被称为Event Loop


举个栗子




  • setTimeout(cb, 1000),当1000ms后,就将cb压入Event Queue。

  • ajax(请求条件, cb),当http请求发送成功后,cb压入Event Queue。



EventLoop执行流程


Event Loop执行的流程如下:
在这里插入图片描述


下面一起来看一个例子,熟悉一下上述流程。


// 下面代码的打印结果?
// 同步任务 打印 first
console.log("first");
setTimeout(() => {
// 异步任务 压入Event Table 4ms之后cb压入Event Queue
console.log("second");
},0)
// 同步任务 打印last
console.log("last");
// 读取Event Queue 打印second

常见异步任务

DOM事件

AJAX请求

定时器setTimeoutsetlnterval

ES6Promise


异步任务的优先级


下面继续来看一个案例:


setTimeout(() => {
console.log(1);
}, 1000)
new Promise(function(resolve){
console.log(2);
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log(3)
});
console.log(4)

按照上面的学习:
可以很轻松得出案例的打印结果:2,4,1,3



Promise定义部分为同步任务,回调部分为异步任务



将案例代码在控制台运行,最终返回结果却有些出人意料:


在这里插入图片描述


刚看到如此结果,我的第一感觉是,setTimeout函数1s触发太慢导致它加入Event Queue的时间晚于Promise.then


于是我修改了setTimeout的回调时间为0(浏览器最小触发时间为4ms),但结果仍为发生改变。


那么也就意味着,JavaScript的异步任务是存在优先级的。


宏任务和微任务


JavaScript除了广义上将任务划分为同步任务和异步任务,还对异步任务进行了更精细的划分。异步任务又进一步分为微任务和宏任务。


在这里插入图片描述




  • history traversal任务(h5当中的历史操作)

  • process.nextTicknodejs中的一个异步操作)

  • MutationObserverh5里面增加的,用来监听DOM节点变化的)



宏任务和微任务分别有各自的任务队列Event Queue,即宏任务队列和微任务队列。


Event Loop执行过程


了解到宏任务与微任务过后,我们来学习宏任务与微任务的执行顺序。

代码开始执行,创建一个全局调用栈,script作为宏任务执行

执行过程过同步任务立即执行,异步任务根据异步任务类型分别注册到微任务队列和宏任务队列

同步任务执行完毕,查看微任务队列

若存在微任务,将微任务队列全部执行(包括执行微任务过程中产生的新微任务)

若无微任务,查看宏任务队列,执行第一个宏任务,宏任务执行完毕,查看微任务队列,重复上述操作,直至宏任务队列为空


更新一下Event Loop的执行顺序图:


在这里插入图片描述


总结


在上面学习的基础上,重新分析当前案例:


setTimeout(() => {
console.log(1);
}, 1000)
new Promise(function(resolve){
console.log(2);
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log(3)
});
console.log(4)

分析过程见下图:
在这里插入图片描述



收起阅读 »

iOS swiftUI 创建 macos图片 1.1

第六节 组合列表视图与过滤器视图创建一个组列过滤器和列表的视图。为过滤器提供新的状态信息,同时绑定地标选择到主视图的父视图上。步骤1 项目中添加一个新的SwiftUI视图,命名为NavigationPrimary.swift。步骤2 声明一...
继续阅读 »

第六节 组合列表视图与过滤器视图

创建一个组列过滤器和列表的视图。为过滤器提供新的状态信息,同时绑定地标选择到主视图的父视图上。

section 6

步骤1 项目中添加一个新的SwiftUI视图,命名为NavigationPrimary.swift

步骤2 声明一个FilterType状态。这个状态会被绑定到过滤器和列表视图中。

section 6 step2

步骤3 添加过滤器视图并绑定FilterType状态。现在预览是失败的,因为过滤器依赖环境中的用户数据,下一步会处理这块儿。

section 6 step3

步骤4 注入用户数据对角到环境中。导航主视图是不直接需要用户数据的,但它的子视图需要。为了可以进行预览,把用户数据作为环境对象注入到导航主视图中。

section 6 step4

步骤5 添加一个绑定到当前选中地标的关系。

步骤6 添加地标列表视图,并把它绑定到选中的地标和过滤器状态上。预览视图中选中第二个选项,因为输入数据是landmarkData[1]作为用户选中的地标输入数据。

section 6 step6

步骤7 限制导航视图的宽度,防止用户让它变的太宽或太窄。

section 6 step7

第七节 复用CircleImage

有时只需要经过稍微修改,就可以跨平台复用一些视图。当构建macOS平台的地标详情页视图时,会复用iOS版地标应用中的CircleImage视图。为了适配macOS平台下的不同布局要求,会添加一个参数来控件阴影半径。

section 7

步骤1 在项目导航栏中选中Landmarks -> Supporting Views并选择CircleImage.swift文件。

section 7 step1

步骤2 把CircleImage.swift文件添加到时MacLandmarks编译目标。

section 7 step2

步骤3 在CircleImage.swift文件中,修改结构体,使用新的阴影半径参数。通过给新参数提供默认值,可以确保iOSwatchOS平台的应用都能与原来保持一致,同时还能在macOS平台上使用。

section 7 step3

第八节 为macOS扩展MapView

类似于CircleImage,这里要在macOS上复用MapView。然而,MapView要做更大的改动,因为MapView使用的是MapKit依赖于UIKit框架。在macOS平台上使用MapKit需要依赖于AppKit框架,所以需要添加编译器指令,让编译过程在macOS目标上进行正确的依赖。

section 8

步骤1 在项目导航器中,选择Landmarks -> Supporting Views,选中MapView.swift文件。

步骤2 把MapView.swift文件添加到MacLandmarks编译目标上。此时Xcode会报错,因为MapView使用了UIViewRepresentable协议,这个协议在macOS SDK里是没有的。下面的步骤中,会使用NSViewRepresentable协议来扩展MapView,让它能在macOS平台上使用。

section 8 step2

步骤3 插入条件编译指令,用来指定特定平台行为。用条件编译的两个条件分支把协议UIViewRepresentableNSViewRepresentable协议的遵循分开。

步骤4 使用条件编译,把在iOS平台上要实现的协议UIViewRepresentable及协议方法makeUIViewupdateUIView放在MapView的扩展实现中,这样就把MapKit的平台依赖性解耦了。

步骤5 添加在macOS平台上的NSViewRepresentable协议遵循。与UIViewRepresentable协议一样,NSViewRepresentable协议的实现也可以使用主类中的方法。

section 8 step5

第九节 构建详情视图

详情视图展示用户选中的地标信息。创建一个类似iOS平台地标应用的地标详情视图,不同之处在于,macOS平台有不同的数据表示方法,这就需要针对macOS平台对详情视图作一些裁剪,复用一些之前调整过的视图。

section 9

步骤1 项目中添加一个新的视图,命名为NavigationDetail.swift,并添加一个landmark属性。初始化详情视图时会使用landmark属性来指定详情页展示的地标信息。

步骤2 在NavigationDetail.swift内部创建一个滚动视图,滚动视图中包含一个VStackVStack中又包含一个HStack,HStack中展示关于地标的图片CircleImageText地标文本信息。通过设置VStack的最大最小宽度,确保展示的内容保持一定的宽度,以适合用户阅读。跨平台复用视图是非常方便的,定制一下CircleImage视图,以满足当前的布局要求。

section 9 step2

步骤3 把输入的图片变为可缩放,并设置图片按视图大小展示,这样可以让CircleImage视图的大小与Text块文本的大小看上去比较匹配。这种修改方法不需要调整CircleImage的内部实现。

section 9 step3

步骤4 调整阴影半径,以匹配更小的图片。这个修改依赖之前对CircleImage视图所作的参数化改造。

section 9 step4

用户使用按钮标记一个地标是否被收藏。为了让这个动作生效,需要访问用户数据中的对应变量。

步骤5 添加用户数据对应的环境对象,并创建一个基于当前选中地标的存储属性landmarkIndex

section 9 step5

步骤6 添加一个按钮,水平方式对齐地标名称,使用星星图标,并在点击时可以切换用户对这个地标的收藏状态。当用户修改地标数据时,在用户数据中查找被修改的地标数据,并用最新的数据更新原来的数据,让数据保持最新状态。

section 9 step6

步骤7 在分割区载下再添加一个地标的信息,对应数据中新增的字段description

section 9 step7

预览视图中标题块会被挤到左边,因为描述内容比较多,把水平方向的宽度撑满了。

步骤8 在详情视图顶部插入地图,调整地图的偏移,让地图和其它内容有一定区域的重叠。地图占满视图全宽,因此会把详情文本挤到预览视图的底部看不到的位置,但它实际上是存在的。

section 9 step8

步骤9 导入MapKit并添加一个Open in Maps的按钮,当按钮被点击时,打开地图应用并定位到地标位置。

section 9 step9

步骤10 把Open in Maps按钮叠放在地图的右下角。

section 9 step10

第十节 把主视图和详情视图组合起来

已经构建了所有的视图元素,把主视图和详情视图组合起来,共同构成ContentView

section 10

步骤1 在MacLandmarks文件夹中,选择ContentView.swift文件。

步骤2 为选中的地标设置对应的属性selectedLandmark,并用@State属性标识为状态属性。使用可选类型定义selectedLandmark,可以不用为它设置默认值。因此,无论是预览视图还是应用初始化时,都可以不需要用户选中地标进行渲染。

步骤3 把用户数据作为环境对象注入。ContentView本身不会直接依赖用户数据,但它的子视图需要访问用户数据。对于预览视图来说,为了正常预览和编译成功,ContentView需要获取用户数据。

section 10 step3

步骤4 在AppDelegate.swift中,为ContentView注入环境对象,这样可以让它的子视图访问到用户数据,应用也可以编译成功。

section 10 step4

步骤5 在ContentView中添加NavigationView作为顶级视图,并设置一个最小尺寸。

section 10 step5

步骤6 添加主视图,展示选中的地标。当用户选中地标列表中的某个地标时,被选中的地标数据就会被赋值到selectedLandmark属性上。

section 10 step6

步骤7 添加详情视图,详情视图不接收可选地标数据, 因些传入详情视图的地标数据需要确保不为空。用户选中地标前,地标详情视图不会渲染,这就是为会预览视图没有任何改变,还是和之前一样。

section 10 step7

步骤8 构建并运行应用。尝试改变过滤器的设置,或者点击详情页中的收藏按钮,观察视图内容的变化。

section 10 step8


收起阅读 »

iOS swiftUI 创建 macos图片 1.0

创建MACOS应用创建了watchOS平台的Landmarks应用后,下一步就是把Landmarks带到MacOS平台上。运用之前学到的所有知识,完成在iOS、watchOS及macOS的全平台应用。在项目工程中添加macOS编译目标,复用在iOS应用中的代码...
继续阅读 »

创建MACOS应用

创建了watchOS平台的Landmarks应用后,下一步就是把Landmarks带到MacOS平台上。运用之前学到的所有知识,完成在iOSwatchOSmacOS的全平台应用。

在项目工程中添加macOS编译目标,复用在iOS应用中的代码和资源,使用SwiftUI创建macOS平台上的列表和详情视图。

按照步骤来编译工程,或者下载工程查看完成后的代码。


第一节 项目中添加macOS编译目标

项目中添加macOS编译目标,Xcode会自动添加一个文件组与一些初始文件,还会生成一个编译运行方案。

section 1

步骤1 选择File->New->Target,模板选择页面出现后,选择macOS选项卡,选中App模板并点击Next。这个模板会添加一个新的macOS编译目标到项目里。

section 1 step1

步骤2 在信息表中,输入MacLandmarks作为项目的名称,设置编程语言为Swift,界面构建方法为SwiftUI,然后点击Finish

section 1 step2

步骤3 设置运行方案为MacLandmarks -> My Mac。这样就可以编译并运行macOS应用。

section 1 step3

这个应用的运行依赖一些特性,这些特性在早期的macOS上是不支持的,所以可能需要改变部署目标。

步骤4 在项目导航器中,选择顶部的Xcode项目,在可用编译运行目标栏中,选择部署目标为10.15

section 1 step4

步骤5 在MacLandmarks文件夹中,选择ContentView.swift文件,打开预览画布,点击恢复(Resume),查看预览。SwiftUI会提供main视图和它的预览视图提供者,就像iOS应用,可以预览应用的主窗口。

section 1 step5

第二节 共享数据和资源

下一步,复用来自iOS应用的模型和资源文件到macOS应用中。

section 2

步骤1 在项目导航器中,打开Landmarks文件夹并选中所有ModelsResources文件夹。landmarkData.json文件包含在教程的启动项目,里面包含了一个新的description字段,这是之前的教程中所没有的内容。

section 2 step1

步骤2 在文件检查器中,为选中的文件设置目标成员关系为MacLandmarks项目。应用编译时需要访问这些共享资源。要使用新的description字段,需要在Landmark结构体中添加一个对应的字段。

section 2 step2

步骤3 打开Landmark.swift文件,添加一个description属性。因为载入的数据遵循Codable协议,只需要确保属性名称和json文件中对应的字段名称一致就可以导入新增的字段数据了。

section 2 step3

第三节 创建行视图

对于使用SwiftUI来构建视图,一般是自底向上的方式,先创建小视图,然后用小视图组合成更大的视图。下面将创建一个列表的行视图。这个行视图包含地标的名称、地理位置、图片以及一个可选的标记,表标这个地标是否被收藏。

section 3

步骤1 在MacLandmarks文件夹下添加一个新的SwiftUI视图,命名为LandmarkRow.swiftiOS应用下也有一个与之同名的文件,重名文件可以通过设置文件的目标成员为适合的App来解决重名的问题。

section 3 step1

步骤2 添加一个landmark属性到LandmarkRow结构体中,并更新预览视图,让新创建的视图可以在预览视图中展示出来。

section 3 step2

步骤3 用VStack包裹的地标图片视图替换占位文本Text视图。

section 3 step3

步骤4 添加一个包裹在VStack中的描述地标的文本视图。

section 3 step4

步骤5 添加一个收藏指示视图,把它和其它现有的内容用一个Spacer分割开。Spacer会把已有的视图推向左边,但是收藏指示视图要放在右边,目前是不可见状态,因为此时还没有图片资源与之对应。

section 3 step5

步骤6 从Resources文件夹下拖动star-filled.pdfstar-empty.pdf文件到macOS应用的Assets.xcassets文件内。

section 3 step6

步骤7 给行视图添加内边距,现在就能够把黄色的收藏标记显示出来了。行视图的内边距可以提高可读性,当把多个行视图集合到列表视图内时,这一点就能很明显的看出来了。

section 3 step7

第四节 把行视图组合进列表视图中

使用上一节创建的行视图,创建一个列表视图,用来展示用户了解的所有地标。当showFavoritesOnly属性为真时,列表中只展示那些被用户收藏的地标。

section 4

步骤1 添加一个名为LandmarkList.swift的新的SwiftUI视图

section 4 step1

步骤2 添加userData属性作为环境注入对象,并更新预览视图。这样就可以让视图访问全局用户地标数据。

section 4 step2

步骤3 创建一个列表,行使用使用landmarkRow定义的类型。

section 4 step3

步骤4 让列表的行可以被用户选中,需要给列表提供一个绑定可选地标成员的关系,并用地标数据自己来标识行。之后会使用这个被选中的地标来展示地标详情页。

section 4 step4

步骤5 根据showFavoritesOnly的状态值以及地标数据是否被用户标记为收藏来决定列表中展示的行的内容。

section 4 step5

第五节 创建过滤器来管理列表的展示内容

因为用户可以标记地标为收藏状态,所以需要提供方式让用户只看到自己收藏过的地标。现在要创建一个过滤器视图,使用Toggle控件给用户提供一个勾选设置,让用户选择是否过滤列表中的非收藏地标,只展示收藏过的地标。

为了让用户可以快速筛选出自己喜欢的地标,这里会添加一下选择器弹出按钮,让用户可以根据地标的不同类别,选择过滤展示自己收藏的地标数据。

section 5

步骤1 添加一个名为Filter.swiftSwiftUI视图。

步骤2 添加userData属性作为环境注入对象,并更新预览视图。

步骤3 用Toggle控件来展示布尔值showFavoritesOnly属性,并给它一个恰当的标签文本。

section 5 step3

当用户选择勾选框时,列表视图也会跟着一起刷新展示,因为它们都绑定了同一上环境注入对象中的值showFavoritesOnly。除此之外,还可以使用地标的类别来定义额外的过滤条件。

步骤4 创建FilterType类型,用来存放地标的类别以及类别对应的名称。确保FilterType遵循Hashable协议,这样FilterType就可以被用在选择器。FilterType中的名称属性可以展示在选择器中,让用户选择过滤哪一种类别的地标。

section 5 step4

步骤5 定义一个all类型用来表示不使用任何地标类别过滤。这个额外的过滤类别要求FilterType有一个特殊的初始构建器,用来处理类别为空的初始化场景。

section 5 step5

遵循CaseIterableIdentifiable协议,让FilterType可以做为ForEach的初始化入参,之后就可以使用这个FilterType类型了。

步骤6 遵循CaseIterable协议,给列表提供所有可能的类别。

section 5 step6

步骤7 遵循Identifiable协议并定义一个id属性。

section 5 step7

步骤8 在Filter.swift中,给Filter视图添加一个选择器,选择器使用一个FilterType的绑定用来记录用户选择,FilterType的名称用来表示用户在选择器菜单中的选项。使用FilterType的绑定关系可以让父视图观察到用户的选择。

section 5 step8

步骤9 返回到列表视图,添加FilterType绑定关系。对于过滤器视图来说,这允许它和父视图共享变量filter

步骤10 更新列表行的创建逻辑,让它包含类别过滤功能。查找那些与用户选中的过滤类别相匹配的地标类别,或者任何用户选择的特色类别地标。

section 5 step10


收起阅读 »

「一探究竟」迷之序列化

事件起因 今天,我需要上线一个非常小但是又非常重要的系统改动,即给核心接口的RPC接口出参增加序列化接口(由下图可见,原实体类未实现序列化)。 编码、测试、代码审核一气呵成,然后收到驳回通知,架构师说实现序列化接口时注意不要忘记配置serialversio...
继续阅读 »

事件起因


今天,我需要上线一个非常小但是又非常重要的系统改动,即给核心接口的RPC接口出参增加序列化接口(由下图可见,原实体类未实现序列化)。


image-20210907025636984.png




编码、测试、代码审核一气呵成,然后收到驳回通知,架构师说实现序列化接口时注意不要忘记配置serialversionUID,还非常贴心的跟我说,IDEA 有一个插件可以自动生成UID,推荐我下载使用(IDEA serialversionUID 插件地址),按照要求调整之后,提测、编译、发布一气呵成,进入今天的午觉模式 (😎)




梦中惊魂


我突然梦见企业微信以每毫秒弹出一个窗口的速度不停的闪烁,周围的人熙熙攘攘,面露忧色,不知道在说些什么...


线上出问题了?和我有什么关系呢(🤪)肯定不是我的问题,不过为了保险起见,还是回忆一下今天都做了什么事吧。


**做了什么?**中台系统上线。**改了什么?**对部分类增加了序列化接口,并增加了serialversionUID... 会导致什么? 接口调用失败...COE...


蹭的一下,我立即从梦中醒来,开始看企业微信,看监控,看接口可用率,看了一切数据正常无误后才逐渐心安。




纳尼?我们不用Java序列化?


回顾自己所了解的关于序列化的知识,打开了各种关于序列化的文章,都给我指向了一个答案:我这种改动铁定会影响序列化,就像下面这样程序会报错。


Exception in thread "main" java.io.InvalidClassException: ser.demo.StuDemo; local class incompatible: stream classdesc serialVersionUID = 6395135316924936201, local class serialVersionUID = 1
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1843)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2000)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
at ser.demo.App.main(App.java:27)

现在线上没报错,只有一种可能,即:我们的RPC框架并没有使用原生的序列化方式。遇事不决架构师,咨询完毕之后果然和我猜测的一样,还从架构师的口中知晓了另外几种序列化方式,比如:MessagePack、Hessian等等。




常见序列化方式


Java序列化


Java序列化就是将一个对象转化为一个二进制表示的字节数组,通过保存或则转移这些二进制数组达到持久化的目的。


要实现序列化,需要实现java.io.Serializable接口,反序列化是和序列化相反的过程,就是把二进制数组转化为对象的过程,在反序列化的时候,必须有原始类的模板才能将对象还原,其核心方法在于以下两个方法,其中Serializable接口起到的作用是标识是否实现序列化、以及前后对象是否一致等作用。


序列化:java.io.ObjectOutputStream#writeObject0


反序列化:java.io.ObjectInputStream#readObject0


以测试类(StuDemo)为例,序列化后的结果如下:


// 序列化
FileOutputStream fos = new FileOutputStream("C:\\Users\\Kerwin\\Desktop\\log\\object.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
StuDemo demo = new StuDemo("Kerwin");
oos.writeObject(demo);
oos.flush();
oos.close();

// 结果如下
//  sr ser.demo.StuDemoX??莅 L namet Ljava/lang/String;xpt Kerwin

一堆乱码,但还是能看出来文件内容大致是指向某一个类,有什么字段、对应的值等信息。




MessagePack 序列化


MessagePack(简写Msgpack)是一个高效的二进制序列化格式,它让你像JSON一样可以在各种语言之间交换数据,但是它比JSON更快、更小。


更快更小就代表着性能更高,它是如何实现的?


Msgpack序列化的时候,字段不会标明Key,仅会按照字段的先后顺序存储,类似数组一样,它的编码方式是类型 + 长度 + 内容,如下所示:


image-20210907041011662.png


这种高效的编码方式就带来一些限制,例如:



  • 服务端不可随意在任意位置增加字段,因为客户端不升级的话会导致反序列化失败

  • 不能使用第三方包提供的集合类工具包作为返回值


使用方式如下:


// 其中 StuDemo 类需要增加 @Message 注解标识需要被MessagePack序列化
// MessagePack 序列化方式不需要依赖 Serializable
public static void main(String[] args) throws IOException {
StuDemo demo = new StuDemo("Kerwin");
MessagePack pack = new MessagePack();

// 序列化
byte[] bytes = pack.write(demo);

// 反序列化
StuDemo res = pack.read(bytes, StuDemo.class);
System.out.println(res.getName());



PS:我司的RPC框架目前就使用的MessagePack序列化方式,也是因为此,所以上述调整 serialVersionUID 时没有发生任何问题
同理,受制于底层序列化的限制,我们的新人文档中也明确提到了上述的限制,比如必须在最末尾增加字段等等。





Hessian2 序列化


Hessian是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架,在Hessian的基础之上,Hessian2的性能和压缩率大大提升。


Hessian会把复杂的对象所有属性存储在一个类似Map的结构中进行序列化,所以在父类、子类中存在同名成员变量的情况下,它先序列化子类,然后序列化父类,因此会导致子类同名成员变量的值被父类覆盖等情况。


它有八大核心设计目标,官网



  • 必须自我描述序列化类型,即不需要外部模式或接口定义

  • 必须与语言无关,包括支持脚本语言

  • 必须在一次传递中可读或可写

  • 必须尽可能紧凑(压缩)

  • 必须简单

  • 必须尽可能快

  • 必须支持Unicode字符串

  • 必须支持8位二进制数据

  • 必须支持加密


使用方式如下:


public class StuHessianDemo implements Serializable {

private static final long serialVersionUID = -640696903073930546L;

private String name;

public StuHessianDemo(String name) {
this.name = name;
}

public String getName() {
return name;
}

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

public static void main(String[] args) throws IOException {
StuHessianDemo hessianDemo = new StuHessianDemo("Kerwin");

ByteArrayOutputStream stream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(stream);
hessianOutput.writeObject(hessianDemo);

ByteArrayInputStream inputStream = new ByteArrayInputStream(stream.toByteArray());

// Hessian的反序列化读取对象
HessianInput hessianInput = new HessianInput(inputStream);
System.out.println(((StuHessianDemo) hessianInput.readObject()).getName());
}

// 结果:Kerwin



选择的依据


由上文我们得知了几种常用的序列化方式,及其优劣,比如MessagePack就是极致的压缩和快,Hessian2则依赖Serializable接口,在保证安全性、自身描述性的基础上,尽可能的追求空间利用率,效率等,而Java序列化方式则一直被诟病,难等大雅之堂,因此在RPC框架选择底层序列化方式时,需要根据自身所需,有所侧重的选择某一项序列化方式。


选择的依据如下,优先级从高到低:


image-20210907051517893.png




一点思考


JSON序列化的地位


其实JSON序列化才是我们最熟知的序列化方式,它本身也不需要实现Serializable接口,为什么大多数RPC框架没有选择用它作为默认的序列化方式呢?


在了解完上文的内容后,我们知道关键还是在性能,效率、空间开销上,因为JSON是一种文本类型序列化框架,采用KEY-VALUE的方式存储数据,它在进行序列化的额外空间开销相对就更大,在反序列化时更不必说,需要依赖反射,因此性能进一步缩水。


然而JSON本身又具备极强的可读性、因此被作为Web中HTTP协议的事实标准。




为什么还要自定义 serialVersionUID


在《Effect Java》中有一句提到:


不管你选择了哪种序列化方式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID。


为什么架构师会提醒我实现它?为什么书中也会这么说?


serialVersionUID分解下来全称为:serial Version UID,序列版本UID,每一个可序列化的类都有一个long域中显式地指定该编号,如果编码者未定义的话,系统就会对这个类的结构运用一个加密的散列函数(SHA-1),从而在运行时自动产生该标识号,该编号会受类名称、接口名称、公有及受保护的成员变量所影响,一旦有相关改动例如增加一个不重要的公有方法即会影响UID,导致异常发生。


因此这是一个习惯问题,也是为了避免潜在风险。




总结


截止到这里,我们了解了原来之前学习到的Java序列化是那么的不实用(甚至到了被吐槽的地步),也知晓了一些框架使用注意事项底层的秘密(比如MsgPack增加字段),下面是关于序列化的一些小建议:



  1. 无论是否依赖Serializable,接口出参都建议实现序列化接口。

  2. 如果实现了序列化接口,务必自行实现serialVersionUID。

  3. 接口出参对象不宜使用特殊的数据类型(如MsgPack第三方集合等)、过于复杂的结构(继承等),不然会导致很多莫名其妙的问题发生。

  4. 当发生服务端/客户端数据不一致时,第一时间想到是序列化问题,并针对当前序列化方式的特点,仔细排查。

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

kafka!还好我留了一手

本文面试情节虚假,但知识真实,请在家人或者朋友的陪同下仔细观看,防止在观看的过程发呆、走神导致没学到知识。性能篇一位身穿格子衬衣,头发好似一拳超人的中年人走了过来,没错他就是面试官,他手握简历,若有所思,我当时害怕极了,然后他开口:小伙子啊,我们这边是基础架构...
继续阅读 »

本文面试情节虚假,但知识真实,请在家人或者朋友的陪同下仔细观看,防止在观看的过程发呆、走神导致没学到知识。

性能篇

一位身穿格子衬衣,头发好似一拳超人的中年人走了过来,没错他就是面试官,他手握简历,若有所思,我当时害怕极了,然后他开口:小伙子啊,我们这边是基础架构的中间件组,既然你的简历没提到kafka,那我接下来问问你kafka的知识吧。

:好的,kafka平时看的不多,但也还了解一点,不是特别精通所以没写了。(嘿嘿,我是故意没写的,早就知道你要来这一套,kafka其实是俺最精通的东西了)
面试官捋了捋他那稀疏的胡须:那我们开始吧,先说说kafka的Log文件存在什么地方?
:kafka的topic可以分区,所以Log对应了一个命名形式为topic-partition的文件夹,比如对于一个有两个分区的topic来说,它的log分别存在xxx/topic-1和xxx/topic-2中。
面试官:那按照这样的说法,所以log文件的位置应该就是xxx/topic-1/data.log或者xxx/topic-2/data.log?
:不是的,kafka的log会分段,每个分区文件夹下,其实有很多的log段,它们共同组成了log,每个日志段大小是1G,如果一个日志段写完,会自动写入一个新的段。
面试官:为什么要分段?不分段行不行?
:分段可以很好的维护数据,首先不分段,当查找一条数据的时候会很麻烦,就像在一本没有目录的新华字典里找数据一样,如果分了段我们只要知道数据在哪个段中,然后在对应的段中查找即可。同时由于log是持久化磁盘的,磁盘的空间不可能无穷无尽的,当需要清除一些老数据,通过分段机制,只需要删除较老的数据段即可。
面试官:hold on,hold on~,你说分了段后我们只要知道数据在哪个段中即可,那么我们怎么知道数据在哪个段中的?
:easy,easy~,kafka内部维护一个跳跃表,跳跃表的节点就是每个段的段号。这样当查询数据的时候,先根据跳跃表就可以快速定位到目标数据段。
面试官:跳跃表是可以加速访问,但是每个段的段号是咋确定的?
:kakfa的段号其实就是根据偏移量来的,它代表当前段内偏移量最小的那条数据的offset,比如:

 segment1的段号是200,segment2的段号是500,那么segment1就存储了偏移量200-499的消息。
面试官:嗯嗯,那定位到段后,如何定位到具体的消息,直接遍历吗?
:不是直接遍历,直接遍历效率太低,kafka采用稀疏索引的方式来搜索具体的消息,其实每个log分段后,除了log文件外,还有两个索引文件,分别是.index和.timeindex,

 其中.index就是我说的偏移量索引文件,它不会为每条消息创建索引,它会每隔一个范围区间创建索引,所以称之为稀疏索引。

 比如我们要查找消息6的时候,首先加载稀疏文件索引.index到内存中,然后通过二分法定位到消息5,最后通过消息5指向的物理地址接着向下顺序查找,直至找到消息6。
面试官:那稀疏索引的好处是什么?
:稀疏索引是一个折中的方案,既不占用太多空间,也提供了一定的快速检索能力。
面试官:上面你说到了.timeindex文件,它是干嘛的?
:这和kafka清理数据有着密切的关系,kafka默认保留7天内的数据,对于超过7天的数据,会被清理掉,这里的清理逻辑主要根据timeindex时间索引文件里最大的时间来判断的,如果最大时间与当前时间差值超过7天,那么对应的数据段就会被清理掉。
面试官:说到数据清理,除了你说的根据时间来判断的,还有哪些?
:还有根据日志文件大小和日志起始偏移量的方式,对于日志文件大小,如果log文件(所有的数据段总和)大于我们设定的阈值,那么就会从第一个数据段开始清理,直至满足条件。对于日志起始偏移量,如果日志段的起始偏移量小于等于我们设定的阈值,那么对应的数据段就会被清理掉。
面试官:你知道消息合并吗?如果知道说说消息合并带来的好处。
:了解一点,消息合并就是把多条消息合并在一起,然后一次rpc调用发给broker,这样的好处无疑会减少很多网络IO资源,其次消息会有个crc校验,如果不合并每条消息都要crc,合并之后,多条消息可以一起crc一次。
面试官:那合并之后的消息,什么时候会给broker?
:合并的消息会在缓冲区内,如果缓冲区快满了或者一段时间内没有生产消息了,那么就会把消息发给broker。
面试官:那你知道消息压缩吗?
:知道一点,压缩是利用cpu时间来节省带宽成本,压缩可以使数据包的体积变得更小,生产者负责将数据消息压缩,消费者拿到消息后自行解压。
面试官:所有只有生产者可以压缩?
:不是的,broker也可以压缩,当生产者指定的压缩算法和broker指定压缩算法的不一样的时候,broker会先按照生产者的压缩算法解压缩一下,然后再按照自己的压缩算法压缩一下,这是需要注意的,如果出现这种情况会影响整体的吞吐。还有就是新老版本的问题,如果新老版本的压缩算法不兼容,比如broker版本比较老,不支持新的压缩算法,那么也会发生一样的事情。
面试官:我们知道kafka的消息是要写入磁盘的,磁盘IO会不会很慢?
:是这样的,kafka的消息是磁盘顺序读写的,有关测试结果表明,一个由6块7200r/min的RAID-5阵列组成的磁盘簇的线性(顺序)写入速度可以达到 600MB/s,而随机写入速度只有 100KB/s,两者性能相差6000倍。操作系统可以针对线性读写做深层次的优化,比如预读(read-ahead,提前将一个比较大的磁盘块读入内存)和后写(write-behind,将很多小的逻辑写操作合并起来组成一个大的物理写操作)技术。顺序写盘的速度不仅比随机写盘的速度快,而且也比随机写内存的速度快。
面试官:顺序读写是为了解决了缓慢的磁盘问题,那在网络方面还有其他的优化吗?
:有,零拷贝,在没有零拷贝的时候,消息是这样交互的:

  1. 切到内核态:内核把磁盘数据copy到内核缓冲区
  2. 切到用户态:把内核的数据copy到用户程序
  3. 切到内核态:用户数据copy到内核socket缓冲区
  4. socket把数据copy给网卡

可以发现一份数据经过多次copy,最终兜兜转转又回到了内核态,实属浪费。

当有了零拷贝之后:

  1. 磁盘数据copy到内核缓冲
  2. 内核缓冲把描述符和长度发给socket,同时直接把数据发给网卡

可以发现通过零拷贝,减少了两次copy过程,大大降低了开销。

可靠篇

面试官:(关于性能方面的问的差不多了,接下来换换口味吧),kafka的多消费者模型是怎么做到的?
:如果要支持多个消费者同时消费一个topic,最简单的方式就是把topic复制一份,但这无疑会浪费很多空间,尤其在消费者很多的情况下,

于是kafka设计出一套offset机制,即一份数据,不同的消费者根据位置来获取不同的消息即可。

面试官:那你知道消费者的offset存在哪吗?
:很久以前,是存在zookeeper中的,但是offset需要频繁更新,zookeeper又不适合频繁更新,所以后来就把消费者位移存在了一个叫_consumer_offset的topic中,这个topic会在第一个消费者启动的时候自动创建,默认50个分区,3个副本。
面试官:那你说说这个_consumer_offset里面具体存了什么?
:这里其实主要分为key和value,value可以简单的认为就是我们的消费者位移,关于key,这里要细说下,由于每个消费者都属于一个消费者组,并且每个消费者其实消费的是某个topic的分区,所以通过group-topic-partition就可以关联上对应的消费者了,这也就是key的组成。
面试官:那你能介绍下消费者提交位移的方式吗?
:这里分为自动提交和手动提交。自动提交的话,就不需要我们干预,我们消费完消息后,kafka会自动帮我们提交,手动提交的话,就需要我们在消费到消息后自己主动commit。
面试官:自动提交会有什么问题?
:自动提交的策略是consumer默认每隔5秒提交一次位移,如果consumer在接下来的很长时间内都没有数据消费,那么自动提交策略就会一直提交重复的位移,导致_consumer_offset有很多重复的消息。
面试官:那这有什么解决方案吗?
:有,这种情况的核心问题就是可能会有大量的、重复的位移消息占用存储空间,只要把重复的去掉即可,kafka提供一种类似redis的aofrewrite的功能,叫compact策略,compact是由一个logCleaner线程来完成的,它会把重复的、并且较老的消息清除掉。

面试官:那如果consumer自动重启了,位移没来的及提交咋办?
:这个会造成重复消费,一般业务上需要配合做幂等。
面试官:那手动提交能解决这个问题吗?
:不能,如果我们在业务处理完之后手动提交,但是在还没得及提交的情况下,也发生了重启或者其他原因导致提交不上去,在消费者恢复后也会发生重复消费。
面试官:那如果我是先提交,后处理业务逻辑呢?
:这种情况也不能保证100%没问题,如果提交成功,但是处理业务时出错,正常来说,这时希望重新消费这条数据是不行的,因为已经提交了,除非你重置offset。总之无论哪种方案都不能保证100%的完美,我们需要自己根据业务情况做幂等或者根据log来找到丢失的数据。
面试官:消费者提交消费位移时提交的是是当前消费到的最新消息的offset还是offset+1?
:offset+1。
面试官:从生产者的角度谈谈消息不丢失的看法。
:关于消息丢失问题,kafka的生产者提供了3种策略来供使用者选择,每种策略各有利弊,需要结合业务的实际状况来选择。

  1. 第一种就是生产者不关心消息的情况,只负责发,这种模式无疑速度是最快的,吞吐是最好的,但是可能造成大量的数据丢失,比如在borker出现问题的时候,生产者还不停的发,那么到broker恢复期间的数据都将丢失。
  2. 第二种就是生产者需要所有副本都写入成功,不管是Leader副本还是Follower副本,那么当Follower副本越多,吞吐理论就越差,但是这种模式下,消息是最安全的。
  3. 第三种就是生产者只需要收到Leader副本的ack即可,不用关心Follower副本的写入情况,它是个折中的做法,保证了一定的安全性的同时也不会太影响吞吐。

如果你不在意自己的数据丢失问题,追求吞吐,比如像log这种,可以采用第一种,如果你非常在意自己的数据安全性,那么就选第二种。如果你希望吞吐稍微好点,同时数据又能安全些,建议第三种,但是第三种在Follower副本出现的问题的时候对生产者来说是无法感知的。

面试官:那你说说一个Follower副本如何被选举成Leader的?
:在kafka中有这样几个概念:

  • AR:所有副本集合
  • ISR:所有符合选举条件的副本集合
  • OSR:落后太多或者挂掉的副本集合

AR = ISR + OSR,在正常情况下,AR应该是和ISR一样的,但是当某个Follower副本落后太多或者某个Follower副本节点挂掉了,那么它会被移出ISR放入OSR中,kafka的选举也比较简单,就是把ISR中的第一个副本选举成新的Leader节点。比如现在AR=[1,2,3],1挂掉了,那么ISR=[2,3],这时会选举2为新的Leader。

面试官捋了捋自己左边的刘海:你还有什么要问我的吗?
:老师,请问你会组合拳吗?

 面试官:组合拳我不会,但是等会会有很多人组合过来面你。

未完待续...


作者:假装懂编程
链接:https://juejin.cn/post/7018702635544870948
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

【Java字符串】字符串虽简单,但这些你不一定知道

前言: 字符串是程序开发当中,使用最频繁的类型之一,有着与基础类型相同的地位(字符串不属于基本类型),甚至在 JVM(Java 虚拟机)编译的时候会对字符串做特殊的处理,比如拼加操作可能会被 JVM 直接合成为一个最终的字符串,从而到达高效运行的目的。 1 :...
继续阅读 »

前言:


字符串是程序开发当中,使用最频繁的类型之一,有着与基础类型相同的地位(字符串不属于基本类型),甚至在 JVM(Java 虚拟机)编译的时候会对字符串做特殊的处理,比如拼加操作可能会被 JVM 直接合成为一个最终的字符串,从而到达高效运行的目的。


1 :构造方法:


将字节数组或者字符数组转成字符串。


String s1 = new String();//创建了一个空内容的字符串。

String s2 = null;//s2没有任何对象指向,是一个null常量值。

String s3 = "";//s3指向一个具体的字符串对象,只不过这个字符串中没有内容。

//一般在定义字符串时,不用new。

String s4 = new String("abc");

String s5 = "abc"; 一般用此写法

new String(char[]);//将字符数组转成字符串。

new String(char[],offset,count);//将字符数组中的一部分转成字符串。


2 :一般方法:


    按照面向对象的思想:


2.1 获取:


    2.1.1:获取字符串的长度。length() ;


    2.1.2:指定位置的字符。char charAt(int index);


    2.1.3:获取指定字符的位置。如果不存在返回-1,所以可以通过返回值-1来判断某一个字符不存在的情况。           


 int indexOf(int ch);//返回第一次找到的字符角标

 int indexOf(int ch,int fromIndex); //返回从指定位置开始第一次找到的角标

 int indexOf(String str); //返回第一次找到的字符串角标

int indexOf(String str,int fromIndex);

 int lastIndexOf(int ch);

 int lastIndexOf(int ch,int fromIndex);

 int lastIndexOf(String str);
int lastIndexOf(String str,int fromIndex);


    2.1.4:获取子串。


String substring(int start);//从start位开始,到length()-1为止.
String substring(int start,int end);//从start开始到end为止。//包含start位,不包含end位。
substring(0,str.length());//获取整串


2.2 判断:


    2.2.1:字符串中包含指定的字符串吗?


            boolean contains(String substring);


    2.2.2:字符串是否以指定字符串开头啊?


            boolean startsWith(string);


    2.2.3:字符串是否以指定字符串结尾啊?


            boolean endsWith( string);


    2.2.4:判断字符串是否相同


            boolean equals(string);//覆盖了Object中的方法,判断字符串内容是否相同。


    2.2.5:判断字符串内容是否相同,忽略大小写。


            boolean equalsIgnoreCase(string) ;


2.3 转换:


    2.3.1:通过构造函数可以将字符数组或者字节数组转成字符串。


    2.3.2:可以通过字符串中的静态方法,将字符数组转成字符串。


            static String copyValueOf(char[] );

            static String copyValueOf(char[],int offset,int count);

            static String valueOf(char[]);

            static String valueOf(char[],int offset,int count);


    2.3.3:将基本数据类型或者对象转成字符串。


            static String valueOf(char);

            static String valueOf(boolean);

            static String valueOf(double);

            static String valueOf(float);

            static String valueOf(int);

            static String valueOf(long);

            static String valueOf(Object);


    2.3.4:将字符串转成大小写。


            String toLowerCase();


            String toUpperCase();


    2.3.5:将字符串转成数组。


            char[] toCharArray();//转成字符数组。


            byte[] getBytes();//可以加入编码表。转成字节数组。


    2.3.6:将字符串转成字符串数组。切割方法。


            String[] split(分割的规则-字符串);


    2.3.7:将字符串进行内容替换。注意:修改后变成新字符串,并不是将原字符串直接修改。


            String replace(oldChar,newChar);


            String replace(oldstring,newstring);


    2.3.8: String concat(string); //对字符串进行追加。


            String trim();//去除字符串两端的空格


    int compareTo();//如果参数字符串等于此字符串,则返回值 0;如果此字符串按字典顺序小于字符串参数,则返回一个小于 0 的值;如果此字符串按字典顺序大于字符串参数,则返回一个大于 0 的值。


3.StringBuffer 字符串缓冲区:


构造一个其中不带字符的字符串缓冲区,初始容量为 16 个字符。


特点:


1 :可以对字符串内容进行修改。


2 :是一个容器。


3 :是可变长度的。


4 :缓冲区中可以存储任意类型的数据。


5 :最终需要变成字符串。


容器通常具备一些固定的方法:


1 ,添加。


    StringBuffer append(data):在缓冲区中追加数据。追加到尾部。


    StringBuffer insert(index,data):在指定位置插入数据。


2 ,删除。


    StringBuffer delete(start,end);删除从start至end-1范围的元素


    StringBuffer deleteCharAt(index);删除指定位置的元素


//sb.delete(0,sb.length());//清空缓冲区。


3 ,修改。


     StringBuffer replace(start,end,string);将start至end-1替换成string


    void setCharAt(index,char);替换指定位置的字符


    void setLength(len);将原字符串置为指定长度的字符串


4 ,查找。 (查不到返回-1)


    int indexOf(string); 返回指定子字符串在此字符串中第一次出现处的索引。

    int indexOf(string,int fromIndex);从指定位置开始查找字符串

    int lastIndexOf(string); 返回指定子字符串在此字符串中最右边出现处的索引。

    int lastIndexOf(string,int fromIndex); 从指定的索引开始反向搜索


5,获取子串。


    string substring(start); 返回start到结尾的子串


    string substring(start,end); 返回start至end-1的子串


6 ,反转。


    StringBuffer reverse();字符串反转


4. StringBuilder 字符串缓冲区:


JDK1.5 出现StringBuiler; 构造一个其中不带字符的字符串生成器,初始容量为 16 个字符。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。


方法和StringBuffer一样;


5.StringBuffer 和 StringBuilder 的区别:


StringBuffer 线程安全。


StringBuilder 线程不安全。


单线程操作,使用StringBuilder 效率高。


多线程操作,使用StringBuffer 安全。


        StringBuilder sb = new StringBuilder("abcdefg");

        sb.append("ak");  //abcdefgak

        sb.insert(1,"et");//aetbcdefg

        sb.deleteCharAt(2);//abdefg

        sb.delete(2,4);//abefg

        sb.setLength(4);//abcd

        sb.setCharAt(0,'k');//kbcdefg

        sb.replace(0,2,"hhhh");//hhhhcdefg
//想要使用缓冲区,先要建立对象。

        StringBuffer sb = new StringBuffer();     

        sb.append(12).append("haha");//方法调用链。

        String s = "abc"+4+'q';

        s = new StringBuffer().append("abc").append(4).append('q').toString();


class  Test{

    public static void main(String[] args) {

        String s1 = "java";

        String s2 = "hello";

        method_1(s1,s2);

        System.out.println(s1+"...."+s2); //java....hello

       

        StringBuilder s11 = new StringBuilder("java");

        StringBuilder s22 = new StringBuilder("hello");

        method_2(s11,s22);

        System.out.println(s11+"-----"+s22); //javahello-----hello

    }

    public static void method_1(String s1,String s2){

        s1.replace('a','k');

        s1 = s2;

    }

    public static void method_2(StringBuilder s1,StringBuilder s2){

        s1.append(s2);

        s1 = s2;

    }

}

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

完蛋,公司被一条 update 语句干趴了!

sql
大家好,我是小林。 昨晚在群划水的时候,看到有位读者说了这么一件事。 大概就是,在线上执行一条 update 语句修改数据库数据的时候,where 条件没有带上索引,导致业务直接崩了,被老板教训了一波 这次我们就来看看: 为什么会发生这种的事故? 又该如何...
继续阅读 »

大家好,我是小林。


昨晚在群划水的时候,看到有位读者说了这么一件事。


在这里插入图片描述


大概就是,在线上执行一条 update 语句修改数据库数据的时候,where 条件没有带上索引,导致业务直接崩了,被老板教训了一波


这次我们就来看看:



  • 为什么会发生这种的事故?

  • 又该如何避免这种事故的发生?


说个前提,接下来说的案例都是基于 InnoDB 存储引擎,且事务的隔离级别是可重复读。


为什么会发生这种的事故?


InnoDB 存储引擎的默认事务隔离级别是「可重复读」,但是在这个隔离级别下,在多个事务并发的时候,会出现幻读的问题,所谓的幻读是指在同一事务下,连续执行两次同样的查询语句,第二次的查询语句可能会返回之前不存在的行。


因此 InnoDB 存储引擎自己实现了行锁,通过 next-key 锁(记录锁和间隙锁的组合)来锁住记录本身和记录之间的“间隙”,防止其他事务在这个记录之间插入新的记录,从而避免了幻读现象。


当我们执行 update 语句时,实际上是会对记录加独占锁(X 锁)的,如果其他事务对持有独占锁的记录进行修改时是会被阻塞的。另外,这个锁并不是执行完 update 语句就会释放的,而是会等事务结束时才会释放。


在 InnoDB 事务中,对记录加锁带基本单位是 next-key 锁,但是会因为一些条件会退化成间隙锁,或者记录锁。加锁的位置准确的说,锁是加在索引上的而非行上。


比如,在 update 语句的 where 条件使用了唯一索引,那么 next-key 锁会退化成记录锁,也就是只会给一行记录加锁。


这里举个例子,这里有一张数据库表,其中 id 为主键索引。



假设有两个事务的执行顺序如下:


在这里插入图片描述


可以看到,事务 A 的 update 语句中 where 是等值查询,并且 id 是唯一索引,所以只会对 id = 1 这条记录加锁,因此,事务 B 的更新操作并不会阻塞。


但是,在 update 语句的 where 条件没有使用索引,就会全表扫描,于是就会对所有记录加上 next-key 锁(记录锁 + 间隙锁),相当于把整个表锁住了


假设有两个事务的执行顺序如下:



可以看到,这次事务 B 的 update 语句被阻塞了。


这是因为事务 A的 update 语句中 where 条件没有索引列,所有记录都会被加锁,也就是这条 update 语句产生了 4 个记录锁和 5 个间隙锁,相当于锁住了全表。



因此,当在数据量非常大的数据库表执行 update 语句时,如果没有使用索引,就会给全表的加上 next-key 锁, 那么锁就会持续很长一段时间,直到事务结束,而这期间除了 select ... from 语句,其他语句都会被锁住不能执行,业务会因此停滞,接下来等着你的,就是老板的挨骂。


那 update 语句的 where 带上索引就能避免全表记录加锁了吗?


并不是。


关键还得看这条语句在执行过程种,优化器最终选择的是索引扫描,还是全表扫描,如果走了全表扫描,就会对全表的记录加锁了


又该如何避免这种事故的发生?


我们可以将 MySQL 里的 sql_safe_updates 参数设置为 1,开启安全更新模式。



官方的解释:



If set to 1, MySQL aborts UPDATE or DELETE statements that do not use a key in the WHERE clause or a LIMIT clause. (Specifically, UPDATE statements must have a WHERE clause that uses a key or a LIMIT clause, or both. DELETE statements must have both.) This makes it possible to catch UPDATE or DELETE statements where keys are not used properly and that would probably change or delete a large number of rows. The default value is 0.


大致的意思是,当 sql_safe_updates 设置为 1 时。


update 语句必须满足如下条件之一才能执行成功:



  • 使用 where,并且 where 条件中必须有索引列;

  • 使用 limit;

  • 同时使用 where 和 limit,此时 where 条件中可以没有索引列;


delete 语句必须满足如下条件之一才能执行成功:



  • 使用 where,并且 where 条件中必须有索引列;

  • 同时使用 where 和 limit,此时 where 条件中可以没有索引列;


如果 where 条件带上了索引列,但是优化器最终扫描选择的是全表,而不是索引的话,我们可以使用 force index([index_name]) 可以告诉优化器使用哪个索引,以此避免有几率锁全表带来的隐患。


总结


不要小看一条 update 语句,在生产机上使用不当可能会导致业务停滞,甚至崩溃。


当我们要执行 update 语句的时候,确保 where 条件中带上了索引列,并且在测试机确认该语句是否走的是索引扫描,防止因为扫描全表,而对表中的所有记录加上锁。


我们可以打开 MySQL sql_safe_updates 参数,这样可以预防 update 操作时 where 条件没有带上索引列。


如果发现即使在 where 条件中带上了列索引列,优化器走的还是全标扫描,这时我们就要使用 force index([index_name]) 可以告诉优化器使用哪个索引。


这次就说到这啦,下次要小心点,别再被老板挨骂啦。


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

庆祝神舟十三号发射成功,来一个火箭发射动画

前言 北京时间10月16日0时23分,神舟十三号飞船成功发射,目前三名航天员已经顺利进驻空间站,开始为期6个月的“太空差旅”生活。 国家的航天技术的突飞猛进也让岛上码农很自豪,今天看 Flutter 的动画知识,看到了 AnimatedPositioned ...
继续阅读 »

前言


北京时间10月16日0时23分,神舟十三号飞船成功发射,目前三名航天员已经顺利进驻空间站,开始为期6个月的“太空差旅”生活。
image.png
国家的航天技术的突飞猛进也让岛上码农很自豪,今天看 Flutter 的动画知识,看到了 AnimatedPositioned 这个组件,可以用于控制组件的相对位置移动。结合这个神舟十三号的发射,灵机一动,正好可以使用AnimatedPositioned 这个组件实现火箭发射动画。话不多说,先上效果!
火箭发射动画.gif


效果说明


这里其实是两张图片叠加,一张是背景地球星空的背景图,一张是火箭。火箭在发射过程中有两个变化:



  • 高度越来越高,其实就是相对图片背景图底部的位置越来越大就可以实现;

  • 尺寸越来越小,这个可以控制整个组件的尺寸实现。


然后是动画取消的选择,火箭的速度是越来越快,试了几个 Flutter 自带的曲线,发现 easeInCubic 这个效果挺不错的,开始慢,后面越来越快,和火箭发射的过程是类似的。


AnimatedPositioned介绍


AnimatedPositioned组件的使用方式其实和 AnimatedContainer 类似。只是AnimatedPositionedPositioned 组件的替代。构造方法定义如下:


const AnimatedPositioned({
Key? key,
required this.child,
this.left,
this.top,
this.right,
this.bottom,
this.width,
this.height,
Curve curve = Curves.linear,
required Duration duration,
VoidCallback? onEnd,
})

前面的参数和 Positioned 一样,后面是动画控制参数,这些参数的定义和 AnimatedContainer 的是一样的:



  • curve:动画效果曲线;

  • duration:动画时长;

  • onEnd:动画结束后回调。


我们可以改变 lefttopwidth等参数来实现动画过渡的效果。比如我们的火箭发射,就是修改 bottom (飞行高度控制)和 width (尺寸大小控制)来实现的。


火箭发射动画实现


有了上面的两个分析,火箭发射动画就简单了!完整代码如下:


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

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

class _RocketLaunchState extends State<RocketLaunch> {
var rocketBottom = -80.0;
var rocketWidth = 160.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('火箭发射'),
brightness: Brightness.dark,
backgroundColor: Colors.black,
),
backgroundColor: Colors.black,
body: Center(
child: Stack(
alignment: Alignment.bottomCenter,
children: [
Image.asset(
'images/earth.jpeg',
height: double.infinity,
fit: BoxFit.fill,
),
AnimatedPositioned(
child: Image.asset(
'images/rocket.png',
fit: BoxFit.fitWidth,
),
bottom: rocketBottom,
width: rocketWidth,
duration: Duration(seconds: 5),
curve: Curves.easeInCubic,
),
],
),
),
floatingActionButton: FloatingActionButton(
child: Text(
'发射',
style: TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
),
onPressed: () {
setState(() {
rocketBottom = MediaQuery.of(context).size.height;
rocketWidth = 40.0;
});
},
),
);
}
}

其中一开始设置 bottom 为负值,是为了隐藏火箭的焰火,这样会更有感觉一些。然后就是在点击发射按钮的时候,通过 setState 更改底部距离和火箭尺寸就可以搞定了。


总结


通过神舟十三飞船发射,来一个火箭动画是不是挺有趣?其实这篇主要的知识点还是介绍 AnimatedPositioned 的应用。通过 AnimatedPositioned可以实现很多层叠组件的相对移动变化效果,比如进度条的滑块,滑动开关等。各位 Flutter 玩家也可以利用 AnimatedPositioned 这个组件自己来玩一下好玩的动画效果哦!


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

iOS SwiftUI 框架集成 1.1

第三节 在SwiftUI视图的状态下跟踪页面如果要添加一个自定义的UIPageControl控件,就需要一种方式能够在PageView中跟踪当前展示的页面。这就需要在PageView中声明一个@State属性,并传递一个针对该属性的绑定关系给PageViewC...
继续阅读 »

第三节 在SwiftUI视图的状态下跟踪页面

如果要添加一个自定义的UIPageControl控件,就需要一种方式能够在PageView中跟踪当前展示的页面。这就需要在PageView中声明一个@State属性,并传递一个针对该属性的绑定关系给PageViewController视图,在PageViewController中通过绑定关系更新状态属性,来反映当前展示的页面。

section 3

步骤1 在PageViewController中添加一个绑定属性currentPage。除了使用关键字@Binding声明属性为绑定属性外,还需要更新一下函数setViewControllers(_:direction:animated:),给它传入currentPage绑定属性

section 3 step 1

做到这一步还不能正常运行,继续进行下一步。

步骤2 在PageView中声明@State变量,并在创建PageViewController时把绑定属性传入。注意使用$语法创建一个针对状态变量的绑定关系。

section 3 step 2

步骤3 通过改变PageView视图中的currentPage初始值来测试绑定关系是否正常生效。也可以做一个测试按钮,点击按钮时让第二个页面展示出来

section 3 step 3

步骤4 添加一个TextView控件来展示状态变量currentPage的值,拖动页面切换时观察TextView上的值,目前不会发生变化。因为PageViewController内部没有在切换页面的过程中更新currentPage的值。

section 3 step 4

步骤5 在PageViewController.swift中让coordinator作为UIPageViewController的代理,并添加pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted completed: Bool) 方法。因为SwiftUI在页面切换动画完成时会调用这个方法,这样就可以这个方法内部获取当前正在展示的页面的下标,并同时更新绑定属性currentPage的值。

section 3 step 5

步骤6 coordinator除了是UIPageViewController数据源外,再把它赋值为UIPageViewController的代理。由于绑定关系是双向的,所以当页面切换时,PageView视图上的Text就会实时展示当前的页码。

section 3 step 6

section 3 step 6 gif

第四节 添加一个自定义PageControl

我们已经为包裹在UIViewRepresentable视图中的子视图上添加了一个自定义UIPageControl

section 4

步骤1 创建一个新的SwiftUI视图,命名为PageControl.swift,并使用PageControl类型遵循UIViewRepresentable协议。UIViewRepresentableUIViewControllerRepresentable类型有相同的生命周期,在UIKit类型中都有对应的生命周期方法。

section 4 step 1

步骤2 在PageView中用PageControl替换Text,并把VStack换成ZStack。因为总页数和当前页面都已经传入PageControl,所以PageControl已经可以正确的显示。

section 4 step 2

下一步要处理PageControl与用户的交互,让它可以被用户点击任意一边进行页面间的切换。

步骤3 在PageControl中创建一个嵌套类型Coordiantor,添加一个makeCoordinator()方法创建并返回一个coordinator实例。因为UIControl子类(包括UIPageControl)使用Target-Action模式,Coordinator实现一个@objc方法来更新currentPage绑定属性的值。

section 4 step 3

步骤4 把coordinator作为PageControl值改变事件的目标处理器,并指定updateCurrentPage(sender:)方法为处理函数

section 4 step 4

步骤5 现在就可以尝试PageControl的各种交互来切换页面,PageView展示了SwiftUIUIKit视图如何混合使用。

section 4 step 5 gif

检查是否理解

问题1 下面哪个协议可以用来把UIKit中的视图控件器桥接进SwiftUI

  •  UIViewRepresentable
  •  UIHostingController
  •  UIViewControllerRepresentable

问题2 对于UIViewControllerRepresentable类型,下面哪个方法可以为它创建一个代理或数据源?

  •  在makeUIViewController(context:)方法中创建UIViewController实例的地方
  •  在UIViewControllerRepresentable类型的初始化器中
  •  在makeCoordinator()方法中
收起阅读 »

iOS SwiftUI 框架集成 1.0

框架集成混合使用SwiftUI框架和平台相关的其它UI框架(视图和视图控制器)包含章节与UIKit交互创建watchOS应用创建macOS应用与UIKIT交互SwiftUI可以在苹果全平台上无缝兼容现有的UI框架。例如,可以在SwiftUI视图中嵌入UIKit...
继续阅读 »

框架集成

混合使用SwiftUI框架和平台相关的其它UI框架(视图和视图控制器)

framework and integeration

包含章节

与UIKIT交互

SwiftUI可以在苹果全平台上无缝兼容现有的UI框架。例如,可以在SwiftUI视图中嵌入UIKit视图UIKit视图控制器,反过来在UIKit视图UIKit视图控制器中也可以嵌入SwiftUI视图。

本篇教程展示如何把landmark应用的主页混合使用UIPageViewControllerUIPageControl。使用UIPageViewController来展示由SwiftUI视图构成的轮播图,使用状态变量和绑定来操作用户界面数据的更新。

跟着教程一步步走,可以下载工程文件进行实践。


第一节 创建一个用来展示UIPageViewController的SwiftUI视图

为了在SwiftUI视图中展示UIKit视图和UIKit视图控制器,需要创建遵循UIViewRepresentableUIViewControllerRepresentable协议的类型。创建的自定义视图类型,用来创建和配置所要展示的UIKit类型,SwiftUI框架来管理UIKIt类型的生命周期并在适当的时机更新它们。

section 1

步骤1 创建一个新的SwiftUI视图文件,命名为PageViewController.swift,并且声明PageViewController类型遵循UIViewControllerRepresentable。这个页面视图控制器存放一个UIViewController实例数组,数组中的每一个元素代表在地标滚动过程中的一页视图。

section 1 step 1

下一步添加UIViewControllerRepresentable协议的两个实现, 目前因为协议方法没有完成实现,会有报错提示。

步骤2 添加一个makeUIViewController(context:)方法,方法内部以指定的配置创建一个UIPageViewControllerSwiftUI会在准备显示视图时调用一次makeUIViewController(context:)方法创建UIViewController实例,并管理它的生命周期。

section 1 step 2

由于还缺少一个协议方法没有实现,所以目前还是会报错。

步骤3 添加updateUIViewController(_:context:)方法,这个方法里调用setViewControllers(_:direction:animated:)方法展示数组中的第一个视图控制器

section 1 step 3

创建另一个SwiftUI视图展示遵循UIViewControllerRepresentable协议的视图

步骤4 创建一个名为PageView.swift的视图,声明一个PageViewController作为子视图。初始化时使用一个视图数组来初始化,并把每一个视图都嵌入在一个UIHostingController中。UIHostingController是一个UIViewController的子类,用来在UIKit环境中表示一个SwiftUI视图。

section 1 step 4

步骤5 更新预览视图,并传入视图数组,预览视图就会开始工作了

section 1 step 5

步骤6 在继续下面的步骤前,先把PageView的预览视图固定住,以避免在文件切换时不能实现预览到PageView的改变。

section 1 step 6

第二节 创建视图控制器的数据源

短短几个步骤就做了很多事,PageViewController使用UIPageViewController去展示来自SwiftUI内容。现在是时候添加挥动手势进行页面之间的翻动了。

section 2

一个展示UIKit视图控制器的SwiftUI视图可以定义一个Coordinator类型,这个Coordinator类型由SwitUI管理,用来作为视图展示的环境

步骤1 在PageViewControlelr中定义一个嵌套类型CoordiantorSwiftUI管理UIViewController Representable类型的coordinator,并在调用方法时把它作为环境的一部分。

section 2 step 1

步骤2 在PageView Controller中添加另一个方法,创建coordinatorSwiftUI在调用makeUIViewController(context:)前会先调用makeCoordinator()方法,因此在配置视图控制器时是可以访问到coordiantor对象的。可以使用coordinator为实现通用的Cocoa模式,例如:代理模式数据源以及目标-动作

section 2 step 2

步骤3 让Coordinator类型添加UIPageViewControllerDataSource协议遵循,并且实现两个必要方法。这两个必要方法会建立起视图控制器之间的联系,因此可以实现页面之前的前后切换。

section 2 step 3

步骤4 把coordiantor作为UIPageViewController的数据源

section 2 step 4

步骤5 打开实时预览,并测试一下前后页面切换的功能是否正常

swipe landmarks

收起阅读 »

iOS SwiftUI 应用设计与布局 1.2

玩转UI控件在Landmarks应用中,用户可以创建一个简介来描述他们自已的个人情况。为了让用户可以编辑自己的简介,我们需要添加一个编辑模式并设计一个偏好设置界面。这里使用多种通用控件来展示用户的各种数据,并在用户保存他们所做的数据修改时更新地标数据模型。按照...
继续阅读 »

玩转UI控件

Landmarks应用中,用户可以创建一个简介来描述他们自已的个人情况。为了让用户可以编辑自己的简介,我们需要添加一个编辑模式并设计一个偏好设置界面。

这里使用多种通用控件来展示用户的各种数据,并在用户保存他们所做的数据修改时更新地标数据模型。

按照步骤在下面的项目工程中一步步进行实践。


第一节 展示用户简介

Landmarks应用在本地存储了一些配置和用户偏好设置。在用户编辑这些数据前,会被展示在一个没有编辑按钮的概要视图上。

secion 1

步骤1

在项目文件导航栏的Landmarks文件组下面新建一个名为Profile的文件组,并在这个新建的文件组下面添加一个新视图ProfileHost, 这个新视图包含一个TextView,用来展示用户名称。ProfileHost将会展示静态概要信息,同时支持编辑模式

secion 1 step 2

步骤2 用步骤1创建的ProfileHost替换Home.swift中的静态文本Text视图。现在主页中的profile按钮点击时可以调起一个用户简介页面了

secion 1 step 2

步骤3 创建一个新的视图命名为ProfileSummary,它会持有一个Profile实例,并显示一些用户的基本信息。Profile概要视图持有一个Profile对像的原因是,因为它的父视图ProfileHost管理着视图的状态,它不能与Profile进行绑定。

secion 1 step 3

步骤4 更新ProfileHost文件,显示新的概要视图

secion 1 step 4

步骤5 创建一个名为HikeBadge的新视图,这个新视图由Badge视图和一些描述性文字构成。Badge仅仅是一个图形,在HikeBadge视图中的文本与accessibility(label:)属性修改器一起,可以让这个徽章对用户更加清晰。注意frame(width:height:)的两种不同的用法用来配置徽章以不同的缩放尺寸显示。

secion 1 step 5

步骤6 更新ProfileSummary文件,添加几个不同的徽章代表用户得到的不同徽章

secion 1 step 6

步骤7 把HikeView包含在ProfileSummary页面中后,就完成了第一节的实践内容了。

secion 1 step 7

第二节 添加编辑模式

用户需要能够在浏览模式和编辑模式之间进行切换来查看或者修改用户简介的信息。通过在ProfileHost上添加一个Edit Button,然后创建一个用来编辑简介信息的页面。

secion 2

步骤1 添加一个Enviornment视图属性,用来使用\.edit模式。可以使用这个属性来读写当前编辑模式。

secion 2 step 1

步骤2 创建一个编辑按钮,可以切换编辑模式

secion 2 step 2

步骤3 更新UserData类,包含一个Profile实例,即使用户简介页面消失后也可以存储编辑后的信息

secion 2 step 3

步骤4 从环境变量中读取用户简介信息,并把数据传递给ProfileHost视图的控件上进行展示。为了在编辑状态下修改简介信息后确认修改前避免更新全局状态(例如在编辑用户名的过程中),编辑视图在一个备份属性中进行相应的修改操作,确认修改后,才把备份属性同步到全局应用状态中。

secion 2 step 4

步骤5 添加一个条件视图,可以用来显示静态用户简介视图或者是用户简介视图的编辑模式。当前的编辑模式只支持静态文本框的编辑。

secion 2 step 5

第三节 定义简介编辑器

用户简介编辑器包含几个单独的控件用来修改对应简介信息。在简介中,一些项例如徽章是不可以编辑修改的,所以它们不会出现在简介编辑器中。为了保持简介在编辑模式和浏览模式的一致性,需要按照简介页面各项相同的顺序进行添加。

步骤1 创建一个名为ProfileEditor的新视图,并绑定用户简介中的草稿。视图中的第一个控件是TextField,用来更新用户名字段值。创建TextField时要提供一个标签和一个绑定字符串。

secion 3 step 1

步骤2 更新ProfileHost中的条件内容,让它包含条件编辑器并把简单的绑定关系传递给简介编辑器。现在当你点击Edit按钮,简介视图就会变成编辑模式了。

secion 3 step 2

步骤3 添加一个切换开关,用来设置用户是否接收相关地标事件的推送通知。这个Toggle控件打开和关闭正好对应着布尔值的truefalse

secion 3 step 3

步骤4 把一个Picker和一个Text放在VStack结构里,让这个地标可以选择不同季节。

secion 3 step 4

步骤5 最后,在季节图片选择器下方添加一个DatePicker,用来修改地标的目标浏览日期

secion 3 step 5

第四节 延迟编辑传播

在编辑模式时,使用用户简介信息的备份进行修改,当用户确认进行修改后,再用修改的备份信息覆盖真正的用户信息。直到用户退出编辑模式前都不让编辑的备份生效。

secion 4

步骤1 在ProfileHost视图上添加一个取消按钮。不像编辑模式按钮提供的完成按钮,取消按钮不会应用修改后的简介备份信息到实际的简介数据上。

secion 4 step 1

步骤2 当用户点击完成按钮后,使用onAppear(perform:)onDisappear(perform:)来更新或保存用户简介数据。下一次进入编辑模式时,使用上一次的用户简介数据来展示。

secion 4 step 2

检查是否理解

问题1 编辑状态改变时,怎样更新一个视图,例如,当用户编辑了用户简介信息后点击完成按钮的情况下,是怎么更新一个视图的

  • problem 1 answer 1
  • problem 1 answer 2
  • problem 1 answer 3

问题2 什么情况下需要添加一个accessiblity标签,使用accessibility(label:)修改器?

  •  在应用的每一个视图都添加一个accessibility标签
  •  当可以让用户界面元素对用户变的更清晰时,添加一个accessibility标签
  •  只有当你没有给视图清加tag时才可以使用accessibility(label:)

问题3 模态和非模态视图展示有什么差别?

  •  当模态展示一个视图时,源视图设置目标视图的编辑模式
  •  当非模态展示一个视图时,目标视图会盖住源视图并且替代当前的导航栈
  •  当模态展示一个视图时,目标视图盖住源视图并替换当前导航栈
收起阅读 »

iOS SwiftUI 应用设计与布局 1.1

第四节 组合首页Landmarks应用的首页在用户点击查看地标详情前需要先把地标的一些简单信息展示出来。复用之前创建的视图构建具体某一类别地标的行视图步骤1 在CategoryRow.swift文件中,与CategoryRow类型并列,创建一个新的自...
继续阅读 »

第四节 组合首页

Landmarks应用的首页在用户点击查看地标详情前需要先把地标的一些简单信息展示出来。复用之前创建的视图构建具体某一类别地标的行视图

section 4

步骤1 在CategoryRow.swift文件中,与CategoryRow类型并列,创建一个新的自定义视图类型CategoryItem,用这个新的视图类型替换CategoryRow的地标名称Text控件

section 4 step 1

步骤2 在CategoryHome.swift中,添加一个名为FeaturedLandmarks的简单视图,这个视图用来显示地标数据中isFeatured属性为真的那些地标。在之后的教程中,会把FeaturedLandmarks这个视图修改成一个交互式轮播图。目前,这个视图仅仅展示一张缩放和剪裁后的地标图片。

section 4 step 2

步骤3 把视图的边距设置为0,让展示内容可以尽量贴着屏幕边沿

section 4 step 3

第五节

现在所有类别的地标都可以在首页视图中展示出来,用户还需要能够进入应用其它页面的方法。使用页面导航和相关API来实现用户从应用首页到地标详情页、收藏列表页及用户个人中心页的跳转。

section 5

步骤1 在CategoryRow.swift中,把CategoryItem视图包裹在NavigationLink视图中。CategoryItem这时做为跳转按钮的内容,destination指定点击NavigationLink按钮时要跳转的目标视图。

section 5 step 1

section 5 step 1 gif

步骤2 使用renderingMode(_:)foregroundColor(_:)这两个属性修改器来改变地标类别项的导航样式。做为NavigationLink标签的CategoryItem中的文本会使用Environment中的强调颜色,图片可能以模板图片的方式渲染,这些都可以使用属性修改器来调整,达到最佳效果。

section 5 step 2

步骤3 在CategoryHome.swift中,添加一个模态展示的用户信息展示页,点击了用户图标时弹出展示。当状态showProfile被置为true时,展示用户信息页,当showProfile状态置为false时,用户信息页消失。

section 5 step 3

步骤4 在导航条上添加一个按钮,用来切换showProfile状态的值:true或者false

section 5 step 4

section 5 step 4 gif

步骤5 在CategoryHome.swift中添加一个跳转链接,点击时跳转到全部地标的筛选页面。

section 5 step 5

section 5 step 5 gif

步骤6 把LandmarkList.swift中的把包裹地标列表视图的NavigationView移动到对应的预览视图中。因为在应用中,LandmarkList总是会被展示在CategoryHome.swift定义的导航视图中。

section 5 step 6

检查是否理解

问题1 对于Landmarks这个应用来说,哪一个视图是它的根视图?

  •  SceneDelegate
  •  Landmarks
  •  CategoryHome

问题2 CategoryHome这个视图是如何与应用的其它视图联动起来的

  •  在不同地标之间复用图片资源
  •  与其它视图使用一致的命名规范和属性修改器语法
  •  使用导航结构把地标应用中所有视图连接在一起


收起阅读 »