Android SDK中无法安装HAXM installer - 轨迹1995 - 博客园

mikel阅读(609)

来源: Android SDK中无法安装HAXM installer – 轨迹1995 – 博客园

一、原因:

      刚搭建好环境,当我想打开Android Studio 自带的模拟器时,出现如下:

      /dev/kvm is not found

  

二、解决步骤:

1、点击File — Settings… — appearance&Behavior — Android SDK — SDK Tools

 

2、勾选:Intel x86 Emulator Accelerator(HAXM installer),点击OK

 

 

3、可是安装不了,出现如下错误:

 

4、网上查看办法,说需要进入的电脑的BIOS中修改,找到Intel Virtual Technology设置为Enabled(表示开启虚拟化)就可以了。可是还是不行。

   再查看,说在C:\Users\Administrator\AppData\Local\Android\Sdk\extras\intel 里面有intellhaxm-android.exe,需要手动安装。

      可是我一看intel文件夹里面什么都没有。

 

 

5、既然Android SDK不能帮我下载安装,那只能自己手动下载安装。查了资料,点击:https://github.com/intel/haxm  不要直接下载,因为没exe应用程序。拉到下面的Downloads,点击here,之后点击Latest release(最新版本),点击haxm-windows_v7_5_2.zip(我的是windows系统)。

 

 

 

6、下载完成在,解压放到自己的…sdk\extras\intel的目录下,进行安装。

 

 

7、重启Android Studio,打开模拟器,成功,如下:

 

Flutter环境搭建以及开发软件安装

mikel阅读(539)

来源: Flutter环境搭建以及开发软件安装

这里主要讲解Flutter基本环境搭建,目前Flutter已经正式发布,网上已经有好多关于介绍Flutter的文章。写作此文章的目的是记录自己学习过程中的点点滴滴,同时也是为了督促自己,不是为了哗众取宠。废话不多说,直奔主题!

一、Flutter SDK下载以及安装

1.1、网络环境
一般来说,要想安装或者依赖的某个框架,它自身也可能依赖别的框架或者组件之类的。因此,你就要将所有相关的框架或组件下载下来并进行安装。然而这些框架或组件,来源可能不是一个网站,特别是你访问的这些网站都是国外网站,如果你生活在我天朝,就不得不面临一个问题,就是访问某些国外网站会受到限制。为了避免不必要的麻烦,有以下两种方式:

  • 翻墙
    鄙人采用的这种方式,直接购买VPN
  • 使用镜像
    庆幸,Flutter官方为中国开发者搭建了临时镜像;你只需要将环境变量加入到用户环境变量中,如下:

$ export PUB_HOSTED_URL=https://pub.flutter-io.cn
$ export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

备注:
1、这里说的是临时镜像,是因为后期Flutter官方会推出一个新的稳定版本,到时候你只需要更换对应的地址就可以了,具体详情查看Using Flutter in China。
2、这里添加环境变量添加到用户环境变量一般采用添加到.bash_profile文件中,全局生效(下文会详细介绍)。

1.2、SDK下载

  • Flutter官网下载
  • github上下载
    备注:
    个人经验,Flutter官网下载没有github下载快(你如果已经翻墙就当我没有说)。

1.3、SDK安装
解压安装包到你指定的目录
记住,这里指定好的目录以后不要轻易移动,否则就要重新配置路径。
这里我们在根目录下新建development文件夹,在development文件夹下面放置刚才的解压包,如图:

图1 SDK路径

 

1.4、配置环境变量
1.4.1、临时环境变量配置
打开终端、执行下面代码:

$ export PATH=`pwd`/flutter/bin:$PATH

备注:
1、这段代码只对当前窗口,换句话说要是新开一个终端窗口,就不生效了。这样每次打开一个新的窗口就需要执行上述代码,实在是不方便。
2、执行这段代码需要在flutter当前目录执行,换句换说就是先执行

$ cd /Users/yangshichuan/development

然后执行

$ export PATH=`pwd`/flutter/bin:$PATH

1.4.2、设置永久环境变量
为了避免临时环境变量的弊端,最好设置永久环境变量。方法如下:

  • 打开(或创建).bash_profile

$ touch $HOME/.bash_profile

  • 输入以下代码:

$ export PATH=/Users/yangshichuan/development/flutter/bin:$PATH

这里的路径/Users/yangshichuan/development/flutter是你电脑放置flutter的绝对路径,更换为你自己的路径就好。

  • 运行指令

$ source $HOME/.bash_profile

  • 验证路径是否正确,执行指令:

$ echo $PATH

此时不受终端窗口限制,已经和你的计算机绑定在一起了。
上面提到的配置临时镜像路径也可以添加到.bash_profile文件中。

1.5、运行flutter doctor
此命令可以帮助你查看是否需要安装其它依赖项

$ flutter doctor

效果图如下:

图2

因为我已经安装完所有的配置项,即使初次安装也不用担心,按照终端给出的提示可以顺利安装成功,如果其中出现安装失败,唯一可能的原因是网络不好或者需要翻墙。

总结:

1、总的来说,安装Flutter主要是:下载Flutter框架➡️指定Flutter解压路径➡️配置环境变量➡️运行flutter doctor

2、对于习惯终端操作的同学可以直接运行下面指令:

$ export PUB_HOSTED_URL=https://pub.flutter-io.cn
$ export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
$ git clone -b dev https://github.com/flutter/flutter.git
$ export PATH=”$PWD/flutter/bin:$PATH”
$ cd ./flutter
$ flutter doctor

如果已经有VPN了,可以不用执行前面两句,如下:

$ git clone -b dev https://github.com/flutter/flutter.git
$ export PATH=”$PWD/flutter/bin:$PATH”
$ cd ./flutter
$ flutter doctor

二、安装编译器

对于编译器,有好几种选择如Android Studio、IntelliJ IDEA、VS Code等。这里主要说的是Android Studio的配置方法,大同小异。

2.1、Flutter和Dart插件安装
打开Android Studio、打开偏好设置、具体操作如下图:

图3

 

点击左侧标签Plugins,在右侧搜索框输入flutter/dart,搜索出相应插件,点击Install,如下图:

图4图5

因为我已经安装,所以上面没有显示Install按钮。
此时,需要重新退出Android Studio,否则不生效。
备注:
这里对Android Studio是有版本要求的,要高于3.0版本,详情可以查看官网Flutter官网。

2.2、新建Flutter项目
经过上面步骤,重新打开Android Studio,如下图:

图6

 

图7

 

图8
备注:
这里的Flutter SDK path指的是我们前面解压包放在的位置<这也就印证了为什么不要轻易移动SDK的原因>

 

图9

点击Finish按钮,至此项目建立完成,如下图:

图10

三、运行第一个程序

经过以上步骤,我们已经做好所有准备工作,此时已经迫不及待想要运行以下程序,看一下效果。
经过上面创建的工程,里面会包含一个默认demo,一个简单的计数器,效果如下:

图11

点击右上角运行按钮,查看效果:

图12

如果想修改一下,查看实时效果,点击右上角热重载按钮:

图13

备注:
最好的学习方法是下载官方demo运行,看看效果,然后多思考,多模仿,后续会陆续深入学习并更新文章。

四、Android studio在mac上卡顿问题解决

在初次安装Android studio软件时,运行时,发现非常卡顿。虽然我的电脑配置不是太高,但是MacBook Pro那也不是盖的,足以碾压绝大数Windows电脑。
经过仔细查找资料分析,得出以下结论:
初次安装Android studio,mac会为它分配一定的运行内存,但是比较低,这就造成了即使当前mac有多余内存也不会分配给Android studio,运行软件就会卡顿,解决方案请查看解决Android Studio在MacbookPro 13下卡顿的问题,这里就不在啰嗦了。

关联文章:
Flutter学习之Dart语法

参考网址:

Flutter官网
Flutter中文网
Flutter社区中文资源

本页内容由塔灯网络科技有限公司通过网络收集编辑所得,所有资料仅供用户参考了本站不拥有所有权,如您认为本网页中由涉嫌抄袭的内容,请及时与我们联系,并提供相关证据,工作人员会在5工作日内联系您,一经查实,本站立刻删除侵权内容。本文链接:https://www.dengtar.com/18395.html

VSCode中调试flutter出现Downloading canvaskit错误_zuozeye的博客-CSDN博客_flutter vscode

mikel阅读(650)

来源: VSCode中调试flutter出现Downloading canvaskit错误_zuozeye的博客-CSDN博客_flutter vscode

又来记录在Flutter遇到的坑了,这次其实应该不是配置和的问题,而是冬奥会开始了,墙被加固了的原因。

情况是这样的,当你写好一个flutter的应用,第一次运行时,会提示你下载几个文件
web sdk,canvaskit,
但是今天一直卡在Downloading canvaskit就下载失败,应为要访问google的源,试了多个梯子都没用,只好换个国内的镜像了。

首先打开flutter SDK 的安装目录,找到flutter.gradle文件。

