注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

SwiftUI开发小技巧总结(不定期更新)

iOS
目前SwiftUI还不完善,而且实际使用还会存在一些缺陷。网上的教程目前还很少,有也是收费的。因此特地整理一些平时开发中遇到的问题,免费提供给读者。 (注:本文主要面向对SwiftUI有一定基础的读者。) 调整状态栏样式 StatusBarStyle 尝试In...
继续阅读 »

目前SwiftUI还不完善,而且实际使用还会存在一些缺陷。网上的教程目前还很少,有也是收费的。因此特地整理一些平时开发中遇到的问题,免费提供给读者。


(注:本文主要面向对SwiftUI有一定基础的读者。)


调整状态栏样式 StatusBarStyle


尝试Info.plistUIApplication.statusBarStyle方法无效。如果有UIViewController作为根视图,重写方法preferredStatusBarStyle,这样可以控制全局;如果要设置单个页面的样式用preferredColorScheme(.light),但测试似乎设置无效。还有另一个方法:stackoverflow.com/questions/5…


调整导航栏样式 NavigationBar


let naviAppearance = UINavigationBarAppearance()
naviAppearance.configureWithOpaqueBackground() // 不透明背景样式
naviAppearance.backgroundColor = UIColor.whiteColor // 背景色
naviAppearance.shadowColor = UIColor.whiteColor // 阴影色
naviAppearance.titleTextAttributes = [:] // 标题样式
naviAppearance.largeTitleTextAttributes = [:] // 大标题样式
UINavigationBar.appearance().standardAppearance = naviAppearance
UINavigationBar.appearance().compactAppearance = naviAppearance
UINavigationBar.appearance().scrollEdgeAppearance = naviAppearance
UINavigationBar.appearance().tintColor = UIColor.blackColor // 导航栏按钮颜色


注意configureWithOpaqueBackground()需要在其它属性设置之前调用,除此之外还有透明背景configureWithTransparentBackground(),设置背景模糊效果backgroundEffect(),背景和阴影图片等,以及导航栏按钮样式也可修改。


调整标签栏样式 TabBar


let itemAppearance = UITabBarItemAppearance()
itemAppearance.normal.iconColor = UIColor.whiteColor // 正常状态的图标颜色
itemAppearance.normal.titleTextAttributes = [:] // 正常状态的文字样式
itemAppearance.selected.iconColor = UIColor.whiteColor // 选中状态的图标颜色
itemAppearance.selected.titleTextAttributes = [:] // 选中状态的文字样式
let tabBarAppearance = UITabBarAppearance()
tabBarAppearance.configureWithOpaqueBackground() // 不透明背景样式
tabBarAppearance.stackedLayoutAppearance = itemAppearance
tabBarAppearance.backgroundColor = UIColor.whiteColor // 背景色
tabBarAppearance.shadowColor = UIColor.clear // 阴影色
UITabBar.appearance().standardAppearance = tabBarAppearance


注意configureWithOpaqueBackground()同样需要在其它属性设置之前调用,和UINavigationBarAppearance一样有同样的设置,除此之外还可以为每个标签项设置指示器外观。


标签视图 TabView


设置默认选中页面:方法如下,同时每个标签项需要设置索引值tag()


TabView(selection: $selectIndex, content: {})
复制代码

控制底部标签栏显示和隐藏:


UITabBar.appearance().isHidden = true


NavigationView与TabView结合使用时,进入子页面TabBar不消失问题:不用默认的TabBar,将其隐藏,自己手动实现一个TabBar,放在根视图中。


键盘


输入框获得焦点(弹出键盘):在iOS15上增加了方法focused(),注意这个方法在视图初始化时是无效的,需要在onAppear()中延迟一定时间调用才可以。在此之前的系统只能自定义控件的方法实现参考这个:stackoverflow.com/questions/5…


关闭键盘,两种方法都可以:


UIApplication.shared.keyWindow?.endEditing(true)
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)


添加键盘工具栏:


.toolbar()


手势


获得按下和松开的状态:


.simultaneousGesture(
DragGesture(minimumDistance: 0)
    .onChanged({ _ in })
    .onEnded({ _ in })
)


通过代码滚动ScrollView到指定位置:借助ScrollViewReader可以获取位置,在onAppear()中设置位置scrollTo(),我们实际使用发现,需要做个延迟执行才会有效,可以把执行放在DispatchQueue.main.async()中执行。


TextEditor


修改背景色:


UITextView.appearance().backgroundColor


处理Return键结束编辑:


.onChange(of: text) { value in
if value.last == "\n" {
UIApplication.shared.keyWindow?.endEditing(true)
}
}


Text文本内部对齐方式


multilineTextAlignment(.center)


页面跳转


容易出错的情况:开发中会经常遇到这样的需求,列表中选择一项,进入子页面。点击按钮返回上一页。此时再次点击列表中的某一项,会发现显示的页面内容是错误的。如果你是用NavigationLink做页面跳转,并传递了isActive参数,那么是会遇到这样的问题。原因在于多个页面的使用的是同一个isActive参数。解决办法是,列表中每一项都用独立的变量控制。NavigationView也尽量不要写在TabView外面,可能会导致莫名其妙的问题。


属性包装器在init中的初始化


init方法中直接赋值会发现无法成功,应该用属性包装器自身的方法包装起来,赋值的属性名前加_,例如:


_value = State<Int>(initialValue: 1)
_value = Binding<Bool>.constant(true) // 也可以使用Swift语法特性直接写成.constant(true)


View如何忽略触摸事件


allowsHitTesting(false)

作者:iOS技术小组
链接:https://juejin.cn/post/7037780197076123685

收起阅读 »

设计一套完整的日志系统

iOS
需求日志对于线上排查问题是非常重要的,很多问题其实是很偶现的,同样的系统版本,同样的设备,可能就是用户的复现,而开发通过相同的操作和设备就是不复现。但是这个问题也不能一直不解决,所以可以通过日志的方式排查问题。可能是后台导致的问题,也可能是客户端逻辑问题,在关...
继续阅读 »

需求

日志对于线上排查问题是非常重要的,很多问题其实是很偶现的,同样的系统版本,同样的设备,可能就是用户的复现,而开发通过相同的操作和设备就是不复现。但是这个问题也不能一直不解决,所以可以通过日志的方式排查问题。可能是后台导致的问题,也可能是客户端逻辑问题,在关键点记录日志可以快速定位问题。

假设我们的用户量是一百万日活,其中有1%的用户使用出现问题,即使这个问题并不是崩溃,就是业务上或播放出现问题。那这部分用户就是一万的用户,一万的用户数量是很庞大的。而且大多数用户在遇到问题后,并不会主动去联系客服,而是转到其他平台上。

虽然我们现在有Kibana网络监控,但是只能排查网络请求是否有问题,用户是否在某个时间请求了服务器,服务器下发的数据是否正确,但是如果定位业务逻辑的问题,还是要客户端记录日志。

现状

我们项目中之前有日志系统,但是从业务和技术的角度来说,存在两个问题。现有的日志系统从业务层角度,需要用户手动导出并发给客服,对用户有不必要的打扰。而且大多数用户并不会答应客服的请求,不会导出日志给客服。从技术的角度,现有的日志系统代码很乱,而且性能很差,导致线上不敢持续记录日志,会导致播放器卡顿。

而且现有的日志系统仅限于debug环境开启主动记录,线上是不开启的,线上出问题后需要用户手动打开,并且记录时长只有三分钟。正是由于现在存在的诸多问题,所以大家对日志的使用并不是很积极,线上排查问题就比较困难。

方案设计

思路

正是针对现在存在的问题,我准备做一套新的日志系统,来替代现有的日志系统。新的日志系统定位很简单,就是纯粹的记录业务日志。Crash、埋点这些,我们都不记录在里面,这些可以当做以后的扩展。日志系统就记录三种日志,业务日志、网络日志、播放器日志。

日志收集我们采用的主动回捞策略,在日志平台上填写用户的uid,通过uid对指定设备下发回捞指令,回捞指令通过长连接的方式下发。客户端收到回捞指令后,根据筛选条件对日志进行筛选,随后以天为单位写入到不同的文件中,压缩后上传到后端。

在日志平台可以根据指定的条件进行搜索,并下载文件查看日志。为了便于开发者查看日志,从数据库取出的日志都会写成.txt形式,并上传此文件。

API设计

对于调用的API设计,应该足够简单,业务层使用时就像调用NSLog一样。所以对于API的设计方案,我采用的是宏定义的方式,调用方法和NSLog一样,调用很简单。

#if DEBUG
#define SVLogDebug(frmt, ...) [[SVLogManager sharedInstance] mobileLogContent:(frmt), ##__VA_ARGS__]
#else
#define SVLogDebug(frmt, ...) NSLog(frmt, ...)
#endif

日志总共分为三种类型,业务日志、播放器日志、网络日志,对于三种日志分别对应着不同的宏定义。不同的宏定义,写入数据库的类型也不一样,可以用户日志筛选。

  • 业务日志:SVLogDebug
  • 播放器日志:SVLogDebugPlayer
  • 网络日子:SVLogDebugQUIC

淘汰策略

不光是要往数据库里写,还需要考虑淘汰策略。淘汰策略需要平衡记录的日志数量,以及时效性的问题,日志数量尽量够排查问题,并且还不会占用过多的磁盘空间。所以,在日志上传之后会将已上传日志删除掉,除此之外日志淘汰策略有以下两种。

  1. 日志最多只保存三天,三天以前的日志都会被删掉。在应用启动后进行检查,并后台线程执行这个过程。
  2. 日志增加一个最大阈值,超过阈值的日志部分,以时间为序,从前往后删除。我们定义的阈值大小为200MB,一般不会超过这个大小。

记录基础信息

在排查问题时一些关键信息也很重要,例如用户当时的网络环境,以及一些配置项,这些因素对代码的执行都会有一些影响。对于这个问题,我们也会记录一些用户的配置信息及网络环境,方便排查问题,但不会涉及用户经纬度等隐私信息。

数据库

旧方案

之前的日志方案是通过DDLog实现的,这种方案有很严重的性能问题。其写入日志的方式,是通过NSData来实现的,在沙盒创建一个txt文件,通过一个句柄来向本地写文件,每次写完之后把句柄seek到文件末尾,下次直接在文件末尾继续写入日志。日志是以NSData的方式进行处理的,相当于一直在频繁的进行本地文件写入操作,还要在内存中维持一个或者多个句柄对象。

这种方式还有个问题在于,因为是直接进行二进制写入,在本地存储的是txt文件。这种方式是没有办法做筛选之类的操作的,扩展性很差,所以新的日志方案我们打算采用数据库来实现。

方案选择

我对比了一下iOS平台主流的数据库,发现WCDB是综合性能最好的,某些方面比FMDB都要好,而且由于是C++实现的代码,所以从代码执行的层面来讲,也不会有OC的消息发送和转发的额外消耗。

根据WCDB官网的统计数据,WCDBFMDB进行对比,FMDB是对SQLite进行简单封装的框架,和直接用SQLite差别不是很大。而WCDB则在sqlcipher的基础上进行的深度优化,综合性能比FMDB要高,以下是性能对比,数据来自WCDB官方文档。

单次读操作WCDB要比FMDB5%左右,在for循环内一直读。

15906481049447.jpg

单次写操作WCDB要比FMDB28%,一个for循环一直写。

15906481114970.jpg

批量写操作比较明显,WCDB要比FMDB180%,一个批量任务写入一批数据。

15906481277664.jpg

从数据可以看出,WCDB在写操作这块性能要比FMDB要快很多,而本地日志最频繁的就是写操作,所以这正好符合我们的需求,所以选择WCDB作为新的数据库方案是最合适的。而且项目中曝光模块已经用过WCDB,证明这个方案是可行并且性能很好的。

表设计

我们数据库的表设计很简单,就下面四个字段,不同类型的日志用type做区分。如果想增加新的日志类型,也可以在项目中扩展。因为使用的是数据库,所以扩展性很好。

  • index:主键,用来做索引。
  • content:日志内容,记录日志内容。
  • createTime:创建时间,日志入库的时间。
  • type:日志类型,用来区分三种类型。

数据库优化

我们是视频类应用,会涉及播放、下载、上传等主要功能,这些功能都会大量记录日志,来方便排查线上问题。所以,避免数据库太大就成了我在设计日志系统时,比较看重的一点。

根据日志规模,我对播放、下载、上传三个模块进行了大量测试,播放一天两夜、下载40集电视剧、上传多个高清视频,累计记录的日志数量大概五万多条。我发现数据库文件夹已经到200MB+的大小,这个大小已经是比较大的,所以需要对数据库进行优化。

我观察了一下数据库文件夹,有三个文件,dbshmwal,主要是数据库的日志文件太大,db文件反而并不大。所以需要调用sqlite3_wal_checkpointwal内容写入到数据库中,这样可以减少walshm文件的大小。但WCDB并没有提供直接checkpoint的方法,所以经过调研发现,执行database的关闭操作时,可以触发checkpoint

我在应用程序退出时,监听了terminal通知,并且把处理实际尽量靠后。这样可以保证日志不被遗漏,而且还可以在程序退出时关闭数据库。经过验证,优化后的数据库磁盘占用很小。143,987条数据库,数据库文件大小为34.8MB,压缩后的日志大小为1.4MB,解压后的日志大小为13.6MB

wal模式

这里顺带讲一下wal模式,以方便对数据库有更深入的了解。SQLite3.7版本加入了wal模式,但默认是不开启的,iOS版的WCDBwal模式自动开启,并且做了一些优化。

wal文件负责优化多线程下的并发操作,如果没有wal文件,在传统的delete模式下,数据库的读写操作是互斥的,为了防止写到一半的数据被读到,会等到写操作执行完成后,再执行读操作。而wal文件就是为了解决并发读写的情况,shm文件是对wal文件进行索引的。

SQLite比较常用的deletewal两种模式,这两种模式各有优势。delete是直接读写db-page,读写操作的都是同一份文件,所以读写是互斥的,不支持并发操作。而walappend新的db-page,这样写入速度比较快,而且可以支持并发操作,在写入的同时不读取正在操作的db-page即可。

由于delete模式操作的db-page是离散的,所以在执行批量写操作时,delete模式的性能会差很多,这也就是为什么WCDB的批量写入性能比较好的原因。而wal模式读操作会读取dbwal两个文件,这样会一定程度影响读数据的性能,所以wal的查询性能相对delete模式要差。

使用wal模式需要控制wal文件的db-page数量,如果page数量太大,会导致文件大小不受控制。wal文件并不是一直增加的,根据SQLite的设计,通过checkpoint操作可以将wal文件合并到db文件中。但同步的时机会导致查询操作被阻塞,所以不能频繁执行checkpoint。在WCDB中设置了一个1000的阈值,当page达到1000后才会执行一次checkpoint

这个1000是微信团队的一个经验值,太大会影响读写性能,而且占用过多的磁盘空间。太小会频繁执行checkpoint,导致读写受阻。

# define SQLITE_DEFAULT_WAL_AUTOCHECKPOINT  1000

sqlite3_wal_autocheckpoint(db, SQLITE_DEFAULT_WAL_AUTOCHECKPOINT);

int sqlite3_wal_autocheckpoint(sqlite3 *db, int nFrame){
#ifdef SQLITE_OMIT_WAL
UNUSED_PARAMETER(db);
UNUSED_PARAMETER(nFrame);
#else
#ifdef SQLITE_ENABLE_API_ARMOR
if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT;
#endif
if( nFrame>0 ){
sqlite3_wal_hook(db, sqlite3WalDefaultHook, SQLITE_INT_TO_PTR(nFrame));
}else{
sqlite3_wal_hook(db, 0, 0);
}
#endif
return SQLITE_OK;
}

也可以设置日志文件的大小限制,默认是-1,也就是没限制,journalSizeLimit的意思是,超出的部分会被覆写。尽量不要修改这个文件,可能会导致wal文件损坏。

i64 sqlite3PagerJournalSizeLimit(Pager *pPager, i64 iLimit){
if( iLimit>=-1 ){
pPager->journalSizeLimit = iLimit;
sqlite3WalLimit(pPager->pWal, iLimit);
}
return pPager->journalSizeLimit;
}

下发指令

日志平台

日志上报应该做到用户无感知,不需要用户主动配合即可进行日志的自动上传。而且并不是所有的用户日志都需要上报,只有出问题的用户日志才是我们需要的,这样也可以避免服务端的存储资源浪费。对于这些问题,我们开发了日志平台,通过下发上传指令的方式告知客户端上传日志。

037C8667-914E-43A7-8B6D-7B6EDD80E3A5.png

我们的日志平台做的比较简单,输入uid对指定的用户下发上传指令,客户端上传日志之后,也可以通过uid进行查询。如上图,下发指令时可以选择下面的日志类型和时间区间,客户端收到指令后会根据这些参数做筛选,如果没选择则是默认参数。搜索时也可以使用这三个参数。

日志平台对应一个服务,点击按钮下发上传指令时,服务会给长连接通道下发一个jsonjson中包含上面的参数,以后也可以用来扩展其他字段。上传日志是以天为单位的,所以在这里可以根据天为单位进行搜索,点击下载可以直接预览日志内容。

长连接通道

指令下发这块我们利用了现有的长连接,当用户反馈问题后,我们会记录下用户的uid,如果技术需要日志进行排查问题时,我们会通过日志平台下发指令。

指令会发送到公共的长连接服务后台,服务会通过长连接通道下发指令,如果指令下发到客户端之后,客户端会回复一个ack消息回复,告知通道已经收到指令,通道会将这条指令从队列中移除。如果此时用户未打开App,则这条指令会在下次用户打开App,和长连接通道建立连接时重新下发。

未完成的上传指令会在队列中,但最多不超过三天,因为超过三天的指令就已经失去其时效性,问题当时可能已经通过其他途径解决。

静默push

用户如果打开App时,日志指令的下发可以通过长连接通道下发。还有一种场景,也是最多的一种场景,用户未打开App怎么解决日志上报的问题,这块我们还在探索中。

当时也调研了美团的日志回捞,美团的方案中包含了静默push的策略,但是经过我们调研之后,发现静默push基本意义不大,只能覆盖一些很小的场景。例如用户App被系统kill掉,或者在后台被挂起等,这种场景对于我们来说并不多见。另外和push组的人也沟通了一下,push组反馈说静默push的到达率有些问题,所以就没采用静默push的策略。

日志上传

分片上传

进行方案设计的时候,由于后端不支持直接展示日志,只能以文件的方式下载下来。所以当时和后端约定的是以天为单位上传日志文件,例如回捞的时间点是,开始时间4月21日19:00,结束时间4月23日21:00。对于这种情况会被分割为三个文件,即每天为一个文件,第一天的文件只包含19:00开始的日志,最后一天的文件只包含到21:00的日志。

这种方案也是分片上传的一种策略,上传时以每个日志文件压缩一个zip文件后上传。这样一方面是保证上传成功率,文件太大会导致成功率下降,另一方面是为了做文件分割。经过观察,每个文件压缩成zip后,文件大小可以控制在500kb以内,500kb这个是我们之前做视频切片上传时的一个经验值,这个经验值是上传成功率和分片数量的一个平衡点。

日志命名使用时间戳为组合,时间单位应该精确到分钟,以方便服务端做时间筛选操作。上传以表单的方式进行提交,上传完成后会删除对应的本地日志。如果上传失败则使用重试策略,每个分片最多上传三次,如果三次都上传失败,则这次上传失败,在其他时机再重新上传。

安全性

为了保证日志的数据安全性,日志上传的请求我们通过https进行传输,但这还是不够的,https依然可以通过其他方式获取到SSL管道的明文信息。所以对于传输的内容,也需要进行加密,选择加密策略时,考虑到性能问题,加密方式采用的对称加密策略。

但对称加密的秘钥是通过非对称加密的方式下发的,并且每个上传指令对应一个唯一的秘钥。客户端先对文件进行压缩,对压缩后的文件进行加密,加密后分片再上传。服务端收到加密文件后,通过秘钥解密得到zip并解压缩。

主动上报

新的日志系统上线后,我们发现回捞成功率只有40%,因为有的用户反馈问题后就失去联系,或者反馈问题后一直没有打开App。对于这个问题,我分析用户反馈问题的途径主要有两大类,一种是用户从系统设置里进去反馈问题,并且和客服沟通后,技术介入排查问题。另一种是用户发生问题后,通过反馈群、App Store评论区、运营等渠道反馈的问题。

这两种方式都适用于日志回捞,但第一种由于有特定的触发条件,也就是用户点击进入反馈界面。所以,对于这种场景反馈问题的用户,我们增加了主动上报的方式。即用户点击反馈时,主动上报以当前时间为结束点,三天内的日志。这样可以把日志上报的成功率提升到90%左右,成功率上来后也会推动更多人接入日志模块,方便排查线上问题。

手动导出

日志上传的方式还包含手动导出,手动导出就是通过某种方式进入调试页面,在调试页面选择对应的日志分享,并且调起系统分享面板,通过对应的渠道分享出去。在新的日志系统之前,就是通过这种方式让用户手动导出日志给客服的,可想而知对用户的打扰有多严重吧。

现在手动导出的方式依然存在,但只用于debug阶段测试和开发同学,手动导出日志来排查问题,线上是不需要用户手动操作的。

dsasdlalfjsdafas.png


作者:刘小壮
链接:https://juejin.cn/post/7028229305050071071

收起阅读 »

iOS-组件化

iOS
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动 通过问题看本质!!! 组件化目的: 组件化可以明确业务模块职责及边界,降低模块之间的耦合以减少复杂依赖,提高代码可维护性,提高业务模块调度的规范性、灵活性,后续也可进一步优化编译速度。 那什么时候要做组...
继续阅读 »

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动


通过问题看本质!!!


组件化目的:


组件化可以明确业务模块职责及边界,降低模块之间的耦合以减少复杂依赖,提高代码可维护性,提高业务模块调度的规范性、灵活性,后续也可进一步优化编译速度。


那什么时候要做组件化呢?随着项目功能的复杂提升,各个业务代码耦合越来越多。这个时候就可以开始考虑组件化了。


通俗的讲,就好比你去宿舍附近的便利店买东西,直接走过去就到了。就没有必要打车了,打车效率还更低了呢。


如果你去公司(车程半小时),就有必要打车或者公交车了,走路那得多慢啊,等你走到了,估计都矿工几小时了。


组件化方案


1. URL路由方案;


2. runtime反射调用(简单反射及二次封装Target-action)


3. Target-action(category及动态调度);


4. protocol方案;


5. notification方案;


组件化的方案有很多种,没有哪种最好,只有哪种最合适。常见的是url-block、protocol-class、target-action方案。所以参考了网上的一些文章,对这3种方案做了一下简单的对比。


url-block 蘑菇街


路由中心维护一张路由表,url为key,block为value。


优点:

1、统一iOS、安卓的平台差异性


缺点:

1、url参数收到限制,只能传常规的字符串参数,无法传递data、image参数;


2、无法区分本地和远程情况;


3、组件本身依赖中间件,且分散注册的耦合较多;


4、启动时提供注册服务,保存在内存中。


protocol-class


优点:

1、扩展了本地调用的功能;


2、通过实现接口来提供服务,只是中间加了一层wrapper;


3、通过protocol-class做一个映射,在内存中保存一张映射表;


缺点:

还是存在内存中维护注册表的问题


target-action


使用target-action方式实现组件间的解耦,本身功能完全独立,不依赖中间件。


1、通过runtime进行反射,直接调用。


2、生成方法签名,通过invocation对象,直接执行invoke方法。


3、通过组件包装一层wrapper来给外界提供服务,不会对原组件代码造成入侵。


4、中间件是通过runtime来调用组件服务的,中间件的catergory提供服务给调度者。


5、使用者只需要依赖中间件,中间件又不需要依赖组件。


作者:龙在掘金62077
链接:https://juejin.cn/post/7023972006957678599
收起阅读 »

Swift 重构:通过预设视图样式,缩减代码量

iOS
通过预设常用视图基础属性,缩减每次创建时需要声明的属性行数(之后创建时不需要再重复声明),项目越大收益越高; 🌰🌰: { func application(_ application: UIApplication, didFinishLaunchin...
继续阅读 »

通过预设常用视图基础属性,缩减每次创建时需要声明的属性行数(之后创建时不需要再重复声明),项目越大收益越高;



🌰🌰:


{
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

UIApplication.setupAppearance(.white, barTintColor: .systemBlue)
}


源码:


@objc public extension UIApplication{

/// 配置 app 外观主题色
static func setupAppearance(_ tintColor: UIColor, barTintColor: UIColor) {
_ = {
$0.barTintColor = barTintColor
$0.tintColor = tintColor
$0.titleTextAttributes = [NSAttributedString.Key.foregroundColor: tintColor,]
}(UINavigationBar.appearance())


_ = {
$0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.black], for: .normal)
}(UIBarButtonItem.appearance(whenContainedInInstancesOf: [UIImagePickerController.self]))


_ = {
$0.setTitleColor(tintColor, for: .normal)
$0.titleLabel?.adjustsFontSizeToFitWidth = true;
$0.titleLabel?.minimumScaleFactor = 1.0;
$0.imageView?.contentMode = .scaleAspectFit
$0.isExclusiveTouch = true
$0.adjustsImageWhenHighlighted = false
}(UIButton.appearance(whenContainedInInstancesOf: [UINavigationBar.self]))


_ = {
$0.tintColor = tintColor

$0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: tintColor,
], for: .normal)
$0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: barTintColor,
], for: .selected)
}(UISegmentedControl.appearance(whenContainedInInstancesOf: [UINavigationBar.self]))


_ = {
$0.tintColor = tintColor
}(UISegmentedControl.appearance())


_ = {
$0.autoresizingMask = [.flexibleWidth, .flexibleHeight]
$0.showsHorizontalScrollIndicator = false
$0.keyboardDismissMode = .onDrag;
if #available(iOS 11.0, *) {
$0.contentInsetAdjustmentBehavior = .never;
}
}(UIScrollView.appearance())


_ = {
$0.separatorInset = .zero
$0.separatorStyle = .singleLine
$0.rowHeight = 60
$0.backgroundColor = .groupTableViewBackground
if #available(iOS 11.0, *) {
$0.estimatedRowHeight = 0.0;
$0.estimatedSectionHeaderHeight = 0.0;
$0.estimatedSectionFooterHeight = 0.0;
}
}(UITableView.appearance())


_ = {
$0.layoutMargins = .zero
$0.separatorInset = .zero
$0.selectionStyle = .none
$0.backgroundColor = .white
}(UITableViewCell.appearance())


_ = {
$0.scrollsToTop = false
$0.isPagingEnabled = true
$0.bounces = false
}(UICollectionView.appearance())


_ = {
$0.layoutMargins = .zero
$0.backgroundColor = .white
}(UICollectionViewCell.appearance())


_ = {
$0.titleLabel?.adjustsFontSizeToFitWidth = true;
$0.titleLabel?.minimumScaleFactor = 1.0;
$0.imageView?.contentMode = .scaleAspectFit
$0.isExclusiveTouch = true
$0.adjustsImageWhenHighlighted = false
}(UIButton.appearance())


_ = {
$0.isUserInteractionEnabled = true;
}(UIImageView.appearance())


_ = {
$0.isUserInteractionEnabled = true;
}(UILabel.appearance())


_ = {
$0.pageIndicatorTintColor = barTintColor
$0.currentPageIndicatorTintColor = tintColor
$0.isUserInteractionEnabled = true;
$0.hidesForSinglePage = true;
}(UIPageControl.appearance())


_ = {
$0.progressTintColor = barTintColor
$0.trackTintColor = .clear
}(UIProgressView.appearance())


_ = {
$0.datePickerMode = .date;
$0.locale = Locale(identifier: "zh_CN");
$0.backgroundColor = .white;
if #available(iOS 13.4, *) {
$0.preferredDatePickerStyle = .wheels
}
}(UIDatePicker.appearance())


_ = {
$0.minimumTrackTintColor = tintColor
$0.autoresizingMask = .flexibleWidth
}(UISlider.appearance())


_ = {
$0.onTintColor = tintColor
$0.autoresizingMask = .flexibleWidth
}(UISwitch.appearance())

}
}

作者:SoaringHeart
链接:https://juejin.cn/post/6974338640784654350

收起阅读 »

iOS Reachability

iOS
大多数App都严重依赖于网络,一款用户体验良好的的app是必须要考虑网络状态变化的。为了更好的用户体验,我们会在无网络时展现本地或者缓存的内容,并对用户进行合适的提示。对于网络状态的检测,苹果提供了Reachability,由此也衍生出各种 Reachabil...
继续阅读 »

大多数App都严重依赖于网络,一款用户体验良好的的app是必须要考虑网络状态变化的。

为了更好的用户体验,我们会在无网络时展现本地或者缓存的内容,并对用户进行合适的提示。对于网络状态的检测,苹果提供了Reachability,由此也衍生出各种 Reachability 框架,比较著名的有Github上的 tonymillion/Reachability 以及 AFNetworking 中的 AFNetworkReachabilityManager 模块,它们的实现原理基本上都是对苹果公司的SCNetworkReachability API进行的封装。

1、SCNetworkReachability (SystemConfiguration.framework)

0.png

获取网络状态:

@property (nonatomic, strong) dispatch_source_t timer;
@property (nonatomic, assign) SCNetworkReachabilityRef reachability;
@property (nonatomic, strong) dispatch_queue_t serialQueue;

-(void)dealloc {
if (_reachability != NULL) {
CFRelease(_reachability);
_reachability = NULL;
}
}

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.

//创建零地址,0.0.0.0地址表示查询本机的网络连接状态
struct sockaddr_in zeroAddress;
bzero(&zeroAddress, sizeof(zeroAddress));
zeroAddress.sin_len = sizeof(zeroAddress);
zeroAddress.sin_family = AF_INET;

_reachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress);
_serialQueue = dispatch_queue_create("com.xmy.serialQueue", DISPATCH_QUEUE_SERIAL);

__weak __typeof(self) weakSelf = self;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"连接状态: %d", [strongSelf isConnectionAvailable]);
});
dispatch_resume(_timer);

[self startMonitor];
}

- (BOOL)isConnectionAvailable
{
SCNetworkReachabilityFlags flags;
//获取连接的标志
BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(_reachability, &flags);

//如果不能获取连接标志,则不能进行网络连接,直接返回
if (!didRetrieveFlags) {
NSLog(@"Error. Could not recover network reachability flags");
return NO;
}

//根据连接标志进行判断
BOOL isReachable = ((flags & kSCNetworkFlagsReachable) != 0);
BOOL needConnection = ((flags & kSCNetworkFlagsConnectionRequired) != 0);

return (isReachable && !needConnection) ? YES : NO;
}

- (void)startMonitor
{
SCNetworkReachabilityContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
if (SCNetworkReachabilitySetCallback(_reachability, ReachabilityCallback, &context)) {
// Schedules the given target with the given run loop and mode.
// SCNetworkReachabilityScheduleWithRunLoop(_reachability, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);

// Schedule or unschedule callbacks for the given target on the given dispatch queue.
SCNetworkReachabilitySetDispatchQueue(_reachability, _serialQueue);
}
}

- (void)stopMonitor
{
SCNetworkReachabilitySetCallback(_reachability, NULL, NULL);

// Unschedules the given target from the given run loop and mode.
// SCNetworkReachabilityUnscheduleFromRunLoop(_reachability, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
SCNetworkReachabilitySetDispatchQueue(_reachability, NULL);
}

static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info)
{
NSLog(@"%@, %d, %@", target, flags, info);
}

优点:

  • 使用简单,只有一个类,官方还有Demo,容易上手
  • 灵敏度高,基本网络一有变化,基本马上就能判断出来

缺点:

  • 现在很流行的公用wifi,需要网页鉴权,鉴权之前无法上网,但本地连接已经建立
  • 存在本地网络连接,但信号很差,实际无法连接到服务器情况
  • 能否连接到指定服务器,比如国内访问墙外的服务器

苹果的Reachability有如下说明,告诉我们其能力受限于此:
The SCNetworkReachability programming interface allows an application to determine the status of a system's current network configuration and the reachability of a target host.
A remote host is considered reachable when a data packet, sent by an application into the network stack, can leave the local device. Reachability does not guarantee that the data packet will actually be received by the host.
当应用程序发送到网络堆栈的数据包可以离开本地设备时,就可以认为远程主机是可访问的,不能保证主机是否实际接收到数据包。

2、SimplePing

ping 是 Windows、Unix 、Linux和macOS 等系统下一个常用的命令,利用 ping 命令可以用来测试数据包能否通过IP 协议到达特定主机,并收到主机的应答,以检查网络是否连通和网络连接速度,帮助我们分析和判定网络故障。

SimplePing是苹果封装好的ping的功能,它利用resolve host,create socket(send&recv data),解析ICMP 包验证 checksum 等实现了 ping功能。并且支持iPv4 和 iPv6。

ping 功能使用是 ICMP 协议(Internet Control Message Protocol),ICMP 协议定义了一组错误信息,当路由器或者主机无法成功处理一个IP 封包的时候,能够将错误信息回送给来源主机:

1.png ICMP用途:差错通知、信息查询、重定向等

2.png [1]给送信者的错误通知;[2]送信者的信息查询。

[1]是到IP 数据包被对方的计算机处理的过程中,发生了什么错误时被使用。不仅传送发生了错误这个事实,也传送错误原因等消息。

[2]的信息询问是在送信方的计算机向对方计算机询问信息时被使用。被询问内容的种类非常丰富,他们有目标IP 地址的机器是否存在这种基本确认,调查自己网络的子网掩码,取得对方机器的时间信息等。

Ping实现:

3.png Ping超时原因:

  • 目标服务器不存在
  • 花在数据包交流上的时间太长ping命令认为超时
  • 目标服务器不回答ping命令

SimplePing实现:

4.png SimplePing初始化:

let hostName = "www.baidu.com"
var pinger: SimplePing?
var sendTimer: NSTimer?

/// Called by the table view selection delegate callback to start the ping.
func start(forceIPv4 forceIPv4: Bool, forceIPv6: Bool) {
let pinger = SimplePing(hostName: self.hostName)
self.pinger = pinger

// By default we use the first IP address we get back from host resolution (.Any)
// but these flags let the user override that.
if (forceIPv4 && !forceIPv6) {
pinger.addressStyle = .ICMPv4
} else if (forceIPv6 && !forceIPv4) {
pinger.addressStyle = .ICMPv6
}

pinger.delegate = self
pinger.start()
}

/// Called by the table view selection delegate callback to stop the ping.
func stop() {
self.pinger?.stop()
self.pinger = nil

self.sendTimer?.invalidate()
self.sendTimer = nil

self.pingerDidStop()
}

/// Sends a ping.
/// Called to send a ping, both directly (as soon as the SimplePing object starts up) and
/// via a timer (to continue sending pings periodically).
func sendPing() {
self.pinger!.sendPingWithData(nil)
}

代理方法:

/// pinger.start()成功之后,解析HostName拿到ip地址后的回调
func simplePing(pinger: SimplePing, didStartWithAddress address: NSData) {
NSLog("pinging %@", MainViewController.displayAddressForAddress(address))

// Send the first ping straight away.
self.sendPing()

// And start a timer to send the subsequent pings.
assert(self.sendTimer == nil)
self.sendTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(MainViewController.sendPing), userInfo: nil, repeats: true)
}

/// pinger.start()功能启动失败的回调
func simplePing(pinger: SimplePing, didFailWithError error: NSError) {
NSLog("failed: %@", MainViewController.shortErrorFromError(error))

self.stop()
}

/// sendPingWithData发送数据成功
func simplePing(pinger: SimplePing, didSendPacket packet: NSData, sequenceNumber: UInt16) {
NSLog("#%u sent", sequenceNumber)
}

/// sendPingWithData发送数据失败,并返回错误信息
func simplePing(pinger: SimplePing, didFailToSendPacket packet: NSData, sequenceNumber: UInt16, error: NSError) {
NSLog("#%u send failed: %@", sequenceNumber, MainViewController.shortErrorFromError(error))
}

/// ping发送后收到响应
func simplePing(pinger: SimplePing, didReceivePingResponsePacket packet: NSData, sequenceNumber: UInt16) {
NSLog("#%u received, size=%zu", sequenceNumber, packet.length)
}

/// ping接收响应封包发生异常
func simplePing(pinger: SimplePing, didReceiveUnexpectedPacket packet: NSData) {
NSLog("unexpected packet, size=%zu", packet.length)
}

如代码所示,每隔一段时间就ping下host,看看是否畅通无阻,因此ping不可能做到及时判断网络变化,会有一定的延迟:
利用Reachability判断当前设备是否联网,利用SimplePing来检查服务器是否连通。

3、RealReachability (Star: 3k)

5.png

4、扩展:traceroute

由于ping命令不一定能判断对方是否存在,为了查看主机及目标主机之间的路由路径,我们使用traceroute 命令。它与ping 并列,也是ICMP 的典型实现之一。

traceroute是利用增加存活时间(TTL)值来实现功能的。每当一个icmp包经过一个路由器时,其存活时间值就会减1,当其存活时间为0时,路由器便会取消包发送,并发送一个ICMP TTL超时封包给原封包发出者。

6.png

7.png 命令行测试:

测试1
> traceroute -I baidu.com
traceroute: Warning: baidu.com has multiple addresses; using 220.181.38.148
traceroute to baidu.com (220.181.38.148), 64 hops max, 72 byte packets
1 172.25.62.254 (172.25.62.254) 2.198 ms 1.690 ms 1.437 ms
2 172.25.100.17 (172.25.100.17) 2.175 ms 1.795 ms 1.769 ms
3 * * *
4 * * *
5 * * *
6 * * *
7 * * *
8 * * *
9 * * *
10 * * *
11 * * *
12 * * *
13 * * *
14 * * *
15 * * *
16 220.181.38.148 (220.181.38.148) 29.700 ms 29.135 ms 29.127 ms

测试2
> traceroute -I baidu.com
traceroute: Warning: baidu.com has multiple addresses; using 39.156.69.79
traceroute to baidu.com (39.156.69.79), 64 hops max, 72 byte packets
1 172.25.62.254 (172.25.62.254) 3.339 ms 1.993 ms 4.845 ms
2 172.25.100.17 (172.25.100.17) 2.146 ms 1.792 ms 1.971 ms
3 * * *
4 * * *
5 * * *
6 * * *
7 * * *
8 * * *
9 * * *
10 * * *
11 * * *
12 * * *
13 * * *
14 * * *
15 * * *
16 * * *
17 * * *
18 39.156.69.79 (39.156.69.79) 29.015 ms 27.569 ms 28.232 ms

net-diagnosis (Star: 0.3k)
通过集成net-diagnosis,您可以轻松地在iOS上实现ping / traceroute /移动公共网络信息/端口扫描等网络诊断相关的功能。

8.png

9.png


收起阅读 »

iOS开发Crash之内存暴涨

iOS
今天遇到了一个线上的Crash,线上包,用户打开APP后就一直闪退,但是我们开发和测试都没有这样的问题,后面等到Bugly上报后,看到问题,找到了相对应的测试包开始复现,同事在某一个tf上的build版本QA测试成功出了这个Crash.找到对应的组件分支,全m...
继续阅读 »

今天遇到了一个线上的Crash,线上包,用户打开APP后就一直闪退,但是我们开发和测试都没有这样的问题,后面等到Bugly上报后,看到问题,找到了相对应的测试包开始复现,同事在某一个tf上的build版本QA测试成功出了这个Crash.找到对应的组件分支,全master指定版本真机测试.


全master真机运行出现的结果为


image.png


Message from debugger:Terminated due to memory issue
复制代码

看见这个错误大概就知道是内存暴涨被看门狗杀死了


关键是如何排查


使用instrument中的leaks工具来查看,但是我们并没有这样排查,我们发现一进APP过了main以后就被杀死了,那么可以初步确定是工作台发生的错误.


那么工作台的主要功能就是加载相应权限,然后配置菜单等功能,想到会不会是因为这个客户的数据异常,我们把网络关了,然后进入工作台,不会Crash了。


然后我们把工作台所用到的几个请求一一排查下来,发现在工作台有一个功能卡片组件,组件里面使用了富文本,而富文本是根据后端来的数据来进行对应的加载。


后端接口返回有一个字段,是否采用富文本,然后哪一段启用富文本,字体,颜色,样式都是由后端决定。


因为是一个tableView的Cell,里面又嵌套了for循环,for循环里面又嵌套了for来展示item,item又记录的一条条的富文本还有图片,犹豫cell每次数据源都会addSubviews,没有remove掉,再加上后端返回了N条,同事写的时候没有限制,我们这边只显示4条,显示的图片占用内存很大,从而导致内存暴涨,这一块因为是很老的代码了,需要重构一下,为了线上先不崩溃,跟后端商量图片压缩以及返回条数限制


最后记录一下crash日志
image.png


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

leetcode-最接近的三数之和

往常周末都是睡懒觉,今天早起去体检了。每年都是到了体检的时候,才会想起来身体才是革命的本钱吧。还好都不是什么大问题,最大的问题就是自己没有坚持锻炼。 先立个Flag,每周至少有5天,专门锻炼30分钟以上吧。先把标准定的低一点,能做到最重要,不然都是5分钟热情,...
继续阅读 »

往常周末都是睡懒觉,今天早起去体检了。每年都是到了体检的时候,才会想起来身体才是革命的本钱吧。还好都不是什么大问题,最大的问题就是自己没有坚持锻炼。

先立个Flag,每周至少有5天,专门锻炼30分钟以上吧。先把标准定的低一点,能做到最重要,不然都是5分钟热情,过了几天这个目标就抛在脑后了吧。

当然,说起坚持,一个比较好的方法是定期review,如果有1天没做到,也不要觉得反正已经没做到,破罐子破摔,后面根本就不再做的。每天坚持是每天新的挑战,能够比之前的自己坚持做更久,就是自己的突破。

每天做1题算法题,也是上个月立下的Flag,虽然已经倒了,还是希望后面能多坚持,毕竟进一寸就有进一寸的欢喜。今天继续刷leetcode第16题,跟昨天的题目非常类似,昨天要求3个数之和=0,今天要求是跟target偏离最小。


题目



给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。




示例

输入: nums = [-1,2,1,-4], target = 1

输出: 2

解释: 与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。



思路


跟昨天的题目是非常类似的,首先肯定还是排序。
排序后,假定一个最小值a,然后从a右边的数里面找出2个b和c,使得abs(a+b+c-target)的值最小。因为a右边的数组也是有序的,这时候找b和c其实也不需要2层for循环来遍历,可以使用双指针,分别指向剩余数组的最小和最大,如果a+b+c-target小于0,就让最小值往右边走一个,如果a+b+c-target大于0,就让最大值往左边走一个。


Java版本代码


class Solution {
public int threeSumClosest(int[] nums, int target) {
int len = nums.length;
int ans = 3001;
Arrays.sort(nums);
for (int i = 0; i < len -2; i++) {
if (i > 0 && nums[i] == nums[i-1]) {
continue;
}
int start = i + 1;
int end = len - 1;
while (start < end) {
int sum = nums[i] + nums[start] + nums[end];
if (sum == target) {
ans = sum;
return ans;
}
if (Math.abs(sum - target) < Math.abs(ans - target)) {
ans = sum;
}
if (sum > target) {
end--;
} else if (sum < target) {
start++;
}
}
}
return ans;
}
}

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

Android正确的保活方案,不要掉进保活需求死循环陷进

在开始前,还是给大家简单介绍一下,以前出现过的一些黑科技: 大概在6年前Github中出现过一个叫MarsDaemon,这个库通过双进程守护的方式实现保活,一时间风头无两。好景不长,进入 Android 8.0时代之后,这个库就废掉了。 最近2年Github上...
继续阅读 »

在开始前,还是给大家简单介绍一下,以前出现过的一些黑科技:


大概在6年前Github中出现过一个叫MarsDaemon,这个库通过双进程守护的方式实现保活,一时间风头无两。好景不长,进入 Android 8.0时代之后,这个库就废掉了。


最近2年Github上面出来一个Leoric 感兴趣的可以去看一下源码,谁敢用在生产环境呢,也就自己玩玩的才会用吧(不能因为保活而导致手机卡巴斯基),我没有试过这个,我想说的是:黑科技能黑的了一时,能黑的了一世吗?


没有规矩,不成方圆,要提升产品的存活率,最终还是要落到产品本身上面来,尊重用户,提升用户体验才是正道。


以前我也是深受保活需求的压迫,最近发现QQ群里有人又提到了如何保活,那么我们就来说一说,如何来正确保活App?




Android 8.0之后: 加强了应用后台限制,当时测试过一组数据:



应用处于前台,启动一个前台Service,里面使用JobScheduler启动定时任务(30秒触发一次),
此时手机锁屏,前10分钟内,定时任务都是正常执行;

大概在12分钟左右,发现应用进程就被kill掉了,解锁屏幕,app也不在前台了;



各大国产手机厂商底层都经过自己魔改,自家都有自己的一套自启动管理,小米手机更乱(当时有个神隐模式的概念,那也是杀后台高手),只能说当时Android手机各种性能方面都不足,各家都会有自己的一套省电模式,以此来达到省电和提高手机性能,Android 系统变得越来越完善,但是厂商定制的自启动、省电模式还在,所以我们要做保活。


1.Android 8.0之前-常用的保活方案



1.开启一个前台Service

2.Android 6.0+ 忽略电池优化开关(稍后会有代码)

3.无障碍服务(只针对有用这个功能的app,如支付宝语音增强提醒用了它)





2.Android 8.0之后-常用的保活方案



1.开启一个前台Service(可以加上,单独启用的话无法满足保活需求)

2.Android 6.0+ 忽略电池优化开关(稍后会有代码)

3.无障碍服务(只针对有用这个功能的app,如支付宝语音增强提醒用了它)

4.应用自启动权限(最简单的方案是针对不同系统提供教程图片-让用户自己去打开)

5.多任务列表窗口加锁(提供GIF教程图片-让用户自己去打开)

6.多任务列表窗口隐藏App(仅针对有这方面需求的App)

7.应用后台高耗电(仅针对Vivo手机)



3.保活方案实现步骤


(1). 前台Service


//前台服务
class ForegroundCoreService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
private var mForegroundNF:ForegroundNF by lazy {
ForegroundNF(this)
}
override fun onCreate() {
super.onCreate()
mForegroundNF.startForegroundNotification()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if(null == intent){
//服务被系统kill掉之后重启进来的
return START_NOT_STICKY
}
mForegroundNF.startForegroundNotification()
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
mForegroundNF.stopForegroundNotification()
super.onDestroy()
}
}

//初始化前台通知,停止前台通知
class ForegroundNF(private val service: ForegroundCoreService) : ContextWrapper(service) {
companion object {
private const val START_ID = 101
private const val CHANNEL_ID = "app_foreground_service"
private const val CHANNEL_NAME = "前台保活服务"
}
private var mNotificationManager: NotificationManager? = null

private var mCompatBuilder:NotificationCompat.Builder?=null

private val compatBuilder: NotificationCompat.Builder?
get() {
if (mCompatBuilder == null) {
val notificationIntent = Intent(this, MainActivity::class.java)
notificationIntent.action = Intent.ACTION_MAIN
notificationIntent.addCategory(Intent.CATEGORY_LAUNCHER)
notificationIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
//动作意图
val pendingIntent = PendingIntent.getActivity(
this, (Math.random() * 10 + 10).toInt(),
notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT
)
val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(this,CHANNEL_ID)
//标题
notificationBuilder.setContentTitle(getString(R.string.notification_content))
//通知内容
notificationBuilder.setContentText(getString(R.string.notification_sub_content))
//状态栏显示的小图标
notificationBuilder.setSmallIcon(R.mipmap.ic_coolback_launcher)
//通知内容打开的意图
notificationBuilder.setContentIntent(pendingIntent)
mCompatBuilder = notificationBuilder
}
return mCompatBuilder
}

init {
createNotificationChannel()
}

//创建通知渠道
private fun createNotificationChannel() {
mNotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//针对8.0+系统
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
channel.setShowBadge(false)
mNotificationManager?.createNotificationChannel(channel)
}
}

//开启前台通知
fun startForegroundNotification() {
service.startForeground(START_ID, compatBuilder?.build())
}

//停止前台服务并清除通知
fun stopForegroundNotification() {
mNotificationManager?.cancelAll()
service.stopForeground(true)
}
}

(2).忽略电池优化(Android 6.0+)


1.我们需要在AndroidManifest.xml中声明一下权限


<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

2.通过Intent来请求忽略电池优化的权限(需要引导用户点击)


//在Activity的onCreate中注册ActivityResult,一定要在onCreate中注册
//监听onActivityForResult回调
mIgnoreBatteryResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
//查询是否开启成功
if(queryBatteryOptimizeStatus()){
//忽略电池优化开启成功
}else{
//开启失败
}
}

通过Intent打开忽略电池优化弹框:


val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.data = Uri.parse("package:$packageName")
//启动忽略电池优化,会弹出一个系统的弹框,我们在上面的
launchActivityResult(intent)

查询是否成功开启忽略电池优化开关:


fun Context.queryBatteryOptimizeStatus():Boolean{
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager?
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
powerManager?.isIgnoringBatteryOptimizations(packageName)?:false
} else {
true
}
}

(3).无障碍服务


看官方文档:创建自己的无障碍服务

它也是一个Service,它的优先级比较高,提供界面增强功能,初衷是帮助视觉障碍的用户或者是可能暂时无法与设备进行全面互动的用户完成操作。

可以做很多事情,使用了此Service,在6.0+不需要申请悬浮窗权限,直接使用WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY 挺方便的

(仅针对有需要此服务的app,可以开启增强后台保活)


(4).自启动权限(即:白名单管理列表页面)


是系统给用户自己去打开“自启动权限”开关的入口,我们需要针对不同的手机厂商和系统版本,弹出提示引导用户是否前去打开“自启动权限”

有的手机厂商叫:白名单管理,有的叫:自启动权限,两个是一个概念;

点击查看跳转到『手机自启动设置页面』完整代码


(需要注意:如果是代码控制跳转,无法保证永远可以调整,系统升级可能就给你屏蔽了,
最简单的方法是:显示一个如何找到自启动页面的引导图,下面以华为手机为例:)



华为手机-自启动管理

(5).多任务列表窗口加锁


可以针对不同手机厂商,显示引导用户,开启App窗口加锁之后,点击清理加速不会导致应用被kill



华为手机窗口加锁-教程图

(6).多任务列表窗口隐藏App窗口


刚刚上面多任务窗口加锁完,再提示用户去App里面把隐藏App窗口开关打开,这样用户就不会多任务列表里面把App窗口给手抖划掉


多任务窗口中『隐藏App窗口』,可以用如下代码控制:

(这个也只是针对有这方面需求App提供的一种增强方案罢了:因为隐藏了窗口,用户就不会去想他,不会去手痒去划掉它)


//在多任务列表页面隐藏App窗口
fun hideAppWindow(context: Context,isHide:Boolean){
try {
val activityManager: ActivityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
//控制App的窗口是否在多任务列表显示
activityManager.appTasks[0].setExcludeFromRecents(isHide)
}catch (e:Exception){
.....
}
}

(7).应用后台高耗电(Vivo手机独有)


开启的入口:“设置”>“电池”>“后台高耗电”>“找到xxxApp打开开关”



vivo允许后台高耗电



最后还是奉劝那些,仍然执着于找寻黑科技的开发者,醒醒吧,太阳晒屁股了。


如果说你的App用户群体不是普通用户,是专门给一些玩机大神们用的,都可以root手机的话,那么直接 move 到系统目录 priv/system/app 即可, 即使被用户强杀也会自动重新拉起。


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

Android 优雅处理重复点击(建议收藏)

一般手机上的 Android App,主要的交互方式是点击。用户在点击后,App 可能做出在页面内更新 UI、新开一个页面或者发起网络请求等操作。Android 系统本身没有对重复点击做处理,如果用户在短时间内多次点击,则可能出现新开多个页面或者重复发起网络请...
继续阅读 »

一般手机上的 Android App,主要的交互方式是点击。用户在点击后,App 可能做出在页面内更新 UI、新开一个页面或者发起网络请求等操作。Android 系统本身没有对重复点击做处理,如果用户在短时间内多次点击,则可能出现新开多个页面或者重复发起网络请求等问题。因此,需要对重复点击有影响的地方,增加处理重复点击的代码。


之前的处理方式


之前在项目中使用的是 RxJava 的方案,利用第三方库 RxBinding 实现了防止重复点击:


fun View.onSingleClick(interval: Long = 1000L, listener: (View) -> Unit) {
    RxView.clicks(this)
        .throttleFirst(interval, TimeUnit.MILLISECONDS)
        .subscribe({
            listener.invoke(this)
        }, {
            LogUtil.printStackTrace(it)
        })
}

但是这样有一个问题,比如使用两个手指同时点击两个不同的按钮,按钮的功能都是新开页面,那么有可能会新开两个页面。因为 Rxjava 这种方式是针对单个控件实现防止重复点击,不是多个控件。


现在的处理方式


现在使用的是时间判断,在时间范围内只响应一次点击,通过将上次单击时间保存到 Activity Window 中的 decorView 里,实现一个 Activity 中所有的 View 共用一个上次单击时间。


fun View.onSingleClick(
    interval: Int = SingleClickUtil.singleClickInterval,
    isShareSingleClick: Boolean = true,
    listener: (View) -> Unit
) {
    setOnClickListener {
        val target = if (isShareSingleClick) getActivity(this)?.window?.decorView ?: this else this
        val millis = target.getTag(R.id.single_click_tag_last_single_click_millis) as? Long ?: 0
        if (SystemClock.uptimeMillis() - millis >= interval) {
            target.setTag(
                R.id.single_click_tag_last_single_click_millis, SystemClock.uptimeMillis()
            )
            listener.invoke(this)
        }
    }
}

private fun getActivity(view: View): Activity? {
    var context = view.context
    while (context is ContextWrapper) {
        if (context is Activity) {
            return context
        }
        context = context.baseContext
    }
    return null
}

参数 isShareSingleClick 的默认值为 true,表示该控件和同一个 Activity 中其他控件共用一个上次单击时间,也可以手动改成 false,表示该控件自己独享一个上次单击时间。


mBinding.btn1.onSingleClick {
    // 处理单次点击
}

mBinding.btn2.onSingleClick(interval = 2000, isShareSingleClick = false) {
    // 处理单次点击
}

其他场景处理重复点击


间接设置点击


除了直接在 View 上设置的点击监听外,其他间接设置点击的地方也存在需要处理重复点击的场景,比如说富文本和列表。


为此将判断是否触发单次点击的代码抽离出来,单独作为一个方法:


fun View.onSingleClick(
    interval: Int = SingleClickUtil.singleClickInterval,
    isShareSingleClick: Boolean = true,
    listener: (View) -> Unit
) {
    setOnClickListener { determineTriggerSingleClick(interval, isShareSingleClick, listener) }
}

fun View.determineTriggerSingleClick(
    interval: Int = SingleClickUtil.singleClickInterval,
    isShareSingleClick: Boolean = true,
    listener: (View) -> Unit
) {
    ...
}

直接在点击监听回调中调用 determineTriggerSingleClick 判断是否触发单次点击。下面拿富文本和列表举例。


富文本


继承 ClickableSpan,在 onClick 回调中判断是否触发单次点击:


inline fun SpannableStringBuilder.onSingleClick(
    listener: (View) -> Unit,
    isShareSingleClick: Boolean = true,
    ...
): SpannableStringBuilder = inSpans(
    object : ClickableSpan() {
        override fun onClick(widget: View) {
            widget.determineTriggerSingleClick(interval, isShareSingleClick, listener)
        }
        ...
    },
    builderAction = builderAction
)

这样会有一个问题, onClick 回调中的 widget,就是设置富文本的控件,也就是说如果富文本存在多个单次点击的地方, 就算 isShareSingleClick 值为 false,这些单次点击还是会共用设置富文本控件的上次单击时间。


因此,这里需要特殊处理,在 isShareSingleClick 为 false 的时候,创建一个假的 View 来触发单击事件,这样富文本中多个单次点击 isShareSingleClick 为 false 的地方都有一个自己的假的 View 来独享上次单击时间。


class SingleClickableSpan(
    ...
) : ClickableSpan() {

    private var mFakeView: View? = null

    override fun onClick(widget: View) {
        if (isShareSingleClick) {
            widget
        } else {
            if (mFakeView == null) {
                mFakeView = View(widget.context)
            }
            mFakeView!!
        }.determineTriggerSingleClick(interval, isShareSingleClick, listener)
    }
    ...
}

在设置富文本的地方,使用设置 onSingleClick 实现单次点击:


mBinding.tvText.movementMethod = LinkMovementMethod.getInstance()
mBinding.tvText.highlightColor = Color.TRANSPARENT
mBinding.tvText.text = buildSpannedString {
    append("normalText")
    onSingleClick({
        // 处理单次点击
    }) {
        color(Color.GREEN) { append("clickText") }
    }
}

列表


列表使用 RecyclerView 控件,适配器使用第三方库 BaseRecyclerViewAdapterHelper。


Item 点击:


adapter.setOnItemClickListener { _, view, _ ->
    view.determineTriggerSingleClick {
        // 处理单次点击
    }
}

Item Child 点击:


adapter.addChildClickViewIds(R.id.btn1, R.id.btn2)
adapter.setOnItemChildClickListener { _, view, _ ->
    when (view.id) {
        R.id.btn1 -> {
            // 处理普通点击
        }
        R.id.btn2 -> view.determineTriggerSingleClick {
            // 处理单次点击
        }
    }
}

数据绑定


使用 DataBinding 的时候,有时会在布局文件中直接设置点击事件,于是在 View.onSingleClick 上增加 @BindingAdapte 注解,实现在布局文件中设置单次点击事件,并对代码做出调整,这个时候需要将项目中 listener: (View) -> Unit 替换成 listener: View.OnClickListener。


@BindingAdapter(
    *["singleClickInterval", "isShareSingleClick", "onSingleClick"],
    requireAll = false
)
fun View.onSingleClick(
    interval: Int? = SingleClickUtil.singleClickInterval,
    isShareSingleClick: Boolean? = true,
    listener: View.OnClickListener? = null
) {
    if (listener == null) {
        return
    }

    setOnClickListener {
        determineTriggerSingleClick(
            interval ?: SingleClickUtil.singleClickInterval, isShareSingleClick ?: true, listener
        )
    }
}

在布局文件中设置单次点击:


<androidx.appcompat.widget.AppCompatButton
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/btn"
    app:isShareSingleClick="@{false}"
    app:onSingleClick="@{()->viewModel.handleClick()}"
    app:singleClickInterval="@{2000}" />

在代码中处理单次点击:


class YourViewModel : ViewModel() {

    fun handleClick() {
        // 处理单次点击
    }
}

总结


对于直接在 View 上设置点击的地方,如果需要处理重复点击使用 onSingleClick,不需要处理重复点击则使用原来的 setOnClickListener。


对于间接设置点击的地方,如果需要处理重复点击,则使用 determineTriggerSingleClick 判断是否触发单次点击。


项目地址


https://github.com/TaylorKunZhang/single-click


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

【Flutter App】GetX框架的实践

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

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

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

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

image.png

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

image.png

image.png

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

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

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

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

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

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

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


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

收起阅读 »

美团跨端一体化富文本管理技术实践

为了减少产品和前端开发人员之间的矛盾,不断降本提效,美团医药技术部构建了跨端一体化富文本管理平台Page-佩奇。本文系统介绍了该平台的定位、设计思路、实现原理以及取得的成效。希望这些实战经验与总结,能给大家带来一些启发或帮助。一、引言在互联网圈,开发和产品经理...
继续阅读 »



为了减少产品和前端开发人员之间的矛盾,不断降本提效,美团医药技术部构建了跨端一体化富文本管理平台Page-佩奇。本文系统介绍了该平台的定位、设计思路、实现原理以及取得的成效。希望这些实战经验与总结,能给大家带来一些启发或帮助。

一、引言

在互联网圈,开发和产品经理之间相爱相杀的故事,相信大家都有所耳闻。归根结底,往往都是从简单的改需求开始,然后你来我往、互不相让,接着吵架斗嘴,最后导致矛盾不断升级,甚至带来比较严重的后果。

pic_4bf1ae9b.png

图1

在这种背景下,如果把一些功能相对简单的、需求变动比较频繁的页面,直接交给产品或者运营自己去通过平台实现,是不是就可以从一定程度上减少产品和开发人员之间的矛盾呢?

二、背景

当然上述的情况,美团也不例外。近些年,美团到家事业群(包括美团外卖、美团配送、闪购、医药、团好货等)的各个业务稳步发展,业务前端对接的运营团队有近几十个,每个运营团队又有不同的运营规则,这些规则还存在一些细微的样式差别,同时规则内容还会随着运营季节、节日、地理位置等进行变化和更新。这些需求具体来说有以下几个特点:

  1. 需求量大:业务稳步发展,业务需求不断叠加,甚至部分业务呈指数级增长,且业务方向涉及到一些业务规则、消息通知、协议文档、规则介绍等需求。

  2. 变更频繁:面对市场监管和法务的要求,以及新业务调整等因素的影响,会涉及到需求的频繁变更,像一些业务FAQ、产品介绍、协议文档、业务规则、系统更新日志等页面,需要做到快速响应和及时上线。

  3. 复杂度低:这些页面没有复杂的交互逻辑,如果能把这些简单的页面交给运营/产品去实现,开发人员就能有更多的时间去进行复杂功能的研发。

  4. 时效性高:临时性业务需求较多,且生命周期较短,具有定期下线和周期性上线等特点。

基于以上特点,为了提高研发效率,美团医药技术部开始构建了一个跨端一体化富文本管理平台,希望提供解决这一大类问题的产研方案。不过,部门最初的目标是开发一套提效工具,解决大量诸如帮助文档、协议页、消息通知、规则说明等静态页面的生产与发布问题,让产品和运营同学能够以所见即所得的方式自主完成静态页面制作与发布,进而缩短沟通成本和研发成本。

但是,随着越来越多业务部门开始咨询并使用这个平台,我们后续不断完善并扩充了很多的功能。经过多次版本的设计和迭代开发后,将该平台命名为Page-佩奇,并且注册成为美团内部的公共服务,开始为美团内部更多同学提供更好的使用体验。

本文将系统地介绍Page-佩奇平台的定位、设计思路、实现原理及取得成效。我们也希望这些实战经验与总结,能给更多同学带来一些启发和思考。

三、跨端一体化富文本管理解决方案

3.1 平台定位

我们希望将Page-佩奇打造成一款为产品、运营、开发等用户提供快速一站式发布网页的产研工作台,这是对该平台的一个定位。

  • 对产品运营而言,他们能够可视化地去创建或修改一些活动说明、协议类、消息类的文章,无需开发排期,省去向开发二次传递消息等繁琐的流程,也无需等待漫长的发布时间,从而达到灵活快速地进行可视化页面的发布与管理。

  • 对开发同学而言,他们能够在线编写代码,并实现秒级的发布上线,并且支持ES 6、JavaScript 、Less、CSS语法,我们还提供了基础的工具、图表库等,能够生成丰富多样的页面。帮助开发同学快速实现数据图表展示,设计特定样式,完成各种交互逻辑等需求。

  • 对项目管理方而言,他们能够清晰地看到整个需求流转状态和开发日志信息,为运营管理提供强大的“抓手”。

一般来讲,传统开发流程是这样的:首先产品提出需求,然后召集研发评审,最后研发同学开发并且部署上线;当需求上线之后,如果有问题需要反馈,产品再找研发同学进行沟通并修复,这种开发流程也是目前互联网公司比较常见的开发流程。

pic_b81cd143.png

图2 传统开发流程图

而美团Page-佩奇平台的开发流程是:首先产品同学提出需求,然后自己在Page平台进行编辑和发布上线,当需求上线之后有问题需要反馈,直接就能触达到产品同学,他们通常可自行进行修复。如果需求需要定制化,或者需要做一些复杂的逻辑处理,那么再让研发人员配合在平台上进行开发并发布上线。

pic_b3e5d331.png

图3 Page-佩奇平台开发流程图

简单来说,对那些功能相对简单、需求变动比较频繁的页面,如果用传统的开发流程将会增加产研沟通和研发排期成本,因此传统方案主要适用于功能复杂型的需求。而Page-佩奇平台开发流程,并不适合功能复杂型的需求,特别适用于功能相对简单、需求变动比较频繁的页面需求。

综上所述,可以看出这两种开发流程其实起到了一个互补的作用,如果一起使用,既可以减少工作量,又可以达到降本提效的目的。

3.2 设计思路

我们最初设计Page-佩奇平台的初心其实很简单,为了给产品和运营提供一个通过富文本编辑器快速制作并发布网页的工具。但是,在使用的过程中,很多缺陷也就慢慢地开始暴露,大致有下面这些问题:

  1. 简单的富文本编辑器满足不了想要的页面效果,怎么办?

  2. 如果能导入想要的模板,是否会更友好?

  3. 怎么查看这个页面的访问数据?如何能监控这个页面的性能问题?

  4. 发布的页面是否有存在安全风险?

于是,我们针对这些问题进行了一些思考和调研:

  • 当富文本编辑器满足不了想要实现的效果的时候,可以引入了WebIDE编辑器,可以让研发同学再二次编辑进行实现。

  • 一个系统想要让用户用得高效便捷,那么就要完善它的周边生态。就需要配备完善的模板素材和物料供用户灵活选择。

  • 如果用户想要了解页面的运行情况,那么页面运行的性能数据、访问的数据也是必不可少的。

  • 如果发布的内容存在不当言论,就会造成不可控的法律风险,所以内容风险审核也是必不可少的。

实现一个功能很容易,但是想要实现一个相对完善的功能,就必须好好下功夫,多思考和多调研。于是,围绕着这些问题,我们不断挖掘和延伸出了一系列功能:

  1. 富文本编辑:强大而简单的可视化编辑器,让一切操作变得简单、直观。产品同学可以通过编辑器自主创建、编辑网页,即使无程序开发经验也可以通过富文本编辑器随意操作,实现自己想要的效果,最终可以实现一键快速发布上线。

  2. WebIDE:定制化需求,比如,与客户端和后端进行一些通信和请求需求,以及针对产品创建的HTML进行二次加工需求,均可以基于WebIDE通过JavaScript代码实现。具备专业开发经验的同学也可以选择通过前端框架jQuery、Vue,Echarts或者工具库Lodash、Axios实现在线编辑代码。

  3. 页面管理:灵活方便地管理页面。大家可以对有权限的文档进行查看、编辑、授权、下线、版本对比、操作日志、回滚等操作,且提供便捷的文档搜索功能。

  4. 模板市场:丰富多样的网页模板,简易而又具备个性。模板市场提供丰富的页面模板,大家可选择使用自己的模板快速创建网页,且发布的每个页面又可以作为自己的模板,再基于这个模板,可随时添加个性化的操作。

  5. 物料平台:提供基础Utils、Echart、Vue、jQuery等物料,方便开发基于产品的页面进行代码的二次开发。

  6. 多平台跨端接入:高效快捷地接入业务系统。通过通信SDK,其他系统可以快速接入Page-佩奇平台。同时支持以HTTP、Thrift方式的开放API供大家选择,支持客户端、后端调用开放API。

  7. 内容风险审核:严谨高效的审核机制。接入美团内部的风险审核公共服务,针对发布的风险内容将快速审核,防止误操作造成不可控的法律风险。

  8. 数据大盘:提供页面的数据监测,帮助大家时刻掌握流量动向。接入美团内部一站式数据分析平台,帮助大家安全、快速、高效地掌握页面的各种监测数据。

  9. 权限管理:创建的每个页面都有相对独立的权限,只有经过授权的人才能查看和操作该页面。

  10. 业务监控:提供页面级别JavaScript错误和资源加载成功率等数据,方便开发排查和解决线上问题。

功能流程图如下所示:

pic_9d007bab.png

图4 Page-佩奇平台功能流程图

3.3 实现原理

3.3.1 基础服务

Page-佩奇平台的基础服务有四个部分,包括物料服务、编译服务、产品赋能、扩展服务。

pic_f0bf9912.png

图5 整体架构图

3.3.2 核心架构

pic_bbb3bb9a.png

图6 核心架构图

Page-佩奇平台核心架构主要包含页面基础配置层、页面组装层以及页面生成层。我们通过Vuex全局状态对数据进行维护。

  • 页面基础配置层主要提供生成页面的各种能力,包括富文本的各种操作能力、编辑源码(HTML、CSS、JavaScript)的能力、自定义域名配置、适配的容器(PC/H5)、发布环境等。

  • 页面组装层则会基于基础配置层所提供的的能力,实现页面的自由编辑,承载大量的交互逻辑,用户的所有操作都在这一层进行。

    • 业务PV和UV埋点,错误统计,访问成功率上报。

    • 自动适配PC和移动端样式。

    • 内网页面显示外网不可访问标签。

  • 页面生成层则需要根据组装后的配置进行解析和预处理、编译等操作,最终生成HTML、CSS、JavaScript渲染到网页当中。

3.3.3 关键流程

pic_b5adef78.png

图7 关键流程图

如上图7所示,平台的核心流程主要包含页面创建之后的页面预览、编译服务、生成页面。

  • 页面预览:创建、编辑之后的页面,将会根据内容进行页面重组,对样式和JavaScript进行预编译之后,对文本+JavaScript+CSS进行组装,生成HTML代码块,然后将代码块转换成Blob URL,最终以iframe的方式预览页面。

  • 编译服务:文件树状结构和代码发送请求到后端接口,基于Webpack将Less编译成CSS,ES 6语法编译成ES 5。通用物料使用CDN进行引入,不再进行二次编译。

  • 生成页面:当创建、编辑之后的页面进行发布时,服务端将会进行代码质量检测、内容安全审查、代码质量检测、单元测试、上传对象存储平台、同步CDN检测,最终生成页面链接进行访问。

3.3.4 多平台接入

Page-佩奇平台也可以作为一个完善的富文本编辑器供业务系统使用,支持内嵌到其他系统内。作为消息发布等功能承载,减少重复的开发工作,同时我们配备完善的SDK供大家选择使用。通过Page-SDK可以直接触发Page平台发布、管理等操作,具体的流程如下图所示:

pic_e3c8e777.png

图8 Page-SDK流程图

3.3.5 Open API

在使用Page-佩奇平台的时候,美团内部一些业务方提出想要通过Page-佩奇平台进行页面的发布,同时想要拿到发布的内容做一些自定义的处理。于是,我们提供了Open API开放能力,支持以HTTP和Thrift两种方式进行调用。下面主要讲一下Thrift API实现的思路,首先我们先了解下Thrift整体流程:

pic_a6286e54.png

图9 Thrift整体流程图

Thrift的主要使用过程如下:

  1. 服务端预先编写接口定义语言 IDL(Interface Definition Language)文件定义接口。

  2. 使用Thrift提供的编译器,基于IDL编译出服务语言对应的接口文件。

  3. 被调用服务完成服务注册,调用发起服务完成服务发现。

  4. 采用统一传输协议进行服务调用与数据传输。

下面具体讲讲,Node语言是如何实现和其他服务语言实现调用的。由于我们的服务使用的Node语言,因此我们的Node服务就充当了服务端的角色,而其他语言(Java等)调用就充当了客户端的角色。

pic_ca36c11d.png

图10 Thrift使用详细流程图

  • 生成文件:由服务端定义IDL接口描述文件,然后基于IDL文件转换为对应语言的代码文件,由于我们用的是Node语言,所以转换成JavaScript文件。

  • 服务端启动服务:引入生成的JavaScript文件,解析接口、处理接口、启动并监听服务。

  • 服务注册:通过服务器内置的“服务治理代理”,将服务注册到美团内部的服务注册路由中心(也就是命名服务),让服务可被调用方发现。

  • 数据传输:被调用时,根据“服务治理服务”协议序列化和反序列化,与其他服务进行数据传输。

目前,美团内部已经有相对成熟的NPM包服务,已经帮我们实现了服务注册、数据传输、服务发现和获取流程。客户端如果想调用我们所提供的的Open API开放能力,首先申请AppKey,然后选择使用Thrift方式或者HTTP的方式,按照所要求的参数进行请求调用即可。

3.4 方案实践

3.4.1 H5协议

能力:富文本编辑。

描述:提供富文本可视化编辑,产品和运营无需前端就可以发布和二次编辑页面。

场景:文本协议,消息通知,产品FAQ。

具体案例:

pic_f883a4d1.png

图11 H5静态文本协议案例

3.4.2 业务自定义渲染

能力:开放API(Thirft + HTTP)。

描述:提供开放API,支持业务自定义和样式渲染到业务系统,同时解决了iframe体验问题。

场景:客户端、后端、小程序的同学,可根据API渲染文案,实现动态化管理富文本信息。

具体案例:

小程序使用组件、Vue使用v-html指令实现动态化渲染商品选择说明。

{
   "code": 0,
   "data": {
     "tag": "苹果,标准",
     "title": "如何挑选苹果",
     "html": "<h1>如何挑选苹果</h1>><p>以下标准可供消费者参考</p><ul><li>酸甜</li><li>硬度</li></ul>",
     "css": "",
     "js": "",
     "file": {}
  },
   "msg": "success"
}

3.4.3 投放需求

能力:WebIDE代码编辑。

描述:开发基于WebIDE代码开发工作,基于渠道和环境修改下载链接,能够做到分钟级支撑。

场景:根据产品创建静态页面进行逻辑和样式开发。

具体案例:

var ua = window.navigator.userAgent
   var URL_MAP = {
       ios: 'https://apps.apple.com/cn/app/xxx',
       android: 'xxx.apk',
       ios_dpmerchant: 'itms-apps://itunes.apple.com/cn/app/xxx'
  }
   
   if (ua.match(/android/i)) location.href = URL_MAP.android
   if (ua.match(/(ipad|iphone|ipod).*os\s([\d_]+)/i)) {
       if (/xx\/com\.xxx\.xx\.mobile/.test(ua)) {
           location.href = URL_MAP.ios_dpmerchant
      } else {
           location.href = URL_MAP.ios
      }
  }

3.4.4 客户端通信中间页

能力:WebIDE代码编辑 + 物料平台。

描述:通过物料平台,引入公司客户端桥SDK,可以快速完成客户端通信需求。方便前端调试客户端基础桥功能。

场景:客户端跳转,通信中间页。

具体案例:

// 业务伪代码
   XXX.ready(() => {
       XXX.sendMessage({
          sign: true,
           params: {
               id: window.URL
          }
      }, () => {
           console.error('通信成功')
      }, () => {
           console.error('通信失败')
      })
  })

3.4.5 业务系统内嵌Page

能力:提供胶水层Page-SDK,连接业务系统和Page。

描述:业务系统与Page-佩奇平台可进行通信,业务系统可调用Page发布、预览、编辑等功能,Page可返回业务系统页面链接、内容、权限等信息。减少重复前后端工作,提升研发效率。

场景:前端富文本信息渲染,后端富文本信息管理后台。

具体案例:

pic_ec5b5f49.png

图12 业务系统内嵌Page案例

3.5 业务成绩

截止目前数据统计,Page-佩奇平台生成网页5000多个,编辑页面次数16000多次,累计页面访问PV超过8260万。现在,美团已经有十多个部门和三十多条业务线接入并使用了Page-佩奇平台。

pic_becbff8a.png

图13 Page-佩奇平台每日生成页面统计

四、总结与展望

富文本编辑器和WebIDE不仅是复杂的系统,而且还是比较热门的研究方向。特别是在和美团的基建结合之后,能够解决团队内部很多效率和质量问题。这套系统还提供了语法智能提示、Diff对比、前置检测、命令行调试等功能,不仅要关注业务发布出去页面的稳定性和质量,更要有内置的一系列研发插件,主动帮助研发提高代码质量,降低不必要的错误。

经过长期的技术和业务演进,Page-佩奇平台已经能够有效地帮助研发人员大幅提升开发效率,具备初级的Design To Code能力,但是仍有许多业务场景值得去我们探索。我们也期待优秀的你参与进来,一起共同建设。

  • WebIDE融合:完善基础设施建设和功能需求,更好地支持Vue、React、ES 6、TS、Less语法,预览模式采用浏览器编译,能有效地提高预览的速度,发布使用后端编译的模式。

  • 研发流程链路:针对代码进行有效评估,包括ESlint、代码重复率、智能提示是否可以三方库替代。出具开发代码质量、业务上线的质量报告。

  • 综合研发平台:减少团队同学了解整体基建的时间成本,内置了监控、性能、任务管理等功能,提升业务开发效率。建设自动化日报、周报系统,降低非开发工作量占比。

  • 物料开放能力:接入公共组件平台,沉淀更多的物料,快速满足产品更多样化的需求。

五、作者简介

高瞻、宇立、肖莹、浩畅,来自美团医药终端团队。王咏、陈文,来自美团闪购终端团队。
来源:https://blog.csdn.net/MeituanTech/article/details/121551030

收起阅读 »

Gradle 与 AGP 构建 API: 配置您的构建文件

欢迎阅读全新的 MAD Skills 系列 之 Gradle 及 Android Gradle plugin API 的第一篇文章。我们将在本文中了解 Android 构建系统的工作方式以及 Gradle 的基础知识。 我们将会从 Gradle 的构建阶段开始...
继续阅读 »

欢迎阅读全新的 MAD Skills 系列GradleAndroid Gradle plugin API 的第一篇文章。我们将在本文中了解 Android 构建系统的工作方式以及 Gradle 的基础知识。


我们将会从 Gradle 的构建阶段开始,讨论如何使用 AGP (Android Gradle Plugin) 的配置选项自定义您的构建,并讨论如何使您的构建保持高效。如果您更喜欢通过视频了解此内容,请在 此处 查看。


通过了解构建阶段的工作原理及配置 Android Gradle plugin 的配置方法,可以帮您基于项目的需求自定义构建。让我们回到 Android Studio,一起看看构建系统是如何工作的吧。


Gradle 简介


Gradle 是一个通用的自动化构建工具。当然,您可以使用 Gradle 来构建 Android 项目,但实际上您可以使用 Gradle 来构建任何类型的软件。


Gradle 支持单一或多项目构建。如果要将项目配置为使用 Gradle,您需要在项目文件夹中添加 build.gradle 文件。


在多项目层级结构中,根项目中会包含一个 settings.gradle 文件,其中列出了构建中包含的其他项目。Android 使用多项目构建来帮您模块化应用。


△ Android 项目结构与 build.gradle 及 settings.gradle 文件


△ Android 项目结构与 build.gradle 及 settings.gradle 文件


由于插件的存在,Gradle 可以处理不同类型的项目,比如 Android 或 Java。这些插件会包含预定义的功能,用于配置和构建特定类型的项目。


例如,为了构建 Android 项目,您需要使用 Android Gradle 插件配置您的 Gradle 构建文件。无论当前的 Android 项目是应用还是依赖库,Android Gradle 插件都知道如何对其进行构建和打包。


Task (任务)


Gradle 的构建流程围绕名为 Task (任务) 的工作单元展开。您可以通过终端查看 Task 列表,或通过启用 Android Studio Gradle 面板中的 Task 列表来查看任务。


△ Gradle Task 列表


△ Gradle Task 列表


这些 Task 可以接收输入、执行某些操作,并根据执行的操作产生输出。


Android Gradle Plugin 定义了自己的 Task,并且知道构建 Android 项目时,需要以何种顺序执行这些 Task。


Gradle 构建文件由许多不同的部分组成。Gradle 的配置语法被称为 Gradle DSL,其为开发者定义了配置插件的方式。Gradle 会解析 build.gradle 文件中的 android DSL 块并创建 AGP DSL 对象,例如 ApplicationExtensionBuildType


典型的 Android 项目会包含一个顶层 Gradle 构建文件。Android 项目中的每个模块又分别有一个 Gradle 构建文件。在示例项目中,我仅有一个应用模块。


在模块层的 build.gradle 文件中,我需要声明和应用构建项目所需的插件。为了让 Gradle 知道我正在构建 Android 项目,我需要应用 com.android.applicationcom.android.library 插件。这两个插件分别定义了如何配置和构建 Android 应用和依赖库。在本例中,我要构建的是 Android 应用项目,所以我需要应用 com.android.application 插件。由于我需要使用 Kotlin,所以在示例中也应用了 kotlin.android 插件。


plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}

Android Gradle Plugin 提供了它自己的 DSL,您可以用它配置 AGP,并使该配置在构建时应用于 Task。


想要配置 Android Gradle Plugin,您需要使用 android 块。在该代码块中,您可以为不同的构建类型 (如 debug 或 release) 定义 SDK 版本、工具版本、应用详情及其它一些配置。如需了解更多有关 gradle 如何使用这些信息来创建变体,以及您可以使用哪些其他选项,请参阅 构建文档:


android {
compileSdk 31

defaultConfig {
applicationId "com.example.myapp"
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}

在下一部分中,您可以定义依赖。Gradle 的依赖管理支持兼容 MavenIvy 的仓库,以及来自文件系统的本地二进制文件。


dependencies {

implementation 'androidx.core:core-ktx:1.7.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

}

构建阶段


Gradle 分三个阶段评估和运行构建,分别是 Initialization (初始化)、Configuration (配置) 和 Execution (执行),更多请参阅 Gradle 文档


在 Initialization (初始化) 阶段,Gradle 会决定构建中包含哪些项目,并会为每个项目创建 Project实例。为了决定构建中会包含哪些项目,Gradle 首先会寻找 settings.gradle 来决定此次为单项目构建还是多项目构建。


在 Configuration (配置) 阶段,Gradle 会评估构建项目中包含的所有构建脚本,随后应用插件、使用 DSL 配置构建,并在最后注册 Task,同时惰性注册它们的输入。


需要注意的是,无论您请求执行哪个 Task,配置阶段都会执行。为了保持您的构建简洁高效,请避免在配置阶段执行任何耗时操作。


最后,在 Execution (执行) 阶段,Gradle 会执行构建所需的 Task 集合。


下篇文章中,在编写我们自己的插件时,我们将深入剖析这些阶段。


Gradle DSL 支持使用 Groovy 与 Kotlin 脚本编写构建文件。到目前为止,我都在使用 Groovy DSL 脚本来配置此工程的构建。您可以在下面看到分别由 Kotlin 和 Groovy 编写的相同构建文件。注意 Kotlin 脚本文件名后缀为 ".kts"。


△ Kotlin 与 Groovy 脚本对比


△ Kotlin 与 Groovy 脚本对比


从 Groovy 迁移到 Kotlin 或其他配置脚本的方法,不会改变您执行 Task 的方式。


总结


以上便是本文的全部内容。Gradle 与 Android Gradle Plugin 有许多可以让您自定义构建的功能。在本文中,您已经了解了 Gradle Task、构建阶段、配置 AGP 以及使用 DSL 配置构建的基础知识。



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

换一个方式组织你的Axios代码?

自从Jquery被mvvm平替了之后,$.ajax 也被 axios 平替了,在使用这个方式之前,我想大部分的人也想到了去封装一个请求,然后每一次调用去做Get 、Post的请求服务,也有一些人习惯在vue里直接编写 this.$axios.get() ,萝卜...
继续阅读 »

自从Jquery被mvvm平替了之后,$.ajax 也被 axios 平替了,在使用这个方式之前,我想大部分的人也想到了去封装一个请求,然后每一次调用去做Get 、Post的请求服务,也有一些人习惯在vue里直接编写 this.$axios.get() ,萝卜青菜各有所爱,没有优劣之分。



灵感来源


d4axios的灵感来源于open-figen,现在功能还没有那么丰富,但是足以应付多大多数的场景,比如上传、下载、get、post等等请求,配合上ts,可以解决数据类型前后台一致性,数据类型的转换,保证了请求的便利性。让代码专注于数据处理,而非复制粘贴模版代码



别忘了 在 [ts | js]config.json 文件里开启对装饰器的支持。"experimentalDecorators":true



在项目中使用 d4axios



d4axios (Decorators for Axios) 是一款基于axios请求方式的装饰器方法组,可以快速地对服务进行管理和控制,增强前端下服务请求方式,有效降低服务的认知难度,并且在基于ts的情况下可以保证服务的前后台的数据接口一致性



npm i d4axios

yarn add d4axios

一、 引入配置信息


在这里提供了几种配置方式,可以采用原始的axios的配置方法,也可以采用 d4axios 提供的方法


// 在 vue3下我们建议使用 `createService` 
// 其他情况下使用 `serviceConfig`
import { createApp } from 'vue'
import {createService,serviceConfig} from 'd4axios'


createApp(App).use(createService({ ... /* 一些配置信息 */}))


1.1 提供的axios配置项


createServiceserviceConfig 使用的配置项是一样的,并且完全兼容axios的配置。在现有的项目中改造的话,可以使用:


// 可以直接使用由d4axios提供的服务
createService()

// 可直接传入axios的相关配置,由d4axios自动基于配置相关构建
createService({axios:{ baseURL:"domain.origin" }})

// 可直接传入已经配置好的 `axios` 实例对象
const axios = Axios.create({ /* 你的配置*/ });


createService({axios})

1.2 提供基于请求和相应数据的配置


createService({
beforeRequest(requestData){
// form对象会被转为JSON对象的浅拷贝,但是会在该方法执行完后重新转为form对象
// 你可在请求前追加一些补充的请求参数
// 比如对请求体进行签名等等
return requestData
},
beforeResponse(responseData){
// 默认情况下会返回 axios的response.data值,而不会返回response的完整对象
// 可以修改返回的响应结果
return responseData
}
})

1.3 提供快速的axios interceptors 配置


createService({
interceptors:{
request:{
use(AxiosRequestConfig){},
reject(){}
},
response{
use(AxiosResponse){},
reject(){}
}
}
})

配置完成后,会返回一个axios实例对象,可以继续对axios对象做更多的操作,可以绑定到vue.prototype.$axios下使用


Vue.prototype.$axios = serviceConfig({... /*一些配置*/})

二、创建请求服务


为了更好的组织业务逻辑,d4axios提供了一系列的组织方法,供挑选使用


import {Service,Prefix,Get,Post,Download,Upload,Param,After,Header} from 'd4axios'

@Service("UserService") // 需要提供一个服务名称,该名称将在使用服务的时候被用到
@Prefix("/user") // 可以给当前的服务添加一个请求前缀
export class UserService {

@Get("/:id") // 等同于 /user/:id
@After((respData)=>{
//在输出给最终结果前,可以对结果做一些简单处理
return respData
})
async getUserById(id:string){
// 异步请求需要增加 `async` 属性以便语法识别
// 支持restful的方式
}


@Post("/regist")
@Header({'plantform':'android'}) // 请求前追加一些header参数
async registUser(form:UserForm){
// 可以在请求的时候做一些参数修改
form.nickName = createNickName(form.nickName);

// return的值是最终请求的参数
return {
...form,
plant:"IOS"
};
}

@Download("/user/card") // 支持文件下载
async downloadCard(@Param("id") id:stirng){
// 当我们的参数较少并且不是一个key-value形式的值时
// 可以使用@Param辅助,确定传参名称
}

@Upload("/user/card") // 支持文件上传
async uploadCard(file:File){
return {file}
}

// 可以定义同步函数,直接做服务计算
someSyncFunc(){
return 1+1
}

// 我们还可以直接定义非请求函数
async someFunc(){
// 所有的当前函数都是可以直接调用的
return await this.getUserById(this.someSyncFunc());
}

}


三、使用服务



使用服务分为几种方式,第一种是在一个服务中调用另一个服务。第二种是在react或者vue中调用服务,对于这两种有不同的方法,也可以用相同的方法。



3.1 在 vue或者react中使用useService 导入服务


// 在 vue 或者 react中,可以直接使用 useService 导入一个服务对象
import {useService} from 'd4axios'
import SomeService from './some.service'

const someService = useService(SomeService)

复制代码

3.2 在一个服务中Use调用另一个服务


import {Use} from 'd4axios'
import SomeService from './some.service'
// 也可以直接像上面一样的导入进来是用
const someService = useService(SomeService)

@Service("MyService")
export class MyService {
@Use(SomeService) // use 导入服务
// 默认的属性名为小写驼峰
// 用 S<T> 包裹服务名称,这样可以得到相应的async方法的响应类型
someService !: S<SomeService>

async someMethod(){
// 就可以使用了,
await this.someService.something();
}
}

四、响应重写


默认情况下,d4axios支持async响应类型值,该值为


 export interface ResponseDataType<T> { }

在项目根路径下定义 d4axios.d.ts文件
然后文件内定义,通过重写该类型,可以得到响应的 response type类型,比如



export interface ResponseDataType<T> {
data : T;
msg:string ;
code:string ;
}

后即可以得到相关内容的提示信息


dataType.png


五、其他一些基于 Decorators 的操作


5.1 在使用装饰器的class上都可以使用 Use 导入服务 比如:


import {Component,Vue,} from 'vue-class-decorator'
import SomeService from './some.service'

@VueServiceBind(MyService,OtherService) // 只能在vue的这种形式下使用,可以绑定多个值
@Component
export default class MyVueComp extends Vue {

@Use(SomeService) // use 导入服务
// 默认的属性名为小写驼峰
// 用 S<T> 包裹服务名称,这样可以得到相应的async方法的响应类型
someService !: S<SomeService>

myService !: S<MyService>

otherService !: S<OtherService>
}

5.2 在一般的vue的服务下可以使用这种 mapService 形式


// 传统的模式下

import { mapService } from 'd4axios';
import MyService from './MyService.service'

export default {
computed:{
...mapService(MyService,OtherService)
},
created(){
this.myService.getName(10086);
}

作者:非思不可
链接:https://juejin.cn/post/7041930275458285582

收起阅读 »

如何在浏览器 console 控制台中播放视频?

如何在浏览器 console 控制台中播放视频? 要实现这个目标,主要涉及到这几个点: 如何获取和解析视频流? 如何在 console 里播放动态内容? 如何在 console 里播放彩色内容? 如何连接视频流和 console? 事实上最后的代码极其简单...
继续阅读 »

如何在浏览器 console 控制台中播放视频?


要实现这个目标,主要涉及到这几个点:



  1. 如何获取和解析视频流?

  2. 如何在 console 里播放动态内容?

  3. 如何在 console 里播放彩色内容?

  4. 如何连接视频流和 console?


事实上最后的代码极其简单,我们就一步一步简单讲一下


效果



测试地址:yu-tou.github.io/colors-web/…


如何获取和解析视频流?


这里我们用电脑摄像头捕获视频流,然后获取视频流每一帧的图像数据,作为下一步的输入。


// 捕捉电脑摄像头的视频流
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
// 创建一个 video 标签
const video = document.createElement("video");
document.body.appendChild(video);

video.onloadedmetadata = function (e) {
video.play(); // 等摄像头数据加载完成后,开始播放
};
// video 标签播放视频流
video.srcObject = mediaStream;

如何获取每一帧图像的数据?创建一个 canvas 画布,可以将 video 当前的内容绘制到画布上,然后通过 canvas 的方法即可拿到图像的像素数据。


const ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;

ctx.drawImage(video, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
// imageData 的结构是平铺的,需要自己去学习下

如何在 console 里播放动态内容?


视频每帧的图像内容我们已经可以拿到了,继续下一步,如果需要在 console 中完成播放视频,首先需要能够一帧一帧绘制内容,但是这个好像是不太现实的,console.log 只能输出文本。


回想远古时代,在终端里大家怎么播放视频的?没错,用字符画一帧一帧绘制,连起来不就是动态的视频了。


当然 chrome dev tool 里如果每一帧绘制后都调用 console.clear() 清空重绘,体验不是很好,闪烁会很严重,所以我们采用持续输出的方式绘制,当你停留在 console 的最后的时候,看起来也算是动态内容了。


如何在 console 里播放彩色内容?


console.log 支持部分 css 特性,可以为输出的字符串指定简单的样式,最基本的支持背景色、字体颜色、下划线等,甚至支持 background-image、padding 等特性,利用这些特性,甚至可以插入图片,但是这些特性在不同浏览器的 console 中或多或少有些兼容问题,不过要实现字体着色,或者输出色块(用背景色),问题不大。


我们在此使用 colors-web 来更方便地输出彩色内容到控制台。


这是一个非常方便的库,可以使用链式调用在控制台快速输出彩色内容,并且支持诸多特性,无需自己去了解,直接使用对应的方法即可。


如:


import { logger, colors } from "colors-web";
logger(
colors().red().fontsize(48).fontfamily("SignPainter").log("hello"),
colors().white.redBg("hello").linethrough(),
"world",
colors().white.padding(2, 5).underline().lightgrey("芋头")
);

相信我不解释,大家也基本理解这些用法,非常简单和自由,而且支持 typescript。


我们这里,用 colors-web 输出色块:


for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
if (i * width + j < data.length) {
const color = `rgb(${data[(i * width + j) * 4 + 0]},${data[(i * width + j) * 4 + 1]},${
data[(i * width + j) * 4 + 2]
})`;
colors()
.bg(color)
.color(color)
.fontfamily(/Chrome/.test(navigator.userAgent) ? "Courier" : "")
.log("╳");
}
}
}

最终逻辑


最终我将每一帧所有的像素值都转换成一个 colors 的实例,记录到数组之后,最终统一调用 logger 即可完成一帧的渲染。


const frameColors = [];
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
if (i * width + j < data.length) {
const color = `rgb(${data[(i * width + j) * 4 + 0]},${data[(i * width + j) * 4 + 1]},${
data[(i * width + j) * 4 + 2]
})`;
frameColors.push(
colors()
.bg(color)
.color(color)
.fontfamily(/Chrome/.test(navigator.userAgent) ? "Courier" : "")
.log("╳")
);
}
}
}
// 绘制,colors() 只是在准备数据结构,logger 才是真正的输出
logger(...frameColors);

大公告成啦!


作者:芋头君
链接:https://juejin.cn/post/7013620775143866376

收起阅读 »

某科技公司前端三面面经

okay, it's me again. 哈哈哈我怎么也没想到,我又会经历多一次三面,这次可以说是被狠狠的按在地上摩擦了,没办法,只能奉行一贯的“技术不够,吹牛来凑”原则 btw 应该看多点别人的面经,而不是自己写面经,当然自己写也可以当作一个很好的复盘 这次...
继续阅读 »

okay, it's me again.


哈哈哈我怎么也没想到,我又会经历多一次三面,这次可以说是被狠狠的按在地上摩擦了,没办法,只能奉行一贯的“技术不够,吹牛来凑”原则


btw 应该看多点别人的面经,而不是自己写面经,当然自己写也可以当作一个很好的复盘


这次是某家准备上市的公司,公司的技术部门也是挺强大的,所以也才会有三面吧可能


其实复盘过程中记的也不太清楚,只能说想起来一点写一点,这里建议将整个面试过程录音,以便做一次彻底的复盘,当然最好还是取得面试官同意才这么做


面试流程:boss直聘聊 -> 发邮件邀约面试 -> 一面技术面 -> 二面技术面 -> 三面HR面 -> 电话沟通薪资和入职事宜 -> offer


一面技术面




  1. 自我介绍




  2. 一个业务场景,PC端用vue做后台管理系统的时候,一般路由是动态生成的,前端的文件与路由是一一对应的,假如不小心删了一个文件,这个时候就会跳404页面,会有不好的用户体验,怎么做才能比较好的防止跳去404页面?




  3. 有一个页面,一个绝对够长的背景图,我们知道不给盒子设定高度的情况下默认是100%的高度,盒子高度会被内容所撑开。那么怎么做到第一屏完全显示背景图,第二屏也能继续显示呢?


    好,来看我的第一个错误回答🤣


    <style>
    * {
    margin: 0;
    padding: 0;
    }
    .container {
    width: 100%;
    height: 100vh;
    background-image: url('./assets/images/long.jpeg');
    }
    </style>

    <body>
    <div class="container">
    <p>1</p>
    这里复制出足够多的<p>1</p>就好,我就不贴出来重复代码占据太大篇幅了
    </div>
    </body>


    这是第一屏的效果,嗯很好完全没有问题! 但是当我们鼠标来到第二屏就哦豁了🙈




WechatIMG83.jpeg


然后我的第二个回答是:将图片绝对定位,这样图片就能适应不管多少屏了,但是图片绝对定位的话,没有内容撑开,那么第一屏根本都不会出现背景,所以这样也是不行的😅


答案:将 height: 100vh; 换成 min-height: 100vh;就可以了😂




  1. 我们都知道在谷歌浏览器里面字体的最低像素是 12px ,就算设置font-size: 8px;也会变成 12px ,我现在有一个需求需要 8px 的字体,怎么才能突破 12px 的限制?


    基本原理是使用css3的 transform: scale(); 属性


    需求是 8px 的字体,那我们就 font-size: 16px; transform: scale(0.5); 即可




  2. 讲一下 ES6 的新特性




  3. 说一些你经常用到的数组的方法




  4. 前端性能优化


    传送门:聊一聊前端性能优化




  5. 原型链


    传送门:继承与原型链


    传送门:JavaScript原型系列(三)Function、Object、null等等的关系和鸡蛋问题




  6. 假设在一个盒子里,里面所有小盒子的宽高都是相等的(PS技术不好,画的不相等),大盒子刚好放得下7个小盒子,使用css实现下面的布局




WechatIMG84.png




  1. 讲一下微信登录流程




  2. 怎么给每个请求加上 Authorization token ? (考察封装请求,axios 拦截器)




  3. 讲一下 vue 的双向数据绑定原理




  4. 移动端防止重复点击,防抖节流




  5. 怎么触发BFC,有什么应用场景?




  6. Promise有哪几种状态?




  7. 如果现在有一个任务,让你来做主力开发,架构已经搭好了,UI设计图也已经出完了,那你第一步会做什么?




  8. 后台管理系统怎么做权限分配?




  9. 怎么判断一个对象是否为空对象?




  10. 数字1-50的累加,不用 for 循环,用递归写


    因为我很抗拒当场写代码,然后满脑子都是1-50的累加为什么不用 for 循环,用 for 循环不是更快吗?为什么要用递归?但是面试官都把纸笔递过来了,没办法也是只能硬着头皮上了,但是这也是很简单的一道题,下面贴出当时手写的代码(是错的)


        // 这是错的这是错的这是错的
    function add(n) {
    let sum = 0;

    if (n > 0) {
    sum += add(n - 1);
    } else {
    return sum;
    }
    }

    // 这是根据上面改进之后的写法
    function add(n, sum) {
    if (n > 0) {
    return add(n - 1, (sum += n));
    } else {
    return sum;
    }
    }

    // 当然还有一种更为优雅与简便的写法
    function add(n) {
    return n === 0 ? 0 : n + add(n - 1);
    }

    // 想一行代码搞定的话就是
    const add = (n) => (n === 0 ? 0 : n + add(n - 1));



  11. 怎么解决 vuex 里的数据在页面刷新后丢失的问题?




  12. 说一下 vue 组件通信有几种方式(老生常谈的问题)




  13. 说一下 vue 和微信小程序各自的生命周期




  14. 看一下这个 ts 问题


        let num: string = '1';
    转一下数据类型转成 number



  15. 说一下 ts 总共有多少种数据类型




二面技术面




  1. 封装一个级联组件,讲一下思路




  2. 封装 v-model




  3. POST请求的 Content-Type 有多少种?




  4. css flex: 1; 是哪几个属性的组合写法




  5. vue provide/inject 的数据不会及时回流到父组件的问题(我记得没错的话好像是这么问的)




  6. 不用Promise的情况下,怎么实现一个Promise.all()方法




  7. [1, 2, 3].map((item, index) => parseInt(item, index))的结果


    这里考察了两点,1是parseInt()方法的第二个参数有什么作用,2是进制转换的相关知识




  8. cookie,sessionStorage,localStorage 3者之间有什么区别?




  9. http://www.xxx.com (a网站) 和 http://www.api.xxx.com (b网站) 两个网站,在b网站里登录授权拿到了 cookie ,怎么在a网站里拿到这个 cookie ?




  10. 说一下 forEach, map, for...in, for...of 的区别




  11. git fetch和git pull的区别(最后一道题)


    git pull:相当于是从远程获取最新版本并 merge 到本地


    git fetch:相当于是从远程获取最新版本到本地,不会自动 merge


    区别就是会不会自动 merge




三面HR面


这里就不展开了,HR面差不多都是那些东西


以上


其实一面二面还有很多问题都没有写出来,但是碍于当时也没有录音,只记得这么多


严格来讲,这并不太算是一篇面经,在上面很多都只是抛出了问题,因为技术的原因并没有做出相应的解答,还是有些遗憾的



作者:Lieo
链接:https://juejin.cn/post/7021394272519716872

收起阅读 »

亲身经历,大龄程序员找工作,为什么这么难!

背景 临近年底,公司还在招人,可筛选的人才真是越来越少,这可能是因为大家都在等年终奖吧。于是在简历筛选时,将学历和年龄都适当的放松了。正因为如此,面试了不少大龄的程序员。 网络上一直有讨论大龄程序员找工作困境的话题,对于我个人来说,是将信将疑的,但作为程序员对...
继续阅读 »

背景


临近年底,公司还在招人,可筛选的人才真是越来越少,这可能是因为大家都在等年终奖吧。于是在简历筛选时,将学历和年龄都适当的放松了。正因为如此,面试了不少大龄的程序员。


网络上一直有讨论大龄程序员找工作困境的话题,对于我个人来说,是将信将疑的,但作为程序员对自己职业生涯和未来的危机感还是有的。同时,作为技术部门领导,我是不介意年龄比我大,能力比我强的人加入的,只要能把事做好,这都不是事。


随着互联网的发展,大量程序员必然增多,都找不到工作是不可能的。而且中国的未来必然也会像发达国家一样,几十岁甚至一辈子都在写代码,也不是有可能的。


那么,我们担忧的是什么呢?又是什么影响了找工作呢?本文就通过自己亲身面试的几个典型案例来说说,可能样本有些小,仅供参考,不喜勿喷。


大厂与小厂招人的区别


前两天在朋友圈发了一条招人的感慨,关于大厂招人和小公司招人的区别。


大厂:有影响力,有钱,能够吸引了大量的应聘者。因此,也就有了筛选的资格,比如必须985名校毕业,必须35岁以下,不能5年3跳,必须这个……不能那个……当员工不合适时,绩效分给的低点或直接赔钱让其出局。


小公司:没有品牌,资金有限,每一分钱都要精打细算。招聘的人选有限,在这有限的选择范围内,还要考虑成本、能不能用、能不能留住等问题。能力太强,给不起钱,留不住;能力太弱,只会让项目越来越糟糕;所以,最好的选择只能是稍微高于现有团队能力,又不至于轻易跳槽的人。


有了上面的基本前提,再来看看大厂与小厂对待大龄程序员的差别。


对于35岁以后的程序员,有的大厂已经直接卡死,也就别死磕了。另外一些大厂还是开放的,但肯定是有一定的要求的,比如职位必须达到什么等级,能力必须达到什么要求。换句话说,如果你是牛人,其实35岁并不是什么问题,如果不是,那么这个选项几乎不存在。


所以,大厂的选择基本上等于没什么选择。再来看看小公司,小公司追求的核心是性价比,或者直白点说就是能干事且节省成本。另外就是能不能一职多能,能不能带新人,能不能加班……


个人看来,相对于大厂的要求,小公司的要求稍微努力一下还是可以满足的。对于加班这一项,不是所有的公司都有加班文化,也不是所有的公司常年需要加班。


招聘案例


挑选面试中几个比较典型的案例来聊聊,看看对你有什么启发。


案例一


84年的应聘者,自己在简历上填写的是应聘“中高级Java开发”。面试中,各项技能都平平,属于有功能开发经验,但没有深钻技术,没有考虑更好解决方案的状态。明确加班不能超过9点。也有写博客和公众号。9月份离职,目前暂未未找到工作。


就这位应聘者而言:第一,能接受员工比自己年龄大的领导不多,因为担心管不住;第二,技能没有亮点,就像他自己定位的那样,十多年工作经验,只是中高级开发;第三,加班这一项卡的太死,哪家公司上个线不到10点以后了,有突发需求不加个班?


案例二


86年的应聘者,别家公司裁员,推荐过来的简历。十来年工作经验,一直负责一个简单彩会不会是敏感词票业务的开发,中间有几年还没有项目履历。简历上写的功能还是:用户管理、权限管理、XX接口对接。推荐他的老板,给他的定位是中级开发。


这位应聘者,真的是将一年的代码写了十年。上家老板裁员选择了他,定位也只是中级,然后帮忙推荐了他到其他公司。这背后的故事,你仔细品,再仔细品。


案例三


87年的应聘者,学历一般,这两年的工作履历有点糟糕,跳槽的时机选择的也不好。长期从事支付行业,十来年的工作履历中,有七八年在做支付及相关的行业,其中在一家支付公司工作了四年。面试中,特意问了行业中的一些解决方案、技术实现,都没问题。基础知识稍微有点弱,但影响不大。面试过后,发了Offer,其中我还在老板面前帮忙争取了一把。


这位应聘者,虽然在学历,近两年的经历,基础知识上都略有不足。但他的行业经验丰富,给人一种踏实做事的感觉。整体能力恰好符合上面提到的小公司选择标准:比现有团队人能力强,有行业经验,薪资适中,稳定性较好。他的长板完全弥补了短板。


案例四


91年的应聘者,一家小有名气二线互联网公司出来。最近半年未工作,给出的原因是:家中有事,处理了半年,现在决定找工作了。聊半年前做过的项目,基本上记不起逻辑了;聊技术知识,也只能说个大概,但能感觉还是做过一些功能的,但没有深入思考过或没有做过复杂逻辑。


这位应聘者,不确定已经面试多久了,但应该不那么容易找工作。第一,半年未工作,即使有原因,也让人多少有些顾虑;第二,面试前完全没做功课,这不是能力问题,而是态度的问题了。


上面的案例,有成功的也有失败的。总结一下失败的原因,基本上有几点:能力与年龄不匹配、不能加班、家庭影响、没有特长……当然,你如果能看到其他的失败原因,那就更好了。


小结


上面只是最近一段时间面试的几个典型案例,至于你能从中获得什么,能不能提前行动,做好准备。那就是大家自己的事了。当然,还是那句话,样本有些小,但也能说明一些问题。仅个人观点,不抬杠,不喜勿喷。


我也曾为职场的未来担忧,也曾为年龄担忧,但始终未放弃的就是持续学习和思考。多位朋友都曾说过:无论你是否当上领导,是否还在写代码,技术能力都不能丢,你必须是团队中技术最牛的那一个。我一直在努力做到,你呢


作者:程序新视界
链接:https://juejin.cn/post/7043589223345029133

收起阅读 »

当 Adapter 遇上 Kotlin DSL,无比简单的调用方式

早在去年的时候我就提到过使用工厂的方式获取 Adapter 而不是为每个 Adapter 定义一个类文件。这样的好处是,对于不是那么复杂的 Adapter 可以节省大量的代码,提升开发效率和解放双手,同时更好的支持多类型布局效果。 1、Kotlin DSL 和...
继续阅读 »

早在去年的时候我就提到过使用工厂的方式获取 Adapter 而不是为每个 Adapter 定义一个类文件。这样的好处是,对于不是那么复杂的 Adapter 可以节省大量的代码,提升开发效率和解放双手,同时更好的支持多类型布局效果。


1、Kotlin DSL 和 Adapter 工厂方法


可以把 Kotlin DSL 当作构建者使用。这里有一篇不错的文章,想了解的可以阅读下,



http://www.ximedes.com/2020-04-21/…



Kotlin DSL 是拓展函数的延申,比如我们常用的 with 等函数就是函数的拓展,


public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}

这里是泛型 T 的拓展。这里的 T 可以类比到 Java 构建者模式中的 Builder,通过方法接收外部参数之后调用 build() 方法创建一个最终的对象即可。


对于 Adapter 工厂方法,之前我是通过如下方式使用的,


fun <T> getAdapter(
@LayoutRes itemLayout:Int,
converter: (helper: BaseViewHolder, item: T) -> Unit,
data: List<T>
): Adapter<T> = Adapter(itemLayout, converter, data)

class Adapter<T>(
@LayoutRes private val layout: Int,
private val converter: (helper: BaseViewHolder, item: T) -> Unit,
val list: List<T>
): BaseQuickAdapter<T, BaseViewHolder>(layout, list) {
override fun convert(helper: BaseViewHolder, item: T) {
converter(helper, item)
}
}

也就是每次想要得到 Adapter 的时候只要调用 getAdapter() 方法即可。这种封装方式比较简陋,支持的功能有限。后来慢慢采用了 Kotlin DSL 之后,我封装了 Kotlin DSL 风格的工厂方法。采用 Kotlin DSL 风格之后更加优雅和方便快捷,同时更好的支持多类型布局效果。


2、使用


2.1 引入依赖


首先,该项目依赖于 BRVAH,所以,你需要引入该库之后才可以使用。BRVAH 可以说是目前开源的最好用的 Adapter,我们没必要再另起炉灶自己再造轮子。这个框架设计最好地方在于通过 SpareArray 收集了 ViewHolder 控件,从而避免了自定义 ViewHolder,这是我们框架设计的基础思想。


该项目已经上传到了 MavenCentral,你需要先在项目中引入该仓库,


allprojects {
repositories {
mavenCentral()
}
}

然后在项目中添加如下依赖,


implementation "com.github.Shouheng88:xadapter:${latest_version}"

2.2 使用 Adapter 工厂方法


使用 xAdapter 之后,当你需要定义一个 Adapter 的时候,你无需单独创建一个类文件,只需要通过 createAdapter() 方法获取一个 Adapter,


adapter = createAdapter {
withType(Item::class.java, R.layout.item_eyepetizer_home) {
// Bind data with viewholder.
onBind { helper, item ->
helper.setText(R.id.tv_title, item.data.title)
helper.setText(R.id.tv_sub_title, item.data.author?.name + " | " + item.data.category)
helper.loadCover(requireContext(), R.id.iv_cover, item.data.cover?.homepage, R.drawable.recommend_summary_card_bg_unlike)
helper.loadRoundImage(requireContext(), R.id.iv_author, item.data.author?.icon, R.mipmap.eyepetizer, 20f.dp2px())
}
// Item level click and long click events.
onItemClick { _, _, position ->
adapter?.getItem(position)?.let {
toast("Clicked item: " + it.data.title)
}
}
}
}

在这种新的调用方式中,你需要通过 withType() 方法指定数据类型及其对应的布局文件,然后在 onBind() 方法中即可实现数据到 ViewHolder 的绑定操作。这里的 onBind() 方法的使用与 BRVAH 中的 convert() 方法使用一致,可以通过阅读该库了解如何使用。总之,xAapter 在 BRVAH 的基础上做了二次封装,可以说,比简单更简单。


xAdapter 支持为每个 ViewHolder 绑定点击和长按事件,同时也支持为 ViewHolder 上的某个单独的 View 添加点击和长按事件。使用方式如上所示,只需要添加 onItemClick() 方法并实现自己的逻辑即可。其他的点击事件可以参考项目的示例代码。


效果,





2.3 使用多类型 Adapter


多类型 Adapter 的使用方式非常简单,类似于上面的调用方式,只需要在 createAdapter() 内再添加一个 withType() 方法即可。下面是一个写起来可能相当复杂的 Adapter,但是采用了 xAdpater 的调用方式之后,一切变得非常简单,


private fun createAdapter() {
adapter = createAdapter {
withType(MultiTypeDataGridStyle::class.java, R.layout.item_list) {
onBind { helper, item ->
val rv = helper.getView<RecyclerView>(R.id.rv)
rv.layoutManager = GridLayoutManager(context, 3)
val adapter = createSubAdapter(R.layout.item_home_page_data_module_1, 1)
rv.adapter = adapter
adapter.setNewData(item.items)
}
}
withType(MultiTypeDataListStyle1::class.java, R.layout.item_home_page_data_module_2) {
onBind { helper, item ->
converter.invoke(helper, item)
}
onItemClick { _, _, position ->
(adapter?.getItem(position) as? MultiTypeDataListStyle1)?.let {
toast("Clicked style[2] item: " + it.item.data.title)
}
}
}
withType(MultiTypeDataListStyle2::class.java, R.layout.item_list) {
onBind { helper, item ->
val rv = helper.getView<RecyclerView>(R.id.rv)
rv.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
val adapter = createSubAdapter(R.layout.item_home_page_data_module_4, 3)
rv.adapter = adapter
adapter.setNewData(item.items)
}
}
withType(MultiTypeDataListStyle3::class.java, R.layout.item_home_page_data_module_3) {
onBind { helper, item ->
converter.invoke(helper, item)
}
onItemClick { _, _, position ->
(adapter?.getItem(position) as? MultiTypeDataListStyle3)?.let {
toast("Clicked style[4] item: " + it.item.data.title)
}
}
}
}
}

xAdapter 对多类型布局方式的支持是在 BRVAH 之上进行的改造,在这种封装方式中,数据类无需实现任何类和接口。Adpater 内部通过 Class 区分各个 ViewHolder.


效果,





总结


相对于为各种类型的数据定义 Adapter 的使用方式,以上封装方式的优势是:



  1. 借助 BRVAH 的优势,封装了大量的方法,进一步简化了 Adapter 的使用;

  2. 通过工厂和 DSL 封装,简化了调用 Adapter 的方式,你无需为数据类型定义 Adapter 文件,减少了项目中需要维护的代码和类文件数量;

  3. 通过以上封装,使用 Adapter 更加简洁,节省了大量的代码,提升开发效率和解放双手;

  4. 自由地在单一类型布局和多类型布局之间进行切换,但是少了没必要的工厂方法。


当有更加简洁的使用方式的时候,继续采用复杂的调用方式无异于抱残守缺,对于程序员而言,做这种重复而没有太大价值的工作,付出再多的汗水都不值得同情。以上是部分功能和代码的展示,可以通过阅读源码了解更多。后续我参考其他优秀的库的设计思想,支持更多 Adapter 特性的封装来实现快速调用。


项目已开源,感兴趣的可以直接阅读项目源码,源码地址:github.com/Shouheng88/…


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

我用Flutter写了一个上班摸鱼应用

网上最近看到了个摸鱼应用,还挺好玩的。我打算自己用flutter写了一个之前我有用flutter制作过mobile应用,但是没有在desktop尝试过;毕竟2.0大更新,我这里就在这试手一下,并说说flutter的体验.当前flutter环境 2.8增加flu...
继续阅读 »

网上最近看到了个摸鱼应用,还挺好玩的。

moyu.jpeg

我打算自己用flutter写了一个

之前我有用flutter制作过mobile应用,但是没有在desktop尝试过;毕竟2.0大更新,我这里就在这试手一下,并说说flutter的体验.

当前flutter环境 2.8

截屏2021-12-18 上午9.59.29.png

增加flutter desktop支持 (默认项目之存在ios,android项目包)

flutter config --enable-<platform>-desktop

我这里是mac,因此platform=macos,详细看flutter官网

代码十分简单,UI部分就不讲了

在摸鱼界面,我是用了 Bloc 做倒计时计算逻辑,默认摸鱼时长15分钟

 MoYuBloc() : super(MoyuInit()) {
on(_handleMoyuStart);
on(_handleUpdateProgress);
on(_handleMoyuEnd);
}

摸鱼开始事件处理

// handle moyu start action
FutureOr<void> _handleMoyuStart(
MoyuStarted event, Emitter<MoyuState> emit) async {
if (_timer != null && _timer!.isActive) {
_timer?.cancel();
}

final totalTime = event.time;
int progressTime = state is MoyuIng ? (state as MoyuIng).progressTime : 0;

_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
add(MoyuProgressUpdated(totalTime, ++progressTime));

if (progressTime >= totalTime) {
timer.cancel();
add(MoyuEnded());
}
});
emit(MoyuIng(progress: 0, progressTime: 0));
}

摸鱼进度更新

// handle clock update
FutureOr<void> _handleUpdateProgress(
MoyuProgressUpdated event, Emitter<MoyuState> emit) async {
final totalTime = event.totalTime;
final progressTime = event.progressTime;
emit(
MoyuIng(progress: progressTime / totalTime, progressTime: progressTime),
);
}

摸鱼结束,释放结束事件

// handle clock end
FutureOr<void> _handleMoyuEnd(
MoyuEnded event, Emitter<MoyuState> emit) async {
emit(MoyuFinish());
}
总结3个event (摸鱼开始,进程更新,摸鱼结束)

abstract class MoyuEvent {}

class MoyuStarted extends MoyuEvent {
final int time;
final System os;

MoyuStarted({required this.time, required this.os});
}

class MoyuProgressUpdated extends MoyuEvent {
final int totalTime;
final int progressTime;

MoyuProgressUpdated(this.totalTime, this.progressTime);
}

class MoyuEnded extends MoyuEvent {
MoyuEnded();
}

其中3个state (摸鱼初始,正在摸鱼,摸鱼结束)

abstract class MoyuState {}

class MoyuInit extends MoyuState {}

class MoyuIng extends MoyuState {
final double progress;
final int progressTime;

MoyuIng({required this.progress, required this.progressTime});
}

class MoyuFinish extends MoyuState {}

启动摸鱼使用, 记录总时长和消耗时间,计算进度百分比,更新UI进度条

下面是界面更新逻辑

BlocConsumer<MoYuBloc, MoyuState>(
builder: (context, state) {
if (state is MoyuIng) {
final progress = state.progress;

return _moyuIngView(progress);
} else if (state is MoyuFinish) {
return _replayView();
}
return const SizedBox();
},
listener: (context, state) {},
listenWhen: (pre, cur) => pre != cur,
),

很简单 最重要的是进度状态,其次结束后是否重新摸鱼按钮

构建运行flutter应用

flutter run -d macos 

最后结果展示

windows_update.png

mac_update.png

总结下flutter desktop使用

  1. 简单上手,按着官网走基本没问题,基本上没踩上什么雷,可能项目比较简单
  2. 构建流程简单,hot reload强大
  3. 性能强大,启动速度很快,并且界面无顿挫感

比较遗憾的事desktop电脑构建系统独立,mac环境下无法构建windows应用,有点小遗憾.

项目完全开源 可以前往GitHub查看 不要忘点个star😊

github.com/lau1944/moy…


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

收起阅读 »

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

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

前言


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


motion_toast 介绍


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



  • 可以通过动画图标实现动效;

  • 内置了成功、警告、错误、提醒和删除类型;

  • 支持自定义;

  • 支持不同的主题色;

  • 支持 null safety;

  • 心跳动画效果;

  • 完全自定义的文本内容;

  • 内置动画效果;

  • 支持自定义布局(LTR 和 RTL);

  • 自定义持续时长;

  • 自定义展现位置(居中,底部或顶部);

  • 支持长文本显示;

  • 自定义背景样式;

  • 自定义消失形式。


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


示例


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


最简单用法


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


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

其他内置的提醒


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


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

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

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

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


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

自定义 toast


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


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

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



  • icon:图标,IconData 类,可以使用系统字体图标;

  • primaryColor:主颜色,也就是大的背景底色;

  • secondaryColor:辅助色,也就是图标和旁边的竖条的颜色;

  • descriptionStyle:toast 文字的字体样式;

  • title:标题文字;

  • titleStyle:标题文字样式;

  • toastDuration:显示时长;

  • backgroundType:背景类型,枚举值,共三个可选值,transparentsolidlighter,默认是 lighterlighter其实就是加了一层白色底色,然后再将原先的背景色(主色调)加上一定的透明度叠加到上面,所以看起来会泛白。

  • onClose:关闭时回调,可以用于出现多个错误时依次展示,或者是关闭后触发某些动作,如返回上一页。


总结


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


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

Android 常见内存泄漏总结、避免踩坑、提供解决方案

对常见的内存泄漏进行一波总结,希望可以帮到大家。静态实例持有非静态内部类描述🤦♀️非静态内部类会持有外部类的实例,所以如果非静态内部类的实例是静态的话,那么它的生命周期就是整个APP的生命周期,而它则会一直持有外部类的引用,阻止外部类实例被系统回收。举个例子🌰...
继续阅读 »



对常见的内存泄漏进行一波总结,希望可以帮到大家。

静态实例持有非静态内部类

描述🤦♀️

非静态内部类会持有外部类的实例,所以如果非静态内部类的实例是静态的话,那么它的生命周期就是整个APP的生命周期,而它则会一直持有外部类的引用,阻止外部类实例被系统回收。

举个例子🌰:

public class TestActivity extends AppCompatActivity {

   static InnerClass innerClass;

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);
       setContentView(R.layout.item_layout);
       findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {

           @Override
           public void onClick(View v) {
               innerClass = new InnerClass();
          }
      });
  }

   class InnerClass{
 
    }
}

此时点击button时,会创建InnerClass实例并且赋值给innerClass。因为innerClassstatic修饰,所以InnerClass实例的生命周期会和应用程序一样长,但是它会持有TestActivity的实例,所以就会导致如果系统需要回收不了TestActivity的实例。造成内存泄漏😮。

解决办法🙆♀️

  • 将非静态内部类替换成静态内部类,因为静态内部类不会持有外部类的引用

  • 一定要用非静态内部类的话,要保证内部类的生命周期短于外部类

耗时任务相关的匿名内部类/非静态内部类

描述🤦♀️

这个和上一个类似,非静态内部类持有外部类的实例大家都知道了,这里不在叙述了;匿名内部类也会持有外部类的实例,而且匿名内部类会结合线程使用得多,这里就拉出来讲一下。

同理因为匿名内部类会持有外部类的实例,比如线程的Runable如果在里面做了耗时任务,在外部类对象需要回收的时候,但是线程任务没有执行完,那么就会因为匿名内部类持有外部类的引用,进而阻止系统回收外部类对象了。

简单举个例子

public class TestActivity extends AppCompatActivity {

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);
       setContentView(R.layout.item_layout);
       findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {

           @Override
           public void onClick(View v) {
               ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.MICROSECONDS, new SynchronousQueue(), new ThreadFactory() {
                   @Override
                   public Thread newThread(Runnable r) {
                       return new Thread(new ThreadGroup("test-thread"),r);
                  }
              });
               threadPoolExecutor.submit(new Runnable() {
                   @Override
                   public void run() {
                       while (true){
                           Log.d("TAG", "run: test");
                      }
                  }
              });
          }
      });
  }
}

点击button执行线程任务,提交了一个runable进去,因为里面死循环永远不会结束。所以匿名内部类会一直持有TestActivitty对象。不会被系统回收掉😮。因为匿名内部类这玩意进场使用,所以还是需要注意的!!!🤦♀️

解决方案🙆♀️

  • 将匿名内部类/非静态内部类替换成静态内部类,因为静态内部类不会持有外部类的引用

  • 一定要用匿名内部类/非静态内部类的话,要保证内部类的生命周期短于外部类

Handle内存泄漏

描述🤦♀️

这个就有点老生常谈了😂,但还是的说一下。

Handler发送的Message会存储在MessageQueue里面,但是他们不一定马上就被处理了。

另外我们知道Message的Target会持有记录当前的Handler对象,用于进行消息分发。所以如果Message不被及时处理,那么Handler就无法被回收。

那么如果此时Handler是非静态的,则Handler也会导致引用它的Activity不能被回收😮。

举个例子🌰

public class TestActivity extends AppCompatActivity {

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);
       setContentView(R.layout.item_layout);
       Handler mHandler = new Handler(){

           @Override
           public void handleMessage(Message msg) {
               super.handleMessage(msg);
          }
      };
       findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {

           @Override
           public void onClick(View v) {
   
               mHandler.sendMessageDelayed(new Message(),100000);
               finish();
          }
      });
  }
}

当点击button时,会finish当前activity。但是因为消息没有被及时处理,间接引用了Handler对象,Handler又是匿名内部类实例,持有了activity对象。所以导致内存泄漏🤦♀️。

解决方案🙆♀️

  • 使用静态Handler内部类,handler的持有者用弱引用。

  • 在onDestroy中将未执行的消息和Callbacks清除。

    if (mHandler != null) {
        mHandler.removeCallbacksAndMessages(null);
    }

Context被长期持有

描述🤦♀️

这个也很简单,比如你把Activity的Context传给了一个长期存在的对象,那其实activity的context就是它自身,那么因为被持有就回收不了。造成内存泄漏

解决方案🙆♀️

  • 对于不是必须使用Activity的Context的情况(Dialog的Context必须使用Activity的Context),可以考虑使用Application来代替Activity的Context,因为一般使用Context无非时获取一些资源而已。

  • 一定要传入Activity的Context的话,一定要注意生命周期,不可以被长期引用。

View被静态修饰

描述🤦♀️

这个。。。。我估计没有多少人这么使用吧。如果View被静态修饰的话,因为View会持有Context,所以就会导致当前Activity不会被回收。🤦♀️

举个例子🌰

public class TestActivity extends AppCompatActivity {

   static View button;

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);
       setContentView(R.layout.item_layout);
       button = findViewById(R.id.button);
  }
}

解决方案🙆♀️

  • 在onDestory中将view置null。

大对象/监听器释放

描述🤦♀️

大对象比如Bitmap

Bitmap对象一般比较大,而且好多操作都需要变换产生新的对象。所以需要注意一定要尽快释放临时的Bitmap对象用于节省内存。尽量避免被静态修饰或者其他长生命周期引用。

监听器的释放

很多服务需要register和unregister监听器,需要在合适的时候及时的unregister这些监听器否则容易产生内存泄漏。

解决方案🙆♀️

  • 大对象及时释放

  • 监听器在合适的时候进行释放

资源对象注意关闭

描述🤦♀️

资源对象比如File、Cursor等,如果不进行正常关闭,会造成内存泄漏。

解决方案🙆♀️

  • 通常使用异常代码块捕获,在finally语句中进行关闭,防止出现异常资源没有被正常释放问题。

集合对象

描述🤦♀️

注意一些生命周期很长的集合,比如被static修饰的集合,它的生命周期会时APP的生命周期,那么它里面维持的对象,如果在没用之后要即使清理掉,否则就会造成内存泄漏。

举个例子🌰

public class TestActivity extends AppCompatActivity {

   static List<View> vies;

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);
       setContentView(R.layout.item_layout);
       vies.add(findViewById(R.id.button));
  }
}

view被加入集合,因为集合生命周期为APP的生命周期,所以View、Activity也回收不了,内存泄漏。

解决方案🙆♀️

  • 这个完全需要自己注意,对于长生命周期集合内部对象的管理。

————————————————
作者:pumpkin的玄学
来源:https://blog.csdn.net/weixin_44235109/article/details/122029725

收起阅读 »

JavaScript函数封装随机颜色验证码

数字或者字母或者数字字母混合的n位验证码带随机的颜色。下面是完整的代码,需要的自取哈!function verify(a = 6,b = "num"){ //定义三个随机验证码验证码库 var num ="0123456789" var str ="ab...
继续阅读 »

数字或者字母或者数字字母混合的n位验证码带随机的颜色。下面是完整的代码,需要的自取哈!

function verify(a = 6,b = "num"){
//定义三个随机验证码验证码库
var num ="0123456789"
var str ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNIPQRSTUVWXYZ"
var mixin = num +str;
 
//定义一个空字符串用来存放验证码
var verify=""
if(a == undefined || b == undefined){
  //验证输入是否合法 不通过就抛出一个异常
  throw new Error("参数异常");
}else{
    if(a ==""||b==""){
      //判断用户是否没有输入
      throw new Error("参数非法.");
    }else{
      //检测输入的类型来判断是否进入
      var typea = typeof(a);
      var typeb = typeof(b);
      if(typea =="number" && typeb =="string"){
          if(b == "num"){
                 
              //定义一个循环来接收验证码   纯数字验证码
              for(var i=0;i<a;i++){
                    //定义一个变量来存储颜色的随机值
                    var r1 = Math.random()*255;
                    var g1 = Math.random()*255;
                    var b1 = Math.random()*255;

                    //确定随机索引
                    var index = Math.floor(Math.random()*(num.length-1))
                    //确定随机的验证码
                    var char = num[index];
                    //给随机的验证码加颜色
                    verify += `<span style ='color:rgb(${r1},${g1},${b1})'>${char}</span>`
                }
                //返回到数组本身
              return verify;
          }else if(b =="str"){
                for(var i=0;i<a;i++){
                  //纯字母的验证码
                  var r1 = Math.random()*255;
                  var g1 = Math.random()*255;
                  var b1 = Math.random()*255;

                  var index = Math.floor(Math.random()*(str.length-1));
                  var char = str[index];

                  verify += `<span style ='color:rgb(${r1},${g1},${b1})'>${char}</span>`
                }
                return verify;  
          }else if(b == "mixin"){
                // 混合型的验证码
              for(var i=0;i<a;i++){
                  var r1 = Math.random()*255;
                  var g1 = Math.random()*255;
                  var b1 = Math.random()*255;

                  var index = Math.floor(Math.random()*(mixin.length-1));
                  var char = mixin[index];

                  verify += `<span style ='color:rgb(${r1},${g1},${b1})'>${char}</span>`
              }
              return verify;
          }else{
              //验证没通过抛出一个异常
              throw new Error("输入类型非法.")
          }
       
      }else{
          //验证没通过抛出一个异常
          throw new Error("输入类型非法.")
      }
    }
}
}

下面我们来调用函数试试看

  //第一个值为用户输入的长度,第二个为类型! 
var arr = verify(8,"mixin");
    document.write(arr)

上面就是结果啦!

这个记录下来为了方便以后使用的方便,也希望大佬们多多交流,多多留言,指出我的不足的之处啦!

有需要的小伙伴可以研究研究啦!!
————————————————
作者:土豆切成丝
来源:https://blog.csdn.net/A20201130/article/details/122030872

收起阅读 »

localhost、127.0.0.1和0.0.0.0和本机IP的区别

localhostlocalhost其实是域名,一般windows系统默认将localhost指向127.0.0.1,但是localhost并不等于127.0.0.1,localhost指向的IP地址是可以配置的 127.0.0.1首先我们要先知道一...
继续阅读 »
localhost
localhost其实是域名,一般windows系统默认将localhost指向127.0.0.1,但是localhost并不等于127.0.0.1,localhost指向的IP地址是可以配置的
 
127.0.0.1
首先我们要先知道一个概念,凡是以127开头的IP地址,都是回环地址(Loop back address),其所在的回环接口一般被理解为虚拟网卡,并不是真正的路由器接口。
所谓的回环地址,通俗的讲,就是我们在主机上发送给127开头的IP地址的数据包会被发送的主机自己接收,根本传不出去,外部设备也无法通过回环地址访问到本机。
 
小说明:正常的数据包会从IP层进入链路层,然后发送到网络上;而给回环地址发送数据包,数据包会直接被发送主机的IP层获取,后面就没有链路层他们啥事了。
而127.0.0.1作为{127}集合中的一员,当然也是个回环地址。只不过127.0.0.1经常被默认配置为localhost的IP地址。
一般会通过ping 127.0.0.1来测试某台机器上的网络设备是否工作正常。
 
0.0.0.0
首先,0.0.0.0是不能被ping通的。在服务器中,0.0.0.0并不是一个真实的的IP地址,它表示本机中所有的IPV4地址。监听0.0.0.0的端口,就是监听本机中所有IP的端口。
 
本机IP
本机IP通常仅指在同一个局域网内,能同时被外部设备访问和本机访问的那些IP地址(可能不止一个)。像127.0.0.1这种一般是不被当作本机IP的。本机IP是与具体的网络接口绑定的,比如以太网卡、无线网卡或者PPP/PPPoE拨号网络的虚拟网卡,想要正常工作都要绑定一个地址,否则其他设备就不知道如何访问它。
 
 

localhost
首先 localhost 是一个域名,在过去它指向 127.0.0.1 这个IP地址。在操作系统支持 ipv6 后,它同时还指向ipv6 的地址 [::1] 
在 Windows 中,这个域名是预定义的,从 hosts 文件中可以看出:
# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost
 
而在 Linux 中,其定义位于 /etc/hosts 中:
127.0.0.1 localhost
 
注意这个值是可修改的,比如把它改成
192.068.206.1 localhost
 
然后再去 ping localhost,提示就变成了
PING localhost (192.168.206.1) 56(84) bytes of data.
 
127.0.0.1
127.0.0.1 这个地址通常分配给 loopback 接口。loopback 是一个特殊的网络接口(可理解成虚拟网卡),用于本机中各个应用之间的网络交互。只要操作系统的网络组件是正常的,loopback 就能工作。Windows 中看不到这个接口,Linux中这个接口叫 lo:
#ifconfig
eth0 Link encap:Ethernet hwaddr 00:00:00:00:00:00
  inet addr :192.168.0.1 Bcase:192.168.0.255 Mask:255.255.255.0
  ......
lo     Link encap:Local Loopback
  inetaddr: 127.0.0.1 Mask: 255.0.0.0
       ......
 
可以看出 lo 接口的地址是 127.0.0.1。事实上整个 127.* 网段都算能够使用,比如你 ping 127.0.0.2 也是通的。 
但是使用127.0.0.1作为loopback接口的默认地址只是一个惯例,比如下面这样:
#ifconfig lo 192.168.128.1
#ping localhost  #糟糕,ping不通了
#ping 192.128.128.1 # 可以通
#ifconfig lo
lo  Link encap:Local Loopback
  inetaddr: 192.168.128.1 Mask: 255.255.255.0
     ......
 
如果随便改这些配置,可能导致很多只认 127.0.0.1 的软件挂掉。
 
本机IP
确切地说,“本机地址”并不是一个规范的名词。通常情况下,指的是“本机物理网卡所绑定的网络协议地址”。由于目前常用网络协议只剩下了IPV4,IPX/Apple Tak消失了,IPV6还没普及,所以通常仅指IP地址甚至ipv4地址。一般情况下,并不会把 127.0.0.1当作本机地址——因为没必要特别说明,大家都知道。 
本机地址是与具体的网络接口绑定的。比如以太网卡、无线网卡或者PPP/PPPoE拨号网络的虚拟网卡,想要正常工作都要绑定一个地址,否则其他设备就不知道如何访问它。
● localhost 是个域名,不是地址,它可以被配置为任意的 IP 地址,不过通常情况下都指向 127.0.0.1(ipv4)和 ::1 
● 整个127.* 网段通常被用作 loopback 网络接口的默认地址,按惯例通常设置为 127.0.0.1。这个地址在其他计算机上不能访问,就算你想访问,访问的也是自己,因为每台带有TCP/IP协议栈的设备基本上都有 localhost/127.0.0.1。 
● 本机地址通常指的是绑定在物理或虚拟网络接口上的IP地址,可供其他设备访问到。 
● 最后,从开发度来看 
○ localhost是个域名,性质跟 “www.baidu.com” 差不多。不能直接绑定套接字,必须先gethostbyname转成IP才能绑定。 
○ 127.0.0.1 是绑定在 loopback 接口上的地址,如果服务端套接字绑定在它上面,你的客户端程序就只能在本机访问

原文链接:https://www.cnblogs.com/absoluteli/p/13958072.html
收起阅读 »

Android 使用 Span 打造丰富多彩的文本

1.引言 在开发过程中经常需要使用文本,有时候需要对一段文字中的部分文字进行特殊的处理,如改变其中部分文字的大小、颜色、加下划线等,这个时候使用Span就能方便地解决这些问题。本文将主要介绍SpannableStringBuilder和各种Span的使用。 2...
继续阅读 »

1.引言


在开发过程中经常需要使用文本,有时候需要对一段文字中的部分文字进行特殊的处理,如改变其中部分文字的大小、颜色、加下划线等,这个时候使用Span就能方便地解决这些问题。本文将主要介绍SpannableStringBuilder和各种Span的使用。


2.SpannableStringBuilder的基本用法


新建一个SpannableStringBuilder对象的操作如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");

SpannableStringBuilder的setSpan()方法如下:


//what:各种文本Span,如BackgroundColorSpan、ForegroundColorSpan等
//start:应用Span的文本的开始位置索引
//end:应用Span的文本的结束位置索引
//flags:标志
public void setSpan(Object what, int start, int end, int flags) {
setSpan(true, what, start, end, flags, true/*enforceParagraph*/);
}

3.使用Span给文本添加效果


3.1 AbsoluteSizeSpan

此Span用来改变文本的绝对大小,示例如下:


 SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new AbsoluteSizeSpan(60),3,9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.2 BackgroundColorSpan

此Span用来改变文本的背景颜色大小,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new BackgroundColorSpan(Color.GREEN),3,9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.3 ClickableSpan

此Span用来给文本添加点击效果,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
Toast.makeText(MainActivity.this,"ClickableSpan",Toast.LENGTH_SHORT).show();
}
}, 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);
tv_content.setMovementMethod(LinkMovementMethod.getInstance());
tv_content.setHighlightColor(Color.TRANSPARENT);

3.4 DrawableMarginSpan

此Span用来给段落添加drawable和padding,这个padding指的是drawable和文本之间的距离,默认值是0,Span要从文本的起始位置设置,否则Span将不会渲染或者错误地渲染,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
Drawable drawable = AppCompatResources.getDrawable(MainActivity.this,R.drawable.ic_launcher);
builder.setSpan(new DrawableMarginSpan(drawable,30), 0, builder.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.5 DynamicDrawableSpan

此Span使用drawable替换文本内容,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new DynamicDrawableSpan() {
@Override
public Drawable getDrawable() {
Drawable drawable =
AppCompatResources.getDrawable(MainActivity.this,R.drawable.ic_launcher);
drawable.setBounds(0,0,drawable.getIntrinsicWidth(),drawable.getIntrinsicHeight());
return drawable;
}
}, 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.6 ForegroundColorSpan

此Span可以用来改变文本的颜色,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new ForegroundColorSpan(Color.GREEN), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.7 IconMarginSpan

此Span可以在文本开始的地方添加位图,而且可以在位图和文本之间设置padding,padding的默认值是0px,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
builder.setSpan(new IconMarginSpan(bitmap,30), 0, builder.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.8 ImageSpan

此Span可以使用Drawable替换文本,创建ImageSpan的构造方法有很多,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new ImageSpan(MainActivity.this,R.drawable.ic_launcher), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.9 MaskFilterSpan

此Span可以给文本设置MaskFilter,例如给文本设置模糊效果,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
MaskFilter maskFilter = new BlurMaskFilter(10f, BlurMaskFilter.Blur.NORMAL);
builder.setSpan(new MaskFilterSpan(maskFilter), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.10 QuoteSpan

此Span可以在文本开始的地方添加一个垂直的线条,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new QuoteSpan(Color.GREEN), 0, builder.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.11 RelativeSizeSpan

此Span可以按一定的比例缩放文本的大小,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new RelativeSizeSpan(2.0f), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.12 ScaleXSpan

此Span以一定的系数在水平方向缩放文本的大小,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new ScaleXSpan(2.5f), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.13 StrikethroughSpan

此Span可以在文本上添加下划线,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new StrikethroughSpan(), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.14 StyleSpan

此Span可以设置文本的样式,可用的样式有Typeface.NORMAL、Typeface.BOLD、Typeface.ITALIC、Typeface.BOLD_ITALIC,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new StyleSpan(Typeface.BOLD), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.15 SubscriptSpan

此Span可以将文本的基线移动到更低的地方,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new SubscriptSpan(), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.16 SuperscriptSpan

此Span可以将文本的基线移动到更高的地方,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new SuperscriptSpan(), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

3.17 UnderlineSpan()

此Span可以在文本下面添加下划线,示例如下:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new UnderlineSpan(), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

4.多个Span组合使用


Span不但可以单独使用,还可以组合在一起使用,以下示例演示了如何同时加粗文字,改变文字的颜色和添加下滑线:


SpannableStringBuilder builder = new SpannableStringBuilder("Hello World!");
builder.setSpan(new UnderlineSpan(), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
builder.setSpan(new ForegroundColorSpan(Color.GREEN), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
builder.setSpan(new StyleSpan(Typeface.BOLD), 3, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
tv_content.setText(builder);

5.总结


Span的功能相当丰富,如改变文本颜色、大小、添加点击效果、加下划线等功能,本文介绍了经常用到的各种Span,Span支持单独使用和组合使用,使用它能够对文本进行各种灵活的操作,去实现个性化的需求。


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

看完这篇Broadcast你还不会,来找我

看完这篇文章,你将明白以下内容: broadcast是什么,使用场景 Android中广播的分类 广播的注册方式 本地广播优点、原理 广播的安全 1.1 什么是 BroadcastReceiver 是四大组件之一, 主要用于接收 app 发送的广播 内部...
继续阅读 »

看完这篇文章,你将明白以下内容:



  • broadcast是什么,使用场景

  • Android中广播的分类

  • 广播的注册方式

  • 本地广播优点、原理

  • 广播的安全


1.1 什么是 BroadcastReceiver



  • 是四大组件之一, 主要用于接收 app 发送的广播

  • 内部通信实现机制:通过 android 系统的 Binder 机制.


1.2 广播分类


1.2.1 无序广播



  • 也叫标准广播,是一种完全异步


执行的广播。



  • 在广播发出之后,所有广播接收器几乎都会在同一时刻接收到这条广播消息,它们之间没有任何先后顺序,广播的效率较高。

  • 优点: 完全异步, 逻辑上可被任何接受者收到广播,效率高

  • 缺点: 接受者不能将处理结果交给下一个接受者, 且无法终止广播.


1.2.2 有序广播




  • 是一种同步执行的广播。




  • 在广播发出之后,同一时刻只有一个广播接收器能够收到这条广播消息,当其逻辑执行完后该广播接收器才会继续传递。




  • 调用 SendOrderedBroadcast() 方法来发送广播,同时也可调用 abortBroadcast() 方法拦截该广播。可通过 <intent-filter> 标签中设置 android:property 属性来设置优先级,未设置时按照注册的顺序接收广播。




  • 有序广播接受器间可以互传数据。




  • 当广播接收器收到广播后,当前广播也可以使用 setResultData 方法将数据传给下一个接收器。




  • 使用 getStringExtra 函数获取广播的原始数据,通过 getResultData 方法取得上个广播接收器自己添加的数据,并可用 abortBroadcast 方法丢弃该广播,使该广播不再被别的接收器接收到。




  • 总结





  1. 按被接收者的优先级循序传播 A > B > C ,

  2. 每个都有权终止广播, 下一个就得不到

  3. 每一个都可进行修改操作, 下一个就得到上一个修改后的结果.


1.2.3 最终广播者



  • Context.sendOrderedBroadcast ( intent , receiverPermission , resultReceiver , scheduler , initialCode , initialData , initialExtras ) 时我们可以指定 resultReceiver 为最终广播接收者.

  • 如果比他优先级高的接受者不终止广播, 那么他的 onReceive 会执行两次

  • 第一次是正常的接收

  • 第二次是最终的接收

  • 如果优先级高的那个终止广播, 那么他还是会收到一次最终的广播


1.2.4 常见的广播接收者运用场景



  • 开机启动, sd 卡挂载, 低电量, 外拨电话, 锁屏等

  • 比如根据产品经理要求, 设计播放音乐时, 锁屏是否决定暂停音乐.


1.3 BroadcastReceiver 的种类


1.3.1 广播作为 Android 组件间的通信方式,如下使用场景:



对前一部分 “ 请描述一下 BroadcastReceiver ” 进行展开补充




  • APP 内部的消息通信。

  • 不同 APP 之间的消息通信。

  • Android 系统在特定情况下与 APP 之间的消息通信。

  • 广播使用了观察者模式,基于消息的发布 / 订阅事件模型。广播将广播的发送者和接受者极大程度上解耦,使得系统能够方便集成,更易扩展。

  • BroadcastReceiver 本质是一个全局监听器,用于监听系统全局的广播消息,方便实现系统中不同组件间的通信。

  • 自定义广播接收器需要继承基类 BroadcastReceiver ,并实现抽象方法 onReceive ( context, intent ) 。默认情况下,广播接收器也是运行在主线程,因此 onReceiver() 中不能执行太耗时的操作( 不超过 10s ),否则将会产生 ANR 问题。onReceiver() 方法中涉及与其他组件之间的交互时,可以使用发送 Notification 、启动 Service 等方式,最好不要启动 Activity


1.3.2 系统广播



  • Android 系统内置了多个系统广播,只要涉及手机的基本操作,基本上都会发出相应的系统广播,如开机启动、网络状态改变、拍照、屏幕关闭与开启、电量不足等。在系统内部当特定时间发生时,系统广播由系统自动发出。

  • 常见系统广播 Intent 中的 Action 为如下值:



  1. 短信提醒:android.provider.Telephony.SMS_RECEIVED

  2. 电量过低:ACTION_BATIERY_LOW

  3. 电量发生改变:ACTION_BATTERY_CHANGED

  4. 连接电源:ACTION_POWER_CO             



  • Android 7.0 开始,系统不会再发送广播 ACTION_NEW_PICTUREACTION_NEW_VIDEO ,对于广播 CONNECTIVITY_ACTION 必须在代码中使用 registerReceiver 方法注册接收器,在 AndroidManifest 文件中声明接收器不起作用。

  • Android 8.0 开始,对于大多数隐式广播,不能在 AndroidManifest 文件中声明接收器。


1.3.3 局部广播



  • 局部广播的发送者和接受者都同属于一个 APP

  • 相比于全局广播具有以下优点:



  1. 其他的 APP 不会受到局部广播,不用担心数据泄露的问题。

  2. 其他 APP 不可能向当前的 APP 发送局部广播,不用担心有安全漏洞被其他 APP 利用。

  3. 局部广播比通过系统传递的全局广播的传递效率更高。



  • Android v4 包中提供了 LocalBroadcastManager 类,用于统一处理 APP 局部广播,使用方式与全局广播几乎相同,只是调用注册 / 取消注册广播接收器和发送广播偶读方法时,需要通过 LocalBroadcastManager 类的 getInstance() 方法获取的实例调用。


1.4 BroadcastReceiver 注册方式


1.4.1 静态注册


AndroidManifest.xml 文件中配置。


<receiver android:name=".MyReceiver" android:exported="true">
<intent-filter>
<!-- 指定该 BroadcastReceiver 所响应的 Intent 的 Action -->
<action android:name="android.intent.action.INPUT_METHOD_CHANGED"
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>


  • 两个重要属性需要关注:



  1. android: exported 其作用是设置此 BroadcastReceiver 能否接受其他 APP 发出的广播 ,当设为 false 时,只能接受同一应用的的组件或具有相同 user ID 的应用发送的消息。这个属性的默认值是由 BroadcastReceiver 中有无 Intent-filter 决定的,如果有 Intent-filter ,默认值为 true ,否则为 false

  2. android: permission 如果设置此属性,具有相应权限的广播发送方发送的广播才能被此 BroadcastReceiver 所接受;如果没有设置,这个值赋予整个应用所申请的权限。


1.4.2 动态注册



  • 调用 ContextregisterReceiver ( BroadcastReceiver receiver , IntentFilter filter ) 方法指定。


1.5 在 Mainfest 和代码如何注册和使用 BroadcastReceiver ? ( 一个 action 是重点 )


1.5.1 使用文件注册 ( 静态广播 )




  • 只要 app 还在运行,那么会一直收到广播消息




  • 演示:





  1. 一个 app 里: 自定义一个类继承 BroadcastReceiver 然后要求重写 onReveiver 方法


public class MyBroadCastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("MyBroadCastReceiver", "收到信息,内容是 :&emsp;" + intent.getStringExtra("info") + "");
}
}


  1. 清单文件注册,并设置 Action , 就那么简单完成接收准备工作


<receiver android:name=".MyBroadCastReceiver">
<intent-filter>
<action android:name="myBroadcast.action.call"/>
</intent-filter>
</receiver>

1.5.2 代码注册 ( 动态广播 )




  • 当注册的 Activity 或者 Service 销毁了那么就会接收不到广播.




  • 演示:





  1. 在和广播接受者相同的 app 里的 MainActivity 添加一个注册按钮 , 用来注册广播接收者

  2. 设置意图过滤,添加 Action


//onCreate创建广播接收者对象
mReceiver = new MyBroadCastReceiver();

//注册按钮
public void click(View view) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("myBroadcast.action.call");
registerReceiver(mReceiver, intentFilter);
}


  1. 销毁的时候取消注册


@Override
protected void onDestroy() {
unregisterReceiver(mReceiver);
super.onDestroy();
}

1.5.3 在另一个 app , 定义一个按钮, 设置意图, 意图添加消息内容, 意图设置 action( ... ) 要匹配 , 然后发送广播即可.


public void click(View view) {
Intent intent = new Intent();
intent.putExtra("info", "消息内容");
intent.setAction("myBroadcast.action.call");
sendBroadcast(intent);
}


  • 运行两个 app 之后:



  1. 静态注册的方法: 另一 app 直接发广播就收到了

  2. 动态注册的方法: 自己的 app 先代码注册,然后另一个 app 直接发广播即可.


1.6 BroadcastReceiver 的实现原理是什么?




  • Android 中的广播使用了设计模式中的观察者模式:基于消息的发布 / 订阅事件模型。




  • 模型中主要有 3 个角色:





  1. 消息订阅者( 广播接收者 )

  2. 消息发布者( 广播发布者 )

  3. 消息中心( AMS,即 Activity Manager Service


1.6.1 原理:



  • 广播接收者通过 Binder 机制在 AMSActivity Manager Service ) 注册;

  • 广播发送者通过 Binder 机制向 AMS 发送广播;

  • AMS 根据广播发送者要求,在已注册列表中,寻找合适的 BroadcastReceiver ( 寻找依据:IntentFilter / Permission );

  • AMS 将广播发送到 BroadcastReceiver 相应的消息循环队列中;

  • 广播接收者通过消息循环拿到此广播,并回调 onReceive() 方法。

  • 需要注意的是:广播的发送和接受是异步的,发送者不会关心有无接收者或者何时收到。


1.7 本地广播



  • 本地广播机制使得发出的广播只能够在应用程序的内部进行传递,并且广播接收器也只能接受来自本应用程序发出的广播,则安全性得到了提高。

  • 本地广播主要是使用了一个 LocalBroadcastManager 来对广播进行管理,并提供了发送广播和注册广播接收器的方法。

  • 开发者只要实现自己的 BroadcastReceiver 子类,并重写 onReceive ( Context context, Intetn intent ) 方法即可。

  • 当其他组件通过 sendBroadcast()sendStickyBroadcast()sendOrderBroadcast() 方法发送广播消息时,如果该 BroadcastReceiver 也对该消息“感兴趣”,BroadcastReceiveronReceive ( Context context, Intetn intent ) 方法将会被触发。

  • 使用步骤:



  1. 调用 LocalBroadcastManager.getInstance() 获得实例

  2. 调用 registerReceiver() 方法注册广播

  3. 调用 sendBroadcast() 方法发送广播

  4. 调用 unregisterReceiver() 方法取消注册


1.7.1 注意事项:



  1. 本地广播无法通过静态注册方式来接受,相比起系统全局广播更加高效。

  2. 在广播中启动 Activity 时,需要为 Intent 加入 FLAG_ACTIVITY_NEW_TASK 标记,否则会报错,因为需要一个栈来存放新打开的 Activity

  3. 广播中弹出 Alertdialog 时,需要设置对话框的类型为 TYPE_SYSTEM_ALERT ,否则无法弹出。

  4. 不要在 onReceiver() 方法中添加过多的逻辑或者进行任何的耗时操作,因为在广播接收器中是不允许开启线程的,当 onReceiver() 方法运行了较长时间而没有结束时,程序就会报错。


1.8 Sticky Broadcast 粘性广播



  • 如果发送者发送了某个广播,而接收者在这个广播发送后才注册自己的 Receiver ,这时接收者便无法接收到刚才的广播

  • 为此 Android 引入了 StickyBroadcast ,在广播发送结束后会保存刚刚发送的广播( Intent ),这样当接收者注册完 Receiver 后就可以继续使用刚才的广播。

  • 如果在接收者注册完成前发送了多条相同 Action 的粘性广播,注册完成后只会收到一条该 Action 的广播,并且消息内容是最后一次广播内容。

  • 系统网络状态的改变发送的广播就是粘性广播。



  1. 粘性广播通过 ContextsendStickyBroadcast ( Intent ) 接口发送,需要添加权限

  2. uses-permission android:name=”android.permission.BROADCAST_STICKY”

  3. 也可以通过 ContextremoveStickyBroadcast ( Intent intent ) 接口移除缓存的粘性广播


1.9 LocalBroadcastManager 详解


1.9.1 特点:



  1. 使用它发送的广播将只在自身APP内传播,因此你不必担心泄漏隐私数据;

  2. 其他 APP 无法对你的 APP 发送该广播,因为你的APP根本就不可能接收到非自身应用发送的该广播,因此你不必担心有安全漏洞可以利用;

  3. 比系统的全局广播更加高效。


1.9.2 源码分析 :



  1. LocalBroadcastManager 内部协作主要是靠这两个 Map 集合:MReceiversMActions ,当然还有一个 List 集合 MPendingBroadcasts ,这个主要就是存储待接收的广播对象。

  2. LocalBroadcastManager 高效的原因主要是因为它内部是通过 Handler 实现的,它的 sendBroadcast() 方法含义并非和我们平时所用的一样,它的 sendBroadcast() 方法其实是通过 handler 发送一个 Message 实现的;

  3. 既然它内部是通过 Handler 来实现广播的发送的,那么相比于系统广播通过 Binder 实现那肯定是更高效了,同时使用 Handler 来实现,别的应用无法向我们的应用发送该广播,而我们应用内发送的广播也不会离开我们的应用;


1.9.3 BroadcastReceiver 安全问题



  • BroadcastReceiver 设计的初衷是从全局考虑可以方便应用程序和系统、应用程序之间、应用程序内的通信,所以对单个应用程序而言BroadcastReceiver 是存在安全性问题的 ( 恶意程序脚本不断的去发送你所接收的广播 ) 。为了解决这个问题 LocalBroadcastManager 就应运而生了。

  • LocalBroadcastManagerAndroid Support 包提供了一个工具,用于在同一个应用内的不同组件间发送 BroadcastLocalBroadcastManager 也称为局部通知管理器,这种通知的好处是安全性高,效率也高,适合局部通信,可以用来代替 Handler 更新 UI


1.9.4 广播的安全性



  • Android 系统中的广播可以跨进程直接通信,会产生以下两个问题:



  1. 其他 APP 可以接收到当前 APP 发送的广播,导致数据外泄。

  2. 其他 APP 可以向当前 APP 放广播消息,导致 APP 被非法控制。



  • 发送广播



  1. 发送广播时,增加相应的 permission ,用于权限验证。

  2. Android 4.0 及以上系统中发送广播时,可以使用 setPackage() 方法设置接受广播的包名。

  3. 使用局部广播。



  • 接受广播



  1. 注册广播接收器时,增加相应的 permission ,用于权限验证。

  2. 注册广播接收器时,设置 android:exported 的值为false。



  • 使用局部广播



  1. 发送广播时,如果增加了 permission

  2. 那接受广播的 APP 必须申请相应权限,这样才能收到对应的广播,反之亦然。


1.9.5 使用 BroadcastReceiver 的好处



  1. 因广播数据在本应用范围内传播,你不用担心隐私数据泄露的问题。

  2. 不用担心别的应用伪造广播,造成安全隐患。

  3. 相比在系统内发送全局广播,它更高效。


1.10 如何让自己的广播只让指定的 app 接收?



  • 在发送广播的 app 端,自定义定义权限, 那么想要接收的另外 app 端必须声明权限才能收到.



  1. 权限, 保护层级是普通正常.

  2. 用户权限


<permission android:name="broad.ok.receiver" android:protectionLevel="normal"/>
<uses-permission android:name="broad.ok.receiver" />


  1. 发送广播的时候加上权限字符串


public void click(View view) {
Intent intent = new Intent();
intent.putExtra("info", "消息内容");
intent.setAction("myBroadcast.action.call");
sendBroadcast(intent, "broad.ok.receiver");
//sendOrderedBroadcast(intent,"broad.ok.receiver");
}


  1. 其他app接收者想好获取广播,必须声明在清单文件权限


<uses-permission android:name="broad.ok.receiver"/>

1.11 广播的优先级对无序广播生效吗?



  • 优先级对无序也生效.


1.12 动态注册的广播优先级谁高?



  • 谁先注册,谁就高


1.13 如何判断当前的 BrodcastReceiver 接收到的是有序还是无序的广播?



  • onReceiver 方法里,直接调用判断方法得返回值


public void onReceive(Context context, Intent intent) {
Log.d("MyBroadCastReceiver", "收到信息,内容是 :&emsp;" + intent.getStringExtra("info") + "");
boolean isOrderBroadcast = isOrderedBroadcast();
}

1.14 BroadcastReceiver 不能执行耗时操作



  • 一方面



  1. BroadcastReceiver 一般处于主线程。

  2. 耗时操作会导致 ANR



  • 另一方面



  1. BroadcastReceiver 启动时间较短。

  2. 如果一个进程里面只存在一个 BroadcastReceiver 组件。并且在其中开启子线程执行耗时任务。

  3. 系统会认为该进程是优先级最低的空进程。很容易将其杀死。


总结






  1. 本文对 BroadcastReceiverContentProvider 的 知识进行了详尽的总结,如果有可以补充的知识点,欢迎大家在评论区指出。




  2. 希望大家通过本次阅读都能有所收获。


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

让 Flutter 在鸿蒙系统上跑起来

前言 鸿蒙系统 (HarmonyOS)是华为推出的一款面向未来、面向全场景的分布式操作系统。在传统单设备系统能力的基础上,鸿蒙提出了基于同一套系统能力、适配多种终端形态的分布式理念。自 2020 年 9 月 HarmonyOS 2.0 发布以来,华为加快了鸿蒙...
继续阅读 »

前言


鸿蒙系统 (HarmonyOS)是华为推出的一款面向未来、面向全场景的分布式操作系统。在传统单设备系统能力的基础上,鸿蒙提出了基于同一套系统能力、适配多种终端形态的分布式理念。自 2020 年 9 月 HarmonyOS 2.0 发布以来,华为加快了鸿蒙系统大规模落地的步伐,预计 2021 年底,鸿蒙系统会覆盖包括手机、平板、智能穿戴、智慧屏、车机在内数亿台终端设备。对移动应用而言,新的系统理念、新的交互形式,也意味着新的机遇。如果能够利用好鸿蒙的开发生态及其特性能力,可以让应用覆盖更多的交互场景和设备类型,从而带来新的增长点。


与面临的机遇相比,适配鸿蒙系统带来的挑战同样巨大。当前手机端,尽管鸿蒙系统仍然支持安卓 APK 安装及运行,但长期来看,华为势必会抛弃 AOSP,逐步发展出自己的生态,这意味着现有安卓应用在鸿蒙设备上将会逐渐变成“二等公民”。然而,如果在 iOS 及 Android 之外再重新开发和维护一套鸿蒙应用,在如今业界越来越注重开发迭代效率的环境下,所带来的开发成本也是难以估量的。因此,通过打造一套合适的跨端框架,以相对低的成本移植应用到鸿蒙平台,并利用好该系统的特性能力,就成为了一个非常重要的选项。


在现有的众多跨端框架当中,Flutter 以其自渲染能力带来的多端高度一致性,在新系统的适配上有着突出的优势。虽然Flutter 官方并没有适配鸿蒙的计划,但经过一段时间的探索和实践,美团外卖 MTFlutter 团队成功实现了 Flutter 对于鸿蒙系统的原生支持。


这里也要提前说明一下,因为鸿蒙系统目前还处于Beta版本,所以这套适配方案还没有在实际业务中上线,属于技术层面比较前期的探索。接下来本文会通过原理和部分实现细节的介绍,分享我们在移植和开发过程中的一些经验。希望能对大家有所启发或者帮助。


背景知识和基础概念介绍


在适配开始之前,我们要明确好先做哪些事情。先来回顾一下 Flutter 的三层结构:



在 Flutter 的架构设计中,最上层为框架层,使用 Dart 语言开发,面向 Flutter 业务的开发者;中间层为引擎层,使用 C/C++ 开发,实现了 Flutter 的渲染管线和 Dart 运行时等基础能力;最下层为嵌入层,负责与平台相关的能力实现。显然我们要做的是将嵌入层移植到鸿蒙上,确切地说,我们要通过鸿蒙原生提供的平台能力,重新实现一遍 Flutter 嵌入层


对于 Flutter 嵌入层的适配,Flutter 官方有一份不算详细的指南,实际操作起来成本很高。由于鸿蒙的业务开发语言仍然可用 Java,在很多基础概念上与 Android 也有相似之处(如下表所示),我们可以从 Android 的实现入手,完成对鸿蒙的移植。



Flutter 在鸿蒙上的适配


如前文所述,要完成 Flutter 在新系统上的移植,我们需要完整实现 Flutter 嵌入层要求的所有子模块,而从能力支持角度,渲染交互以及其他必要的原生平台能力是保证 Flutter 应用能够运行起来的最基本的要素,需要优先支持。接下来会依次进行介绍。


1. 渲染流程打通


我们再来回顾一下 Flutter 的图像渲染流程。如图所示,设备发起垂直同步(VSync)信号之后,先经过 UI 线程的渲染管线(Animate/Build/Layout/Paint),再经过 Raster 线程的组合和栅格化,最终通过 OpenGL 或 Vulkan 将图像上屏。这个流程的大部分工作都由框架层和引擎层完成,对于鸿蒙的适配,我们主要关注的是与设备自身能力相关的问题,即:


(1) 如何监听设备的 VSync 信号并通知 Flutter 引擎?
(2) OpenGL/Vulkan 用于上屏的窗口对象从何而来?



VSync 信号的监听及传递


在 Flutter 引擎的 Android 实现中,设备的 VSync 信号通过 Choreographer 触发,它产生及消费流程如下图所示:


Flutter VSync


Flutter 框架注册 VSync 回调之后,通过 C++ 侧的 VsyncWaiter 类等待 VSync 信号,后者通过 JNI 等一系列调用,最终 Java 侧的 VsyncWaiter 类调用 Android SDK 的 Choreographer.postFrameCallback 方法,再通过 JNI 一层层传回 Flutter 引擎消费掉此回调。Java 侧的 VsyncWaiter 核心代码如下:


@Override
public void asyncWaitForVsync(long cookie) {
Choreographer.getInstance()
.postFrameCallback(
new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
float fps = windowManager.getDefaultDisplay().getRefreshRate();
long refreshPeriodNanos = (long) (1000000000.0 / fps);
FlutterJNI.nativeOnVsync(
frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);
}
});
}

在整个流程中,除了来自 Android SDK 的 Choreographer 以外,大多数逻辑几乎都由 C++ 和 Java 的基础 SDK 实现,可以直接在鸿蒙上复用,问题是鸿蒙目前的 API 文档中尚没有开放类似 Choreographer 的能力。所以现阶段我们可以借用鸿蒙提供的类似 iOS Grand Central Dispatch 的线程 API,模拟出 VSync 的信号触发与回调:


@Override
public void asyncWaitForVsync(long cookie) {
// 模拟每秒 60 帧的屏幕刷新间隔:向主线程发送一个异步任务, 16ms 后调用
applicationContext.getUITaskDispatcher().delayDispatch(() -> {
float fps = 60; // 设备刷新帧率,HarmonyOS 未暴露获取帧率 API,先写死 60 帧
long refreshPeriodNanos = (long) (1000000000.0 / fps);
long frameTimeNanos = System.nanoTime();
FlutterJNI.nativeOnVsync(frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);
}, 16);
};

渲染窗口的构建及传递


在这一部分,我们需要在鸿蒙系统上构建平台容器,为 Flutter 引擎的图形渲染提供用于上屏的窗口对象。同样,我们参考 Flutter for Android 的实现,看一下 Android 系统是怎么做的:



Flutter 在 Android 上支持 Vulkan 和 OpenGL 两种渲染引擎,篇幅原因我们只关注 OpenGL。抛开复杂的注册及调用细节,本质上整个流程主要做了三件事:



  1. 创建了一个视图对象,提供可用于直接绘制的 Surface,将它通过 JNI 传递给原生侧;

  2. 在原生侧获取 Surface 关联的本地窗口对象,并交给 Flutter 的平台容器;

  3. 将本地窗口对象转换为 OpenGL ES 可识别的绘图表面(EGLSurface),用于 Flutter 引擎的渲染上屏。


接下来我们用鸿蒙提供的平台能力实现这三点。


a. 可用于直接绘制的视图对象


鸿蒙系统的 UI 框架提供了很多常用视图组件(Component),比如按钮、文字、图片、列表等,但我们需要抛开这些上层组件,获得直接绘制的能力。借助官方 媒体播放器开发指导 文档,可以发现鸿蒙提供了 SurfaceProvider 类,它管理的 Surface 对象可以用于视频解码后的展示。而 Flutter 渲染与视频上屏从原理上是类似的,因此我们可以借用 SurfaceProvider 实现 Surface 的管理和创建:


// 创建一个用于管理 Surface 的容器组件
SurfaceProvider surfaceProvider = new SurfaceProvider(context);
// 注册视图创建回调
surfaceProvider.getSurfaceOps().get().addCallback(surfaceCallback);

// ... 在 surfaceCallback 中
@Override
public void surfaceCreated(SurfaceOps surfaceOps) {
Surface surface = surfaceOps.getSurface();
// ...将 surface 通过 JNI 交给 Native 侧
FlutterJNI.onSurfaceCreated(surface);
}

b. 与 Surface 关联的本地窗口对象


鸿蒙目前开放的 Native API 并不多,在官方文档中我们可以比较容易地找到 Native_layer API。根据文档的说明,Native API 中的 NativeLayer 对象刚好对应了 Java 侧的 Surface 类,借助 GetNativeLayer 方法,我们实现了两者之间的转化:


// platform_view_android_jni_impl.cc
static void SurfaceCreated(JNIEnv* env, jobject jcaller, jlong shell_holder, jobject jsurface) {
fml::jni::ScopedJavaLocalFrame scoped_local_reference_frame(env);
// 通过鸿蒙 Native API 获取本地窗口对象 NativeLayer
auto window = fml::MakeRefCounted(
GetNativeLayer(env, jsurface));
ANDROID_SHELL_HOLDER->GetPlatformView()->NotifyCreated(std::move(window));
}

c. 与本地窗口对象关联的 EGLSurface


在 Android 的 AOSP 实现中,EGLSurface 可通过 EGL 库的 eglCreateWindowSurface 方法从本地窗口对象 ANativeWindow 创建而来。对于鸿蒙而言,虽然我们没有从公开文档找到类似的说明,但是 鸿蒙标准库 默认支持了 OpenGL ES,而且鸿蒙 SDK 中也附带了 EGL 相关的库及头文件,我们有理由相信在鸿蒙系统上,EGLSurface 也可以通过此方法从前一步生成的 NativeLayer 转化而来,在之后的验证中我们也确认了这一点:


// window->handle() 即为之前得到的 NativeLayer
EGLSurface surface = eglCreateWindowSurface(
display, config_, reinterpret_cast(window->handle()),
attribs);
//...交给 Flutter 渲染管线

2. 交互能力实现


交互能力是支撑 Flutter 应用能够正常运行的另一个基本要求。在 Flutter 中,交互包含了各种触摸事件、鼠标事件、键盘录入事件的传递及消费。以触摸事件为例,Flutter 事件传递的整个流程如下图所示:


Flutter 事件分发


iOS/Android 的原生容器通过触摸事件的回调 API 接收到事件之后,会将其打包传递至引擎层,后者将事件传发给 Flutter 框架层,并完成事件的消费、分发和逻辑处理。同样,整个流程的大部分工作已经由 Flutter 统一,我们要做的仅仅是在原生容器上监听用户的输入,并封装成指定格式交给引擎层而已。


在鸿蒙系统上,我们可以借助平台提供的 多模输入 API,实现多种类型事件的监听:


flutterComponent.setTouchEventListener(touchEventListener); // 触摸及鼠标事件
flutterComponent.setKeyEventListener(keyEventListener); // 键盘录入事件
flutterComponent.setSpeechEventListener(speechEventListener); // 语音录入事件

对于事件的封装处理,可以复用 Android 已有逻辑,只需要关注鸿蒙与 Android 在事件处理上的对应关系即可,比如触摸事件的部分对应关系:



3. 其他必要的平台能力


为了保证 Flutter 应用能够正常运行,除了最基本的渲染和交互外,我们的嵌入层还要提供资源管理、事件循环、生命周期同步等平台能力。对于这些能力 Flutter 大多都在嵌入层的公共部分有抽象类声明,只需要使用鸿蒙 API 重新实现一遍即可。


比如资源管理,引擎提供了 AssetResolver 声明,我们可以使用鸿蒙 Rawfile API 来实现:


class HAPAssetMapping : public fml::Mapping {
public:
HAPAssetMapping(RawFile* asset) : asset_(asset) {}
~HAPAssetMapping() override { CloseRawFile(asset_); }

size_t GetSize() const override { return GetRawFileSize(asset_); }

const uint8_t* GetMapping() const override {
return reinterpret_cast(GetRawFileBuffer(asset_));
}

private:
RawFile* const asset_;

FML_DISALLOW_COPY_AND_ASSIGN(HAPAssetMapping);
};

对于事件循环,引擎提供了 MessageLoopImpl 抽象类,我们可以使用鸿蒙 Native_EventHandler API 实现:


// runner_ 为鸿蒙 EventRunnerNativeImplement 的实例
void MessageLoopHarmony::Run() {
FML_DCHECK(runner_ == GetEventRunnerNativeObjForThread());
int result = ::EventRunnerRun(runner_);
FML_DCHECK(result == 0);
}

void MessageLoopHarmony::Terminate() {
int result = ::EventRunnerStop(runner_);
FML_DCHECK(result == 0);
}

对于生命周期的同步,鸿蒙的 Page Ability 提供了完整的生命周期回调(如下图所示),我们只需要在对应的时机将状态上报给引擎即可。


Page Ability Lifecycle


当以上这些能力都准备好之后,我们就可以成功把 Flutter 应用跑起来了。以下是通过 DevEco Studio 运行官方 flutter gallery 应用的截图,截图中 Flutter 引擎已经使用鸿蒙系统的平台能力进行了重写:


DevEco Running Flutte


借由鸿蒙的多设备支持能力,此应用甚至可在 TV、车机、手表、平板等设备上运行:


Flutter Multiple Devices


总结和展望


通过上述的构建和适配工作,我们以极小的开发成本实现了 Flutter 在鸿蒙系统上的移植,基于 Flutter 开发的上层业务几乎不做任何修改就可以在鸿蒙系统上原生运行,为迎接鸿蒙系统后续的大规模推广也提前做好了技术储备。


当然,故事到这里并没有结束。在最基本的运行和交互能力之上,我们更需要关注 Flutter 与鸿蒙自身生态的结合:如何优雅地适配鸿蒙的分布式技术?如何用 Flutter 实现设备之间的快速连接、资源共享?现有的众多 Flutter 插件如何应用到鸿蒙系统上?未来 MTFlutter 团队将在这些方面做更深入的探索,因为解决好这些问题,才是真正能让应用覆盖用户生活的全场景的关键。


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

Flutter PlatformView 在闲鱼直播业务中的实践

背景 闲鱼近期实现了端上直播间的 Flutter 技术重构,验证和拓展了 Flutter 在音视频领域的业务边界。因为直播丰富的玩法和可变的交互,通常我们会在直播间页面覆盖一层互动层,用于处理和展示业务互动行为。这一互动层,通常是由 H5/Weex 等技术来实...
继续阅读 »

背景


闲鱼近期实现了端上直播间的 Flutter 技术重构,验证和拓展了 Flutter 在音视频领域的业务边界。因为直播丰富的玩法和可变的交互,通常我们会在直播间页面覆盖一层互动层,用于处理和展示业务互动行为。这一互动层,通常是由 H5/Weex 等技术来实现的,以满足动态性和业务投放的需求。因为其背后有着一整套配套的解决方案和能力,显然在 Flutter 场景下,复用或移植成熟的 Native 能力是比较好的解决方案,PlatformView 是最适合用于实现该组件的技术,这也是我们采用的方案。


什么是 PlatformView?


PlatformView 技术是 Flutter 提供的一种能够将 Native 组件嵌入到 Flutter 页面中的能力,有了这种能力,一些 Native 上非常成熟的功能组件,例如地图、广告页面、WebView 就可以很方便地和 Flutter 结合,在 Flutter 页面上展示。


实现技术上,iOS 中,PlatformView 的 Native View 会被加入到 Flutter 的 UI 视图层级中,这种方式称之为 Hybrid Composition;而 Android 支持使用 VirtualDisplay 和 Hybrid Composition 两种模式,前者将 Native View 绘制到内存中,然后和 Flutter Widget 一起渲染到 Flutter 页面中,后者和 iOS 上类似。闲鱼目前在 Android 中使用的是 VirtualDisplay 这种模式。


直播间互动层组件


互动层是一个覆盖整个直播间的组件,隶属于某一特定的直播间,可以跟随该直播间上下翻页滑动,一般情况下处于直播间 View 层级的最上层,这样才能做到可以在直播间任意位置布局任意的元素并展示。它是一个背景透明的组件,当用户点击互动层上的元素时,由互动层来进行响应交互,而当用户点击透明区域时,事件会穿透互动层,由下面层级上最合适的组件来进行响应,不影响正常的直播间功能。这就要求我们对该组件的事件分发进行一些处理。这里主要的处理方案是,获取用户的点击位置坐标点,判断组件该像素点的透明度,根据一定的阈值来区分究竟是该由谁来响应。以 iOS 为例,我们知道 iOS 的事件响应分为两个阶段:第一阶段用于寻找最佳响应者,即在 View Tree 上不断调用 hitTest 和 pointInside 方法进行检测,第二阶段才是真正响应事件。所以对于 iOS 实现来说 ,我们只要干预第一阶段,重写该互动层 View 的 pointInside 方法增加上我们的透明度判断逻辑,就可以实现。Android 也是进行类似的处理。


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wV7LXOVn-1636119253919)(https://upload-images.jianshu.io/upload_images/27208383-1a3401d11fb3db0a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]


PlatformView 互动层的事件分发问题


虽然通过 PlatformView 可以很方便地嵌入互动层 Native View,但在这个场景下事件处理遇到了一些麻烦。通常情况下,如果嵌入的 PlatformView 是一个正常响应事件的组件,那一般不会有问题,但由上面的分析可知,我们需要对事件进行特殊处理,因为互动层 PlatformView 处于整个直播间的最上层,所以正常情况下,所有它下面的 Flutter 组件都无法响应事件。我们可以将 PlatformView 的 hitTestBehavior 设置为 PlatformViewHitTestBehavior.transparent,这样可以将事件穿透到下面其他的 Flutter 组件,但是互动层本身就无法响应事件了。


PlatformView 有两个身份,一个是作为 Native View,另外一个是作为 Flutter 组件树上的一个节点(Widget),因此它上面的事件由两部分共同配合处理。


所以我们需要先探究一下 Flutter 框架是如何处理 PlatformView 在 Native 和 Flutter 两侧的事件分发和控制的,下面以 iOS 为例来进行具体分析。


PlatformView 事件响应分析


在这里插入图片描述



由 UI 层级图和下面的源码可以看出,PlatformView 是 FlutterView 的 subView。源码中的 embeded_view 是我们真正提供给 Flutter 的 Native View,它会传递给 FlutterTouchInterceptingView 的构造方法,成为其 subView,FlutterTouchInterceptingView 又是 ChildClippingView 的 subView。



FlutterTouchInterceptingView


这几个 view 中,FlutterTouchInterceptingView 是 Flutter 用来控制和处理 PlatformView 事件的关键。Flutter 需要有这个一个View 来拦截事件,因为事件毕竟是从 Native 传递给 Flutter 的,而 PlatformView 本身也是个 Native View,如果不对作用在 PlatformView 上的事件进行拦截的话,PlatformView 自身就会消化掉事件,而 Flutter 侧则感知不到也没法控制了。



FlutterTouchInterceptingView 的 frame 和 embededView 保持一致,并作为其 superView,根据 iOS 的事件传递规则,FlutterTouchInterceptingView 会先接收到事件。由注释可知,FlutterTouchInterceptingView 实现了两个能力:一是它会延迟或者阻止事件到达 embededView;二是它会将所有的事件直接转发给 FlutterView。而这两点,分别是由 DelayingGestureRecognizer 和 ForwardingGestureRecognizer 这两个手势来完成的。
在这里插入图片描述


DelayingGestureRecognizer


DelayingGestureRecognizer 需要延迟其他事件响应,并且将除了 ForwardingGestureRecognizer 之外的其他手势都失效。



DelayingGestureRecognizer 是添加在 embededView 的 superView(FlutterTouchInterceptingView) 上的手势(Gesture),之所以能够起作用拦截 embededView 的手势和事件,原因在于 iOS 的手势识别优先级高于普通的事件响应,且响应链一旦确定,每次事件响应,整条响应链上手势的 delegate 方法都会被调用,用于确定最终可以识别的手势。如下图,如果触摸了 View4,确定 View 4 为最佳响应者,则从 View4 到 rootView 上的所有手势(gesture2、gesture3、gesture4、gesture5)的 delegate 方法都会被调用。
在这里插入图片描述


ForwardingGestureRecognizer


ForwardingGestureRecognizer 的实现就很简单了,它重写了事件的相关处理方法,将事件直接转发。


在这里插入图片描述


因为 PlatformView 也作为 Flutter Widget Tree 的一个节点,事件转发到 Flutter 之后,遵循 Flutter 的事件处理机制,和其他手势一起在竞技场(Arena)中角逐是否能够响应。最终如果竞争成功,事件该由 PlatformView 来响应,则 FlutterTouchInterceptingView 的 releaseGesture 方法会被调用,DelayingGestureRecognizer 手势会被置成 Failed 状态,其他事件就可以开始响应。相应地如果竞争失败,那么 FlutterTouchInterceptingView 的 blockGesture 方法会被调用,DelayingGestureRecognizer 手势会被置成 Ended 状态,这表明事件被它响应并且完成了,其他手势或者 View 响应者就不会再响应该事件了。
在这里插入图片描述
在这里插入图片描述


解决事件分发问题


由上面 iOS 上的的事件原理可知,Flutter 主要是通过 DelayingGestureRecoginzer 和 ForwardingGestureRecoginizer 这两个手势来干预和控制 PlatformView 上 Native 的事件分发行为。所以可以想到,如果没有这两个事件,事件的响应又会和我们熟悉的 Native 流程一致。


所以想要自定义 PlatformView 事件分发,在 iOS 上我们可以这么做:


1.根据需要设置 PlatformView Widget 的 hitTestBehavior 参数;


2.重写 PlatformView 的 pointInside 方法,在里面增加控制 Flutter 这两个手势的逻辑。因为 hitTest 寻找最佳响应者的过程一定在响应链响应之前,所以此处对 Flutter 手势的处理,不会影响事件转发给 Flutter 后的处理逻辑。
在这里插入图片描述


具体到直播互动场景来说,为了能让事件在大多数情况下能够被互动层下面的组件响应,在 Flutter 侧 PlatformView Widget 的 hitTestBehavior 需要设置为 PlatformViewHitTestBehavior.transparent,目的是为了让 PlatformView Widget 之下的 Flutter Widget 可以响应事件。重写 PlatformView 的 pointInside,如果透明度判断认为该由互动层来响应,则禁用 Flutter 的这两个手势;如果不该由互动层响应(即该由其他 Flutter 组件来响应),则恢复这两个手势响应,不影响正常的逻辑。相关实现代码如下:


[图片上传失败...(image-1c9fb8-1636119207800)]


因为 DelayingGestureRecoginzer 和 ForwardingGestureRecoginizer 这两个手势是定义在 FlutterTouchInterceptingView 中,而 FlutterTouchInterceptingView 是我们互动层 Native View 的 superView,所以代码中的 self.fluttterForwardGestureRecognizer 和 self.flutterDelayingGestureRecognizer 可以通过反射、循环遍历 superView 的手势列表来获取到。


关于 Android 上的处理


因为 Android 上 PlatfromView 采用了 VirtualDisplay 方案实现的,所以 FlutterView 和 PlatfromView 并不是真正处于同一个 ViewTree 中,因而在这个问题的处理上面和 iOS 略有不同,但原理相通。这里简单的说一下Android 上面的情况。


PlatformView 原本的设计中是由 FlutterView 接收 Android 的原生 TouchEvent,然后转化为 Flutter 中用于事件处理的 Event,再分发给 Flutter中 的 Widget。当 PlatformView 的 Widget(AndroidView)接收到事件后,会再次将事件转化为 Android 的 TouchEvent,然后转给 Native 的 PlatformView 实现 View。如图:


[图片上传失败...(image-7e84ed-1636119207800)]


我们可以在 FlutterView 外面再套一层用于事件处理的 View(EventInterceptView),该 View 会优先接收到事件,然后根据实际需要,决定事件转发给 FlutterView 还是 PlatformView。如图:
在这里插入图片描述


通过上述方案,我们在 iOS 和 Android 两端都可以做到在 PlatformView 互动层中实现透明度判断逻辑,并交给正确的响应者(Flutter 组件或者 Native View)来进行事件响应。使用 PlatformView 也让我们最大限度地复用了直播互动层背后对应的一整套解决方案,业务能力和使用体验都达到了和 Native 原生实现一样的效果。


写在最后


PlatformView 提供了在 Flutter 页面中嵌入 Native View 的能力,方便了很多业务场景和功能的实现,但 PlatformView 技术也还存在一些问题,例如使用了 PlatformView 在某些场景下可能会导致图片、视频的纹理错乱等。闲鱼在直播业务中第一次在生产环境中使用了 PlatformView 技术,也解决了一些已知问题,但仍有很多问题是我们还没有发现和解决的,后续我们也会继续在这方面进行研究,探索使用 PlatformView 的最佳实践!


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

IdleHandler你会用吗?记一次IdleHandler使用误区,导致ANR

1. 示例 问题抛出,当引入线上ANR抓取工具后,发现了不少IdleHandler带来的问题。堆栈具体见下图 思考为什么idleHandler会带来这样的问题呢,或许你会觉得是单个消息执行时间过长导致的。那么请看示例,项目本身代码较为复杂,简化代码如下: ...
继续阅读 »

1. 示例


问题抛出,当引入线上ANR抓取工具后,发现了不少IdleHandler带来的问题。堆栈具体见下图


image.png


思考为什么idleHandler会带来这样的问题呢,或许你会觉得是单个消息执行时间过长导致的。那么请看示例,项目本身代码较为复杂,简化代码如下



  • 工具类


    /**
* 添加任务到IdleHandler
*
* @param runnable runnable
*/
public static void run(Runnable runnable) {
IUiRunnable uiRunnable = new IUiRunnable(runnable);
Looper.getMainLooper().getQueue().addIdleHandler(uiRunnable);
}


  • 使用工具类


public class MainActivity extends AppCompatActivity {

public static final String TAG = "idleHandler";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

@Override
protected void onResume() {
super.onResume();
Log.e(TAG, "onResume: start");
//1 //关键代码处 延迟 3s执行的delay msg
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Log.e(TAG, "delay: msg");
startService(new Intent(MainActivity.this, MyService.class));
}
}, 3000);
//关键代码处 添加到IdleHandler里的三个任务
UIManager.run(() -> test(1));
UIManager.run(() -> test(2));
UIManager.run(() -> test(3));
}
//延迟任务
private void test(final int i) {

try {
Log.e(TAG, "queueIdle:test start " + i);
Thread.sleep(3000);
Log.e(TAG, "queueIdle:test end " + i);

} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

让我们来猜猜下面代码的输出顺序。再回看一下代码,肯定以为是下面这样对吧?


修复后.png


如果你觉得上面的输出没问题,那就更需要继续读下去了


这里把真实log截出来:


WX20211128-185004@2x.png


what??delay3000ms为什么失效了。而是Idle消息先执行了。


为什么呢?不慌遇到这种问题我们肯定要通过IdleHandler机制的源码,来找答案了。


2. 源码分析


//MessageQueue.java
Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}

int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

nativePollOnce(ptr, nextPollTimeoutMillis);

...
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
//1 执行时机
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}

if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
//2 copy副本
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}

// Run the idle handlers.
// We only ever reach this code block during the first iteration.
//3 逐个执行
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler

boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
//4 是否移除当前idleHandler
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}

// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;

// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}

//5 外部调用添加idleHandler
public void addIdleHandler(@NonNull IdleHandler handler) {
if (handler == null) {
throw new NullPointerException("Can't add a null IdleHandler");
}
synchronized (this) {
mIdleHandlers.add(handler);
}
}

鉴于next()方法比较长且相关介绍也比较多,这里不细说。




  • 注释1处,这里是IdleHandler执行时机,即主线程无消息或者未到执行时机(空闲时间)。




  • 注释2处,可以看到mPendingIdleHandlers相当于copy了mIdleHandlers中的内容,由注释5处可以看到我们调用addIdleHandler()添加的任务都存进了mIdleHandlers。




  • 注释3处,这里循环意味着要一次性处理之前添加的全部IdleHandler任务,如果我们短时内调用了多次addIdleHandler(),意味着这些idle msg将被拿出来逐个执行;而且要全部处理掉,才可以继续执行原消息队列的消息,如果idle msg很耗时,便会出现前面的主线程的postDelay任务执行时间就非常不可靠了;同理如果期间有触摸事件发生,那极有可能会因为得不到及时处理而导致ANR发生。赶紧检查下自己的项目中是否有此类问题。




  • 注释4处,控制本次的IdleHandler是会被再次调用还是单次调用呢,是由queueIdle()方法的返回值决定,这点是我们该利用起来的。




上面的工具方法初衷是好的,提供接口暴露给各业务侧在某个需要的时刻将非紧急任务延迟加载,进而减少卡顿,但用起来却没有那么丝滑,甚至导致ANR,希望大家理解后能够避开这个坑。


3.安全用法


IdleHandler再介绍



  • 这里再赘述下IdleHandler作用,我们都知道在做启动性能优化的时候,要尽可能多地减少启动阶段主线程任务。对一些启动阶段非必须且一定要在主线程里完成的任务,我们可以在应用启动完成之后再去加载。正是考虑到这些google官方提供了IdleHandler机制来告诉我们线程空闲时机。


利用IdleHandler设计的工具类


直接使用IdleHandler,肯定不太符合博主这种“大项目”的编码风格,简单封装是必要,方便我们做些功能定制,下面给大家说下项目中的工具类。



  • 正确的方法 将启动过程中非紧急的主线程任务全部放进uiTasks里,然后逐个执行,切记单个消息耗时不要太长。


private static List<Runnable> uiTasks = new ArrayList<>();

public static UIPoolManager addTask(Runnable runnable) {
tasks.add(runnable);
return this;
}

public static UIManager runUiTasks() {
NullHelper.requireNonNull(uiTasks);
IUiTask iUiTask = new IUiTask() {
@Override
public boolean queueIdle() {
if (!uiTasks.isEmpty()) {
Runnable task = uiTasks.get(0);
task.run();
uiTasks.remove(task);
}
//逐次取一个任务执行 避免占用主线程过久
return !uiTasks.isEmpty();
}
};

Looper.myQueue().addIdleHandler(iUiTask);
return this;
}

替换成上面的方法再试一遍。


@Override
protected void onResume() {
super.onResume();
Log.e(TAG, "onResume: start");
//1
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Log.e(TAG, "delay: msg");
startService(new Intent(MainActivity.this, MyService.class));
}
}, 3000);
UIManager.addTask(() -> test(1));
UIManager.addTask(() -> test(2));
UIManager.addTask(() -> test(3));
UIManager.runUiTasks();
}

得到最初希望的时序结果


修复后.png


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

iOS整体框架介绍

iOS
这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战iOS整体框架通常我们称iOS的框架为cocoa框架. 话不多说,官方的整体框架图如下:简单解释一下:Cocoa (Application) Layer(触摸层)Media Layer ...
继续阅读 »

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

iOS整体框架

通常我们称iOS的框架为cocoa框架. 话不多说,官方的整体框架图如下:

image.png

简单解释一下:

  • Cocoa (Application) Layer(触摸层)
  • Media Layer (媒体层)
  • Core Services Layer(核心服务层)
  • Core OS Layer (核心系统操作层)
  • The Kernel and Device Drivers layer(内核和驱动层)

注:Cocoa (Application) Layer(触摸层)其实包含cocoa Touch layer(触摸层) 和Application Layer (应用层).应用层原本在触摸层上面,因为应用层是开发者自己实现,所以和触摸层合在一起.

其实每一层都包含多个子框架, 如下图:

image.png

简单解释下(瞄一眼就得了):

  • Cocoa Touch Layer:触摸层提供应用基础的关键技术支持和应用的外观。如NotificationCenter的本地通知和远程推送服务,iAd广告框架,GameKit游戏工具框架,消息UI框架,图片UI框架,地图框架,连接手表框架,UIKit框架、自动适配等等

  • Media Layer:媒体层提供应用中视听方面的技术,如图形图像相关的CoreGraphics,CoreImage,GLKit,OpenGL ES,CoreText,ImageIO等等。声音技术相关的CoreAudio,OpenAL,AVFoundation,视频相关的CoreMedia,Media Player框架,音视频传输的AirPlay框架等等

  • Core Services Layer:系统服务层提供给应用所需要的基础的系统服务。如Accounts账户框架,广告框架,数据存储框架,网络连接框架,地理位置框架,运动框架等等。这些服务中的最核心的是CoreFoundationFoundation框架,定义了所有应用使用的数据类型。CoreFoundation是基于C的一组接口,Foundation是对CoreFoundation的OC封装

  • Core OS Layer:系统核心层包含大多数低级别接近硬件的功能,它所包含的框架常常被其它框架所使用。Accelerate框架包含数字信号,线性代数,图像处理的接口。针对所有的iOS设备硬件之间的差异做优化,保证写一次代码在所有iOS设备上高效运行。CoreBluetooth框架利用蓝牙和外设交互,包括扫描连接蓝牙设备,保存连接状态,断开连接,获取外设的数据或者给外设传输数据等等。Security框架提供管理证书,公钥和私钥信任策略,keychain,hash认证数字签名等等与安全相关的解决方案。

想看更详细的可以移步:iOS总体框架介绍和详尽说明

我们只需要知道其中重要的框架就是UIKit和Function框架.下面说说这两个框架.

Function框架

Foundation框架为所有应用程序提供基本的系统服务。应用程序以及 UIKit和其他框架,都是建立在 Foundation 框架的基础结构之上。 Foundation框架提供许多基本的对象类和数据类型,使其成为应用程序开发的基础。

话不多说,我们先来看看Foundation框架,三个图,包括了Foundation所以的类,图中灰色的是iOS不支持的,灰色部分是OS X系统的。

image.png

image.png

image.png

这里只需要知道绝大部分Function框架的类都继承NSObject, 小部分继承NSProxy

对于Foundation框架中的一些基本类的使用方法详情参见:iOS开发系列—Objective-C之Foundation框架

UIKit框架

UIKit框架提供一系列的Class(类)来建立和管理iOS应用程序的用户界面( UI )接口、应用程序对象、事件控制、绘图模型、窗口、视图和用于控制触摸屏等的接口。

UIKit框架的类继承体系图如下图所示:

image.png

在图中可以看出,responder 类是图中最大分支的根类,UIResponder为处理响应事件和响应链定义了界面和默认行为。当用户用手指滚动列表或者在虚拟键盘上输入时,UIKit就生成事件传送给UIResponder响应链,直到链中有对象处理这个事件。相应的核心对象,比如:UIApplication ,UIWindowUIView都直接或间接的从UIResponder继承 。

这里需要知道一点:UIKit框架所有的类都继承NSObject

UIKit框架的各个类的简单介绍戳后面的链接:UIKit框架各个类的简要说明 

收起阅读 »

阿里二面:什么是mmap?

iOS
平时在面试中你肯定会经常碰见的问题就是:RocketMQ为什么快?Kafka为什么快?什么是mmap?这一类的问题都逃不过的一个点就是零拷贝,虽然还有一些其他的原因,但是今天我们的话题主要就是零拷贝。传统IO在开始谈零拷贝之前,首先要对传统的IO方式有一个概念...
继续阅读 »

平时在面试中你肯定会经常碰见的问题就是:RocketMQ为什么快?Kafka为什么快?什么是mmap?

这一类的问题都逃不过的一个点就是零拷贝,虽然还有一些其他的原因,但是今天我们的话题主要就是零拷贝。

传统IO

在开始谈零拷贝之前,首先要对传统的IO方式有一个概念。

基于传统的IO方式,底层实际上通过调用read()write()来实现。

通过read()把数据从硬盘读取到内核缓冲区,再复制到用户缓冲区;然后再通过write()写入到socket缓冲区,最后写入网卡设备。

整个过程发生了4次用户态和内核态的上下文切换4次拷贝,具体流程如下:

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发公众号:编程大鑫,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!

  1. 用户进程通过read()方法向操作系统发起调用,此时上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. CPU把读缓冲区数据拷贝到应用缓冲区,上下文从内核态转为用户态,read()返回
  4. 用户进程通过write()方法发起调用,上下文从用户态转为内核态
  5. CPU将应用缓冲区中数据拷贝到socket缓冲区
  6. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write()返回

那么,这里指的用户态内核态指的是什么?上下文切换又是什么?

简单来说,用户空间指的就是用户进程的运行空间,内核空间就是内核的运行空间。

如果进程运行在内核空间就是内核态,运行在用户空间就是用户态。

为了安全起见,他们之间是互相隔离的,而在用户态和内核态之间的上下文切换也是比较耗时的。

从上面我们可以看到,一次简单的IO过程产生了4次上下文切换,这个无疑在高并发场景下会对性能产生较大的影响。

那么什么又是DMA拷贝呢?

因为对于一个IO操作而言,都是通过CPU发出对应的指令来完成,但是相比CPU来说,IO的速度太慢了,CPU有大量的时间处于等待IO的状态。

因此就产生了DMA(Direct Memory Access)直接内存访问技术,本质上来说他就是一块主板上独立的芯片,通过它来进行内存和IO设备的数据传输,从而减少CPU的等待时间。

但是无论谁来拷贝,频繁的拷贝耗时也是对性能的影响。

零拷贝

零拷贝技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域,这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。

那么对于零拷贝而言,并非真的是完全没有数据拷贝的过程,只不过是减少用户态和内核态的切换次数以及CPU拷贝的次数。

这里,仅仅有针对性的来谈谈几种常见的零拷贝技术。

mmap+write

mmap+write简单来说就是使用mmap替换了read+write中的read操作,减少了一次CPU的拷贝。

mmap主要实现方式是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次CPU拷贝。

整个过程发生了4次用户态和内核态的上下文切换3次拷贝,具体流程如下:

  1. 用户进程通过mmap()方法向操作系统发起调用,上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. 上下文从内核态转为用户态,mmap调用返回
  4. 用户进程通过write()方法发起调用,上下文从用户态转为内核态
  5. CPU将读缓冲区中数据拷贝到socket缓冲区
  6. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write()返回

mmap的方式节省了一次CPU拷贝,同时由于用户进程中的内存是虚拟的,只是映射到内核的读缓冲区,所以可以节省一半的内存空间,比较适合大文件的传输。

sendfile

相比mmap来说,sendfile同样减少了一次CPU拷贝,而且还减少了2次上下文切换。

sendfile是Linux2.1内核版本后引入的一个系统调用函数,通过使用sendfile数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间的拷贝,同时由于使用sendfile替代了read+write从而节省了一次系统调用,也就是2次上下文切换。

整个过程发生了2次用户态和内核态的上下文切换3次拷贝,具体流程如下:

  1. 用户进程通过sendfile()方法向操作系统发起调用,上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. CPU将读缓冲区中数据拷贝到socket缓冲区
  4. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,sendfile调用返回

sendfile方法IO数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比如静态文件服务器。

sendfile+DMA Scatter/Gather

Linux2.4内核版本之后对sendfile做了进一步优化,通过引入新的硬件支持,这个方式叫做DMA Scatter/Gather 分散/收集功能。

它将读缓冲区中的数据描述信息--内存地址和偏移量记录到socket缓冲区,由 DMA 根据这些将数据从读缓冲区拷贝到网卡,相比之前版本减少了一次CPU拷贝的过程

整个过程发生了2次用户态和内核态的上下文切换2次拷贝,其中更重要的是完全没有CPU拷贝,具体流程如下:

  1. 用户进程通过sendfile()方法向操作系统发起调用,上下文从用户态转向内核态
  2. DMA控制器利用scatter把数据从硬盘中拷贝到读缓冲区离散存储
  3. CPU把读缓冲区中的文件描述符和数据长度发送到socket缓冲区
  4. DMA控制器根据文件描述符和数据长度,使用scatter/gather把数据从内核缓冲区拷贝到网卡
  5. sendfile()调用返回,上下文从内核态切换回用户态

DMA gathersendfile一样数据对用户空间不可见,而且需要硬件支持,同时输入文件描述符只能是文件,但是过程中完全没有CPU拷贝过程,极大提升了性能。

应用场景

对于文章开头说的两个场景:RocketMQ和Kafka都使用到了零拷贝的技术。

对于MQ而言,无非就是生产者发送数据到MQ然后持久化到磁盘,之后消费者从MQ读取数据。

对于RocketMQ来说这两个步骤使用的是mmap+write,而Kafka则是使用mmap+write持久化数据,发送数据使用sendfile

总结

由于CPU和IO速度的差异问题,产生了DMA技术,通过DMA搬运来减少CPU的等待时间。

传统的IOread+write方式会产生2次DMA拷贝+2次CPU拷贝,同时有4次上下文切换。

而通过mmap+write方式则产生2次DMA拷贝+1次CPU拷贝,4次上下文切换,通过内存映射减少了一次CPU拷贝,可以减少内存使用,适合大文件的传输。

sendfile方式是新增的一个系统调用函数,产生2次DMA拷贝+1次CPU拷贝,但是只有2次上下文切换。因为只有一次调用,减少了上下文的切换,但是用户空间对IO数据不可见,适用于静态文件服务器。

sendfile+DMA gather方式产生2次DMA拷贝,没有CPU拷贝,而且也只有2次上下文切换。虽然极大地提升了性能,但是需要依赖新的硬件设备支持。

收起阅读 »

Swift接入例子-适合多人协作

iOS
在「 Swift接入例子 」中介绍了Swift项目如何接入SOT。但是要求SDK解压到特定目录中,编译配置的路径也是绝对路径,不适合多人协作合开。文本介绍适合多人开发协作的接入方法。 还是以开源的「 SwiftMessages 」Demo为例,该工程全部用Sw...
继续阅读 »

「 Swift接入例子 」中介绍了Swift项目如何接入SOT。但是要求SDK解压到特定目录中,编译配置的路径也是绝对路径,不适合多人协作合开。文本介绍适合多人开发协作的接入方法。


还是以开源的「 SwiftMessages 」Demo为例,该工程全部用Swift语言开发。这里把修改好的版本上传到了git上,分支为 「 sotcollaboration 」SotDebug接入免费版,SotRelease接入网站版,读者只需要进行下面的 Step1.配置编译环境 就可以直接用该分支测试。


现在开始从头讲解,git clone原本的工程后(我的路径为/Applications/SwiftMessages),命令行cd /Applications/SwiftMessages进入根目录,使用版本切换命令:git checkout 1e49de7b3780b699(因本文档制作于21年10月21号,以当日版本为准)。首先进入Demo目录,打开Demo.xcodeproj工程,scheme默认就已经选中了Demo:...


我使用的是Xcode12.4,可以直接编译成功,启动APP能看到画面(模拟器):


......


点击最上面的MESSAGE VIEW控件,会弹出一个错误提示窗口,今天我们就来用SOT热更的方式修改错误提示的文案:...


Step1: 配置编译环境


「 下载SOT的SDK 」,解压到项目目录下 /Applications/SwiftMessages/Demo/sotsdk...


在terminal运行命令:sh /Applications/SwiftMessages/Demo/sotsdk/compile-script/install.sh安装SOT编译工具链,需要输入密码。


用文本编辑器打开 /Applications/SwiftMessages/Demo/sotsdk/project-script/sotconfig.sh,修改EnableSot=1:...新版SDK已经不会再使用sotconfig.sh里的sdkdir,sotbuilder和objbuilder路径了,所以不用修改这些配置了,删掉也可以。


Step2: 增加Configuration


增加两个Configuration,只有切换到这两个Configuration才使用SOT编译模式,平时还是用原来的Configuration做开发,步骤如下:



  1. 选中Demo Project,然后选择Info面板,点击Configurations的下面加号,复制Debug的编译配置,并且命名为SotDebug,用来接入免费版的SOT。再选择复制Release编译配置,命名为SotRelease,用来配置网站版的SOT,注意名字都不要留有空格:...加完就是:...

  2. SwiftMessages也加上这两个Configuration:...


注意:读者应用到自己项目中时,需要把所有的工程都加上这两个Configuration,否则编译会报找不到文件等等的错误。所以加完这两个Configuration的之后,就马上切换到它们去Build和Run一下,看是否有编译错误,如果没有再进行下面的操作,如果有,请检查是否漏了一些工程没有添加上。


Step3: 修改编译选项


添加热更需要的编译选项,添加SOT虚拟机静态库等,步骤如下:




  1. 选中Demo工程,然后选择Demo这个Target,再选择Build Settings:...




  2. Other Linker FlagsSotDebug中添加-sotmodule $(PRODUCT_NAME) sotsdk/libs/libsot_free.a -sotsaved $(SRCROOT)/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/sotsdk/project-script/sotconfig.sh


    SotRelease中添加-sotmodule $(PRODUCT_NAME) sotsdk/libs/libsot_web.a -sotsaved $(SRCROOT)/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/sotsdk/project-script/sotconfig.sh


    每个选项的意义如下:



    • -sotmodule是module的名字,可以直接用$(PRODUCT_NAME),也可以自定义名字,名字不要有空格

    • -sotsaved是编译中间产物保存的目录,补丁自动化生成需要对比前后编译的产物来生成补丁

    • -sotconfig指定了项目sotconfig.sh的路径,该脚本控制sot编译器的工作

    • sotsdk/libs/libsot_free.a是SOT虚拟机静态库的路径,链接的是免费版的虚拟机

    • sotsdk/libs/libsot_web.a是SOT虚拟机静态库的路径,链接的是网站版的虚拟机




  3. Other C Flags以及Other Swift Flags的SotDebug和SotRelease下添加-sotmodule $(PRODUCT_NAME) -sotconfig $(SRCROOT)/sotsdk/project-script/sotconfig.sh,意义跟上一步是一样的,需要保持一致。经过上面两步,相关的编译配置结果如下图:...




  4. Preprocessor Macros添加USE_SOT=1,后面用来控制是否编译调用SDK的代码...




  5. 因为SOT SDK库文件编译时不带Bitcode,所以也需要把Enable Bitcode设为No...




  6. 为了模拟器架构时不编译arm64,给SotRelease增加如下配置...或者把Build Active Architecture Only设为Yes




Step4: 增加拷贝补丁脚本


SDK里提供了一个便利脚本,路径在sdk目录的project-script/sot_package.sh,它会把生成的补丁拷贝到Bundle文件夹下,在每次项目编译成功时调用该脚本,添加步骤如下:


...


脚本内容为:



if [[ "$CONFIGURATION" == "SotDebug" || "$CONFIGURATION" == "SotRelease" ]];then
sh "$SOURCE_ROOT/sotsdk/project-script/sot_package.sh" "$SOURCE_ROOT/sotsdk/project-script/sotconfig.sh" "$SOURCE_ROOT/sotsaved/$CONFIGURATION" Demo
fi

复制代码

...


Based on dependency analysis的勾去掉。




Step5: 链接C++库


SOT需要压缩库和c++标准库的支持,还是在这个页面下,打开Link Binary With Libraries页...


点击加号,分别加入这两,libz.tbdlibc++.tbd...




Step6: 调用SDK API


需要用Swift代码调用OC代码,已经提供了一个样例代码在SDK的swift-call-objc目录中,先把callsot.h和callsot.m拷贝到Demo目录下...


再添加到Demo工程中。点击Xcode软件的File按钮,找到Demo目录下的callsot.h和callsot.m,接着点击Add Files to "Demo",如下图所示:...


点击Add按钮,添加后会弹出询问:是否创建桥接文件。点击按钮Create Bridging Header...


然后可以看到项目中多了3个文件,分别是callsot.h,callsot.m和Demo-Bridging-Header.h:...


打开Demo-Bridging-Header.h,加入一行代码#import "callsot.h"...


打开callsot.m,修改代码为


#import <Foundation/Foundation.h>
#import "callsot.h"
#import "../sotsdk/libs/SotWebService.h"
@implementation CallSot:NSObject
-(void) InitSot
{
#ifdef USE_SOT
#ifdef DEBUG
[SotWebService ApplyBundleShip];
#else

[SotWebService Sync:@"1234567" is_dev:false cb:^(SotDownloadScriptStatus status)
{
if(status == SotScriptStatusSuccess)
{
NSLog(@"SotScriptStatusSuccess");
}
else
{
NSLog(@"SotScriptStatusFailure");
}
}];

#endif
#endif
}
@end
复制代码

注意SotWebService.h的头文件路径不再依赖于绝对路径,并且代码里用了#ifdef USE_SOT宏来隔开API调用代码,不影响正常编译:...


打开AppDelegate.swift,加入两行代码let sot = CallSot()sot.initSot()...


注意:读者在应用到自己项目中时,以上这些配置的路径不要生搬硬套。例如找不到SotWebService.h文件,找不到sotconfig.sh文件等等,读者自己要清楚SDK的目录与自己工程目录的相对关系,灵活调整这些配置的路径。


测试热更-免费版


按上面配置完之后,先测试免费版热更功能


Step1: 热更注入



  1. Build Configuration切换到SotDebug...

  2. 确保sotconfig.sh的配置是,EnableSot=1以及GenerateSotShip=0,先Clean Build Folder一下,然后再Build:...


然后看编译日志的输出,Link日志可以看到run sot link等输出,会告诉你每个文件里哪些函数可以被热更等信息:......


项目编译成功了,该APP可以正常启动。同时它具备了热更能力,可以加载补丁改变程序的代码逻辑,下面介绍如何生成补丁来测试它。




Step2: 生成补丁


上一步进行了热更注入的编译,当时的代码保存到了Demo/sotsaved这个文件夹下,用来和新代码比较生成补丁。生成补丁步骤如下:



  1. 首先启动SOT生成补丁模式,修改sotconfig.shEnableSot=1GenerateSotShip=1

  2. ...

  3. 接下来直接在Xcode里修改源代码,把ViewController.swift文件的”Something is horribly wrong!“改成了”SOT is great“,修改前:...修改后:...

  4. Swift项目生成补丁,每次都需要先Clean项目,再Build项目。然后查看编译日志输出,可以看到生成了补丁并且被脚本拷贝到了Bundle目录下,可以展开Link Demo(x86_64)的编译日志:...点击展开后,可看到生成补丁的Link日志,日志里显示了函数demoBasics被修改了:...

  5. 生成出来的补丁原始文件保存到了Demo/sotsaved/SotDebug/x86_64/ship/ship.sot,还记得之前加了一个script到Build Phase中吗?它会每次编译结束时,会把这个补丁拷贝到了Bundle目录中,并且添加CPU架构到文件名中。可以在Bundle中看到这个补丁,至此完毕。




Step3: 加载补丁


启动APP,API会判断Bundle内是否有补丁,有则加载,加载成功的日志大概如下,提示有一个模块加载了热更补丁:...之后点击最上面的MESSAGE VIEW控件,发现弹出的文案变成了SOT is great:...


如果去Xcode断点调试demoBasics,会发现无法断住了,因为实际执行补丁代码的是SOT虚拟机。


顺便提一嘴,GenerateSotShip=1时,编译APP用的是保存在sotsaved目录下的代码,所以无论怎么修改Xcode里的代码,如果没有把补丁拷贝到Bundle目录里,那么APP都是最后一次GenerateSotShip=0热更注入时的样子。


如果怀疑,可以把拷贝补丁的Script脚本从Build Phases删除,可以发现GenerateSotShip=1怎么改代码都不会生效了。


注意:如果读者接入自己的一个很简单项目进行测试,例如设置某个控件的颜色,热更前是红色,修改后是绿色,发现无法生效。那是因为这样的项目太过于简单,寥寥几行代码。热更前没有访问过绿色的这个全局变量,在热更时也无法访问到了,SOT只能利用原有的能力,无法无中生有。所以不要这样测试,更具体的原因在「 热更能力-语言特性 」说明。通常完整的项目代码比较多,所以就不会有这样的缺陷。


接入网站版


按上面的教程,已经对APP实现了免费版和网站版的接入。它俩区别只是链接的库不一样,具体就是Other Linker Flags根据Configuration区别配置,SotRelease下接入了网站版。但除了APP接入了网站版SDK,还需要用配合网站来管理补丁的发布。


Step1: 注册网站



  1. 第一步当然是注册网站,成为会员。点击跳转注册页面,免费注册,注册需要验证邮箱,然后登录。

  2. 从导航栏进入我的APP:...

  3. 点击创建APP,弹出弹窗填写APP的名字:...

  4. 进入APP页面,点击右上角的创建新版本按钮,会弹出弹窗,需要选择网站版,SDK版本选择1.0,目前只有1.0版本,然后输入版本号,版本号可以是随意字符串,方便区分就行。...

  5. 创建版本成功后,点击版本,进入版本页面,左上角是唯一标识该版本的VersionKey,后面API接口需要这个Key。...


Step2: 修改VersionKey


打开callsot.m,修改网站版的Sync接口,第一个参数填入你在网站创建的版本的VersionKey。...至此,网站版热更就算接入完成了。


Step3: 测试网站热更




  • 网站版生成补丁的步骤免费版是一样的,需要经历热更注入->出包->修改代码->生成补丁,这里不再赘述。


    唯一不同的是,生成出来的补丁要上传到网站上,然后才能通过网络同步到手机上实现热更。通过之前的免费版教程,知道生成的补丁会被拷贝到Bundle目录下,所以去Bundle目录里就能找它,在Xcode导航栏里右键选择Products下的Demo.app,选择Show in Finder:...




  • 右键Demo文件,选择Show Package Contents:...




  • 找到目录下的sotship_arm64.sot,这里用手机测试,cpu是arm64类型,补丁名字带有cpu后缀,这就是补丁了:...




  • 回到网站的版本页面,点击右侧上传补丁按钮:...




  • 弹出页面里,真机的架构一般选择arm64,除非是老的armv7的机器,并把补丁文件拖到框里,点击上传:...




  • 上传成功并且补丁文件无异常(补丁最大支持5MB),则会添加成功,补丁默认是停用状态,需要点击编辑来启用它:...




  • 这里选择全量启用,点击下面的提交按钮,然后补丁就会成功启用了:...




  • 上一步更新了补丁状态,通常很快生效,但CDN有时也需要1到2分钟才能生效。之后手机打开APP,如果成功下载补丁和加载的话,就能看到下面的日志:...这里输出的md5也跟网站上的补丁md5是一致的。




  • 打开APP后,点击最上面的MESSAGE VIEW控件,发现弹出的文案变成了SOT is great:...




注意:使用网站版,需要考虑到网络传输延迟的问题,只有看到了下载补丁和成功加载补丁的日志之后,调用的函数才会使用热修后的函数。例如有的开发问我,首屏代码怎么无法热更生效?那是因为首屏代码调用的时机太早了,SOT去网站上拿补丁,是异步的,不会一直卡住等着,而且在异步线程中等待结果。在补丁没传输回来之前,首屏的代码都已经调用结束了,这种情况下当然调用的还是老的代码了。而免费版没有这个问题,因为免费版是同步加载补丁的,直接去Bundle里加载,不是异步的。


构建热更注入版本和构建补丁必须是同一台机器,同一个Xcode版本。例如上架前APP用Xcode12进行了热更注入,而之后用Xcode13来构建补丁,那么将得到无效甚至错误的补丁。请使用同一个版本Xcode。


Step4: 几点提示



  • 网站版跟免费版主要接入流程差不多,可以用免费版测试,功能通过测试之后再接入网站版。

  • 网站版需要有网络的情况下才能生效,如果手机没有网,即使之前已经下载过了补丁,也无法加载。

  • 网站版费用很低,日活10万的APP,一个月几百块就够了。

  • 网站版补丁和配置都放在CDN上,支持高并发。




非主Target接入热更


上面的教程都是针对主Target,也就是Demo。这个工程还有一个名为SwiftMessages的Framework,也可以热更,下面介绍如何配置。


可以看到SwiftMessages的Mach-O Type是Dynamic Library,通过下图方式查看得到:...


这种类型的话,配置相对麻烦些。还有一种是Static Library,配置起来会简单得多。但本例改成Static Library启动会崩溃,所以按Dynamic Library的方式来介绍。


Step1: 修改编译选项



  1. 选中SwiftMessages.xcodeproject工程,然后选择SwiftMessages这个Target,再选择Build Settings:...

  2. Other Linker FlagsSotDebug中添加-sotmodule $(PRODUCT_NAME) $(SRCROOT)/Demo/sotsdk/libs/libsot_free.a -sotsaved $(SRCROOT)/Demo/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh

  3. Other Linker FlagsSotRelease中添加-sotmodule $(PRODUCT_NAME) $(SRCROOT)/Demo/sotsdk/libs/libsot_web.a -sotsaved $(SRCROOT)/Demo/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh

  4. Other C Flags以及Other Swift FlagsSotDebugSotRelease中,添加-sotmodule $(PRODUCT_NAME) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh,意义跟上一步是一样的,需要保持一致。经过上面两步,相关的编译配置结果如下图:...这一步跟Demo的配置差不多,区别在于有些路径写法不一样,以达到复用Demo配置的目的,读者可以仔细比较一下。

  5. Preprocessor Macros添加USE_SOT=1,后面用来控制是否编译调用SDK的代码...

  6. 需要把Target的Enable Bitcode设为No...

  7. 为了模拟器架构时不编译arm64,给SotRelease增加如下配置...


Step2: 链接C++库


点击Build Phases页面,打开Link Binary With Libraries页,点击加号,分别加入这两,libz.tbdlibc++.tbd...


Step3: 调用SDK API


因为SwiftMessages是动态库,所以需要在它的编译文件中调用SDK的热更初始化接口。跟Demo一样,添加OC文件。先从Demo文件夹中复制callsot.h和callsot.m文件到SwiftMessages文件夹中...


选中SwiftMessages工程,点击Xcode软件的File按钮,接着点击Add Files to "SwiftMessages.xcodeproject",如下图所示:...


选择到SwiftMessages目录,同时选中callsot.h和callsot.m两个文件,勾选下面的Copy items if needed,勾选Add to targets:中的SwiftMessages target,如下图所示:...


点击Add按钮,然后可以看到项目中多了2个文件,分别是callsot.h,callsot.m,修改CallSot类名为CallSotMessage:...


去到右边面板,把文件属性改成public:...


打开callsot.m,做相应路径和类名的修改:...


打开Demo-Bridging-Header.h,加入一行代码#import "SwiftMessages/callsot.h"...


打开AppDelegate.swift,加入两行代码let sot1 = CallSotMessage()sot1.initSot()...


因为这时候有两个Target都可以生成补丁,Demo和SwiftMessages,需要修改拷贝补丁的脚本,加入SwiftMessages:...


Step4: 测试热更




  1. 测试热更的流程跟之前是一模一样的,只是输出的日志可能会有所区别,我们过一遍。EnableSot=1和GenerateSotShip=0热更注入,先Clean后Build,如果去看编译日志的Link SwiftMessages,也可以看到热更注入的信息。




  2. 然后修改MessageView.swift的代码,错误提示的文案会加上“SOT is great”:...




  3. GenerateSotShip=1开启生成补丁模式,Clean后Build,查看Link SwiftMessages日志,有提示该函数被热更:...




  4. 接下来可以看到补丁拷贝脚本日志输出的信息,这里它检测到有两个Target都生成了补丁文件,会把它们两个合成一个,拷贝到Bundle目录下:...




  5. 启动APP,会看到两条加载补丁的日志,因为我们Demo Target和SwiftMessages Target都调用了API接口:...




  6. 点击MESSAGE VIEW控件,可以看到错误提示文案后面多了“SOT is great”,热更成功:


    ...网站版的测试跟以前也是一样的,这里不再重复了。




Step5: 几点提示



  1. Dynamic Library的热更编译改法其实跟主Target,也就是Mach-O Type为Executable的改法是一样,只是这里复用了主Target的一些配置,例如sotsaved目录和sotconfig.sh的路径。增加再多的Target也可以按同样的改法修改它们。

  2. 补丁拷贝脚本只需要主Target有就行了,把要热更的sotmodule对应的名字加上即可,条件就是sotsaved目录必须是同一个。

  3. 如果需要接入网站版,那么每个需要热更的Target都需要调用API跟网站同步,它们的消耗是独立计费的。


Static Library的改法


上面说到Dynamic Library的改法步骤比较多,而且有诸多缺点,如果能把Framework的Mach-O Type改成Static Library是最好的,会少很多步骤和配置。由于本例无法修改,这里简单说一下步骤:



  1. Other Libraian Flags添加-sotmodule $(PRODUCT_NAME) -sotsaved $(SRCROOT)/Demo/sotsaved/$(CONFIGURATION)/$(CURRENT_ARCH) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh ,注意是Other Libraian Flags而不是Other Linker Flags了。还有这里比Dynamic Library加的配置少一个,即没有链接SDK的.a库文件了。

  2. Other C Flags以及Other Swift Flags添加-sotmodule $(PRODUCT_NAME) -sotconfig $(SRCROOT)/Demo/sotsdk/project-script/sotconfig.sh,这步跟之前是一模一样的。

  3. 需要把Target的Enable Bitcode设为No

  4. 修改拷贝补丁的脚本,加入该Target的名字,例如本例加入SwiftMessages,跟之前也是一模一样的:...


然后就配置完了,如果是使用网站版,同步一次消耗,就能实现所有Target的热更,修改简单,对包体影响最小。




总结


本文完整介绍Swift项目如何接入免费版和网站版。


本文的方式是把SDK拷贝到了工程文件夹里,让它可以跟随项目一起进行版本管理,路径也配置成了相对路径,更加灵活。


通过新增Configuration的方式,也做到了不影响原来的开发,Debug和Release相当于没有接入SOT,适合大多数开发平时使用。只需上线前改成SotRelease出包,就能让APP就得到热更能力。


作者:忒修斯科技
链接:https://juejin.cn/post/7033403091550470180
收起阅读 »

Java内存区域异常

一、内存区域划分Java程序执行时在逻辑上按照功能划分为不同的区域,其中包括方法区、堆区、Java虚拟机栈、本地方法栈、程序计数器、执行引擎、本地库接口、本地方法库几个部分,各个区域宏观结构如下,各部分详细功能及作用暂不展开论述。 JVM内存自动管理是Java...
继续阅读 »



一、内存区域划分

Java程序执行时在逻辑上按照功能划分为不同的区域,其中包括方法区、堆区、Java虚拟机栈、本地方法栈、程序计数器、执行引擎、本地库接口、本地方法库几个部分,各个区域宏观结构如下,各部分详细功能及作用暂不展开论述。

JVM内存自动管理是Java语言的一大优良特性,也是Java语言在软件市场长青的一大优势,有了JVM的自动内存管理,程序员不再为令人抓狂的内存泄漏担忧,但深入理解Java虚拟机内存区域依然至关重要,JVM内存自动管理并非永远万无一失,不当的使用也会造成OOM等内存异常,本文尝试列举Java内存溢出相关的常见异常并提供使用建议。

二、栈溢出

  • 方法调用深度过大

我们知道Java虚拟机栈及本地方法栈中存放的是方法调用所需的栈帧,栈帧是一种包括局部变量表、操作数栈、动态链接和方法返回地址的数据结构,每一次方法的调用和返回对应着压栈及出栈操作。Java虚拟机栈及本地方法栈由每个Java执行线程独享,当方法嵌套过深直至超过栈的最大空间时,当再次执行压栈操作,将抛出StackOverflowError异常。为演示栈溢出,假设存在如下代码:

/**
* 最大栈深度
*/
public class MaxInvokeDeep {
  private static int count = 0;

  /**
  * 无终止条件的递归调用,每次调用将进行压栈操作,超过栈最大空间后,将抛出stackOverFlow异常
  */
  public static void increment() {
      count++;
      increment();
  }

  public static void main(String[] args) {
      try {
          increment();
      } catch (Throwable e) {
          System.out.println("invokeDeep:" + count);
          e.printStackTrace();
      }
  }
}

对于示例中的increment()方法,每次方法调用将深度+1,通过不断的栈帧压栈操作,当栈中空间无法再进行扩展时,程序将抛出StackOverflowError异常。Java虚拟机栈的空间大小可以通过JVM参数调整,我们先设置栈空间大小为512K(-Xss512k),程序执行结果如下:(方法调用5355次后触发栈溢出) 我们再将栈空间缩小至256k(-Xss256k),程序执行结果如下:(方法调用2079次时将触发栈溢出): 由此可见,Java虚拟机栈的空间是有限的,当我们进行程序设计时,应尽量避免使用递归调用。生产环境抛出StackOverflowError异常时,可以排查系统中是否存在调用深度过大的方法。方法的不断嵌套调用,不但会占用更多的内存空间,也会影响程序的执行效率。当我们进行程序设计时需要充分考虑并设置合理的栈空间大小,一般情况下虚拟机默认配置即满足大部分应用场景。

  • 局部变量表过大

局部变量表用于存放Java方法中的局部变量,我们需要合理的设置局部变量,避免过多的冗余变量产生,否则可能会导致栈溢出,沿用刚刚的实例代码,我们定义一个创建大量局部变量的重载方法,则在栈空间不变的情况下(-Xss512k),创建大量局部变量的方法将降低栈调用深度,更容易触发StackOverflowError异常,通过分别执行increment及其重载方法,其中代码及执行结果如下:

package org.learn.jvm.basic;

/**
* 最大栈深度
*/
public class MaxInvokeDeep {
  private static int count = 0;

  /**
    * 无终止条件的递归调用,每次调用将进行压栈操作,超过栈最大空间后,将抛出stackOverFlow异常
    */
  public static void increment() {
      count++;
      increment();
  }

  /**
    * 创建大量局部变量,导致局部变量表过大,影响栈调用深度
    *
    * @param a
    * @param b
    * @param c
    */
  public static void increment(long a, long b, long c) {
      long d = 1, e = 2, f = 3, g = 4, h = 5, i = 6,
              j = 7, k = 8, l = 9, m = 10;
      count++;
      increment(a, b, c);
  }

  public static void main(String[] args) {
      try {
          //increment();
          increment(1, 2, 3);
      } catch (Throwable e) {
          System.out.println("invokeDeep:" + count);
          e.printStackTrace();
      }
  }
}

执行increment()时,最大栈深度为5355(栈空间大小-Xss512k)

执行increment(1,2,3)时,最大栈深度为1487(栈空间大小-Xss512k)

通过对比我们知道,创建大量的局部变量将使得局部变量表膨胀从而引发StackOverflowError异常,程序设计时应当尽量避免同一个方法中包含大量局部变量,实在无法避免可以考虑方法的重构及拆分。

三、OOM

对于Java程序员而言,OOM算是比较常见的生产环境异常,OOM往往容易引发线上事故,因此有必要梳理常见可能导致OOM的场景,并尽量规避保障服务的稳定性。

  • 创建大量的类

方法区主要的职责是用于存放类型相关信息,如类名、接口名、父类、访问修饰符、字段描述、方法描述等,Java8以后方法区从永久区移到了Metaspace,若系统中创建过多的类时,可能会引发OOM。Java开发中经常会利用字节码增强技术,通过创建代理类从而实现系统功能,以我们熟知的Spring框架为例,经常通过Cglib运行时创建代理类实现类的动态性,通过设置虚拟机参数,-XX:MaxMetaspaceSize=10M,则示例程序运行一段时间时间后,将触发Full GC并最终抛出OOM异常,因此日常开发过程中要特别留意,系统中创建的类的数量是否合理,以免产生OOM。实例代码如下:

/**
* 大量创建类导致方法区OOM
*/
public class JavaMethodAreaOOM {
  /**
    * VM Args: -XX:MaxMetaspaceSize=10M(设置Metaspace空间为10MB)
    *
    * @param args
    */
  public static void main(String[] args) {
      while (true) {
          Enhancer enhancer = new Enhancer();
          enhancer.setSuperclass(OOMObject.class);
          enhancer.setUseCache(false);
          enhancer.setCallback(new MethodInterceptor() {
              @Override
              public Object intercept(Object obj, Method method,
                                      Object[] args, MethodProxy proxy) throws Throwable {
                  return proxy.invokeSuper(obj, args);
              }
          });
          Object object = enhancer.create();
          System.out.println("hashcode:" + object.hashCode());
      }
  }

  /**
    * 对象
    */
  static class OOMObject {

  }
}

设置虚拟机参数:-Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails 限定元数据区为10M,则虚拟机Full GC之后,将引发OOM异常,程序执行结果如下:

  • 创建大对象

Java堆内存空间非常宝贵,若系统中存在数组、复杂嵌套对象等大对象,将会引发FullGc并最终引发OOM异常,因此程序设计时需要合理设置类结构,避免产生过大的对象,示例代码如下:

/**
* 堆内存溢出
*/
public class HeapOOM {
  /**
    *
    */
  static class InnerClass {
      private int value;
      private byte[] bytes = new byte[1024];
  }

  /**
    * VM Args: -Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails
    *
    * @param args
    */
  public static void main(String[] args) {
      List<InnerClass> innerClassList = new ArrayList<>();
      while (true) {
          innerClassList.add(new InnerClass());
      }
  }
}

设置虚拟机参数:-Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails 限定最大堆内存空间为10M,则虚拟机Full GC之后,将引发OOM异常,程序执行结果如下。

  • 常量池溢出

Java8开始,常量池从永久区移至堆区,当系统中存在大量常量并超过常量池最大容量时,将引发OOM异常。String类的intern()方法内部逻辑是:若常量池中存在字符串常量,则直接返回字符串引用,否则创建常量将其加入常量池中并返回常量池引用。通过每次创建不同的字符串,常量池将因为无法容纳新创建的字符串常量而最终引发OOM,示例代码如下:

/**
* VM Args: -Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails
*
* @param args
*/
public static void main(String[] args) {
  Set<String> set = new HashSet<>();
  int i = 0;
  while (true) {
      set.add(String.valueOf(i++).intern());
  }
}

设置虚拟机参数:-Xms1M -Xmx1M -verbose:gc -XX:+PrintGCDetails 限定最大堆内存空间为10M,则虚拟机Full GC之后,将引发OOM异常,程序执行结果如下。

  • 直接内存溢出

直接内存是因NIO技术而引入的独立于堆区的一块内存空间,对于读写频繁的场景,通过直接操作直接内存可以获得更高的执行效率,但其内存空间受制于操作系统本地内存大小,超过最大限制后,也将抛出OOM 异常,以下代码通过UnSafe类,直接操作内存,程序执行一段时间后,将引发OOM异常。

public class DirectMemoryOOM {
  private static final int _1MB = 1024 * 1024;

  /**
    * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
    * @param args
    * @throws IllegalAccessException
    */
  public static void main(String[] args) throws IllegalAccessException {
      Field field = Unsafe.class.getDeclaredFields()[0];
      field.setAccessible(true);
      Unsafe unsafe = (Unsafe) field.get(null);
      while (true) {
          unsafe.allocateMemory(_1MB*1024);
      }
  }
}

设置虚拟机参数:-Xmx20M -XX:MaxDirectMemorySize=10M 限定最大直接内存空间为10M,程序最终执行结果如下:

四、总结

本文通过实际的案例列举了常见的OOM异常,由此我们可以得知,程序设计过程中应当合理的设置栈区、堆区、直接内存的大小,从实际情况出发,合理设计数据结构,从而避免引发OOM故障,此外通过分析引发OOM的原因也有利于我们针对深入理解JVM并对现有系统进行系统调优。

作者:洞幺幺洞
来源:https://juejin.cn/post/7043442191619850277

收起阅读 »

如何用JavaScript实现双向映射?

本文翻译自 《How to create a Bidirectional Map in JavaScript》双向映射是指在键值对中建立双向一一对应关系的一种模式。它既可以通过键名(key)去获取值(value),也可以通过值去获取键名。让我们看下如何在Jav...
继续阅读 »



本文翻译自 《How to create a Bidirectional Map in JavaScript》

双向映射是指在键值对中建立双向一一对应关系的一种模式。它既可以通过键名(key)去获取值(value),也可以通过值去获取键名。让我们看下如何在JavaScript中实现一个双向映射,以及 TypeScript 中的应用。

双向映射背后的计算机科学与数学

首先看一下双向映射的基本定义:

在计算机科学中,双向映射是由一一对应的键值对组成的数据结构,因此在每个方向都可以建立二元关系:每个值也可以对应唯一的键。

百科指路双向映射

计算机科学中的双向映射,源于数学上的双射函数。双射函数是指两个集合中的每个元素,都可以在另一个集合中找到与之匹配的另一个元素,反之也可以通过后者找到匹配的前者,因此也被叫做可逆函数。

百科指路: 双射函数

扩展:

  • 单射(injection):每一个x都有唯一的y与之对应;

  • 满射(surjection):每一个y都必有至少一个x与之对应;

  • 双射(又叫一一对应,bijection):每一个x都有y与之对应,每一个y都有x与之对应。

根据上面的说明,一个简单的双射函数就像这样:

f(1) = 'D';
f(C) = 3;

另外,双射函数需要两个集合的长度相等,否则会失败。

初始化双向映射

我们可以在JavaScript 中创建一个类来初始化键值对:

const bimap = new BidirectionalMap({
 a: 'A',
 b: 'B',
 c: 'C',
})

在类里面,我们将会创建两个列表,一个用来处理正向映射,存放初始化对象的副本;另一个用来处理逆向映射,存放的内容是「键」「值」翻转后的初始化对象。

class BidirectionalMap {
 fwdMap = {}
 revMap = {}

 constructor(map) {
     this.fwdMap = { ...map }
     this.revMap = Object.keys(map).reduce(
        (acc, cur) => ({
             ...acc,
            [map[cur]]: cur,
        }),
        {}
    )
}
}

注意,由于初始对象本身的性质,你不能用数字当 key,但可以作为值来使用。

const bimap = new BidirectionalMap({
 a: 42,
 b: 'B',
 c: 'C',
})

如果不满足于此,也有更强大健壮的实现方式,按照 JavaScript 映射数据类型 中允许使用数字、函数甚至NaN来作为 key 的规范来实现,当然这会更加复杂。

通过双向映射获取元素

现在,我们有了一个包含两个对象的数据结构,它们互为键值对的镜像。我们现在需要一个方法来取出元素,让我们来实现一个 get() 函数:

 get( key ) {
   return this.fwdMap[key] || this.revMap[key]
}

这个方法非常简单: 如果正向映射里存在就返回,否则返回逆向映射,都没有就返回 undefined

试一下获取元素:

console.log(bimap.get('a')) // displays A
console.log(bimap.get('A')  // displays a

给双向映射添加元素

目前映射还无法添加元素,我们创建一个添加方法:

add(pair) {
   this.fwdMap[pair[0]] = pair[1]
   this.revMap[pair[1]] = pair[0]
}

add 函数接收一个双元素数组(在TypeScript 中叫做元组),按不同键值顺序加入到相应对象中。

现在我们可以添加和读取映射中的元素了:

bimap.add(['d', 'D'])
console.log( bimap.get('D') ) // displays d

在TypeScript中安全使用双向映射

为了确保数据类型安全,我们可以在 TypeScript 中进行改写,对输入类型进行检查,例如初始化的映射必须为一个通用对象,添加的元素必须为一个 元组

class BidirectionalMap {
 fwdMap = {}
 revMap = {}

 constructor(map: { [key: string]: string }) {
     this.fwdMap = { ...map }
     this.revMap = Object.keys(map).reduce(
        (acc, cur) => ({
             ...acc,
            [map[cur]]: cur,
        }),
        {}
    )
}

 get(key: string): string | undefined {
     return this.fwdMap[key] || this.revMap[key]
}

 add(pair: [string, string]) {
   this.fwdMap[pair[0]] = pair[1]
   this.revMap[pair[1]] = pair[0]
}
}

这样我们的映射就更加安全和完美了。在这里,我们的 key 和 value 都必须使用字符串。


翻译:sherryhe
来源:https://juejin.cn/post/6976797991277428750

收起阅读 »

消息队列的使用场景是什么样的?

本文从异步、解耦、削峰填谷等核心应用场景,以及消息中间件常用协议、推拉模式对比来解答此问题。什么是消息中间件作为一种典型的消息代理组件(Message Broker),是企业级应用系统中常用的消息中间件,主要应用于分布式系统或组件之间的消息通讯,提供具有可靠、...
继续阅读 »



本文从异步、解耦、削峰填谷等核心应用场景,以及消息中间件常用协议、推拉模式对比来解答此问题。

什么是消息中间件

作为一种典型的消息代理组件(Message Broker),是企业级应用系统中常用的消息中间件,主要应用于分布式系统或组件之间的消息通讯,提供具有可靠、异步和事务等特性的消息通信服务。应用消息代理组件可以降低系统间耦合度,提高系统的吞吐量、可扩展性和高可用性。

分布式消息服务主要涉及五个核心角色,消息发布者(Publisher)、可靠消息组件(MsgBroker)、消息订阅者(Subscriber)、消息类型(Message Type)和订阅关系(Binding),具体描述如下:

  1. 消息发布者,指发送消息的应用系统,一个应用系统可以发送一种或者多种消息类型,发布者发送消息到可靠消息组件 (MsgBroker)。

  2. 可靠消息组件,即 MsgBroker,负责接收发布者发送的消息,根据消息类型和订阅关系将消息分发投递到一个或多个消息订阅者。整个过程涉及消息类型校验、消息持久化存储、订阅关系匹配、消息投递和消息恢复等核心功能。

  3. 消息订阅者,指订阅消息的应用系统,一个应用系统可以订阅一种或者多种消息类型,消息订阅者收到的消息来自可靠消息组件 (MsgBroker)。

  4. 消息类型:一种消息类型由 TOPIC 和 EVENTCODE 唯一标识。

  5. 订阅关系,用来描述一种消息类型被订阅者订阅,订阅关系也被称为 Binding。

核心功能特色

可为不同应用系统间提供可靠的消息通信,降低系统间耦合度并提高整体架构的可扩展性和可用性。

可为不同应用系统间提供异步消息通信,提高系统吞吐量和性能。

发布者系统、消息代理组件以及订阅者系统均支持集群水平扩展,可依据业务消息量动态部署计算节点。

支持事务型消息,保证消息与本地数据库事务的一致性。

远程调用RPC和消息MQ区别

谈到消息队列,有必要看下RPC和MQ的本质区别,从两者的定义和定位来看,RPC(Remote Procedure Call)远程过程调用,主要解决远程通信间的问题,不需要了解底层网络的通信机制;消息队列(MQ)是一种能实现生产者到消费者单向通信的通信模型。核心区别在于RPC是双向直接网络通讯,MQ是单向引入中间载体的网络通讯。单纯去看队列,队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。在队列前面增加限定词“消息”,意味着通过消息驱动来进行整体的架构实现。RPC和MQ本质上是网络通讯的两种不同的实现机制,RPC同步等待结果对比于MQ在异步、解耦、削峰填谷等上的特征显著差异主要有以下几点差异:

  1. 在架构上,RPC和MQ的差异点是,Message有一个中间结点Message Queue,可以把消息存储起来。

  2. 同步调用:对于要立即等待返回处理结果的场景,RPC是首选。

  3. MQ的使用,一方面是基于性能的考虑,比如服务端不能快速的响应客户端(或客户端也不要求实时响应),需要在队列里缓存;另外一方面,它更侧重数据的传输,因此方式更加多样化,除了点对点外,还有订阅发布等功能。

  4. 随着业务增长,有的处理端调用下游服务太多或者处理量会成为瓶颈,会进行同步调用改造为异步调用,这个时候可以考虑使用MQ。

核心应用场景

针对MQ的核心场景,我们从异步、解耦、削峰填谷等特性进行分析,区别于传统的RPC调用。尤其在引入中间节点的情况下,通过空间(拥有存储能力)换时间(RPC同步等待响应)的思想,增加更多的可能性和能力。

异步通信

针对不需要立即处理消息,尤其那种非常耗时的操作,通过消息队列提供了异步处理机制,通过额外的消费线程接管这部分进行异步操作处理。

解耦

在应用和应用之间,提供了异构系统之间的消息通讯的机制,通过消息中间件解决多个系统或异构系统之间除了RPC之外另一种单向通讯的机制。

扩展性

因为消息队列解耦了主流程的处理过程,只要另外增加处理过程即可,不需要改变代码、不需要调整参数,便于分布式扩容。

分布式事务一致性

在2个应用系统之间的数据状态同步,需要考虑数据状态的最终一致性的场景下,利用消息队列所提供的事务消息来实现系统间的数据状态一致性。

削峰填谷

在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量无法提前预知;如果为了能处理这类瞬间峰值访问提前准备应用资源无疑是比较大的浪费。使用消息队列在突发事件下的防脉冲能力提供了一种保障,能够接管前台的大脉冲请求,然后异步慢速消费。

可恢复性

系统的一部分组件失效时,不会影响到整个系统。消息队列降低了应用间的耦合度,所以即使一个处理消息的应用挂掉,加入队列中的消息仍然可以在系统恢复后被处理。

顺序保证

在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来进行处理。

大量堆积

通过消息堆积能力处理数据迁移场景,针对旧数据进行全量迁移的同时开启增量消息堆积,待全量迁移完毕,再开启增量,保证数据最终一致性且不丢失。

数据流处理

分布式系统产生的海量数据流,如:业务日志、监控数据、用户行为等,针对这些数据流进行实时或批量采集汇总,然后导入到大数据实时计算引擎,通过消息队列解决异构系统的数据对接能力。

业界消息中间件对比

详细的对比可以参考:blog.csdn.net/wangzhipeng…

消息中间件常用协议

AMQP协议

AMQP即Advanced Message Queuing Protocol,提供统一消息服务的高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。

优点:可靠、通用

MQTT协议

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和致动器(比如通过Twitter让房屋联网)的通信协议。

优点:格式简洁、占用带宽小、移动端通信、PUSH、嵌入式系统

STOMP协议

STOMP(Streaming Text Orientated Message Protocol)是流文本定向消息协议,是一种为MOM(Message Oriented Middleware,面向消息的中间件)设计的简单文本协议。STOMP提供一个可互操作的连接格式,允许客户端与任意STOMP消息代理(Broker)进行交互。

优点:命令模式(非topic/queue模式)

XMPP协议

XMPP(可扩展消息处理现场协议,Extensible Messaging and Presence Protocol)是基于可扩展标记语言(XML)的协议,多用于即时消息(IM)以及在线现场探测。适用于服务器之间的准即时操作。核心是基于XML流传输,这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息,即使其操作系统和浏览器不同。

优点:通用公开、兼容性强、可扩展、安全性高,但XML编码格式占用带宽大

基于TCP/IP自定义的协议

有些特殊框架(如:redis、kafka、rocketMQ等)根据自身需要未严格遵循MQ规范,而是基于TCP/IP自行封装了一套二进制编解码协议,通过网络socket接口进行传输,实现了MQ的标准规范相关功能。

消息中间件推和拉模式对比

Push推模式:服务端除了负责消息存储、处理请求,还需要保存推送状态、保存订阅关系、消费者负载均衡;推模式的实时性更好;如果push能力大于消费能力,可能导致消费者崩溃或大量消息丢失

Push模式的主要优点是:

  1. 对用户要求低,方便用户获取需要的信息

  2. 及时性好,服务器端即时地向客户端推送更行的动态信息

Push模式的主要缺点是:

  1. 推送的信息可能并不能满足客户端的个性化需求

  2. Push消息大于消费者消费速率额,需要有协调QoS机制做到消费端反馈

Pull拉模式:客户端除了消费消息,还要保存消息偏移量offset,以及异常情况下的消息暂存和recover;不能及时获取消息,数据量大时容易引起broker消息堆积。

Pull拉模式的主要优点是:

  1. 针对性强,能满足客户端的个性化需求

  2. 客户端按需获取,服务器端只是被动接收查询,对客户端的查询请求做出响应

Pull拉模式主要的缺点是:

  1. 实时较差,针对于服务器端实时更新的信息,客户端难以获取实时信息

  2. 对于客户端用户的要求较高,需要维护位点

相关资料

建议学习以下的技术文档,了解更多详细的技术细节和实现原理,加深对消息中间件的理解和应用,同时可以下载开源的源代码,本地调试相应的代码,加深对技术原理的理解和概念的掌握,以及在实际生产中更多的掌握不同的消息队列应用的场景下,高效和正确地使用消息中间件。

RocketMQ资料: github.com/apache/rock…

Kafka资料: kafka.apache.org/documentati…

阿里云RocketMQ文档: help.aliyun.com/document_de…


作者:阿里巴巴淘系技术
来源:https://juejin.cn/post/7025955365812437028

收起阅读 »

业务实战中经典算法的应用

有网友提问:各种机器学习算法的应用场景分别是什么(比如朴素贝叶斯、决策树、K 近邻、SVM、逻辑回归最大熵模型)?这些在一般工作中分别用到的频率多大?一般用途是什么?需要注意什么?根据问题,核心关键词是基础算法和应用场景,比较担忧的点是这些基础算法能否学有所用...
继续阅读 »



有网友提问:各种机器学习算法的应用场景分别是什么(比如朴素贝叶斯、决策树、K 近邻、SVM、逻辑回归最大熵模型)?

这些在一般工作中分别用到的频率多大?一般用途是什么?需要注意什么?

根据问题,核心关键词是基础算法和应用场景,比较担忧的点是这些基础算法能否学有所用?毕竟,上述算法是大家一上来就接触的,书架上可能还放着几本充满情怀的《数据挖掘导论》《模式分类》等经典书籍,但又对在深度学习时代基础算法是否有立足之地抱有担忧。

网上已经有很多内容解答了经典算法的基本思路和理论上的应用场景,这些场景更像是模型的适用范围,这与工业界算法实际落地中的场景其实有很大区别。

从工业界的角度看,业务价值才是衡量算法优劣的金钥匙,而业务场景往往包含业务目标、约束条件和实现成本。如果我们只看目标,那么前沿的算法往往占据主导,可如果我们需兼顾算法运行复杂度、快速迭代试错、各种强加的业务限制等,经典算法往往更好用,因而就占据了一席之地。

针对这个问题,淘系技术算法工程师感知同学写出本文详细解答。

在实际业务价值中,算法模型的影响面大致是10%

在工业界,算法从想法到落地,你不是一个人在战斗。我以推荐算法为例,假设我们现在接到的任务是支持某频道页feeds流的推荐。我们首先应该意识到对业务来说,模型的影响面大致是10%,其他几个重要影响因子是产品设计(40%)、数据(30%)、领域知识的表示和建模(20%)。

这意味着,你把普通的LR模型升级成深度模型,即使提升20%,可能对业务的贡献大致只有2%。当然,2%也不少,只是这个折扣打的让人脑壳疼。

当然,上面的比例划分并不是一成不变的,在阿里推荐算法元年也就是2015年起,个性化推荐往往对标运营规则,你要是不提升个20%都不好意思跟人打招呼。那么一个算法工程师的日常除了接需求,就是做优化:业务给我输入日志、特征和优化目标,剩下的事情就交给我吭哧吭哧。

可随着一年年的水涨船高,大家所用模型自然也越来越复杂,从LR->FTRL->WDL->DeepFM->MMOE,大家也都沿着前辈们躺过的路径一步步走下去。这时候你要是问谁还在用LR或是普通的决策树,那确实会得到一个尴尬的笑容。

但渐渐的,大家也意识到了,模型优化终究是一个边际收益递减的事情。当我们把业务方屏蔽在外面而只在一个密闭空间中优化,天花板就已经注定。于是渐渐的,淘系的推荐慢慢进入第二阶段,算法和业务共建阶段。业务需求和算法优化虽然还是分开走,但已经开始有融合的地方。

集团对算法工程师的要求也在改变:一个高大上的深度模型,如果不能说清楚业务价值,或带来特别明显提升,那么只能认为是自嗨式的闭门造车。这时,一个优秀的算法工程师,需要熟悉业务,通过和业务反复交流中,能够弄清楚业务痛点。

注意,业务方甚至可能当局者迷,会提出既要又要还要的需求给你,而你需要真正聚焦到那个最值得做的问题上。然后,才是对问题的算法描述。做到这一步你会发现,并不是你来定哪个模型牛逼,而是跟着问题走来选择模型。这个模型的第一版极大可能是一个经典算法,因为,你要尽快跑通链路,快速验证你的这个idea是有效的。后面的模型迭代提升,只是时间问题。

经典算法在淘系的应用场景示例:TF-IDF、K近邻、朴素贝叶斯、逻辑回归等

因而现阶段,在淘系的大多数场景中,并不是算法来驱动业务,而是配合业务一起来完成增长。一个只懂技术的算法工程师,最多只能拿到那10%的满分。为了让大家有体感,这里再举几个小例子:

比如业务问题是对用户进行人群打标,人群包括钓鱼控、豆蔻少女、耳机发烧友、男神style等。在实操中我们不仅需考虑用户年龄、性别、购买力等属性,还需考虑用户在淘系的长期行为,从而得到一个多分类任务。如果模型所用的的特征是按月访问频次,那么豆蔻少女很可能网罗非常多的用户,因为女装是淘系行为频次最多的类目。

比如,某用户对耳机发烧友和豆蔻少女一个月内都有4次访问,假设耳机发烧友人均访问次数是3.2次,而豆蔻少女是4.8次,那么可知该用户对耳机发烧友的偏好分应更高。因此,模型特征不仅应该使用用户对该人群的绝对行为频次,还需参照大盘的水位给出相对行为频次。

这时,入选吴军老师《数学之美》的TF-IDF算法就派上用场了。通过引入TF-IDF构建特征,可以显著提高人群标签的模型效果,而TF-IDF则是非常基础的文本分类算法。

在淘系推荐场景,提升feeds流的点击率或转化率往往是一个常见场景。可业务总会给你惊喜:比如商品的库存只有一件(阿里拍卖),比如推荐的商品大多是新品(天猫新品),比如通过小样来吸引用户复购正品,这些用户大多是第一次来(天猫U先),或者是在提升效率的同时还需兼顾类目丰富度(很多场景)。

在上述不同业务约束背景,才是我们真实面对的应用场景。面对这种情况首先是定方向,比如在阿里拍卖中的问题可描述为“如何在浅库存约束下进行个性化推荐”。假如你判断这是一个流量调控问题,就需要列出优化目标和约束条件,并调研如何用拉格朗日乘子法求解。重要的是,最终的结果还需要和个性化推荐系统结合。详见:阿里拍卖全链路导购策略首次揭秘

面对上述应用场景,你需明白你的战略目标是证明浅库存约束下的推荐是一个流量调控问题,并可以快速验证拿到效果。采用一个成熟经典的方法先快速落地实验,后续再逐步迭代是明智的选择。

又比如,K近邻算法似乎能简单到能用一张图或一句话来描述。可它在解决正负样本不均衡问题中就能派上用场。上采样是说通过将少数类(往往是正样本)的数据复制多份,但上采样后存在重复数据集可能会导致过拟合。过拟合的一种原因是在局部范围正负样本比例存在差异,如下图所示:

我们只需对C类局部样本进行上采样,这其中就运用到了K近邻算法。本例中的经典算法虽然只是链路的一部分,甚至只是配角儿,但离了它不行。

朴素贝叶斯虽然简单了点,但贝叶斯理论往后的发展,包括贝叶斯网络、因果图就不那么simple了。比如不论是金融业务中的LR评分卡模型,或是推荐算法精排中的深度模型,交叉特征往往由人工经验配置,这甚至是算法最不自动的一个环节。

使用贝叶斯网络中的structure learning,并结合业务输入的行业知识,构建出贝叶斯概率图,并从中发现相关特征做交叉,会比人工配置带来一定提升,也具有更好的可解释性。这就是把业务领域知识和算法模型结合的一个很好的例子。可如果贝叶斯理论不扎实,很难走到这一步。

不论深度学习怎么火,LR都是大多场景的一个backup。比如在双十一大促投放场景中,如果在0:00~0:30这样的峰值期,所有场景都走深度模型,机器资源肯定不足,这时候就需要做好一个使用LR或FTRL的降级预案。

如何成为业界优秀的算法工程师?

在淘系,算法并不是孤立的存在,算法工程师也不只是一个闭关的剑客。怎么切中业务痛点,快速验证你的idea,怎么进行合适的算法选型,这要求你有很好的算法基本功,以及较广的算法视野。一个模型无论搞的多复杂最终的回答都是业务价值,一个有良好基本功、具备快速学习能力,且善于发掘业务价值的算法工程师,将有很大成长空间。

好吧,假设上面就是我们的职业目标,可怎么实现呢。元(ye)芳(jie),你怎么看?其实只有一个句话,理论和实践相结合

理论

对算法基本思路的了解,查查知乎你可能只需要20分钟,可你忘记它可能也只需要2周。这里的理论是你从内而外对这个算法的感悟,比如说到决策树,你脑海里好像就能模拟出它的信息增益计算、特征选择、节点分裂的过程,知道它的优和劣。最终,都是为了在实践中能快速识别出它就是最适合解决当前问题的模型。

基本功弄扎实是一个慢活儿,方法上可以是看经典原著、学习视频分享或是用高级语言实现。重要的是心态,能平心静气,不设预期,保持热情;另外,如果你有个可以分享的兴趣小组,那么恭喜你。因为学习书籍或看paper的过程其实挺枯燥的,但如果有分享的动力你可以走的更远。

实践

都知道实践出真知,可实践往往也很残酷。因为需求和约束多如牛毛,问题需要你来发现,留给你的时间窗口又很短暂。也就是,算法是一个偏确定性、有适用边界、标准化的事情;而业务则是发散的、多目标的、经验驱动的事情。

你首先是需要有双发现的眼睛,找到那个最值得发力的点,这个需要数据分析配合业务经验,剥丝抽茧留下最主要的优化目标、约束条件,尽量的简化问题;其次是有一张能说会道的嘴,要不然业务怎么愿意给你这个时间窗口让你尝试呢;最后是,在赌上你的人设之后,需要尽快在这个窗口期做出效果,在这个压力之下,考验你算法基本功的时候就到了。

结语

最后,让大家猜个谜语:它是一个贪心的家伙,计算复杂度不大,可以动态的做特征选择,不论特征是离散还是连续;它还可以先剪枝或后剪枝避免过拟合;它融合了信息熵的计算,也可以不讲道理的引入随机因子;它可以孤立的存在,也完全可以做集成学习,还可以配合LR一起来解决特征组合的问题;对小样本、正负样本不均、在线数据流的学习也可以支持;所得到则是有很强可解释性的规则。它,就是决策树。

每一个基础算法,都好比是颗带有智慧的种子,这不是普通的锦上添花,而是石破天惊的原创思维。有时不妨傻傻的回到过去的年代,伴着大师原创的步伐走一走,这种原创的智慧对未来长期的算法之路,大有裨益。神经网络从种下到开花,将近50年;贝叶斯也正在开花结果的路上。下一个,将会是谁呢?


作者:阿里巴巴淘系技术
来源:https://juejin.cn/post/7025964095530598436

收起阅读 »

android 水波纹控件,仿京东语音评价动画

先上效果gradle 引用implementation 'com.maxcion:multwaveviewlib:1.0.0'https://github.com/Likeyong/MultWaveView第一种样式存在三层水波纹,所以要生成3个Wave对象,...
继续阅读 »



先上效果

gradle 引用

implementation 'com.maxcion:multwaveviewlib:1.0.0'

项目地址

https://github.com/Likeyong/MultWaveView

  • 第一种效果是多重水波纹的效果, 三层水波纹,每层水播放可以设置不同的颜色、波高与波宽

  • 第二种效果是 单层水波纹,并且支持从底部慢慢上涨的效果。 这里也可以设置为多层水波纹上涨效果,颜色、波高、波宽以及背景图都是可以定制

  • 第三种效果就是模仿京东语音评价动画,最底部的seekbar 是用来模拟语音输出的声音大小,声音越大波高越大,并且每条水波纹的粗细以及速度都不同

第一种和第二种效果的背景图是这样的, 如果设置了背景图,那么整个控件的大小就是背景图大小与xml中设置的大小无关

List<Wave> waves = new ArrayList<>();
       Wave wave1 = new Wave(0, 150, 1, Color.parseColor("#00FFFF"), 3);
       Wave wave2 = new Wave(1f / 8, 150, 1, Color.parseColor("#6600FFFF"), 3);
       Wave wave3 = new Wave(1f / 4, 150, 1, Color.parseColor("#4400FFFF"), 3);
       waves.add(wave1);
       waves.add(wave2);
       waves.add(wave3);

       waveView.start(WaveArg.build()
              .setWaveList(waves)
              .setAutoRise(false)
              .setIsStroke(false)
              .setTransformBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.batman_logo))

第一种样式存在三层水波纹,所以要生成3个Wave对象,参数说明如下

设置完水波纹数据后,就可以开始动画了

waveView.start(WaveArg.build()
              .setWaveList(waves)//设置水波纹数据
              .setAutoRise(false)//设置水波纹是否自动上升
              .setIsStroke(false)//设置水波纹是实心的还是线条模式
              .setTransformBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.batman_logo))
               //设置背景图

      );

第二种样式:只有一层水波纹但是需要自动上涨,所以只用生成一条wave对象

List<Wave> waves = new ArrayList<>();
       Wave wave1 = new Wave(0, 150, 1, Color.parseColor("#00FFFF"), 3);
       waves.add(wave1);

waveView2.start(WaveArg.build()
              .setWaveList(waves)
              .setAutoRise(true)//设置自动上涨
              .setIsStroke(false)
              .setTransformBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.batman_logo))

      );

第三种样式,仅线条模式,并且三条水波纹粗细不一样,速度不一样,水波纹的粗细和速度都是通过Wave的构造函数设置的

List<Wave> waves = new ArrayList<>();
       Wave wave1 = new Wave(0, 150, 1, Color.parseColor("#000000"), 3);
       Wave wave2 = new Wave(0, 150, 3, Color.parseColor("#000000"), 3);
       Wave wave3 = new Wave(0, 150, 2, Color.parseColor("#000000"), 3);
       waves.add(wave1);
       waves.add(wave2);
       waves.add(wave3);

waveView3.start(WaveArg.build()
              .setWaveList(waves)
              .setAutoRise(false)
              .setIsStroke(true)

      );

这样写我们的三条粗细、速度都不同的水波纹动画就出来了,接下来就是根据声音大小来调整波高,通过 waveView.setWaveHeightMultiple(mult), 因为语音一直输入,声音大小也是一直变化的,科大讯飞的语音转写SDK中就有声音大小的回调,然后将回调出来的声音大小通过setWaveHeightMultiple,就可以实现波高动态改变了

作者:maxcion
来源:https://www.jianshu.com/p/9a770b0e68ff

收起阅读 »

Vue图片懒加载

1、问题在vue项目中,如果图片是从服务器端加载到页面上,图片较大的时候,就会存在一部分一部分加载的情况,会显示非常卡顿,影响体验。2、实现(1)、图片懒加载首先将图片的src链接设为一张我们已经准备好的图片(比如类似加载中的图片),并将其真正的图片地址存储在...
继续阅读 »

1、问题

在vue项目中,如果图片是从服务器端加载到页面上,图片较大的时候,就会存在一部分一部分加载的情况,会显示非常卡顿,影响体验。

2、实现

(1)、图片懒加载

首先将图片的src链接设为一张我们已经准备好的图片(比如类似加载中的图片),并将其真正的图片地址存储在img标签的自定义属性中。当js监听到该图片元素进入可视窗口时,即将自定义属性中的地址存储到src属性中,达到懒加载的效果。这样就可以缓解服务器压力,并且提高用户体验。

(2)、安装vue-lazyload
npm i vue-lazyload -S
(3)、在main.js中引入
import VueLazyload from "vue-lazyload";
Vue.use(VueLazyload,{
  preLoad: 1.3,
  loading: require('../src/assets/loading.gif'),
  attempt: 1
})

其中../src/assets/loading.gif是我本地的正在加载图片gif路径。

3、查看效果

在LazyLoad.vue中引入一张网络图片,在浏览器中限制网速,模拟图片加载缓慢的情况。

LazyLoad.vue

<template>
<div>
<img v-lazy=url1 alt="">
</div>
</template>
<script>
export default {
data (){
return{
url1: 'https://w.wallhaven.cc/full/pk/wallhaven-pkgkkp.png'
}
}
}
</script>

效果:

图片加载中:

图片加载完成:


常用参数:


作者:小绵杨Yancy
来源:https://blog.csdn.net/ZHANGYANG_1109/article/details/121868420

收起阅读 »

大白话讲解JavaScript 执行机制,一看就懂

JavaScript的运行机制所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。 为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是...
继续阅读 »



JavaScript的运行机制

1.JavaScript为什么是单线程?
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

2、执行机制相关知识点

  • 同步任务

  • 异步任务

举个栗子
有一天,张三要去做饭 这时候他要做两件事 分别是蒸米饭 和 炒菜 ,现在有两种方式去完成这个任务

A. 先去蒸米饭 然后等蒸米饭好了 再去抄菜 ---同步任务
B. 先去蒸米饭 然后等蒸米饭的过程中 再去抄菜 ---异步任务

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

具体来说,异步执行的运行机制如下:(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

总结: 同步任务在主线程执行,形成一个执行栈,执行栈中的所有同步任务执行完毕,就会去读取任务队列,就是对应的异步任务。

JavaScript的宏任务与微任务
除了广义上的定义,我们可以将任务进行更精细的定义,分为宏任务微任务

宏任务(macro-task):包括整体代码script脚本的执行,setTimeout,setInterval,ajax,dom操作,还有如 I/O 操作、UI 渲染等。

微任务(micro-task):Promise回调 node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。

主线程都从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为 Event Loop(事件循环)

我们解释一下这张图:

1、同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
2、当指定的事情完成时,Event Table会将这个函数移入Event Queue。
3、主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
上述过程会不断重复,也就是常说的Event Loop(事件循环)。(Event Loop是javascript的执行机制)

优先级
setTimeout()、setInterval()
setTimeout() 和 setInterval() 产生的任务是 异步任务,也属于 宏任务。
setTimeout() 接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。
如果将第二个参数设置为0或者不设置,意思 并不是立即执行,而是指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。(画重点)
所以说,setTimeout() 和 setInterval() 第二个参数设置的时间并不是绝对的,它需要根据当前代码最终执行的时间来确定的

Promise
Promise 相对来说就比较特殊了,在 new Promise() 中传入的回调函数是会 立即执行 的,但是它的 then() 方法是在 执行栈之后,任务队列之前 执行的,它属于 微任务。

process.nextTick
process.nextTick 是 Node.js 提供的一个与"任务队列"有关的方法,它产生的任务是放在 执行栈的尾部,并不属于 宏任务 和 微任务,因此它的任务 总是发生在所有异步任务之前。

setImmediate
setImmediate 是 Node.js 提供的另一个与"任务队列"有关的方法,它产生的任务追加到"任务队列"的尾部,它和 setTimeout(fn, 0) 很像,但优先级都是 setTimeout 优先于 setImmediate。
有时候,setTimeout 的执行顺序会在 setImmediate 的前面,有时候会在 setImmediate 的后面,这并不是 node.js 的 bug,这是因为虽然 setTimeout 第二个参数设置为0或者不设置,但是 setTimeout 源码中,会指定一个具体的毫秒数(node为1ms,浏览器为4ms),而由于当前代码执行时间受到执行环境的影响,执行时间有所起伏,如果当前执行的代码小于这个指定的值时,setTimeout 还没到推迟执行的时间,自然就先执行 setImmediate 了,如果当前执行的代码超过这个指定的值时,setTimeout 就会先于 setImmediate 执行。

通过上面的介绍,我们就可以得出一个代码执行的优先级:
同步代码(宏任务) > process.nextTick > Promise(微任务)> setTimeout(fn)、setInterval(fn)(宏任务)> setImmediate(宏任务)> setTimeout(fn, time)、setInterval(fn, time),其中time>0

面试回答
面试中该如何回答呢? 下面是我个人推荐的回答:
首先js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务
当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。
任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。
当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。

面试遇到的问题总结
1、同步和异步的区别是什么?分别举一个同步和异步的例子
同步会阻塞代码执行,而异步不会。alert是同步,setTimeout是异步

2、为何需要异步呢?
如果第一个示例中间步骤是一个 ajax 请求,现在网络比较慢,请求需要5秒钟。如果是同步,这5秒钟页面就卡死在这里啥也干不了了。

最后,前端 JS 脚本用到异步的场景主要有两个:

  • 定时 setTimeout setInverval

  • 网络请求,如 ajax 加载

  • 事件绑定

3、写出下图执行顺序

执行顺序是2431
在 new Promise() 中传入的回调函数是 立即执行 的,但是它的 then() 方法是在 执行栈之后,任务队列之前 执行的,
.then是回调函数,链式回调,会被放在挂起,等待执行栈的内容执行完后(输出4)再回调(输出3),最后执行异步的1

作者:我写的代码绝对没有问题
来源:https://www.jianshu.com/p/22641c97e351

收起阅读 »

9个问题带你一起了解什么是元宇宙,如何在现实生活中实现

2021 最火的新概念,莫过于元宇宙。2021 年 10 月 29 日,Facebook 宣布改名 Meta;2021 年 11 月 1 日,“元宇宙第一股” Roblox 经过短暂调整,宣布重新上线。元宇宙概念的热度可见一斑,国内外都在研究,我们程序员能否借...
继续阅读 »

2021 最火的新概念,莫过于元宇宙。2021 年 10 月 29 日,Facebook 宣布改名 Meta;2021 年 11 月 1 日,“元宇宙第一股” Roblox 经过短暂调整,宣布重新上线。元宇宙概念的热度可见一斑,国内外都在研究,我们程序员能否借用元宇宙概念进行技术延申,今天这篇文章或许会给到你一些启发。


一、什么是元宇宙?


1)元宇宙概念的提出


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


image.png


2)元宇宙八大要素


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


image.png


3)元宇宙第一股Roblox上市


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


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


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


image.png


二、元宇宙的架构?


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



  • 元宇宙的底层由基础设施与终端硬件设备组成:包括但不限于人机交互、3D引擎、GIS、设计工具、游戏渲染、画面渲染、隐私计算、AI、操作系统、工业互联网、内容分发、应用商店以及智能合约;

  • 在此基础上,元宇宙还需要大量的软件与技术协同:包括但不限于:基础设施端的5G、6G、云计算、区块链节点、边缘计算节点、DPU;用户端的路由器、传感器、芯片、VR头显、显示器、脑机接口;

  • 基于此,元宇宙可以衍生出相应的应用,并基于元宇宙各类应用发展出潜在的内容载体。


image.png


三、元宇宙的发展模式?


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



  • 技术端,区块链技术在不断演进;

  • 内容端,元宇宙概念的游戏不断增加,生态不断加强,用户数也随之增长。以Roblox、Sandbox为代表的UGC元宇宙概念游戏得益于玩家的参与而不断丰富自己游戏的内容;

  • 载体端,通信技术、虚拟现实、芯片等底层技术也在不断演进。


四、元宇宙的渗透路径?


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


1)游戏端:以Roblox为代表


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



  • 客户端:允许用户探索3D数字世界的应用程序。(面向用户);

  • 工作室:允许开发人员和创作者构建、发布和操作Roblox客户端访问的3D体验和其他内容的工具群。(面向开发者)

  • Roblox云:为共同体验平台提供动力的服务和基础设施。


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


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



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



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


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


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



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


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


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


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


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



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


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


1)网络(通信)


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


2)芯片(算力)


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


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


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



3)云与边缘计算


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


4)AI


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


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


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


1)英伟达:Omniverse平台


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



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



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


2)微软:数字孪生探索


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



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



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


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



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


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


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



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


1)个人信息可携带权


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


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


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



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


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


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


八、元宇宙是否需要NFT?


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


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


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



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


NFT由于自身的数字稀缺性被率先运用于收藏、艺术品以及游戏场景。51aspx认为,NFT在元宇宙中将扮演关键角色。


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


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


未来会发展成什么样还是未知的,但是目前的理念技术还是很值得我们去探讨。关于相应的源码可以点击查看51aspx.com


文章系转载:wallstreetcn.com/articles/36…


设计排版:51aspx.com


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

掀起的“元宇宙”热潮,能给我们带来什么?

近日超火的“元宇宙”,被说成是互联网的未来,它以各种姿势吸金,让创业族们看到闪着金光的未来。然而,关于元宇宙究竟是什么? 小编相信大多数人都有听过这一热词,但是都不清楚它究竟是什么? 那么,元宇宙究竟是什么呢?为什么互联网科技和数字科技等各大巨头纷纷入局元宇宙...
继续阅读 »

近日超火的“元宇宙”,被说成是互联网的未来,它以各种姿势吸金,让创业族们看到闪着金光的未来。然而,关于元宇宙究竟是什么? 小编相信大多数人都有听过这一热词,但是都不清楚它究竟是什么?


那么,元宇宙究竟是什么呢?为什么互联网科技和数字科技等各大巨头纷纷入局元宇宙?


AR.png 


准确来说,元宇宙到目前为止,尚无公认的定义 ,因为元宇宙不是一个新的概念,它算是一个原有的经典概念的重组词,是在扩展现实(XR)、区块链、云计算、数字孪生等新技术下的概念具化。同时也是集互联网、物联网、大数据、5G等为一体的运用,是虚拟世界与现实世界的结合,元宇宙的运用在未来会有很大的发展空间。


比特币.png


 


继续深入发掘,会了解到元宇宙一词诞生于1992年出版的科幻小说《雪崩》,小说里提到了“Met aver se(元宇宙)”和“Avat ar(化身)”两个概念。在小说描绘的虚拟世界里,人们拥有自己的虚拟替身,这个虚拟世界就叫元宇宙。当然,关于元宇宙的争论还在继续,而且我们能够从不同的角度分析能够得出差异性极大的结论报告,但是关于元宇宙的特征情况已获得了业界的认可。


在今年8月份以来,元宇宙概念火遍全球,甚至日本社交巨头GREE宣布将开展元宇宙业务、英伟达发布会上出场了十几秒的“数字替身”、微软在Inspire全球合作伙伴大会上宣布了企业元宇宙解决方案等


但实际上,不仅有各大科技巨头在争相布局元宇宙赛道,同时还有一些国家的政府相关部门也积极参与其中。5月18日,韩国科学技术和信息通信部发起成立了以现代、SK集团、LG集团等200多家韩国本土企业和组织的名叫“元宇宙联盟”的组织,其目的是想要打造国家级别以及增强线下现实平台,并在未来向社会提供公共虚拟服务;今年都的7月13日,日本发布了《关于虚拟空间行业未来可能性与课题的调查报告》,总结了日本虚拟空间行业相关问题,同时也期望能够占据主导地位在全球虚拟空间行业里;今年8月31日,韩国财政部发布了下一年的预算情况报告,计划斥资2000万美元用于元宇宙平台开发。


以上种种报告显示,元宇宙深受科技巨头、政府部门的青睐,那么究竟是什么原因元宇宙可以做到“人见人爱”的这种现象呢?


从企业的角度来看,目前还处于初级阶段的元宇宙,无论是基础的技术还是基本的应用场景,与未来的成熟形态相比较还是有一定的差距,但这也意味着元宇宙相关产业可拓展的空间巨大。 因此,如果想要守住市场,数字科技领域初创企业要获得超速的机会,就要学会提前布局,甚至还要加强马力。


从政府层面来看,元宇宙不仅代表着重要的新兴产业,同时也是需要被重视的社会会治理领域。 伴随着元宇宙这一新兴产业的不断发展,随之而来的将会是一系列的革命性发展和挑战。


元宇宙.png 


同时,元宇宙也算一个具有极致开放的、复杂且巨大的系统,它涵盖了全部的网络空间、多个基础的硬件设备和最为基本的现实条件,是由多类型建设者共同构建的超大型数字科学技术的应用生态。


因此,我们再来从技术的角度来看,技术局限性是元宇宙目前发展的最大瓶颈,XR、区块链、人工智能等相应底层技术距离元宇宙落地应用的需求仍有较大差距。需要做大量的基础研究才能支撑元宇宙产业的成熟。对此,我们不仅要谨防元宇宙成为炒作噱头,还要鼓励各大相关的企业更进一步加强技术创新能力提高产业技术的成熟度。


BI“元宇宙”也是人们热议的话题,大数据有望最先受益于元宇宙放量,就以国内著名BI厂商Smartbi为例,Smartbi持续完善数据产业链,以“Smartb一站式大数据分析平台”为核心引擎,打造大数据运营管理能力,拓展场景应用,加速企业数据化转型。


数据分析.png


数据可视化,简单来说就是将一种将相对复杂的表达形式、抽象的数据通过可视化转化为更容易理解的图形显示的一种形式。数据可视化可以更为生动形象地展现出数据所表达的内在价值,同时也十分方便企业利用数据智能更好地开展业务。 关于数据可视化,Smartbi支持完整的ECharts图库及多种图形,包括瀑布、热力图、树图等数l十种动态交互的图形,可视化功能十分地灵活,同时页面也展现的十分清晰明了,该功能也深受客户喜爱。


数据安全.png


 


数据安全是每个企业的重中之重,我们这里说的数据安全是狭义的,指的是采用现代信息存储手段对数据进行主动防护。 Smartbi在数据的收集、存储、使用、加工、传输、公开等各个环节,都提供了安全、可控的保护手段。其安全管理体系就是通过对多种权限进行控制,从而保障了数据资源的使用安全。另外还提供了定期备份、水印设置、分享设置等功能,大大降低了数据破坏、外泄的风险。


最后,我们可以肯定的是,在技术的不断提高进步和人类不断增多的生活需求的共同推动下,元宇宙场景的实现,元宇宙产业的成熟,只是一个时间问题。它对于我们之后的发展,无论是经济方面还是其他方面都是有着巨大的机遇和革命性作用


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

背包问题_概述(动态规划)

写在前 问题描述 注意:0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。基本思路 第 i 件物品没添加到背包,最大价值:dp[i][j] = dp[i - 1]...
继续阅读 »




写在前

问题描述

有N件物品和一个最多能被重量为W 的背包。一个物品只有两个属性:重量和价值。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

注意:0-1 背包问题无法使用贪心算法来求解,也就是说不能按照先添加性价比最高的物品来达到最优,这是因为这种方式可能造成背包空间的浪费,从而无法达到最优。

基本思路

这里有两个可变量体积和价值,我们定义dp[i][j]表示前i件物品体积不超过j能达到的最大价值,设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:

  • 第 i 件物品没添加到背包,最大价值:dp[i][j] = dp[i - 1][j]

  • 第 i 件物品添加到背包中:dp[i][j] = dp[i - 1][j - w] + v

第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w] + v)

代码实现

// W 为背包总重量
// N 为物品数量
// weights 数组存储 N 个物品的重量
// values 数组存储 N 个物品的价值
public int knapsack(int W, int N, int[] weights, int[] values) {
   // dp[i][0]和dp[0][j]没有价值已经初始化0
   int[][] dp = new int[N + 1][W + 1];
   // 从dp[1][1]开始遍历填表
   for (int i = 1; i <= N; ++i) {
       // 第i件物品的重量和价值
       int w = weights[i - 1], v = values[i - 1];
       for (int j = 1; j <= W; ++j) {
           if (j < w) {
               // 超过当前状态能装下的重量j
               dp[i][j] = dp[i - 1][j];
          } else {
               dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]);
          }
      }
  }
   return dp[N][W];
}

dp[i][j]的值只与dp[i-1][0,...,j-1]有关,所以我们可以采用动态规划常用的方法(滚动数组)对空间进行优化(即去掉dp的第一维)。因此,0-1 背包的状态转移方程为: dp[j] = max(dp[j], dp[j - w] + v)

特别注意:为了防止上一层循环的dp[0,...,j-1]被覆盖,循环的时候 j 只能逆向遍历。优化空间复杂度:

ps:滚动数组:即让数组滚动起来,每次都使用固定的几个存储空间,来达到压缩,节省存储空间的作用。

public int knapsack(int W, int N, int[] weights, int[] values) {
   int[] dp = new int[W + 1];
   for (int i = 1; i <= N; i++) {
       int w = weights[i - 1], v = values[i - 1];
       for (int j = W; j >= 1; --j) {
           if (j >= w) {
               dp[j] = Math.max(dp[j], dp[j - w] + v);
          }
      }
  }
   return dp[W];
}

ps:01背包内循环理解:还原成二维的dp就很好理解,一维的dp是二维dp在空间上进行复用的结果。dp[i]=f(dp[i-num]),等式的右边其实是二维dp上一行的数据,应该是只读的,在被读取前不应该被修改。如果正序的话,靠后的元素在读取前右边的dp有可能被修改了,倒序可以避免读取前被修改的问题。

作者:_code_x
来源:https://www.jianshu.com/p/b789ec845641

收起阅读 »

Android端小到不行的分页加载库

RecyclerView几乎在每个app里面都有被使用,但凡使用了列表就会采用分页加载进行数据请求和加载。android 官方也推出了分页库,但是感觉只有kotlin一起使用才能体会到酸爽。Java 版本的也有很多很强大的第三方库,BaseRecyclerVi...
继续阅读 »

RecyclerView几乎在每个app里面都有被使用,但凡使用了列表就会采用分页加载进行数据请求和加载。android 官方也推出了分页库,但是感觉只有kotlin一起使用才能体会到酸爽。Java 版本的也有很多很强大的第三方库,BaseRecyclerViewAdapterHelper这个库是我用起来最顺手的分页库,里面也包含了各式各样强大的功能:分组、拖动排序、动画,因为功能强大,代码量也相对比较大。 但是很多时候我们想要的就是分页加载,所以参照BaseRecyclerViewAdapterHelper写下了这个分页加载库,只有分页功能。(可以说照搬,也可以说精简,但是其中也加入个人理解)。
这个库相对BaseRecyclerViewAdapterHelper只有两个优点:

  • 代码量小

  • BaseRecyclerViewAdapterHelper 在数据不满一屏时仍然显示加载更多以及页面初始化时都会显示loadmoewView(虽然提供了api进行隐藏,但是看了很长时间注释和文档都没了解该怎么使用),而这个库在初次加载和不满一屏数据时不会显示loadmoreView

gradle引用

implementation 'com.maxcion:pageloadadapter:1.0.0'

项目地址:https://github.com/Likeyong/PageLoadAdapter

如果看不到gif,就到掘金吧 https://juejin.cn/post/6944242618658193415

单列分页加载

混合布局分页加载

Recyclerview 多type分页加载

单列分页加载

//一定要在PageLoadRecyclerVewAdapter<String> 的泛型参数里面指定数据源item格式
public class SimpleAdapter extends PageLoadRecyclerVewAdapter<String> {
   public SimpleAdapter(List<String> dataList) {
       super(dataList);
  }

   //这里进行 数据绑定
   @Override
   protected void convert(BaseViewHolder holder, String item) {
       holder.setText(R.id.text, item);
  }

   //这里返回布局item id
   @Override
   protected int getItemLayoutId() {
       return R.layout.item_simple;
  }
}

第一步 adapter实现好了,现在需要打开adapter的分页加载功能

public class SingleColumnActivity extends BaseActivity<String> implements IOnLoadMoreListener {


   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_single_column);
       RecyclerView rv = findViewById(R.id.rv);
       //实例化adapter
       mAdapter = new SimpleAdapter(null);
       //给adapter 设置loadmoreview
       mAdapter.setLoadMoreView(new CommonLoadMoreView());
       //设置滑动到底部时进行更多加载的回调
       mAdapter.setOnLoadMoreListener(this);
       rv.setAdapter(mAdapter);
       rv.setLayoutManager(new LinearLayoutManager(this));
       request();
  }



   @Override
   public void onLoadMoreRequested() {

       request();
  }

   //这个函数不用管
   @Override
   protected List<String> convertRequestData(List<String> originData) {
       return originData;
  }


}

第二步,RecyclerView也打开了分页加载功能,第三部就是根据接口返回的数据判断到底是 加载失败了、加成成功了还是加载结束(没有更多数据需要加载)

protected void request() {
       NetWorkRequest.request(mAdapter.getDataSize() / PAGE_SIZE + 1, mFailCount, new NetWorkRequest.Callback() {
           @Override
           public void onSuccess(List<String> result) {
               List<T> finalResult = convertRequestData(result);
               if(result.size() >= PAGE_SIZE){// 接口返回了满满一页的数据,这里数据加载成功
                   if (mAdapter.getDataSize() == 0){
                       //当前列表里面没有数据,代表是初次请求,所以这里使用setNewData()

                       mAdapter.setNewData(finalResult);
                  }else {
                       //列表里面已经有数据了,这里使用addDataList(),将数据添加到列表后面
                       mAdapter.addDataList(finalResult);
                  }
                   //这里调用adapter。loadMoreComplete(true) 函数通知列表刷新footview, 这里参数一定要传true
                   mAdapter.loadMoreComplete(true);
              }else {
                   //如果接口返回的数据不足一页,也就代表没有足够的数据了,那么也就没有下一页数据,所以这里
                   //认定分页加载结束
                   //这里的参数也一定要传true
                   mAdapter.loadMoreEnd(true);
              }
          }

           @Override
           public void onFail() {
               mFailCount++;
               //请求失败 通知recyclerview 刷新footview 状态
               mAdapter.loadMoreFail(true);
          }
      });
  }

上面是我写的模拟接口请求,不用在意其他代码,只要关注onSuccess 和onFail 两个回调里面的逻辑。

混合布局的支持

在电商行业经常能看到商品列表中,同一个列表,有的商品占满整整一行,有的一行显示2-3个商品。这种实现方案就是通过GridLayoutManager 的SpanSizeLookup 来控制每个item占几列的。

RecyclerView rv = findViewById(R.id.rv);
       mAdapter = new SimpleAdapter(null);
       mAdapter.setLoadMoreView(new CommonLoadMoreView());
       mAdapter.setOnLoadMoreListener(this);
     //这里我们将列表设置最多两列
       GridLayoutManager layoutManager = new GridLayoutManager(this, 2);
       layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
           @Override
           public int getSpanSize(int position) {
             //根据position 设置每个item应该占几列
             //如果当前的position是3的整数倍 我们就让他占满2列,其他的只占1列
               return position % 3 == 0 ? 2 : 1 ;
          }
      });
       rv.setLayoutManager(layoutManager);
       rv.setAdapter(mAdapter);

RecyclerView多Type支持

如果要使用多type, 在写Adapter的时候要继承PageLoadMultiRecyclerViewAdapter<T, BaseViewHolder>,其中T 是数据源item类型,这个类型必须实现 IMultiItem 接口,并在getItemType()函数中返回当前item对应的type

public class MultiPageLoadAdapter extends PageLoadMultiRecyclerViewAdapter<MultiData, BaseViewHolder> {
   public MultiPageLoadAdapter(List<MultiData> dataList) {
       super(dataList);
       //构造函数里面将 每种type 和 type 对应的布局进行绑定
       addItemLayout(MultiData.TYPE_TEXT, R.layout.item_simple);
       addItemLayout(MultiData.TYPE_IMAGE, R.layout.item_multi_image);
       addItemLayout(MultiData.TYPE_VIDEO, R.layout.item_multi_video);
  }

   @Override
   protected void convert(BaseViewHolder holder, MultiData item) {
       //在convert中针对不同的type 进行不同的bind逻辑
       switch (holder.getItemViewType()){
           case MultiData.TYPE_VIDEO:
               holder.setText(R.id.text, item.content);
               break;

           case MultiData.TYPE_IMAGE:
               holder.setText(R.id.text, item.content);
               break;

           case MultiData.TYPE_TEXT:
               holder.setText(R.id.text, item.content);
           default:
               break;
      }
  }
}

引入方式也和上面两种方式一样

RecyclerView recyclerView = findViewById(R.id.rv);
       mAdapter = new MultiPageLoadAdapter(null);
       mAdapter.setLoadMoreView(new CommonLoadMoreView());
       mAdapter.setOnLoadMoreListener(this);
       recyclerView.setLayoutManager(new LinearLayoutManager(this));
       recyclerView.setAdapter(mAdapter);

作者:maxcion
来源:https://www.jianshu.com/p/aa3054b4d03c

收起阅读 »

12个有用的JavaScript数组技巧

数组是Javascript最常见的概念之一,它为我们提供了处理数据的许多可能性,熟悉数组的一些常用操作是很有必要的。1、数组去重1、from()叠加new Set()方法字符串或数值型数组的去重可以直接使用from方法。var plants = ['Satur...
继续阅读 »

数组是Javascript最常见的概念之一,它为我们提供了处理数据的许多可能性,熟悉数组的一些常用操作是很有必要的。

1、数组去重

1、from()叠加new Set()方法

字符串或数值型数组的去重可以直接使用from方法。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var uniquePlants = Array.from(new Set(plants));
console.log(uniquePlants); // [ 'Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Mars', 'Jupiter' ]

2、spread操作符(…)

扩展运算符是ES6的一大创新,还有很多强大的功能。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var uniquePlants = [...new Set(plants)];
console.log(uniquePlants); // [ 'Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Mars', 'Jupiter' ]

2、替换数组中的特定值

splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目。该方法会改变原始数组。特别需要注意插入值的位置!

// arrayObject.splice(index,howmany,item1,.....,itemX)

var plants = ['Saturn', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var result = plants.splice(2, 1, 'www.shanzhonglei.com')
console.log(plants); // ['Saturn','Uranus','www.shanzhonglei.com','Mercury','Venus','Earth','Mars','Jupiter']
console.log(result); // ['Mercury']

3、没有map()的映射数组

我们先介绍一下map方法。map()方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值,它会按照原始数组元素顺序依次处理元素。注意: map()不会改变原始数组,也不会对空数组进行检测。
下面我们来实现一个没有map的数组映射:

// array.map(function(currentValue,index,arr), thisValue)

var plants = [
  { name: "Saturn" },
  { name: "Uranus" },
  { name: "Mercury" },
  { name: "Venus" },
]
var plantsName = Array.from(plants, ({ name }) => name);
console.log(plantsName); // [ 'Saturn', 'Uranus', 'Mercury', 'Venus' ]

4、空数组

如果要清空一个数组,将数组的长度设置为0即可,额,这个有点简单。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
plants.length = 0;
console.log(plants); // []

5、将数组转换为对象

如果要将数组转换为对象,最快的方法莫过于spread运算符(…)。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var plantsObj = {...plants }
console.log(plantsObj); // {'0': 'Saturn','1': 'Earth', '2': 'Uranus','3': 'Mercury','4': 'Venus','5': 'Earth','6': 'Mars','7': 'Jupiter'}

6、用数据填充数组

如果我们需要用一些数据来填充数组,或者需要一个具有相同值的数据,我们可以用fill()方法。

var plants = new Array(8).fill('8');
console.log(plants); // ['8', '8', '8','8', '8', '8','8', '8']

7、合并数组

当然你会想到concat()方法,但是哦,spread操作符(…)也很香的,这也是扩展运算符的另一个应用。

var plants1 = ['Saturn', 'Earth', 'Uranus', 'Mercury'];
var plants2 = ['Venus', 'Earth', 'Mars', 'Jupiter'];
console.log([...plants1, ...plants2]); // ['Saturn', 'Earth','Uranus', 'Mercury','Venus', 'Earth','Mars', 'Jupiter']

8、两个数组的交集

要求两个数组的交集,首先确保数组不重复,然后使用filter()方法和includes()方法。

var plants1 = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var plants2 = ['Saturn', 'Earth', 'Uranus'];
var alonePlants = [...new Set(plants1)].filter(item => plants2.includes(item));
console.log(alonePlants); // [ 'Saturn', 'Earth', 'Uranus' ]

9、删除数组中的假值

我们时常需要在处理数据的时候要去掉假值。在Javascript中,假值是false, 0, " ", null, NaN, undefined。

var plants = ['Saturn', 'Earth', null, undefined, false, "", NaN, 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
var trueArr = plants.filter(Boolean);
console.log(trueArr); // ['Saturn', 'Earth','Uranus', 'Mercury','Venus', 'Earth','Mars', 'Jupiter']

10、获取数组中的随机值

我们可以根据数组长度获得一个随机索引号。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
console.log(plants[Math.floor(Math.random() * (plants.length + 1))])

11、lastIndexOf()方法

lastIndexOf()可以帮助我们查找元素最后一次出现的索引。

var plants = ['Saturn', 'Earth', 'Uranus', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter'];
console.log(plants.lastIndexOf('Earth')) // 5

12、将数组中的所有值相加

reduce()方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。

// array.reduce(function(total, currentValue, currentIndex, arr), initialValue)

var nums = [1, 2, 3, 4, 5];
var sum = nums.reduce((x, y) => x + y);
console.log(sum); // 15

作者:前端技术驿站
来源:https://www.jianshu.com/p/651338c88bb4

收起阅读 »

字节面试被虐后,是时候搞懂 DNS 了

前几天面了字节 👦🏻:“浏览器从输入URL到显示页面发生了什么?” 👧🏻:%^&@#^&(这我怎么可能没有准备?从网络到渲染说了一通后) 👦🏻:“你刚刚提到了 DNS,那说说 DNS 的查询过程吧” 👧🏻:“DNS 查询是一个递归 + 迭代的...
继续阅读 »

前几天面了字节



👦🏻:“浏览器从输入URL到显示页面发生了什么?”


👧🏻:%^&@#^&(这我怎么可能没有准备?从网络到渲染说了一通后)


👦🏻:“你刚刚提到了 DNS,那说说 DNS 的查询过程吧”


👧🏻:“DNS 查询是一个递归 + 迭代的过程...”


👦🏻:“那具体的递归和迭代过程是怎样的呢?”


👧🏻:“...”



当时我脑子里有个大概的过程,但是细节就记不起来了,所以今天就来梳理一下 DNS 相关的内容,如有不妥之处,还望大家指出。


什么是 DNS


DNS 即域名系统,全称是 Domain Name System。当我们在浏览器输入一个 URL 地址时,浏览器要向这个 URL 的主机名对应的服务器发送请求,就得知道服务器的 IP,对于浏览器来说,DNS 的作用就是将主机名转换成 IP 地址。下面是摘自《计算机网络:自顶向下方法》的概念:



DNS 是:



  1. 一个由分层的 DNS 服务器实现的分布式数据库

  2. 一个使得主机能够查询分布式数据库的应用层协议



也就是,DNS 是一个应用层协议,我们发送一个请求,其中包含我们要查询的主机名,它就会给我们返回这个主机名对应的 IP;


其次,DNS 是一个分布式数据库,整个 DNS 系统由分散在世界各地的很多台 DNS 服务器组成,每台 DNS 服务器上都保存了一些数据,这些数据可以让我们最终查到主机名对应的 IP。


所以 DNS 的查询过程,说白了,就是去向这些 DNS 服务器询问,你知道这个主机名的 IP 是多少吗,不知道?那你知道去哪台 DNS 服务器上可以查到吗?直到查到我想要的 IP 为止。


分布式、层次数据库


什么是分布式?

这个世界上没有一台 DNS 服务器拥有因特网上所有主机的映射,每台 DNS 只负责部分映射。


什么是层次?

DNS 服务器有 3 种类型:根 DNS 服务器、顶级域(Top-Level Domain, TLD)DNS 服务器和权威 DNS 服务器。它们的层次结构如下图所示:



DNS 的层次结构.jpeg



图片来源:《计算机网络:自顶向下方法》



  • 根 DNS 服务器


首先我们要明确根域名是什么,比如 http://www.baidu.com,有些同学可能会误以为 com 就是根域名,其实 com 是顶级域名,http://www.baidu.com 的完整写法是 http://www.baidu.com.,最后的这个 . 就是根域名。


根 DNS 服务器的作用是什么呢?就是管理它的下一级,也就是顶级域 DNS 服务器。通过询问根 DNS 服务器,我们可以知道一个主机名对应的顶级域 DNS 服务器的 IP 是多少,从而继续向顶级域 DNS 服务器发起查询请求。



  • 顶级域 DNS 服务器


除了前面提到的 com 是顶级域名,常见的顶级域名还有 cnorgedu 等。顶级域 DNS 服务器,也就是 TLD,提供了它的下一级,也就是权威 DNS 服务器的 IP 地址。



  • 权威 DNS 服务器


权威 DNS 服务器可以返回主机 - IP 的最终映射。


关于这几个层次的服务器之间是怎么交互的,接下来我们会讲到 DNS 具体的查询过程,结合查询过程,大家就不难理解它们之间的关系了。


本地 DNS 服务器


之前对 DNS 有过了解的同学可能会发现,上一节的 DNS 层次结构,为什么没有提到本地 DNS 服务器?因为严格来说,本地 DNS 服务器并不属于 DNS 的层次结构,但它对 DNS 层次结构是至关重要的。那什么是本地 DNS 服务器呢?


每个 ISP 都有一台本地 DNS 服务器,比如一个居民区的 ISP、一个大学的 ISP、一个机构的 ISP,都有一台或多台本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,本地 DNS 服务器起着代理的作用,并负责将该请求转发到 DNS 服务器层次结构中。


接下来就让我们通过一个简单的例子,看看 DNS 的查询过程是怎样的,看看客户端、本地 DNS 服务器、DNS 服务器层次结构之间是如何交互的。


递归查询、迭代查询


如下图,假设主机 m.n.com 想要获取主机 a.b.com 的 IP 地址,会经过以下几个步骤:



DNS.png





  1. 首先,主机 m.n.com 向它的本地 DNS 服务器发送一个 DNS 查询报文,其中包含期待被转换的主机名 a.b.com




  2. 本地 DNS 服务器将该报文转发到根 DNS 服务器;




  3. 该根 DNS 服务器注意到 com 前缀,便向本地 DNS 服务器返回 com 对应的顶级域 DNS 服务器(TLD)的 IP 地址列表。


    意思就是,我不知道 a.b.com 的 IP,不过这些 TLD 服务器可能知道,你去问他们吧;




  4. 本地 DNS 服务器则向其中一台 TLD 服务器发送查询报文;




  5. 该 TLD 服务器注意到 b.com 前缀,便向本地 DNS 服务器返回权威 DNS 服务器的 IP 地址。


    意思就是,我不知道 a.b.com 的 IP,不过这些权威服务器可能知道,你去问他们吧;




  6. 本地 DNS 服务器又向其中一台权威服务器发送查询报文;




  7. 终于,该权威服务器返回了 a.b.com 的 IP 地址;




  8. 本地 DNS 服务器将 a.b.com 跟 IP 地址的映射返回给主机 m.n.comm.n.com 就可以用该 IP 向 a.b.com 发送请求啦。





bqb4.jpeg



“你说了这么多,递归呢?迭代呢?”


这位同学不要捉急,其实递归和迭代已经包含在上述过程里了。


主机 m.n.com 向本地 DNS 服务器 dns.n.com 发出的查询就是递归查询,这个查询是主机 m.n.com 以自己的名义向本地 DNS 服务器请求想要的 IP 映射,并且本地 DNS 服务器直接返回映射结果给到主机。


而后继的三个查询是迭代查询,包括本地 DNS 服务器向根 DNS 服务器发送查询请求、本地 DNS 服务器向 TLD 服务器发送查询请求、本地 DNS 服务器向权威 DNS 服务器发送查询请求,所有的请求都是由本地 DNS 服务器发出,所有的响应都是直接返回给本地 DNS 服务器


那么问题来了,所有的 DNS 查询都必须遵循这种递归 + 迭代的模式吗?


当然不是。


从理论上讲,任何 DNS 查询既可以是递归的,也可以是迭代的。下图的所有查询就都是递归的,不包含迭代。



DNS2.png



看到这里,大家可能会有个疑问,TLD 一定知道权威 DNS 服务器的 IP 地址吗?


emmm...



bqb7.png



还真不一定,有时 TLD 只是知道中间的某个 DNS 服务器,再由这个中间 DNS 服务器去找到权威 DNS 服务器。这种时候,整个查询过程就需要更多的 DNS 报文。


DNS 缓存


为了让我们更快的拿到想要的 IP,DNS 广泛使用了缓存技术。DNS 缓存的原理非常简单,在一个 DNS 查询的过程中,当某一台 DNS 服务器接收到一个 DNS 应答(例如,包含某主机名到 IP 地址的映射)时,它就能够将映射缓存到本地,下次查询就可以直接用缓存里的内容。当然,缓存并不是永久的,每一条映射记录都有一个对应的生存时间,一旦过了生存时间,这条记录就应该从缓存移出。


事实上,有了缓存,大多数 DNS 查询都绕过了根 DNS 服务器,需要向根 DNS 服务器发起查询的请求很少。


面试感想


这次面试收获还蛮大的,有些东西以为自己懂了,以为自己能说清楚,但到了真的要说的时候,又没有办法完整地梳理出来,描述起来磕磕绊绊,在面试中会很减分。


所以不要偷懒,不要抱有侥幸心理,踏实学。共勉。



作者:我是陆小北
链接:https://juejin.cn/post/6990344840181940261

收起阅读 »

H5页面中调用微信和支付宝支付

最近在工作中,有个H5页面需要实现微信支付和支付宝支付的功能,现在已经完成,抽个时间写出来,分享给有需要的人。 第一步:先判断当前环境 判断用户所属环境,根据环境不同,执行不同的支付程序。 if (/MicroMessenger/.test(window.na...
继续阅读 »

最近在工作中,有个H5页面需要实现微信支付和支付宝支付的功能,现在已经完成,抽个时间写出来,分享给有需要的人。


第一步:先判断当前环境


判断用户所属环境,根据环境不同,执行不同的支付程序。


if (/MicroMessenger/.test(window.navigator.userAgent)) {
// alert('微信');
} else if (/AlipayClient/.test(window.navigator.userAgent)) {
//alert('支付宝');
} else {
//alert('其他浏览器');
}

第二步:如果是微信环境,需要先进行网页授权


网页授权的详细介绍可以查看微信相关文档。这里不做介绍。


第三步:


1、微信支付


微信支付有两种方法:
1:调用微信浏览器提供的内置接口WeixinJSBridge
2:引入微信jssdk,使用wx.chooseWXPay方法,需要先通过config接口注入权限验证配置。
我这里使用的是第一种,在从后台拿到签名、时间戳这些数据后,直接调用微信浏览器提供的内置接口WeixinJSBridge即可完成支付功能。


getRequestPayment(data) {
function onBridgeReady() {
WeixinJSBridge.invoke(
"getBrandWCPayRequest", {
"appId": data.appId, //公众号ID,由商户传入
"timeStamp": data.timeStamp, //时间戳,自1970年以来的秒数
"nonceStr": data.nonceStr, //随机串
"package": data.package,
"signType": data.signType, //微信签名方式:
"paySign": data.paySign //微信签名
},
function(res) {
alert(JSON.stringify(res));
// get_brand_wcpay_request
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 使用以上方式判断前端返回,微信团队郑重提示:
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
}
}
);
}
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener(
"WeixinJSBridgeReady",
onBridgeReady,
false
);
} else if (document.attachEvent) {
document.attachEvent("WeixinJSBridgeReady", onBridgeReady);
document.attachEvent("onWeixinJSBridgeReady", onBridgeReady);
}
} else {
onBridgeReady();
}
},

2、支付宝支付


支付宝支付相对于微信来说,前端这块工作更简单 ,后台会返回给前端一个form表单,我们要做的就是把这个表单进行提交即可。相关代码如下:


this.$api.alipayPay(data).then((res) => {
// console.log('支付宝参数', res.data)
if (res.code == 200) {
var resData =res.data
const div = document.createElement('div')
div.id = 'alipay'
div.innerHTML = resData
document.body.appendChild(div)
document.querySelector('#alipay').children[0].submit() // 执行后会唤起支付宝
}

}).catch((err) => {
})

作者:故友
链接:https://juejin.cn/post/7034033584684204068

收起阅读 »

Android系统启动-Zygote进程

本篇文章基于Android6.0源码分析 相关源码文件: /system/core/rootdir/init.rc /system/core/rootdir/init.zygote64.rc /frameworks/base/cmds/app_proces...
继续阅读 »

本篇文章基于Android6.0源码分析



相关源码文件:


/system/core/rootdir/init.rc
/system/core/rootdir/init.zygote64.rc

/frameworks/base/cmds/app_process/App_main.cpp
/frameworks/base/core/jni/AndroidRuntime.cpp

/frameworks/base/core/java/com/android/internal/os/
- ZygoteInit.java
- Zygote.java
- ZygoteConnection.java

/frameworks/base/core/java/android/net/LocalServerSocket.java
/system/core/libutils/Threads.cpp

Zygote进程启动前的概述


通过init.rc的文件解析会启动zygote相关的服务从而启动zygote进程。通过import导入决定启动哪种类型的zygote服务脚本,这里分为32位和64位架构的zygote服务脚本


import /init.${ro.zygote}.rc

在/system/core/rootdir目录中有四个zygote相关的服务脚本


init.zygote32.rc // 支持32位的zygote
init.zygote32_64.rc // 即支持32位也支持64位的zygote,其中以32位为主,64位为辅
init.zygote64.rc // 支持64位的zygote
init.zygote64_32.rc // 即支持64位也支持32位的zygote,其中以64位为主,32位为辅


下面我们分析只分析64位的zygote服务脚本的Android初始化语言:


service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server
class main
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd
writepid /dev/cpuset/foreground/tasks


zygote进程的执行程序为/system/bin/app_process64中,其中参数为:-Xzygote /system/bin --zygote --start-system-server,classname为main 。除了在Init进程解析时创建Zygote进程,在servicemanager、surfaceflinger、systemserver进程被杀时Zygote进程也会进行重启。


其中/system/bin/app_process64的映射的执行文件为:/frameworks/base/cmds/app_process/app_main.cpp


Zygote进程启动


image.png
如图1所示,zygote进程启动时会先启动app_main类的main()方法:


  // 参数argv为 :  -Xzygote /system/bin --zygote --start-system-server
int main(int argc, char* const argv[])
{
// 创建一个AppRuntime实例,AppRuntime 继承 AndoirdRuntime
AppRuntime runtime (argv[0], computeArgBlockSize(argc, argv));
//忽略第一个参数
argc--;
argv++;


// 解析参数并对变量赋值
bool zygote = false;
bool startSystemServer = false;
bool application = false;
String8 niceName;
String8 className;

++i; // Skip unused "parent dir" argument.
while (i < argc) {
const char * arg = argv [i++];
if (strcmp(arg, "--zygote") == 0) {
// 参数中有--zygote
zygote = true;
niceName = ZYGOTE_NICE_NAME;
} else if (strcmp(arg, "--start-system-server") == 0) {
// 参数中有--start-system-server
startSystemServer = true;
} else if (strcmp(arg, "--application") == 0) {
application = true;
} else if (strncmp(arg, "--nice-name=", 12) == 0) {
niceName.setTo(arg + 12);
} else if (strncmp(arg, "--", 2) != 0) {
className.setTo(arg);
break;
} else {
--i;
break;
}
}


if (zygote) {
//如果zygote为true,则调用AndroidRuntime的start方法,并传入了"com.android.internal.os.ZygoteInit"参数
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied.\n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
return 10;
}
}

在app_main的mian()方法中,主要是根据zygote的脚本的参数进行解析,在解析到有--zygote字符后,则确定执行AndroidRuntime.start方法,并且第一个参数传为com.android.internal.os.ZygoteInit


AndroidRuntime.start()


在此方法中,主要做了三件事:
· 创建虚拟机实例
· JNI方法的注册
· 调用参数的main()方法


  // 这里的className为:com.android.internal.os.ZygoteInit
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{

/* start the virtual machine */
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv * env;
// 1. 创建虚拟机
if (startVm(& mJavaVM, &env, zygote) != 0) {
return;
}
onVmCreated(env);
// 2. JNI方法注册
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}

// 解析classname参数
//将"com.android.internal.os.ZygoteInit"转换为"com/android/internal/os/ZygoteInit"
char * slashClassName = toSlashClassName(className);
jclass startClass = env->FindClass(slashClassName);

if (startClass == NULL) {
} else {
// 得到ZygoteInit的main方法
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
if (startMeth == NULL) {
} else { env ->
// 3. 执行ZygoteInit的main方法
CallStaticVoidMethod(startClass, startMeth, strArray);
}
}
free(slashClassName);

}

对start方法进行了一些删减后,主要是通过startVm 创建虚拟机,通过startReg(env)进行JNI方法注册,最后解析className参数,去执行ZygoteInit.main方法。下面将逐一分析这三种状态。


1. 创建虚拟机实例


startVm


int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
{
// ...
// JNI检测功能
bool checkJni = false;
property_get("dalvik.vm.checkjni", propBuf, "");
if (strcmp(propBuf, "true") == 0) {
checkJni = true;
} else if (strcmp(propBuf, "false") != 0) {
/* property is neither true nor false; fall back on kernel parameter */
property_get("ro.kernel.android.checkjni", propBuf, "");
if (propBuf[0] == '1') {
checkJni = true;
}
}
ALOGD("CheckJNI is %s\n", checkJni ? "ON" : "OFF");
if (checkJni) {
addOption("-Xcheck:jni");
}

// /虚拟机产生的trace文件,主要用于分析系统问题,路径默认为/data/anr/traces.txt
parseRuntimeOption("dalvik.vm.stack-trace-file", stackTraceFileBuf, "-Xstacktracefile:");

//对于不同的软硬件环境,这些参数往往需要调整、优化,从而使系统达到最佳性能
parseRuntimeOption("dalvik.vm.heapstartsize", heapstartsizeOptsBuf, "-Xms", "4m");
parseRuntimeOption("dalvik.vm.heapsize", heapsizeOptsBuf, "-Xmx", "16m");

parseRuntimeOption(
"dalvik.vm.heapgrowthlimit",
heapgrowthlimitOptsBuf,
"-XX:HeapGrowthLimit="
);
parseRuntimeOption("dalvik.vm.heapminfree", heapminfreeOptsBuf, "-XX:HeapMinFree=");
parseRuntimeOption("dalvik.vm.heapmaxfree", heapmaxfreeOptsBuf, "-XX:HeapMaxFree=");
parseRuntimeOption(
"dalvik.vm.heaptargetutilization",
heaptargetutilizationOptsBuf,
"-XX:HeapTargetUtilization="
);
// ...
// 初始化虚拟机
if (JNI_CreateJavaVM(pJavaVM, pEnv, & initArgs) < 0) {
ALOGE("JNI_CreateJavaVM failed\n");
return -1;
}

return 0;
}


startVm方法里面有很多代码,但主要分为三步,第一步是检测,第二步是软硬件参数的设置,第三步是初始化虚拟机。


2. JNI方法的注册


startReg


  int AndroidRuntime::startReg(JNIEnv* env)
{
androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);

ALOGV("--- registering native functions ---\n");
env->PushLocalFrame(200);
//进程JNI方法的注册
if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) { env ->
PopLocalFrame(NULL);
return -1;
}
env->PopLocalFrame(NULL);
return 0;
}

// 这里的array[]是gRegJNI,它是一个映射了很多方法的数组
static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)
{
// 执行很多映射的方法
for (size_t i = 0; i < count; i++) {
if (array[i].mProc(env) < 0) {
return -1;
}
}
return 0;
}

startReg方法是对JNI方法的注册,它通过一个有很多宏定义的数组,并执行数组定义的方法,进行对JNI和Java层方法一一映射。


3. 调用ZygoteInit.main方法


在AndroidRuntime.start()方法的最后,通过反射执行了其ZygoteInit.main()方法。


    if (startClass == NULL) {
} else {
// 得到ZygoteInit的main方法
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
if (startMeth == NULL) {
} else { env ->
// 3. 执行ZygoteInit的main方法
CallStaticVoidMethod(startClass, startMeth, strArray);
}
}

通过反射去执行ZygoteInit.main方法,也是第一次进入java语言的世界。所以AndroidRuntime的start方法做了三件事,一是初始化虚拟机,二是JNI方法的注册,三是通过反射执行ZygoteInit.main方法。


ZygoteInit.main


Zygote进程用于创建管理framework层的SystemServer进程,还用于创建App进程,就是应用App启动创建进程时,是由Zygote进程创建的,并且Zygote创建子进程将使用copy on write的技术,就是子进程直接继承父进程的现有的资源,在子进程对于共有的资源是读时共享,写时复制
ZygoteInit.main方法中主要做了四件事:
· 注册服务端的socket,用于接收创建子进程的信息
· 提前预加载类和资源,用于子进程共享
· 创建SystemServer进程,其管理着framework层
· 循环监听服务socket,创建子进程


  public static void main(String argv[])
{
try {
// 创建服务端Soctet,用于接收创建子进程信息
registerZygoteSocket(socketName);
// 提前预加载类和资源
preload();
// gc操作
gcAndFinalize();
// 创建SystemServer进程
if (startSystemServer) {
startSystemServer(abiList, socketName);
}
// 用服务socket监听创建进程信息,并创建子进程
runSelectLoop(abiList);

closeServerSocket();
} catch (MethodAndArgsCaller caller) {
caller.run();
} catch (RuntimeException ex) {
Log.e(TAG, "Zygote died with exception", ex);
closeServerSocket();
throw ex;
}
}


通过registerZygoteSocket方法去创建服务端的socket,preload()方法去提前加载类和资源,startSystemServer方法去创建SystemServer进程去管理framework层,runSelectLoop方法循环监听创建子进程。


1. 注册服务端Socket


registerZygoteSocket


  private static void registerZygoteSocket(String socketName)
{
if (sServerSocket == null) {
int fileDesc;
final String fullSocketName = ANDROID_SOCKET_PREFIX + socketName;
try {
String env = System . getenv (fullSocketName);
fileDesc = Integer.parseInt(env);
} catch (RuntimeException ex) {
throw new RuntimeException (fullSocketName + " unset or invalid", ex);
}

try {
// 创建服务端的socket
FileDescriptor fd = new FileDescriptor();
fd.setInt$(fileDesc);
sServerSocket = new LocalServerSocket (fd);
} catch (IOException ex) {
throw new RuntimeException (
"Error binding to local socket '" + fileDesc + "'", ex);
}
}
}

创建一个服务端的socket用于接口多个客户端的信息接收,在后面的runSelectLoop方法用于监听服务端的socket信息,以便创建子进程。


2. 预加载资源


preload


  static void preload()
{
preloadClasses(); //预加载位于/system/etc/preloaded-classes文件中的类
preloadResources(); //预加载资源,包含drawable和color资源
preloadOpenGL(); //预加载OpenGL
preloadSharedLibraries(); //预加载"android","compiler_rt","jnigraphics"这3个共享库
preloadTextResources(); //预加载 文本连接符资源
WebViewFactory.prepareWebViewInZygote(); //仅用于zygote进程,用于内存共享的进程
}

preloadClasses()方法通过Class.forName()反射的方法去加载类,preloadResources方法主要是加载位于com.android.internal.R.array.preloaded_drawables和com.android.internal.R.array.preloaded_color_state_lists的资源。
提前加载资源的好处是,在复制创建子进程时,提前加载好的资源可以给子进程直接使用,不用第二次创建,但不好的地方是每个创建的子进程都有拥有很多资源,而不管是否需要。


3. 启动SystemServer进程


startSystemServer


  private static boolean startSystemServer(String abiList, String socketName)
throws MethodAndArgsCaller, RuntimeException
{
// 通过数组保存创建systemserver进程的信息
String args [] = {
"--setuid=1000",
"--setgid=1000",
"--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1032,3001,3002,3003,3006,3007",
"--capabilities=" + capabilities + "," + capabilities,
"--nice-name=system_server",
"--runtime-args",
"com.android.server.SystemServer",
};
ZygoteConnection.Arguments parsedArgs = null;

int pid;

try {
parsedArgs = new ZygoteConnection . Arguments (args);
ZygoteConnection.applyDebuggerSystemProperty(parsedArgs);
ZygoteConnection.applyInvokeWithSystemProperty(parsedArgs);

// 创建systemserver进程
pid = Zygote.forkSystemServer(
parsedArgs.uid, parsedArgs.gid,
parsedArgs.gids,
parsedArgs.debugFlags,
null,
parsedArgs.permittedCapabilities,
parsedArgs.effectiveCapabilities
);
} catch (IllegalArgumentException ex) {
throw new RuntimeException (ex);
}

/* pid==0 则是子进程,就是systemserver */
if (pid == 0) {
if (hasSecondZygote(abiList)) {
waitForSecondaryZygote(socketName);
}

// 完成system_server进程剩余的工作
handleSystemServerProcess(parsedArgs);
}

return true;
}


通过Zygote.forkSystemServer去创建SystemServer进程,其进程是管理着framework的,我们将在下一篇分析SystemServer进程进程的启动。


4. 循环等待孵化进程


runSelectLoop


  private static void runSelectLoop(String abiList) throws MethodAndArgsCaller
{
// FileDescriptor数组
ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
// ZygoteConnection数组
ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();
//sServerSocket是socket通信中的服务端,即zygote进程。保存到fds[0]
fds.add(sServerSocket.getFileDescriptor());
peers.add(null);

while (true) {
// StructPollfd数组,并将相应位置fds的值赋值
StructPollfd[] pollFds = new StructPollfd[fds.size()];
for (int i = 0; i < pollFds.length; ++i) {
pollFds[i] = new StructPollfd ();
pollFds[i].fd = fds.get(i);
pollFds[i].events = (short) POLLIN;
}
try {
//处理轮询状态,当pollFds有事件到来则往下执行,否则阻塞在这里
Os.poll(pollFds, -1);
} catch (ErrnoException ex) {
throw new RuntimeException ("poll failed", ex);
}
for (int i = pollFds.length - 1; i >= 0; --i) {

//采用I/O多路复用机制,当接收到客户端发出连接请求 或者数据处理请求到来,则往下执行;
// 否则进入continue,跳出本次循环。
if ((pollFds[i].revents & POLLIN) == 0) {
continue;
}
if (i == 0) {
ZygoteConnection newPeer = acceptCommandPeer (abiList);
peers.add(newPeer);
fds.add(newPeer.getFileDesciptor());
} else {
//i>0,则代表通过socket接收来自对端的数据,并执行相应操作
boolean done = peers.get (i).runOnce();
if (done) {
peers.remove(i);
fds.remove(i);
}
}
}
}
}

在runSelectLoop方法中有一个轮询的状态,如果有事件接收则会去执行runOnce()的方法操作:


  boolean runOnce() throws ZygoteInit.MethodAndArgsCaller
{

String args [];
Arguments parsedArgs = null;
FileDescriptor[] descriptors;

try {
//读取socket客户端发送过来的参数列表
args = readArgumentList();
descriptors = mSocket.getAncillaryFileDescriptors();
} catch (IOException ex) {
Log.w(TAG, "IOException on command socket " + ex.getMessage());
closeSocket();
return true;
}

if (args == null) {
// EOF reached.
closeSocket();
return true;
}


try {
//将binder客户端传递过来的参数,解析成Arguments对象格式
parsedArgs = new Arguments (args);
...
// fork创建一个新的进程
pid = Zygote.forkAndSpecialize(
parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
parsedArgs.niceName, fdsToClose, parsedArgs.instructionSet,
parsedArgs.appDataDir
);
} catch (ErrnoException ex) {
logAndPrintError(newStderr, "Exception creating pipe", ex);
} catch (IllegalArgumentException ex) {
logAndPrintError(newStderr, "Invalid zygote arguments", ex);
} catch (ZygoteSecurityException ex) {
logAndPrintError(
newStderr,
"Zygote security policy prevents request: ", ex
);
}

try {
if (pid == 0) {
// 处理子进程
IoUtils.closeQuietly(serverPipeFd);
serverPipeFd = null;
handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr);

// should never get here, the child is expected to either
// throw ZygoteInit.MethodAndArgsCaller or exec().
return true;
} else {
// 父进程
IoUtils.closeQuietly(childPipeFd);
childPipeFd = null;
return handleParentProc(pid, descriptors, serverPipeFd, parsedArgs);
}
} finally {
IoUtils.closeQuietly(childPipeFd);
IoUtils.closeQuietly(serverPipeFd);
}
}


所以在runSelectLoop方法中,通过客户端的socket不断的和服务端的socket通信的监听,通过调用起runOnce方法去不断的创建新的进程。


总结


Zygote进程的启动过程主要有:



  1. 创建虚拟机和JNI方法的注册

  2. 注册服务Socket和提前加载系统类和资源

  3. 创建SystemServer进程

  4. 循环等待孵化进程

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

NDK系列:JNI基础

1 Java、JNI、C/C++中的数据类型之间的映射关系 JNI是接口,Java与C/C++交互会有一个数据类型的对应,而JNI为此提供了一套中间类型。 2 JNI动态注册与静态注册 2.1 静态注册 步骤: 编写Java类,比如StaticRegiste...
继续阅读 »

1 Java、JNI、C/C++中的数据类型之间的映射关系


JNI是接口,Java与C/C++交互会有一个数据类型的对应,而JNI为此提供了一套中间类型。


2 JNI动态注册与静态注册


2.1 静态注册


步骤:



  • 编写Java类,比如StaticRegister.java;


package register.staticRegister;

public class StaticRegister {
public static native String func();//注意native关键字
public static void main(String[] args) {
System.out.println(func());
}
}


  • 在.java源文件目录下,命令行输入“javac StaticRegister.java”生成StaticRegister.class文件;

  • 在StaticRegister.class所属包所在目录下,命令行执行“javah register.staticRegister.StaticRegister”(完整类名无后缀),在包所在目录生成register_staticRegister_StaticRegister.h头文件;


image.png


image.png



  • 如果是JDK 1.8或以上,以上步骤可简化为一步:在StaticRegister.java目录下,命令行执行 javac -h . StaticRegister.java,直接在当前目录下得到.class文件和.h文件;

  • 创建CLion项目并拷贝register_staticRegister_StaticRegister.h文件到项目目录;

  • 在CLion项目中添加jni.h头文件和jni_md.h头文件,这两个头文件是JDK自带的,在C:\Program Files\Java\jdk1.8.0_144\include目录下,将这两个头文件拷贝到CLion项目目录;

  • 在register_staticRegister_StaticRegister.h中修改#include 为#include "jni.h"

  • 我们其实可以看到,register_staticRegister_StaticRegister.h文件里面就是一个Java方面native方法的一个JNI声明,格式为JNIEXPORT 关键字一 jstring 返回值的JNI类型 JNICALL 关键字二 Java_register_staticRegister_StaticRegister_func Java_全类名_方法名
    (JNIEnv *, jclass);如下;


/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"
/* Header for class register_staticRegister_StaticRegister */

#ifndef _Included_register_staticRegister_StaticRegister
#define _Included_register_staticRegister_StaticRegister
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: register_staticRegister_StaticRegister
* Method: func
* Signature: ()Ljava/lang/String;
*/

JNIEXPORT jstring JNICALL Java_register_staticRegister_StaticRegister_func
(JNIEnv *, jclass)
;

#ifdef __cplusplus
}
#endif
#endif


  • 编写头文件register_staticRegister_StaticRegister.h对应的register_staticRegister_StaticRegister.c源文件,拷贝并实现register_staticRegister_StaticRegister.h下的函数,如下:


#include "register_staticRegister_StaticRegister.h"

JNIEXPORT jstring JNICALL Java_register_staticRegister_StaticRegister_func
(JNIEnv *env, jclass jobj)
{
return (*env)->NewStringUTF(env,"Hi Java, this is JNI");
};


  • 编写CMakeLists.txt文件,这是库的配置文件。最重要的是最后两个add_library(),其余的都是自动生成的。add_library()声明库的名字、类型和包含的.c&.h文件。SHARED关键字表示创建的库是动态库.dll,STATIC关键字表示创建的库是静态库.a。*注意:库本身有动态库和静态库之分,Java native方法也有静态注册和动态注册之分,二者没有关系。*这里将动态库命名为StaticRegisterLib


cmake_minimum_required(VERSION 3.15)
project(JNI_C)

set(CMAKE_CXX_STANDARD 14)

add_library(JNI_C SHARED library.cpp library.h)
add_library(StaticRegisterLib SHARED register_staticRegister_StaticRegister.c register_staticRegister_StaticRegister.h)


  • 此时CLion项目结构如下图,Build Project生成动态链接库,得到libStaticRegisterLib.dll


image.png


image.png


image.png



  • 在最开始的Java源文件中,添加静态代码块,使用System.load()方法加载该动态链接库,如下:


package register.staticRegister;

public class StaticRegister {

static {
System.load("E:\\_Projects_\\JNI_Projects\\JNI_C\\cmake-build-debug\\libStaticRegisterLib.dll");
}

public static native String func();
public static void main(String[] args) {
System.out.println(func());
}
}


  • 在Java侧运行,得到如下效果,Java成功调用了dll中的方法,静态注册完毕。


image.png



  • 上述过程,我们在JNI中使用Java_PACKAGENAME_CLASSNAME_METHODNAME与Java侧的方法进行匹配,这种方式我们称之为静态注册


2.2 动态注册


步骤:



  • 编写Java类,比如DynamicRegister.java,如下;


package register.dynamicRegister;

public class DynamicRegister {
public static native String func1(String s);
public static native int func2(int[] a);
public static void main(String[] args) {
System.out.println(func1("Hi JNI"));
int[] a = {1,2,3};
System.out.println("该数组有"+func2(a)+"个元素");
}
}


  • 在CLion项目中添加jni.h头文件和jni_md.h头文件,这两个头文件是JDK自带的,在C:\Program Files\Java\jdk1.8.0_144\include目录下,将这两个头文件拷贝到CLion项目目录;

  • 新建CLion项目,新建C/C++源文件dynamicRegister.c。在该.c文件中,实现两个函数,这两个函数将是native方法在JNI的实现,如下:


#include "jni.h"

jstring f1(JNIEnv *env, jclass jobj){
return (*env)->NewStringUTF(env,"Hi Java");
}
//注意JNI侧数组形参的写法以及如何求数组长度
jint f2(JNIEnv *env, jclass jobj, jintArray arr){
int len = (*env)->GetArrayLength(env,arr);
return len;
}


  • 到目前,f1(),f2()与Java侧native方法func1(),func2()还没有任何关联,我们需要手动**管理关联**;

  • 首先,我们新建一个以JNINativeMethod结构体为元素的数组,如下:


static const JNINativeMethod mMethods[] = {
{"func1","(Ljava/lang/String;)Ljava/lang/String;",(jstring *)f1},
{"func2","([I)I",(jint *)f2},
};


  • 以上数组中每一个元素,都是JNI侧实现方法与Java侧native方法的关联,前两个是Java侧native方法的描述,最后一个是JNI侧函数实现的描述,格式为:


{"Java侧的native方法名","方法的签名",函数指针}


  • 我们需要实现jni.h中的JNI_OnLoad()方法,该方法的实现方法是一个模板,如下:


JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv* env = NULL;
//获得 JNIEnv
int r = (*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4);
if( r != JNI_OK){
return -1;
}
jclass mainActivityCls =
(*env)—>FindClass(env,"register/dynamicRegister/DynamicRegister");
// 最后参数是需要注册的native方法的个数,如果小于0则注册失败。
r = (*env)->RegisterNatives(env, mainActivityCls, mMethods, 2);
if(r != JNI_OK ){
return -1;
}
return JNI_VERSION_1_4;
}


  • 注意!第一:以上FindClass(env,"register/dynamicRegister/DynamicRegister")中的字符串是Java侧DynamicRegister类的全类名,注意此处的写法"/";第二:RegisterNatives(env, mainActivityCls, mMethods, 2)中的最后一个参数是需要动态注册的方法个数,手动添加注册或者删除注册都需要对应变化,当然可以直接把mMethod[]的长度传进去,一劳永逸,如下:


int cnt = sizeof(mMethods)/ sizeof(mMethods[0]);
r = (*env)->RegisterNatives(env, mainActivityCls, mMethods, cnt);


  • 在最开始的Java源文件中,添加静态代码块,使用System.load()方法加载该动态链接库,如下:


package register.dynamicRegister;

public class DynamicRegister {
static {
System.load("E:\\_Projects_\\JNI_Projects\\JNI_C\\cmake-build-debug\\libDynamicRegisterLib.dll");
}
public static native String func1(String s);
public static native int func2(int[] a);
public static void main(String[] args) {
System.out.println(func1("Hi JNI"));
int[] a = {1,2,3};
System.out.println("该数组有"+func2(a)+"个元素");
}
}


  • Build Project生成动态链接库,得到libDynamicRegisterLib.dll

  • Java侧运行,效果如下:


image.png



  • 动态注册完毕。


3 system.load()与system.loadLibrary()


System.load()
System.load()参数必须为库文件的绝对路径,可以是任意路径,例如: System.load("C:\Documents and
Settings\TestJNI.dll"); //Windows
System.load("/usr/lib/TestJNI.so"); //Linux


System.loadLibrary()
System.loadLibrary()参数为库文件名,不包含库文件的扩展名
System.loadLibrary("TestJNI"); //加载Windows下的TestJNI.dll本地库
System.loadLibrary("TestJNI"); //加载Linux下的libTestJNI.so本地库


注意:TestJNI.dll 或 libTestJNI.so 必须是在JVM属性java.library.path所指向的路径中。
loadLibary需要[配置当前项目的java.library.path路径]


3 JNI上下文与Java签名


3.1 JNI上下文环境


3.1.1 JNIEnv


JNIEnv类型实际上代表了Java环境,通过JNIEnv*指针,JNI函数可以对Java侧的代码进行操作。例如,创建Java类的对象,调用Java对象的方法,获取对象中的属性等。JNIEnv的指针会被传入到JNI侧的native方法的实现函数中,来对Java端的代码进行操作。例如:


jstring f1(JNIEnv *env, jclass jobj){
return (*env)->NewStringUTF(env,"Hi Java");
}

3.1.2 区分jobject与jclass


在JNI侧声明Java native方法的实现的时候,会有两个默认形参(除开native方法自己的传入参数),分别是JNIEnv指针,另外一个是jobject/jclass,这两个的区别在于:



  • jobject:如果Java侧的native方法是非静态的,那么传给JNI的第二个参数是类的对象,所有类的对象在JNI侧都是jobject类型。

  • jclass:如果Java侧的native方法是静态的,那么传给JNI的第二个参数是类的运行时类,所有运行时类在JNI侧都是jclass类型。

显然,这段JNI代码是native方法在JNI侧的实现,其中先后



  1. 创建了一个jintArray;

  2. 调用了Java侧JNICallJavaMethod类的构造方法;

  3. JNICallJavaMethod类的非静态方法;

  4. JNICallJavaMethod类的静态方法;

  5. JNICallJavaMethodNative类的非静态方法。


我们分别分析,以下代码就是上述代码的分别分析。


调用构造函数


步骤:



  1. 加载类,被调用方法所在类的运行时类,即jclass

  2. 获取方法ID,即jmethodID

  3. 创建一个类的实例,即jobject

  4. 调用方法。


注意:



  1. MethodName形参直接传""即可;

  2. 构造函数在Java侧没有返回值,连void都不是;但是,在JNI侧的方法签名中,返回值是void。比如,一个类的默认构造函数的签名是**"()V"**;

  3. 凡是JNI方法的*GetXxx()*过程,都必须进行异常处理,即使用前判断是否为NULL。


代码:


//todo:调用另一个类的构造函数
jclass jclz1 = NULL;
jclz1 = (*env)->FindClass(env, "JNICallJava/JNICallJavaMethod");
if(jclz1 == NULL){
printf("JNI Side : jclz is NULL.");
return ji;
}
jmethodID jmethodId1 = NULL;
jmethodId1 = (*env)->GetMethodID(env, jclz1, "", "()V");
if(jmethodId1 == NULL){
printf("JNI Side : jmethodId1 is NULL.");
return ji;
}
jobject jobj1 = (*env)->NewObject(env, jclz1, jmethodId1);
(*env)->CallVoidMethod(env, jobj1, jmethodId1);

调用非静态方法


步骤:



  1. 加载类,被调用方法所在类的运行时类,即jclass

  2. 获取方法ID,即jmethodID

  3. 创建一个类的实例,即jobject

  4. 调用方法。


注意:



  1. 因为该非静态方法与上述构造方法同属一个类,所以此时可以省去加载运行时类的步骤一,直接用已经获取到的jclass;

  2. Java侧来说,一个类的对象可以调用多个方法;但是JNI侧的jobject是与jmethodID一一对应的。所以,即使JNI侧调用的不同方法属于同一个类,也需要创建不同的jobject,不能共用;从创建jobject的JNI函数可以看出来:


//jobject与jmethodID是一一对应的关系:
jobject jobj1 = (*env)->NewObject(env, jclz1, jmethodId1);

代码:


//todo:调用另一个类的非静态方法
jmethodID jmethodId2 = NULL;
jmethodId2 = (*env)->GetMethodID(env, jclz1, "func", "(I)I");
if(jmethodId2 == NULL){
return ji;
}
jobject jobj2 = (*env)->NewObject(env, jclz1, jmethodId2);
jint i1 = (*env)->CallIntMethod(env, jobj2, jmethodId2, 5);
printf("JNI Side : func returns %d.\n", i1);

调用静态方法


步骤:



  1. 加载类,被调用方法所在类的运行时类,即jclass

  2. 获取方法ID,即jmethodID

  3. 调用方法。


注意:



  1. 因为该非静态方法与上述构造方法同属一个类,所以此时可以省去加载运行时类的步骤一,直接用已经获取到的jclass;

  2. 因为静态方法的调用不需要对象实例,所以调用Java静态方法时,不需要jobject。


代码:


//todo:调用另一个类的静态方法
jmethodID jmethodId3 = NULL;
jmethodId3 = (*env)->GetStaticMethodID(env, jclz1, "staticFunc", "([I)I");
if(jmethodId3 == NULL){
printf("JNI Side : jmethodId3 is NULL.");
return ji;
}
jint i2 = (*env)->CallStaticIntMethod(env, jclz1, jmethodId3, jArr);
printf("JNI Side : staticFunc returns %d.\n", i2);

调用native方法所在类的方法


这里以非静态方法为例,因为Java侧方法与native方法在同一个类中,而JNI侧实现native方法时,会传入一个jclass/jobject,分别对应Java侧的native方法声明是static native/native。此时我们可以直接使用传入的jclass,或者利用**(*env)->GetObjectClass(env,jobj)**获取到运行时类。关键在于,调用哪个方法,首先需要加载该方法所在的类到JVM运行时环境中


代码:


//todo:调用与native方法同属一个类的方法
jclass jclz0 = NULL;
jclz0 = (*env)->GetObjectClass(env, jobj);
if(jclz0 == NULL){
printf("JNI Side : jclz0 is NULL.");
return ji;
}
jmethodID jmethodId4 = NULL;
jmethodId4 = (*env)->GetMethodID(env, jclz0,"func2","()Ljava/lang/String;");
if(jmethodId4 == NULL){
printf("JNI Side : jmethodId4 is NULL.");
return ji;
}
//todo:接收Java方法返回的字符串,并在JNI侧打印
jstring jstr = (jstring)(*env)->CallObjectMethod(env, jobj, jmethodId4);
char *ptr_jstr = (char *)(*env)->GetStringUTFChars(env,jstr,0);
printf("JNI Side : func2 returns %s\n",ptr_jstr);

JNI调用Java方法答疑


JNI侧如何创建整形数组

步骤:



  1. 声明数组名字与数组长度,即jArr、4

  2. 获取数组元素类型(jint型)的指针,通过调用(*env)->GetIntArrayElements(env, jArr, NULL)

  3. 利用指针,为元素赋值;

  4. 释放指针资源,数组得以保留。


代码:


//todo:JNI侧创建一个int array
jintArray jArr = (*env)->NewIntArray(env, 4);//步骤1
jint *arr = (*env)->GetIntArrayElements(env, jArr, NULL);//步骤2
arr[0] = 0;//步骤3
arr[1] = 10;
arr[2] = 20;
arr[3] = 30;
(*env)->ReleaseIntArrayElements(env,jArr,arr,0);//步骤4

Java侧方法返回String,JNI调用时如何打印返回值?

步骤:



  1. 定义jstring变量,并用(jstring)强转jobject;

  2. 定义字符型指针,并用 (char *)强转;

  3. 打印。


//todo:接收Java方法返回的字符串,并在JNI侧打印
jstring jstr = (jstring)(*env)->CallObjectMethod(env, jobj, jmethodId4);
char *ptr_jstr = (char *)(*env)->GetStringUTFChars(env,jstr,0);
printf("JNI Side : func2 returns %s\n",ptr_jstr);

JNI侧与Java侧的控制台打印顺序

结论是:


JNI侧的控制台打印一定出现在Java侧程序运行结束之后。


我们可以调试看现象:


image.png


两遍I am constructor?

:在调用构造方法和非静态方法的两个调用过程中,都需要通过(*env)->NewObject(env, jclz, jmethodId)创建与jmethodID一一对应的jobject,所以调用了两次构造函数。


两遍func is called?

:待解答!


能否脱离native方法的实现来调用Java侧方法?

:可以,JNI是Java跨平台的实现机制,是Java与原生代码交互的机制。上述的过程我们一般都是在JNI侧的native方法实现中进行的,因为native方法的JNI实现中就有JNIEnv*指针,是获取JNIEnv*最容易的方式,并非唯一方式。如何获取JNIEnv*?待解答!


4.3 JNI处理从Java传来的字符串


Java与C字符串的区别



  • Java内部使用的是utf-16 16bit 的编码方式;

  • JNI里面使用的utf-8 unicode编码方式,英文是1个字节,中文3个字节;

  • C/C++ 使用ASCII编码,中文的编码方式GB2312编码,中文2个字节。


image.png


实战代码


//Java:
package JNICallJava;

public class GetSetJavaString {
static {
System.load("E:\\_Projects_\\JNI_Projects\\JNI_C\\cmake-build-debug\\libGetSetJavaStringLib.dll");
}
public static native String func(String s);
public static void main(String[] args) {
String str = func("--Do you enjoy coding?");
System.out.println(str);
}
}

//C:
#include "stdio.h"
#include "jni.h"
JNIEXPORT jstring JNICALL Java_JNICallJava_GetSetJavaString_func
(JNIEnv *,jclass,jstring)
;//没有用专门的.h文件,此声明可写可不写。

JNIEXPORT jstring JNICALL Java_JNICallJava_GetSetJavaString_func
(JNIEnv *env,jclass jclz,jstring jstr)
{
const char *chr = NULL;//字符指针定义与初始化分开
jboolean iscopy;//判断jstring转成char指针是否成功
chr = (*env)->GetStringUTFChars(env,jstr,&iscopy);//&iscopy位置一般直接传入NULL就好
if(chr == NULL){
return NULL;//异常处理
}
char buf[128] = {0};//申请空间+初始化
sprintf(buf,"%s\n--Yes, I do.",chr);//字符串拼接
(*env)->ReleaseStringUTFChars(env,jstr,chr);//编程习惯,释放内存
return (*env)->NewStringUTF(env,buf);
}

//CMakeLists.txt
add_library(GetSetJavaStringLib SHARED GetSetJavaString.c)

运行结果


image.png


异常处理


上述代码实例中,GetStringUTFChars()方法将JNI的jstring变量转换成C语言能操作的char指针,这个过程可能失败,其实任何转换过程都可能失败,这些过程的目标变量的定义和初始化都需要分开进行,并通过判空进行异常处理


C语言字符串拼接


在C语言中,没有String,字符串都是字符指针。其拼接过程不像Java等语言那么简单,分为以下过程:



  1. malloc申请空间

  2. 初始化

  3. 拼接字符串

  4. 释放内存


灵活的静态注册



  • 此实战代码中,我们没有想一般的静态注册一样使用Java native产生的.h文件,而是直接在实现JNI方法之前写了一个JNI静态注册,这也是可行的,甚至这个提前的声明注册也是可以不写的。此时我们在CMakeLists.txt中的add_library()中值包含了该.c文件。核心在于add_library()中一定要包含native方法在JNI的实现函数,.h文件更多是Java命令生成的教你怎么写JNI实现的一个辅助,无关紧要。

  • JNI无视Java侧的访问控制权限,但会区别静态或非静态。


5 JNI引用


5.1 三种引用


只有当JNI返回值是jobject的引用,才是三种引用之一。


比如(*env)->GetMethodID()返回值就不是引用,是一个结构体。


局部引用



  • 绝大部分JNI方法返回的是局部引用;

  • 局部引用的作用域或者生命周期始于创建它的本地方法,终止于本地方法的返回;

  • 通常在局部引用不再使用时,可以显式使用**DeleteLocalRef()**方法来提前释放它所指向的对象,一边GC回收;

  • 局部引用时线程相关的,只能在创建他的线程里面使用,通过全局变量缓存并使用在其他线程是不合法的。


全局引用


调用NewGlobalRef()基于局部引用创建,会阻止GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,必须调用DeleteGlobalRef()手动释放。


弱全局引用


调用NewWeakGlobalRef()基于局部引用创建,不会阻止GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,必须调用DeleteWeakGlobalRef()手动释放。


5.2 野指针


上一次创建的东西在程序结束的被回收了,但是静态局部变量未释放,不为NULL。


作业1:写代码实现访问java 非静态和静态方法,返回值必须是object类型
作业2:写代码体会野指针异常


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

跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统

前言 跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企...
继续阅读 »
前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

在以flutter为底的app项目中,用户登录,退出等认证必须做在flutter项目里,那么采用何种状态管理,来全局管理用户认证呢?
今天我就借助flutter_bloc这个库来搭建一套可以复用的成熟用户认证系统


搭建前夕准备


一、我们需要了解现有app有多少认证事件,那么常规来说,流程如下:


1、启动app,判断有无token,有token则跳转首页获取数据,无token则跳转需要授权页面如登录页 

2、登录页登录,登陆后保存token,跳转首页 

3、退出登录,删除token跳转需要授权页


那么总结起来就有三种事件

1、启动事件 

2、登录事件 

3、退出登录事件


二、那么有了认证事件,我们还需要有几个认证状态,有哪些状态呢,我来梳理一下:


1、在app启动后,需要初始化用户状态,那么用户当前是一个身份需要初始化的状态 

2、如果有token,或者用户登录后那么用户就是一个已认证的状态 

3、如果用户退出登录,那么用户当前是未认证的状态


三、咱们还需要做一个用户认证接口,接口主要是为了解耦,为了后期扩展能力、接口需要有哪些内容呢继续梳理一下:


1、是否有token,token是决定app是否认证的关键 

2、删除token,退出登录需要删除 

3、保存token,登录需要保存 

4、跳转授权页面 

5、跳转非授权页面


准备好如上工作,那么我们开始搭建用户认证系统吧


1、先编写认证事件:


part of 'authentication_bloc.dart';

//App认证事件,一般来说有三种,启动认证,登录认证,退出认证
abstract class AuthenticationEvent extends Equatable {
const AuthenticationEvent();

@override
List get props => [];
}
//App启动事件
class AppStart extends AuthenticationEvent{}
//App登录事件
class LogIn extends AuthenticationEvent{
final String token;

LogIn(this.token);

@override
List get props => [token];

@override
String toString() =>"LoggedIn { token: $token }";
}
//App退出事件
class LogOut extends AuthenticationEvent{}

2、编写认证状态


part of 'authentication_bloc.dart';

/// 认证状态 
abstract class AuthenticationState extends Equatable {
const AuthenticationState();
@override
List get props => [];
}

/// - uninitialized - 身份验证未初始化
class AuthenticationUninitialized extends AuthenticationState {}

/// - authenticated - 认证成功
class AuthenticationAuthenticated extends AuthenticationState {}

/// - unauthenticated - 未认证
class AuthenticationUnauthenticated extends AuthenticationState {}

3、编写外部接口



abstract class IUserAuthentication{

bool hasToken();

void saveToken(String token);

void deleteToken();

void authPage();

void unAuthPage();
}

4、有了如上的内容咱们就可以编写核心逻辑bloc了


import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'i_user_authentication.dart';

part 'authentication_event.dart';
part 'authentication_state.dart';

class AuthenticationBloc extends Bloc {

final IUserAuthentication iUserAuthentication;

/// 初始化认证是未认证状态
AuthenticationBloc(this.iUserAuthentication) : super(AuthenticationUninitialized());

@override
Stream mapEventToState(
AuthenticationEvent event,
)
async*
{
if(event is AppStart){
// 判断是否有Token
if(iUserAuthentication.hasToken()){
yield AuthenticationAuthenticated();
} else {
yield AuthenticationUnauthenticated();
}
}else if(event is LogIn){
iUserAuthentication.saveToken(event.token);
yield AuthenticationAuthenticated();
}else if(event is LogOut){
iUserAuthentication.deleteToken();
yield AuthenticationUnauthenticated();
}
}
}

为了使用方便咱们需要做一个工具类来支撑外部使用



import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'authentication_bloc.dart';

class Authentication{

static TransitionBuilder init({
TransitionBuilder? builder,
})
{
return (BuildContext context, Widget? child) {
var widget = BlocListener(
listener: (context, state) {
var bloc = BlocProvider.of(context);
if (state is AuthenticationAuthenticated) {
bloc.iUserAuthentication.authPage();
} else if (state is AuthenticationUnauthenticated) {
bloc.iUserAuthentication.unAuthPage();
}
},
child: child,
);
if (builder != null) {
return builder(context, widget );
} else {
return widget;
}
};
}
}

使用


在项目中如何使用呢?? 

1、接口事件类 

2、bloc初始化 

3、监听初始化


代码如下:
接口实现类


class Auth implements IUserAuthentication{
static final String userTokenN = 'userToken';

Auth(){
_userMMKV = MMKVStore.appMMKV(name: "123");
}
@override
void authPage() {
RouterName.navigateTo(LibRouteNavigatorObserver.instance.navigator!.context, RouterName.home,clearStack: true);

}
late MMKV _userMMKV;

@override
void deleteToken() {
_userMMKV.removeValue(userTokenN);
}

@override
bool hasToken() {
return _userMMKV.decodeString(userTokenN)!=null;
}

@override
void saveToken(String token) {
_userMMKV.encodeString(userTokenN, token);
}

@override
void unAuthPage() {
RouterName.navigateTo(LibRouteNavigatorObserver.instance.navigator!.context, RouterName.login,replace: true);
}

}

2、初始化


MultiBlocProvider(
providers: [
//AuthenticationBloc bloc初始化
BlocProvider(create: (_) => AuthenticationBloc(Auth())),
],
child: MaterialApp(
...
builder: Authentication.init() //监听初始化
),
);

3、事件调用


1、退出按钮调用,BlocProvider.of(context).add(LogOut()) 

2、登录页面调用,BlocProvider.of(context).add(LogIn("123")) 

3、SplashPage页面调用 BlocProvider.of(context).add(AppStart())


大功告成


如上搭建的一个用户认证系统,可以抽离项目做成package,再下次开发其他项目时候,就可以直接使用。方便快捷。

收起阅读 »

Kotlin 1.5 新特性 Inline classes,还不了解一下?

Kotlin 1.5 如约而来了。 如果你正在使用Android Studio 4.2.0 、IntelliJ IDEA 2020.3 或更高的版本,近期就会收到 Kotlin 1.5 的Plugin推送了。作为一个大版本,1.5带来了不少新特性,其中最主要的...
继续阅读 »

Kotlin 1.5 如约而来了。


如果你正在使用Android Studio 4.2.0 、IntelliJ IDEA 2020.3 或更高的版本,近期就会收到 Kotlin 1.5 的Plugin推送了。作为一个大版本,1.5带来了不少新特性,其中最主要的要数inline class了。


早在kotlin 1.3 就已经有了 inline class 的alpha版本。到 1.4.30 进入 beta,如今在 1.5.0 中 终于迎来了 Stable 版本。早期的实验版本的 inline 关键字 在 1.5 中被废弃,转而变为 value关键字


//before 1.5
inline class Password(private val s: String)

//after 1.5 (For JVM backends)
@JvmInline
value class Password(private val s: String)

个人很认同从 inline 变为 value 的命名变化,这使得其用途更为明确:



inline class 主要就是用途就是更好地 "包装" value



有时为了语义更有辨识度,我们会使用自定义class包装一些基本型的value,这虽然提高了代码可读性,但额外的包装会带来潜在的性能损失,基本型的value由于被在包装在其他class中,无法享受到jvm的优化(由堆上分配变为栈上分配)。 而 inline class 在最终生成的字节码中被替换成其 “包装”的 value, 进而提高运行时的性能。


// For JVM backends
@JvmInline
value class Password(private val s: String)

如上,inline class 构造参数中有且只能有一个成员变量,即最终被inline到字节码中的value。


val securePassword = Password("Don't try this in production")

如上,Password实例在字节码中被替换为String类型"Don't try this in production"


PS:如何安装 Kotlin 1.5



  1. 首先更新IDE的 Kotlin Plugin,如果没收到推送,可以手动方式升级:


Tools > Kotlin > Configure Kotlin Plugin Updates



  1. 配置languageVersion & apiVersion


compileKotlin {
kotlinOptions {
languageVersion = "1.5"
apiVersion = "1.5"
}
}



经 inline 处理后代码




inline classes 转化为字节码后究竟是怎样的呢?


fun check(password: Password) {
//...
}

fun main() {
val securePassword = Password("Don't try this in production")
check(securePassword)
}

对于Password这个inline class, 字节码反编译的产物如下:


   public static final void check_XYhEtbk/* $FF was: check-XYhEtbk*/(@NotNull String password) {
Intrinsics.checkNotNullParameter(password, "password");
}

public static final void main() {
String securePassword = Password.constructor-impl("Don't try this in production");
check-XYhEtbk(securePassword);
}

// $FF: synthetic method
public static void main(String[] var0) {
main();
}


  • securePassword 的类型由Password替换为String

  • check方法改名为check_XYhEtbk,签名类型也有 Password 替换 String


可见,无论是变量类型或是函数参数类型,所有的inline classes都被替换为其包装的类型。


名字被混淆处理(check_XYhEtbk)主要有两个目的



  1. 防止重载函数的参数经过 inline 后出现相同签名的情况

  2. 防止从Java侧调用到参数经过 inline 后的方法




Inline class 的成员




inline class 具备普通class的所有特性,例如拥有成员变量、方法、初始化块等


@JvmInline
value class Name(val s: String) {
init {
require(s.length > 0) { }
}

val length: Int
get() = s.length

fun greet() {
println("Hello, $s")
}
}

fun main() {
val name = Name("Kotlin")
name.greet() // `greet()`作为static方法被调用
println(name.length) // property getter 也是一个static方法
}

但是,inline class 的成员不能有自己的幕后属性,只能作为代理使用。 inline class的创建的对象在字节码中会被消除,所以这个实例无法拥有自己的状态以及行为,对inline class 实例的方法调用,在实际运行时会变为一格静态方法调用。




Inline class 的继承




interface Printable {
fun prettyPrint(): String
}

@JvmInline
value class Name(val s: String) : Printable {
override fun prettyPrint(): String = "Let's $s!"
}

fun main() {
val name = Name("Kotlin")
println(name.prettyPrint()) // prettyPrint()也是一个 static方法调用
}

inline class 可以实现任意inteface, 但不能继承自class。因为在运行时将无处安放其父类的属性或状态。如果你试图继承另一个Class,IDE会提示错误:Inline class cannot extend classes




自动拆装箱




inline class 在字节码中并非总被消除,有时也是需要存在的。例如当出现在泛型中、或者以 Nullable 类型出现时,此时它会根据情况自动与被包装类型进行转换,实现像Integerint那样的自动拆装箱


@JvmInline
value class WrappedInt(val value: Int)

fun take(w: WrappedInt?) {
if (w != null) println(w.value)
}

fun main() {
take(WrappedInt(5))
}

如上,take 接受一个 Nulable 的 WrappedInt 后进行 print 处理


public static final void take_G1XIRLQ(@Nullable WrappedInt w) {
if (Intrinsics.areEqual(w, (Object)null) ^ true) {
int var1 = w.unbox_impl();
System.out.println(var1);
}
}

public static final void main() {
take_G1XIRLQ(WrappedInt.box_impl(WrappedInt.constructor_impl(5)));
}

字节码中,take的参数并没有变为Int,而仍然是原始类型 WrappedInt。因此,在 take 的调用处,需要通过box_impl 做装箱处理, 而在take的实现中,通过 unbox_impl 拆箱后再进行print


同理,在泛型方法或者泛型容器中使用 inline class 时,需要通过装箱保证传入其原始类型:


genericFunc(color)         // boxed
val list = listOf(color) // boxed
val first = list.first() // unboxed back to primitive

反之,从容器获取 item 时,需要拆箱为被包装类型。



关于自动拆装箱在开发中无需太在意,只要知道有这个特性存在即可。





对比其他类型




与 type aliases 的区别 ?


inline class 与 type aliases 在概念上有点相似,都会在编译后被替换为被代理(包装)的类型, 区别在于



  • inline class 本身是实际存在的Class 只是在字节码中被消除了并被替换为被包装类型

  • type aliases仅仅是个别名,它的类型就是被代理类的类型。


typealias NameTypeAlias = String

@JvmInline
value class NameInlineClass(val s: String)

fun acceptString(s: String) {}
fun acceptNameTypeAlias(n: NameTypeAlias) {}
fun acceptNameInlineClass(p: NameInlineClass) {}

fun main() {
val nameAlias: NameTypeAlias = ""
val nameInlineClass: NameInlineClass = NameInlineClass("")
val string: String = ""

acceptString(nameAlias) // OK: NameTypeAlias等同String,可以传递
acceptString(nameInlineClass) // Not OK: NameInlineClass 与 String是两个类,不能等同

// 反之亦然:
acceptNameTypeAlias(string) // OK: 传入String也是可以的
acceptNameInlineClass(string) // Not OK: String不等同于NameInlineClass
}

与 data class 的区别 ?


inline class 与 data class 在概念上也很相似,都是对一些数据的包装,但是区别很明显



  • inline class 只能有一个成员属性,其主要目的是通过一个额外类型的包装让代码更易用

  • data clas 可以有多个成员属性,其主要目的是更高效地处理一组相关数据的集合




使用场景


上面说到, inline class 的目的是通过包装让代码更易用,这个易用性体现在诸多方面:


场景1:提高可读性


fun auth(userName: String, password: String) { println("authenticating $userName.") }

如上, auth的两个参数都是String,缺乏辨识度,即使像下面这样传错了也难以发觉


auth("12345", "user1") //Error

@JvmInline value class Password(val value: String)
@JvmInline value class UserName(val value: String)

fun auth(userName: UserName, password: Password) { println("authenticating $userName.")}

fun main() {
auth(UserName("user1"), Password("12345"))
//does not compile due to type mismatch
auth(Password("12345"), UserName("user1"))
}

使用 inline class 使的参数更具辨识度,避免发生错误


场景2:类型安全(缩小扩展函数作用域)


inline fun <reified T> String.asJson() = jacksonObjectMapper().readValue<T>(this)

String类型的扩展方法asJson可以转化为指定类型T


val jsonString = """{ "x":200, "y":300 }"""
val data: JsonData = jsonString.asJson()

由于扩展函数是top-level的,所有的String类型都可以访问,造成污染


"whatever".asJson<JsonData> //will fail

通过inline class可以将Receiver类型缩小为指定类型,避免污染


@JvmInline value class JsonString(val value: String)

inline fun <reified T> JsonString.asJson() = jacksonObjectMapper().readValue<T>(this.value)

如上,定义JsonString,并为之定义扩展方法。


场景3:携带额外信息


/**
* parses string number into BigDecimal with a scale of 2
*/
fun parseNumber(number: String): BigDecimal {
return number.toBigDecimal().setScale(2, RoundingMode.HALF_UP)
}

fun main() {
println(parseNumber("100.12212"))
}

如上,parseNumber的功能是将任意字符串解析成数字并保留小数点后两位。


如果我们希望通过一个类型将解析前后的值都保存下来然后分别打印,可能首先想到的使用Pair或者data class。但是当这两个值之间是有换算关系时,其实也可以用inline class实现。如下


@JvmInine value class ParsableNumber(val original: String) {
val parsed: BigDecimal
get() = original.toBigDecimal().setScale(2, RoundingMode.HALF_UP)
}

fun getParsableNumber(number: String): ParsableNumber {
return ParsableNumber(number)
}

fun main() {
val parsableNumber = getParsableNumber("100.12212")
println(parsableNumber.parsed)
println(parsableNumber.original)
}

ParsableNumber的包装类型是String,同时通过parsed携带了解析后的值。如前文提到的那样,字节码中,parsed getter 会以static方法的形式存在,因此虽然携带了更多信息,但实际上并不存在这样一个包装类实例:


@NotNull
public static final String getParsableNumber(@NotNull String number) {
Intrinsics.checkParameterIsNotNull(number, "number");
return ParsableNumber.constructor_impl(number);
}

public static final void main() {
String parsableNumber = getParsableNumber("100.12212");
BigDecimal var1 = ParsableNumber.getParsed_impl(parsableNumber);
System.out.println(var1);
System.out.println(parsableNumber);
}



最后




Inline class 是个好工具,在提高代码的可读性、易用性的同时,不会造成性能的损失。 早期由于一直处于试验状态没有被大家所熟知, 随着如今在 Kotlin 1.5 中的转正,相信未来一定会被在更广泛地使用、发掘更多应用场景。


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

Flutter 2021 中的按钮

在本文中,我们将介绍令人惊叹的 Flutter 按钮,它们可以帮助所有初学者或专业人士为现代应用程序设计漂亮的 UI。 首先让我告诉你关于 Flutter 中按钮的一件重要事情,在flutter最新版本中以下Buttons在fluter中被废弃了:废弃的推荐的...
继续阅读 »

在本文中,我们将介绍令人惊叹的 Flutter 按钮,它们可以帮助所有初学者或专业人士为现代应用程序设计漂亮的 UI。


首先让我告诉你关于 Flutter 中按钮的一件重要事情,在flutter最新版本中以下Buttons在fluter中被废弃了:

废弃的推荐的替代
RaisedButtonElevatedButton
OutlineButtonOutlinedButton
FlatButtonTextButton

那么让我们来探索一下 Flutter 中的按钮。



Elevated Button


StadiumBorder


image.png


ElevatedButton(
onPressed: (){},
child: Text('Button'),
style: ElevatedButton.styleFrom(
shadowColor: Colors.green,
shape: StadiumBorder(),
padding: EdgeInsets.symmetric(horizontal: 35,vertical: 20)),
)

image.png


RoundedRectangleBorder


ElevatedButton(
onPressed: (){},
child: Text('Button'),
style: ElevatedButton.styleFrom(
shadowColor: Colors.green,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),

CircleBorder


image.png


ElevatedButton(
onPressed: () {},
child: Text('Button'),
style: ElevatedButton.styleFrom(
shape: CircleBorder(),
padding: EdgeInsets.all(24),
),
)

BeveledRectangleBorder


image.png


ElevatedButton(
onPressed: () {},
child: Text('Button'),
style: ElevatedButton.styleFrom(
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.circular(12)
),
),
)

Outlined Button


StadiumBorder
image.png


OutlinedButton(
onPressed: () {},
child: Text('Button'),
style: OutlinedButton.styleFrom(
shape: StadiumBorder(),
),
)

RoundedRectangleBorder


image.png


OutlinedButton(
onPressed: () {},
child: Text('Button'),
style: OutlinedButton.styleFrom(
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
)

CircleBorder


image.png


OutlinedButton(
onPressed: () {},
child: Text('Button'),
style: OutlinedButton.styleFrom(
shape: CircleBorder(),
padding: EdgeInsets.all(24),
),
)

BeveledRectangleBorder


image.png


OutlinedButton(
onPressed: () {},
child: Text('Button'),
style: OutlinedButton.styleFrom(
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
)

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

一个录音项目的开发总结(一)

iOS
最近,工作之余,自己做了一个项目,项目的一期主要功能是音频录制和播放,音频格式包含m4a、mp3、wav三种格式,录制过程中要支持变音,还要能获取到metering以绘制录音过程的声音强弱变化图,播放功能包括音频波形图的绘制以及音频播放。 在做之前,我对iOS...
继续阅读 »

最近,工作之余,自己做了一个项目,项目的一期主要功能是音频录制和播放,音频格式包含m4a、mp3、wav三种格式,录制过程中要支持变音,还要能获取到metering以绘制录音过程的声音强弱变化图,播放功能包括音频波形图的绘制以及音频播放。


在做之前,我对iOS中的录音方面的知识了解甚少, 以至于走了很多弯路,虽然浪费了很多时间,但是也从中学到了很多知识,最终完成了项目的编码。


以下,主要介绍录音方面开发总结,播放方面后续记录。


在iOS中如果想实现录音功能,那么有四种方式可以实现:



  1. AVAudioRecorder :这是最简单的录音方式,只需要配置好录音格式就能得到相应的文件,但是相应的,这种方式无法得到录音过程中的音频源数据,无法实现变音功能和录制mp3文件。

  2. AVAudioEngine:AVAudioEngine功能强大,能实现录音、播放、混响、变音等功能,当我发现我这个类时,我高兴坏了,看了很多文档,结果做demo时,发现这个类外强中干,譬如你不能改变AudioEngine默认的inputNode、outputNode、mainMixNode的数据format,即使你千辛万苦找方法成功改变了数据格式,输出的数据也会狠狠地扇你一巴掌,告诉你高兴太早了,而且inputNode、outputNode、mainMixNode不会自动转换数据格式......如果想录制m4a、mp3还是需要其他方式才能实现。

  3. AudioQueue & AudioFile:AudioQueue在录音过程中有个回调方法,抛出编码后的音频数据,如果想处理音频数据,譬如变音、转码,可以在这个回调中实现,而且能获取到metering数据; AudioFile用于存储回调方法中抛出的音频数据,这个音频数据要与AudioFile创建时配置的inFormat一致。如果想了解audioqueue,可以看看这个文档

  4. AudioUnit &  ExtAudioFile:AudioUnit在录音过程中也会有回调,AudioUnit只支持pcm录制,所以要辅以ExtAudioFile来实现其他音频格式的录制,ExtAudioFile是个宝藏类,这个类可以实现音频格式的自动转换。


如果只是单纯的录用,那么使用AVAudioRecorder就可以非常简单的实现了,如果想对音频数据做处理,那么对于录音过程中抛出的数据请务必是pcm,只要能拿到pcm源数据,任何能做到的音频处理只要你想,都能实现。


iOS中,系统支持的录音编码格式为:


官方参考文档点击查看



wav文件是无损编码格式pcm,m4a文件编码格式AAC,mp3文件iOS系统不支持录制,系统支持mp3文件播放,有解码器但是无相应编码器,如果想录制mp3 文件只能依托于第三方的编码库,我采用的是lame库。


变音


iOS系统中本身是支持变音功能的,AudioUnit中有一个属性kNewTimePitchParam_Pitch,可以改变声音的音色,但是这个属性是MixerUnit的属性,录音的OutputUnit不支持这个属性,要想将 混音MixerUnit和录音OutputUnit连接到一块,需要AUGraph类去连接,但是AUGraph已弃用。。。系统推荐去使用AVAudioEngine类,这个类也是个坑,我在做demo时无法实现m4a文件的录制,就放弃了。


所以变音我采用的也是三方类SoundTouch,SoundTouch支持pcm编码、音频数据是lettle-endian,所以录音过程中抛出的音频buffer的format必须是pcm的,不然无法实现边录音边变音。


这样看起来AudioUnit貌似更适合这个项目一些,但是AudioUnit无法获取到录音过程中的metering数据,真是个令人悲伤的事情,AudioUnit很强大,如果AUGraph不被弃用,可能我会用它来录音。


最终我的实现方案是:


1、wav文件录制:AudioQueue录音、 AudioFile存储文件,AudioQueue和AudioFile的dataFormat为:


AudioStreamBasicDescription mDataFormat;

mDataFormat.mSampleRate = 44100;
mDataFormat.mChannelsPerFrame = 1;
mDataFormat.mReserved = 0;
mDataFormat.mBitsPerChannel = 16;
mDataFormat.mFramesPerPacket = 1;
mDataFormat.mSampleRate = self.model.sampleRate;
mDataFormat.mBytesPerFrame = mDataFormat.mBytesPerPacket = 2;
mDataFormat.mFormatID = kAudioFormatLinearPCM;
mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;


AudioFile的fileType为kAudioFileCAFType


2、mp3文件:AudioQueue录音, lame转码, FILE文件存储, AudioQueue的编码格式为:


AudioStreamBasicDescription mDataFormat;

mDataFormat.mSampleRate = 44100;
mDataFormat.mChannelsPerFrame = 1;
mDataFormat.mReserved = 0;
mDataFormat.mBitsPerChannel = 16;
mDataFormat.mFramesPerPacket = 1;
mDataFormat.mSampleRate = self.model.sampleRate;
mDataFormat.mBytesPerFrame = mDataFormat.mBytesPerPacket = 2;
mDataFormat.mFormatID = kAudioFormatLinearPCM;
mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger
| kLinearPCMFormatFlagIsPacked
|kAudioFormatFlagIsNonInterleaved


3、m4a文件:AudioQueue录音,  ExtAudioFile转码加存储,AudioQueue和ExtAudioFile的kExtAudioFileProperty_ClientDataFormat属性的编码格式为:


AudioStreamBasicDescription mDataFormat;

mDataFormat.mSampleRate = 44100;
mDataFormat.mChannelsPerFrame = 1;
mDataFormat.mReserved = 0;
mDataFormat.mBitsPerChannel = 16;
mDataFormat.mFramesPerPacket = 1;
mDataFormat.mSampleRate = self.model.sampleRate;
mDataFormat.mBytesPerFrame = mDataFormat.mBytesPerPacket = 2;
mDataFormat.mFormatID = kAudioFormatLinearPCM;
mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;


ExtAudioFile文件的音频编码格式为:


AudioStreamBasicDescription outputFormat;outputFormat.mSampleRate = 44100;

outputFormat.mFormatID = kAudioFormatMPEG4AAC;outputFormat.mFormatFlags = 0;outputFormat.mBytesPerPacket = 0;outputFormat.mFramesPerPacket = 1024;

outputFormat.mBytesPerFrame = 0;outputFormat.mChannelsPerFrame = 1;outputFormat.mBitsPerChannel = 0;outputFormat.mReserved = 0;


关于四种录音方式的代码实现后续会更新,音频编辑功能,二期可能会上,到时也会研究、记录,希望到时自己不会太懒......


作者:阿喵同学
链接:https://juejin.cn/post/6936869349546426382

收起阅读 »