注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

React-Native 20分钟入门指南

背景为什么需要React-Native?在React-Native出现之前移动端主流的开发模式是原生开发和Hybrid开发(H5混合原生开发),Hybrid app相较于native app的优势是开发成本低开发速度快(H5页面开发跨平台,无需重新写web、a...
继续阅读 »

背景

为什么需要React-Native?

在React-Native出现之前移动端主流的开发模式是原生开发和Hybrid开发(H5混合原生开发),Hybrid app相较于native app的优势是开发成本低开发速度快(H5页面开发跨平台,无需重新写web、android、ios代码),尽管native app在开发上需要更多时间,但却带来了更好的用户体验(页面渲染、手势操作的流畅性),也正是基于这两点Facebook在2015年推出了React-Native

What we really want is the user experience of the native mobile platforms, combined with the developer experience we have when building with React on the web.

上文摘自React-Native发布稿,React-Native的开发既保留了React的开发效率又拥有媲美原生的用户体验,其运行原理并非使用webview所以不属于Hybrid开发,想了解的可以查看React Native运行原理解析这篇文章。React-Native提出的理念是‘learn once,write every where’,之所以不是‘learn once, run every where’,是因为不同平台的用户体验有所不同,因此要运行全平台仍需要一些额外的适配,这里是Occhino对React-Native的介绍。

React-Native在Github的Star数

React-Native的npm下载数

上面两张图展示了React-Native的对于开发者的热门程度,且官方对其的开发状态一直更新,这也是其能抢占原生开发市场的重要因素。

搭建开发环境

在创建项目前我们需要先搭建React-Native所需的开发环境。
第一步需要先安装nodejs、python2、jdk8(windows有所不同,推荐使用macos开发,轻松省事)

brew install node //macos自带python和jdk

第二步安装React Native CLI

npm install -g react-native-cli

第三步安装Android Studio,参考官方的开发文档

创建第一个应用

使用react-native命令创建一个名为HelloReactNative的项目

react-native init HelloReactNative

等待其下载完相关依赖后,运行项目

react-native run-ios
or
react-native run-android

成功运行后的出现的界面是这样的

react-native-helloworld.png

基本的JSX和ES6语法

先看一下运行成功后的界面代码

/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/


import React, {Component} from 'react';
import {
Platform,
StyleSheet,
Text,
View
} from 'react-native';

const instructions = Platform.select({
ios: 'Press Cmd+R to reload,\n' +
'Cmd+D or shake for dev menu',
android: 'Double tap R on your keyboard to reload,\n' +
'Shake or press menu button for dev menu',
});

//noinspection BadExpressionStatementJS
type
Props = {};
//noinspection JSAnnotator
export default class App extends Component<Props> {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Text style={styles.instructions}>
To get started, edit App.js
</Text>
<Text style={styles.instructions}>
{instructions}
</Text>
</View>
);
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});

代码中出现的importexportextendsclass以及未出现的() =>箭头函数均为ES6需要了解的基础语法,import表示引入需要的模块,export表示导出模块,extends表示继承自某个父类,class表示定义一个类,()=>为箭头函数,用此语法定义的函数带有上下文信息,因此不必再处理this引用的问题。
<Text style={styles.welcome}>Welcome to React Native!</Text>这段代码是JSX语法使用方式,和html标记语言一样,只不过这里引用的是React-Native的组件,Text是一个显示文本的组件,可以看到style={styles.welcome}这是JSX的另一个语法可以将有效的js表示式放入大括号内,Welcome to React Native!为其内容文本,可以尝试修改他的内容为Hello React Native!,刷新界面后

react-native-text.png



熟悉更多的ES6语法有助于更有效率的开发。

组件的属性和状态

在了解了一些基本的JSX和ES6语法后,我们还需要了解两个比较重要的概念即propsstateprops为组件的属性,state为组件的状态,两者间的区别在于,props会在组件被实例化时通过构造参数传入,所以props的传递为单向传递,且只能由父组件控制,state为组件的内部状态由组件自己管理,不受外界影响。propsstate都能修改组件的状态,两者的改变会导致相关引用的组件状态改变,也就是说在组件的内部存在子组件引用了propsstate,那么当发生改变时相应子组件会重新渲染,其实这里也可以看出propsstate的使用联系,父组件可以通过setState修改state,并将其传递到子组件的props中使子组件重新渲染从而使父组件重新渲染。

组件生命周期

image

组件的生命周期会经历三个阶段

Mounting:挂载
Updating:更新
Unmounting:移除

对应的生命周期回调方法为

componentWillMount()//组件将要挂载时调用
render()//组件渲染时调用
componentDidMount()//组件挂载完成时调用
componentWillReceiveProps(object nextProps)//组件props和state改变时调用
shouldComponentUpdate(object nextProps,object nextState)//返回false不更新组件,一下两个方法不执行
componentWillUpdate(object nextProps,object nextState)//组件将要更新时调用
componentDidUpdate(object nextProps,object nextState)//组件完成更新时调用
componentWillUnmount()//组件销毁时调用

这里我们需要重点关注的地方在于组件运行的阶段,组件每一次状态收到更新都会调用render()方法,除非shouldComponentUpdate方法返回false,可以通过此方法对组件做一些优化避免重复渲染带来的性能消耗。

样式

React-Native样式实现了CSS的一个子集,样式的属性与CSS稍有不同,其命名采用驼峰命名,对前端开发者来说基本没差。使用方式也很简单,首先使用StyleSheet创建一个styles

const styles = StyleSheet.create({ 
container:{
flex:1
}
})

然后将对应的style传给组件的style属性,例如<View style={styles.container}/>

常用组件

在日常开发中最常使用的组件莫过于View,Text,Image,TextInput的组件。

View基本上作为容器布局,在里面可以放置各种各样的控件,一般只需要为其设置一个style属性即可,常用的样式属性有flex,width,height,backgroundColor,flexDirector,margin,padding更多可以查看Layout Props

Text是一个显示文本的控件,只需要在组件的内容区填写文字内容即可,例如<Text>Hello world</Text>,可以为设置字体大小和颜色<Text style={{fontSize:14,color:'red'}}>Hello world</Text>,同时也支持嵌套Text,例如

<Text style={{fontWeight: 'bold'}}>
I am bold
<Text style={{color: 'red'}}>
and red
</Text>
</Text>

TextInput是文本输入框控件,其使用方式也很简单

<TextInput
style={{width:200,height:50}}
onChangeText={(text)=>console.log(text)}
/>

style设置了他的样式,onChangeText传入一个方法,该方法会在输入框文字发生变化时调用,这里我们使用console.log(text)打印输入框的文字。

Image是一个图片控件,几乎所有的app都会使用图片作为他们的个性化展示,Image可以加载本地和网络上的图片,当加载网络图片时必须设定控件的大小,否则图片将无法展示

加载本地图片,图片地址为相对地址
<Image style={{width:100,height:100}} source={require('../images/img001.png')}/>
加载网络图片
<Image style={{width:100,height:100}} source={{uri:'https://facebook.github.io/react-native/docs/assets/favicon.png'}}/>
收起阅读 »

新建一个简单的React-Native工程

一、环境配置(1)需要一台Mac(OSX)(2)在Mac上安装Xcode(3)安装node.js:https://nodejs.org/download/(4)建议安装watchman,终端命令:brew install watchman(5)安装flow:b...
继续阅读 »

一、环境配置

(1)需要一台Mac(OSX)

(2)在Mac上安装Xcode

(3)安装node.js:https://nodejs.org/download/

(4)建议安装watchman,终端命令:brew install watchman

(5)安装flow:brew install flow

ok,按照以上步骤,你应该已经配置好了环境。

二、Hello, React-Native

现在我们需要创建一个React-Native的项目,因此可以按照下面的步骤:

打开终端,开始React-Native开发的旅程吧。

(1)安装命令行工具:sudo npm install -g react-native-cli

(2)创建一个空项目:react-native init HelloWorld

(3)找到创建的HelloWorld项目,双击HelloWorld.xcodeproj即可在xcode中打开项目。xcodeproj是xcode的项目文件。

(4)在xcode中,使用快捷键cmd + R即可启动项目。基本的Xcode功能可以熟悉,比如模拟器的选择等。

启动完成后,你会看到React-Packger和iOS模拟器,具体的效果如下,说明你创建项目成功了。

Xcode10 上创建RN工程报错:error: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src: Permission deniederror: couldn't create directory /Users/dmy/HelloWorld/node_modules/react-native/third-party/glog-0.3.5/src:

解决办法:

不要直接使用 react-native init HelloWorld 创建项目,

后面加个 --version 0.45.0 之前的版本就好了,

比如:

react-native init HelloWorld --version 0.44.0

收起阅读 »

react-native自定义原生组件

使用react-native的时候能够看到不少函数调用式的组件,像LinkIOS用来呼起url请求 LinkIOS.openUrl('http://www.163.com');复制actionSheetIOS用来实现ios客户端底部弹起的选择对话框Action...
继续阅读 »

使用react-native的时候能够看到不少函数调用式的组件,像LinkIOS用来呼起url请求

 LinkIOS.openUrl('http://www.163.com');

actionSheetIOS用来实现ios客户端底部弹起的选择对话框

ActionSheetIOS.showActionSheetWithOptions({
options: BUTTONS,
cancelButtonIndex: CANCEL_INDEX,
destructiveButtonIndex: DESTRUCTIVE_INDEX,
},
(buttonIndex) => { this.setState({ clicked: BUTTONS[buttonIndex] });
});

这些组件的使用方式都大同小异,通过声明一个native module,然后在这个组件内部通过底层实现方法的具体内容

像ActionSheetIOS在使用的时候,首先需要在工程的pod库中添加ActionSheetIOS对应的RCTActionSheet

pod 'React', :path => 'node_modules/react-native', :subspecs => ['Core','RCTActionSheet'# Add any other subspecs you want to use in your project]

我们可以看到RCTActionSheet相关的实现的代码是放在react-native/Libraries/ActionSheetIOS下的


整个工程包含3个代码文件,ActionSheetIOS.js、RCTActionSheetManager.h、RCTActionSheetManager.m

ActionSheetIOS.js内容很简单,先是定义了引用oc代码的方式

var RCTActionSheetManager = require('NativeModules').ActionSheetManager;

然后定义了ActionSheetIOS组件,并export

var ActionSheetIOS = {
showActionSheetWithOptions(options: Object, callback: Function) {
invariant( typeof options === 'object' && options !== null, 'Options must a valid object'
);
invariant( typeof callback === 'function', 'Must provide a valid callback'
);
RCTActionSheetManager.showActionSheetWithOptions(
{...options, tintColor: processColor(options.tintColor)},
callback
);
},
.....,

};module.exports = ActionSheetIOS;

我们看到关键是引入底层oc的方式,其他的跟写前端没啥差别

然后再看RCTActionSheetManager的实现

#import "RCTBridge.h"@interface RCTActionSheetManager : NSObject@end

主要是实现了RCTBridgeModule这个协议,这个协议是实现前端js-》oc的主要中间件,感兴趣的可以看看实现,

然后就是对RCTActionSheetManager的实现的代码,关键几句

@implementation RCTActionSheetManager
{
// Use NSMapTable, as UIAlertViews do not implement // which is required for NSDictionary keys
NSMapTable *_callbacks;}

RCT_EXPORT_MODULE()
...
RCT_EXPORT_METHOD(showActionSheetWithOptions:(NSDictionary *)options
callback:(RCTResponseSenderBlock)callback
)
{
...
}

主要是RCT_EXPORT_MODULE用来注册react-native module ,然后具体的实现方法放在RCT_EXPORT_METHOD开头的函数内

RCT开头的宏用来区分react-native函数与原声的函数,jspatch的bang有过具体分析,感兴趣的可以看看

http://blog.cnbang.net/tech/2698/

所以我们自己实现一个原生的react-native组件的时候,完全可以照着actionSheetIOS来做

在前端自定义一个js,通过require('NativeModules').XXX 引入

然后在底层实现RCTBridgeModule的类,在类里把RCT_EXPORT_MODULE、RCT_EXPORT_METHOD加上即可


转载自 https://cloud.tencent.com/developer/article/1896500

收起阅读 »

iOS10-iOS15主要适配回顾

iOS
ios15适配1、UITabar、NaBar新增scrollEdgeAppearance,来描述滚动视图滚动到bar边缘时的外观,即使没有滚动视图也需要去指定scrollEdgeAppearance,否则可能导致bar的背景设置无效。具体可以参考UIBarAp...
继续阅读 »

ios15适配

  • 1、UITabar、NaBar新增scrollEdgeAppearance,来描述滚动视图滚动到bar边缘时的外观,即使没有滚动视图也需要去指定scrollEdgeAppearance,否则可能导致bar的背景设置无效。具体可以参考UIBarAppearance
  • 2、tableView 增加sectionHeaderTopPadding属性,默认值是UITableViewAutomaticDimension,可能会使tableView sectionHeader多处一段距离,需要设置 为
  • 3、IDFA 请求权限不弹框问题,解决参考iOS15 ATTrackingManager请求权限不弹框
  • 4、iOS15终于迎来了UIButton的这个改动

ios14适配

  • 1、更改了cell布局视图,之前将视图加载在cell上,将会出现contentView遮罩,导致事件无法响应,必须将customView 放在 contentView 上
  • 2、UIDatePicker默认样式不再是以前的,需要设置preferredDatePickerStyle为 UIDatePickerStyleWheels。
  • 3、IDFA必须要用户用户授权处理,否则获取不到IDFA
  • 4、 UIPageControl的变化 具体参考iOS 14 UIPageControl对比、升级与适配

ios13适配

-1、 iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整

  • 2、支持第三方登录必须,就必须Sign In with Apple
  • 3、MPMoviePlayerController 废弃
  • 4、iOS 13 DeviceToken有变化
  • 5、模态弹出默认不再是全屏。
  • 6、私有方法 KVC 不允许使用
  • 7、蓝牙权限需要申请
  • 8、LaunchImage 被弃用
  • 9、新出UIBarAppearance统一配置navigation bars、tab bars、 toolbars等bars的外观。之前设置na bar和tab bar外观的方法可能会无效

ios12适配

  • 1、C++ 标准库libstdc++相关的3个库(libstdc++、libstdc++.6、libstdc++6.0.9 )废弃,使用libc++代替
  • 2、短信 验证码自动填充api
if (@available(iOS 12.0, *)) {
codeTextFiled.textContentType = UITextContentTypeOneTimeCode;
}

ios11适配

  • 1、ViewController的automaticallyAdjustsScrollViewInsets属性被废弃,用scrollView的contentInsetAdjustmentBehavior代替。
  • 2、safeAreaLayoutGuide的引入
  • 3、tableView默认开启了Size-self
  • 4、新增的prefersLargeTitles属性
  • 5、改善圆角,layer新增了maskedCorners属性
  • 6、tableView右滑删除新增api
  • 7、导航条的层级发生了变化。
    ios11适配相关

ios10适配

  • 1、通知统一使用UserNotifications.framework框架
  • 2、UICollectionViewCell的的优化,新增加Pre-Fetching预加载机制
  • 3、苹果加强了对隐私数据的保护,要对隐私数据权限做一个适配,iOS10调用相机,访问通讯录,访问相册等都要在info.plist中加入权限访问描述,不然之前你们的项目涉及到这些权限的地方就会直接crash掉。
  • 4、AVPlayer增加了多个属性,timeControlStatus、
    automaticallyWaitsToMinimizeStalling
  • 5、tabar未选中颜色设置 用 unselectedItemTintColor代替
收起阅读 »

iOS安全–浅谈关于iOS加固的几种方法

iOS
关于IOS安全这方面呢,能做的安全保护确实要比Android平台下面能做的少很多。 只要你的手机没越狱,基本上来说是比较安全的,当然如果你的手机越狱了,可能也会相应的产生一些安全方面的问题。就比如我在前面几篇博客里面所介绍的一些IOS逆向分析,动态分析以及破...
继续阅读 »

关于IOS安全这方面呢,能做的安全保护确实要比Android平台下面能做的少很多。
只要你的手机没越狱,基本上来说是比较安全的,当然如果你的手机越狱了,可能也会相应的产生一些安全方面的问题。就比如我在前面几篇博客里面所介绍的一些IOS逆向分析,动态分析以及破解方法。
但是尽管这样,对IOS保护这方面来说,需求还不是很乏,所有基于IOS平台的加固产品也不是很多,目前看到几种关于IOS加固的产品也有做的比较好的。
最开始关于爱加密首创的IOS加密,http://www.ijiami.cn/ios 个人感觉这只是一个噱头而已,因为没有看到具体的工具以及加固应用,所以也不知道它的效果怎么样了。
后来在看雪上面看到一个http://www.safengine.com/mobile/ 有关于IOS加密的工具,但是感觉用起来太麻烦了,而且让产品方也不是很放心,要替换xcode默认的编译器。
不久前看到偶然看到一个白盒加密的应用http://kiwisec.com/ 也下下来试用了一下,感觉要比上面两个从使用上方面了许多,而且考虑的东西也是比较多的。
好了,看了别人做的一些工具,这里大概说下都有哪些加固方法以及大概的实现吧,本人也是刚接触这个方面不就,可能分析的深度没有那么深入,大家就随便听听吧。
现在的加固工具总的来说都是从以下几个方面来做的:
一、字符串加密:
现状:对于字符串来说,程序里面的明文字符串给静态分析提供了极大的帮助,比如说根据界面特殊字符串提示信息,从而定义到程序代码块,或者获取程序使用的一些网络接口等等。
加固:对程序中使用到字符串的地方,首先获取到使用到的字符串,当然要注意哪些是能加密,哪些不能加密的,然后对字符串进行加密,并保存加密后的数据,再在使用字符串的地方插入解密算法,这样就很好的保护了明文字符串。
二、类名方法名混淆
现状:目前市面上的IOS应用基本上是没有使用类名方法名混淆的,所以只要我们使用class-dump把应用的类和方法定义dump下来,然后根据方法名就能够判断很多程序的处理函数是在哪。从而进行hook等操作。
加固:对于程序中的类名方法名,自己产生一个随机的字符串来替换这些定义的类名和方法名,但是不是所有类名,方法名都能替换的,要过滤到系统有关的函数以及类,可以参考下开源项目:https://github.com/Polidea/ios-class-guard
三、程序代码混淆
现状:目前的IOS应用找到可执行文件然后拖到Hopper Disassembler或者IDA里面程序的逻辑基本一目了然。
加固:可以基于Xcode使用的编译器clang,然后在中间层也就是IR实现自己的一些混淆处理,比如加入一些无用的逻辑块啊,代码块啊,以及加入各种跳转但是又不影响程序原有的逻辑。可以参考下开源项目:https://github.com/obfuscator-llvm/obfuscator/ 当然开源项目中也是存在一些问题的,还需自己再去做一些优化工作。
四、加入安全SDK
现状:目前大多数IOS应用对于简单的反调试功能都没有,更别说注入检测,以及其它的一些检测了。
加固:加入SDK,包括多处调试检测,注入检测,越狱检测,关键代码加密,防篡改等等功能。并提供接口给开发者处理检测结果。

当然除了这些外,还有很多方面可以做加固保护的,相信大家会慢慢增加对IOS应用安全的意识,保护好自己的APP。

收起阅读 »

CSS揭秘之性能优化技巧篇

CSS揭秘之性能优化技巧篇 一、写在前面 我们说的性能优化与降低开销,那必然都是在都能实现需求的条件下,选取其中的“最优解”,而不是避开需求,泛泛地谈性能和开销。 “沉迷”于寻求最优解,在各行各业都存在,哪怕做一顿晚餐,人们也总在摸索如何能在更短的时间更少的资...
继续阅读 »



CSS揭秘之性能优化技巧篇


一、写在前面


我们说的性能优化与降低开销,那必然都是在都能实现需求的条件下,选取其中的“最优解”,而不是避开需求,泛泛地谈性能和开销。


“沉迷”于寻求最优解,在各行各业都存在,哪怕做一顿晚餐,人们也总在摸索如何能在更短的时间更少的资源,做更多的“美味”。例如要考虑先把米放到电饭煲,
然后把需要解冻的拿出来解冻,把蘑菇黄豆这种需要浸泡的先“预处理”,青菜要放在后面炒,汤要先炖,
洗菜的水要用来浇花...需要切的菜原料排序要靠近,有些菜可以一起洗省时节水,要提前准备好装菜的器皿否则你可能要洗好几次手


瞧做一顿晚餐其实也可以很讲究,归纳一下这些行为,可以统称为“优化行为”,也可以引用一些术语表示,例如寻找“最优解“和”关键路径“,
在 CSS 的使用中,同样也需要”关键路径“、”最优解“和”优化“,下面将从这几个方面解释 CSS 性能优化:



①渲染方向的优化


②加载方向的优化



二、CSS性能优化技巧


2.1 渲染方向的优化


  • ①减少重排(redraw)重绘(repaint)


例如符合条件的vue中应尽可能使用 v-show 代替 v-if。v-show 是通过改变 css display 属性值实现切换效果,
v-if 则是通过直接销毁或创建 dom 元素来达到显示和隐藏的效果。 v-if是真正的条件渲染,当一开始的值为true时才会编译渲染,
而v-show不管怎样都会编译,只是简单地css属性切换。v-if适合条件不经常改变的场景,因为它的切换会重新编译渲染,
会创建或销毁 dom 节点,开销较大。 v-show 适合切换较为频繁的场景,开销较小。




  • ②减少使用性能开销大的属性:例如动画、浮动、特殊定位。



  • ③减少css计算属性的使用,有时它们不是必须使用的:例如 calc(100% - 20px),如果这 20px 是边距,



那么或许可以考虑 border-size:border-box。



  • ④脚本行为的防抖节流,减少不必要的的重复渲染开销。



  • ⑤属性值为 0 时,不必添加单位(无论是相对单位还是绝对单位),那可能会增加计算开销,



且也没有规范提倡0值加单位,那是没有意义的,0rem和0em在结果上是没有区别的,但加上单位可能会带来不必要的计算开销。
关于0不必加单位,想必你也收到过编辑器的优化提示。


  • ⑥css 简写属性的使用,有时开销会更大得不偿失,例如 padding: 0 2px 0 0;和 padding-right:2px;

后者的写法对机器和人的阅读理解和计算的开销都是更小的。常见的 css 可简写属性还有 background,border,
font 和 margin。


  • ⑦尽可能减少 CSS 规则的数量,并删除未使用到的 CSS 规则。一些默认就有的 CSS 规则,就不必写了,具有继承性的样式,

也不必每级节点都写。


-⑧避免使用不必要且复杂的 CSS 选择器(尤其是后代选择器),因为此类选择器需要耗用更多的 CPU 处理能力来执行选择器匹配。
总之不必要的深度,不管是 css 还是 dom 都不是好的选择,这对人和机器都是同样的道理,因为读和理解起来都同样的“费力”。


-⑨关键选择器(key selector)。


览器会从最右边的样式选择器开始,依次向左匹配。最右边的选择器相当于关键选择器(key selector),
浏览器会根据关键选择器从 dom 中筛选出对应的元素,然后再向上遍历相关的父元素,判断是否匹配。


所以组合嵌套选择器时,匹配语句越短越简单,浏览器消耗的时间越短,
同时也应该减少像标签选择器,这样的大范围命中的通配选择器出现在组合嵌套选择器链中,
因为那样会让浏览器做大量的筛选,从而去判断选出匹配的元素。


-⑩少用*{}通配规则,那样的计算开销是巨大的。


2.2 加载方向的优化


  • ①减少 @import 的使用

合理规划 css 的加载引入方式,减少 @import 的使用,页面被加载时,link 会同时被加载,
而 @import 引用的 CSS 会等到页面被加载完再加载。



  • ②css 尽量放在 head 中,会先加载,减少首次渲染时间。



  • ③按需加载,不必一次就加载完全部资源,在单页应用中应尤其注意。



  • ④考虑样式和结构行为分离,抽放到独立css文件,减少重复加载和渲染。



  • ⑤css压缩技术的应用,减少体积。



三、写在后面


有一个好的家务机器人,我们可以省很多事,少操心很多,同样的,
有一个好的 css 预处理工具和打包工具或许也可以帮助程序员节省很多精力。


网速的提升和设备性能提升,也让程序员拥有许多资源可以“挥霍”,例如现在的很多“国民级”的
应用在3g网络下和早期的手机中都无法正常工作,但那似乎不影响它们的“优秀”。诚然,那又是复杂的问题。



正如开头所言,程序员寻求“最优解”和“关键路径”,应当在有可替代方案和能满足需求的前提下进行。



仅是理论空谈优化,无异于是”耍流氓“。矛盾无处无时不在,重要的是衡量取舍和你能承受。


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

系统介绍浏览器缓存机制及前端优化方案

背景 缓存是用来做性能优化的好东西,但是,如果用不好缓存,就会产生一系列问题: 为什么我的页面显示的还是老版本为什么我的网页白屏请刷新下网页... 以上问题大家或多或少都遇到过,归根结底是使用缓存的姿势不对,今天,我们就来一起了解下浏览器是如何进行缓存的,以...
继续阅读 »


背景


image-20220610170115175


缓存是用来做性能优化的好东西,但是,如果用不好缓存,就会产生一系列问题:


  • 为什么我的页面显示的还是老版本
  • 为什么我的网页白屏
  • 请刷新下网页
  • ...

以上问题大家或多或少都遇到过,归根结底是使用缓存的姿势不对,今天,我们就来一起了解下浏览器是如何进行缓存的,以及我们要怎样科学的使用缓存


浏览器的缓存机制


1. 什么是浏览器缓存?


image-20220609105103551


简单说,浏览器把 http 请求的资源保存到本地,供下次使用的行为,就是浏览器缓存


这里先记一个点:http 响应头,决定了浏览器会对资源采取什么样的缓存策略


2. 浏览器是读取缓存还是请求数据?


  • 用户第一次请求资源

image-20220609173401737


  • 整个完整流程

image-20220609171118083


3. 缓存过程分类——强缓存 / 协商缓存



根据是否请求服务,我们把缓存过程分为强缓存和协商缓存,也可以理解为必然经过的过程称为强缓存,如果强缓存没有,那在和服务器协商一下



强缓存


强缓存看的是响应头的 Expires 和 Cache-Control 字段


  • Expires 是老规范,它表示的是一个绝对有效时间,在该时间之前则命中缓存,如果超过则缓存失效,并且,由于它是跟本地时间(可以随意修改)做比较,会导致缓存混乱
  • Cache-Control 是新规范,优先级要高于Expires,也是目前主要使用的缓存策略,字段是max-age,表示的是一个相对时间,例如 Cache-Control: max-age=3600,代表着资源的有效期是 3600 秒。


其他配置


no-cache:需要进行协商缓存,发送请求到服务器确认是否使用缓存。


no-store:禁止使用缓存,每一次都要重新请求数据。


public:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器。


private:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存。



协商缓存


当强缓存没有命中的时候,浏览器会发送一个请求到服务器,服务器根据 header 中的部分信息来判断是否命中缓存。如果命中,则返回 304 ,告诉浏览器资源未更新,可使用本地的缓存。


协商缓存看的是 header 中的 Last-Modified / If-Modified-Since 和 Etag / If-None-Match



缓存生效,返回304,缓存失效,返回200和请求结果


Etag 优先级 Last-Modified 高



  • Last-Modified / If-Modified-Since

浏览器第一次请求一个资源的时候,服务器返回的 header 中会加上 Last-Modify,Last-modify 是一个时间标识该资源的最后修改时间。


当浏览器再次请求该资源时,request 的请求头中会包含 If-Modify-Since,该值为缓存之前返回的
Last-Modify。服务器收到 If-Modify-Since
后,根据资源的最后修改时间判断是否命中缓存,命中返回304使用本缓存,否则返回200和请求最新资源。


  • Etag / If-None-Match

etag 是更为严谨的校验,一般情况下使用时间检验已经足够,但我们想象一个场景,如果我们在短暂时间内修改了服务端资源,然后又迅速的改回去,理论上这种情况本地缓存还是可以继续使用的,这就是 etag 诞生的场景。


使用 etag 时服务端会对资源进行一次类似 hash 的操作获得一个标识(内容不变标识不变),并返回给客户端。


再次请求时客户端会在 If-None-Match 带上 etag 的值给服务端进行对比验证,如果命中返回304使用缓存,否则重新请求资源。



注:由于 e-atg 服务端计算会有额外开销,所以性能较差



扩展:DNS缓存与CDN缓存


DNS 缓存


我们在网上所有的通信行为都需要IP来进行连接,DNS解析是指通过域名获取相应IP地址的过程。


基本上有DNS的地方就有缓存,查询顺序如下:


image-20220610104424261


一般我们日常会接触到的就是有时内网域名访问需要修改本地host映射关系,或者某些科学上网的情况,可以通过修改本地host来正常访问网址


CDN 缓存


CDN 缓存从减轻根服务的分发压力和缩短物理的传输距离(跨地域访问)上2个层面对资源访问进行优化。



CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低。


大部分请求在CDN边缘节点完成,CDN起到了分流作用,减轻了源服务器的负载。



一般CDN服务都由运营商提供,我们只需要了解如何验证CDN是否生效即可



  • 查看域名是否配置了CDN缓存


    ping {{ 域名 }} 会看到转向了其他地址(alikunlun)


    例如: ping customer.kukahome.com


    image-20220610110014867



  • 查看我们的页面资源是否命中CDN缓存



通过查看相应头有 X-cache:HIT 字段,则命中CDN缓存,注意这里名称并不固定,但一般都会有HIT标识,如果是MISS 或None之类的,则没有命中缓存


image-20220610110324860


前端针对缓存部署优化方案


构建演进



构建方面优化的核心思想是如何更优,更快速的加载资源,以及如何保证资源的新鲜度



这个优化过程也分为几个阶段,有些其实已经不适用现在的场景,但也可以了解下



  • 早期的图标合并雪碧图(sprite),多脚本文件整理到一个文件:目的是通过减少碎片化的请求数量来加速资源加载(相关知识点是浏览器一般最多只支持6个并发请求,同一时间请求数量需要控制在合理范围)


    • 现在雪碧图已基本被 iconfont 代替,js 加载更多采用分模块异步加载,而不是一味合并


  • 随着 web 应用的推广和浏览器缓存技术的普及,前端缓存问题也随着而来,最常见的就是服务端资源变了,但是客户端资源始终无法更新,这个阶段工程师们想了很多方案。


    • 打包时在静态资源路径上加上 “?v=version” 或者使用时间戳来给资源文件命名


    • 跟 modified 缓存有点像,由于时间戳并不能识别出文件内容是否变动,所以有了后来的 hash 方案,理论上 hash 出来的文件只要内容不变,文件名就不变,大大提高了缓存的使用寿命,也是现代常用打包工具的默认配置

    image-20220610141324528



  • 然后,重点来了,以上我们对 html 文件里链接的资源做了一系列优化,但是 html
    本身也是一种静态资源,并且,客户在访问页面时是不会带上所谓的时间戳或者版本号的,导致了很多时候虽然服务端资源更新了,但是客户端还是用老的
    html 文本发起请求,这个时候就会导致各种各样的问题了,包括但不限于白屏,展现的旧版本页面等等


    image-20220610150956617


    • 为了解决这个问题,目前主流的解决方案是不对 html 进行缓存(一般单页应用html文件较小,大的是 js),只对 js,css 等静态文件进行本地缓存

    image-20220610151923311



    • 那么,如何让浏览器不缓存 html 呢,目前都是通过设置 Cache-Control实现, 有前端方案和后端方案,风险提示,前端方案很不靠谱,后端很多默认配置会覆盖前端方案,可以做了解,生产中请使用后端配置。


      通过 html 标签设置 cache-control


        <meta http-equiv="Pragma" content="no-cache" />  // 旧协议
      <meta http-equiv="Expires" content="0" /> // 旧协议
      <meta http-equiv="Cache-Control" content="no-cache" /> // 目前主流



部署配置



目前主流的前端部署方式都是使用 nginx,我们来看看 nginx 如何禁用 html 的缓存



location / {
  root **;
  # 配置页面不缓存html和htm结尾的文件
  if ($request_filename ~* .*.(?:htm|html)$)
  {
      add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
  }
  index index.html index.htm;
}

  • Private 会影响到CDN缓存命中率,但本身CDN缓存也会存在版本问题,量不大的情况下也可以禁掉
  • No-cache 可以使用缓存,但是使用前必须到服务端验证,可以是 no-cache,也可以是 max-age=0
  • No-store 完全禁用缓存
  • Must-revalidate 与 no-cache 类似,强制到服务端验证,作用于一些特殊场景,例如发送了校验请求,但发送失败了之类
  • Proxy-revalidate 与上面类似,要求代理缓存服务验证有效性

以上配置可以跟据项目需要灵活配置,考虑到浏览器对缓存协议支持会有些许差异,只是想简单粗暴禁用 html 缓存全上也没有关系,并不会有特别大影响,除非特殊场景需要调优时需要关注。


资源压缩


都讲到这了,前端构建优化还有一个常用的就是 Gzip 资源压缩,可以极大减小资源包体积,前端一般构建工具都有插件支持,需要注意的是也需要 nginx 做配置才能生效


http {
  gzip_static on;
  gzip_proxied any;
}

如果生效了,响应头里可以看到 content-encoding: gzip


image-20220610162843430

收起阅读 »

🦊【低代码相关】表单联动新思路 摆脱if-else的地狱🦄

在低代码解决方案中,表单是一大类低代码搭建解决的问题。表单作为用户信息采集的手段存在于各类应用中,无论是面向C端的手机页面,还是面向B端的运营平台,甚至低代码平台本身都会存在表单这种形式的交互。 表单本身并不复杂,各个组件库,如antd,element ui等...
继续阅读 »


在低代码解决方案中,表单是一大类低代码搭建解决的问题。表单作为用户信息采集的手段存在于各类应用中,无论是面向C端的手机页面,还是面向B端的运营平台,甚至低代码平台本身都会存在表单这种形式的交互。


表单本身并不复杂,各个组件库,如antd,element ui等都提供了表单组件,能够将一组输入控件组织成一个表单,并且都提供了简单的校验功能,能够检查单控件类似非空、输入长度、正则匹配之类的问题,也可以针对类似多字段情形自己定制复杂校验逻辑。然而,对于表单项之间存在联动的情形,比如一些字段的出现/消失依赖于其他字段的情形,或者一些字段填写以后其他字段的选项应当变更,这些情形通用的组件库就没有提供解决方案,而是由开发各显神通了。


表单联动最简单的方式自然是if-else了,对于联动项较少的情形,简单一个if-else就能够实现我们所需要的功能。然而,在复杂的表单上if-else层层嵌套下来代码的可读性会变差,下次开发的时候看着长长的一串if-else,每个人都会超级头痛。更重要的是,采用if-else的维护方式,在表单渲染部分需要一组对应的逻辑,在表单提交校验的时候又需要一组对应的逻辑,两边的逻辑大量都是重复的,但一组是嵌套在视图里,一组是针对表单数据。


在程序语言中,解决if-else的方法是采用模式匹配,在表单联动这个主题上,这个方式也是可行的嘛?让我们就着手试试吧!


模式定义


我们的目标是尽可能多地去掉if-else。表单联动主要是基于表单的值,那模式自然是基于值来定义的。


举个🌰:假设我们需要开发一个会议预订系统,支持单次和循环会议,那么表单的模式有那几种呢?


系统最后的效果就类似Outlook:


image.png



  1. 单次会议,需要会议日期(event_date)、开始时间(event_start)、结束时间(event_end)、主题(subject)、参与者(attenders)、地址(location)



  2. 循环会议,一样需要开始时间(event_start),结束时间(event_end),主题(subject)、参与者(attenders),地址(location),还需要循环的间隔(recurrence_interval)和循环的起始(recurrence_start)、结束日期(recurrence_end)。而循环又可以分为以下几种子模式:


    1. 按日循环
    2. 按周循环,额外需要周几举行会议(recurrence_weekdays)
    3. 按月循环,额外需要几号举行会议(recurrence_date)
    4. 按年循环,额外需要几月几号举行会议(recurrence_month,recurrence_date)


这里除了地址和循环结束日期以外的所有字段都是必选的,循环的间隔需要是一个正整数。


可以看到,这里一共是5种模式。区分模式主要是两个字段——是否循环(is_recurrence)和循环单位(recurrence_unit),并且都是值的唯一匹配,因此我们可以用简单用JSON的方式定义模式:


// 单次会议
{
"is_recurrence": false
}
// 按日循环
{
"is_recurrence": true,
"recurrence_unit": "day"
}
// 按周循环
{
"is_recurrence": true,
"recurrence_unit": "week"
}
// 按月循环
{
"is_recurrence": true,
"recurrence_unit": "month"
}
// 按年循环
{
"is_recurrence": true,
"recurrence_unit": "year"
}

对于更复杂的情况来说,模式的区分可能就不是单一值匹配了。例如我们需要做一个医院急诊管理系统,需要根据用户输入的体温来获取更多信息,体温在38.5度上下需要有不同的反馈,这样的情况就没法简单用JSON来表达,而是需要使用function,但整体的逻辑是一致的,都是将可能的情况定义为模式,并将表单状态与模式相关联。


表单定义


定义完模式后我们需要定义对应的表单。


在我们的会议预订应用中,总共有以下几个字段:


  • event_date
  • event_start
  • event_end
  • subject
  • attenders
  • location
  • is_recurrence
  • recurrence_interval
  • recurrence_unit
  • recurrence_start
  • recurrence_end
  • recurrence_weekdays
  • recurrence_month
  • recurrence_date

在这个场景下,每个字段展示的内容和校验逻辑在5种模式下都是一致的,需要根据模式联动的点只在于每个字段是否展示,整个表单数据的校验逻辑其实所有展示字段的单字段校验逻辑。因此,我们将每个字段通过以下类型表示:


type FormField<T> = {
/**
* 表单展示
*/

render: (value: T | undefined) => ReactNode;
/**
* 校验规则
*/

rules: {
validates: (value: T | undefined) => boolean;
errorMessage: boolean;
}[];
};

所有字段根据字段key通过一个map进行存储与索引。同时,将每个模式下应该展示的字段以字段key的数组的方式进行存储:


/** 所有字段的存储,这里省略实现 */
declare const formFields: Record<keyof Schedule, FormField<any>>;
type Pattern = {
pattern_indicator: Partial<Schedule>;
fields: (keyof Schedule)[];
};
/** 每个模式下应该展示的字段映射 */
const patterns: Pattern[] = [
{
pattern_indicator: { is_recurrence: false },
fields: [
"event_date",
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
],
},
{
pattern_indicator: { is_recurrence: true, recurrence_unit: "day" },
fields: [
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
"recurrence_interval",
"recurrence_unit",
"recurrence_start",
"recurrence_end",
],
},
{
pattern_indicator: { is_recurrence: true, recurrence_unit: "week" },
fields: [
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
"recurrence_interval",
"recurrence_unit",
"recurrence_start",
"recurrence_end",
"recurrence_weekdays",
],
},
{
pattern_indicator: { is_recurrence: true, recurrence_unit: "month" },
fields: [
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
"recurrence_interval",
"recurrence_unit",
"recurrence_start",
"recurrence_end",
"recurrence_date",
],
},
{
pattern_indicator: { is_recurrence: true, recurrence_unit: "year" },
fields: [
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
"recurrence_interval",
"recurrence_unit",
"recurrence_start",
"recurrence_end",
"recurrence_month",
"recurrence_date",
],
},
];

展示逻辑


表单定义好后,具体应该如何展示呢?


对于刚好匹配上一个模式的情况,显而易见地,我们应当展示该模式应当展示的字段。


然而,也存在匹配不上任何模式的情况。比如初始状态下,所有字段都还没有值,自然就不可能匹配上任何模式;又比如is_recurrence选择了true,但其他字段都还没有填写的情况。这种情况下我们该展示哪些字段呢?


我们可以从初始状态这种情况开始考虑,初始情况是是所有情况的起始点,那么只要所有情况下都会展示的字段,那么初始情况也应该展示。然后,当用户将is_recurrence选择了true,那么单次会议这种可能性已经被排除了,还剩下4种循环的情况,这时就应该展示这四种剩余情况都展示的字段。


这样,整套展示逻辑就出来了:


const matchedPattern: Pattern = getMatchedPattern(patterns, answer);
if (matchedPattern) {
return matchedPattern.fields;
}
const possiblePatterns: Pattern[] = removeUnmatchedPatterns(patterns, answer);
return getIntersectionFields(possiblePatterns);

本文用一个简单的例子来阐释了我们通过模式匹配的方式定义表单的思路。其实,像类似决策树、有限状态机等的模型都可以用来帮助我们通过更灵活的方式来定义我们的表单联动逻辑,像formily之类的专业的表单库更是有完整的解决方案,欢迎各位读者一起提供思路哈哈。

 
收起阅读 »

如何编写复杂拖拽组件🐣

阅读本文🦀 1.您将了解到如何让echart做到响应式 2.您将到如何编写复杂的拖拽组件 3.和我一起实现可拖拽组件的增删改查、可编辑、可以拖拽、可排序、可持久化 4.和我一起实现可拖拽组件的删除抖动动画 前言🌵 在业务中得到一个很复杂的需求,需要实现组件中...
继续阅读 »





阅读本文🦀


1.您将了解到如何让echart做到响应式


2.您将到如何编写复杂的拖拽组件


3.和我一起实现可拖拽组件的增删改查、可编辑、可以拖拽、可排序、可持久化


4.和我一起实现可拖拽组件的删除抖动动画


前言🌵



在业务中得到一个很复杂的需求,需要实现组件中展示ecahrts图表,并且图表可编辑,可排序,大小可调整,还要可持续化,下面就是解决方案啦



正文 🦁


先看效果再一步步实现



技术调研



如何做到可拖拽?自己造轮子?显然不是,当然是站在巨人的肩膀上😁



  1. react-dnd
  2. react-beautiful-dnd
  3. dnd-kit
  4. react-sortable-hoc
  5. react-grid-layout

  • react-dnd

    • 文档齐全
    • github star星数16.4k
    • 维护更新良好,最近一月内有更新维护
    • 学习成本较高
    • 功能中等
    • 移动端兼容情况,良好
    • 示例数量中等
    • 概念较多,使用复杂
    • 组件间能解耦

  • react-beautiful-dnd

    • 文档齐全
    • github star星数24.8k
    • 维护更新良好,最近三月内有更新维护
    • 学习成本较高
    • 使用易度中等
    • 功能丰富
    • 移动端兼容情况,优秀
    • 示例数量丰富
    • 是为垂直和水平列表专门构建的更高级别的抽象,没有提供 react-dnd 提供的广泛功能
    • 外观漂亮,可访问性好,物理感知让人感觉更真实的在移动物体
    • 开发理念上是拖拽,不支持copy/clone

  • dnd-kit

    • 文档齐全
    • github star星数2.8k
    • 维护更新良好,最近一月内有更新维护
    • 学习成本中等
    • 使用易度中等
    • 功能中等
    • 移动端兼容情况,中等
    • 示例数量丰富
    • 未看到copy/clone

  • react-sortable-hoc

    • 文档较少
    • github star星数9.5k
    • 维护更新良好,最近三月内有更新维护
    • 学习成本较低
    • 使用易度较低
    • 功能简单
    • 移动端兼容情况,中等
    • 示例数量中等
    • 不支持拖拽到另一个容器中
    • 未看到copy/clone
    • 主要集中于排序功能,其余拖拽功能不丰富

  • react-grid-layout
    • 文档较少
    • github star 星星15.8k
    • 维护更新比较好,近三个月有更新维护
    • 学习成本比较高
    • 功能复杂
    • 支持拖拽、放大缩小


总结:为了实现我们想要的功能,最终选择react-grid-layout,应为我们想要的就是在网格中实现拖拽、放大缩小、排序等功能


Coding🔥



由于代码量比较大,只讲述一些核心的code



1.先创建基础布局


  • isDraggable 控制是否可拖拽
  • isResizable 控制是否可放大缩小
  • rowHeight控制基础行高
  • layout控制当前gird画布中每个元素的排列顺序
  • onLayoutChange 当布局发生改变后的回调函数

  <ReactGridLayout
isDraggable={edit}
isResizable={edit}
rowHeight={250}
layout={transformLayouts}
onLayoutChange={onLayoutChange}
cols={COLS}
>
{layouts && layouts.map((layout, i) => {
if (!chartList?.some(chartId => chartId === layout.i))
return null

return (<div
key={layout.i}
data-grid={layout}
css={css`width: 100%;
height: 100%`}
>

<Chart
setSpinning={setSpinning}
updateChartList={updateChartList}
edit={edit}
key={layout.i}
chartList={chartList}
chartId={Number(layout.i)}
scenarioId={scenarioId}/>

</div>

)
})}
</ReactGridLayout>


2.如何让grid中的每个echarts图表随着外层item的放大缩小而改变


    const resizeObserver = new ResizeObserver((entries) => {
myChart?.resize()//当dom发生大小改变就重置echart大小
})
resizeObserver.observe(chartContainer.current)//通过resizeObserver观察echart对应的item实例对象

3.如何实现排序的持久化


//通过一下代码可以实现记录edit变量的前后状态
const [edit, setEdit] = useState(false)
const prevEdit = useRef(false)
useEffect(() => {
prevEdit.current = edit
})

 //通过将grid中的每个item的排序位置记录为对象,然后对每个属性进行前后的对比,如果没有改变就不进行任何操作,如果发生了改变就可以
//通过网络IO更新grid中item的位置
useEffect(() => {
if (prevEdit && !edit) {
// 对比前后的layout做diff 判断是否需要更新位置
const diffResult = layouts?.every((layout) => {
const changedLayout = changedLayouts.find((changedLayout) => {
// eslint-disable-next-line eqeqeq
return changedLayout.i == layout.i
})
return changedLayout?.w === layout.w
&& changedLayout?.h === layout.h
&& changedLayout?.x === layout.x
&& changedLayout?.y === layout.y
})
// diffResult为false 证明发生了改变
if (!diffResult) {
//这里就可以做图表发生改变后的操作
//xxxxx
}
}, [edit])

4.如何实现编辑时的抖动动画


.wobble-hor-bottom{
animation:wobble-hor-bottom infinite 1.5s ;
}

@-webkit-keyframes wobble-hor-bottom {
0%,
100% {
-webkit-transform: translateX(0%);
transform: translateX(0%);
-webkit-transform-origin: 50% 50%;
transform-origin: 50% 50%;
}
15% {
-webkit-transform: translateX(-10px) rotate(-1deg);
transform: translateX(-10px) rotate(-1deg);
}
30% {
-webkit-transform: translateX(5px) rotate(1deg);
transform: translateX(5px) rotate(1deg);
}
45% {
-webkit-transform: translateX(-5px) rotate(-0.6deg);
transform: translateX(-5px) rotate(-0.6deg);
}
60% {
-webkit-transform: translateX(3px) rotate(0.4deg);
transform: translateX(3px) rotate(0.4deg);
}
75% {
-webkit-transform: translateX(-2px) rotate(-0.2deg);
transform: translateX(-2px) rotate(-0.2deg);
}
}

总结 🍁


本文大致讲解了下如何使用react-grid-layout如何与echart图表结合使用,来完成复杂的拖拽、排序、等功能,但是这个组件实现细节还有很多,本文只能提供一个大值的思路,还是希望能够帮助到大家,给大家提供一个思路,欢迎留言和我讨论,如果你有什么更好的办法实现类似的功能


结束语 🌞



那么我的如何编写复杂拖拽组件🐣就结束了,文章的目的其实很简单,就是对日常工作的总结和输出,输出一些觉得对大家有用的东西,菜不菜不重要,但是热爱🔥,希望大家能够喜欢我的文章,我真的很用心在写,也希望通过文章认识更多志同道合的朋友,如果你也喜欢折腾,欢迎加我好友,一起沙雕,一起进步

收起阅读 »

前端取消请求与取消重复请求

一、前言 大家好,我是大斌,一名野生的前端工程师,今天,我想跟大家分享几种前端取消请求的几种方式。相信大家在平时的开发中,肯定或多或少的会遇到需要取消重复请求的场景,比如最常见的,我们在使用tab栏时,我们都会使用一个盒子去存放内容,然后在切换tab栏时,会清...
继续阅读 »





一、前言


大家好,我是大斌,一名野生的前端工程师,今天,我想跟大家分享几种前端取消请求的几种方式。相信大家在平时的开发中,肯定或多或少的会遇到需要取消重复请求的场景,比如最常见的,我们在使用tab栏时,我们都会使用一个盒子去存放内容,然后在切换tab栏时,会清除掉原来的内容,然后替换上新的内容,这个时候,如果我们的数据是通过服务从后端获取的,就会存在一个问题,由于获取数据是需要一定的时间的,就会存在当我们切换tab栏到新的tab页时,原来的tab页的服务还在响应中,这时新的tab页的数据服务已经响应完成了,且页面已经显示了新的tab页的内容,但是,这个时候旧的tab页的数据也成功了并返回了数据,并将新的tab页的内容覆盖了。。。所以为了避免这种情况的发生,我们就需要在切换tab栏发送新的请求之前,将原来的的请求取消掉,至于如何取消请求,这便是今天我要讲的内容。


二、项目准备


在正式学习之前,我们先搭建一个项目,并还原刚刚所说的场景,为了节省时间,我们使用脚手架搭建了一个前端vue+TS+vite项目,简单的做了几个Demo,页面如下,上面是我们现实内容的区域,点击tab1按钮时获取并展示tab1的内容,点击tab2按钮时获取并展示tab2的内容,以此类推,内容比较简单,这里就不放具体代码了。


image.png


然后我们需要搭建一个本地服务器,这里我们新建一个app.ts文件,使用express以及cors解决跨域问题去搭建一个简单的服务器,具体代码如下:

 
// app.ts
const express = require('express')
const app = express()

const cors = require('cors')
app.use(cors())

app.get('/tab1', (req, res) => {
res.send('这是tab1的内容...')
})

app.get('/tab2', (req, res) => {
setTimeout(() => {
res.send('这是tab2的内容...')
}, 3000)
})

app.get('/tab3', (req, res) => {
res.send('这是tab3的内容...')
})

app.listen('3000', () => {
console.log('server running at 3000 port...')
})



上面代码,我们新建了一个服务器并让他运行在本地的3000端口,同时在获取tab2的内容时,我们设置了3秒的延迟,以便实现我们想要的场景,然后我们使用node app.ts启动服务器,当终端打印了server running at 3000 port...就说明服务器启动成功了。


然后我们使用axios去发送请求,安装axios,然后我们在项目中src下面新建utils文件夹,然后新建request.ts文件,具体代码如下:


作者:还是那个大斌啊
链接:https://juejin.cn/post/7108359238598000671
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


import axios, { AxiosRequestConfig } from 'axios'

// 新建一个axios实例
const ins = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000,
})

export function request(Args: AxiosRequestConfig) {
return ins.request(Args)
}



这里我们新建了一个axios实例,并配置了baseURL和超时时间,并做了一个简单的封装然后导出,需要注意的是,axios请求方法的别名有很多种,如下图这里就不做过多介绍了,大家想了解的可以去看官网,我们这里使用request方法。


image.png


最后,我们在页面上引入并绑定请求:

// bar.vue
<script setup lang="ts">
import { ref } from 'vue'
import { request } from '@/utils/request'

const context = ref('tab1的内容...')

const getTab1Context = async () => {
const { data } = await request({
url: '/tab1',
})

context.value = data
}
const getTab2Context = async () => {
const { data } = await request({
url: '/tab2',
})

context.value = data
}
const getTab3Context = async () => {
const { data } = await request({
url: '/tab3',
})

context.value = data
}
</script>



为了方便理解,将template部分代码也附上:

// bar.vue
<template>
<div class="container">
<div class="context">{{ context }}</div>
<div class="btns">
<el-button type="primary" @click="getTab1Context">tab1</el-button>
<el-button type="primary" @click="getTab2Context">tab2</el-button>
<el-button type="primary" @click="getTab3Context">tab3</el-button>
</div>
</div>
</template>



到这里,我们的项目准备工作就好了,看下效果图


取消请求1.gif


然后看下我们前面提到的问题:


取消请求2.gif
注意看,在我点击了tab2之后立马点击tab3,盒子中会先显示tab3的内容,然后又被tab2的内容覆盖了。




三、原生方法


项目准备好之后,我们就可以进入正题了,其实,关于取消请求的方法,axios官方就已经有了,所以我们先来了解下使用axios原生的方法如何取消请求:
先看下官方的代码:


可以使用 CancelToken.source 工厂方法创建 cancel token 像这样:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else { /* 处理错误 */ }
});

axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
});

// 取消请求 (message 参数是可选的)
source.cancel('Operation canceled by the user.');



同时还可以通过传递一个executor函数到CancelToken的构造函数来创建 cancel token :

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});

// 取消请求
cancel();



这是官方提供的两种方法,我们将他们用到我们的项目上,因为都差不多,所以我们这里就只演示一种,选择通过传递函数的方式来取消请求;


进入项目utils文件夹下的request.ts文件,修改代码如下:

 
// request.ts

import axios, { AxiosRequestConfig } from 'axios'
const CancelToken = axios.CancelToken

const ins = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000,
})

// 新建一个取消请求函数并导出
export let cancelFn = (cancel: string) => {
console.log(cancel)
}
export function request(Args: AxiosRequestConfig) {
// 在请求配置中增加取消请求的Token
Args.cancelToken = new CancelToken(function (cancel) {
cancelFn = cancel
})
return ins.request(Args)
}



然后我们就可以在想要取消请求的地方调用cancelFn函数就可以了,我们给tab1tab3按钮都加上取消请求功能:

// bar.vue

<script setup lang="ts">
import { ref } from 'vue'
import { request, cancelFn } from '@/utils/request'

const context = ref('tab1的内容...')

const getTab1Context = async () => {
cancelFn('取消了tab2的请求')
const { data } = await request({
url: '/tab1',
})

context.value = data
}
const getTab2Context = async () => {
const { data } = await request({
url: '/tab2',
})

context.value = data
}
const getTab3Context = async () => {
cancelFn('取消了tab2的请求')
const { data } = await request({
url: '/tab3',
})

context.value = data
}
</script>



这样取消请求的功能就完成了,看下效果:


取消请求3.gif


四、promise


除了官网的方式之外,其实我们也可以借助Promise对象,我们都知道,Promise对象的状态一旦确定就不能再改变的,基于这个原理,我们可以使用Promise封装下我们的请求,然后通过手动改变Promise的状态去阻止请求的响应,看下面代码:

 
// request.ts

import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'

const ins = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000,
})

// 新建一个取消请求函数并导出
export let cancelFn = (cancel: string) => {
console.log(cancel)
}

export function request(Args: AxiosRequestConfig): Promise<AxiosResponse> {
return new Promise((resolve, reject) => {
ins.request(Args).then((res: AxiosResponse) => {
resolve(res)
})
cancelFn = (msg) => {
reject(msg)
}
})
}



效果也是一样的


取消请求4.gif


需要注意的是,虽然效果是一样的,但是使用Promise的方式,我们只是手动修改了Promise的状态为reject,但是请求还是一样发送并响应了,没有取消,这个是和使用Axios原生方法的不同之处。


五、借助Promise.race


讲完了取消请求,其实还有一种场景也很常见,那就是取消重复请求,如果是要取消重复请求,我们又该怎么实现呢?其实我们可以借助Promise.racePromise.race的作用就是将多个Promise对象包装成一个,即它接受一个数组,每一个数组成员都是一个Promise对象,只要这些成员中有一个状态改变,Promise.race的状态就随之改变,基于这个原理,我们可以实现取消重复请求请求的目的。


基本思路就是,我们给每一个请求身边都放一个Promise对象,这个对象就是一颗炸弹,将他们一起放到Promise.race里面,当我们需要取消请求的时候就可以点燃这颗炸药。


还是上面的例子,我们针对按钮tab2做一个取消重复请求的功能,我们先声明一个类,在里面做取消重复请求的功能,在utils下新建cancelClass.ts文件:

 
// cancelClass.ts

import { AxiosResponse } from 'axios'
export class CancelablePromise {
pendingPromise: any
reject: any
constructor() {
this.pendingPromise = null
this.reject = null
}

handleRequest(requestFn: any): Promise<AxiosResponse> {
if (this.pendingPromise) {
this.cancel('取消了上一个请求。。。')
}
const promise = new Promise((resolve, reject) => (this.reject = reject))
this.pendingPromise = Promise.race([requestFn(), promise])
return this.pendingPromise
}

cancel(reason: string) {
this.reject(reason)
this.pendingPromise = null
}
}

上面代码中,我们声明了一个类,然后在类中声明了两个属性pendingPromisereject,一个request请求方法用来封装请求并判断上一个请求是否还在响应中,如果还未响应则手动取消上一次的请求,同时声明了一个promise对象,并将他的reject方法保存在类的reject属性中,然后用promise.race包装了请求函数和刚刚声明的promise对象。最后声明了一个cancel方法,在cancel方法中触发reject函数,来触发promise对象的状态改变,这样就无法获取到reuestFn的响应数据了。从而达到了取消请求的目的;


因为requestFn必须是一个函数,所以我们需要改装下Axiosrequest函数,让他返回一个函数;

 

因为requestFn必须是一个函数,所以我们需要改装下Axiosrequest函数,让他返回一个函数;


// request.ts

export function request(Args: AxiosRequestConfig) {
return () => ins.request(Args)
}

最后在页面中引入并使用:


// bar.vue

<script setup lang="ts">
import { ref } from 'vue'
import { request, cancelFn } from '@/utils/request'
import { CancelablePromise } from '@/utils/cancelClass'

...
const cancelablePromise = new CancelablePromise()
...
const getTab2Context = async () => {
const { data } = await cancelablePromise.handleRequest(
request({
url: '/tab2',
})
)

context.value = data
}
</script>

最后看下效果


取消请求5.gif


六、总结


到这里,我们前端取消请求和取消重复请求的方法就学习完了,需要注意的是,即使是使用官方的方法,也仅仅是取消服务器还没接收到的请求,如果请求已经发送到了服务端是取消不了的,只能让后端同时去处理了,使用promise的方法,仅仅只是通过改变promise的状态来阻止响应结果的接收,服务还是照常发送的。今天的分享就到这里了,如果对你有帮助的,请给我一个赞吧!

 










 


收起阅读 »

100w的数据表比1000w的数据表查询更快吗?

当我们对一张表发起查询的时候,是不是这张表的数据越少,查询的就越快?答案是不一定,这和mysql B+数索引结构有一定的关系。innodb逻辑存储结构从Innodb存储引擎的逻辑存储结构来看,所有数据都被逻辑的放在一个表空间(tablespace)中,默认情况...
继续阅读 »

当我们对一张表发起查询的时候,是不是这张表的数据越少,查询的就越快?

答案是不一定,这和mysql B+数索引结构有一定的关系。

innodb逻辑存储结构

从Innodb存储引擎的逻辑存储结构来看,所有数据都被逻辑的放在一个表空间(tablespace)中,默认情况下,所有的数据都放在一个表空间中,当然也可以设置每张表单独占用一个表空间,通过innodb_file_per_table来开启。

mysql> show variables like 'innodb_file_per_table';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_file_per_table | ON   |
+-----------------------+-------+
1 row in set (0.00 sec)

表空间又是由各个段组成的,常见的有数据段,索引段,回滚段等。因为innodb的索引类型是b+树,那么数据段就是叶子结点,索引段为b+的非叶子结点。

段空间又是由区组成的,在任何情况下,每个区的大小都为1M,innodb引擎一般默认页的大小为16k,一般一个区中有64个连续的页(64*16k=1M)。

通过段我们知道,还存在一个最小的存储单元页。它是innodb管理的最小的单位,默认是16K,当然也可以通过innodb_page_size来设置为4K、8K...,我们的数据都是存在页中的

mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name   | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.00 sec)

所以innodb的数据结构应该大致如下:


B+ 树

b+树索引的特点就是数据存在叶子结点上,并且叶子结点之间是通过双向链表方式组织起来的。

假设存在这样一张表:

CREATE TABLE `user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL DEFAULT '',
`age` int(10) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

聚集索引

对于主键索引id,假设它的b+树结构可能如下:


  • 此时树的高度是2

  • 叶子节点之间双向链表连接

  • 叶子结点除了id外,还存了name、age字段(叶子结点包含整行数据)

我们来看看 select * from user where id=30 是如何定位到的。

  • 首先根据id=30,判断在第一层的25-50之间

  • 通过指针找到在第二层的p2中

  • 把p2再加载到内存中

  • 通过二分法找到id=30的数据

总结:可以发现一共发起两次io,最后加载到内存检索的时间忽略不计。总耗时就是两次io的时间。

非聚集索引

通过表结构我们知道,除了id,我们还有name这个非聚集索引。所以对于name索引,它的结构可能如下:


  • 此时树的高度是2

  • 叶子节点之间双向链表连接

  • 叶子结点除了name外,还有对应的主键id

我们来看看 select * from user where name=jack 是如何定位到的。

  • 首先根据 name=jack,判断在第一层的mary-tom之间

  • 通过指针找到在第二层的p2中

  • 把p2再加载到内存中

  • 通过二分法找到name=jack的数据(只有name和id)

  • 因为是select *,所以通过id再去主键索引查找

  • 同样的原理最终在主键索引中找到所有的数据

总结:name查询两次io,然后通过id再次回表查询两次io,加载到内存的时间忽略不计,总耗时是4次io。另外,搜索公众号GitHub猿后台回复“天猫”,获取一份惊喜礼包。

一棵树能存多少数据

以上面的user表为例,我们先看看一行数据大概需要多大的空间:通过show table status like 'user'\G

mysql> show table status like 'user'\G
*************************** 1. row ***************************
          Name: user
        Engine: InnoDB
      Version: 10
    Row_format: Dynamic
          Rows: 10143
Avg_row_length: 45
  Data_length: 458752
Max_data_length: 0
  Index_length: 311296
    Data_free: 0
Auto_increment: 10005
  Create_time: 2021-07-11 17:22:56
  Update_time: 2021-07-11 17:31:52
    Check_time: NULL
    Collation: utf8mb4_general_ci
      Checksum: NULL
Create_options:
      Comment:
1 row in set (0.00 sec)

我们可以看到Avg_row_length=45,那么一行数据大概占45字节,因为一页的大小是16k,那么一页可以存储的数据是16k/45b = 364行数据,这是叶子结点的单page存储量。

以主键索引id为例,int占用4个字节,指针大小在InnoDB中占6字节,这样一共10字节,从root结点出来多少个指针,就可以知道root的下一层有多少个页。因为root结点只有一页,所以此时就是16k/10b = 1638个指针。

  • 如果树的高度是2,那么能存储的数据量就是1638 * 364 = 596232

  • 如果树的高度是3,那么能存储的数据量就是1638 * 1638 * 364 = 976628016


如何知道一个索引树的高度

innodb引擎中,每个页都包含一个PAGE_LEVEL的信息,用于表示当前页所在索引中的高度。默认叶子节点的高度为0,那么root页的PAGE_LEVEL + 1就是这棵索引的高度。


那么我们只要找到root页的PAGE_LEVEL就行了。

通过以下sql可以定位user表的索引的page_no:

mysql> SELECT b.name, a.name, index_id, type, a.space, a.PAGE_NO FROM information _schema.INNODB_SYS_INDEXES a, information _schema.INNODB_SYS_TABLES b WHERE a.table_id = b.table_id AND a.space <> 0 and b.name='test/user';
+-----------+---------+----------+------+-------+---------+
| name     | name   | index_id | type | space | PAGE_NO |
+-----------+---------+----------+------+-------+---------+
| test/user | PRIMARY |     105 |   3 |   67 |       3 |
| test/user | name   |     106 |   0 |   67 |       4 |
+-----------+---------+----------+------+-------+---------+
2 rows in set (0.00 sec)

可以看到主键索引的page_no=3,因为PAGE_LEVEL在每个页的偏移量64位置开始,占用两个字节。所以算出它在文件中的偏移量:16384*3 + 64 = 49152 + 64 =49216,再取前两个字节就是root的PAGE_LEVEL了。

通过以下命令找到ibd文件目录

show global variables like "%datadir%" ;
+---------------+-----------------------+
| Variable_name | Value                 |
+---------------+-----------------------+
| datadir       | /usr/local/var/mysql/ |
+---------------+-----------------------+
1 row in set (0.01 sec)

user.ibd/usr/local/var/mysql/test/下。

通过hexdump来分析data文件。

hexdump -s 49216 -n 10 user.ibd
000c040 00 01 00 00 00 00 00 00 00 69
000c04a
000c040 00 01 00 00 00 00 00 00 00 69

00 01就是说明PAGE_LEVEL=1,那么树的高度就是1+1=2

回到题目

100w的数据表比1000w的数据表查询更快吗?通过查询的过程我们知道,查询耗时和树的高度有很大关系。如果100w的数据如果和1000w的数据的树的高度是一样的,那其实它们的耗时没什么区别。

来源:juejin.cn/post/6984034503362609165

收起阅读 »

PHP语法和PHP变量

PHP
一.PHP语言标记在一个后缀为.php的文件立马,以<?php ?>开始和结束的文件,就是php标记文件,具体格式如下:1.xml风格,是PHP的标准风格,推荐使用 2.简短风格,遵循SGML处理。需要在php.ini中将指令short_open...
继续阅读 »

一.PHP语言标记

在一个后缀为.php的文件立马,以<?php ?>开始和结束的文件,就是php标记文件,具体格式如下:

1.xml风格,是PHP的标准风格,推荐使用

2.简短风格,遵循SGML处理。需要在php.ini中将指令short_open_tag打开,或者在php编译时加入–enable-short-tags.如果你想你的程序移植性好,就抛弃这种风格,它就比1.1少了个php

3.ASP 风格(已移除)

种标记风格与 ASP 或 ASP.NET 的标记风格相同,默认情况下这种风格是禁用的。如果想要使用它需要在配置设定中启用了 asp_tags 选项。
不过该标记风格在 PHP7 中已经不再支持,了解即可。

4.SCRIPT 风格(已移除)

种标记风格是最长的,如果读者使用过 JavaScript 或 VBScript,就会熟悉这种风格。该标记风格在 PHP7 中已经不再支持,了解即可。
注意:如果文件内容是纯 PHP 代码,最好将文件末尾的 PHP 结束标记省略。这样可以避免在 PHP 结束标记之后,意外插入了空格或者换行符之类的误操作,而导致输出结果中意外出现空格和换行

位置

可以将PHP语言放在后缀名为.php的HTML文件的任何地方。注意了,是以.php结尾的HTML文件。比如

PHP 注释规范

单行注释 每行必须单独使用注释标记,称为单行注释。它用于进行简短说明,形如 //php

多行注释

多行注释用于注释多行内容,经常用于多行文本的注释。注释的内容需要包含在(/* 和 */)中,以“/*”开头,以“*/结尾

php里面常见的几种注释方式

1.文件头的注释,介绍文件名,功能以及作者版本号等信息

2.函数的注释,函数作用,参数介绍及返回类型

3.类的注释


二.PHP变量

什么是变量呢?

程序中的变量源于数学,在程序语言中能够储存结果或者表示抽象概念。简单理解变量就是临时存储值的容器,它可以储存数字、文本、或者一些复杂的数据等。变量在 PHP 中居于核心地位,是使用 PHP 的关键所在,变量的值在程序运行中会随时发生变化,能够为程序中准备使用的一段数据起一个简短容易记的名字,另外它还可以保存用户输入的数据或运算的结果。

声明(创建)变量

因为 PHP 是一种弱类型的语言,所以使用变量前不用提前声明,变量在第一次赋值时会被自动创建,这个原因使得 PHP 的语法和C语言、Java 等强类型语言有很大的不同。声明 PHP 变量必须使用一个美元符号“$”后面跟变量名来表示,然后再使用“=”给这个变量赋值。如下所示


变量命名规则

变量名并不是可以随意定义的,一个有效的变量名应该满足以下几点要求:
1. 变量必须以 $ 符号开头,其后是变量的名称,$ 并不是变量名的一部分;
2. 变量名必须以字母或下划线开头;
3. 变量名不能以数字开头;
4.变量名只能包含字母(A~z)、数字(0~9)和下划线(_);
5.与其它语言不通的是,PHP 中的一些关键字也可以作为变量名(例如 $true、$for)。
注意:PHP 中的变量名是区分大小写的,因此 $var 和 $Var 表示的是两个不同的变量

错误的变量命名示范


当使用多个单词构成变量名时,可以使用下面的命名规范:
下划线命名法:将构成变量名的单词以下划线分割,例如 $get_user_name、$set_user_name;
驼峰式命名法(推荐使用):第一个单词全小写,后面的单词首字母小写,例如 $getUserName、$getDbInstance;
帕斯卡命名法:将构成变量名的所有单词首字母大写,例如 $Name、$MyName、$GetName。
收起阅读 »

PHP 基本语法2

PHP
一、PHP 标记PHP 也是通过标记来识别的,像 JSP 的 <% %> 的一样,PHP 的最常用的标记是:<?php php 代码 ?> 。 以 “<?” 开始,“?>”结束。 该风格是最简单的标记风格,默认是禁止的,可...
继续阅读 »

一、PHP 标记

PHP 也是通过标记来识别的,像 JSP 的 <% %> 的一样,PHP 的最常用的标记是:<?php php 代码 ?>

以 “<?” 开始,“?>”结束。
该风格是最简单的标记风格,默认是禁止的,可以通过修改 short_open_tag 选项来允许使用这种风格。

[捂脸哭] 我们其实目前不需要去配置这个风格哈,老老实实用 <?php php 代码 ?> 就够了~

二、基础语法

1. PHP 语句都以英文分号【;】结束。

2. PHP 注释

大体上有三种:

<?php
/*
多行注释
*/

echo "string";// 单行注释
echo "string";# 单行注释
?>

sublime text 3 神奇快捷键:ctrl shift d => 复制当前行到下一行

3. 输出语句:echo

<?php
echo "string";
echo("string");
?>

PHP 可以嵌套在 HTML 里面写,所以也可以输出 HTML、CSS、JavaScript 语句等。

 <font id="testPhpJs"></font>
<?php
echo "<style type='text/css'>#testPhpJs {color: red}</style>";
echo "<h1>一级标题</h1>";
echo "<script>var font = document.getElementById('testPhpJs');font.innerText='php输出js填充的文字';</script>";
?>
<input type="text" name="test" value="<?php echo "123"; ?>">


网页输出结果:

4. 变量及变量类型

PHP 的类型有六种,整型、浮点型、字符串、布尔型、数组、对象。

但是定义的方式只有一种:$ 变量名。PHP 变量的类型会随着赋值的改变而改变(动态类型)

<?php
$variable = 1; //整型
$variable = 1.23; //浮点型
$variable = "字符串"; //字符串 ""
$variable = '字符串'; //字符串 ''
$variable = false; //布尔型
?>

特殊的变量(见附录)。

5. 字符串

关于字符串,我们还有几点需要说的:

a. 双引号和单引号

这两者包起来的都是字符串:'阿'"阿"。注意单引号里不能再加单引号,双引号里不能再加双引号,实在要加的话记得用转义符 “ \

b. 定界符

如果想输出很大一段字符串,那么就需要定界符来帮忙。定界符就是由头和尾两部分。

<?php
echo <<<EOT
hello world!
lalala~
EOT;
// 这个定界符的尾巴和前面<<<后面的字符应该一样
// !定界符的尾巴必须靠在最左边
?>

定界符的名字是自己起的,乐意叫啥就叫啥,但是它的尾巴必须靠在最左边,不能有任何其他的字符!空格也不行:

<?php
//定界符的名字随便起
echo <<<ERROR
ERROR;
//但是尾巴必须靠左,前面不能有任何东西。比如这样就是错的 ↑
?>

看!上面这个注释都变成绿色了~ 它都报错了,大家写的时候可不能这么写哦~O(∩_∩)O哈哈~

6. 字符串连接

不同于 Java 的 “+” 号连接符,PHP 用的是点【.】。在做数据库查询语句的时候,常会遇到要与变量拼接的情况。这里给个小技巧:

在数据库相关软件中先用一个数据例子写好查询语句,并测试直到执行成功:

然后将数据换成变量:

  1. 将 sql 语句用字符串变量存储。
  2. 将写死的数据换成两个双引号
  3. 在双引号中间加两个连接符 点【.】
  4. 在连接符中间将变量放入
<?php
$isbn = "9787508353937";//存储isbn的变量
$sql = "SELECT * FROM bookinfo WHERE isbn = '9787508353937'";
// $sql = "SELECT * FROM bookinfo WHERE isbn = '""'";
// $sql = "SELECT * FROM bookinfo WHERE isbn = '".."'";
$sql = "SELECT * FROM bookinfo WHERE isbn = '".$isbn."'";
//修改完成
?>

保证不会出错哈哈(这个多用于数据库的增删改查,避免 sql 语句的错误)

7. 表单数据

表单在提交数据的时候,method 有两种方式:post & get。所以 PHP 有几种不同的方式来获取表单数据:

<?php
$_POST['表单控件名称'] //对应POST方式提交的数据
$_GET['表单控件名称'] //对应GET方式提交的数据
$_REQUEST['表单控件名称'] //同时适用于两种方式
?>

8. 运算符

运算符和其他语言基本一致,如果不了解的可以去看看我的 java 运算符(https://blog.csdn.net/ahanwhite/article/details/89461167)。

但这里还是有一个比较特殊的:

字符串连接赋值:【.=】

<?php
$str = "这是连接";
$str .= "字符串的运算符";
// 那么现在的$str = "这是连接字符串的运算符";
?>

9. 分支与选择

同样和其他语言差别不大,有兴趣可以看我的 java 控制语句(https://blog.csdn.net/ahanwhite/article/details/89461652

10. PHP 函数

PHP 的函数和 Java 还是有点儿区别,定义的格式:

<?php
function 函数名($参数) {
函数体;
}
?>

a. 函数参数可以为空

b. 如果需要修改函数的值,可以使用引用参数传递,但是需要在参数前面加上【&】

c. 函数的参数可以使用默认值,在定义函数是参数写成: $ 参数 =“默认值”; 即可。(默认值又叫缺省值)。

<?php
//改变参数变量的值
function myName(&$name) {
$name = "baibai";
echo $name;
}
$name = "huanhuan";
myName($name);
//设置默认参数值
function myName2($name="baibai") {
echo "<br>".$name;
}
//不传参测试默认值
myName2();
?>


输出结果:

d. PHP 也有一些自己的系统函数(比如 echo),这里再列几个常用的字符串函数:

  • 字符串长度计算
$a = mb_strlen("abdsd");
$b = mb_strlen("lalalal",'UTF-8')

我一般用后面这个,按 utf-8 编码计算长度。

  • 在一个字符串中查找另一个字符串
strstr(字符串1,字符串2)

补充一个函数 var_dump() 【实名感谢石老师】
用来判断一个变量的类型与长度, 并输出变量的数值, 如果变量有值输的是变量的值并回返数据类型. 此函数显示关于一个或多个表达式的结构信息,包括表达式的类型与值。数组将递归展开值,通过缩进显示其结构。

<?php
$a = strstr("asgduiashufai","dui");
$b = strstr("asgduiashufai","?");

echo var_dump($a);
echo "<br>";
echo var_dump($b);
?>

如果存在前面的字符串里存在后面的字符串,那么会返回字符串 2 以及在字符串 1 里后面的所有字符。如果不存在,就会返回 false(但是不能直接输出,直接输出好像是空值,判断一下再输出提示信息会比较好)

  • 按照 ASCII 码比较两个字符串大小
strcmp("字符串1","字符串2")

//1比2打,返回大于0,2比1打,返回小于0,一样大的话返回等于0
  • 将 html 标记作为字符串输出
htmlspecialchars("字符串")
  • 改变字符串大小写
strtolower("字符串");//将字符串全变成小写

strtoupper("字符串");//将字符串全变成大写
  • 加密函数
    md5() 将一个字符串进行 MD5 加密计算。(没有解密的函数,用于密码,检验时将用户提交的密码加密之后进行对比)
$a = md5("字符串");

附录

特殊的变量


收起阅读 »

PHP-Beast 加密你的PHP源代码

PHP
前言首先说说为什么要用PHP-Beast? 有时候我们的代码会放到代理商上, 所以很有可能代码被盗取,或者我们写了一个商业系统而且不希望代码开源,所以这时候就需要加密我们的代码。 另外PHP-Beast是完全免费和开源的, 当其不能完成满足你的需求时, 可...
继续阅读 »

前言

首先说说为什么要用PHP-Beast?
有时候我们的代码会放到代理商上, 所以很有可能代码被盗取,或者我们写了一个商业系统而且不希望代码开源,所以这时候就需要加密我们的代码。
另外PHP-Beast是完全免费和开源的, 当其不能完成满足你的需求时, 可以修改其代码而满足你的要。

编译安装如下

注意:如果你需要使用,首先修改key。可以参考下文

Linux编译安装:
$ wget https://github.com/liexusong/php-beast/archive/master.zip
$ unzip master.zip
$ cd php-beast-master
$ phpize
$ ./configure
$ sudo make && make install

编译好之后修改php.ini配置文件, 加入配置项: extension=beast.so, 重启php-fpm 。

配置项:
 beast.cache_size = size
beast.log_file = "path_to_log"
beast.log_user = "user"
beast.enable = On
beast.log_level支持参数:
 1. DEBUG
2. NOTICE
3. ERROR
支持的模块有:
 1. AES
2. DES
3. Base64
通过测试环境:
Nginx + Fastcgi + (PHP-5.2.x ~ PHP-7.1.x)

怎么加密你的项目

加密方案1:

安装完 php-beast 后可以使用 tools 目录下的 encode_files.php 来加密你的项目。使用 encode_files.php 之前先修改 tools 目录下的 configure.ini 文件,如下:

; source path
src_path = ""
; destination path
dst_path = ""
; expire time
expire = ""
; encrypt type (selection: DES, AES, BASE64)
encrypt_type = "DES"

src_path 是要加密项目的路径,dst_path 是保存加密后项目的路径,expire 是设置项目可使用的时间 (expire 的格式是:YYYY-mm-dd HH:ii:ss)。encrypt_type是加密的方式,选择项有:DES、AES、BASE64。 修改完 configure.ini 文件后就可以使用命令 php encode_files.php 开始加密项目。

加密方案2:

使用beast_encode_file()函数加密文件,函数原型如下:

beast_encode_file(string $input_file, string $output_file, int expire_timestamp, int encrypt_type)
  1. $input_file: 要加密的文件
  2. $output_file: 输出的加密文件路径
  3. $expire_timestamp: 文件过期时间戳
  4. $encrypt_type: 加密使用的算法(支持:BEAST_ENCRYPT_TYPE_DES、BEAST_ENCRYPT_TYPE_AES)

制定自己的php-beast

php-beast 有多个地方可以定制的,以下一一列出:

  1. 使用 header.c 文件可以修改 php-beast 加密后的文件头结构,这样网上的解密软件就不能认识我们的加密文件,就不能进行解密,增加加密的安全性。
  2. php-beast 提供只能在指定的机器上运行的功能。要使用此功能可以在 networkcards.c 文件添加能够运行机器的网卡号,例如:
char *allow_networkcards[] = {
"fa:16:3e:08:88:01",
NULL,
};

这样设置之后,php-beast 扩展就只能在 fa:16:3e:08:88:01 这台机器上运行。另外要注意的是,由于有些机器网卡名可能不一样,所以如果你的网卡名不是 eth0 的话,可以在 php.ini 中添加配置项: beast.networkcard = "xxx" 其中 xxx 就是你的网卡名,也可以配置多张网卡,如:beast.networkcard = "eth0,eth1,eth2"。

  1. 使用 php-beast 时最好不要使用默认的加密key,因为扩展是开源的,如果使用默认加密key的话,很容易被人发现。所以最好编译的时候修改加密的key,aes模块 可以在 aes_algo_handler.c 文件修改,而 des模块 可以在 des_algo_handler.c 文件修改。

函数列表 & Debug

开启debug模式:

可以在configure时加入 --enable-beast-debug 选项来开启debug模式。开启debug模式后需要在php.ini配置文件中加入配置项:beast.debug_path 和 beast.debug_mode。beast.debug_mode 用于指定是否使用debug模式,而 beast.debug_path 用于输出解密后的php脚本源码。这样就可以在 beast.debug_path 目录中看到php-beast解密后的源代码,可以方便知道扩展解密是否正确。

函数列表:
  1. beast_encode_file(): 用于加密一个文件
  2. beast_avail_cache(): 获取可以缓存大小
  3. beast_support_filesize(): 获取beast支持的最大可加密文件大小
  4. beast_file_expire(): 获取一个文件的过期时间
  5. beast_clean_cache(): 清空beast的所有缓存(如果有文件更新, 可以使用此函数清空缓存)

修改默认加密的key

1,修改加密后的文件头结构:打开header.c文件,找到以下代码:

char encrypt_file_header_sign[] = {
0xe8, 0x16, 0xa4, 0x0c,
0xf2, 0xb2, 0x60, 0xee
};

int encrypt_file_header_length = sizeof(encrypt_file_header_sign);
自定义修改以下代码(其中的数字的范围为:0-8,字母的范围为:a-f):

0xe8, 0x16, 0xa4, 0x0c,
0xf2, 0xb2, 0x60, 0xee

2,修改aes模块加密key:
打开php-beast-master/aes_algo_handler.c文件,找到以下代码:

static uint8_t key[] = {
0x2b, 0x7e, 0x61, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xi7, 0x10, 0x88, 0x09, 0xcf, 0xef, 0xxc,
};

自定义修改以下代码(其中的数字的范围为:0-8,字母的范围为:a-f):

0x2b, 0x7e, 0x61, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xi7, 0x10, 0x88, 0x09, 0xcf, 0xef, 0xxc,

3,修改des模块加密key:
打开php-beast-master/des_algo_handler.c文件,找到以下代码:

static char key[8] = {
0x21, 0x1f, 0xe1, 0x1f,
0xy1, 0x9e, 0x01, 0x0e,
};

自定义修改以下代码(其中的数字的范围为:0-8,字母的范围为:a-f):

0x21, 0x1f, 0xe1, 0x1f,
0xy1, 0x9e, 0x01, 0x0e,

4,修改base64模块加密key:
打开php-beast-master/base64_algo_handler.c文件,自定义修改以下代码:

static const short base64_reverse_table[256] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
};

php-beast自定义加密模块

一,首先创建一个.c的文件。例如我们要编写一个使用base64加密的模块,可以创建一个名叫base64_algo_handler.c的文件。然后在文件添加如下代码:
#include "beast_module.h"
int base64_encrypt_handler(char *inbuf, int len, char **outbuf, int *outlen)
{
...
}
int base64_decrypt_handler(char *inbuf, int len, char **outbuf, int *outlen)
{
...
}
void base64_free_handler(void *ptr)
{
...
}
struct beast_ops base64_handler_ops = {
.name = "base64-algo",
.encrypt = base64_encrypt_handler,
.decrypt = base64_decrypt_handler,
.free = base64_free_handler,
};

模块必须实现3个方法,分别是:encrypt、decrypt、free方法。
encrypt方法负责把inbuf字符串加密,然后通过outbuf输出给beast。
decrypt方法负责把加密数据inbuf解密,然后通过outbuf输出给beast。
free方法负责释放encrypt和decrypt方法生成的数据

二,写好我们的加密模块后,需要在global_algo_modules.c添加我们模块的信息。代码如下:
#include <stdlib.h>
#include "beast_module.h"
extern struct beast_ops des_handler_ops;
extern struct beast_ops base64_handler_ops;
struct beast_ops *ops_handler_list[] = {
&des_handler_ops,
&base64_handler_ops, /* 这里是我们的模块信息 */
NULL,
};
三,修改config.m4文件,修改倒数第二行,如下代码:

PHP_NEW_EXTENSION(beast, beast.c des_algo_handler.c beast_mm.c spinlock.c cache.c beast_log.c global_algo_modules.c * base64_algo_handler.c *, $ext_shared)

base64_algo_handler.c的代码是我们添加的,这里加入的是我们模块的文件名。
现在大功告成了,可以编译试下。如果要使用我们刚编写的加密算法来加密php文件,可以修改php.ini文件的配置项,如下:
``
beast.encrypt_handler = "base64-algo"`

名字就是我们模块的name。


转载自:https://cloud.tencent.com/developer/article/1911039

收起阅读 »

Java中的数据类型

Java是强类型语言什么是强类型语言? 就是一个变量只能对应一种类型。而不是模棱两可的类型符号。 下面我通过一个例子来解释一下这个现象.javascript中可以用var表示许多数据类型// 此时a为number var a = 1; // 此时a为字符串...
继续阅读 »

Java是强类型语言

什么是强类型语言?
就是一个变量只能对应一种类型。而不是模棱两可的类型符号。
下面我通过一个例子来解释一下这个现象.

// 此时a为number
var a = 1;
// 此时a为字符串形式的'1'
var a = '1';

可以看到,javascript里面,可以用var来承载各种数据类型,但是在Java,你必须对变量声明具体的数据类型(Java10中也开放了var,目前我们讨论的版本为Java8) 。

8大数据类型

基本类型

存储所需大小

取值范围

int

4字节

-2147483648~2147483647

short

2字节

-32768~32767

long

8字节

-9223372036854775808~9223372036854775807

byte

1字节

-128~127

float

4字节

1.4e-45f~ 3.4028235e+38f

double

8字节

4.9e-324~1.7976931348623157e+308

char

2字节

\u0000~\uFFFF

boolean

根据JVM的编译行为会有不同的结果(1/4)

布尔(boolean)类型的大小没有明确的规定,通常定义为取字面值 “true” 或 “false”

NaN与无穷大

  • NaN

在浮点数值计算中,存在一个NaN来表示该值不是一个数字

/**
* @author jaymin<br>
* 如何表示一个值不是数字
* 2021/3/21 14:54
*/

public class NaNDemo {
public static void main(String[] args) {
Double doubleNaN = new Double(0.0/0.0);
// 一个常数,其值为double类型的非数字(NaN)值
Double nan = Double.NaN;
System.out.println(doubleNaN.isNaN());
System.out.println(nan.isNaN());
}
}
  • 正负无穷大
    private static void isPositiveInfinityAndNegativeInfinity(){
double positiveInfinity = Double.POSITIVE_INFINITY;
double negativeInfinity = Double.NEGATIVE_INFINITY;
System.out.println(positiveInfinity);
System.out.println(negativeInfinity);
}

Result:

Infinity
-Infinity

浮点数存在精度问题

Java中无法用浮点数值来表示分数,因为浮点数值最终采用二进制系统表示。

/**
* @author jaymin<br>
* 浮点数无法表示分数
* @since 2021/3/21 15:07
*/

public class PrecisionDemo {
public static void main(String[] args) {
System.out.println(2.0 - 1.1);
// 如何解决?使用BigDecimal
BigDecimal a = BigDecimal.valueOf(2.0);
BigDecimal b = BigDecimal.valueOf(1.1);
System.out.println(a.subtract(b));
}
}

精度

向上转型和向下强转

  • 向上转型
/**
*
*
* @author jaymin
* @since 2021/3/21 15:40
*/

public class ForcedTransfer {

public static void main(String[] args) {
int n = 123456789;
// 整型向上转换丢失了精度
float f = n;
System.out.println(f);
int n1 = 1;
float f1 = 2.2f;
// 不同类型的数值进行运算,将向上转型
System.out.println(n1 + f1);
}
}

这里我们看到两个现象:

  1. 整型可以赋值给浮点型,但是可能会丢失精度.
  2. 整形和浮点数进行相加,先将整型向上转型为float,再进行float的运算.

层级关系:double>float>long>int

  • 面试官经常问的一个细节

此处能否通过编译?

short s1= 1;
s1 = s1 + 1;

答案是不能的,如果我们对小于 int 的基本数据类型(即 char、byte 或 short)执行任何算术或按位操作,这些值会在执行操作之前类型提升为 int,并且结果值的类型为 int。若想重新使用较小的类型,必须使用强制转换(由于重新分配回一个较小的类型,结果可能会丢失精度).
可以简单理解为: 比int类型数值范围小的数做运算,最终都会提升为int,当然,使用final可以帮助你解决这种问题.

  • 正确示例
short s1= 1;
// 1. 第一个种解决办法
s1 = (short) (s1 + 1);
// 2. 第二种解决办法
s1+=1;
        final short a1 = 1;
final short a2 = 2;
short result = a1 + a2;
  • 向下转型(强制转换)

场景: 在程序中得到了一个浮点数,此时将其转成整形,那么你就可以使用强转.

/**
* 数值之间的强转
*
* @author jaymin
* @since 2021/3/21 15:40
*/

public class ForcedTransfer {

public static void main(String[] args) {
double x = 2021.0321;
// 强转为整型
int integerX = (int) x;
System.out.println(integerX);
x = 2021.8888;
// 四舍五入
int round = (int) Math.round(x);
System.out.println(round);
}
}

Result:

2021  
2022

如果强转的过程中,上层的数据类型范围超出了下层的数据类型范围,那么会进行截断.
可以执行以下程序来验证这个问题.

        long l = Long.MAX_VALUE;
int l1 = (int) l;
System.out.println(l1);
int i = 300;
byte b = (byte) i;
// 128*2 = 256,300-256=44
System.out.println(b);

Reuslt:

-1
44

初始值

基本数据类型都会有默认的初始值.

基本类型

初始值

boolean

false

char

\u0000 (null)

byte

(byte) 0

short

(short) 0

int

0

long

0L

float

0.0f

double

0.0d

在定义对象的时候,如果你使用了基本类型,那么类在初始化后,如果你没有显性地赋值,那么就会为默认值。这在某些场景下是不对的(比如你需要在http中传输id,当对方没有传输id时,你应该报错,但是由于使用了基本的数据类型,id拥有了默认值0,那么此时程序就会发生异常)

定义对象的成员,最好使用包装类型,而不是基础类型.

Integer对象的缓存区

在程序中有些值是需要经常使用的,比如定义枚举时,经常会使用1,2,3作为映射值.Java的语言规范JLS中要求将-128到127的值进行缓存。(高速缓存的大小可以由-XX:AutoBoxCacheMax = <size>选项控制。在VM初始化期间,可以在sun.misc.VM类的私有系统属性中设置并保存java.lang.Integer.IntegerCache.high属性。)

  • 使用==比较Integer可能会出现意想不到的结果
    public static void main(String[] args) {
Integer a1 = Integer.valueOf(127);
Integer a2 = Integer.valueOf(127);
System.out.println(a1==a2);
Integer a3 = Integer.valueOf(128);
Integer a4 = Integer.valueOf(128);
System.out.println(a3==a4);
}

Result:

true
false

解决的办法很简单,使用equals来进行比较即可,Integer内部重写了equals和hashcode.

常用的一些转义字符

在字符串中,如果你想让输出的字符串换行,你就需要用到转义字符

转义字符

Unicode

含义

\b

\u0008

退格

\t

\u0009

制表

\n

\u000a

换行

\r

\u000d

回车

\"

\u0022

双引号

\'

\u0027

单引号

\\

\u005c

反斜杠

\\.

-

.

  • 换行输出字符串
    System.out.println("我马上要换行了\n我是下一行");
收起阅读 »

Java文字转图片防爬虫

最近部分页面数据被爬虫疯狂的使用,主要就是采用动态代理IP爬取数据,主要是不控制频率,这个最恶心。因为对方是采用动态代理的方式,所以没什么特别好的防止方式。具体防止抓取数据方案大全,下篇博客我会做一些讲解。本篇也是防爬虫的一个方案。就是部分核心文字采用图片输出...
继续阅读 »

最近部分页面数据被爬虫疯狂的使用,主要就是采用动态代理IP爬取数据,主要是不控制频率,这个最恶心。因为对方是采用动态代理的方式,所以没什么特别好的防止方式。

具体防止抓取数据方案大全,下篇博客我会做一些讲解。本篇也是防爬虫的一个方案。就是部分核心文字采用图片输出。加大数据抓取方的成本。

图片输出需求

上图红色圈起来的数据为图片输出了备案号,就是要达到这个效果,如果数据抓取方要继续使用,必须做图片解析,成本和难度都加到了。也就是我们达到的效果了。

Java代码实现

import javax.imageio.ImageIO;

import java.awt.*;

import java.awt.font.FontRenderContext;

import java.awt.geom.AffineTransform;

import java.awt.geom.Rectangle2D;

import java.awt.image.BufferedImage;

import java.io.File;

import java.nio.file.Paths;

public class ImageDemo {

public static void main(String[] args) throws Exception {

System.out.println(System.currentTimeMillis());

//输出目录

String rootPath = "/Users/sojson/Downloads/";

//这里文字的size,建议设置大一点,其实就是像素会高一点,然后缩放后,效果会好点,最好是你实际输出的倍数,然后缩放的时候,直接按倍数缩放即可。

Font font = new Font("微软雅黑", Font.PLAIN, 130);

createImage("https://www.sojson.com", font, Paths.get(rootPath, "sojson-image.png").toFile());

}

private static int[] getWidthAndHeight(String text, Font font) {

Rectangle2D r = font.getStringBounds(text, new FontRenderContext(

AffineTransform.getScaleInstance(1, 1), false, false));

int unitHeight = (int) Math.floor(r.getHeight());//

// 获取整个str用了font样式的宽度这里用四舍五入后+1保证宽度绝对能容纳这个字符串作为图片的宽度

int width = (int) Math.round(r.getWidth()) + 1;

// 把单个字符的高度+3保证高度绝对能容纳字符串作为图片的高度

int height = unitHeight + 3;

return new int[]{width, height};

}

// 根据str,font的样式以及输出文件目录

public static void createImage(String text, Font font, File outFile)

throws Exception {

// 获取font的样式应用在输出内容上整个的宽高

int[] arr = getWidthAndHeight(text, font);

int width = arr[0];

int height = arr[1];

// 创建图片

BufferedImage image = new BufferedImage(width, height,

BufferedImage.TYPE_INT_BGR);//创建图片画布

//透明背景 the begin

Graphics2D g = image.createGraphics();

image = g.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);

g=image.createGraphics();

//透明背景 the end

/**

如果你需要白色背景或者其他颜色背景可以直接这么设置,其实就是满屏输出的颜色

我这里上面设置了透明颜色,这里就不用了

*/

//g.setColor(Color.WHITE);

//画出矩形区域,以便于在矩形区域内写入文字

g.fillRect(0, 0, width, height);

/**

* 文字颜色,这里支持RGB。new Color("red", "green", "blue", "alpha");

* alpha 我没用好,有用好的同学可以在下面留言,我开始想用这个直接输出透明背景色,

* 然后输出文字,达到透明背景效果,最后选择了,createCompatibleImage Transparency.TRANSLUCENT来创建。

* android 用户有直接的背景色设置,Color.TRANSPARENT 可以看下源码参数。对alpha的设置

*/

g.setColor(Color.gray);

// 设置画笔字体

g.setFont(font);

// 画出一行字符串

g.drawString(text, 0, font.getSize());

// 画出第二行字符串,注意y轴坐标需要变动

g.drawString(text, 0, 2 * font.getSize());

//执行处理

g.dispose();

// 输出png图片,formatName 对应图片的格式

ImageIO.write(image, "png", outFile);

}

}

输出图片效果:


当然我这里是做了放缩,要不然效果没那么好。

注意点:

其实代码里注释说的已经比较清楚了。主要设置透明色这里。

//透明背景 the begin
Graphics2D g = image.createGraphics();
image = g.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
g=image.createGraphics();
//透明背景 the end

Android 参考的颜色值

android.graphics.Color 包含颜色值
Color.BLACK 黑色
Color.BLUE 蓝色
Color.CYAN 青绿色
Color.DKGRAY 灰黑色
Color.GRAY 灰色
Color.GREEN 绿色
Color.LTGRAY 浅灰色
Color.MAGENTA 红紫色
Color.RED 红色
Color.TRANSPARENT 透明
Color.WHITE 白色
Color.YELLOW 黄色




收起阅读 »

一日正则一日神,一直正则一直神

本篇带来 15 个正则使用场景,按需索取,收藏恒等于学会!! 千分位格式化 在项目中经常碰到关于货币金额的页面显示,为了让金额的显示更为人性化与规范化,需要加入货币格式化策略。也就是所谓的数字千分位格式化。 123456789 => ...
继续阅读 »


本篇带来 15 个正则使用场景,按需索取,收藏恒等于学会!!


千分位格式化


在项目中经常碰到关于货币金额的页面显示,为了让金额的显示更为人性化与规范化,需要加入货币格式化策略。也就是所谓的数字千分位格式化。


  1. 123456789 => 123,456,789
  2. 123456789.123 => 123,456,789.123


const formatMoney = (money) => {
return money.replace(new RegExp(`(?!^)(?=(\\d{3})+${money.includes('.') ? '\\.' : '$'})`, 'g'), ',')
}

formatMoney('123456789') // '123,456,789'
formatMoney('123456789.123') // '123,456,789.123'
formatMoney('123') // '123'


想想如果不是用正则,还可以用什么更优雅的方法实现它?


解析链接参数


你一定常常遇到这样的需求,要拿到 url 的参数的值,像这样:





// url <https://qianlongo.github.io/vue-demos/dist/index.html?name=fatfish&age=100#/home>

const name = getQueryByName('name') // fatfish
const age = getQueryByName('age') // 100
 



通过正则,简单就能实现 getQueryByName 函数:


const getQueryByName = (name) => {
const queryNameRegex = new RegExp(`[?&]${name}=([^&]*)(&|$)`)
const queryNameMatch = window.location.search.match(queryNameRegex)
// Generally, it will be decoded by decodeURIComponent
return queryNameMatch ? decodeURIComponent(queryNameMatch[1]) : ''
}

const name = getQueryByName('name')
const age = getQueryByName('age')

console.log(name, age) // fatfish, 100
 



驼峰字符串




JS 变量最佳是驼峰风格的写法,怎样将类似以下的其它声明风格写法转化为驼峰写法?


1. foo Bar => fooBar
2. foo-bar---- => fooBar
3. foo_bar__ => fooBar

正则表达式分分钟教做人:


const camelCase = (string) => {
const camelCaseRegex = /[-_\s]+(.)?/g
return string.replace(camelCaseRegex, (match, char) => {
return char ? char.toUpperCase() : ''
})
}

console.log(camelCase('foo Bar')) // fooBar
console.log(camelCase('foo-bar--')) // fooBar
console.log(camelCase('foo_bar__')) // fooBar
 

小写转大写


这个需求常见,无需多言,用就完事儿啦:


const capitalize = (string) => {
const capitalizeRegex = /(?:^|\s+)\w/g
return string.toLowerCase().replace(capitalizeRegex, (match) => match.toUpperCase())
}

console.log(capitalize('hello world')) // Hello World
console.log(capitalize('hello WORLD')) // Hello World

实现 trim()


trim() 方法用于删除字符串的头尾空白符,用正则可以模拟实现 trim:


const trim1 = (str) => {
return str.replace(/^\s*|\s*$/g, '') // 或者 str.replace(/^\s*(.*?)\s*$/g, '$1')
}

const string = ' hello medium '
const noSpaceString = 'hello medium'
const trimString = trim1(string)

console.log(string)
console.log(trimString, trimString === noSpaceString) // hello medium true
console.log(string)

trim() 方法不会改变原始字符串,同样,自定义实现的 trim1 也不会改变原始字符串;

HTML 转义


防止 XSS 攻击的方法之一是进行 HTML 转义,符号对应的转义字符:


正则处理如下:


const escape = (string) => {
const escapeMaps = {
'&': 'amp',
'<': 'lt',
'>': 'gt',
'"': 'quot',
"'": '#39'
}
// The effect here is the same as that of /[&amp;<> "']/g
const escapeRegexp = new RegExp(`[${Object.keys(escapeMaps).join('')}]`, 'g')
return string.replace(escapeRegexp, (match) => `&${escapeMaps[match]};`)
}

console.log(escape(`
<div>
<p>hello world</p>
</div>
`
))
/*
&lt;div&gt;
&lt;p&gt;hello world&lt;/p&gt;
&lt;/div&gt;
*/


HTML 反转义


有了正向的转义,就有反向的逆转义,操作如下:


const unescape = (string) => {
const unescapeMaps = {
'amp': '&',
'lt': '<',
'gt': '>',
'quot': '"',
'#39': "'"
}
const unescapeRegexp = /&([^;]+);/g
return string.replace(unescapeRegexp, (match, unescapeKey) => {
return unescapeMaps[ unescapeKey ] || match
})
}

console.log(unescape(`
&lt;div&gt;
&lt;p&gt;hello world&lt;/p&gt;
&lt;/div&gt;
`
))
/*
<div>
<p>hello world</p>
</div>
*/


校验 24 小时制


处理时间,经常要用到正则,比如常见的:校验时间格式是否是合法的 24 小时制:


const check24TimeRegexp = /^(?:(?:0?|1)\d|2[0-3]):(?:0?|[1-5])\d$/
console.log(check24TimeRegexp.test('01:14')) // true
console.log(check24TimeRegexp.test('23:59')) // true
console.log(check24TimeRegexp.test('23:60')) // false
console.log(check24TimeRegexp.test('1:14')) // true
console.log(check24TimeRegexp.test('1:1')) // true

校验日期格式


常见的日期格式有:yyyy-mm-dd, yyyy.mm.dd, yyyy/mm/dd 这 3 种,如果有符号乱用的情况,比如2021.08/22,这样就不是合法的日期格式,我们可以通过正则来校验判断:


const checkDateRegexp = /^\d{4}([-\.\/])(?:0[1-9]|1[0-2])\1(?:0[1-9]|[12]\d|3[01])$/

console.log(checkDateRegexp.test('2021-08-22')) // true
console.log(checkDateRegexp.test('2021/08/22')) // true
console.log(checkDateRegexp.test('2021.08.22')) // true
console.log(checkDateRegexp.test('2021.08/22')) // false
console.log(checkDateRegexp.test('2021/08-22')) // false

匹配颜色值


在字符串内匹配出 16 进制的颜色值:


const matchColorRegex = /#(?:[\da-fA-F]{6}|[\da-fA-F]{3})/g
const colorString = '#12f3a1 #ffBabd #FFF #123 #586'

console.log(colorString.match(matchColorRegex))
// [ '#12f3a1', '#ffBabd', '#FFF', '#123', '#586' ]

判断 HTTPS/HTTP


这个需求也是很常见的,判断请求协议是否是 HTTPS/HTTP


const checkProtocol = /^https?:/

console.log(checkProtocol.test('https://medium.com/')) // true
console.log(checkProtocol.test('http://medium.com/')) // true
console.log(checkProtocol.test('//medium.com/')) // false

校验版本号


版本号必须采用 x.y.z 格式,其中 XYZ 至少为一位,我们可以用正则来校验:


// x.y.z
const versionRegexp = /^(?:\d+\.){2}\d+$/

console.log(versionRegexp.test('1.1.1'))
console.log(versionRegexp.test('1.000.1'))
console.log(versionRegexp.test('1.000.1.1'))

获取网页 img 地址


这个需求可能爬虫用的比较多,用正则获取当前网页所有图片的地址。在控制台打印试试,太好用了~~


const matchImgs = (sHtml) => {
const imgUrlRegex = /<img[^>]+src="((?:https?:)?\/\/[^"]+)"[^>]*?>/gi
let matchImgUrls = []

sHtml.replace(imgUrlRegex, (match, $1) => {
$1 && matchImgUrls.push($1)
})
return matchImgUrls
}

console.log(matchImgs(document.body.innerHTML))

格式化电话号码


这个需求也是常见的一匹,用就完事了:


let mobile = '18379836654' 
let mobileReg = /(?=(\d{4})+$)/g

console.log(mobile.replace(mobileReg, '-')) // 183-7983-6654
 











收起阅读 »

4 个 JavaScript 的心得体会

按需所取,冲冲冲ヾ(◍°∇°◍)ノ゙ 一、你能说出 JavaScript 的编程范式吗?   首先要说出:JavaScript 是一门多范式语言!支持面向过程(命令式)、面向对象(OOP)和函数式编程(声明式)。 其次,最重要的是说出:JavaScr...
继续阅读 »




按需所取,冲冲冲ヾ(◍°∇°◍)ノ゙


一、你能说出 JavaScript 的编程范式吗?


 


首先要说出:JavaScript 是一门多范式语言!支持面向过程(命令式)、面向对象(OOP)和函数式编程(声明式)。


其次,最重要的是说出:JavaScript 是通过原型继承(OLOO-对象委托)来实现面向对象(OOP)的;


如果还能说出以下,就更棒了:JavaScript 通过闭包、函数是一等公民、lambda 运算来实现函数式编程的。


如果再进一步,回答出 JavaScript 演进历史,就直接称绝叫好了:JavaScript的语言设计主要受到了Self(一种基于原型的编程语言)和Scheme(一门函数式编程语言)的影响。在语法结构上它又与C语言有很多相似。

 

  • Self 语言 => 基于原型 => JavaScript 用原型实现面向对象编程;
  • Scheme 语言 => 函数式编程语言 => JavaScript 函数式编程;
  • C 语言 => 面向过程 => JavaScript 面向过程编程;




推荐 Eric Elliott 的另外两篇文章,JavaScript 的两大支柱:


  1. 基于原型的继承
  2. 函数式编程



二、什么是函数式编程?




函数式编程是最早出现的编程范式,通过组合运算函数来生成程序。有一些重要的概念:


  • 纯函数
  • 避免副作用
  • 函数组合
  • 高阶函数(闭包)
  • 函数组合
  • 其它函数式编程语言,比如 Lisp、Haskell

本瓜觉得这里最 nb 就是能提到 monad 和延迟执行了~




三、类继承和原型继承有什么区别?





类继承,通过构造函数实现( new 关键字);tips:即使不用 ES6 class,也能实现类继承;


原型继承,实例直接从其他对象继承,工厂函数或 Object.create();


本瓜这里觉得能答出以下就很棒了:


类继承:基于对象复制;


原型继承:基于对象委托;


推荐阅读:


 

四、面向对象和函数式的优缺点




面向对象优点:对象的概念容易理解,方法调用灵活;


面向对象缺点:对象可在多个函数中共享状态、被修改,极有可能会产生“竞争”的情况(多处修改同一对象);


函数式优点:避免变量的共享、修改,纯函数不产生副作用;声明式代码风格更易阅读,更易代码重组、复用;


函数式缺点:过度抽象,可读性降低;学习难度更大,比如 Monad;

 

OK,以上便是本篇分享。点赞关注评论,为好文助力👍 🌏









收起阅读 »

十分详细的diff算法原理解析

diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom。顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom。 虚拟Dom 上面的概念我们提到了虚拟Dom,相信大家对...
继续阅读 »


diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom。顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom



虚拟Dom


上面的概念我们提到了虚拟Dom,相信大家对这个名词并不陌生,下面为大家解释一下虚拟Dom的概念,以及diff算法中为什么要对比虚拟Dom,而不是直接去操作真实Dom。

虚拟Dom,其实很简单,就是一个用来描述真实Dom的对象


它有六个属性,sel表示当前节点标签名,data内是节点的属性,children表示当前节点的其他子标签节点,elm表示当前虚拟节点对应的真实节点(这里暂时没有),key即为当前节点的key,text表示当前节点下的文本,结构类似这样。

 
let vnode = {
sel: 'ul',
   data: {},
children: [
{
sel: 'li', data: { class: 'item' }, text: 'son1'
},
{
sel: 'li', data: { class: 'item' }, text: 'son2'
},    
  ],
   elm: undefined,
   key: undefined,
   text: undefined
}



那么虚拟Dom有什么用呢。我们其实可以把虚拟Dom理解成对应真实Dom的一种状态。当真实Dom发生变化后,虚拟Dom可以为我们提供这个真实Dom变化之前和变化之后的状态,我们通过对比这两个状态,即可得出真实Dom真正需要更新的部分,即可实现最小量更新。在一些比较复杂的Dom变化场景中,通过对比虚拟Dom后更新真实Dom会比直接更新真实Dom的效率高,这也就是虚拟Dom和diff算法真正存在的意义。


h函数


在介绍diff算法原理之前还需要简单让大家了解一下h函数,因为我们要靠它为我们生成虚拟Dom。这个h函数大家应该也比较熟悉,就是render函数里面传入的那个h函数


h函数可以接受多种类型的参数,但其实它内部只干了一件事,就是执行vnode函数。根据传入h函数的参数来决定执行vnode函数时传入的参数。那么vnode函数又是干什么的呢?vnode函数其实也只干了一件事,就是把传入h函数的参数转化为一个对象,即虚拟Dom。

 
// vnode.js
export default function (sel, data, children, text, elm) {
const key = data.key
return {sel, data, children, text, elm, key}
}



执行h函数后,内部会通过vnode函数生成虚拟Dom,h函数把这个虚拟在return出去。


diff对比规则


明确了h函数是干什么的,我们可以简单用h函数生成两个不同的虚拟节点,我们将通过一个简易版的diff算法代码介绍diff对比的具体流程。



// 第一个参数是sel 第二个参数是data 第三个参数是children
const myVnode1 = h("h1", {}, [
 h("p", {key: "a"}, "a"),
 h("p", {key: "b"}, "b"),
]);

const myVnode2 = h("h1", {}, [
 h("p", {key: "c"}, "c"),
 h("p", {key: "d"}, "d"),
]);



patch


比较的第一步就是执行patch,它相当于对比的入口。既然是对比两个虚拟Dom,那么就将两个虚拟Dom作为参数传入patch中。patch的主要作用是对比两个虚拟Dom的根节点,并根据对比结果操作真实Dom。


patch函数的核心代码如下,注意注释。

 
// patch.js

import vnode from "./vnode"
import patchDetails from "./patchVnode"
import createEle from "./createEle"

/**
* @description 用来对比两个虚拟dom的根节点,并根据对比结果操作真实Dom
* @param {*} oldVnode
* @param {*} newVnode
*/
export function patch(oldVnode, newVnode) {
 // 1.判断oldVnode是否为虚拟节点,不是的话转化为虚拟节点
 if(!oldVnode.sel) {
   // 转化为虚拟节点
   oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
}

 // 2.判断oldVnode和newVnode是否为同一个节点
 if(oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
   console.log('是同一个节点')
   // 比较子节点
   patchDetails(oldVnode, newVnode)
}else {
   console.log('不是同一个节点')
   // 插入newVnode
   const newNode = createEle(newVnode) // 插入之前需要先将newVnode转化为dom
   oldVnode.elm.parentNode.insertBefore(newNode, oldVnode.elm) // 插入操作
   // 删除oldVnode
   oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}

// createEle.js

/**
* @description 根据传入的虚拟Dom生成真实Dom
* @param {*} vnode
* @returns real node
*/
export default function createEle (vnode) {
 const realNode = document.createElement(vnode.sel)

 // 子节点转换
 if(vnode.text && (vnode.children == undefined || (vnode.children && vnode.children.length == 0)) ) {
   // 子节点只含有文本
   realNode.innerText = vnode.text  
}else if(Array.isArray(vnode.children) && vnode.children.length > 0) {
   // 子节点为其他虚拟节点 递归添加node
   for(let i = 0; i < vnode.children.length; i++) {
     const childNode = createEle(vnode.children[i])
     realNode.appendChild(childNode)
  }
}

 // 补充vnode的elm属性
 vnode.elm = realNode

 return vnode.elm
}



patchVnode


patchVnode用来比较两个虚拟节点的子节点并更新其子节点对应的真实Dom节点



// patchVnode.js

import updateChildren from "./updateChildren"
import createEle from "./createEle"

/**
* @description 比较两个虚拟节点的子节点(children or text) 并更新其子节点对应的真实dom节点
* @param {*} oldVnode
* @param {*} newVnode
* @returns
*/
export function patchDetails(oldVnode, newVnode) {
 // 判断oldVnode和newVnode是否为同一个对象, 是的话直接不用比了
 if(oldVnode == newVnode) return

 // 默认newVnode和oldVnode只有text和children其中之一,真实的源码这里的情况会更多一些,不过大同小异。

 if(hasText(newVnode)) {
   // newVnode有text但没有children

   /**
    * newVnode.text !== oldVnode.text 直接囊括了两种情况
    * 1.oldVnode有text无children 但是text和newVnode的text内容不同
    * 2.oldVnode无text有children 此时oldVnode.text为undefined
    * 两种情况都可以通过innerText属性直接完成dom更新
    * 情况1直接更新text 情况2相当于去掉了children后加了新的text
    */
   if(newVnode.text !== oldVnode.text) {
     oldVnode.elm.innerText = newVnode.text
  }

}else if(hasChildren(newVnode)) {
   // newVnode有children但是没有text
   
   if(hasText(oldVnode)) {
     // oldVnode有text但是没有children
     
     oldVnode.elm.innerText = '' // 删除oldVnode的text
     // 添加newVnode的children
     for(let i = 0; i < newVnode.children.length; i++) {
       oldVnode.elm.appendChild(createEle(newVnode.children[i]))
    }

  }else if(hasChildren(oldVnode)) {
     // oldVnode有children但是没有text

     // 对比两个节点的children 并更新对应的真实dom节点
     updateChildren(oldVnode.children, newVnode.children, oldVnode.elm)
  }
}
}

// 有children没有text
function hasChildren(node) {
 return !node.text && (node.children && node.children.length > 0)
}

// 有text没有children
function hasText(node) {
 return node.text && (node.children == undefined || (node.children && node.children.length == 0))
}



updateChildren


该方法是diff算法中最复杂的方法(大的要来了)。对应上面patchVnodeoldVnodenewVnode都有children的情况。


首先我们需要介绍一下这里的对比规则。


对比过程中会引入四个指针,分别指向oldVnode子节点列表中的第一个节点和最后一个节点(后面我们简称为旧前旧后)以及指向newVnode子节点列表中的第一个节点和最后一个节点(后面我们简称为新前新后


对比时,每一次对比按照以下顺序进行命中查找


  • 旧前与新前节点对比(1)
  • 旧后与新后节点对比(2)
  • 旧前与新后节点对比(3)
  • 旧后与新前节点对比(4)

上述四种情况,如果某一种情况两个指针对应的虚拟Dom相同,那么我们称之为命中。命中后就不会接着查找了,指针会移动,(还有可能会操作真实Dom,3或者4命中时会操作真实Dom移动节点)之后开始下一次对比。如果都没有命中,则去oldVnode子节点列表循环查找当前新前指针所指向的节点,如果查到了,那么操作真实Dom移动节点,没查到则新增真实Dom节点插入。


这种模式的对比会一直进行,直到满足了终止条件。即旧前指针移动到了旧后指针的后面或者新前指针移动到了新后指针的后面,我们可以理解为旧子节点先处理完毕新子节点处理完毕。那么我们可以预想到新旧子节点中总会有其一先处理完,对比结束后,我们会根据没有处理完子节点的那一对前后指针决定是要插入真实Dom还是删除真实Dom。


  • 如果旧子节点先处理完了,新子节点有剩余,说明有要新增的节点。将根据最终新前新后之间的虚拟节点执行插入操作
  • 如果新子节点先处理完了,旧子节点有剩余,说明有要删除的节点。将根据最终旧前旧后之间的虚拟节点执行删除操作

下面将呈现代码,注意注释

 
// updateChildren.js

import patchDetails from "./patchVnode"
import createEle from "./createEle";

/**
* @description 对比子节点列表并更新真实Dom
* @param {*} oldCh 旧虚拟Dom子节点列表
* @param {*} newCh 新虚拟Dom子节点列表
* @param {*} parent 新旧虚拟节点对应的真实Dom
* @returns
*/

export default function updateChildren(oldCh, newCh, parent) {
 // 定义四个指针 旧前 旧后 新前 新后 (四个指针两两一对,每一对前后指针所指向的节点以及其之间的节点为未处理的子节点)
 let oldStartIndex = 0;
 let oldEndIndex = oldCh.length - 1;
 let newStartIndex = 0;
 let newEndIndex = newCh.length - 1;

 // 四个指针对应的节点
 let oldStartNode = oldCh[oldStartIndex];
 let oldEndNode = oldCh[oldEndIndex];
 let newStartNode = newCh[newStartIndex];
 let newEndNode = newCh[newEndIndex];

 // oldCh中每个子节点 key 与 index的哈希表 用于四种对比规则都不匹配的情况下在oldCh中寻找节点
 const keyMap = new Map();

 /**
  * 开始遍历两个children数组进行细节对比
  * 对比规则:旧前-新前 旧后-新后 旧前-新后 旧后-新前
  * 对比之后指针进行移动
  * 直到指针不满足以下条件 意味着有一对前后指针之间再无未处理的子节点 则停止对比 直接操作DOM
  */

 while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
   // 这四种情况是为了让指针在移动的过程中跳过空节点
   if (oldStartNode == undefined) {
     oldStartNode = oldCh[++oldStartIndex];
  } else if (oldEndNode == undefined) {
     oldEndNode = oldCh[--oldEndIndex];
  } else if (newStartNode == undefined) {
     newStartNode = newCh[++newStartIndex];
  } else if (newEndNode == undefined) {
     newEndNode = newCh[--newEndIndex];
  } else if (isSame(oldStartNode, newStartNode)) {
     console.log("method1");
     // 旧前-新前是同一个虚拟节点

     // 两个子节点再对比他们的子节点并更新dom (递归切入点)
     patchDetails(oldStartNode, newStartNode);
     // 指针移动
     oldStartNode = oldCh[++oldStartIndex];
     newStartNode = newCh[++newStartIndex];
  } else if (isSame(oldEndNode, newEndNode)) {
     console.log("method2");
     // 旧后-新后是同一个虚拟节点

     // 两个子节点再对比他们的子节点并更新dom (递归切入点)
     patchDetails(oldEndNode, newEndNode);
     // 指针移动
     oldEndNode = oldCh[--oldEndIndex];
     newEndNode = newCh[--newEndIndex];
  } else if (isSame(oldStartNode, newEndNode)) {
     console.log("method3");
     // 旧前-新后是同一个虚拟节点

     // 两个子节点再对比他们的子节点并更新dom (递归切入点)
     patchDetails(oldStartNode, newEndNode);

     /**
      * 这一步多一个移动(真实)节点的操作
      * 需要把当前指针所指向的子节点 移动到 oldEndIndex所对应真实节点之后(也就是未处理真实节点的尾部)
      * 注意:这一步是在操作真实节点
      */
     parent.insertBefore(oldStartNode.elm, oldEndNode.elm.nextSibling);

     // 指针移动
     oldStartNode = oldCh[++oldStartIndex];
     newEndNode = newCh[--newEndIndex];
  } else if (isSame(oldEndNode, newStartNode)) {
     console.log("method4");
     // 旧后-新前 是同一个虚拟节点

     // 两个子节点再对比他们的子节点并更新dom (递归切入点)
     patchDetails(oldEndNode, newStartNode);
     /**
      * 这一步多一个移动(真实)节点的操作
      * 与method3不同在移动位置
      * 需要把当前指针所指向的子节点 移动到 oldStartIndex所对应真实节点之前(也就是未处理真实节点的顶部)
      * 注意:这一步是在操作真实节点
      */
     parent.insertBefore(oldEndNode.elm, oldCh[oldStartIndex].elm);

     // 指针移动
     oldEndNode = oldCh[--oldEndIndex];
     newStartNode = newCh[++newStartIndex];
  } else {
     console.log("does not match");
     // 四种规则都不匹配

     // 生成keyMap
     if (keyMap.size == 0) {
       for (let i = oldStartIndex; i <= oldEndIndex; i++) {
         if (oldCh[i].key) keyMap.set(oldCh[i].key, i);
      }
    }

     // 在oldCh中搜索当前newStartIndex所指向的节点
     if (keyMap.has(newStartNode.key)) {
       // 搜索到了

       // 先获取oldCh中该虚拟节点
       const oldMoveNode = oldCh[keyMap.get(newStartNode.key)];
       // 两个子节点再对比他们的子节点并更新dom (递归切入点)
       patchDetails(oldMoveNode, newStartNode);

       // 移动这个节点(移动的是真实节点)
       parent.insertBefore(oldMoveNode.elm, oldStartNode.elm);

       // 该虚拟节点设置为undefined(还记得最开始的四个条件吗,因为这里会将子节点制空,所以加了那四个条件)
       oldCh[keyMap.get(newStartNode.key)] = undefined;
         
    } else {
       // 没搜索到 直接插入
       parent.insertBefore(createEle(newStartNode), oldStartNode.elm);
    }

     // 指针移动
     newStartNode = newCh[++newStartIndex];
  }
}

 /**
  * 插入和删除节点
  * while结束后 有一对前后指针之间仍然有未处理的子节点,那么就会进行插入或者删除操作
  * oldCh的双指针中有未处理的子节点,进行删除操作
  * newCh的双指针中有未处理的子节点,进行插入操作
  */
 if (oldStartIndex <= oldEndIndex) {
   // 删除
   for (let i = oldStartIndex; i <= oldEndIndex; i++) {
     // 加判断是因为oldCh[i]有可能为undefined
     if(oldCh[i]) parent.removeChild(oldCh[i].elm);
  }
} else if (newStartIndex <= newEndIndex) {
   /**
    * 插入
    * 这里需要注意的点是从哪里插入,也就是appendChild的第二个参数
    * 应该从oldStartIndex对应的位置插入
    */
   for (let i = newStartIndex; i <= newEndIndex; i++) {
     // oldCh[oldStartIndex]存在是从头部插入
     parent.insertBefore(createEle(newCh[i]), oldCh[oldStartIndex] ? oldCh[oldStartIndex].elm : undefined);
  }
}
}

// 判断两个虚拟节点是否为同一个虚拟节点
function isSame(a, b) {
 return a.sel == b.sel && a.key == b.key;
}



这里的逻辑稍微比较复杂,需要大家多理几遍,必要的话,自己手画一张图自己移动一下指针。着重需要注意的地方是操作真实Dom时,插入、移动节点应该将节点从哪里插入或者移动到哪里,其实基本插入到oldStartIndex对应的真实Dom的前面,除了第三种命中后的移动节点操作,是移动到oldEndIndex所对应真实节点之后


总结


由于diff算法对比的是虚拟Dom,而虚拟Dom是呈树状的,所以我们可以发现,diff算法中充满了递归。总结起来,其实diff算法就是一个 patch —> patchVnode —> updateChildren —> patchVnode —> updateChildren —> patchVnode这样的一个循环递归的过程。


这里再提一嘴key,我们面试中经常会被问到vue中key的作用。根据上面我们分析的,key的主要作用其实就是对比两个虚拟节点时,判断其是否为相同节点。加了key以后,我们可以更为明确的判断两个节点是否为同一个虚拟节点,是的话判断子节点是否有变更(有变更更新真实Dom),不是的话继续比。如果不加key的话,如果两个不同节点的标签名恰好相同,那么就会被判定为同一个节点(key都为undefined),结果一对比这两个节点的子节点发现不一样,这样会凭空增加很多对真实Dom的操作,从而导致页面更频繁地重绘和回流。


所以我认为合理利用key可以有效减少真实Dom的变动,从而减少页面重绘和回流的频率,进而提高页面更新的效率。

 








收起阅读 »

关于 Axios 的再再再封装,总是会有所不一样

特性 class 封装 可以多次实例化默认全局可以共用一个实例对象可以实例化多个对象,实例化时可以配置该实例特有的 headers根据各个接口的要求不同,也可以针对该接口进行配置设置请求拦截和响应拦截,这个都是标配了拦截处理系统响应状态码对应的提示语 拦截器 ...
继续阅读 »


特性


  • class 封装 可以多次实例化
  • 默认全局可以共用一个实例对象
  • 可以实例化多个对象,实例化时可以配置该实例特有的 headers
  • 根据各个接口的要求不同,也可以针对该接口进行配置
  • 设置请求拦截和响应拦截,这个都是标配了
  • 拦截处理系统响应状态码对应的提示语

拦截器


首先为防止多次执行响应拦截,这里我们将拦截器设置在类外部,如下:

import axios from "axios";

// 添加请求拦截器
axios.interceptors.request.use((config) => {
// 在发送请求之前做些什么 添加 token 等鉴权功能
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use((res) => {
const {status} = res;
// 对错误状态提示进行处理
let message = '';
if (status < 200 || status >= 300) {
// 处理http错误,抛到业务代码
message = showResState(status)
if (typeof res.data === 'string') {
res.data = {code: status, message: message}
} else {
res.data.code = status
res.data.message = message
}
}
return res.data;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});

function showResState(state) {
let message = '';
// 这里只做部分常见的示例,具体根据需要进行配置
switch (state) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
default:
message = `连接出错(${state})!`
}
return `${message},请检查网络或联系网站管理员!`
}



封装主体


这里为了方便起见,实例化对象处理的其实就是传入的配置文件,而封装的方法还是按照 axios 原生的方法处理的。为了方便做校验在接口上都统一增加了客户端发起请求的时间,以方便服务端做校验。配置参数可参照文档 axios 配置文档

// 构造函数
constructor(config) {
// 公共的 header
let defaultHeaders = {
'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/json', // 通过头指定,获取的数据类型是JSON 'application/json, text/plain, */*',
'Authorization': null
}

let defaultConfig = {
headers: defaultHeaders
}

// 合并配置文件
if (config) {
for (let i in config) {
if (i === 'headers' && config.headers) {
for (let i in config.headers) {
defaultHeaders[i] = config.headers[i];
}
defaultConfig.headers = defaultHeaders;
} else {
defaultConfig[i] = config[i];
}
}
}
// 全局使用
this.init = axios;
this.config = defaultConfig;
}



get 方法的配置

// Get 请求
get(url, params = {}, headers = {}) {

params.time = Date.now();

// 合并 headers
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.get(url[, config])
this.init.get(url, {
...this.config,
...{params: params}
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



post 请求

// POST 请求
post(url, params = {}, headers = {}) {

url = url + '?time=' + Date.now();

if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.post(url[, data[, config]])
this.init.post(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



PUT 请求

// PUT 请求
put(url, params = {}, headers = {}) {

url = url + '?time=' + Date.now();

if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.put(url[, data[, config]])
this.init.put(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



Delete 请求

// Delete 请求
delete(url, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.delete(url[, config])
this.init.delete(url, {
...this.config,
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}



>>使用


完整的代码的代码在文末会贴出来,这里简单说下如何使用

// @/api/index.js
import Http,{Axios} from '@/api/http'; // Axios 数据请求方法

// ① 可以使用文件中实例化的公共对象 Axios


// ②也可以单独实例化使用
const XHttp = new Http({
headers: {
'x-token': 'xxx'
}
});


export const getArticles = (params={}) => {
return XHttp.get('https://api.ycsnews.com/api/v1/blog/getArticles', params);
}

export const getArticle = (params={}) => {
return Axios.get('https://api.ycsnews.com/api/v1/blog/getArticles', params);
}



在页面中使用

// @/views/home.vue
import { getArticles,getArticle } from '@/api/index.js'

// 两个方法名差一个字母 's'
getArticle({id:1234444}).then((res) => {
console.log(res)
})
.catch(err => {
console.log(err)
})

getArticles({id:1234444}).then((res) => {
console.log(res)
})
.catch(err => {
console.log(err)
})



完整代码

// @/api/http.js
/**
* 说明:
* 1.多实例化,可以根据不同的配置进行实例化,满足不同场景的需求
* 2.多实例化情况下,可共用公共配置
* 3.请求拦截,响应拦截 对http错误提示进行二次处理
* 4.接口可单独配置 header 满足单一接口的特殊需求
* body 直传字符串参数,需要设置 headers: {"Content-Type": "text/plain"}, 传参:System.licenseImport('"'+this.code+'"');
* import Http,{Axios} from '../http'; // Http 类 和 Axios 数据请求方法 如无特殊需求 就使用实例化的 Axios 方法进行配置 有特殊需求再进行单独实例化
*
*
*/
import axios from "axios";

// 添加请求拦截器
axios.interceptors.request.use((config) => {
// 在发送请求之前做些什么 添加 token 等鉴权功能
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use((res) => {
const {status} = res;
// 对错误状态提示进行处理
let message = '';
if (status < 200 || status >= 300) {
// 处理http错误,抛到业务代码
message = showResState(status)
if (typeof res.data === 'string') {
res.data = {code: status, message: message}
} else {
res.data.code = status
res.data.message = message
}
}
return res.data;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});

function showResState(state) {
let message = '';
// 这里只做部分常见的示例,具体根据需要进行配置
switch (state) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
default:
message = `连接出错(${state})!`
}
return `${message},请检查网络或联系网站管理员!`
}

class Http {
constructor(config) {
// 公共的 header
let defaultHeaders = {
'Content-Type': 'application/json;charset=UTF-8',
'Accept': 'application/json', // 通过头指定,获取的数据类型是JSON 'application/json, text/plain, */*',
'Authorization': null
}

let defaultConfig = {
headers: defaultHeaders
}

// 合并配置文件
if (config) {
for (let i in config) {
if (i === 'headers' && config.headers) {
for (let i in config.headers) {
defaultHeaders[i] = config.headers[i];
}
defaultConfig.headers = defaultHeaders;
} else {
defaultConfig[i] = config[i];
}
}
}
this.init = axios;
this.config = defaultConfig;
}

// Get 请求
get(url, params = {}, headers = {}) {
// 合并 headers
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.get(url[, config])
this.init.get(url, {
...this.config,
...{params: params}
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}

// POST 请求
post(url, params = {}, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.post(url[, data[, config]])
this.init.post(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}

// PUT 请求
put(url, params = {}, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}

return new Promise((resolve, reject) => {
// axios.put(url[, data[, config]])
this.init.put(url, params, this.config).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}


// Delete 请求
delete(url, headers = {}) {
if (headers) {
for (let i in headers) {
this.config.headers[i] = headers[i];
}
}
return new Promise((resolve, reject) => {
// axios.delete(url[, config])
this.init.delete(url, {
...this.config,
}).then(response => {
resolve(response);
})
.catch(error => {
reject(error)
}).finally(() => {
})
});
}
}

export default Http;

// 无特殊需求的只需使用这个一个对象即可 公共 header 可在此配置, 如需多个实例 可按照此方式创建多个进行导出
export const Axios = new Http({
baseURL:'https://docs.ycsnews.com',
headers: {
'x-http-token': 'xxx'
}
});











收起阅读 »

JS堆栈内存的运行机制也需时常回顾咀嚼

在js引擎中对变量的存储主要有两个位置,堆内存和栈内存。栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null, 以及对象变量的指针(地址值)。栈内存中的变量一般都是已知大小或者有范围上限的,算作...
继续阅读 »



在js引擎中对变量的存储主要有两个位置,堆内存和栈内存。栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null,


以及对象变量的指针(地址值)。栈内存中的变量一般都是已知大小或者有范围上限的,算作一种简单存储。而堆内存主要负责像对象Object这种变量类型的存储,对于大小这方面,一般都是未知的。

栈内存 ECStack


栈内存ECStack(Execution Context Stack)(作用域)




栈内存ECStack(Execution Context Stack)(作用域)



JS之所以能够在浏览器中运行,是因为浏览器给JS提供了执行的环境栈内存





浏览器会在计算机内存中分配一块内存,专门用来供代码执行=》栈内存ECStack(Execution Context Stack)执行环境栈,每打开一个网页都会生成一个全新的ECS


ECS的作用




  • 提供一个供JS代码自上而下执行的环境(代码都在栈中执行)
  • 由于基本数据类型值比较简单,他们都是直接在栈内存中开辟一个位置,把值直接存储进去的,当栈内存被销毁,存储的那些基本值也都跟着销毁



堆内存


堆内存:引用值对应的空间,堆内存是区别于栈区、全局数据区和代码区的另一个内存区域。堆允许程序在运行时动态地申请某个大小的内存空间。


存储引用类型值(对象:键值对, 函数:代码字符串),当内存释放销毁,那么这个引用值彻底没了
堆内存释放


当堆内存没有被任何得变量或者其他东西所占用,浏览器会在空闲的时候,自主进行内存回收,把所有不被占用得内存销毁掉


谷歌浏览器(webkit),每隔一定时间查找对象有没有被占用
引用计数器:当对象引用为0时释放它

收起阅读 »

大家好啊,新手一枚,请多关照哈

大家好啊,新手一枚,请多关照哈。。。。。。。。。。

大家好啊,新手一枚,请多关照哈。。。。。。。。。。

使用环信提供的uni-app Demo,快速实现一对一单聊

如何利用环信提供的uni-app Demo,快速实现一对一单聊?真真保姆级别教程! 写在前面: 1)因为初期直接下载环信的uni-app的demo源码直接看可能一头雾水,因此写下这篇文档帮助项目周期较急,想要快速集成环信uni-app端IM开发者小伙伴。2)这...
继续阅读 »

如何利用环信提供的uni-app Demo,快速实现一对一单聊?真真保姆级别教程!


写在前面:


1)因为初期直接下载环信的uni-app的demo源码直接看可能一头雾水,因此写下这篇文档帮助项目周期较急,想要快速集成环信uni-app端IM开发者小伙伴。

2)这篇文档只帮助实现单聊功能,群组功能其实与单聊基本相仿,可以在参考单聊后的流程,自行看看源码实现群聊。

3)尽管已经从原项目中剥离了很多无关核心逻辑的代码,但仍然可能还有一些小伙伴本身项目中用不到的代码,因此化繁去简这一步就不再本文档中展示,请在按照这篇文档,完成核心逻辑后自行进行优化。

然后就不多啰嗦了,下面开搞~


1、 下载环信uni-app demo 源码 源码地址:

https://github.com/easemob/webim-uniapp-demo


2、在编辑器中打开项目,建议进行一次试运行确保demo源码可以正常跑起来,大概率是可以正常跑起来的。
运行没有问题之后,强烈建议先在README.md中了解一下demo中的目录结构,做个初期的了解。 参考实际目录结构如图:



3、由于是作为演示,所以我只是简单的新建一个示例项目,写一个简易的聊天界面界面作为即时通讯功能的入口。
仿咸鱼在线一对一沟通界面入口:


这个就是默认的项目目录(该示例项目为Vue)


4、这一步就正式开始从环信的Uni-App demo中CV代码到自有的项目中:

setp1:先把最核心的SDKcopy进来,复制demo源码的 newSDK 这个文件到项目中(demo中的SDK其实有很多个,建议选择版本号最新的一个即可)。

自己的项目目录如图:


setp2:复制demo中的 utils 文件到项目中。

utils目录结构如图:


其中 WebIMConfig.js 是作为SDK的Config配置使用,WebIM.js 是针对于SDK进行初始化,并挂载一些常用方法,Dispatcher.js broadcast.js Observer.js 是用作发布订阅的使用,因为源码中有所使用,所以这几个文件都是必须引入。


setp3:copy static 静态资源包到自己的项目当中,因为组件的聊天界面里面的emoji是图片所以要用到。

此时的目录结构如图:



setp4:copy uview-ui进来,因为组件中有用到这个包的UI组件,使用过UI组件的朋友应该都知道,除了这个还要引入相应的样式,这个组件的README.md中,说明了要进行什么样的配置,这里就不再一一赘述。




setp5:在示例项目中新建components文件夹,分别copy demo当中的 components 文件夹下的整个 chat 组件,pages 文件夹下的chatroom组件,由于示例项目中的App.vue组件没有自己的其他逻辑,所以我直接将demo中的Appp.vue中的所有代码全部copy到示例项目中。



PS:特别说明demo的App.vue尽管不是每一行代码都是必要的,但是如果要做优化或者copy,确保import的引入部分先全部粘贴上,conn.listen 监听回调也一定要先copy上。确保先跑起来的原则,优化放在之后。


此时的目录结构如图:


以上步骤执行完成之后便可以跑一把试试了,运行起来查看一下是否有什么引入类型或者其他类型的报错在集中解决一下。


下图是运行到小程序的界面:




VM22 WAService.js:2 TypeError: Cannot read property 'forceUpdate' of undefined

这个报错原因是没有在HbuilderX配置微信小程序的AppId。

5、开始登陆环信,执行跳转至chat聊天界面进行单聊消息的发送测试


step1:确保先登陆环信(能到这一步相信也都已经注册了环信的账号创建了应用,或者利用环信官方demo注册了测试id)

我在示例项目中是在index.vue写的入口页面,因此登陆也写在了这个页面,示例代码截图可以看下图:




step2:运行项目看 App.vue中的监听回调--onOpened回调是否触发(这一步很重要,因为所有功能性接口调用都必须保证环信的连接成功)






看到代码中的打印输出之后证明已成功的建立websocket连接,正式可以开始下一步跳转至chat页面。


step3: 给引入的chatroom组件在pages.json中配置对应的路由映射,并在pages/index.vue组件再给"我想要"按钮添加事件执行路由跳转至chatroom组件。

index.vue中的示例代码如图:




chatroom组件不需要执行其他操作,onload直接将路由传递的参数进行了接收:


step:4 跑起来看看吧!

这个时候顺利的话你会跳转至这样的一个页面,有可能出现这样一个报错:



这是因为demo重写了一个setData并放在了main.jsmixin里面,手动加上去即可,代码所在位置如图:



6、重新编译启动,点击进入chat页面测试聊天,就没问题了!




不排除列位遇到一些其他阻力导致没有成功跑起来,如果还遇到有其他问题,可以在评论区友好交流,我看到会帮忙解决的。


源码下载: uni-app-singleChat-demo.zip

收起阅读 »

Flutter 小技巧之 MediaQuery 和 build 优化你不知道的秘密

今天这篇文章的目的是补全大家对于 MediaQuery 和对应 rebuild 机制的基础认知,相信本篇内容对你优化性能和调试 bug 会很有帮助。 Flutter 里大家应该都离不开 MediaQuery ,比如通过 MediaQuery.of(cont...
继续阅读 »

今天这篇文章的目的是补全大家对于 MediaQuery 和对应 rebuild 机制的基础认知,相信本篇内容对你优化性能和调试 bug 会很有帮助


Flutter 里大家应该都离不开 MediaQuery ,比如通过 MediaQuery.of(context).size 获取屏幕大小 ,或者通过 MediaQuery.of(context).padding.top 获取状态栏高度,那随便使用 MediaQuery.of(context) 会有什么问题吗?


首先我们需要简单解释一下,通过 MediaQuery.of 获取到的 MediaQueryData 里有几个很类似的参数:



  • viewInsets被系统用户界面完全遮挡的部分大小,简单来说就是键盘高度

  • padding简单来说就是状态栏和底部安全区域,但是 bottom 会因为键盘弹出变成 0

  • viewPadding padding 一样,但是 bottom 部分不会发生改变


举个例子,在 iOS 上,如下图所示,在弹出键盘和未弹出键盘的情况下,可以看到 MediaQueryData 里一些参数的变化:



  • viewInsets 在没有弹出键盘时是 0,弹出键盘之后 bottom 变成 336

  • padding 在弹出键盘的前后区别, bottom 从 34 变成了 0

  • viewPadding 在键盘弹出前后数据没有发生变化


image-20220624115935998



可以看到 MediaQueryData 里的数据是会根据键盘状态发生变化,又因为 MediaQuery 是一个 InheritedWidget ,所以我们可以通过 MediaQuery.of(context) 获取到顶层共享的 MediaQueryData



那么问题来了,InheritedWidget 的更新逻辑,是通过登记的 context 来绑定的,也就是 MediaQuery.of(context) 本身就是一个绑定行为,然后 MediaQueryData 又和键盘状态有关系,所以:键盘的弹出可能会导致使用 MediaQuery.of(context) 的地方触发 rebuild,举个例子:


如下代码所示,我们在 MyHomePage 里使用了 MediaQuery.of(context).size 并打印输出,然后跳转到 EditPage 页面,弹出键盘 ,这时候会发生什么情况?



class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("######### MyHomePage ${MediaQuery.of(context).size}");
return Scaffold(
body: Container(
alignment: Alignment.center,
child: InkWell(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(builder: (context) {
return EditPage();
}));
},
child: new Text(
"Click",
style: TextStyle(fontSize: 50),
),
),
),
);
}
}

class EditPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text("ControllerDemoPage"),
),
extendBody: true,
body: Column(
children: [
new Spacer(),
new Container(
margin: EdgeInsets.all(10),
child: new Center(
child: new TextField(),
),
),
new Spacer(),
],
),
);
}
}

如下图 log 所示 , 可以看到在键盘弹起来的过程,因为 bottom 发生改变,所以 MediaQueryData 发生了改变,从而导致上一级的 MyHomePage 虽然不可见,但是在键盘弹起的过程里也被不断 build 。


image-20220624121917686



试想一下,如果你在每个页面开始的位置都是用了 MediaQuery.of(context) ,然后打开了 5 个页面,这时候你在第 5 个页面弹出键盘时,也触发了前面 4 个页面 rebuild,自然而然可能就会出现卡顿。



那么如果我不在 MyHomePage 的 build 方法直接使用 MediaQuery.of(context) ,那在 EditPage 里弹出键盘是不是就不会导致上一级的 MyHomePage 触发 build



答案是肯定的,没有了 MediaQuery.of(context).size 之后, MyHomePage 就不会因为 EditPage 里的键盘弹出而导致 rebuild。



所以小技巧一:要慎重在 Scaffold 之外使用 MediaQuery.of(context) ,可能你现在会觉得奇怪什么是 Scaffold 之外,没事后面继续解释。


那到这里有人可能就要说了:我们通过 MediaQuery.of(context) 获取到的 MediaQueryData ,不就是对应在 MaterialApp 里的 MediaQuery 吗?那它发生改变,不应该都会触发下面的 child 都 rebuild 吗?



这其实和页面路由有关系,也就是我们常说的 PageRoute 的实现



如下图所示,因为嵌套结构的原因,事实上弹出键盘确实会导致 MaterialApp 下的 child 都触发 rebuild ,因为设计上 MediaQuery 就是在 Navigator 上面,所以弹出键盘自然也就触发 Navigator 的 rebuild


image-20220624141749056


那正常情况下 Navigator 都触发 rebuild 了,为什么页面不会都被 rebuild 呢


这就和路由对象的基类 ModalRoute 有关系,因为在它的内部会通过一个 _modalScopeCache 参数把 Widget 缓存起来,正如注释所说:



缓存区域不随帧变化,以便得到最小化的构建




举个例子,如下代码所示:



  • 首先定义了一个 TextGlobal ,在 build 方法里输出 "######## TextGlobal"

  • 然后在 MyHomePage 里定义一个全局的 TextGlobal globalText = TextGlobal();

  • 接着在 MyHomePage 里添加 3 个 globalText

  • 最后点击 FloatingActionButton 触发 setState(() {});


class TextGlobal extends StatelessWidget {
const TextGlobal({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
print("######## TextGlobal");
return Container(
child: new Text(
"测试",
style: new TextStyle(fontSize: 40, color: Colors.redAccent),
textAlign: TextAlign.center,
),
);
}
}
class MyHomePage extends StatefulWidget {
final String? title;
MyHomePage({Key? key, this.title}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
TextGlobal globalText = TextGlobal();
@override
Widget build(BuildContext context) {
print("######## MyHomePage");
return Scaffold(
appBar: AppBar(),
body: new Container(
alignment: Alignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
globalText,
globalText,
globalText,
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {});
},
),
);
}
}

那么有趣的来了,如下图 log 所示,"######## TextGlobal" 除了在一开始构建时有输出之外,剩下 setState(() {}); 的时候都没有在触发,也就是没有 rebuild ,这其实就是上面 ModalRoute 的类似行为:弹出键盘导致了 MediaQuery 触发 Navigator 执行 rebuild,但是 rebuild 到了 ModalRoute 就不往下影响



其实这个行为也体现在了 Scaffold 里,如果你去看 Scaffold 的源码,你就会发现 Scaffold 里大量使用了 MediaQuery.of(context)


比如上面的代码,如果你给 MyHomePageScaffold 配置一个 3333 的 ValueKey ,那么在 EditPage 弹出键盘时,其实 MyHomePageScaffold 是会触发 rebuild ,但是因为其使用的是 widget.body ,所以并不会导致 body 内对象重构。




如果是 MyHomePage 如果 rebuild ,就会对 build 方法里所有的配置的 new 对象进行 rebuild;但是如果只是 MyHomePage 里的 Scaffold 内部触发了 rebuild ,是不会导致 MyHomePage 里的 body 参数对应的 child 执行 rebuild 。



是不是太抽象?举个简单的例子,如下代码所示:



  • 我们定义了一个 LikeScaffold 控件,在控件内通过 widget.body 传递对象

  • LikeScaffold 内部我们使用了 MediaQuery.of(context).viewInsets.bottom ,模仿 Scaffold 里使用 MediaQuery

  • MyHomePage 里使用 LikeScaffold ,并给 LikeScaffold 的 body 配置一个 Builder ,输出 "############ HomePage Builder Text " 用于观察

  • 跳到 EditPage 页面打开键盘


class LikeScaffold extends StatefulWidget {
final Widget body;

const LikeScaffold({Key? key, required this.body}) : super(key: key);

@override
State<LikeScaffold> createState() => _LikeScaffoldState();
}

class _LikeScaffoldState extends State<LikeScaffold> {
@override
Widget build(BuildContext context) {
print("####### LikeScaffold build ${MediaQuery.of(context).viewInsets.bottom}");
return Material(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [widget.body],
),
);
}
}
····
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
var routeLists = routers.keys.toList();
return new LikeScaffold(
body: Builder(
builder: (_) {
print("############ HomePage Builder Text ");
return InkWell(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(builder: (context) {
return EditPage();
}));
},
child: Text(
"FFFFFFF",
style: TextStyle(fontSize: 50),
),
);
},
),
);
}
}

可以看到,最开始 "####### LikeScaffold build 0.0############ HomePage Builder Text 都正常执行,然后在键盘弹出之后,"####### LikeScaffold build 跟随键盘动画不断输出 bottom 的 大小,但是 "############ HomePage Builder Text ") 没有输出,因为它是 widget.body 实例。



所以通过这个最小例子,可以看到虽然 Scaffold 里大量使用 MediaQuery.of(context) ,但是影响范围是约束在 Scaffold 内部


接着我们继续看修改这个例子,如果在 LikeScaffold 上嵌套多一个 Scaffold ,那输出结果会是怎么样?



class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
var routeLists = routers.keys.toList();
///多加了个 Scaffold
return Scaffold(
body: new LikeScaffold(
body: Builder(
·····
),
),
);
}

答案是 LikeScaffold 内的 "####### LikeScaffold build 也不会因为键盘的弹起而输出,也就是: LikeScaffold 虽然使用了 MediaQuery.of(context) ,但是它不再因为键盘的弹起而导致 rebuild


因为此时 LikeScaffoldScaffold 的 child ,所以在 LikeScaffold 内通过 MediaQuery.of(context) 指向的,其实是 Scaffold 内部经过处理的 MediaQueryData


image-20220624150712453



Scaffold 内部有很多类似的处理,例如 body 里会根据是否有 AppbarBottomNavigationBar 来决定是否移除该区域内的 paddingTop 和 paddingBottom 。



所以,看到这里有没有想到什么?为什么时不时通过 MediaQuery.of(context) 获取的 padding ,有的 top 为 0 ,有的不为 0 ,原因就在于你获取的 context 来自哪里


举个例子,如下代码所示, ScaffoldChildPage 作为 Scaffold 的 child ,我们分别在 MyHomePageScaffoldChildPage 里打印 MediaQuery.of(context).padding


class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("MyHomePage MediaQuery padding: ${MediaQuery.of(context).padding}");
return Scaffold(
appBar: AppBar(
title: new Text(""),
),
extendBody: true,
body: Column(
children: [
new Spacer(),
ScaffoldChildPage(),
new Spacer(),
],
),
);
}
}
class ScaffoldChildPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("ScaffoldChildPage MediaQuery padding: ${MediaQuery.of(context).padding}");
return Container();
}
}

如下图所示,可以看到,因为此时 MyHomePageAppbar ,所以 ScaffoldChildPage 里获取到 paddingTop 是 0 ,因为此时 ScaffoldChildPage 获取到的 MediaQueryData 已经被 MyHomePage 里的 Scaffold 改写了。


image-20220624151522429


如果此时你给 MyHomePage 增加了 BottomNavigationBar ,可以看到 ScaffoldChildPage 的 bottom 会从原本的 34 变成 90 。


image-20220624152008795


到这里可以看到 MediaQuery.of 里的 context 对象很重要:



  • 如果页面 MediaQuery.of 用的是 Scaffold 外的 context ,获取到的是顶层的 MediaQueryData ,那么弹出键盘时就会导致页面 rebuild

  • MediaQuery.of 用的是 Scaffold 内的 context ,那么获取到的是 Scaffold 对于区域内的 MediaQueryData ,比如前面介绍过的 body ,同时获取到的 MediaQueryData 也会因为 Scaffold 的配置不同而发生改变


所以,如下动图所示,其实部分人会在 push 对应路由地方,通过嵌套 MediaQuery 来做一些拦截处理,比如设置文本不可缩放,但是其实这样会导致键盘在弹出和收起时,触发各个页面不停 rebuild ,比如在 Page 2 弹出键盘的过程,Page 1 也在不停 rebuild。


1111333


所以,如果需要做一些全局拦截,推荐通过 useInheritedMediaQuery 这种方式来做全局处理。


return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith(boldText: false),
child: MaterialApp(
useInheritedMediaQuery: true,
),
);

所以最后做个总结,本篇主要理清了:



  • MediaQueryDataviewInsets \ padding \ viewPadding 的区别

  • MediaQuery 和键盘状态的关系

  • MediaQuery.of 使用不同 context 对性能的影响

  • 通过 Scaffold 内的 context 获取到的 MediaQueryData 受到 Scaffold 的影响

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

Flutter 实现背景图片毛玻璃效果

前言 继续我们绘图相关篇章,这次我们来看看如何使用 CustomPaint 实现毛玻璃背景图效果。毛玻璃背景图其实就是将图片进行一定程度的模糊,背景图经过模糊后更加虚幻,使得前景和后景就会有层次感。相比直接加蒙层的效果来说,毛玻璃看起来更加好看一些。下面是背景...
继续阅读 »

前言


继续我们绘图相关篇章,这次我们来看看如何使用 CustomPaint 实现毛玻璃背景图效果。毛玻璃背景图其实就是将图片进行一定程度的模糊,背景图经过模糊后更加虚幻,使得前景和后景就会有层次感。相比直接加蒙层的效果来说,毛玻璃看起来更加好看一些。下面是背景图处理前后的对比,我们的前景图片的透明度并没有改变,但是背景图模糊虚化后,感觉前景更加显眼了一样。
模糊前后对比.jpg
本篇涉及如下内容:



  • 使用 canvas 绘制图片。

  • 绘制图片时如何更改图片的填充范围。

  • 使用 ImageFilter 模糊图片,实现毛玻璃效果。


使用 canvas 绘制图片


Flutter 为 canvas 提供了drawImage 方法用于绘制图片,方法定义如下:


void drawImage(Image image, Offset offset, Paint paint)

其中各个参数说明如下:



  • imagedart:ui 中的 Image 对象,注意不是Widget 中的 Image,因此绘制的时候需要将图片资源转换为 ui.Image 对象。下面是转换的示例代码,fillImage 即最终得到的 ui.Image 对象。注意转换需要一定的时间,因此需要使用异步 async / await 操作。


Future<void> init() async {
final ByteData data = await rootBundle.load('images/island-coder.png');
fillImage = await loadImage(Uint8List.view(data.buffer));
}

Future<ui.Image> loadImage(Uint8List img) async {
final Completer<ui.Image> completer = Completer();
ui.decodeImageFromList(img, (ui.Image img) {
setState(() {
isImageLoaded = true;
});
return completer.complete(img);
});
return completer.future;
}


  • offset:绘制图片的起始位置。

  • paint:绘图画笔对象,在 paint 上可以应用各种处理效果,比如本篇要用到的图片模糊效果。


注意,drawImage 方法无法更改图片绘制的区域大小,默认就是按图片的实际尺寸绘制的,所以如果要想保证全屏的背景图,我们就需要使用另一个绘制图片的方法。


更改绘制图片的绘制范围


Flutter 的 canvas 为绘制图片提供了一个尺寸转换方法,即可以通过指定原绘制区域的矩形和目标区域的矩形,将图片某个区域映射到新的矩形框中绘制。也就是我们甚至可以实现绘制图片的局部区域。该方法名为 drawImageRect,定义如下:


void drawImageRect(Image image, Rect src, Rect dst, Paint paint)

方法的参数比较容易懂,我们来看看 Flutter 的文档说明。



Draws the subset of the given image described by the src argument into the canvas in the axis-aligned rectangle given by the dst argument.
翻译:通过 src 参数将给定图片的局部(subset)绘制到坐标轴对齐的目标矩形区域内。



下面是我们将源矩形框设置为实际图片的尺寸和一半宽高的对比图,可以看到取一半宽高的只绘制了左上角的1/4区域。实际我们可以定位起始位置来截取部分区域绘制。
截取原图的一半宽高.jpg


毛玻璃效果实现


毛玻璃效果实现和我们上两篇使用 paintshader属性有点类似,Paint 类提供了一个imageFilter属性专门用于图片处理,其中dart:ui 中就提供了ui.ImageFilter.blur方法构建模糊效果处理的 ImageFilter对象。方法定义如下:


factory ImageFilter.blur({ 
double sigmaX = 0.0,
double sigmaY = 0.0,
TileMode tileMode = TileMode.clamp
})

这个方法实际调用的是一个高斯模糊处理器,高斯模糊其实就是应用一个方法将像素点周边指定范围的值进行处理,进而实现模糊效果,有兴趣的可以自行百度一下。下面的 sigmaXsigmaY 分布代表横轴方向和纵轴方向的模糊程度,数值越大,模糊程度越厉害。因此我们可以通过这两个参数控制模糊程度。


return _GaussianBlurImageFilter(
sigmaX: sigmaX,
sigmaY: sigmaY,
tileMode: tileMode
);

**注意,这里 sigmaX 和 sigmaY 不能同时为0,否则会报错!**这里应该是如果同时为0会导致除0操作。
下面来看整体的绘制实现代码,如下所示:


class BlurImagePainter extends CustomPainter {
final ui.Image bgImage;
final double blur;

BlurImagePainter({
required this.bgImage,
required this.blur,
});
@override
void paint(Canvas canvas, Size size) {
var paint = Paint();
// 模糊的取值不能为0,为0会抛异常
if (blur > 0) {
paint.imageFilter = ui.ImageFilter.blur(
sigmaX: blur,
sigmaY: blur,
tileMode: TileMode.mirror,
);
}

canvas.drawImageRect(
bgImage,
Rect.fromLTRB(0, 0, bgImage.width.toDouble(), bgImage.height.toDouble()),
Offset.zero & size,
paint,
);
}

代码其实很短,就是在模糊值不为0的时候,应用 imageFilter 进行模糊处理,然后使用 drawImageRect 方法确保图片填充满整个背景。完整代码已经提交至:绘图相关代码,文件名为:blur_image_demo.dart。变换模糊值的效果如下动图所示。
背景图模糊过程.gif


总结


本篇介绍了使用 CustomPaint 实现背景图模糊,毛玻璃的效果。关键点在于 使用 Paint 对象的 imageFilter属性,使用高斯模糊应用到图片上。以后碰到需要模糊背景图的地方就可以直接上手用啦!


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

Native 如何快速集成 Flutter

如何 Android 项目中集成 Flutter 概述 目前flutter越来越受欢迎,但对于一些成熟的产品来说,完全摒弃原有App全面转向Flutter是不现实的。因此使用Flutter去统一Android、iOS技术栈,把它作为已有原生App的扩展能力,通...
继续阅读 »

如何 Android 项目中集成 Flutter


概述


目前flutter越来越受欢迎,但对于一些成熟的产品来说,完全摒弃原有App全面转向Flutter是不现实的。因此使用Flutter去统一Android、iOS技术栈,把它作为已有原生App的扩展能力,通过有序推进来提升移动终端的开发效率。 目前,想要在已有的原生App里嵌入一些Flutter页面主要有两种方案。一种是将原生工程作为Flutter工程的子工程,由Flutter进行统一管理,这种模式称为统一管理模式。另一种是将Flutter工程作为原生工程的子模块,维持原有的原生工程管理方式不变,这种模式被称为三端分离模式,如下图所示。
1.png
三端代码分离模式的原理是把Flutter模块作为原生工程的子模块,从而快速地接入Flutter模块,降低原生工程的改造成本。


如何在Native项目中接入flutter 模块


在原生项目中集成flutter模块有两种方式,第一种是直接在项目中新建一个flutter module,第二种将flutter项目模块打包成aar或so包集成到Native项目中。一下将详细介绍这两种方式 (以Android为例)


采用module引用的方式


直接通过Android stuido



File->New ->New Module 选择 Flutter Module 来生成一个Flutter Module.



image.png


image.png



如下图:Android studio为原生项目创建了一个module



image.png


手动创建Flutter module


假设你在 some/path/MyApp 路径下已有一个 Android 应用,并且你希望 Flutter 项目作为同级项目:


 cd some/path/
$ flutter create -t module --org com.example my_flutter

image.png


注意:



  1. 这会创建一个 some/path/my_flutter/ 的 Flutter 模块项目,其中包含一些 Dart 代码来帮助你入门以及一个隐藏的子文件夹 .android/。 .android 文件夹包含一个 Android 项目,该项目不仅可以帮助你通过 flutter run 运行这个 Flutter 模块的独立应用,而且还可以作为封装程序来帮助引导 Flutter 模块作为可嵌入的 Android 库。

  2. 为了避免 Dex 合并出现问题,flutter.androidPackage 不应与应用的包名相同


引入 Java 8


Flutter Android 引擎需要使用到 Java 8 中的新特性。


在尝试将 Flutter 模块项目集成到宿主 Android 应用之前,请先确保宿主 Android 应用的 build.gradle 文件的 android { } 块中声明了以下源兼容性,例如:


android {
//...
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}

采用AAR资源包的方式导入Flutter模块


flutter 工程作为独立的项目开发迭代,原生工程不直接使用Flutter项目,而是通过导入flutter 的资源包来引用Flutter 模块。



创建Flutter module 工程。



image.png



编译生成AAR包



image.png



flutter 工程会创建一个本地maven仓库和aar文件,同时在Flutter 项目也会输出指引导入的步骤文本,按照提示步骤操作即可。
为方便使用将该maven仓库拷贝到native 项目中。



image.png



提示步骤如下



Consuming the Module




  1. Open \app\build.gradle




  2. Ensure you have the repositories configured, otherwise add them:


    String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "storage.googleapis.com"
    repositories {
    maven {
    url 'D:<path>\build\host\outputs\repo'
    }
    maven {
    url '$storageUrl/download.flutter.io'
    }
    }




  3. Make the host app depend on the Flutter module:




dependencies {
debugImplementation 'com.example.untitled1:flutter_debug:1.0'
profileImplementation 'com.example.untitled1:flutter_profile:1.0'
releaseImplementation 'com.example.untitled1:flutter_release:1.0'
}


  1. Add the profile build type:


android {
buildTypes {
profile {
initWith debug
}
}
}

To learn more, visit flutter.dev/go/build-aa…
Process finished with exit code 0


在 Android 应用中添加 Flutter 页面


步骤 1:在 AndroidManifest.xml 中添加 FlutterActivity


Flutter 提供了 FlutterActivity,用于在 Android 应用内部展示一个 Flutter 的交互界面。和其他的 Activity 一样,FlutterActivity 必须在项目的 AndroidManifest.xml 文件中注册。将下边的 XML 代码添加到你的 AndroidManifest.xml 文件中的 application 标签内:


<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
/>

上述代码中的 @style/LaunchTheme 可以替换为想要在你的 FlutterActivity 中使用的其他 Android 主题。主题的选择决定 Android 系统展示框架所使用的颜色,例如 Android 的导航栏,以及 Flutter UI 自身的第一次渲染前 FlutterActivity 的背景色。


步骤 2:加载 FlutterActivity


在你的清单文件中注册了 FlutterActivity 之后,根据需要,你可以在应用中的任意位置添加打开 FlutterActivity 的代码。下边的代码展示了如何在 OnClickListener 的点击事件中打开 FlutterActivity


myButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity.createDefaultIntent(currentActivity)
);
}
});

Flutter 启动优化


每一个 FlutterActivity 默认会创建它自己的 FlutterEngine。每一个 FlutterEngine 会有一个明显的预热时间。这意味着加载一个标准的 FlutterActivity 时,在你的 Flutter 交互页面可见之前会有一个短暂的延迟。想要最小化这个延迟时间,你可以在抵达你的 FlutterActivity 之前,初始化一个 FlutterEngine,然后使用这个已经预热好的 FlutterEngine
如果直接启动FlutterActivity则无法避免预热时间,用户会感受到一个较长时间的白屏等待。


优化


提前初始化一个  FlutterEngine,启动的FlutterActivty时直接使用已经初始化的FlutterEngine.



提前初始化



public class MyApplication extends Application {
public FlutterEngine flutterEngine;

@Override
public void onCreate() {
super.onCreate();
// Instantiate a FlutterEngine.
flutterEngine = new FlutterEngine(this);

// Start executing Dart code to pre-warm the FlutterEngine.
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartEntrypoint.createDefault()
);

// Cache the FlutterEngine to be used by FlutterActivity.
FlutterEngineCache
.getInstance()
.put("my_engine_id", flutterEngine);
}
}


使用预热的FlutterEngine



myButton.addOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(currentActivity)
);
}
});

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

黑科技!让Native Crash 与ANR无处发泄!

ANR
前言 高产似母猪的我,又带来了干货记录,本次是对signal的一个总结与回顾。不知道你们开发中,是否会遇到小部分的nativecrash 或者 anr,这部分往往是由第三方库导致的或者当前版本没办法修复的bug导致的,往往这些难啃的crash,对现有的cras...
继续阅读 »

前言


高产似母猪的我,又带来了干货记录,本次是对signal的一个总结与回顾。不知道你们开发中,是否会遇到小部分的nativecrash 或者 anr,这部分往往是由第三方库导致的或者当前版本没办法修复的bug导致的,往往这些难啃的crash,对现有的crash数据指标造成一定影响,同时也对这小部分crash用户不友好,那么我们有没有办法实现一套crash or anr重启机制呢?其实是有的,相信在各个大厂都有一套“安全气囊”装置,比如crash一定次数就启用轻量版本或者自动重新启动等等,下面我们来动手搞一个这样的装置!这也是我第三个s开头的开源库Signal


注意:前方高能!阅读本文最好有一点ndk开发的知识噢!没有也没关系,冲吧!


Native Crash


native crash不同于java/kotlin层的crash,在java环境中,如果程序出现了不可预期的crash(即没有捕获),就会往上抛出给最终的线程uncaghtexceptionhandler,在这里我们可以再次处理,比如屏蔽某个exception即可保持app的稳定,然后native层的crash不一样,native 层的crash大多数是“不可恢复”的,比如某个内存方面的错误,这些往往是不可处理的,需要中断当前进程,所以如果发生了native crash,我们转移到自定义的安全处理,比如自动重启后提示用户等等,就会提高用户很大的体验感(比起闪退)


信号量机制


当native 层发生异常的时候,往往是通过信号的方式发送,给相对应的信号处理器处理


image.png
我们可以从signal.h看到,大概已经定义的信号量有


/**
* #define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGTRAP 5
#define SIGABRT 6
#define SIGIOT 6
#define SIGBUS 7
#define SIGFPE 8
#define SIGKILL 9
#define SIGUSR1 10
#define SIGSEGV 11
#define SIGUSR2 12
## define SIGPIPE 13
#define SIGALRM 14
#define SIGTERM 15
#define SIGSTKFLT 16
#define SIGCHLD 17
#define SIGCONT 18
#define SIGSTOP 19
#define SIGTSTP 20
#define SIGTTIN 21
#define SIGTTOU 22
#define SIGURG 23
#define SIGXCPU 24
#define SIGXFSZ 25
#define SIGVTALRM 26
#define SIGPROF 27
#define SIGWINCH 28
#define SIGIO 29
#define SIGPOLL SIGIO
#define SIGPWR 30
#define SIGSYS 31
*/

具体的含义可自定百度或者google,相信如果开发者都能在bugly等bug平台上看到


信号量处理函数sigaction


一般的我们有很多种方式定义信号量处理函数,这里介绍sigaction
头文件:#include<signal.h>


定义函数:int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact)


函数说明:sigaction会依参数signum指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。如参数结构sigaction定义如下


struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};

信号处理函数可以采用void (*sa_handler)(int)或void (*sa_sigaction)(int, siginfo_t *, void *)。到底采用哪个要看sa_flags中是否设置了SA_SIGINFO位,如果设置了就采用void (*sa_sigaction)(int, siginfo_t *, void *),此时可以向处理函数发送附加信息;默认情况下采用void (*sa_handler)(int),此时只能向处理函数发送信号的数值。


sa_handler:此参数和signal()的参数handler相同,代表新的信号处理函数,其他意义请参考signal();
sa_mask:用来设置在处理该信号时暂时将sa_mask指定的信号集搁置;
sa_restorer:此参数没有使用;
sa_flags :用来设置信号处理的其他相关操作,下列的数值可用。sa_flags还可以设置其他标志:
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号。参考


即我们可以通过这个函数,注册我们想要的信号处理,如果当SIGABRT信号到来时,我们希望将其引到自我们自定义信号处理,即可采用以下方式


 sigaction(SIGABRT, &sigc, nullptr);

其中sigc为sigaction结构体的变量


struct sigaction sigc;
//sigc.sa_handler = SigFunc;
sigc.sa_sigaction = SigFunc;
sigemptyset(&sigc.sa_mask);
sigc.sa_flags = SA_SIGINFO;

SigFunc为我们定义处理函数的指针,我们可以设定这样一个函数,去处理我们想要拦截的信号


void SigFunc(int sig_num, siginfo *info, void *ptr) {
自定义处理
}

native crash拦截


有了前面这些基础知识,我们就开始封装我们的crash拦截吧,作为库开发者,我们希望把拦截的信号量交给上层去处理,所以我们的层次是这样的


image.png
所以我们可以有以下代码,具体细节可以看Signal
我们给出函数处理器


jobject currentObj;
JNIEnv *currentEnv = nullptr;

void SigFunc(int sig_num, siginfo *info, void *ptr) {
// 这里判空并不代表这个对象就是安全的,因为有可能是脏内存

if (currentEnv == nullptr || currentObj == nullptr) {
return;
}
__android_log_print(ANDROID_LOG_INFO, TAG, "%d catch", sig_num);
__android_log_print(ANDROID_LOG_INFO, TAG, "crash info pid:%d ", info->si_pid);
jclass main = currentEnv->FindClass("com/example/lib_signal/SignalController");
jmethodID id = currentEnv->GetMethodID(main, "callNativeException", "(I)V");
if (!id) {
return;
}
currentEnv->CallVoidMethod(currentObj, id, sig_num);
currentEnv->DeleteGlobalRef(currentObj);


}

当so库被加载的时候由系统自动调用JNI_OnLoad
extern "C" jint JNI_OnLoad(JavaVM *vm, void *reserved) {
jint result = -1;
// 直接用vm进行赋值,不然不可靠
if (vm->GetEnv((void **) &currentEnv, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
return JNI_VERSION_1_4;
}

其中currentEnv代表着当前jni环境,我们在JNI_OnLoad阶段进行初始化即可,currentObj即代表我们要调用的方法对象,因为我们要回调到java层,所以native肯定需要一个java对象,具体可以看到Signal里面的处理,值得注意的是,我们在native想要在其他函数使用java对象的话,在初始函数赋值的时候,就必须采用env->NewGlobalRef方式分配一个全局变量,不然在该函数结束的时候,对象的内存就会变成脏变量(注意不是NULL)。


Spi机制的运用


如果还不明白spi机制的话,可以查看我之前写的这篇spi机制,因为我们最终会将信号信息传递给java层,所以最终会在java最后执行我们的重启处理,但是重启前我们可能会使用各种自定义的处理方案,比如弹出toast或者各种自定义操作,那么这种自定义的处理就很合适用spi接口暴露给具体的使用者即可,所以我们Signal定义了一个接口


interface CallOnCatchSignal {
fun onCatchSignal(signal: Int,context: Context)
}

外部库的调用者实现这个接口,将实现类配置在META-INF.services目录即可,如图


image.png
如此一来,我们就可以在自定义的MyHandler实现自己的重启逻辑,比如重启/自定义上报crash等等,demo可以看Signal的处理


ANR


关于anr也是一个很有趣的话题,我们可以看到anr也会导致闪退,主要是国内各个厂商都有自己的自定义化处理,比如常规的弹出anr框或者主动闪退,无论是哪一种,对于用户来说都不是一个好的体验。


ANR传递过程


以android 11为例子,最终anr被检测发生后,会调用ProcessErrorStateRecord类的appNotResponding方法,去进行dump 墓碑文件的操作,这个时候就会调用发送一个信号为Signal_Quit的信号,对应的常量为3,所以如果我们想检测到anr后去进行自定义处理的话,按照上面所说直接用sigaction可以吗?


image.png


然而如果直接用sigaction去注册Signal_Quit信号进行处理的话,会发现居然什么都没有回调!那么这发生了什么!


原因就是我们进程继承Zygote进行的时候就把主线程信号的掩码也继承了,Zygote进程把这三个信号量加入了掩码,该方法被调用在init方法中


image.png
掩码的作用就是使得当前的线程不相应这三个信号量,交给其他线程处理


那么其他线程这里指的是什么?其实就是SignalCatcher线程,通常我们发生anr的时候也能看到log输出,最终在run方法注册处理函数


image.png
最终调用WaitForSignal


image.png
调用wait方法


image.png
这个sigwait方法也是一个注册信号处理函数的方法,跟sigaction的区别可参考


取消block


经过上面的分析,相信能了解到为什么Signal_Quit监听不了了,我们也知道,zygote通过掩码把信号进行了屏蔽,那么我们有办法把这个屏蔽给打开吗?答案是有的


pthread_sigmask(SIG_UNBLOCK, &mask, &old))

sigemptyset(&mask);
sigaddset(&mask, SIGQUIT);

我们可以通过pthread_sigmask设置为非block,即参数1的标志,把要取消屏蔽的信号放入即可,如图就是把SIGQUIT取消了,这样一来我们再使用sigaction去注册SIGQUIT就可以在信号出发时执行我们的anr处理逻辑了。值得注意的是,SIGQUIT触发也不一定由anr发生,这是一个必要但不充分的条件,所以我们还要添加其他的判断,比如我们可以判断一个queue里面的当前message的when参数来判断这个消息在队列待了多久,又或者是我们自定义一个异步消息去查看这个消息什么时候回调了handler等等方法,最终判断是否是anr,当然这个不是百分百准确,目前我也没想到百分百准确的方法,因为FileObserve监听traces文件已经在android5以上不能用了,所以Signal里面没有给出具体的判断,只给了一个参考例子。


最后


上述所讲的都在Signal这个库里面有源码与注释,用起来吧!自定义处理可以用作检测crash,anr,也可以用作一个安全装置,发生crash重启等等,只要有脑洞,都可以实现!最后记得点个赞啦!


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

破防了!Web3还没整明白,Web5居然出现了?

自Web3问世并爆火以后,关于“Web3是什么、有何用”的解读文章层出不穷。就在多数人还是云里雾里的当下,神奇的事情发生了。Twitter的创始人Jack Dorsey在日前公布了打造“Web5”的计划,声称“这很可能将是我们对于互联网最重大的贡献”,并戏谑地...
继续阅读 »

自Web3问世并爆火以后,关于“Web3是什么、有何用”的解读文章层出不穷。就在多数人还是云里雾里的当下,神奇的事情发生了。

Twitter的创始人Jack Dorsey在日前公布了打造“Web5”的计划,声称“这很可能将是我们对于互联网最重大的贡献”,并戏谑地为Web3的创投人们“点蜡”,意指Web3必凉,Web5才是未来的“天命之子”。


图片来源:推特截图

那么,Web5到底是何方神圣?

PART 01 Web5横空出世

围绕Web5的定义和核心概念,Jack Dorsey的团队(Block旗下的比特币部门TBD)出具了一份报告。报告不长,包括联系页在内,总共18页PPT。

第一眼看到Web5,可能会有不少人诧异Web4去哪儿了,这个横跳是有什么寓意吗?Dorsey团队给出的答案是:Web5相当于Web2和Web3的集合。


图片来源:Dorsey团队报告截图

也就是说,因为2+3=5,Web5之名由此诞生。乍看之下简单粗暴,细看一下又如何呢?

他们提出,所谓Web5,就是要建立一个去中心化的Web平台(DWP),使开发人员能够利用去中心化的身份标识(DIDs)和去中心化的网络节点(DWNs),来编写去中心化的Web应用程序(DWAs),从而将个人身份和数据的所有权和控制权交还给个人。

从这个概念可以看到,Web5的核心跟 Web3一样,突出“去中心化”特性。其目的是要打破当前互联网世界中用户数据被沉淀在不同的应用中,用户本身无法掌控和自由使用的现状。

从这一点出发,Web5中提及的几个新概念,某种程度上都可以和Web3进行对标。有位名为“ntkris”的网友对此进行了精要的总结。

  • 去中心化身份标识(DID)=公钥

  • 去中心化网络节点(DWN)=智能合约(但在本地运行)

  • 可验证凭证(VCs)=零知识证明

  • 去中心化应用程序(DWAs)=dApps


图片来源:推特截图

不过也有人因此提出质疑,Web5相较Web3来说似乎是“新瓶装旧酒”。但事实上,就目前披露的资料来看,两者还是有两点核心不同。

其一,Web5只有身份标识存储在区块链上,其他所有内容都存储在用户运行的节点上。而Web3中所有用户数据都通过区块链的形式存储在所有的节点中。

其二,Web5的实现最大的依仗是比特币网络,而不是当前在Web3中被广泛使用的以太坊以及其他基于智能合约的区块链。可以说,如果有朝一日Web5真的成功,那么除了比特币外的其他加密货币都将失去意义。

Jack Dorsey及其团队之所以会提出Web5,其实都有迹可循。

首先,Dorsey本人一直都是比特币的狂信徒。他早在2018年就预言未来世界上只有一种通用货币——比特币,比特币会是现有货币体系的终结者。歌手Cardi B曾在Twitter上问Dorsey比特币是否会取代美元,Dorsey坚定地回答“Yes,Bitcon will”。

再者,Dorsey认为,Web3根本不能实现真正的去中心化。去年年底Dorsey就曾公开表示,Web3不是所有人的,它只是贴了个不同标签的集权模式,不要白日做梦。所谓的Web3更像是一个营销热词,实际话语权仍掌握在少数风投和公司手里。如今的Web5可能就是他对于“去中心化”实现路径给出的新答案。

PART 02 Web3的“九宗罪”

虽然Web5的横空出世似乎让人看到了下一代互联网形态的新解,但目前为止它还只是个存在于PPT中的“新蓝图”。相比之下,仍旧是Web3离我们更近,虽然它现在依然“面目模糊”。

在对Web3一探究竟之前,我们先回顾一下Web的三个主要时代:

  • Web1:以静态网页为主的“活化石”

  • Web2:大量的通信和商业行为都集中在少数科技巨头所拥有的封闭平台上。大多数情况下,用户对自己的数据只拥有使用权,而平台却对用户数据和用户创作内容拥有所有权

  • Web3:以用户为主导,基于区块链技术打造的去中心化的互联网生态。用户数据的所有权和控制权均归属用户本人


图片来源于网络

虽然关于Web3的讨论总是毁誉参半,但不可否认的是,从形态上来说,Web3相较Web2来说是一种进化。

Web3创建了一种无需准入的数据存储方式。所有数据都被存储在区块链网络中的公共账本上。不再是某个公司或平台拥有数据,而是由多个节点共同存储数据,并就数据的真实性和有效性达成共识。以此为基础,Web3可以开启无数崭新的用例。

但Web3要面对的现实是,天马行空的想象和天花乱坠的陷阱总是相伴而生,也正因为如此,关于Web3的争议总是不绝于耳。

不久前,分析公司Forrester发布了两份评估Web3的文件,犀利地评价其“包含了一场反乌托邦噩梦的种子”。

分析文章指出,一方面,“Web3”之名正在被滥用。“几乎在一夜之间”,无数“区块链项目”、“NFT倡议”和“元宇宙”相关事务都被神奇地命名为“Web3项目”;另一方面,虽然Web3“承诺了一个更好的在线未来”,但其关键内容却不堪一击。

Forrester定义了Web3的九个关键原则,然后又将其一一推翻。

1、愿景:去中心化

现实:这是不可能实现的,实证就是目前的众多加密货币项目通常都由大型平台或公司主导

2、愿景:相信代码,而不是公司

现实:智能合约及其规则通常由某一个公司开发并执行。那么我们真的能信任这些陌生的开发人员吗

3、愿景:始终使用公开透明的代码

现实:这并不会阻止垄断的形成,这反而会导致依赖于一小部分有能力评估代码的人

4、愿景:加密经济原则的设计使系统普惠所有参与者

现实:只是有利于富人和发展垄断

5、愿景:用户能够拥有和控制他们创建的数据和内容

现实:“所有权”的概念是模糊的。大多数用户不愿意或没有能力对他们的数据做出持续一致的决定

6、愿景:用户自己管理自己的身份和凭证

现实:没有多少人愿意为此费心,部分原因是这很难

7、愿景:用户能控制他们所使用的应用程序和网络

现实:除了少数精通技术的人之外,这种状况极其罕见

8、愿景:去中心化的自治组织和实体作为智能合约的集合而存在

现实:它们没有法律基础,并在一个乌托邦式的假设下工作,即所有的可能性都可以被编码

9、愿景:去中心化金融(DeFi)

现实:虽然是个不错的主意,可惜缺乏对消费者的保护,而风控需要代码检查,这很少有人能做到

自诞生伊始就饱受质疑的Web3真的会有未来吗?如果它真的到来,又会对我们的工作和生活产生何种影响呢?

PART 03 你期待Web3的到来吗

如何打造一个更加开放、共享、安全的数字环境在每一次互联网技术革命中都是核心议题。Web3虽然离我们好像很远,但其思想内核是否值得希冀未来者再次下注呢?毕竟一切都只是刚刚开始。

其一,数字化转型是个不可逆的过程。但随着转型的深化,以及重大创新技术的每一次进步,其目标会不断变化。以Web3为前景,区块链技术的发展已经经历了充分的时间考验,并在一定程度上获得了更加繁盛的土壤。

其二,去中心化或许是IT系统的重要发展趋向。企业运营业务所需的许多重要数据将越来越多地保存在更私密和受保护的地方,存储在区块链和其他类型的分布式账本中。随着时间的推移,越来越多的应用程序将更类似于开源项目,并使所有利益相关者可以公开透明地查看、验证、达成共识。

其三,一些更直接的转变,例如接受某些形式的加密货币作为支付或以NFT的形式发行知识产权。

PART 04 结语

Web3如今还是一个混沌未开的世界,这里有创业者、梦想家、理想主义者,也有骗子、吹牛大王、浑水摸鱼者。这个概念下,关于数字世界的一切本质似乎都在受到质疑和重塑。但不管下一代互联网形态如何,是循规蹈矩地过渡,还是摧枯拉朽地颠覆,我们都应该思考:在一个未知的数字世界里,信任、安全、隐私应该是何种模样,应该如何保障。

参考链接:

https://stackoverflow.blog/2022/05/25/web3-skeptics-and-believers-both-need-a-reality-check/

https://www.pingwest.com/a/265452

https://developer.tbd.website/docs/Decentralized%20Web%20Platform%20-%20Public.pdf

https://www.theregister.com/2022/04/01/forrester_web3_criticism/

https://www.zdnet.com/article/how-decentralization-and-web3-will-impact-the-enterprise/

来源:mp.weixin.qq.com/s/3VjcqRjxta-acEoM1v3Ndw

收起阅读 »

不要滥用effect哦

你或你的同事在使用useEffect时有没有发生过以下场景:当你希望状态a变化后发起请求,于是你使用了useEffect:useEffect(() => { fetch(xxx); }, [a])这段代码运行符合预期,上线后也没问题。随着需求不断迭代...
继续阅读 »

你或你的同事在使用useEffect时有没有发生过以下场景:

当你希望状态a变化后发起请求,于是你使用了useEffect

useEffect(() => {
fetch(xxx);
}, [a])

这段代码运行符合预期,上线后也没问题。

随着需求不断迭代,其他地方也会修改状态a。但是在那个需求中,并不需要状态a改变后发起请求。

你不想动之前的代码,又得修复这个bug,于是你增加了判断条件:

useEffect(() => {
if (xxxx) {
fetch(xxx);
}
}, [a])

某一天,需求又变化了!现在请求还需要b字段。

这很简单,你顺手就将b作为useEffect的依赖加了进去:

useEffect(() => {
if (xxxx) {
fetch(xxx);
}
}, [a, b])

随着时间推移,你逐渐发现:

  • 是否发送请求if条件相关
  • 是否发送请求还与a、b等依赖项相关
  • a、b等依赖项又与很多需求相关

根本分不清到底什么时候会发送请求,真是头大...

如果以上场景似曾相识,那么React新文档里已经明确提供了解决办法。

欢迎加入人类高质量前端框架群,带飞

一些理论知识

新文档中这一节名为Synchronizing with Effects,当前还处于草稿状态。

但是其中提到的一些概念,所有React开发者都应该清楚。

首先,effect这一节隶属于Escape Hatches(逃生舱)这一章。


从命名就能看出,开发者并不一定需要使用effect,这仅仅是特殊情况下的逃生舱。

React中有两个重要的概念:

  • Rendering code(渲染代码)
  • Event handlers(事件处理器)

Rendering code开发者编写的组件渲染逻辑,最终会返回一段JSX

比如,如下组件内部就是Rendering code

function App() {
const [name, update] = useState('KaSong');

return <div>Hello {name}</div>;
}

Rendering code的特点是:他应该是不带副作用的纯函数

如下Rendering code包含副作用(count变化),就是不推荐的写法:

let count = 0;

function App() {
count++;
const [name, update] = useState('KaSong');

return <div>Hello {name}</div>;
}

处理副作用

Event handlers组件内部包含的函数,用于执行用户操作,可以包含副作用

下面这些操作都属于Event handlers

  • 更新input输入框
  • 提交表单
  • 导航到其他页面

如下例子中组件内部的changeName方法就属于Event handlers

function App() {
const [name, update] = useState('KaSong');

const changeName = () => {
update('KaKaSong');
}

return <div onClick={changeName}>Hello {name}</div>;
}

但是,并不是所有副作用都能在Event handlers中解决。

比如,在一个聊天室中,发送消息是用户触发的,应该交给Event handlers处理。

除此之外,聊天室需要随时保持和服务端的长连接,保持长连接的行为属于副作用,但并不是用户行为触发的。

对于这种:在视图渲染后触发的副作用,就属于effect,应该交给useEffect处理。

回到开篇的例子:

当你希望状态a变化后发起请求,首先应该明确,你的需求是:

状态a变化,接下来需要发起请求

还是

某个用户行为需要发起请求,请求依赖状态a作为参数

如果是后者,这是用户行为触发的副作用,那么相关逻辑应该放在Event handlers中。

假设之前的代码逻辑是:

  1. 点击按钮,触发状态a变化
  2. useEffect执行,发送请求

应该修改为:

  1. 点击按钮,在事件回调中获取状态a的值
  2. 在事件回调中发送请求

经过这样修改,状态a变化发送请求之间不再有因果关系,后续对状态a的修改不会再有无意间触发请求的顾虑。

总结

当我们编写组件时,应该尽量将组件编写为纯函数。

对于组件中的副作用,首先应该明确:

用户行为触发的还是视图渲染后主动触发的

对于前者,将逻辑放在Event handlers中处理。

对于后者,使用useEffect处理。

这也是为什么useEffect所在章节在新文档中叫做Escape Hatches —— 大部分情况下,你不会用到useEffect,这只是其他情况都不适应时的逃生舱。

原文:https://segmentfault.com/a/1190000041942007

收起阅读 »

web前端-JavaScript中的函数(创建,参数,返回值,方法,函数作用域,立即执行函数)

文章目录 简介函数的创建1 用构造函数创建2 用函数声明创建3 用函数表达式创建 函数的参数 参数特性1 调用函数时解析器不会检查实参的类型2 调用函数时解析器不会检查实参的数量3 当形参和实参过多,可以用一个对象封装 函数的返回值...
继续阅读 »


文章目录

  • 简介
  • 函数的创建

  • 函数的参数






  • 函数的返回值
  • 立即执行函数
  • 方法
  • 函数作用域
  • 补充:JavaScript中的作用域相关概念
  •  

    简介


    函数(Function


    • 函数也是一个对象
    • 函数中可以封装一些功能(代码),在需要时可以执行这些功能(代码)。
    • 函数中可以保存一些代码,在需要的时候调用。

    函数的创建




    在JavaScript中有三种方法来创建函数


    1. 构造函数创建
    2. 函数声明创建
    3. 函数表达式创建

    其中第一种方法在实际使用中并不常用。创建函数之后需调用函数才可执行函数体内的代码。
    函数的调用:





    语法:函数名();



    1 用构造函数创建



    语法:var 函数名 = new Function(“语句;”)





    使用new关键字创建一个函数,将要封装的功能(代码)以字符串的形式传递给封装函数,在调用函数时,封装的功能(代码)会按照顺序执行。


    2 用函数声明创建



    语法:function 函数名([形参1,形参2....]){语句...}



    用函数声明显而易见的要简便许多,小括号中的形参视情况而写,语句写在中括号内。与构造函数不同的是不需要以字符串的形式写入。




    3 用函数表达式创建



    语法:var 变量(函数名)=function([形参1,形参2....]){语句...};



    函数表达式和函数声明的方式创建函数的方法相似,不同的是用函数表达式创建函数是将一个匿名函数赋值给一个变量,同时在语句结束后需加分号结尾。













    web前端-JavaScript中的函数(创建,参数,返回值,方法,函数作用域,立即执行函数)







    苏凉.py

    已于 2022-06-16 00:40:01 修改

    596



    收藏

    88


















    🐚作者简介:苏凉(专注于网络爬虫,数据分析,正在学习前端的路上)
    🐳博客主页:苏凉.py的博客
    🌐系列专栏:web前端基础教程
    👑名言警句:海阔凭鱼跃,天高任鸟飞。
    📰要是觉得博主文章写的不错的话,还望大家三连支持一下呀!!!
    👉关注✨点赞👍收藏📂






    简介


    函数(Function


    • 函数也是一个对象
    • 函数中可以封装一些功能(代码),在需要时可以执行这些功能(代码)。
    • 函数中可以保存一些代码,在需要的时候调用。

    函数的创建


    在JavaScript中有三种方法来创建函数


    1. 构造函数创建
    2. 函数声明创建
    3. 函数表达式创建

    其中第一种方法在实际使用中并不常用。创建函数之后需调用函数才可执行函数体内的代码。
    函数的调用:



    语法:函数名();



    1 用构造函数创建



    语法:var 函数名 = new Function(“语句;”)



    使用new关键字创建一个函数,将要封装的功能(代码)以字符串的形式传递给封装函数,在调用函数时,封装的功能(代码)会按照顺序执行。


    在这里插入图片描述


    2 用函数声明创建



    语法:function 函数名([形参1,形参2....]){语句...}



    用函数声明显而易见的要简便许多,小括号中的形参视情况而写,语句写在中括号内。与构造函数不同的是不需要以字符串的形式写入。


    在这里插入图片描述


    3 用函数表达式创建



    语法:var 变量(函数名)=function([形参1,形参2....]){语句...};



    函数表达式和函数声明的方式创建函数的方法相似,不同的是用函数表达式创建函数是将一个匿名函数赋值给一个变量,同时在语句结束后需加分号结尾。


    在这里插入图片描述


    函数的参数


    • 可以在函数的()中来指定一个或多个形参(形式参数)。
    • 多个形参之间使用,隔开,声明形参就相当于在函数内部声明了对应的变量但是并不赋值。
    • 调用函数时,可以在()中指定实参(实际参数),实参将会赋值给函数中对应的形参。



    参数特性


    1 调用函数时解析器不会检查实参的类型


    函数的实参可以时任意数据类型,在调用函数时传递的实参解析器并不会检查实参的类型,因此需要注意,是否有可能接收到非法的参数,如果有可能则需要对参数进行类型的检查。




    2 调用函数时解析器不会检查实参的数量


    在调用函数传入实参时,解析器不会检查实参的数量,当实参数大于形参数时,多余实参不会被赋值




    当实参数小于形参数时,没有被赋值的形参为undefined。


    3 当形参和实参过多,可以用一个对象封装


    当形参和实参数量过多时,我们很容易将其顺序搞乱或者传递参数时出错,此时我们可以将数据封装在一个对象中,在进行实参传递时,传入该对象即可。




    函数的返回值


    可以使用return来设置函数的返回值



    语法:return 值


     


  • return后的值将会作为函数的执行结果返回
  • 可以定义一个变量,来接收该结果。
  • 在return后的语句都不会执行。

  • 若return后不跟任何值或者不写return,函数的返回值都是undefined。


    另外,在函数体中return返回的是什么,变量接受的就是什么。


    立即执行函数


    • 函数定义完,立即被调用,这种函数叫做立即执行函数
    • 立即执行函数往往只会执行一次
    • 通常为匿名函数的调用。


    语法:(function(形参...){语句...})(实参...);





    方法


    对象的属性值可以时任意的数据类型,当属性值为一个函数时,在对象中调用该函数,就叫做调用该对象的方法。




    函数作用域




  • 调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁
  • 每调用一次函数就会创建一个新的函数作用域,他们之间是互相独立的
    在函数作用域中可以访问到全局作用域

  • 的变量,在全局作用域中无法访问到函数作用域的变量


    当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用,如果没有则向上一级作用域中寻找,直到找到全局作用域,如果全局作用域中依然没有找到,则会报错ReferenceError




    补充:JavaScript中的作用域相关概念







    在全局作用域中有一个全局对象window它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用

  • 作用域指一个变量的作用范围
  • 在JavaScript中有两种作用域1.全局作用域 2.函数作用域
  • 直接编写在script标签中的JS代码,都在全局作用域
  • 全局作用域在页面打开时创建,在页面关闭时销毁

  • 简而言之我们创建的全局变量都作为一个属性保存在window这个对象中。


    而在函数中创建局部变量时,必须使用var关键字创建,否则为全局变量。



    收起阅读 »

    JavaScript映射与集合(Map、Set)数据类型基础知识介绍与使用

    文章目录 映射与集合(Map、Set)映射(Map)Map常用的方法不要使用map[key]访问属性对象作为Map的键Map的遍历与迭代默认的迭代方式forEach() 从数组、对象创建Map从数组、Map创建对象 集合(Set)集合迭代 ...
    继续阅读 »







    映射与集合(Map、Set)

    前文的学习过程中,我们已经了解了非常多的数据类型,包括基础类型、复杂的对象、顺序存储的数组等。为了更好的应对现实生产中的情况,我们还需要学习更多的数据类型:映射(Map)和集合(Set)。
    映射(Map)

    Map是一个键值对构成的集合,和对象非常相似,都是由一个名称对应一个值组成的。Map和对象区别在于,Map的键可以采用任何类型的数据,而对象只能使用字符串作为属性名称
    Map常用的方法

    new Map()——创建Map对象;
    map.set(key, val)——添加一个键值对;
    map.get(key)——通过键找到val值,如果不存在key,返回undefined
    map.has(key)——判断map是否存在键key,存在返回true,不存在返回false
    map.delete(key)——删除指定键;
    map.clear()——清空map中所有的内容;
    map.size——map中键值对的数量;

    举个例子:

    let map = new Map()//创建一个空的Map
    map.set('name','xiaoming') //字符串作为键
    map.set(3120181049,'ID') //数字作为键
    map.set(true,'Bool') //bool作为键

    console.log(map.get('name'))//xiaoming
    console.log(map.has(true)) //true
    console.log(map.delete(true))//删除true键
    console.log(map.size) //2
    console.log(map.clear()) //清空
    console.log(map.size) //0


    代码执行结果:



    map.set(key, val)方法返回map本身。


    不要使用map[key]访问属性


    虽然map[key]方式同样可以访问映射的键值对,但是不推荐使用这种方式,因为它会造成歧义。我们可以看下面的案例:



    let map = new Map()
    map[123] = 123 //创建一个键值对
    console.log(map[123])//123
    console.log(map['123'])


    这里就出现了一个奇怪的结果:


    image-20220610213719690


    不仅使用键123还可以使用'123'访问数据。


    甚至,如果我们使用map.set()map[]混用的方式,会引起程序错误。


    JavaScript中,如果我们对映射使用了map[key]=val的方式,引擎就会把map视为plain object,它暗含了对应所有相应的限制(仅支持StringSymbol键)。


    所以,我们不要使用map[key]的方式访问Map的属性!!


    对象作为Map的键




    由于Map对键的类型不做任何限制,我们还可以把对象当作键值使用:
    let clazz = {className:'9年1班'}
    let school = new Map()
    school.set(clazz,{stu1:'xiaoming',stu2:'xiaohong'})
    console.log(school.get(clazz))



    代码执行结果:


    image-20220610215432261


    在对象中,对象是不能作为属性名称存在的,如果我们把对象作为属性名,也会发生奇怪的事:

    let obj = {}
    let objKey = {key:'key'}
    obj[objKey] = 'haihaihai'
    console.log(obj['[object Object]'])


    代码执行结果:


    image-20220610215731673


    发生这种现象的原因也非常简单,对象会把非字符串、Symbol类型的属性名转为字符串类型,对象相应的就转为'[object Object]'了,于是对象中就出现了一个名为'[object Object]'的属性。





    Map键值比较方法


    Map使用SameValueZero算法比较键值是否相等,和===差不多,但是NaNNaN是相等的,所以NaN也可以作为键使用!





    链式调用


    由于map.set返回值是map本身,我们可以使用如下调用方式:


    map.set(1,1)
    .set(2,2)
    .set(3,3)




    Map的遍历与迭代


    我们可以在以下三个函数的帮助下完成映射的迭代:


    1. map.keys()——返回map所有键的可迭代对象;
    2. map.values()——返回map所有值的可迭代对象;
    3. map.entries()——返回map所有键值对的可迭代对象;

    举个栗子:



    let map = new Map([
    ['key1',1],
    ['key2',2],
    ['key3',3],
    ])

    //遍历所有的键
    for(let key of map.keys()){
    console.log(key)
    }

    //遍历所有的值
    for(let val of map.values()){
    console.log(val)
    }

    //遍历所有的键值对
    for(let ky of map.entries()){
    console.log(ky)
    }


    代码执行结果:


    image-20220611202407661



    遍历的顺序


    遍历的顺序和元素插入顺序是相同的,这是和对象的区别之一。





    默认的迭代方式


    实际上,我们很少使用map.entries()方法遍历Map中的键值对,因为map.entries()map的默认遍历方式,我们可以直接使用如下代码:


    let map = new Map([
    ['key1',1],
    ['key2',2],
    ['key3',3],
    ])
    for(let kv of map){
    console.log(kv)
    }





    码执行结果:


    image-20220611203140858


    forEach()


    我们还可以通过Map内置的forEach()方法,为每个元素设置一个遍历方法,就像遍历数组一样。


    举例如下:




    let map = new Map([
    ['key1',1],
    ['key2',2],
    ['key3',3],
    ])
    map.forEach((val,key,map)=>{
    console.log(`${key}-${val}`)
    })



    代码执行结果:


    image-20220611203643650


    从数组、对象创建Map


    可能童鞋们已经发现了,在上面的案例中,我们使用了一种独特的初始化方式(没有使用set方法)




    let map = new Map([
    ['key1',1],
    ['key2',2],
    ['key3',3],
    ])




    我们通过向new Map()传入一个数组,完成了快速的映射创建。


    我们还可以通过Object.entires(obj)方法将对象转为数组,该数组的格式和Map需要的格式完全相同。


    举个例子:




    let obj = {
    xiaoming:'heiheihei',
    xiaohong:'hahahahah'
    }
    let map = new Map(Object.entries(obj))
    console.log(map)



    代码执行结果:


    image-20220611205622630


    Object.entries(obj)会返回obj对应的数组:[['xiaoming':'heiheihei'],['xiaoming':'hahahahah']]




    从数组、Map创建对象


    Object.fromEntries()Object.entries()功能相反,可以把数组和Map转为对象。


    数组转对象:




    let obj = Object.fromEntries([
    ['key1','val1'],
    ['key2','val2'],
    ['key3','val3'],
    ])
    console.log(obj)



    代码执行结果:


    image-20220611210835380


    Map转对象:




    let map = new Map()
    map.set('key1','val1')
    .set('key2','val2')
    .set('key3','val3')
    let obj = Object.fromEntries(map)
    console.log(obj)



    代码执行结果:


    image-20220611211125496


    map.entries()会返回映射对应的键值对数组,我们也可以使用一种稍微麻烦的方式:

    let obj = Object.fromEntries(map.entries())





    1. new Set([iter])——创建一个集合,如果传入了一个可迭代变量(例如数组),就使用这个变量初始化集合
    2. set.add(val)——向集合中添加一个元素val
    3. set.delete(val)——删除集合中的val




    4. set.has(val)——判断集合中是否存在val,存在返回true,否则返回false
    5. set.clear()——清空集合中所有的元素
    6. set.size——返回集合中元素的数量


    集合使用案例:


    let set = new Set()
    let xiaoming = {name:'xiaoming'}
    let xiaohong = {name:'xiaohong'}
    let xiaojunn = {name:'xiaojunn'}



    set.add(xiaoming)
    set.add(xiaohong)
    set.add(xiaojunn)
    console.log(set)



    代码执行结果:


    image-20220611212417105





    虽然Set的功能很大程度上可以使用Array代替,但是如果使用arr.find判断元素是否重复,就会造成巨大的性能开销。


    所以我们需要在合适的场景使用合适的数据结构,从而保证程序的效率。



    集合迭代




    集合的迭代非常简单,我们可以使用for...offorEach两种方式:

    let set = new Set(['xiaoming','xiaohong','xiaoli'])//使用数组初始化集合
    for(let val of set){
    console.log(val)
    }
    set.forEach((val,valAgain,set)=>{
    console.log(val)
    })


    代码执行结果:


    image-20220611212802952




    注意,使用forEach遍历集合时,和map一样有三个参数,而且第一个和第二个参数完全相同。这么做的目的是兼容Map,我们可以方便的使用集合替换Map而程序不会出错。


    Map中使用的方法,Set同样适用:


    1. set.keys()——返回一个包含所有值的可迭代对象
    2. set.values()——返回值和set.keys()完全相同
    3. set.entries()——返回[val,val]可迭代对象



    看起啦这些方法有些功能上的重复,很奇怪。实际上,和forEach一样,都是为了和Map兼容。


    总结


    Map 是一个带键的数据项的集合。




    常用方法:





    1. map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined
    2. map.has(key) —— 如果 key 存在则返回 true,否则返回 false
    3. new Map([iter]) —— 创建 map,可选择带有 [key,value] 对的 iterable(例如数组)来进行初始化;
    4. map.set(key, val) —— 根据键存储值,返回 map 自身,可用于链式插入元素;




    5. map.delete(key) —— 删除指定键对应的值,如果在调用时 key 存在,则返回 true,否则返回 false
    6. map.clear() —— 清空 map中所有键值对 ;
    7. map.size —— 返回键值对个数

    与普通对象 Object 的不同点主要是任何类型都可以作为键,包括对象、NaN


    Set —— 是一组值的集合。


    常用方法和属性:
















    MapSet 中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素。


















  • new Set([iter]) —— 创建 set,可选择带有 iterable(例如数组)来进行初始化。
  • set.add(value) —— 添加一个值(如果 value 存在则不做任何修改),返回 set 本身。
  • set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false
  • set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false
  • set.clear() —— 清空 set。
  • set.size —— 元素的个数。
  • 收起阅读 »

    React-Native热更新 - 3分钟教你实现

    此文使用当前最新版本的`RN`与`Code-Push`进行演示,其中的参数不会过多进行详细解释,更多参数解释可参考其它文章,这里只保证APP能正常进行热更新操作,方便快速入门,跟着大猪一起来快活吧。操作指南以下操作在Mac系统上完成的,毕竟 大猪 工作多年之后...
    继续阅读 »

    此文使用当前最新版本的`RN`与`Code-Push`进行演示,其中的参数不会过多进行详细解释,更多参数解释可参考其它文章,这里只保证APP能正常进行热更新操作,方便快速入门,跟着大猪一起来快活吧。

    操作指南

    以下操作在Mac系统上完成的,毕竟 大猪 工作多年之后终于买得起一个Mac了。

    1. 创建`React-Native`项目

    ```
    react-native init dounineApp
    `
    ``

    2. 安装`code-push-cli`

    ```
    npm install -g code-push-cli
    `
    ``

    3. 注册`code-push`帐号

    ```
    code-push register
    Please login to Mobile Center in the browser window we've just opened.
    Enter your token from the browser:
    #会弹出一个浏览器,让你注册,可以使用github帐号对其进行授权,授权成功会给一串Token,点击复制,在控制进行粘贴回车(或者使用code-push login命令)。
    `
    ``
    ```
    Enter your token from the browser: b0c9ba1f91dd232xxxxxxxxxxxxxxxxx
    #成功提示如下方
    Successfully logged-in. Your session file was written to /Users/huanghuanlai/.code-push.config. You can run the code-push logout command at any time to delete this file and terminate your session.
    `
    ``
    ![](http://upload-images.jianshu.io/upload_images/9028759-7736182c03cea82a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    4. 在`code-push`添加一个ios的app

    ```
    code-push app add dounineApp-ios ios react-native
    #成功提示如下方
    Successfully added the "dounineApp-ios" app, along with the following default deployments:
    ┌────────────┬──────────────────────────────────────────────────────────────────┐
    │ Name │ Deployment Key │
    ├────────────┼──────────────────────────────────────────────────────────────────┤
    │ Production │ yMAPMAjXpfXoTfxCd0Su9c4-U4lU6dec4087-57cf-4c9d-b0dc-ad38ce431e1d │
    ├────────────┼──────────────────────────────────────────────────────────────────┤
    │ Staging │ IjC3_iRGEZE8-9ikmBZ4ITJTz9wn6dec4087-57cf-4c9d-b0dc-ad38ce431e1d │
    └────────────┴──────────────────────────────────────────────────────────────────┘
    `
    ``

    5. 继续在`code-push`添加一个android的app

    ```
    code-push app add dounineApp-android android react-native
    #成功提示如下方
    Successfully added the "dounineApp-android" app, along with the following default deployments:
    ┌────────────┬──────────────────────────────────────────────────────────────────┐
    │ Name │ Deployment Key │
    ├────────────┼──────────────────────────────────────────────────────────────────┤
    │ Production │ PZVCGLlVW-0FtdoCF-3ZDWLcX58L6dec4087-57cf-4c9d-b0dc-ad38ce431e1d │
    ├────────────┼──────────────────────────────────────────────────────────────────┤
    │ Staging │ T0NshYi9X8nRkIe_cIRZGbAut90a6dec4087-57cf-4c9d-b0dc-ad38ce431e1d │
    └────────────┴──────────────────────────────────────────────────────────────────┘
    `
    ``

    6. 在项目根目录添加`react-native-code-push`

    ```
    npm install react-native-code-push --save
    #或者
    yarn add react-native-code-push
    `
    ``

    7. link react-native-code-push

    ```
    react-native link
    Scanning folders for symlinks in /Users/huanghuanlai/dounine/oschina/dounineApp/node_modules (8ms)
    ? What is your CodePush deployment key for Android (hit to ignore) T0NshYi9X8nRkIe_cIRZGbAut90a6dec4087-57cf-4c9d-b0dc-ad38ce431e1d
    #将刚才添加的Android App的Deployment Key复制粘贴到这里,复制名为Staging测试Deployment Key。
    rnpm-install info Linking react-native-code-push android dependency
    rnpm-install info Android module react-native-code-push has been successfully linked
    rnpm-install info Linking react-native-code-push ios dependency
    rnpm-install WARN ERRGROUP Group 'Frameworks' does not exist in your Xcode project. We have created it automatically for you.
    rnpm-install info iOS module react-native-code-push has been successfully linked
    Running ios postlink script
    ? What is your CodePush deployment key for iOS (hit to ignore) IjC3_iRGEZE8-9ikmBZ4ITJTz9wn6dec4087-57cf-4c9d-b0dc-ad38ce431e1d
    #继续复制Ios的Deployment Key
    Running android postlink script
    `
    ``

    8. 在`react-native`的`App.js`文件添加自动更新代码

    ```
    import codePush from "react-native-code-push";
    const codePushOptions = { checkFrequency: codePush.CheckFrequency.MANUAL };
    export default class App extends Component<{}> {
    componentDidMount(){
    codePush.sync({
    updateDialog: true,
    installMode: codePush.InstallMode.IMMEDIATE,
    mandatoryInstallMode:codePush.InstallMode.IMMEDIATE,
    //deploymentKey为刚才生成的,打包哪个平台的App就使用哪个Key,这里用IOS的打包测试
    deploymentKey: 'IjC3_iRGEZE8-9ikmBZ4ITJTz9wn6dec4087-57cf-4c9d-b0dc-ad38ce431e1d',
    });
    }
    ...
    `
    ``

    9. 运行项目在ios模拟器上

    ```
    react-native run-ios
    `
    ``

    如图下所显

    1:开启debug调试

    2:`CodePush`已经成功运行

    目前App已经是最新版本

    ![](http://upload-images.jianshu.io/upload_images/9028759-41607a87f412b06a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    10. 发布一个ios新版本

    ```
    code-push release-react dounineApp-ios ios
    `
    ``
    发布成功如图下
    ```
    Detecting ios app version:
    Using the target binary version value "1.0" from "ios/dounineApp/Info.plist".
    Running "react-native bundle" command:
    node node_modules/react-native/local-cli/cli.js bundle --assets-dest /var/folders/m_/xcdff0xd62j4l2xbn_nfz00w0000gn/T/CodePush --bundle-output /var/folders/m_/xcdff0xd62j4l2xbn_nfz00w0000gn/T/CodePush/main.jsbundle --dev false --entry-file index.js --platform ios
    Scanning folders for symlinks in /Users/huanghuanlai/dounine/oschina/dounineApp/node_modules (10ms)
    Scanning folders for symlinks in /Users/huanghuanlai/dounine/oschina/dounineApp/node_modules (10ms)
    Loading dependency graph, done.
    bundle: start
    bundle: finish
    bundle: Writing bundle output to: /var/folders/m_/xcdff0xd62j4l2xbn_nfz00w0000gn/T/CodePush/main.jsbundle
    bundle: Done writing bundle output
    Releasing update contents to CodePush:
    Upload progress:[==================================================] 100% 0.0s
    Successfully released an update containing the "/var/folders/m_/xcdff0xd62j4l2xbn_nfz00w0000gn/T/CodePush" directory to the "Staging" deployment of the "dounineApp-ios" app.
    `
    ``

    11. 重新Load刷新应用

    ![](http://upload-images.jianshu.io/upload_images/9028759-30c17d2f5db173cd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    12. 安卓发布

    与上面9~11步骤是一样的,命令改成Android对应的,以下命令结果简化

    1.修改App.js的deploymentKey为安卓的

    ```
    deploymentKey:'T0NshYi9X8nRkIe_cIRZGbAut90a6dec4087-57cf-4c9d-b0dc-ad38ce431e1d'
    `
    ``

    2.运行

    ```
    react-native run-android
    `
    ``

    3.发布

    ```
    code-push release-react dounineApp-android android
    `
    ``
    收起阅读 »

    React-Native iOS 列表(ListView)优化方案

    在项目开发中,很多地方用到了列表,而 React-Native 官网中提供的组件 ListView,虽然能够满足我们的需求,但是性能问题并没有很好的解决,对于需要展现大量数据的列表,app 的内存将会非常庞大。针对 React-Native 的列表性能问题,现...
    继续阅读 »

    在项目开发中,很多地方用到了列表,而 React-Native 官网中提供的组件 ListView,虽然能够满足我们的需求,但是性能问题并没有很好的解决,对于需要展现大量数据的列表,app 的内存将会非常庞大。针对 React-Native 的列表性能问题,现在提供几套可行性方案:

    1.利用 Facebook 提供的建议对 ListView 进行优化

    Facebook 官方对 ListView 的性能优化做了简单介绍,并提供了以下几个方法:

    • initialListSize
    • 这个属性用来指定我们第一次渲染时,要读取的行数。如果我们想尽可能的快,我们可以设置它为1, 然后可以在后续的帧中,填弃其它的行。每一次读取的行数,由 pageSize 决定.
    • pageSize
    • 在使用了 initialListSize 之后,ListView 根据 pageSize 来决定每一帧读取的行数,默认值为1, 但如果你的的 views 非常的小,并且读取时占的资源很少, 你可以调整这个值,在找到适合你的值。
    • scrollRenderAheadDistance
    • 如果我们的列表有2000个项,而让它一次性读取,它会导致内存和计算资源的耗尽。所以 scrollRenderAhead distance 可以指定,超出当前视图多少,继续宣染。
    • removeClippedSubviews
    • “当它设置为true时,当本地端的superview为offscreen时 ,不在屏幕上显示的子视图offscreen(它的overflow的值为hidden) 会被删除。它可以改善长列表的滚动的性能,默认值为true.
      这对于大的ListViews来说是一个非常重要。在Android, overflow的值通常为hidden. 所以我们并不需要担心它的设置,但是对于iOS来说,你需要设置row Container的样式为overflow: hidden。

    在使用了上述方法后,我们可以看到app的内存占用有了一定的下降(加载100张图片时的效果):

    使用前


    使用后


    3.桥接 Native tableview

    第二种方法里面,能够比较好的解决屏幕外的 cell 内存问题,但是和 native tableview 相比,并没有 native 的 cell 重用机制完美,那么,我们可以讲 native 的 tableview 桥接到 React-native 中来,让我们可以在 React-Native 中也可以重用 cell

    我们创建一些 VirtualView,他只是遵从了 RCTComponent 协议,其实并不是一个真正的 View,我把它形成一个组件,把它 Bridge 到 JS,这就使得,你在写 JSX 的时候,就可以直接用 VirtualView 来去做布局了。在RN里面做布局的时候我们用VirtualView来做布局。但是最终在 insertReactSubview 时,我们把这些 VirtualView 当做数据去处理,通过 VirtualView 和RealView 的对应关系,把它转化成一个真实的 View 对象添加到 TableView 中去。


    但是使用这种方法,我们需要将 tableview 的所有常用数据源方法和代理方法都桥接到 React-Native 中来,甚至对于一些 cell 组件,我们也需要自己桥接,并不能像 React-Native 那样使用自己的组件。当我们的需求比较复杂或者需求发生变化时,就需要重新桥接我们的自定义 cell,这样工作量就会比较大。大多数的 cell 里面如果做展示来用的话,Label 和 Image 基本上能够满足大多数的需求了。所以我们现在只是做了 Label 和 Image 的对应工作,但在 RN 的一些官方控件,在这个 view 里面都是没法直接使用的。

    4.用 JS 实现一套 cell 重用的逻辑

    基于 RN 的 ScrollView,我们也监听 OnScroll(),他往上滑的时候,我们需要把上面的 cellComponent 挪下来,挪到上面去用。但是这个方式最终的效果并不是特别好。

    问题在于,如果我们所有的 Cell 都是一样高的,里面的元素不是很多的情况下,性能还相对好一些,我们每次 OnScroll 的时候,他处理的Cell比较少。如果你希望有一个界面滚动能够达到流畅的话,所有的处理都需要在 16ms 内完成,但是这又造成了 onScroll 都要去刷新页面,导致这样的交互会非常非常多,导致你从 JS,到 native 的 bridge 要频繁的通讯,JS 中的很多处理方式都是异步的,使得这个方案的效果没有达到很好的预期。

    总结

    从上面的几种方案可以看出,方案1、2、3、4都能够比较好的解决列表的性能问题 ,而且各有优缺点,那么,我们在项目开发中该如何应用呢?

    • 当我们在进行列表展示的时候,如果数据量不是特别的庞大(不是无限滚动的),且界面比较复杂的时候,方案1能够比较好的解决性能问题,而且操作起来比较简单,只需要对 listview 的一些属性进行基本设置。
    • 当我们需要展示很多数据的时候(不是无限滚动的),我们可以使用方案2,对那些超出屏幕外的部分,对他进行组件最小化
    • 当我们需要展示大量数据(可以无限滚动的),我们可以通过方案3/4,来达到重用的目的


    收起阅读 »

    应急响应 WEB 分析日志攻击,后门木马(手动分析 和 自动化分析.)

    💛不登上悬崖💚,💛又怎么领略一览众山的绝顶风光💚🍪目录:🌲应急响应的概括:🌲应急响应阶段:🌲应急响应准备工作:🌲从入侵面及权限面进行排查:🌲工具下载🌲应急响应的日志分析:🌷手动化分析日志:🌷自动化化分析日志: (1)360星图.(支持 iis /...
    继续阅读 »


    💛不登上悬崖💚,💛又怎么领略一览众山的绝顶风光💚
    🍪目录:

    🌲应急响应的概括:

    🌲应急响应阶段:

    🌲应急响应准备工作:

    🌲从入侵面及权限面进行排查:

    🌲工具下载

    🌲应急响应的日志分析:

    🌷手动化分析日志:

    🌷自动化化分析日志:

    (1)360星图.(支持 iis / apache / nginx日志)

    (2)方便大量日志查看工具.

    🌷后门木马检测.

    (1)D盾_Web查杀. 

    🌲应急响应的概括:
    🌾🌾🌾应急响应”对应的英文是“Incident Response”或“Emergency Response”等,通常是指一个组织为了应对各种意外事件的发生所做的准备以及在事件发生后所采取的措.
    🌾🌾🌾网络安全应急响应:针对已经发生的或可能发生的安全事件进行监控、分析、协调、处理、保护资产安全.

    🌲应急响应阶段:

    保护阶段:断网,备份重要文件(防止攻击者,这些期间删除文件重要文件.)

    分析阶段:分析攻击行为,找出相应的漏洞.

    复现阶段:复现攻击者攻击的过程,有利于了解当前环境的安全问题和安全检测.

    修复阶段:对相应的漏洞提出修复.

    建议阶段:对漏洞和安全问题提出合理解决方案.
    目的:分析出攻击时间,攻击操作,攻击后果,安全修复等并给出合理解决方案

    🌲应急响应准备工作:
    (1)收集目标服务器各类信息.

    (2)部署相关分析软件及平台等.

    (3)整理相关安全渗透工具指纹库.

    (4)针对异常表现第一时间触发思路. 

    🌲从入侵面及权限面进行排查:
    有明确信息网站被入侵: 1.基于时间 2.基于操作 3.基于指纹 4.基于其他.


    无明确信息网站被入侵:(1)WEB 漏洞-检查源码类别及漏洞情况.
    (2)中间件漏洞-检查对应版本及漏洞情况.
    (3)第三方应用漏洞-检查是否存在漏洞应用.
    (4)操作系统层面漏洞-检查是否存在系统漏洞.
    (5)其他安全问题(口令,后门等)- 检查相关应用口令及后门扫描.


    🌲工具下载   链接:https://pan.baidu.com/s/14njkNfj3HisIKN26IYOZXQ 
                        提取码:tian 




    🌲应急响应的日志分析:


    🌷手动化分析日志:


    (1)弱口令的爆破日志.(可以看到是一个IP在同一个时间,使用多个账号和密码不停测试)





    (2)SQL注入的日志.(搜索 select 语句.)


    (3)有使用SQLmap工具的注入.(搜索SQLmap)


    我的靶场日志没有记录SQLmap.(这里就不加图了)


         


    (4)目录扫描日志.(看的时候会发现,前面的目录都是一样的.)




         


    (5)XSS攻击日志.(搜索:script,javascript,onclick,%3Cimg对这些关键字进行查看)
    (7)目录遍历攻击日志.


    (8)后门木马日志.(搜索连接工具:anTSword,菜刀,冰蝎等工具 排查后门.)


           


           


    🌷自动化化分析日志:


    (1)360星图.(支持 iis / apache / nginx日志


    1.设置日志分析路径.
    2.点击进行日志分析.


        


    3.点击查看日志.




        


    安全分析报告.




    常规分析报告.


         


    (2)方便大量日志查看工具.


    1.工具的设置.




    2.SQL注入攻击日志.


    3.目录遍历攻击.


    4.XSS攻击日志.


          


    🌷后门木马检测.


    (1)D盾_Web查杀.


    1.选择扫描的目录.




    2.扫描到的后门木马.












    收起阅读 »

    堆(优先级队列)

     目录 🥬堆的性质 🥬堆的分类  🥬堆的向下调整 🥬堆的建立 🥬堆得向上调整 🥬堆的常用操作 🍌入队列 🍌出队列 🍌获取队首元素 🥬TopK 问题 🥬堆的性质 堆逻辑上是一棵完全二叉树,堆物理上是保存在数组中 。总...
    继续阅读 »


     目录


    🥬堆的性质


    🥬堆的分类


     🥬堆的向下调整


    🥬堆的建立


    🥬堆得向上调整


    🥬堆的常用操作


    🍌入队列


    🍌出队列


    🍌获取队首元素


    🥬TopK 问题



    🥬堆的性质




    堆逻辑上是一棵完全二叉树,堆物理上是保存在数组中 。

    总结:一颗完全二叉树以层序遍历方式放入数组中存储,这种方式的主要用法就是堆的表示。
    并且 如果已知父亲(parent) 的下标,则:
    左孩子(left) 下标 = 2 * parent + 1;
    右孩子(right) 下标 = 2 * parent + 2;
    已知孩子(不区分左右)(child)下标,则:
    双亲(parent) 下标 = (child - 1) / 2;
    🥬堆的分类
    大堆:根节点大于左右两个子节点的完全二叉树 (父亲节点大于其子节点),叫做大堆,或者大根堆,或者最大堆 。 


    小堆:根节点小于左右两个子节点的完全二叉树叫
    小堆(父亲节点小于其子节点),或者小根堆,或者最小堆。

    🥬堆的向下调整

    现在有一个数组,逻辑上是完全二叉树,我们通过从根节点开始的向下调整算法可以把它调整成一个小堆或者大堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

    以小堆为例:

    1、先让左右孩子结点比较,取最小值。

    2、用较小的那个孩子结点与父亲节点比较,如果孩子结点<父亲节点,交换,反之,不交换。

    3、循环往复,如果孩子结点的下标越界,则说明已经到了最后,就结束。

    //parent: 每棵树的根节点
    //len: 每棵树的调整的结束位置

    public void shiftDown(int parent,int len){
    int child=parent*2+1; //因为堆是完全二叉树,没有左孩子就一定没有右孩子,所以最起码是有左孩子的,至少有1个孩子
    while(child<len){
    if(child+1<len && elem[child]<elem[child+1]){
    child++;//两孩子结点比较取较小值
    }
    if(elem[child]<elem[parent]){
    int tmp=elem[parent];
    elem[parent]=elem[child];
    elem[child]=tmp;
    parent=child;
    child=parent*2+1;
    }else{
    break;
    }
    }
    }


    🥬堆的建立
    给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆(左右子树不满足都是大堆或者小堆),现在我们通过算法,把它构建成一个堆(大堆或者小堆)。该怎么做呢?这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。 这里我们就要用到刚才写的向下调整。

    public void creatHeap(int[] arr){
    for(int i=0;i<arr.length;i++){
    elem[i]=arr[i];
    useSize++;
    }
    for(int parent=(useSize-1-1)/2;parent>=0;parent--){//数组下标从0开始
    shiftDown(parent,useSize);
    }
    }



    建堆的空间复杂度为O(N),因为堆为一棵完全二叉树,满二叉树也是一种完全二叉树,我们用满二叉树(最坏情况下)来证明。


    🥬堆得向上调整


    现在有一个堆,我们需要在堆的末尾插入数据,再对其进行调整,使其仍然保持堆的结构,这就是向上调整。


    以大堆为例:




    代码示例:

    public void shiftup(int child){
    int parent=(child-1)/2;
    while(child>0){
    if(elem[child]>elem[parent]){
    int tmp=elem[parent];
    elem[parent]=elem[child];
    elem[child]=tmp;
    child=parent;
    parent=(child-1)/2;
    }else{
    break;
    }
    }
    }



    🥬堆的常用操作


    🍌入队列


    往堆里面加入元素,就是往最后一个位置加入,然后在进行向上调整

    public boolean isFull(){
    return elem.length==useSize;
    }

    public void offer(int val){
    if(isFull()){
    elem= Arrays.copyOf(elem,2*elem.length);//扩容
    }
    elem[useSize++]=val;
    shiftup(useSize-1);
    }



    🍌获取队首元素



    public int peek() {
    if (isEmpty()) {
    throw new RuntimeException("优先级队列为空");
    }
    return elem[0];
    }


    🥬TopK 问题

    给你6个数据,求前3个最大数据。这时候我们用堆怎么做的?

    解题思路:

    1、如果求前K个最大的元素,要建一个小根堆。
    2、如果求前K个最小的元素,要建一个大根堆。
    3、第K大的元素。建一个小堆,堆顶元素就是第K大的元素。
    4、第K小的元素。建一个大堆,堆顶元素就是第K大的元素。

    🍌举个例子:求前n个最大数据

     
    代码示例:

    public static int[] topK(int[] array,int k){
    //创建一个大小为K的小根堆
    PriorityQueue<Integer> minHeap=new PriorityQueue<>(k, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
    return o1-o2;
    }
    });
    //遍历数组中元素,将前k个元素放入队列中
    for(int i=0;i<array.length;i++){
    if(minHeap.size()<k){
    minHeap.offer(array[i]);
    }else{
    //从k+1个元素开始,分别和堆顶元素比较
    int top=minHeap.peek();
    if(array[i]>top){
    //先弹出后存入
    minHeap.poll();
    minHeap.offer(array[i]);
    }
    }
    }
    //将堆中元素放入数组中
    int[] tmp=new int[k];
    for(int i=0;i< tmp.length;i++){
    int top=minHeap.poll();
    tmp[i]=top;
    }
    return tmp;
    }

    public static void main(String[] args) {
    int[] array={12,8,23,6,35,22};
    int[] tmp=topK(array,3);
    System.out.println(Arrays.toString(tmp));
    }



    结果:


    🍌数组排序


     再者说如果要对一个数组进行从小到大排序,要借助大根堆还是小根堆呢?


    ---->大根堆


      代码示例:

    public void heapSort(){
    int end=useSize-1;
    while(end>0){
    int tmp=elem[0];
    elem[0]=elem[end];
    elem[end]=tmp;
    shiftDown(0,end);//假设这里向下调整为大根堆
    end--;
    }
    }



    🥬小结


    以上就是今天的内容了,有什么问题可以在评论区留言✌✌✌



    收起阅读 »

    Kotlin 协程调度切换线程是时候解开真相了

    在前面的文章里,通过比较基础的手段演示了如何开启协程、如何挂起、恢复协程。并没有涉及到如何切换线程执行,而没有切换线程功能的协程是没有灵魂的。 本篇将重点分析协程是如何切换线程执行以及如何回到原来的线程执行等知识。 通过本篇文章,你将了解到: 如何指定协程...
    继续阅读 »

    在前面的文章里,通过比较基础的手段演示了如何开启协程、如何挂起、恢复协程。并没有涉及到如何切换线程执行,而没有切换线程功能的协程是没有灵魂的。

    本篇将重点分析协程是如何切换线程执行以及如何回到原来的线程执行等知识。

    通过本篇文章,你将了解到:




    1. 如何指定协程运行的线程?

    2. 协程调度器原理

    3. 协程恢复时线程的选择



    1. 如何指定协程运行的线程?


    Android 切换线程常用手法


    常规手段


    平常大家用的切换到主线程的手段:Activity.runOnUiThread(xx),View.post(xx),Handler.sendMessage(xx) 等简单方式。另外还有一些框架,如AsyncTask、RxJava、线程池等。
    它们本质上是借助了Looper+Handler功能。

    先看个Demo,在子线程获取学生信息,拿到结果后切换到主线程展示:


        private inner class MyHandler : Handler(Looper.getMainLooper()) {
    override fun handleMessage(msg: Message) {
    //主线程弹出toast
    Toast.makeText(context, msg.obj.toString(), Toast.LENGTH_SHORT).show()
    }
    }

    //获取学生信息
    fun showStuInfo() {
    thread {
    //模拟网络请求
    Thread.sleep(3000)
    var handler = MyHandler()
    var msg = Message.obtain()
    msg.obj = "我是小鱼人"
    //发送到主线程执行
    handler.sendMessage(msg)
    }
    }

    我们知道Android UI 刷新是基于事件驱动的,主线程一直尝试从事件队列里拿到待执行的事件,没拿到就等待,拿到后就执行对应的事件。这也是Looper的核心功能,不断检测事件队列,而往队列里放事件即是通过Handler来操作的。



    子线程通过Handler 往队列里存放事件,主线程在遍历队列,这就是一次子线程切换到主线程运行的过程。



    当然了,因为主线程有消息队列,若想要抛事件到子线程执行,在子线程构造消息队列即可。


    协程切换到主线程


    同样的功能,用协程实现:


        fun showStuInfoV2() {
    GlobalScope.launch(Dispatchers.Main) {
    var stuInfo = withContext(Dispatchers.IO) {
    //模拟网络请求
    Thread.sleep(3000)
    "我是小鱼人"
    }

    Toast.makeText(context, stuInfo, Toast.LENGTH_SHORT).show()
    }
    }

    很明显,协程简洁太多。

    相较于常规手段,协程无需显示构造线程,也无需显示通过Handler发送,在Handler里接收信息并展示。

    我们有理由猜测,协程内部也是通过Handler+Looper实现切换到主线程运行的。


    协程切换线程


    当然协程不只能够从子线程切换到主线程,也可以从主线程切换到子线程,甚至在子线程之间切换。


        fun switchThread() {
    println("我在某个线程,准备切换到主线程")
    GlobalScope.launch(Dispatchers.Main) {
    println("我在主线程,准备切换到子线程")
    withContext(Dispatchers.IO) {
    println("我在子线程,准备切换到子线程")
    withContext(Dispatchers.Default) {
    println("我在子线程,准备切换到主线程")
    withContext(Dispatchers.Main) {
    println("我在主线程")
    }
    }
    }
    }
    }

    无论是launch()函数还是withContext()函数,只要我们指定了运行的线程,那么协程将会在指定的线程上运行。


    2. 协程调度器原理


    指定协程运行的线程


    接下来从launch()源码出发,一步步探究协程是如何切换线程的。

    launch()简洁写法:


        fun launch1() {
    GlobalScope.launch {
    println("launch default")
    }
    }

    launch()函数有三个参数,前两个参数都有默认值,第三个是我们的协程体,也即是 GlobalScope.launch 花括号里的内容。


    #Builders.common.kt
    public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
    ): Job {
    //构造新的上下文
    val newContext = newCoroutineContext(context)
    //构造completion
    val coroutine = if (start.isLazy)
    LazyStandaloneCoroutine(newContext, block) else
    StandaloneCoroutine(newContext, active = true)
    //开启协程
    coroutine.start(start, coroutine, block)
    return coroutine
    }

    接着看newCoroutineContext 实现:


    #CoroutineContext.kt
    actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
    //在Demo 环境里 coroutineContext = EmptyCoroutineContext
    val combined = coroutineContext + context
    //DEBUG = false
    val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
    //没有指定分发器,默认使用的分发器为:Dispatchers.Default
    //若是指定了分发器,就用指定的
    return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
    debug + Dispatchers.Default else debug
    }

    这块涉及到CoroutineContext 一些重载运算符的操作,关于CoroutineContext 本次不会深入,只需理解其意思即可。


    只需要知道:

    CoroutineContext 里存放着协程的分发器。


    协程有哪些分发器呢?


    Dispatchers.Main



    UI 线程,在Android里为主线程



    Dispatchers.IO



    IO 线程,主要执行IO 操作



    Dispatchers.Default



    主要执行CPU密集型操作,比如一些计算型任务



    Dispatchers.Unconfined



    不特意指定使用的线程



    指定协程在主线程运行


    不使用默认参数,指定协程的分发器:


        fun launch1() {
    GlobalScope.launch(Dispatchers.Main) {
    println("我在主线程执行")
    }
    }

    以此为例,继续分析其源码。

    上面提到过,开启协程使用coroutine.start(start, coroutine, block)函数:



    #AbstractCoroutine.kt
    fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
    //start 为CoroutineStart里的函数
    //最终会调用到invoke
    start(block, receiver, this)
    }
    #CoroutineStart.kt
    public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
    when (this) {
    //this 指的是StandaloneCoroutine,默认走default
    CoroutineStart.DEFAULT -> block.startCoroutineCancellable(receiver, completion)
    CoroutineStart.ATOMIC -> block.startCoroutine(receiver, completion)
    CoroutineStart.UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
    CoroutineStart.LAZY -> Unit // will start lazily
    }

    CoroutineStart.DEFAULT、CoroutineStart.ATOMIC 表示的是协程的启动方式,其中DEFAULT 表示立即启动,也是默认启动方式。


    接下来就是通过block去调用一系列的启动函数,这部分我们之前有详细分析过,此处再简单过一下:



    block 代表的是协程体,其实际编译结果为:匿名内部类,该类继承自SuspendLambda,而SuspendLambda 间接实现了Continuation 接口。



    继续看block的调用:


    #Cancellable.kt
    //block 的扩展函数
    internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(
    receiver: R, completion: Continuation<T>,
    onCancellation: ((cause: Throwable) -> Unit)? = null
    ) =
    //runSafely 为高阶函数,里边就是调用了"{}"里的内容
    runSafely(completion) {
    createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit), onCancellation)
    }

    流程流转到createCoroutineUnintercepted()函数了,在少年,你可知 Kotlin 协程最初的样子? 里有重点分析过:该函数是真正创建协程体的地方。


    直接上代码:


    #IntrinsicsJvm.kt
    actual fun <R, T> (suspend R.() -> T).createCoroutineUnintercepted(
    receiver: R,
    completion: Continuation<T>
    ): Continuation<Unit> {
    //包装completion
    val probeCompletion = probeCoroutineCreated(completion)
    return if (this is BaseContinuationImpl)
    //创建协程体类
    //receiver completion 皆为协程体对象 StandaloneCoroutine
    create(receiver, probeCompletion)
    else {
    createCoroutineFromSuspendFunction(probeCompletion) {
    (this as Function2<R, Continuation<T>, Any?>).invoke(receiver, it)
    }
    }
    }

    该函数的功能为创建一个协程体类,我们暂且称之为MyAnnoy。


    class MyAnnoy extends SuspendLambda implements Function2 {
    @Nullable
    @Override
    protected Object invokeSuspend(@NotNull Object o) {
    //...协程体逻辑
    return null;
    }
    @NotNull
    @Override
    public Continuation<Unit> create(@NotNull Continuation<?> completion) {
    //...创建MyAnnoy
    return null;
    }
    @Override
    public Object invoke(Object o, Object o2) {
    return null;
    }
    }

    新的MyAnnoy 创建完成后,调用intercepted(xx)函数,这个函数很关键:


    #Intrinsics.Jvm.kt
    public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
    //判断如果是ContinuationImpl,则转为ContinuationImpl 类型
    //继而调用intercepted()函数
    (this as? ContinuationImpl)?.intercepted() ?: this

    此处为什么要将MyAnnoy 转为ContinuationImpl ?

    因为它要调用ContinuationImpl里的intercepted() 函数:


    #ContinuationImpl.kt
    public fun intercepted(): Continuation<Any?> =
    intercepted
    //1、如果intercepted 为空则从context里取数据
    //2、如果context 取不到,则返回自身,最后给intercepted 赋值
    ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
    .also { intercepted = it }

    先看intercepted 变量类型:


    #ContinuationImpl.kt
    private var intercepted: Continuation<Any?>? = null

    还是Continuation 类型,初始时intercepted = null。

    context[ContinuationInterceptor] 表示从CoroutineContext里取出key 为ContinuationInterceptor 的Element。

    既然要取出,那么得要放进去的时候,啥时候放进去的呢?


    答案是:



    newCoroutineContext(context) 构造了新的CoroutineContext,里边存放了分发器。



    又因为我们设定的是在主线程进行分发:Dispatchers.Main,因此context[ContinuationInterceptor] 取出来的是Dispatchers.Main。


    Dispatchers.Main 定义:


    #Dispatchers.kt
    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
    #MainCoroutineDispatcher.kt
    public abstract class MainCoroutineDispatcher : CoroutineDispatcher() {}

    MainCoroutineDispatcher 继承自 CoroutineDispatcher,而它里边有个函数:


    #CoroutineDispatcher.kt
    public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
    DispatchedContinuation(this, continuation)

    而 Dispatchers.Main 调用的就是interceptContinuation(xx)函数。

    该函数入参为Continuation 类型,也就是MyAnnoy 对象,函数的内容很简单:




    • 构造DispatchedContinuation 对象,传入的参数分别是Dispatchers.Main和MyAnnoy 对象。

    • Dispatchers.Main、MyAnnoy 分别赋值给成员变量dispatcher和continuation。



    DispatchedContinuation 继承自DispatchedTask,它又继承自SchedulerTask,本质上就是Task,Task 实现了Runnable接口:


    #Tasks.kt
    internal abstract class Task(
    @JvmField var submissionTime: Long,
    @JvmField var taskContext: TaskContext
    ) : Runnable {
    //...
    }

    至此,我们重点关注其实现了Runnable接口里的run()函数即可。


    再回过头来看构造好DispatchedContinuation 之后,调用resumeCancellableWith()函数:


    #DispatchedContinuation.kt
    override fun resumeWith(result: Result<T>) {
    val context = continuation.context
    val state = result.toState()
    //需要分发
    if (dispatcher.isDispatchNeeded(context)) {
    _state = state
    resumeMode = MODE_ATOMIC
    //调用分发器分发
    dispatcher.dispatch(context, this)
    } else {
    executeUnconfined(state, MODE_ATOMIC) {
    withCoroutineContext(this.context, countOrElement) {
    continuation.resumeWith(result)
    }
    }
    }
    }

    而Demo里此处的dispatcher 即为Dispatchers.Main。


    好了,总结一下launch()函数的功能:



    image.png


    Dispatchers.Main 实现


    接着来看看Dispatchers.Main 如何分发任务的,先看其实现:


    #MainDispatcherLoader.java
    internal object MainDispatcherLoader {

    //默认true
    private val FAST_SERVICE_LOADER_ENABLED = systemProp(FAST_SERVICE_LOADER_PROPERTY_NAME, true)

    @JvmField
    val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()
    //构造主线程分发
    private fun loadMainDispatcher(): MainCoroutineDispatcher {
    return try {
    val factories = if (FAST_SERVICE_LOADER_ENABLED) {
    //加载分发器工厂①
    FastServiceLoader.loadMainDispatcherFactory()
    } else {
    ...
    }
    //通过工厂类,创建分发器②
    factories.maxByOrNull { it.loadPriority }?.tryCreateDispatcher(factories)
    ?: createMissingDispatcher()
    } catch (e: Throwable) {
    ...
    }
    }
    }

    先看①:


    #FastServiceLoader.kt
    internal fun loadMainDispatcherFactory(): List<MainDispatcherFactory> {
    val clz = MainDispatcherFactory::class.java
    //...
    return try {
    //反射构造工厂类:AndroidDispatcherFactory
    val result = ArrayList<MainDispatcherFactory>(2)
    FastServiceLoader.createInstanceOf(clz,
    "kotlinx.coroutines.android.AndroidDispatcherFactory")?.apply { result.add(this) }
    FastServiceLoader.createInstanceOf(clz,
    "kotlinx.coroutines.test.internal.TestMainDispatcherFactory")?.apply { result.add(this) }
    result
    } catch (e: Throwable) {
    //...
    }
    }

    该函数返回的工厂类为:AndroidDispatcherFactory。


    再看②,拿到工厂类后,就该用它来创建具体的实体了:


    #HandlerDispatcher.kt
    internal class AndroidDispatcherFactory : MainDispatcherFactory {
    //重写createDispatcher 函数,返回HandlerContext
    override fun createDispatcher(allFactories: List<MainDispatcherFactory>) =
    HandlerContext(Looper.getMainLooper().asHandler(async = true), "Main")
    //...
    }

    //定义
    internal class HandlerContext private constructor(
    private val handler: Handler,
    private val name: String?,
    private val invokeImmediately: Boolean
    ) : HandlerDispatcher(), Delay {
    }

    最终创建了HandlerContext。

    HandlerContext 继承自类:HandlerDispatcher


    #HandlerDispatcher.kt
    sealed class HandlerDispatcher : MainCoroutineDispatcher(), Delay {
    //重写分发函数
    override fun dispatch(context: CoroutineContext, block: Runnable) {
    //抛到主线程执行,handler为主线程的Handler
    handler.post(block)
    }
    }

    很明显了,DispatchedContinuation里借助dispatcher.dispatch()进行分发,而dispatcher 是Dispatchers.Main,最终的实现是HandlerContext。

    因此dispatch() 函数调用的是HandlerDispatcher.dispatch()函数,该函数里将block 抛到了主线程执行。

    block 为啥是呢?

    block 其实是DispatchedContinuation 对象,从上面的分析可知,它间接实现了Runnable 接口。

    查看其实现:


    #DispatchedTask.kt
    override fun run() {
    val taskContext = this.taskContext
    var fatalException: Throwable? = null
    try {
    //delegate 为DispatchedContinuation 本身
    val delegate = delegate as DispatchedContinuation<T>
    //delegate.continuation 为我们的协程体 MyAnnoy
    val continuation = delegate.continuation
    withContinuationContext(continuation, delegate.countOrElement) {
    val context = continuation.context
    //...
    val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null
    if (job != null && !job.isActive) {
    //...
    } else {
    if (exception != null) {
    continuation.resumeWithException(exception)
    } else {
    //执行协程体
    continuation.resume(getSuccessfulResult(state))
    }
    }
    }
    } catch (e: Throwable) {
    //...
    } finally {
    //...
    }
    }

    continuation 变量是我们的协程体:MyAnnoy。

    MyAnnoy.resume(xx) 这函数我们很熟了,再重新熟悉一下:


    #ContinuationImpl.kt
    override fun resumeWith(result: Result<Any?>) {
    // This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume
    var current = this
    var param = result
    while (true) {
    with(current) {
    //completion 即为开始时定义的StandaloneCoroutine
    val completion = completion!! // fail fast when trying to resume continuation without completion
    val outcome: Result<Any?> =
    try {
    //执行协程体里的代码
    val outcome = invokeSuspend(param)
    if (outcome === kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) return
    kotlin.Result.success(outcome)
    } catch (exception: Throwable) {
    kotlin.Result.failure(exception)
    }
    //...
    }
    }
    }

    invokeSuspend(param) 调用的是协程体里的代码,也就是launch 花括号里的内容,因此这里面的内容是主线程执行的。


    再来看看launch(Dispatchers.Main)函数执行步骤如下:




    1. 分发器HandlerContext 存储在CoroutineContext(协程上下文)里。

    2. 构造DispatchedContinuation 分发器,它持有变量dispatcher=HandlerContext,continuation=MyAnnoy。

    3. DispatchedContinuation 调用dispatcher(HandlerContext) 进行分发。

    4. HandlerContext 将Runnable(DispatchedContinuation) 抛到主线程。



    经过上面几步,launch(Dispatchers.Main) 任务算是完成了,至于Runnable什么时候执行与它无关了。


    当Runnable 在主线程被执行后,从DispatchedContinuation 里取出continuation(MyAnnoy),并调用continuation.resume()函数,进而执行MyAnnoy.invokeSuspend()函数,最后执行了launch{}协程体里的内容。

    于是协程就愉快地在主线程执行了。


    老规矩,结合代码与函数调用图:



    image.png


    3. 协程恢复时线程的选择


    以主线程为例,我们知道了协程指定线程运行的原理。

    想象另一种场景:



    在协程里切换了子线程执行,子线程执行完毕后还会回到主线程执行吗?



    对上述Demo进行改造:


        fun launch2() {
    GlobalScope.launch(Dispatchers.Main) {
    println("我在主线程执行")
    withContext(Dispatchers.IO) {
    println("我在子线程执行")//②
    }
    println("我在哪个线程执行?")//③
    }
    }

    大家先猜猜③ 的答案是什么?是主线程还是子线程?


    withContext(xx)函数上篇(讲真,Kotlin 协程的挂起没那么神秘(原理篇))已经深入分析过了,它是挂起函数,主要作用:



    切换线程执行协程。




    image.png


    MyAnnoy1 对应协程体1,为父协程体。

    MyAnnoy2 对应协程体2,为子协程体。

    当② 执行完成后,会切换到父协程执行,我们看看切换父协程的流程。

    每个协程的执行都要经历下面这个函数:


    #BaseContinuationImpl.kt
    override fun resumeWith(result: Result<Any?>) {
    //...
    while (true) {
    //..
    with(current) {
    val completion = completion!! // fail fast when trying to resume continuation without completion
    val outcome: Result<Any?> =
    try {
    //执行协程体
    val outcome = invokeSuspend(param)
    if (outcome === kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) return
    kotlin.Result.success(outcome)
    } catch (exception: Throwable) {
    kotlin.Result.failure(exception)
    }
    releaseIntercepted() // this state machine instance is terminating
    if (completion is BaseContinuationImpl) {
    //...
    } else {
    //如果上一步的协程体不阻塞,则执行completion
    completion.resumeWith(outcome)
    return
    }
    }
    }
    }

    此处以withContext(xx)函数协程体执行为例,它的completion 为何物?

    上面提到过launch()开启协程时,它的协程体的completion 为StandaloneCoroutine,也就是说MyAnnoy1.completion = StandaloneCoroutine。

    从withContext(xx)源码里得知,它的completion 为DispatchedCoroutine,DispatchedCoroutine,它继承自ScopeCoroutine,ScopeCoroutine 有个成员变量为:uCont: Continuation。

    当构造DispatchedCoroutine 时,传入的协程体赋值给uCont。
    也就是DispatchedCoroutine.uCont = MyAnnoy1,MyAnnoy2.completion = DispatchedCoroutine。



    此时,子协程体与父协程 通过DispatchedCoroutine 关联起来了。



    因此completion.resumeWith(outcome)==DispatchedCoroutine.resumeWith(outcome)。
    直接查看 后者实现即可:


    #AbstractCoroutine.kt
    public final override fun resumeWith(result: Result<T>) {
    val state = makeCompletingOnce(result.toState())
    if (state === COMPLETING_WAITING_CHILDREN) return
    afterResume(state)
    }

    #Builders.common.kt
    #DispatchedCoroutine 类里
    override fun afterResume(state: Any?) {
    //uCont 为父协程体
    uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont))
    }

    到此就豁然开朗了,uCont.intercepted() 找到它的拦截器,因为uCont为MyAnnoy1,它的拦截器就是HandlerContext,又来了一次抛回到主线程执行。


    因此,上面Demo里③ 的答案是:



    它在主线程执行。



    小结来看,就两步:




    1. 父协程在主线程执行,中途遇到挂起的方法切换到子线程(子协程)执行。

    2. 当子协程执行完毕后,找到父协程的协程体,继续让其按照原有规则分发。



    老规矩,有代码有图有真相:



    image.png


    至此,切换到主线程执行的原理已经分析完毕。


    好奇的小伙伴可能会问:你这举例都是子线程往主线程切换,若是子线程往子线程切换呢?

    往主线程切换依靠Handler,而子线程切换依赖线程池,这块内容较多,单独拎出来分析。

    既然都提到这个点了,那这里再提一个问题:


        fun launch3() {
    GlobalScope.launch(Dispatchers.IO) {
    withContext(Dispatchers.Default) {
    println("我在哪个线程运行")
    delay(2000)
    println("delay 后我在哪个线程运行")
    }
    println("我又在哪个线程运行")
    }
    }

    你知道上面的答案吗?


    我们下篇将重点分析协程线程池的调度原理,通过它你将会知道上面的答案。


    本文基于Kotlin 1.5.3,文中完整Demo请点击


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

    掘金x得物公开课 - Flutter 3.0下的混合开发演进

    hello 大家好,我是《Flutter 开发实战详解》的作者,Github GSY 项目的负责人郭树煜,同时也是今年新晋的 Flutter GDE,借着本次 Google I/O 之后发布的 Flutter 3.0,来和大家聊一聊 Flutter 里混合开...
    继续阅读 »

    hello 大家好,我是《Flutter 开发实战详解》的作者,Github GSY 项目的负责人郭树煜,同时也是今年新晋的 Flutter GDE,借着本次 Google I/O 之后发布的 Flutter 3.0,来和大家聊一聊 Flutter 里混合开发的技术演进。


    为什么混合开发在 Flutter 里是特殊的存在?因为它渲染的控件是通过 Skia 直接和 GPU 交互,也就是说 Flutter 控件和平台无关,甚至连 UI 绘制线程都和原生平台 UI 线程是相互独立,所以甚至于 Flutter 在诞生之初都不支持和原生平台的控件进行混合开发,也就是不支持 WebView ,这就成了当时最大的缺陷之一


    其实从渲染的角度看 Flutter 更像是一个 2D 游戏引擎,事实上 Flutter 在这次 Google I/O 也分享了基于 Flutter 的游戏开发 ToolKit 和第三方工具包 Flame ,如图所示就是本次 Google I/O 发布的 Pinball 小游戏,所以从这些角度上看都可以看出 Flutter 在混合开发的特殊性。



    如果说的更形象简单一点,那就是如何把原生控件渲染到 WebView



    TT


    最初的社区支持


    不支持 WebView 在最初可以说是 Flutter 最大的痛点之一,所以在这样窘迫的情况下,社区里涌现出一些临时的解决方法,比如 flutter_webview_plugin


    类似 flutter_webview_plugin 的出现,解决了当时大部分时候 App 里打开一个网页的简单需求,如下图所示,它的思路就是:



    在 Flutter 层面放一个占位控件提供大小,然后原生层在同样的位置把 WebView 添加进去,从而达到看起来把 WebView 集成进去的效果,这个思路在后续也一直被沿用



    image-20220625170833702


    这样的实现方式无疑成本最低速度最快,但是也带来了很多的局限性


    相信大家也能想到,因为 Flutter 的所有控件都是渲染一个 FlutterView 上,也就是从原生的角度其实是一个单页面的效果,所以这种脱离 Flutter 渲染树的添加控件的方法,无疑是没办法和 Flutter 融合到一起,举个例子:



    • 如图一所示,从 Flutter 页面跳到 Native 页面的时候,打开动画无法同步,因为 AppBar 是 Flutter 的,而 Native 是原生层,它们不在同一个渲染树内,所以无法实现同步的动画效果

    • 如图二所示,比如在打开 Native 页面之后,通过 Appbar 再打开一个黄色的 Bottm Sheet ,可以看到此时黄色的 Bottm Sheet 打开了,但是却被 Native 遮挡住(Demo 里给 Native 设置了透明色),因为 Flutter 的 Bottm Sheet 是被渲染在 FlutterView 里面,而 Native UI 把 FlutterView 挡住了,所以新的 Flutter UI 自然也被遮挡

    • 如图三所示,当我们通过 reload 重刷 Flutter UI 之后,可以看到 Flutter 得 UI 都被重置了,但是此时 Native UI 还在,因为此时已经没有返回按键之类的无法关闭,这也是这种集成方式一不小心就影响开发的问题

    • 如图四通过 iOS 上的 debug 图层,我们可以更形象地看到这种方式的实现逻辑和堆叠效果



















    动画不同步页面被挡reload 之后iOS
    11111111222222222333333image-20220616142126589

    PlatformView


    随着 Flutter 的发展,官方支持混合开发势在必行,所以第一代 PlatformView 的支持还是诞生了,但是由于 Android 和 iOS 平台特性的不同,最初Android 的 AndroidView 和 iOS 的 UIKitView 实现逻辑相差甚远,以至于后面 Flutter 的 PlatformView 的每次大调整都是围绕于 Android 在做优化


    Android


    最初 Flutter 在 Android 上对 PlatformView 的支持是通过 VirtualDisplay 实现,VirtualDisplay 类似于一个虚拟显示区域,需要结合 DisplayManager 一起调用,VirtualDisplay 一般在副屏显示或者录屏场景下会用到,而在 Flutter 里 VirtualDisplay 会将虚拟显示区域的内容渲染在一个内存 Surface上。


    在 Flutter 中通过将 AndroidView 需要渲染的内容绘制到 VirtualDisplays 中 ,然后通过 textureId 在 VirtualDisplay 对应的内存中提取绘制的纹理, 简单看实现逻辑如下图所示:


    image-20220626151538054



    这里其实也是类似于最初社区支持的模式:通过在 Dart 层提供一个 AndroidView ,从而获取到控件所需的大小,位置等参数,当然这里多了一个 textureId ,这个 id 主要是提交给 Flutter Engine ,通过 id Flutter 就可以在渲染时将画面从内存里提出出来。



    iOS


    在 iOS 平台上就不使用类似 VirtualDisplay 的方法,而是通过将 Flutter UI 分为两个透明纹理来完成组合,这种方式无疑更符合 Flutter 社区的理念,这样的好处是:



    需要在 PlatformView 下方呈现的 Flutter UI 可以被绘制到其下方的纹理;而需要在 PlatformView 上方呈现的 Flutter UI 可以被绘制到其上方的纹理, 它们只需要在最后组合起来就可以了。



    是不是有点抽象?


    简单看下面这张图,其实就是通过在 NativeView 的不同层级设置不同的透明图层,然后把不同位置的控件渲染到不同图层,最终达到组合起来的效果。


    image-20220626151526444


    那明明这种方法更好,为什么 Android 不一开始也这样实现呢?


    因为当时在实现思路上, VirtualDisplay 的实现模式并不支持这种模式,因为在 iOS 上框架渲染后系统会有回调通知,例如:当 iOS 视图向下移动 2px 时,我们也可以将其列表中的所有其他 Flutter 控件也向下渲染 2px


    但是在 Android 上就没有任何有关的系统 API,因此无法实现同步输出的渲染。如果强行以这种方式在 Android 上使用,最终将产生很多如 AndroidView 与 Flutter UI 不同步的问题


    问题


    事实上 VirtualDisplay 的实现方式也带来和很多问题,简单说两个大家最直观的体会:


    触摸事件


    因为控件是被渲染在内存里,虽然你在 UI 上看到它就在那里,但是事实上它并不在那里,你点击到的是 FlutterView ,所以用户产生的触摸事件是直接发送到 FlutterView


    所以触摸事件需要在 FlutterView 到 Dart ,再从 Dart 转发到原生,然后如果原生不处理又要转发回 Flutter ,如果中间还存在其他派生视图,事件就很容易出现丢失和无法响应,而这个过程对于 FlutterView 来说,在原生层它只有一个 View 。


    所以 Android 的 MotionEvent 在转化到 Flutter 过程中可能会因为机制的不同,存在某些信息没办法完整转化的丢失。


    文字输入


    一般情况下 AndroidView 是无法获取到文本输入,因为 VirtualDisplay 所在的内存位置会始终被认为是 unfocused 的状态



    InputConnectionsunfocused 的 View 中通常是会被丢弃。



    所以 Flutter 重写了 checkInputConnectionProxy 方法,这样 Android 会认为 FlutterView 是作为 AndroidView 和输入法编辑器(IME)的代理,这样 Android 就可以从 FlutterView 中获取到 InputConnections 然后作用于 AndroidView 上面。



    在 Android Q 开始又因为非全局的 InputMethodManager 需要新的兼容



    当然还有诸如性能等其他问题,但是至少先有了支持,有了开始才会有后续的进阶,在 Flutter 3.0 之前, VirtualDisplay 一直默默在 PlatformView 的背后耕耘。


    HybridComposition


    时间来到 Flutter 1.2,Hybrid Composition 是在 Flutter 1.2 时发布的 Android 混合开发实现,它使用了类似 iOS 的实现思路,提供了 Flutter 在 Android 上的另外一种 PlatformView 的实现。


    如下图是在 Dart 层使用 VirtualDisplay 切换到 HybridComposition 模式的区别,最直观的感受应该是需要写的 Dart 代码变多了。


    111111


    但是其实 HybridComposition 的实现逻辑是变简单了: PlatformView 是通过 FlutterMutatorView 把原生控件 addViewFlutterView 上,然后再通过 FlutterImageView 的能力去实现图层的混合



    又懵了?不怕,马上你就懂了



    简单来说就是 HybridComposition 模式会直接把原生控件通过 addView 添加到 FlutterView 上 。这时候大家可能会说,咦~这不是和最初的实现一样吗?怎么逻辑又回去了



    其实确实是社区的进阶版实现,Flutter 直接通过原生的 addView 方法将 PlatformView 添加到 FlutterView 里,而当你还需要在 PlatformView 上渲染 Flutter 自己的 Widget 时,Flutter 就会通过再叠加一个 FlutterImageView 来承载这个 Widget 的纹理。



    举一个简单的例子,如下图所示,一个原生的 TextView 被通过 HybridComposition 模式接入到 Flutter 里(NativeView),而在 Android 的显示布局边界和 Layout Inspector 上可以清晰看到: 灰色 TextView 通过 FlutterMutatorView 被添加到 FlutterView 上被直接显示出来


    image-20220618152055492


    所以在 HybridCompositionTextView 是直接在原生代码上被 add 到 FlutterView 上,而不是提取纹理


    那如果我们看一个复杂一点的案例,如下图所示,其中蓝色的文本是原生的 TextView ,红色的文本是 Flutter 的 Text 控件,在中间 Layout Inspector 的 3D 图层下可以清晰看到:



    • 两个蓝色的 TextView 是通过 FlutterMutatorView 被添加在 FlutterView 之上,并且把没有背景色的红色 RE 遮挡住了

    • 最顶部有背景色的红色 RE 也是 Flutter 控件,但是因为它需要渲染到 TextView 之上,所以这时候多一个 FlutterImageView ,它用于承载需要显示在 Native 控件之上的纹理,从而达 Flutter 控件“真正”和原生控件混合堆叠的效果。


    image-20220616165047353


    可以看到 Hybrid Composition 上这种实现,能更原汁原味地保流下原生控件的事件和特性,因为从原生角度看它就是原生层面的物理堆叠,需要都一个层级就多加一个 FlutterImageView ,同一个层级的 Flutter 控件共享一个 FlutterImageView


    当然,在 HybridCompositionFlutterImageView 也是一个很有故事的对象,由于篇幅原因这里就不详细展开,这里大家可以简单看这张图感受下,也就是在有 PlatformView 和没有 PlatformView 是,Flutter 的渲染会有一个转化的过程,而在这个变化过程,在 Flutter 3.0 之前可以通过 PlatformViewsService.synchronizeToNativeViewHierarchy(false); 取消


    image-20220618153757996


    最后,Hybrid Composition 也不少问题,比如上面的转化就是为了解决动画同步问题,当然这个行为也会产生一些性能开销,例如:



    在 Android 10 之前, Hybrid Composition 需要将内存中的每个 Flutter 绘制的帧数据复制到主内存,之后再从 GPU 渲染复制回来 ,所以也会导致 Hybrid Composition 在 Android 10 之前的性能表现更差,例如在滚动列表里每个 Item 嵌套一个 Hybrid CompositionPlatformView ,就可能会变卡顿甚至闪烁。



    其他还有线程同步,闪烁等问题,由于篇幅就不详细展开,如果感兴趣的可以详细看我之前发布过的 《Flutter 深入探索混合开发的技术演进》


    TextureLayer


    随着 Flutter 3.0 的发布,第一代 PlatformView 的实现 VirtualDisplay 被新的 TextureLayer 所替代,如下图所示,简单对比 VirtualDisplayTextureLayer 的实现差异,可以看到主要还是在于原生控件纹理的提取方式上


    image-20220618154327890


    从上图我们可以得知:



    • VirtualDisplayTextureLayerPlugin 的实现是可以无缝切换,因为主要修改的地方在于底层对于纹理的提取和渲染逻辑

    • 以前 Flutter 中会将 AndroidView 需要渲染的内容绘制到 VirtualDisplays ,然后在 VirtualDisplay 对应的内存中,绘制的画面就可以通过其 Surface 获取得到;现在 AndroidView 需要的内容,会通过 View 的 draw 方法被绘制到 SurfaceTexture 里,然后同样通过 TextureId 获取绘制在内存的纹理


    是不是又有点蒙?简单说就是不需要绘制到副屏里,现在直接通过 override Viewdraw 方法就可以了。


    TextureLayer 的实现里,同样是需要把控件添加到一个 PlatformViewWrapper 的原生布局控件里,但是这个控件通过 override 了 Viewdraw 方法,把原本的 Canvas 替换成 SurfaceTexture 在内存的 Canvas ,所以 PlatformViewWrapper 的 child 会把控件绘制到内存的 SurfaceTexture 上。



    举个例子,还是之前的代码,如下图所示,这时候通过 TextureLayer 模式运行之后,通过 Layout Inspector 的 3D 图层可以看到,两个原生的 TextView 通过 PlatformViewWrapper 被添加到 FlutterView 上。


    但是不同的是,在 3D 图层里看不到 TextView 的内容,因为绘制 TextView 的 Canvas 被替换了,所以 TextView 的内容被绘制到内存的 Surface 上,最终会在渲染时同步 Flutter Engine 里。



    看到这里,你可能也发现了,这时候因为有 PlatformViewWrapper 的存在,点击会被 PlatformViewWrapper 内部拦截,从而也解决了触摸的问题, 而这里刚好有人提了一个问题,如下图所示:



    "从图 1 Layout Inspector 看, PlatformWrapperView 是在 FlutterSurfaceView 上方,为什么如图 2 所示,点击 Flutter button 却可以不触发 native button的点击效果?"。
















    图1图2
    image.pngimg

    思考一下,因为最直观的感受:点击不都是被 PlatformViewWrapper 拦截了吗?明明 PlatformViewWrapper 是在 FlutterSurfaceView 之上,为什么 FlutterSurfaceView 里的 FlutterButton 还能被点击到


    这里简单解释一下:



    • 1、首先那个 Button 并不是真的被摆放在那里,而是通过 PlatformViewWrappersuper.draw绘制到 surface 上的,所以在那里的是 PlatformViewWrapper ,而不是 Button ,Button 的内容已经变成纹理去到了 FlutterSurfaceView 里面

    • 2、 PlatformViewWrapper 里重写了 onInterceptTouchEvent 做了拦截onInterceptTouchEvent 这个事件是从父控件开始往子控件传,因为拦截了所以不会让 Button 直接响应,然后在 PlatformViewWrapperonTouchEvent 响应里是做了点击区域的分发,响应会分发到了 AndroidTouchProcessor 之后,会打包发到 _unpackPointerDataPacket 进入 Dart

    • 3、 在 Dart 层的点击区域,如果没有 Flutter 控件响应,会是 _PlatformViewGestureRecognizer-> updateGestureRecognizers -> dispatchPointerEvent -> sendMotionEvent 又发送回原生层

    • 4、回到原生 PlatformViewsControllercreateForTextureLayer 里的 onTouch ,执行 view.dispatchTouchEvent(event);


    image-20220625171101069


    总结起来就是:**PlatfromViewWrapper 拦截了 Event ,通过 Dart 做二次分发响应,从而实现不同的事件响应 ** ,它和 VirtualDisplay 的不同是, VirtualDisplay 的事件响应都是在 FlutterView 上,但是TextureLayout 模式,是有独立的原生 PlatfromViewWrapper 控件来开始,所以区域效果和一致性会更好。


    问题


    最后这里还需要提个醒,如果你之前使用的插件使用的是 HybirdComposition ,但是没做兼容,也就是使用的还是 PlatformViewsService.initSurfaceAndroidView 的话,它也会切换成 TextureLayer 的逻辑,所以你需要切换为 PlatformViewsService.initExpensiveAndroidView ,才能继续使用原本 HybirdComposition 的效果



    ⚠️我也比较奇怪为什么 Flutter 3.0 没有提及 Android 这个 breaking change ,因为对于开发来说其实是无感的,不小心就掉坑里。



    那你说为什么还要 HybirdComposition


    前面我们说过, TextureLayer 是通过在 super.draw 替换 Canvas 的方法去实现绘制,但是它替换不了 Surface 里的一些 Canvas ,所以比如一些需要 SurfaceViewTextureView 或者有自己内部特殊 Canvas 的场景,你还是需要 HybirdComposition ,只不过可能会和官方新的 API 名字一样,它 Expensive 。


    Expensive 是因为在 Flutter 3.0 正式版开始,FlutterView 在使用 HybirdComposition 时一定会 converted to FlutterImageView ,这也是 Flutter 3.0 下一个需要注意的点。


    image-20220616170253242



    更多内容可见 《Flutter 3.0 之 PlatformView :告别 VirtualDisplay ,拥抱 TextureLayer》



    image-20220625164049356


    最后


    最后做个总结,可以看到 Flutter 为了混合开发做了很多的努力,特别是在 Android 上,也是因为历史埋坑的原因,由于时间关系这里没办法都详细介绍,但是相信本次之后大家对 Flutter 的 PlatformView 实现都有了全面的了解,这对大家在未来使用 Flutter 也会有很好的帮助,如果你还有什么问题,欢迎交流。


    image-20220626151444011


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

    少年,你可知 Kotlin 协程最初的样子?

    如果有人问你,怎么开启一个 Kotlin 协程?你可能会说通过runBlocking/launch/async,回答没错,这几个函数都能开启协程。不过这次咱们换个角度分析,通过提取这几个函数的共性,看看他们内部是怎么开启一个协程的。相信通过本篇,你将对协程原理...
    继续阅读 »

    如果有人问你,怎么开启一个 Kotlin 协程?你可能会说通过runBlocking/launch/async,回答没错,这几个函数都能开启协程。不过这次咱们换个角度分析,通过提取这几个函数的共性,看看他们内部是怎么开启一个协程的。
    相信通过本篇,你将对协程原理有个深刻的认识。
    文章目录:

    1、suspend 关键字背后的原理
    2、如何开启一个原始的协程?
    3、协程调用以及整体流程
    4、协程代码我为啥看不懂?

    1、suspend 关键字背后的原理

    suspend 修饰函数

    普通的函数

    fun launchEmpty(block: () -> Unit) {   
    }

    定义一个函数,形参为函数类型。
    查看反编译结果:

    public final class CoroutineRawKt {
    public static final void launchEmpty(@NotNull Function0 block) {
    }
    }

    可以看出,在JVM 平台函数类型参数最终是用匿名内部类表示的,而FunctionX(X=0~22) 是Kotlin 将函数类型映射为Java 的接口。
    来看看Function0 的定义:

    public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
    }

    有一个唯一的方法:invoke(),它没有任何参数。
    可作如下调用:

    fun launchEmpty(block: () -> Unit) {
    block()//与block.invoke()等价
    }
    fun main(array: Array<String>) {
    launchEmpty {
    println("I am empty")
    }
    }

    带suspend 的函数

    以上写法大家都比较熟悉了,就是典型的高阶函数的定义和调用。
    现在来改造一下函数类型的修饰符:

    fun launchEmpty1(block: suspend () -> Unit) {
    }

    相较之前,加了"suspend"关键字。
    老规矩,查看反编译结果:

    public static final void launchEmpty1(@NotNull Function1 block) {
    }

    参数从Function0 变为了Function1:

    /** A function that takes 1 argument. */
    public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
    }

    Function1 的invoke()函数多了一个入参。

    也就是说,加了suspend 修饰后,函数会默认加个形参。

    当我们调用suspend修饰的函数时:

    image.png

    意思是:

    "suspend"修饰的函数只能在协程里被调用或者是在另一个被"suspend"修饰的函数里调用。

    suspend 作用

    何为挂起

    suspend 意为挂起、阻塞的意思,与协程相关。
    当suspend 修饰函数时,表明这个函数可能会被挂起,至于是否被挂起取决于该函数里是否有挂起动作。 比如:

    suspend fun testSuspend() {
    println("test suspend")
    }

    这样的写法没意义,因为函数没有实现挂起功能。
    你可能会说,挂起需要切换线程,好嘛,换个写法:

    suspend fun testSuspend() {
    println("test suspend")
    thread {
    println("test suspend in thread")
    }
    }

    然而并没啥用,编译器依然提示:

    image.png

    意思是可以不用suspend 修饰,没啥意义。

    挂起于协程的意义

    第一点
    当函数被suspend 修饰时,表明协程执行到此可能会被挂起,若是被挂起那么意味着协程将无法再继续往下执行,直到条件满足恢复了协程的运行。

    fun main(array: Array<String>) {
    GlobalScope.launch {
    println("before suspend")//①
    testSuspend()//挂起函数②
    println("after suspend")//③
    }
    }

    执行到②时,协程被挂起,将不会执行③,直到协程被恢复后才会执行③。
    注:关于协程挂起的生动理解&线程的挂起 下篇将着重分析。

    第二点
    如果将suspend 修饰的函数类型看做一个整体的话:

    suspend () -> T

    无参,返回值为泛型。
    Kotlin 里定义了一些扩展函数,可用来开启协程。

    第三点 suspend 修饰的函数类型,当调用者实现其函数体时,传入的实参将会继承自SuspendLambda(这块下个小结详细分析)。

    2、如何开启一个原始的协程?

    ##launch/async/runBlocking 如何开启协程
    纵观这几种主流的开启协程方式,它们最终都会调用到:

    #CoroutineStart.kt
    public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
    when (this) {
    DEFAULT -> block.startCoroutineCancellable(receiver, completion)
    ATOMIC -> block.startCoroutine(receiver, completion)
    UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
    LAZY -> Unit // will start lazily
    }

    无论走哪个分支,都是调用block的函数,而block 就是我们之前说的被suspend 修饰的函数。
    以DEFAULT 为例startCoroutineUndispatched接下来会调用到IntrinsicsJvm.kt里的:

    #IntrinsicsJvm.kt
    public actual fun <R, T> (suspend R.() -> T).createCoroutineUnintercepted(
    receiver: R,
    completion: Continuation<T>
    )

    该函数带了俩参数,其中的receiver 为接收者,而completion 为协程结束后调用的回调。
    为了简单,我们可以省略掉receiver。
    刚好IntrinsicsJvm.kt 里还有另一个函数:

    #IntrinsicsJvm.kt
    public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
    completion: Continuation<T>
    ): Continuation<Unit>

    createCoroutineUnintercepted 为 (suspend () -> T) 类型的扩展函数,因此只要我们的变量为 (suspend () -> T)类型就可以调用createCoroutineUnintercepted(xx)函数。
    查找该函数的使用之处,发现Continuation.kt 文件里不少扩展函数都调用了它。
    如:

    #Continuation.kt
    //创建协程的函数
    public fun <T> (suspend () -> T).createCoroutine(
    completion: Continuation<T>
    ): Continuation<Unit> =
    SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)

    其中Continuation 为接口:

    #Continuation.kt
    interface Continuation<in T> {
    //协程上下文
    public val context: CoroutineContext
    //恢复协程
    public fun resumeWith(result: Result<T>)
    }

    Continuation 接口很重要,协程里大部分的类都实现了该接口,通常直译过来为:"续体"。

    创建完成后,还需要开启协程函数:

    #Continuation.kt
    //启动协程的函数
    public inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))

    简单创建/调用协程

    协程创建

    由上分析可知,Continuation.kt 里有我们开启协程所需要的一些基本信息,接着来看看如何调用上述函数。

    fun <T> launchFish(block: suspend () -> T) {
    //创建协程,返回值为SafeContinuation(实现了Continuation 接口)
    //入参为Continuation 类型,参数名为completion,顾名思义就是
    //协程结束后(正常返回&抛出异常)将会调用它。
    var coroutine = block.createCoroutine(object : Continuation<T> {
    override val context: CoroutineContext
    get() = EmptyCoroutineContext

    //协程结束后调用该函数
    override fun resumeWith(result: Result<T>) {
    println("result:$result")
    }
    })
    //开启协程
    coroutine.resume(Unit)
    }

    定义了函数launchFish,该函数唯一的参数为函数类型参数,被suspend 修饰,而(suspend () -> T)定义一系列扩展函数,createCoroutine 为其中之一,因此block 可以调用createCoroutine。
    createCoroutine 返回类型为SafeContinuation,通过SafeContinuation.resume()开启协程。

    协程调用

    fun main(array: Array<String>) {
    launchFish {
    println("I am coroutine")
    }
    }

    打印结果:

    image.png

    3、协程调用以及整体流程

    协程调用背后的玄机

    反编译初窥门径

    看到上面的打印大家可能比较晕,"println("I am coroutine")"是咋就被调用的?没看到有调用它的地方啊。
    launchFish(block) 接收的是函数类型,当调用launchFish 时,在闭包里实现该函数的函数体即可,我们知道函数类型最终会替换为匿名内部类。
    因为kotlin 有不少语法糖,无法一下子直击本质,老规矩,反编译看看结果:

        public static final void main(@NotNull String[] array) {
    launchFish((Function1)(new Function1((Continuation)null) {
    int label;

    @Nullable
    public final Object invokeSuspend(@NotNull Object var1) {
    Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
    switch(this.label) {
    case 0:
    //闭包里的内容
    String var2 = "I am coroutine";
    boolean var3 = false;
    //打印
    System.out.println(var2);
    return Unit.INSTANCE;
    }
    }

    @NotNull
    public final Continuation create(@NotNull Continuation completion) {
    //创建一个Continuation,可以认为是续体
    Function1 var2 = new <anonymous constructor>(completion);
    return var2;
    }

    public final Object invoke(Object var1) {
    //Function1 接口里的方法
    return ((<undefinedtype>)this.create((Continuation)var1)).invokeSuspend(Unit.INSTANCE);
    }
    }));
    }

    为了更直观,删除了一些不必要的信息。
    看到这,你发现了什么?通常传入函数类型的实参最后将会被编译为对应的匿名内部类,此时应该编译为Function1, 实现其唯一的函数:invoke(xx),而我们发现实际上还多了两个函数:invokeSuspend(xx)与create(xx)
    我们有理由相信,invokeSuspend(xx)函数一定在某个地方被调用了,原因是:闭包里打印的字符串:"I am coroutine" 只在该函数里实现,而我们测试的结果是这个打印执行了。
    还记得我们上面说的suspend 意义的第三点吗?

    suspend 修饰的函数类型,其实参是匿名内部类,继承自抽象类:SuspendLambda。

    也就是说invokeSuspend(xx)与create(xx) 的定义很有可能来自SuspendLambda,我们接着来分析它。

    SuspendLambda 关系链

    #ContinuationImpl.kt
    internal abstract class SuspendLambda(
    public override val arity: Int,
    completion: Continuation<Any?>?
    ) : ContinuationImpl(completion), FunctionBase<Any?>, SuspendFunction {
    constructor(arity: Int) : this(arity, null)
    ...
    }

    该类本身并没有太多内容,此处继承了ContinuationImpl类,查看该类也没啥特殊的,继续往上查找,找到BaseContinuationImpl类,在里面发现了线索:

    #ContinuationImpl.kt
    internal abstract class BaseContinuationImpl(
    val completion: Continuation<Any?>?
    ) : Continuation<Any?>, CoroutineStackFrame, Serializable {
    protected abstract fun invokeSuspend(result: Result<Any?>): Any?
    open fun create(completion: Continuation<*>): Continuation<Unit> {
    }
    }

    终于看到了眼熟的:invokeSuspend(xx)与create(xx)。
    我们再回过头来捋一下类之间关系:

    image.png

    闭包生成的匿名内部类:

    • 实现了Function1 接口,并实现了该接口里的invoke函数。
    • 继承了SuspendLambda,并重写了invokeSuspend函数和create函数。

    你可能会说还不够直观,那好,继续改写一下:

        class MyAnonymous extends SuspendLambda implements Function1 {
    int label;
    public final Object invokeSuspend(@NotNull Object var1) {
    Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
    switch(this.label) {
    case 0:
    String var2 = "I am coroutine";
    boolean var3 = false;
    System.out.println(var2);
    return Unit.INSTANCE;
    }
    }
    public final Continuation create(@NotNull Continuation completion) {
    Intrinsics.checkNotNullParameter(completion, "completion");
    Function1 var2 = new <anonymous constructor>(completion);
    return var2;
    }
    public final Object invoke(Object var1) {
    return ((<undefinedtype>)this.create((Continuation)var1)).invokeSuspend(Unit.INSTANCE);
    }
    }

    public static final void launchFish(@NotNull MyAnonymous block) {
    Continuation coroutine = ContinuationKt.createCoroutine(block, (new Continuation() {
    @NotNull
    public CoroutineContext getContext() {
    return (CoroutineContext) EmptyCoroutineContext.INSTANCE;
    }

    public void resumeWith(@NotNull Object result) {
    String var2 = "result:" + Result.toString-impl(result);
    boolean var3 = false;
    System.out.println(var2);
    }
    }));
    //开启
    coroutine.resumeWith(Result.constructor-impl(var3));
    }

    public static final void main(@NotNull String[] array) {
    MyAnonymous myAnonymous = new MyAnonymous();
    launchFish(myAnonymous);
    }

    这么看就比较清晰了,此处我们单独声明了一个MyAnonymous类,并构造对象传递给launchFish函数。

    闭包的执行

    既然匿名类的构造清晰了,接下来分析闭包是如何被执行的,也就是查找invokeSuspend(xx)函数是怎么被调用的?
    将目光转移到launchFish 函数本身。

    createCoroutine()
    先看createCoroutine()函数调用,直接上代码:

    #Continuation.kt
    fun <T> (suspend () -> T).createCoroutine(
    completion: Continuation<T>
    ): Continuation<Unit> =
    //返回SafeContinuation 对象
    //SafeContinuation 构造函数需要2个参数,一个是delegate,另一个是协程状态
    //此处默认是挂起
    SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)

    #IntrinsicsJvm.kt
    actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
    completion: Continuation<T>
    ): Continuation<Unit> {
    val probeCompletion = probeCoroutineCreated(completion)
    return if (this is BaseContinuationImpl)
    //此处的this 即为匿名内部类对象 MyAnonymous,它间接继承了BaseContinuationImpl
    //调用MyAnonymous 重写的create 函数
    //create 函数里new 新的MyAnonymous 对象
    create(probeCompletion)
    else
    createCoroutineFromSuspendFunction(probeCompletion) {
    (this as Function1<Continuation<T>, Any?>).invoke(it)
    }
    }

    #IntrinsicsJvm.kt
    public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
    //判断是否是ContinuationImpl 类型的Continuation
    //我们的demo里是true,因此会继续尝试调用拦截器
    (this as? ContinuationImpl)?.intercepted() ?: this

    #ContinuationImpl.kt
    public fun intercepted(): Continuation<Any?> =
    //查看是否已经有拦截器,如果没有,则从上下文里找,上下文没有,则用自身,最后赋值。
    //在我们的demo里上下文里没有,用的是自身
    intercepted
    ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
    .also { intercepted = it }

    最后得出的Continuation 赋值给SafeContinuation 的成员变量:delegate。
    至此,SafeContinuation 对象已经构造完毕,接着继续看如何用它开启协程。

    再看 resume()

    #SafeContinuationJvm.kt
    actual override fun resumeWith(result: Result<T>) {
    while (true) { // lock-free loop
    val cur = this.result // atomic read
    when {
    //初始化状态为UNDECIDED,因此直接return
    cur === CoroutineSingletons.UNDECIDED -> if (SafeContinuation.RESULT.compareAndSet(this,
    CoroutineSingletons.UNDECIDED, result.value)) return
    //如果是挂起,将它变为恢复状态,并调用恢复函数
    //demo 里初始化状态为COROUTINE_SUSPENDED,因此会走到这
    cur === COROUTINE_SUSPENDED -> if (SafeContinuation.RESULT.compareAndSet(this, COROUTINE_SUSPENDED,
    CoroutineSingletons.RESUMED)) {
    //delegate 为之前创建的Continuation,demo 里因为没有拦截,因此为MyAnonymous
    delegate.resumeWith(result)
    return
    }
    else -> throw IllegalStateException("Already resumed")
    }
    }
    }

    #ContinuationImpl.kotlin
    #BaseContinuationImpl类的成员函数
    override fun resumeWith(result: Result<Any?>) {
    var current = this
    var param = result
    while (true) {
    probeCoroutineResumed(current)
    with(current) {
    val completion = completion!!
    val outcome: Result<Any?> =
    try {
    //invokeSuspend 即为MyAnonymous 里的方法
    val outcome = invokeSuspend(param)
    //如果返回值是挂起状态,则函数直接退出
    if (outcome === kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) return
    kotlin.Result.success(outcome)
    } catch (exception: Throwable) {
    kotlin.Result.failure(exception)
    }
    releaseIntercepted() // this state machine instance is terminating
    if (completion is BaseContinuationImpl) {
    current = completion
    param = outcome
    } else {
    //执行到这,最终执行外层的completion,在demo里会输出"result:$result"
    completion.resumeWith(outcome)
    return
    }
    }
    }
    }

    最后再回头看 invokeSuspend

             public final Object invokeSuspend(@NotNull Object var1) {
    Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
    switch(this.label) {
    case 0:
    ResultKt.throwOnFailure(var1);
    String var2 = "I am coroutine";
    boolean var3 = false;
    System.out.println(var2);
    return Unit.INSTANCE;
    default:
    throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
    }
    }

    你兴许已经发现了,此处的返回值永远是Unit.INSTANCE啊,那么协程永远不会挂起。
    没有挂起功能的协程就是鸡肋...
    没错,咱们的demo里实现的是一个无法挂起的协程,回到最初的launchFish()的调用:

        launchFish {
    println("I am coroutine")
    }
    }

    因为闭包里只有一个打印语句,根本没有挂起函数,当然就没有挂起的说法了。

    协程调用整体流程

    上面花很多篇幅去分析协程的调用,其实就是为了从kotlin 的简洁里脱离出来,从而真正了解其背后的原理。
    Demo里的协程构造比较原始,相较于launch/async 等启动方式,它没有上下文、没有线程调度,但并不妨碍我们通过它去了解协程的运作。当我们了解了其运作的核心,到时候再去看launch/async/runBlocking 就非常容易了,毕竟它们都是提供给开发者更方便操作协程的工具,是在原始携程的基础上演变的。
    协程创建调用栈简易图:

    image.png

    4、协程代码我为啥看不懂?

    之前有一些小伙伴跟我反馈说:"小鱼人,我尝试去看协程源码,感觉找不到入口,又或是跟着源码跟到一半就断了... 你是咋阅读的啊?"
    有一说一,协程源码确实不太好懂,若要比较顺畅读懂源码,根据个人经验可能需要以下前置条件:

    1、kotlin 语法基础,这是必须的。
    2、高阶函数&扩展函数。
    3、平台代码差异,有一些类、函数是与平台相关,需要定位到具体平台,比如SafeContinuation,找到Java 平台的文件:SafeContinuationJvm.kt。
    4、断点调试时,有些单步断点不会进入,需要指定运行到的位置。
    5、有些代码是编译时期构造的,需要对照反编译结果查看。
    6、还有些代码是没有源码的,可能是ASM插入的,此时只能靠肉眼理解了。

    如果你对kotlin 基础/高阶函数 等有疑惑,请查看之前的文章。

    本篇仅仅构造了一个简陋的协程,协程的最重要的挂起/恢复并没有涉及,下篇将会着重分析如何构造一个挂起函数,以及协程到底是怎么挂起的。

    本文基于Kotlin 1.5.3,文中完整Demo请点击


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

    收起阅读 »

    一个小故事讲明白进程、线程、Kotlin 协程到底啥关系?

    相信稍微接触过Kotlin的同学都知道Kotlin Coroutine(协程)的大名,甚至有些同学认为重要到"无协程,不Kotlin"的地步,吓得我赶紧去翻阅了协程源码,同时也学习了不少博客,博客里比较典型的几个说法: 协程是轻量级线程、比线程耗费资源少 ...
    继续阅读 »

    相信稍微接触过Kotlin的同学都知道Kotlin Coroutine(协程)的大名,甚至有些同学认为重要到"无协程,不Kotlin"的地步,吓得我赶紧去翻阅了协程源码,同时也学习了不少博客,博客里比较典型的几个说法:




    • 协程是轻量级线程、比线程耗费资源少

    • 协程是线程框架

    • 协程效率高于线程

    • ...



    一堆术语听起来是不是很高端的样子?这些表述正确吗?妥当吗?你说我学了大半天,虽然我也会用,但还是没弄懂啥是协程...

    为了彻底弄懂啥是协程,需要将进程、线程拉进来一起pk。

    通过本篇文章,你将了解到:



    1、程序、进程、CPU、内存关系

    2、进程与线程的故事

    3、线程与Kotlin协程的故事

    4、Kotlin 协程的使命



    1、程序、进程、CPU、内存关系



    image.png


    如上图,平时我们打包好一个应用,放在磁盘上,此时我们称之为程序或者应用,是静态的。也就是咱们平常说的:我下载个程序,你传给apk给我,它们都是程序(应用)。

    当我们执行程序(比如点击某个App),OS 会将它加载进内存,CPU 从内存某个起始地址开始读取指令并执行程序。

    程序从磁盘上加载到内存并被CPU运行期间,称之为进程。因此我们通常说某个应用是否还在存活,实际上说的是进程是否还在内存里;也会说某某程序CPU占用率太高,实际上说的是进程的CPU占用率。

    而操作系统负责管理磁盘、内存、CPU等交互,可以说是大管家。


    2、进程与线程的故事


    接下来我们以一个故事说起。


    上古时代的合作



    image.png


    在上古时候,一个设备里只有一个CPU,能力比较弱,单位时间内能够处理的任务量有限,内存比较小,能加载的应用不多,相应的那会儿编写的程序功能单一,结构简单。


    OS 说:"大家都知道,我们的情况比较具体,只有一个CPU,内存也很小,而现在有不少应用想要占用CPU和内存,无规矩不成方圆,我现在定下个规矩:"



    每个应用加载到内存后,我将给他安排内存里的一块独立的空间,并记录它的一些必要信息,最后规整为一个叫进程的东西,就是代表你这个应用的所有信息,以后我就只管调度进程即可。

    并且进程之间的内存空间是隔离的,无法轻易访问,特殊情况需要经过我的允许。



    应用(程序)说:"哦,我知道了,意思就是:进程是资源分派的基本单位嘛"

    OS 说:"对的,悟性真好,小伙子。"


    规矩定下了,大家就开始干活了:



    1、应用被加载到内存后,OS分派了一些资源给它。

    2、CPU 从内存里逐一取出并执行进程。

    3、其它没有得到CPU青睐的进程则静候等待,等待被翻牌。



    中古时代的合作


    一切都有条不紊的进行着,大家配合默契,其乐融融,直到有一天,OS 发现了一些端倪。

    他发现CPU 在偷懒...找到CPU,压抑心中的愤怒说到:

    "我发现你最近不是很忙哎,是不是工作量不饱和?"

    CPU 忙不迭说到:"冤枉啊,我确实不是很忙,但这不怪我啊。你也知道我最近升级了频率,处理速度快了很多,进程每次给我的任务我都快速执行完了,不过它们却一直占用我,不让我处理其它进程的,我也是没办法啊。"

    OS 大吃一惊到:"大胆进程,居然占着茅坑不拉屎!"

    CPU 小声到:"我又不是茅坑..."


    OS 找来进程劈头盖脸训斥一道:"进程你好大的胆,我之前不是给你说请CPU 做事情要讲究一个原则:按需占有,用完就退。你把我话当耳边风了?"

    进程直呼:"此事与我无关啊,你知道的我最讲原则了,你之前说过对CPU 的使用:应占尽占。我现在不仅要处理本地逻辑,还要从磁盘读取文件,这个时候我虽然不占用CPU,但是我后面文件读结束还是需要他。"


    OS 眉头紧皱,略微思索了一下对进程和CPU道:"此事前因后果均已知悉,容我斟酌几日。"

    几天后,OS 过来对他俩说:"我现在重新拟定一个规则:进程不能一直占用CPU到任务结束为止,需要规定占用的时间片,在规定的时间片内进程能完成多少是多少,时间一到立即退出CPU换另一个进程上,没能完成任务的进程等下个轮到自己的时间片再上"

    进程和CPU 对视一眼,立即附和:"谨遵钧令,使命必达!"


    近现代的合作



    自从实行新规定以来,进程们都有机会抢占CPU了,算是雨露均沾,很少出现某进程长期霸占CPU的现象了,OS 对此很是满意。



    一则来自进程的举报打破这黎明前的宁静。

    OS 收到一则举报:"我进程实名举报CPU 偷懒。"

    OS 心里咯噔一跳,寻思着咋又是CPU,于是叫来CPU 对簿公堂。

    CPU 听到OS 召唤,暗叫不妙,心里立马准备了一套说辞。

    OS 对着CPU 和 进程说:"进程说你偷懒,你在服务进程的时间片内无所事事,我希望你能给我一个满意的答复。"

    CPU 一听这话,心里一阵鄙视,果不出我所料,就知道你问这事。虽然心里诽腹不已,脸上却是郑重其事道:"这事是因为进程交给我的任务很快完成了,它去忙别的事了,让我等等他。"

    OS 诧异道:"你这么快就将进程的任务处理完成了?"

    CPU 面露得以之色道:"你知道的我一直追求进步,这不前阵子又升级了一下嘛,处理能力又提升了。如果说优秀是一种原罪的话,那这个罪名由我承担吧,再如果..."

    OS 看了进程一眼,对CPU 说:"行行行,打住,此事确实与你无关。进程虽然你误会了CPU,但是你提出的问题确实是一个好的思考点,这个下来我想个方案,回头咱们评审一下。"


    一个月后,OS 将进程和CPU召集起来,并拿出方案说:"我们这次将进行一次大的调整,鉴于CPU 处理能力提升,他想要承担更多的工作,而目前以进程为单位提交任务颗粒度太大了,需要再细化。我建议将进程划分为若干线程,这些线程共享进程的资源池,进程想要执行某任务直接交给线程即可,而CPU每次以线程为单位执行。接下来,你们说说各自的意见吧。"

    进程说到:"这个方案很优秀,相当于我可以弄出分身,让各个分身干各项任务,处理UI一个线程,处理I/O是另一个线程,处理其它任务是其它线程,我只需要分派各个任务给线程,剩下的无需我操心了。CPU 你觉得呢?"

    CPU 心底暗道:"你自己倒是简单,只管造分身,脏活累活都是我干..."

    表面故作沉重说到:"这个改动有点大,我现在需要直接对接线程,这块需要下来好好研究一下,不过问题不大。"

    进程补充道:"CPU 你可以要记清楚了,以后线程是CPU 调度的基本单位了。"

    CPU 应道:"好的,好的,了解了(还用你复述OS 的话嘛...)。"


    规矩定下了,大家热火朝天地干活。



    image.png



    进程至少有一个线程在运行,其余按需制造线程,多个线程共用进程资源,每个线程都被CPU 执行。



    新时代的合作


    OS 照例视察各个模块的合作,这天进程又向它抱怨了:"我最近各个线程的数据总是对不上,是不是内存出现了差错?"

    OS 愣了一下,说到:"这问题我知道了,还没来得及和你说呢。最近咱们多放了几个CPU 模块提升设备的整体性能,你的线程可能在不同的CPU上运行,因此拿到的数据有点问题。"

    进程若有所思道:"以前只有一个CPU,各个进程看似同时运行,实则分享CPU时间片,是并发行为。现在CPU 多了,不同的线程有机会同时运行,这就是真并行了吧。"

    OS 道:"举一反三能力不错哦,不管并行还是并发,多个线程共享的数据有可能不一致,尤其加入了多CPU后,现象比较明显,这就是多线程数据安全问题。底层已经提供了一些基本的机制,比如CPU的MESI,但还是无法完全解决这问题,剩下的交给上层吧。"

    进程道:"了解了,那我告诉各个线程,如果他们有共享数据的需求,自己协商解决一下。"

    进程告知线程自己处理线程安全问题,线程答到:"我只是个工具人,谁用谁负责处理就好。"

    一众编程语言答到:"我自己来处理吧。"

    多CPU 如下,每个线程都有可能被其它CPU运行。



    image.png


    3、线程与Kotlin协程的故事


    Java 线程调用


    底层一众大佬已经将坑踩得差不多了,这时候得各个编程语言出场了。

    C 语言作为骨灰级人物远近闻名,OS、驱动等都是由他编写,这无需介绍了。

    之后如雨后春笋般又冒出了许多优秀的语言,如C++、Java、C#、Qt 等,本小结的主人公:Java。

    Java 从小目标远大,想要跨平台运行,借助于JVM他可以实现这个梦想,每个JVM 实例对应一个进程,并且OS 还给了他操作线程的权限。

    Java 想既然大佬这么支持,那我要撸起袖子加油干了,刚好在Android 上接到一个需求:



    通过学生的id,向后台(联网)查询学生的基本信息,如姓名、年龄等。



    Java 心想:"这还不简单,且看我猛如虎的操作。"

    先定义学生Bean类型:


    public class StudentInfo {
    //学生id
    private long stuId = 999;
    private String name = "fish";
    private int age = 18;
    }

    再定义一个获取的动作:


        //从后台获取信息
    public StudentInfo getWithoutThread(long stuId) {
    try {
    //模拟耗时操作
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    return new StudentInfo();
    }

    信心满满地运行,却被现实无情打脸,只见控制台显目的红色:



    不能在主线程进行网络请求。



    同步调用


    Java 并不气馁,这问题简单,我开个线程取获取不就得了?


        Callable<StudentInfo> callable = new Callable<StudentInfo>() {
    @Override
    public StudentInfo call() throws Exception {
    try {
    //模拟耗时操作
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    return new StudentInfo();
    }
    };

    public StudentInfo getStuInfo(long stuId) {
    //定义任务
    FutureTask<StudentInfo> futureTask = new FutureTask<>(callable);
    //开启线程,执行任务
    new Thread(futureTask).start();
    try {
    //阻塞获取结果
    StudentInfo studentInfo = futureTask.get();
    return studentInfo;
    } catch (ExecutionException e) {
    e.printStackTrace();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    return null;
    }

    而后,再在界面上弹出学生姓名:


     JavaStudent javaStudent = new JavaStudent();
    StudentInfo studentInfo = javaStudent.getWithoutThread(999);
    Toast.makeText(this, "学生姓名:" + studentInfo.getName(), Toast.LENGTH_LONG).show();

    刚开始能弹出Toast,然而后面动不动UI就卡顿,甚至出现ANR 弹窗。

    Java 百思不得其解,后得到Android 本尊指点:



    Android 主线程不能进行耗时操作。



    Java 说到:"我就简单获取个信息,咋这么多限制..."

    Android 答到:"Android 通常需要在主线程更新UI,主线程不能做过多耗时操作,否则影响UI 渲染流畅度。不仅是Android,你Java 本身的主线程(main线程)通常也不会做耗时啊,都是通过开启各个线程去完成任务,要不然每一步都要主线程等待,那主线程的其它关键任务就没法开启了。"

    Java 沉思道:"有道理,容我三思。"


    异步调用与回调


    Java 果不愧是编程语言界的老手,闭关几天就想出了方案,直接show us code:


        //回调接口
    public interface Callback {
    void onCallback(StudentInfo studentInfo);
    }

    //异步调用
    public void getStuInfoAsync(long stuId, Callback callback) {
    new Thread(() -> {
    try {
    //模拟耗时操作
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    StudentInfo studentInfo = new StudentInfo();
    if (callback != null) {
    //回调给调用者
    callback.onCallback(studentInfo);
    }
    }).start();
    }

    在调用耗时方法时,只需要将自己的凭证(回调对象)传给方法即可,调用者不管方法里具体是咋实现的,才不管你开几个线程呢,反正你有结果通过回调给我。

    调用者只需要在需要的地方实现回调接收即可:


            JavaStudent javaStudent = new JavaStudent();
    javaStudent.getStuInfoAsync(999, new JavaStudent.Callback() {
    @Override
    public void onCallback(StudentInfo studentInfo) {
    //异步调用,回调从子线程返回,需要切换到主线程更新UI
    runOnUiThread(() -> {
    Toast.makeText(TestJavaActivity.this, "学生姓名:" + studentInfo.getName(), Toast.LENGTH_LONG).show();
    });
    }
    });

    异步调用的好处显而易见:



    1、不用阻塞调用者,调用者可继续做其它事情。

    2、线程没有被阻塞,相比同步调用效率更高。



    缺点也是比较明显:



    1、没有同步调用直观。

    2、容易陷入多层回调,不利于阅读与调试。

    3、从内到外的异常处理缺失传递性。



    Kotlin 协程毛遂自荐


    Java 靠着左手同步调用、右手异步调用的左右互搏技能,成功实现了很多项目,虽然异步调用有着一些缺点,但瑕不掩瑜。

    这天,Java 又收到需求变更了:



    通过学生id,获取学生信息,通过学生信息,获取他的语文老师id,通过语文老师id,获取老师姓名,最后更新UI。



    Java 不假思索到:"简单,我再嵌套一层回调即可。"


        //回调接口
    public interface Callback {
    void onCallback(StudentInfo studentInfo);
    //新增老师回调接口
    default void onCallback(TeacherInfo teacherInfo){}
    }

    //异步调用
    public void getTeachInfoAsync(long stuId, Callback callback) {
    //先获取学生信息
    getStuInfoAsync(stuId, new Callback() {
    @Override
    public void onCallback(StudentInfo studentInfo) {
    //获取学生信息后,取出关联的语文老师id,获取老师信息
    new Thread(() -> {
    try {
    //模拟耗时操作
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    TeacherInfo teacherInfo = new TeacherInfo();
    if (callback != null) {
    //老师信息获取成功
    callback.onCallback(teacherInfo);
    }
    }).start();
    }
    });
    }

    眼看Java 一下子实现了功能,Android再提需求:



    通过老师id,获取他所在的教研组信息,再通过教研组id获取教研组排名...



    Java 抗议道:"哪有这么奇葩的需求,那我不是要无限回调吗,我可以实现,但不好维护,过几天我自己看都看不懂了。"

    Android:"不就是几个回调的问题嘛,亏你还是老员工,实在不行,我找其他人。"

    Java:"...我再想想。"


    正当Java 一筹莫展之际,吃饭时刚好碰到了Kotlin,Java 难得有时间和这位新入职的小伙伴聊聊天,发发牢骚。

    Kotlin 听了Java 的遭遇,表达了同情并劝说Java 赶紧离职,Android 这块不适合他。


    Kotlin 随后找到Android,略微紧张地说:"吾有一计,可安天下。"

    Android 对于毛遂自荐的人才是非常欢迎的,问曰:"计将安出"

    Kotlin 随后激动到:协程。

    Android 诧异道:"协程,旅游?"



    image.png


    Kotlin 赶紧道:"非也,此协程非彼携程...而是它"



    jj.png


    Android 说:"看这肌肉挺大的,想必比较强,请开始你的表演吧。"

    Koltin 立马展示自己。


    class StudentCoroutine {
    private val FIXED_TEACHER_ID = 888
    fun getTeachInfo(act: Activity, stuId: Long) {
    GlobalScope.launch(Dispatchers.Main) {

    var studentInfo: StudentInfo
    var teacherInfo: TeacherInfo? = null

    //先获取学生信息
    withContext(Dispatchers.IO) {
    //模拟网络获取
    Thread.sleep(2000)
    studentInfo = StudentInfo()
    }
    //再获取教师信息
    withContext(Dispatchers.IO) {
    if (studentInfo.lanTechId.toInt() === FIXED_TEACHER_ID) {
    //模拟网络获取
    Thread.sleep(2000)
    teacherInfo = TeacherInfo()
    }
    }
    //更新UI
    Toast.makeText(act, "teacher name:${teacherInfo?.name}", Toast.LENGTH_LONG).show()
    }
    Toast.makeText(act, "主线程还在跑...", Toast.LENGTH_LONG).show()
    }
    }

    外部调用:


        var student = StudentCoroutine()
    student.getTeachInfo(this@MainActivity, 999)

    Android 一看,大吃一惊:"想不到,语言界竟然有如此厚颜无耻之...不对,如此简洁的写法。"

    Kotlin 道:"协程这概念早就有了,其它兄弟语言Python、Go等也实现了,我也是站在巨人的肩膀上,秉着解决用户痛点的思路来设计的。"

    Android 随即大手一挥道:"就冲着你这简洁的语法,今后Android 业务你来搞吧,希望你能够担起重担。"

    Kotlin 立马道:"没问题,我本身也是跨平台的,只是Java 那边...。"

    Android:"这个你无需顾虑,Java 的工作我来做,成年人应该知道这世界是残酷的。"

    Java 听到Kotlin 逐渐蚕食了自己在Android上的业务,略微生气,于是看了Kotlin 的写法,最后长舒一口气:"确实比较简洁,看起来功能阻塞了主线程,实际并没有。其实就是 用同步的写法,表达异步的调用。"

    Koltin :"知我者,老大哥Java 也。"


    4、Kotlin 协程的使命


    通过与Java 的比对,大家也知道了协程最大的特色:



    将异步编程同步化。



    当然还有一些特点,如异常处理、协程取消等。

    再回过头来看看上面的疑问。


    1、协程是轻量级线程、比线程耗费资源少
    这话虽然是官方说的,但我觉得有点误导的作用,协程是语言层面的东西,线程是系统层面的东西,两者没有可比性。

    协程就是一段代码块,既然是代码那就离不开CPU的执行,而CPU调度的基本单位是线程。


    2、协程是线程框架
    协程解决了移步编程时过多回调的问题,既然是异步编程,那势必涉及到不同的线程。Kotlin 协程内部自己维护了线程池,与Java 线程池相比有些优化的地方。在使用协程过程中,无需关注线程的切换细节,只需指定想要执行的线程即可,从对线程的封装这方面来说这说话也没问题。


    3、协程效率高于线程
    与第一点类似,协程在运行方面的高效率其实换成回调方式也是能够达成同样的效果,实际上协程内部也是通过回调实现的,只是在编译阶段封装了回调的细节而已。因此,协程与线程没有可比性。


    阅读完上述内容,想必大家都知道进程、线程、协程的关系了,也许大家还很好奇协程是怎么做到不阻塞调用者线程的?它又是怎么在获取结果后回到原来的位置继续执行呢?线程之间如何做到丝滑般切换的?

    不要着急,这些点我们一点点探秘,下篇文章开始徒手开启一个协程,并分析其原理。

    Kotlin 源码阅读需要一定的Kotlin 基础,尤其是高阶函数,若是这方面还不太懂的同学可以查阅之前的文章:Kotlin 高阶函数从未如此清晰 系列


    本文基于Kotlin 1.5.3,文中完整Demo请点击



    coroutine_.gif


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

    Flutter 混合开发(Android)Flutter跟Native相互通信

    前言Flutter 作为混合开发,跟native端做一些交互在所难免,比如说调用原生系统传感器、原生端的网络框架进行数据请求就会用到 Flutter 调用android 及android 原生调用 Flutter的方法,这里就涉及到Platform Chann...
    继续阅读 »

    前言

    Flutter 作为混合开发,跟native端做一些交互在所难免,比如说调用原生系统传感器、原生端的网络框架进行数据请求就会用到 Flutter 调用android 及android 原生调用 Flutter的方法,这里就涉及到Platform Channels(平台通道)

    Platform Channels (平台通道)

    Flutter 通过Channel 与客户端之间传递消息,如图:


    图中就是通过MethodChannel的方式实现Flutter 与客户端之间的消息传递。MethodChannel是Platform Channels中的一种,Flutter有三种通信类型:

    BasicMessageChannel:用于传递字符串和半结构化的信息

    MethodChannel:用于传递方法调用(method invocation)通常用来调用native中某个方法

    EventChannel: 用于数据流(event streams)的通信。有监听功能,比如电量变化之后直接推送数据给flutter端。

    为了保证UI的响应,通过Platform Channels传递的消息都是异步的。

    更多关于channel原理可以去看这篇文章:channel原理篇

    Platform Channels 使用

    1.MethodChannel的使用

    原生客户端写法(以Android 为例)

    首先定义一个获取手机电量方法

    private int getBatteryLevel() {
    return 90;
    }

    这函数是要给Flutter 调用的方法,此时就需要通过 MethodChannel 来建立这个通道了。

    首先新增一个初始化 MethodChannel 的方法

    private String METHOD_CHANNEL = "common.flutter/battery";
    private String GET_BATTERY_LEVEL = "getBatteryLevel";
    private MethodChannel methodChannel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
    initMethodChannel();
    getFlutterView().postDelayed(() ->
    methodChannel.invokeMethod("get_message", null, new MethodChannel.Result() {
    @Override
    public void success(@Nullable Object o) {
    Log.d(TAG, "get_message:" + o.toString());
    }

    @Override
    public void error(String s, @Nullable String s1, @Nullable Object o) {

    }

    @Override
    public void notImplemented() {

    }
    }), 5000);

    }

    private void initMethodChannel() {
    methodChannel = new MethodChannel(getFlutterView(), METHOD_CHANNEL);
    methodChannel.setMethodCallHandler(
    (methodCall, result) -> {
    if (methodCall.method.equals(GET_BATTERY_LEVEL)) {
    int batteryLevel = getBatteryLevel();

    if (batteryLevel != -1) {
    result.success(batteryLevel);
    } else {
    result.error("UNAVAILABLE", "Battery level not available.", null);
    }
    } else {
    result.notImplemented();
    }
    });


    }

    private int getBatteryLevel() {
    return 90;
    }

    METHOD_CHANNEL 用于和flutter交互的标识,由于一般情况下会有多个channel,在app里面需要保持唯一性

    MethodChannel 都是保存在以通道名为Key的Map中。所以要是设了两个名字一样的channel,只有后设置的那个会生效。

    onMethodCall 有两个参数,onMethodCall 里包含要调用的方法名称和参数。Result是给Flutter的返回值。方法名是客户端与Flutter统一设定。通过if/switch语句判断 MethodCall.method 来区分不同的方法,在我们的例子里面我们只会处理名为“getBatteryLevel”的调用。在调用本地方法获取到电量以后通过 result.success(batteryLevel) 调用把电量值返回给Flutter。

    MethodChannel-Flutter 端

    直接先看一下Flutter端的代码

    class _MyHomePageState extends State<MyHomePage> {
    int _counter = 0;
    static const platform = const MethodChannel('common.flutter/battery');

    void _incrementCounter() {
    setState(() {
    _counter++;
    _getBatteryLevel();
    });
    }

    @override
    Widget build(BuildContext context) {
    platform.setMethodCallHandler(platformCallHandler);
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    body: Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
    Text(
    'You have pushed the button this many times:',
    ),
    Text(
    '$_counter',
    style: Theme.of(context).textTheme.display1,
    ),
    Text('$_batteryLevel'),
    ],
    ),
    ),
    floatingActionButton: FloatingActionButton(
    onPressed: _incrementCounter,
    tooltip: 'Increment',
    child: Icon(Icons.add),
    ),
    );
    }

    String _batteryLevel = 'Unknown battery level.';

    Future<Null> _getBatteryLevel() async {
    String batteryLevel;
    try {
    final int result = await platform.invokeMethod('getBatteryLevel');
    batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
    batteryLevel = "Failed to get battery level: '${e.message}'.";
    }

    setState(() {
    _batteryLevel = batteryLevel;
    });
    }

    //客户端调用
    Future<dynamic> platformCallHandler(MethodCall call) async {
    switch (call.method) {
    case "get_message":
    return "Hello from Flutter";
    break;
    }
    }
    }

    上面代码解析:
    首先,定义一个常量result.success(platform),和Android客户端定义的channel一致;
    接下来定义一个 result.success(_getBatteryLevel())方法,用来调用Android 端的方法,result.success(final int result = await platform.invokeMethod('getBatteryLevel');) 这行代码就是通过通道来调用Native(Android)方法了。因为MethodChannel是异步调用的,所以这里必须要使用await关键字。

    在上面Android代码中我们把获取到的电量通过result.success(batteryLevel);返回给Flutter。这里await表达式执行完成以后电量就直接赋值给result变量了。然后通过result.success(setState); 去改变Text显示值。到这里为止,是通过Flutter端调用原生客户端方法。

    MethodChannel 其实是一个可以双向调用的方法,在上面的代码中,其实我们也体现了,通过原生客户端调用Flutter的方法。

    在原生端通过 methodChannel.invokeMethod 的方法调用

    methodChannel.invokeMethod("get_message", null, new MethodChannel.Result() {
    @Override
    public void success(@Nullable Object o) {
    Log.d(TAG, "get_message:" + o.toString());
    }

    @Override
    public void error(String s, @Nullable String s1, @Nullable Object o) {

    }

    @Override
    public void notImplemented() {

    }
    });

    在Flutter端就需要给MethodChannel设置一个MethodCallHandler

    static const platform = const MethodChannel('common.flutter/battery');
    platform.setMethodCallHandler(platformCallHandler);
    Future<dynamic> platformCallHandler(MethodCall call) async {
    switch (call.method) {
    case "get_message":
    return "Hello from Flutter";
    break;
    }
    }

    以上就是MethodChannel的相关用法了。

    EventChannel

    将数据推送给Flutter端,类似我们常用的推送功能,有需要就推送给Flutter端,是否需要去处理这个推送由Flutter那边决定。相对于MethodChannel是主动获取,EventChannel则是被动推送。

    EventChannel 原生客户端写法

    private String EVENT_CHANNEL = "common.flutter/message";
    private int count = 0;
    private Timer timer;

    private void initEventChannel() {
    new EventChannel(getFlutterView(), EVENT_CHANNEL).setStreamHandler(new EventChannel.StreamHandler() {
    @Override
    public void onListen(Object arguments, EventChannel.EventSink events) {
    timer.schedule(new TimerTask() {
    @Override
    public void run() {
    if (count < 10) {
    count++;
    events.success("当前时间:" + System.currentTimeMillis());
    } else {
    timer.cancel();
    }
    }
    }, 1000, 1000);
    }

    @Override
    public void onCancel(Object o) {

    }
    });
    }

    在上面的代码中,我们做了一个定时器,每秒向Flutter推送一个消息,告诉Flutter我们当前时间。为了防止一直倒计时,我这边做了个计数,超过10次就停止发送。

    EventChannel Flutter端

    String message = "not message";
    static const eventChannel = const EventChannel('common.flutter/message');
    @override
    void initState() {
    super.initState();
    eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
    }

    void _onEvent(Object event) {
    setState(() {
    message =
    "message: $event";
    });
    }

    void _onError(Object error) {
    setState(() {
    message = 'message: unknown.';
    });
    }

    上面的代码就是Flutter端接收原生客户端数据,通过_onEvent 来接收数据,将数据显示Text。这个实现相对简单,如果要达到业务分类,需要将数据封装成json,通过json数据包装一些对应业务标识和数据来做区分。

    BasicMessageChannel

    BasicMessageChannel (主要是传递字符串和一些半结构体的数据)

    BasicMessageChannel Android端

    private void initBasicMessageChannel() {
    BasicMessageChannel<Object> basicMessageChannel = new BasicMessageChannel<>(getFlutterView(), BASIC_CHANNEL, StandardMessageCodec.INSTANCE);
    //主动发送消息到flutter 并接收flutter消息回复
    basicMessageChannel.send("send basic message", (object)-> {
    Log.e(TAG, "receive reply msg from flutter:" + object.toString());
    });

    //接收flutter消息 并发送回复
    basicMessageChannel.setMessageHandler((object, reply)-> {
    Log.e(TAG, "receive msg from flutter:" + object.toString());
    reply.reply("reply:got your message");

    });

    }

    BasicMessageChannel Flutter端

      static const basicChannel = const BasicMessageChannel('common.flutter/basic', StandardMessageCodec());
    //发送消息到原生客户端 并且接收到原生客户端的回复
    Future<String> sendMessage() async {
    String reply = await basicChannel.send('this is flutter');
    print("receive reply msg from native:$reply");
    return reply;
    }

    //接收原生消息 并发送回复
    void receiveMessage() async {
    basicChannel.setMessageHandler((msg) async {
    print("receive from Android:$msg");
    return "get native message";
    });

    上面例子中用到的编解码器为StandardMessageCodec ,例子中通信都是String,用StringCodec也可以。

    以上就是Flutter提供三种platform和dart端的消息通信方式。

    本文转载自: https://www.jianshu.com/p/1f12e53f5fb3
    收起阅读 »

    在浏览器输入URL到页面展示发生了什么

    查询缓存其实从填写上url按下回车后,我们就进入了第一步就是 DNS 解析过程,首先需要找到这个 url 域名的服务器 ip,为了寻找这个 ip,浏览器首先会寻找缓存,查看缓存中是否有记录缓存的查找记录为:浏览器缓存=》系统缓存=》路由 器缓存缓存中没有则查找...
    继续阅读 »

    查询缓存


    其实从填写上url按下回车后,我们就进入了第一步就是 DNS 解析过程,首先需要找到这个 url 域名的服务器 ip,为了寻找这个 ip,浏览器首先会寻找缓存,查看缓存中是否有记录缓存的查找记录为:浏览器缓存=》系统缓存=》路由 器缓存缓存中没有则查找系统的 hosts 文件中是否有记录,

    DNS服务器


    如果没有缓存则查询 DNS 服务器,得到服务器的 ip 地址后,浏览器根据这个 ip 以及相应的端口号发送连接请求;当然如果DNS服务器中没有解析成功,他会向上一步获得的顶级DNS服务器发送解析请求。


    TCP三次握手


    客户端和服务端都需要直到各自可收发,因此需要三次握手。

    从图片可以得到三次握手可以简化为:

    1、浏览器发送连接请求;
    2、服务器允许连接后并发送ACK报文给浏览器;
    2、浏览器接受ACK后并向后端发送一个ACK,TCP连接建立成功
    HTTP协议包

    构造一个 http 请求,这个请求报文会包括这次请求的信息,主要是请求方法,请求说明和请求附带的数据,并将这个 http 请求封装在一个 tcp 包中;这个 tcp 包也就是会依次经过传输层,网络层, 数据链路层,物理层到达服务器,服务器解析这个请求来作出响应;返回相应的 html 给浏览器;
    浏览器处理HTML文档

    因为 html 是一个树形结构,浏览器根据这个 html 来构建 DOM 树,在 dom 树的构建过程中如果遇到 JS 脚本和外部 JS 连接,则会停止构建 DOM 树来执行和下载相应的代码,这会造成阻塞,这就是为什么推荐 JS 代码应该放在 html 代码的后面;

    渲染树
    之后根据外部样式,内部样式,内联样式构建一个 CSS 对象模型树 CSSOM 树,构建完成后和 DOM 树合并为渲染树,在排除非视觉节点,比如 script,meta 标签和排除 display 为 none 的节点,之后进行布局,布局主要是确定各个元素的位置和尺寸,之后是渲染页面,因为 html 文件中会含有图片,视频,音频等资源,在解析 DOM 的过 程中,遇到这些都会进行并行下载,浏览器对每个域的并行下载数量有一定的限制,一 般是 4-6 个,当然在这些所有的请求中我们还需要关注的就是缓存,缓存一般通过 Cache-Control、Last-Modify、Expires 等首部字段控制。

    Cache-Control 和 Expires 的区别
    在于 Cache-Control 使用相对时间,Expires 使用的是基于服务器 端的绝对时间,因为存 在时差问题,一般采用 Cache-Control,在请求这些有设置了缓存的数据时,会先 查看 是否过期,如果没有过期则直接使用本地缓存,过期则请求并在服务器校验文件是否修 改,如果上一次 响应设置了 ETag 值会在这次请求的时候作为 If-None-Match 的值交给 服务器校验,如果一致,继续校验 Last-Modified,没有设置 ETag 则直接验证 Last-Modified,再决定是否返回 304

    到这里就结束了么?其实按照标题所说的到渲染页面我们确实到此就说明完了,但是严格意义上其实我们后面还会有TCP的四次挥手断开连接,这个我们就放到后面单独出一篇为大家介绍吧!
    TCP 和 UDP 的区别

    1、TCP 是面向连接的,udp 是无连接的即发送数据前不需要先建立链接。
    2、TCP 提供可靠的服务。也就是说,通过 TCP 连接传送的数据,无差错,不丢失, 不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付。 并且因为 tcp 可靠, 面向连接,不会丢失数据因此适合大数据量的交换。
    3、TCP 是面向字节流,UDP 面向报文,并且网络出现拥塞不会使得发送速率降低(因 此会出现丢包,对实时的应用比如 IP 电话和视频会议等)。
    4、TCP 只能是 1 对 1 的,UDP 支持 1 对 1,1 对多。
    5、TCP 的首部较大为 20 字节,而 UDP 只有 8 字节。
    6、TCP 是面向连接的可靠性传输,而 UDP 是不可靠的。



    收起阅读 »

    什么?你连个三色渐变圆角按钮都需要UI切图?

    废话不多说,先上效果图: 该效果其实由三部分组成: 渐变 圆角 文本 渐变 关于渐变,估计大家都不会陌生,以往都是使用gradient进行制作: shape_gradient.xml <?xml version="1.0" encoding="ut...
    继续阅读 »

    废话不多说,先上效果图:



    该效果其实由三部分组成:



    • 渐变

    • 圆角

    • 文本


    渐变


    关于渐变,估计大家都不会陌生,以往都是使用gradient进行制作:


    shape_gradient.xml


    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android">
    <gradient
    android:startColor="#B620E0"
    android:endColor="#E38746" />
    </shape>

        <View
    android:layout_width="match_parent"
    android:layout_height="70dp"
    android:background="@drawable/shape_gradient" />


    但是,这个只能支持双色渐变,超过双色就无能为力了,所以,我们要考虑使用其它方式:


        /**
    * Create a shader that draws a linear gradient along a line.
    *
    * @param x0 The x-coordinate for the start of the gradient line
    * @param y0 The y-coordinate for the start of the gradient line
    * @param x1 The x-coordinate for the end of the gradient line
    * @param y1 The y-coordinate for the end of the gradient line
    * @param colors The colors to be distributed along the gradient line
    * @param positions May be null. The relative positions [0..1] of
    * each corresponding color in the colors array. If this is null,
    * the the colors are distributed evenly along the gradient line.
    * @param tile The Shader tiling mode
    */
    public LinearGradient(float x0, float y0, float x1, float y1, @NonNull @ColorInt int colors[],
    @Nullable float positions[], @NonNull TileMode tile)

        /**
    * x0、y0、x1、y1为决定渐变颜色方向的两个坐标点,x0、y0为起始坐标,x1、y1为终点坐标
    * @param colors 所有渐变颜色的数组,即放多少个颜色进去,就有多少种渐变颜色
    * @param positions 渐变颜色的比值,默认为均匀分布。
    * 把总长度理解为1,假如里面的值为[0.3,0.2,0.5],那么,渐变的颜色就会以 0.3 : 0:2 :0.5 比例进行排版
    * @param tile 着色器模式
    */
    public LinearGradient(float x0, float y0, float x1, float y1, int colors[], float positions[],
    TileMode tile)

    创建自定义View


    public class ColorView extends View {
    public ColorView(Context context) {
    super(context);
    }

    public ColorView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    }

    public ColorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //获取宽高
    int width = getWidth();
    int height = getHeight();

    //渐变的颜色
    int colorStart = Color.parseColor("#E38746");
    int color1 = Color.parseColor("#B620E0");
    int colorEnd = Color.parseColor("#5995F6");
    //绘画渐变效果
    Paint paintColor = new Paint();
    LinearGradient backGradient = new LinearGradient(0, height, width, 0, new int[]{colorStart, color1, colorEnd}, null, Shader.TileMode.CLAMP);
    paintColor.setShader(backGradient);
    canvas.drawRect(0, 0, width, height, paintColor);
    }
    }

        <com.jm.xpproject.ColorView
    android:layout_width="match_parent"
    android:layout_height="70dp" />

    效果:



    圆角


    关于圆角,我们需要使用到BitmapShader,使用方式:


            BitmapShader bitmapShaderColor = new BitmapShader(bitmapColor, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintFillet = new Paint();
    paintFillet.setAntiAlias(true);
    paintFillet.setShader(bitmapShaderColor);
    //绘画到画布中
    canvas.drawRoundRect(new RectF(0, 0, width, height), radius, radius, paintFillet);

    由于这里的BitmapShader是对于Bitmap进行操作的,所以,对于渐变效果,我们不能直接把他绘画到原始画布上,而是生成一个Bitmap,将渐变绘画记录下来:


    还是刚刚的自定义View


        @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //获取View的宽高
    int width = getWidth();
    int height = getHeight();

    //第一步,绘画出一个渐变效果的Bitmap
    //创建存放渐变效果的bitmap
    Bitmap bitmapColor = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvasColor = new Canvas(bitmapColor);
    //渐变的颜色
    int colorStart = Color.parseColor("#E38746");
    int color1 = Color.parseColor("#B620E0");
    int colorEnd = Color.parseColor("#5995F6");
    //绘画渐变效果
    Paint paintColor = new Paint();
    LinearGradient backGradient = new LinearGradient(0, height, width, 0, new int[]{colorStart, color1, colorEnd}, null, Shader.TileMode.CLAMP);
    paintColor.setShader(backGradient);
    canvasColor.drawRect(0, 0, width, height, paintColor);
    //第二步,绘画出一个圆角渐变效果
    //绘画出圆角渐变效果
    BitmapShader bitmapShaderColor = new BitmapShader(bitmapColor, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintFillet = new Paint();
    paintFillet.setAntiAlias(true);
    paintFillet.setShader(bitmapShaderColor);
    //绘画到画布中
    canvas.drawRoundRect(new RectF(0, 0, width, height), 100, 100, paintFillet);
    }

    效果:



    至于中间的空白部分,其实我们依葫芦画瓢,再画上一个白色的圆角Bitmap即可:


            //创建存放白底的bitmap
    Bitmap bitmapWhite = Bitmap.createBitmap(width - colorWidth * 2, height - colorWidth * 2, Bitmap.Config.RGB_565);
    bitmapWhite.eraseColor(Color.parseColor("#FFFFFF"));

    BitmapShader bitmapShaderWhite = new BitmapShader(bitmapWhite, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintWhite = new Paint();
    paintWhite.setAntiAlias(true);
    paintWhite.setShader(bitmapShaderWhite);
    // 将白色Bitmap绘制到画布上面
    canvas.drawRoundRect(new RectF(colorWidth, colorWidth, width - colorWidth, height - colorWidth), radius, radius, paintWhite);

    总体代码:



    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //获取View的宽高
    int width = getWidth();
    int height = getHeight();

    //第一步,绘画出一个渐变效果的Bitmap
    //创建存放渐变效果的bitmap
    Bitmap bitmapColor = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvasColor = new Canvas(bitmapColor);
    //渐变的颜色
    int colorStart = Color.parseColor("#E38746");
    int color1 = Color.parseColor("#B620E0");
    int colorEnd = Color.parseColor("#5995F6");
    //绘画渐变效果
    Paint paintColor = new Paint();
    LinearGradient backGradient = new LinearGradient(0, height, width, 0, new int[]{colorStart, color1, colorEnd}, null, Shader.TileMode.CLAMP);
    paintColor.setShader(backGradient);
    canvasColor.drawRect(0, 0, width, height, paintColor);
    //第二步,绘画出一个圆角渐变效果
    //绘画出圆角渐变效果
    BitmapShader bitmapShaderColor = new BitmapShader(bitmapColor, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintFillet = new Paint();
    paintFillet.setAntiAlias(true);
    paintFillet.setShader(bitmapShaderColor);
    //绘画到画布中
    canvas.drawRoundRect(new RectF(0, 0, width, height), 100, 100, paintFillet);

    //第三步,绘画出一个白色的bitmap覆盖上去
    //创建存放白底的bitmap
    Bitmap bitmapWhite = Bitmap.createBitmap(width - 5 * 2, height - 5 * 2, Bitmap.Config.RGB_565);
    bitmapWhite.eraseColor(Color.parseColor("#FFFFFF"));

    BitmapShader bitmapShaderWhite = new BitmapShader(bitmapWhite, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintWhite = new Paint();
    paintWhite.setAntiAlias(true);
    paintWhite.setShader(bitmapShaderWhite);
    // 将白色Bitmap绘制到画布上面
    canvas.drawRoundRect(new RectF(5, 5, width - 5, height - 5), 100, 100, paintWhite);
    }

    效果:



    文本


    像文本就简单了,使用drawText即可,只要注意在绘画的时候,要对文本进行居中显示,因为 Android 默认绘画文本,是从左下角进行绘画的,就像这样:


            Paint paintText = new Paint();
    paintText.setAntiAlias(true);
    paintText.setColor(Color.parseColor("#000000"));
    paintText.setTextSize(100);
    canvas.drawText("收藏", width / 2, height / 2, paintText);
    canvas.drawLine(width / 2, 0, width / 2, height, paintText);
    canvas.drawLine(0, height / 2, width, height / 2, paintText);


    正确做法:


            String text = "收藏";
    Rect rect = new Rect();
    Paint paintText = new Paint();
    paintText.setAntiAlias(true);
    paintText.setColor(Color.parseColor("#000000"));
    paintText.setTextSize(100);
    paintText.getTextBounds(text, 0, text.length(), rect);
    int widthFont = rect.width();//文本的宽度
    int heightFont = rect.height();//文本的高度
    canvas.drawText(text, (width - widthFont) / 2, (height+heightFont) / 2, paintText);


    至此,基本功能的制作就完成了



    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //获取View的宽高
    int width = getWidth();
    int height = getHeight();

    //第一步,绘画出一个渐变效果的Bitmap
    //创建存放渐变效果的bitmap
    Bitmap bitmapColor = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvasColor = new Canvas(bitmapColor);
    //渐变的颜色
    int colorStart = Color.parseColor("#E38746");
    int color1 = Color.parseColor("#B620E0");
    int colorEnd = Color.parseColor("#5995F6");
    //绘画渐变效果
    Paint paintColor = new Paint();
    LinearGradient backGradient = new LinearGradient(0, height, width, 0, new int[]{colorStart, color1, colorEnd}, null, Shader.TileMode.CLAMP);
    paintColor.setShader(backGradient);
    canvasColor.drawRect(0, 0, width, height, paintColor);
    //第二步,绘画出一个圆角渐变效果
    //绘画出圆角渐变效果
    BitmapShader bitmapShaderColor = new BitmapShader(bitmapColor, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintFillet = new Paint();
    paintFillet.setAntiAlias(true);
    paintFillet.setShader(bitmapShaderColor);
    //绘画到画布中
    canvas.drawRoundRect(new RectF(0, 0, width, height), 100, 100, paintFillet);

    //第三步,绘画出一个白色的bitmap覆盖上去
    //创建存放白底的bitmap
    Bitmap bitmapWhite = Bitmap.createBitmap(width - 5 * 2, height - 5 * 2, Bitmap.Config.RGB_565);
    bitmapWhite.eraseColor(Color.parseColor("#FFFFFF"));

    BitmapShader bitmapShaderWhite = new BitmapShader(bitmapWhite, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    Paint paintWhite = new Paint();
    paintWhite.setAntiAlias(true);
    paintWhite.setShader(bitmapShaderWhite);
    // 将白色Bitmap绘制到画布上面
    canvas.drawRoundRect(new RectF(5, 5, width - 5, height - 5), 100, 100, paintWhite);

    String text = "收藏";
    Rect rect = new Rect();
    Paint paintText = new Paint();
    paintText.setAntiAlias(true);
    paintText.setColor(Color.parseColor("#000000"));
    paintText.setTextSize(100);
    paintText.getTextBounds(text, 0, text.length(), rect);
    int widthFont = rect.width();//文本的宽度
    int heightFont = rect.height();//文本的高度
    canvas.drawText(text, (width - widthFont) / 2, (height+heightFont) / 2, paintText);
    }

    封装


    上面虽然已经把全部功能都讲解完了,但是,假如就直接这样放入项目中,是极其不规范的,无法动态设置文本、文本大小、颜色厚度等等


    这里,我进行了简易封装,大家可以基于此进行业务修改:


    attrs.xml


        <declare-styleable name="GradientColorButton">
    <attr name="btnText" format="string" />
    <attr name="btnTextSize" format="dimension" />
    <attr name="btnTextColor" format="color" />
    <attr name="colorWidth" format="dimension" />
    <attr name="colorRadius" format="dimension" />
    </declare-styleable>

    public class GradientColorButton extends View {

    /**
    * 文本
    */
    private String text = "";
    /**
    * 文本颜色
    */
    private int textColor;
    /**
    * 文本大小
    */
    private float textSize;
    /**
    * 颜色的宽度
    */
    private float colorWidth;
    /**
    * 圆角度数
    */
    private float radius;

    //渐变的颜色
    private int colorStart = Color.parseColor("#E38746");
    private int color1 = Color.parseColor("#B620E0");
    private int colorEnd = Color.parseColor("#5995F6");

    //控件的宽高
    private int width;
    private int height;
    /**
    * 渐变颜色的Bitmap
    */
    private Bitmap bitmapColor;

    //画笔
    private Paint paintColor;
    private Paint paintFillet;
    private Paint paintWhite;
    private Paint paintText;
    //字体的宽高
    private int widthFont;
    private int heightFont;

    public GradientColorButton(Context context) {
    super(context);
    }

    public GradientColorButton(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public GradientColorButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    //获取参数
    TypedArray a = context.obtainStyledAttributes(attrs,
    R.styleable.GradientColorButton, defStyleAttr, 0);

    text = a.getString(R.styleable.GradientColorButton_btnText);
    textColor = a.getColor(R.styleable.GradientColorButton_btnTextColor, Color.BLACK);
    textSize = a.getDimension(R.styleable.GradientColorButton_btnTextSize, 16);
    colorWidth = a.getDimension(R.styleable.GradientColorButton_colorWidth, 5);
    radius = a.getDimension(R.styleable.GradientColorButton_colorRadius, 100);


    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);

    //获取View的宽高
    width = getWidth();
    height = getHeight();

    //制作一个渐变效果的Bitmap
    createGradientBitmap();

    //初始化圆角配置
    initFilletConfiguration();

    //初始化白色Bitmap配置
    initWhiteBitmapConfiguration();

    //初始化文本配置
    initTextConfiguration();

    }


    /**
    * 创建渐变颜色的Bitmap
    */
    private void createGradientBitmap() {
    //创建存放渐变效果的bitmap
    bitmapColor = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvasColor = new Canvas(bitmapColor);
    LinearGradient backGradient = new LinearGradient(0, height, width, 0, new int[]{colorStart, color1, colorEnd}, null, Shader.TileMode.CLAMP);
    //绘画渐变效果
    paintColor = new Paint();
    paintColor.setShader(backGradient);
    canvasColor.drawRect(0, 0, width, height, paintColor);
    }


    /**
    * 初始化圆角配置
    */
    private void initFilletConfiguration() {
    //绘画出圆角渐变效果
    BitmapShader bitmapShaderColor = new BitmapShader(bitmapColor, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    paintFillet = new Paint();
    paintFillet.setAntiAlias(true);
    paintFillet.setShader(bitmapShaderColor);
    }

    /**
    * 初始化白色Bitmap配置
    */
    private void initWhiteBitmapConfiguration() {
    //创建存放白底的bitmap
    Bitmap bitmapWhite = Bitmap.createBitmap((int) (width - colorWidth * 2), (int) (height - colorWidth * 2), Bitmap.Config.RGB_565);
    bitmapWhite.eraseColor(Color.parseColor("#FFFFFF"));

    BitmapShader bitmapShaderWhite = new BitmapShader(bitmapWhite, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    // 初始化画笔
    paintWhite = new Paint();
    paintWhite.setAntiAlias(true);
    paintWhite.setShader(bitmapShaderWhite);
    }

    /**
    * 初始化文本配置
    */
    private void initTextConfiguration() {
    Rect rect = new Rect();
    paintText = new Paint();
    paintText.setAntiAlias(true);
    paintText.setColor(textColor);
    paintText.setTextSize(textSize);
    if (!TextUtils.isEmpty(text)) {
    paintText.getTextBounds(text, 0, text.length(), rect);
    widthFont = rect.width();//文本的宽度
    heightFont = rect.height();//文本的高度

    }
    }


    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    //将圆角渐变bitmap绘画到画布中
    canvas.drawRoundRect(new RectF(0, 0, width, height), radius, radius, paintFillet);
    // 将白色Bitmap绘制到画布上面
    canvas.drawRoundRect(new RectF(colorWidth, colorWidth, width - colorWidth, height - colorWidth), radius, radius, paintWhite);


    if (!TextUtils.isEmpty(text)) {
    canvas.drawText(text, (width - widthFont) / 2, (height + heightFont) / 2, paintText);
    }

    }
    }

        <com.jm.xpproject.GradientColorButton
    android:layout_width="120dp"
    android:layout_height="70dp"
    android:layout_margin="10dp"
    app:btnText="收藏"
    app:btnTextColor="#123456"
    app:btnTextSize="18sp"
    app:colorRadius="50dp"
    app:colorWidth="5dp" />


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

    Kafka QUICKSTART

    一. 安装和启动Kafka我本地机器已经安装CDH 6.3.1版本,此处省略安装和启动Kafka的步骤。Kafka版本:2.2.1ps -ef|grep '/libs/kafka.\{2,40\}.jar'复制1.1 Kafka的配置文件[root@hp1 c...
    继续阅读 »

    一. 安装和启动Kafka

    我本地机器已经安装CDH 6.3.1版本,此处省略安装和启动Kafka的步骤。

    Kafka版本:2.2.1

    ps -ef|grep '/libs/kafka.\{2,40\}.jar'

    1.1 Kafka的配置文件

    [root@hp1 config]# find / -name server.properties
    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/etc/kafka/conf.dist/server.properties

    常用的配置如下:

    #broker 的全局唯一编号,不能重复
    broker.id=0
    #删除 topic 功能使能
    delete.topic.enable=true
    #处理网络请求的线程数量
    num.network.threads=3
    #用来处理磁盘 IO 的线程数量
    num.io.threads=8
    #发送套接字的缓冲区大小
    socket.send.buffer.bytes=102400
    #接收套接字的缓冲区大小
    socket.receive.buffer.bytes=102400
    #请求套接字的缓冲区大小
    socket.request.max.bytes=104857600
    #kafka 运行日志存放的路径
    log.dirs=/opt/module/kafka/logs
    #topic 在当前 broker 上的分区个数
    num.partitions=1
    #用来恢复和清理 data 下数据的线程数量
    num.recovery.threads.per.data.dir=1
    #segment 文件保留的最长时间,超时将被删除
    log.retention.hours=168
    #配置连接 Zookeeper 集群地址
    zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181

    二. 创建一个主题来存储事件

    Kafka是一个分布式的事件流平台,可以让你跨多台机器读、写、存储和处理事件(在文档中也称为记录或消息)。

    示例事件包括支付交易、来自移动电话的地理位置更新、发货订单、来自物联网设备或医疗设备的传感器测量,等等。这些事件被组织并存储在主题中。很简单,一个主题类似于文件系统中的一个文件夹,事件就是该文件夹中的文件。

    2.1 创建主题

    所以在你写你的第一个事件之前,你必须创建一个主题。打开另一个终端会话并运行:

    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic first

    2.2 查看当前事件描述

    所有Kafka的命令行工具都有额外的选项:运行不带任何参数的Kafka -topics.sh命令来显示使用信息。例如,它还可以显示新主题的分区计数等详细信息:

    -- 查看主题topic的描述
    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic first
    -- 查看所有的topic的描述
    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --describe --zookeeper localhost:2181

    一个分区一个副本

    我们来看看创建多分区多副本

    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic first_1_1
    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 2 --topic first_1_2
    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 2 --partitions 2 --topic first_2_2
    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 3 --topic first_3_3

    本地测试只有3台broker,所以最多只能创建3个replication-factor

    2.3 删除主题

    需要 server.properties中设置 delete.topic.enable=true否则只是标记删除。 否则只是标记删除。

    cd /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin
    ./kafka-topics.sh --zookeeper localhost:2181 --delete --topic first

    三. 在主题中加入一些事件

    Kafka客户端通过网络与Kafka的代理通信,用于写(或读)事件。一旦收到,代理将以持久和容错的方式存储事件,只要您需要—甚至永远。

    运行控制台生成程序客户端,在主题中写入一些事件。默认情况下,您输入的每一行都将导致一个单独的事件被写入主题。

    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-console-producer.sh --broker-list 10.31.1.124:9092 --topic first

    四. 读事件

    打开另一个终端会话并运行控制台消费者客户端来读取你刚刚创建的事件:

    /opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-console-consumer.sh --from-beginning --bootstrap-server 10.31.1.124:9092 --topic first

    --from-beginning:会把主题中以往所有的数据都读取出来。

    您可以随时使用Ctrl-C停止客户端。

    您可以自由地进行试验:例如,切换回您的生产者终端(上一步)来编写额外的事件,并查看这些事件如何立即显示在您的消费者终端上。

    因为事件是持久性存储在Kafka中,它们可以被任意多的消费者读取。您可以通过再次打开另一个终端会话并再次运行前面的命令来轻松验证这一点。

    六. 用kafka connect导入/导出你的数据作为事件流

    您可能在现有系统(如关系数据库或传统消息传递系统)中有许多数据,以及许多已经使用这些系统的应用程序。Kafka Connect允许你不断地从外部系统获取数据到Kafka,反之亦然。因此,将现有系统与Kafka集成是非常容易的。为了使这个过程更容易,有数百个这样的连接器。

    看看Kafka Connect部分,了解更多关于如何不断地导入/导出你的数据到Kafka。

    七. 用kafka流处理你的事件

    一旦你的数据以事件的形式存储在Kafka中,你就可以用Java/Scala的Kafka Streams客户端库来处理这些数据。它允许你实现关键任务实时应用和微服务,其中输入和/或输出数据存储在Kafka主题。Kafka Streams结合了客户端编写和部署标准Java和Scala应用程序的简单性和Kafka服务器端集群技术的优点,使这些应用程序具有高度的可扩展性、弹性、容错性和分布式。该库支持一次处理、有状态操作和聚合、窗口、连接、基于事件时间的处理等等。

    本文转载自: https://www.jianshu.com/p/9d900eed46d7
    收起阅读 »

    kafka源码之旅------Kafka元数据管理

    我们往kafka集群中发送数据的时候,kafka是怎么感知到需要发送到哪一台节点中呢?其实这其中的奥秘就在kafka的Metadata中。这一篇我们就来看看kafka中的Metadata管理。我们来看看构建Kakfa中的代码片段:KafkaProducer构造...
    继续阅读 »

    我们往kafka集群中发送数据的时候,kafka是怎么感知到需要发送到哪一台节点中呢?其实这其中的奥秘就在kafka的Metadata中。这一篇我们就来看看kafka中的Metadata管理。

    我们来看看构建Kakfa中的代码片段:


    KafkaProducer构造函数代码片段

    从上面的代码片段可以看出,如果metadata变量不为空,直接赋值给KafkaProducer类成员变量metadata,否则需要新构建一个ProducerMetadata对象,然后根据用户传递的kafka集群服务器地址信息,构建Metadata类中cache成员变量的值,类型为MetadataCache。

    下面我们来分析一下Metadata这个类,看看里面都封装了哪些属性。

    refreshBackoffMs

    这个参数的作用是防止轮询的过于频繁。用于设置两次元数据刷新之间,最小有效时间间隔,超过这个设置的时间间隔,则这次元数据刷新就失效了。默认值是100ms。

    metadataExpireMs

    这个参数的含义是如果不刷新,元数据可以保持有效的最大时间。默认值是5分钟。

    updateVersion

    这个参数对应每一个元数据的响应。每一次自增+1。

    requestVersion

    这个参数对应每一次创建一个新的Topic。每一次自增+1。

    lastRefreshMs

    这个参数的含义是上一次更新元数据的时间。

    lastSuccessfulRefreshMs

    这个参数的含义是上一次成功更新元数据的时间。正常情况下每一次更新元数据都应该是成功的,那么lastRefreshMs和lastSuccessfulRefreshMs的值,应该是一样的。但是如果出现更新没有成功的情况,那么lastRefreshMs的值大于lastSuccessfulRefreshMs的值。

    fatalException

    这个参数的类型是kafka自己封装的KafkaException。继承了RuntimeException。如果在元数据相关的操作中抛出了这种异常,kafka将停止元数据相关的操作。

    invalidTopics

    这个参数的含义是存储非法的Topic元数据信息。

    unauthorizedTopics

    这个参数的含义是存储未授权的Topic元数据信息。

    cache

    这个参数的含义是在Metadata类的内部构建一个MetadataCache对象,把元数据信息缓存起来,方便在集群中进行快速的数据获取。

    needFullUpdate

    这个参数的含义是Metadata是否需要全部更新。

    needPartialUpdate

    这个参数的含义是Metadata是否需要部分更新。

    clusterResourceListeners

    这个参数的含义是抽象了一个接收元数据更新集群资源的监听器集合。

    lastSeenLeaderEpochs

    这个参数是一个Map结构,映射的是TopicPartition和Integer之间的关系。也就是说某一个主题分区,它的主分区上一次更新的版本号是多少,在这个Map结构中存储。真正构建Metadata对象的时候,实现类是HashMap。

    接下来我们来看看MetadataCache这个类,看看里面封装了哪些属性。这个类存在是kafka一种缓存的思想,把一些重要的属性用缓存来保存起来,提高Metadata的读取效率。

    clusterId

    这个参数用来标识整个kafka集群。

    nodes

    这个参数是一个Map类型,用来映射kafka集群中节点编号和节点的关系。

    unauthorizedTopics

    这个参数是一个Set类型,用来存储未授权的Topic集合。

    invalidTopics

    这个参数是一个Set类型,用来存储无效的Topic集合。

    internalTopics

    这个参数是一个Set类型,用来存储kafka内部的Topic集合,例如__consumer_offsets。

    controller

    这个参数是表示kafka controller所在broker。

    metadataByPartition

    这个参数是Map类型,用来存储分区和分区对应的元数据的映射关系。

    clusterInstance

    这个参数抽象了集群中的数据,我们接下来进行重点分析。

    Cluster类是封装在MetadataCache中的,用来表示kafka的集群信息。

    nodes

    这个参数封装了集群中节点信息列表。

    unauthorizedTopics

    这个参数是一个Set类型,用来存储未授权的Topic集合。

    invalidTopics

    这个参数是一个Set类型,用来存储无效的Topic集合。

    internalTopics

    这个参数是一个Set类型,用来存储kafka内部的Topic集合,例如__consumer_offsets。

    partitionsByTopicPartition

    这个参数记录了TopicPartition与PartitionInfo的映射关系。

    partitionsByTopic

    这个参数记录了Topic名称与PartitionInfo的映射关系。可以按照Topic名称查询其中全部分区的详细信息。

    availablePartitionsByTopic

    这个参数记录了Topic与PartitionInfo的映射关系。这里的List<PartitionInfo>中存放的分区必须是有Leader副本的Partition,而partitionsByTopic中记录的分区则不一定有Leader副本,因为某些中间状态,例如Leader副本所在节点,发生了节点下线,进而触发了Leader副本的选举,在这一时刻分区不一定有Leader副本。

    partitionsByNode

    这个参数记录了Node与PartitionInfo的映射关系。可以按照节点Id查询该节点上分布的全部分区的详细信息。

    nodesById

    这个参数记录了BrokerId与Node节点之间的映射关系。方便使用BrokerId进行索引,可以根据BrokerId得到关联的Node节点信息。

    clusterResource

    这个参数是ClusterResource类型,这个类只是封装了一个clusterId成员属性,用于区分每一个kafka的集群。

    我们再来看看Node这个类。Node这个类是对kafka集群中一个物理服务器的抽象,它所拥有的属性如下所示。

    id

    这个参数记录了kafka集群中的服务器编号,是我们配置参数的时候指定的。

    host

    这个参数记录了服务器的主机名。

    port

    这个参数记录了服务器的端口号。

    rack

    这个参数记录了服务器所属的机架。

    我们再来看看TopicPartition这个类。这个类里面封装了主题,以及对应的一个分区。它所拥有的属性如下所示:

    partition

    这个参数记录了一个分区编号。

    topic

    这个参数记录了主题名称。

    我们再来看看PartitionInfo这个类。这个类抽象了一个分区的详细信息,它所拥有的属性如下所示:

    topic

    这个参数记录了主题名称,表示这个分区是属于哪一个主题的。

    partition

    这个参数记录了分区编号。

    leader

    这个参数记录了分区主副本在哪台服务器上。

    replicas

    这个参数是Node类型的数组,记录了这个分区所有副本所在服务器。

    inSyncReplicas

    这个参数是Node类型的数组,记录了这个分区同步正常的副本所在服务器。

    offlineReplicas

    这个参数是Node类型的数组,记录了这个分区同步不正常的副本所在服务器。

    本文转载自:  https://www.jianshu.com/p/61a58cba354f

    收起阅读 »

    QQ被盗,发给暗恋女生的第一条消息竟是h图

    昨日凌晨,“QQ 盗.号”这一词条登上微博热搜,直至第二天中午都还在热搜榜上待着。看到这条热搜后,小编火速登录 QQ 查看自己是否也被盗号,所幸没有遭殃。但是我的室友就没这么幸运了,暗恋了三年的妹子,军训时候加别人QQ,现在快毕业了还不敢跟别人说一句话,这下好...
    继续阅读 »

    昨日凌晨,“QQ 盗.号”这一词条登上微博热搜,直至第二天中午都还在热搜榜上待着。


    看到这条热搜后,小编火速登录 QQ 查看自己是否也被盗号,所幸没有遭殃。但是我的室友就没这么幸运了,暗恋了三年的妹子,军训时候加别人QQ,现在快毕业了还不敢跟别人说一句话,这下好了,QQ被.盗,别人给他暗恋对象发H图,还没办法撤回,不是我们拦着真要出人命了。他说这个事件对他影响太大,盗贼不但盗取了他的账号,还玷污了他的爱情。

    我们也发现沉默多年的QQ列表里的许多群组突然“活跃”起来。点进去一看,有些动作快的群主已撤回成员消息,但也有些群还未及时处理:好几个群成员在凌晨时候突然发黄图,甚至还发在了群成员 599 人、其中还有十几位老师的大学学院群里……借用网友的一句话:我这替人尴尬的老毛病又犯了。


    1.大型社死现场

    既然这次 QQ 盗号事件能登上热搜且热度居高不下,说明波及范围不小,从众多网友对此的反馈上也证明了这一点——一整个就是大型社死现场

    • “救命,我们有人在先进预备党员的群里发黄图,结果直接被踢出群了,取消评党员的资格。”

    • “我两个朋友被盗,说就是在电脑上面登了一下 QQ 而已,然后我朋友的同学他们也被盗,发了一些 yellow 图带网址那种。”

    • “我真的会谢,半夜四点群发淫.秽图片,往我朋友同事长辈甚至工作群发,登录保护保了个寂寞,名营损失费怎么说?腾讯你欠我的拿什么还!”

    • “主要是这个范围和受害人群不确定,我有好多群都有人被盗号了,也不知道怎么做到的,针对什么群体的。”

    事发过后,很快就有网友进行提醒,为了以防万一不要查看那些淫.秽图片上的网址

    • “被盗的 QQ 号会发送附着链接的淫.秽图片消息,当受害者将所附链接用浏览器打开的时候,犯罪分子的电脑可能启动脚本进一步盗取受害者信息。如果不幸中招了,大家要谨慎处理。”

    • “千万不要在 QQ 登陆的情况下点击陌生的链接,也不要点开 QQ 邮箱里陌生的邮件。”

    与此同时,发现被盗.号的人也在第一时间忙着找回 QQ 并修改密码,希望尽快出面解释一下这尴尬的场景、挽回一下即将崩塌的人设。但许多人无奈地发现:我的号被封了,没法证明我的清白了啊!




    2.腾讯回应:“系用户扫描过不法分子伪造的游戏登录二维码”

    结合众多网友并周围人的反馈来看,本次 QQ 盗.号的波及范围显然较大,许多人在冷静之后也开始猜测其背后原因。虽然各人看法不一,但网传的主要有三种:

    • 腾讯内部协议被偷

    这一说法主要是有网友曝光了一张聊天内容的截图,其中讲到腾讯内部协议被偷导致可随机生成 key,无需知道密码即可盗号。


    • 与学习通泄露数据有关

    考虑到这次 QQ 盗.号事件与上周学习通被曝数据泄露的时间较为接近,有部分网友怀疑这两起事件可能有关,即黑客通过学习通撞库以盗.取 QQ 号。

    • 误点了不安全链接或误扫了二维码等

    还有一种最常见的方式,即用户误点了不安全链接或误扫了二维码等,导致授权了 QQ 登入信息。

    面对网络上逐渐发酵的负面言论和用户投诉,腾讯 QQ 官方在昨日中午针对这起事件给出了调查结果并对用户致歉:“系用户扫描过不法分子伪造的游戏登录二维码并授权登录,该登录行为被黑.产团伙劫.持并记录,随后被不法分子利用发送不良图片广告。”


    此外,也有记者以用户身份向学习通客服询问本次事件,而学习通方面否认 QQ 盗.号与其有关:尚未发现明确的用户信息泄露证据,已经报案,公.安机关已介入调查。

    那么,你是否在本次 QQ 盗.号的风波中遭殃,或是目睹了其他人的“社死”现场?

    参考链接:

    收起阅读 »

    一定要优雅,高端前端程序员都应该具备的基本素养

    近来看到很多公司裁员,忽然惊醒,之前是站在项目角度考虑问题,却没站在咱们程序员本身看待问题,险些酿成大错,如果人人都能做到把项目维护得井井有条,无论什么人都能看明白都能快速接手,那咱们的竞争力在哪里呢?这个时候我再看项目中那些被我天天骂的代码,顿时心中就无限景...
    继续阅读 »

    近来看到很多公司裁员,忽然惊醒,之前是站在项目角度考虑问题,却没站在咱们程序员本身看待问题,险些酿成大错,如果人人都能做到把项目维护得井井有条,无论什么人都能看明白都能快速接手,那咱们的竞争力在哪里呢?这个时候我再看项目中那些被我天天骂的代码,顿时心中就无限景仰起来,原来屎山才是真能能够保护我们的东西,哪有什么岁月静好,只是有人替你负屎前行罢了


    为了能让更多人认识到这一点,站在前端的角度上,我在仔细拜读了项目中的那些暗藏玄机的代码后,决定写下此文,由于本人功力尚浅,且之前一直走在错误的道路上,所以本文在真正的高手看来可能有些班门弄斧,在此献丑了🐶



    用 TypeScript,但不完全用


    TypeScript大行其道,在每个团队中,总有那么些个宵小之辈想尽一切办法在项目里引入 ts,这种行为严重阻碍了屎山的成长速度,但同是打工人我们也不好阻止,不过就算如此,也无法阻止我们行使正义


    众所周知,TypeScript 别名 AnyScript,很显然,这就是TypeScript创始人Anders Hejlsberg给我们留下的暗示,我们有理由相信AnyScript 才是他真正的目的

    const list: any = []
    const obj: any = {}
    const a: any = 1

    引入了 ts的项目,由于是在原可运行代码的基础上额外添加了类型注释,所以代码体积毫无疑问会增大,有调查显示,可能会增加 30%的代码量,如果充分发挥 AnyScript 的宗旨,意味着你很轻松地就让代码增加了 30% 毫无用处但也挑不出啥毛病的代码,这些代码甚至还会增加项目的编译时间(毕竟增加了ts校验和移除的成本嘛)


    你不仅能让自己写的代码用上 AnyScript,甚至还可以给那些支持 ts 的第三方框架/库一个大嘴巴子

    export default defineComponent({
    props: {
    // 现在 data 是 any 类型的啦
    data: {
    type: Number as PropType<any>,
    },
    },
    setup(_, { emit }) {
    // 现在 props 是 any 类型的啦
    const props: any = _
    ...
    }
    })

    当然了,全屏 any可能还是有点明显了,所以你可以适当地给部分变量加上具体类型,但是加上类型不意味着必须要正确使用

    const obj: number[] = []
    // ...
    // 虽然 obj 是个 number[],但为了实现业务,就得塞入一些不是 number 的类型,我也不想的啊是不是
    // 至于编辑器会划红线报错?那是小问题,不用管它,别人一打开这个项目就是满屏的红线,想想就激动
    obj.push('2')
    obj.push([3])

    命名应该更自由

    命名一直是个困扰很多程序员的问题,究其原因,我们总想给变量找个能够很好表达意思的名称,这样一来代码的可阅读性就高了,但现在我们知道,这并不是件好事,所以我们应该放纵自我,既摆脱了命名困难症,又加速了屎山的堆积进度

    const a1 = {}
    const a2 = {}
    const a3 = 2
    const p = 1

    我必须强调一点,命名不仅是变量命名,还包含文件名、类名、组件名等,这些都是我们可以发挥的地方,例如类名

    <div class="box">
    <div class="box1"></div>
    <div class="box2"></div>
    <div>
    <div class="box3"></div>

    乍一看似乎没啥毛病,要说有毛病似乎也不值当单独挑出来说,没错,要的就是这个效果,让人单看一段代码不好说什么,但是如果积少成多,整个项目都是 box呢?全局搜索都给你废了!如果你某些组件再一不小心没用 scoped 呢?稍不留意就不知道把什么组件的样式给改了,想想就美得很


    关于 css我还想多说一点,鉴于其灵活性,我们还可以做得更多,总有人说什么 BEMBEM的,他们敢用我们就敢写这样的代码

    &-card {
    &-btn {
    &_link {
    &--right {
    }
    }
    &-nodata {
    &_link {
    &--replay {
    &--create {}
    }
    }
    }
    }
    &-desc {}
    }

    好了,现在请在几百行(关于这一点下一节会说到)这种格式的代码里找出类名 .xxx__item_current.mod-xxx__link 对应的样式吧


    代码一定要长


    屎山一定是够高够深的,这就要求我们的代码应该是够长够多的


    大到一个文件的长度,小到一个类、一个函数,甚至是一个 if 的条件体,都是我们自由发挥的好地方。


    什么单文件最好不超过 400行,什么一个函数不超过 100行,简直就是毒瘤,


    1.jpg


    所以这就要求我们要具备将十行代码就能解决的事情写成一百行的能力,最好能给人一种多即是少的感觉

    data === 1
    ? 'img'
    : data === 2
    ? 'video'
    : data === 3
    ? 'text'
    : data === 4
    ? 'picture'
    : data === 5
    ? 'miniApp'

    三元表达式可以优雅地表达逻辑,像诗一样,虽然这段代码看起来比较多,但逻辑就是这么多,我还专门用了三元表达式优化,不能怪我是不是?什么map映射枚举优化听都没听过

    你也可以选择其他一些比较容易实现的思路,例如,多写一些废话

    if (a > 10) {
    // 虽然下面几个 if 中对于 a 的判断毫无用处,但不仔细看谁能看出来呢?看出来了也不好说什么,毕竟也没啥错
    // 除此之外,多级 if 嵌套也是堆屎山的一个小技巧,什么提前 return 不是太明白
    if (a > 5) {
    if (a > 3 && b) {

    }
    }
    if (a > 4) {

    }
    }

    除此之外,你还可以写一些中规中矩的方法,但重点在于这些方法根本就没用到,这种发挥的地方就更多了,简直就是扩充代码体积的利器,毕竟单看这些方法没啥毛病,但谁能想到根本就用不到呢?就算有人怀疑了,但你猜他敢随便从运行得好好的业务项目里删掉一些没啥错的代码吗?


    组件、方法多多滴耦合


    为了避免其他人复用我的方法或组件,那么在写方法或组件的时候,一定要尽可能耦合,提升复用的门槛


    例如明明可以通过 Props传参解决的事情,我偏要从全局状态里取,例如vuex,独一份的全局数据,想传参就得改 store数据,但你猜你改的时候会不会影响到其他某个页面某个组件的正常使用呢?如果你用了,那你就可能导致意料之外的问题,如果你不用你就得自己重写一个组件


    组件不需要传参?没关系,我直接把组件的内部变量给挂到全局状态上去,虽然这些内部变量确实只有某一个组件在用,但我挂到全局状态也没啥错啊是不是


    嘿,明明一个组件就能解决的事情,现在有了倆,后面还可能有仨,这代码量不就上来了吗?


    方法也是如此,明明可以抽取参数,遵循函数式编程理念,我偏要跟外部变量产生关联

    // 首先这个命名就很契合上面说的自由命名法
    function fn1() {
    // ...
    // fn1 的逻辑比较长,且解决的是通用问题,
    // 但 myObj 偏偏是一个外部变量,这下看你怎么复用
    window.myObj.name = 'otherName'
    window.myObj.children.push({ id: window.myObj.children.length })
    // ...
    }

    魔术字符串是个好东西

    实际上,据我观察,排除掉某些居心不轨的人之外,大部分人还是比较喜欢写魔术字符串的,这让我很欣慰,看着满屏的不知道从哪里冒出来也不知道代表着什么的硬编码字符串,让人很有安全感

    if (a === 'prepare') {
    const data = localStorage.getItem('HOME-show_guide')
    // ...
    } else if (a === 'head' && b === 'repeating-error') {
    switch(c) {
    case 'pic':
    // ...
    break
    case 'inDrawer':
    // ...
    break
    }
    }

    基于此,我们还可以做得更多,比如用变量拼接魔术字符串,debug的时候直接废掉全局搜索

    if (a === query.name + '_head') {

    }

    大家都是中国人,为什么不试试汉字呢?

    if (data === '正常') {

    } else if (data === '错误') {

    } else if (data === '通过') {

    }

    轮子就得自己造才舒心


    众所周知,造轮子可以显著提升我们程序员的技术水平,另外由于轮子我们已经自己造了,所以减少了对社区的依赖,同时又增加了项目体积,有力地推动了屎山的成长进程,可以说是一鱼两吃了


    例如我们可能经常在项目中使用到时间格式化的方法,一般人都是直接引入 dayjs完事,太肤浅了,我们应该自己实现,例如,将字符串格式日期格式化为时间戳

    function format(str1: any, str2: any) {
    const num1 = new Date(str1).getTime()
    const num2 = new Date(str2).getTime()
    return (num2 - num1) / 1000
    }

    多么精简多么优雅,至于你说的什么格式校验什么 safari下日期字符串的特殊处理,等遇到了再说嘛,就算是dayjs不也是经过了多次 fixbug才走到今天的嘛,多一些宽松和耐心好不好啦


    如果你觉得仅仅是 dayjs这种小打小闹难以让你充分发挥,你甚至可以造个 vuexvue官网上写明了eventBus可以充当全局状态管理的,所以我们完全可以自己来嘛,这里就不举例了,这是自由发挥的地方,就不局限大家的思路了


    借助社区的力量-轮子还是别人的好


    考虑到大家都只是混口饭吃而已,凡事都造轮子未免有些强人所难,所以我们可以尝试走向另外一个极端——凡事都用轮子解决


    判断某个变量是字符串还是对象,kind-of拿来吧你;获取某个对象的 keyobject-keys拿来吧你;获取屏幕尺寸,vue-screen-size拿来吧你……等等,就不一一列举了,需要大家自己去发现


    先甭管实际场景是不是真的需要这些库,也甭管是不是杀鸡用牛刀,要是大家听都没听过的轮子那就更好了,这样才能彰显你的见多识广,总之能解决问题的轮子就是好问题,


    在此我得特别提点一下 lodash,这可是解决很多问题的利器,但是别下载错了,得是 commonjs版本的那个,量大管饱还正宗,es module版本是不行滴,太小家子气


    import _ from 'lodash'

    多尝试不同的方式来解决相同的问题


    世界上的路有很多,很多路都能通往同一个目的地,但大多数人庸庸碌碌,只知道沿着前人的脚步,没有自己的思想,别人说啥就是啥,这种行为对于我们程序员这种高端的职业来说,坏处很大,任何一个有远大理想的程序员都应该避免


    落到实际上来,就是尝试使用不同的技术和方案解决相同的问题

    搞个css模块化方案,什么BEMOOCSSCSS ModulesCSS-in-JS 都在项目里引入,紧跟潮流扩展视野

    vue项目只用 template?逊啦你,render渲染搞起来

    之前看过什么前端依赖注入什么反射的文章,虽然对于绝大多数业务项目而言都是水土不服,但问题不大,能跑起来就行,引入引入

    还有那什么 rxjs,人家都说好,虽然我也不知道好在哪里,但胜在门槛高一般人搞不清楚所以得试试

    Pinia 是个好东西,什么,我们项目里已经有 vuex了?out啦,人家官网说了 vue2也可以用,我们一定要试试,紧跟社区潮流嘛,一个项目里有两套状态管理有什么值得大惊小怪的!


    做好自己,莫管他人闲事

    看过一个小故事,有人问一个年纪很大的老爷爷的长寿秘诀是什么,老爷爷说是从来不管闲事

    这个故事对我们程序员来说也很有启发,写好你自己的代码,不要去关心别人能不能看得懂,不要去关心别人是不是会掉进你写的坑里

    mounted() {
    setTimeout(() => {
    const width = this.$refs.box.offsetWidth
    const itemWidth = 50
    // ...
    }, 200)
    }

    例如对于上述代码,为什么要在 mounted里写个 setTimeout呢?为什么这个 setTimeout的时间是 200呢?可能是因为 box 这个元素大概会在 mounted之后的 200ms左右接口返回数据就有内容了,就可以测量其宽度进行其他一系列的逻辑了,至于有没有可能因为网络等原因超过 200ms还是没有内容呢?这些不需要关心,你只要保证在你开发的时候 200ms这个时间是没问题的就行了;
    itemWidth代表另外一个元素的宽度,在你写代码的时候,这个元素宽度就是 50,所以没必要即时测量,你直接写死了,至于后面其他人会不会改变这个元素的宽度导致你这里不准了,这就不是你要考虑的事情了,你开发的时候确实没问题,其他人搞出来问题其他人负责就行,管你啥事呢?


    代码自解释


    高端的程序员,往往采用最朴素的编码方式,高手从来不写注释,因为他们写的代码都是自解释的,什么叫自解释?就是你看代码就跟看注释一样,所以不需要注释


    我觉得很有道理,代码都在那里搁着了,逻辑写得清清楚楚,为啥还要写注释呢,直接看代码不就行了吗?


    乍一看,似乎这一条有点阻碍堆屎山的进程,实则不然


    一堆注定要被迭代无数版、被无数人修改、传承多年的代码,其必定是逻辑错综复杂,难免存在一些不可名状的让人说不清道不明的逻辑,没有注释的加成,这些逻辑大概率要永远成为黑洞了,所有人看到都得绕着走,相当于是围绕着这些黑洞额外搭起了一套逻辑,这代码体积和复杂度不就上来了吗?


    如果你实在手痒,倒也可以写点注释,我这里透露一个既能让你写写注释过过瘾又能为堆屎山加一把力的方法,那就是:在注释里撒谎!


    没错,谁说注释只能写对的?我理解不够,所以注释写得不太对有什么奇怪的吗?我又没保证注释一定是对的,也没逼着你看注释,所以你看注释结果被注释误导写了个bug,这凭啥怪我啊

    // 计算 data 是否可用
    //(实际上,这个方法的作用是计算 data 是否 不可用)
    function isDisabledData(data: any) {
    // ...
    }

    上述这个例子只能说是小试牛刀,毕竟多调试一下很容易被发现的,但就算被发现了,大家也只会觉得你只是个小粗心鬼罢了,怎么好责怪你呢,这也算是给其他人的一个小惊喜了,况且,万一真有人不管不顾就信了,那你就赚大了


    编译问题坚决不改


    为了阻碍屎山的成长速度,有些阴险的家伙总想在各种层面上加以限制,例如加各种lint,在编译的时候,命令行中就会告诉你你哪些地方没有按照规则来,但大部分是 waring 级别的,即你不改项目也能正常运行,这就是我们的突破点了。


    尽管按照你的想法去写代码,lint的事情不要去管,waring报错就当没看到,又不是不能用?在这种情况下,如果有人不小心弄了个 error级别的错误,他面对的就是从好几屏的 warning 中找他的那个 error 的场景了,这就相当于是提前跟屎山来了一次面对面的拥抱


    根据破窗理论,这种行为将会影响到越来越多的人,大家都将心照不宣地视 warning于无物(从好几屏的 warning中找到自己的那个实在是太麻烦了),所谓的 lint就成了笑话


    小结


    一座历久弥香的屎山,必定是需要经过时间的沉淀和无数人的操练才能最终成型,这需要我们所有人的努力,多年之后,当你看到你曾经参与堆砌的屎山中道崩殂轰然倒塌的时候,你就算是真的领悟了我们程序员所掌控的恐怖实力!🐶


    链接:https://juejin.cn/post/7107119166989336583
    收起阅读 »

    Android实现消息总线的几种方式,你都会吗?

    Android中消息总线的几种实现方式前言消息总线又叫事件总线,为什么我们需要一个消息总线呢?是因为随着项目变大,页面变多,我们可能出现跨页面、跨组件、跨线程、跨进程传递消息与数据,为了更方便的直接通知到指定的页面实现具体的逻辑,我们需要消息总线来实现。从最基...
    继续阅读 »

    Android中消息总线的几种实现方式

    前言

    消息总线又叫事件总线,为什么我们需要一个消息总线呢?是因为随着项目变大,页面变多,我们可能出现跨页面、跨组件、跨线程、跨进程传递消息与数据,为了更方便的直接通知到指定的页面实现具体的逻辑,我们需要消息总线来实现。

    从最基本的 BroadcastReceiver 到 EventBus 再到RxBus ,后来官方出了AndroidX jetpack 我们开始使用LiveDataBus,最后到Kotlin的流行出来了FlowBus。我们看看他们是怎么一步一步演变的。

    一、BroadcastReceiver 广播

    我们再初入 Android 的时候都应该学过广播接收者,分为静态广播和动态注册广播,在高版本的 Android 中限制了我们一些静态广播的使用,不过我们还是能通过动态注册的方式获取一些系统的状态改变。像常用的电量变化、网络状态变化、短信发送接收的状态等等。

    比如网络变化的监听:

        IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
    application.getApplicationContext().registerReceiver(InstanceHolder.INSTANCE, intentFilter);

    在消息中线中,我们可以使用本地广播来实现 LocalBroadcastManager 消息的通知。

        LocalBroadcastManager mLocalBroadcastManager = LocalBroadcastManager.getInstance(mContext);

    BroadcastReceiver mLoginReceiver = new LoginSuccessReceiver();
    mLocalBroadcastManager.registerReceiver(mLoginReceiver, new IntentFilter(Constants.ACTION_LOGIN_SUCCESS));

    private class LoginSuccessReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
    //刷新Home界面
    refreshHomePage();

    //刷新未读信息
    requestUnreadNum();
    }
    }

    //记得要解绑对应的接收器
    mLocalBroadcastManager.unregisterReceiver(mLoginReceiver);

    这样就可以实现一个消息通知了。相比 EventBus 它的性能和空间的消耗都是较大的,并且只能固定在主线程运行。

    二、EventBus

    EventBus最大的特点就是简洁、解耦,可以直接传递我们自定义的消息Message。EventBus简化了应用程序内各组件间、组件与后台线程间的通信。记得2015年左右是非常火爆的。

    EventBus的调度灵活,不依赖于 Context,使用时无需像广播一样关注 Context 的注入与传递。可继承、优先级、粘滞,是 EventBus 比之于广播的优势。几乎可以满足我们全部的需求。

    最初的EventBus其实就是一个方法的集合与查找,核心是通过register方法把带有@Subscrib注解的方法和参数之类的东西全部放入一个List集合,然后通过post方法去这个list循环查找到符合条件的方法去执行。

    如何使用EventBus,一共分5步:

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

    EventBus.getDefault().register(MainActivity.this); //1.注册广播
    }
      @Override
    protected void onDestroy() {
    super.onDestroy();
    EventBus.getDefault().unregister(MainActivity.this); //2.解注册广播
    }
    /**
    * 3.传递什么类型的。定义一个消息类
    */
    public class MessageEvent {
    public String name;

    public MessageEvent(String name) {
    this.name = name;
    }
    }
        @OnClick({R.id.bt_eventbus_send_main, R.id.bt_eventbus_send_sticky})
    public void onClick(View view) {
    switch (view.getId()) {
    case R.id.bt_eventbus_send_main:
    //4.发送消息
    EventBus.getDefault().post(new MessageEvent("我是主页面发送过来的消息"));
    finish();
    break;
    }
    }
       /**
    * 5.接受到消息。需要注解
    *
    * @param event
    */
    @Subscribe(threadMode = ThreadMode.MAIN) //主线程执行
    public void MessageEventBus(MessageEvent event) {
    //5。显示接受到的消息
    mTvEventbusResult.setText(event.name);
    }

    EventBus的性能开销其实不大,EventBus2.4.0 版是利用反射来实现的,后来改成 APT 实现之后会好很多。主要问题是需要定义很多的消息对象,消息太多之后就感觉管理起来很麻烦。当消息太多之后容器内部的查找会出现性能瓶颈。

    就算如此 EventBus 也是值得大家使用的。

    三、RxBus

    RxBus是基于RxJava实现的,强大是强大,但是学习成本比较高,需要额外导入RxJava RxAndroid等库,这些库体积还是较大的。可以实现异步的消息等。

    本身的实现是很简单的:

    public class RxBus {
    private volatile static RxBus mDefaultInstance;
    private final Subject<Object> mBus;

    private RxBus() {
    mBus = PublishSubject.create().toSerialized();
    }

    public static RxBus getInstance() {
    if (mDefaultInstance == null) {
    synchronized (RxBus.class) {
    if (mDefaultInstance == null) {
    mDefaultInstance = new RxBus();
    }
    }
    }
    return mDefaultInstance;
    }

    /**
    * 发送事件
    */
    public void post(Object event) {
    mBus.onNext(event);
    }

    /**
    * 根据传递的 eventType 类型返回特定类型(eventType)的 被观察者
    */
    public <T> Observable<T> toObservable(final Class<T> eventType) {
    return mBus.ofType(eventType);
    }

    /**
    * 判断是否有订阅者
    */
    public boolean hasObservers() {
    return mBus.hasObservers();
    }

    public void reset() {
    mDefaultInstance = null;
    }

    }

    定义消息对象:

    public class MsgEvent {
    private String msg;

    public MsgEvent(String msg) {
    this.msg = msg;
    }

    public String getMsg() {
    return msg;
    }

    public void setMsg(String msg) {
    this.msg = msg;
    }
    }

    发送与接收:

    RxBus.getInstance().toObservable(MsgEvent.class).subscribe(new Observer<MsgEvent>() {
    @Override
    public void onSubscribe(Disposable d) {

    }

    @Override
    public void onNext(MsgEvent msgEvent) {
    //处理事件
    }

    @Override
    public void onError(Throwable e) {

    }

    @Override
    public void onComplete() {

    }
    });


    RxBus.getInstance().post(new MsgEvent("Java"));

    缺点是容易内存泄露,我们需要使用rxlifecycle 或者使用CompositeDisposable 自己对生命周期进行处理解绑。

    四、LiveDataBus

    官方出了AndroidX jetpack 内部包含LiveData,它可以感知并遵循Activity、Fragment或Service等组件的生命周期。

    为什么要使用LiveDataBus,正是基于LiveData对组件生命周期可感知的特点,因此可以做到仅在组件处于生命周期的激活状态时才更新UI数据。

    一个简单的LiveDataBus的实现:

    public final class LiveDataBus {

    private final Map<String, BusMutableLiveData<Object>> bus;

    private LiveDataBus() {
    bus = new HashMap<>();
    }

    private static class SingletonHolder {
    private static final LiveDataBus DEFAULT_BUS = new LiveDataBus();
    }

    public static LiveDataBus get() {
    return SingletonHolder.DEFAULT_BUS;
    }

    public <T> MutableLiveData<T> with(String key, Class<T> type) {
    if (!bus.containsKey(key)) {
    bus.put(key, new BusMutableLiveData<>());
    }
    return (MutableLiveData<T>) bus.get(key);
    }

    public MutableLiveData<Object> with(String key) {
    return with(key, Object.class);
    }

    private static class ObserverWrapper<T> implements Observer<T> {

    private Observer<T> observer;

    public ObserverWrapper(Observer<T> observer) {
    this.observer = observer;
    }

    @Override
    public void onChanged(@Nullable T t) {
    if (observer != null) {
    if (isCallOnObserve()) {
    return;
    }
    observer.onChanged(t);
    }
    }

    private boolean isCallOnObserve() {
    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
    if (stackTrace != null && stackTrace.length > 0) {
    for (StackTraceElement element : stackTrace) {
    if ("android.arch.lifecycle.LiveData".equals(element.getClassName()) &&
    "observeForever".equals(element.getMethodName())) {
    return true;
    }
    }
    }
    return false;
    }
    }

    private static class BusMutableLiveData<T> extends MutableLiveData<T> {

    private Map<Observer, Observer> observerMap = new HashMap<>();

    @Override
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {
    super.observe(owner, observer);
    try {
    hook(observer);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    @Override
    public void observeForever(@NonNull Observer<T> observer) {
    if (!observerMap.containsKey(observer)) {
    observerMap.put(observer, new ObserverWrapper(observer));
    }
    super.observeForever(observerMap.get(observer));
    }

    @Override
    public void removeObserver(@NonNull Observer<T> observer) {
    Observer realObserver = null;
    if (observerMap.containsKey(observer)) {
    realObserver = observerMap.remove(observer);
    } else {
    realObserver = observer;
    }
    super.removeObserver(realObserver);
    }

    private void hook(@NonNull Observer<T> observer) throws Exception {
    //get wrapper's version
    Class<LiveData> classLiveData = LiveData.class;
    Field fieldObservers = classLiveData.getDeclaredField("mObservers");
    fieldObservers.setAccessible(true);
    Object objectObservers = fieldObservers.get(this);
    Class<?> classObservers = objectObservers.getClass();
    Method methodGet = classObservers.getDeclaredMethod("get", Object.class);
    methodGet.setAccessible(true);
    Object objectWrapperEntry = methodGet.invoke(objectObservers, observer);
    Object objectWrapper = null;
    if (objectWrapperEntry instanceof Map.Entry) {
    objectWrapper = ((Map.Entry) objectWrapperEntry).getValue();
    }
    if (objectWrapper == null) {
    throw new NullPointerException("Wrapper can not be bull!");
    }
    Class<?> classObserverWrapper = objectWrapper.getClass().getSuperclass();
    Field fieldLastVersion = classObserverWrapper.getDeclaredField("mLastVersion");
    fieldLastVersion.setAccessible(true);
    //get livedata's version
    Field fieldVersion = classLiveData.getDeclaredField("mVersion");
    fieldVersion.setAccessible(true);
    Object objectVersion = fieldVersion.get(this);
    //set wrapper's version
    fieldLastVersion.set(objectWrapper, objectVersion);
    }
    }
    }

    注册与发送:

    LiveDataBus.get()
    .with("key_test", String.class)
    .observe(this, new Observer<String>() {
    @Override
    public void onChanged(@Nullable String s) {
    }
    });

    LiveDataBus.get().with("key_test").setValue(s);

    LiveDataBus已经算是很好用的,自动注册解绑,根据Key传递泛型T对象,容易查找对应的接收者,也可以实现可见的触发和直接触发,可以实现跨进程,

    LiveData有几点不足,只能在主线程更新数据,操作符无法转换数据,基于 Android Api 实现的,换一个平台无法适应,基于这几点又开发出了FlowBus。

    五、FlowBus

    很多人都说Flow 的出现导致 LiveData 没那么重要了,就是因为 LiveData 的场景 都可以使用 Flow 平替了,还能更为的强大和灵活。

    StateFlow 可以 替代ViewModel中传递数据,SharedFlow 可以实现事件总线。(这两者的异同如果大家有兴趣,我可以单独开一篇讲下)。

    SharedFlow 就是一种热流,可以实现一对多的关系,其构造方法支持天然支持普通的消息发送与粘性的消息发送。一般我们FlowBus都是基于 SharedFlow 来实现:

    object FlowBus {
    private val busMap = mutableMapOf<String, EventBus<*>>()
    private val busStickMap = mutableMapOf<String, StickEventBus<*>>()

    @Synchronized
    fun <T> with(key: String): EventBus<T> {
    var eventBus = busMap[key]
    if (eventBus == null) {
    eventBus = EventBus<T>(key)
    busMap[key] = eventBus
    }
    return eventBus as EventBus<T>
    }

    @Synchronized
    fun <T> withStick(key: String): StickEventBus<T> {
    var eventBus = busStickMap[key]
    if (eventBus == null) {
    eventBus = StickEventBus<T>(key)
    busStickMap[key] = eventBus
    }
    return eventBus as StickEventBus<T>
    }

    //真正实现类
    open class EventBus<T>(private val key: String) : LifecycleObserver {

    //私有对象用于发送消息
    private val _events: MutableSharedFlow<T> by lazy {
    obtainEvent()
    }

    //暴露的公有对象用于接收消息
    val events = _events.asSharedFlow()

    open fun obtainEvent(): MutableSharedFlow<T> = MutableSharedFlow(0, 1, BufferOverflow.DROP_OLDEST)

    //主线程接收数据
    fun register(lifecycleOwner: LifecycleOwner, action: (t: T) -> Unit) {
    lifecycleOwner.lifecycle.addObserver(this)
    lifecycleOwner.lifecycleScope.launch {
    events.collect {
    try {
    action(it)
    } catch (e: Exception) {
    e.printStackTrace()
    YYLogUtils.e("FlowBus - Error:$e")
    }
    }
    }
    }

    //协程中发送数据
    suspend fun post(event: T) {
    _events.emit(event)
    }

    //主线程发送数据
    fun post(scope: CoroutineScope, event: T) {
    scope.launch {
    _events.emit(event)
    }
    }

    //自动销毁
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy() {
    YYLogUtils.w("FlowBus - 自动onDestroy")
    val subscriptCount = _events.subscriptionCount.value
    if (subscriptCount <= 0)
    busMap.remove(key)
    }
    }

    class StickEventBus<T>(key: String) : EventBus<T>(key) {
    override fun obtainEvent(): MutableSharedFlow<T> = MutableSharedFlow(1, 1, BufferOverflow.DROP_OLDEST)
    }

    }

    发送与接收消息

        // 主线程-发送消息
    FlowBus.with<String>("test-key-01").post(this@Demo11OneFragment2.lifecycleScope, "Test Flow Bus Message")
        // 接收消息
    FlowBus.with<String>("test-key-01").register(this) {
    LogUtils.w("收到FlowBus消息 - " + it)
    }

    发送粘性消息

     FlowBus.withStick<String>("test-key-02").post(lifecycleScope, "Test Stick Message")
       FlowBus.withStick<String>("test-key-02").register(this){
    LogUtils.w("收到粘性消息:$it")
    }

    Log如下:

    总结

    其实这么多消息总线框架,目前比较常用的是EventBus LiveDataBus FlowBus这三种。

    总的来说,我们尽量不依赖第三方的框架来实现,那么 FlowBus 是语言层级的,基于Kotlin的特性实现,比较推荐了。LiveDataBus 是基于Android SDK 中的类实现的(我本人是比较喜欢用),只适应于 Android 开发,但也几乎能满足日常使用了。EventBus 是基于 Java 的语言特性注解和APT,也是比较好用的。

    如果大家有源码方面的需求可以看看这里,上面的源码也都贴出来了。

    本文的代码也只是简单的实现,只是为了抛砖引玉的实现几种基本的代码,如果大家需要在实战汇总使用,更推荐大家根据不同的类型自行去 Github 上面找对应的实现封装,功能会更多,健壮性也更好。

    好了,关于消息总线就说到这了,如果觉得不错还请点赞支持哦!

    完结!


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

    收起阅读 »

    Flutter 中关于 angle 的坑

    这个问题是我最近做业务开发和业余开发都遇到的,这里的 angle 指的是旋转弧度。不是旋转角度。 先看一下我使用 angle 的场景吧: 图一中使用了 canvas.drawArc,传入了 startAngle 和 sweepAngle。图二也是如此。图...
    继续阅读 »

    这个问题是我最近做业务开发和业余开发都遇到的,这里的 angle 指的是旋转弧度。不是旋转角度


    先看一下我使用 angle 的场景吧:


    任务进度


    报警类型分布


    指针的旋转角度


    图一中使用了 canvas.drawArc,传入了 startAngle 和 sweepAngle。图二也是如此。图三是 Flutter ConstraintLayout 中圆形定位的 example,我没有使用 Flutter ConstraintLayout 自带的旋转能力,而是用了 Transform.rotate,传入了 angle。Flutter ConstraintLayout 自带的对 Widget 的旋转能力用了 canvas.rotate,也传入了 angle。


    我现在还没搞明白弧度和角度的对应关系,官网文档中也没有详细说明。但对于我来说,我根本就不想去关心弧度是多少,我只关心角度,这个角度的范围是 [0.0, 360.0]。以图三中的时钟为例,旋转 0.0 或 360.0 度时,指针应该指向 12,旋转 90.0 度时,指针应该指向 3,旋转 180.0 度时,指针应该指向 6,旋转 270.0 度时,指针应该指向 9。


    于是我们需要将旋转弧度转换成旋转角度,我研究出的转换公式如下:


    Transform.rotate:


    pi + pi * (angle / 180)

    canvas.rotate:


    angle * pi / 180

    canvas.drawArc:


    startAngle = -pi / 2
    sweepAngle = angle * pi / 180

    看见没有,这三类旋转的转换公式都不一样。我不明白 Flutter 官方为什么要这么设计,为啥这么优秀的 Flutter 引入了这么糟糕的 API。于是我带着气愤给官方提了个 Issue,想喷一喷设计这几个 API 的哥们:



    结果我被反杀了。


    冷静下来之后,我决定提交一个 Pull Request 来修正这个 API。但这需要时间,因为提交 Pull Request 的周期很长,上次我提了个 bug,Oppo 的一个哥们修复了它,Pull Request 等了将近两个月才合并。


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

    GraphQL在Flutter中的基本用法

    GraphQL是一个用于API的查询语言,它可以使客户端准确地获得所需的数据,没有任何冗余。在Flutter项目中怎么使用Graphql呢?我们需要借助graphql-flutter插件 Tip: 这里以4.0.1为例 1. 添加依赖 首先添加到pubspec...
    继续阅读 »

    GraphQL是一个用于API的查询语言,它可以使客户端准确地获得所需的数据,没有任何冗余。在Flutter项目中怎么使用Graphql呢?我们需要借助graphql-flutter插件


    Tip: 这里以4.0.1为例


    1. 添加依赖


    首先添加到pubspec.yaml


    image.png


    然后我们再看看graphql-flutter(API)有什么,以及我们该怎么用。


    2.重要API


    GraphQLClient



    • 仿apollo-client,通过配置LinkCache构造客户端实例

    • 像apollo-client一样通过构造不同Link来丰富client实例的功能


    image.png


    client实例方法几乎跟apollo-client一致,如querymutatesubscribe,也有些许差别的方法watchQuerywatchMutation 等,后面具体介绍使用区别


    Link



    graphql-flutter里基于Link实现了一些比较使用的类,如下


    HttpLink



    • 设置请求地址,默认header等


    image.png


    AuthLink



    • 通过函数的形式设置Authentication


    image.png


    ErrorLink



    • 设置错误拦截


    image.png


    DedupeLink



    • 请求去重


    GraphQLCache



    • 配置实体缓存,官方推荐使用 HiveStore 配置持久缓存


    image.png



    • HiveStore在项目中关于环境是Web还是App需要作判断,所以我们需要一个方法


    image.png


    综上各个Link以及Cache构成了Client,我们稍加对这些API做一个封装,以便在项目复用。


    3.基本封装



    • 代码及释义如下


    import 'dart:async';
    import 'package:flutter/material.dart';
    import 'package:graphql/client.dart';

    import 'package:flutter/foundation.dart' show kIsWeb;
    import 'package:path_provider/path_provider.dart'
    show getApplicationDocumentsDirectory;
    import 'package:path/path.dart' show join;
    import 'package:hive/hive.dart' show Hive;

    class Gql {
    final String source;
    final String uri;
    final String token;
    final Map<String, String> header;

    HttpLink httpLink;
    AuthLink authLink;
    ErrorLink errorLink;

    GraphQLCache cache;
    GraphQLClient client;

    String authHeaderKey = 'token';
    String sourceKey = 'source';

    Gql({
    @required this.source,
    @required this.uri,
    this.token,
    this.header = const {},
    }) {
    // 设置url,复写传入header
    httpLink = HttpLink(uri, defaultHeaders: {
    sourceKey: source,
    ...header,
    });
    // 通过复写getToken动态设置auth
    authLink = AuthLink(getToken: getToken, headerKey: authHeaderKey);
    // 错误拦截
    errorLink = ErrorLink(
    onGraphQLError: onGraphQLError,
    onException: onException,
    );
    // 设置缓存
    cache = GraphQLCache(store: HiveStore());

    client = GraphQLClient(
    link: Link.from([
    DedupeLink(), // 请求去重
    errorLink,
    authLink,
    httpLink,
    ]),
    cache: cache,
    );
    }

    static Future<void> initHiveForFlutter({
    String subDir,
    Iterable<String> boxes = const [HiveStore.defaultBoxName],
    }) async {
    if (!kIsWeb) { // 判断App获取path,初始化
    var appDir = await getApplicationDocumentsDirectory(); // 获取文件夹路径
    var path = appDir.path;
    if (subDir != null) {
    path = join(path, subDir);
    }
    Hive.init(path);
    }

    for (var box in boxes) {
    await Hive.openBox(box);
    }
    }

    FutureOr<String> getToken() async => null;

    void _errorsLoger(List<GraphQLError> errors) {
    errors.forEach((error) {
    print(error.message);
    });
    }

    // LinkError处理函数
    Stream<Response> onException(
    Request req,
    Stream<Response> Function(Request) _,
    LinkException exception,
    ) {
    if (exception is ServerException) { // 服务端错误
    _errorsLoger(exception.parsedResponse.errors);
    }

    if (exception is NetworkException) { // 网络错误
    print(exception.toString());
    }

    if (exception is HttpLinkParserException) { // http解析错误
    print(exception.originalException);
    print(exception.response);
    }

    return _(req);
    }

    // GraphqlError
    Stream<Response> onGraphQLError(
    Request req,
    Stream<Response> Function(Request) _,
    Response res,
    ) {
    // print(res.errors);
    _errorsLoger(res.errors); // 处理返回错误
    return _(req);
    }
    }

    4. 基本使用



    • main.dart


    void main() async {

    await Gql.initHiveForFlutter(); // 初始化HiveBox

    runApp(App());
    }


    • clent.dart


    import 'dart:async';
    import 'package:flutter/material.dart';
    import 'package:shared_preferences/shared_preferences.dart';


    const codeMessage = {
    401: '登录失效,',
    403: '用户已禁用',
    500: '服务器错误',
    503: '服务器错误',
    };

    // 通过复写,实现错误处理与token设置
    class CustomGgl extends Gql {
    CustomGgl({
    @required String source,
    @required String uri,
    String token,
    Map<String, String> header = const {},
    }) : super(source: source, uri: uri, token: token, header: header);

    String authHeaderKey = 'token';

    @override
    FutureOr<String> getToken() async { // 设置token
    final sharedPref = await SharedPreferences.getInstance();
    return sharedPref.getString(authHeaderKey);
    }

    @override
    Stream<Response> onGraphQLError( // 错误处理并给出提示
    Request req,
    Stream<Response> Function(Request) _,
    Response res,
    ) {
    res.errors.forEach((error) {
    final num code = error.extensions['exception']['status'];
    Toast.error(message: codeMessage[code] ?? error.message);
    print(error);
    });
    return _(req);
    }
    }

    // 创建ccClient
    final Gql ccGql = CustomGgl(
    source: 'cc',
    uri: 'https://xxx/graphql',
    header: {
    'header': 'xxxx',
    },
    );


    • demo.dart


    import 'package:flutter/material.dart';

    import '../utils/client.dart';
    import '../utils/json_view/json_view.dart';
    import '../models/live_bill_config.dart';
    import '../gql_operation/gql_operation.dart';

    class GraphqlDemo extends StatefulWidget {
    GraphqlDemo({Key key}) : super(key: key);

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

    class _GraphqlDemoState extends State<GraphqlDemo> {
    ObservableQuery observableQuery;
    ObservableQuery observableMutation;

    Map<String, dynamic> json;
    num pageNum = 1;
    num pageSize = 10;

    @override
    void initState() {
    super.initState();

    Future.delayed(Duration(), () {
    initObservableQuery();
    initObservableMutation();
    });
    }

    @override
    Widget build(BuildContext context) {
    return SingleChildScrollView(
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
    Wrap(
    spacing: 10.0,
    runSpacing: 10.0,
    children: [
    RaisedButton(
    onPressed: getLiveBillConfig,
    child: Text('Basic Query'),
    ),
    RaisedButton(
    onPressed: sendPhoneAuthCode,
    child: Text('Basic Mutation'),
    ),
    RaisedButton(
    onPressed: () {
    pageNum++;

    observableQuery.fetchMore(FetchMoreOptions(
    variables: {
    'pageNum': pageNum,
    'pageSize': pageSize,
    },
    updateQuery: (prev, newData) => newData,
    ));
    },
    child: Text('Watch Query'),
    ),
    RaisedButton(
    onPressed: () {
    observableMutation.fetchResults();
    },
    child: Text('Watch Mutation'),
    ),
    ],
    ),
    Divider(),
    if (json != null)
    SingleChildScrollView(
    child: JsonView.map(json),
    scrollDirection: Axis.horizontal,
    ),
    ],
    ),
    );
    }


    @override
    dispose() {
    super.dispose();

    observableQuery.close();
    }

    void getLiveBillConfig() async {
    Toast.loading();

    try {
    final QueryResult result = await ccGql.client.query(QueryOptions(
    document: gql(LIVE_BILL_CONFIG),
    fetchPolicy: FetchPolicy.noCache,
    ));

    final liveBillConfig =
    result.data != null ? result.data['liveBillConfig'] : null;
    if (liveBillConfig == null) return;

    setState(() {
    json = LiveBillConfig.fromJson(liveBillConfig).toJson();
    });
    } finally {
    if (Toast.loadingType == ToastType.loading) Toast.dismiss();
    }
    }


    void sendPhoneAuthCode() async {
    Toast.loading();

    try {
    final QueryResult result = await ccGql.client.mutate(MutationOptions(
    document: gql(SEND_PHONE_AUTH_CODE),
    fetchPolicy: FetchPolicy.cacheAndNetwork,
    variables: {
    'phone': '15883300888',
    'authType': 2,
    'platformName': 'Backend'
    },
    ));

    setState(() {
    json = result.data;
    });
    } finally {
    if (Toast.loadingType == ToastType.loading) Toast.dismiss();
    }
    }

    void initObservableQuery() {
    observableQuery = ccGql.client.watchQuery(
    WatchQueryOptions(
    document: gql(GET_EMPLOYEE_CONFIG),
    variables: {
    'pageNum': pageNum,
    'pageSize': pageSize,
    },
    ),
    );

    observableQuery.stream.listen((QueryResult result) {
    if (!result.isLoading && result.data != null) {
    if (result.isLoading) {
    Toast.loading();
    return;
    }

    if (Toast.loadingType == ToastType.loading) Toast.dismiss();
    setState(() {
    json = result.data;
    });
    }
    });
    }

    void initObservableMutation() {
    observableMutation = ccGql.client.watchMutation(
    WatchQueryOptions(
    document: gql(LOGIN_BY_AUTH_CODE),
    variables: {
    'phone': '15883300888',
    'authCodeType': 2,
    'authCode': '5483',
    'statisticInfo': {'platformName': 'Backend'},
    },
    ),
    );

    observableMutation.stream.listen((QueryResult result) {
    if (!result.isLoading && result.data != null) {
    if (result.isLoading) {
    Toast.loading();
    return;
    }

    if (Toast.loadingType == ToastType.loading) Toast.dismiss();
    setState(() {
    json = result.data;
    });
    }
    });
    }
    }

    总结


    这篇文章介绍了如何在Flutter项目中简单快速的使用GraphQL。并实现了一个简单的Demo。但是上面demo将UI和数据绑定在一起,导致代码耦合性很高。在实际的公司项目中,我们都会将数据和UI进行分离,常用的做法就是将GraphQL的 ValueNotifier client 调用封装到VM层中,然后在Widget中把VM数据进行绑定操作。网络上已经有大量介绍Provider|Bloc|GetX的文章,这里以介绍GraphQL使用为主,就不再赘述了。


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

    Android UI 测试基础

    UI 测试UI 测试的一种方法是直接让测试人员对目标应用执行一系列用户操作,验证其行为是否正常。这种人工操作的方式一般非常耗时、繁琐、容易出错且 case 覆盖面不全。而另一种高效的方法是为编写 UI 测试,以自动化的方式执行用户操作。自动化方法可以可重复且快...
    继续阅读 »

    UI 测试

    UI 测试的一种方法是直接让测试人员对目标应用执行一系列用户操作,验证其行为是否正常。这种人工操作的方式一般非常耗时、繁琐、容易出错且 case 覆盖面不全。而另一种高效的方法是为编写 UI 测试,以自动化的方式执行用户操作。自动化方法可以可重复且快速可靠地运行测试。

    使用 Android Studio 自动执行 UI 测试,需要在 src/AndroidTest/java 中实现测试代码,这种测试属于插桩单元测试。Android 的 Gradle 插件会根据测试代码构建一个测试应用,然后在目标应用所在的设备上加载该测试应用。在测试代码中,可以使用 UI 测试框架来模拟目标应用上的用户交互。

    注意:并不是所有对 UI 的测试都是插桩单元测试,在本地单元测试中,也可以通过第三方框架(例如 Robolectric )来模拟 Android 运行环境,但这种测试是跑在开发计算机上的,基于 JVM 运行,而不是 Android 模拟器或物理设备的真实环境。

    涉及 UI 测试的场景有两种情况:

    • 单个 App 的 UI 测试:这种类型的测试可以验证目标应用在用户执行特定操作或在其 Activity 中输入特定内容时行为是否符合预期。Espresso 之类的 UI 测试框架可以实现通过编程的方式模拟用户交互。
    • 流程涵盖多个 App 的 UI 测试:这种类型的测试可以验证不同 App 之间或是用户 App 与系统 App 之间的交互流程是否正常运行。比如在一个应用中打开系统相机进行拍照。UI Automator 框架可以支持跨应用交互。

    Android 中的 UI 测试框架

    Jetpack 包含了丰富的官方框架,这些框架提供了用于编写 UI 测试的 API:

    • Espresso :提供了用于编写 UI 测试的 API ,可以模拟用户与单个 App 进行 UI 交互。使用 Espresso 的一个主要好处是它提供了测试操作与您正在测试的应用程序 UI 的自动同步。Espresso 会检测主线程何时空闲,因此它能够在适当的时间运行您的测试命令,从而提高测试的可靠性。
    • Jetpack Compose :提供了一组测试 API 用来启动 Compose 屏幕和组件之间的交互,融合到了开发过程中。算是 Compose 的一个优势。
    • UI Automator : 是一个 UI 测试框架,适用于涉及多个应用的操作流程的测试。
    • Robolectric :在 JVM 上运行本地单元测试,而不是模拟器或物理设备上。可以配合 Espresso 或 Compose 的测试 API 与 UI 组件进行模拟交互。

    异常行为和同步处理

    因为 Android 应用是基于多线程实现的,所有涉及 UI 的操作都会发送到主线程排队执行,所以在编写测试代码时,需要处理这种异步存在的问题。当一个用户输入注入时,测试框架必须等待 App 对用户输入进行响应。当一个测试没有确定性行为的时候,就会出现异常行为。

    像 Compose 或 Espresso 这样的现代框架在设计时就考虑到了测试场景,因此可以保证在下一个测试操作或断言之前 UI 将处于空闲状态,从而保证了同步行为。

    流程图显示了在通过测试之前检查应用程序是否空闲的循环:

    流程图显示了在通过测试之前检查应用程序是否空闲的循环.png

    在测试中使用 sleep 会导致测试缓慢或者不稳定,如果有动画执行超过 2s 就会出现异常情况。

    显示同步基于等待固定时间时的测试失败的图表.png

    应用架构和测试

    另一方面,应用的架构应该能够快速替换一些组件,以支持 mock 数据或逻辑进行测试,例如,在有异步加载数据的场景,但我们并不关心异步数据获取相关逻辑的情况下,仅关心获取到数据后的 UI 层测试,就可以将异步逻辑替换成假的数据源,从而能够更加高效的进行测试:

    生产和测试架构图。 生产图显示了向存储库提供数据的本地和远程数据源,而存储库又将数据异步提供给 UI。 测试图显示了一个 Fake 存储库,该存储库将其数据同步提供给 UI.png

    推荐使用 Hilt 框架实现这种注入数据的替换操作。

    为什么需要自动化测试?

    Android App 可以在不同的 API 版本的上千种不同设备上运行,并且手机厂商有可能修改系统代码,这意味着 App 可能会在一些设备上不正确地运行甚至导致 crash 。

    UI 测试可以进行兼容性测试,验证 App 在不同环境中的行为。例如可以测试不同环境下的行为:

    • API level 不同
    • 位置和语言设置不同
    • 屏幕方向不同

    此外,还要考虑设备类型的问题,例如平板电脑和可折叠设备的行为,可能与普通手机设备环境下,产生不同的行为。

    AndroidX 测试框架的使用

    环境配置

    1. 修改根目录下的 build.gradle文件,确保项目依赖仓库:
    allprojects {
    repositories {
    jcenter()
    google()
    }
    }
    1. 添加测试框架依赖:
    dependencies {
    // 核心框架
    androidTestImplementation "androidx.test:core:$androidXTestVersion0"

    // AndroidJUnitRunner and JUnit Rules
    androidTestImplementation "androidx.test:runner:$testRunnerVersion"
    androidTestImplementation "androidx.test:rules:$testRulesVersion"

    // Assertions 断言
    androidTestImplementation "androidx.test.ext:junit:$testJunitVersion"
    androidTestImplementation "androidx.test.ext:truth:$truthVersion"

    // Espresso 依赖
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
    androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
    androidTestImplementation "androidx.test.espresso:espresso-accessibility:$espressoVersion"
    androidTestImplementation "androidx.test.espresso:espresso-web:$espressoVersion"
    androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espressoVersion"

    // 下面的依赖可以使用 "implementation" 或 "androidTestImplementation",
    // 取决于你是希望这个依赖出现在 Apk 中,还是测试 apk 中
    androidTestImplementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion"
    }

    发行版本号参阅: developer.android.com/jetpack/and…

    另外值得注意的一点是 espresso-idling-resource 这个依赖在生产代码中使用的话,需要打包到 apk 中。

    AndroidX 中的 Junit4 Rules

    AndroidX 测试框架包含了一组配合 AndroidJunitRunner 使用的 Junit Rules。

    关于什么是 JUnit Rules ,可以查看 wiki:github.com/junit-team/…

    JUnit Rules 提供了更大的灵活性并减少了测试中所需的样板代码。可以将 JUnit Rules 理解为一些模拟环境用来测试的 API 。例如:

    • ActivityScenarioRule : 用来模拟 Activity 。
    • ServiceTestRule :可以用来模拟启动 Service 。
    • TemporaryFolder :可以用来创建文件和文件夹,这些文件会在测试方法完成时被删除(若不能删除,会抛出异常)。
    • ErrorCollector :发生问题后继续执行测试,最后一次性报告所有错误内容。
    • ExpectedException :在测试过程中指定预期的异常。

    除了上面几个例子,还有很多 Rules ,可以将 Rules 理解为用来在测试中快捷实现一些能力的 API 。

    ActivityScenarioRule

    ActivityScenarioRule 用来对单个 Activity 进行功能测试。声明一个 ActivityScenarioRule 实例:

        @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    这个规则,会在执行标注有 @Test 注解的测试方法启动前,绑定构造参数中执行的 Activity ,并且在带有 @Test 测试方法执行前,先执行所有带有 @Before 注解的方法,并在执行的测试方法结束后,执行所有带有 @After 注解的方法。

    @RunWith(AndroidJUnit4::class)
    class MainActivityTest {
    @Before
    fun beforeActivityCreate() {
    Log.d(TAG, "beforeActivityCreate")
    }

    @Before
    fun beforeTest() {
    Log.d(TAG, "beforeTest")
    }

    @Test
    fun onCreate() {
    activityRule.scenario.moveToState(Lifecycle.State.CREATED).onActivity {
    Log.d(TAG, "in test thread: ${Thread.currentThread()}}")
    }
    }

    @After
    fun afterActivityCreate() {
    Log.d(TAG, "afterActivityCreate")
    }
    // ...
    }

    执行这个带有 @Test 注解的 onCreate方法,其日志为:

    2022-06-17 17:29:07.341 I/TestRunner: started: onCreate(com.chunyu.accessibilitydemo.MainActivityTest)
    2022-06-17 17:29:08.006 D/MainActivityTest: beforeTest
    2022-06-17 17:29:08.006 D/MainActivityTest: beforeActivityCreate
    2022-06-17 17:29:08.565 D/MainActivityTest: in ui thread: Thread[main,5,main]
    2022-06-17 17:29:08.566 D/MainActivityTest: afterActivityCreate
    2022-06-17 17:29:09.054 I/TestRunner: finished: onCreate(com.chunyu.accessibilitydemo.MainActivityTest)

    在执行完所有的 @After 方法后,会终止模拟启动的这个 Activity 。

    访问 Activity

    测试方法中的重点是通过 ActivityScenarioRule 模拟构造 Activity ,并对其中的一些行为进行测试。

    如果要在测试逻辑中访问指定的 Activity ,可以通过 ActivityScenarioRule.getScenario().onActivity{ ... } 回调中指定一些代码逻辑。例如上面的 onCreate() 测试方法中,稍加修改,就可以展示访问 Activity 的能力:

        @Test
    fun onCreate() {
    activityRule.scenario.onActivity { it ->
    Log.d(TAG, "${it.isFinishing}")
    }
    }

    不光可以访问 Activity 中公开的属性和方法,还可以访问指定 Activity 中 public 的内容,例如:

        @Test
    fun test() {
    activityRule.scenario.onActivity { it ->
    it.button.performClick()
    }
    }

    控制 Activity 的生命周期

    在最开始的例子中,我们通过 moveToState 来控制了这个 Activity 的生命周期,修改代码:

        @Test
    fun onCreate() {
    activityRule.scenario.moveToState(Lifecycle.State.CREATED).onActivity {
    Log.d(TAG, "${it.lifecycle.currentState}")
    }
    }

    我们在 onActivity 中打印 Activity 的当前生命周期,检查一下是否真的是在 moveToState 中指定的状态,打印结果:

    2022-06-17 17:45:30.425 D/MainActivityTest: CREATED

    moveToState 的确生效了,它可以将 Activity 控制到我们想要的状态。

    通过 ActivityScenarioRule 的 getState() ,也可以直接获取到模拟的 Activity 的状态,这个方法可能存在的状态包括:

    • State.CREATED
    • State.STARTED
    • State.RESUMED
    • State.DESTROYED

    而 moveToState 能够设置的值包括:

        public enum State {
    // 这个状态表示 Activity 已销毁
    DESTROYED,

    // 初始化状态,还没调用 onCreate
    INITIALIZED,

    // 存在两种情况,在 onCreate 开始后,onStop 结束前
    CREATED,

    // 存在两种情况,在 onStart 开始后,在 onPause 结束前。
    STARTED,

    // onResume 开始后调用。
    RESUMED;

    // ...
    }

    当 moveToState 设置为 DESTROYED ,再访问 Activity ,会抛出异常

    java.lang.NullPointerException: Cannot run onActivity since Activity has been destroyed already

    如果要测试 Fragment ,可以通过 FragmentScenario 进行,此类需要引用

     debugImplementation "androidx.fragment:fragment-testing:$fragment_version"

    ServiceTestRule

    ServiceTestRule 用来在单元测试情况下模拟启动指定的 Service ,包括 bindService 和 startService 两种方式,创建一个 ServiceTestRule 实例:

        @get:Rule
    val serviceTestRule = ServiceTestRule()

    在测试方法中通过 ServiceTestRule 启动 Service ,下面是一个普通的服务,在真实环境下通过 startService 可以正常启动:

    class RegularService: Service() {

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    Log.d("onStartCommand", ": ${Thread.currentThread().name}")
    Toast.makeText(this, "in Service", Toast.LENGTH_SHORT).show()
    return super.onStartCommand(intent, flags, startId)
    }

    override fun onBind(intent: Intent?): IBinder? {
    return null
    }
    }

    startService

        @Test
    fun testService() {
    serviceTestRule.startService(Intent(ApplicationProvider.getApplicationContext(), RegularService::class.java))
    }

    但是这样会抛出异常:

    java.util.concurrent.TimeoutException: Waited for 5 SECONDS, but service was never connected

    这是因为,通过 ServiceTestRule 的 startService(Intent) 启动一个 Service ,会在 5s 内阻塞直到 Service 已连接,即调用到了 ServiceConnection.onServiceConnected(ComponentName, IBinder) 。

    也就是说,你的 Service 的 onBind(Intent) 方法,不能返回 null ,否则就会抛出 TimeoutException 。

    修改 RegularService :

    class RegularService: Service() {

    private val binder = RegularBinder()

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    Log.d("RegularServiceTest", "onStartCommand")
    return super.onStartCommand(intent, flags, startId)
    }

    override fun onBind(intent: Intent?): IBinder? {
    return binder
    }

    inner class RegularBinder: Binder() {
    fun getService(): RegularService = this@RegularService
    }
    }

    这样,通过 ServiceTestRule 的 startService 启动服务就可以正常运行了:

    2022-06-17 19:51:59.772 I/TestRunner: started: testService(com.chunyu.accessibilitydemo.service.RegularServiceTest)
    2022-06-17 19:51:59.777 D/RegularServiceTest: beforeService1
    2022-06-17 19:51:59.777 D/RegularServiceTest: beforeService2
    2022-06-17 19:51:59.795 D/RegularServiceTest: onStartCommand
    2022-06-17 19:51:59.820 D/RegularServiceTest: afterService1
    2022-06-17 19:51:59.820 D/RegularServiceTest: afterService2
    2022-06-17 19:51:59.830 I/TestRunner: finished: testService(com.chunyu.accessibilitydemo.service.RegularServiceTest)

    ServiceTestRule 和 ActivityScenarioRule 一样,都会在执行测试前执行所有的 @Before 方法,执行结束后,继续执行所有的 @After 方法。

    bindService

        @Test
    fun testService() {
    serviceTestRule.bindService(Intent(ApplicationProvider.getApplicationContext(), RegularService::class.java))
    }

    ServiceTestRule.bindService 效果和 Context.bindService 相同,都不走 onStartCommand 而是 onBind 方法。

    2022-06-17 19:57:19.274 I/TestRunner: started: testService(com.chunyu.accessibilitydemo.service.RegularServiceTest)
    2022-06-17 19:57:19.277 D/RegularServiceTest: beforeService1
    2022-06-17 19:57:19.277 D/RegularServiceTest: beforeService2
    2022-06-17 19:57:19.296 D/RegularServiceTest: onBind
    2022-06-17 19:57:19.302 D/RegularServiceTest: afterService1
    2022-06-17 19:57:19.302 D/RegularServiceTest: afterService2
    2022-06-17 19:57:19.314 I/TestRunner: finished: testService(com.chunyu.accessibilitydemo.service.RegularServiceTest)

    测试方法的执行顺序也是一样的。

    访问 Service

    startService 启动的 Service 无法获取到 Service 实例,ServiceTestRule 并没有像 ActivityScenarioRule 那样提供 onActivity {... } 回调方法。

    bindService 的返回类型是 IBinder ,可以通过 IBinder 对象获取到 Service 实例:

        @Test
    fun testService() {
    val binder = serviceTestRule.bindService(Intent(ApplicationProvider.getApplicationContext(), RegularService::class.java))
    val service = (binder as? RegularService.RegularBinder)?.getService()
    // access RegularService info
    }


    作者:自动化BUG制造器
    链接:https://juejin.cn/post/7110184974791213064
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    收起阅读 »