安装目录\packages\flutter_tools\gradle\flutter.gradle

然后用记事本打开,主要不要改变了编码格式,搜索 buildscript 找到代码位置,大概是50行左右。
把原有的google()和mavenCentral()注释掉,添加国内的源地址。

一开始我只指定了google的源,不太好使,有添加了阿里的源,如果可选择的地址,可以多添加几个作为备用。

buildscript {undefined
repositories {undefined
//google()
//mavenCentral()
maven { url ‘https://maven.aliyun.com/repository/google’}
maven { url ‘https://maven.aliyun.com/repository/jcenter’ }
maven { url ‘http://maven.aliyun.com/nexus/content/groups/public’}
maven { url ‘https://dl.google.com/dl/Android/maven2/’}
}

然后在系统环境变量中增加

FLUTTER_STORAGE_BASE_URL — https://storage.flutter-io.cn

PUB_HOSTED_URL — https://pub.flutter-io.cn

最后重启VSCode,再次尝试编译。

OK!搞定。
————————————————
版权声明:本文为CSDN博主「无死角姐夫」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zuozeye/article/details/122632431

thinkphp中的volist标签详解-php教程-PHP中文网

mikel阅读(527)

来源: thinkphp中的volist标签详解-php教程-PHP中文网

本文通过实例案例给大家介绍了thinkphp中的volist标签,本文给大家介绍的非常详细,具有参考借鉴价值,需要的朋友参考下吧,希望能帮助到大家。
属性:

name(必须):要输出的数据模板变量

id(必须):循环变量

offset(可选):要输出数据的offset

length(可选):输出数据的长度

key(可选):循环的key变量,默认值为i

mod(可选):对key值取模,默认为2

empty(可选):如果数据为空显示的字符串

通常模型的select方法返回的结果是一个二维数组,可以直接使用volist标签进行输出。

$User = M(‘User’);

$list = $User->select();

$this->assign(‘list’,$list);

{$vo.id}

{$vo.name}

输出偶数记录

{$vo.name}

mod从0开始计数,这里mod循环计数为0,1

Mod属性还用于控制一定记录的换行,例如:

{$vo.name}

mod循环计数,0,1,2,3,4 当值为4的时候换行。也就是5条数据换一行。

输出循环变量

{$k}.{$vo.name}

循环变量从1开始1、2、3、4

如果没有指定key属性的话,默认使用循环变量i,例如:

{$i}.{$vo.name}

综合案例:

checked=”checked”>{sh:$vo.name} 电话({sh:$vo.tel})

{sh:$mod}

效果

iBox-面向Flutter的一站式研发工作台

mikel阅读(586)

来源: iBox-面向Flutter的一站式研发工作台

一  前言

Flutter 一码多端的特性,解放了端上同学的人力,带来了研发效率的提升,淘特技术团队因为早期双端研发同学数量不匹配以及对研发效率的诉求,也是阿里集团内部比较早在业务上落地 Flutter 的团队之一。
虽然有了一码多端的便利,但是伴随而来的还有研发链路中的各种问题,例如研发环境搭建,双端工程环境,集成发布流程繁琐等等。为了深入了解开发同学们的痛点,我们在团队内部发起了一份问卷调查。
我们针对研发幸福感指数以及研发链路中遇到的各种问题进行了调查。结果如下:
图片
在研发幸福感指数的打分中平均得分是 3.38(5分制)。我们针对影响研发幸福感的问题进行了分析,筛选出了一些大家普遍认为影响研发效率的问题。其中排名最高的是研发环境+工程环境(Flutter相关)的搭建,开发调试(Flutter相关)等问题。
接下来我们就来看看这些问题的具体痛点,以及解决这些问题的时候面临的一些挑战。

二  问题与挑战

1  问题

在影响研发幸福感的问题中,主要是以下三个方面的问题比较突出。
1)研发环境问题
研发环境配置是编码的前置工作,它也会影响新人落地对团队研发体验的第一印象。由于 Flutter 涉及 Android 与 iOS 双端的环境配置,导致不熟悉另一个端的同学配置起来,十分麻烦,上手难度高。另外,Flutter 本地版本不一致,缺乏 Flutter 版本管理工具,文档零散更新不及时,这些都极大的耗费了团队同学的精力。
2)工程环境问题
解决了研发环境问题,还需要解决工程环境问题,双端工程架构复杂,不熟悉某个端的同学面对编译问题难以解决。甚至很多同学就直接放弃了配置另外一个端的工程,平时开发只对着自己熟悉的端调试,违背了 Flutter 双端开发的理念。
点评:从团队的调研采访来看,一个新人同学搭建 Flutter 的研发环境和工程环境,先需要一天时间搭建好基础环境,后面的两三天时间折腾各种编译问题,特别是 iOS 的相关环境对于 Android 同学来说想要完整跑起来十分费力。
3)集成流程问题
等到代码开发测试完成以后,集成步骤多,平台间来回切换,集成流程割裂,没有形成完整的 SOP。整个集成流程既费时费力,又容易引发质量问题。
点评:现有的 Flutter 模块集成流程分为六步:1 模块分支代码合并 -> 2 模块生成新Tag版本 -> 3 主工程修改模块版本号 -> 4 主工程代码合并 -> 5 主工程生成版本号 -> 6 摩天轮提交正式包打包,步骤繁琐,需要在 Aone、MTL 等平台来回切换,而且手工操作各种版本号,很容易引发线上质量问题。

2  挑战

为了解决这些问题,之前也有沉淀一些文档和脚本,但是文档有很多步骤、命令,弄错任何一个就可能导致环境搭建出错,另外文档有时候也没有及时更新。
我们想如果能有一个桌面端 GUI 形式的研发工作台,研发同学日常研发遇到的各种问题都可以在这上面解决,新来的同学也可以借助这个研发工作台快速落地,那对研发的幸福感将是一个质的提升。
于是我们便决定打造一款桌面端的研发工作台,在实现这个目标的过程中我们也遇到了很多挑战。
1)如何降低开发同学的接入和使用成本
  1. 接入和使用成本。研发工作台本来作为一款工具软件,它本身如果再操作复杂,需要看各种文档,那就背离了工具软件简单易用的初衷,所以复杂操作一键化是做相关功能时必须做到的,例如软件环境一键配置,工程环境一键配置,一键集成发布等,很多功能都是按照这个思路来做的。
  2. 兼容现有的研发环境和工程环境。除了新来的同学,大部分开发同学的电脑上已经有了部分环境,如何与现有的环境共存,不改变开发同学现有的使用习惯,也是我们重点考虑的问题。
2)如何保障架构设计的合理性
我们想把研发工作台打造成一个人人都可以参与进来共建的开放平台,因为个人的时间是有限的,工作台本身作为一个工具集的聚合,需要更多的同学参与进来,更多的 idea 落地,所以如何做好仓库权限控制以及设计一个好的插件化框架就显得很重要。
3)新技术的落地,相关问题需要自己探索解决。
在桌面端研发工作台的开发中,我们使用的是 Flutter Desktop 技术(至于原因,技术调研部分会讲),国内目前 Flutter Desktop 技术在生产环境落地的并不多,相关经验还比较缺乏,遇到一些问题的时候,需要自己去探索解决。
接下来我们就来看一看我们为了解决这些问题,在 iBox 上设计了哪些核心的功能,以及这些功能是如何解决这些问题的。

三  技术全景

1  技术调研

业界的客户端研发工作台的发展现状。如下所示:
  • 业界:EasyBox,MBox 等工具。这些工具的核心一方面在于解决 Native 环境搭建,开发效率低的问题。另一方面深度封装 Git、Cocoapods,统一开发模式。
  • 淘特:也有一些零散的脚本工具。但是整体上没有解决研发环境配置,开发调试,集成发布等问题。
整体上看是一个客户端研发工作台落地的契机,业界有团队在尝试,淘特在 Flutter 研发链路也有痛点和需求。既然要进行桌面端开发,选择一个桌面端开发框架就成了首先要考虑的事情,当下比较流行的桌面端开发框架主要有以下两种:
  • 面向前端的 Electron:使用 JavaScript、HTML 和 CSS 构建桌面端应用程序。
  • 面向客户端的 Flutter Desktop:使用 Flutter 构建桌面端应用程序。
通常我们在做技术选型的时候会从问题解决,团队现状,技术领域,业务趋势等几个方面层层递进去思考。
  • 问题解决:Electron 和 Flutter Desktop 这两套方案都能解决我们的问题,虽然性能上有差异,但是这个不是我们最关注的点。
  • 团队现状:客户端同学对 Flutter 熟悉,客户端同学的上手成本更低,不依赖其他端的人力。从这个角度来看,Flutter Desktop 会更好。
  • 技术领域:Electron 和 Flutter Desktop 都在向前发展,Flutter 团队今年推出的 Flutter 2.10 将 Windows 平台正式带入稳定版本的支持,今年也会陆续完成 Linux、MacOS 等平台的稳定版本的支持。
  • 业务趋势:工作台未来可能会向全平台扩展。例如在桌面端是一个研发工作台,在移动端(Android&iOS)iBox 是一个应用小工具集或者是一个类似于蚂蚁伙伴的app,在 Web 端是一个数据看板。从这个角度讲,Flutter Desktop 会更好。另外我们还想在工作台上做 UI 等组件的展示,如果基于 Flutter 来做,那么就能做到所见即所得,这会是一个非常好的体验,
