环信即时通讯云

环信即时通讯云

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

环信开发文档

环信客服云

环信客服云

无需下载,注册即用
环信机器人

环信机器人

智能,高效
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

高薪捐卵联系方式--新闻发布

高薪捐卵联系方式【微信uri618】补偿1~10W,十天左右完成 打钱快 姿势帅 Only 12 Day慢慢地更加相信,幸福,就是找一个温暖的人,平平淡淡相守一辈子,因为那么多的轰轰烈烈,只能属于小说和电影。两颗心,只要相爱,即便粗茶淡饭,修篱种田,只要有你陪...
继续阅读 »

高薪捐卵联系方式【微信uri618】补偿1~10W,十天左右完成 打钱快 姿势帅 Only 12 Day慢慢地更加相信,幸福,就是找一个温暖的人,平平淡淡相守一辈子,因为那么多的轰轰烈烈,只能属于小说和电影。两颗心,只要相爱,即便粗茶淡饭,修篱种田,只要有你陪伴就好。你若不离,我便不弃,生生世世缠绵,分分秒秒相守。


收起阅读 »

正规高薪招聘捐卵卖卵子女孩--【新闻发布】

正规高薪招聘捐卵卖卵子女孩【╇薇:15918886504】补偿1~10W,十天左右完成 打钱快 姿势帅 Only 12 Day我是不是被世界遗忘了?”我沮丧地说,眼睛都不肯抬起来“为什么别人有这么多的技能,是老天他不开眼吗?”  ”不行,我要变强,我要让被遗忘...
继续阅读 »

正规高薪招聘捐卵卖卵子女孩【╇薇:15918886504】补偿1~10W,十天左右完成 打钱快 姿势帅 Only 12 Day我是不是被世界遗忘了?”我沮丧地说,眼睛都不肯抬起来“为什么别人有这么多的技能,是老天他不开眼吗?”
  ”不行,我要变强,我要让被遗忘在世界的我被人瞧得起!“我开始振作起来,继续向前走“乌龟能赢过兔子那我为什么不行?我镇定下来,“比自己小十倍的蚂蚁有什么可怕的!”我似乎有了超能力是的能跑的很快,上去就咬了蚂蚁一口,再用
  自己的头横一刀竖一刀把他们甩到菜叶上,他们也不甘示弱,跑到我的身上左一咬右一咬“哼,老天给我的超能力,不是白给的!
收起阅读 »

正规高薪招聘捐卵--【新闻发布】

正规高薪招聘捐卵【微信15918886504】【无需打开,直接添加,长期有效】【爱心捐卵】【打钱快】【姿势帅】【不拖欠】【安全靠谱】【有偿供卵】【补偿高】【时间短】

正规高薪招聘捐卵【微信15918886504】【无需打开,直接添加,长期有效】【爱心捐卵】【打钱快】【姿势帅】【不拖欠】【安全靠谱】【有偿供卵】【补偿高】【时间短】


广州孕妈招聘,136-8182-2252宝来喜代妈公司给您介绍一下!

    今日介绍:“广州孕妈招聘”(136-8182-2252)宝来喜代妈公司长期通过网络平台面向全国有偿招聘广州孕妈招聘,22-35岁,身体健康,价格22-30W。正规三甲医Y操作。公司规模为全上海包括全广州也是的实体广州孕妈招聘公司,确...
继续阅读 »
    今日介绍:“广州孕妈招聘”(136-8182-2252)宝来喜代妈公司长期通过网络平台面向全国有偿招聘广州孕妈招聘,22-35岁,身体健康,价格22-30W。正规三甲医Y操作。公司规模为全上海包括全广州也是的实体广州孕妈招聘公司,确保佣金也是同行业最高的,包吃包住,报销车费,有阿姨护士照顾孕妈的日常起居,成功率待遇全国领先。


无关=================================
桑平若有所思了一下,起身说:“我去顺子家一趟。你俩搁这儿跟你们嫂子说话。”

听到堂屋的钟铛铛响了八下,余笙不紧不慢的起床。她可能是整个桑树村里起来最晚的那一个。

郭小军看到全家福,认出主位上的男人。

看他杵在门口目瞪口呆地望着桑丽丽,布置碗筷的余笙说了他一句:“你小姑跟你小军叔来了,你咋不叫人啊。”

到了家门口,余笙让他别着急走。

以往桑平去砖厂,一忙就是两天不着家,回来也是歇一晚上就走了。

桑平整了一大盆温水端门口,受不了他侄子身上的那个味道,“臭死了,你是搁顺子家的猪圈打滚儿了吧!”

桑平憨笑:“我拿回去给我媳妇儿吃滴。她这两天胃口不好,我正想着给她弄点开胃的吃。”

桑平看不下去,“臭烘烘的就往你妹的被窝里钻。”

她掀被子下床,摸着黑循声而去。

余笙惊醒,发现身旁的男人枕着手臂目光灼灼的看着自己。

郭小军蠢蠢欲动,“那我…那我学!我好好学,回去我就跟我妈说,让她把我送回学校去!我要考高中考大学,考农业大学!”

余笙笑盈盈的说:“云妮儿是我让你三哥抱回来的。我打算把云妮儿和青子都养在身边。”

余笙抱起云妮儿,招呼上桑青往外走。
收起阅读 »

西安代妈公司有哪些,136-8182-2252宝来喜代妈公司给您介绍一下!

    今日介绍:“西安代妈公司有哪些”(136-8182-2252)宝来喜代妈公司长期通过网络平台面向全国有偿招聘西安代妈公司有哪些,22-35岁,身体健康,价格22-30W。正规三甲医Y操作。公司规模为全上海包括全广州也是的实体西安代妈...
继续阅读 »
    今日介绍:“西安代妈公司有哪些”(136-8182-2252)宝来喜代妈公司长期通过网络平台面向全国有偿招聘西安代妈公司有哪些,22-35岁,身体健康,价格22-30W。正规三甲医Y操作。公司规模为全上海包括全广州也是的实体西安代妈公司有哪些公司,确保佣金也是同行业最高的,包吃包住,报销车费,有阿姨护士照顾孕妈的日常起居,成功率待遇全国领先。


无关=================================
主要是,桑丽丽以为余笙这个城里女子眼高于顶看不惯他们这些粗野的乡下人,而她也看不惯余笙瞎讲究的做派。

云妮儿转身跑到他跟前来。

她进巷子里来的时候路过顺子家被破坏的院墙,一下子就跟得了被迫害妄想症一样,脑子里浮现出好些余笙被院墙砸到的片段,吓得心脏肝胆一颤一颤。

顺子坦然说:“我数学成绩烂的很。老师都已经放弃我嘞。这次的比赛,平均分在90分以上的同学才能参加。加上青子,我们学校就六个。”

说不定下次她再来空间,幼苗就长成小树了。

这边的河桥是石头垒起来的土桥,有好些年头了,年年被大雨过后暴涨的河水冲垮。年年修,还是年年垮。

她用的这台缝纫机还是新的,是桑平给她下的彩礼,包括他常骑的那辆自行车也是。

“不是一毛钱的事好呗!”桑青拔高声音,“我问他凭啥找我要两毛,他说哪次活动课我们一块儿打沙包,我砸疼他嘞。几百年前的事,我都不记得,他记得怪清楚,还恨上我嘞!就算有这回事,那我也不是故意的哦。我一生气就说‘我一毛钱都不给你,大不了我不坐你家的车,我自己走到县城的学校去’。他倒好,没完没了嘞,放学骑着他们家的自行车追着我要车费。我理都没理他就跑回来嘞。”

警察同志停那儿不再往前去,对他们虎视眈眈,手往腰间的警棍摸去。

好在这会儿雨下的不大。

“不要!我不许你离开我!”

桑丽丽不敢接着往下想。

桑英谁都不理,好脸对桑平说:“平子,只要你对我们家的俩孩儿好,将来有他们孝敬你的时候。”

桑平抄着手电筒打着灯光往他们仨脸上晃了一圈,冷冷地说:
收起阅读 »

【Jetpack篇】协程+Retrofit网络请求状态封装实战

前言 在App中,对于网络请求状态一般性的就分为加载中、请求错误、请求成功、请求成功但数据为null。为了用户体验,不同的状态需要对用户展示不同的界面,例如网络异常的提醒,点击重新请求等。 之前项目一直都是以Retrofit+RxJava+OkHttp为网络请...
继续阅读 »

前言


在App中,对于网络请求状态一般性的就分为加载中、请求错误、请求成功、请求成功但数据为null。为了用户体验,不同的状态需要对用户展示不同的界面,例如网络异常的提醒,点击重新请求等。


之前项目一直都是以Retrofit+RxJava+OkHttp为网络请求框架,RxJava已经很好的封装了不同的请求状态,onSubscribe、onNext、onError等,只需要在不同的回调中做出相应的动作就ok了。


RxJava很好用,但随着新技术的出现,RxJava的可替代性也就越高。Kotlin的协程就是这么一个存在。


本文是以Jetpack架构为基础,协程+Retrofit+Okhttp为网络请求框架,对不同的请求状态(loading,error,empty等)做了封装,让开发者不用再去关心哪里需要loading,哪里需要展示error提示。


同时,在封装的过程中,Jetpack和协程的使用也存在着几个坑,本文也将一一描述。


协程的基本使用



API:www.wanandroid.com/project/tre… 来自鸿洋大大的wanandroid



如果需要使用协程,则添加依赖


dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}
复制代码

在Retrofit2.6.0前,我们使用协程,api请求后返回的数据可以用Call或者Defeerd包裹处理,2.6后,可以直接返回数据,只不过需要加上suspend的修饰,如下:


interface ProjectApi {

@GET("project/tree/json")
suspend fun loadProjectTree(): BaseResp<List<ProjectTree>>
}
复制代码

因为使用的是Jetpack架构,所以将整个网络请求主要分为UI、ViewModel、Repository三层,以LiveData为媒介进行通信。


首先是Repository层进行网络请求,


 class ProjectRepo{
private lateinit var mService: ProjectApi

init {
mService = RetrofitManager.initRetrofit().getService(ProjectApi::class.java)
}

suspend fun loadProjectTree(): List<ProjectTree> {
return mService.loadProjectTree()
}
}
复制代码

利用Retrofit和OkHttp创建了一个apiService,内部细节在这里就先不展开,接着直接调用loadProjectTree()进行网络请求,将数据返回。loadProjectTree()用suspend关键字进行标记,Kotlin 利用此关键字强制从协程内调用函数。


接着ViewModel层,


class ProjectViewModel : ViewModel(){
//LiveData
val mProjectTreeLiveData = MutableLiveData<List<ProjectTree>>()
fun loadProjectTree() {
viewModelScope.launch(Dispatchers.IO) {
val data = mRepo.loadProjectTree()
mProjectTreeLiveData.postValue(data)
}
}
}
复制代码

创建类ProjectViewModel并继承ViewModel,内部新建一个LiveData做UI通信使用,利用viewModelScope.launch(Dispatchers.IO) 创建一个新的协程,然后在 I/O 线程上执行网络请求,请求的数据利用LiveData通知给UI。


这里提到了viewModelScope.launch(Dispatchers.IO)viewModelScope是一个协程的作用域,ViewModel KTX 扩展中已经将此作用域封装好,直接使用就可以。Dispatchers.IO 表示此协程在 I/O线程上执行,而launch则是创建一个新的协程。


最后是UI层,


class ProjectFragment : Fragment {

override fun initData() {
//请求数据,调用loadProjectTree
mViewModel?.loadProjectTree()
mViewModel?.mProjectTreeLiveData?.observe(this, Observer {
//更新UI
})
}
复制代码

UI层开始调用ViewModel的请求方法执行网络请求,LiveData注册一个观察者,观察数据变化,并且更新UI。


到这里,网络请求的逻辑基本上通顺了。


在一切环境正常的情况下,上面的请求是可以的,但是app还存在网络不畅,异常,数据为null的情况,上述就不在满足要求了,接下来就开始对数据异常的情况进行处理。


网络请求异常处理


对于协程异常的处理,Android开发者的官网上也给出了答案(developer.android.google.cn/kotlin/coro… ) ,直接对网络请求进行一个try-catch处理,发生异常了,直接在catch中做出相应动作就ok了,我们就来看看具体实现。


class ProjectViewModel : ViewModel(){
//LiveData
val mProjectTreeLiveData = MutableLiveData<List<ProjectTree>>()
fun loadProjectTree() {
viewModelScope.launch(Dispatchers.IO) {
try {
val data = mRepo.loadProjectTree()
mProjectTreeLiveData.postValue(data)
} catch (e: Exception) {
//异常
error(e)
} finally {

}
}
}
}
复制代码

还是在ViewModel层,对mRepo.loadProjectTree()的请求加上了try-catch块,当发生异常时根据Exception类型对用户做出提示。


到这里,异常的来源已经找到了,接着就需要将异常显示在UI层来提醒用户。我们都知道mProjectTreeLiveData利用PostValue将数据分发给了UI,如法炮制,也就可以利用LiveData将异常也分发给UI。


说干就干。


网络请求状态封装


1、 [Error状态]


依旧在ViewModel层,我们新添加一个针对异常的LiveData:errorLiveData


class ProjectViewModel : ViewModel(){
//异常LiveData
val errorLiveData = MutableLiveData<Throwable>()
//LiveData
val mProjectTreeLiveData = MutableLiveData<List<ProjectTree>>()
fun loadProjectTree() {
viewModelScope.launch(Dispatchers.IO) {
try {
val data = mRepo.loadProjectTree()
mProjectTreeLiveData.postValue(data)
} catch (e: Exception) {
//异常
error(e)
errorLiveData.postValue(e)
} finally {

}
}
}
}
复制代码

在UI层,利用errorLiveData注册一个观察者,如果有异常通知,则显示异常的UI(UI层代码省略)。这样确实可以实现我们一开始要的功能:请求成功则显示成功界面,失败显示异常界面。但是有一个问题,就是不够优雅,如果有多个ViewModel,多个UI,那就要每个页面都要写errorLiveData,很冗余。


那我们可以将公共方法抽离出来,新建一个BaseViewModel类,


open class BaseViewModel : ViewModel() {
val errorLiveData = MutableLiveData<Throwable>()

fun launch(
block: suspend ()
-> Unit,
error: suspend (Throwable) -> Unit,
complete: suspend () -> Unit
)
{
viewModelScope.launch(Dispatchers.IO) {
try {
block()
} catch (e: Exception) {
error(e)
} finally {
complete()
}
}
}


}
复制代码

除了定义errorLiveData外,还将新建协程的操作放到其中,开发者只需要将每个ViewModel继承BaseViewModel,重写launch()即可,那么上面的案例中的ViewModel就修改成下面这种,


class ProjectViewModel : BaseViewModel(){

//LiveData
val mProjectTreeLiveData = MutableLiveData<List<ProjectTree>>()
fun loadProjectTree() {
launch(
{
val state = mRepo.loadProjectTree()
mProjectTreeLiveData.postValue(state.data)
},
{
errorLiveData.postValue(it)
},
{
loadingLiveData.postValue(false)
}
)
}
}
复制代码

同样的,UI层也可以新建一个BaseFragment抽象类,在onViewCreated中利用errorLiveData注册观察者,收到异常通知,则进行相应的动作


abstract class BaseFragment<T : ViewDataBinding, VM : BaseViewModel> : Fragment(){

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mViewModel = getViewModel()

mViewModel?.errorLiveData?.observe(viewLifecycleOwner, Observer {
Log.d(TAG, "onViewCreated: error ")
showError()
throwableHandler(it)
})
}
}
复制代码

每个子Fragment只需要继承BaseFragment即可,具体的异常监听就不用开发者管理。


2、 [Loading状态]


除了异常状态外,请求必不可少的就是Loading,这里Loading分为两种,一种是整个页面替换为Loading,例如Recyclerview列表时,就可以直接整个页面先Loading,而后显示数据;还有一种是数据界面不替换,只是个Loading Dialog显示在上层,例如点击登录时,需要一个loading。


Loading和异常处理的思路一致,可以在BaseViewModel中添加一个LoadingLiveData,数据类型为Boolean,在每个请求一开始LoadingLiveData.postValue(true),结束请求或者请求异常时,就LoadingLiveData.postValue(false)。UI层BaseFragment中,则可以监听LoadingLiveData发出的是true还是false,以便对Loading的显示和隐藏进行控制。


ViewModel层:


open class BaseViewModel : ViewModel() {
//加载中
val loadingLiveData = SingleLiveData<Boolean>()
//异常
val errorLiveData = SingleLiveData<Throwable>()

fun launch(
block: suspend ()
-> Unit,
error: suspend (Throwable) -> Unit,
complete: suspend () -> Unit
)
{
loadingLiveData.postValue(true)
viewModelScope.launch(Dispatchers.IO) {
try {
block()
} catch (e: Exception) {
Log.d(TAG, "launch: error ")
error(e)
} finally {
complete()
}
}
}
}
复制代码

在BaseViewModel 中launch一开始就通知Loading显示,在try-catch-finally代码块的finally中将请求结束的通知分发出去。


UI层:


abstract class BaseFragment<T : ViewDataBinding, VM : BaseViewModel> : Fragment(){

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mViewModel = getViewModel()
//Loading 显示隐藏的监听
mViewModel?.loadingLiveData?.observe(viewLifecycleOwner, Observer {
if (it) {
//show loading
showLoading()
} else {

dismissLoading()
}
})

//请求异常的监听
mViewModel?.errorLiveData?.observe(viewLifecycleOwner, Observer {
Log.d(TAG, "onViewCreated: error ")
showError()
throwableHandler(it)
})
}
}
复制代码

注册一个loading的观察者,当通知为true时,显示loading,false则隐藏。


3、 [Empty状态]


数据为空的状态发生在请求成功后,对于这种情况,可以直接在UI层中,请求成功的监听中对数据是否为null进行判断。


到这里,网络请求的基本封装已经完成,但是在运行测试的过程中,存在几个问题需要去解决,例如网络不通的情况下try-catch却不会抛出异常。接下来就开始进行二次封装。


暴露问题二次封装


问题一:网络请求异常,try-catch却不会将异常抛出


因为业务场景比较复杂,只依赖try-catch来获取异常,明显也会有所遗漏,那这种情况下我们可以直接以服务器返回的code,作为请求状态的依据。以上面Wanandroid的api为例,当errorCode=0时,则表示请求成功,其他的值都表示失败,那这就好办了。


我们新建一个密封类ResState,存放Success和Error状态,


sealed class ResState<out T : Any> {
data class Success<out T : Any>(val data: T) : ResState<T>()
data class Error(val exception: Exception) : ResState<Nothing>()
}
复制代码

对Repository层请求返回的数据进行code判断处理,新建一个BaseRepository类,


open class BaseRepository() {

suspend fun <T : Any> executeResp(
resp: BaseResp<T>, successBlock: (suspend CoroutineScope.() -> Unit)? = null,
errorBlock: (suspend CoroutineScope.() -> Unit)? = null
): ResState<T> {
return coroutineScope {
if (resp.errorCode == 0) {
successBlock?.let { it() }
ResState.Success(resp.data)
} else {
Log.d(TAG, "executeResp: error")
errorBlock?.let { it() }
ResState.Error(IOException(resp.errorMsg))
}
}
}

}
复制代码

errorCode == 0时,将ResState置为Success并将数据返回,errorCode !=0时,则将状态置为Error并将Exception返回。而子Repository则只需要继承BaseRepository即可,


class ProjectRepo : BaseRepository() {

suspend fun loadProjectTree(): ResState<List<ProjectTree>> {
return executeResp(mService.loadProjectTree())
}
复制代码

修改后返回值用ResState<>包裹,并直接将请求的结果传给executeResp()方法,而ViewModel中也做出相应的修改,


class ProjectViewModel : BaseViewModel() {
val mProjectTreeLiveData = MutableLiveData<List<ProjectTree>>()

fun loadProjectTree() {
launch(
{
val state = mRepo.loadProjectTree()
//添加ResState判断
if (state is ResState.Success) {
mProjectTreeLiveData.postValue(state.data)
} else if (state is ResState.Error) {
Log.d(TAG, "loadProjectTree: ResState.Error")
errorLiveData.postValue(state.exception)
}
},
{
errorLiveData.postValue(it)
},
{
loadingLiveData.postValue(false)
}
)
}
}
复制代码

ViewModel层新增了一个ResState判断,通过请求的返回值ResState,如果是ResState.Success则将数据通知给UI,如果是ResState.Error,则将异常通知给UI。


服务器返回的code值进行判断,无疑是最准确的。


问题二:errorLiveData注册观察者一次后,不管请求失败还是成功,它还是会收到通知。


这是MutableLiveData的一个特性,只要当注册的观察者处于前台时,都会收到通知。那这个特性又影响了什么呢?
我在errorLiveData的监听中,对不同的异常进行了Toast的弹出提醒,如果每次进入一个页面,虽然请求成功了,但是因为errorLiveData还是能接收到通知,就会弹出一个Toast提醒框。现象如下:


dem.gif


那我们针对MutableLiveData将其修改为单事件响应的liveData,只有一个接收者能接收到信息,可以避免不必要的业务的场景中的事件消费通知。


class SingleLiveData<T> : MutableLiveData<T>() {

private val mPending = AtomicBoolean(false)

@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {

if (hasActiveObservers()) {
Log.w(TAG, "多个观察者存在的时候,只会有一个被通知到数据更新")
}

super.observe(owner, Observer { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})

}

override fun setValue(value: T?) {
mPending.set(true)
super.setValue(value)
}

@MainThread
fun call() {
value = null
}

companion object {
private const val TAG = "SingleLiveData"
}
}
复制代码

将BaseViewModel中的MutableLiveData替换为SingleLiveData就可以了。


最后


至此,协程+Retrofit网络请求状态封装也就完成了,对于Error、Empty等view的切换以及点击重新请求等操作,这里就不一一展示了,可以移步到github里查看。最后我们来看一下请求效果。


demoo.gif



源码:组件化+Jetpack+kotlin+mvvm


收起阅读 »

LiveData 单元测试

文参考自 作者:HaroldGao链接:https://juejin.cn/post/6956588138487775240来源:掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

文参考自 Unit-testing LiveData and other common observability problems


参考 Google 代码官方测试代码 here



==单元测试时,LiveData.value 返回 null==


    @Test
@Throws(Exception::class)
fun testLiveDataFail() = runBlocking {
meditationDao.insert(MeditationTrip())
val trips = meditationDao.getAllTrips()
assertEquals(1, trips.value!!.size) // NullPointerException
}
复制代码

首先,Transformations#map 得到的 LiveData 必须有观察者,才会在原始 LiveData 更新时调用 map 函数更新值。理解起来也很合理,没有人观察的值没有必要被实时更新。实现原理是,Transformations#map 方法将 LiveData 转化为 MediatorLiveData,最终通过 LiveData#observeForever 向原始的 LiveData 添加一个 AlwaysActiveObserver,但是前提是这个 MediatorLiveData 必须要有 active 观察者(androidx.lifecycle.MediatorLiveData#addSource)。


Room 库中为 DAO 注解生成的实现类,返回的 LiveData 是 androidx.room.RoomTrackingLiveData 类型,类似地也只有在有 active Observer 的前提下,才会在数据库表更新时,执行查询语句,更新 value。因为没有观察者时,没必要更新。实现原理是在 RoomTrackingLiveData 第一次添加 Obeserver 时(OnActive),往 RoomDatabase 的 InvalidationTracker 中添加 WeakObserver,这样当数据库发生变化时,就会通知这些 Observer(androidx.room.InvalidationTracker#addWeakObserver)


以上问题的原因都是因为没有 active Observer,解决办法:


fun <T> LiveData<T>.getOrWaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(t: T) {
data = t
latch.countDown()
this@getOrWaitValue.removeObserver(this) // 添加了观察者
}
}
this.observeForever(observer)
afterObserve.invoke()

// wait for short time
if (!latch.await(time, timeUnit)) {
this.removeObserver(observer)
throw TimeoutException("LiveData value is never set!")
}

@Suppress("unchecked_cast")
return data as T
}
复制代码

单元测试时,又报错:


java.lang.IllegalStateException: Cannot invoke observeForever on a background thread
at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:487)
at androidx.lifecycle.LiveData.observeForever(LiveData.java:224)
复制代码

这是因为 LiveData 注册 Observer 时,要求必须是在主线程,通过 ArchTaskExecutor.getInstance().isMainThread() 来判断。


解决办法是为单元测试添加 InstantTaskExecutorRule:


    @Rule
@JvmField
val instantExecutorRule = InstantTaskExecutorRule()
复制代码

