注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android一个专注于App更新,一键傻瓜式集成App版本升级的开源库!

AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版本升级的轻量开源库。(无需担心通知栏适配;无需担心重复点击下载;无需担心App安装等问题;这些AppUpdater都已帮您处理好。) 核心库主要包括app-update...
继续阅读 »




AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版本升级的轻量开源库。(无需担心通知栏适配;无需担心重复点击下载;无需担心App安装等问题;这些AppUpdater都已帮您处理好。) 核心库主要包括app-updater和app-dialog。

下载更新和弹框提示分开,是因为这本来就是两个逻辑。完全独立开来能有效的解耦。

  • app-updater 主要负责后台下载更新App,无需担心下载时各种配置相关的细节,一键傻瓜式升级。
  • app-dialog 主要是提供常用的Dialog和DialogFragment,简化弹框提示,布局样式支持自定义。

app-updater + app-dialog 配合使用,谁用谁知道。

功能介绍

  •  专注于App更新一键傻瓜式升级
  •  够轻量,体积小
  •  支持监听下载过程
  •  支持下载失败,重新下载
  •  支持下载优先取本地缓存
  •  支持通知栏提示内容和过程全部可配置
  •  支持Android Q(10)
  •  支持取消下载
  •  支持使用OkHttpClient下载

Gif 展示

Image

引入

Maven:

    //app-updater
<dependency>
<groupId>com.king.app</groupId>
<artifactId>app-updater</artifactId>
<version>1.0.10</version>
<type>pom</type>
</dependency>

//app-dialog
<dependency>
<groupId>com.king.app</groupId>
<artifactId>app-dialog</artifactId>
<version>1.0.10</version>
<type>pom</type>
</dependency>

Gradle:


//----------AndroidX 版本
//app-updater
implementation 'com.king.app:app-updater:1.0.10-androidx'
//app-dialog
implementation 'com.king.app:app-dialog:1.0.10-androidx'

//----------Android Support 版本
//app-updater
implementation 'com.king.app:app-updater:1.0.10'
//app-dialog
implementation 'com.king.app:app-dialog:1.0.10'

Lvy:

    //app-updater
<dependency org='com.king.app' name='app-dialog' rev='1.0.10'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>

//app-dialog
<dependency org='com.king.app' name='app-dialog' rev='1.0.10'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
    allprojects {
repositories {
//...
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

    //一句代码,傻瓜式更新
new AppUpdater(getContext(),url).start();
    //简单弹框升级
AppDialogConfig config = new AppDialogConfig(context);
config.setTitle("简单弹框升级")
.setOk("升级")
.setContent("1、新增某某功能、\n2、修改某某问题、\n3、优化某某BUG、")
.setOnClickOk(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AppUpdater.Builder()
.setUrl(mUrl)
.build(getContext())
.start();
AppDialog.INSTANCE.dismissDialog();
}
});
AppDialog.INSTANCE.showDialog(getContext(),config);
    //简单DialogFragment升级
AppDialogConfig config = new AppDialogConfig(context);
config.setTitle("简单DialogFragment升级")
.setOk("升级")
.setContent("1、新增某某功能、\n2、修改某某问题、\n3、优化某某BUG、")
.setOnClickOk(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AppUpdater.Builder()
.setUrl(mUrl)
.setFilename("AppUpdater.apk")
.build(getContext())
.setHttpManager(OkHttpManager.getInstance())//不设置HttpManager时,默认使用HttpsURLConnection下载,如果使用OkHttpClient实现下载,需依赖okhttp库
.start();
AppDialog.INSTANCE.dismissDialogFragment(getSupportFragmentManager());
}
});
AppDialog.INSTANCE.showDialogFragment(getSupportFragmentManager(),config);

更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档

混淆

app-updater Proguard rules

app-dialog Proguard rules

代码下载:AppUpdater.zip

收起阅读 »

一个支持可拖动多边形,可拖动多边形的角改变其形状的任意多边形控件

DragPolygonViewDragPolygonView for Android 是一个支持可拖动多边形,支持通过拖拽多边形的角改变其形状的任意多边形控件。特性说明 支持添加多个任意多边形 支持通过触摸多边形拖动改变其位置 支...
继续阅读 »


DragPolygonView

DragPolygonView for Android 是一个支持可拖动多边形,支持通过拖拽多边形的角改变其形状的任意多边形控件。

特性说明

  •  支持添加多个任意多边形
  •  支持通过触摸多边形拖动改变其位置
  •  支持通过触摸多边形的角改变其形状
  •  支持点击、长按、改变等事件监听
  •  支持多边形单选或多选模式

Gif 展示

Image

DragPolygonView 自定义属性说明

属性值类型默认值说明
dpvStrokeWidthfloat4画笔描边的宽度
dpvPointStrokeWidthMultiplierfloat1.0绘制多边形点坐标时基于画笔描边的宽度倍数
dpvPointNormalColorcolor#FFE5574C多边形点的颜色
dpvPointPressedColorcolor多边形点按下状态时的颜色
dpvPointSelectedColorcolor多边形点选中状态时的颜色
dpvLineNormalColorcolor#FFE5574C多边形边线的颜色
dpvLinePressedColorcolor多边形边线按下状态的颜色
dpvLineSelectedColorcolor多边形边线选中状态的颜色
dpvFillNormalColorcolor#3FE5574C多边形填充的颜色
dpvFillPressedColorcolor#7FE5574C多边形填充按下状态时的颜色
dpvFillSelectedColorcolor#AFE5574C多边形填充选中状态时的颜色
dpvAllowableOffsetsdimension16dp触点允许的误差偏移量
dpvDragEnabledbooleantrue是否启用拖动多边形
dpvChangeAngleEnabledbooleantrue是否启用多边形的各个角的角度支持可变
dpvMultipleSelectionbooleanfalse是否是多选模式,默认:单选模式
dpvClickToggleSelectedbooleanfalse是否点击就切换多边形的选中状态
dpvAllowDragOutViewbooleanfalse是否允许多边形拖出视图范围
dpvTextSizedimension16sp是否允许多边形拖出视图范围
dpvTextNormalColorcolor#FFE5574C多边形文本的颜色
dpvTextPressedColorcolor多边形文本按下状态的颜色
dpvTextSelectedColorcolor多边形文本选中状态的颜色
dpvShowTextbooleantrue是否显示多边形的文本
dpvFakeBoldTextbooleanfalse多边形Text的字体是否为粗体

引入

Maven:

<dependency>
<groupId>com.king.view</groupId>
<artifactId>dragpolygonview</artifactId>
<version>1.0.2</version>
<type>pom</type>
</dependency>

Gradle:

implementation 'com.king.view:dragpolygonview:1.0.2'

Lvy:

<dependency org='com.king.view' name='dragpolygonview' rev='1.0.2'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

布局示例

    <com.king.view.dragpolygonview.DragPolygonView
android:id="@+id/dragPolygonView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

代码示例

    //添加多边形
dragPolygonView.addPolygon(Polygon polygon);
//添加多边形(多边形的各个点)
dragPolygonView.addPolygon(PointF... points);
//根据位置将多边形改为选中状态
dragPolygonView.setPolygonSelected(int position);
//改变监听
dragPolygonView.setOnChangeListener(OnChangeListener listener);
//点击监听
dragPolygonView.setOnPolygonClickListener(OnPolygonClickListener listener);
//长按监听
dragPolygonView.setOnPolygonLongClickListener(OnPolygonLongClickListener listener)

更多使用详情,请查看app中的源码使用示例

代码下载:jenly1314-DragPolygonView-master.zip

收起阅读 »

iOS崩溃统计原理 & 日志分析整理

简介当应用崩溃时,会产生崩溃日志并且保存在设备上。崩溃日志描述了应用结束时所处的环境信息,通常包含完整的线程堆栈追溯信息,这些数据对于调试应用错误非常有帮助。包含追溯信息的崩溃日志在分析前需要进行符号化。符号化将内存地址替换为更直观的函数名以及行数。崩溃原因崩...
继续阅读 »

简介

当应用崩溃时,会产生崩溃日志并且保存在设备上。崩溃日志描述了应用结束时所处的环境信息,通常包含完整的线程堆栈追溯信息,这些数据对于调试应用错误非常有帮助。
包含追溯信息的崩溃日志在分析前需要进行符号化。符号化将内存地址替换为更直观的函数名以及行数。

崩溃原因

崩溃是指应用产生了系统不允许的行为时,系统终止其运行导致的现象。崩溃发生的原因有:

1、存在CPU无法运行的代码
不存在或者无法执行
2、操作系统执行某项策略,终止程序
启动时间过长或者消耗过多内存时,操作系统会终止程序运行
3、编程语言为了避免错误终止程序:抛出异常
4、开发者为了避免失败终止程序:Assert

产生崩溃日志

在程序出现以上问题时,系统会抛出异常,结束程序:
出现异常情况,终止程序:


分析崩溃日志

在发生崩溃时,会产生崩溃日志并且保存在设备上,用于后期对问题定位,崩溃日志的内容包括以下部分:程序信息、异常信息、崩溃堆栈、二进制镜像。下面对每部分进行说明。

崩溃日志程序信息:

Incident Identifier: B6FD1E8E-B39F-430B-ADDE-FC3A45ED368C
CrashReporter Key: f04e68ec62d3c66057628c9ba9839e30d55937dc
Hardware Model: iPad6,8
Process: TheElements [303]
Path: /private/var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
Identifier: com.example.apple-samplecode.TheElements
Version: 1.12
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: com.example.apple-samplecode.TheElements [402]

Date/Time: 2016-08-22 10:43:07.5806 -0700
Launch Time: 2016-08-22 10:43:01.0293 -0700
OS Version: iPhone OS 10.0 (14A5345a)
Report Version: 104

汇总部分包含崩溃发生环境的基本信息:

1、Incident Identifier:日志ID。

2、CrashReport Key:设备匿名ID,同一设备的崩溃日志该值相同。

3、Beta Identifier:设备和崩溃应用组合ID。

4、Process:执行程序名,等同CFBundleExecutable。

5、Version:程序版本号,等同CFBundleVersion/CFBundleVersionString。

6、Code type:程序构造:ARM-64、ARM、x86

异常信息:

Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000000
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [0]
Triggered by Thread: 0

异常信息:

1、Exception Codes:使用十六进制表示的程序特定信息,一般不展示。

2、Exception Subtype:易读(相比十六进制地址)的异常信息。

3、Exception Message:异常的额外信息。

4、Exception Note:不特指某种异常类型的额外信息。

5、Termination Reason:程序终止的异常信息。

6、Triggered Thread:异常发生时的线程。

崩溃堆栈:

Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 TheElements 0x000000010006bc20 -[AtomicElementViewController myTransitionDidStop:finished:context:] (AtomicElementViewController.m:203)
1 UIKit 0x0000000194cef0f0 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 312
2 UIKit 0x0000000194ceef30 -[UIViewAnimationState animationDidStop:finished:] + 160
3 QuartzCore 0x0000000192178404 CA::Layer::run_animation_callbacks(void*) + 260
4 libdispatch.dylib 0x000000018dd6d1c0 _dispatch_client_callout + 16
5 libdispatch.dylib 0x000000018dd71d6c _dispatch_main_queue_callback_4CF + 1000
6 CoreFoundation 0x000000018ee91f2c __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
7 CoreFoundation 0x000000018ee8fb18 __CFRunLoopRun + 1660
8 CoreFoundation 0x000000018edbe048 CFRunLoopRunSpecific + 444
9 GraphicsServices 0x000000019083f198 GSEventRunModal + 180
10 UIKit 0x0000000194d21bd0 -[UIApplication _run] + 684
11 UIKit 0x0000000194d1c908 UIApplicationMain + 208
12 TheElements 0x00000001000653c0 main (main.m:55)
13 libdyld.dylib 0x000000018dda05b8 start + 4