基于以上思考,我们最终选择了 Flutter Desktop。有了开发框架,我们接着来看看 iBox 的架构设计

2  功能设计

iBox 的核心定位
图片
iBox 是一款基于 Flutter Desktop 技术栈研发的一站式、多样化、可定制的研发工作台。提供从研发环境到集成发布全流程的研发支持。核心功能包含工作台、研发环境、工程管理、引擎管理、社区生态、变更单管理与工具箱等。
iBox 在功能设计上分为工作台、研发、发布、工具箱四大板块。其中研发、发布、工具箱又各自包含了很多子模块功能。
我们着重介绍一下工作台、研发环境与工程管理、社区生态、变更单管理等核心功能,让大家对 iBox 的整体功能有一个基本认识。

工作台

提供了最近变更单,常用平台快捷入口等功能,让常用功能一键直达。另外工作台还预留了技术展示 Banner 位的功能,可以展示一些团队内外的优秀技术产出。后续还考虑将值班提醒,集成提醒,发布提醒做在工作台上。

图片

研发环境与工程管理

研发环境 + 工程管理 解决的是如何快速进入本地开发的问题,如果是新人进入团队开发,从拿到电脑到进入开发,一般需要经历研发环境配置和工程环境配置这两个流程。
在这个过程中需要去各个地方翻阅文档,按照文档进行操作,在操作的过程中,还经常伴随着文档更新不及时,操作报错,出了错误又得去 Google 或者问身边的同事,整个过程费时又费力。
而 iBox 的研发环境和工程管理者两个功能模块则通过操作一键化来解决上述的问题。
首先是研发环境提供了 Flutter、Android、iOS 研发环境的检查和一键配置的功能,让不熟悉某个端的同学更加便捷的配置自己的研发环境,如下所示:

图片

然后是工程管理提供了混合工程下 Flutter、Android、iOS 等壳工程环境环境检测,一键环境配置等功能,解决了环境配置复杂难以上手的问题,如下所示:

图片

工程环境的复杂性在于它涉及 Flutter、Android、iOS 三个端的编译,编译的过程还会因为本地环境的差异而有所不同,各种编译报错,使得开发同学穷于应付。iBox 将从环境到工程的各种错误类型进行了梳理,并将错误信息展示出来,如下所示:
图片
不仅让开发同学知道自己的工程环境有什么问题,还提供了对工程环境问题的一键修复,一键修复功能会先删除缓存(flutter clean,删除lock文件等),然后按照以下流程重新跑整个工程,确保可以修复工程环境,如下所示:
图片
研发环境和工程管理这两个功能模块相互配合,真正解决了开发同学环境配置难的问题,同时它还打破了 Android 与 iOS 之间的门槛,让不熟悉另外一个端的同学也能进行这个端的调试和打包。

社区生态

集团内外针对 Flutter 都贡献了不少功能组件,但是并没有一个统一的地方展示这些组件,导致开发同学在需要用的时候,需要去 pub 库里各种搜索。

而 iBox 的社区生态功能提供了 Flutter 社区(集团内外)引擎、UI 组件、路由、动态化等各个方面的技术沉淀的展示,特别是 UI 组件,由于 iBox 本身就是基于 Flutter 开发的,那么这些 UI 组件的 Dart 代码可以直接在 iBox 上运行展示和交互,这种所见即所得的体验是非常棒的,如下所示:

图片

变更单管理

在以前的开发流程中,Flutter 的研发流程是比较繁琐的,而且这些流程需要开发自己手工操作,如下所示:
  • 开始开发
    • 创建Android摩天轮变更单
    • 创建iOS摩天轮变更单
    • 拉取变更分支
    • 修改Flutter主工程的模块依赖代码
    • 关联Aone需求
  • 开发中
    • Android打包
    • iOS打包
    • 提交模块代码CR
    • 本地源码依赖修改
  • 完成开发
    • 模块分支代码合并
    • 模块生成新Tag版本
    • 主工程修改模块版本号
    • 主工程代码合并
    • 主工程生成版本号
    • 摩天轮提交正式包打包
而 iBox 的变更单功能,可以帮助 Flutter 研发同学快捷的完成研发流程的各种操作,如下所示:
图片
  • 开始开发:iBox 可以关联 Aone 需求一键创建变更单,同时创建新的变更分支,准备好当前变更所需的工程环境。
  • 开发中:一键打 Android & iOS 双端包,一键提交变更仓库的 CR。
  • 完成开发:一键提交集成,提交的过程中会自动完成上述的集成步骤。
这些一键式的操作不仅很好的提升了 Flutter 研发的效率,也规范了Flutter 的分支管理、集成方式,避免个人随意操作带来的工程问题。
以上便是 iBox 一期规划和完成的功能,它从根本上解决了上面提到的团队研发链路存在的种种问题,同时也感谢闲鱼同学在集成发布这块为我们提供的飞鱼工作台相关实践参考。

架构设计

iBox 在架构设计上主要关注以下几个问题:
  • 问题1:iBox 作为一款具备一定规模的 GUI 软件,如何方便且安全的组织各个功能模块的代码。
  • 问题2:iBox 既然要面向共建,如何保证 iBox 自身的开发体验。
  • 问题3:iBox 如何在保证共建开放的同时保证软件整体的质量和性能。
通过对以上几个问题的思考,我们对 iBox 采取了纵向分层,横向模块化的设计,具体说来:
  • 问题1:不同的功能进行模块化设计,模块源码彼此独立。这样可以精细控制源码仓库的权限,不同模块之间的修改不会相互影响。
  • 问题2:基于 git repo 进行多仓库管理。既可以使用 git 操作单个的仓库,也可以使用 git repo 对多个仓库进行代码同步,代码提交,代码 Review 等操作,保障了 iBox 多仓库协同的开发体验。
  • 问题3:
    • 约定每个模块的基本架构设计。包括源码组织方式、状态管理方案等方面内容,并通过静态扫描来保障这些约定的落地。
    • 限制三方库的引用以及指定三方库的版本(不使用^号来指定版本,例如:url_launcher: ^6.0.20)。^号指定的版本会导致后两位版本会自动升级(flutter upgrade 和 重新生成 pubspec.lock 文件的时候),导致打包的时候使用了一些意料之外的版本,引发质量问题(参考最近的 url_launcher 的 url_launcher_macos 自动升级导致链接打不开的问题 issue1 issue2 )。
整体架构大图如下所示:
图片
从上面的架构图可以看出辅助功能作为基础模块,为其他核心功能提供基础能力。接下来我们按照从工程到模块的顺序分别讲一下具体的设计方案:
  • 首先是整体工程的设计。
  • 然后是具体模块的设计。

工程结构

在整体工程上采用多仓库设计,之所以使用这样的设计,是因为 iBox 会涉及跨团队开发,多仓库可以让各个模块的源码彼此独立,不同模块之间不会相互干扰。
iBox 基于 git-repo 实现了多仓库的管理,仓库结构如下所示:
open-ibox git group|-----------------------------------------------------------|--- ibox|--- ibox_common                               基础模块|--- ibox_dashboard                             工作台|---                                       ====== 研发 =====|--- ibox_software                              研发环境|--- ibox_project                               工程管理|--- ibox_engine                                引擎管理|--- ibox_community_ecology                     社区生态|---                                       ====== 发布 =====|--- ibox-app-size                               包大小|--- ibox-change-order                           变更单|---                                       ===== 工具箱 =====|--- ibox-toolkit                              研发小工具
ibox 作为主工程,ibox_common 作为基础模块,其他模块都依赖于 ibox_common。开发 iBox 的同学只需要几行简单的命令,就可以同步 iBox 的全部源码工程。
mkdir open-iboxcd open-boxgit repo init -u http://xxx/open-ibox/manifest.gitgit repo syncgit repo start --all master
然后在自己的模块进行开发和代码提交即可,彼此之间互不干扰。聊完了整体工程的设计,我们再来看看各个模块的设计。

模块设计

每个模块的核心功能在于处理UI交互与逻辑交互,不同于传统客户端的命令式 UI 框架,Flutter 采用的是声明式 UI 框架,驱动 UI 发生变化的是状态(State),如下所示:
图片
图片引用自 Start thinking declaratively
Flutter 里的状态指的是在 Widget 之间或者内部存储和传递的数据或者信息,它可以分为短时状态和应用状态两种。
  • 短时状态(exphmeral state):也称为用户UI状态或者局部状态,是一个完全独立在 Widget 里的状态。Widget 树里的其他部分不需要访问这种状态,它也不需要使用状态架构架构(ScopedModel,Redux)去管理这种状态,它只需要一个 StatefulWidget 就可以了。
  • 应用状态(app state):它是一个应用之间多个部分共享的一个非短时状态,并且用户在会话期间,保留这个状态。
