注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

JAVA中线程间通信的小故事

从掘金的大佬中偷学到一个技能,为了提升知识提炼与字面表达能力,斟酌贴代码的篇幅,尽量用文字表达清楚技术知识的本质。(简单点就是“多说人话”) 正文开始! 前情提要 关于“线程间通信”的这个叫法,没查到比较官方的定义,也许它是一个通俗词吧。下面是基于笔...
继续阅读 »

从掘金的大佬中偷学到一个技能,为了提升知识提炼与字面表达能力,斟酌贴代码的篇幅,尽量用文字表达清楚技术知识的本质。(简单点就是“多说人话”)



正文开始!


前情提要


关于“线程间通信”的这个叫法,没查到比较官方的定义,也许它是一个通俗词吧。下面是基于笔者个人理解所总结出的定义,重在严谨。


不同线程之间通过资源状态同步相互影响彼此的执行逻辑。


线程间的基本通信可以划分为启动与结束,线程等待与唤醒线程。在JAVA中他们都对应了固定的API与固定用法,是还存在其他的通信方式,但本文不做展开。


一、线程停止的性格差异


张三与小明的故事


1. thread.stop() 愚蠢且粗鲁的张三



立即强制停止某个线程的执行,中断代码执行指令,并退出线程。被停止的线程无法安全的进行善后处理。



代入角色举个栗子,愚蠢的张三安排他儿子小明烧开水。小明很聪明,已经牢记了烧水的步骤,拿锅接水,开火,水沸关火。


小明很听话,便进入厨房开始了忙碌。


半分钟后愚蠢张三的电话突然响了 ,有关部门通知马上会停止燃气供应,张三意识到小明不能再烧水了,决定停止小明的工作。


此时小明还在接水,但愚蠢的张三假装没看见,他一把将小明拉出了厨房。小明内心非常懵逼,但是他有苦说不出。


他们离开后,厨房的水还在哗哗的流,最后淹了厨房...


总结:完全不需要考虑善后的线程才能用stop()


2. thread.interrupt() 温柔的张三



通知某个线程中断当前在执行的任务,被中断的线程可以先进内部善后处理,再退出线程,或者不退出。



代入角色,还是上面的栗子。这不过这次张三并没后直接抱走小明,而是大声告诉小明,该离开厨房了。


小明此时有两种选择,第一中是丢下手上的事情,马上走出厨房,让水继续哗哗的流。第二种是关闭水龙头再走出厨房。


如果你是小明,你准备怎么做?


总结:中断线程用thread.interrupt()就对了,最起码温柔


3.Thread.interrupted() 可怜的小明,正确答案只能获取一次



每次在被中断后的第一次调用时返回true,之后在没有被在此中断前都一直返回false



代入角色,还是上面的栗子。小明知道张三有可能会通知他出现了例外情况,所以小明在每一个关键步骤前检查是否需要停止,如果发现被叫停就马上进行善后工作,离开厨房。因为他知道他基本只有一次机会。


总结:用于简单任务的中断判断,如果无法衡量是否简单,那就没必要用,除非你对中断次数是非常敏感的。


4.isInterrupted() 快乐的小明,获取正确答案不限次数



只要被中断过一次,之后获取到的状态都是true



小明的快乐你懂了吗


总结:小明的快乐你懂了吗


5.Thread.sleep(x)小明在厨房睡着了



当前线程进入挂起状态,挂起的过程中可能会被中断,被中断时则会被catch (InterruptedException e)捕获,可以进行善后处理,选择是否退出。



代入角色,没错小明真睡着了!如果温柔的张三大声告诉小明离开厨房,小明被惊醒后要是不犯迷糊就会有序的停止当且阶段的工作,比如关闭水龙头,然后离开厨房。


要是小明犯迷糊呢?小明一般不会犯迷糊


因为他知道


犯迷糊的小明会被张三暴揍!


总结:Thread.sleep(x)后需要捕获的异常catch (InterruptedException e),理解为例外更好些,因为它并不代表程序错误


二、等待的细节与唤醒的差别


小明与小芳的故事


1.wait() 小明的素质



当需要访问的资源不满足条件时,选择进入等待区。直到被唤醒后重新竞争锁,获取锁后接着之前的逻辑继续执行



小明和小芳一起看电视,小明先抢到了遥控器,他想看足球比赛,切到了足球频道,球员A准备射门,但是小明点的啤酒还没到,小明看比赛必须得有啤酒。


如果小明没礼貌,那么他就暂停电视,把遥控器坐在屁股下面,一直盯着电视,直到啤酒来了,小明恢复电视,继续看。


如果小明有礼貌,那么他就先让出了遥控器,小芳拿到遥控器开心的放起了甄嬛传。 小明呢则开始发呆(细节1),直到(细节2)有人告诉他啤酒来了,他便重新(细节3)去抢遥控器,抢到后遥控器后起到足球频道,电视机画面直接从球员A准备射门处(细节3)开始播放。


如果小明发呆的时候出现了意外怎么办呢?不用担心这会立即叫醒小明,他可以自主选择下一步怎么办。


这就是为什么wait()时也需要catch (InterruptedException e)


总结:用wait()让出锁和资源,减少兄弟线程的等待时间


2.notify() 幸运女神



由当前作为锁的对象随机从与当前锁相关且进入wait()的线程中唤醒一个,被唤醒的线程重新进行锁的竞争



从上帝视角看,当资源只能满足一个线程使用时,使用notify(),能节约不必要的额外开销。


而被选中的那个线程就是唯一的幸运儿~


3.notifyAll() 阳光普照



由当前作为锁的对象唤醒所有与当前锁相关且进入wait()的线程,被唤醒的线程重新进行锁的竞争



如果没有特殊考虑,为了世界和平,通常你应当唤醒所有进入等待的线程。


三、join 快来绑一绑timing



将多个并行线程任务,连成一个串行的线程任务,带头线程不管成功还是失败,跟随线程都会立即执行



再举个栗子吧,张三安排小芳做饭,并让小明负责打酱油。


接下来的情况就会变得非常有趣。


小芳炒完菜要出锅的时候需要酱油,但是此时小明还没有买回酱油。小芳便使用join大法将自己绑定到了小明买回酱油这件任务的结束timing上。


结果呢?如果小明顺利买回了酱油,小芳使用酱油提鲜后装盘出锅。


如果小明路上摔跤了,导致提前退出了任务。小芳则使用空酱油后装盘出锅


这不怪小芳,她哪知道小明没有带回酱油呢。


总结: join()之后应该在此判断条件是否满足,避免拿到NPE


四、yield



稍微让出一点时间片给同级别线程,又立即恢复自己的执行。



像是快速wait()(不用别人叫的那种),再快速自动恢复


缺少科学分析验证,不敢多说~    




END

收起阅读 »

一文带你实现遍历android内存模块

1.Android内存模块遍历原理 在android系统上,要遍历app进程中的内存模块数据,必须用到proc文件系统。 proc它是由linux内核挂着到内存中,它提供内核配置、进程状态输出等功能。 用adb命令方式可以进行查看app进程中所有加载的模块...
继续阅读 »

1.Android内存模块遍历原理


在android系统上,要遍历app进程中的内存模块数据,必须用到proc文件系统。
proc它是由linux内核挂着到内存中,它提供内核配置、进程状态输出等功能。


用adb命令方式可以进行查看app进程中所有加载的模块信息。
cat /proc/%d/maps : cat是查看的意思, %d表示要查看的APP的进程pid


maps文件中显示出来的各个列信息解释:


第1列:模块内容在内存中的地址范围,以16进制显示。


第2列:模块内容在内存中的读取权限,r代表可读,w代表可写,x代表可执行,p代表私有,s代码共享。


第3列:模块内容对应模块文件中的偏移。


第4列:模块文件在文件系统中的主次设备号。


第5列:模块文件在文件系统中的节点号。


第6列:模块文件在文件系统中的路径。 image.png


2.android内存模块遍历实现



//存储模块信息的结构体
struct ProcMap {
void *startAddr;
void *endAddr;
size_t length;
std::string perms;
long offset;
std::string dev;
int inode;
std::string pathname;

bool isValid() { return (startAddr != NULL && endAddr != NULL && !pathname.empty()); }
};

//获取模块信息函数
bool getAPPMod(int pid)
{

ProcMap retMap;
char line[512] = {0};
char mapPath[128] = {0};

sprintf(mapPath, "/proc/%d/maps", pid);

FILE *fp = fopen(mapPath, "r");
if (fp != NULL)
{

while (fgets(line, sizeof(line), fp)) {

char tmpPerms[5] = {}, tmpDev[12] = {}, tmpPathname[455] = {};

sscanf(line, "%llx-%llx %s %ld %s %d %s",
(long long unsigned *) &retMap.startAddr,
(long long unsigned *) &retMap.endAddr,
tmpPerms, &retMap.offset, tmpDev, &retMap.inode, tmpPathname);

}
}

return true;

}

收起阅读 »

官方推荐 Flow 取代 LiveData,有必要吗?

前言打开Android架构组件页面,我们可以发现一些最新发布的jetpack组件,如Room,DataStore, Paging3,DataBinding 等都支持了FlowGoogle开发者账号最近也发布了几篇使用Flow的文章,比如:从...
继续阅读 »

前言

打开Android架构组件页面,我们可以发现一些最新发布的jetpack组件,如RoomDataStorePaging3,DataBinding 等都支持了Flow
Google开发者账号最近也发布了几篇使用Flow的文章,比如:从 LiveData 迁移到 Kotlin 数据流
看起来官方在大力推荐使用Flow取代LiveData,那么问题来了,有必要吗?
LiveData用得好好的,有必要再学Flow吗?本文主要回答这个问题,具体包括以下内容
1.LiveData有什么不足?
2.Flow介绍以及为什么会有Flow
3.SharedFlowStateFlow的介绍与它们之间的区别

本文具体目录如下所示:

1. LiveData有什么不足?

1.1 为什么引入LiveData?

要了解LiveData的不足,我们先了解下LiveData为什么被引入

LiveData 的历史要追溯到 2017 年。彼时,观察者模式有效简化了开发,但诸如 RxJava 一类的库对新手而言有些太过复杂。为此,架构组件团队打造了 LiveData: 一个专用于 Android 的具备自主生命周期感知能力的可观察的数据存储器类。LiveData 被有意简化设计,这使得开发者很容易上手;而对于较为复杂的交互数据流场景,建议您使用 RxJava,这样两者结合的优势就发挥出来了

可以看出,LiveData就是一个简单易用的,具备感知生命周期能力的观察者模式
它使用起来非常简单,这是它的优点,也是它的不足,因为它面对比较复杂的交互数据流场景时,处理起来比较麻烦

1.2 LiveData的不足

我们上文说过LiveData结构简单,但是不够强大,它有以下不足
1.LiveData只能在主线程更新数据
2.LiveData的操作符不够强大,在处理复杂数据流时有些捉襟见肘

关于LiveData只能在主线程更新数据,有的同学可能要问,不是有postValue吗?其实postValue也是需要切换到到主线程的,如下图所示:

这意味着当我们想要更新LiveData对象时,我们会经常更改线程(工作线程→主线程),如果在修改LiveData后又要切换回到工作线程那就更麻烦了,同时postValue可能会有丢数据的问题。

2. Flow介绍

Flow 就是 Kotlin 协程与响应式编程模型结合的产物,你会发现它与 RxJava 非常像,二者之间也有相互转换的 API,使用起来非常方便。

2.1 为什么引入Flow

为什么引入Flow,我们可以从Flow解决了什么问题的角度切入

  1. LiveData不支持线程切换,所有数据转换都将在主线程上完成,有时需要频繁更改线程,面对复杂数据流时处理起来比较麻烦
  2. RxJava又有些过于麻烦了,有许多让人傻傻分不清的操作符,入门门槛较高,同时需要自己处理生命周期,在生命周期结束时取消订阅

可以看出,Flow是介于LiveDataRxJava之间的一个解决方案,它有以下特点

  • Flow 支持线程切换、背压
  • Flow 入门的门槛很低,没有那么多傻傻分不清楚的操作符
  • 简单的数据转换与操作符,如 map 等等
  • 冷数据流,不消费则不生产数据,这一点与LiveData不同:LiveData的发送端并不依赖于接收端。
  • 属于kotlin协程的一部分,可以很好的与协程基础设施结合

关于Flow的使用,比较简单,有兴趣的同学可参阅文档:Flow文档

3. SharedFlow介绍

我们上面介绍过,Flow 是冷流,什么是冷流?

  • 冷流 :只有订阅者订阅时,才开始执行发射数据流的代码。并且冷流订阅者只能是一对一的关系,当有多个不同的订阅者时,消息是重新完整发送的。也就是说对冷流而言,有多个订阅者的时候,他们各自的事件是独立的。
  • 热流:无论有没有订阅者订阅,事件始终都会发生。当 热流有多个订阅者时,热流订阅者们的关系是一对多的关系,可以与多个订阅者共享信息。

3.1 为什么引入SharedFlow

上面其实已经说得很清楚了,冷流订阅者只能是一对一的关系,当我们要实现一个流,多个订阅者的需求时(这在开发中是很常见的),就需要热流
从命名上也很容易理解,SharedFlow即共享的Flow,可以实现一对多关系,SharedFlow是一种热流

3.2 SharedFlow的使用

我们来看看SharedFlow的构造函数

public fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
)
: MutableSharedFlow<T>

其主要有3个参数
1.replay表示当新的订阅者Collect时,发送几个已经发送过的数据给它,默认为0,即默认新订阅者不会获取以前的数据
2.extraBufferCapacity表示减去replayMutableSharedFlow还缓存多少数据,默认为0
3.onBufferOverflow表示缓存策略,即缓冲区满了之后Flow如何处理,默认为挂起

简单使用如下:

//ViewModel
val sharedFlow=MutableSharedFlow<String>()

viewModelScope.launch{
sharedFlow.emit("Hello")
sharedFlow.emit("SharedFlow")
}

//Activity
lifecycleScope.launch{
viewMode.sharedFlow.collect {
print(it)
}
}

3.3 将冷流转化为SharedFlow

普通flow可使用shareIn扩展方法,转化成SharedFlow

    val sharedFlow by lazy {
flow<Int> {
//...
}.shareIn(viewModelScope, WhileSubscribed(500), 0)
}

shareIn主要也有三个参数:

@param scope 共享开始时所在的协程作用域范围
@param started 控制共享的开始和结束的策略
@param replay 状态流的重播个数

started 接受以下的三个值:
1.Lazily: 当首个订阅者出现时开始,在scope指定的作用域被结束时终止。
2.Eagerly: 立即开始,而在scope指定的作用域被结束时终止。
3.WhileSubscribed: 这种情况有些复杂,后面会详细讲解

对于那些只执行一次的操作,您可以使用Lazily或者Eagerly。然而,如果您需要观察其他的流,就应该使用WhileSubscribed来实现细微但又重要的优化工作

3.4 Whilesubscribed策略

WhileSubscribed策略会在没有收集器的情况下取消上游数据流,通过shareIn运算符创建的SharedFlow会把数据暴露给视图 (View),同时也会观察来自其他层级或者是上游应用的数据流。
让这些流持续活跃可能会引起不必要的资源浪费,例如一直通过从数据库连接、硬件传感器中读取数据等等。当您的应用转而在后台运行时,您应当保持克制并中止这些协程。

public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
)

如上所示,它支持两个参数:

  • 1.stopTimeoutMillis 控制一个以毫秒为单位的延迟值,指的是最后一个订阅者结束订阅与停止上游流的时间差。默认值是 0 (立即停止).这个值非常有用,因为您可能并不想因为视图有几秒钟不再监听就结束上游流。这种情况非常常见——比如当用户旋转设备时,原来的视图会先被销毁,然后数秒钟内重建。
  • 2.replayExpirationMillis表示数据重播的过时时间,如果用户离开应用太久,此时您不想让用户看到陈旧的数据,你可以用到这个参数

4. StateFlow介绍

4.1 为什么引入StateFlow

我们前面刚刚看了SharedFlow,为什么又冒出个StateFlow?
StateFlow 是 SharedFlow 的一个比较特殊的变种,StateFlow 与 LiveData 是最接近的,因为:

  • 1.它始终是有值的。
  • 2.它的值是唯一的。
  • 3.它允许被多个观察者共用 (因此是共享的数据流)。
  • 4.它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的。

可以看出,StateFlowLiveData是比较接近的,可以获取当前的值,可以想像之所以引入StateFlow就是为了替换LiveData
总结如下:
1.StateFlow继承于SharedFlow,是SharedFlow的一个特殊变种
2.StateFlowLiveData比较相近,相信之所以推出就是为了替换LiveData

4.2 StateFlow的简单使用

我们先来看看构造函数:

public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)

1.StateFlow构造函数较为简单,只需要传入一个默认值
2.StateFlow本质上是一个replay为1,并且没有缓冲区的SharedFlow,因此第一次订阅时会先获得默认值
3.StateFlow仅在值已更新,并且值发生了变化时才会返回,即如果更新后的值没有变化,也没会回调Collect方法,这点与LiveData不同

StateFlow类似,我们也可以用stateIn将普通流转化成SharedFlow

val result: StateFlow<Result<UiState>> = someFlow
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = Result.Loading
)

shareIn类似,唯一不同的时需要传入一个默认值
同时之所以WhileSubscribed中传入了5000,是为了实现等待5秒后仍然没有订阅者存在就终止协程的功能,这个方法有以下功能

  • 用户将您的应用转至后台运行,5 秒钟后所有来自其他层的数据更新会停止,这样可以节省电量。
  • 最新的数据仍然会被缓存,所以当用户切换回应用时,视图立即就可以得到数据进行渲染。
  • 订阅将被重启,新数据会填充进来,当数据可用时更新视图。
  • 在屏幕旋转时,因为重新订阅的时间在5s内,因此上游流不会中止

4.3 在页面中观察StateFlow

LiveData类似,我们也需要经常在页面中观察StateFlow
观察StateFlow需要在协程中,因此我们需要协程构建器,一般我们会使用下面几种

  1. lifecycleScope.launch : 立即启动协程,并且在本 ActivityFragment 销毁时结束协程。
  2. LaunchWhenStarted 和 LaunchWhenResumed,它会在lifecycleOwner进入X状态之前一直等待,又在离开X状态时挂起协程


如上图所示:
1.使用launch是不安全的,在应用在后台时也会接收数据更新,可能会导致应用崩溃
2.使用launchWhenStartedlaunchWhenResumed会好一些,在后台时不会接收数据更新,但是,上游数据流会在应用后台运行期间保持活跃,因此可能浪费一定的资源

这么说来,我们使用WhileSubscribed进行的配置岂不是无效了吗?订阅者一直存在,只有页面关闭时才会取消订阅
官方推荐repeatOnLifecycle来构建协程
在某个特定的状态满足时启动协程,并且在生命周期所有者退出该状态时停止协程,如下图所示。

比如在某个Fragment的代码中:

onCreateView(...) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
myViewModel.myUiState.collect { ... }
}
}
}

当这个Fragment处于STARTED状态时会开始收集流,并且在RESUMED状态时保持收集,最终在Fragment进入STOPPED状态时结束收集过程。
结合使用repeatOnLifecycle APIWhileSubscribed,可以帮助您的应用妥善利用设备资源的同时,发挥最佳性能

4.4 页面中观察Flow的最佳方式

通过ViewModel暴露数据,并在页面中获取的最佳方式是:

  • ?? 使用带超时参数的 WhileSubscribed 策略暴露 Flow示例 1
  • ?? 使用 repeatOnLifecycle 来收集数据更新。示例 2


最佳实践如上图所示,如果采用其他方式,上游数据流会被一直保持活跃,导致资源浪费
当然,如果您并不需要使用到Kotlin Flow的强大功能,就用LiveData好了 :)

StateFlowSharedFlow有什么区别?

从上文其实可以看出,StateFlowSharedFlow其实是挺像的,让人有些傻傻分不清,有时候也挺难选择该用哪个的

我们总结一下,它们的区别如下:

  1. SharedFlow配置更为灵活,支持配置replay,缓冲区大小等,StateFlowSharedFlow的特化版本,replay固定为1,缓冲区大小默认为0
  2. StateFlowLiveData类似,支持通过myFlow.value获取当前状态,如果有这个需求,必须使用StateFlow
  3. SharedFlow支持发出和收集重复值,而StateFlowvalue重复时,不会回调collect
  4. 对于新的订阅者,StateFlow只会重播当前最新值,SharedFlow可配置重播元素个数(默认为0,即不重播)

可以看出,StateFlow为我们做了一些默认的配置,在SharedFlow上添加了一些默认约束,这些配置可能并不符合我们的要求

  1. 它忽略重复的值,并且是不可配置的。这会带来一些问题,比如当往List中添加元素并更新时,StateFlow会认为是重复的值并忽略
  2. 它需要一个初始值,并且在开始订阅时会回调初始值,这有可能不是我们想要的
  3. 它默认是粘性的,新用户订阅会获得当前的最新值,而且是不可配置的,而SharedFlow可以修改replay

StateFlow施加在SharedFlow上的约束可能不是最适合您,如果不需要访问myFlow.value,并且享受SharedFlow的灵活性,可以选择考虑使用SharedFlow

总结

简单往往意味着不够强大,而强大又常常意味着复杂,两者往往不能兼得,软件开发过程中常常面临这种取舍。
LiveData的简单并不是它的缺点,而是它的特点。StateFlowSharedFlow更加强大,但是学习成本也显著的更高.
我们应该根据自己的需求合理选择组件的使用

  1. 如果你的数据流比较简单,不需要进行线程切换与复杂的数据变换,LiveData对你来说相信已经足够了
  2. 如果你的数据流比较复杂,需要切换线程等操作,不需要发送重复值,需要获取myFlow.valueStateFlow对你来说是个好的选择
  3. 如果你的数据流比较复杂,同时不需要获取myFlow.value,需要配置新用户订阅重播无素的个数,或者需要发送重复的值,可以考虑使用SharedFlow


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

收起阅读 »

实战:5分钟搞懂OkHttp断点上传

1、前言 经常会有同学问:文件的断点上传如何实现? 断点上传/下载,这是在客户端经常遇到的场景,当我们需要上传或下载一个大文件时,都会考虑使用断点续传的方式。 断点上传相较于断点下载来说,最大的区别就在于断点位置的记录,上传记录在服务端,下载记录在客户端...
继续阅读 »

1、前言


经常会有同学问:文件的断点上传如何实现?


断点上传/下载,这是在客户端经常遇到的场景,当我们需要上传或下载一个大文件时,都会考虑使用断点续传的方式。


断点上传相较于断点下载来说,最大的区别就在于断点位置的记录,上传记录在服务端,下载记录在客户端,因此,客户端需要在上传前,通过接口去拿到文件的断点位置,然后在上传时,将文件输入流跳转到断点位置


2、准备工作


对于文件上传,其实就是打开文件的输入流,不停的读取数据到byte数组中,随后写出到服务端;那客户端要做的就是跳过已经上传的部分,也就是直接跳到断点位置,这样就可以从断点位置去读取数据,也就达到了断点上传的目的。


伪代码如下:


String filePath = "...";
long skipSize = 100; //假设断点位置是 100 byte
InputStream input = input = new FileInputStream(filePath);
input.skip(skipSize) //跳转到断点位置

然而,OkHttp并没有直接提供设置断点的方法,所以需要客户端自定义RequestBody,取名为FileRequestBody,如下:


//为简化阅读,已省略部分代码
public class FileRequestBody extends RequestBody {

private final File file;
private final long skipSize; //断点位置
private final MediaType mediaType;

public FileRequestBody(File file, long skipSize, @Nullable MediaType mediaType) {
this.file = file;
this.skipSize = skipSize;
this.mediaType = mediaType;
}

@Override
public long contentLength() throws IOException {
return file.length() - skipSize;
}

@Override
public void writeTo(@NotNull BufferedSink sink) throws IOException {
InputStream input = null;
Source source = null;
try {
input = new FileInputStream(file);
if (skipSize > 0) {
input.skip(skipSize); //跳到断点位置
}
source = Okio.source(input);
sink.writeAll(source);
} finally {
OkHttpCompat.closeQuietly(source, input);
}
}
}


为方便阅读,以上省略部分源码,FileRequestBody类完整源码



有了FileRequestBody类,我们只需要传入一个断点位置,剩下的工作就跟普通的文件上传一样。 接下来,直接进入代码实现。


3、代码实现


3.1 获取断点位置


首先,需要服务端提供一个接口,通过userId去查找该用户未上传完成的任务列表,代码如下:


RxHttp.get("/.../getToUploadTask")
.add("userId", "88888888")
.asList<ToUploadTask>()
.subscribe({
//成功回调,这里通过 it 拿到 List<ToUploadTask>
}, {
//异常回调
});

其中ToUploadTask类如下:


//待上传任务
data class ToUploadTask(
val md5: String, //文件的md5,用于验证文件的唯一性
val filePath: String, //文件在客户端的绝对路径
val skipSize: Long = 0 //断点位置
)

注:md5、filePath 这两个参数需要客户端在文件上传时传递给服务端,用于对文件的校验,防止文件错乱


3.2 断点上传


有了待上传任务,客户端就可以执行断点上传操作,OkHttp代码如下:


fun uploadFile(uploadTask: ToUploadTask) {
//1.校验文件是否存在
val file = File(uploadTask.filePath)
if (!file.exists() && !file.isFile) return
//2.校验文件的 md5 值
val fileMd5 = FileUtils.getFileMD5ToString(file)
if (!fileMd5.equals(uploadTask.md5)) return
//3.构建请求体
val fileRequestBody = FileRequestBody(file, uploadTask.skipSize, BuildUtil.getMediaType(file.name))
val multipartBody = MultipartBody.Builder()
.addFormDataPart("userId", "88888888")
.addFormDataPart("md5", fileMd5)
.addFormDataPart("filePath", file.absolutePath)
.addFormDataPart("file", file.name, fileRequestBody) //添加文件body
.build()
//4.构建请求
val request = Request.Builder()
.url("/.../uploadFile")
.post(multipartBody)
.build()
//5.执行请求
val okClient = OkHttpClient.Builder().build()
okClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
//异常回调
}
override fun onResponse(call: Call, response: Response) {
//成功回调
}
})
}


FIleUtils源码BuildUtil源码



当然,考虑到很少人会直接使用OkHttp,所以这里也贴出RxHttp的实现代码,很简单,仅需构建一个UpFile对象即可,就可很方便的监听上传进度,代码如下:


fun uploadFile(uploadTask: ToUploadTask) {                            
//1.校验文件是否存在
val file = File(uploadTask.filePath)
if (!file.exists() && !file.isFile) return
//2.校验文件的 md5 值
val fileMd5 = FileUtils.getFileMD5ToString(file)
if (!fileMd5.equals(uploadTask.md5)) return
val upFile = UpFile("file", file, file.name, uploadTask.skipSize)
//3.直接上传
RxHttp.postForm("/.../uploadFile")
.add("userId", "88888888")
.add("md5", fileMd5)
.add("filePath", file.absolutePath)
.addFile(upFile)
.upload(AndroidSchedulers.mainThread()) {
//上传进度回调
}
.asString()
.subscribe({
//成功回调
}, {
//异常回调
})
}

4、小结


断点上传相较普通的文件上传,客户端多了一个断点的设置,大部分工作量在服务端,服务端不仅需要处理文件的拼接逻辑,还需记录未上传完成的任务,并通过接口暴露给客户端。



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

iOS底层探索开发 必不可少的 clang插件

Clang插件LLVM下载由于国内的网络限制,我们需要借助镜像下载LLVM的源码https://mirror.tuna.tsinghua.edu.cn/help/llvm/下载llvm项目git clone https://mirrors.tuna....
继续阅读 »

Clang插件

LLVM下载

由于国内的网络限制,我们需要借助镜像下载LLVM的源码

https://mirror.tuna.tsinghua.edu.cn/help/llvm/

  • 下载llvm项目

git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git

  • 在LLVM的tools目录下下载Clang

cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git

  • 在 LLVM 的 projects 目录下下载 compiler-rt, libcxx, libcxxabi

cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.g it
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git

  • 在Clang的tools下安装extra工具

cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/LLvm/cLang-tooLs-e xtra.git

LLVM编译

由于最新的LLVM只支持c make来编译了,我们还需要安装c make。

安装cmake

  • 查看brew是否安装cmake如果有就跳过下面步骤

brew list

  • 通过brew安装cmake

brew install cmake

编译LLVM

通过xcode编译LLVM

  • cmake编译成Xcode项目

mkdir build_xcode
cd build_xcode
cmake -G Xcode ../llvm

  • 使用Xcode编译Clang。
    • 选择自动创建Schemes




  • 在HKPlugin目录下新建一个名为HKPlugin.cpp的文件和CMakeLists.txt的文件。在CMakeLists.txt中写上

add_llvm_library( HKPlugin MODULE BUILDTREE_ONLY
HKPlugin.cpp
)
接下来利用cmake重新生成一下Xcode项目,在build_xcode中cmake -g Xcode ../llvm

  • 最后可以在LLVM的Xcode项目中可以看到Loadable modules目录下有自己 的Plugin目录了。我们可以在里面编写插件代码。


添加下自己的插件,等下编译

#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;

namespace HKPlugin {
class HKConsumer: public ASTConsumer {
public:
//解析一个顶级的声明就回调一次
bool HandleTopLevelDecl(DeclGroupRef D) {
cout<<"正在解析..."<<endl;
return true;
}

//整个文件都解析完成的回调
void HandleTranslationUnit(ASTContext &Ctx) {
cout << "文件解析完毕" << endl;
}
};

//继承PluginASTAction 实现我们自定义的Action
class HKASTAction:public PluginASTAction{
public:
//重写
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg){
return true;
}

std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
return unique_ptr<HKConsumer>(new HKConsumer);
}
};
}
//注册插件

static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> HK("HKPlugin","this is HKPlugin");

先简单写些测试代码,然后编译生成dylib



int sum(int a);
int a;
int sum(int a){
int b = 10;
return 10 + b;
}
int sum2(int a,int b){
int c = 10;
return a + b + c;
}


写些测试代码

自己编译的 clang 文件路径 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.5.sdk/ -Xclang -load -Xclang 自己编译的 clang 文件路径 -Xclang -add-plugin -Xclang 自己编译的 clang 文件路径 -c 自己编译的 clang 文件路径

例: /Users/xxx/Desktop/LLVMENV/build_xcode/Debug/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.5.sdk/ -Xclang -load -Xclang /Users/xxx/Desktop/LLVMENV/build_xcode/Debug/lib/HKPlugin.dylib -Xclang -add-plugin -Xclang HKPlugin -c ./hello.m
注:iPhoneSimulator13.5.sdk换成自己目录下的sdk版本


正在解析...
正在解析...
正在解析...
正在解析...
文件解析完毕

现在在viewController中声明属性

#import "ViewController.h"

@interface ViewController ()
@property(nonatomic, strong) NSDictionary* dict;
@property(nonatomic, strong) NSArray* arr;
@property(nonatomic, strong) NSString* name;
@end

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

然后通过语法分析,查看抽象语法树

clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.5.sdk -fmodules -fsyntax-only -Xclang -ast-dump ViewController.m




TranslationUnitDecl 0x7f9e57000008 <<invalid sloc>> <invalid sloc> <undeserialized declarations>
|-TypedefDecl 0x7f9e570008a0 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7f9e570005a0 '__int128'
|-TypedefDecl 0x7f9e57000910 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x7f9e570005c0 'unsigned __int128'
|-TypedefDecl 0x7f9e570009b0 <<invalid sloc>> <invalid sloc> implicit SEL 'SEL *'
| `-PointerType 0x7f9e57000970 'SEL *' imported
| `-BuiltinType 0x7f9e57000800 'SEL'
|-TypedefDecl 0x7f9e57000a98 <<invalid sloc>> <invalid sloc> implicit id 'id'
| `-ObjCObjectPointerType 0x7f9e57000a40 'id' imported
| `-ObjCObjectType 0x7f9e57000a10 'id' imported
|-TypedefDecl 0x7f9e57000b78 <<invalid sloc>> <invalid sloc> implicit Class 'Class'
| `-ObjCObjectPointerType 0x7f9e57000b20 'Class' imported
| `-ObjCObjectType 0x7f9e57000af0 'Class' imported
|-ObjCInterfaceDecl 0x7f9e57000bd0 <<invalid sloc>> <invalid sloc> implicit Protocol
|-TypedefDecl 0x7f9e57000f48 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag'
| `-RecordType 0x7f9e57000d40 'struct __NSConstantString_tag'
| `-Record 0x7f9e57000ca0 '__NSConstantString_tag'
|-TypedefDecl 0x7f9e58008400 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x7f9e57000fa0 'char *'
| `-BuiltinType 0x7f9e570000a0 'char'
|-TypedefDecl 0x7f9e580086e8 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7f9e58008690 'struct __va_list_tag [1]' 1
| `-RecordType 0x7f9e580084f0 'struct __va_list_tag'
| `-Record 0x7f9e58008458 '__va_list_tag'
|-ImportDecl 0x7f9e5852bc18 <./ViewController.h:9:1> col:1 implicit UIKit
|-ObjCInterfaceDecl 0x7f9e58541e00 <line:11:1, line:14:2> line:11:12 ViewController
| |-super ObjCInterface 0x7f9e5852be78 'UIViewController'
| `-ObjCImplementation 0x7f9e5857f460 'ViewController'
|-ObjCCategoryDecl 0x7f9e58541f30 <ViewController.m:11:1, line:15:2> line:11:12
| |-ObjCInterface 0x7f9e58541e00 'ViewController'
| |-ObjCPropertyDecl 0x7f9e58548a00 <line:12:1, col:44> col:44 dict 'NSDictionary *' readwrite nonatomic strong
| |-ObjCMethodDecl 0x7f9e58548a80 <col:44> col:44 implicit - dict 'NSDictionary *'
| |-ObjCMethodDecl 0x7f9e58548c28 <col:44> col:44 implicit - setDict: 'void'
| | `-ParmVarDecl 0x7f9e58548cb0 <col:44> col:44 dict 'NSDictionary *'
| |-ObjCPropertyDecl 0x7f9e58551cd0 <line:13:1, col:39> col:39 arr 'NSArray *' readwrite nonatomic strong
| |-ObjCMethodDecl 0x7f9e58551d50 <col:39> col:39 implicit - arr 'NSArray *'
| |-ObjCMethodDecl 0x7f9e58551ea8 <col:39> col:39 implicit - setArr: 'void'
| | `-ParmVarDecl 0x7f9e58551f30 <col:39> col:39 arr 'NSArray *'
| |-ObjCPropertyDecl 0x7f9e585650d0 <line:14:1, col:40> col:40 name 'NSString *' readwrite nonatomic strong
| |-ObjCMethodDecl 0x7f9e58565150 <col:40> col:40 implicit - name 'NSString *'
| `-ObjCMethodDecl 0x7f9e585652a8 <col:40> col:40 implicit - setName: 'void'
| `-ParmVarDecl 0x7f9e58565330 <col:40> col:40 name 'NSString *'
`-ObjCImplementationDecl 0x7f9e5857f460 <line:17:1, line:25:1> line:17:17 ViewController
|-ObjCInterface 0x7f9e58541e00 'ViewController'
|-ObjCMethodDecl 0x7f9e5857f580 <line:19:1, line:22:1> line:19:1 - viewDidLoad 'void'
| |-ImplicitParamDecl 0x7f9e585c9c08 <<invalid sloc>> <invalid sloc> implicit self 'ViewController *'
| |-ImplicitParamDecl 0x7f9e585c9c70 <<invalid sloc>> <invalid sloc> implicit _cmd 'SEL':'SEL *'
| `-CompoundStmt 0x7f9e585cf2b8 <col:21, line:22:1>
| `-ObjCMessageExpr 0x7f9e585c9cd8 <line:20:5, col:23> 'void' selector=viewDidLoad super (instance)
|-ObjCIvarDecl 0x7f9e585c8168 <line:12:44> col:44 implicit _dict 'NSDictionary *' synthesize private
|-ObjCPropertyImplDecl 0x7f9e585c81c8 <<invalid sloc>, col:44> <invalid sloc> dict synthesize
| |-ObjCProperty 0x7f9e58548a00 'dict'
| `-ObjCIvar 0x7f9e585c8168 '_dict' 'NSDictionary *'
|-ObjCIvarDecl 0x7f9e585c84e0 <line:13:39> col:39 implicit _arr 'NSArray *' synthesize private
|-ObjCPropertyImplDecl 0x7f9e585c8540 <<invalid sloc>, col:39> <invalid sloc> arr synthesize
| |-ObjCProperty 0x7f9e58551cd0 'arr'
| `-ObjCIvar 0x7f9e585c84e0 '_arr' 'NSArray *'
|-ObjCIvarDecl 0x7f9e585c9890 <line:14:40> col:40 implicit _name 'NSString *' synthesize private
|-ObjCPropertyImplDecl 0x7f9e585c98f0 <<invalid sloc>, col:40> <invalid sloc> name synthesize
| |-ObjCProperty 0x7f9e585650d0 'name'
| `-ObjCIvar 0x7f9e585c9890 '_name' 'NSString *'
|-ObjCMethodDecl 0x7f9e585c82f8 <line:12:44> col:44 implicit - dict 'NSDictionary *'
|-ObjCMethodDecl 0x7f9e585c8450 <col:44> col:44 implicit - setDict: 'void'
| `-ParmVarDecl 0x7f9e58548cb0 <col:44> col:44 dict 'NSDictionary *'
|-ObjCMethodDecl 0x7f9e585c8670 <line:13:39> col:39 implicit - arr 'NSArray *'
|-ObjCMethodDecl 0x7f9e585c9800 <col:39> col:39 implicit - setArr: 'void'
| `-ParmVarDecl 0x7f9e58551f30 <col:39> col:39 arr 'NSArray *'
|-ObjCMethodDecl 0x7f9e585c9a20 <line:14:40> col:40 implicit - name 'NSString *'
`-ObjCMethodDecl 0x7f9e585c9b78 <col:40> col:40 implicit - setName: 'void'
`-ParmVarDecl 0x7f9e58565330 <col:40> col:40 name 'NSString *'

我们可以找到其中的属性节点和他的修饰符

| |-ObjCPropertyDecl 0x7f9e585650d0 <line:14:1, col:40> col:40 name 'NSString *' readwrite nonatomic strong
完整代码如下

#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;

namespace HKPlugin {

class HKMatchCallback:public MatchFinder::MatchCallback{
private:
CompilerInstance &CI;

bool isUserSourceCode(const string fileName){
if (fileName.empty()) return false;
//非xcode中的源码都认为是用户的
if(fileName.find("/Applications/Xcode.app/") == 0)return false;
return true;
}

//判断是否应该用copy修饰
bool isShouldUseCopy(const string typeStr){
if (typeStr.find("NSString") != string::npos ||
typeStr.find("NSArray") != string::npos ||
typeStr.find("NSDictionary") != string::npos ) {
return true;
}
return false;
}
public:
HKMatchCallback(CompilerInstance &CI):CI(CI){}
void run(const MatchFinder::MatchResult &Result) {
//通过Result获得节点
//之前绑定的标识
const ObjCPropertyDecl * propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl");


//获取文件名称
string fileName = CI.getSourceManager().getFilename(propertyDecl-> getSourceRange().getBegin()).str();

//判断节点有值并且是用户文件
if (propertyDecl && isUserSourceCode(fileName)) {
//节点类型转为字符串
string typeStr = propertyDecl->getType().getAsString();
//拿到及诶单的描述信息
ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
//判断应该使用copy但是没有使用copy
if (isShouldUseCopy(typeStr) && !(attrKind & clang::ObjCPropertyDecl::OBJC_PR_copy)) {
cout << typeStr << "应该用copy修饰!但你没有" << endl;
//诊断引擎
DiagnosticsEngine &diag = CI.getDiagnostics();
//Report 报告
diag.Report(propertyDecl->getBeginLoc(),diag.getCustomDiagID(DiagnosticsEngine::Warning, "--- %0 这个地方推荐使用copy"))<<typeStr
}
// cout<<"--拿到了:"<<typeStr<<"---属于文件:"<<fileName<<endl;

}
}
};

class HKConsumer: public ASTConsumer {
private:
//AST节点的查找过滤器
MatchFinder matcher;
HKMatchCallback callback;
public:
HKConsumer(CompilerInstance &CI):callback(CI){
//添加一个MatchFinder去匹配objcPropertyDecl节点
//回调在HKMatchCallback里面run方法

matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &callback);
}
//解析一个顶级的声明就回调一次
bool HandleTopLevelDecl(DeclGroupRef D) {
cout<<"正在解析..."<<endl;
return true;
}

//整个文件都解析完成的回调
void HandleTranslationUnit(ASTContext &Ctx) {
cout << "文件解析完毕" << endl;
matcher.matchAST(Ctx);//将语法树交给过滤器
}
};

//继承PluginASTAction 实现我们自定义的Action
class HKASTAction:public PluginASTAction{
public:
//重写
bool ParseArgs(const CompilerInstance &CI, const std::vector<std::string> &arg){
return true;
}

std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI, StringRef InFile) {
//ASTConsumer是一个抽象类,这里返回一个自定义的类来继承
return unique_ptr<HKConsumer>(new HKConsumer(CI));
}
};
}
//注册插件

static FrontendPluginRegistry::Add<HKPlugin::HKASTAction> HK("HKPlugin","this is HKPlugin");






作者:Mjs
链接:https://www.jianshu.com/p/d613d935662d



收起阅读 »

OC底层原理-动态方法决议

当lookupImpOrForward函数从cache和methodTable中找不到对应Method,继续向下执行就会来到resolveMethod_locked函数也就是我们常说的动态方法决议 if (slowpath(behavior & ...
继续阅读 »

当lookupImpOrForward函数从cache和methodTable中找不到对应Method,继续向下执行就会来到resolveMethod_locked函数也就是我们常说的动态方法决议


    if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}

resolveMethod_locked

    runtimeLock.assertLocked();
ASSERT(cls->isRealized());

runtimeLock.unlock();

if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}

// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);

只执行一次

behavior & LOOKUP_RESOLVER
behavior ^= LOOKUP_RESOLVER;
这俩步操作保证resolveMethod_locked只被执行一次

resolveInstanceMethod


static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);

if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
//根类NSObject有默认实现兜底,不会走到这里
return;
}

BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//向cls对象发送resolveInstanceMethod:消息,参数为当前的sel
bool resolved = msg(cls, resolve_sel, sel);

// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
//从方法缓存中再快速查找一遍
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

resolveClassMethod


static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());

if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
// NSObject有兜底实现
return;
}

Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

resolveMethod_locked会发送resolveInstanceMethod:和resolveClassMethod:消息,为了减少程序的崩溃提用户体验,苹果在这里给开发者一次机会去补救,这个过程就叫做动态方法决议。这里也体现了aop编程思想,在objc_msg流程中给开发者提供了一个切面,切入自己想要处理,比如安全处理,日志收集等等。

resolveClassMethod


static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());

if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}

Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

看完源码思考俩个问题

1.为什么在resloveInstanceMethod函数中调用了一次lookUpImpOrNilTryCache,resolveMethod_locked函数最后又调用了一次lookUpImpOrNilTryCache?这俩次分别有什么作用?

  • 第一次TryCache流程分析

堆栈信息-->第一次tryCache会把我动态添加的方法存进cache

本次TryCache,会调用lookUpImpOrForWard函数查找MethodTable。入参behavior值为4,找不到imp的话不会再走动态决议和消息转发,直接return nil,分支如下:

    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
所以这次tryCache实际的作用就是在动态决议添加方法之后,找到方法,并调用log_and_fill_cache函数存进缓存(佐证了下面这段注释)

   // Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
  • 第二次TryCache流程分析

这次我们直接看注释吧

    // chances are that calling the resolver have populated the cache
// so attempt using it

调用动态决议可能填充了得缓存,并尝试使用它。嗯,第二次tryCache的作用已经简单明了。
本次调用入参behavior值为1,methodTable查找不到imp不会走动态决议流程,但会调用消息转发

  • 为什么分为俩次呢,一次不行吗?

为什么不最后查找方法,填充缓存再返回,反而要先填充缓存,再尝试从缓存中查找,这么做有什么好处呢?

有个关于多线程的猜想:

假如线程a发送消息s进入了动态决议流程,此时线程b也发送消息s,这时候如果缓存中有已添加的imp响应消息s,是不是就不会继续慢速查找,动态决议等后续流程。这么想,动态决议添加的方法是不是越先添加到缓存越好。

另外一点我们看到resolveClassMethod之后,也尝试从缓存中查找,而且找不到又调用了一遍resolveInstanceMethod。

可已看出苹果开发者在设计这段流程的思考🤔可能是:
既然你愿意通过动态方法决议去添加这个imp,费了这么大功夫,很显然你想使用该imp,而且使用的频率可能不低。既然如此在resolver方法调用完毕,我就帮你放进缓存吧。以后你想用直接从缓存中找。

2. 为什么类resolver之后会尝试调用instance的resolver?难道instance的resolver还能解决类方法缺失的问题?

关于这个问题,我们来看张经典的

如果我们查找一个类方法沿着继承链最终会找到NSObject(rootMetaClass的父类是NSObject),这会导致一个有意思的问题:我们的NSObject对象方法可以响应类方法的sel

看个实例

给NSObect添加个instaceMethod



是不是很惊喜,其实我们底层对classMethod和InstanceMethod根本没有区分,classMethod也是InstanceMethod

* class_getClassMethod.  Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;

return class_getInstanceMethod(cls->getMeta(), sel);
}
只不过,找classMethod是从MetaClass查找InstanceMethod,找InstanceMethod是从class找InstanceMethod。
透过现象看本质,这里就可以解释,为什么resolveClass完毕,缓存中找不到imp,会再次调用resolveInstance。显然,我们给NSObject添加InstanceMethod可以解决问题,而且可以在这里我们也可以添加classMethod。毕竟classMethod也是InstanceMethod。






作者:可可先生_3083
链接:https://www.jianshu.com/p/2d1372b4d2c9





收起阅读 »

iOS 攻防 - DYLD_INSERT_LIBRARIES

Tweak是通过DYLD_INSERT_LIBRARIES来插入动态库的,那么它是怎么做到的呢?这就需要去dyld源码中探究了。一、 DYLD_INSERT_LIBRARIES原理由于dyld源码中b不同版本有变动,需要分别看下新老版本的实现1.1 dyld-...
继续阅读 »

Tweak是通过DYLD_INSERT_LIBRARIES来插入动态库的,那么它是怎么做到的呢?这就需要去dyld源码中探究了。

一、 DYLD_INSERT_LIBRARIES原理

由于dyld源码中b不同版本有变动,需要分别看下新老版本的实现

1.1 dyld-519.2.2 源码

打开dyld源码工程,搜索DYLD_INSERT_LIBRARIES关键字,在dyld.cpp5906行有如下代码:

// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}


这段代码是判断DYLD_INSERT_LIBRARIES不为空就循环加载插入动态库


if ( gLinkContext.processIsRestricted ) {
pruneEnvironmentVariables(envp, &apple);
// set again because envp and apple may have changed or moved
setContext(mainExecutableMH, argc, argv, envp, apple);
}


这里判断进程如果受限制(processIsRestricted不为空)执行pruneEnvironmentVariablespruneEnvironmentVariables会移除DYLD_INSERT_LIBRARIES中的数据,相当于被清空了。这样插入的动态库就不会被加载了。

既然越狱插件是通过DYLD_INSERT_LIBRARIES插入的,那么只要让自己的进程受限就能起到保护作用了。

搜索processIsRestricted = true是在4696行设置值的:

// any processes with setuid or setgid bit set or with __RESTRICT segment is restricted
if ( issetugid() || hasRestrictedSegment(mainExecutableMH) ) {
gLinkContext.processIsRestricted = true;
}

issetugid不能在上架的App中设置,那么就只能设置hasRestrictedSegment了,这里传入的参数是主程序:

static bool hasRestrictedSegment(const macho_header* mh)
{
//load command 数量
const uint32_t cmd_count = mh->ncmds;
const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(macho_header));
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND:
{
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;

//dyld::log("seg name: %s\n", seg->segname);
//读取__RESTRICT SEGMENT
if (strcmp(seg->segname, "__RESTRICT") == 0) {
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
//读取__restrict SECTION
if (strcmp(sect->sectname, "__restrict") == 0)
return true;
}
}
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}

return false;
}

这段代码的意思是判断load commands中有没有__RESTRICT SECTIONSECTION中有没有__restrict SEGMENT

也就是说只要有这个SECTION就会开启进程受限了。

1.2 dyld-851.27源码

dyld2.cpp7120行中仍然有DYLD_INSERT_LIBRARIES的判断
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}

processIsRestricted变成了一个函数

bool processIsRestricted()
{
#if TARGET_OS_OSX
return !gLinkContext.allowEnvVarsPath;
#else
return false;
#endif
}

这里可以看到只在OSX下才有效。

6667行也只有OSX下才有可能清空环境变量:

#if TARGET_OS_OSX
if ( !gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache ) {
pruneEnvironmentVariables(envp, &apple);
// set again because envp and apple may have changed or moved
setContext(mainExecutableMH, argc, argv, envp, apple);
}
else
#endif
{
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
}

hasRestrictedSegment也变成了OSX下专属:

#if TARGET_OS_OSX
static bool hasRestrictedSegment(const macho_header* mh)
{
const uint32_t cmd_count = mh->ncmds;
const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(macho_header));
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND:
{
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;

//dyld::log("seg name: %s\n", seg->segname);
if (strcmp(seg->segname, "__RESTRICT") == 0) {
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
if (strcmp(sect->sectname, "__restrict") == 0)
return true;
}
}
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}

return false;
}
#endif

结论:iOS 10以前dyld会判断主程序是否有__RESTRICT,__restrict来决定是否加载DYLD_INSERT_LIBRARIESiOS 10及以后并不会进行判断直接进行了加载。


二、 DYLD_INSERT_LIBRARIES 攻防

2.1 iOS10以前攻防

2.1.1 RESTRIC段防护


Other Linker Flags中输入-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null


这样通过DYLD_INSERT_LIBRARIES注入的库就无效了。越狱手机上的插件就无效了。(仅在iOS 10以下有效)。


2.1.2 修改二进制破解

针对RESTRIC的防护可以用二进制修改器将段名称修改掉,就可以绕过检测了。
修改Data中的任意一位这个值就变了:



修改后重签就可以了。


2.1.3 防止RESTRICT被修改


针对RESTRICT被修改可以在代码中判断MachO中是否有对应的RESTRIC,如果没有就证明被修改了。参考dyld源码修改判断如下:
#import <mach-o/dyld.h>

#if __LP64__
#define macho_header mach_header_64
#define LC_SEGMENT_COMMAND LC_SEGMENT_64
#define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT
#define LC_ENCRYPT_COMMAND LC_ENCRYPTION_INFO
#define macho_segment_command segment_command_64
#define macho_section section_64
#else
#define macho_header mach_header
#define LC_SEGMENT_COMMAND LC_SEGMENT
#define LC_SEGMENT_COMMAND_WRONG LC_SEGMENT_64
#define LC_ENCRYPT_COMMAND LC_ENCRYPTION_INFO_64
#define macho_segment_command segment_command
#define macho_section section
#endif

static bool hp_hasRestrictedSegment(const struct macho_header* mh) {
const uint32_t cmd_count = mh->ncmds;
const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(struct macho_header));
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND: {
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
printf("seg->segname: %s\n",seg->segname);
//dyld::log("seg name: %s\n", seg->segname);
if (strcmp(seg->segname, "__RESTRICT") == 0) {
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
printf("sect->sectname: %s\n",sect->sectname);
if (strcmp(sect->sectname, "__restrict") == 0)
return true;
}
}
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
return false;
}
调用

+ (void)load {
//获取主程序 macho_header
const struct macho_header *header = _dyld_get_image_header(0);
if (hp_hasRestrictedSegment(header)) {
NSLog(@"没有修改");
} else {
NSLog(@"被修改了");
}
}
这样就能知道RESTRICT有没有被修改。要Hook检测逻辑就需要找到hp_hasRestrictedSegment函数的地址进行inline hook。或者找到调用hp_hasRestrictedSegment的地方,那么在检测过程中就不能有明显的特征。一般将结果告诉服务端。或者做一些破坏功能的逻辑,比如网络请求相关的内容。

2.2 iOS10及以后攻防

2.2.1 使用DYLD源码防护(黑白名单)

既然iOS10以上系统不进行判断检测了,那么我们可以自己扫描判断哪些应该被加载哪些不能被加载。


#import <mach-o/dyld.h>

const char *whiteListLibStrs =
"/usr/lib/substitute-inserter.dylib/System/Library/Frameworks/Foundation.framework/Foundation/usr/lib/libobjc.A.dylib/usr/lib/libSystem.B.dylib/System/Library/Frameworks/UIKit.framework/UIKit/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation/System/Library/PrivateFrameworks/CoreAutoLayout.framework/CoreAutoLayout/usr/lib/libcompression.dylib/System/Library/Frameworks/CFNetwork.framework/CFNetwork/usr/lib/libarchive.2.dylib/usr/lib/libicucore.A.dylib/usr/lib/libxml2.2.dylib/usr/lib/liblangid.dylib/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit/usr/lib/libCRFSuite.dylib/System/Library/PrivateFrameworks/SoftLinking.framework/SoftLinking/usr/lib/libc++abi.dylib/usr/lib/libc++.1.dylib/usr/lib/system/libcache.dylib/usr/lib/system/libcommonCrypto.dylib/usr/lib/system/libcompiler_rt.dylib/usr/lib/system/libcopyfile.dylib/usr/lib/system/libcorecrypto.dylib";

const char *blackListLibStrs =
"/usr/lib/libsubstitute.dylib/usr/lib/substitute-loader.dylib/usr/lib/libsubstrate.dylib/Library/MobileSubstrate/DynamicLibraries/RHRevealLoader";

void imageListCheck() {
//进程依赖的库数量
int count = _dyld_image_count();
//第一个为自己。过滤掉,因为每次执行的沙盒路径不一样。
for (int i = 1; i < count; i++) {
const char *image_name = _dyld_get_image_name(i);
// printf("%s",image_name);
//黑名单检测
if (strstr(blackListLibStrs, image_name)) {//不在白名单
printf("image_name in black list: %s\n",image_name);
break;
}
//白名单检测
if (!strstr(whiteListLibStrs, image_name)) {
printf("image_name not in white list: %s\n",image_name);
}
}
}

调用

+ (void)load {
imageListCheck();
}
  • 白名单可以直接通过_dyld_get_image_name获取,这里和系统版本有关。需要跑支持的系统版本获取得到并集。维护起来比较麻烦。
  • 黑名单中可以将一些越狱库和检测到的异常库放入其中。
  • 一般检测到问题直接上报服务端。不要直接表现出异常。

黑白名单一般都通过服务端下发,黑名单直接检测出问题上报服务端处理,白名单维护用来检测上报未知的库供分析更新黑白名单。

这种防护方式可以通过fishhook Hook _dyld_image_count_dyld_get_image_name来做排查是哪块做的检测从而去绕过。

  • 对于检测代码最好混淆函数名称。
  • 返回值不要返回一个布尔值,函数被hook之后或者被修改成返回YES 之后很多判断代码都没用了。最好返回特定字符串加密这种。
  • 检测到被注入时不要exit(0)完事,太明显了,这种很容易被绕过。攻防的核心不在于防护技术,而在于会不会被对方发现。微信的做法就是上报服务端封号处理。
  • 在检测到时可以悄悄对业务逻辑做一些处理,比如网络请求正常返回但是页面显示异常或者功能不全等。

没有绝对安全的代码,只不过在与会不会被对方发现以及破解的代价。如果破解代价大于收益很少有人去破解的。



作者:HotPotCat
链接:https://www.jianshu.com/p/79a24b728b99。


收起阅读 »

iOS 攻防 - ptrace

在破解一款App的时候,在实际破解之前肯定是在做调试。LLDB之所以能附加进程时因为debugserver,而debugserver附加是通过ptrace函数来trace process的。ptrace是系统函数,此函数提供一个进程去监听和控制另一个进程,并且...
继续阅读 »

在破解一款App的时候,在实际破解之前肯定是在做调试。LLDB之所以能附加进程时因为debugserver,而debugserver附加是通过ptrace函数来trace process的。
ptrace是系统函数,此函数提供一个进程去监听和控制另一个进程,并且可以检测被控制进程的内存和寄存器里面的数据。ptrace可以用来实现断点调试和系统调用跟踪。

一、反调试ptrace

iOS#import 头文件不能直接导入,所以需要我们自己导出头文件引入调用。当然也可以声明ptrace函数直接调用。

1.1 ptrace 头文件

  1. 直接创建一个macOS程序导入#import 头文件,点进去拷贝生成一个.h文件就可以了:


/*
* Copyright (c) 2000-2005 Apple Computer, Inc. All rights reserved.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. The rights granted to you under the License
* may not be used to create, or enable the creation or redistribution of,
* unlawful or unlicensed copies of an Apple operating system, or to
* circumvent, violate, or enable the circumvention or violation of, any
* terms of an Apple operating system software license agreement.
*
* Please obtain a copy of the License at
*
http://www.opensource.apple.com/apsl/
and read it before using this file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_END@
*/

/* Copyright (c) 1995 NeXT Computer, Inc. All Rights Reserved */
/*-
* Copyright (c) 1984, 1993
* The Regents of the University of California. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* @(#)ptrace.h 8.2 (Berkeley) 1/4/94
*/


#ifndef _SYS_PTRACE_H_
#define _SYS_PTRACE_H_

#include
#include

enum {
ePtAttachDeprecated __deprecated_enum_msg("PT_ATTACH is deprecated. See PT_ATTACHEXC") = 10
};


#define PT_TRACE_ME 0 /* child declares it's being traced */
#define PT_READ_I 1 /* read word in child's I space */
#define PT_READ_D 2 /* read word in child's D space */
#define PT_READ_U 3 /* read word in child's user structure */
#define PT_WRITE_I 4 /* write word in child's I space */
#define PT_WRITE_D 5 /* write word in child's D space */
#define PT_WRITE_U 6 /* write word in child's user structure */
#define PT_CONTINUE 7 /* continue the child */
#define PT_KILL 8 /* kill the child process */
#define PT_STEP 9 /* single step the child */
#define PT_ATTACH ePtAttachDeprecated /* trace some running process */
#define PT_DETACH 11 /* stop tracing a process */
#define PT_SIGEXC 12 /* signals as exceptions for current_proc */
#define PT_THUPDATE 13 /* signal for thread# */
#define PT_ATTACHEXC 14 /* attach to running process with signal exception */

#define PT_FORCEQUOTA 30 /* Enforce quota for root */
#define PT_DENY_ATTACH 31

#define PT_FIRSTMACH 32 /* for machine-specific requests */

__BEGIN_DECLS


int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);


__END_DECLS

#endif /* !_SYS_PTRACE_H_ */

  1. 直接声明函数:
int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);
  • _request:要处理的事情
  • _pid:要操作的进程
  • _addr_data:取决于_pid参数,要传递的数据地址和数据本身。

1.2 ptrace调用

//告诉系统当前进程拒绝被debugserver附加
ptrace(PT_DENY_ATTACH, 0, 0, 0);
//ptrace(31, 0, 0, 0);

PT_DENY_ATTACH表示拒绝附加,值为31。如果仅仅是声明函数就传31就好了。_pid0表示当前进程。这里不传递任何数据。

分别在以下方法中调用

  1. load方法中调用:
+ (void)load {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
  1. constructor中调用:
__attribute__((constructor)) static void entry() {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
  1. main函数中调用:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;

@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}


  1. didFinishLaunchingWithOptions中调用:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
return YES;
}

123情况下Xcode启动调试后调试直接断开,App能正常操作不能调试。4在调试情况下App直接闪退,正常打开没问题。同时调用的情况下以第一次为准。

也就是说 :ptracemain函数之后调用App会直接闪退,main以及之前调用会停止进程附加,以第一次调用为准。正常打开App没有问题,只影响LLDB调试。

通过上面的验证说明在程序没有加载的时候调用ptrace会设置一个标志,后续程序就不会被附加了,如果在已经被附加了的情况下调用ptrace会直接退出(因为这里ptrace附加传递的pid0主程序本身)。


PT_DENY_ATTACH
This request is the other operation used by the traced process; it allows a process that is not currently being traced to deny future traces by its parent. All other arguments are ignored. If the process is currently being traced, it will exit with the exit status of ENOTSUP; otherwise, it sets a flag that denies future traces. An attempt by the parent to trace a process which has set this flag will result in a segmentation violation in the parent.
ENOTSUP含义如下:
define ENOTSUP 45 //Operation not supported
之前在手机端通过debugserver附加程序直接报错11,定义如下:
PT_DETACH 11 // stop tracing a process

二、 破解ptrace

ptrace的特征:附加不了、Xcode运行闪退/停止附加、使用正常。

既然ptrace可以组织调试,那么我们只要Hook了这个函数绕过PT_DENY_ATTACH的调用就可以了。首先想到的就是fishhook


#import "fishhook.h"

int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);

int hp_ptrace(int _request, pid_t _pid, caddr_t _addr, int _data){
if (_request != 31) {//不是拒绝附加
return ptrace_p(_request, _pid, _addr, _data);
}
return 0;
}

void hp_hook_ptrace() {
struct rebinding ptrace_rb;
ptrace_rb.name ="ptrace";
ptrace_rb.replacement = hp_ptrace;
ptrace_rb.replaced = (void *)&ptrace_p;

struct rebinding bds[] = {ptrace_rb};
rebind_symbols(bds, 1);
}

+ (void)load {
hp_hook_ptrace();
}

这样就能够进行附加调试了。


三、防止ptrace被破解


3.1 提前Hook防止ptrace被Hook


既然ptrace能够被Hook,那么自己先Hookptrace。调用的时候直接调用自己存储的地址就可以了。我们可以在自己的项目中增加一个Framework。这个库在Link Binary With Libraries中尽可能的靠前。这与dyld加载动态库的顺序有关。
这样就可以不被ptrace Hook了。代码逻辑和1.2中相同,只不过调用要换成ptrace_p
记的头文件中导出ptrace_p

CF_EXPORT int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);

创建一个Monkey工程,将3.1生成的.app包拖入工程重签名,这个时候主程序通过调用ptrace已经不能阻止我们调试了,但是调用ptrace_p的地方Monkey Hook不到了。

3.2 修改二进制破解提前Hook ptrace


Monkey的工程中打ptrace符号断点:

这个时候可以看到是didFinishLaunchingWithOptions中调用了ptrace_p函数:
Hopper打开MachO文件找到didFinishLaunchingWithOptions方法:

然后一直点下去找到ptrace_p是属于Inject.framework的:

.appFrameworks中找到Inject.frameworkHopper打开,可以看到_rebind_symbols,上面的参数是ptrace

这里我们可以直接修改ptrace让先Hook的变成另外一个函数,但是有风险点是App内部调用ptrace_p的时候如果没有判断空就crash了。如果判断了可以这么处理。
还有另外一个方式是修改didFinishLaunchingWithOptions代码中的汇编,修改blr x8NOP这样就绕过了ptrace_p的调用。





作者:HotPotCat
链接:https://www.jianshu.com/p/9ed2de5e7497












收起阅读 »

Android顶部悬浮条控件HoveringScroll

上滑停靠顶端悬浮框,下滑恢复原有位置滑动时,监听ScrollView的滚动Y值和悬浮区域以上的高度进行比较计算,对两个控件(布局)的显示隐藏来实现控件的顶部悬浮,通过addView和removeView来实现。###具体实现步骤:1.让ScrollView实现...
继续阅读 »


上滑停靠顶端悬浮框,下滑恢复原有位置

滑动时,监听ScrollView的滚动Y值和悬浮区域以上的高度进行比较计算,对两个控件(布局)的显示隐藏来实现控件的顶部悬浮,通过addViewremoveView来实现。

###具体实现步骤:

1.让ScrollView实现滚动监听

具体参见HoveringScrollview

2.布局实现

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin" >

<!-- zing定义view: HoveringScrollview -->
<com.steve.hovering.samples.HoveringScrollview
android:id="@+id/hoveringScrollview"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >

<RelativeLayout
android:id="@+id/rlayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" >

<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="TOP信息\nTOP信息\nTOP信息\nTOP信息"
android:textColor="#d19275"
android:textSize="30sp" />
</RelativeLayout>

<!-- 这个悬浮条必须是固定高度:如70dp -->
<LinearLayout
android:id="@+id/search02"
android:layout_width="match_parent"
android:layout_height="70dp" >

<LinearLayout
android:id="@+id/hoveringLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#A8A8A8"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="10dp" >

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:padding="10dp"
android:text="¥188\r\n原价:¥399"
android:textColor="#FF7F00" />

<Button
android:id="@+id/btnQiaBuy"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="#FF7F00"
android:padding="10dp"
android:onClick="clickListenerMe"
android:text="立即抢购"
android:textColor="#FFFFFF" />
</LinearLayout>
</LinearLayout>

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="1测试内容\n2测试内容\n3测试内容\n4测试内容\n5测试内容\n6测试内容\n7测试内容\n8测试内容\n9测试内容\n10测试内容\n11测试内容\n12测试内容\n13测试内容\n14测试内容\n15测试内容\n16测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n25测试内容"
android:textSize="40sp" />
</LinearLayout>
</com.steve.hovering.samples.HoveringScrollview>

<LinearLayout
android:id="@+id/search01"
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="vertical" >
</LinearLayout>

</RelativeLayout>

3.监听变化,实现悬停效果

通过search01和search02的addViewremoveView来实现


代码下载:ijustyce-HoveringScroll-master.zip

收起阅读 »

Android仿微信图片选择器-LQRImagePicker

LQRImagePicker完全仿微信的图片选择,并且提供了多种图片加载接口,选择图片后可以旋转,可以裁剪成矩形或圆形,可以配置各种其他的参数##一、简述:本项目是基于ImagePicker完善及界面修改。 主要工作:原项目中UI方面与微信有明显差别,如:文件...
继续阅读 »

LQRImagePicker

完全仿微信的图片选择,并且提供了多种图片加载接口,选择图片后可以旋转,可以裁剪成矩形或圆形,可以配置各种其他的参数

##一、简述:

本项目是基于ImagePicker完善及界面修改。 主要工作:

  1. 原项目中UI方面与微信有明显差别,如:文件夹选择菜单的样式就不是很美观,高度比例与微信的明显不同,故对其进行美化;

  2. 原项目在功能方面有一个致命的BUG,在一开始打开菜单后,随便点击一张图片就会直接崩溃(亲测4.4可用,但6.0直接崩溃),本项目已对此进行了解决;

  3. 编码方面,原项目中获取本地文件uri路径时,使用Uri.fromFile(),这种方式不好,控制台会一直报错(such file or directory no found),故使用Uri.parse()进行代替。

##二、使用:

不得不说,原项目是一个非常不错的项目,有很多地方值得我们学习,其中图片的加载方案让我受益匪浅,通过定义一个接口,由第三方开发者自己在自己项目中实现,避免了在库中强制使用指定图片加载工具的问题,使得本项目的扩展性增强。当然也有其他值得学习的地方,在 ImagePicker中有详细的配置方式,如有更多需求请前往原项目查看学习。这里我只记录下我自己项目中的使用配置:

###1、在自己项目中添加本项目依赖:

compile 'com.lqr.imagepicker:library:1.0.0'

###2、实现ImageLoader接口(注意不是com.nostra13.universalimageloader.core.ImageLoader),实现图片加载策略:

/**
* @创建者 CSDN_LQR
* @描述 仿微信图片选择控件需要用到的图片加载类
*/
public class UILImageLoader implements com.lqr.imagepicker.loader.ImageLoader {

@Override
public void displayImage(Activity activity, String path, ImageView imageView, int width, int height) {
ImageSize size = new ImageSize(width, height);
com.nostra13.universalimageloader.core.ImageLoader.getInstance().displayImage(Uri.parse("file://" + path).toString(), imageView, size);
}

@Override
public void clearMemoryCache() {
}
}

###3、在自定义Application中初始化(别忘了在AndroidManifest.xml中使用该自定义Application):

/**
* @创建者 CSDN_LQR
* @描述 自定义Application类
*/
public class App extends Application {

@Override
public void onCreate() {
super.onCreate();
initUniversalImageLoader();
initImagePicker();
}

private void initUniversalImageLoader() {
//初始化ImageLoader
ImageLoader.getInstance().init(
ImageLoaderConfiguration.createDefault(getApplicationContext()));
}

/**
* 初始化仿微信控件ImagePicker
*/
private void initImagePicker() {
ImagePicker imagePicker = ImagePicker.getInstance();
imagePicker.setImageLoader(new UILImageLoader()); //设置图片加载器
imagePicker.setShowCamera(true); //显示拍照按钮
imagePicker.setCrop(true); //允许裁剪(单选才有效)
imagePicker.setSaveRectangle(true); //是否按矩形区域保存
imagePicker.setSelectLimit(9); //选中数量限制
imagePicker.setStyle(CropImageView.Style.RECTANGLE); //裁剪框的形状
imagePicker.setFocusWidth(800); //裁剪框的宽度。单位像素(圆形自动取宽高最小值)
imagePicker.setFocusHeight(800); //裁剪框的高度。单位像素(圆形自动取宽高最小值)
imagePicker.setOutPutX(1000);//保存文件的宽度。单位像素
imagePicker.setOutPutY(1000);//保存文件的高度。单位像素
}
}

###4、打开图片选择界面代码:

public static final int IMAGE_PICKER = 100;

Intent intent = new Intent(this, ImageGridActivity.class);
startActivityForResult(intent, IMAGE_PICKER);

###5、获取所选图片信息:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == ImagePicker.RESULT_CODE_ITEMS) {//返回多张照片
if (data != null) {
//是否发送原图
boolean isOrig = data.getBooleanExtra(ImagePreviewActivity.ISORIGIN, false);
ArrayList<ImageItem> images = (ArrayList<ImageItem>) data.getSerializableExtra(ImagePicker.EXTRA_RESULT_ITEMS);

Log.e("CSDN_LQR", isOrig ? "发原图" : "不发原图");//若不发原图的话,需要在自己在项目中做好压缩图片算法
for (ImageItem imageItem : images) {
Log.e("CSDN_LQR", imageItem.path);
}
}
} }

代码下载:ImagePicker-master.zip

收起阅读 »

Android高度自定义日历控件-CalenderView

CalenderViewAndroid上一个优雅、高度自定义、性能高效的日历控件,支持标记、自定义颜色、农历等。Canvas绘制,速度快、占用内存低Gradlecompile 'com.haibin:calendarview:1.0.4'<depende...
继续阅读 »

CalenderView

Android上一个优雅、高度自定义、性能高效的日历控件,支持标记、自定义颜色、农历等。Canvas绘制,速度快、占用内存低

Gradle

compile 'com.haibin:calendarview:1.0.4'
<dependency>
<groupId>com.haibin</groupId>
<artifactId>calendarview</artifactId>
<version>1.0.4</version>
<type>pom</type>
</dependency>

使用方法

 <com.haibin.calendarview.CalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:min_year="2004"
app:scheme_text="假"
app:scheme_theme_color="@color/colorPrimary"
app:selected_color="#30cfcfcf"
app:selected_text_color="#333333"
app:week_background="#fff"
app:week_text_color="#111" />

attrs

<declare-styleable name="CalendarView">
       <attr name="week_background" format="color" /> <!--星期栏的背景-->
       <attr name="week_text_color" format="color" /> <!--星期栏文本颜色-->
       <attr name="scheme_theme_color" format="color" /> <!--标记的颜色-->
       <attr name="current_day_color" format="color" /> <!--今天的文本颜色-->
<attr name="scheme_text" format="string" /> <!--标记文本-->
<attr name="selected_color" format="color" /> <!--选中颜色-->
<attr name="selected_text_color" format="color" /> <!--选中文本颜色-->
       <attr name="min_year" format="integer" />  <!--最小年份1900-->
       <attr name="max_year" format="integer" /> <!--最大年份2099-->
</declare-styleable>

api

public int getCurDay(); //今天
public int getCurMonth(); //当前的月份
public int getCurYear(); //今年
public void showSelectLayout(final int year); //快速弹出年份选择月份
public void closeSelectLayout(final int position); //关闭选择年份并跳转日期
public void setOnDateChangeListener(OnDateChangeListener listener);//添加事件
public void setOnDateSelectedListener(OnDateSelectedListener listener);//日期选择事件
public void setSchemeDate(List<Calendar> mSchemeDate);//标记日期
public void setStyle(int schemeThemeColor, int selectLayoutBackground, int lineBg);
public void update();//动态更新

代码下载:CalendarView.zip


收起阅读 »

Git-flow作者称其不适用于持续交付?

Git
前言 Git-flow是由Vincent Driessen在2010年提出的一个Git分支模型,在这10年中,Git-flow在许多软件团队中变得非常流行,以至于人们开始将其视为某种标准。 不过最近Vincent Driessen更新了他10年前那篇著名的A...
继续阅读 »

前言


Git-flow是由Vincent Driessen在2010年提出的一个Git分支模型,在这10年中,Git-flow在许多软件团队中变得非常流行,以至于人们开始将其视为某种标准。
不过最近Vincent Driessen更新了他10年前那篇著名的A successful Git branching model,大意是Git-flow已不适用于当今持续交付的软件工程方式,推荐更简单的Github flow等模型


Git-flow作者都承认Git-flow不适合持续交付了,那我们更有必要好好研究一下了,以免掉坑里。
本文主要包括以下内容:
1.Git-flow介绍
2.为什么Git-flow不适用于持续交付?
3.Github flow介绍
4.Gitlab flow介绍


1. Git-flow是什么?


Git-flow是由Vincent Driessen在2010年提出的一个Git分支模型,其结构图如下所示,相信大家都看过

Git-flow主要包括以下分支



  • master 是长期分支,一般用于管理对外发布版本,每个commit对一个tag,也就是一个发布版本

  • develop 是长期分支,一般用于作为日常开发汇总,即开发版的代码

  • feature 是短期分支,一般用于一个新功能的开发

  • hotfix 是短期分支 ,一般用于正式发布以后,出现bug,需要创建一个分支,进行bug修补。

  • release 是短期分支,一般用于发布正式版本之前(即合并到 master 分支之前),需要对预发布的版本进行测试。release 分支在经历测试之后,测试确认验收,将会被合并到 developmaster


1.1 Git-flow工作流程


一般工作流程如下:



  • 1.日常在develop开发

  • 2.如果有比较大的功能或者其他需求,那么新开分支:feature/xxx 来做,并在这个分支上进行打包和提测。

  • 3.在封版日,将该版本上线的需求合并到develop,然后将开个新的分支release/版本号(如release/1.0.1),将develop合并至该分支。

  • 4.灰度阶段,在releases/版本号 分支上修复BUG,打包并发布,发布完成后反合入masterdevelop分支

  • 5.如果在全量发布后,发现有线上问题,那么在对应的master分支上新开分支hotfix/{版本号}来修复,并升级版本号,修复完成后,然后将hotfix合并到master,同时将合并到develop


2. 为什么Git-flow不适用于持续交付?



在这 10 年中,Git 本身已经席卷全球,并且使用 Git 开发的最受欢迎的软件类型正在更多地转向 Web 应用程序——至少在我的过滤器气泡中。 Web 应用程序通常是持续交付的,而不是回滚的,而且您不必支持同时 运行的多个版本的软件。



Vincent Driessen所述。Git-flow描述了feature分支、release分支、masterdevelop分支以及hotfix分支是如何相互关联的。
这种方法非常适用于用户下载的打包软件,例如库和桌面应用程序。


然而,对于许多Web应用来说,Git-flow是矫枉过正的。有时,您的develop分支和release分支之间没有足够大的差异来区分值得。或者,您的hotfix分支和feature分支的工作流程可能相同。
在这种情况下,Vincent Driessen推荐Github flow分支模型


Git-flow的主要优点在于结构清晰,每个分支的任务划分的很清楚,而它的缺点自然就是有些复杂了
Git-flow需要同时维护两个长期分支。大多数工具都将master当作默认分支,可是开发是在develop分支进行的,这导致经常要切换分支,非常烦人。
更大问题在于,这个模式是基于"版本发布"的,目标是一段时间以后产出一个新版本。但是,很多网站项目是"持续发布",代码一有变动,就部署一次。这时,master分支和develop分支的差别不大,没必要维护两个长期分支


2.1 Git-fow何时值得额外的复杂性


当然,是否使用Git-flow取决于你的业务复杂性,有时使用Git-flow是必须的,主要是当你需要同时维护多版本的时候,适合的是需要『多个版本并存』的场景
所谓『多版本并存』,就是说开发团队要同时维护多个有客户使用的版本,对于传统软件,比如我开发一个新的操作系统叫做Doors,先卖v1,卖出去1000万份,然后看在v1的基础上开发v2,但是客户会持续给v1bug,这些bug既要在v1的后续补丁中fix,也要在v2fix,等v2再卖出去2000万份开始开发v3的时候,v1依然有客户,我就必须要维持v1v2v3三个多版本都要支持。


关于Git-flow同时支持多个版本,很多人可能会有疑问,因为develop只针对一个版本能持续交付
说实话我也感觉挺疑问的,后面查阅资料发现还有一个衍生的support分支,可以同时支持多个版本,在兴趣的同学可参考:mindsers.blog/post/severa…


3.Github flow介绍



Github flow它只有一个长期分支,就是master,因此用起来非常简单。



  • 第一步:根据需求,从master拉出新分支,不区分功能分支或补丁分支。

  • 第二步:新分支开发完成后,或者需要讨论的时候,就向master发起一个pull request(简称PR)。

  • 第三步:Pull Request既是一个通知,让别人注意到你的请求,又是一种对话机制,大家一起评审和讨论你的代码。对话过程中,你还可以不断提交代码

  • 第四步:布署流程:当项目负责人同意新功能可以发布,且代码也通过审核了。但是在代码合并之前还是要进行测试。所以要把feature分支的代码部署到测试环境进行测试

  • 第五步:你的Pull Request被接受,合并进master,重新部署到生产环境后,原来你拉出来的那个分支就被删除。

  • 第六步:修复正式环境bug流程:从master分支切一个HotFix分支,经过以上同样的流程发起PR合并即可


3.1 Github flow的优点


Github flow的最大优点就是简单,对于"持续发布"的产品,可以说是最合适的流程。


3.2 Github flow的缺点


它的问题也在于它的假设:master分支的更新与产品的发布是一致的。也就是说,master分支的最新代码,默认就是当前的线上代码。
可是,有些时候并非如此,代码合并进入master分支,并不代表它就能立刻发布。比如,苹果商店的APP提交审核以后,等一段时间才能上架。这时,如果还有新的代码提交,master分支就会与刚发布的版本不一致。另一个例子是,有些公司有发布窗口,只有指定时间才能发布,这也会导致线上版本落后于master分支。
上面这种情况,只有master一个主分支就不够用了。通常,你不得不在master分支以外,另外新建一个production分支跟踪线上版本。


同时对于Github flow我还有个疑问,合并到master分支后即会部署到生产环境,但是在merge后的代码难道不会产生冲突吗?合并冲突难道不需要重新测试吗?如果评论区有了解的小伙伴可以解惑下


Github flow用起来比较简单,但是在很多公司的业务开发过程中一般都有开发、测试、预发布、生产几个环境,没有强有力的工具来支撑,我认为很难用这种简单的模式来实现管理。
看起来这种模式特别适合小团队,人少,需求少,比较容易通过这种方式管理分支。


4.Gitlab flow介绍


Gitlab flowGit-flowGithub flow的综合。它吸取了两者的优点,既有适应不同开发环境的弹性,又有单一主分支的简单和便利。它是Gitlab.com推荐的做法。
Gitlab flow的最大原则叫做”上游优先”(upsteam first),即只存在一个主分支master,它是所有其他分支的”上游”。只有上游分支采纳的代码变化,才能应用到其他分支。
Gitlab flow分为持续发布与版本发布两种情况,以适应不同的发布类型


4.1 持续发布



对于”持续发布”的项目,它建议在master分支以外,再建立不同的环境分支。
比如,”开发环境”的分支是master,”预发环境”的分支是pre-production,”生产环境”的分支是production


开发分支是预发分支的"上游",预发分支又是生产分支的"上游"。代码的变化,必须由"上游"向"下游"发展。比如,生产环境出现了bug,这时就要新建一个功能分支,先把它合并到master,确认没有问题,再cherry-pickpre-production,这一步也没有问题,才进入production


只有紧急情况,才允许跳过上游,直接合并到下游分支。


4.2 版本发布



对于"版本发布"的项目,建议的做法是每一个稳定版本,都要从master分支拉出一个分支,比如2-3-stable2-4-stable等等。
以后,只有修补bug,才允许将代码合并到这些分支,并且此时要更新小版本号。


4.3 Gitlab flow开发流程


对于Android开发,我们一般使用版本发布,因此我们使用Gitlab flow开发的工作流为



  • 1.新的迭代开始,所有开发人员从主干master拉个人分支开发特性, 分支命名规范 feature-name

  • 2.开发完成后,在迭代结束前,合入master分支

  • 3.master分支合并后,自动cicddev环境

  • 4.开发自测通过后,从master拉取要发布的分支,release-$version,将这个分支部署到测试环境进行测试

  • 5.测出的bug,通过从release-$versio拉出分支进行修复,修复完成后,再合入release-$versio

  • 6.正式发布版本,如果上线后,又有bug,根据5的方式处理

  • 7.等发布版本稳定后,将release-$versio反合入主干master分支


值得注意的是,按照Github flow规范,第5步如果测出bug,应该在master上修改,然后cherry-pickreleases上来,但是这样做太麻烦了,直接在releases分支上修复bug然后再反合入master分支应该是一个简单而且可以接受的做法


总结


正如Vincent Driessen所说的,总而言之,请永远记住,灵丹妙药并不存在。考虑你自己的背景。不要讨厌。自己决定


Git-flow适用于大团队多版本并存迭代的开发流程
Github-flow适用于中小型团队持续集成的开发流程
Gitlab-flow适用范围则介于上面二者之间,支持持续发布与版本发布两种情况


总得来说,各种Git工作流自有其适合工作的场景,毕竟软件工程中没有银弹,读者可根据自己的项目情况对比选择使用,自己决定~


参考资料


如何看待 Git flow 发明人称其不适用于持续交付?
Git 开发工作流程:Git Flow 与 GitHub Flow
Git 工作流程
高效团队的gitlab flow最佳实践

收起阅读 »

Jetpack Compose初体验--(导航、生命周期等)

普通导航 在Jetpack Compose中导航可以使用Jetpack中的Navigation组件,引入相关的扩展依赖就可以了 Navigation官方文档 implementation "androidx.navigation:navigation-co...
继续阅读 »

普通导航


在Jetpack Compose中导航可以使用Jetpack中的Navigation组件,引入相关的扩展依赖就可以了 Navigation官方文档


implementation "androidx.navigation:navigation-compose:2.4.0-alpha01"

使用Navigation导航用到两个比较重要的对象NavHost和NavController。



  • NavHost用来承载页面,和管理导航图

  • NavController用来控制如何导航还有参数回退栈等


导航的路径使用字符串来表示,当使用NavController导航到某个页面的时候,NavHost内部会自动进行页面重组。


来个小栗子实践一下


@Composable
fun MainView(){
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first_screen"){
composable("first_screen"){
FirstScreen(navController = navController)
}
composable("second_screen"){
SecondScreen(navController = navController)
}
composable("third_screen"){
ThirdScreen(navController = navController)
}
}
}


  • 通过rememberNavController()方法创建navController对象

  • 创建NavHost对象,传入navController并指定首页

  • 通过composable()方法来往NavHost中添加页面,构造方法中的字符串就代表该页面的路径,后面的第二个参数就是具体的页面。


下面把这三个页面写出来,每个页面里面都有个按钮继续执行其他导航


@Composable
fun FirstScreen(navController: NavController){
Column(modifier = Modifier.fillMaxSize().background(Color.Blue),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = {
navController.navigate("second_screen")
}) {
Text(text = "I am First 点击我去Second")
}
}
}
@Composable
fun SecondScreen(navController: NavController){
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = {
navController.navigate("third_screen")
}) {
Text(text = "I am Second 点击我去Third")
}
}
}
@Composable
fun ThirdScreen(navController: NavController){
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = {
navController.navigate("first_screen")
}) {
Text(text = "I am Third 点击我去first")
}
}
}

这样一个简单的导航效果就完成了,感觉用了这个之后,要跟activity和fragment说拜拜了~~ ,全场只需一个activity加一堆可组合项(@Composable),新建一个页面简单了太多太多。


当然页面之间跳转传参是少不了的,Compose中如何传参呢?


参数传递肯定有发送端和接收端,navController是发送端,NavHost是接收端。先在NavHost中配置参数占位符,和接收取参数的方法。


@Composable
fun MainView(){
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first_screen"){
composable("first_screen"){
FirstScreen(navController = navController)
}
composable("second_screen/{userId}/{isShow}",
//默认情况下 所有参数都会被解析为字符串 如果不是字符串需要单独指定 type
arguments = listOf(navArgument("isShow"){type = NavType.BoolType})
){ backStackEntry ->
SecondScreen(navController = navController,
backStackEntry.arguments?.getString("userId"),
backStackEntry.arguments?.getBoolean("isShow")!!
)
}
composable("third_screen?selectable={selectable}",
arguments = listOf(navArgument("selectable"){defaultValue = "哈哈哈我是可选参数的默认值"})){
ThirdScreen(navController = navController,it.arguments?.getString("selectable"))
}
composable("four_screen"){
FourScreen(navController = navController)
}
}
}

如上代码,接收参数直接在在该页面地址后面添加参数占位符类似second_screen/{userId}/{isShow},然后通过arguments参数来接收arguments = listOf(navArgument("isShow"){type = NavType.BoolType})。还可以通过defaultValue来定义参数的默认值。


默认情况下 所有参数都会被解析为字符串 如果不是字符串需要单独指定 type。


参数发送端更简单,参数直接跟到页面路径后面就可以,类似navController.navigate("second_screen/12345/true") 下面给前面的页面添加上参数


@Composable
fun FirstScreen(navController: NavController){
Column(modifier = Modifier
.fillMaxSize()
.background(Color.Blue),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = {
navController.navigate("second_screen/12345/true"){
}
}) {
Text(text = "I am First 点击我去Second")
}
Spacer(modifier = Modifier.size(30.dp))
}
}
@Composable
fun SecondScreen(navController: NavController,userId:String?,isShow:Boolean){
Column(modifier = Modifier
.fillMaxSize()
.background(Color.Green),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = {
navController.navigate("third_screen?selectable=测试可选参数"){
popUpTo(navController.graph.startDestinationId){saveState = true}
}
}) {
Text(text = "I am Second 点击我去Third")
}
Spacer(modifier = Modifier.size(30.dp))
Text(text = "arguments ${userId}")
if(isShow){
Text(text = "测试boolean值")
}
}
}
@Composable
fun ThirdScreen(navController: NavController,selectable:String?){
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = {
navController.navigate("first_screen")
}) {
Text(text = "I am Third 点击我去first")
}
Spacer(modifier = Modifier.size(30.dp))
Button(onClick = {
navController.navigate("four_screen")
}) {
Text(text = "I am Third 点击我去four")
}
selectable?.let { Text(text = it) }
}
}

效果如下


copmose_21.gif


生命周期


既然新的界面不使用activity或者fragment了,但是activity和fragment中的生命周期是非常有用的比如创建和销毁某些对象。那么Jetpack Compose中的每个组合函数的生命周期是怎样的呢?


可组合项的生命周期比视图比activity 和 fragment 的生命周期更简单,一般是进入组合、执行0次或者多次重组、退出组合。生命周期相关的函数主要有下面的几个,使用@Composable修饰的可组合函数中没有自带的生命周期函数,想要监听其生命周期,需要使用Effect API



  • LaunchedEffect:第一次调用Compose函数的时候调用

  • DisposableEffect:内部有一个 onDispose()函数,当页面退出时调用

  • SideEffect:compose函数每次执行都会调用该方法


来个小例子体验一下


@Composable
fun LifecycleDemo() {
val count = remember { mutableStateOf(0) }

Column {
Button(onClick = {
count.value++
}) {
Text("Click me")
}

LaunchedEffect(Unit){
Log.d("Compose", "onactive with value: " + count.value)
}
DisposableEffect(Unit) {
onDispose {
Log.d("Compose", "onDispose because value=" + count.value)
}
}
SideEffect {
Log.d("Compose", "onChange with value: " + count.value)
}
Text(text = "You have clicked the button: " + count.value.toString())
}
}

效果如下:


copmose_26.gif


然后把前面的例子稍微改一下,我们把LaunchedEffect和DisposableEffect一起放到一个if语句里面


@Composable
fun LifecycleDemo() {
val count = remember { mutableStateOf(0) }

Column {
Button(onClick = {
count.value++
}) {
Text("Click me")
}

if (count.value < 3) {
LaunchedEffect(Unit){
Log.d("Compose", "onactive with value: " + count.value)
}
DisposableEffect(Unit) {
onDispose {
Log.d("Compose", "onDispose because value=" + count.value)
}
}
}

SideEffect {
Log.d("Compose", "onChange with value: " + count.value)
}
Text(text = "You have clicked the button: " + count.value.toString())
}
}

那么此时的生命周期就是:当首次进入if语句的时候执行LaunchedEffect函数,离开if语句的时候,就执行DisposableEffect方法。


底部导航


说到导航就不得不说底部导航和顶部导航,底部导航的实现非常简单,直接使用JetPack Compose提供的脚手架在结合navController和NavHost就能轻松实现


@Composable
fun BottomMainView(){
val bottomItems = listOf(Screen.First,Screen.Second,Screen.Third)
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
bottomItems.forEach{screen ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite,"") },
label = { Text(stringResource(screen.resourceId)) },
selected = currentRoute == screen.route,
onClick = {
navController.navigate(screen.route){
//当底部导航导航到在非首页的页面时,执行手机的返回键 回到首页
popUpTo(navController.graph.startDestinationId){saveState = true}
//从名字就能看出来 跟activity的启动模式中的SingleTop模式一样 避免在栈顶创建多个实例
launchSingleTop = true
//切换状态的时候保存页面状态
restoreState = true
}
})
}

}
}
){
NavHost(navController = navController, startDestination = Screen.First.route ){
composable(Screen.First.route){
First(navController)
}
composable(Screen.Second.route){
Second(navController)
}
composable(Screen.Third.route){
Third(navController)
}
}
}
}
@Composable
fun First(navController: NavController){
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "First",fontSize = 30.sp)
}
}
@Composable
fun Second(navController: NavController){
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Second",fontSize = 30.sp)
}
}
@Composable
fun Third(navController: NavController){
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Third",fontSize = 30.sp)
}
}

效果如下


copmose_22.gif


顶部导航


顶部导航使用TabRow和ScrollableTabRow这两个组件,其内部都是由一个一个的Tab组件组成。TabRow是平分整个屏幕的宽度,ScrollableTabRow可以超出屏幕宽度并且可以滑动,用法都是一样。


@Composable
fun TopTabRow(){
var state by remember { mutableStateOf(0) }
var titles = listOf("Java","Kotlin","Android","Flutter")
Column {
TabRow(selectedTabIndex = state) {
titles.forEachIndexed{index,title ->
run {
Tab(
selected = state == index,
onClick = { state = index },
text = {
Text(text = title)
})
}
}
}
Column(Modifier.weight(1f)) {
when (state){
0 -> TopTabFirst()
1 -> TopTabSecond()
2 -> TopTabThird()
3 -> TopTabFour()
}
}
}
}
@Composable
fun TopTabFirst(){
Column(modifier = Modifier.fillMaxSize(), verticalArrangement=Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Java")
}
}
@Composable
fun TopTabSecond(){
Column(modifier = Modifier.fillMaxSize(), verticalArrangement=Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Kotlin")
}
}
@Composable
fun TopTabThird(){
Column(modifier = Modifier.fillMaxSize(), verticalArrangement=Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Android")
}
}
@Composable
fun TopTabFour(){
Column(modifier = Modifier.fillMaxSize(), verticalArrangement=Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Flutter")
}
}

copmose_23.gif


上面只能实现点击每个Tab 切换不同的页面,如果我们想要实现类似我们在xml布局中的ViewPage+TabLayout的效果呢


在Jetpack中怎么实现ViewPage的效果呢,Google的github上提供了一个半官方的库名字叫pager:github.com/google/acco…


implementation "com.google.accompanist:accompanist-pager:0.13.0"

该库目前还是实验性的,以后API都可能会修改,目前使用的时候需要使用@ExperimentalPagerApi注解标记。


@ExperimentalPagerApi
@Composable
fun TopScrollTabRow(){
var titles = listOf("Java","Kotlin","Android","Flutter","scala","python")
val scope = rememberCoroutineScope()
var pagerState = rememberPagerState(
pageCount = titles.size, //总页数
initialOffscreenLimit = 2, //预加载的个数
infiniteLoop = true, //无限循环
initialPage = 0, //初始页面
)
Column {
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
modifier = Modifier.wrapContentSize(),
edgePadding = 16.dp
) {
titles.forEachIndexed{index,title ->
run {
Tab(
selected = pagerState.currentPage == index,
onClick = {
scope.launch {
pagerState.scrollToPage(index)
}
},
text = {
Text(text = title)
})
}
}
}
HorizontalPager(
state=pagerState,
modifier = Modifier.weight(1f)
) {index ->
Column(modifier = Modifier.fillMaxSize(),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = titles[index])
}
}
}
}

pagerState.scrollToPage(index)方法可以控制pager滚动,不过它是一个suspend修饰的方法,需要运行在协程中,在jetpack compose中使用协程可以使用rememberCoroutineScope()方法来获取一个compose中的协程的作用域


效果如下:


copmose_24.gif


Banner


pager库都引入了那顺便吧Banner效果也练习一下,为了显示网络图片还得引入一个新的库,accompanist-coil。在JetPack Compose中官方提供了两个显示网络图片的库accompanist-coil和accompanist-glide,这里使用accompanist-coil。


implementation 'com.google.accompanist:accompanist-coil:0.11.1'

@ExperimentalPagerApi
@Composable
fun Third(navController: NavController){
var pics = listOf("https://wanandroid.com/blogimgs/8a0131ac-05b7-4b6c-a8d0-f438678834ba.png",
"https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png",
"https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png",
"https://www.wanandroid.com/blogimgs/90c6cc12-742e-4c9f-b318-b912f163b8d0.png")
Column(modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "Third",fontSize = 30.sp)
var pagerState = rememberPagerState(
pageCount = 4, //总页数
initialOffscreenLimit = 2, //预加载的个数
infiniteLoop = true, //无限循环
initialPage = 0, //初始页面
)
Box(modifier = Modifier
.fillMaxWidth()
.height(260.dp)
.background(color = Color.Yellow)) {
HorizontalPager(
state=pagerState,
modifier = Modifier.fillMaxSize()
) {index ->
Image(modifier = Modifier.fillMaxSize(),
painter = rememberCoilPainter(request = pics[index]),
contentScale=ContentScale.Crop,
contentDescription = "图片描述")
}
HorizontalPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.padding(16.dp).align(Alignment.BottomStart),
)
}
}
}

使用Jetpack Compose写页面感觉比使用xml简单了很多,相信未来Android中的xml布局会像前端的jquary一样用的越来越少。



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

使用更为安全的方式收集 Android UI 数据流

在 Android 应用中,通常需要从 UI 层收集 Kotlin 数据流,以便在屏幕上显示数据更新。同时,您也会希望通过收集这些数据流,来避免产生不必要的操作和资源浪费 (包括 CPU 和内存),以及防止在 View 进入后台时泄露数据。 本文将会带您学习如...
继续阅读 »

在 Android 应用中,通常需要从 UI 层收集 Kotlin 数据流,以便在屏幕上显示数据更新。同时,您也会希望通过收集这些数据流,来避免产生不必要的操作和资源浪费 (包括 CPU 和内存),以及防止在 View 进入后台时泄露数据。


本文将会带您学习如何使用 LifecycleOwner.addRepeatingJobLifecycle.repeatOnLifecycle 以及 Flow.flowWithLifecycle API 来避免资源的浪费;同时也会介绍为什么这些 API 适合作为在 UI 层收集数据流时的默认选择。


资源浪费


无论数据流生产者的具体实现如何,我们都 推荐 从应用的较底层级暴露 Flow API。不过,您也应该保证数据流收集操作的安全性。


使用一些现存 API (如 CoroutineScope.launchFlow.launchInLifecycleCoroutineScope.launchWhenX) 收集基于 channel 或使用带有缓冲的操作符 (如 bufferconflateflowOnshareIn) 的冷流的数据是 不安全的,除非您在 Activity 进入后台时手动取消启动了协程的 Job。这些 API 会在内部生产者在后台发送项目到缓冲区时保持它们的活跃状态,而这样一来就浪费了资源。



注意: 冷流 是一种数据流类型,这种数据流会在新的订阅者收集数据时,按需执行生产者的代码块。



例如下面的例子中,使用 callbackFlow 发送位置更新的数据流:‍


// 基于 Channel 实现的冷流,可以发送位置的更新
fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e) // 在出现异常时关闭 Flow
}
// 在 Flow 收集结束时进行清理操作
awaitClose {
removeLocationUpdates(callback)
}
}
复制代码


注意: callbackFlow 内部使用 channel 实现,其概念与阻塞 队列 十分类似,并且默认容量为 64。



使用任意前述 API 从 UI 层收集此数据流都会导致其持续发送位置信息,即使视图不再展示数据也不会停止!示例如下:


class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// 最早在 View 处于 STARTED 状态时从数据流收集数据,并在
// 生命周期进入 STOPPED 状态时 SUSPENDS(挂起)收集操作。
// 在 View 转为 DESTROYED 状态时取消数据流的收集操作。
lifecycleScope.launchWhenStarted {
locationProvider.locationFlow().collect {
// 新的位置!更新地图
}
}
// 同样的问题也存在于:
// - lifecycleScope.launch { /* 在这里从 locationFlow() 收集数据 */ }
// - locationProvider.locationFlow().onEach { /* ... */ }.launchIn(lifecycleScope)
}
}
复制代码

lifecycleScope.launchWhenStarted 挂起了协程的执行。虽然新的位置信息没有被处理,但 callbackFlow 生产者仍然会持续发送位置信息。使用 lifecycleScope.launchlaunchIn API 会更加危险,因为视图会持续消费位置信息,即使处于后台也不会停止!这种情况可能会导致您的应用崩溃。


为了解决这些 API 所带来的问题,您需要在视图转入后台时手动取消收集操作,以取消 callbackFlow 并避免位置提供者持续发送项目并浪费资源。举例来说,您可以像下面的例子这样操作:


class LocationActivity : AppCompatActivity() {

// 位置的协程监听器
private var locationUpdatesJob: Job? = null

override fun onStart() {
super.onStart()
locationUpdatesJob = lifecycleScope.launch {
locationProvider.locationFlow().collect {
// 新的位置!更新地图。
}
}
}

override fun onStop() {
// 在视图进入后台时停止收集数据
locationUpdatesJob?.cancel()
super.onStop()
}
}
复制代码

这是一个不错的解决方案,美中不足的是有些冗长。如果这个世界有一个有关 Android 开发者的普遍事实,那一定是我们都不喜欢编写模版代码。不必编写模版代码的一个最大好处就是——写的代码越少,出错的概率越小!


LifecycleOwner.addRepeatingJob


现在我们境遇相同,并且也知道问题出在哪里,是时候找出一个解决方案了。我们的解决方案需要: 1. 简单;2. 友好或者说便于记忆与理解;更重要的是 3. 安全!无论数据流的实现细节如何,它都应能够应对所有用例。


事不宜迟——您应该使用的 API 是 lifecycle-runtime-ktx 库中所提供的 LifecycleOwner.addRepeatingJob。请参考下面的代码:


class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// 最早在 View 处于 STARTED 状态时从数据流收集数据,并在
// 生命周期进入 STOPPED 状态时 STOPPED(停止)收集操作。
// 它会在生命周期再次进入 STARTED 状态时自动开始进行数据收集操作。
lifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// 新的位置!更新地图
}
}
}
}
复制代码

addRepeatingJob 接收 Lifecycle.State 作为参数,并用它与传入的代码块一起,在生命周期到达该状态时,自动创建并启动新的协程;同时也会在生命周期低于该状态时取消正在运行的协程


由于 addRepeatingJob 会在协程不再被需要时自动将其取消,因而可以避免产生取消操作相关的模版代码。您也许已经猜到,为了避免意外行为,这一 API 需要在 Activity 的 onCreate 或 Fragment 的 onViewCreated 方法中调用。下面是配合 Fragment 使用的示例:


class LocationFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// ...
viewLifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// 新的位置!更新地图
}
}
}
}
复制代码


注意: 这些 API 在 lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 库或其更新的版本中可用。



使用 repeatOnLifecycle


出于提供更为灵活的 API 以及保存调用中的 CoroutineContext 的目的,我们也提供了 挂起函数 Lifecycle.repeatOnLifecycle 供您使用。repeatOnLifecycle 会挂起调用它的协程,并会在进出目标状态时重新执行代码块,最后在 Lifecycle 进入销毁状态时恢复调用它的协程。


如果您需要在重复工作前执行一次配置任务,同时希望任务可以在重复工作开始前保持挂起,该 API 可以帮您实现这样的操作。示例如下:


class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

lifecycleScope.launch {
// 单次配置任务
val expensiveObject = createExpensiveObject()

lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// 在生命周期进入 STARTED 状态时开始重复任务,在 STOPED 状态时停止
// 对 expensiveObject 进行操作
}

// 当协程恢复时,`lifecycle` 处于 DESTROY 状态。repeatOnLifecycle 会在
// 进入 DESTROYED 状态前挂起协程的执行
}
}
}
复制代码

Flow.flowWithLifecycle


当您只需要收集一个数据流时,也可以使用 Flow.flowWithLifecycle 操作符。这一 API 的内部也使用 suspend Lifecycle.repeatOnLifecycle 函数实现,并会在生命周期进入和离开目标状态时发送项目和取消内部的生产者。


class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

locationProvider.locationFlow()
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.onEach {
// 新的位置!更新地图
}
.launchIn(lifecycleScope)
}
}
复制代码


注意: Flow.flowWithLifecycle API 的命名以 Flow.flowOn(CoroutineContext) 为先例,因为它会在不影响下游数据流的同时修改收集上游数据流的 CoroutineContext。与 flowOn 相似的另一点是,Flow.flowWithLifecycle 也加入了缓冲区,以防止消费者无法跟上生产者。这一特点源于其实现中使用的 callbackFlow



配置内部生产者


即使您使用了这些 API,也要小心那些可能浪费资源的热流,就算它们没有被收集亦是如此!虽然针对这些热流有一些合适的用例,但是仍要多加注意并在必要时进行记录。另一方面,在一些情况下,即使可能造成资源的浪费,令处于后台的内部数据流生产者保持活跃状态也会利于某些用例,如: 您需要即时刷新可用数据,而不是去获取并暂时展示陈旧数据。您可以根据用例决定生产者是否需要始终处于活跃状态


您可以使用 MutableStateFlowMutableSharedFlow 两个 API 中暴露的 subscriptionCount 字段来控制它们,当该字段值为 0 时,内部的生产者就会停止。默认情况下,只要持有数据流实例的对象还在内存中,它们就会保持生产者的活跃状态。针对这些 API 也有一些合适的用例,比如使用 StateFlowUiState 从 ViewModel 中暴露给 UI。这么做很合适,因为它意味着 ViewModel 总是需要向 View 提供最新的 UI 状态。


相似的,也可以为此类操作使用 共享开始策略 配置 Flow.stateInFlow.shareIn 操作符。WhileSubscribed() 将会在没有活跃的订阅者时停止内部的生产者!相应的,无论数据流是 Eagerly (积极) 还是 Lazily (惰性) 的,只要它们使用的 CoroutineScope 还处于活跃状态,其内部的生产者就会保持活跃。



注意: 本文中所描述的 API 可以很好的作为默认从 UI 收集数据流的方式,并且无论数据流的实现方式如何,都应该使用它们。这些 API 做了它们要做的事: 在 UI 于屏幕中不可见时,停止收集其数据流。至于数据流是否应该始终处于活动状态,则取决于它的实现。



在 Jetpack Compose 中安全地收集数据流


Flow.collectAsState 函数可以在 Compose 中收集来自 composable 的数据流,并可以将值表示为 State,以便能够更新 Compose UI。即使 Compose 在宿主 Activity 或 Fragment 处于后台时不会重组 UI,数据流生产者仍会保持活跃并会造成资源的浪费。Compose 可能会遭遇与 View 系统相同的问题。


在 Compose 中收集数据流时,可以使用 Flow.flowWithLifecycle 操作符,示例如下:


@Composable
fun LocationScreen(locationFlow: Flow<Flow>) {

val lifecycleOwner = LocalLifecycleOwner.current
val locationFlowLifecycleAware = remember(locationFlow, lifecycleOwner) {
locationFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
}

val location by locationFlowLifecycleAware.collectAsState()

// 当前位置,可以拿它做一些操作
}
复制代码

注意,您 需要记得 生命周期感知型数据流使用 locationFlowlifecycleOwner 作为键,以便始终使用同一个数据流,除非其中一个键发生改变。


Compose 的副作用 (Side-effect) 便是必须处在 受控环境中,因此,使用 LifecycleOwner.addRepeatingJob 不安全。作为替代,可以使用 LaunchedEffect 来创建跟随 composable 生命周期的协程。在它的代码块中,如果您需要在宿主生命周期处于某个 State 时重新执行一个代码块,可以调用挂起函数 Lifecycle.repeatOnLifecycle


对比 LiveData


您也许会觉得,这些 API 的表现与 LiveData 很相似——确实是这样!LiveData 可以感知 Lifecycle,而且它的重启行为使其十分适合观察来自 UI 的数据流。同理 LifecycleOwner.addRepeatingJobsuspend Lifecycle.repeatOnLifecycle 以及 Flow.flowWithLifecycle 等 API 亦是如此。


在纯 Kotlin 应用中,使用这些 API 可以十分自然地替代 LiveData 收集数据流。如果您使用这些 API 收集数据流,换成 LiveData (相对于使用协程和 Flow) 不会带来任何额外的好处。而且由于 Flow 可以从任何 Dispatcher 收集数据,同时也能通过它的 操作符 获得更多功能,所以 Flow 也更为灵活。相对而言,LiveData 的可用操作符有限,且它总是从 UI 线程观察数据。


数据绑定对 StateFlow 的支持


另一方面,您会想要使用 LiveData 的原因之一,可能是它受到数据绑定的支持。不过 StateFlow 也一样!更多有关数据绑定对 StateFlow 的支持信息,请参阅 官方文档


在 Android 开发中,请使用 LifecycleOwner.addRepeatingJobsuspend Lifecycle.repeatOnLifecycle Flow.flowWithLifecycle 从 UI 层安全地收集数据流。


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

TurboDex: 在Android瞬间加载Dex

众所周知,Android中在Runtime加载一个 未优化的Dex文件 (尤其在 ART 模式)需要花费 很长的时间. 当你在App中使用 插件化框架 的时候, 首次加载插件就需要耗费很长的时间.Qu...
继续阅读 »

TurboDex: 在Android瞬间加载Dex

众所周知,Android中在Runtime加载一个 未优化的Dex文件 (尤其在 ART 模式)需要花费 很长的时间. 当你在App中使用 插件化框架 的时候, 首次加载插件就需要耗费很长的时间.

TurboDex 就是为了解决这一问题而生, 就像是给AndroidVM开启了上帝模式, 在引入TurboDex后, 无论你加载了多大的Dex文件,都可以在毫秒级别内完成.

Quick Start Guide

Building TurboDex

TurboDex的 pre-compiled 版本在 /Prebuilt 目录下, 如果你想要构建自己的TurboDex, 你需要安装 Android-NDK.

 lody@MacBook-Pro  ~/TurboDex/TurboDex/jni> ndk-build                  
SharedLibrary : libturbo-dex.so
Install : libturbo-dex.so => libs/armeabi/libturbo-dex.so
SharedLibrary : libturbo-dex.so
Install : libturbo-dex.so => libs/x86/libturbo-dex.so

Config

Maven


com.github.asLody
turbodex
1.1.0
pom

Gradle

compile 'com.github.asLody:turbodex:1.1.0'

Usage

使用TurboDex, 你需要将library 添加到你的项目中, 在 Application 中写入以下代码:


@Override
protected void attachBaseContext(Context base) {
TurboDex.enableTurboDex();
super.attachBaseContext(base);
}

开启 TurboDex后, 下列调用都不再成为拖慢你App运行的元凶:

new DexClassLoader(...):

DexFile.loadDex(...);

##其它的分析和评论 http://note.youdao.com/share/?id=28e62692d218a1f1faef98e4e7724f22&type=note#/

然而,不知道这篇笔记的作者为什么会认为Hook模块是我实现的, 我并没有给Substrate那部分的模块自己命名,而是采用了原名:MSHook, 而且, 所有的Cydia源码我也保留了头部的协议申明,你知道源码的出处,却没有意识到这一点?

代码下载:lody-WelikeAndroid-master.zip

收起阅读 »

WelikeAndroid 是一款引入即用的便捷开发框架,一行代码完成http请求,bitmap异步加载,数据库增删查改,同时拥有最超前的异常隔离机制!

##WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.##Welike带来了哪些特征?WelikeAndroid...
继续阅读 »

##WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,
使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.

##Welike带来了哪些特征?

WelikeAndroid目前包含五个大模块:

  • 异常安全隔离模块(实验阶段):当任何线程抛出任何异常,我们的异常隔离机制都会让UI线程继续运行下去.
  • Http模块: 一行代码完成POST、GET请求和Download,支持上传, 高度优化Disk的缓存加载机制,
    自由设置缓存大小、缓存时间(也支持永久缓存和不缓存).
  • Bitmap模块: 一行代码完成异步显示图片,无需考虑OOM问题,支持加载前对图片做自定义处理.
  • Database模块: 支持NotNull,Table,ID,Ignore等注解,Bean无需Getter和Setter,一键式部署数据库.
  • ui操纵模块: 我们为Activity基类做了完善的封装,继承基类可以让代码更加优雅.
  • :请不要认为功能相似,框架就不是原创,源码摆在眼前,何不看一看?

使用WelikeAndroid需要以下权限:

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

##下文将教你如何圆润的使用WelikeAndroid:
###通过WelikeContext在任意处取得上下文:

  • WelikeContext.getApplication(); 就可以取得当前App的上下文
  • WelikeToast.toast("你好!"); 简单一步弹出Toast.

##WelikeGuard(异常安全隔离机制用法):

  • 第一步,开启异常隔离机制:
WelikeGuard.enableGuard();
  • 第二步,注册一个全局异常监听器:

WelikeGuard.registerUnCaughtHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable ex) {

WelikeGuard.newThreadToast("出现异常了: " + ex.getMessage() );

}
});
  • 你也可以自定义异常:

/**
*
* 自定义的异常,当异常被抛出后,会自动回调onCatchThrowable函数.
*/
@Catch(process = "onCatchThrowable")
public class CustomException extends IllegalAccessError {

public static void onCatchThrowable(Thread t){
WeLog.e(t.getName() + " 抛出了一个异常...");
}
}
  • 另外,继承自UncaughtThrowable的异常我们不会对其进行拦截.

使用Welike做屏幕适配:

Welike的ViewPorter类提供了屏幕适配的Fluent-API,我们可以通过一组流畅的API轻松做好屏幕适配.

        ViewPorter.from(button).ofScreen().divWidth(2).commit();//宽度变为屏幕的二分之一
ViewPorter.from(button).of(viewGroup).divHeight(2).commit();//高度变为viewGroup的二分之一
ViewPorter.from(button).div(2).commit();//宽度和高度变为屏幕的四分之一
ViewPorter.from(button).of(this).fillWidth().fillHeight().commit();//宽度和高度铺满Activity
ViewPorter.from(button).sameAs(imageView).commit();//button的宽度和高度和imageView一样

WelikeHttp入门:

首先来看看框架的调试信息,是不是一目了然. DEBUG DEBUG2

  • 第一步,取得WelikeHttp默认实例.
WelikeHttp welikeHttp = WelikeHttp.getDefault();
  • 第二步,发送一个Get请求.
HttpParams params = new HttpParams();
params.putParams("app","qr.get",
"data","Test");//一次性放入两对 参数 和 值

//发送Get请求
HttpRequest request = welikeHttp.get("http://api.k780.com:88", params, new HttpResultCallback() {
@Override
public void onSuccess(String content) {
super.onSuccess(content);
WelikeToast.toast("返回的JSON为:" + content);
}

@Override
public void onFailure(HttpResponse response) {
super.onFailure(response);
WelikeToast.toast("JSON请求发送失败.");
}

@Override
public void onCancel(HttpRequest request) {
super.onCancel(request);
WelikeToast.toast("请求被取消.");
}
});

//取消请求,会回调onCancel()
request.cancel();

当然,我们为满足需求提供了多种扩展的Callback,目前我们提供以下Callback供您选择:

  • HttpCallback(响应为byte[]数组)
  • FileUploadCallback(仅在上传文件时使用)
  • HttpBitmapCallback(建议使用Bitmap模块)
  • HttpResultCallback(响应为String)
  • DownloadCallback(仅在download时使用)

如需自定义Http模块的配置(如缓存时间),请查看HttpConfig.

WelikeBitmap入门:

  • 第一步,取得默认的WelikeBitmap实例:

//取得默认的WelikeBitmap实例
WelikeBitmap welikeBitmap = WelikeBitmap.getDefault();
  • 第二步,异步加载一张图片:
BitmapRequest request = welikeBitmap.loadBitmap(imageView,
"http://img0.imgtn.bdimg.com/it/u=937075122,1381619862&fm=21&gp=0.jpg",
android.R.drawable.btn_star,//加载中显示的图片
android.R.drawable.ic_delete,//加载失败时显示的图片
new BitmapCallback() {

@Override
public Bitmap onProcessBitmap(byte[] data) {
//如果需要在加载时处理图片,可以在这里处理,
//如果不需要处理,就返回null或者不复写这个方法.
return null;
}

@Override
public void onPreStart(String url) {
super.onPreStart(url);
//加载前回调
WeLog.d("===========> onPreStart()");
}

@Override
public void onCancel(String url) {
super.onCancel(url);
//请求取消时回调
WeLog.d("===========> onCancel()");
}

@Override
public void onLoadSuccess(String url, Bitmap bitmap) {
super.onLoadSuccess(url, bitmap);
//图片加载成功后回调
WeLog.d("===========> onLoadSuccess()");
}

@Override
public void onRequestHttp(HttpRequest request) {
super.onRequestHttp(request);
//图片需要请求http时回调
WeLog.d("===========> onRequestHttp()");
}

@Override
public void onLoadFailed(HttpResponse response, String url) {
super.onLoadFailed(response, url);
//请求失败时回调
WeLog.d("===========> onLoadFailed()");
}
});
  • 如果需要自定义Config,请看BitmapConfig这个类.

##WelikeDAO入门:

  • 首先写一个Bean.

/*表名,可有可无,默认为类名.*/
@Table(name="USER",afterTableCreate="afterTableCreate")
public class User{
@ID
public int id;//id可有可无,根据自己是否需要来加.

/*这个注解表示name字段不能为null*/
@NotNull
public String name;

public static void afterTableCreate(WelikeDao dao){
//在当前的表被创建时回调,可以在这里做一些表的初始化工作
}
}
  • 然后将它写入到数据库
WelikeDao db = WelikeDao.instance("Welike.db");
User user = new User();
user.name = "Lody";
db.save(user);
  • 从数据库取出Bean

User savedUser = db.findBeanByID(1);
  • SQL复杂条件查询
List<User> users = db.findBeans().where("name = Lody").or("id = 1").find();
  • 更新指定ID的Bean
User wantoUpdateUser = new User();
wantoUpdateUser.name = "NiHao";
db.updateDbByID(1,wantoUpdateUser);
  • 删除指ID定的Bean
db.deleteBeanByID(1);
  • 更多实例请看DEMO和API文档.

##十秒钟学会WelikeActivity

  • 我们将Activity的生命周期划分如下:

=>@initData(所有标有InitData注解的方法都最早在子线程被调用)
=>initRootView(bundle)
=>@JoinView(将标有此注解的View自动findViewByID和setOnClickListener)
=>onDataLoaded(数据加载完成时回调)
=>点击事件会回调onWidgetClick(View Widget)

###关于@JoinView的细节:

  • 有以下三种写法:
@JoinView(name = "welike_btn")
Button welikeBtn;
@JoinView(id = R.id.welike_btn)
Button welikeBtn;
@JoinView(name = "welike_btn",click = false)
Button welikeBtn;
  • clicktrue时会自动调用view的setOnClickListener方法,并在onWidgetClick回调.
  • 当需要绑定的是一个Button的时候, click属性默认为true,其它的View则默认为false.
收起阅读 »

Java原生的Http网络框架,底层基于HttpNet,动态代理+构建的!

#Elegant项目结构如下 Elegant采用Retrofit动态代理+构建的思想,本身并不做网络请求,网络部分基于HttpNet实现,本着简洁清晰的思想,保持了和Retrofit相似的API##gradlecompile 'com.haibin:...
继续阅读 »


#Elegant项目结构如下 输入图片说明

Elegant采用Retrofit动态代理+构建的思想,本身并不做网络请求,网络部分基于HttpNet实现,本着简洁清晰的思想,保持了和Retrofit相似的API

##gradle

compile 'com.haibin:elegant:1.1.9'

##创建API接口

public interface LoginService {

//普通POST
@Headers({"Cookie:cid=adcdefg;"})
@POST("api/users/login")
Call<BaseModel<User>> login(@Form("email") String email,
@Form("pwd") String pwd,
@Form("versionNum") int versionNum,
@Form("dataFrom") int dataFrom);

// 上传文件
@POST("action/apiv2/user_edit_portrait")
@Headers("Cookie:xxx=hbbb;")
Call<String> postAvatar(@File("portrait") String file);


//JSON POST
@POST("action/apiv2/user_edit_portrait")
@Headers("Cookie:xxx=hbbb;")
Call<String> postJson(@Json String file);

//PATCH
@PATCH("mobile/user/{uid}/online")
Call<ResultBean<String>> handUp(@Path("uid") long uid);
}

##执行请求

public static final String API = "http://www.oschina.net/";
public static Elegant elegant = new Elegant();

static {
elegant.registerApi(API);
}

LoginService service = elegant.from(LoginService.class)
.login("xxx@qq.com", "123456", 2, 2);
.withHeaders(Headers...)
.execute(new CallBack<BaseModel<User>>() {
@Override
public void onResponse(Response<BaseModel<User>> response) {

}

@Override
public void onFailure(Exception e) {

}                               });

代码下载:dev-Elegant-master.zip

收起阅读 »

CSS 奇思妙想 | 巧妙的实现带圆角的三角形

之前在这篇文章中 -- 《老生常谈之 CSS 实现三角形》,介绍了 6 种使用 CSS 实现三角形的方式。 但是其中漏掉了一个非常重要的场景,如何使用纯 CSS 实现带圆角的三角形呢?,像是这样: 本文将介绍几种实现带圆角的三角形的实现方式。 法一. 全兼容...
继续阅读 »

之前在这篇文章中 -- 《老生常谈之 CSS 实现三角形》,介绍了 6 种使用 CSS 实现三角形的方式。


但是其中漏掉了一个非常重要的场景,如何使用纯 CSS 实现带圆角的三角形呢?,像是这样:


A triangle with rounded


本文将介绍几种实现带圆角的三角形的实现方式。


法一. 全兼容的 SVG 大法


想要生成一个带圆角的三角形,代码量最少、最好的方式是使用 SVG 生成。


使用 SVG 的 多边形标签 <polygon> 生成一个三边形,使用 SVG 的 stroke-linejoin="round" 生成连接处的圆角。


代码量非常少,核心代码如下:


<svg  width="250" height="250" viewBox="-50 -50 300 300">
<polygon class="triangle" stroke-linejoin="round" points="100,0 0,200 200,200"/>
</svg>

.triangle {
fill: #0f0;
stroke: #0f0;
stroke-width: 10;
}

实际图形如下:


A triangle with rounded


这里,其实是借助了 SVG 多边形的 stroke-linejoin: round 属性生成的圆角,stroke-linejoin 是什么?它用来控制两条描边线段之间,有三个可选值:



  • miter 是默认值,表示用方形画笔在连接处形成尖角

  • round 表示用圆角连接,实现平滑效果

  • bevel 连接处会形成一个斜接



我们实际是通过一个带边框,且边框连接类型为 stroke-linejoin: round 的多边形生成圆角三角形的


如果,我们把底色和边框色区分开,实际是这样的:


.triangle {
fill: #0f0;
stroke: #000;
stroke-width: 10;
}


通过 stroke-width 控制圆角大小


那么如何控制圆角大小呢?也非常简单,通过控制 stroke-width 的大小,可以改变圆角的大小。


当然,要保持三角形大小一致,在增大/缩小 stroke-width 的同时,需要缩小/增大图形的 width/height



完整的 DEMO 你可以戳这里:CodePen Demo -- 使用 SVG 实现带圆角的三角形


法二. 图形拼接


不过,上文提到了,使用纯 CSS 实现带圆角的三角形,但是上述第一个方法其实是借助了 SVG。那么仅仅使用 CSS,有没有办法呢?


当然,发散思维,CSS 有意思的地方正在于此处,用一个图形,能够有非常多种巧妙的解决方案!


我们看看,一个圆角三角形,它其实可以被拆分成几个部分:



所以,其实我们只需要能够画出一个这样的带圆角的菱形,通过 3 个进行旋转叠加,就能得到圆角三角形:



绘制带圆角的菱形


那么,接下来我们的目标就变成了绘制一个带圆角的菱形,方法有很多,本文给出其中一种方式:



  1. 首先将一个正方形变成一个菱形,利用 transform 有一个固定的公式:



<div></div>

div {
width: 10em;
height: 10em;
transform: rotate(-60deg) skewX(-30deg) scale(1, 0.866);
}



  1. 将其中一个角变成圆角:


div {
width: 10em;
height: 10em;
transform: rotate(-60deg) skewX(-30deg) scale(1, 0.866);
+ border-top-right-radius: 30%;
}


至此,我们就顺利的得到一个带圆角的菱形了!


拼接 3 个带圆角的菱形


接下来就很简单了,我们只需要利用元素的另外两个伪元素,再生成 2 个带圆角的菱形,将一共 3 个图形旋转位移拼接起来即可!


完整的代码如下:


<div></div>

div{
position: relative;
background-color: orange;
}
div:before,
div:after {
content: '';
position: absolute;
background-color: inherit;
}
div,
div:before,
div:after {
width: 10em;
height: 10em;
border-top-right-radius: 30%;
}
div {
transform: rotate(-60deg) skewX(-30deg) scale(1,.866);
}
div:before {
transform: rotate(-135deg) skewX(-45deg) scale(1.414, .707) translate(0,-50%);
}
div:after {
transform: rotate(135deg) skewY(-45deg) scale(.707, 1.414) translate(50%);
}

就可以得到一个圆角三角形了!效果如下:


image


完整的代码你可以戳这里:CodePen Demo -- A triangle with rounded


法三. 图形拼接实现渐变色圆角三角形


完了吗?没有!


上述方案,虽然不算太复杂,但是有一点还不算太完美的。就是无法支持渐变色的圆角三角形。像是这样:



如果需要实现渐变色圆角三角形,还是有点复杂的。但真就还有人鼓捣出来了,下述方法参考至 -- How to make 3-corner-rounded triangle in CSS


同样也是利用了多块进行拼接,但是这次我们的基础图形,会非常的复杂。


首先,我们需要实现这样一个容器外框,和上述的方法比较类似,可以理解为是一个圆角菱形(画出 border 方便理解):



<div></div>

div {
width: 200px;
height: 200px;
transform: rotate(30deg) skewY(30deg) scaleX(0.866);
border: 1px solid #000;
border-radius: 20%;
}

接着,我们同样使用两个伪元素,实现两个稍显怪异的图形进行拼接,算是对 transform 的各种用法的合集:


div::before,
div::after {
content: "";
position: absolute;
width: 200px;
height: 200px;
}
div::before {
border-radius: 20% 20% 20% 55%;
transform: scaleX(1.155) skewY(-30deg) rotate(-30deg) translateY(-42.3%) skewX(30deg) scaleY(0.866) translateX(-24%);
background: red;
}
div::after {
border-radius: 20% 20% 55% 20%;
background: blue;
transform: scaleX(1.155) skewY(-30deg) rotate(-30deg) translateY(-42.3%) skewX(-30deg) scaleY(0.866) translateX(24%);
}

为了方便理解,制作了一个简单的变换动画:



本质就是实现了这样一个图形:


image


最后,给父元素添加一个 overflow: hidden 并且去掉父元素的 border 即可得到一个圆角三角形:



由于这两个元素重叠空间的特殊结构,此时,给两个伪元素添加同一个渐变色,会完美的叠加在一起:


div::before,
div::after, {
background: linear-gradient(#0f0, #03a9f4);
}

最终得到一个渐变圆角三角形:



上述各个图形的完整代码,你可以戳这里:CodePen Demo -- A triangle with rounded and gradient background


最后


本文介绍了几种在 CSS 中实现带圆角三角形的方式,虽然部分有些繁琐,但是也体现了 CSS ”有趣且折磨人“ 的一面,具体应用的时候,还是要思考一下,对是否使用上述方式进行取舍,有的时候,切图也许是更好的方案。


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

收起阅读 »

微前端模块共享你真的懂了吗

前言:我们运用微前端架构解决了应用体积庞大的问题,通过实践微前端的理念,将前端应用拆分为多个微应用(可独立部署、松散耦合的应用)。同时微应用的存在,使得我们无需在构建一个庞大的应用,而是按需构建,极大了加快了构建效率。但只是解决了应用层面的问题,在中后台应用场...
继续阅读 »

前言:我们运用微前端架构解决了应用体积庞大的问题,通过实践微前端的理念,将前端应用拆分为多个微应用(可独立部署、松散耦合的应用)。同时微应用的存在,使得我们无需在构建一个庞大的应用,而是按需构建,极大了加快了构建效率。但只是解决了应用层面的问题,在中后台应用场景中,不同微应用和基座之间可能存在通用的模块依赖,那么如果应用间可以实现模块共享,那么可以大大优化单应体积大小



image.png


1.Npm 依赖



最简单的方式,就是把需要共享的模块抽出,可能是一个工具库,有可能是一个组件库,然后讲其打包成为npm包,然后在每个子应用中都安装该模块依赖,以此达到多个项目复用的效果



也就代表每个应用都有相同的npm包,本质上没有真正意义上的实现模块共享和复用,只是代码层次共享和复用了,应用打包构建时,还是会将依赖包一起打包


image.png


劣势有以下👇 几点:



  • 每个微应用都会打包该模块,导致依赖的包冗余,没有真正意义上的共享复用

  • npm包进行更新发布了,微应用还需要重新构建,调试麻烦且低效 (除非用npm link


2.Git Submodule (子模块)



阿乐童鞋: 那如果我们没有搭建npm内网,又不想把模块开源出去,而且依赖npm,只要涉及变更需要重新发布,有没有其他方式可以解决以上问题呀?



image.png


2.1 对比 npm


你可以试试 Git Submodule ,它提供了一种类似于npm package的依赖管理机制,两者差别如下图所示👇


image.png


2.2 如何使用


通过在应用项目中,通过git submodule add <submodule_url>远程拉取子模块项目,这时会发现应用项目中多了两个文件.gitmodules子模块目录


image.png


这个子模块就是我们共享的模块,它是一个完整的Git仓库,换句话说:我们在应用项目目录中无论使用git add/commit都对其不影响,即子模块拥有自身独立的版本控制


总结: submodule本质上是通过git submodule add把项目依赖的模块加起来,最终构成一个完整的项目。而且add进来的模块,项目中并不实际包含,而只是一个包含索引信息,也就是上文提到的 .gitmodule来存储子模块的联系方式, 以此实现同步关联子模块。当下载到本地运行的时候才会再拉取文件


部分命令行:




  • git submodule add <子模块repository> <path> : 添加子模块




  • git submodule update --recursive --remote : 拉取所有子模块的更新




2.3 Monorepo



阿乐童鞋: 🌲 树酱,我记得有个叫Monorepo又是什么玩意,跟 Git Submodule 有啥区别?



image.png


Monorepo 全称叫monolithic respoitory,即单体式仓库,核心是允许我们将多个项目放到同一个仓库里面进行管理。主张不拆分repo,而是在单仓库里统一管理各个模块的构建流程、版本号等等


这样可以避免大量的冗余node_module冗余,因为每个项目都会安装vue、vue-router等包,再或者本地开发需要的webpack、babel、mock等都会造成储存空间的浪费


那么Monorepo是怎么管理的呢? 开源社区中诸如babel、vue的项目都是基于Monorepo去维护的(Lerna工具)


我们以Babel为例,在github中可以看到其每个模块都在指定的packages目录下, 也就意味着将所有的相关package都放入一个repository来管理,这不是显得项目很臃肿?


image.png
也就这个问题,啊乐同学和啊康同学展开了辩论~


image.png



最终是选用Monorepo单体式仓库还是Multirepo多仓库管理, 具体还是要看你业务场景来定,Monorepo集中管理带来的便利性,比如方便版本、依赖等管理、方便调试,但也带来了不少不便之处 👇




  • 统一构建工具所带来更高的要求

  • 仓库体积过大,维护成本也高


🌲 酱 不小心扯多了,还有就是Monorepo 跟 Git Submodule 的区别




  • 前者:monorepo在单repo里存放所有子模块源码




  • 后者:submodules只在主repo里存放所有子模块“索引”




目前内部还未使用Monorepo进行落地实际,目前基于微前端架构中后台应用存在依赖重叠过多的情况,后期会通过实践来深入分享


3. Webpack external



我们知道webpack中有externals的配置,主要是用来配置:webpack输出的bundle中排除依赖,换句话说通过在external定义的依赖,最终输出的bundle不存在该依赖,主要适用于不需要经常打包更新的第三方依赖,以此来实现模块共享。



下面是一个vue.config.js 的配置文件,通过配置exteral移除不经常更新打包的第三方依赖👇
carbon (26).png


你可以通过在packjson中script定义的命令后添加--report查看打包📦后的分析图,如果是webpack就是用使用插件webpack-bundle-analyzer



阿乐童鞋: 🌲 树酱,那移除了这些依赖之后,如何保证应用正常使用?



浏览器环境:我们使用cdn的方式在入口文件引入,当然你也可以预先打包好,比如把vue全家桶打包成vue-family.min.js文件,最终达成多应用共享模块的效果


<script src="<%= VUE_APP_UTILS_URL %>static/js/vue-family.min.js"></script>


总结:避免公共模块包(package) 一起打到bundle 中,而是在运行时再去从外部获取这些扩展依赖


通过这种形式在微前端基座应用加载公共模块,并将微应用引用同样模块的external移除掉,就可以实现模块共享了
但是存在微应用技术栈多样化不统一的情况,可能有的使用vue3,有的使用react开发,但externals 并无法支持多版本共存的情况,针对这种情况该方式就不太适用


4. Webpack DLL


官方介绍:"DLL" 一词代表微软最初引入的动态链接库, 换句话说我的理解,可以把它当做缓存,通过预先编译好的第三方外部依赖bundle,来节省应用在打包时混入的时间



Webpack DLL 跟 上一节提到的external本质是解决同样的问题:就是避免将第三方外部依赖打入到应用的bundle中(业务代码),然后在运行时再去加载这部分依赖,以此来实现模块复用,也提升了编译构建速度



webpack dll模式下需要配置两份webpack配置,下面是主要两个核心插件


image.png


4.1 DllPlugin


DllPlugin:在一个独立的webpack进行配置webpack.dll.config.js,目的是为了创建一个把所有的第三方库依赖打包到一起的bundle的dll文件里面,同时还会生成一个manifest.json的文件,用于:让使用该第三方依赖集合的应用配置的DllReferencePlugin能映射到相关的依赖上去 具体配置看下图👇


carbon.png


image.png


4.2 DllReferencePlugin


DllReferencePlugin:插件核心是把上一节提到的通过webpack.dll.config.js中打包生成的dll文件,引用到需要实际项目中使用,引用机制就是通过DllReferencePlugin插件来读取vendor-manifest.json文件,看看是否有该第三方库,最后通过add-asset-html-webpack-plugin插件在入口html自动插入上一节生成的vendor.dll.js 文件, 具体配置看下图👇
carbon (1).png


5. 联邦模块 Module Federation


模块联邦是 Webpack5 推出的一个新的重要功能,可以真正意义上实现让跨应用间做到模块共享,解决了从前用 NPM 公共包方式共享的不便利,同时也可以作为微前端的落地方案,完美秒杀了上两节介绍webpack特征


用过qiankun的小伙伴应该知道,qiankun微前端架构控制的粒度是在应用层面,而Module Federation控制的粒度是在模块层面。相比之下,后者粒度更小,可以有更多的选择


与qiankun等微前端架构不同的另一点是,我们一般都是需要一个中心基座去控制微应用的生命周期,而Module Federation则是去中心化的,没有中心基座的概念,每一个模块或者应用都是可以导入或导出,我们可以称为:host和remote,应用或模块即可以是host也可以是remote,亦或者两者共同体


image.png


看看下面这个例子👇


carbon (3).png


核心在于 ModuleFederationPlugin中的几个属性



  • remote : 示作为 Host 时,去消费哪些 Remote;

  • exposes :表示作为 Remote 时,export 哪些属性提供给 Host 消费

  • shared: 可以让远程加载的模块对应依赖改为使用本地项目的 vue,换句话说优先用 Host 的依赖,如果 Host 没有,最后再使用自己的


后期也会围绕 Module Federation 去做落地分享


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

收起阅读 »

全自动jQuery与渣男的故事

我是个恋旧的人,Github头像还是上古时期端游仙剑奇侠传的截图。 对于前端,如果能jQuery一把梭,我是很开心的。 React、Vue的普及让大家习惯了虚拟DOM的存在。但是虚拟DOM一定是最优解么? 举个例子,要进行如下DOM移动操作: // 变化前 ...
继续阅读 »

我是个恋旧的人,Github头像还是上古时期端游仙剑奇侠传的截图。



对于前端,如果能jQuery一把梭,我是很开心的。


ReactVue的普及让大家习惯了虚拟DOM的存在。但是虚拟DOM一定是最优解么?


举个例子,要进行如下DOM移动操作:


// 变化前
abcd
// 变化后
dabc

jQuery时调用insertBefored挪到a前面就行。而React基于虚拟DOMDiff会依次对abc执行appendChild,将他们依次挪到最后。


1次DOM操作 vs 3次DOM操作,显然前者更高效。


那么有没有框架能砍掉虚拟DOM,直接对DOM节点执行操作,实现全自动jQuery


有的,这就是最近出的petite-vue


阅读完本文,你会从原理层面了解该框架,如果你还有精力,可以在此基础上深入框架源码。


全自动jQuery的实现


可以将原理概括为一句话:



建立状态更新DOM的方法之间的联系



比如,对于如下DOM


<p v-show="showName">我是卡颂</p>

期望showName状态的变化能影响p的显隐(通过改变diaplay)。


实际是建立showName的变化调用如下方法的联系:


() => {
el.style.display = get() ? initialDisplay : 'none'
}

其中el代表pget()获取showName当前值。


再比如,对于如下DOM


<p v-text="name"></p>

name改变后ptextContent会变为对应值。


实际是建立name的变化调用如下方法的联系:


() => {
el.textContent = toDisplayString(get())
}

所以,整个框架的工作原理呼之欲出:初始化时遍历所有DOM,根据各种v-xx属性建立DOM操作DOM的方法之间的联系。


当改变状态后,会自动调用与其有关的操作DOM的方法,简直就是全自动jQuery



所以,框架的核心在于:如何建立联系?


一个渣男的故事


这部分源码都收敛在@vue/reactivity库中。我并不想带你精读源码,因为这样很没意思,看了还容易忘。


接下来我会通过一个故事为你展示其工作原理,当你了解原理后如果感兴趣可以自己去看源码。



我们的目标是描述:状态变化更新DOM的方法之间的联系。说得再宽泛点,是建立状态副作用之间的联系。


即:状态变化 -> 执行副作用


对于一段关系,可以从当事双方的角度描述,比如:


男生指着女生说:这是我女朋友。


接着女生指着男生说:这是我男朋友。


你作为旁观者,通过双方的描述就知道他们处于一段恋爱关系。


推广到状态副作用,则是:


副作用指着状态说:我依赖这个状态,他变了我就会执行。


状态指着副作用说:我订阅了这个副作用,当我变了后我会通知他。



可以看到,发布订阅其实是对一段关系站在双方视角的阐述



举个例子,如下DOM结构:


<div v-scope="{num: 0}">
<button @click="num++">add 1</button>
<p v-show="num%2">
<span v-text="num"></span>
</p>
</div>

经过petite-vue遍历后的关系图:



框架的交互流程为:




  1. 触发点击事件,状态num变化




  2. 通知其订阅的副作用effect1effect2),执行对应DOM操作




如果从情侣关系角度解读,就是:


num指着effect1说:这是我女朋友。


effect1指着num说:这是我男朋友。


num指着effect2说:这是我女朋友。


effect2指着num说:这是我男朋友。



总结


今天我们学习了一个框架petite-vue,他的底层实现由多段混乱的男女关系组成,上层是一个个直接操作DOM的方法。


不知道看完后你有没有兴趣深入了解下这种关系呢?


感兴趣的话可以看看Vue MasteryVue 3 Reactivity课程。



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

收起阅读 »

拖拽竟然还能这样玩!

在大多数低代码平台中的设计器都支持组件拖拽的功能,这样大大地提高了用户的设计体验。而拖拽另一个比较常见的场景就是文件上传,通过拖拽的方式,可以让用户方便地上传文件。其实利用拖拽功能,我们还可以 跨越浏览器的边界,实现数据共享。 那么如何 跨越浏览器的边界,实现...
继续阅读 »

在大多数低代码平台中的设计器都支持组件拖拽的功能,这样大大地提高了用户的设计体验。而拖拽另一个比较常见的场景就是文件上传,通过拖拽的方式,可以让用户方便地上传文件。其实利用拖拽功能,我们还可以 跨越浏览器的边界,实现数据共享


那么如何 跨越浏览器的边界,实现数据共享 呢?本文阿宝哥将介绍谷歌的一个开源项目 —— transmat,利用该项目可以实现上述功能。不仅如此,该项目还可以帮助我们实现一些比较好玩的功能,比如针对不同的可释放目标,做出不同的响应。


下面我们先通过 4 张 Gif 动图来感受一下,使用 transmat 开发的 神奇、好玩 的拖拽功能。


图 1(把可拖拽的元素,拖拽至富文本编辑器)



图 2(把可拖拽的元素,拖拽至 Chrome 浏览器,也支持其他浏览器)



图 3(把可拖拽的元素,拖拽至自定义的释放目标)



图 4(把可拖拽的元素,拖拽至 Chrome 开发者工具)




以上示例使用的浏览器版本:Chrome 91.0.4472.114(正式版本) (x86_64)



以上 4 张图中的 可拖拽元素都是同一个元素,当它被放置到不同的可释放目标时,产生了不同的效果。同时,我们也跨越了浏览器的边界,实现了数据的共享。看完以上 4 张动图,你是不是觉得挺神奇的。其实除了拖拽之外,该示例也支持复制、粘贴操作。不过,在详细介绍如何使用 transmat 实现上述功能之前,我们先来简单介绍一下 transmat 这个库。


一、Transmat 简介


Transmat 是一个围绕 DataTransfer API 的小型库 ,它使用 drag-dropcopy-paste 交互简化了在 Web 应用程序中传输和接收数据的过程。 DataTransfer API 能够将多种不同类型的数据传输到用户设备上的其他应用程序,该 API 所支持的数据类型,常见的有这几种:text/plaintext/htmlapplication/json 等。



(图片来源:google.github.io/transmat/)


了解完 transmat 是什么之后,我们来看一下它的应用场景:



  • 想以便捷的方式与外部应用程序集成。

  • 希望为用户提供与其他应用程序共享数据的能力,即使是那些你不知道的应用程序。

  • 希望外部应用程序能够与你的 Web 应用程序深度集成。

  • 想让你的应用程序更好地适应用户现有的工作流程。


现在你已经对 transmat 有了一定的了解,下面我们来分析如何使用 transmat 实现以上 4 张 Gif 动图对应的功能。


二、Transmat 实战


2.1 transmat-source


html


在以下代码中,我们为 div#source 元素添加了 draggable 属性,该属性用于标识元素是否允许被拖动,它的取值为 truefalse


<script src="https://unpkg.com/transmat/lib/index.umd.js"></script>
<div id="source" draggable="true" tabindex="0">大家好,我是阿宝哥</div>

css


#source {
background: #eef;
border: solid 1px rgba(0, 0, 255, 0.2);
border-radius: 8px;
cursor: move;
display: inline-block;
margin: 1em;
padding: 4em 5em;
}

js


const { Transmat, addListeners, TransmatObserver } = transmat;

const source = document.getElementById("source");

addListeners(source, "transmit", (event) => {
const transmat = new Transmat(event);
transmat.setData({
"text/plain": "大家好,我是阿宝哥!",
"text/html": `
<h1>大家好,我是阿宝哥</h1>
<p>聚焦全栈,专注分享 TS、Vue 3、前端架构等技术干货。
<a href="https://juejin.cn/user/764915822103079">访问我的主页</a>!
</p>
<img src="https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/
075d8e781ba84bf64035ac251988fb93~300x300.image" border="1" />
`,
"text/uri-list": "https://juejin.cn/user/764915822103079",
"application/json": {
name: "阿宝哥",
wechat: "semlinker",
},
});
});

在以上代码中,我们利用 transmat 这个库提供的 addListeners 函数为 div#source 元素,添加了 transmit 的事件监听。在对应的事件处理器中,我们先创建了 Transmat 对象,然后调用该对象上的 setData 方法设置不同 MIME 类型的数据。


下面我们来简单回顾一下,示例中所使用的 MIME 类型:



  • text/plain:表示文本文件的默认值,一个文本文件应当是人类可读的,并且不包含二进制数据。

  • text/html:表示 HTML 文件类型,一些富文本编辑器会优先从 dataTransfer 对象上获取 text/html 类型的数据,如果不存在的话,再获取 text/plain 类型的数据。

  • text/uri-list:表示 URI 链接类型,大多数浏览器都会优先读取该类型的数据,如果发现是合法的 URI 链接,则会直接打开该链接。如果不是的合法 URI 链接,对于 Chrome 浏览器来说,它会读取 text/plain 类型的数据并以该数据作为关键词进行内容检索。

  • application/json:表示 JSON 类型,该类型对前端开发者来说,应该都比较熟悉了。


介绍完 transmat-source 之后,我们来看一下图 3 自定义目标(transmat-target)的实现代码。


2.2 transmat-target


html


<script src="https://unpkg.com/transmat/lib/index.umd.js"></script>
<div id="target" tabindex="0">放这里哟!</div>

css


body {
text-align: center;
font: 1.2em Helvetia, Arial, sans-serif;
}
#target {
border: dashed 1px rgba(0, 0, 0, 0.5);
border-radius: 8px;
margin: 1em;
padding: 4em;
}
.drag-active {
background: rgba(255, 255, 0, 0.1);
}
.drag-over {
background: rgba(255, 255, 0, 0.5);
}

js


const { Transmat, addListeners, TransmatObserver } = transmat;

const target = document.getElementById("target");

addListeners(target, "receive", (event) => {
const transmat = new Transmat(event);
// 判断是否含有"application/json"类型的数据
// 及事件类型是否为drop或paste事件
if (transmat.hasType("application/json")
&& transmat.accept()
) {
const jsonString = transmat.getData("application/json");
const data = JSON.parse(jsonString);
target.textContent = jsonString;
}
});

在以上代码中,我们利用 transmat 这个库提供的 addListeners 函数为 div#target 元素,添加了 receive 的事件监听。顾名思义,该 receive 事件表示接收消息。在对应的事件处理器中,我们通过 transmat 对象的 hasType 方法过滤了 application/json 的消息,然后通过 JSON.parse 方法进行反序列化获得对应的数据,同时把对应 jsonString 的内容显示在 div#target 元素内。


在图 3 中,当我们把可拖拽的元素,拖拽至自定义的释放目标时,会产生高亮效果,具体如下图所示:



这个效果是利用 transmat 这个库提供的 TransmatObserver 类来实现,该类可以帮助我们响应用户的拖拽行为,具体的使用方式如下所示:


const obs = new TransmatObserver((entries) => {
for (const entry of entries) {
const transmat = new Transmat(entry.event);
if (transmat.hasType("application/json")) {
entry.target.classList.toggle("drag-active", entry.isActive);
entry.target.classList.toggle("drag-over", entry.isTarget);
}
}
});
obs.observe(target);

第一次看到 TransmatObserver 之后,阿宝哥立马想到了 MutationObserver API,因为它们都是观察者且拥有类似的 API。利用 MutationObserver API 我们可以监视 DOM 的变化。DOM 的任何变化,比如节点的增加、减少、属性的变动、文本内容的变动,通过这个 API 我们都可以得到通知。如果你对该 API 感兴趣的话,可以阅读 是谁动了我的 DOM? 这篇文章。


现在我们已经知道 transmat 这个库如何使用,接下来阿宝哥将带大家一起来分析这个库背后的工作原理。



Transmat 使用示例:Transmat Demo


gist.github.com/semlinker/c…



三、Transmat 源码分析


transmat 源码分析环节,因为在前面实战部分,我们使用到了 addListenersTransmatTransmatObserver 这三个 “函数” 来实现核心的功能,所以接下来的源码分析,我们将围绕它们展开。这里我们先来分析 addListeners 函数。


3.1 addListeners 函数


addListeners 函数用于设置监听器,调用该函数后会返回一个用于移除事件监听的函数。在分析函数时,阿宝哥习惯先分析函数的签名:


// src/transmat.ts
function addListeners<T extends Node>(
target: T,
type: TransferEventType,
listener: (event: DataTransferEvent, target: T) => void,
options = {dragDrop: true, copyPaste: true}
): () => void

通过观察以上的函数签名,我们可以很直观的了解该函数的输入和输出。该函数支持以下 4 个参数:



  • target:表示监听的目标,它的类型是 Node 类型。

  • type:表示监听的类型,该参数的类型 TransferEventType 是一个联合类型 —— 'transmit' | 'receive'

  • listener:表示事件监听器,它支持的事件类型为 DataTransferEvent,该类型也是一个联合类型 —— DragEvent | ClipboardEvent,即支持拖拽事件和剪贴板事件。

  • options:表示配置对象,用于设置是否允许拖拽和复制、粘贴操作。


addListeners 函数体中,主要包含以下 3 个步骤:



  • 步骤 ①:根据 isTransmitEventoptions.copyPaste 的值,注册剪贴板相关的事件。

  • 步骤 ②:根据 isTransmitEventoptions.dragDrop 的值,注册拖拽相关的事件。

  • 步骤 ③:返回函数对象,用于移除已注册的事件监听。


// src/transmat.ts
export function addListeners<T extends Node>(
target: T,
type: TransferEventType, // 'transmit' | 'receive'
listener: (event: DataTransferEvent, target: T) => void,
options = {dragDrop: true, copyPaste: true}
): () => void {
const isTransmitEvent = type === 'transmit';
let unlistenCopyPaste: undefined | (() => void);
let unlistenDragDrop: undefined | (() => void);

if (options.copyPaste) {
// ① 可拖拽源监听cut和copy事件,可释放目标监听paste事件
const events = isTransmitEvent ? ['cut', 'copy'] : ['paste'];
const parentElement = target.parentElement!;
unlistenCopyPaste = addEventListeners(parentElement, events, event => {
if (!target.contains(document.activeElement)) {
return;
}
listener(event as DataTransferEvent, target);

if (event.type === 'copy' || event.type === 'cut') {
event.preventDefault();
}
});
}

if (options.dragDrop) {
// ② 可拖拽源监听dragstart事件,可释放目标监听dragover和drop事件
const events = isTransmitEvent ? ['dragstart'] : ['dragover', 'drop'];
unlistenDragDrop = addEventListeners(target, events, event => {
listener(event as DataTransferEvent, target);
});
}

// ③ 返回函数对象,用于移除已注册的事件监听
return () => {
unlistenCopyPaste && unlistenCopyPaste();
unlistenDragDrop && unlistenDragDrop();
};
}

以上代码的事件监听最终是通过调用 addEventListeners 函数来实现,在该函数内部会循环调用 addEventListener 方法来添加事件监听。以前面 Transmat 的使用示例为例,在对应的事件处理回调函数内部,我们会以 event 事件对象为参数,调用 Transmat 构造函数创建 Transmat 实例。那么该实例有什么作用呢?要搞清楚它的作用,我们就需要来了解 Transmat 类。


3.2 Transmat 类


Transmat 类被定义在 src/transmat.ts 文件中,该类的构造函数含有一个类型为 DataTransferEvent 的参数 event


// src/transmat.ts
export class Transmat {
public readonly event: DataTransferEvent;
public readonly dataTransfer: DataTransfer;

// type DataTransferEvent = DragEvent | ClipboardEvent;
constructor(event: DataTransferEvent) {
this.event = event;
this.dataTransfer = getDataTransfer(event);
}
}

Transmat 构造函数内部还会通过 getDataTransfer 函数来获取 DataTransfer 对象并赋值给内部的 dataTransfer 属性。DataTransfer 对象用于保存拖动并放下(drag and drop)过程中的数据。它可以保存一项或多项数据,这些数据项可以是一种或者多种数据类型。


下面我们来看一下 getDataTransfer 函数的具体实现:


// src/data_transfer.ts
export function getDataTransfer(event: DataTransferEvent): DataTransfer {
const dataTransfer =
(event as ClipboardEvent).clipboardData ??
(event as DragEvent).dataTransfer;
if (!dataTransfer) {
throw new Error('No DataTransfer available at this event.');
}
return dataTransfer;
}

在以上代码中,使用了空值合并运算符 ??。该运算符的特点是:当左侧操作数为 null 或 undefined 时,其返回右侧的操作数,否则返回左侧的操作数。即先判断是否为剪贴板事件,如果是的话就会从 clipboardData 属性获取 DataTransfer 对象。否则,就会从 dataTransfer 属性获取。


对于可拖拽源,在创建完 Transmat 对象之后,我们就可以调用该对象上的 setData 方法保存一项或多项数据。比如,在以下代码中,我们设置了不同类型的多项数据:


transmat.setData({
"text/plain": "大家好,我是阿宝哥!",
"text/html": `
<h1>大家好,我是阿宝哥</h1>
...
`,
"text/uri-list": "https://juejin.cn/user/764915822103079",
"application/json": {
name: "阿宝哥",
wechat: "semlinker",
},
});

了解完 setData 方法的用法之后,我们来看一下它的具体实现:


// src/transmat.ts
setData(
typeOrEntries: string | {[type: string]: unknown},
data?: unknown
): void {
if (typeof typeOrEntries === 'string') {
this.setData({[typeOrEntries]: data});
} else {
// 处理多种类型的数据
for (const [type, data] of Object.entries(typeOrEntries)) {
const stringData =
typeof data === 'object' ? JSON.stringify(data) : `${data}`;
this.dataTransfer.setData(normalizeType(type), stringData);
}
}
}

由以上代码可知,在 setData 方法内部最终会调用 dataTransfer.setData 方法来保存数据。dataTransfer 对象的 setData 方法支持两个字符串类型的参数:formatdata。它们分别表示要保存的数据格式和实际的数据。如果给定数据格式不存在,则将对应的数据保存到末尾。如果给定数据格式已存在,则将使用新的数据替换旧的数据


下图是 dataTransfer.setData 方法的兼容性说明,由图可知主流的现代浏览器都支持该方法。



(图片来源:caniuse.com/mdn-api_dat…


Transmat 类除了拥有 setData 方法之外,它也含有一个 getData 方法,用于获取已保存的数据。getData 方法支持一个字符串类型的参数 type,用于表示数据的类型。在获取数据前,会调用 hasType 方法判断是否含有该类型的数据。如果有包含的话,就会通过 dataTransfer 对象的 getData 方法来获取该类型对应的数据。


// src/transmat.ts
getData(type: string): string | undefined {
return this.hasType(type)
? this.dataTransfer.getData(normalizeType(type))
: undefined;
}

此外,在调用 getData 方法前,还会调用 normalizeType 函数,对传入的 type 类型参数进行标准化操作。具体的如下所示:


// src/data_transfer.ts
export function normalizeType(input: string) {
const result = input.toLowerCase();
switch (result) {
case 'text':
return 'text/plain';
case 'url':
return 'text/uri-list';
default:
return result;
}
}

同样,我们也来看一下 dataTransfer.getData 方法的兼容性:



(图片来源:caniuse.com/mdn-api_dat…


好的,Transmat 类中的 setDatagetData 这两个核心方法就先介绍到这里。接下来我们来介绍另一个类 —— TransmatObserver 。


3.3 TransmatObserver 类


TransmatObserver 类的作用是可以帮助我们响应用户的拖拽行为,可用于在拖拽过程中高亮放置区域。比如,在前面的示例中,我们通过以下方式来实现放置区域的高亮效果:


const obs = new TransmatObserver((entries) => {
for (const entry of entries) {
const transmat = new Transmat(entry.event);
if (transmat.hasType("application/json")) {
entry.target.classList.toggle("drag-active", entry.isActive);
entry.target.classList.toggle("drag-over", entry.isTarget);
}
}
});
obs.observe(target);

同样,我们先来分析一下 TransmatObserver 类的构造函数:


// src/transmat_observer.ts
export class TransmatObserver {
private readonly targets = new Set<Element>(); // 观察的目标集合
private prevRecords: ReadonlyArray<TransmatObserverEntry> = []; // 保存前一次的记录
private removeEventListeners = () => {};

constructor(private readonly callback: TransmatObserverCallback) {}
}

由以上代码可知,TransmatObserver 类的构造函数支持一个类型为 TransmatObserverCallback 的参数 callback,该参数对应的类型定义如下:


// src/transmat_observer.ts
export type TransmatObserverCallback = (
entries: ReadonlyArray<TransmatObserverEntry>,
observer: TransmatObserver
) => void;

TransmatObserverCallback 函数类型接收两个参数:entriesobserver。其中 entries 参数的类型是一个


只读数组(ReadonlyArray),数组中每一项的类型是 TransmatObserverEntry,对应的类型定义如下:


// src/transmat_observer.ts
export interface TransmatObserverEntry {
target: Element;
/** type DataTransferEvent = DragEvent | ClipboardEvent */
event: DataTransferEvent;
/** Whether a transfer operation is active in this window. */
isActive: boolean;
/** Whether the element is the active target (dragover). */
isTarget: boolean;
}

在前面 transmat-target 的示例中,当创建完 TransmatObserver 实例之后,就会调用该实例的 observe 方法并传入待观察的对象。observe 方法的实现并不复杂,具体如下所示:


// src/transmat_observer.ts
observe(target: Element) {
/** private readonly targets = new Set<Element>(); */
this.targets.add(target);
if (this.targets.size === 1) {
this.addEventListeners();
}
}

observe 方法内部,会把需观察的元素保存到 targets Set 集合中。当 targets 集合的大小等于 1 时,就会调用当前实例的 addEventListeners 方法来添加事件监听:


// src/transmat_observer.ts
private addEventListeners() {
const listener = this.onTransferEvent as EventListener;
this.removeEventListeners = addEventListeners(
document,
['dragover', 'dragend', 'dragleave', 'drop'],
listener,
true
);
}

在私有的 addEventListeners 方法内部,会利用我们前面介绍的 addEventListeners 函数来为 document 元素批量添加与拖拽相关的事件监听。而对应的事件说明如下所示:



  • dragover:当元素或选中的文本被拖到一个可释放目标上时触发;

  • dragend:当拖拽操作结束时触发(比如松开鼠标按键);

  • dragleave:当拖拽元素或选中的文本离开一个可释放目标时触发;

  • drop:当元素或选中的文本在可释放目标上被释放时触发。


其实与拖拽相关的事件并不仅仅只有以上四种,如果你对完整的事件感兴趣的话,可以阅读 MDN 上 HTML 拖放 API 这篇文章。下面我们来重点分析 onTransferEvent 事件监听器:


private onTransferEvent = (event: DataTransferEvent) => {
const records: TransmatObserverEntry[] = [];
for (const target of this.targets) {
// 当光标离开浏览器时,对应的事件将会被派发到body或html节点
const isLeavingDrag =
event.type === 'dragleave' &&
(event.target === document.body ||
event.target === document.body.parentElement);

// 页面上是否有拖拽行为发生
// 当拖拽操作结束时触发dragend事件
// 当元素或选中的文本在可释放目标上被释放时触发drop事件
const isActive = event.type !== 'drop'
&& event.type !== 'dragend' && !isLeavingDrag;

// 判断可拖拽的元素是否被拖到target元素上
const isTargetNode = target.contains(event.target as Node);
const isTarget = isActive && isTargetNode
&& event.type === 'dragover';

records.push({
target,
event,
isActive,
isTarget,
});
}

// 仅当记录发生变化的时候,才会调用回调函数
if (!entryStatesEqual(records, this.prevRecords)) {
this.prevRecords = records as ReadonlyArray<TransmatObserverEntry>;
this.callback(records, this);
}
}

在以上代码中,使用了 node.contains(otherNode) 方法来判断可拖拽的元素是否被拖到 target 元素上。当 otherNodenode 的后代节点或者 node 节点本身时,返回 true,否则返回 false。此外,为了避免频繁地触发回调函数,在调用回调函数前会先调用 entryStatesEqual 函数来检测记录是否发生变化。entryStatesEqual 函数的实现比较简单,具体如下所示:


// src/transmat_observer.ts
function entryStatesEqual(
a: ReadonlyArray<TransmatObserverEntry>,
b: ReadonlyArray<TransmatObserverEntry>
): boolean {
if (a.length !== b.length) {
return false;
}
// 如果有一项不匹配,则立即返回false。
return a.every((av, index) => {
const bv = b[index];
return av.isActive === bv.isActive && av.isTarget === bv.isTarget;
});
}

MutationObserver 一样,TransmatObserver 也提供了用于获取最近已触发记录的 takeRecords 方法和用于 “断开” 连接的 disconnect 方法:


// 返回最近已触发记录
takeRecords() {
return this.prevRecords;
}

// 移除所有目标及事件监听器
disconnect() {
this.targets.clear();
this.removeEventListeners();
}

到这里 Transmat 源码分析的相关内容已经介绍完了,如果你对该项目感兴趣的话,可以自行阅读该项目的完整源码。该项目是使用 TypeScript 开发,已入门 TypeScript 的小伙伴可以利用该项目巩固一下所学的 TS 知识及 OOP 面向对象的设计思想。



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

收起阅读 »

XVideo 一个能自动进行压缩的小视频录制库

XVideo一个能自动进行压缩的小视频录制库特征支持自定义小视频录制时的视频质量。支持自定义视频录制的界面。支持自定义最大录制时长和最小录制时长。支持自定义属性的视频压缩。演示(请star支持)Demo下载添加Gradle依赖1.在项目根目录的 build.g...
继续阅读 »

XVideo

一个能自动进行压缩的小视频录制库

特征

  • 支持自定义小视频录制时的视频质量。

  • 支持自定义视频录制的界面。

  • 支持自定义最大录制时长和最小录制时长。

  • 支持自定义属性的视频压缩。

演示(请star支持)

Demo下载

Github

添加Gradle依赖

1.在项目根目录的 build.gradle 的 repositories 添加:

allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}

2.在主项目的 build.gradle 中增加依赖。

dependencies {
···
implementation 'com.github.xuexiangjys:XVideo:1.0.2'
}

3.进行视频录制存储目录地址的设置。

/**
* 初始化xvideo的存放路径
*/
public static void initVideo() {
XVideo.setVideoCachePath(PathUtils.getExtDcimPath() + "/xvideo/");
// 初始化拍摄
XVideo.initialize(false, null);
}

视频录制

1.视频录制需要CAMERA权限和STORAGE权限。在Android6.0机器上需要动态获取权限,推荐使用XAOP进行权限申请。

2.调用MediaRecorderActivity.startVideoRecorder开始视频录制。

/**
* 开始录制视频
* @param requestCode 请求码
*/
@Permission({PermissionConsts.CAMERA, PermissionConsts.STORAGE})
public void startVideoRecorder(int requestCode) {
MediaRecorderConfig mediaRecorderConfig = MediaRecorderConfig.newInstance();
XVideo.startVideoRecorder(this, mediaRecorderConfig, requestCode);
}

3.MediaRecorderConfig是视频录制的配置对象,可自定义视频的宽、高、时长以及质量等。

MediaRecorderConfig config = new MediaRecorderConfig.Builder()
.fullScreen(needFull) //是否全屏
.videoWidth(needFull ? 0 : Integer.valueOf(width)) //视频的宽
.videoHeight(Integer.valueOf(height)) //视频的高
.recordTimeMax(Integer.valueOf(maxTime)) //最大录制时间
.recordTimeMin(Integer.valueOf(minTime)) //最小录制时间
.maxFrameRate(Integer.valueOf(maxFrameRate)) //最大帧率
.videoBitrate(Integer.valueOf(bitrate)) //视频码率
.captureThumbnailsTime(1)
.build();

视频压缩

使用libx264进行视频压缩。由于手机本身CPU处理能力有限的问题,在手机上进行视频压缩的效率并不是很高,大约压缩的时间需要比视频拍摄本身的时长还要长一些。

LocalMediaConfig.Builder builder = new LocalMediaConfig.Builder();
final LocalMediaConfig config = builder
.setVideoPath(path) //设置需要进行视频压缩的视频路径
.captureThumbnailsTime(1)
.doH264Compress(compressMode) //设置视频压缩的模式
.setFramerate(iRate) //帧率
.setScale(fScale) //压缩比例
.build();
CompressResult compressResult = XVideo.startCompressVideo(config);

混淆配置

-keep class com.xuexiang.xvideo.jniinterface.** { *; }

代码下载:XVideo.zip

收起阅读 »

模版空壳Android工程,快速搭建(集成了XUI、XUtil、XAOP、XPage、XUpdate和XHttp2)

TemplateAppProjectAndroid空壳模板工程,快速搭建(集成了XUI、XUtil、XAOP、XPage、XUpdate、XHttp2、友盟统计和walle多渠道打包)效果使用方式视频教程-如何使用模板工程1.克隆项目git clone htt...
继续阅读 »

TemplateAppProject

Android空壳模板工程,快速搭建(集成了XUI、XUtil、XAOP、XPage、XUpdate、XHttp2、友盟统计和walle多渠道打包)

效果

templateproject_demo.gif


使用方式

视频教程-如何使用模板工程

1.克隆项目

2.修改项目名(文件夹名),并删除目录下的.git文件夹(隐藏文件)

3.使用AS打开项目,然后修改包名applicationIdapp_name

  • 修改包名

templateproject_1.png

templateproject_2.png

  • 修改applicationId

templateproject_3.png

  • 修改app_name

templateproject_5.png

项目打包

1.修改工程根目录的gradle.properties中的isNeedPackage=true

2.添加并配置keystore,在versions.gradle中修改app_release相关参数。

3.如果考虑使用友盟统计的话,在local.properties中设置应用的友盟ID:APP_ID_UMENG

4.使用./gradlew clean assembleReleaseChannels进行多渠道打包。

代码下载:TemplateAppProject-master.zip

收起阅读 »

Android直播间的送礼物动画-GiftSurfaceView

GiftSurfaceViewGiftSurfaceView 最初出自于2014年开发HalloStar项目时所写,主要用于HalloStar项目直播间的送礼物动画。现在想来,那夕阳下的奔跑,是我逝去的青春。因高仿全民TV项目时想起,所以抽空整理了下,以此记录...
继续阅读 »


GiftSurfaceView

GiftSurfaceView 最初出自于2014年开发HalloStar项目时所写,主要用于HalloStar项目直播间的送礼物动画。现在想来,那夕阳下的奔跑,是我逝去的青春。因高仿全民TV项目时想起,所以抽空整理了下,以此记录。

Gif展示


引入

Maven:

<dependency>
<groupId>com.king.view</groupId>
<artifactId>giftsurfaceview</artifactId>
<version>1.1.0</version>
<type>pom</type>
</dependency>

Gradle:

compile 'com.king.view:giftsurfaceview:1.1.0'

Lvy:

<dependency org='com.king.view' name='giftsurfaceview' rev='1.1.0'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

    public void updateGiftSurfaceView(int type){

frame.removeAllViews();

GiftSurfaceView giftSurfaceView = new GiftSurfaceView(context);
if(type == RANDOM){
giftSurfaceView.setImageResource(R.drawable.rose);
}else{
giftSurfaceView.setImageBitmap(bitmap,.5f);
}

giftSurfaceView.setPointScale(1,width/10,(int)(height/3.8f));
giftSurfaceView.setRunTime(10000);

try {

switch (type){
case RANDOM:
giftSurfaceView.setRandomPoint(9);
break;
case V:
giftSurfaceView.setListPoint(PointUtils.getListPointByResourceJson(context,ASSET_V),true);
break;
case HEART:
giftSurfaceView.setListPoint(PointUtils.getListPointByResourceJson(context,ASSET_HEART),true);
break;
case LOVE:
giftSurfaceView.setListPoint(PointUtils.getListPointByResourceJson(context,ASSET_LOVE));
break;
case SMILE:
giftSurfaceView.setListPoint(PointUtils.getListPointByResourceJson(context,ASSET_SMILE));
break;
case X:
giftSurfaceView.setListPoint(PointUtils.getListPointByResourceJson(context,ASSET_X));
break;
case V520:
giftSurfaceView.setListPoint(PointUtils.getListPointByResourceJson(context,ASSET_V520));
break;
case V1314:
giftSurfaceView.setRunTime(GiftSurfaceView.LONG_TIME);
giftSurfaceView.setListPoint(PointUtils.getListPointByResourceJson(context,ASSET_V1314));
break;

}
frame.addView(giftSurfaceView);
} catch (IOException e) {
e.printStackTrace();
}


}

以上为部分代码使用示例,更多详情请下载查看。

代码下载:GiftSurfaceView.zip

收起阅读 »

HarmonyOS开发者创新大赛作品《智能农场》相关开发技术分享

HarmonyOS开发者创新大赛已于2021年5月24日落幕,在本次赛事中,来自古都西安的开拓者战队凭借《智能农场》这款作品最终获得大赛三等奖,该作品通过HarmonyOS的分布式软总线、分布式数据库技术、分布式任务调度、分布式跨设备数据流转等能力实现了多设备...
继续阅读 »

HarmonyOS开发者创新大赛已于2021年5月24日落幕,在本次赛事中,来自古都西安的开拓者战队凭借《智能农场》这款作品最终获得大赛三等奖,该作品通过HarmonyOS的分布式软总线、分布式数据库技术、分布式任务调度、分布式跨设备数据流转等能力实现了多设备(传感器、智慧屏等)的互联互通、自动控制,实现了农场场景下多设备协同智能养殖体验,令人印象深刻。

以下是“开拓者战队”基于HarmonyOS打造《智能农场》作品的相关思考以及关键技术的简单分享:

1.背景介绍

目前,市面上智慧农业相关的厂商设备(传感器等)相对独立,没有统一的操作系统平台,互联互通困难,且大多数设备部署需要连线,部署成本时间长,成本高,维护复杂度高。随着5G网络的覆盖,下一代全场景操作系统(HarmonyOS)的出现,让万物互联变得更加方便,可以实现一部手机操作所有IoT设备,实现各个IoT设备的互联互通。智能农场系统是基于HarmonyOS实现了多个IoT设备(传感器,电机,大屏等设备)的互联互通、自动控制,并实现全场景化的智慧养殖。智能农场通过各项传感器设备对农场的各项环境指标进行实时检测,并且可以进行自适应调节,让动物一直处于一个良好的生长环境。通过本系统可以实现指标超过阈值预警,智能提醒,智能求助等功能,让农场养殖门槛变低,让农场主轻松成为养殖专家。

2.需求分析

智能农场系统通过对农场的空气温,湿度、光照度等各项环境参数进行实时采集,确保农场主可随时通过智能手机APP了解农场状况。同时,系统可以根据农场内外环境因子的变化进行自适应调解,不仅能保证农场中的动物长期处于良好的生长环境中,还能提升动物的产量和质量。本系统的特色业务功能包括:精细化智能提醒,专家视频求助等。

智能提醒功能体现在多个场景中,如:农场温度过高,降温设备有损坏或者指定时间内温度没有降下来等异常情况出现时,系统会直接给管理者进行电话提醒或者消息推送;不仅如此,系统还会根据动物的年龄,对不同动物的生长周期进行预测,提醒管理者为动物打疫苗。智能求助功能则体现为,当管理者遇到一些养殖常识问题,可以通过智能求助查到相关帮助信息;同时,也提供了养殖专家视频求助功能,帮助管理者及时的解决养殖方面遇到的疑难杂症。

3.解决方案

本解决方案涉及角色:农场主,养殖专家;涉及硬件设备:手机、智慧屏、开发板,各类型传感器(比如:温湿度传感器、可燃气体传感器、光敏传感器、人体红外传感器等)以及各项外设(比如:风扇,加水设备,取暖设备等)。手机、智慧屏、开发板基于HarmonyOS,通过WIFI组网,实现各项设备之间的互联互通。农场主可以通过手机APP对养殖场景中的各项环境指标(温湿度阈值范围、可燃气体浓度范围、光照强度等)进行设置,也可设置定时任务(比如:定时加水、加料,定时播放音乐等),实现智能化提醒和自动化控制。解决方案中养殖技术和案例等信息的查询和分析等服务,由云端的数据服务提供,专家视频求助功能的视频通话服务由云端提供。


4.主要模块介绍

数据采集模块 (以采集湿度为例)

系统启动成功后,数据采集模块会启动定时任务采集温度数据,定时从温度传感器采集一次当前温度数据。如果采集成功,存入分布式数据库(KV方式存储),采集到的温度数据会实时刷新到温度显示界面。温度数据范围:-40~80℃。采集到的数据,可以流转到大屏方便用户查看。(采集流程见下图1)

业务流程


图1采集温度数据流程

自动控制模块(以温度控制为例)

定时获取当前温度数据与用户设置的正常阈值范围或者最大阈值范围(来自:Preferences)进行比较。如果当前温度在正常阈值范围内,不做处理;如果超过正常阈值范围,未超过最大阈值范围(比如:正常范围:5~30℃),包括两种情况:

1.低于5℃,打开加热设备,并调用智能提醒模块通知用户;温度恢复正常范围,关闭设备。

2.高于30℃,打开风扇降温,并调用智能提醒模块通知用户;温度恢复正常范围,关闭设备。

如果超过最大阈值范围(比如:最大阈值范围:<-20℃或>60℃),包括两种情况:

1.低于-20℃,打开多个加热设备,并调用智能提醒模块,发送通知,并拨打电话通知用户;温度恢复正常范围,关闭设备。

2.高于60℃,打开喷水降温,并调用智能提醒模块,发送通知,并拨打电话通知用户;温度恢复正常范围,关闭设备。(控制流程见下图2)


图2 温度控制流程

5.关键技术细节实现

1)分布式多设备发现,实现多设备协同、调度

分布式设备发现关键代码:

List<DeviceInfo> onlineDevices = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);

分布式设备任务调度关键代码:

Intent intent = new Intent();

Operation operation =

new Intent.OperationBuilder()

.withDeviceId(devicesId)

.withBundleName(getBundleName())

.withAbilityName(Ability.class.getName())

.withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)

.build();

intent.setOperation(operation);

2)分布式数据流转

调用continueAbility进行实现数据流转(关键代码)

continueAbility(chooseDevice.getDeviceInfo().getDeviceId());

3)socket通信实现设备间互联互通(如下关键代码)

//调用NetManager.getInstance(Context)获取网络管理的实例对象。

NetManager netManager = NetManager.getInstance(context);

//调用NetManager.getDefaultNet()获取默认的数据网络。

NetHandle netHandle = netManager.getDefaultNet();

//调用NetHandle.bindSocket()绑定网络。

DatagramSocket socket = new DatagramSocket();

netHandle.bindSocket(socket);

//使用socket发送数据

socket.send(request);

4)踩坑(分布式任务调度和分布式数据库技术配合使用)(功能:智能农场手机端采集的数据实时同步到TV端)

当手机端收到采集到的环境数据(如温度、湿度及可燃气体浓度),需要流转到智慧屏上进行显示,团队一开始使用的分布式任务调度,流转到TV端后,发现TV端显示的数据并没有实时刷新,显然不符合现实需求。

为了实现数据的实时刷新,团队发现HarmonyOS有分布式数据服务的能力,可以实现同应用,同网络,同账号在不同设备之间实现数据实时共享,因此最终采用了HarmonyOS的分布式数据库技术,确保了手机端和TV端数据同步刷新的功能。在不依赖云端服务的情况下,实现此功能。

下面是实现的关键代码:

手机端数据存储:

//初始化获取SingleKvStore对象

KvManagerConfig kvManagerConfig = new KvManagerConfig(context);

kvManager = KvManagerFactory.getInstance().createKvManager(kvManagerConfig);

Options options = new Options();

options.setCreateIfMissing(true)

.setEncrypt(false)

.setKvStoreType(KvStoreType.SINGLE_VERSION)

.setAutoSync(true);

SingleKvStore singleKvStore = kvManager.getKvStore(options, storeId);

将采集到的传感器数据,存储在分布式数据库:

singleKvStore.putString("key",

" +…此处省略

"}");

TV端进行数据获取:

//初始化singleKvStore,并为其注册监听器kvStoreObserverClient,观察数据变化:

KvManagerConfig config = new KvManagerConfig(getContext());

KvManager kvManager = KvManagerFactory.getInstance().createKvManager(config);

Options CREATE = new Options();

CREATE.setCreateIfMissing(true).setEncrypt(false).setKvStoreType(KvStoreType.SINGLE_VERSION)

.setAutoSync(true);

singleKvStore = kvManager.getKvStore(CREATE, Constant.KV_STORE_NAME);

kvStoreObserverClient = new KvStoreObserverClient();

singleKvStore.subscribe(SubscribeType.SUBSCRIBE_TYPE_ALL, kvStoreObserverClient);

//实现KvStoreObserver,重新onChange()方法,获取分布式数据,更新UI需要切换到主线程。

private class KvStoreObserverClient implements KvStoreObserver {

@Override

public void onChange(ChangeNotification notification) {

String value = singleKvStore.getString("***");

DataCollectionEntry entry = ZSONObject.stringToClass(value, DataCollectionEntry.class);

getUITaskDispatcher().asyncDispatch(() -> initView(entry));

}

}

从报名HarmonyOS开发者创新大赛开始,团队从一群从来没有配合过的HarmonyOS新手开发者成长为了专业开发者。参加大赛也让团队深刻感受到了HarmonyOS强大的分布式技术以及先进的设计理念,为今后开发更具创意和社会价值的作品打下了坚实的基础。

星光不问赶路人,每一位HarmonyOS开发者都是华为汇聚的星星之火,希望越来越多的开发人才能够加入到HarmonyOS开发者生态,一起创造无限可能!

收起阅读 »

Java静态代理和动态代理

前言 再开始之前我们先不使用任何代理来实现一个网络请求的流程。 定义一个请求的接口: public interface Request { void request(); } 使用OkHttp来实现这个接口 public class ...
继续阅读 »

  • 前言



再开始之前我们先不使用任何代理来实现一个网络请求的流程。


定义一个请求的接口:


public interface Request {
void request();
}

使用OkHttp来实现这个接口


public class OkHttpImpl implements Request {
@Override
public void request() {
System.out.println("OkHttp请求成功");
}
}

现在我们的网络请求已经写好了,我们测试一下:


Request request = new OkHttpImpl();
request.request();

输出: OkHttp请求成功

看起来挺好用的,但是项目经理是个老程序员了,没有用过OkHttp,非要说Volley比OkHttp好用,让你把所有网络请求换成Volley框架


我们使用Volley来实现Request接口


public class VolleyImpl implements Request{
@Override
public void request() {
System.out.println("Volley请求成功");
}
}

重新测试测试一下:


Request request = new VolleyImpl();
request.request();

输出: Volley请求成功

现在项目经理又来了,说:“你网络请求怎么连个加载框都有没?”,这个时候又得去改代码了,但是公司网络框架已经封住好了,不让随便修改,这个时候没有办法了,只能这样写了:


showDialog(); //显示加载进度条

Request request = new VolleyImpl();
request.request();

hideDialog(); //隐藏加载进度条

看起来代码没问题,但是项目中有上百个网络请求,难道每次写网络请求都要手动加上进度条的代码吗?这个时候你去问项目经理,项目经理说:“你去看看Java静态代理和动态代理,或许能找到答案~”。



  • 静态代理



在这里插入图片描述 看起来用户不需要直接访问网络框架了,而是先访问一个代理类,由代理类去执行网络请求,那我们先新建一个代理类:


public class RequestProxy implements Request {

private final Request mRequest;

public RequestProxy(Request request) {
mRequest = request;
}

public void before(){
System.out.println("开始请求");
showDialog(); //显示加载进度条
}

public void after(){
System.out.println("请求完成");
hideDialog(); //隐藏加载进度条
}

@Override
public void request() {
before();
mRequest.request();
after();
}
}

现在我们来测试一下:


Request request = new VolleyImpl();
RequestProxy proxy = new RequestProxy(request);
proxy.request();

输出:
开始请求
Volley请求成功
请求完成

静态代理优点:



  1. 可以在代理类中对目标类进行扩展。

  2. 用户只需要使用代理类的方法,不需要关心真正实现方法。

  3. 用户可以通过代理类实现与真正逻辑的解耦。


静态代理的缺点:



  1. 如果增加一个接口,还需要重新写一个代理类。



  • 动态代理



动态代理不需要写代理类,能很好的弥补静态代理的缺点


我们需要使用Java内部给我们提供好的**Proxy.newProxyInstance()**方法


public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)


newProxyInstance方法需要传入三个参数:



  1. loader: 类加载器

  2. interfaces: 要代理的接口

  3. InvocationHandler: 会回调动态代理的消息


我们先来实现一下动态代理:


Request request = new VolleyImpl();
Object o = Proxy.newProxyInstance(request.getClass().getClassLoader(), new Class[]{Request.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("请求前");
method.invoke(request, args);
System.out.println("请求后");
return null;
}
});

((Request) o).request();

输出:
请求前
Volley请求成功
请求后


  • 动态代理代码解析



我们先把要代理的接口传入到newProxyInstance方法中,并拿到代理对象“o”。


Object o = Proxy.newProxyInstance(request.getClass().getClassLoader(), new Class[]{Request.class}, new InvocationHandler() {})

我们可以把代理类强转成我们要代理的接口,然后直接调用方法


((Request) o).request();

这样代理类的invoke()方法就会被回调,我们看一下invoke()的三个参数:


@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

return null;
}


  1. proxy: 代理类的对象

  2. method: 代理类调用的方法

  3. args: 代理类调用方法传的参数


既然回调方法中有method参数了,我们就可以利用反射直接掉用method.invoke(request, args)来调用方法了,同时我们也可以在调用方法前后加上要扩展的代码。


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

优化Android工程中的图片资源

场景 在一些上古工程中,由于年久失修,架构演进跟不上业务发展需要,会衍生出非常多比较明显的性能问题,其中就包括工程中图片资源的问题。 最明显的例子就是,工程中的图片资源未经任何压缩,直接使用来自设计稿中的原图,非常占用安装包体积;其次,显示效果不理想,在对...
继续阅读 »

场景


在一些上古工程中,由于年久失修,架构演进跟不上业务发展需要,会衍生出非常多比较明显的性能问题,其中就包括工程中图片资源的问题。


最明显的例子就是,工程中的图片资源未经任何压缩,直接使用来自设计稿中的原图,非常占用安装包体积;其次,显示效果不理想,在对应分辨率的图片资源文件夹中放入了错误尺寸的图片,导致应用运行时 UI 图片出现模糊、大颗粒等情况。


优化方案


压缩图片资源文件夹的大小


优化工作往往要从业务入手,在业务发展方向明确的前提下,并不是所有的 UI 效果都需要用图片文件的方式进行显示,对于一些简单的 UI,可以考虑使用代码进行绘制。使用代码绘制可以极其明显的减少图片对硬件资源的占用,一来可以减小包体积,二来通常可以减小运行时的内存。


对于一些必须需要通过图片文件来实现的 UI 效果,也需要对图片文件进行相应的压缩后再放入对应分辨率的文件夹,可以考虑无损压缩和有损压缩。


这里重点提下有损压缩,并不是所有的有损压缩都会直接影响 UI 呈现的,如果事先获知应用所运行的设备屏幕硬件本身色彩还原度很差,尺寸较小,分辨率也较低,那么有损压缩往往是更具性价比的选择。


注意这里的压缩不单单指图片质量的压缩,同时也包括图片尺寸的缩放。对于一些特定设备屏幕尺寸,我们可以限定一个最大的图片尺寸作为约束。


检查对应分辨率资源文件夹下的图片


种种原因下,代码工程中往往会存在对于分辨率资源文件夹下放错图片资源的情况。


比如,在 drawable-xxhdpi 下放入了本应该放在 drawable-mdpi 的图片资源,那么最终的 UI 呈现就可能会出现模糊、大颗粒、锯齿感等情况。


image.png


比如下图,在一个 xhdpi 的设备中,实际加载了 mdpi 的图片资源,导致出现 UI 模糊情况。


定义一个 48dp×48dp 的控件,实际控件大小为 96px×96px


<ImageView    
android:id="@+id/iv"   
android:src="@mipmap/ic_launcher"   
app:layout_constraintBottom_toBottomOf="parent"   
app:layout_constraintTop_toTopOf="parent"   
app:layout_constraintRight_toRightOf="parent"   
app:layout_constraintLeft_toLeftOf="parent"   
android:layout_width="48dp"   
android:layout_height="48dp"/>


如果放错了图片资源,则实际加载了 48px×48px 大小的图片。


image.png 将应用进行截图,放大后可以很明显看到模糊情况。


image.png


提供两种方案供参考。


第一种是运行时检查,结合 BitmapCanary 工具,判断应用运行时 UI 控件是否加载了对应尺寸的图片,如果加载的图片资源尺寸小于控件自身的尺寸,那么就需要特别关注,并返回代码工程中进行修改。


第二种是开发时检查,通过脚本工具遍历工程图片资源文件夹中的图片文件,逐一检查图片尺寸,结合我们之前定义过的图片最大尺寸约束,可以剔除并发现放错的图片资源,再针对筛选出的这些特定的图片资源作压缩和缩放。


优化工具


为了让优化工具更加通用,我编写了 ImageRes361Tool 工具,它的工作流程和架构图如下。


架构图


image.png



  • ImageRes361Tool 层:应用层,负责一键执行

  • ImageFinder 层:负责查找工程中不合规的图片资源

  • ImageSaver 层:保存图片

  • Config 层:配置压缩等级、策略以及目标文件夹

  • ImageCompressTool 层:包装图片压缩功能,简化压缩 API

  • PIL、OpenCV 层:负责压缩、处理图片

  • Logger 层:记录日志

  • Thread 层:多线程操作,提升执行效率


工作流程


image.png


使用流程


python 环境


python3 环境要求


输入工程地址


image.png


回车运行


image.png


最终效果


以工程中其中一个 module 为例,清理掉超出图片最大尺寸约束的图片后,图片资源大小可以由 4.4Mb 锐减至 88Kb ;检查并修改对应分辨率的图片资源后,应用运行时不再出现 UI 模糊的情况。


后记


优化类工作往往解决的不仅仅是技术问题,更是管理问题。


制定了开发标准能否顺利执行?架构演进能否跟上业务的不断发展?为了性能指标能否排除万难团结协作?......


管理类问题只能交由管理解决,绝不是某个技术工具就能解决得了的。


看着那些来自大厂的头部 APP,白屏、卡顿、高内存占用等都非常常见,再加上给用户定制的“私人专属”开屏广告,使得启动速度异常地慢。从用户体验的角度来说,不可谓优秀。是它们的技术力不够吗?


应该不是。


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

玩会儿Compose,原神主题列表

Jetpack Compose出来有一段时间了,一直都没有去尝试,这次有点想法去玩一玩这个声明性界面工具,就以“原神”为主题写个列表吧。 整体设计参考DisneyCompose 效果图: 数据源 因为数据比较简单,也就只包含图片、姓名、描述等。...
继续阅读 »

Jetpack Compose出来有一段时间了,一直都没有去尝试,这次有点想法去玩一玩这个声明性界面工具,就以“原神”为主题写个列表吧。


整体设计参考DisneyCompose


效果图:


image.png


image.png


数据源


因为数据比较简单,也就只包含图片、姓名、描述等。所以在后台数据存储上选择的是Bmob后端云,一个方便前端开发的后端服务平台。


主要数据也是从原神各大网站搜集下来的,新建表结构并且将数据填充,我们简单看一下Bmob的后台。


image.png


数据准备好了,那就开始我们的Compose之旅。


首页UI绘制


整体结构


从上面的项目效果图来看,首页总布局属于是一个网格列表,平分两格,列表中的每个Item上方带有头像,头像下面是角色名称以及角色其他信息。


image.png


网格布局


因为整体分成两列,所以选择的是网格布局,Compose提供了一个实现-LazyVerticalGrid


fun LazyVerticalGrid(
cells: GridCells,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
content: LazyGridScope.() -> Unit
)

LazyVerticalGrid中有几个重要参数先说明一下:



  • GridCells :主要控制如何将单元格构建为列,如GridCells.Fixed(2),表示两列平分。

  • Modifier : 主要用来对列表进行额外的修饰。

  • PaddingValues :主要设置围绕整个内容的padding。

  • LazyListState :用来控制或观察列表状态的状态对象


首页布局是平分两列的网格布局,那相应的代码如下:


LazyVerticalGrid(cells = GridCells.Fixed(2)) {}

单个Item


看过了外部框架,那现在来看每个Item的布局。每个Item为卡片式,外边框为圆角,且带有阴影。内部上方是一张图片Image,图片下方是两行文字Text。那Item具体该怎样布局?


我们先来看看在Compose之前,在xml中是怎么写?例如使用ConstraintLayout布局,顶部放一个ImageView,再来一个TextView layout_constraintTop_toBottomOf ImageView,最后在来个TextViewTopToBottomOf第一个TextView


那使用Compose应该怎么写?


其实在Compose里也存在着ConstraintLayout布局并且具体Api的调用思路与在xml中使用也是一致的。我们就来看看具体操作。


ConstraintLayout() {
Image()
Text()
Text()
}

一共两个元素:ImageText,分别代表着xml里的ImageViewTextView



  • Image:


Image(
painter = rememberCoilPainter(request = item.url),
contentDescription = "",
contentScale = ContentScale.Crop,
modifier = Modifier
.clickable(onClick = {
val objectId = item.objectId
navController.navigate("detail/$objectId")
})
.padding(0.dp, 4.dp, 0.dp, 0.dp)
.width(180.dp)
.height(160.dp)
.constrainAs(image) {
centerHorizontallyTo(parent)
top.linkTo(parent.top)
})

Image加载的是网络图片,则使用painter加载图片链接,contentScale与xml中的scaleType相似,modifier主要设置图片的样式,点击事件、宽高等。里面有一个需要注意的点constrainAs(image)


constrainAs(image) {
centerHorizontallyTo(parent)
top.linkTo(parent.top)
}

这段代码主要表示Image在父布局中的位置,例如相对父布局,相对其他子控件等,有点xml中layout_constraintTop_toBottomOf内味。下面Text也是相同的道理。



  • Text


Text(text = item.name,
color = Color.Black,
style = MaterialTheme.typography.h6,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(0.dp, 4.dp, 0.dp, 0.dp)
.constrainAs(title) {
centerHorizontallyTo(parent)
top.linkTo(image.bottom)
}
)

Text的设置主要包含Text内容、文字类型、大小、颜色等。在constrainAs(title)里有一句top.linkTo(image.bottom),这句代码指的就是xml中,TextView layout_constraintTop_toBottomOf ImageView


在Image和Text中发现了一个点,constrainAs(?)中传入了一个值,且设置相对位置时也是以此值为控件的代表。这是在进行相对位置的设定之前,利用createRefs创建多个引用,在ConstraintLayout中作为Modifier.constrainAs的一部分分配给布局。


val (image, title, content) = createRefs()

具体代码:


ConstraintLayout() {
val (image, title, content) = createRefs()
//头像
Image(
//图片地址
painter = rememberCoilPainter(request = item.url),
contentDescription = "",
//图片缩放规则
contentScale = ContentScale.Crop,
modifier = Modifier
.clickable(onClick = {//点击事件
val objectId = item.objectId
navController.navigate("detail/$objectId")
})
.padding(0.dp, 4.dp, 0.dp, 0.dp)
.width(180.dp)
.height(160.dp)
.constrainAs(image) {
centerHorizontallyTo(parent) //水平居中
top.linkTo(parent.top)//位于父布局的顶部
})
//文字
Text(text = item.name,
color = Color.Black,//颜色
style = MaterialTheme.typography.h6,//字体格式
textAlign = TextAlign.Center,
modifier = Modifier
.padding(0.dp, 4.dp, 0.dp, 0.dp)
.constrainAs(title) {
centerHorizontallyTo(parent)//水平居中
top.linkTo(image.bottom)//位于图片的下方
}
)
Text(text = item.from,
color = Color.Black,
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(4.dp)
.constrainAs(content) {
centerHorizontallyTo(parent)
top.linkTo(title.bottom)

})
}

image.png


数据填充


UI已经画好了,接下来就是数据展示的事情。还是以ViewModel-LiveData-Repository为整体请求方式。 因为数据都存储到了Bmob后台,就直接使用Bmob的方式查询数据:


private val bmobQuery: BmobQuery<GcDataItem> = BmobQuery()

fun queryRoleData(successLiveData: MutableLiveData<List<GcDataItem>>) {
bmobQuery.findObjects(object : FindListener<GcDataItem>() {
override fun done(list: MutableList<GcDataItem>?, e: BmobException?) {
if (e == null) {
successLiveData.value = list
}
}

})
}

具体的请求方式可参考Bmob的完档,这里就不在赘述。 ViewModel中还是抛出一个LiveData,而UI层相对之前有一些变化。


@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HomePoster(navController: NavController, model: HomeViewModel
= viewModel()) {
model.queryGcData()
val data: List<GcDataItem> by model.getDataLiveData().observeAsState(listOf())

LazyVerticalGrid(cells = GridCells.Fixed(2)) {
items(data) {
ItemPoster(navController, item = it)
}
}

}

Compose提供了一个viewModel()方法来获取ViewModel实例,至于怎么拿到数据,Compose提供了LiveData的一个扩展方法 observeAsState(listOf()) 。它的主要作用是用来观察这个LiveData,并通过State表示它的值,每次有新值提交到LiveData时,返回的状态将被更新,从而导致每个状态的重新组合。


拿到List数据后,网格LazyVerticalGrid就开始使用items(data){}添加列表,


 LazyVerticalGrid(cells = GridCells.Fixed(2)) {
items(data) {
ItemPoster(navController, item = it)
}
}

而ItemPoster就是我们设置Item布局的地方,将每个Item的数据传递给ItemPoster,利用Image、Text等控件设置imageUrl、text内容等。


@Composable
fun ItemPoster(navController: NavController, item: GcDataItem) {
Surface(
modifier = Modifier
.padding(4.dp),
color = Color.White,
elevation = 8.dp,
shape = RoundedCornerShape(8.dp)
)
{
ConstraintLayout() {
val (image, title, content) = createRefs()

Image(
//设置图片Url-item.url
painter = rememberCoilPainter(request = item.url),
...)

Text(text = item.name
...)

Text(text = item.from
...)
}

}

跳转


样例中还有一个从列表跳转到详情页的功能,Compose提供了一个跳转组件-navigation。这个navigation与之前管理Fragment的navigation思路也是一致的,利用NavHostController进行不同页面的管理。我们先使用 rememberNavController()方法创建一个NavHostController实例。


val navController = rememberNavController()

接着将navController与NavHost相关联,且设置导航图的起始目的地startDestination


 NavHost(navController = navController, startDestination = "Home") {}

我们将起始目的地暂时先标记为“Home”。 那如何对页面进行管理?这就需要在NavHost中使用composable添加页面,例如该项目有两个页面,一个首页列表页,一个详情页。我们就可以这样写:


 NavHost(
navController = navController, startDestination = "Home"
)
{
composable(
route = "Home",
)
{
HomePoster(navController)
}

composable("detail/{objectId}"){
val objectId = it.arguments?.getString("objectId")
DetailPoster(objectId){
navController.popBackStack()
}
}
}

第一个composable则代表的是列表页,并且将到达目的地的路线route设置为“Home”,其实类似于ARouter框架中在每个Activity上设置Path,做一个标识作用,后面做跳转时也是依据该route进行跳转。


第二个composable则代表的是详情页,同样设置route="detail"


那如何从列表页跳到详情页?只需要在点击事件里使用navController.navigate("detail"),传入想要跳转的route即可。


携带参数跳转


因为详情页需要根据所点击列表Item的Id进行数据查询,点击时要将id传到详情页,这就需要携带参数。 在Compose中,向route添加参数占位符,如"detail/{objectId}",从composable()函数提取 NavArguments。 如下修改详情页:


 composable("detail/{objectId}"){
val objectId = it.arguments?.getString("objectId")
DetailPoster(objectId){
navController.popBackStack()
}
}

跳转时将objectId传到route的占位符中即可。


clickable(onClick = {
val objectId = item.objectId
navController.navigate("detail/$objectId")})

当然,compose navigation还支持launchMode设置、深层链接等,具体可查看官方文档


一点感受


对于用习惯了xml编写UI的我来说,首次上手Compose其实还是蛮不习惯,Compose打破了原有的格局,给了我们一个全新的视角去看待Android,学完后有种“哦,原来UI还可以这么干!!”的感叹。对于Android开发者来说,其实需要这些新的路线去突破自己的固有化思维。


Compose的风格其实和Flutter有点像,估计是出于同一个爸爸的原因。但是Compose没有Flutter的无限套娃,对Android开发者来说还是比较友好的。如果想要学习Flutter,可以用Compose作为过渡。


以上便是本篇内容,感谢阅读,如果对你有帮助,欢迎点赞收藏关注三连走一波?   



项目地址:genshin-compose


收起阅读 »

基于环信MQTT消息云,Web版MQTT客户端快速实现消息收发

本文介绍Web版MQTT 客户端,如何连接环信MQTT消息云快速实现消息的自收自发。一、前提条件1.部署Web开发环境下载安装 IDE。您可以使用VS Code或者WebStorm,本文以VS Code IDEA为例。下载安装浏览器,本文使...
继续阅读 »

本文介绍Web版MQTT 客户端,如何连接环信MQTT消息云快速实现消息的自收自发。

一、前提条件

1.部署Web开发环境

下载安装 IDE。您可以使用VS Code或者WebStorm,本文以VS Code IDEA为例。

下载安装浏览器,本文使用谷歌浏览器

2.导入项目依赖
在VS Code IDEA中创建index.html文件,并在文件中引入Eclipse Paho JavaScript SDK
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.js" type="text/javascript"></script>

二、实现流程

1、获取鉴权

     为保障客户安全性需求,环信MQTT消息云服务为客户提供【token+clientID】方式实现鉴权认证,其中AppID(clientID中的关键字段)及token标识获取流程如下:

【登录console】
    欢迎您登录环信云console控制台,在此控制台中,为您提供应用列表、解决方案、DEMO体验以及常见问题等功能。
     在应用列表中,若您未在APP中开通MQTT业务,可参见APP  MQTT开通流程
     若APP已开通MQTT业务,可在应用列表中选中Appname,点击【查看】操作,进入应用详情。


【获取AppID及连接地址】 
      进入【查看】后,点击左侧菜单栏【MQTT】->【服务概览】,在下图红色方框内获取当前AppID及服务器连接地址。


【获取token】
     为实现对用户管控及接入安全性,环信云console提供用户认证功能,支持对用户账户的增、删、改、查以及为每个用户账户分配唯一token标识,获取token标识可选择以下两种形式。
  形式一:console控制台获取(管理员视角)
  * 点击左侧菜单栏【应用概览】->【用户认证】页面,点击【创建IM用户】按钮,增添新的账户信息(包  括用户名及密码)。
  * 创建成功后,在【用户ID】列表中选中账户,点击【查看token】按钮获取当前账户token信息。


  形式二:客户端代码获取(客户端视角)
  * 获取域名:点击左侧菜单栏【即时通讯】->【服务概览】页面,查看下图中token域名、org_name、app_name。


  * 拼接URL:获取token URL格式为:http:/ /token域名/org_name/app_name/token。 
  * 用户名/密码:使用【用户ID】列表中已有账户的用户名及密码,例“用户名:test/密码:test123”。

// 客户端获取token(password)代码示例如下: 
function getAccessToken() {
var grantType = 'password'
var request = new XMLHttpRequest()
// token 域名
var baseUrl = 'a3.easemob.com'
// org_name
var orgName = 'easemob-test'
// app_name
var appName = 'ease-test'
// 拼接token接口
var api = `http://${baseUrl}/${orgName}/${appName}/token`
var token = ''
// Post请求
request.open('post', api)

request.onreadystatechange = function () {
if (request.readyState == 4 && request.status == 200) {
var res = JSON.parse(request.responseText)
// 从响应体中解析出token
token = res.access_token
console.log(token, 'accessToken')
} else {
throw new Error('请求失败,响应码为' + request.status)
}
}

var params = {
grant_type: grantType,
username: 'test',
password: 'test1'
}
// 发送ajax请求
request.send(JSON.stringify(params))
}

//返回结果
{
"access_token": "YWMtN8a0oqV3EeuF0AmiqRgEh-grzF8zZk2Wp8GS3pF-orDW_F-gj3kR6os3h_oz3ROQAwMAAAF5BxhGlwBPGgAvTR8vDrdVsDPNZMQj0fFjv7EaohgZhzMHM9ncVLE30g",
"expires_in": 5184000,
"user": {
"uuid": "d6fc5fa0-8f79-11ea-8b37-87fa33dd1390",
"type": "user",
"created": 1588756404898,
"modified": 1588756404898,
"username": "test",
"activated": true
}
}
access_token即为要获取的token


2、初始化

      在VS CodeIDEA工程中创建index.html,客户端初始配置包括创建clientID,port,连接地址等信息。

<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.js" type="text/javascript"></script>
var mqtt
// 设置当前用户的接入点域名,进入console控制台获取
var host = '//xxx.xxx.xxx.xxx'
// WebSocket 协议服务端口,如果是走 HTTPS,设置443端口
var port = 80
// 从console控制台获取
var appId = 'TESTAPPID'
// MQTT 用户自定义deviceID
var deviceId = 'deviceId'
// clientId 格式 deviceID@AppID
var clientId = deviceId + '@' + appId

mqtt = new Paho.MQTT.Client(
host,
port,
clientId
)


3、连接服务器

    配置连接密码、cleansession标志、心跳间隔、超时时间等信息,调用connect()函数连接至环信MQTT消息云。

// 是否走加密 HTTPS,如果走 HTTPS,设置为 true
var useTLS = false
// cleansession标志
var cleansession = true
// 超时重连时间
var reconnectTimeout = 2000
// 用户名,在console中注册
var username = 'test'
// 用户密码为第一步中申请的token
var password = 'test123'

var options = {
timeout: 3,
// 连接成功回调
onSuccess: onConnect,
mqttVersion: 4,
cleanSession: cleansession,
// 连接失败回调
onFailure: function (message) {
setTimeout(MQTTconnect, reconnectTimeout)
}
}

options.userName = username
options.password = password
// 如果使用 HTTPS 加密则配置为 true
options.useSSL = useTLS

mqtt.connect(options)


4、订阅【subscribe】

【订阅主题】

当客户端成功连接环信MQTT消息云后,需尽快向服务器发送订阅主题消息。

// 需要订阅或发送消息的topic名称
var topic = 't/t1'
// 订阅消息 QoS参数代表传输质量,可选0,1,2。详细信息,请参见名词解释。
mqtt.subscribe(topic, { qos: 1 })

【取消订阅】

// 取消订阅
mqtt.unsubscribe(topic)

【接收消息】

    配置接收消息回调方法,从环信MQTT消息云接收订阅消息。(tip: 需要在连接之前设置回调方法)

function onMessageArrived(message) {
var topic = message.destinationName
var payload = message.payloadString
console.log("recv msg : " + topic + " " + payload)
}
mqtt.onMessageArrived = onMessageArrived


5、发布【publish】

   环信MQTT消息云中指定topic发送消息。

//set body
message = new Paho.MQTT.Message("Hello mqtt!!")
// set topic
message.destinationName = topic
mqtt.send(message)


6、结果验证

connect success
send msg : t/t1 Hello mqtt!!
recv msg : t/t1 Hello mqtt!!

三、更多信息

  完整demo示例,请参见demo下载或直接下载:MQTTChatDemo-Web.zip

  * 目前MQTT客户端支持多种语言,请参见 SDK下载

  * 如果您在使用MQTT服务中,有任何疑问和建议,欢迎您联系我们

收起阅读 »

JS循环大总结, for, forEach,for in,for of, map区别

map(数组方法): 特性: map不改变原数组但是会 返回新数组 可以使用break中断循环,可以使用return返回到外层函数 实例: let newarr=arr.map(i=>{ return i+=1; console.log(i); })...
继续阅读 »

map(数组方法):


特性:



  1. map不改变原数组但是会 返回新数组

  2. 可以使用break中断循环,可以使用return返回到外层函数


实例:


let newarr=arr.map(i=>{
return i+=1;
console.log(i);
})
console.log(arr)//1,3,4---不会改变原数组
console.log(newarr)//[2,4,5]---返回新数组

forEach(数组方法):


特性:



  1. 便利的时候更加简洁,效率和for循环相同,不用关心集合下标的问题,减少了出错的概率。

  2. 没有返回值

  3. 不能使用break中断循环,不能使用return返回到外层函数


实例:


let newarr=arr.forEach(i=>{
i+=1;
console.log(i);//2,4,5
})
console.log(arr)//[1,3,4]
console.log(newarr)//undefined

注意:



  1. forEach() 对于空数组是不会执行回调函数的。

  2. for可以用continue跳过循环中的一个迭代,forEach用continue会报错。

  3. forEach() 需要用 return 跳过循环中的一个迭代,跳过之后会执行下一个迭代。


for in(大部分用于对象):


用于循环遍历数组或对象属性


特性:


可以遍历数组的键名,遍历对象简洁方便
###实例:


   let person={name:"小白",age:28,city:"北京"}
let text=""
for (let i in person){
text+=person[i]
}
输出结果为:小白28北京
//其次在尝试一些数组
let arry=[1,2,3,4,5]
for (let i in arry){
console.log(arry[i])
}
//能输出出来,证明也是可以的

for of(不能遍历对象):


特性:



  1. (可遍历map,object,array,set string等)用来遍历数据,比如组中的值

  2. 避免了for in的所有缺点,可以使用break,continue和return,不仅支持数组的遍历,还可以遍历类似数组的对象。


   let arr=["nick","freddy","mike","james"];
for (let item of arr){
console.log(item)
}
//暑促结果为nice freddy mike james
//遍历对象
let person={name:"老王",age:23,city:"唐山"}
for (let item of person){
console.log(item)
}
//我们发现它是不可以的
//但是它和forEach有个解决方法,结尾介绍

总结:



  • forEach 遍历列表值,不能使用 break 语句或使用 return 语句

  • for in 遍历对象键值(key),或者数组下标,不推荐循环一个数组

  • for of 遍历列表值,允许遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等.在 ES6 中引入的 for of 循环,以替代 for in 和 forEach() ,并支持新的迭代协议。

  • for in循环出的是key,for of循环出的是value;

  • for of是ES6新引入的特性。修复了ES5的for in的不足;

  • for of不能循环普通的对象,需要通过和Object.keys()搭配使用。


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

收起阅读 »

Vue3+Vite+TS+Eslint(Airbnb规则)搭建生产项目,踩坑详记(三):引入Element-plus,解决字体文件404问题

vue
今天我们来看引入大杯Element,其实引入很简单,跟着文档操作就完事了。所以这篇文章重点是看如何修改主题以及在修改主题中我遇到的问题。 废话少说,开整! 引入Element-plus npm install element-plus --save // m...
继续阅读 »

今天我们来看引入大杯Element,其实引入很简单,跟着文档操作就完事了。所以这篇文章重点是看如何修改主题以及在修改主题中我遇到的问题。


image.png


废话少说,开整!


引入Element-plus


npm install element-plus --save

// main.ts
import { createApp } from 'vue';
import ElementPlus from 'element-plus'; // ++
import 'element-plus/lib/theme-chalk/index.css'; // ++
import App from './App.vue';

createApp(App).use(ElementPlus).mount('#app'); // edit

此时在项目中引入Element组件测试发现,已经可以正常使用了。


image.png


修改Element主题


在Element文档中有如何修改主题的教程,我们项目中主要的需求就是修改主题色,因此本文也以修改主题色为例子。


创建文件


首先我新增了两个文件,color.sass 和 element-theme.sass(这里假设你的项目已经引入了sass)。之所以创建两个文件,是因为 color.sass 除了给element主题提供颜色配置,还会引入为全局变量,方便在组件中使用。


image.png


配置主题


// color.sass
$--color-primary: red

// element-theme.sass
@improt "./color.sass" // 引入主题色文件

$--font-path: '~element-plus/lib/theme-chalk/fonts'
@import "~element-plus/packages/theme-chalk/src/index"

// main.ts
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import 'element-plus/lib/theme-chalk/index.css'; // --
import './styles/element-theme.sass'; // ++
import App from './App.vue';

createApp(App).use(ElementPlus).mount('#app');

如上按照element官网给的例子引入以后,在vite项目中会报错。


image.png
这是因为 ~ 这种路径写法是vue-cli中的约定,它使我们可以引用node_modules包内的资源,详见文档:URL转换规则
image.png


所以我们在这里需要把路径 ~element-plus 改成 node_modules/element-plus。也就是文件变成了这个样子:


@import "./color.sass"

$--font-path: 'node_modules/element-plus/lib/theme-chalk/fonts'
@import "node_modules/element-plus/packages/theme-chalk/src/index"

文章写到这里的时候遇到了一个尴尬的问题,在我们的生产项目搭建框架时,路径改成由 node_modules 引入后,主题色修改没有问题,可以生效。但是fonts文件加载请求报404,如下图1。但是文章用的实验项目,同样的方式修改后,一切正常,如图2。


image.png


image.png


经过反复测试后发现,是因为生产项目配置了多入口,启动项目时对应了不同的入口文件,导致引入fonts文件报错。具体原因有待研究,希望了解的兄弟不吝赐教


关于多入口文件配置以及解决element字体引入的问题,后边会有一篇文章单独介绍,这里就先不剧透了。


配置全局变量


前面单独创建了一个color.sass是为了将文件里的颜色变量引入到全局,方便在组件中使用。
为了简化使用,我们可以在文件中为常用颜色额外定义简短变量,但是要注意,不能修改element需要的变量!


$--color-primary: #ff0000

$primary: #ff0000

引入全局变量需要在vite.config.ts文件中配置css预处理器,并将引入的变量文件传给预处理器。配置方式如下


// vite.config.ts
...
export default defineConfig({
...
css: {
preprocessorOptions: {
sass: {
// \n 处理文件中多个引入报换行错误的问题
additionalData: "@import './src/styles/color.sass'\n",
},
},
},
});

引入后我们在组件内进行测试


// HelloWorld.vue
<style scoped lang="sass">
a
color: $primary
</style>

可以看到页面上已经生效了
image.png


因为通过这种方式插入全局变量,会为所有的.sass文件都插入对应的文件引入,所以在前面我们定义的 element-theme.sass 文件中就可以不写 color.sass 文件的引入了。


// element-theme.sass

// @import "./color.sass" // edit

$--font-path: 'node_modules/element-plus/lib/theme-chalk/fonts'
@import "node_modules/element-plus/packages/theme-chalk/src/index"

修改默认语言


可能是为了立足中国,走向世界。使用组件时会发现大杯Element的默认语言变成了英文,我们需要自己引入并修改默认语言为中文。


// main.ts
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import './styles/element-theme.sass';
import locale from 'element-plus/lib/locale/lang/zh-cn'; // ++
import App from './App.vue';

createApp(App).use(ElementPlus, { locale }).mount('#app'); // edit

修改完成后,再去看看组件,是不是已经变成了中文。


引入大杯Element并修改主题的工作已经完成了,项目中我们就可以使用自定义主题色的Element组件。并且抛出了主题色全局变量,方便我们在组件中使用。


下次我们将一次性引入Vuex和Vue Router,这两项工作完成后,就已经完成了项目框架的雏形,可以开始开发了。不过后续我们仍然会有一些优化以及Vue3开发过程中相较于Vue2有较大变化的方法总结,整理不易,希望大家多多支持。


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

收起阅读 »

Vue3+Vite+TS+Eslint(Airbnb规则)搭建生产项目,踩坑详记(二):配置husky和lint-staged

vue
上回我们说到配置ESLint以及添加vue-recommended、airbnb-base、prettier规则,一切都很顺利。唯有一点需要注意的就是 .eslintrc 文件extends配置项中,plugin:prettier/recommended一定要...
继续阅读 »

上回我们说到配置ESLint以及添加vue-recommended、airbnb-base、prettier规则,一切都很顺利。唯有一点需要注意的就是 .eslintrc 文件extends配置项中,plugin:prettier/recommended一定要在airbnb-base之后添加,上篇文章没有看到的童鞋们可以回去看看原因。


上篇文章最后我们提到,在开发阶段进行ESLint校验,效果是一件靠自觉的事。因此我们需要在代码提交前再次执行ESLint,加强校验力度以保证Git上得到的都是优美的代码。


我们本次需要用到的工具有两个:huskylint-staged


husky


它的主要作用就是关联git的钩子函数,在执行相关git hooks时进行自定义操作,比如在提交前执行eslint校验,提交时校验commit message等等。


Install


husky官网推荐使用自动初始化命令,因为我们就按照官网推荐的方式进行安装,以npm为例


// && 连接符在vscode中会报错,建议在windows的powershell执行
npx husky-init && npm install

执行完成后,项目根目录会多出来 .husky 文件夹。


image.png


内部的_文件夹我们在此无需关心,pre-commit文件便是在git提交前会执行的操作,如图。我们可以在当前目录创建钩子文件来完成我们想要的操作。


image.png


需要注意的是,新版husky的配置方式做出了破坏性的改变,如果在使用过程中发现配置完以后没有生效,可以注意查看一下安装版本


升级方式可以查看官方文档:typicode.github.io/husky/#/?id…


配置


我们想要在提交前执行eslint校验代码,因此修改husky的pre-commit文件即可。我们在文件中添加如下代码


#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

eslint . --ext .js,.ts,.vue --fix #++ 校验所有的.js .ts .vue文件,并修复可自动修复的问题
git add . #++ 用于将自动修复后改变的文件添加到暂存区
exit 1 #++ 终止命令,用来测试钩子

此时提交代码执行commit是可以看到已经进入了pre-commit文件执行命令。但是会报错


image.png


这是因为此处执行shell命令,需要我们全局安装eslint。执行 npm install -g eslint。
安装完成后再次执行git commit,可以看到已经可以正常运行了


image.png


错误处理




  • 截图中第一个报错是书写错误,直接改掉就好。




  • 第二个错误,是因为我们的ESLint中没有配置TS的解析器,导致ESLint不能正常识别并校验TS代码。解决它,我们安装 @typescript-eslint/parser,并修改ESLint配置即可。




npm install @typescript-eslint/parser --save-dev

// .eslintrc.js
...
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser', // ++
},
...


  • 第三个错误,它说的是我们引入的vite和@vitejs/plugin-vue两个包在 package.json 中应该是dependencies而不是devDependencies依赖。这个错误是因为airbnb-base规则设置了不允许引入开发依赖包,但是很明显我们不应该修改这两个框架生成的依赖结构。那我们看一下airbnb关于这条规则的定义


image.png
可以看到,airbnb对这条规格做了列外处理,那就很好办了,我们只需要在它的基础上,添加上上面报错的两个包。


在eslint中添加如下规则:


// .eslintrc.js
...
rules: {
...
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
... // 保持airbnb-base中的规则不变
'**vite**', // ++
'**@vitejs**', // ++
],
optionalDependencies: false,
},
],
}
...

修改完上述错误后,我们去掉 .husky/pre-commit 文件中 exit 1 这行代码,再次执行提交操作,可以看到,已经可以提交成功了。


image.png


思考


通过配置husky,我们已经实现了在提交前对代码进行检查。但是eslint配置的是 eslint . --ext .js,.ts,.vue --fix,检查所有的js、ts、vue文件,随着项目代码越来越多,每次提交前校验所有代码显然是不现实的。所以需要一个办法每次只检查新增或修改的文件。


这就需要开头提到的第二个工具来祝我们一臂之力了。


lint-staged


lint-staged的作用就是对暂存区的文件执行lint,可以让我们每次提交时只校验自己修改的文件。


npm install lint-staged --save-dev

配置lint-staged


安装完成后,在package.json文件中添加lint-staged的配置


// package.json
...
"scripts": {
...
"lint-staged": "lint-staged"
},
"lint-staged": {
// 校验暂存区的ts、js、vue文件
"*.{ts,js,vue}": [
"eslint --fix",
"git add ."
]
}

添加scripts里的lint-staged命令,是因为不建议全局安装lint-staged,以防在其他同学电脑上没有全局安装导致运行报错。


修改husky


添加lint-staged配置后,husky就不在需要直接调用eslint了。修改pre-commit文件如下:


#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

# eslint . --ext .js,.ts,.vue --fix
# git add .
# exit 1
npm run lint-staged

lint-staged配置后,我们不再需要配置husky时全局安装的eslint,因为lint-staged可以检测项目里局部安装的脚本。同时,不建议全局安装脚本,原因同上。


测试


到此,提交阶段对代码执行lint需要的配置我们已经完成了。再次提交代码测试,可以看到commit后执行的命令已经变成了lint-staged。


image.png


下一篇踩坑记我们将引入Element-plus,详细介绍其中遇到的问题,并修改element组件主题。


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

Vue3+Vite+TS+Eslint(Airbnb规则)搭建生产项目,踩坑详记(一)

vue
前段时间领导告知公司将开启一个全新的项目。 从零开始,如果不尝试一下最近火热的 Vue3 + Vite 岂不是白白浪费了这么好的吃螃蟹的机会。 说干就干,然后就开始读各种文档,从 0 开始,一步一步搭完这个项目到可以正常开发,这对于我一个第一次搭生产项目的菜鸡...
继续阅读 »

前段时间领导告知公司将开启一个全新的项目。


从零开始,如果不尝试一下最近火热的 Vue3 + Vite 岂不是白白浪费了这么好的吃螃蟹的机会。


说干就干,然后就开始读各种文档,从 0 开始,一步一步搭完这个项目到可以正常开发,这对于我一个第一次搭生产项目的菜鸡来说,着实艰难。


到今天,项目已经进入联调阶段,并且已经在环境上部署成功可以正常访问。这个实验也算是有了阶段性的成功吧,因此来写文章记录此次Vue3项目搭建历险记。


下载.jfif


第一篇文章主要是项目初始化和ESLint导入,废话不多说,开整。


初始化项目


image.png
按照自己需要的框架选择就可以了,我这里用的Vue3+TS。
初始化完成后的目录结构如下:


image.png


启动项目


执行 npm run dev,大概率会直接报错,因为项目默认启动在3000端口,可能会被拒绝。


image.png


解决这个问题,我们需要在根目录下的 vite.config.ts 文件中修改开发服务器的配置,手动指定端口号。


image.png


修改完成后重新启动项目,就可以访问了。


image.png


添加ESLint支持


安装ESLint



  • eslint只有开发阶段需要,因此添加到开发阶段的依赖中即可


npm install eslint --save-dev


  • 在VS Code中安装eslint插件,以在开发中自动进行eslint校验


配置ESLint


创建 .eslintrc.js 文件


添加基础配置


module.exports = {
root: true,
env: {
browser: true, // browser global variables
es2021: true, // adds all ECMAScript 2021 globals and automatically sets the ecmaVersion parser option to 12.
},
parserOptions: {
ecmaVersion: 12,
},
}

引入规则


为了规范团队成员代码格式,以及保持统一的代码风格,项目采用当前业界最火的 Airbnb规范 ,并引入代码风格管理工具 Prettier


eslint-plugin-vue


ESLint官方提供的Vue插件,可以检查 .vue文件中的语法错误


npm install eslint-plugin-vue

// .eslintrc.js
...
extends: [
'plugin:vue/vue3-recommended' // ++
]
...

eslint-config-airbnb-base


Airbnb基础规则的eslint插件


// npm version > 5
npx install-peerdeps --dev eslint-config-airbnb-base

// .eslintrc.js
...
extends: [
'plugin:vue/vue3-recommended',
'airbnb-base', // ++
],
...

这个时候就应该可以看到一些项目原有代码的eslint报错信息了,如果没有的话,可以尝试重启编辑器的eslint服务。


eslint-plugin-prettier


本次项目不单独引入prettier,而是使用eslint插件将prettier作为eslint规则执行。


npm install --save-dev eslint-plugin-prettier
npm install --save-dev --save-exact prettier

// .eslintrc.js
...
plugins: ['prettier'], // ++
rules: {
'prettier/prettier': 'error', // ++
},
...

配置到此时,大概率会遇到 eslint 规则和 prettier 规则冲突的情况,比如下图。eslint告诉我们要使用单引号,但是改为单引号以后,prettier有告诉我们要使用双引号。


image.png


image.png


这时候就需要另一个eslint的插件 eslint-config-prettier,这个插件的作用是禁用所有与格式相关的eslint规则,也就是说把所有格式相关的校验都交给 prettier 处理。


npm install --save-dev eslint-config-prettier

// .eslintrc.js
...
plugins: ['prettier'],
extends: [
'plugin:vue/vue3-recommended',
'airbnb-base',
'plugin:prettier/recommended', // ++
],
rules: {
'prettier/prettier': 'error',
},
...

plugin:prettier/recommended 的配置需要注意的是,一定要放在最后。因为extends中后引入的规则会覆盖前面的规则。


我们还可以在根目录新建 .prettierrc.js 文件自定义 prettier 规则,保存规则后,重启编辑器的eslint服务以更新编辑器读取的配置文件。


// .prettierrc.js
module.exports = {
singleQuote: true, // 使用单引号
}

到此,我们的ESLint基本配置结束了,后续需要时可以对规则进行调整。


这篇文章到这里就结束了,但是只在开发阶段约束代码风格是一件靠自觉性的是,因为我们还需要增强ESLint的约束度。下一篇文章,我们一起研究如果在提交代码前进行ESLint二次校验,保证提交到Git的代码都是符合规定的~


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

收起阅读 »

有趣的JS存储

今天给大家分享一下关于JS存储的问题。 建议阅读时间:5-10分钟。 序章 首先看一道经典的关于JS存储的题目,来一场紧张又刺激的脑内吃鸡大战吧: var a = {n:1};a.x = a = {n:2};console.log(a.x);console....
继续阅读 »

今天给大家分享一下关于JS存储的问题。


建议阅读时间:5-10分钟。




序章


首先看一道经典的关于JS存储的题目,来一场紧张又刺激的脑内吃鸡大战吧:


var a = {n:1};
a.x = a = {n:2};
console.log(a.x);
console.log(a);·


问输出?
想必大家心中都有答案了 ...
结果很显然是有趣的,


image.png


到这里有部分现场观众朋友就问了,这特喵咋undefined?不是赋值了吗?别急先别骂人,往下看:




探索时刻


我们先将代码这样修改:


a.x = a = {n:2};   ---- >  a = a.x = {n:2};

image.png


结果显然是一致的,不论是先给 a 赋值还是先给 a.x 赋值结果都是一致的,
查了一些资料后,得知这等式中 . 的优先级别是最高的,


因此这题的思路:


JS会把变量存到栈中,而对象则会存在堆中。


image.png



  1. 第一行代码:变量 a 的指针指向堆栈;

  2. 第二行代码:a.x = a = {n:2}; 堆1中的变量对像X指向堆2 { n:2 }, 接着给a赋值 a={n:2} ,a的指针被改变指向堆2,然后堆1没有被指针指向,被GC回收,因此输出的 a.x 是underfinde 而 a 的值是 {n:2};


理解上述代码只需要稍微理解一下js变量储存:


大家都知道,JavaScript中的变量类型分为两种,一种是基本数据类型,另外一种就是引用类型


两种数据类型的存储方式在JS中也有所不同。


另外,内存分为栈区(stack)和堆区(heap),然后在JS中开发人员并不能直接操作堆区,堆区数据由JS引擎操作完成,那这二者在存储数据上到底有什么区别呢?




揭晓时刻


一幅图告诉你:


image.png


 JS中变量的定义在内存中包括三个部分:



  • 变量标示  (比如上图中的Str,变量标示存储在内存的栈区)

  • 变量值   (比如上面中的Str的值souvenir或者是obj1对象的指向堆区地址,这个值也是存储在栈区)

  • 对象   (比如上图中的对象1或者对象2,对象存储在堆区)


也就是说,对于基本数据类型来说,只使用了内存的栈区。
我们再做一个有趣的改动:


var a = {n:1};
var b=a;
a.x = a = {n:2};
console.log(a.x);
console.log(a);
console.log(b);
console.log(b.x);

可以看到我们并没有对 b 进行操作但是 b.x 等于{n:2},这是一个被操作过的值,就如上述可知 b的指针指向堆1,所以堆没有被回收,而被显示出来了 ~


从这么一个简单例子,你是否对JS存储机制有了新的认识呢 ~


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

收起阅读 »

android侧滑菜单SuperSlidingPaneLayout

SuperSlidingPaneLayout     SuperSlidingPaneLayout是在SlidingPaneLayout的基础之上扩展修改,新增几种不同的侧滑效果,基本用法与SlidingPan...
继续阅读 »


SuperSlidingPaneLayout

Download Jitpack API License Blog QQGroup

SuperSlidingPaneLayout是在SlidingPaneLayout的基础之上扩展修改,新增几种不同的侧滑效果,基本用法与SlidingPaneLayout一致。

Image

引入

Maven:

<dependency>
<groupId>com.king.view</groupId>
<artifactId>superslidingpanelayout</artifactId>
<version>1.1.0</version>
<type>pom</type>
</dependency>

Gradle:

compile 'com.king.view:superslidingpanelayout:1.1.0'

Lvy:

<dependency org='com.king.view' name='superslidingpanelayout' rev='1.1.0'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

使用布局示例:

<?xml version="1.0" encoding="utf-8"?>
<com.king.view.superslidingpanelayout.SuperSlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/superSlidingPaneLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/menu_bg1"
app:mode="default_"
app:compat_sliding="false">
<include layout="@layout/menu_layout"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/top_title_bar"/>
<TextView
android:id="@+id/tvMode"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:gravity="center"
android:text="Default"
android:textSize="24sp"/>
</LinearLayout>

</com.king.view.superslidingpanelayout.SuperSlidingPaneLayout>

代码设置侧滑模式效果:

        superSlidingPaneLayout.setMode(SuperSlidingPaneLayout.Mode.DEFAULT);

superSlidingPaneLayout.setMode(SuperSlidingPaneLayout.Mode.TRANSLATION);

superSlidingPaneLayout.setMode(SuperSlidingPaneLayout.Mode.SCALE_MENU);

superSlidingPaneLayout.setMode(SuperSlidingPaneLayout.Mode.SCALE_PANEL);

superSlidingPaneLayout.setMode(SuperSlidingPaneLayout.Mode.SCALE_BOTH);

更多使用详情请查看demo示例。

相关博文:http://blog.csdn.net/jenly121/article/details/52757409

代码下载:SuperSlidingPaneLayout.zip

收起阅读 »

CounterView for Android 一个数字变化效果的计数器视图控件。

CounterViewCounterView for Android 一个数字变化效果的计数器视图控件。Gif 展示引入Maven:<dependency> <groupId>com.king.view</groupId>...
继续阅读 »


CounterView

CounterView for Android 一个数字变化效果的计数器视图控件。

Gif 展示

Image

引入

Maven:

<dependency>
<groupId>com.king.view</groupId>
<artifactId>counterview</artifactId>
<version>1.1.0</version>
<type>pom</type>
</dependency>

Gradle:

compile 'com.king.view:counterview:1.1.0'

Lvy:

<dependency org='com.king.view' name='counterview' rev='1.1.0'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

核心代码

counterView.showAnimation(10000);

代码下载:CounterView.zip

收起阅读 »

NeverCrash for Android 一个全局捕获Crash的库。信NeverCrash,永不Crash。

NeverCrashNeverCrash for Android 一个全局捕获Crash的库。信NeverCrash,永不Crash。Gif 展示引入Maven: com.king.thread nevercrash 1.0.0 pom Gra...
继续阅读 »

NeverCrash

NeverCrash for Android 一个全局捕获Crash的库。信NeverCrash,永不Crash。

Gif 展示

Image

引入

Maven:


com.king.thread
nevercrash
1.0.0
pom

Gradle:

compile 'com.king.thread:nevercrash:1.0.0'

Lvy:



如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)

allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

核心代码(大道至简)

NeverCrash.init(CrashHandler);

代码示例

public class App extends Application {

@Override
public void onCreate() {
super.onCreate();
NeverCrash.init(new NeverCrash.CrashHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
Log.d("Jenly", Log.getStackTraceString(e));
// e.printStackTrace();
showToast(e.getMessage());


}
});
}

private void showToast(final String text){

new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(),text,Toast.LENGTH_SHORT).show();
}
});
}

}

代码下载:NeverCrash.zip

收起阅读 »

SlideBar for Android 一个很好用的联系人快速索引。

SlideBarSlideBar for Android 一个很好用的联系人快速索引。Gif 展示引入Maven:<dependency> <groupId>com.king.view</groupId> <a...
继续阅读 »

SlideBar

SlideBar for Android 一个很好用的联系人快速索引。

Gif 展示


引入

Maven:

<dependency>
<groupId>com.king.view</groupId>
<artifactId>slidebar</artifactId>
<version>1.1.0</version>
<type>pom</type>
</dependency>

Gradle:

compile 'com.king.view:slidebar:1.1.0'

Lvy:

<dependency org='com.king.view' name='slidebar' rev='1.1.0'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

具体实现详情请戳传送门

代码下载:SlideBar.zip

收起阅读 »

Android码表变化的旋转计数器动画控件

SpinCounterViewSpinCounterView for Android 一个类似码表变化的旋转计数器动画控件。Gif 展示引入Maven:<dependency> <groupId>com.king.view</...
继续阅读 »

SpinCounterView

SpinCounterView for Android 一个类似码表变化的旋转计数器动画控件。

Gif 展示

Image

引入

Maven:

<dependency>
<groupId>com.king.view</groupId>
<artifactId>spincounterview</artifactId>
<version>1.1.1</version>
<type>pom</type>
</dependency>

Gradle:

compile 'com.king.view:spincounterview:1.1.1'

Lvy:

<dependency org='com.king.view' name='spincounterview' rev='1.1.1'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>

如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)

allprojects {
repositories {
//...
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

布局

    <com.king.view.spincounterview.SpinCounterView
android:id="@+id/scv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:max="100"
app:maxValue="1000"/>

核心动画代码

spinCounterView.showAnimation(80);

代码下载:SpinCounterView.zip

收起阅读 »

Objective-C 消息转发深度理解(2)

4.1.3 forwarding_prep_0伪代码分析Hopper分析完毕后直接搜索forwarding_prep_0查看反汇编伪代码:int ___forwarding_prep_0___(int arg0, int arg1, int arg2, int...
继续阅读 »


4.1.3 forwarding_prep_0伪代码分析

Hopper分析完毕后直接搜索forwarding_prep_0查看反汇编伪代码:

int ___forwarding_prep_0___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
//……
rax = ____forwarding___(&stack[0], 0x0);
if (rax != 0x0) {
rax = *rax;
}
else {
//arg0,arg1
rax = objc_msgSend(stack[0], stack[8]);
}
return rax;
}
  • 可以看到内部是对___forwarding___的调用。
  • ____forwarding___返回值不存在的时候调用的是objc_msgSend参数是arg0
    arg1

4.1.4 __forwarding__伪代码分析


点击进去查看___forwarding___的实现:


int ____forwarding___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
r9 = arg5;
r8 = arg4;
rcx = arg3;
r13 = arg1;
r15 = arg0;
rax = COND_BYTE_SET(NE);
if (arg1 != 0x0) {
r12 = *_objc_msgSend_stret;
}
else {
r12 = *_objc_msgSend;
}
rbx = *(r15 + rax * 0x8);
rsi = *(r15 + rax * 0x8 + 0x8);
var_140 = rax * 0x8;
if (rbx >= 0x0) goto loc_115af7;

loc_115ac0:
//target pointer处理
rax = *_objc_debug_taggedpointer_obfuscator;
rax = *rax;
rcx = (rax ^ rbx) >> 0x3c & 0x7;
rax = ((rax ^ rbx) >> 0x34 & 0xff) + 0x8;
if (rcx != 0x7) {
rax = rcx;
}
if (rax == 0x0) goto loc_115ea6;

loc_115af7:
var_150 = r12;
var_138 = rsi;
var_148 = r15;
rax = object_getClass(rbx);
r15 = rax;
r12 = class_getName(rax);
//是否能响应 forwardingTargetForSelector,不能响应跳转 loc_115bab 否则继续执行 也就是forwardingTargetForSelector方法返回nil或者自身
if (class_respondsToSelector(r15, @selector(forwardingTargetForSelector:)) == 0x0) goto loc_115bab;

loc_115b38:
//rax返回值
rax = [rbx forwardingTargetForSelector:var_138];
//返回值是否存在,返回值是否等于自己 是则跳转 loc_115bab
if ((rax == 0x0) || (rax == rbx)) goto loc_115bab;

loc_115b55:
if (rax >= 0x0) goto loc_115b91;

loc_115b5a:
rcx = *_objc_debug_taggedpointer_obfuscator;
rcx = *rcx;
rdx = (rcx ^ rax) >> 0x3c & 0x7;
rcx = ((rcx ^ rax) >> 0x34 & 0xff) + 0x8;
if (rdx != 0x7) {
rcx = rdx;
}
if (rcx == 0x0) goto loc_115e95;

loc_115b91:
*(var_148 + var_140) = rax;
r15 = 0x0;
goto loc_115ef1;

loc_115ef1:
if (**___stack_chk_guard == **___stack_chk_guard) {
rax = r15;
}
else {
rax = __stack_chk_fail();
}
//返回 forwardingTargetForSelector 为消息的接收者
return rax;

loc_115e95:
rbx = rax;
r15 = var_148;
r12 = var_150;
goto loc_115ea6;

loc_115ea6:
if (dyld_program_sdk_at_least(0x7e30901ffffffff) != 0x0) goto loc_116040;

loc_115ebd:
r14 = _getAtomTarget(rbx);
*(r15 + var_140) = r14;
___invoking___(r12, r15, r15, 0x400, 0x0, r9, var_150, var_148, var_140, var_138, var_130, stack[-304], stack[-296], stack[-288], stack[-280], stack[-272], stack[-264], stack[-256], stack[-248], stack[-240]);
if (*r15 == r14) {
*r15 = rbx;
}
goto loc_115ef1;

loc_116040:
____forwarding___.cold.1();
rax = objc_opt_class(@class(NSInvocation));
*____forwarding___.invClass = rax;
rax = class_getInstanceSize(rax);
*____forwarding___.invClassSize = rax;
return rax;

loc_115bab:
var_140 = rbx;
//是否僵尸对象
if (strncmp(r12, "_NSZombie_", 0xa) == 0x0) goto loc_115f30;

loc_115bce:
r14 = var_140;
//是否能够响应 methodSignatureForSelector
if (class_respondsToSelector(r15, @selector(methodSignatureForSelector:)) == 0x0) goto loc_115f46;

loc_115bef:
rbx = var_138;
//调用
rax = [r14 methodSignatureForSelector:rbx];
if (rax == 0x0) goto loc_115fc1;

loc_115c0e:
r15 = rax;
rax = [rax _frameDescriptor];
r12 = rax;
if (((*(int16_t *)(*rax + 0x22) & 0xffff) >> 0x6 & 0x1) != r13) {
rax = sel_getName(rbx);
rcx = "";
if ((*(int16_t *)(*r12 + 0x22) & 0xffff & 0x40) == 0x0) {
rcx = " not";
}
r8 = "";
if (r13 == 0x0) {
r8 = " not";
}
_CFLog(0x4, @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.", rax, rcx, r8, r9, var_150);
}
//是否能够响应_forwardStackInvocation
if (class_respondsToSelector(object_getClass(r14), @selector(_forwardStackInvocation:)) == 0x0) goto loc_115d61;

loc_115c9a:
if (*____forwarding___.onceToken != 0xffffffffffffffff) {
dispatch_once(____forwarding___.onceToken, ^ {/* block implemented at ______forwarding____block_invoke */ } });
}
[NSInvocation requiredStackSizeForSignature:r15];
var_138 = r15;
rdx = *____forwarding___.invClassSize;
r13 = &var_150 - (rdx + 0xf & 0xfffffffffffffff0);
memset(r13, 0x0, rdx);
objc_constructInstance(*____forwarding___.invClass, r13);
var_150 = rax;
r15 = var_138;
[r13 _initWithMethodSignature:var_138 frame:var_148 buffer:&stack[-8] - (0xf + rax & 0xfffffffffffffff0) size:rax];
[var_140 _forwardStackInvocation:r13];
rbx = 0x1;
goto loc_115dce;

loc_115dce:
if (*(int8_t *)(r13 + 0x34) != 0x0) {
rax = *r12;
if (*(int8_t *)(rax + 0x22) < 0x0) {
rcx = *(int32_t *)(rax + 0x1c);
rdx = *(int8_t *)(rax + 0x20) & 0xff;
memmove(*(rdx + var_148 + rcx), *(rdx + rcx + *(r13 + 0x8)), *(int32_t *)(*rax + 0x10));
}
}
rax = [r15 methodReturnType];
r14 = rax;
rax = *(int8_t *)rax;
if ((rax != 0x76) && (((rax != 0x56) || (*(int8_t *)(r14 + 0x1) != 0x76)))) {
r15 = *(r13 + 0x10);
if (rbx != 0x0) {
r15 = [[NSData dataWithBytes:r15 length:var_150] bytes];
[r13 release];
rax = *(int8_t *)r14;
}
if (rax == 0x44) {
asm { fld tword [r15] };
}
}
else {
r15 = ____forwarding___.placeholder;
if (rbx != 0x0) {
r15 = ____forwarding___.placeholder;
[r13 release];
}
}
goto loc_115ef1;

loc_115d61:
var_138 = r12;
r12 = r14;
//forwardInvocation的判断,如果没有实现直接跳转loc_115f8e
if (class_respondsToSelector(object_getClass(r14), @selector(forwardInvocation:)) == 0x0) goto loc_115f8e;

loc_115d8d:
rax = [NSInvocation _invocationWithMethodSignature:r15 frame:var_148];
r13 = rax;
[r12 forwardInvocation:rax];
var_150 = 0x0;
rbx = 0x0;
r12 = var_138;
goto loc_115dce;

loc_115f8e:
//错误日志
r14 = @selector(forwardInvocation:);
____forwarding___.cold.4(&var_130, r12);
rcx = r14;
_CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead", var_140, rcx, r8, r9, var_150);
goto loc_115fba;

loc_115fba:
rbx = var_138;
goto loc_115fc1;

loc_115fc1:
rax = sel_getName(rbx);
r14 = rax;
rax = sel_getUid(rax);
if (rax != rbx) {
rcx = r14;
r8 = rax;
_CFLog(0x4, @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort", var_138, rcx, r8, r9, var_150);
}
if (class_respondsToSelector(object_getClass(var_140), @selector(doesNotRecognizeSelector:)) == 0x0) goto loc_116034;

loc_11601b:
[var_140 doesNotRecognizeSelector:rdx];
asm { ud2 };
rax = loc_116034(rdi, rsi, rdx, rcx, r8, r9);
return rax;

loc_116034:
____forwarding___.cold.3(var_140);
goto loc_116040;

loc_115f46:
rbx = class_getSuperclass(r15);
r14 = object_getClassName(r14);
if (rbx == 0x0) {
rax = object_getClassName(var_140);
rcx = r14;
r8 = rax;
_CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- did you forget to declare the superclass of '%s'?", var_140, rcx, r8, r9, var_150);
}
else {
rcx = r14;
_CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead", var_140, rcx, r8, r9, var_150);
}
goto loc_115fba;

loc_115f30:
r14 = @selector(forwardingTargetForSelector:);
____forwarding___.cold.2(var_140, r12, var_138, rcx, r8);
goto loc_115f46;
}

可以看到汇编伪代码的调用流程与看到的API调用流程差不多。


4.1.5 __forwarding__伪代码还原


还原主要逻辑伪代码如下:


#include <stdio.h>

@interface NSInvocation(additions)

+ (unsigned long long)requiredStackSizeForSignature:(NSMethodSignature *)signature;

-(id)_initWithMethodSignature:(id)arg1 frame:(void*)arg2 buffer:(void*)arg3 size:(unsigned long long)arg4;

+(id)_invocationWithMethodSignature:(id)arg1 frame:(void*)arg2;

@end


@interface NSObject(additions)

- (void)_forwardStackInvocation:(NSInvocation *)invocation;

@end


void forwardingTargetForSelector(Class cls, SEL sel, const char * className, id obj);
void methodSignatureForSelector(Class cls, id obj, SEL sel);
void doesNotRecognizeSelector(id obj, SEL sel);
void _forwardStackInvocation(id obj,NSMethodSignature *signature);
void forwardInvocation(id obj,NSMethodSignature *signature);

int ____forwarding___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
SEL sel = NULL;
id obj;
Class cls = object_getClass(obj);
const char * className = class_getName(cls);
forwardingTargetForSelector(cls,sel,className,obj);
return 0;
}

void forwardingTargetForSelector(Class cls, SEL sel, const char * className, id obj) {
//是否能响应 forwardingTargetForSelector,不能响应跳转 loc_115bab 否则继续执行 也就是forwardingTargetForSelector方法返回nil或者自身
if (class_respondsToSelector(cls, @selector(forwardingTargetForSelector:))) {
id obj = [cls forwardingTargetForSelector:sel];
if ((obj == nil) || (obj == cls)) {
methodSignatureForSelector(cls,obj,sel);
} else if (obj >= 0x0) {
//返回 forwardingTargetForSelector 备用消息接收者
// return obj;
} else {
//taggedpointer 处理
//返回NSInvocation size数据
}
} else {
//是否僵尸对象
if (strncmp(className, "_NSZombie_", 0xa)) {
methodSignatureForSelector(cls,obj,sel);
} else {
SEL currentSel = @selector(forwardingTargetForSelector:);
doesNotRecognizeSelector(obj,currentSel);
}
}
}


void methodSignatureForSelector(Class cls, id obj, SEL sel) {
if (class_respondsToSelector(cls, @selector(methodSignatureForSelector:))) {
NSMethodSignature *signature = [obj methodSignatureForSelector:sel];
if (signature) {
_forwardStackInvocation(obj,signature);
} else {
doesNotRecognizeSelector(obj,sel);
}
} else {
doesNotRecognizeSelector(obj,sel);
}
}

void _forwardStackInvocation(id obj,NSMethodSignature *signature) {
//是否能够响应_forwardStackInvocation
if (class_respondsToSelector(object_getClass(obj), @selector(_forwardStackInvocation:))) {
//执行dispatch_once相关逻辑
[NSInvocation requiredStackSizeForSignature:signature];
void *bytes;
// objc_constructInstance([NSInvocation class], bytes);
NSInvocation *invocation = [invocation _initWithMethodSignature:signature frame:NULL buffer:NULL size:bytes];
[obj _forwardStackInvocation:invocation];
const char * type = [signature methodReturnType];
//返回signature
} else {
forwardInvocation(obj,signature);
}
}

void forwardInvocation(id obj,NSMethodSignature *signature) {
//forwardInvocation的判断,如果没有实现直接跳转loc_115f8e
if (class_respondsToSelector(object_getClass(obj), @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:signature frame:NULL];
[obj forwardInvocation:invocation];
const char * type = [signature methodReturnType];
//返回signature
} else {
SEL sel = @selector(forwardInvocation:);
doesNotRecognizeSelector(obj,sel);
}
}

void doesNotRecognizeSelector(id obj, SEL sel) {
if (class_respondsToSelector(object_getClass(obj), @selector(doesNotRecognizeSelector:))) {
[obj doesNotRecognizeSelector:sel];
/*
____forwarding___.cold.1();
rax = objc_opt_class(@class(NSInvocation));
*____forwarding___.invClass = rax;
rax = class_getInstanceSize(rax);
*____forwarding___.invClassSize = rax;
return rax;
*/

} else {
/*
____forwarding___.cold.1();
rax = objc_opt_class(@class(NSInvocation));
*____forwarding___.invClass = rax;
rax = class_getInstanceSize(rax);
*____forwarding___.invClassSize = rax;
return rax;
*/

}
}
为了方便分析我这里class-dumpCoreFoundation头文件。手机端使用cycript进入SpringBoard应用,然后classdumpdyld导出CoreFoudation的头文件,最后拷贝到电脑端,具体操作如下:

cycript -p SpringBoard
@import net.limneos.classdumpdyld;
classdumpdyld.dumpBundle([NSBundle > bundleWithIdentifier:@"com.apple.CoreFoudation"]);
//输出导出头文件路径
@"Wrote all headers to /tmp/CoreFoundation"
//拷贝到电脑的相应目录
scp -r -P 12345 root@localhost:/tmp/CoreFoundation/ ./CoreFoundation_Headers/

伪代码流程图如下



反汇编流程与根据API分析的流程差不多。

  • forwardingTargetForSelector快速转发会对返回值会进行判断,如果是返回的自身或者nil直接进入下一流程(慢速转发)。
  • 如果返回taggedpointer有单独的处理。
  • methodSignatureForSelector慢速转发会先判断有没有实现_forwardStackInvocation(私有方法)。实现_forwardStackInvocation后不会再进入forwardInvocation流程,相当于_forwardStackInvocation是一个私有的前置条件。
  • methodSignatureForSelector如果没有返回签名信息不会继续进行下面的流程。
  • forwardInvocation没有实现就直接走到doesNotRecognizeSelector流程了。

4.2 流程分析


上篇文章分析resolveInstanceMethod在消息转发后还会调用一次resolveInstanceMethod(在日志文件中看到是在doesNotRecognizeSelector之前,methodSignatureForSelector之后)。那么实现对应的方法做下验证:

HPObject resolveInstanceMethod: HPObject-0x100008290-instanceMethod
-[HPObject forwardingTargetForSelector:] - instanceMethod
-[HPObject methodSignatureForSelector:] - instanceMethod
HPObject resolveInstanceMethod: HPObject-0x100008290-instanceMethod
-[HPObject doesNotRecognizeSelector:] - instanceMethod

证实是在methodSignatureForSelector之后,doesNotRecognizeSelector之前有一次进行了方法动态决议。那么为什么要这么处理呢?因为消息转发的过程中可能已经加入了对应的sel-imp,所以再给一次机会进行方法动态决议。这次决议后不会再进行消息转发。

但是在反汇编分析中并没有明确的再次进行动态方法决议的逻辑。


4.2.1 反汇编以及源码探究

那么在第二次调用resolveInstanceMethod前打断点查看下堆栈信息
macOS堆栈如下:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 5.1
frame #0: 0x0000000100300f53 libobjc.A.dylib`resolveMethod_locked(inst=0x0000000000000000, sel="instanceMethod", cls=HPObject, behavior=0) at objc-runtime-new.mm:6339:13
frame #1: 0x00000001002ffbd5 libobjc.A.dylib`lookUpImpOrForward(inst=0x0000000000000000, sel="instanceMethod", cls=HPObject, behavior=0) at objc-runtime-new.mm:6601:16
frame #2: 0x00000001002d6df9 libobjc.A.dylib`class_getInstanceMethod(cls=HPObject, sel="instanceMethod") at objc-runtime-new.mm:6210:5
* frame #3: 0x00007fff2e33fc68 CoreFoundation`__methodDescriptionForSelector + 282
frame #4: 0x00007fff2e35b57c CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 38
frame #5: 0x0000000100003a21 HPObjcTest`-[HPObject methodSignatureForSelector:](self=0x0000000100706a30, _cmd="methodSignatureForSelector:", aSelector="instanceMethod") at HPObject.m:29:12 [opt]
frame #6: 0x00007fff2e327fc0 CoreFoundation`___forwarding___ + 408
frame #7: 0x00007fff2e327d98 CoreFoundation`__forwarding_prep_0___ + 120
frame #8: 0x0000000100003c79 HPObjcTest`main + 153
frame #9: 0x00007fff683fecc9 libdyld.dylib`start + 1
frame #10: 0x00007fff683fecc9 libdyld.dylib`start + 1
可以看到methodSignatureForSelector调用后进入了__methodDescriptionForSelector随后调用了class_getInstanceMethod。查看汇编确实在__methodDescriptionForSelector中调用了class_getInstanceMethod


那么系统是如何从methodSignatureForSelector调用到__methodDescriptionForSelector的?
当前的methodSignatureForSelector的实现是:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [super methodSignatureForSelector:aSelector];
}

如果改为返回nil呢?

HPObject resolveInstanceMethod: HPObject-0x100008288-instanceMethod
-[HPObject forwardingTargetForSelector:] - instanceMethod
-[HPObject methodSignatureForSelector:] - instanceMethod
-[HPObject doesNotRecognizeSelector:] - instanceMethod
这个时候发现没有第二次调用了,那也就是说核心逻辑在[super methodSignatureForSelector:aSelector]的实现中。
查看源码:

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("+[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
_objc_fatal("-[NSObject methodSignatureForSelector:] "
"not available without CoreFoundation");
}

注释说的已经很明显了实现在CoreFoundation中,直接搜索methodSignatureForSelector的反汇编实现:


/* @class NSObject */
-(void *)methodSignatureForSelector:(void *)arg2 {
rdx = arg2;
if ((rdx != 0x0) && (___methodDescriptionForSelector(objc_opt_class(), rdx) != 0x0)) {
rax = [NSMethodSignature signatureWithObjCTypes:rdx];
}
else {
rax = 0x0;
}
return rax;
}
  • sel不为nil的时候会调用___methodDescriptionForSelector。这样就串联起来了。

class_getInstanceMethod的实现如下:


Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
return _class_getMethod(cls, sel);
}

4.2.2 断点调试验证

既然上面已经清楚了resolveInstanceMethod第二次调用是methodSignatureForSelector之后调用的,那么不妨打个符号断点跟踪下methodSignatureForSelector:




显然只需要关心调用的函数以及跳转逻辑。

跟进去__methodDescriptionForSelector


这样通过断点也从methodSignatureForSelector定位到了resolveInstanceMethod

结论:

  • 实例方法 - methodSignatureForSelector-> ___methodDescriptionForSelector -> class_getInstanceMethod-> lookUpImpOrForward->resolveMethod_locked-> resolveInstanceMethod
  • 类方法 + methodSignatureForSelector -> ___methodDescriptionForSelector(传递的是元类) -> class_getInstanceMethod- lookUpImpOrForward->resolveMethod_locked-> resolveClassMethod

⚠️总结:

  1. 在methodSignatureForSelector内部调用了class_getInstanceMethod进行lookUpImpOrForward随后进入方法动态决议。这也就是class_getInstanceMethod调用第二次的来源入口。
  2. methodSignatureForSelector后第二次调用class_getInstanceMethod是为了再给一次进行消息查找和动态决议流程,因为消息转发流程过程中有可能实现了对应的sel-imp

动态方法决议以及消息转发整个流程如下:




五、消息发送查找总结

前面已经通过objc_msgSend分析整个消息缓存、查找、决议、转发整个流程。

  • 通过CacheLookup进行消息快速查找
    • 整个cache查找过程相当于是insert过程的逆过程,找到imp就解码跳转,否则进入慢速查找流程。
  • 通过lookUpImpOrForward进行消息慢速查找
    • 慢速查找涉及到递归查找,查找过程分为二分查找/循环查找。
    • 找到imp直接跳转,否则查找父类缓存。父类缓存依然找不到则在父类方法列表中查找,直到找到nil。查找到父类方法/缓存方法直接插入自己的缓存中。
  • imp找不到的时候进行方法动态决议
    • 当快速和慢速消息查找都没有找到imp的时候就进入了方法动态决议流程,在这个流程中主要是添加imp后再次进行快速慢速消息查找。
  • 之后进入本篇的消息转发流程,消息转发分为快速以及慢速。
    • 在动态方法决议没有返回imp的时候就进入到了消息转发阶段。
    • 快速消息转发提供一个备用消息接收者,返回值不能为nil与自身。这个过程不能修改参数和返回值。
    • 慢速消息转发需要提供消息签名,只要提供有效签名就可以解决消息发送错误问题。同时要实现forwardInvocation配合处理消息。
    • forwardInvocation配合处理消息,使target生效起作用。
    • 在慢速消息转发后系统会再进行一次慢速消息查找流程。这次不会再进行消息转发。
    • 消息转发仍然没有解决问题会进入doesNotRecognizeSelector,这个方法并不能处理错误,实现它仍然会报错。只是能拿到错误信息而已。

⚠️慢速消息转发后系统仍然给了一次机会进行 慢速消息查找!!!(并不仅仅是动态方法决议)。

整个流程如下:







作者:HotPotCat
链接:https://www.jianshu.com/p/f5bf0549b1f5







收起阅读 »

iOS Hook原理 - 反hook& MonkeyDev

一、 反 hook 初探我们Hook别人的代码一般使用OC的MethodSwizzle,如果我们用fishhook将MethodSwizzle hook了,别人是不是就hook不了我们的代码了?1.1 创建主工程 AntiHookDemo创建一个工程AntiH...
继续阅读 »

一、 反 hook 初探

我们Hook别人的代码一般使用OCMethodSwizzle,如果我们用
fishhookMethodSwizzle hook了,别人是不是就hook不了我们的代码了?

1.1 创建主工程 AntiHookDemo

创建一个工程AntiHookDemo,页面中有两个按钮btn1btn2:



对应两个事件:

- (IBAction)btn1Click:(id)sender {
NSLog(@"click btn1");
}

- (IBAction)btn2Click:(id)sender {
NSLog(@"click btn2");
}

1.2 创建防护 HookManager (FrameWork 动态库)

这个时候要使用fishhook防护,在FrameWork中写防护代码。基于两点:

  1. Framework在主工程+ load执行之前执行+ load
  2. 别人注入的Framework也在防护代码之后。

创建一个HookManager Framework,文件结构下:




AntiHookManager.h

#import <Foundation/Foundation.h>
#import <objc/message.h>

//暴露给外界使用
CF_EXPORT void (*exchange_p)(Method _Nonnull m1, Method _Nonnull m2);

@interface AntiHookManager : NSObject

@end

AntiHookManager.m:

#import "AntiHookManager.h"
#import "fishhook.h"

@implementation AntiHookManager

+ (void)load {
//基本防护
struct rebinding exchange;
exchange.name = "method_exchangeImplementations";
exchange.replacement = hp_exchange;
exchange.replaced = (void *)&exchange_p;
struct rebinding bds[] = {exchange};
rebind_symbols(bds, 1);
}


//指回原方法
void (*exchange_p)(Method _Nonnull m1, Method _Nonnull m2);

void hp_exchange(Method _Nonnull m1, Method _Nonnull m2) {
//可以在这里进行上报后端等操作
NSLog(@"find Hook");
}

@end

HookManager.h中导出头文件:

#import <HookManager/AntiHookManager.h>

然后将AntiHookManager.h放入public Headers

修改主工程的ViewController.m如下:


#import <HookManager/HookManager.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
exchange_p(class_getInstanceMethod(self.class, @selector(btn2Click:)),class_getInstanceMethod(self.class, @selector(test)));
}

- (void)test {
NSLog(@"self Hook Success");
}

- (IBAction)btn1Click:(id)sender {
NSLog(@"click btn1");
}

- (IBAction)btn2Click:(id)sender {
NSLog(@"click btn2");
}

@end

在工程中Hook自己的方法,这个时候运行主工程:


AntiHookDemo[1432:149145] click btn1
AntiHookDemo[1432:149145] self Hook Success

btn2能够被自己正常Hook


1.3 创建注入工程 HookDemo

  1. 在根目录创建APP文件夹以及Payload文件夹,拷贝AntiHookDemo.appAPP/Payload目录,压缩zip -ry AntiHookDemo.ipa Payload/生成.ipa文件
  2. 拷贝appResign.sh重签名脚本以及yololib注入工具到根目录。
  3. 创建HPHook注入Framework

HPHook代码如下:


#import "HPInject.h"
#import <objc/message.h>

@implementation HPInject

+ (void)load {
method_exchangeImplementations(class_getInstanceMethod(objc_getClass("ViewController"), @selector(btn1Click:)), class_getInstanceMethod(self, @selector(my_click)));
}

- (void)my_click {
NSLog(@"inject Success");
}

@end

编译运行:

AntiHookDemo[1437:149999] find  Hook
AntiHookDemo[1437:149999] click btn1
AntiHookDemo[1437:149999] self Hook Success

首先是检测到了Hook,其次自己内部btn2 hook成功了,btn1 hook没有注入成功。到这里暴露给自己用和防止别人Hook都已经成功了。对于三方库中正常使用到的Hook可以在防护代码中做逻辑判断可以加白名单等调用回原来的方法。如果自己的库在image list最后一个那么三方库其实已经Hook完了。

当然只Hook method_exchangeImplementations不能完全防护,还需要Hook class_replaceMethod以及method_setImplementation

这种防护方式破解很容易,一般不这么处理:
1.在Hopper中可以找到method_exchangeImplementations,直接在MachO中修改这个字符串HookManager中就Hook不到了(这里会直接crash,因为viewDidLoad中调用了exchange_p,对于有保护逻辑的就可以绕过了,并且method_exchangeImplementations没法做混淆)


2.可以很容易定位到防护代码,直接在防护代码之前Hook,或者将fishhook中的一些系统函数Hook也能破解。本质上是不执行防护代码。


二、MonkeyDev

MonkeyDev是逆向开发中一个常用的工具 MonkeyDev。能够帮助我们进行重签名和代码注入。


2.1 安装 MonkeyDev

theos安装(Cydia Substrate就是 theos中的工具)

sudo git clone --recursive https://github.com/theos/theos.git /opt/theos

配置环境变量

#逆向相关配置
#export THEOS=/opt/theos

#写入环境变量
#export PATH=$THEOS/bin:$PATH

运行nic.pl查看theos信息。



[error] Cowardly refusing to make a project inside $THEOS (/opt/theos/)出现这个错误则是export配置有问题。

指定Xcode

sudo xcode-select -s /Applications/Xcode.app

安装命令

这里是安装Xcode插件。安装完成后重启XcodeXcode中会出现MonkeyDev对应的功能:



  • MonkeyApp:自动给第三方应用集成RevealCycript和注入dylib的模块,支持调试dylib和第三方应用,支持Pod给第三放应用集成SDK,只需要准备一个砸壳后的ipa或者app文件即可。
  • MonkeyPod:提供了创建Pod的项目。
  • CaptainHook Tweak:使用CaptainHook提供的头文件进行OC函数的Hook以及属性的获取。
  • Command-line Tool:可以直接创建运行于越狱设备的命令行工具。
  • Logos Tweak:使用theos提供的logify.pl工具将.xm文件转成.mm文件进行编译,集成了CydiaSubstrate,可以使用MSHookMessageExMSHookFunctionHook OC函数和指定地址。


错误处理
1.MonkeyDev 安装出现:Types.xcspec not found
添加一个软连接:
sudo ln -s /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Xcode/PrivatePlugIns/IDEOSXSupportCore.ideplugin/Contents/Resources /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Xcode/Specifications

2.2 重签名

创建一个MonkeyDemo工程:


工程目录如下:



在工程目录下有一个TargetApp目录,直接将微信8.0.2版本拖进去:


编译运行工程:

这个时候就重签名成功了。相比用脚本自己跑方便很多,也能避免很多异常。

2.3 MonkeyDev 代码注入



工程配置

MonkeyDemo注入一下AntiHookDemo,将AntiHookDemo编译生成的App加入MonkeyDemoTargetApp中:


代码注入

MonkeyDemo工程MonkeyDemoDylib->Logos目录,.xm文件可以写OCC++C




MonkeyDemoDylib.xmtype改为Objective-C++ Preprocessed Source

这里面的默认代码就是Logos语法:




.xm默认打开方式修改为Xcode后重启Xcode就能识别代码了,否则就还是默认文本文件。将默认的代码删除,写Hook btn1Click的代码:

#import <UIKit/UIKit.h>

//要hook的类
%hook ViewController

//要hook的方法
- (void)btn1Click:(id)sender {
NSLog(@"Monkey Hook Success");
//调用原来的方法
%orig;
}

%end

直接运行工程后点击btn1

AntiHookDemo[9306:5972601] find  Hook
AntiHookDemo[9306:5972601] find Hook
AntiHookDemo[9309:5973617] Monkey Hook Success
AntiHookDemo[9350:5987306] click btn1




这个时候就Hook成功了,并且检测到了Hook。这里没有防护住是因为Monkey中用的是getImpsetImp
AntiHookManager做下改进:
AntiHookManager .h:

#import <Foundation/Foundation.h>
#import <objc/message.h>

//暴露给外界使用
CF_EXPORT void (*exchange_p)(Method _Nonnull m1, Method _Nonnull m2);

CF_EXPORT IMP _Nonnull (*getImp_p)(Method _Nonnull m);

CF_EXPORT IMP _Nonnull(*setImp_p)(Method _Nonnull m, IMP _Nonnull imp);

@interface AntiHookManager : NSObject

@end

AntiHookManager .m:

#import "AntiHookManager.h"
#import "fishhook.h"

@implementation AntiHookManager

+ (void)load {
//基本防护
struct rebinding exchange;
exchange.name = "method_exchangeImplementations";
exchange.replacement = hp_exchange;
exchange.replaced = (void *)&exchange_p;

struct rebinding setIMP;
setIMP.name = "method_setImplementation";
setIMP.replacement = hp_setImp;
setIMP.replaced = (void *)&setImp_p;


struct rebinding getIMP;
getIMP.name = "method_getImplementation";
getIMP.replacement = hp_getImp;
getIMP.replaced = (void *)&getImp_p;

struct rebinding bds[] = {exchange,setIMP,getIMP};
rebind_symbols(bds, 3);
}


//指回原方法
void (*exchange_p)(Method _Nonnull m1, Method _Nonnull m2);

IMP _Nonnull (*getImp_p)(Method _Nonnull m);

IMP _Nonnull(*setImp_p)(Method _Nonnull m, IMP _Nonnull imp);

void hp_exchange(Method _Nonnull m1, Method _Nonnull m2) {
//可以在这里进行上报后端等操作
NSLog(@"find Hook");
}

void (hp_getImp)(Method _Nonnull m) {
NSLog(@"find Hook getImp");
}

void (hp_setImp)(Method _Nonnull m, IMP _Nonnull imp) {
NSLog(@"find Hook setImp");
}

@end

这个时候控制台输出:


AntiHookDemo[1488:207119] find  Hook getImp
AntiHookDemo[1488:207119] find Hook
AntiHookDemo[1488:207119] find Hook getImp
AntiHookDemo[1488:207119] find Hook
AntiHookDemo[1488:207119] click btn1

点击btn1也没有Hook到了。在这里运行时有可能CrashJSEvaluateScript的时候,直接删除App重新跑一次就可以了。
libsubstrate.dylib解析的,
其实这里.xm文件是被libsubstrate.dylib解析成MonkeyDemoDylib.mm中的内容(.xm代码是不参与编译的):



MSHookMessageEx底层用的是setImpgetImpOC进行Hook的。

错误问题
1.Signing for "MonkeyDemoDylib" requires a development team. Select a development team in the Signing & Capabilities editor.

直接在该targetbuild settings 中添加CODE_SIGNING_ALLOWED=NO





2.Failed to locate Logos Processor. Is Theos installed? If not, see https://github.com/theos/theos/wiki/Inst allation.
出现这个错误一般是theos没有安装好。或者路径配置的有问题。

3.library not found for -libstdc++
需要下载对应的库到XCode目录中。参考:https://github.com/longyoung/libstdc.6.0.9-if-help-you-give-a-star

4.The WatchKit app’s Info.plist must have a WKCompanionAppBundleIdentifier key set to the bundle identifier of the companion app.
删除DerivedData重新运行。

5.This application or a bundle it contains has the same bundle identifier as this application or another bundle that it contains. Bundle identifiers must be unique.
这种情况大概率是手机上之前安装过相同bundleIdApp安装不同版本导致,需要删除重新安装。还有问题的话删除DerivedDatabundleId

6.This app contains a WatchKit app with one or more Siri Intents app extensions that declare IntentsSupported that are not declared in any of the companion app's Siri Intents app extensions. WatchKit Siri Intents extensions' IntentsSupported values must be a subset of the companion app's Siri Intents extensions' IntentsSupported values.
需要删除com.apple.WatchPlaceholder(在/opt/MonkeyDev/Tools目录中修改pack.sh):


rm -rf "${TARGET_APP_PATH}/com.apple.WatchPlaceholder" || true

然后删除DerivedData重新运行。

  1. LLVM Profile Error: Failed to write file "default.profraw": Operation not permitted
    这个说明App内部做了反调试防护。直接在Monkey中开启sysctl
rebind_symbols((struct rebinding[1]){{"sysctl", my_sysctl, (void*)&orig_sysctl}},1);
8.Attempted to load Reveal Library twice. Are you trying to load dynamic library with Reveal Framework already linked?
直接删除dylibOther Linker Flags的设置即可(可能的原因是手机端已经导入了这个库):



⚠️遇见莫名其妙的错误建议删除DerivedData重启Xcode重新运行。


总结

  • Hook
    • 使用fishhook Hookmethod_exchangeImplementationsclass_replaceMethodmethod_setImplementation
    • 需要在动态库中添加防护代码。
    • 本地导出原函数IMP供自己项目使用,配合白名单。
    • 这种防护很容易破解,一般不推荐这么使用。
  • MonkeyDev:逆向开发中一个常用的工具。
    • 重签名:很容易,直接拖进去.ipa或者.app运行工程就可以了。
    • 代码注入:Logos主要是编写.xm文件。底层依然是getImpsetImp的调用。



作者:HotPotCat
链接:https://www.jianshu.com/p/a68890a8fdb2

收起阅读 »

iOS逆向 - fishhook

一、Hook概述HOOK中文译为挂钩或钩子。在iOS逆向中是指改变程序运行流程的一种技术。通过hook可以让别人的程序执行自己所写的代码。在逆向中经常使用这种技术。只有了解其原理才能够对恶意代码进行有效的防护。比如很久之前的微信自动抢红包插件:1.1Hook的...
继续阅读 »

一、Hook概述

HOOK中文译为挂钩钩子。在iOS逆向中是指改变程序运行流程的一种技术。通过hook可以让别人的程序执行自己所写的代码。在逆向中经常使用这种技术。只有了解其原理才能够对恶意代码进行有效的防护。

比如很久之前的微信自动抢红包插件:


1.1Hook的几种方式

iOSHOOK技术的大致上分为5种:Method SwizzlefishhookCydia Substratelibffiinlinehook

1.1.1 Method Swizzle (OC)

利用OCRuntime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法。

可以将SEL 和 IMP 之间的关系理解为一本书的目录SEL 就像标题IMP就像页码。他们是一一对应的关系。(书的目录不一定一一对应,可能页码相同,理解就行。)。

Runtime提供了交换两个SELIMP对应关系的函数:

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

通过这个函数交换两个SELIMP对应关系的技术,称之为Method Swizzle(方法欺骗)


runtime中有3种方式实现方法交换:

  • method_exchangeImplementations:在分类中直接交换就可以了,如果不在分类需要配合class_addMethod实现原方法的回调。
  • class_replaceMethod:直接替换原方法。
  • method_setImplementation:重新赋值原方法,通过getImpsetImp配合。

1.1.2 fishhook (外部函数)

Facebook提供的一个动态修改链接mach-O文件的工具。利用MachO文件加载原理,通过修改懒加载非懒加载两个表的指针达到C(系统C函数)函数HOOK的目的。fishhook

总结下来是:dyld 更新 Mach-O 二进制的 __DATA segment 的 __la_symbol_str 中的指针,使用 rebind_symbol方法更新两个符号位置来进行符号的重新绑定。


1.1.3 Cydia Substrate

Cydia Substrate 原名为 Mobile Substrate ,主要作用是针对OC方法C函数以及函数地址进行HOOK操作。并不仅仅针对iOS而设计,安卓一样可以用。Cydia Substrate官方

Cydia Substrate主要分为3部分:Mobile HookerMobileLoadersafe mode

Mobile Hooker

它定义了一系列的宏和函数,底层调用objcruntimefishhook来替换系统或者目标应用的函数。其中有两个函数:


void MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP result) 
MSHookFunction :(inline hook)主要作用于CC++函数 MSHookFunction。 Logos语法的%hook就是对这个函数做了一层封装。

void MSHookFunction(voidfunction,void* replacement,void** p_original)

MobileLoader

MobileLoader用于加载第三方dylib在运行的应用程序。启动时MobileLoader会根据规则把指定目录的第三方的动态库加载进去,第三方的动态库也就是我们写的破解程序。

safe mode

破解程序本质是dylib寄生在别人进程里。 系统进程一旦出错,可能导致整个进程崩溃,崩溃后就会造成iOS瘫痪。所以CydiaSubstrate引入了安全模式,在安全模式下所有基于CydiaSubstratede的三方dylib都会被禁用,便于查错与修复。

1.1.4 libffi

基于libbfi动态调用C函数。使用libffi中的ffi_closure_alloc构造与原方法参数一致的"函数" (stingerIMP),以替换原方法函数指针;此外,生成了原方法和Block的调用的参数模板cifblockCif。方法调用时,最终会调用到void _st_ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata), 在该函数内,可获取到方法调用的所有参数、返回值位置,主要通过ffi_call根据cif调用原方法实现和切面blockAOP库 StingerBlockHook就是使用libbfi做的。

1.1.5 inlinehook 内联钩子 (静态)

Inline Hook 就是在运行的流程中插入跳转指令来抢夺运行流程的一个方法。大体分为三步:

  • 将原函数的前 N 个字节搬运到 Hook 函数的前 N 个字节;
  • 然后将原函数的前 N 个字节填充跳转到 Hook 函数的跳转指令;
  • 在 Hook 函数末尾几个字节填充跳转回原函数 +N 的跳转指令;


MSHookFunction就是inline hook

基于 Dobby 的 Inline HookDobby 是通过插入 __zDATA 段和__zTEXT 段到 Mach-O 中。

  • __zDATA 用来记录 Hook 信息(Hook 数量、每个 Hook 方法的地址)、每个 Hook 方法的信息(函数地址、跳转指令地址、写 Hook 函数的接口地址)、每个 Hook 的接口(指针)。
  • __zText 用来记录每个 Hook 函数的跳转指令。

dobby
Dobby(原名:HOOKZz)是一个全平台的inlineHook框架,它用起来就和fishhook一样。
Dobby 通过 mmap 把整个 Mach-O 文件映射到用户的内存空间,写入完成保存本地。所以 Dobby 并不是在原 Mach-O上进行操作,而是重新生成并替换。



二 fishHook

2.1 fishhook的使用

fishhook源码.h文件中只提供了两个函数和一个结构体rebinding

rebind_symbols、rebind_symbols_image


FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);

FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel)
;

  • rebindings[]:存放rebinding结构体的数组(可以同时交换多个函数)。
  • rebindings_nelrebindings数组的长度。
  • slideASLR
  • headerimageHeader

只有两个函数重新绑定符号,两个函数的区别是一个指定image一个不指定。按照我们一般的理解放在前面的接口更常用,参数少的更简单。

rebinding

struct rebinding {
const char *name;//需要HOOK的函数名称,C字符串
void *replacement;//新函数的地址
void **replaced;//原始函数地址的指针!
};

  • name:要HOOK的函数名称,C字符串。
  • replacement:新函数的地址。(函数指针,也就是函数名称)。
  • replaced:原始函数地址的指针。(二级指针)。

2.1.1 Hook NSLog

现在有个需求,Hook系统的NSLog函数。
Hook代码:

- (void)hook_NSLog {
struct rebinding rebindNSLog;
rebindNSLog.name = "NSLog";
rebindNSLog.replacement = HP_NSLog;
rebindNSLog.replaced = (void *)&sys_NSLog;

struct rebinding rebinds[] = {rebindNSLog};

rebind_symbols(rebinds, 1);
}

//原函数,函数指针
static void (*sys_NSLog)(NSString *format, ...);

//新函数
void HP_NSLog(NSString *format, ...) {
format = [format stringByAppendingFormat:@"\n Hook"];
//调用系统NSLog
sys_NSLog(format);
}
调用:

    [self hook_NSLog];
NSLog(@"hook_NSLog");
输出:

hook_NSLog
Hook

这个时候就已经HookNSLog,走到了HP_NSLog中。
Hook代码调用完毕,sys_NSLog保存系统NSLog原地址,NSLog指向HP_NSLog

2.1.2 Hook 自定义 C 函数

Hook一下自己的C函数:

void func(const char * str) {
NSLog(@"%s",str);
}

- (void)hook_func {
struct rebinding rebindFunc;
rebindFunc.name = "func";
rebindFunc.replacement = HP_func;
rebindFunc.replaced = (void *)&original_func;

struct rebinding rebinds[] = {rebindFunc};

rebind_symbols(rebinds, 1);
}
//原函数,函数指针
static void (*original_func)(const char * str);

//新函数
void HP_func(const char * str) {
NSLog(@"Hook func");
original_func(str);
}
调用:

 [self hook_func];
func("HotPotCat");
输出:
HotPotCat

这个时候可以看到没有Hookfunc

结论:自定义的函数fishhook hook 不了,系统的可以hook

2.2 fishhook原理

fishHOOK可以HOOK C函数,但是我们知道函数是静态的,也就是说在编译的时候,编译器就知道了它的实现地址,这也是为什么C函数只写函数声明调用时会报错。那么为什么fishhook还能够改变C函数的调用呢?难道函数也有动态的特性存在?

是否意味着C Hook就必须修改调用地址?那意味着要修改二进制。(原理上使用汇编可以实现。fishhook不是这么处理的)

那么系统函数和本地函数区别到底在哪里?

2.2.1 符号 & 符号绑定 & 符号表 & 重绑定符号

NSLog函数的地址在编译的那一刻并不知道NSLog的真实地址。NSLogFoundation框架中。在运行时NSLog的地址在 共享缓存 中。在整个手机中只有dyld知道NSLog的真实地址。

LLVM编译器生成MachO文件时,如果让我们做就先空着系统函数的地址,等运行起来再替换。我们知道MachO中分为Text(只读)和Data(可读可写),那么显然这种方式行不通。

那么可行的方案是在Data段放一个 占位符(8字节)让代码编译的时候直接bl 占位符。在运行的时候dyld加载应用的时候将Data段的地址修改为NSLog真实地址,代码bl 占位符没有变 。这个技术就叫做 PIC(position independent code`)位置无关代码。(实际不是这么简单)

  • 占位符 就叫做 符号
  • dylddata段符号进行修改的这个过程叫做 符号绑定
  • 一个又一个的符号放在一起形成了一个列表,叫做 符号表

对于外部的C函数通过 符号 找 地址 也就给了我们机会动态的Hook外部C函数。OC是修改SELIMP对应的关系,符号 也是修改符号所对应的地址。这个动作叫做 重新绑定符号表。这也就是fishhook``hook的原理。

2.2.2验证

Hook NSLog前后分别调用NSLog:

    NSLog(@"before");
[self hook_NSLog];
NSLog(@"after");




MachO中我们能看到懒加载和非懒加载符号表,dyld绑定过程中绑定的是非懒加载符号和弱符号的。NSLog是懒加载符号,只有调用的时候才去绑定。

MachO中可以看到_NSLogData(值)是0000000100006960offset为:0x8010
在第一个NSLog处打个断点 运行查看:
主程序开始0x0000000100b24000ASLR0xb24000

0x0000000100b24000 + 0x8010中存储的内容为0x0100b2a960
0000000100006960 + 0xb24000 (ASLR) = 0x100B2A960
所以这里就对应上了。0x0100b2a960这个地址就是(符号表中的值其实是一个代码的地址,指向本地代码。)。



执行完第一个NSLog后(hook前):



符号表指向了HP_NSLog

这也就是fishhook能够Hook的真正原因(修改懒加载符号表)。

2.3 符号绑定过程(间接)

刚才在上面NSLog第一次执行之前我们拿到的地址0x0100b2a960实际上指向一段本地代码,加上ASLR后执行对应地址的代码然后就修改了懒加载符号表。

那么这个过程究竟是怎么做的呢?

先说明一些符号的情况:

  • 本地符号:只能本MachO用。
  • 全局符号:暴露给外面用。
  • 间接符号:当我们要调用外部函数/方法时,在编译时期地址是不知道的。比如系统的NSLog

间接符号专门有个符号表Indirect Symbols





比首地址大0x0000000100e0c000,所以这个地址在本MachO中。
0x100e12998 - 0x0000000100e0c000 = 0x6998

6998MachOSymbol Stubs中:





这个时候就对应上了:



这段代码的意思是执行桩中的代码找到符号表中代码跳转执行(0000000100006A28)。

6A28这段代码在__stub_helper中:



对应上了。实际上执行的是dyld_stub_binder。也就是说懒加载符号表里面的初始值都是执行符号绑定的函数

dyld_stub_binder是外部函数,那么怎么得到的dyld_stub_binder函数呢?



所以dyld_stub_binder是通过去非懒加载表中查找。
验证 :




验证确认,No-Lazy Symbol Pointers表中默认值是0

符号绑定过程:

  • 程序一运行,先绑定No-Lazy Symbol Pointers表中dyld_stub_binder的值。
  • 调用NSLog先找桩,执行桩中的代码。桩中的代码是找懒加载符号表中的代码去执行。
  • 懒加载符号表中的初始值是本地的源代码,这个代码去NoLazy表中找绑定函数地址。
  • 进入dyldbinder函数进行绑定。

binder函数执行完毕后就调用第一次的NSLog了。这个时候再看一下懒加载符号表中的符号:




符号已经变了。这个时候符号就已经绑定成功了。

接着执行第二次NSLog,这个时候依然是去找桩中的代码执行:



这个时候通过桩直接跳到了真实地址(还是虚拟的)。这个做的原因是符号表中保存地址执行代码,代码是保存在代码段的(桩)。





  • 外部函数调用时执行桩中的代码(__TEXT,__stubs)。
  • 桩中的代码去懒加载符号表中找地址执行(__DATA,__la_symbo_ptrl)。
    • 通过懒加载符号表中的地址去执行。要么直接调用函数地址(绑定过了),要么去__TEXT,__stubhelper中找绑定函数进行绑定。懒加载符号表中默认保存的是寻找binder的代码。
  • 懒加载中的代码去__TEXT,__stubhelper中执行绑定代码(binder函数)。
  • 绑定函数在非懒加载符号表中(__DATA._got),程序运行就绑定好了dyld

2.4 通过符号找字符串

上面使用fishhook的时候我们是通过rebindNSLog.name = "NSLog";告诉fishhookhook NSLog。那么fishhook通过NSLog怎么找到的符号的呢?

首先,我们清楚在绑定的时候是去Lazy Symbol中去找的NSLog对应的绑定代码:




找的是0x00008008这个地址,在Lazy SymbolNSLog排在第一个。

Indirect Symbols中可以看到顺序和Lazy Symbols中相同,也就是要找Lazy Symbols中的符号,只要找到Indirect Symbols中对应的第几个就可以了。




那么怎么确认Indirect Symbols中的第几个呢?
Indirect Symbolsdata对应值(十六进制)这里NSLog101,这个代表着NSLog在总的符号表(Symbols)中的角标:



在这里我们可以看到NSLogString Table中偏移为0x98(十六进制)。


通过偏移值计算得到0xCC38就确认到了_NSLog(长度+首地址)。

这里通过.隔开,函数名前面有_

这样我们就从Lazy Symbols -> Indirect Symbols -> Symbols - > String Table 通过符号找到了字符串。那么fishhook的过程就是这么处理的,通过遍历所有符号和要hook的数组中的字符串做对比。

fishhook中有一张图说明这个关系:




这里是通过符号查找close字符串。

  1. Lazy Symbol Pointer Tableclose index1061
  2. Indirect Symbol Table 1061 对应的角标为0X00003fd7(十进制16343)。
  3. Symbol Table找角标16343对应的字符串表中的偏移值70026
  4. String Table中找首地址+偏移值(70026)就找到了close
    字符串。

实际的原理还是通过传递的字符串找到符号进行替换:通过字符串找符号过程:

  1. String Table中找到字符串计算偏移值。
  2. 通过偏移值在Symbols中找到角标。
  3. 通过角标在Indirect Symbols中找到对应的符号。这个时候就能拿到这个符号的index了。
  4. 通过找到的indexLazy Symbols中找到对应index的符号。

2.5 去掉符号&恢复符号

符号本身在MachO文件中,占用包体积大小 ,在我们分析别人的App时符号是去掉的。

2.5.1 去除符号

符号基本分为:全局符号、间接符号(导出&导入)、本地符号。
对于App来说会去掉所有符号(间接符号除外)。对于动态库来说要保留全局符号(外部要调用)。

去掉符号在Build setting中设置:




  • Deployment Postprocessing:设置为YES则在编译阶段去符号,否则在打包阶段去符号。
  • Strip StyleAll Symbols去掉所有符号(间接除外),Non-Global Symbols去掉除全局符号外的符号。Debugging Symbols去掉调试符号。

设置Deployment PostprocessingYESStrip StyleAll Symbols。编译查看多了一个.bcsymbolmap文件,这个文件就是bitcode


这个时候的MachO文件中Symbols就只剩下间接符号表中的符号了:


其中
value为函数的实现地址(imp)。间接符号不会找到符号表中地址执行,是找Lazy Symbol Table中的地址。

代码中打断点就断不住了:




先计算出偏移值,下次直接ASLR+偏移值直接断点。这个也就是动态调试常用的方法。


2.5.2 恢复符号

前面动态调试下断点比较麻烦,如果能恢复符号的话就方便很多了。
在上面的例子中去掉所有符号后Symbol Table中只有间接符号了。虽然符号表中没有了,但是类列表和方法列表中依然存在。

这也就为我们提供了创建Symbol Table的机会。
可以通过restore-symbol工具恢复符号(只能恢复oc的,runtime机制导致):./restore-symbol 原始Macho文件 -o 恢复后文件

./restore-symbol FishHookDemo -o recoverDemo



这个时候就可以重签名后进行动态调试了。

2.6 fishhook源码解析

rebind_symbols
rebind_symbols的实现:

//第一次是拿dyld的回调,之后是手动拿到所有image去调用。这里因为没有指定image所以需要拿到所有的。
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
//prepend_rebindings的函数会将整个 rebindings 数组添加到 _rebindings_head 这个链表的头部
//Fishhook采用链表的方式来存储每一次调用rebind_symbols传入的参数,每次调用,就会在链表的头部插入一个节点,链表的头部是:_rebindings_head
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
//根据上面的prepend_rebinding来做判断,如果小于0的话,直接返回一个错误码回去
if (retval < 0) {
return retval;
}
//根据_rebindings_head->next是否为空判断是不是第一次调用。
if (!_rebindings_head->next) {
//第一次调用的话,调用_dyld_register_func_for_add_image注册监听方法.
//已经被dyld加载的image会立刻进入回调。之后的image会在dyld装载的时候触发回调。这里相当于注册了一个回调到 _rebind_symbols_for_image 函数。
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
//不是第一次调用,遍历已经加载的image,进行的hook
uint32_t c = _dyld_image_count();//这个相当于 image list count
for (uint32_t i = 0; i < c; i++) {
//遍历重新绑定image header aslr
_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
}
}
return retval;
}
  • 首先通过prepend_rebindings函数生成链表,存放所有要Hook的函数。
  • 根据_rebindings_head->next是否为空判断是不是第一次调用,第一次调用走系统的回调,第二次则自己获取所有的image list进行遍历。
  • 最后都会走_rebind_symbols_for_image函数。

  • _rebind_symbols_for_image

    //两个参数 header  和 ASLR
    static void _rebind_symbols_for_image(const struct mach_header *header,
    intptr_t slide) {
    //_rebindings_head 参数是要交换的数据,head的头
    rebind_symbols_for_image(_rebindings_head, header, slide);
    }

    这里直接调用了rebind_symbols_for_image,传递了head链表地址。

    rebind_symbols_image

    int rebind_symbols_image(void *header,
    intptr_t slide,
    struct rebinding rebindings[],
    size_t rebindings_nel) {
    struct rebindings_entry *rebindings_head = NULL;
    int retval = prepend_rebindings(&rebindings_head, rebindings, rebindings_nel);
    //如果指定image就直接调用了 rebind_symbols_for_image,没有遍历了。
    rebind_symbols_for_image(rebindings_head, (const struct mach_header *) header, slide);
    if (rebindings_head) {
    free(rebindings_head->rebindings);
    }
    free(rebindings_head);
    return retval;
    }

    底层和rebind_symbols都调用到了rebind_symbols_for_image,由于给定了image所以不需要循环遍历。

    rebind_symbols_for_image

    //回调的最终就是这个函数! 三个参数:要交换的数组  、 image的头 、 ASLR的偏移
    static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
    const struct mach_header *header,
    intptr_t slide) {

    /*dladdr() 可确定指定的address 是否位于构成进程的进址空间的其中一个加载模块(可执行库或共享库)内,如果某个地址位于在其上面映射加载模块的基址和为该加载模块映射的最高虚拟地址之间(包括两端),则认为该地址在加载模块的范围内。如果某个加载模块符合这个条件,则会搜索其动态符号表,以查找与指定的address 最接近的符号。最接近的符号是指其值等于,或最为接近但小于指定的address 的符号。
    */

    /*
    如果指定的address 不在其中一个加载模块的范围内,则返回0 ;且不修改Dl_info 结构的内容。否则,将返回一个非零值,同时设置Dl_info 结构的字段。
    如果在包含address 的加载模块内,找不到其值小于或等于address 的符号,则dli_sname 、dli_saddr 和dli_size字段将设置为0 ; dli_bind 字段设置为STB_LOCAL , dli_type 字段设置为STT_NOTYPE 。
    */


    // typedef struct dl_info {
    // const char *dli_fname; //image 镜像路径
    // void *dli_fbase; //镜像基地址
    // const char *dli_sname; //函数名字
    // void *dli_saddr; //函数地址
    // } Dl_info;

    Dl_info info;//拿到image的信息
    //dladdr函数就是在程序里面找header
    if (dladdr(header, &info) == 0) {
    return;
    }
    //准备从MachO里面去找!
    segment_command_t *cur_seg_cmd;//临时变量
    //这里与MachOView中看到的对应
    segment_command_t *linkedit_segment = NULL;//SEG_LINKEDIT
    struct symtab_command* symtab_cmd = NULL;//LC_SYMTAB 符号表地址
    struct dysymtab_command* dysymtab_cmd = NULL;//LC_DYSYMTAB 动态符号表地址
    //cur为了跳过header的大小,找loadCommands cur = 首地址 + mach_header大小
    uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
    //循环load commands找对应的 SEG_LINKEDIT LC_SYMTAB LC_DYSYMTAB
    for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    //这里`SEG_LINKEDIT`获取和`LC_SYMTAB`与`LC_DYSYMTAB`不同是因为在`MachO`中分别对应`LC_SEGMENT_64(__LINKEDIT)`、`LC_SYMTAB`、`LC_DYSYMTAB`
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
    if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
    linkedit_segment = cur_seg_cmd;
    }
    } else if (cur_seg_cmd->cmd == LC_SYMTAB) {
    symtab_cmd = (struct symtab_command*)cur_seg_cmd;
    } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
    dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
    }
    }
    //有任何一项为空就直接返回,nindirectsyms表示间接符号表中符号数量,没有则直接返回
    if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
    !dysymtab_cmd->nindirectsyms) {
    return;
    }

    // Find base symbol/string table addresses
    //符号表和字符串表都属于data段中的linkedit,所以以linkedit基址+偏移量去获取地址(这里的偏移量不是整个macho的偏移量,是相对基址的偏移量)
    //链接时程序的基址 = __LINKEDIT.VM_Address -__LINKEDIT.File_Offset + silde的改变值
    uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
    //printf("地址:%p\n",linkedit_base);
    //符号表的地址 = 基址 + 符号表偏移量
    nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
    //字符串表的地址 = 基址 + 字符串表偏移量
    char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);

    // Get indirect symbol table (array of uint32_t indices into symbol table)
    //动态(间接)符号表地址 = 基址 + 动态符号表偏移量
    uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

    cur = (uintptr_t)header + sizeof(mach_header_t);
    for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    cur_seg_cmd = (segment_command_t *)cur;
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
    //寻找到load command 中的data【LC_SEGEMNT_64(__DATA)】,相当于拿到data段的首地址
    if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
    strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
    continue;
    }

    for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
    section_t *sect =
    (section_t *)(cur + sizeof(segment_command_t)) + j;
    //找懒加载表(lazy symbol table)
    if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
    //找到直接调用函数 perform_rebinding_with_section,这里4张表就都已经找到了。传入要hook的数组、ASLR、以及4张表
    perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
    }
    //非懒加载表(Non-Lazy symbol table)
    if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
    perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
    }
    }
    }
    }
    }
    • 找到SEG_LINKEDITLC_SYMTABLC_DYSYMTABload commans

    SEG_LINKEDIT获取和LC_SYMTABLC_DYSYMTAB不同是因为在Load Commands中本来就不同,我们解析其它字段也要做类似操作
    • 根据linkedit和偏移值分别找到符号表的地址字符串表的地址以及间接符号表地址
    • 遍历load commandsdata段找到懒加载符号表非懒加载符号表
    • 找到表的同时就直接调用perform_rebinding_with_section进行hook替换函数符号。

    perform_rebinding_with_section

    //rebindings:要hook的函数链表,可以理解为数组
    //section:懒加载/非懒加载符号表地址
    //slide:ASLR
    //symtab:符号表地址
    //strtab:字符串标地址
    //indirect_symtab:动态(间接)符号表地址
    static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
    section_t *section,
    intptr_t slide,
    nlist_t *symtab,
    char *strtab,
    uint32_t *indirect_symtab) {
    //nl_symbol_ptr和la_symbol_ptrsection中的reserved1字段指明对应的indirect symbol table起始的index。也就是第几个这里是和间接符号表中相对应的
    //这里就拿到了index
    uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
    //slide+section->addr 就是符号对应的存放函数实现的数组也就是我相应的__nl_symbol_ptr和__la_symbol_ptr相应的函数指针都在这里面了,所以可以去寻找到函数的地址。
    //indirect_symbol_bindings中是数组,数组中是函数指针。相当于lazy和non-lazy中的data
    void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
    //遍历section里面的每一个符号(懒加载/非懒加载)
    for (uint i = 0; i < section->size / sizeof(void *); i++) {
    //找到符号在Indrect Symbol Table表中的值
    //读取indirect table中的数据
    uint32_t symtab_index = indirect_symbol_indices[i];
    if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
    symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
    continue;
    }
    //以symtab_index作为下标,访问symbol table,拿到string table 的偏移值
    uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
    //获取到symbol_name 首地址 + 偏移值。(char* 字符的地址)
    char *symbol_name = strtab + strtab_offset;
    //判断是否函数的名称是否有两个字符,因为函数前面有个_,所以方法的名称最少要1个
    bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1];
    //遍历最初的链表,来判断名字进行hook
    struct rebindings_entry *cur = rebindings;
    while (cur) {
    for (uint j = 0; j < cur->rebindings_nel; j++) {
    //这里if的条件就是判断从symbol_name[1]两个函数的名字是否都是一致的,以及判断字符长度是否大于1
    if (symbol_name_longer_than_1 &&
    strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
    //判断replaced的地址不为NULL 要替换的自己实现的方法和rebindings[j].replacement的方法不一致。
    if (cur->rebindings[j].replaced != NULL &&
    indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
    //让rebindings[j].replaced保存indirect_symbol_bindings[i]的函数地址,相当于将原函数地址给到你定义的指针的指针。
    *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
    }
    //替换内容为自己自定义函数地址,这里就相当于替换了内存中的地址,下次桩直接找到lazy/non-lazy表的时候直接就走这个替换的地址了。
    indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
    //替换完成跳转外层循环,到(懒加载/非懒加载)数组的下一个数据。
    goto symbol_loop;
    }
    }
    //没有找到就找自己要替换的函数数组的下一个函数。
    cur = cur->next;
    }
    symbol_loop:;
    }
    }
    • 首先通过懒加载/非懒加载符号表和间接符号表找到所有的index
    • 将懒加载/非懒加载符号表的data放入indirect_symbol_bindings数组中。
    indirect_symbol_bindings就是存放lazynon-lazy表中的data数组:
    • 遍历懒加载/非懒加载符号表。
      • 读取indirect_symbol_indices找到符号在Indrect Symbol Table表中的值放入symtab_index
      • symtab_index作为下标,访问symbol table,拿到string table的偏移值。
      • 根据 strtab_offset偏移值获取字符地址symbol_name,也就相当于字符名称。
      • 循环遍历rebindings也就是链表(自定义的Hook数据)
      • 判断&symbol_name[1]rebindings[j].name两个函数的名字是否都是一致的,以及判断字符长度是否大于1
      • 相同则先保存原地址到自定义函数指针(如果replaced传值的话,没有传则不保存)。并且用要Hook的目标函数replacement替换indirect_symbol_bindings,这里就完成了Hook
    • reserved1确认了懒加载和非懒加载符号在间接符号表中的index值。

    疑问点:懒加载和非懒加载怎么和间接符号表index对应的呢?
    直接Hook dyld_stub_binder以及NSLog看下index对应的值:




    在间接符号表中非懒加载符号从20开始供两个,懒加载从22开始,这也就对应上了。这也就验证了懒加载和非懒加载符号都在间接符号表中能对应上。

    总结


    作者:HotPotCat
    链接:https://www.jianshu.com/p/ca52a45b3a2f



    作者:HotPotCat
    链接:https://www.jianshu.com/p/ca52a45b3a2f

    收起阅读 »

    petite-vue源码分析:无虚拟DOM的极简版Vue

    vue
    最近发现Vue增加了一个petite-vue的仓库,大概看了一下,这是一个无虚拟DOM的mini版Vue,前身貌似是vue-lite(瞎猜的~),主要用于在服务端渲染的HTML页面中上"sprinkling"(点缀)一些Vue式的交互。颇有意思,于是看了下源码...
    继续阅读 »

    最近发现Vue增加了一个petite-vue的仓库,大概看了一下,这是一个无虚拟DOM的mini版Vue,前身貌似是vue-lite(瞎猜的~),主要用于在服务端渲染的HTML页面中上"sprinkling"(点缀)一些Vue式的交互。颇有意思,于是看了下源码(v0.2.3),整理了本文。



    起步


    开发调试环境


    整个项目的开发环境非常简单


    git clone git@github.com:vuejs/petite-vue.git

    yarn

    # 使用vite启动
    npm run dev

    # 访问http://localhost:3000/

    (不得不说,用vite来搭开发环境还是挺爽的~


    新建一个测试文件exmaples/demo.html,写点代码


    <script type="module">
    import { createApp, reactive } from '../src'

    createApp({
    msg: "hello"
    }).mount("#app")
    </script>

    <div id="app">
    <h1>{{msg}}</h1>
    </div>

    然后访问http://localhost:3000/demo.html即可


    目录结构


    从readme可以看见项目与标准vue的一些差异



    • Only ~5.8kb,体积很小

    • Vue-compatible template syntax,与Vue兼容的模板语法

    • DOM-based, mutates in place,基于DOM驱动,就地转换

    • Driven by @vue/reactivity,使用@vue/reactivity驱动


    目录结构也比较简单,使用ts编写,外部依赖基本上只有@vue/reactivity



    核心实现


    createContext


    从上面的demo代码可以看出,整个项目从createApp开始。


    export const createApp = (initialData?: any) => {
    // root context
    const ctx = createContext()
    if (initialData) {
    ctx.scope = reactive(initialData) // 将初始化数据代理成响应式
    }
    // app的一些接口
    return {
    directive(name: string, def?: Directive) {},
    mount(el?: string | Element | null) {},
    unmount() {}
    }
    }

    关于Vue3中的reactive,可以参考之前整理的:Vue3中的数据侦测reactive,这里就不再展开了。


    createApp中主要是通过createContext创建根context,这个上下文现在基本不陌生了,来看看createContext


    export const createContext = (parent?: Context): Context => {
    const ctx: Context = {
    ...parent,
    scope: parent ? parent.scope : reactive({}),
    dirs: parent ? parent.dirs : {}, // 支持的指令
    effects: [],
    blocks: [],
    cleanups: [],
    // 提供注册effect回调的接口,主要使用调度器来控制什么时候调用
    effect: (fn) => {
    if (inOnce) {
    queueJob(fn)
    return fn as any
    }
    // @vue/reactivity中的effect方法
    const e: ReactiveEffect = rawEffect(fn, {
    scheduler: () => queueJob(e)
    })
    ctx.effects.push(e)
    return e
    }
    }
    return ctx
    }

    稍微看一下queueJob就可以发现,还是Vue中熟悉的nextTick实现,



    • 通过一个全局变量queue队列保存回调

    • 在下一个微任务处理阶段,依次执行queue中的每一个回调,然后清空queue


    mount


    基本使用


    createApp().mount("#app")

    mount方法最主要的作用就是处理el参数,找到应用挂载的根DOM节点,然后执行初始化流程


    mount(el?: string | Element | null) {
    let roots: Element[]
    // ...根据el参数初始化roots
    // 根据el创建Block实例
    rootBlocks = roots.map((el) => new Block(el, ctx, true))
    return this
    }

    Block是一个抽象的概念,用于统一DOM节点渲染、插入、移除和销毁等操作。


    下图是依赖这个Block的地方,可以看见主要在初始化、iffor这三个地方使用



    看一下Block的实现


    // src/block.ts
    export class Block {
    template: Element | DocumentFragment
    ctx: Context
    key?: any
    parentCtx?: Context

    isFragment: boolean
    start?: Text
    end?: Text

    get el() {
    return this.start || (this.template as Element)
    }

    constructor(template: Element, parentCtx: Context, isRoot = false) {
    // 初始化this.template
    // 初始化this.ctx

    // 构建应用
    walk(this.template, this.ctx)
    }
    // 主要在新增或移除时使用,可以先不用关心实现
    insert(parent: Element, anchor: Node | null = null) {}
    remove() {}
    teardown() {}
    }

    这个walk方法,主要的作用是递归节点和子节点,如果之前了解过递归diff,这里应该比较熟悉。但petite-vue中并没有虚拟DOM,因此在walk中会直接操作更新DOM。


    export const walk = (node: Node, ctx: Context): ChildNode | null | void => {
    const type = node.nodeType
    if (type === 1) {
    // 元素节点
    const el = node as Element
    // ...处理 如v-if、v-for
    // ...检测属性执行对应的指令处理 applyDirective,如v-scoped、ref等

    // 先处理子节点,在处理节点自身的属性
    walkChildren(el, ctx)

    // 处理节点属性相关的自定,包括内置指令和自定义指令
    } else if (type === 3) {
    // 文本节点
    const data = (node as Text).data
    if (data.includes('{{')) {
    // 正则匹配需要替换的文本,然后 applyDirective(text)
    applyDirective(node, text, segments.join('+'), ctx)
    }
    } else if (type === 11) {
    walkChildren(node as DocumentFragment, ctx)
    }
    }

    const walkChildren = (node: Element | DocumentFragment, ctx: Context) => {
    let child = node.firstChild
    while (child) {
    child = walk(child, ctx) || child.nextSibling
    }
    }

    可以看见会根据node.nodeType区分处理处理



    • 对于元素节点,先处理了节点上的一些指令,然后通过walkChildren处理子节点。

      • v-if,会根据表达式决定是否需要创建Block然后执行插入或移除

      • v-for,循环构建Block,然后执行插入



    • 对于文本节点,替换{{}}表达式,然后替换文本内容


    v-if


    来看看if的实现,通过branches保存所有的分支判断,activeBranchIndex通过闭包保存当前位于的分支索引值。


    在初始化或更新时,如果某个分支表达式结算结果正确且与上一次的activeBranchIndex不一致,就会创建新的Block,然后走Block构造函数里面的walk。


    export const _if = (el: Element, exp: string, ctx: Context) => {
    const parent = el.parentElement!
    const anchor = new Comment('v-if')
    parent.insertBefore(anchor, el)

    // 存放条件判断的各种分支
    const branches: Branch[] = [{ exp,el }]

    // 定位if...else if ... else 等分支,放在branches数组中

    let block: Block | undefined
    let activeBranchIndex: number = -1 // 通过闭包保存当前位于的分支索引值

    const removeActiveBlock = () => {
    if (block) {
    parent.insertBefore(anchor, block.el)
    block.remove()
    block = undefined
    }
    }

    // 收集依赖
    ctx.effect(() => {
    for (let i = 0; i < branches.length; i++) {
    const { exp, el } = branches[i]
    if (!exp || evaluate(ctx.scope, exp)) {
    // 当判断分支切换时,会生成新的block
    if (i !== activeBranchIndex) {
    removeActiveBlock()
    block = new Block(el, ctx)
    block.insert(parent, anchor)
    parent.removeChild(anchor)
    activeBranchIndex = i
    }
    return
    }
    }
    // no matched branch.
    activeBranchIndex = -1
    removeActiveBlock()
    })

    return nextNode
    }

    v-for


    for指令的主要作用是循环创建多个节点,这里还根据key实现了类似于diff算法来复用Block的功能


    export const _for = (el: Element, exp: string, ctx: Context) => {
    // ...一些工具方法如createChildContexts、mountBlock

    ctx.effect(() => {
    const source = evaluate(ctx.scope, sourceExp)
    const prevKeyToIndexMap = keyToIndexMap
    // 根据循环项创建多个子节点的context
    ;[childCtxs, keyToIndexMap] = createChildContexts(source)
    if (!mounted) {
    // 首次渲染,创建新的Block然后insert
    blocks = childCtxs.map((s) => mountBlock(s, anchor))
    mounted = true
    } else {
    // 更新时
    const nextBlocks: Block[] = []
    // 移除不存在的block
    for (let i = 0; i < blocks.length; i++) {
    if (!keyToIndexMap.has(blocks[i].key)) {
    blocks[i].remove()
    }
    }
    // 根据key进行处理
    let i = childCtxs.length
    while (i--) {
    const childCtx = childCtxs[i]
    const oldIndex = prevKeyToIndexMap.get(childCtx.key)
    const next = childCtxs[i + 1]
    const nextBlockOldIndex = next && prevKeyToIndexMap.get(next.key)
    const nextBlock =
    nextBlockOldIndex == null ? undefined : blocks[nextBlockOldIndex]
    // 不存在旧的block,直接创建
    if (oldIndex == null) {
    // new
    nextBlocks[i] = mountBlock(
    childCtx,
    nextBlock ? nextBlock.el : anchor
    )
    } else {
    // 存在旧的block,复用,检测是否需要移动位置
    const block = (nextBlocks[i] = blocks[oldIndex])
    Object.assign(block.ctx.scope, childCtx.scope)
    if (oldIndex !== i) {
    if (blocks[oldIndex + 1] !== nextBlock) {
    block.insert(parent, nextBlock ? nextBlock.el : anchor)
    }
    }
    }
    }
    blocks = nextBlocks
    }
    })

    return nextNode
    }

    处理指令


    所有的指令都是通过applyDirectiveprocessDirective来处理的,后者是基于前者的二次封装,主要处理一些内置的指令快捷方式builtInDirectives


    export const builtInDirectives: Record<string, Directive<any>> = {
    bind,
    on,
    show,
    text,
    html,
    model,
    effect
    }

    每种指令都是基于ctx和el等来实现快速实现某些逻辑,具体实现可以参考对应源码。


    当调用app.directive注册自定义指令时,


    directive(name: string, def?: Directive) {
    if (def) {
    ctx.dirs[name] = def
    return this
    } else {
    return ctx.dirs[name]
    }
    },

    实际上是向contenx的dirs添加一个属性,当调用applyDirective时,就可以得到对应的处理函数


    const applyDirective = (el: Node,dir: Directive<any>,exp: string,ctx: Context,arg?: string,modifiers?: Record<string, true>) => {
    const get = (e = exp) => evaluate(ctx.scope, e, el)
    // 执行指令方法
    const cleanup = dir({
    el,
    get,
    effect: ctx.effect,
    ctx,
    exp,
    arg,
    modifiers
    })
    // 收集那些需要在卸载时清除的副作用
    if (cleanup) {
    ctx.cleanups.push(cleanup)
    }
    }

    因此,可以利用上面传入的这些参数来构建自定义指令


    app.directive("auto-focus", ({el})=>{
    el.focus()
    })

    小结


    整个代码看起来,确实非常精简



    • 没有虚拟DOM,就无需通过template构建render函数,直接递归遍历DOM节点,通过正则处理各种指令就行了

    • 借助@vue/reactivity,整个响应式系统实现的十分自然,除了在解析指令的使用通过ctx.effect()收集依赖,基本无需再关心数据变化的逻辑


    文章开头提到,petite-vue的主要作用是:在服务端渲染的HTML页面中上"sprinkling"(点缀)一些Vue式的交互。


    就我目前接触到的大部分服务端渲染HTML的项目,如果要实现一些DOM交互,一般使用



    • jQuery操作DOM,yyds

    • 当然Vue也是可以通过script + template的方式编写的,但为了一个div的交互接入Vue,又有点杀鸡焉用牛刀的感觉

    • 其他如React框架等同上


    petite-vue使用了与Vue基本一致的模板语法和响应式功能,开发体验上应该很不错。且其无需考虑虚拟DOM跨平台的功能,在源码中直接使用浏览器相关API操作DOM,减少了框架runtime运行时的成本,性能方面应该也不错。


    总结一下,感觉petite-vue结合了Vue标准版本的开发体验,以非常小的代码体积、良好的开发体验和还不错的运行性能,也许可以用来替代jQuery,用更现代的方式来操作DOM。


    该项目是6月30号提交的第一个版本,目前相关的功能和接口应该不是特别稳定,可能会有调整。但就exmples目录中的示例而言,应该能满足一些简单的需求场景了,也许可以尝试在一些比较小型的历史项目中使用。


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

    收起阅读 »

    【学不动了就回家喂猪】尤大大新活 petite-vue 尝鲜

    vue
    前言 打开尤大大的GitHub,发现多了个叫 petite-vue 的东西,好家伙,Vue3 和 Vite 还没学完呢,又开始整新东西了?本着学不死就往死里学的态度,咱还是来瞅瞅这到底是个啥东西吧,谁让他是咱的祖师爷呢! 简介 从名字来看可以知道 peti...
    继续阅读 »


    前言


    image.png


    打开尤大大的GitHub,发现多了个叫 petite-vue 的东西,好家伙,Vue3 和 Vite 还没学完呢,又开始整新东西了?本着学不死就往死里学的态度,咱还是来瞅瞅这到底是个啥东西吧,谁让他是咱的祖师爷呢!


    简介


    image.png


    从名字来看可以知道 petite-vue 是一个 mini 版的vue,大小只有5.8kb,可以说是非常小了。据尤大大介绍,petite-vue 是 Vue 的可替代发行版,针对渐进式增强进行了优化。它提供了与标准 Vue 相同的模板语法和响应式模型:



    • 大小只有5.8kb

    • Vue 兼容模版语法

    • 基于DOM,就地转换

    • 响应式驱动


    上活


    下面对 petite-vue 的使用做一些介绍。


    简单使用


    <body>
    <script src="https://unpkg.com/petite-vue" defer init></script>
    <div v-scope="{ count: 0 }">
    <button @click="count--">-</button>
    <span>{{ count }}</span>
    <button @click="count++">+</button>
    </div>
    </body>

    通过 script 标签引入同时添加 init ,接着就可以使用 v-scope 绑定数据,这样一个简单的计数器就实现了。



    了解过 Alpine.js 这个框架的同学看到这里可能有点眼熟了,两者语法之间是很像的。



    <!--  Alpine.js  -->
    <div x-data="{ open: false }">
    <button @click="open = true">Open Dropdown</button>
    <ul x-show="open" @click.away="open = false">
    Dropdown Body
    </ul>
    </div>

    除了用 init 的方式之外,也可以用下面的方式:


    <body>
    <div v-scope="{ count: 0 }">
    <button @click="count--">-</button>
    <span>{{ count }}</span>
    <button @click="count++">+</button>
    </div>
    <!-- 放在body底部 -->
    <script src="https://unpkg.com/petite-vue"></script>
    <script>
    PetiteVue.createApp().mount()
    </script>
    </body>

    或使用 ES module 的方式:


    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'
    createApp().mount()
    </script>

    <div v-scope="{ count: 0 }">
    <button @click="count--">-</button>
    <span>{{ count }}</span>
    <button @click="count++">+</button>
    </div>
    </body>

    根作用域


    createApp 函数可以接受一个对象,类似于我们平时使用 data 和 methods 一样,这时 v-scope 不需要绑定值。


    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'
    createApp({
    count: 0,
    increment() {
    this.count++
    },
    decrement() {
    this.count--
    }
    }).mount()
    </script>

    <div v-scope>
    <button @click="decrement">-</button>
    <span>{{ count }}</span>
    <button @click="increment">+</button>
    </div>
    </body>

    指定挂载元素


    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'
    createApp({
    count: 0
    }).mount('#app')
    </script>

    <div id="app">
    {{ count }}
    </div>
    </body>

    生命周期


    可以监听每个元素的生命周期事件。


    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'
    createApp({
    onMounted1(el) {
    console.log(el) // <span>1</span>
    },
    onMounted2(el) {
    console.log(el) // <span>2</span>
    }
    }).mount('#app')
    </script>

    <div id="app">
    <span @mounted="onMounted1($el)">1</span>
    <span @mounted="onMounted2($el)">2</span>
    </div>
    </body>

    组件


    在 petite-vue 里,组件可以使用函数的方式创建,通过template可以实现复用。


    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'

    function Counter(props) {
    return {
    $template: '#counter-template',
    count: props.initialCount,
    increment() {
    this.count++
    },
    decrement() {
    this.count++
    }
    }
    }

    createApp({
    Counter
    }).mount()
    </script>

    <template id="counter-template">
    <button @click="decrement">-</button>
    <span>{{ count }}</span>
    <button @click="increment">+</button>
    </template>

    <!-- 复用 -->
    <div v-scope="Counter({ initialCount: 1 })"></div>
    <div v-scope="Counter({ initialCount: 2 })"></div>
    </body>

    全局状态管理


    借助 reactive 响应式 API 可以很轻松的创建全局状态管理


    <body>
    <script type="module">
    import { createApp, reactive } from 'https://unpkg.com/petite-vue?module'

    const store = reactive({
    count: 0,
    increment() {
    this.count++
    }
    })
    // 将count加1
    store.increment()
    createApp({
    store
    }).mount()
    </script>

    <div v-scope>
    <!-- 输出1 -->
    <span>{{ store.count }}</span>
    </div>
    <div v-scope>
    <button @click="store.increment">+</button>
    </div>
    </body>

    自定义指令


    这里来简单实现一个输入框自动聚焦的指令。


    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'

    const autoFocus = (ctx) => {
    ctx.el.focus()
    }

    createApp().directive('auto-focus', autoFocus).mount()
    </script>

    <div v-scope>
    <input v-auto-focus />
    </div>
    </body>

    内置指令



    • v-model

    • v-if / v-else / v-else-if

    • v-for

    • v-show

    • v-html

    • v-text

    • v-pre

    • v-once

    • v-cloak



    注意:v-for 不需要key,另外 v-for 不支持 深度解构



    <body>
    <script type="module">
    import { createApp } from 'https://unpkg.com/petite-vue?module'

    createApp({
    userList: [
    { name: '张三', age: { a: 23, b: 24 } },
    { name: '李四', age: { a: 23, b: 24 } },
    { name: '王五', age: { a: 23, b: 24 } }
    ]
    }).mount()
    </script>

    <div v-scope>
    <!-- 支持 -->
    <li v-for="{ age } in userList">
    {{ age.a }}
    </li>
    <!-- 不支持 -->
    <li v-for="{ age: { a } } in userList">
    {{ a }}
    </li>
    </div>
    </body>

    不支持


    为了更轻量小巧,petite-vue 不支持以下特性:



    • ref()、computed

    • render函数,因为petite-vue 没有虚拟DOM

    • 不支持Map、Set等响应类型

    • Transition, KeepAlive, Teleport, Suspense

    • v-on="object"

    • v-is &

    • v-bind:style auto-prefixing


    总结


    以上就是对 petite-vue 的一些简单介绍和使用,抛砖引玉,更多新的探索就由你们去发现了。


    总的来说,prtite-vue 保留了 Vue 的一些基础特性,这使得 Vue 开发者可以无成本使用,在以往,当我们在开发一些小而简单的页面想要引用 Vue 但又常常因为包体积带来的考虑而放弃,现在,petite-vue 的出现或许可以拯救这种情况了,毕竟它真的很小,大小只有 5.8kb,大约只是 Alpine.js 的一半。


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

    10张脑图带你快速入门Vue3 | 附高清原图

    vue
    前言 这个月重新开始学习Vue3 目前已经完结第一部分:基础部分 我将所有内容吸收整理成10张脑图,一来快速入门Vue3,二来方便以后查看 脑图 应用实例和组件实例 模板语法 配置选项 计算属性和监听器 绑定class和style 条件渲染 列表渲...
    继续阅读 »

    前言


    这个月重新开始学习Vue3


    目前已经完结第一部分:基础部分


    我将所有内容吸收整理成10张脑图,一来快速入门Vue3,二来方便以后查看


    脑图


    应用实例和组件实例


    1应用实例和组件实例.png


    模板语法


    2模板语法.png


    配置选项


    3配置选项.png


    计算属性和监听器


    4计算属性和监听器.png


    绑定class和style


    5绑定class和style.png


    条件渲染


    6条件渲染.png


    列表渲染


    7列表渲染v-for.png


    事件处理


    8事件处理.png


    v-model及其修饰符


    9v-model及其修饰符.png


    组件的基本使用


    10组件的基本使用.png


    温馨小贴士



    1. 由于图片较多,为了避免一张张保存的麻烦


    我已将上述原图已上传githubgithub.com/jCodeLife/m…



    1. 如果需要更改图片,为了方便你按照自己的习惯进行修改


    我已将原始文件xmind上传github
    github.com/jCodeLife/m…



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

    收起阅读 »

    面试官问我CORS跨域,我直接一套操作斩杀!

    前言 我们都知道由于同源策略的存在,导致我们在跨域请求数据的时候非常的麻烦。首先阻挡我们的所谓同源到底是什么呢?,所谓同源就是浏览器的一个安全机制,不同源的客户端脚本没有在明确授权的情况下,不能读写对方资源。由于存在同源策略的限制,而又有需要跨域的业务,所以就...
    继续阅读 »

    前言


    我们都知道由于同源策略的存在,导致我们在跨域请求数据的时候非常的麻烦。首先阻挡我们的所谓同源到底是什么呢?,所谓同源就是浏览器的一个安全机制,不同源的客户端脚本没有在明确授权的情况下,不能读写对方资源。由于存在同源策略的限制,而又有需要跨域的业务,所以就有了CORS的出现。


    我们都知道,jsonp也可以跨域,那为什么还要使用CORS



    • jsonp只可以使用 GET 方式提交

    • 不好调试,在调用失败的时候不会返回任何状态码

    • 安全性,万一假如提供jsonp的服务存在页面注入漏洞,即它返回的javascript的内容被人控制的。那么结果是什么?所有调用这个jsonp的网站都会存在漏洞。于是无法把危险控制在一个域名下…所以在使用jsonp的时候必须要保证使用的jsonp服务必须是安全可信的。


    开始CORS


    CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing),他允许浏览器向跨源服务器发送XMLHttpRequest请求,从而克服啦 AJAX 只能同源使用的限制


    CORS需要浏览器和服务器同时支持,整个 CORS通信过程,都是浏览器自动完成不需要用户参与,对于开发者来说,CORS的代码和正常的 ajax 没有什么差别,浏览器一旦发现跨域请求,就会添加一些附加的头信息,


    CORS这么好吗,难道就没有缺点嘛?


    答案肯定是NO,目前所有最新浏览器都支持该功能,但是万恶的IE不能低于10


    简单请求和非简单请求


    浏览器将CORS请求分成两类:简单请求和非简单请求


    简单请求


    凡是同时满足以下两种情况的就是简单请求,反之则非简单请求,浏览器对这两种请求的处理不一样



    • 请求方法是以下方三种方法之一

      • HEAD

      • GET

      • POST



    • HTTP的头信息不超出以下几种字段

      • Accept

      • Accept-Language

      • Content-Language

      • Last-Event-ID

      • Content-Type:只限于三个值 application/x-www-form-urlencodedmultipart/form-datatext/plain




    对于简单请求来说,浏览器之间发送CORS请求,具体来说就是在头信息中,增加一个origin字段,来看一下例子


    GET /cors? HTTP/1.1
    Host: localhost:2333
    Connection: keep-alive
    Origin: http://localhost:2332
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
    Accept: */*
    Referer: http://localhost:2332/CORS.html
    Accept-Encoding: gzip, deflate, br
    Accept-Language: zh-CN,zh;q=0.9
    If-None-Match: W/"1-NWoZK3kTsExUV00Ywo1G5jlUKKs"

    上面的头信息中,Origin字段用来说名本次请求来自哪个源,服务器根据这个值,决定是否同意这次请求。


    如果Origin指定的源不在允许范围之内,服务器就会返回一个正常的HTTP回应,然后浏览器发现头信息中没有包含Access-Control-Allow-Origin 字段,就知道出错啦,然后抛出错误,反之则会出现这个字段(实例如下)


    Access-Control-Allow-Origin: http://api.bob.com
    Access-Control-Allow-Credentials: true
    Access-Control-Expose-Headers: FooBar
    Content-Type: text/html; charset=utf-8



    • Access-Control-Allow-Origin 这个字段是必须的,表示接受那些域名的请求(*为所有)




    • Access-Control-Allow-Credentials 该字段可选, 表示是否可以发送cookie




    • Access-Control-Expose-Headers 该字段可选,XHMHttpRequest对象的方法只能够拿到六种字段: Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma ,如果想拿到其他的需要使用该字段指定。




    如果你想要连带Cookie一起发送,是需要服务端和客户端配合的


    // 服务端
    Access-Control-Allow-Credentials: true
    // 客户端
    var xhr = new XMLHttpRequest();
    xhr.withCredentials = true;
    // 但是如果省略withCredentials属性的设置,有的浏览器还是会发送cookie的
    xhr.withCredentials = false;

    非简单请求


    非简单请求则是不满足上边的两种情况之一,比如请求的方式为 PUT,或者请求头包含其他的字段


    非简单请求的CORS请求是会在正式通信之前进行一次预检请求


    浏览器先询问服务器,当前网页所在的域名是否可以请求您的服务器,以及可以使用那些HTTP动词和头信息,只有得到正确的答复,才会进行正式的请求


    // 前端代码
    var url = 'http://localhost:2333/cors';
    var xhr = new XMLHttpRequest();
    xhr.open('PUT', url, true);
    xhr.setRequestHeader('X-Custom-Header', 'value');
    xhr.send();

    由于上面的代码使用的是 PUT 方法,并且发送了一个自定义头信息.所以是一个非简单请求,当浏览器发现这是一个非简单请求的时候,会自动发出预检请求,看看服务器可不可以接收这种请求,下面是"预检"HTTP 头信息


    OPTIONS /cors HTTP/1.1
    Origin: localhost:2333
    Access-Control-Request-Method: PUT // 表示使用的什么HTTP请求方法
    Access-Control-Request-Headers: X-Custom-Header // 表示浏览器发送的自定义字段
    Host: localhost:2332
    Accept-Language: zh-CN,zh;q=0.9
    Connection: keep-alive
    User-Agent: Mozilla/5.0...

    "预检"使用的请求方法是 OPTIONS , 表示这个请求使用来询问的,


    预检请求后的回应,服务器收到"预检"请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。


    预检的响应头:


    HTTP/1.1 200 OK
    Date: Mon, 01 Dec 2008 01:15:39 GMT
    Server: Apache/2.0.61 (Unix)
    Access-Control-Allow-Origin: http://localhost:2332 // 表示http://localhost:2332可以访问数据
    Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: X-Custom-Header
    Content-Type: text/html; charset=utf-8
    Content-Encoding: gzip
    Content-Length: 0
    Keep-Alive: timeout=2, max=100
    Connection: Keep-Alive
    Content-Type: text/plain

    如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS的头相关信息,这是浏览器就认定,服务器不允许此次访问,从而抛出错误


    预检之后的请求


    当预检请求通过之后发出正经的HTTP请求,还有一个就是一旦通过了预检请求就会,请求的时候就会跟简单请求,会有一个Origin头信息字段。


    通过预检之后的,浏览器发出发请求


    PUT /cors HTTP/1.1
    Origin: http://api.bob.com // 通过预检之后的请求,会自动带上Origin字段
    Host: api.alice.com
    X-Custom-Header: value
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...

    感谢


    谢谢你读完本篇文章,希望对你能有所帮助,如有问题欢迎各位指正。




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

    收起阅读 »

    「百毒不侵」面试官最喜欢问的13种Vue修饰符

    1.lazy lazy修饰符作用是,改变输入框的值时value不会改变,当光标离开输入框时,v-model绑定的值value才会改变 <input type="text" v-model.lazy="value"> <div>{{val...
    继续阅读 »

    image.png


    1.lazy


    lazy修饰符作用是,改变输入框的值时value不会改变,当光标离开输入框时,v-model绑定的值value才会改变


    <input type="text" v-model.lazy="value">
    <div>{{value}}</div>

    data() {
    return {
    value: '222'
    }
    }

    lazy1.gif


    2.trim


    trim修饰符的作用类似于JavaScript中的trim()方法,作用是把v-model绑定的值的首尾空格给过滤掉。


    <input type="text" v-model.trim="value">
    <div>{{value}}</div>

    data() {
    return {
    value: '222'
    }
    }

    number.gif


    3.number


    number修饰符的作用是将值转成数字,但是先输入字符串和先输入数字,是两种情况


    <input type="text" v-model.number="value">
    <div>{{value}}</div>

    data() {
    return {
    value: '222'
    }
    }


    先输入数字的话,只取前面数字部分



    trim.gif



    先输入字母的话,number修饰符无效



    number2.gif


    4.stop


    stop修饰符的作用是阻止冒泡


    <div @click="clickEvent(2)" style="width:300px;height:100px;background:red">
    <button @click.stop="clickEvent(1)">点击</button>
    </div>

    methods: {
    clickEvent(num) {
    不加 stop 点击按钮输出 1 2
    加了 stop 点击按钮输出 1
    console.log(num)
    }
    }

    5.capture


    事件默认是由里往外冒泡capture修饰符的作用是反过来,由外网内捕获


    <div @click.capture="clickEvent(2)" style="width:300px;height:100px;background:red">
    <button @click="clickEvent(1)">点击</button>
    </div>

    methods: {
    clickEvent(num) {
    不加 capture 点击按钮输出 1 2
    加了 capture 点击按钮输出 2 1
    console.log(num)
    }
    }

    6.self


    self修饰符作用是,只有点击事件绑定的本身才会触发事件


    <div @click.self="clickEvent(2)" style="width:300px;height:100px;background:red">
    <button @click="clickEvent(1)">点击</button>
    </div>

    methods: {
    clickEvent(num) {
    不加 self 点击按钮输出 1 2
    加了 self 点击按钮输出 1 点击div才会输出 2
    console.log(num)
    }
    }

    7.once


    once修饰符的作用是,事件只执行一次


    <div @click.once="clickEvent(2)" style="width:300px;height:100px;background:red">
    <button @click="clickEvent(1)">点击</button>
    </div>

    methods: {
    clickEvent(num) {
    不加 once 多次点击按钮输出 1
    加了 once 多次点击按钮只会输出一次 1
    console.log(num)
    }
    }

    8.prevent


    prevent修饰符的作用是阻止默认事件(例如a标签的跳转)


    <a href="#" @click.prevent="clickEvent(1)">点我</a>

    methods: {
    clickEvent(num) {
    不加 prevent 点击a标签 先跳转然后输出 1
    加了 prevent 点击a标签 不会跳转只会输出 1
    console.log(num)
    }
    }

    9.native


    native修饰符是加在自定义组件的事件上,保证事件能执行


    执行不了
    <My-component @click="shout(3)"></My-component>

    可以执行
    <My-component @click.native="shout(3)"></My-component>

    10.left,right,middle


    这三个修饰符是鼠标的左中右按键触发的事件


    <button @click.middle="clickEvent(1)"  @click.left="clickEvent(2)"  @click.right="clickEvent(3)">点我</button>

    methods: {
    点击中键输出1
    点击左键输出2
    点击右键输出3
    clickEvent(num) {
    console.log(num)
    }
    }

    11.passive


    当我们在监听元素滚动事件的时候,会一直触发onscroll事件,在pc端是没啥问题的,但是在移动端,会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符


    <div @scroll.passive="onScroll">...</div>

    12.camel


    不加camel viewBox会被识别成viewbox
    <svg :viewBox="viewBox"></svg>

    加了canmel viewBox才会被识别成viewBox
    <svg :viewBox.camel="viewBox"></svg>

    12.sync


    父组件传值进子组件,子组件想要改变这个值时,可以这么做


    父组件里
    <children :foo="bar" @update:foo="val => bar = val"></children>

    子组件里
    this.$emit('update:foo', newValue)

    sync修饰符的作用就是,可以简写:


    父组件里
    <children :foo.sync="bar"></children>

    子组件里
    this.$emit('update:foo', newValue)

    13.keyCode


    当我们这么写事件的时候,无论按什么按钮都会触发事件


    <input type="text" @keyup="shout(4)">

    那么想要限制成某个按键触发怎么办?这时候keyCode修饰符就派上用场了


    <input type="text" @keyup.keyCode="shout(4)">

    Vue提供的keyCode:


    //普通键
    .enter
    .tab
    .delete //(捕获“删除”和“退格”键)
    .space
    .esc
    .up
    .down
    .left
    .right
    //系统修饰键
    .ctrl
    .alt
    .meta
    .shift

    例如(具体的键码请看键码对应表


    按 ctrl 才会触发
    <input type="text" @keyup.ctrl="shout(4)">

    也可以鼠标事件+按键
    <input type="text" @mousedown.ctrl.="shout(4)">

    可以多按键触发 例如 ctrl + 67
    <input type="text" @keyup.ctrl.67="shout(4)">

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

    收起阅读 »

    iOS 自定义键盘

    很多项目中都使用自定义键盘,实现自定义键盘有很多方法,本文讲的是修改UITextField/UITextView的inputView来实现自定义键盘。如何修改已经知道了,但是怎么修改。有两种思路:自定义CustomTextField/CustomTextVie...
    继续阅读 »

    很多项目中都使用自定义键盘,实现自定义键盘有很多方法,本文讲的是修改UITextField/UITextView的inputView来实现自定义键盘。
    如何修改已经知道了,但是怎么修改。有两种思路:

    1. 自定义CustomTextField/CustomTextView,直接实现如下代码
    textField.inputView = customView;   
    textView.inputView = customView;

    但是这样写有个弊端,就是通用性不强。比如项目中可能要实现某个具体业务逻辑,这个textField/textView是继承ATextField/ATextView,其他地方又有用到的是继承BTextField/BTextView,那我们再写代码时候,可能需要写n个自定义textField/textView,用起来就非常麻烦了,所以这种方法不推荐。

    1. 使用分类来实现自定义键盘
      思路就是在分类中增加一个枚举,这个枚举定义了不同类型的键盘
    typedef NS_ENUM(NSUInteger, SJKeyboardType)
    {
    SJKeyboardTypeDefault, // 使用默认键盘
    SJKeyboardTypeNumber // 使用自定义数字键盘
    // 还可以根据需求 自定义其他样式...
    };

    写一个属性,来标记键盘类型

    @property (nonatomic, assign) SJKeyboardType sjKeyboardType;
    在.m文件中实现getter和setter方法

    static NSString *sjKeyboardTypeKey = @"sjKeyboardTypeKey";
    - (SJKeyboardType)sjKeyboardType
    {
    return [objc_getAssociatedObject(self, &sjKeyboardTypeKey) integerValue];
    }

    - (void)setSjKeyboardType:(SJKeyboardType)sjKeyboardType
    {
    objc_setAssociatedObject(self, &sjKeyboardTypeKey, @(sjKeyboardType), OBJC_ASSOCIATION_ASSIGN);
    [self setupKeyboard:sjKeyboardType];
    }

    在set方法中来实现自定义键盘视图设置及对应点击方法实现

    - (void)setupKeyboard:(SJKeyboardType)sjKeyboardType
    {

    switch (sjKeyboardType) {
    case SJKeyboardTypeDefault:
    break;
    case SJKeyboardTypeNumber: {
    SJCustomKeyboardView *numberInputView = [[[NSBundle mainBundle] loadNibNamed:@"SJCustomKeyboardView" owner:self options:nil] lastObject];
    numberInputView.frame = CGRectMake(0, 0, SJSCREEN_WIDTH, SJNumberKeyboardHeight + SJCustomKeyboardBottomMargin);
    self.inputView = numberInputView;
    numberInputView.textFieldReplacementString = ^(NSString * _Nonnull string) {
    BOOL canEditor = YES;
    if ([self.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) {
    canEditor = [self.delegate textField:self shouldChangeCharactersInRange:NSMakeRange(self.text.length, 0) replacementString:string];
    }

    if (canEditor) {
    [self replaceRange:self.selectedTextRange withText:string];
    }
    };
    numberInputView.textFieldShouldDelete = ^{
    BOOL canEditor = YES;
    if ([self.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)] && self.text.length) {
    canEditor = [self.delegate textField:self shouldChangeCharactersInRange:NSMakeRange(self.text.length - 1, 1) replacementString:@""];
    }
    if (canEditor) {
    [self deleteBackward];
    }
    };
    numberInputView.textFieldShouldClear = ^{
    BOOL canClear = YES;
    if ([self.delegate respondsToSelector:@selector(textFieldShouldClear:)]) {
    canClear = [self.delegate textFieldShouldClear:self];
    }
    if (canClear) {
    [self setText:@""];
    }
    };
    numberInputView.textFieldShouldReturn = ^{
    if ([self.delegate respondsToSelector:@selector(textFieldShouldReturn:)]) {
    [self.delegate textFieldShouldReturn:self];
    }
    };
    break;
    }
    }
    }
    之后就需要实现自定义键盘视图,这里需要注意一点,就是如果使用新建子类实现自定义键盘,个人感觉按钮响应用代理实现会看起来逻辑更清晰

    /* 用代理看的更清楚 但是分类不能实现代理 所以只能用block实现回调 如果自定义textField可以用代理 @protocol SJCustomKeyboardViewDelegate - (void)textFieldReplacementString:(NSString *_Nullable)string; - (BOOL)textFieldShouldDelete; - (BOOL)textFieldShouldClear; - (BOOL)textFieldShouldReturn; @end */

    但是分类不能实现代理,所以只能用block来实现回调


    @property (nonatomic, copy) void (^textFieldReplacementString)(NSString *string);
    @property (nonatomic, copy) void (^textFieldShouldDelete)(void);
    @property (nonatomic, copy) void (^textFieldShouldClear)(void);
    @property (nonatomic, copy) void (^textFieldShouldReturn)(void);

    .m中只需要实现按钮的点击方法和对应的回调方法即可。
    这样好处是只需要引入头文件,修改一个属性即可实现自定义键盘,不会影响项目中其他的业务逻辑。

    self.textField = [[UITextField alloc] initWithFrame:CGRectMake(20, 100, SJSCREEN_WIDTH - 40, 40)];  
    self.textField.placeholder = @"input";
    self.textField.borderStyle = UITextBorderStyleBezel;
    self.textField.delegate = self;
    [self.view addSubview:self.textField];

    self.textField.sjKeyboardType = SJKeyboardTypeNumber;





    收起阅读 »

    回顾 | Jetpack WindowManager 更新

    在今年年初,我们发布了 Jetpack WindowManager 库 alpha02 版本,这是一个较为重大的版本更新,并且包含部分已弃用的 API (目前已经发布到 1.0.0-alpha09 版),本文将为您回顾这次版本更新的内容。 Jetpack W...
    继续阅读 »

    在今年年初,我们发布了 Jetpack WindowManager 库 alpha02 版本,这是一个较为重大的版本更新,并且包含部分已弃用的 API (目前已经发布到 1.0.0-alpha09 版),本文将为您回顾这次版本更新的内容。


    Jetpack WindowManager 库可帮助您构建能够感知折叠和铰链等新设备功能的应用,使用以前不存在的新功能。在开发 Jetpack WindowManager 库时,我们结合了开发者的反馈意见,并且在 Alpha 版本中持续迭代 API,以提供一个更干净完整的 API 界面。我们一直在关注 WindowManager 空间中的不同领域以提供更多的功能,我们引入了 WindowMetrics,以便您可以在 Android 4.1 (API 级别 16) 及以上版本使用这些在 Android 11 加入的新 API


    首版发布后,我们用了大量时间来分析开发者反馈,并在 alpha02 版本中进行了大量的更新,接下来我们来看在 alpha02 版本中更新的具体内容!


    新建一个 WindowManager


    Alpha02 版本提供了一个简单的构造函数,这个构造函数只有一个参数,参数指向一个可见实体 (比如当前显示的 Activity) 的 Context:


    val windowManager = WindowManager(context: Context)

    原有的构造函数 仍可使用,但已被标记为废弃:


    @Deprecated
    val windowManager = WindowManager(context: Context, windowBackend: WindowBackend?)

    当您想在一个常见的设备或模拟器上使用一个自定义的 WindowBackend 模拟一个可折叠设备时,可使用原有的构造函数进行测试。这个 样例工程 中的实现可以供您参考。


    在 alpha02 版本,您仍可给参数 WindowBackend 传参为 null,我们计划在未来的版本中将 WindowBackend 设置为必填参数,移除 deprecation 标志,以推动此接口在测试时使用。


    添加 DisplayFeature 弃用 DeviceState


    另一个重大变化是弃用了 DeviceState 类,同时也弃用了使用它通知您应用的回调。之所以这样做,是因为我们希望提供更加通用的 API,这些通用的 API 允许系统向您的应用返回所有可用的 DisplayFeature 实例,而不是定义全局的设备状态。我们在 alpha06 的版本中已经将 DeviceState 从公共 API 中移除,请改用 FoldingFeature。


    alpha02 版本引入了带有更新了回调协议的新 DisplayFeature 类,以在 DisplayFeature 更改时通知您的应用。您可以注册、反注册回调来使用这些方法:


    registerLayoutChangeCallback(@NonNull Executor executor, @NonNull Consumer<WindowLayoutInfo> callback)

    unregisterLayoutChangeCallback(@NonNull Consumer<WindowLayoutInfo> callback)

    WindowLayoutInfo 包含了位于 window 内的 DisplayFeature 实例列表。


    FoldingFeature 类实现了 DisplayFeature 接口,其中包含了有关下列类型功能的信息:


    TYPE_FOLD(折叠类型)

    TYPE_HINGE(铰链类型)

    设备可能的折叠状态如下:


    △ DisplayFeature 可能的状态: 完全展开、半开、翻转


    △ DisplayFeature 可能的状态: 完全展开、半开、翻转


    需要注意的是这里没有与 DeviceState 中 POSTURE_UNKNOWN 和 POSTURE_CLOSED 姿态对应的状态。


    要获取最新的状态信息,您可以使用已注册回调返回的 FoldingFeature 信息:


    class LayoutStateChangeCallback : Consumer<WindowLayoutInfo> {
    override fun accept(newLayoutInfo: WindowLayoutInfo) {
    // 检查 newLayoutInfo. getDisplayFeatures() 的返回值,
    // 看它是否为 FoldingFeature 实例,并获取其中的信息。
    }
    }

    如何使用这些信息,请参阅: github.com/android/use…


    更好的回调注册


    上述示例代码的回调 API 也更加健壮了。在之前版本中,如果应用在 window 可用之前注册回调,将会抛出异常。


    在 aplha02 版本中我们修改了上述的行为。您可在对您应用设计有用的任何时候,注册这些回调,库会在 window 可用时发送初始 WindowLayoutInfo。


    R8 规则


    我们在库中添加了 R8 的 "keep" 规则,以保留那些因为内部模块的组织架构而可能被删除的方法或类。这些规则会自动合并到应用最终的 R8 规则中,这样可以防止应用出现如 alpha01 版本上的崩溃。


    WindowMetrics


    由于历史的命名习惯和各种可能的 Window Manager 状态,在 Android 上获取当前 window 的尺寸信息比较困难。Android 11 中一些被废弃的方法 (例如 Display#getSize 和 Display#getMetrics) 和在 window 尺寸新的 API 的使用,都凸显了可折叠设备从全屏到多窗口和自适应窗口这一上升的趋势。为了简化这一过渡过程,我们在 Android 11 中增加了 WindowMetrics API


    在第一次布局完成之前,WindowMetrics 可以让您轻松获取当前 window 状态信息,和系统当前状态下最大 Window 尺寸信息。例如像 Surface Duo 这样的设备,设备会有一个默认的配置决定应用从哪一个屏幕启动,但是也可以跨过设备的铰链扩展到两块屏幕上。在默认的状态,'getMaximumWindowMetrics' 方法返回应用当前所在屏幕的边界信息。当应用被移动到处于跨屏状态,'getMaximumWindowMetrics' 方法返回反映新状态的边界信息。这些信息最早在 onCreate 期间就会提供,您的 Activity 可以利用这些信息进行计算或者尽早做出决定,以便在第一时间选择正确的布局。


    API 返回的结果不包括系统 inset 信息,比如状态栏或导航栏,这是由于目前支持的所有 Android 版本中,在第一次布局完成之前,这些值对应的区域都不可用。关于使用 ViewCompat 去获取系统可用 inset 信息,Chris Banes 的文章 - 处理视觉冲突|手势导航 (二) 是非常好的资源。API 返回的边界信息也不会对布局填充时可能发生变化的布局参数作出响应。


    要访问这些 API,您需要像上文说明的那样先获取一个 WindowManager 对象:


    val windowManager = WindowManager(context: Context)

    现在您就可以访问 WindowMetrics API,并可轻松获取当前 window 的尺寸以及最大尺寸信息。


    windowManager.currentWindowMetrics

    windowManager.maximumWindowMetrics

    例如,如果您的应用在手机和平板电脑上的布局或导航模式截然不同,那么可以在视图填充之前依赖此信息对布局做出选择。如果您认为用户会对布局的明显变化感到疑惑,您可以忽略当前 window 尺寸信息的变化,选择部分信息作为常量。在选择填充哪些之前,您可以使用 window 最大尺寸信息。


    尽管 Android 11 平台已经包含了在 onCreate 期间获取 inset 信息的 API,但是我们还没有将这个 API 添加到 WindowManager 库中,这是因为我们想了解这些功能中哪些对开发者有用。您可以积极反馈,以便我们了解在您第一次布局之前,需要知道哪些能够使编写布局更为简便的值或抽象。


    我们希望这些可以用在 Android 低版本上的 API 能够帮助您构建响应 window 尺寸变化的应用,同时帮助您替换上文提到的已废弃 API。


    联系我们


    我们非常希望得到您对这些 API 的反馈,尤其是您认为缺少的那些,或者可让您开发变得更轻松的那些反馈。有一些使用场景我们可能没有考虑到,所以希望您在 public tracker 上向我们提交 bug 或功能需求。


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

    Android so文件的加载原理

    so
    先说说so的编译类型 Android只支持3种cpu架构分为:arm,mips,x86,目前用的最多的是arm体系cpu,x86和mips体系的很少用到了。 arm体系中,又分32位和64位: armeabi/armeabi-v7a:这个架构是arm类型的,主...
    继续阅读 »



    1. 先说说so的编译类型
      Android只支持3种cpu架构分为:arm,mips,x86,目前用的最多的是arm体系cpu,x86和mips体系的很少用到了。
      arm体系中,又分32位和64位:

      armeabi/armeabi-v7a:这个架构是arm类型的,主要用于Android 4.0之后的,cpu是32位的,其中armeabi是相当老旧的一个版本, 缺少对浮点数的硬件支持,基本已经淘汰,可以不用考虑了。

      arm64-v8a:这个架构是arm类型的,主要是用于Android 5.0之后,cpu是64位的。平时项目中引入第三方的so文件时,第三方会根据cpu的架构编译成不同类型的so文件,项目引入这些so文件时,会将这些文件分别放入jniLibs目录下的arm64-v8a,armeabi-v7a等这些目录下,其实对于arm体系的so文件,没这个必要,因为arm体系是向下兼容的,比如32位的so文件是可以在64位的系统上运行的。Android上每启动一个app都会创建一个虚拟机,Android 64位的系统加载32位的so文件时,会创建一个64位的虚拟机的同时,还会创建一个32位的虚拟机,这样就能兼容32位的app应用了。鉴于兼容的原理,在app中,可以只保留armeabi-v7a版本的so文件就足够了。64位的操作系统会在32位的虚拟机上加载这个它。这样就极大的精简了app打包后的体积。虽然这样可以精简apk的体积,但是,在64位平台上运行32位版本的ART和Android组件,将丢失专为64位优化过的性能(ART,webview,media等等)所以,更好的方法是,为相应的abi打对应的apk包,这样就可以为不同abi版本生成不同的apk包。具体在build.gradle中的配置如下:



    android {

    ...

    splits {
    abi {
    enable true
    reset()
    include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
    universalApk true //generate an additional APK that contains all the ABIs
    }
    }

    // map for the version code
    project.ext.versionCodes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 5, 'mips64': 6, 'x86': 8, 'x86_64': 9]

    android.applicationVariants.all { variant ->
    // assign different version code for each output
    variant.outputs.each { output ->
    output.versionCodeOverride =
    project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * 1000000 + android.defaultConfig.versionCode
    }
    }
    }


    1. so的加载流程
      可以通过以下命令来查看手机的cpu型号(以OPPO R7手机为例),在AS中的Terminal窗口中,输入如下命令


      C:\Users\xg\Desktop\AndroidSkill>adb shell
    shell@hwmt7:/ $ getprop ro.product.cpu.abilist
    arm64-v8a,armeabi-v7a,armeabi

    手机支持的种类存在一个abiList 的集合中,有个前后顺序,比如我的手机,支持三种类型, abiList 的集合中就有三个元素,第一个元素是arm64-v8a ,第二个元素是armeabi-v7a,第三个元素是armeabi 。按照这个先后顺序,我们遍历jniLib 目录,如果这个目录下有arm64-v8a子目录并且里面有so文件,那么接下来将加载arm64-v8a下的所有so文件,就不再去看其他子目录(比如armeabi-v7a)了,以此类推。在我的手机上,如果arm64-v8a 下有a.so,armeabi-v7a下有a.so和b.so那么我的手机只会加载arm64-v8a下的a.so,而永远不会加载到b.so,这时候就会抛出找不到b.so的异常,这是由Android 中的so加载算法导致的。因此,为了节省apk的体积,我们只能保存一份so文件,那就是armeabi-v7a下的so文件。32位的arm手机,肯定能加载到armeabi-v7a下的so文件。64位的arm手机,想要加载32位的so文件,千万不要在arm64 -v8a目录下放置任何so文件。把so文件都放在armeabi-v7a目录下就可以加载到了。


    下面举个例子来说明上面so的加载过程:
    32位的arm手机,如果项目的jniLibs目录下存在如下的so文件,
    jniLibs/arm64-v8a/libmsc.so
    jniLibs/armeabi-v7a/libmsc.so
    当要加载msc这个so文件时,就会直接到areabi-v7a目录下找。找到就加载, 找不到就报 couldn’t find “libmsc.so”
    如果armeabi-v7a这个目录都不存在时,也报 couldn’t find “libmsc.so”


    64位的arm手机,如果项目的jniLibs目录下存在如下的so文件,
    jniLibs/arm64-v8a/libmsc.so
    jniLibs/armeabi-v7a/libmsc.so
    当要加载msc.so文件时,就先到arm64-v8a目录下找,找到后,就不会去其他目录下找了。
    如果arm64-v8a目录下未找到,则到armeabi-v7a目录下找,找到就使用,找不到就去其他目录找,依次类推,如果都找到不到就报 couldn’t find “libmsc.so”。
    这个查找过程可以看下图:
    在这里插入图片描述



    1. so的加载方式
      方式一:System.loadLibrary方法,加载jniLibs目录下的so文件。例如,jniLibs目录下的arm64-v8a目录下有一个libHello.so文件,那么加载这个so文件是:


         System.loadLibray("Hello");//注意,没有lib前缀

    方式二:使用System.load方法,加载任意路径下的so文件,需要传入一个参数,这个参数就是so文件所在的完整路径。这两种方式最终都是调用的底层的dlopen方法加载so文件。但是方式二,由于可以传入so的路径,这样就可以实现动态加载so文件。so的插件化,就是使用的这种方式。动态加载so文件时,有时会出现 dlopen failed:libXXX.so is 32-bit instead of 64 bit 的异常。出现这个异常的原因是,手机的操作系统是64位的,这样加载这个32位的so文件时,会默认使用64位的虚拟机去加载,这样就报了这个异常。解决这个问题的方式,可以先在jniLibs目录下armeabi-v7a目录下,放入一个很简单的32位的libStub.so文件,在动态加载插件的so文件时,先去加载这个jniLibs/armeabi-v7a目录下的libStub.so文件,这样就会创建一个32位的虚拟机,当加载插件的32位的so文件时,就会使用这个32位的虚拟机来加载插件的so文件,这样也就不会报错了。


    注意,每个abi目录下的so文件数量要相同,因为,如果,在arm64-v8a目录下,存在a.so文件,在armeabi-v7a目录下,存在a.so和b.so文件,如果是在64位的arm系统的手机上加载a.so和b.so文件,由于先找a.so文件会先到arm64-v8a目录下找,找到后,后续的其他so文件就会都在这个目录下找了,有arm64-v8a目录下没有b.so文件,这样就会报couldn’t find "b.so"文件异常。所以,要保持每个abi目录下的so文件个数一致。


    关于加载插件中的so文件,是通过先创建加载插件的DexClassLoader,将插件中的so文件的路径传递给DecClassLoader的构造函数的第三个参数,这样,后续使用这个DexClassLoader去加载插件中的类或方法,插件中这些类或者方法中去加载插件的so文件。


    ————————————————
    版权声明:本文为CSDN博主「hujin2017」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/hujin2017/article/details/102804883

    收起阅读 »