注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

监管发关注函,媒体发文降温,元宇宙概念热度或将退去

元宇宙这一概念今年下半年迅速走红并且热度居高不下,中信证券称,元宇宙是未来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

收起阅读 »

Flutter 基础 | Dart 语法

该系列记录了从零开始学习 Flutter 的学习路径,第一站就是 Dart 语法。本文可以扫除看 Flutter 教程,写 Flutter 代码中和语言有关的绝大部分障碍。值得收藏~声明并初始化变量int i = 1; // 非空类型必须被初始化 int? k...
继续阅读 »

该系列记录了从零开始学习 Flutter 的学习路径,第一站就是 Dart 语法。本文可以扫除看 Flutter 教程,写 Flutter 代码中和语言有关的绝大部分障碍。值得收藏~

声明并初始化变量

int i = 1; // 非空类型必须被初始化
int? k = 2; // 可空类型
int? h; // 只声明未初始化,则默认为 null
var j = 2; // 自动推断类型为int
late int m; // 惰性加载
final name = 'taylor'; // 不可变量
final String name = 'taylor'; // 不可变量

Dart 中语句的结尾是带有分号;的。

Dart 中声明变量时可以选择是否为它赋初始值。但非空类型必须被初始化。

Dart 中声明变量可以显示指明类型,类型分为可空和非空,前者用类型?表示。也可以用var来声明变量,此时编译器会根据变量初始值自动推断类型。

late关键词用于表示惰性加载,它让非空类型惰性赋值成为可能。得在使用它之前赋值,否则会报运行时错误。

惰性加载用于延迟计算耗时操作,比如:

late String str = readFile();

str 的值不会被计算,直到它第一次被使用。

??可为空类型提供默认值

String? name;
var ret = name ?? ''

如果 name 为空则返回空字串,否则返回 name 本身。

数量

在 Dart 中intdouble是两个有关数量的内建类型,它们都是num的子类型。

若声明变量为num,则可同时被赋值为intdouble

num i = 1;
i = 2.5;

字串

''""都可以定义一个字串

var str1 = 'this is a str';
var str2 = "this is another str";

字串拼接

使用+拼接字符串

var str = 'abc'+'def'; // 输出 abcdef

多行字串