所以状态管理是编写 UI 和逻辑核心要面对的问题,它也会影响我们组织源码的方式,在 Flutter 状态管理的官方文档中,提供了 14 种状态管理方案,我们着重讨论官方比较推荐的前四种,至于其他的方案,感兴趣的可以去查阅官方文档。
  • setState
  • InheritedWidget
  • Provider
  • Riverpod
我们先来看原生的两种状态管理方式:
  • setState:通常用于处理 Widget 内部的短时状态。
  • InheritedWidget:setState 只能处理 Widget 内部的短时状态,如果需要处理应用状态,在 Widget 树之间进行通信,则需要用到 InheritedWidget,InheritedWidget 可以为其子孙节点提供数据和服务。
setState 在应用场景上比较受限,InheritedWidget 对于开发者来说过于底层,使用起来比较复杂。既然官方的方案都有限制,我们再来看看社区提供的提供的比较推荐的方案。
  • Provider:它对 InheritedWidget 组件进行了封装,使其更加易用,更易复用。
  • Riverpod:基于 Provider 改进而来,它是一个响应式的状态管理和依赖注入框架,编译安全,支持 DevTools 调试,本身不依赖于 Flutter。
事实上,Provider 和 Riverpod 的作者都是 Remi Rousselet,Riverpod 这个名字是 Provider 的字母重新排序后得到的,它的推出主要是为了解决 Provider 的一些功能缺陷,如下所示:
功能
Provider
Riverpod
是否编译安全
存在运行时异常ProviderNotFoundException
编译时安全,在编译期间捕获错误,能编译就能正常运行。
是否对 provder 的编写和引用有限制
不支持同一类型的多个 provider
支持同一类型的多个 provider,也可以在任何地方编写和引用 provider。
是否依赖 Flutter
依赖于 Flutter 的 Widget。UI 代码和依赖注入耦合在一起。
不依赖 Flutter 的 Widget。可以独立的创建/共享/测试 provider,这也包括在可以在没有BuildContext的情况监听 provder。
基于以上的比较,我们最终选择了 Riverpod 这一套方案,并由此设计了模块的源码结构,如下所示:
图片
iBox 模块源码结构|-----------------------------------------------------------|--- provider                             基于 Riverpod 实现的 State 管理方式(官方推荐)|----- xxx.provider.dart                  provider|--- service                              接口请求、数据处理相关实现|----- xxx.service.dart                   service|--- ui                                   页面与组件|----- xxx.screen.dart                    页面
一个常见的编写 UI 逻辑的流程如下所示:
  1. 在 ui 部分编写页面和组件。
  2. 在 service 里编写和数据相关的逻辑。
  3. 在 provider 里编写相关 provider 类,它可以调用 service 里的功能。
  4. 在 ui 或者其他任何需要的位置引用 provider,操作相关逻辑。
这种方式实现了 UI 与逻辑的解耦和分离,UI 部分可以自由迭代,逻辑部分也实现了复用。
以上便是 iBox 的整体架构设计,相当于是一个简化版的插件化方案,如何后续有更丰富的插件生态进来,我们会考虑上架一个类似于 VSCode 的插件市场,不过目前对于我们来说,已经够用了。
插件化的设计使得可以自由组装各个模块,不同团队需要的模块功能不一样,我们推出了 app variant(变体)的功能。不同的 app variant(变体)拥有不同的 tab 栏配置,打包的时候就可以针对不团的团队打出不同功能的包。

4  上线效果

iBox 在研发链路核心痛点上使用前后对比
研发痛点
使用前
使用后
研发环境 + 工程环境
一般需要一天时间搭建好研发环境,后面的两三天时间折腾工程的各种编译问题。
实现了两个“一”,环境配置(Flutter)一小时完成,打包集成一键完成。
社区生态
组件比较分散,不知道有哪些组件可以使用,没有引导文档,也不知道该怎么使用。
在 iBox 上进行可视化展示,所见即所得,可进行交互操作,附带 Demo 代码,使用方法清晰明了。
引擎管理
全局引擎切换需要修改文件夹名或者 profile
UI 界面一键切换,本地引擎版本统一管理。
变更单
需要跨两个平台,执行六步操作才能完成,手工操作还可能引发线上质量问题。
实现了集成步骤的 N 到 1,一键提交 Android 和 iOS 的双端集成。
iBox 用户在使用一段时间以后,也给了不少不错的反馈。
“一键安装还是非常好用的,帮开发节省了不少时间。以前各个地方下载,安装配置,还要解决版本冲突的问题,浪费不少时间。”
“这次版本集成全走的iBox, 用着很爽。”

“大幅简化了 Flutter 环境配置、集成繁琐等问题。”

此外,iBox 还处在一个刚起步的阶段,我们希望把它作为一款产品去迭代和运营。为此我们也为 iBox 设计了不同视角下的运维指标,如下所示:
全局视角:整体数据
  • 全站 PV
  • 全站 UV
  • 覆盖的团队数
  • 各个团队的用户覆盖率
  • 访问量前10的用户
  • 访问量前10的页面
用户视角:不同团队/个人偏好的功能
  • 团队 -> 功能模块 访问次数
  • 个人 -> 功能模块 访问次数
业务视角:做的比较好,受欢迎的功能
  • 最热功能模块排名
运维数据体系需要长期建设,它对我们后续的功能迭代和体验优化有着重要的指导意义,开发同学也是用户,只凭着拍脑袋想出来的功能,不一定能获得大家的认可。

四  技术总结

在做 iBox 之前,对于 Flutter 做过一些原理上的探究,之前整理编写了《从架构到源码:一文了解Flutter渲染机制》一文,但是没有好的机会应用在生产实践上。这次的 iBox 开发之旅让我收获颇多,借着这个机会,我们就来聊一聊 Flutter Desktop 技术在生产实践上的应用。

1  Flutter Desktop 的发展历程

从2018年2月15日Flutter 团队发起的 flutter-desktop-embedding 项目到现在,已经四年过去了,中间的过程也是起起伏伏,从最初的不支持生产环境,到如今 Flutter 2.10 发布,正式宣布支持 Windows 平台生产环境 app 的开发,Flutter Desktop 的发展历程如下所示:
  • 2018.02.15, 在 flutter-desktop-embedding 项目里提交第一个 initial commit。
  • 2019.12.05,支持了 MacOS 平台。
  • 2020.07.08,Linux 平台进入 alpha 阶段。
  • 2020.09.24,Windows 平台进入 alpha 阶段。
  • 2021.03.05,Flutter 2 正式发布,Flutter 对桌面端的支持进入稳定版本的前期准备阶段。
  • 2022.02.15,Flutter 2.10 发布,Windows 平台率先进入稳定版本,可用于生产级 app 的研发,其他平台也在积极准备中。
在 2022 年,Flutter 团队计划按照 Windows、Linux、MacOS 的顺序逐个推进,将对主流桌面端平台的支持带入到 stable channel,最终实现 Flutter “write once, run anywhere” 的愿景。

2  Flutter Desktop 的社区生态

和对 Android 和 iOS 的支持一样,Flutter 也实现了基于 Windows 等平台的 Embedder,Embedder 的上层是 C++ Engine 和 Dart Framework,它自己负责翻译和发送 Windows 等平台的消息。整体架构如下所示:
图片
图片引用自 Announcing Flutter for Windows
点评:Linux、MacOS 等其他桌面端平台也是类似的实现结构,更深入的细节可以参见 platform 的实现。
移动端应用和桌面端的应用相比既有相同之处,例如:
  • GPU 图形加速
  • 渲染系统
  • 动画
  • 主题
  • 文本输入
  • 国际化
  • UI 组件
这也使得大部分现有的 Flutter 社区组件都可以在桌面端使用,但两者也有不同之处,例如:
  • 更大的屏幕尺寸
  • 支持鼠标/键盘输入
  • 输入法
  • 导航方式
  • 可访问性
  • 系统独有的视觉样式
  • 与底层系统的交互
基于这些不同,Flutter 针对桌面端平台也提供了针对性的支持,如下所示:
图片
图片引用自 Announcing Flutter for Windows
在 iBox 的开发过程中,我们也使用了不少原生能力,这里针对 Flutter Desktop 常用的一些社区组件做个总结,如下所示:

图片

现有的社区组件基本能满足我们的开发需求。

3  Flutter Desktop 的应用场景