Thread 1:
0 libsystem_kernel.dylib 0x000000018deb2a88 __workq_kernreturn + 8
1 libsystem_pthread.dylib 0x000000018df75188 _pthread_wqthread + 968
2 libsystem_pthread.dylib 0x000000018df74db4 start_wqthread + 4

...

第一行列出了线程信息以及所在队列,之后是追溯链中独立栈帧的详细信息:

1、栈帧号。栈帧号为0的代表当前执行停顿的函数,1则是调用当前停顿函数的主调函数,即0为1的被调函数,1为0的主调函数,以此类推。
2、执行函数所在的二进制包
3、地址信息:对于0栈帧来说,代表当前执行停顿的地址。其他栈帧则是获取控制权后接下来执行的地址。
4、函数名

二进制镜像:

Binary Images:
0x100060000 - 0x100073fff TheElements arm64 <2defdbea0c873a52afa458cf14cd169e> /var/containers/Bundle/Application/888C1FA2-3666-4AE2-9E8E-62E2F787DEC1/TheElements.app/TheElements
...

日之内包含多个二进制镜像,每个二进制镜像内包含以下信息:

1、二进制镜像在程序内的地址空间
2、二进制的名称或者bundleID
3、二进制镜像的架构信息 arm64等
4、二进制镜像的UUID,每次构建都会改变,该值用于在符号化日志时定位对应的dSYM文件。
5、磁盘上的二进制路径

符号化

app.xcarchive文件,包内容包含dSYM和应用的二进制文件。
更精确的符号化,可以结合崩溃日志、项目二进制文件、dSYM文件,对其进行反汇编,从而获得更详细的信息。

符号化就是将追溯的地址信息转换成函数名及行数等信息,便于研发人员定位问题。
当程序结束运行时,会产生崩溃日志,日志内包含每个线程的堆栈信息。当我们使用Xcode进行调试时,崩溃或者断点信息都会展示出实例和方法名等信息(符号信息)。相反,当应用被发布后,符号信息并不会包含在应用的二进制文件中,所以服务端收到的是未符号化的包含十六进制地址信息的日志文件。
查看本机崩溃日志步骤如下:

1、将手机连接到Mac
2、启动Xcode->Window->Devices and simulators
3、选择View Device Logs

选择左侧应用,之后就可以在右侧看到崩溃日志信息:


日志内包含符号化内容-[__NSArrayI objectAtIndex:]和十六进制地址0x000db142 0xb1000 + 172354。这种日志类型成为部分符号化崩溃日志。
部分符号化的原因在于,Xcode只能符号化系统组件,例如UIKit、CoreFoundation等。但是对于非系统库产生的崩溃,在没有符号表的情况下就无法符号化。
分析第三行未符号化的代码:

0x000db142 0xb1000 + 172354

以上内容说明了崩溃发生在内存地址0x000db142,此地址和0xb1000 + 172354是相等的。0xb1000代表这部分许的起始地址,172354代表偏移位。

崩溃日志类型:

崩溃日志可能包含几种状态:未符号化、完全符号化、部分符号化。
未符号化的崩溃日志追溯链中没有函数的名字等信息,而是二进制镜像执行代码的十六进制地址。
完全符号化的崩溃日志中,所有的十六进制地址都被替换为对应的函数符号。

符号化流程

符号化需要两部分内容:崩溃的二进制代码和编译产生的对应dSYM。

符号表

当编译器将源码转换为机器码时,会生成一个调试符号表,表内是二进制结构到原始源码的映射关系。调试符号表保存在dSYM(debug symbol调试符号表)文件内。调试模式下符号表会保存在编译的二进制内,发布模式则将符号表保存在dSYM文件内用于减少包的体积。

当崩溃发生时,会在设备存储一份未符号化的崩溃日志
获取崩溃日志后,通过dSYM对追溯链中的每个地址进行符号化,转换为函数信息,产生的结果就是符号化后的崩溃日志。

函数调用堆栈

我们知道,崩溃日志内包含函数调用的追溯信息,明白堆栈是怎么产生的有利于我们理解和分析崩溃日志。


函数调用是在栈进行的,函数从调用和被调用方分为:主调函数和被调函数,这次我们只讨论每个函数在栈中的几个核心部分:

1、上一个函数(主调函数)的堆栈信息。
2、入参。
3、局部变量。

入参和局部变量容易理解,下面讨论为什么要保存主调函数的堆栈信息。
说到这点就需要聊到寄存器。

寄存器


寄存器的类型和基本功能:

eax:累加寄存器,用于运算。
ebx:基址寄存器,用于地址索引。
ecx:计数寄存器,用于计数。
edx:数据寄存器,用于数据传递。
esi:源变址寄存器,存放相对于DS段之源变址指针。
edi:目的变址寄存器,存放相对于ES段之目的的变址指针。
esp:堆栈指针,指向当前堆栈位置。
ebp:基址指针寄存器,相对基址位置。

寄存器约定

背景:

1、所有函数都可以访问和操作寄存器,寄存器对于单个CPU来说数量是固定的
2、单个CPU来说,某一时刻只有一个函数在执行
3、需要保证函数调用其他函数时,被调函数不会修改或覆盖主调函数稍后使用的寄存器值

被调函数在执行时,需要使用寄存器来保存数据和执行计算,但是在被调函数完成时,需要把寄存器还原,用于主调函数的执行,所以出现了寄存器约定。

约定内容:

1、主调函数的保存寄存器,在唤起被调函数前,需要显示的将其保存在栈中。
主调寄存器:%eax、%edx、%ecx
2、被调函数的保存寄存器,使用前压栈,并在函数返回前从栈中恢复原值。
被调寄存器:%ebx、%esi、%edi
3、被调函数必须保存%ebp和%esp,并在函数返回后恢复调用前的值。

遵守寄存器约定的函数堆栈调用

了解了寄存器功能和寄存器约定后,我们再看函数调用堆栈:


1、栈帧逻辑:栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。
2、保存栈帧:被调函数必须保持寄存器%ebp和%esp,并在函数返回后将其恢复到调用前的值,亦即必须恢复主调函数的栈帧。
3、回溯:所以获取到崩溃时线程的ebp和esp 就能回溯到上一个调用,依次类推,回溯出所有的调用堆栈

总结

通过以上内容,我们了解了崩溃日志产生原理、崩溃日志内容和崩溃日志分析,下面分享几个分析崩溃日志的小提示作为结束:

1、不止关注崩溃本行,结合上下文进行分析。
2、不止关注崩溃线程,要结合其他线程的堆栈信息。
3、通过多个崩溃日志,组合分析。
4、使用地址定位和野指针工具重现内存问题。

参考资料

Apple Understanding and Analyzing Application Crash Reports
Overview Of iOS Crash Reporting Tools
Understanding Crashes and Crash Logs Video

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

收起阅读 »

如何构建优雅的ViewController

前言关于ViewController讨论的最多的是它的肥胖和臃肿,但是哪怕是采用MVC模式,ViewController同样可以写的很优雅,这无关乎设计模式,对于那些以设计模式论高低的,我只能呵呵。其实这关乎的是你对设计模式的理解有多深,你对于职责划分的认知是...
继续阅读 »

前言

关于ViewController讨论的最多的是它的肥胖和臃肿,但是哪怕是采用MVC模式,ViewController同样可以写的很优雅,这无关乎设计模式,对于那些以设计模式论高低的,我只能呵呵。其实这关乎的是你对设计模式的理解有多深,你对于职责划分的认知是否足够清晰。ViewController也从很大程度上反应一个程序员的真实水平,一个平庸的程序员他的ViewController永远是臃肿的、肥胖的,什么功能都可以往里面塞,不同功能间缺乏清晰的界限。而一个优秀的程序员它的ViewController显得如此优雅,让你产生一种竟不能修改一笔一画的感觉。

ViewController职责

1、UI 属性 和 布局
2、用户交互事件
3、用户交互事件处理和回调

用户交互事件处理: 通常会交给其他对象去处理

回调: 可以根据具体的设计模式和应用场景交给 ViewController 或者其他对象处理

而通常我们在阅读别人ViewController代码的时候,我们关注的是什么?

控件属性配置在哪里?
用户交互的入口位置在哪里?
用户交互会产生什么样的结果?(回调在哪里?)
所以从这个角度来说,这三个功能一开始就应该是被分离的,需要有清新明确的界限。因为谁都不希望自己在查找交互入口的时候 ,去阅读一堆控件冗长的控件配置代码, 更不愿意在一堆代码去慢慢理清整个用户交互的流程。 我们通常只关心我当前最关注的东西,当看到一堆无关的代码时,第一反应就是我想注释掉它。

基于协议分离UI属性的配置

protocol MFViewConfigurer {
var rootView: UIView { get }
var contentViews: [UIView] { get }
var contentViewsSettings: [() -> Void] { get }

func addSubViews()
func configureSubViewsProperty()
func configureSubViewsLayouts()

func initUI()
}

依赖这个协议就可以完成所有控件属性配置,然后通过extension protocol 大大减少重复代码,同时提高可读性

extension MFViewConfigurer {
func addSubViews() {
for element in contentViews {
if let rootView = rootView as? UIStackView {
rootView.addArrangedSubview(element)
} else {
rootView.addSubview(element)
}
}
}

func configureSubViewsProperty() {
for element in contentViewsSettings {
element()
}
}

func configureSubViewsLayouts() {
}

func initUI() {
addSubViews()
configureSubViewsProperty()
configureSubViewsLayouts()
}
}

这里 我将控件的添加和控件的配置分成两个函数addSubViews和configureSubViewsProperty, 因为在我的眼里函数就应该遵循单一职责这个概念:
addSubViews: 明确告诉阅读者,我这个控制器只有这些控件
configureSubViewsProperty: 明确告诉阅读者,控件的所有属性配置都在这里,想要修改属性请阅读这个函数

来看一个实例:

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.

// 初始化 UI
initUI()

// 绑定用户交互事件
bindEvent()

// 将ViewModel.value 绑定至控件
bindValueToUI()

}

// MARK: - UI configure

// MARK: - UI

extension MFWeatherViewController: MFViewConfigurer {
var contentViews: [UIView] { return [scrollView, cancelButton] }

var contentViewsSettings: [() -> Void] {
return [{
self.view.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.7)
self.scrollView.hiddenSubViews(isHidden: false)
}]
}

func configureSubViewsLayouts() {
cancelButton.snp.makeConstraints { make in
if #available(iOS 11, *) {
make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top)
} else {
make.top.equalTo(self.view.snp.top).offset(20)
}

make.left.equalTo(self.view).offset(20)
make.height.width.equalTo(30)
}

scrollView.snp.makeConstraints { make in
make.top.bottom.left.right.equalTo(self.view)
}
}

}


而对于UIView 这套协议同样适用

