注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

元宇宙报告(2021-2022)

元宇宙并非仅仅像扎克伯格这样的技术公司高管的逐利梦想,而且还是一个伟大的技术和工程上的创新,如果它得到正确的应用,也是一个能为我们实现世界带来确实好处的有益工具。它的应用场景不仅仅在社会和娱乐,而是对制造业、城市规划、零售业、教育、医疗乃至整个人类世界有着广泛...
继续阅读 »

元宇宙并非仅仅像扎克伯格这样的技术公司高管的逐利梦想,而且还是一个伟大的技术和工程上的创新,如果它得到正确的应用,也是一个能为我们实现世界带来确实好处的有益工具。它的应用场景不仅仅在社会和娱乐,而是对制造业、城市规划、零售业、教育、医疗乃至整个人类世界有着广泛和深远的意义。

pic_5c80dd79.png
全文共计929字,预计阅读时间10分钟

来源| 五道口供应链研究院(转载请注明来源)

编辑 | 赵超

元宇宙的应用场景可分为核心层、技术层和环境层。核心层是元宇宙最基本最普及的应用场景,具有用户覆盖面广、技术实现度高与生活最贴近的特点,满足用户基本的元宇宙生活需求;技术层是元宇宙的领先场景,具有技术创新性、概念引领性、话语斗争性的特点,是大型企业与跨国公司角逐的关键领域,也是元宇由的重要支撑;环境层是元宇宙发展的综合应用场景,具有复合性、生态性特征,“元宇宙+”生态大量涌现,对用户注意力的争夺常态化。

面向元宇宙的各项技术和应用还在快速发展中,对元宇宙未来发展的趋势预测可以结合技术性与社会变革性展开讨论,并且按照元宇宙率的程度高低进行未来趋势排序。在未来几年,元宇宙的核心维度将越来越强,包括算力、响应力、逼真性、沉浸性、互动性、用户自主性、数字财产保护、数字货币支付等,它在制造业、城市规划、零售业、教育、医疗、娱乐和社交等方面的应用也将越来越多。

元宇宙是现实世界延伸,而不是现实世界的替代。它将深刻地影响我们对时间、空间、真实身体、关系、伦理、工作、学习、教育等的认知。

具体内容如下

pic_f0d6b919.png

pic_59cea2a7.png

pic_8ed34462.png

pic_99b27122.png

pic_0d489692.png

pic_eede67f6.png

pic_020497dc.png

pic_d5ed070a.png

pic_80a1e616.png

pic_d45fb31f.png

pic_3802f855.png

pic_66fa0c90.png

pic_761ac01b.png

pic_8ebc9e52.png

pic_e25d9ab9.png

pic_169e82e7.png

pic_90eb6711.png

pic_80f082d7.png

pic_eafbc460.png

pic_2150c472.png

pic_523068b5.png

pic_45a0c125.png

pic_81b69b9e.png

pic_47e713df.png

pic_7d2368f7.png

pic_862d0245.png

pic_daf11962.png

pic_0c790141.png

pic_7175dfb3.png

pic_592d6c1b.png

pic_61ed918f.png

pic_85856d45.png

pic_fdb2f3fe.png

pic_c166c2dd.png

pic_dd83f3a8.png

pic_bc2ed523.png

pic_88606776.png

pic_f50242eb.png

pic_59605cc8.png

pic_017b2ec5.png

pic_14b17196.png

pic_481bb186.png

pic_230e7802.png

pic_f5fcfb89.png

pic_795c56cd.png

pic_fe88e633.png

pic_24bffe6b.png

pic_b7430b06.png

pic_a09619d9.png

pic_131ff824.png

pic_70280e82.png

pic_eb19bc25.png

pic_21a6dc6b.png

pic_4edf1aae.png

pic_4954f42e.png

pic_0fc296b7.png

pic_e89a97db.png

pic_a0884b6c.png

pic_b50607e1.png

pic_5599603b.png

pic_6d82c58f.png

pic_ee6ae484.png

pic_a5e6a61d.png

pic_8f40faf0.png

来源:五道口供应链研究院

收起阅读 »

中美程序员不完全对比

我是在美国工作过两年,回国经历了逆文化冲击,现在勉强算是适应了国内互联网公司的节奏。随便聊聊,没有崇洋媚外的意图,只是刚好最近被剥削得很不爽,趁机吐槽一下。1.年龄美国公司:同事里 20 多到 70 多岁的都有,众数是三四十的中年人,大部分工作目标都是为了早日...
继续阅读 »


我是在美国工作过两年,回国经历了逆文化冲击,现在勉强算是适应了国内互联网公司的节奏。随便聊聊,没有崇洋媚外的意图,只是刚好最近被剥削得很不爽,趁机吐槽一下。

1.年龄

美国公司:

  • 同事里 20 多到 70 多岁的都有,众数是三四十的中年人,大部分工作目标都是为了早日退休,攒够钱就随时办退休 party。也有些纯粹因为热爱工作、热爱写代码选择不退休的。
  • 我们组的核心成员之一,是位 72 岁的老头,他每天 4 点多起床到公司写一会儿代码,等天全亮就戴上头盔去骑山地车锻炼,9 点多回公司继续工作。对这老头印象深刻,是因为他逻辑清晰、思路锐利,他是 code review 小组的成员,经常在邮件里破口大骂其他人写的代码写得有多烂,被投诉,只好在邮件里道歉,过几天继续骂,在我工作的两年里一直循环。
  • 我的另一位资深同事,是位 68 岁的架构师,热爱工作,每天都乐呵呵的,对我这种新毕业生也很友好,有人问他什么时候退休,他回答说他死的那天。

我国公司:

  • 回国之后我现在工作的公司,员工平均年龄在 30 岁以下。年纪大的都去哪里了呢?极少数在管理层。

2. 加班

美国公司:

  • 从没加过班,晚上发版除外(会默认第二天调休)。
  • 经常正开着会,时间到了 5 点半,产品打断领导说到点了他要回去喂狗(他是一个 50 岁的不婚族,养了一院子狗),然后就散会下班了。
  • 加班需要申请,有次我申请工作日晚上加班,没批准只好回家了。因为加班费会比较高,需要从项目预算走,领导控制预算不给批。
  • 偶尔周末去办公室取东西,几层停车场只有两三辆车。

我国公司:

  • 996 是常事了。
  • 印象比较深的是我司之前有个清华本科+美国硕士的小伙子,每天 7 点半准时下班,结果试用期被辞退了,原因是工作态度不积极,据说后来还和公司打了官司,不知输赢。

3. 代码质量

美国公司:

  • 项目在前期花的时间是最多的,比如说需求分析、架构讨论、技术讨论。
  • 写代码会考虑得比较长远,比较有时间去考虑开发原则、维护成本,领导也会乐意去安排版本来解决技术债务。

我国公司:

  • 国内互联网节奏会要快得多,讲究小步快跑,就几天的开发时间,不管三七二十一先上线再说,刚开始我都惊呆了。

4.工作氛围

美国公司:

  • 老美的公司确实比较尊重员工,在员工关怀上做得比较好。我可以感受到,和领导职位不同,但是我们人格是平等的,彼此尊重。
  • 记得有一次发版前几天,组里程序员说他压力太大,领导给他假期让他放松调整,版本被延迟上线。
  • 美国有 family first 的文化。有个老印同事,家里老人身体不好,公司同意他回印度工作照顾家人,远程跨国工作。经常有同事因为要看孩子比赛请假。领导自己也会偶尔周五请假,因为要去和女儿一起参加学校的公益活动。
  • 对差异性接受度也比较高。同事有变性人、残疾人,大家相处得都很好。

我国公司:

  • 绝大部分领导高高在上(我遇到的),官威很大。请个假,和求他借钱似的,组长还提醒我让我请假原因不要写“旅游”不然可能会不给批假。
  • 记得有个需求,大家都认为不合理没必要,我去找领导沟通,刚提了一句还没展开,领导直接甩脸色“你是领导还是我是领导”。
  • 有个同事因为耿直,和领导不和,被各种排挤冷暴力,逼他自己辞职拒给赔偿金。
  • 开个线上事故复盘会,做 root cause 分析,就像要把人钉在耻辱柱一样,我不理解这对解决问题有什么帮助。

5.工作之外

美国公司:

  • 很注重对健康的投资。至少 1/3 同事有每天早上去健身房的习惯。公司很多球场,晚上下班能看到很多同事在楼下踢足球、打排球。健身不只是为了锻炼,还是很多同事的爱好。看起来平平无奇的程序员,可能都是隐藏的运动高手,多年马拉松选手、山地车骑手遍地都是,还有不少极限运动爱好者。
  • 喜欢看牙医。喜欢看各种体育比赛。喜欢旅游,基本上每年至少一次家庭旅游,游轮是热门项目。
  • 一部分同事热衷慈善回馈社会,小到捐血捐钱做公益,大到组织慈善拍卖会。
  • 据我观察都没啥夜生活,下了班就开车直接回家两点一线,偶尔聚餐也是和同事朋友。可能是我自己的感觉,人和人之间的链接比较淡薄,所以华人码农也会经常吐槽空虚无聊。
  • 已婚同事的其他时间和我国的一样,花在养孩子和投资上。

我国公司:

  • 办公室的好多同事,不敢看体检报告。都是 20 多岁的年轻人,检查出来啥的都有,胆囊炎、结石、痛风。。。前几天还有一个要好的同事请假去做痔疮手术的(捂脸),据他说是因为久坐,加班经常吃小龙虾。
  • 相比之下离职率高太多了,每个月都有几个认识的同事离职,跳槽的、转行的、回老家躺平的。
  • 除了领导们,几乎每个人看起来都很焦虑,都想着退路,想着搞点什么副业。
··········  END  ··············

原文链接:https://www.zhihu.com/question/497793332/answer/2216734220
收起阅读 »

我为大家带来了二十张登录界面?!!!

我为大家带来了二十张登录界面😎!!!这次给大家带来了20张Web登录界面,真的是辛辛苦苦收集了好久,如果有喜欢的不妨给我点个赞吧,感谢!以下所有设计图均来自网络,如有侵权,请联系我删除,感谢各位分享~本人最喜欢的一张😎:(大家在评论区投票选出最喜欢的一张吧)按...
继续阅读 »

我为大家带来了二十张登录界面😎!!!这次给大家带来了20张Web登录界面,真的是辛辛苦苦收集了好久,如果有喜欢的不妨给我点个赞吧,感谢!

以下所有设计图均来自网络,如有侵权,请联系我删除,感谢各位分享~

本人最喜欢的一张😎:(大家在评论区投票选出最喜欢的一张吧)

按钮的圆角让我看了真的是身心愉悦,简约却又不是高雅! 9-Ghostlamp登录页.png

其他的十九张😍:

1.这一款我也是吹爆,层次非常明显,颜色令人舒适,各位觉得怎么样呢?

attachment.png

2.这张就是经典款,左侧突出自己网页的主题,右侧是一个简约的登录,这张非常实用,只需将左侧的图片一换即可!

08f40964340715.5aceef34f2ea3.png

3. 这就很像校园的网站,这样的配色突出校园青春气息,登录框与背景的交叠凸显层次

index_1_2x.png

4.这一款可是上榜2020年6月的设计榜,层次相当丰富,主题也很突出,不愧是上榜的作品

64f84e93d8d542cdbe6a60feacb0bd7f.png

5.哇塞当时看到这一款的时候,我是惊讶到的,这款色彩和层次是真的丰富🤩!!!

f8c2790e78604506b66a5ecb0c6e41d2.png

6.这款也是相当的好,一款卡通风格的旅游网页,用于游戏的官网也可以(例如原神),个人很喜欢

98efa92b63444ab9a3bf2ca077d6407c.png

7.这篇也是很通用的网页,不过右边的圆角登录框也是比较有特色的

721f79495b177004b3bea0ab56b67817.png

8.这一款乍一看很普通,但是当你关注到了细节时,它的背景分块真的是很好,爱了爱了

1328fcb2fb61101d827f407b4bae60b0.png

9.这一款是相较于其他有很大不同的,它更多的是在展示自己的界面美,没有去注意功能的突出,这样别具一格的其实也很不错!

1720266fcb564e00883b3cc377411225.png

10.以蓝色为底色,利用圆角层次分明,也突出了功能,很适合电商网页

2d43921946fe96563215085c13f6e6b2.png

11.这一款除了背景有点留白太多,其他还是很不错的,换一下背景就可以直接商用了~

beautiful-login-pages-01.png

12.也是一如往常的好,乍一看是很常规,但是当你细看的时候,它对于输入框的处理还是很到位的

bffebdfb7df843c5ab51741502a73da7.png

13.这一款就比较有特色的,左侧一个轮播图用于展示网页与业务的特点,右侧的输入框与背景交叠丰富了层次,很棒的作品!

c6d18b48663171.589df36ab8757.png

14.这款就是介绍自己app的一个网页,也是很不错,但是不知道为啥图片有点糊😭

c42a74c97fe724e8b0cec9cdf383cec4.png

15.这款黑色与紫色交融,满满的高级感!

dribbble_-_form_2x_4x.png

16.这一款也在设计榜上,不过,我水平不够,没有欣赏到它的美,蓝色的底色(好了我真是编不下去啦哈哈,原作者看到的话不要打我,这只能证明我的审美不够)

83dd6861ca7f401780933ff7e1b1f112.png

17.漫威蜘蛛侠咱就不用多说了吧,懂得都懂,5星通过👌

matheus-bedeschi-telabhance.png

18.唯美风格,相当舒服了,很适合一些助睡眠的网页🌙

preview.png

19.外星风格,我觉得可以卖玩具了哈哈😝

login-form-page-galaxy-universe-260nw-1765035455.png

每一张都是设计师辛辛苦苦设计的,每一张都很棒,感谢!!!


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

【Flutter App】GetX框架的实践

正在做的这款App是一个打卡软件,旨在让用户能够更好地坚持自己所设置的目标,坚持自己的初心。由于项目还只是在前期阶段,目前根据需要建立了以下结构: 参考了部分官方插件以及结合官方getX文档中建议的目录:暂时没有对state分离出来一层的想法。 以下...
继续阅读 »

正在做的这款App是一个打卡软件,旨在让用户能够更好地坚持自己所设置的目标,坚持自己的初心。

由于项目还只是在前期阶段,目前根据需要建立了以下结构: image.png

参考了部分官方插件以及结合官方getX文档中建议的目录:

image.png

暂时没有对state分离出来一层的想法。 以下是各层详细内容:

image.png

image.png

在使用GetX的时候,往往每次都是用需要手动实例化一个控制器final controller = Get.put(CounterController());,如果每个界面都要实例化一次,有些许麻烦。使用Binding 能解决上述问题,可以在项目初始化时把所有需要进行状态管理的控制器进行统一初始化,直接使用Get.find()找到对应的GetxController使用。

  • 可以将路由、状态管理器和依赖管理器完全集成
  • 这里介绍2种使用方式,推荐第一种使用getx的命名路由的方式
  • 不使用binding,不会对功能有任何的影响。
  • 第一种:使用命名路由进行Binding绑定
/// 入口类
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
/// 这里使用 GetMaterialApp
/// 初始化路由
return GetMaterialApp(
initialRoute: RouteConfig.onePage,
getPages: RouteConfig.getPages,
);
}
}

/// 路由配置
class RouteConfig {
static const String onePage = "/onePage";
static const String twoPage = "/twoPage";

static final List<GetPage> getPages = [
GetPage(
name: onePage,
page: () => const OnePage(),
binding: OnePageBinding(),
),
// GetPage(
// name: twoPage,
// page: () => TwoPage(),
// binding: TwoPageBinding(),
// ),
];
}

/// binding层
class OnePageBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => CounterController());
}
}

/// 逻辑层
class CounterController extends GetxController{
var count = 0;
/// 自增方法
void increase(){
count++;
update();
}
}
  • 第二种:使用initialBinding初始化所有的Binding
/// 入口类
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetMaterialApp(
/// 初始化所有的Binding
initialBinding: AllControllerBinding(),
home: const OnePage(),
);
}
}

/// 所有的Binding层
class AllControllerBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => CounterController());
///Get.lazyPut(() => OneController());
///Get.lazyPut(() => TwoController());
}
}


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

android Handler架构思考

前言写这篇文章不是为了分析Handler怎么使用,目的是想从设计的角度来看Handler的演进过程,以及为什么会出现Looper,MessageQueue,Handler,Message这四个类。一.线程通信的本质?线程区别于进程的主要因素在于,线程之间是共享...
继续阅读 »

前言

写这篇文章不是为了分析Handler怎么使用,目的是想从设计的角度来看Handler的演进过程,以及为什么会出现Looper,MessageQueue,Handler,Message这四个类。

一.线程通信的本质?

线程区别于进程的主要因素在于,线程之间是共享内存的。在android系统中,堆中的对象可以被所有线程访问。因此无论是哪种线程通信方式,考虑到性能问题,一定会选用持有对方线程的某个对象来实现通信。

1.1 AsyncTask

public AsyncTask(@Nullable Looper callbackLooper) {
mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
? getMainHandler()
: new Handler(callbackLooper);

mWorker = new WorkerRunnable() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Result result = null;
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
result = doInBackground(mParams);
Binder.flushPendingCommands();
} catch (Throwable tr) {
mCancelled.set(true);
throw tr;
} finally {
postResult(result);
}
return result;
}
};

mFuture = new FutureTask(mWorker) {
@Override
protected void done() {
try {
postResultIfNotInvoked(get());
} catch (InterruptedException e) {
android.util.Log.w(LOG_TAG, e);
} catch (ExecutionException e) {
throw new RuntimeException("An error occurred while executing doInBackground()",
e.getCause());
} catch (CancellationException e) {
postResultIfNotInvoked(null);
}
}
};
}

private Result postResult(Result result) {
@SuppressWarnings("unchecked")
Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult(this, result));
message.sendToTarget();
return result;
}

从用法可以看出,AsyncTask也是间接通过handler机制实现从当前线程给Looper所对应线程发送消息的,如果不传,默认选的就是主线程的Looper。

1.2 Handler

借助ThreadLocal获取thread的Looper,传输message进行通信。本质上也是持有对象线程的Looper对象。

public Handler(@Nullable Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}

mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}


public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage(r), 0);
}

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}

1.3 View.post(Runnable)

public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}

// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}

getRunQueue().post(action)仅仅是在没有attachToWindow之前缓存了Runnable到数组中

private HandlerAction[] mActions;

public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}

等到attachToWindow时执行,因此本质上也是handler机制进行通信。

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
....
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}

....

}

1.4 runOnUiThread

public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}

通过获取UIThread的handler来通信。

从以上分析可以看出,android系统的四种常见通信方式本质上都是通过Handler技术进行通信。

二.handler解决什么问题?

handler解决线程通信问题,以及线程切换问题。本质上还是共享内存,通过持有其他线程的Looper来发送消息。

我们常提的Handler技术通常包括以下四部分

  • Handler
  • Looper
  • MessageQueue
  • Message

三.从架构的演进来看Handler

3.1 原始的线程通信

String msg = "hello world";
Thread thread = new Thread(){
@Override
public void run() {
super.run();
System.out.println(msg);
}
};
thread.start();

Thread thread1 = new Thread(){
@Override
public void run() {
super.run();
System.out.println(msg);
}
};
thread1.start();

3.2 结构化数据支持

为了发送结构化数据,因此设计了Message

Message msg = new Message();
Thread thread = new Thread(){
@Override
public void run() {
super.run();
msg.content = "hello";
System.out.println(msg);
}
};
thread.start();

Thread thread1 = new Thread(){
@Override
public void run() {
super.run();
System.out.println(msg);
}
};
thread1.start();

3.3 持续通信支持

Message msg = new Message();
Thread thread = new Thread(){
@Override
public void run() {
for (;;){
msg.content = "hello";
}

}
};
thread.start();

Thread thread1 = new Thread(){
@Override
public void run() {
super.run();
for (;;){
System.out.println(msg.content);
}
}
};
thread1.start();

通过无限for循环阻塞线程,Handler中对应的是Looper。

3.4 线程切换支持

上述方法都只能是thread1接受改变,而无法通知thread。因此设计了Handler, 同时封装了发送和接受消息的方法.

class Message{
String content = "123";
String from = "hch";
}

abstract class Handler{
public void sendMessage(Message message){
handleMessage(message);
}

public abstract void handleMessage(Message message);
}

Message msg = new Message();
Thread thread = new Thread(){
@Override
public void run() {
for (;;){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
msg.content = "hello";
if (handler != null){
handler.sendMessage(msg);
}

}

}
};
thread.start();

Thread thread1 = new Thread(){
@Override
public void run() {
super.run();
handler = new Handler(){
@Override
public void handleMessage(Message message) {
System.out.println(message.content);
}
};
}
};
thread1.start();

3.5 对于线程消息吞吐量的支持

abstract class Handler{
BlockingDeque messageQueue = new LinkedBlockingDeque<>();
public void sendMessage(Message message){
messageQueue.add(message);
}

public abstract void handleMessage(Message message);
}

...
Thread thread1 = new Thread(){
@Override
public void run() {
super.run();
handler = new Handler(){
@Override
public void handleMessage(Message message) {
if (!handler.messageQueue.isEmpty()){
System.out.println(messageQueue.pollFirst().content);
}

}
};
}
};
thread1.start();

增加消息队列MessageQueue来缓存消息,处理线程按顺序消费。形成典型的生产者消费者模型。

3.6 对于多线程的支持

上述模型最大的不便之后在于Handler的申明和使用,通信线程双方必须能够非常方便的获取到相同的Handler。

同时考虑到使用线程的便利性,我们不能限制Handler在某个固定的地方申明。如果能够非常方便的获取到对应线程的消息队列,然后往里面塞我们的消息,那该多么美好。

因此Looper和ThreadLocal闪亮登场。

  • Looper抽象了无限循环的过程,并且将MessageQueue从Handler中移到Looper中。
  • ThreadLocal将每个线程通过ThreadLocalMap将Looper与Thread绑定,保证能够通过任意Thread获取到对应的Looper对象,进而获取到Thread所需的关键MessageQueue.

image

//ThreadLocal获取Looper
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

//Looper写入到ThreadLocal
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

// 队列抽象
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

//Handler获取Looper
public Handler(@Nullable Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}

mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}

3.7 google对于Handler的无奈妥协

思考一个问题,由于Handler可以在任意位置定义,sendMessage到对应的线程可以通过线程对应的Looper--MessageQueue来执行,那handleMessage的时候,如何能找到对应的Handler来处理呢?我们可没有好的办法能直接检索到每个消息对应的Handler

两种解决思路

  • 通过公共总线,比如定义Map来索引,这种方式要求map必须定义到所有的线程都能方便获取到的地方,比如可以定义为static
  • 通过消息带Message来携带属性target到对应线程,当消息被消费后,可以通过Message来获得Handler.

第一种方式的问题比较明显,公共总线需要手动维护它的生命周期,google采用的是第二种方式。

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis)
{
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();

if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

3.8.妥协造成Handler泄露问题的根源

由于Message持有了Handler的引用,当我们通过内部类的形式定义Handler时,持有链为

Thread->MessageQueue->Message->Handler->Activity/Fragment

长生命周期的Thread持有了短生命周期的Activity.

解决方式: 使用静态内部类定义Handler,静态内部类不持有外部类的引用,所以使用静态的handler不会导致activity的泄露。

四.总结

  • 1.线程通信本质上通过共享内存来实现
  • 2.android系统常用的四种通信方式,实际都采用Handler实现
  • 3.Handler机制包含四部分Handler,MessageQueue,Message,Looper,它是架构演进的结果。
  • 4.Handler泄露本质是由于长生命周期的对象Thead间接持有了短生命周期的对象造成。

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

Android架构学习之路一-漫谈

架构是什么对于架构,我也有些一知半解,读了一些架构相关的文章,结合实际项目经历,有了自己的一些理解。关于架构是什么?这点可以顾名思义去看,架构=架+构,即整体的一个架子和各个组件之间的组合结构。当然可能不同的程序员对于项目架构的风格和习惯不一样,但是底层的思想...
继续阅读 »

架构是什么

对于架构,我也有些一知半解,读了一些架构相关的文章,结合实际项目经历,有了自己的一些理解。

关于架构是什么?这点可以顾名思义去看,架构=+,即整体的一个架子和各个组件之间的组合结构。当然可能不同的程序员对于项目架构的风格和习惯不一样,但是底层的思想应该都是类似的,诸如我们可能听到起了茧子的“关注点分离”,“低耦合高内聚”,“可扩展可复用易维护”等等,听完这些话,感觉自己懂了,又感觉啥也不懂,好像有所收获了,准备开始写代码的时候,脑子里想的可能又是“工期太赶了,就这样写吧,反正干完这几票就跑路了”。

架构离我们并不远,反而在我们的实际开发中无处不在,它是一个很笼统的概念,上至框架选型,组件化等,下至业务代码,设计模式都能称为架构的一部分。对于架构学习而言,我觉得首先得对面向对象(抽象,继承,多态等)及设计原则有一定的理解,进而结合 Android 常用的一些架构如 MVVM, MVP, MVI 等思想,基础与理论理解清楚了,架构就在日常的开发中,多思考,多结合理论与实际,一点一点地积累起来了。

一起吐槽

我想每个程序员在写代码的时候可能都有这些历程(夸张):

  1. 这坨代码谁写的,怎么要这样写啊,我这个需求该怎么加代码!
  2. (尝试在shit山上小心地走,并添加新代码)写的好难受,shit越改越chou了...
  3. 算了,爷来重构一下,结束掉一切吧!
  4. 重构的一天:我曰,这个地方怎么埋了个雷,我来排一下;哇,怎么这里还有奇怪的逻辑,哼哧哼哧问了之前的同事说是PM改的需求;哎,爱咋地咋地。
  5. Several days later -> git revert -> 下班
  6. 在原来的shit山上再拉一坨,OK,很稳定,提测。

新员工整天都想着重构,而经验丰富的老人早就知道能不动别人的代码就不动的(doge),shit都是互相的,你来我往才能生生不息。写代码嘛,就讲究一个礼尚往来~

shit2_gaitubao_252x387.jpg

背后的原因令人XX

吐槽不是针对某个人,这种现象其实也挺正常的,因为技术在发展和迭代,业务也在丰富和重构,所以在当时看起来,这块代码是很优秀的,只不过由于一步一步的发展,以及一些历史包袱(PM: ??),慢慢的原先的架构可能就跟不上业务需求了,毕竟,架构不是一成不变的,业务在发展,技术在迭代,熵增很正常。到了一定的地步,评估好成本和收入,老老实实提需求重构吧。

当然,虽说随着业务的发展,熵增是必然情况,但是也得注意自己的代码质量呀,毕竟大家应该都不想被后面的同事接手的时候偷偷吐槽你的shit太chou了吧,除非真的抱着干完这一票就溜溜球的想法(doge)。

简而言之原因可以分为两种:

  • 产品的发展,技术的更新迭代
  • 每个人的代码习惯可能不一样,比较参差

怎么做

学好面向对象

听说即使是许多年的老 Java 人,可能在开发中也不怎么注意面向对象的思想,我也经常疏忽这点,啊不对,我不算老 Java 人(囧)。

关于面向对象和面向过程的区别,网上很多介绍的,随便抄了一份:

  • 面向对象:面向对象是一种风格,会以类作为代码的基本单位,通过对象访问,并拥有 封装、继承、抽象、多态 四种特性作为基石,可让其更为智能。
  • 面向过程:分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了,更侧重于功能的设计。

在开始做需求的时候,先别急着写代码,思考一下这个需求的本质是干嘛,用面向对象的思想去抽象这个过程,不是直接搞几个类就可以了的。

举个栗子,之前在老东家的时候做过后台启动的需求,当时的情况是这样子的:针对不同的 Android 版本,可能有一种或者多种不同的启动方式,需要分版本挨个尝试。直截了当的方式是 if else 从头到尾一路火花带闪电,但是觉得这样子肯定是比较难以维护的,所以就把每种启动方式都抽象成了一个 Starter 类:

abstract class Starter {
// 做后台启动的事情
abstract fun handle(context: Context, intent: Intent)

// 是否满足特定Android版本和业务场景等
abstract fun satisfy(): Boolean
}

然后把这些启动方式串起来,通过类似责任链的设计模式去工作,具体代码不贴了,有兴趣可以看看之前的文章: 实战|Android后台启动Activity实践之路。可能现在看当时的代码,会觉得有些稚嫩,但程序员不就是得一直进步的嘛~

接着就是那些常用的面向对象设计模式了,讲道理这些设计模式是很有用的,另外还有面向对象的六大设计原则,这些网上应该很多很多的文章都会讲,此处就不赘述了。

设计架构

前面已经提过随着技术和业务的发展,架构也在一步一步迭代,比如说一开始的单体架构,把用户界面,业务逻辑,数据管理都糅合到一起,到后面根据业务和技术拆分结构,如 MVC, MVP, MVVM, MVI 这些,以及模块化和组件化,服务注册和发现等等,另外还有比较复杂的 Clean 架构,Android 版的 Redux 架构之类的等等。多的一批,哎,好卷。

有时候会产生疑问,这么多新的东西冒出来到底是技术必需的迭代还是由于 OKR, KPI 太卷了(doge)。但能怎么着哦,还是得哼哧哼哧学习。这些文章计划在后面慢慢整理,就当给我年初补充的flag 两年半,加油Android|2021年终总结 吧!

不过架构再多,也都是为业务服务的,没有什么完美的架构,适合当前需求的才是最好的。

写在最后

写代码的时候,记得三思而后行,想一想你写的代码是不是在它该在的位置,是不是以该有的形式存在的。

架构不是一蹴而就的,希望我们有一天的时候,能够从自己写的代码中找到架构的成就感,而不是干几票就跑路的想法,这个系列应该会一直更新,记录我在架构之路上学习的脚印儿,一件一件扒开架构神秘的面纱


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

丢掉丑陋的 toast,会动的 toast 更有趣!

前言我们通常会用 toast(也叫吐司)来显示提示信息,例如网络请求错误,校验错误等等。大多数 App的 toast 都很简单,简单的半透明黑底加上白色文字草草了事,比如下面这种.  说实话,这种toast 的体验很糟糕。假设是新手用户,他...
继续阅读 »

前言

我们通常会用 toast(也叫吐司)来显示提示信息,例如网络请求错误,校验错误等等。大多数 App的 toast 都很简单,简单的半透明黑底加上白色文字草草了事,比如下面这种. image.png 说实话,这种toast 的体验很糟糕。假设是新手用户,他们并不知道 toast 从哪里出来,等出现错误的时候,闪现出来的时候,可能还没抓住内容的重点就消失了(尤其是想截屏抓错误的时候,更抓狂)。这是因为一个是这种 toast 一般比较小,而是动效非常简单,用来提醒其实并不是特别好。怎么破?本篇来给大家介绍一个非常有趣的 toast 组件 —— motion_toast

motion_toast 介绍

从名字就知道,motion_toast 是支持动效的,除此之外,它的颜值还很高,下面是它的一个示例动图,仔细看那个小闹钟图标,是在跳动的哦。这种提醒效果比起常用的 toast 来说醒目多了,也更有趣味性。 center_motion_toast_2.gif 下面我们看看 motion_toast 的特性:

  • 可以通过动画图标实现动效;
  • 内置了成功、警告、错误、提醒和删除类型;
  • 支持自定义;
  • 支持不同的主题色;
  • 支持 null safety;
  • 心跳动画效果;
  • 完全自定义的文本内容;
  • 内置动画效果;
  • 支持自定义布局(LTR 和 RTL);
  • 自定义持续时长;
  • 自定义展现位置(居中,底部或顶部);
  • 支持长文本显示;
  • 自定义背景样式;
  • 自定义消失形式。

可以看到,除了能够开箱即用之外,我们还可以通过自定义来丰富 toast 的样式,使之更有趣。

示例

介绍完了,我们来一些典型的示例吧,首先在 pubspec.yaml 中添加依赖motion_toast: ^2.0.0(最低Dart版本需要2.12)。

最简单用法

只需要一行代码搞定!其他参数在 success 的命名构造方法中默认了,因此使用非常简单。

MotionToast.success(description: '操作成功!').show(context);
复制代码

其他内置的提醒

内置的提醒也支持我们修改默认参数进行样式调整,如标题、位置、宽度、显示位置、动画曲线等等。

// 错误提示
MotionToast.error(
description: '发生错误!',
width: 300,
position: MOTION_TOAST_POSITION.center,
).show(context);

//删除提示
MotionToast.delete(
description: '已成功删除',
position: MOTION_TOAST_POSITION.bottom,
animationType: ANIMATION.fromLeft,
animationCurve: Curves.bounceIn,
).show(context);

// 信息提醒(带标题)
MotionToast.info(
description: '这是一条提醒,可能会有很多行。toast 会自动调整高度显示',
title: '提醒',
titleStyle: TextStyle(fontWeight: FontWeight.bold),
position: MOTION_TOAST_POSITION.bottom,
animationType: ANIMATION.fromBottom,
animationCurve: Curves.linear,
dismissable: true,
).show(context);

不过需要注意的是,一个是 dismissable 参数只对显示位置在底部的有用,当在底部且dismissable为 true 时,点击空白处可以让 toast 提前消失。另外就是显示位置 position 和 animationType 是存在某些互斥关系的。从源码可以看到底部显示的时候,animationType不能是 fromTop,顶部显示的时候 animationType 不能是 fromBottom

void _assertValidValues() {
assert(
(position == MOTION_TOAST_POSITION.bottom &&
animationType != ANIMATION.fromTop) ||
(position == MOTION_TOAST_POSITION.top &&
animationType != ANIMATION.fromBottom) ||
(position == MOTION_TOAST_POSITION.center),
);
}

自定义 toast

自定义其实就是使用 MotionToast 构建一个实例,其中,descriptionicon 和 primaryColor参数是必传的。自定义的参数很多,使用的时候建议看一下源码注释。

MotionToast(
description: '这是自定义 toast',
icon: Icons.flag,
primaryColor: Colors.blue,
secondaryColor: Colors.green[300],
descriptionStyle: TextStyle(
color: Colors.white,
),
position: MOTION_TOAST_POSITION.center,
animationType: ANIMATION.fromRight,
animationCurve: Curves.easeIn,
).show(context);

下面对自定义的一些参数做一下解释:

  • icon:图标,IconData 类,可以使用系统字体图标;
  • primaryColor:主颜色,也就是大的背景底色;
  • secondaryColor:辅助色,也就是图标和旁边的竖条的颜色;
  • descriptionStyle:toast 文字的字体样式;
  • title:标题文字;
  • titleStyle:标题文字样式;
  • toastDuration:显示时长;
  • backgroundType:背景类型,枚举值,共三个可选值,transparentsolid和 lighter,默认是 lighterlighter其实就是加了一层白色底色,然后再将原先的背景色(主色调)加上一定的透明度叠加到上面,所以看起来会泛白。
  • onClose:关闭时回调,可以用于出现多个错误时依次展示,或者是关闭后触发某些动作,如返回上一页。

总结

看完之后,是不是觉得以前的 toast 太丑了?用 motion_toast来一个更有趣的吧。另外,整个 motion_toast 的源码并不多,有兴趣的可以读读源码,了解一下toast 的实现也是不错的。


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

2022年为什么要使用Flutter构建应用程序?

今天每个人都想构建一个应用程序,但是谁又能责怪他们呢?事实上,如今每个人都拥有智能手机,它已迅速成为我们白天最常使用的工具。当我们没有它们时,我们会感到缺少一些东西,我们甚至把它们带到洗手间,我们甚至想不出没有它们,如何出门。无论我们喜欢与否,它对我们生活都在...
继续阅读 »

今天每个人都想构建一个应用程序,但是谁又能责怪他们呢?事实上,如今每个人都拥有智能手机,它已迅速成为我们白天最常使用的工具。当我们没有它们时,我们会感到缺少一些东西,我们甚至把它们带到洗手间,我们甚至想不出没有它们,如何出门。无论我们喜欢与否,它对我们生活都在进行最快,最积极的影响,而这要归功于应用程序。


应用有一种特殊的方式来吸引用户,而其他事物则没有。这里给大家顺便带一下,我之前写过的一篇文章你想好,如何为你的应用做推广了吗?这可能是由于其漂亮的用户界面,经过深思熟虑的用户体验或完美的可用性。这就是为什么编程可以被认为是一门艺术的全部原因,而Flutter在这里为我们提供了这条道路。


什么是Flutter?



"Flutter是Google的UI工具包,用于从单个代码库为移动,Web桌面构建美观,可以的应用程序。



Flutter是一个跨平台框架,使开发人员能够从单个代码库在不同的平台上编程。 这为桌面带来了很多优势。


以下是关于Flutter的一些最特点:



  • 它是开源的

  • 它有一个清晰的文档和一个伟大的社区

  • 由谷歌开发

  • 它有一个适合一切的小部件

  • 提高开发人员的工作效率

  • 一个单一的代码库来统治它们


为什么跨平台如此重要?


跨平台开发允许创建与多个操作系统兼容的软件应用程序。通过这种方式,该技术克服了为每个平台构建唯一代码的原始开发困难。


当然,今天开发一个应用程序意味着出现在两个相关操作系统上:Android和iOS。 在过去,这意味着拥有两个代码,两个团队和两倍的成本。多亏了跨平台,我们可以让一个团队从一个代码库为多个平台创建一个应用程序。


毫无疑问,Flutter并不是唯一的跨平台解决方案,我们可以继续讨论其他人如何尝试采取不同的方向,但这是另一篇文章。但是,有一件事是肯定的,那就是:跨平台将继续存在。 这也是2022年为什么要学习Flutter的理由


单个代码库,单个技术栈。


为了继续我要去的地方,如果管理应用程序的开发是困难的,想象一下管理两种不同技术的开发。每个更改都必须在两种不同的技术中编码和批准。团队必须分为两个,iOS团队和Android团队。这就是为什么让一个团队在单个代码库中工作更有益的原因。


Flutter 擅长的地方


*任何软件开发人员都熟悉这个概念,因为我们做出的每一个选择都决定了优点和缺点。因此,再次选择Flutter在您的项目中有利有弊。


在本文中,我想提供有关它的信息,以便在适合您的项目时进行权衡。以下是它的一些好处


缩短上市时间


Flutter 是一项出色的原型设计技术 - 不仅是 MVP ,还包括具有实际产品功能的应用程序。通过使用Flutter,您将为两个平台(iOS和Android)构建一个应用程序,这可以大大减少开发时间,从而可以更快地将您推向市场。此外,基本上将小部件用于所有内容的可能性以及具有大量可用库的可能性是加快速度的另一个重要因素。


单个开发团队


通过使用Flutter,你可以拥有一个开发团队,而不需要有两个iOS和Android专家团队。您不必担心同步两台计算机,两个代码库,您可以简单地同时在两个平台上发布。


降低开发成本


拥有一个开发团队还有其他好处 ,例如大大降低成本。 这对任何想要构建应用程序的人来说都非常有吸引力,因为进入应用程序市场的经济门槛较低。使其具有成本效益


但是等等,上面说了这么多好处,有什么不利吗


什么时候使用Flutter不方便?


当然,在某些情况下,Flutter并不完全适合您的项目。当这种情况发生时,我们必须简单地接受它,并选择原生开发或其他选择。


例如,如果你的应用需要并且完全依赖于某些特定的硬件设备密集型功能,你可能想要找出是否存在某种Flutter插件。但是,由于它非常新,我强烈建议您进行概念验证,需求分析,以降低技术不是障碍的风险。


此外,还有一些Flutter尚未到达的地方,例如增强现实和3D游戏。在这些情况下,Unity 可能更适合您的项目。请记住,您始终可以尽可能使用 Flutter,然后对于特定的事情使用 native 或 Unity。请记住,将 Flutter 与原生集成始终是一个可用的选项。


想学习另一个技术?


如果你对学习另一种技术有想法,我明白了。但是,请在这里继续等我,让我向您展示它到目前为止是如何演变的:


Flutter的测试版于2018年3月推出,并于2018年12月首次上线。从那时起 ,Flutter稳固了其在市场上的地位,并继续高速崛起。


Flutter社区也在不断发展。Flutter受到大型市场参与者和顶级公司的信任 ,如Google Ads,丰田,还有国内的很多大厂等等。


关于这点你可以去检查你的手机的应用程序,相信会发现很多关于Flutter的踪迹。


最后:


自信地迁移到 Flutter


可以肯定地说,Flutter 有着光明的未来。所以,如果你一直生活在一块石头下并且还没有听说过它,现在就去看看。这是官网flutter.dev/


就我的使用来说,Flutter 不仅达到了我的期望,而且超出了我的期望。这无疑是一项我们从头到尾都爱上的技术。它使我们能够在创纪录的时间内高效地构建应用程序。


这就是我信任 Flutter 的原因。我相信它的未来。我也愿意为此推广Flutter。


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

我们对 DiffUtil 的使用可能被带偏了

我们对 DiffUtil 的使用可能被带偏了 前面都是我的流水账, 觉得看起来很没劲的, 可以直接跳转到本质小节,. DiffUtil 的优势 我在最初接触 DiffUtil 时, 心中便对它有颇多的好感, 包括: 算法听提来就很nb, 一定是个好东西;...
继续阅读 »

我们对 DiffUtil 的使用可能被带偏了



前面都是我的流水账, 觉得看起来很没劲的, 可以直接跳转到本质小节,.



DiffUtil 的优势


我在最初接触 DiffUtil 时, 心中便对它有颇多的好感, 包括:



  1. 算法听提来就很nb, 一定是个好东西;

  2. 简化了 RecyclerView 的刷新逻辑, 无须关心该调用 notifyItemInserted 还是 notifyItemChanged, 一律submitList 就完事了(虽然 notifyDataSetChanged 也能做到, 但是性能拉胯, 而且没有动画);

  3. LiveData 或者 Flow 监听单一 List 数据源时, 往往很难知道, 整个 List 中到底哪些数据项被更新了, 只能调用notifyDataSetChanged 方法, 而 DiffUtil 恰好就能解决这个问题, 无脑 submitList 就完事了.


DiffUtil 代码示例


使用 DiffUtil 时, 代码大致如下:


data class Item (
var id: Long = 0,
var data: String = ""
)

class DiffAdapter : RecyclerView.Adapter<DiffAdapter.MyViewHolder>() {
// AsyncListDiffer 类位于 androidx.recyclerview.widget 包下
// 这里以 AsyncListDiffer 的使用来举例, 使用 ListAdapter 或者直接用 DiffUtil, 也存在后面的问题
private val differ = AsyncListDiffer<Item>(this, object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
// Kotlin中的 == 运算符, 相当于 Java 中调用 equals 方法
return oldItem == newItem
}

private val payloadResult = Any()
override fun getChangePayload(oldItem: Item, newItem: Item): Any {
// payload 用于Item局部字段更新的时候使用,具体用法可以自行搜索了解
// 当检测到同一个Item有更新时, 会调用此方法
// 此方法默认返回null, 此时会触发Item的更新动画, 表现为Item会闪一下
// 当返回值不为null时, 可以关闭Item的更新动画
return payloadResult
}
})

class MyViewHolder(val view: View):RecyclerView.ViewHolder(view){
private val dataTv:TextView by lazy{ view.findViewById(R.id.dataTv) }
fun bind(item: Item){
dataTv.text = item.data
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(LayoutInflater.from(parent.context).inflate(
R.layout.item_xxx,
parent,
false
))
}

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(differ.currentList[position])
}

override fun getItemCount(): Int {
return differ.currentList.size
}

public fun submitList(newList: List<Item>) {
differ.submitList(newList)
}
}

val dataList = mutableListOf<Item>(item1, item2)
differ.submitList(dataList)

以上代码的关键在于以下两个方法的实现, 作用分别是:



  • areItemsTheSame(oldItem: Item, newItem: Item) :比较两个 Item 是否表示同一项数据;

  • areContentsTheSame(oldItem: Item, newItem: Item) :比较两个 Item 的数据是否相同.


DiffUtil 的踩坑过程


上述示例代码很看起来简单, 也比较好理解, 当我们尝试添加一条数据时:


val dataList = mutableListOf<Item>(item1, item2)
differ.submitList(dataList)

// 增加数据
dataList.add(item3)
differ.submitList(dataList)

发现 item3 并未在界面上显示出来, 怎么回事呢? 我们来看 AsyncListDiffer 关键代码的实现:


public void submitList(@Nullable final List<T> newList) {
submitList(newList, null);
}

public void submitList(@Nullable final List<T> newList, @Nullable final Runnable commitCallback) {
// ...省略无关代码
// 注意这里是 Java 代码, 正在比较 newList 与 mList 是否为同一个引用
if (newList == mList) {
// nothing to do (Note - still had to inc generation, since may have ongoing work)
if (commitCallback != null) {
commitCallback.run();
}
return;
}
// ...省略无关代码

mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
@Override
public void run() {
final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
// ...省略 newList 和 mList 的 Diff 算法比较代码
});

// ...省略无关代码
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (mMaxScheduledGeneration == runGeneration) {
latchList(newList, result, commitCallback);
}
}
});
}
});
}

void latchList(
@NonNull List<T> newList,
@NonNull DiffUtil.DiffResult diffResult,
@Nullable Runnable commitCallback) {
// ...省略无关代码
mList = newList;
// 将结果更新到具体的
diffResult.dispatchUpdatesTo(mUpdateCallback);
// ...省略无关代码
}

可以看到, 单参数的 submitList 方法会调用 双参数的 submitList 方法, 重点在于双参数的submitList 方的实现:



  • 首先检查新提交的 newList 与内部持有的 mList 的引用是否相同, 如果相同, 就直接返回;

  • 如果不同的引用, 就对 newListmListDiff 算法比较, 并生成比较结果 DiffUtil.DiffResult;

  • 最后通过 latchList 方法将 newList 赋值给 mList , 并将 Diff 算法的结果 DiffUtil.DiffResult 应用给 mUpdateCallback.



最后的 mUpdateCallback, 其实就是上述示例代码中, 创建 AsyncListDiffer 对象时, 传入的 RecyclerView.Adapter 对象, 这里就不贴代码了.



浅拷贝


分析代码后, 我们可以知道, 每次 submitList 时, 必须传入不同的 List 对象, 否者方法内部不会做 Diff 算法比较, 而是直接返回, 界面也不会刷新. 需要要不同的 List 是吧? 哪还不简单, 我创建一个新的 List 不就行了?


于是我们修改一下 submitList 方法:


class DiffAdapter : RecyclerView.Adapter<DiffAdapter.MyViewHolder>() {
private val differ = AsyncListDiffer<Item>(this, object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
})

// ...省略无关代码

public fun submitList(newList: List<Item>) {
// 创建一个新的 List, 再调用 submitList
differ.submitList(newList.toList())
}
}

相应的测试代码也变成了:


val diffAdapter = ...
val dataList = mutableListOf<Item>(item1, item2)
diffAdapter.submitList(dataList)

// 增加数据
dataList.add(item3)
diffAdapter.submitList(dataList)

// 删除数据
dataList.removeAt(0)
diffAdapter.submitList(dataList)

// 更新数据
dataList[1].data = "最新的数据"
diffAdapter.submitList(dataList)

运行代码后发现, 单独运行"增加数据"和"删除数据"的测试代码, 表现都是正常的, 唯独"更新数据"单独运行时, 界面毫无反应.


其实仔细想想也能明白, 虽然我们调用 differ.submitList(newList.toList()) 方法时, 确实对 List 做了一份拷贝, 但却是浅拷贝, 真正在运行 Diff 算法比较时, 其实是同一个 Item 对象在自己和自己比较(areContentsTheSame 方法参数的 oldItemnewItem 为同一个对象引用), 也就判定为没有数据更新.


data class 的 copy


有的同学,可能有话要说: "你应该在更新 data 字段时, 应该调用 copy 方法, 拷贝一个新的对象, 更新新的值后, 再把原始的 Item 替换掉!".


于是就有了以下代码:


val diffAdapter = ...
val dataList = mutableListOf<Item>(item1, item2)
diffAdapter.submitList(dataList)

// 更新数据
dataList[0] = dataList[0].copy(data = "最新的数据")
diffAdapter.submitList(dataList)

运行代码后, "更新数据"也变得正常了, 当业务比较简单时, 也就到此为止了, 没有新的坑来踩了. 但如果业务比较复杂时, 更新数据的代码可能是这样的:


data class InnerItem(
val innerData: String = ""
)
data class Item(
val id: Long = 0,
val data: String = "",
val innerItem: InnerItem = InnerItem()
)

val diffAdapter = ...
val dataList = mutableListOf<Item>(item1, item2)
diffAdapter.submitList(dataList)

// 更新数据
val item = dataList[0]
// 内部的数据也不能直接赋值, 需要拷贝一份, 否者和上面的情况类似了
val innerNewItem = item.innerItem.copy(innerData = "内部最新的数据")
dataList[0] = item.copy(innerItem = innerNewItem)
diffAdapter.submitList(dataList)

好像稍微有些复杂的样子, 那如果我们嵌套再深一些呢? 我里面还嵌套了一个List呢? 就要依次递归copy, 代码好像就比较复杂了.


此时我们再回想起开篇提到的 DiffUtil 第 2 点优势要打个疑问了. 本来以为会简化代码, 反而使代码变得更复杂了, 我还不如手动赋值, 然后自己去调用 notifyItemXxx , 代码怕是要简单一些.


深拷贝


于是乎, 为了避免递归copy, 导致更新数据的代码变得过于复杂, 就有了深拷贝的方案. 我管你套了几层, 我先深拷贝一份, 我直接对深拷贝的数据进行修改, 然后直接设置回去, 代码如下:


data class InnerItem(
var innerData: String = ""
)
data class Item(
val id: Long = 0,
var data: String = "",
var innerItem: InnerItem = InnerItem()
)


val diffAdapter = ...
val dataList = mutableListOf<Item>(item1, item2)
diffAdapter.submitList(dataList)

// 更新数据
dataList[0] = dataList[0].deepCopy().apply {
innerItem.innerData = "内部最新的数据"
}
diffAdapter.submitList(dataList)


代码看上去又变得简洁了许多, 我们的关注点又来到了深拷贝的如何实现:


利用 Serializable 或者 Parcelable 即可实现对象深拷贝, 具体可自行搜索.



  • 使用 Serializable, 代码看起来比较简单, 但是性能稍差;

  • 使用 Parcelable, 性能好, 但是需要生成更多额外的代码, 看起来不够简洁;


其实选择 Serializable 或者 Parcelable 都无所谓, 看个人喜好即可. 关键在于实现了 Serializable 或者 Parcelable 接口后, Item 中的数据类型会被限制, 要求 Item 中所有的直接或间接字段也必须实现 Serializable Parcelable 接口, 否者就会序列化失败.


比如说, Item 中就不能声明类型为 android.text.SpannableString 的字段(用于显示富文本), 因为 SpannableString 既没有实现 Serializable 接口, 也没有实现 Parcelable 接口.


本质


回过头去, 我们再来审视一下 DiffUtil 两个核心方法:




  • areItemsTheSame(oldItem: Item, newItem: Item) : 比较两个 Item 是否表示同一项数据;

  • areContentsTheSame(oldItem: Item, newItem: Item) : 比较两个 Item 的数据是否相同.



先问个问题, 这两个方法分别为了实现什么目的呢, 或者说他们在算法中起的作用是什么?


简单, 就算不懂 DiffUtil 算法实现(其实是我不懂 o( ̄▽ ̄)o ), 也能猜到, 仅凭 areItemsTheSame 方法我们就能实现以下三种操作:



  • itemRemove

  • itemInsert

  • itemMove


而最后一种 itemChange 操作, 需要 areItemsTheSame 方法先返回 true, 然后调用 areContentsTheSame 方法返回 false, 才能判定为 itemChange 操作, 这也和此方法的注释说明相对应:


This method is called only if {@link #areItemsTheSame(T, T)} returns {@code true} for these items.

所以, areContentsTheSame 方法的作用, 仅仅是为了判定 Item 用于界面显示的部分是否有更新, 而不一定需要调用 equals 方法来全量比较两个item的所有字段. 其实 areContentsTheSame 方法的代码注释也有说明:


This method to check equality instead of {@link Object#equals(Object)} so that you can change its behavior depending on your UI.
For example, if you are using DiffUtil with a {@link RecyclerView.Adapter RecyclerView.Adapter}, you should return whether the items' visual representations are the same.


你会发现, 网上很多教程的代码示例就是用的 equals 来判定数据是否被修改, 然后基于 equals 的比较前提, 更新数据的时候, 又是递归copy, 又是深拷贝, 其实是被带偏了, 思想被限制住了.



改进办法


既然 areItemsTheSame 方法仅用于判定 Item 用于显示的部分是否有更新, 从而判定 itemChange 操作, 那我们完全可以新起一个 contentId 字段, 用于标识内容的唯一性, areItemsTheSame 方法的实现也仅比较 contentId , 代码看起来像这样:


private val contentIdCreator = AtomicLong()
abstract class BaseItem(
open val id: Long,
val contentId: Long = contentIdCreator.incrementAndGet()
)
data class InnerItem(
var innerData: String = ""
)
data class ItemImpl(
override val id: Long,
var data: String = "",
var innerItem: InnerItem = InnerItem()
) : BaseItem(id)

class DiffAdapter : RecyclerView.Adapter<DiffAdapter.MyViewHolder>() {
private val differ = AsyncListDiffer<Item>(this, object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
// contentId不一致时, 就认为此Item数据有更新
return oldItem.contentId == newItem.contentId
}

private val payloadResult = Any()
override fun getChangePayload(oldItem: Item, newItem: Item): Any {
// payload 用于Item局部字段更新的时候使用,具体用法可以自行搜索了解
// 当检测到同一个Item有更新时, 会调用此方法
// 此方法默认返回null, 此时会触发Item的更新动画, 表现为Item会闪一下
// 当返回值不为null时, 可以关闭Item的更新动画
return payloadResult
}
})

// ... 省略无关代码

public fun submitList(newList: List<Item>) {
// 创建新的List的浅拷贝, 再调用submitList
differ.submitList(newList.toList())
}
}


val diffAdapter: DiffAdapter = ...
val dataList = mutableListOf<Item>(item1, item2)
diffAdapter.submitList(dataList)

// 更新数据
// 仅需copy外层的数据,保证contentId不一致即可, 内部嵌套的数据仅需直接赋值即可
// 由于ItemImpl继承自BaseItem, 当执行ItemImpl.copy方法时, 会调用父类BaseItem的构造方法, 生成新的contentId
dataList[0] = dataList[0].copy().apply {
data = "最新的数据"
innerItem.innerData = "内部最新的数据"
}
diffAdapter.submitList(dataList)

因为 areContentsTheSame 方法执行时,需要不同的两个对象比较,所以有字段更新时,还是需要通过 copy 方法生成新的对象.



这种方式存在误判的可能, 因为 ItemImpl 中的一些字段的更新可能不会影响到界面的显示, 此时 areContentsTheSame 方法应该返回 false. 但个人认为这种情况是少数, 误判是可以接受的, 代价仅仅只会额外多更新了一次界面 item 而已.



其实, 了解了本质后, 我们还可以根据自己的业务需求按自己的方式来定制. 比如说用Java该怎么办? 我们也许可以这么做:


class Item{
int id;
boolean isUpdate; // 此字段用于标记此Item是否有更新
String data;
}

class JavaDiffAdapter extends RecyclerView.Adapter<JavaDiffAdapter.MyViewHolder>{
public void submitList(List<Item> dataList){
differ.submitList(new ArrayList<>(dataList));
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(
R.layout.item_xxx,
parent,
false
));
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
// 绑定一次数据后, 将需要更新的标识赋值为false
differ.getCurrentList().get(position).isUpdate = false;
holder.bind(differ.getCurrentList().get(position));
}
@Override
public int getItemCount() {
return differ.getCurrentList().size();
}
class MyViewHolder extends RecyclerView.ViewHolder{
private TextView dataTv;
public MyViewHolder(View itemView) {
super(itemView);
dataTv = itemView.findViewById(R.id.dataTv);
}
private void bind(Item item){
dataTv.setText(item.data);
}
}
private AsyncListDiffer<Item> differ = new AsyncListDiffer<Item>(this, new DiffUtil.ItemCallback<Item>() {
@Override
public boolean areItemsTheSame(@NonNull Item oldItem, @NonNull Item newItem) {
return oldItem.id == newItem.id;
}
@Override
public boolean areContentsTheSame(@NonNull Item oldItem, @NonNull Item newItem) {
// 通过读取isUpdate来确定数据是否有更新
return !newItem.isUpdate;
}
private final Object payloadResult = new Object();
@Nullable
@Override
public Object getChangePayload(@NonNull Item oldItem, @NonNull Item newItem) {
// payload 用于Item局部字段更新的时候使用,具体用法可以自行搜索了解
// 当检测到同一个Item有更新时, 会调用此方法
// 此方法默认返回null, 此时会触发Item的更新动画, 表现为Item会闪一下
// 当返回值不为null时, 可以关闭Item的更新动画
return payloadResult;
}
});
}


// 更新数据
List<Item> dataList = ...;
Item target = dataList.get(0);
// 标识数据有更新
target.isUpdate = true;
target.data = "新的数据";
adapter.submitList(dataList);

最后


其实, 如果我们的 List<Item> 来源于 Room, 其实没有这么多麻烦事, 直接调用 submitList 即可, 不用考虑这里提到的问题, 因为 Room 数据有更新时, 会自动生成新的 List<Item>, 里面的每项 Item 也是新的, 具体代码示例可参考 AsyncListDiffer 或者 ListAdapter 类的顶部的注释. 需要注意的是数据更新太频繁时, 会不会生成了太多的临时对象.


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

快速实现分布式session?厉害了

我们在开发一个项目时通常需要登录认证,常用的登录认证技术实现框架有Spring Security和shiro Spring Security Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于spring的应用程序的...
继续阅读 »

我们在开发一个项目时通常需要登录认证,常用的登录认证技术实现框架有Spring Security和shiro


Spring Security


Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于spring的应用程序的事实上的标准。


Spring Security是一个专注于为Java应用程序提供身份验证和授权的框架。与所有Spring项目一样,Spring Security的真正强大之处在于它可以很容易地扩展以满足定制需求,并且Spring Security和spring更加适配贴合,我们工作中常常使用到Spring Security。


Apache Shiro


Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。


不足:


这些都是认证技术框架,在单体应用中都是常用的技术框架,但是在分布式中,应用可能要部署多份,这时通过nginx分发请求,但是每个单体应用都要可能重复验证,因为他们的seesion数据是放在他们自己服务中的。


Session作用


Session是客户端与服务器通讯会话跟踪技术,服务器与客户端保持整个通讯的会话基本信息。


客户端在第一次访问服务端的时候,服务端会响应一个sessionId并且将它存入到本地cookie中,在之后的访问会将cookie中的sessionId放入到请求头中去访问服务器。


spring-session


Spring Session是Spring的项目之一,Spring Session把servlet容器实现的httpSession替换为spring-session,专注于解决session管理问题。


Spring Session提供了集群Session(Clustered Sessions)功能,默认采用外置的Redis来存储Session数据,以此来解决Session共享的问题。


spring-session提供对用户session管理的一系列api和实现。提供了很多可扩展、透明的封装方式用于管理httpSession/WebSocket的处理。


支持功能



  1. 轻易把session存储到第三方存储容器,框架提供了redis、jvm的map、mongo、gemfire、hazelcast、jdbc等多种存储session的容器的方式。这样可以独立于应用服务器的方式提供高质量的集群。

  2. 同一个浏览器同一个网站,支持多个session问题。 从而能够很容易地构建更加丰富的终端用户体验。

  3. Restful API,不依赖于cookie。可通过header来传递jessionID 。控制session id如何在客户端和服务器之间进行交换,这样的话就能很容易地编写Restful API,因为它可以从HTTP 头信息中获取session id,而不必再依赖于cookie

  4. WebSocket和spring-session结合,同步生命周期管理。当用户使用WebSocket发送请求的时候


分布式seesion实战


步骤1:依赖包


因为是web应用。我们加入springboot的常用依赖包web,加入SpringSession、redis的依赖包,移支持把session存储在redis


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

这里因为是把seesion存储在redis,这样每个服务登录都是去查看redis中数据进行验证的,所有是分布式的。
这里要引入spring-session-data-redis和spring-boot-starter-redis


步骤2:配置文件


spring.application.name=spring-boot-redis
server.port=9090
# 设置session的存储方式,采用redis存储
spring.session.store-type=redis
# session有效时长为15分钟
server.servlet.session.timeout=PT15M

## Redis 配置
## Redis数据库索引
spring.redis.database=1
## Redis服务器地址
spring.redis.host=127.0.0.1
## Redis服务器连接端口
spring.redis.port=6379
## Redis服务器连接密码(默认为空)
spring.redis.password=

步骤3:实现逻辑


初始化用户数据


@Slf4j
@RestController
@RequestMapping(value = "/user")
public class UserController {

Map<String, User> userMap = new HashMap<>();

public UserController() {
//初始化1个用户,用于模拟登录
User u1=new User(1,"user1","user1");
userMap.put("user1",u1);
}
}

这里就不用使用数据库了,初始化两条数据代替数据库,用于模拟登录


登录


 @GetMapping(value = "/login")
public String login(String username, String password, HttpSession session) {
//模拟数据库的查找
User user = this.userMap.get(username);
if (user != null) {
if (!password.equals(user.getPassword())) {
return "用户名或密码错误!!!";
} else {
session.setAttribute(session.getId(), user);
log.info("登录成功{}",user);
}
} else {
return "用户名或密码错误!!!";
}
return "登录成功!!!";
}


登录接口,根据用户名和密码登录,这里进行验证,如果验证登录成功,使用 session.setAttribute(session.getId(), user);把相关信息放到session中。


查找用户


    /**
* 通过用户名查找用户
*/
@GetMapping(value = "/find/{username}")
public User find(@PathVariable String username) {
User user=this.userMap.get(username);
log.info("通过用户名={},查找出用户{}",username,user);
return user;
}


模拟通过用户名查找用户


获取session


  /**
*拿当前用户的session
*/
@GetMapping(value = "/session")
public String session(HttpSession session) {
log.info("当前用户的session={}",session.getId());
return session.getId();
}

退出登录


  /**
* 退出登录
*/
@GetMapping(value = "/logout")
public String logout(HttpSession session) {
log.info("退出登录session={}",session.getId());
session.removeAttribute(session.getId());
return "成功退出!!";
}

这里退出时,要把session中的用户信息删除。


步骤4:编写session拦截器


session拦截器的作用:验证当前用户发来的请求是否有携带sessionid,如果没有携带,提示用户重新登录。


 @Configuration
public class SecurityInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
HttpSession session = request.getSession();
//验证当前session是否存在,存在返回true true代表能正常处理业务逻辑
if (session.getAttribute(session.getId()) != null){
log.info("session拦截器,session={},验证通过",session.getId());
return true;
}
//session不存在,返回false,并提示请重新登录。
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write("请登录!!!!!");
log.info("session拦截器,session={},验证失败",session.getId());
return false;
}
}

步骤5:把拦截器注入到拦截器链中


@Slf4j
@Configuration
public class SessionCofig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SecurityInterceptor())
//排除拦截的2个路径
.excludePathPatterns("/user/login")
.excludePathPatterns("/user/logout")
//拦截所有URL路径
.addPathPatterns("/**");
}
}

步骤6:测试


登录user1用户:http://127.0.0.1:9090/user/login?username=user1&password=user1


查询user1用户session:http://127.0.0.1:9090/user/session


退出登录: http://127.0.0.1:9090/user/logout


登录后查看redis中数据:


image.png
seesion数据已经保存到redis了,到这里我们就整合了使用spring-seesion实现分布式seesion功能。


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

你真的了解反射吗?

1. 啥是反射 1.初识反射 刚开始学反射的时候,我是一脸懵逼的,这玩意真的是“抽象的妈妈给抽象开门-抽象到家了。” 为什么创建对象要先获取 Class 对象?这不多此一举吗?我直接 new 一下不是更简单吗? 什么是程序运行时获取类的属性和方法?平时都是程序...
继续阅读 »

1. 啥是反射


1.初识反射


刚开始学反射的时候,我是一脸懵逼的,这玩意真的是“抽象的妈妈给抽象开门-抽象到家了。”


为什么创建对象要先获取 Class 对象?这不多此一举吗?我直接 new 一下不是更简单吗?


什么是程序运行时获取类的属性和方法?平时都是程序编译出错了再修改代码,我为什么要考虑程序运行时的状态?


我平时开发也用不到,学这玩意有啥用?


后来学了注解、spring、SpringMVC 等技术之后,发现反射无处不在。


2.JVM 加载类


我们写的 java 程序要放到 JVM 中运行,所以要学习反射,首先需要了解 JVM 加载类的过程。



1.我们写的 .java 文件叫做源代码。


2.我们在一个类中写了一个 main 方法,然后点击 IDEA 的 run 按钮,JVM 运行时会触发 jdk 的 javac 指令将源代码编译成 .class 文件,这个文件又叫做字节码文件。



3.JVM 的类加载器(你可以理解成一个工具)通过一个类的全限定名来获取该类的二进制字节流,然后将该 class 文件加载到 JVM 的方法区中。


4.类加载器加载一个 .class 文件到方法区的同时会在堆中生成一个唯一的 Class 对象,这个 Class 包含这个类的成员变量、构造方法以及成员方法。


5.这个 Class 对象会创建与该类对应的对象实例。


所以表面上你 new 了一个对象,实际上当 JVM 运行程序的时候,真正帮你创建对象的是该类的 Class 对象。


也就是说反射其实就是 JVM 在运行程序的时候将你创建的所有类都封装成唯一一个 Class 对象。这个 Class 对象包含属性、构造方法和成员方法。你拿到了 Class 对象,也就能获取这三个东西。


你拿到了反射之后(Class)的属性,就能获取对象的属性名、属性类别、属性值,也能给属性设置值。


你拿到了反射之后(Class)的构造方法,就能创建对象。


你拿到了反射之后(Class)的成员方法,就能执行该方法。


3.反射的概念


JAVA 反射机制是在程序运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 java 语言的反射机制。


知道了 JVM 加载类的过程,相信你应该更加深入的了解了反射的概念。


反射:JVM 运行程序 --> .java 文件 --> .class 文件 --> Class 对象 --> 创建对象实例并操作该实例的属性和方法


接下来我就讲一下反射中的相关类以及常用方法。


2. Class 对象


获取 Class 对象


先建一个 User 类:


public class User {
private String name = "知否君";
public String sex = "男";

public User() {
}

public User(String name, String sex) {
this.name = name;
this.sex = sex;
}

public void eat(){
System.out.println("人要吃饭!");
}

private void run(){
System.out.println("人要跑步!");
}

public String getName() {
return name;
}

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

public String getSex() {
return sex;
}

public void setSex(String sex) {
this.sex = sex;
}
}

获取 Class 对象的三种方式:


1. Class.forName("全类名")


全类名:包名+类名


Class userClass = Class.forName("com.xxl.model.User");

2. 类名.class


Class userClass = User.class;

3. 对象.getClass()


User user = new User();
Class userClass = user.getClass();

尽管有三种方式获取 Class 对象,但是我们一般采用上述第一种方式。


拿到 Class 对象之后,我们就可以操作与它相关的方法了。


3. 获取类名


1.获取完整类名:包名+类名


getName()


Class userClass = Class.forName("com.xxl.model.User");
String name = userClass.getName();
System.out.println(name);

打印结果:



2.获取简单类名:不包括包名


getSimpleName()


Class userClass = Class.forName("com.xxl.model.User");
String simpleName = userClass.getSimpleName();
System.out.println(simpleName);

打印结果:



4. 属性


4.1 获取属性


1.获取所有公有属性:public 修饰


getFields()


Class userClass = Class.forName("com.xxl.model.User");
Field[] fields = userClass.getFields();
for (Field field : fields) {
System.out.println(field);
}

打印结果:



2.获取单个公有属性


getField("属性名")


Class userClass = Class.forName("com.xxl.model.User");
Field field = userClass.getField("sex");
System.out.println(field);

打印结果:



3.获取所有属性:公有+私有


getDeclaredFields()


Class userClass = Class.forName("com.xxl.model.User");
Field[] fields = userClass.getDeclaredFields();
for (Field field : fields) {
System.out.println(field);
}

打印结果:



4.获取单个属性:公有或者私有


getDeclaredField("属性名")


Class userClass = Class.forName("com.xxl.model.User");
Field nameField = userClass.getDeclaredField("name");
Field sexField = userClass.getDeclaredField("sex");
System.out.println(nameField);
System.out.println(sexField);

打印结果:



4.2 操作属性


1.获取属性名称


getName()


Class userClass = Class.forName("com.xxl.model.User");
Field nameField = userClass.getDeclaredField("name");
System.out.println(nameField.getName());

打印结果:



2.获取属性类型


getType()


Class userClass = Class.forName("com.xxl.model.User");
Field nameField = userClass.getDeclaredField("name");
System.out.println(nameField.getType());

打印结果:



3.获取属性值


get(object)


Class userClass = Class.forName("com.xxl.model.User");
Field nameField = userClass.getDeclaredField("sex");
User user = new User();
System.out.println(nameField.get(user));

打印结果:



注: 通过反射不能直接获取私有属性的值,但是可以通过修改访问入口来获取私有属性的值。


设置允许访问私有属性:


field.setAccessible(true);

例如:


Class userClass = Class.forName("com.xxl.model.User");
Field nameField = userClass.getDeclaredField("name");
nameField.setAccessible(true);
User user = new User();
System.out.println(nameField.get(user));

打印方法:



4.设置属性值


set(object,"属性值")


Class userClass = Class.forName("com.xxl.model.User");
Field nameField = userClass.getDeclaredField("name");
nameField.setAccessible(true);
User user = new User();
nameField.set(user,"张无忌");
System.out.println(nameField.get(user));

打印结果:



5. 构造方法


1.获取所有公有构造方法


getConstructors()


Class userClass = Class.forName("com.xxl.model.User");
Constructor[] constructors = userClass.getConstructors();
for (Constructor constructor : constructors) {
System.out.println(constructor);
}

打印结果:



2.获取与参数类型匹配的构造方法


getConstructor(参数类型)


Class userClass = Class.forName("com.xxl.model.User");
Constructor constructor = userClass.getConstructor(String.class, String.class);
System.out.println(constructor);

打印结果:



6. 成员方法


6.1获取成员方法


1.获取所有公共方法


getMethods()


Class userClass = Class.forName("com.xxl.model.User");
Method[] methods = userClass.getMethods();
for (Method method : methods) {
System.out.println(method);
}

打印结果:



我们发现,打印结果除了自定义的公共方法,还有继承自 Object 类的公共方法。


2.获取某个公共方法


getMethod("方法名", 参数类型)


Class userClass = Class.forName("com.xxl.model.User");
Method method = userClass.getMethod("setName", String.class);
System.out.println(method);

打印结果:



3.获取所有方法:公有+私有


getDeclaredMethods()


Class userClass = Class.forName("com.xxl.model.User");
Method[] declaredMethods = userClass.getDeclaredMethods();
for (Method method : declaredMethods) {
System.out.println(method);
}

打印结果:



4.获取某个方法:公有或者私有


getDeclaredMethod("方法名", 参数类型)


Class userClass = Class.forName("com.xxl.model.User");
Method method = userClass.getDeclaredMethod("run");
System.out.println(method);

打印结果:



6.2 执行成员方法


invoke(object,"方法参数")


Class userClass = Class.forName("com.xxl.model.User");
Method method = userClass.getDeclaredMethod("eat");
User user = new User();
method.invoke(user);

打印结果:



注: 通过反射不能直接执行私有成员方法,但是可以设置允许访问。


设置允许执行私有方法:


method.setAccessible(true);

7. 注解


1.判断类上或者方法上时候包含某个注解


isAnnotationPresent(注解名.class)

例如:


Class userClass = Class.forName("com.xxl.model.User");
if(userClass.isAnnotationPresent(Component.class)){
Component annotation = (Component)userClass.getAnnotation(Component.class);
String value = annotation.value();
System.out.println(value);
};

2.获取注解


getAnnotation(注解名.class)

例如:


Class userClass = Class.forName("com.xxl.model.User");
// 获取类上的注解
Annotation annotation1 = userClass.getAnnotation(Component.class);
Method method = userClass.getMethod("eat");
// 获取方法上的某个注解
Annotation annotation2 = userClass.getAnnotation(Component.class);

8. 创建类的实例


1.通过 Class 实例化对象


Class.newInstance()


Class userClass = Class.forName("com.xxl.model.User");
User user = (User)userClass.newInstance();
System.out.println("姓名:"+user.getName()+" 性别:"+user.getSex());

打印结果:



2.通过构造方法实例化对象


constructor.newInstance(参数值)


Class userClass = Class.forName("com.xxl.model.User");
Constructor constructor = userClass.getConstructor(String.class, String.class);
User user = (User)constructor.newInstance\("李诗情", "女"\);
System.out.println("姓名:"+user.getName()+" 性别:"+user.getSex());

打印结果:



9. 反射案例


有一天技术总监对张三说:"张三,听说你最近学反射了呀。那你设计一个对象的工厂类给我看看。"


张三心想:"哟,快过年了,领导这是要给我涨工资啊。这次我一定好好表现一次。"


5分钟过后,张三提交了代码:


public class ObjectFactory {

public static User getUser() {

User user = null;
try {
Class userClass = Class.forName("com.xxl.model.User");
user = (User) userClass.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return user;
}

public static UserService getUserService() {
UserService userService = null;
try {
Class userClass = Class.forName("com.xxl.service.impl.UserServiceImpl");
userService = (UserService) userClass.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return userService;
}
}

技术总监瞄了一眼代码,对张三说:"你这个工厂类存在两个问题。"


1.代码存在大量冗余。如果有一万个类,你是不是要写一万个静态方法?


2.代码耦合度太高。如果这些类存放的包路径发生改变,你再用 forName()获取 Class 对象是不是就会有问题?你还要一个个手动改代码,然后再编译、打包、部署。。你不觉得麻烦吗?


“发散你的思维想一下,能不能只设计一个静态类,通过传参的方式用反射创建对象,传递的参数要降低和工厂类的耦合度。顺便提醒你一下,可以参考一下 JDBC 获取数据库连接参数的方式。”


张三一听:"不愧是总监啊,醍醐灌顶啊!等我 10 分钟。"


10 分钟后,张三再次提交了代码:


object.properties


user=com.xxl.model.User
userService=com.xxl.service.impl.UserServiceImpl

ObjectFactory


public class ObjectFactory {

private static Properties objectProperty = new Properties();

// 静态方法在类初始化时执行,且只执行一次
static{
try {
InputStream inputStream = ObjectFactory.class.getResourceAsStream("/object.properties");
objectProperty.load(inputStream);
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}

public static Object getObject(String key){
Object object = null;
try {
Class objectClass = Class.forName(objectProperty.getProperty(key));
object = objectClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return object;
}
}

测试方法:


@Test
void testObject() {
User user = (User)ObjectFactory.getObject("user");
UserService userService = (UserService)ObjectFactory.getObject("userService");
System.out.println(user);
System.out.println(userService);
}

执行结果:



总监看后连连点头,笑着对张三说:“用 properties 文件存放类的全限定名降低了代码的耦合度,通过传参的方式使用反射创建对象又降低了代码的冗余性,这次改的可以。"


"好啦,今晚项目要上线,先吃饭去吧,一会还要改 bug。”


张三:"..........好的总监。"


10. 反射的作用


我们或多或少都听说过设计框架的时候会用到反射,例如 Spring 的 IOC
就用到了工厂模式和反射来创建对象,BeanUtils 的底层也是使用反射来拷贝属性。所以反射无处不在。


尽管我们日常开发几乎用不到反射,但是我们必须要搞懂反射的原理,因为它能帮我们理解框架设计的原理。


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

HashMap除了死循环之外,还有什么问题?

本篇的这个问题是一个开放性问题,HashMap 除了死循环之外,还有其他什么问题?总体来说 HashMap 的所有“问题”,都是因为使用(HashMap)不当才导致的,这些问题大致可以分为两类: 程序问题:比如 HashMap 在 JDK 1.7 中,并发插...
继续阅读 »

本篇的这个问题是一个开放性问题,HashMap 除了死循环之外,还有其他什么问题?总体来说 HashMap 的所有“问题”,都是因为使用(HashMap)不当才导致的,这些问题大致可以分为两类:



  1. 程序问题:比如 HashMap 在 JDK 1.7 中,并发插入时可能会发生死循环或数据覆盖的问题。

  2. 业务问题:比如 HashMap 无序性造成查询结果和预期结果不相符的问题。


接下来我们一个一个来看。


1.死循环问题


死循环问题发生在 JDK 1.7 版本中,形成的原因是 JDK 1.7 HashMap 使用的是头插法,那么在并发扩容时可能就会导致死循环的问题,具体产生的过程如下流程所示。
HashMap 正常情况下的扩容实现如下图所示:
image.png
旧 HashMap 的节点会依次转移到新 HashMap 中,旧 HashMap 转移的顺序是 A、B、C,而新 HashMap 使用的是头插法,所以最终在新 HashMap 中的顺序是 C、B、A,也就是上图展示的那样。有了这些前置知识之后,咱们来看死循环是如何诞生的?


1.1 死循环执行流程一


死循环是因为并发 HashMap 扩容导致的,并发扩容的第一步,线程 T1 和线程 T2 要对 HashMap 进行扩容操作,此时 T1 和 T2 指向的是链表的头结点元素 A,而 T1 和 T2 的下一个节点,也就是 T1.next 和 T2.next 指向的是 B 节点,如下图所示:
image.png


1.2 死循环执行流程二


死循环的第二步操作是,线程 T2 时间片用完进入休眠状态,而线程 T1 开始执行扩容操作,一直到线程 T1 扩容完成后,线程 T2 才被唤醒,扩容之后的场景如下图所示:
image.png
从上图可知线程 T1 执行之后,因为是头插法,所以 HashMap 的顺序已经发生了改变,但线程 T2 对于发生的一切是不可知的,所以它的指向元素依然没变,如上图展示的那样,T2 指向的是 A 元素,T2.next 指向的节点是 B 元素。


1.3 死循环执行流程三


当线程 T1 执行完,而线程 T2 恢复执行时,死循环就建立了,如下图所示:
image.png
因为 T1 执行完扩容之后 B 节点的下一个节点是 A,而 T2 线程指向的首节点是 A,第二个节点是 B,这个顺序刚好和 T1 扩完容完之后的节点顺序是相反的。T1 执行完之后的顺序是 B 到 A,而 T2 的顺序是 A 到 B,这样 A 节点和 B 节点就形成死循环了,这就是 HashMap 死循环导致的原因。


1.4 解决方案


使用线程安全的容器来替代 HashMap,比如 ConcurrentHashMap 或 Hashtable,因为 ConcurrentHashMap 的性能远高于 Hashtable,因此推荐使用 ConcurrentHashMap 来替代 HashMap。


2.数据覆盖问题


数据覆盖问题发生在并发添加元素的场景下,它不止出现在 JDK 1.7 版本中,其他版本中也存在此问题,数据覆盖产生的流程如下:



  1. 线程 T1 进行添加时,判断某个位置可以插入元素,但还没有真正的进行插入操作,自己时间片就用完了。

  2. 线程 T2 也执行添加操作,并且 T2 产生的哈希值和 T1 相同,也就是 T2 即将要存储的位置和 T1 相同,因为此位置尚未插入值(T1 线程执行了一半),于是 T2 就把自己的值存入到当前位置了。

  3. T1 恢复执行之后,因为非空判断已经执行完了,它感知不到此位置已经有值了,于是就把自己的值也插入到了此位置,那么 T2 的值就被覆盖了。


具体执行流程如下图所示。


2.1 数据覆盖执行流程一


线程 T1 准备将数据 k1:v1 插入到 Null 处,但还没有真正的执行,自己的时间片就用完了,进入休眠状态了,如下图所示:
image.png


2.2 数据覆盖执行流程二


线程 T2 准备将数据 k2:v2 插入到 Null 处,因为此处现在并未有值,如果此处有值的话,它会使用链式法将数据插入到下一个没值的位置上,但判断之后发现此处并未有值,那么就直接进行数据插入了,如下图所示:
image.png


2.3 数据覆盖执行流程三


线程 T2 执行完成之后,线程 T1 恢复执行,因为线程 T1 之前已经判断过此位置没值了,所以会直接插入,此时线程 T2 插入的值就被覆盖了,如下图所示:
image.png


2.4 解决方案


解决方案和第一个解决方案相同,使用 ConcurrentHashMap 来替代 HashMap 就可以解决此问题了。


3.无序性问题


这里的无序性问题指的是 HashMap 添加和查询的顺序不一致,导致程序执行的结果和程序员预期的结果不相符,如以下代码所示:


HashMap<String, String> map = new HashMap<>();
// 添加元素
for (int i = 1; i <= 5; i++) {
map.put("2022-" + i, "Hello,Java:" + i);
}
// 查询元素
map.forEach((k, v) -> {
System.out.println(k + ":" + v);
});

我们添加的顺序:
image.png
我们期望查询的顺序和添加的顺序是一致的,然而以上代码输出的结果却是:
image.png
执行结果和我们预期结果不相符,这就是 HashMap 的无序性问题。我们期望输出的结果是 Hello,Java 1、2、3、4、5,而得到的顺序却是 2、1、4、3、5。


解决方案


想要解决 HashMap 无序问题,我们只需要将 HashMap 替换成 LinkedHashMap 就可以了,如下代码所示:


LinkedHashMap<String, String> map = new LinkedHashMap<>();
// 添加元素
for (int i = 1; i <= 5; i++) {
map.put("2022-" + i, "Hello,Java:" + i);
}
// 查询元素
map.forEach((k, v) -> {
System.out.println(k + ":" + v);
});

以上程序的执行结果如下图所示:
image.png


总结


本文演示了 3 个 HashMap 的经典问题,其中死循环和数据覆盖是发生在并发添加元素时,而无序问题是添加元素的顺序和查询的顺序不一致的问题,这些问题本质来说都是对 HashMap 使用不当才会造成的问题,比如在多线程情况下就应该使用 ConcurrentHashMap,想要保证插入顺序和查询顺序一致就应该使用 LinkedHashMap,但刚开始时我们对 HashMap 不熟悉,所以才会造成这些问题,不过了解了它们之后,就能更好的使用它和更好的应对面试了。


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

还在用策略模式解决 if-else?Map+函数式接口方法才是YYDS!

本文介绍策略模式的具体应用以及Map+函数式接口如何 “更完美” 的解决 if-else的问题。 需求 最近写了一个服务:根据优惠券的类型resourceType和编码resourceId来 查询 发放方式grantType和领取规则实现方式:根据优惠券类型...
继续阅读 »

本文介绍策略模式的具体应用以及Map+函数式接口如何 “更完美” 的解决 if-else的问题。

需求

最近写了一个服务:根据优惠券的类型resourceType和编码resourceId来 查询 发放方式grantType和领取规则

实现方式:

  1. 根据优惠券类型resourceType -> 确定查询哪个数据表

  2. 根据编码resourceId -> 到对应的数据表里边查询优惠券的派发方式grantType和领取规则

优惠券有多种类型,分别对应了不同的数据库表:

  • 红包 —— 红包发放规则表

  • 购物券 —— 购物券表

  • QQ会员

  • 外卖会员

实际的优惠券远不止这些,这个需求是要我们写一个业务分派的逻辑

第一个能想到的思路就是if-else或者switch case:

switch(resourceType){

case "红包"
查询红包的派发方式 
break;
case "购物券"
查询购物券的派发方式
break;
case "QQ会员" :
break;
case "外卖会员" :
break;
......
default : logger.info("查找不到该优惠券类型resourceType以及对应的派发方式");
break;
}

如果要这么写的话, 一个方法的代码可就太长了,影响了可读性。(别看着上面case里面只有一句话,但实际情况是有很多行的)

而且由于 整个 if-else的代码有很多行,也不方便修改,可维护性低。

策略模式

策略模式是把 if语句里面的逻辑抽出来写成一个类,如果要修改某个逻辑的话,仅修改一个具体的实现类的逻辑即可,可维护性会好不少。

以下是策略模式的具体结构(详细可看这篇博客: 策略模式.):


策略模式在业务逻辑分派的时候还是if-else,只是说比第一种思路的if-else 更好维护一点。。。

switch(resourceType){

case "红包"
String grantType=new Context(new RedPaper()).ContextInterface();
break;
case "购物券"
String grantType=new Context(new Shopping()).ContextInterface();
break;

......
default : logger.info("查找不到该优惠券类型resourceType以及对应的派发方式");
break;

但缺点也明显:

  • 如果 if-else的判断情况很多,那么对应的具体策略实现类也会很多,上边的具体的策略实现类还只是2个,查询红包发放方式写在类RedPaper里边,购物券写在另一个类Shopping里边;那资源类型多个QQ会员和外卖会员,不就得再多写两个类?有点麻烦了

  • 没法俯视整个分派的业务逻辑

Map+函数式接口

用上了Java8的新特性lambda表达式

  • 判断条件放在key中

  • 对应的业务逻辑放在value中

这样子写的好处是非常直观,能直接看到判断条件对应的业务逻辑

需求: 根据优惠券(资源)类型resourceType和编码resourceId查询派发方式grantType

上代码:

@Service
public class QueryGrantTypeService {

   @Autowired
   private GrantTypeSerive grantTypeSerive;
   private Map<StringFunction<String,String>> grantTypeMap=new HashMap<>();

   /**
    * 初始化业务分派逻辑,代替了if-else部分
    * key: 优惠券类型
    * value: lambda表达式,最终会获得该优惠券的发放方式
    */
   @PostConstruct
   public void dispatcherInit(){

       grantTypeMap.put("红包",resourceId->grantTypeSerive.redPaper(resourceId));
       grantTypeMap.put("购物券",resourceId->grantTypeSerive.shopping(resourceId));
       grantTypeMap.put("qq会员",resourceId->grantTypeSerive.QQVip(resourceId));
  }

   public String getResult(String resourceType){
  
    
    
       //Controller根据 优惠券类型resourceType、编码resourceId 去查询 发放方式grantType
       Function<String,String> result=getGrantTypeMap.get(resourceType);
       if(result!=null){

      //传入resourceId 执行这段表达式获得String型的grantType
           return result.apply(resourceId);
      }
       return "查询不到该优惠券的发放方式";
  }
}

如果单个 if 语句块的业务逻辑有很多行的话,我们可以把这些 业务操作抽出来,写成一个单独的Service,即:

//具体的逻辑操作

@Service
public class GrantTypeSerive {

   public String redPaper(String resourceId){

       //红包的发放方式
       return "每周末9点发放";
  }
   public String shopping(String resourceId){

       //购物券的发放方式
       return "每周三9点发放";
  }
   public String QQVip(String resourceId){

       //qq会员的发放方式
       return "每周一0点开始秒杀";
  }
}

入参String resourceId是用来查数据库的,这里简化了,传参之后不做处理。

用http调用的结果:

@RestController
public class GrantTypeController {

   @Autowired
   private QueryGrantTypeService queryGrantTypeService;

   @PostMapping("/grantType")
   public String test(String resourceName){

       return queryGrantTypeService.getResult(resourceName);
  }
}


用Map+函数式接口也有弊端:
你的队友得会lambda表达式才行啊,他不会让他自己百度去

最后捋一捋本文讲了什么:

  1. 策略模式通过接口、实现类、逻辑分派来完成,把 if语句块的逻辑抽出来写成一个类,更好维护。

  2. Map+函数式接口通过Map.get(key)来代替 if-else的业务分派,能够避免策略模式带来的类增多、难以俯视整个业务逻辑的问题。

    ————————————————
    作者:zhongh Jim
    来源:https://blog.csdn.net/qq_44384533/article/details/109197926

收起阅读 »

DDD划分领域、子域、核心域、支撑域的目的

名词解释在DDD兴起的原因以及与微服务的关系中曾举了一个研究桃树的例子,如果要研究桃树,将桃树根据器官分成根、茎、叶、花、果实、种子,这每一种器官都可以认为是一个研究领域,而领域又有更加具体的细分,分成子域、核心域、通用域、支撑域等,下面回顾桃树这个例子看上面...
继续阅读 »

名词解释

DDD兴起的原因以及与微服务的关系中曾举了一个研究桃树的例子,如果要研究桃树,将桃树根据器官分成根、茎、叶、花、果实、种子,这每一种器官都可以认为是一个研究领域,而领域又有更加具体的细分,分成子域、核心域、通用域、支撑域等,下面回顾桃树这个例子


看上面这张图 ,如果研究桃树是我们的业务,那么如何更加快速有效的研究桃树呢? 根据回忆,初中课本是这样研究的:

第一步: 确定研究的对象,即研究领域 ,这里是一棵桃树。

第二步: 根据研究对象的某些维度,对其进行进一步的拆分,例如拆分成器官,而器官又可以分成营养器官,生殖器官,其中营养器官包括根、茎、叶,生殖器官包括花、果实、种子,那么这些就是我们要研究的子域。

第三步: 现在就可以最子域进行划分了,找出核心域,通用域,支撑域,至于为什么要这么划分,后面再解释,当我们找到核心域之后,再各个子域进行深一步的划分,划分成组织,例如分成保护组织,营养组织,疏导组织,这就儿也可以理解成将领域继续划分为子域的过程。

第四步:对组织进行进一步的划分,可以分成细胞,例如根毛细胞、导管细胞等等

我们有没有必要继续拆分细胞呢?这个取决于我们研究的业务,例如在之前光学显微镜时,研究到细胞也就截止了,具体到其他业务,也是研究到某一步就不需要继续拆分,而这最小层次的领域,通常就是我们所说的实体,聚合、聚合根、实体以及值对象等内容会在后面深入了解。


下面归纳一下上面提到的几个名词的概念 :

领域: 往往就是业务的某一个部分 , 例如电商的销售部分、物流部分、供应链部分等, 这些对于电商来说就是各个领域(模块),领域主要作用就是用来驱动范围, DDD 会将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。简言之,DDD 的领域就是这个边界内要解决的业务问题域。

子域:相对的一个概念, 我们可以将领域进行进一步的划分 , 这时候就是子域, 甚至可以对子域继续划分形成 子子域(依旧叫子域),就好比当我们研究植物时,如果研究的对象是桃树,那么果实根茎叶是领域,可是如果不仅仅要研究果实,还要研究组织甚至细胞,那么研究的就是果实的子域、组织的子域。

核心域:所有领域中最关键的部分 , 什么意思呢, 就是最核心的部分, 对于业务来说, 核心域是企业根本竞争力, 也是创造利润里最关键的部分 , 例如电商里面那么多领域, 最重要的是什么? 就是销售系统, 无论你是2B还是2C, 还是PDD ,这些核心模块就是核心域。

通用域:除了核心域之外, 还需要自己做的一些领域, 例如鉴权、日志等, 特点是可能被多个领域公用的部分。

支撑域:系统中业务分析阶段最不重点关注的领域, 也就是非核心域非通用域的领域, 例如电商里面的支付、物流,仅仅是为了支撑业务的运转而存在, 甚至可以去购买别人的服务, 这类的领域就是支撑域。

需要注意的是,这些名词在实际的微服务设计和开发过程中不一定用得上,但是可以帮助理解DDD的核心设计思想以及理念,而这些思想和理念在实际的IT战略设计业务建模和微服务设计上都是可以借鉴的。

为什么要划分核心域、通用域、支撑域 ?

通过上面可以知道,决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。还有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域。

这三类子域相较之下,核心域是最重要的,我们下面讲目的的时候还会以核心域为例详细介绍。通用域和支撑域如果对应到企业系统,举例来说的话,通用域则是你需要用到的通用系统,比如认证、权限等等,这类应用很容易买到,没有企业特点限制,不需要做太多的定制化。而支撑域则具有企业特性,但不具有通用性,例如数据代码类的数据字典等系统。

那么为什么要划分出这些新的名词呢? 先想一个问题,对于桃树而言,根、茎、叶、花、果实、种子六个领域哪一个是核心域?

是不是有不同的理解? 有人说是种子,有人说是根,有人说是叶子,也有人说是茎等等,为什么会有这种情况呢?

因为每个人站的角度不一样,你如果是果农,那么果实就是核心域,你的大部分操作应该都是围绕提高果实产量进行,如果你是景区管理员,那么芳菲四月桃花盛开才是你重点关注,如果比是林场工作人员,那么树干才应该是你重点关注的领域,看到没,对于同一个领域划分的子域,每个人都有不同的理解,那么要通过讨论确定核心域,确保大家认同一致,对于实际业务开发来说,参与的人员众多,有业务方面的,有架构师,有后端开发人员,营销市场等等,势必要最开始就确定我们的核心域,除了统一大家的认识之外还有什么好处呢?

对于一个企业来说,预算以及时间是有限的,也就意味着时间以及精力甚至金钱要尽可能多的花在核心的的地方。就好比电商,电商企业那么多,每一家核心域都有所差别,造成的市场结果也千差万别,那么公司战略重点和商业模式应该找到核心域,且重点关注核心域。

总的来说,核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能
属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。

作者:等不到的口琴
来源:https://www.cnblogs.com/Courage129/p/14853600.html


收起阅读 »

Android 启动优化杂谈 | 另辟蹊径

新年快乐 新年伊始,万象更新,虾哥开卷,天下无敌。 首先感谢各位大佬的支持,今年终于喜提掘金优秀作者了。 给各位大佬跪了,祝各位安卓同学新年快乐啊。 开篇 先介绍下徐公大佬的文章,如果有前置需要的话建议看下这个系列。 启动优化这个系列都可以好好看看,感谢徐公大...
继续阅读 »

新年快乐


新年伊始,万象更新,虾哥开卷,天下无敌。


首先感谢各位大佬的支持,今年终于喜提掘金优秀作者了。


给各位大佬跪了,祝各位安卓同学新年快乐啊。


开篇


先介绍下徐公大佬的文章,如果有前置需要的话建议看下这个系列。


启动优化这个系列都可以好好看看,感谢徐公大佬。


本文将不发表任何关于 有向无环图(DAG) 相关,会更细致的说一些我自己的奇怪的观点,以及从一些问题出发,介绍如何做一些有意思的调整。


当前仓库还处于一个迭代状态中,并不是一个特别稳定的状态,所以文章更多的是给大家打开一些小思路。


有想法的同学可以留言啊,我个人感觉一个迭代库才是可以持续演进的啊。



demo 地址 AndroidStartup



demo中很多代码参考了android-startup,感谢这位大佬,u1s1这位大佬很强啊。


Task粒度


这一点其实蛮重要的,相信很多人在接入启动框架之后,更多的事情是把原来可以用的代码,直接用几个Task的把之前的代码包裹起来,之后然后这样就相当于完成了简单的启动框架接入了。


其实这个基本算是违背了启动框架设计的初衷了。我先抛出一个观点,启动框架并不会真实帮你加快多少启动速度,他解决的场景只是让你的sdk的初始化更加的有序,让你可以在长时间的迭代过程中,可以更加稳妥的添加一些新的sdk。


举个栗子,当你的埋点框架依赖了网络库,abtest配置中心也依赖了网络库,然后网络库则依赖了dns等等,之后所有的业务依赖了埋点配置中心图片库等等sdk的初始化完成之后。



当然还是有极限情况下会出现依赖成环问题,这个时候可能就需要开发同学手动的把这个依赖问题给解决了 比如特殊情况网络库需要唯一id,上报库依赖了网络库,而上报库又依赖了唯一id,唯一id又需要进行数据上报



所以我个人的看法启动框架的粒度应该细化到每个sdk的初始化,如果粒度可以越细致当然就越好了。其实一般的启动框架都会对每个task的耗时进行统计的,这样我们后续在跟进对应的问题也会变的更简便,比如查看某些的任务耗时是否增加了啊之类的。


当前我们在设计的时候可能会把一个sdk的初始化拆分成三个部分去做,就是为了去解决这种依赖成环的问题。


子线程间的等待


之前发现项目内的启动框架只保证了放入线程的时候的顺序是按照dag执行的。如果只有主线程和池子大小为1线程池的情况下,这种是ok的。但是如果多线程并发的情况下,这个就变成了一个危险操作了。


所以我们需要在并发场景下加上一个等待的情况下,一定要等到依赖的任务完成了之后,才能继续向下执行初始化代码。


机制的话还是使用CountDownLatch,当依赖的任务都执行完成之后,await会被释放,继续向下执行。而设计上我还是采取了装饰者,不需要使用方更改原始的逻辑就能继续使用了。


代码如下,主要就是一次任务完成的分发,之后发现当前的依赖是有该任务的则latch-1. 当latch到0的情况下就会释放当前线程了。


class StartupAwaitTask(val task: StartupTask) : StartupTask {

private var dependencies = task.dependencies()
private lateinit var countDownLatch: CountDownLatch
private lateinit var rightDependencies: List
var awaitDuration: Long = 0

override fun run(context: Context) {
val timeUsage = SystemClock.elapsedRealtime()
countDownLatch.await()
awaitDuration = (SystemClock.elapsedRealtime() - timeUsage) / 1000
KLogger.i(
TAG, "taskName:${task.tag()} await costa:${awaitDuration} "
)
task.run(context)
}

override fun dependencies(): MutableList {
return dependencies
}

fun allTaskTag(tags: HashSet) {
rightDependencies = dependencies.filter { tags.contains(it) }
countDownLatch = CountDownLatch(rightDependencies.size)
}

fun dispatcher(taskName: String) {
if (rightDependencies.contains(taskName)) {
countDownLatch.countDown()
}
}

override fun mainThread(): Boolean {
return task.mainThread()
}

override fun await(): Boolean {
return task.await()
}

override fun tag(): String {
return task.tag()
}

override fun onTaskStart() {
task.onTaskStart()
}

override fun onTaskCompleted() {
task.onTaskCompleted()
}

override fun toString(): String {
return task.toString()
}

companion object {
const val TAG = "StartupAwaitTask"
}
}

这个算是一个能力的补充完整,也算是多线程依赖必须要完成的一部分。


同时将依赖模式从class变更成tag的形式,但是这个地方还没完成最后的设计,当前还是有点没想好的。主要是解决组件化情况下,可以更随意一点。


线程池关闭


这里是我个人考虑哦,当整个启动流程结束之后,默认情况下是不是应该考虑把线程池关闭了呢。我发现很多都没有写这些的,会造成一些线程使用的泄漏问题。


fun dispatcherEnd() {
if (executor != mExecutor) {
KLogger.i(TAG, "auto shutdown default executor")
mExecutor.shutdown()
}
}

代码如上,如果当前线程池并不是传入的线程池的情况下,考虑执行完毕之后关闭线程池。


dsl + 锚点


因为我既是开发人员,同时也是框架的使用方。所以我自己在使用的过程中发现原来的设计上问题还是很多的,我自己想要插入一个在所有sdk完成之后的任务非常不方便。


然后我就考虑这部分通过dsl的方式去写了动态添加task。kotlin是真的很香,如果后续开发没糖我估计就是个废人了。


image.png


我就是死从这里跳下去,卧槽语法糖真香。


fun Application.createStartup(): Startup.Builder = run {
startUp(this) {
addTask {
simpleTask("taskA") {
info("taskA")
}
}
addTask {
simpleTask("taskB") {
info("taskB")
}
}
addTask {
simpleTask("taskC") {
info("taskC")
}
}
addTask {
simpleTaskBuilder("taskD") {
info("taskD")
}.apply {
dependOn("taskC")
}.build()
}
addTask("taskC") {
info("taskC")
}
setAnchorTask {
MyAnchorTask()
}
addTask {
asyncTask("asyncTaskA", {
info("asyncTaskA")
}, {
dependOn("asyncTaskD")
})
}
addAnchorTask {
asyncTask("asyncTaskB", {
info("asyncTaskB")
}, {
dependOn("asyncTaskA")
await = true
})
}
addAnchorTask {
asyncTaskBuilder("asyncTaskC") {
info("asyncTaskC")
sleep(1000)
}.apply {
await = true
dependOn("asyncTaskE")
}.build()
}
addTaskGroup { taskGroup() }
addTaskGroup { StartupTaskGroupApplicationKspMain() }
addMainProcTaskGroup { StartupTaskGroupApplicationKspAll() }
addProcTaskGroup { StartupProcTaskGroupApplicationKsp() }
}
}

这种DSL写法适用于插入一些简单的任务,可以是一些没有依赖的任务,也可以是你就是偷懒想这么写。好处就是可以避免自己用继承等的形式去写过多冗余的代码,然后在这个启动流程内能看到自己做了些什么事情。


一般等到项目稳定之后,会设立几个锚点任务。他们的作用是后续任务只要挂载到锚点任务之后执行即可,定下一些标准,让后续的同学可以更快速的接入。


我们会把这些设置成一些任务组设置成基准,比如说是网络库,图片库,埋点框架,abtest等等,等到这些任务完成之后,别的业务代码就可以在这里进行初始化了。这样就不需要所有人都写一些基础的依赖关系,也可以让开发同学舒服一点点。


怎么又成环了


在之前的排序阶段,存在一个非常鬼畜的问题,如果你依赖的任务并不在当前的图中存在,就会报出依赖成环问题,但是你并不知道是因为什么原因成环的。


这个就非常不方便开发同学调试问题了,所以我增加了前置任务有效性判断,如果不存在的则会直接打印Log日志,也增加了debugmode,如果测试情况下可以直接已任务不存在的崩溃结束。


ksp


我想偷懒所以用ksp生成了一些代码,同时我希望我的启动框架也可以应用于项目的组件化和插件化中,这样反正就是牛逼啦。


启动任务分组


当前完成的一个功能就是通过注解+ksp生成一个启动任务的分组,这次ksp的版本我们采用的是1.5.30的版本,同时api也有了一些变更。



之前在ksp的文章说过process死循环的问题,最近和米忽悠乌蝇哥交流(吹牛)的时候发现,系统提供一个finish方法,因为process的时候只要有类生成就会重新出发process方法,导致stackoverflow,所以后续代码生成可以考虑迁移到新方法内。



class StartupProcessor(
val codeGenerator: CodeGenerator,
private val logger: KSPLogger,
val moduleName: String
) : SymbolProcessor {
private lateinit var startupType: KSType
private var isload = false
private val taskGroupMap = hashMapOf>()
private val procTaskGroupMap =
hashMapOf>>>()

override fun process(resolver: Resolver): List {
logger.info("StartupProcessor start")

val symbols = resolver.getSymbolsWithAnnotation(StartupGroup::class.java.name)
startupType = resolver.getClassDeclarationByName(
resolver.getKSNameFromString(StartupGroup::class.java.name)
)?.asType() ?: kotlin.run {
logger.error("JsonClass type not found on the classpath.")
return emptyList()
}
symbols.asSequence().forEach {
add(it)
}
return emptyList()
}

private fun add(type: KSAnnotated) {
logger.check(type is KSClassDeclaration && type.origin == Origin.KOTLIN, type) {
"@JsonClass can't be applied to $type: must be a Kotlin class"
}

if (type !is KSClassDeclaration) return

//class type

val routerAnnotation = type.findAnnotationWithType(startupType) ?: return
val groupName = routerAnnotation.getMember("group")
val strategy = routerAnnotation.arguments.firstOrNull {
it.name?.asString() == "strategy"
}?.value.toString().toValue() ?: return
if (strategy.equals("other", true)) {
val key = groupName
if (procTaskGroupMap[key] == null) {
procTaskGroupMap[key] = mutableListOf()
}
val list = procTaskGroupMap[key] ?: return
list.add(type.toClassName() to (routerAnnotation.getMember("processName")))
} else {
val key = "${groupName}${strategy}"
if (taskGroupMap[key] == null) {
taskGroupMap[key] = mutableListOf()
}
val list = taskGroupMap[key] ?: return
list.add(type.toClassName())
}
}

private fun String.toValue(): String {
var lastIndex = lastIndexOf(".") + 1
if (lastIndex <= 0) {
lastIndex = 0
}
return subSequence(lastIndex, length).toString().lowercase().upCaseKeyFirstChar()
}
// 开始代码生成逻辑
override fun finish() {
super.finish()
// logger.error("className:${moduleName}")
try {
taskGroupMap.forEach { it ->
val generateKt = GenerateGroupKt(
"${moduleName.upCaseKeyFirstChar()}${it.key.upCaseKeyFirstChar()}",
codeGenerator
)
it.value.forEach { className ->
generateKt.addStatement(className)
}
generateKt.generateKt()
}
procTaskGroupMap.forEach {
val generateKt = GenerateProcGroupKt(
"${moduleName.upCaseKeyFirstChar()}${it.key.upCaseKeyFirstChar()}",
codeGenerator
)
it.value.forEach { pair ->
generateKt.addStatement(pair.first, pair.second)
}
generateKt.generateKt()
}
} catch (e: Exception) {
logger.error(
"Error preparing :" + " ${e.stackTrace.joinToString("\n")}"
)
}
}
}


class StartupProcessorProvider : SymbolProcessorProvider {
override fun create(
environment: SymbolProcessorEnvironment
)
: SymbolProcessor {
return StartupProcessor(
environment.codeGenerator,
environment.logger,
environment.options[KEY_MODULE_NAME] ?: "application"
)
}
}

fun String.upCaseKeyFirstChar(): String {
return if (Character.isUpperCase(this[0])) {
this
} else {
StringBuilder().append(Character.toUpperCase(this[0])).append(this.substring(1)).toString()
}
}

const val KEY_MODULE_NAME = "MODULE_NAME"

其中processor被拆分成两部分,SymbolProcessorProvider负责构造,SymbolProcessor则负责处理ast逻辑。以前的initapi 被移动到SymbolProcessorProvider中了。


逻辑也比较简单,收集注解,然后基于注解的入参生成一个taskGroup逻辑。这个组会被我手动加入到启动流程内。


未完成


另外我想做的一件事就是通过注解来去生成一个Task任务,然后通过不同的注解的排列组合,组合出一个新的task任务。


这部分功能还在设计中,后续完成之后再给大家水一篇好了。


调试组件


这部分是我最近设计的重中之重了。当接了启动框架这个活之后,更多的时候你是需要去追溯启动变慢的问题的,我们把这种情况叫做劣化。如何快速定位劣化问题也是启动框架所需要关心的。


一开始我们打算通过日志上报,之后在版本发布之后重新推导线上的任务耗时,但是因为计算出来的是平均值,而且我们的自动化测试同学每个版本发布前都会跑自动化case,观察启动时间的状况,如果时间均值变长就会来通知我们,这个时候看埋点数据其实挺难发现问题的。


核心原因还是我想偷懒,因为排查问题必须要基于之前的版本和当前版本进行对比,比较各个task之间的耗时状况,我们当前大概应该有30+的启动任务,这尼玛不是要了我老命了吗。


所以我和我大佬沟通了下,就对这部分进行了立项,打算折腾一个调试工具,可以记录下启动任务的耗时,还有启动任务的列表,通过本地对比的形式,可以快速推导出出现问题任务,方便我们快速定位问题。



小贴士 调试工具的开发最好不要有太多的依赖 然后通过debug 的buildtype来加入 所以使用了contentprovider来初始化



device-2022-01-02-120141.png


启动时间轴


江湖上一直流传着我的外号-ui大湿,在下也不是浪得虚名,ui大湿画出来的图形那叫一个美如画啊。


device-2022-01-02-120203.png


这部分原理比较简单,我们把当前启动任务的数据进行了收集,然后根据线程名进行分发,记录任务开始和结束的节点,然后通过图形化进行展示。


如果你第一时间看不懂,可以参考下自选股列表,每一列都是代表一个线程执行的时间轴。


启动顺序是否变更


我们会在每次启动的时候将当前启动的顺序进行数据库记录,然后通过数据库找出和当前hashcode不一样的任务,然后比对下用textview的形式展示出来,方便测试同学反馈问题。


这个地方的原理的,我是将整个启动任务通过字符串拼接,然后生成一个字符串,之后通过字符串的hashcode作为唯一标识符,不同字符串生成的hashcode也是不同的。


这里有个傻事就是我一开始对比的是stringbuilder的hashcode,然后发现一样的任务竟然值变更了,我真傻真的。


device-2022-01-02-120221.png


别问,问就是ui大湿,textview不香?


平均任务耗时


这个地方的数据库设计让我思考了好一会,之后我按照天为维度,之后记录时间和次数,然后在渲染的时候取出均值。


之后把之前的历史数据取出来,然后进行汇总统计,之后重新生成list,一个当前task下面跟随一个历史的task。然后进行牛逼的ui渲染。


device-2022-01-02-120246.png


这个时候你要喷了啊,为什么你全部都是textview还自称ui大湿啊。


u=3176961766,3525766337&fm=253&fmt=auto&app=138&f=JPEG.webp


虾扯蛋你听过吗,没错就是这样的。


总结


卷来,天不生我逮虾户,卷道万古长如夜。


与诸君共勉。


真的总结


UI方面我后续还是会进行迭代的,毕竟第一个版本丑陋不堪主要是想完成数据的手机,而且开发看起来也不是特别显眼,后面可能会把差异部分直接输出。


做大做强,搞一波大新闻。


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

SpringBoot 三大开发工具,你都用过么?

1、SpringBoot Dedevtools 他是一个让SpringBoot支持热部署的工具,下面是引用的方法 要么在创建项目的时候直接勾选下面的配置: 要么给springBoot项目添加下面的依赖: <dependency> <...
继续阅读 »

1、SpringBoot Dedevtools


他是一个让SpringBoot支持热部署的工具,下面是引用的方法


要么在创建项目的时候直接勾选下面的配置:


图片


要么给springBoot项目添加下面的依赖:


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>



  • idea修改完代码后再按下 ctrl + f9 使其重新编译一下,即完成了热部署功能




  • eclipse是按ctrl + s保存 即可自动编译




如果你想一修改代码就自动重新编译,无需按ctrl+f9。只需要下面的操作:


1.在idea的setting中把下面的勾都打上


图片


2.进入pom.xml,在build的反标签后给个光标,然后按Alt+Shift+ctrl+/


图片


3.然后勾选下面的东西,接着重启idea即可


图片


2、Lombok


Lombok是简化JavaBean开发的工具,让开发者省去构造器,getter,setter的书写。


在项目初始化时勾选下面的配置,即可使用Lombok


图片


或者在项目中导入下面的依赖:


<dependency>    
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

使用时,idea还需要下载下面的插件:


图片


下面的使用的例子


@AllArgsConstructor
//全参构造器
@NoArgsConstructor
//无参构造器
@Data//getter + setterpublic
class User {
private Long id;
private String name;
private Integer age;
private String email;
}

3、Spring Configuration Processor


该工具是给实体类的属性注入开启提示,自我感觉该工具意义不是特别大!


因为SpringBoot存在属性注入,比如下面的实体类:


@Component
@ConfigurationProperties(prefix = "mypet")
public class Pet {
private String nickName;
private String strain;

public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public String getStrain() {
return strain;
}
public void setStrain(String strain) {
this.strain = strain;
}
@Override public String toString() {
return "Pet [nickName=" + nickName + ", strain=" + strain + "]";
}
}

想要在application.properties和application.yml中给mypet注入属性,却没有任何的提示,为了解决这一问题,我们在创建SpringBoot的时候勾选下面的场景:


图片


或者直接在项目中添加下面的依赖:


<dependency>     
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

并在build的标签中排除对该工具的打包:(减少打成jar包的大小)


<build>    
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins></build>


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

过年想要红包?年前你先把咱们的红包系统上线了呗!

红包分类 产品需求设计分为两类红包,个人红包,群红包。群红包又分为专属、均分、群手气三种。分别适应不同的场景。如下图所示: 红包实现 发红包流程: 1、用户进入发红包界面发起请求; 2、服务端接受到请求后,对用户的红包金额进行冻结(前提用户事先开通余额账户)...
继续阅读 »

红包分类


产品需求设计分为两类红包,个人红包,群红包。群红包又分为专属、均分、群手气三种。分别适应不同的场景。如下图所示:
image.png


红包实现


发红包流程:


1、用户进入发红包界面发起请求;
2、服务端接受到请求后,对用户的红包金额进行冻结(前提用户事先开通余额账户)。
3、是否余额充足(兜底教研),如果充足发红包成功,并且生成红包记录,如果不充足提示错误信息。
4、推送最终结果给用户,如果发成功了会推两条消息,一个是发送人告诉用户红包发成功了,且推送群/个人立即领取。
5、如果红包发成功后,发一个延迟1天的 MQ 消息,做一个超期未退款处理。把冻结账户的钱返还给用户。
image.png


领红包流程:


当用户收到红包的推送过后,用户就可以通过该消息,进行红包的领取。
1、参数状态判断,判断红包是否过期,红包是否领完,红包是否重复领取等逻辑;
2、生成红包领取记录;
3、生成红包入账记录,对领取者上账。生成余额流水,并且增加余额流水。
4、减少发红包者的冻结余额,完成红包领取流程。
未命名文件.png


红包高并发


高并发设计


对于群手气红包肯定会存在并发问题,比如微信群红包领取的时候。一个 200 人的群同时来领取1个 200 元,10 人可以领取的红包,从用户的发起可能到领取基本是在 1-2 秒内领完。
怎么样保证既能高效的领取,又能保证金额都能正确,且不会记错账。我们采用的方案就是“一快一慢”的方案。


1、对于群红包的场景,我们首先会将红包金额提前计算。然后存储到 Redis 中 Key : redpacket:amount_list:#{id}存储的结构是一个 List 集合。
2、 每次领取的时候我们会做一个 rpop操作,获取到红包的金额。由于这块是redis 操作,是非常高效的。
image.png
3、为了保证数据的持久性、可靠性。我们会生成一个领取记录到 mysql 数据库中持久化。
4、然后发送红包领取成功的消息出去,在记账服务中进行订阅,异步入账。


具体的流程如下:
image.png


幂等性保证


为了保证领取过程用户有序的领取,且保证一个用户只能领取成功一次,如果第二次来领取,咱们就提示已经领取过了,不能重复领取。这是我们在高并发场景下必须严保证的问题。当时我们选择的是通过分布式锁的方式来解决的,锁的 key 设计如下:
redpacket:amount_list:#{id}_#{user_id}
这样设计的好处就是能够保证一当前分布式系统中,当前只能一个有效的请求进入正真的处理逻辑中。
兜底保障:
1、在领取记录表中增加 user_id, redpacket_id 为唯一索引。
2、对红包的剩余金额做乐观锁更新(可以使用 tk.mapper 的 @Version)。


可拓展性设计


为了保证可拓展性的设计,我们当时采用的是 策略 + 模板方法的设计模型进行低耦合设计。


手气红包金额计算


我们采用的是中位数随机算法(大致的逻辑就是控制一个中位数的值最大金额,最小金额的区间不能超过中位数的浮动水位线),更多的随机算法,大家可以参阅:为啥春节抢红包总不是手气最佳?看完微信抢红包算法你就明白了!


金额随机代码


public class RedPacketUtils {

private static final Random random = new Random();


/**
* 根据总数分割个数及限定区间进行数据随机处理
* 数列浮动阀值为0.95
*
* @param totalMoney - 被分割的总数
* @param splitNum - 分割的个数
* @param min - 单个数字下限
* @param max - 单个数字上限
* @return - 返回符合要求的数字列表
*/
public static List<BigDecimal> genRandomList(BigDecimal totalMoney, Integer splitNum, BigDecimal min, BigDecimal max) {
totalMoney = totalMoney.multiply(new BigDecimal(100));
min = min.multiply(new BigDecimal(100));
max = max.multiply(new BigDecimal(100));
List<Integer> li = genRandList(totalMoney.intValue(), splitNum, min.intValue(), max.intValue(), 0.95f);
List<BigDecimal> randomList = new CopyOnWriteArrayList<>();
for (Integer v : li) {
BigDecimal randomVlue = new BigDecimal(v).divide(new BigDecimal(100));
randomList.add(randomVlue);
}

randomList = randomArrayList(randomList);
return randomList;
}

/**
* 根据总数分割个数及限定区间进行数据随机处理
*
* @param total - 被分割的总数
* @param splitNum - 分割的个数
* @param min - 单个数字下限
* @param max - 单个数字上限
* @param thresh - 数列浮动阀值[0.0, 1.0]
*/
public static List<Integer> genRandList(int total, int splitNum, int min, int max, float thresh) {
assert total >= splitNum * min && total <= splitNum * max : "请校验红包参数设置的合理性";
assert thresh >= 0.0f && thresh <= 1.0f;
// 平均分配
int average = total / splitNum;
List<Integer> list = new ArrayList<>(splitNum);
int rest = total - average * splitNum;
for (int i = 0; i < splitNum; i++) {
if (i < rest) {
list.add(average + 1);
} else {
list.add(average);
}
}
// 如果浮动阀值为0则不进行数据随机处理
if (thresh == 0) {
return list;
}
// 根据阀值进行数据随机处理
int randOfRange = 0;
int randRom = 0;
int nextIndex = 0;
int nextValue = 0;
int surplus = 0;//多余
int lack = 0;//缺少
for (int i = 0; i < splitNum - 1; i++) {
nextIndex = i + 1;
int itemThis = list.get(i);
int itemNext = list.get(nextIndex);
boolean isLt = itemThis < itemNext;
int rangeThis = isLt ? max - itemThis : itemThis - min;
int rangeNext = isLt ? itemNext - min : max - itemNext;
int rangeFinal = (int) Math.ceil(thresh * (Math.min(rangeThis, rangeNext) + 100));
randOfRange = random.nextInt(rangeFinal);
randRom = isLt ? 1 : -1;
int iValue = list.get(i) + randRom * randOfRange;
nextValue = list.get(nextIndex) + randRom * randOfRange * -1;
if (iValue > max) {
surplus += (iValue - max);
list.set(i, max);
} else if (iValue < min) {
list.set(i, min);
lack += (min - iValue);
} else {
list.set(i, iValue);
}
list.set(nextIndex, nextValue);
}
if (nextValue > max) {
surplus += (nextValue - max);
list.set(nextIndex, max);
}
if (nextValue < min) {
lack += (min - nextValue);
list.set(nextIndex, min);
}
if (surplus - lack > 0) {//钱发少了 给低于max的凑到max
for (int i = 0; i < list.size(); i++) {
int value = list.get(i);
if (value < max) {
int tmp = max - value;
if (surplus >= tmp) {
surplus -= tmp;
list.set(i, max);
} else {
list.set(i, value + surplus);
return list;
}
}
}
} else if (lack - surplus > 0) {//钱发多了 给超过高于min的人凑到min
for (int i = 0; i < list.size(); i++) {
int value = list.get(i);
if (value > min) {
int tmp = value - min;
if (lack >= tmp) {
lack -= tmp;
list.set(i, min);
} else {
list.set(i, min + tmp - lack);
return list;
}
}
}
}
return list;
}

/**
* 打乱ArrayList
*/
public static List<BigDecimal> randomArrayList(List<BigDecimal> sourceList) {
if (sourceList == null || sourceList.isEmpty()) {
return sourceList;
}
List<BigDecimal> randomList = new CopyOnWriteArrayList<>();
do {
int randomIndex = Math.abs(new Random().nextInt(sourceList.size()));
randomList.add(sourceList.remove(randomIndex));
} while (sourceList.size() > 0);

return randomList;
}


public static void main(String[] args) {
Long startTi = System.currentTimeMillis();
List<BigDecimal> li = genRandomList(new BigDecimal(100000), 26000, new BigDecimal(2), new BigDecimal(30));
li = randomArrayList(li);
BigDecimal total = BigDecimal.ZERO;
System.out.println("======total=========total:" + total);
System.out.println("======size=========size:" + li.size());
Long endTi = System.currentTimeMillis();
System.out.println("======耗时=========:" + (endTi - startTi) / 1000 + "秒");
}
}

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

SpringBoot:如何优雅地进行参数传递、响应数据封装、异常处理?

在项目开发中,接口与接口之间、前后端之间的数据传输都使用 JSON 格式。 1 fastjson使用 阿里巴巴的 fastjson是目前应用最广泛的JSON解析框架。本文也将使用fastjson。 1.1 引入依赖 <dependency> &nb...
继续阅读 »

在项目开发中,接口与接口之间、前后端之间的数据传输都使用 JSON 格式。


1 fastjson使用


阿里巴巴的 fastjson是目前应用最广泛的JSON解析框架。本文也将使用fastjson。


1.1 引入依赖


<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.35</version>
</dependency>

2 统一封装返回数据


在web项目中,接口返回数据一般要包含状态码、信息、数据等,例如下面的接口示例:


import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author guozhengMu
 * @version 1.0
 * @date 2019/8/21 14:55
 * @description
 * @modify
 */
@RestController
@RequestMapping(value = "/test", method = RequestMethod.GET)
public class TestController {
    @RequestMapping("/json")
    public JSONObject test() {
        JSONObject result = new JSONObject();
        try {
            // 业务逻辑代码
            result.put("code", 0);
            result.put("msg", "操作成功!");
            result.put("data", "测试数据");
        } catch (Exception e) {
            result.put("code", 500);
            result.put("msg", "系统异常,请联系管理员!");
        }
        return result;
    }
}

这样的话,每个接口都这样处理,非常麻烦,需要一种更优雅的实现方式。


2.1 定义统一的JSON结构


统一的 JSON 结构中属性包括数据、状态码、提示信息,其他项可以自己根据需要添加。一般来说,应该有默认的返回结构,也应该有用户指定的返回结构。由于返回数据类型无法确定,需要使用泛型,代码如下:


public class ResponseInfo<T> {
    /**
     * 状态码
     */
    protected String code;
    /**
     * 响应信息
     */
    protected String msg;
    /**
     * 返回数据
     */
    private T data;

    /**
     * 若没有数据返回,默认状态码为 0,提示信息为“操作成功!”
     */
    public ResponseInfo() {
        this.code = 0;
        this.msg = "操作成功!";
    }

    /**
     * 若没有数据返回,可以人为指定状态码和提示信息
     * @param code
     * @param msg
     */
    public ResponseInfo(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    /**
     * 有数据返回时,状态码为 0,默认提示信息为“操作成功!”
     * @param data
     */
    public ResponseInfo(T data) {
        this.data = data;
        this.code = 0;
        this.msg = "操作成功!";
    }

    /**
     * 有数据返回,状态码为 0,人为指定提示信息
     * @param data
     * @param msg
     */
    public ResponseInfo(T data, String msg) {
        this.data = data;
        this.code = 0;
        this.msg = msg;
    }
    // 省略 get 和 set 方法
}

2.2 使用统一的JSON结构


我们封装了统一的返回数据结构后,在接口中就可以直接使用了。如下:


import com.example.demo.model.ResponseInfo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author guozhengMu
 * @version 1.0
 * @date 2019/8/21 14:55
 * @description
 * @modify
 */
@RestController
@RequestMapping(value = "/test", method = RequestMethod.GET)
public class TestController {
    @RequestMapping("/json")
    public ResponseInfo test() {
        try {
            // 模拟异常业务代码
            int num = 1 / 0;
            return new ResponseInfo("测试数据");
        } catch (Exception e) {
            return new ResponseInfo(500, "系统异常,请联系管理员!");
        }
    }
}

如上,接口的返回数据处理便优雅了许多。针对上面接口做个测试,启动项目,通过浏览器访问:localhost:8096/test/json,得到响应结果:


{"code":500,"msg":"系统异常,请联系管理员!","data":null}

3 全局异常处理


3.1 系统定义异常处理


新建一个 ExceptionHandlerAdvice 全局异常处理类,然后加上 @RestControllerAdvice 注解即可拦截项目中抛出的异常,如下代码中包含了几个异常处理,如参数格式异常、参数缺失、系统异常等,见下例:


@RestControllerAdvice
@Slf4j
public class ExceptionHandlerAdvice {

    // 参数格式异常处理
    @ExceptionHandler({IllegalArgumentException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseInfo badRequestException(IllegalArgumentException exception) {
     log.error("参数格式不合法:" + e.getMessage());
        return new ResponseInfo(HttpStatus.BAD_REQUEST.value() + "", "参数格式不符!");
    }

 // 权限不足异常处理
    @ExceptionHandler({AccessDeniedException.class})
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ResponseInfo badRequestException(AccessDeniedException exception) {
        return new ResponseInfo(HttpStatus.FORBIDDEN.value() + "", exception.getMessage());
    }

 // 参数缺失异常处理
    @ExceptionHandler({MissingServletRequestParameterException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseInfo badRequestException(Exception exception) {
        return new ResponseInfo(HttpStatus.BAD_REQUEST.value() + "", "缺少必填参数!");
    }

    // 空指针异常
    @ExceptionHandler(NullPointerException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseInfo handleTypeMismatchException(NullPointerException ex) {
        log.error("空指针异常,{}", ex.getMessage());
        return new JsonResult("500", "空指针异常");
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonResult handleUnexpectedServer(Exception ex) {
        log.error("系统异常:", ex);
        return new JsonResult("500", "系统发生异常,请联系管理员");
    }
    
    // 系统异常处理
    @ExceptionHandler(Throwable.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseInfo exception(Throwable throwable) {
        log.error("系统异常", throwable);
        return new ResponseInfo(HttpStatus.INTERNAL_SERVER_ERROR.value() + "系统异常,请联系管理员!");
    }
}


  1. @RestControllerAdvice 注解包含了 @Component 注解,说明在 Spring Boot 启动时,也会把该类作为组件交给 Spring 来管理。

  2. @RestControllerAdvice 注解包含了 @ResponseBody 注解,为了异常处理完之后给调用方输出一个 JSON 格式的封装数据。

  3. @RestControllerAdvice 注解还有个 basePackages 属性,该属性用来拦截哪个包中的异常信息,一般我们不指定这个属性,我们拦截项目工程中的所有异常。

  4. 在方法上通过 @ExceptionHandler 注解来指定具体的异常,然后在方法中处理该异常信息,最后将结果通过统一的 JSON 结构体返回给调用者。

  5. 但在项目中,我们一般都会比较详细地去拦截一些常见异常,拦截 Exception 虽然可以一劳永逸,但是不利于我们去排查或者定位问题。实际项目中,可以把拦截 Exception 异常写在 GlobalExceptionHandler 最下面,如果都没有找到,最后再拦截一下 Exception 异常,保证输出信息友好。


下面我们通过一个接口来进行测试:


@RestController
@RequestMapping(value = "/test", method = RequestMethod.POST)
public class TestController {
    @RequestMapping("/json")
    public ResponseInfo test(@RequestParam String userName, @RequestParam String password) {
        try {
            String data = "登录用户:" + userName + ",密码:" + password;
            return new ResponseInfo("0", "操作成功!", data);
        } catch (Exception e) {
            return new ResponseInfo("500", "系统异常,请联系管理员!");
        }
    }
}

接口调用,password这项故意空缺:


MarkerHub


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

建议收藏 | SpringBoot 元数据配置原来可以这么拓展!

一、背景 最近在调试reactive-steams源码的时候看到spring-boot源码包里面的 spring-configuration-metadata.json additional-spring-configuration-metadata....
继续阅读 »

一、背景


最近在调试reactive-steams源码的时候看到spring-boot源码包里面的




  • spring-configuration-metadata.json




  • additional-spring-configuration-metadata.json





说实话主要是metadata吸引了我,因为最近在调整引擎元数据管理确实折腾了很久。


查了官方的资料发现这里也是 SpringBoot 提供的元数据配置拓展,但是这里的元数据不是只在 Spring bean 管理的元数据类似。


▐ 官方解释




访问地址:docs.spring.io/spring-boot…



简单点可以理解为这类元数据的配置时为了让我们在使用 IDEA 开发的过程中,使用application.properties或者 application.yml配置的时候更有注释说明,更方便我们开发使用。


▐ 官方案例


以我们常用的 logging 配置为例




  • 元数据配置







  • 定义配置



二、应用实例


▐ 插件工厂配置定义


配置元数据文件位于 jar 下面。META-INF/spring-configuration-metadata.json它们使用简单的 JSON 格式,其中的项目分类在“groups”或“properties”下



{
"properties": [
{
"name": "plugin-cache.basePackage",
"type": "java.lang.String",
"description": "文档扫描包路径。"
},
{
"name": "plugin-cache.title",
"type": "java.lang.String",
"description": "Plugin Cache 插件工厂"
},
{
"name": "plugin-cache.description",
"type": "java.lang.String",
"description": "插件工厂描述"
},
{
"name": "plugin-cache.version",
"type": "java.lang.String",
"defaultValue": "V1.0",
"description": "版本。"
}
]
}

大部分元数据文件是在编译时通过处理所有带注释的项目自动生成的


@ConfigurationProperties 可以查看先前的文章


@EnableConfigurationProperties 的工作原理


参考下面 properties 表格进行配置上的理解。



deprecation 每个 properties 元素的属性中包含的 JSON 对象可以包含以下属性:



▐ 插件工厂配置注入


@Data
@Component
@ConfigurationProperties(PluginCacheProperties.PREFIX)
class PluginCacheProperties {

public static final String PREFIX = "plugin-cache";

/**
* 文档扫描包路径
*/
private String basePackage = "";

/**
* Plugin Cache 插件工厂
*/
private String title = "Plugin Cache 插件工厂";

/**
* 服务文件介绍
*/
private String description = "插件缓存说明";

/**
* 版本
*/
private String version = "V1.0";

/**
* 默认编码
*/
private String charset="UTF-8";

}

▐ 配置应用




三、总结


对于元数据配置,理解起来不难!主要为了组件库为了让使用者更加优化使用提供的一套 IDEA 提示说明。借此我们在开放私有组件或者插件的时候在对于配置项可对外提供开放能力,可以根据元数据配置来完善 IDEA 提示说明。这样其他人用起来的时候能很快知道对应的参数的配置类型以及相关的配置属性说明。总结本篇文章希望对从事相关工作的同学能够有所帮助或者启发


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

KTV歌词解析, 音准评分组件

iOS
KTV歌词解析, 音准评分组件介绍支持XML歌词解析, LRC歌词解析, 解决了多行歌词进度渲染的问题, 评分根据人声实时计算评分欢迎各位大佬提交PR, 有问题提issue, 我会不定时fixGithub使用方法初始化    private...
继续阅读 »

KTV歌词解析, 音准评分组件

KTV歌词解析, 音准评分组件

介绍

支持XML歌词解析, LRC歌词解析, 解决了多行歌词进度渲染的问题, 评分根据人声实时计算评分

欢迎各位大佬提交PR, 有问题提issue, 我会不定时fix

Github

使用方法

初始化

    private lazy var lrcScoreView: AgoraLrcScoreView = {
       let lrcScoreView = AgoraLrcScoreView(delegate: self)
       lrcScoreView.config.scoreConfig.scoreViewHeight = 100
       lrcScoreView.config.scoreConfig.emitterColors = [.systemPink]
       lrcScoreView.config.lrcConfig.lrcFontSize = .systemFont(ofSize: 15)
       return lrcScoreView
   }()

配置属性

组件base配置
    /// 评分组件配置
   public var scoreConfig: AgoraScoreItemConfigModel = .init()
   /// 歌词组件配置
   public var lrcConfig: AgoraLrcConfigModel = .init()
   /// 是否隐藏评分组件
   public var isHiddenScoreView: Bool = false
   /// 背景图
   public var backgroundImageView: UIImageView?
   /// 评分组件和歌词组件之间的间距 默认: 0
   public var spacing: CGFloat = 0
歌词配置
    /// 无歌词提示文案
   public var tipsString: String = "纯音乐,无歌词"
   /// 提示文字颜色
   public var tipsColor: UIColor = .black
   /// 提示文字大小
   public var tipsFont: UIFont = .systemFont(ofSize: 17)
   /// 分割线的颜色
   public var separatorLineColor: UIColor = .lightGray
   /// 是否隐藏分割线
   public var isHiddenSeparator: Bool = false
   /// 默认歌词背景色
   public var lrcNormalColor: UIColor = .gray
   /// 高亮歌词背景色
   public var lrcHighlightColor: UIColor = .white
   /// 实时绘制的歌词颜色
   public var lrcDrawingColor: UIColor = .orange
   /// 歌词文字大小 默认: 15
   public var lrcFontSize: UIFont = .systemFont(ofSize: 15)
   /// 歌词高亮文字缩放大小 默认: 1.1
   public var lrcHighlightScaleSize: Double = 1.1
   /// 歌词左右两边间距
   public var lrcLeftAndRightMargin: CGFloat = 15
   /// 等待开始圆点背景色 默认: 灰色
   public var waitingViewBgColor: UIColor? = .gray
   /// 等待开始圆点大小 默认: 10
   public var waitingViewSize: CGFloat = 10
   /// 是否可以拖动歌词 默认: true
   public var isDrag: Bool = true
评分配置
    /// 评分视图高度 默认:100
   public var scoreViewHeight: CGFloat = 100
   /// 圆的起始位置: 默认: 100
   public var innerMargin: CGFloat = 100
   /// 线的高度 默认:10
   public var lineHeight: CGFloat = 10
   /// 线的宽度 默认: 120
   public var lineWidht: CGFloat = 120
   /// 默认线的背景色
   public var normalColor: UIColor = .gray
   /// 匹配后线的背景色
   public var highlightColor: UIColor = .orange
   /// 分割线的颜色
   public var separatorLineColor: UIColor = .systemPink
   /// 是否隐藏垂直分割线
   public var isHiddenVerticalSeparatorLine: Bool = false
   /// 是否隐藏上下分割线
   public var isHiddenSeparatorLine: Bool = false
   /// 游标背景色
   public var cursorColor: UIColor = .systemPink
   /// 游标的宽
   public var cursorWidth: CGFloat = 20
   /// 游标的高
   public var cursorHeight: CGFloat = 20
   /// 是否隐藏粒子动画效果
   public var isHiddenEmitterView: Bool = false
   /// 使用图片创建粒子动画
   public var emitterImages: [UIImage]?
   /// emitterImages为空默认使用颜色创建粒子动画
   public var emitterColors: [UIColor] = [.red]
   /// 尾部动画图片
   public var tailAnimateImage: UIImage?
   /// 尾部动画颜色
   public var tailAnimateColor: UIColor? = .yellow
   /// 评分默认分数: 50
   public var defaultScore: Double = 50

事件回调

歌词Delegate
weak var delegate: AgoraLrcViewDelegate?

protocol AgoraLrcViewDelegate {
   /// 当前播放器的时间 单位: 秒
   func getPlayerCurrentTime() -> TimeInterval
   /// 获取歌曲总时长
   func getTotalTime() -> TimeInterval

   /// 设置播放器时间
   @objc
   optional func seekToTime(time: TimeInterval)
   /// 当前正在播放的歌词和进度
   @objc
   optional func currentPlayerLrc(lrc: String, progress: CGFloat)
}
歌词下载Delegate
weak var downloadDelegate: AgoraLrcDownloadDelegate?

protocol AgoraLrcDownloadDelegate {
   /// 开始下载
   @objc
   optional func beginDownloadLrc(url: String)
   /// 下载完成
   @objc
   optional func downloadLrcFinished(url: String)
   /// 下载进度
   @objc
   optional func downloadLrcProgress(url: String, progress: Double)
   /// 下载失败
   @objc
   optional func downloadLrcError(url: String, error: Error?)
   /// 下载取消
   @objc
   optional func downloadLrcCanceld(url: String)
   /// 开始解析歌词
   @objc
   optional func beginParseLrc()
   /// 解析歌词结束
   @objc
   optional func parseLrcFinished()
}
评分Delegate
weak var scoreDelegate: AgoraKaraokeScoreDelegate?

protocol AgoraKaraokeScoreDelegate {
   /// 分数实时回调
   /// score: 每次增加的分数
   /// cumulativeScore: 累加分数
   /// totalScore: 总分
   @objc optional func agoraKaraokeScore(score: Double, cumulativeScore: Double, totalScore: Double)
}

集成方式

本地pod引入,暂时使用本地pod 待后续发布cocoapods

把 'AgoraLrcScoreView' 复制到根目录, 执行pod

pod 'AgoraLrcScore', :path => "AgoraLrcScoreView"

作者:莫烦恼
来源:https://juejin.cn/post/7054443857928257550

收起阅读 »

支付宝集五福手画福字功能(含撤销操作)用Flutter如何实现?

今早一觉醒来发现支付宝一年一度的集五福活动又开始了,其中包含了一个功能就是手写福字,还包括撤销一笔,清除重写,保存相册等,那么使用Flutter应该如何实现这些功能呢?需求包含需求的具体有:界面随着用户手指的滑动显示走过轨迹,也就是对应的笔画。点击清空按钮可以...
继续阅读 »

支付宝集五福手画福字功能(含撤销操作)用Flutter如何实现?

今早一觉醒来发现支付宝一年一度的集五福活动又开始了,其中包含了一个功能就是手写福字,还包括撤销一笔,清除重写,保存相册等,那么使用Flutter应该如何实现这些功能呢?

需求

包含需求的具体有:

  • 界面随着用户手指的滑动显示走过轨迹,也就是对应的笔画。

  • 点击清空按钮可以清除所有的笔画。

  • 点击撤销按钮可以清除上一步画过的笔画。

  • 保存所写的文字样式到相册。

实现思路

显示笔画轨迹

使用Listener组件对用户手指落下、滑动和收起的动作进行监听,在onPointerDown,onPointerMove,onPointerUp3个监听方法中返回的PointerMoveEvent对象包含了手指所在的位置坐标偏移量localPosition,用户每次滑动时都会记录下轨迹经过的坐标点,这些坐标点连接起来就是一条线。其次,再配合使用CustomPainter进行画布自绘,将所有划过的点的连接成线使用画笔绘制在界面上即可。

搜集坐标点:

Listener(
child: Container(
  alignment: Alignment.center,
  color: Colors.transparent,
  width: double.infinity,
  height: MediaQuery.of(context).size.height,
),
onPointerDown: (PointerDownEvent event) {
  setState(() {
     
  });
},
onPointerMove: (PointerMoveEvent event) {
  setState(() {
     
  });
},
onPointerUp: (PointerUpEvent event) {
  setState(() {
     
  });
},
),

绘制:

@override
void paint(Canvas canvas, Size size) {
myPaint.strokeCap = StrokeCap.round;
myPaint.strokeWidth = 15.0;
if (lines.isEmpty) {
  canvas.drawPoints(PointMode.polygon, [Offset.zero, Offset.zero], myPaint);
} else {
  for (int k = 0; k < lines.length; k++) {
    for (int i = 0; i < lines[k].length - 1; i++) {
      if (lines[k][i] != Offset.zero && lines[k][i + 1] != Offset.zero) {
        canvas.drawLine(lines[k][i], lines[k][i + 1], myPaint);
      }
    }
  }
}
}

撤销与清空

图片

看到上面的代码有的人可能会比较疑惑,绘制时为什么这么复杂,还出现了双重循环。这就和撤销功能有关了,先假设不需要撤销功能,其实我们就可以直接把所有笔画的点连接到一起进行绘制就可以了,但是一旦引入了撤销功能,就要记录每一笔笔画,福字笔画是13画,那么理论上是需要记录13个笔画的,才能保证每次撤销时都能正常退回上一次画过的笔迹,所以第一反应就是使用集合将每一次笔画记录下来。而上面也说了每一个笔画其实也是多个坐标点的集合,所以所有笔画就是一个坐标点集合的集合,即:

/// 所有笔画划线集合
List<List<Offset>> _lines = [];

另外,也不难想到,我们可以轻易通过手指按下和手指手指的方法回调来区分笔画开始和结束。在两个方法中进行笔画的add和更新。

onPointerDown: (PointerDownEvent event) {
setState(() {
  _event = event;
  _points.add(_event?.localPosition ?? Offset.zero);
  _lines.add(_points);
});
},
onPointerMove: (PointerMoveEvent event) {
setState(() {
  _event = event;
  _points.add(_event?.localPosition ?? Offset.zero);
  _lines.last = _points;
});
},
onPointerUp: (PointerUpEvent event) {
setState(() {
  _event = event;
  _points.add(Offset.zero);
  _lines.last = _points;
});
_points = [];
},

而前面说的双重遍历这时也比较好理解了:

  • 第一层循环是遍历所有的笔画,遍历次数就是福字的笔画数。

  • 第二层循环是每一个笔画包括的好多个坐标点,遍历出来使用drawLine方法绘制到界面上形成一条线。

这样在进行撤销操作时,调用list的removeLast方法移除最后一项再刷新界面就能实现退回一笔的效果了,清空就是清空笔画集合。

保存到相册

保存相册主要是引入了两个插件库:permission_handlerimage_gallery_saver,一个用来获取存储权限,一个用来保存到相册。 使用RepaintBoundary组件将画布包裹起来,并指定key,在点击保存时按顺序调用如下方法先获取截图后保存即可:

RenderRepaintBoundary boundary =
  key.currentContext!.findRenderObject() as RenderRepaintBoundary;
var image = await boundary.toImage(pixelRatio: 3.0);
ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
_postBytes = byteData?.buffer.asUint8List();
var result = await ImageGallerySaver.saveImage(_postBytes!);

完整代码与demo下载

github地址

安卓手机扫码下载

作者:单总不会亏待你
来源:https://juejin.cn/post/7054833357267402788

收起阅读 »

35岁奥地利最帅总理辞职,超360万年薪加入硅谷当「码农」

1、 除了北妈,说实话你们🍋不?有意思的是,这位世界上最年轻的政付首脑,虽曾就读于在维也纳大学的法学院,但中途就辍学了。又是一个辍学的学渣逆袭的故事,为什么外国人都喜欢辍学啊,我气啊。在过去的四年里,这位35岁的年轻人一直是奥地利政治的主人。但一桩腐...
继续阅读 »
1、


先看个图,客观的说,这人帅不帅?气质可以不?

此人就是刚在年底辞职的奥地利总理。

此人名叫Sebastian Kurz,他是世界最年轻的郭嘉总理,27岁就任外交部长,31岁当选总理。

35岁辞职总理职务,转行去硅谷科技公司做打工人!

刺激不?励志不?

现在互联网这么卷了吗?35岁奥地利总理辞职转行去硅谷当码农?我们看看咋回事。

但你不得不服,有些人一出生就站在罗马城外了。

Kurz 在2021年12月辞职总理,但不到一个月的时间,他就在美国硅谷找到了薪酬最高的经理职位。
 
据估计,Kurz的年薪将超过50万欧元(约360万元),远远高于他担任总理时31.2万欧元(约226万元)的收入。不过总理200多万年薪,也算是很高了啊。

果然,资本主义太邪恶了,连人民公仆都给这么高收入,腐蚀人啊。

在政府工作了十年的Kurz,依然是世界上最年轻的外交部长和最年轻的总理保持者。

除了北妈,说实话你们🍋不?



有意思的是,这位世界上最年轻的政付首脑,虽曾就读于在维也纳大学的法学院,但中途就辍学了。

又是一个辍学的学渣逆袭的故事,为什么外国人都喜欢辍学啊,我气啊。

在过去的四年里,这位35岁的年轻人一直是奥地利政治的主人。但一桩腐败丑闻,让他的第二个总理职位戛然而止。
 
警方突袭了维也纳的各个部门,揭开了奥地利国家检察官对政府核心部门腐败的调查。
 
虽然没有提出正式指控,但调查的一部分正是关于Kurz在2013年至2017年担任外交部长期间是否使用国家资金并开具假发票以购买有利的媒体报道。
 
12月2日,星期四,Sebastian Kurz 在新闻发布会上,他宣布自己将辞去所有职务。
 
这个决定对他来说很不容易,但尽管如此,他说「我并不感到任何忧郁。因为我非常感谢在过去十年中我所经历的一切」。

瞧瞧,政客真是说话的艺术家。

2、
随后,在不到一个月的时间里,他转头去找了老朋友,一位在美国硅谷的大资本家 Thiel资本的创始人Peter Thiel。

就是那个和如今的世界首富 马斯克年轻时候一起创造 第一个移动支付paypal的家伙。他们俩也是靠paypal捞到了第一桶金。

如今 Thiel 身家91.3亿美元,排名世界富豪279名。

对于Kurz来说,算是老熟人了,他在当总理期间就市场去硅谷访问考察,对科技有浓烈兴趣。 然后结交了各种硅谷CEO和各种人脉,这是要提前布局啊这是。


看来这帅哥一直都有个互联网梦,也难怪从政坛辞职,立马加入硅谷公司。

目前前总理 Kurz 加入 Thiel的资本公司,这是一家风险对冲基金资本公司,严格意义来说算是一家科技金融公司。

用技术来指导商业投资理财和商业战略等业务。

而Kurz的职位严格意义来说 是战略执行官,负责欧洲奥地利地缘政治和欧洲战略指导判断,并不是具体写业务的码农。

看来这位最年轻的总理帅哥,还可能干老本行。而且收入更高,身份更自由,完美实现了,破圈副业,妥妥的人生赢家。

说实话,我已经吃了一筐柠檬了。酸🍋🍋

3、
那么给我们的启示是什么呢?我认为有三点。

一是无论身份职位差异多么大,未来都是不可确定的。你贵为总理也要未雨绸缪,说下台就下台了,更别说我们普通小人物。

二是人脉和保持好奇心的重要性,我们要时常维护职场关系,甚至利用职场和工作便利 要懂得权衡利弊为自己拓展相关人脉,日后你绝对用得上。

刘邦之所以草根得天下,我觉得他比那朱元璋还难,他利用的很重要的一点是什么?很大程度就是人脉的力量。

而如果日后,个人创业,很大程度也是整合资源,利用人脉。

因为web2.0时代,技术已经完全不是壁垒了,资源和生产资料才是。而web3.0又要来了呢。

而人脉资源属于生产资料的一种重要资料。

35岁其实大家不用害怕,每年互联网圈都会裁人,而在5年前,都在传说30岁码农就找不到工作了,如今变成了35岁危机。

我想过不了几年,40岁的码农还依然是很多的。因为从业者其实是在减少的,总人口趋势也不容乐观。

每年都在谈 中年危机,但真正的危机并不是大环境带给你的危机, 而是你自己在年轻时候意识不到你也会老。

你没有人无远虑必有近忧的觉悟。

大家主张躺平,而你真的信了!躺不了几年,你就真的危机了。

三是,成年人追求利益和money没有错。错的是你怎么追求,君子爱财取之有道,才是正常的社会想象。

今天北妈又在贩卖焦虑?但焦虑不是北妈写出来你才焦虑,而是它一直在你内心,而你不愿意面对它罢了,一起成长吧。

作者:北妈
来源:https://mp.weixin.qq.com/s/kHyPm1ww8msBGn-TMtn58w
收起阅读 »

字节面:什么是伪共享?

CPU 如何读写数据的?先来认识 CPU 的架构,只有理解了 CPU 的 架构,才能更好地理解 CPU 是如何读写数据的,对于现代 CPU 的架构图如下: 可以看到,一个 CPU 里通常会有多个 CPU 核心,比如上图中的 1 号和 2 号 CPU 核心,并且...
继续阅读 »

CPU 如何读写数据的?

先来认识 CPU 的架构,只有理解了 CPU 的 架构,才能更好地理解 CPU 是如何读写数据的,对于现代 CPU 的架构图如下:


pic_dab4651e.png

可以看到,一个 CPU 里通常会有多个 CPU 核心,比如上图中的 1 号和 2 号 CPU 核心,并且每个 CPU 核心都有自己的 L1 Cache 和 L2 Cache,而 L1 Cache 通常分为 dCache(数据缓存) 和 iCache(指令缓存),L3 Cache 则是多个核心共享的,这就是 CPU 典型的缓存层次。


上面提到的都是 CPU 内部的 Cache,放眼外部的话,还会有内存和硬盘,这些存储设备共同构成了金字塔存储层次。如下图所示:


pic_f0854715.png

从上图也可以看到,从上往下,存储设备的容量会越大,而访问速度会越慢。至于每个存储设备的访问延时,你可以看下图的表格:


pic_6b362bd7.png

你可以看到, CPU 访问 L1 Cache 速度比访问内存快 100 倍,这就是为什么 CPU 里会有 L1~L3 Cache 的原因,目的就是把 Cache 作为 CPU 与内存之间的缓存层,以减少对内存的访问频率。


CPU 从内存中读取数据到 Cache 的时候,并不是一个字节一个字节读取,而是一块一块的方式来读取数据的,这一块一块的数据被称为 CPU Line(缓存行),所以 CPU Line 是 CPU 从内存读取数据到 Cache 的单位。


至于 CPU Line 大小,在 Linux 系统可以用下面的方式查看到,你可以看我服务器的 L1 Cache Line 大小是 64 字节,也就意味着 L1 Cache 一次载入数据的大小是 64 字节。


pic_4c8081a7.png

那么对数组的加载, CPU 就会加载数组里面连续的多个数据到 Cache 里,因此我们应该按照物理内存地址分布的顺序去访问元素,这样访问数组元素的时候,Cache 命中率就会很高,于是就能减少从内存读取数据的频率, 从而可提高程序的性能。


但是,在我们不使用数组,而是使用单独的变量的时候,则会有 Cache 伪共享的问题,Cache 伪共享问题上是一个性能杀手,我们应该要规避它。


接下来,就来看看 Cache 伪共享是什么?又如何避免这个问题?


现在假设有一个双核心的 CPU,这两个 CPU 核心并行运行着两个不同的线程,它们同时从内存中读取两个不同的数据,分别是类型为 long 的变量 A 和 B,这个两个数据的地址在物理内存上是连续的,如果 Cahce Line 的大小是 64 字节,并且变量 A 在 Cahce Line 的开头位置,那么这两个数据是位于同一个 Cache Line 中,又因为 CPU Line 是 CPU 从内存读取数据到 Cache 的单位,所以这两个数据会被同时读入到了两个 CPU 核心中各自 Cache 中。


pic_54538b4a.png

我们来思考一个问题,如果这两个不同核心的线程分别修改不同的数据,比如 1 号 CPU 核心的线程只修改了 变量 A,或 2 号 CPU 核心的线程的线程只修改了变量 B,会发生什么呢?


分析伪共享的问题

现在我们结合保证多核缓存一致的 MESI 协议,来说明这一整个的过程,如果你还不知道 MESI 协议,你可以看我这篇文章「[10 张图打开 CPU 缓存一致性的大门][10 _ CPU]」。


①. 最开始变量 A 和 B 都还不在 Cache 里面,假设 1 号核心绑定了线程 A,2 号核心绑定了线程 B,线程 A 只会读写变量 A,线程 B 只会读写变量 B。


pic_20cb02eb.png

②. 1 号核心读取变量 A,由于 CPU 从内存读取数据到 Cache 的单位是 Cache Line,也正好变量 A 和 变量 B 的数据归属于同一个 Cache Line,所以 A 和 B 的数据都会被加载到 Cache,并将此 Cache Line 标记为「独占」状态。


pic_31e05dfd.png

③. 接着,2 号核心开始从内存里读取变量 B,同样的也是读取 Cache Line 大小的数据到 Cache 中,此 Cache Line 中的数据也包含了变量 A 和 变量 B,此时 1 号和 2 号核心的 Cache Line 状态变为「共享」状态。


pic_611082d7.png

④. 1 号核心需要修改变量 A,发现此 Cache Line 的状态是「共享」状态,所以先需要通过总线发送消息给 2 号核心,通知 2 号核心把 Cache 中对应的 Cache Line 标记为「已失效」状态,然后 1 号核心对应的 Cache Line 状态变成「已修改」状态,并且修改变量 A。


pic_c6c22e2c.png

⑤. 之后,2 号核心需要修改变量 B,此时 2 号核心的 Cache 中对应的 Cache Line 是已失效状态,另外由于 1 号核心的 Cache 也有此相同的数据,且状态为「已修改」状态,所以要先把 1 号核心的 Cache 对应的 Cache Line 写回到内存,然后 2 号核心再从内存读取 Cache Line 大小的数据到 Cache 中,最后把变量 B 修改到 2 号核心的 Cache 中,并将状态标记为「已修改」状态。


pic_33e76a58.png

所以,可以发现如果 1 号和 2 号 CPU 核心这样持续交替的分别修改变量 A 和 B,就会重复 ④ 和 ⑤ 这两个步骤,Cache 并没有起到缓存的效果,虽然变量 A 和 B 之间其实并没有任何的关系,但是因为同时归属于一个 Cache Line ,这个 Cache Line 中的任意数据被修改后,都会相互影响,从而出现 ④ 和 ⑤ 这两个步骤。


因此,这种因为多个线程同时读写同一个 Cache Line 的不同变量时,而导致 CPU Cache 失效的现象称为伪共享(False Sharing)。


避免伪共享的方法

因此,对于多个线程共享的热点数据,即经常会修改的数据,应该避免这些数据刚好在同一个 Cache Line 中,否则就会出现为伪共享的问题。


接下来,看看在实际项目中是用什么方式来避免伪共享的问题的。


在 Linux 内核中存在 cacheline_aligned_in_smp 宏定义,是用于解决伪共享的问题。


pic_2ad0ad52.png

从上面的宏定义,我们可以看到:



  • 如果在多核(MP)系统里,该宏定义是 cacheline_aligned,也就是 Cache Line 的大小;
  • 而如果在单核系统里,该宏定义是空的;

因此,针对在同一个 Cache Line 中的共享的数据,如果在多核之间竞争比较严重,为了防止伪共享现象的发生,可以采用上面的宏定义使得变量在 Cache Line 里是对齐的。


举个例子,有下面这个结构体:


pic_46aebc19.png

结构体里的两个成员变量 a 和 b 在物理内存地址上是连续的,于是它们可能会位于同一个 Cache Line 中,如下图:


pic_56ff524f.png

所以,为了防止前面提到的 Cache 伪共享问题,我们可以使用上面介绍的宏定义,将 b 的地址设置为 Cache Line 对齐地址,如下:


pic_1eeffcc3.png

这样 a 和 b 变量就不会在同一个 Cache Line 中了,如下图:


pic_e35118bc.png

所以,避免 Cache 伪共享实际上是用空间换时间的思想,浪费一部分 Cache 空间,从而换来性能的提升。


我们再来看一个应用层面的规避方案,有一个 Java 并发框架 Disruptor 使用「字节填充 + 继承」的方式,来避免伪共享的问题。


Disruptor 中有一个 RingBuffer 类会经常被多个线程使用,代码如下:


pic_ce4a0542.png

你可能会觉得 RingBufferPad 类里 7 个 long 类型的名字很奇怪,但事实上,它们虽然看起来毫无作用,但却对性能的提升起到了至关重要的作用。


我们都知道,CPU Cache 从内存读取数据的单位是 CPU Line,一般 64 位 CPU 的 CPU Line 的大小是 64 个字节,一个 long 类型的数据是 8 个字节,所以 CPU 一下会加载 8 个 long 类型的数据。


根据 JVM 对象继承关系中父类成员和子类成员,内存地址是连续排列布局的,因此 RingBufferPad 中的 7 个 long 类型数据作为 Cache Line 前置填充,而 RingBuffer 中的 7 个 long 类型数据则作为 Cache Line 后置填充,这 14 个 long 变量没有任何实际用途,更不会对它们进行读写操作。


pic_4fc4c5eb.png

另外,RingBufferFelds 里面定义的这些变量都是 final 修饰的,意味着第一次加载之后不会再修改, 又由于「前后」各填充了 7 个不会被读写的 long 类型变量,所以无论怎么加载 Cache Line,这整个 Cache Line 里都没有会发生更新操作的数据,于是只要数据被频繁地读取访问,就自然没有数据被换出 Cache 的可能,也因此不会产生伪共享的问题。


作者:小林
来源:https://mp.weixin.qq.com/s/zeGxBx77TFGtVeMRBVR-Lg

收起阅读 »

元宇宙的“42条共识”

罗马不是一天建成的,元宇宙也一样。元宇宙融合了信息技术(5G/6G)、互联网时代(web3.0)、人工智能、云算力、大数据、区块链以及VR、AR、MR,游戏引擎在内的虚拟现实技术的成果。 它将引发基础数学(算法)、信息学(编程、信息熵)、生命科学(脑机接入)、...
继续阅读 »

01元宇宙不是一天建成的

罗马不是一天建成的,元宇宙也一样。

人类从未像今天这样,可以自己成为“创世主”

元宇宙融合了信息技术(5G/6G)、互联网时代(web3.0)、人工智能云算力大数据区块链以及VR、AR、MR,游戏引擎在内的虚拟现实技术的成果。



pic_85b46dac.png

它将引发基础数学(算法)、信息学(编程、信息熵)、生命科学(脑机接入)、区块链(加密金融)、量子计算(算力)等学科的深入研究和交叉互动。

还会推动未来学、哲学、逻辑学、伦理学、科幻等人文科学体系的全新突破。

但这一切需要时间,不能急功近利的思考去变现。

02元宇宙不是“虚拟世界”

《头号玩家》不是元宇宙,《黑客帝国》也不是元宇宙。

元宇宙不是简单的虚拟世界,它与平行世界也不是相互割裂,而是交汇融合

线上+线下是元宇宙未来的存在模式

线下的场景会成为元宇宙的一个重要组成部分,元宇宙也会为线下的沉浸式娱乐带来更多可能。



pic_eda6ce07.png

元宇宙与现实世界一开始存在边界,但两者的边界会变得越来越模糊,最终变成一个硬币的两面,相互依存。

元宇宙是虚实共生,不是镜像孪生。

03算力即权力

在元宇宙世界,算力是同水、电、油、气一样的基础设施。

没有算力,元宇宙将停止运转

谁拥有了算力,谁就拥有了财富

谁拥有了控制算力的权限,谁就拥有控制世界的权力。



pic_6b068420.png

未来世界的算力分为:

中心化算力去中心化算力

如果中心化算力占据上峰,那这个世界将更加不平等。

如果去中心化算力占据上身,那这个世界相对来说更加公平。

人类未来最大的矛盾,是日益增长的数据处理有限算力之间的矛盾!

04元宇宙不是游戏

游戏是元宇宙入口之一,但元宇宙不是游戏。

将元宇宙视为一个超级大型3D虚拟游戏是片面浅薄的。

元宇宙是整合多种新技术而产生的新型虚实相融的数字文明。



pic_52445957.png

涉及主权财富、生态建设、经济体系、价值设定等多重要素,是自然人的高维度拓展。

05元宇宙不属于

任何一家科技巨头

元宇宙的核心是“去中心化”,不会被某一家科技巨头公司控制。

被科技巨头的控制的元宇宙,不是元宇宙。

任何一家科技巨头,也无法真正建成完整的元宇宙。


pic_8baff518.png

真正的元宇宙最终需要实现跨链互通、身份互认、价值共享,它不属于任何一家科技巨头,而是属于每一个人。

06元宇宙与星辰大海不是对立关系

太空歌剧和赛博朋克相互融合,不是非此即彼。

元宇宙与星辰大海不是竞争对立关系,前者向内拓展,后者向外延伸,最终殊途同归,共同发展。

pic_f8f972c9.png

更好的数字虚拟技术和更发达的太空技术,其实是相辅相成的。

07概念股不是“元宇宙”

股市中的“元宇宙”,都是打着概念炒作的韭菜盘。

元宇宙不是单纯的游戏,现阶段核心是个人数字身份和个人主权财富

pic_abda03ed.png

概念股大部分是“伪元宇宙”,在意的是短期的套利炒作。

所以,了解元宇宙的深层逻辑,认识什么好的项目,别成为韭菜。

08Web3.0是元宇宙

不可或缺的一部分

没有CryptoWeb3.0不是Web3.0,没有Web3.0的元宇宙不是元宇宙。

pic_03810ba6.png

实现Web3.0所需的技术路径中,跨链、分布式存储隐私计算是Web3.0生态目前发展阶段的核心技术栈,这些,与元宇宙中的去中心化身份、信用体系建设都高度吻合。

09元宇宙是“元叙事”,是人类大目标

元宇宙是跟“全球化”一样的元叙事,它是关于“永恒真理”“人类解救”的故事,是一种对未来进程有始有终的构想形式,具有五大特征

预测未来世界、追寻终极目标、产业利益趋同、统一前进方向、构建多重世界。pic_a8d7095a.png

10元宇宙是一个熵+系统

薛定谔在《生命是什么》中提到“生命是负熵”这一概念。

我们所处的现实世界是一个不断熵增的封闭系统,按照热力学第二定律,现实世界一定会有热寂的一天。pic_a109e725.png

元宇宙通过各类技术集成,可以最大程度减少元宇宙系统的熵增无序

在某一个时刻,元宇宙是一个熵-系统

但它没有办法摆脱母世界的影响,最终仍然是一个熵+系统

11元宇宙:数学>人性

元宇宙的底层逻辑,遵循“数学契约论”

在现实世界中,社会契约论主宰着现代文明

在元宇宙中,数学契约代替社会契约,用数学约束人性,用合约前置来消除人类基因中的自私自利

pic_60c26f31.png

图片数学契约自动检测订约人在订约后的行为,在元宇宙的任何行为都会被数个数学契约智能监督。

12元宇宙不是“文明内卷”

很多人用刘慈欣在2018年的演讲反驳“元宇宙”,称元宇宙是“文明内卷”

但刘慈欣讲的这段演讲时目标对象是“虚拟世界”,当时没有元宇宙的概念。

元宇宙是虚拟世界现实世界的融合,也是多种技术发展到一定阶段后的融合结晶pic_3be0e5a1.png

元宇宙不会带来文明内卷,最终实现文明的跃迁

13区块链是元宇宙的补天石

区块链让元宇宙有了现实驱动,而不再只是精神上的虚无飘缈。

区块链带来了数字ID所承载独一无二的身份,也带来了经济体系的安全稳定运行。pic_3a7bed60.png

甚至还包括私钥即一切,最终带来上层建筑的变化:

公正与自由、去中心化、数据私有化、反垄断……

物理硬件只是外在的肉身,区块链才是鲜活的灵魂。

14元宇宙“大爆炸奇点”不可知

就像宇宙诞生源于一个奇点,元宇宙的最终爆发也将由一个“奇点”点燃。

但目前这一“奇点”是未知的。大多数人只盯着已经成形的项目或者大公司,但风起于青萍之末,元宇宙带来的是一个全新时代,只怕没有那么容易猜到结局。pic_44e3ab15.png

一个伟大的元宇宙的形成,也许一个故事,一个钱包,一个插件,一套NFT图像,一个合约都可能会成为它的爆炸奇点。

15数字身份是元宇宙的最终入口

在现实世界中,我们拥有自己的身份证,其承载我们在现实世界价值

在元宇宙中,数字身份会是最终入口,其背后负载的是数字世界的社交关系资产pic_d1c7dd30.png

这个数字身份根据自己的价值观、元宇宙观、个体定位等因素,通过你在元宇宙中的种种行为选择进行确认赋权。它不仅仅是一个头像,而是一个真实存在且影响未来的数字ID

16元宇宙是新思想的“源泉”

可以明确的是,元宇宙将是新思想的源泉。

技术新思想:

5G+AI+XR云计算,区块链,高度沉浸社交,引擎技术、脑机接口,数字人,边缘计算,数学算法,3D操作系统等都会出现新技术。

金融新思想:

Web3.0、区块链、DAO、DeFi、GameFi、NFT、DEX、AMM、以太坊、USDC。

同时也是人类反思自我的核心场景,通过自我创世从而思考“创世主本意”pic_041c483f.png

从这个层次上来讲,元宇宙将产生新哲学。

17元宇宙是非线性时空

现实世界,我们遵循时间走向,以单箭头形式前行。

pic_d7a179cf.png

而元宇宙却可以是时间尺度上完全不同的平行和非线性。利用的数字身份多重人格,可以进行类**平行宇宙式的体验,在同一时间尺度上,可以完成不同的事情,实现小说中才可能出现的“非线性叙事”。

18DAO是元宇宙

核心治理模式

DAO就是未来。

DAO保障了规则制定权在社区手中,形成元宇宙的治理基石。

在DAO的治理模式下,元宇宙完全是去中心化的,其特征是开源、资产自由流动、人员自由贡献、社区投票表决、治理结果执行不受干扰。pic_182881c4.png

没有人可以发号施令,管控是分散存在而不是按等级划分。它允许每个人参与讨论,也鼓励团队合作

基于DAO治理,元宇宙将治理权交给所有参与者,建立可信规则的社区自治。

19元宇宙:互联网与

区块链的结合

元宇宙主要分为两派:互联网派和区块链派。

两派之争,分化出完全不同的元宇宙进化路线

无论是互联网派,还是区块链派,在实现元宇宙的过程中都有自己的烦恼和担忧

互联网最大的问题在于“安全性”,区块链最大的问题在于“效率性”,但同时两派又都有自己的优势pic_b84d136f.png

通过将互联网技术区块链原理结合起来,是实现元宇宙的最好路径。

20元宇宙的“大一统理论”

自然世界的科学家一直在寻找“大一统理论”

依照量子理论的观点:

物质是一份份的信息。

最前沿的弦理论更认为宇宙就是一个大提琴家奏出的乐章

pic_b6092c7f.png

自然世界的本质难道是信息比特

元宇宙的大一统理论是什么?

我们可以看得非常清楚:

元宇宙的源头是原子世界

原子世界是创世主(第一推动力),他们构建的01就是“大一统理论”

21元宇宙经济学:人即货币

元宇宙下的“元经济学”遵循“人即货币”理念。

每个人都有自己映射的Token,这些Token可以量化自己的价值。pic_706bda4d.png

在“人即货币”理念下,人从出生起就是天生的点对点的信任机器,人本身成了衡量一切的价值标的。

“人即货币”有三大定律

1、每个人都有发行货币的自由;

2、个人价值=个人币值;

3、人币同在。

“人即货币”是元宇宙共识时代的高版本,通过自律来换取更大的自由和信用,让自发行的货币更有价值。

22元宇宙是技术的“结晶点”

元宇宙为什么受大企业青睐,因为它是技术“结晶点”。

谁能成为这个“结晶点”,谁就可能成为未来最强大的企业。

20世纪初的汽车,20世纪末的互联网

21世纪初的移动互联网(手机),未来的元宇宙(技术载体未定)。 pic_cf4af863.png

它不是某一项技术,而是一系列“连点成线”技术的集合。 其中包括芯片技术、通信技术、区块链技术、交互技术、虚拟引擎技术、AI技术、网络及软硬件编程等各种数字技术之大成。

23元宇宙是一个开源世界

在元宇宙中,除了个体掌控的私有数据外,所有一切都是开源的。

代码是开源的,你可以随意查看那些开源代码;

技术是开源的,所有元宇宙的技术底层逻辑你都可以学习;

公共数据是开源的,所有人皆可查看和使用,以此规避中心化平台的垄断;

内容是开源的,只要你有创意,你就能创造元宇宙的内容;

智能合约是开源的,你可以查看所有在链上的智能合约内容;

……

pic_f7b0283b.png

24“主权财富”是最大公约数

在元宇宙这个话题上,大家短期内是没办法达成共识的,每个人只看到自己所认可的元宇宙。pic_86807320.png

但获得自由、可自我分配的“主权财富”是所有人的共识……

25NFT是元宇宙中的“生命体”

NFT是数字世界中一种不能被复制、更换、切分的,用于检验特定数字资产真实性或权利的唯一数据表示。pic_3cecd2aa.png

在元宇宙中生成的原生NFT,是元宇宙独一无二的“生命体”。它不是一种简单的数字模拟信号,最终都可以形成一个自己的、超出任何产品的世界。

26元宇宙原创主权:内容至上

元宇宙是一个包容万象的新世界,由所有参与者共同创建。

未来的元宇宙,它的内容全部来源于参与者。相比与传统互联网,在元宇宙中,内容的重要性要远远大于平台的重要性。pic_b2796bb7.png

依托开源的方式,让所有人都能够参与到内容创造中,享受共同创建元宇宙的乐趣。谁拥有了创意,谁拥有了优质内容,谁自己就能在元宇宙中建立新平台。

27元宇宙:游戏即劳动

元宇宙不是游戏世界,但游戏却是元宇宙的一大组成部分。

游戏是元宇宙的最佳突破口,完整的元宇宙需要具有博弈策略的游戏来完成行为创造。pic_7c4b4d00.png

在元宇宙中,游戏即生活游戏即劳动,它将物理世界的劳动和虚拟世界的游戏光滑连接,将游戏和劳动结合起来。

28元宇宙不能让你逃避现实

元宇宙不是一个让你逃避世界的新去处。

pic_a044c385.png

堕落者进入元宇宙只会更加堕落,而那些优秀的人则会创造更多价值财富

29元宇宙是全生态进化

元宇宙是一个非常宏大复杂的结构,这样一个系统不是像某个游戏那样可以一起打包升级,整个系统的变化和升级是非常复杂的,它又同属于一个宇宙,所以它的升级和改造是全生态的进化

pic_33f038e8.png

这个全生态的进化一共可分为七层

自然层、物理层、交互层、数据层、协议层、合约层、应用层。

一个不断生长壮大的元宇宙,它的系统架构最终会向生命体学习,它的进化会向自然进化学习。

30超现实治理

元宇宙是超现实世界,以下几点都是超现实治理要素。

去中心化治理:

去中心化的社会组织里,管控分散存在而不是按等级划分

代码即法律:

由代码构成的智能代码合约形成了“自规则”“法律前置”降低了法律执行成本,有《少数派报告》的味道。

共算主义:

每个人都有获得算力的权利,每个人也有贡献算力的义务

数据私有:

用户持有私钥掌控个人数据,用户拥有完全自主管理个人数据的权力。

分布式金融:

金融体系以分布式为主,任何第三方不能逆转任何一笔交易。

pic_78e41459.png

31没有共识,就没有数字土地

没有形成共识的元宇宙大地,所谓数字土地就是泡沫。

宇宙最初三秒钟是在高温和碰撞中创造“物体规律”,当宇宙世界的规律形成后,才开始慢慢形成星球星系

pic_4521e2fc.png

现在元宇宙还没有达到星球自成、达成共识、进行数字土地财富分配的阶段。
数字土地,只是一场泡沫。
可以作为数字实验,切莫高价入局

32比特世界的“互操作性”

张飞杀岳飞,杀得满天飞。这样的事情在原子世界不可能发生。

是梅西厉害还是马拉多纳厉害?跨时空也很难比较。
但在元宇宙的“比特世界”,底层数字协议一旦形成共识,“复仇者联盟”分分钟钟就可以组队打BOSS。

pic_3f035c72.png
这就是元宇宙的“互操作性”,数字底层协议保证了算法形成的比特数字可跨宇宙穿越。你穿着“2140元宇宙”的数字盔甲,在Axie Infinity城堡卖掉盖亚蓝戒,换得了Decentraland上的一块火星大陆……
所有在元宇宙世界伟大的数字IP,都可以正面交锋。

33数字人进化:AI数字人将成为大多数

数字人将由以下几种人构成:

虚拟的假人:

产生了虚拟数据身份

意识上传的真人:

以人为模板,以人的意识为主体,他们是从真实世界移居到元宇宙的移民,不是原住民。

AI数字人:

一开始就是程序设计而成,没有真实的碳基或硅基身体,不受到任何束缚,原则上可以在元宇宙中遨游,它们是元宇宙的原住民。这些人将成为大多数。

pic_9c9a5fcb.png

AI数字人摆脱了肉身束缚,只受到数学规律限制,而数学的世界更抽象,这意味着AI数字人的能力将以数学为边界,而不以物理为边界,它们的未来将发展到什么程度?暂时无法评估,但一旦形成进化态,那将完全颠覆元宇宙

34乌合之众的极乐狂欢:一九定律

技术潮流不可阻挡,人类娱乐化亦不可阻挡。
元宇宙会整合现有世界各项技术,让生产力得到迅速提升。
与此同时,刺激丰富,体验极致,在元宇宙中能保持清醒的人会越来越少;元宇宙沉浸程度比现实社会游戏更深,时间一久,偏理性的人和偏感性的人的分布,就会从二八定律变成一九定律

pic_e1cf3762.png

元宇宙的技术将实现感官享乐主义,各种小宇宙就是寺庙和教堂,乌合之众是它的狂热粉丝和朝拜者。

那么,元宇宙的牧师和神会是谁?

35加密朋克2.0的“暗宇宙”

顶尖程序员、自由主义者、无政府主义者、科技金融玩家等元宇宙高级玩家,他们发现元宇宙被乌合之众占领,无法实现当初建设元宇宙的梦想——一个人人自由、共享开放的“美丽新世界”

pic_5a7c8969.png

这些人在元宇宙的隐秘节点,通过多层链接,建立暗宇宙,只有少数人能够通过自己去发现、审查、注册进入暗宇宙。

这些少数派成了暗宇宙背后的操控人,类似于现在的tor世界(暗网)。
基于元宇宙的“去中心化”的特质,“暗宇宙”会比现在“暗网”影响力要大。

36程序员的“个人屠龙”时代

一开始,大公司垄断元宇宙会被唾弃。
但少数人最终会成为主宰,程序员权力更大。

顶尖玩家操控世界,因为非线性时空以及“一九定律”关系,聚集的粉丝信徒可在数小时内到达数十万,基于个人信仰的组织会越变得越强,而基于血脉、原始宗教、疆域的组织会被淡化。

pic_1e762f25.png

一个基于去中心化理念形成的元宇宙,最终会形成中心化的个人帝国
这是元宇宙最难破解的“二元悖论”之一。

传统宗教必须浴火重生,否则新的科哲宗教将诞生。
屠龙者,最终成为巨龙。

37元宇宙是一台可自主学习的计算机

原子宇宙,从某种程度上来看,它的矩阵架构是通过自主学习演化而来。
它貌似宏观的算法精准,又是从最微不足道的布朗运动开始。

pic_b649959d.png

就算是伟大的相对论,在138亿年前的操作影响与138亿年后并不一样。热力学第二定律未知,这意味着“物理学”是图灵完备的。

现在元宇宙最有影响力的以太坊协议,也是一个图灵完备协议,以它为基石的元宇宙更有可能形成一台可自主学习的计算机。

38高维宇宙:虚实二相性

现实世界,微粒的波粒二象性很难理解。

德布罗意提出任何实物粒子都具有波粒二象性,波动与微粒之间的关系:E=hν,p=h/λ。

pic_090e097c.png

但只有从更高维的角度去解释:

两者是统一的。

元宇宙也是一样,如果完全进入到虚实共生的元宇宙世界,当最初创造元宇宙的原子人彻底消失后,拥有实体和虚拟身份的数字人同样也存在同样的疑问:为什么存在虚实二相性?

39意识形态之争,比特与原子的对抗

选择“比特(BIT)”,还是选择“原子(ATOM)”,可能是未来元宇宙世界最大的意识形态争论。

基于这一矛盾,元宇宙可能会分化出两种不同形态人种

选择现实世界原子人、选择数字世界比特人。

这两个人种沦为两个势力,最终各自为战。

pic_3b66c9e0.png

有人会拒绝元宇宙,坚守在现实世界;

有人会维护元宇宙,不允许其他异类存在。

比特与原子的对抗,未来元宇宙的矛盾冲突焦点

所以,选择比特,还是选择原子

40人格分裂症的春天到了

数字身份不遵循牛顿力学,而遵循MWI(多重世界)理论。
人可以选择不同身份体验人生,开启“多重人格社交”,在宇宙1中当一个植物学家,在宇宙2中做王国的领袖,元宇宙3中变成一只小动物……

pic_b6d0c286.png

多重身份连接到同一经济系统中,身份越多,利益越大。这是一个“人格分裂者”的乐园。
越是人格分裂的人,越是在元宇宙如鱼得水

41无法预知元宇宙尽头

目前的元宇宙,依旧在起步阶段,我们暂时无法预知元宇宙的未来发展。

微观世界,海森堡提出了不确定性原理**ΔxΔp≥h/4π**。

元宇宙世界,刚刚出生就饱受争议,未来之路同样艰险。

就像在全球化被提出时,没有人能预想到它的发展竟会如此曲折

元宇宙像一个新生儿,我们不能确定,它最终会成长至什么模样。

pic_b6084c3e.png

但唯一能够确定的是,我们每一个人都有机会去影响元宇宙尽头的结局。

因为在元宇宙尽头下,每个人都是创世的一份子

42元宇宙最大的共识,是没有共识

最大共识是没有共识,又一个二元悖论

前面提到的所有共识,是对元宇宙的短暂的认知和总结。

元宇宙三大定律:

非定域实在性,多世界诠释,虚实二象相(互补原理)。

都充满着矛盾辩证

元宇宙处在起步阶段,基础设施、底层逻辑、价值观等都还没有完全建立。

今天的共识,可能在明天就被推翻。

元宇宙不断变化的,又是去中心化的,它的共识会一直迭代

元宇宙的第42条共识,正是没有共识

作者:罗金海
来源:https://mp.weixin.qq.com/s/6ovJ8KcjXFYHFCxMV8CUbQ


收起阅读 »

API安全接口安全设计

如何保证外网开放接口的安全性。使用加签名方式,防止数据篡改信息加密与密钥管理搭建OAuth2.0认证授权使用令牌方式搭建网关实现黑名单和白名单一、令牌方式搭建搭建API开放平台方案设计:1.第三方机构申请一个appId,通过appId去获取accessToke...
继续阅读 »

如何保证外网开放接口的安全性。

  • 使用加签名方式,防止数据篡改

  • 信息加密与密钥管理

  • 搭建OAuth2.0认证授权

  • 使用令牌方式

  • 搭建网关实现黑名单和白名单

一、令牌方式搭建搭建API开放平台


方案设计:

1.第三方机构申请一个appId,通过appId去获取accessToken,每次请求获取accessToken都要把老的accessToken删掉

2.第三方机构请求数据需要加上accessToken参数,每次业务处理中心执行业务前,先去dba持久层查看accessToken是否存在(可以把accessToken放到redis中,这样有个过期时间的效果),存在就说明这个机构是合法,无需要登录就可以请求业务数据。不存在说明这个机构是非法的,不返回业务数据。

3.好处:无状态设计,每次请求保证都是在我们持久层保存的机构的请求,如果有人盗用我们accessToken,可以重新申请一个新的taken.

二、基于OAuth2.0协议方式

原理

第三方授权,原理和1的令牌方式一样

1.假设我是服务提供者A,我有开发接口,外部机构B请求A的接口必须申请自己的appid(B机构id)

2.当B要调用A接口查某个用户信息的时候,需要对应用户授权,告诉A,我愿同意把我的信息告诉B,A生产一个授权token给B。

3.B使用token获取某个用户的信息。

联合微信登录总体处理流程

  1. 用户同意授权,获取code

  2. 通过code换取网页授权access_token

  3. 通过access_token获取用户openId

  4. 通过openId获取用户信息

三、信息加密与密钥管理

  • 单向散列加密

  • 对称加密

  • 非对称加密

  • 安全密钥管理

1.单向散列加密

散列是信息的提炼,通常其长度要比信息小得多,且为一个固定长度。加密性强的散列一定是不可逆的,这就意味着通过散列结果,无法推出任何部分的原始信息。任何输入信息的变化,哪怕仅一位,都将导致散列结果的明显变化,这称之为雪崩效应。

散列还应该是防冲突的,即找不出具有相同散列结果的两条信息。具有这些特性的散列结果就可以用于验证信息是否被修改。

单向散列函数一般用于产生消息摘要,密钥加密等,常见的有:

  • MD5(Message Digest Algorithm 5):是RSA数据安全公司开发的一种单向散列算法,非可逆,相同的明文产生相同的密文。

  • SHA(Secure Hash Algorithm):可以对任意长度的数据运算生成一个160位的数值;

SHA-1与MD5的比较

因为二者均由MD4导出,SHA-1和MD5彼此很相似。相应的,他们的强度和其他特性也是相似,但还有以下几点不同:

  • 对强行供给的安全性:最显著和最重要的区别是SHA-1摘要比MD5摘要长32 位。使用强行技术,产生任何一个报文使其摘要等于给定报摘要的难度对MD5是2128数量级的操作,而对SHA-1则是2160数量级的操作。这样,SHA-1对强行攻击有更大的强度。

  • 对密码分析的安全性:由于MD5的设计,易受密码分析的攻击,SHA-1显得不易受这样的攻击。

  • 速度:在相同的硬件上,SHA-1的运行速度比MD5慢。

1、特征:雪崩效应、定长输出和不可逆。

2、作用是:确保数据的完整性。

3、加密算法:md5(标准密钥长度128位)、sha1(标准密钥长度160位)、md4、CRC-32

4、加密工具:md5sum、sha1sum、openssl dgst。

5、计算某个文件的hash值,例如:md5sum/shalsum FileName,openssl dgst –md5/-sha

2.对称加密

秘钥:加密解密使用同一个密钥、数据的机密性双向保证、加密效率高、适合加密于大数据大文件、加密强度不高(相对于非对称加密)

对称加密优缺点

  • 优点:与公钥加密相比运算速度快。

  • 缺点:不能作为身份验证,密钥发放困难


DES是一种对称加密算法,加密和解密过程中,密钥长度都必须是8的倍数

public class DES {
public DES() {
}

// 测试
public static void main(String args[]) throws Exception {
// 待加密内容
String str = "123456";
// 密码,长度要是8的倍数 密钥随意定
String password = "12345678";
byte[] encrypt = encrypt(str.getBytes(), password);
System.out.println("加密前:" +str);
System.out.println("加密后:" + new String(encrypt));
// 解密
byte[] decrypt = decrypt(encrypt, password);
System.out.println("解密后:" + new String(decrypt));
}

/**
* 加密
*
* @param datasource
*           byte[]
* @param password
*           String
* @return byte[]
*/
public static byte[] encrypt(byte[] datasource, String password) {
try {
  SecureRandom random = new SecureRandom();
  DESKeySpec desKey = new DESKeySpec(password.getBytes());
  // 创建一个密匙工厂,然后用它把DESKeySpec转换成
  SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
  SecretKey securekey = keyFactory.generateSecret(desKey);
  // Cipher对象实际完成加密操作
  Cipher cipher = Cipher.getInstance("DES");
  // 用密匙初始化Cipher对象,ENCRYPT_MODE用于将 Cipher 初始化为加密模式的常量
  cipher.init(Cipher.ENCRYPT_MODE, securekey, random);
  // 现在,获取数据并加密
  // 正式执行加密操作
  return cipher.doFinal(datasource); // 按单部分操作加密或解密数据,或者结束一个多部分操作
} catch (Throwable e) {
  e.printStackTrace();
}
return null;
}

/**
* 解密
*
* @param src
*           byte[]
* @param password
*           String
* @return byte[]
* @throws Exception
*/
public static byte[] decrypt(byte[] src, String password) throws Exception {
// DES算法要求有一个可信任的随机数源
SecureRandom random = new SecureRandom();
// 创建一个DESKeySpec对象
DESKeySpec desKey = new DESKeySpec(password.getBytes());
// 创建一个密匙工厂
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");// 返回实现指定转换的
                  // Cipher
                  // 对象
// 将DESKeySpec对象转换成SecretKey对象
SecretKey securekey = keyFactory.generateSecret(desKey);
// Cipher对象实际完成解密操作
Cipher cipher = Cipher.getInstance("DES");
// 用密匙初始化Cipher对象
cipher.init(Cipher.DECRYPT_MODE, securekey, random);
// 真正开始解密操作
return cipher.doFinal(src);
}
}

输出

加密前:123456
加密后:>p.72|
解密后:123456

3.非对称加密

非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。

公钥与私钥是一对

  • 公钥对数据进行加密,只有用对应的私钥才能解密

  • 私钥对数据进行加密,只有用对应的公钥才能解密

过程:

  • 甲方生成一对密钥,并将公钥公开,乙方使用该甲方的公钥对机密信息进行加密后再发送给甲方;

  • 甲方用自己私钥对加密后的信息进行解密。

  • 甲方想要回复乙方时,使用乙方的公钥对数据进行加密

  • 乙方使用自己的私钥来进行解密。

  • 甲方只能用其私钥解密由其公钥加密后的任何信息。

特点:

  • 算法强度复杂

  • 保密性比较好

  • 加密解密速度没有对称加密解密的速度快。

  • 对称密码体制中只有一种密钥,并且是非公开的,如果要解密就得让对方知道密钥。所以保证其安全性就是保证密钥的安全,而非对称密钥体制有两种密钥,其中一个是公开的,这样就可以不需要像对称密码那样传输对方的密钥了。这样安全性就大了很多

  • 适用于:金融,支付领域

RSA加密是一种非对称加密

import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import org.apache.commons.codec.binary.Base64;


/**
* RSA加解密工具类
*
*
*/
public class RSAUtil {

public static String publicKey; // 公钥
public static String privateKey; // 私钥

/**
* 生成公钥和私钥
*/
public static void generateKey() {
// 1.初始化秘钥
KeyPairGenerator keyPairGenerator;
try {
  keyPairGenerator = KeyPairGenerator.getInstance("RSA");
  SecureRandom sr = new SecureRandom(); // 随机数生成器
  keyPairGenerator.initialize(512, sr); // 设置512位长的秘钥
  KeyPair keyPair = keyPairGenerator.generateKeyPair(); // 开始创建
  RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
  RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
  // 进行转码
  publicKey = Base64.encodeBase64String(rsaPublicKey.getEncoded());
  // 进行转码
  privateKey = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
} catch (NoSuchAlgorithmException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}
}

/**
* 私钥匙加密或解密
*
* @param content
* @param privateKeyStr
* @return
*/
public static String encryptByprivateKey(String content, String privateKeyStr, int opmode) {
// 私钥要用PKCS8进行处理
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyStr));
KeyFactory keyFactory;
PrivateKey privateKey;
Cipher cipher;
byte[] result;
String text = null;
try {
  keyFactory = KeyFactory.getInstance("RSA");
  // 还原Key对象
  privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
  cipher = Cipher.getInstance("RSA");
  cipher.init(opmode, privateKey);
  if (opmode == Cipher.ENCRYPT_MODE) { // 加密
  result = cipher.doFinal(content.getBytes());
  text = Base64.encodeBase64String(result);
  } else if (opmode == Cipher.DECRYPT_MODE) { // 解密
  result = cipher.doFinal(Base64.decodeBase64(content));
  text = new String(result, "UTF-8");
  }

} catch (Exception e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}
return text;
}

/**
* 公钥匙加密或解密
*
* @param content
* @param privateKeyStr
* @return
*/
public static String encryptByPublicKey(String content, String publicKeyStr, int opmode) {
// 公钥要用X509进行处理
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyStr));
KeyFactory keyFactory;
PublicKey publicKey;
Cipher cipher;
byte[] result;
String text = null;
try {
  keyFactory = KeyFactory.getInstance("RSA");
  // 还原Key对象
  publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
  cipher = Cipher.getInstance("RSA");
  cipher.init(opmode, publicKey);
  if (opmode == Cipher.ENCRYPT_MODE) { // 加密
  result = cipher.doFinal(content.getBytes());
  text = Base64.encodeBase64String(result);
  } else if (opmode == Cipher.DECRYPT_MODE) { // 解密
  result = cipher.doFinal(Base64.decodeBase64(content));
  text = new String(result, "UTF-8");
  }
} catch (Exception e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}
return text;
}

// 测试方法
public static void main(String[] args) {
/**
  * 注意: 私钥加密必须公钥解密 公钥加密必须私钥解密
  * // 正常在开发中的时候,后端开发人员生成好密钥对,服务器端保存私钥 客户端保存公钥
  */
System.out.println("-------------生成两对秘钥,分别发送方和接收方保管-------------");
RSAUtil.generateKey();
System.out.println("公钥:" + RSAUtil.publicKey);
System.out.println("私钥:" + RSAUtil.privateKey);

System.out.println("-------------私钥加密公钥解密-------------");
  String textsr = "11111111";
  // 私钥加密
  String cipherText = RSAUtil.encryptByprivateKey(textsr,
  RSAUtil.privateKey, Cipher.ENCRYPT_MODE);
  System.out.println("私钥加密后:" + cipherText);
  // 公钥解密
  String text = RSAUtil.encryptByPublicKey(cipherText,
  RSAUtil.publicKey, Cipher.DECRYPT_MODE);
  System.out.println("公钥解密后:" + text);

System.out.println("-------------公钥加密私钥解密-------------");
// 公钥加密
String textsr2 = "222222";

String cipherText2 = RSAUtil.encryptByPublicKey(textsr2, RSAUtil.publicKey, Cipher.ENCRYPT_MODE);
System.out.println("公钥加密后:" + cipherText2);
// 私钥解密
String text2 = RSAUtil.encryptByprivateKey(cipherText2, RSAUtil.privateKey, Cipher.DECRYPT_MODE);
System.out.print("私钥解密后:" + text2 );
}

}


四、使用加签名方式,防止数据篡改

客户端:请求的数据分为2部分(业务参数,签名参数),签名参数=md5(业务参数)

服务端:验证md5(业务参数)是否与签名参数相同
————————————————
作者:单身贵族男
来源:https://blog.csdn.net/zhou920786312/article/details/95536556

收起阅读 »

一个向上帝买了挂的男人!

约翰·冯·诺依曼是20世纪最有影响力的人物之一。从原子弹,到计算机、再到量子力学、气候变化,你可能很难出对我们今天的世界和生活影响更大的科学家了。 在20世纪的天才中,有几个杰出的人物:爱因斯坦、图灵、霍金,毫无疑问,冯·诺依曼也属于他们中的一个,尽管许多人不...
继续阅读 »

pic_8ae2a872.png

约翰·冯·诺依曼是20世纪最有影响力的人物之一。从原子弹,到计算机、再到量子力学、气候变化,你可能很难出对我们今天的世界和生活影响更大的科学家了。

在20世纪的天才中,有几个杰出的人物:爱因斯坦、图灵、霍金,毫无疑问,冯·诺依曼也属于他们中的一个,尽管许多人不知道他是谁。 约翰·冯·诺依曼是20世纪最有影响力的人物之一。他可能比过去150年中任何一位伟大的思想更直接地影响了你的生活,他的研究涉及从量子力学到气候科学的一切。 pic_1f8dc88d.png 冯·诺依曼最大的贡献是现代计算机,他采用了图灵奠定的卓越理论框架,并实际提出了为几乎所有数字计算机提供动力的架构:冯·诺依曼架构。 更有争议的是,冯·诺依曼在二战期间对曼哈顿计划做出了重大贡献,包括完善原子弹本身的设计和对其功能至关重要的机制。 pic_7ec06076.png 与曼哈顿计划的其他一些老兵不同,冯·诺依曼从未对自己在该计划中的角色表示遗憾,甚至在冷战期间推动了「同归于尽」的政策。 至少可以说,约翰·冯·诺依曼是一个复杂的人物,但他在20世纪几乎无人能及,可以说他对现代世界的责任比他同时代的任何人都大。

神童降世

约翰·冯·诺依曼于1903年12月28日出生在匈牙利首都布达佩斯。 冯·诺依曼的父亲是银行家,母亲是奥匈贵族的女儿,他的父母富有且受人尊敬。
pic_34745df7.png
1913年,奥匈帝国皇帝弗朗茨·约瑟夫授予冯·诺依曼的父亲贵族地位,并给了这个家族一个世袭的头衔「马吉塔」,即现在的罗马尼亚马吉塔。 这个头衔纯粹是尊称,因为这个家庭与这个地方没有任何联系,但这是冯·诺依曼一生都会坚持的。 年轻的冯·诺依曼在同龄人中,被认为是真正的神童,尤其是在数学方面。人们认为他有摄影一般的记忆力,这帮助他从很小的时候就吸收了大量的知识。 pic_9118426a.png 冯·诺伊曼11岁时与表妹 Katalin Alcsuti在一起 六岁时,他就开始在头脑中进行两个八位数的除法,八岁时,他已经掌握了微积分。 pic_1875c648.png 他的父亲认为,他所有的孩子都需要说除母语匈牙利语以外的欧洲主要语言,所以冯·诺依曼学习了英语、法语、意大利语和德语。 小时候,他对历史也有很深的兴趣,并阅读了德国历史学家威廉·昂肯的整部46卷专著《通史》。
pic_b93227b9.png
德国历史学家威廉·昂肯 在老师的鼓励下,冯·诺依曼在学习上取得了优异的成绩,但他的父亲不相信数学家的职业会带来经济上的利益。 相反,冯·诺依曼和他的父亲达成共识,同意冯·诺依曼从事化学工程,他17岁去柏林学习,后来在苏黎世学习。 化学似乎是冯·诺依曼少数几个不感兴趣的领域之一,尽管他确实获得了苏黎世化学工程文凭,同时还获得了数学博士学位。
pic_c0043b57.png

职业生涯早期

约翰·冯·诺依曼很早就发表了论文,从20岁开始,他写了一篇定义序数的论文,这仍然是我们今天使用的定义。 他写了关于集合论的博士论文,并在一生中对该领域做出了若干贡献。
到1927年,冯·诺依曼已经发表了12篇著名的数学论文。到1929年,他已经出版了32部作品,以大约一个月一篇学术论文的速度写出了很多重要的工作。 pic_1cb6f516.png 1928年,他成为柏林大学的一名私 人教师,也是柏林大学历史上获得该职位最年轻的人。 这个职位使他能够在大学里讲课,直到1929年他成为汉堡大学的一名私人教师。 冯·诺依曼在父亲于1929年去世后,皈依了天主教。1930年元旦,约翰·冯·诺依曼与布达佩斯大学经济学学生玛丽埃塔·科维西结婚,并于1935年与她生下了他唯一的孩子玛丽娜。 虽然冯·诺依曼似乎注定要在德国科学院从事一个有前途的职业,但在1929年10月,他获得了新泽西州普林斯顿大学的一个职位,他最终接受了这个职位,并于1930年与妻子一起前往美国。

移民美国

到1933年,约翰·冯·诺依曼成为新成立的普林斯顿高等研究院最初的六名数学教授之一,他也将在这个岗位上度过余生。 当他搬到新泽西时,像他之前的许多美国移民一样,冯·诺依曼将他的匈牙利名字英语化了(由玛吉塔伊·诺依曼·亚诺斯变成约翰·冯·诺依曼,使用德国式的世袭尊称)。 1937年,他和妻子离婚,第二年冯·诺依曼再婚,这次是和克拉拉·丹,他在第二次世界大战前最后一次访问匈牙利时,在布达佩斯第一次见到了克拉拉·丹。 pic_75b34244.png 1937年,冯·诺依曼成为美国公民,1939年,他的母亲、兄弟姐妹和姻亲也都移民到了美国(他的父亲早些时候去世了)。

战争年代与「曼哈顿计划」

约翰·冯·诺依曼对历史最重要的贡献之一是他在第二次世界大战期间对曼哈顿计划的研究。 一如既往,冯·诺依曼不能让数学挑战得不到解决,更困难的问题之一是如何模拟爆炸的影响。 冯·诺依曼在20世纪30年代投身于这些问题的研究,并成为该领域的专家。如果他有特长的话,那应该是研究聚能装药(Shaped Charges,用于爆破)领域的数学问题,聚能装药是用来控制和引导爆炸能量的。 pic_434c04b8.png 这使他与美国军方,特别是美国海军进行了相当多的定期磋商。当曼哈顿计划在20世纪40年代初开始工作时,冯·诺依曼因其专业知识而被招募。 1943年,冯·诺依曼对曼哈顿计划产生了最重大、最持久的影响。 pic_dd9691a8.png 曼哈顿计划成员 当时,设计原子弹的洛斯阿拉莫斯实验室发现,钚-239(该项目使用的裂变材料之一)与实验室的工作炸弹设计不兼容。 实验室的物理学家塞斯·尼德迈尔一直在研究一种独立的内爆型炸弹设计,这种设计很有希望,但许多人认为它不可行。
pic_bbad35a1.png
核弹内爆机制的动画 要引爆核爆炸,需要在炸弹的反应物中引发失控的裂变链式反应。链式反应的速度是指数级的,因此控制爆炸足够长的时间,使足够的裂变材料进行所需的反应,是一项重大挑战。 内爆型炸弹需要更复杂的控制来产生反应,但它也不需要像洛斯阿拉莫斯开发的枪型炸弹设计那样耗费那么多的材料。 内爆型装置使用一系列受控的常规爆炸来压缩其核心中的裂变反应物。 在这种压力下,裂变材料迅速开始核裂变链式反应,通过内爆的力量保持在原位,并允许更多的裂变材料释放其能量。
pic_32a52999.png
控制这些爆炸以产生精确的内爆力来产生期望的反应是一项很大的挑战,而冯·诺依曼以极大的热情接受了它。 他认为,使用较少的球形材料并通过内爆力适当压缩,可以产生更有效的爆炸,尽可能多地使用现有的裂变材料。 他经常是少数几个主张内爆方法的人之一,并最终计算出了一个数学公式,表明如果内爆能以至少95%的精度保持球形的几何形状,该方法就是可以实现的。 冯·诺依曼还计算出,如果爆炸在目标上方一定距离引爆,而不是在击中地面时引爆,爆炸的有效性将会提高。 这大大增加了原子弹的杀伤力,也减少了爆炸产生的尘埃量。 之后,冯·诺依曼被选为科学顾问团队的一员,他们就炸弹的可能目标咨询军方。 冯·诺依曼建议将目标定为日本京都,因为京都作为文化之都,其毁灭可能足以迫使战争迅速结束。 提出这一建议的不止他一个人,但战争部长亨利·史汀生否决了将目标对准京都这个提议,因为那里有许多历史建筑和重要的宗教圣地,所以最后选择了广岛和长崎。 1945年7月16日的三位一体试验中,冯·诺依曼在场,当时第一枚原子武器被引爆。广岛和长崎被炸后,日本投降,第二次世界大战结束。 pic_87622a6c.png 三位一体试验 与曼哈顿计划中同时代的一些人不同,冯·诺依曼似乎没有任何反思的痛苦、遗憾,甚至对他在原子弹方面的工作也没有一丝怀疑。 事实上,冯·诺依曼是核武器发展和相互保证毁灭(MAD)理论的最有力支持者之一,认为这是防止另一场灾难性世界大战的唯一方法。

核武器的「轻量化」思想

像战后初期的许多美国人一样,冯·诺依曼担心美国在核武器发展方面落后于苏联。 到上世纪40 年代末到50 年代初,用战略轰炸机向敌人投掷更多原子弹的理念,逐渐被新的火箭技术所取代。
冯·诺依曼认为,导弹是核武器的未来,由于他与参与苏联武器研制的德国科学家有过接触,他知道苏联对此问题的看法与他是一样的。 军备竞赛开始了,美苏两国把氢弹越做越小,可以装入洲际弹道导弹的弹头,冯·诺依曼积极为美国效力,努力缩小与苏联的「导弹差距」。 战后,冯·诺依曼在原子能委员会任职,为政府和军方提供核技术开发和战略方面的建议,并被广泛认为是「确保相互毁灭」理论(MAD)的设计者,而 MAD 在冷战期间确实被政府采纳,成为事实上的美国国策。

建造第一台真正的计算机

pic_75fe701f.png 冯·诺依曼在上世纪30年代初遇到了艾伦·图灵,当时图灵正在普林斯顿攻读博士学位。1937年,图灵发表了具有里程碑意义的论文《论可计算数》,奠定了现代计算的理论基础。
pic_3623b10a.png 冯·诺依曼很快认识到了图灵这一发现的重要性,并在30年代推动了计算机科学的发展。在普林斯顿大学,他和图灵围绕人工智能的思想曾进行了长时间的讨论。 作为一名数学家,冯·诺依曼从更抽象的角度研究计算机科学,另一个原因也是因为在30年代时,并没有真正可以工作的计算机。 在二战结束后,这种情况很快就改变了。 pic_bf84e043.png 冯·诺依曼深入参与了第一台可编程电子计算机「ENIAC」的开发,这台计算机能够识别和决定其他数据操作规则集,而不是最初使用的规则集。是冯诺依曼将 ENIAC 修改为作为存储程序机器运行。 后者让使我们今天理解的现代程序成为可能。冯·诺依曼本人编写了几个在 ENIAC 上运行的首批程序,并用这些程序模拟原子能委员会的部分核武器研究。 毫无疑问,冯·诺依曼对计算机科学领域最持久的贡献是在当今运行的每台计算机中使用的两个基本概念:冯·诺依曼体系架构和存储程序概念。 pic_e701e538.png 冯·诺依曼架构涉及构成计算机的物理电子电路的组织方式。按照这种方式构建的计算机被称为「冯·诺依曼机」。该架构由算术和逻辑单元 (ALU)、控制单元和临时存储器寄存器组成,它们共同构成了中央处理器 (CPU)。 CPU 连接到内存单元,该内存单元包含将要由CPU处理和操作的所有数据。CPU还连接到输入和输出设备,以根据需要更改数据,并检索运行程序的结果。 自 1945 年冯诺依曼提出这一架构以来,直到今天,它基本上仍是当今大多数通用计算机的运行方式,几乎没有改变。 另一项重大创新与冯·诺依曼架构有关,即存储程序概念,也就是说,被操作或处理的数据,以及描述如何操纵和处理该数据的程序,都存储在计算机的内存中。 这两项相互交织的创新实现了图灵机的理论框架,实际上将它们变成了可以用来计算工资、火炮轨迹、游戏、互联网等几乎所有一切数据的机器。

对其他领域的杰出贡献

除了数学和计算机科学之外,冯·诺依曼一生都对其他几个领域也做出了重大贡献。
在早期的职业生涯中,冯·诺依曼为新兴的量子力学领域做出了重大贡献。 pic_e52acca3.png 1932年,他和保罗·狄拉克在《量子力学的数学基础》一书中发表了狄拉克-冯·诺依曼公理,这是该领域的第一个完整的数学框架。在这本书中,他还提出了量子逻辑的形式系统,也是同类体系中的首创。 冯·诺依曼还将博弈论确立为一门严谨的数学学科,这无疑影响了他后来关于MAD理论的地缘政治战略工作。 pic_a9fc558f.png 冯·诺依曼的博弈论中包含这样一个观点,即在广泛的博弈类别中,总是有可能找到一个平衡,任何参与者都不应单方面偏离这个平衡。 在生命科学领域,冯·诺依曼对元胞自动机的自我复制进行了彻底的数学分析,主要是构造函数、正在构建的事物以及构造函数构建所讨论事物所遵循的蓝图之间的关系。该分析描述了一种自我复制的机器,它是在40年代设计的,没有使用计算机。 冯·诺依曼的数学造诣也惠及气候科学。1950年,他编写了第一个气候建模程序,并使用 ENIAC 使用数值数据进行了世界上第一个气象预测。 冯·诺依曼预计,全球变暖是人类活动的结果,他在1955年写道: 「工业燃烧煤和石油释放到大气中的二氧化碳,可能已经充分改变了大气的成分,导致全球普遍变暖约1华氏度。」 冯·诺依曼也被认为是第一个描述「技术奇点」的人。冯·诺依曼的朋友斯坦·乌拉姆 (Stan Ulam) 后来描述了与他的一次对话,这次对话在今天听起来非常有先见之明。 pic_ccf72d4b.png 斯坦·乌拉姆、理查德·费曼和冯·诺依曼在一起 乌拉姆回忆说:「有一次谈话集中在不断加速的技术进步和人类生活方式的变化上,这让我们看到了人类历史上一些本质上的奇点。一旦超越了这些奇点,我们所熟知的人类事务就将无法继续下去了。」

冯·诺依曼的去世,和他的光辉遗产

1955 年,冯·诺依曼在看医生时发现他的锁骨上长了一块肉,他被诊断患有癌症,但他并没有充分接受这个事实。 众所周知,冯·诺依曼对即将到来的结局感到恐惧。他的一生好友尤金·维格纳 (Eugene Wigner) 写道:
pic_4c140e80.png 「当冯·诺依曼意识到自己病入膏肓时,他的逻辑迫使他意识到,自己即将不复存在,因此也不再有思想……亲眼目睹这一过程是令人心碎的,所有的希望都消失了,即将到来的命运尽管难以接受,但已经不可避免。」 冯·诺依曼的病情在1956年持续恶化,最终被送进了华盛顿特区的沃尔特里德陆军医疗中心。为防止泄密,军方对他实施了特殊的安全措施。 冯·诺依曼邀请一位天主教神父在他临终前商量,并接受了他的临终仪式安排。不过这位神父本人表示,冯诺依曼看上去似乎并没有从仪式中得到安慰。 1957年2月8日,冯·诺依曼因癌症逝世,享年53岁,他被安葬在新泽西州的普林斯顿公墓。 关于冯·诺依曼的癌症是否与他在「曼哈顿计划」期间遭受辐射有关,人们一直存在争议,但毫无争议的是,人类过早地失去了当代最伟大的科学巨人之一。 pic_1628bda4.png 冯·诺依曼的助手 P.R. Halmos 在1973年写道: 「人类的英雄有两种:一种和我们所有人一样,但更加相似,另一种显然具有一些「超人」的特质。 我们都可以跑步,我们中的一些人可以在不到4分钟的时间内跑完一英里。但有些事,我们大多数人一辈子都无法做到。冯·诺依曼的伟大贡献是惠及全人类的。在某些时候,我们或多或少都能清晰地思考,但冯·诺依曼的清晰思维始终比我们大多数人高出好几个数量级。」 冯·诺依曼的才华是毋庸置疑的,尽管他留下的遗产,尤其是核武器方面的贡献,比他的朋友和崇拜者愿意承认的要复杂得多。 无论我们最终如何看待冯·诺依曼和他的成就,我们都可以肯定地说,在未来一代人甚至几代人的时间里,都不太可能出现像他一样,对人类历史产生如此重大影响的人了。

转自:新智元 | David 小咸鱼
原文链接:https://interestingengineering.com/john-von-neumann-human-the-computer-behind-project-manhattan

收起阅读 »

【小声团队】 - 我们为什么选择了Flutter Desktop

本文由小声团队出品,小声团队是一个专注于音频&音乐技术的初创团队,深度使用Flutter构建跨平台应用,希望与大家一起共同探索Flutter在桌面端&移动端的可能性。 背景 我们计划研发一款全功能跨平台的音乐制作平台(DAW),从立项之初我们...
继续阅读 »

本文由小声团队出品,小声团队是一个专注于音频&音乐技术的初创团队,深度使用Flutter构建跨平台应用,希望与大家一起共同探索Flutter在桌面端&移动端的可能性。


背景


小声团队使用Flutter Desktop研发的DAW截屏


我们计划研发一款全功能跨平台的音乐制作平台(DAW),从立项之初我们就已经明确了全平台的支持计划(即Windows / MacOS / Linux / iOS / Android ) ,也因此我们也是以这个为目标来寻找技术解决方案,经过一段时间的研究与学习,大致确定了几个可选项,内部的调研结果如下(本结果仅代表团队内部认知,如有差异还请包涵):

技术方案性能研发效率跨平台兼容性扩展能力原生代码交互能力
HTML5
QT极低
React Native
Flutter



为什么不使用基于HTML5打造的技术栈?


HTML5是众所周知的最易上手的跨平台UI解决方案,并且产业成熟,有众多可选的框架与开源组件可直接使用。但是DAW作为一款专业生产力工具并不适合完全在浏览器环境中运行,比如第三方插件系统浏览器则无法支撑,另外在内存资源上的使用也不是很便捷,通常一个音乐工程可能需要占据数G内存,运行时需要维护数万个对象,这对于Javascript来说还是浏览器来说都是很严重的负担。
从另一个方面来看,就算我们需要以一种阉割的形式支持Web,那么WASM技术则是我们更佳的选择。
因此,我们不考虑基于HTML5的技术方案。


为什么不选择QT & GTK 等老牌原生高性能框架?


在传统技术上来看,QT是最符合我们需求的技术方案,很多老牌工具厂商背后也都是基于QT技术栈完成。QT在运行效率上而言无疑是最佳的选择,我们的主要顾虑在对于CPP的掌控能力与研发效率,UI开发与引擎开发有一个很大的根本区别在于引擎开发通常使用单元测试来完成逻辑验证,而UI则很难使用单元测试来验证UI效果,也很少看到有团队真的依赖单元测试的方式来进行UI开发,而QT没有像Webpack类似的hot reload技术,UI的验证效率会非常的低下,甚至于不是我们一个小团队可以承受得起的。
而CPP也是入门门槛极高的编程语言,我们对于QT方案也存疑,但是没有完全放弃。


Flutter 的什么特性吸引了我们



  • Flutter使用基于Skia绘图引擎直接构建组件,操作系统只需要提供像素级的绘图能力即可,因此也就保证了跨平台的UI一致性(像素级一致),而对React Native的兼容性吐槽一直充斥着社区。

  • Dart对于UI开发也是非常舒服的。

    • 对象默认引用传递。

    • 支持HOT Reload。这为开发效率带来本质的提升,使得Flutter研发效率不弱于HTML5

    • AOT支持,生产级代码运行效率飞升,不逊色于原生应用的表现。

    • FFI 支持 。 可以直接与原生C & Cpp代码进行交互而几乎没有任何性能损失。

    • Web 支持。 Flutter 即可直接编译到Web运行,这也为我们提供Web服务打下了可能性。




Flutter的这些特性都是直击我们需求的,所以我们决定尝试使用Flutter来构建我们的平台。


结论


如果你也在寻找一个技术技术方案兼顾研发效率与运行时效率,那么Flutter应该是一个很不错的选择。


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

10分钟用Flutter写出摸鱼桌面版App

起因摸鱼是不可能摸鱼的,这辈子都不会摸鱼。缘起是 郭佬 分享了一则微博:share.api.weibo.cn/share/26900…顿时这个念头划过了我的脑海:好东西,但是我用的是 MacBook,不能用这个应用。但是貌似我可以自己写一个...
继续阅读 »

起因

摸鱼是不可能摸鱼的,这辈子都不会摸鱼。

缘起是 郭佬 分享了一则微博:share.api.weibo.cn/share/26900…

image-20211217002031951

顿时这个念头划过了我的脑海:好东西,但是我用的是 MacBook,不能用这个应用。但是貌似我可以自己写一个?

准备工作

年轻最需要的就是行动力,想到就干,尽管我此刻正在理顺 DevFest 的讲稿,但丝毫不妨碍我用 10 分钟写一个 App。于是我打出了一套组合拳:

  • flutter config --enable-macos-desktop
  • flutter create --platforms=macos touch_fish_on_macos

一个支持 macOS 的 Flutter 项目就创建好了。(此时大约过去了 1 分钟)

开始敲代码

找到资源

我们首先需要一张高清无码的  图片,这里你可以在网上进行搜寻,有一点需要注意的是,使用 LOGO 要注意使用场景带来的版权问题。找到图片后,丢到 assets/apple-logo.png,并在 pubspec.yaml 中加上资源引用:

flutter:
use-material-design: true
+ assets:
+ - assets/apple-logo.png

思考布局

我们来观察一下 macOS 的启动画面,有几个要点:

image-20211217003055127

  • LOGO 在屏幕中间,固定大小约为 100dp;
  • LOGO 与进度条间隔约 100 dp;
  • 进度条高度约 5dp,宽度约 200dp,圆角几乎完全覆盖高度,值部分为白色,背景部分为填充色+浅灰色边框。

(别问我为什么这些东西能观察出来,问就是天天教 UI 改 UI。)

确认了大概的布局模式,接下来我们开始搭布局。(此时大约过去了 2 分钟)

实现布局

首先将 LOGO 居中、着色、设定宽度为 100,上下间隔 100:

return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(vertical: 100),
child: Image.asset(
'assets/apple-logo.png',
color: CupertinoColors.white, // 使用 Cupertino 系列的白色着色
width: 100,
),
),
const Spacer(),
],
),
);

然后在下方放一个相对靠上的进度条:

return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(vertical: 100),
child: Image.asset(
'assets/apple-logo.png',
color: CupertinoColors.white, // 使用 Cupertino 系列的白色
width: 100,
),
),
Expanded(
child: Container(
width: 200,
alignment: Alignment.topCenter, // 相对靠上中部对齐
child: DecoratedBox(
border: Border.all(color: CupertinoColors.systemGrey), // 设置边框
borderRadius: BorderRadius.circular(10), // 这里的值比高大就行
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10), // 需要进行圆角裁剪
child: LinearProgressIndicator(
value: 0.3, // 当前的进度值
backgroundColor: CupertinoColors.lightBackgroundGray.withOpacity(.3),
color: CupertinoColors.white,
minHeight: 5, // 设置进度条的高度
),
),
),
),
],
),
);

到这里你可以直接 run,一个静态的界面已经做好了。(此时大约过去了 4 分钟)

打开 App,你已经可以放在一旁挂机了,老板走到你的身边,可能会跟你闲聊更新的内容。但是,更新界面不会动,能称之为更新界面? 当老板一而再再而三地从你身边经过,发现还是这个进度的时候,也许就已经把你的工资划掉了,或者第二天你因为进办公室在椅子上坐下而被辞退。

那么下一步我们就要思考如何让它动起来。

思考动画

来看看启动动画大概是怎么样的:

Kapture 2021-12-17 at 00.51.40

  • 开始是没有进度条的;
  • 进度条会逐级移动、速度不一定相等。

基于以上两个条件,我设计了一种动画处理方式:

  • 构造分段的时长 (Duration),可以自由组合由多个时长;
  • 动画通过时长的数量决定每个时长最终的进度;
  • 每段时长控制起始值到结束值的间隔。

只有三个条件,简单到起飞,开动!(此时大约过去了 5 分钟)

实现动画

开局一个 AnimationController

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
/// 巧用 late 初始化,节省代码量
late final AnimationController _controller = AnimationController(vsync: this);

/// 启动后等待的时长
Duration get _waitingDuration => const Duration(seconds: 5);

/// 分段的动画时长
List<Duration> get _periodDurations {
return <Duration>[
const Duration(seconds: 5),
const Duration(seconds: 10),
const Duration(seconds: 4),
];
}

/// 当前进行到哪一个分段
final ValueNotifier<int> _currentPeriod = ValueNotifier<int>(1);

接下来实现动画方法,采用了递归调用的方式,减少调用链的控制:

@override
void initState() {
super.initState();
// 等待对应秒数后,开始进度条动画
Future.delayed(_waitingDuration).then((_) => _callAnimation());
}

Future<void> _callAnimation() async {
// 取当前分段
final Duration _currentDuration = _periodDurations[currentPeriod];
// 准备下一分段
currentPeriod++;
// 如果到了最后一个分段,取空
final Duration? _nextDuration = currentPeriod < _periodDurations.length ? _periodDurations.last : null;
// 计算当前分段动画的结束值
final double target = currentPeriod / _periodDurations.length;
// 执行动画
await _controller.animateTo(target, duration: _currentDuration);
// 如果下一分段为空,即执行到了最后一个分段,重设当前分段,动画结束
if (_nextDuration == null) {
currentPeriod = 0;
return;
}
// 否则调用下一分段的动画
await _callAnimation();
}

以上短短几行代码,就完美的实现了进度条的动画操作。(此时大约过去了 8 分钟)

最后一步,将动画、分段二者与进度条绑定,在没进入分段前不展示进度条,在动画开始后展示对应的进度:

ValueListenableBuilder<int>(
valueListenable: _currentPeriod,
builder: (_, int period, __) {
// 分段为0时,不展示
if (period == 0) {
return const SizedBox.shrink();
}
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: CupertinoColors.systemGrey),
borderRadius: BorderRadius.circular(10),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: AnimatedBuilder( // 使用 AnimatedBuilder,在动画进行时触发更新
animation: _controller,
builder: (_, __) => LinearProgressIndicator(
value: _controller.value, // 将 controller 的值绑定给进度
backgroundColor: CupertinoColors.lightBackgroundGray.withOpacity(.3),
color: CupertinoColors.white,
minHeight: 5,
),
),
),
);
},

大功告成,总共用时 10 分钟,让我们跑起来看看效果。(下图 22.1 M)

Kapture 2021-12-17 at 01.15.08

这还原度,谁看了不迷糊呢?🤩从此开启上班摸鱼之路...

打包发布

发布正式版的 macOS 应用较为复杂,但我们可以打包给自己使用,只需要一行命令即可:flutter build macos

成功后,产物将会输出在 build/macos/Build/Products/Release/touch_fish_on_macos.app,双击即可使用。

结语

至此,一个简单的能在 macOS 上运行的摸鱼 Flutter App 就这么开发完成了。完整的 demo 可以访问我的仓库:github.com/AlexV525/fl… 。你可以给它提一些需求,我觉得这样一款软件还是挺有意思的。

可能大多数人都没有想到,编写一个 Flutter 应用,跑在 macOS 上,能有这么简单。当然,看似短暂的 10 分钟并没有包括安装环境、搜索素材、提交到 git 的时间,但在这个时间范围内,完成相关的事情也是绰绰有余。

希望这篇文章能激起你学习 Flutter 的兴趣,或是在桌面端尝试 Flutter 的兴趣,又或是挑战自己编程速度的兴趣(别说 10 分钟不可能,实际上我用的时间还要更少)。


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

收起阅读 »

Flutter(一)Hello, Flutter!

1. 移动端开发演变过程 1.1 原生开发 原生应用程序是指某一个移动平台(比如iOS或安卓)所特有的应用,使用相应平台支持的开发工具和语言,直接调用系统SDK API。 Android原生应用:使用Java或Kotlin直接调用Android SDK开发...
继续阅读 »

1. 移动端开发演变过程


1.1 原生开发


原生应用程序是指某一个移动平台(比如iOS或安卓)所特有的应用,使用相应平台支持的开发工具和语言,直接调用系统SDK API。




  • Android原生应用:使用Java或Kotlin直接调用Android SDK开发的应用程序;




  • iOS原生应用:通过Objective-C或Swift语言直接调用iOS SDK开发的应用程序。




主要优势:




  • 可直接无障碍的访问平台全部功能;




  • 速度快、性能高、可以实现复杂动画及绘制,整体用户体验好;




主要缺点:




  • 开发成本高;不同平台必须维护不同代码,人力成本、测试成本大;




  • 内容固定,动态化弱,大多数情况下,有新功能更新时只能发版,但应用上架、审核是需要周期的,这对高速变化的互联网时代来说是很难接受的;




针对动态化和开发成本两个问题,诞生了一些跨平台的动态化框架。👇👇👇👇👇👇


1.2 跨平台技术简介


这里的跨平台指Android和iOS两个平台。根据其原理,主要分为三类:




  • H5+原生(Cordova、Ionic、微信小程序)=> Hybrid/混合开发





  • 原理:APP需要动态变化的内容通过H5来实现,进一步通过原生的网页加载控件WebView (Android)或WKWebView(iOS)来加载。






  • WebView实质上是一个浏览器内核,其JavaScript依然运行在一个权限受限的沙箱中,所以对于大多数系统能力都没有访问权限,如无法访问文件系统、不能使用蓝牙等。所以,对于H5不能实现的功能,都需要原生去做。






  • 核心:混合框架会在原生代码中预先实现一些访问系统能力的API, 暴露给WebView以供JavaScript调用,让WebView成为JavaScript与原生API之间通信的桥梁,主要负责JavaScript与原生之间传递调用消息 => JsBridge(WebView JavaScript Bridge)





  • JavaScript开发+原生渲染 (React Native、Weex、快应用)




这里主要介绍下 React Native 特点




  • 和 React 原理相似,支持响应式编程,开发者只需关注状态转移,不需要关注UI绘制;




  • React 和 React Native 主要的区别在于虚拟DOM映射的对象是什么:





  • React中虚拟DOM最终会映射为浏览器DOM树;






  • RN中虚拟DOM会通过 JavaScriptCore 映射为原生控件树。(两步)







  • 第一步:布局消息传递; 将虚拟DOM布局信息传递给原生;








  • 第二步:原生根据布局信息通过对应的原生控件渲染控件树;






  • 优点:





  • 采用Web开发技术栈,社区庞大、上手快、开发成本相对较低。






  • 原生渲染,性能相比H5提高很多。






  • 动态化较好,支持热更新。





  • 不足:





  • 渲染时需要JavaScript和原生之间通信,在有些场景如拖动可能会因为通信频繁导致卡顿;






  • JavaScript为脚本语言,执行时需要JIT(Just In Time),执行效率和AOT(Ahead Of Time)代码仍有差距;






  • 由于渲染依赖原生控件,不同平台的控件需要单独维护。





RN架构升级,进行中,解决频繁和原生通信的瓶颈问题。)




  • 自绘UI+原生(QT for mobile、Flutter)





  • QT for mobile(是移动端开发跨平台自绘引擎的先驱,也是烈士。)






  • Flutter 👇👇👇👇👇👇





2. Flutter 介绍


Flutter 是 Google 推出并开源的移动应用框架,主打跨平台、高保真、高性能。


2.1 跨平台自绘引擎


Flutter 与用于构建应用程序的其他框架不同,是因为 Flutter 既不使用 WebView 也不使用操作系统的原生控件。相反,Flutter 自己实现了自绘引擎。这样不仅能保证一套代码可以同时运行在 IOS 和 Android 平台上,还保证了 Android 和 IOS 上 UI 的一致性,而且也可以避免对原生控件依赖带来的限制和高昂的成本维护,也不用和native层做过多的通信,大大提高性能。


Flutter 使用 Skia 作为其2D渲染引擎,Skia 是一个 Google 的2D图形处理函数,包含字符、坐标转换,以及点阵图都有高效能且简介的表现,Skia 是跨平台的,并提供了非常友好的 API ,目前 Google Chrome 浏览器和 Android 均采用 Skia 作为其绘图引擎。


目前 Flutter 默认支持 Android、IOS、Fuchsia(Google新的自研操作系统)、鸿蒙四个移动平台,也支持 Web 开发(Flutter for web)、 PC 、小程序的开发。


2.2 采用Dart语言


这是一个很有意思也很有争议的问题,Flutter为什么选择Dart语言?


开发效率高。Dart运行时和编译器支持Flutter两个关键特性的组合:


基于JIT的快读开发周期:Flutter在开发阶段采用JIT模式。这样就避免了每次改动都要进行编译,极大节省了开发时间;并且在iOS和Android模拟器或真机上可以实现毫秒级热重载,并且不会丢失状态。


基于AOT的发布包:Flutter 在发布时间可以通过AOT生成高效的ARM代码以保证应用性能。而JavaScript则不具有这个能力(虽然可以通过打包工具实现)。



目前,程序主要有两种运行方式:静态编译与动态解释。

静态编译:在执行前全部被翻译为机器码

👉 AOT(Ahead of time)即“提前编译”; AOT程序的典型代表是用C/C++开发的应用,他们必须在执行前编译成机器码; 动态解释(解释执行)则是一句一句边翻译边运行

👉 JIT(Just-in-time)即“即时编译”。 JIT的代表则非常多,如JavaScript、Python等。

事实上,所有脚本语言都支持JIT模式。但需要注意的是,JIT和AOT 指的是程序运行方式,和编译语言并非强相关,有些语言既可以以JIT方式运行也可以以AOT方式运行,如Java、Python,他们可以在第一次执行时编译成中间字节码,然后在之后执行时可以直接执行字节码,也许有人会说,中间字节码并非机器码,在程序执行时仍需动态将字节码转换成机器码,是的,这没有错,不过通常我们区分是否为AOT的标准就是看代码在执行前是否需要编译,只要需要编译,无论其编译产物是字节码还是机器码,都属于AOT(不必纠结于概念,概念是为了传达精神而发明的,理解原理即可)。



高性能。Flutter提供流畅、高保真的UI体验,为了实现这一点,Flutter中需要能够在每个动画帧中运行大量的代码。这意味着需要一种既能提供高性能的语言,而不会出现丢帧的周期性暂停的问题,而Dart支持AOT,在这一点上可以比JavaScript做的更好。


类型安全。由于Dart 是类型安全的语言,支持静态类型检查,所以可以在编译前发现一些类型错误,并排除潜在问题。这一点对于前端开发者极具吸引力,为了解决JavaScript弱类型的缺点,前端社区出现了很多给JavaScript代码添加静态类型检测的扩展语言和工具,如:微软的TypeScript、Facebook的Flow。而 Dart 本身就支持静态类型,这是他的一个重要优势。


快速内存分配。Flutter框架使用函数式流,这使得它在很大程度上依赖底层内存分配器。因此,拥有一个能够有效的处理琐碎任务的内存分配器将显得十分重要。但其实在内存分配上Dart并没有超越JavaScript,只是Flutter需要,Dart 恰好满足。


Dart 团队就在 Flutter身边。Dart 语言也是谷歌推出的,由于有Dart团队的积极投入,Flutter团队可以获得更多、更方便的支持。



例如:Dart 最初并没有提供原生二进制文件的工具链(这对于实现可预测的高性能有很大帮助),但是现在它实现了,因为 Dart 团队专门为 Flutter 构建了它。 Dart VM 之前已经针对吞吐量进行了优化,但团队现在正忙于优化VM的延迟,这对于Flutter的工作负载更为重要。



2.3 高性能


Flutter 高性能主要是靠以上刚刚介绍的两点来保证的:


采用 Dart 语言开发。Dart 在 JIT(即时编译)模式下,速度与 JavaScript 基本持平,但是 Dart 支持 AOT,当以 AOT 模式运行时,JavaScript 就远远追不上了。速度的提升对高帧率下的数据计算很有帮助。


使用自己的渲染引擎来绘制 UI,布局数据由 Dart 语言直接控制,在布局过程中不需要像 RN 那样要在 JavaScript 和 Native 之间通信,这在一些滑动拖动场景下有明显优势,因为在滑动和拖动过程中往往都会引起布局发生变化,所以 JavaScript 需要在 Native 之间不停的同步布局信息,这和在浏览器中要 JavaScript 频繁操作 DOM 所带来的问题是相同的,都会带来比较大的性能开销。


2.4 响应式框架


借鉴 React 响应式的 UI 框架设计思想。中心思想是用 widget 构建你的 UI。 Widget 描述了他们的视图在给定其当前配置和状态时应该看起来像什么。当 widget 的状态发生变化时,widget 会重新构建 UI,Flutter 会对比前后变化的不同, 来确定底层渲染树从一个状态转换到下一个状态所需的最小更改(类似于 React/Vue 中虚拟 DOM 的 diff 算法)。


2.5 多端编译


2.5.1 移动端




  • 打包 Android 并发布手机商店 👉 VSCode





  • 打包命令



    flutter build apk





  • 发布:







  • 注册各大应用市场开发者账号








  • 创建要发布的app信息








  • 按照流程上传






  • 打包 IOS 并发布 Apple Store 👉 XCode





  • 打包命令



    flutter build ios





  • 发布:







  • 注册 Apple 开发者








  • 创建要发布的app信息








  • 配置钥匙串








  • 在xcode配置apple stoer的证书和bundle id








  • xcode>Product>Archive>Distribute App







2.5.2 Web端




  • Flutter 2.x




  • 检查是否支持web开发 $ flutter devices







  • 打包命令


    flutter build web --web-renderer html # 打开速度最快,兼容性好
    flutter build web # 打开速度一般,兼容性好
    flutter build web --web-renderer canvaskit # 打开速度最慢,兼容性好




  • 部署:直接部署到服务器上即可




3. Flutter 框架结构


3.1 移动架构



3.1.1 Framework 框架层


框架层。这是一个纯dart实现的响应式框架,它实现了一套基础库。




  • 底下两层(Foundation、Animation、Painting、Gestures)在Google的一些视频中被合并为一个Dart UI层,对应的是Flutter中的dart:ui包,它是Flutter引擎暴露的底层UI库,提供动画、手势及绘制能力。




  • Rendering 层,是一个抽象层,它依赖于 Dart UI ,这一层会构建一个UI树,当UI有变化时,会计算出有变化的部分,然后更新UI树,最终将UI树绘制到屏幕上,这个过程类似于React中的虚拟dom。Rendering层可以说是Flutter UI 框架的核心,它除了确定每个UI元素的位置、大小之外,还要调用底层dart:ui进行坐标变化和绘制。




  • Widgets层是Flutter提供的一套基础组件




  • Material 和 Cupertino 是 Flutter 提供的两种视觉风格的组件库,Material 安卓风格,Cupertino 是IOS风格。




实际开发过程中,主要都是和最上面的Widgets、Material/Cupertino两层打交道。


3.1.2 Engine 引擎层


纯C++实现的SDK,为Flutter的核心,提供了Flutter核心API的底层实现。其中包括了Dart运行时、Skia引擎、文字排版引擎等,是连接框架和系统(Android/IOS)的桥梁。在代码调用dart:ui库时,调用最终会走到Engine层,然后实现真正的绘制逻辑。


3.1.3 Embedder 嵌入层


嵌入层基本是由平台对应的语言来实现的。例如:在Android上,是由Java和C++ 来实现的,IOS是由Object-C和Object-C++来实现的。


嵌入层为Flutter提供了一个入口,Flutter系统是通过该入口访问底层系统提供的服务,例如输入法、绘制surface等。


3.2 Web端架构



3.3 移动端开发方式架构比较



4. 移动端开发对比


4.1 方案对比



4.2 性能对比



感兴趣 这里 有详细的性能对比介绍,不赘述。


5. Flutter 周边——调试工具


Flutter日志 和 print(); 是比较常用的检查数据的调试方法,但是不支持图形化界面、布局等的检查。👇👇👇👇👇👇


5.1 inspector 插件


可视化和浏览Flutter Widget树的工具。查看渲染树,了解布局信息。


5.2 Dev Tools


5.3 UME


字节 Flutter Infra 团队推出的开源 应用内 调试工具。



Pub.dev 发布地址

pub.flutter-io.cn/packages/fl…

GitHub 开源地址

github.com/bytedance/f…



功能 => UI 检查、代码查看、日志查看、性能工具 ...


服务于 => 设计师、产品经理、研发工程师或质量工程师 ...



  • UI 检查插件包。点选 widget 获取 widget 基本信息、代码所在目录、widget 层级结构、RenderObject 的 build 链与描述的能力,颜色拾取与对齐标尺在视觉验收环节提供有力帮助。







  • 代码查看插件。允许用户输入关键字,进行模糊匹配,对任意代码内容的查看能力。




  • 日志查看。日志信息可通过统一面板提供筛选、导出等。




  • 性能检测包。提供了当前 VM 对象实例数量与内存占用大小等信息。




6. Flutter 周边——学习路线


6.1 学习社区




  • Flutter 中文网 👉 入门文档




  • Flutter 中文社区 👉 入门文档




  • 咸鱼Flutter 👉 可借鉴的架构方案




  • 掘进Flutter 👉 专业性强的博客




  • StackOverflow 👉 问答社区,我遇到的flutter的问题,这里的答案最靠谱




  • 源码及注释 👉 🙋‍♂️🙋‍♂️🙋‍♂️🙋‍♂️🙋‍♂️🙋‍♂️ 最方便开发时查找




6.2 学习路线


1、准备期



  • 目标:




  1. 通过学习 Dart 基本语法,了解 Flutter 中的 Dart 的基本使用;




  2. 学习 Flutter 提供的基础布局和基本内容组件,完成基本页面展示。





2、入门期



3、进阶期



6.3 学习开源项目


UI组件集项目 FlutterUnit


7. Flutter 周边——JS2Flutter



JS 转 Dart => JS2Flutter


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

一兜糖 APP :用 Flutter 链接关于家的一切

一兜糖家居,成立于2009年,2015年获得腾讯系战略投资,截至2020年平台已累积3000万装修用户、5万设计师和10万经销商。自成立以来,我们通过「找灵感——学知识——做决策」全路径的优质内容输出,帮助用户提高消费决策的效率;通过门店线上化、口碑线上化和获...
继续阅读 »

一兜糖家居,成立于2009年,2015年获得腾讯系战略投资,截至2020年平台已累积3000万装修用户、5万设计师和10万经销商。自成立以来,我们通过「找灵感——学知识——做决策」全路径的优质内容输出,帮助用户提高消费决策的效率;通过门店线上化、口碑线上化和获客线上化,帮助品牌全面种草;通过内容、社群及用户资源,全面释放设计师价值,帮助设计师提高选品及获客效率。通过链接关于家的一切,我们希望让家变得有温度。



说重点


一兜糖 APP 在 2020 年开始尝试使用 Flutter 重构 iOS / Android 应用,使用 4 个月时间逐步迁移至 Flutter 工作流上。然后又使用一个月的时间,将 H5 以及微信小程序迁移到 MPFlutter 上。\


在重构完成后,一兜糖仅使用一套代码,便完成了在 iOS / Android / H5 / 微信小程序四端的部署,提升近四倍开发效率。


本文将与您分享一兜糖在重构过程中遇到的问题和解决方案。


一兜糖 APP 重构前的问题


2015 年,一兜糖家居开始发行 Android / iOS 两个平台的 APP,经过五年多的打磨,APP 内容日渐丰富,风格也越来越贴近用户使用习惯。直至 2020 年 4 月份,一兜糖家居 APP 均采用原生(Swift / Java)开发的方式进行,随着业务的快速增长以及五年多的技术迭代,面临的问题也越来越多。



  • Android / iOS 两个平台开发进度不统一,实现效果差异性较大,随着业务需求的累积,差异越来越大。

  • 项目日常维护开发所需人力越来越多,开发成本越来越高,Android / iOS 需要开发人员分别进行开发,同一个功能需要双倍的工作量来完成,如果把 H5 / 小程序涵括在内,同一个功能甚至需要四倍的工作量。

  • 将近五年的技术变更,冗余代码越来越多,导致项目的编译时长越来越长,往往一个小小的变动,都要经过漫长的等待时间后才能看到改动后的效果。


为了解决以上问题,2020 年 4 月,一兜糖 APP 开发团队决定对 APP 进行重构,希望重构能够实现快速开发,减少两个平台的差异等问题。而此时,市面上 APP 开发方案除了原生开发,跨平台方案也已经相对成熟,使用哪种技术重构 APP 成为一兜糖移动团队需要面临的第一个抉择。


原生 VS 跨平台


原生开发的方式,两个平台的开发人员分别进行开发,可以以一种很高效的方式调度系统资源,性能更加优秀。缺点也很明显:



  • 同一个业务却需要对两个平台分别进行开发,增加开发成本,业务代码无法在两个平台上复用。

  • 重构时间只有短短的两个月,如果要以原生的方式对 APP 进行整体重构,不现实,人力不足也会增加重构失败的风险。

  • 两个平台的差异性大,无法给不同平台的用户提供一致的使用体验。

  • 原生开发的方式,需要分别开发相应的需求,后续的测试流程,也需要测试人员分别对相应平台进行测试,增加测试成本。


而跨平台方案则完美解决了以上问题,一兜糖 APP 开发团队尝试使用 Flutter 跨平台方案重构 APP。


为什么选择 Flutter


市面上跨平方技术方案除 Flutter 之外,还有多种其他技术,为何最终定型 Flutter 呢?团队主要从以下几点考虑。


节省人力


跨平台方案能够有效减少适配多个平台所带来的工作量,提高业务代码的复用率,原本需要两个平台开发人员一起开发的工作,现在只需要投入一半的人力即可完成需求。同样是跨平台方案,React Native 则需要投入更多的精力用于 UI 兼容等琐碎的事项。


性能上限


相比其他跨平台方案,Flutter 通过 Skia 引擎渲染 UI 视图,这种渲染方式其性能上限非常接近原生。也因为 Flutter 的自绘引擎设计,跨平台 UI 一致性得以保证,因 iOS / Android 系统升级而导致的适配问题更少,可维护性更高,开发人员也可以更专注于业务开发而无需担心平台兼容问题。


声明式 UI


有别于命令式编程,Flutter 使用声明式 UI 设计。使用声明式 UI,开发人员只需要描述当前的 UI 状态,无须关注不同 UI 状态之间的转换,状态转换的相关逻辑则交由 Flutter 框架处理。这无疑提高了开发人员的效率,开发人员可以更加专注于页面的结构。


热重载


在调试模式下,Flutter 支持运行时编译即热重载功能。热重载模块首先扫描工程中发生改动的文件,接着将发生变化的 Dart 代码编译转化为 Dart Kernel 文件,然后将增量的 Dart Kernel 文件发送给 Dart VM,Dart VM 则将接收到的增量文件与原本的 Dart Kernel 文件合并,重新加载合并后的 Dart Kernel 文件,最后在确认 Dart VM 资源加载成功后,重置 UI 线程,完成 Widget 的重建工作。


热重载对于页面的改动,无需重新启动 APP 即可呈现。可以保存页面的视图状态,对于一些在页面栈很深情况,无需为恢复视图栈而重复大量调试步骤,节省调试复杂交互界面的时间。


Flutter 生态


Flutter 开发社区生态活跃,开发中遇到的问题基本都可以在开发社区上找到解决方案。


开工大吉


我们按照以下步骤重构应用



  1. 统一中心思想,做好全员培训工作。

  2. 设计应用架构,以渐进式的方式过渡到 Flutter 工作流。

  3. 逐步重构,有计划地重构。

  4. 拆除旧有代码。


全员培训工作


在重构工作开始时,除 TL 外,团队成员均无 Flutter / Dart 开发经验。


重构工作的关键在于人,我们通过数次重要的会议,告知团队成员目前面临的困境,以及使用  Flutter 重构能为团队带来的收益,并鼓励团队成员尽快学习 Flutter / Dart,让成员感知该技术的便利性。


通过2~3周的学习,团队成员均已上手 Flutter,具备重构条件。


渐进式地过渡到 Flutter


一兜糖家居 APP 已经迭代了五年多,使用 Flutter 重构 APP 的时候,会面临另一个抉择:是以原生为主,Flutter 为辅还是以 Flutter 为主,原生为辅?


市面上大部分 APP 使用 Flutter 的过程中,都是小范围使用 Flutter,绝大部分功能以原生的方式实现,新功能则尝试使用 Flutter 进行开发。这样对 APP 的改动及影响会保持在一个很小的范围,然而这对于开发人员来说却不够友好,调试的时候,需要在原生模块和 Flutter 模块来回切换,比较麻烦;原生项目唤起 Flutter 页面时,也需要大量的资源支持,这同样不利于提升 APP 的性能。因此,一兜糖移动团队决定以 Flutter 为主,原生为辅 的方式重构 APP。


而对于 Android/iOS 这两个已经迭代了五年的原生项目,一下子舍弃原有的代码显然不太现实,我们先创建一个新的 Flutter 项目(ydt_flutter),将原本的两个原生项目(iOS / Android)迁移到新建的 ydt_flutter 项目目录下。


合并仓库 合并构建流程


完成这一步后,即可开始将功能从原生往 Flutter 的方向迁移了。但如果直接在 lib 目录下开发的话,会有以下几个问题:


开发阶段编译项目的时候,会将原生冗余的代码也编译进去,造成第一次编译项目时需要经过漫长的等待。


不利于项目的迁移,原本高度耦合的项目代码会对重构中的 Flutter 项目造成干扰,也导致调试链的增长。


因此,在将两个原生项目迁移到 ydt_flutter 下后,在该目录下又创建了一个 common 模块(纯 Flutter 项目),后续重构的工作全部放在该模块下进行。这样做的好处是可以区分出 Flutter 重构后的项目和原本的原生项目,还可以简化调试工作,降低了两个项目的耦合度。后续相应的业务模块完成后,返回原生项目将相关的业务代码移除即可,一步步的将项目迁移到 Flutter 上。



逐步重构,有计划地重构。


接下来,是开发团队使用 4 个月的时间,逐步以模块为单位,重构应用。\


我们的应用是内容平台应用,在重构的过程中也同时顺带地配合设计组的同事重新整理了 APP 的 UI 风格,统一整理出 ListItemWidget(列表类型的组件) & WaterfallItem(瀑布流类型的组件)。该 APP 最核心的场景是各类列表与内容详情页,我们可以通过 CustomScrollView 的方式,以搭积木的方法,拼接出所有页面。



CustomScrollView + ListItem + WaterfallItem


通过这种方式,配合 GraphQL 下发的数据,我们拼接出了一个完整的内容应用。在这个过程中,Flutter 的一切皆 Widget 的思想,为重构提供了非常大的便利。


拆除旧有代码


在完成重构以后,便是将原有的原生代码删除,直至拆除上面提到的渐进式架构。


收益


重构对开发团队来说是一件高风险高收益的事,重构过程需要对旧业务进行整理,不仅要填上遗留的暗坑,同时还要避免新坑出现。好处也显而易见,可以解决绝大部分原本令人头痛的问题。


借助 Flutter 的诸多优秀特性,四个月时间,一兜糖家居 APP 重构成功。经过将近一年的线上运营,APP 总体使用和用户反馈良好。借助 CustomScrollView + ListItem + Waterfall,有一些小需求甚至无需发版即可将新改动呈现在用户面前。


重构后的 APP 中 Flutter 覆盖场景超过 90%。Dart 语法对比原生开发所使用的开发语言,语法更加简洁,如 await & async 可以有效的避免原生开发中处处可见的回调地狱,代码逻辑更加清晰。这些新特性,帮助开发人员减少 40% 的日常业务开发时间,所需的人力成本也降为原来的一半,开发效率提升 50%。


因为 Flutter 跨平台的优越性,不仅可以缩短开发周期,后续的测试周期也可以相应缩短,只需编写一次 Flutter 的自动化测试用例,而无需分别对 Android/iOS 编写测试用例,测试效率提升的同时降低测试成本。不仅如此,APP 交付给 UI 设计师阶段,也可以大大节省设计师 UI 走查的时间,走查效率提升 50%。


使用 Flutter 开发后,各开发相关的流程所需成本都显著降低,流程也同步简化了不少。接下来,一兜糖移动开发团队将会致力于将 Flutter 覆盖 APP 中 95% 以上的场景。


额外收获


对于 H5 和 微信小程序,一兜糖团队也尝试使用 MPFlutter 进行重构、迁移。


MPFlutter 是一个跨平台的 Flutter 开发框架,致力于推广 Flutter 至官方未能涉足的领域,如微信小程序。


借助 MPFlutter,一兜糖只用了一个月时间,简单修改一些关键的节点代码,便将应用部署到 H5 和微信小程序中。


体验一兜糖应用


如果您对一兜糖的成果感兴趣,不妨现在就通过以下方式体验。



  • 在 AppStore 或各大安卓应用商店,搜索『一兜糖』下载应用。

  • 在微信搜索『一兜糖』体验小程序。

  • 点击 h5.yidoutang.com/v6/ ,体验一兜糖 H5 网站。


如果您正考虑如何装修新房,或者想知道屋主们的生活状态,欢迎持续使用一兜糖 APP。


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

Google 推荐使用 MVI 架构?卷起来了~

前言 前段时间写了一些介绍MVI架构的文章,不过软件开发上没有最好的架构,只有最合适的架构,同时众所周知,Google推荐的是MVVM架构。相信很多人都会有疑问,我为什么不使用官方推荐的MVVM,而要用你说的这个什么MVI架构呢? 不过我这几天查看Androi...
继续阅读 »

前言


前段时间写了一些介绍MVI架构的文章,不过软件开发上没有最好的架构,只有最合适的架构,同时众所周知,Google推荐的是MVVM架构。相信很多人都会有疑问,我为什么不使用官方推荐的MVVM,而要用你说的这个什么MVI架构呢?

不过我这几天查看Android应用架构指南,发现谷歌推荐的最佳实践已经变成了单向数据流动 + 状态集中管理,这不就是MVI架构吗?
看起来Google已经开始推荐使用MVI架构了,大家也有必要开始了解一下Android应用架构指南的最新版本了~


本文主要基于Android应用架构指南,感兴趣的也可以直接查看原文


总体架构


两个架构原则


Android的架构设计原则主要有两个


分离关注点


要遵循的最重要的原则是分离关注点。一种常见的错误是在一个 ActivityFragment 中编写所有代码。这些基于界面的类应仅包含处理界面和操作系统交互的逻辑。
总得来说,ActivityFragment中的代码应该尽量精简,尽量将业务逻辑迁移到其它层


通过数据驱动界面


另一个重要原则是您应该通过数据驱动界面(最好是持久性模型)。数据模型独立于应用中的界面元素和其他组件。

这意味着它们与界面和应用组件的生命周期没有关联,但仍会在操作系统决定从内存中移除应用的进程时被销毁。

数据模型与界面元素,生命周期解耦,因此方便复用,同时便于测试,更加稳定可靠。


推荐的应用架构


基于上一部分提到的常见架构原则,每个应用应至少有两个层:



  • 界面层 - 在屏幕上显示应用数据。

  • 数据层 - 提供所需要的应用数据。


您可以额外添加一个名为“网域层”的架构层,以简化和复用使用界面层与数据层之间的交互

p1.png


如上所示,各层之间的依赖关系是单向依赖的,网域层,数据层不依赖于界面层


界面层


界面的作用是在屏幕上显示应用数据,并响应用户的点击。每当数据发生变化时,无论是因为用户互动(例如按了某个按钮),还是因为外部输入(例如网络响应),界面都应随之更新,以反映这些变化。

不过,从数据层获取的应用数据的格式通常不同于UI需要展示的数据的格式,因此我们需要将数据层数据转化为页面的状态

因此界面层一般分为两部分,即UI层与State HolderState Holder的角色一般由ViewModel承担

p2.png


数据层的作用是存储和管理应用数据,以及提供对应用数据的访问权限,因此界面层必须执行以下步骤:



  1. 获取应用数据,并将其转换为UI可以轻松呈现的UI State

  2. 订阅UI State,当页面状态发生改变时刷新UI

  3. 接收用户的输入事件,并根据相应的事件进行处理,从而刷新UI State

  4. 根据需要重复第 1-3 步。


主要是一个单向数据流动,如下图所示:


p3.png


因此界面层主要需要做以下工作:



  1. 如何定义UI State

  2. 如何使用单向数据流 (UDF),作为提供和管理UI State的方式。

  3. 如何暴露与更新UI State

  4. 如何订阅UI State


如何定义UI State


如果我们要实现一个新闻列表界面,我们该怎么定义UI State呢?我们将界面需要的所有状态都封装在一个data class中。

与之前的MVVM模式的主要区别之一也在这里,即之前通常是一个State对应一个LiveData,而MVI架构则强调对UI State的集中管理


data class NewsUiState(
val isSignedIn: Boolean = false,
val isPremium: Boolean = false,
val newsItems: List<NewsItemUiState> = listOf(),
val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
val title: String,
val body: String,
val bookmarked: Boolean = false,
...
)

以上示例中的UI State定义是不可变的。这样的主要好处是,不可变对象可保证即时提供应用的状态。这样一来,UI便可专注于发挥单一作用:读取UI State并相应地更新其UI元素。因此,切勿直接在UI中修改UI State。违反这个原则会导致同一条信息有多个可信来源,从而导致数据不一致的问题。


例如,如上中来自UI StateNewsItemUiState对象中的bookmarked标记在Activity类中已更新,那么该标记会与数据层展开竞争,从而产生多数据源的问题。


UI State集中管理的优缺点


MVVM中我们通常是多个数据流,即一个State对应一个LiveData,而MVI中则是单个数据流。两者各有什么优缺点?

单个数据流的优点主要在于方便,减少模板代码,添加一个状态只需要给data class添加一个属性即可,可以有效地降低ViewModelView的通信成本

同时UI State集中管理可以轻松地实现类似MediatorLiveData的效果,比如可能只有在用户已登录并且是付费新闻服务订阅者时,您才需要显示书签按钮。您可以按如下方式定义UI State


data class NewsUiState(
val isSignedIn: Boolean = false,
val isPremium: Boolean = false,
val newsItems: List<NewsItemUiState> = listOf()
){
val canBookmarkNews: Boolean get() = isSignedIn && isPremium
}

如上所示,书签的可见性是其它两个属性的派生属性,其它两个属性发生变化时,canBookmarkNews也会自动变化,当我们需要实现书签的可见与隐藏逻辑,只需要订阅canBookmarkNews即可,这样可以轻松实现类似MediatorLiveData的效果,但是远比MediatorLiveData要简单


当然,UI State集中管理也会有一些问题:



  • 不相关的数据类型:UI所需的某些状态可能是完全相互独立的。在此类情况下,将这些不同的状态捆绑在一起的代价可能会超过其优势,尤其是当其中某个状态的更新频率高于其他状态的更新频率时。

  • UiState diffingUiState 对象中的字段越多,数据流就越有可能因为其中一个字段被更新而发出。由于视图没有 diffing 机制来了解连续发出的数据流是否相同,因此每次发出都会导致视图更新。当然,我们可以对 LiveDataFlow使用 distinctUntilChanged() 等方法来实现局部刷新,从而解决这个问题


使用单向数据流管理UI State


上文提到,为了保证UI中不能修改状态,UI State中的元素都是不可变的,那么如何更新UI State呢?

我们一般使用ViewModel作为UI State的容器,因此响应用户输入更新UI State主要分为以下几步:



  1. ViewModel 会存储并公开UI StateUI State是经过ViewModel转换的应用数据。

  2. UI层会向ViewModel发送用户事件通知。

  3. ViewModel会处理用户操作并更新UI State

  4. 更新后的状态将反馈给UI以进行呈现。

  5. 系统会对导致状态更改的所有事件重复上述操作。


举个例子,如果用户需要给新闻列表加个书签,那么就需要将事件传递给ViewModel,然后ViewModel更新UI State(中间可能有数据层的更新),UI层订阅UI State订响应刷新,从而完成页面刷新,如下图所示:


p4.png


为什么使用单向数据流动?


单向数据流动可以实现关注点分离原则,它可以将状态变化来源位置、转换位置以及最终使用位置进行分离。

这种分离可让UI只发挥其名称所表明的作用:通过观察UI State变化来显示页面信息,并将用户输入传递给ViewModel以实现状态刷新。


换句话说,单向数据流动有助于实现以下几点:



  1. 数据一致性。界面只有一个可信来源。

  2. 可测试性。状态来源是独立的,因此可独立于界面进行测试。

  3. 可维护性。状态的更改遵循明确定义的模式,即状态更改是用户事件及其数据拉取来源共同作用的结果。


暴露与更新UI State


定义好UI State并确定如何管理相应状态后,下一步是将提供的状态发送给界面。我们可以使用LiveData或者StateFlowUI State转化为数据流并暴露给UI

为了保证不能在UI中修改状态,我们应该定义一个可变的StateFlow与一个不可变的StateFlow,如下所示:


class NewsViewModel(...) : ViewModel() {

private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

...

}

这样一来,UI层可以订阅状态,而ViewModel也可以修改状态,以需要执行异步操作的情况为例,可以使用viewModelScope启动协程,并且可以在操作完成时更新状态。


class NewsViewModel(
private val repository: NewsRepository,
...
) : ViewModel() {

private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

private var fetchJob: Job? = null

fun fetchArticles(category: String) {
fetchJob?.cancel()
fetchJob = viewModelScope.launch {
try {
val newsItems = repository.newsItemsForCategory(category)
_uiState.update {
it.copy(newsItems = newsItems)
}
} catch (ioe: IOException) {
// Handle the error and notify the notify the UI when appropriate.
_uiState.update {
val messages = getMessagesFromThrowable(ioe)
it.copy(userMessages = messages)
}
}
}
}
}

在上面的示例中,NewsViewModel 类会尝试进行网络请求,然后更新UI State,然后UI层可以对其做出适当反应


订阅UI State


订阅UI State很简单,只需要在UI层观察并刷新UI即可


class NewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}

UI State实现局部刷新


因为MVI架构下实现了UI State的集中管理,因此更新一个属性就会导致UI State的更新,那么在这种情况下怎么实现局部刷新呢?

我们可以利用distinctUntilChanged实现,distinctUntilChanged只有在值发生变化了之后才会回调刷新,相当于对属性做了一个防抖,因此我们可以实现局部刷新,使用方式如下所示


class NewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Bind the visibility of the progressBar to the state
// of isFetchingArticles.
viewModel.uiState
.map { it.isFetchingArticles }
.distinctUntilChanged()
.collect { progressBar.isVisible = it }
}
}
}
}

当然我们也可以对其进行一定的封装,给Flow或者LiveData添加一个扩展函数,令其支持监听属性即可,使用方式如下所示


class MainActivity : AppCompatActivity() {
private fun initViewModel() {
viewModel.viewStates.run {
//监听newsList
observeState(this@MainActivity, MainViewState::newsList) {
newsRvAdapter.submitList(it)
}
//监听网络状态
observeState(this@MainActivity, MainViewState::fetchStatus) {
//..
}
}
}
}

关于MVI架构下支持属性监听,更加详细地内容可见:MVI 架构更佳实践:支持 LiveData 属性监听


网域层


网域层是位于界面层和数据层之间的可选层。


p5.png
网域层负责封装复杂的业务逻辑,或者由多个ViewModel重复使用的简单业务逻辑。此层是可选的,因为并非所有应用都有这类需求。因此,您应仅在需要时使用该层。

网域层具有以下优势:



  1. 避免代码重复。

  2. 改善使用网域层类的类的可读性。

  3. 改善应用的可测试性。

  4. 让您能够划分好职责,从而避免出现大型类。


我感觉对于常见的APP,网域层似乎并没有必要,对于ViewModel重复的逻辑,使用util来说一般就已足够

或许网域层适用于特别大型的项目吧,各位可根据自己的需求选用,关于网域层的详细信息可见:developer.android.com/jetpack/gui…


数据层


数据层主要负责获取与处理数据的逻辑,数据层由多个Repository组成,其中每个Repository可包含零到多个Data Source。您应该为应用处理的每种不同类型的数据创建一个Repository类。例如,您可以为与电影相关的数据创建 MoviesRepository 类,或者为与付款相关的数据创建 PaymentsRepository 类。当然为了方便,针对只有一个数据源的Repository,也可以将数据源的代码也写在Repository,后续有多个数据源时再做拆分


p6.png
数据层跟之前的MVVM架构下的数据层并没用什么区别,这里就不多介绍了,关于数据层的详细信息可见:developer.android.com/jetpack/gui…


总结


相比老版的架构指南,新版主要是增加了网域层并修改了界面层,其中网域层是可选的,各位各根据自己的项目需求使用。

而界面层则从MVVM架构变成了MVI架构,强调了数据的单向数据流动状态的集中管理。相比MVVM架构,MVI架构主要有以下优点



  1. 强调数据单向流动,很容易对状态变化进行跟踪和回溯,在数据一致性,可测试性,可维护性上都有一定优势

  2. 强调对UI State的集中管理,只需要订阅一个ViewState便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

  3. 添加状态只需要添加一个属性,降低了ViewModelView层的通信成本,将业务逻辑集中在ViewModel中,View层只需要订阅状态然后刷新即可


当然在软件开发中没有最好的架构,只有最合适的架构,各位可根据情况选用适合项目的架构,实际上在我看来Google在指南中推荐使用MVI而不再是MVVM,很可能是为了统一AndroidCompose的架构。因为在Compose中并没有双向数据绑定,只有单向数据流动,因此MVI是最适合Compose的架构。


当然如果你的项目中没有使用DataBinding,或许也可以开始尝试一下使用MVI,不使用DataBindingMVVM架构切换为MVI成本不高,切换起来也比较简单,在易用性,数据一致性,可测试性,可维护性等方面都有一定优势,后续也可以无缝切换到Compose


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

使用 Nginx 构建前端日志统计服务

之前的几篇文章都是关于低代码平台的。这个大的项目以 low code 为核心,囊括了编辑器前端、编辑器后端、C 端 H5、组件库、组件平台、后台管理系统前端、后台管理系统后台、统计服务、自研 CLI 九大系统。先放一下整体流程图吧:日志收集在常见的埋点方案中,...
继续阅读 »

背景

之前的几篇文章都是关于低代码平台的。


这个大的项目以 low code 为核心,囊括了编辑器前端、编辑器后端、C 端 H5、组件库、组件平台、后台管理系统前端、后台管理系统后台、统计服务、自研 CLI 九大系统。

今天就来说一下其中的统计服务:目的主要是为了实现 H5 页面的分渠道统计(其实不仅仅是分渠道统计,核心是想做一个自定义事件统计服务,只是目前有分渠道统计的需求),查看每个渠道具体的 PV 情况。(具体会在 url 上面体现,会带上页面名称、id、渠道类型等)

先放一下整体流程图吧:


日志收集

常见的日志收集方式有手动埋点和自动埋点,这里我们不关注于如何收集日志,而是如何将收集的日志的发送到服务器。

在常见的埋点方案中,通过图片来发送埋点请求是一种经常被采纳的,它有很多优势:

  • 没有跨域

  • 体积小

  • 能够完成整个 HTTP 请求+响应(尽管不需要响应内容)

  • 执行过程无阻塞

这里的方案就是在 nginx 上放一张 1px * 1px 的静态图片,然后通过访问该图片(http://xxxx.png?env=xx&event=xxx),并将埋点数据放在query参数上,以此将埋点数据落到nginx日志中。

iOS 上会限制 get 请求的 url 长度,但我们这里真实场景发送的数据不会太多,所以目前暂时采用这种方案

这里简单阐述一下为什么图片地址的query key 要这么设计,如果单纯是为了统计渠道和作品,很有可能会把key设计为channelworkId这种,但上面也说到了,我们是想做一个自定义事件统计服务,那么就要考虑字段的可扩展性,字段应更有通用语义。所以参考了很多统计服务的设计,这里采用的字段为:

  • env

  • event

  • key

  • value

之后每次访问页面,nginx就会自动记录日志到access_log中。

有了日志,下面我们来看下如何来对其进行拆分。

日志拆分

为何要拆分日志

access.log日志默认不会拆分,会越积累越多,系统磁盘的空间会被消耗得越来越多,将来可能面临着日志写入失败、服务异常的问题。

日志文件内容过多,对于后续的问题排查和分析也会变得很困难。

所以日志的拆分是有必要也是必须的。

如何拆分日志

我们这里拆分日志的核心思路是:将当前的access.log复制一份重命名为新的日志文件,之后清空老的日志文件。

视流量情况(流量越大日志文件积累的越快),按天、小时、分钟来拆分。可以把access.log按天拆分到某个文件夹中。

log_by_day/2021-12-19.log
log_by_day/2021-12-20.log
log_by_day/2021-12-21.log

但上面的复制 -> 清空操作肯定是要自动处理的,这里就需要启动定时任务,在每天固定的时间(我这里是在每天凌晨 00:00)来处理。

定时任务

其实定时任务不仅在日志拆分的时候会用到,在后面的日志分析和日志清除都会用到,这里先简单介绍一下,最终会整合拆分、分析和清除。

linux中内置的cron进程就是来处理定时任务的。在node中我们一般会用node-schedulecron来处理定时任务。

这里使用的是cron

/**
   cron 定时规则 https://www.npmjs.com/package/cron
   *    *    *    *    *    *
           
           
            day of week (0 - 6) (Sun-Sat)
          └───── month (1 - 12)
        └────────── day of month (1 - 31)
      └─────────────── hour (0 - 23)
    └──────────────────── minute (0 - 59)
  └───────────────────────── second (0 - 59)
*/

具体使用方式就不展开说明了。

编码

有了上面这些储备,下面我就来写一下这块代码,首先梳理下逻辑:

1️⃣ 读取源文件 access.log

2️⃣ 创建拆分后的文件夹(不存在时需自动创建)

3️⃣ 创建日志文件(天维度,不存在时需自动创建)

4️⃣ 拷贝源日志至新文件

5️⃣ 清空 access.log

/**
* 拆分日志文件
*
* @param {*} accessLogPath
*/
function splitLogFile(accessLogPath) {
 const accessLogFile = path.join(accessLogPath, "access.log");

 const distFolder = path.join(accessLogPath, DIST_FOLDER_NAME);
 fse.ensureDirSync(distFolder);

 const distFile = path.join(distFolder, genYesterdayLogFileName());
 fse.ensureFileSync(distFile);
 fse.outputFileSync(distFile, ""); // 防止重复,先清空

 fse.copySync(accessLogFile, distFile);

 fse.outputFileSync(accessLogFile, "");
}

日志分析

日志分析就是读取上一步拆分好的文件,然后按照一定规则去处理、落库。这里有一个很重要的点要提一下:node在处理大文件或者未知内存文件大小的时候千万不要使用readFile,会突破 V8 内存限制。正是考虑到这种情况,所以这里读取日志文件的方式应该是:createReadStream创建一个可读流交给 readline 逐行读取处理

readline

readline 模块提供了用于从可读流每次一行地读取数据的接口。 可以使用以下方式访问它:

const readline = require("readline");

readline 的使用也非常简单:创建一个接口实例,传入对应的参数:

const readStream = fs.createReadStream(logFile);
const rl = readline.createInterface({
 input: readStream,
});

然后监听对应事件即可:

rl.on("line", (line) => {
 if (!line) return;

 // 获取 url query
 const query = getQueryFromLogLine(line);
 if (_.isEmpty(query)) return;

 // 累加逻辑
 // ...
});
rl.on("close", () => {
 // 逐行读取结束,存入数据库
 const result = eventData.getResult();
 resolve(result);
});

这里用到了lineclose事件:

  • line事件:每当 input 流接收到行尾输入(\n、\r 或 \r\n)时,则会触发 'line' 事件

  • close事件:一般在传输结束时会触发该事件

逐行分析日志结果

了解了readline 的使用,下面让我们来逐行对日志结果进行分析吧。

首先来看下access.log中日志的格式:

我们取其中一行来分析:

127.0.0.1 - - [19/Feb/2021:15:22:06 +0800] "GET /event.png?env=h5&event=pv&key=24&value=2 HTTP/1.1" 200 5233 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36" "-"

我们要拿到的就是urlquery部分,也就是我们在h5中自定义的数据。

通过正则匹配即可:

const reg = /GET\s\/event.png\?(.+?)\s/;
const matchResult = line.match(reg);
console.log("matchResult", matchResult);

const queryStr = matchResult[1];
console.log("queryStr", queryStr);

打印结果为:

queryStr可通过node中的querystring.parse()来处理:

const query = querystring.parse(queryStr);

console.log('query', query)
{
env: 'h5',
event: 'pv',
key: '24',
value: '2'
}

剩下的就是对数据做累加处理了。

但如何去做累加,我们要想一下,最开始也说了是要去做分渠道统计,那么最终的结果应该可以清晰的看到两个数据:

  • 所有渠道的数据

  • 每个渠道单独的数据

只有这样的数据对于运营才是有价值的,数据的好坏也直接决定了后面在每个渠道投放的力度。

这里我参考了 Google Analytics中的多渠道漏斗的概念,由上到下分维度记录每个维度的数据,这样就可以清晰的知道每个渠道的情况了。

具体实现也不麻烦,我们先来看下刚刚从一条链接中得到的有用数据:

{
 env: 'h5',
 event: 'pv',
 key: '24',
 value: '2'
}

这里的env代表环境,这里统计的都是来源于h5页面,所以envh5,但是为了扩展,所以设置了这个字段。

event表示事件名称,这里主要是统计访问量,所以为pv

key是作品 id。

value是渠道 code,目前主要有:1-微信、2-小红书、3-抖音。

再来看下最终统计得到的结果吧:

{
 date: '2021-12-21',
 key: 'h5',
 value: { num: 1276}
}
{
 date: '2021-12-21',
 key: 'h5.pv',
 value: { num: 1000}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12',
 value: { num: 200}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12.1',
 value: { num: 56}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12.2',
 value: { num: 84}
}
{
 date: '2021-12-21',
 key: 'h5.pv.12.3',
 value: { num: 60}
}

这是截取了2021-12-21当天的数据,我给大家分析一波:

1️⃣ h5:当天 h5 页面的自定义事件上报总数为 1276

2️⃣ h5.pv:其中 所有 pv(也就是 h5.pv)为 1000

3️⃣ h5.pv.12:作品 id 为 12 的 pv 一共有 200

4️⃣ h5.pv.12.1:作品 id 为 12 的在微信渠道的 pv 为 56

5️⃣ h5.pv.12.2:作品 id 为 12 的在小红书渠道的 pv 为 84

6️⃣ h5.pv.12.2:作品 id 为 12 的在抖音渠道的 pv 为 60

这样就能清楚的得到某一天某个作品在某条渠道的访问情况了,后续再以这些数据为支撑做成可视化报表,效果就一目了然了。

统计结果入库

目前这部分数据是放在了mongoDB中,关于node中使用mongoDB就不展开说了,不熟悉的可以参考我另外一篇文章Koa2+MongoDB+JWT 实战--Restful API 最佳实践

这里贴下model吧:

/**
* @description event 数据模型
*/
const mongoose = require("../db/mongoose");

const schema = mongoose.Schema(
{
   date: Date,
   key: String,
   value: {
     num: Number,
  },
},
{
   timestamps: true,
}
);

const EventModel = mongoose.model("event_analytics_data", schema);

module.exports = EventModel;

日志删除

随着页面的持续访问,日志文件会快速增加,超过一定时间的日志文件存在的价值也不是很大,所以我们要定期清除日志文件。

这个其实比较简单,遍历文件,因为文件名都是以日期命名的(格式:2021-12-14.log),所以只要判断时间间隔大于 90 天就删除日志文件。

贴一下核心实现:

// 读取日志文件
const fileNames = fse.readdirSync(distFolder);
fileNames.forEach((fileName) => {
 try {
   // fileName 格式 '2021-09-14.log'
   const dateStr = fileName.split(".")[0];
   const d = new Date(dateStr);
   const t = Date.now() - d.getTime();
   if (t / 1000 / 60 / 60 / 24 > 90) {
     // 时间间隔,大于 90 天,则删除日志文件
     const filePath = path.join(distFolder, fileName);
     fse.removeSync(filePath);
  }
} catch (error) {
   console.error(`日志文件格式错误 ${fileName}`, error);
}
});

定时任务整合

到这里,日志的拆分、分析和清除都说完了,现在要用cron来对他们做整合了。

首先来创建定时任务:

function schedule(cronTime, onTick) {
 if (!cronTime) return;
 if (typeof onTick !== "function") return;

 // 创建定时任务
 const c = new CronJob(
   cronTime,
   onTick,
   null, // onComplete 何时停止任务
   true, // 初始化之后立刻执行
   "Asia/Shanghai" // 时区
);

 // 进程结束时,停止定时任务
 process.on("exit", () => c.stop());
}

然后每一阶段都在不同的时间阶段去处理(定时拆分 -> 定时分析 -> 定时删除)

定时拆分

function splitLogFileTiming() {
 const cronTime = "0 0 0 * * *"; // 每天的 00:00:00
 schedule(cronTime, () => splitLogFile(accessLogPath));
 console.log("定时拆分日志文件", cronTime);
}

定时分析并入库

function analysisLogsTiming() {
 const cronTime = "0 0 3 * * *"; // 每天的 3:00:00 ,此时凌晨,访问量较少,服务器资源处于闲置状态
 schedule(cronTime, () => analysisLogsAndWriteDB(accessLogPath));
 console.log("定时分析日志并入库", cronTime);
}

定时删除

function rmLogsTiming() {
 const cronTime = "0 0 4 * * *"; // 每天的 4:00:00 ,此时凌晨,访问量较少,服务器资源处于闲置状态
 schedule(cronTime, () => rmLogs(accessLogPath));
 console.log("定时删除过期日志文件", cronTime);
}

然后在应用入口按序调用即可:

// 定时拆分日志文件
splitLogFileTiming();
// 定时分析日志并入库
analysisLogsTiming();
// 定时删除过期日志文件
rmLogsTiming();

总结

ok,到这里,一个简易的统计服务就完成了。

作者:前端森林
来源:https://segmentfault.com/a/1190000041184489

收起阅读 »

淘特 Flutter 流畅度优化实践

不同的业务背景引出不同的技术诉求,“用户体验特爽”是淘特的不懈追求,本文将介绍笔者加入淘特以来在Flutter流畅度方面的诸多优化实践,这些优化不涉及Engine改造、不涉及高大上的“轮子建设“,只需细心细致深入业务抽丝剥茧,坚持实际体感导向,即能为用户体验带...
继续阅读 »

不同的业务背景引出不同的技术诉求,“用户体验特爽”是淘特的不懈追求,本文将介绍笔者加入淘特以来在Flutter流畅度方面的诸多优化实践,这些优化不涉及Engine改造、不涉及高大上的“轮子建设“,只需细心细致深入业务抽丝剥茧,坚持实际体感导向,即能为用户体验带来显著提升,值得Flutter开发者将其应用在产品的每一个像素。

背景

淘特具备鲜明的三大特征:

  1. 业务特征:淘特拥有业界最复杂的淘系电商链路

  2. 用户特征:淘特用户中有大量的中老年用户,大量的用户手机系统版本较低,大量的用户使用中低端机

  3. 技术特征:淘特大规模采用Flutter跨平台渲染技术

综上所述:

最复杂业务链路+最低性能用户群体+最新的跨平台技术==>核心问题之一:页面流畅度受到严峻挑战

Flutter核心链路20S快速滚动帧率卡顿率(每秒卡顿率)
直播Tab277.04%
我的41.36667.63%
详情26.715.58%

注:相关数据以vivo Y67,淘特

3.32.999.10 (103) 测得

目标

流畅度是用户体验的关键一环,大家都不希望手机用起来像看电影/刷PPT,尤其是现在高刷屏(90/120hz)的普及,更是极大强化了用户对流畅度的感知,但流畅度也跟产品复杂度强相关,也是一次繁与简的取舍,淘特流畅度一期优化目标:

Flutter核心链路页面达到高流畅度(平均帧率:低端机45FPS、中端机50FPS、高端50FPS)

一期优化后的状态

事项平均帧率卡顿率提升效果
1.直播Tab推荐、分类栏目46.00.35%帧率提高19帧、卡顿率降低6.7%
2.我的页面46.00%帧率提高4.6帧,卡顿率降低7.6%
3.详情45.02%帧率提高18.3桢,卡顿率降低13.58%

旧版3.32如视频左,新版3.37如视频右。因uiautomator工具会触发无障碍性能ISSUE,此版本对比为人工测试。

视频请见:淘特 Flutter 流畅度优化实践

除了数据上的明显提升,体感上,旧版快滑卡顿明显,画面突变明显,新版则基本消除明显的卡顿,画面连续平稳。

问题

回到技术本身,Flutter为什么会卡顿、帧率低?总的来说均为以下2个原因:

  1. UI线程慢了-->渲染指令出的慢

  2. GPU线程慢了-->光栅化慢、图层合成慢、像素上屏慢

那么,怎么解上述的 2 个问题是咱们所关心的重点。既然知道某块有问题,我们自然要有工具系统化的度量问题水平,以及系统化的理论支撑实践,且看以下2节,过程中穿插相关策略在淘特的实践, 理论与实践结合理解更透。

怎么解

解法 - 案例

降低setState的触发节点

大家都知道Flutter的刷新机制,在越高的Widget树层级触发setState标脏Element,“脏树越大”,在越低层级越局部的Widget触发状态更新,“脏树越小”,被标记为脏树后将触发Element.Rebuild,遍历组件树。原理请看下图“Flutter页面刷新机制源码解析”:




“Element.updateChild源码分析”请见下文优化二。

实际应用淘特为例。直播Tab的视频预览功能为例,最初直播Tab的视频播放index通过状态层层传递给子组件,一旦状态变更,顶层setState触发播放index更新, 造成整个页面刷新。但实际整个页面需要更新状态的只有“需要暂停的原VideoWidget”和“待播放的VideoWidget”, 我们改为监听机制,页面中的所有VideoWidget注册监听,顶层用EventBus统一分发播放index至各VideoWidget,局部Widget Check后改变自身状态。

再比如详情页,由于使用了“上一个页面借图”的功能,监听到滚动后隐藏借的图,但setState的调用节点放在了详情顶层Widget,造成了全局刷新。实际该监听刷新逻辑可下放至“借图组件”,降低“脏树”的大小。



缓存不变的Widget

缓存不变的Widget有2大好处。1.被缓存的Widget将无需重复创建, 虽然Flutter官方认为Widget是一种非常轻量级的对象,在实际业务中,Build耗时过高仍是一种常见现象。2.返回相同引用的Widget将使Flutter停止该子树后续遍历, 即Flutter认为该子树无变化无需更新。原理请看下图“Element.updateChild源码分析”


应用场景以淘特实际页面为例。详情页部分组件使用了DXWidget,理论上组件内容一经创建后当次页面生命周期不会再有变化,此种情况即可缓存不变的Widget,避免重复动态渲染DX,停止子树遍历。

Feed流的Item组件,布局复杂,创建成本较高,理论上创建一次后内容也不会再变化,但item可能被删除,此时应该用Objectkey唯一标识组件,防止状态错位。



减少不必要的build(setState)

直播Tab用到一个埋点曝光组件,经过DevTools检查,发现其在每一次进度回调中重新创建itemWidget,虽然这不会造成业务异常,但理论上itemWidget只需被创建一次,这块经排查是使用组件时误传了builder函数,而不是直接传itemWidget实例。

详情页的逻辑非常复杂,AppBar根据滚动距离实时计算透明度,这会导致高频的setState,实际上透明度变化前后应该满足一个差值后才应刷新一次状态, 为了性能考量,透明度应该只有少数几种值变更。



多变图层与不变图层分离

在日常开发中,会经常遇到页面中大部分元素不变,某个元素实时变化。如Gif,动画。这时我们就需要RepaintBoundary,不过独立图层合成也是有消耗,这块需实测把握。以淘特为例。

直播Feed中的Gif图是不断高频跳动,这会导致页面同一图层重新Paint。此时可以用RepaintBoundary包裹该多变的Gif组件,让其处在单独的图层,待最终再一块图层合成上屏。

同理, 秒杀倒计时也是电商常见场景, 该组件也适用于RepaintBoundary场景。



避免频繁的triggerGC

因为AliFlutter的关系,我们得以主动触发DartGC,但GC同样也是有消耗的,高频的GC更是如此。淘特之前因为iOS的内存压力,在列表滚动停止时ScrollEndNotification则会触发GC,ScrollEndNotification在每一次手Down->up事件后都会触发一次,如果用户多次触摸,则会较为频繁的触发GC,实测影响Y67 4帧左右的性能,这块增加页面不可见时GC 和在Y67等android低端机关闭滑动GC,提高滑动性能。


大JSON解析子线程化

Flutter的isolate默认是单线程模型,而所有的UI操作又都是在UI线程进行的,想应用多线程的并发优势需新开isolate 或compute。无论如何await,scheduleTask 都只是延后任务的调用时机,仍然会占用“UI线程”, 所以在大Json解析或大量的channel调用时,一定要观测对UI线程的消耗情况。在淘特中,我们在低端机开启json解析compute化,不阻塞UI线程。

尽量减少或降级Clip、Opacity等组件的使用

Flutter中,Clip主要用于裁剪,裁矩形、圆角矩形、圆形。一旦调用,后续所有的绘图指令都会受其Clip影响。有些ClipRRect可以用ShapeDecoration代替,Opacitiy改用AnimatedOpacity, 针对图片的Clip裁切,可以走定制图片库Transform实现。


降级CustomScrollView预渲染区域为合理值

默认情况下,CustomScrollView除了渲染屏幕内的内容,还会渲染上下各250区域的组件内容,即如双列瀑布流,当前屏幕可显示4个组件,实际仍有上下共4个组件在显示状态,如果setState(加载更多时),则会进行8个组件重绘。实际用户只看到4个,其实应该也只需渲染4个, 且上下滑动也会触发屏幕外的Widget创建销毁,造成滚动卡顿。高性能的手机可预渲染,淘特在低端机降级该区域距离为0或较小值。


高频埋点 Channel批量化操作

在组件曝光时上报埋点是很常见的行为,但在快速滚动的场景下, 瞬间10+ item的略过,20+ channel的调用同样会占用一定的UI线程资源和Native UI线程资源。这里淘特针对部分场景做了批量、定时上传, 维护一个埋点队列,默认定时3S或50条,业务不可见时上报,合并20+channel调用为单次。业务也可在合适时机点强制flush队列上报, 同时在Native侧,将埋点行为切换至子线程进行。

其他有效优化措施

部分业务特效,业务繁忙度在低端机上都是可以适度降级的,如淘特将Feed视频预览播放延迟时间从500ms降为1.5S,Feed流预加载阈值距离从2000+降为500,图片圆角降直角等降级措施的核心思路都是先保证最低端的用户也能用的顺畅,再美化细节锦上添花。

Flutter在无障碍开启情况下,快速滚动场景存在性能问题,如确定业务无需无障碍或用户误触发无障碍,可添加ExcludeSemantics Widget屏蔽无障碍。

通过DevTools检测,发现high_available高可用帧率检测在老版本存在性能问题,这块可升级插件版本或低端机屏蔽该检测。

解法 - 优化案例总结

上述十条优化实践,抛开细节看原理,大致分为以下几类, 融会贯通,实践出真知。

如何提高UI线程性能:

  • 如何提高build性能

    • 降低遍历出发点,降低setState的触发节

    • 停止树的遍历,不变的内容,返回同样的组件实例、Flutter将停止遍历该树(SlideTransition)

    • 减少非必要的build(setState)

  • 如何提高layout性能

    • layout暂时不太容易出问题

  • 如何提高paint性能

    • RepaintBoundary分离多变和不变的图层,如Gif、动画, 但多图层的合成也是有开销的

  • 其他

    • 耗时方法如大JSON解析用compute子线程化

    • 减少不必要的channel调用或批量合并

    • 减少动画

    • 减少Release时的log

    • 提高UI线程在Android/iOS的优先级

    • 列表组件支持局部build

    • 较小的cacheExtent值,减少渲染范围

如何提高GPU线程性能:

  1. 谨慎saveLayer

  2. 尽量少ClipPath、一旦调用,后续所有绘图指令需与Path做相交。(ClipRect、ClipRRect等)

  3. 减少毛玻璃BackdropFilter、阴影boxShadow

  4. 减少Opacity使用,必要时用AnimatedOpacity

解法 - 测量工具

工欲善其事,必先利其器。工具主要分为以下两块。

  1. 流畅度检测:无需侵入代码的流畅度检测方案有几种, 既可以通过adb取surfaceflinger数据, 也可以基于VirtualDisplay做图像对比,或者使用官方DevTools。第三方比较成熟的如PerfDog

  2. 卡顿排查:DevTools是官方的开发配套工具,非常实用

    1. Performance检测单帧CPU耗时(build、layout、paint)、GPU耗时、Widget Build次数

    2. CPUProfiler 检测方法耗时

    3. Flutter Inspector观察不合理布局

    4. Memory 监控Dart内存情况

DevTools

Flutter分为三种编译模式,Debug/Release大家都很熟悉,Debug最大特性为HotReload可调试,Release为最高性能,Profile模式则取其中间,专用于性能分析,其产物以AOT模式无限接近Release性能运行,又保留了丰富的性能分析途径。

如何以Profile模式运行flutter?

如果是混合工程,android为例,在app/build.gradle添加profile{init with debug}即可, 部分应用资源区分debug/profile,也可Copy一份profile。当然,更hack更彻底的方式,可直接修改$flutterRoot/packages/flutter_tools/gradle/flutter.gradle文件中buildModeFor方法,默认返回想要的Profile/Release模式。

如何在Profile模式下打开DevTools?

推荐使用IDE的flutter attach 或者 命令行采用flutter pub global run devtools,填入observatory的地址,即可开始使用DevTools。

Flutter Performance&Inspector

以AS为例,右侧会出现Flutter Performance和Inspector2个功能区。Performance功能区如下图:


Overlay效果如下图。可以看到有2排柱状图,上方为GPU帧耗时,下方为CPU耗时,实时显示最近300帧情况,当当前帧耗时超过16ms时,绿色扫描线会变红色, 此图常用于观察动态过程中的“瞬时卡顿点”。


Inspector较为简单,可观看Widget树结构和实际的Render Tree结构,包含基本的布局信息,DevTools中Inspector包含更详细信息。



DevTools&Flutter Inspector


DevTools&Performance



Performance功能是性能优化的核心工具,这里可以分析出大部分UI线程、GPU线程卡顿的原因。为方便分析,此图用Debug模式得来,实际性能分析以Profile模式为准。

如上图1所示,Build函数耗时明显过长,且连续数十帧如此,必然是Build的逻辑有严重问题。理论上Widget创建一次后状态未改变时无需重建。由前文淘特案例可以发现,这里实际是业务错误的在滚动进度回调中重复创建Widget所致。实际的Build应只在瀑布流Layout逻辑中创建执行2次。

Paint函数详情可在debug模式通过debugProfilePaintsEnabled=true开启。当多变的元素与不变的元素混在同一图层时可造成图层整体的过度重复绘制, 如元素内容无变化,Paint函数中也不应出现多余元素的绘制耗时。通过前面提及的Repain RainBow开关或debugRepaintRainbowEnabled=true, 可实时观察重绘情况,如下图所示。

每一个图层都有对应的不同颜色框体。只有发生Repaint的图层颜色会发生变化,多余的图层变色,我们就要排查是否正常。


GPU耗时过多一般源于重量级组件的过度使用如Clip、Opacity、阴影, 这块发现耗时过多可参考前文解法进行优化或降级, 关于GPU更多的优化可参考liyuqian的高性能图形引擎分享。

在图1最下方的CPU Profile即代表当帧的CPU耗时情况,BottomUp方便查找最耗时的方法。

DevTools&CPU Profiler


在Performance的隔壁是CPU Profiler,这里用于统计一段时间内CPU的耗时情况,一般根据方法名结合经验判断是业务异常还是正常耗时,根据visitChilddren-->getScrollRenderObject方法名搜索,发现高可用帧率监控存在性能问题。

Devtools还有内存、Debugger、网络、日志等功能模块,这块流畅度优化中使用不多,后续有更好的经验再和大家分享。

DebugFlags&Build


上图是一张针对build阶段常见的debug功能表, debugPrintRebuildDirtyWidgets开关将在控制台打印什么树当前正在被重建,debugProfileBuildsEnabled作用同Performance的Track Widget Builds,监控Build函数详情。前3个字段在debug模式使用,最后一个可在Profile模式使用。

DebugFlag&Paint

上图是一张针对Paint阶段常见的debug功能表。debugDumpLayerTree()函数可用于打印layer树,debugPaintLayerBordersEnabled可在每一个图层周围形成边界(框),debugRepaintRainbowEnabled作用同Inspector中的RainBow Enable, 图层重绘时边框颜色将变化。debugProfilePaintsEnabled前文已提到,方便分析paint函数详情。


展望

以上便是淘特Flutter流畅度优化第一期实践,也是体感优化最明显的的一期优化。但距离极致的用户体验目标仍有不小的差距。集团同学提供了很多秀实践学习。如UC Hummer的Engine流畅度优化, 闲鱼的局部刷新复用列表组件PowerScrollView、线上线下的高精准多维度检测卡顿,及如何防止流畅度优化不恶化的方案, 淘特也在不断学习成长挑战极限,在二期实践中,为了最极致的体验,淘特将结合Hummer引擎,深度优化高性能图片库、高性能流式容器、建立全面的线下线上数据监控体系,做一个”让用户爽的淘特App“。

参考资料

收起阅读 »

偷偷看了同事的代码找到了优雅代码的秘密

真正的大师永远怀着一颗学徒的心引言对于一个软件平台来说,软件平台代码的好坏直接影响平台整体的质量与稳定性。同时也会影响着写代码同学的创作激情。想象一下如果你从git上面clone下来的的工程代码乱七八糟,代码晦涩难懂,难以快速入手,有种想推到重写的冲动,那么程...
继续阅读 »

真正的大师永远怀着一颗学徒的心

引言

对于一个软件平台来说,软件平台代码的好坏直接影响平台整体的质量与稳定性。同时也会影响着写代码同学的创作激情。想象一下如果你从git上面clone下来的的工程代码乱七八糟,代码晦涩难懂,难以快速入手,有种想推到重写的冲动,那么程序猿在这个工程中写好代码的初始热情都没了。相反,如果clone下的代码结构清晰,代码优雅易懂,那么你在写代码的时候都不好意思写烂代码。这其中的差别相信工作过的同学都深有体会,那么我们看了那么多代码之后,到底什么样的代码才是好代码呢?它们有没有一些共同的特征或者原则?本文通过阐述优雅代码的设计原则来和大家聊聊怎么写好代码。

代码设计原则

好代码是设计出来的,也是重构出来的,更是不断迭代出来的。在我们接到需求,经过概要设计过后就要着手进行编码了。但是在实际编码之前,我们还需要进行领域分层设计以及代码结构设计。那么怎么样才能设计出来比较优雅的代码结构呢?有一些大神们总结出来的优雅代码的设计原则,我们分别来看下。

SRP

所谓SRP(Single Responsibility Principle)原则就是职责单一原则,从字面意思上面好像很好理解,一看就知道什么意思。但是看的会不一定就代表我们就会用,有的时候我们以为我们自己会了,但是在实际应用的时候又会遇到这样或者那样的问题。原因就是实际我们没有把问题想透,没有进行深度思考,知识还只是知识,并没有转化为我们的能力。就比如这里所说的职责单一原则指的是谁的单一职责,是类还是模块还是域呢?域可能包含多个模块,模块也可以包含多个类,这些都是问题。

为了方便进行说明,这里以类来进行职责单一设计原则的说明。对于一个类来说,如果它只负责完成一个职责或者功能,那么我们可以说这个类时符合单一职责原则。请大家回想一下,其实我们在实际的编码过程中,已经有意无意的在使用单一职责设计原则了。因为实际它是符合我们人思考问题的方式的。为什么这么说呢?想想我们在整理衣柜的时候,为了方便拿衣服我们会把夏天的衣服放在一个柜子中,冬天的衣服放在一个柜子。这样季节变化的时候,我们只要到对应的柜子直接拿衣服就可以了。否则如果冬天和夏天的衣服都放在一个柜子中,我们找衣服的时候可就费劲了。放到软件代码设计中,我们也需要采用这样的分类思维。在进行类设计的时候,要设计粒度小、功能单一的类,而不是大而全的类。

举个栗子,在学生管理系统中,如果一个类中既有学生信息的操作比如创建或者删除动作,又有关于课程的创建以及修改动作,那么我们可以认为这个类时不满足单一职责的设计原则的,因为它将两个不同业务域的业务混杂在了一起,所以我们需要进行拆分,将这个大而全的类拆分为学生以及课程两个业务域,这样粒度更细,更加内聚。

笔者根据自身的经验,总结了需要考虑进行单一职责拆分的几个场,希望对大家判断是否需要进行拆分有个简单的判断的标准: 1、不同的业务域需要进行拆分,就像上面的例子,另外如果与其他类的依赖过多,也需要考虑是不是应该进行拆分; 2、如果我们在类中编写代码的时候发现私有方法具有一定的通用性,比如判断ip是不是合法,解析xml等,那我们可以考虑将这些方法抽出来形成公共的工具类,这样其他类也可以方便的进行使用。 另外单一职责的设计思想不止在代码设计中使用,我们在进行微服务拆分的时候也会一定程度的遵循这个原则。

OCP

OCP(Open Closed Principle)即对修改关闭,对扩展开放原则,个人觉得这是设计原则中最难的原则。不仅理解起来有一定的门槛,在实际编码过程中也是不容易做到的。 首先我们得先搞清楚这里的所说的修改以及扩展的区别在什么地方,说实话一开始看到这个原则的时候,我总觉得修改和开放说的不是一个意思嘛?想来想去都觉得有点迷糊。后来在不断的项目实践中,对这个设计原则的理解逐渐加深了。

设计原则中所说的修改指的是对原有代码的修改,而扩展指的是在原有代码基础上的能力的扩展,并不修改原先已有的代码。这是修改与扩展的最大的区别,一个需要修改原来的代码逻辑,另一个不修改。因此才叫对修改关闭但是对扩展开放。弄清楚修改和扩展的区别之后,我们再想一想为什么要对修改关闭,而要对扩展开放呢? 我们都知道软件平台都是不断进行更新迭代的,因此我们需要不断在原先的代码中进行开发。那么就会涉及到一个问题如果我们的代码设计的不好,扩展性不强,那么每次进行功能迭代的时候都会修改原先已有的代码,有修改就有可能引入bug,造成系统平台的不稳定。因此我们为了平台的稳定性,需要对修改关闭。但是我们要添加新的功能怎么办呢?那就是通过扩展的方式来进行,因此需要实现对扩展开放。

这里我们以一个例子来进行说明,否则可能还是有点抽象。在一个监控平台中,我们需要对服务所占用CPU、内存等运行信息进行监控,第一版代码如下。

public class Alarm {
private AlarmRule alarmRule;
   private AlarmNotify alarmNotify;
   
   public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {
       this.alarmRule = alarmRule;
       this.alarmNotify = alarmNotify;
  }
   
   public void checkServiceStatus(String serviecName, int cpu, int memory) {
       if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
       
        if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
   
  }

}

代码逻辑很简单,就是根据对应的告警规则中的阈值判断是否达到触发告警通知的条件。如果此时来了个需求,需要增加判断的条件,就是根据服务对应的状态,判断需不需要进行告警通知。我们来先看下比较low的修改方法。我们在checkServiceStatus方法中增加了服务状态的参数,同事在方法中增加了判断状态的逻辑。

public class Alarm {
private AlarmRule alarmRule;
   private AlarmNotify alarmNotify;
   
   public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {
       this.alarmRule = alarmRule;
       this.alarmNotify = alarmNotify;
  }
   
   public void checkServiceStatus(String serviecName, int cpu, int memory, int status) {
       if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
       
        if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
       
        if(status == alarmRule.getRule(ServiceConstant.Status).getStatusThreshold) {
           alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
      }
   
  }

}

很显然这种修改方法非常的不友好,为什么这么说呢?首先修改了方法参数,那么调用该方法的地方可能也需要修改,另外如果改方法有单元测试方法的话,单元测试用例必定也需要修改,在原有测试过的代码中添加新的逻辑,也增加了bug引入的风险。因此这种修改的方式我们需要进行避免。那么怎么修改才能够体现对修改关闭以及对扩展开放呢? 首先我们可以先将关于服务状态的属性抽象为一个ServiceStatus 实体,在对应的检查方法中以ServiceStatus 为入参,这样以后如果还有服务状态的属性增加的话,只需要在ServiceStatus 中添加即可,并不需要修改方法中的参数以及调用方法的地方,同样单元测试的方法也不用修改。

@Data
public class ServiceStatus {
   String serviecName;
   int cpu;
   int memory;
   int status;

}

另外在检测方法中,我们怎么修改才能体现可扩展呢?而不是在检测方法中添加处理逻辑。一个比较好的实现方式就是通过抽象检测方法,具体的实现在各个实现类中。这样即使新增检测逻辑,只需要扩展检测实现方法就可,不需要在修改原先代码的逻辑,实现代码的可扩展。

LSP

LSP(Liskov Substitution Principle)里氏替换原则,这个设计原则我觉得相较于前面的两个设计原则来说要简单些。它的内容为子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

我们怎么判断有没有违背LSP呢?我觉得有两个关键点可以作为判断的依据,一个是子类有没有改变父类申明需要实现的业务功能,另一个是否违反父类关于输入、输出以及异常抛出的规定。

ISP

ISP(Interface Segregation Principle)接口隔离原则,简单理解就是只给调用方需要的接口,它不需要的就不要硬塞给他了。这里我们举个栗子,以下是关于产品的接口,其中包含了创建产品、删除产品、根据ID获取产品以及更新产品的接口。如果此时我们需要对外提供一个根据产品的类别获取产品的接口,我们应该怎么办?很多同学会说,这还不简单,我们直接在这个接口里面添加根据类别查询产品的接口就OK了啊。大家想想这个方案有没有什么问题。

public interface ProductService { 
   boolean createProduct(Product product);
   boolean deleteProductById(long id);
   Product getProductById(long id);
   int updateProductInfo(Product product);
}

public class UserServiceImpl implements UserService { //...}

这个方案看上去没什么问题,但是再往深处想一想,外部系统只需要一个根据产品类别查询商品的功能,,但是实际我们提供的接口中还包含了删除、更新商品的接口。如果这些接口被其他系统误调了可能会导致产品信息的删除或者误更新。因此我们可以将这些第三方调用的接口都隔离出来,这样就不存在误调用以及接口能力被无序扩散的情况了。

public interface ProductService { 
   boolean createProduct(Product product);
   boolean deleteProductById(long id);
   Product getProductById(long id);
   int updateProductInfo(Product product);
}

public interface ThirdSystemProductService{
   List<Product> getProductByType(int type);
}

public class UserServiceImpl implements UserService { //...}

LOD

LOD(Law of Demeter)即迪米特法则,这是我们要介绍的最后一个代码设计法则了,光从名字上面上看,有点不明觉厉的感觉,看不出来到底到底表达个什么意思。我们可以来看下原文是怎么描述这个设计原则的。 Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers. 按照我自己的理解,这迪米特设计原则的最核心思想或者说最想达到的目的就是尽最大能力减小代码修改带来的对原有的系统的影响。所以需要实现类、模块或者服务能够实现高内聚、低耦合。不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。打个比方这就像抗战时期的的地下组织一样,相关联的聚合到一起,但是与外部保持尽可能少的联系,也就是低耦合。

总结

本文总结了软件代码设计中的五大原则,按照我自己的理解,这五大原则就是程序猿代码设计的内功,而二十三种设计模式实际就是内功催生出来的编程招式,因此深入理解五大设计原则是我们用好设计模式的基础,也是我们在平时设计代码结构的时候需要遵循的一些常见规范。只有不断的在设计代码-》遵循规范-》编写代码-》重构这个循环中磨砺,我们才能编写出优雅的代码。

作者:慕枫技术笔记
来源:https://juejin.cn/post/7046404022143549447

收起阅读 »

二维码扫码登录是什么原理

前几天看了极客时间一个二维码的视频,写的不错,这里总结下在日常生活中,二维码出现在很多场景,比如超市支付、系统登录、应用下载等等。了解二维码的原理,可以为技术人员在技术选型时提供新的思路。对于非技术人员呢,除了解惑,还可以引导他更好地辨别生活中遇到的各种二维码...
继续阅读 »

前几天看了极客时间一个二维码的视频,写的不错,这里总结下

在日常生活中,二维码出现在很多场景,比如超市支付、系统登录、应用下载等等。了解二维码的原理,可以为技术人员在技术选型时提供新的思路。对于非技术人员呢,除了解惑,还可以引导他更好地辨别生活中遇到的各种二维码,防止上当受骗。

二维码,大家再熟悉不过了

购物扫个码,吃饭扫个码,坐公交也扫个码

在扫码的过程中,大家可能会有疑问:这二维码安全吗?会不会泄漏我的个人信息?更深度的用户还会考虑:我的系统是不是也可以搞一个二维码来推广呢?

这时候就需要了解一下二维码背后的技术和逻辑了!

二维码最常用的场景之一就是通过手机端应用扫描PC或者WEB端的二维码,来登录同一个系统。 比如手机微信扫码登录PC端微信,手机淘宝扫码登录PC端淘宝。 那么就让我们来看一下,二维码登录是怎么操作的!

二维码登录的本质

二维码登录本质上也是一种登录认证方式。既然是登录认证,要做的也就两件事情!

  1. 告诉系统我是谁

  2. 向系统证明我是谁

比如账号密码登录,账号就是告诉系统我是谁, 密码就是向系统证明我是谁; 比如手机验证码登录,手机号就是告诉系统我是谁,验证码就是向系统证明我是谁;

那么扫码登录是怎么做到这两件事情的呢?我们一起来考虑一下

手机端应用扫PC端二维码,手机端确认后,账号就在PC端登录成功了!这里,PC端登录的账号肯定与手机端是同一个账号。不可能手机端登录的是账号A,而扫码登录以后,PC端登录的是账号B。

所以,第一件事情,告诉系统我是谁,是比较清楚的!

通过扫描二维码,把手机端的账号信息传递到PC端,至于是怎么传的,我们后面再说

第二件事情,向系统证明我是谁。扫码登录过程中,用户并没有去输入密码,也没有输入验证码,或者其他什么码。那是怎么证明的呢?

有些同学会想到,是不是扫码过程中,把密码传到了PC端呢? 但这是不可能的。因为那样太不安全的,客户端也根本不会去存储密码。我们仔细想一下,其实手机端APP它是已经登录过的,就是说手机端是已经通过登录认证。所说只要扫码确认是这个手机且是这个账号操作的,其实就能间接证明我谁。

认识二维码

那么如何做确认呢?我们后面会详细说明,在这之前我们需要先认识一下二维码! 在认识二维码之前我们先看一下一维码!

所谓一维码,也就是条形码,超市里的条形码--这个相信大家都非常熟悉,条形码实际上就是一串数字,它上面存储了商品的序列号。

二维码其实与条形码类似,只不过它存储的不一定是数字,还可以是任何的字符串,你可以认为,它就是字符串的另外一种表现形式,

在搜索引擎中搜索二维码,你可以找到很多在线生成二维码的工具网站,这些网站可以提供字符串与二维码之间相互转换的功能,比如 草料二维码网站

在左边的输入框就可以输入你的内容,它可以是文本、网址,文件........。然后就可以生成代表它们的二维码

你也可以把二维码上传,进行”解码“,然后就可以解析出二维码代表的含义

系统认证机制

认识了二维码,我们了解一下移动互联网下的系统认证机制。

前面我们说过,为了安全,手机端它是不会存储你的登录密码的。 但是在日常使用过程中,我们应该会注意到,只有在你的应用下载下来后,第一次登录的时候,才需要进行一个账号密码的登录, 那之后呢 即使这个应用进程被杀掉,或者手机重启,都是不需要再次输入账号密码的,它可以自动登录。

其实这背后就是一套基于token的认证机制,我们来看一下这套机制是怎么运行的,

  1. 账号密码登录时,客户端会将设备信息一起传递给服务端,

  2. 如果账号密码校验通过,服务端会把账号与设备进行一个绑定,存在一个数据结构中,这个数据结构中包含了账号ID,设备ID,设备类型等等

const token = {
 acountid:'账号ID',
 deviceid:'登录的设备ID',
 deviceType:'设备类型,如 iso,android,pc......',
}

然后服务端会生成一个token,用它来映射数据结构,这个token其实就是一串有着特殊意义的字符串,它的意义就在于,通过它可以找到对应的账号与设备信息,

  1. 客户端得到这个token后,需要进行一个本地保存,每次访问系统API都携带上token与设备信息。

  2. 服务端就可以通过token找到与它绑定的账号与设备信息,然后把绑定的设备信息与客户端每次传来的设备信息进行比较, 如果相同,那么校验通过,返回AP接口响应数据, 如果不同,那就是校验不通过拒绝访问

从前面这个流程,我们可以看到,客户端不会也没必要保存你的密码,相反,它是保存了token。可能有些同学会想,这个token这么重要,万一被别人知道了怎么办。实际上,知道了也没有影响, 因为设备信息是唯一的,只要你的设备信息别人不知道, 别人拿其他设备来访问,验证也是不通过的。

可以说,客户端登录的目的,就是获得属于自己的token。

那么在扫码登录过程中,PC端是怎么获得属于自己的token呢?不可能手机端直接把自己的token给PC端用!token只能属于某个客户端私有,其他人或者是其他客户端是用不了的。在分析这个问题之前,我们有必要先梳理一下,扫描二维码登录的一般步骤是什么样的。这可以帮助我们梳理清楚整个过程,

扫描二维码登录的一般步骤

大概流程

  1. 扫码前,手机端应用是已登录状态,PC端显示一个二维码,等待扫描

  2. 手机端打开应用,扫描PC端的二维码,扫描后,会提示"已扫描,请在手机端点击确认"

  3. 用户在手机端点击确认,确认后PC端登录就成功了

可以看到,二维码在中间有三个状态, 待扫描,已扫描待确认,已确认。 那么可以想象

  1. 二维码的背后它一定存在一个唯一性的ID,当二维码生成时,这个ID也一起生成,并且绑定了PC端的设备信息

  2. 手机去扫描这个二维码

  3. 二维码切换为 已扫描待确认状态, 此时就会将账号信息与这个ID绑定

  4. 当手机端确认登录时,它就会生成PC端用于登录的token,并返回给PC端

好了,到这里,基本思路就已经清晰了,接下来我们把整个过程再具体化一下

二维码准备

按二维码不同状态来看, 首先是等待扫描状态,用户打开PC端,切换到二维码登录界面时。

  1. PC端向服务端发起请求,告诉服务端,我要生成用户登录的二维码,并且把PC端设备信息也传递给服务端

  2. 服务端收到请求后,它生成二维码ID,并将二维码ID与PC端设备信息进行绑定

  3. 然后把二维码ID返回给PC端

  4. PC端收到二维码ID后,生成二维码(二维码中肯定包含了ID)

  5. 为了及时知道二维码的状态,客户端在展现二维码后,PC端不断的轮询服务端,比如每隔一秒就轮询一次,请求服务端告诉当前二维码的状态及相关信息

二维码已经准好了,接下来就是扫描状态

扫描状态切换

  1. 用户用手机去扫描PC端的二维码,通过二维码内容取到其中的二维码ID

  2. 再调用服务端API将移动端的身份信息与二维码ID一起发送给服务端

  3. 服务端接收到后,它可以将身份信息与二维码ID进行绑定,生成临时token。然后返回给手机端

  4. 因为PC端一直在轮询二维码状态,所以这时候二维码状态发生了改变,它就可以在界面上把二维码状态更新为已扫描

那么为什么需要返回给手机端一个临时token呢?临时token与token一样,它也是一种身份凭证,不同的地方在于它只能用一次,用过就失效。

在第三步骤中返回临时token,为的就是手机端在下一步操作时,可以用它作为凭证。以此确保扫码,登录两步操作是同一部手机端发出的,

状态确认

最后就是状态的确认了。

  1. 手机端在接收到临时token后会弹出确认登录界面,用户点击确认时,手机端携带临时token用来调用服务端的接口,告诉服务端,我已经确认

  2. 服务端收到确认后,根据二维码ID绑定的设备信息与账号信息,生成用户PC端登录的token

  3. 这时候PC端的轮询接口,它就可以得知二维码的状态已经变成了"已确认"。并且从服务端可以获取到用户登录的token

  4. 到这里,登录就成功了,后端PC端就可以用token去访问服务端的资源了

扫码动作的基础流程都讲完了,有些细节还没有深入介绍,

比如二维码的内容是什么?

  • 可以是二维码ID

  • 可以是包含二维码ID的一个url地址

在扫码确认这一步,用户取消了怎么处理? 这些细节都留给大家思考

总结

我们从登陆的本质触发,探索二维码扫码登录是如何做到的

  1. 告诉系统我是谁

  2. 向系统证明我谁

在这个过程中,我们先简单讲了两个前提知识,

  • 一个是二维码原理,

  • 一个是基于token的认证机制。

然后我们以二维码状态为轴,分析了这背后的逻辑: 通过token认证机制与二维码状态变化来实现扫码登录.

需要指出的是,前面的讲的登录流程,它适用于同一个系统的PC端,WEB端,移动端。

平时我们还有另外一种场景也比较常见,那就是通过第三方应用来扫码登录,比如极客时间/掘金 都可以选择微信/QQ等扫码登录,那么这种通过第三方应用扫码登录又是什么原理呢?

感兴趣的同学可以思考研究一下,欢迎在评论区留下你的见解。


作者:望道同学
来源:https://juejin.cn/post/6940976355097985032

收起阅读 »

Nginx 配置在线一键生成“神器”

基于以上的原因,肯定很多读者伙伴经常会收集一些配置文档、或者电脑里也保存着一些自己日常的常用配置案例,但是终究还是不是很便利。今天,民工哥给大家介绍一款「超级牛掰的神器」,可以在线一键生成Nginx的配置。网址:https://nginxconfig.io/操...
继续阅读 »

Nginx作为一个轻量级的HTTP服务器,相比Apache优势也是比较明显的,在性能上它占用资源少,能支持更高更多的并发连接,从而达到提高访问效率;在功能上它是一款非常优秀的代理服务器与负载均衡服务器;在安装配置上它安装,配置都比较简单。

但在实际的生产配置环境中,肯定会经常遇到需要修改、或者重新增加Nginx配置的问题,有的时候需求更是多种多样,修修改改经常会出现这样、那样的一些错误,特别的烦索。

基于以上的原因,肯定很多读者伙伴经常会收集一些配置文档、或者电脑里也保存着一些自己日常的常用配置案例,但是终究还是不是很便利。今天,民工哥给大家介绍一款「超级牛掰的神器」,可以在线一键生成Nginx的配置。

网址:https://nginxconfig.io/

NGINX Config 支持 HTTP、HTTPS、PHP、Python、Node.js、WordPress、Drupal、缓存、逆向代理、日志等各种配置选项。在线生成 Web 服务器 Nginx 配置文件。

操作配置也非常简单,你需要做的只需要2步:

  • 打开官方网站

  • 按需求配置相关参数

系统就会自动生成特定的配置文件。虽然界面是英文的,但是功能的页面做的非常直观,生成的Nginx格式规范。

案例展示

配置域名:mingongge.com 实现用户访问*.mingongge.com 域名时会自动跳转到 mingongge.com 此配置,并且开启http强制跳转到https的配置。

这时,Nginx的配置就会实时自动生成在下面,我把生成的配置复制过来,如下:

/etc/nginx/sites-available/mingongge.com.conf#文件名都给你按规则配置好了
server {
listen 443 ssl http2;

server_name mingongge.com;

# SSL
ssl_certificate /etc/letsencrypt/live/mingongge.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mingongge.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/mingongge.com/chain.pem;

# security
include nginxconfig.io/security.conf;

# additional config
include nginxconfig.io/general.conf;
}

# subdomains redirect
server {
listen 443 ssl http2;

server_name *.mingongge.com;

# SSL
ssl_certificate /etc/letsencrypt/live/mingongge.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mingongge.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/mingongge.com/chain.pem;

return 301 https://mingongge.com$request_uri;
}

# HTTP redirect
server {
listen 80;

server_name .mingongge.com;

include nginxconfig.io/letsencrypt.conf;

location / {
return 301 https://mingongge.com$request_uri;
}
}

非常的方便与快速。

官方还提供一些Nginx的基础优化配置,如下:

/etc/nginx/nginx.conf
# Generated by nginxconfig.io

user www-data;
pid /run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 65535;

events {
multi_accept on;
worker_connections 65535;
}

http {
charset utf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
log_not_found off;
types_hash_max_size 2048;
client_max_body_size 16M;

# MIME
include mime.types;
default_type application/octet-stream;

# logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;

# load configs
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

还有基于安全的配置,如下:

/etc/nginx/nginxconfig.io/security.conf
# security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;

# . files
location ~ /\.(?!well-known) {
deny all;
}

都相当于是提供一些基础的模板配置,可以根据自己的实际需求去修改。

有了这个神器在手,再也不用为配置Nginx的各类配置而烦恼了!!

作者:民工哥

来源:https://www.bbsmax.com/A/kjdw09OwzN/

收起阅读 »

如何搭建一套前端团队的组件系统

使用第三方组件库优缺点快速开发系统管理或中台产品B端产品比较适合,用户群体比较小众,重点在与功能和业务逻辑上手简单,学习成本低体积大,用户访问时间过长,对于C端产品,时间就是金钱,除非部署在高性能服务器或者使用cdn弥补,需要更轻量级组件永恒不变的风格,产品没...
继续阅读 »

伴着公司业务发展,开源的组件库已无法满业务需要,搭建一套更适合公司业务的UI组件库,势在必行,目前市面上有很多功能强大且完善的组件库,比如基于react的开源组件库antDesign,vue的开源组件库elementUI等。

使用第三方组件库优缺点

优点

  • 快速开发系统管理或中台产品

  • B端产品比较适合,用户群体比较小众,重点在与功能和业务逻辑

  • 上手简单,学习成本低

缺点

  • 体积大,用户访问时间过长,对于C端产品,时间就是金钱,除非部署在高性能服务器或者使用cdn弥补,需要更轻量级组件

  • 永恒不变的风格,产品没有差异性

自己搭建组件库相比第三方的优点

  • 打包体积小,更轻量,更贴近业务使用场景

  • 采用内部组件库安全性更高,防止嵌入攻击还有防止类似antDesign圣诞节彩蛋的suprise

  • 构建和开发更灵活,且组合性更高

搭建流程

  • 搭建打包组件库脚手架

  • 组件系统设计思路和模式

  • 组件库的划分

  • 组件库文档生成

  • 将组件库部署到github并发布到npm

搭建打包组件库脚手架

打包组件库工具有很多

  • rollup,打包js利器,非常轻量,集成tree-shaking

  • create-react-app/vue-cli3,可快速改造一个组件库的脚手架

  • webpack自行封装

  • umi/father,基于rollup和babel组件打包功能,集成docz的文档,支持TypeScript等

组件系统设计思路和模式

可以看到基础UI组件是原子组件,作为各种复杂组件的重要组成部分,只有组件的颗粒度足够细,才能满足业务组件使用,区块组件是我们把相同的业务结合基础UI组件进行封装。

这样一套完整的组件化系统就完成了,其中各个组件之间关系是单向的,业务组件只能包含基础UI组件,不能包含区块组件,区块组件里由基础UI组件和业务组件组成。

组件库的划分

我们的基础UI组件库可以参考目前非常流行的UI组件库antd,划分为:通用、布局、导航、数据录入、数据展示、反馈、其他

具体如下:

组件库文档生成

StoryBook

StoryBookReactVueAngular最受欢迎的UI组件开发工具。它可以在隔离的环境中开发和设计应用程序;也可以那个使用它来快速构建UI组件的文档

安装

yarn add @storybook/react

// package.json设置scripts
"scripts": {  
   "storybook": "start-storybook -p 8000"
}

创建文件例如:Button.stories.js

import React from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'

import Button from './button'
import '../../styles/index.scss'

const defaultButton = () => (
 <Button onClick={action('clicked')}> default button </Button>
)

const buttonWithSize = () => (
 <>
   <Button size="lg"> large button </Button>
   <Button size="sm"> small button </Button>
 </>
)

const buttonWithType = () => (
 <>
   <Button btnType="primary"> primary button </Button>
   <Button btnType="danger"> danger button </Button>
   <Button btnType="link" href="https://www.baidu.com"> link button </Button>
 </>
)
storiesOf('Button Component', module)
.add('Button', defaultButton)
.add('不同尺寸的 Button', buttonWithSize)
.add('不同类型的 Button', buttonWithType)

基于umi/father脚手架

集成了docz文档功能,一个开箱即用的组件库打包工具,省去了很多配置工作。docz文档

将组件库部署到github并发布到npm上

package.json配置github地址

"repository": { 
   "type": "git",
   "url": "https://github.com:riyue/zhixing.git"
}

首先在npm官网注册账号,然后执行如下命令,也可发布到自己团队私服上

// 输入用户名和密码
npm adduser

// 发布
npm publish

结束

至此整个组件系统设计思路介绍完毕,在开发中一些细节没有展开叙述,例如:整个组件系统全局主题色配置、单元测试、代码规范检查等,需要大家在实践中去发现问题并解决问题。

希望本文能帮助到你或者给正在搭建组件系统的你有所启发。

作者:日月之行_
来源:https://juejin.cn/post/6999987294534893599

收起阅读 »

2021年互联网公司“死亡”名单,看看有没有你的老东家

几年前我就知道IT桔子一直在统计互联网公司的死亡清单,没想到他们一直坚持在做,目前统计了2014年至2022年,随时更新,上榜公司表示:简直是社死现场!找ofo小黄车 、学霸君、环球易购、叮咚快买、菜鸟团、巨人教育、买卖宝、DaDa英语等因为是公墓嘛,所有还有...
继续阅读 »

几年前我就知道IT桔子一直在统计互联网公司的死亡清单,没想到他们一直坚持在做,目前统计了2014年至2022年,随时更新,上榜公司表示:简直是社死现场!

这里面包括也很多的知名公司,如:

找ofo小黄车 、学霸君、环球易购、叮咚快买、菜鸟团、巨人教育、买卖宝、DaDa英语等

因为是公墓嘛,所有还有一个让人哭笑不得的功能,就是“上香“,你要不要为你已经倒闭的前公司上个香呢?

在这份名单中,清晰的记录了公司名称,存活时长、倒闭时长、所属行业、公司地点、成立时间、获投状态,我们根据行业来看看,2021年总计倒闭公司821家,都是什么情况。

2021年《教育类》公司死亡清单

2021年《金融类》公司死亡清单

2021年《游戏类》公司死亡清单

2021年《区块链类》公司死亡清单

总计分为23个大类,详细的可以去IT桔子的网站查看。

针对这些行业公司的死亡原因,该网站也进行了统计。

如:手机游戏类公司 公司死亡795家,死亡原因70%都是因为资金问题。

如:交友社区类公司 公司死亡464家,死亡原因40%都是市场定位有问题,30%的原因是产品问题,也有10%的原因是资金问题。

针对除了行业的大类分析,每个公司的死亡原因也进行了详细说明。

如买卖宝,存活时间有14年2个月,曾经获得过红杉资本、腾讯、京东等的融资。估值97.5亿元。

死亡原因:竞争不足。

如杰睿教育,存活时间有10年9个月,曾经获得过赛领资本、千帆资本、真格基金等的融资。估值26.6亿元。

死亡原因:政策监管。

数据说明

死亡公司数据库网页声明:

一、本网页基于IT桔子投资数据库而打造的“死亡公司数据库”,致力于展现中国新经济领域近些年倒闭的创新创业公司;
二、“死亡公司数据库”的公司关闭时间是依据公开媒体报道及部分估算,可能会存在些许误差,但我们着力确保更高的可靠性;
三、IT桔子对所收录公司运营状况的判定来源如下:
1、公开媒体报道公司关闭、破产清算的;
2、公司自身在微信、微博等渠道宣布关闭、破产清算的;
3、公司明显经营异常:公司被注销;公司产品比如APP或微信持续6个月及以上没更新;公司因为监管被抓、无法经营……交叉比对后确认没有持续经营。

这几天也看了不少博主对2022年的进行了预测,但是,我们始终要相信:世界上唯一不变的是一直在变。特别是作为一名在互联网觅食的程序员,一定要提升自己的技术和管理能力,投资自己是最正确的选择。

对于那些想创业的朋友,一定要三思而后行,别一时想不开就出来创业!

守住自己的钱袋子,别乱投资!猥琐发育,只要还有本金,没下赌桌,就还有翻身的机会,不要梭哈!

2022年,我们一起加油!

作者:机器人的秘密探索
来源:https://www.sohu.com/a/515405099_121124372

收起阅读 »

求解“微信群覆盖”的三种方法:暴力,染色,链表,并查集

(1) 题目简介;(3) 思路二:染色法;(5) 思路四:并查集法;(1) 每个微信群由一个唯一的gid标识;(3) 一个用户可以加入多个群;g1{u1, u2, u3}可以看到,用户u1加入了g1与g2两个群。gid和uid都是uint64;(1) ...
继续阅读 »



这是一篇聊算法的文章,从一个小面试题开始,扩展到一系列基础算法,包含几个部分:

(1) 题目简介;

(2) 思路一:暴力法;

(3) 思路二:染色法;

(4) 思路三:链表法;

(5) 思路四:并查集法;

除了聊方案,重点分享思考过程。文章较长,可提前收藏。


第一部分:题目简介


问题提出:求微信群覆盖


微信有很多群,现进行如下抽象:

(1) 每个微信群由一个唯一的gid标识;

(2) 微信群内每个用户由一个唯一的uid标识;

(3) 一个用户可以加入多个群;

(4) 群可以抽象成一个由不重复uid组成的集合,例如:

g1{u1, u2, u3}

g2{u1, u4, u5}

可以看到,用户u1加入了g1与g2两个群。

画外音:

gid和uid都是uint64;

集合内没有重复元素;


假设微信有M个群(M为亿级别),每个群内平均有N个用户(N为十级别).


现在要进行如下操作:

(1) 如果两个微信群中有相同的用户则将两个微信群合并,并生成一个新微信群;

例如,上面的g1和g2就会合并成新的群:

g3{u1, u2, u3, u4, u5};

画外音:集合g1中包含u1,集合g2中包含u1,合并后的微信群g3也只包含一个u1。

(2) 不断的进行上述操作,直到剩下所有的微信群都不含相同的用户为止

将上述操作称:求群的覆盖。


设计算法,求群的覆盖,并说明算法时间与空间复杂度。

画外音:你遇到过类似的面试题吗?


对于一个复杂的问题,思路肯定是“先解决,再优化”,大部分人不是神,很难一步到位。先用一种比较“笨”的方法解决,再看“笨方法”有什么痛点,优化各个痛点,不断升级方案。


第二部分:暴力法


拿到这个问题,很容易想到的思路是:

(1) 先初始化M个集合,用集合来表示微信群gid与用户uid的关系;

(2) 找到哪两个(哪些)集合需要合并;

(3) 接着,进行集合的合并;

(4) 迭代步骤二和步骤三,直至所有集合都没有相同元素,算法结束;


第一步,如何初始化集合?

set这种数据结构,大家用得很多,来表示集合:

(1) 新建M个set来表示M个微信群gid;

(2) 每个set插入N个元素来表示微信群中的用户uid;


set有两种最常见的实现方式,一种是树型set,一种是哈希型set


假设有集合:

s={7, 2, 0, 14, 4, 12}


树型set的实现如下:

其特点是:

(1) 插入和查找的平均时间复杂度是O(lg(n));

(2) 能实现有序查找;

(3) 省空间;


哈希型set实现如下:

其特点是:

(1) 插入和查找的平均时间复杂度是O(1);

(2) 不能实现有序查找;

画外音:求群覆盖,哈希型实现的初始化更快,复杂度是O(M*N)。


第二步,如何判断两个(多个)集合要不要合并?

集合对set(i)和set(j),判断里面有没有重复元素,如果有,就需要合并,判重的伪代码是:

// 对set(i)和set(j)进行元素判断并合并

(1)    foreach (element in set(i))

(2)    if (element in set(j))

         merge(set(i), set(j));


第一行(1)遍历第一个集合set(i)中的所有元素element;

画外音:这一步的时间复杂度是O(N)。

第二行(2)判断element是否在第二个集合set(j)中;

画外音:如果使用哈希型set,第二行(2)的平均时间复杂度是O(1)。


这一步的时间复杂度至少是O(N)*O(1)=O(N)。


第三步,如何合并集合?

集合对set(i)和set(j)如果需要合并,只要把一个集合中的元素插入到另一个集合中即可:

// 对set(i)和set(j)进行集合合并

merge(set(i), set(j)){

(1)    foreach (element in set(i))

(2)    set(j).insert(element);

}


第一行(1)遍历第一个集合set(i)中的所有元素element;

画外音:这一步的时间复杂度是O(N)。

第二行(2)把element插入到集合set(j)中;

画外音:如果使用哈希型set,第二行(2)的平均时间复杂度是O(1)。


这一步的时间复杂度至少是O(N)*O(1)=O(N)。


第四步:迭代第二步与第三步,直至结束

对于M个集合,暴力针对所有集合对,进行重复元素判断并合并,用两个for循环可以暴力解决:

(1)for(i = 1 to M)

(2)    for(j= i+1 to M)

         //对set(i)和set(j)进行元素判断并合并

         foreach (element in set(i))

         if (element in set(j))

         merge(set(i), set(j));


递归调用,两个for循环,复杂度是O(M*M)。


综上,如果这么解决群覆盖的问题,时间复杂度至少是:

O(M*N) // 集合初始化的过程

+

O(M*M) // 两重for循环递归

*

O(N) // 判重

*

O(N) // 合并

画外音:实际复杂度要高于这个,随着集合的合并,集合元素会越来越多,判重和合并的成本会越来越高。


第三部分:染色法


总的来说,暴力法效率非常低,集合都是一个一个合并的,同一个元素在合并的过程中要遍历很多次。很容易想到一个优化点,能不能一次合并多个集合?


暴力法中,判断两个集合set和set是否需要合并,思路是:遍历set中的所有element,看在set中是否存在,如果存在,说明存在交集,则需要合并。


哪些集合能够一次性合并?

当某些集合中包含同一个元素时,可以一次性合并。


怎么一次性发现,哪些集合包含同一个元素,并合并去重呢?


回顾一下工作中的类似需求:

M个文件,每个文件包含N个用户名,或者N个手机号,如何合并去重?

最常见的玩法是:

cat file_1 file_2 … file_M | sort | uniq > result


这里的思路是什么?

(1) 把M*N个用户名/手机号输出;

(2) sort排序,排序之后相同的元素会相邻

(3) uniq去重,相邻元素如果相同只保留一个;


排序之后相同的元素会相邻”,就是一次性找出所有可合并集合的关键,这是染色法的核心。


举一个栗子

假设有6个微信群,每个微信群有若干个用户:

s1={1,0,5} s2={3,1} s3={2,9}

s4={4,6} s5={4,7} s6={1,8}

假设使用树形set来表示集合。

首先,给同一个集合中的所有元素染上相同的颜色,表示来自同一个集合。

然后,对所有的元素进行排序,会发现:

(1) 相同的元素一定相邻,并且一定来自不同的集合;

(2) 同一个颜色的元素被打散了;

这些相邻且相同的元素,来自哪一个集合,这些集合就是需要合并的,如上图:

(1) 粉色的1来自集合s1,紫色的1来自集合s2,黄色的1来自集合s6,所以s1s2s6需要合并;

(2) 蓝色的4来自集合s4,青色的4来自集合s5,所以s4s5需要合并;


不用像暴力法遍历所有的集合对,而是一个排序动作,就能找到所有需要合并的集合。

画外音:暴力法一次处理2个集合,染色法一次可以合并N个集合。

集合合并的过程,可以想象为,相同相邻元素所在集合,染成第一个元素的颜色

(1) 紫色和黄色,染成粉色;

(2) 青色,染成蓝色;


最终,剩余三种颜色,也就是三个集合:

s1={0,1,3,5,8}

s3={2,9}

s4={4,6,7}


神奇不神奇!!!


染色法有意思么?但仍有两个遗留问题

(1) 粉色1,紫色1,黄色1,三个元素如何找到这三个元素所在的集合s1s2s6呢?

(2) s1s2s6三个集合如何快速合并

画外音:假设总元素个数n=M*N,如果使用树形set,合并的复杂度为O(n*lg(n)),即O(M*N*lg(M*N))。


我们继续往下看。


第四部分:链表法


染色法遗留了两个问题:

步骤(2)中,如何通过元素快速定位集合

步骤(3)中,如何快速合并集合

我们继续聊聊这两个问题的优化思路。


问题一:如何由元素快速定位集合?

普通的集合,只能由集合根(root)定位元素,不能由元素逆向定位root,如何支持元素逆向定位root呢?

很容易想到,每个节点增加一个父指针即可。


更具体的:

element{

         int data;

         element* left;

         element* right;

}


升级为:

element{

         element* parent;    // 指向父节点

         int data;

         element* left;

         element* right;

}

如上图:所有节点的parent都指向它的上级,而只有root->parent=NULL。


对于任意一个元素,找root的过程为:

element* X_find_set_root(element* x){

         element* temp=x;

         while(temp->parent != NULL){

                   temp= temp->parent;

         }

         return temp;

}


很容易发现,由元素找集合根的时间复杂度是树的高度,即O(lg(n))


有没有更快的方法呢?

进一步思考,为什么每个节点要指向父节点,直接指向根节点是不是也可以。


更具体的:

element{

         int data;

         element* left;

         element* right;

}


升级为:

element{

         element* root;         // 指向集合根

         int data;

         element* left;

         element* right;

}

如上图:所有节点的parent都指向集合的根。


对于任意一个元素,找root的过程为:

element* X_find_set_root(element* x){

         return x->root;

}


很容易发现,升级后,由元素找集合根的时间复杂度是O(1)

画外音:不能更快了吧。


另外,这种方式,能在O(1)的时间内,判断两个元素是否在同一个集合内

bool in_the_same_set(element* a, element* b){

         return (a->root == b->root);

}

甚为方便。

画外音:两个元素的根相同,就在同一个集合内。


问题二:如何快速进行集合合并? 

暴力法中提到过,集合合并的伪代码为:

merge(set(i), set(j)){

         foreach(element in set(i))

                   set(j).insert(element);

}

把一个集合中的元素插入到另一个集合中即可。


假设set(i)的元素个数为n1,set(j)的元素个数为n2,其时间复杂度为O(n1*lg(n2))。


在“微信群覆盖”这个业务场景下,随着集合的不断合并,集合高度越来越高,合并会越来越慢,有没有更快的集合合并方式呢?


仔细回顾一下:

(1) 树形set的优点是,支持有序查找,省空间;

(2) 哈希型set的优点是,快速插入与查找;


而“微信群覆盖”场景对集合的频繁操作是:

(1) 由元素找集合根;

(2) 集合合并;


那么,为什么要用树形结构或者哈希型结构来表示集合呢?

画外音:优点完全没有利用上嘛。


让我们来看看,这个场景中,如果用链表来表示集合会怎么样,合并会不会更快?

s1={7,3,1,4}

s2={1,6}

如上图,分别用链表来表示这两个集合。可以看到,为了满足“快速由元素定位集合根”的需求,每个元素仍然会指向根。


s1和s2如果要合并,需要做两件事:

(1) 集合1的尾巴,链向集合2的头(蓝线1);

(2) 集合2的所有元素,指向集合1的根(蓝线2,3);


合并完的效果是:

变成了一个更大的集合。


假设set(1)的元素个数为n1,set(2)的元素个数为n2,整个合并的过程的时间复杂度是O(n2)。

画外音:时间耗在set(2)中的元素变化。


咦,我们发现:

(1) 将短的链表,接到长的链表上;

(2) 将长的链表,接到短的链表上;

所使用的时间是不一样的。


为了让时间更快,一律使用更快的方式:“元素少的链表”主动接入到“元素多的链表”的尾巴后面。这样,改变的元素个数能更少一些,这个优化被称作“加权合并”。


对于M个微信群,平均每个微信群N个用户的场景,用链表的方式表示集合,按照“加权合并”的方式合并集合,最坏的情况下,时间复杂度是O(M*N)。

画外音:假设所有的集合都要合并,共M次,每次都要改变N个元素的根指向,故为O(M*N)。


于是,对于“M个群,每个群N个用户,微信群求覆盖”问题,使用“染色法”加上“链表法”,核心思路三步骤:

(1) 全部元素全局排序

(2) 全局排序后,不同集合中的相同元素,一定是相邻的,通过相同相邻的元素,一次性找到所有需要合并的集合

(3) 合并这些集合,算法完成;


其中:

步骤(1),全局排序,时间复杂度O(M*N);

步骤(2),染色思路,能够迅猛定位哪些集合需要合并,每个元素增加一个属性指向集合根,实现O(1)级别的元素定位集合;

步骤(3),使用链表表示集合,使用加权合并的方式来合并集合,合并的时间复杂度也是O(M*N);


总时间复杂度是:

O(M*N)    //排序

+

O(1)        //由元素找到需要合并的集合

*

O(M*N)    //集合合并


神奇不神奇!


神奇不止一种,还有其他方法吗?我们接着往下看。


第五部分:并查集法


分离集合(disjoint set)是一种经典的数据结构,它有三类操作:

Make-set(a):生成一个只有一个元素a的集合;

Union(X, Y):合并两个集合X和Y;

Find-set(a):查找元素a所在集合,即通过元素找集合;


这种数据结构特别适合用来解决这类集合合并与查找的问题,又称为并查集


能不能利用并查集来解决求“微信群覆盖”问题呢?


一、并查集的链表实现


链表法里基本聊过,为了保证知识的系统性,这里再稍微回顾一下。

如上图,并查集可以用链表来实现。


链表实现的并查集,Find-set(a)的时间复杂度是多少?

集合里的每个元素,都指向“集合的句柄”,这样可以使得“查找元素a所在集合S”,即Find-set(a)操作在O(1)的时间内完成


链表实现的并查集,Union(X, Y)的时间复杂度是多少?

假设有集合:

S1={7,3,1,4}

S2={1,6}


合并S1和S2两个集合,需要做两件事情:

(1) 第一个集合的尾元素,链向第二个集合的头元素(蓝线1);

(2) 第二个集合的所有元素,指向第一个集合的句柄(蓝线2,3);


合并完的效果是:

变成了一个更大的集合S1。


集合合并时,将短的链表,往长的链表上接,这样变动的元素更少,这个优化叫做“加权合并”。

画外音:实现的过程中,集合句柄要存储元素个数,头元素,尾元素等属性,以方便上述操作进行。


假设每个集合的平均元素个数是nUnion(X, Y)操作的时间复杂度是O(n)


能不能Find-set(a)与Union(X, Y)都在O(1)的时间内完成呢?

可以,这就引发了并查集的第二种实现方法。


二、并查集的有根树实现


什么是有根树,和普通的树有什么不同?

常用的set,就是用普通的二叉树实现的,其元素的数据结构是:

element{

         int data;

         element* left;

         element* right;

}

通过左指针与右指针,父亲节点指向儿子节点。


而有根树,其元素的数据结构是:

element{

         int data;

         element* parent;

}

通过儿子节点,指向父亲节点。


假设有集合:

S1={7,3,1,4}

S2={1,6}


通过如果通过有根树表示,可能是这样的:

所有的元素,都通过parent指针指向集合句柄,所有元素的Find-set(a)的时间复杂度也是O(1)。

画外音:假设集合的首个元素,代表集合句柄。


有根树实现的并查集,Union(X, Y)的过程如何?时间复杂度是多少?

通过有根树实现并查集,集合合并时,直接将一个集合句柄,指向另一个集合即可。

如上图所示,S2的句柄,指向S1的句柄,集合合并完成:S2消亡,S1变为了更大的集合。


容易知道,集合合并的时间复杂度为O(1)


会发现,集合合并之后,有根树的高度变高了,与“加权合并”的优化思路类似,总是把节点数少的有根树,指向节点数多的有根树(更确切的说,是高度矮的树,指向高度高的树),这个优化叫做“按秩合并”。


新的问题来了,集合合并之后,不是所有元素的Find-set(a)操作都是O(1)了,怎么办?

如图S1与S2合并后的新S1,首次“通过元素6来找新S1的句柄”,不能在O(1)的时间内完成了,需要两次操作。


但为了让未来“通过元素6来找新S1的句柄”的操作能够在O(1)的时间内完成,在首次进行Find-set(“6”)时,就要将元素6“寻根”路径上的所有元素,都指向集合句柄,如下图。

某个元素如果不直接指向集合句柄,首次Find-set(a)操作的过程中,会将该路径上的所有元素都直接指向句柄,这个优化叫做“路径压缩”。

画外音:路径上的元素第二次执行Find-set(a)时,时间复杂度就是O(1)了。


实施“路径压缩”优化之后,Find-set的平均时间复杂度仍是O(1)


稍微总结一下。


通过链表实现并查集:

(1) Find-set的时间复杂度,是O(1)常数时间;

(2) Union的时间复杂度,是集合平均元素个数,即线性时间;

画外音:别忘了“加权合并”优化。


通过有根树实现并查集:

(1) Union的时间复杂度,是O(1)常数时间;

(2) Find-set的时间复杂度,通过“按秩合并”与“路径压缩”优化后,平均时间复杂度也是O(1);


即,使用并查集,非常适合解决“微信群覆盖”问题。


知其然,知其所以然,思路往往比结果更重要

算法,其实还是挺有意思的。

作者:58沈剑
来源:https://mp.weixin.qq.com/s/2MNL4vDpXQR94KGts4JUhA

收起阅读 »

一张图看懂开源许可协议,开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别

首先借用有心人士的一张相当直观清晰的图来划分各种协议:开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别 以下是上述协议的简单介绍:BSD开源协议BSD开源协议是一个给于使用者很大自由的协议。基本上使用者可以”为所欲为”,可以自由...
继续阅读 »

首先借用有心人士的一张相当直观清晰的图来划分各种协议:开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别



以下是上述协议的简单介绍:
BSD开源协议
BSD开源协议是一个给于使用者很大自由的协议。基本上使用者可以”为所欲为”,可以自由的使用,修改源代码,也可以将修改后的代码作为开源或者专有软件再发布。

但”为所欲为”的前提当你发布使用了BSD协议的代码,或则以BSD协议代码为基础做二次开发自己的产品时,需要满足三个条件:

    如果再发布的产品中包含源代码,则在源代码中必须带有原来代码中的BSD协议。
    如果再发布的只是二进制类库/软件,则需要在类库/软件的文档和版权声明中包含原来代码中的BSD协议。
    不可以用开源代码的作者/机构名字和原来产品的名字做市场推广。

BSD 代码鼓励代码共享,但需要尊重代码作者的著作权。BSD由于允许使用者修改和重新发布代码,也允许使用或在BSD代码上开发商业软件发布和销售,因此是对商业集成很友好的协议。而很多的公司企业在选用开源产品的时候都首选BSD协议,因为可以完全控制这些第三方的代码,在必要的时候可以修改或者二次开发。

Apache Licence 2.0
Apache Licence是著名的非盈利开源组织Apache采用的协议。该协议和BSD类似,同样鼓励代码共享和尊重原作者的著作权,同样允许代码修改,再发布(作为开源或商业软件)。需要满足的条件也和BSD类似:

    需要给代码的用户一份Apache Licence
    如果你修改了代码,需要再被修改的文件中说明。
    在延伸的代码中(修改和有源代码衍生的代码中)需要带有原来代码中的协议,商标,专利声明和其他原来作者规定需要包含的说明。
    如果再发布的产品中包含一个Notice文件,则在Notice文件中需要带有Apache Licence。你可以在Notice中增加自己的许可,但不可以表现为对Apache Licence构成更改。

Apache Licence也是对商业应用友好的许可。使用者也可以在需要的时候修改代码来满足需要并作为开源或商业产品发布/销售。
GPL

我们很熟悉的Linux就是采用了GPL。GPL协议和BSD, Apache Licence等鼓励代码重用的许可很不一样。GPL的出发点是代码的开源/免费使用和引用/修改/衍生代码的开源/免费使用,但不允许修改后和衍生的代码做为闭源的商业软件发布和销售。这也就是为什么我们能用免费的各种linux,包括商业公司的linux和linux上各种各样的由个人,组织,以及商业软件公司开发的免费软件了。

GPL协议的主要内容是只要在一个软件中使用(”使用”指类库引用,修改后的代码或者衍生代码)GPL 协议的产品,则该软件产品必须也采用GPL协议,既必须也是开源和免费。这就是所谓的”传染性”。GPL协议的产品作为一个单独的产品使用没有任何问题,还可以享受免费的优势。

由于GPL严格要求使用了GPL类库的软件产品必须使用GPL协议,对于使用GPL协议的开源代码,商业软件或者对代码有保密要求的部门就不适合集成/采用作为类库和二次开发的基础。

其它细节如再发布的时候需要伴随GPL协议等和BSD/Apache等类似。

LGPL
LGPL是GPL的一个为主要为类库使用设计的开源协议。和GPL要求任何使用/修改/衍生之GPL类库的的软件必须采用GPL协议不同。LGPL 允许商业软件通过类库引用(link)方式使用LGPL类库而不需要开源商业软件的代码。这使得采用LGPL协议的开源代码可以被商业软件作为类库引用并发布和销售。

但是如果修改LGPL协议的代码或者衍生,则所有修改的代码,涉及修改部分的额外代码和衍生的代码都必须采用LGPL协议。因此LGPL协议的开源代码很适合作为第三方类库被商业软件引用,但不适合希望以LGPL协议代码为基础,通过修改和衍生的方式做二次开发的商业软件采用。

GPL/LGPL都保障原作者的知识产权,避免有人利用开源代码复制并开发类似的产品

MIT
MIT是和BSD一样宽范的许可协议,作者只想保留版权,而无任何其他了限制.也就是说,你必须在你的发行版里包含原许可协议的声明,无论你是以二进制发布的还是以源代码发布的.

MPL
MPL是The Mozilla Public License的简写,是1998年初Netscape的 Mozilla小组为其开源软件项目设计的软件许可证。MPL许可证出现的最重要原因就是,Netscape公司认为GPL许可证没有很好地平衡开发者对源代码的需求和他们利用源代码获得的利益。同著名的GPL许可证和BSD许可证相比,MPL在许多权利与义务的约定方面与它们相同(因为都是符合OSIA 认定的开源软件许可证)。但是,相比而言MPL还有以下几个显著的不同之处:

◆ MPL虽然要求对于经MPL许可证发布的源代码的修改也要以MPL许可证的方式再许可出来,以保证其他人可以在MPL的条款下共享源代码。但是,在MPL 许可证中对“发布”的定义是“以源代码方式发布的文件”,这就意味着MPL允许一个企业在自己已有的源代码库上加一个接口,除了接口程序的源代码以MPL 许可证的形式对外许可外,源代码库中的源代码就可以不用MPL许可证的方式强制对外许可。这些,就为借鉴别人的源代码用做自己商业软件开发的行为留了一个豁口。
◆ MPL许可证第三条第7款中允许被许可人将经过MPL许可证获得的源代码同自己其他类型的代码混合得到自己的软件程序。
◆ 对软件专利的态度,MPL许可证不像GPL许可证那样明确表示反对软件专利,但是却明确要求源代码的提供者不能提供已经受专利保护的源代码(除非他本人是专利权人,并书面向公众免费许可这些源代码),也不能在将这些源代码以开放源代码许可证形式许可后再去申请与这些源代码有关的专利。
◆ 对源代码的定义
而在MPL(1.1版本)许可证中,对源代码的定义是:“源代码指的是对作品进行修改最优先择取的形式,它包括:所有模块的所有源程序,加上有关的接口的定义,加上控制可执行作品的安装和编译的‘原本’(原文为‘Script’),或者不是与初始源代码显著不同的源代码就是被源代码贡献者选择的从公共领域可以得到的程序代码。”
◆ MPL许可证第3条有专门的一款是关于对源代码修改进行描述的规定,就是要求所有再发布者都得有一个专门的文件就对源代码程序修改的时间和修改的方式有描述。

作者:微wx笑
翻译:https://blog.csdn.net/testcs_dn/article/details/38496107
原文:http://www.mozilla.org/MPL/MPL-1.1.html

收起阅读 »

拒绝白嫖,开源项目作者删库跑路,数千个应用程序无限输出乱码

「我删我自己的开源项目代码,需要经过别人允许吗?」几天前,开源库「faker.js」和「colors.js」的用户打开电脑,发现自己的应用程序正在输出乱码数据,那一刻,他们惊呆了。更令人震惊的是,开发者们发现,造成这一混乱局面的就是「faker.js」和「co...
继续阅读 »



「我删我自己的开源项目代码,需要经过别人允许吗?」

几天前,开源库「faker.js」和「colors.js」的用户打开电脑,发现自己的应用程序正在输出乱码数据,那一刻,他们惊呆了。

更令人震惊的是,开发者们发现,造成这一混乱局面的就是「faker.js」和「colors.js」的作者 Marak Squires 本人。

一夜之间,Marak Squires 主动删除了「faker.js」和「colors.js」项目仓库的所有代码,让正在使用这两个开源项目的数千位开发者直接崩溃。

「faker.js」和「colors.js」

faker.js 在 npm 上的每周下载量接近 250 万,color.js 每周的下载量约为 2240 万,本次删库的影响是极其严重的,使用这两个项目开发的工具包括 AWS CDK 等。

如果在构建和测试应用时,真实的数据量远远不够,那么 Faker 类工具将帮助开发者生成伪数据。faker.js 就是可为多个领域生成伪数据的 Node.js 库,包括地址、商业、公司、日期、财务、图像、随机数、名称等。

faker.js 支持生成英文、中文等多语种信息,包含丰富的 API,此前版本通常一个月迭代更新一次。faker.js 不仅可以使用在服务器端的 JavaScript,还可以应用在浏览器端的 JavaScript。

现在,faker.js 项目的所有 commit 信息都被改为「endgame」,在 README 中,作者写下这样一句话:「What really happened with Aaron Swartz?」

Swartz 是一位杰出的开发人员,帮助建立了 Creative Commons、RSS 和 Reddit。2011 年,Swartz 被指控从学术数据库 JSTOR 中窃取文件,目的是免费访问这些文件。Swartz 在 2013 年自杀,Squires 提到 Swartz 可能意指围绕这一死亡疑云。

Marak Squires 向 colors.js 提交了恶意代码,添加了一个「a new American flag module」,然后将其发布到了 GitHub 和 npm。

随后他在 GitHub 和 npm 发布了 faker.js 6.6.6,这两个动作引发了同样的破坏性事件。破坏后的版本导致应用程序无限输出奇怪的字母和符号,从三行写着「LIBERTY LIBERTY LIBERTY」的文本开始,后面跟着一系列非 ASCII 字符:

目前,color.js 已经更新了一个可以使用的版本。faker.js 项目尚未恢复,开发者只能通过降级到此前的 5.5.3 版本来解决问题。

为了解决问题,Squires 在 GitHub 上还发布了更新以解决「zalgo 问题」,该问题是指损坏文件产生的故障文本。

「我们注意到在 v1.4.44-liberty-2 版本的 colors 中有一个 zalgo 错误,」Squires 以一种讽刺的语气写道。「我们现在正在努力解决这个问题,很快就会有解决方案。」

在将更新推送到 faker.js 两天后,Squires 发了一条推文,表示自己存储了数百个项目的 GitHub 账户已经被封。Squires 在 1 月 4 日发布了 faker.js 的最新 commit,在 1 月 6 日被封,直到 1 月 7 日推送了 colors.js 的「liberty」版本。然而,从 faker.js 和 colors.js 的更新日志来看,他的账户似乎被解封过。目前尚不清楚 Squires 的帐户是否再次被封。

至此,故事并没有就此结束。Squires 2020 年 11 月发在 GitHub 上的一篇帖子被挖出来,在帖子中他写道自己不再想做免费的工作了。「恕我直言,我不想再用我的免费工作来支持财富 500 强(和其他小型公司),以此为契机,向我发送一份六位数的年度合同,或者 fork 项目并让其他人参与其中。」

Squires 的大胆举动引起了人们对开源开发者的道德和财务困境的关注,这可能是 Marak Squires 行动的目标。大量网站、软件和应用程序依赖开源开发人员来创建基本工具和组件,而所有这些都是免费的,无偿开发人员经常不知疲倦地工作,努力修复其开源软件中的安全问题。

开发者们怎么看

软件工程师 Sergio Gómez 表示:「从 GitHub 删除自己的代码违反了他们的服务条款?WTF?这是绑架。我们需要开始分散托管免费软件源代码。」

「不知道发生了什么,但我将我所有的项目都托管在 GitLab 私有 instance 上,永远不要相信任何互联网服务提供商。」

有网友认为 faker.js 团队的反应有些夸张了,并说道:「没有人会用一个只生成一些虚假数据的包赚大钱。faker.js 的确为开发者生成伪数据节省了一些时间,但我们也可以让实习生编写类似程序来生成数据。这对企业来说并没有那么重要。」

甚至有人认为 Marak 这么做是一种冲动行为,不够理性,并和他之前「卖掉房子购买 NFT」的传闻联系起来,认为 Marak 需要学会控制自己的情绪:

这种说法很快带偏部分网友的看法,有人原本同情开源项目被「白嫖」,但现在已转向认为 Marak 是恶意删库,并指出:「停止维护他的项目或完全删除都是他的权利,但故意提交有害代码是不对的。」

当然,也有人为开源软件(FOSS)开发者的待遇鸣不平:「希望有相关的基金会位 FOSS 开发人员提供资金支持」,而软件的可靠性和稳定性也是至关重要的

有人表示:一些大公司确实不尊重开源项目的版权,滥用开源项目对于 FOSS 开发者来说是绝对不公平的。但 Marak 对 faker.js 的做法并不可取,不是正面例子,存在 Marak 的个人负面原因。

对此,你有什么看法?

作者:机器之心Pro

来源:https://www.163.com/dy/article/GTC8PE5M0511AQHO.html

收起阅读 »

崩溃的一天,西安一码通崩溃背后的技术问题

12月20号,算得上西安崩溃的一天。西安防疫压力巨大,各单位公司要求,需48小时核酸检测报告上班。足足瘫痪超过 15 个多小时!到了下午,新闻甚至提示:这是解决问题的方法吗?今天,我们就试着分析一下这个业务、以及对应的技术问题。西安一码通其它业务我们暂且不分析...
继续阅读 »

1.崩溃的一天

12月20号,算得上西安崩溃的一天。

12月19号新增病例21个,20号新增病例42个,并且有部分病例已经在社区内传播.

西安防疫压力巨大,各单位公司要求,需48小时核酸检测报告上班。

在这样严峻的情况下,作为防控最核心的系统:西安一码通竟然崩溃了,并且崩溃得是那么的彻底。

足足瘫痪超过 15 个多小时!

整整一天的时间呀,多少上班族被堵在地铁口,多少旅客被冻在半路上,进退不能...

到了下午,新闻甚至提示:

为了减轻系统压力,建议广大市民非必要不展码、亮码,在出现系统卡顿时,请耐心等待,尽量避免反复刷新,也感谢广大市民朋友们的理解配合。

这是解决问题的方法吗?

如果真的需要限流来防止系统崩溃,用技术手段来限流是不是会更简单一些,甚至前面加一个 nginx 就能解决的问题。

今天,我们就试着分析一下这个业务、以及对应的技术问题。

2.产品分析

西安一码通其它业务我们暂且不分析,那并不是重点,并且当天也没有完全崩溃,崩溃的仅有扫码功能。

其实这是一个非常典型的大量查询、少数更新的业务,闭着眼睛分析一下,可以说, 90% 以上的流量都是查询。

我们先来看看第一版的产品形态,扫码之后展示个人部分姓名和身份政信息,同时下面展示绿、黄、红码。

这是西安一码通最开始的样子,业务流程仅仅只需要一个请求,甚至一个查询的 SQL 就可以搞定。

到了后来,这个界面做了2次比较大的改版。

第一次改版新增了疫苗接种信息,加了一个边框;第二次改版新增了核酸检测信息,在最下方展示核酸检测时间、结果。

整个页面增加了2个查询业务,如果系统背后使用的是关系数据库,可能会多增加至少2个查询SQL。

基本上就是这样的一个需求,据统计西安有1300万人口,按照最大10%的市民同时扫码(我怀疑不会有这么多),也就是百万的并发量。

这样一个并发量的业务,在互联网公司很常见,甚至比这个复杂的场景也多了去了。

那怎么就崩了呢?

3.技术分析

在当天晚上的官方回复中,我们看到有这样一句话:

12月20日早7:40分左右,西安“一码通”用户访问量激增,每秒访问量达到以往峰值的10倍以上,造成网络拥塞,致使包括“一码通”在内的部分应用系统无法正常使用。“

一码通”后台监控第一时间报警,各24小时驻场通信、网络、政务云、安全和运维团队立即开展排查,平台应用系统和数据库运行正常,判断问题出现在网络接口侧。

根据上面的信息,数据库和平台系统都正常,是网络出现了问题。

我之前在文章《一次dns缓存引发的惨案》画过一张访问示意图,用这个图来和大家分析一下,网络出现问题的情况。

一般用户的请求,会先从域名开始,经过DNS服务器解析后拿到外网IP地址,经过外网IP访问防火墙和负载之后打到服务器,最后服务器响应后将结果返回到浏览器。

如果真的是网络出现问题,一般最常见的问题就是 DNS 解析错误,或者外网的宽带被打满了。

DNS解析错误一定不是本次的问题,不然可能不只是这一个功能出错了;外网的宽带被打满,直接增加带宽就行,不至于一天都没搞定。

如果真的是网络侧出现问题,一般也不需要改动业务,但实际上系统恢复的时候,大家都发现界面回到文章开头提到了第一个版本了。

也就是说系统“回滚”了。

界面少了接种信息和核酸检测信息的内容,并且在一码通的首页位置,新增加了一个核酸查询的页面。

所以,仅仅是网络接口侧出现问题吗?我这里有一点点的疑问。

4.个人分析

根据我以往的经验,这是一个很典型的系统过载现象,也就是说短期内请求量超过服务器响应。

说人话就是,外部请求量超过了系统的最大处理能力。

当然了,系统最大处理能力和系统架构息息相关,同样的服务器不同的架构,系统负载量差异极大。

应对这样的问题,解决起来无非有两个方案,一个是限流,另外一个就是扩容了。

限流就是把用户挡在外面,先处理能处理的请求;扩容就是加服务器、增加数据库承载能力。

上面提到官方让大家没事别刷一码通,也算是人工限流的一种方式;不过在技术体系上基本上不会这样做。

技术上的限流方案有很多,但最简单的就是前面挂一个 Nginx 配置一下就能用;复杂一点就是接入层自己写算法。

当然了限流不能真正的解决问题,只是负责把一部分请求挡在外面;真正解决问题还是需要扩容,满足所有用户。

但实际上,根据解决问题的处理和产品回滚的情况来看,一码通并没有第一时间做扩容,而是选择了回滚。

这说明,在系统架构设计上,没有充分考虑扩容的情况,所以并不能支持第一时间选择这个方案。

5.理想的方案?

上面说那么多也仅仅是个人推测,实际上可能他们会面临更多现实问题,比如工期紧张、老板控制预算等等...

话说回来,如果你是负责一码通公司的架构师,你会怎么设计整个技术方案呢?欢迎大家留言,这里说说我的想法。

第一步,读写分离、缓存。

至少把系统分为2大块,满足日常使用的读业务单独抽取出来,用于承接外部的最大流量。

单独抽出一个子系统负责业务的更新,比如接种信息的更新、核酸信息的变化、或者根据业务定时变更码的颜色。

同时针对用户大量的单查询,上缓存系统,优先读取缓存系统的信息,防止压垮后面的数据库。

第二步,分库分表、服务拆分。

其实用户和用户之间的单个查询是没有关系的,完全可以根据用户的属性做分库分表。

比如就用用户ID取模分64个表,甚至可以分成64个子系统来查询,在接口最前端将流量分发掉,减轻单个表或者服务压力。

上面分析没有及时扩容,可能就是没有做服务拆分,如果都是单个的业务子服务的话,遇到过载的问题很容易做扩容。

当然,如果条件合适的话,上微服务架构就更好了,有一套解决方案来处理类似的问题。

第三步,大数据系统、容灾。

如果在一个页面中展示很多信息,还有一个技术方案,就是通过异步的数据清洗,整合到 nosql 的一张大表中。

用户扫描查询等相关业务,直接走 nosql 数据库即可。

这样处理的好处是,哪怕更新业务完全挂了,也不会影响用户扫码查询,因为两套系统、数据库都是完全分开的。

使用异地双机房等形式部署服务,同时做好整体的容灾、备灾方案,避免出现极端情况,比如机房光缆挖断等。

还有很多细节上的优化,这里就不一一说明了,这里也只是我的一些想法,欢迎大家留言补充。

6.最后

不管怎么分析,这肯定是人祸而不是天灾。

系统在没有经过严格测试之下,就直接投入到生产,在强度稍微大一点的环境中就崩溃了。

比西安大的城市很多,比西安现在疫情还要严重的情况,其它城市也遇到过,怎么没有出现类似的问题?

西安做为一个科技大省,出现这样的问题真的不应该,特别是我看了这个小程序背后使用的域名地址之后。

有一种无力吐槽的感觉,虽然说这和程序使用没有关系,但是从细节真的可以看出一个技术团队的实力。

希望这次能够吸取教训,避免再次出现类似的问题!

作者:纯洁的微笑
出处:https://www.cnblogs.com/ityouknow/p/15719395.html

收起阅读 »

一个命名引发的性能问题

故事背景我最近主要在定位、解决当前项目中的一些性能相关问题。在反馈的问题中,比较严重的问题之一是在户型预览编辑过程,电脑的 CPU 占用率高,及时什么都不做的情况下,CPU 占用也非常的高。同样的,利用 Chrome 提供的 Performance 录制 ⏺ ...
继续阅读 »



故事背景

我最近主要在定位、解决当前项目中的一些性能相关问题。

在反馈的问题中,比较严重的问题之一是在户型预览编辑过程,电脑的 CPU 占用率高,及时什么都不做的情况下,CPU 占用也非常的高。

同样的,利用 Chrome 提供的 Performance 录制 ⏺ 了无任何操作的 JavaScript 调用火焰图,发现 Pixi 内部会利用浏览器的requestAnimationFrame接口,执行自身的 render 方法进行 2D 场景的绘制渲染。

CPU占用率居高不下

发现问题

初步定位,CPU 占用只可能与 2D 场景中 PIXI 的 render() 有关系,使用 Performance 分析事件调用过程中发现,每次在调用需要使用 9ms 的时间

每次Render使用9ms

这个时间是否属于正常的时间范畴呢?

在了解了 PIXI 及绝大部分图形框架后得知,图形框架内部在调用 Render 的过程中其实正常的不会任何过多的计算内容,所有使用到的需要计算生成 Graphics 的地方,都只会生成一遍。

在之后的调用过程中,都会拿到之前生成好的 Buffer 直接进入 Renderer 进行下一帧的渲染,render() 大部分情况下都是在执行脏检查,有任意 Graphics 需要更新时,Graphicsdirty就会被更新,然后重新生成渲染 Buffer

所以,了解了render()方法的作用后,可以确定在静止不动时render()只是执行一些递归判断,不会耗费9ms这么长的时间,这其中定有蹊跷!

在查看到最终调用到的方法部分发现,在 PIXI 内部的render()方法中竟然会调用到 vue 的方法。

这样不正常的方法调用让我立刻想起来 vue 中,对data()返回的对象数据做原型链的改写。以及之前看到过的一篇文章:《一个 Vue 引发的性能问题》

之前在看到这篇文章时,只是觉得是一个非常有意思的案例,虽然结局办法并不见得是最优方法。但发现问题的过程非常有价值,原本打算拿到组里来进行分享。没想到报应不爽,这么快就发现我们的项目也存着这一个这样的问题,且影响程度远远大于这篇文章!

所以立马去检查了,2D 与 3D 场景实例化过程中对场景的定义,发现一个并没有对命名做_$的规范处理,这样会导致 scene2D 中的所有对象,都将会被 vue 处理为 vue 的可观察对象,也就是会在原型链中带入 getter/setter。

export default {
 data() {
   return {
     scene2d: null
  };
},
 created() {
   this.initScene2D();
},
 methods: {
   initScene2D() {
     this.scene2d = Scene2D.getInstance();
  }
}
};

解决办法

查看到 vue 的官方文档解释:

vue官方解释

所以,为了解决这个问题,需要对data()中不希望 vue 挂载原型链实现数据响应的对象做好命名规范处理。

export default {
 data() {
   return {
     $_scene2d: null
  };
},
 created() {
   this.initScene2D();
},
 methods: {
   initScene2D() {
     this.$_scene2d = Scene2D.getInstance();
  }
}
};

对,就这么简单!

在修改了Scene2D在 Vue 组件的命名后,从整体体验感受来讲“轻快”了许多,再次查看 CPU 及内存占用率都降了许多,render()时间的调用降低到0.94ms,对比如图:

Before

After

作者:Yee Wang
来源:https://yeee.wang/posts/42c7.html

收起阅读 »

如何接"地气"的接入微前端

前言微前端,这个概念已经在国内不止一次的登上各大热门话题,它所解决的问题也很明显,这几个微前端所提到的痛点在我们团队所维护的项目中也是非常凸显。但我始终认为,一个新的技术、浪潮,每每被讨论最热门的一定是他背后所代表的杰出思考。“微前端就是…xx 框架,xx 技...
继续阅读 »



前言

微前端,这个概念已经在国内不止一次的登上各大热门话题,它所解决的问题也很明显,这几个微前端所提到的痛点在我们团队所维护的项目中也是非常凸显。

但我始终认为,一个新的技术、浪潮,每每被讨论最热门的一定是他背后所代表的杰出思考。

“微前端就是…xx 框架,xx 技术”

这种话就有点把这种杰出的思路说的局限了,我只能认为他是外行人,来蹭这个词的热度。

在我所负责的项目和团队中,已经有非常大的存量技术栈和页面已经在线上运行,任何迭代升级都必须要保证小心翼翼,万无一失。

可以说,从一定程度来讲,微前端所带来的这些好处是从用户体验和技术维护方面的,对业务的价值并不能量化体现,落地这项技术秉着既要也要还要的指导方针。

我们对存量技术栈一定需要保持敬畏,隔离,影响范围可控的几个基本要素,然后再考虑落地实施微前端方案。

所以,在这个基本要素和指导方针下。要落地这项新的技术时,一定充分充分了解,当前改造站点所存在的技术方案、占比 以及 当前成熟微前端框架已提供的能力差异,切勿生搬硬套。

背景

我所在团队维护的项目都是些 PC 操作后台(Workstation),这些工作台会存在不同的国家,不同时区,不同合作方等等问题。

如果需要开发一个新的页面需求,很可能投入进来的开发人员都来自不同团队,此时我们要在完成现有需求的同时还需要保证多个管理页面的风格统一,设计规范统一,组件统一,交互行为统一这非常困难。

当该业务需要迁移到另外一个工作台时,虽然需要保持逻辑一致,但导航栏、主题等却不同。

当前存量的方案都是采用 Java 直接进行 Template 渲染出 HTML,经过前面几代前辈的迭代,不同系统中已经存在几种不同技术栈产出的页面。

虽然都是 React 来实现的,但是前辈们都非常能折腾,没有一个是按照常规 React 组件形式开发出来的。

比如:

  1. 大部分页面是通过一份 JSON 配置,消费组件生成的页面。

  2. 部分页面是通过另外一个团队定义的 JSON 配置消费组件生成的,与上面 JSON 完全不一样。

  3. 还有一部分页面,是通过一套页面发布平台提供的 JS Bundle 加载出来的。

面对这样的技术背景下,除了微笑的喊 MMP,含泪说着自己听不懂的话(存在即合理,不难要你干吗?),还得接地气出这样一个落地方案。

方案 & 流程图

首先,需要明确的分析出站点所有页面,所需要加载的通用特性:

上述是精简过后的一些通用功能特性,这里简单做下介绍:

  • Layout Loader 用于加载不同工作台的导航

  • DADA Loader 用于加载 JSON 配置的页面

  • Source Code Loader 用于加载 JS Bundle

  • Micro Loader 用于处理微前端加载

  • Log Report 用于日志埋点

  • Time Zone 用于切换时区

  • i18n 用于切换多语言

  • Guider 用于统一管控用户引导

除此以外可能还会存在以下这些页面扩展能力:

  • 安全监控

  • 流量管控

  • 弹窗管控

  • 问卷调查

  • 答疑机器人

粗略统一归类后来看,页面的大体加载流程应该是这样:

实现细则

基于上述一个加载思路,首先需要做的是页面加载路径收口,需要保证所有页面的加载入口是在一个统一的 Loader 下,然后才可以较为系统的处理所有页面的加载生命周期。

在收敛的同时,同样需要保持开放,对核心加载路径要保持插件化开放,随时支持不同的扩展能力,渲染技术栈接入。

插件机制

所以,在主路径上,通过 Loader 加载配置进行处理,这份配置在主路径中提供上下文,然后交由插件进行消费,如图所示:

举个例子,拿一个独立的 JS Bundle 类型的子应用来说:

<div id="root"></div>
<script src="https://cdn.address/schema-resolver/index.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/layout.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/source-code.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/micro-loader.js"></script>
<script src="https://cdn.address/schema-resolver/plugin/i18n.js"></script>

<script>
 SchemaResolver.render(
  {
     micro: true,
     host: "dev.address",
     hfType: "layout1",
     externals: ["//{HOST}/theme1/index.css"],
     // host is cdn prefix, the resource maybe in different env & country
     resource: {
       js: "/index.js",
       css: "/index.css",
    },
  },
  { container: document.querySelector("#root") }
);
</script>

通过上述的 Plugin 引入,即可开启和消费不同的配置。

这里引入了 Layout Plugin,该插件会消费 hfType 字段然后去加载对于的 Layout 资源提供 Container 交给下一个环节。

按照配置,当前页面开启了微前端,那么 Micro Loader 将会消费提供下来的 Container,然后建立沙箱(这里基于 qiankun),再提供 Container 出来。

最后交由 SourceCode Plugin 进行 Bundle 加载运行和渲染。如果这里是另外一种渲染协议或者技术栈,则可以根据不同配置交由不同插件消费 Container。

这个过程中,每个环节的插件是不依赖的,可插拔的

比如:

如果不加载 Layout Plugin 将不会消费 hfType 字段,也就不会将Layout插件逻辑注入到getContainer方法中,那么将直接得到由最外层下穿的Container进行渲染,也就不会有菜单相关透出。

如果不加载 Micro Plugin 同样不会有微前端的逻辑注入,也就不会建立沙箱,那么页面渲染流程将会按照常规模式继续运行。

当前SchemaResolver已经支持的插件有以下几种,详情参考 SchemaResolver

  • MicroLoader – Base on qiankun using this plug-in can make your content loaded through a micro application so that your content can use all the features of the Micro-Front-End.

  • DadaLoader – Use this plugin can make your app render content by Dada.

  • SourceCodeLoader – Use this plugin can load your js\css bundle to render content, our bundle standard is same as qiankun. You can quick start developing your own page through our toolkit lzd-toolkit-asc.

  • LayoutLoader – Use this plugin can make your page load layout(menu), you can use different hfType configuration to switch different layouts.

  • i18n – Use this plugin can make your page have multi-lang. schema.locale will be the mapping of multilingual keys in MCMS. The plugin will inject and register the language automatically.

  • APlus – Use this plugin can make your page have the feature of APlus .Statistics page interaction events, such as pv\uv. With DadaLoader you can even see every module data(click pv, exposed pv) in pages.

  • WalkThrough – Use this plugin can make your page have the feature of Walk Through. One-stop page features guide.

SchemaResolver的插件能力采用plugin-decorator,如要了解更多插件设计思路可以参考:为你的JavaScript库提供插件能力

SchemaResolver plugin feature is base on plugin-decorator. It’s very easy to develop a new plugin.

More information about plugin can read this article Provide plugin capabilities for your JavaScript library

安全迁移

对于我所在团队负责的项目来说,万万做不得一刀切的方案,所以针对现有存量页面,需要完整分析当前存量技术栈:

针对上述存量页面来说,需要从左到右分批进行页面级别控制上线部署,对于左侧部分页面甚至需要做些项目改造后才可部署接入上线。

这类迁移测试需要处理出一套 自动化e2e测试 流程,通过分批迁移同时梳理出 微前端注册表

有了这两项流程保证及范围控制,当前方案所上线内容完全可控,剩下要处理的大部分就是较为重复的体力活了,覆盖率也可量化。

微前端形态

按照上述方案迁移,那么预期的微前端形态将会是:

  1. 每个开启微前端的页面都可成为主应用

  2. 微前端是插件可选项,如果因为微前端导致的业务异常可随时关闭

  3. 同为微前端的页面路由相互之间切换可实现局部刷新形态,而跳转至非微前端注册表中的页面则会直接页面跳转。随着微前端页面覆盖率提高,局部刷新的覆盖率也会逐渐提高

  4. 可通过不同扩展插件,加载不同技术栈类型的存量页面,转换为对应子应用

在SchemaResolver中的注册和调用路径如下:

总结

透过技术看本质,微前端所代表的杰出思维,才是真正解决具体问题关键所在,只有解决了具体的业务问题,这项技术才有价值转换。

不要为了微前端做微前端,不要为了小程序做小程序。

当前,通过 SchemaResolver,可以针对不同角色提供不同的开放能力:

  • 针对平台管理员,提供插件能力开放全局扩展能力

  • 针对页面开发者,提供标准化接入方案路径,提供多种技术栈接入能力,并无感知提供微前端,多语言,埋点,菜单,主题加载等能力。解耦了不同系统公共能力,同时,这种方式可以让页面开发者快速将具体业务逻辑迁移到其他平台。

  • 针对第三方接入者,不需要关心了解系统菜单、主题接入方式,提供统一的接入口径,通过微前端隔离技术栈、隔离子应用样式。最后通过统一的页面系统管控,轻松入住对应平台,同时可以全局看到站点页面情况。

作者:Yee Wang
来源:https://yeee.wang/posts/3469.html

收起阅读 »

为你的JavaScript库提供插件能力

前言最近在做一个中台框架的设计开发,在做了主框架的基础能力后,思考在框架落实真实业务需求过程中,需要对主框架功能有非常多的定制化内容存在。如果在主体框架中做了哪怕一点业务改动,都可能会对后面的拓展性及灵活性有所限制。所以为了让主体框架做的更加灵活、扩展性更搞,...
继续阅读 »



前言

最近在做一个中台框架的设计开发,在做了主框架的基础能力后,思考在框架落实真实业务需求过程中,需要对主框架功能有非常多的定制化内容存在。如果在主体框架中做了哪怕一点业务改动,都可能会对后面的拓展性及灵活性有所限制。

所以为了让主体框架做的更加灵活、扩展性更搞,在主框架有了基础能力后,就不再对主框架做任何非主框架能力的业务功能开发。

要为主框架不断的”开槽”

其实在很多前端库中都有类似的设计,才能够让更多的开发者参与进来,完成各种各样的社区驱动开发。比如:WebpackBabelHexoVuePress等。

那么如何为自己的项目开槽,做插件呢?

调研

在了解了很多插件的项目源码后,发现实现大多大同小异,主要分为以下几个步骤:

  1. 为框架开发安装插件能力,插件容器

  2. 暴露主要生命运行周期节点方法(开槽)

  3. 编写注入业务插件代码

这些框架都是在自己实现一套这样的插件工具,几乎和业务强相关,不好拆离。或者做了一个改写方法的工具类。总体比较离散,不好直接拿来即用。

另外在实现方式上,大部分的插件实现是直接改写某方法,为他在运行时多加一个Wrap,来依次运行外部插件的逻辑代码。

// main.js
const main = {
 loadData:()=>{},
 render:()=>{}
}

// plugin1.js
const plugin1 = {
 render:()=>{}
}

// install.js
const install = (main, plugin) => {
 main.render = ()=>{
   plugin1.render()
   main.render()
}
}

在上述代码中的插件有几个明显的问题:

  • plugin1 无法控制 render() 的顺序

  • main 中无法得知什么函数可能会被插件改写,什么函数不会被插件改写

  • 如果按照模块文件拆分,团队成员中根本不知道 main.js 中的函数修改会存在风险,因为压根看不到 install.js 中的代码

那么后来,为了解决这些问题,可能会变成这样:

const component = {
 hooks:{
   componentWillMounted(){},
   componentDidMounted(){}
},
 mounte(){
   this.hooks.componentWillMounted();
   //... main code
   this.hooks.componentDidMounted();
}
}

const plugin = {
 componentWillMounted(){
   //...
},
 componentDidMounted(){
   //...
}
}

// install.js
const install = (main, plugin) => {
 // 忽略实现细节。
 main.hooks.componentWillMounted = ()=>{
   plugin1.componentWillMounted()
   main.hook.componentWillMounted()
}
 main.hooks.componentDidMounted = ()=>{
   plugin1.componentDidMounted()
   main.hook.componentDidMounted()
}
}

另外,还有一种解法,会给插件中给 next 方法,如下:

// main.js
const main = {
 loadData:()=>{},
 render:()=>{}
}

// plugin1.js
const plugin1 = {
 render:next=>{
   // run some thing before
   next();
   // run some thing after
}
}

// install.js
const install = (main, plugin) => {
 main.render = ()=>{
   plugin1.render(main.render)
}
}

如上,从调研结构来看,虽然都实现了对应功能,但是从实现过程来看,有几个比较明显的问题:

  • 对原函数侵入性修改过多

  • 对方法rewrite操作过多,太hack

  • 对TypeScript不友好

  • 多成员协作不友好

  • 对原函数操作不够灵活,不能修改原函数的入参出参

开搞

在调研了很多框架的实现方案的后,我希望以后我自己的插件库可以使用一个装饰器完成开槽,在插件类中通过一个装饰器完成注入,可以像这样使用和开发:

import { Hook, Inject, PluginTarget, Plugin } from 'plugin-decorator';

class DemoTarget extends PluginTarget {
 @Hook
 public method1() {
   console.log('origin method');
}
}

class DemoPlugin extends Plugin {
 @Inject
 public method1(next) {
   next();
   console.log('plugin method');
}
}

const demoTarget = new DemoTarget();
demoTarget.install(new DemoPlugin());
demoTarget.method1();

// => origin method
// => plugin method

Decorator

并且可以支持对原函数的入参出参做装饰修改,如下:

import { Hook, Inject, PluginTarget, Plugin } from 'plugin-decorator';

class DemoTarget extends PluginTarget {
 @Hook
 public method1(name:string) {
   return `origin method ${name}`;
}
}

class DemoPlugin extends Plugin {
 @Inject
 public method1(next, name) {
   return `plugin ${next(name)}`;
}
}

const demoTarget = new DemoTarget();
demoTarget.install(new DemoPlugin());

console.log(demoTarget.method1('cool'));

// => plugin origin method cool

Promise

当然,如果原函数是一个Promise的函数,那插件也应该支持Promise了!如下:

import { Hook, Inject, PluginTarget, Plugin } from 'plugin-decorator';

class DemoTarget extends PluginTarget {
 @Hook
 public methodPromise() {
   return new Promise(resolve => {
     setTimeout(() => resolve('origin method'), 1000);
  });
}
}

class DemoPlugin extends Plugin {
 @Inject
 public async methodPromise(next) {
   return `plugin ${await next()}`;
}
}

const demoTarget = new DemoTarget();
demoTarget.install(new DemoPlugin());

demoTarget.methodPromise().then(console.log);

// => Promise<plugin origin method>

Duang!

最终,我完成了这个库的开发:plugin-decorator

GitHub: 地址

没错,我就知道你会点Star,毕竟你这么帅气、高大、威猛、酷炫、大佬!

总结

在该项目中,另外值得提的一点是,该项目是我在开发自己的一套中台框架中临时抽出来的一个工具库。

在工具库中采用了:

  • TypeScript

  • Ava Unit Test

  • Nyc

  • Typedoc

整体开发过程是先写测试用例,然后再按照测试用例进行开发,也就是传说中的 TDD(Test Drive Development)。

感觉这种方式,至少在我做库的抽离过程中,非常棒,整体开发流程非常高效,目的清晰。

在库的编译搭建中使用了 typescript-starter 这个库,为我节省了不少搭建项目的时间!

作者:Yee Wang
来源:https://yeee.wang/posts/dfa4.html

收起阅读 »

轻松生成小程序分享海报

小程序海报组件github.com/jasondu/wxa…需求小程序分享到朋友圈只能使用小程序码海报来实现,生成小程序码的方式有两种,一种是使用后端方式,一种是使用小程序自带的canvas生成;后端的方式开发难度大,由于生成图片耗用内存比较大对服务端也是不小...
继续阅读 »



小程序海报组件

github.com/jasondu/wxa…

需求

小程序分享到朋友圈只能使用小程序码海报来实现,生成小程序码的方式有两种,一种是使用后端方式,一种是使用小程序自带的canvas生成;后端的方式开发难度大,由于生成图片耗用内存比较大对服务端也是不小的压力;所以使用小程序的canvas是一个不错的选择,但由于canvas水比较深,坑比较多,还有不同海报需要重现写渲染流程,导致代码冗余难以维护,加上不同设备版本的情况不一样,因此小程序海报生成组件的需求十分迫切。

在实际开发中,我发现海报中的元素无非一下几种,只要实现这几种,就可以通过一份配置文件生成各种各样的海报了。

海报中的元素分类

要解决的问题

  • 单位问题

  • canvas隐藏问题

  • 圆角矩形、圆角图片

  • 多段文字

  • 超长文字和多行文字缩略问题

  • 矩形包含文字

  • 多个元素间的层级问题

  • 图片尺寸和渲染尺寸不一致问题

  • canvas转图片

  • IOS 6.6.7 clip问题

  • 关于获取canvas实例

单位问题

canvas绘制使用的是px单位,但不同设备的px是需要换算的,所以在组件中统一使用rpx单位,这里就涉及到单位怎么换算问题。

通过wx.getSystemInfoSync获取设备屏幕尺寸,从而得到比例,进而做转换,代码如下:

const sysInfo = wx.getSystemInfoSync();
const screenWidth = sysInfo.screenWidth;
this.factor = screenWidth / 750; // 获取比例
function toPx(rpx) { // rpx转px
return rpx * this.factor;
}
function toRpx(px) { // px转rpx
return px / this.factor;
},

canvas隐藏问题

在绘制海报过程时,我们不想让用户看到canvas,所以我们必须把canvas隐藏起来,一开始想到的是使用display:none; 但这样在转化成图片时会空白,所以这个是行不通的,所以只能控制canvas的绝对定位,将其移出可视界面,代码如下:

.canvas.pro {
  position: absolute;
  bottom: 0;
  left: -9999rpx;
}

圆角矩形、圆角图片

由于canvas没有提供现成的圆角api,所以我们只能手工画啦,实际上圆角矩形就是由4条线(黄色)和4个圆弧(红色)组成的,如下:

圆弧可以使用canvasContext.arcTo这个api实现,这个api的入参由两个控制点一个半径组成,对应上图的示例

canvasContext.arcTo(x1, y1, x2, y2, r)

接下来我们就可以非常轻松的写出生成圆角矩形的函数啦

/**
* 画圆角矩形
*/
_drawRadiusRect(x, y, w, h, r) {
  const br = r / 2;
  this.ctx.beginPath();
  this.ctx.moveTo(this.toPx(x + br), this.toPx(y));   // 移动到左上角的点
  this.ctx.lineTo(this.toPx(x + w - br), this.toPx(y)); // 画上边的线
  this.ctx.arcTo(this.toPx(x + w), this.toPx(y), this.toPx(x + w), this.toPx(y + br), this.toPx(br)); // 画右上角的弧
  this.ctx.lineTo(this.toPx(x + w), this.toPx(y + h - br)); // 画右边的线
  this.ctx.arcTo(this.toPx(x + w), this.toPx(y + h), this.toPx(x + w - br), this.toPx(y + h), this.toPx(br)); // 画右下角的弧
  this.ctx.lineTo(this.toPx(x + br), this.toPx(y + h)); // 画下边的线
  this.ctx.arcTo(this.toPx(x), this.toPx(y + h), this.toPx(x), this.toPx(y + h - br), this.toPx(br)); // 画左下角的弧
  this.ctx.lineTo(this.toPx(x), this.toPx(y + br)); // 画左边的线
  this.ctx.arcTo(this.toPx(x), this.toPx(y), this.toPx(x + br), this.toPx(y), this.toPx(br)); // 画左上角的弧
}

如果是画线框就使用this.ctx.stroke();

如果是画色块就使用this.ctx.fill();

如果是圆角图片就使用

this.ctx.clip();
this.ctx.drawImage(***);

clip() 方法从原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内(不能访问画布上的其他区域)。可以在使用 clip() 方法前通过使用 save() 方法对当前画布区域进行保存,并在以后的任意时间对其进行恢复(通过 restore() 方法)。

多段文字

如果是连续多段不同格式的文字,如果让用户每段文字都指定坐标是不现实的,因为上一段文字的长度是不固定的,这里的解决方案是使用ctx.measureText (基础库 1.9.90 开始支持)Api来计算一段文字的宽度,记住这里返回宽度的单位是px(),从而知道下一段文字的坐标。

超长文字和多行文字缩略问题

设置文字的宽度,通过ctx.measureText知道文字的宽度,如果超出设定的宽度,超出部分使用“...”代替;对于多行文字,经测试发现字体的高度大约等于字体大小,并提供lineHeight参数让用户可以自定义行高,这样我们就可以知道下一行的y轴坐标了。

矩形包含文字

这个同样使用ctx.measureText接口,从而控制矩形的宽度,当然这里用户还可以设置paddingLeft和paddingRight字段;

文字的垂直居中问题可以设置文字的基线对齐方式为middle(this.ctx.setTextBaseline('middle');),设置文字的坐标为矩形的中线就可以了;水平居中this.ctx.setTextAlign('center');;

多个元素间的层级问题

由于canvas没有Api可以设置绘制元素的层级,只能是根据后绘制层级高于前面绘制的方式,所以需要用户传入zIndex字段,利用数组排序(Array.prototype.sort)后再根据顺序绘制。

图片尺寸和渲染尺寸不一致问题

绘制图片我们使用ctx.drawImage()API;

如果使用drawImage(dx, dy, dWidth, dHeight),图片会压缩尺寸以适应绘制的尺寸,图片会变形,如下图:

在基础库1.9.0起支持drawImage(sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight),sx和sy是源图像的矩形选择框左上角的坐标,sWidth和sHeight是源图像的矩形选择框的宽度和高度,如下图:

如果绘制尺寸比源图尺寸宽,那么绘制尺寸的宽度就等于源图宽度;反之,绘制尺寸比源图尺寸高,那么绘制尺寸的高度等于源图高度;

我们可以通过wx.getImageInfoApi获取源图的尺寸;

canvas转图片

在canvas绘制完成后调用wx.canvasToTempFilePathApi将canvas转为图片输出,这样需要注意,wx.canvasToTempFilePath需要写在this.ctx.draw的回调中,并且在组件中使用这个接口需要在第二个入参传入this(),如下

this.ctx.draw(false, () => {
  wx.canvasToTempFilePath({
      canvasId: 'canvasid',
      success: (res) => {
          wx.hideLoading();
          this.triggerEvent('success', res.tempFilePath);
      },
      fail: (err) => {
          wx.hideLoading();
          this.triggerEvent('fail', err);
      }
  }, this);
});

IOS 6.6.7 clip问题

在IOS 6.6.7版本中clip方法连续裁剪图片时,只有第一张有效,这是微信的bug,官方也证实了(developers.weixin.qq.com/community/d…

关于获取canvas实例

我们可以使用wx.createCanvasContext获取小程序实例,但在组件中使用切记第二个参数需要带上this,如下

this.ctx = wx.createCanvasContext('canvasid', this);

如何使用组件

github.com/jasondu/wxa…

作者:jasondu41833
来源:https://juejin.cn/post/6844903663840788493

收起阅读 »

利用好 git bisect 这把利器,帮助你快速定位疑难 bug

Git
使用git bisect二分法定位问题的基本步骤:git bisect start [最近的出错的commitid] [较远的正确的commitid]测试相应的功能git bisect good 标记正确直到出现问题则 标记错误 git bisect bad提...
继续阅读 »

使用git bisect二分法定位问题的基本步骤:

  1. git bisect start [最近的出错的commitid] [较远的正确的commitid]

  2. 测试相应的功能

  3. git bisect good 标记正确

  4. 直到出现问题则 标记错误 git bisect bad

  5. 提示的commitid就是导致问题的那次提交

问题描述

我们以Vue DevUI组件库的一个bug举例子🌰

5d14c34b这一次commit,执行yarn build报错,报错信息如下:

✓ building client + server bundles...
✖ rendering pages...
build error:
ReferenceError: document is not defined

我可以确定的是上一次发版本(d577ce4)是可以build成功的。

git bisect 简介

git bisect命令使用二分搜索算法来查找提交历史中的哪一次提交引入了错误。它几乎能让你闭着眼睛快速定位任何源码导致的问题,非常实用。

你只需要告诉这个命令一个包含该bug的坏commit ID和一个引入该bug之前的好commit ID,这个命令会用二分法在这两个提交之间选择一个中间的commit ID,切换到那个commit ID的代码,然后询问你这是好的commit ID还是坏的commit ID,你告诉它是好还是坏,然后它会不断缩小范围,直到找到那次引入bug的凶手commit ID

这样我们就只需要分析那一次提交的代码,就能快速定位和解决这个bug(具体定位的时间取决于该次提交的代码量和你的经验),所以我们提交代码时一定要养成小批量提交的习惯,每次只提交一个小的独立功能,这样出问题了,定位起来会非常快。

接下来我就以Vue DevUI之前出现过的一个bug为例,详细介绍下如何使用git bisect这把利器。

定位过程

git bisect start 5d14c34b d577ce4
or
git bisect start HEAD d577ce4

其中5d14c34b这次是最近出现的有bug的提交,d577ce4这个是上一次发版本没问题的提交。

执行完启动bisect之后,马上就切到中间的一次提交啦,以下是打印结果:

kagol:vue-devui kagol$ git bisect start 5d14c34b d577ce4
Bisecting: 11 revisions left to test after this (roughly 4 steps)
[1cfafaaa58e03850e0c9ddc4246ae40d18b03d71] fix: read-tip icon样式泄露 (#54)

可以看到已经切到以下提交:

[1cfafaaa] fix: read-tip icon样式泄露 (#54)

执行命令:

yarn build

构建成功,所以标记下good

git bisect good
kagol:vue-devui kagol$ git bisect good
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[c0c4cc1a25c5c6967b85100ee8ac636d90eff4b0] feat(drawer): add service model (#27)

标记万good,马上又通过二分法,切到了一次新的提交:

[c0c4cc1a] feat(drawer): add service model (#27)

再次执行build命令:

yarn build

build失败了,出现了我们最早遇到的报错:

✓ building client + server bundles...
✖ rendering pages...
build error:
ReferenceError: document is not defined

标记下bad,再一次切到中间的提交:

kagol:vue-devui kagol$ git bisect bad
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[86634fd8efd2b808811835e7cb7ca80bc2904795] feat: add scss preprocessor in docs && fix:(Toast) single lifeMode bug in Toast

以此类推,不断地验证、标记、验证、标记...最终会提示我们那一次提交导致了这次的bug,提交者、提交时间、提交message等信息。

kagol:vue-devui kagol$ git bisect good
c0c4cc1a25c5c6967b85100ee8ac636d90eff4b0 is the first bad commit
commit c0c4cc1a25c5c6967b85100ee8ac636d90eff4b0
Author: nif <lnzhangsong@163.com>
Date: Sun Dec 26 21:37:05 2021 +0800

feat(drawer): add service model (#27)

* feat(drawer): add service model

* docs(drawer): add service model demo

* fix(drawer): remove 'console.log()'

packages/devui-vue/devui/drawer/index.ts | 7 +++--
.../devui-vue/devui/drawer/src/drawer-service.ts | 33 ++++++++++++++++++++++
packages/devui-vue/devui/drawer/src/drawer.tsx | 3 ++
packages/devui-vue/docs/components/drawer/index.md | 29 +++++++++++++++++++
4 files changed, 69 insertions(+), 3 deletions(-)
create mode 100644 packages/devui-vue/devui/drawer/src/drawer-service.ts

最终定位到出问题的commit:

c0c4cc1a is the first bad commit

github.com/DevCloudFE/…

整个定位过程几乎是机械的操作,不需要了解项目源码,不需要了解最近谁提交了什么内容,只需要无脑地:验证、标记、验证、标记,最后git会告诉我们那一次提交出错。

这么香的工具,赶紧来试试吧!

问题分析

直到哪个commit出问题了,定位起来范围就小了很多。

如果平时提交代码又能很好地遵循小颗粒提交的话,bug呼之欲出。

这里必须表扬下我们DevUI的田主(Contributor)们,他们都养成了小颗粒提交的习惯,这次导致bug的提交c0c4cc1a,只提交了4个文件,涉及70多行代码。

我们在其中搜索下document关键字,发现了两处,都在drawer-service.ts整个文件中:

一处是12行的:

static $body: HTMLElement | null = document.body

另一处是17行的:

this.$div = document.createElement('div')

最终发现罪魁祸首就是12行的代码!

破案!

作者:DevUI团队
来源:https://juejin.cn/post/7046409685561245733

收起阅读 »

前端开发的积木理论——像搭积木一样做前端开发

1 概述用户界面是由一系列组件组合而成,组件将数据和交互封装在内,仅保留必要的接口与其他组件进行通信。在前端开发中,组件就像一个一个的小积木块,我们用这些积木块拼出一个一个页面,这些页面组成了一个完整的为用户提供价值的业务。相信大部分前端工程师都使用过组件库,...
继续阅读 »



1 概述

用户界面是由一系列组件组合而成,组件将数据和交互封装在内,仅保留必要的接口与其他组件进行通信。

在前端开发中,组件就像一个一个的小积木块,我们用这些积木块拼出一个一个页面,这些页面组成了一个完整的为用户提供价值的业务。

相信大部分前端工程师都使用过组件库,比如Ant DesignElementUI,都是我们非常熟悉的组件库,以及我们团队做的DevUI组件库。

组件库就像一个工具箱,里面包含了各式各样奇形怪状、功能各异的组件,我们直接拿这些组件小积木来拼页面,非常方便。

2 界面的基本元素

从抽象的角度来看,任何用户界面都是由组件数据交互组成的。

2.1 组件

组件是一个具有一定功能的独立单元,不同的组件可以组合在一起,构成功能更强大的组件.

组件是网页的器官。

组件的概念要和HTML标签的概念区分开来,HTML标签是网页的基本单元,而组件是基于HTML标签的、包含特定功能的独立单元,可以简单理解为组件是HTML标签的一个超集。

组件内部封装的是数据和交互,对外暴露必要的接口,以与其他组件进行通信。

2.2 数据

用户界面中包含很多数据,有不变的静态数据,也有随时间和交互改变的动态数据,它们大多来自于后台数据库。

组件内部包含数据,组件之间传递的也是数据。

数据是网页的核心。

在前端开发中,数据主要以JSON格式进行存储和传递。

2.3 交互

交互是用户的操作,主要通过鼠标和键盘等计算机外设触发,点击一次按钮、在文本框中输入一些字符、按下回车键等都是交互。

在网页中,所有的交互都是通过事件的方式进行响应的。

交互是网页的灵魂,不能进行交互的网页就像干涸的河流,了无生气。

3 组件的特性

一个设计良好的组件应该包含以下特性:

  • 复用性(Reuseability)

  • 组合性(Composability)

  • 扩展性(Scalability)

3.1 复用性

组件作为一个独立的单元,除了自身的特定功能之外,不应该包含任何业务相关的数据。

组件应该能够复用到任何业务中,只要该业务需要用到组件所包含的功能。

组件应该是资源独立的,以增强组件的复用能力。

3.2 组合性

组件与组件之间可以像积木一样进行组合,组合之后的组件拥有子组件的所有功能,变得更强大。

组合的方式可以是包裹别的组件,也可以是作为参数传入别的组件中。

3.3 扩展性

可以基于现有的组件进行扩展,开发功能更加定制化的组件,以满足业务需求。

组件的可扩展能力依赖于接口的设计,接口要尽可能的灵活,以应对不断变化的需求。

4 组件间通信

4.1 从外向内通信

通过props将数据传递到组件内部,以供组件自身或其子组件使用。

props是不可变的:

  • 这意味着我们无法在组件内部修改props的原始值

  • 也意味着只有外部传入了props,才能在组件内部获取和使用它

4.2 从内向外通信

可以通过两种方式将组件内部的数据传递到组件外:

  • 通过ref属性(组件实例的引用),通过组件的ref属性,可以获取到组件实例的引用,进而获取组件内部的数据和方法

  • 通过事件回调(比如:click),通过事件回调的方式,可以通过props将一个回调函数传递到组件内部,并将组件内部的数据通过回调传递到外部

4.3 双向通信

可以通过全局context的方式进行双向通信。

父组件声明context对象,那么其下所有的子组件,不管嵌套多深,都可以使用该对象的数据,并且可以通过回调函数的方式将子组件的数据传递出来供父组件使用。

4.4 多端通信

通过事件订阅的方式可以实现多个组件之间互相通信。

通过自定义事件(Custom Event),任何组件都可以与其他组件进行通讯,采用的是发布/订阅模式,通过向事件对象上添加监听和触发事件来实现组件间通信。

5 案例

接下来通过广告详情页面的开发来演示如何用积木理论来构建网页。

设计图如下:

5.1 第一步:拆积木

将设计图拆分成有层次的积木结构。

5.1.1 顶层积木

最顶层拆分成四个大积木:

  • Header(头部组件)

  • ChartBlock(图表块组件)

  • DetailBlock(详情块组件)

  • TableBlock(表格块组件)

5.1.2 中间层积木

  • 每个大积木又可以拆分成若干小积木

  • 中间层有些是不可拆分的原子积木,比如:ButtonCheckbox

  • 有些是由原子积木组合而成的复合积木,比如:DateRangePickerTable

层次结构如下:

  • Header

    • Breadcrumb

    • Button

    • DateRangePicker

  • ChartBlock

    • Tabs

    • LineChart

    • BarChart

  • DetailBlock

    • Button

    • List

  • TableBlock

    • Checkbox

    • Select

    • Button

    • Table

5.1.3 底层积木

最底层的积木都是不可拆分的原子积木。

5.2 第二步:造积木

已经将积木的层次结构设计出来,接下来就要考虑每个层次的积木怎么制造的问题。

这一块后面会专门写一个系列文章给大家分享如何自己制造组件。

5.3 第三步:搭积木

5.3.1 顶层积木

对应的代码:

<div class="ad-detail-container">
   <Header />
   <div class="content">
       <div class="chart-detail-block">
           <ChartBlock />
           <DetailBlock />
       </div>
       <TableBlock />
   </div>
</div>

其中的<div>标签是为了布局方便加入的。

5.3.2 中间层积木

Header对应的代码:

<div class="header">
 <div class="breadcrumb-area">
   <div class="breadcrumb-current">gary audio</div>
     <div class="breadcrumb-from">
      From:
       <d-breadcrumb class="breadcrumb" separator="">
         <d-breadcrumb-item href="http://www.qq.com">Campaign List</d-breadcrumb-item>
         <span class="breadcrumb-seprator">> </span>
         <d-breadcrumb-item href="http://www.qq.com">gary audio test</d-breadcrumb-item>
       </d-breadcrumb>
     </div>
   </div>
 </div>
 <div class="operation-area">
   <d-button icon="mail" class="flat" (click)="sendReportEmail()">Send Report Email</d-button>
   <d-date-range-picker (change)="changeDate()" />
 </div>
</div>

需要注意的是:为了方便阐述积木理论的核心思想,这里的原子组件大多都是已经造好的(使用DevUI组件库),也可以选择自己制造,后面会专门写一个系列文章给大家分享如何自己制造组件。

ChartBlock对应的代码:

<div class="chart-block">
   <d-tabs [defaultActiveKey]="1">
       <d-tab-item tab="Ad performance" [key]="1">
           <d-line-chart></d-line-chart>
       </d-tab-item>
       <d-tab-item tab="Audience" [key]="2">
           <d-bar-chart></d-bar-chart>
       </d-tab-item>
   </d-tabs>
</div>

DetailBlock对应的代码:

<div class="detail-block">
   <div class="detail-header">
       <div class="detail-title">Ad detail</div>
       <div class="detail-operation">
           <d-button icon="edit" class="flat" (click)="edit()">Edit</d-button>
           <d-button icon="delete" class="flat" (click)="delete()">Delete</d-button>
           <d-button icon="eye" class="flat" (click)="preview">Preview</d-button>
       </div>
   </div>
   <d-list [data]="adDetail" [config]="detailConfig"></d-list>
</div>

TableBlock对应的代码:

<div class="table-block">
   <div class="table-operation-bar">
       <d-checkbox (change)="changeDelivery()">Has delivery</Checkbox>
       <d-select class="select-table-column" [defaultValue]="1"
           (change)="select()">
           <d-select-option value="1">Performance</d-select-option>
           <d-select-option value="2">Customize</d-select-option>
       </Select>
       <d-button icon="export" (click)="exportData()">Export Data</d-button>
   </div>
   <d-table [dataSource]="adsList" [columns]="columns"></d-table>
</div>

由于篇幅原因,这个案例并没有包含交互的部分,不过基本能够阐述清楚积木理论的核心思想。

作者:DevUI团队
来源:https://juejin.cn/post/7047503485054484516

收起阅读 »

深入理解 redux 数据流和异步过程管理

前端框架的数据流前端框架实现了数据驱动视图变化的功能,我们用 template 或者 jsx 描述好了数据和视图的绑定关系,然后就只需要关心数据的管理了。数据在组件和组件之间、组件和全局 store 之间传递,叫做前端框架的数据流。一般来说,除了某部分状态数据...
继续阅读 »


前端框架的数据流

前端框架实现了数据驱动视图变化的功能,我们用 template 或者 jsx 描述好了数据和视图的绑定关系,然后就只需要关心数据的管理了。

数据在组件和组件之间、组件和全局 store 之间传递,叫做前端框架的数据流。

一般来说,除了某部分状态数据是只有某个组件关心的,我们会把状态数据放在组件内以外,业务数据、多个组件关心的状态数据都会放在 store 里面。组件从 store 中取数据,当交互的时候去通知 store 改变对应的数据。

这个 store 不一定是 redux、mobox 这些第三方库,其实 react 内置的 context 也可以作为 store。但是 context 做为 store 有一个问题,任何组件都能从 context 中取出数据来修改,那么当排查问题的时候就特别困难,因为并不知道是哪个组件把数据改坏的,也就是数据流不清晰。

正是因为这个原因,我们几乎见不到用 context 作为 store,基本都是搭配一个 redux。

所以为什么 redux 好呢?第一个原因就是数据流清晰,改变数据有统一的入口。

组件里都是通过 dispatch 一个 action 来触发 store 的修改,而且修改的逻辑都是在 reducer 里面,组件再监听 store 的数据变化,从中取出最新的数据。

这样数据流动是单向的,清晰的,很容易管理。

这就像为什么我们在公司里想要什么权限都要走审批流,而不是直接找某人,一样的道理。集中管理流程比较清晰,而且还可以追溯。

异步过程的管理

很多情况下改变 store 数据都是一个异步的过程,比如等待网络请求返回数据、定时改变数据、等待某个事件来改变数据等,那这些异步过程的代码放在哪里呢?

组件?

放在组件里是可以,但是异步过程怎么跨组件复用?多个异步过程之间怎么做串行、并行等控制?

所以当异步过程比较多,而且异步过程与异步过程之间也不独立,有串行、并行、甚至更复杂的关系的时候,直接把异步逻辑放组件内不行。

不放组件内,那放哪呢?

redux 提供的中间件机制是不是可以用来放这些异步过程呢?

redux 中间件

先看下什么是 redux 中间件:

redux 的流程很简单,就是 dispatch 一个 action 到 store, reducer 来处理 action。那么如果想在到达 store 之前多做一些处理呢?在哪里加?

改造 dispatch!中间件的原理就是层层包装 dispatch。

下面是 applyMiddleware 的源码,可以看到 applyMiddleware 就是对 store.dispatch 做了层层包装,最后返回修改了 dispatch 之后的 store。

function applyMiddleware(middlewares) {
let dispatch = store.dispatch
middlewares.forEach(middleware =>
dispatch = middleware(store)(dispatch)
)
return { ...store, dispatch}
}

所以说中间件最终返回的函数就是处理 action 的 dispatch:

function middlewareXxx(store) {
return function (next) {
return function (action) {
// xx
};
};
};
}

中间件会包装 dispatch,而 dispatch 就是把 action 传给 store 的,所以中间件自然可以拿到 action、拿到 store,还有被包装的 dispatch,也就是 next。

比如 redux-thunk 中间件的实现:

function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}

return next(action);
};
}

const thunk = createThunkMiddleware();

它判断了如果 action 是一个函数,就执行该函数,并且把 store.dispath 和 store.getState 传进去,否则传给内层的 dispatch。

通过 redux-thunk 中间件,我们可以把异步过程通过函数的形式放在 dispatch 的参数里:

const login = (userName) => (dispatch) => {
dispatch({ type: 'loginStart' })
request.post('/api/login', { data: userName }, () => {
dispatch({ type: 'loginSuccess', payload: userName })
})
}
store.dispatch(login('guang'))

但是这样解决了组件里的异步过程不好复用、多个异步过程之间不好做并行、串行等控制的问题了么?

没有,这段逻辑依然是在组件里写,只不过移到了 dispatch 里,也没有提供多个异步过程的管理机制。

解决这个问题,需要用 redux-saga 或 redux-observable 中间件。

redux-saga

redux-saga 并没有改变 action,它会把 action 透传给 store,只是多加了一条异步过程的处理。

redux-saga 中间件是这样启用的:

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import rootReducer from './reducer'
import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = createStore(rootReducer, {}, applyMiddleware(sagaMiddleware))
sagaMiddleware.run(rootSaga)

要调用 run 把 saga 的 watcher saga 跑起来:

watcher saga 里面监听了一些 action,然后调用 worker saga 来处理:

import { all, takeLatest } from 'redux-saga/effects'

function* rootSaga() {
yield all([
takeLatest('login', login),
takeLatest('logout', logout)
])
}
export default rootSaga

redux-saga 会先把 action 透传给 store,然后判断下该 action 是否是被 taker 监听的:

function sagaMiddleware({ getState, dispatch }) {
return function (next) {
return function (action) {
const result = next(action);// 把 action 透传给 store

channel.put(action); //触发 saga 的 action 监听流程

return result;
}
}
}

当发现该 action 是被监听的,那么就执行相应的 taker,调用 worker saga 来处理:

function* login(action) {
try {
const loginInfo = yield call(loginService, action.account)
yield put({ type: 'loginSuccess', loginInfo })
} catch (error) {
yield put({ type: 'loginError', error })
}
}

function* logout() {
yield put({ type: 'logoutSuccess'})
}

比如 login 和 logout 会有不同的 worker saga。

login 会请求 login 接口,然后触发 loginSuccess 或者 loginError 的 action。

logout 会触发 logoutSuccess 的 action。

redux saga 的异步过程管理就是这样的:先把 action 透传给 store,然后判断 action 是否是被 taker 监听的,如果是,则调用对应的 worker saga 进行处理。

redux saga 在 redux 的 action 流程之外,加了一条监听 action 的异步处理的流程。

其实整个流程还是比较容易理解的。理解成本高一点的就是 generator 的写法了:

比如下面这段代码:

function* xxxSaga() {
while(true) {
yield take('xxx_action');
//...
}
}

它就是对每一个监听到的 xxx_action 做同样的处理的意思,相当于 takeEvery:

function* xxxSaga() {
yield takeEvery('xxx_action');
//...
}

但是因为有一个 while(true),很多同学就不理解了,这不是死循环了么?

不是的。generator 执行后返回的是一个 iterator,需要另外一个程序调用 next 方法才会继续执行。所以怎么执行、是否继续执行都是由另一个程序控制的。

在 redux-saga 里面,控制 worker saga 执行的程序叫做 task。worker saga 只是告诉了 task 应该做什么处理,通过 call、fork、put 这些命令(这些命令叫做 effect)。

然后 task 会调用不同的实现函数来执行该 worker saga。

为什么要这样设计呢?直接执行不就行了,为啥要拆成 worker saga 和 task 两部分,这样理解成本不就高了么?

确实,设计成 generator 的形式会增加理解成本,但是换来的是可测试性。因为各种副作用,比如网络请求、dispatch action 到 store 等等,都变成了 call、put 等 effect,由 task 部分控制执行。那么具体怎么执行的就可以随意的切换了,这样测试的时候只需要模拟传入对应的数据,就可以测试 worker saga 了。

redux saga 设计成 generator 的形式是一种学习成本和可测试性的权衡。

还记得 redux-thunk 有啥问题么?多个异步过程之间的并行、串行的复杂关系没法处理。那 redux-saga 是怎么解决的呢?

redux-saga 提供了 all、race、takeEvery、takeLatest 等 effect 来指定多个异步过程的关系:

比如 takeEvery 会对多个 action 的每一个做同样的处理,takeLatest 会对多个 action 的最后一个做处理,race 会只返回最快的那个异步过程的结果,等等。

这些控制多个异步过程之间关系的 effect 正是 redux-thunk 所没有的,也是复杂异步过程的管理必不可少的部分。

所以 redux-saga 可以做复杂异步过程的管理,而且具有很好的可测试性。

其实异步过程的管理,最出名的是 rxjs,而 redux-observable 就是基于 rxjs 实现的,它也是一种复杂异步过程管理的方案。

redux-observable

redux-observable 用起来和 redux-saga 特别像,比如启用插件的部分:

const epicMiddleware = createEpicMiddleware();

const store = createStore(
rootReducer,
applyMiddleware(epicMiddleware)
);

epicMiddleware.run(rootEpic);

和 redux saga 的启动流程是一样的,只是不叫 saga 而叫 epic。

但是对异步过程的处理,redux saga 是自己提供了一些 effect,而 redux-observable 是利用了 rxjs 的 operator:

import { ajax } from 'rxjs/ajax';

const fetchUserEpic = (action$, state$) => action$.pipe(
ofType('FETCH_USER'),
mergeMap(({ payload }) => ajax.getJSON(`/api/users/${payload}`).pipe(
map(response => ({
type: 'FETCH_USER_FULFILLED',
payload: response
}))
)
);

通过 ofType 来指定监听的 action,处理结束返回 action 传递给 store。

相比 redux-saga 来说,redux-observable 支持的异步过程的处理更丰富,直接对接了 operator 的生态,是开放的,而 redux-saga 则只是提供了内置的几个 effect 来处理。

所以做特别复杂的异步流程处理的时候,redux-observable 能够利用 rxjs 的操作符的优势会更明显。

但是 redux-saga 的优点还有基于 generator 的良好的可测试性,而且大多数场景下,redux-saga 提供的异步过程的处理能力就足够了,所以相对来说,redux-saga 用的更多一些。

总结

前端框架实现了数据到视图的绑定,我们只需要关心数据流就可以了。

相比 context 的混乱的数据流,redux 的 view -> action -> store -> view 的单向数据流更清晰且容易管理。

前端代码中有很多异步过程,这些异步过程之间可能有串行、并行甚至更复杂的关系,放在组件里并不好管理,可以放在 redux 的中间件里。

redux 的中间件就是对 dispatch 的层层包装,比如 redux-thunk 就是判断了下 action 是 function 就执行下,否则就是继续 dispatch。

redux-thunk 并没有提供多个异步过程管理的机制,复杂异步过程的管理还是得用 redux-saga 或者 redux-observable。

redux-saga 透传了 action 到 store,并且监听 action 执行相应的异步过程。异步过程的描述使用 generator 的形式,好处是可测试性。比如通过 take、takeEvery、takeLatest 来监听 action,然后执行 worker saga。worker saga 可以用 put、call、fork 等 effect 来描述不同的副作用,由 task 负责执行。

redux-observable 同样监听了 action 执行相应的异步过程,但是是基于 rxjs 的 operator,相比 saga 来说,异步过程的管理功能更强大。

不管是 redux-saga 通过 generator 来组织异步过程,通过内置 effect 来处理多个异步过程之间的关系,还是 redux-observable 通过 rxjs 的 operator 来组织异步过程和多个异步过程之间的关系。它们都解决了复杂异步过程的处理的问题,可以根据场景的复杂度灵活选用。

原文:https://juejin.cn/post/7011835078594527263


收起阅读 »

重构B端 ? 表单篇

随着业务的庞大。B端的业务越来越重,导致后面的需求越来越难满足,人在工位坐,锅从天上来,一个小前端就地开启了重构之旅 1. 梳理待重构的B端 上面是待重构 B端 的结构图,由 PHP 编写,利用约定的字段上传 JSON 文件,让 Controller 读取文...
继续阅读 »

随着业务的庞大。B端的业务越来越重,导致后面的需求越来越难满足,人在工位坐,锅从天上来,一个小前端就地开启了重构之旅


1. 梳理待重构的B端



上面是待重构 B端 的结构图,由 PHP 编写,利用约定的字段上传 JSON 文件,让 Controller 读取文件配置 在 DB 生成一个表,再由 Controller 直出到 View 层;在应对那些比较简单的逻辑或者功能性单一的业务时可以起到非常大的作用。但是需求只会越来越多。越来越复杂,尤其在处理复杂业务的时,整个 Controller 作为数据中枢,如果夹杂了太多的冗余逻辑,渐渐的就跟蜘蛛网一样难以维护。


2. 设计重构方案


了解完整个大概的数据走向以及业务背景之后,接下来就是设计整个重构方案了。



  • 继承之前的业务逻辑、通过 JSON 文件 渲染整个页面

  • 整体 UI 升级 ,因为用 React 重构,UI 这里选择 Antd

  • 前后端分离,本地开发先 Mock ,后期用 node 去代理或者用其他更灵活的方式交互数据


3. 配置文件改造



脚手架搭建,这里用 Antd Pro 脚手架 ( umi.js + antd ) , Router 配置 以及 React-Redux 等工具 umi 都帮我们内置了 ,基础搭建就不展开描述了。



{
"字段名称": {
"name_cfg": {
"label": "label",
"type": "select",
"tips": "tips",
"required": "required",
"max_length": 0,
"val_arr": {
"-1": "请选择",
"1": "字段 A",
"2": "字段 B",
"default": "-1"
},
"tabhide": {
"1": "A 字段, B 字段",
"2": "B 字段, C 字段",
"3": "C 字段, D 字段"
}
"relation_select": {
"url": "其他业务的接口请求",
"value": "Option id",
"name": "Option name",
"relation_field": "relation_field1, relation_field2"
}
},
"sql_cfg": {
"type": "int(11)",
"length": 11,
"primary": 0,
"default": -1,
"auto_increment": 0
}
},
...
}

这个 JSON 文件共有几十个字段,这里拿了一个比较有代表性的字段。这是一个 Select 类型的表单,val_arr 是这个 Select 的值,relation_select 请求其他业务的接口填进去这个 val_arr 供给给 Select , tabhide 表示的是,当值为 Key 时 隐藏表单的 Value 字段,多字段时以逗号分割。


此时需要一个过度文件将这个 JSON 文件处理成咋们方便处理的格式


// transfrom.js 
let Arr = [];
let Data = json.field_cfg;
for (let i in Data) {
let obj = {};
let val_Array = [];
let tab_hide = Data[i]['name_cfg']['tabhide']
for (let index in Data[i]['name_cfg']['val_arr']) {
let obj = {};
if (index !== 'default') {
obj['value'] = index;
obj['label'] = Data[i]['name_cfg']['val_arr'][index];
if(tab_hide && tab_hide[index]){
obj['tab_hide'] = tab_hide[index].split(',');
}
val_Array.push(obj);
} else {
val_Array.map((item) => {
if (item.value === Data[i]['name_cfg']['val_arr'][index]) {
item['default'] = true;
}
});
}
}
obj['id'] = i;
obj['name'] = Data[i]['name_cfg']['label'];
obj['type'] = Data[i]['name_cfg']['type'];
obj['required'] = Data[i]['name_cfg']['required'];
obj['tips'] = Data[i]['name_cfg']['tips'];
obj['multiple'] = Data[i]['name_cfg']['multiple'];
obj['val_arr'] = val_Array;
obj['sql_cfg_type'] = Data[i]['sql_cfg']['type'];
obj['sql_default'] = Data[i]['sql_cfg']['default'];
Arr.push(obj);
}

// config.js
const config = [
{
id: '字段名称',
name: 'label -> Name',
type: 'select',
required: 'required',
tips: 'tips',
val_arr: [
{ value: '-1', label: '请选择', default: true }
{ value: '1', label: 'Option A', tab_hide: ['字段A', '字段B'] }
{ value: '2', label: 'Option B', tab_hide: ['字段B', '字段C'] }
],
sql_cfg_type: 'int(11)',
sql_default: -1,
},
...
]

4. 表单渲染


Antd 的表单组件的功能非常丰富。拿到 Config 按照文档一把唆就完事了。

React 的 函数组件 和 Hooks 让代码更加简洁了,可太棒了!


  const renderForm: () => (JSX.Element | undefined)[] = () => {
// renderSelect(); renderText(); renderNumber(); renderDatePicker();
}
const renderSelect: (configItem: ConfigListData) => JSX.Element = (configItem) => {...}

const renderText: (configItem: ConfigListData) => JSX.Element = (configItem) => {...}

const renderNumber: (configItem: ConfigListData) => JSX.Element = (configItem) => {...}

const renderDatePicker: (configItem: ConfigListData) => JSX.Element = (configItem) => {...}

5. 默认字段 & 隐藏字段


componentDidMount 的时候处理这些字段,用 Hooks 可以这么表达 React.useEffect(()=>{...},[])

默认字段 : 给 Form 表单 设置上配置表的 sql_default 即可。

隐藏字段 : 这里涉及到字段会重叠,之前用 JQuery 单纯操作 DOM 节点去 show() 和 hide()。在操作 Dom 这方面,JQ 确实是有他的优势。

现在的解决方案如下图所示:


  const [hideListMap, setHideListMap] = useState<any>(new Map());
configObj.forEach((item: { val_arr: { tab_hide: any; value: any; }[]; sql_default: any; id: any; }) => {
item.val_arr.forEach((val_arr_item: { tab_hide: any; value: any; }) => {
if (val_arr_item.tab_hide && val_arr_item.value === item.sql_default) {
hideListMap.set(item.id, val_arr_item.tab_hide);
}
});
});
setHideListMap(hideListMap)

new Map() 用字段名作为 key ,value 的值为 tab_hide 数组, 之前用 Array 创建一个动态的 key 、value ,发现操作起来没有 Map 好使。


  const [hideList, setHideList] = useState<string[]>();

React.useEffect(() => {
let arr: string[] = []
hideListMap.forEach((item: any, key: any) => {
arr.push(...item);
});
setHideList(arr);
}, [hideListMap])

const selctChange = React.useCallback((value: SelectValue, configItem: ConfigListData) => {
hideListMap.forEach((item: any, key: string) => {
if (key === configItem.id) {
configItem.val_arr.forEach((val_arr_item: { value: SelectValue; tab_hide: any; }) => {
if (val_arr_item.value === value) {
hideListMap.set(configItem.id, val_arr_item.tab_hide);
setHideListMap(new Map(hideListMap))
}
})
}
});
}, [hideListMap]);

React.useEffect 不仅可以当 componentDidMount ,还可以当 componentDidUpdate 使用,只需要在第二个参数加上监听的值 React.useEffect(()=>{...},[n]), 这里监听了 hideListMap 如果 Select 框触发了 改变了 hideListMap 会自动帮我个更新 hideList,只要拿这个 hideList 作为条件判断是否渲染。就满足了多字段隐藏的需求了。


  const renderForm: () => (JSX.Element | undefined)[] = () => {
return configObj.map((item: ConfigListData) => {
if (hideList && hideList.includes(item.id)) {
return undefined
}
if (item.type === 'text') {
if (item.sql_cfg_type.indexOf('int') !== -1) {
return renderNumber(item)
} else {
return renderText(item)
}
}
if (item.type === 'select') {
return renderSelect(item)
}
if (item.type === 'datetime') {
return renderDatePicker(item);
}
return undefined;
})
}

6. Select 动态渲染值


// transfrom.js
let service = [];
let Data = json.field_cfg;
for (let i in Data) {
let relation_select = Data[i]['name_cfg']['relation_select'];
if (relation_select && relation_select['relation_field']) {
relation_select['relation_field'] = relation_select['relation_field'].split(',');
service.push(relation_select);
}
}

// config.ts
const SERVICE = [
{
url: "其他业务的接口请求",
value: "Option id",
name: "Option name",
relation_field: ["relation_field1", "relation_field2" ],
method: 'get'
}
...
]
// utils.ts
import request from 'umi-request'; // 请求库
export const getSelectApi = async (url: string, method: string) => {
return request(url, {
method,
})
}

之前接口请求与 Config 是耦合在一起的,Config 的字段如果增加到一定的数量时就会变得难以维护。最后决定把 表单Config 和 Service 层解耦,目的是为了更为直观的区别 Config 和 Service,方便以后维护。



数据接口参数格式以及字段都要有所约束。这里需要起一个 node 层,主要是处理第三方接口跨域处理,以及参数统一等。



import { CONFIG_OBJ, SERVICE } from './config';
const Form: React.FC = () => {
const [configObj, setConfigObj] = React.useState(CONFIG_OBJ);
React.useEffect(() => {
(async () => {
for (let item of SERVICE) {
for (let fieldItem of item.relation_field) { // 多字段支持
insertObj[fieldItem] = await getSelectApi(item.url, item.method)
}
}
for (let key in insertObj) {
if (key === item.id) {
item.val_arr.push(...insertObj[key].list)
}
}
setConfigObj([...configObj]);
})()
})
...
}
export default Form

将解耦出来的接口配置融合在 form 表单。useState 检测到数据指向变动就会重新渲染。

到这里 Form 表单组件基本搭建完成了。


6. 最后


再进一步思考,这里还有些优化的点,我们可以把这些配置文件也融进去这个B端里,将 Config 和 Service 都写进 DB,脱离用文件上传这种方式。


B端产品的服务群体是企业内人员。B端的用户体验也一样重要,一个优秀的B端也是提高效率、节省成本的途径之一。



本文到此结束,希望对各位看官有帮助 (‾◡◝)


作者:Coffee_C
链接:https://juejin.cn/post/6922286595290693646

收起阅读 »