iBox 是对 Flutter Desktop 技术的一次有意义的探索,它为我们的产品带来了更多的可能性,扩展了产品触达的边界。
那么,Flutter Desktop 适合哪些应用场景呢?
  • 企业内部使用的工具类软件,尤其是在团队人员不足的时候,想快速落地一些工具和功能。
  • 企业的ToB应用,例如收银台,饿了么商家端等。
  • 团队自身已经有基于 Flutter 开发的移动端应用,想把部分功能扩展到桌面端。
任何技术都有长短,Flutter Desktop 也有不适合的应用场景,如下所示:
  • 对桌面端原生能力依赖较大的应用,因为针对桌面端的社区生态支持还还不如移动端这么完善,遇到缺乏的能力,需要自己去从零开始搭建。
当然技术也是不断发展的,当前存在的问题,也许在将来就被解决了。笔者对 Flutter Desktop 技术的发展还是很有信心的。

五  结语

Flutter 一份代码,在兼顾性能的同时上可以多端运行,是它的优势所在,解放了端上的生产力。尤其是对于 iOS 和 Android 同学比例严重失调的团队来说,这无疑是一个福音。
但是如果不注重 Flutter 开发周边配套工具的建设,从最开始的环境搭建、开发调试、再到集成发布没有好的工具去支撑,就很容易就演变成了 “Flutter 从入门到放弃”。这是因为业务团队和技术团队的诉求是不一样的,技术团队觉得解决 Flutter 各种问题的过程就是一个学习的过程,但是业务团队业务压力大,他们的第一诉求是快速开发,快速上线,如果周边配套工具缺失,他们很有可能就会选择放弃这个方案,这对于 Flutter 的推广是十分不利的。
我们希望刚接触 Flutter 的开发同学,他们的使用体验是平滑的,能一键完成的就一键完成,例如我们提出的“一小时完成 Flutter 环境搭建”、“一键配置/修复工程环境” 等等,这些理念也与 Flutter 团队最近发布的年度规划中的“提升开发者体验”不谋而合。
今年 2月10号,Flutter 团队发布了他们 2022 年的年度战略(Flutter 2022 Strategy)和路线 (Flutter 2022 Roadmap)。如下所示:
图片
可以看到未来一年,Flutter 团队将开发者体验提到了非常重要的位置,他们将从 Flutter 的各个层面去改善开发者体验,例如:
  • 提升 DevTools 的易用性。
  • 让 API 的使用体验更加平滑,逐步弃用和删除旧的 API。
  • 修复 Hot Reload 有些时候不生效的问题。
  • 让入门 Flutter 体验更加平滑,降低入门门槛。
上述提到的一些理念,例如 “让入门 Flutter 体验更加平滑,降低入门门槛”,和我们做 iBox 的初衷不谋而合。另外在 iBox 后续的规划中,我们除了降低开发同学的 Flutter 入门门槛,还希望降低新团队接入 Flutter 的成本。
现有的 Flutter 接入方案以混合工程方案 add Flutter to existing app 为主,这套官方提供的方案有着不小的接入、重构以及维护的成本,而且这是一个重复踩坑的过程,很多相同的问题会被不同的团队再次遇到,如果 iBox 可以提供一键接入的方案,那么将大大降低 Flutter 的接入和填坑成本,助力 Flutter 的推广。
Flutter 团队在年度战略(Flutter 2022 Strategy)中提到 “以用户(指 Flutter 开发者)为中心,其他一切都会随之而来”。
We believe in “focus on the user and all else will follow”. This manifests in our emphasis on developer experience. 引用自  Flutter 2022 Strategy
相信在新的一年,Flutter 的研发体验会越来越好,iBox 也能为 Flutter 的推广尽一份绵薄之力。

六  参考资料

  • macOS Performance Comparison: Flutter Desktop vs. Electron:https://getstream.io/blog/flutter-desktop-vs-electron/
  • List of state management approaches:https://docs.flutter.dev/development/data-and-backend/state-mgmt/options
  • riverpod:https://riverpod.dev/
  • provider:https://pub.dev/packages/provider
  • Announcing Flutter for Windows:https://medium.com/flutter/announcing-flutter-for-windows-6979d0d01fed
  • flutter-desktop-embedding:https://github.com/google/flutter-desktop-embedding
  • Start thinking declaratively:https://docs.flutter.dev/development/data-and-backend/state-mgmt/declarative
  • git-repo:https://git-repo.info/zh_cn/docs/getting-started/installation/
  • Announcing Flutter for Windows:https://git-repo.info/zh_cn/docs/getting-started/installation/
  • Flutter Desktop shells:https://github.com/flutter/flutter/wiki/Desktop-shells
  • Flutter 2022 Strategy:https://docs.google.com/document/d/e/2PACX-1vTI9X2XHN_IY8wDO4epQSD1CkRT8WDxf2CEExp5Ef4Id206UOMopkYqU73FvAnnYG6NAecNSDo9TaEO/pub
  • Add Flutter to existing app:https://docs.flutter.dev/development/add-to-app

【Redis】redis分页查询理解 - 不无聊 - 博客园

mikel阅读(546)

来源: 【Redis】redis分页查询理解 – 不无聊 – 博客园

偶然在代码中发现一个接口,接口定义说是分页查询,但逻辑实现是Redis。不太理解,Redis怎么分页?后来看到一篇文章,然后了解了。

1、Zrevrange实现

通过SortedSetzrevrange topicId (page-1)×10 (page-1)×10+perPage指令可以实现分页功能。

Redis Zrevrange 命令 – 返回有序集中指定区间内的成员,通过索引,分数从高到底。
参考:Redis Zrevrange 命令_返回有序集中指定区间内的成员,通过索引,分数从高到底

详细了解见原文:在 Redis 中进行分页排序查询 | NiuNiu’s Note

注:Zrevrange 命令后面的命令值就是索引值。所以不会有问题。

思考:可能会有一个小问题。如果在分页查询过程中,有新数据插入。那么已有数据的索引值会改变,这样就会查到重复数据或者遗失数据了。

而Zrevrangebyscore,如果将数据插入时间点作为score,那么通过score来划分范围,肯定不会出现数据重复或丢失的情况。

2、Zrevrangebyscore命令实现

另外,我们的代码中使用的是Zrevrangebyscore命令。

这个命令应该只保证每页范围一致,做到不遗漏数据,但不保证每页数据总数一定。

一般不适用于传统每页多少条数据的分页方式,比较适合于瀑布流或者评论那种动态加载数据的方式。

注:Zrevrangebyscore 命令后面的命令值就是分数值。所以有数据总数不一致情况。

3、参考:

  1. 在 Redis 中进行分页排序查询 | NiuNiu’s blog
    http://liu-xin.me/2015/11/17/%E5%9C%A8Redis%E4%B8%AD%E8%BF%9B%E8%A1%8C%E5%88%86%E9%A1%B5%E6%8E%92%E5%BA%8F%E6%9F%A5%E8%AF%A2/

使用redis的zset实现高效分页查询(附完整代码) - 东汉 - 博客园

mikel阅读(492)

来源: 使用redis的zset实现高效分页查询(附完整代码) – 东汉 – 博客园

一、需求

移动端系统里有用户和文章,文章可设置权限对部分用户开放。现要实现的功能是,用户浏览自己能看的最新文章,并可以上滑分页查看。

 

二、数据库表设计

涉及到的数据库表有:用户表TbUser、文章表TbArticle、用户可见文章表TbUserArticle。其中,TbUserArticle的结构和数据如下图,字段有:自增长主键id、用户编号uid、文章编号aid。

 

自增长主键和分布式增长主键如何选(题外讨论):

TbUserArticle的主键是自增id,它有个缺陷是,当你的数据库有主从复制时,主从库的自增可能因死锁等原因导致不同步。不过,我们可以知道,这里的TbUserArticle的主键id不会用在其它表里,所以可以是自增id。不像用户表的主键,它就不能用自增id,因为用户表主键(uid)会经常出现在其它表中,当主从库自增不一致时,很多有uid字段的表数据在从库中就不正确了。用户表主键最好是用分布式增长主键算法生成的id(比如Snowflake雪花算法)。

那么你可能就要说了,TbUserArticle的主键为什么不直接用雪花算法产生,不管有没有用,先让主从库主键值一致总是有恃无恐。要知道,雪花算法产生的id一般是18位,而redis的zset的score是double类型,只能表达到16位”整数”部分(精确的说是9007199254740992=2的53次方)。因此,TbUserArticle的主键选择自增id。那么能不能产生一个16位(具体是53bit)的分布式增长id用于支持zset的score呢,当然也是可以的,因为目前的雪花算法是可以根据实际系统环境压缩bit位的,怎么压缩bit位呢,有许多方案,以后有需要我可以把它写出来。

建议:主键一般都要选自增id或分布式增长id,这种主键好处多多,它符合自增长(物理存储时都是在末尾追加数据,减少数据移动)、唯一性、长度小、查询快的特性,是聚集索引的很好选择。

 

三、redis缓存设计-zset

zset的作法及其优点说明:

1.zset的score倒序取数可以很好的满足取最新数据的需求。

