注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

重要!后面几个月,iOS开发需要注意的3件事情

iOS
这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战前言这里非常感谢@恋猫de小郭,大佬的一篇文章让我醍醐灌顶。通过大佬的这篇文章对开发者而言《个人信息保护法》更新究竟是什么?如何应对适配?,我回过头,老老实实看了苹果开发的一些新闻,有一些...
继续阅读 »


这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

前言

这里非常感谢@恋猫de小郭,大佬的一篇文章让我醍醐灌顶。

通过大佬的这篇文章对开发者而言《个人信息保护法》更新究竟是什么?如何应对适配?,我回过头,老老实实看了苹果开发的一些新闻,有一些非常重要的信息。

如果你从事iOS开发,抑或针对Flutter开发,有需要上架App Store,我建议各位都来了解这3件事情。

自2022年1月31日起,需在app内提供帐户删除的功能

截屏2021-11-12 15.31.56.png

撇开11月1日颁布的《中华人民共和国个人信息保护法》,早上10月6日,Apple就发布了新闻说明:

需要在App中提供账户删除的功能,并且对于在2022年1月31日开始提交的App生效。

这说明了一个什么事呢?

如果你有一个App,后面会持续迭代,那么这个账户删除功能必须加上!!!

这事最好和项目、技术、后台一起讨论一下,我个人认为在App端无非多了一个交互,多了一个接口,多了一个逻辑,但是这个删除对后端来说,可能需要删除的东西就多了。

另外,从这字面上看,这应该是一个硬删除吧(软删除大家都懂的)。

自2022年4月起,必须使用Xcode 13和iOS 15 SDK构建App,提交至App Store

截屏2021-11-12 15.42.35.png


说简单点:

说白了,这是一波强行的让你升级Xcode的做法,没办法,在此道上混,就只能这么走。

迟早都要升级Xcode13,早点升级早踩坑。


另外需要注意的是,有些项目可能会在Xcode12上运行的很好,但是在Xcode13上一运行就报错,需要提前做好准备,我手上就有一个这样的项目。

年末假期接受app提交

截屏2021-11-12 15.56.06.png

大家都知道的,由于11月和12月都有西方的传统节日,所以一般情况下,在一些时间点提交App到App Store审核会异常缓慢。

今年Apple自己内卷了一把,在年末的假期也接受app提交了,虽然不知道具体速度如何,不过这也算是迎合国内的市场需求吧。

因为有些app就是在年末或者过节的时候迭代的非常频繁。毕竟大家都有剁手嘛~😁

参考文档

“需在 app 内提供帐户删除”的要求将于 1 月 31 日生效

将iOS和iPadOS app提交至App Store

年末假期接受app提交

macOS Monterey 与以下电脑兼容

总结

今天就想讲这么几件事情,其中第一件和第二件事情在我看来还是挺重要的,大家自己也掂量一下呗。

一周有空去Apple Developer网站看看新闻,有些对开发还是非常重要的。

Apple Developer新闻与更新

收起阅读 »

iOS App 的最佳架构,存在么?

iOS
iOS App 的最佳架构,存在么?本文翻译自 The best architecture for the iOS app, does it even exist?,建议参考原文阅读,也可查看这里前一段时间,我偶然发现了有关 iOS 体系结构模式的文...
继续阅读 »

iOS App 的最佳架构,存在么?

本文翻译自 The best architecture for the iOS app, does it even exist?,建议参考原文阅读,也可查看这里

前一段时间,我偶然发现了有关 iOS 体系结构模式的文章,标题颇具挑衅性:“唯一可行的 iOS 架构”。标题中问题的答案实际上是 MVC。简而言之,MVC 是 iOS 应用程序唯一可行的也是最好的架构。

该文章的主要思想是人们只是以错误的方式去理解 MVC。该 ViewController 实际上是表示层的一部分,而 Model 部分则代表整个 Domain Model,而不仅仅是某些数据实体。总的来说,我同意那个帖子的想法,但是如果我同意那个帖子的每一个陈述,我就不会写这篇文章了,不是吗?

我注意到作者基本上没有涉及格式良好的应用程序体系结构的一个非常重要的方面:使用单元测试(UT)覆盖了应用程序业务逻辑(BL)。对我而言,这是明智的应用程序体系结构的最重要因素之一。如果无法提取应用程序 BL 并以足够的覆盖范围实现 UT,那么这种架构简直糟透了。

此外,如果一个应用没有 UT,那么证明上述观点是不可行的,因此其架构最有可能出现问题。您可以向自己保证,在你从紧张工作中的片刻休息时间可以轻松实现UT,或者仅仅是因为您在 XCode 项目中拥有专用的“Tests”目标以及一些模板 UT。相信我,但这不过是一种幻想。我坚信,如果单元测试未随功能一起实施或在交付后不久就将无法实施。

以该声明为公理,应用程序体系结构必须提供将 BL 与 UI 表示分离并使其可测试的功能。很明显,由于 UIViewController 子类对生命周期的依赖性,因此它不是该角色的最佳候选人。这意味着负责 BL 的类必须位于 UIViewController 和 Services 之间,或者换句话说,位于 View 和 Domain Model 之间。

值得注意的是,这里的 Services 是指负责联网,与数据库、传感器、蓝牙、钥匙串、第三方服务等进行通信的逻辑。换句话说,是应用中多个位置、页面的共享部分。而图上的业务逻辑部分仅对应于一个页面或一个由视图控制器表示的页面组件。在开头提到的有关 MVC 的文章中,作者将 BL 和 Domain Model 部分结合在一起,同时接受 UIViewController 是表示逻辑(即视图)的一部分。

现在,当确定了将表示和业务逻辑分离的需要时,让我们考虑一下这两个部分如何相互通信。 这就是那些著名的架构模式出现的地方。

MVP

在 MVP 模式中,Presenter 和 View 通过协议相互链接。Presenter 被注入了 View 协议的实例,反之亦然,View 的协议必须具有足够的接口才能在 UI 中呈现原始数据,而Presenter 的协议必须具有传输从用户或系统接收到的事件(如触摸,手势,摇动等)的接口。UIViewController 子类在此处表示 View 部分,而 Presenter 类不能依赖UIKit(例如,有时需要导入 UIKit 才能对 UIImage 等数据类进行操作)。

在 iOS 上,由于 UIViewController 生命周期的工作方式,它必须具有对 Presenter 实例的强引用,而最后一个必须是弱引用,以避免循环引用。此配置使人联想到委托模式。 在大多数情况下,UIViewController 可能具有对 Presenter 的直接类型化引用,但在某些情况下,最后一个角色也可以通过协议注入到第一个中。如果 presentation 用于不同的业务逻辑,这可能会很有用。Presenter 到 UIViewController 的链接必须通过协议才能进行模拟,并用 UT 覆盖。在 Service 部分,我不会做太多具体说明,但是为了测试 Presenter,还必须将其与协议一起注入。

有关 MVP 的更多详细信息以及基本示例,请参见此处1

MVVM

在 MVVM 模式中,表示和业务部分使用响应性绑定相互通信,它们分别称为 View 和 ViewModel。在 iOS 中,通常会使用 ReactiveCocoa,RxSwift 或现代的 Combine 框架进行响应性绑定,它们通常位于 ViewModel 类中,并且也由 ViewController 通过协议使用。在与 Services 或 Domain Model 进行通信的一部分中,MVP 并没有太大的区别,但人们可能更喜欢在这里使用绑定或响应性事件。与前面的模式一样,必须在协议中注入依赖项,以便在 UT 中模拟它们。

可以在此处2找到有关 MVVM 的更多详细信息以及基本示例。

MVVM+Router

这里独立的主题是路由。在 iOS 中,以模态方式显示新屏幕或推送到导航堆栈是通过 UIViewController 子类来实现的。但是,这些操作可能是 BL 的一部分,并且可能会被 UT 覆盖,例如如果发生特定事件,则必须关闭屏幕。在这种情况下,将应用程序逻辑的这一部分分为一个称为 Router 的类是有意义的。因此,模式变为 MVP+R 或 MVVM+R。在某些来源中,您可能会发现此部分分别命名为 Coordinator 和 MVVP+C 或 MVVM+C。尽管协调器可能具有除路由之外的其他一些逻辑,但我更喜欢在概念上将它们等同。ViewModel 和 Router 之间的链接必须通过协议,并且最后一个必须仅负责屏幕操作,所有 BL 必须仍然集中在第一个中。因此,Router 不是 UT 的主题。

具有 MVVM+R 架构模式实现的示例项目可以在我的 GitHub3 上找到。

其它

VIPER iOS 体系结构模式是 MVVM+R 的扩展,其中 ViewModel 分为两部分:Interactor 和 Presenter。第一个负责与实体(即域模型)的通信。第二部分准备要在视图中呈现的模型类。老实说,我从未使用过这种模式,因为对我而言,它似乎过于分散和复杂。 MVVM+R 关注点分离对我而言总是足够的。

在 MVVM+R 中,每个模块(屏幕)必须至少显示 3 个类:ViewController,ViewModel 和 Router。并且必须有一个实例化所有这些部分并将它们彼此链接的位置,即模块构建的关键。最合适的位置是 Router,因为它没有与 iOS UIViewController 生命周期耦合,并且必须知道如何显示页面才能正确关闭它。但是,在某些情况下,将这一部分移到名为 Builder 的单独的类中会更方便,这就是 RIB(Uber的架构模式)中发生的情况。ViewModel 重命名为 Interactor,其余部分保持不变。这种模式具有 Uber 引入的一些更有趣的想法和技术,您可以在 RIB Wiki 上阅读。但是,我在 RIBs 代码库中发现的最实用的东西是 XCode 模板,当在项目中引入新的 RIBlet 时,它可以帮助避免样板编码。这些模板也可以很容易地用于 MVVM+R 类。为 Uber 的 iOS 工程师👏。

最后简单聊聊关于 iOS 上的单向数据流架构模式。如果看一下以上模式的方案,它们在组件之间都具有双向连接。在 Redux 中不是这种情况。这种架构模式最初是从 Web 迁发到移动应用的,尤其是 React 框架。在 ReSwift 框架中,此概念是 iOS 开发中最受欢迎的实现。我不会详细介绍,因为尚未在生产应用中使用此架构。但是,很明显,从 Web 开发进入 iOS 的人们发现这种架构模式最为直观和熟悉。

结论

什么才是最好的应用程序架构始终是一个热门的主题,所以现在我更倾向于约翰·桑德尔在他的最近一次演讲中提出的想法:

最好的架构是您和您的团队共同创建的架构,通过将标准模式和技术与系统设计相结合来适合您的项目。

参考

[1]https://medium.com/@saad.eloulladi/ios-swift-mvp-architecture-pattern-a2b0c2d310a3
[2]https://medium.com/flawless-app-stories/practical-mvvm-rxswift-a330db6aa693
[3]https://github.com/OlexandrStepanov/MVVM-RouterDemo

链接:https://juejin.cn/post/6844904067374776334

收起阅读 »

滴滴DoKit For Flutter正式开源,还是熟悉的"配方"

社区的小伙伴们,大家好啊,艰难的2020已经过去,随着2021的到来,在新的一年里希望大家都能够需求不变更、准点上下班、代码0 Bug。同时我们DoKit团队也给大家准备了一份特殊的“新年礼物”,没错它就是DoKit For Flutter,它来啦,它带着熟悉...
继续阅读 »

社区的小伙伴们,大家好啊,艰难的2020已经过去,随着2021的到来,在新的一年里希望大家都能够需求不变更、准点上下班、代码0 Bug。同时我们DoKit团队也给大家准备了一份特殊的“新年礼物”,没错它就是DoKit For Flutter,它来啦,它带着熟悉的配方来啦。接下来就让我们揭开它神秘的面纱吧。


背景


Flutter是Google开源的跨端技术框架。凭借其区别于RN/Weex的自渲染模式,在社区里引起了广泛关注,不管是终端还是前端的小伙伴都趋之若鹜,大有一统大前端江湖的气势。而国内大厂如闲鱼、字节、美团等,也都在其核心业务上完成了落地。


滴滴做为国内最大的出行平台,早在两年前就有多个内部团队开始在Flutter领域进行尝试。但是在开发过程中,我们遇到了很多调试性问题,如日志、帧率、抓包等。为了解决这些开发测试过程中遇到的各类问题,我们DoKit团队联合滴滴代驾和货运团队,把平时工作过程中沉淀下来的效率工具进行业务剥离和脱敏,并最终打造出DoKit For Flutter,在服务内部业务的同时,也为社区贡献一份力量,这也是滴滴的开源精神。


介绍


DoKit For Flutter是一个DoKit针对Flutter环境的产研工具包,内部集成了各种丰富的小工具,UI、网络、内存、监控等等。DoKit始终站在用户的角度,为用户提供最便利的产研工具。


Github地址


Pub仓库地址


操作文档



图片名称

那么接下来就让我来列举一下DoKit For Flutter的功能以及核心实现。


工具详解


基本信息


基本信息模块会展示当前dart虚拟机进程、CPU、Flutter版本信息、当前App包名和dart工程构建版本信息;



图片名称

VM信息通过VMService获取。Flutter版本实际上是通过Devtools服务注入的"flutterVersion"方法获取到的,在flutter attach后,本地会起一个websocket服务,连接VMService并注入flutterVersion和其余方法(HotReload、HotRestart等),通过VMService调用flutterVersion方法,会从本地flutter sdk目录下解析version文件返回版本号。


路由信息



图片名称

在Flutter中,每个页面对应一个Route,通过Navigator管理Route。Navigator内部会包含一个Overlay Widget,每个Route最终都转化成一个_OverlayEntryWidget添加到Overlay上。这个地方可以把Overlay理解为Android中的FrameLayout,内部子View上下叠加。每打开一个新的Route,都相当于往FrameLayout添加一个新的子View。Navigator会存在嵌套的情况,即Route所创建的页面本身也包含一个Navigator,比如App的根Widget是MaterialApp(自带Navigator),Route页面也用MaterialApp包裹,就会形成Navigator嵌套的情况。还是以FrameLayout来理解,这也就相当于嵌套的FrameLayout。
路由信息功能会打印出当前栈顶页面所处的Route信息,如果存在Navigator嵌套的情况,也会向上遍历打印出每层Navigator的信息。具体的实现方式是,先获取当前根app根Element,可以使用WidgetsBinding.instance.renderViewElement作为根Element,再通过递归调用element的visitChildElements方法,向下遍历整棵树找到最后一个RenderObejctElement,该RenderObejctElement即为当前显示的页面上的元素。然后使用ModalRoute.of(element)方法即可获取到当前页面的路由信息。


至于嵌套的路由信息,则可以通过找到的RenderObejctElement的findAncestorStateOfType方法,反向向上递归遍历,获得所处的Navigator的NavigatorState,再调用ModalRoute.of(navigatorState.context),如果返回不为空则表示存在嵌套。


方法通道



图片名称

Flutter的Method Channel调用最终都会经过ServiceBinding.instance._defaultBinaryMessenger这个对象,类型为BinaryMessenger,由于这个对象是个私有对象,无法动态进行修改。不过查看ServiceBinding的源码可以发现这个对象是通过ServiceBinding.createBinaryMessenger方法创建的,通过使用flutter的mixins,可以实现对该方法的重写。
我们知道,ServiceBinding实际也是通过mixins在WidgetsFlutterBinding.ensureInitialized方法中一起被初始化的,所以只要在WidgetsFlutterBinding这个类额外mixin一个继承于ServiceBinding并且重写了createBinaryMessenger方法的类,就能实现对ServiceBinding中createBinaryMessenger的覆盖,代码如下:


class DoKitWidgetsFlutterBinding extends WidgetsFlutterBinding
with DoKitServicesBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null) DoKitWidgetsFlutterBinding();
return WidgetsBinding.instance;
}
}

mixin DoKitServicesBinding on BindingBase, ServicesBinding {
@override
BinaryMessenger createBinaryMessenger() {
return DoKitBinaryMessenger(super.createBinaryMessenger());
}
}

接下去把runApp的入口调用改成如下,就能实现BinaryMessenger的替换
static void _runWrapperApp(DoKitApp wrapper) {
DoKitWidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(wrapper)
..scheduleWarmUpFrame();
}
至于Method Channel具体信息的捕获,只要hook住BinaryMessenger.handlePlatformMessage和BinaryMessenger.send两个方法就行了,具体可看DoKitBinaryMessenger这个类


控件检查



图片名称

和路由功能类似,通过从根element向下遍历,在遍历过程中记录和选中的View有交集的所有RendereObjectElement,并且记录用以标志当前页面的RendereObjectElement,获取它的Route信息。遍历完成后,遍历记录下来的RendereObjectElement,过滤掉Route信息和当前页面不一致的,这些Element属于被遮盖住的页面。然后通过比对RendereObjectElement和选中View的交叉区域面积占RendereObjectElement面积的比例,占比最大的为当前选中的组件。
在Debug模式下可以获取选中组件在工程中的代码位置,将WidgetInspectorService.instance.selection.current赋值为选中element的renderObject,再调用WidgetInspectorService.instance.getSelectedSummaryWidget方法,会返回一个json字符串,解析这个字符串就能获取源码文件名、行列信息等。


日志查看



图片名称

日志查看功能比较简单,只要使用runZoned方法替代runApp,传入zoneSpecification,就能为日志输出设置一个代理函数,在这个代理函数内进行日志捕获,同时,还可以为onError设置一个代理函数,在这里将捕获的异常也会传入到日志当中。


帧率



图片名称

使用WidgetsBinding.instance.addTimingsCallback可以统计帧率信息,在每帧渲染完成时会触发回调,包含该帧渲染的信息。


内存



图片名称

同VM信息,使用VMService可以获取到内存详细使用信息。


网络请求



图片名称

Flutter自带的网络请求通过HttpClient类发送,只要hook住HttpClient的创建就可以hook整个网络请求的过程。查看HttpClient的构造函数可以发现,如果存在HttpOverrides,就会使用HttpOverrids来创建HttpClient


factory HttpClient({SecurityContext? context}) {
HttpOverrides? overrides = HttpOverrides.current;
if (overrides == null) {
return new _HttpClient(context);
}
return overrides.createHttpClient(context);
}
所以这里重写了一个HttpOverrids
class DoKitHttpOverrides extends HttpOverrides {
final HttpOverrides origin;

DoKitHttpOverrides(this.origin);

@override
HttpClient createHttpClient(SecurityContext context) {
if (origin != null) {
return DoKitHttpClient(origin.createHttpClient(context));
}
// 置空,防止递归调用,使得_HttpClient可以被初始化
HttpOverrides.global = null;
HttpClient client = DoKitHttpClient(new HttpClient(context: context));
// 创建完成后继续置回DoKitHttpOverrides
HttpOverrides.global = this;
return client;
}
}

替换HttpOverrides


HttpOverrides origin = HttpOverrides.current;
HttpOverrides.global = new DoKitHttpOverrides(origin);

hook住HttpClient方法后,对于请求和返回结果的hook过程就和Android中的HttpUrlConnection类似了,具体可以看DoKitHttpClient、DoKitHttpClientRequest、DoKitHttpClientResponse三个类。


版本API兼容


Flutter版本更新还是比较快的,每一个大版本更新都会带来一些API的变更,目前DoKit的方案需要重写一些framework层的类,在兼容多版本时就会有一些问题。以上面的BinaryMessager为例,1.17版本只有四个方法,用来hook的DoKitBinaryMessager是这么写的


class DoKitBinaryMessenger extends BinaryMessenger {
final MethodCodec codec = const StandardMethodCodec();
final BinaryMessenger origin;

DoKitBinaryMessenger(this.origin);

@override
Future<void> handlePlatformMessage(String channel, ByteData data, callback) {
ChannelInfo info = saveMessage(channel, data, false);
PlatformMessageResponseCallback wrapper = (ByteData data) {
resolveResult(info, data);
callback(data);
};
return origin.handlePlatformMessage(channel, data, wrapper);
}

@override
Future<ByteData> send(String channel, ByteData message) async {
ChannelInfo info = saveMessage(channel, message, true);
ByteData result = await origin.send(channel, message);
resolveResult(info, result);
return result;
}

@override
void setMessageHandler(
String channel, Future<ByteData> Function(ByteData message) handler) {
origin.setMessageHandler(channel, handler);
}

@override
void setMockMessageHandler(
String channel, Future<ByteData> Function(ByteData message) handler) {
origin.setMockMessageHandler(channel, handler);
}
}

用来hook的wrapper类需要调用oring对象的同名方法。但在1.20版本BinaryMessager增加了两个新方法checkMessageHandler和checkMockMessageHandler,如果使用1.17.5版本的flutter sdk去编译,就无法调用origin.checkMessageHandler方法,因为不存在;如果使用1.20.4版本的flutter sdk去编译,编译和发布没问题,但编出来的sdk在1.17.5的工程被引用后,也会因为checkMessageHandler方法不存在导致编译失败。
针对这种多个Flutter版本API不同导致的兼容性问题,可以使用扩展方法extension关键字来解决。
建立一个_BinaryMessengerExt类如下:


extension _BinaryMessengerExt on BinaryMessenger {
bool checkMessageHandler(String channel, MessageHandler handler) {
return this.checkMessageHandler(channel, handler);
}

bool checkMockMessageHandler(String channel, MessageHandler handler) {
return this.checkMockMessageHandler(channel, handler);
}
}

在1.17.5版本,调用origin.checkMessageHandler会走到扩展方法的checkMessageHandler中,编译能通过,由于这个方法在1.17.5中是绝对不会被调用到的,虽然会形成递归调用,但没影响。而在1.20版本,BinaryMessenger本身实现了checkMessageHandler方法,所以调用checkMessageHandler方法会走到BinaryMessenger的checkMessageHandler方法中,也能正常使用。
通过extentsion,只要以最低兼容版本的类作为基础,在扩展类中定义新版本中新增的API,就能解决多版本API兼容的问题。


总结


以上就是DoKit For Flutter的现有功能以及工具的基本原理介绍。 我们知道当前它的功能还不是完善,后续我们会继续不断深入的挖掘业务中的痛点并持续输出各种提高用户效率的工具,努力让DoKit For Flutter变得更加优秀,符合大家的期望。


DoKit一直追求给开发者提供最便捷和最直观的开发体验,同时我们也十分欢迎社区中能有更多的人参与到DoKit的建设中来并给我们提出宝贵的意见或PR。 DoKit的未来需要大家共同的努力。


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

什么?你还不会用位运算来操作状态?

回顾首先来回顾一下位运算,什么是位运算呢?位运算就是直接对整数在内存中的二进制位进行操作。在 Java 语言中,位运算有如下这些:左移(<<)。右移(>>)。无符号右移(>>>)。与(&)。或(|)。非(~)。...
继续阅读 »

回顾

首先来回顾一下位运算,什么是位运算呢?

位运算就是直接对整数在内存中的二进制位进行操作。

在 Java 语言中,位运算有如下这些:

  • 左移(<<)。

  • 右移(>>)。

  • 无符号右移(>>>)。

  • 与(&)。

  • 或(|)。

  • 非(~)。

  • 异或(^)。

在本篇文章中,我们所需要用到的有如下几个(其他的后续文章再讲):

  • &(与运算):只有当两方都为 true 时,结果才是 true,否则为 false。

  • |(或运算):只要当一方为 true 时,结果就是 true,否则为 false。

  • ^(异或运算):只要两方不同,结果就是 true,否则为 false。

以 true、false 为例:


true & true = true

true & false = false



true | false = true;

false | false = false;



true ^ true = false;

true ^ false = true;

以数字运算为例:


6 & 4 = ?

6 | 4 = ?

6 ^ 4 = ?

当以数字运算时,我们首先需要知道这些数字的二进制,假设 6 是 int 类型,那么其二进制如下:

00000000 00000000 00000000 00000110

在 Java 中,int 占了 4 个字节(Byte),一个字节呢又等于 8 个 Bit 位。所以 int 类型的二进制表现形式如上。

在这里为方便讲解,直接取后 8 位:00000110。

4 的二进制码如下:


00000100

在二进制码中,1 为 true,0 为 false,根据这个,我们再来看看 6 & 4 的运算过程:


00000110

00000100

-----------

00000100

对每位的数进行运算后,结果为 4。

再来看看 | 运算:


6 | 4 = ?

6 和 4 的二进制上面已经说了:


00000110

00000100

-----------

00000110

可以发现最后的结果是 6。

最后再来看看 ^ 运算:


6 ^ 4 = ?


00000110

00000100

-----------

00000010

结果是 2。

应用

通过上面的例子,我们已经回顾了 & 、 | 以及 ^ 运算。现在来将它应用到实际的应用中。

假如我们现在要定义一个人的模型,这个人可能会包含有多种性格,比如说什么乐观型、内向型啦...

要是想要知道他包含了哪种性格,那么我们该如何判断呢?

可能在第一时间会想到:


if(这个人是乐观性){

....

}else if(这个人是内向型){

...

}

那么如果有很多种性格呢?一堆判断写起来真的是很要命..

下面就来介绍一种更简单的方式。首先来定义一组数:


public static final int STATUS_NORMAL = 0;
public static final int STATUS_OPTIMISTIC = 1;
public static final int STATUS_OPEN = 2;
public static final int STATUS_CLOSE = 4;

把它们转换为二进制:


0000 0000 0000 0000
0000 0000 0000 0001
0000 0000 0000 0010
0000 0000 0000 0100

发现其中二进制的规律没有?都是 2 的次幂,并且二进制都只有一个为 1 位,其他都是 0 !

然后再来定义一个变量,用于存储状态(默认值是 0):


private static int mStatus = STATUS_NORMAL;

当我们要保存状态时,直接用 | 运算即可:


mStatus |= STATUS_OPTIMISTIC;

保存的运算过程如下:


00000000

执行 | 运算(只要有 1 则为 1)

00000001

-----------

00000001 = 1

相当于就把这个 1 存储到 0 的二进制当中了。

那么如果要判断 mStatus 中是否有某个状态呢?使用 & 运算:


System.out.println((mStatus & STATUS_OPTIMISTIC) != 0);// true,代表有它

计算过程如下:


00000001

执行 & 运算(都为 1 才为 1)

00000001

-----------

00000001 = 1

再来判断一个不存在的状态 mStatus & STATUS_OPEN


System.out.println((mStatus & STATUS_OPEN) != 0);// false,代表没有它

计算过程如下:


00000001

00000010

-----------

00000000 = 0

可以发现,因为 STATUS_OPEN 这个状态的二进制位,1 的位置处,mStatus 的二进制并没有对于的 1,而又因为其他位都是 0,导致全部归 0,计算出来的结果自然也就是 0 了。

这也就是为什么定义状态的数字中,是 1、2、4 这几位数了,因为他们的特定就是二进制只有一个为 1 的位,其他位都是 0,并同其他数位 1 的位不冲突。

如果换成其他的数,就会有问题了。比如说 3:


mStatus |= 3

计算过程:


00000000

00000011

-----------

00000011 = 3

运算完毕,这时候 mStatus 中已经存储了 3 这个值了,我们再来判断下是否存在 2:


System.out.println((mStatus & 2) != 0);// true,代表有它,但是其实是没有的

00000011

00000010

-----------

00000010 = 2

结果是 true,但是其实我们只存储了 3 到 mStatus 中,结果肯定是错误的。

所以我们在定义的时候,一定不要手滑定义错了数字。

存储和判断已经说了,那么如何取出呢?这时候就要用到 ^ 运算了。

假如现在 mStatus 中已经存储了 STATUS_OPTIMISTIC 状态了,要把它给取出来,这样写即可:


mStatus ^= STATUS_OPTIMISTIC

其中的运算过程:


00000001

执行 ^ 运算,两边不相同,则为 true

00000001

-----------

00000000

可以看到状态又回到了最初没有存储 STATUS_OPTIMISTIC 状态的时候了。

最后再来看一个取出的例子,这次是先存储两个状态,然后再取出其中一个:


mStatus |= STATUS_OPTIMISTIC

mStatus |= STATUS_OPEN

存储完后,mStatus 的二进制为:


00000011

再来取出 STATUS_OPEN 这个状态:


mStatus ^= STATUS_OPEN

运算过程:


00000011

00000010

-----------

00000001

mStatus 现在就只有 STATUS_OPTIMISTIC 的状态了。

总结

通过 |、^、& 运算,我们可以很方便快捷的对状态值进行操作。当然,位运算的应用不仅限于状态值,知道了其中的二进制运算原理后,还有更多的其他应用场景,等着你去发现。


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

收起阅读 »

Android 多渠道打包看这一篇就够了

Android 多渠道打包看这一篇就够了 本文三个流程 一、多渠道配置 1、多渠道配置 2、不同渠道不同签名配置 3、不同渠道不同资源文件配置 4、不同渠道不同依赖配置 二、注意事项 三、打包 1、命令行打包 2、IDE 打包 多渠道配置(2 种方式) 1、可...
继续阅读 »

Android 多渠道打包看这一篇就够了


本文三个流程


一、多渠道配置


1、多渠道配置

2、不同渠道不同签名配置

3、不同渠道不同资源文件配置

4、不同渠道不同依赖配置

二、注意事项


三、打包


1、命令行打包


2、IDE 打包


多渠道配置(2 种方式)


1、可写在主模块(app)的 build.gradle 下


android {  
compileSdkVersion 29
buildToolsVersion "29.0.3"

defaultConfig {
applicationId "com.test.moduledemo"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
}

flavorDimensions "versionCode"

productFlavors {
xiaomi{
applicationId = “com.test.xiaomi"
//不同渠道配置不同参数
buildConfigField "int", "TEST_VALUE", "1"
buildConfigField "String", "TEST_NAME", "\"xiaomi\""
}
huawei{
applicationId = "com.test.huawei"
//不同渠道配置不同参数
buildConfigField "int", "TEST_VALUE", "2"
buildConfigField "String", "TEST_NAME", "\"huawei\""
}
productFlavors.all {//遍历productFlavors多渠道,设置渠道号(xiaomi 、huawei)
flavor -> flavor.manifestPlaceholders.put("CHANNEL", name)
}
}
applicationVariants.all { variant ->
// 打包完成后输出路径
def name = ((project.name != "app") ? project.name : rootProject.name.replace(" ", "")) +
"_" + variant.flavorName +
"_" + variant.buildType.name +
"_" + variant.versionName +
"_" + new Date().format('yyyyMMddhhmm') + ".apk"
//相对路径app/build/outputs/apk/huawei/release/
def path = "../../../../../apk/" //相当于路径 app/apk/
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.apk')) {
//指定路径输出
output.outputFileName = new File(path, name)
}
}
// 在打包完成后还可以做一些别的操作,可以复制到指定目录,或者移动文件到指定目录
variant.assemble.doLast {
File out = new File(“${project.rootDir}/apk”)
variant.outputs.forEach { file ->
//复制apk到指定文件夹
//copy {
// from file.outputFile
// into out
//}
//把文件移动到指定文件夹
ant.move file: file.outputFile,
todir: "${project.rootDir}/apk"
}
}
}
//多渠道签名的配置
signingConfigs {
test {
storeFile file("../test.keystore")
storePassword 'test'
keyAlias 'test'
keyPassword 'test'
v1SigningEnabled true
v2SigningEnabled true
}
xiaomi {
storeFile file("../xiaomi.keystore")
storePassword 'xiaomi'
keyAlias 'xiaomi'
keyPassword 'xiaomi'
v1SigningEnabled true
v2SigningEnabled true
}
huawei {
storeFile file("../huawei.keystore")
storePassword 'huawei'
keyAlias 'huawei'
keyPassword 'huawei'
v1SigningEnabled true
v2SigningEnabled true
}
}
buildTypes {
debug {
// debug这里设置不起作用,可能是编译器的问题?
// productFlavors.xiaomi.signingConfig signingConfigs.test
// productFlavors.huawei.signingConfig signingConfigs.test
}
release {
productFlavors.xiaomi.signingConfig signingConfigs.xiaomi
productFlavors.huawei.signingConfig signingConfigs.huawei
}
}
//不同渠道不同资源文件配置
sourceSets{
xiaomi.res.srcDirs 'src/main/res-xiaomi'
huawei.res.srcDirs 'src/main/res-huawei'
}
//不同渠道不同的依赖文件
dependencies {
xiaomiApi('xxxxxxx')
huaweiImplementation('xxxxxxxx')
}
}

2、在项目根目录下(与settings.gradle同目录)新建 flavors.gradle 文件


 android {  
flavorDimensions "versionCode"

productFlavors {
xiaomi{
applicationId = "com.test.xiaomi"
//不同渠道配置不同参数
buildConfigField "int", "TEST_VALUE", "1"
buildConfigField "String", "TEST_NAME", "\"xiaomi\""
}
huawei{
applicationId = "com.test.huawei"
//不同渠道配置不同参数
buildConfigField "int", "TEST_VALUE", "2"
buildConfigField "String", "TEST_NAME", "\"huawei\""
}
productFlavors.all {//遍历productFlavors多渠道,设置渠道号(xiaomi 、huawei)
flavor -> flavor.manifestPlaceholders.put("CHANNEL", name)
}
}
// ............ 更多配置
}

在主模块(app)的 build.gradle 下引用该 flavors.gradle 文件即可
apply from: rootProject.file('flavors.gradle')


注意


如果项目较为复杂,有可能通过 buildConfigField 设置不同的渠道包,不同的信息字段有可能失效,则把
buildConfigField "int", "TEST_VALUE", "1"
换成
manifestPlaceholders.put("TEST_VALUE", 1)
然后再 AndroidManifest.xml 里添加


<application>
<meta-data
android:name="TEST_VALUE"
android:value="${TEST_VALUE}" />
</application>

在 代码通过一下操作获取其值:


ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(getPackageName(),  
PackageManager.GET_META_DATA);
int testValue = applicationInfo.metaData.getInt("TEST_VALUE");

打包


命令行打包:


Windows下: gradlew assembleRelease
Mac 下:./gradlew assembleRelease
assembleRelease 是打所有渠道的 Release 包
assembleDebug 是打所有渠道的 Debug 包
还可以打指定渠道的包:
gradlew assembleXiaoMiRelease assembleHuaWeiRelease
(空格隔开要打的渠道包的任务名称即可,任务名称可以通过点击 android studio 右边的 Gradle 根据图中目录查看)



编译器打包





当渠道很多的时候,不同渠道不同配置就会变得相当繁琐了,欢迎查看我的下一篇推文多渠道打包-进阶


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

【灵魂拷问】当面试官问你JavaScript预编译

(一) 前言 在腾讯字节等其他大厂的面试中,JavaScript预编译是经常会被问到的问题,本文将带大家了解JS预编译中的具体过程 (二)编译执行步骤 传统编译语言编译步骤 对传统编译型语言来说,其编译步骤一般为:词法分析->语法分析->代码生成,...
继续阅读 »

(一) 前言


在腾讯字节等其他大厂的面试中,JavaScript预编译是经常会被问到的问题,本文将带大家了解JS预编译中的具体过程


(二)编译执行步骤


传统编译语言编译步骤


对传统编译型语言来说,其编译步骤一般为:词法分析->语法分析->代码生成,下面让我们来分别介绍这3个过程。



  1. 词法分析


这个过程会将代码分隔成一个个语法单元,比如var a = 520;这段代码通常会被分解为vara=520这4个词法单元。



  1. 语法分析


这个过程是将词法单元整合成一个多维数组,即抽象语法树(AST),以下面代码为例


if(typeof a == "undefined" ){ 
a = 0;
} else {
a = a;
}
alert(a);

语法树.jpg


当JavaScript解释器在构造语法树的时候,如果发现无法构造,就会报语法错误(syntaxError),并结束整个代码块的解析。



  1. 代码生成


这个过程是将抽象语法树AST转变为可执行的机器代码,让计算机能读懂执行。


JavaScript编译步骤


比起传统的只有3个步骤的语言的编译器,JavaScript引擎要复杂的多,但总体来看,JavaScript编译过程只有下面三个步骤:
1. 语法分析
2. 预编译
3. 解释执行


(三)预编译详解


预编译概述


JavaScript预编译发生在代码片段执行前的几微秒(甚至更短!),预编译分为两种,一种是函数预编译,另一种是全局预编译,全局预编译发生在页面加载完成时执行,函数预编译发生在函数执行的前一刻。预编译会创建当前环境的执行上下文。


函数的预编译执行四部曲



  1. 创建Activation Object(以下简写为AO对象);

  2. 找形参和变量声明,将变量声明和形参作为AO的属性名,值为underfined;

  3. 将实参和形参值统一;

  4. 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体。


案例代码


//请问下面的console.log()输出什么?
function fn(a) {
//console.log(a);
var a = 123//变量赋值
//console.log(a);
function a() { }//函数声明
//console.log(a);
var b = function () { }//变量赋值(函数表达式)
//console.log(b);
function d() { }//函数声明
}
fn(1)//函数调用

根据上面的四部曲,对代码注解后,我们可以很轻松的知道四个console.log()输出什么,让我们来看下AO的变化



  1. 创建AO对象


AO{
//空对象
}


  1. 找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined;


AO{
a: undefined
b: undefined
}


  1. 将实参和形参值统一;


AO{
a: 1,
b: undefined
}


  1. 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体。


AO{
a: function(){}
b: undefined
d: function(){}
}

最后,下面是完整的预编译过程


AO:{
a:undefined -> 1 -> function a(){}
b:undefined
d:function d(){}
}


全局的预编译执行三部曲



  1. 创建Global Object(以下简写为GO对象);

  2. 找形参和变量声明,将变量声明和形参作为GO的属性名,值为undefined;

  3. 在全局里找函数声明,将函数名作为GO对象的属性名,值赋予函数体。


案例代码


global = 100;
function fn() {
//console.log(global);
global = 200;
//console.log(global);
var global = 300;
}
fn();

根据全局预编译三部曲我们可以知道他的GO变化过程



  1. 创建GO对象


GO{
// 空对象
}


  1. 找形参和变量声明,将变量声明和形参作为GO的属性名,值为underfined


GO: {
global: undefined
}


  1. 在全局里找函数声明,将函数名作为GO对象的属性名,值赋予函数体


GO: {
global: undefined
fn: function() { }
}


注意这里函数声明会带来函数自己的AO,预编译过程继续套用四部曲即可



(四)总结


当遇到面试官问你预编译过程时,可以根据上面的内容轻松解答,同时面试时也会遇到很多问你console.log()输出值的问题,也可以用上面的公式获取正确答案。


作者:橙玉米
链接:https://juejin.cn/post/7030370931478364196

收起阅读 »

面试题:实现小程序平台的并发双工 rpc 通信

前几天面试的时候遇到一道面试题,还是挺考验能力的。 题目是这样的: rpc 是 remote procedure call,远程过程调用,比如一个进程调用另一个进程的某个方法。很多平台提供的进程间通信机制都封装成了 rpc 的形式,比如 electron 的 ...
继续阅读 »

前几天面试的时候遇到一道面试题,还是挺考验能力的。


题目是这样的:


rpc 是 remote procedure call,远程过程调用,比如一个进程调用另一个进程的某个方法。很多平台提供的进程间通信机制都封装成了 rpc 的形式,比如 electron 的 remote 模块。


小程序是双线程机制,两个线程之间要通信,提供了 postMessage 和 addListener 的 api。现在要在两个线程都会引入的 common.js 文件里实现 rpc 方法,支持并发的 rpc 通信。


达到这样的使用效果:


const res = await rpc('method', params);

这道题是有真实应用场景的题目,比一些逻辑题和算法题更有意思一些。


实现思路


两个线程之间是用 postMessage 的 api 来传递消息的:



  • 在 rpc 方法里用 postMessage 来传递要调用的方法名和参数

  • 在 addListener 里收到调用的时候,调用 api,然后通过 postMessage 返回结果或者错误


我们先实现 rpc 方法,通过 postMessage 传递消息,返回一个 promise:


function rpc(method, params) {
postMessage(JSON.stringify({
method,
params
}));

return new Promise((resolve, reject) => {

});
}

这个 promise 什么时候 resolve 或者 reject 呢? 是在 addListener 收到消息后。那就要先把它存起来,等收到消息再调用 resolve 或 reject。


为了支持并发和区分多个调用通道,我们加一个 id。


let id = 0;
function genId() {
return ++id;
}

const channelMap = new Map();

function rpc(method, params) {
const curId = genId();

postMessage(JSON.stringify({
id: curId,
method,
params
}));

return new Promise((resolve, reject) => {
channelMap.set(curId, {
resolve,
reject
});
});
}

这样,就通过 id 来标识了每一个远程调用请求和与它关联的 resolve、reject。


然后要处理 addListener,因为是双工的通信,也就是通信的两者都会用到这段代码,所以要区分一下是请求还是响应。


addListener((message) => {
const { curId, method, params, res}= JSON.parse(message);
if (res) {
// 处理响应
} else {
// 处理请求
}
});

处理请求就是调用方法,然后返回结果或者错误:


try {
const data = global[method](...params);
postMessage({
id
res: {
data
}
});
} catch(e) {
postMessage({
id,
res: {
error: e.message
}
});
}

处理响应就是拿到并调用和 id 关联的 resolve 和 reject:


const { resolve, reject  } = channelMap.get(id);
if(res.data) {
resolve(res.data);
} else {
reject(res.error);
}

全部代码是这样的:


let id = 0;
function genId() {
return ++id;
}

const channelMap = new Map();

function rpc(method, params) {
const curId = genId();

postMessage(JSON.stringify({
id: curId,
method,
params
}));

return new Promise((resolve, reject) => {
channelMap.set(curId, {
resolve,
reject
});
});
}

addListener((message) => {
const { id, method, params, res}= JSON.parse(message);
if (res) {
const { resolve, reject } = channelMap.get(id);
if(res.data) {
resolve(res.data);
} else {
reject(res.error);
}
} else {
try {
const data = global[method](...params);
postMessage({
id
res: {
data
}
});
} catch(e) {
postMessage({
id,
res: {
error: e.message
}
});
}
}
});

我们实现了最开始的需求:



  • 实现了 rpc 方法,返回一个 promise

  • 支持并发的调用

  • 两个线程都引入这个文件,支持双工的通信


其实主要注意的有两个点:



  • 要添加一个 id 来关联请求和响应,这在 socket 通信的时候也经常用

  • resolve 和 reject 可以保存下来,后续再调用。这在请求取消,比如 axios 的 cancelToken 的实现上也有应用


这两个点的应用场景还是比较多的。


总结


rpc 是远程过程调用,是跨进程、跨线程等场景下通信的常见封装形式。面试题是小程序平台的双线程的场景,在一个公共文件里实现双工的并发的 rpc 通信。


思路文中已经讲清楚了,主要要注意的是 promise 的 resolve 和 reject 可以保存下来后续调用,通过添加 id 来标识和关联一组请求响应。


作者:zxg_神说要有光
链接:https://juejin.cn/post/7030803556282155022

收起阅读 »

localStorage灵魂五问。 5M?? 10M !!!

灵魂五问 localStorage 存储的键值采用什么字符编码 5M 的单位是什么 localStorage 键占不占存储空间 localStorage的键的数量,对写和读性能的影响 写个方法统计一个localStorage已使用空间 我们挨个解答,之后给...
继续阅读 »

灵魂五问



  1. localStorage 存储的键值采用什么字符编码

  2. 5M 的单位是什么

  3. localStorage 键占不占存储空间

  4. localStorage的键的数量,对写和读性能的影响

  5. 写个方法统计一个localStorage已使用空间


我们挨个解答,之后给各位面试官又多了一个面试题。


我们常说localStorage存储空间是5M,请问这个5M的单位是什么?


localStorage 存储的键值采用什么字符编码?


打开相对权威的MDN localStorage#description



The keys and the values stored with localStorage are always in the UTF-16 DOMString format, which uses two bytes per character. As with objects, integer keys are automatically converted to strings.



翻译成中文:



localStorage 存储的键和值始终采用 UTF-16 DOMString 格式,每个字符使用两个字节。与对象一样,整数键将自动转换为字符串。



答案: UTF-16


MDN这里描述的没有问题,也有问题,因为UTF-16,每个字符使用两个字节,是有前提条件的,就是码点小于0xFFFF(65535), 大于这个码点的是四个字节。


这是全文的关键。


5M 的单位是什么


5M的单位是什么?


选项:



  1. 字符的个数

  2. 字节数

  3. 字符的长度值

  4. bit 数

  5. utf-16编码单元


以前不知道,现代浏览器,准确的应该是 选项3,字符的长度 ,亦或 选项5, utf-16编码单元


字符的个数,并不等于字符的长度,这一点要知道:


"a".length // 1
"人".length // 1
"𠮷".length // 2
"🔴".length // 2

现代浏览器对字符串的处理是基于UTF-16 DOMString


但是说5M字符串的长度,显然有那么点怪异。


而根据 UTF-16编码规则,要么2个字节,要么四个字节,所以不如说是 10M 的字节数,更为合理。


当然,2个字节作为一个utf-16的字符编码单元,也可以说是 5M 的utf-16的编码单元。


我们先编写一个utf-16字符串计算字节数的方法:非常简单,判断码点决定是2还是4


function sizeofUtf16Bytes(str) {
var total = 0,
charCode,
i,
len;
for (i = 0, len = str.length; i < len; i++) {
charCode = str.charCodeAt(i);
if (charCode <= 0xffff) {
total += 2;
} else {
total += 4;
}
}
return total;
}

我们再根绝10M的字节数来存储


我们留下8个字节数作为key,8个字节可是普通的4个字符换,也可是码点大于65535的3个字符,也可是是组合。


下面的三个组合,都是可以的,



  1. aaaa

  2. aa🔴

  3. 🔴🔴


在此基础上增加任意一个字符,都会报错异常异常。


const charTxt = "人";
let count = (10 * 1024 * 1024 / 2) - 8 / 2;
let content = new Array(count).fill(charTxt).join("");
const key = "aa🔴";
localStorage.clear();
try {
localStorage.setItem(key, content);
} catch (err) {
console.log("err", err);
}

const sizeKey = sizeofUtf16Bytes(key);
const contentSize = sizeofUtf16Bytes(content);
console.log("key size:", sizeKey, content.length);
console.log("content size:", contentSize, content.length);
console.log("total size:", sizeKey + contentSize, content.length + key.length);

现代浏览器的情况下:


所以,说是10M的字节数,更为准确,也更容易让人理解。


如果说5M,那其单位就是字符串的长度,而不是字符数。


答案: 字符串的长度值, 或者utf-16的编码单元


更合理的答案是 10M字节空间。


localStorage 键占不占存储空间


我们把 key和val各自设置长 2.5M的长度


const charTxt = "a";
let count = (2.5 * 1024 * 1024);
let content = new Array(count).fill(charTxt).join("");
const key = new Array(count).fill(charTxt).join("");
localStorage.clear();
try {
console.time("setItem")
localStorage.setItem(key, content);
console.timeEnd("setItem")
} catch (err) {
console.log("err code:", err.code);
console.log("err message:", err.message)
}

执行正常。


我们把content的长度加1, 变为 2.5 M + 1, key的长度依旧是 2.5M的长度


const charTxt = "a";
let count = (2.5 * 1024 * 1024);
let content = new Array(count).fill(charTxt).join("") + 1;
const key = new Array(count).fill(charTxt).join("");
localStorage.clear();
try {
console.time("setItem")
localStorage.setItem(key, content);
console.timeEnd("setItem")
} catch (err) {
console.log("err code:", err.code);
console.log("err message:", err.message)
}

image.png


产生异常,存储失败。 至于更多异常详情吗,参见 localstorage_功能检测


function storageAvailable(type) {
var storage;
try {
storage = window[type];
var x = '__storage_test__';
storage.setItem(x, x);
storage.removeItem(x);
return true;
}
catch(e) {
return e instanceof DOMException && (
// everything except Firefox
e.code === 22 ||
// Firefox
e.code === 1014 ||
// test name field too, because code might not be present
// everything except Firefox
e.name === 'QuotaExceededError' ||
// Firefox
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
// acknowledge QuotaExceededError only if there's something already stored
(storage && storage.length !== 0);
}
}

答案: 占空间


键的数量,对读写的影响


我们500 * 1000键,如下


let keyCount = 500 * 1000;

localStorage.clear();
for (let i = 0; i < keyCount; i++) {
localStorage.setItem(i, "");
}

setTimeout(() => {
console.time("save_cost");
localStorage.setItem("a", "1");
console.timeEnd("save_cost");
}, 2000)


setTimeout(() => {
console.time("read_cost");
localStorage.getItem("a");
console.timeEnd("read_cost");

}, 2000)

// save_cost: 0.05615234375 ms
// read_cost: 0.008056640625 ms

你单独执行保存代码:


localStorage.clear();    
console.time("save_cost");
localStorage.setItem("a", "1");
console.timeEnd("save_cost");
// save_cost: 0.033203125 ms

可以多次测试, 影响肯定是有的,也仅仅是数倍,不是特别的大。


反过来,如果是保存的值表较大呢?


const charTxt = "a";
const count = 5 * 1024 * 1024 - 1
const val1 = new Array(count).fill(charTxt).join("");

setTimeout(() =>{
localStorage.clear();
console.time("save_cost_1");
localStorage.setItem("a", val1);
console.timeEnd("save_cost_1");
},1000)


setTimeout(() =>{
localStorage.clear();
console.time("save_cost_2");
localStorage.setItem("a", "a");
console.timeEnd("save_cost_2");
},1000)

// save_cost_1: 12.276123046875 ms
// save_cost_2: 0.010009765625 ms

可以多测试很多次,单次值的大小对存的性能影响非常大,读取也一样,合情合理之中。


所以尽量不要保存大的值,因为其是同步读取,纯大数据,用indexedDB就好。


答案:键的数量对读取性能有影响,但是不大。值的大小对性能影响更大,不建议保存大的数据。


写个方法统计一个localStorage已使用空间


现代浏览器的精写版本:


function sieOfLS() {
return Object.entries(localStorage).map(v => v.join('')).join('').length;
}

测试代码:


localStorage.clear();
localStorage.setItem("🔴", 1);
localStorage.setItem("🔴🔴🔴🔴🔴🔴🔴🔴", 1111);
console.log("size:", sieOfLS()) // 23
// 🔴*9 + 1 *5 = 2*9 + 1*5 = 23

html的协议标准


WHATWG 超文本应用程序技术工作组 的localstorage 协议定了localStorage的方法,属性等等,并没有明确规定其存储空间。也就导致各个浏览器的最大限制不一样。


其并不是ES的标准。


页面的utf-8编码


我们的html页面,经常会出现 <meta charset="UTF-8">
告知浏览器此页面属于什么字符编码格式,下一步浏览器做好解码工作。


<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>容器</title>
</head>

这和localStorage的存储没有半毛钱的关系。


localStorage扩容


localStorage的空间是 10M的字节数,一般情况是够用,可是人总是有贪欲。
真达到了空间限制,怎么弄?


localStorage扩容就是一个话题。


作者:云的世界
链接:https://juejin.cn/post/7030585901524713508

收起阅读 »

(转载)网文也要元宇宙!虚拟看书,这也行?

“元宇宙”概念股热度持续。此刻,已经包括了来自网文领域的元宇宙。例如中文在线,自从拉上和元宇宙的关系,就一度飞涨。从10月底至今,中文在线在投资者互动平台上发布了超过6000字的与元宇宙相关的内容。面投资者对于其蹭热点的质疑,中文在线11月9日在投资者互动平台...
继续阅读 »

“元宇宙”概念股热度持续。

此刻,已经包括了来自网文领域的元宇宙。

例如中文在线,自从拉上和元宇宙的关系,就一度飞涨。

从10月底至今,中文在线在投资者互动平台上发布了超过6000字的与元宇宙相关的内容。

面投资者对于其蹭热点的质疑,中文在线11月9日在投资者互动平台表示,“公司已申请如中文元宇宙、IP元宇宙、17K元宇宙、四月天元宇宙等商标。”

中文在线表示,“借助5G、AI、AR/VR等技术的发展,公司的沉浸式互动阅读将会借助技术的赋能加速延展,构建一个互动性更强的平行世界。作为数字内容公司,本质是生产内容、打造IP、连接人群、丰富娱乐、学习和社交。文字创作和阅读仅存在于2D,但随着元宇宙的到来通过AR/VR等技术可以让人沉浸式体验新世界,而公司拥有海量的库存将会有力地支持构建平行的互动阅读世界。”

据了解,中文在线2000年成立于清华大学,2015年1月21日在深交所创业板上市,成为中国“数字出版第一股”。

其拥有数字内容资源超过460万种,签约版权机构600余家,签约知名作家、畅销书作者2000余位。

旗下拥有17K小说网、四月天小说网、汤圆创作三大网络文学原创平台,驻站网络作者超过390万名。

作为网络文学公司,除中文在线外,阅文集团、掌阅科技均未披露与元宇宙相关的信息。

而海量IP支持构建平行的互动元宇宙场景又是否立得住脚呢?

对此,《华夏时报》记者于玉金、《南方都市报》记者张洁莹分别和书乐进行了一番交流。

贫道以为:

中文在线目前而言,技术实力尚不足以支撑起以VR为底色的元宇宙。

但宣布进军元宇宙,某种意义上是表现出自身的目标与追求,后期则看是否投入海量资金进行研发,才能确定是实干还是蹭热点。

整个资本市场上的元宇宙热,则可以视作一个爆炒蹭热点,毕竟元宇宙真正要形成战斗力,至少还要十年。

事实上,这种概念炒作并不新鲜。

此前每一轮风口,都会有各种游资散户进入和蹭一波热点,尤其是在互联网科技巨头集体追捧的大风口下。

但这一波风口本身需要强大的技术支撑,而非此前O2O、比特币那种低门槛、以人力驱动的概念。

因此,随着市场对云宇宙的认知更加深刻,这样的蹭热点行径因为能量不足和,势必消歇。

事实上,所有互联网科技和游戏公司,都可以和元宇宙有关,不足为奇。

所以宣布进军元宇宙,哪怕未来只是在别人的元宇宙里开个“书店”,也是一种进军。

当然,此刻之所以蹭热点有用,还在于,元宇宙包含广阔,而且还处在待开发的史前时代,因此,任何方向进击都是可能的路径。

也因此,此前9月份,元宇宙概念也曾一度火热,遭遇市场降温后现下热度重燃,就在于真的巨头开始进击,如facebook、微软和其他。

真正互联网科技巨头本身认知到元宇宙必然是未来的大趋势(名称不重要,存在形式才是关键点),纷纷进击之后,导致了死灰复燃。

不过,贫道判断,由于技术门槛过高,蹭热点者很快会知难而退,让热度在年底前被新的热点所取代。

作者:张书乐(人民网、人民邮电报专栏作者,互联网和游戏产业观察者)

原文链接:https://new.qq.com/omn/20211115/20211115A06C8Z00.html

收起阅读 »

(转载)元宇宙的经济逻辑

继脸书(Facebook)把自己公司的名字改成了Meta之后,一贯对新概念保持低调态度的微软也终于放下了矜持,高调地加入了元宇宙的开拓大军。有意思的是,微软带给元宇宙的第一款产品叫做Mesh for Microsoft Teams。这是个什么样的产品呢?微软官...
继续阅读 »

继脸书(Facebook)把自己公司的名字改成了Meta之后,一贯对新概念保持低调态度的微软也终于放下了矜持,高调地加入了元宇宙的开拓大军。有意思的是,微软带给元宇宙的第一款产品叫做Mesh for Microsoft Teams。这是个什么样的产品呢?微软官方给出的说明是,这款产品要实现的主要是混合现实功能,把办公协作功能带入元宇宙。也就是说,现在人们在元宇宙也能用Excel制表、用PPT做幻灯片了!看着微软的这款新产品,打工人们不禁感叹:果然是“打工人,打工魂”,就算是躲进了元宇宙,终究还是逃不开打工的宿命啊!

脸书、微软对于元宇宙的青睐当然不是个例。事实上,自从元宇宙的概念爆火之后,就不断有公司宣称自己是元宇宙公司。例如,很多原来做VR的企业就说自己做的是元宇宙,因为人们通过他们的VR眼镜就能看到区别于现实的另一个世界;很多原来做游戏的企业也说自己做的是元宇宙,因为在玩游戏时,人的精神就会飘移到游戏所创造的新世界,而且还是“沉浸式”的。我不知道这些公司在进行类似的宣传时是否相信自己的说辞,但至少我身边的大多数朋友对这些说法的反应是:“就这?”

那么,问题出在哪儿呢?为什么这些企业确实在某些意义上把人们带入了一个新的世界,但人们却直观地认为,这些并不足以作为一个元宇宙呢?答案很简单——很多自我标榜为元宇宙的项目其实都有形无实。

从构词法上看,元宇宙是Meta和Verse的合体词。Meta是希腊语中超越的意思,而Verse则是宇宙(Universe)的简写。很显然,这样的构词结果已经明明白白地告诉了我们,元宇宙要成其为元宇宙,就必须满足两个层面的要求:一是要超越;二是要复刻。

所谓超越,要求的就是元宇宙必须要和现实世界不一样,在元宇宙里面,人们一定要能够做到某些现实世界中做不到的事情。而所谓复刻,指的则是需要在元宇宙里面建立起与现实世界类似的社会运行逻辑。从具体的设计上看,元宇宙的规则不必和现实世界完全一致,否则元宇宙也就失去了它存在的必要。但是,决定现实世界运行的一些关键逻辑和规则必须要在元宇宙中找到对应,否则人们就很难将元宇宙认可为一个真正的世界。

而从操作上看,建立一套与现实世界对应的运行逻辑,恐怕要比创造出各种令人着迷的VR特效难上不知道多少个数量级。从这个意义上看,如果我们要想创造出一个真正意义上的元宇宙,最艰巨的挑战,恐怕还是在元宇宙运行规则的复刻上。

元宇宙的运行规则可能是多层次的。在诸多的规则当中,最重要的应该是其中的经济逻辑。那么,在未来可能到来的元宇宙当中,经济逻辑究竟会怎样?或者说,它们应该怎样?抱着这些问题,我们不妨来作一番畅想吧。

稀缺的构建

在思考元宇宙的经济逻辑之前,我们不妨先对现实世界中的经济逻辑进行一些思考。在现实世界中,经济逻辑的起点是什么?似乎每一本关于经济学的教科书都有不同的回答。有的说是效用,有的说是价格,也有的说是产权。在我看来,其实以上所有这些概念都可以从一个更为原初的概念——“稀缺”(Scarcity),里面衍生出来。

所谓“稀缺”,指人欲望的无限性和现实条件有限性之间的矛盾。我们每个人都有各种各样的欲望,并且这种欲望是无限膨胀的。没吃饱时想吃饱,吃饱了后想吃好,吃得好了,又想穿得更好……欲望的膨胀总是无穷无尽。但相比于欲望,现实条件总是有限的,随心所欲的“买买买”很快就会把我们的钱袋掏空。面对有限的预算条件,就必须收敛无限的欲望。在进行购物决策时,我们就要给所有的欲望排一个序,确认出哪个更重要,而哪个相对次要一些,哪个要先买,哪个则可以先放一放。

在经济学的教科书里面,这个心理的排序被抽象出来,就是效用的概念。由于各种物品都可以给人带来各种各样的效用,因而人们就会对它们进行争夺。这时,谁拥有某物,而谁不拥有,谁可以支配某物,而谁不可以等问题就变得很重要了。于是,产权的概念也就应运而生。有了产权,人们就可以定分止争,从而免于为获取某物而诉诸暴力。这时,市场的手段,也就是用自己拥有的去换取自己所想要的,就成了配置资源的最主要方式。如果一个人是理性的,那么他在购买商品时总会不断在自己的所欲与所有之间精打细算。他们需要尽力地在市场上讨价还价,以便让自己所占有的东西能够尽可能换到更多自己所要的东西。通过市场上无数个人的交换,就形成了对世间万物的供给和需求,而供给和需求一起,就决定了世间万物的价格。

通过以上的简单推演,我们可以看到,无论是效用、价格,还是产权,其实都可以还原到稀缺这个概念。在现实的世界为什么会有稀缺,这一点似乎是不言自明的,因为物理的规律决定了这一切,原子化的世界决计不可能支撑起我们所有的欲望。但是在元宇宙,稀缺这个概念本身可能就会成为一个问题。元宇宙是一个比特的世界,里面的万事万物,归根到底就是一串代码,都可以简单地修改代码来得到。想要什么,就可以有什么。即使是在真实世界里千金难买的时间,人们也可以调整对于脑部的刺激来实现。在这样的情况下,稀缺本身可能就不存在了。

那么,在元宇宙当中,真的不会有稀缺了吗?情况当然不会是这样。事实上,即使在元宇宙,稀缺也会存在。而且,它必须有。“虚拟经济学”(virtual economics)领域的先驱、美国印第安纳大学教授爱德华·卡斯特罗诺瓦(Edward Castronova)曾经对数字条件下稀缺性存在的必然性给出过一个解释。他认为,稀缺性的存在,其实是人们为了提升在虚拟世界中的体验而作出的一种人为设定。从人性上看,我们每个人都喜欢拥有自己的个性,而拥有差异化的物品,就是个性在外界的一种投射。试想,如果所有的人只能够吃一样的东西、穿一样的衣服、住一样的房子,那么这个世界将是多么无聊啊。正是由于这个原因,即使从技术上看,人们完全可以在虚拟世界中获得任何自己想要的东西,他们也必须人为地制造出差异化和稀缺来。

应该说,卡斯特罗诺瓦的以上观点是颇具吸引力的。作为特殊的虚拟世界,在元宇宙当中,这个逻辑当然也依然成立。不过,在我看来,除了这个理由之外,在元宇宙当中设定稀缺性,其实还有一个重要的理由,就是要确立起人们建设元宇宙的激励基础。

在大多数的想象版本中,元宇宙都不是一个一次性整体成型的世界。相反,和现实世界一样,它本身可以不断地演化。而这种演化,则需要参与其中的每一个人的共同努力。例如,在一些具有元宇宙要素的游戏当中,整个世界的搭建需要所有人的协力,大家配合得越好,游戏中的世界就发展越好。而对于一些更为开放式的元宇宙,则不仅需要参与者在元宇宙内部通力合作,更需要他们在游戏之外改进游戏的代码,甚至更新硬件。参与者奉献得越多,这个元宇宙发展得也就越好。在这种情况下,如何激励好每一个参与者,让他们保持充足的奉献精神就成了一个问题。

经验告诉我们,在合作参与的人数较少时,要协调每一个人的激励是相对容易的。在大多数时候,人们依靠自己内心的道德准则,或者“为爱发电”的冲动,就可以解决所有问题。然而,当合作规模扩大的时候,单纯依靠自愿和利他的道德冲动就很难持久维系每一个合作者的参与热情。例如,互联网早年的很多论坛,在规模发展到一定程度之后,就会陷入混乱或者沉寂。因此,要维护社区的长期活力,就必须能够构建起一套相应的激励,让每一个人的付出都能获得相应的回报。为了达到这个目的,人们就需要在合作中引入能够被用于激励的价值,而为了实现这一点,就必须首先创造出它的逻辑起点,也就是稀缺。

尽管看起来有点不可思议:在类似元宇宙这样的一个虚拟世界,稀缺并非像真实世界那样,源自于物理规律的限制,而是来自于人们的建构,但现实确实是如此。事实上,现在人们所做的很多工作,就是试图在元宇宙当中构建起稀缺性。例如,现在的数字水印、数字权利管理(Digital Rights Management,简称DRM),以及非同质化通证(Non-Fungible Token,简称NFT)等重要技术,其实都是为了构建稀缺的技术。以前一段时间十分火爆的NFT为例,很多人都认为,这将是支撑未来元宇宙发展的一项关键技术。但是,NFT究竟有什么用呢?究其本质,就是它可以在元宇宙内创造出差异化、创造出稀缺。在元宇宙当中,人们完全可以对数码造物实现无限的复制,稀缺本来可以不存在。而借助于NFT技术,每一个物品都可以被打上独有的标签,或者赋予特殊的涵义,从而成为独一无二的东西。这样一来,稀缺就被制造了出来。

价值的确定

有了稀缺性之后,元宇宙经济系统就有了自己的逻辑起点。从理论上看,一切在现实世界中有的经济概念,都可以随之演化出来。

和现实中一样,人们在元宇宙当中通过不断地交互,可以逐步摸索出各种物品的相对价值。类似的实践,其实已经可以在不少大型的网络游戏中看到了。事实上,早在十多年前,“远古”游戏《暗黑破坏神2》中就自发演化出了一个价值交换体系。游戏中的一种符文被玩家作为了刻画价值的一般等价物,被人们用来交换各种道具。这里的交换策略并不是游戏开发者给定的,但大批的玩家在长期的交互过程中就形成了一种约定俗成的规律。而在现在有元宇宙概念的游戏当中,由于一般都引入了通证(Token)体系,所以这种价值的自发演化就会变得更快。在游戏中,一件物品可以值多少个通证,可以和其他的什么物品进行交换,都可以在自发当中被安排得明明白白。我想,游戏既然如此,未来更大规模的元宇宙实践当然也可以实现类似的过程。

和现实世界不同的是,在元宇宙的经济系统发展之初,现实的世界就已经是一个前定的存在了。因此,现实世界对元宇宙的影响可能会成为元宇宙价值决定的一个重要影响因素。

事实上,现在很多元宇宙当中的资产都是直接通过直接拍卖来进行初次配置的。比如,在Axie Infinity、Decentraland、Sandbox等有元宇宙概念的游戏当中,就都有土地拍卖的概念。参与其中的玩家可以像参与真实世界的土地拍卖一样,购买虚拟世界当中的地产,而荷式拍卖则是实现这种交易的最重要手段。从交易的结果来看,这些虚拟土地的价格通常都价格不菲。例如,2021年6月,Axie Infinity的9块虚拟土地以888.25以太坊(ETH)的高价出售,根据以太坊当时的价格,这批虚拟土地的成交价格约为150万美元;而2021年7月,Sandbox上面积超过530万“平方米”(注:这里的一个平方米指的是一个24*24的点阵)的虚拟土地以近88万美元的价格出售。

很显然,通过上述的拍卖,元宇宙中物品的价值就可以很容易地与现实世界建立一定的锚定。而根据每种物品与现实世界之间的价值比值,这些物品在元宇宙内部的交换价值也就可以更容易被确定了。

货币的引入

当各种物品的价值被确定后,元宇宙内的交易就可以开展起来了。当然,就像在现实世界一样,当交易的规模扩展到一定程度之后,它就不可能持续地以一种以货易货的形式存在了,基于货币的交易将会成为发展的必然。

那么,在元宇宙当中,货币会采用一种什么样的形式呢?我想,要思考这个问题,不妨先看一下现实世界当中的货币是怎么演化的。马克思在《资本论》当中曾经有一句名言,叫做“金银天生不是货币,但货币天生是金银”。这是什么意思呢?大致上就是讲,金银天生具有能够充当固定的一般等价物,也就是货币的各种良好性质:它们不仅质地均匀,容易分割,价值稳定,而且便于携带——虽然金银本身分量并不轻,但由于它们代表的价值很高,所以在现实中,人们携带很轻的一部分金银就足以满足大部分的交易需要了——用更为现代的经济学语言讲,就是用金银来充当货币,是可以最大幅度降低交易成本的。当然,后来随着交易规模的不断扩大,金银也不再适合作为流通货币存在于市场上了。于是人们就发明了纸币,而金银则作为储备,用以支撑纸币的价值。

参考现实世界中的货币演化,我们就可以重新来思考元宇宙中的货币问题。在元宇宙中,如果有货币,那么它本身就是以数字形态存在的,它的质地、分割等,显然不是问题。问题的关键是,它到底能否有效保证价值稳定,以及能否有效地达到节约交易成本的目的。在审视可行的方案时,这两个问题就可以作为检验的指标。

一些人认为,在元宇宙当中,以比特币为代表的加密货币可能会扮演货币的角色。我对此是有些怀疑的。一方面,加密货币的币值太不稳定,这很难满足作为流通货币的需要。另一方面,至少从目前看,加密货币的交易效率非常低。以比特币为例,其设计要求每完成一笔交易就需要对全网进行广播验证,因而每笔交易都需要花费很长的时间,花费很大的算力。从这个意义上看,要用加密货币来直接作为元宇宙的货币恐怕并不合适——尤其是当这个元宇宙的规模比较大时,这种设定的效率就会非常低。

一种或许更为可取的方式是,在每一个元宇宙内部都开发独立的通证。为了实现交易的效率,这些通证未必需要和比特币一样建筑于区块链技术之上。而为了保证币值的稳定,这些通证可以采用某些资产锚定,以资产作为储配的方式来发行。这里的资产可以是现实世界当中的货币,也可以是一篮子加密货币,选取的标准应当以它们有相对稳定的价值为标准。这样,元宇宙内的货币体系就可以建立起来了。

各种设计必然是有利也有弊的。如果使用了区块链技术来作为支持,尽管交易效率较低,但交易的安全性却可以获得比较好的保证。而反过来,如果放弃了直接使用区块链技术,那么交易的效率固然高,但交易的安全性则可能会受到影响。考虑到这点,我们或许可以引入一种抽检制度,在所有的交易当中按照一定比例抽取部分交易作为检查,一旦发现交易有造假,则给予重罚。从理论上讲,只要处罚设置足够高,就可以有效抑制人们的造假动机——这种惩罚策略在现实世界当中或许不成立,但在元宇宙这样的虚拟世界,可能就会更加适应。通过引入这样的机制,我们就可以在让交易安全达到足够程度保证的基础上,尽可能实现交易的高效率。

需要指出的是,当把元宇宙内部的通证和外部的资产直接挂钩时,也会引起一些副作用。因为在这种设定下,就相当于取消了元宇宙的货币发行独立性。如果有人将大批外部资产(如美元、人民币)兑换为元宇宙内部的通证时,元宇宙就可能面临急速的通货膨胀,并伴随着巨大的分配失衡。这就好像在一个网游当中,一下子引入了一批满身神装的“RMB玩家”,那么其他玩家的游玩体验就会大幅降低。考虑到这种情况,或许可以设计一个机制,当外部的货币流入达到一定值后,就启动“汇率”的浮动,让元宇宙内的通证对外补货币升值。通过这种机制,就可以比较好的保证在元宇宙内部货币价值的稳定性。

生产要素的交易

下面讨论一下元宇宙的生产要素交易。在现实世界,劳动、资本和土地是最为重要的生产要素。那么,元宇宙当中,有哪些是重要的生产要素呢?在我看来,比较关键的可能有两样,一是劳动,二是算力。至于元宇宙中所谓的“土地”,正如前面所说的,它们更多是一种人为构建出来的稀缺资源,如果有了足够的劳动和算力,它们本身是可以被构建出来的。由于这个原因,我更愿意把它作为一种一般意义上的商品来看待,而不认为它是一种独立的生产要素。

与元宇宙相关的劳动可以分为很多种:

第一种是在元宇宙经济体系内的劳动。作为一个虚拟的空间,元宇宙的价值很大程度上取决于其给人的体验。而为了保证这种体验,元宇宙中就需要安排一些专门用于和人交互的NPC。当然,这种NPC由谁来当,就是一个选择。一个方案找一些AI来当NPC。但是,从现在看,这些AI给人的交互体验绝对达不到《失控玩家》里面那样的水平,因而很难满足人们的需要。而另一个方案,就是专门找一些人来扮演NPC。如果采用这种方案,那么NPC和人的交互活动就形成了一种劳动。和真实世界当中一样,这样的劳动也需要得到报偿。

除此之外,在元宇宙当中,很多任务可能是需要多人协同完成的。例如,在被称为“元宇宙第一股”的Roblox游戏中,人们就需要一起建设社区,一起建设城市。这种共同的建设如果是出于所有玩家自愿的,那么这就是一种协作。但如果这种共同建设是某人要求其他人做的,那么它就成为了一种劳动的雇佣关系。这个时候,城市的建设也就成了一种在元宇宙内的打工劳动。

第二种是支撑元宇宙的劳动。如前所述,元宇宙要运转好,需要很多相应的技术支撑。比如,程序的底层需要有人开发,bug需要有人来处理,这些劳动,尽管不发生在元宇宙内部,但它们对元宇宙的发展却是必不可少的。

第三种则是发生在元宇宙内部的劳动。例如,真实世界的打工人转战元宇宙,在里面用微软的MeshforMi-crosoftTeams写Word,做PPT,然后拿着PPT开会。这些活动只是发生在元宇宙内部,但是从本质上来讲,它们依然是真实世界劳动的延伸。

对于以上三类劳动,第一种毫无疑问应该用元宇宙内部的通证来激励。事实上,在AxieInfinity等具有元宇宙概念游戏中,已经提出了“玩中赚”(play toearn,简称P2E)的概念。在这些游戏中,玩家或可以根据其游戏的时长来获取相应的通证,或可以通过打怪升级来获得独有的装备NFT,所有的这些,都可以作为他们的报酬。值得一提的是,在AxieInfinity上,有不少玩家都是因疫情而失业的人。对于他们来讲,P2E就成了获得收入的一个重要来源。

除了第一类劳动,第二、三类劳动严格来说都是元宇宙之外的,因而他们的报酬可以通过真实世界的货币,也可以通过通证来结算。当然,从促进元宇宙的发展来说,以通证结算或许是比较有利的。这可以促使人们以更高的频率使用元宇宙,从而可以从多个方面促进其发展。

至于元宇宙发展所需要的算力,则可以通过仿照比特币网络的做法,以工作量证明来分配一定的通证作为回报。当然,在现实中,为了吸引普通用户进行分布式的算力供应,也有一些产品试图将算力的供应包装成某种形式的游戏。这样,人们就可以在游戏当中就实现了算力提供,并且同时获得了相应的报酬。

“跨宇宙”的价值交换

从构建看,元宇宙绝对不可能是一蹴而就的,它更可能以一个个独立项目的形式发展起来——这就好像互联网的发展并不是源自于一个统一的设计,而是源自于一个个独立建立的网站。不过,就像一个个孤立的网站并没有价值一样,如果每一个元宇宙项目都是彼此独立的,而不能彼此联通,那么它们的价值也终将是有限的。唯有把各个元宇宙打通,让人们在它们之间自由穿梭,元宇宙的价值才能得到真正的体现。

然而,一旦我们要让彼此独立发展的元宇宙实现互联互通,就会涉及很多的问题。这些问题中,除了技术的困难外,还有一些经济问题恐怕是需要关心的:由于各个元宇宙项目是独立打造的,其建造者和建造规范各不相同,那么它们之间的通证价值如何兑换?一个人在A宇宙当中拥有的财富,应该通过什么比例折算到B宇宙?

如果从现实世界的经济理论看,这会是一个十分复杂的汇率决定问题。所幸的是,随着去中心化金融(DeFi)的发展,这个问题的解决就有了很好的解决基础。现在,借助于Uniswap等DeFi产品,人们已经可以很容易在不同的区块链项目之间实现兑换。所有的一切,程序都可以根据供求状况,通过可编程(Programmable)的方式来实现。在未来,这套系统应该可以被用到跨元宇宙的通证兑换当中去。不过,在应用到类似系统时,我们也需要对DeFi产品的运行风险做好监管和预防。比如,在现实当中,已经有人利用不同区块链产品之间的设计差异,综合使用多种DeFi产品来设计出了套利方案,公然套取了大量的财富。对于类似的情况,应当要做好提前的预防工作。否则,一不小心就可能酿成“跨宇宙”的金融风险,其影响可能是十分巨大的。

元宇宙治理和分配政策

通过以上的分析,我们已经可以大致看到一个类似于现实世界的元宇宙经济运作框架。但是,和现实世界一样,元宇宙也会遭遇很多的经济和社会问题。在现实世界中,很多问题可以诉诸于政府来进行解决,而在元宇宙当中,可能没有类似现实世界当中这样强力的政府组织,因此各种问题的解决恐怕需要依靠元宇宙参与者的自发治理来实现。

从现阶段看,区块链上“分布式自主组织”(DAO)的治理实践或许可以为未来的元宇宙治理提供借鉴。DAO有很多重要的特点,例如,它具有自动化执行的统一规则,具有很强的透明度,权益相关者都可以表达自己的利益诉求,并且可以通证来对参与者进行有效的激励。实践证明,只要DAO的设计比较得当,就可以充分地调动各方面的积极性,比较好地达成组织治理的目的。我想,在元宇宙的场景下,类似的经验也可以得到有效的复制。

这里需要指出的是,一个组织的有效治理,必须是以地位平等为前提的。在现实中,随着贫富分化的日益加深,人与人之间的经济地位会形成重大的差异。而经济地位上的差异则会反过来影响政治参与的热情——事实上,很多国家的实践都告诉我们,赤贫的人是几乎没有参政的积极性的。而反过来,由于他们的声音无法被听到,诉求无法被表达,因而也就很难被政府关注,其境况也就很难得到改善。和现实类似,在元宇宙当中,人与人之间也可能会形成贫富分化,那么如何类似以上的现象出现呢?我想,一个可能的做法是制定最低收入制度,为所有人发放能够在元宇宙生存的基本收入。唯有如此,才可能调动哪些元宇宙当中那些“穷人”的积极性,让他们也参与到对于元宇宙的治理当中来。

结语

好了,我想是时候结束这次对于元宇宙经济逻辑的畅想了。尽管我自认为在进行以上畅想时已经秉承了比较严谨的态度,但我几乎可以肯定,未来的元宇宙发展几乎不可能和上面的推测一样。

现实永远要比想象更为精彩!

原文链接:https://finance.ifeng.com/c/8BBhOmKdq9Z

收起阅读 »

(转载)10个问题说清楚,什么是元宇宙

作者: 赵颖2021-11-12 19:14英伟达、微软、阿里云早已布局,建立起元宇宙的底座?区块链、NFT将迎来怎样的发展?去中心化又将如何实现?人们对于元宇宙的构想十分多元且抽象,这十个问题将抽象的元宇宙具象化,帮助人们更好地理解。一、什么是元宇宙?1)元...
继续阅读 »


作者: 赵颖
英伟达、微软、阿里云早已布局,建立起元宇宙的底座?区块链、NFT将迎来怎样的发展?去中心化又将如何实现?

人们对于元宇宙的构想十分多元且抽象,这十个问题将抽象的元宇宙具象化,帮助人们更好地理解。

一、什么是元宇宙?

1)元宇宙概念的提出

元宇宙在很长一段时间内仅存在于文学与影视作品中。元宇宙(Metaverse)由Meta和Verse两个词根组成,Meta表示“超越”“元”,verse表示“宇宙Universe”。Metaverse一词最早来自1992年的科幻小说《雪崩》。小说描绘人们在虚拟现实世界中通过控制自己的数字化身相互竞争以提升社会地位。在其后的接近30年间,元宇宙的概念在《黑客帝国》《头号玩家》《西部世界》等影视作品,《模拟人生》等游戏中有所呈现。在这一阶段,元宇宙的概念比较模糊,更多地被理解为平行的虚拟世界

2)元宇宙八大要素

根据元宇宙概念上市公司Roblox的定义,元宇宙应具备身份、朋友、沉浸感、低延迟、多元化、随地、经济系统、文明等八大要素。元宇宙的表现形式大多以游戏为起点,并逐渐整合互联网、数字化娱乐、社交网络等功能,长期来看甚至可以整合社会经济与商业活动。

3)元宇宙第一股Roblox上市

2021年3月10日,Roblox采取直接挂牌模式(DPO)在纽约证券交易所上市。Roblox认为元宇宙用于描述虚拟宇宙中持久的、共享的、三维虚拟空间的概念。

4)元宇宙总结了前期科技的发展方向

东方证券分析师张颖指出,在迸发元宇宙概念前,5G基础设施、用于智能终端的显示屏、AI芯片等技术不断发展演进,同时工业互联网、产业互联网、数字孪生、VR游戏等概念均不断成熟。而在元宇宙概念诞生后,可以很好地总结这一时期大部分技术的发展方向。

二、元宇宙的架构?

东方证券认为,元宇宙的载体与内容这两个概念十分宽泛,主要分三部分:

  • 元宇宙的底层由基础设施与终端硬件设备组成:包括但不限于人机交互、3D引擎、GIS、设计工具、游戏渲染、画面渲染、隐私计算、AI、操作系统、工业互联网、内容分发、应用商店以及智能合约;
  • 在此基础上,元宇宙还需要大量的软件与技术协同:包括但不限于:基础设施端的5G、6G、云计算、区块链节点、边缘计算节点、DPU;用户端的路由器、传感器、芯片、VR头显、显示器、脑机接口;
  • 基于此,元宇宙可以衍生出相应的应用,并基于元宇宙各类应用发展出潜在的内容载体。

三、元宇宙的发展模式?

1)元宇宙的发展是循序渐进的过程,技术端、内容端、载体端都在不断演变

  • 技术端,区块链技术在不断演进,以以太坊为代表的社区在探索区块链应用如何丰富化,以Coinbase、Uniswap以及Opesea为代表的交易所也在为区块链经济提供更好的交易能力;
  • 内容端,元宇宙概念的游戏不断增加,生态不断加强,用户数也随之增长。以Roblox、Sandbox为代表的UGC元宇宙概念游戏得益于玩家的参与而不断丰富自己游戏的内容;
  • 载体端,通信技术、虚拟现实、芯片等底层技术也在不断演进。

四、元宇宙的渗透路径?

这个问题也可以理解成我们距离元宇宙的距离,东方证券张颖表示,元宇宙的内容短期将集中于游戏端与艺术端(NFT艺术藏品),长期来看,元宇宙的渗透路径预计将为“游戏/艺术-工作-生活”

1)游戏端:以Roblox为代表

Roblox平台主要由三个产品构成:

  • 客户端:允许用户探索3D数字世界的应用程序。(面向用户);
  • 工作室:允许开发人员和创作者构建、发布和操作Roblox客户端访问的3D体验和其他内容的工具群。(面向开发者)
  • Roblox云:为共同体验平台提供动力的服务和基础设施。

Roblox的主要营收来源为用户的游戏内支出。玩家需要充值换取游戏中的代币Robux获取Roblox的各种功能,这也是Roblox的营收来源。

并且,Roblox的激励机制十分明确,除掉25%支付给APPStore的营收以及用于平台各种费用的营收(约26%),剩下约49%的营收基本由公司和开发者平分。

其实,游戏UGC平台的概念可以追溯至魔兽争霸3:魔兽争霸3WE地图编辑器支持开发者创造出许多RTS(即时战略游戏)、MOBA(多人在线战术竞技)的游戏地图和游戏类型。但魔兽3对于开发者的奖励机制缺失,导致整体商用化程度不高。

2)艺术端:NFT构建元宇宙经济基础

非同质化代币(NFT)具有不可互换性、独特性、不可分性、低兼容性以及物品属性。并且产品流通渠道单一,市场透明度、价格发现能力均有较高提升空间。

目前,多家互联网大厂正试水NFT领域:2021年6月,阿里巴巴发售支付宝付款码皮肤NFT,2021年8月,腾讯围绕NFT进行一系列战略布局。

3)工作端:Facebook与英伟达的布局

Infinite office是Facebook元宇宙战略中重要环节。2020年9月,Facebook宣布推出VR虚拟办公应用InfiniteOffice,支持用户们创建虚拟办公空间,提高工作效率。

英伟达推出了NVIDIA Omniverse,一个专为虚拟协作和物理属性准确的实时模拟打造的开放式平台。并且已经开始投入使用,宝马公司正在内部推进NVIDIAOmniverse平台,以协调全球31座工厂的生产。而根据英伟达官网披露的信息,NVIDIA Omniverse将宝马的生产规划效率提高30%。

4)生活端:面向体验场景

东方证券认为,元宇宙的未来在于探索其应用场景的共性。这些应用场景均需考量用户的体验,元宇宙未来的商业模式与智能手机类似,即通过体验感增加用户的使用时间,进而提高用户粘性。这些时间(体验)成为元宇宙中各项服务的基础。

五、元宇宙时代有哪些确定性趋势?

元宇宙主要的载体(基础设施)主要包括如下几部分

1)网络(通信)

5G作为具有高速率、低时延和大连接特点的新一代宽带移动通信技术,是实现人机物互联的网络基础设施。

2)芯片(算力)

元宇宙的内容、网络、区块链、图形显示等功能均需要更为强大的算力。

云端算力方面,DPU芯片(数据处理芯片)通过分流、加速和隔离各种高级网络、存储和安全服务,为云、数据中心或边缘等环境中的各种工作负载提供安全的加速基础设施。

终端算力方面,异构芯片可以让SoC中的CPU、GPU、FPGA、DPU、ASIC等芯片协同工作,不断提升算力以提升用户体验。

3)云与边缘计算

云计算与边缘计算为用户提供所需的计算资源,降低用户触达元宇宙的门槛。

4)AI

AI在元宇宙中应用渗透较广泛。AI可以帮助创建元宇宙资产、艺术品和其他内容(AIGC),并可以改进我们用来构建所有这些内容的软件和流程。

六、元宇宙在产业端如何发挥价值?

全球范围内许多互联网企业、工业软件企业就工业元宇宙的相关技术已有长期的布局,其中包括数字孪生、工业互联网、仿真测试、数字化工厂、CAD、CAE、EDA等工业软件。

1)英伟达:Omniverse平台

Omniverse的架构包括Connect、Nucleus、Kit、Simulation、RTXrenderer等五个部分,他们与第三方数字内容创建工具(DCC)以及基于Omniverse的微服务构成了Omniverse的生态。

黄仁勋在参加Computex2021线上会议表示未来虚拟世界与现实世界将产生交叉融合,元宇宙与NFT将在其中扮演重要角色。其中Omniverse平台主要面向建筑、工程和施工;制造业;媒体和娱乐以及超级计算场景

根据英伟达官网对于Omniverse平台的介绍,通过Omniverse平台,用户可以完成实时虚拟协作、模拟现实的设计、模拟环境以及搭建未来工厂等操作。

2)微软:数字孪生探索

2021年9月,微软CEO Satya Nadella在Inspire2021演讲中提出全新“企业元宇宙”概念。微软的元宇宙计划中期望元宇宙可以打破现在的通信和业务流程之间的障碍,把他们融合在一起,让工业场景更为便捷。在宣布“企业元宇宙”概念之前,微软就已通过Azure数字孪生及AI等技术建立了工业元宇宙的底座。

Azure数字孪生是一个物联网(IoT)平台,可用于创建真实物品、地点、业务流程和人员的数字表示形式。通用电气航空的数字集团使用Azure数字孪生构建了一个实时和自动演变的模型,因此客户将随时可以访问其飞机的更新、准确和可用的数据模型。并且,通过内置数字可追溯性,可以实时记录每架飞机上每一个实物资产和部件。

3)能科股份:布局仿真与测试

能科股份主要业务包括智能制造、智能电气两个板块,其中公司智能制造业务基于数字孪生理念,整合业内先进工业软件和数字化IOT设备,虚拟世界内定义生产力中台并为客户开发个性化的工业微应用,物理世界内建立数字化、智能化的生产线和测试台,满足制造业企业产品全生命周期的数据与业务协同需求,帮助企业实现其自主创新、运营成本、生产效率、不良品率和客户满意度等业务目标。

4)阿里云:数字工厂“新基建”

根据德国工程师协会的定义,数字工厂(DF)是由数字化模型、方法和工具构成的综合网络,包含仿真和3D虚拟现实可视化,通过连续的没有中断的数据管理集成在一起。

阿里云工业互联网平台助力制造企业数字化转型,打造工厂内、供应链、产业平台全面协同的新基建,将工厂的设备、产线、产品、供应链、客户紧密地连接协同起来,为企业提供可靠的基础平台和上层丰富的工业应用,结合全面的产业支撑,助力企业完成数字化转型。

七、元宇宙是否需要区块链?

首先来理解一下区块连的技术特性,区块链是一种按时间顺序将不断产生的信息区块以顺序相连方式组合而成的一种可追溯的链式数据结构,是一种以密码学方式保证数据不可篡改、不可伪造的分布式账本。区块链借助自身的特性可以用于数字资产、内容平台、游戏平台、共享经济与社交平台的应用。基于自身的技术特性,天然适配元宇宙的关键应用场景。

东方证券认为,在元宇宙的整体架构中需要一套完善、缜密且成熟的技术系统支撑元宇宙的治理与激励。凭借区块链技术,元宇宙参与者可以根据在元宇宙的贡献度(时间、金钱、内容创造)等获得奖励。

八、元宇宙是否需要去中心化?

1)个人信息可携带权

在个人信息可携带权的时代,用户成为关键参与者,由用户主动发起个人信息数据传输并自行上传,从而实践个人数据可携带权,去中心化成为必不可少的条件。

2)去中心化不等于没有中心、没有监管

在去中心化概念下,仍有较为高级的节点参与治理或运营,这与分布式架构完全舍弃中心的概念不同。在去中心化概念下,有效的监管和治理仍可存在。

3)去中心化如何践行?参考DAO

去中心化自治组织(DecentralizedAutonomousOrganization,DAO)是基于区块链核心思想理念,由达成同一个共识的群体自发产生的共创、共建、共治、共享的协同行为衍生出来的一种组织形态,是区块链解决信任问题后的附属产物。

DAO将组织的管理和运营规则以智能合约的形式编码在区块链上,从而在没有集中控制或第三方干预的情况下自主运行。DAO具有充分开放、自主交互、去中心化控制、复杂多样以及涌现等特点,可成为应对不确定、多样、复杂环境的有效组织。

4)去中心化如何交易?参考DEX

去中心化交易所(DEX)在没有任何形式的中央权力的情况下以分散的方式运作。DEX不需要第三方来管理用户的资产,并允许用户随时保留对私人密钥的控制。由于DEX交易是点对点交易,它们提供了更高的透明度水平。

全球主流区块链去中心化交易所包括基于以太坊网络的Uniswap、Sushiswap、IDEX、Bancor、Kyber,基于币安智能链的Pancakeswap,基于Heco链上的MDEX等。

九、元宇宙是否需要NFT?

1)什么是NFT?区块链的主流资产之一。

NFT(Non-fungibleToken)代表不可替代的代币,是可以用来表示独特物品所有权的代币。NFT让艺术品、收藏品甚至房地产等事物标记化。他们一次只能拥有一个正式所有者,并且他们受到以太坊等区块链的保护,没有人可以修改所有权记录或复制/粘贴新的NFT。

NFT具有不可互换性、独特性、不可分性、低兼容性以及物品属性,可应用于流动性挖矿、艺术品交易、游戏/VR以及链下资产NFT化等场景,大幅提升数据流转效率。

2)NFT应用:一种潜在的元宇宙经济模式

NFT由于自身的数字稀缺性被率先运用于收藏、艺术品以及游戏场景。

东方证券认为,NFT在元宇宙中将扮演关键角色。首先,区块链是连接元宇宙概念的重要技术;其次,在元宇宙的整体架构中,在基础设施、数据和算法层之上、应用层之下,需要一套完善、缜密且成熟的技术系统支撑元宇宙的治理与激励;而NFT可以充当元宇宙激励环节的媒介

十、元宇宙时代,互联网形态是否会发生变化?

东方证券认为,基于元宇宙的发展,互联网的协议可能发生改变,互联网会针对于打造可信化的数字底座进行演进。而区块链技术也在攻克自身的缺陷:交易吞吐量低、与外界沟通困难等。

注:本文内容主要摘录自东方证券研报《十问元宇宙:如何将抽象的概念具象化?》

原文链接:https://wallstreetcn.com/articles/3644791
收起阅读 »

Vue新玩具VueUse

vue
什么是 VueUse VueUse 是一个基于 Composition API 的实用函数集合。通俗的来说,这就是一个工具函数包,它可以帮助你快速实现一些常见的功能,免得你自己去写,解决重复的工作内容。以及进行了基于 Composition API 的封装。让...
继续阅读 »

什么是 VueUse


VueUse 是一个基于 Composition API 的实用函数集合。通俗的来说,这就是一个工具函数包,它可以帮助你快速实现一些常见的功能,免得你自己去写,解决重复的工作内容。以及进行了基于 Composition API 的封装。让你在 vue3 中更加得心应手。


简单上手


安装 VueUse


npm i @vueuse/core

使用 VueUse


// 导入
import { useMouse, usePreferredDark, useLocalStorage } from '@vueuse/core'

export default {
setup() {
// tracks mouse position
const { x, y } = useMouse()

// is user prefers dark theme
const isDark = usePreferredDark()

// persist state in localStorage
const store = useLocalStorage(
'my-storage',
{
name: 'Apple',
color: 'red',
},
)

return { x, y, isDark, store }
}
}

上面从 VueUse 当中导入了三个函数, useMouseusePreferredDarkuseLocalStorageuseMouse 是一个监听当前鼠标坐标的一个方法,他会实时的获取鼠标的当前的位置。usePreferredDark 是一个判断用户是否喜欢深色的方法,他会实时的判断用户是否喜欢深色的主题。useLocalStorage 是一个用来持久化数据的方法,他会把数据持久化到本地存储中。


还有我们熟悉的 防抖节流


import { throttleFilter, debounceFilter, useLocalStorage, useMouse } from '@vueuse/core'

// 以节流的方式去改变 localStorage 的值
const storage = useLocalStorage('my-key', { foo: 'bar' }, { eventFilter: throttleFilter(1000) })

// 100ms后更新鼠标的位置
const { x, y } = useMouse({ eventFilter: debounceFilter(100) })

还有还有在 component 中使用的函数


<script setup>
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'

const el = ref()

function close () {
/* ... */
}

onClickOutside(el, close)
</script>

<template>
<div ref="el">
Click Outside of Me
</div>
</template>

上面例子中,使用了 onClickOutside 函数,这个函数会在点击元素外部时触发一个回调函数。也就是这里的 close 函数。在 component 中就是这么使用


<script setup>
import { OnClickOutside } from '@vueuse/components'

function close () {
/* ... */
}
</script>

<template>
<OnClickOutside @trigger="close">
<div>
Click Outside of Me
</div>
</OnClickOutside>
</template>


注意⚠️ 这里的 OnClickOutside 函数是一个组件,不是一个函数。需要package.json 中安装了 @vueuse/components



还还有全局状态共享的函数


// store.js
import { createGlobalState, useStorage } from '@vueuse/core'

export const useGlobalState = createGlobalState(
() => useStorage('vue-use-local-storage'),
)

// component.js
import { useGlobalState } from './store'

export default defineComponent({
setup() {
const state = useGlobalState()
return { state }
},
})

这样子就是一个简单的状态共享了。扩展一下。传一个参数,就能改变 store 的值了。


还有关于 fetch, 下面👇就是一个简单的请求了。


import { useFetch } from '@vueuse/core'

const { isFetching, error, data } = useFetch(url)

它还有很多的 option 参数,可以自定义。


// 100ms超时
const { data } = useFetch(url, { timeout: 100 })

// 请求拦截
const { data } = useFetch(url, {
async beforeFetch({ url, options, cancel }) {
const myToken = await getMyToken()

if (!myToken)
cancel()

options.headers = {
...options.headers,
Authorization: `Bearer ${myToken}`,
}

return {
options
}
}
})

// 响应拦截
const { data } = useFetch(url, {
afterFetch(ctx) {
if (ctx.data.title === 'HxH')
ctx.data.title = 'Hunter x Hunter' // Modifies the resposne data

return ctx
},
})

作者:我只是一个小菜鸡
链接:https://juejin.cn/post/7029699344596992031

收起阅读 »

token过期自动跳转到登录页面

vue
这几天项目提测,测试给我提了个bug,说token过期,路由应该自动跳转到登陆页面,让用户重新登录。先说下一些前置条件, 1:我公司的token时效在生产环境设置为一个小时,当token过期,所有接口都直接返回 2:每次路由跳转都会对token进行判断,设置了...
继续阅读 »

这几天项目提测,测试给我提了个bug,说token过期,路由应该自动跳转到登陆页面,让用户重新登录。先说下一些前置条件,
1:我公司的token时效在生产环境设置为一个小时,当token过期,所有接口都直接返回
2:每次路由跳转都会对token进行判断,设置了一个全局的beforeEach钩子函数,如果token存在就跳到你所需要的页面,否则就直接跳转到登录页面,让用户登录重新存取token


接口返回的信息
{
code:10009,
msg:'token过期',
data:null
}
全局的路由钩子函数
router.beforeEach(async(to, from, next) => {
//获取token
// determine whether the user has logged in
const hasToken = getToken()

if (hasToken) {
//token存在,如果当前跳转的路由是登录界面
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done()
} else {
//在这里,就拉去用户权限,判断用户是否有权限访问这个路由
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
} else {
//token不存在
if (whiteList.indexOf(to.path) !== -1) {
//如果要跳转的路由在白名单里,则跳转过去
next()
} else {
//否则跳转到登录页面
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})

所以我直接在对所有的请求进行拦截,当响应的数据返回的code是10009,就直接清空用户信息,重新加载页面。我对代码简化了下,因为用户在登录时就会把token,name以及权限信息存在store/user.js文件里,所以只要token过期,把user文件的信息清空。这样,在token过期后,刷新页面或者跳转组件时,都会调用全局的beforeEach判断,当token信息不存在就会直接跳转到登录页面


import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
//发送请求时把token携带过去
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['sg-token'] = getToken()
}
return config
},
error => {
console.log(error)
return Promise.reject(error)
}
)

service.interceptors.response.use(
response => {
console.log(response.data)
const res = response.data

// token过期,重返登录界面
if (res.code === 10009) {
store.dispatch('user/logout').then(() => {
location.reload(true)
})
}
return res
},
error => {
console.log('err' + error) // for debug
Message({
message: error.msg,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)

export default service

好啦,关于token的分享就到这里了,以上代码根据你们项目的情况换成你们的数据,有错误欢迎指出来!


作者:阿狸要吃吃的
链接:https://juejin.cn/post/6947970204320137252

收起阅读 »

Vue3,我决定不再使用Vuex

vue
在开发基于Vue3的项目中发现我们可以不再依赖Vuex也能很方便的来管理数据,只需要通过Composition Api可以快捷的建立简单易懂的全局数据存储. 创建State 通过reactive我们来创建state,暴露的IState是用来方便其他文件来接受S...
继续阅读 »

在开发基于Vue3的项目中发现我们可以不再依赖Vuex也能很方便的来管理数据,只需要通过Composition Api可以快捷的建立简单易懂的全局数据存储.


创建State


通过reactive我们来创建state,暴露的IState是用来方便其他文件来接受State对象


import { reactive } from 'vue'

export interface IState {
code: string
token: string
user: any
}

export const State: IState = {
code: '',
token: '',
user: {}
}

export function createState() {
return reactive(State)
}


创建Action


我们来创建Action来作为我们修改State的方法


import { reactive } from 'vue'
import { IState } from './state'

function updateCode(state: IState) {
return (code: string) => {
state.code = code
}
}

function updateToken(state: IState) {
return (token: string) => {
state.token = token
}
}

function updateUser(state: IState) {
return (user: any) => {
state.user = user
}
}

/**
* 创建Action
* @param state
*/
export function createAction(state: IState) {
return {
updateToken: updateToken(state),
updateCode: updateCode(state),
updateUser: updateUser(state)
}
}

通过暴露的IState我们也可以实现对State的代码访问.


创建Store


创建好StateAction后我们将它们通过Store整合在一起.


import { reactive, readonly } from 'vue'
import { createAction } from './action'
import { createState } from './state'

const state = createState()
const action = createAction(state)

export const useStore = () => {
const store = {
state: readonly(state),
action: readonly(action)
}

return store
}

这样我们就可以在项目中通过调用useStore访问和修改State,因为通过useStore返回的State是通过readonly生成的,所以就确认只有Action可以对其进行修改.


// 访问state
const store = useStore()
store.state.code

// 调用action
const store = useStore()
store.action.updateCode(123)

这样我们就离开了Vuex并创建出了可是实时更新的数据中心.


持久化存储


很多Store中的数据还是需要实现持久化存储,来保证页面刷新后数据依然可用,我们主要基于watch来实现持久化存储


import { watch, toRaw } from 'vue'

export function createPersistStorage<T>(state: any, key = 'default'): T {
const STORAGE_KEY = '--APP-STORAGE--'

// init value
Object.entries(getItem(key)).forEach(([key, value]) => {
state[key] = value
})

function setItem(state: any) {
const stateRow = getItem()
stateRow[key] = state
const stateStr = JSON.stringify(stateRow)
localStorage.setItem(STORAGE_KEY, stateStr)
}

function getItem(key?: string) {
const stateStr = localStorage.getItem(STORAGE_KEY) || '{}'
const stateRow = JSON.parse(stateStr) || {}
return key ? stateRow[key] || {} : stateRow
}

watch(state, () => {
const stateRow = toRaw(state)
setItem(stateRow)
})

return readonly(state)
}

通过watchtoRaw我们就实现了statelocalstorage的交互.


只需要将readonly更换成createPersistStorage即可


export const useStore = () => {
const store = {
state: createPersistStorage<IState>(state),
action: readonly(action)
}

return store
}

这样也就实现了对Store数据的持久化支持.


作者:程序员紫菜苔
链接:https://juejin.cn/post/6898504898380464142

收起阅读 »

如何优雅的在java中统计代码块耗时

在我们的实际开发中,多多少少会遇到统计一段代码片段的耗时的情况,我们一般的写法如下 long start = System.currentTimeMillis(); try { // .... 具体的代码段 } finally { System...
继续阅读 »

在我们的实际开发中,多多少少会遇到统计一段代码片段的耗时的情况,我们一般的写法如下


long start = System.currentTimeMillis();
try {
// .... 具体的代码段
} finally {
System.out.println("cost: " + (System.currentTimeMillis() - start));
}

上面的写法没有什么毛病,但是看起来就不太美观了,那么有没有什么更优雅的写法呢?



1. 代理方式


了解 Spring AOP 的同学可能立马会想到一个解决方法,如果想要统计某个方法耗时,使用切面可以无侵入的实现,如


// 定义切点,拦截所有满足条件的方法
@Pointcut("execution(public * com.git.hui.boot.aop.demo.*.*(*))")
public void point() {
}

@Around("point()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try{
return joinPoint.proceed();
} finally {
System.out.println("cost: " + (System.currentTimeMillis() - start));
}
}

Spring AOP 的底层支持原理为代理模式,为目标对象提供增强功能;在 Spring 的生态体系下,使用 aop 的方式来统计方法耗时,可以说少侵入且实现简单,但是有以下几个问题



2. AutoCloseable


在 JDK1.7 引入了一个新的接口AutoCloseable, 通常它的实现类配合try{}使用,可在 IO 流的使用上,经常可以看到下面这种写法


// 读取文件内容并输出
try (Reader stream = new BufferedReader(new InputStreamReader(new FileInputStream("/tmp")))) {
List<String> list = ((BufferedReader) stream).lines().collect(Collectors.toList());
System.out.println(list);
} catch (IOException e) {
e.printStackTrace();
}

注意上面的写法中,最值得关注一点是,不需要再主动的写stream.close了,主要原因就是在try(){}执行完毕之后,会调用方法AutoCloseable#close方法;


基于此,我们就会有一个大单的想法,下一个Cost类实现AutoCloseable接口,创建时记录一个时间,close 方法中记录一个时间,并输出时间差值;将需要统计耗时的逻辑放入try(){}代码块


下面是一个具体的实现:


public static class Cost implements AutoCloseable {
private long start;

public Cost() {
this.start = System.currentTimeMillis();
}

@Override
public void close() {
System.out.println("cost: " + (System.currentTimeMillis() - start));
}
}

public static void testPrint() {
for (int i = 0; i < 5; i++) {
System.out.println("now " + i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
try (Cost c = new Cost()) {
testPrint();
}
System.out.println("------over-------");
}

执行后输出如下:


now 0
now 1
now 2
now 3
now 4
cost: 55
------over-------

如果代码块抛异常,也会正常输出耗时么?


public static void testPrint() {
for (int i = 0; i < 5; i++) {
System.out.println("now " + i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i == 3) {
throw new RuntimeException("some exception!");
}
}
}

再次输出如下,并没有问题


now 0
now 1
now 2
now 3
cost: 46
Exception in thread "main" java.lang.RuntimeException: some exception!
at com.git.hui.boot.order.Application.testPrint(Application.java:43)
at com.git.hui.boot.order.Application.main(Application.java:50)

3. 小结


除了上面介绍的两种方式,还有一种在业务开发中不太常见,但是在中间件、偏基础服务的功能组件中可以看到,利用 Java Agent 探针技术来实现,比如阿里的 arthas 就是在 JavaAgent 的基础上做了各种上天的功能,后续介绍 java 探针技术时会专门介绍


下面小结一下三种统计耗时的方式


基本写法


long start = System.currentTimeMillis();
try {
// .... 具体的代码段
} finally {
System.out.println("cost: " + (System.currentTimeMillis() - start));
}

优点是简单,适用范围广泛;缺点是侵入性强,大量的重复代码


Spring AOP


在 Spring 生态下,可以借助 AOP 来拦截目标方法,统计耗时


@Around("...")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try{
return joinPoint.proceed();
} finally {
System.out.println("cost: " + (System.currentTimeMillis() - start));
}
}

优点:无侵入,适合统一管理(比如测试环境输出统计耗时,生产环境不输出);缺点是适用范围小,且粒度为方法级别,并受限于 AOP 的使用范围


AutoCloseable


这种方式可以看做是第一种写法的进阶版


// 定义类
public static class Cost implements AutoCloseable {
private long start;

public Cost() {
this.start = System.currentTimeMillis();
}

@Override
public void close() {
System.out.println("cost: " + (System.currentTimeMillis() - start));
}
}

// 使用姿势
try (Cost c = new Cost()) {
...
}

优点是:简单,适用范围广泛,且适合统一管理;缺点是依然有代码侵入


说明


上面第二种方法看着属于最优雅的方式,但是限制性强;如果有更灵活的需求,建议考虑第三种写法,在代码的简洁性和统一管理上都要优雅很多,相比较第一种可以减少大量冗余代码


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

Flutter -项目架构篇

介绍 多姿的青春,迷茫的青春,懵懂的青春,落泪的青春,责任的青春,青春的婀娜,青春的美妙全部撒播在了沿途的风景之中,迷茫,酸楚,欢声笑语在记忆的天空中承载着梦想而飞翔,青春才成了心中的永恒 本文带你一步一步搭建flutter项目架构,方便你项目直接集成使用...
继续阅读 »

介绍



多姿的青春,迷茫的青春,懵懂的青春,落泪的青春,责任的青春,青春的婀娜,青春的美妙全部撒播在了沿途的风景之中,迷茫,酸楚,欢声笑语在记忆的天空中承载着梦想而飞翔,青春才成了心中的永恒



本文带你一步一步搭建flutter项目架构,方便你项目直接集成使用。项目主要用到以下技术栈,小编秉着分享的宗旨,为你讲解


1.全局捕获异常


2.路由(Route)


3.Dio(网络)


4.OverlayEntry


5.网络dio抓包工具配置(ALice)


6.状态管理(Provider)


7.通知(这个是小编自己写的, 很方便,类似EventBus)


全局捕获异常


在Flutter中 ,有些异常是可以捕获到的,有些则是捕获不到的。那么,我们要做到错误日志上报给服务器,方便线上跟踪问题,怎么办呢?有个东西了解一下,捕获不到的用runZoned。代码如下,代码中有详细的注释,这里就不一一解释了。


void main() {
/// 捕获flutter能try catch 捕获的异常
/// 还有一些异常是try catch 捕获不到的 用runZoned
FlutterError.onError = (FlutterErrorDetails errorDetails) {
if (Application.debug) {
/// 测试环境 日志直接打印子啊控制台
FlutterError.dumpErrorToConsole(errorDetails);
} else {
/// 在生产环境上 重定向到runZone 处理
Zone.current
.handleUncaughtError(errorDetails.exception, errorDetails.stack);
}
reportErrorAndLog(errorDetails);
};
WidgetsFlutterBinding.ensureInitialized();
GlobalKey<NavigatorState> globalKey = new GlobalKey<NavigatorState>();
Application.globalKey = globalKey;

/// dio 网络抓包工具配置
Alice alice = Alice(
showInspectorOnShake: true,
showNotification: true,
navigatorKey: globalKey);
Application.alice = alice;

/// 初始化网络配置
HttpManager.initNet();

/// 捕获try catch 捕获不到的异常
runZoned(
() => runApp(MultiProvider(
providers: [
///注册通知
/// 这个是相当于通知 的作用 用于 这个类中的属性改变 然后通知到用到的页面 进行刷新
ChangeNotifierProvider(create: (_) => CounterProvider()),
],
child: MyApp(),
)), zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
/// 这里捕获所有print 日志
},
), onError: (Object obj, StackTrace stack) {
var detail = makeDetails(obj, stack);
reportErrorAndLog(detail);
});
}

void reportErrorAndLog(FlutterErrorDetails errorDetails) {
/// 错误日志上报 服务器
}

/// 构建错误信息
FlutterErrorDetails makeDetails(Object obj, StackTrace stack) {
FlutterErrorDetails details =
FlutterErrorDetails(stack: stack, exception: obj);
return details;
}

路由相关


路由跳转配置


跳转有2种方式。一种是直接用Widget, 另一种是用routeName。 这里小编为你讲解routeName跳转


先附上路由跳转封装类



import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

/// Created by zhengxiangke
/// des:
class NavigatorUtil {
///直接跳转
static void push(BuildContext context, Widget widget) {
Navigator.push(context, MaterialPageRoute(builder: (context) => widget));
}

///根据路径跳转 可传参数
static void pushName(BuildContext context, String name, {Object arguments}) {
Navigator.pushNamed(context, name, arguments: arguments);
}

///销毁页面
static void pop(BuildContext context) {
Navigator.of(context).pop(context);
}

/// 推到 指定路由页面 这指定路由页面上的页面全部销毁
/// 注意: 若果没有指定路由 会报错
static void popUntil(BuildContext context, String routeName) {
Navigator.popUntil(context, ModalRoute.withName(routeName));
}


/// 把当前页面在栈中的位置替换为跳转的页面, 当新的页面进入后,之前的页面将执行dispose方法
static void pushReplacementNamed(BuildContext context, String routeName,
{Object arguments}) {

Navigator.of(context).pushReplacementNamed(routeName, arguments: arguments);
}
}


我们可以看到代码中的routeName, routeName这个是我们自己可以配置的 ,简单而言,就是根据路径去跳到指定的页面。路由配置如下


class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
localeResolutionCallback:
(Locale locale, Iterable<Locale> supportedLocales) {
//print("change language");
return locale;
},
navigatorKey: Application.globalKey,

/// 这个routes 不能写 如果写了的话 就不能传递参数
// routes: routes,
/// 这个既可以传递参数 也可以不传递参数 用这一个就够了 无须用这个routes
onGenerateRoute: onGenerateRoute,
navigatorObservers: [
/// 路由监听 作用:对用户行为流的埋点监测
GLObserver()
],
home: MyHomePage(),
);
}
}

final routes = {
'/second' : (context) => SecondPage(),
'/NestedScrollViewDemo' : (context, {arguments}) => NestedScrollViewDemo(value: arguments['value'] as String),
'/ProviderDemo': (context) => ProviderDemo()
};

// ignore: missing_return, top_level_function_literal_block
final onGenerateRoute = (settings) {
Function pageContentBuilder = routes[settings.name];
Route route;
if (pageContentBuilder != null) {
if (settings.arguments != null) {
/// 传递参数
route = MaterialPageRoute(
settings: settings,
builder: (context) => pageContentBuilder(context, arguments: settings.arguments));
return route;
} else {
/// 不传递参数 只管跳
route = MaterialPageRoute(
settings: settings,
builder: (context) => pageContentBuilder(context));
return route;
}

}
};

观察者


页面跳转添加观察者,能获取用户行为数据(GLObserver)


  @override
Widget build(BuildContext context) {
return MaterialApp(
localeResolutionCallback:
(Locale locale, Iterable<Locale> supportedLocales) {
//print("change language");
return locale;
},
navigatorKey: Application.globalKey,

/// 这个routes 不能写 如果写了的话 就不能传递参数
// routes: routes,
/// 这个既可以传递参数 也可以不传递参数 用这一个就够了 无须用这个routes
onGenerateRoute: onGenerateRoute,
navigatorObservers: [
/// 路由监听 作用:对用户行为流的埋点监测
GLObserver()
],
home: MyHomePage(),
);
}

Dio相关


dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等...


dependencies:
dio: ^3.0.9 // 请使用pub上3.0.0分支的最新版本

基本配置


小编带你写个Dio单例, 在这个单例中配置Dio基本配置


/// 这个是域名  请书写自己项目中的域名
const String BASEURL = '';
class HttpConfig {
static const baseUrl = BASEURL;
static const timeout = 5000;

static const codeSuccess = 10000;

}

class HttpManager {
factory HttpManager() => getInstance();
static HttpManager get install => getInstance();
static HttpManager _install;
static Dio dio;
HttpManager._internal() {
// 初始化
}
static HttpManager getInstance() {
if (_install == null) {
_install = HttpManager._internal();
}
return _install;
}
/// 初始化网络配置
static void initNet() {
dio = Dio(BaseOptions(
baseUrl: HttpConfig.baseUrl,
contentType: 'application/x-www-form-urlencoded',
connectTimeout: HttpConfig.timeout,
receiveTimeout: HttpConfig.timeout
));
}

}

设置代理


有这么个需求背景, 有一天,测试来问,怎么抓网络信息。Dio 为我们提供了代理, 测试可以根据chanles等抓包工具进行查看网络信息


    if (Application.proxy) {
/// 用于代理 抓包
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.findProxy = (uri) {
//// PROXY 是固定 后面的localhost:8888 指的是别人的机器ip
return 'PROXY localhost:8888';
};
};
}

拦截器


我们可以在拦截器中添加一些公共的参数,如用户信息,手机信息,App版本信息等等, 也可以打印请求的url, 请求头,请求体信息。也可以进行参数签名。这里签名就不一一说了,


  /// 添加拦截器
dio.interceptors.add(CustomInterceptors());

///  des:  这里的api 规范是 200成功
class CustomInterceptors extends InterceptorsWrapper {
@override
Future onRequest(RequestOptions options) {
/// 在拦截里设置公共请求头
options.headers = {HttpHeaders.authorizationHeader: '这是token'};
if (Application.debug) {
try {
print("请求url:${options.path}");
print('请求头: ' + options.headers.toString());
/// 可以在这里个性定制 如签名 key value 从小到大排序 options.data 再次赋值即可
print('请求体: ' + options.data);
} catch (e) {
print(e);
}
}
return super.onRequest(options);
}
@override
Future onResponse(Response response) async{
LoadingUtil.closeLoading();
if (Application.debug) {
print('code=${response.statusCode.toString()} ==data=${response.data
.toString()}');
}
return super.onResponse(response);
}
@override
Future onError(DioError err) {
// TODO: implement onError
LoadingUtil.closeLoading();
if (err.type == DioErrorType.CONNECT_TIMEOUT
|| err.type == DioErrorType.RECEIVE_TIMEOUT
|| err.type == DioErrorType.SEND_TIMEOUT) {
Fluttertoast.showToast(msg: '请求超时');
} else {
Fluttertoast.showToast(msg: '服务异常');
}
return super.onError(err);
}
}

ALice


这是一个网络请求查看库,有了这个就不需要指定代理了,很方便。下面为dio 进行Alice 拦截,以便查看Dio 发出的请求


dependencies:
alice: 0.1.4
dio.interceptors.add(Application.alice.getDioInterceptor());

注意:ALice 一定要配置navigatorKey


  GlobalKey<NavigatorState> globalKey = new GlobalKey<NavigatorState>();
Application.globalKey = globalKey;
/// dio 网络抓包工具配置
Alice alice = Alice(
showInspectorOnShake: true,
showNotification: true,
navigatorKey: globalKey);
Application.alice = alice;

  class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp( navigatorKey:Application.globalKey,
],
home: MyHomePage(),
);
}
}

Provider


Flutter 状态管理,实际来说就是数据和视图的绑定和刷新; 这块对应到 H5,就比较好理解,这个概念也是从前端来到; 对应到 客户端,就是监听回调,类似事件总线(EventBus)


简而言之,就是监听的类中的变量属性发生变化就会刷新用到这个变量的Widget 页面


dependencies:
provider: ^4.3.2+2

说说这个库的中心思想


1.注册


2.定义类


3.赋值


4.取值


注册


runApp(MultiProvider(
providers: [
///注册通知
/// 这个是相当于通知 的作用 用于 这个类中的属性改变 然后通知到用到的页面 进行刷新
ChangeNotifierProvider(create: (_) => CounterProvider()),
],
child: MyApp(),
))

定义类


class CounterProvider with ChangeNotifier {
int count = 0;
void addCount() {
count ++;
notifyListeners();
}
}

赋值


 Provider.of<CounterProvider>(context, listen: false).addCount()

取值


context.watch<CounterProvider>().count

通知


小编之前在做Android开发,就用到了这个通知。后来做了Flutter开发一年来头了, 借鉴其思想,创下了这个通知


先说说通知原理:


我们知道EventBus有一个action事件和一个可以传递的数据对象。在页面初始化生命周期中注册通知,在页面销毁生命周期中销毁该通知。在需要发送通知 刷新数据地方, 调用发送通知 ,一个Action 对应发送到哪个通知,通知数据是一个泛型的Object, 可以发送字符串,对象,数组等任何数据


通知管理类


在这个类中提供几个方法


1.注册通知


2.销毁通知


3.发送通知


///这是一个例子
///这个在initState() 注册 <> 里的类型 要跟 发送的数据类型对应 可以是String int bool 还有model list
/// IUpdateViewManager.instance.registerIUPdateView(UpdateView(NoticeAction.action1,
/// IUpdateView<List<String>>(
/// callback: (List<String> msg) {
/// print(msg);
/// }
/// )));、
///发送通知 注意 这里可以发送任何类型的数据 因为范型
///IUpdateViewManager.instance.notifyIUpdateView(NoticeAction.action1, ['xx', 'vvv']);
///在dispose中 取消注册 注意: 有注册就有取消 这是对应的 否则会出现功能不正常请客
///IUpdateViewManager.instance.unRegistIUpdateView(NoticeAction.action1);
class IUpdateViewManager{
List<UpdateView> updateViews = [];

// 工厂模式
factory IUpdateViewManager() =>_getInstance();
static IUpdateViewManager get instance => _getInstance();
static IUpdateViewManager _instance;
IUpdateViewManager._internal() {
// 初始化
}
static IUpdateViewManager _getInstance() {
_instance ??= IUpdateViewManager._internal();
return _instance;
}
///注册通知 在initstatus 注册
void registerIUPdateView(UpdateView updateView) {
///在数组中不能存在多个相同的action
updateViews.insert(0, updateView);
}
///发送通知 在业务场景需要的地方 调用这个方法
void notifyIUpdateView <T>(String action, T t) {
if (updateViews != null && updateViews.isNotEmpty) {
for (var item in updateViews) {
if (item.action == action) {
item.iUpdateView.updateView(t);
break;
}
}
}
}
///通知解绑 在dispose方法中解绑 注意 有注册 就有解绑 这是一定必须的
void unRegistIUpdateView(String action) {
if (updateViews != null && updateViews.isNotEmpty) {
updateViews.remove(UpdateView(action, null));
}
}
}
///这个类是时间action 用到这个类的通知的action 在这里定义常量
class NoticeAction {
static const String action1 = 'action1';
static const String action2 = 'action2';
}

其次,通知类如下


class UpdateView {
String action;
IUpdateView iUpdateView;


UpdateView(this.action, this.iUpdateView);

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UpdateView &&
runtimeType == other.runtimeType &&
action == other.action;

@override
int get hashCode => action.hashCode;
}

class IUpdateView <T>{
Function(T msg) callback;
void updateView (T t) {
if (callback != null) {
callback(t);
}
}

IUpdateView({@required this.callback});
}

注册通知


这个在initState() 注册 <> 里的类型 要跟 发送的数据类型对应 可以是String int bool 还有model list


   IUpdateViewManager.instance.registerIUPdateView(UpdateView(NoticeAction.action1,
IUpdateView<List<String>>(
callback: (List<String> msg) {
print(msg);
}
)));

发送通知


IUpdateViewManager.instance.notifyIUpdateView(NoticeAction.action1, ['xx', 'vvv']);

销毁通知


在dispose中 取消注册 注意: 有注册就有取消 这是对应的 否则会出现功能不正常


IUpdateViewManager.instance.unRegistIUpdateView(NoticeAction.action1);

架构篇


小编先说说搭建项目的总体思想,




  1. 我们知道每一个页面刚进去的时候都会有一个loading,因此小编用一个widget的基类,所有的页面都会继承这个基类。在这个基类中提供了Appbar的方法,加载试图显示和隐藏,加载失败重试,网络请求的方法,另外还有个buildBody方法,所有继承该基类的widget都必须重写这个方法,详见BasePage




  2. 网络: 本文采用Dio,并添加了拦截器,可在拦截器中打印请求信息,有个HttpManager管理单例的dio实例,并添加了Alice网络查看器,方便测试人员查看请求信息。HttpRequest里有个请求方法, 可定义请求方式,传递方式,失败回调,成功回调。并在回调中返回ResultData(这是一个返回的数据结构封装类)




  3. 对于埋点上报,新增了一个GLObserver路由观察者,在这里可以进行简单的用户行为进行捕获




  4. 错误日志上报 详见main.dart




  5. 由于复杂的页面交互,那么通知也是少不了的,一个页面的某个行为会影响上个页面的展现内容或者刷新数据,那么 这里小编定义了2中方式:1.Provider 2.IupdateViewManager 大家可以任选其一即可




  6. 基于SmartRefresher刷新封装的CustomerSmartRefresh


    最后,代码已上传github , 欢迎下载阅读
    如有疑问 加QQ群 883130953


    github.com/zhengxiangk…


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

关于Android架构,你是否还在生搬硬套?

前言 关于Android架构,可能在很多人心里一直都是虚无缥缈的存在,似懂非懂、为了用而用、处处生搬硬套,这种情况使用的意义真的很有限。本人有多个项目重构的经验,恰好对设计领域较为感兴趣,今天我将毫无保留的将自己对架构、设计的理解分享给大家。 本文不会具体去讲...
继续阅读 »

前言


关于Android架构,可能在很多人心里一直都是虚无缥缈的存在,似懂非懂、为了用而用、处处生搬硬套,这种情况使用的意义真的很有限。本人有多个项目重构的经验,恰好对设计领域较为感兴趣,今天我将毫无保留的将自己对架构、设计的理解分享给大家。


本文不会具体去讲什么是MVC、MVP、MVVM,但我描述的点应该都是这些模式的基石,从本质上讲明白为什么这样做,这样做的好处是什么,有了这些底层思想的支持再去看对应的架构模式,相信会让你有一种焕然一新的感觉。


知识储备:需掌握Java面向对象、六大设计原则,如果不理解也无妨,我尽量将用到的设计原则加以详细描述


目录



  • 1. 模块化的意义何在?

    • 1.1 基本概念以及底层思想

    • 1.2 我们要基于哪些特性去做模块化划分?

    • 1.3 Android如何做分层处理?

    • 1.4 Data Mapper或许是解药

    • 1.5 无处安放的业务逻辑



  • 2. 合理分层是给 数据驱动UI 做铺垫

    • 2.1 什么是 控制反转?

    • 2.2 什么是数据驱动UI?

    • 2.3 为什么说数据驱动UI底层思想是控制反转?

    • 2.4 为什么引入Diff?



  • 3. 为什么我建议使用 函数式编程

    • 3.1 什么是 函数式编程?

    • 3.2 Android视图开发可以借鉴函数式编程思想




1. 模块化的意义何在?


1.1 基本概念以及底层思想



所有的模块化都是为了满足单一设计原则 (字面意思理解即可),一个函数或者一个类再或者一个模块,职责越单一复用性就越强,同时能够间接降低耦合性



在软件工程的背景下,改动就会有出错的可能,不要说"我注意一点就不会出错"这种话,因为人不是机器。我们能做的就是尽可能让模块更加单一,职责越单一影响到外层模块的可能性就越小,这样出错的概率也就越低。


所以模块化核心思想即:单一设计原则


1.2 我们要基于哪些特性去做模块化划分?


做模块化处理的时候尽量基于两种特性进行功能特性业务特性


功能特性



网络、图片加载等等都可称之为功能特性。比如网络:我们可以将网络框架的集成、封装等等写到同一个模块(module、package等)当中,这样可以增强可读性(同一目录一目了然)、降低误操作概率,方便于维护也更加安全。同时也可将模块托管至远程如maven库,可供多个项目使用,进一步提升复用性



业务特性



业务特性字面意思理解即可,就是我们常常编写的业务,需要以业务的特性进行模块划分



为什么说业务特性优先级要高于功能特性


举个例子如下图:


image.png


相信很多人见过或者正在使用这种分包方式,在业务层把所有的AdapterPresenterActivity等等都放在对应的包中,这种方式合理吗?先说答案不合理,首先这已经是在业务层,我们做的所有事情其实都在为业务层服务,所以业务的优先级应该是最高的,我们应当优先根据业务特性将对应的类放入到同一个包中。


功能模块核心是功能,应当以功能进行模块划分。业务模块核心是业务,应当优先以业务进行模块划分,其次再以功能进行模块划分。


1.3 Android如何做分层处理?


前端开发其实就是做数据搬运,再展示到视图中。数据视图是两个不同的概念,为了提高复用性以及可维护性,我们应当根据单一设计原则我们应当将二者进行分层处理,所以无论是MVCMVP还是MVVM最核心的点都是将数据视图进行分层。


绊脚石:



通常来讲,我们通过网络请求拿到数据结构都是后端定义的,这也就意味着视图层不得不直接使用后端定义的字段,一旦后端进行业务调整会迫使我们前端从数据层-->视图层都会进行对应的改动,如下伪代码所示:



//原始逻辑
数据层
Model{
title
}
UI层
View{
textView = model.title
}

//后端调整后
数据层
Model{
title
prefix
}
UI层
View{
textView = model.prefix + model.title
}

起初我们的textView显示的是model中的title,但后端调整后我们需要在model中加一个prefix字段,同时textView显示内容也要做一次字符串拼接。视图层因为数据层的改动而被动做了修改。既然做了分层我们想要的肯定是视图、数据互不干扰,如何解决?往下看...


1.4 Data Mapper或许是解药


Data Mapper是后端常用的一个概念,一般情况下他们是不会直接使用数据库里面的字段,而是加一个Data Mapper(数据映射)将数据库表转按需换成Java Bean,这样做的好处也很明显,表结构甭管怎么折腾都不会影响到业务层代码。


对于前端我觉得可以适当引入Data Mapper,将后端数据转换成本地模型,本地模型只与设计图对应,将后端业务视图完全隔离。这也就解决了 1.3 面临的问题,具体方式如下:


数据层
Model{
title
prefix
}
本地模型(与设计图一一对应)
LocalModel{
//将后端模型转换为本地模型
title = model.prefix + model.title
}
UI层
View{
textView = localModel.title
}

LocalModel相当于一个中间层,通过适配器模式将数据层与视图层做隔离。


前端引入Data Mapper后可以脱离后端进行开发,只要需求明确就可以做视图层的开发,完全不需要担心后端返回什么结构字段。并且这种做法是一劳永逸的,比如后端需要对某些字段做调整,我们可以不暇思索直奔数据层,涉及到的调整100%不会影响到视图层


注意点:



当下有一部分公司为了将前后端分离更彻底,由前端开发人员提供Java Bean(相当于LocalModel)的结构,好处也很明显,更多的业务内聚到后端,很大程度提升了业务的灵活性,毕竟App发一次版成本还是比较大的。面对这种情况我们其实没必要再编写Data Mapper。所以任何架构设计都要结合实际情况,适合自己的才是最好的。



1.5 无处安放的业务逻辑


关于业务逻辑其实是一个很笼统的概念,甚至可以将任意一行代码称之为业务逻辑,如此宽泛的概念我们该如何去理解?我先大致将它分为两个方面:




  • 界面交互逻辑:视图层的交互逻辑,比如手势控制、吸顶悬浮等等都是根据业务需要实现的,所以严格来说这部分也属于业务逻辑。但这部分业务逻辑一般在视图层实现。

  • 数据逻辑:这部分是大家常说的业务逻辑,属于强业务逻辑,比如根据不同用户类型获取不同数据、展示不同界面,加上Data Mapper一系列操作其实就是给后端兜底,帮他们补全剩余逻辑而已。为了方便大家理解下文我将数据逻辑统称为业务逻辑



前面我们说到,Android开发应该具备数据层视图层,那业务逻辑放在哪一层比较合适呢?比如MVVM模式下大家都说将业务逻辑放到ViewModel处理,这么说也没有太大的问题,但如果一个界面足够复杂那对应的ViewModel代码可能会有成百上千行,看起来会很臃肿可读性也非常差。最重要的一点这些业务很难编写单元测试用例


关于业务逻辑我建议单独写一个use case处理。


use case通常放在ViewModel/Presenter数据层之间,业务逻辑以及Data Mapper都应该放在use case中,每一个行为对应一个use case。这样就解决了ViewModel/Presenter臃肿的问题,同时更方便编写测试用例。


注意点:



好的设计都是特定场景解决特定问题,过度设计不仅解决不了任何问题反而会增加开发成本。以我目前经验来看Android开发至少一半的场景都很简单:请求-->拿数据-->渲染视图最多再加个Data Mapper,流程很单一并且后期改动的可能也不太大,这种情况就没必要写一个use case,Data Mapper扔到数据层即可。



2. 合理分层是给 数据驱动UI 做铺垫


先说结论:数据驱动UI的本质是控制反转


2.1 什么是 控制反转?


控制即对程序流程的控制,一般由我们开发者承担,此过程为控制。但开发者是人所以不可避免出现错误,此时可以将角色做一个反转由成熟的框架负责整个流程,程序员只需要在框架预留的扩展点上,添加跟自己的业务代码,就可以利用框架来驱动整个程序流程的执行,此过程为反转


控制反转概念和设计原则中的依赖倒置很相似,只是少了一个依赖抽象


打个比方:



现有一个HTTP请求的需求,如果想自己维护HTTT链接、自己管理TCP Socket、自己处理HTTP缓存.....就是整个HTTP协议全部自己封装,先不说这个工程能不能靠个人实现,就算实现也是漏洞百出,此时可以换个思路:通过OkHttp去实现,OkHttp是一个成熟的框架用它基本上不会出错。个人封装HTTP协议到使用OkHttp框架,这个过程在控制HTTP的角色上发生了一个反转个人--->成熟的框架OkHttp即控制反转,好处也很明显,框架出错的概率远低于个人。



2.2 什么是数据驱动UI?


通俗一点说就是当数据改变时对应的UI也要跟着变,反过来说当需要改变UI只需要改变对应的数据即可。现在比较流行的UI框架如FlutterComposeVue其本质都是基于函数式编程实现数据驱动UI,它们共同的目的都是为了解决数据,UI一致性问题。


在当前的Android中可以使用DataBinding实现同样的效果,以Jetpack MVVM为例:ViewModelRepository拿到数据暂存到ViewModel对应的ObservableFiled即可实现数据驱动UI,但前提是从Repository拿到的数据可以直接用,如果在Activity或者Adapter做数据二次处理再notify UI,已经违背数据驱动UI核心思想。所以想实现数据驱动UI必须要有合理的分层(UI层拿到的数据无需处理,可以直接用)Data Mapper恰好解决这一问题,同时也可规避大量编写BindAdapter的现状。



DataBinding并非函数式编程,它只是通过AbstractProcessor生成中间代码,将数据映射到XML中



2.3 为什么说数据驱动UI底层思想是控制反转?


当前Android生态能实现数据绑定UI的框架只有两个:DataBinding、Compose(暂不讨论)


在引入DataBinding之前渲染一条数据通常需要两步,如下:


var title = "iOS"
fun setTitle(){
//第一步更改数据源
title = "Android"
//第二个更改UI
textView = title
}

共需要两步更改数据源、更改UI,数据源UI有一个忘记修改便会出现BUG,千万不要说:“两个我都不会忘记修改”,当面临复杂的逻辑以及十几个甚至几十个的数据源很难保证不出错。这种问题可以通过DataBinding解决,只需更改对应的ObservableFiledUI便会同步修改,控制UI状态也从个人反转到的DataBinding,个人疏忽的事情DataBinding可不会。


所以说数据驱动UI底层思想是控制反转


2.4 为什么引入Diff?


引入diff之前:



RecyclerView想要实现动态删除、添加、更新需要分别手动更新数据和UI,这样在中间插了一道并且分别更新数据和UI已经违背了前面所说的数据驱动UI,而我们想要的是不管删除、添加或者更新只有一个入口,只要改变数据源就会驱动UI做更新,想要满足这一原则只能改变数据源后对RecyclerView做全部刷新,但这样会造成性能问题,复杂的界面会感到明显的卡顿。



引入diff之后:



Diff算法通过对oldItemnewItem做差异化比对,会自动更新改变的item,同时支持删除、添加的动画效果,这一特性解决了RecyclerView需要实现数据驱动UI的性能问题



3 为什么我建议使用 函数式编程


3.1 什么是 函数式编程?



  • 一个入口,一个出口。

  • 不在函数链内部执行与运算本身无关的操作

  • 不在函数链内部使用外部变量(实际上这一条很难遵守,可以适当突破)


说的通俗点就是给定一个初始值,经过函数链的运行会得到一个目标值,运算的过程中外部没有插手的权限,同时不做与本身无关的操作,从根本上解决了不可预期错误的产生。


举个例子:


//Kotlin代码

listOf(10, 20).map {
it + 1
}.forEach {
Log.i("list", "$it")
}

上面这种链式编程就是标准的函数式编程,输入到输出之间开发者根本没有插手的机会(即Log.i(..)之前开发者没有权限处理list),所以整个流程是100%安全的,RxJavaFlow链式高阶函数都是标准的函数式编程,它们从规范层面解决数据安全问题。所以我建议在Kotlin中 碰到数据处理尽量使用链式高阶函数(RxJava、Kotlin Flow亦然)


其实函数式编程的核心思想就是 门面模式 以及 迪米特法则


3.2 Android视图开发可以借鉴函数式编程思想


Android视图开发大都遵循如下流程:请求-->处理数据-->渲染UI,这一流程可以借鉴函数式编程,将请求作为入口,渲染做为出口,在这个流程中尽量不做与当前行为无关的事(这也要求ViewModel,Repository中的函数要符合单一原则)。这样说有点笼统,下面举个反例:


    View{
//刷新
fun refresh(){
ViewModel.load(true)
}
//加载更多
fun loadMore(){
ViewModel.load(false)
}
}

ViewModel{
//加载数据
load(isRefresh){
if (isRefresh){
//刷新
}else{
//加载更多
}
}
}

View层有刷新、加载更多两种行为,load(isRefresh)一个入口,两个出口。面临的问题很明显,修改刷新加载更多都会对对方产生影响,违反开闭原则中的闭(对修改关闭:行为没变不准修改源代码),导致存在不可预期的问题产生。可以借鉴函数式编程思想对其进行改进,将ViewModelload函数拆分成refreshloadMore,这样刷新加载更多两种行为、两个入口、两个出口互不干涉,通过函数的衔接形成两条独立的业务链条。


函数式编程可以约束我们写出规范的代码,面对不能使用函数式编程的场景,我们可以尝试自我约束往函数式编程方向靠拢,大致也能实现相同的效果。


综上所述



  • 合理的分层可以提升复用性、降低模块间耦合性

  • Data Mapper 可以让视图层脱离于后端进行开发

  • 复杂的业务逻辑应该写到use case中

  • 数据驱动UI的本质是控制反转

  • 通过函数式编程可以写出更加安全的代码

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

iOS-应用程序的加载

iOS
资料准备: 1、dyld源码下载opensource.apple.com/ 2、libdispatch源码下载opensource.apple.com/ 3、libSystem源码下载opensource.apple.com/ 前情提要: 在探索分析app启动...
继续阅读 »

资料准备:


1、dyld源码下载opensource.apple.com/

2、libdispatch源码下载opensource.apple.com/

3、libSystem源码下载opensource.apple.com/


前情提要:


在探索分析app启动之前,我们需要先了解iOS中App代码的编译过程以及动态库和静态库。


编译过程


1预编译:处理代码中的#开头的预编译指令,比如删除#define并展开宏定义,将#include包含的文件插入到该指令位置等(即替换宏,删除注释,展开头文件,产生.i文件)

2编译:对预编译处理过的文件进行词法分析、语法分析和语义分析,并进行源代码优化,然后生成汇编代码(即将.i文件转换为汇编语言,产生.s文件)

3汇编:通过汇编器将汇编代码转换为机器可以执行的指令,并生成目标文件.o文件

4链接:将目标文件链接成可执行文件.这一过程中,链接器将不同的目标文件链接起来,因为不同的目标文件之间可能有相互引用的变量或调用的函数,如我们经常调用Foundation框架和UIKit框架中的方法和变量,但是这些框架跟我们的代码并不在一个目标文件中,这就需要链接器将它们与我们自己的代码链接起来


流程如下



Foundation和UIKit这种可以共享代码、实现代码的复用统称为库——它是可执行代码的二进制文件,可以被操作系统写入内存,它又分为静态库和动态库



静态库


链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
.a.lib、非系统framework都是静态库


动态库


链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。如.dylib.framework都是动态库


dyld:


简介


dyld(The dynamic link editor)是苹果的动态链接器,负责程序的链接及加载工作,是苹果操作系统的重要组成部分,存在于MacOS系统的(/usr/lib/dyld)目录下.在应用被编译打包成可执行文件格式的Mach-O文件之后 ,交由dyld负责链接,加载程序。

整体流程如下


image.png


dyld_shared_cache


由于不止一个程序需要使用UIKit系统动态库,所以不可能在每个程序加载时都去加载所有的系统动态库.为了优化程序启动速度和利用动态库缓存,苹果从iOS3.1之后,将所有系统库(私有与公有)编译成一个大的缓存文件,这就是dyld_shared_cache,该缓存文件存在iOS系统下的/System/Library/Caches/com.apple.dyld/目录下


dyld加载流程:


load方法处加一个断点,点击函数调用栈/使用LLDB——bt指令打印,都能看到最初的起点_dyld_start


_dyld_start


可以看到_dyld_start是汇编写的,从注释中可以看出dyldbootstrap::start方法就是最开始的start方法。


image.png


dyldbootstrap::start


dyldbootstrap::start其实C++的语法,其中dyldbootstrap代表命名空间,start则是这个命名空间中的方法。


image.png


image.png
可以看到start这个方法的核心是dyld::main


dyld::main


_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue) {

代码省略......

/// 环境变量的配置
// Grab the cdHash of the main executable from the environment
/// 从环境变量中获取主要可执行文件的cdHash
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
unsigned bufferLenUsed;
if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
mainExecutableCDHash = mainExecutableCDHashBuffer;
}
/// 根据Mach-O头部获取当前运行的架构信息
getHostInfo(mainExecutableMH, mainExecutableSlide);

代码省略......

/// 检查共享缓存是否开启,在iOS中必须开启
// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
if ( sSharedCacheOverrideDir)
mapSharedCache(mainExecutableSlide);
#else
/// 检查共享缓存是否映射到了共享区域
mapSharedCache(mainExecutableSlide);
#endif

代码省略......

/// 加载可执行文件,并生成一个ImageLoder实例对象
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

代码省略......

/// 加载所有DYLD_INSERT_LIBRARIES指定的库
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}

代码省略......

/// link主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);

代码省略......

/// link动态库
// link any inserted libraries
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
if ( gLinkContext.allowInterposing ) {
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing(gLinkContext);
}
}
}

代码省略......

sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
uint64_t bindMainExecutableEndTime = mach_absolute_time();
ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
gLinkContext.notifyBatch(dyld_image_state_bound, false);

// Bind and notify for the inserted images now interposing has been registered
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true, nullptr);
}
}

代码省略......

/// 弱符号绑定
// <rdar://problem/12186933> do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);

代码省略......

/// 执行初始化方法
// run all initializers
initializeMainExecutable();

代码省略......

/// 寻找m目标可执行文件ru入口并执行
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();

}#### 1 环境变量配置


通过以上代码分析大体流程如下


1 环境变量配置



  • 平台,版本,路径,主机信息的确定

  • 从环境变量中获取主要可执行文件的cdHash

  • checkEnvironmentVariables(envp)检查设置环境变量

  • defaultUninitializedFallbackPaths(envp)DYLD_FALLBACK为空时设置默认值

  • getHostInfo(mainExecutableMH, mainExecutableSlide)获取程序架构


image.png
image.png
image.png


Xcode设置了这两个环境变量参数,在App启动时就会打印相关参数、环境变量信息
如下


image.png


image.png


2 共享缓存



  • checkSharedRegionDisable检查是否开启共享缓存(在iOS中必须开启)

  • mapSharedCache加载共享缓存库,其中调用loadDyldCache函数有这么几种情况:

    • 仅加载到当前进程mapCachePrivate(模拟器仅支持加载到当前进程)

    • 共享缓存是第一次被加载,就去做加载操作mapCacheSystemWide

    • 共享缓存不是第一次被加载,那么就不做任何处理




image.png


image.png


3 主程序的初始化


调用instantiateFromLoadedImage函数实例化了一个ImageLoader对象
image.png
通过instantiateMainExecutable方法创建ImageLoader实例对象
image.png
这里主要是为主可执行文件创建映像,返回一个ImageLoader类型的image对象,即主程序.其中sniffLoadCommands函数会获取Mach-O类型文件的Load Command的相关信息,并对其进行各种校验
image.png


4 插入动态库


遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载,通过该环境变量我们可以注入自定义的一些动态库代码从而完成安全攻防,loadInsertedDylib内部会从DYLD_ROOT_PATHLD_LIBRARY_PATHDYLD_FRAMEWORK_PATH等路径查找dylib并且检查代码签名,无效则直接抛出异常
image.png
image.png


5 link主程序
image.png


5 link动态库
image.png


6 弱符号绑定
image.png


7 执行初始化方法


image.png
initializeMainExecutable源码主要是循环遍历,都会执行runInitializers方法
image.png
runInitializers(cons其核心代码是processInitializers函数的调用
image.png
processInitializers函数对镜像列表调用recursiveInitialization函数进行递归实例化
image.png
recursiveInitialization函数其作用获取到镜像的初始化,核心方法有两个notifySingledoInitialization
image.png
notifySingle函数,其重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());这句
image.png
sNotifyObjCInit只有赋值操作
image.png
registerObjCNotifiers发现在_dyld_objc_notify_register进行了调用,这个函数只在运行时提供给objc使用
image.png
objc4源码中查找_dyld_objc_notify_register,发现在_objc_init源码中调用了该方法,并传入了参数,所以sNotifyObjCInit的赋值的就是objc中的load_images所以综上所述,notifySingle是一个回调函数
image.png
load_images中可以看到call_load_methods方法调用
image.png
call_load_methods方法其核心是通过do-while循环调用call_class_loads方法
image.png
call_class_loads这里调用的load方法就是类的load方法
image.png
至此也证实了load_images调用了所有的load函数。


doInitialization中调用了两个核心方法doImageInitdoModInitFunctions
image.png
doImageInit其核心主要是for循环加载方法的调用,这里需要注意的一点是libSystem的初始化必须先运行
image.png
doModInitFunctions中加载了所有Cxx文件,这里需要注意的一点是libSystem的初始化必须先运行
image.png
通过doImageInitdoModInitFunctions方法知道libSystem初始化必须先运行,这里也和堆栈信息相互验证一致性
image.png
libSystem库中的初始化函数libSystem_initializer中调用了libdispatch_init函数
image.png
libdispatch_init方法中调用了_os_object_init函数
image.png
_os_object_init方法中调用了_objc_init函数
image.png
结合上面的分析,从初始化_objc_init注册的_dyld_objc_notify_register的参数2,即load_images,到sNotifySingle --> sNotifyObjCInie=参数2sNotifyObjcInit()调用,形成了一个闭环


8 寻找主程序入口
image.png
dyld汇编源码实现
image.png


dyld加载流程


image.png




链接:https://juejin.cn/post/7030253046210773029

收起阅读 »

谈谈iOS项目的多环境配置

iOS
在项目中配置多环境,需要了解的三个芝士点: Project: 包含了项目所有的代码,资源文件,所有信息。 Target: 对指定代码和资源文件的具体构建方式。 Scheme: 对指定Target的环境配置。 配置多环境的三种方案 多Target 先复制一...
继续阅读 »

在项目中配置多环境,需要了解的三个芝士点:



  • Project: 包含了项目所有的代码,资源文件,所有信息。

  • Target: 对指定代码和资源文件的具体构建方式。

  • Scheme: 对指定Target的环境配置。


配置多环境的三种方案


多Target



  1. 先复制一份一样的Target


image.png



  1. 对其进行重新命名,此时对于项目会增加一个新的info.plist文件


image.png



  1. 设置其对应的info.plist文件


image.png
4. 对于这个新的Target修改其对应BundleID


image.png



  1. 设置宏定义来实现多环境配置



  • Objc:在Objc中通过在Preprocessor Macros中配置宏定义


image.png



  • Swift:在Swift中通过在Other Swfit Flags中增加配置


image.png


总结
通过多Target方案会有两个缺点,第一每生成一个Target都会产生一个Info.plist文件,会比较冗余,第二就是比较麻烦,因为每次都会要设置宏定义,故不建议采纳。


通过Scheme实现多环境配置



  1. 添加新的Configuration


image.png



  1. 增加新的Scheme


image.png



  1. schemeBuild Configuration一一对应


image.png



  1. 新增定义设置(这里以区分不同环境需要访问的域名来举例)


image.png


image.png



  1. Info.plist中新增访问接口


image.png



  1. 在项目中进行访问


image.png


image.png


image.png


image.png


可以看到实现了不同的scheme访问了不同的值,实现了多环境配置,不过这个方案依然不够方便,因为有些Build Settings里针对不同环境需要做不同设置,这样还是不够方便。


xcconfig


1.在项目中创建自己的xcconfig文件,这里分别创建debugreleaserc对应的文件


image.png


2.在ProjectConfigurations进行对应


image.png


3.在xcconfig文件中进行配置(同样以不同环境的域名为例子)


image.png


image.png


image.png


4.在plist文件中提供接口
image.png


5.运行程序发现报错


image.png


这里涉及使用pod,如果另外创建xcconfig文件会导致这个错误,如果不涉及pod则不会报错,来看下控制台的报错


image.png


6.引入pods工程下的xcconfig相关文件
仅举例debug.xcconfig文件,其余操作均如下


image.png


7.选中不同的scheme运行,即可实现多环境配置


image.png


image.png


image.png


注意
在自己创建的xcconfig进行设置一些Build Settings里的参数时,可能会覆盖掉pods里的设置,这时需要加上关键字$(inherited),这样就会继承pods文件中的设置。


链接:https://juejin.cn/post/7030327656738455565
收起阅读 »

iOS autorelease与自动释放池

iOS
autorelease、autorelease pool以及原理 autorelease与MRC、ARC autorelease:在MRC下,内存管理允许有三个操作,分别是release,retain,autorelease。release会使对象的引用计数...
继续阅读 »

autorelease、autorelease pool以及原理


autorelease与MRC、ARC



  • autorelease:在MRC下,内存管理允许有三个操作,分别是release,retain,autoreleaserelease会使对象的引用计数立刻-1,retain使对象的引用计数立刻+1,autorelease也会让对象的引用计数-1,但不是立刻-1.调用autorelease的对象会被加入到autorelease pool中,在合适的时间autorelease pool向对象调用release,也就是说,对象被延迟释放了。

  • 而在ARC下,Apple禁止了手动调用autorelease方法。使用@autoreleaseblock创建自动释放池后,runtime会自动向在block中的对象加上autorelease


autorelease pool



A thread's autorelease pool is a stack of pointers. Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary. A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released. The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary. Thread-local storage points to the hot page, where newly autoreleased objects are stored.



以上是objc-781源码中NSObject.mm对于自动释放池的定义。从定义里面可以得知,自动释放池实际上是一个存放了指针的栈,栈中的指针有两类,一类是等待释放的对象指针,一类是名为POOL_BOUNDARY的哨兵指针。释放池之间以链表的形式相连,一个Page通常是4096个字节的大小(虚拟内存中的一页)。而前面提到的POOL_BOUNDARY哨兵指针的作用就是标示每个池子的尾端。


当在MRC中调用autorelease方法或者在ARC中将对象编写在@autoreleaseblock中,对象将会被注册到自动释放池中,当合适的时机到来自动释放池将会向这些对象调用release方法,以释放对象。



The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.



以上是Developer Documentation中Apple对于autorelease pool的一个介绍。可以看到在主线程中,每一个事件循环Runloop的开始,Appkit框架都会为程序创建一个自动释放池,并且在每次Runloop结束时释放所有在池中的对象。
需要注意的是,在这里Apple提到了一点:如果程序中临时创建了大量的autorelease对象,那么更好的做法是开发者自行新增一个释放池来最小化内存峰值的发生。


原理


First of all,最简单的代码


我们先来写出最常见的@autorelease代码。
-w461
当我们在Xcode中建立一个macOS项目时,通常模版中的main函数就包含了这样一段类似的代码。其中的@autorelease pool就是在ARC环境下使用自动释放池的API。


OC to C++


在终端中使用clang -rewrite-objc main.m命令将main.m文件转换成C++代码文件。
转换出来的代码会很多,我们挑重点的看。


-w511
在C++代码的main函数中,@autoreleasepool{}已经被转换成了如上代码。我们可以看到熟悉的objc_msgSeng,这是OC的灵魂-消息发送。
同时,@autoreleasepool{}变成了__AtAutoreleasePool,看来自动释放池的真实结构就是这个。我们再找一下它的定义在哪里。


通过搜索关键字,我们找到了它的定义语句。
-w543


可以看到,__AtAutoreleasePool结构体中定义了一个构造函数和一个析构函数,并调用了objc_autoreleasePoolPush()objc_autoreleasePoolPop()两个函数。


objc源码


这一步,我们到objc4-781代码中找上述两个方法的实现。
-w397
可以看到,这两个方法实际上是调用了AutoreleasePoolPage类的push()pop()方法,也就是说,自动释放池在runtime中的实际结构其实是AutoreleasePoolPage,这就是它的最终面目了。


-w596


AutoreleasePoolPage类继承于AutoreleasePoolPageData


-w766


从这里我们可以看到,autoreleasepool 是与线程一一对应的。同时线程池之间以双向链表相连。


这里引用网上一位同学分享的内存分布图
AutoreleasePoolPage


接着我们来看一下几个关键方法的具体实现。


autorelease


首先是autorelease方法。调用该方法会将对象加入到自动释放池中。
autorelease


第一行和第二行代码分别对传入参数做了一些检验,从第二行代码可以见到,如果传入的对象是TaggedPointer类型的,比如由小于等于9个字符的字面量字符串创建的NSString,将会有其他的处理操作。


autoreleaseFast-w465


该方法是autorelease方法的关键方法。可以看到第一行通过hotPage()方法拿到一个最近使用过的Page,然后来到流程控制。



  • 如果获取到了该hotPage并且Page还没有满,那么将对象加入到该Page中;

  • 如果Page满了,则调用autoreleaseFullPage方法创建一个新的page,将对象加入到新创建的page后并将新建立的page与通过hotPage()获取到的page相连接。

  • 如果没有获取到hotpage,那么将会调用autoreleaseNoPage方法建立并初始化自动释放池。


AutoreleasePoolPage::push()


在前面我们提到将对象加入到自动释放池时首先调用objc_autoreleasePoolPush方法,而该方法只起到了调用AutoreleasePoolPage::push()方法的作用。
-w688


其中,if-else的if分支是当出错时Debug会执行的流程,正常将会执行else分支里的代码。而autoreleaseFast()方法的实现在上一小段中已给出,这里传入的POOL_BOUNDARY就是哨兵对象。当创建一个自动释放池时,会调用该push()函数。


_objc_autoreleasePoolPrint()


使用_objc_autoreleasePoolPrint();方法可以打印出目前自动释放池中的对象,当然在使用前要先extern void _objc_autoreleasePoolPrint(void);.
该方法会调用AutoreleasePoolPage::printAll();打印出自动释放池中的相关信息。


-w553


从打印出的信息来看,自动释放池确实是跟线程一一对应的,并且在创建时会将一个哨兵对象加入到池中,这与我们在上文的代码分析结果相互映证。


写在最后


关于autoreleaseautoreleasePool的原理和代码的分析大概就是这些,当然还有很多具体实现是本文没有提到的,有兴趣的读者也可以自行到objc4-781的源码里找到NSObject.mm文件更加详细地研究。
总得来说,自动释放池机制延迟了对象的生命周期,并且可以为开发者自动释放需要被释放的对象,减少了内存泄漏发生的可能。


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

iOS 简单模拟 https 证书信任逻辑

iOS
废话开篇:https 证书是什么?如何进行认证呢?带着这些疑问来简单的实现一下验证过程简单的了解一下 https 在数据传输前的一些操作,如图:这里总结一下上面的流程图关键的步骤:1、认证网络请求的安全性:服务器会在建立真正的数据传输之前返回一个公钥数字证书。...
继续阅读 »

废话开篇:https 证书是什么?如何进行认证呢?带着这些疑问来简单的实现一下验证过程

简单的了解一下 https 在数据传输前的一些操作,如图:

image.png

这里总结一下上面的流程图关键的步骤:

1、认证网络请求的安全性

服务器会在建立真正的数据传输之前返回一个公钥数字证书。这里客户端需要在 URLSession 进行认证挑战方法回调里进行判断然后确定是否要继续进行请求。代理方法如下:

- (void)URLSession:(NSURLSession *)session

              task:(NSURLSessionTask *)task

didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge

 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler

可以这样理解,URLSession 做 https 网络请求的时候其实会把请求鉴权的权限通过代理的方法给暴露出来,是否信任并继续建立连接可以按照特定规则去执行(如自签证书),只有 https 请求会走代理方法,http 则不进行回调,这也是为什么 iOS系统 为什么提倡使用 https 的原因。

2、认证通过,通过公私钥非对称加密方式对最后的对称加密密钥进行加、解密:

这话听起来有点绕,基于第一步的公钥数字证书信任,那么,生成一个用于请求数据对称加密的密钥(对称加密更快),用这个公钥进行非对称加密,在由服务器的私钥进行解密,得到这个密钥,那么,真正建立的数据传输就以此密钥进行加、解密。

下面,模拟一下如何进行的公钥证书受信

创建 公钥.der 及 证书.cer 文件

在终端依次输入如下命令:


//生成私钥
openssl genrsa -out private_key.pem 1024

//获取 证书.cer
openssl req -new -key private_key.pem -out rsaCertReq.csr

openssl x509 -req -days 3650 -in rsaCertReq.csr -signkey private_key.pem -out rsaCert.crt

//将 .crt 格式证书转换为 .cer 格式证书,后面iOS程序里需要 .cer格式证书
openssl x509 -in rsaCert.crt -out rsaCert.cer -outform der

//获得 公钥.der
openssl x509 -outform der -in rsaCert.crt -out public_key.der



过程中会有一些简单信息输入,这里没有特别的要求,文件创建后目录如图:

image.png

把 .cer 格式证书 和 公钥.der 格式证书 全部拖到工程里:

image.png

下面输出一段代码,用 .cer 证书去验证 公钥.der 是否可信。


- (void)trustIsVaild

{
//获取工程下所有cer证书(https 网络请求鉴权必需证书)
    NSArray *paths = [[NSBundle mainBundle] pathsForResourcesOfType:@"cer" inDirectory:@"."];

//保存工程内的所有 cer 证书(并在后面设置为鉴权锚点)
    NSMutableArray *pinnedCertificates = [NSMutableArray array];

    for (NSString *path in paths) {

        NSData *certificateData = [NSData dataWithContentsOfFile:path];

        [pinnedCertificates addObject:( __bridge_transfer id)SecCertificateCreateWithData(NULL, ( __bridge CFDataRef)certificateData)];

    }

//获取工程下的公钥数字证书(在https网络请求认证挑战中由服务器返回)
    NSString * publicKeyPath = [[NSBundle mainBundle] pathForResource:@"public_key" ofType:@"der"];

    NSData *derData = [[NSData alloc] initWithContentsOfFile:publicKeyPath];

//证书资源
    SecCertificateRef myCertificate = SecCertificateCreateWithData(kCFAllocatorDefault, ( __bridge CFDataRef)derData);

//验证政策设置
    SecPolicyRef myPolicy = SecPolicyCreateBasicX509();

    SecTrustRef myTrust;

//SecTrust 赋值
    OSStatus status = SecTrustCreateWithCertificates(myCertificate,myPolicy,&myTrust);

    if (status == noErr) {
//设置证书锚点(这里的意思就是如果鉴权到指定的证书是有效的,那么,就信任此公钥数字签名,这里如果不设置,那么就会一直找向根证书,由于工程里的公钥数字证书是自签的,所以,一定不会受信)
        SecTrustSetAnchorCertificates(myTrust, ( __bridge CFArrayRef)pinnedCertificates);

        SecTrustResultType result;

        if (SecTrustEvaluate(myTrust, &result) == 0) {

//kSecTrustResultUnspecified 隐式信任
//kSecTrustResultProceed 可继续进行
            if ((result == kSecTrustResultUnspecified || result == kSecTrustResultProceed)) {

                NSLog(@"受信任的证书");

            } else {

                NSLog(@"未受信任的证书");

            }

        } else {

            NSLog(@"未受信任的证书初始化操作失败");

        }

    }

}

运行如下:

image.png

顺便输出一下不设置 证书锚点 控制台内容:

if (status == noErr) {
//不设置锚点
//SecTrustSetAnchorCertificates(myTrust, (__bridge CFArrayRef)pinnedCertificates);

        SecTrustResultType result;

        if (SecTrustEvaluate(myTrust, &result) == 0) {

            if ((result == kSecTrustResultUnspecified || result == kSecTrustResultProceed)) {

                NSLog(@"受信任的证书");

            } else {

                NSLog(@"未受信任的证书");
            }
        } else {

            NSLog(@"未受信任的证书初始化操作失败");
        }
    }

image.png

到这里,公钥证书如果受信,那么,下一步就规定一个 对称加密 session key 用这个公钥加密,发送到服务器,然后用对应的私钥解密,供以后的数据传输进行 对称加密 操作。

所以,移动端在做自定义证书鉴权的时候就需要存储服务器生成的 .cer 证书文件!

AFNetworking 下的鉴权方式处理相对复杂,因为 URLSession 的认证挑战回调是允许程序员全部无条件开启的,所以,AFNetworking 在默认鉴权行为的基础上添加了几种自定义鉴权方式:

typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {

    AFSSLPinningModeNone,//无条件开启

    AFSSLPinningModePublicKey,//认证公钥内容

    AFSSLPinningModeCertificate,//认证证书

};

而且,在此之前 AFNetworking 通过

@property (readwrite, nonatomic, copy) AFURLSessionTaskAuthenticationChallengeBlock authenticationChallengeHandler;

暴露给外界闭包进行自定义鉴权逻辑及处理结果。

- (void)URLSession:(NSURLSession *)session

              task:(NSURLSessionTask *)task

didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
BOOL evaluateServerTrust = NO;
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential *credential = nil;

//AFNetworking 暴露给程序员自定义处理入口
if (self.authenticationChallengeHandler) {
id result = self.authenticationChallengeHandler(....);
... (解析处理结果)
}
...(证书认证处理代码)

//最后调用 completionHandler 继续执行操作
    if (completionHandler) {

        completionHandler(disposition, credential);

    }
}

disposition: 可以设置继续鉴权挑战(NSURLSessionAuthChallengeUseCredential) 或者中断鉴权挑战(NSURLSessionAuthChallengeCancelAuthenticationChallenge

credential: 如果证书认证通过则直接进行赋值,

credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];

否则为 nil

这里只是简单的梳理一下证书信任逻辑,就不再赘述 AFNetworking 源码部分。代码拙劣,大神勿笑。


作者:头疼脑胀的代码搬运工
链接:https://juejin.cn/post/7030345610704191501
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Dart中async和async*有什么区别?

水文我在行 在Dart中两个关键字,长得很像async和async*,可能还有的朋友还不知道他们两个有什么区别。现在简单介绍一下。 简单答案 简单回答这个问题就是: async返回Future. async*返回Stream. async async不必多...
继续阅读 »

水文我在行


在Dart中两个关键字,长得很像asyncasync*,可能还有的朋友还不知道他们两个有什么区别。现在简单介绍一下。


简单答案


简单回答这个问题就是:



  • async返回Future.

  • async*返回Stream.


async


async不必多言,有了解的都知道这是异步调用。
当一个函数被标记成async的时候,意味这个方法可能要从事耗时工作,比如说网络请求、处理图片等等。被async标记的方法会把返回值用Future包裹一下。


Future<int> doSomeLongTask() async {
await Future.delayed(const Duration(seconds: 1));
return 42;
}

我们可以通过await来获取Future里的返回值:


main() async {
int result = await doSomeLongTask();
print(result); // 等待一分钟后打印 '42'
}

async*


async*比多了一个*,加上*其实是函数生成器的意思。
async*标记的函数会在返回一组返回值,这些返回值会被包裹在Stream中。async*其实是为yield关键字发出的值提供了一个语法糖。


Stream<int> countForOneMinute() async* {
for (int i = 1; i <= 60; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i;
}
}

上面的其实就是异步生成器了。我们可以使用yield替代return返回数据,因为这个是时候我们的函数还在执行中。
此时,我们就可以使用await for去等待Stream发出的每一个值了。


main() async {
await for (int i in countForOneMinute()) {
print(i); // 打印 1 到 60,一个秒一个整数
}
}

应用


初一看,好像并没有什么用。因为自从我使用Flutter以来,我几乎没有使用过async*。但是现在假使我们有这样的一个需求,我们需要每一秒钟请求一次接口,一共请求10次,来看看京东还剩多少茅台。


首先看看使用async的代码:


  getMaoTai() async{
for (int i = 0; i <10; i++){
await Future.delayed(Duration(seconds: 1), ()async {
MaoTaiData data = await fetchMaoTaiData();
setState(){
//更新UI
};
});
}

上面的代码里使用了循环,然后每一秒钟请求依次接口,返回数据后调用setState()更新UI。这样做会导致你每隔一两秒就setState()一次,如果不怕性能问题,不怕产品经理打你,你这么玩玩。这个时候async*就应该上场了:


    Stream<MaoTaiData> getData() async* {
for (int i = 0; i <10; i++) {
await Future.delayed(Duration(seconds: 1));
yield await fetchMaoTaiData();
}
}

这样我们就可以使用StreamBuilder包裹下Widget,就不必每次都去setState()了。


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

Flutter + Rust 高性能的跨端尝试

稍作配置,同一份代码横跨 Android & IOS,相比于 React Native 方案更加高性能。除此之外,得益于 Rust 跨平台加持,Rust 部分的代码可在种种场合复用。 这篇文章旨在记录作者尝试结合 Rust 和 Flutter 的过程,...
继续阅读 »

稍作配置,同一份代码横跨 Android & IOS,相比于 React Native 方案更加高性能。除此之外,得益于 Rust 跨平台加持,Rust 部分的代码可在种种场合复用。


这篇文章旨在记录作者尝试结合 Rust 和 Flutter 的过程,且仅为初步尝试。不会涉及诸如:



  • 如何搭建一个 Flutter 开发环境,以及 Dart 语言怎么用

  • 如何搭建一个 Rust 开发环境,以及 Rust 语言怎么学


Environment



  • Flutter: Android, IOS 工具配置妥当
    -w672

  • Rust: Stable 就好
    -w513


Rust Part


Prepare cross-platform toolchains & deps


IOS


# Download targets for IOS ( 64 bit targets (real device & simulator) )
rustup target add aarch64-apple-ios x86_64-apple-ios

# Install cargo-lipo to generate the iOS universal library
cargo install cargo-lipo

Android


这里有一些行之有效的辅助脚本用于更加快捷配置交叉编译工具。




  1. 获取 Android NDK


    sdkmanager --verbose ndk-bundle

    如果已经准备好了 Android NDK ,则设置环境变量 $ANDROID_NDK_HOME


    # example:
    export ANDROID_NDK_HOME=/Users/yinsiwei/Downloads/android-ndk-r20b


  2. Create the standalone NDK


    # $(pwd) == ~/Downloads
    git clone https://github.com/kennytm/rust-ios-android.git
    cd rust-ios-android
    ./create-ndk-standalone.sh


  3. 在 Cargo default config VS 配置 Android 交叉编译工具


    cat cargo-config.toml >> ~/.cargo/config

    执行上述命令后会在 Cargo 默认配置中,增加有关 Android 跨平台目标 (targets, aarch64-linux-android, armv7-linux-androideabi, i686-linux-android) 的工具信息,指向刚刚创建的 standalone NDK


    [target.aarch64-linux-android]
    ar = ...
    linker = ..

    [target.armv7-linux-androideabi]
    ...

    [target.i686-linux-android]
    ..


  4. 下载 Rust 支持 Android 交叉编译的依赖





rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android
```


Start a simple rust library




  1. 创建一个 Rust 项目





cargo init my-app-base --lib
```




  1. 编辑 Cargo.toml 修改 crate-type





[lib]
name = "my_app_base"
crate-type = ["staticlib", "cdylib"]
```
Rust 构建出来的二进制库,在 IOS 中是静态链接进最终的程序之中,需要对构建 staticlib 的支持;在 Android 是通过动态链接在运行时装在进程序运行空间的,需要对构建 cdylib 的支持。




  1. 写一些符合 C ABI 的函数 src/lib.rs


    use std::os::raw::c_char;
    use std::ffi::CString;

    #[no_mangle]
    pub unsafe extern fn hello() -> *const c_char {
    let s = CString::new("world").unwrap();
    s.into_raw()
    }

    在上述代码中,每次当外部调用 hello 函数时,会在晋城堆空间中创建一个字符串 ( CString ),并将所有权 ( 释放该字符串所占堆空间的权利 ) 移交给调用者




Build libraries


# IOS
cargo lipo --release

# Android
cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release

然后在 target 目录下会得到以下有用的物料。


target
├── aarch64-linux-android
│   └── release
│   ├── libmy_app_base.a
│   └── libmy_app_base.so
├── armv7-linux-androideabi
│   └── release
│   ├── libmy_app_base.a
│   └── libmy_app_base.so
├── i686-linux-android
│   └── release
│   ├── libmy_app_base.a
│   └── libmy_app_base.so
├── universal
│   └── release
│   └── libmy_app_base.a

至此, Rust 部分就告于段落了。


Flutter Part


Copy build artifacts to flutter project


from: target/universal/release/libmy_app_base.a 
to: ios/

from: target/aarch64-linux-android/release/libmy_app_base.so
to: android/app/src/main/jniLibs/arm64-v8a/

from: target/armv7-linux-androideabi/release/libmy_app_base.so
to: android/app/src/main/jniLibs/armeabi-v7a/

from: target/i686-linux-android/release/libmy_app_base.so
to: android/app/src/main/jniLibs/x86/

Call FFI function in Dart




  1. 添加依赖


    pubspec.yaml -> dev_dependencies: += ffi: ^0.1.3




  2. 添加代码


    (直接在生成的项目上修改,暂不考虑代码设计问题,就简简单单的先把项目跑起来 )


    import 'dart:ffi';
    import 'package:ffi/ffi.dart';

    // ...
    final dylib = Platform.isAndroid ? DynamicLibrary.open('libmy_app_base.so') :DynamicLibrary.process();
    var hello = dylib.lookupFunction<Pointer<Utf8> Function(),Pointer<Utf8> Function()>('hello');

    // ...
    hello();
    // -> world


Build Android Project


flutter run # 如果连接着 Android 设备就直接运行了起来

Build IOS Project


( 复杂了许多 )



  1. 跟随 Flutter 官方文档,配置 XCode 项目。

  2. Build PhasesLink Binary With Libraries 添加 libmy_app_base.a 文件
    (按照图上箭头点...)
    -w1140

  3. Build SettingsOther Linker Flags 中添加 force_load 的参数。
    -w855


这是由于在 Dart 中通过动态的方式调用了该库的相关函数,但在编译期间静态分析的时候,这些都是未曾被调用过的无用函数,就被剪裁掉了。要通过 force_load 方式解决这个问题。


Result


2020-02-15 12.39.59-w300


ezgif-6-785f61b1b53b


Troubleshooting


XCode & IOS


Error getting attached iOS device: ideviceinfo could not find device


sudo xattr -d com.apple.quarantine ~/flutter/bin/cache/artifacts/libimobiledevice/ideviceinfo

将后面的路径替换成你的


dyld: Library not loaded


dyld: Library not loaded: /b/s/w/ir/k/homebrew/Cellar/libimobiledevice-flutter/HEAD-398c120_3/lib/libimobiledevice.6.dylib
Referenced from: /Users/hey/flutter/bin/cache/artifacts/libimobiledevice/idevice_id
Reason: image not found

删除&重新下载


rm -rf /Users/hey/flutter/bin/cache && flutter doctor -v

真机无法启动 Flutter 程序


参见 github.com/flutter/flu…
不要升级到 IOS 13.3.1 系统


What's next




  • 如何高效的实现 Rust & Dart 部分的通信


    我们知道 Flutter 和广大 GUI 库类似,属于单线程模型结合事件系统,因此在主线程中使用 FFI 调用 Rust 部分的代码不能阻塞线程。Dart 语言提供 async/await 语法特性用于在 Flutter 中处理网络请求等阻塞任务。而 Rust 也在最近版本中提供了 async/await 语法支持,如何优雅的把两部分结合起来,这是一个问题。




  • 对 MacOS Windows Linux 桌面端的支持


    Flutter 已经有了对桌面端的实验性支持,可以研究下如何结合在一起,实现跨 6 个端共享代码。




References



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

(转载)中国新观察|疯狂的元宇宙,暴富的新机会?

中新网客户端北京11月12日电(记者 李金磊)近期,元宇宙掀起疯狂热潮,众多中外科技公司竞相涌入,资本市场上的相关概念股暴涨,A股已有股票在20个交易日内涨幅翻倍。如此火爆的元宇宙,也勾起了民众的好奇心。元宇宙到底是什么?会颠覆未来的人类社会吗?又会催生哪些机...
继续阅读 »

中新网客户端北京11月12日电(记者 李金磊)近期,元宇宙掀起疯狂热潮,众多中外科技公司竞相涌入,资本市场上的相关概念股暴涨,A股已有股票在20个交易日内涨幅翻倍。

如此火爆的元宇宙,也勾起了民众的好奇心。元宇宙到底是什么?会颠覆未来的人类社会吗?又会催生哪些机遇?会否是下一个暴富机会?对于人类文明意味着什么?

对此,中国社会科学院数量经济与技术经济研究所信息化与网络经济研究室副主任、中国社会科学院信息化研究中心秘书长左鹏飞接受中新网“中国新观察”栏目专访进行了解读。

访谈实录摘编如下:

元宇宙基本场景的实现需10-20年时间

中新网:元宇宙大火,但是这个概念对于普通人来说还比较陌生,很多人认为元宇宙就是游戏,能否通俗地解释一下到底什么是元宇宙?目前市场上对于元宇宙的认识存在哪些误区?

左鹏飞:当前,元宇宙、区块链等重要概念缺乏公认的定义,在前沿科技和产业领域是一种普遍现象。元宇宙,作为一个正在快速发展的新生事物,从技术、感知、经济、行业等不同视角去分析,就如同从不同角度去观察一个多面体,必然会得出不同的结论。

例如,从技术视角看,元宇宙就是一个3D虚拟空间;从感知视角看,元宇宙是神经元感知的延伸和具化;从经济视角看,元宇宙是跨越实体和虚拟的新型数字经济形态;从认知视角看,元宇宙是一种机器视角对现实世界的认识,等等。其实,通俗来讲,元宇宙是一种可以大规模连接的虚拟现实应用场景。

除了认为元宇宙就是VR之外,目前对于元宇宙的认识误区,主要包括三类:

一是认为元宇宙是一个伪命题。在当前元宇宙热潮涌起的同时,也有不少人认为元宇宙只不过是一场新的割韭菜资本骗局,其实,在技术演进和人类需求的共同推动下,随着时间的不断推进,元宇宙应用的成熟只是一个时间问题。

二是认为元宇宙场景的实现将非常漫长。其实从目前的算力条件、网络技术和虚拟现实技术的发展现状来看,元宇宙基本场景的实现大概只需要10-20年的时间。

三是认为元宇宙只是大企业的游戏。大企业在元宇宙竞争赛道上虽然存在一定优势,但是作为新生事物,中小企业在这个新领域同样具备“从0到1”的突破能力。同时,初创企业更容易在元宇宙发展过程中,做到快速适应和融入创新。

美国Facebook公司宣布更名为Meta。中新社记者 刘关关 摄

庞大网民规模是中国在元宇宙赛道的最大优势

中新网:众多科技巨头纷纷布局元宇宙,脸书(Face book)甚至直接为元宇宙改名“Meta”,为什么元宇宙概念突然如此火爆?中国企业发展元宇宙有哪些优势?

左鹏飞:元宇宙概念在2021年突然爆发,主要有三方面原因:

第一,从规律和趋势来看,支持元宇宙场景落地的相关技术正在接近技术奇点,技术跃变效应正在形成;

第二,从互联网发展阶段来看,当前已经进入后互联网时代,传统网络红利已经到顶并开始消退,元宇宙作为虚拟世界和现实世界融合的载体,对企业来说,蕴含着巨大发展机遇;

第三,从市场先行者来看,罗布乐思(Roblox)作为元宇宙第一股,今年3月份上市以来,业绩远超预期,市场的正反馈效应明显。

中国企业发展元宇宙的优势主要包括三方面:第一,庞大的网民规模。当前,我国网民规模已经超过10亿,庞大的人口基数是丰富和完善元宇宙应用场景的重要力量,这将是中国企业在元宇宙赛道上最大的优势。

第二,丰富的创新经验。近20年来,互联网行业是中国竞争最激烈的行业之一,企业之间竞争长期白热化,只有勇于创新、善于创新、乐于创新的企业才能生存下来。这些实战积累形成的创新经验也是中国企业优势之一。

第三,有力的政策支持。从我国数字经济发展所取得的成就来看,无论是中央还是地方,都积极支持企业在数字经济领域探索新技术、新业态,并通过具体政策进行支持。今年以来,已有多个国家部委和地方政府出台(支持)VR产业发展的相关政策。

抖音打造的虚拟博主柳夜熙爆红。

元宇宙将打破人们所习惯的现实世界物理规则

中新网:从电影《头号玩家》中打造的避世虚拟游戏世界绿洲,到今年上映的《失控玩家》里虚拟NPC(非玩家角色)的自我觉醒,再到抖音打造的虚拟博主柳夜熙爆红,您怎么看这种现象?元宇宙真的如脸书创始人扎克伯格所言会颠覆未来的人类社会吗?

左鹏飞: 近几年,伴随虚拟现实技术的不断进步,各类平台和机构纷纷打造虚拟偶像。今年由于元宇宙的突然爆发,部分虚拟偶像顺势爆红,柳夜熙就是其中之一。

这一现象发生的背后,具有更深层涵义:一方面,从用户角度来说,高度拟人化、共情化的虚拟偶像,满足了用户的新鲜体验,增强了用户对于元宇宙场景的初步认知,具有较为深远的意义;另一方面,从企业角度来说,虚拟偶像日益成为企业价值和理念的载体,未来有可能成为企业最显著的标志,因此越来越多的企业推出自己的虚拟偶像。

元宇宙是一把双刃剑。一方面,元宇宙的发展,会让更多的人沉浸在虚拟世界中。从上网时间来看,人类平均上网时间呈现逐年增长趋势,而且青少年一代表现更加明显,元宇宙带来全新的视觉冲击,肯定会进一步延长人类的“在网时间”。

另一方面,元宇宙的发展,将打破我们所习惯的现实世界物理规则,在虚拟空间,重新定义我们绝大部分的生产生活方式,以全新的生产方式和合作方式提高全社会生产效率。元宇宙对于人类社会的影响,取决于人类本身。

因此,我们需要前瞻性研究元宇宙发展的原则规范、技术伦理等一系列内容,让元宇宙更好地服务人类,而不是成为时间黑洞,吞噬人类的未来。

资料图:游客体验VR技术 梁婷 摄

元宇宙产业崛起将带来新财富格局

中新网:普华永道预计元宇宙市场规模在2030年将达到1.5万亿美元。从现实看,元宇宙发展处于什么阶段?会催生哪些机遇?是否为下一个暴富机会?

左鹏飞:元宇宙的发展大致可以分为三个阶段:萌芽阶段,由企业创造一些简单的虚拟现实内容(提供)给用户,用户在虚拟现实场景下可以进行简单的人与人交互;成长阶段,用户在元宇宙场景下,可以进行复杂的人与人交互,以及简单的人机交互;成熟阶段,用户、机器在元宇宙场景下可以进行高度互动。

我们目前处于萌芽阶段,大概需要10-20年时间,才能进入成长阶段。

从产业链角度来看,元宇宙发展将催生六个方面的机遇:

首先,影响元宇宙应用场景(实现)的直接相关技术产业,如XR、AI、空间映射、数字孪生等领域;其次,支撑元宇宙场景运行的底层技术产业,元宇宙需要实现大量底层技术进行深度融合,如区块链、云计算、大数据、未来网络、半导体等领域;第三,虚拟与现实连接(技术)产业。即把虚拟世界与现实世界连接在一起的相关技术,如可穿戴设备、脑机接口、微传感器等领域;第四,元宇宙(应用)场景下的产业“元化”,如购物、娱乐、社交、学习、办公等领域;第五,视觉仿真下的创意产业,一种虚拟世界的新型创造的经济;第六,助力元宇宙系统健康有序运行的支付、安全等辅助产业。

数字化革命正在重塑传统产业格局,而新产业的崛起,往往会带来新的财富格局。目前,已有多个国际知名咨询机构,对元宇宙市场前景表示乐观。我们认为,各行各业,只要能与元宇宙的发展做到良好衔接,也就抓住了一次新的财富增长机遇。

谨防借元宇宙概念的名义催生科技泡沫

中新网:现在资本市场热炒元宇宙概念,相关股票暴涨。从当下看,元宇宙市场估值是否被高估了,已产生了泡沫?

左鹏飞:今年以来,全球主要投资机构纷纷投资元宇宙领域,推高了元宇宙的关注度和热度,带来了相关概念股的大涨,在一定程度上增加了元宇宙泡沫。资本市场习惯热捧新概念,在此之前,也热炒过区块链、人工智能、大数据等概念,因此很多人担心元宇宙会成为一个新的泡沫。

与以往概念有所不同,元宇宙承载着真实世界的延伸与拓展,企业和用户对其场景实现有着较为明确的目标和预期,是由技术进步与人类需求共同推进的。正因如此,我们更应保持理性的行为认知和正确的市场导向,推动其产业逐步走向成熟,谨防借技术和概念的名义催生科技泡沫。

儿童戴着VR眼镜体验“太空飞行”。中新社记者 任东 摄

元宇宙发展对反垄断和隐私保护是个大挑战

中新网:我们距离真正的元宇宙世界还有多远,面临哪些挑战和难题?若真的实现,它将给我们的生活和社会经济发展带来哪些巨变?

左鹏飞:我们距离元宇宙基本场景的实现大概需要10-20年时间,除了技术瓶颈以外,我们主要面临四个方面的挑战:

第一,如何确立元宇宙运行的基本框架?元宇宙是现实经济社会的场景模拟,其中涉及到价值观念、制度设计和法律秩序等一系列基本框架选择问题。

第二,如何避免形成高度垄断?元宇宙场景的实现,需要巨大的人力和物力投入,同时又要实现超大规模的连接,因此元宇宙具有一种内在垄断基因。我们需要避免元宇宙被少数力量所垄断。

第三,如何维系现实世界和元宇宙之间的正面互动关系?也就是用好元宇宙这把双刃剑,谨防人们沉浸在元宇宙场景中不能自拔,要发挥元宇宙的积极作用。

第四,如何保护隐私和数据安全?元宇宙的发展,需要搜集人们更多的个人信息,保护个人隐私和数据安全将是一个非常大的挑战。

元宇宙将给我们的生活和社会经济发展带来五个方面的巨变:一是从技术创新和协作方式上,进一步提高社会生产效率;二是催生出一系列新技术新业态新模式,促进传统产业变革;三是推动文创产业跨界衍生,极大刺激信息消费;四是重构工作生活方式,大量工作和生活将在虚拟世界发生;五是推动智慧城市建设,创新社会治理模式。

资料图:参观者体验高科技眼镜。 中新社记者 许少峰 摄

元宇宙或让人类衍生出“双世界”文明形态

中新网:有人认为,如果人类在走向太空文明之前就实现了元宇宙世界,这将是一场灾难。您是否认同这个观点?您如何看待元宇宙对人类文明走向的影响?

左鹏飞:技术的本质是实现与所处环境更好相融的一种手段。走向太空文明和进入元宇宙世界,分别反映了人类对外部世界探索和内心世界需求的两种不同技术追求。认为人类在走向太空文明之前实现元宇宙(世界)将是一场灾难,其根源是担心人类沉浸于虚拟世界不能自拔,失去了探索精神。这种担忧具有一定的合理性,但是用“灾难”来形容可能并不合适。

第一,这两种技术追求,本身并不是一种对立关系,而是可以并行的。最基本的考虑是,并非所有人都会喜欢虚拟世界,就如同并非所有人都喜欢游戏一样,所以不用担心所有人都会沉浸在虚拟世界;第二,我们发展元宇宙过程中,必然会注重形成一种现实世界和元宇宙之间的正面互动关系,对负向发展会进行必要纠正,如同现在的游戏防沉迷系统。第三,探索太空文明很重要,我们与地球和谐相处也很重要,元宇宙的发展会促进我们与地球环境更好相容。

元宇宙会引导人类积极探索内心世界,在虚拟世界创造理想生活,这在一定程度上影响人类现实社会活动。因此,伴随元宇宙发展,未来可能形成一种虚拟世界与现实世界高度互动,衍生出一种在观念、习惯、技术、思维等层面相互补和平衡的“双世界”文明形态。

受访嘉宾简介:

左鹏飞,博士、副研究员,中国社会科学院数量经济与技术经济研究所信息化与网络经济研究室副主任、中国社会科学院信息化研究中心秘书长,主要研究领域:信息技术经济、互联网经济、信息化。


收起阅读 »

监管发关注函,媒体发文降温,元宇宙概念热度或将退去

元宇宙这一概念今年下半年迅速走红并且热度居高不下,中信证券称,元宇宙是未来20年的下一代互联网,是人类未来的数字化生存。终极元宇宙将包含互联网、物联网、AR/VR、3D图形渲染、AI人工智能、高性能计算、云计算等技术。11月10日晚,腾讯控股董事会主席马化腾表...
继续阅读 »

元宇宙这一概念今年下半年迅速走红并且热度居高不下,中信证券称,元宇宙是未来20年的下一代互联网,是人类未来的数字化生存。终极元宇宙将包含互联网、物联网、AR/VR、3D图形渲染、AI人工智能、高性能计算、云计算等技术。

11月10日晚,腾讯控股董事会主席马化腾表示,公司拥有大量探索和开发元宇宙的技术和能力。

A股多家上市公司蹭上这一概念股价表现火热,11月以来短短12天,就元宇宙业务进展,投资者在沪深交易所已经与上市公司互动逾2000次。以中青宝、佳创视讯为代表,两只个股价格在近10个工作日内涨幅接近100%。



元宇宙概念板块的火热引起了监管层的注意,近几个交易日,已有佳创视讯、盛天网络、中青宝、昆仑万维等7家上市公司因业务涉元宇宙收到监管关注函。其中,上市公司是否存在主动迎合热点炒作股价成为关注重点,且上市公司公告、投资者关系互动平台以及相关公众号内容均纳 入了关注范围。

经济日报刊文称,个人投资者应对当前被热炒的“元宇宙”概念股保持清醒认识,切莫贸然为一个刚刚兴起且不成熟的概念买单。判断行业的成长性,首先要看应用终端是否普及,能否建立虚拟和现实的联系。其次要有真实的内容建设和落地场景。“元宇宙”应是一项长期投资建设的系统性工程,其基本面还有待时间验证,短期热炒不可取。

证券日报也发文为持续大火的元宇宙降温,部分上市公司的描述过于“虚拟”,并未在现实业绩中找到支撑。对于仍处于雏形探索阶段的新生事物,投资者在保持敏 锐的同时也需要保持理性。从部分上市公司与投资者的互动来看,有些公司属于“被元宇宙”——公司反复说,“我没有 ”,但投资者说,“不,你有!”科技的眼界和触角确实可以向未来无限延伸,但是信息披露并不能穿越时空。毕竟,仅仅依赖过于虚拟的描绘,上市公司很可能在经历爆炒后被市场抛弃。

百瑞赢证券咨询认为,元宇宙目前还处于技术发展的初期,行业发展尚未成熟,谁能成为真正的龙头尚未可知,市场上的资金跟风炒作居多,随着监管层发关注函,媒体发文,元宇宙热度或将退去,市场上的资金一哄而散,投资者盲目追随极有可能高位站岗。

原文链接:https://www.163.com/dy/article/GOK0ISRK0519K5IU.html


收起阅读 »

起初Jetpack Navigation把我逼疯了,可是后来真香

1. Navigation到底该如何正确的使用 相信大家对 Navigation都有所耳闻,我不细说怎么用了,官方的讲解也很详细。我是想说一下到底该如何更好的使用这个组件。 这个组件其实是需要配合官方的MVVM架构使用的,ViewModel+LiveData结...
继续阅读 »

1. Navigation到底该如何正确的使用


相信大家对 Navigation都有所耳闻,我不细说怎么用了,官方的讲解也很详细。我是想说一下到底该如何更好的使用这个组件。


这个组件其实是需要配合官方的MVVM架构使用的,ViewModel+LiveData结合才能更好的展现出Navigation的优势。


在官方的讲解示例中没有用到ViewModelLiveData,官方只是演示了Navigation怎么用怎么在页面之间传值,和这个组件的一些特性之类的。但真正用好还是要结合ViewModelLiveData


2. Navigation大家都以为的缺陷


起初我用Navigation的时候,最头疼的是当按下返回键回到上个页面的时候整个页面被重建了,这是开发中不想要的结果,很多时候大家都会去寻求一种方式:将官方的replace方式替换为HideShow。起初也是想到这个方式,然后结合在网上得到的资料自己写了一个方式FragmentNavigatorHideShow


3. 然而这不是缺陷


但是很快啊,我发现这个方式(HideShow)存在严重的逻辑问题。



这里可以看到,有一些场景下,我们有某个页面可以打开和自己相同的页面,只不过是展示的数据不同而已。当我用hideshow的方式展示下个页面的时候,会发现打开的还是上个页面。当按下返回键之后,上个相同的页面不见了,新打开的页面和上个页面尽然是同一个对象,这肯定不符合业务逻辑。于是我又开始研究起replace的方式,当然我在使用这个Navigation的时候就采用了MVVM + ViewModel+LiveData,这时候我想起ViewModel是不受Fragment重建影响的。于是我打印了一下在使用replace方式下页面生命周期的变化。


HomeFragment进入MyFragment生命周期变化:


变化Log


可以看到,在replace之后HomeFragment并没有执行onDestory而是执行了onDestoryView这也使得页面必须要重建。而onDestoryView不会导致 ViewModel的销毁。也就是说 ViewModel还在,ViewModel中的LiveData所保存的数据也是存在的。当我按下返回键,重新回到HomeFragment页面理所当然的执行了onViewCreated,此时代码中页面对ViewModel中的LiveData所观察数据又重新进行了observe观察,因为LiveData之前保存过数据所以这段代码也理所当然的被执行了。页面上也重新填充了数据。


    override fun initLiveData() {
viewModel.liveData.observe(this) {
Log.d(TAG, "data change : $it ")
textView.text = it
}
}

这个时候,你会发现,页面好像没有重建一样。我这才理解了谷歌的用意。它这步棋下的很巧啊。


也里所当然的我抛弃了FragmentNavigatorHideShow,又拥抱回了谷歌爸爸。


说回上面那个问题,当一个页面中可以打开自己的时候,在FragmentNavigator源码中只要是导航到下一个目的地就会重新创建一个新的Fragment,上一个Fragment会被加入回退栈里,所以才可以在Fragment中打开一个新的自己,来展示不同的信息。而hideshow的方式会每次都去查找之前有没有创建过这个页面,如果有,就Show,如果没有就创建。所以才会导致自己打开自己,永远都是同一个Fragment对象。


4. 那么到底该如何正确使用


到底该如何正确使用Navigation,这也是我这段时间使用的一点点经验。


Fragment中的所有动态数据都由ViewModel中的LiveData保存。我们只监听LiveData的数据变化,这也符合MVVM 的架构麻,当然还有一个Model我没说Repository,这个我就不解释了。



Fragment之间传递的数据都交给Bundle页面重建的时候这些数据也会被保存,再次走一遍从Bundle中取数据的过程是完全不会报错的。所以页面上的数据不会被丢失了,而像RecyclerView,ViewPager之类的控件它们也会保存自己之前的状态,页面重建后,RecyclerView,ViewPager会记录自己滑动的位置的,这个不用担心,还有一点就是有一些控件,比如CoordinatorLayout你可能需要给它和它的子View控件一个Id才能保存滑动状态。


遵循这样的一个规则之后呢,就可以忽略这个页面重建的问题了。


5. Navigation的页面转场动画的一些问题


用过Navigation的都知道,页面转场动画要一个一个的添加,就像这样:


<!--这是官方的Demo示例-->
<fragment
android:id="@+id/title_screen"
android:name="com.example.android.navigationsample.TitleScreen"
android:label="fragment_title_screen"
tools:layout="@layout/fragment_title_screen">
<action
android:id="@+id/action_title_screen_to_register"
app:destination="@id/register"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"/>
<action
android:id="@+id/action_title_screen_to_leaderboard"
app:destination="@id/leaderboard"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"/>
</fragment>

每一个标签都要写一遍一样的代码,让我很头疼。于是我还是想到了,重写FragmentNavigator将所有的增加一个判断如果标签中没有设置专场动画,那么我就给这个Fragment添加上专场动画。


      	//我一开始设想的载源码位置处添加的动画操作
int enterAnim = navOptions != null ? navOptions.getEnterAnim() : 动画id;
int exitAnim = navOptions != null ? navOptions.getExitAnim() : 动画id;
int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : 动画id;
int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : 动画id;
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = enterAnim != -1 ? enterAnim : 0;
exitAnim = exitAnim != -1 ? exitAnim : 0;
popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
}

然而我太天真了,我们想到的,谷歌爸爸都考虑过了。因为如果像我一样天真的加上这样的判断之后,你会发现,第一个默认Fragment也拥有了动画属性。而且做隐式链接跳转的时候,这个动画会非常影响观感。所以第一个默认Fragment不能有转场动画。当然后来我想到了判断返回栈是否存在为空,通过这个判断是否是第一个页面。但是我都能想到谷歌爸爸肯定也想到了。他们不这么做肯定是有原因的吧。还是等待官方优化,于是我放弃了,老老实实的挨个复制粘贴,


不过后来我在Navigationissues 找到了这个问题,因该在优化的计划中吧。


6. Replace在重建Fragment的时候,过度动画卡顿


在使用 Navigation的时候,按下返回键回到上个页面,页面重建,这个时候会发现过度动画会有那么几百毫秒卡那么一下,一个转场动画也就400毫秒左右,卡那么一下效果是非常明显的。这也归功于Fragment重建的原因了,页面展示的数据量巨大的时候,重建时的绘制工作量也是相当的大,所以肯定会卡那么一下下啦。


后来我发现了一个方法:


    override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
return super.onCreateAnimation(transit, enter, nextAnim)
}

我们可以把数据加载的过程放在动画执行之后再请求。


    override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (enter) {
if (nextAnim > 0) {
val animation = AnimationUtils.loadAnimation(requireActivity(), nextAnim)
animation.setAnimationListener(object : Animation.AnimationListener {

override fun onAnimationEnd(animation: Animation?) {
onEnterAnimEnd()//动画结束后再去请求网络数据、或者初始化LiveData
}

})
return animation
} else {
onEnterAnimEnd()
}
} else {
if (nextAnim > 0) {
return AnimationUtils.loadAnimation(requireActivity(), nextAnim)
}
}
return super.onCreateAnimation(transit, enter, nextAnim)
}

/**
* 子类重写,判断是否需要加载数据,或者初始化LiveData
*/
fun onEnterAnimEnd(){
Log.d(TAG, "onEnterAnimEnd: ")
}

然后我们再找到onViewCreated方法,因为Base类我们通常会将初始化方法进行抽象所以我们要进行两个事情:


1: 在View进行绘制初始化的时候暂停过场动画


2: 在View与Data初始化结束后再开始动画的执行



override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//暂停过场动画
postponeEnterTransition()
//View与数据初始化
initViewAndData(view)
initLiveData()//LiveData的初始化可以放到动画结束之后
//最后使用这个方法监听视图结构,并开始执行过场动画
(view.parent as? ViewGroup)?.apply {
OneShotPreDrawListener.add(this){
startPostponedEnterTransition()
}
}
}

这样操作之后就可以预防由于RecyclerView大量数据加载时导致的过场动画掉帧问题了,但是也不是完全不掉帧,不过这个解决办法还是有效的,剩下的就是优化自己代码了,防止做太多的耗时操作。


我以前的解决办法是将过场动画进行延时100毫秒执行,但这个方式,我自己也是不满足,于是还是翻阅官方的文档,查找解决办法,掘友也给我提出过相关的问题,这段时间不忙了,所以重新修改一下以前自己犯的问题。


其他推荐


《LiveData巧妙封装,我再也不怕Navigation重建Fragment啦!》对这篇文章进行了LiveData的使用补充


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

(转载)最近大火的元宇宙到底是什么?

准确地说,元宇宙不是一个新的概念,它更像是一个经典概念的重生。1992年,美国著名科幻大师尼尔·斯蒂芬森在其小说《雪崩》中这样描述元宇宙:“戴上耳机和目镜,找到连接终端,就能够以虚拟分身的方式进入由计算机模拟、与真实世界平行的虚拟空间。”今年3月,元宇宙概念第...
继续阅读 »

准确地说,元宇宙不是一个新的概念,它更像是一个经典概念的重生。1992年,美国著名科幻大师尼尔·斯蒂芬森在其小说《雪崩》中这样描述元宇宙:“戴上耳机和目镜,找到连接终端,就能够以虚拟分身的方式进入由计算机模拟、与真实世界平行的虚拟空间。”

今年3月,元宇宙概念第一股罗布乐思(Roblox)在美国纽约证券交易所正式上市;5月,Facebook表示将在5年内转型成一家元宇宙公司;8月,字节跳动斥巨资收购VR创业公司Pico……今年,元宇宙无疑成为了科技领域最火爆的概念之一。

那么,元宇宙到底是什么?为何各大数字科技巨头纷纷入局元宇宙?我国元宇宙产业又该如何布局与发展?

元宇宙目前尚无公认定义

准确地说,元宇宙不是一个新的概念,它更像是一个经典概念的重生,是在扩展现实(XR)、区块链、云计算、数字孪生等新技术下的概念具化。

1992年,美国著名科幻大师尼尔·斯蒂芬森在其小说《雪崩》中这样描述元宇宙:“戴上耳机和目镜,找到连接终端,就能够以虚拟分身的方式进入由计算机模拟、与真实世界平行的虚拟空间。”

当然,核心概念缺乏公认的定义是前沿科技领域的一个普遍现象。元宇宙虽然备受各方关注和期待,但同样没有一个公认的定义。回归概念本质,可以认为元宇宙是在传统网络空间基础上,伴随多种数字技术成熟度的提升,构建形成的既映射于、又独立于现实世界的虚拟世界。同时,元宇宙并非一个简单的虚拟空间,而是把网络、硬件终端和用户囊括进一个永续的、广覆盖的虚拟现实系统之中,系统中既有现实世界的数字化复制物,也有虚拟世界的创造物。

当前,关于元宇宙的一切都还在争论中,从不同视角去分析会得到差异性极大的结论,但元宇宙所具有的基本特征则已得到业界的普遍认可。

其基本特征包括:沉浸式体验,低延迟和拟真感让用户具有身临其境的感官体验;虚拟化分身,现实世界的用户将在数字世界中拥有一个或多个ID身份;开放式创造,用户通过终端进入数字世界,可利用海量资源展开创造活动;强社交属性,现实社交关系链将在数字世界发生转移和重组;稳定化系统,具有安全、稳定、有序的经济运行系统。

受到科技巨头、政府部门的青睐

8月以来,元宇宙概念更加炙手可热,日本社交巨头GREE宣布将开展元宇宙业务、英伟达发布会上出场了十几秒的“数字替身”、微软在Inspire全球合作伙伴大会上宣布了企业元宇宙解决方案……事实上,不仅是各大科技巨头在争相布局元宇宙赛道,一些国家的政府相关部门也积极参与其中。5月18日,韩国科学技术和信息通信部发起成立了“元宇宙联盟”,该联盟包括现代、SK集团、LG集团等200多家韩国本土企业和组织,其目标是打造国家级增强现实平台,并在未来向社会提供公共虚拟服务;7月13日,日本经济产业省发布了《关于虚拟空间行业未来可能性与课题的调查报告》,归纳总结了日本虚拟空间行业亟须解决的问题,以期能在全球虚拟空间行业中占据主导地位;8月31日,韩国财政部发布2022年预算,计划斥资2000万美元用于元宇宙平台开发。

元宇宙为何能受到科技巨头、风险投资企业、初创企业,甚至政府部门的青睐?

从企业来看,目前元宇宙仍处于行业发展的初级阶段,无论是底层技术还是应用场景,与未来的成熟形态相比仍有较大差距,但这也意味着

元宇宙相关产业可拓展的空间巨大。因此,拥有多重优势的数字科技巨头想要守住市场,数字科技领域初创企业要获得弯道超车的机会,就必须提前布局,甚至加码元宇宙赛道。

从政府来看,元宇宙不仅是重要的新兴产业,也是需要重视的社会治理领域。伴随着元宇宙产业的快速发展,随之而来的将是一系列新的问题和挑战。元宇宙资深研究专家马修·鲍尔提出:“元宇宙是一个和移动互联网同等级别的概念。”以移动互联网去类比元宇宙,就可以更好地理解政府部门对其关注的内在逻辑。政府希望通过参与元宇宙的形成和发展过程,以便前瞻性考虑和解决其发展所带来的相关问题。

在技术、标准等方面做好前瞻性布局

元宇宙是一个极致开放、复杂、巨大的系统,它涵盖了整个网络空间以及众多硬件设备和现实条件,是由多类型建设者共同构建的超大型数字应用生态。为了加快推动元宇宙从概念走向现实,并在未来的全球竞争中抢占先机,我国应在技术、标准、法律3个方面做好前瞻性布局。

从技术方面来看,技术局限性是元宇宙目前发展的最大瓶颈,XR、区块链、人工智能等相应底层技术距离元宇宙落地应用的需求仍有较大差距。元宇宙产业的成熟,需要大量的基础研究做支撑。对此,要谨防元宇宙成为一些企业的炒作噱头,应鼓励相关企业加强基础研究,增强技术创新能力,稳步提高相关产业技术的成熟度。

从行业标准方面来看,只有像互联网那样通过一系列标准和协议来定义元宇宙,才能实现元宇宙不同生态系统的大连接。对此,应加强元宇宙标准统筹规划,引导和鼓励科技巨头之间展开标准化合作,支持企事业单位进行技术、硬件、软件、服务、内容等行业标准的研制工作,积极地参与制定元宇宙的全球性标准。

从法律方面来看,随着元宇宙的发展,以及逐步走向成熟,平台垄断、税收征管、监管审查、数据安全等一系列问题也将随之产生,提前思考如何防止和解决元宇宙所产生的法律问题成为必不可少的环节。对此,应加强数字科技领域立法工作,在数据、算法、交易等方面及时跟进,研究元宇宙相关法律制度。

可以肯定的是,在技术演进和人类需求的共同推动下,元宇宙场景的实现,元宇宙产业的成熟,只是一个时间问题。作为真实世界的延伸与拓展,元宇宙所带来的巨大机遇和革命性作用是值得期待的,但正因如此,我们更需要理性看待当前的元宇宙热潮,推动元宇宙产业健康发展。

(作者系中国社会科学院数量经济与技术经济研究所副研究员)


收起阅读 »

TypeScript 函数的重载

函数的重载 什么是函数重载呢?允许函数接收不同数量或类型的参数时,做出不同的处理。比如说这个例子: function double(x: number | string): number | string { if (typeof x === 'num...
继续阅读 »

函数的重载


什么是函数重载呢?允许函数接收不同数量或类型的参数时,做出不同的处理。比如说这个例子:


function double(x: number | string): number | string {
if (typeof x === 'number') {
return x * 2;
} else {
return x + ', ' + x;
}
}

本来这个函数,期望输入是 number,输出就是 number。或者输入是 string,输出就是 string。但是我们使用这种联合类型来书写的话,就会存在一个问题。


image-20211011230002191.png


那如何解决这个问题呢?我们可以使用函数的重载


function double(x: number): number; // 输入是 number 类型,输出也是 number 类型
function double(x: string): string;
function double(x: number | string): number | string {
if (typeof x === 'number') {
return x * 2;
} else {
return x + ', ' + x;
}
}

let d = double(1);

image-20211011230140623.png
需要注意的是,函数重载是从上往下匹配,如果有多个函数定义,有包含关系的话,需要把精确的,写在最前面。


习题-根据函数重载知识,完善下面代码块


function paramType (param: ______): string;
function paramType (param: string): string;
function paramType (param: string | number): string {
return typeof param;
};

paramType('panda');
paramType(10);

答案:number


解析:


重载允许一个函数接收不同数量或类型的参数,然后做不同处理。


// 函数声明
function paramType (param: ______): string;
function paramType (param: string): string;
// 函数实现
function paramType (param: string | number): string {
return typeof param;
};

在函数实现中参数的类型为 string | number,故答案为 number;


资料-高阶函数


在维基百科中对于高阶函数的定义是这样的:



在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:



  • 接受一个或多个函数作为输入

  • 输出一个函数



在 JavaScript 的世界中,大家应该都有听说过「函数是一等公民」( first-class citizens )的说法。这里实际上是指在 JavaScript 中,函数被视为 Object 类型,我们可以将函数( function )作为参数传递给其他函数,并且也可以将函数作为其他函数的返回值传递。


因为函数是对象,所以 JavaScript 天然就支持高阶函数的写法。


或许你对高阶函数感到陌生,但实际上在日常的代码编写中,我们一定都用到过高阶函数。


接受一个或多个函数作为输入


我们在日常的学习和开发中,一定遇到过使用回调函数( callback )的场景。回调函数是在完成所有其他操作后,在操作结束时执行的函数。我们通常将回调函数作为最后一个参数,用匿名函数的形式传入。在拥有异步操作的场景中,支持回调函数的传入是至关重要的。


例如我们发送一个 Ajax 请求,我们通常需要在服务器响应完成后进行一些操作,同时在 Web 端,一些需要等待用户响应的行为,如点击、键盘输入等场景也需要用到回调函数。我们看下面的例子


let $submitButton = document.querySelector('#submit-button');

$submitButton.addEventListener('click', function() {
alert('您点击了提交按钮!');
});

这里我们通过将匿名函数作为参数的形式将它传递了 addEventListener 函数。我们也可以改造一下:


let $submitButton = document.querySelector('#submit-button');

let showAlert = function() {
alert('您点击了提交按钮!');
}

$submitButton.addEventListener('click', showAlert);

请注意,这里我们给 addEventListener 传递参数的时候,使用的是 showAlert 而不是 showAlert()。在没有括号的时候,我们传递的是函数本身,而有括号的话,我们传递的是函数的执行结果。


这里将具名函数( named function )作为参数传递给其他函数的能力也为我们使用纯函数(pure functions )提供了很大的想象空间,我们可以定义一个小型的 纯函数库 ,其中的每个纯函数都可以作为参数被复用至多处。


将函数作为结果返回


我们来假想一种场景:假如你拥有一个个人网站,在里面写了很多篇文章。在你的文章中经常介绍你的个人网站,网址是 myblog.com,后来你的站点域名变成了 my-blog.com。这时候你需要将文章中的 myblog.com 替换为 my-blog.com。你或许会这样做:


let replaceSiteUrl = function(text) {
return text.replace(/myblog\.com/ig, 'my-blog.com');
}

在域名变更后,你又想更改网站名称,你可能会这么做:


let replaceSiteName = function(text) {
return text.replace(/MySite/ig, 'MyBlog');
}

上述做法是行之有效的,但是你或许会烦于每次信息变更都要写一个新的函数来适配,而且上述两段代码看起来相似度极高。这时候我们可以考虑使用 高阶函数 来复用这段代码:


let replaceText = function(reg, newText, source){
return function(source) {
return source.replace(reg, newText);
}
}

let replaceSiteUrl = replaceText(/myblog\.com/ig, 'my-blog.com');

console.log(replaceSiteUrl('My site url is https://myblog.com')); // My site url is https://my-blog.com

在上述代码中,我们用到了 JavaScript 函数并不关心他们收到多少个参数的特性,如果没有传递会自动忽略并且认为是 undefined。


总结


高阶函数看起来并没有那么神秘,它是我们日常很自然而然就用到的场景。高阶函数可以帮助我们将一些通用的场景抽象出来,达到多处复用的结果。这也是一种良好的编程习惯。


链接:https://juejin.cn/post/7029481950691737630

收起阅读 »

js 有哪些内置对象

全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。 这里说的全局的对象是说在全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。 js 中的内置对象主要指的是...
继续阅读 »

全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。


这里说的全局的对象是说在全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。



js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般我们经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。



标准内置对象的分类


(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。


   例如 Infinity、NaN、undefined、null 字面量

(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。


   例如 eval()、parseFloat()、parseInt() 等

(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。


   例如 Object、Function、Boolean、Symbol、Error 等

(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。


   例如 Number、Math、Date 

(5)字符串,用来表示和操作字符串的对象。


   例如 String、RegExp

(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。


例如 Array

(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。


   例如 Map、Set、WeakMap、WeakSet

(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。


   例如 SIMD 等

(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。


   例如 JSON 等

(10)控制抽象对象


   例如 Promise、Generator 等

(11)反射


   例如 Reflect、Proxy

(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。


   例如 Intl、Intl.Collator 等

(13)WebAssembly


(14)其他


例如 arguments

链接:https://juejin.cn/post/7029486810745012260

收起阅读 »

京东七鲜一面总结

iOS
京东七鲜一面总结1. http 链接到断开的过程?第一步:TCP建立连接:三次握手HTTP 是应用层协议,他的工作还需要数据层协议的支持,最常与它搭配的就是 TCP 协议(应用层、数据层是 OSI 七层模型中的,以后有机会会说到的)。TCP...
继续阅读 »

京东七鲜一面总结

1. http 链接到断开的过程?

第一步:TCP建立连接:三次握手

HTTP 是应用层协议,他的工作还需要数据层协议的支持,最常与它搭配的就是 TCP 协议(应用层、数据层是 OSI 七层模型中的,以后有机会会说到的)。TCP 协议称为数据传输协议,是可靠传输,面向连接的,并且面向字节流的。

面向连接:通信之前先建立连接,确保双方在线。 可靠传输:在网络正常的情况下,数据不会丢失。 面向字节流:传输灵活,但是 TCP 的传输存在粘包问题,没有明显的数据约定。

在正式发送请求之前,需要先建立 TCP 连接。建立 TCP 连接的过程简单地来说就是客户端和服务端之间发送三次消息来确保连接的建立,这个过程称为三次握手

第二步:浏览器发送请求命令

TCP 连接建立完成后,客户端就可以向服务端发送请求报文来请求了

请求报文分为请求行、请求头、空行、请求体,服务端通过请求行和请求头中的内容获取客户端的信息,通过请求体中的数据获取客户端的传递过来的数据。

第三步:应答响应

在接收到客户端发来的请求报文并确认完毕之后。服务端会向客户端发送响应报文

响应报文是有状态行、响应头、空行和响应体组成,服务端通过状态行和响应头告诉客户端请求的状态和如何对数据处理等信息,真正的数据则在响应体中传输给客户端。

第四步:断开 TCP 连接

当请求完成后,还需要断开 tcp 连接,断开的过程

断开的过程简单地说就算客户端和服务端之间发送四次信息来确保连接的断开,所以称为四次挥手。

延伸:

一、单向请求 HTTP 请求是单向的,是只能由客户端发起请求,由服务端响应的请求-响应模式。(如果你需要双向请求,可以用 socket)

二、基于 TCP 协议 HTTP 是应用层协议,所以其数据传输部分是基于 TCP 协议实现的。

三、无状态 HTTP 请求是无状态的,即没有记忆功能,不能获取之前请求或响应的内容。起初这种简单的模式,能够加快处理速度,保证协议的稳定,但是随着应用的发展,这种无状态的模式会使我们的业务实现变得麻烦,比如说需要保存用户的登录状态,就得专门使用数据库来实现。于是乎,为了实现状态的保持,引入了 Cookie 技术来管理状态。

四、无连接 HTTP 协议不能保存连接状态,每次连接只处理一个请求,用完即断,从而达到节约传输时间、提高并发性。在 TCP 连接断开之后,客户端和服务端就像陌生人一样,下次再发送请求,就得重新建立连接了。有时候,当我们需要发送一段频繁的请求时,这种无连接的状态反而会耗费更多的请求时间(因为建立和断开连接本身也需要时间),于是乎,HTTP1.1 中提出了持久连接的概念,可以在请求头中设置 Connection: keep-alive 来实现。

2. 深拷贝、浅拷贝

深拷贝、浅拷贝实例说明?

深拷贝:是对对象本身的拷贝; 浅拷贝:是对指针的拷贝;

在 oc 中父类的指针可以指向子类的对象,这是多态的一个特性 声明一个 NSString 对象,让它指向一个 NSMutableString 对象,这一点是完全可以的,因为 NSMutableString 的父类就是 NSString。NSMutableString 是一个可以改变的对象,如果我们用 strong 修饰,NSString 对象强引用了 NSMutableString 对象。假如我们在其他的地方修改了这个 NSMutableString 对象,那么 NSString 的值会随之改变。

关于copy修饰相关

1、对 NSString 进行 copy -> 这是一个浅拷贝,但是因为是不可变对象,后期值也不会改变;

2、对 NSString 进行 mutableCopy -> 这是一个深拷贝,但是拷贝出来的是一个可变的对象 NSMutableString;

3、对 NSMutableString 进行 copy -> 这是一个深拷贝,拷贝出来一个不可变的对象;

4、对 NSmutableString 进行 mutableCopy -> 这是一个深拷贝,拷贝出来一个可变的对象;

总结:

对对象进行 mutableCopy,不管对象是可变的还是不可变的都是深拷贝,并且拷贝出来的对象都是可变的;

对对象进行 copy,copy 出来的都是不可变的。

对于系统的非容器类对象,我们可以认为,如果对一不可变对象复制,copy 是指针复制(浅拷贝)和 mutableCopy 就是对象复制(深拷贝)。如果是对可变对象复制,都是深拷贝,但是 copy 返回的对象是不可变的。

指 NSArrayNSDictionary 等。对于容器类本身,上面讨论的结论也是适用的,需要探讨的是复制后容器内对象的变化。

//copy返回不可变对象,mutablecopy返回可变对象
NSArray *array1 = [NSArray arrayWithObjects:@"a",@"b",@"c",nil];
NSArray *arrayCopy1 = [array1 copy];
//arrayCopy1是和array同一个NSArray对象(指向相同的对象),包括array里面的元素也是指向相同的指针
NSLog(@"array1 retain count: %d",[array1 retainCount]);
NSLog(@"array1 retain count: %d",[arrayCopy1 retainCount]);
NSMutableArray *mArrayCopy1 = [array1 mutableCopy];

mArrayCopy1 是 array1 的可变副本,指向的对象和 array1 不同,但是其中的元素和 array1 中的元素指向的是同一个对象。

mArrayCopy1 还可以修改自己的对象 [mArrayCopy1 addObject:@"de"];

[mArrayCopy1 removeObjectAtIndex:0]; array1 和 arrayCopy1 是指针复制,而 mArrayCopy1 是对象复制,mArrayCopy1 还可以改变期内的元素:删除或添加。但是注意的是,容器内的元素内容都是指针复制。

NSArray *mArray1 = [NSArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil];
NSArray *mArrayCopy2 = [mArray1 copy];
NSMutableArray *mArrayMCopy1 = [mArray1 mutableCopy];
NSMutableString *testString = [mArray1 objectAtIndex:0];
[testString appendString:@" tail"];
NSLog(@"%@-%@-%@",mArray1,mArrayMCopy1,mArrayCopy2);
结果:mArray1,mArrayMCopy1,mArrayCopy2三个数组的首元素都发生了变化!

补充:来自开发者留言

面试时我有时会问说说 copy 和 mutableCopy,候选人几乎 100% 说你是说深拷贝和浅拷贝啊 ....,我会说不是!

> 下面用 NSArrayNSMutableArray 举例,因为 NSStringNSMutableString 并不会再引用其它对象,因此不足以说明问题。

1、NSArray 等类型的 copy 实际并没有 copy,或者最多只能说 copy 了引用,因为 copy 方法只返回了 self,这是对内存的优化;

2、而 NSMutableArray 的 copy 确实 copy 了,得到的是新的 NSArray 对象,但并不是所谓的深拷贝,因为它只浅浅地 copy 了一个 NSArray,其中的内容仍然是 NSMutableArray 的内容,可以用 == 直接判等;

3、NSArray 和 NSMutableArray 的 mutableCopy 与 2 相似,只是结果是个 NSMutableArray

4、以上说法一般只适用于 Foundation 提供的一些类型,很多时候并不适用于自己写的类 —— 思考一下你自己写的类是怎么实现 NSCopying 协议的?有实现 NSMutableCopying 协议吗?

所以 ObjC 并没有所谓的深拷贝,要想实现真正的深拷贝,基本上只能依赖序列化+反序列化,这样得到的结果才是深到见底的深拷贝。

如果你说道理大家都懂,深拷贝、浅拷贝只是一种叫法而已,那我只能说你太不严谨了,官方文档从来没这么教过;而且这种说法也不利于初学者理解,以及再学习其它语言时触类旁通,比如 Java。

所以建议严谨一点可以叫引用拷贝和浅拷贝,深拷贝很少用到;或者非要两个互为反义词,可以叫真拷贝和假拷贝。

3. load 和 initialize 区别

load 方法和 initialize 方法区别,以及在子类、父类、分类中调用顺序?

+(void)load

1、+load 方法加载顺序:父类> 子类> 分类 (load 方法都会加载)注意:(如果分类中有 AB,顺序要看 AB 加入工程中顺序) ,可能结果:( 父类> 子类> 分类A> 分类B ) 或者( 父类> 子类> 分类B> 分类A )

2、+load 方法不会被覆盖(比如有父类,子类,分类A,分类B,这四个 load 方法都会加载)。

3、+load 方法调用在 main函数前

+(void)initialize

1、分类 (子类没有 initialize 方法,父类存在或者没有 1initialize 方法)

2、分类> 子类 (多个分类就看编译顺序,只有存在一个)

3、父类> 子类 (分类没有 initialize 方法)

4、父类 (子类,分类都没有 initialize 方法)

总结 +initialize:

1、当调用子类的 + initialize 方法时候,先调用父类的,如果父类有分类, 那么分类的 + initialize 会覆盖掉父类的

2、分类的 + initialize 会覆盖掉父类的

3、子类的 + initialize 不会覆盖分类的

4、父类的 + initialize 不一定会调用, 因为有可能父类的分类重写了它

5、发生在main函数后。

4. 同名方法调用顺序

同名方法在子类、父类、分类的调用顺序?

load,initialize方法调用源码分析

注意+load 方法是根据方法地址直接调用,并不是经过 objc_msgSend 函数调用(通过 isa 和 superclass 找方法),所以不会存在方法覆盖的问题。

5. 事件响应链

事件响应链(同一个控制器有三个view,如何判断是否拥有相同的父视图)

iOS 系统检测到手指触摸( Touch )操作时会将其打包成一个 UIEvent 对象,并放入当前活动 Application 的事件队列,单例的 UIApplication 会从事件队列中取出触摸事件并传递给单例的 UIWindow 来处理,UIWindow 对象首先会使用 hitTest:withEvent: 方法寻找此次 Touch 操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为 hit-test view

UIAppliction --> UIWiondw -->递归找到最适合处理事件的控件-->控件调用 touches 方法-->判断是否实现 touches 方法-->没有实现默认会将事件传递给上一个响应者-->找到上一个响应者。

UIResponder 是所有响应对象的基类,在 UIResponder 类中定义了处理上述各种事件的接口。我们熟悉的 UIApplication、 UIViewController、 UIWindow 和所有继承自 UIView 的 UIKit 类都直接或间接的继承自 UIResponder,所以它们的实例都是可以构成响应者链的响应者对象。

//如何获取父视图
UIResponder *nextResponder = gView.nextResponder;
NSMutableString *p = [NSMutableString stringWithString:@"--"];
while (nextResponder) {
NSLog(@"%@%@", p, NSStringFromClass([nextResponder class]));
[p appendString:@"--"];
nextResponder = nextResponder.nextResponder;
}

如果有父视图则 nextResponder 指向父视图如果是控制器根视图则指向控制器;

控制器如果在导航控制器中则指向导航控制器的相关显示视图最后指向导航控制器;

如果是根控制器则指向 UIWindow

UIWindow 的 nexResponder 指向 UIApplication 最后指向 AppDelegate

6.TCP丢包

TCP 会不会丢包?该怎么处理?网络断开会断开链接还是一直等待,如果一直网络断开呢?

TCP 在不可靠的网络上实现可靠的传输,必然会有丢包。TCP 是一个“”协议,一个详细的包将会被 TCP 拆分为好几个包上传,也是将会把小的封裝成大的上传,这就是说 TCP 粘包和拆包难题。

TCP丢包总结

7.自动释放池

自动释放池创建和释放的时机,在子线程是什么时候创建释放的?

默认主线程的运行循环(runloop)是开启的,子线程的运行循环(runloop)默认是不开启的,也就意味着子线程中不会创建 autoreleasepool,所以需要我们自己在子线程中创建一个自动释放池。(子线程里面使用的类方法都是 autorelease,就会没有池子可释放,也就意味着后面没有办法进行释放,造成内存泄漏。)

在主线程中如果产生事件那么 runloop 才回去创建 autoreleasepool,通过这个道理我们就知道为什么子线程中不会创建自动释放池了,因为子线程的 runloop 默认是关闭的,所以他不会自动创建 autoreleasepool,需要我们手动添加。

如果你生成一个子线程的时候,要在线程开始执行的时候,尽快创建一个自动释放池,否则会内存泄露。因为子线程无法访问主线程的自动释放池。

8.计算机编译流程

源文件: 载入.h.m.cpp 等文件

预处理: 替换宏,删除注释,展开头文件,产生 .i 文件

编译: 将 .i 文件转换为汇编语言,产生 .s 文件

汇编: 将汇编文件转换为机器码文件,产生 .o 文件

链接: 对 .o 文件中引用其他库的地方进行引用,生成最后的可执行文件

dyld加载流程

收起阅读 »

Swift 中的函数盘点

iOS
Swift中的函数盘点「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」前言Swift已经被越来越多的公司使用起来,因此Swift的学习也应该提上日程了。本篇就先探索Swift中的函数,主要包括以下几个方面:Swift函数定义Swif...
继续阅读 »

Swift中的函数盘点

前言

Swift已经被越来越多的公司使用起来,因此Swift的学习也应该提上日程了。本篇就先探索Swift中的函数,主要包括以下几个方面:

  • Swift函数定义
  • Swift函数参数与返回值
  • Swift函数重载
  • 内敛函数优化
  • 函数类型、嵌套函数

一、Swift函数定义

函数的定义包含函数名、函数体、参数及返回值,定义了函数会做什么、接收什么以及返回什么。函数名前要加上 func 关键字修饰。如下为一个完整的函数定义事例:

func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}

  • 函数名: greet
  • 参数:圆括号中(person: String)即为参数,person为参数名,String为类型
  • 返回值:使用一个 -> 来明确函数的返回值,在该事例中定义了一个 String类型的返回值

二、函数返回值与参数

2.1 函数返回值

从返回值的角度看,函数可以分为有返回值无返回值两种。无返回值的函数可以有如下三种定义方式:

func testA() -> Void {
}

func testB() -> () {
}

func testC() {
}

let a = testA()
let b = testB()
let c = testC()


打印 a、b、c 可以发现,三者的类型均为(),即空元组。在 Void 的定义处也可以发现,Swift中 Void 就是空元组。Xnip2021-11-10_18-27-32.png也就是说上面三种方式是等价的,都表示无返回值的情况,不过从代码简洁程度上来说,最后一种更方便使用。

还有一种函数有返回值的情况,如同第一节中所述的函数定义方式,即为一种返回值为String的函数。在Swift中,函数的返回值可以隐式返回,如果函数体中只有一句返回代码,则可以省略return关键字。如下代码所示,两种写法是等价的:

func testD() -> String {
    return "正常返回"
}
func testE() -> String {
    "隐式返回"
}

Swift中还可以通过元组实现多个返回值的情况,如下所示:

func compute(a:Int, b: Int) -> (sum: Int, difference: Int) {
    return (a+b, a-b);
}

compute函数返回一个元组,包含了求和与求差,实现了返回多个值的情况。

2.2 函数参数

与OC不同的是,Swift中函数的参数是let修饰的,参数值是不支持修改的。如下图所示,可以证明。Xnip2021-11-10_22-30-01.png

2.2.1 函数标签

Swift的函数参数除了形参外,还包含一个参数标签。形参在函数内部使用,使得函数体中使用没有歧义,而函数标签用于在函数调用时使用,其目的是增加可读性。函数标签是可以省略的,使用_表示即可,需要注意的是,_与不设置函数标签是不一样的,如下图所示:Xnip2021-11-10_23-04-29.pngXnip2021-11-10_23-05-00.png当使用_时,调用函数不会显示函数标签,而不设置函数标签会把形参作为函数标签。

2.2.2 函数默认参数值

Swift可以给函数参数设置默认值,设置了默认值的参数,在函数调用时可以不传参Xnip2021-11-10_23-30-40.png由于参数 a 有了默认值 8,所以在调用时只传参 b 就可以。同样的,如果参数均有默认值,则在调用函数时,都可以不传值。Xnip2021-11-10_23-35-15.png如图所示,由于两个参数均有默认值,在调用时都不传值,就像调用了一个无参函数一样。

Swift中设置函数参数默认值可以不按照顺序,因为Swift中有函数标签,不会造成歧义。而在C++中,则必须要按照从右往左的顺序依次设置,两者对比如下:Xnip2021-11-10_23-46-29.pngXnip2021-11-10_23-44-45.png下面一张图是C++的调用,没有按照顺序设置默认值,直接报错缺失b的默认值,而Swift中则不会。但是,如果Swift函数参数都隐藏了函数标签,则无法识别是给哪个参数,只能按照从右往左的方向赋值,这样就会照成报错,如下图所示:Xnip2021-11-10_23-55-41.png在调用函数时,直接报错缺失第二个参数。因此,在Swift中,如果省略了函数参数标签,要保证所有的函数参数都有值,或者都可以得到赋值。

2.2.3 可变参数

与OC的NSLog参数一样,Swift函数也提供了可变参数,其定义方式是 参数名:类型...,可以参照系统的print函数定义:Xnip2021-11-11_09-39-17.pngprint函数的第一个参数即为可变参数,参数类型为Any,可以接受任意类型,输入时以,分割即可。

可变参数需要注意的一点是,在紧随其后的一个参数不能省略参数标签,如下图所示:Xnip2021-11-11_09-47-15.png

参数b也是一个Any类型,如果省略了参数标签,则在调用函数时就没有了标签区分,仅凭,编译器无法确定该将参数赋值给item还是b,因此会报错。

可变参数本质上是一个数组,可以在函数内部使用参数,查看其类型如下:Xnip2021-11-11_09-35-56.png可以看到 item 实际上是一个 Any 类型的数组。

2.2.4 inout修饰的参数

在OC和C中,我们可以通过指针传参,以达到在函数内部修改函数外部实参的值的目的。在Swift中,也提供了类似的方法,不过需要使用inout修饰一下参数,具体使用方式如下:

Xnip2021-11-11_11-19-31.png

number的值本来为10,经过inoutFunc函数调用,结果变为了20。那么 inout 是如何改变了外部实参的值的呢?有种说法是与OC一样,采用了指针传值的方式改变;还有说法是 inout 在底层是一个函数,将其修饰的函数内部的值通过这个函数重新赋值外部实参。针对这两种说法,我们可以通过汇编来验证下,本次使用的是真机调试,因此使用的是ARM下的汇编。

将上图中12行22行的断点打开,并打开XCode的汇编调试 Debug -> Debug Workflow -> Always show Disassembly。运行工程,首先进入22行的断点:Xnip2021-11-11_11-29-56.png图中红框处为 inoutFunc 函数的调用处,在上面28行可以发现一行代码 ldr x0, [sp, #0x10],这句代码的意思是,将[sp, #0x10]的值赋值给 x0 寄存器,[sp, #0x10]表示 sp+#0x10的地址,也就是说 x0 寄存器现在存储的是一个地址,通过 register read x0 命令可知改地址为 x0 = 0x000000016dbf9a80

单步调试进入 inoutFunc 函数,得到如下代码:

Xnip2021-11-11_11-36-36.png

执行到第4行,再次读取 x0 寄存器得到了相同的值x0 = 0x000000016dbf9a80,此时通过 x/4gx 读取内存地址0x000000016dbf9a80的值,得到结果如下:

Xnip2021-11-11_11-39-01.png

红框中的值 0x000000000000000a 换算成十进制正是 10。走到第6行汇编代码,将x0存储的地址所指向的内容存到x8寄存器,然后将值加10,就此完成对外部实参值的改变。在viewDidLoad中调用inoutFunc后并没有对于number的重新赋值,也证实了inout是通过地址传递改变外部实参的值。

使用inout需要注意两点:

  • 1、inout只能传入可以被多次赋值的,即不能传入常量和字面量
  • 2、inout不能修饰可变参数

三、函数重载

函数重载指的是函数名相同,但是参数名称不同 || 参数类型不同 || 参数个数不同 || 参数标签不同。需要注意的是,函数重载(overload)与函数重写(override)是两个概念,函数重写涉及到继承关系,而函数重载不涉及继承关系。另外,在OC中没有函数或方法的重载,只有重写。以下是几个函数重载的例子:

Xnip2021-11-11_14-28-28.png

可以看到,四个函数的方法名称相同,但是参数不同,实际上并不会报错,这就是方法重载。

不过方法重载也有需要注意的地方:

  • 方法重载与函数返回值无关,即函数名及参数完全相同的情况下,如果返回值不同,不构成函数重载,编译器会报错。

Xnip2021-11-11_14-35-59.png

如图所示,在调用方法时,编译器不知道该调用哪个函数,因此会报二义性错误。

  • 方法重载与默认参数值的情况

Xnip2021-11-11_14-38-50.png

从图中可以发现,由于第二个函数给参数c设置了默认值,在调用时形式上与第一个函数一样,不过编译器在此并不会报错,猜想是因为第二个函数还有一种test(a: , b: , c: )的调用形式。

四、inline内联函数

内联函数,其实是指开启了编译器内联优化后,编译器会将某些函数优化处理,该优化会将函数体抽离出来直接调用,而不会给这个函数再开辟栈空间。

func test() {
    print("test123")
}
test()
复制代码

如以上函数所示,调用test()时,需要为其开辟栈空间,而其内部只调用了一个print函数,所以在开启内联优化的情况下,可能会直接调用print函数。

开启内联优化的方式如下图:Xnip2021-11-11_15-15-38.pngDebug模式下默认不开启优化,Release模式下默认是开启的。为了测试内联优化的现象,这里先将Debug模式开启优化,之后在test()调用处打断点,再运行工程会发现,直接打印了test123,然后在test函数内部打断点,进入汇编如下:Xnip2021-11-11_14-58-28.png全局搜索发现没有test函数的调用,而是直接调用了print函数。

不过内联优化,也不是对所有函数都会进行优化,以下几点不会优化:

  • 函数体代码比较多
  • 函数存在递归调用
  • 函数包含动态派发,例如类与子类的多态调用

内联函数还有内联参数控制@inline(never) 和 @inline(__always)

  • 使用@inline(never)修饰,即使开启了编译器优化,也不会内联
  • 使用@inline(__always)修饰,开启编译器优化后,即使函数体代码很长也会内联,但是递归和动态派发依然不会优化

五、函数类型

每一个函数都可以符合一种函数类型,例如:

func test() {
    print("test123")
}

对应 () -> ()

func compute(a:Int = 8, b: Int = 9) -> Int {
    return a+b;
}

对应 (Int, Int) -> Int

复制代码

上述代码中,() -> () 和 (Int, Int) -> Int都表示一种函数类型。可以发现函数类型是不需要参数名的,直接标明参数类型即可。

函数类型也可以用作函数的参数和返回值,使用函数类型作为返回值的函数被称为高阶函数,例如:

// 函数类型作为参数
func testFunc(action:(Int) -> Int) {
    var result = action(2)
    print(result)
}

func action(a:Int) -> Int {
    return a
}
testFunc(action: action(a:))

// 函数类型作为返回值
func action(a:Int) -> Int {
    return a
}
func testFunc() -> (Int) -> Int {
    return action(a:)
}
let fu = testFunc()
print(fu(3))

复制代码

六、嵌套函数

Swift中,可以在函数内部定义函数,被称为嵌套函数,如下代码所示:

func forward(_ forward: Bool) -> (Int) -> Int {
    func next(_ input: Int) -> Int {
        input + 1
    }
    func previous(_ input: Int) -> Int {
        input - 1
    }

    return forward ? next : previous
}
复制代码

像上面这样在函数内部定义其他的函数,其目的是为了将函数内部的实现封装起来,外部只看到调用了 forward,而不需要知道其内部的实现逻辑,当然也不能直接调用内部的嵌套函数。

总结

相对于OC,Swift中主要增加了以下几点:

  • 参数标签
  • 函数重载
  • 嵌套函数

整体而言,个人感觉Swift的函数使用起来更加方便,参数标签使得代码可读性更强。以上即为本篇关于Swift函数的总结,如有不足之处,欢迎大家指正。

收起阅读 »

iOS 自定义通知声音

iOS
iOS 自定义通知声音场景在消息推送里面播放自定义生成的声音解决方案生成自定义声音文件后,必须要写入到【/Library/Sounds/】才能进行播放///往声音目录/Library/Sounds/写入音频文件 - (void)writeMusicDataWi...
继续阅读 »

iOS 自定义通知声音

场景

在消息推送里面播放自定义生成的声音

解决方案

  1. 生成自定义声音文件后,必须要写入到【/Library/Sounds/】才能进行播放
///往声音目录/Library/Sounds/写入音频文件
- (void)writeMusicDataWithUrl:(NSString*)filePath
callback:(void(^)(BOOL success,NSString * fileName))blockCallback{
NSString *bundlePath = filePath;
NSString *libPath = [NSHomeDirectory() stringByAppendingString:@"/Library/Sounds/"];

NSFileManager *manager = [NSFileManager defaultManager];
if (![manager fileExistsAtPath:libPath]) {
NSError *error;
[manager createDirectoryAtPath:libPath withIntermediateDirectories:YES attributes:nil error:&error];
}

NSData *data = [NSData dataWithContentsOfFile:bundlePath];

BOOL flag = [data writeToFile:[libPath stringByAppendingString:[filePath lastPathComponent]] atomically:YES];
if (flag) {
NSLog(@"文件写成功");
if (blockCallback) {
blockCallback(YES,[filePath lastPathComponent]);
}
}else{
NSLog(@"文件写失败");
if (blockCallback) {
blockCallback(NO,nil);
}
}
}

  1. 在【UNMutableNotificationContent】的【sound】参数中写入文件名
///!!!!:推送语音播报
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];

UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; //标题
content.sound = [UNNotificationSound soundNamed:fileName];

content.body = @"语音播报";// 本地推送一定要有内容,即body不能为空。

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
if (@available(iOS 15.0, *)) {
content.interruptionLevel = UNNotificationInterruptionLevelTimeSensitive;//会使手机亮屏且会播放声音;可能会在免打扰模式(焦点模式)下展示
// @"{\"aps\":{\"interruption-level\":\"time-sensitive\"}}";
// @"{\"aps\":{\"interruption-level\":\"active\"}}";
content.body = @"语音播报";// 本地推送一定要有内容,即body不能为空。
}
#endif
// repeats,是否重复,如果重复的话时间必须大于60s,要不会报错
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.01 repeats:NO];
/* */
//添加通知的标识符,可以用于移除,更新等搡作
NSString * identifier = [[NSUUID UUID] UUIDString];
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
[center addNotificationRequest:request withCompletionHandler:^(NSError *_Nullable error) {
completed();
}];

参考: http://www.jianshu.com/p/a6eba8cfb… blog.csdn.net/LANGZI77585…

https://juejin.cn/post/7029245981149364255

收起阅读 »

iOS内购详解

iOS
iOS内购详解概述iOS内购是指苹果 App Store 的应用内购买,即In-App Purchase,简称IAP(以下本文关于内购都简称为IAP),是苹果为 App 内购买虚拟商品或服务提供的一套交易系统。为什么我们需要掌握IAP这套流程呢,因为App S...
继续阅读 »

iOS内购详解

概述

iOS内购是指苹果 App Store 的应用内购买,即In-App Purchase,简称IAP(以下本文关于内购都简称为IAP),是苹果为 App 内购买虚拟商品或服务提供的一套交易系统。为什么我们需要掌握IAP这套流程呢,因为App Store审核指南规定:

如果您想要在 app 内解锁特性或功能 (解锁方式有:订阅、游戏内货币、游戏关卡、优质内容的访问
限或解锁完整版等),则必须使用 App 内购买项目。App 不得使用自身机制来解锁内容或功能,
如许可证密钥、增强现实标记、二维码等。App 及其元数据不得包含按钮、外部链接或其他行动号
召用语,以指引用户使用非 App 内购买项目机制进行购买。
复制代码

这段话的大概意思就是APP内的虚拟商品或服务,必须使用 IAP 进行购买支付,不允许使用支付宝、微信支付等其它第三方支付方式(包括Apple Pay),也不允许以任何方式(包括跳出App、提示文案等)引导用户通过应用外部渠道购买。如果违反此规定,apple审核人员不会让你的APP上架!!!

内购前准备

APP内集成IAP代码之前需要先去开发账号的ITunes Connect进行以下三步操作:

1,后台填写银行账户信息

2,配置商品信息,包括产品ID,产品价格等

3,配置用于测试IAP支付功能的沙箱账户。

填写银行账户信息一般交由产品管理人员负责,开发者不需要关注,开发者需要关注的是第二步和第三步。

配置内购商品

IAP 是一套商品交易系统,而非简单的支付系统,每一个购买项目都需要在开发者后台的Itunes Connect后台为 App 创建一个对应的商品,提交给苹果审核通过后,购买项目才会生效。内购商品有四种类型:

  • 消耗型项目:只可使用一次的产品,使用之后即失效,必须再次购买,如:游戏币、一次性虚拟道具等
  • 非消耗型项目:只需购买一次,不会过期或随着使用而减少的产品。如:电子书
  • 自动续期订阅:允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期,如:Apple Music这类按月订阅的商品(有些鸡贼的开发者以此收割对IAP商品不熟悉的用户,参考App Store“流氓”软件)
  • 非续期订阅:允许用户购买有时限性服务的产品,此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期

配置商品信息需要注意产品ID和产品价格

1,产品 ID 具有唯一性,建议使用项目的 Bundle Identidier 作为前缀后面拼接自定义的唯一的商品名或者 ID(字母、数字),这里有个坑:一旦新建一个内购商品,它的产品ID将永远被占用,即使该商品已经被删除,已创建的内购商品除了产品 ID 之外的所有信息都可以修改,如果删除了一个内购商品,将无法再创建一个相同产品 ID 的商品,也意味着该产品 ID 永久失效!!!

2,在创建IAP项目的时候,需要设定价格,产品价格只能从苹果提供的价格等级去选择,这个价格等级是固定的,同一价格等级会对应各个国家的货币,比如等级1对应1美元、6元人民币,等级2对应2美元、12元人民币……最高等级87对应999.99美元、6498元人民币。另外可能是为了照顾某些货币区的开发者和用户,还有一些特殊的等级,比如备用等级A对应1美元、1元人民币,备用等级B对应1美元、3元人民币这样。除此之外,IAP项目不能定一个9.9元人民币这样不符合任何等级的价格。详细价格等级表可以看苹果的官方价格等级文档

苹果的价格等级表通常是不会调整的,但也不排除在某些货币汇率发生巨大变化的情况下,对该货币的定价进行调整,调整前苹果会发邮件通知开发者。

3,商品分成

App Store上的付费App和App内购,苹果与开发者默认是3/7分成。但实际上,在某些地区苹果与开发者分成之前需要先扣除交易税,开发者的实际分成不一定是70%。从2015年10月开始,苹果对中国地区的App Store购买扣除了2%的交易税,对于中国区帐号购买的IAP,开发者的实际分成在68%~69%之间。而且中国以外不同地区的交易税标准也存在差异,如苹果的官方价格等级文档

,如果需要严格计算实际收入,可能需要把这个部分也考虑进来。

针对不同地区的内购,内购价格和对应的开发者实际收入在苹果的价格等级表中有详细列举。

另外,根据苹果在2016年6月的新规则,针对Auto-Renewable Subscription类型的IAP,如果用户购买的订阅时间超过1年,那么从第二年开始,开发者可以获得85%的分成。详情可查看苹果的订阅产品价格说明

沙箱账户

新的内购产品上线之前,测试人员一般需要对内购产品进行测试,但是内购涉及到钱,所以苹果为内购测试提供了 沙箱测试账号 的功能,Apple Pay 推出之后 沙箱测试账号`也可以用于 Apple Pay 支付的测试,沙箱测试账号 简单理解就是:只能用于内购和 Apple Pay 测试功能的 Apple ID,它并不是真实的 Apple ID。

填写沙箱测试账号信息需要注意以下几点:

  • 电子邮件不能是别人已经注册过 AppleID 的邮箱
  • 电子邮箱可以不是真实的邮箱,但是必须符合邮箱格式
  • App Store 地区的选择,测试的时候弹出的提示框以及结算的价格会按照沙箱账号选择的地区来,建议测试的时候新建几个不同地区的账号进行测试!!!

沙箱账号测试的使用:

  • 首先沙箱测试账号必须在真机环境下进行测试,并且是 adhoc 证书或者 develop 证书签名的安装包,沙盒账号不支持直接从 App Store 下载的安装包
  • 去真机的 App Store 退出真实的 Apple ID 账号,退出之后并不需要在App Store 里面登录沙箱测试账号
  • 然后去 App 里面测试购买商品,会弹出登录框,选择 使用现有的 Apple ID,然后登录沙箱测试账号,登录成功之后会弹出购买提示框,点击 购买,然后会弹出提示框完成购买。

内购流程

IAP的支付流程分为客户端和服务端,客户端的工作如下:

  • 获取内购产品列表(从App内读取或从自己服务器读取),向用户展示内购列表
  • 用户选择某个内购产品后,先请求可用的内购产品的本地化信息列表,此次调用Apple的StoreKit库的代码
  • 得到内购产品的本地化信息后,根据用户选择的内购产品的ID得到内购产品
  • 根据内购产品发起IAP购买请求,收到购买完成的回调
  • 购买流程结束后, 向服务器发起验证凭证以及支付结果的请求
  • 自己的服务器将支付结果信息返回给前端并发放虚拟产品

前端支付流程图如下:

------------------------------ IAPManager.h -----------------------------
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef enum {
IAPPurchSuccess = 0, // 购买成功
IAPPurchFailed = 1, // 购买失败
IAPPurchCancel = 2, // 取消购买
IAPPurchVerFailed = 3, // 订单校验失败
IAPPurchVerSuccess = 4, // 订单校验成功
IAPPurchNotArrow = 5, // 不允许内购
}IAPPurchType;

typedef void (^IAPCompletionHandle)(IAPPurchType type,NSData *data);

@interface IAPManager : NSObject
+ (instancetype)shareIAPManager;
- (void)startPurchaseWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle;
@end

NS_ASSUME_NONNULL_END



------------------------------ IAPManager.m -----------------------------

#import "IAPManager.h"
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>

@interface IAPManager()<SKPaymentTransactionObserver,SKProductsRequestDelegate>{
NSString *_currentPurchasedID;
IAPCompletionHandle _iAPCompletionHandle;
}
@end

@implementation IAPManager

+ (instancetype)shareIAPManager{

static IAPManager *iAPManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
iAPManager = [[IAPManager alloc] init];
});
return iAPManager;
}
- (instancetype)init{
self = [super init];
if (self) {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}

- (void)dealloc{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}


- (void)startPurchaseWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle{
if (purchID) {
if ([SKPaymentQueue canMakePayments]) {
_currentPurchasedID = purchID;
_iAPCompletionHandle = handle;

//从App Store中检索关于指定产品列表的本地化信息
NSSet *nsset = [NSSet setWithArray:@[purchID]];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];
}else{
[self handleActionWithType:IAPPurchNotArrow data:nil];
}
}
}

- (void)handleActionWithType:(IAPPurchType)type data:(NSData *)data{
#if DEBUG
switch (type) {
case IAPPurchSuccess:
NSLog(@"购买成功");
break;
case IAPPurchFailed:
NSLog(@"购买失败");
break;
case IAPPurchCancel:
NSLog(@"用户取消购买");
break;
case IAPPurchVerFailed:
NSLog(@"订单校验失败");
break;
case IAPPurchVerSuccess:
NSLog(@"订单校验成功");
break;
case IAPPurchNotArrow:
NSLog(@"不允许程序内付费");
break;
default:
break;
}
#endif
if(_iAPCompletionHandle){
_iAPCompletionHandle(type,data);
}
}

- (void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction{
//交易验证
NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:recepitURL];

if(!receipt){
// 交易凭证为空验证失败
[self handleActionWithType:IAPPurchVerFailed data:nil];
return;
}
// 购买成功将交易凭证发送给服务端进行再次校验
[self handleActionWithType:IAPPurchSuccess data:receipt];

NSError *error;
NSDictionary *requestContents = @{
@"receipt-data": [receipt base64EncodedStringWithOptions:0]
};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];

if (!requestData) { // 交易凭证为空验证失败
[self handleActionWithType:IAPPurchVerFailed data:nil];
return;
}

NSString *serverString = @"https:xxxx";
NSURL *storeURL = [NSURL URLWithString:serverString];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];

[[NSURLSession sharedSession] dataTaskWithRequest:storeRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
// 无法连接服务器,购买校验失败
[self handleActionWithType:IAPPurchVerFailed data:nil];
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) {
// 服务器校验数据返回为空校验失败
[self handleActionWithType:IAPPurchVerFailed data:nil];
}

NSString *status = [NSString stringWithFormat:@"%@",jsonResponse[@"status"]];
if(status && [status isEqualToString:@"0"]){
[self handleActionWithType:IAPPurchVerSuccess data:nil];
} else {
[self handleActionWithType:IAPPurchVerFailed data:nil];
}
#if DEBUG
NSLog(@"----验证结果 %@",jsonResponse);
#endif
}
}];

// 验证成功与否都注销交易,否则会出现虚假凭证信息一直验证不通过,每次进程序都得输入苹果账号
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
NSArray *product = response.products;
if([product count] <= 0){
#if DEBUG
NSLog(@"--------------没有商品------------------");
#endif
return;
}

SKProduct *p = nil;
for(SKProduct *pro in product){
if([pro.productIdentifier isEqualToString:_currentPurchasedID]){
p = pro;
break;
}
}

#if DEBUG
NSLog(@"productID:%@", response.invalidProductIdentifiers);
NSLog(@"产品付费数量:%lu",(unsigned long)[product count]);
NSLog(@"产品描述:%@",[p description]);
NSLog(@"产品标题%@",[p localizedTitle]);
NSLog(@"产品本地化描述%@",[p localizedDescription]);
NSLog(@"产品价格:%@",[p price]);
NSLog(@"产品productIdentifier:%@",[p productIdentifier]);
#endif

SKPayment *payment = [SKPayment paymentWithProduct:p];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}

//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
#if DEBUG
NSLog(@"------------------从App Store中检索关于指定产品列表的本地化信息错误-----------------:%@", error);
#endif
}

- (void)requestDidFinish:(SKRequest *)request{
#if DEBUG
NSLog(@"------------requestDidFinish-----------------");
#endif
}

#pragma mark - SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
for (SKPaymentTransaction *tran in transactions) {
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
[self verifyPurchaseWithPaymentTransaction:tran];
break;
case SKPaymentTransactionStatePurchasing:
#if DEBUG
NSLog(@"商品添加进列表");
#endif
break;
case SKPaymentTransactionStateRestored:
#if DEBUG
NSLog(@"已经购买过商品");
#endif
// 消耗型不支持恢复购买
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:tran];
break;
default:
break;
}
}
}

// 交易失败
- (void)failedTransaction:(SKPaymentTransaction *)transaction{
if (transaction.error.code != SKErrorPaymentCancelled) {
[self handleActionWithType:IAPPurchFailed data:nil];
}else{
[self handleActionWithType:IAPPurchCancel data:nil];
}

[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
@end


/* 调用支付方法
- (void)purchaseWithProductID:(NSString *)productID{

[[IAPManager shareIAPManager] startPurchaseWithID:productID completeHandle:^(IAPPurchType type,NSData *data) {

}];
}
*/


服务端的工作:

  • 接收iOS端发过来的购买凭证,判断凭证是否已经存在或验证过,然后存储该凭证。将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。

恢复购买

内购有4种:消耗型项目,非消耗型,自动续期订阅,非续期订阅。 其中”非消耗型“和”自动续期订阅“需要提供恢复购买的功能,例如创建一个恢复按钮,不然审核很可能会被拒绝。

//调起苹果内购恢复接口
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];

“消耗型项目”和“非续期订阅”苹果不会提供恢复的接口,不要调用上述方法去恢复,否则有可能被拒!!!

“非续期订阅”也是跨设备同步的,所以原则上来说也需要提供恢复购买的功能,但需要依靠app自建的账户体系恢复,不能用上述苹果提供的接口。

内购掉单

掉单是用户付款买商品,钱扣了,商品却没到账。掉单一旦发生,用户通常会很生气地来找客服。然后客服只能找开发人员把商品给用户手动加上。显然,伤害用户的体验,特别是伤害付费用户的体验,是一件相当糟糕的事情。

掉单是如何产生的呢?这需要从IAP支付的技术流程说起。

IAP的支付流程:

1,发起支付

2,扣费成功

3,得到receipt(支付凭据)

4,去后台验证凭据获取商品交易状态

5,返回数据,验证成功前端刷新数据

  • 漏单情况一:

    2到3环节出问题属于苹果的问题,目前没做处理。

  • 漏单情况二:

3到4的时候出问题,比如断网。此时前端会把支付凭据持久化存储下来,如果期间用户卸载APP此单在前端就真漏了,如果没有协助,下次重新打开app进入购买页会先判断有无未成功的支付,有就提示用户,用户选择找回,重走4,5流程。这一步看产品需求怎么做,可以让用户自主选择是否恢复未成功的支付也可以前端默默恢复就行。

  • 漏单情况三:

4到5的时候出问题。此时后台其实已经成功,只是前端没获取到数据,当漏单处理,下次进入的时候先刷新数据即可。

内购注意事项

  • 交易凭据receipt判重

一般来说验证支付凭据(receipt)是否有效放后台去做,如果后台不做判重,同一个凭据就可以无数次验证通过,因为苹果也不判重,这就会导致前端可以凭此取到的一个支付凭据可以去后台无数次做校验!!!!,后台就会给前端发放无数次商品,但是用户只支付了一次钱,所以安全的做法是后台把验证通过的支付凭据做个记录,每次来新的凭据先判断是否已经使用过,防止多次发放商品。

参考

iOS 内购(In-App Purchase)总结

https://juejin.cn/post/7029252038252822564

收起阅读 »

(转载)AR/VR中的元宇宙Metaverse以及趋势分析

最近【三次方】的知识社区有很多小伙伴讨论关于Metaverse元宇宙,这个精选几个观点为大家分享出来:谁将拥有元宇宙?腾讯的元宇宙生态图谱AR中的Metaverse元宇宙的9大趋势Roblox、Epic、Genies 和 Zepeto 使用术语 metaver...
继续阅读 »

最近【三次方】的知识社区有很多小伙伴讨论关于Metaverse元宇宙,这个精选几个观点为大家分享出来:

  • 谁将拥有元宇宙?
  • 腾讯的元宇宙生态图谱
  • AR中的Metaverse
  • 元宇宙的9大趋势

Roblox、Epic、Genies 和 Zepeto 使用术语 metaverse,而 Facebook 使用 Live Maps,而 Magic Leap 更喜欢 Magicverse。Kevin Kelley 在 Wired 中将其称为 Mirrorworld,而 Nvidia 使用术语 Omniverse。其他人更喜欢术语 AR 云、空间互联网或空间网络。

谁将拥有元宇宙?

Metaverse与其说是一个完整的现实,不如说是一个想法,它指的是我们共享的不断扩展的虚拟性。作为生活在信息时代的人类,我们在世界中的存在越来越多地被中介化;通过能够将几乎所有可以想象的人类活动记录和传输为数字化信息,我们在虚拟的抽象领域中工作。元宇宙是这种模式的最终实现;不一定通过计算机和智能手机,而是通过强大的沉浸式技术,如虚拟现实和增强现实。当这些技术变得可访问时,这可能比我们想象的更早发生,这样一个 Metaverse 的价值将难以想象。

原因很简单:虚拟比现实更实惠、更灵活。例如,虚拟办公室和屏幕几乎不需要任何费用,因此不需要长时间通勤上班。然而,除了开放新的解决方案;Metaverse 的价值首先将与已经在虚拟环境中完成的一切相关联,例如社交媒体、银行、信息、游戏和娱乐。将这些任务综合、转化为我们包容的现实的能力完全改变了虚拟性的本质。我们在万维网上已经有效地做的一切,我们都可以在“现实的格式”中体验,而不是我们智能手机上的抽象符号。至少可以说,这具有巨大的商业潜力,因为我们人类将深入参与该技术;简而言之,元宇宙的潜力是巨大的,即使尚未实现,巨头们也在确保它会实现。

元宇宙之战已经开始,,我们可能会受到影响——无论是通过广告还是政治信念。赋予私营公司如此大的权力所面临的挑战是我们的利益与私营参与者的利益之间存在潜在差异。我们今天已经在社交媒体上看到了这一点,随着沉浸感和数据收集的增加,问题只会越来越多。

腾讯的元宇宙生态图谱

AR中的Metaverse

Metaverse出现在科幻小说家Neal Stephenson的作品“ Snow Crash”中。它指的是互联网上的虚拟世界。因此,它现在被用作“ Internet上的第二个现实世界”的通用术语,但是没有正确的定义。以下七个元素始终包含在metaverse元素中:

1.永无止境

2.始终保持同步

3.任何人都可以参与,不受限制地访问

4.拥有自己的经济系统

5.提供离线和在线,开放和封闭的体验

6.数据数字化资产内容的互操作性达到前所未有的水平等等

7.各种人创造的内容和经验

元宇宙的9大趋势

1.虚拟化的盛行

人们越来越认为虚拟世界和物理世界一样真实。

在物理世界中,信任是人际关系和机构的运作方式。它是企业在法律体系中蓬勃发展、货币市场持续运作的基础,也是一种衡量人与人之间连接的方式。信任使上述系统得以发展与延伸。

随着信任在“虚拟”领域中的增加,也即拥有在线朋友、虚拟物品、加密资产、智能合同和实时在线体验,元宇宙以及支撑它的行业领域的可延展性也随之增加。

但大势之下,总有逆流。人们对虚拟世界的重视同时也会让那些试图利用它的人蠢蠢欲动。

网络犯罪便是很多人都熟悉的一个例子,比如通过网络钓鱼来窃取你的账户信息、实施各种网络诈骗、利用勒索软件攻击和传播病毒软件等。

网络欺凌与虐待、游戏中的作弊行为和关系中的欺骗行为的危害性都将增加,因为人们相信虚拟关系和虚拟资产是真实的。随着其可能带来的价值的增加,上述行为将会变本加厉,而旨在打击犯罪和虐待的公司的成本则会变得更高。

单靠产品并不能解决这些问题,还需要教育、培训、虚拟素养、社区以及家长的支持。

2.低代码平台

低代码和零代码应用程序平台(LCAP)提供了更高级的程序工具(如可视化开发脚手架和拖拽工具),以取代流程、逻辑和应用程序的人工编码。

这一趋势最明显的好处在于非程序员得以做一些程序员以前做的工作。但是,低代码平台的影响远不止于此,公司采用这些平台也不仅仅只是出于上述考虑。

LCAP的神奇之处在于可视化层所发生的大量的自动化:工作流、部署、安全监测、扩展以及各种数据端点的集成。一般而言,达到这种复杂度和规模已经相当于完成了互联网应用程序很大一部分的开发工作。

这不仅改变了谁来做这份工作,同时也使得创建应用程序所需的工作量大量减少。

信息研究和分析公司Gartner预测,到2023年将会有超过50%的大型企业使用LCAP来运营至少部分的企业基础设施。

类似地,这些开发人员中有许多都在向无服务器体系结构(一个有点令人困惑的术语,因为通常是有服务器的,只是不需要自己部署、管理或编码)迈进。

在企业的另一端,人们拥有越来越多的创建工具,可以轻松创建元宇宙内容、编写复杂行为和参与商业活动。

人们总认为产品要么服务大企业,要么服务小企业,但事实并非如此。虽然服务于大企业的技术通常很难向下扩展到个人身上,但也有很多例子表明,个人能够把控得住,而这也成为了企业最简单的选择。上述这些可以说是囊括了Adobe公司做过的所有事情。最近,像Shopify这样的无代码/低代码平台已经能够支持从小型企业到一些世界上最大的品牌(如孩之宝、百威等)。

更广泛的创建者群体将构建越来越多的元宇宙,而这些元宇宙也将得到更深层次的插件应用程序以及逻辑目录的支持。

3.机器智能系统

机器正在做更多以前人类做的工作,包括有时被称为深度学习、机器学习和人工智能的领域。

我们生活在一个广告信息、商品营销和在线约会均被学习算法调整过的世界里。但这种自然语言处理和图像识别尚且处于早期阶段。在物理世界中,自动驾驶等应用的实现指日可待。

在元宇宙中,机器智能与你在这看到的所有其他趋势一样。

它将影响创造力,因为计算机在创作过程中已成为合作者——不妨看看AI Dungeon如何生成故事,或者Promethean AI如何建立虚拟景观——想象一下未来十年它将走多远。

人工智能将被用于设计启动元宇宙的微芯片,并生成代码来辅助程序员。

机器将会翻译手势动作、预测我们的目光所在、识别情绪,甚至识别我们的神经冲动。

机器智能将内嵌到我们的无代码和低代码应用程序平台中,它们将作为服务架构以及设计顾问的一部分一起运行。

由我们的偏好和兴趣驱动的代理会在我们有需要时把我们想要的信息呈现出来。与此同时,越来越多的虚拟元素将遍布我们所访问的世界。

4.控制论的兴起

控制论悄然而至,现在它的应用并未形成规模,也不够发达和惊人,但未来一定会。

控制论是指人类感觉系统、运动系统与计算机的结合。目前的用例是利用电子游戏的输入/输出设备、可穿戴设备、手机加速度传感器和VR头显设备等来实现的。

微型化和高速网络已经将固定工作站中的设备转变成我们口袋里的移动超级电脑。这些电脑已经越来越靠近我们的身体了。

我们正在由仅从外部角度看待计算机的现在走向一个我们将占据虚拟空间、生活在一个被计算机包围的未来。

“智能手机”已经感觉像是一个古老的术语,因为这些都不是手机——它们是高度便携的电脑,只不过恰好预先安装了手机app。我们已经可以通过诸如Oculus等品牌的VR头显来占据虚拟空间,这些VR设备还会对我们眼睛、头部的位置和手势有所响应。当这些变成智能眼镜时,我们就能够将这些体验带到周围更广阔的空间。在未来,我们甚至可能会有功能性智能隐形眼镜。

光场技术甚至可以让我们将光子以其相应的场深投射到视网膜上,让你的眼睛聚焦于虚拟场景的不同部分,从而获得真正的全息体验。

这些设备将越来越多地识别我们的语音指令、手势和生物特征信息。神经接口甚至可以让我们的设备理解我们的意图——甚至比我们自己知道的还要快。

那结果呢?元宇宙将不仅仅只是我们的一个去处,而且会成为我们的“无处不在”。

可穿戴技术和移动技术的融合不仅是一个技术变革,也是一种社会变革。它将改变我们的家庭、公共交通、社区和工作场所的组织结构。它将改变你与人见面、点餐、发现世界和合作项目的方式。

5.开放系统所面临的挑战

人们建立互联网的最初是想要一个高度分布式、去中心化、协同的计算机和应用程序网络。

而如今的互联网由几个非常大的平台充当看门人和收费站。

不过,技术和开放的标准正在形成,这可能会使元宇宙的未来更加民主化。

WebAssembly (Wasm)承诺为开放的网络提供快速、安全、沙盒的二进制应用程序。WebGL和WebXR将提供应用程序商店之外的计算机图形和沉浸式体验。像Unity数据导向型技术栈(Unity DOTS)这样的平台正在利用这些平台提供能达到元宇宙所要求水平的高效的二进制压缩文件(特别是Unity的Project Tiny)。

开放系统也是一种社会现象,因为它们允许软件工程项目之间的广泛协作。里德定律预测了Slack或WhatsApp等应用程序的指数价值,它也可以应用于开源运动——这本质上是一个软件开发人员无需许可的社交网络

像Wasm这样的开源和开放平台可以最大限度地增加潜在合作者的数量,创造出比所有需要许可的平台总和更多的价值。像Linux和PC这样无需许可的平台也应该在未来得到蓬勃发展。

同样地,人们也可以利用零知识证明和去中心化的数字身份系统等技术重新获得对自己数据的主权。这可能会鼓励消费者将更多的个人数据放心地提交到互联网应用程序中——仅仅因为他们不需要信任任何人。

如果我们可以开放应用程序和数据,就有可能实现网络效应的指数级增长。

6.区块链的采用

区块链作为一种分布式账本技术,可以对资产和数据起到开源和开放互联网对软件和应用程序所起到的作用。

区块链允许无信任的数据交换;去中心化的权威、历史和来源的记录,可证明的资产稀缺。当它们分散时,区块链支持无权限者参与,或通过分散的形式进行治理。

可编程性是区块链的一个关键特点。虽然并非所有的区块链都能编程,但它是以太坊和其他“智能合同”链的一个关键方面。

为什么这点这么重要?还是因为网络效应。能够参与网络的节点越多,网络的价值就越高,再者因为群体可以策划一些特定的活动(比如游戏、金融积木等),所以根据里德定律,网络的价值也会进一步增加。

这些价值贡献都是指数级别的。更多个人、更多应用程序和更多组件的集成,就意味着更智能的合同和更分散的应用程序。

区块链被认为是“无信任的”,因为你不需要信任任何一个权威机构;信任自存在于区块链之中。

所有这些无信任的应用程序、合同和组件的总体长尾分布赋予了区块链的社会可伸缩性。

网络效应已经为链上数据源(预言机)铺平了道路,其可充当智能合同的条件;而这又导致了去中心化的贷款、金融和资产交换。区块链计算的出现可能会取代云计算的某些方面;NFT(非同质化资产)的崛起可能成为新兴一代游戏中虚拟商品、皮肤定制和元宇宙体验的基础。

当你在开源的网络上开放资产、数据和可编程的合同时,无限的可能也由此书写。

7.围墙花园生态系统

围墙花园——我喜欢使用这个词,因为花园可以很美观,同时又井然有序——从所有其他影响元宇宙的大趋势中受益。

并不是每个应用程序或每个世界都会开放。有时,许可、集成、管理和控制是平台或应用程序的理想特性。如果没有这些特性的集成,Roblox就永远不会流行起来。

令人感到讽刺的是,围墙花园也受益于那些挑战它们的开放系统。他们使用了和其他人一样的开源和区块链,而且许多客户在其内部可能会感到更安全。

围墙花园本身并不成问题,围墙花园太少才是2021年生态系统的问题所在。你理应能轻松地创建自己的围墙花园,并邀请其他创建者根据你所定义的规则参与、添加、修改和互连。

随着围墙花园越来越多,问题也随之浮现,即如何让每个花园被发现。有些诸如Roblox(一个的“游戏版YouTube”)的分级探索系统,就是由搜索和人气带动起来的。而由于人们喜欢管理,开发人员热衷于链接更多的访客,上述这种情况还会继续下去。但是,便携的化身、便携的社交网络和互操作的方法已初见端倪——这可能会通过开放平台将各种围墙花园连接起来,同时引发探索发现和管理的新契机。

在未来,我们可能会有一个类似超媒体的结构,门户网站将不同的世界和不同的体验连接起来——就好比虚拟世界中的网页超链接,或者是元宇宙的超级门户网站。

8.分布式网络速度的加快

5G网络把移动网络的速度、并发连接数和延迟率提升与改善了好几个数量级。而5G也并非道路的尽头,因为6G将把这些指标再提高10-100倍。我们会在十年内看到10Gbps的网速与下降到1ms的延迟。

加快网速对于支持元宇宙是很必要的,但也的确是当网络中的所有参与者能够共享实时数据时所引发的网络效应为我们带来了一些最有趣的应用程序。

因为局域网络层已不再是瓶颈,所以重点将转向把更多的计算力直接移至网络的“远”端。有时是在当地的手机发射塔,有时可能就在你的家中,那里的信息将被预处理并出现到你的控制设备上。

许多驱动应用程序的AI将在边缘运行,因为要以远程/集中的方式处理所有信息还是太慢了。未来需要本地计算设备和数据源的快速互操作。这有时会意味着在边缘对元宇宙中的应用进行预测,因为元宇宙中对行为和物理的预测非常准确。

9.模拟现实情况

多年来,几乎每款具有3D图像的游戏都是通过一个叫shader编程的软件系统来生成实时图像的。光线追踪技术利用光学原理模拟光子在不同材料间反弹与穿梭从而形成图像的过程,可以创造出更美丽和真实的图像——这也是为什么它被用于电影等预渲染内容的原因——不过其对处理能力的要求也更苛刻。

但实时的光线追踪离我们并不遥远了。

这只是我们如何在机器内模拟现实的一个例子。比如NVIDIA Omniverse平台的用例之一就是模拟流体动力学:先想象一下它能准确地呈现河流的样貌,或者模拟暖通空调系统(可用于鉴定一栋建筑在呼吸道疾病大流行期间的适应能力)。然后想象一下所有这些模拟物和人工智能引擎都被嵌入到一个互操作的架构中,并且该框架允许逻辑和预测去模拟出一个拥有虚拟机、物品、环境和人的世界。

数据也将来自物理世界中呈指数级增长的数据。这包括地理空间数据和交通数据、用于报告属性的物理对象的数字孪生、向智能合同报告财务数据的预言机(oracles),以及关于人和运行过程的实时数据。

我们将拥有的不只是一个物联网——而会是一个“万物互联”的网络——与预测分析、人工智能和实时可视化相结合。

这些创新都将使元宇宙能够超越并预测现实世界,同时也为基于物理学的下一代游戏提供动力,让它们变得比迄今为止的任何游戏都更美丽、更沉浸。

收起阅读 »

(转载)5G、元宇宙和被重新定义的社交出海

【融云全球互联网通信云】[疫情突发,人们的社交生活被重新定义。熬过“孤独”的后疫情时代,海外市场线上社交需求不断增长,社交玩法与场景也逐渐多元。](WICC 2021 全球互联网通信云大会-广州站 预约报名-融云活动-活动行 (huodongxing.com)...
继续阅读 »

【融云全球互联网通信云】[疫情突发,人们的社交生活被重新定义。熬过“孤独”的后疫情时代,海外市场线上社交需求不断增长,社交玩法与场景也逐渐多元。](WICC 2021 全球互联网通信云大会-广州站 预约报名-融云活动-活动行 (huodongxing.com))

作为安全、可靠的互联网通信云服务商,全球通信云服务商中的佼佼者,“融云”是如何看待风云莫测的海外市场环境变化的?又是如何应对后疫情时代的通信需求?

近期,在出海赛道行业媒体【扬帆出海】的专访中,融云联合创始人兼 CTO 杨攀分享了“新航海时代”融云的思考与实践。

以下为访谈实录。

图片

疫情下的出海趋势

扬帆出海记 者: 能否请您介绍一下,疫情爆发前后,全球通信产业、社交业务发生了怎样的变化?

融云杨攀: 海内外市场在疫情影响下,发展方向不甚相同。

国内市场受疫情倒逼,明显的趋势是 “云经济” 崛起,各种线下服务都搬到了线上去做,借机进行了数字化转型,发展势头迅猛。

而海外市场,在漫长的疫情周期里,“居家抗疫”成为主流,满足人们日常精神需求和情感连接的在线社交娱乐类产品,逐渐成为社会的刚需。而这恰巧也是我国目前出海的重要品类,借此迎来了新一轮的爆发。

自 2016 年移动互联网浪潮崛起,中国企业尤其活跃。在我国互联网企业出海初始阶段,出海应用多是与地方文化关系不大的工具类 APP,后来由于出海赛道变宽,社交和游戏品类紧随其后,逐渐占领市场份额。

早期,还只有以直播为主的单一类型社交应用出海;时过境迁,如今国内各种各样新奇的玩法,如 1v1、语聊房等,都已经成为出海市场中非常火热的产品类型。

“新大陆”在哪里

*扬帆出海记者: *海外不同区域市场之间,是否也呈现差异化的发展?我们能看到哪些市场机会呢?

融云杨攀: 经过一定时间的摸索验证,以及出海策略的调整优化,国内绝大多数移动互联网产品,都已经把目光从美欧发达市场,转移到了那些较中国互联网发展稍落后的国家和地区,通过先进的产品和领先的商业逻辑领跑整个海外市场。

从地区差异角度看:从东南亚到印度、到中东、非洲、南美,在时间线上依次落后几年。 目前我们能看到的主要趋势是,刨除受国际关系影响的印度市场,东南亚、中东是中国应用出海最热的两个地区,其次就是非洲和南美。

东南亚因为距离中国很近,成为中国互联网出海品类最全、业务覆盖最广的区域,国内各种各样的出海品类在东南亚都能看得到,包括社交游戏、电商购物等。

中东市场,虽然用户基础规模较大,但用户付费能力分布不均匀,除少数客户付费能力强,大部分用户的付费能力都比较弱。但如果能够利用这一市场特点,有针对性地设计出一些产品,让付费意愿高的用户多付费,付费能力差一些的客户主要负责促进平台活跃,或许也会是出海社交  APP 很好的机会。比如融云客户 Beeto 就在中东市场取得了很好的成绩( 《WICC 话题剧透|Beeto 陈昊:中东爆款社交平台是怎样炼成的》 )。

非洲受支付和网络基建的掣肘,通常产品还停留在小游戏和较简单的社交、新闻类 APP,而较大的直播社交平台和线上游戏还尚未普及。这也是未来增长的机会。

南美市场上,类似巴西这类整体基础设施条件比较不错的地域,近年来有很多国内公司去开拓自己的业务,都收获了比较可观的收益。

应对全球市场的复杂性

*扬帆出海记者: *相比于国内来说,国外的通信环境更复杂。融云是如何应对海外通信市场这种复杂性的?

融云杨攀: 置身全球,并非所有国家和地区都与中国一样,有着非常好的互联网基础设施,反而很多国家地区的基建相当有限。作为通信云服务商,融云能做的就是通过技术手段,解决“最后一公里”的质量问题,确保全球范围内的通信低延迟。

比如,融云一直在经营的全球通信网络**,除了遍布全球数量极多的节点外,更能够通过独有的算法调度流量,帮助客户在最后一公里找到更快、更高质量的接入节点。** 这是基础通信服务厂商要突破的核心技术难点,也恰是融云的核心优势之一。

而除了纯技术层面,我们也会跟客户探讨一些业务层问题,共同应对出海挑战。

图片

(杨攀在扬帆出海 PAGC 活动发表主题演讲)

比如内容全球化问题。 美欧很多社交应用如 WhatsApp、Instagram 等都定位于“Global”产品,它们假定跨国用户交流都使用英语。

但实际上,这几年随着技术的发展,我们几乎已经能够做到文字、图片、语音的实时翻译。把翻译技术与社交结合起来,可以让社交突破语言的边界,让不同地区的人使用各自的母语流畅沟通。依据“六度分割理论”,社交的核心是连接,连接越多,社交用户的规模自然越大,用户群也越活跃,其中就蕴含着巨大的商业价值。

图片

再比如合规问题。 随着各国数据主权意识提升,GDPR 等法律法规的发布推动,合规的重要性日益突显。融云的专长或许不在于权威讲解具体的法律条规,但我们能够从全局的视角帮助客户梳理。想做全球业务,单“合规”这一个点就能够拆分出个人隐私、数据安全、数据主权、内容安全、通信安全等方方面面。任何一方面考虑不到,都有可能给出海业务造成比较大的隐患。

现今的融云,无论从技术、服务还是客户范围上,都早已经做到全球覆盖。未来我们的方向,在于针对不同区域的市场特征,跟厂商共同打造具有地域特色的产品品类,不断打磨更细致的场景应用和玩法

元宇宙与 5G 时代下的社交风口

*扬帆出海记者: *尽管服务理念不断升级,但“一切为了开发者”始终是融云不变的使命。说到开发者,他们的需求必然也会随着环境不断变化,比如说像现在比较火的元宇宙、云社交、云游戏等,那么融云是如何应对新领域中的新需求呢?

融云杨攀: 元宇宙确实是当下热门的话题。其实元宇宙涉及的产业链是非常之多的,比如硬件 VR 设备,以及 3D 技术等。其中就涉及通信技术。

在元宇宙范畴中,与通信相关的主要有两类:

一类是产品中与聊天相关的基础设施,但表现形态可能与日常所用的微信等不太一样。

另一类则是音频。在三维空间中,人们需要社交,本质上是进行语音在线对话。其中涉及很多相关技术,比如音频处理技术 —— 用户之间的语音会被谁听见,用户之间的距离处理等,更核心的是,这种处理技术需要持续在线以保证产品的通信能力,这正是元宇宙产品对通信厂商的基础需求。

*扬帆出海记者: *元宇宙要表达的核心是虚拟与现实的场景融合,所以要完成元宇宙的构建,5G成为核心一环,5G 创造的社会生活新范式,也将让更多人实实在在地享受到数字技术的红利。国内 5G 正处于高速建设的状态,全球范围又是怎样?融云又是如何利用 5G 来突破自我的呢?

融云杨攀: 5G 带来的是整个市场翻天覆地的变化。3G、4G 时代的特点是“业务推着网络走”,具体表现就是业务需求已经到了,但网络速度跟不上,人们只能更多地应用缓存、下载到本地的方式实现需要;4G 的到来,才逐渐可以满足人们“开盖即食”的视频需求。但 5G 则全然不同,5G 领先需求提前一步到来,应用场景还处于相对落后的状态。

就 5G 的三大特点【低延迟】、【高并发】、【海量连接】 来说,目前纯粹依赖这些技术特点的场景,如远程手术操作、VR三维空间视频流通信、物联网等,尚处于探索阶段。所以从通信产业运营商或设备厂商的角度来说,仍要继续探索 5G 三大特点能够同时覆盖的场景,甚至包括 5G 协议的版本迭代也是未来仍需继续探索的道路。

显然,融云对未来的规模化发展已经进行了周密的布局。5G 时代下的新机会,对融云来说又会是一个新的跨越。

*扬帆出海记者: *5G 时代还需要通信产业不断摸索,但听说 6G 都已经在路上了,这是怎么回事?

融云杨攀: 当年 5G 的设计工作大概从 2012 年就已经开始了,而实际应用则是在十年后的今天,所以通常来说,技术开发和设计需要很长时间的铺垫。

而这也恰巧引申出一个现象。像传统的短信、通话产品,迭代速度很慢,但如今互联网基础上的迭代速度却非常之快。随着 4G、5G 时代的来临,流量都流向互联网,分工趋向专业。 底层运营商的职责就是把基础设施铺好、解决好“管道”的问题;而管道上面通信能力衍化的场景,则会由融云这样的厂商在软件层去解决 

未来已来,拥抱变化

*扬帆出海记者: *未来已来。据我了解,在引领行业发展方面融云一直走在行业前列,这是不是也是融云举办全球互联网通信云大会 WICC 的初衷?

融云杨攀: 是的。实际上,融云在 2019 创办第一届全球互联网通信云大会 WICC 时,便以为通信领域开发者和技术人员创建技术交流和行业探索的平台为出发点。如今第四次举办,我们最大的期望就是,整个行业可以搭乘 WICC 这个平台, 一起去见证时代的发展和产业的变化,一起踏踏实实探讨和解决现有的各种难题,一起仰望星空、脚踏实地。****

本届 WICC 我们会专注于泛娱乐、社交、出海等领域话题,相信这次也一定会收获一场硕果累累的行业盛会。

扬帆出海记者: 我们也共同期待这场盛会的到来。预祝 11 月 20 日 WICC 广州站取得圆满成功!

原文链接: https://juejin.cn/post/7022871011284484126


收起阅读 »

(转载)从区块链到元宇宙 Metaverse

数字化的迅速发展影响着人们生活的方方面面 —— 包括人际交往、工作、购物和获得服务的方式,还影响着创造和交换价值的方式。随着 5G 技术的发展,加速了数字化的进程,国家也在大力支持并推动数字经济健康发展。区块链、VR、3D等技术也越来越成熟,数字进行的发展将在...
继续阅读 »

数字化的迅速发展影响着人们生活的方方面面 —— 包括人际交往、工作、购物和获得服务的方式,还影响着创造和交换价值的方式。随着 5G 技术的发展,加速了数字化的进程,国家也在大力支持并推动数字经济健康发展。区块链、VR、3D等技术也越来越成熟,数字进行的发展将在未来十年更加加速,在这样的背景下就出现了元宇宙(Metaverse)的概念。

在本文中,将介绍什么是元宇宙、它为何重要、当前的一些趋势以及元宇宙发展的预期。“元宇宙”意味着很多东西,但主要组成是无处不在的网络、加密货币和像比特币和以太坊这样的加密网络、包括 VR 和 AR 在内的扩展现实 (XR),以及不可替代的代币 (NFT)。

什么是元宇宙

简而言之,元宇宙就是数字世界,可以想象的任何事物都可以存在。最终,将一直连接到元宇宙,扩展视觉、听觉和触觉,将数字项目融入物理世界,或者随时进入完全身临其境的 3D 环境。这一系列技术统称为扩展现实 (XR)。

相信元宇宙有一天会成为一个巨大的经济体,可能会创造当前全球经济总价值的 10 倍。今天,看到了元宇宙可能很快会变成什么样的影子。要了解将是什么,首先应该看看它是从哪里来的。

1985 年,Richard Garriott 创造了“阿凡达”一词来描述电子游戏中玩家的角色。

“《Ultima IV》是我希望玩家对我所谓的‘道德困境和伦理挑战’做出反应的第一款游戏。在我研究美德和伦理时,为了寻找伦理寓言或道德哲学,我在很多印度文本中发现了“阿凡达”这个词的概念。在这种情况下,化身是神降临人间时的物理表现。太好了,因为我想在我虚构的世界里测试你的精神。——理查德·加略特

在 1992 年出版的《雪崩》一书中,尼尔·斯蒂芬森想象了一个类似互联网的虚拟现实世界,他称之为“元宇宙”,用户可以在其中与自己称为“化身”的数字形式进行交互。从《雪崩》开始,“阿凡达”这个词在流行的小说系列中传播开来,包括 Earnest Cline 的《头号玩家》,它被改编成了一部流行电影。

在《头号玩家》中,一个名为 “绿洲” 的中心化元宇宙托管了可以通过多种方式定制的化身。玩家可以购买物品和服装在游戏中使用。这些物品具有真正的价值,丢失它们是一件大事。

2018年,美国科幻冒险片《头号玩家》上映。

很多电子游戏玩家都有同感。我们努力获得这些游戏内部奖励,却发现这些道具随时可能被拿走,或者它们的价值可能被控制所有道具及其功能的集权力量所破坏。

在比特币成为世界上第一个可行的加密货币之前的几年里,Vitalik Buterin 是一名狂热的魔兽世界玩家。

“暴雪从我心爱的术士的生命虹吸法术中移除了伤害成分。我哭着睡着了,那天我意识到中心化服务会带来什么恐怖。” ~ 维塔利克·布特林

这促使 Vitalik 提出了以太坊的想法——一个像支持比特币加密货币的去中心化加密网络,但一个可以执行任意、图灵完备程序的加密网络,称为智能合约。这些智能合约可以做很多事情。其中之一是代表一个独特的数字项目,称为不可替代令牌(NFT)。

为什么元宇宙需要加密货币、NFT 和开放标准

数字产品的市场已经超过 100 亿美元,仅 Fortnite 的销售额就超过 10 亿美元。但目前,Fortnite 的数字物品只能在 Fortnite 中使用,如果 Epic Games 决定关闭 Fortnite,这些物品将在一夜之间变得一文不值。一个数十亿美元的市场将消失在以太中。

8月13日,《堡垒之夜》的开发商 Epic Games 就游戏内置付费30%的问题起诉苹果公司。2020年12月,Facebook 宣布将在诉讼中支持 Epic Games,因为他们在试图在苹果应用商店发布带有应用内置付费功能的产品时也遇到了问题。

不可替代的代币 (NFT) 是一种数字项目,可以在公开市场上创建(铸造)、出售或购买,并由任何个人用户拥有和控制,无需任何中心化公司的许可或支持。

为了让数字产品具有真正的、持久的价值,它们必须独立于某个可能随时决定删除或禁用该产品的实体而存在。正是 NFTs 的这一属性使它们能够控制数十万美元。例如,特雷弗·琼斯和DC漫画艺术家José Delbo的合作作品在 2020 年 12 月 2 日 被出售,售价为 302.5 ETH,当时为 11万1千美元,按照现在 ETH 的价值来算相当于超过 120 万美元。

Fortnite 和 NFT 中的项目之间的区别很简单:真正的所有权。NFT 的购买者永远不必担心云中的某些公司会停止他们的服务或冻结他们的帐户。元宇宙必须是一个开放的生态系统,而不是由任何一家公司的奇思妙想主导的生态系统。

元宇宙由许多部分组成,但下面这些是最基本的基础:

  • 互联网:一种分散的计算机网络,不属于任何单一实体或政府所有,不需要任何此类实体的中央许可即可使用。

  • 媒体的开放标准:包括文本、图像、音频、视频、3D 项目、3D 场景和几何、矢量、序列以及生成和组合任何这些内容的程序。皮克斯的USD和英伟达的MDL是3D应用互操作性的重要一步。

  • 开放编程语言标准,此类标准包括 HTML、JavaScript、WebAssembly、WebXR、WebGPU Shader Language、etc 等。

  • 扩展现实 (XR) 硬件,例如智能眼镜、触觉和全方位跑步机。

  • 去中心化账本和智能合约平台(例如区块链),用于透明、无需许可和抗审查的交易。包括比特币、以太坊、Flow 和 Theta、币安智能链 (BSC) 等。这些构成了所有权经济的重要基础,将支持元宇宙并使其成为可行的公共产品。

如果有一个控制资产、用户能力和银行账户的中央参与者,就不可能拥有真正开放的经济。只有开放、可互操作的规范和去中心化、无需许可、图灵完备的智能合约平台才能支持元宇宙蓬勃发展所需的所有权经济。

在 《头号玩家》中,一个名为 IOI 的邪恶公司试图解决寻宝问题以完全控制绿洲。IOI 的动机是不惜一切代价获取最大利润,包括合法监禁和奴役大片人类以偿还债务。

这有点极端,但如果任何一家公司对元宇宙拥有过多的控制,它们可能会决定效仿苹果,从元宇宙的所有交易中敲诈巨额利润,从而扼杀经济效率,扼杀创新和新的有益商业模式的发现。

去中心化的经济比将元宇宙的钥匙交给任何一家公司更公平、更高效、更长期可持续。在密码世界中,每个人都拥有自己王国的钥匙。

多元宇宙

目前,还没有一个能像《头号玩家》中的绿洲那样普遍互操作的元宇宙。相反,我们有许多不同的平台在争夺用户。第一款MMO和开放世界游戏,如《魔兽世界》和《第二人生》在2003-2004年左右开始奠定现代3D多元宇宙的基础,但正如 Vitalik 发现的那样,它们的经济100%依赖于单一的集权公司,你必须信任它,尊重用户的需求。你不能将道具和金钱从一个游戏世界带到另一个游戏世界。

Decentraland 是一个3D空间,在这里可以创建虚拟世界,玩游戏,探索充满非功能性艺术的博物馆,参加现场音乐会等。如果安装了 MetaMask 扩展,让你访问加密货币和非功能性功能,它可以在标准的 WEB 浏览器中工作。可以买卖财产,为艺术画廊创造和出售虚拟艺术,或者建造世界。几家公司已经在去中心区投资了土地,其中一些公司可能愿意付钱给技术熟练的建筑商来开发它。

Decentraland 甚至进入了展会空间,并证明有很多有趣的机会可以为供应商创造独特而富有创意的展位体验。

1.jpeg

3D 场景设计师可能有朝一日能够通过为元宇宙主持的展会设计供应商体验而谋生。

2.jpeg

Decentraland 中有各种可玩的迷你游戏,其中一些游戏会用 NFT 奖励你,可以在 OpenSea 上出售这些游戏。类似的平台,如 Somnium Space 和 The Sandbox 也出现了。

多元宇宙需要一个全宇宙

今天的互联网有点像这样,许多不同的应用程序和空间,它们之间共享的信息相对较少,但加密和去中心化计算开始打破其中的一些壁垒。

例如,你可以将自己拥有的东西从一个应用程序带到另一个应用程序。例如,你可以在 Uniswap 上进行加密货币交换,然后查看 Zerion 中反映的余额。同样,你可以在 OpenSea 上出售你的 Decentraland 可穿戴设备。

大多数 3D 世界资产或环境本身并非如此。例如,无法在 The Sandbox 中探索 Decentraland 世界,也无法使用 Unreal Engine 应用打开 Unity 制作的游戏。

如果我们希望我们的世界在不同平台、设备和引擎上真正开放和可探索,我们就需要数据开放和可访问,我们需要即时服务和数据订阅,以便在我们需要的时候和地点交付资产。NVIDIA 的 Omniverse 将 Pixar 的通用场景描述等开放文件格式与网络服务相结合,您可以将这些服务与用于创建 VR 媒体的软件工具连接起来。

结果是世界创建者可以跨各种应用程序实时协作,所有编辑和查看相同的资产。这基本上就像在 3D 世界的 Google Docs 中进行协作。

Omniverse 使用 Pixar 的 USD 作为原生文件格式,但它更进了一步,将资产作为实时云服务提供,许多应用程序可以同时连接到这些服务。Pixar 的 USD 技术是开源的,这意味着任何开发人员都可以下载工具并采用这些技术并将它们集成到他们的应用程序和游戏中。

但数据共享并没有结束。元宇宙的基础是共享数据、共享计算和共享带宽,当它们聚集在一起时,它可以扩展我们作为一个物种可以共同完成的范围。

这就需要的是一个去中心化的全宇宙,作为一种公共物品,任何人都可以使用、贡献、托管节点并在其上构建。

加密货币连接

像 Folding@Home 这样的分布式计算软件从 2000 年就已经存在。点对点文件共享从 1990 年代就已经存在。几十年前,本可以为文件共享和分散计算构建一个通用操作系统。但是缺少一个关键组成部分:如何奖励为公共服务做出贡献的用户?

许多人会贡献他们的 CPU 时间来帮助对抗癌症和 COVID-19,但 P2P 文件共享服务的主要问题之一是免费加载。很多人会连接,使用共享资源,然后在他们做出足够的贡献来弥补他们拿走的资源的成本之前就断开了。

因此需要激励措施。加密货币是可编程的货币,有了它们,可以创建自给自足的协议:文件共享服务,人们可以通过分享空间和带宽获得报酬;计算共享服务,人们可以在不玩游戏时通过分享昂贵的游戏GPU获得报酬。类似地,可以将资金集中在一起,提供流动性,这样用户就可以有效地从一种数字货币转换到另一种数字货币。流动性提供者通过向市场增加流动性而获得报酬。

加密货币使我们能够团结在一个共享的元宇宙周围,并为支持它所需的服务付费,而无需一家公司拥有所有资源。无需每个人都支付 AWS,任何人都可以在自己的家中运行服务节点,并收回与元宇宙交互所需的部分硬件成本。

人工智能和元宇宙

元宇宙最重要和最容易被忽视的方面之一将是人工智能,有很多用例。

这里有一些例子:

  • AI 可用于创建、审计和保护智能合约,使其更安全、更易于创建和使用。对此有迫切的需求,需要的技术已经存在。

  • 智能 AI 生物可以在元宇宙中漫游,与我们以及彼此互动。如今,人工智能可以生成逼真的图像和人脸的3D模型,生成用于对话的文本,将文本转换为人类发音的语言,并使3D角色看起来像是在说话

  • AI 可以帮助我们创造元宇宙资产、艺术品和内容。

  • AI 可以改进我们用来构建所有这些东西的软件和流程。再过几年,AI 将改进 AI ,导致智能和技术的大爆炸。

最终,人工智能可能能够在我们探索时实时生成完整的虚拟世界。图形渲染技术和人工智能技术之间的界限可能会继续模糊。有一天,人工智能可以接受一些输入,例如“郁郁葱葱的丛林环境,从瀑布中流出的溪流”,并将其转变为我们可以探索和互动的完全沉浸式 3D 环境。

今天,由于增强的创造力和语言技能,人工智能甚至可以生成描述。

硬件

XR 硬件的当前仍然是Microsoft Hololens 2。

收起阅读 »

iOS 教你如何像RN一样实时编译

工具类代码开源Github 一、效果 最终效果: 代码在保存之后,立马在模拟器上看到修改后的效果, 避免Command+R重新编译耗费时间的问题; 如果APP页面层级太深的话,传统调试要一步步点进到指定页面,使用该方案直接就能看到效果,所见即所得,👏👏👏 ...
继续阅读 »

工具类代码开源Github



一、效果


最终效果: 代码在保存之后,立马在模拟器上看到修改后的效果, 避免Command+R重新编译耗费时间的问题; 如果APP页面层级太深的话,传统调试要一步步点进到指定页面,使用该方案直接就能看到效果,所见即所得,👏👏👏



修改标题、修改背景色演示

二、背景


每次都被我们项目的编译速度整的快没脾气了,一直想着优化项目的编译速度。 想想之前做的RN项目的热部署效果真的很爽,不爽之余想到:他用个杂交品种能热部署,而我用苹果亲儿子没道理不行啊!能不能搞个runtime之类的跟新啊。
人有多大胆,地有多大产;不怕办不到,就怕想不到。终于找到了这个成吨减少工作量的方案。


超级简单,只有三步:
1、一个工具
2、选定项目目录
3、把一个文件放到项目中


无需其他任何配置,不对项目结构造成任何侵害。


三、一步步教你使用


1、工具下载 InjectionIII


InjectionIII 是我们需要用到个一个工具,不要因为要用一个工具而厌烦这个方案,它很简单。
它是免费的,app store 搜索:InjectionIII,Icon是 一个针筒。
也是开源的,


GitHub链接: github.com/johnno1962/…


App Store链接: https://itunes.apple.com/cn/app/injectioniii/id1380446739?mt=12


2、配置路径


打开InjectionIII工具,选择Open Project,选择你的代码所在的路径,然后点击Select Project Directory保存。


image.png


image.png


注意:InjectionIII 的File Watcher选项要保持选中状态。


3、导入配置文件


这步我简单写了一个配置文件,直接 GitHub下载 导入项目即可。
如果你比较反感下载文件也可以自己处理:
1.设置AppDelegate.m
打开你的源码,在AppDelegate.m的didFinishLaunchingWithOptions方法添加一行代码:


#if DEBUG
// iOS
[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection10.bundle"] load];
// tvOS
//[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/tvOSInjection.bundle"] load];
// macOS
//[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle"] load];
#endif


2.设置ViewController
在需要修改界面的ViewController添加方法- (void)injected,或者给ViewController类扩展添加方法- (void)injected。
所有修改控件的代码都写在这里面。


- (void)injected
{
//自定义修改...
//重新加载view
[self viewDidLoad];
}


4、启动项目,修改验证


在Xcode Command+R运行项目 ,看到Injection connected 提示即表示配置成功。
image.png


在需要修改的页面,修改控件UI,然后Command+S保存一下代码,立刻就在模拟器上显示修改的信息了。


工具使用中如有问题可以参考github上的过往经验,也欢迎留言我们一起讨论。
工具git地址:github.com/johnno1962/…


5、每个VC要使用的话,还需要去写injected,有点烦人,但是我们有方案


用runtime 给每个VC加个方法class_addMethod


依托InjectionIII的iOS热部署配置文件,无侵害,导入即用。


@implementation InjectionIIIHelper

#if DEBUG
/**
InjectionIII 热部署会调用的一个方法,
runtime给VC绑定上之后,每次部署完就重新viewDidLoad
*/

void injected (id self, SEL _cmd) {
//重新加载view
[self loadView];
[self viewDidLoad];
[self viewWillLayoutSubviews];
[self viewWillAppear:NO];
}

+ (void)load
{
//注册项目启动监听
__block id observer =
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
//更改bundlePath
[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection10.bundle"] load];
//[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle"] load];

[[NSNotificationCenter defaultCenter] removeObserver:observer];
}];

//给UIViewController 注册injected 方法
class_addMethod([UIViewController class], NSSelectorFromString(@"injected"), (IMP)injected, "v@:");

}
#endif
@end



iOS如何提高10倍以上编译速度


更多iOS提高开发效率插件Github


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

使用RN笔记

一、学习说明 了解React和RN的基本语法; RN无法使用div、p、img都不能使用,只能使用RN固有的组件; 需要结合安卓的签名打包步骤,并使用RN提供的打包命令进行完整apk文件发布,最终发出来的就是Release版本的项目 webAPP开发方式: ...
继续阅读 »

一、学习说明



  1. 了解React和RN的基本语法;

  2. RN无法使用div、p、img都不能使用,只能使用RN固有的组件;

  3. 需要结合安卓的签名打包步骤,并使用RN提供的打包命令进行完整apk文件发布,最终发出来的就是Release版本的项目

  4. webAPP开发方式:



  • **H5+****:**需要做出一个完整的网站,然后在网站的基础上使用打包技术,其内部运行的还是网站,

  • **RN:**需要开发一个模板项目,这个模板不能运行到浏览器和手机中,完成后使用RN的打包命令后,把模板的代码翻译成原生的java代码,最终打包成原生手机app,只不过使用前端技术开发而已。


二、搭建开发环境



  1. http://www.react-native.cn/docs/enviro…(注:一定要仔细看文档的译注否则根本运行不了,根据文档的注释下载相应的包)

  2. 运行‘adb devices’的命令查看手机是否连接成功


三、遇到的问题


react-active-webview****直接使用会报**"RNCWebView" was not found in the UIManager.**



  • 解决办法:1.停止项目,cd ios目录运行npx pod install命令下载包

  • 包下完了运行npx react-active link react-native-webview 这时会提示连接ios 和android 成功

  • 重新编译项目 npx react-active run-android 后就可以正常使用了


在React Native开发的时候编译androidreact-native run-android莫名遇到以下的buildfailure:


:app:compileDebugAidl:app:compileDebugRenderscript:app:generateDebugBuildConfig:app:mergeDebugShaders UP-TO-DATE:app:compileDebugShaders UP-TO-DATE:app:generateDebugAssets UP-TO-DATE:app:mergeDebugAssets UP-TO-DATE:app:generateDebugResValues:app:generateDebugResources:app:mergeDebugResources:app:recordFilesBeforeBundleCommandDebug FAILED
复制代码

解决办法:cd android运行./gradlew --stop


react-native 其他请求都没有问题,但是文件上传会报错(‘Network request failed’)




  • 原因:Flipper Network构建initializeFlipper时出现的问题。




  • 解决:找到android/app/src/debug/java/com/**/ReactNativeFlipper.java文件注释43行 


    new NetworkingModule.CustomClientBuilder() {
    @Override
    public void apply(OkHttpClient.Builder builder) {
    // builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
    }
    });




打包时报错JVM内存不够



  • 打开gradle.properties文件 添加org.gradle.jvmargs=-Xmx4608M ,如果是真机测试可以注释。


打包时报错Execution failed for task ':xxxxx:verifyReleaseResources'



  • 是因为Android版本更新到了28,而第三方插件未及时更新,需要打开第三方包的android/build.gradle文件 将23修改成28


react-native-webView 交互




  • RN发送给HTML:


    RN页面首先绑定ref={webView => this.webView = webView} 通过this.webView.message.postMessage(data)来传递内容,html通过
    window.onload = function() {
    document.addEventListener('message', function(msg) {
    console.log(msg)
    });
    }来获取




  •  HTML发送给RN:


    RN页面首先绑定ref={webView => this.webView = webView} 通过webView自带的
    onMessage={(event)=>{
    const data = event.nativeEvent.data
    this._handleMessage(data);
    }}来获取
    HTML通过window.ReactNativeWebView.postMessage("h5 to rn") 来传递内容




四、常用命令和插件


 ./gradlew clean --stacktrace android清除缓存 


 ./gradlew assembleRelease --stacktrace android打包 


rm -rf node_modules && yarn cache clean 删除项目依赖包以及 yarn 缓存 


rm -rf ~/.rncache 清除 React-Native 缓存 


react-native-image-picker 上传图片


react-native-calendars 日历 


react-native-file-selector 文件管理


teaset ui组件


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

元宇宙下的前端现状

资本热词:Metaverse7 月 28 日 扎克伯格表示,该公司正在组建一个产品团队,致力于元宇宙(Metaverse)的开发。“未来五年内,将 Facebook 从社交媒体网络转变为一个元宇宙公司。”英伟达通过一部纪录片自曝: “今年 4 月份那场发布会,...
继续阅读 »

资本热词:Metaverse

  • 7 月 28 日 扎克伯格表示,该公司正在组建一个产品团队,致力于元宇宙(Metaverse)的开发。“未来五年内,将 Facebook 从社交媒体网络转变为一个元宇宙公司。”
  • 英伟达通过一部纪录片自曝: “今年 4 月份那场发布会,全部是合成的”
  • 今年3月初,“元宇宙第一股” 的美国多人在线 3D 创意社区 Roblox(罗布乐思) 已在纽交所上市,而其当天股价暴涨 54.4%
    • 腾讯拿下了 Roblox 中国区代理
    • 2020 年 12 月,腾讯 CEO 马化腾表示,移动互联网时代已经过去,全真互联网时代才是未来。
  • 游戏公司 Epic Games 在 4 月获得 10 亿美元投资用来构建元宇宙
  • 国内方面号称要打造全年龄段元宇宙世界的 MeteApp 公司,在 Roblox 上市后拿到了 SIG 海纳亚洲资本领投的 1 亿美元 C 轮融资
  • 字节跳动于 4 月被曝光已投资 “中国版 Roblox ” 代码乾坤近亿元
  • 陌陌王力表示,未来随着虚拟现实的进一步发展,VR/AR 硬件的不断成熟向家用普及以及人机交互模式的变化,必然会出现新的机会,也就是一种直接将人背后的生活串联起来的方式。
  • 阿里前端委员会互动技术方向重点也是“虚拟角色”和“ AR/VR ”

可以看到:“交互娱乐类资本瞄准的互联网未来 - 元宇宙”

何为元宇宙

  • 首次出现:1992 年尼尔·斯蒂芬森的科幻小说《雪崩》当中,在这部小说中讲述了大量有关虚拟化身、赛博朋克等场景。

  • 维基百科:通过虚拟增强的物理现实,呈现收敛性和物理持久性特征,基于未来互联网,具有链接感知和共享特征的3D虚拟空间。

    • 简单点讲就是:我们在虚拟世界中与一个全新的身份一一对应,并且不会间断地“生活下去”
  • Roblox 提出一个真正的元宇宙产品应该具备八大要素,很容易就能让人联想到《头号玩家》这部电影:

    • 身份:拥有一个虚拟身份,无论与现实身份有没有相关性。

    • 朋友:在元宇宙当中拥有朋友,可以社交,无论在现实中是否认识。

    • 沉浸感:能够沉浸在元宇宙的体验当中,忽略其他的一切。

    • 低延迟:元宇宙中的一切都是同步发生的,没有异步性或延迟性。

    • 多元化:元宇宙提供多种丰富内容,包括玩法、道具、美术素材等。

    • 随地:可以使用任何设备登录元宇宙,随时随地沉浸其中。

    • 经济系统:与任何复杂的大型游戏一样,元宇宙应该有自己的经济系统。

    • 文明:元宇宙应该是一种虚拟的文明。

作为大家口中的“互联网的最终形态”,需要如今大热的包括 AR、VR、5G、云计算、区块链等软硬件技术的成熟。才能构建出一个去中心化的、不受单一控制的、永续的、不会终止的世界。

上面提到的各项技术,和目前前端关联比较大的,便是 AR、VR。

AR 现状

有种新瓶装旧酒的感觉,VR、AR 概念大火的时候还是 17、18 年。几年来,AR 被用来创建虚拟的地方游览、设计和协作 3D 模型、游戏、娱乐、购物、营销、学习、可视化等等。从可用到易用,再到体验的升级,这是用户体验 UX 上一轮的主要革新命题,新一轮的用户体验革命会聚焦在如何真正提供体验的价值。目前 AR 在生活中发挥的就是这样的作用。

案例:

  • AR + 旅游:导航、门店提示、广告、优惠活动提示等等
    • image.png
  • 购物:AR 试鞋、试衣、试妆
  • 游戏:

WebXR

WebXR 是标准也是概念,指的基于 Web 实现虚拟现实和增强现实的能力。

其实就是在 Web 上开发 AR(Augmented Reality)和 VR(Virtual Reality)应用的 API, “X”代表沉浸式体验中的任何事物。

API

  • API 演进:主要是 google 在推进,从 2016 年开始提出的 WebVR 标准,到由于缺了增强现实这一块,2018 年改为 WebXR
    • 相关 API 示例:immersive-web.github.io/webxr-sampl…
    • 最新动态:2021 年 4月13日 Chrome 的 90 版本增加新 WebXR API:
      • WebXR Depth API:获取用户的设备与现实环境中物体的距离
      • WebXR AR Lighting Estimation:获取环境的光线情况
  • 示例代码:
async function activateXR() {
// 创建 WebGL 上下文
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
const gl = canvas.getContext("webgl", { xrCompatible: true });

// 初始化three.js
const scene = new THREE.Scene();

// 创建一个有不同颜色面的立方体
const materials = [
new THREE.MeshBasicMaterial({ color: 0xff0000 }),
new THREE.MeshBasicMaterial({ color: 0x0000ff }),
new THREE.MeshBasicMaterial({ color: 0x00ff00 }),
new THREE.MeshBasicMaterial({ color: 0xff00ff }),
new THREE.MeshBasicMaterial({ color: 0x00ffff }),
new THREE.MeshBasicMaterial({ color: 0xffff00 })
];

// 将立方体添加到场景中
const cube = new THREE.Mesh(new THREE.BoxBufferGeometry(0.2, 0.2, 0.2), materials);
cube.position.set(1, 1, 1);
scene.add(cube);

// 使用three.js设置渲染:创建渲染器、挂载相机
const renderer = new THREE.WebGLRenderer({
alpha: true,
preserveDrawingBuffer: true,
canvas: canvas,
context: gl
});
renderer.autoClear = false;

// API 直接更新相机矩阵
// 禁用矩阵自动更新
const camera = new THREE.PerspectiveCamera();
camera.matrixAutoUpdate = false;


// 使用“immersive-ar”初始化 WebXR 会话
const session = await navigator.xr.requestSession("immersive-ar");
session.updateRenderState({
baseLayer: new XRWebGLLayer(session, gl)
});

const referenceSpace = await session.requestReferenceSpace('local');

// 创建一个渲染循环,允许我们在 AR 视图上绘图
const onXRFrame = (time, frame) => {
session.requestAnimationFrame(onXRFrame);

// 将图形帧缓冲区绑定到 baseLayer 的帧缓冲区
gl.bindFramebuffer(gl.FRAMEBUFFER, session.renderState.baseLayer.framebuffer)

// 检索设备的姿态
// XRFrame.getViewerPose 可以在会话尝试建立跟踪时返回 null
const pose = frame.getViewerPose(referenceSpace);
if (pose) {
// 在移动端 AR 中,只有一个视图
const view = pose.views[0];

const viewport = session.renderState.baseLayer.getViewport(view);
renderer.setSize(viewport.width, viewport.height)

// 使用视图的变换矩阵和投影矩阵来配置 THREE.camera
camera.matrix.fromArray(view.transform.matrix)
camera.projectionMatrix.fromArray(view.projectionMatrix);
camera.updateMatrixWorld(true);

// 使用 THREE.WebGLRenderer 渲染场景
renderer.render(scene, camera)
}
}
session.requestAnimationFrame(onXRFrame);
}


  • 兼容性:作为 W3C 的前沿标准,目前主要是 Chrome 在推进。市面上浏览器对 WebXR 的支持整体较弱,后面会介绍相关的兼容库和现成的解决方案。

模型观察者:model-viewer

  • 谷歌实现的一个 web component,可用于查看 Web 上的 3D 模型并与之交互



  • 实际效果:

Unity

作为知名的 3d 游戏引擎,也有相应的 WebWR 支持库

社区生态

  • XR Swim:为开发者提供了一个发布 WebXR 内容的统一平台,相当于网页端 AR/VR 应用领域的 Steam 平台。

挑战

  • 如何保持低延迟、高精度的场景,以及快速处理数据进行渲染和展示动画的能力。
  • 传统的通信方法速度不够快。查看场景产生的大量数据可能超出渲染限制。

WebAR

优缺点

和 WebXR 有相似的优缺点。

  • 优点:跨平台、传播方便( URL 的格式传播)
  • 缺点:
    • 各浏览器标准不统一
    • 3D 内容加载慢,无法实现复杂的内容
    • 渲染质量低
    • 无法实现复杂交互(受限于浏览器传统交互方式)

WebAr 框架及关键原理











  • 效果如下: codepen 地址识别图片地址

  • 还有一些独立功能的框架:

    • 识别与追踪:Tracking.js、JSFeat、ConvNetJS、deeplearn.js、keras.js 。获取到视频流之后的工作就是识别和追踪。不管是对于 native AR 还是 WebAR,目前的识别算法与框架已经非常成熟,难就难在识别之后如何跟踪,如何更好更稳定更高质量的跟踪。
      • 方式一:在前端直接处理视频流。在前端直接进行图像处理,可以用 Tracking.js 和 JSFeat。这两个库类似,都是在前端做计算机视觉的,包括提取特征点、人脸识别等。
      • 方式二:前端传输视频流给后端,后端处理完毕返回结果到前端,目前有一些云识别服务就是如此。
    • 渲染与交互A-Frame、Three.js、Babylon.js、Pixi.js、WebGL
      • A-Frame:基于 Three.js 的开源框架,可以在 HTML 中直接配置场景,适用于简单的 3D 场景搭建
  • 框架库实现原理:上面提到的 AR 框架实现原理大都如下图所示:

性能方案

  • 把纯计算的代码移到 WebGL 的 shader 或 Web Worker 里
    • WebGL 调用 GPU 加速
      • shader 可以用于加速只和渲染(重绘)有关的代码,无关渲染的代码放入 shader 中反而会造成重复计算
    • Web Worker
      • 适用于事先计算或实时性要求不高的代码,如布局算法
  • WebAssembly
  • gpu.js
    • 将简单的 JavaScript 函数转换为着色器语言并编译它们,以便它们在您的 GPU 上运行。如果 GPU 不可用,函数仍将在常规 JavaScript 中运行。
  • 用滤波算法(比如卡尔曼滤波)将卡顿降到更小,让用户从视觉感受上似乎更流畅

市场化解决方案

扩展

  • 企业 AR:2021 年的 7 个实际用例:arvrjourney.com/enterprise-…
    • 主流领域:远程协助、医疗诊断、销售、培训、物流、制造、原型设计

相关资料


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

收起阅读 »

ReactNative与iOS的交互

本文简要展示RN与iOS原生的交互功能。 1.1 RCTRootView初始化问题 /** * - Designated initializer - */ - (instancetype)initWithBridge:(RCTBridge *)bridge...
继续阅读 »

本文简要展示RN与iOS原生的交互功能。


1.1 RCTRootView初始化问题


/**
* - Designated initializer -
*/
- (instancetype)initWithBridge:(RCTBridge *)bridge
moduleName:(NSString *)moduleName
initialProperties:(NSDictionary *)initialProperties NS_DESIGNATED_INITIALIZER;

/**
* - Convenience initializer -
* A bridge will be created internally.
* This initializer is intended to be used when the app has a single RCTRootView,
* otherwise create an `RCTBridge` and pass it in via `initWithBridge:moduleName:`
* to all the instances.
*/
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
moduleName:(NSString *)moduleName
initialProperties:(NSDictionary *)initialProperties
launchOptions:(NSDictionary *)launchOptions;


1、当Native APP内只有一处RN的入口时,可以使用initWithBundleURL,否则的话就要使用initWithBridge方法。

2、因为initWithBundleURL会在内部创建一个RCTBridge,当有多个RCTRootView入口时,就会存在多个RCTBridge,容易导致Native端与RN交互时多次响应,出现BUG。



1.2 创建自定义的RNBridgeManager



由于APP内有RN多入口的需求,所以共用一个RCTBridge



RNBridgeManager.h



#import <Foundation/Foundation.h>
#import <React/RCTBridge.h>

NS_ASSUME_NONNULL_BEGIN

@interface RNBridgeManager : RCTBridge
/**
RNBridgeManager单例
*/
+ (instancetype)sharedManager;

@end

NS_ASSUME_NONNULL_END


RNBridgeManager.m


#import "RNBridgeManager.h"

#import <React/RCTBundleURLProvider.h>

//dev模式下:RCTBridge required dispatch_sync to load RCTDevLoadingView Error Fix
#if RCT_DEV
#import <React/RCTDevLoadingView.h>
#endif
/**
自定义类,实现RCTBridgeDelegate
*/
@interface BridgeHandle : NSObject<RCTBridgeDelegate>

@end

@implementation BridgeHandle

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge{
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
}
@end


@implementation RNBridgeManager

+ (instancetype)sharedManager{
static RNBridgeManager *manager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[RNBridgeManager alloc] initWithDelegate:[[BridgeHandle alloc] init] launchOptions:nil];
#if RCT_DEV
[manager moduleForClass:[RCTDevLoadingView class]];
#endif
});
return manager;
}


@end


1.3 Native进入RN页面


 RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:[RNBridgeManager sharedManager] moduleName:@"RNTest" initialProperties:nil];
UIViewController *vc = [[UIViewController alloc] init];
vc.view = rootView;
[self.navigationController pushViewController:vc animated:YES];

1.4 RN调用Native方法



  • 创建一个交互的类,实现<RCTBridgeModule>协议;

  • 固定格式:在.m的实现中,首先导出模块名字RCT_EXPORT_MODULE();RCT_EXPORT_MODULE接受字符串作为其Module的名称,如果不设置名称的话默认就使用类名作为Modul的名称;

  • 使用RCT_EXPORT_METHOD导出Native的方法;


1.4.1 比如我们导出Native端的SVProgressHUD提示方法:


RNInterractModule.h


#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

NS_ASSUME_NONNULL_BEGIN

@interface RNInterractModule : NSObject<RCTBridgeModule>

@end

NS_ASSUME_NONNULL_END


RNInterractModule.m


import "RNInterractModule.h"
#import "Util.h"
#import <SVProgressHUD.h>

@implementation RNInterractModule
////RCT_EXPORT_MODULE接受字符串作为其Module的名称,如果不设置名称的话默认就使用类名作为Modul的名称
RCT_EXPORT_MODULE();

//==============1、提示==============
RCT_EXPORT_METHOD(showInfo:(NSString *) info){
dispatch_sync(dispatch_get_main_queue(), ^{
[SVProgressHUD showInfoWithStatus:info];
});
}
@end


1.4.2 RN端调用导出的showInfo方法:


我们在RN端把Native的方法通过一个共同的utils工具类引入,如下



import { NativeModules } from 'react-native';

//导出Native端的方法
export const { showInfo} = NativeModules.RNInterractModule;

具体的RN页面使用时:


import { showInfo } from "../utils";

//通过Button点击事件触发
<Button
title='1、调用Native提示'
onPress={() => showInfo('我是原生端的提示!')}
/>

调用效果:

image


1.4.3 RN回调Native



RN文档显示,目前iOS端的回调还处于实验阶段



我们提供一个例子来模拟:目前的需求是做面包,RN端能提供面粉,但是不会做,Native端是有做面包的功能;所以我们需要先把面粉,传给Native端,Native加工好面包之后,再通过回调回传给RN端。


Native端提供方法


// 比如调用原生的方法处理图片、视频之类的,处理完成之后再把结果回传到RN页面里去
//TODO(RN文档显示,目前iOS端的回调还处于实验阶段)
RCT_EXPORT_METHOD(patCake:(NSString *)flour successBlock:(RCTResponseSenderBlock)successBlock errorBlock:(RCTResponseErrorBlock)errorBlock){
__weak __typeof(self)weakSelf = self;
dispatch_sync(dispatch_get_main_queue(), ^{
NSString *cake = [weakSelf patCake:flour];
//模拟成功、失败的block判断
if([flour isKindOfClass:[NSString class]]){
successBlock(@[@[cake]]);//此处参数需要放在数组里面
}else{
NSError *error = [NSError errorWithDomain:@"com.RNTest" code:-1 userInfo:@{@"message":@"类型不匹配"}];
errorBlock(error);
}
});
}


//使用RN端传递的参数字符串:"",调用Native端的做面包方法,加工成面包,再回传给RN
- (NSString *)patCake:(NSString *)flour{
NSString * cake = [NSString stringWithFormat:@"使用%@,做好了:🎂🍞🍞🍰🍰🍰",flour];
return cake;
}

RN端调用:


//首先工具类里先引入
export const { showInfo,patCake } = NativeModules.RNInterractModule;


//具体页面使用
<Button
title='4、回调:使用面粉做蛋糕'
onPress={() => patCake('1斤面粉',
(cake) => alert(cake),
(error) => alert('出错了' + error.message))}
/>

调用效果:


image


1.4.4 使用Promise回调


Native端提供方法



RCT_EXPORT_METHOD(callNameTointroduction:(NSString *)name resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock) reject){
__weak __typeof(self)weakSelf = self;
dispatch_sync(dispatch_get_main_queue(), ^{
if ([name isKindOfClass:NSString.class]) {
resolve([weakSelf introduction:name]);
}else{
NSError *error = [NSError errorWithDomain:@"com.RNTest" code:-1 userInfo:@{@"message":@"类型不匹配"}];
reject(@"class_error",@"Needs NSString Class",error);
}
});
}

- (NSString *)introduction:(NSString *)name{
return [NSString stringWithFormat:@"我的名字叫%@,今年18岁,喜欢运动、听歌...",name];
}

RN端调用:


//首先工具类里先引入
export const { showInfo,patCake, callNameTointroduction} = NativeModules.RNInterractModule;

//具体页面使用
<Button
title='5、Promise:点名自我介绍'
onPress={
async () => {
try {
let introduction = await callNameTointroduction('小明');
showInfo(introduction);
} catch (e) {
alert(e.message);
}
}
}
/>


调用效果:

image


1.5 Native端发送通知到RN


Native端继承RCTEventEmitter,实现发送RN通知类:


RNNotificationManager.h



#import <Foundation/Foundation.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTBridgeModule.h>

NS_ASSUME_NONNULL_BEGIN

@interface RNNotificationManager : RCTEventEmitter

+ (instancetype)sharedManager;

@end

NS_ASSUME_NONNULL_END


RNNotificationManager.m



#import "RNNotificationManager.h"

@implementation RNNotificationManager
{
BOOL hasListeners;
}


+ (instancetype)sharedManager{
static RNNotificationManager *manager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
manager = [[self alloc] init];
});
return manager;
}

- (instancetype)init{
self = [super init];
if (self) {
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center removeObserver:self];
[center addObserver:self selector:@selector(handleEventNotification:) name:@"kRNNotification_Login" object:nil];
[center addObserver:self selector:@selector(handleEventNotification:) name:@"kRNNotification_Logout" object:nil];
};
return self;
}


RCT_EXPORT_MODULE()

- (NSArray<NSString *> *)supportedEvents{
return @[
@"kRNNotification_Login",
@"kRNNotification_Logout"
];
}
//优化无监听处理的事件
//在添加第一个监听函数时触发
- (void)startObserving{
//setup any upstream listenerse or background tasks as necessary
hasListeners = YES;
NSLog(@"----------->startObserving");
}

//will be called when this mdules's last listener is removed,or on dealloc.
- (void)stopObserving{
//remove upstream listeners,stop unnecessary background tasks.
hasListeners = NO;
NSLog(@"----------->stopObserving");
}

+ (BOOL)requiresMainQueueSetup{
return YES;
}

- (void)handleEventNotification:(NSNotification *)notification{
if (!hasListeners) {
return;
}

NSString *name = notification.name;
NSLog(@"通知名字-------->%@",name);
[self sendEventWithName:name body:notification.userInfo];

}

@end


RN端注册监听:


//utils工具类中导出
export const NativeEmitterModuleIOS = new NativeEventEmitter(NativeModules.RNNotificationManager);


//具体页面使用
import { NativeEmitterModuleIOS } from "../utils";

export default class ActivityScene extends Component {

constructor(props) {
super(props);
this.subscription = null;
this.state = {
loginInfo: '当前未登录',
};
}

updateLoginInfoText = (reminder) => {
this.setState({loginInfo: reminder.message})
};

//添加监听
componentWillMount() {
this.subscription = NativeEmitterModuleIOS.addListener('kRNNotification_Login', this.updateLoginInfoText);

}
//移除监听
componentWillUnmount() {
console.log('ActivityScene--------->', '移除通知');
this.subscription.remove();
}
render() {
return (
<View style={{flex: 1, backgroundColor: 'white'}}>
<Button
title='3、RN Push到Native 发送通知页面'
onPress={() => pushNative(RNEmitter)}
/>
<Text style={{fontSize: 20, color: 'red', textAlign: 'center',marginTop:50}}>{this.state.loginInfo}</Text>
</View>
);
}
}

效果展示:


image


1.6 完整Demo(包含iOS & Android)


RN-NativeTest


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

iOS NerdyUI and Cupcake

iOS
NerdyUI 使用小技巧前言首先本文并不是完整的使用说明,不会对每个属性的用法都面面俱到。如果您想了解更多信息,可以到对应的头文件中查看。这里列出了一些在实际项目中可能会用到的小技巧以及注意事项,希望能对您有所帮助。如果看完觉得有用,麻烦点个赞。如果觉得值得...
继续阅读 »

NerdyUI 使用小技巧

前言

首先本文并不是完整的使用说明,不会对每个属性的用法都面面俱到。如果您想了解更多信息,可以到对应的头文件中查看。这里列出了一些在实际项目中可能会用到的小技巧以及注意事项,希望能对您有所帮助。如果看完觉得有用,麻烦点个赞。如果觉得值得一试,麻烦到 github 给个星,让我有继续写下去的动力。下一篇将解释 NerdyUI 实现上的一些小技巧,敬请期待。

如果您还不知道 NerdyUI 是什么,请先移步这里

Str

  1. .a() 可用来拼接字符串,.ap() 可用来拼接路径。它们能接受的参数跟 Str() 一样。传 nil 的话则什么事都不做,很适合用来拼接多个字符串。

     @"1".a(@"2").a(3).a(nil).a(4.0f).a(@5).a(@"%d", 6);    //@"123456"
    Str(province).a(city).a(district).a(address); //不用担心有的变量可能为 nil
  2. .subFrom() 和 .subTo() 用来截取子串,你可以传一个索引或字符串。

     @"hello".subFrom(2);         //"llo"
    @"hello".subFrom(@"l"); //"llo"
    @"hello".subTo(2); //"he"
    @"hello".subTo(@"ll"); //"he"
  3. .subMatch() 和 .subReplace() 可用正则表达式来查找和替换子串。

     @"pi: 3.13".subMatch(@"[0-9.]+");               //"3.13"
    @"pi: 3.13".subReplace(@"[0-9.]+", @"3.14"); //"pi: 3.14"

AttStr

  1. AttStr() 可以把多个 NSString、NSAttributedString 和 UIImage 拼接成一个 NSAttributedString。后面设置的属性默认会覆盖前面设置的相同属性,可以使用 .ifNotExists 来避免这种情况。

     .color(@"red").color(@"blue");                //蓝色
    .color(@"red").ifNotExists.color(@"blue"); //红色

    AttStr(
    @"small text, ",
    AttStr(@"large text, ").fnt(@40),
    AttStr(@"red small text, ").color(@"red"),
    Img(@"moose"),
    @"small text"
    ).ifNotExists.fnt(20);
  2. NSAttributedString 里能包含图片这个事实打开了无限的可能,很多之前要用用多个 Label 和 ImageView 才能实现的 UI 用 AttStr 可以很轻易的搞定。

     AttStr(@"A hat ", Img(@"hat"), @" and a moose", Img(@"moose");
  3. AttStr 的属性默认会应用到整个字符串,你可以用 .range()、 .match()、 .matchNumber、 .matchURL.matchHashTag 和 .matchNameTag 等来缩小范围。

     id str = @"Hello @Tim_123";

    AttStr(str).color(@"blue"); //整个字符串都为蓝色
    AttStr(str).range(0, 5).color(@"blue"); //"Hello" 为蓝色
    AttStr(str).match(@"Tim").color(@"blue"); //"Tim" 为蓝色
    AttStr(str).matchNumber.color(@"blue"); //"123" 为蓝色
    AttStr(str).matchNameTag.color(@"blue"); //"@Time_123" 为蓝色

    AttStr(str).range(0, 3).range(-3, 3).match(@"@").color(@"blue");
    //"Hel", "@", "123" 为蓝色

    .match() 可以使用正则表达式,负数的 range 表示从尾部往前数。.range() 和 .match() 可连续使用,表示同时选取多个子串。

  4. 使用 .lineGap() 可以设置行间距。但你应该很少会用到,因为 Label 也有一个 .lineGap() 快捷属性。.linkForLabel 只适用于 Label,不适用于其他视图。

Img

  1. 给 Img() 传色值的话会返回一个 1x1 大小的图片,这在大部分情况貌似都没什么用。除了 Button 的 .bgImg() 和 .highBgImg(),因为 Button 的 backgroundImage 会自动拉伸占满整个视图。

     Img(@"red").resize(100, 100);        //100x100 大小的红色图片
  2. .stretchable 会返回一个可拉伸的图片,拉伸位置在图片中心点。如果你想更具体的控制可拉伸区域,可以使用 .tileInsets() 和 .stretchInsets()

     Img(@"button-bg").stretchable;    //等于 Img(@"#button-bg");
    Img(@"pattern").tileInsets(0); //平铺图片
  3. .templates 和 UIView 的 .tint() 配合可以用来给图片上色。

    ImageView.img(Img(@"moose").templates).tint(@"red");

Color

  1. 你可以用 .opacity() 来修改 Color 的 alpha 值:

     Color(@"red").opacity(0.5);        //等于 Color(@"red,0.5");
  2. 你可以用 .brighten().darken().saturate().desaturate() 和 .hueOffset() 等来修改颜色。

     View.wh(100, 100).bgColor(@"#289DCE").onClick(^(UIView *v) {
    v.bgColor(v.backgroundColor.darken(0.2)); //模拟点击变暗效果
    });

Screen

  1. 你可以用 Screen.sizeScreen.width 和 Screen.height 来访问屏幕大小。Screen 还有一个比较有用的属性是 Screen.onePixel, 它始终返回一个像素的大小而不管是在什么设备上。比如设计师可能要求 App 里的分割线都是一个像素的大小,那么你就可以这么用:

     Style(@"separator").wh(Screen.width, Screen.onePixel).bgColor(@"#d9d9d9");
    ...
    id s1 = View.styles(@"separator");
    id s2 = View.styles(@"separator").x(15).w(Screen.width - 30);

View

  1. 如果你想设置一个视图的大小,可以用.wh(50, 50)。但如果你想让一个它的等于另一个视图的大小呢,你可以这么写 .wh(otherView.w, otherView.h), 或者更简单一点 .wh(otherView.wh), 这是因为 .wh() 既可以接受两个 CGFloat, 也可以接受一个 CGSize。.xy().cxy().maxXY() 和 .xywh() 也与此类似,比如 .cxy(otherView.center).xywh(otherView.frame) 和 .xywh(otherView.xy, 50, 50)等等。

  2. 当你想给一个视图设置 border 时,你可只传一个宽度 .border(2), 或者同时带上一个颜色 .border(2, @"red")。如果你已经有一个 UIColor 对象,那么也可以直接传这个对象 .border(2, borderColor),这对于 .tint().color() 和 .bgColor() 等也适用。

  3. 使用 .borderRadius() 会自动把 masksToBounds 设为 YES(如果没有设置阴影的话)。shadow() 默认向下投影,它有几种形式:

     .shadow(0.6);            //shadowOpacity
    .shadow(0.6, 2); //shadowOpacity + shadowRadius
    .shadow(0.3, 3, 3, 3); //shadowOpacity + shadowRadius + shadowOffsetXY
  4. .onClick() 可以用来给任意视图添加一个单击手势,如果这个视图是一个 UIButton,则它使用的是 Button 的 UIControlEventTouchUpInside 事件。使用 onClick 时还会自动把 userInteractionEnabled 设为 YES,毕竟当你给一个 UILabel 或者 UIImageView 添加单击事件时,你想让它们可以点击。

    你可以传一个 block 来作为回调方法,最简单的形式就是 .onClick(^{ ... })。 onClick 已经自动对 self 做了 weakify 处理,虽然标准做法是要在 block 里对 self再做个强引用,防止它提前释放。但大部分情况下你都不需要这么做,因为很多时候 self 对应的都是当前视图的父视图或者它所在的 ViewController,而它们是不会提前释放的。如果你还是不放心,那么你可以这么写:

     .onClick(^{ typeof(self) strongSelf = self; ... });

    如果需要在 block 里访问当前视图,你不能这么写:

     UIView *box = View.onClick(^{
    box.bgColor(@"blue"); //box为nil,因为此时onClick还没返回
    });

    正确写法应该是:

     UIView *box = View.onClick(^(UIView *box) {
    box.bgColor(@"blue"); //使用的是 block 参数
    });

    如果回调代码比较多,或者你更喜欢传统的 target-action 方式,那么你可以这么用:

     .onClick(@"boxDidTap")        //target 默认为 self,action 为字符串,请小心拼写
    .onClick(@"boxDidTap:") //如果你需要当前视图作为参数的话

    这里提到的 .onClick() 的用法同样适用于 .onChange().onFinish() 和 .onLink()等。

  5. 把一个视图添加到另一个视图里有三种方式:

     parentView.addChild(view1, view2, view3, ...);    //使用 addChild 添加多个子视图
    view1.addTo(parentView); //使用 addTo 加到父视图里
    view1.embedIn(parentView); //使用 embedIn 加到父视图里,会同时添加上下左右的约束

    .embedIn() 可以有额外的参数,用来设置距离父视图上下左右的偏移量:

     .embedIn(parentView, 10, 20, 30, 40);    //上:10, 左:20, 下:30, 右:40
    .embedIn(parentView, 10, 20, 30); //上:10,左右:20,下:30
    .embedIn(parentView, 10, 20); //上下:10, 左右:20
    .embedIn(parentView, 10); //上下左右:10
    .embedIn(parentView); //上下左右:0

    这中用法跟 HTML 里的 Margin 和 Padding 类似。如果有某些方向你不想加约束的话,你可以用 NERNull 代替:

     .embedIn(parentView, 10, 20, NERNull, NERNull);    //上:10,左:20
    .embedIn(parentView, 10, NERNull); //上下:10

    .embedIn()这种可变参数的用法同时也适用于 .insets(),后面会说到。

  6. 如果你习惯于手动布局,那么你可能会经常用到 .fitSize、 .fitWidth 和 .fitHeight 来改变视图的大小,用 .flexibleLeft、 .flexibleRight ... .flexibleWH等来设置 autoresizingMask。

    如果你习惯使用 AutoLayout, 则 .fixWidth().fixHeight()、 .fixWH()、 .makeCons()、 remakeCons() 和 updateCons() 等会是你的好朋友。.fixWidth() 等3个内部使用了 .remakeCons() 来设置宽高约束,所以你可以重复使用它们而不用担心会引起约束冲突。

Label

  1. 你可以用 .str() 来设置 text 或者 attributedText。同时你还可以直接传内置类型,省去了转换为字符串的过程:.str(1024)

  2. .fnt() 和 .color() 可以直接传 UIFont 或 UIColor 对象。

  3. .highColor() 可以用来设置 highlighted 状态下的字体颜色,比如 Cell 被选中时。

  4. 允许多行可以用 .lines(0) 或者 .multiline

  5. Label 链接的默认颜色是蓝色,你可以改成其他颜色:

     AttStr(@"hello world").match(@"world").linkForLabel.color(@"red");    //红色链接

    链接选中的样式也可以修改:

     //修改单个 Label 的样式
    label.nerLinkSelectedBorderRadius = 0;
    label.nerLinkSelectedColor = [UIColor blueColor];
    //全局修改
    [UILabel setDefaultLinkSelectedBackgroundColor:[UIColor blueColor] corderRadius:0];

    因为 UILabel 默认是不接受事件的,你必须使用 .touchEnabled 或者 .onLink() 才能点击链接。因为 .onLink() 也会把 userInteractionEnabled 设为 YES。

ImageView

  1. .img() 还会自动把当前视图的大小设置为图片的大小(如果你没设置过 frame 的话)。

     id iv1 = ImageView.img(@"cat");                //iv1 的大小等于图片的大小
    id iv2 = ImageView.wh(50,50).img(@"cat"); //iv2 的大小等于(50,50)

    .img() 和 .highImg() 还可以接受图片数组。

  2. 你可以用 .aspectFit.aspectFill 和 .centerMode 来设置 contentMode。

Button

  1. Button 标题默认为一行,可以使用 .multiline 来让它支持多行显示。

     Button.str(@"hello\nhow are you").multiline;
  2. Button 的 .bgImg() 和 .highBgImg() 非常的灵活和好用。

     .bgImg(@"btn-normal").highBgImg(@"btn-high");      //使用图片
    .bgImg(@"#btn-normal").highBgImg(@"#btn-high"); //使用可拉伸的图片
    .bgImg(@"red").highBgImg(@"blue"); //使用颜色

    之所以用 .bgImg() 而不是 .bgColor() 来设置按钮背景颜色是因为后者在 Cell 选中时会被清空。.bgImg() 跟 .img() 一样会把当前视图的大小设置为图片的大小(如果你没设置过 frame 的话)。

  3. 因为 UIButton 里带有一个 UILabel 和 一个 UIImageView,很适合用来创建这样的 UI:“一个图标后面跟着一段文字” 或者 “一段文字后面跟着一个图标”,并且图标和文字都可点击。

     //评论图标后跟着评论数
    .img(@"comment_icon").str(commentCount).gap(10);
    //"查看更多"后跟着向右箭头
    .img(@"disclosure_arrow").str(@"查看更多").gap(10).reversed;

    使用 .gap() 可在 image 和 title 之间加上一些间隙。使用 .reversed 可以调换 image 和 title 的位置。

  4. 有的时候你可能想在按钮内容和边框之间留一点空间,那么可以使用 .insets()

     .str(@"Done").insets(5, 10).fitSize;          //宽高跟着 title 的变化而变化
    .str(@"Done").insets(5, 10); //autolayout version
    .str(@"Done").h(45).insets(0, 10).fitWidth; //高度固定,宽度变化
    .str(@"Done").fixHeight(45).insets(0, 10); //autolayout version

    .insets() 还有一个妙用就是当按钮的背景图片带有阴影时,title 的显示位置会不太对,这时候就可以用 .insets() 来调整。 它能接受的参数跟 .embedIn() 的可变参数一样。

  5. 组合的使用 .borderRadius()、 .border()、 .color()、 .highColor()、 .bgImg()、 .highBgImg() 、.insets() 以及 AttStr() 等,可以创建出各种各样的按钮。

Constarints

  1. 一个完整的 NSLayoutConstraint 必须包含这个公式里的全部要素:

      view1.attr1 [= , >= , <=] view2.attr2 * multiplier + constant;

    所以当您使用 .makeCons() 来创建约束时,也必须包含这些要素:

      //让当前视图的左边和上边等于父视图的左边和上边
    make.left.equal.view(superview).left.multipliers(1).constants(0);
    make.top.equal.view(superview).top.multipliers(1).constants(0);

    //让当前视图的大小等于 view2 的大小
    make.width.equal.view(view2).width.multipliers(1).constants(0);
    make.height.equal.view(view2).height.multipliers(1).constants(0);

    可以看到要写不少代码,幸好这里面很多属性都有默认值,我们可以一步步的精简它们:

      //1. 如果有多个约束同时涉及到 view1 和 view2,则可以把它们合并在一起
    make.left.top.equal.view(superview).left.top.multipliers(1, 1).constants(0, 0);
    make.width.height.equal.view(view2).width.height.multipliers(1, 1).constants(0, 0);

    //2. 如果 multipliers 和 constants 的参数都是一样的,则可以把它们合并成一个
    make.left.top.equal.view(superview).left.top.multipliers(1).constants(0);
    make.width.height.equal.view(view2).width.height.multipliers(1).constants(0);

    //3. 如果 attr1 和 attr2 是一样的,则可以省略 attr2
    make.left.top.equal.view(superview).multipliers(1).constants(0);
    make.width.height.equal.view(view2).multipliers(1).constants(0);

    //4. multipliers 的默认值是 1, constants 的默认值是 0,所以它们也可以省略掉
    make.left.top.equal.view(superview);
    make.width.height.equal.view(view2);

    //5. 同时设置 width 和 height 的话可以用 size 来表示
    make.left.top.equal.view(superview);
    make.size.equal.view(view2);

    //6. relation 默认为 equal,所以也可以省略掉(坏处是可读性会降低)
    make.left.top.view(superview);
    make.size.view(view2);

    //7. 如果没指定 view2,则默认为父视图
    make.left.top; //虽然很奇怪,但你可以这么写。不过这时候会有警告,因为我们没用到返回值。
    make.size.view(view2);

    //8. 为了消除警告,可以使用 End() 结尾
    make.left.top.End();
    make.size.view(view2);

    //或者用 And 把它们拼接在一起
    make.left.top.And.size.view(view2);

    可以看到到最后变得非常的精简,但可读性也变得很差了。这就需要各位自己权衡了。

  2. 前面说过如果没有指定 view2, 则默认为父视图。这其实有一个例外,就是涉及到 width 和 height 时:

     make.size.equal.constants(100, 200);

    make.width.constants(100);
    make.height.equal.width.End(); //这里的 equal 不能省略,否则就意义不明了

    这里设置的都是当前视图的大小。如果想让它们相对于其他视图,则需要显示的指定:

     make.width.height.equal.view(view2).height.width.multipliers(0.5);
  3. .priority() 可用来设置优先级。.identifier() 可用来设置标识。

  4. 使用 .makeCons().remakeCons() 和 .updateCons() 前必须把当前视图加到父视图里。

     .addTo(superView).makeCons(^{});

TextField / TextView

  1. 你可以用 .hint() 来设置 placeholder, .maxLength() 来限制输入长度。这两个对 UITextField 和 UITextView 来说几乎是标配,奇怪的是系统默认只支持设置 UITextField 的 placeholder。

     .hint(@"Enter your name");      //使用默认的大小和颜色

    id att = AttStr(@"Enter your name").fnt(15).color(@"#999");
    .hint(att); //使用自定义的大小和颜色
  2. .onChange() 会在文本改变时回调,.onFinish() 会在点击键盘上的 return button 时回调。.insets() 的用法跟 UIButton 一样。UITextView 一个不一样的地方在于它默认是有 insets 的,如果你不想要,可以用 .insets(0) 来清空。

  3. 你可以用 .becomeFocus 来获取输入焦点。

HorStack / VerStack

  1. HorStack() 默认的对齐方式是 centerAlignment,VerStack() 默认的对齐方式是 leftAlignment。它们的用法类似于 UIStackView 及 Android 的 LinearLayout。

  2. 如果你设置了 Stack 的宽高约束,那么当 Stack 里子视图的宽度总和或高度总和小于 Stack 本身的宽或高时,有个子视图将会被拉伸。当 Stack 里子视图的宽度总和或高度总和大于 Stack 本身的宽或高时,有个子视图将会被压缩。对于使用 intrinsicContentSize 的子视图来说,你可以通过 .horHugging()、 .verHugging()、 horResistance().verResistance()、 .lowHugging 和 .lowResistance 等来修改 contentHuggingPriority 和 contentCompressionResistancePriority 的值,进而控制哪个子视图可以被拉伸或压缩。对于第一种情况,你还可以使用 NERSpring, 它相当于一个弹簧,会占用尽可能多的空间,这样所有的子视图都不会被拉伸。

  3. 如果你没有设置 StackView 的宽高约束,那么它的大小会跟随着子视图的变化而变化。一般只有最外层的 StackView 我们会设置它的宽或高(不管是直接或者间接,比如 .embedIn 可能会间接的影响它的宽高)。

     //宽度等于父视图宽度,高度跟随子视图变化
    VerStack(view1, view2, view3).centerAlignment.gap(10).embedIn(self.view, 0, 0, NERNull, 0);

    //固定宽高,使用 NERSpring 来避免子视图被拉伸
    VerStack(view1, @10, view2, NERSpring, view3, @20, view4).wh(self.view.wh).addTo(self.view);

    虽然后一个例子我们设置的是frame,但因为 UIView 的 translatesAutoresizingMaskIntoConstraints 默认为 YES,所以也相当于设置了宽高约束。加到 Stack 里的子视图的 translatesAutoresizingMaskIntoConstraints 会被设为 NO,所以只有最外层的 Stack 可以用设置 frame 的方式来布局。

  4. .gap() 会在每个子视图之间添加相同的间隙。@(n) 会在两个子视图之间添加间隙,这就允许不同的子视图之间有不同的间隙。

  5. 可以通过 -addArrangedSubview:、 -insertArrangedSubview:atIndex:、 -removeArrangedSubview: 和 removeArrangedSubviewAtIndex: 来添加或删除子视图。如果想临时隐藏子视图,可以直接设置子视图的 hidden 属性,这是一个非常好用的功能。

Alert / ActionSheet

  1. 可以同时有多个 Action 按钮,其中 .action() 和 . destructiveAction() 必须传标题和回调 block, .cancelAction() 可以只传一个标题:

     Alert.action(@"Action1", ^{

    }).action(@"Action2", ^{

    }).action(@"Action3", ^{

    }).destructiveAction(@"Delete", ^{

    }).cancelAction(@"Cancel").show();
  2. .title().message() 和 .action() 有个隐藏的功能是可以传 NSAttributedString,这就表示它们的显示样式是可以修改的。不过这不是官方提供的功能,可能只在某一些版本的系统上有效,不推荐大家使用。

  3. 使用 .tint() 可以改变所有普通按钮的字体颜色,这是系统提供的功能。

  4. 最后必须调用 .show() 才能显示出来。

Style

  1. View(及其子类)、AttStr 和 Style 可同时使用一个或多个 Styles。对 Style 来说,就相当于继承: Style(@"headline").fnt(@20).color(@"#333");

     Style(@"round-border").borderRadius(8).border(1, @"red");

    AttStr(someString).styles(@"headline");
    Label.styles(@"headline round-border"); //使用空格作为分隔符,就像 CSS 一样

    id roundHeadline = Style().styles(@"headline round-border").bgColor(@"lightGray");
    Button.styles(roundHeadline);
  2. 全局 Style 一般在程序启动的时候设置,比如 -application:didFinishLaunchingWithOptions: 或者 +load 里。

最后

  1. 链式属性分为两种:一种带参数,比如 .color(@"red"),一种不带参数,比如 .centerAlignment。如果最后一个属性是不带参数的属性,且它的返回值没有赋值给一个变量,那么那么编译器将给出警告。你可以使用 .End() 来消除警告。

     UILabel *someLabel = ...;
    ...
    someLabel.str(newString).fitSize; //Warning: Property access result unused

    someLabel.str(newString).fitSize.End(); //no more warning
  2. 尽可能的使用 id,如果后续不需要再访问某个变量的属性,定义为 id 可以减少不少代码。

  3. 多考虑使用 NSAttributedString。因为 AttStr() 的存在,使得创建 NSAttributedString 变得非常简单。并且系统控件早就全面的支持 NSAttributedString 了。

  4. 学会使用 StackView 或 LinearLayout 的方式来思考问题,即同时对几个视图进行布局而不是对每个视图单独进行布局。

  5. 学会使用特殊字符和表情符号,有一些图标乍一看像是图片,但是其实是可以使用特殊字符或表情来表示的。Unicode 提供了非常多的特殊字符,像是 ⚽︎♠︎♣︎☁︎☃☆★⚾︎◼︎▶︎✔︎✖︎♚✎✿✪ 等等,最重要的一点是这些图标就像普通文字一样可以改变大小和颜色。

  6. 如果发现有一些属性没找到,请更新到最新版本。

收起阅读 »

为什么的我的z-index不生效了??

最近开发时遇到了一个有趣的现象z-index&transform等连用造成了z-index不生效,因此想借此机会记录一下学习成果。本篇文章偏概念性,请在专业人士的监督下食用。Stacking Context 层叠上下文这是 HTML 中的一个三维概念(...
继续阅读 »

最近开发时遇到了一个有趣的现象z-index&transform等连用造成了z-index不生效,因此想借此机会记录一下学习成果。

本篇文章偏概念性,请在专业人士的监督下食用。

Stacking Context 层叠上下文

这是 HTML 中的一个三维概念(举个不太合适的🌰类似皮影戏?)用图片更容易明白。

假设这是我们看到的一个页面(左图),但是实际上他是通过这样展现的(右图),我们肉眼所见的画面可能是多个层叠加展示的,由此就会涉及到不同层的层级问题。

stacking context - mdn - 链接

Stacking Context 的创建

这里只简单介绍一下常用的 Stacking Context 。

  1. 天生具有 Stacking Context 的元素: HTML

  2. 常用混搭款:

    1. position的值为relative/absolute && z-index !== 'auto'会产生 Stacking Context ;
    2. position的值为fixed/sticky
    3. display: flex/inline-flex/grid && z-index !== 'auto'
    4. transform/filter !== 'none'
    5. opacity < 1
    6. -webkit-overflow-scrolling: touch

Stacking Context 的关系

  1. Stacking Context 可以嵌套,但内部的 Stacking Context 将受制于外部的 Stacking Context;
  2. Stacking Context 和兄弟元素相互独立;
  3. 元素的层叠次序是被包含在父元素的 Stacking Context 中的;
  4. 通常来说,最底层的 Stacking Context 是 <HTML>标签创建的。

层叠次序

在一个层叠上下文内,不同元素的层叠次序如下图所示(由内到外):

  1. 最底层的是当前层叠上下文的装饰性内容,如背景颜色、边框等;
  2. 其次是负值的 z-index;
  3. 然后是布局相关的内容,如块状盒子、浮动盒子;
  4. 接着是内容相关的元素,如inline水平盒子;
  5. 再接着是 z-index:auto/0/不依赖 z-index 的(子)层叠上下文;
  6. 最上面的就是 z-index 值为正的元素;

概括来说,z-index为正 > z-index:auto/z-index:0/不依赖z-index的层叠上下文 > 内容 > 布局 > 装饰。

选自张鑫旭《CSS世界》图7-7

回应标题:为什么我的 z-index 不生效了?

这个标题内容非常的宽泛,我们提供如下的解题思路:

  1. 是否配合使用了可用的 postion / 弹性布局 / grid布局?
    1. 没配合自然不生效。
  2. z-index: -1; 为什么没有生效?
    1. 检查你对应的父元素是否也创建了 Stacking Context,大概率是的,根据 #层叠顺序那一章可以知道,负的z-index的次序是高于当前层叠上下文的背景的;
    2. 解决方案:取消父元素的 Stacking Context / 元素外包裹一层新的元素。

作者:vivi_chen
链接:https://juejin.cn/post/7028858045882957838

收起阅读 »

【译】3 个能优化网站可用性但被忽视的细节

根据 Adobe 的调查显示,给定 15 分钟的时间浏览内容,三分之二的用户更愿意将时间花费在视觉上吸引人的内容。用户也希望网站能在至少 5 秒内加载。因此,设计一个速度快、满意度高的网站(或应用)应成为每个设计师关注的重点。 视觉设计是很难被忽视的,因为我们...
继续阅读 »

根据 Adobe 的调查显示,给定 15 分钟的时间浏览内容,三分之二的用户更愿意将时间花费在视觉上吸引人的内容用户也希望网站能在至少 5 秒内加载。因此,设计一个速度快、满意度高的网站(或应用)应成为每个设计师关注的重点。


视觉设计是很难被忽视的,因为我们作为设计师,喜欢设计视觉上吸引人的东西。虽然美学非常重要,但在有限的时间内,设计师往往倾向于放弃可用性。优化应用/网站的可用性需要你深入地了解客户的目标。网站的可以性可以通过不同的方式来衡量,举例如下:



  1. UI 的清晰度有多高?

  2. 页面上的“障碍“有多少?

  3. 导航是否遵循逻辑结构?


让我们谈谈这些可以提高可用性的细节。


1. 更少的选择


作出选择是耗费精力的,所以为用户理清或甚至是排除不必要的障碍能减少所谓的分析瘫痪。分析瘫痪是指用户因为要考虑的选择太多而感到困惑或沮丧。



根据心理学家 Mark Lepper 和 Sheena Iyengar 的研究指出,更多的选择往往会导致更少的销售额。他们分析了 754 名消费者在面临多种选择时的行为,研究是这样进行的:


在第一天,他们在一家高档食品超市中摆了 24 种果酱,但在第二天,桌子上只摆有 6 种果酱。结果显示摆有 24 种果酱的桌子收到了更多的关注,但第二天的销量却比第一天的来得更好。这个现象可以用一句话来说明:“选择泛滥(spoilt for choice)”。


过量的选择将导致分析瘫痪。用户无法做出决策,因为他们面临太多的选择了。当你考虑那些各种各样的果酱时,它们之中可能某些比较便宜,而另一些可能味道更好等等。我们的大脑将尝试“解码”哪个选择最物有所值。这需要时间和思考,所以结果是转换的速度降低了,甚至导致用户放弃做出选择。



深入阅读:希克定律。希克定律指出,做出决定所需的时间会随着选项的数量增加而增加。该定律证实了减少选择数量能提高转化率这个概念。正如他们所说,“少即是多”



解决方法:个性化的内容


预期设计(由 Spotify,Netflix 和 Google 采用)能帮助使用者减少决策疲劳,其中人工智能(AI)能用于预测用户想要什么。应用和网站展示的“最受欢迎”的栏目就是例子之一,背后的逻辑是:因为其他的用户对这件商品感兴趣,所以你也可能对它感兴趣。


对于零售网站来说,另一种方式是整合“最畅销的商品”或“心愿单”,例如亚马逊的“购买此商品的客户也购买了……”推荐引擎。



冷知识:亚马逊的推荐引擎占其总收入的 30%。人工智能根据用户搜索历史和购物篮中的商品来预测用户想要购买的商品。




2. 极简导航


对于包含多个类别和子类的的网站,导航应成为用户体验(UX)的重中之重,尤其是在移动设备上。移动端网站难以导航,更容易导致分析瘫痪。


为了提升可用性,菜单中包含的项目数量应维持在 7 个以内(这同样适用于下拉菜单)。这样做还能更容易地指示用户所在的位置,降低用户跳出率。


为什么?因为用户时常会忘记他们之前在做什么,尤其是当他们打开多个标签页的时候!


3. 在导航中显示当前位置


进度追踪器能指示用户在界面中的当前位置。根据 Kantar 和 Lightspeed Research 的研究指出,这些指示器能提高用户参与度和客户满意度。


典型的网络冲浪者(或应用程序用户)通常会在一时之内打开多个标签页(或应用程序),因此他们很容易忘记在某个标签页中未完成的任务。有时侯,分析瘫痪的困境是由用户自己造成的!


设计师应该意识到他们的应用程序或网站不会是用户使用的唯一应用程序或网站,当用户面临太多打开着的标签页时,这通常会导致健忘。一个标注用户所在位置的指示器是非常有帮助的。否则,用户可能不仅会忘记他们在做什么,而且会完全不再注意它。


解决方法:面包屑导航


面包屑用于表示用户在哪里,以及他们来自哪里。你可能听说过《汉赛尔和格莱特》这个经典童话故事,这对兄妹用面包屑帮助他们找到回家的路,也避免了他们在森林中绕圈子。


面包屑导航能描述用户的路径。你在亚马逊,NewEgg 和其他的一些需要展示大量内容的线上零售网站都能看到这一点。这能帮助用户记得他们上次所在的位置(如果他们中途因任何原因离开屏幕),并帮助他们在遇到死胡同时找到回去的路。



结论


总的来说,你可以通过帮助用户专注于重要的事情来有效的提高网站的可用性; 温和地引导他们,在必要时进行总结,并优化用户体验以确保用户能找到他们想找的东西。


作者:披着狼皮的羊_
链接:https://juejin.cn/post/7028491022107672613

收起阅读 »

(转载)元宇宙 Metaverse

元宇宙为什么会这么火?有人说它是未来的趋势,有人说它是资本家为了割韭菜炒作的概念。元宇宙到底是什么? 为什么大量资本对它趋之若鹜?元宇宙是什么你还记得电影《头号玩家》里的“绿洲”么,那大概就是元宇宙的真实写照。2018年,美国科幻冒险片《头号玩家》上映,没有看...
继续阅读 »


元宇宙为什么会这么火?有人说它是未来的趋势,有人说它是资本家为了割韭菜炒作的概念。元宇宙到底是什么? 为什么大量资本对它趋之若鹜?

元宇宙是什么

你还记得电影《头号玩家》里的“绿洲”么,那大概就是元宇宙的真实写照。

2018年,美国科幻冒险片《头号玩家》上映,没有看过的同学,强烈建议看一下。

当前概念的元宇宙代表了,「互联网」未来将要迭代发展出来的,一个可以「被感知」的、「持久存在」的、大家共享的、存在3D虚拟空间的「虚拟宇宙」

元宇宙一词是由Metaverse翻译而来。meta作为前缀通常表示“超出、超越的意思,翻译成中文可以是“超、元”。verse代表宇宙,是通过去除universe(宇宙)的前缀uni而来的。Metaverse翻译成中文就是超越现实宇宙的意思,可以翻译为元宇宙或者超宇宙,现在绝大多数人都叫元宇宙

Metaverse这个英文单词是由国外作家尼尔斯·史蒂芬森(Neal Stephenson)于1992年,在他的科幻小说《雪崩》(Snow Crash)中创造的。小说中,人类在一个虚拟的三维空间中,通过自己的化身互相交流。史蒂芬森在雪崩的后记写到,完成小说后,他还体验了一款类似于元宇宙的角色扮演网络游戏,Habitat。在更早的1982年就出现了网络空间(cyberspace)的概念,Metaverse可以看作是对这些概念的延伸和发展。

Metaverse不同于当前的大型多人在线游戏,相比当前主流的在线游戏,它持久存在,更加真实,与现实世界更加接近。

元宇宙的现状

2021年发生了很多和元宇宙相关的事:

沙盒游戏平台Roblox是第一个将“元宇宙”概念写进招股书的公司,该公司今年内成功股登陆纽交所上市,上市首日市值突破400亿美元。

腾讯和Roblox合作,代理游戏《罗布乐思》已在国内发型。

韩国宣布成立元宇宙联盟,励志打造全国统一的VR、AR平台。

Facebook宣布开发元宇宙,扎克伯格表示未来要把Facebook打造成一个元宇宙公司。

微软发布依托其智能眼镜-HoloLens2的虚拟软件Microsoft Mesh。

游戏公司Epic Games决定把Fortnite打造成一款元宇宙游戏。

小米正式发布了小米智能眼镜探索版。

字节投资的代码乾坤,也在今年正式上线了元宇宙游戏《重启世界》。

英伟达CEO黄仁勋宣布英伟达将布局元宇宙业务。
...

当前各大巨头都纷纷布局元宇宙,但是大家参与形式不尽相同。像微软、苹果、小米等推出智能穿戴设备,大家可以通过智能眼镜感受虚拟世界;一些游戏公司推出各种角色扮演类的游戏和游戏的穿戴设备,试图通过游戏打造一个虚拟世界;有的公司通过社交,创造更加真实、立体的聊天互动场景来入局元宇宙;有的公司推出虚拟工作空间,让大家在家里可以感受到和办公室一样的工作场景...

元宇宙需要的技术

元宇宙要成为现实离不开互联网硬件、软件、网络的发展。

元宇宙中接近真实场景的用户体验,需要更加丰富、智能的传感器,把人的感官和数字世界打通。比如戴上智能眼镜能让人看到虚拟世界;HaptX研发的反馈手套,可以制造出触摸感;在味觉和嗅觉方面的传感器技术发展还比较慢,距离模拟真实体验还需要有重大突破。真正的元宇宙还需要更多设备,触达人的更多感官,提供全方位的体验。

元宇宙需要VR、AR、MR等技术,去描述虚拟世界,让虚拟的3D世界更加真实生动。沉浸式的体验需要更加真实的环境,不仅仅是眼睛看到的场景要真实,还包括耳朵听到的,身体感受到的,全方位的3D立体效果才能带来身临其境的感觉。

元宇宙中虚拟经济如何调控。如何定义虚拟物品的价值,大家如何交易,虚拟世界的房价谁来调控,谁来管理虚拟世界的通货膨胀?区块链技术或许是很好的选择之一,但完全解决虚拟经济发展的所有问题还需要更多的规则和指导方法。

元宇宙的持久性需要超大规模数据的持久化存储,这对我们的实时读写技术,持久化技术,存储设备都是提出了新的要求。

网路方面,更加真实的虚拟场景、交互体验,势必会有更大规模数据的传输。5G何时全面普及,6G时代何时到来,Web3.0如何落地,都是我们面临的问题和挑战。

总结

元宇宙的完整愿景还没有被完全定义,我们距离元宇宙还有多远,尚未可知,但是不管元宇宙一词是噱头还是理想,它背后代表的互联网技术肯定是会不断的发展和进步;而新技术,往往都首先在最具商业价值的场景中落地。在个别场景落地并发展成熟之后,会不断延伸最终遍布到生活的每一个角落。

就像三次工业革命一样,新的技术必然要改变人类的生活方式,未来可期。

当然,关心元宇宙的同时,我们也要立足当下,关心现实的宇宙。

原文链接:https://juejin.cn/post/7012091658049159181

收起阅读 »

(转载)腾讯、网易纷纷出手,火到出圈的元宇宙到底是个啥?

本文首发于:行者AI最近,一个叫“元宇宙”的概念火到出圈,腾讯、网易、字节跳动、Facebook等各路大佬纷纷入局,甚至连社交软件Soul也称自己是给Z世代提供以灵魂为链接的社交元宇宙。元宇宙到底是个啥?各路资本又为何如此疯狂?这还要从1992年出版的科幻小说...
继续阅读 »

本文首发于:行者AI

最近,一个叫“元宇宙”的概念火到出圈,腾讯、网易、字节跳动、Facebook等各路大佬纷纷入局,甚至连社交软件Soul也称自己是给Z世代提供以灵魂为链接的社交元宇宙。

元宇宙到底是个啥?各路资本又为何如此疯狂?这还要从1992年出版的科幻小说《雪崩》说起。

《雪崩》作者尼尔·斯蒂芬森构想了一个与社会紧密相连的三维数字空间。这个空间与现实世界平行,只要戴上耳机和目镜,找到一个终端,就可以通过连接进入一个由计算机模拟的三维现实。这里,每个人都可以拥有自己的分身(avatar),你可以定义自己分身的形象,可以通过分身聊天、斗剑、交友,还可以随意支配自己的收入。

这个虚拟的三维现实就是元宇宙(Metaverse)的雏形。

2018年,电影《头号玩家》将元宇宙的概念搬上了大银幕。这次,元宇宙换了一个名字——绿洲。

This is the "oasis" world, where the only limit is your own imagination. 这里是“绿洲”世界,在这里唯一限制你的是你自己的想象力。

“绿洲”脱胎于现实世界,又独立于现实世界。它有自己的运行逻辑,现实中有的,绿洲有;现实中不可能发生的事情,绿洲里却可能发生。只要你想,你可以化身高达、金刚、鬼娃恰奇,可以和小丑共舞,和春丽战斗,和蝙蝠侠一起勇登珠峰。就算现实生活中,你是个彻头彻尾的失败者,在绿洲里,你也有机会成为鲜衣怒马的少年英雄,一日看尽长安繁华。

元宇宙给了玩家无限的可能,这或许就是元宇宙的魅力,也是元宇宙吸引无数资本圈的大佬入局的原因。正如Roblox CEO Dave Baszucki曾说过的那样:“Metaverse是科幻作家和未来主义者构想了超过30年的事情。而现在,随着拥有强大算力的设备的逐步普及,网络带宽的提升,实现Metaverse的时机已经趋于成熟”。

在Baszucki眼中,元宇宙是一个我们可以工作、玩耍、娱乐的线上空间。元宇宙具有8个关键的特征:Identity(身份)、Friends(朋友)、Immersiveness(沉浸感)、Low Friction(低延迟)、Variety(多样性)、Anywhere(随地)、Economy(经济)、Civility(文明)。

首先你需要一个虚拟的身份,你可以是摇滚明星也可以是时尚model,你可以在元宇宙中进行社交,不管是跟已有的朋友联系还是认识新的朋友。这必须是极具沉浸感的,甚至能让你忽略现实世界,元宇宙内的一切都是同步发生的,没有延迟性,丰富的差异化内容保证了这个虚拟世界的吸引力,你可以随时随地的进入这个世界,无论你在床上还是地铁里。此外,这个世界应该具有自己的一套经济系统以支撑它的独立运行,也必然拥有自己的文明体系促使它的不断发展。

尽管与《头号玩家》中的“绿洲”相比,Roblox的画面并没有真实世界那么逼真,一眼看上去,会觉得这不就是一个乐高沙盒游戏?但Roblox为用户提供了一个自由创造的平台,玩家可以发挥自己的想象力设计游戏人物形象,构建新的游戏模式,创造自己的世界或是加入别人的世界。

所有Roblox的用户都可以是这款游戏的开发者,这种用户创造游戏与社群形成强互动的运行模式保证了用户的稳定增长,也使得Roblox难以复制,这让资本圈的大佬看到了Roblox或者说元宇宙在游戏未来发展中的巨大价值。

2020年3月,沙盒游戏平台Roblox在美国纽交所上市。作为首个将“元宇宙”写入招股说明书的公司,Roblox上市首日开盘价45美元,当日涨幅54.4%,市值突破400亿美元。

无独有偶,2021年4月,《堡垒之夜》的开发商Epic Games宣布获得10亿美金的巨额融资,主要用于元宇宙的研发。

与创意游戏平台Roblox相同,《堡垒之夜》同样具备元宇宙的倾向,但是这款游戏的娱乐性更强。在《堡垒之夜》中,玩家具有极高的自由度,可以重新搭建游戏场景,可以化身漫威超级英雄守卫堡垒世界,还可以听Travis Scott的虚拟演唱会,看诺兰的《盗梦空间》……以Travis Scott的虚拟演唱会为例,这场大约10分钟的沉浸式线上演出,吸引了超过1200万名玩家的同时参与,场次横跨亚洲、欧洲、大洋洲、美国等各个地区。这次演唱会不断变换的演唱场地、炫酷的游戏特效、超千万的观众人数,都不是任何一场线下演唱会可以比拟的。

值得一提的是,早在2019年,腾讯就与Roblox达成战略合作,而在2020年2月Roblox获得的1.5亿美元G轮融资中也有腾讯参投,此外,腾讯还握有Epic Games 40%的股份,《艾兰岛》、《乐高无限》等沙盒游戏的背后也有腾讯的身影。由此可见,腾讯对于打赢这场元宇宙战役的决心。

除腾讯外,网易、莉莉丝、字节跳动等大厂也在加快元宇宙的布局。网易的河狸计划,莉莉丝的达芬奇 UGC 平台、字节跳动对于《重启世界》的投资……都显示出大厂对于元宇宙的重视。VR、AR、5G、云计算、区块链等技术的逐渐成熟使元宇宙这个疯狂的、存在于科幻小说中的模糊概念有了实现的可能。

元宇宙确实会发生,但它还非常遥远,它很难被一家超级大公司独立搭建,而是需要更多的参与方与无数的内容创作者共同探索。科幻小说之父儒勒·凡尔纳曾说过“但凡人能想象到的事物,必定有人能将它实现”,我们期待着元宇宙的出现。

原文链接:https://juejin.cn/post/6987312286666850317

收起阅读 »

setTimeout的执行你真的了解吗?

setTimeout的创建和执行 我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。 首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了...
继续阅读 »

setTimeout的创建和执行


我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。


首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了保证setTimeout能够在规定的时间内执行,setTimeout创建的任务不会被添加到消息队列里,与此同时,浏览器还维护了另外一个队列叫做延迟消息队列,该队列就是用来存放延迟任务,setTimeout创建的任务会被存放于此,同时它会被记住创建时间,延迟执行时间。


然后我们看下具体例子:


setTimeout(function showName() { console.log('showName') }, 1000)
setTimeout(function showName() { console.log('showName1') }, 1000)
console.log('martincai')

以上例子执行是这样:



  • 1.从消息队列中取出宏任务进行执行(首次任务直接执行)

  • 2.执行setTimeout,此时会创建一个延迟任务,延迟任务的回调函数是showName,发起时间是当前的时间,延迟时间是第二个参数1000ms,然后该延迟任务会被推入到延迟任务队列

  • 3.执行console.log('martincai')代码

  • 4.从延迟队列里去筛选所有已过期任务(当前时间 >= 发起时间 + 延迟时间),然后依次执行


所以我们可以看到showName和showName1同时执行,原因是两个延迟任务都已经过期


循环源码:


void MainTherad(){
for(;;){
// 执行消息队列中的任务
Task task = task_queue.takeTask();
ProcessTask(task);

// 执行延迟队列中的任务
ProcessDelayTask()

if(!keep_running) // 如果设置了退出标志,那么直接退出线程循环
break;
}
}

删除延迟任务


clearTimeout是windows下提供的原生方法,用于删除特定的setTimeout的延迟任务,我们在定义setTimeout的时候返回值就是当前任务的唯一id值,那么clearTimeout就会拿着id在延迟消息队列里查找对应的任务,将其踢出队列即可


setTimeout的几个注意点:



  1. setTimeout会受到消息队列里的宏任务的执行时间影响,上面我们可以看到延迟消息队列的任务会在消息队列的弹出的当前任务执行完之后再执行,所以当前任务的执行时间会阻碍到setTimeout的延迟任务的执行时间


  function showName() {
setTimeout(function show() {
console.log('show')
}, 0)
for (let i = 0; i <= 5000; i++) {}
}
showName()

这里去执行一遍可以发现setTimeout并不是在0ms左右执行,中间会有明显的延迟,因为setTimeout在执行的时候首先会将任务放入到延迟消息队列里,等到showName执行完之后,才会去延迟队列里去查找已过期的任务,这里setTimeout任务会被showName耽误



  1. setTimeout嵌套下会有4ms的延迟


Chrome会把嵌套5层以上的setTimeout后当作阻塞方法,在第6次调用setTimeout的时候会自动将延时器更改为至少4ms的延迟时间



  1. 未激活的页面的setTimeout更改为至少1000ms


当前tab页面不在active状态的时候,setTimeout的延迟至少会被更改1000ms,这样做是为了减少性能消耗和电量消耗



  1. 延迟时间有最大值


目前Chrome、Firefox等主流浏览器都是用32bit去存储延时时间,所以最大值是2的31次方 - 1


  setTimeout(() => {
console.log(1)
}, 2 ** 31)

以上代码会立即执行


链接:https://juejin.cn/post/7028836586745757710
收起阅读 »

从22行有趣的源码库中,我学到了 callback promisify 化的 Node.js 源码实现

我们经常会在本地git仓库切换tags,或者git仓库切换tags。那么我们是否想过如果获取tags呢。本文就是学习 remote-git-tags 这个22行代码的源码库。源码不多,但非常值得我们学习。 阅读本文,你将学到: 1. Node 加载采用什么模块...
继续阅读 »

我们经常会在本地git仓库切换tags,或者git仓库切换tags。那么我们是否想过如果获取tags呢。本文就是学习 remote-git-tags 这个22行代码的源码库。源码不多,但非常值得我们学习。


阅读本文,你将学到:


1. Node 加载采用什么模块
2. 获取 git 仓库所有 tags 的原理
3. 学会调试看源码
4. 学会面试高频考点 promisify 的原理和实现
5. 等等

刚开始先不急着看上千行、上万行的源码。源码长度越长越不容易坚持下来。看源码讲究循序渐进。比如先从自己会用上的百来行的开始看。


我之前在知乎上回答过类似问题。


一年内的前端看不懂前端框架源码怎么办?


简而言之,看源码


循序渐进
借助调试
理清主线
查阅资料
总结记录

2. 使用


import remoteGitTags from 'remote-git-tags';

console.log(await remoteGitTags('https://github.com/lxchuan12/blog.git'));
//=> Map {'3.0.5' => '6020cc35c027e4300d70ef43a3873c8f15d1eeb2', …}

3. 源码



Get tags from a remote Git repo



这个库的作用是:从远程仓库获取所有标签。


原理:通过执行 git ls-remote --tags repoUrl (仓库路径)获取 tags


应用场景:可以看有哪些包依赖的这个包。
npm 包描述信息


其中一个比较熟悉的是npm-check-updates



npm-check-updates 将您的 package.json 依赖项升级到最新版本,忽略指定的版本。



还有场景可能是 github 中获取所有 tags 信息,切换 tags 或者选定 tags 发布版本等,比如微信小程序版本。


看源码前先看 package.json 文件。


3.1 package.json


// package.json
{
// 指定 Node 以什么模块加载,缺省时默认是 commonjs
"type": "module",
"exports": "./index.js",
// 指定 nodejs 的版本
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"scripts": {
"test": "xo && ava"
}
}

众所周知,Node 之前一直是 CommonJS 模块机制。 Node 13 添加了对标准 ES6 模块的支持。


告诉 Node 它要加载的是什么模块的最简单的方式,就是将信息编码到不同的扩展名中。
如果是 .mjs 结尾的文件,则 Node 始终会将它作为 ES6 模块来加载。
如果是 .cjs 结尾的文件,则 Node 始终会将它作为 CommonJS 模块来加载。


对于以 .js 结尾的文件,默认是 CommonJS 模块。如果同级目录及所有目录有 package.json 文件,且 type 属性为module 则使用 ES6 模块。type 值为 commonjs 或者为空或者没有 package.json 文件,都是默认 commonjs 模块加载。


关于 Node 模块加载方式,在《JavaScript权威指南第7版》16.1.4 Node 模块 小节,有更加详细的讲述。此书第16章都是讲述Node,感兴趣的读者可以进行查阅。


3.2 调试源码


# 推荐克隆我的项目,保证与文章同步,同时测试文件齐全
git clone https://github.com/lxchuan12/remote-git-tags-analysis.git
# npm i -g yarn
cd remote-git-tags && yarn
# VSCode 直接打开当前项目
# code .

# 或者克隆官方项目
git clone https://github.com/sindresorhus/remote-git-tags.git
# npm i -g yarn
cd remote-git-tags && yarn
# VSCode 直接打开当前项目
# code .

用最新的VSCode 打开项目,找到 package.jsonscripts 属性中的 test 命令。鼠标停留在test命令上,会出现 运行命令调试命令 的选项,选择 调试命令 即可。


调试如图所示:


调试如图所示


VSCode 调试 Node.js 说明如下图所示:


VSCode 调试 Node.js 说明


跟着调试,我们来看主文件。


3.3 主文件仅有22行源码


// index.js
import {promisify} from 'node:util';
import childProcess from 'node:child_process';

const execFile = promisify(childProcess.execFile);

export default async function remoteGitTags(repoUrl) {
const {stdout} = await execFile('git', ['ls-remote', '--tags', repoUrl]);
const tags = new Map();

for (const line of stdout.trim().split('\n')) {
const [hash, tagReference] = line.split('\t');

// Strip off the indicator of dereferenced tags so we can override the
// previous entry which points at the tag hash and not the commit hash
// `refs/tags/v9.6.0^{}` → `v9.6.0`
const tagName = tagReference.replace(/^refs\/tags\//, '').replace(/\^{}$/, '');

tags.set(tagName, hash);
}

return tags;
}

源码其实一眼看下来就很容易懂。


3.4 git ls-remote --tags


支持远程仓库链接。


git ls-remote 文档


如下图所示:


ls-remote


获取所有tags git ls-remote --tags https://github.com/vuejs/vue-next.git


把所有 tags 和对应的 hash值 存在 Map 对象中。


3.5 node:util


Node 文档



Core modules can also be identified using the node: prefix, in which case it bypasses the require cache. For instance, require('node:http') will always return the built in HTTP module, even if there is require.cache entry by that name.



也就是说引用 node 原生库可以加 node: 前缀,比如 import util from 'node:util'


看到这,其实原理就明白了。毕竟只有22行代码。接着讲述 promisify


4. promisify


源码中有一段:


const execFile = promisify(childProcess.execFile);

promisify 可能有的读者不是很了解。


接下来重点讲述下这个函数的实现。


promisify函数是把 callback 形式转成 promise 形式。


我们知道 Node.js 天生异步,错误回调的形式书写代码。回调函数的第一个参数是错误信息。也就是错误优先。


我们换个简单的场景来看。


4.1 简单实现


假设我们有个用JS加载图片的需求。我们从 这个网站 找来图片。


examples
const imageSrc = 'https://www.themealdb.com/images/ingredients/Lime.png';

function loadImage(src, callback) {
const image = document.createElement('img');
image.src = src;
image.alt = '公众号若川视野专用图?';
image.style = 'width: 200px;height: 200px';
image.onload = () => callback(null, image);
image.onerror = () => callback(new Error('加载失败'));
document.body.append(image);
}

我们很容易写出上面的代码,也很容易写出回调函数的代码。需求搞定。


loadImage(imageSrc, function(err, content){
if(err){
console.log(err);
return;
}
console.log(content);
});

但是回调函数有回调地狱等问题,我们接着用 promise 来优化下。


4.2 promise 初步优化


我们也很容易写出如下代码实现。


const loadImagePromise = function(src){
return new Promise(function(resolve, reject){
loadImage(src, function (err, image) {
if(err){
reject(err);
return;
}
resolve(image);
});
});
};
loadImagePromise(imageSrc).then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
});

但这个不通用。我们需要封装一个比较通用的 promisify 函数。


4.3 通用 promisify 函数


function promisify(original){
function fn(...args){
return new Promise((resolve, reject) => {
args.push((err, ...values) => {
if(err){
return reject(err);
}
resolve(values);
});
// original.apply(this, args);
Reflect.apply(original, this, args);
});
}
return fn;
}

const loadImagePromise = promisify(loadImage);
async function load(){
try{
const res = await loadImagePromise(imageSrc);
console.log(res);
}
catch(err){
console.log(err);
}
}
load();

需求搞定。这时就比较通用了。


这些例子在我的仓库存放在 examples 文件夹中。可以克隆下来,npx http-server .跑服务,运行试试。


examples


跑失败的结果可以把 imageSrc 改成不存在的图片即可。


promisify 可以说是面试高频考点。很多面试官喜欢考此题。


接着我们来看 Node.js 源码中 promisify 的实现。


4.4 Node utils promisify 源码


github1s node utils 源码


源码就暂时不做过多解释,可以查阅文档。结合前面的例子,其实也容易理解。


utils promisify 文档


const kCustomPromisifiedSymbol = SymbolFor('nodejs.util.promisify.custom');
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');

let validateFunction;

function promisify(original) {
// Lazy-load to avoid a circular dependency.
if (validateFunction === undefined)
({ validateFunction } = require('internal/validators'));

validateFunction(original, 'original');

if (original[kCustomPromisifiedSymbol]) {
const fn = original[kCustomPromisifiedSymbol];

validateFunction(fn, 'util.promisify.custom');

return ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
}

// Names to create an object from in case the callback receives multiple
// arguments, e.g. ['bytesRead', 'buffer'] for fs.read.
const argumentNames = original[kCustomPromisifyArgsSymbol];

function fn(...args) {
return new Promise((resolve, reject) => {
ArrayPrototypePush(args, (err, ...values) => {
if (err) {
return reject(err);
}
if (argumentNames !== undefined && values.length > 1) {
const obj = {};
for (let i = 0; i < argumentNames.length; i++)
obj[argumentNames[i]] = values[i];
resolve(obj);
} else {
resolve(values[0]);
}
});
ReflectApply(original, this, args);
});
}

ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));

ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
return ObjectDefineProperties(
fn,
ObjectGetOwnPropertyDescriptors(original)
);
}

promisify.custom = kCustomPromisifiedSymbol;

5. ES6+ 等知识


文中涉及到了Mapfor of、正则、解构赋值。


还有涉及封装的 ReflectApplyObjectSetPrototypeOfObjectDefinePropertyObjectGetOwnPropertyDescriptors 等函数都是基础知识。



作者:若川
链接:https://juejin.cn/post/7028731182216904740

收起阅读 »

3D 穿梭效果?使用 CSS 轻松搞定

背景 周末在家习惯性登陆 Apex,准备玩几盘。在登陆加速器的过程中,发现加速器到期了。 我一直用的腾讯网游加速器,然而点击充值按钮,提示最近客户端升级改造,暂不支持充值(这个操作把我震惊了~)。只能转头下载网易 UU 加速器。 打开 UU 加速器首页,映入眼...
继续阅读 »

背景


周末在家习惯性登陆 Apex,准备玩几盘。在登陆加速器的过程中,发现加速器到期了。


我一直用的腾讯网游加速器,然而点击充值按钮,提示最近客户端升级改造,暂不支持充值(这个操作把我震惊了~)。只能转头下载网易 UU 加速器


打开 UU 加速器首页,映入眼帘的是这样一幅画面:


11.gif


瞬间,被它这个背景图吸引。


出于对 CSS 的敏感,盲猜了一波这个用 CSS 实现的,至少也应该是 Canvas。打开控制台,稍微有点点失望,居然是一个 .mp4文件:



再看看 Network 面板,这个 .mp4 文件居然需要 3.5M?



emm,瞬间不想打游戏了。这么个背景图,CSS 不能搞定么


使用 CSS 3D 实现星际 3D 穿梭效果


这个技巧,我在 奇思妙想 CSS 3D 动画 | 仅使用 CSS 能制作出多惊艳的动画? 也有提及过,感兴趣的可以一并看看。


假设我们有这样一张图形:



这张图先放着备用。在使用这张图之前,我们会先绘制这样一个图形:


<div class="g-container">
<div class="g-group">
<div class="item item-right"></div>
<div class="item item-left"></div>
<div class="item item-top"></div>
<div class="item item-bottom"></div>
<div class="item item-middle"></div>
</div>
</div>

body {
background: #000;
}
.g-container {
position: relative;
}
.g-group {
position: absolute;
width: 100px;
height: 100px;
left: -50px;
top: -50px;
transform-style: preserve-3d;
}
.item {
position: absolute;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, .5);
}
.item-right {
background: red;
transform: rotateY(90deg) translateZ(50px);
}
.item-left {
background: green;
transform: rotateY(-90deg) translateZ(50px);
}
.item-top {
background: blue;
transform: rotateX(90deg) translateZ(50px);
}
.item-bottom {
background: deeppink;
transform: rotateX(-90deg) translateZ(50px);
}
.item-middle {
background: rgba(255, 255, 255, 0.5);
transform: rotateX(180deg) translateZ(50px);
}

一共设置了 5 个子元素,不过仔细看 CSS 代码,其中 4 个子元素都设置了 rotateX/Y(90deg/-90deg),也就是绕 X 轴或者 Y 轴旋转了 90°,在视觉上是垂直屏幕的一张平面,所以直观视觉上我们是不到的,只能看到一个平面 .item-middle


我将 5 个子 item 设置了不同的背景色,结果如下:



现在看来,好像平平无奇,确实也是。


不过,见证奇迹的时候来了,此时,我们给父元素 .g-container 设置一个极小的 perspective,譬如,设置一个 perspective: 4px,看看效果:


.g-container {
position: relative;
+ perspective: 4px;
}
// ...其余样式保持不变

此时,画风骤变,整个效果就变成了这样:



由于 perspective 生效,原本的平面效果变成了 3D 的效果。接下来,我们使用上面准备好的星空图,替换一下上面的背景颜色,全部都换成同一张图,神奇的事情发生了:



由于设置的 perspective 非常之下,而每个 item 的 transform: translateZ(50px) 设置的又比较大,所以图片在视觉上被拉伸的非常厉害。但是整体是充满整个屏幕的。


接下来,我们只需要让视角动起来,给父元素增加一个动画,通过控制父元素的 translateZ() 进行变化即可:


.g-container{
position: relative;
perspective: 4px;
perspective-origin: 50% 50%;
}

.g-group{
position: absolute;
// ... 一些定位高宽代码
transform-style: preserve-3d;
+ animation: move 8s infinite linear;
}

@keyframes move {
0%{
transform: translateZ(-50px) rotate(0deg);
}
100%{
transform: translateZ(50px) rotate(0deg);
}
}


看看,神奇美妙的星空穿梭的效果就出来了,Amazing:



美中不足之处在于,动画没能无限衔接上,开头和结尾都有很大的问题。


当然,这难不倒我们,我们可以:



  1. 通过叠加两组同样的效果,一组比另一组通过负的 animation-delay 提前行进,使两组动画衔接起来(一组结束的时候另外一组还在行进中)

  2. 再通过透明度的变化,隐藏掉 item-middle 迎面飞来的突兀感

  3. 最后,可以通过父元素的滤镜 hue-rotate 控制图片的颜色变化


我们尝试修改 HTML 结构如下:


<div class="g-container">
<div class="g-group">
<div class="item item-right"></div>
<div class="item item-left"></div>
<div class="item item-top"></div>
<div class="item item-bottom"></div>
<div class="item item-middle"></div>
</div>
<!-- 增加一组动画 -->
<div class="g-group">
<div class="item item-right"></div>
<div class="item item-left"></div>
<div class="item item-top"></div>
<div class="item item-bottom"></div>
<div class="item item-middle"></div>
</div>
</div>


修改后的核心 CSS 如下:


.g-container{
perspective: 4px;
position: relative;
// hue-rotate 变化动画,可以让图片颜色一直变换
animation: hueRotate 21s infinite linear;
}

.g-group{
transform-style: preserve-3d;
animation: move 12s infinite linear;
}
// 设置负的 animation-delay,让第二组动画提前进行
.g-group:nth-child(2){
animation: move 12s infinite linear;
animation-delay: -6s;
}
.item {
background: url(https://z3.ax1x.com/2021/08/20/fLwuMd.jpg);
background-size: cover;
opacity: 1;
// 子元素的透明度变化,减少动画衔接时候的突兀感
animation: fade 12s infinite linear;
animation-delay: 0;
}
.g-group:nth-child(2) .item {
animation-delay: -6s;
}
@keyframes move {
0%{
transform: translateZ(-500px) rotate(0deg);
}
100%{
transform: translateZ(500px) rotate(0deg);
}
}
@keyframes fade {
0%{
opacity: 0;
}
25%,
60%{
opacity: 1;
}
100%{
opacity: 0;
}
}
@keyframes hueRotate {
0% {
filter: hue-rotate(0);
}
100% {
filter: hue-rotate(360deg);
}
}

最终完整的效果如下,星空穿梭的效果,整个动画首尾相连,可以一直无限下去,几乎没有破绽,非常的赞:



上述的完整代码,你可以猛击这里:CSS 灵感 -- 3D 宇宙时空穿梭效果


这样,我们就基本还原了上述见到的网易 UU 加速器首页的动图背景。


更进一步,一个图片我都不想用


当然,这里还是会有读者吐槽,你这里不也用了一张图片资源么?没有那张星空图行不行?这张图我也懒得去找。


当然可以,CSS YYDS。这里我们尝试使用 box-shadow,去替换实际的星空图,也是在一个 div 标签内实现,借助了 SASS 的循环函数:


<div></div>

@function randomNum($max, $min: 0, $u: 1) {
@return ($min + random($max)) * $u;
}

@function randomColor() {
@return rgb(randomNum(255), randomNum(255), randomNum(255));
}

@function shadowSet($maxWidth, $maxHeight, $count) {
$shadow : 0 0 0 0 randomColor();

@for $i from 0 through $count {
$x: #{random(10000) / 10000 * $maxWidth};
$y: #{random(10000) / 10000 * $maxHeight};


$shadow: $shadow, #{$x} #{$y} 0 #{random(5)}px randomColor();
}

@return $shadow;
}

body {
background: #000;
}

div {
width: 1px;
height: 1px;
border-radius: 50%;
box-shadow: shadowSet(100vw, 100vh, 500);
}

这里,我们用 SASS 封装了一个函数,利用多重 box-shadow 的特性,在传入的大小的高宽内,生成传入个数的点。


这样,我们可以得到这样一幅图,用于替换实际的星空图:



我们再把上述这个图,替换实际的星空图,主要是替换 .item 这个 class,只列出修改的部分:


// 原 CSS,使用了一张星空图
.item {
position: absolute;
width: 100%;
height: 100%;
background: url(https://z3.ax1x.com/2021/08/20/fLwuMd.jpg);
background-size: cover;
animation: fade 12s infinite linear;
}

// 修改后的 CSS 代码
.item {
position: absolute;
width: 100%;
height: 100%;
background: #000;
animation: fade 12s infinite linear;
}
.item::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 1px;
height: 1px;
border-radius: 50%;
box-shadow: shadowSet(100vw, 100vh, 500);
}

这样,我们就实现了这样一个效果,在不借助额外资源的情况下,使用纯 CSS 实现上述效果:



CodePen Demo -- Pure CSS Galaxy Shuttle 2


通过调整动画的时间,perspective 的值,每组元素的 translateZ() 变化距离,可以得到各种不一样的观感和效果,感兴趣的读者可以基于我上述给的 DEMO 自己尝试尝试。


作者:chokcoco
链接:https://juejin.cn/post/7028757824695959588

收起阅读 »

iOS 蓝牙设备名称缓存问题总结

1. 问题背景当设备已经在 App 中连接成功后修改设备名称App 扫描到的设备名称仍然是之前的名称App 代码中获取名称的方式为(perpheral.name)2. 问题分析当 APP 为中心连接其他的蓝牙设备时。首次连接成功过后,iOS系统内会将该外设缓存...
继续阅读 »

1. 问题背景

  1. 当设备已经在 App 中连接成功后
  2. 修改设备名称
  3. App 扫描到的设备名称仍然是之前的名称
  4. App 代码中获取名称的方式为(perpheral.name)

2. 问题分析

当 APP 为中心连接其他的蓝牙设备时。

首次连接成功过后,iOS系统内会将该外设缓存记录下来。

下次重新搜索时,搜索到的蓝牙设备时,直接打印 (peripheral.name),得到的是之前缓存中的蓝牙名称。

如果此期间蓝牙设备更新了名称,(peripheral.name)这个参数并不会改变,所以需要换一种方式获取设备的名称,在广播数据包内有一个字段为 kCBAdvDataLocalName,可以实时获取当前设备名称。

3. 问题解决

下面给出OC 和 Swift 的解决方法:

OC

-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI{
NSString *localName = [advertisementData objectForKey:@"kCBAdvDataLocalName"];
}

Swift

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
let localName = advertisementData["kCBAdvDataLocalName"]
}
收起阅读 »

元宇宙来了:虚拟办公场景最靠谱,Meta和微软争做“头号玩家”

我明明坐在家里办公,但开会的时候,为什么会有一个虚拟的老板坐在我旁边?这个听起来让人emo的场景,可能就是不少人好奇的“元宇宙”的一部分。10月28日,Facebook召开了一场有关“元宇宙”的Connect 2021大会。会上,马克·扎克伯格宣布Facebo...
继续阅读 »

我明明坐在家里办公,但开会的时候,为什么会有一个虚拟的老板坐在我旁边?这个听起来让人emo的场景,可能就是不少人好奇的“元宇宙”的一部分。

10月28日,Facebook召开了一场有关“元宇宙”的Connect 2021大会。会上,马克·扎克伯格宣布Facebook即日起改名为Meta,并宣告从“社交网站”向“元宇宙”业务进军。随即,“元宇宙”概念火爆网络。然而,到底什么是“元宇宙”呢?

“元宇宙离我们不远,我们从2017年就已经开始从事相关工作,行内很多公司也早已着手了。”上海境腾科技CTO(首席技术官)谢宾对南方财经全媒体记者表示。他告诉记者,在文章开头描述的那个场景,目前是“元宇宙”世界里最靠谱、最接近成为现实的一个场景。

境腾科技由几名前微软中国员工联合创立,目前专注于用VR(虚拟现实)和AR(增强现实)技术开发虚拟办公软件、远程培训软件等工作。包括虚拟会议在内,一个巨大的“元宇宙”生态圈正在悄然生长。

“目前流行的头戴设备,比如头显、智能眼镜等,仍然在拼技术阶段,正一步步进化。我们尚未触碰、但也聚集了不少人才和资金的VR游戏、VR影视,是‘元宇宙’内容成长的必要组成部分。”谢宾称。

南方财经全媒体记者近期采访了多位开发虚拟会议、VR游戏和头戴设备生产的市场人士,向他们了解元宇宙的发展阶段。“扎克伯格说的元宇宙是一个拥有24小时生活的未来场景,离我们太远了,但我们今天聊到的这些是大家在不久的将来可以接触到的‘元宇宙’的一部分。”谢宾说。

“永久远程”时代来临

今年9月,谢宾参加了高通公司(Qualcomm)举办的XR生态合作伙伴大会。在一块电脑屏幕上,他身着一件绛红色的T-shirt,手指轻触身后的黑板,切换到一张PPT说道:“大家现在看到的是我的化身,是我的一张照片,通过30秒的软件处理产生的。”

那一刻,现实中的谢宾站在屏幕前,他握了个拳,视频上的“化身”也握了个拳,他微笑,“化身”也随之微笑。而这远程也能够实现,如果谢宾坐在上海家中,他也可以远程“参加”这场实际发生在青岛的会议。

远程会议软件已非新事,上述软件在2018年4月就已经首次上线。自2020年初新冠疫情在全球蔓延以来,原本聚集于办公室里的同事们纷纷开始在手机或电脑上参加“网上群聊”,更是令远程会议软件市场大爆发。例如在纳斯达克上市的Zoom,在截至2020年1月底前12个月收入为6.23亿美元,但在截至2021年1月底前的12个月,收入成倍增长,高达26.5亿美元。

然而,远程会议的效果与现场会议仍存在差距,临场感、沉浸感,甚至仪式感,在前者的体验中大量缺失。“目前的远程会议方式仍有很多问题,比如说,会大大降低创新性,因为头脑风暴要求人们聚集在一个空间里、精神高度集中地专注于手中共同的一件事情。”谢宾指出。然而,远程视频会议上,精神涣散、开小差、一心二用,可能是常态,在读文章的你有同感吗?

在此背景下,“虚拟现实”的需求应运而生,它更想要贴近“现实”。“比如使用一个化身在会议上发言,和对着手机讲话就是不一样的。”谢宾指出区别,“人在说话的时候,会有很多身体语言、微表情,这些都在传递信息。”

形势正在悄然变化,随着疫情下的“临时远程”拉长时间线,“永久远程”这个话题变得越来越热。低碳、安全、不限时间地点……远程的优点令人难以舍弃。

“随着居家办公变得不可避免,人们必须解决当下远程会议存在的问题。改善人与人之间的连接,这就是核心。”谢宾分析称。

让人起“鸡皮疙瘩”的真实感

一种更“科幻”的远程办公场景,出现在马克·扎克伯格10月28日的演讲中,这就是“全息虚拟会议”。戴上特制的眼镜,全息投影的会议室、产品模型、屏幕统统出现在你的眼前,参会的同伴也以三维形式出现在身边。

“当我第一次在全息虚拟会议里和别人握手时,我的鸡皮疙瘩都起来了。”谢宾形容自己参与这种虚拟会议时,感受到惊喜和一种真实感。

(图片为10月28日Meta举行Connect 2021大会时,扎克伯格线上演讲中展示的全息虚拟会议画面)

Facebook的研究中心在10月23日发布了一篇名为《视频会议及VR会议比较:沟通行为研究(Videoconference and Embodied VR: Communication Pattens Across Task and Medium)》的文章。这个大型的实验揭示了视频会议与自然交流之间的分野。例如,在视频会议上,人们倾向更少地打断对方,话题转换也更正式,注视对方眼睛的频率较现实交流更高,也更少借助手势来表达自己。与此同时,采用化身的虚拟会议,则更贴近自然情形下的人类交流。“人与人在物理意义上的面对面交流才是交流的最高形式,否则,为什么高级商务会谈、国家元首会谈都需要飞到现场面对面交流呢?”谢宾说。

上述扎克伯格推广的全息虚拟会议软件,于今年8月19日推出,称为“Horizon Workrooms(地平线工作室)”。使用者戴上Facebook旗下的头戴设备Oculus Quest 2,进入软件工作界面,首先为自己制作一款“化身(avatar)”,这种“化身”可以通过“捏脸”的方式做得和自己很像,但仍然以卡通人的三维形象出现在会议中。

当时,电视台主持人与扎克伯格进行了一场远程的、全息的虚拟采访,而两人在现实生活中的手势、表情都很好地展现在会议的虚拟空间中,主持人举着话筒,而扎克伯格坐在桌前时不时伸手移动面前的键盘。

全息会议展现的另一大特色,是“空间感”。就像在一个现实的房间中,人们在虚拟的工作室里也可以从桌前站起来,走到黑板前面根据想法书写。这其中似乎还蕴含着一些其他的商业机会,10月28日,扎克伯格宣布在今年晚些时候,这种会议室将可以“定制化”,能让人们在虚拟会议里放上商标、海报等。

(图为今年8月19日Meta发布Horizon Workrooms时在视频中展示的模拟画面,视频左下角写着“部分图像为模拟,观看者或因视角所看到将有所不同。”)

谢宾表示,全息虚拟技术,不仅可以用在会议上,还可以用作“说明书”或者“远程流程指导”,也就是参照实物用3D投影做出一个digital twin(数字化的虚拟“孪生”形象)。例如,当开始操作复杂仪器时,工人如果戴上一台AR眼镜,就能根据“立体”的演示学习操作流程。“尤其在工厂、实验室、手术室环境中可以得到广泛推广。”谢宾说。

“目前,境腾科技开发的全息会议软件和全息远程指导软件,主要在开发to C(面向企业)的业务。”谢宾表示。在面向个人的业务方面,还因为成本较高而难以实现。

据悉,国内不少银行、券商已经在“试水”使用全息会议软件。这种新颖、具有一定便利性的沟通方式,被金融机构视为可提供给VIP客户的优越服务,可从北、上、广、深这样的超大城市进一步向其他城市里的高净值人群拓展。“想象一下在会议里有全息的K线图、资产负债图,这能给客户一种无缝连接现实的感觉。”谢宾介绍称。

会议软件竞争激烈

会议软件市场已经十分庞大,但虚拟现实、全息投影的技术引入,势必将这个市场推向更大的量级。有机会进入行业竞争的市场玩家,分布在会议方案(meeting solutions)、UCaaS(统一通信即服务)和CCaaS(呼叫中心即服务)中,云集了不少企业办公软件公司、通信公司、云服务公司。

广受关注的Zoom就是UCaaS领域一大典型,该公司目前已经能提供多人会议、举办大型会议、语音和信息等多种服务。从收入上来说,个人用户对盈利的贡献不足10%,绝大多数收入来自于企业级客户,截至今年7月底,该公司已有50.5万名企业级别客户。

Zoom在新冠疫情期间获得了全球瞩目的成长。2020年2月至今年1月底的12个月中,该公司收入按年增长326%至27亿美元,净利润从前一年的2200万美元暴增至6.72亿美元,而股价曾在9个月内上升接近700%。按照11月5日的收盘价,Zoom目前市值786.5亿美元,是全球市值领先的一家软件开发商。这个单一公司的经营,部分反映出会议软件市场的需求爆发。

进入2021年以来,市场一直担心随着全球新冠疫苗的普及,远程会议软件的使用量会减少。然而根据已发布的财报,今年2-4月、5-7月的两个季度里,Zoom的单季度收入分别达到9.6亿美元、10.21亿美元,按年增长率分别为191.4%、54%。

据此,市场保持了“押注”会议方案供应服务的热度。今年以来,Salesforce、Adobe、思科、微软股价分别上扬38%、32.5%、32%、52%,它们各自旗下的Slack、Adobe Connect、Webex、Teams软件均在远程会议软件市场上排名靠前。

“虚拟化”成会议软件尖峰技术

在“元宇宙”概念的激发下,保持软件“虚拟化”技术水平,将成为会议软件服务商的一大重要战略方向。目前全球市场上,有两大巨头正在深入参与、激烈交战,这就是微软和Meta。

Meta和微软,在硬件、软件两方面都展开了竞争。Meta开发头戴显示设备Oculus Quest 2,而微软推出智能眼镜HoloLens2,两者目前都是在全球科技爱好者中畅销的热门产品;Meta在今年8月推出Horizon Workrooms,而微软实际上已经在3月推出Mesh,两种产品不能说一模一样,但有可能朝非常相似的方向去发展。

在人们误以为是Meta开启元宇宙时,有不少行内人士都指出,微软的步伐并不晚于Meta。10月末,微软的股价一再攀升,市值超过2.5万亿美元,取代苹果成为全球市值最高的一家上市公司。

将VR或AR技术纳入会议软件,成为Meta和微软的“新战场”。11月3日,微软宣布,将在会议软件Teams中增加VR和AR功能,包括3D的“化身”功能,这令该公司的“元宇宙”企图更加清晰了。有市场调查显示,目前微软Teams是仅次于Zoom、全球市场份额第二大的会议软件。

而Meta除了推出Horizon Workrooms以外,也不忘将当前市场上排名第一的Zoom拉拢到自己的阵营内。

在9月13日,Zoom宣布将与Meta在Horizon Workrooms上合作。根据发布的演示视频,Zoom用户可以接入Horizon Workrooms的虚拟3D会议室,但不能像Horizon Workrooms用户一样以3D化身卡通形象现身,而是出现在悬浮的一块屏幕上。Zoom开发的演示桌面Whiteboard也可以接入会议,以方便参会者在屏幕上做文件演示。

原文链接:https://mbd.baidu.com/newspage/data/landingsuper?context=%7B%22nid%22%3A%22news_9964091296466564971%22%7D&n_type=-1&p_from=-1

收起阅读 »