注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android架构之路--热更新Tinker

当前市面的热补丁方案有很多,其中比较出名的有阿里的 AndFix、美团的 Robust 以及 QZone 的超级补丁方案。但它们都存在无法解决的问题,这也是正是最后使用 Tinker 的原因。先看一张图对比:Tinker热补丁方案不仅支持类、So 以及资源的替...
继续阅读 »

一、简介

当前市面的热补丁方案有很多,其中比较出名的有阿里的 AndFix、美团的 Robust 以及 QZone 的超级补丁方案。但它们都存在无法解决的问题,这也是正是最后使用 Tinker 的原因。先看一张图对比:

1-1:热更新对比图

Tinker热补丁方案不仅支持类、So 以及资源的替换,它还是2.X-7.X的全平台支持。利用Tinker我们不仅可以用做 bugfix,甚至可以替代功能的发布。Tinker已运行在微信的数亿Android设备上。

TinkerPatch 平台在 Github 为大家提供了各种各样的 Sample,大家可点击前往 [TinkerPatch Github].

Tinker原理图

1-2:原理图

Tinker流程图

1-3:Tinker 流程图

二、Tinker相关网站

微信Tinker Patch官网:Tinker Patch
Github地址:tinker

三、接入Tinker步骤

基础步骤

  • 注册Tinker账户、添加APP、记录AppKey,添加 APP 版本、 发布补丁。详细步骤请移步Tinker平台使用文档

主要来说下配置Gradle和代码

1. 配置Tinker版本信息

我们使用配置文件去配置Tinker版本信息,易于统一版本和后面更换版本,如图:

2-1 gradle.properties文件

代码如下:

TINKER_VERSION=1.9.6
TINKERPATCH_VERSION=1.2.6

2. 使用Tinker插件

在根目录下的build.gradle文件下配置,如图:

2-2 添加Tinker插件



代码如下:

classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:${TINKERPATCH_VERSION}"

3. 配置Tinker的gradle脚本

在项目app目录下新建tinkerparch.gradle文件,如图:

2-3 tinkerpatch.gradle

代码如下:

apply plugin: 'tinkerpatch-support'
/**
* TODO: 请按自己的需求修改为适应自己工程的参数
*/

def bakPath = file("${buildDir}/bakApk/")
def baseInfo = "app-1.0.0-0529-14-38-02"
def variantName = "release"
/**
* 对于插件各参数的详细解析请参考
* http://tinkerpatch.com/Docs/SDK
*/

tinkerpatchSupport {
/** 可以在debug的时候关闭 tinkerPatch **/
/** 当disable tinker的时候需要添加multiDexKeepProguard和proguardFiles,
* 这些配置文件本身由tinkerPatch的插件自动添加,当你disable后需要手动添加
* 你可以copy本示例中的proguardRules.pro和tinkerMultidexKeep.pro,
* 需要你手动修改'tinker.sample.android.app'本示例的包名为你自己的包名,
* com.xxx前缀的包名不用修改
**/

tinkerEnable = true
reflectApplication = true
/**
* 是否开启加固模式,只能在APK将要进行加固时使用,否则会patch失败。
* 如果只在某个渠道使用了加固,可使用多flavors配置
**/

protectedApp = false
/**
* 实验功能
* 补丁是否支持新增 Activity (新增Activity的exported属性必须为false)
**/

supportComponent = true

autoBackupApkPath = "${bakPath}"
/** 注意:换成自己在Tinker平台上申请的appKey**/
appKey = "521db2518e0ca16d"

/** 注意: 若发布新的全量包, appVersion一定要更新 **/
appVersion = "1.0.0"

def pathPrefix = "${bakPath}/${baseInfo}/${variantName}/"
def name = "${project.name}-${variantName}"

baseApkFile = "${pathPrefix}/${name}.apk"
baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
baseResourceRFile = "${pathPrefix}/${name}-R.txt"

/**
* 若有编译多flavors需求, 可以参照:
* https://github.com/TinkerPatch/tinkerpatch-flavors-sample
* 注意: 除非你不同的flavor代码是不一样的,
* 不然建议采用zip comment或者文件方式生成渠道信息
* (相关工具:walle 或者 packer-ng)
**/

}

/**
* 用于用户在代码中判断tinkerPatch是否被使用
*/

android {
defaultConfig {
buildConfigField "boolean", "TINKER_ENABLE",
"${tinkerpatchSupport.tinkerEnable}"
}
}

/**
* 一般来说,我们无需对下面的参数做任何的修改
* 对于各参数的详细介绍请参考:
* https://github.com/Tencent/tinker/wiki/Tinker-
* %E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
*/

tinkerPatch {
ignoreWarning = false
useSign = true
dex {
dexMode = "jar"
pattern = ["classes*.dex"]
loader = []
}
lib {
pattern = ["lib/*/*.so"]
}

res {
pattern = ["res/*", "r/*", "assets/*", "resources.arsc",
"AndroidManifest.xml"]
ignoreChange = []
largeModSize = 100
}

packageConfig {
}
sevenZip {
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
// path = "/usr/local/bin/7za"
}
buildConfig {
keepDexApply = false
}
}

注意

  • AppKey:换成自己在Tinker平台上申请的。
  • baseInfo:基准包名称,使用Tinker脚本编译在模块的build/bakApk生成编译副本。
  • variantName: 这个一般对应buildTypes里面你基准包生成的类型,release、debug或其他
  • appVersion:配置和Tinker后台新建补丁包的一致。

其他地方可以暂时不用改

4. 配置模块下的build.gradle

配置签名

如果有不会的同学可以看这篇 Android Studio的两种模式及签名配置

2-4:配置签名

在配置混淆代码的时候,想要提醒下大家,当设置 minifyEnabled 为false时代表不混淆代码,shrinkResources也应设置为false ,它们通常是彼此关联。
要是你设置minifyEnabled 为false,shrinkResources为true,将会报异常,信息如下:

Error:A problem was found with the configuration of task':watch:packageOfficialDebug'.
File '...\build\intermediates\res\resources-official-debug-stripped.ap_' specified for property 'resourceFile' does not exist.

2-4-1:混淆配置

配置依赖

2-5:配置Tinker依赖

使用插件

2-6:使用Tinker插件

具体代码如下:

apply plugin: 'com.android.application'
apply from: 'tinkerpatch.gradle'

android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "qqt.com.tinkerdemo"
minSdkVersion 17
targetSdkVersion 25
versionCode 1
versionName "1.0.0"
multiDexEnabled true
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
signingConfigs {
release {
storeFile file("./jks/tinker.jks")
storePassword "123456"
keyAlias "tinker"
keyPassword "123456"

}
debug {
storeFile file("./jks/debug.keystore")
}
}
buildTypes {
release {
minifyEnabled false
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
signingConfig signingConfigs.debug
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'

annotationProcessor("com.tinkerpatch.tinker:tinker-android-anno:${TINKER_VERSION}") {
changing = true
}
provided("com.tinkerpatch.tinker:tinker-android-anno:${TINKER_VERSION}") {
changing = true
}
compile("com.tinkerpatch.sdk:tinkerpatch-android-sdk:${TINKERPATCH_VERSION}") {
changing = true
}

compile 'com.android.support:multidex:1.0.1'
}

5. 代码集成

最后一步,在自己的代码新建一个Application,把代码集成在App中,别忘了在AndroidManifest里面配置APP。。。
如图:

2-7:集成代码



我是继承MultiDexApplication主要是防止64k异常。有关这块知识,请看 Android 方法数超过64k、编译OOM、编译过慢解决方案

具体代码如下:

package qqt.com.tinkerdemo;

import android.support.multidex.MultiDexApplication;

import com.tencent.tinker.loader.app.ApplicationLike;
import com.tinkerpatch.sdk.TinkerPatch;
import com.tinkerpatch.sdk.loader.TinkerPatchApplicationLike;

/**
* 邮箱:ljh12520jy@163.com
*
*
@author Ljh on 2018/5/28
*/


public class App extends MultiDexApplication {

private ApplicationLike mApplicationLike;

@Override
public void onCreate() {
super.onCreate();
mApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();
// 初始化TinkerPatch SDK, 更多配置可参照API章节中的,初始化SDK
TinkerPatch.init(mApplicationLike)
.reflectPatchLibrary()
.fetchPatchUpdate(true)
// 强制更新
.setPatchRollbackOnScreenOff(true)
.setPatchRestartOnSrceenOff(true)
.setFetchPatchIntervalByHours(3);

// 每隔3个小时(通过setFetchPatchIntervalByHours设置)去访问后台时候有更新,
//通过handler实现轮训的效果
TinkerPatch.with().fetchPatchUpdateAndPollWithInterval();
}
}

四、生成基准包

在生成基准包的时候,要注意一个问题,就是关闭 instant run(当tinkerEnable = true时,false的时候,就不需要),如图:

3-1:关闭InstantRun

在Android Studio的右上角,点击Gradle,如图:

3-2:准备生成基准包

双击assembleRelease生成成功后安装模块/build/outputs/apk/release/app-release.apk就OK了,这时候进去模块/build/bakApk里面记录一下类似app-1.0.0-0530-18-01-59的文件名称,只生成一次基准包,那么就会生成一个。这里需要注意一下,如果点太多生成太多的话确定不了刚刚生成的是哪个,那么就选最新那个或者删掉重新生成基准包。
生成后的基准包如图:

3-3:生成基准包

五、修改bug

在自己的代码中随便修改点代码(Tinker1.9.6 里面支持新增Activity代码)

六、生成补丁包

在生成补丁包前,我们需要去tinkerpatch.gradle文件下修改一些信息。

  • baseInfo :把前面app-1.0.0-0529-14-38-02换成我们刚生成记录下的基准包(app-1.0.0-0530-18-01-59)就可以。
  • variantName : 因为刚刚我们使用assembleRelease生成的补丁,所以我们只需要使用release

双击TinkerPatchRelease生成差分包,patch_signed_7zip.apk就是补丁包

生成的补丁包如图:

3-4:生成补丁包

3-5:tinkerPatch下的一些文件说明

七、发布补丁包

回到Tinker后台,选中我们开始新建的项目,补丁下发->添加APP版本。然后上传刚刚的patch_signed_7zip.apk。

APP开启强制更新的话那么重启应用就会更新,否则会通过轮询去更新。应用重启才生效。

3-6:发布补丁包

注:在Tinker后台发布的差分包(补丁包)是根据app-1.0.0-0530-18-01-59为基准包下,修复bug生成的补丁包,只对于app-1.0.0-0530-18-01-59版本的apk生效。

3-7:差分包


一、多渠道打包

tinker官方文档推荐用walle或者packer-ng-plugin来辅助打渠道包。估计有不少同学用过,今天我想推荐另外一款多渠道打包的插件ApkMultiChannelPlugin,它作为Android Studio插件进行多渠道打包。
安装步骤:打开 Android Studio: 打开 Setting/Preferences -> Plugins -> Browse repositories 然后搜索 ApkMultiChannel 安装重启。
有不了解的同学,可以直接看它的文档。

我是采用add channel file to META-INF方式进行多渠道打包,在这里提供一个读取渠道的工具类ChannelHelper。

二、多渠道打包步骤

1. 选择一个基准包

选择基准包的一个apk,然后右键,点击Build MultiChannel

1-1:选择基准包

2. 配置

配置签名信息,打包方式和渠道等。

1-2:配置多渠道

配置说明:

Key Store Path: 签名文件的路径
Key Store Password: 签名文件的密码
Key Alias: 密钥别名
Key Password: 密钥密码

Zipalign Path: zipalign 文件的路径(用于优化 apk;zipalign 可以确保所有未压缩的数据均是以相对于文件开始部分的特定字节对齐开始,这样可减少应用消耗的 RAM 量。)
Signer Version: 选择签名版本:apksigner 和 jarsigner
Build Type: 打包方式

Channels: 渠道列表,每行一个,最前面可加 > 或不加(保存信息的时候,程序会自行加上)

我们刚才刚才配置的东西会保存在根目录的 channels.properties里

1-3:channel配置文件

3. 开始打包

配置完成后,选择基准包的一个apk,然后右键,点击Build MultiChannel,就会开始进行多渠道打包,文件会输出在选中的apk的当前目录下的channels是目录下,如图:

1-4:多渠道打包

4. 发布APK

将刚才打包完成的包,分别发布到对应的应用市场。

5. 修改bug

随便修改部分代码

6. 生成补丁包

在生成补丁包前,我们需要去tinkerpatch.gradle文件下修改一些信息。

  • baseInfo :改成我们刚才选择基准包的目录app-1.0.1-0601-14-30-42就可以。
    双击TinkerPatchRelease生成差分包,patch_signed_7zip.apk就是补丁包
  • variantName : 因为刚刚我们使用assembleRelease生成的补丁,所以我们只需要使用release

双击TinkerPatchRelease生成差分包,patch_signed_7zip.apk就是补丁包

1-5:生成补丁包

7. 发布补丁包

回到Tinker后台,选中我们开始新建的项目,补丁下发->添加APP版本。然后上传刚刚的patch_signed_7zip.apk。

APP开启强制更新的话那么重启应用就会更新,否则会通过轮询去更新。应用重启才生效。

1-6: 发布补丁包.png

注:

  1. 这个补丁包对于以app-1.0.1-0601-14-30-42为基准宝,进行多渠道打包的apk都能生效(亲测成功),如果你把该渠道包进行360加固(protectedApp = true),也生效。
  2. 当我们在正式环境需要混淆代码:设置 minifyEnabled true,添加混淆:
-keep public class * implements com.tencent.tinker.loader.app.ApplicationLike

如图:

1-7: 混淆代码

本文转载自: https://cloud.tencent.com/developer/article/1872556

收起阅读 »

AFNetworking源码探究 —— UIKit相关之UIProgressView+AFNetworking分类

iOS
下面我们先看一下接口的API/** This category adds methods to the UIKit framework's `UIProgressView` class. The methods in this category provide...
继续阅读 »

接口API

下面我们先看一下接口的API

/**
This category adds methods to the UIKit framework's `UIProgressView` class. The methods in this category provide support for binding the progress to the upload and download progress of a session task.
*/
@interface UIProgressView (AFNetworking)

///------------------------------------
/// @name Setting Session Task Progress
///------------------------------------

/**
Binds the progress to the upload progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated;

/**
Binds the progress to the download progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated;

@end


该类为UIKit框架的UIProgressView类添加方法。 此类别中的方法为将进度绑定到会话任务的上载和下载进度提供了支持。

该接口比较少,其实就是一个上传任务和一个下载任务分别和进度的绑定,可动画。

这里大家还要注意一个关于类的继承的细节。

// 上传
@interface NSURLSessionUploadTask : NSURLSessionDataTask
@interface NSURLSessionDataTask : NSURLSessionTask

// 下载
@interface NSURLSessionDownloadTask : NSURLSessionTask

给大家贴出来就是想让大家注意下这个结构。

runtime获取是否可动画

这里还是用runtime分别绑定下载和上传是否可动画。

- (BOOL)af_uploadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_uploadProgressAnimated)) boolValue];
}

- (void)af_setUploadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_uploadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)af_downloadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_downloadProgressAnimated)) boolValue];
}

- (void)af_setDownloadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_downloadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

这个还算是很好理解的,有了前面的基础,这里就不多说了。

接口的实现

下面我们就看一下接口的实现。

1. 上传任务

static void * AFTaskCountOfBytesSentContext = &AFTaskCountOfBytesSentContext;
static void * AFTaskCountOfBytesReceivedContext = &AFTaskCountOfBytesReceivedContext;
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];
[task addObserver:self forKeyPath:@"countOfBytesSent" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];

[self af_setUploadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesSent,最后就是设置是否可动画的状态。

2. 下载任务

- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];
[task addObserver:self forKeyPath:@"countOfBytesReceived" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];

[self af_setDownloadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesReceived,最后就是设置是否可动画的状态。


KVO观察实现

下面看一下KVO观察的实现,这里也是这个类的精髓所在。

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(__unused NSDictionary *)change
context:(void *)context
{
if (context == AFTaskCountOfBytesSentContext || context == AFTaskCountOfBytesReceivedContext) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) {
if ([object countOfBytesExpectedToSend] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) {
if ([object countOfBytesExpectedToReceive] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self.af_downloadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(state))]) {
if ([(NSURLSessionTask *)object state] == NSURLSessionTaskStateCompleted) {
@try {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(state))];

if (context == AFTaskCountOfBytesSentContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesSent))];
}

if (context == AFTaskCountOfBytesReceivedContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))];
}
}
@catch (NSException * __unused exception) {}
}
}
}
}

这里还是很简单的吧。

  • 如果keyPath是@"countOfBytesSent",那么就获取countOfBytesExpectedToSend,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];得到进度。
  • 如果keyPath是@"countOfBytesReceived",那么就获取countOfBytesExpectedToReceive,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self. af_downloadProgressAnimated];得到进度。
  • 如果keyPath是@"state"并且任务是完成状态NSURLSessionTaskStateCompleted,那么就要移除对这几个keyPath的观察者。

后记

本篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。

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





收起阅读 »

AFNetworking源码探究(二十五) —— UIKit相关之UIRefreshControl+AFNetworking分类

iOS
上一篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。这一篇主要分析UIRefreshControl+AFNetworking这个分类。接口API下面我们先看一下接口的API/** This ...
继续阅读 »

回顾

上一篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。这一篇主要分析UIRefreshControl+AFNetworking这个分类。


接口API

下面我们先看一下接口的API

/**
This category adds methods to the UIKit framework's `UIProgressView` class. The methods in this category provide support for binding the progress to the upload and download progress of a session task.
*/
@interface UIProgressView (AFNetworking)

///------------------------------------
/// @name Setting Session Task Progress
///------------------------------------

/**
Binds the progress to the upload progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated;

/**
Binds the progress to the download progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated;

@end

该类为UIKit框架的UIProgressView类添加方法。 此类别中的方法为将进度绑定到会话任务的上载和下载进度提供了支持。

该接口比较少,其实就是一个上传任务和一个下载任务分别和进度的绑定,可动画。

这里大家还要注意一个关于类的继承的细节。

// 上传
@interface NSURLSessionUploadTask : NSURLSessionDataTask
@interface NSURLSessionDataTask : NSURLSessionTask

// 下载
@interface NSURLSessionDownloadTask : NSURLSessionTask

给大家贴出来就是想让大家注意下这个结构。


runtime获取是否可动画

这里还是用runtime分别绑定下载和上传是否可动画。

- (BOOL)af_uploadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_uploadProgressAnimated)) boolValue];
}

- (void)af_setUploadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_uploadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)af_downloadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_downloadProgressAnimated)) boolValue];
}

- (void)af_setDownloadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_downloadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

这个还算是很好理解的,有了前面的基础,这里就不多说了。


接口的实现

下面我们就看一下接口的实现。

1. 上传任务

static void * AFTaskCountOfBytesSentContext = &AFTaskCountOfBytesSentContext;
static void * AFTaskCountOfBytesReceivedContext = &AFTaskCountOfBytesReceivedContext;
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];
[task addObserver:self forKeyPath:@"countOfBytesSent" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];

[self af_setUploadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesSent,最后就是设置是否可动画的状态。

2. 下载任务

- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];
[task addObserver:self forKeyPath:@"countOfBytesReceived" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];

[self af_setDownloadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesReceived,最后就是设置是否可动画的状态。


KVO观察实现

下面看一下KVO观察的实现,这里也是这个类的精髓所在。

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(__unused NSDictionary *)change
context:(void *)context
{
if (context == AFTaskCountOfBytesSentContext || context == AFTaskCountOfBytesReceivedContext) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) {
if ([object countOfBytesExpectedToSend] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) {
if ([object countOfBytesExpectedToReceive] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self.af_downloadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(state))]) {
if ([(NSURLSessionTask *)object state] == NSURLSessionTaskStateCompleted) {
@try {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(state))];

if (context == AFTaskCountOfBytesSentContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesSent))];
}

if (context == AFTaskCountOfBytesReceivedContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))];
}
}
@catch (NSException * __unused exception) {}
}
}
}
}

这里还是很简单的吧。

  • 如果keyPath是@"countOfBytesSent",那么就获取countOfBytesExpectedToSend,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];得到进度。
  • 如果keyPath是@"countOfBytesReceived",那么就获取countOfBytesExpectedToReceive,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self. af_downloadProgressAnimated];得到进度。
  • 如果keyPath是@"state"并且任务是完成状态NSURLSessionTaskStateCompleted,那么就要移除对这几个keyPath的观察者。

后记

本篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。

本文转载自腾讯社区:作者conanma  https://cloud.tencent.com/developer/article/1872401



收起阅读 »

AFNetworking源码探究 —— UIKit相关之AFAutoPurgingImageCache缓存

iOS
回顾上一篇主要讲述了UIRefreshControl+AFNetworking这个分类,将刷新状态和任务状态进行了绑定和同步。这一篇主要讲述AFAutoPurgingImageCache有关的缓存。接口API按照老惯例,我们还是先看一下接口API文档。这个接口...
继续阅读 »

回顾

上一篇主要讲述了UIRefreshControl+AFNetworking这个分类,将刷新状态和任务状态进行了绑定和同步。这一篇主要讲述AFAutoPurgingImageCache有关的缓存。


接口API

按照老惯例,我们还是先看一下接口API文档。这个接口文档包括三个部分,两个协议一个类。

  • 协议AFImageCache
  • 协议AFImageRequestCache
  • AFAutoPurgingImageCache

1. AFImageCache

这个协议包括四个方法

/**
Adds the image to the cache with the given identifier.

@param image The image to cache.
@param identifier The unique identifier for the image in the cache.
*/
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier;

/**
Removes the image from the cache matching the given identifier.

@param identifier The unique identifier for the image in the cache.

@return A BOOL indicating whether or not the image was removed from the cache.
*/
- (BOOL)removeImageWithIdentifier:(NSString *)identifier;

/**
Removes all images from the cache.

@return A BOOL indicating whether or not all images were removed from the cache.
*/
- (BOOL)removeAllImages;

/**
Returns the image in the cache associated with the given identifier.

@param identifier The unique identifier for the image in the cache.

@return An image for the matching identifier, or nil.
*/
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier;

该协议定义了包括加入、移除、获取缓存中的图片。

2. AFImageRequestCache

该协议包含下面几个方法,这里注意这个协议继承自协议AFImageCache

@protocol AFImageRequestCache <AFImageCache>

/**
Asks if the image should be cached using an identifier created from the request and additional identifier.

@param image The image to be cached.
@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.

@return A BOOL indicating whether or not the image should be added to the cache. YES will cache, NO will prevent caching.
*/
- (BOOL)shouldCacheImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

/**
Adds the image to the cache using an identifier created from the request and additional identifier.

@param image The image to cache.
@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.
*/
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

/**
Removes the image from the cache using an identifier created from the request and additional identifier.

@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.

@return A BOOL indicating whether or not all images were removed from the cache.
*/
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

/**
Returns the image from the cache associated with an identifier created from the request and additional identifier.

@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.

@return An image for the matching request and identifier, or nil.
*/
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

@end

根据请求和标识对图像进行是否需要缓存、加入到缓存或者移除缓存等进行操作。

3. AFAutoPurgingImageCache

这个是这个类的接口,大家注意下这个类遵循协议AFImageRequestCache

/**
The `AutoPurgingImageCache` in an in-memory image cache used to store images up to a given memory capacity. When the memory capacity is reached, the image cache is sorted by last access date, then the oldest image is continuously purged until the preferred memory usage after purge is met. Each time an image is accessed through the cache, the internal access date of the image is updated.
*/
@interface AFAutoPurgingImageCache : NSObject <AFImageRequestCache>

/**
The total memory capacity of the cache in bytes.
*/
// 内存缓存总的字节数
@property (nonatomic, assign) UInt64 memoryCapacity;

/**
The preferred memory usage after purge in bytes. During a purge, images will be purged until the memory capacity drops below this limit.
*/
// 以字节为单位清除后的首选内存使用情况。 在清除过程中,图像将被清除,直到内存容量降至此限制以下。
@property (nonatomic, assign) UInt64 preferredMemoryUsageAfterPurge;

/**
The current total memory usage in bytes of all images stored within the cache.
*/
// 当前所有图像内存缓存使用的总的字节数
@property (nonatomic, assign, readonly) UInt64 memoryUsage;

/**
Initialies the `AutoPurgingImageCache` instance with default values for memory capacity and preferred memory usage after purge limit. `memoryCapcity` defaults to `100 MB`. `preferredMemoryUsageAfterPurge` defaults to `60 MB`.
// 初始化,memoryCapcity为100M,preferredMemoryUsageAfterPurge为60M

@return The new `AutoPurgingImageCache` instance.
*/
- (instancetype)init;

/**
Initialies the `AutoPurgingImageCache` instance with the given memory capacity and preferred memory usage
after purge limit.

@param memoryCapacity The total memory capacity of the cache in bytes.
@param preferredMemoryCapacity The preferred memory usage after purge in bytes.

@return The new `AutoPurgingImageCache` instance.
*/
- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity;

@end

内存中图像缓存中的AutoPurgingImageCache用于存储图像到给定内存容量。 达到内存容量时,图像缓存按上次访问日期排序,然后最旧的图像不断清除,直到满足清除后的首选内存使用量。 每次通过缓存访问图像时,图像的内部访问日期都会更新。


AFAutoPurgingImageCache接口及初始化

从接口描述中我们可以看出来,类的初始化规定了内存总的使用量以及清楚以后的内存最优大小。

- (instancetype)init {
return [self initWithMemoryCapacity:100 * 1024 * 1024 preferredMemoryCapacity:60 * 1024 * 1024];
}

- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity {
if (self = [super init]) {
self.memoryCapacity = memoryCapacity;
self.preferredMemoryUsageAfterPurge = preferredMemoryCapacity;
self.cachedImages = [[NSMutableDictionary alloc] init];

NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(removeAllImages)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];

}
return self;
}

我们看一下这个初始化方法中都做了什么事情

设置置缓存图像的字典

self.cachedImages = [[NSMutableDictionary alloc] init];

常见和UUID关联的并发队列

NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

增加移除所有图像的通知

[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(removeAllImages)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
- (BOOL)removeAllImages {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
if (self.cachedImages.count > 0) {
[self.cachedImages removeAllObjects];
self.currentMemoryUsage = 0;
removed = YES;
}
});
return removed;
}

这里就是在上面生成的队列中,清空数组,重置一些属性值。


AFCachedImage接口及初始化

这里我们就看一下AFCachedImage的接口及初始化。

@interface AFCachedImage : NSObject

@property (nonatomic, strong) UIImage *image;
@property (nonatomic, strong) NSString *identifier;
@property (nonatomic, assign) UInt64 totalBytes;
@property (nonatomic, strong) NSDate *lastAccessDate;
@property (nonatomic, assign) UInt64 currentMemoryUsage;

@end

- (instancetype)initWithImage:(UIImage *)image identifier:(NSString *)identifier {
if (self = [self init]) {
self.image = image;
self.identifier = identifier;

CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
CGFloat bytesPerPixel = 4.0;
CGFloat bytesPerSize = imageSize.width * imageSize.height;
self.totalBytes = (UInt64)bytesPerPixel * (UInt64)bytesPerSize;
self.lastAccessDate = [NSDate date];
}
return self;
}

这个初始化方法里面初始化图像的字节数,并更新上次获取数据的时间。


协议方法的实现

1. AFImageCache协议的实现

将图像根据标识添加到内存

- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier;
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
dispatch_barrier_async(self.synchronizationQueue, ^{
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];

AFCachedImage *previousCachedImage = self.cachedImages[identifier];
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}

self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});

dispatch_barrier_async(self.synchronizationQueue, ^{
if (self.currentMemoryUsage > self.memoryCapacity) {
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
[sortedImages sortUsingDescriptors:@[sortDescriptor]];

UInt64 bytesPurged = 0;

for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
self.currentMemoryUsage -= bytesPurged;
}
});
}

这里用到了两个阻塞

第一个阻塞

dispatch_barrier_async(self.synchronizationQueue, ^{
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];

AFCachedImage *previousCachedImage = self.cachedImages[identifier];
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}

self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});

这里的作用其实很清楚,就是先根据image和identify实例化AFCachedImage对象。然后在字典中根据identifier查看是否有AFCachedImage对象,如果有的话,那么就减小当前使用内存的值。并将前面实例化的AFCachedImage对象存入字典中,并增加当前使用内存的值。

第二个阻塞

dispatch_barrier_async(self.synchronizationQueue, ^{
if (self.currentMemoryUsage > self.memoryCapacity) {
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
[sortedImages sortUsingDescriptors:@[sortDescriptor]];

UInt64 bytesPurged = 0;

for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
self.currentMemoryUsage -= bytesPurged;
}
});

这里完成的功能是,首先判断如果当前内存使用量大于内存总量,那么就需要清理了,这里需要计算需要清理多少内存,就是当前内存值 - 最优内存值。然后sortedImages实例化字典中所有的图片,并对这些图片进行按照时间的排序,遍历这个排序后的数组,逐一从字典中移除,终止条件就是移除的字节数大于上面计算的要清除的字节数值。最后,更新下当前内存使用的值。

根据指定标识将图像移出内存

- (BOOL)removeImageWithIdentifier:(NSString *)identifier;
- (BOOL)removeImageWithIdentifier:(NSString *)identifier {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
if (cachedImage != nil) {
[self.cachedImages removeObjectForKey:identifier];
self.currentMemoryUsage -= cachedImage.totalBytes;
removed = YES;
}
});
return removed;
}

这个还是很好理解的,在定义的并行队列中,取出identifier对应的AFCachedImage对象,然后从字典中移除,并更新当前内存的值。

从内存中移除所有的图像

- (BOOL)removeAllImages;
- (BOOL)removeAllImages {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
if (self.cachedImages.count > 0) {
[self.cachedImages removeAllObjects];
self.currentMemoryUsage = 0;
removed = YES;
}
});
return removed;
}

其实就是一句话,清空字典,更新当前内存使用值。

根据指定的标识符从内存中获取图像

- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier;
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
__block UIImage *image = nil;
dispatch_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
image = [cachedImage accessImage];
});
return image;
}
- (UIImage*)accessImage {
self.lastAccessDate = [NSDate date];
return self.image;
}

其实就是从字典中取值,并更新上次获取图像的时间。

2. AFImageRequestCache协议的实现

根据请求和标识符将图像加入到内存

- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
[self addImage:image withIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
- (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier {
NSString *key = request.URL.absoluteString;
if (additionalIdentifier != nil) {
key = [key stringByAppendingString:additionalIdentifier];
}
return key;
}

这里其实是调用上面我们讲过的那个根据identifier取出AFCachedImage对象的那个方法。不过下面这个identifier是通过调用下面这个方法生成的。、

根据请求和标识符将图像移出内存

- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
return [self removeImageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}

这个,就是还是利用那个生成indentifier的方法,获取identify,然后调用前面我们讲过的方法移除对应的图像。

根据请求和标识符获取内存中图像

- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
return [self imageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}

这个,就是还是利用那个生成indentifier的方法,获取identify,然后调用前面我们讲过的方法获取对应的图像。

是否将图像缓存到内存

- (BOOL)shouldCacheImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (BOOL)shouldCacheImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier {
return YES;
}

这里就是写死的,默认就是需要进行缓存。

后记

本篇主要讲述了关于图像缓存方面的内容,包括使用标识符或者请求进行图像相关的缓存操作

本文转载自:https://cloud.tencent.com/developer/article/1872403

收起阅读 »

Idea不推荐使用@Autowired进行Field注入的原因

大家在使用IDEA开发的时候有没有注意到过一个提示,在字段上使用Spring的依赖注入注解@Autowired后会出现如下警告 Field injection is not recommended (字段注入是不被推荐的) 但是使用@Resource却不会...
继续阅读 »

大家在使用IDEA开发的时候有没有注意到过一个提示,在字段上使用Spring的依赖注入注解@Autowired后会出现如下警告



Field injection is not recommended (字段注入是不被推荐的)



但是使用@Resource却不会出现此提示


网上文章大部分都是介绍两者的区别,没有提到为什么,当时想了好久想出了可能的原因,今天来总结一下


Spring常见的DI方式



  • 构造器注入:利用构造方法的参数注入依赖

  • Setter注入:调用Setter的方法注入依赖

  • 字段注入:在字段上使用@Autowired/Resource注解


@Autowired VS @Resource


事实上,他们的基本功能都是通过注解实现依赖注入,只不过@AutowiredSpring定义的,而@ResourceJSR-250定义的。大致功能基本相同,但是还有一些细节不同:



  • 依赖识别方式:@Autowired默认是byType可以使用@Qualifier指定Name,@Resource默认ByName如果找不到则ByType

  • 适用对象:@Autowired可以对构造器、方法、参数、字段使用,@Resource只能对方法、字段使用

  • 提供方:@Autowired是Spring提供的,@Resource是JSR-250提供的


各种DI方式的优缺点


参考Spring官方文档,建议了如下的使用场景:



  • 构造器注入强依赖性(即必须使用此依赖),不变性(各依赖不会经常变动)

  • Setter注入可选(没有此依赖也可以工作),可变(依赖会经常变动)

  • Field注入:大多数情况下尽量少使用字段注入,一定要使用的话, @Resource相对@Autowired对IoC容器的耦合更低


Field注入的缺点



  • 不能像构造器那样注入不可变的对象

  • 依赖对外部不可见,外界可以看到构造器和setter,但无法看到私有字段,自然无法了解所需依赖

  • 会导致组件与IoC容器紧耦合(这是最重要的原因,离开了IoC容器去使用组件,在注入依赖时就会十分困难)

  • 导致单元测试也必须使用IoC容器,原因同上

  • 依赖过多时不够明显,比如我需要10个依赖,用构造器注入就会显得庞大,这时候应该考虑一下此组件是不是违反了单一职责原则


为什么IDEA只对@Autowired警告


Field注入虽然有很多缺点,但它的好处也不可忽略:那就是太方便了。使用构造器或者setter注入需要写更多业务无关的代码,十分麻烦,而字段注入大幅简化了它们。并且绝大多数情况下业务代码和框架就是强绑定的,完全松耦合只是一件理想上的事,牺牲了敏捷度去过度追求松耦合反而得不偿失。


那么问题来了,为什么IDEA只对@Autowired警告,却对@Resource视而不见呢?


个人认为,就像我们前面提到过的: @AutowiredSpring提供的,它是特定IoC提供的特定注解,这就导致了应用与框架的强绑定,一旦换用了其他的IoC框架,是不能够支持注入的。而 @ResourceJSR-250提供的,它是Java标准,我们使用的IoC容器应当去兼容它,这样即使更换容器,也可以正常工作。


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

官方core-ktx库能对富文本Span开发带来哪些便利?

当前SpannableStringBuilder的使用现状private fun test() { val stringBuilder = SpannableStringBuilder() var length = stringBuilder....
继续阅读 »

当前SpannableStringBuilder的使用现状

private fun test() {
val stringBuilder = SpannableStringBuilder()
var length = stringBuilder.length
stringBuilder.append("开始了")
//设置文本大小
stringBuilder.setSpan(
RelativeSizeSpan(20f),
length,
stringBuilder.length,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
length = stringBuilder.length
stringBuilder.append("执行了")
//设置背景颜色
stringBuilder.setSpan(
BackgroundColorSpan(Color.parseColor("#ffffff")),
length,
stringBuilder.length,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
length = stringBuilder.length
stringBuilder.append("结束了")
//设置点击事件
stringBuilder.setSpan(
object : ClickableSpan() {
override fun onClick(widget: View) {
}
},
length,
stringBuilder.length,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
}

以上代码就实现了三个功能,设置文本大小、背景颜色及点击事件,却写了这么一大坨代码,写起来好麻烦!!

core-ktx库的SpannableStringBuilder扩展

  1. 看下如何构造一个SpannableStringBuilder:

    image.png

    我们就可以在代码中这样使用:

    private fun test4() {
    val build = buildSpannedString {
    //操作各种Span
    }
    }

    请注意,这个buildSpannedString()方法的函数类型属于带接收者的函数类型,意为着我们可以访问SpannableStringBuilder定义的公共的属性方法(包括扩展方法),接下来我们就看下core-ktx库为SpannableStringBuilder提供了哪些扩展方法。

  2. SpannableStringBuilder.backgroundColor()设置背景色:

    image.png

    这个扩展方法需要传入一个颜色值充当背景色,backgroundColor()会自动帮助我们创建一个ForegroundColorSpan对象;还可以传入一个函数类型builderAction,比如用作使用append()方法设置要渲染的文本内容,最终会调用到inSpan()方法:

    image.png

    是不是明白了,最终我们是在这个方法中将xxxSpan设置给SpannableStringBuilder的。最终就可以这样使用了:

    val build = buildSpannedString {
    //操作各种Span
    backgroundColor(Color.RED) {
    append("开始了")
    }
    }
  3. SpannableStringBuilder.bold()设置粗体:

    image.png

    可以看到bold()方法中会自动帮助我们创建一个StyleSpan对象,使用起来和上面差不多:

    val build = buildSpannedString {
    bold {
    append("开始了")
    }
    }
  4. 其他SpannableStringBuilder.xxx()富文本设置扩展:

    core-ktx库提供了很多富文本设置的扩展方法,这里就只介绍上面的两个,其他的就不再这里介绍了,可以自行看下源码:

    image.png

  5. 一个非常非常简单的使用技巧

    假设当前有一小段文本遮天是一群人的完美,完美是一个人的遮天,我想要对整段文本设置一个背景色,对一群人这三个字设置一个粗体大小,利用上面core-ktx库提供的扩展,我们可以这样实现:

    private fun test4() {
    val build = buildSpannedString {
    backgroundColor(Color.RED) {
    append("遮天是")
    bold {
    append("一群人")
    }
    append("的完美,完美是一个人的遮天")
    }
    }
    }

    核心就是SpannableStringBuilder.xxx()系列的富文本扩展方法的第二个参数是一个接收者为SpannableStringBuilder的函数类型,所以backgroundColor()bold()strikeThrough()等等可以相互嵌套使用,从来更简单的实现一些富文本效果。

使用时请注意,buildSpannedString()这个方法创建的SpannableStringBuilder最终会包装成一个SpannedString不可变对象,请根据实际情况使用。

core-ktx库的Spannable扩展

SpannableStringBuilderSpannableString等实现了Spannable接口,所以Spannable定义的扩展方法对常用的SpannableStringBuilderSpannableString同样适用。

  1. Spannable.clearSpans清理所有标识(包括各种Span)

    image.png

    使用时,直接对Spannable及其子类调用clearSpans()即可。

  2. Spannable.set(start: Int, end: Int, span: Any)设置Span

    image.png

    这个扩展方法就比较牛逼了,它是一个运算符重载函数且重载了[xxx]运算符来设置Span的,我们看下使用:

    val stringBuilder = SpannableStringBuilder()
    //设置背景色
    stringBuilder[0, 2] = BackgroundColorSpan(Color.RED)

    有没有眼前一亮的感觉哈!!

  3. Spannable.set(range: IntRange, span: Any)设置Span

    image.png

    这个方法和上一个方法很像,不过传入的设置Span标识范围的方式发生了改变,变成了一个IntRange类型,我们直接看下使用:

    val stringBuilder = SpannableStringBuilder()
    //设置背景色
    stringBuilder[0..3] = BackgroundColorSpan(Color.RED)
  4. CharSequence.toSpannable()转换CharSequenceSpannableString

    image.png

    这个很简单,就不再进行举例说明了。

core-ktx库的Spanned扩展

Spanned的子接口包括我们上面刚讲到的Spannable,所以它定义的扩展方法对于SpannableStringBuilderSpannableString同样适用。

  1. CharSequence.toSpanned()转换CharSequenceSpannedString

    image.png

    注意和isSpannable()转换的区别,一个能设置Span,一个不能设。

  2. Spanned.getSpans()获取指定类型的Span标识

    image.png

    借助于Kotlin的泛型实化reified+inline简化了传入具体Span类型的逻辑,我们看下使用:

    private fun test4(builder: SpannableStringBuilder) {
    val spans = builder.getSpans<BackgroundColorSpan>()
    }

    获取类型为BackgroudColorSpan的所有Span对象,如果我们想要获取所有的Span对象,直接将传入的泛型类型改为Any即可。

  3. Spanned.toHtml()将富文本转换成同等效果显示的html代码

    image.png

    也就是说如果你富文本中存在ImageSpan,转换成html代码时,就会帮你在对应位置添加一个<img src="" />的标签,我们简单看下其核心源码Html.withinParagraph()中的片段:

    image.png

富文本绘制复杂布局的两种技巧

  1. ReplacementSpan这个Span使用非常灵活,它提供了方法draw()可自定义绘制你想要的布局效果;

  2. 如果使用ReplacementSpan自定义绘制布局还是太过于复杂,可以考虑先使用原生组件在xml中实现这个布局效果,然后将这个xml通过Inflate转换成View,并将调用ViewonDraw()方法,手动绘制到我们自定义Bitmap中,经过这个流程,我们就将这个复杂的布局转换成了Bitmap图像,然后使用ImageSpan加载该Bitmap,最终渲染到富文本中即可。

    请注意,请根据实际情况判断,是否需要先手动测量这个转换的View,然后再将其绘制到我们自定义的Bitmap中,否则可能不生效。

总结

以上就是core-ktx库针对于富文本提供的所有扩展方法,核心的源码就在SpannableStringBuilder.ktSpannableString.ktSpannedString.kt这三个文件中,大家有需要请自行查看。


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

收起阅读 »

Android 无障碍监听通知的过程

监听通知 Android 中的 AccessibilityService 可以监听通知信息的变化,首先需要创建一个无障碍服务,这个教程可以自行百度。在无障碍服务的配置文件中,需要以下配置: <accessibility-service ... and...
继续阅读 »

监听通知


Android 中的 AccessibilityService 可以监听通知信息的变化,首先需要创建一个无障碍服务,这个教程可以自行百度。在无障碍服务的配置文件中,需要以下配置:


<accessibility-service
...
android:accessibilityEventTypes="其他内容|typeNotificationStateChanged"
android:canRetrieveWindowContent="true" />

然后在 AccessibilityService 的 onAccessibilityEvent 方法中监听消息:


override fun onAccessibilityEvent(event: AccessibilityEvent?) {
when (event.eventType) {
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED -> {
Log.d(Tag, "Notification: $event")
}
}
}

当有新的通知或 Toast 出现时,在这个方法中就会收到 AccessibilityEvent 。


另一种方案是通过 NotificationListenerService 进行监听,这里不做详细介绍了。两种方案的应用场景不同,推荐使用 NotificationListenerService 而不是无障碍服务。stackoverflow 上一个比较好的回答:



It depends on WHY you want to read it. The general answer would be Notification Listener. Accessibility Services are for unique accessibility services. A user has to enable an accessibility service from within the Accessibility Service menu (where TalkBack and Switch Access are). Their ability to read notifications is a secondary ability, to help them achieve the goal of creating assistive technologies (alternative ways for people to interact with mobile devices).


Whereas, Notification Listeners, this is their primary goal. They exist as part of the context of an app and as such don't need to be specifically turned on from the accessibility menu.


Basically, unless you are in fact building an accessibility service, you should not use this approach, and go with the generic Notification Listener.



无障碍服务监听通知逻辑


从用法中可以看出一个关键信息 -- TYPE_NOTIFICATION_STATE_CHANGED ,通过这个事件类型入手,发现它用于两个类中:



  • ToastPresenter:用于在应用程序进程中展示系统 UI 样式的 Toast 。

  • NotificationManagerService:通知管理服务。


ToastPresenter


ToastPresenter 的 trySendAccessibilityEvent 方法中,构建了一个 TYPE_NOTIFICATION_STATE_CHANGED 类型的消息:


public void trySendAccessibilityEvent(View view, String packageName) {
if (!mAccessibilityManager.isEnabled()) {
return;
}
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
event.setClassName(Toast.class.getName());
event.setPackageName(packageName);
view.dispatchPopulateAccessibilityEvent(event);
mAccessibilityManager.sendAccessibilityEvent(event);
}

这个方法的调用在 ToastPresenter 中的 show 方法中:


public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
int xOffset, int yOffset, float horizontalMargin, float verticalMargin,
@Nullable ITransientNotificationCallback callback) {
// ...
trySendAccessibilityEvent(mView, mPackageName);
// ...
}

而这个方法的调用就是在 Toast 中的 TN 类中的 handleShow 方法。


Toast.makeText(this, "", Toast.LENGTH_SHORT).show()

在 Toast 的 show 方法中,获取了一个 INotificationManager ,这个是 NotificationManagerService 在客户端暴露的 Binder 对象,通过这个 Binder 对象的方法可以调用 NMS 中的逻辑。


也就是说,Toast 的 show 方法调用了 NMS :


public void show() {
// ...
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
final int displayId = mContext.getDisplayId();

try {
if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
if (mNextView != null) {
// It's a custom toast
service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
} else {
// It's a text toast
ITransientNotificationCallback callback = new CallbackBinder(mCallbacks, mHandler);
service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback);
}
} else {
service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
}
} catch (RemoteException e) {
// Empty
}
}

这里是 enqueueToast 方法中,最后调用:


private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text,
@Nullable ITransientNotification callback, int duration, int displayId,
@Nullable ITransientNotificationCallback textCallback) {
// ...
record = getToastRecord(callingUid, callingPid, pkg, token, text, callback, duration, windowToken, displayId, textCallback);
// ...
}

getToastRecord 中根据 callback 是否为空产生了不同的 Toast :


private ToastRecord getToastRecord(int uid, int pid, String packageName, IBinder token,
@Nullable CharSequence text, @Nullable ITransientNotification callback, int duration,
Binder windowToken, int displayId,
@Nullable ITransientNotificationCallback textCallback) {
if (callback == null) {
return new TextToastRecord(this, mStatusBar, uid, pid, packageName, token, text,duration, windowToken, displayId, textCallback);
} else {
return new CustomToastRecord(this, uid, pid, packageName, token, callback, duration, windowToken, displayId);
}
}

两者的区别是展示对象的不同:




  • TextToastRecord 因为 ITransientNotification 为空,所以它是通过 mStatusBar 进行展示的:


        @Override
    public boolean show() {
    if (DBG) {
    Slog.d(TAG, "Show pkg=" + pkg + " text=" + text);
    }
    if (mStatusBar == null) {
    Slog.w(TAG, "StatusBar not available to show text toast for package " + pkg);
    return false;
    }
    mStatusBar.showToast(uid, pkg, token, text, windowToken, getDuration(), mCallback);
    return true;
    }



  • CustomToastRecord 调用 ITransientNotification 的 show 方法:


        @Override
    public boolean show() {
    if (DBG) {
    Slog.d(TAG, "Show pkg=" + pkg + " callback=" + callback);
    }
    try {
    callback.show(windowToken);
    return true;
    } catch (RemoteException e) {
    Slog.w(TAG, "Object died trying to show custom toast " + token + " in package "
    + pkg);
    mNotificationManager.keepProcessAliveForToastIfNeeded(pid);
    return false;
    }
    }

    这个 callback 最在 Toast.show() 时传进去的 TN :


    TN tn = mTN;
    service.enqueueToast(pkg, mToken, tn, mDuration, displayId);

    也就是调用到了 TN 的 show 方法:


            @Override
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
    }




TN 的 show 方法中通过 mHandler 来传递了一个类型是 SHOW 的消息:


            mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try {
getService().cancelToast(mPackageName, mToken);
} catch (RemoteException e) {
}
break;
}
}
}
};

而这个 Handler 在处理 SHOW 时,会调用 handleShow(token) 这个方法里面也就是会触发 ToastPresenter 的 show 方法的地方:


public void handleShow(IBinder windowToken) {
// If a cancel/hide is pending - no need to show - at this point
// the window token is already invalid and no need to do any work.
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
// 【here】
mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY, mHorizontalMargin, mVerticalMargin, new CallbackBinder(getCallbacks(), mHandler));
}
}

本章节最开始介绍到了 ToastPresenter 的 show 方法中会调用 trySendAccessibilityEvent 方法,也就是从这个方法发送类型是 TYPE_NOTIFICATION_STATE_CHANGED 的无障碍消息给无障碍服务的。


NotificationManagerService


在通知流程中,是通过 NMS 中的 sendAccessibilityEvent 方法来向无障碍发送消息的:


void sendAccessibilityEvent(Notification notification, CharSequence packageName) {
if (!mAccessibilityManager.isEnabled()) {
return;
}

AccessibilityEvent event =
AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
event.setPackageName(packageName);
event.setClassName(Notification.class.getName());
event.setParcelableData(notification);
CharSequence tickerText = notification.tickerText;
if (!TextUtils.isEmpty(tickerText)) {
event.getText().add(tickerText);
}

mAccessibilityManager.sendAccessibilityEvent(event);
}

这个方法的调用有两处,均在 NMS 的 buzzBeepBlinkLocked 方法中,buzzBeepBlinkLocked 方法是用来处理通知是否应该发出铃声、震动或闪烁 LED 的。省略无关逻辑:


int buzzBeepBlinkLocked(NotificationRecord record) {
// ...
if (!record.isUpdate && record.getImportance() > IMPORTANCE_MIN && !suppressedByDnd) {
sendAccessibilityEvent(notification, record.getSbn().getPackageName());
sentAccessibilityEvent = true;
}

if (aboveThreshold && isNotificationForCurrentUser(record)) {
if (mSystemReady && mAudioManager != null) {
// ...
if (hasAudibleAlert && !shouldMuteNotificationLocked(record)) {
if (!sentAccessibilityEvent) {
sendAccessibilityEvent(notification, record.getSbn().getPackageName());
sentAccessibilityEvent = true;
}
// ...
} else if ((record.getFlags() & Notification.FLAG_INSISTENT) != 0) {
hasValidSound = false;
}
}
}
// ...
}

buzzBeepBlinkLocked 的调用路径有两个:




  • handleRankingReconsideration 方法中 RankingHandlerWorker (这是一个 Handler)调用 handleMessage 处理 MESSAGE_RECONSIDER_RANKING 类型的消息:


    @Override
    public void handleMessage(Message msg) {
    switch (msg.what) {
    case MESSAGE_RECONSIDER_RANKING:
    handleRankingReconsideration(msg);
    break;
    case MESSAGE_RANKING_SORT:
    handleRankingSort();
    break;
    }
    }

    handleRankingReconsideration 方法中调用了 buzzBeepBlinkLocked :


    private void handleRankingReconsideration(Message message) {
    // ...
    synchronized (mNotificationLock) {
    // ...
    if (interceptBefore && !record.isIntercepted()
    && record.isNewEnoughForAlerting(System.currentTimeMillis())) {
    buzzBeepBlinkLocked(record);
    }
    }
    if (changed) {
    mHandler.scheduleSendRankingUpdate();
    }
    }



  • PostNotificationRunnable 的 run 方法。




PostNotificationRunnable


这个东西是用来发送通知并进行处理的,例如提示和重排序等。


PostNotificationRunnable 的构建和 post 在 EnqueueNotificationRunnable 中。在 EnqueueNotificationRunnable 的 run 最后,进行了 post:


public void run() {
// ...
// tell the assistant service about the notification
if (mAssistants.isEnabled()) {
mAssistants.onNotificationEnqueuedLocked(r);
mHandler.postDelayed(new PostNotificationRunnable(r.getKey()), DELAY_FOR_ASSISTANT_TIME);
} else {
mHandler.post(new PostNotificationRunnable(r.getKey()));
}
}

EnqueueNotificationRunnable 在 enqueueNotificationInternal 方法中使用,enqueueNotificationInternal 方法是 INotificationManager 接口中定义的方法,它的实现在 NotificationManager 中:


    public void notifyAsPackage(@NonNull String targetPackage, @Nullable String tag, int id,
@NonNull Notification notification) {
INotificationManager service = getService();
String sender = mContext.getPackageName();

try {
if (localLOGV) Log.v(TAG, sender + ": notify(" + id + ", " + notification + ")");
service.enqueueNotificationWithTag(targetPackage, sender, tag, id,
fixNotification(notification), mContext.getUser().getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}

@UnsupportedAppUsage
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
{
INotificationManager service = getService();
String pkg = mContext.getPackageName();

try {
if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
fixNotification(notification), user.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}

一般发送一个通知都是通过 NotificationManager 或 NotificationManagerCompat 来发送的,例如:


NotificationManagerCompat.from(this).notify(1, builder.build());

NotificationManagerCompat 中的 notify 方法本质上调用的是 NotificationManager:


// NotificationManagerCompat
public void notify(int id, @NonNull Notification notification) {
notify(null, id, notification);
}

public void notify(@Nullable String tag, int id, @NonNull Notification notification) {
if (useSideChannelForNotification(notification)) {
pushSideChannelQueue(new NotifyTask(mContext.getPackageName(), id, tag, notification));
// Cancel this notification in notification manager if it just transitioned to being side channelled.
mNotificationManager.cancel(tag, id);
} else {
mNotificationManager.notify(tag, id, notification);
}
}

mNotificationManager.notify(tag, id, notification) 中的实现:


public void notify(String tag, int id, Notification notification) {
notifyAsUser(tag, id, notification, mContext.getUser());
}

public void cancel(@Nullable String tag, int id) {
cancelAsUser(tag, id, mContext.getUser());
}

串起来了,最终就是通过 NotificationManager 的 notify 相关方法发送通知,然后触发了通知是否要触发铃声/震动/LED 闪烁的逻辑,并且在这个逻辑中,发送出了无障碍消息。


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

监控主线程耗时操作,从开发中解决ANR

ANR
背景:在 debug 环境中主线程中方法体执行的时间与指定的时间做对比后的堆栈信息,针对性的优化超过指定时间的耗时方法体,减少 ANR 的发生此工具类主要是通过向主线程Looper打印超过指定时间的耗时堆栈信息以及耗时时...
继续阅读 »

背景:

在 debug 环境中主线程中方法体执行的时间与指定的时间做对比后的堆栈信息,针对性的优化超过指定时间的耗时方法体,减少 ANR 的发生

此工具类主要是通过向主线程Looper打印超过指定时间的耗时堆栈信息以及耗时时长,其中校验时间自已定义,主动查看主线程中的耗时操作,防患未然。

原理:

此工具类为最简单最直接处理、优化耗时操作的工具

大家都知道Android 对于ANR的判断标准:

最简单的一句话就是:ANR——应用无响应,Activity是5秒,BroadCastReceiver是10秒,Service是20秒

然后此工具类的方案就是将主线程的堆栈信息作时间对比监控,超时的打印出来

Looper.loop 解析:

  1. 应用之所以未退出,就是运行在loop 中,如果有阻塞loop 的操作就会发生ANR、崩溃
public static void loop() {
final Looper me = myLooper();
//....
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}
  1. 主要看死循环

loopOnce

private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}

// This must be in a local variable, in case a UI event sets the logger
// *当有任务的时候打印Dispatching to *
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what);
}
//.... 中间部分未任务执行的代码

//执行结束之后打印 Finished to
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}

msg.recycleUnchecked();

return true;
}
  1. 上述注释之间的耗时就是主线程在执行某个任务时的耗时,我们只要拿这个时间和指定时间相比就能监控主线程的耗时堆栈信息了

使用方式:

  1. Application:
 //主线程中方法体执行的时间与指定的时间做对比后的堆栈信息,针对性的优化超过指定时间的耗时方法体,
MainThreadDoctor.init(500)
  1. 查看日志:

image.png

日志等级为明显起见使用error级别

工具类:

 /**
* @author kong
* @date 2022/7/6 15:55
* @description 在debug环境中主线程中方法体执行的时间与指定的时间做对比后的堆栈信息,针对性的优化超过指定时间的耗时方法体,减少ANR的发生
**/
object MainThreadDoctor {

private var startTime = 0L
private var currentJob: Job? = null
private const val START = ">>>>> Dispatching"
private const val END = "<<<<< Finished"

fun init(diagnoseStandardTime: Long) {
if (BuildConfigs.DEBUG) {
diagnoseFromMainThread(diagnoseStandardTime)
}
}

/**
* @param diagnoseStandardTime 执行诊断的标准时间
*/
fun diagnoseFromMainThread(diagnoseStandardTime: Long) {
Looper.getMainLooper().setMessageLogging {
if (it.startsWith(START)) {
startTime = System.currentTimeMillis()
currentJob = GlobalScope.launch(Dispatchers.IO) {
delay(diagnoseStandardTime)
val stackTrace = Looper.getMainLooper().thread.stackTrace
val builder = StringBuilder()
for (s in stackTrace) {
builder.append(s.toString())
builder.append("\n")
}
PPLog.e("looperMessageMain $builder")
}
}

if (it.startsWith(END)) {
if (currentJob?.isCompleted == false) {
currentJob?.cancel()
} else {
PPLog.e("looperMessageMain 总时间 = ${System.currentTimeMillis() - startTime} 毫秒")
}
}
}
}
}


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

收起阅读 »

听说Compose与RecyclerView结合会有水土不服?

背景&笔者碎碎谈 最近Compose也慢慢火起来了,作为google力推的ui框架,我们也要用起来才能进步呀!在最新一期的评测中LazyRow等LazyXXX列表组件已经慢慢逼近RecyclerView的性能了!但是还是有很多同学顾虑呀!没关系,我们就...
继续阅读 »

背景&笔者碎碎谈


最近Compose也慢慢火起来了,作为google力推的ui框架,我们也要用起来才能进步呀!在最新一期的评测中LazyRow等LazyXXX列表组件已经慢慢逼近RecyclerView的性能了!但是还是有很多同学顾虑呀!没关系,我们就算用原有的view开发体系,也可以快速迁移到compose,这个利器就是ComposeView了,那么我们在RecyclerView的基础上,集成Compose用起来!这样我们有RecyclerView的性能又有Compose的好处不是嘛!相信很多人都有跟我一样的想法,但是这两者结合起来可是有隐藏的性能开销!(本次使用compose版本为1.1.1)


在原有view体系接入Compose


在纯compose项目中,我们都会用setContent代替原有view体系的setContentView,比如


setContent {
ComposeTestTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Greeting("Android")
Hello()
}
}
}

那么setContent到底做了什么事情呢?我们看下源码


public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView

if (existingComposeView != null) with(existingComposeView) {
setParentCompositionContext(parent)
setContent(content)
} else ComposeView(this).apply {
// 第一步走到这里
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
}

由于是第一次进入,那么一定就走到了else分支,其实就是创建了一个ComposeView,放在了android.R.id.content里面的第一个child中,这里就可以看到,compose并不是完全脱了原有的view体系,而是采用了移花接木的方式,把compose体系迁移了过来!ComposeView就是我们能用Compose的前提啦!所以在原有的view体系中,我们也可以通过ComposeView去“嫁接”到view体系中,我们举个例子


class CustomActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom)
val recyclerView = this.findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.adapter = MyRecyclerViewAdapter()
recyclerView.layoutManager = LinearLayoutManager(this)
}
}


class MyRecyclerViewAdapter:RecyclerView.Adapter<MyComposeViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyComposeViewHolder {
val view = ComposeView(parent.context)
return MyComposeViewHolder(view)
}

override fun onBindViewHolder(holder: MyComposeViewHolder, position: Int) {
holder.composeView.setContent {
Text(text = "test $position", modifier = Modifier.size(200.dp).padding(20.dp), textAlign = TextAlign.Center)
}

}

override fun getItemCount(): Int {
return 200
}
}

class MyComposeViewHolder(val composeView:ComposeView):RecyclerView.ViewHolder(composeView){

}

这样一来,我们的compose就被移到了RecyclerView中,当然,每一列其实就是一个文本。嗯!普普通通,好像也没啥特别的对吧,假如这个时候你打开了profiler,当我们向下滑动的时候,会发现内存会慢慢的往上浮动


image.png
滑动嘛!有点内存很正常,毕竟谁不生成对象呢,但是这跟我们平常用RecyclerView的时候有点差异,因为RecyclerView滑动的涨幅可没有这个大,那究竟是什么原因导致的呢?


探究Compose


有过对Compose了解的同学可能会知道,Compose的界面构成会有一个重组的过程,当然!本文就不展开聊重组了,因为这类文章有挺多的(填个坑,如果有机会就填),我们聊点特别的,那么什么时候停止重组呢?或者说什么时候这个Compose被dispose掉,即不再参与重组!


Dispose策略


其实我们的ComposeView,以1.1.1版本为例,其实创建的时候,也创建了取消重组策略,即


@Suppress("LeakingThis")
private var disposeViewCompositionStrategy: (() -> Unit)? =
ViewCompositionStrategy.DisposeOnDetachedFromWindow.installFor(this)

这个策略是什么呢?我们点进去看源码


object DisposeOnDetachedFromWindow : ViewCompositionStrategy {
override fun installFor(view: AbstractComposeView): () -> Unit {
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}

override fun onViewDetachedFromWindow(v: View?) {
view.disposeComposition()
}
}
view.addOnAttachStateChangeListener(listener)
return { view.removeOnAttachStateChangeListener(listener) }
}
}

看起来是不是很简单呢,其实就加了一个监听,在onViewDetachedFromWindow的时候调用的view.disposeComposition(),声明当前的ComposeView不参与接下来的重组过程了,我们再继续看


fun disposeComposition() {
composition?.dispose()
composition = null
requestLayout()
}

再看dispose方法


override fun dispose() {
synchronized(lock) {
if (!disposed) {
disposed = true
composable = {}
val nonEmptySlotTable = slotTable.groupsSize > 0
if (nonEmptySlotTable || abandonSet.isNotEmpty()) {
val manager = RememberEventDispatcher(abandonSet)
if (nonEmptySlotTable) {
slotTable.write { writer ->
writer.removeCurrentGroup(manager)
}
applier.clear()
manager.dispatchRememberObservers()
}
manager.dispatchAbandons()
}
composer.dispose()
}
}
parent.unregisterComposition(this)
}

那么怎么样才算是不参与接下里的重组呢,其实就是这里


slotTable.write { writer ->
writer.removeCurrentGroup(manager)
}

...
composer.dispose()

而removeCurrentGroup其实就是把当前的group移除了


for (slot in groupSlots()) {
when (slot) {
....
is RecomposeScopeImpl -> {
val composition = slot.composition
if (composition != null) {
composition.pendingInvalidScopes = true
slot.composition = null
}
}
}
}

这里又多了一个概念,slottable,我们可以这么理解,这里面就是Compose的快照系统,其实就相当于对应着某个时刻view的状态!之所以Compose是声明式的,就是通过slottable里的slot去判断,如果最新的slot跟前一个slot不一致,就回调给监听者,实现更新!这里又是一个大话题了,我们点到为止


image.png


跟RecyclerView有冲突吗


我们看到,默认的策略是当view被移出当前的window就不参与重组了,嗯!这个在99%的场景都是有效的策略,因为你都看不到了,还重组干嘛对吧!但是这跟我们的RecyclerView有什么冲突吗?想想看!诶,RecyclerView最重要的是啥,Recycle呀,就是因为会重复利用holder,间接重复利用了view才显得高效不是嘛!那么问题就来了


image.png
如图,我们item5其实完全可以利用item1进行显示的对不对,差别就只是Text组件的文本不一致罢了,但是我们从上文的分析来看,这个ComposeView对应的composition被回收了,即不参与重组了,换句话来说,我们Adapter在onBindViewHolder的时候,岂不是用了一个没有compositon的ComposeView(即不能参加重组的ComposeView)?这样怎么行呢?我们来猜一下,那么这样的话,RecyclerView岂不是都要生成新的ComposeView(即每次都调用onCreateViewHolder)才能保证正确?emmm,很有道理,但是却不是的!如果我们把代码跑起来看的话,复用的时候依旧是会调用onBindViewHolder,这就是Compose的秘密了,那么这个秘密在哪呢


override fun onBindViewHolder(holder: MyComposeViewHolder, position: Int) {
holder.composeView.setContent {
Text(text = "test $position", modifier = Modifier.size(200.dp).padding(20.dp), textAlign = TextAlign.Center)
}

}

其实就是在ComposeView的setContent方法中,


fun setContent(content: @Composable () -> Unit) {
shouldCreateCompositionOnAttachedToWindow = true
this.content.value = content
if (isAttachedToWindow) {
createComposition()
}
}

fun createComposition() {
check(parentContext != null || isAttachedToWindow) {
"createComposition requires either a parent reference or the View to be attached" +
"to a window. Attach the View or call setParentCompositionReference."
}
ensureCompositionCreated()
}

最终调用的是


private fun ensureCompositionCreated() {
if (composition == null) {
try {
creatingComposition = true
composition = setContent(resolveParentCompositionContext()) {
Content()
}
} finally {
creatingComposition = false
}
}
}

看到了吗!如果composition为null,就会重新创建一个!这样ComposeView就完全嫁接到RecyclerView中而不出现问题了!


其他Dispose策略


我们看到,虽然在ComposeView在RecyclerView中能正常运行,但是还存在缺陷对不对,因为每次复用都要重新创建一个composition对象是不是!归根到底就是,我们默认的dispose策略不太适合这种拥有复用逻辑或者自己生命周期的组件使用,那么有其他策略适合RecyclerView吗?别急,其实是有的,比如DisposeOnViewTreeLifecycleDestroyed


object DisposeOnViewTreeLifecycleDestroyed : ViewCompositionStrategy {
override fun installFor(view: AbstractComposeView): () -> Unit {
if (view.isAttachedToWindow) {
val lco = checkNotNull(ViewTreeLifecycleOwner.get(view)) {
"View tree for $view has no ViewTreeLifecycleOwner"
}
return installForLifecycle(view, lco.lifecycle)
} else {
// We change this reference after we successfully attach
var disposer: () -> Unit
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
val lco = checkNotNull(ViewTreeLifecycleOwner.get(view)) {
"View tree for $view has no ViewTreeLifecycleOwner"
}
disposer = installForLifecycle(view, lco.lifecycle)

// Ensure this runs only once
view.removeOnAttachStateChangeListener(this)
}

override fun onViewDetachedFromWindow(v: View?) {}
}
view.addOnAttachStateChangeListener(listener)
disposer = { view.removeOnAttachStateChangeListener(listener) }
return { disposer() }
}
}
}

然后我们在ViewHolder的init方法中对composeview设置一下就可以了


class MyComposeViewHolder(val composeView:ComposeView):RecyclerView.ViewHolder(composeView){
init {
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
}
}

为什么DisposeOnViewTreeLifecycleDestroyed更加适合呢?我们可以看到在onViewAttachedToWindow中调用了
installForLifecycle(view, lco.lifecycle) 方法,然后就removeOnAttachStateChangeListener,保证了该ComposeView创建的时候只会被调用一次,那么removeOnAttachStateChangeListener又做了什么呢?


val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
view.disposeComposition()
}
}
lifecycle.addObserver(observer)
return { lifecycle.removeObserver(observer) }

可以看到,是在对应的生命周期事件为ON_DESTROY(Lifecycle.Event跟activity生命周期不是一一对应的,要注意)的时候,才调用view.disposeComposition(),本例子的lifecycleOwner就是CustomActivity啦,这样就保证了只有当前被lifecycleOwner处于特定状态的时候,才会销毁,这样是不是就提高了compose的性能了!


扩展


我们留意到了Compose其实存在这样的小问题,那么如果我们用了其他的组件类似RecyclerView这种的怎么办,又或者我们的开发没有读过这篇文章怎么办!(ps:看到这里的同学还不点赞点赞),没关系,官方也注意到了,并且在1.3.0-alpha02以上版本添加了更换了默认策略,我们来看一下


val Default: ViewCompositionStrategy
get() = DisposeOnDetachedFromWindowOrReleasedFromPool

object DisposeOnDetachedFromWindowOrReleasedFromPool : ViewCompositionStrategy {
override fun installFor(view: AbstractComposeView): () -> Unit {
val listener = object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}

override fun onViewDetachedFromWindow(v: View) {
// 注意这里
if (!view.isWithinPoolingContainer) {
view.disposeComposition()
}
}
}
view.addOnAttachStateChangeListener(listener)

val poolingContainerListener = PoolingContainerListener { view.disposeComposition() }
view.addPoolingContainerListener(poolingContainerListener)

return {
view.removeOnAttachStateChangeListener(listener)
view.removePoolingContainerListener(poolingContainerListener)
}
}
}

DisposeOnDetachedFromWindow从变成了DisposeOnDetachedFromWindowOrReleasedFromPool,其实主要变化点就是一个view.isWithinPoolingContainer = false,才会进行dispose,isWithinPoolingContainer定义如下


image.png


也就是说,如果我们view的祖先存在isPoolingContainer = true的时候,就不会进行dispose啦!所以说,如果我们的自定义view是这种情况,就一定要把isPoolingContainer变成true才不会有隐藏的性能开销噢!当然,RecyclerView也要同步到1.3.0-alpha02以上才会有这个属性改写!现在稳定版本还是会存在本文的隐藏性能开销,请注意噢!不过相信看完这篇文章,性能优化啥的,不存在了对不对!


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

反对居家办公,马斯克尴尬了:车没地停、工位不够坐、Wi-Fi还太差

每一个特斯拉员工每周都要在办公室工作 40 个小时……如果你不来,那么我们就认为你辞职了。在马斯克“蛮横”地放出了这番话后,迫于失业危机,许多特斯拉员工只能整顿心情并起个大早,只为及时到达办公室——位于美国加利福尼亚州弗里蒙特的特斯拉工厂。万万没想到,员工做好...
继续阅读 »


每一个特斯拉员工每周都要在办公室工作 40 个小时……如果你不来,那么我们就认为你辞职了。

在马斯克“蛮横”地放出了这番话后,迫于失业危机,许多特斯拉员工只能整顿心情并起个大早,只为及时到达办公室——位于美国加利福尼亚州弗里蒙特的特斯拉工厂。

万万没想到,员工做好了返回办公室上班的准备,特斯拉自己却状况百出。

1 车没地停、工位不够、Wi-Fi 太差

首先是停车位问题。很多特斯拉员工花费数小时驱车赶往公司后,面临的第一个问题就是:停车位早已爆满。转悠了好几圈之后,他们只能无奈选择把车停在附近的 BART(一种有轨捷运交通)车站,再转车去上班。

好不容易历经周折到达公司后,看到工位上乌泱泱地挤满了同受马斯克“威胁”来上班的人,员工们再次眉头一紧:我们坐哪里办公?据了解,特斯拉曾在疫情期间将弗里蒙特工厂进行了一些区域调动,新员工不知所措的同时,老员工也一片茫然。

当部分员工幸运地在角落找到位置坐下、以为终于可以开始投入工作后,没想到居然还有一道坎:Wi-Fi 信号太差,差到大多数员工根本无法正常工作。

……就很想问马斯克一句:你当初在威胁员工来公司上班时,是不是忘记了你公司的员工人数早在疫情期间已经翻一番了?

根据外媒 The Information 的数据,从 2019 年到 2021 年,即特斯拉允许远程办公后,其员工人数已增加至 99210 名员工,几乎翻了一番,其中光弗里蒙特工厂就有 2 万多名员工。

在此情形下,即便马斯克一心要求,但由于特斯拉目前无法有效处理因重返办公司政策引发的一系列问题,有些员工被要求每周来公司上班的天数不得超过 5 天。

然而,结合特斯拉最近的裁员计划,这一安排又令许多人心生担忧。

2 暂停全球招聘,计划裁员 10%

本月初,在马斯克发布要求员工每周到岗办公 40 小时的通知后,又在 6 月 2 日向公司高管发送了一封名为“暂停全球所有招聘”的电子邮件。据路透社报道,马斯克在邮件中表示,他对经济形势“感觉非常糟糕”,需要裁员约 10%,并暂停全球招聘。

而上周,一些特斯拉前员工宣布起诉,称特斯拉要裁员 10% 的决定违反了联邦法律,因为没有提供裁员所需的提前通知,即“警告法案”(WARN Act),其中要求公司在任何影响到 50 名或以上员工的大规模裁员前需提前 60 天通知。

据起诉书称,在被裁的 500 多名特斯拉员工中,John Lynch 在 6 月 10 日收到被解雇的通知,立即生效,Daxton Hartsfield 也在 6 月 15 日收到即刻生效的解雇通知。

因而,面临这次的裁员风波,目前在职的特斯拉员工大多都牟足了劲,想凭借勤勉优秀、不出错的表现躲过这次大规模裁员。实际上,马斯克之前在推特全体大会上曾透露,到岗办公并非强制要求,一些“优秀”人才仍然可以选择远程工作。

只是,对于这个毫无具体说明的“优秀”标准,大多数员工都不敢赌,因此应马斯克要求选择到岗办公的员工人数自然也就暴涨——但从目前的情况来看,特斯拉显然没有做好同时容纳这么多到岗员工的准备。

参考链接:

本文转自公众号“CSDN”,ID:CSDNnews

收起阅读 »

插件化工程R文件瘦身技术方案

随着业务的发展及版本迭代,客户端工程中不断增加新的业务逻辑、引入新的资源,随之而来的问题就是安装包体积变大,前期各个业务模块通过无用资源删减、大图压缩或转上云、AB实验业务逻辑下线或其他手段在降低包体积上取得了一定的成果。在瘦身的过程中我们关注到了R文件瘦身的...
继续阅读 »

随着业务的发展及版本迭代,客户端工程中不断增加新的业务逻辑、引入新的资源,随之而来的问题就是安装包体积变大,前期各个业务模块通过无用资源删减、大图压缩或转上云、AB实验业务逻辑下线或其他手段在降低包体积上取得了一定的成果。

在瘦身的过程中我们关注到了R文件瘦身的概念,目前京东APP是支持插件化的,有业务插件工程、宿主工程,对业务插件包文件进行分析,发现除了常规的资源及代码外,R类文件大概占包体积的3%~5%左右,对宿主工程包文件进行分析,R类文件占比也有3%左右。我们先后在对R类文件瘦身的可行性及业界开源项目进行调研后,探索出了一套适用于插件化工程的R文件瘦身技术方案。

理论基础—R文件

R文件也就是我们日常工作中经常打交道的R.java文件,在Android开发规范中我们需要将应用中用到的资源分别放入专门命名的资源目录中,外部化应用资源以便对其进行单独维护。


外部化应用资源后,我们可在项目中使用R类ID来访问这些资源,且R类ID具有唯一性。

public class MainActivity extends BaseActivity {
  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
  }
}

在android apk打包流程中R类文件是由aapt(Android Asset Packaing Tool)工具打包生成的,在生成R类文件的同时对资源文件进行编译,生成resource.arsc文件,resource.arsc文件相当于一个文件索引表,应用层代码通过R类ID 可以访问到对应的资源。


R文件瘦身的可行性分析

日常开发阶段,在主工程中通过R.xx.xx的方式引用资源,经过编译后R类引用对应的常量会被编译进class中。

setContentView(2131427356);

这种变化叫做内联,内联是java的一种机制(如果一个常量被标记为static final,在java编译的过程中会将常量内联到代码中,减少一次变量的内存寻址)。

非主工程中,R类资源ID以引用的方式编译进class中,不会产生内联。

setContentView(R.layout.activity_main);

产生这种现象的原因是AGP打包工具导致的。具体细节,大家可以去查阅一下android gradle plugin在R文件上的处理过程。

结论:R类id内联后程序可运行,但并非所有的工程都会自动产生内联现象,我们需要通过技术手段在合适的时机将R类id内联到程序中,内联完成后,由于不再依赖R类文件,则可以将R类文件删除,在应用正常运行的同时,达到包瘦身目的。

插件化工程R文件瘦身实战

制定技术方案

目前京东Android客户端是支持插件化的,整个插件化工程包含公共库(是一个aar工程,用来存放组件和宿主共用的类和资源)、业务插件(插件工程是一个独立的工程,编译产物可以运行在宿主环境中)、宿主(主工程,提供运行环境)。在插件化的过程中为了防止宿主和插件资源冲突,通过修改插件packageId保证了资源的唯一性。由于公共资源库、宿主是被很多业务依赖,对这两个项目进行改动评估影响涉及比较多,插件一般都是业务模块自行维护,不存在被依赖问题,所以先在业务插件模块进行R类瘦身实践。

对业务插件工程打出的包进行反编译以后,发现R类ID无内联现象,且R类文件具有一定的大小,对包内的R文件进行分析,发现R文件中仅包含业务自身的资源,不包含业务依赖的公共资源R类。

public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, Bundle paramBundle)
{
  this.b = paramLayoutInflater.inflate(R.layout.lib_pd_main_page, paramViewGroup, false);
  this.h = (PDBuyStatusView) this.b.findViewById(R.id.pd_buy_status_view);
  this.f = (PageRecyclerView) this.b.findViewById(R.id.lib_pd_recycle_view);
}


结合对业界开源项目的调研分析,尝试制定符合京东商城的技术方案并优先在业务插件内完成R类ID内联并删除对应的R文件。

1.通过**transform** api 收集要处理的class文件

Transform 是 Android Gradle 提供的操作字节码的一种方式,它在 class 编译成 dex 之前通过一系列 Transform 处理来实现修改.class文件。

@Override
public void transform(TransformInvocation transformInvocationthrows TransformExceptionInterruptedExceptionIOException {
super.transform(transformInvocation);
// 通过TransformInvocation.getInputs()获取输入文件,有两种
// DirectoryInpu以源码方式参与编译的目录结构及目录下的文件
// JarInput以jar包方式参与编译的所有jar包
   allDirs = new ArrayList<>(invocation.getInputs().size());
   allJars = new ArrayList<>(invocation.getInputs().size());
   Collection<TransformInput> inputs = invocation.getInputs();
   for (TransformInput input : inputs) {
       Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
        for (DirectoryInput directoryInput : directoryInputs) {
              allDirs.add(directoryInput.getFile());
            }
           Collection<JarInput> jarInputs = input.getJarInputs();
        for (JarInput jarInput : jarInputs) {
               allJars.add(jarInput.getFile());
            }
    }
}

2.对收集到的.class文件结合ASM框架进行分析处理

ASM是一个操作Java字节码的类库,通过ASM我们可以方便对.class文件进行修改。

优先识别R类文件,通过ClassVisitor访问R.class文件,读取文件中的静态常量,进行临时变量存储:

@Overridepublic FieldVisitor visitField(int access, String name, String desc, String signature, Object value)
{ //R类中收集 
  public static final int 对应的变量
  if(JDASMUtil.isPublic(access) && JDASMUtil.isStatic(access) && JDASMUtil.isFinal(access) && JDASMUtil.isInt(desc))
  {
      jdRstore.addInlineRField(className, name, value);
  }
  return super.visitField(access, name, desc, signature, value);
}

非R类文件,通过MethodVisitor识别到代码中的R类引用,获取引用对应的值,进行id值替换:

@Override
   public void visitFieldInsn(int opcodeString ownerString nameString desc) {
       if (opcode == Opcodes.GETSTATIC) {
           //owner:包名;name:具体变量名;value:R类变量对应的具体id值
           Object value = jdRstore.getRFieldValue(ownername);
           if (value != null) {
             //调用该api实现值替换
               mv.visitLdcInsn(value);
               return;
          }
      }
       super.visitFieldInsn(opcodeownernamedesc);
  }

*注:以上代码仅为部分示意代码,非正式插件代码。


在业务模块引入R类瘦身插件后,业务模块功能可正常运行,且插件包大小均有3%~5%不同程度的减少。

公共资源R类ID内联

由于在京东android客户端代码中,更多的资源文件集中在公共资源库中,相对的公共库生成的R类文件也更大,对编译后的apk包内容进行分析后,公共资源库的R类文件占比高达3%。

公共库跟随宿主一起打包,在宿主打包过程中引入R类瘦身插件,打包后的apk有明显的减小,手机安装apk后启动首页正常展示无问题,但在打开某些业务插件时,会有异常闪退现象,崩溃类型为R.x resource not found。对崩溃原因分析如下:业务插件代码中使用了公共库中的R类资源、插件打包流程独立于宿主打包,在插件打包的过程中仅完成了业务模块R类的内联,并没有考虑到公共资源R类的内联,基于上述原因当宿主打包过程完成R类文件删除瘦身后,我们在运行某业务插件的过程中,自然就会报公共资源R类找不到的问题从而产生崩溃。


为了解决这个问题一开始的方案设想是增加白名单机制,keep住所有被业务模块使用的公共资源,但很快这个想法就被推翻,公共资源存在本身就是希望各个业务模块直接引用这部分资源,而不是自己定义,如果keep住的话,必然有很大一部分的资源无法删减,瘦身的效果会大打折扣。

既然保留的方案并不合适,那就将公共资源R类id也内联到代码中去。前面提到京东是支持插件化的,整个插件化方案是基于aura平台实现的,我们向aura团队进行了咨询,然后get到了新的方案切入点。

aura平台在插件化的过程中已通过aapt2引入了公共资源id固定的能力,在该能力下,已定义的公共资源id会一直固定(各个业务插件中引用的公共资源id一致),且公共资源库中已有的资源不可被其他模块重复定义,否则会覆盖之前已定义好的资源,基于上述的结果和规则,我们对之前的R文件瘦身gralde plugin功能进行完善,将公共资源的R类id 内联到项目中。

利用appt2的-stable-ids和-emit-ids两个参数实现固化资源id的功能,并将将固化后的ids文件命名为shared_res_public.xml存储在公共资源库中,业务插件依赖公共资源库,在打包编译的过程中aura会将shared_res_public.xml复制到业务工程临时编译文件夹intermediates下的指定位置并参与业务模块的打包过程中,其文件内容格式如下:


修改R文件瘦身gradle plugin 代码,从指定位置读取并识别这部分公共资源,按照<name,id>的形式进行变量存储,并在后续过程中对业务模块中的公共资源部分进行id替换。


public Map<StringString> parse() thro ws Exce ption {
       if (in == null) {
           return null;
      }
       DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
       DocumentBuilder builder = factory.newDocumentBuilder();
       Document doc = builder.parse(in);
       Element rootElement = doc.getDocumentElement();
       NodeList list = rootElement.getChildNodes();
      ......
       return resNode;
  }
}

至此,我们的R文件瘦身gradle plugin将R资源分为两部分进行存储,一部分为业务自身的R类资源,一部分为我们解析固定目录下的公共R类资源,对之前的R文件瘦身流程进行如下修改:


R类资源id内联部分代码如下:

public void visitFieldInsn(int opcodeString ownerString nameString desc) {
       if (opcode == Opcodes.GETSTATIC) {
           //优先从业务模块R类资源中查找
           Object value = jdRstore.getRFieldValue(ownername);
           if (value != null) {
               mv.visitLdcInsn(value);
               return;
          }
          //从公共R类资源中查找
           value = getPublicRFileValue(name);
           if (value != null) {
               mv.visitLdcInsn(value);
               return;
          }
      }
       super.visitFieldInsn(opcodeownernamedesc);
  }

该方案完善后,结合商详业务插件进行了验证,在商详及宿主均完成R文件内联瘦身后,商详模块业务功能可正常使用,无异常现象。

考虑到R文件内联瘦身gradle plugin是在打包编译阶段引入的,我们也统计了一下引入该插件以后对打包时长的影响,数据如下:


结合数据来看,引入R文件瘦身插件后对整体打包时长并无显著影响。

至此,基于京东商城探索的插件化工程R文件瘦身gradle plugin就开发完成,目前已在部分业务插件模块进行了线上验证,在功能上线以后我们也及时的进行了崩溃观测以及用户反馈的跟进,暂无异常问题。当然围绕R文件瘦身缩减包体积这个目的,开发人员有各种各样的技术方案,上述方案不一定适用于所有的客户端开发体系,另外后续也将围绕包瘦身这一常态事务建设一系列的相关工具,介入工作当中的各个阶段,高效、有效的控制包体积的增长,如大家在瘦身方面有相关建议和想法也欢迎大家来一起讨论。

参考文章:

Gradle Plugin:

https://docs.gradle.org/current/userguide/custom_plugins.html

Gradle Transform:

https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/transform/Transform

APK 构建流程:

https://developer.android.com/studio/build/index.html?hl=zh-cn#build-process

作者:耿蕾 田创新 京东零售技术

收起阅读 »

DevOps之【持续集成】

引言对于客户或者需求方来说,可以集成交付的软件才是有价值的。每个软件都有集成的过程,如果软件规模比较小,比如只有一个人而且没有外部依赖,那集成没什么问题。随着软件变得复杂,依赖变多,开发人员变多,那么早集成、常集成,就可以尽早暴露问题,做出相应的调整,防止在软...
继续阅读 »

引言

对于客户或者需求方来说,可以集成交付的软件才是有价值的。

每个软件都有集成的过程,如果软件规模比较小,比如只有一个人而且没有外部依赖,那集成没什么问题。随着软件变得复杂,依赖变多,开发人员变多,那么早集成、常集成,就可以尽早暴露问题,做出相应的调整,防止在软件后期才发现问题,从而导致软件失败。

定义

大师Martin Fowler对持续集成是这样定义的:

持续集成是一种软件开发实践,即团队开发成员经常集成它们的工作,通常每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽快地发现集成错误。许多团队发现这个过程可以大大减少集成的问题,让团队能够更快的开发内聚的软件。

优点

  • 降低软件风险 早集成,常集成,并且做了有效的测试,有利于尽早暴露问题和软件缺陷,了解软件的健康情况。假定越少的软件,对于维护和新业务开发都是有利的。 如果botslab每一个品类的设备都单独分支开发,不能及时集成进来,依赖问题、冲突问题、业务复用问题不能尽早解决,问题累积到一定程度解决成本会变大,业务发展是不会为代码重构让路的,那么软件质量的降低是必然的,很多项目都是这样失败的

  • 减少重复过程 软件集成的过程看起来简单,但是做起来难。软件的编译,测试,审查,部署,反馈,这些重复劳动是非常耗时,且没有意义的,自动化集成可以让开发解放出来,做一些用脑袋的事情。 如果出现疏漏,会给下游的参与者带来额外工作量和项目质量的误判。软件的输出质量是会影响项目计划的,所以持续集成很重要的一点就是自动化

  • 随时生成可以部署的软件 持续集成可以随时随地输出可以部署的软件,这一点对于需求方或客户是明显的好处。我们可以对客户说软件有多么好的架构,多么高质量的代码,但是对于客户来说,一个可以使用的软件才是他的实际资产。持续交付可以尽早的暴露产品问题和开发方向,客户才能给出有效的意见和开发重点。

  • 软件是透明的 持续集成会生成软件构建状态和品质信息,经常集成可以看到一些趋势,预测一些软件质量走向。

  • 团队信心 持续集成可以建立团队的信心,开发清楚的知道自己的代码产生了什么影响,测试对软件质量的预测稳定,产品或客户可以放心的需求了

步骤

  1. 统一的代码库

  2. 自动构建

  3. 自动测试

  4. 每个人每天都要向代码库主干提交代码

  5. 每次代码递交后都会在持续集成服务器上触发一次构建

  6. 保证快速构建

  7. 模拟生产环境的自动测试

  8. 每个人都可以很容易的获取最新可执行的应用程序

  9. 每个人都清楚正在发生的状况

  10. 自动化的部署

原则

  1. 所有的开发人员需要在本地机器上做本地构建,然后再提交的版本控制库中,从而确保他们的变更不会导致持续集成失败。

  2. 开发人员每天至少向版本控制库中提交一次代码。

  3. 开发人员每天至少需要从版本控制库中更新一次代码到本地机器。

  4. 需要有专门的集成服务器来执行集成构建,每天要执行多次构建。

  5. 每次构建都要100%通过。

  6. 每次构建都可以生成可发布的产品。

  7. 修复失败的构建是优先级最高的事情。



作者:QiShare
来源:juejin.cn/post/6986884632222384141

收起阅读 »

Vue2全家桶之一:vue-cli

vue
vue.js有著名的全家桶系列,包含了vue-router,vuex, vue-resource,再加上构建工具vue-cli,就是一个完整的vue项目的核心构成。 1.安装vue-cli② 全局安装vue-cli,在cmd中输入命令:安装成功:打开C:\U...
继续阅读 »

都说Vue2简单上手容易,的确,看了官方文档确实觉得上手很快,除了ES6语法和webpack的配置让你感到陌生,重要的是思路的变换,以前用jq随便拿全局变量和修改dom的锤子不能用了,vue只用关心数据本身,不用再频繁繁琐的操作dom,注册事件、监听事件、取消事件。。。。(确实很烦)。vue的官方文档还是不错的,由浅到深,如果不使用构建工具确实用的很爽,但是这在实际项目应用中是不可能的,当用vue-cli构建一个工程的时候,发现官方文档还是不够用,需要熟练掌握es6,而vue的全家桶(vue-cli,vue-router,vue-resource,vuex)还是都要上的。

vue.js有著名的全家桶系列,包含了vue-router,vuex, vue-resource,再加上构建工具vue-cli,就是一个完整的vue项目的核心构成。

vue-cli这个构建工具大大降低了webpack的使用难度,支持热更新,有webpack-dev-server的支持,相当于启动了一个请求服务器,给你搭建了一个测试环境,只关注开发就OK。

1.安装vue-cli

使用npm(需要安装node环境)全局安装webpack,打开命令行工具输入:npm install webpack -g或者(npm install -g webpack),安装完成之后输入 webpack -v,如下图,如果出现相应的版本号,则说明安装成功。

全局安装vue-cli,在cmd中输入命令:

npm install --global vue-cli

安装成功:



安装完成之后输入 vue -V(注意这里是大写的“V”),如下图,如果出现相应的版本号,则说明安装成功。

打开C:\Users\Andminster\AppData\Roaming\npm目录下可以看到:



打开node_modules也可以看到:

2.用vue-cli来构建项目

① 我首先在D盘新建一个文件夹(dxl_vue)作为项目存放地,然后使用命令行cd进入到项目目录输入:

vue init webpack baoge

baoge是自定义的项目名称,命令执行之后,会在当前目录生成一个以该名称命名的项目文件夹。
输入命令后,会跳出几个选项让你回答:

  • Project name (baoge): -----项目名称,直接回车,按照括号中默认名字(注意这里的名字不能有大写字母,如果有会报错Sorry, name can no longer contain capital letters),阮一峰老师博客为什么文件名要小写 ,可以参考一下。
  • Project description (A Vue.js project): ----项目描述,也可直接点击回车,使用默认名字
  • Author (): ----作者,输入dongxili
    接下来会让用户选择:
  • Runtime + Compiler: recommended for most users 运行加编译,既然已经说了推荐,就选它了
    Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specificHTML) are ONLY allowed in .vue files - render functions are required elsewhere 仅运行时,已经有推荐了就选择第一个了
  • Install vue-router? (Y/n) 是否安装vue-router,这是官方的路由,大多数情况下都使用,这里就输入“y”后回车即可。
  • Use ESLint to lint your code? (Y/n) 是否使用ESLint管理代码,ESLint是个代码风格管理工具,是用来统一代码风格的,一般项目中都会使用。
    接下来也是选择题Pick an ESLint preset (Use arrow keys) 选择一个ESLint预设,编写vue项目时的代码风格,直接y回车
  • Setup unit tests with Karma + Mocha? (Y/n) 是否安装单元测试,我选择安装y回车
  • Setup e2e tests with Nightwatch(Y/n)? 是否安装e2e测试 ,我选择安装y回车

回答完毕后上图就开始构建项目了。

配置完成后,可以看到目录下多出了一个项目文件夹baoge,然后cd进入这个文件夹:
安装依赖

npm install

 ( 如果安装速度太慢。可以安装淘宝镜像,打开命令行工具,输入:
npm install -g cnpm --registry=https://registry.npm.taobao.org
 然后使用cnpm来安装 )

npm install :安装所有的模块,如果是安装具体的哪个个模块,在install 后面输入模块的名字即可。而只输入install就会按照项目的根目录下的package.json文件中依赖的模块安装(这个文件里面是不允许有任何注释的),每个使用npm管理的项目都有这个文件,是npm操作的入口文件。因为是初始项目,还没有任何模块,所以我用npm install 安装所有的模块。安装完成后,目录中会多出来一个node_modules文件夹,这里放的就是所有依赖的模块。


然后现在,baoge文件夹里的目录是这样的:



解释下每个文件夹代表的意思(仔细看一下这张图):

image.png

3.启动项目

npm run dev


如果浏览器打开之后,没有加载出页面,有可能是本地的 8080 端口被占用,需要修改一下配置文件 config里的index.js

还有,如果本地调试项目时,建议将build 里的assetsPublicPath的路径前缀修改为 ' ./ '(开始是 ' / '),因为打包之后,外部引入 js 和 css 文件时,如果路径以 ' / ' 开头,在本地是无法找到对应文件的(服务器上没问题)。所以如果需要在本地打开打包后的文件,就得修改文件路径。
我的端口没有被占用,直接成功(服务启动成功后浏览器会默认打开一个“欢迎页面”):



注意:在进行vue页面调试时,一定要去谷歌商店下载一个vue-tool扩展程序。

4.vue-cli的webpack配置分析

  • package.json可以看到开发和生产环境的入口。

  • 可以看到dev中的设置,build/webpack.dev.conf.js,该文件是开发环境中webpack的配置入口。
  • 在webpack.dev.conf.js中出现webpack.base.conf.js,这个文件是开发环境和生产环境,甚至测试环境,这些环境的公共webpack配置。可以说,这个文件相当重要。
  • 还有config/index.js 、build/utils.js 、build/build.js等,具体请看这篇介绍:
    https://segmentfault.com/a/1190000008644830

5.打包上线

注意,自己的项目文件都需要放到 src 文件夹下。
在项目开发完成之后,可以输入 npm run build 来进行打包工作。

npm run build

另:

1.npm 开启了npm run dev以后怎么退出或关闭?
ctrl+c
2.--save-dev
自动把模块和版本号添加到模块配置文件package.json中的依赖里devdependencies部分
3. --save-dev 与 --save 的区别
--save 安装包信息将加入到dependencies(生产阶段的依赖)
--save-dev 安装包信息将加入到devDependencies(开发阶段的依赖),所以开发阶段一般使用它

打包完成后,会生成 dist 文件夹,如果已经修改了文件路径,可以直接打开本地文件查看。
项目上线时,只需要将 dist 文件夹放到服务器就行了。

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

收起阅读 »

[PHP 安全] pcc —— PHP 安全配置检测工具

PHP
背景在 PHP 安全测试中最单调乏味的任务之一就是检查不安全的 PHP 配置项。作为一名 PHP 安全海报的继承者,我们创建了一个脚本用来帮助系统管理员如同安全专家一样尽可能快速且全面地评估 php.ini 和相关主题的状态。在下文中,该脚本被称作“PHP 安...
继续阅读 »

背景

在 PHP 安全测试中最单调乏味的任务之一就是检查不安全的 PHP 配置项。作为一名 PHP 安全海报的继承者,我们创建了一个脚本用来帮助系统管理员如同安全专家一样尽可能快速且全面地评估 php.ini 和相关主题的状态。在下文中,该脚本被称作“PHP 安全配置项检查器”,或者 pcc

https://github.com/sektioneins/pcc

概念

  • 一个便于分发的单文件
  • 有对每个安全相关的 ini 条目的简单测试
  • 包含一些其他测试 - 但不太复杂
  • 兼容 PHP >= 5.4, 或者 >= 5.0
  • 没有复杂/过度设计的代码,例如没有类/接口,测试框架,类库等等。它应该第一眼看上去是显而易见的-甚至对于新手-这个工具怎么使用能用来做什么。
  • 没有(或者少量的)依赖

使用 / 安装

  • CLI:简单调用 php phpconfigcheck.php。然后,添加参数 -a 以便更好的查看隐藏结果, -h 以 HTML 格式输出, -j 以 JSON 格式输出.
  • WEB: 复制这个脚本文件到你的服务器上的任意一个可访问目录,比如 root 目录。参见下面的“防护措施”。
    在非 CLI 模式下默认输出 HTML 格式。可以通过修改设置环境变量PCC_OUTPUT_TYPE=text 或者 PCC_OUTPUT_TYPE=json改变这个行为。
    一些测试用例默认是被隐藏的,特别是skipped、ok和 unknown/untested这些。要显示全部结果,可以用 phpconfigcheck.php?showall=1,但这并不适用于 JSON 输出,它默认返回全部结果。
    在 WEB 模式下控制输出格式用 phpconfigcheck.php?format=...format的值可以是 text, html 或者 json中的一个,例如: phpconfigcheck.php?format=textformat 参数优先于 PCC_OUTPUT_TYPE。

保障措施

大多数情况下,最好是自己来关注与安全性相关的问题比如PHP的配置。脚本已实现下列保障措施:

  • mtime检查:脚本在非CLI环境中只能工作两天。可以通过touch phpconfigcheck.php或者将脚本文件再次复制到你的服务器(例如通过SCP)来重新进行mtime检查。可以通过设置环境量: PCC_DISABLE_MTIME=1,比如在apache的.htaccess文件中设置SetEnv PCC_DISABLE_MTIME 1来禁用mtime检查。
  • 来源IP检查:默认情况下,只有localhost (127.0.0.1 和 ::1)才能访问这个脚本。其他主机可以通过在PCC_ALLOW_IP中添加IP地址或者通配符表达式的方式来访问脚本,比如在.htaccess文件中设置SetEnv PCC_ALLOW_IP 10.0.0.*。你还可以选择通过SSH端口转发访问您的web服务器, 比如 ssh -D 或者 ssh -L

下载

可以通过github下载第一个完整的开发版: https://github.com/sektioneins/pcc

如果有好的建议或者遇到bug请给我们提issue:

截图

HTML输出的列表是根据问题严重性排序的,通过颜色代码的形式列出了所有建议。列表顶部的状态行会显示问题的数量。


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

收起阅读 »

Java Exception

异常指不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。Java通 过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例,...
继续阅读 »

Java异常

异常指不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。Java通 过API中Throwable类的众多子类描述各种不同的异常。因而,Java异常都是对象,是Throwable子类的实例,描述了出现在一段编码中的 错误条件。当条件生成时,错误将引发异常。
Java异常类层次结构图:


图1 Java异常类层次结构图
在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)。Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性。 Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception(异常):是程序本身可以处理的异常。
Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。
注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
通常,Java的异常(包括Exception和Error)分为可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)
可查异常(编译器要求必须处置的异常):正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

不可查异常(编译器不要求强制处置的异常):包括运行时异常(RuntimeException与其子类)和错误(Error)。

Exception 这种异常分两大类运行时异常和非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。

运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。 非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

4.处理异常机制

在 Java 应用程序中,异常处理机制为:抛出异常,捕捉异常。

抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。

捕获异常 :在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适 的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

对于运行时异常、错误或可查异常,Java技术所要求的异常处理方式有所不同。

由于运行时异常的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常。

对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。

对于所有的可查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉可查异常时,它必须声明将抛出异常。

能够捕捉异常的方法,需要提供相符类型的异常处理器。所捕捉的异常,可能是由于自身语句所引发并抛出的异常,也可能是由某个调用的方法或者Java运行时 系统等抛出的异常。也就是说,一个方法所能捕捉的异常,一定是Java代码在某处所抛出的异常。简单地说,异常总是先被抛出,后被捕捉的。

任何Java代码都可以抛出异常,如:自己编写的代码、来自Java开发环境包中代码,或者Java运行时系统。无论是谁,都可以通过Java的throw语句抛出异常。

从方法中抛出的任何异常都必须使用throws子句。

捕捉异常通过try-catch语句或者try-catch-finally语句实现。

总体来说,Java规定:对于可查异常必须捕捉、或者声明抛出。允许忽略不可查的RuntimeException和Error。

4.1 捕获异常:try、catch 和 finally

1.try-catch语句

在Java中,异常通过try-catch语句捕获。其一般语法形式为:

try {  
// 可能会发生异常的程序代码
} catch (Type1 id1){
// 捕获并处置try抛出的异常类型Type1
}
catch (Type2 id2){
//捕获并处置try抛出的异常类型Type2
}

关键词try后的一对大括号将一块可能发生异常的代码包起来,称为监控区域。Java方法在运行过程中出现异常,则创建异常对象。将异常抛出监控区域之 外,由Java运行时系统试图寻找匹配的catch子句以捕获异常。若有匹配的catch子句,则运行其异常处理代码,try-catch语句结束。

匹配的原则是:如果抛出的异常对象属于catch子句的异常类,或者属于该异常类的子类,则认为生成的异常对象与catch块捕获的异常类型相匹配。

例1 捕捉throw语句抛出的“除数为0”异常。

public class TestException {  
public static void main(String[] args) {
int a = 6;
int b = 0;
try { // try监控区域

if (b == 0) throw new ArithmeticException(); // 通过throw语句抛出异常
System.out.println("a/b的值是:" + a / b);
}
catch (ArithmeticException e) { // catch捕捉异常
System.out.println("程序出现异常,变量b不能为0。");
}
System.out.println("程序正常结束。");
}
}

运行结果:程序出现异常,变量b不能为0。

程序正常结束。

例1 在try监控区域通过if语句进行判断,当“除数为0”的错误条件成立时引发ArithmeticException异常,创建 ArithmeticException异常对象,并由throw语句将异常抛给Java运行时系统,由系统寻找匹配的异常处理器catch并运行相应异 常处理代码,打印输出“程序出现异常,变量b不能为0。”try-catch语句结束,继续程序流程。

事实上,“除数为0”等ArithmeticException,是RuntimException的子类。而运行时异常将由运行时系统自动抛出,不需要使用throw语句。

例2 捕捉运行时系统自动抛出“除数为0”引发的ArithmeticException异常。

public static void main(String[] args) {  
int a = 6;
int b = 0;
try {
System.out.println("a/b的值是:" + a / b);
} catch (ArithmeticException e) {
System.out.println("程序出现异常,变量b不能为0。");
}
System.out.println("程序正常结束。");
}
}

运行结果:程序出现异常,变量b不能为0。
程序正常结束。

例2 中的语句:

System.out.println("a/b的值是:" + a/b);

在运行中出现“除数为0”错误,引发ArithmeticException异常。运行时系统创建异常对象并抛出监控区域,转而匹配合适的异常处理器catch,并执行相应的异常处理代码。

由于检查运行时异常的代价远大于捕捉异常所带来的益处,运行时异常不可查。Java编译器允许忽略运行时异常,一个方法可以既不捕捉,也不声明抛出运行时异常。

例3 不捕捉、也不声明抛出运行时异常。

public class TestException {  
public static void main(String[] args) {
int a, b;
a = 6;
b = 0; // 除数b 的值为0
System.out.println(a / b);
}
} 复制

运行结果:
Exception in thread "main" java.lang.ArithmeticException: / by zero at Test.TestException.main(TestException.java:8)

例4 程序可能存在除数为0异常和数组下标越界异常。

public class TestException {  
public static void main(String[] args) {
int[] intArray = new int[3];
try {
for (int i = 0; i <= intArray.length; i++) {
intArray[i] = i;
System.out.println("intArray[" + i + "] = " + intArray[i]);
System.out.println("intArray[" + i + "]模 " + (i - 2) + "的值: "
+ intArray[i] % (i - 2));
}
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("intArray数组下标越界异常。");
} catch (ArithmeticException e) {
System.out.println("除数为0异常。");
}
System.out.println("程序正常结束。");
}
}

运行结果:

intArray[0] = 0

intArray[0]模 -2的值: 0

intArray[1] = 1

intArray[1]模 -1的值: 0

intArray[2] = 2

除数为0异常。

程序正常结束。

例4 程序可能会出现除数为0异常,还可能会出现数组下标越界异常。程序运行过程中ArithmeticException异常类型是先行匹配的,因此执行相匹配的catch语句:

catch (ArithmeticException e){  
System.out.println("除数为0异常。");
}

需要注意的是,一旦某个catch捕获到匹配的异常类型,将进入异常处理代码。一经处理结束,就意味着整个try-catch语句结束。其他的catch子句不再有匹配和捕获异常类型的机会。

Java通过异常类描述异常类型,异常类的层次结构如图1所示。对于有多个catch子句的异常程序而言,应该尽量将捕获底层异常类的catch子 句放在前面,同时尽量将捕获相对高层的异常类的catch子句放在后面。否则,捕获底层异常类的catch子句将可能会被屏蔽。

RuntimeException异常类包括运行时各种常见的异常,ArithmeticException类和ArrayIndexOutOfBoundsException类都是它的子类。因此,RuntimeException异常类的catch子句应该放在 最后面,否则可能会屏蔽其后的特定异常处理或引起编译错误。

收起阅读 »

Object.defineProperty也能监听数组变化?

首先,解答一下标题:Object.defineProperty 不能监听原生数组的变化。如需监听数组,要将数组转成对象。 在 Vue2 时是使用了 Object.defineProperty 监听数据变化,但我查了下 文档,发现 Object.define...
继续阅读 »


首先,解答一下标题:Object.defineProperty 不能监听原生数组的变化。如需监听数组,要将数组转成对象。




Vue2 时是使用了 Object.defineProperty 监听数据变化,但我查了下 文档,发现 Object.defineProperty 是用来监听对象指定属性的变化。没有看到可以监听个数组变化的。


Vue2 有的确能监听到数组某些方法改变了数组的值。本文的目标就是解开这个结。






基础用法


Object.defineProperty() 文档


关于 Object.defineProperty() 的用法,可以看官方文档。


基础部分本文只做简单的讲解。




语法


Object.defineProperty(obj, prop, descriptor)

参数


  • obj 要定义属性的对象。
  • prop 要定义或修改的属性的名称或 Symbol
  • descriptor 要定义或修改的属性描述符。

const data = {}
let name = '雷猴'

Object.defineProperty(data, 'name', {
get() {
console.log('get')
return name
},
set(newVal) {
console.log('set')
name = newVal
}
})

console.log(data.name)
data.name = '鲨鱼辣椒'

console.log(data.name)
console.log(name)

上面的代码会输出


get
雷猴
set
鲨鱼辣椒
鲨鱼辣椒



上面的意思是,如果你需要访问 data.name ,那就返回 name 的值。


如果你想设置 data.name ,那就会将你传进来的值放到变量 name 里。


此时再访问 data.name 或者 name ,都会返回新赋予的值。




还有另一个基础用法:“冻结”指定属性


const data = {}

Object.defineProperty(data, 'name', {
value: '雷猴',
writable: false
})

data.name = '鲨鱼辣椒'
delete data.name
console.log(data.name)

这个例子,把 data.name 冻结住了,不管你要修改还是要删除都不生效了,一旦访问 data.name 都一律返回 雷猴


以上就是 Object.defineProperty 的基础用法。






深度监听


上面的例子是监听基础的对象。但如果对象里还包含对象,这种情况就可以使用递归的方式。


递归需要创建一个方法,然后判断是否需要重复调用自身。


// 触发更新视图
function updateView() {
console.log('视图更新')
}

// 重新定义属性,监听起来(核心)
function defineReactive(target, key, value) {

// 深度监听
observer(value)

// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue != value) {
// 深度监听
observer(newValue)

// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue

// 触发视图更新
updateView()
}
}
})
}

// 深度监听
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}

// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}

// 准备数据
const data = {
name: '雷猴'
}

// 开始监听
observer(data)

// 测试1
data.name = {
lastName: '鲨鱼辣椒'
}

// 测试2
data.name.lastName = '蟑螂恶霸'

上面这个例子会输出2次“视图更新”。




我创建了一个 updateView 方法,该方法模拟更新 DOM (类似 Vue的操作),但我这里简化成只是输出 “视图更新” 。因为这不是本文的重点。




测试1 会触发一次 “视图更新” ;测试2 也会触发一次。


因为在 Object.definePropertyset 里面我有调用了一次 observer(newValue)observer 会判断传入的值是不是对象,如果是对象就再次调用 defineReactive 方法。


这样可以模拟一个递归的状态。




以上就是 深度监听 的原理,其实就是递归。


但递归有个不好的地方,就是如果对象层次很深,需要计算的量就很大,因为需要一次计算到底。






监听数组


数组没有 key ,只有 下标。所以如果需要监听数组的内容变化,就需要将数组转换成对象,并且还要模拟数组的方法。


大概的思路和编码流程顺序如下:


  1. 判断要监听的数据是否为数组
  2. 是数组的情况,就将数组模拟成一个对象
  3. 将数组的方法名绑定到新创建的对象中
  4. 将对应数组原型的方法赋给自定义方法



代码如下所示


// 触发更新视图
function updateView() {
console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原形指向 oldArrayProperty,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);

['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName = 收起阅读 »

JS 将伪数组转换成数组

在 JS 中,伪数组 是非常常见的,它也叫 类数组。伪数组可能会给 JS 初学者带来一点困扰。 本文将详细讲解 什么是伪数组,以及分别在 ES5 和 ES6 中将伪数组转换成真正的数组 。 什么是伪数组? 伪数组的主要特征:它是一个对象,并且该对象有 le...
继续阅读 »




JS 中,伪数组 是非常常见的,它也叫 类数组。伪数组可能会给 JS 初学者带来一点困扰。


本文将详细讲解 什么是伪数组,以及分别在 ES5ES6 中将伪数组转换成真正的数组






什么是伪数组?


伪数组的主要特征:它是一个对象,并且该对象有 length 属性


比如


let arrayLike = {
"0": "a",
"1": "b",
"2": "c",
"length": 3
}

像上面的 arrayLike 对象,有 length 属性,key 也是有序序列。可以遍历,也可以查询长度。但却不能调用数组的方法。比如 push、pop 等方法。


ES6 之前,还有一个常见的伪数组:arguments


arguments 看上去也很像一个数组,但它没有数组的方法。


比如 arguments.push(1) ,这样做一定会报错。




除了 arguments 之外,NodeList 对象表示节点的集合也是伪数组,比如通过 document.querySelectorAll 获取的节点集合等。






转换


将伪数组转换成真正的数组的方法不止一个,我们先从 ES5 讲起。




ES5 的做法


在 ES6 问世之前,开发者通常需要用以下的方法把伪数组转换成数组。




方法1


// 通过 makeArray 方法,把数组转成伪数组
function makeArray(arrayLike) {
let result = [];
for (let i = 0, len = arrayLike.length; i < len; i++) {
result.push(arrayLike[i]);
}
return result;
}

function doSomething () {
let args = makeArray(arguments);
console.log(args);
}

doSomething(1, 2, 3);

// 输出: [1, 2, 3]

这个方法虽然有效,但要多写很多代码。




方法2


function doSomething () {
let args = Array.prototype.slice.call(arguments);
console.log(args);
}
doSomething(1, 2, 3);

// 输出: [1, 2, 3]

这个方法的功能和 方法1 是一样的,虽然代码量减少了,但不能很直观的让其他开发者觉得这是在转换。




ES6的做法


直到 ES6 提供了 Array.from 方法完美解决以上问题。


function doSomething () {
let args = Array.from(arguments);
console.log(args);
}

doSomething('一', '二', '三');

// 输出: ['一', '二', '三']

Array.from 的主要作用就是把伪数组和可遍历对象转换成数组的。




说“主要作用”的原因是因为 Array.from 还提供了2个参数可传。这样可以延伸很多种小玩法。


Array.from 的第二个参数是一个函数,类似 map遍历 方法。用来遍历的。


Array.from 的第三个参数接受一个 this 对象,用来改变 this 指向。




第三个参数的用法(不常用)


let helper = {
diff: 1,
add (value) {
return value + this.diff; // 注意这里有个 this
}
};

function translate () {
return Array.from(arguments, helper.add, helper);
}

let numbers = translate(1, 2, 3);

console.log(numbers); // 2, 3, 4



Array.from 其他玩法


创建长度为5的数组,且初始化数组每个元素都是1


let array = Array.from({length: 5}, () => 1)
console.log(array)

// 输出: [1, 1, 1, 1, 1]

第二个参数的作用和 map遍历 差不多的,所以 map遍历 有什么玩法,这里也可以做相同的功能。就不多赘述了。




把字符串转换成数组


let msg = 'hello';
let msgArr = Array.from(msg);
console.log(msgArr);

// 输出: ["h", "e", "l", "l", "o"]

如果传一个真正的数组给 Array.from 会返回一个一模一样的数组。

 
收起阅读 »

我写了一个将 excel 文件转化成 本地json文件的插件

插件介绍 excel-2b-json 插件用于将 google excel 文件转化成 本地json文件。 适用场景: 项目国际化,配置多语言 使用方法 1. 安装excel-2b-json npm install excel-2b-json 2. 引入使用...
继续阅读 »


插件介绍


excel-2b-json 插件用于将 google excel 文件转化成 本地json文件。


适用场景: 项目国际化,配置多语言


使用方法


1. 安装excel-2b-json


npm install excel-2b-json

2. 引入使用


const excelToJson = require('excel-2b-json');
// path 生成的json文件目录

excelToJson('https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/edit#gid=0', path)


转化得到



下面是插件的实现



源码已放到github:github.com/Sunny-lucki…



一、涉及的算法


1. 26字母转换成数字,26进制,a为1,aa为27,ab为28


  function colToInt(col) {
const letters = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
col = col.trim().split('')
let n = 0

for (let i = 0; i < col.length; i++) {
n *= 26
n += letters.indexOf(col[i])
}

return n
}

2. 生成几行几列的二维空数组


function getEmpty2DArr(rows, cols) {
let arrs = new Array(rows);
for (var i = 0; i < arrs.length; i++) {
arrs[i] = new Array(cols).fill(''); //每行有cols列
}
return arrs;
}


3. 清除二维数组中空的数组


[
[1,2,3],
['','',''],
[7,8,9]
]

转化为
[
[1,4,7],
[3,6,9]
]

  clearEmptyArrItem(matrix) {
return matrix.filter(function (val) {
return val.some(function (val1) {
return val1.replace(/\s/g, '') !== ''
})
})
}


4. 矩阵的翻转


[
[1,2,3],
[4,5,6],
[7,8,9]
]

转化为
[
[1,4,7],
[2,5,8],
[3,6,9]
]

算法实现


  /**
*
* @param {array*2} matrix 一个二维数组,返回旋转后的二维数组。
*/

rotateExcelDate(matrix) {
if (!matrix[0]) return []
var results = [],
result = [],
i,
j,
lens,
len
for (i = 0, lens = matrix[0].length; i < lens; i++) {
result = []
for (j = 0, len = matrix.length; j < len; j++) {
result[j] = matrix[j][i]
}
results.push(result)
}
return results
}

二、插件的实现


1. 下载google Excel文档到本地


我们先看看google Excel文档的url的组成


https://docs.google.com/spreadsheets/d/文档ID/edit#哈希值

例如下面这条,你可以尝试打开,下面这条链接是可以打开的。


https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/edit#gid=0


下载google文档的步骤非常简单,只要获取原始的链接,然后拼接成下面的url,向这个Url发起请求,然后以流的方式写入生成文件就可以了。


https://docs.google.com/spreadsheets/d/ + "文档ID" + '/export?format=xlsx&id=' + id + '&' + hash

因此实现下载的方法非常简单,可以直接看代码


downLoadExcel.js



const fs = require('fs')
const request = require('superagent')
const rmobj = require('./remove')

/**
* 下载google excel 文档到本地
* @param {*} url // https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/edit#gid=0
* @returns
*/

function downLoadExcel(url) {

// 记录当前下载文件的目录,方便删除
rmobj.push({
path: __dirname,
ext: 'xlsx'
})
return new Promise((resolve, reject) => {
var down1 = url.split('/')
var down2 = down1.pop() // edit#gid=0
var url2 = down1.join('/') // https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM
var id = down1.pop() // 12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM
var hash = down2.split('#').pop() // gid=0
var downurl = url2 + '/export?format=xlsx&id=' + id + '&' + hash // https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/export?format=xlsx&id=12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM&gid=0
var loadedpath = __dirname + '/' + id + '.xlsx'
const stream = fs.createWriteStream(loadedpath)
const req = request.get(downurl)
req.pipe(stream).on('finish', function () {
resolve(loadedpath)
// 已经成功下载下来了,接下来将本地excel转化成json的工作就交给Excel对象来完成
})
})

}

module.exports = downLoadExcel

入口文件可以这样写


async function excelToJson(excelPathName, outputPath) {
if (Util.checkAddress(excelPathName) === 'google') {
// 1.判断是谷歌excel文档,需要交给Google对象去处理,主要是下载线上的,生成本地excel文件
const filePath = await downLoadExcel(excelPathName)

// 2.解析本地excel成二维数组
const data = await parseXlsx(filePath)

// 3.生成json文件
generateJsonFile(data, outputPath)
}

}
module.exports = excelToJson


之所以写if判断,是为了后面扩展,也许就不止是解析google文档了,或许也要解析腾讯等其他文档呢


第一步已经实现了,接下来就看第二步怎么实现


2. 解析本地excel成二维数组


解析本地excel文件,获取excel的sheet信息和strings信息


excel 文件其实本质上是多份xml文件的压缩文件。



xml是存储数据的,而html是显示数据的



而在这里我们只需要获取两份xml 文件,一份是strings,就是excel里的内容,一份是sheet,概括整个excel文件的信息。


async function parseXlsx(path) {

// 1. 解析本地excel文件,获取excel的sheet信息和content信息
const files = await extractFiles(path);

// 2. 根据strings和sheet解析成二维数组
const data = await extractData(files)

// 3. 处理二维数组的内容,
const fixData = handleData(data)
return fixData;
}

所以第一步我们看看怎么获取excel的sheet信息和strings信息


function extractFiles(path) {

// excel的本质是多份xml组成的压缩文件,这里我们只需要xl/sharedStrings.xml和xl/worksheets/sheet1.xml
const files = {
strings: {}, // strings内容
sheet: {},
'xl/sharedStrings.xml': 'strings',
'xl/worksheets/sheet1.xml': 'sheet'
}

const stream = path instanceof Stream ? path : fs.createReadStream(path)

return new Promise((resolve, reject) => {
const filePromises = [] // 由于一份excel文档,会被解析成好多分xml文档,但是我们只需要两份xml文档,分别是(xl/sharedStrings.xml和xl/worksheets/sheet1.xml),所以用数组接受

stream
.pipe(unzip.Parse())
.on('error', reject)
.on('close', () => {
Promise.all(filePromises).then(() => {
return resolve(files)
})
})
.on('entry', entry => {

// 每解析某个xml文件都会进来这里,但是我们只需要xl/sharedStrings.xml和xl/worksheets/sheet1.xml,并将内容保存在strings和sheet中
const file = files[entry.path]
if (file) {
let contents = ''
let chunks = []
let totalLength = 0
filePromises.push(
new Promise(resolve => {
entry
.on('data', chunk => {
chunks.push(chunk)
totalLength += chunk.length
})
.on('end', () => {
contents = Buffer.concat(chunks, totalLength).toString()
files[file].contents = contents
if (/�/g.test(contents)) {
throw TypeError('本次转化出现乱码�')
} else {
resolve()
}
})
})
)
} else {
entry.autodrain()
}
})
})
}

可以断点看看entry.path,你就会看到分别进来了好几次,然后我们会分别看到我们想要的那两个文件



两份xml文件解析之后就会到close方法里了,这时就可以看到strings和sheet都有内容了,而且内容都是xml



我们分别看看strings和sheet的内容


stream
.pipe(unzip.Parse())
.on('error', reject)
.on('close', () => {
Promise.all(filePromises).then(() => {
console.log(files.strings.contents);
console.log(files.sheet.contents);
return resolve(files)
})
})


格式化一下


strings



sheet


可以发现strings的内容非常简单,现在我们借助xmldom将内容解析为节点对象,然后用xpath插件来获取内容


xpath的用法:github.com/goto100/xpa…


  const XMLDOM = require('xmldom')
const xpath = require('xpath')
const ns = { a: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' }
const select = xpath.useNamespaces(ns)

const valuesDoc = new XMLDOM.DOMParser().parseFromString(
files.strings.contents
)

// 把所有每个格子的内容都放进了values数组里。
values = select('//a:si', valuesDoc).map(string =>

select('.//a:t', string)
.map(t => t.textContent)
.join('')
)


'//a:si' 是xpath语法,//表示选择当前节点下的所有子孙节点,a是schemas.openxmlformats.org/spreadsheet…




可以看到,xpath的用法很简单,就是找到si节点下的子节点t的内容,然后放进数组里



最终生成的values数组是[ 'lang', 'cn','en', 'lang001','我是阳光', 'i am sunny','lang002', '前端阳光','FE Sunny', 'lang003','带带我', 'ddw']


现在我们要获取sheet的内容了,我们先分析一下xml结构



可以看到sheetData节点其实就是记录strings的内容的信息的,strings的内容是我们真正输入的,而sheet则是类似一种批注。


我们分析看看


row就是表示表格中的行,c则表示的是列,属性t="s"表示的是当前这个格子有内容,r="A1"表示的是在第一行中的A列



而节点v则表示该格子是该表格的第几个有值的格子,不信?我们可以试试看




可以看到这打印出来的xml内容,strings中已经没有了那两个值,而sheet中的那两个格子的c节点的t属性没了,而且v节点也没有了。


现在我们可以知道,string只保存有值的格子里的值,而sheet则是一个网格,不管格子有没有值都会记录,有值的会有个序号存在v节点中。


现在就要收集c节点


  const na = {
textContent: ''
}

class CellCoords {
constructor(cell) {
cell = cell.split(/([0-9]+)/)
this.row = parseInt(cell[1])
this.column = colToInt(cell[0])
}
}

class Cell {
constructor(cellNode) {
const r = cellNode.getAttribute('r')
const type = cellNode.getAttribute('t') || ''
const value = (select('a:v', cellNode, 1) || na).textContent
const coords = new CellCoords(r)

this.column = coords.column // 该格子所在列数
this.row = coords.row // 该格子所在行数
this.value = value // 该格子的顺序
this.type = type // 该格子是否为空
}
}

const cells = select('/a:worksheet/a:sheetData/a:row/a:c', sheet).map(
node => new Cell(node)
)

每个c节点用cell对象来表示


可以看到cell节点有四个属性。


你现在知道它为什么要保存顺序了吗?


因为这样才可以直接从strings生成的values数组中拿出对应顺序的值填充到网格中。


接下来要获取总共有多少列数和行数。这就需要获取最大最小行数列数,然后求差得到


// 计算该表格的最大最小列数行数
d = calculateDimensions(cells)

const cols = d[1].column - d[0].column + 1
const rows = d[1].row - d[0].row + 1

function calculateDimensions(cells) {
const comparator = (a, b) => a - b
const allRows = cells.map(cell => cell.row).sort(comparator)
const allCols = cells.map(cell => cell.column).sort(comparator)
const minRow = allRows[0]
const maxRow = allRows[allRows.length - 1]
const minCol = allCols[0]
const maxCol = allCols[allCols.length - 1]

return [{ row: minRow, column: minCol }, { row: maxRow, column: maxCol }]
}

接下来就根据列数和行数造空二维数组,然后再根据cells和values填充内容


  // 计算该表格的最大最小列数行数
d = calculateDimensions(cells)

const cols = d[1].column - d[0].column + 1
const rows = d[1].row - d[0].row + 1

// 生成二维空数组
data = getEmpty2DArr(rows, cols)

// 填充二维空数组
for (const cell of cells) {
let value = cell.value

// s表示该格子有内容
if (cell.type == 's') {
value = values[parseInt(value)]
}

// 填充该格子
if (data[cell.row - d[0].row]) {
data[cell.row - d[0].row][cell.column - d[0].column] = value
}
}
return data

我们看看最终生成的data,可以发现,excel的网格已经被二维数组模拟出来了



所以我们看看extractData的完整实现


function extractData(files) {
let sheet
let values
let data = []

try {
sheet = new XMLDOM.DOMParser().parseFromString(files.sheet.contents)
const valuesDoc = new XMLDOM.DOMParser().parseFromString(
files.strings.contents
)

// 把所有每个格子的内容都放进了values数组里。
values = select('//a:si', valuesDoc).map(string =>
select('.//a:t', string)
.map(t => t.textContent)
.join('')
)

console.log(values);
} catch (parseError) {
return []
}



const na = {
textContent: ''
}

class CellCoords {
constructor(cell) {
cell = cell.split(/([0-9]+)/)
this.row = parseInt(cell[1])
this.column = colToInt(cell[0])
}
}

class Cell {
constructor(cellNode) {
const r = cellNode.getAttribute('r')
const type = cellNode.getAttribute('t') || ''
const value = (select('a:v', cellNode, 1) || na).textContent
const coords = new CellCoords(r)

this.column = coords.column // 该格子所在列数
this.row = coords.row // 该格子所在行数
this.value = value // 该格子的顺序
this.type = type // 该格子是否为空
}
}

const cells = select('/a:worksheet/a:sheetData/a:row/a:c', sheet).map(
node => new Cell(node)
)

// 计算该表格的最大最小列数行数
d = calculateDimensions(cells)

const cols = d[1].column - d[0].column + 1
const rows = d[1].row - d[0].row + 1

// 生成二维空数组
data = getEmpty2DArr(rows, cols)

// 填充二维空数组
for (const cell of cells) {
let value = cell.value

// s表示该格子有内容
if (cell.type == 's') {
value = values[parseInt(value)]
}

// 填充该格子
if (data[cell.row - d[0].row]) {
data[cell.row - d[0].row][cell.column - d[0].column] = value
}
}
return data
}


接下来就是要去除空行和空列,并将二维数组翻转成我们需要的格式


function handleData(data) {
if (data) {
data = clearEmptyArrItem(data)
data = rotateExcelDate(data)
data = clearEmptyArrItem(data)
}
return data
}


可以看到,现在数组的第一项子数组则是key列表了。


接下来就可以根据key来生成对应的json文件了。


3. 生成json数据


这一步非常简单


function generateJsonFile(excelDatas, outputPath) {

// 获得转化成json格式
const jsons = convertProcess(excelDatas)

// 生成写入文件
writeFile(jsons, outputPath)
}

首先就是获取json数据


先获取data数组的第一项数组,第一项数组是key,然后生成每种语言的json对象


  /**
*
* @param {array*2} data
* 返回处理完后的多语言数组,每一项都是一个json对象。
*/

function convertProcess(data) {
var keys_arr = [],
data_arr = [],
result_arr = [],
i,
j,
data_arr_len,
col_data_json,
col_data_arr,
data_arr_col_len
// 表格合并处理,这是json属性列。
keys_arr = data[0]
// 第一例是json描述,后续是语言包
data_arr = data.slice(1)

for (i = 0, data_arr_len = data_arr.length; i < data_arr_len; i++) {
// 取出第一个列语言包
col_data_arr = data_arr[i]
// 该列对应的临时对象
col_data_json = {}
for (
j = 0, data_arr_col_len = col_data_arr.length;
j < data_arr_col_len;
j++
) {

col_data_json[keys_arr[j]] = col_data_arr[j]
}
result_arr.push(col_data_json)
}

return result_arr
}


我们可以看看生成的result_arr



可见已经成功生成每一种语言的json对象了。


接下来只需要生成json文件就可以了,注意把之前生成的excel文件删除


  //得到的数据写入文件
function writeFile(datas, outputPath) {
for (let i = 0, len = datas.length; i < len; i++) {
fs.writeFileSync(outputPath +
(datas[i].filename || datas[i].lang) +
'.json',
JSON.stringify(datas[i], null, 4)
)
}
rmobj.flush();
}

到此,一个稍微完美的插件就此完成了。 撒花撒花!!!!

收起阅读 »

V8系列第二篇:从执行上下文的角度看JavaScript到底是怎么运行的

1.前言 先来说一说V8引擎和浏览器 V8引擎要想运行起来,就必须依附于浏览器,或者依附于Node.js宿主环境。因此V8引擎是被浏览器或者Node.js启动的。比如在Chrome浏览器中,你打开一个网址后,渲染进程便会初始化V8引擎,同时在V8中会初始化堆空...
继续阅读 »


1.前言


先来说一说V8引擎和浏览器


V8引擎要想运行起来,就必须依附于浏览器,或者依附于Node.js宿主环境。因此V8引擎是被浏览器或者Node.js启动的。比如在Chrome浏览器中,你打开一个网址后,渲染进程便会初始化V8引擎,同时在V8中会初始化堆空间和栈空间,而栈空间就是来管理执行上下文的。而执行上下文就可以说是我们平常写的JavaScript代码的运行环境。


好了简单理解一下,那接下来本篇就重点来学习一下V8引擎中的执行上下文


2.执行上下文概述



首先从宏观的角度来说: JavaScript代码要想能够被执行,就必须先被V8引擎编译,编译完成之后才会进入到执行阶段,总结为六个字:先编译再执行



在V8引擎编译的过程中,同时会生成执行上下文。最开始执行代码的时候通常会生成全局执行上下文、执行一个函数时会生成该函数的执行上下文、当执行一个代码块时也会生成代码块的可执行上下文。所以一段代码可以说成是先编译再执行,那么整个过程就是无数个先编译再执行构成的(通常编译发生在执行代码前的几微秒,甚至更短的时间)。


我们再来理解一下上面说到的执行上下文, 在JavaScript 高级程序设计(第四版)中大概是这样描述的:



执行上下文的概念在JavaScript中是非常重要的。变量或者函数的执行上下文决定了它们可以访问哪些数据,以及他们拥有哪些行为(可以执行哪些方法吧)。每个执行上下文都有一个关联的变量对象,而这个执行上下文中定义的所有变量和函数都存在于这个对象上。这个对象我们在代码中是无法访问的。



执行上下文可以说是执行一段JavaScript代码的运行环境,可以算作是一个抽象的概念。


简单的理解一下概念(下文如果再需要的时候你可以返回顶部再次理解查看)后,我们就来看看JavaScript是怎么将一个变量和函数运行起来的。


3.准备测试代码


这里为了更直观的查看代码的运行效果,我特意新建了一个xxxx.html文件,文件所有代码如下所示:
这里突然发现html文件中只有script标签和js代码也是可以执行的,不清楚以前是不是也是可以,还是说JavaScript引擎在后期做了优化处理。


<script>
a_Function()
var a_variable = 'aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>


特别强调一个点,我上面声明变量使用的var关键字



运行后的执行结果


image.png


4.调试var声明的变量


相信通过运行结果,你心中应该有了自己的代码执行过程了。我们接着往下操作,在第2行代码(下文截图中的位置)打个调试断点,如下图所示


image.png


此时代码已经准备开始要执行a_Function函数了。脑补一下,我们就以此为分割点(按正常来说这肯定是不合理的,因为代码已经开始执行了,不过你可以暂且这样尝试去理解一下),就是运行到第2行代码之前的时间段或者状态,我们就称它为编译阶段,这之后代码就开始运行了,我们称它为执行阶段


1、通过截图可以发现,作用域下的全局 已经有了一个a_Function函数,以及一个a_variable变量其值为 undefined,这里可以看到许许多多的其他变量、函数,这其实就是全局window对象。


2、使用过JavaScript的人都清楚,JavaScript是按照顺序执行代码的,但是通过截图去看,好像又不太对劲,所以执行前的编译阶段,JavaScript引擎还是处理了不少事情的,它做了什么事情呢?


V8引擎编译这段代码的时候,同时会生成一个全局执行上下文,在截图的第二行代码发现是一个函数,便会在代码中查找到该函数的定义,并将该函数体放到全局执行上下文词法环境中。该函数体里的代码还未执行,所以不会去编译,继续第三行代码,发现是var声明的一个变量,便会将该变量放到全局执行上文变量环境中,同时给该变量赋值为undefined。


具体如下模拟代码


function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
var a_variable = undefined

这段代码主要在编译代码阶段做了变量提升,会将var声明的变量存放到变量环境中(let和const声明的变量存放到词法环境中),而函数的声明会被存放到词法环境中。
词法环境变量环境是存在于执行上下文的,变量的默认值会被设置为undefined,函数的执行体会被带到词法环境
然后还会生成可执行代码,其实编译生成的是字节码,下面的代码算是模拟代码:


a_Function()
a_variable = 'aehyok'
console.log(a_variable)

  • 执行阶段
    接下来开始按照顺序执行上面生成的可执行代码,其实在执行阶段已经变成了机器码

a_Function()
a_variable = 'aehyok'
console.log(a_variable)

第一行模拟代码:先调用a_Function,此时会开始生成该函数的函数执行上下文, 执行a_Function中的代码,函数a_Function执行了 undefined,因为此时的a_variable还没给予赋值操作


第二行模拟代码:对a_variable变量进行赋值字符串"aehyok",此时变量环境中的a_variable值变为"aehyok"


第三行模拟代码:打印已经赋值为aehyok的变量。


5.调试let声明的变量


5.1主要是将上面的测试代码中:声明变量的关键字var改为let


<script>
a_Function()
let a_variable = 'aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>

执行代码以后发现直接报错了,报错内容如下图所示


image.png


5.2打断点调试代码


image.png
代码断点打到如截图中第2行位置,可以看到let声明的变量,存在于单独的Script作用域中,并且赋值为undefined。


5.3分析5.1和5.2的代码


  • 通过varlet两种方式代码运行比对情况来看,let声明变量的方式不存在变量提升的情况。
  • 通过3.2截图可以发现,let声明变量的方式,在作用域中的已经创建,并赋值为undefined,但通过查阅资料发现:


let声明的变量,主要是因为V8虚拟机做了限制,虽然a_variable已经在内存中并且赋值为undefined,但是当你在let a_variable 之前访问a_variable时,根据ECMAScript定义,虚拟机会阻止的访问!也可以说成是形成了暂时性的死区,这是语法规定出来的。所以就会报错。



6.调试let声明的变量继续执行


主要添加了一个let声明的变量,以及为其进行了赋值操作,代码如下所示


<script>
a_Function()
var a_variable = 'a_aehyok'
let aa_variable = 'aa_aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>

执行后情况截图如下


image.png


可以发现通过var声明的变量和let(也可以使用const)声明的变量被储存在了不同的位置,之前上面说过通过var声明的变量被存放到了变量环境中了。那么现在我再告诉你,通过let(也可以是const)声明的变量被存放到了词法环境中了。


  • var声明的变量存放在变量环境
  • let和const声明的变量存放在词法环境
  • 函数的声明存放在词法环境
  • 变量环境词法环境都存在于执行上下文

7.总结三种执行上下文


在上面的一小段代码中,我们已经使用过了两种执行上下文,全局执行上下文函数执行上下文


  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。


var声明的变量会在全局window对象上,而let和const声明的变量是不会在全局window对象上的。而全局函数时会在全局window对象上。




  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。



  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。



8.总结



  • 1、通过这篇简单的文章,我想我自己理清楚了,原来JavaScript代码是先编译再执行的。



  • 2、然后代码在编译的时候就生成了执行上下文,也就是代码运行的环境。



  • 3、var声明的变量存在变量提升,并且在编译阶段存放到了变量环境中,变量环境其实也是一个词法环境



  • 4、通过变量提升发现,代码会先生成执行上下文,然后再生成可执行的代码



  • 5、const和let声明的变量不存在变量提升,并且再编译阶段被存放到了词法环境中。



  • 6、所有var定义的全局变量和全局定义的函数,都会在window对象上。



  • 7、所有let和const定义的全局变量不会定义在全局上下文中,但是在作用域链的解析效果上是一样的(跟var定义的)。

 
收起阅读 »

V8开篇:V8是如何运行JavaScript(let a = 1)代码的?

我们知道,机器是不能直接理解我们平常工作或者自己学习的代码的。所以,在执行程序之前,需要将代码翻译成机器能读懂的机器语言。按语言的执行流程,可以把计算机语言划分为编译型语言和解释型语言: 编译型语言:在代码运行前编译器直接将对应的代码转换成机器码,运行时不需...
继续阅读 »


我们知道,机器是不能直接理解我们平常工作或者自己学习的代码的。所以,在执行程序之前,需要将代码翻译成机器能读懂的机器语言。按语言的执行流程,可以把计算机语言划分为编译型语言和解释型语言:



编译型语言:在代码运行前编译器直接将对应的代码转换成机器码,运行时不需要再重新翻译,直接可以使用编译后的结果。




解释型语言:需要将代码转换成机器码,和编译型语言的区别在于运行时需要转换。解释型语言的执行速度要慢于编译型语言,因为解释型语言每次执行都需要把源码转换一次才能执行。



Java 和 C++ 等语言都是编译型语言,而 JavaScript 是解释性语言,它整体的执行速度会略慢于编译型的语言。V8 是众多JavaScript引擎中性能表现最好的一个,并且它是 Chrome 的内核,Node.js 也是基于 V8 引擎研发的。


1.运行的整体过程


未命名文件 (4).png


2.英译汉翻译的过程


比如我们看到了google V8官网的一篇英文文章 v8.dev/blog/faster…,在阅读的过程中,可以就是要对每一个单词进行解析翻译成中文,然后多个单词进行语法的解析,再通过对整句话进行整个语句进行解析,那么这句话就翻译结束了。


下面我们就举例一句英文的翻译过程:I am a programmer。


  • 1、首先对输入的字符串I am a programmer。进行拆分便会拆分成 I am a programmer


相当于词法分析




  • 2、I 是一个主语, am 是一个谓语, a是一个形容词, programmer是个名词, 标点符号。



  • 3、I的意思, am的意思, a一个的意思, programmer程序员的意思, 句号的意思。




2和3一起相当于语法分析



  • 4、对3中的语法分析进行拼接处理:我是一个程序员。当然这是非常简单的一个英译汉,一篇文章的话,就会复杂一些了。


相当于语义分析



3.V8运行的整个过程


3.1.准备一段JavaScript源代码


let a = 10

3.2.词法分析:


一段源代码,就是一段字符串。编译器识别源代码的第一步就是要进行分词,将源代码拆解成一个个的token。所谓的token,就是不可再分的单个字符或者字符串。


3.3.token


通过 esprima.org/demo/parse.… 可以查看生成的tokens,也就是上面那段源代码生成的所有token。


Token类别: 关键字、标识符、字面量、操作符、数据类型(String、Numeric)等


image.png


3.4.语法分析


将上一步生成的 token 数据,根据语法规则转为 AST。通过astexplorer.net 可以查看生成AST抽象语法树。


3.5.AST


生成的AST如下图所示,生成过程就是先分词(词法分析),再解析(语法分析)


image.png
当然你也可以查看生成的AST的JSON结构


{
"type": "Program",
"start": 0,
"end": 9,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 9,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 9,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "a"
},
"init": {
"type": "Literal",
"start": 8,
"end": 9,
"value": 1,
"raw": "1"
}
}
],
"kind": "let"
}
],
"sourceType": "module"
}

同样我在本地下载了v8,直接用v8来查看AST


v8-debug  --print-ast hello.js

image.png


3.6.解释器


解释器会将AST生成字节码,生成字节码的过程也就是对AST抽象语法树进行遍历循环,并进行语义分析


3.7.字节码


在最开始的V8引擎中是没有字节码,是直接将AST转换生成为机器码。这种架构存在的问题就是内存消耗特别大,尤其是在移动设备上,编译出来的机器码占了整个chorme浏览器的三分之一,这样为代码运行时留下的内存就更小了。
于是后来在V8中加入了Ignition 解释器,引入字节码,主要就是为了减少内存消耗。
本地可以使用V8命令行查看生成的字节码


v8-debug  --print-bytecode hello.js

image.png


3.8.热点代码


首先判断字节码是否为热点代码。通常第一次执行的字节码,Ignition 解释器会逐条解释执行。在执行的过程中,如果发现是热点代码,比如for 循环中的代码被执行了多次,这种就称之为热点代码。那么后台的TurboFan就会把该段热点代码编译为高效的机器码,然后再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了, 这样就大大提升了代码的执行效率。


3.9.编译器


TurboFan编译器也可以说是JIT的即时编译器,也可以说是优化编译器。



Ignition 解释器: 可以将AST生成字节码,还可以解释执行字节码。



4、总结


  • 了解V8整个的运行机制
  • 学习JavaScript到底是怎么运行的
  • 对日后编写JavaScript代码有非常多的好处
  • 看完学习了,能提升我们的技术水平
  • 对于日后遇到问题,能够从底层去思考问题出在那里,更快速的定位和解决问题
  • 真的非常熟悉了,可以自己开发一门新的语言
 
收起阅读 »

一盏茶的功夫,拿捏作用域&作用域链

前言 我们需要先知道的是引擎,引擎的工作简单粗暴,就是负责javascript从头到尾代码的执行。引擎的一个好朋友是编译器,主要负责代码的分析和编译等;引擎的另一个好朋友就是今天的主角--作用域。那么作用域用来干什么呢?作用域链跟作用域又有什么关系呢? 一、作...
继续阅读 »


前言


我们需要先知道的是引擎,引擎的工作简单粗暴,就是负责javascript从头到尾代码的执行。引擎的一个好朋友是编译器,主要负责代码的分析和编译等;引擎的另一个好朋友就是今天的主角--作用域。那么作用域用来干什么呢?作用域链跟作用域又有什么关系呢?


一、作用域(scope)


作用域的定义:作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。


1、作用域的分类

  1. 全局作用域

var name="global";
function foo(){
console.log(name);
}
foo();//global

这里函数foo()内部并没有声明name变量,但是依然打印了name的值,说明函数内部可以访问到全局作用域,读取name变量。再来一个例子:


hobby='music';
function foo(){
hobby='book';
console.log(hobby);
}
foo();//book

这里全局作用域和函数foo()内部都没有声明hobby这个变量,为什么不会报错呢?这是因为hobby='music';写在了全局作用域,就算没有var,let,const的声明,也会被挂在window对象上,所以函数foo()不仅可以读取,还可以修改值。也就是说hobby='music';等价于window.hobby='music';


  1. 函数体作用域

函数体的作用域是通过隐藏内部实现的。换句话说,就是我们常说的,内层作用域可以访问外层作用域,但是外层作用域不能访问内层。原因,说到作用域链的时候就迎刃而解了。


function foo(){
var age=19;
console.log(age);
}
console.log(age);//ReferenceError:age is not defined

很明显,全局作用域下并没有age变量,但是函数foo()内部有,但是外部访问不到,自然而然就会报错了,而函数foo()没有调用,也就不会执行。


  1. 块级作用域

块级作用域更是见怪不怪,像我们接触的let作用域,代码块{},for循环用let时的作用域,if,while,switch等等。然而,更深刻理解块级作用域的前提是,我们需要先认识认识这几个名词:


--标识符:能在作用域生效的变量。函数的参数,变量,函数名。需要格外注意的是:函数体内部的标识符外部访问不到


--函数声明:function 函数名(){}


--函数表达式: var 函数名=function(){}


--自执行函数: (function 函数名(){})();自执行函数前面的语句必须有分号,通常用于隐藏作用域。


接下来我们就用一个例子,一口气展示完吧


function foo(sex){
console.log(sex);
}
var f=function(){
console.log('hello');
}
var height=180;
(
function fn(){
console.log(height);
}
)();
foo('female');
//依次打印:
//180
//female
//hello

分析一下:标识符:foo,sex,height,fn;函数声明:function foo(sex){};函数表达式:var f=function(){};自执行函数:(function fn(){})();需要注意,自执行函数fn()前面的var height=180;语句,分号不能抛弃。否则,你可以试一下。


二、预编译


说好只是作用域和作用域链的,但是考虑到理解作用域链的必要性,这里还是先聊聊预编译吧。先讨论预编译在不同环境发生的情况下,是如何进行预编译的。


  1. 发生在代码执行之前

(1)声明提升


console.log(b);
var b=123;//undefined

这里打印undefined,这不是报错,与Refference:b is not defined不同。这是代码执行之前,预编译的结果,等同于以下代码:


var b;//声明提升
console.log(b);//undefined
b=123;

(2)函数声明整体提升


test();//hello123  调用函数前并没有声明,但是任然打印,是因为函数声明整体提升了
function test(){
var a=123;
console.log('hello'+a);
}

2.发生在函数执行之前


理解这个只需要掌握四部曲


(1)创建一个AO(Activation Object)


(2)找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined


(3)将实参和形参统一


(4)在函数体内找函数声明,将函数名作为AO对象的属性名,属性值予函数体
那么接下来就放大招了:


var global='window';
function foo(name,sex){
console.log(name);
function name(){};
console.log(name);
var nums=123;
function nums(){};
console.log(nums);
var fn=function(){};
console.log(fn);
}
foo('html');

这里的结果是什么呢?分析如下:


//从上到下
//1、创建一个AO(Activation Object)
AO:{
//2、找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined
name:undefined,
sex:undefined,
nums=undefined,
fn:undefined,
//3、将实参和形参统一
name:html,
sex:undefined,
nums=123,
fn:function(){},
//4、在函数体内找函数声明,将函数名作为AO对象的属性名,属性值予函数体
name:function(){},
sex:undefined,
fn:function(){},
nums:123//这里不仅存在nums变量声明,也存在nums函数声明,但是取前者的值

以上步骤得到的值,会按照后面步骤得到的值覆盖前面步骤得到的值
}
//依次打印
//[Function: name]
//[Function: name]
//123
//[Function: fn]

3.发生在全局(内层作用域可以访问外层作用域)


同发生在函数执行前一样,发生在全局的预编译也有自己的三部曲:


(1)创建GO(Global Object)对象
(2)找全局变量声明,将变量声明作为GO的属性名,属性值为undefined
(3)在全局找函数声明,将函数名作为GO对象的属性名,属性值赋予函数体
举个栗子:


var global='window';
function foo(a){
console.log(a);
console.log(global);
var b;
}
var fn=function(){};
console.log(fn);
foo(123);
console.log(b);

这个例子比较简单,一样的步骤和思路,就不在赘述分析了,相信你已经会了。打印结果依次是:


[Function: fn]
123
window
ReferenceError: b is not defined

好啦,进入正轨,我们接着说作用域链。


三、作用域链


作用域链就可以帮我们找到,为什么内层可以访问到外层,而外层访问不到内层?但是同样的,在认识作用域链之前,我们需要见识见识一些更加晦涩抽象的名词。


  1. 执行期上下文:当函数执行的时候,会创建一个称为执行期上下文的对象(AO对象),一个执行期上下文定义了一个函数执行时的环境。 函数每次执行时,对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行期上下文会被销毁。
  2. 查找变量:从作用域链的顶端依次往下查找。

3. [[scope]]:作用域属性,也称为隐式属性,仅支持引擎自己访问。函数作用域,是不可访问的,其中存储了运行期上下文的结合。


我们先看一眼函数的自带属性:


function test(){//函数被创建的那一刻,就携带name,prototype属性
console.log(123);
}
console.log(test.name);//test
console.log(test.prototype);//{} 原型
// console.log(test[[scope]]);访问不到,作用域属性,也称为隐式属性

// test() --->AO:{}执行完毕会回收
// test() --->AO:{}执行完毕会回收

接下来看看作用域链怎么实现的:


var global='window';
function foo(){
function fn(){
var fn=222;
}
var foo=111;
console.log(foo);
}
foo();

分析:


GO:{
foo:function(){}
}
fooAO:{
foo:111,
fn:function(){}
}
fnAO:{
fn:222
}
// foo定义时 foo.[[scope]]---->0:GO{}
// foo执行时 foo.[[scope]]---->0:AO{} 1:GO{} 后访问的在前面
//fn定义时 fn.[[scope]]---->0:fnAO{} 1:fooAO{} 2:GO{}
fnAO:fn的AO对象;fooAO:foo的AO对象

 


综上而言:作用域链就是[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。



收起阅读 »

Dart(三)—方法定义、箭头函数、函数相互调用、匿名、自执行方法及闭包

方法定义 dart自定义方法的基本格式: 返回类型 方法名称(参数1,参数2,...){ 方法体 return 返回值 / 或无返回值; } 定义方法的的几个例子: void printInfo(){ print('我是一个自定义方法');...
继续阅读 »

方法定义



dart自定义方法的基本格式:

返回类型 方法名称(参数1,参数2,...){
方法体
return 返回值 / 或无返回值;
}

定义方法的的几个例子:


void printInfo(){
print('我是一个自定义方法');
}

int getNum(){
var count = 123;
return count;
}

String printUserInfo(){

return 'this is str';
}

List getList(){

return ['111','2222','333'];
}

Dart没有public 、private等关键字,_ 下横向直接代表 private


方法的作用域


void main(){

void outFun(){
innerFun(){

print('aaa');
}
innerFun();
}

// innerFun(); 错误写法

outFun(); //调用方法
}

方法传参


一般定义:


String getUserInfo(String username, int age) {
//形参
return "姓名:$username -> 年龄:$age";
}

print(printUserInfo('小明', 23)); //实参

Dart中可以定义一个带可选参数的方法 ,可选参数需要指定类型默认值:


void main() {
String printUserInfo(String username, [int age = 0]) { //age格式表示可选
//形参
if (age != 0) {
return "姓名:$username -> 年龄:$age";
}
return "姓名:$username -> 年龄不详";
}

print(printUserInfo('小明', 28)); //实参
//可选就可以不传了
print(printUserInfo('李四'));
}

定义一个带默认参数的方法:


String getUserInfo(String username,[String sex='男',int age=0]){  //形参
if(age!=0){
return "姓名:$username -> 性别:$sex -> 年龄:$age";
}
return "姓名:$username -> 性别:$sex -> 年龄不详";
}
print(getUserInfo('张三'));
print(getUserInfo('李四','男'));
print(getUserInfo('李梅梅','女',25));

定义一个命名参数的方法,定义命名参数需要指定类型默认值:


命名参数的好处是在使用时可以不用按顺序赋值,看下面代码:


String getUserInfo(String username, {int age = 0, String sex = '男'}) {//形参
if (age != 0) {
return "姓名:$username -> 性别:$sex -> 年龄:$age";
}
return "姓名:$username -> 性别:$sex -> 年龄保密";
}
print(getUserInfo('张三',sex: '男',age: 20));

定义一个把方法当做参数的方法:


其实就是方法可以当做参数来用,这点和Kotlin也是一样的:


//方法1 随便打印一下
fun1() {
print('fun1');
}

//方法2 参数是一个方法
fun2(fun) {
fun();
}

//调用fun2这个方法 把fun1这个方法当做参数传入
fun2(fun1());

箭头函数和函数的相互调用


箭头函数


在之前的学习中,我们知道可以使用forEach来遍历List,其一般格式如下:


List list = ['a', 'b', 'c'];
list.forEach((value) {
print(value);
});

而箭头函数就是可以简写这种格式:


list.forEach((value) => print(value));

箭头后面指向的就是方法的返回值,这里要注意的是:



箭头函数内只能写一条语句,并且语句后面没有分号(;)



对于之前map转换的例子也可以使用箭头方法来简化一下:


List list = [1, 3, 6, 8, 9];
var newList = list.map((value) {
if (value > 3) {
return value * 2;
}
return value;
});

这里就是修改List里面的数据,让数组中大于3的值乘以2。那用箭头函数简化后可以写成:


var newList = list.map((value) => value > 3 ? value*2 : value);

一句代码完成,非常有意思。


函数的相互调用


  // 定义一个方法来判断一个数是否是偶数  
bool isEvenNumber(int n) {
if (n % 2 == 0) {
return true;
}
return false;
}
// 定义一个方法打印1-n以内的所有偶数
prinEvenNumber(int n) {
for (var i = 1; i <= n; i++) {
if (isEvenNumber(i)) {
print(i);
}
}
}
prinEvenNumber(10);

匿名方法、自执行方法及方法的递归


匿名方法


var printNum = (){
print(12);
};
printNum();

这里很明显跟Kotlin中的特性基本是一样的。带参数的匿名方法:


var printNum = (int n) {
print(n + 2);
};

printNum(3);

自执行方法


自执行方法顾名思义就是不需要调用,会自动去执行的,这是因为自执行函数的定义和调用合为了一体。当我们创建了一个匿名函数,并执行了它,由于外部无法引用的它的内部变量,所以在执行完就会很快被释放,而且这种做法不会污染到全局对象。看如下代码:


((int n) {
print("这是一个自执行方法 + $n");
})(666);
}

方法的递归


方法的递归无非就是在条件满足的条件下继续在方法内调用自己本身,看以下代码:


var sum = 0;
void fn(int n) {
sum += n;
if (n == 0) {
return;
}
fn(n - 1);
}
fn(100);
print(sum);

实现的是1加到100。


闭包


闭包是一个前端的概念,客户端开发早期使用Java可以说是不支持闭包,或是不完整的闭包,但Kotlin是可以支持闭包的操作。


闭包的意思就是函数嵌套函数, 内部函数会调用外部函数的变量或参数, 变量或参数不会被系统回收(不会释放内存)。所以闭包解决的两个问题是:



  • 变量常驻内存

  • 变量不污染全局


闭包的一般写法是:



  • 函数嵌套函数,并return 里面的函数,这样就形成了闭包


闭包的写法:


Function func() {
var a = 1; /*不会污染全局 常驻内存*/
return () {
a++;
print(a);
};
}

这里return匿名方法后,a的值就可以常驻内存了:


var mFun = func();
mFun();
mFun();
mFun();

打印:2、3、4。


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

Dart(二)—循环表达式、List、Set及Map的常用属性和方法

Dart的循环表达式 for循环 for (int i = 1; i<=100; i++) { print(i); } 也可以写成: for (var i = 1; i<=10; i++) { print(i); } 对于List...
继续阅读 »

Dart的循环表达式


for循环


for (int i = 1; i<=100; i++) {   
print(i);
}

也可以写成:


for (var i = 1; i<=10; i++) {
print(i);
}

对于List的遍历我们可以这样做:


var list = <String>["张三","李四","王五"];
for (var element in list) {
print(element);
}

对于Map的迭代我们也可以使用for循环语句:


var person={
"name":"小明",
"age":28,
"work":["程序员","Android开发"]
};

person.forEach((key, value) {
print(value);
});

while语句


while有两种语句格式:


while(表达式/循环条件){    

}

do{
语句/循环体

}while(表达式/循环条件);


注意:



  • 1、最后的分号不要忘记

  • 2、循环条件中使用的变量需要经过初始化

  • 3、循环体中,应有结束循环的条件,否则会造成死循环。



看下面代码:


int i = 1;
while (i <= 10) {
print(i);
i++;
}

do...while()最大的区别就是不管条件成立与否都会至少执行一次:


var i = 2;
do{
print('执行代码');
}while(i < 2);

break和continue语句


break语句功能:



  • switch语句中使流程跳出switch结构。

  • 在循环语句中使流程跳出当前循环,遇到break循环终止,后面代码也不会执行


需要强调的是:



  • 如果在循环中已经执行了break语句,就不会执行循环体中位于break后的语句。

  • 在多层循环中,一个break语句只能向外跳出一层


break可以用在switch case中 也可以用在for循环和while循环中。


continue语句的功能:


只能在循环语句中使用,使本次循环结束,即跳过循环体中下面尚未执行的语句,接着进行下次的是否执行循环的判断。


continue可以用在for循环以及while循环中,但是不建议用在while循环中,不小心容易死循环。


break使用:


//如果 i等于4的话跳出循环
for(var i=1;i<=10;i++){
if(i==4){
break; /*跳出循环体*/
}
print(i);
}

//break语句只能向外跳出一层
for(var i = 0;i < 5;i++){
for(var j = 0;j< 3;j++){
if(j == 1){
break;
}
}
}

while循环跳出:


//while循环 break跳出循环

var i = 1;

while(i< =10){
if(i == 4){
break;
}
print(i);
i++;
}

continue使用:


//如果i等于4的话跳过

for(var i=1;i<=5;i++){
if(i == 2){
continue; //跳过当前循环体 然后循环还会继续执行
}
print(i);
}

List常用属性和方法


常用属性:



  • length 长度

  • reversed 翻转

  • isEmpty 是否为空

  • isNotEmpty 是否不为空


常用方法:



  • add 增加

  • addAll 拼接数组

  • indexOf 查找 传入具体值

  • remove 删除 传入具体值

  • removeAt 删除 传入索引值

  • fillRange 修改

  • insert(index,value) 指定位置插入

  • insertAll(index,list) 指定位置插入List

  • toList() 其他类型转换成List

  • join() List转换成字符串

  • split() 字符串转化成List

  • forEach

  • map

  • where

  • any


一些常用属性和方法使用举例:


var list=['张三','李四','王五',"小明"];
print(list.length);
print(list.isEmpty);
print(list.isNotEmpty);
print(list.reversed); //对列表倒序排序

print(list.indexOf('李四')); //indexOf查找数据 查找不到返回-1 查找到返回索引值

list.remove('王五');

list.removeAt(2);

list.fillRange(1, 2,'a'); //修改 1是开始的位置 2二是结束的位置

print(list);

list.insert(1,'a');

print(list);

list.insertAll(1, ['a','b']); //插入多个

Set


Set的最主要的功能就是去除数组重复内容,它是没有顺序且不能重复的集合,所以不能通过索引去获取值。


var s = new Set();
s.add('A');
s.add('B');
s.add('B');

print(s); //{A, B}

add相同内容时候无法添加进去的。


Set可以通过add方法添加一个List,并清除值相同的元素:


var list = ['香蕉','苹果','西瓜','香蕉','苹果','香蕉','苹果'];
var s = new Set();
s.addAll(list);
print(s);
print(s.toList());

Map常用属性和方法


Map是无序的键值对,它的常用属性主要有以下:


常用属性:



  • keys 获取所有的key值

  • values 获取所有的value值

  • isEmpty 是否为空

  • isNotEmpty 是否不为空


常用方法:



  • remove(key) 删除指定key的数据

  • addAll({...}) 合并映射 给映射内增加属性

  • containsValue 查看映射内的值 返回true/false

  • forEach

  • map

  • where

  • any

  • every


map转换:


List list = [1, 3, 4];
//map转换,根据返回值返回新的元素列表
var newList = list.map((value) {
return value * 2;
});
print(newList.toList());

where:获取符合条件的元素:


List list = [1,3,4,5,7,8,9];

var newList = list.where((value){
return value > 5;
});
print(newList.toList());

any:是否有符合条件的元素


List list = [1, 3, 4, 5, 7, 8, 9];
//只要集合里面有满足条件的就返回true
var isContain = list.any((value) {
return value > 5;
});
print(isContain);

every:需要每一个都满足条件


List myList=[1,3,4,5,7,8,9];
//每一个都满足条件返回true 否则返回false
var flag = myList.every((value){

return value > 5;
});
print(flag);

Set使用forEach遍历:


var s=new Set();

s.addAll([11,22,33]);

s.forEach((value) => print(value));

Map使用forEach遍历:


Map person={
"name":"张三",
"age":28
};

person.forEach((key,value){
print("$key -> $value");
});

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

Dart(一)—变量、常量、基本类型、运算符、条件判断以及类型转换

前言 Dart语言跟Kotlin都是一种强大的脚本语言,它的很多语法跟Kotlin是很相似的。比如Dart也是可以不预先定义变量类型 ,自动会类型推倒,它修饰一般变量的关键字也是var,所以如果我们熟悉Kotlin,Dart也会很容易上手。 Dart变量和常量...
继续阅读 »

前言


Dart语言跟Kotlin都是一种强大的脚本语言,它的很多语法跟Kotlin是很相似的。比如Dart也是可以不预先定义变量类型 ,自动会类型推倒,它修饰一般变量的关键字也是var,所以如果我们熟悉KotlinDart也会很容易上手。


Dart变量和常量


变量


如前言所说,DartKotlin一样是强大的脚本类语言,可以不预先定义变量类型 ,自动会类型推倒,Dart中定义变量可以通过var关键字可以通过类型来申明变量:


var str = 'dart';

String str2 = 'this is dart';

int count = 123;


注意: var 后就不要写类型 , 写了类型 不要var 两者都写 var a int = 5; 报错



常量:final 和 const修饰符



  • const修饰的值不变 要在定义变量的时候就得赋值;

  • final可以开始不赋值 只能赋一次,而final不仅有const的编译时常量的特性,最重要的它是运行时常量,并且final是惰性初始化,即在运行时第一次使用前才初始化。


final name = 'Max';
final String sex = '男';

const bar = 1000000;
const double atm = 1.01325 * bar;

如果我们使用了阿里的代码规范插件,其实他会提示我们最好用const代替final


Dart的命名规则



  • 变量名称必须由数字、字母、下划线和美元符($)组成。

  • 注意:标识符开头不能是数字

  • 标识符不能是保留字和关键字。

  • 变量的名字是区分大小写的如: age和Age是不同的变量。在实际的运用中,也建议,不要用一个单词大小写区分两个变量。

  • 标识符(变量名称)一定要见名思意 :变量名称建议用名词,方法名称建议用动词


Dart的入口方法


Dart 入口方法main有两种定义


//表示main方法没有返回值
void main(){
print('dart');
}

main(){

print('dart');
}

Dart基本类型


数据类型


Dart中常用的数据类型有以下的类型:


Numbers(数值):
int
double
Strings(字符串)
String
Booleans(布尔)
bool
List(数组)
在Dart中,数组是列表对象,所以大多数人只是称它们为列表
Maps(字典)
通常来说,Map 是一个键值对相关的对象。 键和值可以是任何类型的对象。每个键只出现一次,而一个值则可以出现多次

数值类型: int double


int整型:


  int a=123;
a=45;

double既可以是整型,也可是浮点型:


double b=23.5;

b=24;

字符串类型


字符串定义:


var str1='this is str1';

String str2='this is str2';

字符串拼接:


print("$str1 $str2");

print(str1 + str2);

布尔类型


定义方式:


bool flag1=true;

var flag2=true;

判断条件上和Kotlin使用无异。


List(数组/集合)


不指定类型定义List


var list1 = ["张三",20,true];

print(list1);
print(list1[2]);

这就有点颠覆我们以往的观念了,一个list里面还可以有不同的类型。


指定类型定义List


var list2 = <String>["张三","李四"];

print(list2);

通过[]来定义Lsit


通过[]创建的集合的容量可以变化:


var list = [];

list.add("小明");
list.add(24);
list.add(true);

print(list);

也可以指定List中的元素类型:


List<String> list = [];

又或者是:


List<String> list = List.empty(growable: true);

growable 为 false 是为 固定长度列表,为 true 是为 长度可变列表


通过List.filled创建的集合长度是固定:


var list1 = List.filled(2, "");

var list2 = List<String>.filled(2, "");

Map


Map的定义:


直接赋值方式:


var person={
"name":"小明",
"age":28,
"work":["程序员","Android开发"]
};

print(person["name"]);

print(person["age"]);

print(person["work"]);


通过Map分别赋值:


var map =new Map();

map["name"]="小明";
map["age"]=26;
map["work"]=["程序员","Android开发"];
print(map);

is 关键词来判断类型


var str = 123;

if(str is String){
print('是string类型');
}else if(str is int){
print('int');
}else{
print('其他类型');
}

运算符


算术运算符


使用和符号上和Kotlin中的基本无异:


int a=13;
int b=5;

print(a+b); //加
print(a-b); //减
print(a*b); //乘
print(a/b); //除
print(a%b); //其余
print(a~/b); //取整

关系运算符


关系运算符主要有:


==    !=   >    <    >=    <=

使用:


int a=5;
int b=3;

print(a==b); //判断是否相等
print(a!=b); //判断是否不等
print(a>b); //判断是否大于
print(a<b); //判断是否小于
print(a>=b); //判断是否大于等于
print(a<=b); //判断是否小于等于

逻辑运算符


! 取反:


bool flag=false;
print(!flag); //取反

&&并且:全部为true的话值为true 否则值为false:


bool a=true;
bool b=true;

print(a && b);

||或者:全为false的话值为false 否则值为true:


bool a=false;
bool b=false;

print(a || b);

赋值运算符


基础赋值运算符 =、??= ++ --


int c=a+b;   //从右向左

b??=23;  表示如果b为空的话把 23赋值给b

++ --


// ++  --   表示自增 自减 1
//在赋值运算里面 如果++ -- 写在前面 这时候先运算 再赋值,如果++ --写在后面 先赋值后运行运算

var a = 10;
var b = a--;

print(a); //9
print(b); //10

// var a=10;

// a++; //a=a+1;

// print(a);

复合赋值运算符 +=、-= 、*= 、 /= 、%= 、~/=


+=


var a=12;
a+=12; //a = a+12
print(a);

-=


a-=6; // a = a-6

*=


a*=3;  //a=a*3;

/=


需要返回double类型


double a=12;
a/=12;

%=


double a=12;
a %= 12;

~/=


返回的是int整型


int a1 = 3;
int a2 = 2;

int a = a1 ~/= a2;

a = 1.


条件表达式


**if else **


 bool flag=true;

if(flag){
print('true');
}else{
print('false');
}

switch case


var sex = "女";
switch (sex) {
case "男":
print('性别是男');
break;
case "女":
print('性别是女');
break;
default:
print('传入参数错误');
break;
}

三目运算符


bool flag = false;
String str = flag?'我是true':'我是false';
print(str);

??运算符


var a;
var b= a ?? 10;

print(b); // a为空,则赋值为10

// var a=22;
// var b= a ?? 10;
//
// print(b); // 20

类型转换


Number与String类型之间的转换



  • Number类型转换成String类型toString()

  • String类型转成Number类型int.parse()


StringNumber


String str = '123';

var myNum = int.parse(str);

print(myNum is int);

// String str='123.1';

// var myNum=double.parse(str);

// print(myNum is double);


String:


var myNum=12;

var str=myNum.toString();

print(str is String);

其他类型转换成Boolean类型


isEmpty:判断字符串是否为空


var str = '';
if (str.isEmpty) {
print('str空');
} else {
print('str不为空');
}

isNaN:判断值是否为非数字


var myNum = 0 / 0;

if (myNum.isNaN) {
print('NaN');
}

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

Flutter 桌面端实践之识别外接媒体设备

最近我们希望Flutter技术在桌面端的应用能有所突破,所以笔者跨进了本不熟悉的桌面端应用领域。今天给大家分享下我们是如何让Flutter如何识别外接媒体设备,并且实现视频流渲染和拍照;从官方插件外界纹理到platformView实践,都尝试了一遍,最后选择了...
继续阅读 »

最近我们希望Flutter技术在桌面端的应用能有所突破,所以笔者跨进了本不熟悉的桌面端应用领域。今天给大家分享下我们是如何让Flutter如何识别外接媒体设备,并且实现视频流渲染和拍照;从官方插件外界纹理platformView实践,都尝试了一遍,最后选择了webRtc,整个预研过程一波三折,学到了很多知识!



需求背景


需求是在win10和Android9的设备上支持外接摄像头,能够进行实时拍摄,做一个类似相机的应用。

从技术流程上来分析,我们需要识别出相机设备,拿到媒体流信息然后做渲染(渲染机制一般通过外接纹理Texture去实现),最后捕获帧进行拍照/录制。Flutter中,任何对象渲染后自然能拿到RanderObject,只要有RanderObject这个真实的渲染对象,我们就能进行照片的存储。

以上流程,理论上库已经帮我们做好,但是桌面端的生态,往往没那么简单~~~


一、官方Plugin


Android端使用camera,windows使用camera_windows。官方的库对于内置相机的支持做的很不错,直接引用后在手机和普通电脑上效果都很好;但是两个库都是明确不支持外接设备,见issus-1issus-2,优先级分别是P4、P5,显然官方认为这些问题优先级不高。
而纵观整个Flutter生态对USB外设的支持,并没有一个官方的库,pub上的基本也是参差不齐,大多只支持单一平台。


实现原理



  • Android端的camera插件,使用原生Camera2 Api,通过TextureRegistry创建纹理,然后Flutter用Texture进行绘制。



  1. 创建相机实例,返回textureId


// camera_android-0.9.8+3\lib\src\android_camera.dart
@override
Future<int> createCamera(
CameraDescription cameraDescription,
ResolutionPreset? resolutionPreset, {
bool enableAudio = false,
}) async {
try {
final Map<String, dynamic>? reply = await _channel
.invokeMapMethod<String, dynamic>('create', <String, dynamic>{
'cameraName': cameraDescription.name,
'resolutionPreset': resolutionPreset != null
? _serializeResolutionPreset(resolutionPreset)
: null,
'enableAudio': enableAudio,
});

return reply!['cameraId']! as int;
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}


  1. 预览控件返回Flutter Texture Widget,与原生返回的纹理id形成绑定,从而接收纹理信息然后绘制


// camera_android-0.9.8+3\lib\src\android_camera.dart
@override
Widget buildPreview(int cameraId) {
return Texture(textureId: cameraId);
}


  1. Android端通过TextureRegistry创建createSurfaceTexture,把textureId返回到Dart层。


// camera_android-0.9.8+3\android\src\main\java\io\flutter\plugins\camera\MethodCallHandlerImpl.java
private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException {
String cameraName = call.argument("cameraName");
String preset = call.argument("resolutionPreset");
boolean enableAudio = call.argument("enableAudio");

TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture =
textureRegistry.createSurfaceTexture();
DartMessenger dartMessenger =
new DartMessenger(
messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper()));
CameraProperties cameraProperties =
new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity));
ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset);

camera =
new Camera(
activity,
flutterSurfaceTexture,
new CameraFeatureFactoryImpl(),
dartMessenger,
cameraProperties,
resolutionPreset,
enableAudio);

Map<String, Object> reply = new HashMap<>();
reply.put("cameraId", flutterSurfaceTexture.id());
result.success(reply);
}

值得一提的是Flutter3.0后,官方的原生绘制方式已经抛弃了VirtualDisplay,拥抱TextureLayer,性能上已经优化了不少,让Flutter的音视频渲染能力提升了不少。 但问题就是在instantiateCamera之前,官方在Camera2的实现上,没有对外界设备进行处理,从而搜索不到对应的外接相机。



  • Windows端的实现完全一样,都是通过Texture做渲染,原因也是获取相机列表的时候没有做外接设备的实现,这里不在赘述。


解决方案


基于多端的camera接口做处理,把外设设备的逻辑加上,应该就可以了。 在Texture纹理这块官方的实现是没有问题的。
当然这个思路我目前只停留在理论层面,并未真正去实现,原因如下:



  1. 两个库都是设计原生知识,我们维护成本会很大;

  2. 官方的库维护的很频繁,后面更多优化还得看官方,很有可能哪个版本就得全部推翻重新来一遍。


二、PlatformView


明确一个观点,这个方案不可落地。预研这个方案的原因是我们本身已经有原生的代码封装,基于CameraX的Android实现,我只需要在Plugin上注册下视图即可,具体实现代码如下:



  1. 注册视图


override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
val key: String = "camera"

channel = MethodChannel(flutterPluginBinding.binaryMessenger, "camera_plugin")
channel.setMethodCallHandler(this)
CameraInfoManager.getCameraInfoList().forEach {
Log.d(TAG, "onAttachedToEngine: $it")
}

// 注册视图
flutterPluginBinding.platformViewRegistry.registerViewFactory(
key,
CameraFactory(flutterPluginBinding.binaryMessenger)
)
}


  1. 视图工厂


class CameraFactory(private val messenger: BinaryMessenger) :
PlatformViewFactory(StandardMessageCodec.INSTANCE) {

override fun create(context: Context?, id: Int, args: Any?): PlatformView {
return CameraPlatformView(context!!)
}

}


  1. 引入CameraX视图


class CameraPreView(context: Context, attrs: AttributeSet?) :
LinearLayout(context, attrs), LifecycleOwner {

private var camera: PreviewView

private val mLifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)

init {
val view: View = LayoutInflater.from(context).inflate(R.layout.layout_camera_preview, this)
camera = view.findViewById(R.id.camera_preview_view)
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)

// 这里是封装好的CameraX预览视图
CameraXPreview
.bindLifecycle(this)
.setPreviewView(camera)
.setCameraId(0)
.startPreview(context)
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}

override fun getLifecycle(): Lifecycle = mLifecycleRegistry
}

问题显而易见,Flutter引擎启动白屏300ms,视图同步产生延时,内存平均新增20M+,而且视图生命周期没法同步,都是致命问题。

基于上面的实践,Windows上我们没有再做尝试了,Fail。


三、webRtc


上面两种方案都以失败告终后,大佬提到了webRtc,从基础协议出发,往往能解决核心问题。于是flutter_webrtc上场,WebRTC提供音视频的采集、编解码、网络传输、显示等功能,并且还支持跨平台:windows,linux,mac,android,已经被纳入被纳入W3C推荐标准。webRtc开发文档



  • 引用flutter_webrtc这个库,其渲染原理依旧是外接纹理,使用方法查看官方的example实例即可;

  • 重点在实现拍照功能,拍照无非就是进行帧捕获,Android已经实现:


  final videoTrack = localStream!
.getVideoTracks()
.firstWhere((track) => track.kind == 'video');
final frame = await videoTrack.captureFrame();

// 使用image.memory即可渲染
frameList = frame.asUint8List();


  • 而windows端很遗憾,还没有实现拍照功能,见issus;于是我想到了曲线救国,通过截取屏幕来保存图像由于是使用Texture渲染,通常的RenderRepaintBoundary+GlobalKey是没办法拿到RanderObject的!
    幸好插件提供了截取屏幕的方式,也算完成曲线救国了。


try {
var sources = await desktopCapturer.getSources(types: [SourceType.Window]);
DesktopCapturerSource capture =
sources.firstWhere((element) => element.name == 'my_camera');

// 使用image.memory即可渲染
frameList = capture.thumbnail;
return;
} catch (e) {
print(e.toString());
}

写在最后


到此,坎坷的外接相机预研之路告一段落。但是性能比起原生,真的差了一截,这让我们意识到,在官方不支持外接设备之前,针对此类需求,还是少用Flutter来实现。

Flutter桌面应用虽然发布了Stable版本,但说句实话生态确实比移动端差了不少,这意味着我们需要共同建设这个生态,但是趋势起来了,我们也愿意社区共建!

另外插个题外话,关于上面windows截取屏幕的需求,其实是有issus未关闭的,7月1号下午刚参与了issue的讨论,傍晚作者就拉了pull request,并且更了一版,解了燃眉之急啊!!!

怎么说呢,开源万岁,Respect!


image.png


image.png


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

iOS block与__block、weak、__weak、__strong

iOS

首先需要知道:

block,本质是OC对象,对象的内容,是代码块。
封装了函数调用以及函数调用环境。

block也有自己的isa指针,依据block的类别不同,分别指向
__NSGlobalBlock __ ( _NSConcreteGlobalBlock )
__NSStackBlock __ ( _NSConcreteStackBlock )
__NSMallocBlock __ ( _NSConcreteMallocBlock )
需要注意是,ARC下只存在__NSGlobalBlock和__NSMallocBlock。
通常作为参数时,才可能是栈区block,但是由于ARC的copy作用,会将栈区block拷贝到堆上。
通常不管作为属性、参数、局部变量的block,都是__NSGlobalBlock,即使block内部出现了常量、静态变量、全局变量,也是__NSGlobalBlock,
除非block内部出现其他变量,auto变量或者对象属性变量等,就是__NSMallocBlock

为什么block要被拷贝到堆区,变成__NSMallocBlock,可以看如下链接解释:Ios开发-block为什么要用copy修饰

对于基础数据类型,是值传递,修改变量的值,修改的是a所指向的内存空间的值,不会改变a指向的地址。

对于指针(对象)数据类型,修改变量的值,是修改指针变量所指向的对象内存空间的地址,不会改变指针变量本身的地址
简单来说,基础数据类型,只需要考虑值的地址,而指针类型,则需要考虑有指针变量的地址和指针变量指向的对象的地址

以变量a为例

1、基础数据类型,都是指值的地址

1.1无__block修饰,

a=12,地址为A
block内部,a地址变B,不能修改a的值
block外部,a的地址依旧是A,可以修改a的值,与block内部的a互不影响
内外a的地址不一致

1.2有__block修饰

a=12,地址为A
block内部,地址变为B,可以修改a的值,修改后a的地址依旧是B
block外部,地址保持为B,可以修改a的值,修改后a的地址依旧是B

2、指针数据类型

2.1无__block修饰

a=[NSObject new],a指针变量的地址为A,指向的对象地址为B
block内部,a指针变量的地址为C,指向的对象地址为B,不能修改a指向的对象地址
block外部,a指针变量的地址为A,指向的对象地址为B,可以修改a指向的对象地址,
block外部修改后,
外部a指针变量的地址依旧是A,指向的对象地址变为D
内部a指针变量的地址依旧是C,指向的对象地址依旧是B

2.1有__block修饰

a=[NSObject new],a指针变量的地址为A,指向的对象地址为B
block内部,a指针变量的地址为C,指向的对象地址为B,能修改a指向的对象地址
block外部,a指针变量的地址为C,指向的对象地址为B,能修改a指向的对象地址
block内外,或者另一个block中,无论哪里修改,a指针变量地址都保持为C,指向的对象地址保持为修改后的一致

block内修改变量的实质(有__block修饰):

block内部能够修改的值,必须都是存放在堆区的。
1、基础数据类型,__block修饰后,调用block时,会在堆区开辟新的值的存储空间,
指针数据类型,__block修饰后,调用block时,会在堆区开辟新的指针变量地址的存储空间

2、并且无论是基础数据类型还是指针类型,block内和使用block之后,变量的地址所有地址(包括基础数据类型的值的地址,指针类型的指针变量地址,指针指向的对象的地址),都是保持一致的
当然,只有block进行了真实的调用,才会在调用后发生这些地址的变化

另外需要注意的是,如果对一个已存在的对象(变量a),进行__block声明另一个变量b去指向它,
a的指针变量地址为A,b的指针变量会是B,而不是A,
原因很简单,不管有没__block修饰,不同变量名指向即使指向同一个对象,他们的指针变量地址都是不同的。

__weak,__strong

两者本身也都会增加引用计数。
区别在于,__strong声明,会在作用域区间范围增加引用计数1,超过其作用域然后引用计数-1
而__weak声明的变量,只会在其使用的时候(这里使用的时候,指的是一句代码里最终并行使用的次数),临时生成一个__strong引用,引用+次数,一旦使用使用完毕,马上-次数,而不是超出其作用域再-次数

    NSObject *obj = [NSObject new];
NSLog(@"声明时obj:%p, %@, 引用计数:%ld",&obj, obj, CFGetRetainCount((__bridge CFTypeRef)(obj)));
__weak NSObject *weakObj = obj;
NSLog(@"声明时weakObj:%p, %@,%@, %@, 引用计数:%ld",&weakObj, weakObj,weakObj,weakObj, CFGetRetainCount((__bridge CFTypeRef)(weakObj)));
NSLog(@"声明后weakObj引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

声明时obj:0x16daa3968, , 引用计数:1
声明时weakObj:0x16daa3960, ,, , 引用计数:5
声明后weakObj引用计数:2

这个5,是因为obj本来计数是1,

    NSLog(@"声明时weakObj:%p, %@,%@, %@, 引用计数:%ld",&weakObj, weakObj,weakObj,weakObj, CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

这句代码打印5,是因为除去&weakObj(&这个不是使用weakObj指向的对象,而只是取weakObj的指针变量地址,所以不会引起计数+1),另外还使用了4次weakObj,导致引用计数+4

   NSLog(@"声明后weakObj引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

这句打印2,说明上一句使用完毕后,weakObj引用增加的次数会马上清楚,重新变回1,而这句使用了一次weakObj,加上obj的一次引用,就是2了

__weak 与 weak

通常,__weak是单独为某个对象,添加一条弱引用变量的。
weak则是property属性里修饰符。

LGTestBlockObj *testObj = [LGTestBlockObj new];
self.prpertyObj = testObj;
__weak LGTestBlockObj *weakTestObj = testObj;
NSLog(@"testObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(testObj)));
NSLog(@"prpertyObj:%p, %@,%@, %@, 引用计数:%ld",&(_prpertyObj), self.prpertyObj,self.prpertyObj,self.prpertyObj, CFGetRetainCount((__bridge CFTypeRef)(self.prpertyObj)));
NSLog(@"prpertyObj:%p, %@,%@, %@, 引用计数:%ld",&(_prpertyObj), _prpertyObj,_prpertyObj,_prpertyObj, CFGetRetainCount((__bridge CFTypeRef)(_prpertyObj)));
NSLog(@"prpertyObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(_prpertyObj)));
NSLog(@"testObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(testObj)));
NSLog(@"weakTestObj:%p, %@,%@, %@, 引用计数:%ld",&weakTestObj, weakTestObj,weakTestObj,weakTestObj, CFGetRetainCount((__bridge CFTypeRef)(weakTestObj)));

prpertyObj:0x1088017b0, ,, , 引用计数:2
prpertyObj:, 引用计数:2
testObj:, 引用计数:2
weakTestObj:0x16b387958, ,, , 引用计数:6

待补充...

Block常见疑问收录

1、block循环引用

通常,block作为属性,并且block内部直接引用了self,就会出现循环引用,这时就需要__weak来打破循环。

2、__weak为什么能打破循环引用?

一个变量一旦被__weak声明后,这个变量本身就是一个弱引用,只有在使用的那行代码里,才会临时增加引用结束,一旦那句代码执行完毕,引用计数马上-1,所以看起来的效果是,不会增加引用计数,block中也就不会真正持有这个变量了

3、为什么有时候又需要使用__strong来修饰__weak声明的变量?

在block中使用__weak声明的变量,由于block没有对该变量的强引用,block执行的过程中,一旦对象被销毁,该变量就是nil了,会导致block无法继续正常向后执行。
使用__strong,会使得block作用区间,保存一份对该对象的强引用,引用计数+1,一旦block执行完毕,__strong变量就会销毁,引用计数-1
比如block中,代码执行分7步,在执行第二步时,weak变量销毁了,而第五步要用到weak变量。
而在block第一步,可先判断weak变量是否存在,如果存在,加一个__strong引用,这样block执行过程中,就始终存在对weak变量的强引用了,直到block执行完毕

4、看以下代码,obj对象最后打印的引用计数是多少,为什么?

    NSObject *obj = [NSObject new];
void (^testBlock)(void) = ^{
NSLog(@"%@",obj);
};
NSLog(@"引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));

最后的打印的是3
作为一个局部变量的block,由于引用了外部变量(非静态、常量、全局),定义的时候其实是栈区block,但由于ARC机制,使其拷贝到堆上,变成堆block,所以整个函数执行的过程中,实际上该block,存在两份,一个栈区,一个堆区,这就是使得obj引用计数+2了,加上创建obj的引用,就是3了

5、为什么栈区block要copy到堆上

block:我们称代码块,他类似一个方法。而每一个方法都是在被调用的时候从硬盘到内存,然后去执行,执行完就消失,所以,方法的内存不需要我们管理,也就是说,方法是在内存的栈区。所以,block不像OC中的类对象(在堆区),他也是在栈区的。如果我们使用block作为一个对象的属性,我们会使用关键字copy修饰他,因为他在栈区,我们没办法控制他的消亡,当我们用copy修饰的时候,系统会把该 block的实现拷贝一份到堆区,这样我们对应的属性,就拥有的该block的所有权。就可以保证block代码块不会提前消亡。

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

iOS皮肤适配

iOS
1、皮肤颜色资源和图片路径配置皮肤配置文件light.json 配置示例dark.json 配置示例2、设置全局的colorKey 来对应颜色路径 imageKey 来对应图片路径,利于维护颜色key配置图片key配置1、获取皮肤资源协议方法2、皮肤使用3、...
继续阅读 »
皮肤配置文件创建

1、皮肤颜色资源和图片路径配置


皮肤配置文件

如图所示,创建 light.json 和 dark.json ( light 和 dark 配置路径key 一样,对应的value 不同)

light.json 配置示例

{
"statusBarStyle": "black",
"colors":{
"mainFunction":"#E92424",
"gradientStockUp":[
"#0FFFFFFF",
"#0FE92424"
]
},
"images": {
"selfStock_info_icon": "appres/skinImage/light/selfStock_info_icon.png",
"selfStock_money_icon": "appres/skinImage/light/selfStock_money_icon.png",
}

// appres/skinImage/light/selfStock_info_icon.png 对应的图片文件夹路径
}

dark.json 配置示例

{
"statusBarStyle": "red",
"colors":{
"mainFunction":"#BC935C",
"gradientStockUp":[
"#26171717",
"#26E92424"
]
},
"images": {
"selfStock_info_icon": "appres/skinImage/dark/selfStock_info_icon.png",
"selfStock_money_icon": "appres/skinImage/dark/selfStock_money_icon.png",
}
}

2、设置全局的colorKey 来对应颜色路径 imageKey 来对应图片路径,利于维护


颜色key配置


图片key配置

皮肤使用

1、获取皮肤资源协议方法

// 获取皮肤资源协议方法
- (HJThemeDataModel *)getThemeModelWithName:(NSString *)name {
NSString *path = [NSString stringWithFormat:@"appres/theme/%@",name];
NSDictionary *colorDic = [self getDictFromJsonName:path][@"colors"];
NSDictionary *imageDic = [self getDictFromJsonName:path][@"images"];
HJThemeDataModel *model = [HJThemeDataModel new];
model.colorDic = colorDic;
model.imageDic = imageDic;
return model;
}

/// 设置默认主题(使用皮肤,至少有一个默认皮肤)
- (HJThemeDataModel *)getDefaultThemeModel {
return [self getThemeModelWithName:@"light"];
}

2、皮肤使用

// 导入头文件
#import "HJThemeManager.h"

// 设置当前皮肤 或切换 皮肤为 @"light"
[[HJThemeManager sharedInstance] switchThemeWithName:@"light"];

// 设置当前view 的背景色
//1、适配皮肤
self.view.themeBackgroundColor = backgroundColorKey;
//2、不适配皮肤,必须带#号
self.view.themeBackgroundColor = @“#333333;
//3、适配皮肤,随皮肤变化
self.view.themeBackgroundColor = [HJThemeManager getThemeColor:backgroundColorKey];
//4、指定皮肤,不会随皮肤变化
self.view.themeBackgroundColor = [HJThemeManager getThemeColor:backgroundColorKey themeName:@"light"];

/**
* [HJThemeManager getThemeColor:backgroundColorKey];
* 实质上是
theme://
"backgroundColorKey"?
*
* [HJThemeManager getThemeColor:backgroundColorKey themeName:@"light"];
* 实质上是
theme://"backgroundColorKey"?themeName=light
*/

//所以可以直接写URL 例如:
self.view.themeBackgroundColor = theme://backgroundColorKey?themeName=light;

// 设置当前imageView 的image
//1、适配皮肤
imageView.themeImage = imageKey;
//2、适配皮肤,随皮肤变化
imageView.themeImage = [HJThemeManager getThemeImage:imageKey];
//3、指定皮肤,不会随皮肤变化
imageView.themeImage = [HJThemeManager getThemeImage:imageKey themeName:@"light"];

/**
* [HJThemeManager getThemeImage:imageKey];
* 实质上是
theme://
"imageKey"?
*
* [HJThemeManager getThemeImage:imageKey themeName:@"light"];
* 实质上是
theme://"imageKey"?themeName=light
*/


//完整写法,指定皮肤
imageView.themeImage = theme://"imageKey"?themeName=light;

// 兼容不适配皮肤写法
// imageNamed 加载图片
imageView.themeImage = bundle://"imageKey";
// sdwebimage 解析 http/https 加载图片
imageView.themeImage = http://imagePath;
// 使用serverManager getimage 的协议方法获取图片
imageView.themeImage = imagePath;

3、皮肤的实现原理
1、创建一个NSObject分类(category),然后关联一个字典属性(themes),用于进行缓存UI控件调用的颜色方法和参数或者是图片方法和参数。再关联属性的时候添加一个通知监听,用于切换皮肤时,发送通知,然后再次调用缓存的方法和参数,进行颜色和图片的更换。

2、创建UI控件的分类(category),然后每个分类都有themes字典,然后设置新的方法来设置颜色或图片。在该方法内,需要做的处理有:

颜色举例说明:themeBackgroundColor = colorKey

a、在 themeBackgroundColor 的set方法中,判断是否是皮肤设置,皮肤的设置都是带有 theme:// 的字符串。这个(theme://)字符串是约定的。
b、皮肤适配模式,即带有 theme:// 字符串,就会用 themes 字典保存 系统的方法setBackgroundColor: 方法和参数colorKeythemeName,当切换皮肤时,再次调用 setBackgroundColor: 方法和参数colorKeythemeName
c、@"#333333", 直接是色值方法的 不需要 themes 字典保存,只需要直接调用系统方法 setBackgroundColor:[UIColor colorFromHexString:@"#333333"];

图片举例说明:imageView.themeImage = imageKey

a、在 themeImage 的set方法中,判断是否是皮肤设置,皮肤的设置都是带有 theme:// 的字符串。这个(theme://)字符串是约定的。
b、皮肤适配模式,即带有 theme:// 字符串,就会用 themes 字典保存 系统的方法setImage: 方法和参数imageKeythemeName,当切换皮肤时,再次调用 setImage: 方法和参数imageKeythemeName
c、bundle://, 直接是调用系统方法setImage:[UIImage imageNamed:@"imageNamed"] 进行赋值,不需要进行 themes 字典保存处理;
d、http:// 或 https:// , 采用SD框架加载图片,不需要进行 themes 字典保存处理;

3、主要的UI控件的分类

#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>

@interface UIView (CMSThemeView)

/// 设置皮肤文件名称 默认为空值,取当前皮肤
/// 可以设置指定皮肤 例如: @"Dark" / @"Light" ;
/// defaultThemeKey 为默认皮肤
/// 如何设置 Color 或 Image 有 themeName ,优先使用 themeName
指定皮肤
@property (nonatomic, copy) NSString *themeStyle;
@property (nonatomic, copy) NSString *themeBackgroundColor;
@property (nonatomic, copy) NSString *themeTintColor;
/// 根据路径获取color 并缓存方法和参数 ()
- (void)setThemeColorWithIvarName:(NSString *)ivarName colorPath:(NSString *)path;
@end

@interface UILabel (ThemeLabel)
@property (nonatomic, copy) NSString *themeTextColor;
@property (nonatomic, copy) NSString *themeHighlightedTextColor;
@property (nonatomic, copy) NSString *themeShadowColor;
/// 主要是颜色
@property (nonatomic, strong) NSAttributedString *themeAttributedText;
@end

@interface UITextField (ThemeTextField)
@property (nonatomic, copy) NSString *themeTextColor;
@end

@interface UIImageView (CMSThemeImageView)
@property (nonatomic, copy) NSString *themeImage;

// 带有 UIImageRenderingMode 的处理,image 修改渲染色的,即tintColor
- (void)themeSetImageKey:(NSString *)imageKey
renderingMode:(UIImageRenderingMode)mode;

@end

@interface UIButton (ThemeButton)

- (void)themeSetImage:(NSString *)path forState:(UIControlState)state;
- (void)themeSetImage:(NSString *)path forState:(UIControlState)state renderingMode:(UIImageRenderingMode)mode;
- (void)themeSetBackgroundImage:(NSString *)path forState:(UIControlState)state;
- (void)themeSetTitleColor:(NSString *)path forState:(UIControlState)state;
@end

@interface UITableView (ThemeTableView)
@property (nonatomic, copy) NSString *themeSeparatorColor;
@end

@interface CALayer (ThemeLayer)
/// 设置皮肤文件名称 默认为空值,取当前皮肤 eg: @"Dark" / @"Light" ; defaultThemeKey 为默认皮肤
@property (nonatomic, copy) NSString *themeStyle;
@property (nonatomic, copy) NSString *themeBackgroundColor;
@property (nonatomic, copy) NSString *themeBorderColor;
@property (nonatomic, copy) NSString *themeShadowColor;
/// 根据路径获取cgcolor 并缓存方法和参数 ()
- (void)setThemeCGColorWithIvarName:(NSString *)ivarName colorPath:(NSString *)path;
@end

以上是简单列举了几个,其他UIKIt 控件一样分类处理即可

皮肤颜色流程图



皮肤颜色流程图

皮肤图片流程图


皮肤图片流程图

存在的缺陷

1、不能全局统一处理,需要一处一处的设置,比较麻烦。
2、目前还不支持网络下载皮肤功能,需要其他位置处理下载解压过程。
3、XIB的使用还需要其他的处理,这个比较重要

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

收起阅读 »

Python爬虫 | 一条高效的学习路径

数据是创造和决策的原材料,高质量的数据都价值不菲。而利用爬虫,我们可以获取大量的价值数据,经分析可以发挥巨大的价值,比如:豆瓣、知乎:爬取优质答案,筛选出各话题下热门内容,探索用户的舆论导向。淘宝、京东:抓取商品、评论及销量数据,对各种商品及用户的消费场景进行...
继续阅读 »

数据是创造和决策的原材料,高质量的数据都价值不菲。而利用爬虫,我们可以获取大量的价值数据,经分析可以发挥巨大的价值,比如:

豆瓣、知乎:爬取优质答案,筛选出各话题下热门内容,探索用户的舆论导向。

淘宝、京东:抓取商品、评论及销量数据,对各种商品及用户的消费场景进行分析。

搜房、链家:抓取房产买卖及租售信息,分析房价变化趋势、做不同区域的房价分析。

拉勾、智联:爬取各类职位信息,分析各行业人才需求情况及薪资水平。

雪球网:抓取雪球高回报用户的行为,对股票市场进行分析和预测。

爬虫是入门Python最好的方式,没有之一。Python有很多应用的方向,比如后台开发、web开发、科学计算等等,但爬虫对于初学者而言更友好,原理简单,几行代码就能实现基本的爬虫,学习的过程更加平滑,你能体会更大的成就感。

掌握基本的爬虫后,你再去学习Python数据分析、web开发甚至机器学习,都会更得心应手。因为这个过程中,Python基本语法、库的使用,以及如何查找文档你都非常熟悉了。

对于小白来说,爬虫可能是一件非常复杂、技术门槛很高的事情。比如有的人则认为先要掌握网页的知识,遂开始 HTMLCSS,结果入了前端的坑,瘁……

但掌握正确的方法,在短时间内做到能够爬取主流网站的数据,其实非常容易实现,但建议你从一开始就要有一个具体的目标。

在目标的驱动下,你的学习才会更加精准和高效。那些所有你认为必须的前置知识,都是可以在完成目标的过程中学到的。这里给你一条平滑的、零基础快速入门的学习路径。

- ❶ -

学习 Python 包并实现基本的爬虫过程

大部分爬虫都是按“发送请求——获得页面——解析页面——抽取并储存内容”这样的流程来进行,这其实也是模拟了我们使用浏览器获取网页信息的过程。

Python中爬虫相关的包很多:urllib、requests、bs4、scrapy、pyspider 等,建议从requests+Xpath 开始,requests 负责连接网站,返回网页,Xpath 用于解析网页,便于抽取数据。

如果你用过 BeautifulSoup,会发现 Xpath 要省事不少,一层一层检查元素代码的工作,全都省略了。这样下来基本套路都差不多,一般的静态网站根本不在话下,豆瓣、糗事百科、腾讯新闻等基本上都可以上手了

-❷-

掌握各种技巧,应对特殊网站的反爬措施

当然,爬虫过程中也会经历一些绝望啊,比如被网站封IP、比如各种奇怪的验证码、userAgent访问限制、各种动态加载等等。

遇到这些反爬虫的手段,当然还需要一些高级的技巧来应对,常规的比如访问频率控制、使用代理IP池、抓包、验证码的OCR处理等等

往往网站在高效开发和反爬虫之间会偏向前者,这也为爬虫提供了空间,掌握这些应对反爬虫的技巧,绝大部分的网站已经难不到你了。

-❸-

学习 scrapy,搭建工程化的爬虫

掌握前面的技术一般量级的数据和代码基本没有问题了,但是在遇到非常复杂的情况,可能仍然会力不从心,这个时候,强大的 scrapy 框架就非常有用了。

scrapy 是一个功能非常强大的爬虫框架,它不仅能便捷地构建request,还有强大的 selector 能够方便地解析 response,然而它最让人惊喜的还是它超高的性能,让你可以将爬虫工程化、模块化。

学会 scrapy,你可以自己去搭建一些爬虫框架,你就基本具备爬虫工程师的思维了。

- ❹ -

学习数据库基础,应对大规模数据存储

爬回来的数据量小的时候,你可以用文档的形式来存储,一旦数据量大了,这就有点行不通了。所以掌握一种数据库是必须的,学习目前比较主流的 MongoDB 就OK。

MongoDB 可以方便你去存储一些非结构化的数据,比如各种评论的文本,图片的链接等等。你也可以利用PyMongo,更方便地在Python中操作MongoDB。

因为这里要用到的数据库知识其实非常简单,主要是数据如何入库、如何进行提取,在需要的时候再学习就行。

-❺-

分布式爬虫,实现大规模并发采集

爬取基本数据已经不是问题了,你的瓶颈会集中到爬取海量数据的效率。这个时候,相信你会很自然地接触到一个很厉害的名字:分布式爬虫

分布式这个东西,听起来很恐怖,但其实就是利用多线程的原理让多个爬虫同时工作,需要你掌握 Scrapy + MongoDB + Redis 这三种工具

Scrapy 前面我们说过了,用于做基本的页面爬取,MongoDB 用于存储爬取的数据,Redis 则用来存储要爬取的网页队列,也就是任务队列。

所以有些东西看起来很吓人,但其实分解开来,也不过如此。当你能够写分布式的爬虫的时候,那么你可以去尝试打造一些基本的爬虫架构了,实现一些更加自动化的数据获取。

你看,这一条学习路径下来,你已然可以成为老司机了,非常的顺畅。所以在一开始的时候,尽量不要系统地去啃一些东西,找一个实际的项目(开始可以从豆瓣、小猪这种简单的入手),直接开始就好

在这里有一套非常系统的爬虫课程,除了为你提供一条清晰的学习路径,我们甄选了最实用的学习资源以及庞大的主流爬虫案例库。短时间的学习,你就能够很好地掌握 Python 爬虫,获取你想得到的数据,同时具备数据分析、机器学习的Python基础。

如果你希望在短时间内学会Python爬虫,少走弯路

- 高效的学习路径 -

一上来就讲理论、语法、编程语言是非常不合理的,我们会直接从具体的案例入手,通过实际的操作,学习具体的知识点。我们为你规划了一条系统的学习路径,让你不再面对零散的知识点。

说点具体的,比如我们会直接用 lxml+Xpath取代 BeautifulSoup 来进行网页解析,减少你不必要的检查网页元素的操作,多种工具都能完成的,我们会给你最简单的方法,这些看似细节,但可能是很多人都会踩的坑。

《Python爬虫:入门+进阶》大纲

第一章:Python 爬虫入门

1、什么是爬虫

网址构成和翻页机制

网页源码结构及网页请求过程

爬虫的应用及基本原理

2、初识Python爬虫

Python爬虫环境搭建

创建第一个爬虫:爬取百度首页

爬虫三步骤:获取数据、解析数据、保存数据

3、使用Requests爬取豆瓣短评

Requests的安装和基本用法

用Requests爬取豆瓣短评信息

一定要知道的爬虫协议

4、使用Xpath解析豆瓣短评

解析神器Xpath的安装及介绍

Xpath的使用:浏览器复制和手写

实战:用Xpath解析豆瓣短评信息

5、使用pandas保存豆瓣短评数据

pandas的基本用法介绍

pandas文件保存、数据处理

实战:使用pandas保存豆瓣短评数据

6、浏览器抓包及headers设置(案例一:爬取知乎)

爬虫的一般思路:抓取、解析、存储

浏览器抓包获取Ajax加载的数据

设置headers突破反爬虫限制

实战:爬取知乎用户数据

7、数据入库之MongoDB(案例二:爬取拉勾)

MongoDB及RoboMongo的安装和使用

设置等待时间和修改信息头

实战:爬取拉勾职位数据

将数据存储在MongoDB中

补充实战:爬取微博移动端数据

8、Selenium爬取动态网页(案例三:爬取淘宝)

动态网页爬取神器Selenium搭建与使用

分析淘宝商品页面动态信息

实战:用Selenium爬取淘宝网页信息

第二章:Python爬虫之Scrapy框架

1、爬虫工程化及Scrapy框架初窥

html、css、js、数据库、http协议、前后台联动

爬虫进阶的工作流程

Scrapy组件:引擎、调度器、下载中间件、项目管道等

常用的爬虫工具:各种数据库、抓包工具等

2、Scrapy安装及基本使用

Scrapy安装

Scrapy的基本方法和属性

开始第一个Scrapy项目

3、Scrapy选择器的用法

常用选择器:css、xpath、re、pyquery

css的使用方法

xpath的使用方法

re的使用方法

pyquery的使用方法

4、Scrapy的项目管道

Item Pipeline的介绍和作用

Item Pipeline的主要函数

实战举例:将数据写入文件

实战举例:在管道里过滤数据

5、Scrapy的中间件

下载中间件和蜘蛛中间件

下载中间件的三大函数

系统默认提供的中间件

6、Scrapy的Request和Response详解

Request对象基础参数和高级参数

Request对象方法

Response对象参数和方法

Response对象方法的综合利用详解

第三章:Python爬虫进阶操作

1、网络进阶之谷歌浏览器抓包分析

http请求详细分析

网络面板结构

过滤请求的关键字方法

复制、保存和清除网络信息

查看资源发起者和依赖关系

2、数据入库之去重与数据库

数据去重

数据入库MongoDB

第四章:分布式爬虫及实训项目

1、大规模并发采集——分布式爬虫的编写

分布式爬虫介绍

Scrapy分布式爬取原理

Scrapy-Redis的使用

Scrapy分布式部署详解


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

收起阅读 »

什么是爬虫?怎么样玩爬虫

看到上面的那只蜘蛛没?别误会,今天要教你如何玩上面的蜘蛛。我们正式从0到1轻松学会Python爬虫.......知识碎片化学习难度学习特点爬虫的概念网络爬虫(又被称为网页蜘蛛、网页机器人)就是模拟客户端(主要是指浏览器)发送请求,接收请求响应,按照一定规则、自...
继续阅读 »

Python爬虫入门:什么是爬虫?

看到上面的那只蜘蛛没?别误会,今天要教你如何玩上面的蜘蛛。我们正式从0到1轻松学会Python爬虫.......

爬虫特点概要

  • 知识碎片化

爬虫方向的知识是十分碎片化的,因为我们写爬虫的时候会面对各种各样的网站,每个网站实现的技术都是相似的,但是大多数时候还是有差别的,这就要求我们对不同的网站使用不同的技术手段。爬虫并不像在学习web的时候要实现某一功能只要按照一定的套路就能做出来。

  • 学习难度

爬虫的入门相对而言还是要比web简单,但是在后期,爬虫的难度要大于web。难点在于爬虫工程师与运维人员进行对抗,可能你写一个网站的爬虫,结果该网站的运维人员加了反爬的措施,那么作为爬虫工程师就要解决这个反爬。

  • 学习特点

学习爬虫并不像学习web,学习web有一个完整的项目可以练手,因为爬虫的特点,也导致学习爬虫是以某网站为对象的,可以理解为一个技术点一个案例。

爬虫的概念

模拟浏览器,发送请求,获取响应

网络爬虫(又被称为网页蜘蛛、网页机器人)就是模拟客户端(主要是指浏览器)发送请求,接收请求响应,按照一定规则、自动抓取互联网信息的程序。

  • 原则上,只要是浏览器能做的事情,爬虫都能做
  • 爬虫也只能获取浏览器所展示出来的数据

在浏览器中输入百度网址,打开开发者工具,点击network,点击刷新,即可进行抓包。


了解爬虫概念

爬虫的作用

爬虫在互联网中的作用

  • 数据采集
  • 软件测试
  • 网站投票
  • 网络安全

爬虫的分类

根据被爬网闸的数量不同,可以分为:

  • 通用爬虫,如搜索引擎
  • 聚焦爬虫,如12306抢票,或者专门抓取某一网站的某一类数据

根据是否以获取数据为目的,可以分为:

  • 功能性爬虫,给你喜欢的明星,投票点赞
  • 数据增量式爬虫,比如招聘信息

根据URL地址和对应页面内容是否改变,数据增量爬虫可以分为:

  • 基于URL地址变化,内容变化的增量式爬虫
  • URL地址不变,内容变化的数据增量式爬虫

爬虫分类


了解爬虫分类


爬虫流程

1、获取一个URL

2、向URL发送请求,并获取响应(http协议)

3、如果从响应中提取URL,则继续发送请求获取响应

4、如果从响应中获取数据,则数据进行保存


掌握爬虫流程


http以及https的概念和区别

在爬虫流程的第二步,向URL发送请求,那么就要依赖于HTTP/HTTPS协议。

HTTPS比HTTP更安全,但是性能更低

  • HTTP:超文本传输协议,默认端口为80
    。超文本:是指超过文本,不限于文本,可以传输图片、视频、音频等数据
    。传输协议:是指使用公共约定的固定格式来传递转换成字符串的超文本内容
  • HTTPS:HTTP+SSL(安全套接字),即带有安全套接字层的超文本传输协议,默认端口443
    。SSL对传输内容(超文本,也就是请求头和响应体)进行加密
  • 可以打开一个浏览器访问URL,右键检查,点击network,选择一个URL,查看HTTP协议的形式。

掌握http及https的概念和默认端口


爬虫特别注意的请求头

请求头与响应头

http请求形式如上图所示,爬虫要特别关注以下几个请求头字段

  • Content-Type
  • Host
  • Connection
  • Upgrade-Insecure-Requests(升级为https请求)
  • User-Agent(用户代理)
  • Referer
  • Cookie(保持用户状态)
  • Authorization(认证信息)


例如,使用浏览器访问百度进行抓包

当我点击view source的时候,就会出现另外一种格式的请求头,这个是原始的版本,如果没有点击view source的请求头格式是经过浏览器优化的。

爬虫特别注意的响应头

  • set-cookie

cookie是基于服务端生成的,在客户端头信息中,在第一次把请求发送到服务端,服务端生成cookie,存放到客户端,下次发送请求时会带上cookie。

常见的响应状态码

  • 200:成功
  • 302:跳转,新的URL在响应中的Location头中给出
  • 303:浏览器对于post响应进行重定向至新的URL
  • 307:浏览器对于get响应进行重定向至新的URL
  • 403:资源不可用,服务器理解客户端的请求,但拒绝处理它(没有权限)
  • 404:找不到页面
  • 500:服务器内部错误
  • 503:服务器由于维护或者负载过重未能应答。在响应中可能会携带Retry-After响应头,有可能是因为爬虫频繁访问URL,使服务器忽视爬虫的请求,最终返回503状态码

所有的状态码都不可信,一切要以抓包得到的响应中获取的数据为准

network中抓包得到的源码才是判断依据。element中的源码是渲染之后的源码,不能作为判断标准。


了解常见的响应状态码


http请求的过程

1、浏览器在拿到域名对应的IP之后,先向地址栏中的URL发起请求,并获取响应。

2、在返回响应内容(HTML)中,会带有CSS、JS、图片等URL地址,以及Ajax代码,浏览器按照响应内容中的顺序依次发送其他请求,并获取响应。

3、浏览器每获取一个响应就对展示出的结果进行添加(加载),JS、CSS等内容会修改页面内容,JS也可以重新发送请求,获取响应。

4、从获取第一个响应并在浏览器中展示,直到最终获取全部响应,并在展示结果中添加内容或修改,这个过程叫做浏览器的渲染

注意

在爬虫中,爬虫只会请求URL地址,对应的拿到URL地址对应的响应(该响应可以是HTML、CSS 、JS或是是图片、视频等等)。

浏览器渲染出来的页面和爬虫请求抓取的页面很多时候是不一样的,原因是爬虫不具有渲染功能。

  • 浏览器最终展示的结果是由多次请求响应共同渲染的结果
  • 爬虫只对一个URL地址发起请求并得到响应

理解浏览器展示的结果可以是多次请求响应共同渲染的结果,而爬虫是一次请求对应一个响应。

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

收起阅读 »

实时监控股市公告的Python爬虫

精力有限的我们,如何更加有效率地监控信息? 很多时候特别是交易时,我们需要想办法监控一些信息,比如股市的公告。如果现有的软件没有办法实现我们的需求,那么就要靠我们自己动手,才能丰衣足食。你在交易看盘时,如果有一个小窗口,平时默默的不声不响,但是如果有公告发布...
继续阅读 »

精力有限的我们,如何更加有效率地监控信息?

很多时候特别是交易时,我们需要想办法监控一些信息,比如股市的公告。如果现有的软件没有办法实现我们的需求,那么就要靠我们自己动手,才能丰衣足食。

你在交易看盘时,如果有一个小窗口,平时默默的不声不响,但是如果有公告发布,就会显示公告的信息:这是什么公告,然后给我们公告的链接。这样,既不会像弹窗那样用信息轰炸我们,又能够定制我们自己想要的内容,做到想看就看,想不看就不看,那就很方便了。

爬虫抓取的是东方财富上的上市公司公告,上市公司公告有些会在盘中公布。实时监控的原理,其实就是程序代替人工,定期地去刷新网页,然后用刷新前后得到的数据进行比对,如果一样,那么等待下一个周期继续刷新,如果不一样,那么就把增量信息提取出来,供我们查阅。

利用python爬虫实时监控公告信息四部曲

第一步,导入随机请求头和需要的包

我们使用json来解析获取的信息,使用什么方法解析数据取决于我们请求数据的返回形式,这里使用json最方便,我们就导入json包。

第二步,获取初始的公告数据

我们发现,每一个公告都有一个独有的文章号码:art_code,因此我们以这个号码作为新旧比较的基准,如果新页面的头一个公告的art_code和已有的一致,那么就进入下一个刷新周期,如果不一致,那么说明页面已经更新过了,我们提取最新的报告,同时更新这个art_code,用于下一次比对。

原始url的获取。获取之后,通过json解析其中的内容,得到art_code,覆盖写入在tmp.txt文件中,用于比对。

读取了tmp.txt文件中的art_code,跟页面解析的art_code比对。

第三步,获取公告标题和文章链接

通过json我们基本上已经能够解析出大部分的数据内容。

通过观察网站的公告链接的特点,我们发现主要的差别就是在art_code,因此通过网址链接的拼接,我们就能够得到公告的pdf链接。

第四步,运行我们的程序

程序运行的结果会打印到窗口当中,每当有新的公告发布,程序上就会出现一串新的信息。

总结

自此,我们通过程序把我们要的信息打印到了程序的运行窗口,同时,我们的程序也可以根据我们需求进行强化和扩充。首先,这些信息也可以非常方便的通过接口发送到邮箱、钉钉等平台,起到实时提醒的作用,其次,我们也可以从不同的地方抓取信息,完成所需信息的自定义整合,这些将在我们后续的文章中提到。

收起阅读 »

我对 React 实现原理的理解

React 是前端开发每天都用的前端框架,自然要深入掌握它的原理。我用 React 也挺久了,这篇文章就来总结一下我对 react 原理的理解。react 和 vue 都是基于 vdom 的前端框架,我们先聊下 vdom:vdom为什么 react 和 vue...
继续阅读 »

React 是前端开发每天都用的前端框架,自然要深入掌握它的原理。我用 React 也挺久了,这篇文章就来总结一下我对 react 原理的理解。

react 和 vue 都是基于 vdom 的前端框架,我们先聊下 vdom:

vdom

为什么 react 和 vue 都要基于 vdom 呢?直接操作真实 dom 不行么?

考虑下这样的场景:

渲染就是用 dom api 对真实 dom 做增删改,如果已经渲染了一个 dom,后来要更新,那就要遍历它所有的属性,重新设置,比如 id、clasName、onclick 等。

而 dom 的属性是很多的:


有很多属性根本用不到,但在更新时却要跟着重新设置一遍。

能不能只对比我们关心的属性呢?

把这些单独摘出来用 JS 对象表示不就行了?

这就是为什么要有 vdom,是它的第一个好处。

而且有了 vdom 之后,就没有和 dom 强绑定了,可以渲染到别的平台,比如 native、canvas 等等。

这是 vdom 的第二个好处。

我们知道了 vdom 就是用 JS 对象表示最终渲染的 dom 的,比如:

{
   type'div',
   props: {
       id'aaa',
       className: ['bbb''ccc'],
       onClickfunction() {}
  },
   children: []
}

然后用渲染器把它渲染出来。

但是要让开发去写这样的 vdom 么?

那肯定不行,这样太麻烦了,大家熟悉的是 html 那种方式,所以我们要引入编译的手段。

dsl 的编译

dsl 是 domain specific language,领域特定语言的意思,html、css 都是 web 领域的 dsl。

直接写 vdom 太麻烦了,所以前端框架都会设计一套 dsl,然后编译成 render function,执行后产生 vdom。

vue 和 react 都是这样:


这套 dsl 怎么设计呢?

前端领域大家熟悉的描述 dom 的方式是 html,最好的方式自然是也设计成那样。

所以 vue 的 template,react 的 jsx 就都是这么设计的。

vue 的 template compiler 是自己实现的,而 react 的 jsx 的编译器是 babel 实现的,是两个团队合作的结果。

比如我们可以这样写:


编译成 render function 后再执行就是我们需要的 vdom。

接下来渲染器把它渲染出来就行了。

那渲染器怎么渲染 vdom 的呢?

渲染 vdom

渲染 vdom 也就是通过 dom api 增删改 dom。

比如一个 div,那就要 document.createElement 创建元素,然后 setAttribute 设置属性,addEventListener 设置事件监听器。

如果是文本,那就要 document.createTextNode 来创建。

所以说根据 vdom 类型的不同,写个 if else,分别做不同的处理就行了。

没错,不管 vue 还是 react,渲染器里这段 if else 是少不了的:

switch (vdom.tag) {
 case HostComponent:
   // 创建或更新 dom
 case HostText:
   // 创建或更新 dom
 case FunctionComponent
   // 创建或更新 dom
 case ClassComponent
   // 创建或更新 dom
}

react 里是通过 tag 来区分 vdom 类型的,比如 HostComponent 就是元素,HostText 就是文本,FunctionComponent、ClassComponent 就分别是函数组件和类组件。

那么问题来了,组件怎么渲染呢?

这就涉及到组件的原理了:

组件

我们的目标是通过 vdom 描述界面,在 react 里会使用 jsx。

这样的 jsx 有的时候是基于 state 来动态生成的。如何把 state 和 jsx 关联起来呢?

封装成 function、class 或者 option 对象的形式。然后在渲染的时候执行它们拿到 vdom 就行了。

这就是组件的实现原理:

switch (vdom.tag) {
 case FunctionComponent
      const childVdom = vdom.type(props);
      
      render(childVdom);
      //...
 case ClassComponent
    const instance = new vdom.type(props);
    const childVdom = instance.render();
    
    render(childVdom);
    //...
}

如果是函数组件,那就传入 props 执行它,拿到 vdom 之后再递归渲染。

如果是 class 组件,那就创建它的实例对象,调用 render 方法拿到 vdom,然后递归渲染。

所以,大家猜到 vue 的 option 对象的组件描述方式怎么渲染了么?

{
   data: {},
   props: {}
   render(h) {
       return h('div', {}, '');
  }
}

没错,就是执行下 render 方法就行:

const childVdom = option.render();

render(childVdom);

大家可能平时会写单文件组件 sfc 的形式,那个会有专门的编译器,把 template 编译成 render function,然后挂到 option 对象的 render 方法上:


所以组件本质上只是对产生 vdom 的逻辑的封装,函数的形式、option 对象的形式、class 的形式都可以。

就像 vue3 也有了函数组件一样,组件的形式并不重要。

基于 vdom 的前端框架渲染流程都差不多,vue 和 react 很多方面是一样的。但是管理状态的方式不一样,vue 有响应式,而 react 则是 setState 的 api 的方式。

真说起来,vue 和 react 最大的区别就是状态管理方式的区别,因为这个区别导致了后面架构演变方向的不同。

状态管理

react 是通过 setState 的 api 触发状态更新的,更新以后就重新渲染整个 vdom。

而 vue 是通过对状态做代理,get 的时候收集以来,然后修改状态的时候就可以触发对应组件的 render 了。

有的同学可能会问,为什么 react 不直接渲染对应组件呢?

想象一下这个场景:

父组件把它的 setState 函数传递给子组件,子组件调用了它。

这时候更新是子组件触发的,但是要渲染的就只有那个组件么?

明显不是,还有它的父组件。

同理,某个组件更新实际上可能触发任意位置的其他组件更新的。

所以必须重新渲染整个 vdom 才行。

那 vue 为啥可以做到精准的更新变化的组件呢?

因为响应式的代理呀,不管是子组件、父组件、还是其他位置的组件,只要用到了对应的状态,那就会被作为依赖收集起来,状态变化的时候就可以触发它们的 render,不管是组件是在哪里的。

这就是为什么 react 需要重新渲染整个 vdom,而 vue 不用。

这个问题也导致了后来两者架构上逐渐有了差异。

react 架构的演变

react15 的时候,和 vue 的渲染流程还是很像的,都是递归渲染 vdom,增删改 dom 就行。

但是因为状态管理方式的差异逐渐导致了架构的差异。

react 的 setState 会渲染整个 vdom,而一个应用的所有 vdom 可能是很庞大的,计算量就可能很大。

浏览器里 js 计算时间太长是会阻塞渲染的,会占用每一帧的动画、重绘重排的时间,这样动画就会卡顿。

作为一个有追求的前端框架,动画卡顿肯定是不行的。但是因为 setState 的方式只能渲染整个 vdom,所以计算量大是不可避免的。

那能不能把计算量拆分一下,每一帧计算一部分,不要阻塞动画的渲染呢?

顺着这个思路,react 就改造为了 fiber 架构。

fiber 架构

优化的目标是打断计算,分多次进行,但现在递归的渲染是不能打断的,有两个方面的原因导致的:

  • 渲染的时候直接就操作了 dom 了,这时候打断了,那已经更新到 dom 的那部分怎么办?

  • 现在是直接渲染的 vdom,而 vdom 里只有 children 的信息,如果打断了,怎么找到它的父节点呢?

第一个问题的解决还是容易想到的:

渲染的时候不要直接更新到 dom 了,只找到变化的部分,打个增删改的标记,创建好 dom,等全部计算完了一次性更新到 dom 就好了。

所以 react 把渲染流程分为了两部分: render 和 commit。

render 阶段会找到 vdom 中变化的部分,创建 dom,打上增删改的标记,这个叫做 reconcile,调和。

reconcile 是可以打断的,由 schedule 调度。

之后全部计算完了,就一次性更新到 dom,叫做 commit。

这样,react 就把之前的和 vue 很像的递归渲染,改造成了 render(reconcile + schdule) + commit 两个阶段的渲染。

从此以后,react 和 vue 架构上的差异才大了起来。

第二个问题,如何打断以后还能找到父节点、其他兄弟节点呢?

现有的 vdom 是不行的,需要再记录下 parent、silbing 的信息。所以 react 创造了 fiber 的数据结构。


除了 children 信息外,额外多了 sibling、return,分别记录着兄弟节点、父节点的信息。

这个数据结构也叫做 fiber。(fiber 既是一种数据结构,也代表 render + commit 的渲染流程)

react 会先把 vdom 转换成 fiber,再去进行 reconcile,这样就是可打断的了。

为什么这样就可以打断了呢?

因为现在不再是递归,而是循环了:

function workLoop() {
 while (wip) {
   performUnitOfWork();
}

 if (!wip && wipRoot) {
   commitRoot();
}
}

react 里有一个 workLoop 循环,每次循环做一个 fiber 的 reconcile,当前处理的 fiber 会放在 workInProgress 这个全局变量上。

当循环完了,也就是 wip 为空了,那就执行 commit 阶段,把 reconcile 的结果更新到 dom。

每个 fiber 的 reconcile 是根据类型来做的不同处理。当处理完了当前 fiber 节点,就把 wip 指向 sibling、return 来切到下个 fiber 节点。:

function performUnitOfWork() {
 const { tag } = wip;

 switch (tag) {
   case HostComponent:
     updateHostComponent(wip);
     break;

   case FunctionComponent:
     updateFunctionComponent(wip);
     break;

   case ClassComponent:
     updateClassComponent(wip);
     break;
   case Fragment:
     updateFragmentComponent(wip);
     break;
   case HostText:
     updateHostTextComponent(wip);
     break;
   default:
     break;
}

 if (wip.child) {
   wip = wip.child;
   return;
}

 let next = wip;

 while (next) {
   if (next.sibling) {
     wip = next.sibling;
     return;
  }
   next = next.return;
}

 wip = null;
}

函数组件和 class 组件的 reconcile 和之前讲的一样,就是调用 render 拿到 vdom,然后继续处理渲染出的 vdom:

function updateClassComponent(wip) {
 const { typeprops } = wip;
 const instance = new type(props);
 const children = instance.render();

 reconcileChildren(wipchildren);
}

function updateFunctionComponent(wip) {
 renderWithHooks(wip);

 const { typeprops } = wip;

 const children = type(props);
 reconcileChildren(wipchildren);
}

循环执行 reconcile,那每次处理之前判断一下是不是有更高优先级的任务,就能实现打断了。

所以我们在每次处理 fiber 节点的 reconcile 之前,都先调用下 shouldYield 方法:

function workLoop() {
while (wip && shouldYield()) {
performUnitOfWork();
}

if (!wip && wipRoot) {
commitRoot();
}
}

shouldYiled 方法就是判断待处理的任务队列有没有优先级更高的任务,有的话就先处理那边的 fiber,这边的先暂停一下。

这就是 fiber 架构的 reconcile 可以打断的原理。通过 fiber 的数据结构,加上循环处理前每次判断下是否打断来实现的。

聊完了 render 阶段(reconcile + schedule),接下来就进入 commit 阶段了。

前面说过,为了变为可打断的,reconcile 阶段并不会真正操作 dom,只会创建 dom 然后打个 effectTag 的增删改标记。

commit 阶段就根据标记来更新 dom 就可以了。

但是 commit 阶段要再遍历一次 fiber 来查找有 effectTag 的节点,更新 dom 么?

这样当然没问题,但没必要。完全可以在 reconcile 的时候把有 effectTag 的节点收集到一个队列里,然后 commit 阶段直接遍历这个队列就行了。

这个队列叫做 effectList。

react 会在 commit 阶段遍历 effectList,根据 effectTag 来增删改 dom。

dom 创建前后就是 useEffect、useLayoutEffect 还有一些函数组件的生命周期函数执行的时候。

useEffect 被设计成了在 dom 操作前异步调用,useLayoutEffect 是在 dom 操作后同步调用。

为什么这样呢?

因为都要操作 dom 了,这时候如果来了个 effect 同步执行,计算量很大,那不是把 fiber 架构带来的优势有毁了么?

所以 effect 是异步的,不会阻塞渲染。

而 useLayoutEffect,顾名思义是想在这个阶段拿到一些布局信息的,dom 操作完以后就可以了,而且都渲染完了,自然也就可以同步调用了。

实际上 react 把 commit 阶段也分成了 3 个小阶段。

before mutation、mutation、layout。

mutation 就是遍历 effectList 来更新 dom 的。

它的之前就是 before mutation,会异步调度 useEffect 的回调函数。

它之后就是 layout 阶段了,因为这个阶段已经可以拿到布局信息了,会同步调用 useLayoutEffect 的回调函数。而且这个阶段可以拿到新的 dom 节点,还会更新下 ref。

至此,我们对 react 的新架构,render、commit 两大阶段都干了什么就理清了。

总结

react 和 vue 都是基于 vdom 的前端框架,之所以用 vdom 是因为可以精准的对比关心的属性,而且还可以跨平台渲染。

但是开发不会直接写 vdom,而是通过 jsx 这种接近 html 语法的 DSL,编译产生 render function,执行后产生 vdom。

vdom 的渲染就是根据不同的类型来用不同的 dom api 来操作 dom。

渲染组件的时候,如果是函数组件,就执行它拿到 vdom。class 组件就创建实例然后调用 render 方法拿到 vdom。vue 的那种 option 对象的话,就调用 render 方法拿到 vdom。

组件本质上就是对一段 vdom 产生逻辑的封装,函数、class、option 对象甚至其他形式都可以。

react 和 vue 最大的区别在状态管理方式上,vue 是通过响应式,react 是通过 setState 的 api。我觉得这个是最大的区别,因为它导致了后面 react 架构的变更。

react 的 setState 的方式,导致它并不知道哪些组件变了,需要渲染整个 vdom 才行。但是这样计算量又会比较大,会阻塞渲染,导致动画卡顿。

所以 react 后来改造成了 fiber 架构,目标是可打断的计算。

为了这个目标,不能变对比变更新 dom 了,所以把渲染分为了 render 和 commit 两个阶段,render 阶段通过 schedule 调度来进行 reconcile,也就是找到变化的部分,创建 dom,打上增删改的 tag,等全部计算完之后,commit 阶段一次性更新到 dom。

打断之后要找到父节点、兄弟节点,所以 vdom 也被改造成了 fiber 的数据结构,有了 parent、sibling 的信息。

所以 fiber 既指这种链表的数据结构,又指这个 render、commit 的流程。

reconcile 阶段每次处理一个 fiber 节点,处理前会判断下 shouldYield,如果有更高优先级的任务,那就先执行别的。

commit 阶段不用再次遍历 fiber 树,为了优化,react 把有 effectTag 的 fiber 都放到了 effectList 队列中,遍历更新即可。

在dom 操作前,会异步调用 useEffect 的回调函数,异步是因为不能阻塞渲染。

在 dom 操作之后,会同步调用 useLayoutEffect 的回调函数,并且更新 ref。

所以,commit 阶段又分成了 before mutation、mutation、layout 这三个小阶段,就对应上面说的那三部分。

我觉得理解了 vdom、jsx、组件本质、fiber、render(reconcile + schedule) + commit(before mutation、mutation、layout)的渲染流程,就算是对 react 原理有一个比较深的理解了。


作者:zxg_神说要有光
来源:juejin.cn/post/7117051812540055588

收起阅读 »

Flutter中的异步

同步与异步程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对于用...
继续阅读 »

同步与异步

程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对于用户层面来说,我们可以选择stop the world,等待操作完成返回结果后再继续操作,也可以选择继续去执行其他操作,等事件返回结果后再通知回来。这就是从用户角度来看的同步与异步。

从操作系统的角度,同步异步,与任务调度,进程间切换,中断,系统调用之间有着更为复杂的关系。

同步I/O 与 异步I/O的区别


为什么使用异步

用户可以阻塞式的等待,因为人的操作和计算机相比是非常慢的,计算机如果阻塞那就是很大的性能浪费了,异步操作让您的程序在等待另一个操作的同时完成工作。三种异步操作的场景:

  • I/O操作:例如:发起一个网络请求,读写数据库、读写文件、打印文档等,一个同步的程序去执行这些操作,将导致程序的停止,直到操作完成。更有效的程序会改为在操作挂起时去执行其他操作,假设您有一个程序读取一些用户输入,进行一些计算,然后通过电子邮件发送结果。发送电子邮件时,您必须向网络发送一些数据,然后等待接收服务器响应。等待服务器响应所投入的时间是浪费的时间,如果程序继续计算,这将得到更好的利用

  • 并行执行多个操作:当您需要并行执行不同的操作时,例如进行数据库调用、Web 服务调用以及任何计算,那么我们可以使用异步

  • 长时间运行的基于事件驱动的请求:这就是您有一个请求进来的想法,并且该请求进入休眠状态一段时间等待其他一些事件的发生。当该事件发生时,您希望请求继续,然后向客户端发送响应。所以在这种情况下,当请求进来时,线程被分配给该请求,当请求进入睡眠状态时,线程被发送回线程池,当任务完成时,它生成事件并从线程池中选择一个线程发送响应

计算机中异步的实现方式就是任务调度,也就是进程的切换

任务调度采用的是时间片轮转的抢占式调度方式,进程是任务调度的最小单位。

计算机系统分为用户空间内核空间,用户进程在用户空间,操作系统运行在内核空间,内核空间的数据访问修改拥有高于普通进程的权限,用户进程之间相互独立,内存不共享,保证操作系统的运行安全。如何最大化的利用CPU,确定某一时刻哪个进程拥有CPU资源就是任务调度的过程。内核负责调度管理用户进程,以下为进程调度过程


在任意时刻, 一个 CPU 核心上(processor)只可能运行一个进程

每一个进程可以包含多个线程,线程是执行操作的最小单元,因此进程的切换落实到具体细节就是正在执行线程的切换

Future

Future<T> 表示一个异步的操作结果,用来表示一个延迟的计算,返回一个结果或者error,使用代码实例:

Future<int> future = getFuture();
future.then((value) => handleValue(value))
    .catchError((error) => handleError(error))
.whenComplete(func);

future可以是三种状态:未完成的返回结果值返回异常

当一个返回future对象被调用时,会发生两件事:

  • 将函数操作入队列等待执行结果并返回一个未完成的Future对象

  • 函数操作完成时,Future对象变为完成并携带一个值或一个错误

首先,Flutter事件处理模型为先执行main函数,完成后检查执行微任务队列Microtask Queue中事件,最后执行事件队列Event Queue中的事件,示例:

void main(){
 Future(() => print(10));
Future.microtask(() => print(9));
 print("main");
}
/// 打印结果为:
/// main
/// 9
/// 10

基于以上事件模型的基础上,看下Future提供的几种构造函数,其中最基本的为直接传入一个Function

factory Future(FutureOr<T> computation()) {
   _Future<T> result = new _Future<T>();
   Timer.run(() {
     try {
       result._complete(computation());
    } catch (e, s) {
       _completeWithErrorCallback(result, e, s);
    }
  });
   return result;
}

Function有多种写法:

//简单操作,单步
Future(() => print(5));
//稍复杂,匿名函数
Future((){
 print(6);
});
//更多操作,方法名
Future(printSeven);

printSeven(){
 print(7);
}
 

Future.microtask

此工程方法创建的事件将发送到微任务队列Microtask Queue,具有相比事件队列Event Queue优先执行的特点

factory Future.microtask(FutureOr<T> computation()) {
   _Future<T> result = new _Future<T>();
//
   scheduleMicrotask(() {
     try {
       result._complete(computation());
    } catch (e, s) {
       _completeWithErrorCallback(result, e, s);
    }
  });
   return result;
}

Future.sync

返回一个立即执行传入参数的Future,可理解为同步调用

factory Future.sync(FutureOr<T> computation()) {
   try {
     var result = computation();
     if (result is Future<T>) {
       return result;
    } else {
       // TODO(40014): Remove cast when type promotion works.
       return new _Future<T>.value(result as dynamic);
    }
  } catch (error, stackTrace) {
     /// ...
  }
}
Future.microtask(() => print(9));
 Future(() => print(10));
 Future.sync(() => print(11));

/// 打印结果: 11、9、10

Future.value

创建一个将来包含value的future

factory Future.value([FutureOr<T>? value]) {
   return new _Future<T>.immediate(value == null ? value as T : value);
}

参数FutureOr含义为T value 和 Future value 的合集,因为对于一个Future参数来说,他的结果可能为value或者是Future,所以对于以下两种写法均合法:

    Future.value(12).then((value) => print(value));
 Future.value(Future<int>((){
   return 13;
}));

这里需要注意即使value接收的是12,仍然会将事件发送到Event队列等待执行,但是相对其他Future事件执行顺序会提前

Future.error

创建一个执行结果为error的future

factory Future.error(Object error, [StackTrace? stackTrace]) {
/// ...
return new _Future<T>.immediateError(error, stackTrace);
}

_Future.immediateError(var error, StackTrace stackTrace)
: _zone = Zone._current {
_asyncCompleteError(error, stackTrace);
}
Future.error(new Exception("err msg"))
.then((value) => print("err value: $value"))
.catchError((e) => print(e));

/// 执行结果为:Exception: err msg

Future.delayed

创建一个延迟执行回调的future,内部实现为Timer加延时执行一个Future

factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
/// ...
new Timer(duration, () {
if (computation == null) {
result._complete(null as T);
} else {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
}
});
return result;
}

Future.wait

等待多个Future并收集返回结果

static Future<List<T>> wait<T>(Iterable<Future<T>> futures,
{bool eagerError = false, void cleanUp(T successValue)?}) {
/// ...
}

FutureBuilder结合使用:

child: FutureBuilder(
future: Future.wait([
firstFuture(),
secondFuture()
]),
builder: (context,snapshot){
if(!snapshot.hasData){
return CircularProgressIndicator();
}
final first = snapshot.data[0];
final second = snapshot.data[1];
return Text("data $first $second");
},
),

Future.any

返回futures集合中第一个返回结果的值

static Future<T> any<T>(Iterable<Future<T>> futures) {
var completer = new Completer<T>.sync();
void onValue(T value) {
if (!completer.isCompleted) completer.complete(value);
}
void onError(Object error, StackTrace stack) {
if (!completer.isCompleted) completer.completeError(error, stack);
}
for (var future in futures) {
future.then(onValue, onError: onError);
}
return completer.future;
}

对上述例子来说,Future.any snapshot.data 将返回firstFuturesecondFuture中第一个返回结果的值

Future.forEach

为传入的每一个元素,顺序执行一个action

static Future forEach<T>(Iterable<T> elements, FutureOr action(T element)) {
var iterator = elements.iterator;
return doWhile(() {
if (!iterator.moveNext()) return false;
var result = action(iterator.current);
if (result is Future) return result.then(_kTrue);
return true;
});
}

这里边action是方法作为参数,头一次见这种形式语法还是在js中,当时就迷惑了很大一会儿,使用示例:

Future.forEach(["one","two","three"], (element) {
print(element);
});

Future.doWhile

执行一个操作直到返回false

Future.doWhile((){
for(var i=0;i<5;i++){
print("i => $i");
if(i >= 3){
return false;
}
}
return true;
});
/// 结果打印到 3

以上为Future中常用构造函数和方法

在Widget中使用Future

Flutter提供了配合Future显示的组件FutureBuilder,使用也很简单,伪代码如下:

child: FutureBuilder(
future: getFuture(),
builder: (context, snapshot){
if(!snapshot.hasData){
return CircularProgressIndicator();
} else if(snapshot.hasError){
return _ErrorWidget("Error: ${snapshot.error}");
} else {
return _ContentWidget("Result: ${snapshot.data}")
}
}
)

Async-await

使用

这两个关键字提供了异步方法的同步书写方式,Future提供了方便的链式调用使用方式,但是不太直观,而且大量的回调嵌套造成可阅读性差。因此,现在很多语言都引入了await-async语法,学习他们的使用方式是很有必要的。

两条基本原则:

  • 定义一个异步方法,必须在方法体前声明 async

  • await关键字必须在async方法中使用

首先,在要执行耗时操作的方法体前增加async:

void main() async { ··· }

然后,根据方法的返回类型添加Future修饰

Future<void> main() async { ··· }

现在就可以使用await关键字来等待这个future执行完毕

print(await createOrderMessage());

例如实现一个由一级分类获取二级分类,二级分类获取详情的需求,使用链式调用的代码如下:

var list = getCategoryList();
list.then((value) => value[0].getCategorySubList(value[0].id))
.then((subCategoryList){
var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
print(courseList);
}).catchError((e) => (){
print(e);
});

现在来看下使用async/await,事情变得简单了多少

Future<void> main() async {
await getCourses().catchError((e){
print(e);
});
}
Future<void> getCourses() async {
var list = await getCategoryList();
var subCategoryList = await list[0].getCategorySubList(list[0].id);
var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
print(courseList);
}

可以看到这样更加直观

缺陷

async/await 非常方便,但是还是有一些缺点需要注意

因为它的代码看起来是同步的,所以是会阻塞后面的代码执行,直到await返回结果,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但后边自己的代码被阻塞。

这意味着代码可能会由于有大量await代码相继执行而阻塞,本来用Future编写表示并行的操作,现在使用await变成了串行,例如,首页有一个同时获取轮播接口,tab列表接口,msg列表接口的需求

Future<String> getBannerList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "banner list";
});
}

Future<String> getHomeTabList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "tab list";
});
}

Future<String> getHomeMsgList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "msg list";
});
}

使用await编写很可能会写成这样,打印执行操作的时间

Future<void> main2() async {
var startTime = DateTime.now().second;
await getBannerList();
await getHomeTabList();
await getHomeMsgList();
var endTime = DateTime.now().second;
print(endTime - startTime); // 9
}

在这里,我们直接等待所有三个模拟接口的调用,使每个调用3s。后续的每一个都被迫等到上一个完成, 最后会看到总运行时间为9s,而实际我们想三个请求同时执行,代码可以改成如下这种:

Future<void> main() async {
var startTime = DateTime.now().second;
var bannerList = getBannerList();
var homeTabList = getHomeTabList();
var homeMsgList = getHomeMsgList();

await bannerList;
await homeTabList;
await homeMsgList;
var endTime = DateTime.now().second;
print(endTime - startTime); // 3
}

将三个Future存储在变量中,这样可以同时启动,最后打印时间仅为3s,所以在编写代码时,我们必须牢记这点,避免性能损耗。

原理

线程模型

当一个Flutter应用或者Flutter Engine启动时,它会启动(或者从池中选择)另外三个线程,这些线程有些时候会有重合的工作点,但是通常,它们被称为UI线程GPU线程IO线程。需要注意一点这个UI线程并不是程序运行的主线程,或者说和其他平台上的主线程理解不同,通常的,Flutter将平台的主线程叫做"Platform thread"


UI线程是所有的Dard代码运行的地方,例如framework和你的应用,除非你启动自己的isolates,否则Dart将永远不会运行在其他线程。平台线程是所有依赖插件的代码运行的地方。该线程也是native frameworks为其他任务提供服务的地方,一般来说,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个Platform thread为其提供服务。跟Flutter Engine的所有交互(接口调用)必须发生在Platform Thread,试图在其它线程中调用Flutter Engine会导致无法预期的异常。这跟Android/iOS UI相关的操作都必须在主线程进行相类似。

Isolates是Dart中概念,本意是隔离,它的实现功能和thread类似,但是他们之间的实现又有着本质的区别,Isolote是独立的工作者,它们之间不共享内存,而是通过channel传递消息。Dart是单线程执行代码,Isolate提供了Dart应用可以更好的利用多核硬件的解决方案。

事件循环

单线程模型中主要就是在维护着一个事件循环(Event Loop) 与 两个队列(event queue和microtask queue)当Flutter项目程序触发如点击事件IO事件网络事件时,它们就会被加入到eventLoop中,eventLoop一直在循环之中,当主线程发现事件队列不为空时发现,就会取出事件,并且执行。

microtask queue中事件优先于event queue执行,当有任务发送到microtask队列时,会在当前event执行完成后,阻塞当前event queue转而去执行microtask queue中的事件,这样为Dart提供了任务插队的解决方案。

event queue的阻塞意味着app无法进行UI绘制,响应鼠标和I/O等事件,所以要谨慎使用,如下为流程图:


这两个任务队列中的任务切换在某些方面就相当于是协程调度机制

协程

协程是一种协作式的任务调度机制,区别于操作系统的抢占式任务调度机制,它是用户态下面的,避免线程切换的内核态、用户态转换的性能开销。它让调用者自己来决定什么时候让出cpu,比操作系统的抢占式调度所需要的时间代价要小很多,后者为了恢复现场会保存相当多的状态(不仅包括进程上下文的虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态),并且会频繁的切换,以现在流行的大多数Linux机器来说,每一次的上下文切换要消耗大约1.2-1.5μs的时间,这是仅考虑直接成本,固定在单个核心以避免迁移的成本,未固定情况下,切换时间可达2.2μs


对cpu来说这算一个很长的时间吗,一个很好的比较是memcpy,在相同的机器上,完成一个64KiB数据的拷贝需要3μs的时间,上下文的切换比这个操作稍微快一些


协程和线程非常相似,是从异步执行任务的角度来看,而并不是从设计的实体角度像进程->线程->协程这样类似于细胞->原子核->质子中子这样的关系。可以理解为线程上执行的一段函数,用yield完成异步请求、注册回调/通知器、保存状态,挂起控制流、收到回调/通知、恢复状态、恢复控制流的所有过程

多线程执行任务模型如图:


线程的阻塞要靠系统间进程的切换,完成逻辑流的执行,频繁的切换耗费大量资源,而且逻辑流的执行数量严重依赖于程序申请到的线程的数量。

协程是协同多任务的,这意味着协程提供并发性但不提供并行性,执行流模型图如下:


协程可以用逻辑流的顺序去写控制流,协程的等待会主动释放cpu,避免了线程切换之间的等待时间,有更好的性能,逻辑流的代码编写和理解上也简单的很多

但是线程并不是一无是处,抢占式线程调度器事实上提供了准实时的体验。例如Timer,虽然不能确保在时间到达的时候一定能够分到时间片运行,但不会像协程一样万一没有人让出时间片就永远得不到运行……

总结

  • 同步与异步

  • Future提供了Flutter中异步代码链式编写方式

  • async-wait提供了异步代码的同步书写方式

  • Future的常用方法和FutureBuilder编写UI

  • Flutter中线程模型,四个线程

  • 单线程语言的事件驱动模型

  • 进程间切换和协程对比

参考

dart.cn/tutorials/l…

dart.cn/codelabs/as…

medium.com/dartlang/da…

juejin.cn/post/684490…

developer.mozilla.org/en-US/docs/…

http://www.zhihu.com/question/19…

http://www.zhihu.com/question/50…

en.wikipedia.org/wiki/Asynch…

eli.thegreenplace.net/2018/measur…


作者:QiShare
来源:juejin.cn/post/6987637272375984165

收起阅读 »

计算机会是下一个土木吗?

最近互联网裁员,有网友热议:2022年大规模裁员后,计算机专业会不会成为下一个土木?今年是我进入这个行业的第 10 年,算是抓住了这个行业的红利期,不用依靠家里也可以在一线城市买房、成家立业。但反观现在,“被毕业、逃离互联网、躺平算了...”却成了这个行业的主...
继续阅读 »

最近互联网裁员,有网友热议:2022年大规模裁员后,计算机专业会不会成为下一个土木?

今年是我进入这个行业的第 10 年,算是抓住了这个行业的红利期,不用依靠家里也可以在一线城市买房、成家立业。

但反观现在,“被毕业、逃离互联网、躺平算了...”却成了这个行业的主旋律,身边陆续有一些同事润到国企和外企去了,放低了对工资的预期,转而追求稳定和平衡。

互联网行业真的不行了吗?未来计算机专业会怎么样?真的会变成“土木专业”吗?

有个匿名网友写了这样一个回答,我挺认同的,想跟大家分享一下。


我学了10年计算机,现在还在找工作,我爸干了一辈子土木现在也在找工作。我觉得计算机不会成为下一个土木。

至于是不是49年入国军,我觉得楼主的眼光可以看长远一点,就是你这辈子到底想从事什么职业,或者干个什么事情。这篇相当于回忆录,供大家参考。

我2012年高考完了那年选专业还十分纠结,我记得那年最好的专业是金融,其次是建筑、土木、医生这些。生物、化学这些更次。

但这些分都还比计算机高点或者差不多,我爸就说让我自己想,学什么都可以。

我爸倒是觉得干土木也不错,但是我高考太差了,国内最好的那几个土木专业都报不进去。

一个18岁的无知少年,对专业选择能有什么想法呢。当时可以填5个专业,我前面几个都是填的金融管理这些热门专业,我最后填的计算机。

为什么填计算机呢,因为我喜欢打游戏,我觉得学计算机的应该可以去编游戏,那我也挺喜欢的。最后,金融那么火,所以前面的都没录上,调剂到计算机。

一、中国互联网的黄金十年

从11,12年到最近这一两年,我想称之为中国互联网的黄金十年。

如果进入社会就恰好在这黄金十年的开端,你可能根本意识不到有些精彩和癫狂只是短暂的。

我上本科那会儿,你以为大家是想着去哪工作吗?不是的,我们很多同学都在想着怎么创业,有技术追求的都在想着怎么造轮子,或者出国读博士搞学术,只有躺平的人才想着本科毕业找工作。

从大一开始就会有VC到学校劝人创业。那个年代滴滴和快滴还没有合并,美团也没有外卖,知乎过百赞都算非常热门的回答,短视频和直播更是连网速基础都还不具备。

你能做出一个app 雏形,哪怕之后怎么赚钱都不知道,你都能拿到投资。所以有同学真的就拿着种子轮出去创业了。

更有专业课老师原话:“你们这些搞计算机的,就是年轻的时候要想着几年赚个几百万几千万,把这辈子钱全赚够。” 可见当时的空气有多浮躁。

工作很难找吗?你能想象有公司给大二的学生全职吗,有些同学课不上,出去工作,只来考试和交作业。

我算是学的很渣的,因为我上大学一直在搞乱七八糟的东西,平均分不到80。好在我游戏打的多且好(多款游戏全区前10),面试反转链表都写的磕磕巴巴的,毕业依然进了某游戏大厂。

我们本科毕业那年单说就业的人,工作最好的去了google,微软,微信总部,阿里核心电商业务,去腾讯当产品经理,或者知名PE,VC。

对!你没有看错,互联网全是热钱,搞投资的也需要招懂点技术的啊。技术学的不好怎么办呢,转产品经理呀,或者游戏策划,再不济运维测试。

反正最后怎么都可以找到个工作。那时候工资最低的是去四大行总部做开发,但人家通常朝九晚五,有编制啊。

毕业我去游戏公司工作了1-2年,感觉做的都是换皮游戏,和我理想中的游戏开发差距太大了,就去美国继续读书了。

那时候工作一年就有猎头打你电话让你跳槽了。这有可能是我人生中的一个错误决定。出国之后发现工作越来越难找,中国和美国都难,面试越来越难。

头一年大家可能一般考个medium难度的题目,好家伙,第二年可能就直接变全考hard。好不容易找到工作,结果疫情一来offer直接取消了。

本来国内公司也面了一下,给了offer但是一看工资,和之前猎头说的工资多不了多少。合着回国的话硕士两年相当于白读。于是不服,转了博士继续读。

就算到了博士工作还是不好找,因为适合博士的岗位更少。面试机器学习要问,论文要聊,之前的实习经验会问,最后还要考leetcode hard,拜托我只是面个实习好吗。要知道8-6年前面试都只有easy,转专业刷50道题进美国大厂的人大有人在。

我还认识一个年纪比我大很多的博士,他说10年左右那会儿,你要是学校招聘会去了Amazon的摊位,交了简历,那就可以当场拿到一个offer。看来美国也有就业市场十分疯狂的年代。

归根结底,还是因为移动互联网的红利消失。没有那么多业务扩张,不会每年扩招。工资依然高,但进入的门槛也越来越高。有逐渐精英化的趋势。

也不奇怪,早几年不是就已经说中国互联网已经进入了下半场,对我们这些打工人来说意思就是变的更卷了呗。

二、聊一聊土木

接下来说一说土木。我爸工作那会儿是80年代末,大学毕业分配去了铁路某局。那时候土木也不算最热门的。

80年代末,90年代初最热的是下海经商。那会儿好多人从体制内出来经商。

我爸当时比较怂,我爷爷奶奶都是农民,家里没有本钱和关系。另一方面铁路系统工作也是个香饽饽,看病铁路医院,学校铁路小学,吃饭单位食堂,基本上不花钱。

我爸说小时候饿怕了,他觉得改革开放中国接下来几十年会修很多房子,很多路桥,所以他学土木完全是为了保证以后能长期吃上饭。

他这话倒是没说错,从90年代初到20年,中国一直在大修基建和住房。直到最近房地产房子才真的卖不太动,高铁和高速也修的足够多了。但我爸并没有干30年,他40多岁就退休了,在单位挂职。

中国土木最火的年代应该是2000年后到2015年左右,尤其是08年的四万亿,那时候很多包工头,建材行业的老板真的是在地上捡钱。但整个黄金年代差不多也就10年出头。

还是以我爸为例,我上小学那年,我爸嫌弃铁路系统给钱少,跳到上海一个建筑企业管工程,3年之后居然在上海全款买房了。

论买房速度,当年土木赚钱可能比现在互联网还多点。然后又干了几年在二线城市也买了两套房子,和一个商铺,我爸就辞职不干了。

因为他根本不喜欢土木,他喜欢炒股和种菜。结果呢炒股这么多年了也没赚几个钱。反倒是土木行情不好,挂靠那公司不给他挂靠了,但是他又还没有到领退休金的年纪,还要找个公司交社保。

所以他又开始找工作,可能也是因为太闲了。但是根本没有公司要他,现在年轻人都找不到工作,谁会要一个快60的老头呢?

但是土木就业市场也有癫狂的年代呀,我爸的原话是2010年左右,“什么阿猫阿狗都能来工作”,你只要是学土木,专科都可以。

现在土木凉了,简单来说是国内建设搞的差不多了。美国也有基建浪潮,但是那一波过后,就不再需要那么多人了,现在美国土木工程师也不算高收入群体。

所以我算是睥见了两个行业的黄金的年代。现代社会发展变化极快,一个行业要是能赚钱可以在3年之内迅速内卷。10年可能一整个周期就过了。

三、计算机到底怎么样?

所以你问要不要转计算机?我想说你喜欢编程吗?你喜欢用你的技术去解决问题吗?而不仅仅是因为赚钱,因为可能最好的日子也就那几年,但你需要工作40年(按法定退休年龄65)。

大多数人都不能洞见未来,当初12年我说自己录了计算机专业,很多长辈还说你那个估计和培训班出来的工资差不多。

谁又能知道,12年微信只用了几个月就成了国民APP,大家瞬间就进入了移动互联网时代呢?谁又能知道几年之后浪潮就已经过了呢?

所以现在这个时间节点我认为计算机只适合喜欢的人。

计算机我认为还是很好就业的,只是前几年找工作太容易。转专业,学两天java/python也没什么实际项目经历就可以找到高薪工作。

世界怎么可能一直如此美好?但是你要是有个正儿八经的计算机学位,上学期间认真做了些项目或者有实习,找工作应该不难。前提是不要往头部大厂算法岗位去卷。

至于裁员,你放心,你真要有技术,绝不可能裁员到你头上。

我原来做游戏,一个组十几个人。一个主程,一个引擎程序员(看着40岁以上,头发都半白了),带着我们这些刚毕业1-2年新人做开发。

整组平时有问题都是找他们两个。我相信就算裁员,也不会裁他们两个。就算公司倒闭,猎头第二天就会打来电话。

裁的会是光写简单业务逻辑,既不能做架构,也没有在某一块有足够技术深度的人。因为可替代性太高了。

实际上就算在美国,FB几年之内不能升资深工程师就会被裁,Amazon好像每年固定开除绩效末尾6%,有些公司更高到10%。

为什么这些公司敢这么做?因为这些人可替代性太高了,招个毕业生培训一下就和他们干一样的活。

四、计算机依然是最好的专业之一

但是学计算机,做软件工程师依然是接下来几十年最好的工作之一。

虽然上一个黄金时代已经过去,但是白银时代也香啊。说不定下一轮技术革命来到(元宇宙,通用AI,脑机接口,生物信息等等,太多了,都可以产生新的红利)大家又都进入了黄金时代。

现在这个时间节点卷cs的性价比依然较高。能和cs比工资的基本上也就投行,药厂,半导体,律师,医生可能都还差了一点。

药厂律师医生这些哪个不要博士毕业?进美国法学院医学院比其他专业都难,中国医学法律高考分也不低吧。半导体行业总体收入还是比软件工程师少点吧。而且人家也很难啊,做实验要扛几十斤的示波器,焊板子一坐就是一天,人家也要编程修bug。

至于投行,那必须要是名校毕业,各种社会活动,本科期间多个实习,或者家里有关系本来就不缺钱,甚至就我观察还要长得帅或者比较漂亮。

至于cs,你只要卷出一个本科/硕士学位,和面试刷题这两关。职业生涯初期拿的更多。

医生律师大后期会比较厉害,但是搞计算机也一样啊,走技术路线不说卷到60岁,卷到45+岁没问题的。主任工程师,资深科学家收入不比主任医师差。

再者,工作多年后,收入很大一部分是投资性收入。这些个体差异更大,而不是行业差异。

最后说回土木,土木怎么就不好了呢?去非洲一年也好几十万呢。人除了为了事业(钱)而奋斗,更重要的难道不是为了理想吗?

来源:知乎

收起阅读 »

“𠈌”计划2月优秀环友表彰及3月获选标准

环信“𠈌”计划(https://www.imgeek.org/article/825360062),以传递人人为我,我为人人的开发者互助精神为目标,将程序员自由开放和共享精神发扬光大。每月结束后由社区综合评选出当月积极帮助他人或参与环信社区建设的优秀开发者,优...
继续阅读 »

环信“𠈌”计划https://www.imgeek.org/article/825360062,以传递人人为我,我为人人的开发者互助精神为目标,将程序员自由开放和共享精神发扬光大。每月结束后由社区综合评选出当月积极帮助他人或参与环信社区建设的优秀开发者,优秀环友墙上留名并奖励环信大礼包,下面让我们康康2月的优秀环友们吧~

上帝之眼
王二蛋和他的张大花
孤狼☞小九

山雾sys

待补充。。。

恭喜以上开发者,请查收并回复站内私信,领取相应礼包。


“𠈌”计划3月礼包发放标准:
1、社区/社群活跃用户,积极参与社区活动 --2-5人
2、IMGeek发文章2篇原创或5篇转载 不限人数
3、反馈IM SDK bug并技术确认 不限人数

3月徽章👇


欢迎环信资深用户加入开发者交流群

加好友备注“社区”



往期回顾:

4月优秀环友:https://www.imgeek.org/article/825360198

5月优秀环友:https://www.imgeek.org/question/472968

2023年1月优秀环友

上帝之眼
王二蛋和他的张大花
孤狼☞小九
新新人类
一诗一画一菩提
林鹏
nikewei
willpein
LDFeng
emojiiii
yoyobiubiu
shiyl
yfliu
Fire_raiN

环信“𠈌”计划11月优秀环友如下:

上帝之眼
王二蛋和他的张大花
emojiiii
雷厉风行•琛
加长挡泥板
JERRY_694
孤狼☞小九
bilierha
HW1
fushixin

环信“𠈌”计划10月优秀环友如下:

xugj
马师傅
九漏鱼
暖光
hongg
冯小姐
conanma
Hc.
tjss
上帝之眼
月兑さん
山雾
Tsj
redme
孤狼☞小九

环信“𠈌”计划9月优秀环友如下:

山雾
孤狼☞小九
阿城627236
大锤子
微信用户_657
hongg
恋雨
mg13209643545
容颜难忘
bilierha
殷离恨
诺一啦
努力奋斗
lsp1007
马师傅
我要打中单
九漏鱼
微信用户_119
张尚斌
行走的商人
加菲猫
xugj
little28
Ju独一 -
上帝之眼
暗夜公爵
Empty_353
查南
诺一
ByteDance
万众一心摆烂到底
不开心就大概
依旧
微信用户_297
wilpein
微信用户_43
安和桥
魂断兴哥

环信“𠈌”计划8月优秀环友如下:

容颜难忘

山雾

孤狼☞小九

上帝之眼

阿城627236

柳天明

xugj

美国队长

Jiayun

Hc.

查南哥

查南

李全喜

yangjian

conanma

马师傅
王二蛋和他的张大花
雨淋湿了天空


环信“𠈌”计划7月优秀环友如下:

内容组:conanma、王二蛋和他的张大花、上帝之眼、环Sir、思密达、Hc.、little28、马师傅、sweetloser、柳天明
社群组:孤狼☞小九


环信“𠈌”计划6月优秀环友如下:

内容组:conanma、环Sir、上帝之眼、王二蛋和他的张大花、马师傅、Hc.、栈狮的咆哮

社群组:孤狼☞小九
收起阅读 »

Android组件化思路引文

前言本文并不是具体的实现文,主要讨论组件化的思路,分模块 和 模块通信。并实践一个简单的路由组件,完成模块间页面跳转和通信这两个功能,意图在于帮助刚接触组件化的同学提供一个简单不复杂的思路,对组件化有感性认识快速入门具体的实践文,可看这篇:Android 组件...
继续阅读 »

前言

本文并不是具体的实现文,主要讨论组件化的思路,分模块 和 模块通信。并实践一个简单的路由组件,完成模块间页面跳转和通信这两个功能,意图在于帮助刚接触组件化的同学提供一个简单不复杂的思路,对组件化有感性认识快速入门

具体的实践文,可看这篇:Android 组件化最佳实践 - 掘金 (juejin.cn)

什么是组件化

组件化本质上是一种组织代码的方式,只不过它的粒度更大,以module为单位。在未使用组件化之前,所有的代码都放在app模块中,在app模块内部通过分包划分业务代码和功能代码

如下图所示,

根据业务划分三个包:

  1. find 发现
  2. home 首页
  3. shop 商城

根据功能划分两个包:

  1. http 网络请求
  2. utils 工具类

Untitled.png 上述是未使用组件化情况,所有代码都在一个模块中编写,这样做并没有什么问题,但是当项目代码越来越多的 或者 有多人参数到项目中就有很大的问题了,比如:

  1. 代码都在写在一个模块中,不论怎么细致的分包,都免不了一个包下出现10多个类甚至更多的情况
  2. 分包的形式几乎对代码没有约束
  3. 开发人员多了,代码都写在一个模块中,每一位开发都拥有对文件读写的权利,容易出现代码覆盖冲突问题

总之组件化是为了应对代码多,人多 或者代码和人都多的情况而使用的一种组织代码的方式,一个模块中的代码分散到多个模块中。由于代码不在一个模块中,会出现到A模块无法引用到B模块中的类,引出通信问题。

所以组件化面对的主要问题主要有两个:

  1. 分模块
  2. 模块间通信

分模块

模块依据什么划分呢? 四个大字:单一职责 。老实说,写代码的时候 能够时刻牢记 单一职责,就能写出很不错的代码了。

拆分巨型单模块 与 拆分巨型单一类 的思想都是一致的。 其实它们出现的原因也一致,把不同职责的代码都放到一个类/模块中。所以拆分代码可以理解为代码归类

代码大致可以分为业务代码 和 功能代码,比如:

  1. 首页属于业务,网络请求属于功能
  2. 商城属于业务,数据库属于功能

所以当你的项目计划进行模块化的时候,只需要根据项目实际情况划分即可,没有什么硬性规定。

拆分代码有两个好处:

  1. 高复用性
    1. 体现功能模块上,比如:网络请求,轮播图,播放器,支付,分享等功能,任何一个业务都能可能会使用。实现为一个单独的模块,哪里使用哪里引入。
  2. 代码隔离
    1. 体现在业务模块,比如:A模块实现商城,B模块实现文章论坛,两者绝大部分代码没有任何关联,独立存在。
    2. 假设有一天项目不做文章论坛了,业务直接砍掉。那么删除B模块即可,A模块不受任何影响
    3. 但A,B模块都有可能有到分享功能,所以分享作为功能模块出现,不包含任何业务,只提供分享功能。

经过划分模块的代码结构如图

Untitled 1.png

三个业务模块:

  1. module_find 发现
  2. module_home 首页
  3. module_shop 商城

两个功能模块:

  1. library_network 网络请求
  2. library_utils 工具类

总之代码模块的拆分是单一职责的体现,大概可以分为业务模块和功能模块两种,功能模块的粒度更小可复用性更高,比如:轮播图,播放器任何位置都可能使用。

业务模块的粒度更大,可以引用多个功能模块解决问题,大多数代码都是依据业务逻辑编写,与功能模块相比 除非是同一个公司有相同业务否则复用性没那么高,

通信分析

上面主要讲了拆分模块的思路,现在聊聊模块间通信。特意设计模块间通信方案,主要是用于业务模块间通信。

业务模块和功能模块之间是单向通信,业务模块直接引用功能模块,调用功能模块暴露的方法即可。

但业务模块不同,业务模块之间存在互相通信的情况,核心情况有两种:

  1. 页面跳转
    1. A模块跳转B模块的页面,B模块跳转A模块的页面
  2. 数据通信
    1. A模块获取B模块的数据,比如调用B模块的网络请求。
    2. 可能会有点疑问,直接在A模块写要调用的接口不就好了,为什么要费劲巴拉的进行模块间通信,可以是可以。组件化就是为了隔离,解耦,复用。如果A模块直接实现了要用的网络请求,还要组件化干嘛呢,出现类似情况都这么干,项目内就会出现很多重复代码,除了图方便 没有别好处

单一模块开发时所有的类都能直接访问,上述的问题简直不是问题,从MainActivity 跳转到 TestActivity ,可以直接获取TestActivity的class对象完成跳转

val intent = Intent(this@MainActivity,TestActivity::class.java)
startActivity(intent)

但是分开多模块就是问题了,MainActivity 和 TestActivity 分别在A,B两个模块中,两个业务模块之间没有直接引用代码隔离,所以不能直接调用到想使用的类。

这种情况就需要一个中间人,帮助A,B模块通信。

(需求简单,实现简单)中间人好像邮局,两人住在同一个村甚至对门,想要唠嗑,送点东西,因为距离近走着就去了。如果两人相隔千里不能见面,想要唠嗑需要写信,标记地址交给邮局,让邮局转发。

(需求复杂,实现复杂)信件好保存一般不会损坏,运送比较方便。如果想要快点到,加钱用更快的运送工具。 如果想要送一块家乡的红烧肉,为了保鲜原汁原味,可能要加更多的钱用飞机+各种保险措施送过去

模块间通信也是类似,A,B模块通过中间人,也就是路由组件通信。页面跳转是最简单的通信需求实现简单,如果想要访问数据,获取对象应用等更复杂的需求,可能需要更加复杂的设计和其他技术手段才实现目标。

但总之A,B模块代码隔离之后不会无缘无故就实现了通信,一定会存在路由角色帮助A,B模块通信。区别在于路由是否强大,支持多少功能。

Untitled 2.png

粗糙的路由实现

页面跳转

实现路由组件最基本的功能页面跳转,讨论具体技术方案之前,先理清思路。

Android原生跳转页面只有一种办法 startActivity(intent(context,class)) ,调用startActivity方法有三要素

  1. context 提供的 startActivity方法
  2. 构造intent 需要 context
  3. 构造intent 需要 目标类的class对象

世面上所有的路由组件封装跳转页面功能,就算他封装出花来,也是基于AndroidSDK,无法脱离原生提供的方法。

所以我们现在需要想办法调用完整的startActivity(intent(context,class))

关键点在于,由于代码隔离,我们无法直接获取目标activity的class,直白点说无法 直接**“.”**出class。那么怎么可以在代码隔离的情况下拿到目标类的class呢

有个小技巧,先要说明一个事,模块A,模块B仅仅在编码的时候处于代码隔离的状态,但是打包之后它们还是一个应用,代码在一个虚拟机中。所以可以使用 Class.forName(包名+类名) 运行时获取class对象,完成跳转

val clazz = Class.forName("com.xxx.TestActivity")
val intent = Intent(this,clazz);
startActivity(intent)

这种方式可以帮助我们实现页面跳转的逻辑,但是非常粗糙,总不能需要模块间页面跳转,就硬编码包名+类名 获取class,太麻烦了,太容易出错了,代码散落在程序各处。

但是这种粗糙的方式也为我们提供了一点思想火花

如果我们能通过一种方式收集到 有模块间跳转需求的页面class对象 或者 包名+类名,在需要跳转的时候取出不就可以了么。

大概步骤:

  1. 创建路由组件
  2. 模块向路由注册页面信息
  3. 从路由取出页面信息实现跳转

创建路由组件,只有一个Route类

object Route {

private val routeMap = ArrayMap<String, Class<*>>()

fun register(path: String, clazz: Class<*>) {
routeMap[path] = clazz
}

fun navigation(context: Context, path: String) {
val clazz = routeMap[path]
val intent = Intent(context, clazz)
context.startActivity(intent)
}

}

其他组件在初始化时注册路由

Route.register("home/HomeActivity", HomeActivity::class.java)

模块间跳转页面

class TestActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
val button: Button = findViewById(R.id.button)
button.setOnClickListener {
Route.navigation(this, "home/HomeActivity")
}
}
}

把握住核心思想快速实现简单的模块间页面跳转还是非常简单的,在来回顾一下

  1. 代码隔离后 实现页面跳转最关键的问题是,无法直接获取目标类的Class引用
  2. 项目只是在编码期隔离,打包之后仍然是在一个虚拟机内,可以通过 Class.forName(包名+类名) 获取引用
  3. key-value的形式存储 需要模块间跳转类的Class信息,在需要的时候取出

看没什么用的效果图

QQ图片20220603100940.gif

上述代码肯定是可用的,但是实际运行并不是仅仅引入一个路由组件就可以了,还有很多项目配置细节,可以参考 开头推荐的文章

模块间通信

接口下沉方案,在Route组件中定义通讯接口,使所有模块都可以引用,具体实现只在某个业务模块中,在初始化时注册实现类,运行时通过反射动态创建实现类对象。

添加模块通信后,Route组件有两种逻辑要处理,页面跳转和模块通信。 保存的Class可能是Activity 或 某个接口实现类,业务操作也不同。

为了区分两种不同的业务,对Route组件进行一点小改造,新增 RouteEntity 保存数据,RouteType 路由类型用于区分,如下:

object Route {

private val routeMap = ArrayMap<String, RouteEntity>()

/**
* 注册信息
*/
fun register(route: RouteEntity) {
routeMap[route.path] = route
}

/**
* 页面导航
*/
fun navigation(context: Context, path: String) {
val routeEntity = routeMap[path] ?: throw RuntimeException("path错误 找不到类")
val intent = Intent(context, routeEntity.clazz)
context.startActivity(intent)
}

/**
* 获取通信实例
*/
fun getService(path: String): Any {
val routeEntity = routeMap[path] ?: throw RuntimeException("path错误 找不到类")
return routeEntity.clazz.newInstance()
}

}

/**
* 保存路由信息
* @param path 路径 用于查找class
* @param type 类型 区分 页面跳转 和 通信
* @param clazz 类信息
*/
data class RouteEntity(val path: String,@RouteType val type:Int,val clazz: Class<*>)

/**
* 路由类型
*/
@IntDef(RouteType.ACTIVITY, RouteType.SERVICE)
annotation class RouteType() {
companion object {
const val ACTIVITY = 0
const val SERVICE = 1
}
}

使用如下:

//在 Route组件中 定义接口

interface IShopService {
fun getPrice(): Int
}

//业务模块中实现接口
class ShopServiceImpl :IShopService {
override fun getPrice(): Int {
return 12
}
}

//模块初始化时注册
override fun create(context: Context) {
Route.register(RouteEntity("shop/ShopActivity",RouteType.ACTIVITY,ShopActivity::class.java))
Route.register(RouteEntity("shop/ShopService",RouteType.SERVICE,ShopServiceImpl::class.java))
}

//其他模块中使用

class HomeActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.home_activity_home)
val btnGoShop = findViewById<Button>(R.id.btn_go_shop)
val btnGetPrice = findViewById<Button>(R.id.btn_get_price)
btnGoShop.setOnClickListener {
//跳转页面
Route.navigation(this, "shop/ShopActivity")
}
btnGetPrice.setOnClickListener {
//模块通信
val shopService: IShopService = Route.getService("shop/ShopService") as IShopService
Toast.makeText(this, "价格:${shopService.getPrice()}", Toast.LENGTH_SHORT).show()
}
}
}

几点路由优化思路

  1. 路由信息 每次都要手动注册 很麻烦
    1. 利用编译时注解结合APT技术优化
    2. 自定义注解,跳转的页面和通信类添加注解
    3. 定义注解处理器,在编译时读取注解
    4. 根据注解携带的信息 处理业务逻辑 生成java类 完成组件注册功能
  2. 路由组件 所有路由信息在初始化的时候一次性加载到内存中,需要优化
    1. 分组保存,懒加载信息
    2. 根据路径 把路由信息分组保存,
      1. RootManager 保存 内部持有map 保存所有group 信息
      2. Group 内部持有 List 保存所有 节点信息
    3. 当用到某一group时,
      1. 通过反射实例化Group 加载当前Group下的节点信息到内存中
  3. 每次获取对象时,都是通过反射创建新对象,消耗内存
    1. 新增缓存机制,只在第一次创建新对象
    2. 可以使用 LruCache 缓存

上述路由组件的例子是非常简单的,难点在于从零开始,没有任何借鉴的情况下搞出这个”简单的”路由组件,反正我是没有这个创造能力 哈哈。

如果想要搞一个成熟完美的路由组件还是非常难的,但是最初肯定都是从基础功能开始一点一点迭代。除非是大佬,不然不推荐自定义路由组件


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

收起阅读 »

FlutterWeb浏览器刷新后无法回退的解决方案

一、问题 在Flutter开发的网页运行时,浏览器刷新网页后,虽然会显示刷新前的页面(前提是用静态路由跳转),但这时调用Navigator.pop方法是回不到上一页的,包括点击浏览器的回退按钮也是无效的(地址栏中的url会变,页面不会变)。 二、原因 当浏览器...
继续阅读 »

一、问题


在Flutter开发的网页运行时,浏览器刷新网页后,虽然会显示刷新前的页面(前提是用静态路由跳转),但这时调用Navigator.pop方法是回不到上一页的,包括点击浏览器的回退按钮也是无效的(地址栏中的url会变,页面不会变)。


二、原因


当浏览器刷新时,Flutter引擎会重新启动,并加载当前页面,也就是说,刷新后的Flutter内存中所有静态变量都被初始化,页面栈内之前的页面记录都未保留,只有当前的页面。就像是浏览网页时,把其中一页的网址拷出来,在新的标签页再次打开。


三、解决方案


1. 思路


知道什么原因引起的,就针对性解决。页面栈记录丢失,那么就代码中自己维护一套备用栈,监听页面路由,每次进入新页面时,记录当前页面的URL,当退出时,删除记录的URL,在浏览器刷新栈记录失效时,帮助回退到上一页。


2.方案优缺点


优点: 可实现回退效果无异常,调用Navigator.pop方法或点击浏览器回退按钮都支持;


缺点: Navigator.pushName().then的回调无法生效,因为是重新生成的上一页,所以并不会调用回调;回退后的页面中的临时数据都会消失,比如输入框内的内容,成员变量等;跳转必须用静态路由的方式,并且传参要用Uri包裹,不能用构造函数传参。


四、实现


1. Web本地存储工具—localStorage


localStorage是在html包下window中的一个存储对象,以keyvalue的形式进行存储


// 导包
import 'dart:html' as html;

// 使用方式
html.window.localStorage["key"] = "value"

对存储工具的封装这里就不写到文章里了,根据实现业务情况去封装,方便调用就行。


2. 栈记录工具类RouterHistory


这是一个栈记录工具,主要作用是注册监听,添加删除记录等。


/// DB()为封装好的本地数据库
class RouterHistory {
/// 监听浏览器刷新前的回调
static Function(html.Event event)? _beforeUnload;

/// 监听浏览器回退时的回调
static Function(html.Event event)? _popState;

/// 目前页面是否被刷新过
static bool isRefresh = false;

/// 初始化与注册监听
static void register() {
// 刷新时回调
_beforeUnload = (event) {
// 本地记录,标记成"已刷新"
DB(DBKey.isRefresh).value = true;
// 移除刷新前的实例的监听
html.window.removeEventListener('beforeunload', _beforeUnload);
html.window.removeEventListener('popstate', _popState);
};
// 浏览器回退按钮回调
_popState = (event) {
// 页面被刷新,触发备用回调
if (isRefresh) {
_back(R.currentContext); //R.currentContext 为当前页面的Context
}
};
// 添加监听
html.window.addEventListener('beforeunload', _beforeUnload);
html.window.addEventListener('popstate', _popState);

// 从本地数据库中取出"刷新"标记
isRefresh = DB(DBKey.isRefresh).get(false);

// 如果未被刷新,清除上次备用栈中的历史记录
if (!isRefresh) {
clean();
}

// 还原本地库中的刷新标记
DB(DBKey.isRefresh).value = false;
}


static bool checkBack(currentContext) {
// 是否能正常 pop
if (Navigator.canPop(currentContext)) {
return true;
}

// 不能则启用备用栈
_back(currentContext);
return false;
}

// 返回
static void _back(currentContext) {
List history = get();
if (history.length > 1) {
history.removeLast();
set(history);
//跳转至上一页并关闭当前页
Navigator.of(currentContext).popAndPushNamed(history.last);
}
}

// 添加记录
static add(String? path) {
if (path ` null) return;
List history = get();
if (history.contains(path)) return;
history.add(path);
set(history);
}

// 删除记录
static remove(String? path) {
if (path ` null) return;
List history = get();
history.remove(path);
set(history);
}

// 设置备用栈数据
static String set(List<dynamic> history) => DB(DBKey.history).value = json.encode(history);

// 取出备用栈数据
static get() => json.decode(DB(DBKey.history).get('[]'));

// 清除备用栈
static clean() => DB(DBKey.history).value = '[]';
}

3. 监听Flutter路由


自定义类并实现NavigatorObserver,并将实现类放在MaterialApp中的navigatorObservers参数中。


// 实现类
class HistoryObs extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
// 存路由信息
RouterHistory.add(route.settings.name);
}

@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
// 删路由信息
RouterHistory.remove(route.settings.name);
}
}


// 设置监听
MaterialApp(
......
navigatorObservers: [ HistoryObs() ],
......
)

4. 路由方法封装


跳转方法必须为静态路由,以保证参数和路径都能在url中,才可实现回退效果


 /// 替换 Navigator.pop ,
static pop() {
// 检测是否能正常返回,不能则返回FALSE
if (RouterHistory.checkBack(currentContext)) {
Navigator.pop(currentContext);
}
}

/// 静态路由跳转
static Future toName(String pageName, {Map<String, dynamic>? params}) {
// 封装路径以及参数
var uri = Uri(scheme: RoutePath.scheme, host: pageName, queryParameters: params ?? {});
return Navigator.of(currentContext).pushNamed(uri.toString());
}


5. 初始化位置


放在MaterialApp外层的build中,或initState中即可。


  @override
void initState() {
super.initState();
RouterHistory.register();
}

@override
Widget build(BuildContext context) {
// 或 RouterHistory.register();
return MaterialApp(
navigatorObservers: [MiddleWare()],
);
}


以上就是该方案的关键代码


五、最后


该方案只是能解决问题,但不是最好的解决方案。有更好的解决方案欢迎留言~


Flutter官方的Navigator 2.0 虽然能实现回退,本质上也是跳转了新页面,并造成栈内记录混乱,不能像真正的web一样,感兴趣的同学可以自行了解下Navigator 2.0



Navigator2.0在浏览器回退按钮的处理上又与Navigator1.0不同,点击回退按钮时Navigator2.0并不是执行pop操作,而是执行setNewRoutePath操作,本质上应该是从浏览器的history中获取上一个页面的url,然后重新加载。这样确实解决了刷新后回退的问题,因为刷新后浏览器的history并未丢失,但是也导致了文章中我们提到的flutter中的页面栈混乱的问题。


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

Android - setVisibility() 失效,竟然是因为内存泄露

一、前情概要 目前,我在开发的一个 Android 项目需要各个功能做到线上动态化,其中,App 启动时显示的 Loading 模块,会优先检测加载远程的 Loading 模块,加载失败时,会使用 App 本身默认的 Loading 视图,为此,我编写了一个 ...
继续阅读 »

一、前情概要


目前,我在开发的一个 Android 项目需要各个功能做到线上动态化,其中,App 启动时显示的 Loading 模块,会优先检测加载远程的 Loading 模块,加载失败时,会使用 App 本身默认的 Loading 视图,为此,我编写了一个 LoadingLoader 工具类:


/**
* Loading 加载器
*
* @author GitLqr
* @since 2022/7/2
*/
object LoadingLoader {

private var isInited = false // 防止多次初始化
private lateinit var onLoadFail: () -> Unit // 远程loading加载失败时的回调
private lateinit var onLoadComplete: () -> Unit // 加载完成后回调(无论成功失败)

fun init(onLoadFail: () -> Unit = {}, onLoadComplete: () -> Unit = {}): LoadingLoader {
if (!isInited) {
this.onLoadFail = onLoadFail
this.onLoadComplete = onLoadComplete
isInited = true
} else {
log("you have inited, this time is not valid")
}
return this
}

fun go() {
if (isInited) {
loadRemoteLoading(callback = { isSuccess ->
if (!isSuccess) onLoadFail()
onLoadComplete()
})
} else {
log("you must invoke init() firstly")
}
}

private fun loadRemoteLoading(callback: (boolean: Boolean) -> Unit) {
// 模拟远程 Loading 模块加载失败
Handler(Looper.getMainLooper()).postDelayed({
callback(false)
}, 1000)
}

private fun log(msg: String) {
Log.e("LoadingUpdater", msg)
}
}

LoadingLoader 工具类使用 Kotlin 的单例模式,init() 方法接收 2 个回调参数,go() 方法触发加载远程 Loading 模块,并根据加载结果执行回调,其中 isInited 用于防止该工具类被初始化多次。然后,在 App 的主入口 LoadingActivity 中使用 LoadingLoader,当加载远程 Loading 模块失败时,将原本隐藏的默认 Loading 视图显示出来;当加载 Loading 模块完成后(无论成功失败),模拟初始化数据并跳转主界面,关闭 LoadingActivity:


/**
* App 启动时的 Loading 界面
*
* @author GitLqr
* @since 2022/7/2
*/
class LoadingActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_loading)
// Loading 模块加载器
LoadingLoader.init(onLoadFail, onLoadComplete).go()
}

override fun onDestroy() {
super.onDestroy()
Log.e("GitLqr", "onDestroy")
}

private val onLoadFail: () -> Unit = {
// 显示默认 loading 界面
findViewById<View>(R.id.cl_def_loading).setVisibility(View.VISIBLE)
}

private val onLoadComplete: () -> Unit = {
// 模拟初始化数据,1秒后跳转主界面
Handler(Looper.getMainLooper()).postDelayed({
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
// 注意:此处意图使用的 flag,会将 LoadingActivity 界面关闭,触发 onDestroy()
startActivity(intent)
}, 1000)
}
}

LoadingActivity 的 xml 布局代码如下,默认的 Loading 布局初始状态不可见,即 visibility="gone"


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_def_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f00"
android:visibility="gone"
tools:visibility="visible">

<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="很好看的默认loading界面"
android:textColor="@color/white"
android:textSize="60dp" />

<ProgressBar
android:id="@+id/pb_loading"
android:layout_width="0dp"
android:layout_height="0dp"
android:indeterminateDrawable="@drawable/anim_loading"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.75"
app:layout_constraintWidth_percent="0.064" />

</androidx.constraintlayout.widget.ConstraintLayout>

</FrameLayout>

以上代码比较简单,现在来看下演示效果:



这里会发现一个问题,因为是以清空栈的方式启动 MainActivity,所以第二次启动时,理论上应该会跟第一次启动时界面显示效果完全一致,即每次启动都会显示默认的 Loading 视图,但是实际情况并没有,而控制台的日志也证实了 LoadingActivity 的 onDestroy() 有被触发:



二、摸索过程


1、代码执行了吗?


难道第二次启动 App 时,LoadingActivity.onLoadFail 没有触发吗?加上日志验证一下:


class LoadingActivity : AppCompatActivity() {
...
private val onLoadFail: () -> Unit = {
// 显示默认 loading 界面
val defLoading = findViewById<View>(R.id.cl_def_loading)
defLoading.setVisibility(View.VISIBLE)
Log.e("GitLqr", "defLoading.setVisibility --> ${defLoading.visibility}")
}
}

重新打包再执行一遍上面的演示操作,日志输出如下:



说明 2 次启动都是有触发 LoadingActivity.onLoadFail 的,并且结果都是 0 ,即 View.VISIBLE。



此时有点怀疑人生,于是网上找了一圈 setVisibility() 失效 的原因,基本上都是同一个内容(都特么抄来抄去的),说是做动画导致的,可是我这里并没有做动画,所以与网上说的情况不相符。



2、视图不显示的直接原因是什么?


既然,代码有输出日志,那说明 setVisibility(View.VISIBLE) 这行代码肯定执行过了,而界面上不显示,直接原因是什么?是因为默认 Loading 视图的 visibility 依旧为 View.GONE?又或者是因为其他因素导致 View 的尺寸出现了问题?这时,可以使用 AndroidStudio 的 Layout Inspector 工具,可以直观的分析界面的布局情况,为了方便 Layout Inspector 工具获取 LoadingActivity 的布局信息,需要将 LoadingActivity.onLoadComplete 中跳转主界面的代码注释掉,其他保持不变:


class LoadingActivity : AppCompatActivity() {
...
private val onLoadComplete: () -> Unit = {
// 模拟初始化数据,1秒后跳转主界面
Handler(Looper.getMainLooper()).postDelayed({
// val intent = Intent(this, MainActivity::class.java)
// intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
// // 注意:此处意图的 flag,会将 LoadingActivity 界面关闭,触发 onDestroy()
// startActivity(intent)
}, 1000)
}
}

然后重复上述演示操作,第一次启动,显示出默认 Loading,手动按返回键退出 App,再第二次启动,不显示默认 Loading:



控制台日志信息也如期输出,第二次启动确实执行了 setVisibility(View.VISIBLE)



这时,使用 Layout Inspector(菜单栏 -> Tools -> Layout Inspector),获取到 LoadingActivity 的布局信息:



这里可以断定,就是默认 Loading 视图的 visibility 依旧为 View.GONE 的情况。



注:因为 View.GONE 不占据屏幕空间,所以宽高都为 0,是正常的。



3、操作的视图是同一个吗?


现在回顾一下上述的 2 个线索,首先,代码中确定执行了 setVisibility(View.VISIBLE),并且日志里也显示了该视图的显示状态为 0,即 View.VISIBLE:



其次,使用 Layout Inspector 看到的的视图状态却为 View.GONE:



所以,真相只有一个,日志输出的视图 和 Layout Inspector 看到的的视图,肯定不是同一个!!为了验证这一点,代码再做如下调整,分别在 onCreate() 和 onLoadFail 中打印默认 Loading 视图信息:


class LoadingActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_loading)

val defLoading = findViewById<View>(R.id.cl_def_loading)
Log.e("GitLqr", "onCreate ---> view is ${defLoading}")

// Loading 模块加载器
LoadingLoader.init(onLoadFail, onLoadComplete).go()
}

private val onLoadFail: () -> Unit = {
// 显示默认 loading 界面
val defLoading = findViewById<View>(R.id.cl_def_loading)
defLoading.setVisibility(View.VISIBLE)
Log.e("GitLqr", "defLoading.setVisibility --> ${defLoading.visibility}, view is ${defLoading}")
}
}

再如上述演示操作一遍,日志输出如下:



可以看到第二次启动时,LoadingActivity.onLoadFail 中操作的视图,还是第一次启动时的那个视图,该视图是通过 findViewById 获取到的,说明 LoadingActivity.onLoadFail 中引用的 Activity 是第一次启动时的 LoadingActivity,也就是说 LoadingActivity 发生内存泄露了。此时才焕然大悟,Kotlin 中的 Lambda 表达式(像 onLoadFail、onLoadComplete 这种),对应到 Java 中就是匿名内部类,通过 Kotlin Bytecode 再反编译成 java 代码可以验证这点:


public final class LoadingActivity extends AppCompatActivity {
private final Function0 onLoadFail = (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}

public final void invoke() {
View defLoading = LoadingActivity.this.findViewById(1000000);
defLoading.setVisibility(0);
StringBuilder var10001 = (new StringBuilder()).append("defLoading.setVisibility --> ");
Intrinsics.checkExpressionValueIsNotNull(defLoading, "defLoading");
Log.e("GitLqr", var10001.append(defLoading.getVisibility()).append(", view is ").append(defLoading).toString());
}
});
private final Function0 onLoadComplete;

protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(1300004);
View defLoading = this.findViewById(1000000);
Log.e("GitLqr", "onCreate ---> view is " + defLoading);
LoadingLoader.INSTANCE.init(this.onLoadFail, this.onLoadComplete).go();
}

protected void onDestroy() {
super.onDestroy();
Log.e("GitLqr", "onDestroy");
}

public LoadingActivity() {
this.onLoadComplete = (Function0)null.INSTANCE;
}
}

我们知道,Java 中,匿名内部类会持有外部类的引用,即匿名内部类实例 onLoadFail 持有 LoadingActivity 实例,而 onLoadFail 又会通过 LoadingLoader.init() 方法传递给 LoadingLoader 这个单例对象,所以间接导致 LoadingLoader 持有了 LoadingActivity,因为单例生命周期与整个 App 进程相同,所以只要 App 进程不死,内存中就只有一分 LoadingLoader 实例,又因为是强引用,所以 GC 无法回收掉第一次初始化时传递给 LoadingLoader 的 LoadingActivity 实例,所以,无论重启多少次,onLoadFail 中永远都是拿着第一次启动时的 LoadingActivity 来执行 findViewById,拿到的 Loading 视图自然也不会是当前最新 LoadingActivity 的 Loading 视图。


三、解决方案


既然知道是因为 LoadingActivity 内存泄露导致的,那么解决方案也简单,就是在 LoadingLoader 完成它的使命之后,及时释放掉对 LoadingActivity 的引用即可,又因为 LoadingActivity 实际上并不是被 LoadingLoader 直接引用,而是被其内部变量 onLoadFail 直接引用的,那么在 LoadingLoader 中只需要将 onLoadFail 的引用切断就行了:


object LoadingLoader {
private var isInited = false // 防止多次初始化
private lateinit var onLoadFail: () -> Unit // 远程loading加载失败时的回调
private lateinit var onLoadComplete: () -> Unit // 加载完成后回调

fun go() {
if (isInited) {
loadRemoteLoading(callback = { isSuccess ->
if (!isSuccess) onLoadFail()
onLoadComplete()
destroy() // 使命完成,释放资源
})
} else {
log("you must invoke init() firstly")
}
}

fun destroy() {
this.onLoadFail = {}
this.onLoadComplete = {}
this.isInited = false
}
}

至此,因内存泄露导致 setVisibility() 失效的问题就解决掉了,要坚信,在代码的世界里,没有魔法~


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

一文读懂Kotlin的数据流

一、Android分层架构 不管是早期的MVC、MVP,还是最新的MVVM和MVI架构,这些框架一直解决的都是一个数据流的问题。一个良好的数据流框架,每一层的职责是单一的。例如,我们可以在表现层(Presentation Layer)的基础上添加一个领域层(D...
继续阅读 »

一、Android分层架构


不管是早期的MVC、MVP,还是最新的MVVM和MVI架构,这些框架一直解决的都是一个数据流的问题。一个良好的数据流框架,每一层的职责是单一的。例如,我们可以在表现层(Presentation Layer)的基础上添加一个领域层(Domain Layer) 来保存业务逻辑,使用数据层(Data Layer)对上层屏蔽数据来源(数据可能来自远程服务,可能是本地数据库)。


在Android中,一个典型的Android分层架构图如下:


image.png


其中,我们需要重点看下Presenter 和 ViewModel, Presenter 和 ViewModel向 View 提供数据的机制是不同的。



  • Presenter: Presenter通过持有 View 的引用并直接调用操作 View,以此向 View 提供和更新数据。

  • ViewModel:ViewModel 通过将可观察的数据暴露给观察者来向 View 提供和更新数据。


目前,官方提供的可观察的数据组件有LiveData、StateFlow和SharedFlow。可能大家对LiveData比较熟悉,配合ViewModel可以很方便的实现数据流的流转。不过,LiveData也有很多常见的缺陷,并且使用场景也比较固定,如果网上出现了KotlinFlow 替代 LiveData的声音。那么 Flow 真的会替代 LiveData吗?Flow 真的适合你的项目吗?看完下面的分析后,你定会有所收获。


二、ViewModel + LiveData


ViewModel的作用是将视图和逻辑进行分离,Activity或者Fragment只负责UI显示部分,网络请求或者数据库操作则有ViewModel负责。ViewModel旨在以注重生命周期的方式存储和管理界面相关的数据,让数据可在发生屏幕旋转等配置更改后继续留存。并且ViewModel不持有View层的实例,通过LiveData与Activity或者Fragment通讯,不需要担心潜在的内存泄漏问题。


而LiveData 则是一种可观察的数据存储器类,与常规的可观察类不同,LiveData 具有生命周期感知能力,它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保LiveData当数据源发生变化的时候,通知它的观察者更新UI界面。同时它只会通知处于Active状态的观察者更新界面,如果某个观察者的状态处于Paused或Destroyed时那么它将不会收到通知,所以不用担心内存泄漏问题。


下面是官方发布的架构组件库的生命周期的说明:


image.png


2.1 LiveData 特性


通过前面的介绍可以知道,LiveData 是 Android Jetpack Lifecycle 组件中的内容,具有生命周期感知能力。一句话概括就是:LiveData 是可感知生命周期的,可观察的,数据持有者。特点如下:



  • 观察者的回调永远发生在主线程

  • 仅持有单个且最新的数据

  • 自动取消订阅

  • 提供「可读可写」和「仅可读」两个版本收缩权限

  • 配合 DataBinding 实现「双向绑定」


观察者的回调永远发生在主线程


因为LiveData 是被用来更新 UI的,因此 Observer 接口的 onChanged() 方法必须在主线程回调。


public interface Observer<T> {
void onChanged(T t);
}

背后的道理也很简单,LiveData 的 setValue() 发生在主线程(非主线程调用会抛异常),而如果调用postValue()方法,则它的内部会切换到主线程调用 setValue()。


protected void postValue(T value) {
boolean postTask;
synchronized (mDataLock) {
postTask = mPendingData == NOT_SET;
mPendingData = value;
}
if (!postTask) {
return;
}
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

可以看到,postValue()方法的内部调用了postToMainThread()实现线程的切换,之后遍历所有观察者的 onChanged() 方法。


仅持有单个且最新数据


作为数据持有者,LiveData仅持有【单个且最新】的数据。单个且最新,意味着 LiveData 每次只能持有一个数据,如果有新数据则会覆盖上一个。并且,由于LiveData具备生命周期感知能力,所以观察者只会在活跃状态下(STARTED 到 RESUMED)才会接收到 LiveData 最新的数据,在非活跃状态下则不会收到。


自动取消订阅


可感知生命周期的重要优势就是可以自动取消订阅,这意味着开发者无需手动编写那些取消订阅的模板代码,降低了内存泄漏的可能性。背后的实现逻辑是在生命周期处于 DESTROYED 时,移除观察者。


@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
if (currentState == DESTROYED) {
removeObserver(mObserver);
return;
}
... //省略其他代码
}

提供「可读可写」和「仅可读」两种方式


LiveData 提供了setValue() 和 postValue()两种方式来操作实体数据,而为了细化权限,LiveData又提供了mutable(MutableLiveData) 和 immutable(LiveData) 两个类,前者「可读可写」,后者则「仅可读」。


image.png


配合 DataBinding 实现「双向绑定」


LiveData 配合 DataBinding 可以实现更新数据自动驱动UI变化,如果使用「双向绑定」还能实现 UI 变化影响数据的变化功能。


2.2 LiveData的缺陷


正如前面说的,LiveData有自己的使用场景,只有满足使用场景才会最大限度的发挥它的功能,而下面这些则是在设计时将自带的一些缺陷:



  • value 可以是 nullable 的

  • 在 fragment 订阅时需要传入正确的 lifecycleOwner

  • 当 LiveData 持有的数据是「事件」时,可能会遇到「粘性事件」

  • LiveData 是不防抖的

  • LiveData 的 transformation 需要工作在主线程


value 可以是 nullable 的


由于LiveData的getValue() 是可空的,所以在使用时应该注意判空,否则容易出现空指针的报错。


@Nullable
public T getValue() {
Object data = mData;
if (data != NOT_SET) {
return (T) data;
}
return null;
}

传入正确的 lifecycleOwner


Fragment 调用 LiveData的observe() 方法时传入 this 和 viewLifecycleOwner 的含义是不一样的。因为Fragment与Fragment中的View的生命周期并不一致,有时候我们需要的让observer感知Fragment中的View的生命周期而非Fragment。


粘性事件


粘性事件的定义是,发射的事件如果早于注册,那么注册之后依然可以接收到的事件,这一现象称为粘性事件。解决办法是:将事件作为状态的一部分,在事件被消费后,不再通知观察者。推荐两种解决方式:



  • KunMinX/UnPeek-LiveData

  • 使用kotlin 扩展函数和 typealias 封装解决「粘性」事件的 LiveData


默认不防抖


当setValue()/postValue() 传入相同的值且多次调用时,观察者的 onChanged() 也会被多次调用。不过,严格来讲,这也不算一个问题,我们只需要在调用 setValue()/postValue() 前判断一下 vlaue 与之前是否相同即可。


transformation 工作在主线程


有些时候,我们需要对从Repository 层得到的数据进行处理。例如,从数据库获得 User列表,我们需要根据 id 获取某个 User, 那么就需要用到MediatorLiveData 和 Transformatoins 来实现。



  • Transformations.map

  • Transformations.switchMap


并且,map 和 switchMap 内部均是使用 MediatorLiveData的addSource() 方法实现的,而该方法会在主线程调用,使用不当会有性能问题。


@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
Source<S> e = new Source<>(source, onChanged);
Source<?> existing = mSources.putIfAbsent(source, e);
if (existing != null && existing.mObserver != onChanged) {
throw new IllegalArgumentException(
"This source was already added with the different observer");
}
if (existing != null) {
return;
}
if (hasActiveObservers()) {
e.plug();
}
}

2.3 LiveData 小结


LiveData 是一种可观察的数据存储器类,与常规的可观察类不同,LiveData 具有生命周期感知能力,它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保LiveData当数据源发生变化的时候,通知它的观察者更新UI界面。同时它只会通知处于Active状态的观察者更新界面,如果某个观察者的状态处于Paused或Destroyed时那么它将不会收到通知,所以不用担心内存泄漏问题。


同时,LiveData 专注单一功能,因此它的一些方法使用上是有局限性的,并且需要配合 ViewModel 使用才能显示其价值。


三、Flow


3.1 简介


Flow是Google官方提供的一套基于kotlin协程的响应式编程模型,它与RxJava的使用类似,但相比之下Flow使用起来更简单,另外Flow作用在协程内,可以与协程的生命周期绑定,当协程取消时,Flow也会被取消,避免了内存泄漏风险。


协程是轻量级的线程,本质上协程、线程都是服务于并发场景下,其中协程是协作式任务,线程是抢占式任务。默认协程用来处理实时性不高的数据,请求到结果后整个协程就结束了。比如,有下面一个例子:


image.png


其中,红框中需要展示的内容实时性不高,而需要交互的,比如转发和点赞属于实时性很高的数据需要定时刷新。对于实时性不高的场景,直接使用 Kotlin 的协程处理即可,比如。


suspend fun loadData(): Data

uiScope.launch {
val data = loadData()
updateUI(data)
}

而对于实时性要求较高的场景,上面的方式就不起作用了,此时需要用到Kotlin提供的Flow数据流。


fun dataStream(): Flow<Data>uiScope.launch { 
dataStream().collect { data ->
updateUI(data)
}
}

3.2 基本概念


Kotlin的数据流主要由三个成员组成,分别是生产者、消费者和中介。
生产者:生成添加到数据流中的数据,可以配合得协程使用,使用异步方式生成数据。
中介(可选):可以修改发送到数据流的值,或修正数据流本身。
消费者:使用方则使用数据流中的值。


其中,中介可以对数据流中的数据进行更改,甚至可以更改数据流本身,他们的架构示意图如下。


image.png


在Kotlin中,Flow 是一种冷流,不过有一种特殊的Flow( StateFlow/SharedFlow) 是热流。什么是冷流,他和热流又有什么关系呢?


冷流:只有订阅者订阅时,才开始执行发射数据流的代码。并且冷流和订阅者只能是一对一的关系,当有多个不同的订阅者时,消息是重新完整发送的。也就是说对冷流而言,有多个订阅者的时候,他们各自的事件是独立的。
热流:无论有没有订阅者订阅,事件始终都会发生。当 热流有多个订阅者时,热流与订阅者们的关系是一对多的关系,可以与多个订阅者共享信息。


3.3 StateFlow


前面说过,冷流和订阅者只能是一对一的关系,当我们要实现一个流多个订阅者的场景时,就需要使用热流了。


StateFlow 是一个状态容器式可观察数据流,可以向其收集器发出当前状态更新和新状态更新。可以通过其 value 属性读取当前状态值,如需更新状态并将其发送到数据流,那么就需要使用MutableStateFlow。


3.3.1 基本使用


在Android 中,StateFlow 非常适合需要让可变状态保持可观察的类。由于StateFlow并不是系统API,所以使用前需要添加依赖:


dependencies {
... //省略其他

implementation "androidx.activity:activity-ktx:1.3.1"
implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
}

接着,我们需要创建一个ViewModel,比如:


class StateFlowViewModel: ViewModel() {
val data = MutableStateFlow<Int>(0)
fun add(v: View) {
data.value++
}
fun del(v: View) {
data.value--
}
}

可以看到,我们使用MutableStateFlow包裹需要操作的数据,并添加了add()和del()两个方法。然后,我们再编写一段测试代码实现数据的修改,并自动刷新数据。


class StateFlowActivity : AppCompatActivity() {
private val viewModel by viewModels<StateFlowViewModel>()
private val mBinding : ActivityStateFlowBinding by lazy {
ActivityStateFlowBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mBinding.root)
initFlow()
}
private fun initFlow() {
mBinding.apply {
btnAdd.setOnClickListener {
viewModel.add(it)
}
btnDel.setOnClickListener {
viewModel.del(it)
}
}
}

}

上面代码中涉及到的布局代码如下:


<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="stateFlowViewModel"
type="com.xzh.demo.flow.StateFlowViewModel" />
</data>

<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="200dp"
android:layout_marginTop="30dp"
android:text="@{String.valueOf(stateFlowViewModel.data)}"
android:textSize="24sp" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:layout_marginStart="10dp"
android:layout_marginBottom="10dp"
android:contentDescription="start"
android:src="@android:drawable/ic_input_add" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_del"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:contentDescription="cancel"
android:src="@android:drawable/ic_menu_close_clear_cancel" />
</FrameLayout>
</layout>

上面代码中,我们使用了DataBing写法,因此不需要再手动的绑定数据和刷新数据。


3.4 SharedFlow


3.4.1 SharedFlow基本概念


SharedFlow提供了SharedFlow 与 MutableSharedFlow两个版本,平时使用较多的是MutableSharedFlow。它们的区别是,SharedFlow可以保留历史数据,MutableSharedFlow 没有起始值,发送数据时需要调用 emit()/tryEmit() 方法。


首先,我们来看看SharedFlow的构造函数:


public fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>

可以看到,MutableSharedFlow需要三个参数:



  • replay:表示当新的订阅者Collect时,发送几个已经发送过的数据给它,默认为0,即默认新订阅者不会获取以前的数据

  • extraBufferCapacity:表示减去replay,MutableSharedFlow还缓存多少数据,默认为0

  • onBufferOverflow:表示缓存策略,即缓冲区满了之后Flow如何处理,默认为挂起。除此之外,还支持DROP_OLDEST 和DROP_LATEST 。


 //ViewModel
val sharedFlow=MutableSharedFlow<String>()
viewModelScope.launch{
sharedFlow.emit("Hello")
sharedFlow.emit("SharedFlow")
}

//Activity
lifecycleScope.launch{
viewMode.sharedFlow.collect {
print(it)
}
}

3.4.2 基本使用


SharedFlow并不是系统API,所以使用前需要添加依赖:


dependencies {
... //省略其他

implementation "androidx.activity:activity-ktx:1.3.1"
implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
}

接下来,我们创建一个SharedFlow,由于需要一对多的进行通知,所以我们MutableSharedFlow,然后重写postEvent()方法,代码如下:


object LocalEventBus  {
private val events= MutableSharedFlow< Event>()
suspend fun postEvent(event: Event){
events.emit(event)
}
}
data class Event(val timestamp:Long)

接下来,我们再创建一个ViewModel,里面添加startRefresh()和cancelRefresh()两个方法,如下。


class SharedViewModel: ViewModel() {
private lateinit var job: Job

fun startRefresh(){
job=viewModelScope.launch (Dispatchers.IO){
while (true){
LocalEventBus.postEvent(Event(System.currentTimeMillis()))
}
}
}

fun cancelRefresh(){
job.cancel()
}
}

前面说过,一个典型的Flow是由三部分构成的。所以,此处我们先新建一个用于数据消费的Fragment,代码如下:


class FlowFragment: Fragment() {
private val mBinding : FragmentFlowBinding by lazy {
FragmentFlowBinding.inflate(layoutInflater)
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return mBinding.root
}
override fun onStart() {
super.onStart()
lifecycleScope.launchWhenCreated {
LocalEventBus.events.collect {
mBinding.tvShow.text=" ${it.timestamp}"
}
}
}
}

FlowFragment的主要作用就是接收LocalEventBus的数据,并显示到视图上。接下来,我们还需要创建一个数据的生产者,为了简单,我们只在生产者页面中开启协程,代码如下:


class FlowActivity : AppCompatActivity() {
private val viewModel by viewModels<SharedViewModel>()
private val mBinding : ActivityFlowBinding by lazy {
ActivityFlowBinding.inflate(layoutInflater)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mBinding.root)
initFlow()
}

private fun initFlow() {
mBinding.apply {
btnStart.setOnClickListener {
viewModel.startRefresh()
}
btnStop.setOnClickListener {
viewModel.cancelRefresh()
}
}
}
}

其中,FlowActivity代码中涉及的布局如下:


<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".fragment.SharedFragment">

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

<fragment
android:name="com.xzh.demo.FlowFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:layout_marginStart="10dp"
android:layout_marginBottom="10dp"
android:src="@android:drawable/ic_input_add"
android:contentDescription="start" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="cancel" />
</FrameLayout>
</layout>

最后,当我们运行上面的代码时,就会在FlowFragment的页面上显示当前的时间戳,并且页面的数据会自动进行刷新。


3.5 冷流转热流


前文说过,Kotlin的Flow是一种冷流,而StateFlow/SharedFlow则属于热流。那么有人会问:怎么将冷流转化为热流呢?答案就是kotlin提供的shareIn()和stateIn()两个方法。


首先,来看一下StateFlow的shareIn的定义:


public fun <T> Flow<T>.stateIn(
scope: CoroutineScope,
started: SharingStarted,
initialValue: T
): StateFlow<T>

shareIn方法将流转换为SharedFlow,需要三个参数,我们重点看一下started参数,表示流启动的条件,支持三种:



  • SharingStarted.Eagerly:无论当前有没有订阅者,流都会启动,订阅者只能接收到replay个缓冲区的值。

  • SharingStarted.Lazily:当有第一个订阅者时,流才会开始,后面的订阅者只能接收到replay个缓冲区的值,当没有订阅者时流还是活跃的。

  • SharingStarted.WhileSubscribed:只有满足特定的条件时才会启动。


接下来,我们在看一下SharedFlow的shareIn的定义:


public fun <T> Flow<T>.shareIn(
scope: CoroutineScope,
started: SharingStarted,
replay: Int = 0
): SharedFlow<T>

此处,我们重点看下replay参数,该参数表示转换为SharedFlow之后,当有新的订阅者的时候发送缓存中值的个数。


3.6 StateFlow与SharedFlow对比


从前文的介绍可以知道,StateFlow与SharedFlow都是热流,都是为了满足流的多个订阅者的使用场景的,一时间让人有些傻傻分不清,那StateFlow与SharedFlow究竟有什么区别呢?总结起来,大概有以下几点:



  • SharedFlow配置更为灵活,支持配置replay、缓冲区大小等,StateFlow是SharedFlow的特殊化版本,replay固定为1,缓冲区大小默认为0。

  • StateFlow与LiveData类似,支持通过myFlow.value获取当前状态,如果有这个需求,必须使用StateFlow。

  • SharedFlow支持发出和收集重复值,而StateFlow当value重复时,不会回调collect给新的订阅者,StateFlow只会重播当前最新值,SharedFlow可配置重播元素个数(默认为0,即不重播)。


从上面的描述可以看出,StateFlow为我们做了一些默认的配置,而SharedFlow泽添加了一些默认约束。总的来说,SharedFlow相比StateFlow更灵活。


四、总结


目前,官方提供的可观察的数据组件有LiveData、StateFlow和SharedFlow。LiveData是Android早期的数据流组件,具有生命周期感知能力,需要配合ViewModel才能实现它的价值。不过,LiveData也有很多使用场景缺陷,常见的有粘性事件、不支持防抖等。


于是,Kotlin在1.4.0版本,陆续推出了StateFlow与SharedFlow两个组件,StateFlow与SharedFlow都是热流,都是为了满足流的多个订阅者的使用场景,不过它们也有微妙的区别,具体参考前面内容的说明。


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

Flutter JSON 解析最佳实践

这篇文章其实早该写了,之前的业余时间一直花在开源项目或其它文章上了。 JSON 解析对于 Flutter 新人来讲是个绕不开的话题,大家都在吐槽 Flutter 没有反射,导致 JSON 解析无法像 Android 那样方便,其实是不必要的,因为可以做到一样方...
继续阅读 »

这篇文章其实早该写了,之前的业余时间一直花在开源项目或其它文章上了。


JSON 解析对于 Flutter 新人来讲是个绕不开的话题,大家都在吐槽 Flutter 没有反射,导致 JSON 解析无法像 Android 那样方便,其实是不必要的,因为可以做到一样方便。


网上讲 JSON 解析的文章很多,大家自行去学习即可,本篇文章直接给出我创造出的、我认为的最佳方案,如有雷同,纯属巧合:


使用 JsonToDart 插件自动生成 Bean 类,再使用 dynamic 关键字的能力,自动将 JSON 字符串代表的数据填充到 Bean 类中


我们以如下 JSON 文本为例:


{
"nickName": "hackware",
"realName": "陈方兵",
"age": 29,
"sex": "男"
}

这是个 Person 对象的描述,我们先使用 JsonToDart 插件将其转换成 Bean 类,这样我们就无需手写解析代码了:


Snipaste_2022-07-02_07-31-58.png


Snipaste_2022-07-02_07-33-45.png


生成的 Bean 类代码如下:


class Person {
Person({
this.nickName,
this.realName,
this.age,
this.sex,
});

Person.fromJson(dynamic json) {
nickName = json['nickName'];
realName = json['realName'];
age = json['age'];
sex = json['sex'];
}

String? nickName;
String? realName;
int? age;
String? sex;

Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['nickName'] = nickName;
map['realName'] = realName;
map['age'] = age;
map['sex'] = sex;
return map;
}
}

这段代码最核心的是 fromJson 这个构造函数,由于 Flutter 中没有反射,我们无法动态的调用 fromJson 方法。但我们可以先构造一个空的 Person 对象,再使用 dynamic 关键字调用它,但需要对 fromJson 做一下更改,将它从构造函数改为普通函数,如下:


Person fromJson(dynamic json) {
nickName = json['nickName'];
realName = json['realName'];
age = json['age'];
sex = json['sex'];
return this;
}

或是:


void fromJson(dynamic json) {
nickName = json['nickName'];
realName = json['realName'];
age = json['age'];
sex = json['sex'];
}

真正的解析代码如下:


String jsonData = ''; // 从网络加载的 JSON 文本
Person person = Person().fromJson(jsonDecode(jsonData));

我想说明的是:使用反射自动创建对象和手动创建对象后再自动为该对象填充数据是一样方便的。因此即便没有反射,我们也能对网络请求做很好的封装。


目前由于我用的是自创的 PVState 架构模式,它是 MVC 的改进版,它也是个轻量级的状态管理方案。只有不到 120 行代码。它分为 PState 和 VState,这里的 State 指的是 StatefulWidget 的 State。前者封装业务逻辑,后者描述 UI,UI 和业务逻辑可以完全隔离。我把网络请求的基础能力封装到了 BasePState 中,如下:


void sendRequest<BEAN>({
required Future<Response<String>> call,
required BEAN bean,
OnStartCallback? startCallback,
OnSuccessCallback<BEAN>? successCallback,
OnFailCallback<BEAN>? failCallback,
}) async {
startCallback?.call();
bool? success;
Object? exception;
try {
Response<String> resp = await call;
dynamic result = (bean as dynamic).fromJson(jsonDecode(resp.data!));
success = result.success;
} catch (e) {
debugPrint('$e');
exception = e;
} finally {
try {
if (success == true) {
successCallback?.call(bean);
} else {
failCallback?.call(bean, exception);
}
} catch (e) {
exception = e;
failCallback?.call(bean, exception);
}
}
}

真正发起请求的代码如下:


sendRequest(
call: dio.get(
'https://xxx',
),
bean: RealtimeAlarmListBean(),
startCallback: () {
setState(() {
loadingRealtimeAlarm = true;
});
},
successCallback: (RealtimeAlarmListBean bean) {
setState(() {
realtimeAlarmListBean = bean;
loadingRealtimeAlarm = false;
});
},
failCallback: (_, __) {
setState(() {
loadingRealtimeAlarm = false;
showToast('请求失败');
});
},
);

可见我在外部构造好了空的 Bean 对象传进去,当请求回来后会把数据填充进去,最后在 successCallback 再把非空的 Bean 对象回传回来。整个过程我没有手动对 JSON 做解析。是不是挺方便的呢?


我比较喜欢这种网络模块的封装模式,当然你也可以使用 async、await 做“同步”的封装,萝卜青菜各有所爱吧。


这是目前我看到的最好的 JSON 解析方法,如果你有更好的方法,欢迎在评论区交流哦!


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

Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套

这次的 Flutter 小技巧是 ListView 和 PageView 的花式嵌套,不同 Scrollable 的嵌套冲突问题相信大家不会陌生,今天就通过 ListView 和 PageView 的三种嵌套模式带大家收获一些不一样的小技巧。 正常嵌套 最常...
继续阅读 »

这次的 Flutter 小技巧是 ListViewPageView 的花式嵌套,不同 Scrollable 的嵌套冲突问题相信大家不会陌生,今天就通过 ListViewPageView 的三种嵌套模式带大家收获一些不一样的小技巧。


正常嵌套


最常见的嵌套应该就是横向 PageView 加纵向 ListView 的组合,一般情况下这个组合不会有什么问题,除非你硬是要斜着滑


最近刚好遇到好几个人同时在问:“斜滑 ListView 容易切换到 PageView 滑动” 的问题,如下 GIF 所示,当用户在滑动 ListView 时,滑动角度带上倾斜之后,可能就会导致滑动的是 PageView 而不是 ListView


xiehuadong


虽然从我个人体验上并不觉得这是个问题,但是如果产品硬是要你修改,难道要自己重写 PageView 的手势响应吗?


我们简单看一下,不管是 PageView 还是 ListView 它们的滑动效果都来自于 Scrollable ,而 Scrollable 内部针对不同方向的响应,是通过 RawGestureDetector 完成:



  • VerticalDragGestureRecognizer 处理垂直方向的手势

  • HorizontalDragGestureRecognizer 处理水平方向的手势


所以简单看它们响应的判断逻辑,可以看到一个很有趣的方法 computeHitSlop根据 pointer 的类型确定当然命中需要的最小像素,触摸默认是 kTouchSlop (18.0)


image-20220613103745974


看到这你有没有灵光一闪:如果我们把 PageView 的 touchSlop 修改了,是不是就可以调整它响应的灵敏度? 恰好在 computeHitSlop 方法里,它可以通过 DeviceGestureSettings 来配置,而 DeviceGestureSettings 来自于 MediaQuery ,所以如下代码所示:


body: MediaQuery(
///调高 touchSlop 到 50 ,这样 pageview 滑动可能有点点影响,
///但是大概率处理了斜着滑动触发的问题
data: MediaQuery.of(context).copyWith(
gestureSettings: DeviceGestureSettings(
touchSlop: 50,
)),
child: PageView(
scrollDirection: Axis.horizontal,
pageSnapping: true,
children: [
HandlerListView(),
HandlerListView(),
],
),
),

小技巧一:通过嵌套一个 MediaQuery ,然后调整 gestureSettingstouchSlop 从而修改 PageView 的灵明度 ,另外不要忘记,还需要把 ListViewtouchSlop 切换会默认 的 kTouchSlop


class HandlerListView extends StatefulWidget {
@override
_MyListViewState createState() => _MyListViewState();
}
class _MyListViewState extends State<HandlerListView> {
@override
Widget build(BuildContext context) {
return MediaQuery(
///这里 touchSlop 需要调回默认
data: MediaQuery.of(context).copyWith(
gestureSettings: DeviceGestureSettings(
touchSlop: kTouchSlop,
)),
child: ListView.separated(
itemCount: 15,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
separatorBuilder: (context, index) {
return const Divider(
thickness: 3,
);
},
),
);
}
}

最后我们看一下效果,如下 GIF 所示,现在就算你斜着滑动,也很触发 PageView 的水平滑动,只有横向移动时才会触发 PageView 的手势,当然, 如果要说这个粗暴的写法有什么问题的话,大概就是降低了 PageView 响应的灵敏度


xiehuabudong


同方向 PageView 嵌套 ListView


介绍完常规使用,接着来点不一样的,在垂直切换的 PageView 里嵌套垂直滚动的 ListView , 你第一感觉是不是觉得不靠谱,为什么会有这样的场景?



对于产品来说,他们不会考虑你如何实现的问题,他们只会拍着脑袋说淘宝可以,为什么你不行,所以如果是你,你会怎么做?



而关于这个需求,社区目前讨论的结果是:PageViewListView 的滑动禁用,然后通过 RawGestureDetector 自己管理



如果对实现逻辑分析没兴趣,可以直接看本小节末尾的 源码链接



看到自己管理先不要慌,虽然要自己实现 PageViewListView 的手势分发,但是其实并不需要重写 PageViewListView ,我们可以复用它们的 Darg 响应逻辑,如下代码所示:



  • 通过 NeverScrollableScrollPhysics 禁止了 PageViewListView 的滚动效果

  • 通过顶部 RawGestureDetector VerticalDragGestureRecognizer 自己管理手势事件

  • 配置 PageControllerScrollController 用于获取状态


body: RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer instance) {
instance
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
})
},
behavior: HitTestBehavior.opaque,
child: PageView(
controller: _pageController,
scrollDirection: Axis.vertical,
///屏蔽默认的滑动响应
physics: const NeverScrollableScrollPhysics(),
children: [
ListView.builder(
controller: _listScrollController,
///屏蔽默认的滑动响应
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return ListTile(title: Text('List Item $index'));
},
itemCount: 30,
),
Container(
color: Colors.green,
child: Center(
child: Text(
'Page View',
style: TextStyle(fontSize: 50),
),
),
)
],
),
),

接着我们看 _handleDragStart 实现,如下代码所示,在产生手势 details 时,我们主要判断:



  • 通过 ScrollController 判断 ListView 是否可见

  • 判断触摸位置是否在 ListIView 范围内

  • 根据状态判断通过哪个 Controller 去生产 Drag 对象,用于响应后续的滑动事件



void _handleDragStart(DragStartDetails details) {
///先判断 Listview 是否可见或者可以调用
///一般不可见时 hasClients false ,因为 PageView 也没有 keepAlive
if (_listScrollController?.hasClients == true &&
_listScrollController?.position.context.storageContext != null) {
///获取 ListView 的 renderBox
final RenderBox? renderBox = _listScrollController
?.position.context.storageContext
.findRenderObject() as RenderBox;

///判断触摸的位置是否在 ListView 内
///不在范围内一般是因为 ListView 已经滑动上去了,坐标位置和触摸位置不一致
if (renderBox?.paintBounds
.shift(renderBox.localToGlobal(Offset.zero))
.contains(details.globalPosition) ==
true) {
_activeScrollController = _listScrollController;
_drag = _activeScrollController?.position.drag(details, _disposeDrag);
return;
}
}

///这时候就可以认为是 PageView 需要滑动
_activeScrollController = _pageController;
_drag = _pageController?.position.drag(details, _disposeDrag);
}

前面我们主要在触摸开始时,判断需要响应的对象时 ListView 还是 PageView ,然后通过 _activeScrollController 保存当然响应对象,并且通过 Controller 生成用于响应手势信息的 Drag 对象。



简单说:滑动事件发生时,默认会建立一个 Drag 用于处理后续的滑动事件,Drag 会对原始事件进行加工之后再给到 ScrollPosition 去触发后续滑动效果。



接着在 _handleDragUpdate 方法里,主要是判断响应是不是需要切换到 PageView :



  • 如果不需要就继续用前面得到的 _drag?.update(details)响应 ListView 滚动

  • 如果需要就通过 _pageController 切换新的 _drag 对象用于响应


void _handleDragUpdate(DragUpdateDetails details) {
if (_activeScrollController == _listScrollController &&

///手指向上移动,也就是快要显示出底部 PageView
details.primaryDelta! < 0 &&

///到了底部,切换到 PageView
_activeScrollController?.position.pixels ==
_activeScrollController?.position.maxScrollExtent) {
///切换相应的控制器
_activeScrollController = _pageController;
_drag?.cancel();

///参考 Scrollable 里
///因为是切换控制器,也就是要更新 Drag
///拖拽流程要切换到 PageView 里,所以需要 DragStartDetails
///所以需要把 DragUpdateDetails 变成 DragStartDetails
///提取出 PageView 里的 Drag 相应 details
_drag = _pageController?.position.drag(
DragStartDetails(
globalPosition: details.globalPosition,
localPosition: details.localPosition),
_disposeDrag);
}
_drag?.update(details);
}


这里有个小知识点:如上代码所示,我们可以简单通过 details.primaryDelta 判断滑动方向和移动的是否是主轴



最后如下 GIF 所示,可以看到 PageView 嵌套 ListView 同方向滑动可以正常运行了,但是目前还有个两个小问题,从图示可以看到:



  • 在切换之后 ListView 的位置没有保存下来

  • 产品要求去除 ListView 的边缘溢出效果


7777777777777


所以我们需要对 ListView 做一个 KeepAlive ,然后用简单的方法去除 Android 边缘滑动的 Material 效果:



  • 通过 with AutomaticKeepAliveClientMixinListView 在切换之后也保持滑动位置

  • 通过 ScrollConfiguration.of(context).copyWith(overscroll: false) 快速去除 Scrollable 的边缘 Material 效果


child: PageView(
controller: _pageController,
scrollDirection: Axis.vertical,
///去掉 Android 上默认的边缘拖拽效果
scrollBehavior:
ScrollConfiguration.of(context).copyWith(overscroll: false),


///对 PageView 里的 ListView 做 KeepAlive 记住位置
class KeepAliveListView extends StatefulWidget {
final ScrollController? listScrollController;
final int itemCount;

KeepAliveListView({
required this.listScrollController,
required this.itemCount,
});

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

class KeepAliveListViewState extends State<KeepAliveListView>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return ListView.builder(
controller: widget.listScrollController,

///屏蔽默认的滑动响应
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return ListTile(title: Text('List Item $index'));
},
itemCount: widget.itemCount,
);
}

@override
bool get wantKeepAlive => true;
}

所以这里我们有解锁了另外一个小技巧:通过 ScrollConfiguration.of(context).copyWith(overscroll: false) 快速去除 Android 滑动到边缘的 Material 2效果,为什么说 Material2, 因为 Material3 上变了,具体可见: Flutter 3 下的 ThemeExtensions 和 Material3


000000000



本小节源码可见: github.com/CarGuo/gsy_…



同方向 ListView 嵌套 PageView


那还有没有更非常规的?答案是肯定的,毕竟产品的小脑袋,怎么会想不到在垂直滑动的 ListView 里嵌套垂直切换的 PageView 这种需求。


有了前面的思路,其实实现这个逻辑也是异曲同工:PageViewListView 的滑动禁用,然后通过 RawGestureDetector 自己管理,不同的就是手势方法分发的差异。


RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer instance) {
instance
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
})
},
behavior: HitTestBehavior.opaque,
child: ListView.builder(
///屏蔽默认的滑动响应
physics: NeverScrollableScrollPhysics(),
controller: _listScrollController,
itemCount: 5,
itemBuilder: (context, index) {
if (index == 0) {
return Container(
height: 300,
child: KeepAlivePageView(
pageController: _pageController,
itemCount: itemCount,
),
);
}
return Container(
height: 300,
color: Colors.greenAccent,
child: Center(
child: Text(
"Item $index",
style: TextStyle(fontSize: 40, color: Colors.blue),
),
));
}),
)

同样是在 _handleDragStart 方法里,这里首先需要判断:



  • ListView 如果已经滑动过,就不响应顶部 PageView 的事件

  • 如果此时 ListView 处于顶部未滑动,判断手势位置是否在 PageView 里,如果是响应 PageView 的事件


  void _handleDragStart(DragStartDetails details) {
///只要不是顶部,就不响应 PageView 的滑动
///所以这个判断只支持垂直 PageView 在 ListView 的顶部
if (_listScrollController.offset > 0) {
_activeScrollController = _listScrollController;
_drag = _listScrollController.position.drag(details, _disposeDrag);
return;
}

///此时处于 ListView 的顶部
if (_pageController.hasClients) {
///获取 PageView
final RenderBox renderBox =
_pageController.position.context.storageContext.findRenderObject()
as RenderBox;

///判断触摸范围是不是在 PageView
final isDragPageView = renderBox.paintBounds
.shift(renderBox.localToGlobal(Offset.zero))
.contains(details.globalPosition);

///如果在 PageView 里就切换到 PageView
if (isDragPageView) {
_activeScrollController = _pageController;
_drag = _activeScrollController.position.drag(details, _disposeDrag);
return;
}
}

///不在 PageView 里就继续响应 ListView
_activeScrollController = _listScrollController;
_drag = _listScrollController.position.drag(details, _disposeDrag);
}

接着在 _handleDragUpdate 方法里,判断如果 PageView 已经滑动到最后一页,也将滑动事件切换到 ListView


void _handleDragUpdate(DragUpdateDetails details) {
var scrollDirection = _activeScrollController.position.userScrollDirection;

///判断此时响应的如果还是 _pageController,是不是到了最后一页
if (_activeScrollController == _pageController &&
scrollDirection == ScrollDirection.reverse &&

///是不是到最后一页了,到最后一页就切换回 pageController
(_pageController.page != null &&
_pageController.page! >= (itemCount - 1))) {
///切换回 ListView
_activeScrollController = _listScrollController;
_drag?.cancel();
_drag = _listScrollController.position.drag(
DragStartDetails(
globalPosition: details.globalPosition,
localPosition: details.localPosition),
_disposeDrag);
}
_drag?.update(details);
}

当然,同样还有 KeepAlive 和去除列表 Material 边缘效果,最后运行效果如下 GIF 所示。


22222222222



本小节源码可见:github.com/CarGuo/gsy_…



最后再补充一个小技巧:如果你需要 Flutter 打印手势竞技的过程,可以配置 debugPrintGestureArenaDiagnostics = true;来让 Flutter 输出手势竞技的处理过程


import 'package:flutter/gestures.dart';
void main() {
debugPrintGestureArenaDiagnostics = true;
runApp(MyApp());
}

image-20220613115808538


最后


最后总结一下,本篇介绍了如何通过 Darg 解决各种因为嵌套而导致的手势冲突,相信大家也知道了如何利用 ControllerDarg 来快速自定义一些滑动需求,例如 ListView 联动 ListView 的差量滑动效果:


///listView 联动 listView
class ListViewLinkListView extends StatefulWidget {
@override
_ListViewLinkListViewState createState() => _ListViewLinkListViewState();
}

class _ListViewLinkListViewState extends State<ListViewLinkListView> {
ScrollController _primaryScrollController = ScrollController();
ScrollController _subScrollController = ScrollController();

Drag? _primaryDrag;
Drag? _subDrag;

@override
void initState() {
super.initState();
}

@override
void dispose() {
_primaryScrollController.dispose();
_subScrollController.dispose();
super.dispose();
}

void _handleDragStart(DragStartDetails details) {
_primaryDrag =
_primaryScrollController.position.drag(details, _disposePrimaryDrag);
_subDrag = _subScrollController.position.drag(details, _disposeSubDrag);
}

void _handleDragUpdate(DragUpdateDetails details) {
_primaryDrag?.update(details);

///除以10实现差量效果
_subDrag?.update(DragUpdateDetails(
sourceTimeStamp: details.sourceTimeStamp,
delta: details.delta / 30,
primaryDelta: (details.primaryDelta ?? 0) / 30,
globalPosition: details.globalPosition,
localPosition: details.localPosition));
}

void _handleDragEnd(DragEndDetails details) {
_primaryDrag?.end(details);
_subDrag?.end(details);
}

void _handleDragCancel() {
_primaryDrag?.cancel();
_subDrag?.cancel();
}

void _disposePrimaryDrag() {
_primaryDrag = null;
}

void _disposeSubDrag() {
_subDrag = null;
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("ListViewLinkListView"),
),
body: RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer instance) {
instance
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
})
},
behavior: HitTestBehavior.opaque,
child: ScrollConfiguration(
///去掉 Android 上默认的边缘拖拽效果
behavior:
ScrollConfiguration.of(context).copyWith(overscroll: false),
child: Row(
children: [
new Expanded(
child: ListView.builder(

///屏蔽默认的滑动响应
physics: NeverScrollableScrollPhysics(),
controller: _primaryScrollController,
itemCount: 55,
itemBuilder: (context, index) {
return Container(
height: 300,
color: Colors.greenAccent,
child: Center(
child: Text(
"Item $index",
style: TextStyle(
fontSize: 40, color: Colors.blue),
),
));
})),
new SizedBox(
width: 5,
),
new Expanded(
child: ListView.builder(

///屏蔽默认的滑动响应
physics: NeverScrollableScrollPhysics(),
controller: _subScrollController,
itemCount: 55,
itemBuilder: (context, index) {
return Container(
height: 300,
color: Colors.deepOrange,
child: Center(
child: Text(
"Item $index",
style:
TextStyle(fontSize: 40, color: Colors.white),
),
),
);
}),
),
],
),
),
));
}
}

44444444444444


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

环信IM重大更新:新SDK+新场景+质量洞察+内容审核+出海

即时通讯IM是人与人沟通的基础服务,随着线上场景的进一步丰富,用户对于IM的能力要求日益提升。IM本质是一项服务,用户对于体验质量的要求异常严苛,掌握终端用户质量体验的变化和趋势,能够快速发现问题及根因,成为开发者核心关注的问题。近日,环信IM发布重大更新,包...
继续阅读 »

即时通讯IM是人与人沟通的基础服务,随着线上场景的进一步丰富,用户对于IM的能力要求日益提升。IM本质是一项服务,用户对于体验质量的要求异常严苛,掌握终端用户质量体验的变化和趋势,能够快速发现问题及根因,成为开发者核心关注的问题。近日,环信IM发布重大更新,包括:新SDK+新场景+水晶球+内容审核+出海等重磅特性。

WechatIMG14814.png

环信IM在已经推出的实时热点数据、消息投递查询、用户连接状态查询的基础上,

1、增加水晶球质量洞察能力对终端用户体验数据进行统计分析,帮助开发者实时掌控用户体验;

2、IM中传递的是用户产生的信息,对于所有终端用户而言,有一个安全、干净的聊天环境至关重要,环信IM提供了多种维度的内容审核管理能力,帮助开发者有效地对内容进行管理;

3、IM作为基础的沟通服务,在聊天的基础上衍生出了多种沟通的方式随着元宇宙等新场景的爆发,多人沟通的Discord社区模型受到了游戏玩家的青睐,环信IM提供了一组新的特性对新场景进行支持;

4、2022出海依旧是互联网的一个风口,随着越来越多的企业扬帆出海,环信IM提供的全球加速网络、安全合规、翻译都将助力出海客户快速构建符合当地用户使用习惯的应用,环信是IM行业首家通过全球最严苛安全GDPR认证的厂商。同时,环信IM提供业界最全的SDK矩阵,支持业界最全的小程序生态;

业界最全SDK矩阵提升开发体验

随着新技术、新平台的发展,环信IM响应开发者的需求,提供更多的SDK支持。在跨平台的支持上,提供Flutter、React Native、Unity、Uni-App。同时增加了原生Windows SDK的支持。 

环信IM支持Android、iOS、macOS、Windows、Linux、Web、Flutter、Unity、Electron、React Native、Uni-App、ApiCloud等12大SDK。

同时支持业界最全的小程序生态,包括:微信/QQ小程序、支付宝小程序、字节跳动小程序、快手小程序、百度小程序、360小程序等。

水晶球-质量洞察

实现从质量数据主动监控、异常问题实时告警、消息投递调查分析到历史数据回溯洞察是 IM质量监控功能的闭环。本期增加的质量数据主动监控为开发者提供了全景的用户IM使用质量数据。

终端数据:提供全球终端用户的登录、消息收发、好友管理、用户管理、群组管理、聊天室管理监控数据

image.png

Server API数据:提供全球服务端token获取、用户操作、文件操作、发送消息、群组管理、聊天室管理、用户属性操作的监控数据。

image.png

多维度内容审核能力

互联网不是法外之地!聊天内容的治理一直是即时通讯的核心能力,伴随着个人隐私保护法的实施,为用户提供一个安全、干净的聊天环境愈发重要。环信IM提供多维度多层次的内容审核能力,帮助开发者应对内容治理的挑战。

丰富的用户管理手段

作为开发者在应用中拥有最高权限,可以对用户进行APP级别的封禁、强制下线、删除等操作;在群组和聊天室中,可以对用户进行踢出、拉黑、禁言等操作。

消息举报:终端用户对自己接收的不良内容进行举报,开发者在console中对于发送者和消息进行处理。

image.png基于AI模型的文本、图片、语音、视频消息的审核能力:提供多种违规模型的不同消息类型的审核服务

image.png

社区多人沟通新场景

随着国外Discord爆红出圈,对于多人沟通的社区场景,环信IM增加了消息表情回复、子区等特性。

消息表情回复:用户可以在单聊和群聊中对消息添加、删除表情。表情可以直观地表达情绪,使用表情回复增加用户互动,提升用户使用体验。同时在群组中,利用表情回复可以发起投票,根据不同表情的追加数量来确认投票。

image.png消息子区:子区是群组成员的子集,是支持多人沟通的即时通讯系统,提供子区创建删除、成员管理等能力。

image.png

愈加成熟的全球服务

作为互联网底层通讯服务提供商,环信IM服务的海外客户已经遍布全球各地,随着海外服务经验的积累,环信IM的全球化服务也愈加成熟。环信是IM行业首家通过全球最严苛安全GDPR认证的厂商,帮助出海开发者免除安全后顾之忧。基于环信全球加速网络SD-GMN服务,全球端到端消息发送平均时延低于100ms,保证跨国跨区域沟通的用户体验。

环信5大数据中心覆盖全球200多个国家和地区;集团自建上万台服务器,部署全球300多个补充加速节点,实现低延迟;FPA加速与AWS加速智能切换,确保通信质量和高可用能力;On-Demand就近接入节点,全球加速网络SD-GMN服务,实测北美数据:30-40毫秒、欧洲:20-30毫秒、东南亚/日韩:30-40毫秒、北非:45毫秒、澳洲:50毫秒、中东:70毫秒、南美和南非:90毫秒;

image.png

image.pngimage.png

- SD-GMN实测数据展示 -

翻译功能:文本消息支持翻译功能,包含按需翻译和自动翻译。

1.按需翻译:收到消息时,接收方将消息内容翻译成目标语言。

2.自动翻译:用户发送消息时,SDK 根据设置的目标语言自动翻译消息内容,然后将消息原文和译文一并发送给消息接收方

image.png

结语

除了环信IM的大招,环信PUSH、环信MQTT也火力全开,环信正经历从即时通讯云到全球消息云的生态演化:1、IM平台向下深化和泛化。从Person to Person IM 发展为更通用的消息云平台,应用和应用之间,设备和云之间消息等,以及MQTT物联网消息新机会。2、IM平台向上层场景拓展。包括新场景产品,全渠道通知(IM、企业微信、小程序、短信等)。3、消息领域拓展。包括互联网消息外增加电信类消息,以及5G消息、短信、闪验等。环信全球消息云生态将覆盖包括:chat 聊天、notification 通知、push推送、营销、iot、短信验证码等丰富场景。

未来已来,环信已经准备好了!

即刻免费体验环信IM新特性:https://console.easemob.com/user/register

收起阅读 »

使用 C# 开发 node.js 插件

项目需求最近在开发一个 electron 程序,其中有用到和硬件通讯部分;硬件厂商给的是 .dll 链接库做通讯桥接, 第一版本使用 C 写的 Node.js 扩展 😁;由于有异步任务的关系,实现使用了 N-API 提供的多线程做异步任务调度, 虽然功能实...
继续阅读 »

项目需求

最近在开发一个 electron 程序,其中有用到和硬件通讯部分;硬件厂商给的是 .dll 链接库做通讯桥接,
第一版本使用 C 写的 Node.js 扩展 😁;由于有异步任务的关系,实现使用了 N-API 提供的多线程做异步任务调度,
虽然功能实现了,但是也有些值得思考的点。

  • 纯 C 编程效率低,木有 trycatch 的语言调试难度也大 (磕磕绊绊的)
  • 编写好的 .node 扩展文件,放在 electron 主进程中运行会有一定的隐患稍有差错会导致软件闪退 (后来用子进程隔离运行)
  • 基于 N-API 方式去编写 Node.js 插件会显得有所束缚,木有那种随心所欲写 C 的那种“顺畅”;尤其是多线程部分

综上考虑,加上通讯功能又是调用 .dll 文件,索性转战 C#,对于 windows 来说再合适不过了;但是问题是 C# 咋编译到 Node.js 中?
答案是“编译不了”。
插件实现的功能只是收到命令后调用 .dll 去操作硬件,再时时能把结果返回即可。
基于这个需求我们用 C# 去调用 .dll 文件,然后再解决派发命令、实时获取结果的通讯问题就OK了,剩下的就都是好处啦

  • C# 编写难度低于 C,又是 windows 亲儿子,基于 .NET Framework 编译后的程序仅 19KB (C实现同样功能编出来的.node文件 565KB)
  • 基于 C# 的插件独立于 Node.js 运行环境,程序出了问题不会影响 electron 应用
  • 木有任何的编程束缚,~亲想咋写就咋写

通讯问题

说这个之前我们还忽略了一个问题,这个 C# 的程序(.exe文件)如果启动?
既然是一个程序(.exe文件),我们双击即可执行;既然双击即可执行,我们就可以用 child_process 模块提供的
spawn 去拉起程序(代替鼠标双击);

好!程序已经启动了,那么该到了如果通讯的环节了。
spawn 的执行就是开启了一个单独的进程,通讯问题也就是进程通讯问题。之前如果你用过 spawn 启动过 Node.js 程序(.js文件),那么你肯定知道通讯使用 send 方法即可;这个是 Node.js 内置的方式

我们启动的进程是 C# 程序,通讯问题只能我们自己来解决了;进程通讯的方式有好多这里不展开。对于前端(web)攻城狮来讲,我们最熟悉的莫过于 http 通讯方式了;就用它!

  • C# 程序端启动开启一个 http 服务等待 Node.js 端发送请求过来;根据参数决定要干啥
  • spawn 启动的应用(进程),会返回一个 ChildProcessWithoutNullStreams (这个我也不能很明确的理解);能够接收到标准的 stdio 输入/输出
    那我们就利用这点使用 ChildProcessWithoutNullStreams.stdout.on('data', chunk => console.log(chunk.toString())) 的方式就可以收到 C# 通过 stdioConsole.WriteLine() 发过来的数据;
    哇!好方便~
  • 可能有人会想到用双工的 web socket 实现通讯,很棒!实现方式确实有很多种,这里用 Console.WriteLine() 通过标准的 stdio 方式实现,算不算是一个开发成本不高的讨巧做法呢!

大致流程


  • 如果觉得这篇文章有难度,可以看简单版的哦 Node.js 利用 stdio 标准输入/输出实现与 C# 程序通讯

开发环境

  • C# 代码部分使用 Visual Studio 2017
  • test.js 代码部分使用 VsCode
using System; 
using System.Collections.Generic;
using System.Linq; using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text.RegularExpressions;
namespace NodeAddons {
class Program {
static TcpListener listener;
static int port = 8899;
static string now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
static void Main(string[] args){
listener = new TcpListener(IPAddress.Any, port);
listener.Start();
// 启用服务器线程
new Thread(new ThreadStart(StartServer)).Start();
Console.WriteLine("Http server run at {0}.", port);
}
// Http 服务器
static void StartServer() {
while(true) {
// 这里会阻塞线程,直到接受到一个请求
Socket socket = listener.AcceptSocket();
// 将请求单独开一个线程处理;while(true)会回到等待下一个请求状态,周而复始
new Thread(new ParameterizedThreadStart(HandleRequest)).Start(socket);
} } // 处理一个请求 static void HandleRequest(object args) { Socket socket = (Socket)args; byte[] receive = new byte[1024]; socket.Receive(receive, receive.Length, SocketFlags.None); string httpRawTxt = Encoding.ASCII.GetString(receive); // 通过 stdio(Console.WriteLine) 实现与 node.js 通讯 // ## 开头、结尾,方便区分这个条输出是给 node.js 通讯用的 Console.WriteLine("##" + httpRawTxt + "##"); SendToBrowser(ref socket, now); } // 发送数据 static void SendToBrowser(ref Socket socket, string body) { string header = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "Content-Length: " + body.Length + "\r\n" + "Access-Control-Allow-Origin:*\r\n" // 支持跨域 + "\r\n"; // 响应头与响应体分界 byte[] data = Encoding.ASCII.GetBytes(header + body); if (socket.Connected) { int res = socket.Send(data, data.Length, SocketFlags.None); if (res == -1){ Console.WriteLine("Socket Error cannot Send Packet.");}else{ Console.WriteLine(">> [{0}]", now); } socket.Close();}}}}

Node.js 部分

const http = require('http'); 
const cp = require('child_process');
const path = require('path');
// const handel = cp.spawn(path.join(__dirname, 'dist/NodeAddons.exe'));
const handel = cp.spawn(path.join(__dirname, 'dist/NodeAddons_WithConsole.exe')); handel.stdout.on('data', chunk => { const str = chunk.toString();
// 约定 ##数据## 的字符串为通讯数据
let res = str.match(/##([\S\s]*)##/g);
if (!Array.isArray(res)) return;
res = res[0].match(/(?<=(\?))(.*)(?=(\sHTTP\/1.1))/);
if (!Array.isArray(res)) return;
console.log('[stdout queryString]', res[0]); });
function query(param, cb) {
http.get(`http://127.0.0.1:8899/?${(new URLSearchParams(param)).toString()}`, res => { res.on('data', chunk => { cb(chunk.toString()); });
}); }
query({ name: 'anan', age: 29, time: Date.now() }, httpRawTxt => { console.log('[http response]', httpRawTxt); });
// 监听 Ctrl + c process.on('SIGINT', () => { handel.kill(); process.exit(0); });

测试一下

  • 当然程序不会自己停下来哈,毕竟子进程的 http 服务一直在运行!
    $ node test.js [stdout queryString] name=anan&age=29&time=1595134635733 [http response] 2020-07-19 12:57:15


本文转载自 https://www.jianshu.com/p/9ac4f9ef9625

收起阅读 »

分享4个Linux中Node.js的进程管理器

Node.js进程管理器是一个有用的工具,可以确保Node.js进程或脚本连续(永久)运行,并使其能够在系统引导时自动启动。它允许您监视正在运行的服务,它有助于执行常见的系统管理任务(例如重新启动失败,停止,重新加载配置而无需停机,修改环境变量/设置,显示性能...
继续阅读 »

Node.js进程管理器是一个有用的工具,可以确保Node.js进程或脚本连续(永久)运行,并使其能够在系统引导时自动启动。

它允许您监视正在运行的服务,它有助于执行常见的系统管理任务(例如重新启动失败,停止,重新加载配置而无需停机,修改环境变量/设置,显示性能指标等等)。它还支持应用程序日志记录,群集和负载平衡,以及许多其他有用的流程管理功能。

另请参阅:2019年为开发人员提供的14个最佳NodeJS框架

包管理器尤其适用于在生产环境中部署Node.js应用程序。 在本文中,我们将回顾Linux系统中Node.js应用程序管理的四个进程管理器。

1. PM2

PM2是一个开源,高级,功能丰富,跨平台和最流行的Node.js生产级流程管理器,内置负载均衡器。它允许您列出,监视和处理所有已启动的Nodejs进程,并支持群集模式。


安装PM2以在Linux中运行Nodejs应用程序

它支持应用程序监视:提供一种监视应用程序资源(内存和CPU)使用情况的简单方法。它支持您的流程管理工作流,允许您通过流程文件配置和调整每个应用程序的行为(支持的格式包括Javascript,JSON和YAML)。

应用程序日志始终是生产环境中的关键,在这方面,PM2允许您轻松管理应用程序的日志。它提供了分别处理和显示日志的不同方式和格式。您可以实时显示日志,刷新日志,并在需要时重新加载日志。

重要的是,PM2支持启动脚本,您可以将其配置为在预期或意外的计算机重新启动时自动启动进程。它还支持在当前目录或其子目录中修改文件时自动重新启动应用程序。

此外,PM2还带有一个模块系统,允许用户为Nodejs进程管理创建自定义模块。例如,您可以为日志轮换模块或负载平衡创建模块等等。

最后但同样重要的是,如果您使用Docker容器,PM2允许容器集成,并提供允许您以编程方式使用它的API系统。

2. StrongLoop PM

StrongLoop PM也是一个开源的高级生产过程管理器,用于Node.js应用程序,内置负载平衡,就像PM2一样,它可以通过命令行或图形界面使用。


用于Nodejs的StrongLoop PM进程管理器

它支持应用程序监视(查看性能指标,如事件循环时间、CPU和内存消耗)、多主机部署、集群模式、零停机应用程序重启和升级、故障时自动进程重启以及日志聚合和管理。

此外,它附带Docker支持,允许您将性能指标导出到与状态兼容的服务器,并在第三方控制台(如DataDog、石墨、Splunk以及Syslog和原始日志文件)中查看。

3. Forever

Forever是一个开源,简单且可配置的命令行界面工具,可以连续(Forever)运行给定的脚本。它适用于运行Node.js应用程序和脚本的较小部署。您可以通过两种方式永久使用:通过命令行或将其嵌入代码中。


Forever运行脚本

它允许您管理(启动,列出,停止,停止所有,重新启动,重新启动所有等等。)Node.js进程,它支持监视文件更改,调试模式,应用程序日志,终止进程和退出信号自定义等等。此外,它还支持多种使用选项,您可以直接从命令行传递或将它们传递到JSON文件中。

4. Systemd - 服务和系统管理器

在Linux中,Systemd是一个守护程序,用于管理系统资源,例如进程和文件系统的其他组件。 systemd管理的任何资源都称为一个单元。有不同类型的单元,包括服务,设备,插座,安装,目标和许多其他单元。

Systemd通过称为单元文件的配置文件管理单元。因此,为了像任何其他系统服务一样管理Node.js服务器,您需要为它创建一个单元文件,在这种情况下它将是一个服务文件。

为Node.js服务器创建服务文件后,可以启动它,启用它以在系统引导时自动启动,检查其状态,重新启动(停止并再次启动它)或重新加载其配置,甚至像任何其他系统服务一样停止它。

摘要

Node.js包管理器是在生产环境中部署项目的有用工具。它使应用程序永远存在,并简化了如何控制它。在本文中,我们回顾了Node.js的四个包管理器。如果您有任何疑问或问题,请使用下面的反馈表与我们联系。

本文转载自: https://www.jianshu.com/p/ee49e600dd16
收起阅读 »

Node.js编写组件的几种方式

本文主要备忘为Node.js编写组件的三种实现:纯js实现、v8 API实现(同步&异步)、借助swig框架实现。简介(1)v8 API方式为官方提供的原生方法,功能强大而完善,缺点是需要熟悉v8 API,编写起来比较麻烦,是js强相关的,不容易支持其...
继续阅读 »

Node.js编写组件的几种方式

本文主要备忘为Node.js编写组件的三种实现:纯js实现、v8 API实现(同步&异步)、借助swig框架实现。

关键字:Node.js、C++、v8、swig、异步、回调。

简介

首先介绍使用v8 API跟使用swig框架的不同:

(1)v8 API方式为官方提供的原生方法,功能强大而完善,缺点是需要熟悉v8 API,编写起来比较麻烦,是js强相关的,不容易支持其它脚本语言。

(2)swig为第三方支持,一个强大的组件开发工具,支持为python、lua、js等多种常见脚本语言生成C++组件包装代码,swig使用者只需要编写C++代码和swig配置文件即可开发各种脚本语言的C++组件,不需要了解各种脚本语言的组件开发框架,缺点是不支持javascript的回调,文档和demo代码不完善,使用者不多。

二、纯JS实现Node.js组件

(1)到helloworld目录下执行npm init 初始化package.json,各种选项先不管,默认即可,更多package.json信息参见:https://docs.npmjs.com/files/package.json

(2)组件的实现index.js,例如:

module.exports.Hello = function(name) {
console.log('Hello ' + name);
}

(3)在外层目录执行:npm install ./helloworld,helloworld于是安装到了node_modules目录中。

(4)编写组件使用代码:

var m = require('helloworld');
m.Hello('zhangsan'); //输出: Hello zhangsan

三、 使用v8 API实现JS组件——同步模式

(1)编写binding.gyp, eg:

    { "target_name": "hello", "sources": [ "hello.cpp" ]
}
]
}

关于binding.gyp的更多信息参见:https://github.com/nodejs/node-gyp

(2)编写组件的实现hello.cpp,eg:

#include <node.h>

namespace cpphello { using v8::FunctionCallbackInfo; using v8::Isolate; using v8::Local; using v8::Object; using v8::String; using v8::Value; void Foo(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(isolate, "Hello World"));
} void Init(Local<Object> exports) {
NODE_SET_METHOD(exports, "foo", Foo);
}

NODE_MODULE(cpphello, Init)
}

(3)编译组件

node-gyp configure
node-gyp build

./build/Release/目录下会生成hello.node模块。

(4)编写测试js代码

const m = require('./build/Release/hello')
console.log(m.foo()); //输出 Hello World

(5)增加package.json 用于安装 eg:

{ "name": "hello", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "node test.js" }, "author": "", "license": "ISC" }

(5)安装组件到node_modules

进入到组件目录的上级目录,执行:npm install ./helloc //注:helloc为组件目录

会在当前目录下的node_modules目录下安装hello模块,测试代码这样子写:

var m = require('hello');
console.log(m.foo());

四、 使用v8 API实现JS组件——异步模式

上面三的demo描述的是同步组件,foo()是一个同步函数,也就是foo()函数的调用者需要等待foo()函数执行完才能往下走,当foo()函数是一个有IO耗时操作的函数时,异步的foo()函数可以减少阻塞等待,提高整体性能。

异步组件的实现只需要关注libuv的uv_queue_work API,组件实现时,除了主体代码hello.cpp和组件使用者代码,其它部分都与上面三的demo一致。

hello.cpp:

/* * Node.js cpp Addons demo: async call and call back.
* gcc 4.8.2
* author:cswuyg
* Date:2016.02.22
* */
#include <iostream> #include <node.h> #include <uv.h> #include <sstream> #include <unistd.h> #include <pthread.h>

namespace cpphello { using v8::FunctionCallbackInfo; using v8::Function; using v8::Isolate; using v8::Local; using v8::Object; using v8::Value; using v8::Exception; using v8::Persistent; using v8::HandleScope; using v8::Integer; using v8::String; // async task
struct MyTask{
uv_work_t work; int a{0}; int b{0}; int output{0};
unsigned long long work_tid{0};
unsigned long long main_tid{0};
Persistent<Function> callback;
}; // async function
void query_async(uv_work_t* work) {
MyTask* task = (MyTask*)work->data;
task->output = task->a + task->b;
task->work_tid = pthread_self();
usleep(1000 * 1000 * 1); // 1 second
} // async complete callback
void query_finish(uv_work_t* work, int status) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope handle_scope(isolate);
MyTask* task = (MyTask*)work->data; const unsigned int argc = 3;
std::stringstream stream;
stream << task->main_tid;
std::string main_tid_s{stream.str()};
stream.str("");
stream << task->work_tid;
std::string work_tid_s{stream.str()};

Local<Value> argv[argc] = {
Integer::New(isolate, task->output),
String::NewFromUtf8(isolate, main_tid_s.c_str()),
String::NewFromUtf8(isolate, work_tid_s.c_str())
};
Local<Function>::New(isolate, task->callback)->Call(isolate->GetCurrentContext()->Global(), argc, argv);
task->callback.Reset(); delete task;
} // async main
void async_foo(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
HandleScope handle_scope(isolate); if (args.Length() != 3) {
isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "arguments num : 3"))); return;
} if (!args[0]->IsNumber() || !args[1]->IsNumber() || !args[2]->IsFunction()) {
isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "arguments error"))); return;
}
MyTask* my_task = new MyTask;
my_task->a = args[0]->ToInteger()->Value();
my_task->b = args[1]->ToInteger()->Value();
my_task->callback.Reset(isolate, Local<Function>::Cast(args[2]));
my_task->work.data = my_task;
my_task->main_tid = pthread_self();
uv_loop_t *loop = uv_default_loop();
uv_queue_work(loop, &my_task->work, query_async, query_finish);
} void Init(Local<Object> exports) {
NODE_SET_METHOD(exports, "foo", async_foo);
}

NODE_MODULE(cpphello, Init)
}

异步的思路很简单,实现一个工作函数、一个完成函数、一个承载数据跨线程传输的结构体,调用uv_queue_work即可。难点是对v8 数据结构、API的熟悉。

test.js

// test helloUV module
'use strict';
const m = require('helloUV')

m.foo(1, 2, (a, b, c)=>{
console.log('finish job:' + a);
console.log('main thread:' + b);
console.log('work thread:' + c);
}); /* output:
finish job:3
main thread:139660941432640
work thread:139660876334848 */

五、swig-javascript 实现Node.js组件

利用swig框架编写Node.js组件

(1)编写好组件的实现:.h和.cpp **

eg:

namespace a { class A{ public: int add(int a, int y);
}; int add(int x, int y);
}

(2)编写.i,用于生成swig的包装cpp文件*

eg:

/* File : IExport.i */

%module my_mod

%include "typemaps.i"

%include "std_string.i"

%include "std_vector.i"

%{

#include "export.h"

%}

%apply int *OUTPUT { int *result, int* xx};

%apply std::string *OUTPUT { std::string* result, std::string* yy };

%apply std::string &OUTPUT { std::string& result };

%include "export.h"

namespace std {

%template(vectori) vector<int>;

%template(vectorstr) vector<std::string>;

};

上面的%apply表示代码中的 int* result、int* xx、std::string* result、std::string* yy、std::string& result是输出描述,这是typemap,是一种替换。

C++导出函数返回值一般定义为void,函数参数中的指针参数,如果是返回值的(通过*.i文件中的OUTPUT指定),swig都会把他们处理为JS函数的返回值,如果有多个指针,则JS函数的返回值是list。

%template(vectori) vector 则表示为JS定义了一个类型vectori,这一般是C++函数用到vector 作为参数或者返回值,在编写js代码时,需要用到它。

swig支持的更多的stl类型参见:https://github.com/swig/swig/tree/master/Lib/javascript/v8

(3)编写binding.gyp,用于使用node-gyp编译

**(4)生成warpper cpp文件 **生成时注意v8版本信息,eg:swig -javascript -node -c++ -DV8_VERSION=0x040599 example.i

(5)编译&测试

难点在于stl类型、自定义类型的使用,这方面官方文档太少。

六、其它

在使用v8 API实现Node.js组件时,可以发现跟实现Lua组件的相似之处,Lua有状态机,Node有Isolate。

Node实现对象导出时,需要实现一个构造函数,并为它增加“成员函数”,最后把构造函数导出为类名。Lua实现对象导出时,也需要实现一个创建对象的工厂函数,也需要把“成员函数”们加到table中。最后把工厂函数导出。

Node的js脚本有new关键字,Lua没有,所以Lua对外只提供对象工厂用于创建对象,而Node可以提供对象工厂或者类封装。


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

收起阅读 »

从零打造node.js版scf客户端

node.js是一个划时代的技术,它在原有的Web前端和后端技术的基础上总结并提炼出了许多新的概念和方法,堪称是十多年来Web开发经验的集大成者。转转公司在使用node.js方面,一起走在前沿。8月16日,转转公司的FE王澍老师,在镜泊湖会议室进行了一场主题为...
继续阅读 »

node.js是一个划时代的技术,它在原有的Web前端和后端技术的基础上总结并提炼出了许多新的概念和方法,堪称是十多年来Web开发经验的集大成者。转转公司在使用node.js方面,一起走在前沿。8月16日,转转公司的FE王澍老师,在镜泊湖会议室进行了一场主题为《nodejs全栈之路》的讲座。优秀的语言、平台、工具只有在优秀的程序员的手中才能显现出它的威力。一直听说转转公司在走精英化发展战略,所以学习下转转对node.js的使用方式,就显得很有必要。
对于大多数人使用node.js上的直观感受,就是模块、工具很齐全,要什么有什么。简单request一下模块,就可以开始写javasript代码了。然而出自58同城的转转,同样存在大量服务,使用着58自有的rpc框架scf。scf无论从设计还是实际效果,都算得上业内领先。只不过在跨平台的基础建设上,略显不足。从反编译的源码中,可以找到支持的平台有.net、java、c、php。非java平台的scf版本更新,也有些滞后。之前还听说肖指导管理的应用服务部,以“兼职”的方式开发过c++版客户端。而且也得到umcwrite等服务的实际运用。所以node.js解决好调用scf服务,是真正广泛应用的前提。这也正是我最关心的问题。
王澍老自己的演讲过程并没有介绍scf调用的解决方案,但在提问环节中,进行了解答。我能记住的内容是,目前的采用的方案是使用node-java模块,启动一个jvm进程,最终还是在node.js的项目中编写的java代码,性能尚可接受,但使用中内存占用很大;王澍老师也在尝试自己使用c++开发模块来弃用node-java。
这确实很让我很失望,我所理解的node.js应该是与性能有关的部分,几乎全部是c++编写的。之前肖指导要求发布公共服务,改写成使用scf提供的异步方式执行,借那次机会,我也阅读了一部分反编译的scf源码。感觉如果只是解决node.js调用scf的问题,不应该是个很难的事情。像管理平台、先知等外围功能,可以后期一点点加入。正巧我一直在质疑自己是不是基础差的问题,干脆写一个node.js版的scf客户端,来试试自己的水准。
结合自己之前对node.js的零散知识(其实现在也很零散)。对这次实践提出如下的一些设计要点:
1、序列化版本使用scfv3,虽然难度应该是最大的,但应该能在较长的时间内避免升级序列化版本的琐事。
2、使用管理平台读取配置,禁用scf.config类似的本地配置。想想之前许多部门,推进禁用线上服务直连的过程,就觉得很有必要(管理平台也用线下环境,线下调试根本不是阻碍)。
3、客户端支持全类型,之前偶尔听说了c++版客户端不支持枚举类型,使得有些服务只能调整接口。
4、c++使用libuv库,具备跨平台开发、调试能力。c++版客户端听说只支持linux平台。
5、只提供异步接口,这是当然的,不然node.js就别想用了。

现有的c++客户端,在3、4、5上与我的设想不符合,所以我决定亲自编写。
先是搜了本介绍libuv的pdf——《An Introduction to libuv》,看了几天,对libuv的使用方式有所了解,用上的只有tcp相关接口。(比起java,node.js的资料还是少,介绍的也少有深入的,像这样的底层类库,资料就更少了)
在58作为rd,如果不是做ios,是少有配macbook的员工的。所以我本次是在windows上编写的。不得不说node.js就是霸道,自动安装时,默认全部安装最新的版本。这样在windows平台上编译c++时,就要求visual studio不能低于2015。
网上搜索c++开发node.js模块,基本总是能找到那个addon的示例。可能是由于v8引擎的接口也有过变化,addon的示例使用的类型、接口也存在几种,终于还是试出了自己可以编译过的了。
首先在addon的基础上,写个运用libuv连接tcp的逻辑,一旦试通了,就可以一点点抄写反编译的scf客户端源码了。

在开发过程中,我的设计也进行了一些修改:
1、反序列化逻辑,通过tcp连接,交由一个java程序来执行(基于netty开发)。由于反序列化时,scf的二进制数据是没有足够的类型信息的。大体上,当读取到一个typeid时,如果本地没有对应的类型信息,完全不知道下一个字节是做什么用的。(我其实只希望得到一个类似多叉树的嵌套格式,也做不到。)如果非要使用c++来执行反序列化,也并非不可能。需要将scf反序列化用到的类型信息,整理成一种新的数据格式,存放于c++程序的内存中。为此需要开发一个输出类型配置数据的java离线工具,node.js模块需要开发:读取这个类型配置文件到内存,再将scf反序列化的逻辑使用c++抄一遍。综上来看,使用一个java的反序列化辅助进程,可以在性能几乎无损的情况下,极大的减少了开发量,同时避免了许多反序列化过程中的bug。这不正是一个极简的微服务嘛。
2、javascript入参对象中,需要自带scf序列化相关的类型信息,这样就能在全类型的支持scf对象了。当然我也设想过,有没有机会将序列化,也交由java辅助进程。那样就需要设计一个java对象在javascript中的表示形式,由java辅助进程,先转换为java对象,再序列化。再加上两次额外tcp传输。在没有减少工作量的情况下,浪费了不少性能。当然如果十分拒绝c++开发的话,倒是能因此少写些c++代码。

后续可以做的一些事情:
1、完善的重连、超时处理;
2、管理平台配置热更新;
3、管理平台数据上报;
4、先知;
5、加密、压缩(似乎和node.js的非计算密集场景有些冲突,而且公司的scf配置默认都是关闭这两个的。scf良好的用了这么些年,不开启这两个功能的功劳应该也不小)

当然已开发的内容中,也一定满是bug。等有人用了,我再考虑改bug的事。生产环境下的试错机会,才能让程序真正成长。

收起阅读 »

React-Native与原生模块间的几种通信方式

原理string-NSStringnumber - int/NSInteger/float/double/NSNumberboolean - BOOL/NSNumberarray - NSArrayobject - NSDictionary(NSString型...
继续阅读 »

每种语言都有自己的设计理念、语法、运行环境,这也导致了不同语言间相互交流通信时必须要有中介来翻译,如JAVA与C/C++通过JNI来交流、OC与C/C++需要在.mm文件混编、而JAVA/OC与Lua通信时需要通过C/C++语言来做中介。那么在React-Native中JSX是如何与底层模块进行通信的呢?这里主要以iOS系统来做说明。

原理

通信本质上是信息的交流,具体到计算机语言则是数据的流动。应用中数据在React-Native与原生模块间的流动与共享,完成了与用户的交互,达成了应用的目标。React-Native与OC间通信的数据只能是下面的几种类型(前为JS类型,后为OC类型):

  • string-NSString
  • number - int/NSInteger/float/double/NSNumber
  • boolean - BOOL/NSNumber
  • array - NSArray
  • object - NSDictionary(NSString型key, value可以为这里的其它类型)
  • func - RCTResponseSenderBlock

其它类型的数据需要通过一定的规则转换成这几种类型后(一般都会转换成JSON串)再通信.

React-Native本质是通过JavaScriptCore.framework实现JS代码与OC代码间的互动。因此下面说的几种方式在本质原理上都是相同的,不同的地方只是在于实现形式与方法的差别。

函数调用

在将原生模块封装并提供给React-Native使用时,可以通过RCT_EXPORT_METHOD()宏向React-Native侧定义其可以调用的接口函数,完成两模块间的通信。

//定义了startVPN接口,React-Native将VPN的具体参数通过该接口传入到原生模块,开启指定的VPN
RCT_EXPORT_METHOD(startVPN:(NSDictionary*)config)
{
LSShadowSocksDataMode* mode = [[LSShadowSocksDataMode alloc] initWithDictionary:config];
[self.manager startVPN:mode];
}

除了传入数据外,通过可以通过这种方式从原生侧获取数据。最容易想到的是通过返回值获取,可惜的是RCT_EXPORT_METHOD宏不支持返回值,不过其提供了另外一种实现返回值的方式:

RCT_EXPORT_METHOD(isOpen:(RCTResponseSenderBlock)callback)
{
BOOL open = [self.manager status];
callback(@[[NSNull null], @[@(open)]]);
}

通过回调函数的形式实现返回值的效果,达到了数据交换的目的。

属性共享

这种方式主要针对于UI控件来说的。React-Native中最基础的UI类型是RCTRootView,该类有一个初始化方法initWithBridge:moduleName:initialProperties:,第三个参数initialProperties表示的是UI控件的初始属性值,类型为NSDictionary,其最终会被同步到由第二个参数定义的React-Native类的props中,即完成了两个模块间的数据交流。

NSArray *imageList = @[@"http://foo.com/bar1.png",
@"http://foo.com/bar2.png"];

NSDictionary *props = @{@"images" : imageList};

RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"ImageBrowserApp"
initialProperties:props];
import React, { Component } from 'react';
import {
AppRegistry,
View,
Image,
} from 'react-native';

class ImageBrowserApp extends Component {
renderImage(imgURI) {
return (
<Image source={{uri: imgURI}} />
);
}
render() {
return (
<View>
{this.props.images.map(this.renderImage)}
</View>
);
}
}

AppRegistry.registerComponent('ImageBrowserApp', () => ImageBrowserApp);

初始化接口只能在UI组件建立时使用,如果需要在UI组件的生命周期内通信呢,RCTRootView提供了appProperties这样一种机制:

NSArray *imageList = @[@"http://foo.com/bar3.png",
@"http://foo.com/bar4.png"];
rootView.appProperties = @{@"images" : imageList};

通知

OC中使用NSNotificationCenter向整个应用发送通知,所有对该通知感兴趣的对象都会获得该通知并执行相应的动作。React-Native中也提供有类似的机制:RCTEventEmitter。原生模块继承该类后,就可以向React-Native侧发送通知,而React-Native就能够接收到该通知,并处理一并传送过来的数据了。

-(void)vpnStatusChanged:(NSNotification*)notification
{
NEVPNStatus status = [self.manager status];
NSString* value = nil;
switch (status) {
case NEVPNStatusReasserting:
value = @"重新连接中";
break;
case NEVPNStatusConnecting:
value = @"连接中";
break;
case NEVPNStatusConnected:
value = @"已连接";
break;
case NEVPNStatusDisconnecting:
value = @"断开连接中";
break;
case NEVPNStatusDisconnected:
case NEVPNStatusInvalid:
value = @"末连接";
break;
default:
break;
}
if(value){
[self sendEventWithName:@"VpnStatus" body:@{@"status":value}];
}
}

这里将V**的状态通过通知发送到React-Native侧,由React-Native将V**的状态显示的UI界面上。


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

收起阅读 »

为抵制 7-Zip,列出 “三宗罪” ?网友:“第3个才是重点吧?”

谈及电脑必装软件有哪些时,压缩软件绝对算一个。由于各人需求不同,其选择的压缩软件也不尽相同,如 WinRAR、360 压缩、7-Zip、BandiZip、快压等,其中完全免费且开源的 7-Zip 就深受许多用户青睐。作为一款开源压缩软件,7-Zip 发布于 1...
继续阅读 »
谈及电脑必装软件有哪些时,压缩软件绝对算一个。由于各人需求不同,其选择的压缩软件也不尽相同,如 WinRAR、360 压缩、7-Zip、BandiZip、快压等,其中完全免费且开源的 7-Zip 就深受许多用户青睐。
作为一款开源压缩软件,7-Zip 发布于 1999 年,大多数源代码都基于 GNU LGPL 许可协议下发布,使用了 LZMA 与 LZMA2 算法使其拥有极高的压缩比,小巧的体积也是一大优势。

(图片来自7-Zip中文官网)
然而,近日一位名为 Paul 的开发者却发表了一篇呼吁抵制 7-Zip 的文章,其标题给出的理由是:“有限”的开源 & 安全问题


Paul 给 7-Zip 定下“三宗罪”

然而,在整体看过这篇文章后便可发现,Paul 给 7-Zip 定下的是“三宗罪”。
  • 第一宗罪:“有限”的开源

正如开头所说,大多 7-Zip 的源码均基于 GNU LGPL 许可协议发布,其开源属性理应毋庸置疑。
而 Paul 认为 7-Zip 开源“有限”的点在于:7-Zip 的代码没有托管在 Github、Gitlab 或其他任何公共代码托管平台上,只能在其官方 Sourceforge 页面的 src.7z 中找到,而且“没有历史、没有提交者、没有名字、没有文档,只是一个存档”。
关于这个唯一托管了 7-Zip 源码的 Sourceforge 平台,Paul 直言其声誉不好:“Sourceforge 曾被指控在 Windows.exe 文件和自解压文件中包含间谍软件和恶意软件。”
而对于“没有历史、没有提交者、没有名字、没有文档”这点,Paul 也揣测道,这可能是因为 7-Zip 的作者不希望开发者通过源码构建应用,有了提交历史将更容易跟踪任何更改和恢复任何错误的部分,同时也更容易运输一些“隐藏的黑暗元素”,如隐藏的遥测或后门。
  • 第二宗罪:存在安全问题

在 Paul 看来,7-Zip 不仅过去存在许多漏洞,此前曝出的提权漏洞 CVE-2022-29072 至今也仍未修复,明显存在安全隐患。Paul 还引用了 2012 年作者回应用户建议的言论:“现在没有时间做这些事情,也许以后我会看看。”
除此之外,Paul 还指出 7-Zip 的安装程序似乎从未设置签名——“签名可验证供应商并防止坏人安装软件”。
  • 第三宗罪:软件作者是俄罗斯开发者

抵制 7-Zip 的第三个理由 Paul 没有在标题中体现:7-Zip 是由俄罗斯开发者 Igor Pavlov 所开发的,“当前局势下为了声援乌克兰,最好不要使用俄罗斯软件”。
在举出以上“三宗罪”后,Paul 最后还推荐了一些 7-Zip 的替代品,如 PeaZip、NanaZip,还有与 7-Zip 相当的 Zstd(Zstandard)等。

网友:“一些阴谋论罢了”

Paul 这番抵制 7-Zip 的言论在 reddit 论坛上引起了不少讨论,但从评论情况来看,Paul 的目的没有达成:大多数人都认为 Paul 的理由站不住脚,并抨击 Paul 的“阴谋论”。
  
讨论帖中,点赞数最高的是一位名为 qvop 网友的评论:
即便 7-Zip 源码没有在 Github、Gitlab 等平台上托管,那又怎样?它仍然是开源的,没有任何规定要求开源就一定要在某些特定平台上托管代码,我看是 Paul 自己的认知有问题。
实际上,7-Zip 在 Sourceforge 上的源码是有一些(相对较少的)文档的,包括变更日志和关于如何编译程序及其一些内部工作的描述。而且,如果开发者只想单独开发、不想寻求贡献,那么这些不必需的东西开源也没用
Paul 认为 7-Zip 作者故意不让开发者通过源码构建应用的说法也几乎是“阴谋论”,因为目前没有任何证据支持这一说法,相反 7-Zip 方面有超过 20 年的开发和维护记录。
此外,因为 7-Zip 作者的国籍而放弃使用开源软件更是愚蠢至极,尤其目前没有任何迹象表明其作者有何相关冲突立场。
总而言之,对我来说,这篇文章就是一个大杂烩,其中还掺杂着一些权利和阴谋论。
除此之外,许多网友也对 Paul 发表的这篇博文予以讽刺:“一句话总结就是,发帖者不喜欢 7-Zip的作者名字”、“不为这篇文章辩护,作者就是个白痴”、“我会继续使用它的,谢谢,我没有看到任何停止使用它的理由”。
“开源无国界”一直是开源界所呼吁的口号,然而在当前国际形势下,这句口号似乎已有些站不住脚了:GitHub 封禁俄罗斯开发者账户、起家于俄罗斯的 NGINX 开源项目宣布禁俄……这些事件本就令众多开源爱好者对“开源”的本质提出质疑,Paul 呼吁抵制 7-Zip 的第三个理由更是令许多人无法理解:“难道说以后我们在选择使用开源软件时要考虑作者国籍吗?这真的很奇怪。”
那么,你对于 Paul 的言论有何看法?你平时常用的压缩软件又是什么呢?
参考链接:

整理 | 郑丽媛、出品 | CSDN(ID:CSDNnews)

收起阅读 »

清除 useEffect 副作用

web
在 React 组件中,我们会在 useEffect() 中执行方法,并返回一个函数用于清除它带来的副作用影响。以下是我们业务中的一个场景,该自定义 Hooks 用于每隔 2s 调用接口更新数据。import { useState, useEffect } f...
继续阅读 »

在 React 组件中,我们会在 useEffect() 中执行方法,并返回一个函数用于清除它带来的副作用影响。以下是我们业务中的一个场景,该自定义 Hooks 用于每隔 2s 调用接口更新数据。

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   const id = setInterval(async () => {
     const data = await fetchData();
     setList(list => list.concat(data));
  }, 2000);
   return () => clearInterval(id);
}, [fetchData]);

 return list;
}

🐚 问题

该方法的问题在于没有考虑到 fetchData() 方法的执行时间,如果它的执行时间超过 2s 的话,那就会造成轮询任务的堆积。而且后续也有需求把这个定时时间动态化,由服务端下发间隔时间,降低服务端压力。

所以这里我们可以考虑使用 setTimeout 来替换 setInterval。由于每次都是上一次请求完成之后再设置延迟时间,确保了他们不会堆积。以下是修改后的代码。

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let id;
   async function getList() {
     const data = await fetchData();
     setList(list => list.concat(data));
     id = setTimeout(getList, 2000);
  }
   getList();
   return () => clearTimeout(id);
}, [fetchData]);

 return list;
}

不过改成 setTimeout 之后会引来新的问题。由于下一次的 setTimeout 执行需要等待 fetchData() 完成之后才会执行。如果在 fetchData() 还没有结束的时候我们就卸载组件的话,此时 clearTimeout() 只能无意义的清除当前执行时的回调,fetchData() 后调用 getList() 创建的新的延迟回调还是会继续执行。

在线示例:CodeSandbox


可以看到在点击按钮隐藏组件之后,接口请求次数还是在继续增加着。那么要如何解决这个问题?以下提供了几种解决方案。

🌟如何解决

🐋 Promise Effect

该问题的原因是 Promise 执行过程中,无法取消后续还没有定义的 setTimeout() 导致的。所以最开始想到的就是我们不应该直接对 timeoutID 进行记录,而是应该向上记录整个逻辑的 Promise 对象。当 Promise 执行完成之后我们再清除 timeout,保证我们每次都能确切的清除掉任务。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let getListPromise;
   async function getList() {
     const data = await fetchData();
     setList((list) => list.concat(data));
     return setTimeout(() => {
       getListPromise = getList();
    }, 2000);
  }

   getListPromise = getList();
   return () => {
     getListPromise.then((id) => clearTimeout(id));
  };
}, [fetchData]);
 return list;
}

🐳 AbortController

上面的方案能比较好的解决问题,但是在组件卸载的时候 Promise 任务还在执行,会造成资源的浪费。其实我们换个思路想一下,Promise 异步请求对于组件来说应该也是副作用,也是需要”清除“的。只要清除了 Promise 任务,后续的流程自然不会执行,就不会有这个问题了。

清除 Promise 目前可以利用 AbortController 来实现,我们通过在卸载回调中执行 controller.abort() 方法,最终让代码走到 Reject 逻辑中,阻止了后续的代码执行。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

function fetchDataWithAbort({ fetchData, signal }) {
 if (signal.aborted) {
   return Promise.reject("aborted");
}
 return new Promise((resolve, reject) => {
   fetchData().then(resolve, reject);
   signal.addEventListener("aborted", () => {
     reject("aborted");
  });
});
}
function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let id;
   const controller = new AbortController();
   async function getList() {
     try {
       const data = await fetchDataWithAbort({ fetchData, signal: controller.signal });
       setList(list => list.concat(data));
       id = setTimeout(getList, 2000);
    } catch(e) {
       console.error(e);
    }
  }
   getList();
   return () => {
     clearTimeout(id);
     controller.abort();
  };
}, [fetchData]);

 return list;
}

🐬 状态标记

上面一种方案,我们的本质是让异步请求抛错,中断了后续代码的执行。那是不是我设置一个标记变量,标记是非卸载状态才执行后续的逻辑也可以呢?所以该方案应运而生。

定义了一个 unmounted 变量,如果在卸载回调中标记其为 true。在异步任务后判断如果 unmounted === true 的话就不走后续的逻辑来实现类似的效果。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let id;
   let unmounted;
   async function getList() {
     const data = await fetchData();
     if(unmounted) {
       return;
    }

     setList(list => list.concat(data));
     id = setTimeout(getList, 2000);
  }
   getList();
   return () => {
     unmounted = true;
     clearTimeout(id);
  }
}, [fetchData]);

 return list;
}

🎃 后记

问题的本质是一个长时间的异步任务在过程中的时候组件卸载后如何清除后续的副作用。

这个其实不仅仅局限在本文的 Case 中,我们大家平常经常写的在 useEffect 中请求接口,返回后更新 State 的逻辑也会存在类似的问题。

只是由于在一个已卸载组件中 setState 并没有什么效果,在用户层面无感知。而且 React 会帮助我们识别该场景,如果已卸载组件再做 setState 操作的话,会有 Warning 提示。


再加上一般异步请求都比较快,所以大家也不会注意到这个问题。

所以大家还有什么其他的解决方法解决这个问题吗?欢迎评论留言~


作者:lizheming
链接:juejin.cn/post/7057897311187238919

收起阅读 »

如何用一个插件解决 Serverless 灰度发布难题?

web
Serverless 灰度发布什么是 Serverless ?Serverless 顾名思义就是无服务器,它是一种“来了就用,功能齐全,用完即走”的全新计算提供方式,用户无需预制或管理服务器即可运行代码,只需将代码从部署在服务器上,转换到部署到各厂商 Serv...
继续阅读 »

Serverless 灰度发布

什么是 Serverless ?

Serverless 顾名思义就是无服务器,它是一种“来了就用,功能齐全,用完即走”的全新计算提供方式,用户无需预制或管理服务器即可运行代码,只需将代码从部署在服务器上,转换到部署到各厂商 Serverless 平台上;同时享受 Serverless 按需付费,高弹性,低运维成本,事件驱动,降本提效等优势。

什么是 Serverless 灰度发布?

灰度发布又称为金丝雀发布( Canary Deployment )。过去,矿工们下矿井前,会先放一只金丝雀到井内,如果金丝雀在矿井内没有因缺氧、气体中毒而死亡后,矿工们才会下井工作,可以说金丝雀保护了工人们的生命。

与此类似,在软件开发过程中,也有一只金丝雀,也就是灰度发布(Gray release):开发者会先将新开发的功能对部分用户开放,当新功能在这部分用户中能够平稳运行并且反馈正面后,才会把新功能开放给所有用户。金丝雀发布就是从不发布,然后逐渐过渡到正式发布的一个过程。

那么对于部署在 Serverless 平台上的函数应该怎么进行灰度发布呢?

下文将以阿里云函数计算 FC 为例,为各位展开介绍。

灰度发布有一个流程,两种方式。

一个流程

Serverless 灰度发布是通过配置别名来实现的,别名可以配置灰度版本和主版本的流量比例,在调用函数时使用配置好的别名即可将流量按比例发送到相应版本。


配置灰度发布的流程如下:

  1. Service 中发布一个新版本。

  2. 创建或更新别名,配置别名关联新版本和稳定版本,新版本即为灰度版本。

  3. 将触发器 ( Trigger ) 关联到别名。

  4. 将自定义域名 ( Custom Domain ) 关联到别名。

  5. 在调用函数中使用别名,流量会按配置比例发送到新版本和稳定版本。

传统做法的两种方式

1、阿里云控制台 web 界面:

a.发布版本


b.创建别名


c.关联触发器


d.关联自定义域名


2、使用 Serverless Devs cli

a.发布版本

s cli fc version publish --region cn-hangzhou --service-name fc-deploy-service --description "test publish version"

b.创建别名并设置灰度

s cli fc alias publish --region cn-hangzhou --service-name fc-deploy-service --alias-name pre --version-id 1 --gversion 3 --weight 20

c.关联触发器

需要到控制台配置

d.关联自定义域名

需要到控制台配置可以看到,使用控制台或 Serverless Devs 进行灰度发布流程中的每一步,都需要用户亲自操作。并且由于配置繁多,极易出错。除了这些弊端以外,客户困扰的另一个问题是使用灰度发布策略非常不方便。

常见的灰度发布策略有 5 种:

    1. CanaryStep: 灰度发布,先灰度指定流量,间隔指定时间后再灰度剩余流量。

    2. LinearStep:分批发布,每批固定流量,间隔指定时间后再开始下一个批次。

    3. CanaryPlans:自定义灰度批次,每批次设定灰度流量和间隔时间,间隔指定时间后按照设定的流量进行灰度。

    4. CanaryWeight:手动灰度,直接对灰度版本设置对应的权重。

    5. FullWeight: 全量发布,全量发布到某一版本。

这些灰度策略中,前三项都需要配置间隔时间,而用户在控制台或者使用 Serverless Devs 工具去配置灰度都没有办法通过自动程序来配置间隔时间,不得不通过闹钟等方式提醒用户手动进行下一步灰度流程,这个体验是非常不友好的。下面我们介绍个能够帮您一键灰度发布函数的插件:FC-Canary 。

基于 Serverless Devs 插件 FC-Canary 的灰度发布

为了应对以上问题,基于 Serverless Devs 的插件 FC-Canary 应运而生,该插件可以帮助您通过 Serverless-Devs 工具和 FC 组件实现函数的灰度发布能力,有效解决灰度发布时参数配置繁杂、需要开发人员亲自操作以及可用策略少等问题。


(内容配置及注意事项-部分截图)

详细流程请见:github.com/devsapp/fc-…

FC-Canary 的优势

1、FC-Canary 支持超简配置

用户最短只需在 s.yaml 中增加 5 行代码代码即可开启灰度发布功能。


2、FC-Canary 配置指引简单清晰:


3、FC-Canary 支持多种灰度策略

  • 灰度发布,先灰度指定流量,间隔指定时间后再灰度剩余流量。

此时流量变化为:20%流量到新版本,10 分钟后 100%流量到新版本


  • 手动灰度,指定时直接将灰度版本设置对应的权重。

此时为 10%流量到新版本,90%到稳定版本


  • 自定义灰度,以数组的方式配置灰度变化。

此时流量变化为:10%到新版本 -> (5 分钟后) 30% 流量到新版本 -> (10 分钟后) 100% 流量到新版本


  • 分批发布,不断累加灰度比例直到 100%流量到新版本。

流量变化:40%到新版本 -> (10 分钟后) 80%流量到新版本 -> (再 10 分钟后) 100%流量到新版本


  • 全量发布,100%流量发到新版本


FC-Canary 插件支持上述 5 种灰度策略,用户选择所需策略并进行简单配置,即可体验一键灰度发布。

4、FC-Canary 灰度阶段提示清晰


插件对每一个里程碑都会以 log 的方式展现出来,给开发者足够的信心。

5、FC-Canary 支持钉钉群组机器人提醒


配置钉钉机器人即可在群中收到相关提醒,例如:


FC-Canary 最佳实践

使用 FC-Canary 插件灰度发布 nodejs 12 的函数。

代码仓库:

github.com/devsapp/fc-…

初始化配置

  • 代码配置


  • yaml 配置


我们采用 canaryWeight 的灰度策略:灰度发布后,50%的流量到新版本,50%的流量到旧版本。

进行第一次发布

  1. 执行发布

在 terminal 中输入: s deploy --use-local

  1. 查看结果

命令行输出的 log 中可以看到:


由于是第一次发布,项目中不存在历史版本,所以即使配置了灰度发布策略 ,FC-Canary 插件也会进行全量发布,即流量都发送到版本 1。

修改代码,第二次发布

  1. 在第二次发布前,我们修改一下代码,让代码抛出错误。


  1. 执行发布

在terminal中输入: s deploy --use-local

  1. 结果

命令行输出 log 中可以看到:


第二次发布,应用了灰度发布策略,即 50%流量发送到版本 1, 50%的流量发送到版本 2。

测试

获取 log 中输出的 domain,访问 domain 100 次后查看控制台监控大盘。


可以看到调用了函数 100 次,错误的函数有 49 次,正确的函数有 100 - 49 = 51 次,正确和错误的函数都约占总调用数的 50%。

分析:函数版本 1 为正确函数,函数版本 2 为错误函数,我们的灰度配置为流量 50% 到版本 1,50% 到版本 2,所以调用过程中,错误函数和正确函数应该各占 50%,图中结果符合我们的假设,这证明我们的灰度策略是成功的。

总结

我们可以发现相比使用控制台进行灰度发布,使用 FC-Canary 插件免去了用户手动创建版本、发布别名、关联触发器和管理自定义域名的麻烦,使用起来非常方便。

引申阅读

Serverless Devs 组件和插件的关系

  • 组件是什么?

根据 Serverless Devs Model v0.0.1 中说明, 组件 Component: 是由 Package developer 开发并发布的符合 Serverless Package Model 规范的一段代码,通常这段代码会在应用中被引用,并在 Serverless Devs 开发者工具中被加载,并按照预定的规则进行执行某些动作。例如,将用户的代码部署到 Serverless 平台;将 Serverless 应用进行构建和打包;对 Serverless 应用进行调试等。

举个例子:

如果想要使用 Serverless Devs 管理阿里云函数计算的函数计算资源,则需要在 yaml 配置文件中声明阿里云 FC 组件,之后便可以使用阿里云 FC 组件的能力。

FC 组件可以提供管理阿里云函数计算资源的能力,包括:管理服务、函数、版本、别名 等功能。组件地址:github.com/devsapp/fc

  • 插件是什么?

插件作为组件的补充,提供组件的原子性功能。

举个例子:

  1. 使用 FC 组件 deploy 的功能部署函数,可以在部署结束后采用 FC-Canary 插件对部署的函数进行灰度发布。

  2. 使用 FC 组件 deploy 的功能部署函数,可以在部署开始前采用 layer-fc 插件来降低部署过程中上传代码的耗时:即 layer-fc 可以让函数直接使用公共依赖库(远程)中的依赖,从而在部署时不再需要上传这些远程存在的依赖。

  • 组件和插件的关系?


  • 在 Serverless Devs Model 中,组件是占据核心地位,插件是辅助地位,也就是说,插件的目的是提升组件能力,提供给组件一些可选的原子性功能。

  • Serverless Devs 管理组件和插件的生命周期,如果是 pre 插件,则会让其在组件执行前执行,反之,post 插件则会在组件后完成一些收尾工作。

  • 一个组件可以同时使用多个插件, 其中组件插件的执行顺序是:

    1. 插件按照 yaml 顺序执行, 前一个插件的执行结果为后一个插件的入参

    2. 最后一个 pre 插件的输出作为组件的入参

    3. 组件的输出作为第一个 post 插件的入参

相关概念

  • FC 函数 (Function) 是系统调度和运行的单位,由函数代码和函数配置构成。FC 函数必须从属于服务,同一个服务下的所有函数共享一些相同的设置,例如服务授权、日志配置。函数的相关操作,请参见 管理函数。函数计算支持事件函数和 HTTP 函数两种函数类型,关于二者的区别,请参见 函数类型。

  • 服务 (Service) 可以和微服务对标 ( 有版本和别名 ),多个函数可以共同组成服务单元。创建函数前必须先创建服务,同一个服务下的所有函数共享一些相同的设置,例如服务授权、日志配置。

  • 触发器 (Trigger) 的作用是触发函数执行的。函数计算提供了一种事件驱动的计算模型。函数的执行可以通过函数计算控制台或 SDK 触发,也可以由其他一些事件源来触发。您可以在指定函数中创建触发器,该触发器描述了一组规则,当某个事件满足这些规则,事件源就会触发关联的函数。

  • 自定义域名(Custom Domain) 是函数计算提供为 Web 应用绑定域名的能力。

  • 版本 (Version) 是服务的快照,包括服务的配置、服务内的函数代码及函数配置,不包括触发器,当发布版本时,函数计算会为服务生成快照,并自动分配一个版本号与其关联,以供后续使用。

  • 别名 (Alias) 结合版本,帮助函数计算实现软件开发生命周期中的持续集成和发布。

最后,欢迎大家一起来贡献更多的开源插件!

参考链接:

Serverless Devs:

github.com/Serverless-…

FC 组件地址:

github.com/devsapp/fc

FC-Canary 插件具体信息及其使用请参考:

github.com/devsapp/fc-…

FC 函数管理:

help.aliyun.com/document_de…

FC 函数类型:

help.aliyun.com/document_de…


作者:长淇
来源:https://juejin.cn/post/7116556273662820382

收起阅读 »

Java VS .NET:Java与.NET的特点对比

为什么要写Java跟.NET对比?二、项目构建工欲善其事必先利其器。开发环境配置+工具使用当然要先讲了。平台工具ken.io的解释JavaIdea/EclipseIDE,负责管理项目以及代码的运行调试等,依赖于JDKJavaJDKJRE(Java项目运行环境)...
继续阅读 »

一、前言

为什么要写Java跟.NET对比?

.NET出生之后就带着Java的影子。从模仿到创新,.NET平台也越来越成熟。他们不同的支持者也经常因为孰弱孰强的问题争论不休。但是本文并不是为了一分高下。而是针对Java平台跟.NET平台做一些对比。主要围绕项目构建、Web框架、项目部署展开讨论。相信经过这些讨论可以让Java/.NET工程师对Java平台、.NET平台有更好的了解。

二、项目构建

项目构建工具

工欲善其事必先利其器。开发环境配置+工具使用当然要先讲了。

1、表面上的工具

平台工具ken.io的解释

.NETVisual Studio微软官方IDE,它具备了开发.NET应用程序的几乎所有工具

JavaIdea/EclipseIDE,负责管理项目以及代码的运行调试等,依赖于JDK

JavaMaven负责管理项目模板、打包(jar包等),依赖于JDK

JavaJDKJRE(Java项目运行环境),Java工具(编译器等)

.NET工程师要开展工作,安装Visual Studio(后面简称:VS)就可以进行开发了。但是Java开发,只安装IDE是不行的,就算某些IDE会自动安装JDK,甚至是Maven,但是这些还是需要自己配置,不然还可能会踩坑。从开发环境的配置来说,.NET工程师操作上确实简单一些,一直下一步,等待安装完成即可。Java工程师就先要了解下工具,以及各个工具的职责。然后逐一配置。

从这个点上来说,Java的入门曲线会稍陡一些,但是Java工程师也会比.NET工程师更早关注到项目构建的重要环节。

2、实际上的工具

职责.NET平台Java平台ken.io的解释

项目管理VSIDEA/Eclips.NET只有微软官方IDE,Java没有官方的IDE,没有VS好用,但是有多个选择

项目模板VS+MSBuildIDE+Maven.NET项目的模板是VS自带的,是直接符合MSBuild(编译器)标准的,项目由sln+csproj文件组织,Java平台编译器的标准是公开的,目前主流项目都是基于Maven模板来创建,项目由pom.xml文件组织。

编译&调试VS+MSBuild+SDKIDE+Maven+SDK.NET平台的编译器是独立的,Java平台的编译器是集成在JDK中,Maven模板的项目是由pom.xml文件组织,但是编译器并不是认识pom.xml,所以编译需要Maven的参与

Package管理NuGetMavenNuget是微软官方开源的VS插件,Maven是Apache下的开源项目。ken.io觉得Maven更灵活、强大。NuGet容易上手。

打包/发布VS+MSBuild+SDKIDE+Maven+SDK.NET平台的编译器是独立的,Java平台的编译器是集成在JDK中,Maven模板的项目是由pom.xml文件组织的,但是编译器并不是认识pom.xml,所以打包需要Maven的参与。IDE主要是提供图形化界面替代命令操作

从项目管理上说。VS这个IDE更好用一些,项目模板上,.NET项目模板由于有Visual Studio的存在,可以说简单易用而且丰富,Java平台的Maven模板灵活。

其实大部分差异都是编译器跟模板带来的差异。.NET平台的编译器是独立的,编译器MSBuild有一套标准, 而且Visual Studio提供了丰富好用的项目模板。

Java平台的编译器的编译配置是xml文档,由于Java官方没有项目模板,IDE只负责帮你组织项目,但是并没有模板,你可以将任意目录指定为SourceRoot(代码根目录),ResourceRoot(资源文件根目录:比如配置文件)也可以任意指定,编译的时候,IDE会将你的项目代码,以及编译器所需要的编译描述/配置xml文档告诉编译器该如何编译你的项目。确实非常灵活,但是也增加了项目管理的成本。包的管理也非常麻烦,还好有Maven结束了这个混沌的Java世界。

编码特点

—.NETJava

类的组织namespace:命名空间,name跟目录无关Package:name跟目录名一致

类.cs文件:类名跟文件名无关.java文件,类名跟文件名无关,但一个类文件只能定义一个public类

编译产出.dll,.exe文件.jar,.war文件

三、框架

.NET的Web框架基本上都是微软官方的,官方的框架也最为流行,而Java平台,除了官方提供的Servlet API(相当于.NET的System.Web)其他的基本都由Spring大家族统治了。本次我们主要对比目前Web开发最常用的MVC框架以及持久层框架

功能.NETJavaken.io的说明

Web核心ASP.NETServlet—

Web框架ASP.NET MVCSpring MVCASP.NET MVC是微软官方框架,Srping MVC框架隶属于Spring大家族,依赖于Spring

视图引擎RazorThymeleaf/FreeMarkerRazor是微软官方的视图引擎,非常好用,Spring MVC并没有视图引擎,但是有Thymeleaf,FreeMarker。ken.io更喜欢Razor的风格

持久层Entity FrameworkMyBatisEF是微软官方的持久层框架,易上手、开发效率高、但侵入性强。MyBatis配置灵活,无侵入性。各有利弊。

.NET平台的框架由于都是微软官方的,比较好组织,上手容易。Java平台的框架,灵活可配置。这也是Java平台一贯的风格。但是ken.io不得不吐槽的是,Spring MVC作为一个MVC框架,竟然没有自己的视图引擎,那MVC种的View去哪了?

可能是因为Java作为Web后端的主力平台,确实很少关注视图层,但是Spring MVC没有View层引擎,还是感觉不合适。Thymeleaf跟FreeMarker,ken.io更推荐FreeMarker。因为ken.io更喜欢FreeMaker的语法。可能是用惯了Razor的缘故。

四、项目部署

对于项目部署。.NET平台貌似没得选,只能选Windows+IIS,虽然有Mono,但毕竟不是支持所有的类库。而Java平台既可以选择Windows+Tomcat,也可以选择Linux+Tomcat。但是通常会选择Linux+Tomcat毕竟成本低。

职责.NETJava

操作系统Windows ServerWindows Server、Linux Server

Web服务器IISTomcat(Tomcat是目前最主流的,也有其他的Servlet容易例如:JBoss)

不过Java平台的特性,Java项目的部署会比.NET项目部署偏麻烦一些。

IIS图形化界面一直下一步,再调整下应用程序池的版本就行了。而Tomcat不论是在Windows,还是在Linux,都通过修改配置文件完成站点配置


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

收起阅读 »