2.用TbUserArticle的文章编号当value,用自增长id当score。自增id的唯一性可很方便的取下一页数据,直接取小于上次最后一笔的score即可(用lastScore表示)。而如果用文章的时间做score,则要考虑两笔文章的时间是同分同秒问题,当lastScore落在同分同秒的两篇文章之间时,就尴尬了,虽然有解,但麻烦了一点。有时的场景你用不了自增id当score,只能用文章时间,那怎么解决呢,方案就是当是同分同秒时,再根据文章编号做比较就好了,zset的score相同时,也是再根据value排序的,这块的代码实现请看下文第五点,只需稍微改点代码即可。

3.当新增或重新添加一项时,zset也会保持score排序。而如果用的是redis的list,一般就得从db重载缓存,新增进来的数据项就算是最新的,也不敢直接添加到list第一笔,因为并发情况下,保证不了最新就是在第一笔;至于重新添加进非最新项,那更是要从db取数重新装载缓存(一般是直接删除缓存,要用的时候才装载)。

4.第一次从db加载数据到zset时,可只取前N笔到zset。因为我们移动端的数据浏览,一般是只看最新N笔,当看到昨天浏览过的数据一般就不会再往下浏览。

5.控制zset为固定长度,防止一直增长,一是减少缓存开销,二是队列长度越短操作性能越高。而且redis服务端有两个参数:zset-max-ziplist-entries(zset队列长度,默认值128)和 zset-max-ziplist-value(zset每项大小,默认值64字节),它们的作用是,当zset长度小于128,且每个元素的大小小于64字节时,会启用ziplist(压缩双向链表),它的内存空间可以减少8倍左右,而且操作性能也更快。如果不满足这两个条件则是普通的skiplist(跳跃表)。另,数据结构hash和list默认长度是512。如果系统有100万个用户,每个用户都有自己的队列缓存,那么使用ziplist将节省非常大的内存空间,并提升很大的性能。

注意,当从zset移除一项数据,则看场景是否需要清空队列。否则有可能添加进来了一项很旧的数据,它会跑到缓存队列最底部,如果此旧数据比db中未进队列的数据还旧,那么队列中的数据就不正确了。(此时,用户滑到缓存最后一页时,就有可能浏览到这项不正确的数据,为什么是“有可能”,因为当取到zset最后一笔,很可能不够一页(一页10笔计算的话,90%会取不够一页),而不够一页就会从db直接取一页,从db直接取就不会有这项不正确的数据。而当zset又添加进一项新数据,末端那笔旧数据就会被T出队列(因为队列保持固定长度),zset数据又恢复正确了。不管怎样,这种问题几率虽不高,也是有解决方案,可搞个临界点处理此问题,不细说,否则又是长篇大论,最好的方案就是根据实际场景设计,比如从zset队列移除数据的情况多不多)。而如果添加到zset的数据都是最新数据,则不会有此问题。

当用唯一主键id做score时,这可是非常有用,你可以直接根据id定位到项了,至于如何大用它,我会再出篇博客。

 

四、代码实现

从redis缓存按页取数一般要考虑的点:

1.当根据cacheKey未取到数据时(可能是缓存过期了导致redis无此cacheKey数据),则触发重载数据(reload):从db取limit N笔数据,装载到redis zset队列中,并直接取N笔的第一页数据返回;
2.如果db本身也无对应数据,则添加”no_db_mark”标识到cacheKey队列中,下次请求则不会再触发db重载数据;
3.当取到缓存末尾时,从db取一页数据直接返回。这种情况是很少的,要根据业务场景合理规划缓存长度。

上代码:

代码注释比较详细和有用,请直接看代码。

其中,批量添加数据到zset的函数AddItemsToZset很有用,它使用lua一次性添加多笔数据到zset(注意,使用lua时,要保证lua执行快,否则它会阻塞其它命令的执行),经测试:AddItemsToZset添加1w笔数据,只需要39ms;10w笔需要448ms。因为我们只取前N笔数据到缓存,因此一般不会添加超过1w笔。

另一个通用有用的函数是GetPageDataByLastScoreFromRedis,它支持从指定的score开始取pageSize笔数据,即支持了zset分页。它是第二页(及之后)的取数,而如果取第一页取数,则直接用redis原生函数即可redis.GetRangeWithScoresFromSortedSetDesc(cacheKey, 0, pageSize – 1);。

