注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

集成环信IM自定义添加表情组

除了默认的兔斯基示例,想要自定义添加表情组,如何下手呢。今天手把手教你去哪里研究。1、iOS端添加自定义表情组先集成源码,然后找如下截图部分代码,这部分代码即为表情功能的逻辑,大家可以从此处着手,去实现自己需要的逻辑。2、Android端添加自定义表情组参考下...
继续阅读 »

除了默认的兔斯基示例,想要自定义添加表情组,如何下手呢。今天手把手教你去哪里研究。


1、iOS端添加自定义表情组

先集成源码,然后找如下截图部分代码,这部分代码即为表情功能的逻辑,大家可以从此处着手,去实现自己需要的逻辑。





2、Android端添加自定义表情组

参考下面这块代码添加表情组⬇️⬇️⬇️



初始化之后设置这个provider,根据表情id返回具体表情数据



其他的大家自己研究啦,如果还是没研究明白,欢迎留言~~

收起阅读 »

Android运行时权限终极方案,用PermissionX

痛点在哪里?没有人愿意编写处理 Android 运行时权限的代码,因为它真的太繁琐了。这是一项没有什么技术含量,但是你又不得不去处理的工作,因为不处理它程序就会崩溃。但如果处理起来比较简单也就算了,可事实上,Android 提供给我们的运行时权限 API 并不...
继续阅读 »

痛点在哪里?

没有人愿意编写处理 Android 运行时权限的代码,因为它真的太繁琐了。

这是一项没有什么技术含量,但是你又不得不去处理的工作,因为不处理它程序就会崩溃。但如果处理起来比较简单也就算了,可事实上,Android 提供给我们的运行时权限 API 并不友好。

以一个拨打电话的功能为例,因为 CALL_PHONE 权限是危险权限,所以在我们除了要在 AndroidManifest.xml 中声明权限之外,还要在执行拨打电话操作之前进行运行时权限处理才行。

权限声明如下:

然后,编写如下代码来进行运行时权限处理

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
makeCallBtn.setOnClickListener {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
call()
} else {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE), 1)
}
}
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
1 -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call()
} else {
Toast.makeText(this, "You denied CALL_PHONE permission", Toast.LENGTH_SHORT).show()
}
}
}
}

private fun call() {
try {
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
} catch (e: SecurityException) {
e.printStackTrace()
}
}

}


这段代码中真有正意义的功能逻辑就是 call() 方法中的内容,可是如果直接调用 call() 方法是无法实现拨打电话功能的,因为我们还没有申请 CALL_PHONE 权限。

那么整段代码其他的部分就都是在处理 CALL_PHONE 权限申请。可以看到,这里需要先判断用户是否已授权我们拨打电话的权限,如果没有的话则要进行权限申请,然后还要在 onRequestPermissionsResult() 回调中处理权限申请的结果,最后才能去执行拨打电话的操作。

你可能觉得,这也不算是很繁琐呀,代码量并不是很多。那是因为,目前我们还只是处理了运行时权限最简单的场景,而实际的项目环境中有着更加复杂的场景在等着我们。

比如说,你的 App 可能并不只是单单申请一个权限,而是需要同时申请多个权限。虽然 ActivityCompat.requestPermissions() 方法允许一次性传入多个权限名,但是你在 onRequestPermissionsResult() 回调中就需要判断哪些权限被允许了,哪些权限被拒绝了,被拒绝的权限是否影响到应用程序的核心功能,以及是否要再次申请权限。

而一旦牵扯到再次申请权限,就引出了一个更加复杂的问题。你申请的权限被用户拒绝过了一次,那么再次申请将很有可能再次被拒绝。为此,Android 提供了一个 shouldShowRequestPermissionRationale() 方法,用于判断是否需要向用户解释申请这个权限的原因,一旦 shouldShowRequestPermissionRationale() 方法返回 true,那么我们最好弹出一个对话框来向用户阐明为什么我们是需要这个权限的,这样可以增加用户同意授权的几率。

是不是已经觉得很复杂了?不过还没完,Android 系统还提供了一个 “拒绝,不要再询问” 的选项,如下图所示:

只要用户选择了这个选项,那么我们以后每次执行权限申请的代码都将会直接被拒绝。

可是如果我的某项功能就是必须要依赖这个权限才行呢?没有办法,你只能提示用户去应用程序设置当中手动打开权限,程序方面已无法进行操作。

可以看出,如果想要在项目中对运行时权限做出非常全面的处理,是一件相当复杂的事情。事实上,大部分的项目都没有将权限申请这块处理得十分恰当,这也是我编写 PermissionX 的理由。

PermissionX 的实现原理

在开始介绍 PermissionX 的具体用法之前,我们先来讨论一下它的实现原理。

其实之前并不是没有人尝试过对运行时权限处理进行封装,我之前在做直播公开课的时候也向大家演示过一种运行时权限 API 的封装过程。

但是,想要对运行时权限的 API 进行封装并不是一件容易的事,因为这个操作是有特定的上下文依赖的,一般需要在 Activity 中接收 onRequestPermissionsResult() 方法的回调才行,所以不能简单地将整个操作封装到一个独立的类中。

为此,也衍生出了一系列特殊的封装方案,比如将运行时权限的操作封装到 BaseActivity 中,或者提供一个透明的 Activity 来处理运行时权限等。

不过上述两种方案都不够轻量,因为改变 Activity 的继承结构这可是大事情,而提供一个透明的 Activty 则需要在 AndroidManifest.xml 中进行额外的声明。

现在,业内普遍比较认可使用另外一种小技巧来进行实现。是什么小技巧呢?回想一下,之前所有申请运行时权限的操作都是在 Activity 中进行的,事实上,Android 在 Fragment 中也提供了一份相同的 API,使得我们在 Fragment 中也能申请运行时权限。

但不同的是,Fragment 并不像 Activity 那样必须有界面,我们完全可以向 Activity 中添加一个隐藏的 Fragment,然后在这个隐藏的 Fragment 中对运行时权限的 API 进行封装。这是一种非常轻量级的做法,不用担心隐藏 Fragment 会对 Activity 的性能造成什么影响。

这就是 PermissionX 的实现原理了,书中其实也已经介绍过了这部分内容。但是,在其实现原理的基础之上,后期我又增加了很多新功能,让 PermissionX 变得更加强大和好用,下面我们就来学习一下 PermissionX 的具体用法。

基本用法

要使用 PermissionX 之前,首先需要将其引入到项目当中,如下所示

dependencies {
...
implementation 'com.permissionx.guolindev:permissionx:1.1.1'
}


我在写本篇文章时 PermissionX 的最新版本是 1.1.1,想要查看它的当前最新版本,请访问 PermissionX 的主页:github.com/guolindev/P…

PermissionX 的目的是为了让运行时权限处理尽可能的容易,因此怎么让 API 变得简单好用就是我优先要考虑的问题。

比如同样实现拨打电话的功能,使用 PermissionX 只需要这样写:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
makeCallBtn.setOnClickListener {
PermissionX.init(this)
.permissions(Manifest.permission.CALL_PHONE)
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
call()
} else {
Toast.makeText(this, "您拒绝了拨打电话权限", Toast.LENGTH_SHORT).show()
}
}
}
}

...

}


是的,PermissionX 的基本用法就这么简单。首先调用 init() 方法来进行初始化,并在初始化的时候传入一个 FragmentActivity 参数。由于 AppCompatActivity 是 FragmentActivity 的子类,所以只要你的 Activity 是继承自 AppCompatActivity 的,那么直接传入 this 就可以了。

接下来调用 permissions() 方法传入你要申请的权限名,这里传入 CALL_PHONE 权限。你也可以在 permissions() 方法中传入任意多个权限名,中间用逗号隔开即可。

最后调用 request() 方法来执行权限申请,并在 Lambda 表达式中处理申请结果。可以看到,Lambda 表达式中有 3 个参数:allGranted 表示是否所有申请的权限都已被授权,grantedList 用于记录所有已被授权的权限,deniedList 用于记录所有被拒绝的权限。

因为我们只申请了一个 CALL_PHONE 权限,因此这里直接判断:如果 allGranted 为 true,那么就调用 call() 方法,否则弹出一个 Toast 提示。

运行结果如下:

怎么样?对比之前的写法,是不是觉得运行时权限处理没那么繁琐了?

核心用法

然而我们目前还只是处理了最普通的场景,刚才提到的,假如用户拒绝了某个权限,在下次申请之前,我们最好弹出一个对话框来向用户解释申请这个权限的原因,这个又该怎么实现呢?

别担心,PermissionX 对这些情况进行了充分的考虑。

onExplainRequestReason() 方法可以用于监听那些被用户拒绝,而又可以再次去申请的权限。从方法名上也可以看出来了,应该在这个方法中解释申请这些权限的原因。

而我们只需要将 onExplainRequestReason() 方法串接到 request() 方法之前即可,如下所示:

PermissionX.init(this)
.permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
.onExplainRequestReason { deniedList ->
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
}
}


这种情况下,所有被用户拒绝的权限会优先进入 onExplainRequestReason() 方法进行处理,拒绝的权限都记录在 deniedList 参数当中。接下来,我们只需要在这个方法中调用 showRequestReasonDialog() 方法,即可弹出解释权限申请原因的对话框,如下所示:

PermissionX.init(this)
.permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
.onExplainRequestReason { deniedList ->
showRequestReasonDialog(deniedList, "即将重新申请的权限是程序必须依赖的权限", "我已明白", "取消")
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
}
}


showRequestReasonDialog() 方法接受 4 个参数:第一个参数是要重新申请的权限列表,这里直接将 deniedList 参数传入。第二个参数则是要向用户解释的原因,我只是随便写了一句话,这个参数描述的越详细越好。第三个参数是对话框上确定按钮的文字,点击该按钮后将会重新执行权限申请操作。第四个参数是一个可选参数,如果不传的话相当于用户必须同意申请的这些权限,否则对话框无法关闭,而如果传入的话,对话框上会有一个取消按钮,点击取消后不会重新进行权限申请,而是会把当前的申请结果回调到 request() 方法当中。

另外始终要记得将所有申请的权限都在 AndroidManifest.xml 中进行声明:

重新运行一下程序,效果如下图所示:

当前版本解释权限申请原因对话框的样式还无法自定义,1.3.0 版本当中已支持了自定义权限提醒对话框样式的功能,详情请参阅 PermissionX 重磅更新,支持自定义权限提醒对话框 。

当然,我们也可以指定要对哪些权限重新申请,比如上述申请的 3 个权限中,我认为 CAMERA 权限是必不可少的,而其他两个权限则可有可无,那么在重新申请的时候也可以只申请 CAMERA 权限:


PermissionX.init(this)   
.permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.ACCESS_FINE_LOCATION)
.onExplainRequestReason { deniedList ->
val filteredList = deniedList.filter {
it == Manifest.permission.CAMERA
}
showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白", "取消")
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
}
}


这样当再次申请权限的时候就只会申请 CAMERA 权限,剩下的两个权限最终会被传入到 request() 方法的 deniedList 参数当中。

解决了向用户解释权限申请原因的问题,接下来还有一个头疼的问题要解决:如果用户不理会我们的解释,仍然执意拒绝权限申请,并且还选择了拒绝且不再询问的选项,这该怎么办?通常这种情况下,程序层面已经无法再次做出权限申请,唯一能做的就是提示用户到应用程序设置当中手动打开权限。

更多用法

那么 PermissionX 是如何处理这种情况的呢?我相信绝对会给你带来惊喜。PermissionX 中还提供了一个 onForwardToSettings() 方法,专门用于监听那些被用户永久拒绝的权限。另外从方法名上就可以看出,我们可以在这里提醒用户手动去应用程序设置当中打开权限。代码如下所示:

PermissionX.init(this)
.permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
.onExplainRequestReason { deniedList ->
showRequestReasonDialog(deniedList, "即将重新申请的权限是程序必须依赖的权限", "我已明白", "取消")
}
.onForwardToSettings { deniedList ->
showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白", "取消")
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
}
}


可以看到,这里又串接了一个 onForwardToSettings() 方法,所有被用户选择了拒绝且不再询问的权限都会进行到这个方法中处理,拒绝的权限都记录在 deniedList 参数当中。

接下来,你并不需要自己弹出一个 Toast 或是对话框来提醒用户手动去应用程序设置当中打开权限,而是直接调用 showForwardToSettingsDialog() 方法即可。类似地,showForwardToSettingsDialog() 方法也接收 4 个参数,每个参数的作用和刚才的 showRequestReasonDialog() 方法完全一致,我这里就不再重复解释了。

showForwardToSettingsDialog() 方法将会弹出一个对话框,当用户点击对话框上的我已明白按钮时,将会自动跳转到当前应用程序的设置界面,从而不需要用户自己慢慢进入设置当中寻找当前应用了。另外,当用户从设置中返回时,PermissionX 将会自动重新请求相应的权限,并将最终的授权结果回调到 request() 方法当中。效果如下图所示:

同样,1.3.0 版本也支持了自定义这个对话框样式的功能,详情请参阅 PermissionX 重磅更新,支持自定义权限提醒对话框 。

PermissionX 最主要的功能大概就是这些,不过我在使用一些 App 的时候发现,有些 App 喜欢在第一次请求权限之前就先弹出一个对话框向用户解释自己需要哪些权限,然后才会进行权限申请。这种做法是比较提倡的,因为用户同意授权的概率会更高。

那么 PermissionX 中要如何实现这样的功能呢?

其实非常简单,PermissionX 还提供了一个 explainReasonBeforeRequest() 方法,只需要将它也串接到 request() 方法之前就可以了,代码如下所示:

PermissionX.init(this)
.permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
.explainReasonBeforeRequest()
.onExplainRequestReason { deniedList ->
showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白")
}
.onForwardToSettings { deniedList ->
showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白")
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
}
}


这样,当每次请求权限时,会优先进入 onExplainRequestReason() 方法,弹出解释权限申请原因的对话框,用户点击我已明白按钮之后才会执行权限申请。效果如下图所示:

不过,你在使用 explainReasonBeforeRequest() 方法时,其实还有一些关键的点需要注意。

第一,单独使用 explainReasonBeforeRequest() 方法是无效的,必须配合 onExplainRequestReason() 方法一起使用才能起作用。这个很好理解,因为没有配置 onExplainRequestReason() 方法,我们怎么向用户解释权限申请原因呢?

第二,在使用 explainReasonBeforeRequest() 方法时,如果 onExplainRequestReason() 方法中编写了权限过滤的逻辑,最终的运行结果可能和你期望的会不一致。这一点可能会稍微有点难理解,我用一个具体的示例来解释一下。

观察如下代码:

PermissionX.init(this)
.permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
.explainReasonBeforeRequest()
.onExplainRequestReason { deniedList ->
val filteredList = deniedList.filter {
it == Manifest.permission.CAMERA
}
showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白")
}
...


这里在 onExplainRequestReason() 方法中编写了刚才用到的权限过滤逻辑,当有多个权限被拒绝时,我们只重新申请 CAMERA 权限。

在没有加入 explainReasonBeforeRequest() 方法时,一切都可以按照我们所预期的那样正常运行。但如果加上了 explainReasonBeforeRequest() 方法,在执行权限请求之前会先进入 onExplainRequestReason() 方法,而这里将除了 CAMERA 之外的其他权限都过滤掉了,因此实际上 PermissionX 只会请求 CAMERA 这一个权限,剩下的权限将完全不会尝试去请求,而是直接作为被拒绝的权限回调到最终的 request() 方法当中。

效果如下图所示:

针对于这种情况,PermissionX 在 onExplainRequestReason() 方法中提供了一个额外的 beforeRequest 参数,用于标识当前上下文是在权限请求之前还是之后,借助这个参数在 onExplainRequestReason() 方法中执行不同的逻辑,即可很好地解决这个问题,示例代码如下:

PermissionX.init(this)
.permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
.explainReasonBeforeRequest()
.onExplainRequestReason { deniedList, beforeRequest ->
if (beforeRequest) {
showRequestReasonDialog(deniedList, "为了保证程序正常工作,请您同意以下权限申请", "我已明白")
} else {
val filteredList = deniedList.filter {
it == Manifest.permission.CAMERA
}
showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白")
}
}
...


可以看到,当 beforeRequest 为 true 时,说明此时还未执行权限申请,那么我们将完整的 deniedList 传入 showRequestReasonDialog() 方法当中。

而当 beforeRequest 为 false 时,说明某些权限被用户拒绝了,此时我们只重新申请 CAMERA 权限,因为它是必不可少的,其他权限则可有可无。

最终运行效果如下:

代码下载:XPermission-master.zip

收起阅读 »

Android自定义View 雷达扫描效果

最近在做一个项目,其中有一个页面是要做一个类似于雷达扫描的效果。于是找了其他应用的类似的效果参考一下,刚好我使用的华为手机里的手机管家--病毒查杀页面就是一个雷达扫描的效果。而且看它的样式也挺不错的,刚好符合我的要求。所以就决定仿照它的样式自定义一个类似效果的...
继续阅读 »

最近在做一个项目,其中有一个页面是要做一个类似于雷达扫描的效果。于是找了其他应用的类似的效果参考一下,刚好我使用的华为手机里的手机管家--病毒查杀页面就是一个雷达扫描的效果。而且看它的样式也挺不错的,刚好符合我的要求。所以就决定仿照它的样式自定义一个类似效果的RadarView。 这是华为手机管家的效果:

图片
我写完这个RadarView之后觉得这个View的实现虽然不难,却使用到了自定义属性、View的Measure、Paint、Canvas和坐标的计算等这些自定义View常用的知识,是一个不错的自定义View练习例子,所以决定写一篇博客把它记录起来。

由于我需要雷达的扫描效果,所以画中间的百分比数字。RadarView可以根据自己的需求配置View的主题颜色、扫描颜色、扫描速度、圆圈数量、是否显示水滴等功能样式,方便实现各种样式的情况。下面是自定义RadarView的代码。

public class RadarView extends View {

//默认的主题颜色
private int DEFAULT_COLOR = Color.parseColor("#91D7F4");

// 圆圈和交叉线的颜色
private int mCircleColor = DEFAULT_COLOR;
//圆圈的数量 不能小于1
private int mCircleNum = 3;
//扫描的颜色 RadarView会对这个颜色做渐变透明处理
private int mSweepColor = DEFAULT_COLOR;
//水滴的颜色
private int mRaindropColor = DEFAULT_COLOR;
//水滴的数量 这里表示的是水滴最多能同时出现的数量。因为水滴是随机产生的,数量是不确定的
private int mRaindropNum = 4;
//是否显示交叉线
private boolean isShowCross = true;
//是否显示水滴
private boolean isShowRaindrop = true;
//扫描的转速,表示几秒转一圈
private float mSpeed = 3.0f;
//水滴显示和消失的速度
private float mFlicker = 3.0f;

private Paint mCirclePaint;// 圆的画笔
private Paint mSweepPaint; //扫描效果的画笔
private Paint mRaindropPaint;// 水滴的画笔

private float mDegrees; //扫描时的扫描旋转角度。
private boolean isScanning = false;//是否扫描

//保存水滴数据
private ArrayList mRaindrops = new ArrayList<>();

public RadarView(Context context) {
super(context);
init();
}

public RadarView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
getAttrs(context, attrs);
init();
}

public RadarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
getAttrs(context, attrs);
init();
}

/**
* 获取自定义属性值
*
* @param context
* @param attrs
*/

private void getAttrs(Context context, AttributeSet attrs) {
if (attrs != null) {
TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.RadarView);
mCircleColor = mTypedArray.getColor(R.styleable.RadarView_circleColor, DEFAULT_COLOR);
mCircleNum = mTypedArray.getInt(R.styleable.RadarView_circleNum, mCircleNum);
if (mCircleNum < 1) {
mCircleNum = 3;
}
mSweepColor = mTypedArray.getColor(R.styleable.RadarView_sweepColor, DEFAULT_COLOR);
mRaindropColor = mTypedArray.getColor(R.styleable.RadarView_raindropColor, DEFAULT_COLOR);
mRaindropNum = mTypedArray.getInt(R.styleable.RadarView_raindropNum, mRaindropNum);
isShowCross = mTypedArray.getBoolean(R.styleable.RadarView_showCross, true);
isShowRaindrop = mTypedArray.getBoolean(R.styleable.RadarView_showRaindrop, true);
mSpeed = mTypedArray.getFloat(R.styleable.RadarView_speed, mSpeed);
if (mSpeed <= 0) {
mSpeed = 3;
}
mFlicker = mTypedArray.getFloat(R.styleable.RadarView_flicker, mFlicker);
if (mFlicker <= 0) {
mFlicker = 3;
}
mTypedArray.recycle();
}
}

/**
* 初始化
*/

private void init() {
// 初始化画笔
mCirclePaint = new Paint();
mCirclePaint.setColor(mCircleColor);
mCirclePaint.setStrokeWidth(1);
mCirclePaint.setStyle(Paint.Style.STROKE);
mCirclePaint.setAntiAlias(true);

mRaindropPaint = new Paint();
mRaindropPaint.setStyle(Paint.Style.FILL);
mRaindropPaint.setAntiAlias(true);

mSweepPaint = new Paint();
mSweepPaint.setAntiAlias(true);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//设置宽高,默认200dp
int defaultSize = dp2px(getContext(), 200);
setMeasuredDimension(measureWidth(widthMeasureSpec, defaultSize),
measureHeight(heightMeasureSpec, defaultSize));
}

/**
* 测量宽
*
* @param measureSpec
* @param defaultSize
* @return
*/

private int measureWidth(int measureSpec, int defaultSize) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = defaultSize + getPaddingLeft() + getPaddingRight();
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
result = Math.max(result, getSuggestedMinimumWidth());
return result;
}

/**
* 测量高
*
* @param measureSpec
* @param defaultSize
* @return
*/

private int measureHeight(int measureSpec, int defaultSize) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = defaultSize + getPaddingTop() + getPaddingBottom();
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
result = Math.max(result, getSuggestedMinimumHeight());
return result;
}

@Override
protected void onDraw(Canvas canvas) {

//计算圆的半径
int width = getWidth() - getPaddingLeft() - getPaddingRight();
int height = getHeight() - getPaddingTop() - getPaddingBottom();
int radius = Math.min(width, height) / 2;

//计算圆的圆心
int cx = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
int cy = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;

drawCircle(canvas, cx, cy, radius);

if (isShowCross) {
drawCross(canvas, cx, cy, radius);
}

//正在扫描
if (isScanning) {
if (isShowRaindrop) {
drawRaindrop(canvas, cx, cy, radius);
}
drawSweep(canvas, cx, cy, radius);
//计算雷达扫描的旋转角度
mDegrees = (mDegrees + (360 / mSpeed / 60)) % 360;

//触发View重新绘制,通过不断的绘制实现View的扫描动画效果
invalidate();
}
}

/**
* 画圆
*/

private void drawCircle(Canvas canvas, int cx, int cy, int radius) {
//画mCircleNum个半径不等的圆圈。
for (int i = 0; i < mCircleNum; i++) {
canvas.drawCircle(cx, cy, radius - (radius / mCircleNum * i), mCirclePaint);
}
}

/**
* 画交叉线
*/

private void drawCross(Canvas canvas, int cx, int cy, int radius) {
//水平线
canvas.drawLine(cx - radius, cy, cx + radius, cy, mCirclePaint);

//垂直线
canvas.drawLine(cx, cy - radius, cx, cy + radius, mCirclePaint);
}

/**
* 生成水滴。水滴的生成是随机的,并不是每次调用都会生成一个水滴。
*/

private void generateRaindrop(int cx, int cy, int radius) {

// 最多只能同时存在mRaindropNum个水滴。
if (mRaindrops.size() < mRaindropNum) {
// 随机一个20以内的数字,如果这个数字刚好是0,就生成一个水滴。
// 用于控制水滴生成的概率。
boolean probability = (int) (Math.random() * 20) == 0;
if (probability) {
int x = 0;
int y = 0;
int xOffset = (int) (Math.random() * (radius - 20));
int yOffset = (int) (Math.random() * (int) Math.sqrt(1.0 * (radius - 20) * (radius - 20) - xOffset * xOffset));

if ((int) (Math.random() * 2) == 0) {
x = cx - xOffset;
} else {
x = cx + xOffset;
}

if ((int) (Math.random() * 2) == 0) {
y = cy - yOffset;
} else {
y = cy + yOffset;
}

mRaindrops.add(new Raindrop(x, y, 0, mRaindropColor));
}
}
}

/**
* 删除水滴
*/

private void removeRaindrop() {
Iterator iterator = mRaindrops.iterator();

while (iterator.hasNext()) {
Raindrop raindrop = iterator.next();
if (raindrop.radius > 20 || raindrop.alpha < 0) {
iterator.remove();
}
}
}

/**
* 画雨点(就是在扫描的过程中随机出现的点)。
*/

private void drawRaindrop(Canvas canvas, int cx, int cy, int radius) {
generateRaindrop(cx, cy, radius);
for (Raindrop raindrop : mRaindrops) {
mRaindropPaint.setColor(raindrop.changeAlpha());
canvas.drawCircle(raindrop.x, raindrop.y, raindrop.radius, mRaindropPaint);
//水滴的扩散和透明的渐变效果
raindrop.radius += 1.0f * 20 / 60 / mFlicker;
raindrop.alpha -= 1.0f * 255 / 60 / mFlicker;
}
removeRaindrop();
}

/**
* 画扫描效果
*/

private void drawSweep(Canvas canvas, int cx, int cy, int radius) {
//扇形的透明的渐变效果
SweepGradient sweepGradient = new SweepGradient(cx, cy,
new int[]{Color.TRANSPARENT, changeAlpha(mSweepColor, 0), changeAlpha(mSweepColor, 168),
changeAlpha(mSweepColor, 255), changeAlpha(mSweepColor, 255)
}, new float[]{0.0f, 0.6f, 0.99f, 0.998f, 1f});
mSweepPaint.setShader(sweepGradient);
//先旋转画布,再绘制扫描的颜色渲染,实现扫描时的旋转效果。
canvas.rotate(-90 + mDegrees, cx, cy);
canvas.drawCircle(cx, cy, radius, mSweepPaint);
}

/**
* 开始扫描
*/

public void start() {
if (!isScanning) {
isScanning = true;
invalidate();
}
}

/**
* 停止扫描
*/

public void stop() {
if (isScanning) {
isScanning = false;
mRaindrops.clear();
mDegrees = 0.0f;
}
}

/**
* 水滴数据类
*/

private static class Raindrop {

int x;
int y;
float radius;
int color;
float alpha = 255;

public Raindrop(int x, int y, float radius, int color) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
}

/**
* 获取改变透明度后的颜色值
*
* @return
*/

public int changeAlpha() {
return RadarView.changeAlpha(color, (int) alpha);
}

}

/**
* dp转px
*/

private static int dp2px(Context context, float dpVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dpVal, context.getResources().getDisplayMetrics());
}

/**
* 改变颜色的透明度
*
* @param color
* @param alpha
* @return
*/

private static int changeAlpha(int color, int alpha) {
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
return Color.argb(alpha, red, green, blue);
}
}

自定义属性:在res/values下创建attrs.xml


























效果图:

效果图

代码下载:mirrors-XHRadarView-master.zip
收起阅读 »

Android右侧边栏滚动选择

Android右侧边栏滚动选择涉及到的内容:首先会ListView或RecyclerView的多布局。自定义View右侧拼音列表,简单地绘制并设立监听事件等。会使用pinyin4.jar第三方包来识别汉字的首字母(单独处理重庆多音问题)。将全部的城市列表转化为...
继续阅读 »

Android右侧边栏滚动选择

涉及到的内容:

  1. 首先会ListView或RecyclerView的多布局。

  2. 自定义View右侧拼音列表,简单地绘制并设立监听事件等。

  3. 会使用pinyin4.jar第三方包来识别汉字的首字母(单独处理重庆多音问题)。

  4. 将全部的城市列表转化为{A a开头城市名...,B b开头城市名...}的格式,这个数据转化是重点 !!!

  5. 将第三步获取的数据来多布局展示出来。

难点:

1、RecyclerView的滑动问题

2、RecyclerView的点击问题

3、绘制SideBar

先来看个图,看是不是你想要的

1557800237747.gif

实现思路

根据城市和拼音列表,可以想到多布局,这里无非是把城市名称按其首字母进行排列后再填充列表,如果给你一组数据{A、城市1、城市2、B、城市3、城市4...}这样的数据让你填充你总会吧,无非就是两种布局,将拼音和汉字的背景设置不同就行;右侧是个自定义布局,别说你不会自定义布局,不会也行,这个很简单,无非是平分高度,通过drawText()绘制字母,然后进行滑动监听,右侧滑动或点击到哪里,左侧列表相应进行滚动即可。

其实原先我已经通过ListView做过了,这次回顾使用RecyclerView再实现一次,发现还遇到了一些新东西,带你们看看。这次没有使用BaseQuickAdapter,使用多了都忘记原始的代码怎么敲了话不多说开撸吧

1. 确定数据格式

首先我们需要确定下Bean的数据格式,毕竟涉及到多布局

public class ItemBean {

private String itemName;//城市名或者字母A...
private String itemType;//类型,区分是首字母还是城市名,是首字母的写“head”,不是的填入其它字母都行

// 标记 拼音头,head为0
public static final int TYPE_HEAD = 0;
// 标记 城市名
public static final int TYPE_CITY = 1;

public int getType() {
if (itemType.equals("head")) {
return TYPE_HEAD;
} else {
return TYPE_CITY;
}
}
......Get Set方法
}

可以看到有两个字段,一个用来显示城市名或者字母,另一个用来区分是城市还是首字母。这里定义了个getType()方法,为字母的话返回0,城市名返回1

2. 整理数据

一般我们准备的数据都是这样的


"mycityarray">
北京市
上海市
广州市
天津市
石家庄市
唐山市
秦皇岛市
邯郸市
邢台市
保定市
张家口市
承德市市
沧州市
廊坊市
衡水市
......



想要得到我们那样的数据,需要先获取这些城市名的首字母然后进行排序,这里我使用pinyin4j-2.5.0.jar进行汉字到拼音的转化,jar下载地址

2.1 编写工具类

public class HanziToPinYin {
/**
* 如果字符串string是汉字,则转为拼音并返回,返回的是首字母
*
@param string
*
@return
*/

public static char toPinYin(String string){
HanyuPinyinOutputFormat hanyuPinyin = new HanyuPinyinOutputFormat();
hanyuPinyin.setCaseType(HanyuPinyinCaseType.UPPERCASE);
hanyuPinyin.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
hanyuPinyin.setVCharType(HanyuPinyinVCharType.WITH_U_UNICODE);
String[] pinyinArray=null;
char hanzi = string.charAt(0);
try {
//是否在汉字范围内
if(hanzi>=0x4e00 && hanzi<=0x9fa5){
pinyinArray = PinyinHelper.toHanyuPinyinStringArray(hanzi, hanyuPinyin);
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
e.printStackTrace();
}
//将获取到的拼音返回,只返回其首字母
return pinyinArray[0].charAt(0);
}
}

2.2 整理数据

private List cityList;      //给定的所有的城市名
private List itemList; //整理后的所有的item子项,可能是城市、可能是字母

//初始化数据,将所有城市进行排序,且加上字母和它们一起形成新的集合
private void initData(){

itemList = new ArrayList<>();
//获取所有的城市名
String[] cityArray = getResources().getStringArray(R.array.mycityarray);
cityList = Arrays.asList(cityArray);
//将所有城市进行排序,排完后cityList内所有的城市名都是按首字母进行排序的
Collections.sort(cityList, new CityComparator());

//将剩余的城市加进去
for (int i = 0; i < cityList.size(); i++) {

String city = cityList.get(i);
String letter = null; //当前所属的字母

if (city.contains("重庆")) {
letter = HanziToPinYin.toPinYin("崇庆") + "";
} else {
letter = HanziToPinYin.toPinYin(cityList.get(i)) + "";
}

if (letter.equals(currentLetter)) { //在A字母下,属于当前字母
itemBean = new ItemBean();
itemBean.setItemName(city); //把汉字放进去
itemBean.setItemType(letter); //这里放入其它不是“head”的字符串就行
itemList.add(itemBean);
} else { //不在当前字母下,先将该字母取出作为独立的一个item
//添加标签(B...)
itemBean = new ItemBean();
itemBean.setItemName(letter); //把首字母进去
itemBean.setItemType("head"); //把head标签放进去
currentLetter = letter;
itemList.add(itemBean);

//添加城市
itemBean = new ItemBean();
itemBean.setItemName(city); //把汉字放进去
itemBean.setItemType(letter); //把拼音放进去
itemList.add(itemBean);
}
}
}

经过以上步骤就将原先的数据整理成了以下形式排列的一组数据

{
{itemName:"A",itemType:"head"}
{itemName:"阿拉善盟",itemType:"A"}
{itemName:"安抚市",itemType:"A"}
...
{itemName:"巴中市",itemType:"B"}
{itemName:"白山市",itemType:"B"}
....
}

等等,上面有个Collections.sort(cityList, new CityComparator());letter = HanziToPinYin.toPinYin("崇庆") + "";你可能还会有疑惑,我就来多几嘴 因为pinyin4j.jar这个jar包在将汉字转为拼音的时候,会将重庆的拼音转为zhongqin,所以在排序和获取首字母的时候都需要单独处理

public class CityComparator implements Comparator<String> {

private RuleBasedCollator collator;

public CityComparator() {
collator = (RuleBasedCollator) Collator.getInstance(Locale.CHINA);
}

@Override
public int compare(String lhs, String rhs) {

lhs = lhs.replace("重庆", "崇庆");
rhs = rhs.replace("重庆", "崇庆");
CollationKey c1 = collator.getCollationKey(lhs);
CollationKey c2 = collator.getCollationKey(rhs);

return c1.compareTo(c2);
}
}

这里先指定RuleBasedCollator语言环境为CHINA,然后在compare()比较方法里,如果遇到两边有"重庆"的字符串,就将其替换为”崇庆“,然后通过getCollationKey()获取首个字符然后进行比较。

letter = HanziToPinYin.toPinYin("崇庆") + "";获取首字母的时候也是同样,不是获取"重庆"的首字母而是"崇庆"的首字母。

看到这样的一组数据你总会根据多布局来给RecyclerView填充数据了吧

3. RecyclerView填充数据

既然涉及到多布局,那么有几种布局就该有几个ViewHolder,这次我将采用原始的写法,不用BaseQuickAdapter,那个太方便搞得我原始的都不会写了

新建CityAdapter类,让这个适配器继承自RecyclerView.Adapter,并将泛型指定为RecyclerView.ViewHolder,其代表我们在CityAdapter中定义的内部类

public class CityAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

......
//字母头
public static class HeadViewHolder extends RecyclerView.ViewHolder {
private TextView tvHead;
public HeadViewHolder(View itemView) {
super(itemView);
tvHead = itemView.findViewById(R.id.tv_item_head);
}
}

//城市
public static class CityViewHolder extends RecyclerView.ViewHolder {

private TextView tvCity;
public CityViewHolder(View itemView) {
super(itemView);
tvCity = itemView.findViewById(R.id.tv_item_city);
}
}
}

重写onCreateViewHolder()onBindViewHolder()getItemCount()方法,因为涉及多布局,还需重写getItemViewType()方法来区分是哪种布局

完整代码如下

public class CityAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
//数据项
private List dataList;
//点击事件监听接口
private OnRecyclerViewClickListener onRecyclerViewClickListener;

public void setOnItemClickListener(OnRecyclerViewClickListener onItemClickListener) {
this.onRecyclerViewClickListener = onItemClickListener;
}
public CityAdapter(List dataList) {
this.dataList = dataList;
}
//创建ViewHolder实例
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {

if (viewType == 0) { //Head头字母名称
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_head, viewGroup,false);
RecyclerView.ViewHolder headViewHolder = new HeadViewHolder(view);
return headViewHolder;
} else { //城市名
View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_city, viewGroup,false);
RecyclerView.ViewHolder cityViewHolder = new CityViewHolder(view);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onRecyclerViewClickListener != null) {
onRecyclerViewClickListener.onItemClickListener(v);
}
}
});
return cityViewHolder;
}
}
//对子项数据进行赋值
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {

int itemType = dataList.get(position).getType();
if (itemType == 0) {
HeadViewHolder headViewHolder = (HeadViewHolder) viewHolder;
headViewHolder.tvHead.setText(dataList.get(position).getItemName());
} else {
CityViewHolder cityViewHolder = (CityViewHolder) viewHolder;
cityViewHolder.tvCity.setText(dataList.get(position).getItemName());
}
}
//数据项个数
@Override
public int getItemCount() {
return dataList.size();
}
//区分布局类型
@Override
public int getItemViewType(int position) {
int type = dataList.get(position).getType();
return type;
}
//字母头
public static class HeadViewHolder extends RecyclerView.ViewHolder {
private TextView tvHead;
public HeadViewHolder(View itemView) {
super(itemView);
tvHead = itemView.findViewById(R.id.tv_item_head);
}
}
//城市
public static class CityViewHolder extends RecyclerView.ViewHolder {
private TextView tvCity;
public CityViewHolder(View itemView) {
super(itemView);
tvCity = itemView.findViewById(R.id.tv_item_city);
}
}
}

两种item布局都是只放了一个TextView控件

这里有两处自己碰到和当时使用ListView不同的地方:

1、RecyclerView没有setOnItemClickListener(),需要自己定义接口来实现 2、自己平时加载布局都直接是View view = LayoutInflater.from(context).inflate(R.layout.item_head, null);,也没发现什么问题,但此次就出现了Item子布局无法横向铺满父布局。 解决办法:将改为以下方式加载布局

View view = LayoutInflater.from(context).inflate(R.layout.item_head, viewGroup,false);

(如果遇到不能铺满状况也可能是RecyclerView没有明确宽高而是用权重代替的原因)

建立的监听器

public interface OnRecyclerViewClickListener {
void onItemClickListener(View view);
}


4. 绘制侧边字母栏

这里的自定义很简单,无非是定义画笔,然后在画布上通过drawText()方法来绘制Text即可。

4.1 首先定义类SideBar继承自View,重写构造方法,并在三个方法内调用自定义的init();方法来初始化画笔

public class SideBar extends View {
//画笔
private Paint paint;

public SideBar(Context context) {
super(context);
init();
}
public SideBar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SideBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
//初始化画笔工具
private void init() {
paint = new Paint();
paint.setAntiAlias(true);//抗锯齿
}
}

4.2 在onDraw()方法里绘制字母

public static String[] characters = new String[]{"❤", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
private int position = -1; //当前选中的位置
private int defaultTextColor = Color.parseColor("#D2D2D2"); //默认拼音文字的颜色
private int selectedTextColor = Color.parseColor("#2DB7E1"); //选中后的拼音文字的颜色

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

int height = getHeight(); //当前控件高度
int width = getWidth(); //当前控件宽度
int singleHeight = height / characters.length; //每个字母占的长度

for (int i = 0; i < characters.length; i++) {
if (i == position) { //当前选中
paint.setColor(selectedTextColor); //设置选中时的画笔颜色
} else { //未选中
paint.setColor(defaultTextColor); //设置未选中时的画笔颜色
}
paint.setTextSize(textSize); //设置字体大小

//设置绘制的位置
float xPos = width / 2 - paint.measureText(characters[i]) / 2;
float yPos = singleHeight * i + singleHeight;

canvas.drawText(characters[i], xPos, yPos, paint); //绘制文本
}
}

通过以上两步,右侧边栏就算绘制完成了,但这只是静态的,如果要实现侧边栏滑动的时候,我们还需要监听其触摸事件

4.3 定义触摸回调接口和设置监听器的方法

//设置触摸位置改变的监听器的方法
public void setOnTouchingLetterChangedListener(OnTouchingLetterChangedListener onTouchingLetterChangedListener) {
this.onTouchingLetterChangedListener = onTouchingLetterChangedListener;
}

//触摸位置更改的接口
public interface OnTouchingLetterChangedListener {
void onTouchingLetterChanged(int position);
}

4.4 触摸事件

@Override
public boolean onTouchEvent(MotionEvent event) {

int action = event.getAction();
float y = event.getY();
position = (int) (y / (getHeight() / characters.length)); //获取触摸的位置

if (position >= 0 && position < characters.length) {
//触摸位置变化的回调
onTouchingLetterChangedListener.onTouchingLetterChanged(position);

switch (action) {
case MotionEvent.ACTION_UP:
setBackgroundColor(Color.TRANSPARENT);//手指起来后的背景变化
position = -1;
invalidate();//重新绘制控件
if (text_dialog != null) {
text_dialog.setVisibility(View.INVISIBLE);
}
break;
default://手指按下
setBackgroundColor(touchedBgColor);
invalidate();
text_dialog.setText(characters[position]);//字母框的弹出
break;
}
} else {
setBackgroundColor(Color.TRANSPARENT);
if (text_dialog != null) {
text_dialog.setVisibility(View.INVISIBLE);
}
}
return true; //一定要返回true,表示拦截了触摸事件
}

具体的解释如代码所示,当手指起来时,position为-1,当手指按下,更改背景并弹出字母框(这里的字母框其实就是一个TextView,通过显示隐藏来表示其弹出)

5. Activity中使用

itemList数据填充那些就不写了,在前面整理数据那部分

//所有的item子项,可能是城市、可能是字母
private List itemList;
//目标项是否在最后一个可见项之后
private boolean mShouldScroll;
//记录目标项位置(要移动到的位置)
private int mToPosition;

@Override
protected void onCreate(Bundle savedInstanceState) {
//为左侧RecyclerView设立Item的点击事件
cityAdapter.setOnItemClickListener(this);

sideBar.setOnTouchingLetterChangedListener(new SideBar.OnTouchingLetterChangedListener() {
@Override
public void onTouchingLetterChanged(int position) {

String city_label = SideBar.characters[position]; //滑动到的字母
for (int i = 0; i < cityList.size(); i++) {
if (itemList.get(i).getItemName().equals(city_label)) {
moveToPosition(i); //直接滚过去
// smoothMoveToPosition(recyclerView,i); //平滑的滚动
tvDialog.setVisibility(View.VISIBLE);
break;
}
if (i == cityList.size() - 1) {
tvDialog.setVisibility(View.INVISIBLE);
}
}
}
});
}

//实战中可能会有选择完后此页面关闭,返回当前数据等操作,可在此处完成
@Override
public void onItemClickListener(View view) {
int position = recyclerView.getChildAdapterPosition(view);
Toast.makeText(view.getContext(), itemList.get(position).getItemName(), Toast.LENGTH_SHORT).show();
}

在使用ListView的时候,知道要移动到的位置position时,直接listView.setSelection(position)就可将当前的item移动到屏幕顶部,而RecyclerView的scrollToPosition(position)只是将item移动到屏幕内,所以需要我们通过scrollToPositionWithOffset()方法将其置顶

private void moveToPosition(int position) {
if (position != -1) {
recyclerView.scrollToPosition(position);
LinearLayoutManager mLayoutManager =
(LinearLayoutManager) recyclerView.getLayoutManager();
mLayoutManager.scrollToPositionWithOffset(position, 0);
}
}

6. 总结

再次说明下自己遇到的几个问题:

1、点击问题,ListViewsetOnItemClickListener()方法,而RecyclerView没有,需要建立接口进行监听。 2、滑动问题,listViewsetSelection(position)滑动可以直接将该项滑至屏幕顶部,而recyclerView的 smoothScrollToPosition(position);只是将其移动至屏幕内,需要再次进行处理。 3、listViewisEnable() 方法可以设置字母Item不能点击,而城市名Item可以点击,recycleView的实现(直接在设立点击事件的时候,是头部就不设立点击事件就行) 4、item不充满全屏,加载布局的原因


代码下载:AndroidSlidbar.zip

收起阅读 »

面试题:介绍一下 LiveData 的 postValue ?

很多面试官喜欢会就一个问题不断深入追问。 例如一个小小的 LiveData 的 postValue,就可能会问出一连串问题: postValue 与 setValue postValue 与 setValue 一样都是用来更新 LiveData 数据...
继续阅读 »

很多面试官喜欢会就一个问题不断深入追问。


例如一个小小的 LiveData 的 postValue,就可能会问出一连串问题:


image.png


postValue 与 setValue


postValuesetValue 一样都是用来更新 LiveData 数据的方法:



  • setValue 只能在主线程调用,同步更新数据

  • postValue 可在后台线程调用,其内部会切换到主线程调用 setValue


liveData.postValue("a");
liveData.setValue("b");

上面代码,a 在 b 之后才被更新。


postValue 收不到通知


postValue 使用不当,可能发生接收到数据变更的通知:



If you called this method multiple times before a main thread executed a posted task, only the last value would be dispatched.



如上,源码的注释中明确记载了,当连续调用 postValue 时,有可能只会收到最后一次数据更新通知。


梳理源码可以了解其中原由:


protected void postValue(T value) {
boolean postTask;
synchronized (mDataLock) {
postTask = mPendingData == NOT_SET;
mPendingData = value;
}
if (!postTask) {
return;
}
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

mPendingData 被成功赋值 value 后,post 了一个 Runnable


mPostValueRunnable 的实现如下:


private final Runnable mPostValueRunnable = new Runnable() {
@SuppressWarnings("unchecked")
@Override
public void run() {
Object newValue;
synchronized (mDataLock) {
newValue = mPendingData;
mPendingData = NOT_SET;
}
setValue((T) newValue);
}
};


  • postValue 将数据存入 mPendingDatamPostValueRunnable 在UI线程消费mPendingData


  • 在 Runnable 中 mPendingData 值还没有被消费之前,即使连续 postValue , 也不会 post 新的 Runnable


  • mPendingData 的生产 (赋值) 和消费(赋 NOT_SET) 需要加锁



这也就是当连续 postValue 时只会收到最后一次通知的原因。


源码梳理过了,但是为什么要这样设计呢?


为什么 Runnable 只 post 一次?


mPenddingData 中有数据不断更新时,为什么 Runnable 不是每次都 post,而是等待到最后只 post 一次?


一种理解是为了兼顾性能,UI只需显示最终状态即可,省略中间态造成的频发刷新。这或许是设计目的之一,但是一个更为合理的解释是:即使 post 多次也没有意义,所以只 post 一次即可


我们知道,对于 setValue 来说,连续调用多次,数据会依次更新:


如下,订阅方一次收到 a b 的通知


liveData.setValue("a");
liveData.setValue("b");

通过源码可知,dispatchingValue() 中同步调用 Observer#onChanged(),依次通知订阅方:


//setValue源码

@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}

但对于 postValue,如果当 value 变化时,我们立即post,而不进行阻塞


protected void postValue(T value) {
mPendingData = value;
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

private final Runnable mPostValueRunnable = new Runnable() {
public void run() {
setValue((T) mPendingData);
}
};

liveData.postValue("a")
liveData.postValue("b")

由于线程切换的开销,连续调用 postValue,收到通知只能是b、b,无法收到a。


因此,post 多次已无意义,一次即可。


为什么要加读写锁?


前面已经知道,是否 post 取决于对 mPendingData 的判断(是否为 NOT_SET)。因为要在多线程环境中访问 mPendingData ,不加读写锁无法保证其线程安全。


protected void postValue(T value) {
boolean postTask = mPendingData == NOT_SET; // --1
mPendingData = value; // --2
if (!postTask) {
return;
}
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

private final Runnable mPostValueRunnable = new Runnable() {
public void run() {
Object newValue = mPendingData;
mPendingData = NOT_SET; // --3
setValue((T) newValue);
}
};

如上,如果在 1 和 2 之间,执行了 3,则 2 中设置的值将无法得到更新


使用RxJava替换LiveData


如何避免在多线程环境下不漏掉任何一个通知? 比较好的思路是借助 RxJava 这样的流式框架,任何数据更新都以数据流的形式发射出来,这样就不会丢失了。


fun <T> Observable<T>.toLiveData(): LiveData<T> = RxLiveData(this)

class RxLiveData<T>(
private val observable: Observable<T>
) : LiveData<T>() {
private var disposable: Disposable? = null

override fun onActive() {
disposable = observable
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
setValue(it)
}, {
setValue(null)
})
}

override fun onInactive() {
disposable?.dispose()
}
}

最后


想要保证事件在线程切换过程中的顺序性和完整性,需要使用RxJava这样的流式框架。


有时候面试官会使用追问的形式来挖掘候选人的技术深度,所以大家在准备面试时要多问自己几个问什么,知其然并知其所以然。


当然,我也不赞同这种刨根问底式的拷问方式,尤其是揪着一些没有实用价值的细枝末节不放。所以本文也是提醒广大面试官,挖掘深度的同时要注意分寸,不能以将候选人难倒为目标来问问题。



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

基于FakerAndroid的一次il2cpp游戏逆向精修实录!!!零汇编零二进制纯编码实现

~~~格式优化整理~~~1、下载FakerAndroid工具包 下载地址:https://github.com/Efaker/FakerAndroid/releases 2、cmd切换到FakerAndroid.jar平级目录  [工具包和...
继续阅读 »

~~~格式优化整理~~~
1、下载FakerAndroid工具包
下载地址:https://github.com/Efaker/FakerAndroid/releases 
2、cmd切换到FakerAndroid.jar平级目录 
[工具包和要操作的Apk]

[工具包目录]

3、执行 java -jar FakerAndroid.jar fk <apkpath>生成AndroidStudio工程
[执行命令]

[等待命令执行完成]

4、查看Apk平级目录下面生成的AndroidStudio工程
[查看原安装包目录]

5、AndroidStudio直接打开生成的Android工程
[生成的Android项目工程目录结构]

6、等待加载完成直接运行项目(确认项目加载完成,部分Res或Manifest文件有问题的话需要手动修复一下,实测大部分的未做res混淆的Apk都是没有问题的)
[直接Run运行项目]

7、Java类调用之继承(意在演示Java层原有Java类调用)
[父类继承]

8、Java类调用之Api调用(意在演示Java层原有Java Api调用)
[父类Api调用]

9、Manifest入口Activity替换
[AndroidManifest入口Activity替换]

10、Java类替换(意在演示对原有Java类的直接替换)
[类替换之原类]

[类替换之自己编写的替换类]

11、定义Jni方法进行Hoook操作和il2cpp脚手架的调用
[Jni方法定义]

[HookApi和Il2cpp脚手架的使用]

12、对原il2cpp脚手架定义过的方法进行Hook替换
[Il2cpp脚手架中的UI回调函数替换以及Il2cpp脚手架中的Api调用]

[JniHook Btn]

13、最后上一下效果图,忘记说了,文章中所有图片的宽度都使用了1024px
[效果图]

收起阅读 »

前端智能化看"低代码/无代码"

概念 什么是低代码/无代码开发?业界对于低代码/无代码开发是否存在其他不同的理解? 行业里流行观点,低代码是更加易用的搭建系统,无代码是图形化和可视化编程。这种观点把低代码和无代码开发分别置于 UI 和逻辑两个环节,以工具属性定义搭建和可视化编程要解决的问题。...
继续阅读 »

概念


什么是低代码/无代码开发?业界对于低代码/无代码开发是否存在其他不同的理解?


行业里流行观点,低代码是更加易用的搭建系统,无代码是图形化和可视化编程。这种观点把低代码和无代码开发分别置于 UI 和逻辑两个环节,以工具属性定义搭建和可视化编程要解决的问题。另一种观点则是把低代码/无代码看作一个方法的两个阶段,就像对自动驾驶的 L0 ~ L5 共 6 个不同阶段一样,把我之前在:《人机协同的编程方式》 一文提出的人机协同编程的概念,划分为低代码/无代码两个阶段。较之第一种我更加认同第二种观点,不仅因为是我提出的,更因为第二种观点是以软件工程的统一视角定义、分析和解决问题,而第一种观点只是局部和过程的优化而非颠覆性创新。


如马云先生在香港对年轻人传授创业经验时讲到的,蒸汽机和电力解放了人类的体力,人工智能和机器学习解放了人类的脑力。马云先生在评价蒸汽机和电力带来的失业问题时讲到,人类在科技进步下从繁重的体力劳动中解放出来,逐步向脑力劳动过渡,这是人类社会的进步。今天“人机协同的编程方式”把软件工程从拼装 UI 和编写业务逻辑里解放出来,逐步向业务能力、基础能力、底层能力等高技术含量工作过渡。更多内容参考:《前端智能化:思维转变之路》


低代码开发和无代码开发之间的区别是什么?


接着上述所答,既然低代码和无代码属于“人机协同编程”的两个阶段,低代码就是阶段一、无代码则是阶段二,分别对应“人机协作”和“人机协同”。协作和协同最大的区别就是:心有灵犀。不论低代码还是无代码,均有服务的对象:用户。不论用户是程序员还是非编程人员,均有统一目标:生成代码。不论源码开发、低代码还是无代码,都是在用不同的方式描述程序,有代码、图形、DSL……等。“人机协作”的阶段,这些描述有各种限制、约束,应用的业务场景亦狭窄。“人机协同”的阶段,则限制、约束减少,应用的业务场景亦宽广。“心有灵犀”就是指:通过 AI 对描述进行学习和理解,从而减少限制和约束,适应更多业务场景。因此,传统低代码/无代码和“人机协同编程”生成代码相比,最大的不同就是有心和无心,机器有心而平台无心。


背景


低代码/无代码开发与软件工程领域的一些经典思想、方法和技术,例如软件复用与构件组装、软件产品线、DSL(领域特定语言)、可视化快速开发工具、可定制工作流,以及此前业界流行的中台等概念,之间是什么关系?


从库、框架、脚手架开始,软件工程就踏上了追求效率的道路。在这个道路之上,低代码、无代码的开发方式算是宏愿。复用、组件化和模块化、DSL、可视化、流程编排……都是在达成宏愿过程中的尝试,要么在不同环节、要么以不同方式,但都还在软件工程领域内思考。中台概念更多是在业务视角下提出的,软件工程和技术领域内类似的概念更多是叫:平台。不论中台还是平台,就不仅是在过程中的尝试,而是整体和系统的创新尝试。我提出前端智能化的“人机协同编程”应该同属于软件工程和技术领域,在类似中台的业务领域我提出“需求暨生产”的全新业务研发模式,则属于业务领域。这些概念之间无非:左右、上下、新旧关系而已。


此外,低代码/无代码开发与DevOps、云计算与云原生架构之间又是什么样的关系?


DevOps、云计算……都属于基础技术,基础技术的变化势必带来上层应用层技术变化。没有云计算的容器化、弹性缩扩容,做分布式系统是很困难的,尤其在 CI/CD、部署、运维、监控、调优……等环节更甚,什么南北分布、异地多活、平行扩展、高可用……都需要去关注。但是,云计算和DevOps等基础技术的发展,内化并自动化解决了上述问题,大大降低了关注和使用成本,这就是心有灵犀,在这样的基础技术之上构建应用层技术,限制少、约束小还能适应各种复杂场景。


思想方法


支撑低代码/无代码开发的核心技术是什么?


我认为低代码/无代码开发的核心技术,过去是“复用”,今天是 AI 驱动的“人机协同编程”。过去的低代码/无代码开发多围绕着提升研发效能入手,今天 AI 驱动的“人机协同编程”则是围绕着提升交付效率入手。因此,低代码/无代码开发以“人机协同编程”为主要实现手段的话,AI 是其核心技术。


低代码/无代码开发的火热是软件开发技术上的重要变革和突破,还是经典软件工程思想、方法和技术随着技术和业务积累的不断发展而焕发出的新生机?


计算机最初只在少数人掌握,如今,几乎人人手持一台微型计算机:智慧手机。当初为程序员和所谓“技术人员”的专利,而今,几乎人人都会操作和使用计算机。然而,人们对计算机的操作是间接的,需要有专业的人士和企业提前编写软件,人们通过软件使用计算机的各种功能。随着计算机算力和功能的不断发展,随着社会的数字化和信息化,今天的人们越来越难以被提前定制好的软件所满足。低代码/无代码开发则赋予人们创造软件的能力,进而帮助人们低成本、即时、高效的直接生产符合自己需求的软件,进而操作众多复杂的电子设备和数字世界建立联结。我认为,这是不可逆的趋势,也是低代码/无代码开发的大方向。


现状进展


低代码/无代码开发已经发展到什么程度?


image.png


imgcook



  • 2w 多用户、6w 多模块、 0 前端参与研发的双十一等大促营销活动、70% 阿里前端在使用

  • 79.26% 无人工参与的线上代码可用率、90.9% 的还原度、Icon 识别准确率 83%、组件识别 85%、布局还原度 92.1%、布局人工修改概率 75%

  • 研发效率提升 68%


uicook


-营销活动和大促场景 ui 智能生成比例超过 90% -日常频道导购业务 ui 智能生成覆盖核心业务



  • 纯 ui 智能化和个性化带来的业务价值提升超过 8%


bizcook


初步完成基于 NLP 的需求标注和理解系统 初步完成基于 NLP 的服务注册和理解系统 初步完成基于 NLP 的胶水层业务逻辑代码生成能力


reviewcook



  • 针对资损防控自动化扫描、CV 和 AI 自动化识别资损风险和舆情问题

  • 和测试同学共建的 UI 自动化测试、数据渲染和 Mock 驱动的业务自动化验证

  • 和工程团队共建的 AI Codereview 基于对代码的分析和理解,结合线上 Runtime 的识别和分析,自动化发现问题、定位问题,提升 Codereview 的效率和质量


datacook



  • 社区化运营开源项目,合并 Denfo.js 同其作者共同设立 Datacook 项目,全链路、端到端解决 AI 领域数据采集、存储、处理问题,尤其在海量数据、数据集组织、数据质量评估等深度学习和机器学习领域的能力比肩 HDF5、Pandas……等 Python 专业 LIbrary

  • Google Tensorflow.js 团队合作开发维护 TFData library ,作为 Datacook 的核心技术和基础,共同构建数据集生态和数据集易用性


pipcook



  • 开源了 github.com/alibaba/pip… 纯前端机器学习框架

  • 利用 Boa 打通 Python 技术生态,原生支持 import Python 流行的包和库,原生支持 Python 的数据类型和数据结构,方便跨语言共享数据和调用 API

  • 利用 Pipcook Cloud 打通流行的云计算平台,帮助前端智能化实现 CDML,形成数据和算法工程闭环,帮助开发者打造工业级可用的服务和在线、离线算法能力


有哪些成熟的低代码/无代码开发平台?


image.png image.png image.png


低代码/无代码开发能够在多大程度上改变当前的软件开发方式?


随着计算机算力和功能的不断发展,随着社会的数字化和信息化,今天的人们越来越难以被提前定制好的软件所满足。低代码/无代码开发则赋予人们创造软件的能力,进而帮助人们低成本、即时、高效的直接生产符合自己需求的软件,进而操作众多复杂的电子设备和数字世界建立联结。我认为,这是不可逆的趋势,也是低代码/无代码开发的大方向。最终,软件开发势必从专业程序员手里转向普罗大众,成为今天操作计算机一样的基本生存技能之一。因此,软件开发方式将带来本质变化,从完整的交付转向局部交付、从业务整体交付转向业务能力交付……


展望未来


低代码/无代码开发未来发展的方向是什么?


要我说,低代码/无代码开发未来发展的方向一定是:AI 驱动的“人机协同编程”,将完整开发一个软件变成提供局部的软件功能,类似 Apple 的“捷径”一样,由用户决定这些局部软件功能如何组装成适合用户的软件并交付最终用户。AI 驱动提供两个方面的价值:


降低开发成本


以往开发软件的时候,要有 PRD、交互稿、设计稿、设计文档……等一系列需求规格说明,然后,根据这些需求规格利用技术和工程手段进行实现。然而,低代码/无代码开发交付的是局部功能和半成品,会被无法枚举的目的和环境所使用,既然无法枚举,就不能用 Swith……Case 的方式编写代码,否则会累死。


AI 的特点就是基于特征和环境进行预测,预测的基础是对模式和本质的理解。就像 AI 识别一只猫,不管这个猫在什么环境、什么光照条件下,也不管这只猫是什么品种,AI 都能够以超过人类的准确度识别。试想,作为一个程序员用程序判断一只猫的开发成本何其高?


降低使用成本


今天的搭建体系,本质上是把编程过程用搭建的思想重构了一遍,工作的内容并没有发生变化,成本从程序员转嫁到运营、产品、设计师的身上。这还是其次,今天的搭建平台都是技术视角出发,充斥着运营、产品、设计等非技术人员一脸懵逼的概念,花在答疑解惑和教他们如何在页面上定制一个搜索框的时间,比自己和他们沟通后源码实现的时间还要长,而且经常在撸代码的时候被打断……


基于 AI 的“人机协同编程”不需要透出任何技术概念,运营、产品、设计……等非技术人员也不改变其工作习惯,都用自己熟悉的工具和自己熟悉的概念描述自己的需求,AI 负责对这些需求进行识别和理解,再转换成编程和技术工程领域的概念,进而生成代码并交付,从而大幅度降低使用成本。


举个例子:如果你英文写作能力不好,你拿着朗道词典一边翻译一边拼凑单词写出来的英文文章质量高呢?还是用中文把文章写好,再使用 Google 翻译整篇转换成英文的文章质量高?你自己试试就知道了。究其原因,你在自己熟悉的语言和概念领域内,才能够把自己的意思表达清楚。


围绕低代码/无代码开发存在哪些技术难题需要学术界和工业界共同探索?


最初在 D2 上提出并分享“前端智能化”这个概念的时候,我就提出:识别、理解、表达 这个核心过程。我始终认为,达成 AI 驱动的“人机协同编程”关键路径就是:识别、理解、表达。因此,围绕 AI 识别、 AI 理解、 AI 表达我们和国内外知名大学展开了广泛的合作。


识别


需求的识别:通过 NLP 、知识图谱、图神经网络、结构化机器学习……等 AI 技术,识别用户需求、产品需求、设计需求、运营需求、营销需求、研发需求、工程需求……等,识别出其中的概念和概念之间的关系


设计稿的识别:通过 CV、GAN、对象识别、语义分割……等 AI 技术,识别设计稿中的元素、元素之间的关系、设计语言、设计系统、设计意图


UI 的识别:通过用户用脚投票的结果进行回归,后验的分析识别出 UI 对用户行为的影响程度、影响效果、影响频率、影响时间……等,并识别出 UI 的可变性和这些用户行为影响之间的关系


计算机程序的识别:通过对代码、AST ……等 Raw Data 分析,借助 NLP 技术识别计算机程序中,语言的表达能力、语言的结构、语言中的逻辑、语言和外部系统通过 API 的交互等


日志和数据的识别:通过对日志和数据进行 NLP、回归、统计分析等方式,识别出程序的可用性、性能、易用性等指标情况,并识别出影响这些指标的日志和数据出自哪里,找出其间的关系


理解


横向跨领域的理解:对识别出的概念进行降维,从而在底层更抽象的维度上找出不同领域之间概念的映射关系,从而实现用不同领域的概念进行类比,进而在某领域内理解其它领域的概念


纵向跨层次的理解:利用机器学习和深度学习的 AI 算法能力,放宽不同层次间概念的组成关系,对低层次概念实现跨层次的理解,进而形成更加丰富的技术、业务能力供给和使用机会


常识、通识的理解:以常识、通识构建的知识图谱为基础,将 AI 所面对的开放性问题领域化,将领域内的常识和通识当做理解的基础,不是臆测和猜想,而是实实在在构建在理论基础上的理解


表达


个性化:借助大数据和算法实现用户和软件功能间的匹配,利用 AI 的生成能力降低千人前面的研发成本,从而真正实现个性化的软件服务能力,把软件即服务推向极致


共情:利用端智能在用户侧部署算法模型,既可以解决用户隐私保护的问题,又可以对用户不断变化的情绪、诉求、场景及时学习并及时做出响应,从而让软件从程序功能的角度急用户之所急、想用户之所想,与用户共情、让用户共鸣。举个例子:我用 iPhone 在进入地铁站的时候,因为现在要检查健康码,每次进入地铁站 iOS 都会给我推荐支付宝快捷方式,我不用自己去寻找支付宝打开展示健康码,这就让我感觉 iOS 很智能、很贴心,这就是共情。


后记


从提出前端智能化这个概念到现在已历三年,最初,保持着“让前端跟上 AI 发展的浪潮”的初心上路,到“解决一线研发问题”发布 imgcook.com ,再到“给前端靠谱的机器学习框架”开源github.com/alibaba/pip…


这一路走来,几乎日日夜不能寐。真正想从本质上颠覆现在的编程模式和研发模式谈何容易?这个过程中,我们从一群纯前端变成前端和 AI 的跨界程序员,开发方式从写代码到机器生成,周围的人从作壁上观到积极参与,正所谓:念念不忘,必有回响。低代码/无代码开发方兴未艾,广大技术、科研人员在这个方向上厉兵秣马,没有哪个方法是 Silverbullet ,也没有哪个理论是绝对正确的,只要找到你心中所爱,坚持研究和实践,终会让所有人都能够自定义软件来操作日益复杂和强大的硬件设备,终能让所有人更加便捷、直接、有效的接入数字世界,终于在本质上将软件开发和软件工程领域重新定义!共勉!



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

收起阅读 »

iOS 开发的应用内调试和探索工具-FLEX

FLEX (Flipboard Explorer) 是一套用于 iOS 开发的应用内调试和探索工具。出现时,FLEX 会显示一个位于应用程序上方窗口中的工具栏。从此工具栏上,您可以查看和修改正在运行的应用程序中的几乎所有状态。给自己调试超能力检查和修改层次结构...
继续阅读 »

FLEX (Flipboard Explorer) 是一套用于 iOS 开发的应用内调试和探索工具。出现时,FLEX 会显示一个位于应用程序上方窗口中的工具栏。从此工具栏上,您可以查看和修改正在运行的应用程序中的几乎所有状态。

给自己调试超能力

  • 检查和修改层次结构中的视图。
  • 查看任何对象的属性和变量。
  • 动态修改许多属性和变量。
  • 动态调用实例和类方法。
  • 通过时间、标头和完整响应观察详细的网络请求历史记录。
  • 添加您自己的模拟器键盘快捷键。
  • 查看系统日志消息(例如来自NSLog)。
  • 通过扫描堆访问任何活动对象。
  • 查看应用程序沙箱中的文件系统。
  • 浏览文件系统中的 SQLite/Realm 数据库。
  • 使用 control、shift 和 command 键在模拟器中触发 3D 触摸。
  • 探索您的应用程序和链接系统框架(公共和私有)中的所有类。
  • 快速访问有用的对象,例如[UIApplication sharedApplication]应用程序委托、关键窗口上的根视图控制器等。
  • 动态查看和修改NSUserDefaults值。

与许多其他调试工具不同,FLEX 完全在您的应用程序内部运行,因此您无需连接到 LLDB/Xcode 或其他远程调试服务器。它在模拟器和物理设备上运行良好。用法

在 iOS 模拟器中,您可以使用键盘快捷键来激活 FLEX。f将切换 FLEX 工具栏。敲击?快捷键的完整列表。您还可以以编程方式显示 FLEX:

// Objective-C
[[FLEXManager sharedManager] showExplorer];

// Swift
FLEXManager.shared.showExplorer()

更完整的版本:

#if DEBUG
#import "FLEXManager.h"
#endif

...

- (void)handleSixFingerQuadrupleTap:(UITapGestureRecognizer *)tapRecognizer
{
#if DEBUG
if (tapRecognizer.state == UIGestureRecognizerStateRecognized) {
// This could also live in a handler for a keyboard shortcut, debug menu item, etc.
[[FLEXManager sharedManager] showExplorer];
}
#endif
}


功能示例

修改视图

选择视图后,您可以点击工具栏下方的信息栏以显示有关该视图的更多详细信息。从那里,您可以修改属性和调用方法。



网络历史

启用后,网络调试允许您查看使用 NSURLConnection 或 NSURLSession 发出的所有请求。设置允许您调整缓存的响应主体类型和响应缓存的最大大小限制。您可以选择在应用启动时自动启用网络调试。此设置在启动时保持不变。



堆上的所有对象

FLEX 查询 malloc 以获取所有实时分配的内存块并搜索看起来像对象的内存块。你可以从这里看到一切。

堆/活动对象资源管理器


探索地址

如果您获得任意地址,您可以尝试探索该地址处的对象,如果 FLEX 可以验证该地址指向有效对象,则会打开它。如果 FLEX 不确定,它会警告您并拒绝取消对指针的引用。但是,如果您更了解,则可以通过选择“不安全探索”来选择探索它

地址浏览器


模拟器键盘快捷键

默认键盘快捷键允许您激活 FLEX 工具、使用箭头键滚动以及使用转义键关闭模式。您还可以通过添加自定义键盘快捷键-[FLEXManager registerSimulatorShortcutWithKey:modifiers:action:description]

模拟器键盘快捷键


安装

CocoaPods

pod 'FLEX', :configurations => ['Debug']

Carthage

  1. 不要添加FLEX.framework到目标的嵌入式二进制文件中,否则它会包含在所有构建中(因此也包含在发布版本中)。

  2. 相反,添加$(PROJECT_DIR)/Carthage/Build/iOS到您的目标框架搜索路径(如果您已经在 Carthage 中包含了其他框架,则此设置可能已经存在)。这使得从源文件导入 FLEX 框架成为可能。如果为所有配置添加此设置也无害,但至少应为调试添加此设置。

  3. 向您的目标添加一个运行脚本阶段Link Binary with Libraries例如,在现有阶段之后插入它),并且它只会嵌入FLEX.framework到调试版本中:

if [ "$CONFIGURATION" == "Debug" ]; then
/usr/local/bin/carthage copy-frameworks
fi
最后,添加
$(SRCROOT)/Carthage/Build/iOS/FLEX.framework为这个脚本阶段的输入文件。

手动添加到项目的 FLEX 文件

在 Xcode 中,导航到Build Settings > Build Options > Excluded Source File Names对于您的Release配置,将其设置为FLEX*这样以排除具有FLEX前缀的所有文件


常见问题及demo下载:https://github.com/FLEXTool/FLEX







收起阅读 »

Android 抛弃旧有逆向方式,如何快速逆向:FakerAndroid

FakerAndroidA tool translate apk file to common android project and support so hook and include il2cpp c++ scaffolding when apk is...
继续阅读 »

FakerAndroid

A tool translate apk file to common android project and support so hook and include il2cpp c++ scaffolding when apk is a il2cpp game apk

简介

  • 优雅地在一个Apk上写代码
  • 直接将Apk文件转换为可以进行二次开发的Android项目的工具,支持so hook,对于il2cpp的游戏apk直接生成il2cpp c++脚手架
  • 将痛苦的逆向环境,转化为舒服的开发环境,告别汇编,告别二进制,还有啥好说的~~

特点

  • 提供Java层代码覆盖及继承替换的脚手架,实现java与smali混编
  • 提供so函数Hook Api
  • 对于il2cpp的游戏apk直接生成il2cpp c++脚手架
  • Java层标准的对原有Java api的AndroidStudio编码提示
  • Smali文件修改后运行或打包时自动回编译(AndroidStudio project 文件树模式下可以直接找到smali文件,支持对smali修改,最小文件数增量编译)
  • 对于il2cpp的游戏apk,标准的Jni对原有il2cpp脚本的编码提示
  • 无限的可能性和扩展性,能干啥你说了算~
  • Dex折叠,对敏感已经存在或后续接入的代码进行隐藏规避静态分析

运行环境

使用方式

  • 下载FakerAndroid.jar(2020/11/15/16:53:00)
  • cmd命令行 cd <FakerAndroid.jar平级目录>
  • cmd命令行 java -jar FakerAndroid.jar fk <apkpath>(项目生成路径与apk文件平级) 或 java -jar FakerAndroid.jar fk <apkpath> -o <outdir>
  • 例:java -jar FakerAndroid.jar fk D:\apk\test.apk或 java -jar FakerAndroid.jar fk D:\apk\test.apk -o D:\test

或者使用方式

  • 下载FakerAndroid-AS.zip(2020/11/15/16:53:00)
  • AS->File-Settings->Plugin->SettingIcon->InstallPlugin Plugin From Disk(选择FakerAndroid-AS.zip-安装-启用)->重启AndroidStudio
  • AS->File->FakerAndroid->选择目标Apk文件

生成的Android项目二次开发教程(图文教程)

1、打开项目
  • Android studio直接打开工具生成的Android项目
  • 保持跟目录build.gradle中依赖固定,请勿配置AndroidGradlePlugin,且项目配置NDk版本为21
  • 存在已知缺陷,res下的部分资源文件编译不过,需要手动修复一下,部分Manifest标签无法编译需要手动修复
    (关于Res混淆手动实验了几个,如果遇到了这个问题,可以手动尝试,只要保证res/public.xml中的name对应的资源文件可以正常链路下去然后修复到可编译的程度,程序运行时一般是没有res问题,太完美的解决方案尚未完成)
2、调试运行项目
  • 连接测试机机
  • Run项目
3、进阶
  • 类调用
    借助javaScaffoding 在主模块(app/src/main/java)编写java代码对smali代码进行调用
  • 类替换
    在主模块(app/src/main/java)直接编写Java类,类名与要替换的类的smali文件路径对应
  • Smali 增量编译
    你可以使用传统的smali修改方式对smali代码进行修改,且编译方式为最小文件数增量编译,smali文件修改后javascaffoding会同步,比如遇到final或private的java元素无法掉用时可以先修改smali(执行一次编译后javaScaffoding会同步)
  • So Hook
    借助FakeCpp 使用jni对so函数进行hook替换
  • il2cpp unity游戏脚本二次开发
    借助il2cpp Scaffolding 和FakeCpp,使用jni对原il2cpp游戏脚本进行Hook调用
  • Dex折叠
    build.gradle 配置sensitiveOptions用于隐藏敏感的dex代码,以规避静态分析,(Dex缓存原因在app版本号不变的情况使用第一次缓存,配置项调试请卸载后运行)
4、正在路上

resources.arsc decode 兼容,目前混淆某些大型 apk Res decoder有问题
各种不理想情况兼容

5、兼容性

1、目前某些大型的apk资做过资源文件混淆的会有问题!
2、Google play 90% 游戏apk可以一马平川
3、加固Apk需要先脱壳后才能,暴漏java api
4、有自校验的Apk,须项目运行起来后自行检查破解
5、Manifest莫名奇妙的问题,可以先尝试注释掉异常代码,逐步还原试试
6、Java OOM issue
7、AS打不开,试试Help->Change Memery Settings(搞大点)

github地址:https://github.com/Efaker/FakerAndroid

下载地址:FakerAndroid.zip


收起阅读 »

使用 iOS OpenGL ES 实现长腿功能

本文介绍了如何使用 OpenGL ES 来实现长腿功能。学习这个例子可以加深我们对纹理渲染流程的理解。另外,还会着重介绍一下「渲染到纹理」这个新知识点。警告: 本文属于进阶教程,阅读前请确保已经熟悉 OpenGL ES 纹理渲染的相关概念,否则强行阅读可能导致...
继续阅读 »


本文介绍了如何使用 OpenGL ES 来实现长腿功能。学习这个例子可以加深我们对纹理渲染流程的理解。另外,还会着重介绍一下「渲染到纹理」这个新知识点。

警告: 本文属于进阶教程,阅读前请确保已经熟悉 OpenGL ES 纹理渲染的相关概念,否则强行阅读可能导致走火入魔。传送门

注: 下文中的 OpenGL ES 均指代 OpenGL ES 2.0。

一、效果展示

首先来看一下最终的效果,这个功能简单来说,就是实现了图片的局部拉伸,从逻辑上来说并不复杂。


二、思路

1、怎么实现拉伸

我们来回忆一下,我们要渲染一张图片,需要将图片拆分成两个三角形,如下所示:


如果我们想对图片进行拉伸,很简单,只需要修改一下 4 个顶点坐标的 Y 值即可。


那么,如果我们只想对图片中间的部分进行拉伸,应该怎么做呢?

其实答案也很容易想到,我们只需要修改一下图片的拆分方式。如下所示,我们把图片拆分成了 6 个三角形,也可以说是 3 个小矩形。这样,我们只需要对中间的小矩形做拉伸处理就可以了。


2、怎么实现重复调整

我们观察上面的动态效果图,可以看到第二次的压缩操作,是基于第一次的拉伸操作的结果来进行的。因此,在每一步我们都需要拿到上一步的结果,作为原始图,进行再次调整。

这里的「原始图」就是一个纹理。换句话说,我们需要将每一次的调整结果,都重新生成一个纹理,供下次调整的时候使用。

这一步是本文的重点,我们会通过「渲染到纹理」的方式来实现,具体的步骤我们在后面会详细介绍。

三、为什么要使用 OpenGL ES

可能有人会说:你这个功能平平无奇,就算不懂 OpenGL ES,我用其它方式也能实现呀。

确实,在 iOS 中,我们绘图一般是使用 CoreGraphics。假设我们使用 CoreGraphics,也按照上面的实现思路,对原图进行拆分绘制,重复调整的时候进行重新拼接,目测也是能实现相同的功能。

但是,由于 CoreGraphics 绘图依赖于 CPU,当我们在调节拉伸区域的时候,需要不断地进行重绘,此时 CPU 的占用必然会暴涨,从而引起卡顿。而使用 OpenGL ES 则不存在这样的问题。

四、实现拉伸逻辑

从上面我们知道,渲染图片我们需要 8 个顶点,而拉伸逻辑的关键就是顶点坐标的计算,在拿到计算结果后再重新渲染。

计算顶点的关键步骤如下:

/**
根据当前控件的尺寸和纹理的尺寸,计算初始纹理坐标

@param size 原始纹理尺寸
@param startY 中间区域的开始纵坐标位置 0~1
@param endY 中间区域的结束纵坐标位置 0~1
@param newHeight 新的中间区域的高度
*/
- (void)calculateOriginTextureCoordWithTextureSize:(CGSize)size
startY:(CGFloat)startY
endY:(CGFloat)endY
newHeight:(CGFloat)newHeight {
CGFloat ratio = (size.height / size.width) *
(self.bounds.size.width / self.bounds.size.height);
CGFloat textureWidth = self.currentTextureWidth;
CGFloat textureHeight = textureWidth * ratio;

// 拉伸量
CGFloat delta = (newHeight - (endY - startY)) * textureHeight;

// 判断是否超出最大值
if (textureHeight + delta >= 1) {
delta = 1 - textureHeight;
newHeight = delta / textureHeight + (endY - startY);
}

// 纹理的顶点
GLKVector3 pointLT = {-textureWidth, textureHeight + delta, 0}; // 左上角
GLKVector3 pointRT = {textureWidth, textureHeight + delta, 0}; // 右上角
GLKVector3 pointLB = {-textureWidth, -textureHeight - delta, 0}; // 左下角
GLKVector3 pointRB = {textureWidth, -textureHeight - delta, 0}; // 右下角

// 中间矩形区域的顶点
CGFloat startYCoord = MIN(-2 * textureHeight * startY + textureHeight, textureHeight);
CGFloat endYCoord = MAX(-2 * textureHeight * endY + textureHeight, -textureHeight);
GLKVector3 centerPointLT = {-textureWidth, startYCoord + delta, 0}; // 左上角
GLKVector3 centerPointRT = {textureWidth, startYCoord + delta, 0}; // 右上角
GLKVector3 centerPointLB = {-textureWidth, endYCoord - delta, 0}; // 左下角
GLKVector3 centerPointRB = {textureWidth, endYCoord - delta, 0}; // 右下角

// 纹理的上面两个顶点
self.vertices[0].positionCoord = pointLT;
self.vertices[0].textureCoord = GLKVector2Make(0, 1);
self.vertices[1].positionCoord = pointRT;
self.vertices[1].textureCoord = GLKVector2Make(1, 1);
// 中间区域的4个顶点
self.vertices[2].positionCoord = centerPointLT;
self.vertices[2].textureCoord = GLKVector2Make(0, 1 - startY);
self.vertices[3].positionCoord = centerPointRT;
self.vertices[3].textureCoord = GLKVector2Make(1, 1 - startY);
self.vertices[4].positionCoord = centerPointLB;
self.vertices[4].textureCoord = GLKVector2Make(0, 1 - endY);
self.vertices[5].positionCoord = centerPointRB;
self.vertices[5].textureCoord = GLKVector2Make(1, 1 - endY);
// 纹理的下面两个顶点
self.vertices[6].positionCoord = pointLB;
self.vertices[6].textureCoord = GLKVector2Make(0, 0);
self.vertices[7].positionCoord = pointRB;
self.vertices[7].textureCoord = GLKVector2Make(1, 0);
}

五、渲染到纹理

上面提到:我们需要将每一次的调整结果,都重新生成一个纹理,供下次调整的时候使用。

出于对结果分辨率的考虑,我们不会直接读取当前屏幕渲染结果对应的帧缓存,而是采取「渲染到纹理」的方式,重新生成一个宽度与原图一致的纹理。

这是为什么呢?

假设我们有一张 1000 X 1000 的图片,而屏幕上的控件大小是 100 X 100 ,则纹理渲染到屏幕后,渲染结果对应的渲染缓存的尺寸也是 100 X 100 (暂不考虑屏幕密度)。如果我们这时候直接读取屏幕的渲染结果,最多也只能读到 100 X 100 的分辨率。

这样会导致图片的分辨率下降,所以我们会使用能保持原有分辨率的方式,即「渲染到纹理」。

在这之前,我们都是将纹理直接渲染到屏幕上,关键步骤像这样:

GLuint renderBuffer; // 渲染缓存
GLuint frameBuffer; // 帧缓存

// 绑定渲染缓存要输出的 layer
glGenRenderbuffers(1, &renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];

// 将渲染缓存绑定到帧缓存上
glGenFramebuffers(1, &frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER,
renderBuffer);

我们生成了一个渲染缓存,并把这个渲染缓存挂载到帧缓存的 GL_COLOR_ATTACHMENT0 颜色缓存上,并通过 context 为当前的渲染缓存绑定了输出的 layer 。

其实,如果我们不需要在屏幕上显示我们的渲染结果,也可以直接将数据渲染到另一个纹理上。更有趣的是,这个渲染后的结果,还可以被当成一个普通的纹理来使用。这也是我们实现重复调整功能的基础。

具体操作如下:

// 生成帧缓存,挂载渲染缓存
GLuint frameBuffer;
GLuint texture;

glGenFramebuffers(1, &frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);

glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newTextureWidth, newTextureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);

通过对比我们可以发现,这里我们用 Texture 来替换 Renderbuffer ,并且同样是挂载到 GL_COLOR_ATTACHMENT0 上,不过这里就不需要另外再绑定 layer 了。

另外,我们需要为新的纹理设置一个尺寸,这个尺寸不再受限于屏幕上控件的尺寸,这也是新纹理可以保持原有分辨率的原因。

这时候,渲染的结果都会被保存在 texture 中,而 texture 也可以被当成普通的纹理来使用。

六、保存结果

当我们调整出满意的图片后,需要把它保存下来。这里分为两步,第一步仍然是上面提到的重新生成纹理,第二步就是把纹理转化为图片。

第二步主要通过 glReadPixels 方法来实现,它可以从当前的帧缓存中读取出纹理数据。直接上代码:

// 返回某个纹理对应的 UIImage,调用前先绑定对应的帧缓存
- (UIImage *)imageFromTextureWithWidth:(int)width height:(int)height {
int size = width * height * 4;
GLubyte *buffer = malloc(size);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, size, NULL);
int bitsPerComponent = 8;
int bitsPerPixel = 32;
int bytesPerRow = 4 * width;
CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);

// 此时的 imageRef 是上下颠倒的,调用 CG 的方法重新绘制一遍,刚好翻转过来
UIGraphicsBeginImageContext(CGSizeMake(width, height));
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

free(buffer);
return image;
}

至此,我们已经拿到了 UIImage 对象,可以把它保存到相册里了。

源码

请到 GitHub 上查看完整代码。

参考

iOS 中使用 OpenGL 实现增高功能
学习 OpenGL ES 之渲染到纹理
获取更佳的阅读体验,请访问原文地址【Lyman's Blog】使用 iOS OpenGL ES 实现长腿功能

链接:https://www.jianshu.com/p/433f13a2945e

收起阅读 »

runtime 小结

OC被称之为动态运行时语言,最主要的原因就是因为两个特性,一个是运行时也就是runtime,一个是多态。runtimeruntime又叫运行时,是一套底层的c语言api,其为iOS内部核心之一。OC是动态运行时语言,它会将一些工作放在代码运行时去处理,而非编译...
继续阅读 »

OC被称之为动态运行时语言,最主要的原因就是因为两个特性,一个是运行时也就是runtime,一个是多态。

runtime

runtime又叫运行时,是一套底层的c语言api,其为iOS内部核心之一。OC是动态运行时语言,它会将一些工作放在代码运行时去处理,而非编译时,比如动态的遍历属性和方法,动态的添加属性和方法,动态的修改属性和方法等。

了解runtime,首先要先了解它的核心--消息传递。

消息传递

消息直到运行时才会与方法实践绑定起来。
一个实例对象调用实例方法,像这样[obj doSomething];,编译器转成消息发送objc_msgSend(obj, @selector(doSomething),,);,

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

runtime时的运行流程如下:

1、首先通过调用对象的isa找到class;
2、在class的method_list里面找该方法,这里如果是实例对象,则去实例对象的类的方法列表中找,如果是类对象调用类方法,则去元类的方法列表中找,具体下面解释;
3、如果class里没找到,继续往它的superClass里找;
4、一旦找到doSomething这个函数,就去执行它的实现IMP;

下面介绍一下对象(object),类(class),方法(method)的结构体:

//对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
//方法
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}

类对象(objc_class)

OC中类是Class来表示,实际上是一个指向objc_class结构体的指针。

//对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}

观察一下对象的结构体和类对象的结构体,可以看到里面都有一个isa指针,对象的isa指针指向类,类的isa指针指向元类(metaClass),元类也是类,元类的isa指针最终指向根元类(rootMetaClass),根元类的isa指针指向自己,最终形成一个闭环。



可以看到类结构体中有一个methodLists,也就解释了上文提到的成员方法记录在class method-list中,类方法记录在metaClass中。即Instance-object的信息记录在class-object中,而class-object的信息记录在meta-class中。

结构体中有一个ivars指针指向objc_ivar_list结构体,是该类的属性列表,因为编译器编译顺序是父类,子类,分类,所以这也就是为什么分类category不能添加属性,因为类在编译的时候已经注册在runtime中了,属性列表objc_ivar_list和instance_size内存大小都已经确定了,同时runtime会调用class_setIvarLayout和class_setWeakIvarLayout来处理strong和weak引用。可以通过runtime的关联属性来给分类添加属性(原因是category结构体中有一个instanceProperties,下文会讲到)。因为编译顺序是父类,子类,分类,所以消息遍历的顺序是分类,子类,父类,先进后出。

objc_cache结构体,是一个很有用的方法缓存,把经常调用的方法缓存下来,提高遍历效率。将方法的method_name作为key,method_imp作为value保存下来。

Method(objc_method)

结构体如下:

//方法
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}

可以看到里面有一个SEL和IMP,这里讲一下两者的区别。

SEL是selector的OC表示,数据结构为:typedef struct objc_selector *SEL;是个映射到方法的c字符串;不同于函数指针,函数指针直接保存了方法地址,SEL只是一个编号;也是objc_cache中的key。

ps.这也带来了一个弊端,函数重载不适用,因为函数重载是方法名相同,参数名不同,但是SEL只记了方法名,没有参数,所以没法区分不同的method。

ps.在不同的类中,相同的方法名,方法选择器也是相同的。

IMP是函数指针,数据结构为typedef id (IMP)(id,SEL,**);保存了方法地址,由编译器绑定生成,最终方法执行哪段代码由IMP决定。IMP指向了方法的实现,一组id和SEL可以确定唯一的实现。

有了SEL这个中间过程,我们可以对一个编号和方法实现做些中间操作,也就是说我们一个SEL可以指向不同的函数指针,这样就可以完成一个方法名在不同的时候执行不同的函数体。另外可以将SEL作为参数传递给不同的类执行,也就是我们某些业务只知道方法名但需要根据不同的情况让不同的类执行。个人理解,消息转发就是利用了这个中间过程。

runtime是如何通过selector找到对应的IMP的?
上文讲了类对象中有实例方法的列表,元类对象中有类方法的列表,列表中记录着方法的名称,参数和实现。而selector本质就是方法名称也就是SEL,通过方法名称可以在列表中找到方法实现。

在寻找IMP的时候,runtime提供了两种方法:

1、IMP class_getMethodImplementation(Class cls, SEL name);
2、IMP method_getImplementation(Method m);
对于第一种方法来说,实例方法和类方法都是调用这个方法来找到IMP,不同的是第一个参数,实例方法传的参数是[obj class];,而类方法传的参数是objc_getMetaClass("obj");
对于第二种方法来说,传入的参数只有Method,区分类方法和实例方法在于封装Method的函数,类方法:Method class_getClassMethod(Class cls, SEL name);实例方法:Method class_getInstanceMethod(Class cls, SEL name);

Category(objc_category)

category是表示指向分类的一个结构体指针,结构体如下:

struct category_t { 
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
name:是指 class_name 而不是 category_name。
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对应到对应的类对象。
instanceMethods:category中所有给类添加的实例方法的列表。
classMethods:category中所有添加的类方法的列表。
protocols:category实现的所有协议的列表。
instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。

从上面的结构体可以看出,分类category可以添加实例方法,类方法,协议,以及通过关联对象添加属性,不可以添加成员变量。

runtime消息转发

前文讲到,到一个方法被执行,也就是发送消息,会去相关的方法列表中寻找对应的方法实现IMP,如果一直到根类都没找到就会进入到消息转发阶段,下面介绍一下消息转发的最后三个集会。

1、动态方法解析
2、备用接收者
3、完整消息转发

动态方法解析

首先,当消息传递到根类都找不到方法实现时,运行时runtime会调用+resolveInstanceMethod:或者+resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,并返回了yes,那运行时就会重新走一步消息发送的过程。

实现一个动态方法解析的例子如下:

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");//新的foo函数
}

可以看到虽然没有实现foo这个函数,但是我们通过class_addMethod动态的添加了一个新的函数实现fooMethod,并返回了yes。

如果返回no,就会进入下一步,- forwardingTargetForSelector:。

备用接收者

实现的例子如下:

#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
NSLog(@"Doing foo");//Person的foo函数
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;//返回NO,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
return [Person new];//返回Person对象,让Person对象接收这个消息
}

return [super forwardingTargetForSelector:aSelector];
}

@end

可以看到我们通过-forwardingTargetForSelector:方法将当前viewController的foo函数转发给了Person的foo函数去执行了。

如果在这一步还不能处理未知的消息,则进入下一步完整消息转发。

完整消息转发

首先会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,runtime会发出-doseNotRecognizeSelector消息,程序会挂掉;如果返回一个函数标签,runtime就会创建一个NSInvocation对象,并发送-forwardInvocation:消息给目标对象。

实现例子如下:

#import "ViewController.h"
#import "objc/runtime.h"

@interface Person: NSObject

@end

@implementation Person

- (void)foo {
NSLog(@"Doing foo");//Person的foo函数
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;//返回NO,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;//返回nil,进入下一步转发
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
}

return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;

Person *p = [Person new];
if([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}
else {
[self doesNotRecognizeSelector:sel];
}

}

@end

通过签名,runtime生成了一个anInvocation对象,发送给了forwardInvocation:,我们再forwardInvocation:里面让Person对象去执行了foo函数。

以上就是runtime的三次函数转发流程。

觉得有用,请帮忙点亮红心

Better Late Than Never!
努力是为了当机会来临时不会错失机会。
共勉!

链接:https://www.jianshu.com/p/4ae997a6c599

收起阅读 »

解决集成EaseIMKit源码后没有图片的问题

经过上一篇文章如何集成环信EaseIMKit和EaseCallKit源码?之后,我们在实际使用时,会发现一个非常大的问题:就是图片都加载不出来了.这里我们可以借用easeCallKit的实现方式将EaseCallKit内的文件资源包复制一份,修改一下名,然后打...
继续阅读 »

经过上一篇文章如何集成环信EaseIMKit和EaseCallKit源码?之后,我们在实际使用时,会发现一个非常大的问题:

就是图片都加载不出来了.

这里我们可以借用easeCallKit的实现方式

将EaseCallKit内的文件资源包复制一份,修改一下名,然后打开包,将里面的图片都替换掉,这是一个方法.

但上述方法依然有问题,涉及到自动加载倍图问题.

解决加载倍图也是有方法的,不过都太麻烦了,我们采用一个比较笨的方法.

直接将EaseIMKit内的图片拖进项目内

就像这样:



同时,我们还需要修改加载图片的方式,项目中直接搜索:

EaseIMKit.framework

发现总共三个地方:







至此已完成.

另外我们如果使用官方demo中的代码,直接拖文件进来时,会发现好多报错.这里直接说明原因,图片重复了,搜索报错的图片名,直接保留一份即可.

最后,再次强调:

我们是可以采用EaseCallKit加载图片方式的,但此方式有一个非常大的问题:倍图

(正因为尝试过并失败了,所以放弃了)

如果我们直接采用EaseCallKit加载图片方式,不做任何处理,会自动加载一倍图,而且如果没有一倍图也不会自动加载二倍图和三倍图,我们需要手动判断和手动加载图片名后缀,比较麻烦,所以这里就偷个懒,采用上述方法来解决加载图片问题.


收起阅读 »

runloop 小结

OC的两大核心runtime和runlooprunloop简介runloop本质上是一个do-while循环,当有任务处理时唤醒,没有任务时休眠,如果没有任务没有观察者的时候退出。OSX/iOS系统中,提供了两个这样的对象:NSRunLoop和CFRunLoo...
继续阅读 »

OC的两大核心runtime和runloop

runloop简介

runloop本质上是一个do-while循环,当有任务处理时唤醒,没有任务时休眠,如果没有任务没有观察者的时候退出。

OSX/iOS系统中,提供了两个这样的对象:NSRunLoop和CFRunLoopRef.
CFRunLoopRef是CoreFoundation框架提供的纯c的api,所有这些api都是线程安全的。
NSRunLoop是对CFRunLoopRef的OC封装,提供了面向对象的api,这些api不是线程安全的。

runloop和线程的关系

首先,iOS提供了两个线程对象pthread_t和NSThread,这两个线程对象不能互相转换,但是一一对应。比如:可以通过pthread_main_thread_np()和[NSThread mainThread]获取主线程;也可以通过pthread_self()和[NSThread currentThread]获取当前线程。CFRunLoopRef是基于pthread来管理的。

苹果不允许直接创建runloop,它只有两个获取的函数:CFRunLoopGetMain()和CFRunLoopGetCurrent()。这两个函数的内部实现大致是:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;

/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);

if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}

/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));

if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}

OSSpinLockUnLock(&loopsLock);
return loop;
}

CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}

CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}

可以看出来,线程和RunLoop是一一对应的,保存在一个全局的CFMutableDictionaryRef,key为pthread,value为runloop。线程刚创建时没有runloop,如果你没有主动获取,那它一直不会有。当你第一次获取runloop时,创建runloop,当线程结束时,runloop销毁。

主线程的runloop默认开启,程序启动时,main方法,applicationMain方法内开启runloop。

runloop的类
在Core Foundation框架中提供了五个类关于runloop:

1、CFRunLoopRef
2、CFRunLoopModeRef
3、CFRunLoopSourceRef
4、CFRunLoopTimerRef
5、CFRunLoopObserverRef

它们的关系如下:


一个runloop包含若干个Mode,一个Mode又包含若干个Source/Timer/Observer。每次调用runloop的主函数时,只能指定其中一个mode,如果想切换mode,需要退出当前runloop,再重新指定一个mode进入。这样的好处是,不同组的Source/Timer/Observer互不影响。

CFRunLoopSourceRef是事件产生的地方。Source有两个版本,Source 0(非端口Source)和Source 1(端口Source)。

1、Source 0 只包含一个回调函数指针,它并不能主动触发事件。使用时,需要先调用CFRunLoopSourceSignal(Source 0)将该source标记为待处理,然后手动调用CFRunLoopWakeUp()唤醒runloop,处理该事件。
2、Source 1 包含一个mach port(端口)和一个回调的函数指针,被用于通过内核和其他线程相互发送消息。这种source能主动唤醒runloop。
CFRunLoopTimerRef 是基于时间的触发器。其包含一个时间长度和一个回调的函数指针。当其加入到runloop时,runloop会注册对应的时间点,当时间点到时,runloop会被唤醒以执行这个回调。

CFRunLoopObserverRef 是观察者,每个Observer都包含一个回调,当runloop的状态发生改变时,观察者可以通过回调接受到这个变化。可以接受到的状态有如下几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};

上述的Source/Timer/Observer被统称为一个mode item,一个item可以被加入多个mode,但一个item被重复加入同一个mode,是没有效果的。如果一个mode中一个item都没有,则runloop会自动退出。

runloop的mode

CFRunLoopMode和CFRunLoop的结构大致如下

struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};

struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};

runloop的mode包含:

1、NSDefaultRunLoopMode:默认的mode;
2、UITrackingRunLoopMode:跟踪用户触摸事件的mode,如UIScrollView的上下滚动;
3、NSRunLoopCommonModes:模式集合,将一组item关联到这个模式集合上,等于将这个item关联到这个集合下的所有模式上;
4、自定义Mode。

这里主要解释一下NSRunLoopCommonModes,这个模式集合。
默认NSDefaultRunLoopMode和UITrackingRunLoopMode都是包含在这个模式集合内的,当然也可以自定义一个mode,通过CFRunLoopAddCommonMode添加到这个模式集合中。

应用场景举例:

当一个控制器里有一个UIScrollview和一个NSTimer,UIScrollView不滚动的时候,runloop运行在NSDefaultRunLoopMode下,此时Timer会得到回调,但当UIScrollView滑动时,会将mode切换成UITrackingRunLoopMode,此时Timer得不到回调。一个解决办法就是将这个NSTimer分别绑定到NSDefaultRunLoopMode和UITrackingRunLoopMode,另一个解决办法是将这个NSTimer绑定到NSRunLoopCommonModes,两种方法都能使NSTimer在两个模式下都能得到回调。

ps.让runloop运行在NSRunLoopCommonModes模式下是没有意思的,因为runloop一个时间只能运行在一个模式下。

端口Source通信的步骤
demo如下:

- (void)testDemo3
{
//声明两个端口 随便怎么写创建方法,返回的总是一个NSMachPort实例
NSMachPort *mainPort = [[NSMachPort alloc]init];
NSPort *threadPort = [NSMachPort port];
//设置线程的端口的代理回调为自己
threadPort.delegate = self;

//给主线程runloop加一个端口
[[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode];

dispatch_async(dispatch_get_global_queue(0, 0), ^{

//添加一个Port
[[NSRunLoop currentRunLoop]addPort:threadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

});

NSString *s1 = @"hello";

NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
//过2秒向threadPort发送一条消息,第一个参数:发送时间。msgid 消息标识。
//components,发送消息附带参数。reserved:为头部预留的字节数(从官方文档上看到的,猜测可能是类似请求头的东西...)
[threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];

});

}

//这个NSMachPort收到消息的回调,注意这个参数,可以先给一个id。如果用文档里的NSPortMessage会发现无法取值
- (void)handlePortMessage:(id)message
{

NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);

//只能用KVC的方式取值
NSArray *array = [message valueForKeyPath:@"components"];

NSData *data = array[1];
NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",s1);

// NSMachPort *localPort = [message valueForKeyPath:@"localPort"];
// NSMachPort *remotePort = [message valueForKeyPath:@"remotePort"];

}

声明两个端口,sendPort,receivePort,设置receivePort的代理,分别将sendPort和receivePort绑定到两个线程的自己的runloop上,然后回到发送线程用接收端口发送数据([threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0]; from参数标注从发送端口发出),注意这里发送的数据格式为array,内容格式只能为NSPort或者NSData,在代理方法- (void)handlePortMessage:(id)message中接收数据;

RunLoop的内部实现


内部代码整理,不想看可以跳过,看下方总结:

/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;

/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {

/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}

/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}

/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}

/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

/// 收到消息,处理消息。
handle_msg:

/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}

/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}

/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}

/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);


if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}

/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}

/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

runloop的运行逻辑:

1、通知监听者,即将进入runloop;
2、通知监听者,将要处理Timer;
3、通知监听者,将要处理Source0(非端口InputSource);
4、处理Source0;
5、如果有Source1,跳到第9步;
6、通知监听者,线程即将进入休眠;
7、runloop进入休眠,等待唤醒;
   1.source0;
   2.Timer启动;
   3.外部手动唤醒
8、通知监听者,线程将被唤醒;
9、处理未处理的任务;
   1.如果用户定义的定时器任务启动,处理定时器任务并重启runloop,进入步骤2;
   2.如果输入源启动,传递相应的消息;
   3.如果runloop被显示唤醒,且没有超过设置的时间,重启runloop,进入步骤2;
10、通知监听者,runloop结束。
   1.runloop结束,没有timer或者没有source;
   2.runloop被停止,使用CFRunloopStop停止Runloop;
   3.runloop超时;
   4.runloop处理完事件。

苹果用runloop实现的功能

1、自动释放池,在主程序启动时,再即将进入runloop的时候会执行autoreleasepush(),新建一个autoreleasePoolPage,同时push一个哨兵对象到这个page中;当runloop进入休眠模式时,会执行autoreleasepop(),释放旧池,同时autoreleasepush(),创建新池;当runloop退出时,清空自动释放池。

2、定时器NSTimer实际上就是CFRunloopTimerRef。

觉得有用,请帮忙点亮红心

Better Late Than Never!
努力是为了当机会来临时不会错失机会。
共勉!

链接:https://www.jianshu.com/p/8fdda9f64459

收起阅读 »

如何集成环信EaseIMKit和EaseCallKit源码?

EaseIMKit是一个基于环信sdk的UI库,封装了IM功能常用的控件、fragment等等。官网下载源码EaseCallKit源码EaseIMKit源码第二步 & 第三步整理一份路径 & 整理EaseCallKit文件及文件夹 ...
继续阅读 »

EaseIMKit是一个基于环信sdk的UI库,封装了IM功能常用的控件、fragment等等。

下面给大家分享一下如何引入EaseIMKit源码

第一步

官网下载源码

源码从这里找:环信开源GitHub


EaseCallKit源码



EaseIMKit源码


第二步 & 第三步

整理一份路径 & 整理EaseCallKit文件及文件夹
[注意!!!注意!!!注意!!! 上下两个文件夹窗口内有同名文件夹,因为就是同一个文件夹.这里专门开了两个窗口,为了更加直观!!!比如"00刚下载的源码/01整理之后的内容/02展示项目"这三个文件夹上下窗口的文件夹是同一个路径的文件夹]



第四步
整理EaseIMKit文件及文件夹



第五步
修改两个文件
(EaseIMKit.podspec & EaseCallKit.podspec)
(两个文件内容:文章末尾有文本内容可直接复制)



第六步
创建项目
这里创建项目名叫EaseSourceCode

将整理好内容的源码放入项目文件夹内,创建podfile,podfile内容如下

(podfile内容:文章末尾有文本内容可直接复制)



第七步
pod install后运行项目
先command+b编译,再引入头文件



第八步

最后集成完成,你会发现没有图片!下一篇文章将会讲解如何将图片加载出来.

附:

EaseIMKit.podspec内容如下

#=====================================

Pod::Spec.new do |s|

  s.name = 'EaseIMKit'

  s.version = '3.8.1.1'

  s.summary = 'easemob im sdk UIKit'

  s.description = <<-DESC

        EaseMob YES!!!

  DESC

  s.homepage = 'http://docs-im.easemob.com/im/ios/other/easeimkit'

  s.license          = 'MIT'

  s.platform = :ios, '10.0'

  s.author = { 'easemob' => 'dev@easemob.com' }

  s.source = { :git => 'http://XXX/EaseIMKit.git', :tag => s.version.to_s }

  s.frameworks = 'UIKit'

  s.libraries = 'stdc++'

  s.source_files = 'Class/**/*.{h,m,mm}'

  s.requires_arc = true

  s.resources = 'Class/EaseIMImage.bundle'

  s.static_framework = true

  s.dependency 'EMVoiceConvert', '~> 0.1.0'

  s.dependency 'HyphenateChat'

end

#=====================================

EaseCallKit.podspec内容如下

#=====================================

Pod::Spec.new do |s|

    s.name            ='EaseCallKit'

    s.version          ='3.8.1.1'

    s.summary          ='A UI framework with video and audio call'

    s.description      = <<-DESC

        EaseMob YES!!!

    DESC

    s.license          ='MIT'

    s.homepage ='https://www.easemob.com'

    s.author          = {'easemob'=>'dev@easemob.com'}

    s.source          = { :git =>'http://XXX/EaseCallKit.git', :tag => s.version.to_s }

    s.frameworks ='UIKit'

    s.libraries ='stdc++'

    s.ios.deployment_target ='9.0'

    s.source_files ='Classes/**/*.{h,m}'

    s.requires_arc =true

    s.resources ='Assets/EaseCall.bundle'

    s.dependency'HyphenateChat'

    s.dependency'Masonry'

    s.dependency'AgoraRtcEngine_iOS'

    s.dependency'SDWebImage'

end

#=====================================

podfile文件内容如下

#=====================================

# platform :ios, '9.0'

use_frameworks!

target 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaa' do

    pod'MBProgressHUD'

    pod'SDWebImage'

    pod'Masonry'

    pod'MJRefresh'

    pod'HyphenateChat'

    pod'AgoraRtcEngine_iOS' 

    pod'EaseIMKit', :path => './localPodsLibrary/EaseIMKit'

    pod'EaseCallKit',  :path =>'./localPodsLibrary/EaseCallKit'


end

#=====================================

下一篇:

解决集成EaseIMKit源码后没有图片的问题

收起阅读 »

iOS离屏渲染的触发原理与躲在背后的性能优化

一.带着问题了解什么是离屏渲染?        在iOS开发中,我们经常会写到这样的代码:btn.layer.cornerRadius = 50;btn.clipsToBounds = YE...
继续阅读 »

一.带着问题了解什么是离屏渲染?

        在iOS开发中,我们经常会写到这样的代码:btn.layer.cornerRadius = 50;btn.clipsToBounds = YES;很多的面试官也会问我们平常给VIew设置圆角的时候应该注意什么?在UITableViewCell中,如果出现了btn.layer.cornerRadius = 50;btn.clipsToBounds = YES这两行代码,会给tableVIew的渲染以及滑动带来什么影响?为什么在有些地方不建议使用这样的代码设置圆角?(btn只是一个举例,实际上它可以是UIview,UIbutton,uiimageVIew等),你们是否能回答出面试官心中想要的答案?

二.离屏渲染的由来

        在上一篇文章中,我提到了图像/图形渲染的流程:GPU进⾏渲染->帧缓存区⾥ ->视频控制器->读取帧缓存区信息(位图) -> 数模转化(数字信号处->模 拟型号) ->(逐⾏扫描)显示,重点来了:当帧缓冲区的数据不能直接被视频控制器扫描显示的时候,我们要额外的开辟一个缓冲区------->离屏缓冲区来存储我们不能第一时间交给视频控制器显示的数据,在离屏缓冲区渲染好我们不能直接被视频控制器显示的数据,等到最终我们可以确认当前的VIew到底怎么显示之后,再交给帧缓冲区----->视频控制器显示。




离屏渲染的代价很高,想要进行离屏渲染,首选要创建一个新的缓冲区,屏幕渲染会有一个上下文环境的一个概念,离屏渲染的整个过程需要切换上下文环境,先从当前屏幕切换到离屏,等结束后,又要将上下文环境切换回来。这也是为什么会消耗性能的原因了 (间接回答了如果出现了btn.layer.cornerRadius = 50;btn.clipsToBounds = YES这两行代码,会给tableVIew的渲染以及滑动带来什么影响?)   

        特别提醒:离屏渲染是系统触发,触发了之后才有离屏缓冲区,离屏缓冲区和我上一篇文章讲到的帧缓冲区的二级缓冲机制没有任何的因果关系

        最终当触发了离屏渲染之后,图像/图形的渲染流程变成了:app进⾏额外的渲染和合并-> offscreen Buffer(离屏缓冲区) 组合. -> FrameBuffer(帧缓冲区) -> 屏幕;特点:(离屏渲染-> 额外的存储空间/offscreen Buffer->FrameBuffer ) offscreenBuffer 空间大小-> 屏幕像素点2.5倍 


离屏渲染遵循画家算法:按次序输出到frame buffer,后一层覆盖前一层,就能得到最终的显示结果(值得一提的是,与一般桌面架构不同,在iOS中,设备主存和GPU的显存共享物理内存,这样可以省去一些数据传输开销),然而有些场景并没有那么简单。作为“画家”的GPU虽然可以一层一层往画布上进行输出,但是无法在某一层渲染完成之后,再回过头来擦除/改变其中的某个部分——因为在这一层之前的若干层layer像素数据,已经在渲染中被永久覆盖了。这就意味着,对于每一层layer,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改/剪裁操作

三.btn.layer.cornerRadius = 50&&btn.clipsToBounds = YES就一定会触发离屏渲染?

        首先我们先开启离屏渲染的检测,在模拟器打开color offscreen-rendered,开启后会把那些需要离屏渲染



这里就明显看出1和3变成了黄色,标记为触发了离屏渲染,个人觉得这应该是模拟器的bug吧,如果你的电脑没有出现这个问题,请忽略,有的话就试着选一选其他机型吧!!!

首先普及一下CALayer的层次结构:CALayer由背景色backgroundColor、内容contents、边缘borderWidth&borderColor构成

重点重点重点(重要的事情说三遍):cornerRadius的文档中明确说明对cornerRadius的设置只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染。这也就说明了上面代码为什么1和3触发了离屏渲染,而2和4没有触发离屏渲染

解决办法:

(1)后台绘制圆角图片,前台进行设置




(2)对于 contents 无内容或者内容的背景透明(无涉及到圆角以外的区域)的layer,直接设置layer的 backgroundColor 和 cornerRadius 属性来绘制圆角。

(3)使用混合图层,在layer上方叠加相应mask形状的半透明layer

sublayer.contents=(id)[UIImage imageNamed:@"xxx"].CGImage;

[view.layer addSublayer:sublayer];

(4)- (UIImage *)yy_imageByRoundCornerRadius:(CGFloat)radius corners:(UIRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(UIColor *)borderColor borderLineJoin:(CGLineJoin)borderLineJoin此方法为YY_image处理圆角的方法,你可以去下载YY_image查看源码

其他情况触发离屏渲染以及解决办法:

1. mask(遮罩)------>使用混合图层,在layer上方叠加相应mask形状的半透明layer

2.edge antialiasing(抗锯齿)----->不设置 allowsEdgeAntialiasing 属性为YES(默认为NO)

3. allowsGroupOpacity(组不透明,开启CALayer的allowsGroupOpacity属性后,子 layer 在视觉上的透明度的上限是其父 layer 的opacity(对应UIView的alpha),并且从 iOS 7 以后默认全局开启了这个功能,这样做是为了让子视图与其容器视图保持同样的透明度。)------->关闭 allowsGroupOpacity 属性,按产品需求自己控制layer透明度

4.shadows(阴影)------>设置阴影后,设置CALayer的 shadowPath,view.layer.shadowPath=[UIBezierPath pathWithCGRect:view.bounds].CGPath;

CALayer离屏渲染终极解决方案:当视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES(缓存离屏渲染的数据,当下次用到的时候直接拿,不需要开辟新的离屏缓冲区),此方案最为实用方便。view.layer.shouldRasterize = true;view.layer.rasterizationScale = view.layer.contentsScale;

shouldRasterize (光栅华使用建议):

1.如果layer不需要服用,则没有必要打开

2.如果layer不是静态的,需要被频繁修改,比如出于动画之中,则开启光栅华反而影响性能

3.离屏渲染缓存有时间限制,当超过100ms,内容没有被使用就会被丢弃,无法复用

4.离屏渲染缓存有空间限制,超过屏幕像素的2.5倍则失效,并无法使用

特别说明:当视图内容是动态变化(如后台下载图片完毕后切换到主线程设置)时,使用此方案反而为增加系统负荷。

总结:

(1)离屏渲染是系统触发,触发了之后才有离屏缓冲区,离屏缓冲区和我上一篇文章讲到的帧缓冲区的二级缓冲机制没有任何的因果关系

(2)btn.layer.cornerRadius = 50&&btn.clipsToBounds = YES不一定会触发离屏渲染,cornerRadius的文档中明确说明对cornerRadius的设置只对 CALayer 的backgroundColor和borderWidth&borderColor起作用,如果contents有内容或者内容的背景不是透明的话,只有设置masksToBounds为 true 才能起作用,此时两个属性相结合,产生离屏渲染

(3)在uitableVIewcell触发了离屏渲染,会导致在滑动的时候高频率的开辟离屏缓冲区,这样就会造成tanleView滑动卡顿,如果视图内容是静态不变时,设置 shouldRasterize(光栅化)为YES,此方案最为实用方便,但是当视图内容是动态变化(如后台下载图片完毕后切换到主线程设置)时,使用此方案反而为增加系统负荷。

(4)现在摆在我们面前得有三个选择:当前屏幕渲染、离屏渲染、CPU渲染,该用哪个呢?这需要根据具体的使用场景来决定。·   尽量使用当前屏幕渲染,鉴于离屏渲染、CPU渲染可能带来的性能问题,一般情况下,我们要尽量使用当前屏幕渲染。离屏渲染 VS CPU渲染



作者:枫紫
链接:https://www.jianshu.com/p/3448d19c3495









收起阅读 »

iOS------OpenGL 图形专有名词与坐标解析

一.OpenGL简介OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。它将计算机的资源抽象称为⼀个个OpenGL的对象,对这些资源的操...
继续阅读 »

一.OpenGL简介

OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D3D矢量图形的跨语言跨平台应用程序编程接口(API)。它将计算机的资源抽象称为⼀个个OpenGL的对象,对这些资源的操作抽象为⼀个个的OpenGL指令,开发者可以在mac程序中使用OpenGl来实现图形渲染。图形API的目的就是实现图形的底层渲染,比如游戏场景/游戏人物的渲染,音视频解码后数据的渲染,地图上的渲染,动画绘制等。在iOS开发中,开发者唯一能够GPU的就是图形API。(GPU---图形处理器(英语:Graphics Processing Unit,缩写:GPU),又称显示核心、视觉处理器、显示芯片,是一种专门在个人电脑工作站、游戏机和一些移动设备(如平板电脑智能手机等)上做图像和图形相关运算工作的微处理器。)

二.OpenGL专业名词解析

    1.OpenGL 上下⽂( context )

        OpenGL Context,中文解释就是OpenGL的上下文,因为OpenGL没有窗口的支持,我们在使用OpenGl的时候,一般是在main函数创建窗口: 

        //GLUT窗口大小、窗口标题

        glutInitWindowSize(800, 600);

        glutCreateWindow("Triangle");

        然后我们在创建的窗口里面绘制,个人理解上下文的意思就是指的是OpenGL的作用范围,当然OpenGL的Context不只是这个窗口,这个窗口我们可以理解为OpenGL的default framebuffer,所以Context还包含关于这个framebuffer的一些参数设置信息,具体内容可以查看OpenGL的Context的结构体,Context记录了OpenGL渲染需要的所有信息,它是一个大的结构体,它里面记录了当前绘制使用的颜色、是否有光照计算以及开启的光源等非常多我们使用OpenGL函数调用设置的状态和状态属性等等,你可以把它理解为是一个巨大的状态机,它里面保存OpenGl的指令,在图形渲染的时候,可以理解为这个状态机开始工作了,对某个属性或者开关发出指令。它的特点就是:有记忆功能,接收输入,根据输入的指令,修改当前的状态,并且可以输出内容,当停机的时候不再接收指令。

2.渲染

        渲染就是把数据显示到屏幕上,在OpenGl中,渲染指的是将图形/图像数据转换为2D空间图像操作叫渲染

3.顶点数组/顶点缓冲区

        在OpenGL中,基本图元有三种:点,线,三角形,复杂的图形由这三种图元组成,我们在画点/线/三角形的时候是不是应该先知道每个顶点的坐标,而这些坐标放在数组里,就叫顶点数组。顶点数组存在内存当中,但是为了提高性能,提前分配一块显存,将顶点数组预先存入到显存当中,这部分的显存就叫顶点缓冲区。

4.着色器(shader)

        为什么要使用着色器?我们知道,OpenGL一般使用经典的固定渲染管线来渲染对象,OpenGL在实际调⽤绘制函数之前,还需指定⼀个由shader编译成的着⾊器程序。常⻅的着⾊器主要有顶点着⾊器(VertexShader),⽚段着⾊器(FragmentShader)/像素着⾊器(PixelShader),⼏何着⾊器(GeometryShader),曲⾯细分着⾊器(TessellationShader)。⽚段着⾊器和像素着⾊器只是在OpenGL和DX中的不同叫法⽽已。可惜的是,直到OpenGLES 3.0,依然只⽀持了顶点着⾊器和⽚段着⾊器这两个最基础的着⾊器。OpenGL在处理shader时,和其他编译器⼀样。通过编译、链接等步骤,⽣成了着⾊器程序(glProgram),着⾊器程序同时包含了顶点着⾊器和⽚段着⾊器的运算逻辑。在OpenGL进⾏绘制的时候,⾸先由顶点着⾊器对传⼊的顶点数据进⾏运算。再通过图元装配,将顶点转换为图元。然后进⾏光栅化,将图元这种⽮量图形,转换为栅格化数据。最后,将栅格化数据传⼊⽚段着⾊器中进⾏运算。⽚段着⾊器会对栅格化数据中的每⼀个像素进⾏运算,并决定像素的颜⾊(顶点着色器和片段/片元着色器会在下面讲解)

5.管线

        OpenGL在渲染图形/图像的时候是按照特定的顺序来执行的,不能修改打破,管线的意思个人理解是读取顶点数据—>顶点着色器—>组装图元—>光栅化图元—>片元着色器—>写入帧缓冲区—>显示到屏幕上,类似这样的流水线,当图像/图形显示到屏幕上,这一条管线完成工作。下面是分步讲解:

       (1)读取顶点数据指的是将待绘制的图形的顶点数据传递给渲染管线中。

        (2)顶点着色器最终生成每个定点的最终位置,执行顶点的各种变换,它会针对每个顶点执行一次,确定了最终位置后,OpenGL就可以把这些顶点集合按照给定的参数类型组装成点,线或者三角形。

      (3)组装图元阶段包括两部分:图元的组装和图元处理,图元组装指的是顶点数据根据设置的绘制方式参数结合成完整的图元,例如点绘制方式中每个图元就只包含一个点,线段绘制方式中每个图源包含两个点;图元处理主要是剪裁以使得图元位于视景体内部的部分传递到下一个步骤,视景体外部的部分进行剪裁。视景体的概念与投影有关。

      (4)光栅化图元主要指的是将一个图元离散化成可显示的二维单元片段,这些小单元称为片元。一个片元对应了屏幕上的一个或多个像素,片元包括了位置,颜色,纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。

      (5)片元着色器为每个片元生成最终的颜色,针对每个片元都会执行一次。一旦每个片元的颜色确定了,OpenGL就会把它们写入到帧缓冲区中。

6.顶点着色器

         • ⼀般⽤来处理图形每个顶点变换(旋转/平移/投影等)                     

        • 顶点着⾊器是OpenGL中⽤于计算顶点属性的程序。顶点着⾊器是逐顶点运算的程序,也就是说每个顶点数据都会执⾏⼀次顶点着⾊器,当然这是并⾏的,并且顶点着⾊器运算过程中⽆法访问其他顶点的数据

        • ⼀般来说典型的需要计算的顶点属性主要包括顶点坐标变换、逐顶点光照运算等等。顶点坐标由⾃身坐标系转换到归⼀化坐标系的运算,就是在这⾥发⽣的。

7.片元着色器(片段着色器)

        ⼀般⽤来处理图形中每个像素点颜⾊计算和填充⽚段着⾊器是OpenGL中⽤于计算⽚段(像素)颜⾊的程序。⽚段着⾊器是逐像素运算的程序,也就是说每个像素都会执⾏⼀次⽚段着⾊器,当然也是并⾏的

8.光栅化Rasterization 

        • 是把顶点数据转换为⽚元的过程,具有将图转化为⼀个个栅格组成的图象的作⽤,特点是每个元素对应帧缓冲区中的⼀像素。

        • 光栅化就是把顶点数据转换为⽚元的过程。⽚元中的每⼀个元素对应于帧缓冲区中的⼀个像素。

        • 光栅化其实是⼀种将⼏何图元变为⼆维图像的过程。该过程包含了两部分的⼯作。第⼀部分⼯作:决定窗⼝坐标中的哪些整型栅格区域被基本图元占⽤;第⼆部分⼯作:分配⼀个颜⾊值和⼀个深度值到各个区域。光栅化过程产⽣的是⽚元

        • 把物体的数学描述以及与物体相关的颜⾊信息转换为屏幕上⽤于对应位置的像素及⽤于填充像素的颜⾊,这个过程称为光栅化,这是⼀个将模拟信号转化为离散信号的过程

9.纹理

        纹理可以理解为图⽚. ⼤家在渲染图形时需要在其编码填充图⽚,为了使得场景更加逼真.⽽这⾥使⽤的图⽚,就是常说的纹理.但是在OpenGL,我们更加习惯叫纹理,⽽不是图⽚

10.混合(Blending)

        在测试阶段之后,如果像素依然没有被剔除,那么像素的颜⾊将会和帧缓冲区中颜⾊附着上的颜⾊进⾏混合,混合的算法可以通过OpenGL的函数进⾏指定。但是OpenGL提供的混合算法是有限的,如果需要更加复杂的混合算法,⼀般可以通过像素着⾊器进⾏实现,当然性能会⽐原⽣的混合算法差⼀些,个人理解有点像iOS给RGB中红,绿,蓝设置不同的值得到不同的颜色,只是这里是操作片元着色器,来达到不同的显示。

11.变换矩阵(Transformation)/投影矩阵Projection 

        在iOS核心动画中我们也会和矩阵打交到,变换矩阵顾名思义就是对图像/图形的放大/缩小/平移/选装等座处理。

        投影矩阵就是⽤于将3D坐标转换为⼆维屏幕坐标,实际线条也将在⼆维坐标下进⾏绘制    

12.渲染上屏/交换缓冲区(SwapBuffer)     

    • 渲染缓冲区⼀般映射的是系统的资源⽐如窗⼝。如果将图像直接渲染到窗⼝对应的渲染缓冲区,则可以将图像显示到屏幕上。

    • 但是,值得注意的是,如果每个窗⼝只有⼀个缓冲区,那么在绘制过程中屏幕进⾏了刷新,窗⼝可能显示出不完整的图像

    • 为了解决这个问题,常规的OpenGL程序⾄少都会有两个缓冲区。显示在屏幕上的称为屏幕缓冲区,没有显示的称为离屏缓冲区。在⼀个缓冲区渲染完成之后,通过将屏幕缓冲区和离屏缓冲区交换,实现图像在屏幕上的显示。

    • 由于显示器的刷新⼀般是逐⾏进⾏的,因此为了防⽌交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧,因此交换⼀般会等待显示器刷新完成的信号,在显示器两次刷新的间隔中进⾏交换,这个信号就被称为垂直同步信号,这个技术被称为垂直同步

    • 使⽤了双缓冲区和垂直同步技术之后,由于总是要等待缓冲区交换之后再进⾏下⼀帧的渲染,使得帧率⽆法完全达到硬件允许的最⾼⽔平。为了解决这个问题,引⼊了三缓冲区技术,在等待垂直同步时,来回交替渲染两个离屏的缓冲区,⽽垂直同步发⽣时,屏幕缓冲区和最近渲染完成的离屏缓冲区交换,实现充分利⽤硬件性能的⽬的

13.坐标系

      OpenGl常见的坐标系有:

        1. Object or model coordinates(物体或模型坐标系)每一个实物都有自己的坐标系,在高中数学中,以自身建立的坐标系,自身坐标系由世界坐标系平移而来

        2. World coordinates(世界坐标系)个人理解为地球相对自己建立的坐标系,地球上所有生物都处于这个坐标系当中

        3. Eye (or Camera) coordinates(眼(或相机)坐标系)

        4. Normalized device coordinates(标准化的设备坐标系)

        5. Window (or screen) coordinates(.窗口(或屏幕)坐标系)个人理解为iOS下

        6.Clip coordinates(裁剪坐标系)主要作用是当图形/图像超出时,按照这个坐标系裁剪,裁剪好之后转换到screen坐标系

14.正投影/透视投影

        正投影:类似于照镜子,1:1形成图形大小,这里不做重点讲解

        透视投影:在OpenGL中,如果想对模型进行操作,就要对这个模型的状态(当前的矩阵)乘上这个操作对应的一个矩阵.如果乘以变换矩阵(平移, 缩放, 旋转), 那相乘之后, 模型的位置被变换;如果乘以投影矩阵(将3D物体投影到2D平面), 相乘后, 模型的投影方式被设置;如果乘以纹理矩阵(), 模型的纹理方式被设置.而用来指定乘以什么类型的矩阵, 就是glMatriMode(GLenummode);glMatrixMode有3种模式: GL_PROJECTION 投影, GL_MODELVIEW 模型视图, GL_TEXTURE 纹理.所以,在操作投影矩阵以前,需要调用函数:glMatrixMode(GL_PROJECTION); //将当前矩阵指定为投影矩阵然后把矩阵设为单位矩阵






作者:枫紫
链接:https://www.jianshu.com/p/03d3a5ab2db0

收起阅读 »

一行代码集成Android推送!一个轻量级、可插拔的Android消息推送框架。

快速集成指南添加Gradle依赖1.先在项目根目录的 build.gradle 的 repositories 添加:allprojects { repositories { ... maven { url "https:...
继续阅读 »

快速集成指南

添加Gradle依赖

1.先在项目根目录的 build.gradle 的 repositories 添加:

allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}

2.添加XPush主要依赖:

dependencies {
...
//推送核心库
implementation 'com.github.xuexiangjys.XPush:xpush-core:1.0.1'
//推送保活库
implementation 'com.github.xuexiangjys.XPush:keeplive:1.0.1'
}

3.添加第三方推送依赖(根据自己的需求进行添加,当然也可以全部添加)

dependencies {
...
//选择你想要集成的推送库
implementation 'com.github.xuexiangjys.XPush:xpush-jpush:1.0.1'
implementation 'com.github.xuexiangjys.XPush:xpush-umeng:1.0.1'
implementation 'com.github.xuexiangjys.XPush:xpush-huawei:1.0.1'
implementation 'com.github.xuexiangjys.XPush:xpush-xiaomi:1.0.1'
implementation 'com.github.xuexiangjys.XPush:xpush-xg:1.0.1'
}

初始化XPush配置

1.注册消息推送接收器。方法有两种,选其中一种就行了。

  • 如果你想使用XPushManager提供的消息管理,直接在AndroidManifest.xml中注册框架默认提供的XPushReceiver。当然你也可以继承XPushReceiver,并重写相关方法。

  • 如果你想实现自己的消息管理,可继承AbstractPushReceiver类,重写里面的方法,并在AndroidManifest.xml中注册。

    <!--自定义消息推送接收器-->
<receiver android:name=".push.CustomPushReceiver">
<intent-filter>
<action android:name="com.xuexiang.xpush.core.action.RECEIVE_CONNECT_STATUS_CHANGED" />
<action android:name="com.xuexiang.xpush.core.action.RECEIVE_NOTIFICATION" />
<action android:name="com.xuexiang.xpush.core.action.RECEIVE_NOTIFICATION_CLICK" />
<action android:name="com.xuexiang.xpush.core.action.RECEIVE_MESSAGE" />
<action android:name="com.xuexiang.xpush.core.action.RECEIVE_COMMAND_RESULT" />

<category android:name="${applicationId}" />
</intent-filter>
</receiver>

<!--默认的消息推送接收器-->
<receiver android:name="com.xuexiang.xpush.core.receiver.impl.XPushReceiver">
<intent-filter>
<action android:name="com.xuexiang.xpush.core.action.RECEIVE_CONNECT_STATUS_CHANGED" />
<action android:name="com.xuexiang.xpush.core.action.RECEIVE_NOTIFICATION" />
<action android:name="com.xuexiang.xpush.core.action.RECEIVE_NOTIFICATION_CLICK" />
<action android:name="com.xuexiang.xpush.core.action.RECEIVE_MESSAGE" />
<action android:name="com.xuexiang.xpush.core.action.RECEIVE_COMMAND_RESULT" />

<category android:name="${applicationId}" />
</intent-filter>
</receiver>

注意,如果你的Android设备是8.0及以上的话,静态注册的广播是无法正常生效的,解决的方法有两种:

  • 动态注册消息推送接收器

  • 修改推送消息的发射器

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//Android8.0静态广播注册失败解决方案一:动态注册
XPush.registerPushReceiver(new CustomPushReceiver());

//Android8.0静态广播注册失败解决方案二:修改发射器
XPush.setIPushDispatcher(new Android26PushDispatcherImpl(CustomPushReceiver.class));
}

2.在AndroidManifest.xml的application标签下,添加第三方推送客户端实现类.

需要注意的是,这里注册的PlatformNamePlatformCode必须要和推送客户端实现类中的一一对应才行。

<!--name格式:XPush_[PlatformName]_[PlatformCode]-->
<!--value格式:对应客户端实体类的全类名路径-->

<!--如果引入了xpush-jpush库-->
<meta-data
android:name="XPush_JPush_1000"
android:value="com.xuexiang.xpush.jpush.JPushClient" />

<!--如果引入了xpush-umeng库-->
<meta-data
android:name="XPush_UMengPush_1001"
android:value="com.xuexiang.xpush.umeng.UMengPushClient" />

<!--如果引入了xpush-huawei库-->
<meta-data
android:name="XPush_HuaweiPush_1002"
android:value="com.xuexiang.xpush.huawei.HuaweiPushClient" />

<!--如果引入了xpush-xiaomi库-->
<meta-data
android:name="XPush_MIPush_1003"
android:value="com.xuexiang.xpush.xiaomi.XiaoMiPushClient" />

<!--如果引入了xpush-xg库-->
<meta-data
android:name="XPush_XGPush_1004"
android:value="@string/xpush_xg_client_name" />

3.添加第三方AppKey和AppSecret.

这里的AppKey和AppSecret需要我们到各自的推送平台上注册应用后获得。注意如果使用了xpush-xiaomi,那么需要在AndroidManifest.xml添加小米的AppKey和AppSecret(注意下面的“\ ”必须加上,否则获取到的是float而不是String,就会导致id和key获取不到正确的数据)。

<!--极光推送静态注册-->
<meta-data
android:name="JPUSH_CHANNEL"
android:value="default_developer" />
<meta-data
android:name="JPUSH_APPKEY"
android:value="a32109db64ebe04e2430bb01" />

<!--友盟推送静态注册-->
<meta-data
android:name="UMENG_APPKEY"
android:value="5d5a42ce570df37e850002e9" />
<meta-data
android:name="UMENG_MESSAGE_SECRET"
android:value="4783a04255ed93ff675aca69312546f4" />

<!--华为HMS推送静态注册-->
<meta-data
android:name="com.huawei.hms.client.appid"
android:value="101049475"/>

<!--小米推送静态注册,下面的“\ ”必须加上,否则将无法正确读取-->
<meta-data
android:name="MIPUSH_APPID"
android:value="\ 2882303761518134164"/>
<meta-data
android:name="MIPUSH_APPKEY"
android:value="\ 5371813415164"/>

<!--信鸽推送静态注册-->
<meta-data
android:name="XGPUSH_ACCESS_ID"
android:value="2100343759" />
<meta-data
android:name="XGPUSH_ACCESS_KEY"
android:value="A7Q26I8SH7LV" />

4.在Application中初始化XPush

初始化XPush的方式有两种,根据业务需要选择一种方式就行了:

  • 静态注册
/**
* 静态注册初始化推送
*/
private void initPush() {
XPush.debug(BuildConfig.DEBUG);
//静态注册,指定使用友盟推送客户端
XPush.init(this, new UMengPushClient());
XPush.register();
}
  • 动态注册
/**
* 动态注册初始化推送
*/
private void initPush() {
XPush.debug(BuildConfig.DEBUG);
//动态注册,根据平台名或者平台码动态注册推送客户端
XPush.init(this, new IPushInitCallback() {
@Override
public boolean onInitPush(int platformCode, String platformName) {
String romName = RomUtils.getRom().getRomName();
if (romName.equals(SYS_EMUI)) {
return platformCode == HuaweiPushClient.HUAWEI_PUSH_PLATFORM_CODE && platformName.equals(HuaweiPushClient.HUAWEI_PUSH_PLATFORM_NAME);
} else if (romName.equals(SYS_MIUI)) {
return platformCode == XiaoMiPushClient.MIPUSH_PLATFORM_CODE && platformName.equals(XiaoMiPushClient.MIPUSH_PLATFORM_NAME);
} else {
return platformCode == JPushClient.JPUSH_PLATFORM_CODE && platformName.equals(JPushClient.JPUSH_PLATFORM_NAME);
}
}
});
XPush.register();
}

如何使用XPush

1、推送的注册和注销

  • 通过调用XPush.register(),即可完成推送的注册。

  • 通过调用XPush.unRegister(),即可完成推送的注销。

  • 通过调用XPush.getPushToken(),即可获取消息推送的Token(令牌)。

  • 通过调用XPush.getPlatformCode(),即可获取当前使用推送平台的码。

2、推送的标签(tag)处理

  • 通过调用XPush.addTags(),即可添加标签(支持传入多个)。

  • 通过调用XPush.deleteTags(),即可删除标签(支持传入多个)。

  • 通过调用XPush.getTags(),即可获取当前设备所有的标签。

需要注意的是,友盟推送和信鸽推送目前暂不支持标签的获取,华为推送不支持标签的所有操作,小米推送每次只支持一个标签的操作。

3、推送的别名(alias)处理

  • 通过调用XPush.bindAlias(),即可绑定别名。

  • 通过调用XPush.unBindAlias(),即可解绑别名。

  • 通过调用XPush.getAlias(),即可获取当前设备所绑定的别名。

需要注意的是,友盟推送和信鸽推送目前暂不支持别名的获取,华为推送不支持别名的所有操作。

4、推送消息的接收

  • 通过调用XPushManager.get().register()方法,注册消息订阅MessageSubscriber,即可在任意地方接收到推送的消息。

  • 通过调用XPushManager.get().unregister()方法,即可取消消息的订阅。

这里需要注意的是,消息订阅的回调并不一定是在主线程,因此在回调中如果进行了UI的操作,一定要确保切换至主线程。下面演示代码中使用了我的另一个开源库XAOP,只通过@MainThread注解就能自动切换至主线程,可供参考。

/**
* 初始化监听
*/
@Override
protected void initListeners() {
XPushManager.get().register(mMessageSubscriber);
}

private MessageSubscriber mMessageSubscriber = new MessageSubscriber() {
@Override
public void onMessageReceived(CustomMessage message) {
showMessage(String.format("收到自定义消息:%s", message));
}

@Override
public void onNotification(Notification notification) {
showMessage(String.format("收到通知:%s", notification));
}
};

@MainThread
private void showMessage(String msg) {
tvContent.setText(msg);
}


@Override
public void onDestroyView() {
XPushManager.get().unregister(mMessageSubscriber);
super.onDestroyView();
}

5、推送消息的过滤处理

  • 通过调用XPushManager.get().addFilter()方法,可增加对订阅推送消息的过滤处理。对于一些我们不想处理的消息,可以通过消息过滤器将它们筛选出来。

  • 通过调用XPushManager.get().removeFilter()方法,即可去除消息过滤器。

/**
* 初始化监听
*/
@Override
protected void initListeners() {
XPushManager.get().addFilter(mMessageFilter);
}

private IMessageFilter mMessageFilter = new IMessageFilter() {
@Override
public boolean filter(Notification notification) {
if (notification.getContent().contains("XPush")) {
showMessage("通知被拦截");
return true;
}
return false;
}

@Override
public boolean filter(CustomMessage message) {
if (message.getMsg().contains("XPush")) {
showMessage("自定义消息被拦截");
return true;
}
return false;
}
};

@Override
public void onDestroyView() {
XPushManager.get().removeFilter(mMessageFilter);
super.onDestroyView();
}

6、推送通知的点击处理

对于通知的点击事件,我们可以处理得更优雅,自定义其点击后的动作,打开我们想让用户看到的页面。

我们可以在全局消息推送的接收器IPushReceiver中的onNotificationClick回调中,增加打开指定页面的操作。

@Override
public void onNotificationClick(Context context, XPushMsg msg) {
super.onNotificationClick(context, msg);
//打开自定义的Activity
Intent intent = IntentUtils.getIntent(context, TestActivity.class, null, true);
intent.putExtra(KEY_PARAM_STRING, msg.getContent());
intent.putExtra(KEY_PARAM_INT, msg.getId());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
ActivityUtils.startActivity(intent);
}

需要注意的是,这需要你在消息推送平台推送的通知使用的是自定义动作或者打开指定页面类型,并且传入的Intent uri 内容满足如下格式:

  • title:通知的标题

  • content:通知的内容

  • extraMsg:通知附带的拓展字段,可存放json或其他内容

  • keyValue:通知附带的键值对

xpush://com.xuexiang.xpush/notification?title=这是一个通知&content=这是通知的内容&extraMsg=xxxxxxxxx&keyValue={"param1": "1111", "param2": "2222"}

当然你也可以自定义传入的Intent uri 格式,具体可参考项目中的XPushNotificationClickActivityAndroidManifest.xml


代码下载:XPush.zip

收起阅读 »

一行代码完成http请求!WelikeAndroid 一款引入即用的便捷开发框架

#WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.WelikeAndroid目前包含五个大模块:异常安全隔离模...
继续阅读 »

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

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入门:

  • 第一步,取得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.
收起阅读 »

Android上一个非常优雅好用的日历,全面自定义UI,自定义周起始

CalenderViewAndroid上一个非常优雅、高度自定义、性能高效的日历控件,完美支持周视图,支持标记、自定义颜色、农历等,任意控制月视图显示、任意日期拦截条件、自定义周起始等。Canvas绘制,极速性能、占用内存低,,支持简单定制即可实现任意自定义布...
继续阅读 »


CalenderView

Android上一个非常优雅、高度自定义、性能高效的日历控件,完美支持周视图,支持标记、自定义颜色、农历等,任意控制月视图显示、任意日期拦截条件、自定义周起始等。Canvas绘制,极速性能、占用内存低,,支持简单定制即可实现任意自定义布局、自定义UI,支持收缩展开、性能非常高效, 这个控件内存和效率优势相当明显,而且真正做到收缩+展开,适配多种场景,支持同时多种颜色标记日历事务,支持多点触控,你真的想不到日历还可以如此优雅!更多参考用法请移步Demo,Demo实现了一些精美的自定义效果,用法仅供参考。

插拔式设计

插拔式设计:好比插座一样,插上灯泡就会亮,插上风扇就会转,看用户需求什么而不是看插座有什么,只要是电器即可。此框架使用插拔式,既可以在编译时指定年月日视图,如:app:month_view="xxx.xxx.MonthView.class",也可在运行时动态更换年月日视图,如:CalendarView.setMonthViewClass(MonthView.Class),从而达到UI即插即用的效果,相当于框架不提供UI实现,让UI都由客户端实现,不至于日历UI都千篇一律,只需遵守插拔式接口即可随意定制,自由化程度非常高。

AndroidStudio请使用3.5以上版本

support使用版本

implementation 'com.haibin:calendarview:3.6.8'

Androidx使用版本

implementation 'com.haibin:calendarview:3.6.9'
<dependency>
<groupId>com.haibin</groupId>
<artifactId>calendarview</artifactId>
<version>3.6.9</version>
<type>pom</type>
</dependency>

混淆proguard-rules

-keepclasseswithmembers class * {
public <init>(android.content.Context);
}

或者针对性的使用混淆,请自行配置测试!

-keep class your project path.MonthView {
public <init>(android.content.Context);
}
-keep class your project path.WeekBar {
public <init>(android.content.Context);
}
-keep class your project path.WeekView {
public <init>(android.content.Context);
}
-keep class your project path.YearView {
public <init>(android.content.Context);
}


特别的,请注意不要复制这三个路径,自行替换您自己的自定义路径

app:month_view="com.haibin.calendarviewproject.simple.SimpleMonthView"
app:week_view="com.haibin.calendarviewproject.simple.SimpleWeekView"
app:week_bar_view="com.haibin.calendarviewproject.EnglishWeekBar"

使用方法

 <com.haibin.calendarview.CalendarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:default_status="shrink"
app:calendar_show_mode="only_week_view"
app:calendar_content_view_id="@+id/recyclerView">

<com.haibin.calendarview.CalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#fff"
app:month_view="com.haibin.calendarviewproject.simple.SimpleCalendarCardView"
app:week_view="com.haibin.calendarviewproject.simple.SimpleWeekView"
app:week_bar_view="com.haibin.calendarviewproject.EnglishWeekBar"
app:calendar_height="50dp"
app:current_month_text_color="#333333"
app:current_month_lunar_text_color="#CFCFCF"
app:min_year="2004"
app:other_month_text_color="#e1e1e1"
app:scheme_text="假"
app:scheme_text_color="#333"
app:scheme_theme_color="#333"
app:selected_text_color="#fff"
app:selected_theme_color="#333"
app:week_start_with="mon"
app:week_background="#fff"
app:month_view_show_mode="mode_only_current"
app:week_text_color="#111" />

<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#d4d4d4" />
</com.haibin.calendarview.CalendarLayout>

CalendarView attrs

<declare-styleable name="CalendarView">

<attr name="calendar_padding" format="dimension" /><!--日历内部左右padding-->

<attr name="month_view" format="color" /> <!--自定义类日历月视图路径-->
<attr name="week_view" format="string" /> <!--自定义类周视图路径-->
<attr name="week_bar_height" format="dimension" /> <!--星期栏的高度-->
<attr name="week_bar_view" format="color" /> <!--自定义类周栏路径,通过自定义则 week_text_color week_background xml设置无效,当仍可java api设置-->
<attr name="week_line_margin" format="dimension" /><!--线条margin-->

<attr name="week_line_background" format="color" /><!--线条颜色-->
<attr name="week_background" format="color" /> <!--星期栏的背景-->
<attr name="week_text_color" format="color" /> <!--星期栏文本颜色-->
<attr name="week_text_size" format="dimension" /><!--星期栏文本大小-->

<attr name="current_day_text_color" format="color" /> <!--今天的文本颜色-->
<attr name="current_day_lunar_text_color" format="color" /><!--今天的农历文本颜色-->

       <attr name="calendar_height" format="string" /> <!--日历每项的高度,56dp-->
<attr name="day_text_size" format="string" /> <!--天数文本大小-->
<attr name="lunar_text_size" format="string" /> <!--农历文本大小-->

<attr name="scheme_text" format="string" /> <!--标记文本-->
<attr name="scheme_text_color" format="color" /> <!--标记文本颜色-->
<attr name="scheme_month_text_color" format="color" /> <!--标记天数文本颜色-->
<attr name="scheme_lunar_text_color" format="color" /> <!--标记农历文本颜色-->

<attr name="scheme_theme_color" format="color" /> <!--标记的颜色-->

<attr name="selected_theme_color" format="color" /> <!--选中颜色-->
<attr name="selected_text_color" format="color" /> <!--选中文本颜色-->
<attr name="selected_lunar_text_color" format="color" /> <!--选中农历文本颜色-->

<attr name="current_month_text_color" format="color" /> <!--当前月份的字体颜色-->
<attr name="other_month_text_color" format="color" /> <!--其它月份的字体颜色-->

<attr name="current_month_lunar_text_color" format="color" /> <!--当前月份农历节假日颜色-->
<attr name="other_month_lunar_text_color" format="color" /> <!--其它月份农历节假日颜色-->

<!-- 年视图相关 -->
<attr name="year_view_month_text_size" format="dimension" /> <!-- 年视图月份字体大小 -->
<attr name="year_view_day_text_size" format="dimension" /> <!-- 年视图月份日期字体大小 -->
<attr name="year_view_month_text_color" format="color" /> <!-- 年视图月份字体颜色 -->
<attr name="year_view_day_text_color" format="color" /> <!-- 年视图日期字体颜色 -->
<attr name="year_view_scheme_color" format="color" /> <!-- 年视图标记颜色 -->

<attr name="min_year" format="integer" />  <!--最小年份1900-->
 <attr name="max_year" format="integer" /> <!--最大年份2099-->
<attr name="min_year_month" format="integer" /> <!--最小年份对应月份-->
<attr name="max_year_month" format="integer" /> <!--最大年份对应月份-->

<!--月视图是否可滚动-->
<attr name="month_view_scrollable" format="boolean" />
<!--周视图是否可滚动-->
<attr name="week_view_scrollable" format="boolean" />
<!--年视图是否可滚动-->
<attr name="year_view_scrollable" format="boolean" />
       
<!--配置你喜欢的月视图显示模式模式-->
<attr name="month_view_show_mode">
<enum name="mode_all" value="0" /> <!--全部显示-->
<enum name="mode_only_current" value="1" /> <!--仅显示当前月份-->
<enum name="mode_fix" value="2" /> <!--自适应显示,不会多出一行,但是会自动填充-->
</attr>

<!-- 自定义周起始 -->
<attr name="week_start_with">
<enum name="sun" value="1" />
<enum name="mon" value="2" />
<enum name="sat" value="7" />
</attr>

<!-- 自定义选择模式 -->
<attr name="select_mode">
<enum name="default_mode" value="0" />
<enum name="single_mode" value="1" />
<enum name="range_mode" value="2" />
</attr>

<!-- 当 select_mode=range_mode -->
<attr name="min_select_range" format="integer" />
<attr name="max_select_range" format="integer" />
</declare-styleable>

CalendarView api


public void setRange(int minYear, int minYearMonth, int minYearDay,
int maxYear, int maxYearMonth, int maxYearDay) ;//置日期范围

public int getCurDay(); //今天
public int getCurMonth(); //当前的月份
public int getCurYear(); //今年

public boolean isYearSelectLayoutVisible();//年月份选择视图是否打开
public void closeYearSelectLayout();//关闭年月视图选择布局
public void showYearSelectLayout(final int year); //快速弹出年份选择月份

public void setOnMonthChangeListener(OnMonthChangeListener listener);//月份改变事件

public void setOnYearChangeListener(OnYearChangeListener listener);//年份切换事件

public void setOnCalendarSelectListener(OnCalendarSelectListener listener)//日期选择事件

public void setOnCalendarLongClickListener(OnCalendarLongClickListener listener);//日期长按事件

public void setOnCalendarLongClickListener(OnCalendarLongClickListener listener, boolean preventLongPressedSelect);//日期长按事件

public void setOnCalendarInterceptListener(OnCalendarInterceptListener listener);//日期拦截和日期有效性绘制

public void setSchemeDate(Map<String, Calendar> mSchemeDates);//标记日期

public void update();//动态更新

public Calendar getSelectedCalendar(); //获取选择的日期

/**
* 特别的,如果你需要自定义或者使用其它选择器,可以用以下方法进行和日历联动
*/
public void scrollToCurrent();//滚动到当前日期

public void scrollToCurrent(boolean smoothScroll);//滚动到当前日期

public void scrollToYear(int year);//滚动到某一年

public void scrollToPre();//滚动到上一个月

public void scrollToNext();//滚动到下一个月

public void scrollToCalendar(int year, int month, int day);//滚动到指定日期

public Calendar getMinRangeCalendar();//获得最小范围日期

public Calendar getMaxRangeCalendar();//获得最大范围日期

/**
* 设置背景色
*
* @param monthLayoutBackground 月份卡片的背景色
* @param weekBackground 星期栏背景色
* @param lineBg 线的颜色
*/
public void setBackground(int monthLayoutBackground, int weekBackground, int lineBg)

/**
* 设置文本颜色
*
* @param curMonthTextColor 当前月份字体颜色
* @param otherMonthColor 其它月份字体颜色
* @param lunarTextColor 农历字体颜色
*/
public void setTextColor(int curMonthTextColor,int otherMonthColor,int lunarTextColor)

/**
* 设置选择的效果
*
* @param style 选中的style CalendarCardView.STYLE_FILL or CalendarCardView.STYLE_STROKE
* @param selectedThemeColor 选中的标记颜色
* @param selectedTextColor 选中的字体颜色
*/
public void setSelectedColor(int style, int selectedThemeColor, int selectedTextColor)

/**
* 设置标记的色
*
* @param style 标记的style CalendarCardView.STYLE_FILL or CalendarCardView.STYLE_STROKE
* @param schemeColor 标记背景色
* @param schemeTextColor 标记字体颜色
*/
public void setSchemeColor(int style, int schemeColor, int schemeTextColor)


/**
* 设置星期栏的背景和字体颜色
*
* @param weekBackground 背景色
* @param weekTextColor 字体颜色
*/
public void setWeeColor(int weekBackground, int weekTextColor)

CalendarLayout api

public void expand(); //展开

public void shrink(); //收缩

public boolean isExpand();//是否展开了

CalendarLayout attrs


<!-- 日历显示模式 -->
<attr name="calendar_show_mode">
<enum name="both_month_week_view" value="0" /><!-- 默认都有 -->
<enum name="only_week_view" value="1" /><!-- 仅周视图 -->
<enum name="only_month_view" value="2" /><!-- 仅月视图 -->
</attr>

<attr name="default_status">
<enum name="expand" value="0" /> <!--默认展开-->
<enum name="shrink" value="1" /><!--默认搜索-->
</attr>

<attr name="calendar_content_view_id" format="integer" /><!--内容布局id-->

代码下载:CalendarView.zip

收起阅读 »

iOS-ijkplayer集成

ijkplayer是bibiliB站开源的一个三方,封装好了ffmpeg,可以去面向对象去开发。苹果提供了:AVPlayer播放不了直播文件。需要自己去基于ffmpeg播放。1.搜索查找ijkplayer2.克隆ijkplayer到桌面cd Desktop/ ...
继续阅读 »

ijkplayer是bibiliB站开源的一个三方,封装好了ffmpeg,可以去面向对象去开发。

苹果提供了:AVPlayer播放不了直播文件。需要自己去基于ffmpeg播放。

1.搜索查找ijkplayer





2.克隆ijkplayer到桌面

cd Desktop/
git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-ios



3.下载ffmpeg


4.编译ffmpeg


编译很多情况,64位、32位


ps: 如果提示错误:

./libavutil/arm/asm.S:50:9: error: unknown directive
.arch armv7-a
^
make: *** [libavcodec/arm/aacpsdsp_neon.o] Error 1
最新的 Xcode 已经弱化了对 32 位的支持, 解决方法:
在 compile-ffmpeg.sh 中删除 armv7 , 修改如:
FF_ALL_ARCHS_IOS8_SDK="arm64 i386 x86_64"
再重新执行出现错误的命令: ./compile-ffmpeg.sh all

5.打包framwork并合并

大家会发现除了IJKMediaFramework这个目标,还有一个叫IJKMediaFrameworkWithSSL,但是不推荐使用这个,因为大部分基于ijkplayer的第三方框架都是使用的前者,你把后者导入项目还是会报找不到包的错误,就算你要支持https也推荐使用前者,然后按照上一步添加openssl即可支持

5.1,配置释放模式如下图



5.2,打包真机框架


如图操作,然后按键命令+ B编译即可

如果之前的步骤删除了compile-ffmpeg.sh中armv7,这里会报错,我们直接注释掉就好


用Xcode9可以找到这个 ,但是用Xcode10找不到这个 我只能用Xcode注释完,在用Xcode10编译就没问题了

5.3,打包模拟器 framework


如图操作,然后命令+ B编译即可

5.4,合并框架
如果只需要真机运行或者模拟器运行,可以不用合并,直接找到对应的框架导入项目即可; 一般我们为了方便会合并框架,这样就同时支持模拟器和真机运行。
先找到生成框架的目录:



准备合并:

打开终端, 先 cd 到 Products 目录下
然后执行: lipo -create 真机framework路径 模拟器framework路径 -output 合并的文件路径

lipo -create Release-iphoneos/IJKMediaFramework.framework/IJKMediaFramework Release-iphonesimulator/IJKMediaFramework.framework/IJKMediaFramework -output IJKMediaFramework

合并完成:
可以看到这里生成了一个大概两倍大小的文件, 将生成的 IJKMediaFramework 文件替换掉 真机framework 中的 IJKMediaFramework 文件,然后这个替换掉文件的 真机framework 就是我们需要的 通用的framework 了。



6.集成 framework 到项目中

1、导入 framework

直接将 IJKMediaFramework.framework 拖入到工程中即可
注意记得勾选 Copy items if needed 和 对应的 target

2、添加下列依赖到工程


【参考文章】:
1、ijkplayer 的编译、打包 framework 和 https 支持
2、armv7 armv7s arm64
3、iOS IJKPlayer项目集成(支持RTSP)
4、可用rtmp直播源

链接:https://www.jianshu.com/p/9a69af13835e

收起阅读 »

一文速览苹果WWDC 2021:没有硬件发布的夜晚,iOS 15才是主角

WWDC 2021在成功把M1芯片置入到了iPad Pro之后,我们最关心的另一个问题是,iPad Pro是否能有足够的软件生态来最大程度的利用好这颗高性能核心。当你带着这样的期待去收看这届的WWDC 2021之时,你会发现自己的全部期待都落了空——iPadO...
继续阅读 »

WWDC 2021

在成功把M1芯片置入到了iPad Pro之后,我们最关心的另一个问题是,iPad Pro是否能有足够的软件生态来最大程度的利用好这颗高性能核心。

当你带着这样的期待去收看这届的WWDC 2021之时,你会发现自己的全部期待都落了空——iPadOS并没有得到给力的软件生态支持,并且外界谣传的14英寸版的MacBook Pro也并没有登场。

这次的WWDC 2021总结起来,就是三个关键词:共享、统一与隐私。

iOS 15:更注重分享,也更注重你的「数字健康」

视频通话变得越来越重要,苹果也为自家iOS 15加入了语音突显模式和宽频谱模式。前者可使用机器学习降低环境噪音,增强人声;后者将捕捉周围一切的声音,可以理解为没有经过通话降噪的原声。




Share Play

当然,比起音频增强,更多人关心的是「人像模式」——在使用FaceTime之时,iPhone不仅可以帮助你虚化掉背景,更为重要的是它居然可以帮助你进行实时美颜,当然仅限于在FaceTime通话中。

好在现在的FaceTime已经支持网页接入了,换句话来说,就是除了苹果设备之外,Windows设备和Android手机也能够通过苹果用户分享的链接加入到FaceTime通话中了。

一旦接受了这种设定,你就会发现苹果有多重视「与朋友/家人共享」这件事情了。这里苹果推出了功能,也是这次全系统更新的核心功能——SharePlay。它可让用户在FaceTime通话时,共享音乐、视频以及屏幕。

有了这个功能,你就可以像使用钉钉/飞书/腾讯会议等等一系列的协同类App一样,与同事协同工作,与家人一同刷剧,与朋友一同打游戏

分享不止于此,在苹果的官方信息应用iMessage中,现在新加入了分享Apple Music中的音乐,Apple News中的文章等等功能。





专注模式

为了给你的现实生活和数字生活划上一道界线,iOS 15终于加入了专注模式。这次专注模式,笔者认为是此前「睡眠模式」的延伸——如果说睡眠模式是屏蔽掉一切通知消息,那么专注模式就是可选择性的屏蔽。

你可以设置不同的专注模式,iOS 15会帮你筛选相应的信息。比如,工作模式下,你就只能收到钉钉/微信的消息,而游戏和视频类App的推送就会被忽略掉,并且iOS 15会通过算法判断,哪一项消息更重要,并且将之置顶显示,以避免你错过重要信息。

当然,你也可以自定义不同「专注页面」,在开启相应的专注模式之后,iOS设备就会自动显示相对应的页面。



iOS 15新功能

每一年的iOS系统升级,同样会伴随大量的系统应用升级,这次也不例外


Text Live

今年的相机和图库功能的升级方面主要是体现在,对于AI算法的利用层面上。新增的Text Live功能,它可以识别拍摄/现有图片中的文本,不仅能够转换文字,还能够进行翻译,首发支持英语、汉语、法语等七种语言。

图库中的「回忆」功能再次升级,这次用户可以自定义回忆功能,包括音乐、动画、主题等等。系统也可根据照片的内容和风格,自动匹配合适的歌曲、节奏以及呈现的效果。

钱包功能也得到了升级:这次它不仅能添加信用卡和公交卡,它还支持模拟酒店门卡,迪士尼公园门票,甚至是电子sfz。目前尚不清楚,它能否替代掉你的小区门禁卡。

天气和地图应用的更新升级,则更多的体现在视觉动效的呈现上面:不同的天气会有不同的动画效果,海外部分城市的地图,支持查看海拔高度、地标景点、道路细节等。新增的公交模式,可帮助用户尽快找到附近的公交站。

另外,值得一提的是移动端的Safari现在也支持安装浏览器拓展插件了,并且新加入了「标签组」功能——这一功能与微软推出的Edge浏览器的「集锦」功能类似。

iOS 15还为健康应用带来了一些新功能,允许用户与医疗团队共享数据,评估跌倒风险的指标,以及趋势分析等等。此外还可以将健康数据与家庭成员共享,让关心你的人第一时间了解你的身体状态。



AirPods升级

顺便一提,AirPods(主要是AirPods Pro和AirPods Max)也得到了小幅度的功能升级,比如新增了对话增强模式,利用计算音频和波束成形麦克风,AirPods 可实现更清晰的对话;新增了通知播报功能,AirPods 可自动阅读具有时效性的通知内容;以及新增了和AirTag类似的防丢功能。

简单来说,如果AirPods遗失在外,其会自动发出蓝牙信号,路过的iPhone识别到上传到iCloud,直达用户的「查找app」。至于有些音乐发烧友期待的更高清的码率更新并没有到来,Apple Music也只是新增了Dolby Atmos音效。



OS 15支持的设备

令人意外的是,iOS 15支持的机型与 iOS 14基本一致。iPhone 6s、第一代iPhone SE也可升级。开发者预览版现在已经开始推送更新了,至于公测版则在7月份,也就是下个月开始推送更新,正式版会在秋季发布会之后更新。

iPadOS:你要的Mac级应用并未出现

iPadOS大部分的新功能与iOS 15一样,不过苹果还是为大屏幕新增了一些独有功能,比如说更大尺寸的小组件——现在小组件终于能够与App图标混排了。





iPadOS升级一览

借助这一功能,你可以在iPad上打造出更个性化的页面,比如游戏页面,追剧页面等等,同时iPadOS也终于加入了和iOS一样的App资源库的功能。

同时,iPadOS终于更新分屏操作的逻辑:新增了「多任务控制板」和「App组合架」的操作逻辑。通过多任务控制板,你不仅可以双开应用,甚至可以「三开」——就像之前华为的「智慧分屏」功能一样,拥有第三个悬浮的浏览页面。


多任务新特性一览

同时,你还能够将不同的分屏页面「放」在App组合架上,便于你在多个不同的分屏应用之间快速切换。你可以通过拖动应用程序来创建一个新的分屏视图,比传统的多任务还要方便。而这些操作,也都可以借助iPad妙控键盘用快捷键实现。

苹果也对iPadOS上的备忘录功能进行了升级:你可以在任意应用的角落里,通过手指/Apple Pencil滑动呼出备忘录小窗,快速记录包括手写笔记、连接、Safari 高亮内容、便签等等任何一闪而过的灵感。

快速笔记也是支持多设备同步的,例如你在 Safari 中对某段文字做了备注,当你再次浏览时,便会出现快速笔记的缩略图,将你带回之前浏览过的内容。

iPadOS上Swift Playgrounds的更新,可能是这次唯一称得上是与生产力挂钩的升级了。Swift Playgrounds是苹果推出的可视化的编程操作App。这次的更新允许用户直接在Swift Playgrounds中开发App,并且进行调试甚至是直接上架到App Store进行销售。

尽管与Mac采用同一种M1芯片的iPad Pro已经推出,但iPadOS的升级更多的是「适配更大屏幕的iOS」的逻辑,而非是想要将iPadOS打造成更强生产力,能让它取代掉Mac。这还是让笔者有些失望。

watchOS&macOS:小幅度升级,跨设备交互功能亮眼

今年的watchOS更新还是从两个层面上:一是新增了「照片表盘」功能,你可以将任意图片设置成表盘,这张图片是具备景深效果的,你可以通过表冠来调节虚化效果。




watchOS新特性一览

二是在健康应用层面上,watchOS为「呼吸」功能新增了更漂亮的动画,让「睡眠」除了能记录你的睡眠时长之外,还能记录下你的睡眠呼吸频率,从而分析出你的睡眠质量。最后订阅服务,Apple Fitness+则是增加了两种热门体能训练——太极和普拉提。

新的macOS被命名为Monterey,源自加州的蒙特雷市。新功能与iOS保持一致,但拥有足以改变多设备交互方式的Universal Control功能。


Universal Control

简单来说,通过Universal Control,你能在靠近的不同苹果设备之间共享一套键鼠,并且能够在不同设备之间快速共享文件。比如,你可以通过MacBook上的键盘和触控板,修改iPad上的图片/文稿等等,并且可以直接将文字/图片拖动到当前Mac编辑的文稿/剪辑的视频时间线之中。



macOS新特性一览

此次更新中,最让笔者兴奋的功能是,AirPlay to Mac——你终于能够把移动端的内容通过AirPlay的方式直接投屏到Mac上,通过Mac的大屏和更棒的扬声器,享受更舒适的视听体验了。

最后是iOS上的快捷指令功能被移植到了macOS之上,你终于能够通过自动化的指令,在Mac电脑上名正言顺地「偷懒」了。

隐私:从在世到离世,苹果都在为你的隐私考虑

隐私保护一直是苹果极为重视的方面,这次的多系统更新也一样:这次苹果为原生的「邮件」App新增了隐私保护功能,它不仅能够隐藏你的IP地址,还能隐藏你打开邮件的动作,以确保送信者无法得知你何时,甚至是否打开了邮件。

此前,苹果为iOS设备增加了更多的设备权限管理功能,这次则是新增了App隐私报告。你可以透过它很直观地看到哪些应用使用了相关隐私权限的次数和时间等数据。

Siri增加了语音识别功能。默认设置下,发给Siri的对话将在设备本地处理,不上传至云端。这也意味着Siri可以在离线状态下完成更多的指令,比如打开某个应用,设置提醒/闹钟等等。

为了保护用户的隐私,原有的iCloud业务也升级成了iCloud+:在浏览网页之时,用户可以通过iCloud建立一条加密的链接,实现更安全的访问。

iCloud+还可以给用户生成随机的电子邮件地址,并转发到用户的收件箱。所以在网上填写表格或新用户注册时,不必输入个人真实的电子邮箱。

此外,苹果还新增了「数字遗产计划」:用户可以自行定义遗产联系人,万一用户不幸离世之后,透过这项功能,该联系人可以申请访问离世用户的iCloud数据。

iCloud升级成了iCloud+,但其订阅价格并未改变:50GB存储空间每月付费6元,并且支持一个HomeKit安全摄像头(监控视频无限存储空间);200GB存储空间每月付费21元,支持最多五个HomeKit安全摄像头;2TB存储空间每月付费68元,支持无上限个数的HomeKit安全摄像头。

写在最后:它既是连接数字生活的纽带,也是分割现实生活的界线

每一届的WWDC都会带来苹果设备的系统级更新,而每一次的更新都会让苹果生态系统内的设备关系更加紧密,尤其是随着M1芯片的推出以及在不同平特设备上的应用(Mac和iPad Pro)。

这种密切的联系不仅仅是多设备之间的协同,更是不同设备之间同一种交互逻辑,同一种应用功能和界面。从这次的SharePlay功能和FaceTime跨平台支持的功能来看,苹果不仅想要牢牢绑定现有生态内的用户,还想要拉入其他平台的用户进来,体验苹果生态带来的统一性。

当然,这些更新中最有意思的,还是苹果对于科技与生活的理解:在你使用苹果设备之时,它不仅在意用户的数字隐私,也在意用户的身体健康。

在iOS 15公测版推送更新之后,笔者也将会第一时间给各位读者带来最新的体验。


作者/唐植潇

本文首发钛媒体APP

原地址:https://baijiahao.baidu.com/s?id=1701962186485997583&wfr=spider&for=pc



收起阅读 »

iOS年度盛会 --- iOS 15新增8大更新

各位果粉们早上好,相信不少果粉和小编一样,熬夜看完了苹果这次WWDC开发者大会。看完发布会的第一感受--就这?这可能是近几年来最枯燥无味的一场开发者大会了,要不是以为有“one more thing...”,估计小编看到一半就睡着了。开个玩笑,虽然今年的WWD...
继续阅读 »
各位果粉们早上好,相信不少果粉和小编一样,熬夜看完了苹果这次WWDC开发者大会。看完发布会的第一感受--就这?这可能是近几年来最枯燥无味的一场开发者大会了,要不是以为有“one more thing...”,估计小编看到一半就睡着了。
开个玩笑,虽然今年的WWDC大会可能没那么精彩,但苹果还是用了近两小时的时间向我们介绍了iOS 15、iPadOS 15、 watchOS 8、tvOS 15以及MacOS Monterey系统,没有one more thing...,没有新硬件发布!


1、FaceTime视频通话升级
言归正传,接下来就给大家分享一下iOS 15都加入了哪些新功能,首先介绍的是iOS 15系统升级了FaceTime视频通话,包括加入了空间音频的支持、人声增强、人像模式背景虚化、以及第三方设备支持通过链接打开FaceTime等等。

当你使用FaceTime进行通话时,还能给一起视频的小伙伴们分享视频、歌曲。让用户可以在视频的同时,还能一起同步播放视频、歌曲。支持共享的视频包括迪士尼、NBA、HBO以及Tik Tok等知名视频平台。

2、新增「与你共享」功能
为了方便用户共享更多内容,苹果在iOS 15中加入了“与你共享”新功能,首批支持的的App包括照片、音乐、Safari浏览器、播客等等。

3、通知中心升级
iOS 15对通知中心也进行了升级,通知中心图标将更大,让用户能更轻松识别通知来源。不仅如此,iOS 15中还引入了“通知摘要”功能,用户可以自己设置某一个App的通知时间,且通知仅显示重要通知内容,过滤掉无关信息,以保证用户不会错过这条提示。

4、「专注模式」来了
另外,iOS 15还加入了「专注模式」,包括勿扰模式、工作模式、个人模式以及睡眠模式。每个状态可以设置不同的显示通知,并可与其他设备同步。

5、照片新增「实况文本」
接下来就是照片的升级,iOS 15中为照片加入了「实况文本」功能,在这个功能的帮助下,iPhone相机可自动扫描并识别文字,用户可以长按进行选择、复制与粘贴。毫不夸张的说,这个可能是本次iOS 15更新最实用的功能之一了~

得益于神经网络学习的加持,「实况文本」可识别iPhone中所有照片的文字,支持包括英语和中文等7种文字识别,用户可直接搜索照片中的文字找到这张照片。

6、iPhone门禁卡也来了
苹果在iOS 15中加入了钱包钥匙功能,这些钥匙包括公司徽章、酒店房间钥匙和家庭智能锁钥匙。你的iPhone可以解锁你的家、你的车库、你的酒店房间,甚至你的工作场所。如此看来,iPhone当门禁卡的功能来了。

7、天气App升级
天气App在iOS 15进行了升级,不仅可以显示更多关于天气的信息,新的天气App会根据天气情况的变化而改变。

8、地图更智能、更详细
全新升级的地图不仅显示信息更丰富,同时还将为驾驶员提供更多详细道路信息。地图还会自动跟踪用户的出行路线,如果用户迷路,可扫描附近建筑,通过增强现实给用户提供正确路线。假如用户乘坐公交出行,还能提醒用户什么时间下车。

以上就是iOS 15系统的主要更新内容了,小编已经第一时间更新了iOS 15系统。从使用半天的感受来看,目前iOS 15并无明显影响使用到Bug,仅部分新功能还未完全汉化,首个iOS 15测试版还是很流畅的,想要尝鲜iOS 15的果粉可以放心升级。
转自:果粉技巧公众号
收起阅读 »

鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件

前言 基于安卓平台的SlidingMenu侧滑菜单组件(github.com/jfeinstein1… 实现了鸿蒙化迁移和重构,代码已经开源到(gitee.com/isrc\_ohos/… 欢迎各位下载使用并提出宝贵意见! 背景 SlidingMen...
继续阅读 »

前言


基于安卓平台的SlidingMenu侧滑菜单组件(github.com/jfeinstein1…


实现了鸿蒙化迁移和重构,代码已经开源到(gitee.com/isrc\_ohos/…


欢迎各位下载使用并提出宝贵意见!


背景


SlidingMenu_ohos提供了一个侧滑菜单的导航框架,使菜单可以隐藏在手机屏幕的左侧、右侧或左右两侧。当用户使用时,通过左滑或者右滑的方式调出,既节省了主屏幕的空间,也方便用户操作,在很多主流APP中都有广泛的应用。


效果展示


由于菜单从左右两侧调出的显示效果相似,此处仅以菜单从左侧调出为例进行效果展示。


组件未启用时,应用显示主页面。单指触摸屏幕左侧并逐渐向右滑动,菜单页面逐渐显示,主页面逐渐隐藏。向右滑动的距离超过某个阈值时,菜单页面全部显示,效果如图1所示。


鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


图1 菜单展示和隐藏效果图


Sample解析


Sample部分的内容较为简单,主要包含两个部分。一是创建SlidingMenu_ohos组件的对象,可根据用户的实际需求,调用Library的接口,对组件的具体属性进行设置。二是将设置好的组件添加到Ability中。下面将详细介绍组件的使用方法。


1、导入SlidingMenu类


import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu;

2、设置Ability的布局


此布局用作为主页面的布局,在组件隐藏的时候显示。


DirectionalLayout directionalLayout = 
(DirectionalLayout)LayoutScatter.getInstance(this).parse(ResourceTable.Layout_activity_main,null,false);setUIContent(directionalLayout);

3、实例化组件的对象


SlidingMenu slidingMenu = null;
try {
//初始化SlidingMenu实例
slidingMenu = new SlidingMenu(this);
} catch (IOException e) {
e.printStackTrace();
} catch (NotExistException e) {
e.printStackTrace();
}

4、设置组件属性


此步骤可以根据具体需求,设置组件的位置、触发范围、布局、最大宽度等属性。


//设置菜单放置位置
slidingMenu.setMode(SlidingMenu.LEFT);
//设置组件的触发范围
slidingMenu.setTouchScale(100);
//设置组件的布局
slidingMenu.setMenu(ResourceTable.Layout_layout_left_menu);
//设置菜单最大宽度
slidingMenu.setMenuWidth(800);

5、关联Ability


attachToAbility()方法是Library提供的重要方法,用于将菜单组件关联到Ability。其参数SLIDING_WINDOW和SLIDING_CONTENT是菜单的不同模式,SLIDING_WINDOW模式下的菜单包含Title / ActionBar部分,菜单需在整个手机页面上显示,如图2所示;SLIDING_CONTENT模式下的菜单不包括包含Title / ActionBar部分,菜单可以在手机页面的局部范围内显示,如图3所示。


try {
//关联Ability,获取页面展示根节点
slidingMenu.attachToAbility(directionalLayout,this, SlidingMenu.SLIDING_WINDOW);
} catch (NotExistException e) {
e.printStackTrace();
} catch (WrongTypeException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


图2 SLIDING_WINDOW展示效果图


鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


图3 SLIDING_CONTENT展示效果图


Library解析


Library的工程结构如下图所示,CustomViewAbove表示主页面,CustomViewBehind表示菜单页面,SlidingMenu主要用于控制主页面位于菜单页面的上方,还可以设置菜单的宽度、触发范围、显示模式等属性。为了方便解释,以下均以手指从左侧触摸屏幕并向右滑动为例进行讲解,菜单均采用SLIDING_WINDOW的显示模式。


鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


图4 Library的工程结构


1、CustomViewAbove主页面


CustomViewAbove需要监听触摸、移动、抬起和取消等Touch事件,并记录手指滑动的距离和速度。


(1)对Touch事件的处理


Touch事件决定了菜单的显示、移动和隐藏。例如:在菜单的触发范围内,手指向右滑动(POINT_MOVE)时,菜单会跟随滑动到手指所在位置。手指抬起(PRIMARY_POINT_UP)或者取消滑动(CANCEL)时,会依据手指滑动的距离和速度决定菜单页面的下一状态是全部隐藏还是全部显示。


 switch (action) {
//按下
case TouchEvent.PRIMARY_POINT_DOWN:
.....
mInitialMotionX=mLastMotionX=ev.getPointerPosition(mActivePointerId).getX();
break;
//滑动
case TouchEvent.POINT_MOVE:
......
//菜单滑动到此时手指所在位置(x)
left_scrollto(x);
break;
//抬起
case TouchEvent.PRIMARY_POINT_UP:
......
//获得菜单的下一状态(全屏显示或者全部隐藏)
int nextPage = determineTargetPage(pageOffset, initialVelocity,totalDelta);
//设置菜单的下一状态
setCurrentItemInternal(nextPage,initialVelocity);
......
endDrag();
break;
//取消
case TouchEvent.CANCEL:
......
//根据菜单当前状态mCurItem设置菜单下一状态
setCurrentItemInternal(mCurItem);
//结束拖动
endDrag();
break;
}

(2)对滑动的距离和速度的处理


手指抬起时,滑动的速度和距离分别大于最小滑动速度和最小移动距离,判定此时的操作为快速拖动,菜单立即弹出并全部显示,如图5所示。


private int determineTargetPage(float pageOffset, int velocity, int deltaX) {
//获得当前菜单状态,0:左侧菜单正在展示,1:菜单隐藏,2:右侧菜单正在展示
int targetPage = getCurrentItem();
//针对快速拖动的判断
if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
if (velocity > 0 && deltaX > 0) {
targetPage -= 1;
} else if (velocity < 0 && deltaX < 0){
targetPage += 1;
}
}
}

鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


图5 快速拖动效果图


当手指抬起并且不满足快速拖动标准时,需要根据滑动距离判断菜单的隐藏或显示。若菜单已展开的部分超过自身宽度的1/2,菜单立即弹出全部显示,,效果图如图1所示;若不足自身宽度的1/2,则立即弹回全部隐藏,效果图如图6所示。


//获得当前菜单状态,0:左侧菜单正在展示,1:菜单隐藏,2:右侧菜单正在展示
switch (mCurItem){
case 0:
targetPage=1-Math.round(pageOffset);
break;
case 1:
//菜单隐藏时,首先要判断此时菜单的放置状态是左侧还是右侧
if(current_state == SlidingMenu.LEFT){
targetPage = Math.round(1-pageOffset);
}
if(current_state == SlidingMenu.RIGHT){
targetPage = Math.round(1+pageOffset);
}
break;
case 2:
targetPage = Math.round(1+pageOffset);
break;
}

鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


图6 缓慢拖动效果图


(3)菜单显示和隐藏的实现


主页面的左侧边线与手指的位置绑定,当手指向右滑动时,主页面也会随手指向右滑动,在这个过程中菜单页面渐渐展示出来,实现菜单页面随手指滑动慢慢展开的视觉效果。


void setCurrentItemInternal(int item,int velocity) {
//获得菜单的目标状态
item = mViewBehind.getMenuPage(item);
mCurItem = item;
final int destX = getDestScrollX(mCurItem);
/*菜单放置状态为左侧,通过设置主页面的位置实现菜单的弹出展示或弹回隐藏
1.destX=0,主页面左侧边线与屏幕左侧边线对齐,菜单被全部遮挡,实现菜单弹回隐藏
2.destX=MenuWidth,主页面左侧边线向右移动与菜单总宽度相等的距离,实现菜单弹出展示*/
if (mViewBehind.getMode() == SlidingMenu.LEFT) {
mContent.setLeft(destX);
mViewBehind.scrollBehindTo(destX);
}
......
}

// 菜单放置在左侧时的菜单滑动操作
public void left_scrollto(float x) {
//当menu的展示宽度大于最大宽度时仅展示最大宽度
if(x>getMenuWidth()){
x=getMenuWidth();
}
//主页面(主页面左侧边线)和菜单(菜单右侧边线)分别移动到指定位置X
mContent.setLeft((int)x);
mViewBehind.scrollBehindTo((int)x);
}

2、CustomViewBehind 菜单页面


CustomViewBehind为菜单页面,逻辑相比于主页面简单许多。主要负责根据主页面中的Touch事件改变自身状态值,同时向外暴露接口,用于设置或者获取菜单页面的最大宽度、自身状态等属性。


// 设置菜单最大宽度
public void setMenuWidth(int menuWidth) {
this.menuWidth = menuWidth;
}

// 获得菜单最大宽度
public int getMenuWidth() {
return menuWidth;
}

3. SlidingMenu


分别实例化CustomViewAbove和CustomViewBehind的对象,并按照主页面在上菜单页面在下的顺序分别添加到SlidingMenu的容器中。


//添加菜单子控件
addComponent(mViewBehind, behindParams);
//添加主页面子控件
addComponent(mViewAbove, aboveParams);

项目贡献人


徐泽鑫 郑森文 朱伟 陈美汝 王佳思 张馨心


作者:朱伟ISRC

收起阅读 »

面试官问我:如何使用LeakCanary排查Android中的内存泄露,看我如何用漫画装逼!

1)在项目的build.gradle文件添加: debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5' releaseCompile 'com.squareup.leakc...
继续阅读 »



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述

1)在项目的build.gradle文件添加:


    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'

可以看到,debugCompile跟releaseCompile 引入的是不同的包, 在 debug 版本上,集成 LeakCanary 库,并执行内存泄漏监测,而在 release 版本上,集成一个无操作的 wrapper ,这样对程序性能就不会有影响。


2)在Application类添加:


public class LCApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}
}

LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。


如果是简单的检测activity是否存在内存泄漏,上面两个步骤就可以了,是不是很简单。 那么当某个activity存在内存泄漏的时候,会有什么提示呢?LeakCanary会自动展示一个通知栏,点开提示框你会看到引起内存溢出的引用堆栈信息。




这里写图片描述



在这里插入图片描述

具体使用代码


1)Application 相关代码:


public class LCApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}

}

2)泄漏的activity类代码:


public class MainActivity extends Activity {

private Button next;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

next = (Button) findViewById(R.id.next);
next.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
finish();
}
});
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("=================");
}
}
}).start();
}
}

当点击next跳到第二个界面后,LeakCanary会自动展示一个通知栏,点开提示框你会看到引起内存溢出的引用堆栈信息,如上图所示,这样你就很容易定位到原来是线程引用住当前activity,导致activity无法释放。



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述

上面提到,LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。现在很多app都使用到了fragment,那fragment如何检测呢。


1)Application 中获取到refWatcher对象。


public class LCApplication extends Application {

public static RefWatcher refWatcher;

@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
refWatcher = LeakCanary.install(this);
// Normal app init code...
}
}

2)使用 RefWatcher 监控 Fragment:


public abstract class BaseFragment extends Fragment {
@Override public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = LCApplication.refWatcher;
refWatcher.watch(this);
}
}

这样则像监听activity一样监听fragment。其实这种方式一样适用于任何对象,比如图片,自定义类等等,非常方便。




在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述

LeakCanary.install(this)源码如下所示:


public static RefWatcher install(Application application) {
return ((AndroidRefWatcherBuilder)refWatcher(application).listenerServiceClass(DisplayLeakService.class).excludedRefs(AndroidExcludedRefs.createAppDefaults().build())).buildAndInstall();
}

listenerServiceClass(DisplayLeakService.class):用于分析内存泄漏结果信息,然后发送通知给用户。 excludedRefs(AndroidExcludedRefs.createAppDefaults().build()):设置需要忽略的对象,比如某些系统漏洞不需要统计。 buildAndInstall():真正检测内存泄漏的方法,下面将展开分析该方法。


public RefWatcher buildAndInstall() {
RefWatcher refWatcher = this.build();
if(refWatcher != RefWatcher.DISABLED) {
LeakCanary.enableDisplayLeakActivity(this.context);
ActivityRefWatcher.installOnIcsPlus((Application)this.context, refWatcher);
}

return refWatcher;
}

可以看到,上面方法主要做了三件事情: 1.实例化RefWatcher对象,该对象主要作用是检测是否有对象未被回收导致内存泄漏; 2.设置APP图标可见; 3.检测内存



在这里插入图片描述



在这里插入图片描述

RefWatcher的使用后面讲,这边主要看第二件事情的处理过程,及enableDisplayLeakActivity方法的源码


public static void enableDisplayLeakActivity(Context context) {
LeakCanaryInternals.setEnabled(context, DisplayLeakActivity.class, true);
}

public static void setEnabled(Context context, final Class<?> componentClass, final boolean enabled) {
final Context appContext = context.getApplicationContext();
executeOnFileIoThread(new Runnable() {
public void run() {
LeakCanaryInternals.setEnabledBlocking(appContext, componentClass, enabled);
}
});
}

public static void setEnabledBlocking(Context appContext, Class<?> componentClass, boolean enabled) {
ComponentName component = new ComponentName(appContext, componentClass);
PackageManager packageManager = appContext.getPackageManager();
int newState = enabled?1:2;
packageManager.setComponentEnabledSetting(component, newState, 1);
}

可见,最后调用packageManager.setComponentEnabledSetting()方法,实现应用图标的隐藏和显示。



在这里插入图片描述



在这里插入图片描述

接下来,进入真正的内存检查的方法installOnIcsPlus()


public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
if(VERSION.SDK_INT >= 14) {
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
activityRefWatcher.watchActivities();
}
}

该方法实例化出ActivityRefWatcher 对象,该对象用来监听activity的生命周期,具体实现如下所示:


public void watchActivities() {
this.stopWatchingActivities();
this.application.registerActivityLifecycleCallbacks(this.lifecycleCallbacks);
}

private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacks() {
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}

public void onActivityStarted(Activity activity) {
}

public void onActivityResumed(Activity activity) {
}

public void onActivityPaused(Activity activity) {
}

public void onActivityStopped(Activity activity) {
}

public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}

public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.onActivityDestroyed(activity);
}
};



在这里插入图片描述



在这里插入图片描述

调用了registerActivityLifecycleCallbacks方法后,当Activity执行onDestroy方法后,会触发ActivityLifecycleCallbacks 的onActivityDestroyed方法,在当前方法中,调用refWatcher的watch方法,前面已经讲过RefWatcher对象主要作用是检测是否有对象未被回收导致内存泄漏。下面继续看refWatcher的watch方法源码:


public void watch(Object watchedReference) {
this.watch(watchedReference, "");
}

public void watch(Object watchedReference, String referenceName) {
if(this != DISABLED) {
Preconditions.checkNotNull(watchedReference, "watchedReference");
Preconditions.checkNotNull(referenceName, "referenceName");
long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();
this.retainedKeys.add(key);
KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
this.ensureGoneAsync(watchStartNanoTime, reference);
}
}

可以看到,上面方法主要做了三件事情: 1.生成一个随机数key存放在retainedKeys集合中,用来判断对象是否被回收; 2.把当前Activity放到KeyedWeakReference(WeakReference的子类)中; 3.通过查找ReferenceQueue,看该Acitivity是否存在,存在则证明可以被正常回收,不存在则证明可能存在内存泄漏。 前两件事很简单,这边主要看第三件事情的处理过程,及ensureGoneAsync方法的源码:


private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
this.watchExecutor.execute(new Retryable() {
public Result run() {
return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
}
});
}

Result ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
this.removeWeaklyReachableReferences();
if(this.debuggerControl.isDebuggerAttached()) {
return Result.RETRY;
} else if(this.gone(reference)) {
return Result.DONE;
} else {
this.gcTrigger.runGc();
this.removeWeaklyReachableReferences();
if(!this.gone(reference)) {
long startDumpHeap = System.nanoTime();
long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = this.heapDumper.dumpHeap();
if(heapDumpFile == HeapDumper.RETRY_LATER) {
return Result.RETRY;
}

long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
this.heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, this.excludedRefs, watchDurationMs, gcDurationMs, heapDumpDurationMs));
}

return Result.DONE;
}
}

该方法中首先执行removeWeaklyReachableReferences(),从ReferenceQueue队列中查询是否存在该弱引用对象,如果不为空,则说明已经被系统回收了,则将对应的随机数key从retainedKeys集合中删除。


 private void removeWeaklyReachableReferences() {
KeyedWeakReference ref;
while((ref = (KeyedWeakReference)this.queue.poll()) != null) {
this.retainedKeys.remove(ref.key);
}
}

然后通过判断retainedKeys集合中是否存在对应的key判断该对象是否被回收。


private boolean gone(KeyedWeakReference reference) {
return !this.retainedKeys.contains(reference.key);
}

如果没有被系统回收,则手动调用gcTrigger.runGc();后再调用removeWeaklyReachableReferences方法判断该对象是否被回收。


GcTrigger DEFAULT = new GcTrigger() {
public void runGc() {
Runtime.getRuntime().gc();
this.enqueueReferences();
System.runFinalization();
}

private void enqueueReferences() {
try {
Thread.sleep(100L);
} catch (InterruptedException var2) {
throw new AssertionError();
}
}
};

第三行代码为手动触发GC,紧接着线程睡100毫秒,给系统回收的时间,随后通过System.runFinalization()手动调用已经失去引用对象的finalize方法。 通过手动GC该对象还不能被回收的话,则存在内存泄漏,调用heapDumper.dumpHeap()生成.hprof文件目录,并通过heapdumpListener回调到analyze()方法,后面关于dump文件的分析这边就不介绍了,感兴趣的可以自行去看。



在这里插入图片描述



在这里插入图片描述



在这里插入图片描述








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




收起阅读 »

想做图表?Android优秀图表库MPAndroidChart

嗨,你终于来啦 ~ 等你好久啦~ 喜欢的小伙伴欢迎关注,我会定期分享Android知识点及解析,还会不断更新的BATJ面试专题,欢迎大家前来探讨交流,如有好的文章也欢迎投稿。 前言 在项目当中很多时候要对数据进行分析就要用到图表,在gitHub上有很多优秀的...
继续阅读 »

嗨,你终于来啦 ~ 等你好久啦~ 喜欢的小伙伴欢迎关注,我会定期分享Android知识点及解析,还会不断更新的BATJ面试专题,欢迎大家前来探讨交流,如有好的文章也欢迎投稿。

前言


在项目当中很多时候要对数据进行分析就要用到图表,在gitHub上有很多优秀的图表开源库,今天给大家分享的就是MPAndroidChart中的柱状图。简单介绍一下MPAndroidChart:他可以实现图表的拖动,3D,局部查看,数据动态展示等功能。


官方源码地址:github.com/PhilJay/MPA…


废话就不多说啦,先给看大家看看效果图哟



























操作步骤


第一步:需要将依赖的库添加到你的项目中



implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0-alpha'

implementation 'com.google.android.material:material:1.0.0'



第二步:xml中


   <com.github.mikephil.charting.charts.BarChart
android:id="@+id/chart1"
android:layout_width="match_parent"
android:layout_height="300dp"
/>

第三步:ValueFormatter.java


  /**
* Class to format all values before they are drawn as labels.
*/

public abstract class ValueFormatter implements IAxisValueFormatter, IValueFormatter {

/**
* <b>DO NOT USE</b>, only for backwards compatibility and will be removed in future versions.
*
* @param value the value to be formatted
* @param axis the axis the value belongs to
* @return formatted string label
*/

@Override
@Deprecated
public String getFormattedValue(float value, AxisBase axis) {
return getFormattedValue(value);
}

/**
* <b>DO NOT USE</b>, only for backwards compatibility and will be removed in future versions.
* @param value the value to be formatted
* @param entry the entry the value belongs to - in e.g. BarChart, this is of class BarEntry
* @param dataSetIndex the index of the DataSet the entry in focus belongs to
* @param viewPortHandler provides information about the current chart state (scale, translation, ...)
* @return formatted string label
*/

@Override
@Deprecated
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
return getFormattedValue(value);
}

/**
* Called when drawing any label, used to change numbers into formatted strings.
*
* @param value float to be formatted
* @return formatted string label
*/

public String getFormattedValue(float value) {
return String.valueOf(value);
}

/**
* Used to draw axis labels, calls {@link #getFormattedValue(float)} by default.
*
* @param value float to be formatted
* @param axis axis being labeled
* @return formatted string label
*/

public String getAxisLabel(float value, AxisBase axis) {
return getFormattedValue(value);
}

/**
* Used to draw bar labels, calls {@link #getFormattedValue(float)} by default.
*
* @param barEntry bar being labeled
* @return formatted string label
*/

public String getBarLabel(BarEntry barEntry) {
return getFormattedValue(barEntry.getY());
}

/**
* Used to draw stacked bar labels, calls {@link #getFormattedValue(float)} by default.
*
* @param value current value to be formatted
* @param stackedEntry stacked entry being labeled, contains all Y values
* @return formatted string label
*/

public String getBarStackedLabel(float value, BarEntry stackedEntry) {
return getFormattedValue(value);
}

/**
* Used to draw line and scatter labels, calls {@link #getFormattedValue(float)} by default.
*
* @param entry point being labeled, contains X value
* @return formatted string label
*/

public String getPointLabel(Entry entry) {
return getFormattedValue(entry.getY());
}

/**
* Used to draw pie value labels, calls {@link #getFormattedValue(float)} by default.
*
* @param value float to be formatted, may have been converted to percentage
* @param pieEntry slice being labeled, contains original, non-percentage Y value
* @return formatted string label
*/

public String getPieLabel(float value, PieEntry pieEntry) {
return getFormattedValue(value);
}

/**
* Used to draw radar value labels, calls {@link #getFormattedValue(float)} by default.
*
* @param radarEntry entry being labeled
* @return formatted string label
*/

public String getRadarLabel(RadarEntry radarEntry) {
return getFormattedValue(radarEntry.getY());
}

/**
* Used to draw bubble size labels, calls {@link #getFormattedValue(float)} by default.
*
* @param bubbleEntry bubble being labeled, also contains X and Y values
* @return formatted string label
*/

public String getBubbleLabel(BubbleEntry bubbleEntry) {
return getFormattedValue(bubbleEntry.getSize());
}

/**
* Used to draw high labels, calls {@link #getFormattedValue(float)} by default.
*
* @param candleEntry candlestick being labeled
* @return formatted string label
*/

public String getCandleLabel(CandleEntry candleEntry) {
return getFormattedValue(candleEntry.getHigh());
}

}

第四步:MyValueFormatter


    public class MyValueFormatter extends ValueFormatter{
private final DecimalFormat mFormat;
private String suffix;

public MyValueFormatter(String suffix) {
mFormat = new DecimalFormat("0000");
this.suffix = suffix;
}

@Override
public String getFormattedValue(float value) {
return mFormat.format(value) + suffix;
}

@Override
public String getAxisLabel(float value, AxisBase axis) {
if (axis instanceof XAxis) {
return mFormat.format(value);
} else if (value > 0) {
return mFormat.format(value) + suffix;
} else {
return mFormat.format(value);
}
}
}

第五步:MainAcyivity


  package detongs.hbqianze.him.linechart;
import android.os.Bundle;
import android.util.Log;
import android.view.WindowManager;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet;
import com.github.mikephil.charting.interfaces.datasets.IDataSet;
import com.github.mikephil.charting.utils.ColorTemplate;

import java.util.ArrayList;

import detongs.hbqianze.him.linechart.chart.MyValueFormatter;
import detongs.hbqianze.him.linechart.chart.ValueFormatter;

public class MainActivity extends AppCompatActivity {



private BarChart chart;
private TextView te_cache;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_main);


chart = findViewById(R.id.chart1);
te_cache = findViewById(R.id.te_cache);


chart.getDescription().setEnabled(false);

//设置最大值条目,超出之后不会有值
chart.setMaxVisibleValueCount(60);

//分别在x轴和y轴上进行缩放
chart.setPinchZoom(true);
//设置剩余统计图的阴影
chart.setDrawBarShadow(false);
//设置网格布局
chart.setDrawGridBackground(true);
//通过自定义一个x轴标签来实现2,015 有分割符符bug
ValueFormatter custom = new MyValueFormatter(" ");
//获取x轴线
XAxis xAxis = chart.getXAxis();

//设置x轴的显示位置
xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
//设置网格布局
xAxis.setDrawGridLines(true);
//图表将避免第一个和最后一个标签条目被减掉在图表或屏幕的边缘
xAxis.setAvoidFirstLastClipping(false);
//绘制标签 指x轴上的对应数值 默认true
xAxis.setDrawLabels(true);
xAxis.setValueFormatter(custom);
//缩放后x 轴数据重叠问题
xAxis.setGranularityEnabled(true);
//获取右边y标签
YAxis axisRight = chart.getAxisRight();
axisRight.setStartAtZero(true);
//获取左边y轴的标签
YAxis axisLeft = chart.getAxisLeft();
//设置Y轴数值 从零开始
axisLeft.setStartAtZero(true);

chart.getAxisLeft().setDrawGridLines(false);
//设置动画时间
chart.animateXY(600,600);

chart.getLegend().setEnabled(true);

getData();
//设置柱形统计图上的值
chart.getData().setValueTextSize(10);
for (IDataSet set : chart.getData().getDataSets()){
set.setDrawValues(!set.isDrawValuesEnabled());
}



}



public void getData(){
ArrayList<BarEntry> values = new ArrayList<>();
Float aFloat = Float.valueOf("2015");
Log.v("xue","aFloat+++++"+aFloat);
BarEntry barEntry = new BarEntry(aFloat,Float.valueOf("100"));
BarEntry barEntry1 = new BarEntry(Float.valueOf("2016"),Float.valueOf("210"));
BarEntry barEntry2 = new BarEntry(Float.valueOf("2017"),Float.valueOf("300"));
BarEntry barEntry3 = new BarEntry(Float.valueOf("2018"),Float.valueOf("450"));
BarEntry barEntry4 = new BarEntry(Float.valueOf("2019"),Float.valueOf("300"));
BarEntry barEntry5 = new BarEntry(Float.valueOf("2020"),Float.valueOf("650"));
BarEntry barEntry6 = new BarEntry(Float.valueOf("2021"),Float.valueOf("740"));
values.add(barEntry);
values.add(barEntry1);
values.add(barEntry2);
values.add(barEntry3);
values.add(barEntry4);
values.add(barEntry5);
values.add(barEntry6);
BarDataSet set1;

if (chart.getData() != null &&
chart.getData().getDataSetCount() > 0) {
set1 = (BarDataSet) chart.getData().getDataSetByIndex(0);
set1.setValues(values);
chart.getData().notifyDataChanged();
chart.notifyDataSetChanged();
} else {
set1 = new BarDataSet(values, "点折水");
set1.setColors(ColorTemplate.VORDIPLOM_COLORS);
set1.setDrawValues(false);

ArrayList<IBarDataSet> dataSets = new ArrayList<>();
dataSets.add(set1);

BarData data = new BarData(dataSets);
chart.setData(data);

chart.setFitBars(true);
}
//绘制图表
chart.invalidate();

}

}




github地址:https://github.com/PhilJay/MPAndroidChart

下载地址:MPAndroidChart-master.zip

收起阅读 »

性能优化你会吗 --- iOS开发中常见的性能优化技巧

性能问题的主要原因是什么,原因有相同的,也有不同的,但归根到底,不外乎内存使用、代码效率、合适的策略逻辑、代码质量、安装包体积这一类问题。但从用户体验的角度去思考,当我们置身处地得把自己当做用户去玩一款应用时候,那么都会在意什么呢?假如正在玩一款手游,首先一定...
继续阅读 »

性能问题的主要原因是什么,原因有相同的,也有不同的,但归根到底,不外乎内存使用、代码效率、合适的策略逻辑、代码质量、安装包体积这一类问题。

但从用户体验的角度去思考,当我们置身处地得把自己当做用户去玩一款应用时候,那么都会在意什么呢?假如正在玩一款手游,首先一定不希望玩着玩着突然闪退,然后就是不希望卡顿,其次就是耗电和耗流量不希望太严重,最后就是安装包希望能小一点。简单归类如下:

快:使用时避免出现卡顿,响应速度快,减少用户等待的时间,满足用户期望。
稳:不要在用户使用过程中崩溃和无响应。
省:节省流量和耗电,减少用户使用成本,避免使用时导致手机发烫。
小:安装包小可以降低用户的安装成本。

一、快

应用启动慢,使用时经常卡顿,是非常影响用户体验的,应该尽量避免出现。卡顿的场景有很多,按场景可以分为4类:UI 绘制、应用启动、页面跳转、事件响应。引起卡顿的原因很多,但不管怎么样的原因和场景,最终都是通过设备屏幕上显示来达到用户,归根到底就是显示有问题,

根据iOS 系统显示原理可以看到,影响绘制的根本原因有以下两个方面:

1.绘制任务太重,绘制一帧内容耗时太长。
2.主线程太忙,根据系统传递过来的 VSYNC 信号来时还没准备好数据导致丢帧。

绘制耗时太长,有一些工具可以帮助我们定位问题。主线程太忙则需要注意了,主线程关键职责是处理用户交互,在屏幕上绘制像素,并进行加载显示相关的数据,所以特别需要避免任何主线程的事情,这样应用程序才能保持对用户操作的即时响应。总结起来,主线程主要做以下几个方面工作:

1.UI 生命周期控制
2.系统事件处理
3.消息处理
4.界面布局
5.界面绘制
6.界面刷新

除此之外,应该尽量避免将其他处理放在主线程中,特别复杂的数据计算和网络请求等。

二、稳

应用的稳定性定义很宽泛,影响稳定性的原因很多,比如内存使用不合理、代码异常场景考虑不周全、代码逻辑不合理等,都会对应用的稳定性造成影响。其中最常见的两个场景是:Crash 和 ANR,这两个错误将会使得程序无法使用,比较常用的解决方式如下:

1.提高代码质量。比如开发期间的代码审核,看些代码设计逻辑,业务合理性等。
2.代码静态扫描工具。常见工具有Clang Static Analyzer、OCLint、Infer等等。
3.Crash监控。把一些崩溃的信息,异常信息及时地记录下来,以便后续分析解决。
4.Crash上传机制。在Crash后,尽量先保存日志到本地,然后等下一次网络正常时再上传日志信息。

三、省

在移动设备中,电池的重要性不言而喻,没有电什么都干不成。对于操作系统和设备开发商来说,耗电优化一致没有停止,去追求更长的待机时间,而对于一款应用来说,并不是可以忽略电量使用问题,特别是那些被归为“电池杀手”的应用,最终的结果是被卸载。因此,应用开发者在实现需求的同时,需要尽量减少电量的消耗。

1.CPU

不论用户是否正在直接使用, CPU 都是应用所使用的主要硬件, 在后台操作和处理推送通知时, 应用仍然会消耗 CPU 资源

应用计算的越多,消耗的电量越多.在完成相同的基本操作时, 老一代的设备会消耗更多的电量, 计算量的消耗取决于不同的因素

2.网络

智能的网络访问管理可以让应用响应的更快,并有助于延长电池寿命.在无法访问网络时,应该推迟后续的网络请求, 直到网络连接恢复为止. 此外,应避免在没有连接 WiFi 的情况下进行高宽带消耗的操作.比如视频流, 众所周知,蜂窝无线系统(LTE,4G,3G等)对电量的消耗远远大于 WiFi信号,根源在于 LTE 设备基于多输入,多输出技术,使用多个并发信号以维护两端的 LTE 链接,类似的,所有的蜂窝数据链接都会定期扫描以寻找更强的信号. 因此:我们需要

1)在进行任何网络操作之前,先检查合适的网络连接是否可用
2)持续监视网络的可用性,并在链接状态发生变化时给与适当的反馈
3).定位管理器和** GPS**

我们都知道定位服务是很耗电的,使用 GPS 计算坐标需要确定两点信息:

1)时间锁每个 GPS 卫星每毫秒广播唯一一个1023位随机数, 因而数据传播速率是1.024Mbit/s GPS 的接收芯片必须正确的与卫星的时间锁槽对齐
2)频率锁 GPS 接收器必须计算由接收器与卫星的相对运动导致的多普勒偏移带来的信号误差

计算坐标会不断的使用 CPU 和 GPS 的硬件资源,因此他们会迅速的消耗电池电量, 那么怎么减少呢?

1)关闭无关紧要的特性

判断何时需要跟踪位置的变化, 在需要跟踪的时候调用 startUpdatingLocation方法,无须跟踪时调用stopUpdatingLocation方法.

当应用在后台运行或用户没有与别人聊天时,也应该关闭位置跟踪,也就说说,浏览媒体库,查看朋友列表或调整应用设置时, 都应该关闭位置跟踪

2)只在必要时使用网络

为了提高电量的使用效率, IOS 总是尽可能地保持无线网络关闭.当应用需要建立网络连接时,IOS 会利用这个机会向后台应用分享网络会话,以便一些低优先级能够被处理, 如推送通知,收取电子邮件等

关键在于每当用户建立网络连接时,网络硬件都会在连接完成后多维持几秒的活动时间.每次集中的网络通信都会消耗大量的电量

要想减轻这个问题带来的危害,你的软件需要有所保留的的使用网络.应该定期集中短暂的使用网络,而不是持续的保持着活动的数据流.只有这样,网络硬件才有机会关闭

4.屏幕

屏幕非常耗电, 屏幕越大就越耗电.当然,如果你的应用在前台运行且与用户进行交互,则势必会使用屏幕并消耗电量

这里有一些方案可以优化屏幕的使用:

1)动画优化

当应用在前台时, 使用动画,一旦应用进入了后台,则立即暂停动画.通常来说,你可以通过监听 UIApplicationWillResignActiveNotification或UIApplicationDIdEnterBackgroundNotification的通知事件来暂停或停止动画,也可以通过监听UIApplicationDidBecomeActiveNotification的通知事件来恢复动画

2)视频优化


视频播放期间,最好保持屏幕常量.可以使用UIApplication对象的idleTimerDisabled属性来实现这个目的.一旦设置了 YES, 他会阻止屏幕休眠,从而实现常亮.

与动画类似,你可以通过相应应用的通知来释放和获取锁

用户总是随身携带者手机,所以编写省电的代码就格外重要, 毕竟手机的移动电源并不是随处可见, 在无法降低任务复杂性时, 提供一个对电池电量保持敏感的方案并在适当的时机提示用户, 会让用户体验良好。

四、小

应用安装包大小对应用使用没有影响,但应用的安装包越大,用户下载的门槛越高,特别是在移动网络情况下,用户在下载应用时,对安装包大小的要求更高,因此,减小安装包大小可以让更多用户愿意下载和体验产品。

当然,瘦身和减负虽好,但需要注意瘦身对于项目可维护性的影响,建议根据自身的项目进行技巧的选取。

App安装包是由资源和可执行文件两部分组成,安装包瘦身从以下三部分优化。

资源优化:
1.删除无用的资源
2.删除重复的资源
3.无损压缩图片
4.不常用资源换为下载

编译优化:
1.去除debug符号
2.开启编译优化
3.避免编译多个架构

可执行文件优化:
1.去除无用代码
2.统计库占用,去除无用库
3.混淆类/方法名
4.减少冗余字符串
5.ARC->MRC (一般不到特殊情况不建议这么做,会提高维护成本)

缩减iOS安装包大小是很多中大型APP都要做的事,一般首先会对资源文件下手,压缩图片/音频,去除不必要的资源。这些资源优化做完后,我们还可以尝试对可执行文件进行瘦身,项目越大,可执行文件占用的体积越大,又因为AppStore会对可执行文件加密,导致可执行文件的压缩率低,压缩后可执行文件占整个APP安装包的体积比例大约有80%~90%,还是挺值得优化的。

下面是一些常见的优化方案:
TableViewCell 复用

在cellForRowAtIndexPath:回调的时候只创建实例,快速返回cell,不绑定数据。在willDisplayCell: forRowAtIndexPath:的时候绑定数据(赋值)。

高度缓存

在tableView滑动时,会不断调用heightForRowAtIndexPath:,当cell高度需要自适应时,每次回调都要计算高度,会导致 UI 卡顿。为了避免重复无意义的计算,需要缓存高度。

怎么缓存?

字典,NSCache。

UITableView-FDTemplateLayoutCell

[if !supportLineBreakNewLine]

[endif]

视图层级优化

不要动态创建视图

在内存可控的前提下,缓存subview。

善用hidden。

[if !supportLineBreakNewLine]

[endif]

减少视图层级

减少subviews个数,用layer绘制元素。

少用clearColor,maskToBounds,阴影效果等。

[if !supportLineBreakNewLine]

[endif]

减少多余的绘制操作

图片

不要用JPEG的图片,应当使用PNG图片。

子线程预解码(Decode),主线程直接渲染。因为当image没有Decode,直接赋值给imageView会进行一个Decode操作。

优化图片大小,尽量不要动态缩放(contentMode)。

尽可能将多张图片合成为一张进行显示。

[if !supportLineBreakNewLine]

[endif]

减少透明view

使用透明view会引起blending,在iOS的图形处理中,blending主要指的是混合像素颜色的计算。最直观的例子就是,我们把两个图层叠加在一起,如果第一个图层的透明的,则最终像素的颜色计算需要将第二个图层也考虑进来。这一过程即为Blending。

会导致blending的原因:

UIView的alpha<1。

UIImageView的image含有alpha channel(即使UIImageView的alpha是1,但只要image含有透明通道,则仍会导致blending)。

[if !supportLineBreakNewLine]

[endif]

为什么blending会导致性能的损失?

原因是很直观的,如果一个图层是不透明的,则系统直接显示该图层的颜色即可。而如果图层是透明的,则会引起更多的计算,因为需要把另一个的图层也包括进来,进行混合后的颜色计算。

opaque设置为YES,减少性能消耗,因为GPU将不会做任何合成,而是简单从这个层拷贝。

[if !supportLineBreakNewLine]

[endif]

减少离屏渲染

离屏渲染指的是在图像在绘制到当前屏幕前,需要先进行一次渲染,之后才绘制到当前屏幕。

OpenGL中,GPU屏幕渲染有以下两种方式:

On-Screen

Rendering即当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。

Off-Screen

Rendering即离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

[if !supportLineBreakNewLine]

[endif]

小结

性能优化不是更新一两个版本就可以解决的,是持续性的需求,持续集成迭代反馈。在实际的项目中,在项目刚开始的时候,由于人力和项目完成时间限制,性能优化的优先级比较低,等进入项目投入使用阶段,就需要把优先级提高,但在项目初期,在设计架构方案时,性能优化的点也需要提早考虑进去,这就体现出一个程序员的技术功底了。

什么时候开始有性能优化的需求,往往都是从发现问题开始,然后分析问题原因及背景,进而寻找最优解决方案,最终解决问题,这也是日常工作中常会用到的处理方式。

链接:https://www.jianshu.com/p/965932858d95

收起阅读 »

iOS安全之三攻三防

互联网世界每分钟都在上演黑客攻击,由此导致的财产损失不计其数。金融行业在安全方面的重视不断加深,而传统互联网行业在安全方面并没有足够重视,这样导致开发的APP在逆向开发人员面前等同于裸奔,甚至有些小厂前后台在账号密码处理上采取明文传送,本地存储,这等同于将账号...
继续阅读 »

互联网世界每分钟都在上演黑客攻击,由此导致的财产损失不计其数。金融行业在安全方面的重视不断加深,而传统互联网行业在安全方面并没有足够重视,这样导致开发的APP在逆向开发人员面前等同于裸奔,甚至有些小厂前后台在账号密码处理上采取明文传送,本地存储,这等同于将账号密码直接暴露无疑。当然即使采用加密传送,逆向APP后依然可以获取到账号密码,让你在神不知鬼不觉的情况下将账号密码发送到了黑客邮箱,所以攻防终究是一个相互博弈的过程。本文主要分析常见的几种攻击和防护手段,通过攻击你可以看到你的APP是如何被一步一步被攻破的。有了攻击,我们针对相应的攻击就是见招拆招了。

一、攻击原理

从APPStore下载正式版本版本应用,进行一键砸壳,绝大部分应用均可以脱壳成功。
使用脚本或第三方工具MonkeyDev对应用实现重签名。
利用动态调试(LLDB,Cycript,Reveal)和静态分析(反汇编),找到关键函数进行破解。
Theos编写插件,让使用更加方便。

二、攻守第一回合

1. 第一攻武器:代码注入+method_exchangeImplementations

在shell脚本实现iOS包重签名及代码注入的最后,我们成功使用method_exchange截获微信点击按钮,代码如下:

+(void)load
{
Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountLoginControlLogic"), @selector(onFirstViewRegester));

Method newMethod = class_getInstanceMethod(self, @selector(test));

method_exchangeImplementations(oldMethod, newMethod);
}

-(void)test{
NSLog(@"----截获到微信注册按钮点击------");
}

2. 第一防护盾:framwork+fishHook

关于为什么使用framwork而不是直接在代码中创建一个类,并在类的load方法中编写防护代码,原因是自己创建framwork的加载要早于代码注入的framwork,代码注入的framwork的执行要早于自己类load的加载,具体原理请看dyld加载应用启动原理详解。防护代码如下:


注意:当我们检查到hook代码时,比较好的处理方式是将该手机的UDID,账号等信息发送给后台服务器,让后台服务器进行封号禁设备处理,而不是直接exit(0)让程序强制退出,因为这样的好处是让黑客很难定位。

三、攻守第二回合

1. 第二攻武器:MonkeyDev

MonkeyDev可以帮助我们更加方便的实现代码重签名和hook,底层是使用了方法交换的SET和GET方法进行hook,关于MoneyDev的使用在逆向iOS系统桌面实现一键清空徽标有讲。同样以截获微信注册按钮为例,hook代码示例如下:

%hook WCAccountLoginControlLogic
- (void)onFirstViewRegester:(id)arg{
NSLog(@"---hook-----");
}

%end

2. 第二防护盾:依然framwork+fishHook

+(void)load{
//setIMP
struct rebinding gt;
gt.name = "method_getImplementation";
gt.replacement = my_getIMP;
gt.replaced = (void *)&getIMP;
//getIMP
struct rebinding st;
st.name = "method_setImplementation";
st.replacement = my_setIMP;
st.replaced = (void *)&setIMP;

struct rebinding rebs[2] = {gt,st};
rebind_symbols(rebs, 2);

}

//保存原来的交换函数
IMP (*getIMP)(Method _Nonnull m);
IMP (*setIMP)(Method _Nonnull m, IMP _Nonnull imp);


IMP my_getIMP(Method _Nonnull m){
NSLog(@"🍺----检查到了HOOk-----🍺");
return nil;
}
IMP my_setIMP(Method _Nonnull m, IMP _Nonnull imp){

NSLog(@"🍺----检查到了HOOk-----🍺");
return nil;
}

三、攻守第三回合

上面的两次攻击都是通过代码注入来实现hook目的,我们能不能防止第三方代码进行注入呢?答案当然是可以,接下来我们来防止第三方代码注入。

1. 第三防护盾:在编译设置阶段增加字段"-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null",如下图:


1.1 增加该字段后在MachO文件就会增加_RESTRICT,__restrict段,如下图:


1.2 为什么增加这个字段就可以了呢?这里我们就要回归到dyld的源码了,在dyld加载过程中有一个函数hasRestrictedSegment就是用来判断是否存在__RESTRICT,__RESTRICT中是否是__restrict名称,如果是,则会禁止加载第三方注入的库文件,源码如下:

#if __MAC_OS_X_VERSION_MIN_REQUIRED
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 = §ionsStart[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

2. 第三攻击武器:直接修改MachO二进制文件

通过Synalyze It!工具更改MachO二进制文件字段,然后重新签名打包即可破坏该防护过程:

3. 第三防护2级护盾:代码过滤,增加白名单。

3.1 既然禁止第三方注入代码都很容易被攻破,接下来我们就从代码入手,过滤第三方库注入库,增加白名单,代码如下: 

@implementation ViewController
+(void)load
{

const struct mach_header_64 * header = _dyld_get_image_header(0);
if (hasRestrictedSegment(header)) {
NSLog(@"---- 防止状态 ------");

//如果__RESTRICT字段被绕过,开始开名单检测
CheckWhitelist()

}else{
NSLog(@"--- 防护字段被修改了 -----");
}


}

static bool 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 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 = §ionsStart[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;
}

#pragma mark -- 白名单监测
bool CheckWhitelist(){

int count = _dyld_image_count();//加载了多少数量

for (int i = 0; i < count; i++) {
//遍历拿到库名称!
const char * imageName = _dyld_get_image_name(i);
if (!strstr(libraries, imageName)&&!strstr(imageName, "/var/mobile/Containers/Bundle/Application")) {
printf("该库非白名单之内!!\n%s",imageName);
return NO;
}

return YES;
}

3.2 原理就是使用系统的函数帮我们检测自己设定的__RESTRICT是否被更改,如果被更改说明我们被Hook了,接下来在被hook的字段中增加自己的处理逻辑即可。

总结:对最后一个防护代码也很容易进行攻击,比如找到hasRestrictedSegment函数,让其直接返回YES。所以建议将该函数进行封装,尽量不要使用Bool作为返回值。综上: 攻和守本来就是一个博弈的过程,没有绝对安全的城墙。
最后附上过滤白名单源码下载,直接拖入工程即可使用,达到较好的代码防护目的。如果帮助到你请给一个Star。
我是Qinz,希望我的文章对你有帮助。

链接:https://www.jianshu.com/p/655c91b61f8a

收起阅读 »

iOS逆向(6)-从fishhook看runtime,hook系统C函数

在上篇文章不知MachO怎敢说自己懂DYLD中已经详细介绍了MachO,并且由MachO引出了dyld,再由dyld讲述了App的启动流程,而在App的启动流程中又说到了一些关键的名称如:LC_LOAD_DYLINKER、LC_LOAD_DYLIB以及objc...
继续阅读 »

在上篇文章不知MachO怎敢说自己懂DYLD中已经详细介绍了MachO,并且由MachO引出了dyld,再由dyld讲述了App的启动流程,而在App的启动流程中又说到了一些关键的名称如:LC_LOAD_DYLINKER、LC_LOAD_DYLIB以及objc的回调函数_dyld_objc_notify_register等等。并且在末尾提出了MachO中还有一些符号表,而有哪些符号表,这些符号表又有些什么用呢?笔者在这篇文章就将一一道来。

老规矩,片头先上福利:点击下载demo,demo中有笔者给fishhook每句代码加的详细注释!!!
这篇文章会用到的工具有:

fishhook

在开始正文之前,假设面试官问了一个问题:
都知道Objective-C最大的特性就是runtime,大家可以用使用runtime对OC的方法进行hook,那么C函数能不能hook?

有兴趣回答的朋友可以先行在评论区回答,答完之后再继续阅读或者预先偷窥一下文末的答案,看看这被炒了无数次冷饭的runtime自己是否真的了然于胸。

本将从以下几方面回答上面所提的问题:

1、Runtime的Hook原理
2、为什么C不能hook
3、如何利用MachO“玩坏”系统C函数
4、fishhook源码分析
5、绑定系统C函数过程验证

一、Runtime的Hook原理

Runtime,从名称上就知道是运行时,也是它造就了OC运行时的特性,而要想彻底明白什么是运行时,那么就需要将之与C语言有相比较。
今天咱们就从汇编的角度看一看OC和C在调用方法(函数)上有什么区别。

注:笔者使用的是iPhone 7征集调试,所有一下汇编都是基于arm64,所以以下所有汇编默认为基于arm64。

新建一个工程取名为:FishhookDemo
敲入两个OC方法mylog和mylog2,挂上断点,如图:


开启汇编断点,如图:


运行工程,会跳转到如下图的汇编断点:


从上图可以看的出来调用了两个objc_msgSend,这两个很像是
我们的mylog和mylog2,但现在还不能确定。
想一想objc_msgSend的定义:

OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

第一个参数是self,第二个参数是SEL,所以可以知道SEL是放在x1的寄存器里面(什么是x1?继续关注作者,之后的文章会有相关的汇编的专门篇章)。

马不停蹄,挂上两个汇编断点,查看一下两个x1中存放的到底是什么,如图:



这也就验证了咱们OC方法都是消息转发(objc_msgSend)。而同一个C函数的地址又都是一样的(笔者这次运行的地址就是0x1026ce130) 。

所以在每次调用OC方法的时候就让我们有了一次改变消息转发「目标」的机会。

这里稍微提一下runtime的源码分析流程:
Step 1、方法查找
① 汇编快速查找缓存
② C/C++慢速查找:self->super->NSObject->找到换缓存起来
Step 2、动态方法解析: _class_resolveMethod
① _class_resolveInstanceMethod
② _class_resolveClassMethod
Step 3、消息转发
① _forwardingTargetForSelector
② _methodSignatureForSelector
③ _forwardInvocation
④ _doesNotRecognizeSelector

二、为什么C不能hook

同样我们从汇编的角度切入。
敲入代码一些C函数,挂上断点,如图:


运行工程:
会看到断点断到如下汇编:


可以看到每个NSLog对应跳转的地址都是0x10000a010,每个printf对应跳转的地址都是0x10000a184,也就是说每个C的函数都是一一对应着一个真实的地址空间。每次在调用一个C函数的时候都是执行一句汇编bl 0xXXXXXXXX。

所以上面讲述到的消息转发的机会没有了,也就是没有了利用runtime来Hook的机会了。

三、如何利用MachO“玩坏”系统C函数

既然如此,那么是否C函数就真的那么牢不可破,无法对他进行Hook呢?
答案肯定是否定的!
想要从根上理解这个问题,首先要了解:我们的C函数分为系统C函数和我们自定义的C函数。

1、自定义的C函数

在上面的步骤中我们已经了解到所有C函数的调用都是跳转到一个「固定的地址」,那么就可以推断得出这个「固定的地址」其实是在编译期已经被生成好了,所以才能快速、直接的跳转到这个地址,实现函数调用。
C语言被称之为是静态语言也就是这么个理。

2、系统的C函数

在上篇文章不知MachO怎敢说自己懂DYLD已经提到了在dyld启动app的第二个步骤就是加载共享缓存库,共享缓存库包括Foundation框架,NSLog是被包含在Foundation框架的。那么就可以确定一件事情,在我们将自己工程打包出的MachO文件中是不可能预先确定NSLog的地址的。

但是又因为C语言是静态的特性,没法在运行的时候实时获取共享缓存库中NSLog的地址。而共享缓存库的存在好处太大,既能节省大量内存,又能加快启动速度提升性能,不能弃之而不用。

为了解决这个问题,Apple使用了PIC(Position-independent code)技术,在第一次使用对应函数(NSLog)的时候,从系统内存中将对函数(NSLog)的内存地址取出,绑定到APP中对应函数(NSLog)上,就可以实现正常的C函数(NSLog)调用了。

既然有这么个过程,iOS系统可以动态的绑定系统C函数的地址,那么咱们就也能。

四、fishhook源码分析

1、fishhook的总体思路

Facebook的开源库fishhook就可以完美的实现这个任务。
先上一张官网原理图:


总体来说,步骤是这样的:

先找到四张表Lazy Symbol Pointer Table、Indirect Symbol Table、Symbol Table、String Table。
MachO有个规律:Lazy Symbol Pointer Table中第index行代表的函数和Indirect Symbol Table中第index行代表的函数是一样的。
Indirect Symbol Table中value值表示Symbol Table的index。
找到Symbol Table的中对应index的对象,其data代表String Table的偏移值。
用String Table的基值,也就是第一行的pFile值,加上Symbol Table的中取到的偏移值,就能得到Indirect Symbol Table中value(这个value代表函数的偏移值)代表的函数名了。

2、验证NSLog地址

下面就来验证一下在NSLog的地址是不是真的就存在Indirect Symbol Table中。
同样在NSLog处下好断点,打开汇编断点,运行代码。会发现断点断在如下入位置:


注:笔者的工程重新build了,MachO也重新生成,所以此处的截图和上文中断住NSLog的截图的地址不一样,这是正常情况。

可以发现NSLog的地址是0x104d36010,先记住这个值。

然后查看我们APP在内存中的偏移值。
利用image list命令列出所有image,第一个image就是我们APP的偏移值,也就是内存地址。


可以看到APP在内存中的偏移值为0x104d30000。
接着打开MachOView查看MachO中的Indirect Symbol Table中的value,如图:


其值为0x100006010,去除最高位得到的0x6010就是NSLog在MachO中的偏移值。
最后将NSLog在MachO中的偏移值于APP在内存中的偏移值相加就得到NSLog真实的内存地址:
0x6010+0x104d30000=0x104d36010

最终证明,在Indirect Symbol Table的value中的值就是其对应的函数的地址!!!

3、根据MachO的表查找对应的函数名和函数地址

咱们还是用NSLog来距离查找。

1、Indirect Symbol Table

取出其data值0000010A,用10进制表示,结果为266,如图:


2、Symbol Table

在Symbol Table中找到下标(offset)为266的的对象,取出其data0x124,如图:


3、String Table

将在Symbols中得到的偏移值0x124加上String Table的首个地址DC6C,得到值DD90,然后找到pFile为DD90的值,如下两图:



上述就是根据MachO的表查找对应的函数名和函数地址全过程了。

4、源码分析

fishhook的源码总共只有250行左右,所以结合MachO慢慢看,其实一点也不费劲,在笔者的demo中有对其每一句函数的详细注释。当然也有对fishhook使用的demo。

所以笔者就不在此处对fishhook做太过详细的介绍了。只对其中一些关键参数和关键函数做介绍。

1、fishhook为维护一个链表,用来储存需要hook的所有函数

// 给需要rebinding的方法结构体开辟出对应的空间
// 生成对应的链表结构(rebindings_entry),并将新的entry插入头部
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel)

2、根据linkedit的基值,找到对应的三张表:symbol_table、string_table和indirect_symtab :

// 找到linkedit的头地址
// linkedit_base其实就是MachO的头地址!!!可以通过查看linkedit_base值和image list命令查看验证!!!(文末附有验证图)
/**********************************************************
Linkedit虚拟地址 = PAGEZERO(64位下1G) + FileOffset
MachO地址 = PAGEZERO + ASLR
上面两个公式是已知的 得到下面这个公式
MachO文件地址 = Linkedit虚拟地址 - FileOffset + ASLR(slide)
**********************************************************/
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
// 获取symbol_table的真实地址
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
// 获取string_table的真实地址
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// Get indirect symbol table (array of uint32_t indices into symbol table)
// 获取indirect_symtab的真实地址
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

3、最核心的一个步骤,查找并且替换目标函数:

// 在四张表(section,symtab,strtab,indirect_symtab)中循环查找
// 直到找到对应的rebindings->name,将原先的函数复制给新的地址,将新的函数地址赋值给原先的函数
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)

五、绑定系统C函数过程验证

上面说了这么多,那么咱们来验证一下系统C函数是不是真的会这样被绑定起来,并且看一看,是在什么时候绑定的。

同样,在第一次敲入NSLog函数的地方加上断点,在第二个NSLog处也加上断点:


运行工程后,使用dis -s命令查看该函数的汇编代码,并且继续查看其中第一次b指令,也就是函数调用的汇编,如图:


从上图就可以看到,在我们第一次调用NSLog的时候,系统确实会默认的调用dyld_stub_binder函数对NSLog进行绑定。

继续跳过这个断点,进入下一个NSLog的汇编断点处,同样利用dis -s命令查看该汇编:


得到答案:
系统确实会在第一次调用系统C函数的时候对其进行绑定!

还记得正文开始的时候的那个问题吗?
那么是不是系统C函数可以hook,而自定义的C函数就绝对不能hook了呢?
很显然,国内外大神那么多,肯定是能做到的,有兴趣的读者可以自行查阅Cydia Substrate。

这篇文章利用了一些LLDB命令行看了许多我们想看的内容,如image list,register read还有dis -s,在我们正向开发中,LLDB就是一把利器,而在我们玩逆向的时候,LLDB就成为了我们某些是后的唯一途径了!所以,在下一篇文章中,笔者将会对LLDB进行更加详细的讲解,让大家看到LLBD的伟大。

1、关于道友AmazingYu的提问:
想问下 linkedit_base 地址与 Text 段的初始地址以及 Data 段的初始地址的关系,这三个段在内存中是挨着的吗,还有就是 linkedit_base 大概在进程内存分布中的哪个地方?

在咨询大佬请叫我Hank后,得到最终答案,在下面问回答中有一些问题,再此纠正一下!
linkedit地址(不是linkedit_base,末尾会介绍linkedit_base到底是什么) 与 Text 段的初始地址以及 Data 段确实是连续的,他们的顺序是:
先是Text 段,然后是Data 段,最后是linkedit_base 地址。从下面三幅图的File Offset和File Size可以看出来,两者相加就能得到下一段的地址:




2、几个名词(pFile 、offset 、File Offset)之前解释的有点问题:
1、首先,这三个都是表示相对于MachO的内存偏移,只不过其含义被细分了。
2、pFile 和 offset含义相近,不过offset更详细,能够对应上具体某一个符号(DATA? TEXT?)。比如文件里面有许多类,类里面有许多的属性,pFile就代表各个类的偏移值,offset代表各个属性的偏移值
3、File Offset 这个存在于Segment的字段中。用于从Segment快速找到其代表的「表」真正的偏移值。
最后说一下linkedit_base:
linkedit_base其实代表的就是MachO的真实内存地址!
可以从下图得到验证


因为

Linkedit虚拟地址 = PAGEZERO(64位下1G) + FileOffset 
MachO地址 = PAGEZERO + ASLR
// 上面两个公式是已知的 所以可以得到下面这个公式
MachO地址 = Linkedit虚拟地址 - FileOffset + ASLR(slide)

也就是fishhook中的:

uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;

转自:https://www.jianshu.com/p/b6a72aa6c146

收起阅读 »

iOS利用RunTime来实现万能跳转

1.万能跳转的应用场景:(1)手机App通过推送过来的数据内容来跳转不同的界面,并把界面数据展示出来。(2)手机内部根据不同的cell的点击事件,不同的数据跳转不同的界面。2.工作的流程图:通过动态返回的数据中的class类名,来去查询class是不是存在:(...
继续阅读 »

1.万能跳转的应用场景:

(1)手机App通过推送过来的数据内容来跳转不同的界面,并把界面数据展示出来。
(2)手机内部根据不同的cell的点击事件,不同的数据跳转不同的界面。

2.工作的流程图:

通过动态返回的数据中的class类名,来去查询class是不是存在:(1)存在则获取实例对象然后通过kVC来绑定数据然后去跳转。(2)不存在则动态创建class及其变量,然后手动创建实例对象在通过KVC来绑定数据,最后跳转。


3.主要方法:

//创建Class
objc_allocateClassPair(Class superclass, const char * name, size_t extraBytes)
//注册Class
void objc_registerClassPair(Class cls)
//添加变量
class_addIvar(Class cls, const char * name,size_t size, uint8_t alignment , const char * types)
//添加方法
class_addMethod(Class cls, SEL name, IMP imp, const char * types)
//获取属性
class_getProperty(Class cls, const char * name)
//获取实例变量
class_getInstanceVariable(Class cls, const char * name)

4.代码实现:

1、工程中新建三个控制器,命名为
FirstViewController
SecondViewController
ThredViewController
每一个控制器的viewDidLoad方法里面的内容为

self.view.backgroundColor = [UIColor redColor];

UILabel * titleLab = [[UILabel alloc]initWithFrame:CGRectMake(100, 100, 200, 40)];
titleLab.textColor = [UIColor blackColor];
[self.view addSubview:titleLab];
titleLab.text =self.name;

然后在ViewController模拟根据不同数据跳转不同界面,代码如下

#import "ViewController.h"
#import <objc/message.h>

@interface ViewController ()

@property (nonatomic, weak) UISegmentedControl * seg;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.view.backgroundColor = [UIColor yellowColor];

NSArray * array = @[@"消息1",@"消息2",@"消息3",@"消息4"];
UISegmentedControl * seg = [[UISegmentedControl alloc]initWithItems:array];
seg.frame = CGRectMake(70, 200, 240, 45);
[self.view addSubview:seg];
seg.selectedSegmentIndex = 0;
self.seg = seg;

UIButton * jupBtn = [UIButton buttonWithType:UIButtonTypeCustom];
jupBtn.frame = CGRectMake(100, 250, 60, 45);
[jupBtn setTitle:@"跳转" forState:UIControlStateNormal];
[jupBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
jupBtn.backgroundColor = [UIColor redColor];
[self.view addSubview:jupBtn];
[jupBtn addTarget:self action:@selector(action) forControlEvents:UIControlEventTouchUpInside];

//创建Class
//objc_allocateClassPair(Class superclass, const char * name, size_t extraBytes)
//注册Class
//void objc_registerClassPair(Class cls)
//添加变量
//class_addIvar(Class cls, const char * name,size_t size, uint8_t alignment , const char * types)
//添加方法
//class_addMethod(Class cls, SEL name, IMP imp, const char * types)
//获取属性
//class_getProperty(Class cls, const char * name)
//获取实例变量
//class_getInstanceVariable(Class cls, const char * name)
}

-(void)action{

NSDictionary * infoDic = nil;

switch (self.seg.selectedSegmentIndex) {
case 0:
infoDic = @{@"class":@"FirstViewController",
@"property":@{
@"name":@"尼古拉斯赵四"
}
};
break;
case 1:
infoDic = @{@"class":@"SecondViewController",
@"property":@{
@"age":@"26",
@"sex":@"男"
}
};
break;
case 2:
infoDic = @{@"class":@"ThredViewController",
@"property":@{
@"teacher":@"王老师",
@"money":@"5000"
}
};
break;
case 3:
//NewViewController
infoDic = @{@"class":@"WorkerController",
@"property":@{
@"phoneNumber":@"17710948530"
}
};
break;

default:
break;
}

[self pushToControllerWithData:infoDic];

}
-(void)pushToControllerWithData:(NSDictionary * )vcData{
//1.获取class
const char * className = [vcData[@"class"] UTF8String];
Class cls = objc_getClass(className);
if(!cls){
//创建新的类,并添加变量和方法
Class superClass = [UIViewController class];
cls = objc_allocateClassPair(superClass, className, 0);
//添加phoneNumber变量
class_addIvar(cls, "phoneNumber", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
//添加titleLab控件
class_addIvar(cls, "titleLab", sizeof(UILabel *), log2(sizeof(UILabel *)), @encode(UILabel *));
//添加方法,方法交换,执行viewDidLoad加载
Method method = class_getInstanceMethod([self class], @selector(workerLoad));
IMP methodIMP = method_getImplementation(method);
const char * types = method_getTypeEncoding(method);
class_addMethod(cls, @selector(viewDidLoad), methodIMP, types);
}
//2.创建实例对象,给属性赋值
id instance = [[cls alloc]init];
NSDictionary * values = vcData[@"property"];
[values enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
//检测是否存在为key的属性
if(class_getProperty(cls, [key UTF8String])){
[instance setValue:obj forKey:key];
}
//检测是否存在为key的变量
else if (class_getInstanceVariable(cls, [key UTF8String])){
[instance setValue:obj forKey:key];
}
}];

//2.跳转到对应的界面
[self.navigationController pushViewController:instance animated:YES];

}

-(void)workerLoad{
[super viewDidLoad];
self.view.backgroundColor = [UIColor greenColor];
//初始化titleLab
[self setValue:[[UILabel alloc]initWithFrame:CGRectMake(100, 100, 200, 40)] forKey:@"titleLab"];
UILabel * titleLab = [self valueForKey:@"titleLab"];
//添加到视图上
[[self valueForKey:@"view"] performSelector:@selector(addSubview:) withObject:titleLab];
titleLab.text =[self valueForKey:@"phoneNumber"];
titleLab.textColor = [UIColor blackColor];

}

@end

5.demo的下载地址,喜欢的话给个星,谢谢:

iOS根据不同数据跳转不同界面,动态添加属性及其控件等界面内容

转自:https://www.jianshu.com/p/376a3bc7741b

收起阅读 »

AVPlayer封装

说明基于AVPlayer和MVP模式封装的一个视频播放控制器,支持全屏,暂停播放,进度条拖动。Demo地址AVPlayer框架介绍AVPlay既可以用来播放音频也可以用来播放视频,AVPlay在播放音频方面可以直接用来播放网络上的音频。在使用AVPlay的时候...
继续阅读 »

说明

基于AVPlayer和MVP模式封装的一个视频播放控制器,支持全屏,暂停播放,进度条拖动。

Demo地址

AVPlayer框架介绍

AVPlay既可以用来播放音频也可以用来播放视频,AVPlay在播放音频方面可以直接用来播放网络上的音频。在使用AVPlay的时候我们需要导入AVFoundation.framework框架,再引入头文件#import<AVFoundation/AVFoundation.h>。

主要包括下面几个类

1.AVPlayer:播放器类
2.AVPlayerItem:播放单元类,即一个播放源
3.AVPlayerLayer:播放界面

使用时,需要先根据NSURL生成一个播放源,[AVPlayerItem playerItemWithURL:],再根据这个播放源获得一个播放器对象,[AVPlayer playerWithPlayerItem:];,此时播放器已经准备完成,但还需要根据AVPlayer生成一个AVPlayerLayer,设置frame,再加入到superView.layer中,[AVPlayerLayer playerLayerWithPlayer:]; self.playerLayer.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.width*0.6); [self.layer addSublayer:self.playerLayer];

此时一个简单的播放器就已经配置完成。

暂停播放

AVPlayer有一个rate属性,可以根据这个属性来判断当前是否在播放,rate == 0.f为暂停,反之视频播放。

AVPlayerItemStatus
可以对AVPlayerItem设置kvo,监听视频源是否可播放,系统给了三种状态,如下:

typedef NS_ENUM(NSInteger, AVPlayerItemStatus) {
AVPlayerItemStatusUnknown,
AVPlayerItemStatusReadyToPlay,
AVPlayerItemStatusFailed
};

设置KVO监听:

[self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"status"]) {
AVPlayerItemStatus status = [change[NSKeyValueChangeNewKey] intValue];
if (status == AVPlayerItemStatusReadyToPlay) {
isReadyToPlay = YES;
[self.player play];
}else{
//预留
isReadyToPlay = NO;
}
[self.controlView controlItemStatus:status playItem:object];
}
}

全屏操作

Demo中给出的思路是:

1.首先将当前竖屏状态下的播放器的view的frame保存下来,方便退出全屏时,布局;
2.然后新建一个全屏展示View的控制器,重写该控制器的@property(nonatomic, readonly) UIInterfaceOrientation preferredInterfaceOrientationForPresentation NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;,强制让该控制器旋转;
3.将当前根控制器present到上述的全屏控制器,在completion:回调中,做个简单的动画过渡一下,然后再将承载AVPlayerLayer的view的frame改成横屏状态,然后再修改AVPlayerLayer的frame;

退出全屏:

1.将当前全屏控制器dismiss;
2.再dismiss的成功回调中,设置View的frame为进入全屏前保存的frame;
3.再将AVPlayerLayer的frame修改。

代码如下:

#pragma mark - 进入全屏和退出全屏的动画和present处理
- (void)enterFullScreen:(BOOL)rightOrLeft{
playViewBeforeRect = _playerView.frame;
playViewBeforeCenter = _playerView.center;

TBZAVFullViewController *vc = [[TBZAVFullViewController alloc] init];
vc.type = rightOrLeft;
self.fullVC = vc;

__weak TBZAVPlayerViewController *weakSelf = self;

[self.navigationController presentViewController:vc animated:false completion:^{
[UIView animateWithDuration:0.25 animations:^{
weakSelf.playerView.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height);
} completion:^(BOOL finished) {
[weakSelf.playerView enterFull];
[weakSelf.fullVC.view addSubview:weakSelf.playerView];
[UIApplication.sharedApplication.keyWindow insertSubview:UIApplication.sharedApplication.keyWindow.rootViewController.view belowSubview:vc.view.superview];

self->isFull = YES;
}];
}];
}

- (void)exitFullScreen{
__weak TBZAVPlayerViewController *weakSelf = self;
[self.fullVC dismissViewControllerAnimated:false completion:^{
[UIView animateWithDuration:0.25 animations:^{
weakSelf.playerView.frame = self->playViewBeforeRect;
} completion:^(BOOL finished) {
[weakSelf.playerView exitFull];
[weakSelf.view addSubview:weakSelf.playerView];

self->isFull = NO;
}];
}];
}

播放进度

主要就是需要对AVPlayer添加监听,且注意需要释放该方法返回的对象。AVPlayerItem有两个属性,currentTime和duration,这两个对象都是CMTime类,可以用CMTimeGetSeconds(CMTime t);得到一个float指,秒数。也就是CMTimeGetSeconds(item.currentTime)可以得到当前播放到第几秒,CMTimeGetSeconds(item.duration)可以得到当前视频的总时长。

/*!
@method addPeriodicTimeObserverForInterval:queue:usingBlock:
@abstract Requests invocation of a block during playback to report changing time.
@param interval
The interval of invocation of the block during normal playback, according to progress of the current time of the player.
@param queue
The serial queue onto which block should be enqueued. If you pass NULL, the main queue (obtained using dispatch_get_main_queue()) will be used. Passing a
concurrent queue to this method will result in undefined behavior.
@param block
The block to be invoked periodically.
@result
An object conforming to the NSObject protocol. You must retain this returned value as long as you want the time observer to be invoked by the player.
Pass this object to -removeTimeObserver: to cancel time observation.
@discussion The block is invoked periodically at the interval specified, interpreted according to the timeline of the current item.
The block is also invoked whenever time jumps and whenever playback starts or stops.
If the interval corresponds to a very short interval in real time, the player may invoke the block less frequently
than requested. Even so, the player will invoke the block sufficiently often for the client to update indications
of the current time appropriately in its end-user interface.
Each call to -addPeriodicTimeObserverForInterval:queue:usingBlock: should be paired with a corresponding call to -removeTimeObserver:.
Releasing the observer object without a call to -removeTimeObserver: will result in undefined behavior.
*/
- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;

/*!
@method removeTimeObserver:
@abstract Cancels a previously registered time observer.
@param observer
An object returned by a previous call to -addPeriodicTimeObserverForInterval:queue:usingBlock: or -addBoundaryTimeObserverForTimes:queue:usingBlock:.
@discussion Upon return, the caller is guaranteed that no new time observer blocks will begin executing. Depending on the calling thread and the queue
used to add the time observer, an in-flight block may continue to execute after this method returns. You can guarantee synchronous time
observer removal by enqueuing the call to -removeTimeObserver: on that queue. Alternatively, call dispatch_sync(queue, ^{}) after
-removeTimeObserver: to wait for any in-flight blocks to finish executing.
-removeTimeObserver: should be used to explicitly cancel each time observer added using -addPeriodicTimeObserverForInterval:queue:usingBlock:
and -addBoundaryTimeObserverForTimes:queue:usingBlock:.
*/
- (void)removeTimeObserver:(id)observer;

- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;其实就是一个Timer,每隔1秒执行block,可以设置常驻子线程,如果设为NULL,就是在主线程。
主要使用如下:

__weak AVPlayer *weakAVPlayer = self.player;
__weak TBZAVPlayerView *weakSelf = self;
//监听播放进度,需要再destory方法中,释放timeObserve
self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1, NSEC_PER_SEC) queue:NULL usingBlock:^(CMTime time) {
CGFloat progress = CMTimeGetSeconds(weakAVPlayer.currentItem.currentTime) / CMTimeGetSeconds(weakAVPlayer.currentItem.duration);
if (progress == 1.0f) {
//视频播放完毕
if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(playEnd)]) {
[weakSelf.delegate playEnd];
}
}else{
[weakSelf.controlView controlPlayItem:weakAVPlayer.currentItem];
}
}];

- (void)destroy{
if (self.player || self.playerItem || self.playerLayer) {
[self.player pause];
if (self.timeObserver) {
[self.player removeTimeObserver:self.timeObserver];
}
[self.playerItem removeObserver:self forKeyPath:@"status"];
self.playerItem = nil;
self.player = nil;
[self.playerLayer removeFromSuperlayer];
}
}

总结

1.当视频源切换了之后,需要将当前视频源添加的监听都remove掉,重新给新的视频源添加监听;
2.全屏跟退出全屏,主要是注意AVPlayerLayer的布局,不会跟着superLayer的变动而变动,需要手动再设置一遍;

具体可以结合Demo来看。

Demo下载

觉得有用,请帮忙点亮红心

Better Late Than Never!
努力是为了当机会来临时不会错失机会。
共勉!

10人点赞
iOS知识点

链接:https://www.jianshu.com/p/55825996cb11

收起阅读 »

Android仿魅族桌面悬浮球!

背景 游戏内的悬浮窗通常情况下只出现在游戏内,用做切换账号、客服中心等功能的快速入口。本文将介绍几种实现方案,以及我们踩过的坑 1、方案一:应用外悬浮窗+栈顶权限/生命周期回调 通常实现悬浮窗,首先考虑到的会是要使用悬浮窗权限,用WindowManager...
继续阅读 »

背景



游戏内的悬浮窗通常情况下只出现在游戏内,用做切换账号、客服中心等功能的快速入口。本文将介绍几种实现方案,以及我们踩过的坑



1、方案一:应用外悬浮窗+栈顶权限/生命周期回调


通常实现悬浮窗,首先考虑到的会是要使用悬浮窗权限,用WindowManager在设备界面上addView实现(UI层级较高,应用外显示)


1、弹出悬浮窗需要用到悬浮窗权限

<!--悬浮窗权限-->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>



2、判断悬浮窗游戏内外显示


方式一:使用栈顶权限获取当前


//需要声明权限


//判断当前是否在后台
private boolean isAppIsInBackground(Context context) {
boolean isInBackground = true;
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT_WATCH) {
List runningProcesses = am.getRunningAppProcesses();
for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
//前台程序
if (processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
for (String activeProcess : processInfo.pkgList) {
if (activeProcess.equals(context.getPackageName())) {
isInBackground = false;
}
}
}
}
} else {
List taskInfo = am.getRunningTasks(1);
ComponentName componentInfo = taskInfo.get(0).topActivity;
if (componentInfo.getPackageName().equals(context.getPackageName())) {
isInBackground = false;
}
}

return isInBackground;


这里考虑到这种方案网上有很多具体案例,在这里就不实现了。但是这种方案有如下缺点:


1、适配问题,悬浮窗权限在不同设备上由于不同产商实现不同,适配难。


2、向用户申请权限,打开率较低,体验较差


2、方案二:addContentView实现


原理:Activity的接口中除了我们常用的setContentView接口外,还有addContentView接口。利用该接口可以在Activity上添加View。


这里你可能会问:


1、那只能在一个Activity上添加吧?


没错,是只能在当前Activity上添加,但是由于游戏通常也就在一个Activity跑,因此基本上是可以接受的。


2、只add一个view,那拖动怎么实现?


LayoutParams params = new LayoutParams(mWidth, mHeight);
params.setMargins(mLeft, mTop, 0, 0);
setLayoutParams(params);


通过更新LayoutParams调整子View在父View中的位置就能实现


具体代码如下:


/**
* @author zhuxiaoxin
* 可拖拽贴边的view
*/

public class DragViewLayout extends RelativeLayout {

//手指拖拽得到的位置
int mLeft, mRight, mTop, mBottom;

//view所在的位置
int mLastX, mLastY;

/**
* 屏幕宽度|高度
*/

int mScreenWidth, mScreenHeight;

/**
* view的宽度|高度
*/

int mWidth, mHeight;


/**
* 是否在拖拽过程中
*/

boolean isDrag = false;

/**
* 系统最小滑动距离
* @param context
*/

int mTouchSlop = 0;

public DragViewLayout(Context context) {
this(context, null);
}

public DragViewLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public DragViewLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScreenWidth = context.getResources().getDisplayMetrics().widthPixels;
mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}

@Override
protected void onFinishInflate()
{
super.onFinishInflate();
}

@Override
public boolean dispatchTouchEvent(MotionEvent event)
{
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLeft = getLeft();
mRight = getRight();
mTop = getTop();
mBottom = getBottom();
mLastX = (int) event.getRawX();
mLastY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int x = (int) event.getRawX();
int y = (int) event.getRawY();
int dx = x - mLastX;
int dy = y - mLastY;
if (Math.abs(dx) > mTouchSlop) {
isDrag = true;
}
mLeft += dx;
mRight += dx;
mTop += dy;
mBottom += dy;
if (mLeft < 0) {
mLeft = 0;
mRight = mWidth;
}
if (mRight >= mScreenWidth) {
mRight = mScreenWidth;
mLeft = mScreenWidth - mWidth;
}
if (mTop < 0) {
mTop = 0;
mBottom = getHeight();
}
if (mBottom > mScreenHeight) {
mBottom = mScreenHeight;
mTop = mScreenHeight - mHeight;
}
mLastX = x;
mLastY = y;
//根据拖动举例设置view的margin参数,实现拖动效果
LayoutParams params = new LayoutParams(mWidth, mHeight);
params.setMargins(mLeft, mTop, 0, 0);
setLayoutParams(params);
break;
case MotionEvent.ACTION_UP:
//手指抬起,执行贴边动画
if (isDrag) {
startAnim();
isDrag = false;
}
break;
}
return super.dispatchTouchEvent(event);
}

//执行贴边动画
private void startAnim(){
ValueAnimator valueAnimator;
if (mLeft < mScreenWidth / 2) {
valueAnimator = ValueAnimator.ofInt(mLeft, 0);
} else {
valueAnimator = ValueAnimator.ofInt(mLeft, mScreenWidth - mWidth);
}
//动画执行时间
valueAnimator.setDuration(100);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation)
{
mLeft = (int)animation.getAnimatedValue();
//动画执行依然是使用设置margin参数实现
LayoutParams params = new LayoutParams(mWidth, mHeight);
params.setMargins(mLeft, getTop(), 0, 0);
setLayoutParams(params);
}
});
valueAnimator.start();
}


@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
super.onLayout(changed, l, t, r, b);
if (mWidth == 0) {
//获取view的高宽
mWidth = getWidth();
mHeight = getHeight();
}
}

}


/**
*
@author zhuxiaoxin
* 37悬浮窗基础view
*/

public class SqAddFloatView extends DragViewLayout {

private RelativeLayout mFloatContainer;

public SqAddFloatView(final Context context, final int floatImgId) {
super(context);
setClickable(true);
final ImageView floatView = new ImageView(context);
floatView.setImageResource(floatImgId);
floatView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context, "点击了悬浮球", Toast.LENGTH_SHORT).show();
}
});
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
addView(floatView, params);
}

public void show(Activity activity) {
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
if(mFloatContainer == null) {
mFloatContainer = new RelativeLayout(activity);
}
RelativeLayout.LayoutParams floatViewParams = new RelativeLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
floatViewParams.setMargins(0, (int) (mScreenHeight * 0.4), 0, 0);
mFloatContainer.addView(this, floatViewParams);
activity.addContentView(mFloatContainer, params);

}
}


在Activity中使用


SqAddFloatView(this, R.mipmap.ic_launcher).show(this)


3、方案三:WindowManager+应用内层级实现


WindowManger中的层级有如下两个(其实是一样的~)可以实现在Activity上增加View


        /**
* Start of types of sub-windows. The {@link #token} of these windows
* must be set to the window they are attached to. These types of
* windows are kept next to their attached window in Z-order, and their
* coordinate space is relative to their attached window.
*/

public static final int FIRST_SUB_WINDOW = 1000;

/**
* Window type: a panel on top of an application window. These windows
* appear on top of their attached window.
*/

public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;


具体实现时,WindowManger相关的核心代码如下:


    public void show() {
floatLayoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
//最最重要的一句 WindowManager.LayoutParams.FIRST_SUB_WINDOW,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
PixelFormat.RGBA_8888);
floatLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
floatLayoutParams.x = mMinWidth;
floatLayoutParams.y = (int)(mScreenHeight * 0.4);
mWindowManager.addView(this, floatLayoutParams);
}


添加完view如何更新位置?


使用WindowManager的updateViewLayout方法


mWindowManager.updateViewLayout(DragViewLayout.this, floatLayoutParams);


完整代码如下:


DragViewLayout:


public class DragViewLayout extends RelativeLayout {

//view所在位置
int mLastX, mLastY;

//屏幕高宽
int mScreenWidth, mScreenHeight;

//view高宽
int mWidth, mHeight;

/**
* 是否在拖拽过程中
*/

boolean isDrag = false;

/**
* 系统最小滑动距离
* @param context
*/

int mTouchSlop = 0;

WindowManager.LayoutParams floatLayoutParams;
WindowManager mWindowManager;

//手指触摸位置
private float xInScreen;
private float yInScreen;
private float xInView;
public float yInView;


public DragViewLayout(Context context) {
this(context, null);
}

public DragViewLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public DragViewLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScreenWidth = context.getResources().getDisplayMetrics().widthPixels;
mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
}

@Override
protected void onFinishInflate()
{
super.onFinishInflate();
}

@Override
public boolean dispatchTouchEvent(MotionEvent event)
{
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastX = (int) event.getRawX();
mLastY = (int) event.getRawY();
yInView = event.getY();
xInView = event.getX();
xInScreen = event.getRawX();
yInScreen = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int dx = (int) event.getRawX() - mLastX;
int dy = (int) event.getRawY() - mLastY;
if (Math.abs(dx) > mTouchSlop || Math.abs(dy) > mTouchSlop) {
isDrag = true;
}
xInScreen = event.getRawX();
yInScreen = event.getRawY();
mLastX = (int) event.getRawX();
mLastY = (int) event.getRawY();
//拖拽时调用WindowManager updateViewLayout更新悬浮球位置
updateFloatPosition(false);
break;
case MotionEvent.ACTION_UP:
if (isDrag) {
//执行贴边
startAnim();
isDrag = false;
}
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}

//更新悬浮球位置
private void updateFloatPosition(boolean isUp) {
int x = (int) (xInScreen - xInView);
int y = (int) (yInScreen - yInView);
if(isUp) {
x = isRightFloat() ? mScreenWidth : 0;
}
if(y < 0) {
y = 0;
}
if(y > mScreenHeight - mHeight) {
y = mScreenHeight - mHeight;
}
floatLayoutParams.x = x;
floatLayoutParams.y = y;
//更新位置
mWindowManager.updateViewLayout(this, floatLayoutParams);
}

/**
* 是否靠右边悬浮
* @return
*/

boolean isRightFloat() {
return xInScreen > mScreenWidth / 2;
}


//执行贴边动画
private void startAnim(){
ValueAnimator valueAnimator;
if (floatLayoutParams.x < mScreenWidth / 2) {
valueAnimator = ValueAnimator.ofInt(floatLayoutParams.x, 0);
} else {
valueAnimator = ValueAnimator.ofInt(floatLayoutParams.x, mScreenWidth - mWidth);
}
valueAnimator.setDuration(200);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation)
{
floatLayoutParams.x = (int)animation.getAnimatedValue();
mWindowManager.updateViewLayout(DragViewLayout.this, floatLayoutParams);
}
});
valueAnimator.start();
}

//悬浮球显示
public void show() {
floatLayoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
PixelFormat.RGBA_8888);
floatLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
floatLayoutParams.x = 0;
floatLayoutParams.y = (int)(mScreenHeight * 0.4);
mWindowManager.addView(this, floatLayoutParams);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
super.onLayout(changed, l, t, r, b);
if (mWidth == 0) {
//获取悬浮球高宽
mWidth = getWidth();
mHeight = getHeight();
}
}
}


悬浮窗View


public class SqWindowManagerFloatView extends DragViewLayout {


public SqWindowManagerFloatView(final Context context, final int floatImgId) {
super(context);
setClickable(true);
final ImageView floatView = new ImageView(context);
floatView.setImageResource(floatImgId);
floatView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context, "点击了悬浮球", Toast.LENGTH_SHORT).show();
}
});
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
addView(floatView, params);
}
}


使用:


SqWindowManagerFloatView(this, R.mipmap.float_icon).show()


4、小结


1、方案一需要用到多个权限,显然是不合适的。


2、方案二简单方便,但是用到了Activity的addContentView方法,在某些游戏引擎上使用会有问题。因为有些游戏引擎不是在Activity上跑的,而是在NativeActivity上跑


3、方案三是我们当前采用的方案,目前还暂未发现有显示不出来之类的问题~


4、本文讲述的方案只是Demo哈,实际使用还需要考虑刘海屏的问题,本文暂未涉及


代码下载:way-Doughnut-master.zip 收起阅读 »

安卓自定义view - 2048 小游戏

为了学习自定义 ViewGroup,正碰巧最近无意间玩了下 2048 的游戏,因此这里就来实现一个 2048 小游戏。想必很多人应该是玩过这个游戏的,如果没有玩过的可以下载玩一下。下图是我实现的效果。2048 游戏规则游戏规则比较简单,共有如下几个步骤:向一个...
继续阅读 »

为了学习自定义 ViewGroup,正碰巧最近无意间玩了下 2048 的游戏,因此这里就来实现一个 2048 小游戏。想必很多人应该是玩过这个游戏的,如果没有玩过的可以下载玩一下。下图是我实现的效果。

2048 游戏规则

游戏规则比较简单,共有如下几个步骤:

  1. 向一个方向移动,所有格子会向那个方向移动
  2. 相同的数字合并,即相加
  3. 每次移动时,空白处会随机出现一个数字2或4
  4. 当界面不可移动时,即格子被数字填满,游戏结束,网格中出现 2048 的数字游戏胜利,反之游戏失败。

2048 游戏算法

算法主要是讨论上下左右四个方向如何合并以及移动,这里我以向左和向上来说明,而向下和向右就由读者自行推导,因为十分相似。

向左移动算法

先来看下面两张图,第一张是初始状态,可以看到网格中有个数字 2。在这里用二维数组来描述。它的位置应该是第2行第2列 。第二张则是它向左移动后的效果图,可以看到 2 已经被移动到最左边啦!

我们最常规的想法就是首先遍历这个二维数组,找到这个数的位置,接着合并和移动。所以第一步肯定是循环遍历。

int i;
for (int x = 0; x < 4; x++) {
for (int y = 0; y < 4; ) {
Model model = models[x][y];
int number = model.getNumber();
if (number == 0) {
y++;
continue;
} else {
// 找到不为零的位置. 下面就需要进行合并和移动处理

}
}
}


上面的代码非常简单,这里引入了 Model 类,这个类是封装了网格单元的数据和网格视图。定义如下:先不纠结视图的绘制,我们先把算法理清楚,算法搞明白了也就解决一大部分了,其他就是自定义 View 的知识。上述的过程就是,遍历整个网格,找到不为零的网格位置。


public class Model {

private int number;
/**
* 单元格视图.
*/

private CellView cellView;

public Model(int number, CellView cellView) {
this.number = number;
this.cellView = cellView;
}

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

public CellView getCellView() {
return cellView;
}

public void setCellView(CellView cellView) {
this.cellView = cellView;
}
}




让我们来思考一下,合并要做什么,那么我们再来看一张图。

从这张图中我们可以看到在第一行的最后两个网格单元都是2,当向左移动时,根据 2048 游戏规则,我们需要将后面的一个2 和前面的 2 进行合并(相加)运算。是不是可以推理,我们找到第一个不为零的数的位置,然后找到它右边第一个不为零的数,判断他们是否相等,如果相等就合并。算法如下:

int i;
for (int x = 0; x < 4; x++) {
for (int y = 0; y < 4; ) {
Model model = models[x][y];
int number = model.getNumber();
if (number == 0) {
y++;
continue;
} else {
// 找到不为零的位置. 下面就需要进行合并和移动处理
// 这里的 y + 1 就是找到这个数的右侧
for (i = y + 1; i < 4; i++) {
if (models[x][i].getNumber() == 0) {
continue;
} else if (models[x][y].getNumber() == models[x][i].getNumber()) {
// 找到相等的数
// 合并,相加操作
models[x][y].setNumber(
models[x][y].getNumber() + models[x][i].getNumber())

// 将这个数清0
models[x][i].setNumber(0);

break;
} else {
break;
}
}

// 防止陷入死循环,所以必须要手动赋值,将其跳出。
y = i;
}
}
}


通过上面的过程,我们就将这个数右侧的第一个相等的数进行了合并操作,是不是也好理解的。不理解的话可以在草稿纸上多画一画,多推导几次。

搞定了合并操作,现在就是移动了,移动肯定是要将所有数据的单元格都移动到左侧,移动的条件是,找到第一个不为零的数的坐标,继续向前找到第一个数据为零即空白单元格的位置,将数据覆盖它,并将后一个单元格数据清空。算法如下:

for (int x = 0; x < 4; x++) {
for (y = 0; y < 4; y++) {
if (models[x][y].getNumber() == 0) {
continue;
} else {
// 找到当前数前面为零的位置,即空格单元
for (int j = y; j >= 0 && models[x][j - 1].getNumber() == 0; j--) {
// 数据向前移动,即数据覆盖.
models[j - 1][y].setNumber(
models[j][y].getNumber())
// 清空数据
models[j][y].setNumber(0)
}
}
}
}

到此向左移动算法完毕,接着就是向上移动的算法。

向上移动算法

有了向左移动的算法思维,理解向上的操作也就变得容易一些啦!首先我们先来看合并,合并的条件也就是找到第一个不为零的数,然后找到它下一行第一个不为零且相等的数进行合并。算法如下:

int i = 0;
for (int y = 0; y < 4; y++) {
for (x = 3; x >= 0; ) {
if (models[x][y].getNumber() == 0) {
continue;
} else {
for (i = x + 1; i < 4; i++) {
if (models[i][y].getNumber() == 0) {
continue;
} else if (models[x][y].getNumber() == models[i][y].getNumber()) {
models[x][y].setNumber(
models[x][y].getNumber() + models[i][y].getNumber();
)

models[i][y].setNumber(0);

break;
} else {
break;
}
}
}
}
}


移动的算法也类似,即找到第一个不为零的数前面为零的位置,即空格单元的位置,将数据覆盖并将后一个单元格的数据清空。

for (int x = 0; x < 4; x++) {
for (int y = 0; y < 4; y++) {
if (models[x][y].getNumber() == 0) {
continue;
} else {
for (int j = x; x >
0 && models[j - 1][y].getNumber() == 0; j--) {
models[j -1][y].setNumber(models[j][y].getNumber());

models[j][y].setNumber(0);
}
}
}
}


到此,向左移动和向上移动的算法就描述完了,接下来就是如何去绘制视图逻辑啦!

网格单元绘制

首先先忽略数据源,我们只是单纯的绘制网格,有人可能说了我们不用自定义的方式也能实现,我只想说可以,但是不推荐。如果使用自定义 ViewGroup,将每一个小的单元格作为单独的视图。这样扩展性更好,比如我做了对随机显示的单元格加上动画。

既然是自定义 ViewGroup, 那我们就创建一个类并继承 ViewGroup,其定义如下:

public class Play2048Group extends ViewGroup {

public Play2048Group(Context context) {
this(context, null);
}

public Play2048Group(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public Play2048Group(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

......
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
.....
}

}


我们要根据子视图的大小来测量容器的大小,在 onLayout 中摆放子视图。为了更好的交给其他开发者使用,我们尽量可以让 view 能被配置。那么就要自定义属性。

  1. 自定义属性

这里只是提供了设置网格单元行列数,其实这里我我只取两个值的最大值作为行列的值。













  1. 布局中加载自定义属性

可以看到将传入的 row 和 column 取大的作为行列数。

public Play2048Group(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Play2048Group);

try {
mRow = a.getInteger(R.styleable.Play2048Group_row, 4);
mColumn = a.getInteger(R.styleable.Play2048Group_column, 4);
// 保持长宽相等排列, 取传入的最大值
if (mRow > mColumn) {
mColumn = mRow;
} else {
mRow = mColumn;
}

init();

} catch (Exception e) {
e.printStackTrace();
} finally {
a.recycle();
}
}


  1. 网格子视图

因为整个网格有一个个网格单元组成,其中每一个网格单元都是一个 view, 这个 view 其实也就只是绘制了一个矩形,然后在矩形的中间绘制文字。考虑文章篇幅,我这里只截取 onMeasure 和 onDraw 方法。

 @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

// 我这里直接写死了,当然为了屏幕适配,这个值应该由外部传入的,
// 这里就当我留下的作业吧 😄
setMeasuredDimension(130, 130);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

// 绘制矩形.
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);

// 如果当前单元格的数据不为0,就绘制。
// 如果为零,就使用背景的颜色作为画笔绘制,这么做就是为了不让它显示出来😳
if (!mNumber.equalsIgnoreCase("0")) {
mTextPaint.setColor(Color.parseColor("#E451CD"));
canvas.drawText(mNumber,
(float) (getMeasuredWidth() - bounds.width()) / 2,
(float) (getMeasuredHeight() / 2 + bounds.height() / 2), mTextPaint);
} else {
mTextPaint.setColor(Color.parseColor("#E4CDCD"));
canvas.drawText(mNumber,
(float) (getMeasuredWidth() - bounds.width()) / 2,
(float) (getMeasuredHeight() / 2 + bounds.height() / 2), mTextPaint);
}
}



  1. 测量容器视图

由于网格是行列数都相等,则宽和高都相等。那么所有的宽加起来除以 row, 所有的高加起来除以 column 就得到了最终的宽高, 不过记得要加上边距。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

int width = 0;
int height = 0;

int count = getChildCount();

MarginLayoutParams layoutParams =
(MarginLayoutParams)getChildAt(0).getLayoutParams();

// 每一个单元格都有左边距和上边距
int leftMargin = layoutParams.leftMargin;
int topMargin = layoutParams.topMargin;

for (int i = 0; i < count; i++) {
CellView cellView = (CellView) getChildAt(i);
cellView.measure(widthMeasureSpec, heightMeasureSpec);

int childW = cellView.getMeasuredWidth();
int childH = cellView.getMeasuredHeight();

width += childW;
height += childH;
}

// 需要加上每个单元格的左边距和上边距
setMeasuredDimension(width / mRow + (mRow + 1) * leftMargin,
height / mRow + (mColumn + 1) * topMargin);
}


  1. 布局子视图(网格单元)

布局稍微麻烦点,主要是在换行处的计算有点绕。首先我们找一下什么时候是该换行了,如果是 4 * 4 的 16 宫格,我们可以知道每一行的开头应该是 0、4、8、12,如果要用公式来表示的就是: temp = mRow * (i / mRow), 这里的 mRow 为行数,i 为索引。

我们这里首先就是要确定每一行的第一个视图的位置,后面的视图就好确定了, 下面是推导过程:

第一行: 
网格1:
left = lefMargin;
top = topMargin;
right = leftMargin + width;
bottom = topMargin + height;

网格2:
left = leftMargin + width + leftMargin
top = topMargin;
right = leftMargin + width + leftMargin + width
bottom = topMargin + height

网格3:
left = leftMargin + width + leftMargin + width + leftMargin
right = leftMargin + width + leftMargin + width + leftMargin + width

...
第二行:
网格1:
left = leftMargin
top = topMargin + height
right = leftMargin + width
bottom = topMargin + height + topMargin + height

网格2:
left = leftMargin + width + leftMargin
top = topMargin + height + topMargin
right = leftMargin + width + lefMargin + width
bottom = topMargin + height + topMargin + height


上面的应该很简单的吧,这是根据画图的方式直观的排列,我们可以归纳总结,找出公式。

除了每一行的第一个单元格的 left, right 都相等。 其他的可以用一个公式来总结:

left = leftMargin * (i - temp + 1) + width * (i - temp)
right = leftMargin * (i - temp + 1) + width * (i - temp + 1)

可以随意带数值进入然后对比画图看看结果,比如(1, 1) 即第二行第二列。

temp = row * (i / row) => 4 * 1 = 4

left = leftMargin * (5 - 4 + 1) + width * (5 - 4)
= leftMargin * 2 + width

right = leftMargin * (5 - 4 + 1) + width * (5 - 4 + 1)
= lefMargin * 2 + width * 2

和上面的手动计算完全一样,至于为什么 i = 5 那是因为 i 循环到第二行的第二列为 5


除了第一行第一个单元格其他的 top, bottom 可以用公式:

top = height * row + topMargin * row + topMargin
bottom = height * (row + 1) + topMargin(row + 1)


@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
CellView cellView = (CellView) getChildAt(i);
MarginLayoutParams layoutParams = (MarginLayoutParams) cellView.getLayoutParams();
int leftMargin = layoutParams.leftMargin;
int topMargin = layoutParams.topMargin;

int width = cellView.getMeasuredWidth();
int height = cellView.getMeasuredHeight();

int left = 0, top = 0, right = 0, bottom = 0;

// 每一行开始, 0, 4, 8, 12...
int temp = mRow * (i / mRow);
// 每一行的开头位置.
if (i == temp) {
left = leftMargin;
right = width + leftMargin;
} else {
left = leftMargin * (i - temp + 1) + width * (i - temp);
right = leftMargin * (i - temp + 1) + + width * (i - temp + 1);
}

int row = i / mRow;
if (row == 0) {
top = topMargin;
bottom = height + topMargin;
} else {
top = height * row + topMargin * row + topMargin;
bottom = height * (row + 1) + (row + 1) * topMargin;
}

cellView.layout(left, top, right, bottom);
}
}


  1. 初始数据
private void init() {
models = new Model[mRow][mColumn];
cells = new ArrayList<>(mRow * mColumn);

for (int i = 0; i < mRow * mColumn; i++) {
CellView cellView = new CellView(getContext());
MarginLayoutParams params = new MarginLayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

params.leftMargin = 10;
params.topMargin = 10;
cellView.setLayoutParams(params);

Model model = new Model(0, cellView);
cells.add(model);

addView(cellView, i);
}
}


以上就是未带数据源的宫格绘制过程,接下来开始接入数据源来动态改变宫格的数据啦!

动态改变数据

  1. 初始化数据源,随机显示一个数据 2
private void init() {
... 省略部分代码.....

int i = 0;
for (int x = 0; x < mRow; x++) {
for (int y = 0; y < mColumn; y++) {
models[x][y] = cells.get(i);
i++;
}
}

// 生成一个随机数,初始化数据.
mRandom = new Random();
rand = mRandom.nextInt(mRow * mColumn);
Model model = cells.get(rand);
model.setNumber(2);
CellView cellView = model.getCellView();
cellView.setNumber(2);

// 初始化时空格数为总宫格个数 - 1
mAllCells = mRow * mColumn - 1;

// 程序动态变化这个值,用来判断当前宫格还有多少空格可用.
mEmptyCells = mAllCells;


... 省略部分代码.....
}


  1. 计算随机数生成的合法单元格位置

生成的随机数据必须在空白的单元格上。

 private void nextRand() {
// 如果所有宫格被填满则游戏结束,
// 当然这里也有坑,至于怎么发现,你多玩几次机会发现,
// 这个坑我就不填了,有兴趣的可以帮我填一下😄😄
if (mEmptyCells <= 0) {
findMaxValue();
gameOver();
return;
}

int newX, newY;

if (mEmptyCells != mAllCells || mCanMove == 1) {
do {
// 通过伪随机数获取新的空白位置
newX = mRandom.nextInt(mRow);
newY = mRandom.nextInt(mColumn);
} while (models[newX][newY].getNumber() != 0);

int temp = 0;

do {
temp = mRandom.nextInt(mRow);
} while (temp == 0 || temp == 2);

Model model = models[newX][newY];
model.setNumber(temp + 1);
CellView cellView = model.getCellView();
cellView.setNumber(model.getNumber());
playAnimation(cellView);

// 空白格子减1
mEmptyCells--;
}
}


  1. 向左移动

算法是我们前面推导的,最后调用 drawAll() 绘制单元格文字, 以及调用 nextRand() 生成新的随机数。

public void left() {
if (leftRunnable == null) {
leftRunnable = new Runnable() {
@Override
public void run() {
int i;
for (int x = 0; x < mRow; x++) {
for (int y = 0; y < mColumn; ) {
Model model = models[x][y];
int number = model.getNumber();
if (number == 0) {
y++;
continue;
} else {
// 找到不为零的位置. 往后找不为零的数进行运算.
for (i = y + 1; i < mColumn; i++) {
Model model1 = models[x][i];
int number1 = model1.getNumber();
if (number1 == 0) {
continue;
} else if (number == number1) {
// 如果找到和这个相同的,则进行合并运算(相加)。
int temp = number + number1;
model.setNumber(temp);
model1.setNumber(0);

mEmptyCells++;
break;
} else {
break;
}
}

y = i;
}
}
}

for (int x = 0; x < mRow; x++) {
for (int y = 0; y < mColumn; y++) {
Model model = models[x][y];
int number = model.getNumber();
if (number == 0) {
continue;
} else {
for (int j = y; (j > 0) && models[x][j - 1].getNumber() == 0; j--) {
models[x][j - 1].setNumber(models[x][j].getNumber());
models[x][j].setNumber(0);

mCanMove = 1;
}
}
}
}

drawAll();
nextRand();
}
};
}

mExecutorService.execute(leftRunnable);
}

  1. 随机单元格动画
private void playAnimation(final CellView cellView) {
mainHandler.post(new Runnable() {
@Override
public void run() {
ObjectAnimator animator = ObjectAnimator.ofFloat(
cellView, "alpha", 0.0f, 1.0f);
animator.setDuration(300);
animator.start();
}
});
}


代码下载:i1054959069-simple-2048-games-master.zip

收起阅读 »

一个你想象不到的验证码输入框!

之所以造这个轮子,是因为之前有这样的需求,然后也用过其它类似开源的库(VerificationCodeView),但是需求随着需求的变动,之前使用的库就不太满足现有的需求。所以最近抽空写了一个。 支持设置框数量 支持设置框的风格样式&nbs...
继续阅读 »

SplitEditText for Android 是一个灵活的分割编辑框。常常应用于 验证码输入 、密码输入 、等场景。

之所以造这个轮子,是因为之前有这样的需求,然后也用过其它类似开源的库(VerificationCodeView),但是需求随着需求的变动,之前使用的库就不太满足现有的需求。所以最近抽空写了一个。

特性说明

  •  支持设置框数量
  •  支持设置框的风格样式
  •  支持根据状态区分框颜色
  •  基于EditText实现,更优雅


SplitEditText 自定义属性说明

属性值类型默认值说明
setStrokeWidthdimension1dp画笔描边的宽度
setBorderColorcolor#FF666666边框颜色
setInputBorderColorcolor#FF1E90FF已输入文本的边框颜色
setFocusBorderColorcolor焦点框的边框颜色
setBoxBackgroundColorcolor框的背景颜色
setBorderCornerRadiusdimension0dp框的圆角大小(当 BorderSpacing 为 0dp 时,只有最左和最右两端的框有圆角)
setBorderSpacingdimension8dp框与框之间的间距大小
setMaxLengthinteger6允许输入的最大长度(框个数量)
setBorderStyleenumbox边框风格
setTextStyleenumplain_text文本风格(可以是明文或者密文,默认:明文)
setCipherMaskstring*密文掩码(当 TextStyle 为密文时,可自定义密文掩码)
setFakeBoldTextbooleanfalse是否是粗体

引入

Maven:


com.king.view
splitedittext
1.0.0
pom

Gradle:

//AndroidX
implementation 'com.king.view:splitedittext:1.0.0'

如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)

allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

布局示例

    
android:id="@+id/splitEditText"
android:layout_width="match_parent"
android:layout_height="45dp"
android:inputType="number"/>

代码示例

Kotlin

    //设置监听
splitEditText.setOnTextInputListener(object : SplitEditText.OnTextInputListener {
override fun onTextInputChanged(text: String, length: Int) {
//TODO 文本输入改变
}

override fun onTextInputCompleted(text: String) {
//TODO 文本输入完成
}

})

Java

    //设置监听
splitEditText.setOnTextInputListener(new SplitEditText.OnTextInputListener(){

@Override
public void onTextInputChanged(String text, int length) {
//TODO 文本输入改变
}

@Override
public void onTextInputCompleted(String text) {
//TODO 文本输入完成
}
});

更多使用详情,请查看app中的源码使用示例

代码下载:jenly1314-SplitEditText-master



收起阅读 »

JAVA开发MQTT程序总结

JAVA开发MQTT总结MQTT 介绍它是一种 机器之间通讯 machine-to-machine (M2M)、物联网 Internet of Things (IoT)常用的一种轻量级消息传输协议适用于网络带宽较低的场合包含发布、订阅模式,通过一个代理服务器(...
继续阅读 »

JAVA开发MQTT总结

MQTT 介绍

  • 它是一种 机器之间通讯 machine-to-machine (M2M)、物联网 Internet of Things (IoT)常用的一种轻量级消息传输协议
  • 适用于网络带宽较低的场合
  • 包含发布、订阅模式,通过一个代理服务器(broker),任何一个客户端(client)都可以订阅或者发布某个主题的消息,然后订阅了该主题的客户端则会收到该消息

mqtt还是之前公司有需求所以写的一个demo,在这里记录下来,方便有人使用的时候查阅,不涉及mqtt的具体讲解,只是贴代码和运行过程。

MQTT的入门,以及特性,协议,结构的讲解,请看下面这篇文章

www.runoob.com/w3cnote/mqt…

什么是MQTT,它能干什么,它的应用场景在哪里?请参考下面这篇文章

www.ibm.com/developerwo…

本文中采用的MQTT服务器Apache-Apollo的下载配置搭建过程,请参考下面这篇文章

blog.csdn.net/qq_29350001…

下面就开始创建broker,

RaindeMacBook-Pro:bin rain$ ./apollo create mybroker
Creating apollo instance at: mybroker
Generating ssl keystore...

Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore keystore -destkeystore keystore -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

You can now start the broker by executing:

"/Users/rain/Documents/Soft/apache-apollo-1.7.1/bin/mybroker/bin/apollo-broker" run

Or you can run the broker in the background using:

"/Users/rain/Documents/Soft/apache-apollo-1.7.1/bin/mybroker/bin/apollo-broker-service" start


进入新生成的broker中

RaindeMacBook-Pro:bin rain$ ls
apollo apollo.cmd mybroker testbroker
RaindeMacBook-Pro:bin rain$ cd mybroker/
RaindeMacBook-Pro:mybroker rain$ ls
bin data etc log tmp
RaindeMacBook-Pro:mybroker rain$ cd bin
RaindeMacBook-Pro:bin rain$ ls
apollo-broker apollo-broker-service

可以看到有两个文件,启动apollo-broker

启动成功以后,就可以在浏览器中访问了,默认用户名和密码是admin,password

刚进去是,Topics选项卡是空的,我是在运行程序后截图的,所以有一个topic列表

配置Maven

在pom.xml中添加以下配置

<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.0</version>
</dependency>

再创建下面的类

MqttServer

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

public class MqttServer2 {
/**
* 代理服务器ip地址
*/
public static final String MQTT_BROKER_HOST = "tcp://127.0.0.1:61613";

/**
* 订阅标识
*/
public static final String MQTT_TOPIC = "test2";

private static String userName = "admin";
private static String password = "password";

/**
* 客户端唯一标识
*/
public static final String MQTT_CLIENT_ID = "android_server_xiasuhuei32";
private static MqttTopic topic;
private static MqttClient client;

public static void main(String... args) {
// 推送消息
MqttMessage message = new MqttMessage();
try {
client = new MqttClient(MQTT_BROKER_HOST, MQTT_CLIENT_ID, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
options.setUserName(userName);
options.setPassword(password.toCharArray());
options.setConnectionTimeout(10);
options.setKeepAliveInterval(20);

topic = client.getTopic(MQTT_TOPIC);

message.setQos(1);
message.setRetained(false);
message.setPayload("message from server222222".getBytes());
client.connect(options);

while (true) {
MqttDeliveryToken token = topic.publish(message);
token.waitForCompletion();
System.out.println("已经发送222");
Thread.sleep(10000);
}

} catch (Exception e) {
e.printStackTrace();
}
}
}

MqttClient

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

public class MyMqttClient {
/**
* 代理服务器ip地址
*/
public static final String MQTT_BROKER_HOST = "tcp://127.0.0.1:61613";

/**
* 客户端唯一标识
*/
public static final String MQTT_CLIENT_ID = "android_xiasuhuei321";

/**
* 订阅标识
*/
// public static final String MQTT_TOPIC = "xiasuhuei321";

/**
*
*/
public static final String USERNAME = "admin";
/**
* 密码
*/
public static final String PASSWORD = "password";
public static final String TOPIC_FILTER = "test2";

private volatile static MqttClient mqttClient;
private static MqttConnectOptions options;

public static void main(String... args) {
try {
// host为主机名,clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,
// MemoryPersistence设置clientid的保存形式,默认为以内存保存

mqttClient = new MqttClient(MQTT_BROKER_HOST, MQTT_CLIENT_ID, new MemoryPersistence());
// 配置参数信息
options = new MqttConnectOptions();
// 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,
// 这里设置为true表示每次连接到服务器都以新的身份连接
options.setCleanSession(true);
// 设置用户名
options.setUserName(USERNAME);
// 设置密码
options.setPassword(PASSWORD.toCharArray());
// 设置超时时间 单位为秒
options.setConnectionTimeout(10);
// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
options.setKeepAliveInterval(20);
// 连接
mqttClient.connect(options);
// 订阅
mqttClient.subscribe(TOPIC_FILTER);
// 设置回调
mqttClient.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable throwable) {
System.out.println("connectionLost");
}

@Override
public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {
System.out.println("Topic: " + s + " Message: " + mqttMessage.toString());
}

@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {

}
});
} catch (Exception e) {
e.printStackTrace();
}

}

}

PublishSample

import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

/**
*发布端
*/
public class PublishSample {
public static void main(String[] args) {

String topic = "test2";
String content = "hello 哈哈";
int qos = 1;
String broker = "tcp://127.0.0.1:61613";
String userName = "admin";
String password = "password";
String clientId = "pubClient";
// 内存存储
MemoryPersistence persistence = new MemoryPersistence();

try {
// 创建客户端
MqttClient sampleClient = new MqttClient(broker, clientId, persistence);
// 创建链接参数
MqttConnectOptions connOpts = new MqttConnectOptions();
// 在重新启动和重新连接时记住状态
connOpts.setCleanSession(false);
// 设置连接的用户名
connOpts.setUserName(userName);
connOpts.setPassword(password.toCharArray());
// 建立连接
sampleClient.connect(connOpts);
// 创建消息
MqttMessage message = new MqttMessage(content.getBytes());
// 设置消息的服务质量
message.setQos(qos);
// 发布消息
sampleClient.publish(topic, message);
// 断开连接
sampleClient.disconnect();
// 关闭客户端
sampleClient.close();
} catch (MqttException me) {
System.out.println("reason " + me.getReasonCode());
System.out.println("msg " + me.getMessage());
System.out.println("loc " + me.getLocalizedMessage());
System.out.println("cause " + me.getCause());
System.out.println("excep " + me);
me.printStackTrace();
}
}
}

SubscribeSample

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

/**
*订阅端
*/
public class SubscribeSample {

public static void main(String[] args) throws MqttException {
String HOST = "tcp://127.0.0.1:61613";
String TOPIC = "test2";
int qos = 1;
String clientid = "subClient";
String userName = "admin";
String passWord = "password";
try {
// host为主机名,test为clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,MemoryPersistence设置clientid的保存形式,默认为以内存保存
MqttClient client = new MqttClient(HOST, clientid, new MemoryPersistence());
// MQTT的连接设置
MqttConnectOptions options = new MqttConnectOptions();
// 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
options.setCleanSession(true);
// 设置连接的用户名
options.setUserName(userName);
// 设置连接的密码
options.setPassword(passWord.toCharArray());
// 设置超时时间 单位为秒
options.setConnectionTimeout(10);
// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
options.setKeepAliveInterval(20);
// 设置回调函数
client.setCallback(new MqttCallback() {

public void connectionLost(Throwable cause) {
System.out.println("connectionLost");
}

public void messageArrived(String topic, MqttMessage message) throws Exception {
System.out.println("topic:"+topic);
System.out.println("Qos:"+message.getQos());
System.out.println("message content:"+new String(message.getPayload()));

}

public void deliveryComplete(IMqttDeliveryToken token) {
System.out.println("deliveryComplete---------"+ token.isComplete());
}

});
client.connect(options);
//订阅消息
client.subscribe(TOPIC, qos);
} catch (Exception e) {
e.printStackTrace();
}
}
}

启动程序

1.启动MqttServer2以后,开始循环发送消息。

2.启动MyMqttClient开始接收消息。

到这里,整个程序基本可以运行。

3.启动PublishSample,发布一条消息,在启动SubscribeSample来订阅发布的消息。

4.发布的消息在MyMqttClient中也会显示出来

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

收起阅读 »

MQTT在Android端的使用详解以及MQTT服务器搭建、Paho客户端使用

前言最近的项目中使用了MQTT来接收后端推送过来的一些数据,这篇文章来介绍下Android端如何集成使用,关于MQTT相关介绍将不再阐述。由于光写代码不实践的接收下数据很难验证我们写的是否正确,所以我将简单介绍下如何配置个MQTT服务端,并使用工具来发送数据到...
继续阅读 »

前言

最近的项目中使用了MQTT来接收后端推送过来的一些数据,这篇文章来介绍下Android端如何集成使用,关于MQTT相关介绍将不再阐述。

由于光写代码不实践的接收下数据很难验证我们写的是否正确,所以我将简单介绍下如何配置个MQTT服务端,并使用工具来发送数据到服务端,使得手机端能接收到数据。话不多说直接看。

1. MQTT服务器配置

1.1 下载EMQX

下载地址

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

1.2 启动EMQX

在解压后的bin目录下打开cmd命令,输入emqx.cmd start即可启动。

如果你在启动时遇到could't load module...,那就是因为你的路径中包含中文名导致启动不了,将该文件夹放到纯英文目录下即可启动。 在这里插入图片描述

完事后在浏览器内输入http://127.0.0.1:18083即可打开web管理界面,帐号为admin,密码为public

按如图方式将语言改为中文 在这里插入图片描述

1.3 界面说明

左侧的Clients标签下可以看到当前连接的客户端 在这里插入图片描述 左侧的Topics标签下可以看到当前订阅的主题 在这里插入图片描述

1.4 个人理解

到这服务端就算是配置完成了,你可能会问,服务端就是这,那我手机客户端怎么接收消息呢,服务端从哪里发送消息呢?其实EMQX服务是消息中间件服务,有点像是转发。一个客户端发送消息并指定主题,该消息发送到服务端,那么连接了服务端并且订阅了该主题的所有客户端就都能接收到该消息,所以我们手机客户端想要接收到消息,还需要有一端来给EMQX服务端来发送消息才行。

2. MQTT客户端软件 Paho

2.1 下载MQTT客户端软件

下载地址 在这里插入图片描述

下载勾选中的那个文件即可,下载完后解压得到paho.exe,即我们需要的客户端软件。

2.2 MQTT客户端使用

2.2.1 连接服务器

在这里插入图片描述

按如图所示步骤进行点击,1、新增一个连接,2、填写服务器地址和客户标识,这里的标识为自己定义的,服务器地址可在该地址那查看,可以看到是本地地址,端口号是1883或者11883 点击连接后可以看到连接状态变为已连接,就代表我们客户端已经连接到了EMQX。 在这里插入图片描述

2.2.2 发送消息

在这里插入图片描述

在1处填写主题名,2处填写消息然后3处点击发布,然后可以看到4处显示已发布,代表我们已经发送到服务端了。

2.2.3 订阅主题

订阅我们刚才发送消息的那个主题

在这里插入图片描述

点击1处来新增订阅,点击2处输入我们要订阅的主题,这里我们设置为刚才发布消息的那个主题,然后点击3处的订阅,可以看到历史记录那里显示已订阅。

接下来我们再发送一次该主题消息,观察历史记录

在这里插入图片描述

可以看到,当我们发布后,由于我们订阅了该主题,所以就接收到了该主题消息。

在MQTT服务端配置完成以及MQTT客户端软件测试可行后,现在来看我们的安卓端如何订阅并接收消息。

3. Andoird端集成使用

3.1 添加依赖、权限等配置

//MQTT
implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'

AndroidManifest文件配置

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.myfittinglife.mqttdemo">

<!--必要的三个权限-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<application
...>
...
<!--添加该Service-->
<service android:name="org.eclipse.paho.android.service.MqttService"/>
</application>

3.2 使用

3.2.1 创建MqttAndroidClient对象

var mClient: MqttAndroidClient? = null

private fun createClient() {

//1、创建接口回调
//以下回调都在主线程中(如果使用MqttClient,使用此回调里面的都是非主线程)
val mqttCallback: MqttCallbackExtended = object : MqttCallbackExtended {
override fun connectComplete(reconnect: Boolean, serverURI: String) {
//连接成功
Log.i(TAG, "connectComplete: ")
showToast("连接成功")
}

override fun connectionLost(cause: Throwable) {
//断开连接
Log.i(TAG, "connectionLost: ")
showToast("断开连接")

}

@Throws(Exception::class)
override fun messageArrived(topic: String, message: MqttMessage) {
//得到的消息
var msg = message.payload
var str = String(msg)
Log.i(TAG, "messageArrived: $str")
showToast("接收到的消息为:$str")

}

override fun deliveryComplete(token: IMqttDeliveryToken) {
//发送消息成功后的回调
Log.i(TAG, "deliveryComplete: ")
showToast("发送成功")

}
}

//2、创建Client对象
try {
mClient = MqttAndroidClient(this, "tcp://192.168.14.57:1883", "客户端名称,可随意")
mClient?.setCallback(mqttCallback) //设置回调函数
} catch (e: MqttException) {
Log.e(TAG, "createClient: ", e)
}
}

3.2.2 设置MQTT连接的配置信息

val mOptions = MqttConnectOptions()
mOptions.isAutomaticReconnect = false //断开后,是否自动连接
mOptions.isCleanSession = true //是否清空客户端的连接记录。若为true,则断开后,broker将自动清除该客户端连接信息
mOptions.connectionTimeout = 60 //设置超时时间,单位为秒
//mOptions.userName = "Admin" //设置用户名。跟Client ID不同。用户名可以看做权限等级
//mOptions.setPassword("Admin") //设置登录密码
mOptions.keepAliveInterval = 60 //心跳时间,单位为秒。即多长时间确认一次Client端是否在线
mOptions.maxInflight = 10 //允许同时发送几条消息(未收到broker确认信息)
mOptions.mqttVersion = MqttConnectOptions.MQTT_VERSION_3_1_1 //选择MQTT版本

3.2.3 建立连接

try {
mClient?.connect(mOptions, this, object : IMqttActionListener {
override fun onSuccess(asyncActionToken: IMqttToken?) {
Log.i(TAG, "onSuccess:连接成功 ")
}

override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
Log.i(TAG, "onFailure: " + exception?.message)
}

})
} catch (e: MqttException) {
Log.e(TAG, "onCreate: ", e)
}

3.2.4 订阅主题

//设置监听的topic
try {
mClient?.subscribe("topicName", 0)
} catch (e: MqttException) {
Log.e(TAG, "onCreate: ", e)
}

3.2.5 发送消息

try {
var str = "要发送的消息"
var msg = MqttMessage()
msg.payload =str.toByteArray()
mClient?.publish(Const.Subscribe.mTopic,msg)
} catch (e: MqttException) {
Log.e(TAG, "onCreate: ",e )
}

3.3 最终效果

在我们的Paho MQTT Utility软件发送消息后,我们的手机端由于订阅了该主题,所以就可以接收到该消息。 在这里插入图片描述

4. 注意事项

  • 别忘记在manifest中添加service,否则在connect()的时候会报mClient为空。

    <service android:name="org.eclipse.paho.android.service.MqttService"/>
  • 别忘记添加localbroadcastmanager依赖,否则会报Failed resolution of: Landroidx/localbroadcastmanager/content/LocalBroadcastManager错误。

    implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
  • 启动emqx服务时,一定要将该文件目录放到纯英文的目录下,不能包含中文,否则会出现could't load module的错误。

5. 总结

按以上步骤即可完成最基本的功能,以上只是简单的使用,其实还可以设置用户登录名和密码、设置服务质量、重连的操作等。关于MQTT的相关内容可以看这篇文章MQTT

项目Github地址

如果本文对你有帮助,请别忘记三连,如果有不恰当的地方也请提出来,下篇文章见。


作者:重拾丢却的梦

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

来源:掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

学习MQTT协议,与设备沟通

概述 MQTT是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和制动器之间通信的桥梁。 MQTT协议是为大量计算能力有限,且工作在低带宽、不可靠的网络的远程传感器和控...
继续阅读 »

概述


MQTT是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和制动器之间通信的桥梁。


MQTT协议是为大量计算能力有限,且工作在低带宽、不可靠的网络的远程传感器和控制设备通讯而设计的协议。有以下特点:



  • 使用发布/订阅消息模式,提供一对多的消息发布

  • 使用TCP/IP提供网络连接

  • 小型传输,开销很小(固定长度的头部是 2 字节),协议交换最小化,以降低网络流量,传输的内容最大为256MB。

  • 使用 Last Will 和 Testament 特性通知有关各方客户端异常中断的机制。


1.MQTT协议实现方式




MQTT系统由与服务器通信的客户端组成,通常称服务器为“代理Broker”。客户可以是信息发布者Publish或订阅者Subscribe。每个客户端都可以连接到代理。


信息按主题层次结构组织。当发布者具有要分发的新数据时,它会将包含数据的控制消息发送到连接的代理。然后,代理将信息分发给已订阅该主题的任何客户端。发布者不需要有关于订阅者数量或位置的任何数据,而订阅者又不必配置有关发布者的任何数据。


MQTT传输的消息分为:主题(Topic)和负载(payload)两部分: (1)Topic,可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload); (2)payload,可以理解为消息的内容,是指订阅者具体要使用的内容。


2. MQTT协议中的术语




2.1订阅(Subscription)

订阅包含主题筛选器(Topic Filter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。


2.2会话(Session)

每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。


2.3主题名(Topic Name)

连接到一个应用程序消息的标签,该标签与服务器的订阅相匹配。服务器会将消息发送给订阅所匹配标签的每个客户端。 系统主题:通过定义$SYS开头的主题可以查看一些系统信息,如客户端连接数量等, 详细介绍:github.com/mqtt/mqtt.g…


2.4主题筛选器(Topic Filter)

一个对主题名通配符筛选器,在订阅表达式中使用,表示订阅所匹配到的多个主题。 多级匹配符 # 单级匹配符 + 更多主题讨论,请移步github wiki github.com/mqtt/mqtt.g…


2.5负载(Payload)

消息订阅者所具体接收的内容。


3.保留消息和最后遗嘱




保留消息 Retained Messages

MQTT中,无论是发布还是订阅都不会有任何触发事件。 1个Topic只有唯一的retain消息,Broker会保存每个Topic的最后一条retain消息。 发布消息时把retain设置为true,即为保留信息。每个Client订阅Topic后会立即读取到retain消息。如果需要删除retain消息,可以发布一个空的retain消息,因为每个新的retain消息都会覆盖最后一个retain消息。


最后遗嘱 Last Will & Testament

MQTT本身就是为信号不稳定的网络设计的,所以难免一些客户端会无故的和Broker断开连接。 当客户端连接到Broker时,可以指定LWT,Broker会定期检测客户端是否有异常。 当客户端异常掉线时,Broker就往连接时指定的topic里推送当时指定的LWT消息。


4.消息服务质量




有三种消息发布服务质量qos(Quality of Service):


4.1“至多一次”




至多一次



消息发布完全依赖底层TCP/IP网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。


4.2“至少一次”




至少一次



PUBACK消息是对QoS级别为1的PUBLISH消息的响应.PUBACK消息由服务器发送以响应来自发布端的PUBLISH消息,订阅端也会响应来自服务器的PUBLISH消息。当发布端收到PUBACK消息时,它会丢弃原始消息,因为它也被服务器接收(并记录)。


如果一定时间内,发布端或服务器没有收到PUBACK消息,则会进行重发。这种方式虽然确保了消息到达,但消息重复可能会发生。


4.3“只有一次”




只有一次



PUBREC消息是对QoS级别为2的PUBLISH消息的响应。它是QoS级别2协议流的第二个消息。 PUBREC消息由服务器响应来自发布端的PUBLISH消息,或订阅端响应来自服务器的PUBLISH消息。发布端或服务器收到PUBREC消息时,会响应PUBREL消息。


PUBREL消息是从发布端对PUBREC的响应,或从服务器对订阅端PUBREC消息的响应。 这是QoS 2协议流中第三个消息。当服务器从发布者收到PUBREL消息时,服务器会将PUBLISH消息发送到订阅端,并发送PUBCOMP消息到发布端。 当订阅端收到来自服务器的消息PUBREL时,使得消息可用于应用程序并将PUBCOMP消息发送到服务器。


PUBCOMP消息是服务器对来自发布端的PUBREL消息的响应,或订阅者对来自服务器的PUBREL消息的响应。 它是QoS 2协议流程中的第四个也是最后一个消息。当发布端收到PUBCOMP消息时,它会丢弃原始消息,因为它已经将消息发给了服务器。


在一些要求比较严格的计费系统中,可以使用此级别。在计费系统中,消息重复或丢失会导致不正确的结果。这种最高质量的消息发布服务还可以用于即时通讯类的APP的推送,确保用户收到且只会收到一次。




附录:各编程语言对MQTT客户端/服务器的实现


NameLanguageTypeLast releaseLicense
Adafruit IORuby on RailsNode.jsClient2.0.0?
flespiCBroker?Proprietary License
M2MqttC#Client4.3.0.0Eclipse Public License 1.0
Machine HeadClojureClient1.0.0Creative Commons Attribution 3.0 Unported License
moquetteJavaBroker0.10Apache License 2.0
MosquittoCPythonBroker and client1.4.15Eclipse Public License 1.0, Eclipse Distribution License 1.0 (BSD)
Paho MQTTCC++JavaJavascriptPythonGoClient1.3.0Eclipse Public License 1.0, Eclipse Distribution License 1.0 (BSD)
SharkMQTTCClient1.5Proprietary License
VerneMQErlang/OTPBroker1.4.1Apache License 2.0
wolfMQTTCClient0.14GNU Public License, version 2
MQTTRouteCPythonBroker1.0Proprietary License
HiveMQJavaBroker3.4.0Proprietary License
SwiftMQJavaBroker11.1.0Proprietary License
JoramMQJavaBroker11.1.0Proprietary License

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

收起阅读 »

基于环信MQTT消息云,iOS版MQTT客户端快速实现消息收发

本文介绍iOS版MQTT 客户端,如何连接环信MQTT消息云快速实现消息的自收自发。一、前提条件1.部署iOS开发环境下载安装 Xcode。下载安装cocoapods,本文以cocoapods为例。2.导入项目依赖 在项目的podfile文...
继续阅读 »

本文介绍iOS版MQTT 客户端,如何连接环信MQTT消息云快速实现消息的自收自发。

一、前提条件

1.部署iOS开发环境

下载安装 Xcode

下载安装cocoapods,本文以cocoapods为例。

2.导入项目依赖

 在项目的podfile文件中设置如下:

  source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
pod 'MQTTClient'
pod 'AFNetworking'
pod 'MBProgressHUD'
pod 'Masonry'
pod 'MJRefresh'
pod 'YYModel'
target 'MQTTChat' do
end

二、实现流程

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代码示例如下:

- (void)getTokenWithUsername:(NSString *)username password:(NSString *)password completion:(void (^)(NSString *token))response {

NSString *urlString = getToken_url;
//初始化一个AFHTTPSessionManager
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
//设置请求体数据为json类型
manager.requestSerializer = [AFJSONRequestSerializer serializer];
//设置响应体数据为json类型
manager.responseSerializer = [AFJSONResponseSerializer serializer];
//请求体,参数(NSDictionary 类型)

NSDictionary *parameters = @{@"grant_type":@"password",
@"username":username,
@"password":password
};
__block NSString *token = @"";

[manager POST:urlString parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {

NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:responseObject options:NSJSONWritingPrettyPrinted error:&error];
NSDictionary *jsonDic = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error];
NSLog(@"%s jsonDic:%@",__func__,jsonDic);
token = jsonDic[@"access_token"];

response(token);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"%s error:%@",__func__,error.debugDescription);
response(token);
}];
}

2、初始化

      在cocoapods工程中创建MQTT客户端,客户端初始配置包括创建clientID,topic名称,QoS质量,连接地址等信息。

  NSString *deviceID = [UIDevice currentDevice].identifierForVendor.UUIDString;

self.clientId = [NSString stringWithFormat:@"%@@%@",deviceID,self.appId];

//初始化manager
self.manager = [[MQTTSessionManager alloc] init];
self.manager.delegate = self;

//订阅的主题 格式为 xxx/xxx/xxx 可以为多级话题
self.manager.subscriptions = @{[NSString stringWithFormat:@"%@/IOS", self.rootTopic]:@(self.qos),[NSString stringWithFormat:@"%@/IOS_TestToic", self.rootTopic]:@(1)};

3、连接服务器

    配置连接密码、cleansession标志、心跳间隔、超时时间等信息,调用connect()函数连接至环信MQTT消息云。

//此处从配置文件导入的Host即为MQTT的接入点,该接入点获取方式请参考资源申请章节文档,在控制台上申请MQTT实例,每个实例会分配一个接入点域名
[self.manager connectTo:self.host

port:self.port
tls:self.tls
keepalive:60
clean:true
auth:true
user:userName
pass:token
will:false
willTopic:nil
willMsg:nil
willQos:0
willRetainFlag:FALSE
withClientId:self.clientId];

4、订阅【subscribe】

【订阅主题】

当客户端成功连接环信MQTT消息云后,通过设置subscriptions参数值实现订阅主题与取消订阅主题 。当subscriptions非空时,订阅主题;当subscriptions为空时,取消订阅主题;

/**
订阅主题
格式为 xxx/xxx/xxx 可以为多级话题 @{@"xxx/xxx/xxx...":@(1)}
qos定义{ 0: 最多一次,1:至少一次 2:仅一次}
*/
self.manager.subscriptions = @{[NSString stringWithFormat:@"%@/IOS", self.rootTopic]:@(self.qos),[NSString stringWithFormat:@"%@/IOS_TestToic", self.rootTopic]:@(1)};

【取消订阅】

self.manager.subscriptions = @{};

【接收消息】

    环信MQTT消息云接收订阅消息。

  /*
* MQTTSessionManagerDelegate
*/
- (void)handleMessage:(NSData *)data onTopic:(NSString *)topic retained:(BOOL)retained {
/*
* MQTTClient: process received message
*/

NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[self.receiveMsgs insertObject:[NSString stringWithFormat:@"RecvMsg from Topic: %@ Body: %@", topic, dataString] atIndex:0];
[self.tableView reloadData];
}

5、发布【publish】

   环信MQTT消息云中指定topic发送消息。

- (void)send {
/*
* MQTTClient: send data to broker
*/

[self.manager sendData:[self.messageTextField.text dataUsingEncoding:NSUTF8StringEncoding]
topic:[NSString stringWithFormat:@"%@/%@",
self.rootTopic,
@"IOS"]//此处设置多级子topic
qos:self.qos
retain:FALSE];
}

6、断开连接

  MQTT client向环信MQTT消息云发送断开连接请求。

  /*
* 断开连接
*/
- (void)disConnect {
[self.manager disconnect];
self.manager.subscriptions = @{};
}

7、重新连接

 MQTT client向环信MQTT消息云发送重新连接请求。

/*
* 重新连接
*/
- (void)connect {
[self.manager connectToLast];
}

三、更多信息

  * 完整demo示例,请参见demo下载

  * 目前MQTT客户端支持多种语言,请参见 SDK下载
  * 如果您在使用环信MQTT消息云服务中,有任何疑问和建议,欢迎您联系我们

收起阅读 »

iOS Crash分析中的Signal

下面是一些信号说明SIGHUP本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的...
继续阅读 »

下面是一些信号说明

  • SIGHUP
    本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
    登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录, wget也 能继续下载。
    此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

  • SIGINT
    程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

  • SIGQUIT
    SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

  • SIGILL
    执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

  • SIGTRAP
    由断点指令或其它trap指令产生. 由debugger使用。

  • SIGABRT
    调用abort函数生成的信号。

  • SIGBUS
    非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

  • SIGFPE
    在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

  • SIGKILL
    用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

  • SIGUSR1
    留给用户使用

  • SIGSEGV
    试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

  • SIGUSR2
    留给用户使用

  • SIGPIPE
    管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

  • SIGALRM
    时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

  • SIGTERM
    程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL

  • SIGCHLD
    子进程结束时, 父进程会收到这个信号。
    如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。

  • SIGCONT
    让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符

  • SIGSTOP
    停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

  • SIGTSTP
    停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

  • SIGTTIN
    当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.

  • SIGTTOU
    类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

  • SIGURG
    有”紧急”数据或out-of-band数据到达socket时产生.

  • SIGXCPU
    超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

  • SIGXFSZ
    当进程企图扩大文件以至于超过文件大小资源限制。

  • SIGVTALRM
    虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

  • SIGPROF
    类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

  • SIGWINCH
    窗口大小改变时发出.

  • SIGIO
    文件描述符准备就绪, 可以开始进行输入/输出操作.

  • SIGPWR
    Power failure

  • SIGSYS
    非法的系统调用。

关键点注意

  • 在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP

  • 不能恢复至默认动作的信号有:SIGILL,SIGTRAP

  • 默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ

  • 默认会导致进程退出的信号有:
    SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM

  • 默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU

  • 默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

  • 此外,SIGIOSVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。

作者:Cooci
链接:https://www.jianshu.com/p/3a9dc6bd5e58



收起阅读 »

iOS编译&链接

对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc he...
继续阅读 »

对于平常的应用程序开发,我们很少需要关注编译链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译链接的过程一步完成,通常将这种编译链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc hello.c命令就包含了非常复杂的过程!

正是因为集成开发环境的强大,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。我们看到的是这些问题的现象,但是却很难看清本质,所有这些问题的本质就是软件运行背后的机理及支撑软件运行的各种平台和工具,如果能深入了解这些机制,那么解决这些问题就能够游刃有余,收放自如了。


编译流程分析

现在我们通过一个C语言的经典例子,来具体了解一下这些机制:


#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}
在linux下只需要一个简单的命令(假设源代码文件名为hello.c):

$ gcc hello.c
$ ./a.out
Hello World

其实上述过程可以分解为四步:

  • 预处理(Prepressing)
  • 编译(Compilation)
  • 汇编(Assembly)
  • 链接(Linking)


预编译

首先是源代码文件hello.c和相关的头文件(如stdio.h等)被预编译器cpp预编译成一个.i文件。第一步预编译的过程相当于如下命令(-E 表示只进行预编译):

$ gcc –E hello.c –o hello.i
还可以下面的表达
$ cpp hello.c > hello.i

预编译过程主要处理源代码文件中以”#”开头的预编译指令。比如#include、#define等,主要处理规则如下:

  • 将所有的#define删除,并展开所有的宏定义
  • 处理所有条件预编译指令,比如#if,#ifdef,#elif,#else,#endif
  • 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
  • 删除所有的注释///**/
  • 添加行号和文件名标识,比如#2 “hello.c” 2。
  • 保留所有的#pragma编译器指令

截图个大家看看效果



经过预编译后的文件(.i文件)不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经插入到.i文件中,所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

编译(compliation)

编译过程就是把预处理完的文件进行一系列的:词法分析语法分析语义分析优化后生产相应的汇编代码文件,此过程是整个程序构建的核心部分,也是最复杂的部分之一。其编译过程相当于如下命令:

$ gcc –S hello.i –o hello.s


通过上图我们不难得出,通过命令得到汇编输出文件hello.s.

汇编(assembly)

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。其汇编过程相当于如下命令:

as hello.s –o hello.o

或者

gcc –c hello.s –o hello.o

或者使用gcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件:

gcc –c hello.c –o hello.o


链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?为什么要链接?下面让我们来看看怎么样调用ld才可以产生一个能够正常运行的Hello World程序:

注意默认情况没有gcc / 记得 :
$ brew install gcc

下面在贴出我们的写出的源代码是如何变成目标代码的流程图:

    



主要通过我们的编译器做了以下任务:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化

到这我们就可以得到以下的文件,不知道你是否有和我一起操作,玩得感觉还是不错,继续往下面看


iOS的编译器

iOS现在为了达到更牛逼的速度和优化效果,采用了LLVM

  • 1.LLVM核心库:
    LLVM提供一个独立的链接代码优化器为许多流行CPU(以及一些不太常见的CPU)的代码生成支持。这些库是围绕一个指定良好的代码表示构建的,称为LLVM中间表示(“LLVM IR”)LLVM还可以充当JIT编译器 - 它支持x86 / x86_64和PPC / PPC64程序集生成,并具有针对编译速度的快速代码优化。。

  • 2.LLVM IR 生成器Clang: Clang是一个“LLVM原生”C / C ++ / Objective-C编译器,旨在提供惊人的快速编译(例如,在调试配置中编译Objective-C代码时比GCC快3倍),非常有用的错误和警告消息以及提供构建优秀源代码工具的平台。

  • 3.LLDB项目:
    LLDB项目以LLVMClang提供的库为基础,提供了一个出色的本机调试器。它使用Clang AST表达式解析器LLVM JIT,LLVM反汇编程序等,以便提供“正常工作”的体验。在加载符号时,它也比GDB快速且内存效率更高。

  • 4.libclibc++:
    libc 和libc++ ABI项目提供了C ++标准库的标准符合性和高性能实现,包括对C ++ 11的完全支持。

  • 5.lld项目:
    lld项目旨在成为clang / llvm的内置链接器。目前,clang必须调用系统链接器来生成可执行文件。

LLVM采用三相设计,前端Clang负责解析,验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR,后端LLVM编译把IR通过一系列改进代码的分析和优化过程提供,然后被发送到代码生成器以生成本机机器代码。

编译器前端的任务是进行:

  • 语法分析
  • 语义分析
  • 生成中间代码(intermediate representation )

在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。

iOS程序-详细编译过程

  • 1.写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
  • 2.运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases中可以看到;
  • 3.编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
  • 4.链接文件:将项目中的多个可执行文件合并成一个文件;
  • 5.拷贝资源文件:将项目中的资源文件拷贝到目标包;
  • 6.编译 storyboard 文件:storyboard 文件也是会被编译的;
  • 7.链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
  • 8.编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage
  • 9.运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。
  • 10.生成 .app 包
  • 11.将 Swift 标准库拷贝到包中
  • 12.对包进行签名
  • 13.完成打包

编译过程的确是个比较复杂的过程,还有链接!并不是说难就不需要掌握,我个人建议每一个进阶路上iOS开发人员,都是要了解一下的。不需要你多么牛逼,但是你能在平时的交流讨论,面试中能点出一个两个相应的点,我相信绝对是逼格满满!


作者:Cooci_和谐学习_不急不躁
链接:https://www.jianshu.com/p/b60612c4d9ca





收起阅读 »

RSA概述

RSA概述首先看这个加密算法的命名.很有意思,它其实是三个人的名字.早在1977年由麻省理工学院的三位数学家Rivest、Shamir 和 Adleman一起提出了这个加密算法,并且用他们三个人姓氏开头字母命名.RSA加密算法是一种非对称加密算法,其玩法打破了...
继续阅读 »

RSA概述

首先看这个加密算法的命名.很有意思,它其实是三个人的名字.早在1977年由麻省理工学院的三位数学家Rivest、Shamir 和 Adleman一起提出了这个加密算法,并且用他们三个人姓氏开头字母命名.
RSA加密算法是一种非对称加密算法,其玩法打破了以往所有加密算法的规则.在RSA出现之前,所有的加密方法都是同一种模式:加密解密的规则使用同一种方式.这种长达几个世纪的加密方案有一个致命的缺陷.在传递加密信息时,必须让对方拿到解密的规则才能正常解密.由于加密解密的规则一致,所以保存和传递"密钥",就成了最头疼的问题。
RSA的出现解决了这个问题.我们来看看RSA是怎么玩的.


RSA加密/解密

  • 使用公钥加密的数据,利用私钥进行解密
  • 使用私钥加密的数据,利用公钥进行解密

没错,RSA加密使用了"一对"密钥.分别是公钥私钥,这个公钥和私钥其实就是一组数字!其二进制位长度可以是1024位或者2048位.长度越长其加密强度越大,目前为止公之于众的能破解的最大长度为768位密钥,只要高于768位,相对就比较安全.所以目前为止,这种加密算法一直被广泛使用.

RSA的弊端

由于RSA算法的原理都是大数计算,使得RSA最快的情况也比对称加密算法慢上好几倍。速度一直是RSA的缺陷,一般来说RSA只用于小数据的加密.RSA的速度是对应同样安全级别的对称加密算法的1/1000左右。

RSA终端命令演示

由于Mac系统内置OpenSSL(开源加密库),所以我们可以直接在终端上使用命令来玩RSA.
OpenSSL中RSA算法常用指令主要有三个,其他指令此处不介绍。



命令
含义
genrsa生成并输入一个RSA私钥
rsautl使用RSA密钥进行加密、解密、签名和验证等运算
rsa处理RSA密钥的格式转换等问题

生成RSA私钥,密钥长度为1024bit

hank$ openssl genrsa -out private.pem 1024
Generating RSA private key, 1024 bit long modulus
..++++++
..........................................++++++
e is 65537 (0x10001)

从私钥中提取公钥

hank$ openssl rsa -in private.pem -pubout -out public.pem
writing RSA key

显得非常高大上对吧!那么它里面是什么,我们可以利用终端进行查看.

//查看私钥文件
hank$ cat private.pem
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDbGfA0XdkIpK5h2O9mg5o35pitxwiHDnlpBTCTUH+pkGMdDe6d
9nVQDr61QUEMWAgbnb/irTXh5VigGhHDbG/4kmVy1BgSfLxUx50jmm7jnvnS4Hrb
65g920x26gaBW+I9n9cHF/QShrqaNXP9DDeqhqNzdmrkaaAQQkQ9liN6awIDAQAB
AoGAU0gdvNn7WES4oCrEfPQDF8KIQG3KOQPwdFHrr+NGU161veKA0/xNhTvFk8IV
BqsjkdO5j2EFfTMfJ+Qg4maCfIZN+xknosXRUF3vz5CUz/rXwBupOlOiWFJbB6cV
/Jee045DjiHjciip/ZVd8A2xnUEg4pIFUujAFPH+22t5TvkCQQD73bRqCQF9sWIA
tBeNR10Mygx5wrwKvjgCvaawsgx82kuAb3CWR0G81GfU+lK0YaHdmcFHsAHlDncM
OtY6IPnNAkEA3rKP6+/jUoylsJPWuN9LyuKjtAlsNtbWaYvs8iCNhLyV9hoWjvow
AAZB1uWy5aLDtQI3v48beExwsJEFAlQtFwJASTkKU21s1or0T/oLgtJFdgtjlx6L
JqBojjtus53/zWh1XNCJLddngCtMSHnCA5kCwvcJXvsHgf0zlQWh9GJT3QJAY0+q
EwN1kpiaQzaKqQMbX6zWaDFTitkf4Q2/avLNaYZYMdnMeZJk2X3w2o6wyutc71m/
1rNRAsLD9lmVrEYxnQJAEAHb0lsRgWe/sXX2attg4NbDsEExqDZ+7GGsyvqZn1Xg
S/UPdt6rVkVQ3N7ZEPKV6SxwN9LySI4lVWmFWhCn6w==
-----END RSA PRIVATE KEY-----
//查看公钥文件
hank$ cat public.pem
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDbGfA0XdkIpK5h2O9mg5o35pit
xwiHDnlpBTCTUH+pkGMdDe6d9nVQDr61QUEMWAgbnb/irTXh5VigGhHDbG/4kmVy
1BgSfLxUx50jmm7jnvnS4Hrb65g920x26gaBW+I9n9cHF/QShrqaNXP9DDeqhqNz
dmrkaaAQQkQ9liN6awIDAQAB
-----END PUBLIC KEY-----
其实就是一个文本文件,并且一看就知道是base64编码.那么公钥相比私钥要简单很多.我们可以通过命令,将私钥转换成为明文看看.

//转化为明文信息
hank$ openssl rsa -in private.pem -text -out private.txt
writing RSA key
//查看文本信息
hank$ cat private.txt
Private-Key: (1024 bit)
modulus:
00:db:19:f0:34:5d:d9:08:a4:ae:61:d8:ef:66:83:
9a:37:e6:98:ad:c7:08:87:0e:79:69:05:30:93:50:
7f:a9:90:63:1d:0d:ee:9d:f6:75:50:0e:be:b5:41:
41:0c:58:08:1b:9d:bf:e2:ad:35:e1:e5:58:a0:1a:
11:c3:6c:6f:f8:92:65:72:d4:18:12:7c:bc:54:c7:
9d:23:9a:6e:e3:9e:f9:d2:e0:7a:db:eb:98:3d:db:
4c:76:ea:06:81:5b:e2:3d:9f:d7:07:17:f4:12:86:
ba:9a:35:73:fd:0c:37:aa:86:a3:73:76:6a:e4:69:
a0:10:42:44:3d:96:23:7a:6b
publicExponent: 65537 (0x10001)
privateExponent:
53:48:1d:bc:d9:fb:58:44:b8:a0:2a:c4:7c:f4:03:
17:c2:88:40:6d:ca:39:03:f0:74:51:eb:af:e3:46:
53:5e:b5:bd:e2:80:d3:fc:4d:85:3b:c5:93:c2:15:
06:ab:23:91:d3:b9:8f:61:05:7d:33:1f:27:e4:20:
e2:66:82:7c:86:4d:fb:19:27:a2:c5:d1:50:5d:ef:
cf:90:94:cf:fa:d7:c0:1b:a9:3a:53:a2:58:52:5b:
07:a7:15:fc:97:9e:d3:8e:43:8e:21:e3:72:28:a9:
fd:95:5d:f0:0d:b1:9d:41:20:e2:92:05:52:e8:c0:
14:f1:fe:db:6b:79:4e:f9
prime1:
00:fb:dd:b4:6a:09:01:7d:b1:62:00:b4:17:8d:47:
5d:0c:ca:0c:79:c2:bc:0a:be:38:02:bd:a6:b0:b2:
0c:7c:da:4b:80:6f:70:96:47:41:bc:d4:67:d4:fa:
52:b4:61:a1:dd:99:c1:47:b0:01:e5:0e:77:0c:3a:
d6:3a:20:f9:cd
prime2:
00:de:b2:8f:eb:ef:e3:52:8c:a5:b0:93:d6:b8:df:
4b:ca:e2:a3:b4:09:6c:36:d6:d6:69:8b:ec:f2:20:
8d:84:bc:95:f6:1a:16:8e:fa:30:00:06:41:d6:e5:
b2:e5:a2:c3:b5:02:37:bf:8f:1b:78:4c:70:b0:91:
05:02:54:2d:17
exponent1:
49:39:0a:53:6d:6c:d6:8a:f4:4f:fa:0b:82:d2:45:
76:0b:63:97:1e:8b:26:a0:68:8e:3b:6e:b3:9d:ff:
cd:68:75:5c:d0:89:2d:d7:67:80:2b:4c:48:79:c2:
03:99:02:c2:f7:09:5e:fb:07:81:fd:33:95:05:a1:
f4:62:53:dd
exponent2:
63:4f:aa:13:03:75:92:98:9a:43:36:8a:a9:03:1b:
5f:ac:d6:68:31:53:8a:d9:1f:e1:0d:bf:6a:f2:cd:
69:86:58:31:d9:cc:79:92:64:d9:7d:f0:da:8e:b0:
ca:eb:5c:ef:59:bf:d6:b3:51:02:c2:c3:f6:59:95:
ac:46:31:9d
coefficient:
10:01:db:d2:5b:11:81:67:bf:b1:75:f6:6a:db:60:
e0:d6:c3:b0:41:31:a8:36:7e:ec:61:ac:ca:fa:99:
9f:55:e0:4b:f5:0f:76:de:ab:56:45:50:dc:de:d9:
10:f2:95:e9:2c:70:37:d2:f2:48:8e:25:55:69:85:
5a:10:a7:eb
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDbGfA0XdkIpK5h2O9mg5o35pitxwiHDnlpBTCTUH+pkGMdDe6d
9nVQDr61QUEMWAgbnb/irTXh5VigGhHDbG/4kmVy1BgSfLxUx50jmm7jnvnS4Hrb
65g920x26gaBW+I9n9cHF/QShrqaNXP9DDeqhqNzdmrkaaAQQkQ9liN6awIDAQAB
AoGAU0gdvNn7WES4oCrEfPQDF8KIQG3KOQPwdFHrr+NGU161veKA0/xNhTvFk8IV
BqsjkdO5j2EFfTMfJ+Qg4maCfIZN+xknosXRUF3vz5CUz/rXwBupOlOiWFJbB6cV
/Jee045DjiHjciip/ZVd8A2xnUEg4pIFUujAFPH+22t5TvkCQQD73bRqCQF9sWIA
tBeNR10Mygx5wrwKvjgCvaawsgx82kuAb3CWR0G81GfU+lK0YaHdmcFHsAHlDncM
OtY6IPnNAkEA3rKP6+/jUoylsJPWuN9LyuKjtAlsNtbWaYvs8iCNhLyV9hoWjvow
AAZB1uWy5aLDtQI3v48beExwsJEFAlQtFwJASTkKU21s1or0T/oLgtJFdgtjlx6L
JqBojjtus53/zWh1XNCJLddngCtMSHnCA5kCwvcJXvsHgf0zlQWh9GJT3QJAY0+q
EwN1kpiaQzaKqQMbX6zWaDFTitkf4Q2/avLNaYZYMdnMeZJk2X3w2o6wyutc71m/
1rNRAsLD9lmVrEYxnQJAEAHb0lsRgWe/sXX2attg4NbDsEExqDZ+7GGsyvqZn1Xg
S/UPdt6rVkVQ3N7ZEPKV6SxwN9LySI4lVWmFWhCn6w==
-----END RSA PRIVATE KEY-----

通过公钥加密数据,私钥解密数据

//生成明文文件
hank$ vi message.txt
//查看文件内容
hank$ cat message.txt
密码:123456
//通过公钥进行加密
hank$ openssl rsautl -encrypt -in message.txt -inkey public.pem -pubin -out enc.txt
//通过私钥进行解密
hank$ openssl rsautl -decrypt -in enc.txt -inkey private.pem -out dec.txt

通过私钥加密数据,公钥解密数据

//通过私钥进行加密
hank$ openssl rsautl -sign -in message.txt -inkey private.pem -out enc.txt
//通过公钥进行解密
hank$ openssl rsautl -verify -in enc.txt -inkey public.pem -pubin -out dec.txt

小结

那么看到这些之后,对RSA应该有了一定的了解.由于RSA加密运行效率非常低!并不是所有数据加密都会使用它.那么它的主战场在于加密一些小的数据,比如对称加密算法的密钥.又或者数字签名.



作者:Hank
链接:https://www.jianshu.com/p/6280aa136292







收起阅读 »

RunLoop(二):实际应用

前不久我们我们对RunLoop的底层有了简单的了解,那我们现在就要把我们学到的这些东西,实际应用到我们的项目中。Timer定时器问题我们在vc中创建一个定时器,然后在view上面添加一个滚动视图,比如说scrollView,可以发现在scrollView滚动的...
继续阅读 »

前不久我们我们对RunLoop的底层有了简单的了解,那我们现在就要把我们学到的这些东西,实际应用到我们的项目中。

Timer定时器问题

我们在vc中创建一个定时器,然后在view上面添加一个滚动视图,比如说scrollView,可以发现在scrollView滚动的时候,timer定时器会卡住,停止滚动之后才重新生效。

这个问题比较简单,也是我们经常遇到的。

因为定时器默认是添加在了RunLoop的NSDefaultRunLoopMode模式下,scrollView在滚动的时候会进入UITrackingRunLoopMode,RunLoop在同一时间只能处理一种mode,所以在滚动的时候,自然定时器就没法处理,卡住。

解决方法就是我们创建了timer之后,把他add到RunLoop的NSRunLoopCommonModes,NSRunLoopCommonModes其实并不是一种真实的模式,他只是一个标志,意味着timer在标记为common的模式下都能使用 (标记为common 也就是_commonModes数组)。

这个地方多说一句,这个标记为common是啥意思。我们得看回RunLoop结构体的源码

struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

可以看到里面有一个set类型的变量,CFMutableSetRef _commonModes;,被放到这个set中的mode就等于是被标记为了common。NSDefaultRunLoopMode和UITrackingRunLoopMode都在里面。

下面是我们创建timer的正确姿势 ~

//我们平时可能都是用scheduledTimerWithTimeInterval这个方法创建,这个会默认把timer添加到runloop的defalut模式下,所以我们使用timerWithTimeInterval创建
NSTimer * timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d",++ count);
}];

//NSRunLoopCommonModes 并不是一个真的模式 他只是一个标记,意味着timer在标记为common的模式下都能使用 (标记为common 也就是_commonModes数组)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

线程保活

线程保活并不是所有的项目都用的到,他适应于那种一直有任务需要处理的场景,而且注意,一定要是串行的任务。这种情况下保活一条线程,就可以免去线程创建和销毁的开销,提高性能。

具体怎么保活线程,我下面先直接把我的代码贴出来,然后针对一些点在做一系列的说明。(模拟的项目场景是进入到一个vc中,开一条线程,然后用这条线程来执行任务,当然vc销毁时,线程也要销毁。)

下面是全部代码,大家可以先跳过代码看下面的一些解析。

#import "SecondViewController.h"

@interface MyThread : NSThread
@end
@implementation MyThread
- (void)dealloc {
NSLog(@"%s",__func__);
}
@end

@interface SecondViewController ()
@property (nonatomic, strong) MyThread * thread;
@property (nonatomic, assign, getter=isStopped) BOOL stopped;
@end

@implementation SecondViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];

self.stopped = NO;

UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(40, 100, 100, 40);
btn.backgroundColor = [UIColor blackColor];
[btn setTitle:@"停止" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(stopThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];

__weak typeof(self) weakSelf = self;
// 初始化thread
self.thread = [[MyThread alloc] initWithBlock:^{
NSLog(@"--begin--");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];

while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

NSLog(@"--end--");
}];
[self.thread start];

}

- (void)stopThread {
if (!self.thread) return;

// waitUntilDone YES
[self performSelector:@selector(__stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

// 执行这个方法必须要在我们自己创建的这个线程中
- (void)__stopThread {
// 标识
self.stopped = YES;
// 停止runloop
CFRunLoopStop(CFRunLoopGetCurrent());
//
self.thread = nil;
}


#pragma mark - 添加touch事件 (每点击一次 让线程处理一次事件)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (!self.thread) return;

[self performSelector:@selector(threadDoSomething) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)threadDoSomething {
NSLog(@"work--%@",[NSThread currentThread]);
}


#pragma mark - dealloc
- (void)dealloc {
NSLog(@"%s",__func__);
[self stopThread];
}

@end

最顶部新建了一个继承自NSThread的MyThread类,目的就是为了重写-dealloc方法,在内部有打印内容,方便我调试线程是否被销毁。在我们真是的项目中,可以不需要这部分。

初始化线程,开启RunLoop

__weak typeof(self) weakSelf = self;
// 初始化thread
self.thread = [[MyThread alloc] initWithBlock:^{
NSLog(@"--begin--");
//往runloop里面添加source/timer/observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];

while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

NSLog(@"--end--");
}];
[self.thread start];

这部分是初始化我们的线程,线程的初始化我们一般用的多的是self.thread = [[MyThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];这样的方法,我是觉得这样把self传进线程内部,可能造成一些循环引用问题,最后影响vc和thread的销毁,所以我是用了block的形式。

initWithBlock的意思也就是线程初始化完毕会执行block内的代码。一个子线程默认是没有RunLoop的,RunLoop会在第一次获取的时候创建,所以我们先[NSRunLoop currentRunLoop]获取RunLoop,也就是创建了我们当前线程的RunLoop。

在了解RunLoop底层的时候我们了解到,如果一个RunLoop没有timer、observer、source,就会退出。我们新创建的RunLoop这些都是没有的,如果我们不手动的添加,那我们的RunLoop一跑起来就这就会退出的。所以就等于说我们必须手动给RunLoop添加点事情做。

在代码中我们使用了addPort:forMode这个方法,向当前RunLoop添加一个端口让RunLoop监听。RunLoop有工作做了,自然就不会退出的。

我们在开启线程的时候,用了一个while循环,通过一个属性stopped来控制是否跳出循环,然后循环内部使用了- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;这个方法开启RunLoop。有人有可能会问了,这里的开启RunLoop为什么不直接使用- (void)run;这个方法。这里我稍微解释一下:

查阅一下苹果的文档可以了解到,这个run方法,内部其实也是循环的调用了runMode这个方法的,但是这个循环是永远不会停止的,也就是说我们使用run方法开启的RunLoop是永远都不会停下来的,我们调用了stop之后,也只会停止当前的这一次循环,他还是会继续run起来的。所以文档中也提到,如果我们要创建一个可以停下来的RunLoop,用runMode这个方法。所以我们用这个while循环模拟run的运行原理,但是呢,我们通过stopped这个属性可以控制循环的停止。

while里面的条件weakSelf && !weakSelf.isStopped为什么不仅仅使用stopped判断,而是还要判断weakSelf是否有值?我们下面会提到的。

两个stopThread方法

- (void)stopThread {
if (!self.thread) return;

// waitUntilDone YES
[self performSelector:@selector(__stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

// 执行这个方法必须要在我们自己创建的这个线程中
- (void)__stopThread {
// 标识置为YES,跳出while循环
self.stopped = YES;
// 停止runloop的方法
CFRunLoopStop(CFRunLoopGetCurrent());
// RunLoop退出之后,把线程置空释放,因为RunLoop退出之后就没法重新开启了
self.thread = nil;
}

stopThread是给我们的停止button调用的,但是实际的停止RunLoop操作在__stopThread里面。在stopThread中调用__stopThread一定要使用performSelector:onThread:这一类的方法,这样就可以保证在我们指定的线程中执行这个方法。如果我们直接调用__stopThread,就说明是在主线程调用的,那就代表我们把主线程的RunLoop停掉了,那我们的程序就完了。

touch模拟事件处理

我们在touchBegin方法中,让我们self.thread执行-threadDoSomething这个方法,代表每点击一次,我们的线程就要处理一次-threadDoSomething中的打印事件。做这个操作是为了检测看我们每次工作的线程是不是都是我们最开始创建的这一个线程,没有重新开新线程。

其他细节

那我们仔细观察的话会发现一个问题,-threadDoSomething和stopThread这两个方法中都是用下面这个方法来处理线程间通信

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait

但是两次调用传入的wait参数是不一样的。我们要先知道这个waitUntilDone:(BOOL)wait代表什么意思。

如果wait传的是YES,就代表我们在主线程用self调用这个performSelector的时候,主线程会等待我们的self.thread这个线程执行他需要执行的方法,等着self.thread执行完方法之后,主线程再继续往下走。那如果是NO,肯定就是主线程不会等了,主线程继续往下走,然后我们的self.thread去调用自己该调用的方法。

那为什么在stop方法中是用的YES?

有这么一个情形,如果我们push进这个vc,线程初始化,然后RunLoop开启,但是我们不想通过点击停止button来停止,当我们点击导航的back的时候,我也需要销毁线程。

所以我们在vc的-dealloc方法中也调用了stopThread方法。那如果stopThread中使用
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait
的时候wait不用YES,而是NO,会出现什么情况,那肯定是crash了。

如果wait是NO,代表我们的主线程不会等待self.thread执行__stopThread方法。

#pragma mark - dealloc
- (void)dealloc {
NSLog(@"%s",__func__);
[self stopThread];
}

但是dealloc中主线程调用完stopThread,之后整个dealloc方法就结束了,也就是我们的控制器已经销毁了。但是呢这个时候self.thread还在执行__stopThread方法呢。__stopThread中还要self变量,但是他其实已经销毁了,所以这个地方就会crash了。所以在stopThread中的wait一定要设置为YES。

在当时写代码的时候,这样确实处理了crash的问题,但是我直接返回值后,RunLoop并没有结束,线程没有销毁。这就要讲到上面说的while判断条件是weakSelf && !weakSelf.isStopped的原因了。vc执行了dealloc之后,self被置为nil了,weakSelf.isStopped也是nil,取非之后条件又成立了,while循环还要继续的走,RunLoop又run起来了。所以这里我们加上weakSelf这个判断,也就是self必须不为空。

总结

上面就是我实现的线程保活这一功能的代码和细节分析,当然我们在实际的项目中可能有多个位置需要线程保活这一功能,所以我们应该把这一部分做一下简单的封装,来方便我们在不同的地方调用。大家有兴趣的可以自己封装一下,我在写RunLoop相关的代码时,大多用的是OC层的代码,有兴趣的小伙伴可以尝试一下C语言的API。

RunLoop的应用当前不止这么一点,还可以监控应用卡顿,做性能优化,这些以后研究明白了再继续更博客吧,一起加油。

相关的功能代码和封装已经放到github上面了
https://github.com/Sunxb/RunLoopDemo


链接:https://www.jianshu.com/p/9e0177d40aab

收起阅读 »

细说浏览器输入URL后发生了什么

细说浏览器输入URL后发生了什么总体概览大体上,可以分为六步,当然每一步都可以详细都展开来说,这里先放一张总览图:DNS域名解析在网络世界,你肯定记得住网站的名称,但是很难记住网站的 IP 地址,因而也需要一个地址簿,就是 DNS 服务器。DNS 服务器是高可...
继续阅读 »

细说浏览器输入URL后发生了什么

总体概览

大体上,可以分为六步,当然每一步都可以详细都展开来说,这里先放一张总览图:


DNS域名解析

在网络世界,你肯定记得住网站的名称,但是很难记住网站的 IP 地址,因而也需要一个地址簿,就是 DNS 服务器。DNS 服务器是高可用、高并发和分布式的,它是树状结构,如图:


  • 根 DNS 服务器 :返回顶级域 DNS 服务器的 IP 地址

  • 顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址

  • 权威 DNS 服务器 :返回相应主机的 IP 地址


DNS的域名查找,在客户端和浏览器,本地DNS之间的查询方式是递归查询;在本地DNS服务器与根域及其子域之间的查询方式是迭代查询;


递归过程:

GitHub


在客户端输入 URL 后,会有一个递归查找的过程,从浏览器缓存中查找->本地的hosts文件查找->找本地DNS解析器缓存查找->本地DNS服务器查找,这个过程中任何一步找到了都会结束查找流程。


如果本地DNS服务器无法查询到,则根据本地DNS服务器设置的转发器进行查询。若未用转发模式,则迭代查找过程如下图:

GitHub


结合起来的过程,可以用一个图表示:

GitHub

在查找过程中,有以下优化点:



  • DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。

  • 在域名和 IP 的映射过程中,给了应用基于域名做负载均衡的机会,可以是简单的负载均衡,也可以根据地址和运营商做全局的负载均衡。


建立TCP连接


首先,判断是不是https的,如果是,则HTTPS其实是HTTP + SSL / TLS 两部分组成,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据。


进行三次握手,建立TCP连接。




  1. 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;




  2. 第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;




  3. 第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。




SSL握手过程



  1. 第一阶段 建立安全能力 包括协议版本 会话Id 密码构件 压缩方法和初始随机数

  2. 第二阶段 服务器发送证书 密钥交换数据和证书请求,最后发送请求-相应阶段的结束信号

  3. 第三阶段 如果有证书请求客户端发送此证书 之后客户端发送密钥交换数据 也可以发送证书验证消息

  4. 第四阶段 变更密码构件和结束握手协议


完成了之后,客户端和服务器端就可以开始传送数据。更多 HTTPS 的资料可以看这里:



备注


ACK:此标志表示应答域有效,就是说前面所说的TCP应答号将会包含在TCP数据包中;有两个取值:0和1,为1的时候表示应答域有效,反之为0。TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1。


SYN(SYNchronization):在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1. 因此, SYN置1就表示这是一个连接请求或连接接受报文。


FIN(finis)即完,终结的意思, 用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。


发送HTTP请求,服务器处理请求,返回响应结果


TCP连接建立后,浏览器就可以利用HTTP/HTTPS协议向服务器发送请求了。服务器接受到请求,就解析请求头,如果头部有缓存相关信息如if-none-match与if-modified-since,则验证缓存是否有效,若有效则返回状态码为304,若无效则重新返回资源,状态码为200.


这里有发生的一个过程是HTTP缓存,是一个常考的考点,大致过程如图:

GitHub

其过程,比较多内容,可以参考我的这篇文章《浏览器相关原理(面试题)详细总结一》,这里我就不详细说了~


关闭TCP连接




  1. 第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;




  2. 第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我"同意"你的关闭请求;




  3. 第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;




  4. 第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。




浏览器渲染


按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、栅格化和显示。如图:

GitHub



  1. 渲染进程将 HTML 内容转换为能够读懂DOM 树结构。

  2. 渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。

  3. 创建布局树,并计算元素的布局信息。

  4. 对布局树进行分层,并生成分层树。

  5. 为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分图块,并栅格化将图块转换成位图。

  6. 合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上。


构建 DOM 树


浏览器从网络或硬盘中获得HTML字节数据后会经过一个流程将字节解析为DOM树,先将HTML的原始字节数据转换为文件指定编码的字符,然后浏览器会根据HTML规范来将字符串转换成各种令牌标签,如html、body等。最终解析成一个树状的对象模型,就是dom树。

GitHub


具体步骤:



  1. 转码(Bytes -> Characters)—— 读取接收到的 HTML 二进制数据,按指定编码格式将字节转换为 HTML 字符串

  2. Tokens 化(Characters -> Tokens)—— 解析 HTML,将 HTML 字符串转换为结构清晰的 Tokens,每个 Token 都有特殊的含义同时有自己的一套规则

  3. 构建 Nodes(Tokens -> Nodes)—— 每个 Node 都添加特定的属性(或属性访问器),通过指针能够确定 Node 的父、子、兄弟关系和所属 treeScope(例如:iframe 的 treeScope 与外层页面的 treeScope 不同)

  4. 构建 DOM 树(Nodes -> DOM Tree)—— 最重要的工作是建立起每个结点的父子兄弟关系


样式计算


渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。


CSS 样式来源主要有 3 种,分别是通过 link 引用的外部 CSS 文件、style标签内的 CSS、元素的 style 属性内嵌的 CSS。,其样式计算过程主要为:

GitHub

可以看到上面的 CSS 文本中有很多属性值,如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。处理完成后再处理样式的继承和层叠,有些文章将这个过程称为CSSOM的构建过程。


页面布局


布局过程,即排除 script、meta 等功能化、非视觉节点,排除 display: none 的节点,计算元素的位置信息,确定元素的位置,构建一棵只包含可见元素布局树。如图:

GitHub

其中,这个过程需要注意的是回流和重绘,关于回流和重绘,详细的可以看我另一篇文章《浏览器相关原理(面试题)详细总结二》,这里就不说了~


生成分层树


页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree),如图:

GitHub

如果你熟悉 PS,相信你会很容易理解图层的概念,正是这些图层叠加在一起构成了最终的页面图像。在浏览器中,你可以打开 Chrome 的"开发者工具",选择"Layers"标签。渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面。


并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。那么需要满足什么条件,渲染引擎才会为特定的节点创建新的层呢?详细的可以看我另一篇文章《浏览器相关原理(面试题)详细总结二》,这里就不说了~


栅格化


合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。如图:


GitHub


通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。


显示


最后,合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上,渲染过程完成。




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

浏览器工作原理&前端安全

网络安全 三原则 在传输中,不允许明文传输用户隐私数据; 在本地,不允许明文保存用户隐私数据; 在服务器,不允许明文保存用户隐私数据; http是明文传输,WiFi、路由器、运营商、机房等多个物理设备节点,如果在这中间任意一个节点被监听,传输的内容就会完全...
继续阅读 »

网络安全


三原则



  1. 在传输中,不允许明文传输用户隐私数据;

  2. 在本地,不允许明文保存用户隐私数据;

  3. 在服务器,不允许明文保存用户隐私数据;


http是明文传输,WiFi、路由器、运营商、机房等多个物理设备节点,如果在这中间任意一个节点被监听,传输的内容就会完全暴露,,这一攻击手法叫做MITM(Man In The Middle)中间人攻击。
在网络来说,我们知道不论 POST 请求和 GET 请求都会被抓包,在没有使用 HTTPS 的情况下,抓包我们是防不住的,如果明文传输用户隐私,那后果就不说了。


很多用户密码是通用的,一旦被不法分子窃取,去其他网站撞库,造成损失。
上文说到http传输因为有三大风险



  • 窃听风险(eavesdropping):第三方可以获知通信内容。

  • 篡改风险(tampering):第三方可以修改通信内容。

  • 冒充风险(pretending):第三方可以冒充他人身份参与通信。


所以提到了https
https 可以认为是 http + TLS TLS 是传输层加密协议,它的前身是 SSL 协议,如果没有特别说明,SSL 和 TLS 说的都是同一个协议。


加密传输(避免明文传输)


1. 对称加密

加解密使用同一个密钥
客户端和服务端进行通信,采用对称加密,如果只使用一个秘钥,很容易破解;如果每次用不同的秘钥,海量秘钥的管理和传输成本又会比较高。


2.非对称加密

需要两个密钥来进行加密和解密,这两个秘钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)
非对称加密的模式则是:




  • 乙方生成两把密钥(公钥和私钥)。公钥是公开的,任何人都可以获得,私钥则是保密的




  • 甲方获取乙方的公钥,然后用它对信息加密




  • 乙方得到加密后的信息,用私钥解密。


    但当服务端要返回数据,如果用公钥加密,那么客户端并没有私钥用来解密,而如果用私钥加密,客户端虽然有公钥可以解密,但这个公钥之前就在互联网上传输过,很有可能已经有人拿到,并不安全,所以这一过程只用非对称加密是不能满足的。
    (严格来讲,私钥并不能用来加密,只能用作签名使用,这是由于密码学中生成公钥私钥时对不同变量的数学要求是不同的,因此公钥私钥抵抗攻击的能力也不同)
    所以为了满足即使非对称




image.png


https


HTTPS 的出发点是解决HTTP明文传输时信息被篡改和监听的问题。




  • 为了兼顾性能和安全性,使用了非对称加密+对称加密的方案。




  • 为了保证公钥传输中不被篡改,又使用了非对称加密的数字签名功能,借助CA机构和系统根证书的机制保证了HTTPS证书的公信力。


    只传递证书、明文信息、加签加密后的明文信息,注意不传递CA公钥(防止中间人攻击),客户端浏览器可以通过系统根证书拿到CA公钥。(系统或浏览器中内置的CA机构的证书和公钥成为了至关重要的环节)




加密存储
千万不要用明文存储密码
如果用明文存储密码(不管是存在数据库还是日志中),一旦数据泄露,所有用户的密码就毫无保留地暴露在黑客的面前,开头提到的风险就可能发生,那我们费半天劲加密传输密码也失去了意义。


总结
如果我们想要尽可能保证用户的信息安全,我们需要做以下的工作



  • 使用https请求

  • 利用RSA加密密码并传输数据

  • 用BCrypt或者PBKDF2单向加密,并存储


强制使用HTTPS


一些网站购买了SSL证书并将其配置到Web服务器上,以为这就算完事儿了。但这只是表明你启用了HTTPS选项,而用户很可能不会注意到。为确保每个用户都从HTTPS中受益,你应该将所有传入的HTTP请求重定向至HTTPS。这意味着任何一个访问你的网站的用户都将自动切换到HTTPS,从那以后他们的信息传输就安全了。


配合cookie的secure参数,禁止cookie在最初的http请求中被带出去(中间人拦截)。


TCP三次握手四次挥手


Tcp是传输控制协议(Transmission Control Protocol)是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议


第一次握手:请求连接client->SYN=1, 随机seq=x(数据包首字节序列号)
第二次握手:同意应答,SYN和ACK都置为1,ack=x+1,随机seq=y,返回确认连接
第三次握手:client检查ack是否为x+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=y+1;——>Server,Server检查ack是否为y+1,ACK是否为1,正确则连接成功!


认证授权+浏览器存储


什么是认证(Authentication)

验证当前用户的身份,证明“你是你自己”
互联网中的认证:



  • 用户名密码登录

  • 邮箱发送登录链接

  • 手机号接收验证码


什么是授权(Authorization)

用户授予第三方应用访问该用户某些资源的权限
安装手机应用时(是否允许访问相册、地理位置等权限)
登录微信小程序(是否允许获取昵称、头像、地区、性别等个人信息)



  • 实现授权的方式有:cookie、session、token、OAuth


什么是凭证(Credentials)

实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身份
登录成功后,服务器给用户使用的浏览器颁发一个令牌,表明身份,每次请求时带上。


什么是 Cookie


  • HTTP 是无状态的协议,每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。

  • cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。

  • cookie 是不可跨域的:每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain


特点:
Cookie 的大小受限,一般为 4 KB;
同一个域名下存放 Cookie 的个数是有限制的,不同浏览器的个数不一样,一般为 20 个;
Cookie 支持设置过期时间,当过期时自动销毁;(max-age单位秒,如果是负数,为临时cookie关闭浏览器失效;默认是-1)
每次发起同域下的 HTTP 请求时,都会携带当前域名下的 Cookie;
支持设置为 HttpOnly,防止 Cookie 被客户端的 JavaScript 访问


什么是 Session


  • session 是另一种记录服务器和客户端会话状态的机制

  • session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中


SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。


什么是localStorage

特点



  • 大小限制为 5MB ~10MB;

  • 在同源的所有标签页和窗口之间共享数据;

  • 数据仅保存在客户端,不与服务器进行通信;

  • 数据持久存在且不会过期,重启浏览器后仍然存在;

  • 对数据的操作是同步的。


什么是sessionStorage


  • sessionStorage 的数据只存在于当前浏览器的标签页;

  • 数据在页面刷新后依然存在,但在关闭浏览器标签页之后数据就会被清除;

  • 与 localStorage 拥有统一的 API 接口;

  • 对数据的操作是同步的。


什么是 Token(令牌)


  • 访问资源接口(API)时所需要的资源凭证

  • 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)


特点:



  • 服务端无状态化、可扩展性好

  • 支持移动端设备

  • 安全

  • 支持跨程序调用


什么是 JWT


  • JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。(不使用cookie)


方式:通过Authorization;通过url;跨域的时候,可以把 JWT 放在 POST 请求的数据体里


和session、token的区别是JWT已经包含用户信息,所以不用再去数据库里查询了,而且


什么是 XSS

Cross-Site Scripting(跨站脚本攻击),是一种代码注入攻击



  • 存储性(任何可输入存入数据库的地方,注入脚本,服务端渲染时将脚本拼接html中返回给浏览器)

  • 反射性(脚本写入url,如路由传参,诱导用户点击,服务端渲染时将脚本拼接html中返回给浏览器)

  • DOM性(脚本写入url,前端 JavaScript 取出 URL 中的恶意代码并执行)


防范:cookie设置readOnly禁止js脚本访问cookie
前端服务端对输入框设置格式检查
转义 HTML(存储、反射)
改成纯前端渲染(存储、反射)
使用react就在前端 render 阶段避免 innerHTML、outerHTML 的 XSS 隐患用.textContent、.setAttribute()。


什么是 CSRF
跨站请求伪造(英语:Cross-site request forgery)
用户已经登录了安全网站A,诱导用户访问网站B,B利用A获取的凭证去访问A,绕过用户验证



  • 1.登录受信任网站A,并在本地生成Cookie。

  • 2.在不登出A的情况下,访问危险网站B。


防范:同源策略(origin referrer) token samesite


Base64编码由来


因为有些网络传送渠道并不支持所有的字节,例如传统的邮件只支持可见字符的传送,像ASCII码的控制字符就不能通过邮件传送。Base64就是一种基于64个可打印字符来表示二进制数据的表示方法。


ASCII码
在计算机中,所有的数据在存储和运算时都要使用二进制数表示,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示,用来统一规定上述常用符号用哪些二进制数来表示


unicode、utf-8、ASCII、base64、哈希md5
ASCII美国信息互换标准代码,用一个字节存储128个字符(其中包括33个控制字符(具有某些特殊功能但是无法显示的字符)
产生原因:
在计算机中,所有的数据在存储和运算时都要使用二进制数表示(因为计算机用高电平和低电平分别表示1和0),例如,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示,而具体用哪些二进制数字表示哪个符号,当然每个人都可以约定自己的一套(这就叫编码),而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,于是美国有关的标准化组织就出台了ASCII编码,统一规定了上述常用符号用哪些二进制数来表示 [2]  。


Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。.Base64编码是从二进制到字符的过程


浏览器工作原理


异步编程


与同步相对的异步则可以理解为在异步操作完成后所要做的任务,它们通常以回调函数或者 Promise 的形式被放入事件队列,再由事件循环 ( Event Loop ) 机制在每次轮询时检查异步操作是否完成,若完成则按事件队列里面的执行规则来依次执行相应的任务。


javascript的运行机制(单线程、任务队列、EventLoop、微任务、宏任务)


单线程特点


单线程可以避免多线程操作带来的复杂的同步问题。


任务队列(JavaScript的运行机制)


  1. 所有同步任务都在主线程上执行,形成一个执行栈。

  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。


Event Loop

每次 Tick 会查看任务队列中是否有需要执行的任务。每次 Tick 的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中
1. onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。
2. setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
3. ajax 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。


javascript是单线程的,浏览器是多线程的。
进程和线程都是操作系统的概念,进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,即进程是操作系统进行资源分配和独立运行的最小单元。


进程(process)


进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,即进程是操作系统进行资源分配和独立运行的最小单元。
当我们启动一个应用,计算机会至少创建一个进程,cpu会为进程分配一部分内存,应用的所有状态都会保存在这块内存中,应用也许还会创建多个线程来辅助工作,这些线程可以共享这部分内存中的数据。如果应用关闭,进程会被终结,操作系统会释放相关内存。


线程(thread)



  • 进程内部的一个执行单元,是被系统独立调度和分派的基本单位。系统创建好进程后,实际上就启动执行了该进程的主执行线程

  • 进程就像是一个有边界的生产厂间,而线程就像是厂间内的一个个员工,可以自己做自己的事情,也可以相互配合做同一件事情,所以一个进程可以创建多个线程。

  • 线程自己不需要系统重新分配资源,它与同属一个进程的其它线程共享当前进程所拥有的全部资源。 PS: 进程之间是不共享资源和地址空间的,所以不会存在太多的安全问题,而由于多个线程共享着相同的地址空间和资源,所以会存在线程之间有可能会恶意修改或者获取非授权数据等复杂的安全问题。



Chrome 采用多进程架构


主要进程



  • Browser Process 浏览器的主进程(负责协调、主控) (1)负责包括地址栏,书签栏,前进后退按钮等部分的工作 (2)负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问 (3)负责各个页面的管理,创建和销毁其他进程

  • Renderer Process 负责一个 tab 内关于网页呈现的所有事情,页面渲染,脚本执行,事件处理等


image.png



  • Plugin Process 负责控制一个网页用到的所有插件,如 flash 每种类型的插件对应一个进程,仅当使用该插件时才创建

  • GPU Process 负责处理 GPU 相关的任务,3D 绘制等


优点
由于默认 新开 一个 tab 页面 新建 一个进程,所以单个 tab 页面崩溃不会影响到整个浏览器。
同样,第三方插件崩溃也不会影响到整个浏览器。
多进程可以充分利用现代 CPU 多核的优势。


缺点
系统为浏览器新开的进程分配内存、CPU 等资源,所以内存和 CPU 的资源消耗也会更大。
不过 Chrome 在内存释放方面做的不错,基本内存都是能很快释放掉给其他程序运行的。


一个浏览器至少实现三个常驻线程:JavaScript引擎线程,GUI渲染线程,浏览器事件触发线程。


1.JavaScript引擎是基于事件驱动单线程执行的,JavaScript引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JavaScript线程在运行JavaScript程序。


2.GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。但需要注意,GUI渲染线程与JavaScript引擎是互斥的,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JavaScript引擎空闲时立即被执行。


3.事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JavaScript引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeout、也可来自浏览器内核的其他线程如鼠标点击、Ajax异步请求等,但由于JavaScript的单线程关系所有这些事件都得排队等待JavaScript引擎处理(当线程中没有执行任何同步代码的前提下才会执行异步代码)


问题



  1. 为什么 Javascript 要是单线程的 ?


JavaScript 为处理页面中用户的交互,以及操作 DOM 树、CSS 样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。
如果 JavaScript 是多线程的方式来操作这些 UI DOM,则可能出现 UI 操作的冲突。但为了避免因为引入了锁而带来更大的复杂性,Javascript 在最初就选择了单线程执行。



  1. 为什么 JS 阻塞页面加载 ?


由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。所以为了防止渲染的不可预期结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。



  1. css 加载会造成阻塞吗 ?


CSS 加载不会阻塞 DOM 的解析(并行), Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的所以CSS 加载会阻塞 Dom 的渲染,同时css 会阻塞后面 js 的执行



  1. 什么是 CRP,即关键渲染路径(Critical Rendering Path)? 如何优化 ?


image.png


Html可以逐步解析,和css解析是并行的,但是css不行,因为css的每个属性都是可以改变cssom的,比如后面的把前面设置的font-size覆盖等,所以必须等cssom构建完毕才能进入下一个阶段。CSS的加载速度与构建CSSOM的速度将直接影响首屏渲染速度,因此在默认情况下CSS被视为阻塞渲染的资源。


通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个script标签时,DOM构建将暂停,直至脚本完成执行。但由于JavaScript可以修改CSSOM,所以需要等CSSOM构建完毕后再执行JS。


优化围绕三因素


关键资源数量(js、css)


关键路径长度


关键字节的数量(字节越小、下载和处理速度都会更快——压缩)


具体做法:


优化dom


html文件尽可能小,删除冗余代码,压缩代码,使用缓存(http cache)


优化cssom


仅把首屏需要的css通过style标签内嵌到head里,其余的使用异步方式非阻塞加载(如Critical CSS)


避免使用@import


@import会把css引入从并行变成串行加载


异步js


所有文本资源都应该尽可能小,删除未使用的代码、缩小文件的尺寸(Minify)、使用gzip压缩(Compress)、使用缓存(HTTP Cache)


可以为script添加async属性异步加载


5.从输入url浏览器渲染的流程。


解析 HTML 文件,构建 DOM 树,同时浏览器主进程负责下载 CSS 文件
CSS 文件下载完成,解析 CSS 文件成树形的数据结构,然后结合 DOM 树合并成 RenderObject 树
布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的尺寸,位置等计算
绘制 RenderObject 树 (paint),绘制页面的像素信息
浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面


6.Event Loop至少包含两个队列,macrotask队列和microtask队列


async/await成对出现,async标记的函数会返回一个Promise对象,可以使用then方法添加回调函数。await后面的语句会同步执行。但 await 下面的语句会被当成微任务添加到当前任务队列的末尾异步执行。


先微后宏


回流 (Reflow)


当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:



  • 页面首次渲染

  • 浏览器窗口大小发生改变

  • 元素尺寸或位置发生改变

  • 元素内容变化(文字数量或图片大小等等)

  • 元素字体大小变化

  • 添加或者删除可见的DOM元素

  • 激活CSS伪类(例如::hover)

  • 查询某些属性或调用某些方法


重绘 (Repaint)


当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。


回流比重绘的代价要更高。
有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。



  1. 多线程的优点和缺点分别是什么?


优点:


1、将耗时较长的操作(网络请求、图片下载、音频下载、数据库访问等)放在子线程中执行,可以防止主线程的卡死;


2、可以发挥多核处理的优势,提升cpu的使用率。


缺点:


1、每开辟一个子线程就消耗一定的资源;


2、会造成代码的可读性变差;


3、如果出现多个线程同时访问一个资源,会出现资源争夺的情况。


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

收起阅读 »

使用transform和left改变位置的性能区别

使用transform和left改变位置的性能区别现如今大多数设备的屏幕刷新频率是60Hz,也就是每秒钟屏幕刷新60次;因此网页动画的运行速度只要达到60FPS,我们就会觉得动画很流畅。F(Frames) P(Per) S(Second) 指的画面每秒钟传输的...
继续阅读 »

使用transform和left改变位置的性能区别

现如今大多数设备的屏幕刷新频率是60Hz,也就是每秒钟屏幕刷新60次;因此网页动画的运行速度只要达到60FPS,我们就会觉得动画很流畅。

F(Frames) P(Per) S(Second) 指的画面每秒钟传输的帧数,60FPS指的是每秒钟60帧;换算下来每一帧差不多是16毫秒。 (1 秒 = 1000 毫秒) / 60 帧 = 16.66 毫秒/帧 复制代码但通常浏览器需要花费一些时间将每一帧的内容绘制到屏幕上(包括样式计算、布局、绘制、合成等工作),所以通常我们只有10毫秒来执行JS代码。

那么动画只要接近于60FPS就是比较流畅的,对比一下通过position:left 做动画和transform做动画的性能区别

假设每个人都是用性能最好的手机,浏览器,我们根本用不着去做性能优化,所以在这里为了效果明显,先将环境配置到较低,较差的情况下测试,动画也不能设置为单一的移动

1如何使用google开发者工具查看帧数

1.先按键盘F12, 然后点到performance

2.点击刷新按钮再按确定

image.png

3.把鼠标放在下面就是他对应的帧数

test5.gif

4.现在的浏览器(google为例)已经默认开启了硬件加速器,所以你去对比left和transform其实效果非常不明显,所以先把这个默认关掉

image.png

5.对比效果,应该是在低cpu的情况下测试,将他设置为6

test7.gif

6 查看GPU的使用

image.png

如果你是mac,勾选fps meter, 如果你是windows,勾选我上面写的

我是windows,但是我并看不到帧率的时时变化

7 如果你想查看层级

检查-> layers -> 选择那个可旋转的 -> 查看元素paint code的变化

如果你发现你没有layers, 可以看看三个点里面的more tools,把layers点出来

image.png

4transformcode.gif

2使用position:left (使用left并没有被提升到复合层)

<div class="ball-running"></div>
.ball-running {
width: 100px;
height: 100px;
animation: run-around 2s linear alternate 100;
background: red;
position: absolute;
border-radius: 50%;
}
@keyframes run-around {
0% {
top: 0;
left: 0;
}
25% {
top: 0;
left: 200px;
}
50% {
top: 200px;
left: 200px;
}
75% {
top: 200px;
left: 0;
}
}

3transformcode.gif


test2.gif


在cpu 4slown down的情况下,我们可以看到上面的FPS刚开始在60左右,后面掉到了4FPS,这个动画是不够流畅的.
帧率呈现出锯齿型


这是对应的帧率


image.png


在cpu6 slow down的帧率下甚至会出现掉帧的情况(下面那些红色的就是dropped frame)


test5.gif


3.使用transform进行做动画(transform提升到了复合层)

.ball-running {
width: 100px;
height: 100px;
animation: run-around 2s linear alternate 100;
background: red;
border-radius: 50%;
}
@keyframes run-around {
0% {
transform: translate(0, 0);
}
25% {
transform: translate(200px, 0);
}
50% {
transform: translate(200px, 200px);
}
75% {
transform: translate(0, 200px);
}
}

1transformcode.gif


4.从层级方向解释transform性能优于left


建议看这篇文章:
浏览器层合成与页面渲染优化


基本的渲染流程:


image.png


从左往右边看,我们可以看到,浏览器渲染过程如下:


1.解析HTML,生成DOM树,解析CSS,生成CSSOM树
(CSS Object Model ,CSS 对象模型,里面有很多api,包括style rules-内部样式表中所有的CSS规则)
2.将DOM树和CSSOM树结合,生成渲染树(Render Tree)
3.Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
4.Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
5.display:将像素发送给GPU,展示在页面上。
复制代码

先了解下什么是渲染层


渲染层: 在 DOM 树中每个节点都会对应一个渲染对象(RenderObject),
当它们的渲染对象处于相同的坐标空间(z 轴空间)时,就会形成一个 RenderLayers,也就是渲染层。
复制代码

1先不涉及任何的层级问题

<div class="big">
</div>
<div class="small"></div>
<style>
.big {
width: 200px;
height: 200px;
background-color: yellow;
}
.small {
width: 100px;
height: 100px;
background-color: red;
}
</style>

1普通的代码.gif

从上面来看,只有一个渲染层

2加上index

<div class="big">
</div>
<div class="small"></div>
<style>
.big {
width: 200px;
height: 200px;
background-color: yellow;
position: relative;
}
.small {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
top: 0px;
left: 0px;
z-index: 10000;
}
</style>

1zindex.gif

从视觉上来看,small 的div确实是在big之上,但是和big在同一个渲染层上

3加上transform

<div class="big">
</div>
<div class="small"></div>
<style>
.big {
width: 200px;
height: 200px;
background-color: yellow;
position: relative;
}
.small {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
top: 0px;
left: 0px;
z-index: 10000;
transform: translateZ(0);
}

1transform.gif

如何形成合成层


上面产生了一个新的层级,也就是合成层


首先,如果你改了一个影响元素布局信息的CSS样式,比如width、height、left、top等(transform除外),那么浏览器会将当前的Layout标记为dirty,因为元素的位置信息变了,将可能会导致整个网页其他元素的位置情况都发生改变,所以需要执行Layout全局重新计算每个元素的位置,如果提升为合成层能够开启gpu加速,并且在渲染的时候不会影响其他的层


并且在使用left的时候,document的paint code一直在变化,而使用transform的paint code一直都是不变的,可看上面的动画gif


有关于层级方面的东西,希望大家共同交流,我觉得自己也没有深刻的了解有些定义,只写了自己会的理解的,希望在查看操作方面能帮到大家



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

5个 Chrome 调试混合应用的技巧

对前端开发人员来说,Chrome 真是一个必备的开发工具,大到页面展示,小到 BUG 调试/HTTP 抓包等,本文我将和大家分享自己做混合应用开发过程中经常用到的几个调试技巧。一、调试安卓应用 在进行混合应用开发过程中,经常需要在安卓应用中调试 H5 项目的代...
继续阅读 »

对前端开发人员来说,Chrome 真是一个必备的开发工具,大到页面展示,小到 BUG 调试/HTTP 抓包等,本文我将和大家分享自己做混合应用开发过程中经常用到的几个调试技巧。

一、调试安卓应用


在进行混合应用开发过程中,经常需要在安卓应用中调试 H5 项目的代码,这里我们就需要了解安卓应用如何在 Chrome 上进行调试。
接下来简单介绍一下,希望大家还是能实际进行调试看看:


1. 准备工作


需要准备有以下几个事项:



  1. 安卓包必须为可调试包,如果不可以调试,可以找原生的同事提供;

  2. 安卓手机通过数据线连接电脑,然后开启“开发者模式”,并启用“USB 调试”选项。


2. Chrome 启动调试页面


在 Chrome 浏览器访问“chrome://inspect/#devices”,然后在 WebView 列表中选择你要调试的页面,点击“ Inspect ”选项,跟调试 PC 网页一样,使用 Chrome 控制台进行调试。



然后就可以正常进行调试了,操作和平常 Chrome 上面调试页面是一样的。


3. 注意


如果访问 “chrome://inspect/#devices” 页面会一直提示 404,可以在翻墙情况下,先在 Chrome 访问 chrome-devtools-frontend.appspot.com,然后重新访问“chrome://inspect/#devices”即可。

二、筛选特定条件的请求


在 Network 面板中,我们可以在 Filter 输入框中,通过各种筛选条件,来查看满足条件的请求。



  1. 使用场景:


如只需要查看失败或者符合指定 URL 的请求。



  1. 使用方式:


在 Network 面板在 Filter 输入框中,输入各种筛选条件,支持的筛选条件包括:文本、正则表达式、过滤器和资源类型。
这里主要介绍“过滤器”,包括:


这里输入“-”目的是为了让大家能看到 Chrome 提供哪些高级选项,在使用的时候是不需要输入“-”。
如果输入“-.js -.css”则可以过滤掉“.js”和“.css”类型的文件。


关于过滤器更多用法,可以阅读《Chrome DevTools: How to Filter Network Requests》



三、快速断点报错信息


在 Sources 面板中,我们可以开启异常自动断点的开关,当我们代码抛出异常,会自动在抛出异常的地方断点,能帮助我们快速定位到错误信息,并提供完整的错误信息的方法调用栈。
3速断点报错信息.png



  1. 使用场景:


需要调试抛出异常的情况。



  1. 使用方式:


在 Sources 面板中,开启异常自动断点的开关。
3快速断点报错信息.gif


四、断点时修改代码


在 Sources 面板中,我们可以在需要断点的行数右击,选择“Add conditional breakpoint”,然后在输入框中输入表达式(如赋值操作等),后面代码将使用该结果。
4断点时修改代码1.png
4断点时修改代码2.png



  1. 使用场景:


需要在调试时,方便手动修改数据来完成后续调试的时候。



  1. 使用方式:


在 Sources 面板中,在需要断点的行数右击,选择“Add conditional breakpoint”。
4断点时修改代码.gif


五、自定义断点(事件、请求等)


当我们需要进行自定义断点的时候,比如需要拦截 DOM 事件、网络请求等,就可以在 Source 面板,通过 XHR/fetch Breakpoints 和 Event Listener Breakpoints 来启用对应断点。
5自定义断点.png



  1. 使用场景:


需要在调试时,需要增加自定义断点时(如需要拦截 DOM 事件、网络请求等)。



  1. 使用方式:


在 Sources 面板中,通过 XHR/fetch Breakpoints 和 Event Listener Breakpoints 来启用对应断点。
5自定义断点.gif




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



收起阅读 »

如何处理浏览器的断网情况?

好的断网处理会让人很舒适:lol的断线重连,王者荣耀的断线重连 可以确保游戏的继续进行 坏的断网处理甚至不处理会出bug:比如我手上的项目就出了个bug 业务人员表示非常苦恼 网络问题一直是一个很值得关注的问题。 比如在慢网情况下,增加loading避免重复发...
继续阅读 »

好的断网处理会让人很舒适:lol的断线重连,王者荣耀的断线重连 可以确保游戏的继续进行


坏的断网处理甚至不处理会出bug:比如我手上的项目就出了个bug 业务人员表示非常苦恼


网络问题一直是一个很值得关注的问题。


比如在慢网情况下,增加loading避免重复发请求,使用promise顺序处理请求的返回结果,或者是增加一些友好的上传进度提示等等。


那么大家有没有想过断网情况下该怎么做呢?比如说网络正常->断网->网络正常。


其实我一直也没想过,直到组里的测试测出一个断网导致的bug,让我意识到重度依赖网络请求的前端,在断网情况下可能会出现严重的bug。


因此我将在这里记录一下自己对系统断网情况下的处理,一方面避免bug产生,一方面保证用户及时在应用内知道网络已经断开连接

概览


为了构建一个 “断网(offline)可用”的web应用,你需要知道应用在什么时候是断网(offline)的。
不仅仅要知道什么时候断网,更要知道什么时候网络恢复正常(online)。
可以分解陈本下面两种常见情况:



  1. 你需要知道用户何时online,这样你可以与服务器之间re-sync(重新同步)。

  2. 你需要知道用户何时offline,这样你可以将你未发出的请求过一段时间再向服务器发出。


通常可以通过online/offline事件去做这个事情。


用于检测浏览器是否连网的navigator.onLine


navigator.onLine



  • true online

  • false offline


可以通过network的online选项切换为offline,打印navigator.onLine验证。


当浏览器不能连接到网络时,这个属性会更新。规范中是这样定义的:

The navigator.onLine attribute must return false if the user agent will not contact the network when the user follows links or when a script requests a remote page (or knows that such an attempt would fail)...

用于检测网络状况的navigator.connection


在youtube观看视频时,自动检测网络状况切换清晰度是如何做到的呢?
国内的视频网站也会给出一个切换网络的提醒,该如何去检测呢?
也就是说,有没有办法检测网络状况?判断当前网络是流畅,拥堵,繁忙呢?
可以通过navigator.connection,属性包括effectiveType,rtt,downlink和变更网络事件change。继承自NetworkInformation API。

navigator.connection

online状态下运行console.log(navigator.connection);

{
onchange: null,
effectiveType: "4g",
rtt: 50,
downlink: 2,
saveData: false
}

通过navigator.connection可以判断出online,fast 3g,slow 3g,和offline,这四种状态下的effectiveType分别为4g,3g,2g,4g(rtt,downlink均为0)。


rtt和downlink是什么?NetworkInformation是什么?


这是两个反映网络状况的参数,比type更加具象且更能反映当前网络的真实情况。


常见网络情况rtt和downlink表


注意:rtt和downlink不是定值,而是实时变化的。online时,可能它现在是rtt 100ms,2.2Mb/s,下一秒就变成125ms,2.1Mb/s了。


rtt


  • 连接预估往返时间

  • 单位为ms

  • 值为四舍五入到25毫秒的最接近倍数(就是说这个值x%25===0,可以观察常见网络情况rtt和downlink表)

  • 值越小网速越快。类似ping的time吧

  • 在Web Worker中可用


downlink


  • 带宽预估值

  • 单位为Mbit/s(注意是Mbit,不是MByte。)

  • 值也是四舍五入到最接近的25比特/秒的倍数(就是说这个值x%25===0,可以观察常见网络情况rtt和downlink表)

  • 一般越宽速度越快,也就是,信道上可以传输更多数。(吐槽一句,学过的通信原理还蛮有用。)

  • 值越大网速越快。类似高速一般比国道宽。

  • 在Web Worker中可用


草案(Draft)阶段NetworkInformation API

无论是rtt,还是downlink,都是这个草案中的内容。
除此之外还有downlinkMax,saveData,type等属性。
更多资料可以查询:NetworkInformation


如何检测网络变化去做出响应呢?


NetworkInformation继承自EventTarget,可以通过监听change事件去做一些响应。


例如可以获得网络状况的变更?

var connection = navigator.connection;
var type = connection.effectiveType;

function updateConnectionStatus() {
console.log("网络状况从 " + type + " 切换至" + connection.effectiveType);
type = connection.effectiveType;
}

connection.addEventListener('change', updateConnectionStatus);

监听变更之后,我们可以弹一个Modal提醒用户,也可以出一个Notice通知用户网络有变化,或者可以更高级得去自动切换清晰度(这个应该比较难)。


引出NetworkInformation的概念,只是想起一个抛砖引玉的作用。这种细粒度的网络状况检测,可以结合具体需求去具体实现。


在这篇博文中,我们只处理断网和连网两种情况,下面来看断网事件"offline"和连网事件"online"。


断网事件"offline"和连网事件"online"


浏览器有两个事件:"online" 和 "offline".
这两个事件会在浏览器在online mode和offline mode之间切换时,由页面的<body>发射出去。


事件会按照以下顺序冒泡:document.body -> document -> window。


事件是不能去取消的(开发者在代码上不能手动变为online或者offline,开发时使用开发者工具可以)。


注册上下线事件的几种方式


最最建议window+addEventListener的组合。



  • 通过window或document或document.body和addEventListener(Chrome80仅window有效)

  • 为document或document.body的.ononline或.onoffline属性设置一个js函数。(注意,使用window.ononline和window.onoffline会有兼容性的问题)

  • 也可以通过标签注册事件<body ononline="onlineCb" onoffline="offlineCb"></body>


例子

<div id="status"></div>
<div id="log"></div>
window.addEventListener('load', function() {
var status = document.getElementById("status");
var log = document.getElementById("log");

function updateOnlineStatus(event) {
var condition = navigator.onLine ? "online" : "offline";
status.innerHTML = condition.toUpperCase();

log.insertAdjacentHTML("beforeend", "Event: " + event.type + "; Status: " + condition);
}

window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
});

其中insertAdjacentHTML是在标签节点的邻近位置插入,可以查阅:DOM进阶之insertAdjacentHTML


断网处理项目实战


可以基于vue,react封装出离线处理组件,在需要到的页面引入即可。


思路和效果


只要做到断网提醒+遮罩,上线提醒-遮罩即可。



  • 监听offline,断网给出提醒和遮罩:网络已断开,请检查网络连接。

  • 监听online,连网给出提醒和遮罩:网络已连接。

断网处理组件使用

<OfflineHandle
offlineTitle = "断网处理标题"
desc="断网处理描述"
onlineTitle="连网提醒"
/>
Vue组件
<!--OfflineHandle.vue-->
<template>
<div v-if="mask" class="offline-mask">
<h2 class="offline-mask-title">{{ offlineTitle }}</h2>

<p class="offline-mask-desc">{{ desc }}</p >
</div>
</template>

<script>
export default {
name: "offline-handle",
props: {
offlineTitle: {
type: String,
default: "网络已断开,请检查网络连接。",
},
onlineTitle: {
type: String,
default: "网络已连接",
},
desc: {
type: String,
default: "",
},
duration: {
type: Number,
default: 4.5,
},
},
data() {
return {
mask: false,
};
},
mounted() {
window.addEventListener("offline", this.eventHandle);
window.addEventListener("online", this.eventHandle);
console.log(this.desc);
},
beforeDestroy() {
window.removeEventListener("offline", this.eventHandle);
window.removeEventListener("online", this.eventHandle);
},
methods: {
eventHandle(event) {
const type = event.type === "offline" ? "error" : "success";
this.$Notice[type]({
title: type === "error" ? this.offlineTitle : this.onlineTitle,
desc: type === "error" ? this.desc : "",
duration: this.duration,
});
setTimeout(() => {
this.mask = event.type === "offline";
}, 1500);
},
},
};
</script>

<style lang="css" scoped>
.offline-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
z-index: 9999;
transition: position 2s;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.offline-mask-title {
color: rgba(0, 0, 0, 0.8);
}
.offline-mask-desc {
margin-top: 20px;
color: red;
font-weight: bold;
}
</style>
React组件
// offlineHandle.js
import React, { useState, useEffect } from "react";
import { notification } from "antd";
import "antd/dist/antd.css";
import "./index.css";

const OfflineHandle = (props) => {
const {
offlineTitle = "网络已断开,请检查网络连接。",
onlineTitle = "网络已连接",
desc,
duration = 4.5
} = props;
const [mask, setMask] = useState(false);

const eventHandler = (event) => {
const type = event.type === "offline" ? "error" : "success";
console.log(desc, "desc");
openNotification({
type,
title: type === "error" ? offlineTitle : onlineTitle,
desc: type === "error" ? desc : "",
duration
});
setTimeout(() => {
setMask(event.type === "offline");
}, 1500);
};

const openNotification = ({ type, title, desc, duration }) => {
notification[type]({
message: title,
description: desc,
duration
});
};

useEffect(() => {
window.addEventListener("offline", eventHandler);
window.addEventListener("online", eventHandler);
return () => {
window.removeEventListener("offline", eventHandler);
window.removeEventListener("online", eventHandler);
};
}, []);

const renderOfflineMask = () => {
if (!mask) return null;
return (
<div className="offline-mask">
<h2 className="offline-mask-title">{offlineTitle}</h2>

<p className="offline-mask-desc">{desc}</p >
</div>
);
};

return <>{renderOfflineMask()}</>;
};

export default OfflineHandle;

发现



  • offline和online事件:window有效,document和document.body设置无效


手上的项目只运行在Chrome浏览器,只有为window设置offline和online才生效。
运行环境:"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36



  • 为position增加2s的transition的避免屏闪


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

音视频学习从零到整-关于视频的一些概念

内容1、视频文件格式2、视频封装格式3、视频编解码方式4、音频编解码方式5、颜色模型一.视频相关概念1.1 视频文件格式文件格式这个概念应该是我们比较熟悉的,比如我们常见的 Word 文档的文件格式是 .doc,JPG 图片的文件格式是 .jpg 等等。那对于...
继续阅读 »

内容

1、视频文件格式
2、视频封装格式
3、视频编解码方式
4、音频编解码方式
5、颜色模型

一.视频相关概念

1.1 视频文件格式

文件格式这个概念应该是我们比较熟悉的,比如我们常见的 Word 文档的文件格式是 .doc,JPG 图片的文件格式是 .jpg 等等。那对于视频来说,
我们常见的文件格式则有:.mov、.avi、.mpg、.vob、.mkv、.rm、.rmvb 等等。文件格式通常表现为文件在操作系统上存储时的后缀名,它通常会被操作系统用来与相应的打开程序关联,比如你双击一个 test.doc 文件,系统会调用 Word 去打开它。你双击一个 test.avi 或者 test.mkv 系统会调用视频播放器去打开它。

同样是视频,为什么会有 .mov、.avi、.mpg 等等这么多种文件格式呢?****那是因为它们通过不同的方式实现了视频这件事情,至于这个不同在哪里,那就需要了解一下接下来要说的「视频封装格式」这个概念了。

1.2 视频封装格式

视频封装格式,简称视频格式,相当于一种储存视频信息的容器,它里面包含了封装视频文件所需要的视频信息、音频信息和相关的配置信息(比如:视频和音频的关联信息、如何解码等等)。一种视频封装格式的直接反映就是对应着相应的视频文件格式。


下面我们就列举一些文件封装格式:

1、AVI 格式,对应的文件格式为 .avi,英文全称 Audio Video Interleaved,是由 Microsoft 公司于 1992 年推出。这种视频格式的优点是图像质量好,无损 AVI 可以保存 alpha 通道。缺点是体积过于庞大,并且压缩标准不统一,存在较多的高低版本兼容问题。

2、DV-AVI 格式,对应的文件格式为 .avi,英文全称 Digital Video Format,是由索尼、松下、JVC 等多家厂商联合提出的一种家用数字视频格式。常见的数码摄像机就是使用这种格式记录视频数据的。它可以通过电脑的 IEEE 1394 端口传输视频数据到电脑,也可以将电脑中编辑好的的视频数据回录到数码摄像机中。

3、WMV 格式,对应的文件格式是 .wmv、.asf,英文全称 Windows Media Video,是微软推出的一种采用独立编码方式并且可以直接在网上实时观看视频节目的文件压缩格式。在同等视频质量下,WMV 格式的文件可以边下载边播放,因此很适合在网上播放和传输。

4、MPEG 格式,对应的文件格式有 .mpg、.mpeg、.mpe、.dat、.vob、.asf、.3gp、.mp4 等等,英文全称 Moving Picture Experts Group,是由运动图像专家组制定的视频格式,该专家组于 1988 年组建,专门负责视频和音频标准制定,其成员都是视频、音频以及系统领域的技术专家。MPEG 格式目前有三个压缩标准,分别是 MPEG-1、MPEG-2、和 MPEG-4。MPEG-4 是现在用的比较多的视频封装格式,它为了播放流式媒体的高质量视频而专门设计的,以求使用最少的数据获得最佳的图像质量。

5、Matroska 格式,对应的文件格式是 .mkv,Matroska 是一种新的视频封装格式,它可将多种不同编码的视频及 16 条以上不同格式的音频和不同语言的字幕流封装到一个 Matroska Media 文件当中。

6、Real Video 格式,对应的文件格式是 .rm、.rmvb,是 Real Networks 公司所制定的音频视频压缩规范称为 Real Media。用户可以使用 RealPlayer 根据不同的网络传输速率制定出不同的压缩比率,从而实现在低速率的网络上进行影像数据实时传送和播放。

7、QuickTime File Format 格式,对应的文件格式是 .mov,是 Apple 公司开发的一种视频格式,默认的播放器是苹果的 QuickTime。这种封装格式具有较高的压缩比率和较完美的视频清晰度等特点,并可以保存 alpha 通道。

8、Flash Video 格式,对应的文件格式是 .flv,是由 Adobe Flash 延伸出来的一种网络视频封装格式。这种格式被很多视频网站所采用。

从上面的介绍中,我们大概对视频文件格式以及对应的视频封装方式有了一个概念,接下来则需要了解一下关于视频更本质的东西,那就是视频编解码。

1.3 容器(视频封装格式)
封装格式:就是将已经编码压缩好的视频数据 和音频数据按照一定的格式放到一个文件中.这个文件可以称为容器. 当然可以理解为这只是一个外壳.

通常我们不仅仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据.例如字幕.这多种数据会不同的程序来处理,但是它们在传输和存储的时候,这多种数据都是被绑定在一起的.

常见的视频容器格式:
1、AVI: 是当时为对抗quicktime格式(mov)而推出的,只能支持固定CBR恒定定比特率编码的声音文件
2、MOV:是Quicktime封装
3、WMV:微软推出的,作为市场竞争
4、mkv:万能封装器,有良好的兼容和跨平台性、纠错性,可带外挂字幕
5、flv: 这种封装方式可以很好的保护原始地址,不容易被下载到,目前一些视频分享网站都采用这种封装方式
6、MP4:主要应用于mpeg4的封装,主要在手机上使用。

2.1视频编解码方式
视频编解码的过程是指对数字视频进行压缩或解压缩的一个过程.

在做视频编解码时,需要考虑以下这些因素的平衡:视频的质量、用来表示视频所需要的数据量(通常称之为码率)、编码算法和解码算法的复杂度、针对数据丢失和错误的鲁棒性(Robustness)、编辑的方便性、随机访问、编码算法设计的完美性、端到端的延时以及其它一些因素。

2.2 常见视频编码方式:

H.26X 系列,由国际电传视讯联盟远程通信标准化组织(ITU-T)主导,包括 H.261、H.262、H.263、H.264、H.265

H.261,主要用于老的视频会议和视频电话系统。是第一个使用的数字视频压缩标准。实质上说,之后的所有的标准视频编解码器都是基于它设计的。
H.262,等同于 MPEG-2 第二部分,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
H.263,主要用于视频会议、视频电话和网络视频相关产品。在对逐行扫描的视频源进行压缩的方面,H.263 比它之前的视频编码标准在性能上有了较大的提升。尤其是在低码率端,它可以在保证一定质量的前提下大大的节约码率。
H.264,等同于 MPEG-4 第十部分,也被称为高级视频编码(Advanced Video Coding,简称 AVC),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。该标准引入了一系列新的能够大大提高压缩性能的技术,并能够同时在高码率端和低码率端大大超越以前的诸标准。
H.265,被称为高效率视频编码(High Efficiency Video Coding,简称 HEVC)是一种视频压缩标准,是 H.264 的继任者。HEVC 被认为不仅提升图像质量,同时也能达到 H.264 两倍的压缩率(等同于同样画面质量下比特率减少了 50%),可支持 4K 分辨率甚至到超高画质电视,最高分辨率可达到 8192×4320(8K 分辨率),这是目前发展的趋势。
MPEG 系列,由国际标准组织机构(ISO)下属的运动图象专家组(MPEG)开发。

MPEG-1 第二部分,主要使用在 VCD 上,有些在线视频也使用这种格式。该编解码器的质量大致上和原有的 VHS 录像带相当。
MPEG-2 第二部分,等同于 H.262,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
MPEG-4 第二部分,可以使用在网络传输、广播和媒体存储上。比起 MPEG-2 第二部分和第一版的 H.263,它的压缩性能有所提高。
MPEG-4 第十部分,等同于 H.264,是这两个编码组织合作诞生的标准。
其他,AMV、AVS、Bink、CineForm 等等,这里就不做多的介绍了。

介绍了上面这些「视频编解码方式」后,我们来说说它和上一节讲的「视频封装格式」的关系。

可以把「视频封装格式」看做是一个装着视频、音频、「视频编解码方式」等信息的容器。一种「视频封装格式」可以支持多种「视频编解码方式」,比如:QuickTime File Format(.MOV) 支持几乎所有的「视频编解码方式」,MPEG(.MP4) 也支持相当广的「视频编解码方式」。当我们看到一个视频文件名为 test.mov 时,我们可以知道它的「视频文件格式」是 .mov,也可以知道它的视频封装格式是 QuickTime File Format,

但是无法知道它的「视频编解码方式」。那比较专业的说法可能是以 A/B 这种方式,A 是「视频编解码方式」,B 是「视频封装格式」。比如:一个 H.264/MOV 的视频文件,它的封装方式就是 QuickTime File Format,编码方式是 H.264

3.1 音频编码方式
视频中除了画面通常还有声音,所以这就涉及到音频编解码。在视频中经常使用的音频编码方式有

AAC,英文全称 Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony等公司共同开发,在 1997 年推出的基于 MPEG-2 的音频编码技术。2000 年,MPEG-4 标准出现后,AAC 重新集成了其特性,加入了 SBR 技术和 PS 技术,为了区别于传统的 MPEG-2 AAC 又称为 MPEG-4 AAC。
MP3,英文全称 MPEG-1 or MPEG-2 Audio Layer III,是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在 1991 年,由位于德国埃尔朗根的研究组织 Fraunhofer-Gesellschaft 的一组工程师发明和标准化的。MP3 的普及,曾对音乐产业造成极大的冲击与影响。
WMA,英文全称 Windows Media Audio,由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。

3.2 直播/小视频中的编码格式
视频编码格式

H264编码的优势:
低码率
高质量的图像
容错能力强
网络适应性强
总结: H264最大的优势,具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.
举例: 原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1
音频编码格式:

AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式.

LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s)
HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s)
优势:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码

适合场景:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码

4.1 关于H264
H.264 是现在广泛采用的一种编码方式。关于 H.264 相关的概念,从大到小排序依次是:序列、图像、片组、片、NALU、宏块、亚宏块、块、像素。

图像


H.264 中,「图像」是个集合的概念,帧、顶场、底场都可以称为图像。一帧通常就是一幅完整的图像。

当采集视频信号时,如果采用逐行扫描,则每次扫描得到的信号就是一副图像,也就是一帧。

当采集视频信号时,如果采用隔行扫描(奇、偶数行),则扫描下来的一帧图像就被分为了两个部分,这每一部分就称为「场」,根据次序分为:「顶场」和「底场」。

「帧」和「场」的概念又带来了不同的编码方式:帧编码、场编码逐行扫描适合于运动图像,所以对于运动图像采用帧编码更好;隔行扫描适合于非运动图像,所以对于非运动图像采用场编码更好。




片(Slice),每一帧图像可以分为多个片

网络提取层单元(NALU, Network Abstraction Layer Unit),

NALU 是用来将编码的数据进行打包的,一个分片(Slice)可以编码到一个 NALU 单元。不过一个 NALU 单元中除了容纳分片(Slice)编码的码流外,还可以容纳其他数据,比如序列参数集 SPS。对于客户端其主要任务则是接收数据包,从数据包中解析出 NALU 单元,然后进行解码播放。

宏块(Macroblock),分片是由宏块组成。

4.2 颜色模型
我们开发场景中使用最多的应该是 RGB 模型


在 RGB 模型中每种颜色需要 3 个数字,分别表示 R、G、B,比如 (255, 0, 0) 表示红色,通常一个数字占用 1 字节,那么表示一种颜色需要 24 bits。那么有没有更高效的颜色模型能够用更少的 bit 来表示颜色呢?

现在我们假设我们定义一个「亮度(Luminance)」的概念来表示颜色的亮度,那它就可以用含 R、G、B 的表达式表示为:

Y = kr*R + kg*G + kb*B

Y 即「亮度」,kr、kg、kb 即 R、G、B 的权重值。

这时,我们可以定义一个「色度(Chrominance)」的概念来表示颜色的差异:

Cr = R – Y
Cg = G – Y
Cb = B – Y

Cr、Cg、Cb 分别表示在 R、G、B 上的色度分量。上述模型就是 YCbCr 颜色模型基本原理。

YCbCr 是属于 YUV 家族的一员,是在计算机系统中应用最为广泛的颜色模型,就比如在本文所讲的视频领域。在 YUV 中 Y 表示的是「亮度」,也就是灰阶值,U 和 V 则是表示「色度」。

YUV 的关键是在于它的亮度信号 Y 和色度信号 U、V 是分离的。那就是说即使只有 Y 信号分量而没有 U、V 分量,我们仍然可以表示出图像,只不过图像是黑白灰度图像。在YCbCr 中 Y 是指亮度分量,Cb 指蓝色色度分量,而 Cr 指红色色度分量。

现在我们从 ITU-R BT.601-7 标准中拿到推荐的相关系数,就可以得到 YCbCr 与 RGB 相互转换的公式

Y = 0.299R + 0.587G + 0.114B
Cb = 0.564(B - Y)
Cr = 0.713(R - Y)
R = Y + 1.402Cr
G = Y - 0.344Cb - 0.714Cr
B = Y + 1.772Cb

这样对于 YCbCr 这个颜色模型我们就有个初步认识了,但是我们会发现,这里 YCbCr 也仍然用了 3 个数字来表示颜色啊,有节省 bit 吗?为了回答这个问题,我们来结合视频中的图像和图像中的像素表示来说明

假设图片有如下像素组成


一副图片就是一个像素阵列.每个像素的 3 个分量的信息是完整的,YCbCr 4:4:4


下图中,对于每个像素点都保留「亮度」值,但是省略每行中偶素位像素点的「色度」值,从而节省了 bit。YCbCr4:2:2


上图,做了更多的省略,但是对图片质量的影响却不会太大.YCbCr4:2:0


转自:https://www.jianshu.com/p/15f28fe89329

收起阅读 »

RunLoop(一):源码与逻辑

简述什么是RunLoop?顾名思义RunLoop是一个运行循环,它的作用是使得程序在运行之后不会马上退出,保持运行状态,来处理一些触摸事件、定时器时间等。RunLoop可以使得线程在有任务的时候处理任务,没有任务的时候休眠,以此来节省CPU资源,提高程序性能。...
继续阅读 »

简述

什么是RunLoop?顾名思义RunLoop是一个运行循环,它的作用是使得程序在运行之后不会马上退出,保持运行状态,来处理一些触摸事件、定时器时间等。RunLoop可以使得线程在有任务的时候处理任务,没有任务的时候休眠,以此来节省CPU资源,提高程序性能。

那RunLoop是怎样保持程序的运行状态,到底处理了哪些事件?下面我们就从源码的层面来了解一下RunLoop。

RunLoop

获取runloop对象

NSRunLoop和CFRunLoopRef都代表RunLoop对象,NSRunLoop是对CFRunLoopRef的封装。

Foundation

[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

Core Foundation

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

RunLoop相关类

从源码的代码结构中我们可以找出来一下5个跟RunLoop相关的结构

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopObserverRef
CFRunLoopTimerRef

下面是CFRunLoopRef的结构代码

struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

变量很多,我们不需要全部看,只需要注意这两个

CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;

每一个runloop里面有很多mode(存在一个set集合里面),然后之后后一个mode叫做currentMode,也就是说runloop一次只能处理一种mode。

然后我们再看CFRunLoopModeRef的结构,我已经给大家省略了里面那些我们不需要关注的变量

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};

根据上面这些我们大概的可以概括出来RunLoop这些相关类的关系。


CFRunLoopModeRef
由上面的源码我们可以稍微总结一下这个CFRunLoopModeRef:

1、CFRunLoopModeRef代表RunLoop的运行模式
2、一个RunLoop包含多个CFRunLoopModeRef,每个CFRunLoopModeRef又包含多个_sources0,_sources1,_observers,_timers。
3、RunLoop每次只能运行一种mode,切换mode的时候,要先退出之前的mode。
4、如果mode中没有_sources0、_sources1、_observers、_timers,程序会立刻退出。
常用的两种Mode

kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行

UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

CFRunLoopObserverRef
源码中给出了可以监听的RunLoop状态

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 进入RunLoop
kCFRunLoopEntry = (1UL << 0),
// 即将处理timers
kCFRunLoopBeforeTimers = (1UL << 1),
// 即将处理Sources
kCFRunLoopBeforeSources = (1UL << 2),
// 即将休眠
kCFRunLoopBeforeWaiting = (1UL << 5),
// 被唤醒
kCFRunLoopAfterWaiting = (1UL << 6),
// 退出循环
kCFRunLoopExit = (1UL << 7),
// 所有状态
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

具体的怎么样添加observer来监听RunLoop状态我就不贴代码了,网上一搜有很多的。

RunLoop的运行逻辑

前面我们已经了解了RunLoop相关的结构的源码,知道了RunLoop大概的数据结构,那RunLoop到底是如何工作的呢?它的运行逻辑是什么?

我们了解过了每个mode中会存放不同的_sources0、_sources1、_observers、_timers,这些我们可以全部统称是RunLoop要处理的东西,那每一种具体对应我们了解的哪写事件呢?

Source0
触摸事件处理
performSelector:onThread:

Source1
基于系统Port(端口)的线程间通信
系统事件捕捉

Timers
NSTimer定时器
performSelector:withObject:afterDelay:

Observers
用于监听RunLoop的状态
UI刷新(BeforeWating)
Autorelease Pool (BeforWaiting)

注: UI的刷新并不是即时生效,比如说我们改变了view的backgroundColor,当执行到这行代码是并不是立刻生效,而是先记录下有这么一个任务,然后在RunLoop处理完所有的时间,进入休眠之前UI刷新。


这是大神总结的RunLoop的运行逻辑图,我直接拿过来用了。我们主要是看左边这部分,右边的这些标注是在源码中对应的主要方法名称。

这个图很容易理解,只有从06跳转到08这一步,单从图上看的话不是很清晰,这一块结合源码就比较明了了。第06步,如果存在Source1就直接跳转到08,在代码中使用了goto这个关键字,其实就是跳过了runloop休眠和唤醒这一部分的代码,直接跳转到了处理各种事件的这一部分。

下面我把源码做了一些删减,方便大家可以更清楚的梳理整个过程

// 这个是runloop入口函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */

// 通知Observers 即将进入RunLoop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 核心方法
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知Observers 即将退出RunLoop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

return result;
}

下面是核心方法

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

int32_t retVal = 0;
do {

//通知Observers 即将处理Timers
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

//通知Observers 即将处理Sources
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

//处理Blocks
__CFRunLoopDoBlocks(rl, rlm);

//处理source0,根据返回值决定在处理一次blocks
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}

Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);


// source1相关
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;
// 是否有Source1 有的话跳转到handle_msg
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
}

didDispatchPortLastTime = false;

// 通知Observers: 即将休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//休眠
__CFRunLoopSetSleeping(rl);


//等待别的消息来唤醒当前线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);


__CFRunLoopUnsetSleeping(rl);

// 通知Observers: 即将醒来
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

// 标识标识 !!!!!
handle_msg:;

__CFRunLoopSetIgnoreWakeUps(rl);

//下面根据是什么唤醒的runloop来分别处理

if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
// do nothing on Mac OS
}

// 被Timer唤醒
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// 处理Timers
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}
// 被Timer唤醒
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// 处理Timers
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
// 被GCD唤醒
else if (livePort == dispatchPort) {

// 处理GCD相关
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);

} else {
//被Source1唤醒
//处理Source1
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;

}


//在处理一遍BLocks
__CFRunLoopDoBlocks(rl, rlm);


// 设置返回值 决定是否继续循环
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}

} while (0 == retVal);

return retVal;
}

图和源码结合来看,整个流程就清晰了很多。流程里面的有些东西不需要我们太过深入的研究,我们把这个流程掌握一下就OK了。

细节补充

第一点

我们都知道RunLoop有一个优势,那就是可以使线程在有工作的时候工作,没有工作的时候休眠,来减少占用CPU资源,提高程序性能。

这说明代码在执行到

//等待别的消息来唤醒当前线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

的时候,会阻塞当前的线程。但这种阻塞跟我们之前所用到过的阻塞线程不是一回事。

举个例子,我们可以使用while(1){};这句代码来阻塞线程,这句代码在底层会转换为汇编的代码,我们的线程一直在重读执行这几句代码,所以他仅仅是阻塞线程,并没有使线程休眠,我们的线程一直在工作。但是runloop,通过mach_msg使用了一些内核层的API,真的是实现了线程的休眠,让线程不再占用CPU资源。

第二点

RunLoop与线程的关系?

一个线程对应一个RunLoop对象。
RunLoop默认不创建,在第一次获取的时候创建,主线程中的默认存在RunLoop也是因为在底层代码中,提前获取过一次。
RunLoop储存在一个全局的字典中,线程是key,RunLoop是value。(源码中有所体现)
RunLoop会在线程结束时销毁。

链接:https://www.jianshu.com/p/705aa44405c0

收起阅读 »

微信小程序自定义实现toast进度百分比动画组件

目录结构wxml {{number}} {{ content }} 搭建组件结构jsComponent({ options: { multipleSlots: true // 在组件定义时的选项中...
继续阅读 »

目录结构


wxml



{{number}}



{{ content }}


搭建组件结构

js

Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
/**
* 私有数据,组件的初始数据
* 可用于模版渲染
*/
data: { // 弹窗显示控制
animationData: {},
content: '提示内容',
number: 0,
level_box:-999,
},
/**
* 组件的方法列表
*/
methods: {
/**
* 显示toast,定义动画
*/
numberChange() {
let _this = this
for (let i = 0; i < 101; i++) {
(function () {
setTimeout(() => {
_this.setData({
number: i + '%'
})
}, 100 * i)
})()
}
},
showToast(val) {
this.setData({
level_box:999
})
this.numberChange()
var animation = wx.createAnimation({
duration: 300,
timingFunction: 'ease',
})
this.animation = animation
animation.opacity(1).step()
this.setData({
animationData: animation.export(),
content: val
})
/**
* 延时消失
*/
setTimeout(function () {
animation.opacity(0).step()
this.setData({
animationData: animation.export()
})
}.bind(this), 10000)
}
}
})

json

```javascript
{
"component": true,
"usingComponents": {}
}

wxss

.wx-toast-box {
display: flex;
width: 100%;
justify-content: center;
position: fixed;
top: 400rpx;
opacity: 0;
}

.wx-toast-content {
max-width: 80%;
border-radius: 30rpx;
padding: 30rpx;
background: rgba(0, 0, 0, 0.6);
}

.wx-toast-toast {
height: 100%;
width: 100%;
color: #fff;
font-size: 28rpx;
text-align: center;
}

.progress {
display: flex;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
text-align: CENTER;
color: #07c160;
}

.img_box {
display: flex;
justify-content: center;
margin: 20rpx 0;
}

@keyframes rotate {
from {
transform: rotate(360deg)
}

to {
transform: rotate(0deg)
}
}

.circle {
animation: 3s linear 0s normal none infinite rotate;
}

@keyframes translateBox {
0% {
transform: translateX(0px)
}

50% {
transform: translateX(10px)
}
100% {
transform: translateX(0px)
}
}

.anima_position {
animation: 3s linear 0s normal none infinite translateBox;
}

效果截图



原文:https://juejin.cn/post/6968731176492072968



收起阅读 »

让我们一起实现微信小程序国际化吧

常见的国际化方式官方方案官方链接,其实官方的解决方案最大的问题就是麻烦,主要体现在以下几个方面强依赖目录结构由于gulp.js里是按照此目录结构进行处理的,如果要维持自定义目录需要修改glup文件,如下图特别好笑的一点官方示例里居然不是这个目录结构,不过依然是...
继续阅读 »

常见的国际化方式

官方方案

官方链接,其实官方的解决方案最大的问题就是麻烦,主要体现在以下几个方面

强依赖目录结构

由于gulp.js里是按照此目录结构进行处理的,如果要维持自定义目录需要修改glup文件,如下图


特别好笑的一点官方示例里居然不是这个目录结构,不过依然是强依赖目录结构,因为gulp中路径是写死的

文档简陋

通过官方文档快速入门居然无法搭建起项目,暂时只用这种方式搭建起来了 官方github demo,通过对比发现好多必要代码都没有在文档中说明。


比如需要在app.js中开始便需要执行getI18nInstance(),否则全局都无法正常国际化。这么重要的信息居然在快速入门中没有说明


调试麻烦

每次修改代码都要重新执行npm run build,注意是每次


由于国际化必须通过npm run build来实现,而每次npm run build过后dist文件就会被覆盖,所以每次只能修改src,而小程序预览的却是dist文件,这也就导致必须频繁的执行build命令。下面演示一下增加一个表头的操作步骤

2021-05-22 07-57-21.2021-05-22 08_03_21.gif

说下优点

代码简洁。解释一下,从上图可以看到,他的书写方式和其他主流框架的国际化书写方式很类似(vue,react)。view层都是类似t(key,参数),对js侵入也很小,下面是上图页面对应的js部分。

import { I18nPage } from '@miniprogram-i18n/core'

I18nPage({
onLoad() {
this.onLocaleChange((locale) => {
console.log('current locale:', this.getLocale(), locale)
})

this.setLocale('zh-CN')
},

toggleLocale() {
this.setLocale(
this.getLocale() === 'zh-CN' ? 'en-US' : 'zh-CN'
)
},

nativate() {
wx.navigateTo({
url: '/pages/logs/logs'
})
}
})

可以说除了I18nPage以外没有别的侵入,剩下那些代码都是用于切换语言所需,如果只是最简单国际化,只需要I18nPage({})即可

聊一下为什每次都需build

其实咱们看下dist/i18n/locales.wxs文件即可

var fallbackLocale = "zh-CN";
var translations = {
"en-US": {
test: ["test messages"],
test2: ["test message 2, ", ["label"], ", ", ["label2"]],
nested: ["nested message: ", ["test"]],
toggle: ["Toggle locale"],
navigate: ["Navigate to Log"],
"window.title": ["I18n test"],
"index.test": ["Test fallback"],
navigate2: ["Navigation 2nd"],
},
"zh-CN": {
test: ["测试消息"],
test2: ["测试消息二, ", ["label"], ", ", ["label2"]],
nested: ["嵌套消息: ", ["test"]],
toggle: ["切换语言"],
navigate: ["跳转"],
"window.title": ["国际化测试"],
"index.test": ["备选"],
navigate2: ["导航2"],
},
};
var Interpreter = (function (r) {
var i = "";
function f(r, n) {
return r
? "string" == typeof r
? r
: r
.reduce(function (r, t) {
return r.concat([
(function (n, e) {
if (((e = e || {}), "string" == typeof n)) return n;
if (n[2] && "object" == typeof n[2]) {
var r = Object.keys(n[2]).reduce(function (r, t) {
return (r[t] = f(n[2][t], e)), r;
}, {}),
t = r[e[0]],
u = e[n[0]];
return void 0 !== u
? r[u.toString()] || r.other || i
: t || r.other || i;
}
if ("object" == typeof n && 0 < n.length) {
return (function r(t, n, e) {
void 0 === e && (e = 0);
if (!n || !t || t.length <= 0) return "";
var n = n[t[e]];
if ("string" == typeof n) return n;
if ("number" == typeof n) return n.toString();
if (!n) return "{" + t.join(".") + "}";
return r(t, n, ++e);
})(n[0].split("."), e, 0);
}
return "";
})(t, n),
]);
}, [])
.join("")
: i;
}
function c(r, t, n) {
t = r[t];
if (!t) return n;
t = t[n];
return t || n;
}
return (
(r.getMessageInterpreter = function (i, o) {
function e(r, t, n) {
var e, u;
return f(
((e = r),
(u = o),
((n = (r = i)[(n = n)]) && (n = n[e])) || c(r, u, e)),
t
);
}
return function (r, t, n) {
return 2 === arguments.length
? e(r, null, t)
: 3 !== arguments.length
? ""
: e(r, t, n);
};
}),
r
);
})({});

module.exports.t = Interpreter.getMessageInterpreter(
translations,
fallbackLocale
);
其实搞这么麻烦构建方式主要是为了生成这个wxs文件,我们之所以能在vue中看到{{t('key')}}的方式进行国际化,是因为wxml层本身支持函数调用且函数可以调用外部资源(国际化文件),而小程序只允许通过wxs(官方文档)的方式在页面使用函数,出于性能考虑又不允许wxs引用任何外部资源(除了其他的wxs),所以这个build最核心的诉求是把 国际化文件copy一份到wxs文件中。下面是wxs无法引用外部资源的官方说明

优化

我们国际化的核心诉求就是解决官方国际化问题同时保留他的优点,这里我们列下此次的目标

  •  路径灵活,不强依赖路径减少后期添加国际化的路径改动成本
  •  调试方便,和原始开发调试方式相同
  •  书写简洁,保持和vue一样的书写方式
2021-05-22 10-27-02.2021-05-22 10_28_39.gif

wxml代码

<wxs src="../wxs/i18n.wxs" module="i18n" />
<!-- 标题国际化 -->
<page-meta>
<navigation-bar title="{{i18n.t(locales['主页'])}}" />
</page-meta>
<!-- 一般国际化 -->
<view>{{i18n.t(locales['通往爱人家里的路总不会漫长。'])}}</view>
<!-- js国际化 -->
<view>{{jsMsg}}</view>
<!-- 支持变量国际化 -->
<view>{{i18n.t(locales['当前页面:{path}'],[{key:'path',value:'home-page/home-page'}])}}</view>
<!-- 切换中英文按钮 -->
<button bindtap="zhClick" type="default">{{i18n.t(locales['切换中文'])}}</button>
<button bindtap="enClick" type="warn">{{i18n.t(locales['切换英语'])}}</button>

js代码

const i18n = require('../behaviors/i18n');
// home-page/home-page.js
Component({
behaviors: [i18n],
/**
* 组件的初始数据
*/
data: {
},
/**
* 组件的方法列表
*/
methods: {
zhClick() {
this.switchLanguage('zh_CN')
},
enClick() {
this.switchLanguage('en_US')
},
}
})

基本维持和官方相同的写法,尽可能少的代码写入

代码段链接

解决思路

利用behaviors(官方文档)将国际化文案进行引入每个页面。然后将所有国际化数据和key值以参数的形式传递给wxs函数,这样就可以避开wxs外部资源限制实现和vue i18n相同的效果。



  • behaviors负责将国际化方法和文案导入全局,以下是behaviors/i18n.js源码:

// behaviors/i18n.js

const {
t
} = require('../utils/index')
const i18n = Behavior({
data: {
language:{}, // 当前语种
locales: {}, // 当前语言的全部国际化信息
},
pageLifetimes: {
// 每次页面打开拉取对应语言国际化数据
show() {
if (this.data.language === 'en_US') {
this.setData({
locales: require('../i18n/en_US')
})
} else {
this.setData({
locales: require('../i18n/zh_CN')
})
}
}
},
methods: {
// 全局js国际化便捷调用
$t(key, option) {
return t(key, option)
},
// 由于tab只能通过js修改,所以每次语言切换需要重新更新tab国际化内容
refreshTab() {
wx.setTabBarItem({
index: 0,
text: this.data.locales['主页']
})
wx.setTabBarItem({
index: 1,
text: this.data.locales['我的']
})
},
// 切换语种
switchLanguage(language) {
this.setData({
language
})
if (language === 'zh_CN') {
this.setData({
locales: require('../i18n/zh_CN')
})
} else {
this.setData({
locales: require('../i18n/en_US')
})
}
// 切换下方tab
this.refreshTab()
},
}
})

module.exports = i18n
wxs负责为wxml层提供国际化方法,此处逻辑比较简单,先找到国际化文件中key值对应的语句,然后根据第二参数(arr)将变量进行替换,此处替换逻辑比较粗暴,使用{key}方式代表变量。如:"我的年龄是{age}",age代表变量,参数传递格式为 [{key:'xxx',value:'xxxx'}]

// 国际化.js
{
"ageText":"my age is {age}",
}
// wxml
<view>{{i18n.t(locales['ageText'],[{key:'age',value:'18'}])}}</view>

wxs源码如下:

var i18n = {
t: function (str, arr) {
var result = str;
if (arr) {
arr.forEach(function (item) {
if(result){
result = result.replace('{'+item.key+'}', item.value)
}
})
}
return result
}
}
module.exports = i18n

同时提供一个在js里获取国际化的util方法

// 国际化
const t = (key, option = {}) => {
const language = wx.getStorageSync('language');
let locales = null
if (language === 'en_US') {
locales = require('../i18n/en_US')
} else {
locales = require('../i18n/zh_CN')
}
let result = locales[key]
for (let optionKey in option) {
result = result.replace(`{${optionKey}}`, option[optionKey])
}
return result
}

module.exports = {
t
}

这样就基本实现了同时在wxml,js中进行国际化的基本需求,同时也解决了官方调试体验不足的缺点。

不足




  • 其实调试体验还不是那么完美,由于只有在show中初始化国际化文件内容,所以当开启“热重载”时对国际化文件进行修改,国际化内容不会自动进行更新




  • 每个page的js文件都需要引入behaviors/i18n.js,wxml文件引入<wxs src="../wxs/i18n.wxs" module="i18n" />有些略显繁琐




  • require('../i18n/xxx')整个项目引入了3遍略显繁琐,可以封装一下




  • i18n.t(locales['key']) 其中locals每次都要写一遍比较繁琐,不过由于wxs的限制也想不到太好的方法

使用建议


由于只是写一个demo很多逻辑并没有写完整,所以如果使用的话需要根据项目进行修改




  1. 如果i18n路径和命名方式不同需要同时修改behaviors/i18n.js,以及utils/index.js中的路径




  2. 现在每次刷新页面国际化都会被重置成中文,建议在behaviors/i18n.js的show方法中从全局获取当前的国际化语言。这样就不会每次都被重置成中文了。


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


收起阅读 »