使用'''声明多行字符串

var str = '''
this is a
multiple line string
'''
;

纯字串

使用r声明纯字符串,其中不会发生转义。

var str = r'this is a raw \n string'; // 输出 this is a raw \n string

字串内嵌表达式

字符串中可以内嵌使用${}来包裹一个有返回值的表达式。

var str = 'today is ${data.get()}';

字串和数量相互转化:

int.parse('1'); // 将字串转换为 int
double.parse('1.1'); // 将字串转换为 double
1.toString(); // 将 int 转换为字串
1.123.toStringAsFixed(2); // 将 double 转换为字串,输出 '1.12'

集合

声明 List

与有序列表对应的类型是List

[]声明有序列表,并用,分割列表元素,最后一个列表元素后依然可以跟一个,以消灭复制粘贴带来的错误。

var list = [1,2,3,];

存取 List 元素

列表是基于索引的线性结构,索引从 0 开始。使用[index]可以获取指定索引的列表元素:

var first = list[0]; // 获取列表第一个元素
list[0] = 1; //为列表第一个元素赋值

展开操作符

...是展开操作符,用于将一个列表的所有元素展开:

var list1 = [1, 2, 3];
var list2 = [...list1, 4, 5, 6];

上述代码在声明 list2 时将 list1 展开,此时 list2 包含 [1,2,3,4,5,6]

除此之外,还有一个可空的展开操作符...?,用于过滤为null的列表:

var list; // 声明时未赋初始值,则默认为 null
var list2 = [1, ...?list]; // 此时 list2 内容还是[1]

条件插入

iffor是两个条件表达式,用于有条件的向列表中插入内容:

var list = [
'aa',
'bb',
if (hasMore) 'cc'
];

如果 hasMore 为 true 则 list 中包含'cc',否则就不包含。

var list = [1,2,3];
var list2 = [
'0',
for (var i in list) '$i'
];// list2 中包含 0,1,2,3

在构建 list2 的时候,通过遍历 list 来向其中添加元素。

Set

Set中的元素是可不重复的。

{}声明Set,并用,分割元素:

var set = {1,2,3}; // 声明一个 set 并赋初始元素
var set2 = {}; // 声明一个空 set
var set3 = new Set(); // 声明一个空 set
var set4 = Set(); // 声明一个空 setnew 关键词可有可无

Map

Map是键值对,其中键可以是任何类型但不能重复。

var map = {
'a': 1,
'b': 2,
}; // 声明并初始化一个 map,自动推断类型为 Map

var map2 = Map(); // 声明一个空 map
map2['a'] = 1; // 写 map
var value = map['a']; //读 map

读写Map都通过[]

const

const是一个关键词,表示一经赋值则不可修改:

// list
var list = const [1,2,3];
list.add(4); // 运行时报错,const list 不可新增元素

// set
var set = const {1,2,3};
set.add(4); // 运行时报错,const set 不可新增元素

// map
var map = const {'a': 1};
map['b'] = 2; // 运行时报错,const map 不能新增元素。

声明类

class Pointer {
double x;
double y;

void func() {...} // void 表示没有返回值
double getX(){
return x;
}
}
  • 用关键词 class声明一个类。
  • 类体中用类型 变量名;来声明类成员变量。
  • 类体中用返回值 方法名(){方法体}来声明类实例方法。

构造方法

上述代码会在 x ,y 这里报错,说是非空字段必须被初始化。通常在构造方法中初始化成员变量。

构造方法是一种特殊的方法,它返回类实例且签名和类名一模一样。

class Point {
double x = 0;
double y = 0;
// 带两个参数的构造方法
Point(double x, double y) {
this.x = x;
this.y = y;
}
}

这种给成员变量直接赋值的构造方法有一种简洁的表达方式:

class Point {
double x = 0;
double y = 0;

Point(this.x, this.y); // 当方法没有方法体时,得用;表示结束
}

命名构造方法

Dart 中还有另一个构造方法,它的名字不必和类名一致:

class Point {
double x;
double y;

Point.fromMap(Map map)
: x = map['x'],
y = map['y'];
}

为 Point 声明一个名为fromMap的构造方法,其中的:表示初始化列表,初始化列表用来初始化成员变量,每一个初始化赋值语句用,隔开。

初始化列表的调用顺序是最高的,在一个类实例化时会遵循如下顺序进行初始化:

  1. 初始化列表
  2. 父类构造方法
  3. 子类构造方法

Point.fromMap() 从一个 Map 实例中取值并初始化给成员变量。

然后就可以像这样使用命名构造方法:

Map map = {'x': 1.0, 'y': 2.0};
Point point = Point.fromMap(map);

命名构造方法的好处是可以将复杂的成员赋值的逻辑隐藏在类内部。

继承构造方法

子类的构造方法不能独立存在,而是必须调用父类的构造方法:

class SubPoint extends Point {
SubPoint(double x, double y) {}
}

上述 SubPointer 的声明会报错,提示得调用父类构造方法,于是改造如下:

class SubPoint extends Point {
SubPoint(double x, double y) : super(x, y);
}

在初始化列表中通过super调用了父类的构造方法。父类命名构造方法的调用也是类似的:

class SubPoint extends Point {
SubPoint(Map map) : super.fromMap(map);
}

构造方法重定向

有些构造方法的目的只是调用另一个构造方法,为此可以在初始化列表中通过this实现:

class Point {
double x = 0;
double y = 0;

Point(this.x, this.y);
Point.onlyX(double x): this(x, 0);
}

Point.onlyX() 通过调用另一个构造方法并为 y 值赋值为 0 来实现初始化。

方法

Dart 中方法也是一种类型,对应Function类,所以方法可以被赋值给变量或作为参数传入另一个方法。

// 下面声明的两个方法是等价的。
bool isValid(int value){
return value != 0;
}

isValid(int value){// 可自动推断返回值类型为 bool
return value != 0;
}

声明一个返回布尔值的方法,它需传入一个 int 类型的参数。

其中方法返回值bool是可有可无的。

bool isValid(int value) => value != 0;

如果方法体只有一行表达式,可将其书写成单行方法样式,方法名和方法体用=>连接。

Dart 中的方法不必隶属于一个类,它也可以顶层方法的形式出现(即定义在.dart文件中)。定义在类中的方法没有可见性修饰符public private protected ,而是简单的以下划线区分,_开头的函数及变量是私有的,否则是公有的。

可选参数 & 命名参数

Dart 方法可以拥有任意数据的参数,对于非必要参数,可将其声明为可选参数,调用方法时,就不用为其传入实参:

bool isValid(int value1, [int value2 = 2, int value3 = 3]){...}

定义了一个具有两个可选参数的方法,其中第二三个参数用[]包裹,表示是可选的。而且在声明方法时为可选参数提供了默认值,以便在未提供相应实参时使用。所以如下对该方法的调用都是合法的。

var ret = isValid(1) // 不传任何可选参数
var ret2 = isValid(1,2) // 传入1个可选参数
var ret3 = isValid(1,2,3) // 传入2个可选参数

使用[]定义可选参数时,如果想只给 value1,value3 传参,则无法做到。于是乎就有了{}

bool isValid(int value1, {int value2 = 2, int value3 = 3}) {...}

然后就可以跳过 value2 直接给 value3 传参:

var ret = isValid(1, value3 : 3)

这种语法叫可选命名参数

Dart 还提供了关键词required指定在众多可选命名参数中哪些是必选的:

bool isValid(int value1, {int value2, required int value3}) {...}

匿名方法

匿名方法表示在给定参数上进行一顿操作,它的定义语法如下:

(类型 形参) {
方法体
};

如果方法体只有一行代码可以将匿名函数用单行表示:

(类型 形参) => 方法体;

操作符

三元操作符

三元操作符格式如下:布尔值 ? 表达式1 : 表达式2;

var ret = isValid ? 'good' : 'no-good';

如果 isValid 为 true 则返回表达式1,否则返回表达式2。

瀑布符

该操作符..用于合并在同一对象上的多个连续操作:

val paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0

构建一个画笔对象并连续设置了 3 个属性。

如果对象可控则需使用?..

paint?..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0

类型判定操作符

as 是强转操作符,表示将一个类型强转为另一个类型。

is 是类型判定操作符,用于判断某个实例是否是指定类型。

is! 是与 is 相反的判定。

流程控制

if-else

if (isRaining()) {
you.bringRainCoat();
} else if (isSnowing()) {
you.wearJacket();
} else {
car.putTopDown();
}

for

for (var i = 0; i < 5; i++) {
message.write('!');
}

如果不需要关心循环的索引值,则可以这样:

for (var item in list) {
item.do();
}

while

while (!isDone()) {
doSomething();
}
do {
printLine();
} while (!atEndOfPage());

break & continue

break & continue 可用于 for 和 while 循环。

break用于跳出循环

var i = 0
while (true) {
if (i > 2) break;
print('$i');
i++;
} // 输出 0,1,2

continue用于跳过当前循环的剩余代码:

for (int i = 0; i < 10; i++) {
if (i % 2 == 0) continue;
print('$i');
}// 输出 1,3,5,7,9

switch-case

Dart 中的 switch-case 支持 String、int、枚举的比较,以 String 为例:

var command = 'OPEN';
switch (command) {
case 'CLOSED':
case 'PENDING': // 两个 case 共用逻辑
executePending();
break; // 必须有 break
case 'APPROVED':
executeApproved();
break;
case 'DENIED':
executeDenied();
break;
case 'OPEN':
executeOpen();
break;
default: // 当所有 case 都未命中时执行 default 逻辑
executeUnknown();
}

关键词

所有的关键词如下所示:

abstract 2elseimport 2show 1
as 2enuminstatic 2
assertexport 2interface 2super
async 1extendsisswitch
await 3extension 2late 2sync 1
breakexternal 2library 2this
casefactory 2mixin 2throw
catchfalsenewtrue
classfinalnulltry
constfinallyon 1typedef 2
continueforoperator 2var
covariant 2Function 2part 2void
defaultget 2required 2while
deferred 2hide 1rethrowwith
doifreturnyield 3
dynamic 2implements 2set 2


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

收起阅读 »

从技术演化,看元宇宙未来

文丨陈根21世纪是一个技术井喷的的时代,从互联网、云计算、大数据到通信技术、人工智能、数字孪生等等,一系列的技术都随着其发展和成熟日渐融入人们所生活的社会,并共同雕刻着这个属于技术的时代。元宇宙就是这个时代里一系列创新技术集大成的重要标志之一。元宇宙构建了一个...
继续阅读 »

文丨陈根

21世纪是一个技术井喷的的时代,从互联网、云计算、大数据到通信技术、人工智能、数字孪生等等,一系列的技术都随着其发展和成熟日渐融入人们所生活的社会,并共同雕刻着这个属于技术的时代。元宇宙就是这个时代里一系列创新技术集大成的重要标志之一

元宇宙构建了一个脱胎于现实世界,又与现实世界平行、相互影响,并且始终在线的虚拟世界。在元宇宙理想形态背后,是基于扩展现实技术提供沉浸式体验,基于数字孪生技术生成现实世界的镜像,基于区块链技术搭建经济体系,并且允许每个用户进行内容生产和世界编辑。

技术的发展是元宇宙初现的前提,技术的集成则是元宇宙爆发的背景。尽管当前元宇宙已经吸引了足够多的市场注意力和资本的目光,但站在技术演化的角度,元宇宙又该如何顺应技术发展趋势?元宇宙的终点又是什么技术?

技术聚合,进发元宇宙

乔布斯曾提出一个著名的“项链”比喻,iPhone的出现,串联了多点触控屏、iOS、高像素摄像头、大容量电池等单点技术,重新定义了手机,开启了激荡十几年的移动互联网时代。

正如iPhone的出现一样,元宇宙是一系列连点成线技术创新的总和。元宇宙是算力持续提升、高速无线通信网络、云计算、区块链、虚拟引擎、VR/AR、数字孪生等技术创新逐渐聚合的结果,是整合多种新技术而产生的新型虚实相融的互联网应用和社会形态。

1969年,互联网诞生,元宇宙的实现有了全球化的软硬件基础。1989年,万维网标准开始制定,为互联网技术的发展奠定了基础。1990年到1998年,人类用户主要通过字符化的电子公告牌(BBS)进行信息交互。彼时,在数字空间中没有数字世界环境的构建,没有明确的个人角色,用户交互界面还以字符为主。

1998年到2003年,博客开始兴起,互联网进入2.0时代。数字空间出现了独立的“数字人”角色,主要表现形式为字符和图片构成博客个人空间的主体。自此,互联网用户有了个人身份和角色,并逐渐向一个完善的数字生态系统过渡。

以博客为例,在博客中发言和展示个人信息内容,是数字空间的微观层面;某一个个体的博客平台所吸纳的人群,是数字空间中观层面;整个博客世界则是其宏观层面。博客生态系统不是一个简单的“写”与“看”的供求关系,甚至也不是一种简单的“表演”与“观看”的关系,而是由人们在社会整体生态环境影响下形成的多重需求构成的生态关系

2004年,社交网络开始兴起,Facebook正式上线,QQ、Twitter、微博不断出现并高速发展。数字空间中,数字人(个人用户空间)与数字人可以进行信息交互,形成社交网络和社交关系。2009年,移动互联网进入3G时代,随着智能手机的广泛应用,数字空间伴随社交网络的应用范围扩大而进一步扩大大量的图片,声音元素丰富了虚拟数字空间的内容

2014年,以Facebook购买虚拟现实公司Oculus为标志,虚拟现实成为产业热点。但是由于当时的VR眼镜还不成熟,VR追求的是沉浸式和场景化体验,但由于用户的参与感太过薄弱,只充当观众的用户量级显然无法支撑VR的全民热情,再加上社交网络巨头和消费者对虚拟现实社交网络准备不足。几年后,虚拟现实热潮退去数字空间向维化阶段进化的第一次努力失败

同年,4G网络兴起,视频开始成为社交网络为基础的数字空间的重要内容形式。2018年,电子商务和知识付费、NFT、虚拟货币在社交网络为基础的数字空间中初步成熟并广泛应用。

2019年5G商用正式宣告了5G时代的来临。5G技术具有万物互联、高速度、泛在网、低时延、低功耗、重构安全等特点和优势。5G技术的发展使整个人类社会的生产和生活产生深刻变革,5G构建起万物互联的核心基础能力,不仅带来了更快更好的网络通信,更肩负起赋能各行各业的历史使命。

2020年,以城市大脑、数字政府、组织化转型为主的产业化持续拓展社交网络和数字空间向万物互联、万物交互方向演进,但仍然处于萌芽状态。

在这样的背景下,囊括了上述技术、带着强烈科幻色彩的元宇宙随着开放式游戏创建平台Roblox上市成为了网络讨论的热点。在大厂布局、资本追捧下,现在,“元宇宙”概念已然成为市场最炙手可热的名词。

元宇宙,发展进行时

元宇宙的兴起是技术的聚合,从目前已经呈现的前端征兆和发展趋势看,在通信网络、云计算、区块链、虚拟现实等技术的支持下,元宇宙将生成一个与人类物理世界全方位连接起来的虚拟宇宙,人们得以感受到由此生成的超大尺度、无限扩张、层级丰富和谐运行的虚拟系统,呈现在人们面前的将是现实世界与数字世界融合的全新的文明景观

元宇宙连接虚拟和现实,丰富人的感知,提升体验,延展人的创造力和更多可能。虚拟世界从物理的世界的模拟、复刻,变成物理世界的延伸和拓展,进而反作用于物理世界,最终模糊虚拟世界和现实世界的界限。从这一角度来说,元宇宙的兴起可以看做是数字空间向三维化阶段进化的第二次尝试

虽然当前人们还不能准确描绘出元宇宙的景观,但事实上,现在人们已经以不同的方式生活在元宇宙之中。人们正不断地构建着数字世界,数字化着自己以及物理世界,而元宇宙的变化过程也会从不同的现实变量出发,比如教育、就业、消费等影响着真实社会的生产和生活。

对于元宇宙来说,不同的阶段有着不同的成熟度如果说信息化和数字化,是元宇宙兴起的前提,那么数字孪生,就是元宇宙发展的初级阶段。2011年,迈克尔教授在《几乎完美:通过产品全声明周期管理驱动创新和精益产品》中引用了其合作者约翰·维克斯描述概念模型的名词“数字孪生”,并一直沿用至今。

正如我在《数字孪生》所说的“数字孪生就是在一个设备或系统‘物理实体’的基础上,创造一个数字版的‘虚拟模型’。这个‘虚拟模型’被创建在信息化平台上提供服务”。值得一提的是,与电脑的设计图纸又不同,相比于设计图纸,数字孪生体最大的特点在于它是对实体对象的动态仿真

明眼望去,数字孪生是物理实体的“灵魂”。当前,数字孪生技术在经历了技术准备期、概念产生期和应用探索期后,正在进入大浪淘沙的领先应用期。随着图书馆、博物馆、各种景点孪生体数字孪生还在加速发展,而数字孪生发展的终极,就是走向元宇宙。

2021年初举行的计算机图形学顶级学术会议SIGGRAPH 2021上,英伟达就通过一部纪录片,自曝了2021年4月公司发布会中,英伟达CEO黄仁勋通过数字孪生技术制造了演讲中14秒片段的数字替身。尽管只有短暂的14秒,但黄仁勋标志性的皮衣,表情、动作、头发却足以乱真,几乎骗过了所有人。

未来,数字孪生技术将为元宇宙中的各种虚拟对象提供了丰富的数字孪生体模型,并通过从传感器和其他连接设备收集的实时数据与现实世界中的数字孪生化对象相关联,使得元宇宙环境中的虚拟对象能够镜像、分析和预测其数字孪生化对象的行为。可以说,作为对现实世界的动态模拟,“数字孪生是元宇宙从未来伸过来的一根触角

技术的进路,本质的到达

当然,虽然元宇宙在数字孪生中有所体现,但必然不止于数字孪生。Beamable公司创始人Jon Radoff提出了元宇宙的七层架构:基础设施、人机交互、去中心化、空间计算、创作者经济、发现和体验。

其中,基础设施包括支持元宇宙的设备、将它们连接到网络并提供内容的技术;人机交互则主要是智能可穿戴设备;去中心化是构建元宇宙人与人关系的重要转折,可以把元宇宙的所有资源更公平的分配;计算层将真实计算和虚拟计算进行混合,以消除物理世界和虚拟世界之间的障碍;创作者经济层包含创作者每天用来制作人们喜欢的体验的所有技术;发现层类似于互联网的门户网站和搜索引擎;体验层则是用户直接面对的游戏、社交平台等。

从元宇宙的七层架构可以看出,元宇宙是个比数字孪生更庞大、更复杂的体系。如果数字孪生已经是个复杂技术体系的话,那么元宇宙就是个复杂的技术-社会体系。问题是,这个复杂的技术-社会体系就是人类技术发展的终点吗?元宇宙的尽头又在哪里

显然,元宇宙作为功能意义上的一种技术群存在,是信息技术发展的高级阶段。从社会角度来看元宇宙的生成与发展依赖于现实世界,又反作用于现实世界的发展。

一方面,元宇宙不完全是一种人造的数字化空间或现实世界的数字化映像。元宇宙作为人类制造出来的一种现实性的非实在事物,其非实在性在于,元宇宙中的一切事物包括它本身都是信息的集合而非物质的集合。另一方面,元宇宙也只能部分地、有条件地反映出人类思维空间中的事物。

这意味着,元宇宙是虚拟演化的最终形态,宇宙发展的根本,还是为了促进现实世界的发展。2019年出版的《崛起的超级智能》曾经绘制了一幅世界数字大脑的发育示意图,书中就预言了在混合智能和云反射弧之后,世界数字大脑的思维空间和梦境空间将成为新的热点,而它们与元宇宙存在着密切的关系。

根据《崛起的超级智能》,社交网络对应了神经元网络;3G/4G/5G对应神经纤维;人工智能对应驱动世界数据大脑运转的基础和动力;城市大脑、工业大脑对应世界数字大脑的初步成型。作一个类脑复杂巨系统,世界数据大脑的思维空间和梦境空间将在大数据、虚拟现实、数字孪生等技术的推动下不断成熟。

从这一维度来说,元宇宙从本质上可以看作是思维空间和梦境空间的产业化名称。元宇宙是组成这个世界数字大脑的一部分,承担了它意识和梦境的构造,主要特征是大社交网络为核心的数字空间开始从二维向三维进化,能够将现实世界映射到数字空间,也可以将人类的幻想具象化,带给人类梦境般真实体验。

世界数字大脑的作用是提高人类社会的运行效率,解决人类社会发展过程中面临的复杂问题,更好地为人类协同发展提供支撑。世界神经系统将为人类社会的协同发展,构建起全球统一的类脑智能支撑平台,实现对世界的认知、判断、决策、反馈和改造,共同应对来自自然的各种挑战和风险,满足人类社会的各种需求。

归根到底,从信息化到数字化,从数字化向数字孪生进化,再向元宇宙进发。技术的演化之路也是人类的进化之路,使得人类们对虚拟和现实、物理和数字有更本质的认识。

https://baijiahao.baidu.com/s?id=1716003827693985340&wfr=spider&for=pc

收起阅读 »

原来我一直在错误的使用 setState()?

导语 任何前端系统与用户关系最密切的部分就是UI。一个按钮,一个标签,都是通过对应的UI元素展示与交互。初学时,我们往往只关注如何使用。但如果只知道如何使用,遇到问题我们很难找到解决的办法和思路,也无法针对一些特定场景进行优化。本期针对Flutter的UI系统...
继续阅读 »

导语


任何前端系统与用户关系最密切的部分就是UI。一个按钮,一个标签,都是通过对应的UI元素展示与交互。初学时,我们往往只关注如何使用。但如果只知道如何使用,遇到问题我们很难找到解决的办法和思路,也无法针对一些特定场景进行优化。本期针对Flutter的UI系统和大家一起进阶学习:


1、原来我一直在错误的使用 setState()?


2、面试必问:说说Widget和State的生命周期


3、Flutter的布局约束原理


4、15个例子解析Flutter布局过程


读完本文你将收获:Flutter的渲染机制以及setState()背后的原理




引言


初学Flutter的时候,当需要更新页面数据时,我们通常会想到调用setState()。但很多博客以及官方文章并不建议我们在页面的节点使用setState()因为这样会带来不必要的开销(仅针对页面节点,当然Flutter的Widget刷新一定离不开setState()),很多状态管理方案也是为了达到所谓的“局部刷新”。到这我们不仅要思考为什么使用setState()能刷新页面,又为何可能会带来额外的损耗?这个函数背后做了什么逻辑?这篇文章和大家一一揭晓。




一、为什么setState()能刷新页面


1、setState()


我们的demo从一个最简单的计数器开始



在页面中点击底部的➕号,本地变量加一,之后调用了当前页面的setState(),页面重新构建,显示的数据增加。从现象推断,整个流程必然会经过setState()-···················->当前State的build()-················->页面绘制-············->屏幕刷新。
那么下面我们看看setState()到底做了什么?


State#setState(VoidCallback fn)


@protected
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}

在去掉所有的断言之后,其实setState只做了两件事儿


1、调用我们传入的VoidCallback fn


2、调用_element.markNeedsBuild()




2、element.markNeedsBuild()


Flutter开发中我们一般和Widget打交道,但Widget上有这样一个注释。



Describes the configuration for an [Element].



abstract class Widget extends DiagnosticableTree {
final Key key;
Element createElement();
String toStringShort() {
return key == null ? '$runtimeType' : '$runtimeType-$key';
}

Widget只是用于描述Element的一个配置文件,实际在Framework层管理页面的构建,渲染等,都是通过Element完成,Element由Widget创建,并且持有Widget对象,每一种Widget都会对应的一种Element



在上面的demo中,我们在HomePageState调用了setState(),这里的Element有HomePage对象创建。HomePage(Widget) - HomePageState(State) - HomePageElement(StatefulElement) 三者一一对应。



Element#markNeedsBuild()


/// The object that manages the lifecycle of this element.
/// 负责管理所有element的构建以及生命周期
@override
BuildOwner get owner => _owner;

void markNeedsBuild() {
//将自己标记为脏
_dirty = true;
owner.scheduleBuildFor(this);
}

调用了BuildOwner.scheduleBuildFor(element),这里的BuildOwnerWidgetsBinding的初始化中完成实例化,负责管理widget框架,每个Element对象在mount到element树中之后都会从父节点获得它的引用


WidgetsBinding#initInstances()


void initInstances() {
super.initInstances();
_instance = this;
_buildOwner = BuildOwner();
buildOwner.onBuildScheduled = _handleBuildScheduled;
/······/
}

BuildOwner#scheduleBuildFor(Element element)


void scheduleBuildFor(Element element) {
//添加到_dirtyElements集合中
_dirtyElements.add(element);
element._inDirtyList = true;
}

最后将自己添加到BuildOwner中维护的一个脏element集合。



总结:1、Element: 持有Widget,存放上下文信息,RenderObjectElement 额外持有 RenderObject。通过它来遍历视图树,支撑UI结构。


2、setState()过程其实只是将当前对应的Element标记为脏(demo中对应HomePageState),并且添加到_dirtyElements合中。





3、Flutter渲染机制


上面的过程看起来没做任何渲染相关的事儿,那么页面是如何重新绘制?关键点就在于Flutter的渲染机制



开始FrameWork层会通知Engine表示自己可以进行渲染了,在下一个Vsync信号到来之时,Engine层会通过Windows.onDrawFrame回调Framework进行整个页面的构建与绘制。(这里我想为什么要先由Framework发起通知,而不是直接由Vsync驱动。如果一个页面非常卡顿,恰好每一帧绘制的时间大于一个Vsync周期,这样每帧都不能在一个Vsync的时间段内完成绘制。而先由framework保证上完成构建与绘制后,发起通知在下一个Vsync信号再绘制则可以避免这样的情况)。每次收到渲染页面的通知后,Engine调用Windows.onDrawFrame最终交给_handleDrawFrame()方法进行处理。


@protected
void ensureFrameCallbacksRegistered() {
//构建帧前的处理,主要是进行动画相关的计算
window.onBeginFrame ??= _handleBeginFrame;
//Windows.onDrawFrame交给_handleDrawFrame进行处理
window.onDrawFrame ??= _handleDrawFrame;
}
复制代码

SchedulerBinding#handleDrawFrame()


void handleDrawFrame() {
try {
// PERSISTENT FRAME CALLBACKS
// 关键回调
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
// POST-FRAME CALLBACKS
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally {
/·····························/
}
}

Flutter AnimationController回调原理一期中我们提到过,在Flutter的SchedulerBinding中维护了这样三个队列




  • Transient callbacks,由系统的[Window.onBeginFrame]回调,用于同步应用程序的行为 到系统的展示。例如,[Ticker]s和[AnimationController]s触发器来自与它。

  • Persistent callbacks 由系统的[Window.onDrawFrame]方法触发回调。例如,框架层使用他来驱动渲染管道进行build, layout,paint

  • Post-frame callbacks在下一帧绘制前回调,主要做一些清理和准备工作 Non-rendering tasks 非渲染的任务,可以通过此回调获取一帧的渲染时间进行帧率相关的性能监控



SchedulerBinding.handleDrawFrame()中对_persistentCallbacks_postFrameCallbacks集合进行了回调。根据上面的描述可知,_persistentCallbacks中是一些固定流程的回调,例如build,layout,paint。跟踪这个_persistentCallbacks这个集合,发现在RendererBinding.initInstances()初始化中调用了addPersistentFrameCallback(_handlePersistentFrameCallback)方法。这个方法只有一行调用就是drawFrame()



总结:



  • SchedulerBinding中维护了这样三个队列TransientCallbacks(动画处理),PersistentCallbacks(页面构建渲染),PostframeCallbacks(每帧绘制完成后),并在合适的时机对其进行回调。

  • 当收到Engine的渲染通知之后通过Windows.onDrawFrame方法回调到Framework层调用handleDrawFrame

  • handleDrawFrame回调PersistentCallbacks(页面构建渲染),最终调用drawFrame()





4、drawFrame()


查看drawFrame()方法一般会直接点击到RendererBinding


RendererBinding#drawFrame()


void drawFrame() {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}

从这几个方法名能大致看出,这里调用了布局,绘制,渲染帧的。而且看类名,这是负责渲染的Binding,并没有调用Widget的构建。这是因为WidgetsBinding是onRendererBinding的(理解为继承),其中重写了drawFrame(),实际上调用的应该是WidgetsBinding.drawFrame()


WidgetsBinding#drawFrame()


@override
void drawFrame() {
try {
if (renderViewElement != null)
// buildOwner就是前面提到的负责管理widgetbuild的对象
// 这里的renderViewElement是整个UI树的根节点
buildOwner.buildScope(renderViewElement);
super.drawFrame();
//将不再活跃的节点从UI树中移除
buildOwner.finalizeTree();
} finally {
/·················/
}
}

super.drawFrame()之前,先调用 buildOwner.buildScope(renderViewElement)
BuildOwner#buildScope(Element context, [ VoidCallback callback ])


void buildScope(Element context, [ VoidCallback callback ]) {
if (callback == null && _dirtyElements.isEmpty)
return;
try {
_scheduledFlushDirtyElements = true;
_dirtyElementsNeedsResorting = false;
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
try {
///关键在这
_dirtyElements[index].rebuild();
} catch (e, stack) {
/···············/
}
}
} finally {
for (Element element in _dirtyElements) {
element._inDirtyList = false;
}
_dirtyElements.clear();
}
}

前面在setState()之后,将homePageState添加到_dirtyElements里面。而这个方法会对集合内的每一个对象调用rebuild()rebuild()这个方法最终走到performRebuild(),这是一个Element中的一个抽象方法。




二、为什么高位置的setState ()会消耗性能


1、performRebuild()


查看StatelessElementStatefulElement共同祖先CompantElement中的实现


CompantElement#performRebuild()


void performRebuild() {
Widget built;
try {
built = build();
} catch (e, stack) {
built = ErrorWidget.builder();
}
try {
_child = updateChild(_child, built, slot);
} catch (e, stack) {
built = ErrorWidget.builder();
_child = updateChild(null, built, slot);
}

}

这个方法直接调用子类的build方法返回了一个Widget,对应调用前面的HomePageState()中的build方法。


将这个新build()出来的widget和之前挂载在Element树上的_child(Element类型)作为参数,传入updateChild(_child, built, slot)中。setState()的核心逻辑就在 updateChild(_child, built, slot)


2、updateChild(_child, built, slot)


StatefulElement#updateChild(Element child, Widget newWidget, dynamic newSlot)


@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
if (child != null)
//child == null && newWidget == null
deactivateChild(child);
//child != null && newWidget == null
return null;
}
if (child != null) {
if (child.widget == newWidget) {
//child != null && newWidget == child.widget
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
//child != null && Widget.canUpdate(child.widget, newWidget)
child.update(newWidget);
return child;
}
deactivateChild(child);
}
// child != null && !Widget.canUpdate(child.widget, newWidget)
return inflateWidget(newWidget, newSlot);
}

这个方法上官方提供了这样的注释:






















newWidget == nullnewWidget != null
child == nullReturns null.Returns new [Element].
child != nullOld child is removed, returns null.Old child updated if possible, returns child or new [Element].

总的来说,根据之前挂载在Element树上的_child以及再次调用build()出来的newWidget对象,共有四种情况




  • 如果之前的位置child为null

    • A、如果newWidget为null的话,说明这个位置始终没有子节点,直接返回null即可。

    • B、如果newWidget不为null,说明这个位置新增加了子节点调用inflateWidget(newWidget, newSlot)生成一个新的Element返回



  • 如果之前的child不为null

    • C、如果newWidget为null的话,说明这个位置需要移除以前的节点,调用 deactivateChild(child)移除并且返回null

    • D、如果newWidget不为null的话,先调用Widget.canUpdate(child.widget, newWidget)对比是否能更新。这个方法会对比两个Widget的runtimeTypekey,如果一致则说明子Widget没有改变,只是需要根据newWidget(配置清单)更新下当前节点的数据child.update(newWidget);如果不一致说明这个位置发生变化,则deactivateChild(child)后返回inflateWidget(newWidget, newSlot)





而在demo中,观察代码我们可以知道



在homePageState中调用setState()后,child和newWidget都不为空都是Scaffold类型,并且由于我们没有显示的指定key,所以会走child.update(newWidget)方法**(注意这里的child已经变成Scaffold)**。


3、递归更新


update(covariant Widget newWidget)是一个抽象方法,不同element有不同实现,以StatulElement为例


void update(StatefulWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
final StatefulWidget oldWidget = _state._widget;
// Notice that we mark ourselves as dirty before calling didUpdateWidget to
// let authors call setState from within didUpdateWidget without triggering
// asserts.
_dirty = true;
_state._widget = widget;
try {
final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;
} finally {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
}
rebuild();
}

这个方法先回调用_state.didUpdateWidget我们可以在State中重写这个方法,走到最后发现最终再次调用了rebuild()。但这里需要注意这次调用rebuild()的已经不是HomePageState了,而是他的第一个子节点Scaffold。所以整个过程又会再次走到performRebuild(),又在再次调用updateChild(_child, built, slot)更新子节点。不断的递归直到页面的最子一级节点。如图



build()过程虽然只是调用一个组件的构造方法,不涉及对Element树的挂载操作。但因为我们一个组件往往是N多个Widget的嵌套组合,每个都遍历一遍开销算下来并不小(感兴趣可以数数Scaffold有多少层嵌套)。


回到我们的demo中,其实我们的诉求只是点击+号改变以前显示的数据。



但直接在页面节点调用setState()将会重新调用所有Widget(包括他们中的各种嵌套)的build()方法,如果我们的需求是一个较为复杂的页面,这样带来的开销消耗可想而知。


而要想解决这个问题可以参考告别setState()! 优雅的UI与Model绑定 Flutter DataBus使用~




总结


当我们在一个高节点调用setState()的时候会构建再次build所有的Widget,虽然不一定挂载到Element树中,但是平时我们使用的Widget中往往嵌套多个其他类型的Widget,每个build()方法走下来最终也会带来不小的开销,因此通过各种状态管理方案,Stream等方式,只做局部刷新,是我们日常开发中应该养成的良好习惯。




最后


本期我们分析了setState()过程,重点分析了递归更新的过程。正如安卓Activity或者Fragment的生命周期,Flutter中Widget和State同样也提供了对应的回调,如initState()build()。这些方法背后是谁在调用,他们的调用时序是如何?Element的生命周期是如何调用的?将会在下一期和大家一一分析~


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

最近大火的元宇宙到底是什么?

准确地说,元宇宙不是一个新的概念,它更像是一个经典概念的重生。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、区块链、人工智能等相应底层技术距离元宇宙落地应用的需求仍有较大差距。元宇宙产业的成熟,需要大量的基础研究做支撑。对此,要谨防元宇宙成为一些企业的炒作噱头,应鼓励相关企业加强基础研究,增强技术创新能力,稳步提高相关产业技术的成熟度。

从行业标准方面来看,只有像互联网那样通过一系列标准和协议来定义元宇宙,才能实现元宇宙不同生态系统的大连接。对此,应加强元宇宙标准统筹规划,引导和鼓励科技巨头之间展开标准化合作,支持企事业单位进行技术、硬件、软件、服务、内容等行业标准的研制工作,积极地参与制定元宇宙的全球性标准。

从法律方面来看,随着元宇宙的发展,以及逐步走向成熟,平台垄断、税收征管、监管审查、数据安全等一系列问题也将随之产生,提前思考如何防止和解决元宇宙所产生的法律问题成为必不可少的环节。对此,应加强数字科技领域立法工作,在数据、算法、交易等方面及时跟进,研究元宇宙相关法律制度。

可以肯定的是,在技术演进和人类需求的共同推动下,元宇宙场景的实现,元宇宙产业的成熟,只是一个时间问题。作为真实世界的延伸与拓展,元宇宙所带来的巨大机遇和革命性作用是值得期待的,但正因如此,我们更需要理性看待当前的元宇宙热潮,推动元宇宙产业健康发展。

(作者系中国社会科学院数量经济与技术经济研究所副研究员)

收起阅读 »

Flutter 毁了客户端和 Web 开发!

Google 重磅发布了专为 Web、移动和桌面而构建的 Flutter 2.0!将 Flutter 从移动开发框架扩展成可移植框架,因而开发者无需重写代码即可将应用扩展至桌面或网页。看似为了帮助Web和移动开发者,实际上不然,而本文作者认为,现在不应该再去想...
继续阅读 »

Google 重磅发布了专为 Web、移动和桌面而构建的 Flutter 2.0!将 Flutter 从移动开发框架扩展成可移植框架,因而开发者无需重写代码即可将应用扩展至桌面或网页。看似为了帮助Web和移动开发者,实际上不然,而本文作者认为,现在不应该再去想创建一个需要部署到所有平台的应用程序,Flutter反而毁了Web和移动开发。


以下为译文: 大家好,我是一名软件开发人员,我叫 Luke。


由于我选择了这个相当大胆的标题,为了避免误会,我要对其进行详细的解释。从技术角度来讲,Flutter 的确是一个跨平台的框架。也不止其,所有跨断技术都是非常糟糕的设计。


但是,我有点不同的看法。


从 Flutter 2.0 发布以来,我就察觉到它被炒的有点过了。但不应该再去想创建一个需要部署到所有平台的应用程序,Flutter反而毁了Web和移动开发。


请不要误会,我并不是要否定它,其实我也是 Flutter 的粉丝,亦将一如既往的拥护它。


我在日常工作中经常使用 Flutter 来开发 iOS 和 Android 应用程序。由于早前我是用 Kotlin 或者 Swift 来开发原生的应用,支持多种特性,如:扫描 / 页面识别、pin/biometric 应用程序认证、通知、firebase 统计和一些高级的用户流,现在用 Flutter 来开发应用,我对 Flutter 的优缺点的了解更加透彻。


1、六大平台


image.png


通过今年的 Flutter Engage 会议我们可知已经可以使用 Flutter 在 iOS、 Android、 Mac、 Windows、 Linux 和 Web 这六个平台中的任何一个平台上开发应用。这太棒了!但事情远没有这么简单...你的确可以在这 6 个平台上部署你的应用程序,但是说实话,我很少这么做。我很难想象一个人会在不同的平台上部署同一个应用程序,我认为应该根据不同的平台特点使用不同的设计模式。在大型设备上使用底部弹窗、应用程序条、简洁的列表就很别扭。一般来说,适合在移动设备上的组件和设计模式在桌面设备上却不合时宜,反之亦然。


我的一个非常好的朋友 Filip Hracek 在 Flutter Engage 演讲中提到“神奇的设计开发者”的相关话题,我非常赞同他的看法。我认为需要有更多的开发者真正知道他们正在做的是什么,而且不是盲目地跟从迭代面板。


Scrum Sprint 是一个可重复的固定时间框,在这个时间框内创造一个高价值的产品。-- 维基百科


强烈推荐大家观看 Filip 在 Youtube 上的相关视频片段http://www.youtube.com/watch?v=MIe…


接下来,我们重新回到 Flutter 这个话题:


2、不应该再去想创建一个需要部署到所有平台的应用程序


你更应该去想如何将你要编写的应用程序模块化,以便在未来更好地复用这些模块。给你们举个例子:在我的公司,我们正在开发专注于用户数据的应用程序。


这就需要创建自定义和高级的调查报告,我们不希望每次添加新问题时都要编写新的窗口小部件。我们的做法是:编写一个包含所有可能的调查逻辑的模块,在许多其他项目中复用它(而不需要每次都重写一遍相似的代码)


我给你举上面这个例子的目的是提醒你在构建一个应用程序时,你更应该着重思考你要做的应用程序或整个业务的重点是什么。更应该去重点思考,它背后的业务逻辑是什么?


在计算机软件中,业务逻辑或领域建模也是程序的一部分,它对真实世界的业务规则进行编码,确定如何创建、存储和修改数据。


当你明确了领域划分,你可以将一个领域封装成独立的模块,你可以将该模块在需要开发的 Flutter 应用程序中复用。


但 Luke,这有什么好大惊小怪的吗?


对,这是一个好问题!


对于相同的业务逻辑,你可以用不同的用户流来创建多个 Flutter 应用。你可以将要开发的 Flutter 应用进行分类(如:移动应用、桌面应用和 Web应用),这将能帮助关注到不同平台的差异,对特定平台进行特定处理最终将获得更好的用户体验。


3、针对不同平台要编****写多个应用程序


虽然 Flutter 还算是一个相对比较新的技术,还主要针对小公司和个人开发者,但这不妨碍它成为一个人人皆可用的伟大工具。


我参与开发过多个企业级应用程序。根据我的经验,系统的每个部分都需要有一个清晰的工作流程。开发一个系统通常需要前端、后端等。为了节约成本,编写一个应用程序,在不同的平台运行也越发流行。为了实现这个目的,你需要雇一个团队进行专门开发。你敢想象,十几个人的团队开发同一套代码来实现所有平台的特性吗?这简直是管理层的噩梦。很可能出现:一部分开发人员开发的桌面特性与移动团队正在开发的特性相冲突的情况。


其次,应用程序包也会越来越臃肿,然而很多时候并不是每个平台都需要有一份软件包。现在,正值 Flutter 2.0 发布的时候,由于我并没有将所有的包都进行升级,还不支持 null 安全还需要手动解决依赖冲突的问题。


4、为什么 Flutter 不是一个跨平台的框架


在读了这篇文章之后,或许你能够理解为什么我会认为 Flutter 不是一个真正的跨平台框架。Flutter 是一个为我们提供了为每个平台构建应用程序所需的功能的工具。我认为,真正实现跨平台不应该只开发一个应用程序,更应该开发一组由相同的业务逻辑驱动的应用程序集合。


此外,当我们编写 Flutter 应用程序时,我们并没有跨越任何平台。我们这种所谓的跨平台,不过是用 Xamarin 或其他工具将写好的代码翻译成原生元素。


如果非要把 Flutter 和其他东西进行类比的话,那么与之相似的就是游戏引擎(如 Unity)。我们不需要专门在 Windows 或者 Mac 系统上开发对应平台的游戏。我们可以使用 Unity 编写,然后将其导出到一个特定的平台。使用 Unity 编写一个游戏然后导出到多个平台和真正的跨平台完全也是两码事。


因为每个项目都有技术债务,你应该停止抱怨,并开始重构。每次开发新功能之前都应该进行小型代码重构。但接入 Flutter 大规模的重构和重写永远不会有好结果。


5、结尾


全文都在讨论跨平台相关话题, 以上就是我认为 flutter 毁了 Web 开发的原因。很多人对这一说法很感兴趣,并热切地加入了辩论。如果你认为 flutter 并没有那么糟糕,或许你会持有不同意见 。


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

freeze、seal、preventExtensions对比

在Object常用的方法中,Object.freeze和Object.seal对于初学者而言,是两个较为容易混淆的概念,常常傻傻分不清两者的区别和应用场景 概念 先看看两者定义 Object.freeze在MDN中的定义 Object.freeze() 方法...
继续阅读 »

Object常用的方法中,Object.freezeObject.seal对于初学者而言,是两个较为容易混淆的概念,常常傻傻分不清两者的区别和应用场景


概念


先看看两者定义


Object.freeze在MDN中的定义



Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。



Object.seal在MDN中的定义



Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。



从两者定义可以得到两者差异:Object.freeze核心是冻结,强调的是不可修改。Object.seal核心是封闭,强调的是不可配置,不影响老的属性值修改


差异


定义一个对象,接下来的对比围绕这个对象进行


"use strict";
const obj = {
name: "nordon"
};

使用Object.freeze


Object.freeze(obj);
Object.isFrozen(obj); // true
obj.name = "wy";

使用Object.isFrozen可以检测数据是否被Object.freeze冻结,返回一个Boolean类型数据


此时对冻结之后的数据进行修改,控制台将会报错:Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'


使用Object.seal


Object.seal(obj);
Object.isSealed(obj); // true
obj.name = "wy";

使用Object.isSealed可以检测数据是否被Object.seal封闭,返回一个Boolean类型数据


此时修改name是成功的,此时的obj也成功被修改


注意:若是不开启严格模式,浏览器会采用静默模式,不会在控制台抛出异常信息


共同点


主要以Object.seal演示


不可删除


delete obj.name;

控制台将会抛出异常:Uncaught TypeError: Cannot delete property 'name' of #<Object>


不可配置


可以修改原有的属性值


Object.defineProperty(obj, 'name', {
value: 'wy'
})

不可增加新的属性值


Object.defineProperty(obj, "age", {
value: 12,
});

控制台将会抛出异常:Uncaught TypeError: Cannot define property age, object is not extensible


深层嵌套


两者对于深层嵌套的数据都表现为:无能为力


定义一个嵌套的对象


"use strict";
const obj = {
name: "nordon",
info: {
foo: 'bar'
}
};

对于obj而言,无论是freeze还是seal,操作info内部的数据都无法做到对应的处理


obj.info.msg = 'msg'

数据源obj被修改,不受冻结或者冰封的影响


若是想要做到嵌套数据的处理,需要递归便利数据源处理,此操作需要注意:数据中包含循环引用时,将会触发无限循环


preventExtensions


最后介绍一下Object.preventExtensions,为何这个方法没有放在与Object.freezeObject.seal一起对比呢?因为其和seal基本可保持一致,唯一的区别就是可以delete属性,因此单独放在最后介绍


看一段代码


"use strict";
const obj = {
name: "nordon",
};

Object.preventExtensions(obj);
Object.isExtensible(obj); // false, 代表其不可扩展

delete obj.name;

作者:Nordon
链接:https://juejin.cn/post/7028389571561947172

收起阅读 »

【喵猫秀秀秀】用CSS向你展示猫立方!!

前言 这次,我们用vue2+scss,带大家来实现一个六面体的猫立方。 本次的逻辑我们不适用任何的js代码,仅仅只依靠css来完成。 所以,通过本片文章,你可以收获一些css动画相关的技巧。 先看看效果 预习 本次我们要用到的知识点 transform ...
继续阅读 »

前言


这次,我们用vue2+scss,带大家来实现一个六面体的猫立方。


本次的逻辑我们不适用任何的js代码,仅仅只依靠css来完成。


所以,通过本片文章,你可以收获一些css动画相关的技巧。


先看看效果


cat3D.gif


预习


本次我们要用到的知识点



  1. transform


解释:transform 属性向元素应用 2D 或 3D 转换。该属性允许我们对元素进行旋转、缩放、移动或倾斜。


要用哪个,可以对着这个表格查


image.png



  1. transform-style


解释:transform--style属性指定嵌套元素是怎样在三维空间中呈现。


注意:  使用此属性必须先使用 transform 属性.



  1. transition


解释:transition 属性是一个简写属性,用于设置四个过渡属性:



  • transition-property

  • transition-duration

  • transition-timing-function

  • transition-delay


注释:请始终设置 transition-duration 属性,否则时长为 0,就不会产生过渡效果。


分析


我们先拆解下这个猫3D的特点,它有以下特点



  1. 它一直在不停的转

  2. 它由两个六面体组成,外面一个,里面一个

  3. 鼠标靠近外面的六面体,六面体的六个面会往外扩,露出里面的小六面体


开始


1.因为我们做的是六面体,有2个六面体,一个在里面,一个在外面。2个六面体,12个面,先准备12张猫主子的图片。


image.png



  1. 然后我们新建img3D.vue文件,开干


image.png


步骤一


先来完成第一个特点不停的转


cat3D1.gif
代码如下:


<template>
<div>
<div class="container">
</div>
</div>
</template>

<script>

</script>

<style lang="scss" scoped>
* {
margin: 0px;
padding: 0px;
}

.container {
position: relative;
margin: 0px auto;
margin-top: 9%;
margin-left: 42%;
width: 200px;
height: 200px;
transform: rotateX(-30deg) rotateY(-80deg);
transform-style: preserve-3d;
animation: rotate 15s infinite;
/* 我这里用一个边框线来更容易看到效果 */
border: 1px solid red;
}

/* 旋转立方体 */
@keyframes rotate {
from {
transform: rotateX(0deg) rotateY(0deg);
}

to {
transform: rotateX(360deg) rotateY(360deg);
}
}
</style>

步骤二


弄外面的六面体,并且六面体在鼠标悬停的时候,需要往外扩
效果如下:


cat3D2.gif


<template>
<div>
<div class="container">
<!-- 外层立方体 -->
<div class="outer-container">
<div class="outer-top">
<img :src="cat1" />
</div>
<div class="outer-bottom">
<img :src="cat2" />
</div>
<div class="outer-front">
<img :src="cat3" />
</div>
<div class="outer-back">
<img :src="cat4" />
</div>
<div class="outer-left">
<img :src="cat5" />
</div>
<div class="outer-right">
<img :src="cat6" />
</div>
</div>
</div>
</div>
</template>

<script>
import cat1 from "@/assets/cats/cat1.png";
import cat2 from "@/assets/cats/cat2.png";
import cat3 from "@/assets/cats/cat3.png";
import cat4 from "@/assets/cats/cat4.png";
import cat5 from "@/assets/cats/cat5.png";
import cat6 from "@/assets/cats/cat6.png";

export default {
data() {
return {
cat1,
cat2,
cat3,
cat4,
cat5,
cat6,
};
},
};
</script>

<style lang="scss" scoped>
...
/* 设置图片可以在三维空间里展示 */
.container .outer-container {
transform-style: preserve-3d;
}
/* 设置图片尺寸固定宽高200px */
.outer-container img {
width: 200px;
height: 200px;
}

/* 外层立方体样式 */
.outer-container .outer-top,
.outer-container .outer-bottom,
.outer-container .outer-right,
.outer-container .outer-left,
.outer-container .outer-front,
.outer-container .outer-back {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 200px;
border: 1px solid #fff;
opacity: 0.3;
transition: all 0.9s;
}

/* 全靠transform 属性撑起来了外面的六面体 */
.outer-top {
transform: rotateX(90deg) translateZ(100px);
}

.outer-bottom {
transform: rotateX(-90deg) translateZ(100px);
}

.outer-front {
transform: rotateY(0deg) translateZ(100px);
}

.outer-back {
transform: translateZ(-100px) rotateY(180deg);
}

.outer-left {
transform: rotateY(90deg) translateZ(100px);
}

.outer-right {
transform: rotateY(-90deg) translateZ(100px);
}

/* 鼠标悬停 外面的六面体,六条边都往外扩出去,并且透明度变浅了,猫猫图片更清晰了 */
.cube:hover .outer-top {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(90deg) translateZ(200px);
}

.container:hover .outer-bottom {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(-90deg) translateZ(200px);
}

.container:hover .outer-front {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(0deg) translateZ(200px);
}

.container:hover .outer-back {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: translateZ(-200px) rotateY(180deg);
}

.container:hover .outer-left {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(90deg) translateZ(200px);
}

.container:hover .outer-right {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(-90deg) translateZ(200px);
}
</style>


步骤三


弄里面的六面体,这个六边形比较简单,没有移入移出,鼠标悬停等的样式效果
效果如下:


cat3D.gif
代码如下:


<template>
<div>
<div class="container">
<!-- 外层立方体 -->
...
<!-- 内层立方体 -->
<div class="inner-container">
<div class="inner-top">
<img :src="cat7" />
</div>
<div class="inner-bottom">
<img :src="cat8" />
</div>
<div class="inner-front">
<img :src="cat9" />
</div>
<div class="inner-back">
<img :src="cat10" />
</div>
<div class="inner-left">
<img :src="cat11" />
</div>
<div class="inner-right">
<img :src="cat12" />
</div>
</div>
</div>
</div>
</template>

<script>
...
import cat7 from "@/assets/cats/cat7.png";
import cat8 from "@/assets/cats/cat8.png";
import cat9 from "@/assets/cats/cat9.png";
import cat10 from "@/assets/cats/cat10.png";
import cat11 from "@/assets/cats/cat11.png";
import cat12 from "@/assets/cats/cat12.png";

export default {
data() {
return {
...
cat7,
cat8,
cat9,
cat10,
cat11,
cat12,
};
},
};
</script>

<style lang="scss" scoped>
...
/* 设置图片可以在三维空间里展示 */
.container .outer-container,
.container .inner-container {
transform-style: preserve-3d;
}

...

/* 设置里面的六面体样式 */
/* 嵌套的内层立方体样式 */
.inner-container > div {
position: absolute;
top: 35px;
left: 35px;
width: 130px;
height: 130px;
}
/* 里面的六面体尺寸宽高设置130px */
.inner-container img {
width: 130px;
height: 130px;
}

.inner-top {
transform: rotateX(90deg) translateZ(65px);
}

.inner-bottom {
transform: rotateX(-90deg) translateZ(65px);
}

.inner-front {
transform: rotateY(0deg) translateZ(65px);
}

.inner-back {
transform: translateZ(-65px) rotateY(180deg);
}

.inner-left {
transform: rotateY(90deg) translateZ(65px);
}

.inner-right {
transform: rotateY(-90deg) translateZ(65px);
}
</style>

最后完整代码:


<template>
<div>
<div class="container">
<!-- 外层立方体 -->
<div class="outer-container">
<div class="outer-top">
<img :src="cat1" />
</div>
<div class="outer-bottom">
<img :src="cat2" />
</div>
<div class="outer-front">
<img :src="cat3" />
</div>
<div class="outer-back">
<img :src="cat4" />
</div>
<div class="outer-left">
<img :src="cat5" />
</div>
<div class="outer-right">
<img :src="cat6" />
</div>
</div>
<!-- 内层立方体 -->
<div class="inner-container">
<div class="inner-top">
<img :src="cat7" />
</div>
<div class="inner-bottom">
<img :src="cat8" />
</div>
<div class="inner-front">
<img :src="cat9" />
</div>
<div class="inner-back">
<img :src="cat10" />
</div>
<div class="inner-left">
<img :src="cat11" />
</div>
<div class="inner-right">
<img :src="cat12" />
</div>
</div>
</div>
</div>
</template>

<script>
import cat1 from "@/assets/cats/cat1.png";
import cat2 from "@/assets/cats/cat2.png";
import cat3 from "@/assets/cats/cat3.png";
import cat4 from "@/assets/cats/cat4.png";
import cat5 from "@/assets/cats/cat5.png";
import cat6 from "@/assets/cats/cat6.png";
import cat7 from "@/assets/cats/cat7.png";
import cat8 from "@/assets/cats/cat8.png";
import cat9 from "@/assets/cats/cat9.png";
import cat10 from "@/assets/cats/cat10.png";
import cat11 from "@/assets/cats/cat11.png";
import cat12 from "@/assets/cats/cat12.png";

export default {
data() {
return {
cat1,
cat2,
cat3,
cat4,
cat5,
cat6,
cat7,
cat8,
cat9,
cat10,
cat11,
cat12,
};
},
};
</script>

<style lang="scss" scoped>
* {
margin: 0px;
padding: 0px;
}

.container {
position: relative;
margin: 0px auto;
margin-top: 9%;
margin-left: 42%;
width: 200px;
height: 200px;
transform: rotateX(-30deg) rotateY(-80deg);
transform-style: preserve-3d;
animation: rotate 15s infinite;
/* 我这里用一个边框线来更容易看到效果 */
/* border: 1px solid red; */
}

/* 旋转立方体 */
@keyframes rotate {
from {
transform: rotateX(0deg) rotateY(0deg);
}

to {
transform: rotateX(360deg) rotateY(360deg);
}
}
/* 设置图片可以在三维空间里展示 */
.container .outer-container,
.container .inner-container {
transform-style: preserve-3d;
}
/* 设置图片尺寸固定宽高200px */
.outer-container img {
width: 200px;
height: 200px;
}

/* 外层立方体样式 */
.outer-container .outer-top,
.outer-container .outer-bottom,
.outer-container .outer-right,
.outer-container .outer-left,
.outer-container .outer-front,
.outer-container .outer-back {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 200px;
border: 1px solid #fff;
opacity: 0.3;
transition: all 0.9s;
}

/* 全靠transform 属性撑起来了外面的六面体 */
.outer-top {
transform: rotateX(90deg) translateZ(100px);
}

.outer-bottom {
transform: rotateX(-90deg) translateZ(100px);
}

.outer-front {
transform: rotateY(0deg) translateZ(100px);
}

.outer-back {
transform: translateZ(-100px) rotateY(180deg);
}

.outer-left {
transform: rotateY(90deg) translateZ(100px);
}

.outer-right {
transform: rotateY(-90deg) translateZ(100px);
}

// 鼠标悬停 外面的六面体,六条边都往外扩出去,并且透明度变浅了,猫猫图片更清晰了
.cube:hover .outer-top {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(90deg) translateZ(200px);
}

.container:hover .outer-bottom {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(-90deg) translateZ(200px);
}

.container:hover .outer-front {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(0deg) translateZ(200px);
}

.container:hover .outer-back {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: translateZ(-200px) rotateY(180deg);
}

.container:hover .outer-left {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(90deg) translateZ(200px);
}

.container:hover .outer-right {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(-90deg) translateZ(200px);
}

/* 设置里面的六面体样式 */
/* 嵌套的内层立方体样式 */
.inner-container > div {
position: absolute;
top: 35px;
left: 35px;
width: 130px;
height: 130px;
}
/* 里面的六面体尺寸宽高设置130px */
.inner-container img {
width: 130px;
height: 130px;
}

.inner-top {
transform: rotateX(90deg) translateZ(65px);
}

.inner-bottom {
transform: rotateX(-90deg) translateZ(65px);
}

.inner-front {
transform: rotateY(0deg) translateZ(65px);
}

.inner-back {
transform: translateZ(-65px) rotateY(180deg);
}

.inner-left {
transform: rotateY(90deg) translateZ(65px);
}

.inner-right {
transform: rotateY(-90deg) translateZ(65px);
}
</style>


都看到这里了,求各位观众大佬们点个赞再走吧,你的赞对我非常重要



收起阅读 »

一款强大到没朋友的图片编辑插件,爱了爱了!

前言 最近用户提出了一个新的需求,老师可以批改学生的图片作业,需要对图片进行旋转、缩放、裁剪、涂鸦、标注、添加文本等。乍一听,又要掉不少头发。有没有功能强大的插件实现以上功能,让我有更多的时间去阻止女票双十一剁手呢?答案当然是有的。 效果展示涂鸦 裁剪 ...
继续阅读 »

前言


最近用户提出了一个新的需求,老师可以批改学生的图片作业,需要对图片进行旋转、缩放、裁剪、涂鸦、标注、添加文本等。乍一听,又要掉不少头发。有没有功能强大的插件实现以上功能,让我有更多的时间去阻止女票双十一剁手呢?答案当然是有的。


效果展示

涂鸦



涂鸦2.jpg


裁剪


裁剪.jpg


标注


标注2.jpg


旋转


旋转2.jpg


滤镜


1636088844(1).jpg


是不是很强大!还有众多功能我就不一一展示了。那么还等什么,跟我一起用起来吧~


安装


npm i tui-image-editor
// or
yarn add tui-image-editor

使用

快速体验



复制以下代码,将插件引入到自己的项目中。


<template>
<div class="drawing-container">
<div id="tui-image-editor"></div>
</div>

</template>
<script>
import "tui-image-editor/dist/tui-image-editor.css";
import "tui-color-picker/dist/tui-color-picker.css";
import ImageEditor from "tui-image-editor";
export default {
data() {
return {
instance: null,
};
},
mounted() {
this.init();
},
methods: {
init() {
this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);
document.getElementsByClassName("tui-image-editor-main")[0].style.top = "45px"; // 图片距顶部工具栏的距离
},
},
};
</script>


<style lang="scss" scoped>
.drawing-container {
height: 900px;
}
</style>


可以看到活生生的图片编辑工具就出现了,是不是很简单:


初始效果.jpg


国际化


由于是老外开发的,默认的文字描述都是英文,这里我们先汉化一下:


const locale_zh = {
ZoomIn: "放大",
ZoomOut: "缩小",
Hand: "手掌",
History: '历史',
Resize: '调整宽高',
Crop: "裁剪",
DeleteAll: "全部删除",
Delete: "删除",
Undo: "撤销",
Redo: "反撤销",
Reset: "重置",
Flip: "镜像",
Rotate: "旋转",
Draw: "画",
Shape: "形状标注",
Icon: "图标标注",
Text: "文字标注",
Mask: "遮罩",
Filter: "滤镜",
Bold: "加粗",
Italic: "斜体",
Underline: "下划线",
Left: "左对齐",
Center: "居中",
Right: "右对齐",
Color: "颜色",
"Text size": "字体大小",
Custom: "自定义",
Square: "正方形",
Apply: "应用",
Cancel: "取消",
"Flip X": "X 轴",
"Flip Y": "Y 轴",
Range: "区间",
Stroke: "描边",
Fill: "填充",
Circle: "圆",
Triangle: "三角",
Rectangle: "矩形",
Free: "曲线",
Straight: "直线",
Arrow: "箭头",
"Arrow-2": "箭头2",
"Arrow-3": "箭头3",
"Star-1": "星星1",
"Star-2": "星星2",
Polygon: "多边形",
Location: "定位",
Heart: "心形",
Bubble: "气泡",
"Custom icon": "自定义图标",
"Load Mask Image": "加载蒙层图片",
Grayscale: "灰度",
Blur: "模糊",
Sharpen: "锐化",
Emboss: "浮雕",
"Remove White": "除去白色",
Distance: "距离",
Brightness: "亮度",
Noise: "噪音",
"Color Filter": "彩色滤镜",
Sepia: "棕色",
Sepia2: "棕色2",
Invert: "负片",
Pixelate: "像素化",
Threshold: "阈值",
Tint: "色调",
Multiply: "正片叠底",
Blend: "混合色",
Width: "宽度",
Height: "高度",
"Lock Aspect Ratio": "锁定宽高比例",
};

this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);

效果如下:


汉化.jpg


自定义样式


默认风格为暗黑系,如果想改成白底,或者想改变按钮的大小、颜色等样式,可以使用自定义样式。


const customTheme = {
"common.bi.image": "", // 左上角logo图片
"common.bisize.width": "0px",
"common.bisize.height": "0px",
"common.backgroundImage": "none",
"common.backgroundColor": "#f3f4f6",
"common.border": "1px solid #333",

// header
"header.backgroundImage": "none",
"header.backgroundColor": "#f3f4f6",
"header.border": "0px",

// load button
"loadButton.backgroundColor": "#fff",
"loadButton.border": "1px solid #ddd",
"loadButton.color": "#222",
"loadButton.fontFamily": "NotoSans, sans-serif",
"loadButton.fontSize": "12px",
"loadButton.display": "none", // 隐藏

// download button
"downloadButton.backgroundColor": "#fdba3b",
"downloadButton.border": "1px solid #fdba3b",
"downloadButton.color": "#fff",
"downloadButton.fontFamily": "NotoSans, sans-serif",
"downloadButton.fontSize": "12px",
"downloadButton.display": "none", // 隐藏

// icons default
"menu.normalIcon.color": "#8a8a8a",
"menu.activeIcon.color": "#555555",
"menu.disabledIcon.color": "#ccc",
"menu.hoverIcon.color": "#e9e9e9",
"submenu.normalIcon.color": "#8a8a8a",
"submenu.activeIcon.color": "#e9e9e9",

"menu.iconSize.width": "24px",
"menu.iconSize.height": "24px",
"submenu.iconSize.width": "32px",
"submenu.iconSize.height": "32px",

// submenu primary color
"submenu.backgroundColor": "#1e1e1e",
"submenu.partition.color": "#858585",

// submenu labels
"submenu.normalLabel.color": "#858585",
"submenu.normalLabel.fontWeight": "lighter",
"submenu.activeLabel.color": "#fff",
"submenu.activeLabel.fontWeight": "lighter",

// checkbox style
"checkbox.border": "1px solid #ccc",
"checkbox.backgroundColor": "#fff",

// rango style
"range.pointer.color": "#fff",
"range.bar.color": "#666",
"range.subbar.color": "#d1d1d1",

"range.disabledPointer.color": "#414141",
"range.disabledBar.color": "#282828",
"range.disabledSubbar.color": "#414141",

"range.value.color": "#fff",
"range.value.fontWeight": "lighter",
"range.value.fontSize": "11px",
"range.value.border": "1px solid #353535",
"range.value.backgroundColor": "#151515",
"range.title.color": "#fff",
"range.title.fontWeight": "lighter",

// colorpicker style
"colorpicker.button.border": "1px solid #1e1e1e",
"colorpicker.title.color": "#fff",
};

this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
theme: customTheme, // 自定义样式
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);

效果如下:


自定义样式.jpg


按钮优化


通过自定义样式,我们看到右上角的 Load 和 Download 按钮已经被隐藏了,接下来我们再隐藏掉其他用不上的按钮(根据业务需要),并添加一个保存图片的按钮。


<template>
<div class="drawing-container">
<div id="tui-image-editor"></div>
<el-button class="save" type="primary" size="small" @click="save">保存</el-button>
</div>

</template>

// ...
methods: {
init() {
this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
menu: ["resize", "crop", "rotate", "draw", "shape", "icon", "text", "filter"], // 底部菜单按钮列表 隐藏镜像flip和遮罩mask
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
theme: customTheme, // 自定义样式
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);
document.getElementsByClassName("tui-image-editor-main")[0].style.top ="45px"; // 调整图片显示位置
document.getElementsByClassName("tie-btn-reset tui-image-editor-item help") [0].style.display = "none"; // 隐藏顶部重置按钮
},
// 保存图片,并上传
save() {
const base64String = this.instance.toDataURL(); // base64 文件
const data = window.atob(base64String.split(",")[1]);
const ia = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
ia[i] = data.charCodeAt(i);
}
const blob = new Blob([ia], { type: "image/png" }); // blob 文件
const form = new FormData();
form.append("image", blob);
// upload file
},
}

<style lang="scss" scoped>
.drawing-container {
height: 900px;
position: relative;
.save {
position: absolute;
right: 50px;
top: 15px;
}
}
</style>

效果如下:


按钮优化.jpg


可以看到顶部的重置按钮,以及底部的镜像和遮罩按钮都已经不见了。右上角多了一个我们自己的保存按钮,点击按钮,可以获取到 base64 文件和 blob 文件。


完整代码


<template>
<div class="drawing-container">
<div id="tui-image-editor"></div>
<el-button class="save" type="primary" size="small" @click="save">保存</el-button>
</div>

</template>
<script>
import 'tui-image-editor/dist/tui-image-editor.css'
import 'tui-color-picker/dist/tui-color-picker.css'
import ImageEditor from 'tui-image-editor'
const locale_zh = {
ZoomIn: '放大',
ZoomOut: '缩小',
Hand: '手掌',
History: '历史',
Resize: '调整宽高',
Crop: '裁剪',
DeleteAll: '全部删除',
Delete: '删除',
Undo: '撤销',
Redo: '反撤销',
Reset: '重置',
Flip: '镜像',
Rotate: '旋转',
Draw: '画',
Shape: '形状标注',
Icon: '图标标注',
Text: '文字标注',
Mask: '遮罩',
Filter: '滤镜',
Bold: '加粗',
Italic: '斜体',
Underline: '下划线',
Left: '左对齐',
Center: '居中',
Right: '右对齐',
Color: '颜色',
'Text size': '字体大小',
Custom: '自定义',
Square: '正方形',
Apply: '应用',
Cancel: '取消',
'Flip X': 'X 轴',
'Flip Y': 'Y 轴',
Range: '区间',
Stroke: '描边',
Fill: '填充',
Circle: '圆',
Triangle: '三角',
Rectangle: '矩形',
Free: '曲线',
Straight: '直线',
Arrow: '箭头',
'Arrow-2': '箭头2',
'Arrow-3': '箭头3',
'Star-1': '星星1',
'Star-2': '星星2',
Polygon: '多边形',
Location: '定位',
Heart: '心形',
Bubble: '气泡',
'Custom icon': '自定义图标',
'Load Mask Image': '加载蒙层图片',
Grayscale: '灰度',
Blur: '模糊',
Sharpen: '锐化',
Emboss: '浮雕',
'Remove White': '除去白色',
Distance: '距离',
Brightness: '亮度',
Noise: '噪音',
'Color Filter': '彩色滤镜',
Sepia: '棕色',
Sepia2: '棕色2',
Invert: '负片',
Pixelate: '像素化',
Threshold: '阈值',
Tint: '色调',
Multiply: '正片叠底',
Blend: '混合色',
Width: '宽度',
Height: '高度',
'Lock Aspect Ratio': '锁定宽高比例'
}

const customTheme = {
"common.bi.image": "", // 左上角logo图片
"common.bisize.width": "0px",
"common.bisize.height": "0px",
"common.backgroundImage": "none",
"common.backgroundColor": "#f3f4f6",
"common.border": "1px solid #333",

// header
"header.backgroundImage": "none",
"header.backgroundColor": "#f3f4f6",
"header.border": "0px",

// load button
"loadButton.backgroundColor": "#fff",
"loadButton.border": "1px solid #ddd",
"loadButton.color": "#222",
"loadButton.fontFamily": "NotoSans, sans-serif",
"loadButton.fontSize": "12px",
"loadButton.display": "none", // 隐藏

// download button
"downloadButton.backgroundColor": "#fdba3b",
"downloadButton.border": "1px solid #fdba3b",
"downloadButton.color": "#fff",
"downloadButton.fontFamily": "NotoSans, sans-serif",
"downloadButton.fontSize": "12px",
"downloadButton.display": "none", // 隐藏

// icons default
"menu.normalIcon.color": "#8a8a8a",
"menu.activeIcon.color": "#555555",
"menu.disabledIcon.color": "#ccc",
"menu.hoverIcon.color": "#e9e9e9",
"submenu.normalIcon.color": "#8a8a8a",
"submenu.activeIcon.color": "#e9e9e9",

"menu.iconSize.width": "24px",
"menu.iconSize.height": "24px",
"submenu.iconSize.width": "32px",
"submenu.iconSize.height": "32px",

// submenu primary color
"submenu.backgroundColor": "#1e1e1e",
"submenu.partition.color": "#858585",

// submenu labels
"submenu.normalLabel.color": "#858585",
"submenu.normalLabel.fontWeight": "lighter",
"submenu.activeLabel.color": "#fff",
"submenu.activeLabel.fontWeight": "lighter",

// checkbox style
"checkbox.border": "1px solid #ccc",
"checkbox.backgroundColor": "#fff",

// rango style
"range.pointer.color": "#fff",
"range.bar.color": "#666",
"range.subbar.color": "#d1d1d1",

"range.disabledPointer.color": "#414141",
"range.disabledBar.color": "#282828",
"range.disabledSubbar.color": "#414141",

"range.value.color": "#fff",
"range.value.fontWeight": "lighter",
"range.value.fontSize": "11px",
"range.value.border": "1px solid #353535",
"range.value.backgroundColor": "#151515",
"range.title.color": "#fff",
"range.title.fontWeight": "lighter",

// colorpicker style
"colorpicker.button.border": "1px solid #1e1e1e",
"colorpicker.title.color": "#fff",
};
export default {
data() {
return {
instance: null
}
},
mounted() {
this.init()
},
methods: {
init() {
this.instance = new ImageEditor(document.querySelector('#tui-image-editor'), {
includeUI: {
loadImage: {
path: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image',
name: 'image'
},
menu: ['resize', 'crop', 'rotate', 'draw', 'shape', 'icon', 'text', 'filter'], // 底部菜单按钮列表 隐藏镜像flip和遮罩mask
initMenu: 'draw', // 默认打开的菜单项
menuBarPosition: 'bottom', // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
theme: customTheme // 自定义样式
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600 // canvas 最大高度
})
document.getElementsByClassName('tui-image-editor-main')[0].style.top = '45px' // 调整图片显示位置
document.getElementsByClassName(
'tie-btn-reset tui-image-editor-item help'
)[0].style.display = 'none' // 隐藏顶部重置按钮
},
// 保存图片,并上传
save() {
const base64String = this.instance.toDataURL() // base64 文件
const data = window.atob(base64String.split(',')[1])
const ia = new Uint8Array(data.length)
for (let i = 0; i < data.length; i++) {
ia[i] = data.charCodeAt(i)
}
const blob = new Blob([ia], { type: 'image/png' }) // blob 文件
const form = new FormData()
form.append('image', blob)
// upload file
}
}
}
</script>


<style lang="scss" scoped>
.drawing-container {
height: 900px;
position: relative;
.save {
position: absolute;
right: 50px;
top: 15px;
}
}
</style>


总结


以上就是 tui.image-editor 的基本使用方法,相比其他插件,tui.image-editor 的优势是功能强大,简单易上手。


插件固然好用,但本人也发现一个小 bug,当放大图片,用手掌拖动显示位置,再点击重置按钮时,图片很可能就消失不见了。解决办法有两个,一是改源码,在重置之前,先调用 resetZoom 方法,还原缩放比列;二是自己做一个重置按钮,点击之后调用 this.init 方法重新进行渲染。



收起阅读 »

超详细讲解页面加载过程

说一说从输入URL到页面呈现发生了什么?(知识点) ❝ 这个题可以说是面试最常见也是一道可以无限难的题了,一般面试官出这道题就是为了考察你的前端知识的深度与广度。 ❞ 1.浏览器接受URL开启网络请求线程(涉及到:浏览器机制,线程与进程等) 2.开启网络线...
继续阅读 »

说一说从输入URL到页面呈现发生了什么?(知识点)




这个题可以说是面试最常见也是一道可以无限难的题了,一般面试官出这道题就是为了考察你的前端知识的深度与广度。




1.浏览器接受URL开启网络请求线程(涉及到:浏览器机制,线程与进程等)


2.开启网络线程到发出一个完整的http请求(涉及到:DNS解析,TCP/IP请求,5层网络协议等)


3.从服务器接收到请求到对应后台接受到请求(涉及到:负载均衡,安全拦截,后台内部处理等)


4.后台与前台的http交互(涉及到:http头,响应码,报文结构,cookie等)


5.缓存问题(涉及到:http强缓存与协商缓存等)(请看上一篇文章[这些浏览器面试题,看看你能回答几个?](juejin.cn/post/702653…


6.浏览器接受到http数据包后的解析流程(涉及到html词法分析,解析成DOM树,解析CSS生成CSSOM树,合并生成render渲染树。然后layout布局,painting渲染,复合图层合成,GPU绘制,等)


在浏览器地址栏输入URL


当我们在浏览器地址栏输入URL地址后,浏览器会开一个线程来对我们输入的URL进行解析处理。


浏览器中的各个进程及作用:(多进程)



  • 浏览器进程:负责管理标签页的创建销毁以及页面的显示,资源下载等。

  • 第三方插件进程:负责管理第三方插件。

  • GPU进程:负责3D绘制与硬件加速(最多一个)。

  • 渲染进程:负责页面文档解析(HTML,CSS,JS),执行与渲染。(可以有多个)


DNS域名解析


为什么需要DNS域名解析?


因为我们在浏览器中输入的URL通常是一个域名,并不会直接去输入IP地址(纯粹因为域名比IP好记),但我们的计算机并不认识域名,它只知道IP,所以就需要这一步操作将域名解析成IP。


URL组成部分



  • protocol:协议头,比如http,https,ftp等;

  • host:主机域名或者IP地址;

  • port:端口号;

  • path:目录路径;

  • query:查询的参数;

  • hash:#后边的hash值,用来定位某一个位置。


解析过程



  • 首先会查看浏览器DNS缓存,有的话直接使用浏览器缓存

  • 没有的话就查询计算机本地DNS缓存(localhost)

  • 还没有就询问递归式DNS服务器(就是网络提供商,一般这个服务器都会有自己的缓存)

  • 如果依然没有缓存,那就需要通过 根域名服务器 和TLD域名服务器 再到对应的 权威DNS服务器 找记录,并缓存到 递归式服务器,然后 递归服务器 再将记录返回给本地


「⚠️注意:」




DNS解析是非常耗时的,如果页面中需要解析的域名过多,是非常影响页面性能的。考虑使用dns与加载或减少DNS解析进行优化。




发送HTTP请求


拿到了IP地址后,就可以发起HTTP请求了。HTTP请求的本质就是TCP/IP的请求构建。建立连接时需要**「3次握手」进行验证,断开链接也同样需要「4次挥手」**进行验证,保证传输的可靠性


3次握手



  • 第一次握手:客户端发送位码为 SYN = 1(SYN 标志位置位),随机产生初始序列号 Seq = J 的数据包到服务器。服务器由 SYN = 1(置位)知道,客户端要求建立联机。

  • 第二次握手:服务器收到请求后要确认联机信息,向客户端发送确认号Ack = (客户端的Seq +1,J+1),SYN = 1,ACK = 1(SYN,ACK 标志位置位),随机产生的序列号 Seq = K 的数据包。

  • 第三次握手:客户端收到后检查 Ack 是否正确,即第一次发送的 Seq +1(J+1),以及位码ACK是否为1。若正确,客户端会再发送 Ack = (服务器端的Seq+1,K+1),ACK = 1,以及序号Seq为服务器确认号J 的确认包。服务器收到后确认之前发送的 Seq(K+1) 值与 ACK= 1 (ACK置位)则连接建立成功。


3次握手.gif


「直白理解:」


(客户端:hello,你是server么?服务端:hello,我是server,你是client么 客户端:yes,我是client 建立成功之后,接下来就是正式传输数据。)


4次挥手



  • 客户端发送一个FIN Seq = M(FIN置位,序号为M)包,用来关闭客户端到服务器端的数据传送。

  • 服务器端收到这个FIN,它发回一个ACK,确认序号Ack 为收到的序号M+1。

  • 服务器端关闭与客户端的连接,发送一个FIN Seq = N 给客户端。

  • 客户端发回ACK 报文确认,确认序号Ack 为收到的序号N+1。


4次挥手.gif


「直白理解:」


(主动方:我已经关闭了向你那边的主动通道了,只能被动接收了 被动方:收到通道关闭的信息 被动方:那我也告诉你,我这边向你的主动通道也关闭了 主动方:最后收到数据,之后双方无法通信)


五层网络协议


1、应用层(DNS,HTTP):DNS解析成IP并发送http请求;


2、传输层(TCP,UDP):建立TCP连接(3次握手);


3、网络层(IP,ARP):IP寻址;


4、数据链路层(PPP):封装成帧;


5、物理层(利用物理介质传输比特流):物理传输(通过双绞线,电磁波等各种介质)。


「OSI七层框架:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层」


服务器接收请求做出响应


HTTP 请求到达服务器,服务器进行对应的处理。 最后要把数据传给浏览器,也就是返回网络响应。


跟请求部分类似,网络响应具有三个部分:响应行、响应头和响应体。


响应完成之后怎么办?TCP 连接就断开了吗?


不一定。这时候要判断Connection字段, 如果请求头或响应头中包含Connection: Keep-Alive, 表示建立了持久连接,这样TCP连接会一直保持,之后请求统一站点的资源会复用这个连接。否则断开TCP连接, 请求-响应流程结束。


状态码


状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:



  • 1xx:指示信息–表示请求已接收,继续处理。

  • 2xx:成功–表示请求已被成功接收、理解、接受。

  • 3xx:重定向–要完成请求必须进行更进一步的操作。

  • 4xx:客户端错误–请求有语法错误或请求无法实现。

  • 5xx:服务器端错误–服务器未能实现合法的请求。 平时遇到比较常见的状态码有:200, 204, 301, 302, 304, 400, 401, 403, 404, 422, 500(分别表示什么请自行查找)。


服务器返回相应文件


请求成功后,服务器会返回相应的网页,浏览器接收到响应成功的报文后便开始下载网页,至此,网络通信结束。


浏览器解析渲染页面


浏览器在接收到HTML,CSS,JS文件之后,它是如何将页面渲染在屏幕上的?


render.png


解析HTML构建DOM Tree


浏览器在拿到服务器返回的网页之后,首先会根据顶部定义的DTD类型进行对应的解析,解析过程将被交给内部的GUI渲染线程来处理。


「DTD(Document Type Definition)文档类型定义」


常见的文档类型定义


//HTML5文档定义
<!DOCTYPE html>
//用于XHTML 4.0 的严格型 
<!DOCTYPE HTMLPUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 
//用于XHTML 4.0 的过渡型 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 
//用于XHTML 1.0 的严格型 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
//用于XHTML 1.0 的过渡型
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

HTML解释器的工作就是将网络或者本地磁盘获取的HTML网页或资源从字节流解释成DOM树🌲结构


HTML解释器.png


通过上图可以清楚的了解这一过程:首先是字节流,经过解码之后是字符流,然后通过词法分析器会被解释成词语(Tokens),之后经过语法分析器构建成节点,最后这些节点被组建成一颗 DOM 树。


对于线程化的解释器,字符流后的整个解释、布局和渲染过程基本会交给一个单独的渲染线程来管理(不是绝对的)。由于 DOM 树只能在渲染线程上创建和访问,所以构建 DOM 树的过程只能在渲染线程中进行。但是,从字符串到词语这个阶段可以交给单独的线程来做,Chrome 浏览器使用的就是这个思想。在解释成词语之后,Webkit 会分批次将结果词语传递回渲染线程。


这个过程中,如果遇到的节点是 JS 代码,就会调用 JS引擎 对 JS代码进行解释执行,此时由于 JS引擎GUI渲染线程 的互斥,GUI渲染线程 就会被挂起,渲染过程停止,如果 JS 代码的运行中对DOM树进行了修改,那么DOM的构建需要从新开始


如果节点需要依赖其他资源,图片/CSS等等,就会调用网络模块的资源加载器来加载它们,它们是异步的,不会阻塞当前DOM树的构建


如果遇到的是 JS 资源URL(没有标记异步),则需要停止当前DOM的构建,直到 JS 的资源加载并被 JS引擎 执行后才继续构建DOM


解析CSS构建CSSOM Tree


CSS解释器会将CSS文件解释成内部表示结构,生成CSS规则树,这个过程也是和DOM解析类似的,CSS 字节转换成字符,接着词法解析与法解析,最后构成 CSS对象模型(CSSOM) 的树结构


构建渲染树(Render Tree)


DOM TreeCSSOM Tree都构建完毕后,接着将它们合并成渲染树(Render Tree)渲染树 只包含渲染网页所需的节点,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上。


渲染(布局,绘制,合成)



  • 计算CSS样式 ;

  • 构建渲染树 ;

  • 布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性 ;

  • 绘制,将图像绘制出来。


这个过程比较复杂,涉及到两个概念: reflow(回流)和repain(重绘)。DOM节点中的各个元素都是以盒模型的形式存在,这些都需要浏览器去计算其位置和大小等,这个过程称为relow;当盒模型的位置,大小以及其他属性,如颜色,字体,等确定下来之后,浏览器便开始绘制内容,这个过程称为repain。页面在首次加载时必然会经历reflow和repain。reflow和repain过程是非常消耗性能的,尤其是在移动设备上,它会破坏用户体验,有时会造成页面卡顿。所以我们应该尽可能少的减少reflow和repain。


这里Reflow和Repaint的概念是有区别的:


(1)Reflow:即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树。


(2)Repaint:即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了。


回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流, 所以优化方案中一般都包括,尽量避免回流。


「回流一定导致重绘,但重绘不一定会导致回流」


「合成(composite)」


最后一步合成( composite ),这一步骤浏览器会将各层信息发送给GPU,GPU将各层合成,显示在屏幕上


普通图层和复合图层


可以简单的这样理解,浏览器渲染的图层一般包含两大类:普通图层以及复合图层


首先,普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中)


其次,absolute布局(fixed也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层


然后,可以通过硬件加速的方式,声明一个新的复合图层,它会单独分配资源 (当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘)


可以简单理解下:「GPU中,各个复合图层是单独绘制的,所以互不影响」,这也是为什么某些场景硬件加速效果一级棒


可以Chrome源码调试 -> More Tools -> Rendering -> Layer borders中看到,黄色的就是复合图层信息。



收起阅读 »

VS Code settings.json 10 个高(装)阶(杯)配置!

1. 隐藏活动栏 VS Code 左侧图标列表是“活动栏”,我们可以点击图标跳转到各个模块,我们可以通过配置 workbench.activityBar.visible 来控制活动栏的显示; 如果你想恢复显示,可以自定义快捷键来再次显示这块空间; 如何设置...
继续阅读 »

1. 隐藏活动栏


VS Code 左侧图标列表是“活动栏”,我们可以点击图标跳转到各个模块,我们可以通过配置 workbench.activityBar.visible 来控制活动栏的显示;


image.png


如果你想恢复显示,可以自定义快捷键来再次显示这块空间;


image.png


如何设置快捷键:keybindings


我们可以用 Ctrl+B 来隐藏/显示文件资源管理器,用 Ctrl+Alt+B 来隐藏/显示活动栏;


虽然,你也可以在命令面板 Ctrl+Shift+P 中搜索,不过使用快捷键就更有装杯效果~


活动栏在隐藏状态下,我们也可以通过快捷键跳转到不同的工作空间,比如 Ctrl+Shift+E(跳转到文件资源管理器)、Ctrl+Shift+X(跳转到扩展)、Ctrl+Shift+H(搜索和替换)等


2. AI 编码


GitHub Copilot 是 VS Code 的一个扩展,可在你编写代码时生成片段代码;


由于它是人工智能、机器学习,有可能会产生一些你不喜欢的代码,但是请别仇视它,毕竟 AI 编码是未来趋势!


image.png


处于隐私考虑,建议不要在工作中使用 Copilot,但是可以在个人项目中使用它,有趣又有用,尤其是对于单元测试;


可以在 settings.json 中配置 Copilot;


3. 字体与缩放


这个不多做解释,根据自己的需求进行文字大小及缩放比例的配置;


image.png


当然,你不一定要在 settings.json 中去编写这个配置,也可以在可选项及输入配置窗口进行配置。


4. 无拖拽/删除确认


如果你对自己的编程技能足够自信,或者对 VS Code 的 Ctrl+Z 足够自信,你可以配置取消删除确认;因为拖拽/删除确认有时也会干扰思路~


image.png


image.png


5. 自更新绝对路径


VS Code 的最佳功能之一是它的文件导入很友善,使用绝对路径,例如:@/components/Button../../Button 更让人舒适;


当移动文件重新组织目录时,希望 VS Code 能自动更新文件的路径?你可以配置它们:


image.png


请注意,您需要在 .tsconfig/.jsconfig 文件中配置路径才能使用绝对路径导入。


6. 保存执行


配置过 ESLint 保存修正的应该都知道这个配置。这个非常强大,出了 fixAll,还能 addMissingImports 补充缺少的 Imports,或者其它你想在保存后执行的行为;


image.png


这个配置就像是编程魔法~


7. CSS 格式化


你可能已经在使用 Stylelint 了,如果没有,请在配置中设置它!


image.png


另一个设置是 editor.suggest.insertMode,当设置为“replace”时,意味着——当你选择一个提示并按 Tab 或 Enter 时,将替换整个文本为提示,这非常有用。


8. 开启 Emmet


你可能熟悉 Emmet —— Web 开发人员必备工具包,如果没有,请设置它;虽然它内置于 VS Code,但必须手动配置启用;


image.png


9. Tailwind CSS


Tailwind CSS 是一个功能类优先的 CSS 框架,它集成了诸如 flexpt-4text-center 和 rotate-90 这样的的类,它们能直接在脚本标记语言中组合起来,构建出任何设计。


虽然它目前尚未内置在 VS Code 中,但可作为免费的 VS Code 扩展进行安装使用,还可以配置附加设置增强它的功能!


image.png


10. 单击打开文件


VS Code 默认用户界面,有个奇怪的现象,它需要双击才能从文件资源管理器中打开文件。


单击一下得到的是奇怪的“预览”模式,当你单击下一个文件时,第一个文件就会消失。这就像只有一个标签。


image.png


需要进行这个配置,关闭后,单击将在新选项卡中打开文件。问题解决了~


将配置用 Settings Sync 进行同步,去哪都能个性化、自定义!酷的!


image.png




以上就是本篇分享,你有啥压箱底的 VS Code-settings.json 配置吗?欢迎评论留言,分享交流 (#^.^#)



收起阅读 »

(转载) 爆火的「元宇宙」概念究竟可以为企业带来什么翻天覆地的变化?

“元宇宙”这个概念,最近在全球投资市场突然爆红,近期频繁上热搜的互联网巨头Facebook都改名为Meta(Meta为元宇宙“MetaVerse”的前缀)。承诺让用户在相互连接的虚拟世界中生活、工作和娱乐。那“元宇宙”到底是个什么东西呢?投资市场对它的解释是“...
继续阅读 »


“元宇宙”这个概念,最近在全球投资市场突然爆红,近期频繁上热搜的互联网巨头Facebook都改名为Meta(Meta为元宇宙“MetaVerse”的前缀)。承诺让用户在相互连接的虚拟世界中生活、工作和娱乐。

那“元宇宙”到底是个什么东西呢?投资市场对它的解释是“包涵万物,无所不联”、“所见非所见,所想即所得”,听着就很玄乎是吗?

其实可以把十年前火过的“虚拟增强现实技术”的超级强化版,戴VR眼镜玩游戏,看做是元宇宙的石器时代。

元宇宙(Metaverse)这个词,也不是投资市场凭空捏造的,它源于1992年的科幻小说《雪崩》。小说里有一个虚拟现实世界,人们在里面利用自己的数字化身社交、竞争。这一概念所对应的场景在电影作品中曾多次出现,比如《黑客帝国》《头号玩家》都曾描绘元宇宙场景:人可以在一个完全数字化的虚拟世界中以化身形式生活,获得超越现实的沉浸式体验。

另一个更直观的例子,就是2018年的科幻电影《头号玩家》,电影里的虚拟世界“绿洲”(Oasis)被认为是元宇宙的一个标准。

现在,许多科技公司宣称,当前的互联网已经走到了瓶颈,互联网的下一个阶段就是“元宇宙”,他们要把以前只存在于艺术作品中的“数字平行世界”,搬到现实中来。我们认为,在这样一个时代来临的时候应该准备好,让用户在元宇宙的空间下去实现真正的、超现实的社交体验。

在2021年10月24日程序员节上,蒲公英企服平台发布了Tracup企业元宇宙。这是源自于2016年发布的项目协同管理平台Tracup,通过项目管理协作、实时音视频互动和AR增强现实技术,对数字化企业和组织生产里进行了空间延伸。其核心使命是把传统的项目协同管理的效率大幅度的提升,面向开发者和数字化组织知识工作者的项目协同管理,链接任务、沟通、文件、代码、适配需求、任务、缺陷和迭代管理,针对人员、工作流和OKR进行评估管理,提供敏捷、瀑布、通用任务协同等多种项目模版,实时视频连线、元宇宙场景沉浸沟通,同时满足当下后疫情时代中的不确定,帮助DevOps开发者和知识工作者克服空间阻碍,在办公室、出差、隔离、甚至运动、出行时,可以更加高效地对项目进行管理,对任务进行追踪,同步工作进程、文件和代码,组织有效的会议,与团队、协作伙伴、供应链和客户随时沟通,并将会议和沟通结果同步到工作流当中,帮助企业实现数字世界与现实世界融为一体,在现实增强的企业元宇宙里永续工作、沟通与协作。

面对疫情带来的隔离、出行停滞、交通阻断,原有的企业和组织的工作环境、运行方式显得手足无措、难以从容应对,Tracup项目协同管理通讯平台和元宇宙沉浸场景的Tracup AR眼镜,将对后疫情时代的企业的工作形式和生产力提升带来巨大的影响。


原文链接:https://juejin.cn/post/7026240445692772389

收起阅读 »

(转载)元宇宙是什么,离企业应用还有多远?

Tracup使用空间智能与虚拟现实技术,将协同办公等场景构建在所有网络可以延伸到的区域之上。今年以来,元宇宙成为企业与消费者关心的热门话题。脸书、微软、腾讯、阿里、英伟达等互联网与硬件科技公司躬身入局。据虚拟现实研究领域专家介绍,元宇宙作为虚拟世界与现实世界交...
继续阅读 »

Tracup使用空间智能与虚拟现实技术,将协同办公等场景构建在所有网络可以延伸到的区域之上。

今年以来,元宇宙成为企业与消费者关心的热门话题。脸书、微软、腾讯、阿里、英伟达等互联网与硬件科技公司躬身入局。据虚拟现实研究领域专家介绍,元宇宙作为虚拟世界与现实世界交融的所在,蕴含着工作、社交、内容、游戏等场景革新的重大机遇。

1.png

国内头部厂商已率先完成元宇宙布局

早在2020年底,马化腾就在腾讯内部刊物发文表示“一个令人兴奋的机会正在到来,移动互联网十年发展,即将迎来下一波升级,我们称之为全真互联网。”随后,腾讯迅速出手布局元宇宙赛道,投资了如罗布乐思(Roblox)在内的多家元宇宙概念股。

同样在今年四月,内容平台巨头字节跳动也投资了元宇宙概念公司“代码乾坤”,八月又收购了虚拟现实硬件设备厂商Pico,近日又将西瓜、火山视频等产品并入抖音系,其元宇宙阵列初见端倪。

除此以外,网易、莉莉丝等以游戏制作出名的公司也纷纷进行了元宇宙相关的布局。

抢在C端之前,B端元宇宙已实现落地方案

作为深受研发与项目管理者喜爱的项目协同工具——Tracup,早在2019年就意识到一个革命性的科技窗口即将打开,开始调整产品结构并布局硬件设施,经过两年的努力,面向B端的企业元宇宙的“数字基石”已然打造完成,实现了将协同办公、远距离办公、全场景应用的工程级AR智能眼镜与办公场景的适配,并以此为基础构建了各类虚拟应用。

2.png

举个例子:当你出门在外游玩时,可以不再携带相机乃至手机拍照,工程级AR智能眼镜即可帮你实现高清摄像。面对紧急的工作需求,可以通过手势操作迅速处理,轻松实现发文件、语音、视频、手势输入等等操作。通过项目管理协作、实时音视频互动和AR增强现实技术,对数字化企业和组织生产力进行了空间延伸,帮助DevOps开发者和知识工作者克服空间阻碍,在办公室、出差、隔离、甚至运动、出行时,可以更加高效地对项目进行管理。

设计方面,Tracup企业元宇宙AR智能眼镜采用了工业合金架构设计,拥有68克超轻重量,可以折叠成普通眼镜放在口袋里。同时,采用折返式的光学方案,实现了143英寸等效1080P高清画质,拥有75hz的屏幕刷新率,支持2D/3D无缝切换,具备100000:1的屏幕对比度,即使在夜晚也感受不到 AR 内容的底光。此外实现了快速精准匹配近视用户人群的近视度数。

那么,到底什么是元宇宙?

元宇宙概念最早出现在1992年,美国作家尼尔在其科幻小说《雪崩》中描绘了一个平行于现实世界的虚拟数字世界——“元界”,现实世界中的人在“元界”中都有一个虚拟分身,人们通过控制这个虚拟分身来相互竞争以提高地位。

事实上,当前科技界对元宇宙并无权威定义。

有学者认为,元宇宙是整合多种新技术而产生的新型虚实相融的互联网应用和社会形态,它基于扩展现实技术提供沉浸式体验,以及数字孪生技术生成现实世界的镜像,通过区块链技术搭建经济体系,将虚拟世界与现实世界在经济系统、社交系统、身份系统上密切融合,并且允许每个用户进行内容生产和编辑。

扎克伯格在解释公司为何改名时,用一段视频直观展示了元宇宙的未来:可以创造一个虚拟的“家”,邀请熟悉的人开展社交,戴上设备就可以进入一个虚拟的工作空间与同事一起工作,甚至可以创造一个虚拟世界……

3.png

不少国际知名咨询企业看好元宇宙的未来,如彭博行业研究报告预计元宇宙将在2024年达到8000亿美元市场规模,普华永道预计元宇宙市场规模在2030年将达到1.5万亿美元。

业内人士建议,在法律层面,总结提炼在网络平台发展过程中的治理经验,加强元宇宙前瞻性立法研究,关注监管审查、数据安全等问题。作为B端元宇宙的探索者和领导者,Tracup项目协同管理通讯平台和元宇宙沉浸场景的Tracup AR眼镜系列服务将确保依法合规,持续为后疫情时代的企业的工作形式和生产力带来新的提升与更为全面的解决方案。

原文链接: https://juejin.cn/post/7026273353962897445

收起阅读 »

(转载)最近大火的「元宇宙」究竟是什么

如果要问当下最火的概念是什么,那必然是【元宇宙】。元宇宙到底有多火,对互联网行业有多重要?从 Facebook 创始人兼首席执行官马克·扎克伯格近日的一段采访中可窥知一二。在 The Verge 的专访里,这家世界最大的社交平台掌舵者表示:希望在未来用 5 年...
继续阅读 »

如果要问当下最火的概念是什么,那必然是【元宇宙】。

元宇宙到底有多火,对互联网行业有多重要?从 Facebook 创始人兼首席执行官马克·扎克伯格近日的一段采访中可窥知一二。在 The Verge 的专访里,这家世界最大的社交平台掌舵者表示:希望在未来用 5 年左右的时间,将 Facebook 打造为一家元宇宙公司,并且,为了迎接元宇宙时代的到来,Facebook也在近日将公司的名字改为Meta。元宇宙概念的火爆还体现在,今年的 ChinaJoy 上有关元宇宙的发言屡见报端、连芯片巨头英伟达也忍不住“蹭热点”,等等。

那么问题来了,元宇宙到底是什么?它对我们现在的行业又会产生哪些影响?

一、什么是元宇宙

元宇宙的英语是 Metaverse,Meta 表示“超越”、“元”, verse 表示“宇宙 universe”。元宇宙这个概念最早出现在 1992 年尼尔·斯蒂芬森的科幻小说《雪崩》当中,小说描绘了一个平行于现实世界的虚拟数字世界,在这个虚拟的数字世界,人们用数字化身来控制并相互竞争以提高自己的地位。

在这里插入图片描述 虽然大部分人看元宇宙的文章知道了出处,但是真正是看过原著的人,我估计应该没几个人,对于书中想表达的内涵估计也没几个懂。

相比书记的枯燥无味,2018 年斯皮尔伯格导演的科幻电影《头号玩家》被认为是目前最符合《雪崩》中描述的“元宇宙”形态,在电影中,男主角带上 VR 头盔后,瞬间就能进入自己设计的另一个极其逼真的虚拟游戏世界——“绿洲”(Oasis)。 在这里插入图片描述 在电影《头号玩家》的场景中,人们可以随时随地切换身份,自由穿梭于物理世界和数字世界,在虚拟空间和时间节点所构成的“元宇宙”中学习、工作、交友、购物、旅游等。元宇宙,这个建立在区块链之上的虚拟世界,去中心化平台让玩家享有所有权和自治权。

维基百科对元宇宙的描述是:通过虚拟增强的物理现实,呈现收敛性和物理持久性特征的,基于未来互联网,具有链接感知和共享特征的 3D 虚拟空间。

不过,相比维基百科的定义,我认为朱嘉明教授对元宇宙的描述则更加具体:【元宇宙】是吸纳了信息革命(5G/6G)、互联网革命(web3.0)、人工智能革命,以及 VR、AR、MR,特别是游戏引擎在内的虚拟现实技术革命的成果,向人类展现出构建与传统物理世界平行的全息数字世界的可能性;引发了信息科学、量子科学,数学和生命科学的互动,改变科学范式;推动了传统的哲学、社会学甚至人文科学体系的突破;囊括了所有的数字技术,包括区块链技术成就;丰富了数字经济转型模式,融合 DeFi、IPFS、NFT 等数字金融成果。

二、元宇宙的属性

如果说元宇宙目前还存在于小说和电影中,那么在今年 3 月被称作元宇宙第一股的 Roblox 成功在纽交所上市,则似乎意味着这个虚拟世界正在走向现实。正如大家理解的那样,元宇宙的根基就是建立在游戏这个体系上的,所以不出意外,元宇宙最先落地也是跟游戏相关。

既然元宇宙在游戏里有体现,不妨让我们看看在游戏中元宇宙的八个关键特征,即Identity (身份)、Friends(朋友)、Immersive(沉浸感)、Low Friction(低延迟)、Variety(多样性)、Anywhere(随地)、Economy(经济)、Civility(文明)。

在这里插入图片描述

属性解释如下:

  • 身份:拥有一个虚拟身份,无论与现实身份有没有相关性。
  • 朋友:在元宇宙当中拥有朋友,可以社交,无论在现实中是否认识。
  • 沉浸感:能够沉浸在元宇宙的体验当中,忽略其他的一切。
  • 低延迟:元宇宙中的一切都是同步发生的,没有异步性或延迟性。
  • 多元化:元宇宙提供多种丰富内容,包括玩法、道具、美术素材等。
  • 随地:可以使用任何设备登录元宇宙,随时随地沉浸其中。
  • 经济系统:与任何复杂的大型游戏一样,元宇宙应该有自己的经济系统。
  • 文明:元宇宙应该是一种虚拟的文明。

三、元宇宙的价值链

元宇宙概念为何能在今年爆发,除了资本的追逐外,还因为元宇宙有一套属于自己的价值链,并吸引互联网公司们进入这个赛道,以自己的方式和理解去塑造、定义元宇宙。

虽然,对元宇宙概念目前还很模糊,但不妨碍元宇宙成为一个好的故事。Roblox3 月份上市后,其市值达到 400 亿美元,相比 1 年前 40 亿美元的估值暴涨了 10 倍。App Annie 发布的全球热门游戏收入排名显示,7 月 Roblox 继续蝉联冠军宝座。在国内,号称要打造全年龄段元宇宙世界的 MeteApp 公司,在 Roblox 上市后拿到了 SIG 海纳亚洲资本领投的 1 亿美元 C 轮融资。字节跳动对游戏引擎研发商、“中国版 Roblox”代码乾坤进行了近 1 亿人民币战略投资。

下面我们来看看元宇宙价值链,元宇宙的价值链主要是寻求能够实现的科技体验。因此,从这一价值链出发,元宇宙的价值链包括七个层面:体验(Experience);发现(Discovery);创作者经济(Creator economy);空间计算(Spatial Computing);去中心化(Decentralizition);人机交互(Human Interface);基础设施(Infrastructure)。 在这里插入图片描述

  • 内容和体验:在互联网中,数字化内容占据主要位置。在元宇宙中,数字化行为的比例会大幅度提高,从而使得用户可以尝试各种实时的体验。目前,这一层公司中的业务更多的停留在游戏层面,这是因为当前的硬件还处于比较早期阶段,如VR设备还并没有大规模普及。当硬件设备逐渐成熟和普及后,大量的公司会将现在的各种线下行为数字化,提供丰富的体验,这将包括但不局限于:旅游、教育、体育竞技、演唱会等等。
  • 发现与链接:在元宇宙中,内容和体验的数量是前所未有的,并且会以指数级的方式增长。这就意味着,对于每个人而言,都存在一个问题:如何发现那些有价值的,感兴趣的内容和体验。有能力去解决这个问题的公司,往往会成为内容和体验的包装/分发平台。
  • 硬件:相比于互联网和移动互联网,元宇宙很重要的一个特征是沉浸式体验,这意味着我们需要新一代信息交互设备。而目前最能达到这一条件的设备就是AR/VR智能眼镜,以及后面的计算平台。值得一提的是Facebook旗下的Oculus Quest2,或许能够成为元宇宙发展史上的一个重要产品。

在这里插入图片描述

  • 操作系统:除了AR/VR本身的硬件之外,依然需要一套完整的XR操作系统。目前,大部分VR硬件设备的系统是基于安卓系统,但这很可能并不是最终的答案。XR操作系统将会和手机操作系统有很大的区别,这将体现在交互方式、应用模式、内容分发、硬件形态和体验感等多个方面。
  • 基础设施:单一技术革命,并不能带来时代变革。同样,下一代互联网也需要充足的基础设施,比如我们耳熟能详的VR/AR、5G、AI 、去中心化等技术。

综上,元宇宙的核心价值在于,它将成为一个拥有极致沉浸体验、丰富内容生态、超时空的社交体系、虚实交互的经济系统,能映射现实人类社会文明的超大型数字社区。

四、元宇宙对行业的影响

从元宇宙的价值以及目前的技术情况来看,元宇宙对目前行业的影响体现在泛娱乐行业,特别是游戏行业有望成为元宇宙概念下最早落地的场景。

目前,市场上已经出现一系列基于游戏内核的沉浸式场景体验。比如,美国著名歌手 Travis Scott 在游戏《堡垒之夜》中举办虚拟演唱会,全球 1230 万游戏玩家成为虚拟演唱会观众;加州大学伯克利分校在《Minecraft》重现校园,毕业生以虚拟形象线上场景参加毕业典礼;顶级 AI 学术会议 ACAI 在任天堂《动物森友会》上举行 20 年研讨会,演讲者在游戏中播放 PPT 发表讲话等都是落地的场景。

而在线游戏创作社区 Roblox 因为现象级的内容创作生态带来的游戏自由度和用户活跃度,成为现阶段公认的元宇宙雏形。随着市场对元宇宙认识的加深,游戏之于元宇宙更大的意义在于提供展现方式,是元宇宙搭建虚拟世界的底层逻辑。与此同时,元宇宙概念的火热也吸引了无数游戏厂商的入局。

而另一个容易落地的场景便是社交领域。Facebook(现已改为Meta) CEO 扎克伯格日前表示,Facebook 已经组建了专门研发元宇宙的团队,并表示未来五年要从社交公司变成元宇宙公司。事实上 ,虚拟社交也在改变人们的社交方式,成为未来社交发展的新方向。

而在消费领域,随着元宇宙的到来,用户的消费体验或将迎来新的一波交互体验的升级。目前,新氧已经实现为用户提供 AR 检测脸型的服务,通过手机扫描脸部推算出适合每位用户的妆容发型护肤品等。得物 App 的 AR 虚拟试鞋功能允许用户只需要挑选自己喜欢的鞋型和颜色并 AR 试穿,看到鞋子上脚的效果。在 AR、VR、可穿戴设备、触觉传感等技术的带动下,更加沉浸式的消费或将成为常态。它不局限于购买衣服、鞋子等基本消费,AR 房屋装修、远程看房、甚至模拟旅游景点都将成为可能。

可以看到,元宇宙离不开的便是 AR、VR可穿戴虚拟设备,关于 AR、VR可穿戴虚拟设备的现状,大家可以参考聊聊这个本不存在的 “元宇宙”




收起阅读 »

(转载)初探元宇宙—元宇宙的八大要素

初探元宇宙—元宇宙的八大要素元宇宙的出现也是离不开人工智能技术成熟、以后在元宇宙我们就能以虚拟身份交到 AI 的朋友。在我们开始聊如何将人工智能技术应用到元宇宙中前,我们先了解了解什么是元宇宙。宇宙是我们未来生活的世界,什么是元宇宙。元宇宙是随着信息化的发展,...
继续阅读 »

初探元宇宙—元宇宙的八大要素

元宇宙的出现也是离不开人工智能技术成熟、以后在元宇宙我们就能以虚拟身份交到 AI 的朋友。在我们开始聊如何将人工智能技术应用到元宇宙中前,我们先了解了解什么是元宇宙。

宇宙是我们未来生活的世界,什么是元宇宙。元宇宙是随着信息化的发展,形成一个虚拟世界,这里虚拟世界和我们通过游戏或者电影所了解到虚拟世界有所不同。因为这个虚拟世界内部也会进一步发展,从而反过来影响我们的世界,最终形成一个虚拟和现实交叉共生的新的世界的形态。听起来还是感觉像玩游戏。

roblox_003.jpeg

那么到底什么样虚拟世界可以称为元宇宙,在元宇宙探索的前辈 Roblox 公司给出关于元宇宙的定义。一个元宇宙产品应该具备 8 要素,分别是身份、朋友、沉浸感、低延迟、多元化、随时随地、经济系统和文明。

身份(identity)

虚拟世界中每一个人都拥有自己虚拟身份,与现实身份无关。

朋友(friends)

可以跨空间进行社交,朋友可以真人也可可能是 AI 朋友

沉浸感(immersiveness)

可以沉浸在元宇宙的体验当中,VR/AR 设备提供沉浸体验

低延迟(Low Friction)

元宇宙中的一切都是同步发生的,没有异步性或延迟性,体验完美。通过云平台降低各地服务器之间的延迟。

多元化(variety)

提供丰富、差异化的内容,虚拟世界提供超越现实世界的自由和多元化。

随时随地(anywhere)

随时随地可以登录元宇宙,不受时空的限制。

经济系统(economy)

与现实产业经济系统一样,元宇宙也有自己的经济系统。虽然现在游戏中经济体系过于简单,难于称之为经济系统,不过区块链技术为提供体系的基础。虚拟世界可以使用虚拟货币进行交易,虚拟货币也可以现实货币进行兑换。

文明(civility)

独特的虚拟文明,数字文明,在目前中游戏中还没有形成文明,最多也就是文化,虚拟世界会形成文明社会。

原文链接:https://juejin.cn/post/7025195991703765006

收起阅读 »

iOS 面试题 八股文 1.6

一、面试题 1、说说你认识的Swift是什么? Swift是苹果于2014年WWDC(苹果开发者大会)发布的新开发语言,可与Objective-C共同运行于MAC OS和iOS平台,用于搭建基于苹果平台的应用程序。 2、举例说明Swift里面有哪些...
继续阅读 »

一、面试题


1、说说你认识的Swift是什么?

Swift是苹果于2014年WWDC(苹果开发者大会)发布的新开发语言,可与Objective-C共同运行于MAC OS和iOS平台,用于搭建基于苹果平台的应用程序。


2、举例说明Swift里面有哪些是 Objective-C中没有的?

Swift引入了在Objective-C中没有的一些高级数据类型,例如tuples(元组),可以使你创建和传递一组数值。
wift还引入了可选项类型(Optionals),用于处理变量值不存在的情况。可选项的意思有两种:一是变量是存在的,
例如等于X,二是变量值根本不存在。Optionals类似于Objective-C中指向nil的指针,但是适用于所有的数据类型,而非仅仅局限于类,Optionals 相比于Objective-C中nil指针更加安全和简明,并且也是Swift诸多最强大功能的核心。


3、NSArray与NSSet的区别?

NSArray内存中存储地址连续,而NSSet不连续
NSSet效率高,内部使用hash查找;NSArray查找需要遍历
NSSet通过anyObject访问元素,NSArray通过下标访问


4、Swift比Objective-C有什么优势?

Swift全面优于Objective-C语言,性能是Objective-C的1.3倍,上手更加容易。


5、NSHashTable与NSMapTable?

NSHashTable是NSSet的通用版本,对元素弱引用,可变类型;可以在访问成员时copy
NSMapTable是NSDictionary的通用版本,对元素弱引用,可变类型;
可以在访问成员时copy
(注:NSHashTable与NSSet的区别:NSHashTable可以通过option设置元素弱引用/copyin,只有可变类 型。但是添加对象的时候NSHashTable耗费时间是NSSet的两倍。
NSMapTable与NSDictionary的区别:同上)


6、Swift 是一门安全语言吗?

Swift是一门类型安全的语言,Optionals就是代表。Swift能帮助你在类型安全的环境下工作,如果你的代码中需要使用String类型,Swift的安全机制能阻止你错误的将Int值传递过来,这使你在开发阶段就能及时发现并修正问题。


7、属性关键字assign、retain、weak、copy

assign:用于基本数据类型和结构体。如果修饰对象的话,当销毁时,属性值不会自动置nil,可能造成野指针。
weak:对象引用计数为0时,属性值也会自动置nil
retain:强引用类型,ARC下相当于strong,但block不能用retain修饰,因为等同于assign不安全。
strong:强引用类型,修饰block时相当于copy。


8、weak属性如何自动置nil的?

Runtime会对weak属性进行内存布局,构建hash表:以weak属性对象内存地址为key,weak属性值(weak自身地址)为value。当对象引用计数为0 dealloc时,会将weak属性值自动置nil。


9、Swift 是一门安全语言吗?

Swift是一门类型安全的语言,Optionals就是代表。Swift能帮助你在类型安全的环境下工作,如果你的代码中需要使用String类型,Swift的安全机制能阻止你错误的将Int值传递过来,这使你在开发阶段就能及时发现并修正问题。


10、内存泄露问题?

主要集中在循环引用问题中,如block、NSTime、perform selector引用计数问题。


11、Block的循环引用、内部修改外部变量、三种block

block强引用self,self强引用block
内部修改外部变量:block不允许修改外部变量的值,这里的外部变量指的是栈中指针的内存地址。
__block的作用是只要观察到变量被block使用,就将外部变量在栈中的内存地址放到堆中。
三种block:
NSGlobalBlack(全局)、
NSStackBlock(栈block)、
NSMallocBlock(堆block)


12、KVO底层实现原理?手动触发KVO?swift如何实现KVO?

KVO原理:当观察一个对象时,runtime会动态创建继承自该对象的类,并重写被观察对象的setter方法,重写的setter方法会负责在调用原setter方法前后通知所有观察对象值得更改,最后会把该对象的isa指针指向这个创建的子类,对象就变成子类的实例。
如何手动触发KVO:在setter方法里,手动实现NSObject两个方法:willChangeValueForKey、didChangeValueForKey
swift的kvo:继承自NSObject的类,或者直接willset/didset实现。


13、categroy为什么不能添加属性?怎么实现添加?与Extension的区别?category覆盖原类方法?多个category调用顺序

Runtime初始化时categroy的内存布局已经确定,没有ivar,所以默认不能添加属性。
使用runtime的关联对象,并重写setter和getter方法。
Extenstion编译期创建,可以添加成员变量ivar,一般用作隐藏类的信息。必须要有类的源码才可以添加,如NSString就不能创建Extension。
category方法会在runtime初始化的时候copy到原来前面,调用分类方法的时候直接返回,不再调用原类。如何保持原类也调用(https://www.jianshu.com/p/40e28c9f9da5)。
多个category的调用顺序按照:Build Phases ->Complie Source 中的编译顺序。


14、load方法和initialize方法的异同。——主要说一下执行时间,各自用途,没实现子类的方法会不会调用父类的?

load initialize 调用时机 app启动后,runtime初始化的时候 第一个方法调用前调用 调用顺序 父类->本类->分类 父类->本类(如果有分类直接调用分类,本类不会调用) 没实现子类的方法会不会调用父类的 否 是 是否沿用父类实现 否 是

见图 1

15、对 runtime 的理解。——主要是方法调用时如何查找缓存,如何找到方法,找不到方法时怎么转发,对象的内存布局

OC中向对象发送消息时,runtime会根据对象的isa指针找到对象所属的类,然后在该类的方法列表和父类的方法列表中寻找方法执行。如果在最顶层父类中没找到方法执行,就会进行消息转发:Method resoution(实现方法)、fast forwarding(转发给其他对象)、normal forwarding(完整消息转发。可以转发给多个对象)

16、runtime 中,SEL和IMP的区别?

每个类对象都有一个方法列表,方法列表存储方法名、方法实现、参数类型,SEL是方法名(编号),IMP指向方法实现的首地址


17、autoreleasepool的原理和使用场景?

若干个autoreleasepoolpage组成的双向链表的栈结构,objc_autoreleasepoolpush、objc_autoreleasepoolpop、objc_autorelease
使用场景:多次创建临时变量导致内存上涨时,需要延迟释放
autoreleasepoolpage的内存结构:4k存储大小

见图 2


18、Autorelase对象什么时候释放?

在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。


19、Runloop与线程的关系?Runloop的mode? Runloop的作用?内部机制?

每一个线程都有一个runloop,主线程的runloop默认启动。
mode:主要用来指定事件在运行时循环的优先级
作用:保持程序的持续运行、随时处理各种事件、节省cpu资源(没事件休息释放资源)、渲染屏幕UI


20、iOS中使用的锁、死锁的发生与避免

@synchronized、信号量、NSLock等
死锁:多个线程同时访问同一资源,造成循环等待。GCD使用异步线程、并行队列


21、NSOperation和GCD的区别

GCD底层使用C语言编写高效、NSOperation是对GCD的面向对象的封装。对于特殊需求,如取消任务、设置任务优先级、任务状态监听,NSOperation使用起来更加方便。
NSOperation可以设置依赖关系,而GCD只能通过dispatch_barrier_async实现
NSOperation可以通过KVO观察当前operation执行状态(执行/取消)
NSOperation可以设置自身优先级(queuePriority)。GCD只能设置队列优先级 (DISPATCH_QUEUE_PRIORITY_DEFAULT),无法在执行的block中设置优先级
NSOperation可以自定义operation如NSInvationOperation/NSBlockOperation,而GCD执行任务可以自定义封装但没有那么高的代码复用度
GCD高效,NSOperation开销相对高


22、App启动优化策略?main函数执行前后怎么优化

启动时间 = pre-main耗时+main耗时
pre-main阶段优化:
删除无用代码
抽象重复代码
+load方法做的事情延迟到initialize中,或者+load的事情不宜花费太多时间
减少不必要的framework,或者优化已有framework

Main阶段优化
didFinishLauchingwithOptions里代码延后执行
首次启动渲染的页面优化


23、Swift 支持面向过程编程吗?

它采用了 Objective-C 的命名参数以及动态对象模型,可以无缝对接到现有的 Cocoa 框架,并且可以兼容 Objective-C 代码,支持面向过程编程和面向对象编程



24、Swift 是一门安全语言吗?

Swift是一门类型安全的语言,Optionals就是代表。Swift能帮助你在类型安全的环境下工作,如果你的代码中需要使用String类型,Swift的安全机制能阻止你错误的将Int值传递过来,这使你在开发阶段就能及时发现并修正问题。


25、Swift中如何定义变量和常量?

使用let来声明常量,使用var来声明变量


26、oc与js交互

拦截url
JavaScriptCore(只适用于UIWebView)
WKScriptMessageHandler(只适用于WKWebView)
WebViewJavaScriptBridge(第三方框架)


27、Swift的内存管理是怎样的?

Swift 使用自动引用计数(Automatic Reference Counting, ARC)来简化内存管理


28、struct、Class的区别

class可以继承,struct不可以
class是引用类型,struct是值类型
struct在function里修改property时需要mutating关键字修饰


29、访问控制关键字(public、open、private、filePrivate、internal)

public与open:public在module内部中,class和func都可以被访问/重载/继承,外部只能访问;而open都可以
private与filePrivate:private修饰class/func,表示只能在当前class源文件/func内部使用,外部不可以被继承和访问;而filePrivate表示只能在当前swift源文件内访问
internal:在整个模块或者app内都可以访问,默认访问级别,可写可不写


30、OC与Swift混编

OC调用swift:import "工程名-swift.h” @objc
swift调用oc:桥接文件
31、用Swift定义一个数组和字典?
let emptyArray = String[]()
let emptyDictionary = Dictionary<String, Float>()
32、try、try?与try!
try:手动捕捉异常
try?:系统帮我们处理,出现异常返回nil;没有异常返回对应的对象
try!:直接告诉系统,该方法没有异常。如果出现异常程序会crash
33、guard与defer
guard用于提前处理错误数据,else退出程序,提高代码可读性
defer延迟执行,回收资源。多个defer反序执行,嵌套defer先执行外层,后执行内层
34、架构&设计模式
MVC设计模式介绍
MVVM介绍、MVC与MVVM的区别?
ReactiveCocoa的热信号与冷信号
缓存架构设计LRU方案
SDWebImage源码,如何实现解码
AFNetWorking源码分析
组件化的实施,中间件的设计
哈希表的实现原理?如何解决冲突
35、数据结构&算法
快速排序、归并排序
二维数组查找(每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数)
二叉树的遍历:判断二叉树的层数
单链表判断环
36、内存泄露问题?
主要集中在循环引用问题中,如block、NSTime、perform selector引用计数问题。
37、crash防护?
unrecognized selector crash
KVO crash
NSNotification crash
NSTimer crash
Container crash(数组越界,插nil等)
NSString crash (字符串操作的crash)
Bad Access crash (野指针)
UI not on Main Thread Crash (非主线程刷UI (机制待改善))





收起阅读 »

iOS 面试题 八股文 1.6

如何自定义下标获取 实现 subscript 即可, 如extension AnyList { subscript(index: Int) -> T{ return self.list[index] } subsc...
继续阅读 »

如何自定义下标获取


实现 subscript 即可, 如

extension AnyList {
subscript(index: Int) -> T{
return self.list[index]
}
subscript(indexString: String) -> T?{
guard let index = Int(indexString) else {
return nil
}
return self.list[index]
}
}


索引除了数字之外, 其他类型也是可以的


?? 的作用


可选值的默认值, 当可选值为nil 的时候, 会返回后面的值. 如

let someValue = optional1 ?? 0


lazy 的作用


懒加载, 当属性要使用的时候, 才去完成初始化

class LazyClass {
lazy var someLazyValue: Int = {
print("lazy init value")
return 1
}()
var someNormalValue: Int = {
print("normal init value")
return 2
}()
}
let lazyInstance = LazyClass()
print(lazyInstance.someNormalValue)
print(lazyInstance.someLazyValue)
// 打印输出
// normal init value
// 2
// lazy init value
// 1


一个类型表示选项,可以同时表示有几个选项选中(类似 UIViewAnimationOptions ),用什么类型表示


需要实现自 OptionSet, 一般使用 struct 实现. 由于 OptionSet 要求有一个不可失败的init(rawValue:) 构造器, 而 枚举无法做到这一点(枚举的原始值构造器是可失败的, 而且有些组合值, 是没办法用一个枚举值表示的)

struct SomeOption: OptionSet {
let rawValue: Int
static let option1 = SomeOption(rawValue: 1 << 0)
static let option2 = SomeOption(rawValue:1 << 1)
static let option3 = SomeOption(rawValue:1 << 2)
}
let options: SomeOption = [.option1, .option2]


inout 的作用


输入输出参数, 如:

func swap( a: inout Int, b: inout Int) {
let temp = a
a = b
b = temp
}
var a = 1
var b = 2
print(a, b)// 1 2
swap(a: &a, b: &b)
print(a, b)// 2 1


Error 如果要兼容 NSError 需要做什么操作


其实直接转换就可以, 例如 SomeError.someError as NSError 但是这样没有错误码, 描述等等, 如果想和 NSError 一样有这些东西, 只需要实现 LocalizedErrorCustomNSError 协议, 有些方法有默认实现, 可以略过, 如:

enum SomeError: Error, LocalizedError, CustomNSError {
case error1, error2
public var errorDescription: String? {
switch self {
case .error1:
return "error description error1"
case .error2:
return "error description error2"
}
}
var errorCode: Int {
switch self {
case .error1:
return 1
case .error2:
return 2
}
}
public static var errorDomain: String {
return "error domain SomeError"
}
public var errorUserInfo: [String : Any] {
switch self {
case .error1:
return ["info": "error1"]
case .error2:
return ["info": "error2"]
}
}
}
print(SomeError.error1 as NSError)
// Error Domain=error domain SomeError Code=1 "error description error1" UserInfo={info=error1}


下面的代码都用了哪些语法糖


[1, 2, 3].map{ $0 * 2 }

[1, 2, 3] 使用了, Array 实现的ExpressibleByArrayLiteral 协议, 用于接收数组的字面值

map{xxx} 使用了闭包作为作为最后一个参数时, 可以直接写在调用后面, 而且, 如果是唯一参数的话, 圆括号也可以省略

闭包没有声明函数参数, 返回值类型, 数量, 依靠的是闭包类型的自动推断

闭包中语句只有一句时, 自动将这一句的结果作为返回值

0 在没有声明参数列表的时候, 第一个参数名称为0, 后续参数以此类推


什么是高阶函数


一个函数如果可以以某一个函数作为参数, 或者是返回值, 那么这个函数就称之为高阶函数, 如 map, reduce, filter


如何解决引用循环



  1. 转换为值类型, 只有类会存在引用循环, 所以如果能不用类, 是可以解引用循环的,

  2. delegate 使用 weak 属性.

  3. 闭包中, 对有可能发生循环引用的对象, 使用 weak 或者 unowned, 修饰


下面的代码会不会崩溃,说出原因

var mutableArray = [1,2,3]
for _ in mutableArray {
mutableArray.removeLast()
}


不会, 原理不清楚, 就算是把 removeLast(), 换成 removeAll() ,这个循环也会执行三次, 估计是在一开始, for

in 就对 mutableArray 进行了一次值捕获, 而 Array 是一个值类型 , removeLast() 并不能修改捕获的值.


给集合中元素是字符串的类型增加一个扩展方法,应该怎么声明


使用 where 子句, 限制 Element 为 String

extension Array where Element == String {
var isStringElement:Bool {
return true
}
}
["1", "2"].isStringElement
//[1, 2].isStringElement// error


定义静态方法时关键字 static 和 class 有什么区别


static 定义的方法不可以被子类继承, class 则可以

class AnotherClass {
static func staticMethod(){}
class func classMethod(){}
}
class ChildOfAnotherClass: AnotherClass {
override class func classMethod(){}
//override static func staticMethod(){}// error
}


一个 Sequence 的索引是不是一定从 0 开始?


不一定, 两个 for in 并不能保证都是从 0 开始, 且输出结果一致, 官方文档如下



Repeated Access


The Sequence protocol makes no requirement on conforming types regarding

whether they will be destructively consumed by iteration. As a

consequence, don't assume that multiple for-in loops on a sequence

will either resume iteration or restart from the beginning:

for element in sequence {
if ... some condition { break }
}

for element in sequence {
// No defined behavior
}



有些同学还是不太理解, 我写了一个demo 当作参考

class Countdown: Sequence, IteratorProtocol {
var count: Int
init(count: Int) {
self.count = count
}
func next() -> Int? {
if count == 0 {
return nil
} else {
defer { count -= 1 }
return count
}
}
}

var countDown = Countdown(count: 5)
print("begin for in 1")
for c in countDown {
print(c)
}
print("end for in 1")
print("begin for in 2")
for c in countDown {
print(c)
}
print("end for in 2")


最后输出的结果是

begin for in 1
5
4
3
2
1
end for in 1
begin for in 2
end for in 2


很明显, 第二次没有输出任何结果, 原因就是在第二次for in 的时候, 并没有将count 重置.


数组都实现了哪些协议


MutableCollection, 实现了可修改的数组, 如 a[1] = 2

ExpressibleByArrayLiteral, 实现了数组可以从[1, 2, 3] 这种字面值初始化的能力

...


如何自定义模式匹配


这部分不太懂, 贴个链接吧

http://swifter.tips/pattern-match/


autoclosure 的作用


自动闭包, 会自动将某一个表达式封装为闭包. 如

func autoClosureFunction(_ closure: @autoclosure () -> Int) {
closure()
}
autoClosureFunction(1)


详细可参考http://swifter.tips/autoclosure/


编译选项 whole module optmization 优化了什么


编译器可以跨文件优化编译代码, 不局限于一个文件.

http://www.jianshu.com/p/8dbf2bb05a1c


下面代码中 mutating 的作用是什么

struct Person {
var name: String {
mutating get {
return store
}
}
}


让不可变对象无法访问 name 属性


如何让自定义对象支持字面量初始化


有几个协议, 分别是

ExpressibleByArrayLiteral 可以由数组形式初始化

ExpressibleByDictionaryLiteral 可以由字典形式初始化

ExpressibleByNilLiteral 可以由nil 值初始化

ExpressibleByIntegerLiteral 可以由整数值初始化

ExpressibleByFloatLiteral 可以由浮点数初始化

ExpressibleByBooleanLiteral 可以由布尔值初始化

ExpressibleByUnicodeScalarLiteral

ExpressibleByExtendedGraphemeClusterLiteral

ExpressibleByStringLiteral

这三种都是由字符串初始化, 上面两种包含有 Unicode 字符和特殊字符


dynamic framework 和 static framework 的区别是什么



静态库和动态库, 静态库是每一个程序单独打包一份, 而动态库则是多个程序之间共享



链接: https://www.jianshu.com/p/7c7f4b4e4efe

链接:https://www.jianshu.com/p/cc4a737ddc1d

链接:https://www.jianshu.com/p/23d99f434281

收起阅读 »

iOS 面试题 八股文 1.5

defer 使用场景 defer 语句块中的代码, 会在当前作用域结束前调用, 常用场景如异常退出后, 关闭数据库连接func someQuery() -> ([Result], [Result]){ let db = DBOpen("xxx")...
继续阅读 »

defer 使用场景


defer 语句块中的代码, 会在当前作用域结束前调用, 常用场景如异常退出后, 关闭数据库连接

func someQuery() -> ([Result], [Result]){
let db = DBOpen("xxx")
defer {
db.close()
}
guard results1 = db.query("query1") else {
return nil
}
guard results2 = db.query("query2") else {
return nil
}
return (results1, results2)
}


需要注意的是, 如果有多个 defer, 那么后加入的先执行

func someDeferFunction() {
defer {
print("\(#function)-end-1-1")
print("\(#function)-end-1-2")
}
defer {
print("\(#function)-end-2-1")
print("\(#function)-end-2-2")
}
if true {
defer {
print("if defer")
}
print("if end")
}
print("function end")
}
someDeferFunction()
// 输出
// if end
// if defer
// function end
// someDeferFunction()-end-2-1
// someDeferFunction()-end-2-2
// someDeferFunction()-end-1-1
// someDeferFunction()-end-1-2


String 与 NSString 的关系与区别


NSString 与 String 之间可以随意转换,

let someString = "123"
let someNSString = NSString(string: "n123")
let strintToNSString = someString as NSString
let nsstringToString = someNSString as String


String 是结构体, 值类型, NSString 是类, 引用类型.

通常, 没必要使用 NSString 类, 除非你要使用一些特有方法, 例如使用 pathExtension 属性


怎么获取一个 String 的长度


不考虑编码, 只是想知道字符的数量, 用characters.count

"hello".characters.count // 5
"你好".characters.count // 2
"こんにちは".characters.count // 5


如果想知道在某个编码下占多少字节, 可以用

"hello".lengthOfBytes(using: .ascii) // 5
"hello".lengthOfBytes(using: .unicode) // 10
"你好".lengthOfBytes(using: .unicode) // 4
"你好".lengthOfBytes(using: .utf8) // 6
"こんにちは".lengthOfBytes(using: .unicode) // 10
"こんにちは".lengthOfBytes(using: .utf8) // 15


如何截取 String 的某段字符串


swift 中, 有三个取子串函数,

substring:to , substring:from, substring:with.

let simpleString = "Hello, world"
simpleString.substring(to: simpleString.index(simpleString.startIndex, offsetBy: 5))
// hello
simpleString.substring(from: simpleString.index(simpleString.endIndex, offsetBy: -5))
// world
simpleString.substring(with: simpleString.index(simpleString.startIndex, offsetBy: 5) ..< simpleString.index(simpleString.endIndex, offsetBy: -5))
// ,


使用起来略微麻烦, 具体用法可以参考我的另一篇文章http://www.jianshu.com/p/b3231f9406e9


throws 和 rethrows 的用法与作用


throws 用在函数上, 表示这个函数会抛出错误.

有两种情况会抛出错误, 一种是直接使用 throw 抛出, 另一种是调用其他抛出异常的函数时, 直接使用 try xx 没有处理异常.

enum DivideError: Error {
case EqualZeroError;
}
func divide(_ a: Double, _ b: Double) throws -> Double {
guard b != Double(0) else {
throw DivideError.EqualZeroError
}
return a / b
}
func split(pieces: Int) throws -> Double {
return try divide(1, Double(pieces))
}


rethrows 与 throws 类似, 不过只适用于参数中有函数, 且函数会抛出异常的情况, rethrows 可以用 throws 替换, 反过来不行

func processNumber(a: Double, b: Double, function: (Double, Double) throws -> Double) rethrows -> Double {
return try function(a, b)
}


try? 和 try!是什么意思


这两个都用于处理可抛出异常的函数, 使用这两个关键字可以不用写 do catch.

区别在于, try? 在用于处理可抛出异常函数时, 如果函数抛出异常, 则返回 nil, 否则返回函数返回值的可选值, 如:

print(try? divide(2, 1))
// Optional(2.0)
print(try? divide(2, 0))
// nil


而 try! 则在函数抛出异常的时候崩溃, 否则则返会函数返回值, 相当于(try? xxx)!, 如:

print(try! divide(2, 1))
// 2.0
print(try! divide(2, 0))
// 崩溃


associatedtype 的作用


简单来说就是 protocol 使用的泛型

例如定义一个列表协议

protocol ListProtcol {
associatedtype Element
func push(_ element:Element)
func pop(_ element:Element) -> Element?
}


实现协议的时候, 可以使用 typealias 指定为特定的类型, 也可以自动推断, 如

class IntList: ListProtcol {
typealias Element = Int // 使用 typealias 指定为 Int
var list = [Element]()
func push(_ element: Element) {
self.list.append(element)
}
func pop(_ element: Element) -> Element? {
return self.list.popLast()
}
}
class DoubleList: ListProtcol {
var list = [Double]()
func push(_ element: Double) {// 自动推断
self.list.append(element)
}
func pop(_ element: Double) -> Double? {
return self.list.popLast()
}
}


使用泛型也可以

class AnyList<T>: ListProtcol {
var list = [T]()
func push(_ element: T) {
self.list.append(element)
}
func pop(_ element: T) -> T? {
return self.list.popLast()
}
}


可以使用 where 字句限定 Element 类型, 如:

extension ListProtcol where Element == Int {
func isInt() ->Bool {
return true
}
}


什么时候使用 final


final 用于限制继承和重写. 如果只是需要在某一个属性前加一个 final.

如果需要限制整个类无法被继承, 那么可以在类名之前加一个final


public 和 open 的区别


这两个都用于在模块中声明需要对外界暴露的函数, 区别在于, public 修饰的类, 在模块外无法继承, 而 open 则可以任意继承, 公开度来说, public < open


声明一个只有一个参数没有返回值闭包的别名


没有返回值也就是返回值为 Void

typealias SomeClosuerType = (String) -> (Void)
let someClosuer: SomeClosuerType = { (name: String) in
print("hello,", name)
}
someClosuer("world")
// hello, world


Self 的使用场景


Self 通常在协议中使用, 用来表示实现者或者实现者的子类类型.

例如, 定义一个复制的协议

protocol CopyProtocol {
func copy() -> Self
}


如果是结构体去实现, 要将Self 换为具体的类型

struct SomeStruct: CopyProtocol {
let value: Int
func copySelf() -> SomeStruct {
return SomeStruct(value: self.value)
}
}


如果是类去实现, 则有点复杂, 需要有一个 required 初始化方法, 具体可以看这里 http://swifter.tips/use-self/

class SomeCopyableClass: CopyProtocol {
func copySelf() -> Self {
return type(of: self).init()
}
required init(){}
}


dynamic 的作用


由于 swift 是一个静态语言, 所以没有 Objective-C 中的消息发送这些动态机制, dynamic 的作用就是让 swift 代码也能有 Objective-C 中的动态机制, 常用的地方就是 KVO 了, 如果要监控一个属性, 则必须要标记为 dynamic, 可以参考我的文章http://www.jianshu.com/p/ae26100b9edf


什么时候使用 @objc


@objc 用途是为了在 Objective-C 和 Swift 混编的时候, 能够正常调用 Swift 代码. 可以用于修饰类, 协议, 方法, 属性.

常用的地方是在定义 delegate 协议中, 会将协议中的部分方法声明为可选方法, 需要用到@objc

@objc protocol OptionalProtocol {
@objc optional func optionalFunc()
func normalFunc()
}
class OptionProtocolClass: OptionalProtocol {
func normalFunc() {
}
}
let someOptionalDelegate: OptionalProtocol = OptionProtocolClass()
someOptionalDelegate.optionalFunc?()


Optional(可选型) 是用什么实现的


Optional 是一个泛型枚举

大致定义如下:

enum Optional<Wrapped> {
case none
case some(Wrapped)
}


除了使用 let someValue: Int? = nil 之外, 还可以使用let optional1: Optional<Int> = nil 来定义


收起阅读 »

iOS 面试题 八股文 1.4

励志背下所有的八股文class 和 struct 的区别 class 为类, struct 为结构体, 类是引用类型, 结构体为值类型, 结构体不可以继承 不通过继承,代码复用(共享)的方式有哪些 扩展, 全局函数 Set 独有的方法有哪些?// 定义一个 s...
继续阅读 »

励志背下所有的八股文

class 和 struct 的区别


class 为类, struct 为结构体, 类是引用类型, 结构体为值类型, 结构体不可以继承


不通过继承,代码复用(共享)的方式有哪些


扩展, 全局函数


Set 独有的方法有哪些?

// 定义一个 set
let setA: Set<Int> = [1, 2, 3, 4, 4]// {1, 2, 3, 4}, 顺序可能不一致, 同一个元素只有一个值
let setB: Set<Int> = [1, 3, 5, 7, 9]// {1, 3, 5, 7, 9}
// 取并集 A | B
let setUnion = setA.union(setB)// {1, 2, 3, 4, 5, 7, 9}
// 取交集 A & B
let setIntersect = setA.intersection(setB)// {1, 3}
// 取差集 A - B
let setRevers = setA.subtracting(setB) // {2, 4}
// 取对称差集, A XOR B = A - B | B - A
let setXor = setA.symmetricDifference(setB) //{2, 4, 5, 7, 9}


实现一个 min 函数,返回两个元素较小的元素

func myMin<T: Comparable>(_ a: T, _ b: T) -> T {
return a < b ? a : b
}
myMin(1, 2)


map、filter、reduce 的作用


map 用于映射, 可以将一个列表转换为另一个列表

[1, 2, 3].map{"\($0)"}// 数字数组转换为字符串数组
["1", "2", "3"]


filter 用于过滤, 可以筛选出想要的元素

[1, 2, 3].filter{$0 % 2 == 0} // 筛选偶数
// [2]


reduce 合并

[1, 2, 3].reduce(""){$0 + "\($1)"}// 转换为字符串并拼接
// "123"


组合示例

(0 ..< 10).filter{$0 % 2 == 0}.map{"\($0)"}.reduce(""){$0 + $1}
// 02468


map 与 flatmap 的区别


flatmap 有两个实现函数实现,

public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]

这个方法, 中间的函数返回值为一个可选值, 而 flatmap 会丢掉那些返回值为 nil 的值

例如

["1", "@", "2", "3", "a"].flatMap{Int($0)}
// [1, 2, 3]
["1", "@", "2", "3", "a"].map{Int($0) ?? -1}
//[Optional(1), nil, Optional(2), Optional(3), nil]


另一个实现

public func flatMap<SegmentOfResult>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Iterator.Element] where SegmentOfResult : Sequence

中间的函数, 返回值为一个数组, 而这个 flapmap 返回的对象则是一个与自己元素类型相同的数组

func someFunc(_ array:[Int]) -> [Int] {
return array
}
[[1], [2, 3], [4, 5, 6]].map(someFunc)
// [[1], [2, 3], [4, 5, 6]]
[[1], [2, 3], [4, 5, 6]].flatMap(someFunc)
// [1, 2, 3, 4, 5, 6]


其实这个实现, 相当于是在使用 map 之后, 再将各个数组拼起来一样的

[[1], [2, 3], [4, 5, 6]].map(someFunc).reduce([Int]()) {$0 + $1}
// [1, 2, 3, 4, 5, 6]


什么是 copy on write时候


写时复制, 指的是 swift 中的值类型, 并不会在一开始赋值的时候就去复制, 只有在需要修改的时候, 才去复制.

这里有详细的说明

http://www.jianshu.com/p/7e8ba0659646


如何获取当前代码的函数名和行号


#file 用于获取当前文件文件名

#line 用于获取当前行号

#column 用于获取当前列编号

#function 用于获取当前函数名

以上这些都是特殊的字面量, 多用于调试输出日志

具体可以看这里 apple 文档

https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Expressions.html

这里有中文翻译

http://wiki.jikexueyuan.com/project/swift/chapter3/04_Expressions.html


如何声明一个只能被类 conform 的 protocol


声明协议的时候, 加一个 class 即可

protocol SomeClassProtocl: class {
func someFunction()
}


guard 使用场景


guard 和 if 类似, 不同的是, guard 总是有一个 else 语句, 如果表达式是假或者值绑定失败的时候, 会执行 else 语句, 且在 else 语句中一定要停止函数调用

例如

guard 1 + 1 == 2 else {
fatalError("something wrong")
}


常用使用场景为, 用户登录的时候, 验证用户是否有输入用户名密码等

guard let userName = self.userNameTextField.text,
let password = self.passwordTextField.text else {
return
}


收起阅读 »

n皇后问题

在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 典型的回溯法问题 思路: 尝试性的放置 ,从第一行开始,接着在下一行放置,(这里的好处就是不需要考虑行了,只需要考虑列和对角线) 一直纠...
继续阅读 »

在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。


典型的回溯法问题


思路:


尝试性的放置 ,从第一行开始,接着在下一行放置,(这里的好处就是不需要考虑行了,只需要考虑列和对角线)

一直纠结第一行的问题,代码中直接传参数为0,一直好奇如何控制第一行的列的变化,后来将0自己模拟走了一遍才明白。

注意判断的是否符合规则的公式:(列==列)(abs(列-列)==abs(行-行))

具体细节见注释(仔细阅读,一定能看懂,)

#include<iostream>
#include <math.h>
#define N 8
using namespace std;

int num=0;//用来记录总的放置个数
int cur[8];//此全局变量是用来记录第i行放在得第j列,其中下标为i,值为j
int check(int n){//传进来行
for(int i=0;i<n;i++){
if(cur[i]==cur[n]||abs(n-i)==abs(cur[n]-cur[i])){//判断当前放置的位置是否与之前的放置位置是否在同一列或同斜列
return 0;
}
}
return 1;
}
void putQueen(int n){
if(n==N){//如果找到了最后一行的下一行,那么就可以将次数+1了(就是之前把所有的行已经放完了,数组下标从0开始的勿忘)
num++;
}else{
for(int j=0;j<N;j++){//列的位置从0往最后放置
cur[n]=j;//记录下当前行的当前列
if(check(n)){//判断当前放置的行列是否合适
putQueen(n+1);//开始进行下一行的放置
}
}
}

}
int main(){
putQueen(0);
cout<<num;
return 0;
}

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

还在用Swagger?试试这款零注解侵入的API文档生成工具,跟Postman绝配!

前后端接口联调需要API文档,我们经常会使用工具来生成。之前经常使用Swagger来生成,最近发现一款好用的API文档生成工具smart-doc, 它有着很多Swagger不具备的特点,推荐给大家。 SpringBoot实战电商项目mall(50k+star...
继续阅读 »

前后端接口联调需要API文档,我们经常会使用工具来生成。之前经常使用Swagger来生成,最近发现一款好用的API文档生成工具smart-doc, 它有着很多Swagger不具备的特点,推荐给大家。



SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…


聊聊Swagger


在我们使用Swagger的时候,经常会需要用到它的注解,比如@Api@ApiOperation这些,Swagger通过它们来生成API文档。比如下面的代码:



Swagger对代码的入侵性比较强,有时候代码注释和注解中的内容有点重复了。有没有什么工具能实现零注解入侵,直接根据代码注释生成API文档呢?smart-doc恰好是这种工具!


smart-doc简介


smart-doc是一款API文档生成工具,无需多余操作,只要你规范地写好代码注释,就能生成API文档。同时能直接生成Postman调试文件,一键导入Postman即可调试,非常好用!


smart-doc具有如下优点:



生成API文档



接下来我们把smart-doc集成到SpringBoot项目中,体验一下它的API文档生成功能。




  • 首先我们需要在项目中添加smart-doc的Maven插件,可以发现smart-doc就是个插件,连依赖都不用添加,真正零入侵啊;


<plugin>
<groupId>com.github.shalousun</groupId>
<artifactId>smart-doc-maven-plugin</artifactId>
<version>2.2.8</version>
<configuration>
<!--指定smart-doc使用的配置文件路径-->
<configFile>./src/main/resources/smart-doc.json</configFile>
<!--指定项目名称-->
<projectName>mall-tiny-smart-doc</projectName>
</configuration>
</plugin>


  • 接下来在项目的resources目录下,添加配置文件smart-doc.json,属性说明直接参考注释即可;


{
"serverUrl": "http://localhost:8088", //指定后端服务访问地址
"outPath": "src/main/resources/static/doc", //指定文档的输出路径,生成到项目静态文件目录下,随项目启动可以查看
"isStrict": false, //是否开启严格模式
"allInOne": true, //是否将文档合并到一个文件中
"createDebugPage": false, //是否创建可以测试的html页面
"packageFilters": "com.macro.mall.tiny.controller.*", //controller包过滤
"style":"xt256", //基于highlight.js的代码高设置
"projectName": "mall-tiny-smart-doc", //配置自己的项目名称
"showAuthor":false, //是否显示接口作者名称
"allInOneDocFileName":"index.html" //自定义设置输出文档名称
}


  • 打开IDEA的Maven面板,双击smart-doc插件的smart-doc:html按钮,即可生成API文档;




  • 此时我们可以发现,在项目的static/doc目录下已经生成如下文件;




  • 运行项目,访问生成的API接口文档,发现文档非常详细,包括了请求参数和响应结果的各种说明,访问地址:http://localhost:8088/doc/index.html




  • 我们回过来看下实体类的代码,可以发现我们只是规范地添加了字段注释,生成文档的时候就自动有了;


public class PmsBrand implements Serializable {
/**
* ID
*/
private Long id;

/**
* 名称
* @required
*/
private String name;

/**
* 首字母
* @since 1.0
*/
private String firstLetter;

/**
* 排序
*/
private Integer sort;

/**
* 是否为品牌制造商(0,1)
*/
private Integer factoryStatus;

/**
* 显示状态(0,1)
* @ignore
*/
private Integer showStatus;

/**
* 产品数量
*/
private Integer productCount;

/**
* 产品评论数量
*/
private Integer productCommentCount;

/**
* 品牌logo
*/
private String logo;

/**
* 专区大图
*/
private String bigPic;

/**
* 品牌故事
*/
private String brandStory;
//省略getter、setter方法
}


  • 再来看下Controller中代码,我们同样规范地在方法上添加了注释,生成API文档的时候也自动有了;


/**
* 商品品牌管理
*/
@Controller
@RequestMapping("/brand")
public class PmsBrandController {
@Autowired
private PmsBrandService brandService;

/**
* 分页查询品牌列表
*
* @param pageNum 页码
* @param pageSize 分页大小
*/
@RequestMapping(value = "/list", method = RequestMethod.GET)
@ResponseBody
@PreAuthorize("hasRole('ADMIN')")
public CommonResult<CommonPage<PmsBrand>> listBrand(@RequestParam(value = "pageNum", defaultValue = "1")
Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "3")
Integer pageSize) {
List<PmsBrand> brandList = brandService.listBrand(pageNum, pageSize);
return CommonResult.success(CommonPage.restPage(brandList));
}
}


  • 当然smart-doc还提供了自定义注释tag,用于增强文档功能;

    • @ignore:生成文档时是否要过滤该属性;

    • @required:用于修饰接口请求参数是否必须;

    • @since:用于修饰接口中属性添加的版本号。



  • 为了写出优雅的API文档接口,我们经常会对返回结果进行统一封装,smart-doc也支持这样的设置,在smart-doc.json中添加如下配置即可;


{
"responseBodyAdvice":{ //统一返回结果设置
"className":"com.macro.mall.tiny.common.api.CommonResult" //对应封装类
}
}


  • 我们也经常会用枚举类型来封装状态码,在smart-doc.json中添加如下配置即可;


{
"errorCodeDictionaries": [{ //错误码列表设置
"title": "title",
"enumClassName": "com.macro.mall.tiny.common.api.ResultCode", //错误码枚举类
"codeField": "code", //错误码对应字段
"descField": "message" //错误码描述对应字段
}]
}


  • 配置成功后,即可在API文档中生成错误码列表




  • 有时候我们也会想给某些接口添加自定义请求头,比如给一些需要登录的接口添加Authorization头,在smart-doc.json中添加如下配置即可;


{
"requestHeaders": [{ //请求头设置
"name": "Authorization", //请求头名称
"type": "string", //请求头类型
"desc": "token请求头的值", //请求头描述
"value":"token请求头的值", //请求头的值
"required": false, //是否必须
"since": "-", //添加版本
"pathPatterns": "/brand/**", //哪些路径需要添加请求头
"excludePathPatterns":"/admin/login" //哪些路径不需要添加请求头
}]
}


  • 配置成功后,在接口文档中即可查看到自定义请求头信息了。



使用Postman测试接口



我们使用Swagger生成文档时候,是可以直接在上面测试接口的,而smart-doc的接口测试能力真的很弱,这也许是它拥抱Postman的原因吧,毕竟Postman是非常好用的接口测试工具,下面我们来结合Postman使用下!




  • smart-doc内置了Postman的json生成插件,可以一键生成并导入到Postman中去,双击smart-doc:postman按钮即可生成;




  • 此时将在项目的static/doc目录下生成postman.json文件;




  • postman.json文件直接导入到Postman中即可使用;




  • 导入成功后,所有接口都将在Postman中显示,这下我们可以愉快地测试接口了!



总结


smart-doc确实是一款好用的API文档生成工具,尤其是它零注解侵入的特点。虽然它的接口测试能力有所不足,但是可以一键生成JSON文件并导入到Postman中去,使用起来也是非常方便的!


参考资料


官方文档:gitee.com/smart-doc-t…


项目源码地址


github.com/macrozheng/…


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

你的列表很卡?这4个优化能让你的列表丝般顺滑

前言 列表 ListView 是应用中最为常见得组件,而列表往往也会承载很多元素,当元素多,尤其是那种图片文件比较大的场合,就可能会导致列表卡顿,严重的时候可能导致应用崩溃。本篇来介绍如何优化列表。 优化点1:使用 builder构建列表 当你的列表元素是动态...
继续阅读 »

前言


列表 ListView 是应用中最为常见得组件,而列表往往也会承载很多元素,当元素多,尤其是那种图片文件比较大的场合,就可能会导致列表卡顿,严重的时候可能导致应用崩溃。本篇来介绍如何优化列表。


优化点1:使用 builder构建列表


当你的列表元素是动态增长的时候(比如上拉加载更多),请不要直接用children 的方式,一直往children 的数组增加组件,那样会很糟糕。


//糟糕的用法
ListView(
children: [
item1,
item2,
item3,
...
],
)

//正确的用法
ListView.builder(
itemBuilder: (context, index) => ListItem(),
itemCount: itemCount,
)

对于 ListView.builder 是按需构建列表元素,也就是只有那些可见得元素才会调用itemBuilder 构建元素,这样对于大列表而言性能开销自然会小很多。



Creates a scrollable, linear array of widgets that are created on demand.
This constructor is appropriate for list views with a large (or infinite) number of children because the builder is called only for those children that are actually visible.



优化点2:禁用 addAutomaticKeepAlives 和 addRepaintBoundaries 特性


这两个属性都是为了优化滚动过程中的用户体验的。
addAutomaticKeepAlives 特性默认是 true,意思是在列表元素不可见后可以保持元素的状态,从而在再次出现在屏幕的时候能够快速构建。这其实是一个拿空间换时间的方法,会造成一定程度得内存开销。可以设置为 false 关闭这一特性。缺点是滑动过快的时候可能会出现短暂的白屏(实际会很少发生)。


addRepaintBoundaries 是将列表元素使用一个重绘边界(Repaint Boundary)包裹,从而使得滚动的时候可以避免重绘。而如果列表很容易绘制(列表元素布局比较简单的情况下)的时候,可以关闭这个特性来提高滚动的流畅度。


addAutomaticKeepAlives: false,
addRepaintBoundaries: false,

优化点3:尽可能将列表元素中不变的组件使用 const 修饰


使用 const 相当于将元素缓存起来实现共用,若列表元素某些部分一直保持不变,那么可以使用 const 修饰。


return Padding(
child: Row(
children: [
const ListImage(),
const SizedBox(
width: 5.0,
),
Text('第$index 个元素'),
],
),
padding: EdgeInsets.all(10.0),
);

优化点4:使用 itemExtent 确定列表元素滚动方向的尺寸


对于很多列表,我们在滚动方向上的尺寸是提前可以根据 UI设计稿知道的,如果能够知道的话,那么使用 itemExtent 属性制定列表元素在滚动方向的尺寸,可以提升性能。这是因为,如果不指定的话,在滚动过程中,会需要推算每个元素在滚动方向的尺寸从而消耗计算资源。


itemExtent: 120,

优化实例


下面是一开始未改造的列表,嗯,可以认为是垃圾代码


class LargeListView extends StatefulWidget {
const LargeListView({Key? key}) : super(key: key);

@override
_LargeListViewState createState() => _LargeListViewState();
}

class _LargeListViewState extends State<LargeListView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('大列表'),
brightness: Brightness.dark,
),
body: ListView(
children: List.generate(
1000,
(index) => Padding(
padding: EdgeInsets.all(10.0),
child: Row(
children: [
Image.network(
'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7869eac08a7d4177b600dc7d64998204~tplv-k3u1fbpfcp-watermark.jpeg',
width: 200,
),
const SizedBox(
width: 5.0,
),
Text('第$index 个元素'),
],
),
),
),
),
);
}
}

当然,实际不会是用 List.generate 来生成列表元素,**但是也不要用一个 List<Widget> 列表对象一直往里面加列表元素,然后把这个列表作为 ListView 的 **children
改造后的代码如下所示,因为将列表元素拆分得更细,代码量是多一些,但是性能上会好很多。


import 'package:flutter/material.dart';

class LargeListView extends StatefulWidget {
const LargeListView({Key? key}) : super(key: key);

@override
_LargeListViewState createState() => _LargeListViewState();
}

class _LargeListViewState extends State<LargeListView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('大列表'),
brightness: Brightness.dark,
),
body: ListView.builder(
itemBuilder: (context, index) => ListItem(
index: index,
),
itemCount: 1000,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
itemExtent: 120.0,
),
);
}
}

class ListItem extends StatelessWidget {
final int index;
ListItem({Key? key, required this.index}) : super(key: key);

@override
Widget build(BuildContext context) {
return Padding(
child: Row(
children: [
const ListImage(),
const SizedBox(
width: 5.0,
),
Text('第$index 个元素'),
],
),
padding: EdgeInsets.all(10.0),
);
}
}

class ListImage extends StatelessWidget {
const ListImage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Image.network(
'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7869eac08a7d4177b600dc7d64998204~tplv-k3u1fbpfcp-watermark.jpeg',
width: 200,
);
}
}

总结


本篇介绍了 Flutter ListView 的4个优化要点,非常实用哦!实际上,这些要点都可以从官网的文档里找出对应得说明。因此,如果遇到了性能问题,除了搜索引擎外,也建议多看看官方的文档。另外一个,对于列表图片,有时候也需要前后端配合,比如目前的手机都是号称1亿像素的,如果上传的时候直接上传原图,那么加载如此大的图片肯定是非常消耗资源的。对于这种情况,建议是生成列表缩略图(可能需要针对不同屏幕尺寸生成不同的缩略图,比如掘金的文章头图,就分了几种分辨率)。


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

Handler 源码分析

一、ThreadLocal是什么ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问,ThreadLocal可以让每个线程拥有一个属于自己的变量的副本,不会和其他线程的变量副本冲突,实现了线程的数据隔离。每个线程都有一...
继续阅读 »


一、ThreadLocal是什么

ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问,ThreadLocal可以让每个线程拥有一个属于自己的变量的副本,不会和其他线程的变量副本冲突,实现了线程的数据隔离。每个线程都有一个ThreadLocalMap类型的threadLocals变量,ThreadLocalMap是一个自定义哈希映射,仅用于维护线程本地变量值。ThreadLocalMap是ThreadLocal的内部类,主要有一个Entry数组,Entry的key为ThreadLocal,value为ThreadLocal对应的值。

也就是说ThreadLocal本身并不真正存储线程的变量值,它只是一个工具,用来维护Thread内部的Map,帮助存和取。注意上图的虚线,它代表一个弱引用类型,而弱引用的生命周期只能存活到下次GC前。

二、ThreadLocal是什么ThreadLocal为什么会内存泄漏

ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

但是JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。

三、Handler机制

我们主要看一下Looper源码, Looper 在程序启动的时候系统就已经帮我们创建好了

在main方法中系统调用了 Looper.prepareMainLooper();来创建主线程的Looper以及MessageQueue,并通过Looper.loop()来开启主线程的消息循环。来看看Looper.prepareMainLooper()是怎么创建出这两个对象的

//系统实例化 Handler
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}

可以看到,在这个方法中调用了 prepare(false);方法和 myLooper();方法,那么再进入prepare()

private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}// 往当前线程的私有变量里添加 Looper
sThreadLocal.set(new Looper(quitAllowed));
}

在这里可以看出,sThreadLocal对象保存了一个Looper对象,首先判断是否已经存在Looper对象了,以防止被调用两次。sThreadLocal对象是ThreadLocal类型,因此保证了每个线程中只有一个Looper对象。

// Looper 在实例化的时候也实例化了一个消息队列同时还持有了当前线程的引用
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

//然后我们从发送消息查看源码
public final boolean sendMessage(Message msg){
return sendMessageDelayed(msg, 0);
}

------经过几个方法的调用进入下面的方法

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
// mQueue 在 Handler 实例化的时候就从当前线程中取出消息队列并赋值了
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(this + "sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
// 重点在这里,把当前 Handler 的引用赋值给 msg 的 target
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

------进入消息队列的源码

boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}

synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(msg.target + " sending message to a Handler on a dead thread");
msg.recycle();
return false;
}
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
// p == null 代表前面没有消息, when 是延迟消息的时间值
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
//当前消息的 next 是 p 引用,形成一个单链表结构,如果是第一个消息的话,p 为空
msg.next = p;
// 赋值消息到轮询器
mMessages = msg;
needWake = mBlocked;
} else {
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
// 发送了第一个消息后
mMessages 就不为空了
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
收起阅读 »

Android基础-LRU缓存策略

前言缓存策略在Android开发是比较常见的,尤其是在图片使用业务场景中缓存策略发挥重要作用。对于移动应用常用UI就是图片组件而且图片显示资源常常来自网络,因此为了更快的图片加载和减省流量就需要使用缓存策略实现一个完备的图片加载框架。图片加载库的基本逻辑如下:...
继续阅读 »

前言

缓存策略在Android开发是比较常见的,尤其是在图片使用业务场景中缓存策略发挥重要作用。对于移动应用常用UI就是图片组件而且图片显示资源常常来自网络,因此为了更快的图片加载和减省流量就需要使用缓存策略实现一个完备的图片加载框架。

图片加载库的基本逻辑如下:优先从内存中找图片资源;然后从本地资源找图片;最后两者都没有的情况下才从网络中请求图片资源。

请求图片加载
是否有内存缓存
直接加载内存图片资源
是否有本地缓存
加载本地图片资源
请求加载网络图片资源
图片加载完成

内存缓存策略

关于内存缓存策略,在Android原生代码中有LruCache类。LruCache它的核心缓存策略算法是LRU(Least Recently Used)当缓存超出设置最大值时,会优先删除近期使用最少的缓存对象。当然本地存储缓存也能参考LRU策略,两者相结合就能实现一套如上图展示的完善基础资源缓存策略。

LruCache

LruCache是个泛型类,内部主要采用LinkedHashMap<K, V>存储形式缓存对象。提供put和get操作,当缓存超出最大存储值时会移除使用较少的缓存对象,put新的缓存对象。另外还支持remove方法,主动移除缓存对象释放更多缓存存储值。

  • get
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);

if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}

if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
  • put
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}

V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}

if (previous != null) {
entryRemoved(false, key, previous, value);
}

trimToSize(maxSize);
return previous;
}
  • remove
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}

V previous;
synchronized (this) {
previous = map.remove(key);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}

if (previous != null) {
entryRemoved(false, key, previous, null);
}

return previous;
}

通过put和remove方法会发现不管是添加还是删除都会执行safeSizeOf方法,在safeSizeOf中需要开发者自行实现sizeOf方法计算缓存大小累加到缓存池当中。另外put方法还多了trimToSize方法用来check缓存池是否超出最大缓存值。

收起阅读 »

提高app的响应能力-布局优化

提高app的响应能力-布局优化在刚开始开发安卓的时候,应用无响应(ANR)是很常见的,随着安卓手机的性能和编程能力的提高,现在不会遇到这样低级别的错误了,这篇文档分享如何提高应用程序的响应能力。前言应用响应能力意为用户操作时的速度,也就是让使用者感觉到轻、快、...
继续阅读 »

提高app的响应能力-布局优化

在刚开始开发安卓的时候,应用无响应(ANR)是很常见的,随着安卓手机的性能和编程能力的提高,现在不会遇到这样低级别的错误了,这篇文档分享如何提高应用程序的响应能力。

前言

应用响应能力意为用户操作时的速度,也就是让使用者感觉到轻、快、流畅才行。虽然现在的安卓设备一年比一年强劲,但我们在开发中要避免这些“慢”操作,打造让用户感觉到流畅的应用。

帧率控制

肉眼无法感知超过60FPS的动画

虽然有些证明人眼的感知极限是高于60FPS的,但60FPS的帧率已经完全满足。
那么每一帧的切换就是1000/60 等于16毫秒,手机原本为了保持视觉的流畅度,它的屏幕刷新频率是60hz,所以我们在开发中也应该注意这个时间,处理的间隔应当小于这个时间。

布局优化

GPU过度绘制

在开发者选项中有个一个很好用的功能叫“GPU过度绘制”,它的作用是可视化的显示出过度绘制的区域,那么什么叫过度绘制的区域呢。比如我们组件View是从上到下分布的,最顶部的View如果重合下面的View颜色,就叫做过度绘制。

它通过颜色来表示过度绘制的等级。

1.png

来看看下面的颜色级

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<RelativeLayout
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
android:background="#cfcfcf"
android:padding="10dp">

<RelativeLayout
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_centerInParent="true"
android:background="#cfcfcf"
android:padding="10dp">

<RelativeLayout
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_centerInParent="true"
android:background="#cfcfcf"
android:padding="10dp" />
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>

Screenshot_2021-11-07-20-12-40-86_2a27335eaa331505125090a61677c0b2.jpg

颜色越浅越好,虽然现在的设备对视图的要求并不高,但为了追求极致,我们在开发中还是应该注意这方面的问题。

GPU 呈现模式分析

开发者选项中-GPU呈现模式分析 这个功能开启之后,可以分析GPU的渲染速度,它对每个应用会单独显示一个图形。

Screenshot_2021-11-07-20-27-26-10_2a27335eaa331505125090a61677c0b2.jpg

注意水平方向的每一个竖条代表一帧,绿色线条代表16毫秒,开发者尽量做到没帧在该竖条以下。但由于复杂的业务和动画,我们经常会超出这个值,所以我们作为参考,在闲暇之余往这个方向去追求。


收起阅读 »