复制代码
    /// <summary>
    /// 分页取数帮助类
    /// </summary>
    public class PageDataHelper
    {
        public readonly static string NoDbDataMark = "no_db_data";//在zset中标识db也无数据
        public static RedisHandle RedisClient = new RedisHandle();//redis操作对象示例
        public static DbHandleBase DbHandle = new SqlServerHandle("Data Source=.;Initial Catalog=Test;User Id=sa;Password=123ewq;");//db操作对象示例
        /// <summary>
        /// 按页取数。返回文章编号列表。
        /// </summary>
        /// <param name="lastInfo">上一页最后一笔的score,如果为空,则说明是取第一页。</param>
        /// <param name="getPast">true,用户上滑浏览下一页数据;false,用户上滑浏览最新一页数据</param>
        /// <returns>返回key-value列表,key就是文章编号,value就是自增id(可用于lastScore)</returns>
        public static IDictionary<string, double> GetUserPageData(string uid, int pageSize, string lastInfo, bool getPast)
        {
            long lastScore = 0;
            //1.解析lastInfo信息。->getPast为false,则固定取最新第一页数据,不用解析。lastInfo为空,则也不用解析,默认第一页
            if (getPast && !string.IsNullOrWhiteSpace(lastInfo))
            {
                lastScore = long.Parse(lastInfo);//外层有try..catch..
            }
            string cacheKey = $"usr:art:{uid}";
            bool isFirstPage = lastScore <= 0;
            using (IRedisClient redis = RedisClient.GetRedisClient())
            {
                if (isFirstPage)
                {
                    //2.第一页取数
                    var items = redis.GetRangeWithScoresFromSortedSetDesc(cacheKey, 0, pageSize - 1);
                    if (items.Count == 0)
                    {
                        //2.1 无数据时,则从db reload数据
                        items = ReloadDataToRedis(redis, cacheKey, uid, pageSize);
                        if (items.Count == 0 && pageSize > 0)
                        {
                            //如果db中也无数据,则向zset中添加一笔NoDbDataMark标识
                            redis.AddItemToSortedSet(cacheKey, NoDbDataMark, double.MaxValue);
                        }
                    }
                    else if (items.Count == 1 && items.ContainsKey(NoDbDataMark))
                    {
                        //2.2如果取到的是NoDbDataMark标识,则说明是空数据,则要Clear,返回空列表
                        items.Clear();
                    }
                    //设置缓存有效期,要根据业务场景合理设置缓存有效期,这边以7天为例。
                    redis.ExpireEntryIn(cacheKey, new TimeSpan(7, 0, 0, 0));
                    //2.3 第一页,有多少就返回多少数据。数据如果不够一页,说明本身数据不够。
                    return items;
                }
                else
                {
                    //3.第二页(及之后)取数
                    var items = GetPageDataByLastScoreFromRedis(redis, cacheKey, pageSize, lastScore);
                    if (items.Count < pageSize)
                    {
                        //3.1 如果取不够数据时,就到db取。如果db也不能取到一页数据,前端会显示无更多数据,不会一直db取。
                        return GetPageDataByLastScoreFromDb(uid, pageSize, lastScore);
                    }
                    //3.2 如果缓存数据足够,则返回缓存的数据。
                    return items;
                }
            }
        }
        public static Dictionary<string, double> ReloadDataToRedis(IRedisClient redis, string cacheKey, string uid, int pageSize, string bizId = "")
        {
            //1.db取数 取top 1000笔数据。不需要全取到缓存。
            IEnumerable<dynamic> models;
            using (var conn = DbHandle.CreateConnectionAndOpen())
            {
                var sql = $"select top 1000 id,aid from TbUserArticle where uid=@uid order by id desc;";// limit 1000;";
                models = conn.Query<dynamic>(sql, new { uid = uid });
            }
            if (models.Count() <= 0) return new Dictionary<string, double>();
            //2.数据加载到redis缓存。
            var itemsParam = new Dictionary<string, double>();
            foreach (dynamic model in models)
            {
                itemsParam.Add((string)model.aid, (double)model.id);
            }
            //使用lua一次性添加数据到缓存。lua语句要执行快,经测试添加1w笔数据,只需要39ms;10w笔需要448ms。因为sql中有limit,因此一般不会添加超过1w笔。
            //因为是原子性操作、并且是zset结构,这边不需要加锁。db取到数据应第一时间加载到redis。
            AddItemsToZset(redis, cacheKey, itemsParam, true, true);
            if (pageSize <= 0) return null;
            //3.直接由models返回第一页数据。
            return models.Take(pageSize).ToDictionary(x => (string)x.aid, y => (double)y.id);
        }

        public static Dictionary<string, double> GetPageDataByLastScoreFromDb(string uid, int pageSize, double lastScore)
        {
            //db取一页数据。
            var sql = $"select top {pageSize} id,aid from TbUserArticle where uid=@uid and id<{lastScore}order by id desc;";// limit {pageSize};";
            using (var conn = DbHandle.CreateConnectionAndOpen())
            {
                return conn.Query<dynamic>(sql, new { uid = uid }).ToDictionary(x => (string)x.aid, y => (double)y.id);
            }
        }
        #region 通用函数
        /// <summary>
        /// ZSet第一页之后的取数,从lastScore开始取pageSize笔数据(第一页之后才有lastScore)。
        /// 使用lua,保证原子性操作。
        /// </summary>
        public static Dictionary<string, double> GetPageDataByLastScoreFromRedis(IRedisClient redis, string zsetKey, int pageSize, double lastScore)
        {
            //ZREVRANGEBYSCORE: from lastScore to '-inf'.
            var luaBody = @"local sets = redis.call('ZREVRANGEBYSCORE', KEYS[1], ARGV[1], '-inf', 'WITHSCORES');
            local result = {};
            local index=0;
            local pageSize=ARGV[2]*1;
            local lastScore=ARGV[1]*1;
            for i = 1, #sets, 2 do 
                if index>=pageSize then
                    break;
                end
                if (lastScore>sets[i+1]*1) then
                    table.insert(result, sets[i]);
                    table.insert(result, sets[i+1]);
                    index=index+1;
                end
            end
            return result";
            //ARGV[1]:lastScore ARGV[2]:pageSize
            var list = redis.ExecLuaAsList(luaBody, new string[] { zsetKey }, new string[] { lastScore.ToString(), pageSize.ToString() });
            var result = new Dictionary<string, double>();
            for (var i = 0; i < list.Count; i += 2)
            {
                result.Add(list[i], Convert.ToDouble(list[i + 1]));
            }
            return result;
        }
        /// <summary>
        /// 添加一项到zset缓存中。
        /// </summary>
        /// <param name="item">要添加到zset的数据项</param>
        /// <param name="maxCount">控制zset最大长度,如果为0,则不控制。</param>
        /// <returns></returns>
        public static string AddItemToZset(IRedisClient redis, string zsetKey, KeyValuePair<string, double> item, int maxCount = 0)
        {
            var items = new Dictionary<string, double>() { { item.Key, item.Value } };
            return AddItemsToZset(redis, zsetKey, items);
        }
        /// <summary>
        /// 添加多项到zset缓存中。
        /// </summary>
        /// <param name="items">要添加到zset的数据列表</param>
        /// <param name="hasCacheExpire">缓存zsetKey是否有设置缓存有效期。如果有设置缓存有效期,则当缓存中无数据时,可能是缓存过期;而如果缓存无有效期,缓存中无数据,就是db和缓存都无数据</param>
        /// <param name="isReload">是否是reload情况,true重载情况;false追加</param>
        /// <param name="maxCount">控制zset最大长度,如果为0,则不控制。</param>
        /// <returns></returns>
        public static string AddItemsToZset(IRedisClient redis, string zsetKey, Dictionary<string, double> items, bool hasCacheExpire = true
            , bool isReload = false, int maxCount = 0)
        {
            //!isReload,是因为如果isReload=true情况无数据,则也要进来重载队列为无数据(即,如果之前有数据要重载为无数据)
            if (!isReload && items.Count <= 0) return null;
            var argArr = new List<string>(items.Count * 2 + 2);//lua参数数组
            //var hasCacheExpire = cacheValidTime != null;
            //第一个lua参数是hasCacheExpire
            argArr.Add(hasCacheExpire ? "1" : "0");
            //第二个lua参数是maxCount
            argArr.Add(maxCount.ToString());
            //组合lua其它参数列表:ZADD的参数
            foreach (var item in items)
            {
                //Add score。 //ZADD KEY_NAME SCORE1 VALUE1
                argArr.Add(item.Value.ToString());
                argArr.Add(item.Key);
            }
            #region lua
            /*
            * 以下lua命令说明。
            * 1.ZREVRANGE从大到小取第一笔数据firstMark;
            * 2.缓存有设置有效期时(hasCacheExpire=1),如果第一笔数据firstMark为nil,则说明列表是空(失效key、未生成key),则不做任何处理,直接返回字符串not_exist_key。因为可能是用户失效数据,用户长期未访问,则不添加,后继来访问时重载数据。
            * 3.如果firstMark标识为no_db_data,则是被api标识为db没数据,而此时因要ZADD数据进来,因此要把此标识删除。其中,ZREMRANGEBYRANK从小到大删除,-1是倒数第一笔。
            * 4.ZADD数据进来
            * 5.KeepLength保持队列长度操作。如果队列长度(由ZCARD获取)超过指定的maxCount,则从队列第一笔开始删除多余元素,即score最小开始删除。
            * 6.maxCount为>0才KeepLength。返回数值:curCount - maxCount。(可以用返回值简单算出队列当前长度curCount)。如果返回值小于等于0则说明没有触发删除操作。
            * 7.maxCount为<=0时,直接返回'no_remove'。
            */
            //清空原来,重新加载数据的情况
            const string reloadLua = "redis.call('DEL', KEYS[1]) ";
            //追加数据到zset的情况
            const string addToLua =
            @"local firstMark = redis.call('ZREVRANGE',KEYS[1],0,0);
            local hasCacheExpire=ARGV[1]*1;
            if hasCacheExpire==1 and firstMark and firstMark[1]==nil then
                return 'not_exist_key';
            end
            if firstMark and firstMark[1]=='{0}' then
                redis.call('ZREMRANGEBYRANK', KEYS[1], -1,-1);
            end";
            const string constAllLua =
            @"{0}
            for i=3, #ARGV, 2 
                do redis.call('ZADD', KEYS[1], ARGV[i], ARGV[i+1]);  
            end
            local maxCount=ARGV[2]*1;
            if maxCount>0 then
              local curCount= redis.call('ZCARD', KEYS[1]);
              local removeCount=curCount - maxCount;
              if removeCount>0 then
                redis.call('ZREMRANGEBYRANK', KEYS[1], 0,removeCount-1);    
              end  
              return removeCount;
            end
            return 'no_remove';";
            #endregion
            var luaBody = string.Format(constAllLua, isReload ? reloadLua : string.Format(addToLua, NoDbDataMark));
            var luaResult = redis.ExecLuaAsString(luaBody, new string[] { zsetKey }, argArr.ToArray());
            return luaResult;
        }
        #endregion
    }
复制代码

 

五、用时间做score,同分同秒问题解决

如果是用时间做score,会有同分同秒问题,比如在TbUserArticle里增加了“时间”栏位。解决方法代码只需稍作微改,参数除了lastScore(此时是“时间”),还需要传lastAid(文章编号)。

1. 缓存处理修改,只动了以下红色粗体字。(注:当zset的两笔数据score相同时,是再根据value排序的):

复制代码
   public static Dictionary<string, double> GetPageDataByLastScoreFromRedis(IRedisClient redis, string zsetKey, int pageSize, double lastScore,string lastAid)
        {
            //ZREVRANGEBYSCORE: from lastScore to '-inf'.
            var luaBody = @"local sets = redis.call('ZREVRANGEBYSCORE', KEYS[1], ARGV[1], '-inf', 'WITHSCORES');
            local result = {};
            local index=0;
            local pageSize=ARGV[2]*1;
            local lastScore=ARGV[1]*1;
            local lastAid=ARGV[3];
            for i = 1, #sets, 2 do 
                if index>=pageSize then
                    break;
                end
                if (lastScore>sets[i+1]*1) or (lastScore==sets[i+1]*1 and lastAid>sets[i]) then
                    table.insert(result, sets[i]);
                    table.insert(result, sets[i+1]);
                    index=index+1;
                end
            end
            return result";
            //ARGV[1]:lastScore ARGV[2]:pageSize
            var list = redis.ExecLuaAsList(luaBody, new string[] { zsetKey }, new string[] { lastScore.ToString(), pageSize.ToString(), lastAid });
            var result = new Dictionary<string, double>();
            for (var i = 0; i < list.Count; i += 2)
            {
                result.Add(list[i], Convert.ToDouble(list[i + 1]));
            }
            return result;
        }
复制代码

2.db取数修改

reload SQL

$”select top 1000 时间,aid from TbUserArticle where uid=@uid order by 时间 desc,aid desc;”;

db中取一页的SQL

$”select top {pageSize} 时间,aid from TbUserArticle where uid=@uid and (时间<{lastScore} or (时间={lastScore} and aid<‘{lastAid}’)) order by 时间 desc,aid desc;”;

这样就可以了,中心思想就是:当“时间={lastScore} ”,那么就增加文章编号比较条件。

基于Redis+Token实现登录认证_mameng1988的博客-CSDN博客_redis token