InstantTaskExecutorRule 作为 TestWatcher 的子类,会在单元测试开始前,替换 Archtechture Component 的后台执行器 ArchTaskExecutor,每个任务都是同步运行(runnable#run),isMainThread 返回 true。


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

CompletableFuture使用与解读

1 前言 jdk8后给出的类,android需要N版本之后才能使用;提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,也提供了转换和组合 CompletableFuture 的方法; 本文会从以下方面来介绍 ...
继续阅读 »

1 前言


jdk8后给出的类,android需要N版本之后才能使用;提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,也提供了转换和组合 CompletableFuture 的方法;


本文会从以下方面来介绍



  • 使用、方法意义以及总结归纳

  • 流程解读


2 使用


从类来看,其实现了CompletionStage接口以及Future接口;futrue的用法就不在这里说了,这里仅仅说明CompletionStage方法以及相关方法用法;


调用整个过程,我把它看成是个流,每次方法生成的CompletableFuture都是一个流节点,每个流有自己的完成结果,其后面的流依赖其完成后才可执行


2.1 流的产生



  • 静态方法


    val ff = CompletableFuture<Int>()
复制代码


  • 数据提供者Supplier


CompletableFuture.supplyAsync {
println("create thread ${Thread.currentThread().name}")
100
}
复制代码


  • 任务事件Runnable


 CompletableFuture.runAsync {
println("create: buy meat and vegetables")
}
复制代码


  • 组合并集任务


 CompletableFuture.allOf(CompletableFuture.runAsync{
println("create: wear shoes")
}, CompletableFuture.runAsync{
println("create: wear dress")
})
复制代码


  • 组合互斥任务


    CompletableFuture.anyOf(CompletableFuture.runAsync{
println("create: read a book")
}, CompletableFuture.runAsync{
println("create: write")
})
复制代码

2.2 流的处理


流的处理方法比较多了,有37个,写代码不方便;完成方法表格如下,表格备注表达了我对这些方法的抽象与理解,看了这个,有助于更好理解下面涉及的东西


CompletableFuture详细方法.png
太多了很难记,也不好理解,下面给出了简略精华版本方法表;通过这些方法,清除明了这个类可以做到什么样得组合变换


CompletableFuture简略方法.png


下面给出几个简单事例



  1. 无组合的变化、消费


CompletableFuture.supplyAsync {
println("create thread ${Thread.currentThread().name}")
100
}.thenApply {
println("map thread ${Thread.currentThread().name}")
it * 10
}.thenAccept {
println("consume $it")
}
复制代码


  1. 组合变化、消费


CompletableFuture.supplyAsync {
10
}.applyToEither(CompletableFuture.supplyAsync {
100
}, Function<Int, Int> {
it * 10 + 3
}).thenCombine(CompletableFuture.supplyAsync{
"Lily"
}, BiFunction<Int, String, Stu> { t, u -> Stu(u, t)}).thenAccept {
println("name ${it.name}, age ${it.age}")
}
复制代码


  1. 异常转换、多次消费


val ff = CompletableFuture<Int>()
ff.handle<Int>{
_, _ -> 10
}.whenComplete{
t, u -> println("first handler $t")
}.whenComplete { t, u -> println("second handler $t")}
ff.obtrudeValue(null)
复制代码

2.3 流结果设置


这里也通过表格方式,有下面几种方法


CompletableFuture结果设置.png


我们通过构造器生成时,需要自己设置值,如下


val ff = CompletableFuture<Int>()
ff.thenApply {
it / 2 + 4
}
ff.complete(16)
复制代码

设置值后,后面的流才会执行


3. 源码解读


CompletableFuture是流的一个节点,内部持有了完成状态以及依赖其的任务节点信息,其内部同样实现了完成态时依赖任务执行处理;


3.1 数据结构


这主要体现这两个成员变量上


    volatile Object result; 
volatile Completion stack;
复制代码


  • result:结果为null,表示未执行;执行结果为空,则设置为静态常量NIL,异常则设置为AltResult实例,正常完成,则表示实际的值; AltResult内容如下


   static final AltResult NIL = new AltResult(null);
static final class AltResult {
final Throwable ex;
AltResult(Throwable x) { this.ex = x; }
}
复制代码


  • stack:链表尾部指针,组成了后进先出的链表结构;是依赖当前完成状态需要执行的任务集合;内容如下,其实现ForkJoinTask,只是为了利用ForkJoinPoo线程池,其最大有点就是解决频繁的异步任务的,很配


    abstract static class Completion extends ForkJoinTask<Void>
implements Runnable, AsynchronousCompletionTask {
volatile Completion next;
abstract CompletableFuture<?> tryFire(int mode);

abstract boolean isLive();
public final void run() { tryFire(ASYNC); }
public final boolean exec() { tryFire(ASYNC); return false; }
public final Void getRawResult() { return null; }
public final void setRawResult(Void v) {}
}
复制代码

对于stack处理



  • postFire方法: 通知其依赖的节点,进行完成传播;由于没有使用锁,只使用了原子操作,这样可以防止,有些节点加入到依赖集合中,却不能得到执行

  • cleanStack方法:清除失活以及无效的节点

  • postComplete方法:执行stack集合中任务

  • casStack方法:改变队尾操作

  • tryPushStack方法:尝试加入队尾数据

  • pushStack:队尾加入数据


3.2 Completion以及子类


Completion类,抽象类,待执行的任务节点;其内部持有下个流以及流任务执行的逻辑;其继承关系类图如下:


Completion类图.jpg


内部变量


        CompletableFuture<V> dep; 
CompletableFuture<T> src;
CompletableFuture<U> snd
复制代码

dep代表当前操作新成的流节点,src、snd为其依赖的流节点;其中每个类,还有流任务执行的对象:Runable、Function、ConSumer、BiFunction、BiConsumer等


tryFire方法很重要,其持有的转换对象、消费对象代表了需要执行的操作;其实他们对应的tryFire方法内部实际操作,都在CompletableFuture内有对应方法


tryFire方法


很关键的方法,其持有的转换对象、消费对象代表了需要执行的操作;其情况与具体的模式有关,其情况如下



  • SYNC = 0, 同步状态;执行线程为当前方法调用线程或者上个流执行所在线程;同时其可能仅仅是为了启动线程池启动任务

  • ASYNC = 1,异步,表示需要在线程池内执行

  • NESTED = -1,传播模式,表示依赖的流节点已经处于完成状态,正在传递处理


claim方法


线程池任务提交,并且执行有且提交一次


3.3 中间流生成与执行原理


中间流处理,就是CompletionStage声明的方法;其系列处理方法,基本逻辑相同,也就是方法名称不同而已,而由于持有的任务不同而略有不同


3.3.1 thenRun系列


均是通过私有方法uniRunStage进行处理,进行添加时尝试处理的


  private CompletableFuture<Void> uniRunStage(Executor e, Runnable f) {
if (f == null) throw new NullPointerException();
CompletableFuture<Void> d = newIncompleteFuture();
if (e != null || !d.uniRun(this, f, null)) {
UniRun<T> c = new UniRun<T>(e, d, this, f);
push(c);
c.tryFire(SYNC);
}
return d;
}
复制代码

对于此方法有下面逻辑



  1. 同步执行,且uniRun执行成功,则返回生成流节点

  2. 否则,添加相应Completion子类到等待集合中,并再次尝试执行;和之前提到的postFire结合确保一定能够执行


   final boolean uniRun(CompletableFuture<?> a, Runnable f, UniRun<?> c) {
Object r; Throwable x;
if (a == null || (r = a.result) == null || f == null)
return false;
if (result == null) {
if (r instanceof AltResult && (x = ((AltResult)r).ex) != null)
completeThrowable(x, r);
else
try {
if (c != null && !c.claim())
return false;
f.run();
completeNull();
} catch (Throwable ex) {
completeThrowable(ex);
}
}
return true;
}
复制代码

方法的最后一个参数,当是触发线程池提交任务操作时,需要传入任务实例,否则传入空指;也就是传入空指,代表此方法中直接执行,这时,线程可能为生成流节点方法线程,也可能是上个流节点执行的线程,也可能是线程池创建的线程中(好像等于白说了);这个方法流程如下:




  1. 检验依赖节点执行状态,未完成则结束




  2. 执行异常结束,则设置异常状态,结束




  3. 正常执行结束时,尝试执行当前任务



    • 需要向线程池提交任务,则通过claim方法,进行处理,并返回;提交任务后会执行tryFire方法

    • 不需要向线程池提交任务,执行;若执行成功,有结果直接设置结果,无结果设置NIL值;若是发生已成设置异常




如果调用CompletionStage声明的方法未能立刻执行的,则需要通过依赖的流节点完成后通过postComplete方法进行分发;


    final void postComplete() {
CompletableFuture<?> f = this; Completion h;
while ((h = f.stack) != null ||
(f != this && (h = (f = this).stack) != null)) {
CompletableFuture<?> d; Completion t;
if (f.casStack(h, t = h.next)) {
if (t != null) {
if (f != this) {
pushStack(h);
continue;
}
h.next = null;
}
f = (d = h.tryFire(NESTED)) == null ? this : d;
}
}
}
复制代码

tryFire方法,返回空表示流节点任务没有完成,否则表示已完成,继续这个节点的分发;也就是分发时通过tryFire方法去执行依赖节点的任务


        final CompletableFuture<Void> tryFire(int mode) {
CompletableFuture<Void> d; CompletableFuture<T> a;
if ((d = dep) == null ||
!d.uniRun(a = src, fn, mode > 0 ? null : this))
return null;
dep = null; src = null; fn = null;
return d.postFire(a, mode);
}
复制代码

逻辑如下



  1. 当前任务执行的流节点为空、或者未执行,则返回null,也就是此节点未完成操作

  2. 已经执行成功,则把持有对象全部置空,以便gc;并通过postFire通知其依赖节点进行清理依赖节点集合或者继续传播触发


    final CompletableFuture<T> postFire(CompletableFuture<?> a, int mode) {
if (a != null && a.stack != null) {
if (mode < 0 || a.result == null)
a.cleanStack();
else
a.postComplete();
}
if (result != null && stack != null) {
if (mode < 0)
return this;
else
postComplete();
}
return null;
}
复制代码

主要逻辑




  1. 依赖流节点不为空,且依赖集合不为空



    • 传播模式或者其未完成执行,则进行节点清理

    • 否则,进行传播




  2. 当前流节点执行完毕,且依赖集合不为空



    • 正在处于传播模式,则返回当前对象,继续传播

    • 否则,进行传播处理




整个添加流节点以及执行流程,已经分析完了;那么这个相似处,根据这个例子再来具体的说下:


整个流程:Completion子类(UniRun)以及子类tryFire方法、CompletableFuture中辅助方法(uniRun)以及postFire、postComplete等分发方法


3.3.2 thenRun相似流程系列



  • thenApply系列方法:子类UniApply、辅助方法uniApply

  • thenAccept系列方法:子类UniAccept、辅助方法uniAccept

  • thenCombine系列方法:子类BiApply、辅助方法biApply

  • thenAcceptBoth系列方法:子类BiAccept、辅助方法biAccept

  • runAfterBoth系列方法:子类biRun、辅助方法BiRun

  • applyToEither系列方法:子类orApply、辅助方法OrApply

  • acceptEither系列方法:子类OrAccept、辅助方法orAccept

  • runAfterEither系列方法:子类OrRun、辅助方法orRun

  • handle系列方法:子类UniHandle、辅助方法uniHandle

  • whenComplete系列方法:子类UniWhenComplete、辅助方法uniWhenComplete

  • exceptionally方法:子类UniExceptionally、辅助方法uniExceptionally


uiWhenComplete、uniHandle和uniExceptionally,在异常处理中,因为需要处理异常,而在检测其依赖节点异常时,并不直接退出,而是继续处理


3.3.3 thenCompose系列


这个为何特殊呢,因为它相当于两个任务;



  1. 通过Function<? super T, ? extends CompletionStage>转换流为一个任务

  2. 转换的流执行又是一个任务,其又关联一个流


第一个子类是UniCompose,辅助方法是uniCompose,执行了转换流逻辑,并通过Relay实例把当前加入到转换流执行的依赖集合中;也就是说thenCompose系列方法产生的流,依赖于转换流操作以及转换的流完成


转换的流执行逻辑子类是UniRelay,辅助方法uniRelay


执行逻辑并没有区别;relay表示接力,也就是,其传递上个流节点结果即可


4 小结


CompletableFuture这个类,我觉得异步编程,还是需要一定的功底,它并没有把相应操作等封装的很到位,37个方法组合使用,可以达到不同的效果;


技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给与关注和点赞;如果文章存在错误,也请多多指教!


作者:众少成多积小致巨
链接:https://juejin.cn/post/6956585105875795998
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Swift的高级技巧 - 动态注入和更改代码

虽然Xcode为lldb命令提供了几个可视化抽象,例如通过单击代码行添加断点并通过单击播放按钮来运行,但lldb提供了一些Xcode UI中不存在的有用命令。这可以是从即时创建方法到甚至更改CPU的寄存器以强制应用程序上的特定流而无需重新编译它,并且了解它们可...
继续阅读 »

虽然Xcode为lldb命令提供了几个可视化抽象,例如通过单击代码行添加断点并通过单击播放按钮来运行,但lldb提供了一些Xcode UI中不存在的有用命令。这可以是从即时创建方法到甚至更改CPU的寄存器以强制应用程序上的特定流而无需重新编译它,并且了解它们可以极大地改善您的调试体验。

并非所有Swift都是在Xcode中开发的 - 像Swift编译器或Apple的SourceKit-LSP这样的东西通过其他方式更好地工作,这些方法通常最终会让你手动使用lldb 。如果没有Xcode来帮助您,其中一些技巧可能会阻止您再次编译应用程序以测试某些更改。

注入属性和方法

您可能已经知道po(“打印对象”的缩写) - 通常用于打印属性内容的友好命令:

func foo() {
var myProperty = 0
} // a breakpoint
po myProperty
0

然而,po比这更强大 - 尽管名称暗示它打印的东西,po是一个别名,更原始(或只是)命令的论证版本,使输出更加开放:expression --object-description -- expression e

e myProperty
(Int) $R4 = 0 // not very pretty!

因为它是别名,po所以可以做任何事情e。e用于评估表达式,表达式的范围可以从打印属性到更改其值,甚至可以定义新类。作为一个简单的用法,我们可以在代码中更改属性的值以强制新流而无需重新编译代码:

po myProperty
0
po myProperty = 1
po myProperty
1

除此之外,如果你po单独写,你将能够编写这样的多线表达式。我们可以使用它在我们的调试会话中创建全新的方法和类:

po
Enter expressions, then terminate with an empty line to evaluate:
1 class $BreakpointUtils {
2 static var $counter = 0
3 }
4 func $increaseCounter() {
5 $BreakpointUtils.$counter += 1
6 print("Times I've hit this breakpoint: \($BreakpointUtils.$counter)")
7 }
8

(这里使用美元符号表示这些属性和方法属于lldb,而不是实际代码。)

前面的例子允许我直接从lldb 调用,这将在我的“我无法处理这个bug”计数器上加1。$increaseCounter()

po $increaseCounter()
Times I've hit this breakpoint: 1
po $increaseCounter()
Times I've hit this breakpoint: 2

这样做的能力可以与lldb导入插件的能力相结合,这可以大大增强您的调试体验。一个很好的例子就是Chisel,这是一个由Facebook制作的工具,它包含许多lldb插件 - 就像border命令一样,它增加了一个明亮的边框,UIView这样你就可以在屏幕上快速定位它们,并且它们都通过巧妙的用法来实现。e/ po。

然后,您可以使用lldb的断点操作在命中断点时自动触发这些方法。结合po的属性更改功能,您可以创建特殊的断点,这些断点将改变您尝试执行的测试的应用流程。

通常,所有高级断点命令都非常痛苦地在lldb中手动编写(这就是为什么我会在本文中避免它们),但幸运的是,您可以轻松地在Xcode中设置断点操作:

v- 避免po动态

如果你已经使用po了一段时间,你可能在过去看到过这样一个神秘的错误信息:

error: Couldn't lookup symbols:
$myProperty #1 : Swift.Int in __lldb_expr_26.$__lldb_expr(Swift.UnsafeMutablePointer<Any>) -> ()

这是因为po通过编译来评估您的代码,不幸的是,即使您尝试访问的代码是正确的,仍然存在可能出错的情况。

如果你正在处理不需要评估的东西(比如静态属性而不是方法或闭包),你可以使用v命令(简称frame variable)作为打印的替代,po以便立即获取内容。宾语。

v myProperty
(Int) myProperty = 1

disassemble - 打破内存地址以更改其内容

注意:以下命令仅在极端情况下有用。你不会在这里学习一个新的Swift技巧,但你可能会学到一些有趣的软件工程!

我通过使用越狱的iPad来使用流行的应用程序进入逆向工程,当你这样做时,你没有选择重新编译代码 - 你需要动态地改变它。例如,如果我无法重新编译代码,isSubscribed即使我没有订阅,如何强制以下方法进入条件?

var isSubscribed = false

func run() {
if isSubscribed {
print("Subscribed!")
} else {
print("Not subscribed.")
}
}

我们可以通过使用应用程序的内存来解决 - 在任何堆栈框架内,您可以调用该disassemble命令来查看该堆栈的完整指令集:

myapp`run():
-> 0x100000d60 <+0>: push rbp
0x100000d61 <+1>: mov rbp, rsp
0x100000d64 <+4>: sub rsp, 0x70
0x100000d68 <+8>: lea rax, [rip + 0x319]
0x100000d6f <+15>: mov ecx, 0x20
...
0x100000d9c <+60>: test r8, 0x1
0x100000da0 <+64>: jne 0x100000da7
0x100000da2 <+66>: jmp 0x100000e3c
0x100000da7 <+71>: mov eax, 0x1
0x100000dac <+76>: mov edi, eax
...
0x100000ec7 <+359>: call 0x100000f36
0x100000ecc <+364>: add rsp, 0x70
0x100000ed0 <+368>: pop rbp
0x100000ed1 <+369>: ret

这里整洁的东西不是命令本身,而是你可以用这些信息做些什么。我们习惯在Xcode中设置断点到代码行和特定选择器,但在lldb的控制台中你也可以使用断点特定的内存地址。

我们需要知道一些汇编来解决这个问题:如果我的代码包含一个if,那么该代码的结果汇编肯定会有一个跳转指令。在这种情况下,跳转指令将跳转到存储器地址,如果寄存器(在前一条指令中设置)不等于零(那么,为真)。由于我没有订阅,肯定会为零,这将阻止该指令被触发。0x100000da0 <+64>: jne0x100000da7 0x100000da7 r8 0x100000d9c <+60>: test r8, 0x1 r8

要看到这种情况发生并修复它,让我们首先断点并将应用程序放在jne指令处:

b 0x100000da0
continue
//Breakpoint hits the specific memory address

如果我disassemble再次运行,小箭头将显示我们在正确的内存地址处开始操作。

-> 0x100000da0 <+64>:  jne    0x100000da7

有两种方法可以解决这个问题:

方法1:更改CPU寄存器的内容

该register read和register write命令由LLDB提供,让您检查和修改的CPU寄存器的内容,并解决这个问题的第一种方式是简单地改变的内容r8。

通过定位jne指令,register read将返回以下内容:

General Purpose Registers:
rax = 0x000000010295ddb0
rbx = 0x0000000000000000
rcx = 0x00007ffeefbff508
rdx = 0x0000000000000000
rdi = 0x00007ffeefbff508
rsi = 0x0000000010000000
rbp = 0x00007ffeefbff520
rsp = 0x00007ffeefbff4b0
r8 = 0x0000000000000000General Purpose Registers:

因为r8为零,jne指令不会触发,从而使代码输出"Not subscribed."。但是,这是一个简单的修复 - 我们可以r8通过运行register write和恢复应用程序设置为不为零的东西:

register write r8 0x1
continue
"Subscribed!"

在日常的iOS开发中,register write可以用来替换代码中的整个对象。如果某个方法要返回你不想要的东西,你可以在lldb中创建一个新对象,获取其内存地址e并将其注入所需的寄存器。

方法2:更改指令本身

解决这个问题的第二种也可能是最疯狂的方法是实时重写应用程序本身。

就像寄存器一样,lldb提供memory read并memory write允许您更改应用程序使用的任何内存地址的内容。这可以用作动态更改属性内容的替代方法,但在这种情况下,我们可以使用它来更改指令本身。

这里可以做两件事:如果我们想要反转if指令的逻辑,我们可以改为(所以它检查一个条件),或者(跳空不是)to (跳空,或)。我发现后者更容易,所以这就是我要遵循的。如果我们阅读该指令的内容,我们会看到如下内容:test r8, 0x1 test r8, 0x0 false jne 0x100000da7 je 0x100000da7 if!condition

memory read 0x100000da0
0x100000da0: 75 05 e9 95 00 00 00 b8 01 00 00 00 89 c7 e8 71

这看起来很疯狂,但我们不需要了解所有这些 - 我们只需要知道指令的OPCODE对应于开头的两位(75)。按照这个图表,我们可以看到OPCODE for je是74,所以如果我们想要jne成为je,我们需要将前两位与74交换。

为此,我们可以使用memory write与该地址完全相同的内容,但前两位更改为74。

memory write 0x100000da0 74 05 e9 95 00 00 00 b8 01 00 00 00 89 c7 e8 71
dis
0x100000da0 <+64>:  je     0x100000da7

现在,运行应用程序将导致"Subscribed!"打印。

结论

虽然拆解和写入内存对于日常开发来说可能过于极端,但您可以使用一些更高级的lldb技巧来提高工作效率。更改属性,定义辅助方法并将它们与断点操作混合将允许您更快地导航和测试代码,而无需重新编译它。

转自:https://www.jianshu.com/p/281a2f61937e

收起阅读 »

iOS KVO 与 readonly的讨论 (数组array & setter)

在开发过程中,可能会有这样的需求:当数据源变动的时候及时刷新显示的列表。期望是去监听数据源数组的count,当count有变动就刷新UI,可是实际操作中却发现了不少的问题。例如:self.propertyArray = [NSMutableArray arra...
继续阅读 »

在开发过程中,可能会有这样的需求:当数据源变动的时候及时刷新显示的列表。
期望是去监听数据源数组的count,当count有变动就刷新UI,可是实际操作中却发现了不少的问题。
例如:

self.propertyArray = [NSMutableArray array];
[self.propertyArray addObserver:self forKeyPath:@"count" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

直接就报错了,信息如下:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<__NSArrayM 0x6000033db450> addObserver:forKeyPath:options:context:] is not supported. Key path: count'

字面意思是,不支持对数组count的监听。
回到问题的本质。
我们知道KVO是在属性的setter方法上做文章,进入到数组的类中看一下,发现count属性是readonly

@interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>

@property (readonly) NSUInteger count;

readonly不会自动生成setter方法,但是可以手动添加setter方法。
我们来验证一下 例如:
创建一个people类 添加一个属性 readonly count

@interface People : NSObject

@property (nonatomic, readonly) NSInteger count;

@end

@implementation People

@end

我们来试一下 监听它的count属性会怎样

People *peo = [People new];
[peo addObserver:self forKeyPath:@"count" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

我们发现 并没有报错。
我们来看一下People的方法列表

const char *className = "People";
Class NSKVONotifying_People = objc_getClass(className);
unsigned int methodCount =0;
Method* methodList = class_copyMethodList(NSKVONotifying_People,&methodCount);
NSMutableArray *methodsArray = [NSMutableArray arrayWithCapacity:methodCount];

for(int i=0;i<methodCount;i++)
{
Method temp = methodList[i];
const char* name_s =sel_getName(method_getName(temp));
[methodsArray addObject:[NSString stringWithUTF8String:name_s]];
}
NSLog(@"%@",methodsArray);

通过打印我们可以看到 People中只有两个方法

(
dealloc,
count,
)

我们知道,KVO监听某个对象时,会动态生成名字叫做NSKVONotifying_XX的类,并且重写监听对象的setter方法。下面 我们来看下NSKVONotifying_People的方法列表:

const char *className = "NSKVONotifying_People";
Class NSKVONotifying_People = objc_getClass(className);
unsigned int methodCount =0;
Method* methodList = class_copyMethodList(NSKVONotifying_People,&methodCount);
NSMutableArray *methodsArray = [NSMutableArray arrayWithCapacity:methodCount];

for(int i=0;i<methodCount;i++)
{
Method temp = methodList[i];
const char* name_s =sel_getName(method_getName(temp));
[methodsArray addObject:[NSString stringWithUTF8String:name_s]];
}
NSLog(@"%@",methodsArray);

打印结果如下:

(
class,
dealloc,
"_isKVOA"
)

可以看到,里面没有count的getter方法,多了个class和isKVO, 当然也没有我们需要的setter方法。但是这样并不会导致crash。

下面我们再试一下,手动在People的.m中 添加上setter方法会怎么样:

@implementation People

- (void)setCount:(NSInteger)count{
_count = count;
}

@end

再次查看People和NSKVONotifying_People的方法列表,会发现多了一个count的setter方法。(如下所示)

(
dealloc,
count,
"setCount:"
)
(
"setCount:",
class,
dealloc,
"_isKVOA"
)

这样我们就可以得出一个结论:

KVO动态生成的类,重写setter方法的前提是:原来的类中,要有对应的setter方法。即便是readonly修饰,只要.m中有对应属性的setter方法,都是可以的。

OK 说了这么多,好像还是没有解决我们的问题。 为什么监听数组count就抛异常了呢? 带着这个问题 继续往下走。
通过点击array的监听方法 进入到ArrayObserving类中,我们发现,系统给出了注释:NSArrays are not observable, so these methods raise exceptions when invoked on NSArrays.


系统也不期望我们去监听数组的属性。is not supported. Key path: count' 应该就是系统在实现监听方法时,抛出的异常。

最后,我从网上找到了另一个方法

[self mutableArrayValueForKey:@"propertyArray"]

我们可以转换一个思路,不再监听count,选择监听数组本身,当数组变动时刷新页面。

[self addObserver:self forKeyPath:@"propertyArray" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

[[self mutableArrayValueForKey:@"propertyArray"] addObject:@"a"];

这个方法的具体实现,没有看到源码,但是看到有人说,这个方法会生成一个可变数组,添加完元素后,会将这个生成的数组赋值给叫做Key的数组。我试了一下,确实是有效果的,这里就不做考究了。

转自:https://www.jianshu.com/p/688c2512be01

收起阅读 »

【含视频、课件下载】一天开发一款灵魂社交APP

视频回放: 课件下载:社交应用开发分享.pptx零开发基础、源码共享 内容介绍:从互联网诞生之日起,社交需求就一直作为一种刚需存在,在人际过载与信息过载时代,微信已经不再能承载我们最简单、纯粹、美好的社交需求,在社交疲态和用户迁移的产品契机下,陌生人...
继续阅读 »


视频回放:


课件下载:

社交应用开发分享.pptx

零开发基础、源码共享

 

内容介绍:

从互联网诞生之日起,社交需求就一直作为一种刚需存在,在人际过载与信息过载时代,微信已经不再能承载我们最简单、纯粹、美好的社交需求,在社交疲态和用户迁移的产品契机下,陌生人社交领域逐渐孕育出“陌陌、探探、SOUL”等社交APP新贵。随着5G时代的到来,一波音视频社交领域的创业窗口期又重新打开。

本次课程,环信生态开发者“穿裤衩闯天下”将给我们带来一款基于环信即时通讯云(环信音视频云)开发的免费开源灵魂社交APP,分享其开发过程和项目源码,助力程序员高效开发,快速集成。

 

直播大纲:

1)项目介绍

国内首个程序猿非严肃婚恋交友应用——猿匹配

(2)开发环境

在最新的Android开发环境下开发,使用Java8的一些新特性,比如Lambda表达式等

· Mac OS 10.14.4

· Android Studio 3.3.2

(3)功能介绍

· IM功能

会话与消息功能,包括图片、文本、表情等消息,还包括语音实时通话与视频实时通话功能的开发等

· APP功能

包括聊天、设置、社区等板块开发

· 发布功能

含多渠道打包、签名配置、开发与线上环境配置、敏感信息保护等

(4)配置运行


提供一些地址:

自定义工具库:https://github.com/lzan13/VMLibrary

 

收起阅读 »

常见的8个前端防御性编程方案

关于前端防御性编程我们大多数情况可能遇到过,后端的由于同时请求人数过多,或者数据量过大,又或者是因为异常导致服务异常,接口请求失败,然后前端出现白屏或者报错还有一种情况,是前端自身写的代码存在一些缺陷,整个系统不够健壮,从而会出现白屏,或者业务系统异常,用户误...
继续阅读 »

关于前端防御性编程

  • 我们大多数情况可能遇到过,后端的由于同时请求人数过多,或者数据量过大,又或者是因为异常导致服务异常,接口请求失败,然后前端出现白屏或者报错
  • 还有一种情况,是前端自身写的代码存在一些缺陷,整个系统不够健壮,从而会出现白屏,或者业务系统异常,用户误操作等
  • 那么,就出现了前端防御性编程

常见的问题和防范

1.最常见的问题:
uncaught TypeError: Cannot read property 'c' of undefined

出现这个问题最根本原因是:

当我们初始化一个对象obj为{}时候,obj.a这个时候是undefined.我们打印obj.a可以得到undefined,但是我们打印obj.a.c的时候,就会出现上面的错误。js对象中的未初始化属性值是undefined,从undefined读取属性就会导致这个错误(同理,null也一样)

如何避免?

js和ts目前都出现了一个可选链概念,例如:

const obj = {};
console.log(obj?.b?.c?.d)
上面的代码并不会报错,原因是?.遇到是空值的时候便会返回undefined.
2.前端接口层面的错误机制捕获

前端的接口调用,一般都比较频繁,我们这时候可以考虑使用单例模式,将所有的axios请求都用一个函数封装一层。统一可以在这个函数中catch捕获接口调用时候的未知错误,伪代码如下:

function ajax(url,data,method='get'){
const promise = axios[method](url,data)
return promise.then(res=>{
}).catch(error){
//统一处理错误
}
}

那么只要发生接口调用的未知错误都会在这里被处理了

3.错误边界(Error Boundaries,前端出现未知错误时,展示预先设定的UI界面)

以React为例

部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。

错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

使用示例:

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}

componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}

render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}
注意
  • 错误边界无法捕获以下场景中产生的错误:

    • 事件处理(了解更多)
    • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
    • 服务端渲染
    • 它自身抛出来的错误(并非它的子组件)
4.前端复杂异步场景导致的错误
  • 这个问题可能远不止这么简单,但是大道至简,遵循单向数据流的方式去改变数据,例如:

  • //test.js
    export const obj = {
    a:1,
    b:2
    }

    //使用obj
    import {obj} from './test.js';
    obj.a=3;

    当你频繁使用这个obj对象时,你无法根据代码去知道它的改变顺序(即在某个时刻它的值是什么),而且这里面可能存在不少异步的代码,当我们换一种方式,就能知道它的改变顺序了,也更方便我们debug

    例如://test.js

    export const obj = {
    a:1,
    b:2
    }
    export function setObj (key,value) {
    console.log(key,value)
    obj[key] = value
    }
    这样,我们就做到了
    5.前端专注“前端”
    • 对于一些敏感数据,例如登录态,鉴权相关的。前端应该是尽量做无感知的转发、携带(这样也不会出现安全问题)
    6.页面做到可降级
    • 对于一些刚上新的业务,或者有存在风险的业务模块,或者会调取不受信任的接口,例如第三方的接口,这个时候就要做一层降级处理,例如接口调用失败后,剔除对应模块的展示,让用户无感知的使用
    7.巧用loading和disabled
    • 用户操作后,要及时loading和disabled确保不让用户进行重复,防止业务侧出现bug

    8.慎用innerHTML

    • 容易出现安全漏洞,例如接口返回了一段JavaScript脚本,那么就会立即执行。此时脚本如果是恶意的,那么就会出现不可预知的后果,特别是电商行业,尤其要注意


收起阅读 »

嗨,你真的懂this吗?

this关键字是JavaScript中最复杂的机制之一,是一个特别的关键字,被自动定义在所有函数的作用域中,但是相信很多JsvaScript开发者并不是非常清楚它究竟指向的是什么。听说你很懂this,是真的吗?请先回答第一个问题:如何准确判断this指向的是什...
继续阅读 »

this关键字是JavaScript中最复杂的机制之一,是一个特别的关键字,被自动定义在所有函数的作用域中,但是相信很多JsvaScript开发者并不是非常清楚它究竟指向的是什么。听说你很懂this,是真的吗?

请先回答第一个问题:如何准确判断this指向的是什么?【面试的高频问题】


【图片来源于网络,侵删】

再看一道题,控制台打印出来的值是什么?【浏览器运行环境】

var number = 5;
var obj = {
number: 3,
fn1: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
})()
}
var fn1 = obj.fn1;
fn1.call(null);
obj.fn1();
console.log(window.number);

如果你思考出来的结果,与在浏览中执行结果相同,并且每一步的依据都非常清楚,那么,你可以选择继续往下阅读,或者关闭本网页,愉快得去玩耍。如果你有一部分是靠蒙的,或者对自己的答案并不那么确定,那么请继续往下阅读。

毕竟花一两个小时的时间,把this彻底搞明白,是一件很值得事情,不是吗?

本文将细致得讲解this的绑定规则,并在最后剖析前文两道题。

为什么要学习this?

首先,我们为什么要学习this?

  1. this使用频率很高,如果我们不懂this,那么在看别人的代码或者是源码的时候,就会很吃力。
  2. 工作中,滥用this,却没明白this指向的是什么,而导致出现问题,但是自己却不知道哪里出问题了。【在公司,我至少帮10个以上的开发人员处理过这个问题】
  3. 合理的使用this,可以让我们写出简洁且复用性高的代码。
  4. 面试的高频问题,回答不好,抱歉,出门右拐,不送。

不管出于什么目的,我们都需要把this这个知识点整的明明白白的。

OK,Let's go!

this是什么?

言归正传,this是什么?首先记住this不是指向自身!this 就是一个指针,指向调用函数的对象。这句话我们都知道,但是很多时候,我们未必能够准确判断出this究竟指向的是什么?这就好像我们听过很多道理 却依然过不好这一生。今天咱们不探讨如何过好一生的问题,但是呢,希望阅读完下面的内容之后,你能够一眼就看出this指向的是什么。

为了能够一眼看出this指向的是什么,我们首先需要知道this的绑定规则有哪些?

  1. 默认绑定
  2. 隐式绑定
  3. 硬绑定
  4. new绑定

上面的名词,你也许听过,也许没听过,但是今天之后,请牢牢记住。我们将依次来进行解析。

默认绑定

默认绑定,在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。

function sayHi(){
console.log('Hello,', this.name);
}
var name = 'YvetteLau';
sayHi();

在调用Hi()时,应用了默认绑定,this指向全局对象(非严格模式下),严格模式下,this指向undefined,undefined上没有this对象,会抛出错误。

上面的代码,如果在浏览器环境中运行,那么结果就是 Hello,YvetteLau

但是如果在node环境中运行,结果就是 Hello,undefined.这是因为node中name并不是挂在全局对象上的。

本文中,如不特殊说明,默认为浏览器环境执行结果。

隐式绑定

函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的形式为 XXX.fun().我们来看一段代码:

function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
person.sayHi();

打印的结果是 Hello,YvetteLau.

sayHi函数声明在外部,严格来说并不属于person,但是在调用sayHi时,调用位置会使用person的上下文来引用函数,隐式绑定会把函数调用中的this(即此例sayHi函数中的this)绑定到这个上下文对象(即此例中的person)

需要注意的是:对象属性链中只有最后一层会影响到调用位置。

function sayHi(){
console.log('Hello,', this.name);
}
var person2 = {
name: 'Christina',
sayHi: sayHi
}
var person1 = {
name: 'YvetteLau',
friend: person2
}
person1.friend.sayHi();

结果是:Hello, Christina.

因为只有最后一层会确定this指向的是什么,不管有多少层,在判断this的时候,我们只关注最后一层,即此处的friend。

隐式绑定有一个大陷阱,绑定很容易丢失(或者说容易给我们造成误导,我们以为this指向的是什么,但是实际上并非如此).

function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
var Hi = person.sayHi;
Hi();

结果是: Hello,Wiliam.

这是为什么呢,Hi直接指向了sayHi的引用,在调用的时候,跟person没有半毛钱的关系,针对此类问题,我建议大家只需牢牢继续这个格式:XXX.fn();fn()前如果什么都没有,那么肯定不是隐式绑定,但是也不一定就是默认绑定,这里有点小疑问,我们后来会说到。

除了上面这种丢失之外,隐式绑定的丢失是发生在回调函数中(事件回调也是其中一种),我们来看下面一个例子:

function sayHi(){
console.log('Hello,', this.name);
}
var person1 = {
name: 'YvetteLau',
sayHi: function(){
setTimeout(function(){
console.log('Hello,',this.name);
})
}
}
var person2 = {
name: 'Christina',
sayHi: sayHi
}
var name='Wiliam';
person1.sayHi();
setTimeout(person2.sayHi,100);
setTimeout(function(){
person2.sayHi();
},200);

结果为:

Hello, Wiliam
Hello, Wiliam
Hello, Christina
  • 第一条输出很容易理解,setTimeout的回调函数中,this使用的是默认绑定,非严格模式下,执行的是全局对象
  • 第二条输出是不是有点迷惑了?说好XXX.fun()的时候,fun中的this指向的是XXX呢,为什么这次却不是这样了!Why?

    其实这里我们可以这样理解: setTimeout(fn,delay){ fn(); },相当于是将person2.sayHi赋值给了一个变量,最后执行了变量,这个时候,sayHi中的this显然和person2就没有关系了。

  • 第三条虽然也是在setTimeout的回调中,但是我们可以看出,这是执行的是person2.sayHi()使用的是隐式绑定,因此这是this指向的是person2,跟当前的作用域没有任何关系。

读到这里,也许你已经有点疲倦了,但是答应我,别放弃,好吗?再坚持一下,就可以掌握这个知识点了。


显式绑定

显式绑定比较好理解,就是通过call,apply,bind的方式,显式的指定this所指向的对象。(注意:《你不知道的Javascript》中将bind单独作为了硬绑定讲解了)

call,apply和bind的第一个参数,就是对应函数的this所指向的对象。call和apply的作用一样,只是传参方式不同。call和apply都会执行对应的函数,而bind方法不会。

function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
var Hi = person.sayHi;
Hi.call(person); //Hi.apply(person)

输出的结果为: Hello, YvetteLau. 因为使用硬绑定明确将this绑定在了person上。

那么,使用了硬绑定,是不是意味着不会出现隐式绑定所遇到的绑定丢失呢?显然不是这样的,不信,继续往下看。

function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
fn();
}
Hi.call(person, person.sayHi);

输出的结果是 Hello, Wiliam. 原因很简单,Hi.call(person, person.sayHi)的确是将this绑定到Hi中的this了。但是在执行fn的时候,相当于直接调用了sayHi方法(记住: person.sayHi已经被赋值给fn了,隐式绑定也丢了),没有指定this的值,对应的是默认绑定。

现在,我们希望绑定不会丢失,要怎么做?很简单,调用fn的时候,也给它硬绑定。

function sayHi(){
console.log('Hello,', this.name);
}
var person = {
name: 'YvetteLau',
sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
fn.call(this);
}
Hi.call(person, person.sayHi);

此时,输出的结果为: Hello, YvetteLau,因为person被绑定到Hi函数中的this上,fn又将这个对象绑定给了sayHi的函数。这时,sayHi中的this指向的就是person对象。

至此,革命已经快胜利了,我们来看最后一种绑定规则: new 绑定。

new 绑定

javaScript和C++不一样,并没有类,在javaScript中,构造函数只是使用new操作符时被调用的函数,这些函数和普通的函数并没有什么不同,它不属于某个类,也不可能实例化出一个类。任何一个函数都可以使用new来调用,因此其实并不存在构造函数,而只有对于函数的“构造调用”。

使用new来调用函数,会自动执行下面的操作:
  1. 创建一个新对象
  2. 将构造函数的作用域赋值给新对象,即this指向这个新对象
  3. 执行构造函数中的代码
  4. 返回新对象

因此,我们使用new来调用函数的时候,就会新对象绑定到这个函数的this上。

function sayHi(name){
this.name = name;

}
var Hi = new sayHi('Yevtte');
console.log('Hello,', Hi.name);

输出结果为 Hello, Yevtte, 原因是因为在var Hi = new sayHi('Yevtte');这一步,会将sayHi中的this绑定到Hi对象上。

绑定优先级

我们知道了this有四种绑定规则,但是如果同时应用了多种规则,怎么办?

显然,我们需要了解哪一种绑定方式的优先级更高,这四种绑定的优先级为:

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

这个规则时如何得到的,大家如果有兴趣,可以自己写个demo去测试,或者记住上面的结论即可。

绑定例外

凡事都有例外,this的规则也是这样。

如果我们将null或者是undefined作为this的绑定对象传入call、apply或者是bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。

var foo = {
name: 'Selina'
}
var name = 'Chirs';
function bar() {
console.log(this.name);
}
bar.call(null); //Chirs

输出的结果是 Chirs,因为这时实际应用的是默认绑定规则。

箭头函数

箭头函数是ES6中新增的,它和普通函数有一些区别,箭头函数没有自己的this,它的this继承于外层代码库中的this。箭头函数在使用时,需要注意以下几点:

(1)函数体内的this对象,继承的是外层代码块的this。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

(5)箭头函数没有自己的this,所以不能用call()、apply()、bind()这些方法去改变this的指向.

OK,我们来看看箭头函数的this是什么?

var obj = {
hi: function(){
console.log(this);
return ()=>{
console.log(this);
}
},
sayHi: function(){
return function() {
console.log(this);
return ()=>{
console.log(this);
}
}
},
say: ()=>{
console.log(this);
}
}
let hi = obj.hi(); //输出obj对象
hi(); //输出obj对象
let sayHi = obj.sayHi();
let fun1 = sayHi(); //输出window
fun1(); //输出window
obj.say(); //输出window

那么这是为什么呢?如果大家说箭头函数中的this是定义时所在的对象,这样的结果显示不是大家预期的,按照这个定义,say中的this应该是obj才对。

我们来分析一下上面的执行结果:

  1. obj.hi(); 对应了this的隐式绑定规则,this绑定在obj上,所以输出obj,很好理解。
  2. hi(); 这一步执行的就是箭头函数,箭头函数继承上一个代码库的this,刚刚我们得出上一层的this是obj,显然这里的this就是obj.
  3. 执行sayHi();这一步也很好理解,我们前面说过这种隐式绑定丢失的情况,这个时候this执行的是默认绑定,this指向的是全局对象window.
  4. fun1(); 这一步执行的是箭头函数,如果按照之前的理解,this指向的是箭头函数定义时所在的对象,那么这儿显然是说不通。OK,按照箭头函数的this是继承于外层代码库的this就很好理解了。外层代码库我们刚刚分析了,this指向的是window,因此这儿的输出结果是window.
  5. obj.say(); 执行的是箭头函数,当前的代码块obj中是不存在this的,只能往上找,就找到了全局的this,指向的是window.

你说箭头函数的this是静态的?

依旧是前面的代码。我们来看看箭头函数中的this真的是静态的吗?

我要说:非也

var obj = {
hi: function(){
console.log(this);
return ()=>{
console.log(this);
}
},
sayHi: function(){
return function() {
console.log(this);
return ()=>{
console.log(this);
}
}
},
say: ()=>{
console.log(this);
}
}
let sayHi = obj.sayHi();
let fun1 = sayHi(); //输出window
fun1(); //输出window

let fun2 = sayHi.bind(obj)();//输出obj
fun2(); //输出obj

可以看出,fun1和fun2对应的是同样的箭头函数,但是this的输出结果是不一样的。

所以,请大家牢牢记住一点: 箭头函数没有自己的this,箭头函数中的this继承于外层代码库中的this.

总结

关于this的规则,至此,就告一段落了,但是想要一眼就能看出this所绑定的对象,还需要不断的训练。

我们来回顾一下,最初的问题。

1. 如何准确判断this指向的是什么?

  1. 函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的对象。
  2. 函数是否通过call,apply调用,或者使用了bind(即硬绑定),如果是,那么this绑定的就是指定的对象。
  3. 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this绑定的是那个上下文对象。一般是obj.foo()
  4. 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到undefined,否则绑定到全局对象。
  5. 如果把Null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
  6. 如果是箭头函数,箭头函数的this继承的是外层代码块的this。

2. 执行过程解析

var number = 5;
var obj = {
number: 3,
fn: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
})()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);

我们来分析一下,这段代码的执行过程。

1.在定义obj的时候,fn对应的闭包就执行了,返回其中的函数,执行闭包中代码时,显然应用不了new绑定(没有出现new 关键字),硬绑定也没有(没有出现call,apply,bind关键字),隐式绑定有没有?很显然没有,如果没有XX.fn(),那么可以肯定没有应用隐式绑定,所以这里应用的就是默认绑定了,非严格模式下this绑定到了window上(浏览器执行环境)。【这里很容易被迷惑的就是以为this指向的是obj,一定要注意,除非是箭头函数,否则this跟词法作用域是两回事,一定要牢记在心】

window.number * = 2; //window.number的值是10(var number定义的全局变量是挂在window上的)

number = number * 2; //number的值是NaN;注意我们这边定义了一个number,但是没有赋值,number的值是undefined;Number(undefined)->NaN

number = 3; //number的值为3

2.myFun.call(null);我们前面说了,call的第一个参数传null,调用的是默认绑定;

fn: function(){
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}

执行时:

var num = this.number; //num=10; 此时this指向的是window
this.number * = 2; //window.number = 20
console.log(num); //输出结果为10
number *= 3; //number=9; 这个number对应的闭包中的number;闭包中的number的是3
console.log(number); //输出的结果是9

3.obj.fn();应用了隐式绑定,fn中的this对应的是obj.

var num = this.number;//num = 3;此时this指向的是obj
this.number *= 2; //obj.number = 6;
console.log(num); //输出结果为3;
number *= 3; //number=27;这个number对应的闭包中的number;闭包中的number的此时是9
console.log(number);//输出的结果是27

4.最后一步console.log(window.number);输出的结果是20

因此组中结果为:

10
9
3
27
20

严格模式下结果,大家可以根据今天所学,自己分析,巩固一下知识点。

最后,恭喜坚持读完的小伙伴们,你们成功get到了this这个知识点,但是想要完全掌握,还是要多回顾和练习。如果你有不错的this练习题,欢迎在评论区留言哦,大家一起进步!


原文:https://segmentfault.com/a/1190000018630013

收起阅读 »

前端基础-你真的懂函数吗?

前言众所周知,在前端开发领域中,函数是一等公民,由此可见函数的重要性,本文旨在介绍函数中的一些特性与方法,对函数有更好的认知正文1.箭头函数ECMAScript 6 新增了使用胖箭头(=>)语法定义函数表达式的能力。很大程度上,箭头函数实例化的函数对象与...
继续阅读 »

前言

众所周知,在前端开发领域中,函数是一等公民,由此可见函数的重要性,本文旨在介绍函数中的一些特性与方法,对函数有更好的认知

正文

1.箭头函数

ECMAScript 6 新增了使用胖箭头(=>)语法定义函数表达式的能力。很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方,都可以使用箭头函数:

let arrowSum = (a, b) => { 
return a + b;
};
let functionExpressionSum = function(a, b) {
return a + b;
};
console.log(arrowSum(5, 8)); // 13
console.log(functionExpressionSum(5, 8)); // 13

使用箭头函数须知:

  • 箭头函数的函数体如果不用大括号括起来会隐式返回这行代码的值
  • 箭头函数不能使用 argumentssuper 和new.target,也不能用作构造函数
  • 箭头函数没有 prototype 属性

2.函数声明与函数表达式

JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。

// 没问题 
console.log(sum(10, 10));
function sum(num1, num2) {
return num1 + num2;
}

以上代码可以正常运行,因为函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升(function declaration hoisting)。在执行代码时,JavaScript 引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。因此即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部。如果把前面代码中的函数声明改为等价的函数表达式,那么执行的时候就会出错:

// 会出错
console.log(sum(10, 10));
let sum = function(num1, num2) {
return num1 + num2;
};

上述代码的报错有一些同学可能认为是let导致的暂时性死区。其实原因并不出在这里,这是因为这个函数定义包含在一个变量初始化语句中,而不是函数声明中。这意味着代码如果没有执行到let的那一行,那么执行上下文中就没有函数的定义。大家可以自己尝试一下,就算是用var来定义,也是一样会出错。

3.函数内部

在 ECMAScript 5 中,函数内部存在两个特殊的对象:arguments 和 this。ECMAScript 6 又新增了 new.target 属性。

arguments

它是一个类数组对象,包含调用函数时传入的所有参数。这个对象只有以 function 关键字定义函数(相对于使用箭头语法创建函数)时才会有。但 arguments 对象其实还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针。

function factorial(num) { 
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}

// 上述代码可以运用arguments来进行解耦
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}

这个重写之后的 factorial()函数已经用 arguments.callee 代替了之前硬编码的 factorial。这意味着无论函数叫什么名称,都可以引用正确的函数。

arguments.callee 的解耦示例
let trueFactorial = factorial; 
factorial = function() {
return 0;
};
console.log(trueFactorial(5)); // 120
console.log(factorial(5)); // 0

这里 factorial 函数在赋值给trueFactorial后被重写了 那么我们如果在递归中不使用arguments.callee 那么显然trueFactorial(5)的运行结果也是0,但是我们解耦之后,新的变量还是可以正常的进行

this

函数内部另一个特殊的对象是 this,它在标准函数和箭头函数中有不同的行为。

在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在网页的全局上下文中调用函数时,this 指向 windows)。

在箭头函数中,this引用的是定义箭头函数的上下文。

caller

这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为 null。

function outer() { 
inner();
}
function inner() {
console.log(inner.caller);
}
outer();

以上代码会显示 outer()函数的源代码。这是因为 ourter()调用了 inner(),inner.caller指向 outer()。如果要降低耦合度,则可以通过 arguments.callee.caller 来引用同样的值:

function outer() { 
inner();
}
function inner() {
console.log(arguments.callee.caller);
}
outer();

new.target

ECMAScript 中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。ECMAScript 6 新增了检测函数是否使用 new 关键字调用的 new.target 属性。如果函数是正常调用的,则 new.target 的值是 undefined;如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数。

function King() { 
if (!new.target) {
throw 'King must be instantiated using "new"'
}
console.log('King instantiated using "new"');
}
new King(); // King instantiated using "new"
King(); // Error: King must be instantiated using "new"

这里可以做一些延申,还有没有其他办法来判断函数是否通过new来调用的呢?

可以使用 instanceof 来判断。我们知道在new的时候发生了哪些操作?用如下代码表示:

var p = new Foo()
// 实际上执行的是
// 伪代码
var o = new Object(); // 或 var o = {}
o.__proto__ = Foo.prototype
Foo.call(o)
return o

上述伪代码在MDN是这么说的:

  1. 一个继承自 Foo.prototype 的新对象被创建。
  2. 使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。
  3. 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)

new 的操作说完了 现在我们看一下 instanceof,MDN上是这么说的:instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

也就是说,A的N个__proto__ 全等于 B.prototype,那么A instanceof B返回true,现在知识点已经介绍完毕,可以开始上代码了

  function Person() {
if (this instanceof Person) {
console.log("通过new 创建");
return this;
} else {
console.log("函数调用");
}
}
const p = new Person(); // 通过new创建
Person(); // 函数调用

解析:我们知道new构造函数的this指向实例,那么上述代码不难得出以下结论this.__proto__ === Person.prototype。所以这样就可以判断函数是通过new还是函数调用

这里我们其实还可以将 this instanceof Person 改写为 this instanceof arguments.callee

4.闭包

终于说到了闭包,闭包这玩意真的是面试必问,所以掌握还是很有必要的

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

function foo() {
var a = 20;
var b = 30;

function bar() {
return a + b;
}
return bar;
}

上述代码中,由于foo函数内部的bar函数使用了foo函数内部的变量,并且bar函数return把变量return了出去,这样闭包就产生了,这使得我们可以在外部拿到这些变量。

const b = foo();
b() // 50

foo函数在调用的时候创建了一个执行上下文,可以在此上下文中使用a,b变量,理论上说,在foo调用结束,函数内部的变量会v8引擎的垃圾回收机制通过特定的标记回收。但是在这里,由于闭包的产生,a,b变量并不会被回收,这就导致我们在全局上下文(或其他执行上下文)中可以访问到函数内部的变量。

我之前看到了一个说法:

无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包,闭包包含在函数创建时作用域中的所有变量,类似于背包,函数定义附带一个小背包,它的包中存储了函数定义时作用域中的所有变量

以此引申出一个经典面试题

for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}

怎样可以使得上述代码的输出变为1,2,3,4,5?

使用es6我们可以很简单的做出解答:将var换成let。

那么我们使用刚刚学到的闭包知识怎么来解答呢?代码如下:

for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})(i)
}

根据上面的说法,将闭包看成一个背包,背包中包含定义时的变量,每次循环时,将i值保存在一个闭包中,当setTimeout中定义的操作执行时,则访问对应闭包保存的i值,即可解决。

5.立即调用的函数表达式(IIFE)

如下就是立即调用函数表达式

(function() { 
// 块级作用域
})();

使用 IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。

// IIFE 
(function () {
for (var i = 0; i < count; i++) {
console.log(i);
}
})();
console.log(i); // 抛出错误

ES6的块级作用域:

// 内嵌块级作用域 
{
let i;
for (i = 0; i < count; i++) {
console.log(i);
}
}
console.log(i); // 抛出错误
// 循环的块级作用域
for (let i = 0; i < count; i++) {
console.log(i);
}
console.log(i); // 抛出错误

IIFE的另一个作用就是上文中的解决settimeout的输出问题

附录知识点

关于instanceof

Function instanceof Object;//true
Object instanceof Function;//true

上述代码大家可以尝试在浏览器中跑一下,非常的神奇,那么这是什么原因呢?

借用大佬的一张图


//构造器Function的构造器是它自身
Function.constructor=== Function;//true

//构造器Object的构造器是Function(由此可知所有构造器的constructor都指向Function)
Object.constructor === Function;//true



//构造器Function的__proto__是一个特殊的匿名函数function() {}
console.log(Function.__proto__);//function() {}

//这个特殊的匿名函数的__proto__指向Object的prototype原型。
Function.__proto__.__proto__ === Object.prototype//true

//Object的__proto__指向Function的prototype,也就是上面中所述的特殊匿名函数
Object.__proto__ === Function.prototype;//true
Function.prototype === Function.__proto__;//true

结论:

  1. 所有的构造器的constructor都指向Function
  2. Function的prototype指向一个特殊匿名函数,而这个特殊匿名函数的__proto__指向Object.prototype

结尾

本文主要参考 《JavaScript 高级程序设计 第四版》 由于作者水平有限,如有错误,敬请与我联系,谢谢您的阅读!

原文:https://segmentfault.com/a/1190000039904453



收起阅读 »

避免 iOS 组件依赖冲突的小技巧

问题缘由本文以 YBImageBrowser 组件举例。YBImageBrowser 依赖了 SDWebImage,在使用 CocoaPods 集成到项目中时,可能会出现一些依赖冲突的问题,最近社区提了多个 Issues 并且在 Insights -> ...
继续阅读 »

问题缘由

本文以 YBImageBrowser 组件举例。

YBImageBrowser 依赖了 SDWebImage,在使用 CocoaPods 集成到项目中时,可能会出现一些依赖冲突的问题,最近社区提了多个 Issues 并且在 Insights -> Traffic -> Popular content 中看到了此类问题很高的关注度,所以不得不着手解决。

严格的版本限制

一个开源组件的迭代过程中,保证上层接口的向下兼容就不错了。为了优化性能并且控制内存,YBImageBrowser 没有直接用其最上层的接口,而是单独使用了下载模块和缓存模块,SDWebImage 的迭代升级很容易导致笔者的组件兼容不了,所以之前一直是类似这样依赖的:

s.dependency 'SDWebImage', '~> 5.0.0'

这样做的好处是限制足够小版本范围,降低 SDWebImage 接口变动导致组件代码错误的风险。但如果 SDWebImage 升级到 5.1.0,不管相关 API 是否变动,CocoaPods 都视为依赖冲突。

其它组件依赖了不同版本的 SDWebImage

当两个组件依赖了同一个组件的不同版本,并且依赖的版本没有交集,比如:

A.dependency 'SDWebImage', '~> 4.0.0'
B.dependency 'SDWebImage', '~> 5.0.0'

那么 A 和 B 同时集成进项目会出现依赖冲突。

解决方案

使用 CocoaPods 集成项目非常便捷,对于组件使用者来说,总是想在任何场景下都能轻易集成,并且能在将来享受组件的更新优化,显然前面提到的问题可能会影响集成的便捷性。

更模糊的版本限制

很多时候一个大版本的组件不会改动 API,并且对于社区流行的组件我们可以寄一定希望于其做好向下兼容,所以放宽依赖的版本限制能覆盖将来更多的版本(规则参考:podspec dependency):

s.dependency 'SDWebImage', '>= 5.0.0'

为什么不干脆去掉版本限制呢?
因为 YBImageBrowser 3.x 是基于 SDWebImage 5.0.0 开发的,笔者可以明确不兼容 5.0.0 之前的版本,所以在 SDWebImage 将来迭代版本出现相关 API 不兼容之前,这个限制都是“完美”覆盖所有版本的。

避免依赖冲突的暴力方案

当有其它组件依赖了不同版本的 SDWebImage,粗暴的解决方案如下:

  • 直接修改其它组件依赖的 SDWebImage 版本。

  • 将 YBImageBrowser 手动导入项目,并且修改代码去适应当前的 SDWebImage 版本。

  • 社区朋友一个 Issue 中提到的方法:在 ~/.cocoapods/repos 目录下找到 YBImageBrowser 文件夹,更改对应版本的 podspec.json 文件里对 SDWebImage 的依赖版本。

显然,上面的几种方案不太优雅,手动导入项目难以享受组件的更新优化,修改本地 repo 信息会因为 repo 列表的更新而复位。

避免依赖冲突的优雅方案

出现依赖冲突是必须要解决的问题,其它组件依赖的版本限制可以视为不变量,解决方案可以从组件的制作方面考虑。

要做到的目标是,既满足部分用户快速集成组件,又能让部分用户解决依赖冲突的前提下保证能享受组件将来的更新优化。

答案就是subspec,以下是 YBImageBrowser.podspec 部分代码(完整代码):

s.subspec "Core" do |core|
core.source_files = "YBImageBrowser/**/*.{h,m}"
core.dependency 'SDWebImage', '>= 5.0.0'
end
s.subspec "NOSD" do |core|
core.source_files = "YBImageBrowser/**/*.{h,m}"
core.exclude_files = "YBImageBrowser/WebImageMediator/YBIBDefaultWebImageMediator.{h,m}"
end

由此,用户可以自由的选择是否需要依赖 SDWebImage,在 Podfile 里的观感大致是这样:

// 依赖 SDWebImage
pod 'YBImageBrowser'
// 不依赖 SDWebImage
pod 'YBImageBrowser/NOSD'

那么在 YBImageBrowser 代码中应该如何区分是否依赖了 SDWebImage 并且提供默认实现呢?

第一步是设计一个抽象接口(这个接口不依赖 SDWebImage):

@protocol YBIBWebImageMediator <NSObject>
// Download methode, caching methode, and so on.
@end

第二步是在YBImageBrowser.h中定义一个遵循该接口的属性:

/// 图片下载缓存相关的中介者(赋值可自定义)
@property (nonatomic, strong) id<YBIBWebImageMediator> webImageMediator;

第三步是实现一个默认的中介者(这个类依赖了 SDWebImage):

@interface YBIBDefaultWebImageMediator : NSObject <YBIBWebImageMediator>
@end
@implementation YBIBDefaultWebImageMediator
//通过 SDWebImage 的 API 实现 <YBIBWebImageMediator> 协议方法
@end

第四步是在内部代码中通过条件编译导入并初始化默认中介者:

#if __has_include("YBIBDefaultWebImageMediator.h")
#import "YBIBDefaultWebImageMediator.h"
#endif
...
#if __has_include("YBIBDefaultWebImageMediator.h")
_webImageMediator = [YBIBDefaultWebImageMediator new];
#endif

第五步在 YBImageBrowser.podspec 中也可以看到,在不依赖 SDWebImage 的集成方式时排除了两个文件:YBIBDefaultWebImageMediator.{h.m}。

由此便实现了目标:

  • 用依赖 SDWebImage 的集成方式快速集成。

  • 使用不依赖 SDWebImage 的集成方式避免各种情况下的依赖冲突,但注意这种情况需要自行实现一个遵循<YBIBWebImageMediator>协议的中介者。

以上便是避免依赖冲突的小技巧,希望读者朋友能提出更好的建议或意见😁。

链接:https://www.jianshu.com/p/0e3283275300

收起阅读 »

什么,项目构建时内存溢出了?了解一下 node 内存限制

背景在之前的一篇文章中, 我们遇到了一个项目在构建时内存溢出的问题。当时的解决方案是: 直接调大 node 的内存限制,避免达到内存上限。今天听同事分享了一个新方法,觉得不错, 特此记录, 顺便分享给大家, 希望对大家有所帮助。正文但 Node 进程...
继续阅读 »

背景

在之前的一篇文章中, 我们遇到了一个项目在构建时内存溢出的问题。

当时的解决方案是: 直接调大 node 的内存限制,避免达到内存上限。

今天听同事分享了一个新方法,觉得不错, 特此记录, 顺便分享给大家, 希望对大家有所帮助。

正文


但 Node 进程的内存限制会是多少呢?

在网上查阅了到如下描述:

Currently, by default V8 has a memory limit of 512mb on 32-bit systems, and 1gb on 64-bit systems. The limit can be raised by setting --max-old-space-size to a maximum of ~1gb (32-bit) and ~1.7gb (64-bit), but it is recommended that you split your single process into several workers if you are hitting memory limits.

翻译一下:

当前,默认情况下,V8在32位系统上的内存限制为512mb,在64位系统上的内存限制为1gb。

可以通过将--max-old-space-size设置为最大〜1gb(32位)和〜1.7gb(64位)来提高此限制,但是如果达到内存限制, 建议您将单个进程拆分为多个工作进程

如果你想知道自己电脑的内存限制有多大, 可以直接把内存撑爆, 看报错。

运行如下代码:

// Small program to test the maximum amount of allocations in multiple blocks.
// This script searches for the largest allocation amount.

// Allocate a certain size to test if it can be done.
function alloc (size) {
const numbers = size / 8;
const arr = []
arr.length = numbers; // Simulate allocation of 'size' bytes.
for (let i = 0; i < numbers; i++) {
arr[i] = i;
}
return arr;
};

// Keep allocations referenced so they aren't garbage collected.
const allocations = [];

// Allocate successively larger sizes, doubling each time until we hit the limit.
function allocToMax () {
console.log("Start");

const field = 'heapUsed';
const mu = process.memoryUsage();

console.log(mu);

const gbStart = mu[field] / 1024 / 1024 / 1024;

console.log(`Start ${Math.round(gbStart * 100) / 100} GB`);

let allocationStep = 100 * 1024;

// Infinite loop
while (true) {
// Allocate memory.
const allocation = alloc(allocationStep);
// Allocate and keep a reference so the allocated memory isn't garbage collected.
allocations.push(allocation);
// Check how much memory is now allocated.
const mu = process.memoryUsage();
const gbNow = mu[field] / 1024 / 1024 / 1024;

console.log(`Allocated since start ${Math.round((gbNow - gbStart) * 100) / 100} GB`);
}

// Infinite loop, never get here.
};

allocToMax();


我的电脑是 Macbook Pro masOS Catalina 16GB,Node 版本是 v12.16.1,这段代码大概在 1.6 GB 左右内存时候抛出异常。

那我们现在知道 Node Process 确实是有一个内存限制的, 那我们就来增大它的内存限制再试一下。

用 node --max-old-space-size=6000 来运行这段代码,得到如下结果:


内存达到 4.6G 的时候也溢出了。

你可能会问, node 不是有内存回收吗?这个我们在下面会讲。

使用这个参数:node --max-old-space-size=6000, 我们增加的内存中老生代区域的大小,比较暴力。

就像上文中提到的: 如果达到内存限制, 建议您将单个进程拆分为多个工作进程

这个项目是一个 ts 项目,ts 文件的编译是比较占用内存的,如果把这部分独立成一个单独的进程, 情况也会有所改善。

因为 ts-loader 内部调用了 tsc,在使用 ts-loader 时,会使用 tsconfig.js配置文件。

当项目中的代码变的越来越多,体积也越来越庞大时,项目编译时间也随之增加。

这是因为 Typescript 的语义检查器必须在每次重建时检查所有文件

ts-loader 提供了一个 transpileOnly 选项,它默认为 false,我们可以把它设置为 true,这样项目编译时就不会进行类型检查,也不会输出声明文件。

对一下 transpileOnly 分别设置 false 和 true 的项目构建速度对比:

  • 当 transpileOnly 为 false 时,整体构建时间为 4.88s.
  • 当 transpileOnly 为 true 时,整体构建时间为 2.40s.

虽然构建速度提升了,但是有了一个弊端: 打包编译不会进行类型检查

好在官方推荐了这样一个插件, 提供了这样的能力: fork-ts-checker-webpack-plugin

官方示例的使用也非常简单:

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')

module.exports = {
...
plugins: [
new ForkTsCheckerWebpackPlugin()
]
}

在我这个实际的项目中,vue.config.js 修改如下:

configureWebpack: config => {
// get a reference to the existing ForkTsCheckerWebpackPlugin
const existingForkTsChecker = config.plugins.filter(
p => p instanceof ForkTsCheckerWebpackPlugin,
)[0];

// remove the existing ForkTsCheckerWebpackPlugin
// so that we can replace it with our modified version
config.plugins = config.plugins.filter(
p => !(p instanceof ForkTsCheckerWebpackPlugin),
);

// copy the options from the original ForkTsCheckerWebpackPlugin
// instance and add the memoryLimit property
const forkTsCheckerOptions = existingForkTsChecker.options;

forkTsCheckerOptions.memoryLimit = 4096;

config.plugins.push(new ForkTsCheckerWebpackPlugin(forkTsCheckerOptions));
}

修改之后, 构建就成功了。

关于垃圾回收

在 Node.js 里面,V8 自动帮助我们进行垃圾回收, 让我们简单看一下V8中如何处理内存。

一些定义

  • 常驻集大小:是RAM中保存的进程所占用的内存部分,其中包括:

    1. 代码本身
  • stack:包含原始类型和对对象的引用
  • 堆:存储引用类型,例如对象,字符串或闭包
  • 对象的浅层大小:对象本身持有的内存大小
  • 对象的保留大小:删除对象及其相关对象后释放的内存大小

垃圾收集器如何工作

垃圾回收是回收由应用程序不再使用的对象所占用的内存的过程。

通常,内存分配很便宜,而内存池用完时收集起来很昂贵。

如果无法从根节点访问对象,则该对象是垃圾回收的候选对象,因此该对象不会被根对象或任何其他活动对象引用。

根对象可以是全局对象,DOM元素或局部变量。

堆有两个主要部分,即 New Space和 Old Space

新空间是进行新分配的地方。

在这里收集垃圾的速度很快,大小约为1-8MB

留存在新空间中的物体被称为新生代

在新空间中幸存下来的物体被提升的旧空间-它们被称为老生代

旧空间中的分配速度很快,但是收集费用很高,因此很少执行。

node 垃圾回收

Why is garbage collection expensive?

The V8 JavaScript engine employs a stop-the-world garbage collector mechanism.

In practice, it means that the program stops execution while garbage collection is in progress.

通常,约20%的年轻一代可以存活到老一代,旧空间的收集工作将在耗尽后才开始。

为此,V8 引擎使用两种不同的收集算法

  1. Scavenge: 速度很快,可在新生代上运行,
  2. Mark-Sweep: 速度较慢,并且可以在老生代上运行。

篇幅有限,关于v8垃圾回收的更多信息,可以参考如下文章:

  1. http://jayconrod.com/posts/55...
  2. https://juejin.cn/post/684490...
  3. https://juejin.cn/post/684490...

总结

小小总结一下,上文介绍了两种方式:

  1. 直接加大内存,使用: node --max-old-space-size=4096
  2. 把一些耗内存进程独立出去, 使用了一个插件: fork-ts-checker-webpack-plugin

希望大家留个印象, 记得这两种方式。

好了, 内容就这么多, 谢谢。

才疏学浅,如有错误, 欢迎指正。

谢谢。

原文:https://segmentfault.com/a/1190000039877970


收起阅读 »

前端常用图片文件下载上传方法

本文整理了前端常用的下载文件以及上传文件的方法例子均以vue+element ui+axios为例,不使用el封装好的上传组件,这里自行进行封装实现先附上demo上传文件以图片为例,文件上传可以省略预览图片功能图片上传可以使用2种方式:文件流和base64;1...
继续阅读 »

本文整理了前端常用的下载文件以及上传文件的方法
例子均以vue+element ui+axios为例,不使用el封装好的上传组件,这里自行进行封装实现

先附上demo

上传文件

以图片为例,文件上传可以省略预览图片功能

图片上传可以使用2种方式:文件流base64;

1.文件流上传+预览

<input type="file" id='imgBlob' @change='changeImgBlob' />
<el-image style="width: 100px; height: 100px" :src="imgBlobSrc"></el-image>
// data
imgBlobSrc: ""

// methods
changeImgBlob() {
let file = document.querySelector("#imgBlob");
/**
*图片预览
*更适合PC端,兼容ie7+,主要功能点是window.URL.createObjectURL
*/
var ua = navigator.userAgent.toLowerCase();
if (/msie/.test(ua)) {
this.imgBlobSrc = file.value;
} else {
this.imgBlobSrc = window.URL.createObjectURL(file.files[0]);
}
//上传后台
const fd = new FormData();
fd.append("files", file.files[0]);
fd.append("xxxx", 11111); //其他字段,根据实际情况来
axios({
url: "/yoorUrl", //URL,根据实际情况来
method: "post",
headers: { "Content-Type": "multipart/form-data" },
data: fd
});
}



2.Base64上传+预览

<input type="file" id='imgBase' @change='changeImgBase' />
<el-image style="width: 100px; height: 100px" :src="imgBaseSrc"></el-image>
// data
imgBaseSrc : ""

// methods
changeImgBase() {
let that = this;
let file = document.querySelector("#imgBase");
/**
*图片预览
*更适合H5页面,兼容ie10+,图片base64显示,主要功能点是FileReader和readAsDataURL
*/
if (window.FileReader) {
var fr = new FileReader();
fr.onloadend = function (e) {
that.imgBaseSrc = e.target.result;
//上传后台
axios({
url: "/yoorUrl", //URL,根据实际情况来
method: "post",
data: {
files: that.imgBaseSrc
}
});
};
fr.readAsDataURL(file.files[0]);
}
}


下载文件

图片下载

假设需要下载图片为url文件流处理和这个一样

<el-image style="width: 100px; height: 100px" :src="downloadImgSrc"></el-image>
<el-button type="warning" round plain size="mini" @click='downloadImg'>点击下载</el-button>
  • 注意:这里需要指定 responseTypeblob
//data
downloadImgSrc:'https://i.picsum.photos/id/452/400/300.jpg?hmac=0-o_NOka_K6sQ_sUD84nxkExoDk3Bc0Qi7Y541CQZEs'
//methods
downloadImg() {
axios({
url: this.downloadImgSrc, //URL,根据实际情况来
method: "get",
responseType: "blob"
}).then(function (response) {
const link = document.createElement("a");
let blob = new Blob([response.data], { type: response.data.type });
let url = URL.createObjectURL(blob);
link.href = url;
link.download = `实际需要的文件名.${response.data.type.split('/')[1]}`;
link.click();
document.body.removeChild(link);
});
}

文件下载(以pdf为例)

<el-image style="width: 100px; height: 100px" :src="downloadImgSrc"></el-image>
<el-button type="warning" round plain size="mini" @click='downloadImg'>点击下载</el-button>
//data
downloadImgSrc:'https://i.picsum.photos/id/452/400/300.jpg?hmac=0-o_NOka_K6sQ_sUD84nxkExoDk3Bc0Qi7Y541CQZEs'
//methods
downloadImg() {
axios({
url: this.downloadImgSrc, //URL,根据实际情况来
method: "get",
responseType: "blob"
}).then(function (response) {
const link = document.createElement("a");
let blob = new Blob([response.data], { type: response.data.type });
let url = URL.createObjectURL(blob);
link.href = url;
link.download = `实际需要的文件名.${response.data.type.split('/')[1]}`;
link.click();
document.body.removeChild(link);
});
}

pdf预览可以参考如何预览以及下载pdf文件

原文:https://segmentfault.com/a/1190000039893814



收起阅读 »

iOS核心动画高级技巧-1

1. 图层树图层的树状结构巨妖有图层,洋葱也有图层,你有吗?我们都有图层 -- 史莱克Core Animation其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做Layer Kit这么一个不怎么和动画有关的名字演变而来,所以做动画...
继续阅读 »

1. 图层树

图层的树状结构

巨妖有图层,洋葱也有图层,你有吗?我们都有图层 -- 史莱克
Core Animation其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做Layer Kit这么一个不怎么和动画有关的名字演变而来,所以做动画这只是Core Animation特性的冰山一角。

Core Animation是一个复合引擎,它的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的图层,存储在一个叫做图层树的体系之中。于是这个树形成了UIKit以及在iOS应用程序当中你所能在屏幕上看见的一切的基础。

在我们讨论动画之前,我们将从图层树开始,涉及一下Core Animation的静态组合以及布局特性。

1.1 图层与视图

图层与视图

如果你曾经在iOS或者Mac OS平台上写过应用程序,你可能会对视图的概念比较熟悉。一个视图就是在屏幕上显示的一个矩形块(比如图片,文字或者视频),它能够拦截类似于鼠标点击或者触摸手势等用户输入。视图在层级关系中可以互相嵌套,一个视图可以管理它的所有子视图的位置。图1.1显示了一种典型的视图层级关系

1.2 图层的能力

图层的能力

如果说CALayer是UIView内部实现细节,那我们为什么要全面地了解它呢?苹果当然为我们提供了优美简洁的UIView接口,那么我们是否就没必要直接去处理Core Animation的细节了呢?

一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:1012951431, 分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。

某种意义上说的确是这样,对一些简单的需求来说,我们确实没必要处理CALayer,因为苹果已经通过UIView的高级API间接地使得动画变得很简单。

但是这种简单会不可避免地带来一些灵活上的缺陷。如果你略微想在底层做一些改变,或者使用一些苹果没有在UIView上实现的接口功能,这时除了介入Core Animation底层之外别无选择。

我们已经证实了图层不能像视图那样处理触摸事件,那么他能做哪些视图不能做的呢?这里有一些UIView没有暴露出来的CALayer的功能:

  • 阴影,圆角,带颜色的边框

  • 3D变换

  • 非矩形范围

  • 透明遮罩

  • 多级非线性动画

我们将会在后续章节中探索这些功能,首先我们要关注一下在应用程序当中CALayer是怎样被利用起来的。

1.3 使用图层

使用图层

首先我们来创建一个简单的项目,来操纵一些layer的属性。打开Xcode,使用Single View Application模板创建一个工程。

在屏幕中央创建一个小视图(大约200 X 200的尺寸),当然你可以手工编码,或者使用Interface Builder(随你方便)。确保你的视图控制器要添加一个视图的属性以便可以直接访问它。我们把它称作layerView。

运行项目,应该能在浅灰色屏幕背景中看见一个白色方块,如果没看见,可能需要调整一下背景window或者view的颜色

之后就可以在代码中直接引用CALayer的属性和方法。在清单1.1中,我们用创建了一个CALayer,设置了它的backgroundColor属性,然后添加到layerView背后相关图层的子图层(这段代码的前提是通过IB创建了layerView并做好了连接),图1.5显示了结果。

清单1.1 给视图添加一个蓝色子图层

#import "ViewController.h"
#import
@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
//create sublayer
CALayer *blueLayer = [CALayer layer];
blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
blueLayer.backgroundColor = [UIColor blueColor].CGColor;
//add it to our view
[self.layerView.layer addSublayer:blueLayer];
}
@end

1.4 总结

总结

这一章阐述了图层的树状结构,说明了如何在iOS中由UIView的层级关系形成的一种平行的CALayer层级关系,在后面的实验中,我们创建了自己的CALayer,并把它添加到图层树中。
在第二章,“图层关联的图片”,我们将要研究一下CALayer关联的图片,以及Core Animation提供的操作显示的一些特性。

2. 寄宿图

寄宿图

图片胜过千言万语,界面抵得上千图片 ——Ben Shneiderman
我们在第一章『图层树』中介绍了CALayer类并创建了一个简单的有蓝色背景的图层。背景颜色还好啦,但是如果它仅仅是展现了一个单调的颜色未免也太无聊了。事实上CALayer类能够包含一张你喜欢的图片,这一章节我们将来探索CALayer的寄宿图(即图层中包含的图)。

2.1 contents属性

contents属性

CALayer 有一个属性叫做contents,这个属性的类型被定义为id,意味着它可以是任何类型的对象。在这种情况下,你可以给contents属性赋任何值,你的app仍然能够编译通过。但是,在实践中,如果你给contents赋的不是CGImage,那么你得到的图层将是空白的。

contents这个奇怪的表现是由Mac OS的历史原因造成的。它之所以被定义为id类型,是因为在Mac OS系统上,这个属性对CGImage和NSImage类型的值都起作用。如果你试图在iOS平台上将UIImage的值赋给它,只能得到一个空白的图层。一些初识Core Animation的iOS开发者可能会对这个感到困惑。

头疼的不仅仅是我们刚才提到的这个问题。事实上,你真正要赋值的类型应该是CGImageRef,它是一个指向CGImage结构的指针。UIImage有一个CGImage属性,它返回一个"CGImageRef",如果你想把这个值直接赋值给CALayer的contents,那你将会得到一个编译错误。因为CGImageRef并不是一个真正的Cocoa对象,而是一个Core Foundation类型。

尽管Core Foundation类型跟Cocoa对象在运行时貌似很像(被称作toll-free bridging),他们并不是类型兼容的,不过你可以通过bridged关键字转换。如果要给图层的寄宿图赋值,你可以按照以下这个方法:

layer.contents = (__bridge id)image.CGImage;

如果你没有使用ARC(自动引用计数),你就不需要 __bridge 这部分。但是,你干嘛不用ARC?!

让我们来继续修改我们在第一章新建的工程,以便能够展示一张图片而不仅仅是一个背景色。我们已经用代码的方式建立一个图层,那我们就不需要额外的图层了。那么我们就直接把layerView的宿主图层的contents属性设置成图片。

清单2.1 更新后的代码。

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad]; //load an image
UIImage *image = [UIImage imageNamed:@"Snowman.png"];

//add it directly to our view's layer
self.layerView.layer.contents = (__bridge id)image.CGImage;
}
@end

图表2.1 在UIView的宿主图层中显示一张图片


我们用这些简单的代码做了一件很有趣的事情:我们利用CALayer在一个普通的UIView中显示了一张图片。这不是一个UIImageView,它不是我们通常用来展示图片的方法。通过直接操作图层,我们使用了一些新的函数,使得UIView更加有趣了。

contentGravity

你可能已经注意到了我们的雪人看起来有点。。。胖 ==! 我们加载的图片并不刚好是一个方的,为了适应这个视图,它有一点点被拉伸了。在使用UIImageView的时候遇到过同样的问题,解决方法就是把contentMode属性设置成更合适的值,像这样:
后续精彩内容请转到我的博客继续观看

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

收起阅读 »

web 埋点实现原理了解一下

前言埋点,是网站分析的一种常用的数据采集方法。我们主要用来采集用户行为数据(例如页面访问路径,点击了什么元素)进行数据分析,从而让运营同学更加合理的安排运营计划。现在市面上有很多第三方埋点服务商,百度统计,友盟,growingIO 等大家应该都不太陌生,大多情...
继续阅读 »

前言

埋点,是网站分析的一种常用的数据采集方法。我们主要用来采集用户行为数据(例如页面访问路径,点击了什么元素)进行数据分析,从而让运营同学更加合理的安排运营计划。现在市面上有很多第三方埋点服务商,百度统计,友盟,growingIO 等大家应该都不太陌生,大多情况下大家都只是使用,最近我研究了下 web 埋点,你要不要了解下。

现有埋点三大类型

用户行为分析是一个大系统,一个典型的数据平台。由用户数据采集,用户行为建模分析,可视化报表展示几个模块构成。现有的埋点采集方案可以大致被分为三种,手动埋点,可视化埋点,无埋点
  1. 手动埋点
    手动代码埋点比较常见,需要调用埋点的业务方在需要采集数据的地方调用埋点的方法。优点是流量可控,业务方可以根据需要在任意地点任意场景进行数据采集,采集信息也完全由业务方来控制。这样的有点也带来了一些弊端,需要业务方来写死方法,如果采集方案变了,业务方也需要重新修改代码,重新发布。
  2. 可视化埋点
    可是化埋点是近今年的埋点趋势,很多大厂自己的数据埋点部门也都开始做这块。优点是业务方工作量少,缺点则是技术上推广和实现起来有点难(业务方前端代码规范是个大前提)。阿里的活动页很多都是运营通过可视化的界面拖拽配置实现,这些活动控件元素都带有唯一标识。通过埋点配置后台,将元素与要采集事件关联起来,可以自动生成埋点代码嵌入到页面中。
  3. 无埋点
    无埋点则是前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据,优点是前端只要加载埋点脚本。缺点是流量和采集的数据过于庞大,服务器性能压力山大,主流的 GrowingIO 就是这种实现方案。

我们暂时放弃可视化埋点的实现,在 手动埋点 和 无埋点 上进行了尝试,为了便于描述,下文我会称采集脚本为 SDK。

思考几个问题

埋点开发需要考虑很多内容,贯穿着不轻易动手写代码的原则,我们在开发前先思考下面这几个问题
  1. 我们要采集什么内容,进行哪些采集接口的约定
  2. 业务方通过什么方式来调用我们的采集脚本
  3. 手动埋点:SDK 需要封装一个方法给业务方进行调用,传参方式业务方可控
  4. 无埋点:考虑到数据量对于服务器的压力,我们需要对无埋点进行开关配置,可以配置进行哪些元素进行无埋点采集
  5. 用户标识:游客用户和登录用户的采集数据怎么进行区分关联
  6. 设备Id:用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样,怎么实现
  7. 单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异
  8. 混合应用:app 与 h5 的混合应用我们要怎么进行通讯

我们要采集什么内容,进行哪些采集接口的约定

第一期我们先实现对 PV(即页面浏览量或点击量) 、UV(一天内同个访客多次访问) 、点击量、用户的访问路径的基础指标的采集。精细化分析的流量转化需要和业务相关,需要和数据分析方做约定,我们预留扩展。所以我们的采集接口需要进行以下的约定

{
"header":{ // HTTP 头部
"X-Device-Id":" 550e8400-e29b-41d4-a716-446655440000", //设备ID,用来区分用户设备
"X-Source-Url":"https://www.baidu.com/", //源地址,关联用户的整个操作流程,用于用户行为路径分析,例如登录,到首页,进入商品详情,退出这一整个完整的路径
"X-Current-Url":"", //当前地址,用户行为发生的页面
"X-User-Id":"",//用户ID,统计登录用户行为
},
"body":[{ // HTTP Body体
"PageSessionID":"", //页面标识ID,用来区分页面事件,例如加载和离开我们会发两个事件,这个标识可以让我们知道这个事件是发生在一个页面上
"Event":"loaded", //事件类型,区分用户行为事件
"PageTitle": "埋点测试页", //页面标题,直观看到用户访问页面
"CurrentTime": “1517798922201”, //事件发生的时间
"ExtraInfo": {
} //扩展字段,对具体业务分析的传参
}]
}

以上就是我们现在约定好了的通用的事件采集的接口,所传的参数基本上会根据采集事件的不同而发生变化。但是在用户的整一个访问行为中,用户的设备是不会变化的,如果你想采集设备信息可以重新约定一个接口,在整个采集开始之前发送设备信息,这样可以避免在事件采集接口上重复采集固定数据。

{
"header":{ // HTTP 头部
"X-Device-Id" :"550e8400-e29b-41d4-a716-446655440000" , // 设备id
},
"body":{ // HTTP Body体
"DeviceType": "web" , //设备类型
"ScreenWide" : 768 , // 屏幕宽
"ScreenHigh": 1366 , // 屏幕高
"Language": "zh-cn" //语言
}
}

手动埋点:SDK

如果业务方需要采集更多业务定制的数据,可以调用我们暴露出的方法进行采集

//自定义事件
sdk.dispatch('customEvent',{extraInfo:'自定义事件的额外信息'})

游客与用户关联

我们使用 userId 来做用户标识,同一个设备的用户,从游客用户切换到登录用户,如果我们要把他们关联起来,需要有一个设备Id 做关联

web 设备Id

用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样。web 变量存储,我们第一时间想到的就是 cookie,sessionStorage,localStorage,但是这3种存储方式都和访问资源的域名相关。我们总不能每次访问一个网站就新建一个设备指纹吧,所以我们需要通过一个方法来跨域共享设备指纹

我们想到的方案是,通过嵌套 iframe 加载一个静态页面,在 iframe 上加载的域名上存储设备id,通过跨域共享变量获取设备id,共享变量的原理是采用了iframe 的 contentWindow通讯,通过 postMessage 获取事件状态,调用封装好的回调函数进行数据处理具体的实现方式

//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id,
collect.setIframe = function () {
var that = this
var iframe = document.createElement('iframe')
iframe.id = "frame",
iframe.src = 'http://collectiframe.trc.com' // 配置域名代理,目的是让开发测试生产环境代码一致
iframe.style.display='none' //iframe 设置的目的是用来生成固定的设备id,不展示
document.body.appendChild(iframe)

iframe.onload = function () {
iframe.contentWindow.postMessage('loaded','*');
}

//监听message事件,iframe 加载完成,获取设备id ,进行相关的数据采集
helper.on(window,"message",function(event){
that.deviceId = event.data.deviceId

if(event.data && event.data.type == 'loaded'){
that.sendDevice(that.getDevice(), that.deviceUrl);
setTimeout(function () {
that.send(that.beforeload)
that.send(that.loaded)
},1000)
}
})
}

iframe 与 SDK 通讯

function receiveMessageFromIndex ( event ) {
getDeviceInfo() // 获取设备信息
var data = {
deviceId: _deviceId,
type:event.data
}

event.source.postMessage(data, '*'); // 将设备信息发送给 SDK
}

//监听message事件
if(window.addEventListener){
window.addEventListener("message", receiveMessageFromIndex, false);
}else{
window.attachEvent("onmessage", receiveMessageFromIndex, false)

如果你想知道可以看我的另一篇博客 web 浏览器指纹跨域共享

单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异

我们知道单页面应用都是无刷新的页面加载,所以我们在页面跳转的处理和我们的普通的页面会有所不同。单页面应用的路由插件运用了 window 自带的无刷新修改用户浏览记录的方法,pushState 和 replaceState。

window 的 history 对象 提供了两个方法,能够无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录,所以我们只要改写 history 的方法,在方法执行前执行我们的采集方法就能实现对单页面应用的页面跳转事件的采集了

// 改写思路:拷贝 window 默认的 replaceState 函数,重写 history.replaceState 在方法里插入我们的采集行为,在重写的 replaceState 方法最后调用,window 默认的 replaceState 方法

collect = {}
collect.onPushStateCallback : function(){} // 自定义的采集方法

(function(history){
var replaceState = history.replaceState; // 存储原生 replaceState
history.replaceState = function(state, param) { // 改写 replaceState
var url = arguments[2];
if (typeof collect.onPushStateCallback == "function") {
collect.onPushStateCallback({state: state, param: param, url: url}); //自定义的采集行为方法
}
return replaceState.apply(history, arguments); // 调用原生的 replaceState
};
})(window.history);

这块介绍起来也比较的复杂,如果你想了解更多,可以看我的另一篇博客你需要知道的单页面路由实现原理

混合应用:app 与 h5 的混合应用我们要怎么进行通讯

现在大部分的应用都不是纯原生的应用, app 与 h5 的混合的应用是现在的一种主流。

纯 web 数据采集我们考虑到前端存储数据容易丢失,我们在每一次事件触发的时候都用采集接口传输采集到的数据。考虑到现在很多用户的手机会有流量管家的软件监控,如果在 App 中 h5 还是采集到数据就传输给服务端,很有可能会让流量管家检测到,给用户报警,从而使得用户不再信任你的 App , 所以我们在用户操作的时候将数据传给 app 端,存储到 app。用户切换应用到后台的时候,通过 app 端的 SDK 打包传输到服务器,我们给 app 提供的方法封装了一个适配器

// app 与 h5 混合应用,直接将数信息发给 app
collect.saveEvent = function (jsonString) {

collect.dcpDeviceType && setTimeout(function () {
if(collect.dcpDeviceType=='android'){
android.saveEvent(jsonString)
} else {
window.webkit && window.webkit.messageHandlers ? window.webkit.messageHandlers.nativeBridge.postMessage(jsonString) : window.postBridgeMessage(jsonString)
}

},1000)
}

实现思路

通过上面几个问题的思考,我们对埋点的实现大致已经有了一些想法,我们使用思维导图来还原下我们即将要做的事情,图片记得放大看哦,太小了可能看不清。

我们需要暴露给业务方调用的方法



我们来看下几个核心代码的实现

工具方法

我们定义了几个工具方法,提高开发的幸福指数 😝

var helper = {};

// 生成一个唯一的标识,pageSessionId (用这个变量来关联开始加载、加载完成、离开页面的事件,计算出页面加菜时间,停留时间)
helper.uuid = function(){}

// 元素绑定事件监听,兼容浏览器到IE8
helper.on = function(){}

//元素移除事件监听的适配器函数,兼容浏览器到IE8
helper.remove = function(){}

//将json转为字符串,事件传输的参数类型转化
helper.changeJSON2Query = function(){}

//将相对路径解析成文档全路径
helper.normalize = function(){}

采集逻辑

var collect = {
deviceUrl:'http://collect.trc.com/rest/collect/device/h5/v1',
eventUrl:'http://collect.trc.com/rest/collect/event/h5/v1',
isuploadUrl:'http://collect.trc.com/rest/collect/isupload/app/v1',
parmas:{ ExtraInfo:{} },
device:{}
};

//获取埋点配置
collect.setParames = function(){}

//更新访问路径及页面信息
collect.updatePageInfo = function(){}

//获取事件参数
collect.getParames = function(){}

//获取设备信息
collect.getDevice = function(){}

//事件采集
collect.send = function(){}

//设备采集
collect.sendDevice = function(){}

//判断才否采集,埋点采集的开关
collect.isupload = function(){

1. 判断是否采集,不采集就注销事件监听(项目中区分游客身份和用户身份的采集情况,这个方法会被判断两次)
2. 采集则判断是否已经采集过
a.已经采集过不做任何操作
b.没有采集过添加事件监听
3. 判断是 混合应用还是纯 web 应用
a.如果是web 应用,调用 collect.setIframe 设置 iframe
b.如果是混合应用 将开始加载和加载完成事件传输给 app
}

//点击事件处理函数
collect.clickHandler = function(){}

//离开页面的事件处理函数
collect.beforeUnloadHandler = function(){}

//页面回退事件处理函数
collect.onPopStateHandler = function(){}

//系统事件初始化,注册离开事件,浏览器后退事件
collect.event = function(){}

//获取记录开始加载数据信息
collect.getBeforeload = function(){}

//存储加载完成,获取设备类型,记录加载完成信息
collect.onload = function(){

1. 判断cookie是否有存设备类型信息,有表示混合应用
2. 采集加载完成时间等信息
3. 调用 collect.isupload 判断是否进行采集
}

//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id
collect.setIframe = function(){}

//app 与 h5 混合应用,直接将数信息发给 app,判断设备类型做原生方法适配器
collect.saveEvent = function(){}

//采集自定义事件类型
collect.dispatch = function(){}

//将参数 userId 存入sessionStorage
collect.storeUserId = function(){}

//采集H5信息,如果是混合应用,将采集到的信息发送给 app 端
collect.saveEventInfo = function(){}

//页面初始化调用方法
collect.init = function(){

1. 获取开始加载的采集信息
2. 获取 SDK 配置信息,设备信息
3. 改写 history 两个方法,单页面应用页面跳转前调用我们自己的方法
4. 页面加载完成,调用 collect.onload 方法

}


collect.init(); // 初始化

//暴露给业务方调用的方法
return {
dispatch:collect.dispatch,
storeUserId:collect.storeUserId,
}

原文链接:https://segmentfault.com/a/1190000014922668


收起阅读 »

解决 SourceKitService 内存占用过高

SourceKitService 是用来服务于解析 Swift 代码格式的,和 Swift 的代码着色、类型自动推断等特性息息相关,如果我们在活动监视器中强制停止掉这个服务,那么会发现 Xcode 中 Swift 代码大部分都会变成白色,并代码提示和类型推断都...
继续阅读 »

SourceKitService 是用来服务于解析 Swift 代码格式的,和 Swift 的代码着色、类型自动推断等特性息息相关,如果我们在活动监视器中强制停止掉这个服务,那么会发现 Xcode 中 Swift 代码大部分都会变成白色,并代码提示和类型推断都失效了。

但是在我今天写代码的时候发现,这个服务突然占用了很高的 CPU 以及内存,曾一度达到 201% 和 5.7GB 的占用率,直接导致了无法编译、没有代码提示等问题。

搜索了一些资料后,网络上给出了两个具体的方案,根据这篇问题:https://stackoverflow.com/questions/26151954/sourcekitservice-consumes-cpu-and-grinds-xcode-to-a-halt

回答中指出了,首先可以尝试删除这个服务产生的缓存,然后手动终止掉这个服务,等待 Xcode 重新开启,可能会解决。
第二个办法就是,因为这个服务的天生缺陷,在进行复杂的字面量类型推断时,可能会造成占用大量资源,具体一点讲就是在写一个很长的数组时,不要写成以下这样:

let array = ["": [""], "": [""], "": [""], "": [""], "": [""], "": [""] ... ]

而是要给一个明确的类型,帮助 Xcode 进行推断:

let array: [String: [String]] = ["": [""], "": [""], "": [""], "": [""], "": [""], "": [""] ... ]

道理是这么个道理,但是我检查了我的代码之后,发现并没有类似的写法的数组,甚至连长数组都没有,就算给所有数组都手动加上类型,也无济于事。

后来发现,不光是数组,普通的变量频繁的进行“串联推断”也会导致这个问题,具体例子如下:

let userToken = (dataModel?.id ?? "") + (dataModel?.token ?? "") + (dataModel?.timestamp ?? "") + ...

这种写法同样会增加自动类型推断的负担,偶尔甚至会造成代码不能通过编译阶段。
所以,我改成了这种写法:

let userID = dataModel?.id ?? ""
let token = dataModel?.token ?? ""
let timestamp = dataModel?.timestamp ?? ""
...
let userToken = userID + token + timestamp + ...

经过改动之后,一切回归正常。

明明是想偷个懒,不想多写那么多属性,结果反而造成了雪崩式的麻烦,Xcode 瞬间变成了全球最大的 TXT 编辑器,看来以后还是要多注意一下规范问题啊~

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

收起阅读 »

iOS-网络图片预览器(缩放,拖拽等手势)

预览效果(原位置启动,放大缩小,拖拽关闭,支持长图,跳转其他界面):视图结构:present跳转一个UINavigationController,UINavigationController的根跟控制是UIViewController,在viewcontrol...
继续阅读 »

预览效果(原位置启动,放大缩小,拖拽关闭,支持长图,跳转其他界面):





视图结构:present跳转一个UINavigationController,UINavigationController的根跟控制是UIViewController,在viewcontroller上添加预览器。

功能实现:
1.使用UICollectionView,在UICollectionViewCell上有个一UIScrollView的容器,在上面使用UIImageView展示图片,使用UIScrollView是为了方便实现长图预览,图片放大缩小的功能。

2.网络图片大小的处理:
第一种情况,一般来说在网络较好的情况下都会在列表页(如图列表)时已经加载完图片,在cell中直接拿到图片对象获得大小,根据显示区域的比例计算宽高。

第二种情况,在显示预览时,图片还没有加载完成(图片较大,网络较卡),我会给一个默认的宽高,在网络图片加载完成时,重新刷新他的宽高。(Data形式获取宽高有性能问题,放弃了)

3.拖拽动画和手势的处理:
在UIScrollView上添加了一个UIPanGestureRecognizer拖动手势,开启手势共享。然后根据触摸屏幕时水平方向和垂直方向的速度来判断是进行UICollectionView的左右滑动还是拖拽。在拖拽手势中,根据移动的Y轴距离的和整个区的的高度比例来缩放UIScrollView(使用transform来缩放)

总结:基本能满足一些普通要求的项目,如果想添加更多功能,可以在viewcontroller上添加,改变预览器的显示区域等。

Demo地址

作者:约德尔人郭敬明
链接:https://www.jianshu.com/p/7dda7add67e6
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

2019年11月:JD iOS开发岗面试题及答案!

随着移动互联网技术的不断发展和创新,访谈对于公司和开发人员和设计师来说都是费时且昂贵的项目,面对iOS开发者和设计师在访谈过程中可能遇到的问题,现在为大家总结iOS技术面试题及应对答案。一、如何绘制UIView?绘制一个UIView最灵活的方法就是由它自己完成...
继续阅读 »

随着移动互联网技术的不断发展和创新,访谈对于公司和开发人员和设计师来说都是费时且昂贵的项目,面对iOS开发者和设计师在访谈过程中可能遇到的问题,现在为大家总结iOS技术面试题及应对答案。

一、如何绘制UIView?
绘制一个UIView最灵活的方法就是由它自己完成绘制。实际上你不是绘制一个UIView,而是子类化一个UIView并赋予绘制自己的能力。当一个UIView需要执行绘制操作时,drawRect:方法就会被调用,覆盖此方法让你获得绘图操作的机会。当drawRect:方法被调用,当前图形的上下文也被设置为属于视图的图形上下文,你可以使用Core Graphic或者UIKit提供的方法将图形画在该上下文中。

二、什么是MVVM?主要目的是什么?优点有哪些?
MVVM即 Model-View-ViewModel

1.View主要用于界面呈现,与用户输入设备进行交互、

2.ViewModel是MVVM架构中最重要的部分,ViewModel中包含属性,方法,事件,属性验证等逻辑,负责View与Model之间的通讯

3.Model就是我们常说的数据模型,用于数据的构造,数据的驱动,主要提供基础实体的属性。

MVVM主要目的是分离视图和模型

MVVM优点:低耦合,可重用性,独立开发,可测试

三、get请求与post请求的区别

1.get是向服务器发索取数据的一种请求,而post是向服务器提交数据的一种请求

2.get没有请求体,post有请求体

3.get请求的数据会暴露在地址栏中,而post请求不会,所以post请求的安全性比get请求号

4.get请求对url长度有限制,而post请求对url长度理论上是不会收限制的,但是实际上各个服务器会规定对post提交数据大小进行限制。

四、谈谈你对多线程开发的理解?ios中有几种实现多线程的方法?
好处:

1.使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片,视频的下载;

2.发挥多核处理器的优势,并发执行让系统运行的更快,更流畅,用户体验更好;

缺点:
1.大量的线程降低代码的可读性;

2.更多的线程需要更多的内存空间;

3当多个线程对同一个资源出现争夺的时候要注意线程安全的问题。

ios有3种多线程编程的技术:1.NSThread,2.NSOperationQueue,3.gcd;

五、XMPP工作原理;xmpp系统特点
原理:
1.所有从一个client到另一个client的jabber消息和数据都要通过xmpp server

2.client链接到server

3.server利用本地目录系统的证书对其认证

4.server查找,连接并进行相互认证

5.client间进行交互

特点:1)客户机/服务器通信模式;2)分布式网络;3)简单的客户端;4)XML的数据格式

六、地图的定位是怎么实现的?
1.导入了CoreLocation.framework

2.ios8以后,如果需要使用定位功能,就需要请求用户授权,在首次运行时会弹框提示

3.通过本机自带的gps获取位置信息(即经纬度)

七、苹果内购实现流程

程序通过bundle存储的plist文件得到产品标识符的列表。

程序向App Store发送请求,得到产品的信息。

App Store返回产品信息。

程序把返回的产品信息显示给用户(App的store界面)

用户选择某个产品

程序向App Store发送支付请求

App Store处理支付请求并返回交易完成信息。

App获取信息并提供内容给用户。

八、支付宝,微信等相关类型的sdk的集成

1.在支付宝开发平台创建应用并获取APPID

2.配置密钥

3.集成并配置SDK

4.调用接口(如交易查询接口,交易退款接口)

九、 gcd产生死锁的原因及解锁的方法

产生死锁的必要条件:1.互斥条件,2.请求与保持条件,3.不剥夺条件,4.循环等待条件。

解决办法:采用异步执行block。

十、生成二维码的步骤
1.使用CIFilter滤镜类生成二维码

2.对生成的二维码进行加工,使其更清晰

3.自定义二维码背景色、填充色

4.自定义定位角标

5.在二维码中心插入小图片

十一、在使用XMPP的时候有没有什么困难

发送附件(图片,语音,文档...)时比较麻烦

XMPP框架没有提供发送附件的功能,需要自己实现

实现方法,把文件上传到文件服务器,上传成功后获取文件保存路径,再把附件的路径发送给好友

十二、是否使用过环信,简单的说下环信的实现原理

环信是一个即时通讯的服务提供商

环信使用的是XMPP协议,它是再XMPP的基础上进行二次开发,对服务器Openfire和客户端进行功能模型的添加和客户端SDK的封装,环信的本质还是使用XMPP,基于Socket的网络通信

环信内部实现了数据缓存,会把聊天记录添加到数据库,把附件(如音频文件,图片文件)下载到本地,使程序员更多时间是花到用户体验体验上。

链接:https://www.jianshu.com/p/3b7cc68cce20

收起阅读 »

iOS 可用的热更新、热修复方案

前言JSPatch虽然在两年前被苹果邮件警告,但是ReactNative依然盛行,只不过ReactNative并没有对Native进行热修复的功能,只是动态下发新的bundle模块。动态加载而已。很多时候线上出现bug,可能是很小,很细微的。对此我们可能仅仅需...
继续阅读 »

前言
JSPatch虽然在两年前被苹果邮件警告,但是ReactNative依然盛行,只不过ReactNative并没有对Native进行热修复的功能,只是动态下发新的bundle模块。动态加载而已。

很多时候线上出现bug,可能是很小,很细微的。对此我们可能仅仅需要改动一个返回值就能解决线上bug。但是实际上我们并没有这么一套机制去对线上bug进行热修复,只有通过发版才能解决,这样对用户很不友好。

解决方案
Rollout.io 、 JSpatch、 DynamicCocoa、React Native、 Weex、Wax 、Hybrid
其实业界还是有很多方案的 -_-!

看了一下JSPatch的使用文档,其实就是把JS代码通过Oc的动态运行时,将JS方法调用映射到Oc的对应类和方法。
我们的技术栈储备如下:

<objc/runtime>
<objc/message>
JS

js会写点,ES5就可以。

下面就可以开始。按照JSPatch文档提供的功能,一步一步自己实现对应功能,想一下。以后大家就可以在手机上写代码,很刺激吧~

TTPatch开发问题记录

现在开发成果已经可以热修复,热更新,动态调用Oc方法,参数返回值类型处理,方法hook

对热更新、hook、感兴趣的同学可以下载demo玩玩。后续会跟目前JSPatch支持的功能看齐,但是具体实现是不一样的哦。大家可以对比一下各自实现的优缺点。
我知道肯定是我写的low,算是抛砖引玉吧~,希望大家提问,指正。

Commit问题记录

1.内存问题
解决方式 使用 __unsafe_unretained 修饰临时变量,防止 strong修饰的临时变量在局部方法结束时隐式调用 release,导致出现僵尸对象

2.Oc调用js方法,多参数传递问题
这里面利用arguments和js中的apply,就可以以多参数调用,而不是一个为数组的obj对象

3.关于添加addTarget——action方法
为View对象添加手势响应以及button添加action时,action(sender){sender为当前控制器 self} 为什么Oc中使用的时候sender为当前的手势orbutton对象?
如果Native未实现action方法,那么会导致获取方法签名失败而导致我们无法拿到正确参数,所以获得的参数为当前self.
这里要记录强调一下,如添加不存在的action时,要注意action参数不为当前的事件响应者.

4.JS调用Oc方法,如何支持 多参数、多类型 调用
首先,我们要讲目标Class的forwardingInvocation:方法替换成我们自己的实现TTPatch_Message_handle,
然后通过替换方法的方式,将目标方法的IMP替换为msg__objc_msgForward,直接开始消息住转发,这样直接通过消息转发最终会运行到我们的TTPatch_Message_handle函数中,在函数中我们可以拿到当前正在执行方法的invocation对象,这也就意味着我们可以拿到当前调用方法的全部信息,并且可以操作以及修改。我们也是通过这个方法来实现,返回值类型转换。返回值类型转发这里涉及到

然后通过替换方法的方式,将目标方法的IMP替换为msg__objc_msgForward,直接开始消息住转发,这样直接通过消息转发最终会运行到我们的TTPatch_Message_handle函数中,在函数中我们可以拿到当前正在执行方法的invocation对象,这也就意味着我们可以拿到当前调用方法的全部信息,并且可以操作以及修改。我们也是通过这个方法来实现,返回值类型转换。返回值类型转发这里涉及的细节比较多,暂时只说一下最好的一种解决方案。

《--------------------Github地址----------------》

上传一张Demo动态图


感兴趣的读者可以下载玩一玩.欢迎提出宝贵意见

转自:https://www.jianshu.com/p/1daf20977c4a

收起阅读 »

Android系统开发-选择并启动默认Launcher

如果在Android设备上又安装了一个Launcher应用,当我们返回主页的时候,Android就会弹出一个弹窗,要用户 选择要启动的Launcher应用,如下图所示: 这个是普通Android设备的正常流程,现在我们的需求是不再显示这个提示窗,在设置中增加...
继续阅读 »

如果在Android设备上又安装了一个Launcher应用,当我们返回主页的时候,Android就会弹出一个弹窗,要用户 选择要启动的Launcher应用,如下图所示:



这个是普通Android设备的正常流程,现在我们的需求是不再显示这个提示窗,在设置中增加一个选择默认启动Launcher的页面,默认选择Launcher3。


Settings



在设置中增加一个这样的页面,显示所有声明了"android.intent.category.HOME"的应用


 private fun getAllLauncherApps(): MutableList<AppInfo> {
val list = ArrayList<AppInfo>()
val launchIntent = Intent(Intent.ACTION_MAIN, null)
.addCategory(Intent.CATEGORY_HOME)
val intents = packageManager.queryIntentActivities(launchIntent, 0)

//遍历
for (ri in intents) {
//得到包名
val packageName = ri.activityInfo.applicationInfo.packageName
if (packageName == "com.android.settings") { //不显示原生设置
continue
}
//得到图标
val icon = ri.loadIcon(packageManager)
//得到应用名称
val appName = ri.loadLabel(packageManager).toString()

//封装应用信息对象
val appInfo = AppInfo(icon, appName, packageName)
//添加到list
list.add(appInfo)
}
return list
}
复制代码

使用PackageManager提供的queryIntentActivities方法就可以获取所有Launcher应用,原生设置中也有Activity声明了HOME属性,在这里就把它屏蔽掉。


默认选择Launcher3应用为默认启动


private val DEFAULT_LAUNCHER = "my_default_launcher"
defaultLauncher = Settings.Global.getString(contentResolver, DEFAULT_LAUNCHER)
if (defaultLauncher.isNullOrEmpty()) {
defaultLauncher = "com.android.launcher3"
Settings.Global.putString(contentResolver, DEFAULT_LAUNCHER, defaultLauncher)
}
复制代码

当选择另一个应用,就把选择应用的包名设置到 Settings.Global中。


这样应用选择页面完成,也设置了一个全局的参数提供给系统。


启动


最开始提到了Launcher选择弹窗,我们就考虑在这里做点事,把弹窗的逻辑给跳过,就可以实现默认启动。


弹窗源码位于frameworks/base/core/java/com/android/internal/app/ResolverActivity.java


在这里就不具体分析源码了,就看关键部分


public boolean configureContentView(List<Intent> payloadIntents, Intent[] initialIntents,
List<ResolveInfo> rList, boolean alwaysUseOption) {
// The last argument of createAdapter is whether to do special handling
// of the last used choice to highlight it in the list. We need to always
// turn this off when running under voice interaction, since it results in
// a more complicated UI that the current voice interaction flow is not able
// to handle.
mAdapter = createAdapter(this, payloadIntents, initialIntents, rList,
mLaunchedFromUid, alwaysUseOption && !isVoiceInteraction());

final int layoutId;
if (mAdapter.hasFilteredItem()) {
layoutId = R.layout.resolver_list_with_default;
alwaysUseOption = false;
} else {
layoutId = getLayoutResource();
}
mAlwaysUseOption = alwaysUseOption;

int count = mAdapter.getUnfilteredCount();
if (count == 1 && mAdapter.getOtherProfile() == null) {
// Only one target, so we're a candidate to auto-launch!
final TargetInfo target = mAdapter.targetInfoForPosition(0, false);
if (shouldAutoLaunchSingleChoice(target)) {
safelyStartActivity(target);
mPackageMonitor.unregister();
mRegistered = false;
finish();
return true;
}
}
if (count > 0) {
// add by liuwei,if set my_default_launcher,start default
String defaultlauncher = Settings.Global.getString(this.getContentResolver(), "my_default_launcher");

final TargetInfo defaultTarget = mAdapter.targetInfoForDefault(defaultlauncher);
if(defaultTarget != null){
safelyStartActivity(defaultTarget);
mPackageMonitor.unregister();
mRegistered = false;
finish();
return true;
}
//end
setContentView(layoutId);
mAdapterView = (AbsListView) findViewById(R.id.resolver_list);
onPrepareAdapterView(mAdapterView, mAdapter, alwaysUseOption);
} else {
setContentView(R.layout.resolver_list);

final TextView empty = (TextView) findViewById(R.id.empty);
empty.setVisibility(View.VISIBLE);

mAdapterView = (AbsListView) findViewById(R.id.resolver_list);
mAdapterView.setVisibility(View.GONE);
}
return false;
}
复制代码

在configureContentView中判断launcher应用个数,如果为1,则直接启动,finish当前页面。下面判断count>0,我们就在这里面增加自己的逻辑,获取配置的Settings.Global参数,再去Adapter中判断是否有应用包名和参数匹配,如果有就safelyStartActivity(),关闭弹窗。如果没有匹配包名,就走正常流程,弹窗提示用户。


mAdapter.targetInfoForDefault函数是在 public class ResolveListAdapter extends BaseAdapter中增加函数


 public TargetInfo targetInfoForDefault(String myDefault){
if(myDefault == null){
return null;
}

TargetInfo info = null;
for(int i=0;i<mDisplayList.size();i++){
String disPackageName = mDisplayList.get(i).getResolveInfo().activityInfo.applicationInfo.packageName;
if(myDefault.equals(disPackageName) ){
info = mDisplayList.get(i);
break;
}
}
return info;
}

复制代码

OK,功能实现完成,自测也没有问题。


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

ART虚拟机 | 锁

本文基于Android 11(R) Java中对临界区的锁定通常用synchronize代码块完成,因此标题中的“锁”实际上是对synchronize关键字的剖析。Synchronize代码块使用时必须传入一个对象,这个对象可以是this对象,可以是类对象(e...
继续阅读 »

本文基于Android 11(R)


Java中对临界区的锁定通常用synchronize代码块完成,因此标题中的“锁”实际上是对synchronize关键字的剖析。Synchronize代码块使用时必须传入一个对象,这个对象可以是this对象,可以是类对象(e.g. Foo.class),也可以是任何其他对象。因此我们可以说,锁的状态和对象关联。亦或者,每个对象天生都是一把锁。


Synchronize生成的字节码会对应两条指令,分别是monitor-entermonitor-exit。下面我们针对monitor_enter,分别从解释执行和机器码执行两个方向去寻找这个指令的最终实现。


解释执行


[art/runtime/interpreter/interpreter_switch_impl-inl.h]


HANDLER_ATTRIBUTES bool MONITOR_ENTER() {
...
ObjPtr<mirror::Object> obj = GetVRegReference(A());
if (UNLIKELY(obj == nullptr)) {
...
} else {
DoMonitorEnter<do_assignability_check>(self, &shadow_frame, obj); <===调用
...
}
}
复制代码

[art/runtime/interpreter/interpreter_common.h]


static inline void DoMonitorEnter(Thread* self, ShadowFrame* frame, ObjPtr<mirror::Object> ref)
NO_THREAD_SAFETY_ANALYSIS
REQUIRES(!Roles::uninterruptible_) {
...
StackHandleScope<1> hs(self);
Handle<mirror::Object> h_ref(hs.NewHandle(ref)); <===调用
h_ref->MonitorEnter(self);
...
}
复制代码

[art/runtime/mirror/object-inl.h]


inline ObjPtr<mirror::Object> Object::MonitorEnter(Thread* self) {
return Monitor::MonitorEnter(self, this, /*trylock=*/false); <===调用
}
复制代码

解释执行会使用switch-case方式分别解析每一条指令,由上述代码可知,monitor-enter指令最终会调用Monitor::MonitorEnter静态函数。


机器码执行


[art/runtime/arch/arm64/quick_entrypoints_arm64.S]


ENTRY art_quick_lock_object_no_inline
// This is also the slow path for art_quick_lock_object.
SETUP_SAVE_REFS_ONLY_FRAME // save callee saves in case we block
mov x1, xSELF // pass Thread::Current
bl artLockObjectFromCode // (Object* obj, Thread*) <===调用
...
END art_quick_lock_object_no_inline
复制代码

[art/runtime/entrypoints/quick/quick_lock_entrypoints.cc]


extern "C" int artLockObjectFromCode(mirror::Object* obj, Thread* self){
...
if (UNLIKELY(obj == nullptr)) {
...
} else {
ObjPtr<mirror::Object> object = obj->MonitorEnter(self); // May block <===调用
...
}
}
复制代码

[art/runtime/mirror/object-inl.h]


inline ObjPtr<mirror::Object> Object::MonitorEnter(Thread* self) {
return Monitor::MonitorEnter(self, this, /*trylock=*/false); <===调用
}
复制代码

殊途同归,机器码执行时最终也会调用Monitor::MonitorEnter


锁的两种形态


虚拟机中将锁实现为两种形态,一种称为Thin Lock,另一种称为Fat Lock。


Thin Lock用于竞争较弱的场景。在竞争发生时,采用自旋(spin)和让渡CPU(yield)的方式等待锁,而不是进行系统调用和上下文切换。当持有锁的线程很快完成操作时,短暂的自旋会比上下文切换开销更小。


可是如果自旋一段时间发现还无法获取到锁时,Thin Lock就会膨胀为Fat Lock,一方面增加数据结构存储与锁相关的具体信息,另一方面通过系统调用挂起线程。


总结一下,Fat Lock功能健全,但开销较大。而Thin Lock开销虽小,但无法用于长时间等待的情况。所以实际的做法是先使用Thin Lock,当功能无法满足时再膨胀为Fat Lock。


文章开头提到,每个对象天生都是一把锁。那么这个锁的信息到底存在对象的什么位置呢?


答案是存在art::mirror::Object的对象头中(详见ART虚拟机 | Java对象和类的内存结构)。对象头中有一个4字节长的字段monitor_,其中便存储了锁相关的信息。


monitor字段.png


4字节共32bits,高位的两个bits用于标记状态。不同的状态,存储的信息含义也不同。两个bits共4种状态,分别为ThinOrUnlock(Thin/Unlock共用一个状态),Fat,Hash和ForwardingAddress。ThinOrUnlock和Fat表示锁的状态,Hash是为对象生成HashMap中所用的哈希值,ForwardingAddress是GC时使用的状态。


上图中的m表示mark bit state,r表示read barrier state,都是配合GC使用的标志,在讨论锁的时候可以不关心。


当我们对一个空闲对象进行monitor-enter操作时,锁的状态由Unlock切换到Thin。代码如下。


switch (lock_word.GetState()) {
case LockWord::kUnlocked: {
// No ordering required for preceding lockword read, since we retest.
LockWord thin_locked(LockWord::FromThinLockId(thread_id, 0, lock_word.GCState()));
if (h_obj->CasLockWord(lock_word, thin_locked, CASMode::kWeak, std::memory_order_acquire)) {
...
return h_obj.Get(); // Success!
}
continue; // Go again.
}
复制代码

LockWord对象的大小就是4字节,所以可以将它等同于art::mirror::Objectmonitor_字段,只不过它内部实现了很多方法可以灵活操作4字节中的信息。锁状态切换时,将当前线程的thread id(thread id并非tid,对每个进程而言它都从1开始)存入monitor_字段,与GC相关的mr标志保持不变。


当对象被线程锁定后,假设我们在同线程内对该它再次进行monitor-enter操作,那么就会发生Thin Lock的重入。如果在不同线程对该对象进行monitor-enter操作,那么就会发生Thin Lock的竞争。代码和流程图如下。


case LockWord::kThinLocked: {
uint32_t owner_thread_id = lock_word.ThinLockOwner();
if (owner_thread_id == thread_id) {
uint32_t new_count = lock_word.ThinLockCount() + 1;
if (LIKELY(new_count <= LockWord::kThinLockMaxCount)) {
LockWord thin_locked(LockWord::FromThinLockId(thread_id,
new_count,
lock_word.GCState()));
if (h_obj->CasLockWord(lock_word,
thin_locked,
CASMode::kWeak,
std::memory_order_relaxed)) {
AtraceMonitorLock(self, h_obj.Get(), /* is_wait= */ false);
return h_obj.Get(); // Success!
}
continue; // Go again.
} else {
// We'd overflow the recursion count, so inflate the monitor.
InflateThinLocked(self, h_obj, lock_word, 0);
}
} else {
// Contention.
contention_count++;
Runtime* runtime = Runtime::Current();
if (contention_count <= runtime->GetMaxSpinsBeforeThinLockInflation()) {
sched_yield();
} else {
contention_count = 0;
// No ordering required for initial lockword read. Install rereads it anyway.
InflateThinLocked(self, h_obj, lock_word, 0);
}
}
continue; // Start from the beginning.
}
复制代码

ThinLock.png


在ThinLock膨胀为FatLock前,需要执行50次sched_yieldsched_yield会将当前线程放到CPU调度队列的末尾,这样既不用挂起线程,也不用一直占着CPU。不过android master分支已经将这个流程再度优化了,在50次sched_yield之前,再执行100次自旋操作。和sched_yield相比,自旋不会释放CPU。由于单次sched_yield耗时也有微秒,对于锁持有时间极短的情况,用自旋更省时间。


接下来介绍锁的膨胀过程。


void Monitor::InflateThinLocked(Thread* self, Handle<mirror::Object> obj, LockWord lock_word,
uint32_t hash_code) {
DCHECK_EQ(lock_word.GetState(), LockWord::kThinLocked);
uint32_t owner_thread_id = lock_word.ThinLockOwner();
if (owner_thread_id == self->GetThreadId()) {
// We own the monitor, we can easily inflate it.
Inflate(self, self, obj.Get(), hash_code);
} else {
ThreadList* thread_list = Runtime::Current()->GetThreadList();
// Suspend the owner, inflate. First change to blocked and give up mutator_lock_.
self->SetMonitorEnterObject(obj.Get());
bool timed_out;
Thread* owner;
{
ScopedThreadSuspension sts(self, kWaitingForLockInflation);
owner = thread_list->SuspendThreadByThreadId(owner_thread_id,
SuspendReason::kInternal,
&timed_out);
}
if (owner != nullptr) {
// We succeeded in suspending the thread, check the lock's status didn't change.
lock_word = obj->GetLockWord(true);
if (lock_word.GetState() == LockWord::kThinLocked &&
lock_word.ThinLockOwner() == owner_thread_id) {
// Go ahead and inflate the lock.
Inflate(self, owner, obj.Get(), hash_code);
}
bool resumed = thread_list->Resume(owner, SuspendReason::kInternal);
DCHECK(resumed);
}
self->SetMonitorEnterObject(nullptr);
}
}
复制代码

void Monitor::Inflate(Thread* self, Thread* owner, ObjPtr<mirror::Object> obj, int32_t hash_code) {
DCHECK(self != nullptr);
DCHECK(obj != nullptr);
// Allocate and acquire a new monitor.
Monitor* m = MonitorPool::CreateMonitor(self, owner, obj, hash_code);
DCHECK(m != nullptr);
if (m->Install(self)) {
if (owner != nullptr) {
VLOG(monitor) << "monitor: thread" << owner->GetThreadId()
<< " created monitor " << m << " for object " << obj;
} else {
VLOG(monitor) << "monitor: Inflate with hashcode " << hash_code
<< " created monitor " << m << " for object " << obj;
}
Runtime::Current()->GetMonitorList()->Add(m);
CHECK_EQ(obj->GetLockWord(true).GetState(), LockWord::kFatLocked);
} else {
MonitorPool::ReleaseMonitor(self, m);
}
}
复制代码

膨胀(Inflate)的具体操作比较简单,简言之就是创建一个Monitor对象,存储更多的信息,然后将Monitor Id放入原先的monitor_字段中。


关键的地方在于膨胀的充分条件。如果Thin Lock本来就由本线程持有,那么膨胀不需要经过任何人同意,可以直接进行。但如果该Thin Lock由其他线程持有,那么膨胀之前必须先暂停(这里的暂停并不是指将线程从CPU上调度出去,而是不允许它进入Java世界改变锁状态)持有线程,防止膨胀过程中对锁信息的更新存在竞争。膨胀之后,持有线程恢复运行,此时它看到的Lock已经变成了Fat Lock。


当锁膨胀为Fat Lock后,由于持有锁的动作并未完成,所以该线程会再次尝试。只不过这次走的是Fat Lock分支,执行如下代码。


case LockWord::kFatLocked: {
// We should have done an acquire read of the lockword initially, to ensure
// visibility of the monitor data structure. Use an explicit fence instead.
std::atomic_thread_fence(std::memory_order_acquire);
Monitor* mon = lock_word.FatLockMonitor();
if (trylock) {
return mon->TryLock(self) ? h_obj.Get() : nullptr;
} else {
mon->Lock(self);
DCHECK(mon->monitor_lock_.IsExclusiveHeld(self));
return h_obj.Get(); // Success!
}
}
复制代码

{
ScopedThreadSuspension tsc(self, kBlocked); // Change to blocked and give up mutator_lock_.

// Acquire monitor_lock_ without mutator_lock_, expecting to block this time.
// We already tried spinning above. The shutdown procedure currently assumes we stop
// touching monitors shortly after we suspend, so don't spin again here.
monitor_lock_.ExclusiveLock(self);
}
复制代码

上述代码的ScopedThreadSuspension对象用于完成线程状态的切换,之所以叫scoped,是因为它是通过构造和析构函数完成状态切换和恢复的。在作用域内的局部变量会随着作用域的结束而自动析构,因此花括号结束,线程状态也就由Blocked切换回Runnable了。


最终调用monitor_lock_(Mutex对象)的ExclusiveLock方法。


void Mutex::ExclusiveLock(Thread* self) {
if (!recursive_ || !IsExclusiveHeld(self)) {
#if ART_USE_FUTEXES
bool done = false;
do {
int32_t cur_state = state_and_contenders_.load(std::memory_order_relaxed);
if (LIKELY((cur_state & kHeldMask) == 0) /* lock not held */) {
done = state_and_contenders_.CompareAndSetWeakAcquire(cur_state, cur_state | kHeldMask);
} else {
...
if (!WaitBrieflyFor(&state_and_contenders_, self,
[](int32_t v) { return (v & kHeldMask) == 0; })) {
// Increment contender count. We can't create enough threads for this to overflow.
increment_contenders();
// Make cur_state again reflect the expected value of state_and_contenders.
cur_state += kContenderIncrement;
if (UNLIKELY(should_respond_to_empty_checkpoint_request_)) {
self->CheckEmptyCheckpointFromMutex();
}
do {
if (futex(state_and_contenders_.Address(), FUTEX_WAIT_PRIVATE, cur_state,
nullptr, nullptr, 0) != 0) {
...
cur_state = state_and_contenders_.load(std::memory_order_relaxed);
} while ((cur_state & kHeldMask) != 0);
decrement_contenders();
}
}
} while (!done);
...
exclusive_owner_.store(SafeGetTid(self), std::memory_order_relaxed);
RegisterAsLocked(self);
}
recursion_count_++;
...
}
复制代码

Mutex::ExclusiveLock最终通过futex系统调用陷入内核态,在内核态中将当前线程从CPU中调度出去,实现挂起。值得注意的是,FatLock中依然有spin和yield的操作(WaitBrieflyFor函数),这是因为Thin Lock一旦膨胀为Fat Lock就很难deflate回去,而后续对Fat Lock的使用依然会碰到短时持有锁的情况,这也意味先前的优化此处依然可用。


上面这一块代码算是锁的核心实现,被调用的次数也非常多,因此任何一点微小的优化都很重要。我之前写过一篇文章调试经验 | C++ memory order和一个相关的稳定性问题详细分析了一个由memory order使用错误导致的线程卡死的问题,其中还介绍了C++的memory order,它也正是Java volatile关键字的(ART)底层实现。


此外我还给谷歌提过ExclusiveLock的bug,这个bug既会消耗battery,也会在某些情况下导致系统整体卡死。下面是谷歌的具体回复,感兴趣的可以查看修复


Hans Reply.png


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

Android-Jetpack-Hilt 组件 包爽攻略

Hilt 是啥? Hilt 就是依赖Dagger2 而来的 一个 专属android 端的 依赖注入框架。Dagger2 是啥? Dagger是以前 square 做的 依赖注入框架,但是大量使用了反射,谷歌觉得这东西不错,拿来改了一下,使用编译期注解 大幅度...
继续阅读 »

Hilt 是啥?


Hilt 就是依赖Dagger2 而来的 一个 专属android 端的 依赖注入框架。Dagger2 是啥?
Dagger是以前 square 做的 依赖注入框架,但是大量使用了反射,谷歌觉得这东西不错,拿来改了一下,使用编译期注解 大幅度提高性能以后 的东西就叫Dagger2了, 国外的app 多数都用了Dagger2, 但是这个框架在国内用的人很少。


依赖注入是啥?


说简单一点,如果你构造一个对象所需要的值 是别人给你的,那就叫依赖注入,如果是你自己new出来的,那就不叫依赖注入


class A1 {
public A1(String name) {
this.name = name;
}

private String name;

}

class A2 {
public A2() {
this.name = "wuyue";
}

private String name;

}
复制代码

例如上面的, A1 这个类 构造函数的时候 name的值 是外面传过来的,那这个A1对象的构建过程 就是依赖注入,因为你A1对象 是依赖 外部传递过来的值


再看A2 A2的构造函数 是直接 自己 new出来 赋值的。那自然就不叫依赖注入了。


所以依赖注入 对于大部分人来说 其实每天都在用。


既然每天都在用 那用这些依赖注入的框架有啥用?


这是个好问题, 依赖注入的技术既然每天都在用,为啥我们还要用 这些什么Dagger2 Hilt 之类的依赖注入框架呢? 其实原因就是 你用了这些所谓的依赖注入框架 可以让你少写很多代码,且变的很容易维护


你在构造一个对象的时候 如果是手动new 出来的,那么如果日后这个对象的构造方法发生了改变,那么你所有new
的地方 都要挨个修改,这岂不是很麻烦? 如果有依赖注入框架帮你处理 那你其实只要改一个地方就可以了。


第一个简单的例子


在这个例子中,我们熟悉一下Hilt的基本用法。


首先在root project 中的 dependencies 加入依赖


 classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
复制代码

然后在你的app工程中


apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
复制代码

自定义Application 注意要加注解了。


@HiltAndroidApp
class MyApplication:Application() {

}
复制代码

首先定义一个class


data class Person constructor(val name: String, val age: Int) {
@Inject
constructor() : this("vivo", 18)
}
复制代码

注意 这个class 中 使用了 Inject注解 其实就是告诉 Hilt 如何来提供这个对象


然后写我们的activity 页面


class MainActivity : AppCompatActivity() {

@Inject
lateinit var person: Person

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.v("wuyue", "person:$person")
}
}
复制代码

注意 要加入 @AndroidEntryPoint 这个注解,同样的你 声明person对象的时候也一样 要使用Inject注解。


这就是一个最简单的Hilt的例子


好处是显而易见的, 比方说 以后Person的使用 可以不用那么写了,直接Inject 就可以 我压根不用关心
这个Person对象是怎么被构造出来的,以后构造函数发生了改变 调用的地方 也不用修改代码。


当然了,这里有人会说, 你这我虽然明白了优点,但是实际android编程中 没人这么用呀,


有没有更好的例子呢?


获取 Retrofit/Okhttp 对象


通常来说,我们一个项目里面,总会有网络请求,这些网络请求 都会有一些 基础的Retrofit或者是Okhttp的对象, 我们很多时候都会写成单例 然后去get他们出来, 有没有更简便的写法?
有的


//retrofit的 service
interface BaiduApiService{

}

@Module
@InstallIn(ActivityComponent::class)
object BaiduApiModule{

@Provides
fun provideBaiduService():BaiduApiService{
return Retrofit.Builder().baseUrl("https://baidu.com").build().create(BaiduApiService::class.java)
}
}
复制代码

然后:


@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

@Inject
lateinit var baiduApiService: BaiduApiService

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

}
}

复制代码

即可。仔细体会体会 这种依赖注入框架的 写法 是不是比你之前 单例的写法要简洁方便了很多?


这里解释一下几个注解的含义


@Module 多数场景下 用来提供构造 无法使用@Inject的依赖,啥意思?


第一个例子中 Person 这个class 是我们自己写的吧,构造函数 前面 我们可以加入Inject 注解


但是例如像Retrofit这样的第三方库 ,我们拿不到他们的代码呀, 又想用 Hilt,怎么办呢


自然就是这个Module了,另外用module 的 时候,一般还要配合使用InstallIn
注解,后面跟的参数值 是用来指定module的范围的


可以看下有多少个范围


image.png


最后 就是 @Provides 这个注解, 这个很简单


一般也是用来 和@Module 一起配合的。 你哪个函数 提供了依赖注入 你就在这个函数上加入这个注解就可以了。


多对象 细节不同 怎么处理


举个例子 一个项目里面 可以有多个okhttp的client对吧,有的接口 我们要做一个拦截器 比如说打印一些埋点,
有些接口 我们要做一个拦截器 来判断下登录态是否失效,不一样的场景,我们需要 new不同的okhttp client
那有没有更简便的写法,答案是有的!


@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DataReportsOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class CheckTokenOkHttpClient
复制代码

我们先用Qualifier 限定符 来标记一下


@Module
@InstallIn(ActivityComponent::class)
object OkHttpModule {
@DataReportsOkHttpClient
@Provides
fun provideDataReportInterceptorOkHttpClient(
dataReportInterceptor: DataReportInterceptor
): OkHttpClient {
return OkHttpClient.Builder().addInterceptor(dataReportInterceptor).build()
}

@CheckTokenOkHttpClient
@Provides
fun provideCheckTokenInterceptorOkHttpClient(
checkTokenInterceptor: CheckTokenInterceptor
): OkHttpClient {
return OkHttpClient.Builder().addInterceptor(checkTokenInterceptor).build()
}
}
复制代码

然后这里 provides的方法 注意了 要加上我们前面的我们先用Qualifier 标记, @DataReportsOkHttpClient


但是到这里还没完,这里一定注意一个原则:


使用Hilt的依赖注入组件 他自己的依赖 也必须是Hilt提供的,啥意思?


你看这里 我们2个provide 方法都需要一个参数 这个参数是干嘛的?就是函数参数 是一个okhttp的interceptor
对吧 ,


但是因为我们这里是依赖注入的模块,所以你使用的参数也必须是依赖注入提供的,


所以这里你如果拦截器 这么写:



class DataReportInterceptor : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(chain.request())
}

}
复制代码

那是编译不过的,因为Hilt组件 不知道你这个对象 应该如何去哪里构造,所以这里你必须也指定 这个拦截器的构造 是Hilt 注入的。


所以你只要这么改就可以了:


class DataReportInterceptor  @Inject constructor() : Interceptor {
init {
}

override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(chain.request())
}

}

class CheckTokenInterceptor @Inject constructor() : Interceptor {

init {
}

override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(chain.request())
}

}


复制代码

这样Hilt 就知道要去哪里 取这个依赖了。(这个地方官方文档竟然没有提到,导致很多人照着官方文档写demo 一直报错)


一些小技巧


android中 构造很多对象 都需要Context,Hilt 默认为我们实现了这种Context,不需要我们再费尽心思 构造Context了(实际上你也很难构造处理 因为Context 是无法 new出来的)


class AnalyticsAdapter @Inject constructor(
@ActivityContext private val context: Context,
private val service: AnalyticsService
) { ... }
复制代码

对应的当然还有Application的Context


此外 我们还可以限定 这些依赖注入对象的 作用域


image.png


大家有兴趣可以去官网查看一下。很简单,就不演示了。


其实就是 你对象的作用与 如果是Activity 那么 fragment和view 肯定可以获取到 并且共享他的状态


能理解Activity》Fragment》View 那就很容易理解了。


到底为啥要用Hilt呀


我们学了前面的基础例子以后 一定要把这个问题想明白,否则这个框架你是无法真正理解的,理解他以后 才能用得好。


Hilt要解决的问题就是:


在android开发中,我们太多的场景是干啥?是在Activity里面 构造对象,而这些对象我们是怎么构建出来的?


大部分人都是New出来的对吧,但是这些New出来的对象 所属的Class 一旦发生了构造函数的变更,


我们还得去找出所有 引用这个Class 的 地方 11 去修改 调用方法。 这个就很不方便了。


回顾下前面我们的例子,使用Hilt的话 可以极大避免这种场景。


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

收起阅读 »

iOS多张图片合成一张

在我们的开发过程中,有时候会遇到不同的需求,比如将不同的图片合成一张图片下边是实现代码:#import "RootViewController.h"@interface RootViewController ()@end@implementation Root...
继续阅读 »

在我们的开发过程中,有时候会遇到不同的需求,比如将不同的图片合成一张图片


下边是实现代码:

#import "RootViewController.h"

@interface RootViewController ()

@end

@implementation RootViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}

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

NSArray *imgArray = [[NSArray alloc] initWithObjects:
[UIImage imageNamed:@"1.jpg"],
[UIImage imageNamed:@"2.jpg"],
[UIImage imageNamed:@"3.jpg"],
[UIImage imageNamed:@"4.jpg"],
[UIImage imageNamed:@"5.jpg"],
nil];

NSArray *imgPointArray = [[NSArray alloc] initWithObjects:
@"10", @"10",
@"10", @"25",
@"30", @"15",
@"30", @"50",
@"20", @"80",
nil];


BOOL suc = [self mergedImageOnMainImage:[UIImage imageNamed:@"1.jpg"] WithImageArray:imgArray AndImagePointArray:imgPointArray];

if (suc == YES) {
NSLog(@"Images Successfully Mearged & Saved to Album");
}
else {
NSLog(@"Images not Mearged & not Saved to Album");
}

}
#pragma -mark -functions
//多张图片合成一张
- (BOOL) mergedImageOnMainImage:(UIImage *)mainImg WithImageArray:(NSArray *)imgArray AndImagePointArray:(NSArray *)imgPointArray
{

UIGraphicsBeginImageContext(mainImg.size);

[mainImg drawInRect:CGRectMake(0, 0, mainImg.size.width, mainImg.size.height)];
int i = 0;
for (UIImage *img in imgArray) {
[img drawInRect:CGRectMake([[imgPointArray objectAtIndex:i] floatValue],
[[imgPointArray objectAtIndex:i+1] floatValue],
img.size.width,
img.size.height)];

i+=2;
}

CGImageRef NewMergeImg = CGImageCreateWithImageInRect(UIGraphicsGetImageFromCurrentImageContext().CGImage,
CGRectMake(0, 0, mainImg.size.width, mainImg.size.height));

UIGraphicsEndImageContext();
if (NewMergeImg == nil) {
return NO;
}
else {
UIImageWriteToSavedPhotosAlbum([UIImage imageWithCGImage:NewMergeImg], self, nil, nil);
return YES;
}
}



- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

@end

转自:https://www.cnblogs.com/gchlcc/p/6774420.html

收起阅读 »

Kotlin实战---使用Room封装本地数据层

没有Kotlin基础的小伙伴先进这里→ Koltin基础文章 Kotlin网络模型的实现→ Kotlin网络模型的实现 kotlin实战---MVP模式实现登录,实现Base层封装→ kotlin实战---MVP模式实现登录,实现Base层封装 1、为什么使用...
继续阅读 »

没有Kotlin基础的小伙伴先进这里→
Koltin基础文章


Kotlin网络模型的实现→
Kotlin网络模型的实现


kotlin实战---MVP模式实现登录,实现Base层封装→
kotlin实战---MVP模式实现登录,实现Base层封装


1、为什么使用Room


Room 是一个 SQLite 对象映射库。它可用来避免样板代码,还可以轻松地将 SQLite 表数据转换为 Java 对象。Room 提供 SQLite 语句的编译时检查,并且可以返回 RxJava、Flowable 和 LiveData 可观察对象,使用ROOM可以让你更简单,更流畅的操作数据库,使用简单通过注解的方式就能对数据库进行增删改查,Google工程师帮你封装了访问SqlLite的代码,使你的代码性能更高


2、数据库的封装


先来一个图,理清思路再看代码


在这里插入图片描述


2.1、LocalRoomRequestManager


接口层实现,类似于网络模块里的API,将操作SqlLite的接口写到这里边


/***
* 数据库获取标准接口,数据库读取
* 只为 LocalRoomRequestManager 服务
* DB 数据
*/
interface IDatabaseRequest {
fun insertStudents(vararg students: Student)

fun updateStudents(vararg students: Student)

fun deleteStudents(vararg students: Student)

fun deleteAllStudent()

fun queryAllStudent() : List<Student> ?

// TODO 可扩展 ...
}

/**
* 为了扩展,这样写(在仓库里面的)
* 本地获取标准接口(在仓库里面) 也就是本地的数据读取(包括本地xml数据,等)
* 只为 LocalRoomRequestManager 服务
*
* xml 数据 本地数据
*/
interface ILocalRequest {
}
复制代码

LocalRoomRequestManager类的实现,初始化的通过dataBase层获取dao,然后通过dao层进行增删改查


class LocalRoomRequestManager :ILocalRequest,IDatabaseRequest{
var studentDao:StudentDao?=null
//相当于Java代码的构造代码块
init{
val studentDatabase=StudentDatabase.getDataBase()
studentDao=studentDatabase?.getStudentDao()
}
companion object{
var INSTANCE: LocalRoomRequestManager? = null

fun getInstance() : LocalRoomRequestManager {
if (INSTANCE == null) {
synchronized(LocalRoomRequestManager::class) {
if (INSTANCE == null) {
INSTANCE = LocalRoomRequestManager()
}
}
}
return INSTANCE!!
}
}
override fun updateStudents(vararg students: Student) {
studentDao?.updateStudents(*students)
}

override fun deleteStudents(vararg students: Student) {
studentDao?.deleteStudent(*students)
}

override fun deleteAllStudent() {
studentDao?.deleteAllStudent()
}

override fun queryAllStudent(): List<Student>? {
return studentDao?.queryAllStudents()
}

override fun insertStudents(vararg students: Student) {
studentDao?.insertStudents(*students)
}

}
复制代码

2.2、Room操作


真正用来操作数据库的代码


初始化数据库


@Database(entities = [Student::class],version = 1)
abstract class StudentDatabase: RoomDatabase() {
abstract fun getStudentDao():StudentDao

companion object{
private var INSTANCE:StudentDatabase?=null
//Application 调用
fun getDatabase(context: Context):StudentDatabase?{
if(INSTANCE==null){
INSTANCE=Room.databaseBuilder(context,StudentDatabase::class.java,"student_database.db")
.allowMainThreadQueries()//允许在主线程查询
.build()
}
return INSTANCE
}
//使用者调用
fun getDataBase():StudentDatabase?= INSTANCE
}

}
复制代码

在Application里去初始化database


class MyApplication : Application() {

override fun onCreate() {
super.onCreate()

// 初始化
StudentDatabase.getDatabase(this)
}

}
复制代码

Room.databaseBuilde 就是实例化的DataBase的实现类
实现类里的代码:
在这里插入图片描述
这些都是框架生成的代码,省去了我们许多的样板代码
Dao层和Entity实现


@Dao
interface StudentDao {
/***
* 可变参数,插入数据
*/
@Insert
fun insertStudents(vararg students:Student)
//更新数据
@Update
fun updateStudents(vararg students:Student)

//根据条件删除
@Delete
fun deleteStudent(vararg students:Student)
//删除全部
@Query("delete from student")
fun deleteAllStudent()
//查询全部
@Query("SELECT * FROM student ORDER BY ID DESC")
fun queryAllStudents():List<Student>

}

@Entity
class Student(){
@PrimaryKey(autoGenerate = true)//设置为主键,自动增长
var id:Int=0
@ColumnInfo(name="name")//别名 数据库中的名字如果不设置,默认是属性名称
lateinit var name:String
@ColumnInfo(name ="phoneNumber")
lateinit var phoneNumber:String
//次构造
constructor(name:String,phoneNumber:String): this(){
this.name=name
this.phoneNumber=phoneNumber
}
}
复制代码

框架生成的代码,大家可以自己去看一下,里面自动添加了事务,也加了锁,非常的nice
写完这些再去用MVP把LocalRoomRequestManager和Model层连起来,MVP上一篇贴的很详细了,这次的就不贴了
Kotlin版的适配器写法


class CollectAdapter :RecyclerView.Adapter<CollectAdapter.MyViewHolder>() {
// 接收 数据库的数据
var allStudents: List<Student> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val layoutInflater: LayoutInflater = LayoutInflater.from(parent.context)
val itemView: View = layoutInflater.inflate(R.layout.item_collect_list, parent, false)
return MyViewHolder(itemView)
}

override fun getItemCount(): Int =allStudents.size

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val student: Student = allStudents[position]
holder.tvID.text = "${position + 1}"
holder.tvName.text = student.name
holder.tvPhoneNumber.text = "${student.phoneNumber}"
}

inner class MyViewHolder(itemView: View):RecyclerView.ViewHolder(itemView){
var tvID: TextView = itemView.findViewById(R.id.tv_id)
var tvName: TextView = itemView.findViewById(R.id.tv_name)
var tvPhoneNumber: TextView = itemView.findViewById(R.id.tv_phoneNumber)
}

}
复制代码

最终的效果


在这里插入图片描述


3、总结


需要注意的点在可变参数的传递过程中,不能将参数直接丢给方法得加一个*
LocalRoomRequestManager.getInstance().insertStudents(*students)
体验用Kotlin开发项目的感觉,感觉比Java好用很多,还是很nice的,作为官方直推的语言还是挺值得学习的,


作者:被遗忘的凉白开
链接:https://juejin.cn/post/6955767367192281124
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

你的Android库是否还在Application中初始化?

通常来说,当我们引入一个第三方库,第一件要做的事情是在Application中的onCreate传入context初始化这个库 😞。但是为什么像一些库如Firebase🔥,初始化的时候并不需要在Application中初始化呢?今天我们就来探索一下这个问题 🧐...
继续阅读 »

通常来说,当我们引入一个第三方库,第一件要做的事情是在Application中的onCreate传入context初始化这个库 😞。但是为什么像一些库如Firebase🔥,初始化的时候并不需要在Application中初始化呢?今天我们就来探索一下这个问题 🧐


Android库的初始化


举个栗子,我们需要在app中国呢使用ARouter,在使用前需要初始化传入context,因此如果没有application时我们要创建一个:


class MainApplication : Application() {
override fun onCreate(){
super.onCreate()
ARouter.init(mApplication);
}
}
复制代码

然后要在清单文件 AndroidManifest.xml 中声明才会执行 :


<application
android:name=".MainApplication"
...
复制代码

更多库怎么办


现在想象我们使用了ARouter,友盟统计,Realm,ToastUtils等库时,我们的application可能会是如下形式:


class MainApplication : Application() {
override fun onCreate(){
super.onCreate()
ARouter.init(this)
UMConfigure.init(this,...)
Realm.init(this)
ToastUtils.init(this)
}
}
复制代码

在项目中, 仅仅为了初始化一些库,我就必须得新建Application并且在onCreate中调用它。(译者:也许你认为这也没什么,但是如果你自己创建了多个库需要context时,你每次得预留一个init方法暴露给调用者,使用时又得在application初始化。)



Useless


无需“初始化”的库


如果你的项目加入了Firebase 🔥, 你会发现它并没有要求初始化, 你只要使用它 :


这个数据库访问没有需要context的传入,通过离线访问存储本地。可以猜测它有一个机制获取上下文application context,自动完成初始化。


ContentProvider & Manifest-Merger


developer.android.com/studio/buil…



你的Apk文件只包含一个清单文件AndroidManifest.xml,但是你的Android Studio项目可能会有多个源集(main source set),构建变体(build variants),导入的库(imported libraries)构成。因此在编译构建app时,gradle插件会将多个manifest文件合并到一个清单文件中去。



我们可以看下合并后的清单文件(目录如下):



app/build/intermediates/merged_manifests/MY_APP/processMY_APPDebugManifest/merged/AndroidManifest.xml



我们可以发现一个关于Firebase库的provider被引入到清单文件中:


使用Android Studio点击打开FirebaseInitProvider, 我们知道了这个provider通过this.getContext()来访问上下文。

内容提供者ContentsProviders会直接在Application创建后完成初始化,因此通过它来完成library的初始化不失为一个好办法。


自动初始化我们的库


如果我们自定义了ToastUtils库需要初始化,我们自己提供一个Provider :


class ToastInitProvider : ContentProvider() {

override fun onCreate(): Boolean {
ToastUtils.init(context)
return true
}
...
}
复制代码

然后这个库中的AndroidManifest.xml中加入它


<provider
android:name=".ToastInitProvider"
android:authorities="xxx.xx.ToastInitProvider" />
复制代码

然后当我们使用这个ToastUtils库时,无需添加额外的代码在项目中初始化它😎,直接使用它即可:


ToastUtils.show("this is toast")
复制代码
Stetho.getInstance().configure(…)
复制代码

删除Application


如果一些库没有使用InitProviders,我们可以创建它:


class ARouterInitProvider : ContentProvider() {

override fun onCreate(): Boolean {
ARouter.init(this)
return true
}
...
}
class RealmInitProvider : ContentProvider() {

override fun onCreate(): Boolean {
Realm.init(this)
return true
}
...
}
复制代码

然后加入到清单文件AndroidManifest中 :


<provider
android:name=".ARouterInitProvider"
android:authorities="${applicationId}.ARouterInitProvider" />
<provider
android:name=".RealmInitProvider"
android:authorities="${applicationId}.RealmInitProvider" />
复制代码

现在我们可以 移除 这个 MainApplication


Happy Dance


项目地址


https://github.com/florent37/ApplicationProvider

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

引入Jetpack架构后,你的App会发生哪些变化?

前言 上篇文章我给大家分享了我对Android架构的理解,从思想层面去讲述架构的演进过程。很多小伙伴读完后拍手叫好,表示还想听我讲一下对Jetpack 架构的看法,本着帮人帮到底的精神,今天我将再次动笔 尽量从本质上讲清楚Jetpack 架构存在的意义,以及解...
继续阅读 »

前言


上篇文章我给大家分享了我对Android架构的理解,从思想层面去讲述架构的演进过程。很多小伙伴读完后拍手叫好,表示还想听我讲一下对Jetpack 架构的看法,本着帮人帮到底的精神,今天我将再次动笔 尽量从本质上讲清楚Jetpack 架构存在的意义,以及解决的问题。


同时我也有一个基于Jetpack MVVM的完整开源项目,已经按照上篇文章提出的思想做了重构,目前托管在Github,希望也能为你提供一些帮助。github地址


知识储备:需要对Lifcycle、LiveData、ViewModel、DataBinding有基本了解


目录



  • 1. 有了Lifecycle,再也不用担心生命周期同步问题

    • 1.1 为什么要做生命周期绑定?

    • 1.2 Lifecycle解决了哪些问题?



  • 2. LiveData并不是只运用观察者模式

    • 2.1 观察者模式的优点有哪些?

    • 2.2 LiveData基于观察者模式又做了哪些扩展?

    • 2.3 LiveData + Lifecycle 实现 1 + 1 > 2



  • 3. ViewModel与LiveData真乃天作之合

    • 3.1 如何优雅的实现Fragment之间通讯?

    • 3.2 由ViewModel担任 VM/Presenter 的好处有哪些?



  • 4. 解除你对DataBinding的误解

    • 4.1 使用DataBinding的好处有哪些?

    • 4.2 为什么很多人说DataBinding很难调试?



  • 5. Jetpack和MVVM有什么关系?

    • 5.1 什么是MVVM

    • 5.2 Jetpack只是让MVVM更简单、更安全




1. 有了Lifecycle,再也不用担心生命周期同步问题


1.1 为什么要做生命周期绑定?


关于Activity/Fragment其最重要的概念就是生命周期管理,我们开发者需要在不同生命周期回调中做不同事情。比如onCreate做一些初始化操作,onResume做一些恢复操作等等等等,以上这些操作都比较单一直接去写也没有多大问题。


但有一些组件需要强依赖于Activity/Fragment生命周期,常规写法一旦疏忽便会引发安全问题,比如下面这个案例:


现有一个视频播放界面,我们需要做到当跳到另一个界面就暂停播放,返回后再继续播放,退出后重置播放,常规思路:


#class PlayerActivity
onCreate(){
player.init()
}
onResume(){
player.resume()
}
onPause(){
player.pause()
}
onDestroy(){
player.release()
}
复制代码

读过我上篇文章的小伙伴可能一眼就能看出来这违背了控制反转,人不是机器很容易写错或者忘写,特别是player.release()如果忘写便会引发内存泄漏
此时我们可以基于控制反转思想(将player生命周期控制权交给不会出错的框架)进行改造:
第一步:


interface ObserverLifecycle{
onCreate()
...
onDestroy()
}
复制代码

首先定义一个观察者接口,包含Activity/Fragment主要生命周期方法


第二步:


class BaseActivity{
val observers = mutableList<ObserverLifecycle>()
onCreate(){
observers.forEach{
observer.onCreate()
}
}
...
onDestroy(){
observers.forEach{
observer.onDestroy()
}
}
}
复制代码

BaseActivity中观察生命周期并逐一通知到observers的观察者


第三步:


class VideoPlayer : ObserverLifecycle{
onCreate(){
init()
}
...
onDestroy(){
release()
}
}
class PlayerActivity : BaseActivity{
observers.add(videoPlayer)
}
复制代码

播放器实现ObserverLifecycle接口,并在每个时机调用相应方法。PlayerActivity只需将videoPlayer注册到observers即可实现生命周期同步。


其实不光videoPlayer,任何需要依赖Activity生命周期的组件 只需实现ObserverLifecycle接口最后注册到Activityobservers即可实现生命周期自动化管理,进而可以规避误操作带来的风险


1.2 Lifecycle解决了哪些问题?


既然生命周期的同步如此重要,Google肯定不会视而不见,虽然自定义ObserverLifecycle可以解决这种问题,但并不是每个人都能想到。所以Google就制定了一个标准化的生命周期管理工具Lifecycle,让开发者碰到生命周期问题自然而然的想到Lifecycle,就如同想在Android手机上新建一个界面就会想到Activity一样。


同时ActivityFragment内部均内置了Lifecycle,使用非常简单,以1.1 案例通过Lifecycle改造后如下:


class VideoPlayer : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate(){
init()
}
..
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy(){
release()
}
}
class PlayerActivity : BaseActivity{
lifecycle.addObserver(videoPlayer)
}
复制代码

两步操作即可,不用我们自己向观察者(videoPlayer)做生命周期分发处理。


2. LiveData并不是只运用观察者模式


2.1 观察者模式的优点有哪些?


观察者是一种常见并且非常实用的一种行为型模式,具有扩展性强、耦合性低的特性。


本文1.1 中 生命周期同步设计就是一个标准的观察者模式,ObserverLifecycle可作为观察者,PlayerActivity作为被观察者,当被观察者(PlayerActivity)生命周期发生改变时会主动通知到观察者(VideoPlayer)


同时观察者在不改变代码结构的情况随意扩展,比如PlayerActivity属于一个MVP架构,此时可以将Presenter实现ObserverLifecycle作为观察者 随后 注册到被观察者(PlayerActivity)中,
这样Presenter也可以监测到Activity生命周期,并且代码结构没有任何改变,符合开闭原则(对扩展开发 修改关闭)


2.2 LiveData基于观察者模式又做了哪些扩展?


LiveData符合标准的观察者模式,所以它具备扩展性强、耦合性低的特性,同样它还是一个存储数据的容器,当容器数据改变时会触发观察者,即数据驱动。


数据驱动是前端开发领域非常重要的一个概念,说数据驱动之前我们先思考一个问题,为什么要改变数据?
答案显而易见,无非是想让数据使用者感知到而已,而LiveData可以优雅的实现这一流程,将 改变、通知 两步操作合并为一步 即省事也提高了安全性.


根据LiveData的特性决定它非常适合去做数据驱动UI,下面举个例子简单描述下:


# 需求:改变textView内容以及对应的数据,用LiveData实现方式如下
val liveData = MutableLiveData<String>()
liveData?.observe(this, Observer { value->
textView.text = value
})
//这一步会改变liveData值并且会触发textView重新渲染
liveData.value = "android"
复制代码

看起来平平无奇甚至理所当然,但它确实解决了我们前端开发的痛点,在此之前数据和UI都需要我们开发者单独修改,当面对十几个View时很难做到不漏不忘。
引入liveData后改变数据会自动触发UI渲染,将两步操作合并为一步,大大降低出错的概率
关于数据驱动UI上篇文章我已经做了详细描述,感兴趣的可以翻回去查看。


2.3 LiveData + Lifecycle 实现 1 + 1 > 2


LiveDataLifecycle的加持下可以实现只在可见状态接收通知,说的通俗一点Activity执行了onStop()后内部的LiveData就无法收到通知,这样设计有什么好处?
举个例子:
ActivityAActivityB共享同一个LiveData,伪代码如下


class ActivityA{
liveData?.observe(this, Observer { value->
textView.text = value
})
}
class ActivityB{
liveData?.observe(this, Observer { value->
textView.text = value
})
}
复制代码

ActivityA启动ActivityB后多次改变liveData值,等回到ActivityA时 你肯定不希望Observer收到多次通知而引发textView多次重绘。


引入Lifecycle后这个问题便可迎刃而解,liveData绑定Lifecycle(例子中的this)后,当回到ActivityA时只会取liveData最新的值然后做通知,从而避免多余的操作引发的性能问题


3. ViewModel与LiveData真乃天作之合


3.1 Jetpack ViewModel 并不等价于 MVVM ViewModel


经常有小伙伴将Jetpack ViewModelMVVM ViewModel,其实这二者根本没有在同一个层次,MVVM ViewModelMVVM架构中的一个角色,看不见摸不着只是一种思想。
Jetpack ViewModel是一个实实在在的框架用于做状态托管,有对应的作用域可跟随Activity/Fragment生命周期,但这种特性恰好可以充当MVVM ViewModel的角色,分隔数据层和视图层并做数据托管。


所以结论是Jetpack ViewModel可以充当MVVM ViewModel 但二者并不等价


3.2 如何优雅的实现Fragment之间通讯?


ViewModel官方定义是一个带作用域的状态托管框架,为了将其状态托管发挥到极致,Google甚至单独为ViewModel开了个后门,Activity横竖屏切换时不会销毁对应的ViewModel,为的就是横竖屏能共用同一个ViewModel,从而保证数据的一致性。


既然是状态托管框架那ViewModel的第一要务 就要时时刻刻保证最新状态分发到视图层,这让我不禁想到了LiveData,数据的承载以及分发交给Livedata,而ViewModel专注于托管LiveData保证不丢失,二者搭配简直是天作之合。


有了ViewModelLiveDataFragment之间可以更优雅的通讯。比如我的开源项目中的音乐播放器(属于单Activity多Fragment架构下),播放页和首页悬浮都包含音乐基本自信,如下图所示:


image.png
想要使两个Fragment中播放信息实时同步,最优雅的方式是将播放状态托管在Activity作用域下ViewModelLiveData中,然后各自做状态监听,这样只有要有一方改变就能立即通知到另一方,简单又安全,具体细节可至我的开源项目中查看。


3.3 由ViewModel担任 VM/Presenter 的好处有哪些?


传统MVVMMVP遇到最多的的问题无非就是多线程下的内存泄露,ViewModel可以完全规避这个问题,内部的viewModelScope是一个协程的扩展函数,viewModelScope生命周期跟随ViewModel对应的Lifecycle(Activity/Fragment),当页面销毁时会一并结束viewModelScope协程作用域,所以将耗时操作直接放在viewModelScope即刻


另外在界面销毁时会调用ViewModelonClear方法,可以在该方法做一些释放资源的操作,进一步降低内存泄露的风险


4. 解除你对DataBinding的误解


4.1 使用DataBinding的作用有哪些?


DataBinding最大的优点跟唯一的作用就是数据 UI双向绑定UI和数据修改任何一方另外一方都会自动同步,这样的好处其实跟LiveData的类似,都是做数据跟UI同步操作,用来保证数据和UI一致性。其实写到这可以发现,不管是LiveDataDataBinding还是DiffUtil都是用来解决数据和UI一致性问题,可见Google对这方面有多么重视,所以我们一定要紧跟官方步伐


小知识点:



DataBinding包中的ObservebleFile作用跟LiveData基本一致,但ObservebleFile有一个去重的效果,



4.2 为什么很多人说DataBinding很难调试?


经常听一些小伙伴提DataBinding不好用,原因是要在xml中写业务逻辑不好调试,对于这个观点我是持否定态度的。并不是我同意xml中写业务逻辑这一观点,我觉得碰到问题就得去解决问题,如果解决问题的路上有障碍就尽量扫清障碍,而不是一味的逃避。


{vm.isShow ? View.VISIBLE : View.GONE}之类的业务逻辑不写在xml放在哪好呢?关于这个问题我在上篇文章Data Mapper章节中描述的很清楚,拿到后端数据转换成本地模型(此过程会编写所有数据相关逻辑),本地模型与设计图一一对应,不但可以将视图与后段隔离,而且可以解决xml中编写业务逻辑的问题。


5. Jetpack和MVVM有什么关系?


5.1 什么是MVVM


MVVM其实是前端领域一个专注于界面开发的架构模式,总共分为ViewViewModelRepository三个模块 (需严格按照单一设计原则划分)




  • View(视图层): 专门做视图渲染以及UI逻辑的处理

  • Repository(远程): 代表远程仓库,从Repository取需要的数据

  • ViewModel: Repository取出的数据需暂存到ViewModel,同时将数据映射到视图层



分层固然重要,但MVVM最核心点是通过ViewModel做数据驱动UI以及双向绑定的操作用来解决数据/UI的一致性问题。MVVM就这么些东西,千万不要把它理解的特别复杂


其实我上篇文章也简单说过,好的架构不应该局限到某一种模式(MVC/MVP/MVVM)上,需要根据自己项目的实际情况不断添砖加瓦。如果你们的后端比较善变我建议引入Data Mapper的概念~如果你经常和同事开发同一个界面,可以试图将每一条业务逻辑封装到use case中,这样大概率可以解决Git冲突的问题..等等等等,总之只要能实实在在 提高 开发效率以及项目稳定性的架构就是好架构.


5.2 Jetpack只是让MVVM更简单、更安全


Jetpack是Android官方为确立标准化开发而提供的一套框架,Lifecycle可以让开发者不用过多考虑 生命周期引发的一系列问题 ~ 有了DataBinding的支持让数据UI双向绑定成为了可能 ~ LiveData的存在解除ViewModelActivity双向依赖的问题....


归根到底Jetpack就是一套开发框架,MVVM在这套框架的加持之下变得更加简单、安全。


Tips:作者公司项目引入Jetpack后,项目稳定性有着肉眼可见的提升。


综上所述



  • Lifecycle 解决了生命周期 同步问题

  • LiveData 实现了真正的状态驱动

  • ViewModel 可以让 Fragment 通讯变得更优雅

  • DataBinding 让双向绑定成为了可能

  • Jetpack 只是让 MVVM 更简单、更安全

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

Android系统开发-选择并启动默认Launcher

如果在Android设备上又安装了一个Launcher应用,当我们返回主页的时候,Android就会弹出一个弹窗,要用户 选择要启动的Launcher应用,如下图所示: 这个是普通Android设备的正常流程,现在我们的需求是不再显示这个提示窗,在设置中增加...
继续阅读 »

如果在Android设备上又安装了一个Launcher应用,当我们返回主页的时候,Android就会弹出一个弹窗,要用户 选择要启动的Launcher应用,如下图所示:



这个是普通Android设备的正常流程,现在我们的需求是不再显示这个提示窗,在设置中增加一个选择默认启动Launcher的页面,默认选择Launcher3。


Settings



在设置中增加一个这样的页面,显示所有声明了"android.intent.category.HOME"的应用


 private fun getAllLauncherApps(): MutableList<AppInfo> {
val list = ArrayList<AppInfo>()
val launchIntent = Intent(Intent.ACTION_MAIN, null)
.addCategory(Intent.CATEGORY_HOME)
val intents = packageManager.queryIntentActivities(launchIntent, 0)

//遍历
for (ri in intents) {
//得到包名
val packageName = ri.activityInfo.applicationInfo.packageName
if (packageName == "com.android.settings") { //不显示原生设置
continue
}
//得到图标
val icon = ri.loadIcon(packageManager)
//得到应用名称
val appName = ri.loadLabel(packageManager).toString()

//封装应用信息对象
val appInfo = AppInfo(icon, appName, packageName)
//添加到list
list.add(appInfo)
}
return list
}
复制代码

使用PackageManager提供的queryIntentActivities方法就可以获取所有Launcher应用,原生设置中也有Activity声明了HOME属性,在这里就把它屏蔽掉。


默认选择Launcher3应用为默认启动


private val DEFAULT_LAUNCHER = "my_default_launcher"
defaultLauncher = Settings.Global.getString(contentResolver, DEFAULT_LAUNCHER)
if (defaultLauncher.isNullOrEmpty()) {
defaultLauncher = "com.android.launcher3"
Settings.Global.putString(contentResolver, DEFAULT_LAUNCHER, defaultLauncher)
}
复制代码

当选择另一个应用,就把选择应用的包名设置到 Settings.Global中。


这样应用选择页面完成,也设置了一个全局的参数提供给系统。


启动


最开始提到了Launcher选择弹窗,我们就考虑在这里做点事,把弹窗的逻辑给跳过,就可以实现默认启动。


弹窗源码位于frameworks/base/core/java/com/android/internal/app/ResolverActivity.java


在这里就不具体分析源码了,就看关键部分


public boolean configureContentView(List<Intent> payloadIntents, Intent[] initialIntents,
List<ResolveInfo> rList, boolean alwaysUseOption) {
// The last argument of createAdapter is whether to do special handling
// of the last used choice to highlight it in the list. We need to always
// turn this off when running under voice interaction, since it results in
// a more complicated UI that the current voice interaction flow is not able
// to handle.
mAdapter = createAdapter(this, payloadIntents, initialIntents, rList,
mLaunchedFromUid, alwaysUseOption && !isVoiceInteraction());

final int layoutId;
if (mAdapter.hasFilteredItem()) {
layoutId = R.layout.resolver_list_with_default;
alwaysUseOption = false;
} else {
layoutId = getLayoutResource();
}
mAlwaysUseOption = alwaysUseOption;

int count = mAdapter.getUnfilteredCount();
if (count == 1 && mAdapter.getOtherProfile() == null) {
// Only one target, so we're a candidate to auto-launch!
final TargetInfo target = mAdapter.targetInfoForPosition(0, false);
if (shouldAutoLaunchSingleChoice(target)) {
safelyStartActivity(target);
mPackageMonitor.unregister();
mRegistered = false;
finish();
return true;
}
}
if (count > 0) {
// add by liuwei,if set my_default_launcher,start default
String defaultlauncher = Settings.Global.getString(this.getContentResolver(), "my_default_launcher");

final TargetInfo defaultTarget = mAdapter.targetInfoForDefault(defaultlauncher);
if(defaultTarget != null){
safelyStartActivity(defaultTarget);
mPackageMonitor.unregister();
mRegistered = false;
finish();
return true;
}
//end
setContentView(layoutId);
mAdapterView = (AbsListView) findViewById(R.id.resolver_list);
onPrepareAdapterView(mAdapterView, mAdapter, alwaysUseOption);
} else {
setContentView(R.layout.resolver_list);

final TextView empty = (TextView) findViewById(R.id.empty);
empty.setVisibility(View.VISIBLE);

mAdapterView = (AbsListView) findViewById(R.id.resolver_list);
mAdapterView.setVisibility(View.GONE);
}
return false;
}
复制代码

在configureContentView中判断launcher应用个数,如果为1,则直接启动,finish当前页面。下面判断count>0,我们就在这里面增加自己的逻辑,获取配置的Settings.Global参数,再去Adapter中判断是否有应用包名和参数匹配,如果有就safelyStartActivity(),关闭弹窗。如果没有匹配包名,就走正常流程,弹窗提示用户。


mAdapter.targetInfoForDefault函数是在 public class ResolveListAdapter extends BaseAdapter中增加函数


 public TargetInfo targetInfoForDefault(String myDefault){
if(myDefault == null){
return null;
}

TargetInfo info = null;
for(int i=0;i<mDisplayList.size();i++){
String disPackageName = mDisplayList.get(i).getResolveInfo().activityInfo.applicationInfo.packageName;
if(myDefault.equals(disPackageName) ){
info = mDisplayList.get(i);
break;
}
}
return info;
}

复制代码

OK,功能实现完成,自测也没有问题。


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

在 Kotlin 序列化中使用 DataStore

我们之前已经 数据类 非常适合与 DataStore 结合使用,这是因为它们能够与 Kotlin 序列化无缝协作。DataStore 会依赖数据类自动生成的 equals 和 hashCode。数据类也会生成便于调试和更新数据的 toString 和 copy...
继续阅读 »

我们之前已经 分享Proto DataStore 和 Preferences DataStore 的使用方法。这两个 DataStore 版本都会在后台使用 Protos 对数据进行序列化。您也可以使用 Kotlin 序列化,结合使用 DataStore 与自定义数据类。这有助于减少样板代码,且无需学习或依赖于 Protobuf 库,同时仍可以为数据提供架构。


您需要完成以下几项操作:



  • 定义数据类

  • 确保您的数据类不可变

  • 使用 Kotlin 序列化实现 DataStore 序列化器

  • 开始使用


定义数据类


Kotlin 数据类 非常适合与 DataStore 结合使用,这是因为它们能够与 Kotlin 序列化无缝协作。DataStore 会依赖数据类自动生成的 equalshashCode。数据类也会生成便于调试和更新数据的 toStringcopy 函数。


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

data class UserPreferences(
val showCompleted: Boolean,
val sortOrder: SortOrder
)
复制代码

确保您的数据类不可变


确保您的数据类不可变是非常重要的,这是因为 DataStore 无法兼容可变类型。结合使用可变类型与 DataStore 会导致难以捕获的错误和竞争条件。数据类并非一定不可变。


Vars 是可变的,所以您应使用 vals 代替:


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

data class MyData(
- var num: Int
+ val num: Int
)
- myObj.num = 5 // Fails to compile when num is val
+ val newObj = myObj.copy(num = 5)
复制代码

数组是可变的,所以您不应将其公开。


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

data class MyData(
- var num: IntArray
)
- myObj.num = 5 // This would mutate your object
复制代码

即使将只读列表用作数据类的一部分,该数据类也仍为可变的。您应考虑改用 不可变/持久化集合:


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

data class MyData(
- val nums: List<Int>
+ val nums: PersistentList<Int>
)

- val myInts = mutableListOf(1, 2, 3, 4)
- val myObj = MyData(myInts)
- myInts.add(5) // Fails to compile with PersistentList, but mutates with List
+ val newData = myObj.copy(
+ nums = myObj.nums.mutate { it += 5 } // Mutate returns a new PersistentList
+ )
复制代码

将可变类型用作数据类的一部分会令数据类变为可变状态。您不应采取上述做法,反而要确保所有内容都是不可变类型。


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

data class MyData(
- val mutableType: MutableType
)

- val myType = MutableType()
- val myObj = MyData(myType)
- myType.mutate()
复制代码

实现 DataStore 序列化器


Kotlin 序列化支持包括 JSON 和协议缓冲区在内的 多种格式。我将在此处使用 JSON,因为它十分常见、易于使用且会以明文形式进行存储,便于调试。Protobuf 也是一个不错的选择,因为它规模更小、速度更快且兼容 protobuf-lite


要使用 Kotlin 序列化读取数据类并将其写入 JSON,您需要使用 @Serializable 注释数据类并使用 Json.decodeFromString<YourType>(string)Json.encodeToString(data)。以下是带有 UserPreferences 的示例:


 /* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

@Serializable
data class UserPreferences(
val showCompleted: Boolean = false,
val sortOrder: SortOrder = SortOrder.None
)

object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue = UserPreferences()

override suspend fun readFrom(input: InputStream): UserPreferences {
try {
return Json.decodeFromString(
UserPreferences.serializer(), input.readBytes().decodeToString())
} catch (serialization: SerializationException) {
throw CorruptionException("Unable to read UserPrefs", serialization)
}
}

override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
output.write(Json.encodeToString(UserPreferences.serializer(), t).encodeToByteArray())
}
}
复制代码

⚠️ 将 Parcelables 与 DataStore 一起使用并不安全,因为不同 Android 版本之间的数据格式可能会有所变化。


使用序列化器


在您构建时,将您创建的序列化器传递到 DataStore:


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

val Context.dataStore by dataStore("my_file.json", serializer = UserPreferencesSerializer)

复制代码

其读取数据看起来与使用 protos 进行读取一样:


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

suspend fun getShowCompleted(): Boolean {
context.dataStore.data.first().showCompleted
}
复制代码

您可以使用生成的 .copy() 函数更新数据:


/* Copyright 2021 Google LLC.  
SPDX-License-Identifier: Apache-2.0 */

suspend fun setShowCompleted(newShowCompleted: Boolean) {
// This will leave the sortOrder value untouched:
context.dataStore.updateData { it.copy(newShowCompleted = showCompleted) }
}
复制代码

总结


结合使用 DataStore 与 Kotlin 序列化和数据类可减少样板文件并有助于简化代码,但您必须多加小心,避免因为可变性而引发错误。您只需定义数据类和实现序列化器即可。快来动手尝试一下吧!


如要详细了解 DataStore,您可以查看我们的 文档 并获得一些使用 Proto DataStorePreferences DataStore Codelab 的实践经验。


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

ART虚拟机 | 锁

本文基于Android 11(R) Java中对临界区的锁定通常用synchronize代码块完成,因此标题中的“锁”实际上是对synchronize关键字的剖析。Synchronize代码块使用时必须传入一个对象,这个对象可以是this对象,可以是类对象(e...
继续阅读 »

本文基于Android 11(R)


Java中对临界区的锁定通常用synchronize代码块完成,因此标题中的“锁”实际上是对synchronize关键字的剖析。Synchronize代码块使用时必须传入一个对象,这个对象可以是this对象,可以是类对象(e.g. Foo.class),也可以是任何其他对象。因此我们可以说,锁的状态和对象关联。亦或者,每个对象天生都是一把锁。


Synchronize生成的字节码会对应两条指令,分别是monitor-entermonitor-exit。下面我们针对monitor_enter,分别从解释执行和机器码执行两个方向去寻找这个指令的最终实现。


解释执行


[art/runtime/interpreter/interpreter_switch_impl-inl.h]


HANDLER_ATTRIBUTES bool MONITOR_ENTER() {
...
ObjPtr<mirror::Object> obj = GetVRegReference(A());
if (UNLIKELY(obj == nullptr)) {
...
} else {
DoMonitorEnter<do_assignability_check>(self, &shadow_frame, obj); <===调用
...
}
}
复制代码

[art/runtime/interpreter/interpreter_common.h]


static inline void DoMonitorEnter(Thread* self, ShadowFrame* frame, ObjPtr<mirror::Object> ref)
NO_THREAD_SAFETY_ANALYSIS
REQUIRES(!Roles::uninterruptible_) {
...
StackHandleScope<1> hs(self);
Handle<mirror::Object> h_ref(hs.NewHandle(ref)); <===调用
h_ref->MonitorEnter(self);
...
}
复制代码

[art/runtime/mirror/object-inl.h]


inline ObjPtr<mirror::Object> Object::MonitorEnter(Thread* self) {
return Monitor::MonitorEnter(self, this, /*trylock=*/false); <===调用
}
复制代码

解释执行会使用switch-case方式分别解析每一条指令,由上述代码可知,monitor-enter指令最终会调用Monitor::MonitorEnter静态函数。


机器码执行


[art/runtime/arch/arm64/quick_entrypoints_arm64.S]


ENTRY art_quick_lock_object_no_inline
// This is also the slow path for art_quick_lock_object.
SETUP_SAVE_REFS_ONLY_FRAME // save callee saves in case we block
mov x1, xSELF // pass Thread::Current
bl artLockObjectFromCode // (Object* obj, Thread*) <===调用
...
END art_quick_lock_object_no_inline
复制代码

[art/runtime/entrypoints/quick/quick_lock_entrypoints.cc]


extern "C" int artLockObjectFromCode(mirror::Object* obj, Thread* self){
...
if (UNLIKELY(obj == nullptr)) {
...
} else {
ObjPtr<mirror::Object> object = obj->MonitorEnter(self); // May block <===调用
...
}
}
复制代码

[art/runtime/mirror/object-inl.h]


inline ObjPtr<mirror::Object> Object::MonitorEnter(Thread* self) {
return Monitor::MonitorEnter(self, this, /*trylock=*/false); <===调用
}
复制代码

殊途同归,机器码执行时最终也会调用Monitor::MonitorEnter


锁的两种形态


虚拟机中将锁实现为两种形态,一种称为Thin Lock,另一种称为Fat Lock。


Thin Lock用于竞争较弱的场景。在竞争发生时,采用自旋(spin)和让渡CPU(yield)的方式等待锁,而不是进行系统调用和上下文切换。当持有锁的线程很快完成操作时,短暂的自旋会比上下文切换开销更小。


可是如果自旋一段时间发现还无法获取到锁时,Thin Lock就会膨胀为Fat Lock,一方面增加数据结构存储与锁相关的具体信息,另一方面通过系统调用挂起线程。


总结一下,Fat Lock功能健全,但开销较大。而Thin Lock开销虽小,但无法用于长时间等待的情况。所以实际的做法是先使用Thin Lock,当功能无法满足时再膨胀为Fat Lock。


文章开头提到,每个对象天生都是一把锁。那么这个锁的信息到底存在对象的什么位置呢?


答案是存在art::mirror::Object的对象头中(详见ART虚拟机 | Java对象和类的内存结构)。对象头中有一个4字节长的字段monitor_,其中便存储了锁相关的信息。


monitor字段.png


4字节共32bits,高位的两个bits用于标记状态。不同的状态,存储的信息含义也不同。两个bits共4种状态,分别为ThinOrUnlock(Thin/Unlock共用一个状态),Fat,Hash和ForwardingAddress。ThinOrUnlock和Fat表示锁的状态,Hash是为对象生成HashMap中所用的哈希值,ForwardingAddress是GC时使用的状态。


上图中的m表示mark bit state,r表示read barrier state,都是配合GC使用的标志,在讨论锁的时候可以不关心。


当我们对一个空闲对象进行monitor-enter操作时,锁的状态由Unlock切换到Thin。代码如下。


switch (lock_word.GetState()) {
case LockWord::kUnlocked: {
// No ordering required for preceding lockword read, since we retest.
LockWord thin_locked(LockWord::FromThinLockId(thread_id, 0, lock_word.GCState()));
if (h_obj->CasLockWord(lock_word, thin_locked, CASMode::kWeak, std::memory_order_acquire)) {
...
return h_obj.Get(); // Success!
}
continue; // Go again.
}
复制代码

LockWord对象的大小就是4字节,所以可以将它等同于art::mirror::Objectmonitor_字段,只不过它内部实现了很多方法可以灵活操作4字节中的信息。锁状态切换时,将当前线程的thread id(thread id并非tid,对每个进程而言它都从1开始)存入monitor_字段,与GC相关的mr标志保持不变。


当对象被线程锁定后,假设我们在同线程内对该它再次进行monitor-enter操作,那么就会发生Thin Lock的重入。如果在不同线程对该对象进行monitor-enter操作,那么就会发生Thin Lock的竞争。代码和流程图如下。


case LockWord::kThinLocked: {
uint32_t owner_thread_id = lock_word.ThinLockOwner();
if (owner_thread_id == thread_id) {
uint32_t new_count = lock_word.ThinLockCount() + 1;
if (LIKELY(new_count <= LockWord::kThinLockMaxCount)) {
LockWord thin_locked(LockWord::FromThinLockId(thread_id,
new_count,
lock_word.GCState()));
if (h_obj->CasLockWord(lock_word,
thin_locked,
CASMode::kWeak,
std::memory_order_relaxed)) {
AtraceMonitorLock(self, h_obj.Get(), /* is_wait= */ false);
return h_obj.Get(); // Success!
}
continue; // Go again.
} else {
// We'd overflow the recursion count, so inflate the monitor.
InflateThinLocked(self, h_obj, lock_word, 0);
}
} else {
// Contention.
contention_count++;
Runtime* runtime = Runtime::Current();
if (contention_count <= runtime->GetMaxSpinsBeforeThinLockInflation()) {
sched_yield();
} else {
contention_count = 0;
// No ordering required for initial lockword read. Install rereads it anyway.
InflateThinLocked(self, h_obj, lock_word, 0);
}
}
continue; // Start from the beginning.
}
复制代码

ThinLock.png


在ThinLock膨胀为FatLock前,需要执行50次sched_yieldsched_yield会将当前线程放到CPU调度队列的末尾,这样既不用挂起线程,也不用一直占着CPU。不过android master分支已经将这个流程再度优化了,在50次sched_yield之前,再执行100次自旋操作。和sched_yield相比,自旋不会释放CPU。由于单次sched_yield耗时也有微秒,对于锁持有时间极短的情况,用自旋更省时间。


接下来介绍锁的膨胀过程。


void Monitor::InflateThinLocked(Thread* self, Handle<mirror::Object> obj, LockWord lock_word,
uint32_t hash_code) {
DCHECK_EQ(lock_word.GetState(), LockWord::kThinLocked);
uint32_t owner_thread_id = lock_word.ThinLockOwner();
if (owner_thread_id == self->GetThreadId()) {
// We own the monitor, we can easily inflate it.
Inflate(self, self, obj.Get(), hash_code);
} else {
ThreadList* thread_list = Runtime::Current()->GetThreadList();
// Suspend the owner, inflate. First change to blocked and give up mutator_lock_.
self->SetMonitorEnterObject(obj.Get());
bool timed_out;
Thread* owner;
{
ScopedThreadSuspension sts(self, kWaitingForLockInflation);
owner = thread_list->SuspendThreadByThreadId(owner_thread_id,
SuspendReason::kInternal,
&timed_out);
}
if (owner != nullptr) {
// We succeeded in suspending the thread, check the lock's status didn't change.
lock_word = obj->GetLockWord(true);
if (lock_word.GetState() == LockWord::kThinLocked &&
lock_word.ThinLockOwner() == owner_thread_id) {
// Go ahead and inflate the lock.
Inflate(self, owner, obj.Get(), hash_code);
}
bool resumed = thread_list->Resume(owner, SuspendReason::kInternal);
DCHECK(resumed);
}
self->SetMonitorEnterObject(nullptr);
}
}
复制代码

void Monitor::Inflate(Thread* self, Thread* owner, ObjPtr<mirror::Object> obj, int32_t hash_code) {
DCHECK(self != nullptr);
DCHECK(obj != nullptr);
// Allocate and acquire a new monitor.
Monitor* m = MonitorPool::CreateMonitor(self, owner, obj, hash_code);
DCHECK(m != nullptr);
if (m->Install(self)) {
if (owner != nullptr) {
VLOG(monitor) << "monitor: thread" << owner->GetThreadId()
<< " created monitor " << m << " for object " << obj;
} else {
VLOG(monitor) << "monitor: Inflate with hashcode " << hash_code
<< " created monitor " << m << " for object " << obj;
}
Runtime::Current()->GetMonitorList()->Add(m);
CHECK_EQ(obj->GetLockWord(true).GetState(), LockWord::kFatLocked);
} else {
MonitorPool::ReleaseMonitor(self, m);
}
}
复制代码

膨胀(Inflate)的具体操作比较简单,简言之就是创建一个Monitor对象,存储更多的信息,然后将Monitor Id放入原先的monitor_字段中。


关键的地方在于膨胀的充分条件。如果Thin Lock本来就由本线程持有,那么膨胀不需要经过任何人同意,可以直接进行。但如果该Thin Lock由其他线程持有,那么膨胀之前必须先暂停(这里的暂停并不是指将线程从CPU上调度出去,而是不允许它进入Java世界改变锁状态)持有线程,防止膨胀过程中对锁信息的更新存在竞争。膨胀之后,持有线程恢复运行,此时它看到的Lock已经变成了Fat Lock。


当锁膨胀为Fat Lock后,由于持有锁的动作并未完成,所以该线程会再次尝试。只不过这次走的是Fat Lock分支,执行如下代码。


case LockWord::kFatLocked: {
// We should have done an acquire read of the lockword initially, to ensure
// visibility of the monitor data structure. Use an explicit fence instead.
std::atomic_thread_fence(std::memory_order_acquire);
Monitor* mon = lock_word.FatLockMonitor();
if (trylock) {
return mon->TryLock(self) ? h_obj.Get() : nullptr;
} else {
mon->Lock(self);
DCHECK(mon->monitor_lock_.IsExclusiveHeld(self));
return h_obj.Get(); // Success!
}
}
复制代码

{
ScopedThreadSuspension tsc(self, kBlocked); // Change to blocked and give up mutator_lock_.

// Acquire monitor_lock_ without mutator_lock_, expecting to block this time.
// We already tried spinning above. The shutdown procedure currently assumes we stop
// touching monitors shortly after we suspend, so don't spin again here.
monitor_lock_.ExclusiveLock(self);
}
复制代码

上述代码的ScopedThreadSuspension对象用于完成线程状态的切换,之所以叫scoped,是因为它是通过构造和析构函数完成状态切换和恢复的。在作用域内的局部变量会随着作用域的结束而自动析构,因此花括号结束,线程状态也就由Blocked切换回Runnable了。


最终调用monitor_lock_(Mutex对象)的ExclusiveLock方法。


void Mutex::ExclusiveLock(Thread* self) {
if (!recursive_ || !IsExclusiveHeld(self)) {
#if ART_USE_FUTEXES
bool done = false;
do {
int32_t cur_state = state_and_contenders_.load(std::memory_order_relaxed);
if (LIKELY((cur_state & kHeldMask) == 0) /* lock not held */) {
done = state_and_contenders_.CompareAndSetWeakAcquire(cur_state, cur_state | kHeldMask);
} else {
...
if (!WaitBrieflyFor(&state_and_contenders_, self,
[](int32_t v) { return (v & kHeldMask) == 0; })) {
// Increment contender count. We can't create enough threads for this to overflow.
increment_contenders();
// Make cur_state again reflect the expected value of state_and_contenders.
cur_state += kContenderIncrement;
if (UNLIKELY(should_respond_to_empty_checkpoint_request_)) {
self->CheckEmptyCheckpointFromMutex();
}
do {
if (futex(state_and_contenders_.Address(), FUTEX_WAIT_PRIVATE, cur_state,
nullptr, nullptr, 0) != 0) {
...
cur_state = state_and_contenders_.load(std::memory_order_relaxed);
} while ((cur_state & kHeldMask) != 0);
decrement_contenders();
}
}
} while (!done);
...
exclusive_owner_.store(SafeGetTid(self), std::memory_order_relaxed);
RegisterAsLocked(self);
}
recursion_count_++;
...
}
复制代码

Mutex::ExclusiveLock最终通过futex系统调用陷入内核态,在内核态中将当前线程从CPU中调度出去,实现挂起。值得注意的是,FatLock中依然有spin和yield的操作(WaitBrieflyFor函数),这是因为Thin Lock一旦膨胀为Fat Lock就很难deflate回去,而后续对Fat Lock的使用依然会碰到短时持有锁的情况,这也意味先前的优化此处依然可用。


上面这一块代码算是锁的核心实现,被调用的次数也非常多,因此任何一点微小的优化都很重要。我之前写过一篇文章调试经验 | C++ memory order和一个相关的稳定性问题详细分析了一个由memory order使用错误导致的线程卡死的问题,其中还介绍了C++的memory order,它也正是Java volatile关键字的(ART)底层实现。


此外我还给谷歌提过ExclusiveLock的bug,这个bug既会消耗battery,也会在某些情况下导致系统整体卡死。下面是谷歌的具体回复,感兴趣的可以查看修复


Hans Reply.png


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

iOS 实现类似通讯录按拼音排序 - PinYin4Objc

最近项目中需要实现类似通讯录那样按拼音进行排序以及索引列表的显示的功能,这里使用了 PinYin4Objc 这个库来实现此功能。PinYinObjc是一个效率很高的汉字转拼音类库,智齿简体和繁体中文,有如下特点:1、效率高,使用数据缓存,第一次初始化以后,拼音...
继续阅读 »

最近项目中需要实现类似通讯录那样按拼音进行排序以及索引列表的显示的功能,这里使用了 PinYin4Objc 这个库来实现此功能。

PinYinObjc是一个效率很高的汉字转拼音类库,智齿简体和繁体中文,有如下特点:
1、效率高,使用数据缓存,第一次初始化以后,拼音数据存入文件缓存和内存缓存,后面转换效率大大提高;
2、支持自定义格式化,拼音大小写等等;
3、拼音数据完整,支持中文简体和繁体,与网络上流行的相关项目比,数据很全,几乎没有出现转换错误的问题.

在项目中使用可以cocoapods来管理:pod 'PinYin4Objc', '~> 1.1.1'
也可以直接去github上下载源码:PinYinObjc

项目需求:
获取一个销售人员的列表,并且把自己放到第一个,用#标示,如图:


代码实现过程:

1、获取销售人员列表数据(这里是通过网络请求获取):

///查询列表数据
- (void)fetchSallersList {
[_listAPI startWithCompletionWithSuccess:^(id responseDataDict) {
[self.tableView.mj_header endRefreshing];
///解析数据
NSMutableArray *array = [SCSalesModel mj_objectArrayWithKeyValuesArray:responseDataDict];
self.resultList = [array mutableCopy];
///处理数据
[self conversionResultData];
[self changeResultList];
///刷新UI
[self reloadUI];
} failure:^(NSError *error) {
[self.tableView.mj_header endRefreshing];
[SCAlertHelper handleError:error];
}];
}

2、将每个销售人员的名字转成拼音并转成大写字母:

HanyuPinyinOutputFormat *outputFormat=[[HanyuPinyinOutputFormat alloc] init];
[outputFormat setToneType:ToneTypeWithoutTone];
[outputFormat setVCharType:VCharTypeWithV];
[outputFormat setCaseType:CaseTypeUppercase];
[self.resultList enumerateObjectsUsingBlock:^(SCSalesModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *pinyin = [[PinyinHelper toHanyuPinyinStringWithNSString:obj.salesName withHanyuPinyinOutputFormat:outputFormat withNSString:@""] uppercaseString];
SCLog(@"名字转拼音大写:%@", pinyin);
obj.pinyinName = pinyin;
}];

3、按照拼音字段pinyinName进行排序:

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"pinyinName" ascending:YES];
NSArray *array = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[self.resultList sortUsingDescriptors:array];
4、定义一个全局变量 dataDictionary 来组织数据结构

key: 将汉字转完拼音后的第一个字母, 也就是上图section中的 A、B、C...

value: 是一个成员数组,存放每个section下的成员列表
_dataDictionary = [[NSMutableDictionary alloc] init];
//存放每个 section 下的成员数组
NSMutableArray *currentArray = nil;
//用于获取拼音中第一个字母
NSRange aRange = NSMakeRange(0, 1);
NSString *firstLetter = nil;
//遍历成员列表组织数据结构
for (SCSalesModel *seller in self.resultList) {
//如果是本人,则暂时不放如 dataDictionary 中
if ([seller.salesId isEqualToString:[SCUserModel currentLoggedInUser].userId]) {
_owerSaller = seller;
continue;
}
//获取拼音中第一个字母,如果已经存在则直接将该成员加入到当前的成员数组中,如果不存在,创建成员数据,添加一个 key-value 结构到 dataDictionary 中
firstLetter = [seller.pinyinName substringWithRange:aRange];
if ([_dataDictionary objectForKey:firstLetter] == nil) {
currentArray = [NSMutableArray array];
[_dataDictionary setObject:currentArray forKey:firstLetter];
}
[currentArray addObject:seller];
}

5、再定义一个全局变量 allKeys 用于显示索引列表中索引:

_allKeys = [[NSMutableArray alloc] initWithArray:[[_dataDictionary allKeys] sortedArrayUsingFunction:sortObjectsByKey context:NULL]];
//然后将本人加入到排好序 allKeys 的最前面
if (_owerSaller) {
[_allKeys insertObject:@"#" atIndex:0];
[_dataDictionary setObject:[NSArray arrayWithObjects:_owerSaller, nil] forKey:@"#"];
}

//其中sortObjectsByKey是排序方法
NSInteger sortObjectsByKey(id user1, id user2, void *context) {
NSString *u1,*u2;
//类型转换
u1 = (NSString*)user1;
u2 = (NSString*)user2;
return [u1 localizedCompare:u2];
}

6、最后就是通过 allKeys 和 dataDictionary 进行配置一下 tableview 的各个代理就 OK 了。


借鉴:http://www.cnblogs.com/jerryfeng/p/4288244.html


菜鸟笔记!希望对你有帮助!

转自:https://www.jianshu.com/p/96d141698700


收起阅读 »

iOS 基于AFNetWorking的联想搜索的实现

需求描述:输入框搜索功能,输入小米,键盘输入按照x-i-a-o-m-i的顺序,而请求是根据输入框内容的变化进行请求,输入框每变化一次就要进行一次请求,直到输入停止,请求的结果列表展示。关键点:频繁的网络请求,又不能影响下次请求的进行,这就要求当新的请求开始前,...
继续阅读 »

需求描述:
输入框搜索功能,输入小米,键盘输入按照x-i-a-o-m-i的顺序,而请求是根据输入框内容的变化进行请求,输入框每变化一次就要进行一次请求,直到输入停止,请求的结果列表展示。
关键点:频繁的网络请求,又不能影响下次请求的进行,这就要求当新的请求开始前,1.展示上次请求的结果;2.就是请求还未返回那就直接取消请求直接进行下次请求.
直接上代码,在封装的网络请求工具里,AFHTTPSessionManager在工具类初始化的时候创建,当前任务@property (nonatomic, strong)NSURLSessionDataTask *currentTask;

[_currentTask cancel];为取消当前任务
[_currentManager.operationQueue cancelAllOperations];取消所有任务

- (void)frequentlyPOST:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
hudOnView:(UIView *)onView
{
if (_currentTask) {
[_currentTask cancel];
[_currentManager.operationQueue cancelAllOperations];
[ProgressHUDUtil hideHUD:onView];
}
[ProgressHUDUtil showLoadingWithView:onView];

_currentTask = [_currentManager POST:Append2Str(API_Base, URLString) parameters:parameters progress:^(NSProgress * _Nonnull uploadProgress) {

} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSString *result =[[ NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];
[ProgressHUDUtil hideHUD:onView];
success(task,result);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
failure(task,error);
}];

}


转自:https://www.jianshu.com/p/777cdfb5e681

收起阅读 »

简易版 React-Router实现

上一篇简单的介绍了react-router 的使用方法和基本的API,对于react-router几个重要的API做了源码解读。这篇就实现一个简易版的 react-router设计思路由上图可知,核心内容就是如何监听到URL的改变?图中说到三种方式,其实也就两...
继续阅读 »

上一篇简单的介绍了react-router 的使用方法和基本的API,对于react-router几个重要的API做了源码解读。这篇就实现一个简易版的 react-router

设计思路


由上图可知,核心内容就是如何监听到URL的改变?图中说到三种方式,其实也就两种pushstate 和 浏览器的前进和回退。刷新页面还是处于当前的URL,不涉及URL的改变。上一篇文章中也讲到 前端路由的原理有两点

  1. URL改变 页面不刷新。
  2. 监听到URL的改变。

所以在设计 react-router 的时候需要考虑 pushstate 和 浏览器的前进和回退这两种方式的URL改变。

Router

功能:负责监听页面对象发生了改变,并开始重新渲染页面 **

  1. 先定义一个上下文,方便把history数据传入所有的子组件
const RouteContext = React.createContext({})
  1. 定义 Router 组件,主要内容监听URL变化
const globalHistory = window.history // history 使用window 全局的history
class Router extends React.Component {
constructor(props) {
super(props)
this.state = { // 把location 设置为state 每次URL的改变,能够更新页面
location: window.location
}
// 第一种跳转方式:浏览器的前进后退,触发popstate 事件
window.addEventListener("popstate", () => {
this.setState({
location: window.location
})
})
}
// 第二种跳转方式:pushstate
// 向子组件提供push 方法更新路由,跳转页面
push = (route) => {
globalHistory.pushState({}, "", route)
this.setState({
location: window.location
})
}
// 定义上下文,把通用内容传入子组件
render() {
const { children } = this.props
const { location } = this.state
return (
<RouteContext.Provider value={{
history: globalHistory,
location,
push: this.push,
}}>
{
React.cloneElement(children, {
history: globalHistory,
location,
push: this.push,
})
}
</RouteContext.Provider>
)
}
}

export default Router

Route

功能:页面开始渲染后,根据具体的页面location信息展示具体路由地址对应的内容 **

import React, { useContext } from 'react'
const Route = (props) => {
// 在上下文中获取到相关信息
const context = useContext(RouteContext)
// 计算 location 匹配到的 path
const computedPath = (path, exact) => {
...TODO
// 这里内容和源码一样,其核心使用了path-to-regexp 库,能够计算出URL中的参数
}
// eslint-disable-next-line no-unused-vars
const { render, children, component, path, exact = false, ...rest } = props
const match = computedPath(path, exact)
const params = { ...context, match, location: context.location }
// 渲染 也就是源码中的三目运算。把相关的属性传入子组件
if (match) {
if (children) {
if (typeof children === 'function') {
return children(params)
}
return React.cloneElement(children, params)
} else if (component) {
return component(params)
} else if (render) {
return render(params)
}
}
return null
}

export default Route

这样一个简单的React-Router 就实现了,能够实现页面的跳转。

完整代码:https://github.com/LiuSandy/web

原文链接:https://zhuanlan.zhihu.com/p/366482879


收起阅读 »

React setState数据更新机制

为什么使用setState在React 的开发过程中,难免会与组件的state打交道。使用过React 的都知道,想要修改state中的值,必须使用内部提供的setState 方法。为什么不能直接使用赋值的方式修改state的值呢?我们就分析一下,先看一个de...
继续阅读 »

为什么使用setState

在React 的开发过程中,难免会与组件的state打交道。使用过React 的都知道,想要修改state中的值,必须使用内部提供的setState 方法。为什么不能直接使用赋值的方式修改state的值呢?我们就分析一下,先看一个demo。

class Index extends React.Component {
this.state = {
count: 0
}
onClick = () => {
this.setState({
count: 10
})
}
render() {
return (
<div>
<span>{this.state.count}</span>
<button onClick={this.onClick}>click</button>
</div>
)
}
}

根据上面代码可以看到,点击按钮后把state 中 count 的值修改为 10。并更新页面的显示。所以state的改变有两个作用:对应的值改变 和 页面更新。要想做到这两点在react 中 非 setState 不可。 假如说我们把 onClick 的方法内容修改为 this.state.count = 10 并在方法内打印出 this.state 的值,可以看到state的值已经改变。但是页面并没有更新到最新的值。 ☆总结一下:

  1. state 值的改变,目的是页面的更新,希望React 使用最新的 state来渲染页面。但是直接赋值的方式并不能让React监听到state的变化。
  2. 必须通过setState 方法来告诉React state的数据已经变化。

☆扩展一下:

在vue中,采用的就是直接赋值的方式来更新data 数据,并且Vue也能够使用最新的data数据渲染页面。这是为什么呢? 在vue2中采用的是 Object.defineProperty() 方式监听数据的get 和 set 方法,做到数据变化的监听 在vue3中采用的是ES6 的 proxy 方式监听数据的变化

setState 的用法

想必所有人都会知道setState 的用法,在这里还是想记录一下: setState方法有两个参数:第一个参数可以是对象直接修改属性值,也可以是函数能够拿到上一次的state值。第二个参数是一个可选的回调函数,可以获取最新的state值 回调函数会在组件更新完成之后执行,等价于在 componentDidUpdate 生命周期内执行。

  1. 第一个参数是对象时:如同上文的demo一样,直接修改state的属性值
this.setState({
key:newState
})
  1. 第一个参数是函数时:在函数内可以获取上一次state 的属性值。
// prevState 是上一次的 state,props 是此次更新被应用时的 props
this.setState((prevState, props) => {
return {
key: prevState.key
}
})

他们两者的区别主要体现在setState的异步更新上面!!!

异步更新还是同步更新

setState() 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式 将 setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

先修改一下上面的代码,如果在onClick 方法中连续调用三次setState,根据上文可知 setState是一个异步的方式,每次调用只是将更改加入队列,同步调用的时候只会执行最后一次更新,所以结果是1而不是3。

onClick = () => {
const { count } = this.state
this.setState({ count: count + 1 })
this.setState({ count: count + 1 })
this.setState({ count: count + 1 })
}

可以把上面代码理解为 Object.assign() 方法,

Object.assign(
state,
{ count: state.count + 1 },
{ count: state.count + 1 },
{ count: state.count + 1 }
)

如果第一个参数传入一个函数,连续调用三次,是不是和传入对象方式的结果是一样的呢?

onClick = () => {
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
}

结果和传入对象的方式大相径庭,使用函数的方式就能够实现自增为3的效果。这又是为什么呢? 在函数内能够拿到最新的state 和 props值。由上文可知 setState 的更新是分批次的,使用函数的方式确保了当前state 是建立在上一个state 之上的,所以实现了自增3的效果。

☆总结一下: 为什么setState 方法是异步的呢?

  1. 可以显著的提升性能,react16 引入了 Fiber 架构,Fiber 中对任务进行了划分和优先级的分类,优先处理优先级比较高的任务。页面的响应就是一个优先级比较高任务,所以如果setState是同步,那么更新一次就要更新一次页面,就会阻塞到页面的响应。最好的办法就是获得到多个更新,之后进行批量的更新。只更新一次页面。
  2. 如果同步更新state,但是还没有执行render 函数,那么state 和 props 就不能够保持同步。

是不是所有的setState 都是异步的形式呢?答案是 否!!!在React 中也会存在setState 同步的场景

onClick = () => {
this.setState({ count: this.state.count + 1 })
console.log(this.state)
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log(this.state)
}, 0)
}

上面的代码会打印出0,2。这又是为什么呢?其实React 中的 setState 并不是严格意义上的异步函数。他是通过队列的延迟执行实现的。使用 isBatchingUpdates 判断当前的setState 是加入到更新队列还是更新页面。当 isBatchingUpdates=ture 是加入更新队列,否则执行更新。

知道了React 是使用 isBatchingUpdates 来判断是否加入更新队列。那么为什么在 setTimeout 事件中 isBatchingUpdates 值为 false ? 原因就是在React中,对HTML的原生事件做了一次封装叫做合成事件。所以在React自己的生命周期和合成事件中,可以控制 isBatchingUdates 的值,可以根据值来判断是否更新页面。而在宿主环境提供的原生事件中(即非合成事件),无法将 isBatchingUpdates 的值置为 false,所以就会立即执行更新。

☆所以setState 并不是有同步的场景,而是在特殊的场景下不受React 的控制 **

总结

setState 并不是单纯的同步函数或者异步函数,他的同步和异步的表现差异体现在调用的场景不同。在React 的生命周期和合成事件中他表现为异步函数。而在DOM的原生事件等非合成事件中表现为同步函数。

本节通过分析setState 的更新机制了解到setState 同步和异步的两种场景,下一节深入剖析下调用setState都做了什么?结合源码了解下为什么会出现两种场景?

原文:https://zhuanlan.zhihu.com/p/366781311

收起阅读 »

配置 ESLint 自动格式化自闭合标签(Self closing tag)

对于没有子元素或不需要子元素的 HTML 标签,通常写成其自闭合的形式会显得简洁些,- <SomeComponent></SomeComponent> + <SomeComponent/> 通过配置 ESLint 可在格式化...
继续阅读 »

对于没有子元素或不需要子元素的 HTML 标签,通常写成其自闭合的形式会显得简洁些,

- <SomeComponent></SomeComponent>
+ <SomeComponent/>

通过配置 ESLint 可在格式化的时候将标签自动变成自闭合形式。

create-react-app

如果是使用 create-react-app 创建的项目,直接在 package.json 的 eslint 配置部分加上如下配置即可:

"eslintConfig": {
"extends": "react-app",
+ "rules": {
+ "react/self-closing-comp": [
+ "error"
+ ]
}

安装依赖

安装 ESLint 相关依赖:

$ yarn add eslint eslint-plugin-react

如果是 TypeScript 项目,还需要安装如下插件:

$ yarn add @typescript-eslint/eslint-plugin  @typescript-eslint/parser

配置 ESLint

通过 yarn eslint --init 向导来完成创建,

或手动创建 .eslintrc.json 填入如下配置:

{
"extends": ["eslint:recommended", "plugin:react/recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["react", "@typescript-eslint"],
"rules": {
"react/self-closing-comp": ["error"]
}
}

安装 ESLint for Vscode

当然了,还需要安装 VSCode 插件 dbaeumer.vscode-eslint

然后配置 VSCode 在保存时自动进行修正动作:

"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},

使用

完成上述配置后,如果发现保存时,格式并未生效,或者只 JavaScript 文件生效,需要补上如下的 VSCode 配置:

"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
]

也可查看 VSCode 的状态栏,看是否有报错可确定是什么原因导致 ESLint 工作不正常,比如 mac BigSur 中细化了权限,需要点击警告图标然后点击允许。




原文:https://zhuanlan.zhihu.com/p/368639332

收起阅读 »

iOS 功能丰富的 Category 类型工具库

YYCategories安装CocoaPods在 Podfile 中添加  pod 'YYCategories'。执行 pod install 或 pod update。导入 <YYCategories/...
继续阅读 »

YYCategories

安装

CocoaPods

  1. 在 Podfile 中添加  pod 'YYCategories'
  2. 执行 pod install 或 pod update
  3. 导入 <YYCategories/YYCategories.h>。

Carthage

  1. 在 Cartfile 中添加 github "ibireme/YYCategories"
  2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
  3. 导入 <YYCategories/YYCategories.h>。

手动安装

  1. 下载 YYCategories 文件夹内的所有内容。
  2. 将 YYCategories 内的源文件添加(拖放)到你的工程。
  3. 为 NSObject+YYAddForARC.m 和 NSThread+YYAdd.m 添加编译参数 -fno-objc-arc
  4. 链接以下 frameworks:
    • UIKit
    • CoreGraphics
    • QuartzCore
    • Accelerate
    • ImageIO
    • CoreText
    • CoreFoundation
    • libz
  5. 导入 YYCategories.h

注意

我希望调用 API 时,有着和调用系统自带 API 一样的体验,所以我并没有为 Category 方法添加前缀。我已经用工具扫描过这个项目中的 API,确保没有对系统 API 产生影响。我知道没有前缀的 Category 可能会带来麻烦(比如可能和其他某些类库产生冲突),所以如果你只需要其中少量代码,那最好将那段代码取出来,而不是导入整个库。


常见问题及demo下载:https://github.com/ibireme/YYCategories

源码下载:YYCategories.zip




收起阅读 »

iOS 异步绘制与显示的工具类

YYAsyncLayeriOS 异步绘制与显示的工具类。简单用法@interface YYLabel : UIView @property NSString *text; @property UIFont *font; @end @implementatio...
继续阅读 »

YYAsyncLayer

iOS 异步绘制与显示的工具类。

简单用法

@interface YYLabel : UIView
@property NSString *text;
@property UIFont *font;
@end

@implementation YYLabel

- (void)setText:(NSString *)text {
_text = text.copy;
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)setFont:(UIFont *)font {
_font = font;
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)layoutSubviews {
[super layoutSubviews];
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)contentsNeedUpdated {
// do update
[self.layer setNeedsDisplay];
}

#pragma mark - YYAsyncLayer

+ (Class)layerClass {
return YYAsyncLayer.class;
}

- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {

// capture current state to display task
NSString *text = _text;
UIFont *font = _font;

YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
task.willDisplay = ^(CALayer *layer) {
//...
};

task.display = ^(CGContextRef context, CGSize size, BOOL(^isCancelled)(void)) {
if (isCancelled()) return;
NSArray *lines = CreateCTLines(text, font, size.width);
if (isCancelled()) return;

for (int i = 0; i < lines.count; i++) {
CTLineRef line = line[i];
CGContextSetTextPosition(context, 0, i * font.pointSize * 1.5);
CTLineDraw(line, context);
if (isCancelled()) return;
}
};

task.didDisplay = ^(CALayer *layer, BOOL finished) {
if (finished) {
// finished
} else {
// cancelled
}
};

return task;
}
@end

安装

CocoaPods

  1. 在 Podfile 中添加 pod 'YYAsyncLayer'
  2. 执行 pod install 或 pod update
  3. 导入

Carthage

  1. 在 Cartfile 中添加 github "ibireme/YYAsyncLayer"
  2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
  3. 导入

手动安装

  1. 下载 YYAsyncLayer 文件夹内的所有内容。
  2. 将 YYAsyncLayer 内的源文件添加(拖放)到你的工程。
  3. 导入 YYAsyncLayer.h


系统要求

该项目最低支持 iOS 6.0 和 Xcode 8.0






收起阅读 »

iOS 全局并发队列管理工具

YYDispatchQueuePooliOS 全局并发队列管理工具。当某个 block 所在线程被锁住时,concurrent queue 会创建大量线程以至于占用了过多资源而影响到主线程。这里可以用一个全局的 serial queue pool 来尽量控制全...
继续阅读 »

YYDispatchQueuePool

iOS 全局并发队列管理工具。

当某个 block 所在线程被锁住时,concurrent queue 会创建大量线程以至于占用了过多资源而影响到主线程。这里可以用一个全局的 serial queue pool 来尽量控制全局线程数。

用法

// 从全局的 queue pool 中获取一个 queue
dispatch_queue_t queue = YYDispatchQueueGetForQOS(NSQualityOfServiceUtility);

// 创建一个新的 serial queue pool
YYDispatchQueuePool *pool = [[YYDispatchQueuePool alloc] initWithName:@"file.read" queueCount:5 qos:NSQualityOfServiceBackground];
dispatch_queue_t queue = [pool queue];

安装

CocoaPods

  1. 在 Podfile 中添加 pod 'YYDispatchQueuePool'
  2. 执行 pod install 或 pod update
  3. 导入 <YYDispatchQueuePool/YYDispatchQueuePool.h>。

Carthage

  1. 在 Cartfile 中添加 github "ibireme/YYDispatchQueuePool"
  2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
  3. 导入 <YYDispatchQueuePool/YYDispatchQueuePool.h>。

手动安装

  1. 下载 YYDispatchQueuePool 文件夹内的所有内容。
  2. 将 YYDispatchQueuePool 内的源文件添加(拖放)到你的工程。
  3. 导入 YYDispatchQueuePool.h

系统要求

该项目最低支持 iOS 6.0 和 Xcode 8.0


常见问题及demo下载:https://github.com/ibireme/YYDispatchQueuePool

源码下载:YYDispatchQueuePool.zip




收起阅读 »

iOS 键盘管理工具

YYKeyboardManageriOS 键盘监听管理工具类。'兼容性该项目能很好的兼容 iPhone / iPad / iPod,兼容 iOS 6~11, 并且能很好的处理屏幕旋转。用法// 获取键盘管理器 YYKeyboardManager *manag...
继续阅读 »

YYKeyboardManager

iOS 键盘监听管理工具类。'

兼容性

该项目能很好的兼容 iPhone / iPad / iPod,兼容 iOS 6~11, 并且能很好的处理屏幕旋转。

用法

// 获取键盘管理器
YYKeyboardManager *manager = [YYKeyboardManager defaultManager];

// 获取键盘的 view 和 window
UIView *view = manager.keyboardView;
UIWindow *window = manager.keyboardWindow;

// 获取键盘当前状态
BOOL visible = manager.keyboardVisible;
CGRect frame = manager.keyboardFrame;
frame = [manager convertRect:frame toView:self.view];

// 监听键盘动画
[manager addObserver:self];
- (void)keyboardChangedWithTransition:(YYKeyboardTransition)transition {
CGRect fromFrame = [manager convertRect:transition.fromFrame toView:self.view];
CGRect toFrame = [manager convertRect:transition.toFrame toView:self.view];
BOOL fromVisible = transition.fromVisible;
BOOL toVisible = transition.toVisible;
NSTimeInterval animationDuration = transition.animationDuration;
UIViewAnimationCurve curve = transition.animationCurve;
}

安装

CocoaPods

  1. 在 Podfile 中添加 pod 'YYKeyboardManager'
  2. 执行 pod install 或 pod update
  3. 导入 <YYKeyboardManager/YYKeyboardManager.h>。

Carthage

  1. 在 Cartfile 中添加 github "ibireme/YYKeyboardManager"
  2. 执行 carthage update --platform ios 并将生成的 framework 添加到你的工程。
  3. 导入 <YYKeyboardManager/YYKeyboardManager.h>。

手动安装

  1. 下载 YYKeyboardManager 文件夹内的所有内容。
  2. 将 YYKeyboardManager 内的源文件添加(拖放)到你的工程。
  3. 导入 YYKeyboardManager.h

系统要求

该项目最低支持 iOS 6.0 和 Xcode 8.0










收起阅读 »

浅谈前端权限设计方案

前端权限架构的设计一直都是备受关注的技术点.通过给项目引入了权限控制方案,可以满足我们灵活的调整用户访问相关页面的许可. 比如哪些页面向游客开放,哪些页面必须要登录后才能访问,哪些页面只能被某些角色访问(比如超级管理员).有些页面即使用户登录了但受到角色的限制...
继续阅读 »

前端权限架构的设计一直都是备受关注的技术点.通过给项目引入了权限控制方案,可以满足我们灵活的调整用户访问相关页面的许可.


比如哪些页面向游客开放,哪些页面必须要登录后才能访问,哪些页面只能被某些角色访问(比如超级管理员).有些页面即使用户登录了但受到角色的限制,他也只被允许看到页面的部分内容.


出于实际工作的需要,很多项目(尤其类后台管理系统)需要引入权限控制.倘若权限整体的架构设计的不好或者没有设计,会导致项目中各种权限代码混入业务代码造成结构混乱,其次想给新模块引入权限控制或者功能扩展都十分棘手.


虽然前端在权限层面能做一些事情,但很遗憾真正对权限进行把关的是后端.例如一个软件系统,前端在不写一行权限代码的情况下,当用户进入某个他无权访问的页面时,后端是可以判断他越权访问并拒绝返回数据的.由此可见前端即使不做什么整个系统也是可以正常运行的,但这样应用的体验很不好.另外一个很重要的原因就是前端做的权限校验都是可以被本地数据造假越权通过.


前端如果能判断某用户越权访问页面时,就不要让他进入那张页面后再弹出无权访问的信息提示,因为这样体验很差.最优方案是直接关闭那些页面的入口,只让他看到他能访问的页面.即使他通过输入路径恶意访问,导航最后只会将它带到默认页面或404页面.


前端做的权限控制大抵是先接受后台发送的权限数据,然后将数据注入到应用中.整个应用于是开始对页面的展现内容以及导航逻辑进行控制,从而达到权限控制的目的.前端做的权限控制虽然能提供一层防护,但根本目的还是为了优化体验.


本文接下来将从下面三个层面,从易到难步步推进,讲述目前前端主流的权限控制方案的实现.(下面代码将会以vue3vue-router 4演示)



  • 登录权限控制

  • 页面权限控制

  • 内容权限控制
登录权限控制

登录权限控制要做的事情,是实现哪些页面能被游客访问,哪些页面只有登录后才能被访问.在一些没有引入角色的软件系统中,通过是否登录来评定页面能否被访问在实际工作中非常常见.

实现这个功能也非常简单,首先按照惯例定义一份路由.

export const routes = [
{
path: '/login', //登录页面
name: 'Login',
component: Login,
},
{
path:"/list", // 列表页
name:"List",
component: List,
},
{
path:"/myCenter", // 个人中心
name:"MyCenter",
component: MyCenter,
meta:{
need_login:true //需要登录
}
}
]

假定存在三个页面:登录页、列表页和个人中心页.登录页和列表页所有人都可以访问,但个人中心页面需要登录后才能看到,给该路由添加一个meta对象,并将need_login置为true;


另外对于那些需要登录后才能看到的页面,用户如果没有登录就访问,就将页面跳转到登录页.等到他填写完用户名和密码点击登录后直接跳转到原来他想访问的页面.


在代码层面,通过router.beforeEach可以轻松实现上述目标,每次页面跳转时都会调用router.beforeEach包裹的函数,代码如下.


to是要即将访问的路由信息,从其中拿到need_login的值可以判断是否需要登录.再从vuex中拿到用户的登录信息.


如果用户没有登录并且要访问的页面又需要登录时就使用next跳转到登录页面,并将需要访问的页面路由名称通过redirect_page传递过去,在登录页面就可以拿到redirect_page等登录成功后直接跳转.

//vue-router4 创建路由实例
const router = createRouter({
history: createWebHashHistory(),
routes,
});

router.beforeEach((to, from, next) => {
const { need_login = false } = to.meta;
const { user_info } = store.state; //从vuex中获取用户的登录信息
if (need_login && !user_info) {
// 如果页面需要登录但用户没有登录跳到登录页面
const next_page = to.name; // 配置路由时,每一条路由都要给name赋值
next({
name: 'Login',
params: {
redirect_page: next_page,
...from.params, //如果跳转需要携带参数就把参数也传递过去
},
});
} else {
//不需要登录直接放行
next();
}
});

页面权限控制


页面权限控制要探讨的问题是如何给不同角色赋予不同的页面访问权限,接下来先了解一下角色的概念.


在一些权限设置比较简单的系统里,使用上面第一种方法就足够了,但如果系统引入了角色,那么就要在上面基础上,再进一步改造增强权限控制的能力.


角色的出现是为了更加个性化配置权限列表.比如当前系统设置三个角色:普通会员,管理员以及超级管理员.普通会员能够浏览软件系统的所有内容,但是它不能编辑和删除内容.管理员拥有普通会员的所有能力,另外它还能删除和编辑内容.超级管理员拥有软件系统所有权限,他单独拥有赋予某个账号为管理员或移除其身份的能力.


一旦软件系统引入了角色的概念,那么每个账户在注册之后就会被赋予相应的角色,从而拥有相应的权限.我们前端要做的事情就是依据不同角色给与它相应页面访问和操作的权限.这里要注意,前端依据的客体是角色,不是某个账户,因为账户是依托于角色的.


普通会员,管理员以及超级管理员这样角色的安排还是一种非常简单的划分方式,在实际项目中,角色的划份要更加细致的多.比如一些常见的后台业务系统,软件系统会按照公司的各个部门来建立角色,诸如市场部,销售部,研发部之类.公司的每个成员就会被划分到相应角色中,从而只具备该角色所拥有的权限.


公司另外一些高层领导他们的账户则会被划分到普通管理员或高级管理员中,那么他们相较于其他角色也会拥有更多的权限.


上面介绍那么多角色的概念其实是为了从全栈的维度去理解权限的设计,但真正落地到前端项目中是不需要去处理角色逻辑的,那部分功能主要由后端完成.


现在假定后端不处理角色完全交给前端来做会出现什么问题.首先前端新建一个配置文件,假定当前系统设定三种角色:普通会员,管理员以及超级管理员以及每个角色能访问的页面列表(伪代码如下).

export const permission_list = {
member:["List","Detail"], //普通会员
admin:["List","Detail","Manage"], // 管理员
super_admin:["List","Detail","Manage","Admin"] // 超级管理员
}

数组里每个值对应着前端路由配置的name值.普通会员能访问列表页详情页,管理员能额外访问内容管理页面,超级管理员能额外访问人员管理页面.


整个运作流程简述如下.当用户登录成功之后,通过接口返回值得知用户数据和所属角色.拿到角色值后就去配置文件里取出该角色能访问的页面列表数组,随后将这部分权限数据加载到应用中从而达到权限控制的目的.


从上面流程看,角色放在前端配置也是可以的.但假如项目已经上线,产品经理要求项目急需增加一个新角色合作伙伴,并把原来已经存在的用户张三移动到合作伙伴角色下面.那这样的变动会导致前端需要修改代码文件,在原来的配置文件上再新建角色来满足这一需求.


由此可见由前端来配置角色列表是非常不灵活且容易出错的,那么最优方案是交给后端去配置.用户一旦登录后,后端接口直接返回该账号拥有的权限列表就行了,至于该账户属于什么角色以及角色拥有的页面权限全部丢给后端去处理.


用户登录成功后,后端接口数据返回如下.

{
user_id:1,
user_name:"张三",
permission_list:["List","Detail","Manage"]
}

前端现在不需要理会张三属于什么角色,只需要按照张三的权限列表给他相应的访问权限就行了,其他都交给后端处理.

通过接口的返回值permission_list可知,张三能访问列表页详情页以及内容管理页.我们先回到路由配置页面,看看如何配置.

//静态路由
export const routes = [
{
path: '/login', //登录页面
name: 'Login',
component: Login,
},
{
path:"/myCenter", // 个人中心
name:"MyCenter",
component: MyCenter,
meta:{
need_login:true //需要登录
}
},
{
path:"/", // 首页
name:"Home",
component: Home,
}
]

//动态路由
export const dynamic_routes = [
{
path:"/list", // 列表页
name:"List",
component: List
},
{
path:"/detail", // 详情页
name:"Detail",
component: Detail
},
{
path:"/manage", // 内容管理页
name:"Manage",
component: Manage
},
{
path:"/admin", // 人员管理页
name:"Admin",
component: Admin
}
]

现在将所有路由分成两部分,静态路由routes和动态路由dynamic_routes.静态路由里面的页面是所有角色都能访问的,它里面主要区分登录访问和非登录访问,处理的逻辑与上面介绍的登录权限控制一致.


动态路由dynamic_routes里面存放的是与角色定制化相关的的页面.现在继续看下面张三的接口数据,该如何给他设置权限.

{
user_id:1,
user_name:"张三",
permission_list:["List","Detail","Manage"]
}

用户登录成功后,一般会将上述接口信息存到vuexlocalStorage里面.假如此时刷新浏览器,我们就要动态添加路由信息.

import store from "@/store";

export const routes = [...]; //静态路由

export const dynamic_routes = [...]; //动态路由

const router = createRouter({ //创建路由对象
history: createWebHashHistory(),
routes,
});

//动态添加路由
if(store.state.user != null){ //从vuex中拿到用户信息
//用户已经登录
const { permission_list } = store.state.user; // 从用户信息中获取权限列表
const allow_routes = dynamic_routes.filter((route)=>{ //过滤允许访问的路由
return permission_list.includes(route.name);
})
allow_routes.forEach((route)=>{ // 将允许访问的路由动态添加到路由栈中
router.addRoute(route);
})
}

export default router;

核心代码在动态添加路由里面,主要利用了vue-router 4提供的APIrouter.addRoute,它能够给已经创建的路由实例继续添加路由信息.


我们先从vuex里面拿到当前用户的权限列表,然后遍历动态路由数组dynamic_routes,从里面过滤出允许访问的路由,最后将这些路由动态添加到路由实例里.


这样就实现了用户只能按照他对应的权限列表里的规则访问到相应的页面,至于那些他无权访问的页面,路由实例根本没有添加相应的路由信息,因此即使用户在浏览器强行输入路径越权访问也是访问不到的.


由于vue-router 4废除了之前的router.addRoutes,换成了router.addRoute.每一次只能一个个添加路由信息,所以要将allow_routes遍历循环添加.


动态添加路由这部分代码最好单独封装起来,因为用户第一次使用还没登录时,store.state.user是为空的,上面动态添加路由的逻辑会被跳过.那么在用户登录成功获取到权限列表的信息后,需要再把上面动态添加路由的逻辑执行一遍.


添加嵌套子路由


假如静态路由的形式如下,现在想把列表页添加到Tabs嵌套路由的children里面.

const routes = [
{
path: '/', //标签容器
name: 'Tabs',
component: Tabs,
children: [{
path: '', //首页
name: 'Home',
component: Home,
}]
}
]

export const dynamic_routes = [
{
path:"/list", // 列表页
name:"List",
component: List
}
]

官方router.addRoute给出了相应的配置去满足这样的需求(代码如下).router.addRoute接受两个参数,第一个参数对应父路由的name属性,第二个参数是要添加的子路由信息.

router.addRoute("Tabs", {
path: "/list",
name: "List",
component: List,
});

切换用户信息是非常常见的功能,但是应用在切换成不同账号后可能会引发一些问题.例如用户先使用超级管理员登录,由于超级管理员能访问所有页面,因此所有页面路由信息都会被添加到路由实例里.


此时该用户退出账号,使用一个普通会员的账号登录.在不刷新浏览器的情况下,路由实例里面仍然存放了所有页面的路由信息,即使当前账号只是一个普通会员,如果他越权访问相关页面,路由还是会跳转的,这样的结果并不是我们想要的.


解决方案有两个.第一是用户每次切换账户后刷新浏览器重新加载,刷新后的路由实例是重新配置的所以可以避免这个问题,但是刷新页面会带来不好的体验.


第二个方案是当用户选择登出后,清除掉路由实例里面处存放的路由栈信息(代码如下).

const router = useRouter(); // 获取路由实例
const logOut = () => { //登出函数
//将整个路由栈清空
const old_routes = router.getRoutes();//获取所有路由信息
old_routes.forEach((item) => {
const name = item.name;//获取路由名词
router.removeRoute(name); //移除路由
});
//生成新的路由栈
routes.forEach((route) => {
router.addRoute(route);
});
router.push({ name: "Login" }); //跳转到登录页面
};

移除单个路由主要利用了官方提供的API,即router.removeRoute.


路由栈清空后什么页面都不能访问了,甚至登录页面都访问不了.所以需要再把静态的路由列表routes引入进来,使用router.addRoute再添加进入.这样就能让路由栈恢复到最初的状态.


内容权限控制


页面权限控制它能做到让不同角色访问不同的页面,但对于一些颗粒度更小的项目,比如希望不同的角色都能进入页面,但要求看到的页面内容不一样,这就需要对内容进行权限控制了.


假设某个后台业务系统的界面如下图所示.表格里面存放的是列表数据,当点击发布需求时跳转到新增页面.当勾选列表中的某一条数据后,点击修改按钮显示修改该条数据的弹出框.同理点击删除按钮显示删除该条数据的弹出框


假设项目需求该系统存在三个角色:职员、领导和高层领导.职员不具备修改删除以及发布需求的功能,他只能查看列表.当职员进入该页面时,页面上只显示列表内容,其他三个按钮移除.


领导角色保留列表发布需求按钮.高级领导角色保留页面上所有内容.


我们拿到图片后要先要对页面内容整体分析一遍,按照增删查改四个维度对页面内容进行归类.使用简称CURD来标识(CURD分别代表创建(Create)、更新(Update)、读取(Retrieve)和删除(Delete)).


上图中列表内容属于查询操作,因此设定为R.凡是具备R权限的用户就显示该列表内容.


发布需求属于新增操作,设定凡是具备C权限的用户就显示该按钮.


同理修改按钮对应着U权限,删除按钮对应着D权限.


由此可以推断出职员角色在该页面的权限编码为R,它只能查看列表内容无法操作.


领导角色对应的权限编码为CR.高级领导对应的权限编码为CURD.


现在用户登录完成后,假设后端接口返回的数据如下(将这条数据存到vuex):

{
user_id:1,
user_name:"张三",
permission_list:{
"List":"CR", //权限编码
"Detail":"CURD" //权限编码
}
}

张三除了静态路由设置的页面外,他只能额外访问List列表页以及Detail详情页.其中列表页他只具备创建和新增权限,详情页他具备增删查改所有权限.那么当张三访问上图中的页面时,页面中应该只显示列表发布需求按钮.


我们现在要做的就是设计一个方案尽可能让页面内容方便被权限编码控制.首先创建一个全局的自定义指令permission,代码如下:

import router from './router';
import store from './store';

const app = createApp(App); //创建vue的根实例

app.directive('permission', {
mounted(el, binding, vnode) {
const permission = binding.value; // 获取权限值
const page_name = router.currentRoute.value.name; // 获取当前路由名称
const have_permissions = store.state.permission_list[page_name] || ''; // 当前用户拥有的权限
if (!have_permissions.includes(permission)) {
el.parentElement.removeChild(el); //不拥有该权限移除dom元素
}
},
});

当元素挂载完毕后,通过binding.value获取该元素要求的权限编码.然后拿到当前路由名称,通过路由名称可以在vuex中获取到该用户在该页面所拥有的权限编码.如果该用户不具备访问该元素的权限,就把元素dom移除.


对应到上面的案例,在页面里按照如下方式使用v-permission指令.

<template>
<div>
<button v-permission="'U'">修改</button> <button v-permission="'D'">删除</button>
</div>
<p>
<button v-permission="'C'">发布需求</button>
</p>

<!--列表页-->
<div v-permission="'R'">
...
</div>
</template>

将上面模板代码和自定义指令结合理解一下就很容易明白整个内容权限控制的逻辑.首先前端开发页面时要将页面分析一遍,把每一块内容按照权限编码分类.比如修改按钮就属于U,删除按钮属于D.并用v-permission将分析结果填写上去.


当页面加载后,页面上定义的所有v-permission指令就会运行起来.在自定义指令内部,它会从vuex中取出该用户所拥有的权限编码,再与该元素所设定的编码结合起来判端是否拥有显示权限,权限不具备就移除元素.


虽然分析过程有点复杂,但是以后每个新页面想接入权限控制非常方便.只需要将新页面的各个dom元素添加一个v-permission和权限编码就完成了,剩下的工作都交给自定义指令内部去做.


延伸



如果项目中删除操作并不是单独放置在一个按钮,而是与列表捆绑在一起放在表格的最后一列,如下图所示.

这样的界面样式在实际工作中非常常见,但似乎上面的v-permission就并不能友好的支持这样的样式.自定义指令在这种情况下虽然不能用,但我们仍然可以采用相同的思路去优化我们现有的代码结构.


例如模板代码如下.整个列表被封装成了一个组件List,那么在List内部就可以写很多的逻辑控制。


比如List组件内也可以通过vuex拿到该用户在当前页面的权限编码,如果发现具备D权限就显示列表中最后删除那一列,否则就不显示.至于整个列表的显示隐藏仍然可以使用v-permission来控制.

<template>
<div>
<button v-permission="'C'">添加资源</button>
</div>

<!--列表页-->
<List v-permission="'R'">
...
</List>
</template>

动态导航

下图中的动态导航也是实际工作中非常常见的需求,比如销售部所有成员只能看到销售模块下的两个页面,同理采购部成员只能看到采购模块下的页面.

下面侧边栏导航组件需要根据不同权限显示不同的页面结构,以满足不同角色群体的要求.

我们要把这种需要个性化设置的组件与上面使用v-permission控制的模式区分开.上面那些页面之所以能使用v-permission来控制,主要原因是因为产品经理在设计整个软件系统的页面时是按照增删查改的规则进行的.因此我们就能抽象出其中存在的共性与规律,再借助自定义指令来简化权限系统的开发.


但是侧边栏组件一般全局只有一个,没有什么特别的规律而言,那就只需要在组件内部使用v-if依据权限值动态渲染就行了.


比如后台接口如下:

{
user_id:1,
user_name:"张三",
permission_list:{
"SALE":true, //显示销售大类
"S_NEED":"CR", //权限编码
"S_RESOURCE":"CURD", //权限编码
}
}

张三拥有访问需求资源页面,但注意SALE并没有与哪个页面对应上,它仅仅只是表示是否显示销售这个一级导航.

接下来在侧面栏组件通过vuex拿到权限数据,再动态渲染页面就可以了.

<template>
<div v-if="permission_list['HOME']">系统首页</div>
<div v-if="permission_list['SALE']">
<p>销售</p>
<div v-if="permission_list['S_NEED']">需求</div>
<div v-if="permission_list['S_RESOURCE']">资源</div>
</div>
<div v-if="permission_list['PURCHASE']">
<p>采购</p>
<div v-if="permission_list['P_NEED']">需求</div>
<div v-if="permission_list['P_RESOURCE']">资源</div>
</div>
</template>

尾言

前端提供的权限控制为应用加固了一层保险,但同时也要警惕前端设定的校验都是可以通过技术手段破解的.权限问题关乎到软件系统所有数据的安危,重要性不言而喻.

为了确保系统平稳运行,前后端都应该做好自己的权限防护.

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



收起阅读 »

iOS 一个比较完美的 Growing TextView

iOS 一个比较完美的 Growing TextView文章缘由现在都 2019 年了,App 中使用自动增高的输入框已经很常见了,即时通讯的 Chat 界面、社交类 App 的评论功能都可以看到自增高输入框。但写出一个自增高输入框容易,写好难。现在市面上一些...
继续阅读 »

iOS 一个比较完美的 Growing TextView

文章缘由
现在都 2019 年了,App 中使用自动增高的输入框已经很常见了,即时通讯的 Chat 界面、社交类 App 的评论功能都可以看到自增高输入框。但写出一个自增高输入框容易,写好难。现在市面上一些主流 App 的输入框依然会有一些瑕疵,例如:文字挡住一部分、粘贴大量文字时出现偏移,具体问题下面详细分析。
现在 iOS 开发已经过了搭建 UI 和普通业务功能的初级阶段,App 要想赢得用户的青睐,除了 App 的功能、UI 设计,交互体验的细节处理至关重要。一般用户只要使用输入框能正常输入即可,90% 的用户不会察觉输入框的一些细节,但作为开发人员应该知道这些细节(bug)并做出处理。

主流 App 的输入框之痛

粘贴文本出现文字偏移
这个问题严格来说算 bug,毕竟粘贴还是一个很常见的操作。


挡住部分文字
一个输入框要么显示 N 行文字、要么显示 N + 1行,如果显示 N + 0.5 行就有点不可思议了。这个问题对 App 功能没有任何影响,但这么多 App 竟然都有这个问题而且非常普遍,是我始料未及的。测试了多个 App 后,只有QQ的输入框做的最好,粘贴、遮挡文字等问题根本不存在。






比较完美的输入框
写出一个自增高的输入框还是比较容易的,大致过程就是给 textView 添加左、右、下/上、高度四个约束,然后监听文字变化的通知,进而修改输入框的高度。如果想写好,就要花时间打磨了。我接下来主要说一下大家可能遇到的一些细节问题。

1、粘贴文本,文字偏移

我的做法是继承 UITextView 重写 setBounds 方法,重新调整contentOffset

- (void)setBounds:(CGRect)bounds
{
[super setBounds:bounds];
// NSLog(@”bounds:%@”, NSStringFromCGRect(bounds));
if (self.contentSize.height <= self.bounds.size.height + 1){
self.contentOffset = CGPointZero; // Fix wrong contentOfset
} else if (!self.tracking) {
CGPoint offset = self.contentOffset;
if (offset.y > self.contentSize.height - bounds.size.height) {
offset.y = self.contentSize.height - bounds.size.height;
if (!self.decelerating && !self.tracking && !self.dragging) {
self.contentOffset = offset;
}
// Fix wrong contentOfset when paster huge text
}
}
}

2、文字离输入框顶部间隙时大时小
正常情况下滚动输入框的文字,文字可以滚动到控件顶部。而 QQ 的输入框,不管怎么滑动文字,文字和输入框顶部都有一段固定间隔。


先了解输入框的一个属性textContainerInset,这个值默认是 (8, 0, 8, 0),就是说默认情况下文字和输入框顶部有 8pt 的偏移,所以当文字输入较多的时候文字向上滚动,那么文字和控件顶部间隙会减小到 0。
实现QQ输入框的效果,我能想到的方案是把textContainerInset全设置为 0(或者top/bottom为0),这样文字就紧挨着输入框,文字和输入框顶部的固定间距就是 0 了。接着要给UITextView添加一个 containerView ,containerView 竖向比 UITextView 高出一部分,从而实现 顶部/底部 的固定间距。

3、挡住部分文字
这个问题是因为 单行文字高度 最大行数 != 输入框的最大高度,输入框的最大高度可不是随便设置的,先确定输入框的font和最大行数,font.lineHeight 行数就是输入框的最大高度。这样就不会出现文字挡住一部分的问题了

GrowTextView

接下来就是我自己写的自增高输入框了,目前没发现什么问题,至少没有上面涉及的问题。




收起阅读 »

【直播回放】主题:iOS Runtime 项目实际应用与面试对刚!

视频回放: 直播主题:iOS  Runtime 项目实际应用与面试对刚!嘉宾介绍:Zuyu   环信生态开发者kol分享大纲:1. 如何使用runtime 动态创建类2. 如何使用runtime 进行hook3. Method Swizzling 误区详解 ,...
继续阅读 »

视频回放:

直播主题:

iOS  Runtime 项目实际应用与面试对刚!

嘉宾介绍:

Zuyu   环信生态开发者kol

分享大纲:

1. 如何使用runtime 动态创建类

2. 如何使用runtime 进行hook

3. Method Swizzling 误区详解 ,让你面试和开发如虎添翼

直播交流群:

添加环信冬冬微信,备注:428runtime


收起阅读 »

超过 js 的 number 类型最大值(9007 1992 5474 0992)的解决办法

bug经过:点击修改无法展示信息(修改时调用queryOne,以id(long)为值,页面传过去的id=1480042498255640-00 ,在数据库中该id=148004249825564012,即错误的id)根本原因:js的number类型有个最大值(...
继续阅读 »

bug经过:点击修改无法展示信息(修改时调用queryOne,以id(long)为值,页面传过去的id=1480042498255640-00 ,在数据库中该id=148004249825564012,即错误的id)

根本原因:

js的number类型有个最大值(安全值)。即2的53次方,为9007199254740992。如果超过这个值,那么js会出现不精确的问题。这个值为16位。

解决方法:

1.后端发字符串类型。

将后端发过来的long类型转为string类型再向前端传。如果向前端传的是DAO集合,则每个DAO都需要转类型,太过于繁琐。想想就算了。

2.在userDao中加入一个字段

如果项目已经成型并且修改数据库会造成不可预料的问题那么可以在User对象中再增加一个String类型id映射字段,如下
    private Long userId;
    private String userIdStr;
    public String getUserIdStr() {
        return this.userId+"";
    }
    public void setUserIdStr(String userIdStr) {
        this.userIdStr = userIdStr;

    }

这个方法是比较靠谱的,确实可以正常的显示数据,查询单个数据id的值都是正确的。但修改用户时无法获取前端传过来的userDao中的userIdStr的值,因为上面的getUserIdStr()不能获取userIdStr的值(如果id没有值)。

3.控制用户新建数据时id的长度。兜兜转转觉得这个最方便。

温馨提示:以后设计表字段时尽量用varchar类型。

原文链接:https://blog.csdn.net/sunmerZeal/article/details/80844843


收起阅读 »

JavaScript 对象

为什么要有对象?如果有一组相关的数据,松散的存储不利于使用,存入数组中受下标限制又必须有固定的顺序,而对象可以自定义名称存储一系列无序列表的相关数据什么是对象?现实生活中的对象:万物皆可对象,对象是一个具体的事物,一个具体的事物就会有行为和特征举例:一部车,一...
继续阅读 »

为什么要有对象?

  • 如果有一组相关的数据,松散的存储不利于使用,存入数组中受下标限制又必须有固定的顺序,而对象可以自定义名称存储一系列无序列表的相关数据

什么是对象?

现实生活中的对象:万物皆可对象,对象是一个具体的事物,一个具体的事物就会有行为和特征

举例:一部车,一个手机

车是一类事物。,门口停的那辆车才是对象

特征:红色、四个轮子

行为:驾驶、刹车

  • JavaScript 中的对象:
  1. JavaScript 中的对象其实就是生活中对象的一个抽象
  2. JavaScript 的对象是无序属性的集合
  • 其属性可以包含基本值、对象或函数。对象就是一组没有顺序的值。我们可以把 JavaScript 中的对象想象成键值对,其中值可以是数据和函数
  • 对象的行为和特征:
  1. 特征---在对象中用属性表示
  2. 行为---在对象中用方法表示

对象字面量(用字面量创建对象)

  • 创建一个对象最简单的方法是使用对象字面量赋值给变量。类似数组
  • 对象字面量语法:{}
  • 内部可以存放多条数据,数据与数据之间用逗号分隔,最后一个后面不要加逗号
  • 每条数据都是有属性名和属性值组成,键值对写法: k : v
  • k:属性名
  • v:属性值,可以实任意类型的数据,比如简单类型数据、函数、对象
var obj = {
k:v,
k:v,
k:v,
};

区分属性和方法

  • 属性:对象的描述性特征,一般是名词,相当于定义在对象内部的变量
  • 方法:对象的行为和功能,一般是动词,定义在对象中的函数

调用对象内部属性和方法的语法

  • 用对象的变量名打点调用某个属性名,得到属性值
  • 在对象内部用 this 打点调用属性名。this 替代对象
  • 用对象的变量名后面加 [] 调用,[] 内部是字符串格式的属性名
  • 调用方法时,需要在方法名后加 () 执行
/* 
现实生活中:万物皆对象 对象是一个具体事物 看得见摸得着的实物

对象是一组无序的相关属性和方法的集合 所有事物的是对象

对象是由属性和方法组成的
属性:事物的特征 在对象中用属性来表示(常用名词)
方法:事物的行为 在对象中用方法来表示(常用动词)

对象的字面量:就是花括号{} 里面包含了表达这个具体实物(对象)的属性和方法
*/
//创建一个空的对象
var obj = {
uname:'张三',
age:'男',
sayHi: function () {
console.log('Hi!');
console.log(this.uname + "向你说您好");
}
}
// 1.我们在创建对象时我们采用键值对的形式 键 属性名 : 属性 属性值
// 2.多个属性或者方法中间用逗号隔开
// 3.方法冒号后面跟的是一个匿名函数

// 使用对象
// 1)调用对象的属性 我们采取 对象名.属性名
console.log(obj.uname);
// 2)调用对象也可以 对象名['属性名']
console.log(obj['age']);
// 3)调用对象的方法 对象.方法名
obj.sayHi();


更改对象内部属性和方法的语法

  • 更改属性的属性值方法:先调用属性,再等号赋值
obj.age = 19;
  • 增加新的属性和属性值:使用点语法或者 [] 方法直接定义新属性,等号赋值
obj.height = 180;
  • 删除一条属性:使用一个 delete 关键字,空格后面加属性调用
delete obj.sex;

new Object() 创建对象

  • object() 构造函数,是一种特殊的函数。主要用来再创建对象时初始化对象,即为对象成员变量赋值初始值,总与 new 运算符一起使用在创建对象的语句中
  1. 构造函数用于创建一类对象,首字母要大写
  2. 构造函数要和 new 一起使用才有意义
// 利用new object 创建对象
var obj = new Object();//创建了一个空对象
obj.name = '张三';
obj.age = 18;
obj.sex = '男';
obj.sayHi = function() {
console.log('Hi~');
}
//1.我们是利用等号赋值的方法给对象 属性和方法 赋值
//2.每个 属性和方法 用分号结束

// 调用
console.log(obj.name);
console.log(obj['sex']);
obj.sayHi();


new 在执行时会做四件事情

  • new 会在内存中创建一个新的空对象
  • new 会让 this 指向这个新的对象
  • 执行构造函数 目的 :给这个新对象加属性和方法
  • new 会返回这个新的对象

工厂 函数创建对象

  • 如果要创建多个类似的对象,可以将 new Object() 过程封装到一个函数中,将来调用函数就能创建一个对象,相当于一个生产对象的函数工厂,用来简化代码
// 我们为什么需要使用函数
// 就是因我们前面两种创建对象的方式一次只能创建一次对象
var ldh = {
uname: '刘德华',
age: 55,
sing = function() {
console.log('冰雨');
}
}
var zxy = {
uname: '张学友',
age: 58,
sing = function() {
console.log('李香兰');
}
}
// 因为我们一次创建一个对象 里面有很多的属性和方法是大量相同的 我们只能复制
// 因此我们可以利用函数的方法 重复这些相同的代码
// 又因为这个函数不一样 里面封装的不是普通代码 而是对象
// 函数 可以把我们对象里面一些相同的属性和方法抽象出来封装到函数里面

用 工厂方法 函数创建对象

function createStar(uname, age, sex) {
//创建一个空对象
var Star = new Object();
//添加属性和方法,属性可以接受参数的值
Star.name = uname;
Star.age = age;
Star.sex = sex;

Star.sing = function(sang) {
console.log(sang);
}
//将对象做为函数的返回值
return Star;
}

var p1 = createStar("张三",18,"男");

自定义构造函数

  • 比工厂方法更加简单
  • 自定义一个创建具体对象的构造函数,函数内部不需要 new 一个构造函数的过程,直接使用 this 代替对象进行属性和方法的书写,也不需要 return 一个返回值
  • 使用时,利用 new 关键字调用自定义的构造函数即可
  • 注意:构造函数的函数名首字母需要大写,区别于其他普通函数名
// 利用构造函数创建对象
// 我们需要创建四大天王的对象 相同的属性: 名字 年龄 性别 相同的方法 : 唱歌
// 构造函数的语法格式
/*
function 构造函数名() {
this.属性 = 值;
this.方法 = fucntion() {}
}
// 调用构造函数
new 构造函数名();
*/

function Star(uname, age, sex) {
this.name = uname;
this.age = age;
this.sex = sex;

this.sing = function(sang) {
console.log(sang);
}
}
var ldh = new Star('刘德华', 18, '男');
console.log(typeof ldh);//object
console.log(ldh.name);
console.log(ldh.age);
console.log(ldh.sex);
ldh.sing('冰雨');
// 1.构造函数首字母必须大写
// 2.构造函数不需要return就能返回结果
// 3.调用函数返回的是一个对象
var zxy = new Star('张学友', 29, '男')
console.log(zxy);
// 4.我们调用构造函数必须使用new

对象遍历

  • for in 循环也是循环的一种,专门用来遍历对象,内部会定义一个 k 变量,k 变量在每次循环时会从第一个开始接收属性名,一直接收到最后一个属性名,执行完后会跳出循环。
  • 简单的循环遍历:输出每一项的属性名和属性值
//循环遍历输出每一项
for(var k in obj){
console.log(k + "项的属性值" + obj[k]);
}

案例:

//遍历对象
var obj = {
uname: '王二狗',
age: 18,
sex: '男'
}
console.log(obj.uname);
console.log(obj.age);
console.log(obj.sex);
//但是一个一个输出很累

// 因此我们引出 for...in...语句 --用于对数组或者对象的属性进行循环操作

/*
基本格式:
for (变量 in 对象) {

}

*/
for (k in obj) {
console.log(k); //k变量输出 得到的是属性名
console.log(obj[k]); //obj[k] 输出对象各属性的属性值 切记不要用obj.k 那样就变成输出 k 属性名的属性值了 ---!!!:k是变量不加''
}

简单类型和复杂类型的区别

  • 我们已经学过简单类型数据和一些复杂类型的数据,现在来看一下他们之间的区别有哪些
  • 基本类型又叫做值类型,复杂类型又叫做引用类型
  • 值类型:简单数据类型,基本数据类型,在存储时,变量中存储的是值本身,因此叫做值类型
  • 引用类型:复杂数据类型,在存储时,变量中存储的仅仅是地址(引用),因此叫做引用数据类型

堆和栈

  • JavaScript 中没有堆和栈的概念,此处我们用堆和栈来讲解,目的是方便理解和方便以后的学习
  • 堆栈空间分配区别
  1. 栈(操作系统):由操作系统自动分配释放,存放函数的参数值,局部变量的值相等
  2. 堆(操作系统):存储复杂类型(对象),一般由程序员分配释放,若程序员不释放,有垃圾回收机制回收

简单数据类型(基本类型)在内存中的存储

变量中如果存储的是简单类型的数据,那么变量中存储的是值本身,如果将变量赋值给另一个变量,是将内部的值赋值一份给了另一个变量,两个变量之间没有联系,一个变化,另一个不会同时变化

var a = 5;
var b = a; //将 a 内部存储的数据 5 复制了一份
a = 10;
console.log(a);
console.log(b);
// 因此 a 和 b 发生改变,都不会互相影响


复杂数据类型(引用类型)在内存中的存储

如果将复杂数据赋值给一个变量,复杂类型的数据会在内存中创建一个原型,而变量中存储的是指向对象的一个地址,如果将变量赋值给另一个变量,相当于将地址复制一份给了新的变量,两个变量的地址相同,指向的是同一个原型,不论通过哪个地址更改了原型,都是在原型上发生的更改,两个变量下次访问时,都会发生变化

// 复杂数据类型
var p1 = {
name: "zs",
age: 18,
sex: "male"
}
var p = p1; //p1 将内部存储的指向对象原型的地址复制给了 p
// 两个变量之间是一个联动的关系,一个变化,会引起另一个变化
p.name = "ls";
console.log(p);
console.log(p1);

// 数组和函数存储在变量中时,也是存储的地址
var arr = [1,2,3,4];
var arr2 =arr;
arr[4] = 5;
console.log(arr);
console.log(arr2);

内置对象

  • JavaScript 包含:ECMA DOM BOM
  • ECMAscript 包含:变量、数据、运算符、条件分支语句、循环语句、函数、数组、对象···
  • JavaScript 的对象包含三种:自定义对象 内置对象 浏览器对象
  • ECMAscript 的对象:自定义对象 内置对象
  • 使用一个内置对象,只需要知道对象中有哪些成员,有什么功能,直接使用
  • 需要参考一些说明手册 W3C / MDN

MDN

Mozilla 开发者网络(MDN) 提供有关开放网络技术(Open Web)的信息,包括 HTML、CSS 和 万维网 HTML5 应用的API

如何学习一个方法?

  1. 方法的功能
  2. 参数的意义和类型
  3. 返回值意义和类型
  4. demo 进行测试

Math 对象

  • Math 对象它具有数学常数和函数的属性和方法,我们可以直接进行使用
  • 根据数学相关的运算来找 Math 中的成员(求绝对值,取整)

演示:

Math.PI圆周率
Math.random()生成随机数
Math.floor()/Math.ceil()向下取整/向上取整
Math.round()取整,四舍五入
Math.abs()绝对值
Math.max()/Math.min()求最大和最小值
Math.sin()/Math.cos()正弦/余弦
Math.power()/Math.sqrt()求指数次幂/求平方根

Math.random()

如何求一个区间内的随机值

Math.random()*(max_num - min_num) + min_num

Math.max()/Math.min()

// Math数学对象 不是一个构造函数 所以我们不需要用new来调用 而是直接使用里面的属性和方法即可
console.log(Math.PI); //一个属性值 圆周率
console.log(Math.max(99, 199, 299)); //299
console.log(Math.max(-10, -20, -30)); //-10
console.log(Math.max(-10, -20, '加个字符串')); //NaN
console.log(Math.max()); //-Infinity
console.log(Math.min(99, 199, 299)); //99
console.log(Math.min()); //Infinity

创建数组对象的第二种方式

字面量方式

new Array() 构造函数方法

// 字面量方法
// var arr = [1,2,3];

// 数组也是对象,可以通过构造函数生存
//空数组
var arr = new Array();
//添加数据,可以传参数
var arr2 = new Array(1,2,3);
var arr3 = new Array("zs","ls","ww");
console.log(arr);
console.log(arr2);
console.log(arr3);

// 检测数组的数据类型
console.log(typeof(arr));//object
console.log(typeof(arr2));//object
console.log(typeof(arr3));//object

由于 object 数据类型的范围较大,所以我们需要一个更精确的检测数据类型的方法

  • instanceof 检测某个实例是否时某个对象类型
var arr = [1,2,3];
var arr1 = new Array(1,2,3)
var a = {};
// 检测某个实例对象是否属于某个对象类型
console.log(arr instanceof Array);//true
console.log(arr1 instanceof Array);//true
console.log(a instanceof Array);//true

function fun () {
console.log(1);
}
console.log(fun instanceof Function);//true

数组对象的属性和方法

toString()

  • toString() 把数组转换成字符串,逗号分隔每一项
// 字面量方法
var arr = [1,2,3,4];

// toString() 方法:可以转字符串
console.log(arr.toString());//1,2,3,4

数组常用方法

首尾数据操作:

  • push() 在数组末尾添加一个或多个元素,并返回数组操作后的长度
// 字面量方法
var arr = [1,2,3,4];

// 首尾操作方法
// 尾推,参数是随意的,有一个或者多个
console.log(arr.push(5,6,7,8)); //8(数组长度)
console.log(arr);//[1,2,3,4,5,6,7,8]
console.log(arr.push([5,6,7,8])); //5(数组长度)
console.log(arr);//[1,2,3,4,Array(4)]
  • pop() 删除数组最后一项,返回删除项
// 字面量方法
var arr = [1,2,3,4];

//尾删,删除最后一项数据
// 不需要传参
console.log(arr.pop());//4(被删除的那一项数据)
console.log(arr);//[1,2,3]
  • shift() 删除数组第一项,返回删除项
// 字面量方法
var arr = [1,2,3,4];

// 首删,删除第一项数据,不需要传参
console.log(arr.shift());//1
console.log(arr);//[2,3,4]
  • unshift() 在数组开头添加一个或多个元素,并返回数组的长度
// 字面量方法
var arr = [1,2,3,4];

// 首添,参数与 push 方法类似
console.log(arr.unshift(-1,0));//6
console.log(arr);//[-1,0,1,2,3,4]


案例:将数组的第一项移动到最后一项

// 字面量方法
var arr = [1,2,3,4];

// 将数组的第一项移动到最后一项
// 删除第一项
// 将删除的项到最后一项
arr.push(arr.shift());
console.log(arr);//[2,3,4,1]
arr.push(arr.shift());
console.log(arr);//[3,4,1,2]
arr.push(arr.shift());
console.log(arr);//[4,1,2,3]
arr.push(arr.shift());
console.log(arr);//[1,2,3,4]


数组常用方法

合并和拆分:

concat()

  • 将两个数组合并成一个新的数组,原数组不受影响。参数位置可以是一个数组字面量、数组变量、零散的值
// 字面量方法
var arr = [1,2,3,4];
// 合并方法
// 参数:数组 数组的变量 零散的值
// 返回值:一个新的拼接后的数组
var arr1 = arr.concat([5,6,7]);
console.log(arr);//[1,2,3,4]
console.log(arr1);//[1,2,3,4,5,6,7]

slice(start,end)

  • 从当前数组中截取一个新的数组,不影响原来的数组,返回一个新的数组,包含从 start 到end (不包括该元素)的元素
  • 参数区分正负,正值表示下标位置,负值表示从后面往前数第几个位置,参数可以只传递一个,表示从开始位置截取到字符串结尾
// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10];

// 拆分方法
// 参数为正
var arr1 = arr.slice(3,7);//[4,5,6,7]
// 参数为负数
var arr1 = arr.slice(-7,-1);//[5,6,7,8,9]
// 参数出现问题的情况
var arr1 = arr.slice(-1,-7);//[] 取不到会出现空值
// 只书写一个参数
var arr1 = arr.slice(-7);//[4,5,6,7,8,9,10] 从倒数第七个开始取
var arr1 = arr.slice(8);//[9,10] 从下标为8的数开始取


删除,插入,替换:

splice(index,howmany,element1,element2,...)

用于插入、删除或替换数组的元素

index:删除元素的开始位置

howmany:删除元素的个数,可以是0

element1,element2:要替换的新数据

// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10];

// 拆分方法
// 参数为正
var arr1 = arr.slice(3,7);//[4,5,6,7]
// 参数为负数
var arr1 = arr.slice(-7,-1);//[5,6,7,8,9]
// 参数出现问题的情况
var arr1 = arr.slice(-1,-7);//[] 取不到会出现空值
// 只书写一个参数
var arr1 = arr.slice(-7);//[4,5,6,7,8,9,10] 从倒数第七个开始取
var arr1 = arr.slice(8);//[9,10] 从下标为8的数开始取


位置方法:

indexOf() 查找数据在数组中最先出现的下标

lastndexOf() 查找数据在数组中最后一次出现的下标

注意:如果没有找到返回-1

// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10,4,5];

// 查找某个元素在数组中从前往后第一次 出现位置的下标
console.log(arr.indexOf(4));//3 (数字4的下标)
// 查找某个元素在数组中从前往后最后一次出现位置的下标
console.log(arr.lastIndexOf(4));//10
console.log(arr.lastIndexOf(11));//-1 (代表数组中不存在11这个数据)


排序方法:

倒序:reverse() 将数组完全颠倒,第一项变成最后一项,最后一项变成第一项

// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10];

// 数组倒序
console.log(arr.reverse());//[10,9,8,7,6,5,4,3,2,1]

从大到小排序:sort() 默认根据字符编码顺序,从大到小排序

如果想要根据数值大小进行排序,必须添加sort的比较函数参数

该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数具有两个参数 a 和 b,根据 a 和 b 的关系作为判断条件,返回值根据条件分为三个分支,整数、负数、0:

返回值是负数-1:a 排在 b 前面

返回值是整数1:a 排在 b 后面

返回值是0:a 和 b 的顺序保持不变

人为控制的是判断条件

// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10,20,30];
// 排序,默认按照字符编码顺序来排序
arr.sort();
console.log(arr);//[1,10,2,20,3,30,4,5,6,7,8,9] (如果不添加函数)

// 添加一个比较函数
arr.sort(function(a,b) {
if (a > b) {
return -1;//表示 a 要排在 b 前面
} else if (a < b) {
return 1;//表示 a 要排在 b后面
} else {
return 0;;//表示 a 和 b 保持原样,不换位置
}
});
console.log(arr);//[30,20,10,9,8,7,6,5,4,3,2,1] (添加函数之后)
// 想要从小到大排序只要将函数 大于小于 号,反向即可


转字符串方法:将数组的所有元素连接到一个字符串中

join() 通过参数作为连字符将数组中的每一项用连字符连成一个完整的字符串

var arr = [1,2,3,4,5,6,7,8,9,10,20,30];

// 转字符串方法
var str = arr.join();
console.log(str);//1,2,3,4,5,6,7,8,9,10,20,30
var str = arr.join("*");
console.log(str);//1*2*3*4*5*6*7*8*9*10*20*30
var str = arr.join("");
console.log(str);//123456789102030


清空数组方法总结

var arr = [1,2,3,4,5,6,7,8,9,10,20,30];

// 方式1 推荐
arr = [];

// 方式2
arr.length = 0;

// 方式 3
arr.splice(0,arr.length);

基本包装类型

为了方便操作简单数据类型,JavaScript 还提供了特殊的简单类型对象:String 基本类型时没有方法的。

当调用 str.substring() 等方法的时候,先把 str 包装成 String 类型的临时对象,再调用 substring 方法,最后销毁临时对象

// 基本数据类型:没有属性值和方法
// 对象数据类型:有属性和方法
// 但是:字符串是可以调用一些属性和方法
var str = "这是一个字符串";
var str2 = str.slice(3,5);
console.log(str2);//个字

// 基本包装类型,基本类型的数据在进行一些特殊操作时,会暂时被包装成一个对象,结束后再被销毁
// 字符串也有一种根据构造函数创建方法
var str3 = new String("abcdef");
console.log(str);//这是一个字符串
console.log(str3);//Sring{"abcdef"}

// 模拟计算机的工作
var str4 = new String(str);
// 字符串临时被计算机包装成字符串对象
var str2 = str4.slice(3,5);
str4 = null;


字符串的特点

字符串是不可变的

// 定义一个字符串   
var a = "abc";
a = "cde";
// 字符串是不可变的,当 a 被重新赋值时,原来的值 "abc" 依旧在电脑内存中
// 在 JavaScript 解释器 固定时间释放内存的时候可能会被处理掉

由于字符串的不可变,在大量拼接字符串的时候会有效率问题

由于每次拼接一个字符串就会开辟一个空间去存储字符串

// 大量拼接字符串也效率问题
var sum = "";
for(var i = 1; i <= 10000000; i++) {
sum += i;
}
console.log(sum);

测试一下我们发现,浏览器转了一会才显示出来

因此在我们以后,不要大量用字符串拼接的方法,以后我们会有更好的方法替代


字符串属性

长度属性:str.length

字符串长度指的是一个字符串中所有的字符总数


字符串方法

indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置

  • 找到指定的字符串在原字符串中第一次出现的位置的下标。如果子字符串在原字符串中没有,返回值是 -1

concat() 方法用于连接两个或多个字符串

  • 参数比较灵活,可以是字符串、或者字符串变量、多个字符串
  • 生成的是一个新的字符串,原字符串不发生变化

split() 方法用于把一个字符串分割成字符串数组(和数组中的 join() 方法是对应的)

  • 参数部分是割符,利用分割符将字符串分割成多个部分,多个部分作为数组的每一项组成数组
  • 如果分割符是空字符串,相当于将每个字符拆分成数组中的每一项
// 定义一个字符串
var str = "这是一个字符串,abc, $%#";

// 长度属性
console.log(str.length);//18

// charAt() 返回指定的下标位置的字符
console.log(str.charAt(6));//串 (字符串对象是一种伪数组,所以需要从 0 开始数)

// indexOf() 返回子串在原始字符串中第一次出现位置的下标
console.log(str.indexOf("字"));//4
console.log(str.indexOf("字符串"));//4
console.log(str.indexOf("字 符串"));//-1

// concat() 字符串拼接
var str2 = str.concat("哈哈哈","普通");
console.log(str);//这是一个字符串,abc, $%#
console.log(str2);//这是一个字符串,abc, $%#哈哈哈普通

// split() 分割字符串成一个数组
var arr = str.split("")//一个一个字符分割
console.log(arr);
var arr = str.split(",")//按逗号进行分割
console.log(arr);

// 字符串内容倒置
var arr = str.split("")//一个一个字符分割
arr.reverse();
strn = arr.join("");
console.log(strn);
// 用连续打点方式化简
var arr = str.split("").reverse().join("")
console.log(arr);


toLowerCase() 把字符串转换为小写

toUpperCase() 把字符串转换为大写

  • 将所有的英文字符转为大写或者小写
  • 生成的是新的字符串,原字符串不发生变化
// 大小写转换
var str1 = str.toUpperCase();
console.log(str);//这是一个字符串,abc, $%#
console.log(str1);//这是一个字符串,ABC, $%#
var str2 = str1.toLowerCase();
console.log(str2);//这是一个字符串,abc, $%#
console.log(str1);//这是一个字符串,ABC, $%# --字符串本身不会发生改变


截取字符串的三种方法

slice() 方法可以提取字符串的某个部分,并以新的字符串返回被提取的部分

  • 语气:slice(start,end)
  • 从开始位置截取到结束位置(不包括结束位置)的字符串
  • 参数区分正负,正值表示下标位置,负值表示从后面往前数第几个位置,参数可以只传递一个,表示从开始位置截取到字符串结尾

substr() 方法可在字符串中抽取从 start 下标开始的指定数目的字符

  • 语法:substr(start,howmany)
  • 从开始位置截取到指定长度的字符串
  • start 参数区分正负。正值表示下标位置,负值表示从后往前数第几个位置
  • howmany 参数必须为正数,也可以不写,不写表示从 start 截取到最后

substring() 方法用于提取字符串中介于两个指定下标之间的字符

  • 语法:substring(start,end)
  • 参数只能为正数
  • 两个参数都是指代下标,两个数字大小不限制,执行方法之前会比较一下两个参数的大小,会用小的数当做开始位置,大的当作结束位置,从开始位置截取到结束位置但是不包含结束位置
  • 如果不写第二个参数,从开始截取到字符串结尾
// 截取字符串:三种
// slice(start,end) 从开始位置截取到结束位置,但是不包含结束位置
var str1 = str.slice(3,7);
console.log(str1);//个字符串
var str1 = str.slice(-7);
console.log(str1);//, $%#

// substr() 方法可在字符串中抽取从 start 下标开始的指定数目的字符
var str2 = str.substr(6);
console.log(str2);//串,abc, $%#
var str2 = str.substr(6,3);
console.log(str2);//串,a

// substring() 参数必须为整数 小的数当做开始位置,大的当作结束位置
var str3 = str.substring(3,7);
console.log(str3);//个字符串

注意:如果参数取小数会自动省略小数部分

原文链接:https://zhuanlan.zhihu.com/p/366886609

收起阅读 »

JavaScript 函数

为什么要有函数?如果要在多个地方求某个数的约数个数,应该怎么做函数的概念函数(function),也叫作功能、方法,函数可以将一段代码一起封装起来,被封装起来的函数具备某一项特殊的功能,内部封装的一段代码作为一个完整的结构体,要执行就都执行,要不执行就都不执行...
继续阅读 »

为什么要有函数?

  • 如果要在多个地方求某个数的约数个数,应该怎么做


函数的概念

  • 函数(function),也叫作功能、方法,函数可以将一段代码一起封装起来,被封装起来的函数具备某一项特殊的功能,内部封装的一段代码作为一个完整的结构体,要执行就都执行,要不执行就都不执行。
  • 函数的作用就是封装一段代码,将来可以重复使用

函数声明

  • 函数声明又叫函数定义,函数必须先定义然后才能使用
  • 如果没有定义函数直接使用,会出现一个引用错误
  • 函数声明语法:
function 函数名 (参数) {
封装的结构体;
}

特点:函数声明的时候,函数体并不会执行,只有当函数被调用的时候才会执行

函数调用

  • 调用方法:函数名();
  • 函数调用也叫作函数执行,调用时会将函数内部封装的所有的结构体的代码立即执行
  • 函数内部语句执行的位置,与函数定义的位置无关,与函数调用位置有关
  • 函数可以一次调用,多次执行

函数的参数1

  • 我们希望函数执行结果不是一成不变的,可以根据自定义的内容发生一些变化
  • 函数预留了一个接口,专门用于让用户自定义内容,使函数发生一些执行效果变化
  • 接口:就是函数的参数,函数参数的本质就是变量,可以接收任意类型的数据,导致函数执行结果根据参数不同,结果也不同
  • 一个函数可以设置 0 个或者多个参数,参数之间用逗号分隔

案例:累加求和函数

        // 函数:封装了一段可以重复调用执行的代码块,通过代码块可以实现大量代码的重复使用

// 1、声明一个累加求和函数

// num1~num2之间所有数之和
function getSum(num1,num2) {
var sum = 0;
for (var i = num1; i <= num2; i++) {
sum += i;
}
console.log(sum);
}

// 2、调用函数
getSum(1,100);
getSum(11,1100);
getSum(321,1212);

函数的参数2

  • 函数的参数根据书写位置不同,名称也不相同
  • 形式参数:定义的 () 内部的参数,叫做形式参数,本质是变量,可以接收实际参数传递过来的数据。简称形参
  • 实际参数:调用的 () 内部的参数,叫做实际参数,本质就是传递的各种类型的数据,传递给每个形参,简称实参
  • 函数执行过程,伴随传参的过程

函数的参数优点

  • 不论使用自己封装的函数,还是其他人封装的函数,只需要知道传递什么参数,执行什么功能,没必要知道内部的结构是什么
  • 一般自己封装的函数或者其他人封装的函数需要有一个 API 接口说明,告诉用户参数需要传递什么类型的数据,实现什么功能

函数的返回值

  • 函数能够通过参数接收数据,也能够将函数执行结果返回一个值
  • 利用函数内部的一个 return 的关键字设置函数的返回值
  • 作用 1 :函数内部如果结构体执行到一个 return 的关键字,会立即停止后面代码的执行
  • 作用 2 : 可以在 return 关键字后面添加空格,空格后面任意定义一个数据字面量或者表达式,函数在执行完自身功能之后,整体会被 return 矮化成一个表达式,表达式必须求出一个值继续可以参加程序,表达式的值就是 return 后面的数据

案例:求和函数

var num1 = Number(prompt("请输入第一个数:"));
var num2 = Number(prompt("请输入第二个数:"));
function sum(a,b) {
return a + b;
}
console.log(sum(num1,num2));

函数的返回值应用

  • 函数如果有返回值,执行结果可以当成普通函数参与程序
  • 函数如果有返回值,可以作为一个普通数据赋值给一个变量,甚至赋值给其他函数的实际参数
  • 注意:如果函数没有设置 return 语句,那么函数有默认的返回值 undefined ; 如果函数使用 return 语句,但是 return 后面没有任何值,那么函数的返回值也是 undefined
// 1、return 终止函数
function getSum(num1, num2) {
return num1 + num2;
console.log('return除了返回值还起到终止函数的作用,所以在return后面的代码均不执行!');
}
console.log(getSum(10, 20));

// 2、return 只能返回一个值
function fn(num1,num2) {
return num1, num2; //返回的结果是最后一个值
}
console.log(fn(10, 20));

// 3、 我们求任意两个数 加减乘除 的结果
function getResult(num1, num2) {
return ['求和:' + (num1 + num2), '求差:' + (num1 - num2), '求积:' + (num1 * num2), '求商:' + (num1 / num2)];
}
re = getResult(10, 20);
console.log(re);
// 想要输出多个值可以利用数组
// 4、我们的函数如果有return 则返回的是 return后面的值 如果函数没有 return 则返回undefined

函数表达式

  • 函数表达式是函数定义的另外一种方式
  • 定义方法:就是将函数的定义、匿名函数赋值给一个变量
  • 函数定义赋值给一个变量,相当于将函数整体矮化成了表达式
  • 匿名函数:函数没有函数名
  • 调用函数表达式,方法是给变量名加 () 执行,不能使用函数名加 () 执行
// 函数的两种声明方式
// 1、利用函数关键字自定义函数(命名函数)
function fn() {

}
fn();
// 2、函数表达式(匿名函数)
// var 变量名 = function() {};
var fun = function(aru) {
console.log('我是函数表达式');
console.log(aru);
}
fun('我是默默!');
// (1)fun是变量名 不是函数名
// (2)函数表达式声明方式跟声明变量差不多,只不过变量里面存的是值 而 函数表达式里面存的是函数

函数数据类型

  • 函数是一种独特的数据类型 function -- 是 object 数据类型的一种,函数数据类型
  • 由于函数是一种数据类型,可以参与其他程序
  • 例如,可以把函数作为另外一个函数的参数,在另一个函数中调用
  • 或者,可以把函数作为返回值从函数内部返回
// 函数是一种数据类型,可以当成其他函数的参数
setInterval(function() {
console.log(1);
},1000)
//每隔 1s 输出一个 1

arguments 对象

  • JavaScript 中,arguments 对象是比较特别的一个对象,实际上是当前函数的一个内置属性。也就是说所有函数都内置了一个 arguments 对象,arguments 对象中存储了传递的所有实参。arguments 是一个伪数组,因此及可以进行遍历
  • 函数的实参个数和形参个数可以不一致,所有的实参都会存储在函数内部的 arguments 类数组对象中
/*
当我们不确定有多少个参数传递的时候 可以用arguments来获取 在JS中 arguments其实是当前函数的
一个内置对象 所有函数都内置了一个arguments对象 arguments对象中存储了传递的所有实参
*/
// arguments的使用
function fn() {
console.log(arguments); //里面存储了所有的实参
}
fn(1, 2, 3);

/*
arguments展示形式是一个伪数组,因此可以进行遍历,伪数组有如下特点:
具有length属性
按照索引方式存储数据
不具有数组的 push pop 等方法
*/

案例:利用 arguments 求一组数最大值

function getMax() {
var max = arguments[0];
var arry = arguments;
for (var i = 0; i < arry.length; i++) {
if (arry[i] > max) {
max = arry[i];
}
}
return max;
}

console.log(getMax(1, 2, 5, 11, 3));
console.log(getMax(1, 2, 5, 11, 3, 100, 111));
console.log(getMax(1, 2, 5, 11, 3, 1212, 22, 222, 2333));


函数递归

  • 函数内部可以通过函数名调用函数自身的方式,就是函数递归现象
  • 递归的次数太多容易出现错误:超出计算机的计算最大能力
  • 更多时候,使用递归去解决一些数学的现象
  • 例如可以输出斐波那契数列的某一项的值
// 函数,如果 传入的参数1,返回1,如果传入的是 1 以上的数字,让他返回参数 + 函数调用上一项
function fun (a) {
if (a === 1) {
return 1;
} else {
return a + fun(a - 1);
}
}
// 调用函数
console.log(fun(1));
console.log(fun(2));
console.log(fun(3));
console.log(fun(100));
// 这样我们就用递归做出了 n 以内数累加求和的函数

案例:输出斐波那契数列任意项

// 斐波那契数列(每一项等于前两项之和 1,1,2,3,5,8,13,21,34,55 ···)
// 参数:正整数
// 返回值:对应的整数位置的斐波那契数列的值
function fibo(a) {
if (a === 1 || a === 2) {
return 1;
} else {
return fibo(a - 1) + fibo(a - 2);
}
}
console.log(fibo(1));
console.log(fibo(2));
c