```Swift
// MFWeatherSummaryView
private override init(frame: CGRect) {
super.init(frame: frame)

initUI()
}


// MARK: - UI

extension MFWeatherSummaryView: MFViewConfigurer {
var rootView: UIView { return self }

var contentViews: [UIView] {
return [
cityLabel,
weatherSummaryLabel,
temperatureLabel,
weatherSummaryImageView,
]
}

var contentViewsSettings: [() -> Void] {
return [UIConfigure]
}

private func UIConfigure() {
backgroundColor = UIColor.clear
}

public func configureSubViewsLayouts() {
cityLabel.snp.makeConstraints { make in
make.top.centerX.equalTo(self)
make.bottom.equalTo(temperatureLabel.snp.top).offset(-10)
}

temperatureLabel.snp.makeConstraints { make in
make.top.equalTo(cityLabel.snp.bottom).offset(10)
make.right.equalTo(self.snp.centerX).offset(0)
make.bottom.equalTo(self)
}

weatherSummaryImageView.snp.makeConstraints { make in
make.left.equalTo(self.snp.centerX).offset(20)
make.bottom.equalTo(temperatureLabel.snp.lastBaseline)
make.top.equalTo(weatherSummaryLabel.snp.bottom).offset(5)
make.height.equalTo(weatherSummaryImageView.snp.width).multipliedBy(61.0 / 69.0)
}

weatherSummaryLabel.snp.makeConstraints { make in
make.top.equalTo(temperatureLabel).offset(20)
make.centerX.equalTo(weatherSummaryImageView)
make.bottom.equalTo(weatherSummaryImageView.snp.top).offset(-5)
}
}
}

由于我使用的是MVVM模式,所以viewDidLoad 和MVC模式还是有些区别,如果是MVC可能就是这样

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.

// 初始化 UI
initUI()

// 用户交互事件入口
addEvents()


}

// MARK: callBack
......

由于MVC的回调模式很难统一,有Delegate, closure, notification 等等,所以回调通常会散落在控制器各个角落。最好加个MARK flag, 尽量收集在同一个区域中, 同时对于每个回调加上必要的注释:

1、由哪种操作触发
2、会导致什么后果
3、最终会留下哪里

所以从这个角度来说UITableViewDataSource 和 UITableViewDelegate 完全是两种不一样的行为, 一个是 configure UI , 一个是 control behavior , 所以不要在把这两个东西写一块了, 真的很难看。

总结

基于职责对代码进行分割,这样会让你的代码变得更加优雅简洁,会大大减少一些万金油代码的出现,减少阅读代码的成本也是我们优化的一个方向,比较谁都不想因为混乱的代码影响自己的心情

链接:https://www.jianshu.com/p/266cbca1439c

收起阅读 »

【面试专题】Android屏幕刷新机制

这个问题在其他人整理的面试宝典中也有提及,一般来说都是问View的刷新,基本上从ViewRootImpl的scheduleTraversals()方法开始讲就可以了。之前看别人面试斗鱼的面经,被问到了Android屏幕刷新机制、双缓冲、三缓冲、黄油计划,然后我...
继续阅读 »

这个问题在其他人整理的面试宝典中也有提及,一般来说都是问View的刷新,基本上从ViewRootImpl的scheduleTraversals()方法开始讲就可以了。之前看别人面试斗鱼的面经,被问到了Android屏幕刷新机制、双缓冲、三缓冲、黄油计划,然后我面网易云的时候也确实被问到了这个题目。


屏幕刷新这一整套,你把我这篇文章里的内容讲清楚了,肯定ok了。网易云还附加问了我CPU和GPU怎么交换绘制数据的,这个我个人认为完全是加分题了,我答不出来,感兴趣的小伙伴可以去看一看,你要是能说清楚,肯定能让面试官眼前一亮。


双缓冲


在讲双缓冲这个概念之前,先来了解一些基础知识。


显示系统基础


在一个典型的显示系统中,一般包括CPU、GPU、Display三个部分, CPU负责计算帧数据,把计算好的数据交给GPU, GPU会对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)里存起来,然后Display(屏幕或显示器)负责把buffer里的数 据呈现到屏幕上。



  • 画面撕裂


屏幕刷新频是固定的,比如每16.6ms从buffer取数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一 帧,显示器显示一帧。但是CPU/GPU写数据是不可控的,所以会出现buffer里有些数据根本没显示出来就被重写了,即 buffer里的数据可能是来自不同的帧的。当屏幕刷新时,此时它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,即出现画面撕裂。


简单说就是Display在显示的过程中,buffer内数据被CPU/GPU修改,导致画面撕裂。


那咋解决画面撕裂呢? 答案是使用双缓冲。


双缓冲


由于图像绘制和屏幕读取 使用的是同个buffer,所以屏幕刷新时可能读取到的是不完整的一帧画面。


双缓冲,让绘制和显示器拥有各自的buffer:GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。


VSync


什么时候进行两个buffer的交换呢?


假如是 Back buffer准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。 看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。


当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)。这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现画面撕裂的状况。


VSync(垂直同步)是VerticalSynchronization的简写,它利用VBI时期出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。


所以说VSync这个概念并不是Google首创的,它在早年的PC机领域就已经出现了。


Android屏幕刷新机制


先总体概括一下,Android屏幕刷新使用的是“双缓存+VSync机制”,单纯的双缓冲模式容易造成jank(丢帧)现象,为了解决这个问题,Google在 Android4.1 提出了Project Butter(?油工程),引入了 drawing with VSync 的概念。


jank(丢帧)


VSync.jpeg


以时间的顺序来看下将会发生的过程:



  1. Display显示第0帧数据,此时CPU和GPU渲染第1帧画面,且在Display显示下一帧前完成

  2. 因为渲染及时,Display在第0帧显示完成后,也就是第1个VSync后,缓存进行交换,然后正常显示第1帧

  3. 接着第2帧开始处理,是直到第2个VSync快来前才开始处理的。

  4. 第2个VSync来时,由于第2帧数据还没有准备就绪,缓存没有交换,显示的还是第1帧。这种情况被Android开发组命名为“Jank”,即发生了丢帧。

  5. 当第2帧数据准备完成后,它并不会?上被显示,而是要等待下一个VSync 进行缓存交换再显示。


所以总的来说,就是屏幕平白无故地多显示了一次第1帧。 原因是第2帧的CPU/GPU计算 没能在VSync信号到来前完成。


这里注意一下一个细节,jank(丢帧、掉帧),不是说这一帧丢弃了不显示,而是这一帧延迟显示了,因为缓存交换的时机只能等下一个VSync了。


黄油计划 —— drawing with VSync


为了优化显示性能,Google在Android 4.1系统中对Android Display系统进行了重构,实现了Project Butter(?油工程): 系统在收到VSync pulse后,将?上开始下一帧的渲染。即一旦收到VSync通知(16ms触发一次),CPU和GPU 才立刻开 始计算然后把数据写入buffer。如下图:


VSync2.jpeg


CPU/GPU根据VSYNC信号同步处理数据,可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。 一句话总结,VSync同步使得CPU/GPU充分利用了16.6ms时间,减少jank。


问题又来了,如果界面比较复杂,CPU/GPU的处理时间较?,超过了16.6ms呢?如下图:


VSync3.jpeg



  1. 在第二个时间段内,但却因 GPU 还在处理 B 帧,缓存没能交换,导致 A 帧被重复显示。

  2. 而B完成后,又因为缺乏VSync pulse信号,它只能等待下一个signal的来临。于是在这一过程中,有一大段时间是被浪费的。

  3. 当下一个VSync出现时,CPU/GPU?上执行操作(A帧),且缓存交换,相应的显示屏对应的就是B。这时看起来就是正常的。只不过由于执行时间仍然超过16ms,导致下一次应该执行的缓冲区交换又被推迟了——如此循环反复,便出现了越来越多的“Jank”。


为什么 CPU 不能在第二个 16ms 处理绘制工作呢? 因为只有两个 buffer,Back buffer正在被GPU用来处理B帧的数据, Frame buffer的内容用于Display的显示,这样两个 buffer都被占用,CPU 则无法准备下一帧的数据。 那么,如果再提供一个buffer,CPU、GPU 和显示设备都能使用各自的 buffer工作,互不影响。这就是三缓冲的来源了。


三缓冲


三缓存就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用空闲时间,带来的坏处是多使用的一个 Graphic Buffer 所占用的内存。


VSync4.jpeg



  1. 第一个Jank,是不可避免的。但是在第二个 16ms 时间段,CPU/GPU 使用 第三个 Buffer 完成C帧的计算,虽然还是 会多显示一次 A 帧,但后续显示就比较顺畅了,有效避免 Jank 的进一步加剧。

  2. 注意在第3段中,A帧的计算已完成,但是在第4个vsync来的时候才显示,如果是双缓冲,那在第三个vynsc就可以显示了。


三缓冲有效利用了等待VSync的时间,减少了jank,但是带来了延迟。是不是 Buffer 越多越好呢?这个是否定的, Buffer 正常还是两个,当出现 Jank 后三个足以。


Choreographer


上边讲的都是基础的刷新知识,那么在 Android 系统中,真正来实现绘制的类叫Choreographer


Choreographer负责对CPU/GPU绘制的指导 —— 收到VSync信号才开始绘制,保证绘制拥有完整 16.6ms,避免绘制的随机性。


通常 应用层不会直接使用Choreographer,而是使用更高级的API,例如动画和View绘制相关的 ValueAnimator.start()、View.invalidate()等。


(这边补充说一个面试题,属性动画更新时会回调onDraw吗?不会,因为它内部是通过AnimationHandler中的Choreographer机制来实现的更新,具体的逻辑,如果以后有时间的话可以写篇文章来说一说。)


业界一般通过Choreographer来监控应用的帧率。


(这个东西也是个面试题,会问你如何检测应用的帧率?你可以提一下Choreographer里面的FrameCallback,然后结合一些第三方库的实现具体说一下。)


View刷新的入口


Activity启动,走完onResume方法后,会进行window的添加。window添加过程会调用ViewRootImpl的setView()方法, setView()方法会调用requestLayout()方法来请求绘制布局,requestLayout()方法内部又会走到scheduleTraversals()方法。最后会走到performTraversals()方法,接着到了我们熟知的测量、布局、绘制三大流程了。


当我们使用 ValueAnimator.start()、View.invalidate()时,最后也是走到ViewRootImpl的 scheduleTraversals()方法。(View.invalidate()内部会循环获取ViewParent直到ViewRootImpl的invalidateChildInParent()方法,然后走到scheduleTraversals(),可自行查看源码)


即所有UI的变化都是走到ViewRootImpl的scheduleTraversals()方法。


这里注意一个点:scheduleTraversals()之后不是立即就执行performTraversals()的,它们中间隔了一个Choreographer机制。简单来说就是scheduleTraversals()中,Choreographer会去请求native的VSync信号,VSync信号来了之后才会去调用performTraversals()方法进行View绘制的三大流程。



//ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//添加同步屏障,屏蔽同步消息,保证VSync到来立即执行绘制
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//mTraversalRunnable是TraversalRunnable实例,最终走到run(),也即doTraversal();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); ...
//开始三大绘制流程
performTraversals();
...
}
}


  1. postSyncBarrier 开启同步屏障,保证VSync到来后立即执行绘制

  2. mChoreographer.postCallback()方法,发送一个会在下一帧执行的回调,即在下一个VSync到来时会执行 TraversalRunnable–>doTraversal()—>performTraversals()–>绘制流程。


Choreographer


初始化


mChoreographer,是在ViewRootImpl的构造方法内使用 Choreographer.getInstance()创建。


Choreographer和Looper一样是线程单例的,通过ThreadLocal机制来保证唯一性。因为Choreographer内部通过FrameHandler来发送消息,所以初始化的时候会先判断当前线程有无Looper,没有的话直接抛异常。


public static Choreographer getInstance() {
return sThreadInstance.get();
}

private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper();
if (looper == null) {
//当前线程要有looper,Choreographer实例需要传入
throw new IllegalStateException("The current thread must have a looper!");
}
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer;
}
return choreographer;
}
};

postCallback


mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)方法,第一个参数是CALLBACK_TRAVERSAL,表示回调任务的类型,共有以下5种类型:


//输入事件,首先执行
public static final int CALLBACK_INPUT = 0;
//动画,第二执行
public static final int CALLBACK_ANIMATION = 1;
//插入更新的动画,第三执行
public static final int CALLBACK_INSETS_ANIMATION = 2;
//绘制,第四执行
public static final int CALLBACK_TRAVERSAL = 3;
//提交,最后执行,
public static final int CALLBACK_COMMIT = 4;

五种类型任务对应存入对应的CallbackQueue中,每当收到 VSYNC 信号时,Choreographer 将首先处理 INPUT 类型的任 务,然后是 ANIMATION 类型,最后才是 TRAVERSAL 类型。


postCallback()内部调用postCallbackDelayed(),接着又调用postCallbackDelayedInternal(),正常消息执行scheduleFrameLocked,延迟运行的消息会发送一个MSG_DO_SCHEDULE_CALLBACK类型的meessage:


private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis)
{
...
synchronized (mLock) {
...
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) { //立即执行
scheduleFrameLocked(now);
} else {
//延迟运行,最终也会走到scheduleFrameLocked()
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

FrameHandler这个类是内部专门用来处理消息的,可以看到延迟的MSG_DO_SCHEDULE_CALLBACK类型消息最终也是走到scheduleFrameLocked:


private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
// 执行doFrame,即绘制过程
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
//申请VSYNC信号,例如当前需要绘制任务时
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
//需要延迟的任务,最终还是执行上述两个事件
doScheduleCallback(msg.arg1);
break;
}
}
}

void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
scheduleFrameLocked(now);
}
}
}
}

申请VSync信号


scheduleFrameLocked()方法里面就会去真正的申请 VSync 信号了。


private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
//当前执行的线程,是否是mLooper所在线程
if (isRunningOnLooperThreadLocked()) {
//申请 VSYNC 信号
scheduleVsyncLocked();
} else {
// 若不在,就用mHandler发送消息到原线程,最后还是调用scheduleVsyncLocked方法
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);//异步
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
// 如果未开启VSYNC则直接doFrame方法(4.1后默认开启)
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);//异步
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}

VSync信号的注册和监听是通过mDisplayEventReceiver实现的。mDisplayEventReceiver是在Choreographer的构造方法中创建的,是FrameDisplayEventReceiver的实例。 FrameDisplayEventReceiver是 DisplayEventReceiver 的子类,


private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}

public DisplayEventReceiver(Looper looper, int vsyncSource) {
if (looper == null) {
throw new IllegalArgumentException("looper must not be null");
}
mMessageQueue = looper.getQueue();
// 注册native的VSYNC信号监听者
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,vsyncSource);
mCloseGuard.open("dispose");
}

VSync信号回调


native的VSync信号到来时,会走到onVsync()回调:


private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable
{

@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
...
//将本身作为runnable传入msg, 发消息后 会走run(),即doFrame(),也是异步消息
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}

(这里补充一个面试题:页面UI没有刷新的时候onVsync()回调也会执行吗?不会,因为VSync是UI需要刷新的时候主动去申请的,而不是native层不停地往上面去推这个回调的,这边要注意。)


doFrame


doFrame()方法中会通过doCallbacks()方法去执行各种callbacks,主要内容就是取对应任务类型的队列,遍历队列执行所有任务,其中就包括了 ViewRootImpl 发起的绘制任务mTraversalRunnable了。mTraversalRunnable执行doTraversal()方法,移除同步屏障,调用performTraversals()开始三大绘制流程。


到这里整个流程就闭环了。


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

java设计模式:备忘录模式

前言 备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。 定义 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。 ...
继续阅读 »


前言


备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。


定义


在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。
在这里插入图片描述


优点


提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。


缺点


资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。


结构



  • 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。

  • 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。

  • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。


实现


生活中最常用的计算器自己拥有备忘录的功能,用户计算完后软件会自动为用户记录最后几次的计算结果,我们可以模拟用户使用计算器的过程,以及打开备忘录查看记录。


package com.rabbit;

/**
* 备忘录发起人,模拟计算器加法运算
* Created by HASEE on 2018/4/29.
*/
public class Originator {

private double num1;

private double num2;

//创建备忘录对象
public Memento createMemento() {
return new Memento(num1, num2);
}

public Originator(double num1, double num2) {
this.num1 = num1;
this.num2 = num2;
System.out.println(num1 + " + " + num2 + " = " + (num1 + num2));
}

}
package com.rabbit;

/**
* 备忘录,要保存的属性
* Created by HASEE on 2018/4/29.
*/
public class Memento {

private double num1;//计算器第一个数字

private double num2;//计算器第二个数字

private double result;//计算结果

public Memento(double num1, double num2) {
this.num1 = num1;
this.num2 = num2;
this.result = num1 + num2;
}

public void show() {
System.out.println(num1 + " + " + num2 + " = " + result);
}

}
package com.rabbit;

import java.util.ArrayList;
import java.util.List;

/**
* 备忘录管理者
* Created by HASEE on 2018/4/29.
*/
public class Caretaker {

private List<Memento> mementos;

public boolean addMenento(Memento memento) {
if (mementos == null) {
mementos = new ArrayList<>();
}
return mementos.add(memento);
}

public List<Memento> getMementos() {
return mementos;
}

public static Caretaker newInstance() {
return new Caretaker();
}
}
package com.rabbit;

import org.junit.Test;

import java.util.Random;

/**
* Created by HASEE on 2018/4/29.
*/
public class Demo {

@Test
public void test() {
Caretaker c = Caretaker.newInstance();
//使用循环模拟用户使用计算器做加法运算
Random ran = new Random(1000);
for (int i = 0; i < 5; i++) {
//用户计算
Originator o = new Originator(ran.nextDouble(), ran.nextDouble());
//计算器软件将用户的计算做备份,以便可以查看历史
c.addMenento(o.createMemento());
}
System.out.println("---------------------用户浏览历史记录---------------------");
for (Memento m : c.getMementos()) {
m.show();
}
System.out.println("---------------------用户选择一条记录查看----------------------");
c.getMementos().get(2).show();
}

}

收起阅读 »

java设计模式:访问者模式

前言 访问者模式是一种将数据操作和数据结构分离的设计模式。 定义 将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构...
继续阅读 »


前言


访问者模式是一种将数据操作和数据结构分离的设计模式。


定义


将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。


优点



  1. 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。

  2. 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。

  3. 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。

  4. 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。


缺点



  1. 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。

  2. 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。

  3. 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。


结构



  • 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。

  • 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。

  • 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。

  • 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。

  • 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。


示例


年底,CEO和CTO开始评定员工一年的工作绩效,员工分为工程师和经理,CTO关注工程师的代码量、经理的新产品数量;CEO关注的是工程师的KPI和经理的KPI以及新产品数量。
由于CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的处理。访问者模式此时可以派上用场了。


// 员工基类
public abstract class Staff {

public String name;
public int kpi;// 员工KPI

public Staff(String name) {
this.name = name;
kpi = new Random().nextInt(10);
}
// 核心方法,接受Visitor的访问
public abstract void accept(Visitor visitor);
}

Staff 类定义了员工基本信息及一个 accept 方法,accept 方法表示接受访问者的访问,由子类具体实现。Visitor 是个接口,传入不同的实现类,可访问不同的数据。下面看看工程师和经理的代码:


// 工程师
public class Engineer extends Staff {

public Engineer(String name) {
super(name);
}

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
// 工程师一年的代码数量
public int getCodeLines() {
return new Random().nextInt(10 * 10000);
}
}
// 经理
public class Manager extends Staff {

public Manager(String name) {
super(name);
}

@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
// 一年做的产品数量
public int getProducts() {
return new Random().nextInt(10);
}
}

工程师是代码数量,经理是产品数量,他们的职责不一样,也就是因为差异性,才使得访问模式能够发挥它的作用。Staff、Engineer、Manager 3个类型就是对象结构,这些类型相对稳定,不会发生变化。
然后将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的 showReport 方法查看所有员工的业绩,具体代码如下:


// 员工业务报表类
public class BusinessReport {

private List<Staff> mStaffs = new LinkedList<>();

public BusinessReport() {
mStaffs.add(new Manager("经理-A"));
mStaffs.add(new Engineer("工程师-A"));
mStaffs.add(new Engineer("工程师-B"));
mStaffs.add(new Engineer("工程师-C"));
mStaffs.add(new Manager("经理-B"));
mStaffs.add(new Engineer("工程师-D"));
}

/**
* 为访问者展示报表
* @param visitor 公司高层,如CEO、CTO
*/
public void showReport(Visitor visitor) {
for (Staff staff : mStaffs) {
staff.accept(visitor);
}
}
}


下面看看 Visitor 类型的定义, Visitor 声明了两个 visit 方法,分别是对工程师和经理对访问函数,具体代码如下:


public interface Visitor {

// 访问工程师类型
void visit(Engineer engineer);

// 访问经理类型
void visit(Manager manager);
}

首先定义了一个 Visitor 接口,该接口有两个 visit 函数,参数分别是 Engineer、Manager,也就是说对于 Engineer、Manager 的访问会调用两个不同的方法,以此达成区别对待、差异化处理。具体实现类为 CEOVisitor、CTOVisitor类,具体代码如下:


// CEO访问者
public class CEOVisitor implements Visitor {
@Override
public void visit(Engineer engineer) {
System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
}

@Override
public void visit(Manager manager) {
System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
", 新产品数量: " + manager.getProducts());
}
}

在CEO的访问者中,CEO关注工程师的 KPI,经理的 KPI 和新产品数量,通过两个 visitor 方法分别进行处理。如果不使用 Visitor 模式,只通过一个 visit 方法进行处理,那么就需要在这个 visit 方法中进行判断,然后分别处理,代码大致如下:


public class ReportUtil {
public void visit(Staff staff) {
if (staff instanceof Manager) {
Manager manager = (Manager) staff;
System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi +
", 新产品数量: " + manager.getProducts());
} else if (staff instanceof Engineer) {
Engineer engineer = (Engineer) staff;
System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi);
}
}
}

这就导致了 if-else 逻辑的嵌套以及类型的强制转换,难以扩展和维护,当类型较多时,这个 ReportUtil 就会很复杂。而使用 Visitor 模式,通过同一个函数对不同对元素类型进行相应对处理,使结构更加清晰、灵活性更高。
再添加一个CTO的 Visitor 类:



public class CTOVisitor implements Visitor {
@Override
public void visit(Engineer engineer) {
System.out.println("工程师: " + engineer.name + ", 代码行数: " + engineer.getCodeLines());
}

@Override
public void visit(Manager manager) {
System.out.println("经理: " + manager.name + ", 产品数量: " + manager.getProducts());
}
}

重载的 visit 方法会对元素进行不同的操作,而通过注入不同的 Visitor 又可以替换掉访问者的具体实现,使得对元素的操作变得更灵活,可扩展性更高,同时也消除了类型转换、if-else 等“丑陋”的代码。
下面是客户端代码:


public class Client {

public static void main(String[] args) {
// 构建报表
BusinessReport report = new BusinessReport();
System.out.println("=========== CEO看报表 ===========");
report.showReport(new CEOVisitor());
System.out.println("=========== CTO看报表 ===========");
report.showReport(new CTOVisitor());
}
}

具体输出如下:


=========== CEO看报表 ===========
经理: 经理-A, KPI: 9, 新产品数量: 0
工程师: 工程师-A, KPI: 6
工程师: 工程师-B, KPI: 6
工程师: 工程师-C, KPI: 8
经理: 经理-B, KPI: 2, 新产品数量: 6
工程师: 工程师-D, KPI: 6
=========== CTO看报表 ===========
经理: 经理-A, 产品数量: 3
工程师: 工程师-A, 代码行数: 62558
工程师: 工程师-B, 代码行数: 92965
工程师: 工程师-C, 代码行数: 58839
经理: 经理-B, 产品数量: 6
工程师: 工程师-D, 代码行数: 53125


在上述示例中,Staff 扮演了 Element 角色,而 Engineer 和 Manager 都是 ConcreteElement;CEOVisitor 和 CTOVisitor 都是具体的 Visitor 对象;而 BusinessReport 就是 ObjectStructure;Client就是客户端代码。
访问者模式最大的优点就是增加访问者非常容易,我们从代码中可以看到,如果要增加一个访问者,只要新实现一个 Visitor 接口的类,从而达到数据对象与数据操作相分离的效果。如果不实用访问者模式,而又不想对不同的元素进行不同的操作,那么必定需要使用 if-else 和类型转换,这使得代码难以升级维护。


应用场景


当系统中存在类型数量稳定(固定)的一类数据结构时,可以使用访问者模式方便地实现对该类型所有数据结构的不同操作,而又不会对数据产生任何副作用(脏数据)。


简而言之,就是当对集合中的不同类型数据(类型数量稳定)进行多种操作时,使用访问者模式。


通常在以下情况可以考虑使用访问者模式。



  1. 对象结构相对稳定,但其操作算法经常变化的程序。

  2. 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。

  3. 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。  

收起阅读 »

5分钟快速使用MQTT客户端连接环信MQTT消息云

本文介绍如何使用MQTT客户端快速连接环信MQTT消息云一.操作流程 1、开通MQTT业务 开通环信MQTT消息云服务见快速开通MQTT服务;2、下载MQTT客户端 常见的MQTT客户端整理如下,下载客户端后可快速连接环信MQTT...
继续阅读 »

本文介绍如何使用MQTT客户端快速连接环信MQTT消息云

一.操作流程 

1、开通MQTT业务 
开通环信MQTT消息云服务见快速开通MQTT服务

2、下载MQTT客户端 

常见的MQTT客户端整理如下,下载客户端后可快速连接环信MQTT消息云:

MQTT客户端 操作系统 下载地址 
 MQTT Explorer Windows,macOS,Linux点击下载
 MQTT.fx Windows,macOS,Linux点击下载
 MQTT Box Windows,macOS,Linux点击下载
 mqtt-spy Windows,macOS,Linux点击下载
 Mosquitto CLI Windows,macOS,Linux点击下载


二.接入指引
 

 1、连接五要素 
MQTT客户端在连接环节需要5个基本参数,包括连接地址(Host)、端口(Port)、clientID(MQTT client ID)、用户ID(Username)、token(Password)。 
获取方式如下:
step1.进入console控制台,选择【MQTT】->【服务概览】;
step2.获取clientID,clientID由两部分组成,组织形式为“deviceID@AppID”,deviceID由用户自定义,AppID见【服务配置】下AppID; 
step3.获取连接地址(Host); 
step4.获取端口(Port); 
step5.选择左侧菜单栏【应用概览】->【用户认证】; 
step6.获取用户ID(Username); 
step7.获取token(Password); 




2、连接环信MQTT消息云
本文以MQTT Explorer for MAC版本为例(可通过APP Store下载)。打开MQTT客户端软件,选择“+”新建图标。



step1.用户自定义连接名称; 
step2.是否选择开启tls加密,取值:“开启”、“关闭”;
step3.选择连接协议,取值:“ws:(websocket)”、“mqtt:”,若step2选择开启tls,协议为“wss:”、“mqtts”; 
step4.填写环信MQTT消息云连接地址(Host); 
step5.填写端口(Port); 
step6.填写用户ID(username); 
step7.填写token(Password); 
step8.选择【ADVANCE】,填写clientID,clientID由两部分组成,“deviceID@AppID”; 
step9.填写订阅主题名称,此例为“t/t1” ;
step10.填写后点击【ADD】按钮添加至订阅列表中; 
step11. 填写clientID名称; 
step12. 选择【BACK】按钮,返回至主页面;
step13. 选择主页面中【CONNECT】即可连接成功;



3、订阅/发布消息 
在创建一个MQTT 客户端,执行【连接环信MQTT消息云】流程;

【发布消息】
step1.填写发布的主题,本例中为“/t/t1”; 
step2.选择消息体格式,取值:“raw”、“xml”、“json”;
step3.填写消息体内容,本例中为“hello world”; 
step4.选择QoS等级,取值:“0:至多发送一次,不保留”、“1:至少一次,保留”、“2:仅发一次,保留”; 
step5.选择是否为保留消息,取值:“0:不保留”、“1:保留,订阅客户端重新接入环信MQTT消息云时,可以接收保留消息”; 
step6.发送消息; 


【订阅消息】
订阅/t/t1的MQTT客户端即可接收消息

收起阅读 »

Swift是否可以集成环信IM SDK?

可以。Swift集成SDK时,在自定义cell和EaseIMKit混用时,会导致程序崩溃问题.原因是,无法返回nil,应该怎么处理?解决方法:让用户集成easeIMKit源码,将创建自定义cell回调的返回值添加一个可为空的关键字nullable

可以。

Swift集成SDK时,在自定义cell和EaseIMKit混用时,会导致程序崩溃问题.原因是,无法返回nil,应该怎么处理?
解决方法:让用户集成easeIMKit源码,将创建自定义cell回调的返回值添加一个可为空的关键字nullable

OC对象的本质(上) —— OC对象的底层实现原理

一个NSObject对象占用多少内存?Objective-C的本质平时我们编写的OC代码,底层实现都是C/C++代码Objective-C --> C/C++ --> 汇编语言 --> 机器码所以Objective-C的面向对象都是基于C/C...
继续阅读 »

一个NSObject对象占用多少内存?

Objective-C的本质
平时我们编写的OC代码,底层实现都是C/C++代码

Objective-C --> C/C++ --> 汇编语言 --> 机器码

所以Objective-C的面向对象都是基于C/C++的数据结构实现的,所以我们可以将Objective-C代码转换成C/C++代码,来研究OC对象的本质。

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}

我们在main函数里面定义一个简单对象,然后通过 clang -rewrite-objc main.m -o main.cpp命令,将main.m文件进行重写,即可转换出对应的C/C++代码。但是可以看到一个问题,就是转换出来的文件过长,将近10w行。


因为不同平台支持的代码不同(Windows/Mac/iOS),那么同样一句OC代码,经过编译,转成C/C++代码,以及最终的汇编码,是不一样的,汇编指令严重依赖平台环境。
我们当前关注iOS开发,所以,我们只需要生成iOS支持的C/C++代码。因此,可以使用如下命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc <OC源文件> -o <输出的cpp文件>
-sdk:指定sdk
-arch:指定机器cpu架构(模拟器-i386、32bit、64bit-arm64 )
如果需要链接其他框架,使用-framework参数,比如-framework UIKit
一般我们手机都已经普及arm64,所以这里的架构参数用arm64,生成的cpp代码如下



接下来,我们查看一下main_arm64.cpp源文件,如果熟悉这个文件,你将会发现这么一个结构体

struct NSObject_IMPL {
Class isa;
};

我们再来对比看一下NSObject头文件的定义

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
@end

简化一下,就是

@interface NSObject  {
Class isa ;
}
@end

是不是猜到点什么了?没错,struct NSObject_IMPL其实就是NSObject的底层结构,或者说底层实现。换个角度理解,可以说C/C++的结构体类型支撑了OC的面相对象。

点进Class的定义,我们可以看到 是typedef struct objc_class *Class;

Class isa; 等价于 struct objc_class *isa;

所以NSObject对象内部就是放了一个名叫isa的指针,指向了一个结构体 struct objc_class。

总结一:一个OC对象在内存中是如何布局的?


猜想:NSObject对象的底层就是一个包含了一个指针的结构体,那么它的大小是不是就是8字节(64位下指针类型占8个字节)?
为了验证猜想,我们需要借助runtime提供的一些工具,导入runtime头文件,class_getInstanceSize ()方法可以计算一个类的实例对象所实际需要的的空间大小

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
size_t size = class_getInstanceSize([NSObject class]);
NSLog(@"NSObject对象的大小:%zd",size);
}
return 0;
}

结果是


完美验证,it's over,let's go home!


等等,就这么简单?确定吗?答案是否定的~~~
介绍另一个库#import <malloc/malloc.h>,其下有个方法 malloc_size(),该函数的参数是一个指针,可以计算所传入指针 所指向内存空间的大小。我们来用一下

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
size_t size = class_getInstanceSize([NSObject class]);
NSLog(@"NSObject实例对象的大小:%zd",size);
size_t size2 = malloc_size((__bridge const void *)(obj));
NSLog(@"对象obj所指向的的内存空间大小:%zd",size2);
}
return 0;
}

结果是16,如何解释呢?


想要真正弄清楚其中的缘由,就需要去苹果官方的开源代码里面去一探究竟了。苹果的开源代请看这里。
先看一下class_getInstanceSize的实现。我们需要进到objc4/文件里面下载一份最新的源码,我当前最新的版本是objc4-750.1.tar.gz。下载解压之后,打开工程,就可以查看runtime的实现源码。
搜索class_getInstanceSize找到实现代码

size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}

再点进alignedInstanceSize方法的实现

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}

可以看到该方法的注释说明Class's ivar size rounded up to a pointer-size boundary.,意思就是获得类的成员变量的大小,其实也就是计算类所对应的底层结构体的大小,注意后面的这个rounded up to a pointer-size boundary指的是系统在为类的结构体分配内存时所进行的内存对齐,要以一个指针的长度作为对齐系数,64位系统指针长度(字长)是8个字节,那么返回的结果肯定是8的最小整数倍。为什么需要用指针长度作为对齐系数呢?因为类所对应的结构体,在头部的肯定是一个isa指针,所以指针肯定是该结构体中最大的基本数据类型,所以根据结构体的内存对齐规则,才做此设定。如果对这里有疑惑的话,请先复习一下有关内存对齐的知识,便一目了然了。
所以class_getInstanceSize方法,可以帮我们获取一个类的的实例对象所对应的结构体的实际大小。

我们再从alloc方法探究一下,alloc方法里面实际上是AllocWithZone方法,我们在objc源码工程里面搜索一下,可以在Object.mm文件里面找到一个_objc_rootAllocWithZone方法。

id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;

#if __OBJC2__
// allocWithZone under __OBJC2__ ignores the zone parameter
(void)zone;
obj = class_createInstance(cls, 0);
#else
if (!zone) {
obj = class_createInstance(cls, 0);
}
else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
#endif

if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}

再点进里面的关键方法class_createInstance的实现看一下

id  class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}

继续点进_class_createInstanceFromZone方法

static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;

assert(cls->isRealized());

// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();

size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;

id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;

// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}

if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}

return obj;
}

这个方法有点长,有时分析一个方法,不要过分拘泥细节,先针对我们寻找的问题,找到关键点,像这个比较长的方法,我们知道,它的主要功能就是创建一个实例,为其开辟内存空间,我们可以发现中间的这句代码obj = (id)calloc(1, size);,是在分配内存,这里的size是需要分配的内存的大小,那这句应该就是为对象开辟内存的核心代码,再看它里面的参数size,我们能在上两行代码中找到size_t size = cls->instanceSize(extraBytes);,于是我们继续点进instanceSize看看

size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}

翻译一下这句注//CF requires all objects be at least 16 bytes.我们就明白了,CF作出了硬性的规定:当创建一个实例对象的时候,为其分配的空间不能小于16个字节,为什么这么规定呢,我个人目前的理解是这可能就相当于一种开发规范,或者对于CF框架内部的一些实现提供的规范。
这个size_t instanceSize(size_t extraBytes)返回的字节数,其实就是为 为一个类创建实例对象所需要分配的内存空间。这里我们的NSObject类创建一个实例对象,就分配了16个字节。
我们在点进上面代码中的alignedInstanceSize方法

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}

这不就是我们上面分析class_getInstanceSize方法里面看到的那个alignedInstanceSize嘛。


总结二:class_getInstanceSize&malloc_size的区别

class_getInstanceSize:获取一个objc类的实例的实际大小,这个大小可以理解为创建这个实例对象至少需要的空间(系统实际为这个对象分配的空间可能会比这个大,这是出于系统内存对齐的原因)。
malloc_size:得到一个指针所指向的内存空间的大小。我们的OC对象就是一个指针,利用这个函数,我们可以得到该对象所占用的内存大小,也就是系统为这个对象(指针)所指向对象所实际分配的内存大小。
sizeof():获取一个类型或者变量所占用的存储空间,这是一个运算符。
[NSObject alloc]之后,系统为其分配了16个字节的内存,最终obj对象(也就是struct NSObject_IMPL结构体),实际使用了其中的8个字节内存,(也就是其内部的那个isa指针所用的8个字节,这里我们是在64位系统为前提下来说的)
关于运算符和函数的一些对比理解

函数在编译完之后,是可以在程序运行阶段被调用的,有调用行为的发生
运算符则是在编译按一刻,直接被替换成运算后的结果常量,跟宏定义有些类似,不存在调用的行为,所以效率非常高

更为复杂的自定义类

我们开发中会自定义各种各样的类,基本上都是NSObject的子类。更为复杂的子类对象的内存布局又是如何的呢?我们新建一个NSObject的子类Student,并为其增加一些成员变量

@interface Student : NSObject
{
@public
int _age;
int _no;
}

@end

@implementation Student

@end

使用我们之前介绍过的方法,查看一下这个类的底层实现代码

struct NSObject_IMPL {
Class isa;
};

struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _no;

};

我们发现其实Student的底层结构里,包含了它的成员变量,还有一个NSObject_IMPL结构体变量,也就是它的父类的结构体。根据我们上面的总结,NSObject_IMPL结构体需要的空间是8字节,但是系统给NSObject对象实际分配的内存是16字节,那么这里Student的底层结构体里面的成员变量NSObject_IMPL应该会得到多少的内存分配呢?我们验证一下。

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
//获取`NSObject`类的实例对象的成员变量所占用的大小
size_t size = class_getInstanceSize([NSObject class]);
NSLog(@"NSObject实例对象的大小:%zd",size);
//获取obj所指向的内存空间的大小
size_t size2 = malloc_size((__bridge const void *)(obj));
NSLog(@"对象obj所指向的的内存空间大小:%zd",size2);

Student * std = [[Student alloc]init];
size_t size3 = class_getInstanceSize([Student class]);
NSLog(@"Student实例对象的大小:%zd",size3);
size_t size4 = malloc_size((__bridge const void *)(std));
NSLog(@"对象std所指向的的内存空间大小:%zd",size4);
}
return 0;
}


貌似是对的了,但是为什么用malloc_size得到std所被分配的内存是32?再来一发试试

@interface Student : NSObject
{

@public
//父类的isa还会占用8个字节
int _age;//4字节
int _no;//4字节
int _grade;//4字节
int *p1;//8字节
int *p2;//8字节
}

Student结构体所有成员变量所需要的总空间为 36字节,根据内存对齐原则,最后结构体所需要的空间应该是8的倍数,那应该就是40,我们看一下结果


从结果看没错,但是同时也发现了一个规律,随着std对象成员变量的增加,系统为Student对象std分配的内存空间总是以16的倍数增加(16~32~48......),我们之前分析源码好像没看到有做这个设定


其实上面这个方法只是可以用来计算一个结构体对象所实际需要的内存大小。 [update]其实instanceSize()-->alignedInstanceSize()只是可以用来计算一个结构体对象理论上(按照内存对其规则)所需要分配的内存大小。

真正给实例对象完成分配内存操作的是下面这个方法calloc()


这个方法位于苹果源码的libmalloc文件夹中。但是里面的代码再往下深究,介于我目前的知识储备以及专业出身(数学专业),还是困难比较大。好在从一些大神那里得到了指点。
刚才文章开始,我们讨论到了结构体的内存对齐,这是针对数据结构而言的。从系统层面来说,就以苹果系统而言,出于对内存管理和访问效率最优化的需要,会实现在内存中规划出很多块,这些块有大有小,但都是16的倍数,比如有的是32,有的是48,在libmalloc源码的nano_zone.h里面有这么一段代码

#define NANO_MAX_SIZE    256 /* Buckets sized {16, 32, 48, 64, 80, 96, 112, ...} */

NANO是源码库里面的其中一种内存分配方法,类似的还有frozen、legacy、magazine、purgeable。


这些是苹果基于各种场景优化需求而设定的对应的内存管理相关的库,暂时不用对其过分解读。
上面的NANO_MAX_SIZE解释中有个词Buckets sized,就是苹果事先规划好的内存块的大小要求,针对nano,内存块都被设定成16的倍数,并且最大值是256。举个例子,如果一个对象结构体需要46个字节,那么系统会找一块48字节的内存块分配给它用,如果另一个结构体需要58个字节,那么系统会找一块64字节的内存块分配给它用。
到这里,应该就可以基本上解释清楚,为什么刚才student结构需要40个字节的时候,被分配到的内存大小确实48个字节。至此,针对一个NSObject对象占用内存的问题,以及延伸出来的内存布局,以及其子类的占内存问题,应该就都可以得到解答了。

OC对象的本质(上):OC对象的底层实现
OC对象的本质(中):OC对象的分类
OC对象的本质(下):详解isa&superclass指针

面试题解答

一个NSObject对象占用多少内存?
1)系统分配了16字节给NSObject对象(通过malloc_size函数可以获得)
2)NSObject对象内部只使用了8个字节的空间,用来存放isa指针变量(64位系统下,可以通过class_getInstanceSize函数获得)

链接:https://www.jianshu.com/p/1bf78e1b3594

收起阅读 »

iOS内存(Heap堆内存 && Anonymous VM 虚拟内存) 分析和理解

在使用Instruments 做内存分析的时候, 我们会看到如下的画面,箭头指向的地方有堆内存heap Allocations,和虚拟内存 Anonymous VM , 到底在手机上什么是堆内存,什么是虚拟内存 Anonymous VM 呢? 在观察内存分配的...
继续阅读 »

在使用Instruments 做内存分析的时候, 我们会看到如下的画面,箭头指向的地方有堆内存heap Allocations,和虚拟内存 Anonymous VM , 到底在手机上什么是堆内存,什么是虚拟内存 Anonymous VM 呢? 在观察内存分配的时候 我们是否需要
去了解它

前言所需要的图片(如下图)


1) 什么是堆
堆是一种完全结构的二叉树 堆与二叉树的理解

堆: 是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程 初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。堆里面一般 放的是静态数据,比如static的数据和字符串常量等,资源加载后一般也放在堆里面。一个进程的所有线程共有这些堆 ,所以对堆的操作要考虑同步和互斥的问题。程序里面编译后的数据段都是堆的一部分。

— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表

{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456{row.content}在常量区,p3在栈上。
static int c =0; //全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);//分配得来的10和20字节的区域就在堆区。
strcpy(p1, "123456"); //123456{row.content}放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。}

堆:首 先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结 点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才 能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出分配方式:堆都是动态分配的,没有静态分配的堆。

1.1) 堆上消耗的内存


1、View 函数的调用
2、注册通知
3、抛出通知
4、view 的布局
5、函数代码的执行
6、sqlite 数据库的创建
7、向字典中增加对象


8、等等

都需要消耗内存, 上面的代码都是程序员创建的, 程序员去控制堆的内存

1.2) 堆上的内存是否释放

1.2.1) 已经释放的例子:

点击步骤1)箭头


查看步骤2)箭头


步骤2) 箭头中有 free 函数, 可以看出, 这个对象 已经被释放

1.2.2) 堆上内存不释放的例子:


上图中箭头执行的地方 没有free 函数 说明 这个对象已经释放

2) Anonymous VM

2.1) 苹果官方文档对虚拟内存的解释

更小的内存消耗不仅可以减少内存, 还可以减少cpu 的时间
我们可能会看到这样的情况, All Heap Allocations 是程序真实的内存分配情况,All Anonymous VM则是系统为程序分配的虚拟内存,为的就是当程序有需要的时候,能够及时为程序提供足够的内存空间,而不会现用现创建

Anonymous VM内存是虚拟内存
、All Anonymous VM。我们无法控制Anonymous VM部分 ,(更新,其实还是可以优化 比如图片绘制相关 详情参见iOS内存探究,需要对虚拟内存熟悉 才能优化)

2.2) 问题: 我们需要关注Anonymous VM 内存吗 ?
问答连接
Should you focus on the Live Bytes column for heap allocations or anonymous VM? Focus on the heap allocations because your app has more control over heap allocations. Most of the memory allocations your app makes are heap allocations.

The VM in anonymous VM stands for virtual memory. When your app launches, the operating system reserves a block of virtual memory for your application. This block is usually much larger than the amount of memory your app needs. When your app allocates memory, the operating system allocates the memory from the block it reserved.

Remember the second sentence in the previous paragraph. The operating system determines the size of the virtual memory block, not your app. That’s why you should focus on the heap allocations instead of anonymous VM. Your app has no control over the size of the anonymous VM.

2.3) 不需要关注 Anonymous VM
我们应该关注堆内存, 因为我们对堆内存有更大的掌控, 大部分我们在app的内存分配是堆内存

VM 在匿名空间中代表的是虚拟内存, 当你的app启动的时候, 操作系统为你的应用程序分配内存, 这个分配的虚拟内存一般比你的app需要的内存大很多,

操作系统决定虚拟内存的分配, 而不是你的app, 这就是你为什么要集中精力处理堆内存, 你的app 对虚拟内存没有掌控力

2.4) 虚拟内存过大 (未解之谜) 如果知道结果请评论留言, 多谢


CGBitmapContextCreateImage 函数会导致虚拟内存过大 ,并且还不释放, 用法未发现问题

CGImageRef alphaMaskImage = CGBitmapContextCreateImage(alphaOnlyContext);
UIImage *result = [UIImage imageWithCGImage:alphaMaskImage scale:image.scale orientation:image.imageOrientation];
CGImageRelease(alphaMaskImage);
CGContextRelease(alphaOnlyContext);
return result;

参考文献

iOS中的堆(heap)和栈(stack)的理解

苹果虚拟内存的官方文档

转自:https://www.jianshu.com/p/dffd5c24dc9a

收起阅读 »

【环信IM集成指南】iOS端常见问题整理

建议用浏览器搜索定位问题~本文持续更新,欢迎大家留言点菜~1、集成IM如何自定义添加表情组https://www.imgeek.org/article/8253575062、旧版音视频与EaseCallKit兼容升级方案https://www.imgeek.o...
继续阅读 »
建议用浏览器搜索定位问题~
本文持续更新,欢迎大家留言点菜~


1、集成IM如何自定义添加表情组

https://www.imgeek.org/article/825357506


2、旧版音视频与EaseCallKit兼容升级方案

https://www.imgeek.org/article/825357507

 
3、如何集成环信EaseIMKit和EaseCallKit源码

https://www.imgeek.org/article/825357493

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

https://www.imgeek.org/article/825357495

 
5、EaseIMKit如何设置昵称、头像

https://www.imgeek.org/article/825354241

 
6、Swift是否可以集成环信IM SDK?

https://www.imgeek.org/article/825357511

 
7、环信IM会话列表和聊天界面修改头像和昵称

https://www.imgeek.org/article/825357608

 
8、手把手教集成EaseIMKit源码 

https://www.imgeek.org/article/825357673


9、环信聊天室如何每次进来可以看到之前的已读消息

https://imgeek.org/article/825357723


10、这几个iOS拓展字段,是只对iOS生效吗?对安卓没有影响吧?
em_push_content 自定义推送显示

em_push_category 向 APNs Payload 中添加 category 字段
em_push_sound 自定义推送提示音
em_push_mutable_content 开启 APNs 通知扩展
em_ignore_notification 发送静默消息
em_force_notification 设置强制推送型 APNs

答:下面这三个对安卓不生效,其他的是两端都会起作用。
em_push_category、em_push_sound、em_push_mutable_content

11、无网时发送消息,然后迅速切到有网状态。这时显示发送成功,然后回退到上一个页面再进入到IM页,刚刚那条消息被重复发送了


可以开通服务端消息去重功能。


12、如果设置了离线不踢出聊天室,那聊天室的消息会有离线推送吗?

聊天室没有离线处理,所以没有离线推送。


13、cmd不进行漫游功能配置成功之前的历史消息,在配置好之后还是能拉下来的。


14、iOS和安卓端发视频消息,对视频格式有要求吗?
答:没有


15.图片发送设置了缩略图,收到的消息里面没有缩略图,只有源文件数据



接收方会直接将缩略图下载到本地,SDK会自动把缩略图缓存到本地,您直接通过body.thumbnailLocalPath就可以获取到了, 我们的UI SDK已经对这些做了封装,不需要您再单独进行处理,如果您这边就是想拿到这张缩略图来使用的话,就需要在messagesDidReceive方法里面自己再判断一下,如果是图片消息的话,就去打印缩略图的路径,然后通过这个路径可以获取到缩略图的原图

case EMMessageBodyTypeImage:
{
// 得到一个图片消息body
EMImageMessageBody *body = ((EMImageMessageBody *)msgBody);
NSLog(@"大图remote路径 -- %@" ,body.remotePath);
NSLog(@"大图local路径 -- %@" ,body.localPath); // // 需要使用sdk提供的下载方法后才会存在
NSLog(@"大图的secret -- %@" ,body.secretKey);
NSLog(@"大图的W -- %f ,大图的H -- %f",body.size.width,body.size.height);
NSLog(@"大图的下载状态 -- %lu",body.downloadStatus);


// 缩略图sdk会自动下载
NSLog(@"小图remote路径 -- %@" ,body.thumbnailRemotePath);
NSLog(@"小图local路径 -- %@" ,body.thumbnailLocalPath);
NSLog(@"小图的secret -- %@" ,body.thumbnailSecretKey);
NSLog(@"小图的W -- %f ,大图的H -- %f",body.thumbnailSize.width,body.thumbnailSize.height);
NSLog(@"小图的下载状态 -- %lu”,body.thumbnailDownloadStatus);


16.后端该如何操作用户上麦

后端无法直接控制让谁上麦,所以只能通过发送CMD消息的方式来和移动端进行交互,移动端根据逻辑指令去操作

17.使用[[EMClient sharedClient].chatManager ackConversationRead:_conversation.conversationId completion:nil];将消息置为已读,但是还是有未读数

[[EMClient sharedClient].chatManager ackConversationRead:_conversation.conversationId completion:nil]; —- 这个方法是发送会话已读消息,将通知服务器将此会话未读数置为0,而将消息置为已读是本地操作,可以使用方法:
1).[[EaseIMKitManager shared] markAllMessagesAsReadWithConversation:_conversation];
2). [conversation markMessageAsReadWithId:message.messageId error:nil];
3).[conversation markMessageAsReadWithId:message.messageId error:nil];
注意:方法1是EaseIMKitManager调用的,方法2、3是EMConversation调用的

18.聊天页面头像设置圆角失败
如果要设置聊天页面头像的圆角值,需要先设置avatarType为圆形才会生效,如果想要设置为圆形,则直接给图片宽度的一半即可


19.调用getGroupSpecificationFromServerWithId获取群组详情失败,失败的原因 --- you have no permission to do this, group member permission is required

出现此问题的原因是当前用户不在群组内,获取群组详情必须是群组成员才有权限,如果因为场景特殊的话,可以使用rest接口获取。

20、如何将附件保存在自己的服务器上

1.项目中搜索:isAutoTransferMessageAttachments,将属性值改为no
2.用户上传文件完成后,不建议用户直接使用remotePath,而是使用ext扩展来存放文件链接.


21、请问后台和sdk对群组名称和群组描述,有字数或其他限制吗?分别是多少?
后台:名称 16字符 超出部分截去,描述64字符 超出部分截去
Sdk:无限制


22、全局广播相关:
(1)支持发送自定义类型消息和扩展消息吗?

支持。
(2)会有离线推送延迟的问题吗?
会,慢速堆积,就会延迟。延迟15分钟很正常的。
(3)全局广播的延迟是根据用户量来的,按每秒下发1000个来推,如果有用户1万个,预计需要10秒。

23、同一个环信id在多设备登录,可以同时加入同一个聊天室。但设备数量有限制,根据多端多设备功能配置的数量来。


24、p8证书在开发和生产环境下都可以工作(不需要在证书之间切换),最重要的是,它不会过期!


25、console后台添加推送证书有数量限制吗?

无限制。但不要短时间内快速上传大量证书。

26、获取token的接口,是根据ip做限制的。例如一个ip,每秒最多10次。

27、iOS端对于离线推送扩展字段:em_push_title、em_push_content的显示逻辑。
如果title和content都有,就显示title的,没有title就取content的值,两个是有优先级的。
如果想要标题和内容都有的样式,可以只用em_push_content,然后将内容进行换行


28、群消息可以单独指定给某人吗?
我们没有这个功能,您可以自己实现。
消息带上扩展,可以是指定人的环信id,群成员们收到消息(messagesDidReceive)后判断扩展内容是不是自己的环信id,是的话就展示,不是就不展示。


29、如果同时设置了发送前和发送后回调,会先执行发送前,再执行发送后。

30、回调会保证顺序发送吗?
回调不保证发送顺序 消息里面都是带时间的。


31、自定义的聊天cell,在哪里设置cell 的高度?
自定义cell的高度是自动计算的,自适应的,正常不用单独设置。
如果有问题,看下自定义的cell的布局是不是不对。


32、从服务器端获取会话列表功能相关规则:

(1)、时效是7天(社区、企业等版本都是统一的)。
如果购买了消息漫游,会话列表保存时长延长至购买的漫游时长。
也可以单独延长保存时长,收费相关需要和商务沟通。
(2)、只获取到会话的最新一条消息,要获取这个会话的其他历史消息可以再调用漫游
(3)、调用后会自动同步到本地数据库(app端)
(4)、默认可以获取10个会话,最大可以上调到100个。需要联系商务调整
(5)、cmd消息不计入会话列表
(6)、开通后需要发送新的消息测试,开通前的数据获取不到


33、群组全员禁言、将某成员解禁,此成员还是无法发消息
这个现象是正常的。


34、图片消息的大图、缩略图的服务器端路径为什么是一样的?
这是正常的,对于服务器端来说,下载缩略图就是多个参数,sdk下载时会有区分。



35、发消息超时重试机制
(1)、断网的情况下发消息,30秒后直接返回error消息
(2)、弱网的情况下,发送附件类型消息需要先进行上传,调用 rest接口,60秒 + 60秒重试,2min后返回error消息
(3)、弱网的情况下,发送非附件类型消息直接走mysnc,1min后返回error消息


36. 大小写敏感的问题
Q:获取不到会话~
A: 大小写敏感
message里面 conversation:WePlay_eTl36Lbp***LQRbX6CzL-
conversation数据库里面conversation: weplay_etl36lbp***lqrbx6czl-

37. Q: 请问一下,SDK发送文件、图片、视频的时候,默认是存储在你们的服务器上的吗?存储地址是否可以自定义?
A: 是可以的
1. 关闭环信的自动下载或者上传附件isAutoTransferMessageAttachments
2. 发送正常消息的时候,在ext里面加上传到自己服务器的资源地址、和文件类型

38. sdk 报300 

是客户端连不上msync服务,这个分两种情况 1.客户端自身网络问题,比如设置了代理服务、网络异常 2.环信服务异常 客户端连接的msync服务异常 提示300


39. 发送方网络不好时发了一条消息,接收方收到两条。
原因:就是网络不好,消息发出去后,sdk没有收到服务器端的ack,sdk认为消息没有发送成功,然后又发了一条。
Q:如果客户的场景就是短时间内重复发相同内容的消息,那配置了这个去重,是不是就会把重复的消息也过滤掉?
A:不会。正常情况下,发相同的消息,每条消息的metaid不同,异常情况下metaid是相同的。meta id就是sdk本地临时生成的消息id,就是网络不好,消息发出去后,sdk没有收到服务器端的ack,sdk认为消息没有发送成功,然后又发了一条
就是这种情况,涉及到sdk重发消息,所以两条消息的metaid是相同的
此情况可以配置“服务端消息去重 unique_client_msgid”,或者联系运维配置。

40. EaseCallKit 问题
Q: 声网的音视频发送消息类型全部是EMMessageTypeCmd 吗?

A: 音视频通话过程中的第一条呼叫邀请是文本消息,其他都是cmd消息

41. EaseCallKit 问题
EMMessageTypePictMixText是用来标记图片类型的,在现在的项目里面是没有用到的

42. 场景:
Q: 现在有个这样的场景,客户端申请创建群服务审核后由服务端创建了一个群,还有就是客户端也可以申请加入群,加入成功和创建成功后都会收到这个回调,我有办法区分出来是服务端创建的群还是我自己主动加入的群么?

A: 服务端在创建群组的时候,有一个custom字段,您可以做业务相关的标记~ custom字段对应客户端的字段是:EMGroup -> ext字段

43. iOS应用的强杀,在聊天室里的人需要立马能感受到他的离线。或者离开。推荐怎么做好
A: 正常情况是离线2min,服务器会将此成员踢出聊天室,2min的时间在我们这儿是可以设置的

44、 加个 em_push_name 有没有用?
A: iOS 目前不支持em_push_name,解决方案是通过\n,来模拟类似标题的效果



45.、用户如果杀进程,你们日志里面会添加记录吗
A: 杀死就是直接log突然没记录了



46、我们的SDK,自动登录,是有token校验的机制的?
我在A设备登录完,又在B设备登录了
这个时候,我又从A设备自登录了,A这边能感知到token变化?(或者说已经在别的设备登录过了)
A: 这个需求研发已经在排期做,完成后的预期时A离线再上线时也可以感知到有其他设备登陆过

47、群组的代理在EaseIMKitManager里面可以走,在help类里面是不走的,需要进一步核实~

48、为啥不用UUID用作环信ID?
A:环信最初设计是来源注册规则,其中username是现在客户注册的环信ID,环信系统收到这个username后会自动生成一个内部的UUID,所以不允许用户使用UUID作为环信ID,避免冲突。username是64位的,UUID是36位的,客户可以在UUID的前边加个前缀作为username。


49、 the resource could not be loaded because the App Transport Security policy requires the use of a secure connection.
A: 在app的info.plist 文件中,设置Allow Arbitrary Loads = yes
或者是在EMOptions 调用usingHttpsOnly = YES 仅支持https

flutter问题
50、语音播放

使用RecordAmr,播放remotePath,安卓可以、iOS不可以
目前给的解决方案:把remotePath修改成localPath


51、添加回调规则添加失败。
A:检查下回调规则名称是不是用的汉字,回调规则只能是数字、字母,不能用汉字。


52、对方离线了之后,发送的消息,上线后如何获取?
A:对方离线,消息会进入离线队列,如果没有集成第三方厂商离线推送,用户上线后,服务器下发给客户端。


53、调用SDK 方法报错: Cannot read property 'lookup' of undefined?
A:因为未登陆成功就调用了SDK 的api,需要在onOpened 链接成功回调执行后再去调用SDK 的api。


54、聊天室如何获取历史消息?
A:两种方式:1、环信服务器端主动推,需要联系商务开通服务,默认10条,数量可以调整。2、通过消息漫游接口自己去拉取历史消息,各端都有提供拉取漫游消息接口。


55、拉取消息漫游,conversationId是怎么获取的?
A:单聊的话,conversationId 就是对方用户的环信id。
群聊或聊天室的话,conversationId 就是groupid 或者chatroomid。


56、如何实现只有好友才可以发消息?
A:可以使用环信的发送前回调服务,消息先回调给配置的回调服务器,然后去判断收发双方是否是好友关系,如果是好友关系,那么下发消息,如果是非好友关系,则不下发消息,客户端ui可以根据不下发返回的code做提示。


57、调rest接口报401是什么原因?
A:调环信rest接口,需要管理员权限的token,确认下请求是否有token,且是在有效期,token的有效期以请求时服务器返回的时间为准。


58、调修改群信息报错如下
System.Net.WebException:“远程服务器返回错误: (400) 错误的请求。
A:检查下请求体,看下参数格式是否正确,比如"membersonly",,"allowinvites" 这两个参数的值为布尔值。


59、注册用户username是纯数字可以吗。

调restapi是可以的,serversdk的话,为了让用户使用更规范的名字,命名规则更严格一些,要求首位是字母。


60、 SDK相关
第1个 SDK 3.8.4 会有长链接特殊情况下无故断开情况,升级至3.8.5即可
第2个 SDK3.8.5.1 会有重复收到推送的情况,升级到3.8.5.2即可
第3个 SDK 3.8.2 以下启动闪退,报network问题,升级至 3.8.3.1即可


61、 3.8.0以上版本与3.8.0以下版本有什么区别?

目前官方demo最新版SDK版本号是3.8.6.2,SDK名称叫HyphenateChat
3.8.0以下(不包含3.8.0)名称为Hyphenate
如果您需要easeIMkit源码,建议您直接使用最新版.

Hyphenate和HyphenateChat的关系:
1.Hyphenate和HyphenateChat都是环信SDK,只需要引用一份即可.
2.Hyphenate和HyphenateChat名字不同,版本也不同:
Hyphenate是3.7.4及以下的SDK命名
HyphenateChat是3.8.0及以上的SDK命名

3.升级前后最大的区别:
Hyphenate包含环信音视频功能
配套的EaseIMKit(3.7.4/3.7.3两个版本)内部也有音视频的UI界面

HyphenateChat已去除环信音视频,单有IM功能
需要集成声网SDK
配套EaseIMKit本身也没有音视频界面.
EaseIMKit介绍文档:http://docs-im.easemob.com/im/ios/other/easeimkit#easeimkit_使用指南
对应声网的音视频SDK,我们专门做了对应的UI界面:EaseCallKit.对EaseIMKit没有音视频界面的一个补充.


注意:
1 EaseCallKit就像EaseIMKit一样,也需要集成,而不被EaseIMKit包含.
2 集成EaseCallKit前,还需要集成声网SDK,您可以阅读开发文档来理解其中关系:http://docs-im.easemob.com/im/ios/other/easecallkit#简介


62、 如何集成easeIMkit源码?
建议客户升级到最新版本即可集成easeIMkit源码.

但需要注意:如果集成easeIMkit源码,虽然可以看到源码实现,但不能修改,如果需要修改调整,则需要使用本地集成的方式.
本地集成方式可借鉴官方demo集成方式(这里由于步骤比较繁琐,如果客户还是不会,会考虑远程操作)

63、本地集成easeIMkit,并修改调整,那么后续是不是无法再升级了?
是这样的,虽然后续无法升级,但未来您是需要做出自己的ui界面的,甚至未来可能会移除easeIMkit,所以请大胆修改调整吧.

我们更希望能够看到三个阶段:
第一阶段:您可以简单快捷地集成环信聊天,并实现聊天功能.同时可以在EaseIMKit.framework开放的接口或属性范围内,可做出适合项目需求的调整.
第二阶段:您在使用EaseIMKit.framework时遇到了限制,有诸多需要调整的部分在动态库内部,您无法去做调整.这时,您可以集成EaseIMKit源码来达到您的目的,集成EaseIMKit源码后,您可以修改源码内部的代码,以满足您项目需求.
第三阶段:在您项目发展到一定程度后,往往EaseIMKit无法再陪伴您继续向更高层次发展.在这时,您对于EaseIMKit源码的熟悉度也非常高了.其中有很多实现方式已经为您之前遇到的困惑做了更好的解答,再加上您早已按耐不住的灵感,重建出属于您项目专属的界面吧!如此更加契合您的产品需求,也更加容易维护.

64、 rest相关
 环信服务器的聊天记录能清除吗 删除了用户。重新注册的用户id对上了 又把聊天记录拉下来了

你删了环信id,聊天记录是不会删除的,这么设计的逻辑是因为每个客户的业务场景不同,如果客户误删了环信id,需要重新注册回来,并且需要看到历史聊天记录。如果你这边的业务,是不希望这种场景,你可以去定义注册环信IM的id规则,你用户注册你自己应用的username时,按你定义的规则去注册IM的id,也就是说你这边的username和环信的id不是同一个,环信这边是根据环信id保存历史记录的

 
65、关于rest接口注册用户,批量注册

单次批量注册上限为100,不受接口限流频率影响.


66、解决方案
消息部分

1.当前场景:SDK本身已经创建数据库,并有简单的增删改查,但由于SDK本身对于数据库的操作较为简单,所以舍弃当前SDK内数据库,创建新的数据库.

举例:当前会搜索到携带关键字的所有类型的消息.期望只搜索我想搜索的消息类型

思路:期望从根源上解决此类问题,让客户可以根据自己的业务场景来实现各类增删改查,突破SDK接口功能比较通用,却又无法主动实现复杂业务逻辑的痛点.


67、当前场景:SDK文件存储限制. 
思路:对接第三方文件存储。当前已可以提供上传下载思路与demo,需要的话官方支持群里@杨剑



68、在聊天过程中,返回界面,未读消息数不归0问题
一般未读消息数无法归零,最有可能的一个原因是easeIMkitmanager没有初始化,从而导致代理为nil,也就没有进行下一步的逻辑.可先检查下easeIMkitmanager是否进行初始化了.
如果您这边没用到easeIMkit,可使用下面截图部分api进行操作:


69、使用xcframework动态库时,如何去除动态库内的x86架构?
xcframework不需要去除x86架构,直接打包即可.

70、 3.8.4版本的gif大表情无法正常显示
请升级版本至3.8.6以上

71、 环信im针对appkey迁移用户数据
我们无法操作客户数据,否则会破坏完整性,建议您自己操作

72、在进入聊天界面时,会有一个滚动的动态效果,我不想要这个效果.
您好,您这边将如下图所示红色箭头指向的bool值改成false即可



73、服务端获取新的token之后旧的token是否失效?
不会失效,token失效以服务器返回时间为准。


74、客户APP集成环信后打包上架过程中审核报错:调用了苹果私有Api.

1.尝试再次提交审核;2.发邮件申诉并贴出EMMessage代码。


75、iOS端SDK没有找回密码的API,这个功能如何实现?

需要后台调用Rest Api重置用户密码的接口即可。


76、每次重启App,deviceToken会不会改变?

不会改变,一般是卸载app重装,升级手机系统,deviceToken才会改变。


77、如何获取昵称?
环信是不涉及用户个人信息的,所以通过环信ID是获取不到用户昵称头像的,用户的个人信息可以在自己服务器与环信ID绑定存储维护,知道环信ID就可以到自己服务器下载这个环信ID对应的用户信息。

78、线上的离线推送不成功,开发环境没问题,如何排查?
换一个新的账号

79、用户未读消息数,在服务端怎么获取?
目前服务端不支持获取未读消息数,可在客户端获取。

80、群组功能,邀请用户进入群组,被邀人能直接进入群组吗?还是需要被邀请人同意才能进入群组?
客户端用户接收群邀请可以自动进入群组,也可以被邀请人同意后进入群组。

81、如何同时集成IM和客服云SDK?
开通IM和客服之后,只集成客服即可(客服内部有IM模块,可直接使用此模块)


82、环信IM账号客户端退出登录为什么还能够收到推送消息?
在登出的方法里将BOOL值设置为YES

83、更换AppKey后服务端发送推送消息移动端收到的消息通知不显示消息内容?

更换AppKey,下属账号默认不显示消息内容,客户端的默认设置不显示消息内容,需要将displayStyle打开即可。

84、将用户拉入黑名单还能聊天?
A把B拉黑,A能B发消息,但是B不能给A发消息。


收起阅读 »

旧版音视频与EaseCallKit兼容升级方案

适用场景当前旧App(1.0)使用旧版音视频SDK,想升级到App 2.0,使用EaseCallKit,但不能强制客户的App升级,在一定时间内,App2.0要与App1.0同时存在,且可以进行音视频通信。方案一1、在App2.0中同时集成旧版音视频SDK(H...
继续阅读 »

适用场景
当前旧App(1.0)使用旧版音视频SDK,想升级到App 2.0,使用EaseCallKit,但不能强制客户的App升级,在一定时间内,App2.0要与App1.0同时存在,且可以进行音视频通信。

方案一

1、在App2.0中同时集成旧版音视频SDK(Hyphenate)、声网SDK和EaseCallKit(EaseCallKit需要修改源码,改成使用Hyphenate),新增App Server(或已有AppServer),AppServer包含以下接口

设置版本信息
获取版本信息


2、新App初始化时,调用AppServer,设置本身账户的版本信息 

3、旧App呼叫,依然走旧版呼叫过程,新App集成了旧版音视频SDK,可以接通

4、新App呼叫前,调用AppServer,获取被叫用户的版本信息,能获取到,则使用EaseCallKit呼叫,不能获取到版本信息,则依然走旧版SDK 呼叫过程




问题:
1、被叫方在多端同时登录,且各端的新旧版本不同,那么只有新版本能收到呼叫(多端不考虑。多端是指web端和移动端,不含桌面端)。
2、客户先登录新版本,然后退出,再登录旧版本,无法接听(升级后再降级,不考虑)。

方案二

App 2.0 同时集成旧版本和新版本,初始化时从AppServer获取开关,若为关,使用旧版音视频,若为开,使用新版音视频,在3个月-6个月内开关关闭 ,大部分客户都更新新版本后,打开开关

方案一和方案二可以结合使用

收起阅读 »

集成环信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消息云服务中,有任何疑问和建议,欢迎您联系我们

收起阅读 »