mikel阅读(445)

来源: 基于Redis+Token实现登录认证_mameng1988的博客-CSDN博客_redis token

登录认证即分为登录和认证两部分,下面聊下他们的实现逻辑。

1 登录
前端调用后端的登录接口,后端验证用户名和密码等成功后,则做下面两个主要工作:

1 以用户id为key,生成的token为value,缓存到redis中并设置过期时间
2 以生成的token为key,用户信息为value,缓存到redis中并设置过期时间
最后将token及用户信息返回给前端,前端将token放在header中,调用其他接口时都携带者。
如果退出登录,则将上面的两条信息从redis中删除即可

2 认证
前端调用其他接口时,先经过一个全局拦截器,这个全局拦截器可以实现Spring的HanderInterceptorAdapter并重写preHandle和afterCompletion方法。

1 在preHandle方法中,首先获取前端请求header中的token,并校验该token是否有效的存放在redis中,如果有效,则判断是否需要token续期(例如:token缓存在redis中的过期时间是2小时,如果token当前剩余时间小于(2-1)小时,则将token的过期时间重新设置成2小时);然后将token及用户信息保存到本地ThreadLocal中;最后返回true。
2 在afterCompletion方法中,将ThreadLocal中的信息清除。我们知道:当一个接口请求处理完成后会调用afterCompletion方法,所以这个ThreadLocal也就是当前接口请求有效,当一个接口请求执行完成后,则将ThreadLocal中的用户信息清除。
为什么不使用JWT的方案?
1、使用了JWT,服务端无法对用户请求进行管理,比如:统计登录用户数、统计一个用户登录了多少次。
2、使用了JWT,服务端无法剔除一个已登录的用户,因为JWT一旦生成,在失效之前,总是有效的。

延申
JWT全称JSON Web Token,由三部分组成:header(头部,用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等)、payload(载荷,就是存放有效信息的地方,在这一部分中存放token过期时间、签发时间等,且payload是明文的)和signature(签名。服务器端可以自行选择一个算法和一个secret,与payload拼接上,得到一个签名,secret并不会在网络中传输,所以客户端无法伪造一个JWT)。

扫码登录:https://mp.weixin.qq.com/s/T3YOppJqW85oK7mUujEReA
————————————————
版权声明:本文为CSDN博主「mameng1998」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/mameng1988/article/details/115866883

配置 Flutter 中国镜像 - 简书

mikel阅读(1094)

来源: 配置 Flutter 中国镜像 – 简书

在搭建 Flutter 环境之前,因为众所周知的原因,有可能被墙,所以需要先为 Flutter 配置中国镜像。

中国镜像地址

国内有两个镜像可以用,一个就是官方 Flutter 社区的国内镜像,另一个是上海交通大学 Linux 用户组的镜像,建议用官方 Flutter 社区的国内镜像。

配置方法

需要设置两个环境变量:PUB_HOSTED_URL 和 FLUTTER_STORAGE_BASE_URL。

Windows

1、\color{red}{计算机} -> \color{red}{属性} -> \color{red}{高级系统设置 } -> \color{red}{环境变量 },打开环境变量设置框。
2、在\color{red}{用户变量}下,选择新建环境变量,添加如下的两个环境变量和值:

变量名                         值
FLUTTER_STORAGE_BASE_URL       https://storage.flutter-io.cn
PUB_HOSTED_URL                https://pub.flutter-io.cn

Linux

打开 ~/.bashrc:

$vim ~/.bashrc

将镜像加入环境变量:

export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
保存后,在运行
$source ~/.bashrc

MacOS

在 ~/.bash_profile 上添加:

export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
保存文件后,在运行
$ source ~/.bash_profile

作者:向上_d821
链接:https://www.jianshu.com/p/897a78aec874
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

U盘提示格式化怎么办?数据如何恢复?

mikel阅读(449)

来源: U盘提示格式化怎么办?数据如何恢复?

U盘打不开提示格式化怎么办呢?U盘里有很多重要的文件该怎么解决呢?当遇到U盘打不开提示需要格式化的情况不要紧张,只需简单几步即可解决问题。下面就和小编一起学习下如何解决U盘打不开提示格式化的问题吧。

 

U盘提示格式化问题与数据恢复

不论是在工作还是学习中,U盘都是使用比较频繁的数据存储工具,但是,很多时候U盘并没有预期的那么耐用,尤其是当U盘质量比较差的时候会更脆弱,更容易出现各种问题。比如,平时明明使用正常的U盘却突然打不开了,提示需要进行格式化才能继续使用。如下图所示,在电脑中尝试打开U盘的时候系统提示”使用驱动器K: 中的光盘之前需要将其格式化。是否需要将其格式化?”

U盘提示格式化怎么办?

遇到这种情况,不要慌张,尤其是U盘中的数据比较重要的时候。首先,要将系统弹出的提示格式化的窗口关闭,这是因为格式化会造成数据丢失,并且增加数据恢复的难度。然后,使用专业性强的数据恢复软件进行恢复,专业的数据恢复软件会对U盘进行深度扫描,并准确识别出可恢复的数据。此外,还可以联系专业的数据恢复人员进行人工恢复,这种恢复方法恢复效果最好,但是费用通常比较高。

这里小编推荐使用DiskGenius数据恢复软件自己进行U盘数据恢复,这个软件操作比较简单,恢复成功率高,并且软件的技术人员提供免费的技术指导。初次使用该软件的用户,可以先使用DiskGenius数据恢复软件免费版进行试用,测试丢失的数据是否还可以恢复。

 

U盘提示格式化,如何进行U盘数据恢复?

DiskGenius是专业级的数据恢复软件,对U盘数据恢复有针对性的算法,可以准确快速的找到可恢复的数据,对于U盘相关的各种数据丢失情况例如U盘删除文件恢复、格式化恢复、U盘打不开、提示格式化等均有不错的数据恢复效果。以下是使用DiskGenius恢复U盘数据的图文教程。

第一步:运行DiskGenius软件。

在DiskGenius官网下载最新版的DiskGenius软件,解压缩后双击应用程序即可打开软件。

U盘提示格式化怎么办

第二步:将需要恢复数据的U盘与电脑连接好,让DiskGenius可以识别到该U盘。

如果系统弹出提示格式化的窗口,直接将其关闭,不要进行格式化。

第三步:在DiskGenius软件中选中提示格式化的U盘,然后点击”恢复文件”按钮。

U盘提示格式化怎么办

第四步:在弹出的恢复文件窗口上点击”开始”按钮,进入扫描阶段。

U盘提示格式化怎么办

如果点击”选择文件类型”按钮,会弹出如下窗口。这里可以指定深度扫描时的文件类型。

U盘提示格式化怎么办

第五步:预览扫描结果中的数据。

这一步很重要,因为文件预览可以帮助确认丢失的数据能否被正常恢复。首先找到想要恢复的文件,然后双击文件打开如下图所示的预览窗口。若文件可以正常预览,那说明文件可以被正常恢复。

U盘提示格式化怎么办

第六步:复制文件。

将扫描结果中的数据复制到其他位置,完成数据恢复。由于数据恢复过程为只读操作,因此,恢复的数据不允许直接复制到这个U盘中。

U盘提示格式化怎么办

U盘提示格式化了如何修复?

当U盘数据恢复完成后,可以尝试任何方法对U盘进行修复。最简单的方法是在系统中将U盘格式化。打开文件资源管理器,右击U盘的盘符并选择”格式化”选项;然后在弹出的窗口上点击”开始”按钮。

U盘提示格式化怎么办

如果Windows 无法完成格式化,那说明U盘无法进行正常的读写操作了,这可能是U盘有坏道了,也可能是U盘坏了。可以使用DiskGenius软件为U盘检测坏道:点击”磁盘”菜单>选择”坏道检测与修复”选项。

U盘提示格式化怎么办

在弹出的坏道检测与修复窗口上点击”开始检测”按钮,开始扫描U盘。

U盘提示格式化怎么办

为什么U盘提示格式化?常见原因有哪些?

这里小编简单介绍一下为什么U盘会突然打不开提示格式化,希望可以帮助大家避免类似情况的发生。

  • 不正常插拔:从电脑移除U盘之前没有点击安全弹出U盘,或是在U盘正在进行读写操作的时候拔出、突然断电等。
  • USB口电压不稳:电脑USB口电压不稳、供电不足、静电等都会给U盘造成损坏。
  • U盘质量差:现在市面上的U盘种类繁多、质量参差不齐,比如一不小心就可能买到山寨U盘、扩容U盘等。
  • 其他原因:病毒、坏道、U盘物理故障等。

 

总结

按照上面的方法一步一步地操作,U盘打不开提示格式化的问题就顺利解决了,是不是很简单。希望小伙伴们以后再遇到类型的情况都能从容应对。最后,小编提醒大家,U盘里的重要数据要及时备份,可以帮我们免去很多数据丢失的烦恼。