注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

要不要打造一个轻量的小程序引擎玩玩?

我们的小程序框架的底层,我把它分为四个部分,主要是多线程模型runtime 框架js 沙箱其他我们一个一个来多线程模型和线程通信多线程模型多线程模型是一个非常常见的 UI 模型,包括 RN、flutter 统统都是使用的这个模型,小程序也不例外 它们其实只是线...
继续阅读 »


我们的小程序框架的底层,我把它分为四个部分,主要是

  • 多线程模型
  • runtime 框架
  • js 沙箱
  • 其他

我们一个一个来

多线程模型和线程通信


多线程模型


多线程模型是一个非常常见的 UI 模型,包括 RN、flutter 统统都是使用的这个模型,小程序也不例外


它们其实只是线程主体的不同,比如 RN 主要是 shadow tree 和 jscore,而 flutter 则是 skia 和 dart engine,小程序则是 webview 充当渲染层,js engine(或 worker)充当逻辑层


尽管本质一样,但因为业务场景的不同,小程序的诉求却和 RN/flutter 完全不同


在 RN 中,app 作为一个主体,我们更乐意分配更多资源,以至于 RN 一直在跑 react 这种 runtime 浪费的框架,在过去,这被认为是值得的


但是小程序与之相反,它作为 app 的附属品,我们不乐意给小程序分配更多资源,不乐意分配内存,不乐意分配更多线程,所以我们这次分享的前提是

基于最小分配的前提,探讨小程序的每个环节

请记住前提,然后我们接着往下看


线程通信


说到多线程,我们首先想到的就是多线程的调度和通信,我们先讲通信,通常来说,多线程的非 UI 线程都是没有 dom 环境的,无论是 js 引擎还是 worker


所以为了能跑一个前端框架,我们不得另寻出路,主要方案有三种,其中幻灯片的第二种,写一个 dom 无关的 diff 算法,这是写不出来什么好算法的,所以我们主要看剩下两种思路



幻灯片中,左边的代码是 [ 使用 Proxy 劫持 dom ],右边的是 [ 模拟 dom API ]


这两种思路其实是类似的,模拟 dom API 是最为常见的,比如 react-reconciler,vue3 的 renderer,都是用的这个思路,就是它把用到的 dom API 暴露出来,你在不同的端去模拟 dom 的行为


甚至还有 taro-next,kbone 这种框架,他们模拟了一整个 dom/bom 层


这个思路好处是粗暴方便好用,坏处就是抽象程度低,比如 taro-next 中就用了千行代码做这件事,属于 case by case,没啥逼格


所以我提出了 Proxy 劫持 dom 的思路,其实这个思路在微前端中比较常用,只不过我们现在用 Proxy 不再是劫持一两个 dom 操作了,而是将所有 dom 操作通通记录下来,然后批量发送给 UI 线程


这个实现抽象程度非常高,我使用了不到 200 行代码就可以劫持所有 dom 操作


代码在这里:github.com/yisar/fard/…


除了线程通信,更重要的是线程的调度,因为很重要,我们放到最后说


前端框架


还记得小程序架构的前提吗?没错,就是最小资源分配


因为我们不想给小程序分配过多的资源,所以像 react、vue 这种 runtime 特别重的框架,其实是不适合用作小程序的


甚至 fre 也不适合,因为大家可能对“轻量”这个词有误解,不是代码越少就越轻量,代码量是很重要的一个方面,但是更重要的是代码的内存占用,以及算法的算力和复杂度


fre 虽然代码量少,但它的算法和 vue 是一样的,算力相同,内存占用也不少


所以我们不得不将目光转向 svelte 这类框架,幻灯片可以看到,svelte 通过编译,直接生成原生 dom 操作,没有多余的算法和 vdom


实际上,我们在做性能优化的时候,讲究一个“换”字,react 这种框架,通过浪费 runtime 去做算法,“换”一个最小结果,而 svelte 则是通过编译(浪费用户电脑),去换 runtime



JS 沙箱



然后我们来讲讲沙箱,也就是 js 引擎和 worker,这部分适合语言爱好者

选型


通常来说,一提到 js 引擎,大家都是 v8 v8 v8

但是实际上,v8 是一个高度优化的 JIT 引擎,它跑 js 确实是足够快的,但对于 UI 来说,我们更多要的不是跑得快


实际上,AOT 的语言或引擎更适合 UI 框架,比如 RN 的 hermes,dart 也支持 AOT,它可以编译成字节码,只需要一次构建即可,当然,AOT 也有缺点,就是 热更新 比较难做


另外除了 js 引擎,worker 也是一个非常赞的选择,方便好用,而且还提供了 bom 接口,比如 offscreen canvas,fetch,indexdb,requestAnimationFrame……

总结



哈哈哈总结,我们基于最小分配的前提去设计这个架构,每个环节都选择节省资源的方案


事实上写代码就是这样的,比如我写 fre,那么我追求 1kb,0 依赖,我写业务框架,我追求 0 配置,1mb 的 node_modules 总大小


我写小程序,我追求最小资源分配,不管做啥,有痛点然后有追求然后才有设计


其他

其实小程序还有很多东西可以做,比如现在的小程序都需要兼容微信小程序,也就是类似 wxml,wxss,wxs这些非标准的文件,还要得是个多 Page 的 mpa


比如 ide,我们可以使用 nobundle 的思路来加快构建速度


当然,为了服务业务,在我们公司我没有使用 nobundle


比如剧透一下,我在公司中为了兼容微信小程序,开的新坑


原理是将微信的文件(wxml,wxss,wxs)先编译成可识别的文件(jsx,css,js),然后使用 babel、postCss 去转移,形成一个个 umd 的子应用


然后通过 berial(微前端框架)的路由,沙箱,生命周期,将它们跑在 h5 端,这样就可以在浏览器中模拟和调试啦



最后我们通过三张图和一个问题,来补充和结束一下这次分享


第一张图是微信小程序的后台桌面,有没有感觉和操作系统有点像,但其实不是的,操作系统的软件是进程的关系,只能切换,不能共存,而小程序是多进程,这些小程序可以在后台留驻,随时保持唤醒


第二张图是钉钉的仪表盘,这也是小程序最常用的场景,就是和这种一堆子应用的 app


第三张图是 vscode 的插件系统,是的,想不到吧,这玩意也是小程序架构,而且也是同样的思想,我不让你操作 dom

然后最后的问题:canvas 怎么办?



这个问题实际上非常难搞,如果我们使用 worker 作为 js 沙箱还好,有 offscreen canvas 和 requestAnimationFrame


如果我们使用 js 引擎怎么办呢,走上面提到的线程通信成本就太高了,动画走这个通信,等接收到消息,动画已经卡死了


所以还有什么办法,这里不得不再次提多线程的特性,也就是多线程的内存是共享的,我们可以 js 引擎中,将 canvas 整个元素放到堆栈里,然后 UI 线程和 js 线程共享这一块内存


这样就不需要走线程通信了,适合 canvas,动画这种场景



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


收起阅读 »

微信小程序-自定义日期组件实现

   今日份跟大佬聊到说前端基础架构平台的东西、涉及到基础组件库、公共工具类等内容, 然后期望能自我驱动带头去做, 陷入深深滴沉思, 该考虑如何做?思绪一片混乱, 那就写篇文章冷静冷静,故有了此篇, 仅供参考。微信小程序原生有提供一套日期组...
继续阅读 »

   今日份跟大佬聊到说前端基础架构平台的东西、涉及到基础组件库、公共工具类等内容, 然后期望能自我驱动带头去做, 陷入深深滴沉思, 该考虑如何做?
思绪一片混乱, 那就写篇文章冷静冷静,故有了此篇, 仅供参考。

微信小程序原生有提供一套日期组件, 大概如下:



跟UI预期不一致的点有如下几个:

A. 期望弹窗居中显示、而不是从底部弹出;

B. 期望小于10的展示为1月,1日这种, 而不是01月, 01日;

C. UI样式跟微信原生差别有点大;

D. 不需要头部的取消&确定按钮、预期底部整个确定按钮即可;

想着让产品接受原生日期组件的, But拗不过产品的思维, 只能开干、自己撸一个自定义日期组件, 造轮子=.= 

既然原生的不能用, 那么我们看看小程序是否有提供这种类似的滚动器, 查看官方文档发现: 



那就开干, 为尽可能保持代码的最小颗粒度(这里不考虑弹窗外壳的封装、纯日期组件).
话不多说、这里直接贴上代码、预留的坑位都会在代码内有备注, 请参考:

// 组件wxml
<!-- 预留坑位: 按道理该日期组件应该是做在弹窗上的、这里为了简化代码故直接写在了页面上;
后期使用者烦请自己做个弹窗套上、用showModal属性控制其显示隐藏-->
<view class="picker" wx:if="{{showModal}}">
<picker-view indicator-class="picker-indicator" value="{{pickerIndexList}}" bindchange="bindChangeDate">
<picker-view-column>
<view wx:for="{{yearList}}" wx:key="index" class="{{pickerIndexList[0]==index?'txt-active':''}}">{{item}}年</view>
</picker-view-column>
<picker-view-column>
<view wx:for="{{monthList}}" wx:key="index" class="{{pickerIndexList[1]==index?'txt-active':''}}">{{item}}月</view>
</picker-view-column>
<picker-view-column>
<view wx:for="{{dayList}}" wx:key="index" class="{{pickerIndexList[2]==index?'txt-active':''}}">{{item}}日</view>
</picker-view-column>
</picker-view>
<!-- 预留坑位: 日期组件可能仅允许数据回选、不允许修改。
思路: 通过自定义蒙层盖在日期控件上从而达到禁止控件滚动的效果.
-->
<view wx:if="{{!canEdit}}" class="disabled-picker"></view>
</view>

// 组件wxss
.picker{
position: relative;
height: 300rpx;
width: 600rpx;
margin: 0 auto;
border: 1rpx solid red;
}
.picker picker-view {
width: 100%;
height: 100%;
}
.picker-indicator {
height: 60rpx;
line-height: 60rpx;
}
.picker picker-view-column view {
font-size: 40rpx;
line-height: 60rpx;
text-align: center;
}
.txt-active {
color: #2c2c2c;
}
/* 预留坑位: 为便于区分真的有遮罩层盖住、特意加了个背景色、实际使用过程可改成透明色 */
.disabled-picker{
width: 600rpx;
position: absolute;
top: 0;
left: 0;
height: 300rpx;
z-index: 999;
background: rgb(255,222,173,0.7);
}


// 组件js
Component({
properties: {},
data: {
yearList: [],
monthList: [],
dayList: [],
pickerIndexList: [0, 0, 0]
},
methods: {
// dateString格式: 'YYYY-MM-DD'
initPicker (dateString) {
let nowDate = new Date()
// 预留个坑位: 若需要指定某一日期则从外面传入、否则默认当天
if(dateString){
// 预留个坑位: 判定传入的数据类型是否符合要求、若不符合该报错的报错
nowDate = new Date(dateString)
}

// 预留个坑位: 因为下面的日期指定在1900.01.01-2100.12.31、故这里最好校验下传入日期是否在区间内.
let nowYear = nowDate.getFullYear()
let nowMonth = nowDate.getMonth() + 1
let yearList = this.getYearList(nowYear)
let monthList = this.getMonthList()
let dayList = this.getDayList(nowYear, nowMonth)

// 获取多列选择器的选中值下标
let pickerIndexList = []
pickerIndexList[0] = yearList.findIndex(o => o === nowDate.getFullYear())
pickerIndexList[1] = monthList.findIndex(o => o === nowDate.getMonth()+1)
pickerIndexList[2] = dayList.findIndex(o => o === nowDate.getDate())
this.setData({
yearList,
monthList,
dayList,
pickerIndexList,
showModal: true
})
},
// 获取年份
getYearList (nowYear) {
let yearList = []
if(nowYear < 1900 || nowYear > 2100){
return false
}
for (let i = 1900; i <= 2100; i++) {
yearList.push(i)
}
return yearList
},
// 获取月份
getMonthList () {
let monthList = []
for (let i = 1; i <= 12; i++) {
monthList.push(i)
}
return monthList
},
// 获取日期 -> 根据年份&月份
getDayList (year, month) {
let dayList = []
month = parseInt(month, 10)
// 特别注意: 这里要根据年份&&月份去计算当月有多少天[切记切记]
let temp = new Date(year, month, 0)
let days = temp.getDate()
for (let i = 1; i <= days; i++) {
dayList.push(i)
}
return dayList
},
// 日期选择改变事件
bindChangeDate (e) {
let pickerColumnList = e.detail.value
const { yearList=[], monthList=[] } = this.data
const nowYear = yearList[pickerColumnList[0]]
const nowMonth = monthList[pickerColumnList[1]]
this.setData({
dayList: this.getDayList(nowYear, nowMonth),
pickerIndexList: pickerColumnList
})
},
show (birthday) {
// 预留坑位: 这里也许会有一定的逻辑判定是否允许编辑日期控件, 故预留canEdit属性去控制
this.setData({
canEdit: true
})
this.initPicker(birthday)
},
// 预留坑位、点击确定按钮获取到选中的日期
surePicker () {
const { pickerIndexList, yearList, monthList, dayList } = this.data
// 预留坑位: 月份&日期补0
let txtDate = `${yearList[pickerIndexList[0]]}-${monthList[pickerIndexList[1]]}-${dayList[pickerIndexList[2]]}`
console.log(txtDate)
},
}
})

接下来我们看看使用方是怎么使用的?

// 页面wxml
<!-- 预留坑位: 这里仅展示触发事件、开发者替换成实际业务即可-->
<view bind:tap="openPicker" style="margin:20rpx; text-align:center;">打开日期控件</view>

// 页面json: 记得在使用页面的json处引入该组件、配置组件路径

// 页面js
methods: {
openPicker (){
// 获取组件实例、这里可选择是否传入日期
this.date_picker = this.selectComponent && this.selectComponent('#date_picker')
this.date_picker && this.date_picker.show()
},
}

一切准备就绪、我们看看效果图!
这是日期可编辑时、你是可滚动选择器的:


我们看看日期不可编辑时、仅可查看的效果图:



样式是稍微有点丑、到时开发者按照实际UI去做微调即可、这不难的=.=.

这里预留了几个扩展点:

1.支持外部传入日期、默认选中预设值;

2.支持在弹窗内显示日期控件、需要使用者自行开发弹窗;

3.支持日期控件仅可查看、不可编辑;

4.支持日期控件的关闭、一般是弹窗上有个关闭按钮或者是点击弹窗的蒙层可关闭、使用者自行开发;

Tips: 具体的代码改动点都有在上面的code中有备注、欢迎对号改代码, 若有任何不懂的欢迎留言或者私信、很愿意帮您解答。


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

收起阅读 »

Onboard,迷人的引导页样式制作库

简介Onboard主要用于引导页制作,源码写的相当规范,值得参考.项目主页: https://github.com/mamaral/Onboard实例下载: https://github.com/mamaral/Onboard/archiv...
继续阅读 »

简介




Onboard主要用于引导页制作,源码写的相当规范,值得参考.

项目主页: https://github.com/mamaral/Onboard
实例下载: https://github.com/mamaral/Onboard/archive/master.zip

样式

设置背景图片或者背景movie,然后在它们之上生成数个ViewController,默认是顶部一张图片,下面是标题和详细介绍,最下面是按钮和page

导入

pod 'Onboard'

使用

导入头文件#import “OnboardingViewController.h”

图片为背景

蒙板控制器生成方法

1、title是标题
2、body是介绍
3、image是顶部图片
4、buttonText是按钮文本
5、block是按钮点击事件

OnboardingContentViewController *firstPage = [OnboardingContentViewController contentWithTitle:@"What A Beautiful Photo" body:@"This city background image is so beautiful." image:[UIImage imageNamed:@"blue"] buttonText:@"Enable Location Services" action:^{
}];

OnboardingContentViewController *secondPage = [OnboardingContentViewController contentWithTitle:@"I'm so sorry" body:@"I can't get over the nice blurry background photo." image:[UIImage imageNamed:@"red"] buttonText:@"Connect With Facebook" action:^{
}];
secondPage.movesToNextViewController = YES;
secondPage.viewDidAppearBlock = ^{
};

OnboardingContentViewController *thirdPage = [OnboardingContentViewController contentWithTitle:@"Seriously Though" body:@"Kudos to the photographer." image:[UIImage imageNamed:@"yellow"] buttonText:@"Get Started" action:^{
}];
```


#### 底部图片控制器

```objc
OnboardingViewController *onboardingVC = [OnboardingViewController onboardWithBackgroundImage:[UIImage imageNamed:@"milky_way.jpg"] contents:@[firstPage, secondPage, thirdPage]];




<div class="se-preview-section-delimiter"></div>

底部video控制器

NSBundle *bundle = [NSBundle mainBundle];
NSString *moviePath = [bundle pathForResource:@"yourVid" ofType:@"mp4"];
NSURL *movieURL = [NSURL fileURLWithPath:moviePath];
OnboardingViewController *onboardingVC = [OnboardingViewController onboardWithBackgroundVideoURL:movieURL contents:@[firstPage, secondPage, thirdPage]];




<div class="se-preview-section-delimiter"></div>

定制
1、默认的会给背景图片或者movie加一层黑色的蒙板,可以去掉它们:

onboardingVC.shouldFadeTransitions = YES;




<div class="se-preview-section-delimiter"></div>

2、可以给图片加上模糊效果(相当漂亮):

onboardingVC.shouldBlurBackground = YES;




<div class="se-preview-section-delimiter"></div>

3、可以给蒙板上的文字加上淡出效果:

onboardingVC.shouldFadeTransitions = YES;

转自:https://blog.csdn.net/sinat_30800357/article/details/50016319

收起阅读 »

RAC解析 - 自定义KVO

知识点概述1.KVO实现原理2.runtime使用目的给NSObject添加一个Category,用于给实例对象添加观察者,当该实例对象的某个属性发生变化的时候通知观察者。大体思路添加观察者的方法中- (void)SQ_addObserver:(NSObjec...
继续阅读 »

知识点概述

1.KVO实现原理
2.runtime使用

目的

给NSObject添加一个Category,用于给实例对象添加观察者,当该实例对象的某个属性发生变化的时候通知观察者。

大体思路

添加观察者的方法中

- (void)SQ_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;

会用runtime的方式手动创建一个其子类,并且将该对象变为该子类。该子类会复写观察方法中keyPath的setter方法,使这个setter被调用时利用runtime去调用observer的回调方法

-(void)observeValueForKeyPath:(NSString *)keyPath 
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context;

实现

这里只做KVO的基本功能,当被观察者改变属性的时候通知观察者,所以定义如下方法

NSObject+SQKVO.h

/**
添加观察者

@param observer 观察者
@param keyPath 被观察的属性名
*/
- (void)SQ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;


/**
当被观察的观察属性改变的时候的回调函数

@param keyPath 所观察被观察者的属性名
@param object 被观察者
@param value 被观察的属性的新值
*/
- (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value;

@end

因为这里要用到runtime所以需要添加runtime的头文件

#import <objc/message.h>

而且因为用到objc_msgSend所以要改变一下工程的环境变量


一.动态生成子类

在被观察者调用- SQ_addObserver:forKeyPath:时首先动态生成一个其子类。

// 1.生成子类
// 1.1获取名称
Class selfClass = [self class];
NSString *className = NSStringFromClass(selfClass);
NSString *KVOClassName = [className stringByAppendingString:@"_SQKVO"];
const char *KVOClassNameChar = [KVOClassName UTF8String];
// 1.2创建子类
Class KVOClass = objc_allocateClassPair(selfClass, KVOClassNameChar, 0);
// 1.3注册
objc_registerClassPair(KVOClass);

这里可以看到,我们将子类的类名命名为“类名”+“SQKVO”,譬如类名为“Person”,这个子类是“Person_SQKVO”。
这里有个注意点,一般为动态创建的类名应尽量复杂一些避免重复。最好加上“”。

二.根据KeyPath动态添加对应的setter

1 确定setter的名字
举个例子,如果用户给的keyPath是name,应该动态添加一个-setName:的方法。而这个setter的名字是 "set" + "把keyPath变为首字母大写" + ":"
所以可以得出

NSString *setterString =
[NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]];
SEL setter = NSSelectorFromString(setterString);

2 利用class_addMethod()给子类动态添加方法

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

1、cls:
给哪个类添加方法。即新生成的子类,上面生成的 KVOClass。
2、name:
所添加方法的名称。即上一步生成的字符串 setterString。
3、imp:
所添加方法的实现。即这个方法的C语言实现,首先在下面先写一个C语言的方法。稍后会讲具体实现。

void setValue(id self, SEL _cmd, id newVale) {
}

4、types:
所添加方法的编码类型。setter的返回值是void,参数是一个对象(id)。void用"v"表示,返回值和参数之间用“@:”隔开,对象用"@"表示。最后我们可以得出结果"v@:@"。
具体其他的编码类型可以参考苹果文档。
ps: 这里说下为什么返回值和参数之间用“@:”隔开。“:”代表字符串,所有的OC方法都有两个隐藏参数在参数列表的最前面,“发起者”和 “方法描述符”,“@”就是这个发起者,“:”是方法描述符。而这个types其实是imp返回值和参数的编码。因为OC方法中返回值和参数之间必然有“发起者”和“SEL”隔着,所以“@:”自然而然就成了返回值和参数之间的分隔符。
当然我们还可以用@encode来得到我们想要的编码类型

NSString *encodeString =
[NSString stringWithFormat:@"%s%s%s%s",
@encode(void),
@encode(id),
@encode(SEL),
@encode(id)];

3 将当前对象的类变为我们所创建的子类的类型,即更改isa指针

object_setClass(self, KVOClass);

4 将keyPath和观察者关联(associate)到我们的对象上

用下面这个函数可以很方便的将一个对象用键值对的方式绑定到一个目标对象上。
*如果想了解跟多可以查找《Effective Objective-C》的第10条

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

1、object
目标对象

2、key
绑定对象的键,相当于NSDictionary的key
这里的key一般采用下面的方式声明:

static const void *SQKVOObserverKey = &SQKVOObserverKey;
static const void *SQKVOKeyPathKey = &SQKVOKeyPathKey;

这样做是因为若想令两个键匹配到同一个值,则两者必须是完全相同的指针才行。

3、value
绑定对象,相当于NSDictionary的value

4、policy
绑定对象的缓存策略
@property (nonatomic, weak) :OBJC_ASSOCIATION_ASSIGN
@property (nonatomic, strong) :OBJC_ASSOCIATION_RETAIN_NONATOMIC
@property (nonatomic, copy) :OBJC_ASSOCIATION_COPY_NONATOMIC
@property (atomic, strong) :OBJC_ASSOCIATION_RETAIN
@property (atomic, weak) :OBJC_ASSOCIATION_COPY

最后关联的代码:

objc_setAssociatedObject(self, SQKVOObserverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, SQKVOKeyPathKey, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);

三.setValue()的实现

这个函数的目的主要是:
1.利用objc_msgSend触发原先类的setter
2.利用objc_msgSend触发观察者的回调方法

1. 触发原先的setter方法

// 保存子类
Class KVOClass = [self class];

// 变回原先的类型,去触发setter
object_setClass(self, class_getSuperclass(KVOClass));
NSString *keyPath = objc_getAssociatedObject(self, SQKVOKeyPathKey);
NSString *setterString = [NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]];
SEL setter = NSSelectorFromString(setterString);
objc_msgSend(self, setter, newVale);

2. 调用观察者的回调方法

id observer = objc_getAssociatedObject(self, SQKVOObserverKey);
objc_msgSend(observer, @selector(SQ_observeValueForKeyPath:ofObject:changeValue:), keyPath, self, newVale);

3.改回KVO类

object_setClass(self, KVOClass);

四.实现空的回调方法

- (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value {

}

五.调用自定义的KVO

恭喜你看到这里,并且恭喜你已经成功了!

六.代码

代码下载地址

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

收起阅读 »

Objective-C高级编程笔记一(自动引用计数)

示例代码下载手动引用计数MRC内存管理的思考方式1、自己生成的对象自己持有2、不是自己生成的对象,自己也能持有3、不在需要自己持有的对象时释放4、不是自己持有的对象无法释放对象操作与Objective-C方法的对应实现一个MRCObject类:@impleme...
继续阅读 »

示例代码下载

手动引用计数

MRC内存管理的思考方式

1、自己生成的对象自己持有
2、不是自己生成的对象,自己也能持有
3、不在需要自己持有的对象时释放
4、不是自己持有的对象无法释放

对象操作与Objective-C方法的对应


实现一个MRCObject类:

@implementation MRCObject
- (void)dealloc {
NSLog(@"%@(%@)销毁了", NSStringFromClass(self.class), self);

[super dealloc];
}
+ (instancetype)object {
MRCObject *obj = [self allocObject];
[obj autorelease];
return obj;
}

+ (instancetype)allocObject {
MRCObject *obj = [[MRCObject alloc] init];
NSLog(@"%@(%@)生成了", NSStringFromClass(obj.class), obj);

return obj;
}

@end

自己生成并持有对象:

MRCObject *obj = [MRCObject allocObject];

不是自己生成的对象也能持有:

MRCObject *obj = [MRCObject object];
[obj retain];

不在需要自己持有的对象时释放:

MRCObject *obj = [self allocObject];
[obj release];

无法释放自己没有持有的对象:

MRCObject *obj = [self allocObject];
[obj release];
[obj release];//会奔溃

autorelease

autorelease像c语言的自动变量来对待对象实例,当超出其作用域(相当于变量作用域),对象实例的release方法被调用。与c语言自动变量不同的是,可以autorelease的作用域。

autorelease的使用方法:

1、生成NSAutoreleasePool对象
2、调用已分配对象实例的autorelease方法
3、废弃NSAutoreleasePool对象

在应用程序中,由于主线程的NSRunloop对NSAutoreleasePool对象进行生成、持有和废弃处理。因此开发者不一定非得使用NSAutoreleasePool对象来进行开发工作。如下图:


在大量产生autorelease对象时,只要不废弃NSAutoreleasePool对象,autorelease对象就不会被释放,因此会产生内存不足的现象。如下两段代码:

for (int index = 0; index < 1000; index++) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"1553667540126" ofType:@"jpeg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
[image autorelease];
}
for (int index = 0; index < 1000; index++) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *path = [[NSBundle mainBundle] pathForResource:@"1553667540126" ofType:@"jpeg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
[image autorelease];
[pool drain];
}

ARC

ARC规则

ARC有效时,id类型和对象类型同c语言其他类型不同,必须添加所有权修饰符。共如下4种所有权修饰符:

1、__strong修饰符
2、__weak修饰符
3、__unsafe_unretained修饰符
4、__outoreleasing修饰符

import "ARCObject.h"

实现一个ARCObject类:

@interface ARCObject ()
{
__strong id _strongObj;
__weak id _weakObj;
}

@end

@implementation ARCObject

- (void)dealloc {
NSLog(@"%@(%@)销毁了", NSStringFromClass(self.class), self);
}
+ (instancetype)allocObject {
ARCObject *obj = [[ARCObject alloc] init];
NSLog(@"%@(%@)生成了", NSStringFromClass(obj.class), obj);
return obj;
}
- (void)setStrongObject:(id)obj {
_strongObj = obj;
}
- (void)setWeakObject:(id)obj {
_weakObj = obj;
}

@end

__strong修饰符

__strong修饰符是所有id类型和对象类型默认的所有权修饰符,表示对对象的强引用,在超出其作用域或被重新赋值时被废弃。

{
ARCObject *obj = [ARCObject allocObject];
NSLog(@"作用域最后一行%@", obj);
}
NSLog(@"作用域已经结束");
ARCObject *obj = [ARCObject allocObject];
NSLog(@"重新赋值前%@", obj);
obj = [ARCObject allocObject];
NSLog(@"重新赋值前后%@", obj);

__strong、__weak、__outoreleasing修饰符的自动变量默认初始化为nil。

__weak修饰符

__weak修饰符与__strong修饰符相反,提供弱引用,弱引用不持有对象实例。

循环引用容易发生内存泄漏,内存泄漏就是应当废弃的对象在超出其生存周期后依然存在。可以使用__weak修饰符来避免。

ARCObject *aObj = [ARCObject allocObject];
ARCObject *bObj = [ARCObject allocObject];
[aObj setStrongObject:bObj];
[bObj setStrongObject:aObj];
ARCObject *obj = [ARCObject allocObject];
[obj setStrongObject:obj];
ARCObject *aObj = [ARCObject allocObject];
ARCObject *bObj = [ARCObject allocObject];
ARCObject *cObj = [ARCObject allocObject];
[aObj setWeakObject:bObj];
[bObj setWeakObject:aObj];
[cObj setWeakObject:cObj];

__weak修饰符有一个优点就是:在持有某对象的弱引用时,如果该对象被废弃,则该对象弱引用自动失效且被置为nil。

__unsafe_unretained修饰符

__unsafe_unretained修饰符,正如其名一样是不安全的所有权修饰符。尽管ARC的内存管理是编译器的工作,但是这一点需要注意特别注意,__unsafe_unretained修饰符的变量不属于编译器内存管理的对象。

__unsafe_unretained修饰符和__weak修饰符的变量一样不会持有对象,但是__unsafe_unretained修饰符的变量在销毁时并不会自动置为nil,在其地址被覆盖后就会因为反问垂悬指正而造成奔溃。因此__unsafe_unretained修饰符变量赋值给__strong修饰符变量时要确保对象的真实存在。因为__weak修饰符是在iOS5中实现的,__unsafe_unretained修饰符存在的意义就是在iOS4中代替__weak修饰符的作用。

ARCObject __unsafe_unretained *obj = nil;
{
ARCObject *obj1 = [ARCObject allocObject];
obj = obj1;
}
NSLog(@"%@(%@)", NSStringFromClass(obj.class), obj);

__outoreleasing修饰符

ARC有效时不能使用outorelease方法,也不能使用NSAutoreleasePool类。这样就导致outorelease无法直接使用,但实际上outorelease功能是起作用的。使用@outoreleasepool{}块代码来代替NSAutoreleasePool类对象的生成持有以及废弃。通过赋值给__outoreleasing修饰符的变量来代替调用outorelease方法,也就是说对象被注册到autoreleasepool中。

@autoreleasepool {
ARCObject __autoreleasing *obj1 = [ARCObject allocObject];
NSLog(@"autoreleasepool块最后一行%@", obj1);
}
NSLog(@"autoreleasepool块已经结束");

ARC有效时,cocoa中由于编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到outoreleasepool中。所以非显示的使用__outoreleasing修饰符也是可以的。

NSMutableArray __weak *array = nil;
NSLog(@"作用域块开始前%@", array);
{
NSMutableArray *arr = [NSMutableArray arrayWithObject:@(1)];
array = arr;
NSLog(@"作用域块最后一行%@", array);
}
NSLog(@"作用域块已经结束%@", array);

打印结果:

2019-03-28 11:56:52.316360+0800 ProfessionalExample[82984:16680615] 作用域块开始前(null)
2019-03-28 11:56:52.316538+0800 ProfessionalExample[82984:16680615] 作用域块最后一行(
1
)
2019-03-28 11:56:52.316627+0800 ProfessionalExample[82984:16680615] 作用域块已经结束(
1
)

id的指针和对象的指针在没有显式指定修饰符时会被附加上__outoreleasing修饰符。

- (BOOL)performOperationWithError:(ARCObject **)obj {
*obj = [ARCObject object];
return NO;
}

调用方法则为如下所示,自动转化为__autoreleasing修饰符:

[self performOperationWithError:<#(ARCObject *__autoreleasing *)#>];

id的指针和对象的指针变量必须指明所有权修饰符,并且赋值的所有权修饰符必须一致:

NSObject **pObj;//编报错,没有所有权修饰符
NSObject *obj = [[NSObject alloc] init];
NSObject *__autoreleasing*pObj = &obj;//编译报错,会更改所有权属性

纠正一个比较普遍的错误认知,for循环中并不是循环结束才释放循环内的局部变量,并不是所有产生大量对象的for循环中都需要加NSAutoreleasePool,而是产生大量autorelease对象时才需要添加。如下示例代码:

for (int index = 0; index < 2; index++) {
if (index == 0) {
NSLog(@"-------------begin");
ARCObject *obj = [[ARCObject alloc] init];
NSLog(@"%@(%@)生成了", NSStringFromClass(obj.class), obj);
}
if (index == 1) {
NSLog(@"-------------end");
}
}

下面是这段代码的打印内容:

2019-03-28 15:27:19.179194+0800 ProfessionalExample[85692:16955598] -------------begin
2019-03-28 15:27:19.179366+0800 ProfessionalExample[85692:16955598] ARCObject(<ARCObject: 0x600001ded3a0>)生成了
2019-03-28 15:27:19.179449+0800 ProfessionalExample[85692:16955598] ARCObject(<ARCObject: 0x600001ded3a0>)销毁了
2019-03-28 15:27:19.179521+0800 ProfessionalExample[85692:16955598] -------------end

ARC编码规则

1、不能使用retain/release/retainCount/autorelease
2、不能使用NSAllocateObject/NSDeallocateObject
3、须遵守内存管理的方法命名规则
4、不能显式调用dealloc方法
5、使用@autoreleasepool{}代替NSAutoreleasePool
6、不能使用NSZone
7、对象变量不能作为c语言结构体的成员
8、显式转换id和void *

内存管理的方法命名规则

以alloc/new/copy/mutableCopy开头的方法返回对象时,必须返回给调用方应当持有的对象。这在ARC有效时是一样的,不同的是以init开头的方法必须是实例方法且需要返回对象,该返回对象并不注册到autoreleasepool上。

对象变量不能作为c语言结构体的成员

要把对象类型变量加入到结构体中,需强制转为void *或者前面附加__unsafe_unretained修饰符。

显式转换id和void *

可以使用(__bridge)转换void *和OC对象,但是其安全性和赋值给__unsafe_unretained修饰符相近或者更低。如果管理时不注意赋值对象的所有者就会因为垂悬指针而奔溃或者内存泄漏。

NSObject *obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
obj = (__bridge NSObject *)p;

__bridge_retained转换可使要赋值的变量持有所赋值的变量。__bridge_transfer则与之相反。

NSObject *obj = [[NSObject alloc] init];
void *p = (__bridge_retained void *)obj;
obj = (__bridge_transfer NSObject *)p;

NSObject对象与Core Fundation对象之间的相互转换,即免费桥(Toll-Freee-Bridge)转换。CFBridgingRetain函数(等价于__bridge_retained转换),CFBridgingRelease函数(等价于__bridge_transfer)。

NSObject *obj = [[NSObject alloc] init];
CFTypeRef ref = CFBridgingRetain(obj);
obj = CFBridgingRelease(ref);

属性

属性声明的属性与所有权修饰符对应关系


c数组

c静态数组,各修饰符的使用OC对象一样没有区别。

以__strong为例,其初始化为nil,超过作用域销毁:

{
ARCObject *array[2];
array[0] = [ARCObject allocObject];
NSLog(@"array第一个元素:%@", array[0]);
NSLog(@"array第二个元素:%@", array[1]);
array[1] = nil;
NSLog(@"array第二个元素:%@", array[1]);
}
NSLog(@"作用域块已经结束");

打印结果:

2019-03-28 19:19:26.697408+0800 ProfessionalExample[88859:17353905] ARCObject(<ARCObject: 0x6000005f8500>)生成了
2019-03-28 19:19:26.697661+0800 ProfessionalExample[88859:17353905] array第一个元素:<ARCObject: 0x6000005f8500>
2019-03-28 19:19:26.697761+0800 ProfessionalExample[88859:17353905] array第二个元素:(null)
2019-03-28 19:19:26.697845+0800 ProfessionalExample[88859:17353905] array第二个元素:(null)
2019-03-28 19:19:26.697930+0800 ProfessionalExample[88859:17353905] ARCObject(<ARCObject: 0x6000005f8500>)销毁了
2019-03-28 19:19:26.697995+0800 ProfessionalExample[88859:17353905] 作用域块已经结束

c动态数组,c语言中动态数组声明用指针即id *array(NSObject **array)。需要注意如下几点:

1、_strong/__weak修饰符的OC变量初始化为nil,并不代表其指针初始化为nil。所以分配内存后,需要对其初始化为nil,否则非常危险。calloc函数分配的就是nil初始化后的内存,malloc函数分配内存后必须使用memset将内存填充为0(nil)。
2、必须置空_strong修饰符的态数数组内的元素,使其强引用失效,元素才能释放。因为动态数组的生命周期有开发者管理,编译器不能确定销毁动态数组内元素的时机。

{
ARCObject *__strong *array;
array = (ARCObject *__strong *)calloc(2, sizeof(ARCObject *));
NSLog(@"array第一个元素:%@", array[0]);
NSLog(@"array第二个元素:%@", array[1]);
array[0] = [ARCObject allocObject];
array[1] = [ARCObject allocObject];
array[0] = nil;
NSLog(@"array第一个元素:%@", array[0]);
NSLog(@"array第二个元素:%@", array[1]);
free(array);
}
NSLog(@"作用域块已经结束");

打印结果:

2019-03-28 19:29:26.162245+0800 ProfessionalExample[89048:17394552] array第一个元素:(null)
2019-03-28 19:29:26.162586+0800 ProfessionalExample[89048:17394552] array第二个元素:(null)
2019-03-28 19:29:26.162763+0800 ProfessionalExample[89048:17394552] ARCObject(<ARCObject: 0x600001a32b40>)生成了
2019-03-28 19:29:26.162867+0800 ProfessionalExample[89048:17394552] ARCObject(<ARCObject: 0x600001a395c0>)生成了
2019-03-28 19:29:26.162945+0800 ProfessionalExample[89048:17394552] ARCObject(<ARCObject: 0x600001a32b40>)销毁了
2019-03-28 19:29:26.163011+0800 ProfessionalExample[89048:17394552] array第一个元素:(null)
2019-03-28 19:29:26.163083+0800 ProfessionalExample[89048:17394552] array第二个元素:<ARCObject: 0x600001a395c0>
2019-03-28 19:29:26.163160+0800 ProfessionalExample[89048:17394552] 作用域块已经结束

转自:https://www.jianshu.com/p/82849c350b0b

收起阅读 »

CYLTabBarController的使用

CYLTabBarController 是一个自定义的TabBarController, 集成非常简单https://github.com/ChenYilong/CYLTabBarController1.首先使用CocoaPods 进行集成: pod...
继续阅读 »

CYLTabBarController 是一个自定义的TabBarController, 集成非常简单

https://github.com/ChenYilong/CYLTabBarController

1.首先使用CocoaPods 进行集成: 

pod 'CYLTabBarController'
在终端上执行: 
pod install --verbose --no-repo-update

2. 创建TabBar对应的视图控制器


3.创建CYLTabBarControllerConfig

#import <Foundation/Foundation.h>  

#import "CYLTabBarController.h"
@interface CYLTabBarControllerConfig : NSObject

@property (nonatomic, retain) CYLTabBarController * tabBarController;

@end
#import "CYLTabBarControllerConfig.h"  

#import "FirstViewController.h"
#import "SecondViewController.h"
#import "ThirdViewController.h"
#import "FourthViewController.h"


@implementation CYLTabBarControllerConfig

- (CYLTabBarController *)tabBarController {
if (_tabBarController == nil) {
FirstViewController * firstViewController = [[FirstViewController alloc] init];
UIViewController * firstNavigationController = [[UINavigationController alloc] initWithRootViewController:firstViewController];

SecondViewController * secondViewController = [[SecondViewController alloc] init];
UIViewController * secondNavigationController = [[UINavigationController alloc] initWithRootViewController:secondViewController];

ThirdViewController * thirdViewController = [[ThirdViewController alloc] init];
UIViewController * thirdNavigationController = [[UINavigationController alloc] initWithRootViewController:thirdViewController];

FourthViewController * fourthViewController = [[FourthViewController alloc] init];
UIViewController * fourthNavigationController = [[UINavigationController alloc] initWithRootViewController:fourthViewController];


NSArray * tabBarItemsAttributes = [self tabBarItemsAttributes];
NSArray * viewControllers = @[firstNavigationController, secondNavigationController, thirdNavigationController, fourthNavigationController];

CYLTabBarController * tabBarController = [[CYLTabBarController alloc] init];

tabBarController.tabBarItemsAttributes = tabBarItemsAttributes;
tabBarController.viewControllers = viewControllers;

_tabBarController = tabBarController;

}

return _tabBarController;
}


- (NSArray *)tabBarItemsAttributes {
NSDictionary * tabBarItem1Attribute = @{
CYLTabBarItemTitle : @"首页",
CYLTabBarItemImage : @"home_normal",
CYLTabBarItemSelectedImage : @"home_highlight"
};
NSDictionary * tabBarItem2Attribute = @{
CYLTabBarItemTitle : @"同城",
CYLTabBarItemImage : @"mycity_normal",
CYLTabBarItemSelectedImage : @"mycity_highlight"
};
NSDictionary * tabBarItem3Attribute = @{
CYLTabBarItemTitle : @"消息",
CYLTabBarItemImage : @"message_normal",
CYLTabBarItemSelectedImage : @"message_highlight"
};
NSDictionary * tabBarItem4Attribute = @{
CYLTabBarItemTitle : @"我的",
CYLTabBarItemImage : @"account_normal",
CYLTabBarItemSelectedImage : @"account_highlight"
};
NSArray * tarBarItemsAttrbutes = @[tabBarItem1Attribute, tabBarItem2Attribute, tabBarItem3Attribute, tabBarItem4Attribute];

return tarBarItemsAttrbutes;
}


/**
* 更多TabBar自定义设置:比如:tabBarItem 的选中和不选中文字和背景图片属性、tabbar 背景图片属性
*/
+ (void)customizeTabBarAppearance {

//去除 TabBar 自带的顶部阴影
[[UITabBar appearance] setShadowImage:[[UIImage alloc] init]];

// set the text color for unselected state
// 普通状态下的文字属性
NSMutableDictionary *normalAttrs = [NSMutableDictionary dictionary];
normalAttrs[NSForegroundColorAttributeName] = [UIColor blackColor];

// set the text color for selected state
// 选中状态下的文字属性
NSMutableDictionary *selectedAttrs = [NSMutableDictionary dictionary];
selectedAttrs[NSForegroundColorAttributeName] = [UIColor blackColor];

// set the text Attributes
// 设置文字属性
UITabBarItem *tabBar = [UITabBarItem appearance];
[tabBar setTitleTextAttributes:normalAttrs forState:UIControlStateNormal];
[tabBar setTitleTextAttributes:selectedAttrs forState:UIControlStateSelected];

// Set the dark color to selected tab (the dimmed background)
// TabBarItem选中后的背景颜色
[[UITabBar appearance] setSelectionIndicatorImage:[self imageFromColor:[UIColor colorWithRed:26 / 255.0 green:163 / 255.0 blue:133 / 255.0 alpha:1] forSize:CGSizeMake([UIScreen mainScreen].bounds.size.width / 5.0f, 49) withCornerRadius:0]];

// set the bar background color
// 设置背景图片
// UITabBar *tabBarAppearance = [UITabBar appearance];
// [tabBarAppearance setBackgroundImage:[UIImage imageNamed:@"tabbar_background_ios7"]];
}

+ (UIImage *)imageFromColor:(UIColor *)color forSize:(CGSize)size withCornerRadius:(CGFloat)radius {
CGRect rect = CGRectMake(0, 0, size.width, size.height);
UIGraphicsBeginImageContext(rect.size);

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillRect(context, rect);

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

// Begin a new image that will be the new image with the rounded corners
// (here with the size of an UIImageView)
UIGraphicsBeginImageContext(size);

// Add a clip before drawing anything, in the shape of an rounded rect
[[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius] addClip];
// Draw your image
[image drawInRect:rect];

// Get the image, here setting the UIImageView image
image = UIGraphicsGetImageFromCurrentImageContext();

// Lets forget about that we were drawing
UIGraphicsEndImageContext();
return image;
}

4. AppDelegate 设置根视图控制器

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  
// TabBar
CYLTabBarControllerConfig * TabBarControllerConfig = [[CYLTabBarControllerConfig alloc] init];
self.window.rootViewController = TabBarControllerConfig.tabBarController;
[self customizeInterface];

return YES;
}
- (void)customizeInterface {
[self setUpNavigationBarAppearance];
}

/**
* 设置navigationBar样式
*/
- (void)setUpNavigationBarAppearance {
UINavigationBar *navigationBarAppearance = [UINavigationBar appearance];

UIImage *backgroundImage = nil;
NSDictionary *textAttributes = nil;
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
backgroundImage = [UIImage imageNamed:@"navigationbar_background_tall"];

textAttributes = @{
NSFontAttributeName: [UIFont boldSystemFontOfSize:18],
NSForegroundColorAttributeName: [UIColor blackColor],
};
} else {
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0
backgroundImage = [UIImage imageNamed:@"navigationbar_background"];

textAttributes = @{
UITextAttributeFont: [UIFont boldSystemFontOfSize:18],
UITextAttributeTextColor: [UIColor blackColor],
UITextAttributeTextShadowColor: [UIColor clearColor],
UITextAttributeTextShadowOffset: [NSValue valueWithUIOffset:UIOffsetZero],
};
#endif
}

[navigationBarAppearance setBackgroundImage:backgroundImage
forBarMetrics:UIBarMetricsDefault];
[navigationBarAppearance setTitleTextAttributes:textAttributes];
}

运行即可实现效果,如果想实现凸起的加号效果需要 CYLPlusButtonSubclass

#import "CYLPlusButton.h"  

@interface CYLPlusButtonSubclass : CYLPlusButton <CYLPlusButtonSubclassing>

@end
#import "CYLPlusButtonSubclass.h"  

@interface CYLPlusButtonSubclass ()<UIActionSheetDelegate> {
CGFloat _buttonImageHeight;
}

@end

@implementation CYLPlusButtonSubclass

#pragma mark -
#pragma mark - Life Cycle

+ (void)load {
[super registerSubclass];
}

- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.titleLabel.textAlignment = NSTextAlignmentCenter;
self.adjustsImageWhenHighlighted = NO;
}
return self;
}

//上下结构的 button
- (void)layoutSubviews {
[super layoutSubviews];

// 控件大小,间距大小
CGFloat const imageViewEdge = self.bounds.size.width * 0.6;
CGFloat const centerOfView = self.bounds.size.width * 0.5;
CGFloat const labelLineHeight = self.titleLabel.font.lineHeight;
CGFloat const verticalMarginT = self.bounds.size.height - labelLineHeight - imageViewEdge;
CGFloat const verticalMargin = verticalMarginT / 2;

// imageView 和 titleLabel 中心的 Y 值
CGFloat const centerOfImageView = verticalMargin + imageViewEdge * 0.5;
CGFloat const centerOfTitleLabel = imageViewEdge + verticalMargin * 2 + labelLineHeight * 0.5 + 5;

//imageView position 位置
self.imageView.bounds = CGRectMake(0, 0, imageViewEdge, imageViewEdge);
self.imageView.center = CGPointMake(centerOfView, centerOfImageView);

//title position 位置
self.titleLabel.bounds = CGRectMake(0, 0, self.bounds.size.width, labelLineHeight);
self.titleLabel.center = CGPointMake(centerOfView, centerOfTitleLabel);
}

#pragma mark -
#pragma mark - Public Methods

/*
*
Create a custom UIButton with title and add it to the center of our tab bar
*
*/
+ (instancetype)plusButton {

CYLPlusButtonSubclass *button = [[CYLPlusButtonSubclass alloc] init];

[button setImage:[UIImage imageNamed:@"post_normal"] forState:UIControlStateNormal];
[button setTitle:@"发布" forState:UIControlStateNormal];

[button setTitleColor:[UIColor grayColor] forState:UIControlStateNormal];
button.titleLabel.font = [UIFont systemFontOfSize:9.5];
[button sizeToFit];

[button addTarget:button action:@selector(clickPublish) forControlEvents:UIControlEventTouchUpInside];
return button;
}
/*
*
Create a custom UIButton without title and add it to the center of our tab bar
*
*/
//+ (instancetype)plusButton
//{
//
// UIImage *buttonImage = [UIImage imageNamed:@"hood.png"];
// UIImage *highlightImage = [UIImage imageNamed:@"hood-selected.png"];
//
// CYLPlusButtonSubclass* button = [CYLPlusButtonSubclass buttonWithType:UIButtonTypeCustom];
//
// button.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin;
// button.frame = CGRectMake(0.0, 0.0, buttonImage.size.width, buttonImage.size.height);
// [button setBackgroundImage:buttonImage forState:UIControlStateNormal];
// [button setBackgroundImage:highlightImage forState:UIControlStateHighlighted];
// [button addTarget:button action:@selector(clickPublish) forControlEvents:UIControlEventTouchUpInside];
//
// return button;
//}

#pragma mark -
#pragma mark - Event Response

- (void)clickPublish {
UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController;
UIViewController *viewController = tabBarController.selectedViewController;

UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil
delegate:self
cancelButtonTitle:@"取消"
destructiveButtonTitle:nil
otherButtonTitles:@"拍照", @"从相册选取", @"淘宝一键转卖", nil nil];
[actionSheet showInView:viewController.view];
}

#pragma mark - UIActionSheetDelegate
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
NSLog(@"index: %ld", buttonIndex);
}


#pragma mark - CYLPlusButtonSubclassing
//+ (NSUInteger)indexOfPlusButtonInTabBar {
// return 3;
//}

+ (CGFloat)multiplerInCenterY {
return 0.3;
}

@end

Demo 下载地址:

http://download.csdn.net/detail/vbirdbest/9431253

实现效果如图:


转自:https://blog.csdn.net/man_liang/article/details/56671353

收起阅读 »

iOS 开源项目-FXBlurView

PurposeFXBlurView is a UIView subclass that replicates the iOS 7 realtime background blur effect, but works on iOS 5 and above. It...
继续阅读 »

Purpose
FXBlurView is a UIView subclass that replicates the iOS 7 realtime background blur effect, but works on iOS 5 and above. It is designed to be as fast and as simple to use as possible. FXBlurView offers two modes of operation: static, where the view is rendered only once when it is added to a superview (though it can be updated by calling setNeedsDisplay or updateAsynchronously:completion:) or dynamic, where it will automatically redraw itself on a background thread as often as possible.
FXBlurView 是一个 UIView 的子类,复制了 iOS7 的实时背景模糊效果,但是可以运行在 iOS5 以上的版本。它的设计简单易用。FXBlurView 提供两种模式:静态模糊和动态模糊。

FXBlurView methods

+(void)setBlurEnabled:(BOOL)blurEnabled;

This method can be used to globally enable/disable the blur effect on all FXBlurView instances. This is useful for testing, or if you wish to disable blurring on iPhone 4 and below (for consistency with iOS7 blur view behavior). By default blurring is enabled.

这个方法用来设置全局 使能/不使能 模糊效果在 FXBlurView 的实例上。默认情况下模糊效果是启动。

+(void)setUpdatesEnabled;
+(void)setUpdatesDisabled;

These methods can be used to enable and disable updates for all dynamic FXBlurView instances with a single command. Useful for disabling updates immediately before performing an animation so that the FXBlurView updates don’t cause the animation to stutter. Calls can be nested, but ensure that the enabled/disabled calls are balanced, or the updates will be left permanently enabled or disabled.

这两个方法用来设置所以的动态 FXBlurView 是否进行更新,通过一条指令执行。在展示动画之前立即对没有用的更新进行更新,使 FXBlurView 更新不会产生动画断断续续的效果。调用可以嵌套,但确保 启用/禁用 调用平衡,否则更新会留下永久 启用/禁用。

-(void)updateAsynchronously:(BOOL)async completion:(void (^)())completion;

This method can be used to trigger an update of the blur effect (useful when dynamic = NO). The async argument controls whether the blur will be redrawn on the main thread or in the background. The completion argument is an optional callback block that will be called when the blur is completed.

这个方法可以用于触发更新模糊效果。(在属性 "dynamic = NO"情况下有用)。异步参数控制是否模糊将要在主线程上或在后台进行重绘。完成参数是一个可供选择的回调块,将在模糊完成的时候进行调用。

-(void)setNeedsDisplay;

Inherited from UIView, this method can be used to trigger a (synchronous) update of the view. Calling this method is more-or-less equivalent to calling [view updateAsynchronously:NO completion:NULL].

继承 UIView,这个方法用于触发一个(同步)更新视图。调用这个方法或多或少等同于调用[view updateAsynchronously:NO completion:NULL].

FXBlurView properties

@property (nonatomic, getter = isBlurEnabled) BOOL blurEnabled;

This property toggles blurring on and off for an individual FXBlurView instance. Blurring is enabled by default. Note that if you disable blurring using the +setBlurEnabled method then that will override this setting.

这个属性用来切换 FXBlurView 单独实例模糊启动还是关闭。默认情况下模糊是使能的。请注意,如果您禁用模糊方法 setBlurEnabled,那么它将覆盖此设置。

@property (nonatomic, getter = isDynamic) BOOL dynamic;

This property controls whether the FXBlurView updates dynamically, or only once when the view is added to its superview. Defaults to YES. Note that if dynamic is set to NO, you can still force the view to update by calling setNeedsDisplay or updateAsynchronously:completion:. Dynamic blurring is extremely cpu-intensive, so you should always disable dynamic views immediately prior to performing an animation to avoid stuttering. However, if you have multiple FXBlurViews on screen then it is simpler to disable updates using the setUpdatesDisabledmethod rather than setting the dynamic property to NO.

这个属性控制 FXBlurView 是否动态更新,还是只有在视图加入到它的父视图中。默认情况下是 YES ,请注意,如果你设置 dynamic 属性为 NO,你可以强制视图更新通过调用 setNeedsDisplay或者updateAsynchronously:completion:。动态模糊非常消耗 CPU 内存,所以您应该禁用立即执行的动态视图避免出现断断续续的动画。然而,如果您在屏幕上有多个 FXBlurViews ,通过设置方法 setUpdatedsDisabled来禁止更新比用设置动态属性为 NO 更为简单。

@property (nonatomic, assign) NSUInteger iterations;

The number of blur iterations. More iterations improves the quality but reduces the performance. Defaults to 2 iterations.

模糊迭代的次数。更多的迭代提高质量,但会降低性能。默认值为 2 的迭代。

@property (nonatomic, assign) NSTimeInterval updateInterval;

This controls the interval (in seconds) between successive updates when the FXBlurView is operating in dynamic mode. This defaults to zero, which means that the FXBlurView will update as fast as possible. This yields the best frame rate, but is also extremely CPU intensive and may cause the rest of your app’s performance to degrade, especially on older devices. To alleviate this, try increasing the updateInterval value.

此属性控制 FXBlurView 在动态模式下,距离成功更新的时间间隔(以秒计)。默认值为 0 ,这表示 FXBlurView 更新越快越好。 这将生成最佳的帧速率,但是也是非常消耗 CPU内存,导致你的其他 apps 无法无法加载,特别是旧设备。为了减缓这些情况,尝试增加updateInterval 的值。

@property (nonatomic, assign) CGFloat blurRadius;

This property controls the radius of the blur effect (in points). Defaults to a 40 point radius, which is similar to the iOS 7 blur effect.

此属性控制模糊效果的半径 (以像素点计)。默认是半径为40个像素点,这个值与 iOS7 模糊效果相似。

@property (nonatomic, strong) UIColor *tintColor;

This in an optional tint color to be applied to the FXBlurView. The RGB components of the color will be blended with the blurred image, resulting in a gentle tint. To vary the intensity of the tint effect, use brighter or darker colors. The alpha component of the tintColor is ignored. If you do not wish to apply a tint, set this value to nil or [UIColor clearColor]. Note that if you are using Xcode 5 or above, FXBlurViews created in Interface Builder will have a blue tint by default.

这是应用在 FXBlurView 可选的色调选择。颜色的 RGB 分量将会掺入到模糊图像上,导致产生一个柔和的色调。为了验证色调效果,若要改变色调效果的强度,使用更亮或更暗的颜色。颜色的透明参数是被忽略的。如果您不想应用色调,设置[UIColor clearColor]。请注意,如果您现在使用 Xcode5及以上,FXBlurViews 产生一个接口生成器将默认有一个一个蓝色的色调。

@property (nonatomic, weak) UIView *underlyingView;

This property specifies the view that the FXBlurView will sample to create the blur effect. If set to nil (the default), this will be the superview of the blur view itself, but you can override this if you need to.

此属性表明该视图是 FXBlurView 产生模糊效果的子视图。如果设置为 nil(默认),则该视图的父视图是模糊视图本身,但是如果您有需要您可以进行覆盖它。

总结

今天通过学习 FXBlurView ,提高对英语文档的理解和翻译能力,增加了自己的学习兴趣,也懂得了如何去使用 FXBlurView 的模糊效果特效。在实际中提升自己的能力,年轻,就是资本!Oh Yeah!

参考

https://github.com/cnbin/FXBlurView

转自:https://cnbin.github.io/blog/2015/05/25/ioskai-yuan-xiang-mu-fxblurview/

收起阅读 »

你还没用Logger?用了他我才知道屌

Logger简单,漂亮,强大的android日志 配置下载 implementation 'com.orhanobut:logger:2.2.0' 初始化 Logger.addLogAdapter(new AndroidLogAdapter()); 使用 ...
继续阅读 »

Logger

简单,漂亮,强大的android日志


配置

下载


implementation 'com.orhanobut:logger:2.2.0'

初始化


Logger.addLogAdapter(new AndroidLogAdapter());

使用


Logger.d("hello");

输出


属性

Logger.d("debug");
Logger.e("error");
Logger.w("warning");
Logger.v("verbose");
Logger.i("information");
Logger.wtf("What a Terrible Failure");

支持字符串格式参数


Logger.d("hello %s", "world");

支持集合(仅适用于调试日志)


Logger.d(MAP);
Logger.d(SET);
Logger.d(LIST);
Logger.d(ARRAY);

Json和Xml支持(输出将处于调试级别)


Logger.json(JSON_CONTENT);
Logger.xml(XML_CONTENT);

高级用法

FormatStrategy formatStrategy = PrettyFormatStrategy.newBuilder()
.showThreadInfo(false) // (可选)是否显示线程信息。默认为 true
.methodCount(0) // (可选)要显示的方法行数。默认为 2
.methodOffset(7) // (可选)隐藏内部方法调用直到偏移量。默认值5
.logStrategy(customLog) // (可选)将日志策略更改为打印输出。默认LogCat
.tag("My custom tag") // (可选)每个日志的全局标记。默认PRETTY_LOGGER
.build();

Logger.addLogAdapter(new AndroidLogAdapter(formatStrategy));

日志开启

日志适配器通过检查此功能来检查日志是否应打印。


如果要禁用/隐藏输出日志,请重写isLoggable'方法。true会打印日志消息,false` 会忽略它。


Logger.addLogAdapter(new AndroidLogAdapter() {
@Override public boolean isLoggable(int priority, String tag) {
return BuildConfig.DEBUG;
}
});

将日志保存到文件

//TODO: 稍后将添加更多信息


Logger.addLogAdapter(new DiskLogAdapter());

将自定义标记添加到Csv格式策略


FormatStrategy formatStrategy = CsvFormatStrategy.newBuilder()
.tag("custom")
.build();

Logger.addLogAdapter(new DiskLogAdapter(formatStrategy));

工作原理


更多


  • 使用过滤器以获得更好的结果。或者你的自定义标签


  • 确保已禁用“环绕”选项


  • 也可以通过更改设置来简化输出。





  • Timber 集成
    // 将methodOffset设置为5以隐藏内部方法调用
    Timber.plant(new Timber.DebugTree() {
    @Override protected void log(int priority, String tag, String message, Throwable t) {
    Logger.log(priority, tag, message, t);
    }
    });


github地址:https://github.com/orhanobut/logger
下载地址:
master.zip


收起阅读 »

java设计模式:命令模式

前言在软件开发系统中,“方法的请求者”与“方法的实现者”之间经常存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对方法进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与实现者解耦?”变得很重要,命令模式就能很好地解决这个问题。 在现...
继续阅读 »

前言

在软件开发系统中,“方法的请求者”与“方法的实现者”之间经常存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对方法进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与实现者解耦?”变得很重要,命令模式就能很好地解决这个问题。


在现实生活中,命令模式的例子也很多。比如看电视时,我们只需要轻轻一按遥控器就能完成频道的切换,这就是命令模式,将换台请求和换台处理完全解耦了。电视机遥控器(命令发送者)通过按钮(具体命令)来遥控电视机(命令接收者)。


再比如,我们去餐厅吃饭,菜单不是等到客人来了之后才定制的,而是已经预先配置好的。这样,客人来了就只需要点菜,而不是任由客人临时定制。餐厅提供的菜单就相当于把请求和处理进行了解耦,这就是命令模式的体现。


定义与特点

命令(Command)模式的定义如下:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。


优点

通过引入中间件(抽象接口)降低系统的耦合度。
扩展性良好,增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,且满足“开闭原则”。
可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
可以在现有命令的基础上,增加额外功能。比如日志记录,结合装饰器模式会更加灵活。

缺点

可能产生大量具体的命令类。因为每一个具体操作都需要设计一个具体命令类,这会增加系统的复杂性。
命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构、解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接口),增加了理解上的困难。不过这也是设计模式的通病,抽象必然会额外增加类的数量,代码抽离肯定比代码聚合更加难理解。
命令模式的结构与实现
可以将系统中的相关操作抽象成命令,使调用者与实现者相关分离,其结构如下。

结构

抽象命令类(Command)角色:声明执行命令的接口,拥有执行命令的抽象方法 execute()。
具体命令类(Concrete Command)角色:是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
实现者/接收者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
调用者/请求者(Invoker)角色:是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。
在这里插入图片描述

实现

命令模式的代码如下:


package command;
public class CommandPattern {
public static void main(String[] args) {
Command cmd = new ConcreteCommand();
Invoker ir = new Invoker(cmd);
System.out.println("客户访问调用者的call()方法...");
ir.call();
}
}

//调用者


class Invoker {
private Command command;
public Invoker(Command command) {
this.command = command;
}
public void setCommand(Command command) {
this.command = command;
}
public void call() {
System.out.println("调用者执行命令command...");
command.execute();
}
}

//抽象命令


interface Command {
public abstract void execute();
}

//具体命令



class ConcreteCommand implements Command {
private Receiver receiver;
ConcreteCommand() {
receiver = new Receiver();
}
public void execute() {
receiver.action();
}
}

//接收者


class Receiver {
public void action() {
System.out.println("接收者的action()方法被调用...");
}
}

程序的运行结果如下:



客户访问调用者的call()方法...
调用者执行命令command...
接收者的action()方法被调用...

实例


假如我们开发一个播放器,播放器播放功能、拖动进度条功能、停止播放功能、暂停功能,我们在操作播发器的时候并不知道之间调用播放器
哪个功能,而是通过一个控制传达去传递指令给播放器内核,具体传达什么指令,会被封装成一个个按钮。那么每个按钮就相当于一条命令的封装。
用控制条实现了用户发送指令与播放器内核接收指令的解耦。下面来看代码,首先创建播放器内核类:



public class GPlayer {
public void play() {
System.out.println("正常播放");
}

public void speed() {
System.out.println("拖动进度条");
}

public void stop() {
System.out.println("停止播放");
}

public void pause() {
System.out.println("暂停播放");
}
}

创建命令接口:



public interface IAction {
void execute();
}

创建播放指令类:


public class PlayAction implements IAction {
private GPlayer gplayer;

public PlayAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.play();
}
}

创建暂停指令类:


public class PauseAction implements IAction {
private GPlayer gplayer;

public PauseAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.pause();
}
}

创建拖动进度条类:


public class SpeedAction implements IAction {
private GPlayer gplayer;

public SpeedAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.speed();
}
}

创建停止播放指令:


public class StopAction implements IAction {
private GPlayer gplayer;

public StopAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.stop();
}
}

创建控制条controller类:



public class Controller {
private List<IAction> actions = new ArrayList<IAction>();

public void addAction(IAction action) {
actions.add(action);
}

public void execute(IAction action) {
action.execute();
}

public void executes() {
for (IAction action : actions) {
action.execute();
}
actions.clear();
}
}

从上面代码来看,控制条可以执行单条命令,也可以批量执行多条命令。下面看客户端的测试代码:



public class Test {
public static void main(String[] args) {

GPlayer player = new GPlayer();
Controller controller = new Controller();
controller.execute(new PlayAction(player));

controller.addAction(new PauseAction(player));
controller.addAction(new PlayAction(player));
controller.addAction(new StopAction(player));
controller.addAction(new SpeedAction(player));
controller.executes();
}
}

由于控制条已经与播放器内核解耦了,以后如果想扩展新命令,只需要增加命令即可,控制条的结构无须改动。


java源码中的命令模式


首先来看 JDK 中的 Runnable 接口,Runnable 相当于命令模式中的抽象命令角色。Runnable 中的 run() 方法就当于 execute() 方法。



public interface Runnable {
public abstract void run();
}
public class T implements Runnable {
@Override
public void run() {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " : " + lazySingleton);
}
}

public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("Program End");
}
}

只要是实现了 Runnable 接口的类都被认为是一个线程,相当于命令模式中的具体命令角色。


实际上调用线程的 start() 方法之后,就有资格去抢 CPU 资源,而不需要编写获得 CPU 资源的逻辑。而线程抢到 CPU 资源后,就会执行 run() 方法中的内容,用 Runnable 接口把用户请求和 CPU 执行进行解耦。


收起阅读 »

Java设计模式:迭代器模式

前言在现实生活以及程序设计中,经常要访问一个聚合对象中的各个元素,如“数据结构”中的链表遍历,通常的做法是将链表的创建和遍历都放在同一个类中,但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了 “开闭原则”。 既然将遍历方法封装在聚合...
继续阅读 »

前言

在现实生活以及程序设计中,经常要访问一个聚合对象中的各个元素,如“数据结构”中的链表遍历,通常的做法是将链表的创建和遍历都放在同一个类中,但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了 “开闭原则”。


既然将遍历方法封装在聚合类中不可取,那么聚合类中不提供遍历方法,将遍历方法由用户自己实现是否可行呢?答案是同样不可取,因为这种方式会存在两个缺点:



  1. 暴露了聚合类的内部表示,使其数据不安全;
  2. 增加了客户的负担。

“迭代器模式”能较好地克服以上缺点,它在客户访问类与聚合类之间插入一个迭代器,这分离了聚合对象与其遍历行为,对客户也隐藏了其内部细节,且满足“单一职责原则”和“开闭原则”,如 Java 中的 Collection、List、Set、Map 等都包含了迭代器。


迭代器模式在生活中应用的比较广泛,比如:物流系统中的传送带,不管传送的是什么物品,都会被打包成一个个箱子,并且有一个统一的二维码。这样我们不需要关心箱子里是什么,在分发时只需要一个个检查发送的目的地即可。再比如,我们平时乘坐交通工具,都是统一刷卡或者刷脸进站,而不需要关心是男性还是女性、是残疾人还是正常人等信息。


定义与特点

提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。迭代器模式是一种对象行为型模式


优点


  • 访问一个聚合对象的内容而无须暴露它的内部表示。
  • 遍历任务交由迭代器完成,这简化了聚合类。
  • 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
  • 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
  • 封装性良好,为遍历不同的聚合结构提供一个统一的接口。

缺点

增加了类的个数,这在一定程度上增加了系统的复杂性。


在日常开发中,我们几乎不会自己写迭代器。除非需要定制一个自己实现的数据结构对应的迭代器,否则,开源框架提供的 API 完全够用。


结构

迭代器模式是通过将聚合对象的遍历行为分离出来,抽象成迭代器类来实现的,其目的是在不暴露聚合对象的内部结构的情况下,让外部代码透明地访问聚合的内部数据。现在我们来分析其基本结构与实现方法。



  • 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合对象以及创建迭代器对象的接口。
  • 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、first()、next() 等方法。
  • 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。

在这里插入图片描述

模式的实现

package net.biancheng.c.iterator;
import java.util.*;
public class IteratorPattern {
public static void main(String[] args) {
Aggregate ag = new ConcreteAggregate();
ag.add("中山大学");
ag.add("华南理工");
ag.add("韶关学院");
System.out.print("聚合的内容有:");
Iterator it = ag.getIterator();
while (it.hasNext()) {
Object ob = it.next();
System.out.print(ob.toString() + "\t");
}
Object ob = it.first();
System.out.println("\nFirst:" + ob.toString());
}
}
//抽象聚合
interface Aggregate {
public void add(Object obj);
public void remove(Object obj);
public Iterator getIterator();
}
//具体聚合
class ConcreteAggregate implements Aggregate {
private List<Object> list = new ArrayList<Object>();
public void add(Object obj) {
list.add(obj);
}
public void remove(Object obj) {
list.remove(obj);
}
public Iterator getIterator() {
return (new ConcreteIterator(list));
}
}
//抽象迭代器
interface Iterator {
Object first();
Object next();
boolean hasNext();
}
//具体迭代器
class ConcreteIterator implements Iterator {
private List<Object> list = null;
private int index = -1;
public ConcreteIterator(List<Object> list) {
this.list = list;
}
public boolean hasNext() {
if (index < list.size() - 1) {
return true;
} else {
return false;
}
}
public Object first() {
index = 0;
Object obj = list.get(index);
;
return obj;
}
public Object next() {
Object obj = null;
if (this.hasNext()) {
obj = list.get(++index);
}
return obj;
}
}

运行结果


聚合的内容有:中山大学    华南理工    韶关学院   
First:中山大学

java源码分析

Iterator


public interface Iterator<E> {

boolean hasNext();

E next();

default void remove() {
throw new UnsupportedOperationException("remove");
}

//剩余元素迭代
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}

上面是迭代器Iterator接口的代码,定义了一些需要子类实现的方法和默认的方法。在这里说一下上面两个default方法都是JDK1.8之后才有的接口新特性,在JDK1.8之前接口中不能有方法实体。


ArrayList


public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

public boolean hasNext() {
return cursor != size;
}

@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}

上面是简化的ArrayList类,因为具体实现迭代器Itr的类在ArrayList中作为内部类存在,这个内部类将接口中的方法做了具体实现,并且是只对ArrayList这个类进行实现的。


public interface List<E> extends Collection<E> {
Iterator<E> iterator();
}

上面是简化的List接口,充当的是聚合接口,可以看见内部创建了相应迭代器接口的方法。


public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
public Iterator<E> iterator() {
return new Itr();
}
}

上面是简化的ArrayList类,充当的是具体聚合类角色,在这里是直接返回了一个具体实现迭代器的类。


public class Test1 {

public static void main(String[] args) {
List<Integer> a=new ArrayList<>();
a.add(1);
a.add(2);
a.add(3);
Iterator itr=a.iterator();
while(itr.hasNext()){
System.out.println(itr.next());
}
}
}

收起阅读 »

java设计模式:中介者模式

前言在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。例如,每个人必须记住他(她)所有朋友的电话;而且,朋友中如果有人的电话修改了,他(她)必须让其他所有的朋友一起修改,这叫作“牵...
继续阅读 »

前言

在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。例如,每个人必须记住他(她)所有朋友的电话;而且,朋友中如果有人的电话修改了,他(她)必须让其他所有的朋友一起修改,这叫作“牵一发而动全身”,非常复杂。


如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。如前面所说的“每个人必须记住所有朋友电话”的问题,只要在网上建立一个每个朋友都可以访问的“通信录”就解决了。这样的例子还有很多,例如,你刚刚参加工作想租房,可以找“房屋中介”;或者,自己刚刚到一个陌生城市找工作,可以找“人才交流中心”帮忙。


在软件的开发过程中,这样的例子也很多,例如,在 MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者;还有大家常用的 QQ 聊天程序的“中介者”是 QQ 服务器。所有这些,都可以采用“中介者模式”来实现,它将大大降低对象之间的耦合性,提高系统的灵活性。
模式的定义与特点

定义

定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。


优点

类之间各司其职,符合迪米特法则。
降低了对象之间的耦合性,使得对象易于独立地被复用。
将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。

缺点

中介者模式将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。
模式的结构与实现
中介者模式实现的关键是找出“中介者”,下面对它的结构和实现进行分析。

结构

抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
具体中介者(Concrete Mediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
在这里插入图片描述

实现

中介者模式的实现代码如下:


package net.biancheng.c.mediator;
import java.util.*;
public class MediatorPattern {
public static void main(String[] args) {
Mediator md = new ConcreteMediator();
Colleague c1, c2;
c1 = new ConcreteColleague1();
c2 = new ConcreteColleague2();
md.register(c1);
md.register(c2);
c1.send();
System.out.println("-------------");
c2.send();
}
}
//抽象中介者
abstract class Mediator {
public abstract void register(Colleague colleague);
public abstract void relay(Colleague cl); //转发
}
//具体中介者
class ConcreteMediator extends Mediator {
private List<Colleague> colleagues = new ArrayList<Colleague>();
public void register(Colleague colleague) {
if (!colleagues.contains(colleague)) {
colleagues.add(colleague);
colleague.setMedium(this);
}
}
public void relay(Colleague cl) {
for (Colleague ob : colleagues) {
if (!ob.equals(cl)) {
((Colleague) ob).receive();
}
}
}
}
//抽象同事类
abstract class Colleague {
protected Mediator mediator;
public void setMedium(Mediator mediator) {
this.mediator = mediator;
}
public abstract void receive();
public abstract void send();
}
//具体同事类
class ConcreteColleague1 extends Colleague {
public void receive() {
System.out.println("具体同事类1收到请求。");
}
public void send() {
System.out.println("具体同事类1发出请求。");
mediator.relay(this); //请中介者转发
}
}
//具体同事类
class ConcreteColleague2 extends Colleague {
public void receive() {
System.out.println("具体同事类2收到请求。");
}
public void send() {
System.out.println("具体同事类2发出请求。");
mediator.relay(this); //请中介者转发
}
}

程序的运行结果如下:


具体同事类1发出请求。
具体同事类2收到请求。
-------------
具体同事类2发出请求。
具体同事类1收到请求。

应用场景

前面分析了中介者模式的结构与特点,下面分析其以下应用场景。
当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
当想创建一个运行于多个类之间的对象,又不想生成新的子类时。

java源码中的体现

在看其他人写的关于Timer 的中介者设计模式,我觉得写的都不是很清楚。我大概用源码来解释一下,顺便再分析一下Timer的所有关联类的源码:


private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");

// Constrain value of period sufficiently to prevent numeric
// overflow while still being effectively infinitely large.
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;

synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");

synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}

queue.add(task);
if (queue.getMin() == task)
queue.notify();
}
}

说明:所有的schedule方法都调用了sched ,那这个类的主要作用是啥呢?



将timertask加入到队列里,然后从队列里取出min任务(二叉堆的数据结构,下面会说明),判断如果min任务等于当前任务的话让队列wait的状态变为运行状态,如果不等于的话,那么线程的mainloop方法肯定是一直再运行状态的,其他任务就可以依次执行



看如下的源码


private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// Wait for queue to become non-empty
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die

// Queue nonempty; look at first evt and do the right thing
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}

Timer相当于中介者来执行队列里的任务,用户只管将任务抛给timer就可以了。


如下详细timer源码分析

在Java中,很常见的一个定时器的实现就是 Timer 类,用来实现定时、延迟执行、周期性执行任务的功能。


Timer 是定义在 java.util 中的一个工具类,提供简单的实现定时器的功能。和它配合使用的,是 TimerTask 类,这是对一个可以被调度的任务的封装。使用起来非常简单,如下示例:


// 定义一个可调度的任务,继承自 TimerTask
class FooTimerTask extends TimerTask {

@Override
public void run() {
// do your things
}
}

// 初始化Timer 定时器对象
Timer timer = new Timer("barTimer");

// 初始化需要被调度的任务对象
TimerTask task = new FooTimerTask();

// 调度任务。延迟1000毫秒后执行,之后每2000毫秒定时执行一次
timer.schedule(task, 1000, 2000);

以上,就是一个简单的使用Timer 的示例,下文将会分析Timer的源码实现。


概述

在Timer 机制中,涉及到的关键类如下:



  • Timer: 主要的调用的,提供对外的API;
  • TimerTask: 是一个抽象类,定义一个任务,继承自Runnable
  • TimerThread: 继承自 Thread,是一个自定义的线程类;
  • TaskQueue: 一个任务队列,包含有当前Timer的所有任务,内部使用二叉堆来实现。

以上几个关键类的引用关系如下:
在这里插入图片描述
简要描述的话,是:

1个 TimerThread —-> 实现1个 线程


1个 Timer对象 —-> 持有1个 TimerThread 对象


1个 Timer对象 —-> 持有1个 TimerQueue 对象


1个 TimerQueue 对象 —-> 持有 n个 TimerTask 对象


源码分析

Timer类的源码分析
源码分析的话,我们最好是按照Timer 的使用流程来分析。 首先,是Timer 的创建:

// Timer有四个构造方法,但是本质上其实是做的相同的事情,即
// 1. 使用name 和 isDeamon 两个参数给 thread 对象做了参数设置;
// 2. 调用 thread 的 start() 方法启动线程
public Timer() {
this("" + serialNumber());
}

public Timer(boolean isDaemon) {
this("" + serialNumber(), isDaemon);
}


public Timer(String name) {
thread.setName(name);
thread.start();
}

public Timer(String name, boolean isDaemon) {
thread.setName(name);
thread.setDaemon(isDaemon);
thread.start();
}

那么,或许大家会有一个疑问,thread 成员的初始化呢?这个时候,在代码里面找,就能发现:



// 这两个成员都是直接在声明的时候进行了初始化。
private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);

可以看到 thread 和 queue两个成员都是在声明的时候直接初始化的,并且有意思的是,两个成员都是 final 类型的,这也就意味着这两个成员一旦创建就不会再改了,等于说把 thread、queue 和 Timer 对象这三者的生命周期强行绑定在一起了,大家一起创建,并且一经创建将会无法改变。


然后,创建了Timer 后,与之相关的队列也已经创建成功,而且相关联的线程也启动了,就可以进行任务的调度了,我们看下它的任务调度方法:



// Timer 包含有一组重载方法,参数为以下几个:
// 1. TimerTask task:需要被调度的任务
// 2. long delay: 指定延迟的时间;
// 3. long period: 指定调度的执行周期;
schedule(TimerTask task, long delay, long period)

多个重载的调度方法在经过一些一些列的状态判断、参数设置、以及把delay时间转换成实际的执行时间等之后, 最终完成该功能的是 sched 方法,详情见注释部分:


这里涉及到一个需要留意的点,是在调用schedule 方法的时候,会根据TimerTask 的类型来进行不同的计算,进而给TimerTask设置不同的 period 参数,TimerTask 的类型有以下几种:



  • 非周期性任务;对应 TimerTask.period 值为0;
  • 周期性任务,但是没有delay值,即立即执行;对应 TimerTask.period 值为正数;
  • 周期性任务,同时包含有 delay值;对应 TimerTask.period 值为负数;

在schedule 方法中,会



// 执行任务调度的方法
// 这里的 time 已经是经过转换的,表示该task 需要被执行的时间戳
private void sched(TimerTask task, long time, long period) {
// 参数的合法性检查
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");

if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;

// 核心的调度逻辑
// 由于是在多线程环境中使用的,这里为了保证线程安全,使用的是 synchronized 代码段
// 对象锁使用的是在 Timer 对象中唯一存在的 queue 对象
synchronized(queue) {

// thread.newTasksMayBeScheduled 是一个标识位,在timer cancel之后 或者 thread 被停止后该标识位会被设为false
// newTasksMayBeScheduled 为false 则表示该timer 的关联线程已经停止了。
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");

// 这里是把外部的参数,如执行时间点、执行周期、设置状态等等。
// 这里为了线程安全的考虑,使用对 task 内部的 lock 对象加锁来保证。
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}

// 最后,把新的 task 添加到关联队列里面
queue.add(task);

// 这里,会使用打 TimerQueue 对象的 getMin() 方法,这个方法是获取到接下来将要被执行的TimerTask 对象
// 这里的逻辑是check 新添加的 task 对象是不是接下来马上会被执行
// 如果刚添加的对象是需要马上执行的话,会使用 queue.notify 来通知在等待的线程。

// 那么,会有谁在等待这个 notify 呢?是TimerThread 内部,TimerThread 会有一个死循环,在不停从queue中取任务来执行
// 当queue为空的时候,TimerThread 会进行 queue.wait() 来进行休眠的状态,直到有新的来任务来唤醒它
// 下面的代码就是,当queue为空的时候,这个判断条件会成立,然后就通知 TimerThread 重新唤醒
// 当然,下面的条件成立也不全是 queue 为空的情况下
if (queue.getMin() == task)
queue.notify();
}
}

TimerTask 的源码分析

接下来,本文将会分析 TimerTask 的源码。相对于Timer 来说,它的源码其实很简单,TimerTask 是实现了Runnable 接口,同时也是一个抽象类,它并没有对 Runnable 的 run() 方法提供实现,而是需要子类来实现。


它对外提供了以下几个功能:


包含有一段可以执行的代码(实现的Runnable 接口的run方法)
包含状态的定义。它有一个固定的状态:VIRGIN(新创建)、SCHEDULED(被调度进某一个 timer 的队列中了,但是还没有执行到)、EXECUTED(以及执行过了)、CANCELLED(任务被取消了)。
包含有取消的方法。
包含有获取下一次执行时间的方法。

相关的源码如下:



// 取消该任务
public boolean cancel() {
synchronized(lock) {
boolean result = (state == SCHEDULED);
state = CANCELLED;
return result;
}
}

// 根据执行周期,和设置的执行时间,来确定Task的下一次执行时间。
public long scheduledExecutionTime() {
synchronized(lock) {
// 其中,period 的值分为3种情况:
// 取值为0: 表示该Task是非周期性任务;
// 取值为正数: 表示该Task 是立即执行没有delay的周期性任务,period 的数值表示该Task 的周期
// 取值为负数: 表示该Task 是有 delay 的周期性任务,period 相反数是该Task 的周期
return (period < 0 ? nextExecutionTime + period
: nextExecutionTime - period);
}
}

TimerThread 的源码分析

TimerThread 首先是一个 Thread 的子类,而且我们知道,在Java中,一个Thread 的对象就是代表了一个JVM虚拟机线程。那么,这个 TimerThread 其实也就是一个线程。


对于一个线程来说,那么它的关键就是它的 run() 方法,在调用线程的 start() 方法启动线程之后,接下来就会执行线程的 run() 方法,我们看下 TimerThread 的run() 方法:



public void run() {
try {
// 启动 mainLoop() 方法,这是一个阻塞方法,正常情况下会一只阻塞在这里
// 当 mainLoop() 执行完毕的时候,也即是这个线程退出的时候。
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
// 做一些收尾工作
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}

从以上可以明确得看出,TimerThread 里的实现是调用 mainLoop() 启动了一个死循环,这个死循环内部的工作就是这个线程的具体工作了,一旦线程的死循环执行完毕,线程的 run 方法就执行完了,线程紧接着就退出了。熟悉Android的朋友可能已经觉得这里的实现非常眼熟了,没错,这里的实现和Android平台的 Handler + HandlerThread + Looper 的机制非常相像,可以认为Android平台最初研发这套机制的时候,就是参考的Timer 的机制,然后在上面做了些升级和适合Android平台的一些改动。


下面是 mainLoop() 方法:



private void mainLoop() {
// 一个死循环
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// 会等待到队列不为空,结合上面章节的分析,我们可以确定在新添加 TimerTask 到queue中的时候
// 会触发到 queue.notify() 然后通知到这里。
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();

// queue 为空,说明 timer 被取消了
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die


long currentTime, executionTime;
// 又一次看到这个 queue.getMin() ,这个是根据接下来的执行时间来获取下一个需要被执行的任务
task = queue.getMin();

// 需要修改 task对象的内部数值,使用synchronized 保证线程安全
synchronized(task.lock) {
// TimerTask 有多种状态,一旦一个 TimerTask 被取消之后,它就不会被执行了。
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}

// 获取到当前时间,和这个取出来的task 的下一次执行时间
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;

// 这里会check 当前这个 task 是不是已经到时间了
// 这里会把是否到时间了这个状态保存在 taskFired 里面
if (taskFired = (executionTime<=currentTime)) {
// 根据上文的分析,TimerTask 根据 task.period 值的不同,被分为3种类型
// 这里的 task.period == 0 的情况,是对应于一个非周期性任务
if (task.period == 0) {
// 非周期性任务,处理完就完事了,改状态,移除队列
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else {
// 周期性任务,会被重新调度,也不会被移除队列
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}

// 这里是另一个会等待的地方,这个是为了等待任务的到期,等待时间就是距离到执行之间的时长
if (!taskFired)
queue.wait(executionTime - currentTime);
}

// taskFired 变量经过上面的步骤以及判断过了,如果是 true,说明task以及到时间了
// 到时间就运行完事。
if (taskFired)
task.run();
} catch(InterruptedException e) {
}
}
}

TimerThread 中除了上面的主要逻辑之外,还有一些需要关注的地方,那就是它持有一个 TimerQueue 的对象,这个对象是在创建的时候外部传进来的,也是和当前的Timer 关联的TimerQueue:



// 这里的官方注释,说明了为什么是在TimerThread 中引用了 TimerQueue而不是引用了 Timer。
// 这么做是为了避免循环引用(因为Timer中引用了TimerThread),进而避免循环引用可能导致的JVM gc 失败的问题
// 我们都知道,Java 是一门通用的语言,虽然官方的HotSpot JVM中是能解决循环引用的GC问题的,但是这并不意味着
// 其他第三方的JVM也能解决循环引用导致的GC问题,所以这里干脆就避免了循环引用。

/**
* Our Timer's queue. We store this reference in preference to
* a reference to the Timer so the reference graph remains acyclic.
* Otherwise, the Timer would never be garbage-collected and this
* thread would never go away.
*/
private TaskQueue queue;

TimerQueue 的源码分析(主要是实现一个二叉堆)

TimerQueue 的逻辑上是一个队列,所有它包含有一个队列常见的那些方法,如 size()、add()、clear()等方法。下面我们找一些重要的方法进行分析:


首先,在上文的分析中,我们以及见过TimeQueue 的 getMin() 方法了,这个方法是获取当前的队列里面,接下来应该被执行的TimerTask,也就是说,是执行时间点 数值最小的那一个,那么我们就先看下它的源码:


/**
* Return the "head task" of the priority queue. (The head task is an
* task with the lowest nextExecutionTime.)
*/
TimerTask getMin() {
return queue[1];
}

What??? 就这吗?为啥这么简单?为啥就返回 queue[1] 就对了?


你是不是也有这样的疑问,那么带着疑问往下看吧。


接下来,是添加一个TimerTask 到队列中:



// 内部存放TimerTask 数据的,是一个数组,设置的数组初始大小是128
private TimerTask[] queue = new TimerTask[128];

// 存放当前的TimerTask 的数量
// 而且 TimerTask 是存放在 [1 - size] 位置的,数组的第0位置没有数据
// 至于为什么要 存放在 [1 - size] 请看下文。
private int size = 0;

/**
* Adds a new task to the priority queue.
*/
void add(TimerTask task) {
// check 下数据的容量是否还够添加,不够的话会先进行数组的扩容
// 这扩容一次就是2倍增加
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2*queue.length);

// 把新的TimerTask 放在数组的最后一个位置
// size 的初始化值是0,从这里可以看出来,这里会先把size自增1,然后再添加到数组中
// 其实是从数组位置的 1 开始添加 TimerTask 的,0的位置是空的
queue[++size] = task;

// 然后调用了这个数据上浮的方法
fixUp(size);
}

从上文看出,add 方法本身也没什么奇特的,就是很简单地把新的 TimerTask 放在了数据的最新的位置,只是里面调用了一下另一个方法 fixUp() ,好,那么我们接着分析这个方法:





// 从上文可以看出,参数 k 是当前的数组size 值,也是最后一个TimerTask 的下标索引
private void fixUp(int k) {
// 首先,这是一个循环,循环条件是 k > 1
while (k > 1) {
// 位运算,操作,把 k 右移一位,得到的结果是:偶数相当于除以2,奇数相当于先减1再除以2
int j = k >> 1;

// 比较 j 和 k 两个位置的下次执行时间,j 不大于 k 的话,就停止循环了
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;

// j 大于 k 位置的时间的话,就要进行下面的动作
// 这是一个典型的交换操作
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;

// k 值缩小到j,去逼近循环条件 k>1
k = j;
}
}

看了上面对 fixUp() 的分析,是不是仍然一脸懵?或许也有些熟悉算法的朋友已经觉察出些什么了,那么这个地方的逻辑是什么呢?


有了右移一位、[1, size]的区间等蛛丝马迹,我想聪明的你已经猜出来了,这个数组queue 里面,是存放了一个完全二叉树。


在发现 queue 数组是一个二叉树之后,再去理解上面的 fixUp() 方法其实就很简单了,里面的过程是这样的:


从二叉树的最后一个叶子结点开始循环;
获取这个叶子结点的父结点(完全二叉树中对应的父结点的索引是:子结点位运算右移一位得到的)
判断父结点和子结点中对应的 TimerTask 的 nextExecutionTime 的大小,如果父比子的小,则停止循环;如果父比子的大,则交互负责结点;
重复以上循环,直到遍历到根结点;

通过以上分析,能发现在每一次新增一个结点后,使用 fixUp(),方法直接对整个二叉树进行了重排序,使得 TimerTask 的nextExecutionTime 值最小的结点,永远被放置在了二叉树的根结点上,也即是queue[1]。这也就搞明白了为什么 getMin 的实现,是直接获取的 queue[1] 。


同样的道理,在每一次执行 Timer.purge() 方法,清理了TimerQueue中已经取消的Task之后,会执行另一个 fixDown() 方法,它的逻辑正好是和 fixUp() 相反的,它是从根结点开始遍历的,然后到达每一个叶子结点以整理这个二叉树,这里就不再赘述。


回过头来,我们再看下TimerQueue中的实现,会发现它其实是一个二叉堆,二叉堆是一个带有权重的二叉树,这里不再多说。


总结

通过以上的分析,总的来说,就是每一个 Timer对象中,包含有一个线程(TaskThread)和一个队列(TaskQueue)。TaskQueue 的实现是一个二叉堆(Binary Heap)的结构,二叉堆的每一个节点,就是 TimerTask 的对象。


收起阅读 »

Android RecyclerView 通用适配器

使用方式【最新版本号以这里为准】由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!由于JCenter关...
继续阅读 »

使用方式

【最新版本号以这里为准】

由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!
由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!
由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!

#last-version请查看上面的最新版本号

#只支持AndroidX

#从1.4.5版本开始GroupId、ArtifactId均有更新,请按如下方式引用
implementation "com.lwkandroid.library:rcvadapter:last-version"

基础功能

  • 快速实现适配器,支持多种ViewType模式
  • 支持添加HeaderView、FooterView、EmptyView
  • 支持滑到底部加载更多
  • 支持每条Item显示的动画
  • 支持嵌套Section(1.1.0版本新增)
  • 支持悬浮标签StickyLayout(1.2.0版本新增)

效果图






使用方式

1. 当Item样式一样时,只需继承RcvSingleAdapter<T>即可,示例:

public class TestSingleAdapter extends RcvSingleAdapter<TestData>
{
public TestSingleAdapter(Context context, List<TestData> datas)
{
super(context, android.R.layout.simple_list_item_1, datas);
}

@Override
public void onBindView(RcvHolder holder, TestData itemData, int position)
{
//在这里绑定UI和数据,RcvHolder中提供了部分快速设置数据的方法,详情请看源码
holder.setTvText(android.R.id.text1, itemData.getContent());
}
}


2. 当Item样式不一样时,即存在多种ViewType类型的Item,需要将每种ViewType的Item单独实现,再关联到RcvMultiAdapter<T>中,示例:

//第一步:每种Item分别继承RcvBaseItemView<T>
public class LeftItemView extends RcvBaseItemView<TestData>
{
@Override
public int getItemViewLayoutId()
{
//这里返回该Item的布局id
return R.layout.layout_item_left;
}

@Override
public boolean isForViewType(TestData item, int position)
{
//这里判断何时引用该Item
return position % 2 == 0;
}

@Override
public void onBindView(RcvHolder holder, TestData testData, int position)
{
//在这里绑定UI和数据,RcvHolder中提供了部分快速设置数据的方法,详情请看源码
holder.setTvText(R.id.tv_left, testData.getContent());
}
}

//第二步:将所有Item关联到适配器中
public class TestMultiAdapter extends RcvMultiAdapter<TestData>
{
public TestMultiAdapter(Context context, List<TestData> datas)
{
super(context, datas);
//只需在构造方法里将所有Item关联进来,无论多少种ViewType都轻轻松松搞定
addItemView(new LeftItemView());
addItemView(new RightItemView());
}
}


3.优雅的添加HeaderView、FooterView、EmptyView,只需要在RecyclerView设置LayoutManager后调用相关方法即可:

//要先设置LayoutManager
mRecyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));

//添加HeaderView(若干个)
mAdapter.addHeaderView(headerView01,headerView02,headerView03...);

//添加FooterView(若干个)
mAdapter.addFooterView(footerView01,footerView02,footerView03...);

//添加EmptyView(只能设置一个)
//设置了EmptyView后,当数据量为0的时候会显示EmptyView
mAdapter.setEmptyView(emptyView);
或者
mAdapter.setEmptyView(layoutId);


4.设置滑动到底部自动加载更多,先上示例代码吧:

自1.4.3版本开始删除了之前的调用方式

//可以先设置加载样式,继承RcvBaseLoadMoreView实现自定义样式
//不设置的话会使用默认的样式,参考RcvDefLoadMoreView源码
RcvDefLoadMoreView loadMoreView = new RcvDefLoadMoreView.Builder()
.setBgColor(Color.GREEN)
.setTextColor(Color.RED)
.build(this);
mAdapter.setLoadMoreLayout(loadMoreView);
//再开启并设置监听
mAdapter.enableLoadMore(true);
mAdapter.setOnLoadMoreListener(RcvLoadMoreListener listener);
//禁止加载更多,通常用在配合下拉刷新的过程中
mAdapter.enableLoadMore(false);

注:
① 默认的样式实现是类RcvDefLoadMoreView
② 如需自定义样式,只需继承RcvBaseLoadMoreView,只要重写各状态UI的实现,无须关心状态切换,可参考RcvDefLoadMoreView内的实现方式。

5.设置Item显示动画,先直接上代码:

//使用默认的动画(Alpha动画)
mAdapter.enableItemShowingAnim(true);

//使用自定义动画
mAdapter.enableItemShowingAnim(true, ? extends RcvBaseAnimation);

注:
①默认动画的实现是类RcvAlphaInAnim
②自定义样式需要继承RcvBaseAnimation,可参考RcvAlphaInAnim内部实现。

6.设置Item点击监听:

    //设置OnItemClickListener
mAdapter.setOnItemClickListener(new RcvItemViewClickListener<TestData>()
{
@Override
public void onItemViewClicked(RcvHolder holder, TestData testData, int position)
{
//onClick回调
}
});

//设置OnItemLongClickListener
mAdapter.setOnItemLongClickListener(new RcvItemViewLongClickListener<TestData>()
{
@Override
public void onItemViewLongClicked(RcvHolder holder, TestData testData, int position)
{
//onLongClick回调
}
});


7. 添加分割线,直接上代码:

1.2.9版本针对分割线进行了重写,原有方法不变,新增支持自定义颜色和部分快速创建的方法:

#适用于LinearLayoutManager
//创建默认竖直排列的分割线
RcvLinearDecoration.createDefaultVertical(Context context);
//创建自定义色值默认竖直排列的分割线
RcvLinearDecoration.createDefaultVertical(int color);
//创建默认水平排列的分割线
RcvLinearDecoration.createDefaultHorizontal(Context context);
//创建自定义色值默认水平排列的分割线
RcvLinearDecoration.createDefaultHorizontal(int color);
//构造方法:默认Drawable分割线
new RcvLinearDecoration(Context context, int orientation);
//构造方法:自定义Drawable分割线
new RcvLinearDecoration(Context context, Drawable drawable, int orientation);
//构造方法:自定义Drawable分割线
new RcvLinearDecoration(Context context, @DrawableRes int drawableResId, int orientation);
//构造方法:自定义Color分割线(宽度或者高度默认1px)
new RcvLinearDecoration(@ColorInt int color, int orientation);
//构造方法:自定义Color分割线
new RcvLinearDecoration(@ColorInt int color, int size, int orientation);

#适用于GridLayoutManager、StaggeredGridLayoutManager
//创建默认分割线
RcvGridDecoration.createDefault(Context context);
//创建自定义色值默认分割线
RcvGridDecoration.createDefault(int color);
//构造方法:默认Drawable的分割线
new RcvGridDecoration(Context context);
//构造方法:自定义Drawable的分割线
new RcvGridDecoration(Context context, Drawable drawable);
//构造方法:自定义Drawable的分割线
new RcvGridDecoration(Context context, @DrawableRes int drawableResId);
//构造方法:自定义Color的分割线(默认分割线宽高均为1px)
new RcvGridDecoration(@ColorInt int color);
//构造方法:自定义Color的分割线
new RcvGridDecoration(@ColorInt int color, int width, int height);

注:
①是直接设置给RecyclerView的,不是设置给适配器的,不要看错哦
②支持自定义drawable当分割线

8.嵌套Section,稍微复杂一点,配合代码讲解:

1.4.0版本开始删除以前的使用方法,采用下面的方式

带有Section功能的适配器为RcvSectionMultiLabelAdapterRcvSectionSingleLabelAdapter,需要指定两个泛型,第一个代表Section,第二个代表普通数据Data, 两者都支持多种Data类型的子布局,唯一不同的是,RcvSectionMultiLabelAdapter还支持多种Section类型的子布局,但不可以和RcvStickyLayout联动,而RcvSectionSingleLabelAdapter 仅支持一种Section类型的子布局,但是可以和RcvStickyLayout联动。需要注意的是,传给适配器的数据均需要自行预处理,用RcvSectionWrapper封装后才可传入适配器。

#只有一种Section类型,配合多种Data类型的适配器
public class TestSectionAdapter extends RcvSectionSingleLabelAdapter<TestSection, TestData>
{
public TestSectionAdapter(Context context, List<RcvSectionWrapper<TestSection, TestData>> datas)
{
super(context, datas);
}

@Override
protected RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>[] createDataItemViews()
{
return new RcvBaseItemView[]{new DataItemView01(), new DataItemView02()};
}

@Override
public int getSectionLabelLayoutId()
{
return R.layout.layout_section_label;
}

@Override
public void onBindSectionLabelView(RcvHolder holder, TestSection section, int position)
{
holder.setTvText(R.id.tv_section_label, section.getSection());
}

//第一种Data ItemView
private class DataItemView01 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.adapter_item_long;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return !item.isSection() && item.getData().getType() == 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
TextView textView = holder.findView(R.id.tv_item_long);
textView.setBackgroundColor(Color.GREEN);
textView.setText("第一种数据类型:" + wrapper.getData().getContent());
}
}

//第二种Data ItemView
private class DataItemView02 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.adapter_item_short;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return !item.isSection() && item.getData().getType() != 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
TextView textView = holder.findView(R.id.tv_item_short);
textView.setBackgroundColor(Color.RED);
textView.setText("第二种数据类型:" + wrapper.getData().getContent());
}
}
}

#多种Section类型,配合多种Data类型的适配器
public class TestSectionMultiLabelAdapter extends RcvSectionMultiLabelAdapter<TestSection, TestData>
{
public TestSectionMultiLabelAdapter(Context context, List<RcvSectionWrapper<TestSection, TestData>> datas)
{
super(context, datas);
}

@Override
protected RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>[] createLabelItemViews()
{
return new RcvBaseItemView[]{new LabelItemView01(), new LabelItemView02()};
}

@Override
protected RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>[] createDataItemViews()
{
return new RcvBaseItemView[]{new DataItemView01(), new DataItemView02()};
}


//第一种Label ItemView
private class LabelItemView01 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.layout_section_label;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return item.isSection() && item.getSection().getType() == 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
holder.setTvText(R.id.tv_section_label, wrapper.getSection().getSection());
}
}

//第二种Label ItemView
private class LabelItemView02 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.layout_section_label02;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return item.isSection() && item.getSection().getType() != 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
holder.setTvText(R.id.tv_section_label, wrapper.getSection().getSection());
}
}

//第一种Data ItemView
private class DataItemView01 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.adapter_item_long;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return !item.isSection() && item.getData().getType() == 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
TextView textView = holder.findView(R.id.tv_item_long);
textView.setBackgroundColor(Color.GREEN);
textView.setText("第一种数据类型:" + wrapper.getData().getContent());
}
}

//第二种Data ItemView
private class DataItemView02 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.adapter_item_short;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return !item.isSection() && item.getData().getType() != 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
TextView textView = holder.findView(R.id.tv_item_short);
textView.setBackgroundColor(Color.RED);
textView.setText("第二种数据类型:" + wrapper.getData().getContent());
}
}
}

注:
①传给适配器的数据集合内实体类必须经过RcvSectionWrapper包装。
②向外公布的方法(例如点击监听)的实体类泛型不能传错。

9.悬浮标签StickyLayout

适配器方面无需改动,直接使用RcvSectionSingleLabelAdapter即可,在RecyclerView同级布局下添加RcvStickyLayout,然后在代码中关联起来即可:

    // xml布局中添加RcvStickyLayout:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.v7.widget.RecyclerView
android:id="@+id/rcv_sticky"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

<com.lwkandroid.rcvadapter.ui.RcvStickyLayout
android:id="@+id/stickyLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</FrameLayout>



//代码中关联RecyclerView
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rcv_sticky);
/...省略设置RecyclerView的LayoutMananger和Adapter.../
RcvStickyLayout stickyLayout = (RcvStickyLayout) findViewById(R.id.stickyLayout);
stickyLayout.attachToRecyclerView(recyclerView);

上面就是大部分基础功能的使用方法了,想了解更多方法请看源码。

混淆配置

-dontwarn com.lwkandroid.rcvadapter.**
-keep class com.lwkandroid.rcvadapter.**{*;}


待实现功能

  • 暂时未想到

开源参考

  1. https://github.com/hongyangAndroid/baseAdapter
  2. https://github.com/CymChad/BaseRecyclerViewAdapterHelper
收起阅读 »

一行代码解决RxJava 内存泄漏

xLifeRxLife,相较于trello/RxLifecycle、uber/AutoDispose,具有如下优势:直接支持在主线程回调支持在子线程订阅观察者简单易用,学习成本低性能更优,在实现上更加简单友情提示: RxLife与RxHttp搭配使用,味道更佳...
继续阅读 »

xLife

RxLife,相较于trello/RxLifecycleuber/AutoDispose,具有如下优势:

  • 直接支持在主线程回调
  • 支持在子线程订阅观察者
  • 简单易用,学习成本低
  • 性能更优,在实现上更加简单

友情提示: RxLife与RxHttp搭配使用,味道更佳

RxLife详细介绍:https://juejin.im/post/5cf3e1235188251c064815f1

Gradle引用

jitpack添加到项目的build.gradle文件中,如下:

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

注:RxLife 2.1.0 版本起,已全面从JCenter迁移至jitpack

新版本仅支持AndroidX项目

dependencies {
//kotlin协程
implementation 'com.github.liujingxing.rxlife:rxlife-coroutine:2.1.0'

//rxjava2
implementation 'com.github.liujingxing.rxlife:rxlife-rxjava2:2.1.0'

//rxjava3
implementation 'com.github.liujingxing.rxlife:rxlife-rxjava3:2.1.0'
}

注意:RxJava2 使用Rxlife.asXxx方法;RxJava3使用Rxlife.toXxx方法

非AndroidX项目

非AndroidX项目,请使用旧版本RxLife

implementation 'com.rxjava.rxlife:rxlife:2.0.0'

由于Google在19年就停止了非AndroidX库的更新,故rxlife旧版本不再维护,请尽快将项目迁移至AndroidX

#Usage

1、FragmentActivity/Fragment

FragmentActivity/Fragment销毁时,自动关闭RxJava管道

Observable.timer(5, TimeUnit.SECONDS)
.as(RxLife.as(this)) //此时的this FragmentActivity/Fragment对象
.subscribe(aLong -> {
Log.e("LJX", "accept =" + aLong);
});

2、View

View被移除时,自动关闭RxJava管道

Observable.timer(5, TimeUnit.SECONDS)
.as(RxLife.as(this)) //此时的this 为View对象
.subscribe(aLong -> {
Log.e("LJX", "accept =" + aLong);
});

3、ViewModel

Activity/Fragment销毁时,自动关闭RxJava管道,ViewModel需要继承ScopeViewModel类,如下

public class MyViewModel extends ScopeViewModel {

public MyViewModel(@NonNull Application application) {
super(application);
}

public void test(){
Observable.interval(1, 1, TimeUnit.SECONDS)
.as(RxLife.asOnMain(this)) //继承ScopeViewModel后,就可以直接传this
.subscribe(aLong -> {
Log.e("LJX", "MyViewModel aLong=" + aLong);
});
}
}

注意: 一定要在Activity/Fragment通过以下方式获取ViewModel对象,否则RxLife接收不到生命周期的回调


MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);

4、任意类

Activity/Fragment销毁时,自动关闭RxJava管道,任意类需要继承BaseScope类,如P层:

public class Presenter extends BaseScope {

public Presenter(LifecycleOwner owner) {
super(owner); //添加生命周期监听
}

public void test(){
Observable.interval(1, 1, TimeUnit.SECONDS)
.as(RxLife.as(this)) //继承BaseScope后,就可以直接传this
.subscribe(aLong -> {
Log.e("LJX", "accept aLong=" + aLong);
});
}
}

5、kotlin用户

由于as是kotlin中的一个关键字,所以在kotlin中,我们并不能直接使用as(RxLife.as(this)),可以如下编写

Observable.intervalRange(1, 100, 0, 200, TimeUnit.MILLISECONDS)
.`as`(RxLife.`as`(this))
.subscribe { aLong ->
Log.e("LJX", "accept=" + aLong)
}

当然,相信没多少人会喜欢这种写法,故,RxLife针对kotlin用户,新增更为便捷的写法,如下:

Observable.intervalRange(1, 100, 0, 200, TimeUnit.MILLISECONDS)
.life(this)
.subscribe { aLong ->
Log.e("LJX", "accept=" + aLong)
}

使用life 操作符替代as操作符即可,其它均一样

6、小彩蛋

asOnMain操作符

RxLife还提供了asOnMain操作符,它可以指定下游的观察者在主线程中回调,如下:

Observable.timer(5, TimeUnit.SECONDS)
.as(RxLife.asOnMain(this))
.subscribe(aLong -> {
//在主线程回调
Log.e("LJX", "accept =" + aLong);
});

//等价于
Observable.timer(5, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.as(RxLife.as(this))
.subscribe(aLong -> {
//在主线程回调
Log.e("LJX", "accept =" + aLong);
});

kotlin 用户使用lifeOnMain替代asOnMain操作符,其它均一样

注意: RxLife类里面as操作符,皆适用于Flowable、ParallelFlowable、Observable、Single、Maybe、Completable这6个被观察者对象

混淆

RxLife作为开源库,可混淆,也可不混淆,如果不希望被混淆,请在proguard-rules.pro文件添加以下代码

-keep class com.rxjava.rxlife.**{*;}


代码下载:327744707-rxjava-RxLife-master.zip

收起阅读 »

Android替换系统dialog风格后的通用提示框工具类

DialogUtilsApp一、介绍替换系统dialog风格后的通用提示框工具类,可以覆盖lib下的定义资源,改变现有的颜色风格,需要改变布局风格,可下载项目后自行调整APP 使用示例项目,libs下含有已编译最新的aar资源。dialogutilslib a...
继续阅读 »

DialogUtilsApp

一、介绍

替换系统dialog风格后的通用提示框工具类,可以覆盖lib下的定义资源,改变现有的颜色风格,需要改变布局风格,可下载项目后自行调整

  • APP 使用示例项目,libs下含有已编译最新的aar资源。
  • dialogutilslib arr资源项目,需要引入的资源包项目。
  • aar文件生成,在工具栏直接Gradle - (项目名) - dialogutilslib - Tasks - build - assemble,直到编译完成
  • aar文件位置,打开项目所在文件夹,找到 dialogutilslib\build\outputs\aar 下。

二、工程引入工具包准备

下载项目,可以在APP项目的libs文件下找到DialogUtilsLib.aar文件(已编译为最新版),引入自己的工程 引入aar

dependencies {
implementation files('libs/DialogUtilsLib-release.aar')
...
}

三、使用

注意下方只做了基础展示,dialog的都会返回对应的utils对象,registerActivityLifecycleCallbacks方法设置后,activity销毁时会自动把显示在此activity上的dialog一起关闭。

  • application初始化设置
public class App extends Application {

@Override
public void onCreate() {
super.onCreate();

//初始化dialog工具类设置
DialogLibInitSetting.getInstance()
//设置debug
.setDebug(BuildConfig.DEBUG)
//注册全局activity生命周期监听
.registerActivityLifecycleCallbacks(this);

}
}
  • 普通dialog
            DialogLibCommon.create(this)
.setMessage("普通对话框1")
.setAlias("text1")
.setOnBtnMessage(()->{
//描述区域点击时触发
})
.noShowCancel()
.show();
  • 自定义dialog
            ImageView imageView = new ImageView(this);
imageView.setImageDrawable(getResources().getDrawable(R.mipmap.ic_launcher));
DialogLibCustom.create(this)
.noShowCancel()
.setAlias("text2")
.show(imageView);
  • 输入型dialog
            DialogLibInput.create(this)
.setMessage("输入信息")
.setAlias("text3")
//自动弹出键盘
.setPopupKeyboard()
.setOnBtnOk(str -> {
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
return true;
})
.show();
  • 等待型dialog
            DialogLibLoading.create(this)
.setTimeoutClose(2000)
.setAlias("text4")
.setOnLoading(() -> {
Toast.makeText(MainActivity.this, "我是显示对话框前触发的", Toast.LENGTH_SHORT).show();
})
.show();
  • 完全自定义型dialog
            final DialogLibAllCustom dialog = DialogLibAllCustom.create(this)
.setCancelable(true)
.setAlias("text5");

TextView view = new TextView(this);
view.setBackgroundResource(R.color.design_default_color_secondary);
view.setText("这是一个完全自定义布局的对话框,对话框显示后需要手动关闭");
view.setOnClickListener(v2 -> {
dialog.closeDialog();
});

dialog.show(view);
  • 密码输入型dialog
              DialogLibInput.create(this)
.setMessage("123")
.setLength(6)
.setInputType(EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD)
.setAlias("text6")
//设置显示密码隐藏/显示图片
.setShowLookPassword()
//自动弹出键盘
.setPopupKeyboard()
.setOnBtnOk(str -> {
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
return true;
})
.show();

四、资源覆盖,改变颜色、字体大小、默认文字

  • colors下可覆盖资源及注释,默认黑色和白色不建议覆盖,前景色:字体的颜色,背景色:布局的背景色
<resources>
<!--黑色-->
<color name="dialog_utils_lib_black">#FF000000</color>
<!--白色-->
<color name="dialog_utils_lib_white">#FFFFFFFF</color>

<!--dialog的标题文字的前景色,适用于所有带标题的dialog-->
<color name="dialog_utils_lib_title_fg">@color/dialog_utils_lib_black</color>
<!--dialog的 确认 按钮文字的前景色-->
<color name="dialog_utils_lib_ok_fg">@color/dialog_utils_lib_white</color>
<!--dialog的 取消 按钮文字的前景色-->
<color name="dialog_utils_lib_cancel_fg">@color/dialog_utils_lib_white</color>
<!--dialog的 确认 按钮文字的背景色-->
<color name="dialog_utils_lib_ok_bg">#22C5A3</color>
<!--dialog的 取消 按钮文字的背景色-->
<color name="dialog_utils_lib_cancel_bg">#F8A01A</color>
<!--dialog的输入框下方显示2个按钮时,中间分隔的背景色-->
<color name="dialog_utils_lib_button_split_bg">@color/dialog_utils_lib_white</color>

<!--dialog的内容文字的前景色,适用于 DialogLibCommonUtils-->
<color name="dialog_utils_lib_content_fg">@color/dialog_utils_lib_black</color>

<!--dialog的输入框文字的前景色,适用于 DialogLibInputUtils-->
<color name="dialog_utils_lib_input_fg">@color/dialog_utils_lib_black</color>
<!--dialog的输入框下方分割线的背景色,适用于 DialogLibInputUtils-->
<color name="dialog_utils_lib_input_split_line">@color/dialog_utils_lib_ok_bg</color>

<!--dialog的加载框加载等待区域的背景色-->
<color name="dialog_utils_lib_loading_content_bg">#FFc4c4c4</color>
<!--dialog的加载框加载等待区域文字提示的前景色-->
<color name="dialog_utils_lib_loading_content_text_fg">@color/dialog_utils_lib_white</color>
</resources>
  • dimens下字体大小资源
<resources>
<dimen name="dialog_utils_lib_text_size_normal">14sp</dimen>

<!--标题字体大小,统一设定-->
<dimen name="dialog_utils_lib_title_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--确定 字体大小,统一设定-->
<dimen name="dialog_utils_lib_ok_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--取消 字体大小,统一设定-->
<dimen name="dialog_utils_lib_cancel_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--内容 字体大小,适用于 DialogLibCommonUtils的提示内容区域-->
<dimen name="dialog_utils_lib_content_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--输入框 字体大小,适用于 DialogLibInputUtils 输入区域-->
<dimen name="dialog_utils_lib_input_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--加载框 字体大小,适用于 DialogLibLoadingUtils 提示内容区域-->
<dimen name="dialog_utils_lib_loading_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>

<!--dialog 宽度占屏幕宽度的百分比,取值0-1之间,不包含边界,竖屏时的系数-->
<item name="dialog_utils_lib_portrait_width_factor" format="float" type="dimen">0.85</item>
<!--dialog 宽度占屏幕宽度的百分比,取值0-1之间,不包含边界,横屏时的系数-->
<item name="dialog_utils_lib_landscape_width_factor" format="float" type="dimen">0.5</item>
</resources>
  • strings下资源定义,注意:如果你的项目存在多语言,则必须覆盖
<resources>
<string name="dialog_utils_lib_ok">确定</string>
<string name="dialog_utils_lib_cancel">取消</string>
<string name="dialog_utils_lib_default_title">提示</string>
<string name="dialog_utils_lib_data_processing">数据处理中…</string>
</resources>
  • mipmap下资源定义,注意:此2张图片为密码输入时显示/隐藏按钮的图片,png格式
dialog_utils_lib_password_hide 隐藏图片命名
dialog_utils_lib_password_show 显示图片命名

代码下载:mjsoftking-dialog-utils-app-master.zip

收起阅读 »

30秒上手的HTTP请求库

RxHttp主要优势1. 30秒即可上手,学习成本极低2. 史上最优雅的支持 Kotlin 协程3. 史上最优雅的处理多个BaseUrl及动态BaseUrl4. 史上最优雅的对错误统一处理,且不打破Lambda表达式5. 史上最优雅的文件上传/下载/断点下载/...
继续阅读 »

RxHttp


主要优势

1. 30秒即可上手,学习成本极低

2. 史上最优雅的支持 Kotlin 协程

3. 史上最优雅的处理多个BaseUrl及动态BaseUrl

4. 史上最优雅的对错误统一处理,且不打破Lambda表达式

5. 史上最优雅的文件上传/下载/断点下载/进度监听,已适配Android 10

6. 支持Gson、Xml、ProtoBuf、FastJson等第三方数据解析工具

7. 支持Get、Post、Put、Delete等任意请求方式,可自定义请求方式

8. 支持在Activity/Fragment/View/ViewModel/任意类中,自动关闭请求

9. 支持全局加解密、添加公共参数及头部、网络缓存,均支持对某个请求单独设置

请求三部曲

上手教程

30秒上手教程:30秒上手新一代Http请求神器RxHttp

协程文档:RxHttp ,比Retrofit 更优雅的协程体验

掘金详细文档:RxHttp 让你眼前一亮的Http请求框架

wiki详细文档:https://github.com/liujingxing/rxhttp/wiki (此文档会持续更新)

自动关闭请求用到的RxLife类,详情请查看RxLife库

更新日志      遇到问题,点击这里,99%的问题都能自己解决

上手准备

Maven依赖点击这里

1、RxHttp目前已适配OkHttp 3.12.0 - 4.9.1版本(4.3.0版本除外), 如你想要兼容21以下,请依赖OkHttp 3.12.x,该版本最低要求 API 9

2、asXxx方法内部是通过RxJava实现的,而RxHttp 2.2.0版本起,内部已剔除RxJava,如需使用,请自行依赖RxJava并告知RxHttp依赖的Rxjava版本

必须

jitpack添加到项目的build.gradle文件中,如下:

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

注:RxHttp 2.6.0版本起,已全面从JCenter迁移至jitpack

//使用kapt依赖rxhttp-compiler时必须
apply plugin: 'kotlin-kapt'

android {
//必须,java 8或更高
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
implementation 'com.github.liujingxing.rxhttp:rxhttp:2.6.1'
implementation 'com.squareup.okhttp3:okhttp:4.9.1' //rxhttp v2.2.2版本起,需要手动依赖okhttp
kapt 'com.github.liujingxing.rxhttp:rxhttp-compiler:2.6.1' //生成RxHttp类,纯Java项目,请使用annotationProcessor代替kapt
}

可选

android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [
//使用asXxx方法时必须,告知RxHttp你依赖的rxjava版本,可传入rxjava2、rxjava3
rxhttp_rxjava: 'rxjava3',
rxhttp_package: 'rxhttp' //非必须,指定RxHttp类包名
]
}
}
}
}
dependencies {
implementation 'com.github.liujingxing.rxlife:rxlife-coroutine:2.1.0' //管理协程生命周期,页面销毁,关闭请求

//rxjava2 (RxJava2/Rxjava3二选一,使用asXxx方法时必须)
implementation 'io.reactivex.rxjava2:rxjava:2.2.8'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'com.github.liujingxing.rxlife:rxlife-rxjava2:2.1.0' //管理RxJava2生命周期,页面销毁,关闭请求

//rxjava3
implementation 'io.reactivex.rxjava3:rxjava:3.0.6'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation 'com.github.liujingxing.rxlife:rxlife-rxjava3:2.1.0' //管理RxJava3生命周期,页面销毁,关闭请求

//非必须,根据自己需求选择 RxHttp默认内置了GsonConverter
implementation 'com.github.liujingxing.rxhttp:converter-fastjson:2.6.1'
implementation 'com.github.liujingxing.rxhttp:converter-jackson:2.6.1'
implementation 'com.github.liujingxing.rxhttp:converter-moshi:2.6.1'
implementation 'com.github.liujingxing.rxhttp:converter-protobuf:2.6.1'
implementation 'com.github.liujingxing.rxhttp:converter-simplexml:2.6.1'
}

最后,rebuild一下(此步骤是必须的) ,就会自动生成RxHttp类

混淆

RxHttp v2.2.8版本起,无需添加任何混淆规则(内部自带混淆规则),v2.2.8以下版本,请查看混淆规则,并添加到自己项目中

代码下载:327744707-okhttp-RxHttp-master.zip







收起阅读 »

你确定你会写代码---iOS规范补充

Pod update注意1、先执行pod repo update 公司内部库specs2、再执行pod update --no-repo-update这样就不会update github_specs,速度快JSONSerialization涉及到JSON Ob...
继续阅读 »

Pod update注意

1、先执行pod repo update 公司内部库specs
2、再执行pod update --no-repo-update这样就不会update github_specs,速度快

JSONSerialization

涉及到JSON Object<->NSData数据转换的地方,注意对NSError的处理和JSON Object合法性的校验,如:

BOOL validate = [NSJSONSerialization isValidJSONObject:parament];
if (!validate) {
// 对不是合法的JSON对象错误进行处理
return;
}
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:parament options:NSJSONWritingPrettyPrinted error:&error];
if (error) {
// 对数据转换错误进行处理
return;
}

合法JSON对象满足:
1、Top level object is an NSArray or NSDictionary
2、All objects are NSString, NSNumber, NSArray, NSDictionary, or NSNull
3、All dictionary keys are NSStrings
4、NSNumbers are not NaN or infinity

补充一些代码规范、开发约定

写if else语句时可以else不换行紧跟if的}括号,但写if else if时,为了保持条件{}的可读性,务必请换行书写:

// if else
BOOL flag = YES;
if (flag) {

} else {

}

// if else if
BOOL flag = YES;
BOOL elseIfFlag = (1+1-1+2 == 5);
if (flag) {

}
// 这里换行书写
else if(elseIfFlag) {

}

对于@property声明的属性,如果初始化设置复杂,请采用懒加载getters方式,对于简单初始化的,应在.m文件中提供统一的-initData初始化数据的方法。

/// 懒加载方式-内部配置
@property(nonatomic, strong)UIView *redView;
/// 统一初始化
@property(nonatomic, strong)NSMutableArray *dataSourceArray;

/// 统一数据初始化
- (void)initData{
_dataSourceArray = [NSMutableArray new];
}

/// 懒加载
- (UIView *)redView{
if (!_redView) {
_redView = [UIView new];
_redView.backgroundColor = UIColor.redColor;
}
return _redView;
}

对于NSDictionary、NSArray等的初始化,为提高可读性起见,建议采用语法糖的初始化方式:

_dataSourceArray = [@[@"1", @"2"] mutableCopy];
_parameters = [@{@"action": @"add", @"id": @"22"} mutableCopy];

// X: 不推荐这样做
_dataSourceArray = [NSMutableArray new];
[_dataSourceArray addObject:@"1"];
[_dataSourceArray addObject:@"2"];

_parameters = [NSMutableDictionary new];
[_parameters setValue:@"add" forKey:@"action"];
[_parameters setValue:@"22" forKey:@"id"];

对于Category中的对外公有方法,务必采用categoryName_funcName的命名方式,以区别于主类里没有前缀的方法:

// TALPlayer+LogReport.h

/// 加载播放器
- (void)logReport_loadPlayer;
/// 开始播放
- (void)logReport_startPlay;

对于Category里的私有同名方法,可采用下划线方式如_mainClassFuncName以区别.
对于主类里的私有属性,在多个Category访问时,可采用属性中间件的方式,拆出一个独立的MainClass+InternalProperty来提供一些getters方法:

// TALPlayer+InternalProperty.h

- (TALPlayerLogModel *)internalProperty_logModel;
- (TALPlayerStaticsModel *)internalProperty_staticsModel;


// TALPlayer+InternalProperty.m

- (TALPlayerLogModel *)internalProperty_logModel{
// 这里为方便以后调试断点用,建议拆开2行写
id value = [self valueForKey:@"logModel"];
return value;
}

对于需要跟服务器交互的网络请求参数字符串,务必独立出对应Category的DataInfoKeys扩展文件,方便查询、注释、全局引用、修改和拼写纠错:

// TALPlayer+LogReportDataInfoKeys.h

/// action
extern NSString *const LogReportDataInfoActionKey;
/// 心跳
extern NSString *const LogReportDataInfoActionHeartBeatKey;
/// 严重卡顿
extern NSString *const LogReportDataInfoActionSeriousBufferKey;


// TALPlayer+LogReportDataInfoKeys.m

// action
NSString *const LogReportDataInfoActionKey = @"action";
/// 心跳
NSString *const LogReportDataInfoActionHeartBeatKey = @"heartbeat";
/// 严重卡顿
NSString *const LogReportDataInfoActionSeriousBufferKey = @"seriousbuffer";


// 使用
#import "TALPlayerLogReportDataInfoKeys.h"

NSMutableDictionary *info = [NSMutableDictionary new];
info[LogReportDataInfoActionKey] = LogReportDataInfoActionHeartBeatKey;
// info[XXXKey] = value;

对于.h及.m文件中默认#pragma mark的规范,推荐如下:

// XXX.h

#pragma mark - Protocol

#pragma mark - Properties

#pragma mark - Methods

// XXX.m

#pragma mark - Consts

#pragma mark - UI Components

#pragma mark - Data Properties

#pragma mark - Initial Methods

#pragma mark - Lifecycle Methods

#pragma mark - Override Methods

#pragma mark - Public Methods

#pragma mark - Private Methods

#pragma mark - XXXDelegate

#pragma mark - Getters

#pragma mark - Setters

如上相关#pragma字符在Xcode中的自动配置,有机会我会单独分享给大家。
Xcode FileTemplate路径:Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates/Source/Cocoa Touch Class.xctemplate/


对于OC文件中注释规范说明:

//MARK: mark here(类似于#pragma mark,只不过没有横线)

//TODO: todo here

//FIXME: fix me later

//???: What is this shit

//!!!: DO NOT TOUCH MY CODE

说明:
对于单行注释,尽量用//代替/**/格式,,务必请在//后加一个空格,再进行内容补充,如:// 这是单行注释而不要写成//这是单行注释
对于SDK内部私有方法,如果无参数,则采用/// 这是无参数方法注释格式;
对于SDK需要向外暴露的接口方法注释,请务必按照AppleDoc编写,写明@param、@return等:

/**
* 这是一个demo方法
* @param param1 第一个参数
* @param param2 第二个参数
* @return BOOL值
*/
- (BOOL)thisIsADemoFuncWithParam1: (NSString *)param1
param2: (NSInteger)param2{
return NO;
}

1、对于@property申明的SDK公开属性,务必写成/* 这是SDK公开属性注释 */,方便调用时Xcode提示;

2、对于SDK内部使用的属性,最好写成/// 这是属性注释而不是/**/;

3、另外,务必让对于>=2个参数的方法,各个参数折行对齐,务必保持.h和.m方法参数格式一致;

4、对于方法名的统一性说明:

  4.1 如果方法是功能性的,处理某些事件、计算、数据处理等的私有方法,则可定义方法名为handleXXX:,如-handleRedBtnClick:、-handleResponsedData:、-handlePlayerEvent:等;

  4.2 对于一些需要暴露的公有方法,则命名最好按照n的功能命名,如对于一个TALPlayer它可以play、stop、resume等;

  4.3 对于可以switch两种状态切换的状态方法,最好命名为toggleXXX:(BOOL)on,如- (void)toggleMute:(BOOL)on;

5、对于一些状态描述性的属性,可以用needs、is、should、has+adj/vb组合形式,如needsAutoDisplay、shouldAutoRotateOrientation、isFinished、hasData或hasNoData等;

对于一些NS_ENUM枚举定义,务必遵循统一前缀方式:

typedef enum : NSUInteger {
TALPlayerEventA = 0,
TALPlayerEventB,
TALPlayerEventC,
} TALPlayerEvent;
// 或者
typedef NS_ENUM(NSUInteger, MyEnum) {
MyEnumValueA,
MyEnumValueB,
MyEnumValueC,
};

6、对于一些全局宏的定义,务必SDK前缀全大写_NAME_组合如TALPLAYER_GLOBAL_MACRO_NAME,对于const类型的常量,务必加上k前缀,命名为kConstValue;

7、对于一些typedef的Block,命名最好指明Block的类别+用途,如TALPlayerLogReportHandler,如果有功能性区分的话,则可以定义为TALPlayerLogReportCompletionHandler、TALPlayerLogReportSuccessHandler、TALPlayerLogReportFailureHandler注意是名词组合形式;

8、调用Block时,一定要对block对象进行nil值判断,防止崩溃handler ? handler() : nil;

9、所有对于NSString校验的地方,都应该校验其length > 0,而不是!str;

10、所有对于NSURL校验的地方,都应该校验其[URL.scheme.lowercaseString isEqualToString: @"https"]方式,而不是!URL;

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

收起阅读 »

iOS Cateogry的深入理解&&initialize方法调用理解(二)

上一篇文章我们讲到了load方法,今天我们来看看initialize新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下将原来的load方法换成initialize先告诉大家initialize方法调...
继续阅读 »
  • 上一篇文章我们讲到了load方法,今天我们来看看initialize

新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下

将原来的load方法换成initialize






先告诉大家initialize方法调用的时间,以便大家带着答案去理解initialize:在类第一次接收到消息的时候调用,它区别于load(运行时加载类的时候调用),下面我们来深入理解initialize

    1. 相信大家在想什么叫第一次接收消息了,我们回到main()





说明:NSLog(@"---")是由于我建的是命令行工程,不写这个,貌似不能显示控制台,Xcode版本是12,当然你们的要显示控制台,直接去掉这行代码

从输出结果可以看到没有任何关于initialize的打印,程序直接退出

  • 2.initialize的打印

int main(int argc, const char * argv[]) {
@autoreleasepool {

[TCPerson alloc];
}
return 0;
}
2020-12-04 14:59:17.417072+0800 TCCateogry[1616:79391] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

从上面的输出结果我们可以看到,TCPerson (TCtest2) +initialize打印

load是直接函数指针直接调用,类,分类,继承等等

[TCPerson alloc]就是相当于该类发送消息,但是它只会调用类,分类的其中一个(取决于编译顺序,从输出结果可以看出,initialize走的是objc_msgSend,而load直接通过函数指针直接调用,所以initialize通过isa方法查找调用


多次向TCPerson发送消息的输出结果

int main(int argc, const char * argv[]) {
@autoreleasepool {

[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
}
return 0;
}
2020-12-04 15:11:12.246442+0800 TCCateogry[1659:85317] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

initialize只会调用一次

我们再来看看继承关系中,initialize的调用

int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCStudent alloc];

}
return 0;
}

输出结果:

2020-12-04 15:14:58.705423+0800 TCCateogry[1705:87507] TCPerson (TCtest2) +initialize
2020-12-04 15:14:58.705750+0800 TCCateogry[1705:87507] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0


从输出结果来看,子类调用initialize之前,会先调用父类的initialize,再调用自己的initialize,当然无论父类调用initialize,还是子类调用initialize,如果有多个分类(这里指的是父类调用父类的分类,子类调用子类的分类),调用initialize取决于分类的编译顺序(调用后编译分类中的initialize,类似于压栈,先进后出),值得注意的是,无论父类子类的initialize,都只调用一次

int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCPerson alloc];
[TCStudent alloc];
[TCStudent alloc];
}
return 0;
}
020-12-04 15:23:27.168243+0800 TCCateogry[1731:91248] TCPerson (TCtest2) +initialize
2020-12-04 15:23:27.168601+0800 TCCateogry[1731:91248] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0

如果子类(子类的分类也不实现)不实现initialize,则父类的initialize就调用多次

#import "TCStudent.h"

@implementation TCStudent
//+ (void)initialize{
// NSLog(@"TCStudent +initialize");
//}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCStudent alloc];
}
return 0;
}
2020-12-04 15:37:09.055459+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
2020-12-04 15:37:09.055775+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
Program ended with exit code: 0
如果子类(子类的分类实现initialize)不实现initialize,则子类的initialize不会调用,调用子类分类的initialize(当然多个分类的话,调用哪个的initialize取决于编译顺序)

#import "TCStudent.h"

@implementation TCStudent
+ (void)initialize{
NSLog(@"TCStudent +initialize");
}
@end
#import "TCStudent+TCStudentTest1.h"

@implementation TCStudent (TCStudentTest1)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest1) +initialize");
}
@end#import "TCStudent+TCStudentTest2.h"

@implementation TCStudent (TCStudentTest2)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest2) +initialize");
}
@end
2020-12-04 15:41:21.863260+0800 TCCateogry[1868:100750] TCPerson (TCtest2) +initialize
2020-12-04 15:41:21.863568+0800 TCCateogry[1868:100750] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0




作者:枫紫_6174
链接:https://www.jianshu.com/p/f0150edc0f42


收起阅读 »

iOS Cateogry的深入理解&&load方法调用&&分类重写方法的调用顺序(一)

首先先看几个面试问题Cateogry里面有load方法么? load方法什么时候调用?load方法有继承么?1. 新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类2.新建一个TCStudent类继承自TCPerson,并且给T...
继续阅读 »

首先先看几个面试问题

  • Cateogry里面有load方法么? load方法什么时候调用?load方法有继承么?

1. 新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类


2.新建一个TCStudent类继承自TCPerson,并且给TCStudent也添加两个分类


Cateogry里面有load方法么?

  • 答:分类里面肯定有load

#import "TCPerson.h"

@implementation TCPerson
+ (void)load{

}
@end
#import "TCPerson+TCtest1.h"

@implementation TCPerson (TCtest1)
+ (void)load{

}
@end
#import "TCPerson+TCTest2.h"

@implementation TCPerson (TCTest2)
+ (void)load{

}
@end

load方法什么时候调用?

load方法在runtime加载类和分类的时候调用load

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {

}
return 0;
}

@implementation TCPerson
+ (void)load{
NSLog(@"TCPerson +load");
}
@end


@implementation TCPerson (TCtest1)
+ (void)load{
NSLog(@"TCPerson (TCtest1) +load");
}
@end
@implementation TCPerson (TCTest2)
+ (void)load{
NSLog(@"TCPerson (TCtest2) +load");
}
@end
可以看到我们在main里面不导入任何的头文件,也不引用任何的类,直接运行,控制台输出

从输出结果我们可以看出,三个load方法都被调用

问题:分类重写方法,真的是覆盖原有类的方法么?如果不是,到底分类的方法是怎么调用的?

  • 首先我们在TCPerson申明一个方法+ (void)test并且在它的两个分类都重写+ (void)test


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface TCPerson : NSObject
+ (void)test;
@end

NS_ASSUME_NONNULL_END

#import "TCPerson.h"

@implementation TCPerson
+ (void)load{
NSLog(@"TCPerson +load");
}
+ (void)test{
NSLog(@"TCPerson +test");
}
@end
分类重写test
#import "TCPerson+TCtest1.h"

@implementation TCPerson (TCtest1)
+ (void)load{
NSLog(@"TCPerson (TCtest1) +load");
}
+ (void)test{
NSLog(@"TCPerson (TCtest1) +test1");
}
@end
#import "TCPerson+TCTest2.h"

@implementation TCPerson (TCTest2)
+ (void)load{
NSLog(@"TCPerson (TCtest2) +load");
}
+ (void)test{
NSLog(@"TCPerson (TCtest2) +test2");
}
@end

在main里面我们调用test

#import <Foundation/Foundation.h>
#import "TCPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson test];
}
return 0;
}

从输出结果中我们可以看到,只有分类2中的test被调用,为什么只调用分类2中的test了?





因为编译顺序是分类2在后,1在前,这个时候我们改变编译顺序(拖动文件就行了)




细心的老铁会看到,为什么load方法一直都在调用,这是为什么了?它和test方法到底有什么不同了?真的是我们理解中的load不覆盖,test覆盖了,所以才出现这种情况么?

我们打印TCPerson的类方法

void printMethodNamesOfClass(Class cls)
{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);

// 存储方法名
NSMutableString *methodNames = [NSMutableString string];

// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[I];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}

// 释放
free(methodList);

// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson test];
printMethodNamesOfClass(object_getClass([TCPerson class]));
}
return 0;
}


可以看到,TCPerson的所有类方法名,并不是覆盖,三个load,三个test,方法都在

load源码分析:查看objc底层源码我们可以看到:

void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;

loadMethodLock.assertLocked();

// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;

void *pool = objc_autoreleasePoolPush();

do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}

// 2. Call category +loads ONCE
more_categories = call_category_loads();

// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);

objc_autoreleasePoolPop(pool);

loading = NO;
}
load方法它是先调用 while (loadable_classes_used > 0) {call_class_loads(); }类的load,再调用more_categories = call_category_loads()分类的load,和编译顺序无关,都会调用
我们查看call_class_loads()方法

static void call_class_loads(void)
{
int I;

// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;

// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;

if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}

// Destroy the detached list.
if (classes) free(classes);
}
其通过的是load_method_t函数指针直接调用
函数指针直接调用
typedef void(*load_method_t)(id, SEL);

其分类load方法调用也是一样

static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;

// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;

// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;

cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, SEL_load);
cats[i].cat = nil;
}
}

为什么test不一样了

因为test是因为消息机制调用的,objc_msgSend([TCPerson class], @selector(test));消息机制就牵扯到了isa方法查找,test在元类方法里面顺序查找的

load只在加载类的时候调用一次,且先调用类的load,再调用分类的

load的继承关系调用
首先我们先看TCStudent
#import "TCStudent.h"

@implementation TCStudent

@end

不写load方法调用

TCStudent写上load


从中可以看出子类不写load的方法,调用父类的load,当子类调用load时,先调用父类的load,再调用子类的load,父类子类load取决于你写load方法没有,如果都写了,先调用父类的,再调用子类的

总结:先调用类的load,如果有子类,则先看子类是否写了load,如果写了,则先调用父类的load,再调用子类的load,当类子类调用完了,再是分类,分类的load取决于编译顺序,先编译,则先调用,test的方法调用走的是消息发送机制,其底层原理和load方法有着本质的区别,消息发送主要取决于isa的方法查找顺序



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









收起阅读 »

在iOS中运用React Component的思路,效率更高的开发UI,更好的复用UI组件

最近一直在看React的一些东西,其实很早前就想开始重拾前端,但是一直提不起兴趣再去看JavaScript,对CSS这种布局方式也不是很来感,说白了,就是懒吧😂。去年年底开始在公司app里开始尝试接入Weex,所以不得不把JavaScript再重新撸了一遍,顺...
继续阅读 »

最近一直在看React的一些东西,其实很早前就想开始重拾前端,但是一直提不起兴趣再去看JavaScript,对CSS这种布局方式也不是很来感,说白了,就是懒吧😂。去年年底开始在公司app里开始尝试接入Weex,所以不得不把JavaScript再重新撸了一遍,顺带着把ES6的一些新特性也了解了一下,更好的函数调用方式,Class的引入,Promise的运用等等,其实最吸引我的还是在用了Weex之后,感受到了Component带来的UI复用,高效开发的快感。Weex是运用Vue.js来调用,渲染native控件,来达到one code, run everywhere。不管是Vue.js,还是React,最终都是朝着W3C WebComponent的标准走了(今年会发布的Vue 3.0在组件上的语法基本上跟React一样了)。这篇就来讲讲我对React Component的理解,还有怎么把这个标准也能在native上面做运用

demo源码

iOS UI开发的痛点

对iOS开发来说,最常用的UI组件就是UICollectionView了,就是所谓的一个列表页,现在的app大部分页面都是由一个列表来呈现内容的。对iOS开发者来说,我们可以封装每个UICollectionViewCell,从而可以在每个页面的UICollectionView中能够复用,但是痛点是,这个复用仅仅是UI上的复用,在每写一个新的页面(UIViewController)的时候,还是需要新建一个UICollectionView,然后再把UICollectionView的DataSource和Delegate方法再实现一遍,把这些Cell再在这些方法里重新生成一遍,才能让列表展现出来。比方说我们首页列表底部有猜你喜欢的cell,个人中心页面底部也有猜你喜欢的cell,这两个页面,都需要在自己拥有的UICollectionView中注册这个猜你喜欢的cell,返回这个猜你喜欢cell的高度,设置这个cell的model并刷新数据,如果有Header或者Footer的话,还得重新设置这些Header跟Footer。所以新写一个列表页面,对iOS开发者来说,还是很麻烦。

使用Weex或者RN开发原生列表页

使用Weex开发列表页的时候,我们组内的小伙伴都觉得很爽,很高效,基本上几行代码就能绘制出一个列表页,举个RN和weex的例子

// React
render() {
const cells = this.state.items.map((item, index) => {
if (item.cellType === 'largeCell') {
return <LargeCell cellData={item.entity}></LargeCell>
} else if (item.cellType === 'mediumCell') {
return <MediumCell cellData={item.entity}></MediumCell>
} else if (item.cellType === 'smallCell') {
return <SmallCell cellData={item.entity}></SmallCell>
}
});

return(
<Waterfall>
{ cells }
</Waterfall>
);
}

// Vue
<template>
<waterfall>
<cell v-for="(item, index) in itemsArray" :key="index">
<div class="cell" v-if="item.cellType === 'largeCell'">
<LargeCell :cellData="item.entity"></LargeCell>
</div>
<div class="cell" v-if="item.cellType === 'mediumCell'">
<MediumCell :cellData="item.entity"></MediumCell>
</div>
<div class="cell" v-if="item.cellType === 'smallCell'">
<SmallCell :cellData="item.entity"></SmallCell>
</div>
</cell>
</waterfall>
</template>

const

waterfall对应的就是iOS中的UICollectionView,waterfall这个组件中有cell的子组件,这些cell的子组件可以是我们自己定义的不同类型样式的cell组件。LargeCell,MediumCell,SmallCell对应的就是原生中的我们自定义的UICollectionViewCell。这些Cell子组在任何waterfall组件下面都可以使用,在一个waterfall组件下面,我们只需要把我们把在这个列表中需要展示的cell放进来,通过props把数据传到cell组件中即可。这种方式对iOS开发者来说,真的是太舒服了。在觉得开发很爽的同时,我也在思考,既然这种Component的方式用起来很爽,那么能不能也运用到原生开发中呢?毕竟我们大部分的业务需求还是基于原生来开发的。

React的核心思想

1、先来解释下React中的React Element和React Component
1.1、React Elements

const element = <div id='login-button>Login</div>

这段JSX表达式返回的就是一个React Element,React element描述了用户将在屏幕上看到的那个UI,跟DOM elements不一样的是,React elements是一个单纯的对象,仅仅是对将要呈现到屏幕上的UI的一个描述,并不是真正渲染好的UI,创建一个React element开销是极其小的,渲染的事情是由背后的React DOM来处理的。上面的那段代码相当于:

const element = React.createElement(
'div',
{id: 'login-button'},
'Login'
)

返回的React element对象相当于 =>

{
type: 'div',
props: {
children: 'Login',
id: 'login-button'
}
}

1.2 React Components

React中最核心的一个思想就是Component了,官方的解释是Component允许我们将UI拆分为独立可复用的代码片段,组件中可以包含多个其他组件,这样将组件一个个单独抽离出来,并最终再组合到一起,大大提高了代码的可读性(Readability)、可维护性(Maintainability)、可复用性(Reusability)和可测试性(Testability)。这也是 React 里用 Component 抽象所有 UI 的意义所在。

class Button extends React.Component {
render() {
const element = <div id='login-button>{ this.props.title }</div>
return (
<div>
{ element }
</div>
)
}

这段代码中Button就是一个React Component,这个component接受一个叫props的参数,返回描述UI的React element。

2、可以看出React Component接受props是一个对象,也就是所谓的一种数据结构,返回React Element也是一种对象,所谓的另外一种数据结构,所以我认为的React Component其实就是一个function,这个function的主要功能就是将一种数据结构(描述原始数据)转换成另外一种数据结构(描述UI)。React element仅仅是一个描述UI的对象,可以认为是一个中间状态,我们可以用最小的开销来创建或者销毁element对象。

3、React的核心思想总结下来就是这样的一个流程
1、原始数据到UI数据的转化 props -> React Component -> React Element
2、React Element的作用是将Component的创建跟描述状态分离,Component内部主要负责这个Component的构建,React Element主要用来做描述这个Component的状态
3、多个Component返回的多个Elements,这个流程是进行UI组合
4、React Element并不是一个渲染结果,React DOM的作用是将UI的状态(即Element)和UI的渲染分离,React DOM负责element的渲染
5、最后一个流程就是UI渲染了
上述这几个流程基本上代表了React的核心概念

怎么在iOS中运用React Component概念

说了这么多,其实iOS中缺少的就是这个Component概念,iOS原生的流程是原始数据到UI布局,再到UI绘制。复用的只是UI绘制结果的那个view(e.g. UICollectionViewCell)

在使用UICollectionView的时候,我们的数据都是通过DataSource方法返回给UICollectionView,UICollectionView拿到这些数据之后,就直接去绘制UICollectionViewCell了。所以每个列表页都得重新建一个UICollectionView,再引入自定义的UICollectionViewCell来绘制列表,所有的DataSource跟Delegate方法都得走一遍。所以我在想,我们可以按照React的那种方式来绘制列表么?将一个个UI控件抽象成一个个组件,再将这些组件组合到一起,绘制出最后的页面,React或者Weex的绘制列表其实就是waterfall这个列表component里面按照列表顺序插入自定义的cell component(组合)。那么我们其实可以在iOS中也可以有这个waterfall的component,这个component支持一个insertChildComponent:的方法,这个方法里就是插入自定义的CellComponent到waterfall这个组件中,并通过传入props来创建这个component。所以我就先定义了一个组件的基类BaseComponent

@protocol ComponentProtocol <NSObject>

/**
* 绘制组件
*
* @param view 展示该组件的view
*/
- (void)drawComponentInView:(UIView *)view withProps:(id)props;

/**
* 组件的尺寸
*
* @param props 该component的数据model
* @return 该组件的size
*/
+ (CGSize)componentSize:(id)props;

@end

@interface BaseComponent : NSObject <ComponentProtocol>

- (instancetype)initWithProps:(id)props;

@property (nonatomic, strong, readonly) id props;

所有的Component的创建都是通过传入props参数,来返回一个组件实例,每个Component还遵守一个ComponentProtocol的协议,协议里两个方法:

1、- (void)drawComponentInView:(UIView *)view withProps:(id)props; 每个component通过这个方法来进行native控件的绘制,参数中view是将会展示该组件的view,比方说WaterfallComponent中的该方法view为UIViewController的view,因为UIViewController的view会用来展示WaterfallComponent这个组件,'props'是该组件创建时传入的参数,这个参数用来告诉组件应该怎样绘制UI
2、+ (CGSize)componentSize:(id)props; 来描述组件的尺寸。

有了这个Component概念之后,我们原生的绘制流程就变成

1、创建Component,传入参数props
2、Component内部执行创建代码,保存props
3、当页面需要绘制的时候(React中的render命令),component内部会执行- (void)drawComponentInView:(UIView *)view withProps:(id)props;方法来描述并绘制UI

原生代码中想实现React element,其实不是一件简单的事情,因为原生没有类似JSX这种语言来生成一套只用来描述UI,并不绘制UI的中间状态的对象(可以做,比方说自己定义一套语法来描述UI),所以目前我的做法是在component内部,等到绘制命令来了之后,通过在- (void)drawComponentInView:(UIView *)view withProps:(id)props方法中,调用原生自定义的UIKit控件,通过props来绘制该UIKit

所以将通过封装component的方式,我们之前UIKit代表的UI组件转换成组件,把这些组件一个个单独抽离出来,再通过搭积木的方式,将各种组件一个个组合到一起,怎么绘制交给component内部去描述,而不是交给每个页面对应的UIViewController

Demo

Demo中,我会创建一个WaterfallComponent组件,还有多个CellComponent来绘制列表页,每个不一样列表页面(UIViewController)都可以创建一个WaterfallComponent组件,然后将不一样的CellComponent按照顺序插入到WaterfallComponent组件中,即可完成绘制列表,不需要每个页面再去处理UICollectionView的DataSource,Delegate方法。


WaterfallComponent内部会有一个UICollectionView,WaterfallComponent的insertChildComponent方法中,会创建一个dataController来管理数据源,并用来跟UICollectionView的DataSource方法进行交互从而绘制出列表页,最终UIViewController中绘制列表的方法如下:

self.waterfallComponent = [[WaterfallComponent alloc] initWithProps:nil];

for (NSDictionary *props in datas) {
if ([props[@"type"] isEqualToString:@"1"]) {
FirstCellComponent *cellComponent = [[FirstCellComponent alloc] initWithProps:props];
[self.waterfallComponent insertChildComponent:cellComponent];
} else if ([props[@"type"] isEqualToString:@"2"]) {
SecondCellComponent *cellComponent = [[SecondCellComponent alloc] initWithProps:props];
[self.waterfallComponent insertChildComponent:cellComponent];
}
}
[self.waterfallComponent drawComponentInView:self.view withProps:nil];

这样,每个我们自定义的Cell就可以以CellComponent的形式,被按照随意顺序插入到WaterfallComponent,从而做到了真正意义上的复用,Demo已上传到GitHub上,有兴趣的可以看

总结

React的核心思想是将组件一个个单独抽离出来,并最终再组合到一起,大大提高了代码的可读性、可维护性、可复用性和可测试性。这也是 React 里用 Component 抽象所有 UI 的意义所在。
原生开发中,使用Component的概念,用Component去抽象UIKit控件,也能达到同样的效果,这样也能统一每个开发使用UICollectionView时候的规范,也能统一对所有列表页的数据源做一些统一处理,比方说根据一个逻辑,统一在所有列表页,插入一个广告cell,这个逻辑完全可以在WaterfallComponent里统一处理。

思考

目前我们只用到了Component这个概念,其实React中,React Element的概念也是非常核心的,React Element隔离了UI描述跟UI绘制的逻辑,通过JSX来描述UI,并不去生成,绘制UI,这样我们能够以最小的代价来生成或者销毁React Elements,然后在交付给系统绘制elements里描述的UI,那么如果原生里也有这一套模板语言,那么我们就能真正做到在Component里,传入props,返回一个element描述UI,然后再交给系统去绘制,这样还能省去cell的创建,只创建CellComponent即可。其实我们可以通过定义一套语义去描述UI布局,然后通过解析这套语义,通过Core Text去做绘制,这一套还是值得我再去思考的。

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

收起阅读 »

Swift 5.0 值得关注的特性:增加 Result<T, E: Error> 枚举类型

HackingSwift: What’s new in Swift 5.0Result<T> 还是 Result<T, E: Error>背景在异步获取数据的场景中,常见的回调的数据结构是这样的:表示获取成功的数据,表示获取失败的 er...
继续阅读 »

HackingSwift: What’s new in Swift 5.0
Result<T> 还是 Result<T, E: Error>

背景

在异步获取数据的场景中,常见的回调的数据结构是这样的:表示获取成功的数据,表示获取失败的 error。因为数据可能获取成功,也可能失败。因此回调中的数据和错误都是 optional 类型。
比如 CloudKit 中保存数据的一个函数就是这样:

func save(_ record: CKRecord, completionHandler: @escaping (CKRecord?, Error?) -> Void)

这种形式的缺点是没有体现出两种结果的互斥关系:如果数据成功获取到了,那么 error 一定为空。如果 error 有值,数据一定是获取失败了。

Swift 中枚举的能力相比 OC 有着很大的进步,每个枚举值除了可以是常规的基础类型,还可以是一个关联的类型。有了这样的特性后用枚举来优化返回结果的数据结构显得水到渠成:

enum Result<Success, Failure> where Failure : Error {

/// A success, storing a `Success` value.
case success(Success)

/// A failure, storing a `Failure` value.
case failure(Failure)
}

基本用法

定义异步返回结果是 Int 类型的函数:

func fetchData(_ completionHandler: @escaping (Result<Int, Error>) -> Void) {
DispatchQueue.global().async {
let isSuccess = true
if isSuccess {
let resultValue = 6
return completionHandler(.success(resultValue))
} else {
let error = NSError(domain: "custom error", code: -1, userInfo: nil)
return completionHandler(.failure(error))
}
}
}

返回值的类型通过泛型进行约束,Result 第一个泛型类型表示返回值的类型,第二个类型表示错误的类型。对 Result 赋值和常规的枚举一样:

let valueResult: Result<Int, CustomError> = Result.success(4)

// 因为 swift 中会进行类型推断,编译器在确认返回的是 `Result` 类型后,可以省略枚举类型的声明
let errorResult = .failure(CustomError.inputNotValid)

取出 Result 值和获取普通的关联类型枚举是一样的:

fetchData { (result) in
switch result {
case .success(let value):
print(value)
case .failure(let error)
print(error.localizedDescription)
}
}

如果你只想要获取其中一项的值,也可以直接用 if case 拆包:

fetchDate { (result) in
if case .success(let value) = result {
print(value)
}
}

可以判等

Enum 是一个值类型,是一个值就应该可以判断是否相等。如果 Result 的成功和失败的类型都是 Equatable,那么 Result就可以判等,源码如下:

extension Result : Equatable where Success : Equatable, Failure : Equatable { }

类似的,如果是成功和失败的类型都是 Hashable,那么 Result 也是 Hashable:

extension Result : Hashable where Success : Hashable, Failure : Hashable { }

如果实现了 Hashable ,可以用来当做字典的 key。

辅助的 API

map、mapError
与 Dictionary 类似,Swift 为 Result 提供了几个 map value 和 error 的方法。

let intResult: Result<Int, Error> = Result.success(4)
let stringResult = x.map { (value) -> Result<String, Error> in
return .success("map")
}

let originError = NSError(domain: "origin error", code: -1, userInfo: nil)
let errorResult: Result<Int, Error> = .failure(originError)
let newErrorResult = errorResult.mapError { (error) -> Error in
let newError = NSError(domain: "new error", code: -2, userInfo: nil)
return newError
}

flatMap、flatMapError
map 返回的是具体的结果和错误, flatMap 闭包中返回的是 Result 类型。如果 Result 中包含的是数据,效果和 map 一致,替换数据;如果 Result 中包含的是错误,那么不替换结果。

let intResult: Result<Int, Error> = Result.success(4)

// 替换成功
let flatMapResult = intResult.flatMap { (value) -> Result<String, Error> in
return .success("flatMap")
}

// 没有执行替换操作,flatMapIntResult 值还是 intResult
let flatMapIntResult = intResult.flatMap { (value) -> Result<String, Error> in
return .failure(NSError(domain: "origin error", code: -1, userInfo: nil))
}

get
很多时候只关心 Result 的值,Swift 提供了 get() 函数来便捷的直接获取值,需要注意的是这个函数被标记为 throws,使用时语句前需要加上 try:

let intResult: Result<Int, Error> = Result.success(4)

let value = try? intResult.get()

可抛出异常的闭包初始化器

很多时候获取返回值的闭包中可能会发生异常代表获取失败的错误,基于这个场景 Swift 提供了一个可抛出异常的闭包初始化器:

enum CustomError: Error, Equatable {
case inputNotValid
}

let fetchInt = { () -> Int in
if true {
return 4
} else {
throw CustomError.inputNotValid
}
}

let result: Result<Int, Error> = Result { try fetchInt() }

需要提醒是通过这种方式声明的 Result 的 error 类型只能是 Error,不能指定特定的 Error。

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

收起阅读 »

运行时Hook所有Block方法调用的技术实现

1.方法调用的几种Hook机制iOS系统中一共有:C函数、Block、OC类方法三种形式的方法调用。Hook一个方法调用的目的一般是为了监控拦截或者统计一些系统的行为。Hook的机制有很多种,通常良好的Hook方法都是以AOP的形式来实现的。当我们想Hook一...
继续阅读 »

1.方法调用的几种Hook机制

iOS系统中一共有:C函数、Block、OC类方法三种形式的方法调用。Hook一个方法调用的目的一般是为了监控拦截或者统计一些系统的行为。Hook的机制有很多种,通常良好的Hook方法都是以AOP的形式来实现的。

当我们想Hook一个OC类的某些具体的方法时可以通过Method Swizzling技术来实现、当我们想Hook动态库中导出的某个C函数时可以通过修改导入函数地址表中的信息来实现(可以使用开源库fishhook来完成)、当我们想Hook所有OC类的方法时则可以通过替换objc_msgSend系列函数来实现。。。

那么对于Block方法呢而言呢?

2.Block的内部实现原理和实现机制简介

这里假定你对Block内部实现原理和运行机制有所了解,如果不了解则请参考文章《深入解构iOS的block闭包实现原理》或者自行通过搜索引擎搜索。

源程序中定义的每个Block在编译时都会转化为一个和OC类对象布局相似的对象,每个Block也存在着isa这个数据成员,根据isa指向的不同,Block分为__NSStackBlock、__NSMallocBlock、__NSGlobalBlock 三种类型。也就是说从某种程度上Block对象也是一种OC对象。下面的类图描述了Block类的层次结构。


Block类以及其派生类在CoreFoundation.framework中被定义和实现,并且没有对外公开。

每个Block对象在内存中的布局,也就是Block对象的存储结构被定义如下(代码出自苹果开源出来的库实现libclosure中的文件Block_private.h):

//需要注意的是下面两个只是模板,具体的每个Block定义时总是按这个模板来定义的。

//Block描述,每个Block一个描述并定义在全局数据段
struct Block_descriptor_1 {
uintptr_t reserved; //记住这个变量和结构体,它很重要!!
uintptr_t size;
};

//Block对象的内存布局
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
uintptr_t invoke; //Block对象的实现函数
struct Block_descriptor_1 *descriptor;
// imported variables,这里是每个block对象的特定数据成员区域
};

这里要关注一下struct Block_descriptor_1中的reserved这个数据成员,虽然系统没有用到它,但是下面就会用到它而且很重要!

在了解了Block对象的类型以及Block对象的内存布局后,再来考察一下一个Block从定义到调用是如何实现的。就以下面的源代码为例:

int main(int argc, char *argv[])
{
//定义
int a = 10;
void (^testblock)(void) = ^(){
NSLog(@"Hello world!%d", a);
};

//执行
testblock();

return 0;
}

在将OC代码翻译为C语言代码后每个Block的定义和调用将变成如下的伪代码:

//testblock的描述信息
struct Block_descriptor_1_fortestblock {
uintptr_t reserved;
uintptr_t size;
};

//testblock的布局存储结构体
struct Block_layout_fortestblock {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
uintptr_t invoke; //Block对象的实现函数
struct Block_descriptor_1_fortestblock *descriptor;
int m_a; //外部的传递进来的数据。
};

//testblock函数的实现。
void main_invoke_fortestblock(struct Block_layout_fortestblock *cself)
{
NSLog(@"Hello world!%d", cself->m_a);
}

//testblock对象描述的实例,存储在全局内存区
struct Block_descriptor_1_fortestblock _testblockdesc = {0, sizeof(struct Block_layout_fortestblock)};

int main(int argc, char *argv[])
{
//定义部分
int a = 10;
struct Block_layout_fortestblock testblock = {
.isa = __NSConcreteStackBlock,
.flags =0,
.reserved = 0,
.invoke = main_invoke_fortestblock,
.descriptor = & _testblockdesc,
.m_a = a
};

//调用部分
testblock.invoke();

return 0;
}

可以看出Block对象的生成和调用都是在编译期间就已经固定在代码中了,它不像其他OC对象调用方法时需要通过runtime来执行间接调用。并且线上程序中所有关于Block的符号信息都会被strip掉。所以上述的所介绍的几种Hook方法都无法Hook住一个Block对象的函数调用。

如果想要Hook住系统的所有Block调用,需要解决如下几个问题:
a. 如何在运行时将所有的Block的invoke函数替换为一个统一的Hook函数。
b. 这个统一的Hook函数如何调用原始Block的invoke函数。
c. 如何构建这个统一的Hook函数。

3.实现Block对象Hook的方法和原理

一个OC类对象的实例通过引用计数来管理对象的生命周期。在MRC时代当对象进行赋值和拷贝时需要通过调用retain方法来实现引用计数的增加,而在ARC时代对象进行赋值和拷贝时就不再需要显示调用retain方法了,而是系统内部在编译时会自动插入相应的代码来实现引用计数的添加和减少。不管如何只要是对OC对象执行赋值拷贝操作,最终内部都会调用retain方法。

Block对象也是一种OC对象!!

每当一个Block对象在需要进行赋值或者拷贝操作时,也会激发对retain方法的调用。因为Block对象赋值操作一般是发生在Block方法执行之前,因此我们可以通过Method Swizzling的机制来Hook 类的retain方法,然后在重写的retain方法内部将Block对象的invoke数据成员替换为一个统一的Hook函数!

通过考察__NSStackBlock、__NSMallocBlock、__NSGlobalBlock 三个类的实现发现这三个类都重载了NSObject的retain方法,这样在执行Method Swizzling时就不需要对NSObject的retain方法执行替换,而只要对上述三个类的retain执行替换即可。

你可以说出为什么这三个派生类都会对retain方法进行重载吗?答案可以从这三种Block的类型定义以及所表示的意义中去寻找。

Block技术不仅可以用在OC语言中,LLVM对C语言进行的扩展也能使用Block,比如gcd库中大量的使用了Block。在C语言中如果对一个Block进行赋值或者拷贝系统需要通过C库函数:

//函数声明在Block.h头文件汇总
// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

来实现,这个函数定义在libsystem_blocks.dylib库中,并且库实现已经开源:libclosure。因此可以借助fishhook库来对__Block_copy这个函数进行替换处理,然后在替换的函数函数中将一个Block的原始的invoke函数替换为统一的Hook函数。

另外一个C语言函数objc_retainBlock,也是实现了对Block进行赋值时的引用计数增加,这个函数内部就是简单的调用__Block_copy方法。因此我们也可以添加对objc_retainBlock的替换处理。

解决了第一个问题后,接下来再解决第二个问题。还记得上面提到过的struct Block_descriptor_1中的reserved这个数据成员吗? 当我们通过上述的方法对所有Block对象的invoke成员替换为一个统一的Hook函数前,可以将Block对象的原始invoke函数保存到这个保留字段中去。然后就可以在统一的Hook函数内部读取这个保留字段中的保存的原始invoke函数来执行真实的方法调用了。

因为一个Block对象函数的第一个参数其实是一个隐藏的参数,这个隐藏的参数就是Block对象本身,因此很容易就可以从隐藏的参数中来获取到对应的保留字段。

下面的代码将展示通过方法交换来实现Hook处理的伪代码

struct Block_descriptor {
void *reserved;
uintptr_t size;
};

struct Block_layout {
void *isa;
int32_t flags; // contains ref count
int32_t reserved;
void *invoke;
struct Block_descriptor *descriptor;
};

//统一的Hook函数,这里以伪代码的形式提供
void blockhook(void *obj, ...)
{
struct Block_layout *layout = (struct Block_layout*) obj;
//调用原始的invoke函数
layout->descriptor->reserved(...);
}
//模拟器下如果返回类型是结构体并且大于16字节那么第一个参数是返回值保存的内存地址,block对象变为第二个参数
void blockhook_stret(void *pret, void *obj, ...)
{
struct Block_layout *layout = (struct Block_layout*) obj;
//调用原始的invoke函数
layout->descriptor->reserved(...);
}

//执行Block对象的方法替换处理
void replaceBlockInvokeFunction(const void *blockObj)
{
struct Block_layout *layout = (struct Block_layout*)blockObj;
if (layout != NULL && layout->descriptor != NULL){
int32_t BLOCK_USE_STRET = (1 << 29); //如果模拟器下返回的类型是一个大于16字节的结构体,那么block的第一个参数为返回的指针,而不是block对象。
void *hookfunc = ((layout->flags & BLOCK_USE_STRET) == BLOCK_USE_STRET) ? blockhook_stret : blockhook;
if (layout->invoke != hookfunc){
layout->descriptor->reserved = layout->invoke;
layout->invoke = hookfunc;
}
}
}

void *(*__NSStackBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSStackBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSStackBlock_retain_old(obj, cmd);
}

void *(*__NSMallocBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSMallocBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSMallocBlock_retain_old(obj, cmd);
}

void *(*__NSGlobalBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSGlobalBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSGlobalBlock_retain_old(obj, cmd);
}

int main(int argc, char *argv[])
{
//因为类名和方法名都不能直接使用,所以这里都以字符串的形式来转换获取。
__NSStackBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSStackBlock"), sel_registerName("retain"), (IMP)__NSStackBlock_retain_new, nil);
__NSMallocBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSMallocBlock"), sel_registerName("retain"), (IMP)__NSMallocBlock_retain_new, nil);
__NSGlobalBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSGlobalBlock"), sel_registerName("retain"), (IMP)__NSGlobalBlock_retain_new, nil);

return 0;
}

解决了第二个问题后,就需要解决第三个问题。上面的统一Hook函数blockhook和block_stret只是伪代码实现,因为任何一个Block中的函数的参数类型和个数是不一样的,而且统一Hook函数也需要在适当的时候调用原始的默认Block函数实现,并且不能破坏参数信息。为了解决这些问题就使得这个统一的Hook函数不能用高级语言来实现,而只能用汇编语言来实现。下面就是在arm64位体系下的实现代码:

.text
.align 5
.private_extern _blockhook
_blockhook:
//为了不破坏原有参数,这里将所有参数压入栈中
stp q6, q7, [sp, #-0x20]!
stp q4, q5, [sp, #-0x20]!
stp q2, q3, [sp, #-0x20]!
stp q0, q1, [sp, #-0x20]!
stp x6, x7, [sp, #-0x10]!
stp x4, x5, [sp, #-0x10]!
stp x2, x3, [sp, #-0x10]!
stp x0, x1, [sp, #-0x10]!
stp x8, x30, [sp, #-0x10]!

//这里可以添加任意逻辑来进行hook处理。

//这里将所有参数还原
ldp x8, x30, [sp], #0x10
ldp x0, x1, [sp], #0x10
ldp x2, x3, [sp], #0x10
ldp x4, x5, [sp], #0x10
ldp x6, x7, [sp], #0x10
ldp q0, q1, [sp], #0x20
ldp q2, q3, [sp], #0x20
ldp q4, q5, [sp], #0x20
ldp q6, q7, [sp], #0x20

ldr x16, [x0, #0x18] //将block对象的descriptor数据成员取出
ldr x16, [x16] //获取descriptor中的reserved成员
br x16 //执行reserved中保存的原始函数指针。
LExit_blockhook:

对于x86_64/arm32位系统来说,如果block函数的返回是一个结构体并且长度超过16字节(arm32是8字节)。那么block对象里面的flags属性就会设置为BLOCK_USE_STRET。而x86_64/arm32位系统对于这种返回类型的函数就会将返回值存放到第一个参数所指向的内存中,同时会把原本的block对象变化为第二个参数,因此需要对这种情况进行特殊处理。

关于在运行时Hook所有Block方法调用的技术实现原理就介绍到这里了。当然一个完整的系统可能需要其他一些能力:

1、如果你只想Hook可执行程序中定义的Block,那么请参考我的文章:深入iOS系统底层之映像操作API介绍 中的内容来实现Hook函数的过滤处理。
2、如果你不想借助Block_descriptor中的reserved来保存原始的invoke函数,那么可以参考我的文章:Thunk程序的实现原理以及在iOS中的应用(二)中介绍的技术来实现统一Hook函数以及完成对原始invoke函数的调用技术。

具体完整的代码可以访问我的github中的项目:YSBlockHook。这个项目以AOP的形式实现了真机arm64位模式下对可执行程序中所有定义的Block进行Hook的方法,Hook所做的事情就是在所有Block调用前,打印出这个Block的符号信息。

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

收起阅读 »

性能超高的UI库-AsyncDisplayKit

AsyncDisplayKit 已移动并重命名:Texture性能提升AsyncDisplayKit 的基本单位是node. ASDisplayNode 是对 的抽象UIView,而后者又是对 的抽象CALayer。与只能在主线程上使用的视图不同,节...
继续阅读 »

AsyncDisplayKit 已移动并重命名:Texture

性能提升

AsyncDisplayKit 的基本单位是nodeASDisplayNode 是对 的抽象UIView,而后者又是对 的抽象CALayer与只能在主线程上使用的视图不同,节点是线程安全的:您可以在后台线程上并行实例化和配置它们的整个层次结构。

为了保持其用户界面流畅和响应迅速,您的应用程序应以每秒 60 帧的速度呈现——这是 iOS 的黄金标准。这意味着主线程有六十分之一秒来推动每一帧。执行所有布局和绘图代码需要 16 毫秒!并且由于系统开销,您的代码在导致丢帧之前的运行时间通常不到 10 毫秒。

AsyncDisplayKit 允许您将图像解码、文本大小调整和渲染、布局和其他昂贵的 UI 操作移出主线程,以保持主线程可用于响应用户交互。


随着框架的发展,添加了许多功能,通过消除现代 iOS 应用程序中常见的样板样式结构,可以为开发人员节省大量时间。如果您曾经处理过单元格重用错误,尝试为页面或滚动样式界面高效地预加载数据,或者甚至只是试图防止您的应用丢失太多帧,您都可以从集成 ASDK 中受益。


详细的api介绍:

https://texturegroup.org/appledocs.html


常见问题及demo下载:

https://github.com/facebookarchive/AsyncDisplayKit


源码下载:




收起阅读 »

java设计模式:享元模式

前言在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。 例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些...
继续阅读 »

前言

在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。


例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式的产生背景。


定义

运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。


优点

相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。


缺点

为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
读取享元模式的外部状态会使得运行时间稍微变长。

享元模式的结构与实现

享元模式的定义提出了两个要求,细粒度和共享对象。因为要求细粒度,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。
内部状态指对象共享出来的信息,存储在享元信息内部,并且不回随环境的改变而改变;
外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享。

比如,连接池中的连接对象,保存在连接对象中的用户名、密码、连接URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态。而当每个连接要被回收利用时,我们需要将它标记为可用状态,这些为外部状态。


享元模式的本质是缓存共享对象,降低内存消耗。


结构

抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

享元模式的实现

应用实例的话,其实上面的模板就已经是一个很好的例子了,类似于String常量池,没有的对象创建后存在池中,若池中存在该对象则直接从池中取出。


  为了更好的理解享元模式,这里再举一个实例,比如接了我一个小型的外包项目,是做一个产品展示网站,后来他的朋友们也希望做这样的网站,但要求都有些不同,我们当然不能直接复制粘贴再来一份,有任希望是新闻发布形式的,有人希望是博客形式的等等,而且因为经费原因不能每个网站租用一个空间。


  其实这里他们需要的网站结构相似度很高,而且都不是高访问量网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,这是造成服务器的大量资源浪费。如果整合到一个网站中,共享其相关的代码和数据,那么对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源;而对于代码,由于是一份实例,维护和扩展都更加容易。


  那么此时就可以用到享元模式了。UML图如下:
  在这里插入图片描述
网站抽象类
 

 public abstract class WebSite {

public abstract void use();

}

具体网站类


public class ConcreteWebSite extends WebSite {

private String name = "";

public ConcreteWebSite(String name) {
this.name = name;
}

@Override
public void use() {
System.out.println("网站分类:" + name);
}

}

网络工厂类
  这里使用HashMap来作为池,通过put和get方法实现加入池与从池中取的操作。

public class WebSiteFactory {

private HashMap<String, ConcreteWebSite> pool = new HashMap<>();

//获得网站分类
public WebSite getWebSiteCategory(String key) {
if(!pool.containsKey(key)) {
pool.put(key, new ConcreteWebSite(key));
}

return (WebSite)pool.get(key);
}

//获得网站分类总数
public int getWebSiteCount() {
return pool.size();
}

}

Client客户端
  这里测试用例给了两种网站,原先我们需要做三个产品展示和三个博客的网站,也即需要六个网站类的实例,但其实它们本质上都是一样的代码,可以利用用户ID号的不同,来区分不同的用户,具体数据和模板可以不同,但代码核心和数据库却是共享的。

public class Client {

public static void main(String[] args) {
WebSiteFactory factory = new WebSiteFactory();

WebSite fx = factory.getWebSiteCategory("产品展示");
fx.use();

WebSite fy = factory.getWebSiteCategory("产品展示");
fy.use();

WebSite fz = factory.getWebSiteCategory("产品展示");
fz.use();

WebSite fa = factory.getWebSiteCategory("博客");
fa.use();

WebSite fb = factory.getWebSiteCategory("博客");
fb.use();

WebSite fc = factory.getWebSiteCategory("博客");
fc.use();

System.out.println("网站分类总数为:" + factory.getWebSiteCount());
}

}

源码中的享元模式

享元模式很重要,因为它能帮你在一个复杂的系统中大量的节省内存空间。在JAVA语言中,String类型就是使用了享元模式。String对象是final类型,对象一旦创建就不可改变。在JAVA中字符串常量都是存在常量池中的,JAVA会确保一个字符串常量在常量池中只有一个拷贝。String a=”abc”,其中”abc”就是一个字符串常量。


熟悉java的应该知道下面这个例子:


Stringa="hello";
Stringb="hello";
if(a==b)
 System.out.println("OK");
else
 System.out.println("Error");

输出结果是:OK。可以看出if条件比较的是两a和b的地址,也可以说是内存空间 核心总结,可以共享的对象,也就是说返回的同一类型的对象其实是同一实例,当客户端要求生成一个对象时,工厂会检测是否存在此对象的实例,如果存在那么直接返回此对象实例,如果不存在就创建一个并保存起来,这点有些单例模式的意思。通常工厂类会有一个集合类型的成员变量来用以保存对象,如hashtable,vector等。在java中,数据库连接池,线程池等即是用享元模式的应用。


首先String不属于8种基本数据类型,String是一个对象。
因为对象的默认值是null,所以String的默认值也是null;但它又是一种特殊的对象,有其它对象没有的一些特性。
new String()和new String(“”)都是申明一个新的空字符串,是空串不是null;
String str=”kvill”;
String str=new String (“kvill”);的区别:
在这里,我们不谈堆,也不谈栈,只先简单引入常量池这个简单的概念。
常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。
看例1:

String s0=”kvill”; 

String s1=”kvill”;

String s2=”kv” + “ill”;

System.out.println( s0==s1 );

System.out.println( s0==s2 );

结果为:


true 

true

首先,我们要知结果为道Java会确保一个字符串常量只有一个拷贝。
因为例子中的s0和s1中的”kvill”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而”kv”和”ill”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中”kvill”的一个引用。

所以我们得出s0==s1==s2;


用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。


看例2:


String s0=”kvill”; 

String s1=new String(”kvill”);

String s2=”kv” + new String(“ill”);

System.out.println( s0==s1 );

System.out.println( s0==s2 );

System.out.println( s1==s2 );

结果为:


false 

false

false

例2中s0还是常量池中”kvill”的应用,s1因为无法在编译期确定,所以是运行时创建的新对象”kvill”的引用,s2因为有后半部分new String(“ill”)所以也无法在编译期确定,所以也是一个新创建对象”kvill”的应用;明白了这些也就知道为何得出此结果了。


String.intern():

再补充介绍一点:存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用;看例3就清楚了


例3:


String s0= “kvill”; 

String s1=new String(”kvill”);

String s2=new String(“kvill”);

System.out.println( s0==s1 );

System.out.println( “**********” );

s1.intern();

s2=s2.intern(); //把常量池中“kvill”的引用赋给s2

System.out.println( s0==s1);

System.out.println( s0==s1.intern() );

System.out.println( s0==s2 );

结果为:


false 

**********

false //虽然执行了s1.intern(),但它的返回值没有赋给s1

true //说明s1.intern()返回的是常量池中”kvill”的引用

true

最后我再破除一个错误的理解:


有人说,“使用String.intern()方法则可以将一个String类的保存到一个全局String表中,如果具有相同值的Unicode字符串已经在这个表中,那么该方法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中“如果我把他说的这个全局的String表理解为常量池的话,他的最后一句话,“如果在表中没有相同值的字符串,则将自己的地址注册到表中”是错的:


看例4:


String s1=new String("kvill"); 

String s2=s1.intern();

System.out.println( s1==s1.intern() );

System.out.println( s1+" "+s2 );

System.out.println( s2==s1.intern() );

结果:


false 

kvill kvill

true

在这个类中我们没有声名一个”kvill”常量,所以常量池中一开始是没有”kvill”的,当我们调用s1.intern()后就在常量池中新添加了一个”kvill”常量,原来的不在常量池中的”kvill”仍然存在,也就不是“将自己的地址注册到常量池中”了。


s1==s1.intern()为false说明原来的“kvill”仍然存在;


s2现在为常量池中“kvill”的地址,所以有s2==s1.intern()为true。


关于equals()和==:

这个对于String简单来说就是比较两字符串的Unicode序列是否相当,如果相等返回true;而==是比较两字符串的地址是否相同,也就是是否是同一个字符串的引用。


关于String是不可变的

这一说又要说很多,大家只要知道String的实例一旦生成就不会再改变了,比如说:String str=”kv”+”ill”+” “+”ans”;


就是有4个字符串常量,首先”kv”和”ill”生成了”kvill”存在内存中,然后”kvill”又和” “ 生成 ”kvill “存在内存中,最后又和生成了”kvill ans”;并把这个字符串的地址赋给了str,就是因为String的“不可变”产生了很多临时变量,这也就是为什么建议用StringBuffer的原因了,因为StringBuffer是可改变的。


okhttp3 kotlin ConnectionPool 源码分析

ConnectionPool的说明:
管理http和http/2的链接,以便减少网络请求延迟。同一个address将共享同一个connection。该类实现了复用连接的目标。

class RealConnectionPool(
/** 每个address的最大空闲连接数 */
private val maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) {
/**
* Background threads are used to cleanup expired connections. There will be at most a single
* thread running per connection pool. The thread pool executor permits the pool itself to be
* garbage collected.
*/
//这是一个用于清楚过期链接的线程池,每个线程池最多只能运行一个线程,并且这个线程池允许被垃圾回收
private val executor = ThreadPoolExecutor(
0, // corePoolSize.
Int.MAX_VALUE, // maximumPoolSize.
60L, TimeUnit.SECONDS, // keepAliveTime.
SynchronousQueue(),
threadFactory("OkHttp ConnectionPool", true)
)
//双向队列
private val connections = ArrayDeque<RealConnection>()
//路由的数据库
val routeDatabase = RouteDatabase()
//清理任务正在执行的标志
var cleanupRunning: Boolean = false


  1. 主要就是connections,可见ConnectionPool内部以队列方式存储连接;
  2. routDatabase是一个黑名单,用来记录不可用的route,但是看代码貌似ConnectionPool并没有使用它。所以此处不做分析。
  3. 剩下的就是和清理有关了,所以executor是清理任务的线程池,cleanupRunning是清理任务的标志,cleanupRunnable是清理任务。

class ConnectionPool(
maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) {
//创建一个适用于单个应用程序的新连接池。
//该连接池的参数将在未来的okhttp中发生改变
//目前最多可容乃5个空闲的连接,存活期是5分钟
constructor() : this(5, 5, TimeUnit.MINUTES)
}

init {
//保持活着的时间,否则清理将旋转循环
require(keepAliveDuration > 0L) { "keepAliveDuration <= 0: $keepAliveDuration" }
}

通过这个构造器我们知道了这个连接池最多维持5个连接,且每个链接最多活5分钟。并且包含一个线程池包含一个清理任务。
所以maxIdleConnections和keepAliveDurationNs则是清理中淘汰连接的的指标,这里需要说明的是maxIdleConnections是值每个地址上最大的空闲连接数。所以OkHttp只是限制与同一个远程服务器的空闲连接数量,对整体的空闲连接并没有限制。

这时候说下ConnectionPool的实例化的过程,一个OkHttpClient只包含一个ConnectionPool,其实例化也是在OkHttpClient的过程。这里说一下ConnectionPool各个方法的调用并没有直接对外暴露,而是通过OkHttpClient的Internal接口统一对外暴露。


然后我们来看下他的transmitterAcquirePooledConnection(获取连接)和put方法


fun transmitterAcquirePooledConnection(
address: Address,
transmitter: Transmitter,
routes: List<Route>?,
requireMultiplexed: Boolean
): Boolean {
//断言,判断线程是不是被自己锁住了
assert(Thread.holdsLock(this))
// 遍历已有连接集合
for (connection in connections) {
if (requireMultiplexed && !connection.isMultiplexed) continue
//如果connection和需求中的"地址"和"路由"匹配
if (!connection.isEligible(address, routes)) continue
//复用这个连接
transmitter.acquireConnectionNoEvents(connection)

return true
}
return false
}

put方法更为简单,就是异步触发清理任务,然后将连接添加到队列中


  fun put(connection: RealConnection) {
assert(Thread.holdsLock(this))
if (!cleanupRunning) {
cleanupRunning = true
executor.execute(cleanupRunnable)
}
connections.add(connection)
}

private val cleanupRunnable = object : Runnable {
override fun run() {
while (true) {
val waitNanos = cleanup(System.nanoTime())
if (waitNanos == -1L) return
try {
this@RealConnectionPool.lockAndWaitNanos(waitNanos)
} catch (ie: InterruptedException) {
// Will cause the thread to exit unless other connections are created!
evictAll()
}
}
}
}
这个逻辑也很简单,就是调用cleanup方法执行清理,并等待一段时间,持续清理,其中cleanup方法返回的值来来决定而等待的时间长度。那我们继续来看下cleanup函数:
fun cleanup(now: Long): Long {
var inUseConnectionCount = 0
var idleConnectionCount = 0
var longestIdleConnection: RealConnection? = null
var longestIdleDurationNs = Long.MIN_VALUE

// Find either a connection to evict, or the time that the next eviction is due.
synchronized(this) {
for (connection in connections) {
// If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++
continue
}
//统计空闲连接数量
idleConnectionCount++

// If the connection is ready to be evicted, we're done.
val idleDurationNs = now - connection.idleAtNanos
if (idleDurationNs > longestIdleDurationNs) {
//找出空闲时间最长的连接以及对应的空闲时间
longestIdleDurationNs = idleDurationNs
longestIdleConnection = connection
}
}

when {
longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections -> {
// We've found a connection to evict. Remove it from the list, then close it below
// (outside of the synchronized block).
//在符合清理条件下,清理空闲时间最长的连接
connections.remove(longestIdleConnection)
}
idleConnectionCount > 0 -> {
// A connection will be ready to evict soon.
//不符合清理条件,则返回下次需要执行清理的等待时间,也就是此连接即将到期的时间
return keepAliveDurationNs - longestIdleDurationNs
}
inUseConnectionCount > 0 -> {
// All connections are in use. It'll be at least the keep alive duration 'til we run
// again.
//没有空闲的连接,则隔keepAliveDuration(分钟)之后再次执行
return keepAliveDurationNs
}
else -> {
// No connections, idle or in use.
cleanupRunning = false
return -1
}
}
}
//关闭socket资源
longestIdleConnection!!.socket().closeQuietly()

// Cleanup again immediately.
//这里是在清理一个空闲时间最长的连接以后会执行到这里,需要立即再次执行清理
return 0
}

这里的首先统计空闲连接数量,然后通过for循环查找最长空闲时间的连接以及对应空闲时长,然后判断是否超出最大空闲连接数(maxIdleConnections)或者或者超过最大空闲时间(keepAliveDurationNs),满足其一则清除最长空闲时长的连接。如果不满足清理条件,则返回一个对应等待时间。
这个对应等待的时间又分二种情况:


  1. 有连接则等待下次需要清理的时间去清理:keepAliveDurationNs-longestIdleDurationNs;
  2. 没有空闲的连接,则等下一个周期去清理:keepAliveDurationNs
    如果清理完毕返回-1。

综上所述,我们来梳理一下清理任务,清理任务就是异步执行的,遵循两个指标,最大空闲连接数量和最大空闲时长,满足其一则清理空闲时长最大的那个连接,然后循环执行,要么等待一段时间,要么继续清理下一个连接,知道清理所有连接,清理任务才结束,下一次put的时候,如果已经停止的清理任务则会被再次触发


private fun pruneAndGetAllocationCount(connection: RealConnection, now: Long): Int {
val references = connection.transmitters
var i = 0
//遍历弱引用列表
while (i < references.size) {
val reference = references[i]
//若StreamAllocation被使用则接着循环
if (reference.get() != null) {
i++
continue
}

// We've discovered a leaked transmitter. This is an application bug.
val transmitterRef = reference as TransmitterReference
val message = "A connection to ${connection.route().address.url} was leaked. " +
"Did you forget to close a response body?"
Platform.get().logCloseableLeak(message, transmitterRef.callStackTrace)
//若StreamAllocation未被使用则移除引用,这边注释为泄露
references.removeAt(i)
connection.noNewExchanges = true
//如果列表为空则说明此连接没有被引用了,则返回0,表示此连接是空闲连接
// If this was the last allocation, the connection is eligible for immediate eviction.
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs
return 0
}
}

return references.size
}

pruneAndGetAllocationCount主要是用来标记泄露连接的。内部通过遍历传入进来的RealConnection的StreamAllocation列表,如果StreamAllocation被使用则接着遍历下一个StreamAllocation。如果StreamAllocation未被使用则从列表中移除,如果列表中为空则说明此连接连接没有引用了,返回0,表示此连接是空闲连接,否则就返回非0表示此连接是活跃连接。
接下来让我看下ConnectionPool的connectionBecameIdle()方法,就是当有连接空闲时,唤起cleanup线程清洗连接池

fun connectionBecameIdle(connection: RealConnection): Boolean {
assert(Thread.holdsLock(this))
//该连接已经不可用
return if (connection.noNewExchanges || maxIdleConnections == 0) {
connections.remove(connection)
true
} else {
// Awake the cleanup thread: we may have exceeded the idle connection limit.
//欢迎clean 线程
this.notifyAll()
false
}
}

connectionBecameIdle标示一个连接处于空闲状态,即没有流任务,那么久需要调用该方法,由ConnectionPool来决定是否需要清理该连接。
再来看下evictAll()方法

fun evictAll() {
val evictedConnections = mutableListOf<RealConnection>()
synchronized(this) {
val i = connections.iterator()
while (i.hasNext()) {
val connection = i.next()
if (connection.transmitters.isEmpty()) {
connection.noNewExchanges = true
evictedConnections.add(connection)
i.remove()
}
}
}

for (connection in evictedConnections) {
connection.socket().closeQuietly()
}
}

该方法是删除所有空闲的连接,比较简单,不说了


Integer中的享元模式

那么我们来看看Integer中的享元模式具体是怎么样的吧。
通过如下代码了解一下integer的比较

public static void main(String[] args)
{
Integer integer1 = 9;
Integer integer2 = 9;
System.out.println(integer1==integer2);

Integer integer3 = 129;
Integer integer4 = 129;
System.out.println(integer3==integer4);
}

输出:


true
false

在通过等号赋值的时候,实际上是通过调用valueOf方法的返回一个对象。然后我们观察一下这个方法的源码。


public final class Integer extends Number implements Comparable<Integer> {
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private final int value;
public Integer(int value) {
this.value = value;
}
}

上面是我简化了的Integer类。平常在使用Integer类的时候。你是否思考过用valueOf还是用new创建Integer对象。看完源码就会发现在valueOf这个方法中它会先判断传进去的值是否在IntegerCache中,如果不在就创建新的对象,在就直接返回缓存池里的对象。这个valueOf方法就用到享元模式。它将-128到127的Integer对象先在缓存池里创建好,等我们需要的时候直接返回即可。所以在-128到127中的数值我们用valueOf创建会比new更快。因此我们在使用Integer对象的时候,也一定要记住使用equals(),而不是单纯的使用”==”,否则有可能出现不相等的情况。


收起阅读 »

java设计模式:桥接模式

桥接模式的定义与特点桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。 通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了...
继续阅读 »

桥接模式的定义与特点

桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。


通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。这里将桥接模式的优缺点总结如下。


优点


  • 抽象与实现分离,扩展能力强
  • 符合开闭原则
  • 符合合成复用原则
  • 其实现细节对客户透明

缺点

由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。


桥接模式的结构与实现

可以将抽象化部分与实现化部分分开,取消二者的继承关系,改用组合关系。


模式的结构

抽象化角色:定义抽象类,并包含一个对实现化对象的引用。
扩展抽象化角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
实现化角色:定义实现化角色的接口,供扩展抽象化角色调用。
具体实现化角色:给出实现化角色接口的具体实现。

桥接模式的应用场景

当一个类内部具备两种或多种变化维度时,使用桥接模式可以解耦这些变化的维度,使高层代码架构稳定。


桥接模式通常适用于以下场景。



  1. 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
  2. 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
  3. 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。

桥接模式的一个常见使用场景就是替换继承。我们知道,继承拥有很多优点,比如,抽象、封装、多态等,父类封装共性,子类实现特性。继承可以很好的实现代码复用(封装)的功能,但这也是继承的一大缺点。


示例代码:
在这里插入图片描述


//抽象类:建筑
public abstract class Building {
protected Paint paint;
public Building(Paint paint) {
this.paint = paint;
}
public abstract void decorate();
}
//接口:油漆
public interface Paint {
void decorateImpl();
}
//教学楼
public class TeachingBuilding extends Building {
public TeachingBuilding(Paint paint) {
super(paint);
}

@Override
public void decorate() {
System.out.print("普通的教学楼");
paint.decorateImpl();
}
}
//实验楼
public class LaboratoryBuilding extends Building {
public LaboratoryBuilding(Paint paint) {
super(paint);
}

@Override
public void decorate() {
System.out.print("普通的实验楼");
paint.decorateImpl();
}
}
public class RedPaint implements Paint {
@Override
public void decorateImpl() {
System.out.println("被红色油漆装饰过。");
}
}
public class GreenPaint implements Paint {
@Override
public void decorateImpl() {
System.out.println("被绿色油漆装饰过。");
}
}
public class BulePaint implements Paint {
@Override
public void decorateImpl() {
System.out.println("被蓝色油漆装饰过。");
}
}
public class BridgePatternDemo {
public static void main(String[] args) {
//普通的教学楼被红色油漆装饰。
Building redTeachingBuilding=new TeachingBuilding(new RedPaint());
redTeachingBuilding.decorate();
//普通的教学楼被绿色油漆装饰。
Building greenTeachingBuilding1=new TeachingBuilding(new GreenPaint());
greenTeachingBuilding1.decorate();
//普通的实验楼被红色油漆装饰。
Building redLaboratoryBuilding=new LaboratoryBuilding(new RedPaint());
redLaboratoryBuilding.decorate();
//普通的实验楼被绿色油漆装饰。
Building greenLaboratoryBuilding=new LaboratoryBuilding(new GreenPaint());
greenLaboratoryBuilding.decorate();
//普通的实验楼被蓝色油漆装饰。
Building blueLaboratoryBuilding=new LaboratoryBuilding(new BulePaint());
blueLaboratoryBuilding.decorate();
}
}

运行结果:
普通的教学楼被红色油漆装饰过。
普通的教学楼被绿色油漆装饰过。
普通的实验楼被红色油漆装饰过。
普通的实验楼被绿色油漆装饰过。
普通的实验楼被蓝色油漆装饰过。

桥接模式与装饰模式对比:

两个模式都是为了解决子类过多问题, 但他们的诱因不同:



  1. 桥接模式对象自身有 沿着多个维度变化的趋势 , 本身不稳定;
  2. 装饰者模式对象自身非常稳定, 只是为了增加新功能/增强原功能。

收起阅读 »

你有原则么?懂原则么?想了解么?快看设计模式原则篇,让你做个有原则的程序员

前言无论做啥,要想好设计,就得多扩展,少修改 开闭原则此原则是由”Bertrand Meyer”提出的。原文是:”Software entities should be open for extension,but closed for modificatio...
继续阅读 »

前言

无论做啥,要想好设计,就得多扩展,少修改



开闭原则

此原则是由”Bertrand Meyer”提出的。原文是:”Software entities should be open for extension,but closed for modification”。就是说模块应对扩展开放,而对修改关闭。模块应尽量在不修改原(是”原”,指原来的代码)代码的情况下进行扩展


开闭原则的含义

当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。


开闭原则的作用


  1. 对软件测试的影响
    软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
  2. 可以提高代码的可复用性
    粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
  3. 可以提高软件的可维护性
    遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。

里氏替换原则

里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。


里氏替换原则的作用

它克服了继承中重写父类造成的可复用性变差的缺点。
它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。

里氏替换原则的实现方法

里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。


依赖倒置原则

要面向接口编程,不要面向实现编程。


依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。


由于在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。这里的抽象指的是接口或者抽象类,而细节是指具体的实现类。


使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给它们的实现类去完成。


依赖、倒置原则的作用


  • 依赖倒置原则的主要作用如下。
  • 依赖倒置原则可以降低类间的耦合性。
  • 依赖倒置原则可以提高系统的稳定性。
  • 依赖倒置原则可以减少并行开发引起的风险。
  • 依赖倒置原则可以提高代码的可读性和可维护性。

单一职责原则

单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分


单一职责原则的优点

单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点。



  • 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
  • 提高类的可读性。复杂性降低,自然其可读性会提高。
  • 提高系统的可维护性。可读性提高,那自然更容易维护了。
  • 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。

接口隔离原则

尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。
一个类对另一个类的依赖应该建立在最小的接口上

要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。


接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:



  • 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
  • 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。

    接口隔离原则的优点

    接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点。


  1. 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
  2. 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
  3. 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
  4. 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
  5. 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。

迪米特法则

如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。


迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
迪米特法则的优点
迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点。


  • 降低了类之间的耦合度,提高了模块的相对独立性。
  • 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。

但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。


合成复用原则

要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。


如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。


合成复用原则的重要性

通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。



  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。



  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

收起阅读 »

华为手机升级HarmonyOS全攻略:公测&内测&线下升级

写在前面:本文旨在帮助社区各位小伙伴选择合适的渠道尽早升级HarmonyOS系统,深夜撸稿,还望三连支持一哈!!目前正在进行的升级活动:消费者公测、消费者内测、HarmonyOS体验官(线下)必要说明:所有消费者公测渠道最终都会跳转到花粉俱乐部;初期申请量巨大...
继续阅读 »

写在前面:

本文旨在帮助社区各位小伙伴选择合适的渠道尽早升级HarmonyOS系统,深夜撸稿,还望三连支持一哈!!

目前正在进行的升级活动:消费者公测、消费者内测、HarmonyOS体验官(线下)

必要说明:

所有消费者公测渠道最终都会跳转到花粉俱乐部;

初期申请量巨大,花粉俱乐部很容易就挂掉,心急的小伙伴可尝试线下渠道或者多次尝试或者深夜(两点以后)申请;

申请前务必将“花粉俱乐部”、“我的华为”、“会员中心”升级到最新版本,尤其是“花粉俱乐部”。

消费者公测

包含机型:

Mate X2

Mate40、Mate40E、Mate 40 Pro、Mate 40 Pro+、Mate 40 RS 保时捷设计

P40 5G、P40 4G、P40 Pro、P40 Pro+

Mate 30 4G、Mate 30 Pro 4G、Mate 30 5G、Mate 30 Pro 5G、Mate 30 RS保时捷设计、Mate 30E Pro 5G

MatePad Pro、MatePad Pro 5G

我的华为/花粉俱乐部:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

1.打开“我的华为”,点击“升级尝鲜” / 打开“花粉俱乐部”,点击“公测尝鲜”。

△我的华为

△花粉俱乐部

2.页面加载完成后,点击“公测尝鲜”下的“立即尝鲜”按钮。(花粉俱乐部进入的请忽略此步骤,直接进入下步)

3.接下来在列表中找到当前手机型号对应的公测活动,点击“报名公测”。由于不同手机对应的系统版本不一样,请务必仔细核实你的机器型号。

4.此处会跳转到“花粉论坛”的一篇帖子,划到这篇帖子的末尾,点击“参加公测活动”。接下来系统会引导用户签订《华为公测协议》和《华为公测与隐私声明》,等待10秒点击通过。

5.通过两个协议后,系统会引导你下载协议文件。这个过程会验证你的机型是否符合要求,且下载的文件也是将来升级为正式版的必要文件,如果找到(反正我是没找到)请勿删除!!

6.下载并提示安装完描述文件后,就可以去检测系统更新,下载并更新HarmonyOS了。(P40系列当前版本116)

消费者内测

包含机型:

Mate XS、Mate 20、Mate 20 Pro、Mate 20 RS(保时捷)、Mate 20 X(4G)

nova 8、nova 8 Pro、nova 8 SE、nova 7 5G、nova 7 Pro 5G、nova 7 SE 5G、nova 7 SE 5G活力版、nova 7 SE 5G乐活版、nova 6、nova 6 5G、nova 6 SE

华为畅享20 Plus 5G、华为畅享Z 5G、华为畅享20 Pro5G

华为麦芒9 5G

MatePad 10.8、MatePad 5G 10.4、MatePad 10.4

内测时间:6月2日~6月9日上午10:00

渠道一

会员中心:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

1.打开会员中心,首页上找到“体验先锋”,点击进入。

2.点击顶部的HarmonyOS 2升级尝鲜。

3.进入页面点击报名。如果机型不符合,会弹出提示框。

4.接下来根据流程提示,填写信息,等待审核,坐等更新就好了。

渠道二

花粉俱乐部:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

1.进入首页点击内测报名

2.跳转后,点击立即报名

3.接下来根据流程提示,填写信息,等待审核,坐等更新就好了。

HarmonyOS体验官(线下)

包含机型:

我的华为:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

在APP首页点击“HarmonyOS体验官”海报,经过简单的互动问答即可参加。期间需要提交信息、预约门店时间和信息,最终会生成一张包含数字的海报,用户需要保存此海报才可参与活动。

活动仅在部分门店进行,具体店面和城市请在活动页面查询。到店会提供礼品,并可在线下由店面工作人员协助完成升级。

重要的补充说明

1.消费者公测仅审核设备型号是否合规,避免出现系统和硬件不适配的情况。

2.消费者内测仍然会存在审核机制,但仅审核设备型号是否合规,避免出现系统和硬件不适配的情况。

3.最终稳定的系统版本号预计为:HarmonyOS 2.0.0.116(以实际推送版本号为准!)

4.老荣耀系列机型不在本次消费者公测列表中。

收起阅读 »

ZFPlayer 3.0解析

详细介绍一下ZFPlayer 3.0的用法,如果你有什么问题或者建议可联系我。在3.0之前版本使用ZFPlayer,是不是在烦恼播放器SDK自定义、控制层自定义等问题。作者公司多个项目分别使用不同播放器SDK以及每个项目控制层都不一样,但是为了统一管理、统一调...
继续阅读 »

详细介绍一下ZFPlayer 3.0的用法,如果你有什么问题或者建议可联系我。在3.0之前版本使用ZFPlayer,是不是在烦恼播放器SDK自定义、控制层自定义等问题。作者公司多个项目分别使用不同播放器SDK以及每个项目控制层都不一样,但是为了统一管理、统一调用,我特意写了这个播放器壳子。播放器SDK只要遵守ZFPlayerMediaPlayback协议,控制层只要遵守ZFPlayerMediaControl协议,可以实现自定义播放器和控制层。

目前支持的功能如下:

1、普通模式的播放,类似于腾讯视频、爱奇艺等APP;
2、列表普通模式的播放,包括手动点击播放、滑动到屏幕中间自动播放,wifi网络智能播放等等;
3、列表的亮暗模式播放,类似于微博、UC浏览器视频列表等APP;
4、列表视频滑出屏幕后停止播放、滑出屏幕后小窗播放;
5、优雅的全屏,支持横屏和竖屏全屏模式;

播放器的主要类为ZFPlayerController,具体API请看下边这张图吧,后边我也会详细介绍。在之前版本收到好多开发朋友的Issues建议也好bug也好,ZFPlayer也是致力于解决这些问题和满足各位的建议。

ZFPlayerController(播放器的主要类)

初始化方式:

/// 普通播放的初始化
+ (instancetype)playerWithPlayerManager:(id<ZFPlayerMediaPlayback>)playerManager containerView:(UIView *)containerView;

/// 普通播放的初始化
- (instancetype)initWithPlayerManager:(id<ZFPlayerMediaPlayback>)playerManager containerView:(UIView *)containerView;

/// UITableView、UICollectionView播放的初始化
+ (instancetype)playerWithScrollView:(UIScrollView *)scrollView playerManager:(id<ZFPlayerMediaPlayback>)playerManager containerViewTag:(NSInteger)containerViewTag;

/// UITableView、UICollectionView播放的初始化
- (instancetype)initWithScrollView:(UIScrollView *)scrollView playerManager:(id<ZFPlayerMediaPlayback>)playerManager containerViewTag:(NSInteger)containerViewTag;

/// UIScrollView播放的初始化
+ (instancetype)playerWithScrollView:(UIScrollView *)scrollView playerManager:(id<ZFPlayerMediaPlayback>)playerManager containerView:(UIView *)containerView;

/// UIScrollView播放的初始化
- (instancetype)initWithScrollView:(UIScrollView *)scrollView playerManager:(id<ZFPlayerMediaPlayback>)playerManager containerView:(UIView *)containerView;

属性

/// 初始化时传递的容器视图,用来显示播放器view,和播放器view同等大小
@property (nonatomic, strong) UIView *containerView;

/// 初始化时传递的播放器manager,必须遵守`ZFPlayerMediaPlayback`协议
@property (nonatomic, strong) id<ZFPlayerMediaPlayback> currentPlayerManager;

/// 此属性是设置显示的控制层,自定义UIView遵守`ZFPlayerMediaControl`协议,实现相关协议就可以满足自定义控制层的目的。
@property (nonatomic, strong) UIView<ZFPlayerMediaControl> *controlView;

/// 通知的管理类
@property (nonatomic, strong, readonly) ZFPlayerNotification *notification;

/// 容器的类型(cell和普通View)
@property (nonatomic, assign, readonly) ZFPlayerContainerType containerType;

/// 播放器小窗的容器View
@property (nonatomic, strong, readonly) ZFFloatView *smallFloatView;

/// 播放器小窗是否正在显示
@property (nonatomic, assign, readonly) BOOL isSmallFloatViewShow;

ZFPlayerController (ZFPlayerTimeControl)

/// 0...1.0,调节系统的声音,要是调节播放器声音可以使用播放器管理类设置
@property (nonatomic) float volume;

/// 系统静音,要是调节播放器静音可以使用播放器管理类设置
@property (nonatomic, getter=isMuted) BOOL muted;

// 0...1.0, 系统屏幕亮度
@property (nonatomic) float brightness;

/// 移动网络下自动播放, default is NO.
@property (nonatomic, getter=isWWANAutoPlay) BOOL WWANAutoPlay;

/// 当前播放的下标,只适用于设置了`assetURLs`
@property (nonatomic) NSInteger currentPlayIndex;

/// 在 `assetURLs`中是否是最后一个
@property (nonatomic, readonly) BOOL isLastAssetURL;

/// 在 `assetURLs`中是否是第一个
@property (nonatomic, readonly) BOOL isFirstAssetURL;

/// 当退到后台后是否暂停播放,前提是支持后台播放器模式,default is YES.
@property (nonatomic) BOOL pauseWhenAppResignActive;

/// 当播放器在玩播放时,它会被一些事件暂停,而不是用户点击暂停。
/// 例如,应用程序进入后台或者push到另一个视图控制器
@property (nonatomic, getter=isPauseByEvent) BOOL pauseByEvent;

/// 当前播放器控制器消失,而不是dealloc
@property (nonatomic, getter=isViewControllerDisappear) BOOL viewControllerDisappear;

/// 自定义AVAudioSession, default is NO.
@property (nonatomic, assign) BOOL customAudioSession;

/// 当播放器Prepare时候调用
@property (nonatomic, copy, nullable) void(^playerPrepareToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

///当播放器准备开始播放时候调用
@property (nonatomic, copy, nullable) void(^playerReadyToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

/// 当播放进度改变时候调用.
@property (nonatomic, copy, nullable) void(^playerPlayTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval currentTime, NSTimeInterval duration);

/// 当缓冲进度改变时候调用
@property (nonatomic, copy, nullable) void(^playerBufferTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval bufferTime);

/// 当播放状态改变时候调用
@property (nonatomic, copy, nullable) void(^playerPlayStateChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerPlaybackState playState);

/// 当加载状态改变时候调用.
@property (nonatomic, copy, nullable) void(^playerLoadStateChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerLoadState loadState);

/// 当播放失败时候调用.
@property (nonatomic, copy, nullable) void(^playerPlayFailed)(id<ZFPlayerMediaPlayback> asset, id error);

/// 当播放状态完成时候调用.
@property (nonatomic, copy, nullable) void(^playerDidToEnd)(id<ZFPlayerMediaPlayback> asset);

// 当播放器view的尺寸改变时候调用.
@property (nonatomic, copy, nullable) void(^presentationSizeChanged)(id<ZFPlayerMediaPlayback> asset, CGSize size);

/// 播放下一个,只适用于设置了`assetURLs`
- (void)playTheNext;

/// 播放上一个,只适用于设置了`assetURLs`
- (void)playThePrevious;

/// 播放某一个,只适用于设置了`assetURLs`
- (void)playTheIndex:(NSInteger)index;

/// 停止播放,并且把播放器view和相关通知移除
- (void)stop;

/// 切换当前的PlayerManager,适用场景:播放某一个视频时候使用特定的播放器管理类
- (void)replaceCurrentPlayerManager:(id<ZFPlayerMediaPlayback>)manager;

/**
添加播放器view到cell上
*/
- (void)addPlayerViewToCell;

/**
添加播放器view到容器view上.
*/
- (void)addPlayerViewToContainerView:(UIView *)containerView;

/**
添加播放器到主window上.
*/
- (void)addPlayerViewToKeyWindow;

/**
停止当前在view上的播放并移除播放器view.
*/
- (void)stopCurrentPlayingView;

/**
停止当前在cell上的播放并移除播放器view.
*/
- (void)stopCurrentPlayingCell;

ZFPlayerController (ZFPlayerOrientationRotation)

/// 屏幕旋转管理类
@property (nonatomic, readonly) ZFOrientationObserver *orientationObserver;

///是否支持自动屏幕旋转。
/// iOS8.1~iOS8.3的值为YES,其他iOS版本的值为NO。
///这个属性用于UIViewController ' shouldAutorotate '方法的返回值。
@property (nonatomic, readonly) BOOL shouldAutorotate;

///是否允许视频方向旋转。
///默认值是YES。
@property (nonatomic) BOOL allowOrentitaionRotation;

/// 是否是全屏状态,当ZFFullScreenMode == ZFFullScreenModeLandscape,当currentOrientation是LandscapeLeft或者LandscapeRight,这个值是YES
/// 当ZFFullScreenMode == ZFFullScreenModePortrait,当视频全屏后,这个值是YES
@property (nonatomic, readonly) BOOL isFullScreen;

/// 锁定当前的屏幕方向,目的是禁止设备自动旋转
@property (nonatomic, getter=isLockedScreen) BOOL lockedScreen;

/// 隐藏系统的状态栏
@property (nonatomic, getter=isStatusBarHidden) BOOL statusBarHidden;

/// 使用设备方向旋转屏幕, default NO.
@property (nonatomic, assign) BOOL forceDeviceOrientation;

/// 播放器view当前方向
@property (nonatomic, readonly) UIInterfaceOrientation currentOrientation;

/// 当即将全屏时候会调用
@property (nonatomic, copy, nullable) void(^orientationWillChange)(ZFPlayerController *player, BOOL isFullScreen);

/// 当已经全屏时候会调用
@property (nonatomic, copy, nullable) void(^orientationDidChanged)(ZFPlayerController *player, BOOL isFullScreen);

/// 添加设备方向的监听
- (void)addDeviceOrientationObserver;

/// 移除设备方向的监听
- (void)removeDeviceOrientationObserver;

/// 当 ZFFullScreenMode == ZFFullScreenModeLandscape使用此API设置全屏切换
- (void)enterLandscapeFullScreen:(UIInterfaceOrientation)orientation animated:(BOOL)animated;

/// 当 ZFFullScreenMode == ZFFullScreenModePortrait使用此API设置全屏切换
- (void)enterPortraitFullScreen:(BOOL)fullScreen animated:(BOOL)animated;

/// 内部根据ZFFullScreenMode的值来设置全屏切换
- (void)enterFullScreen:(BOOL)fullScreen animated:(BOOL)animated;

ZFPlayerController (ZFPlayerViewGesture)

/// 手势的管理类
@property (nonatomic, readonly) ZFPlayerGestureControl *gestureControl;

/// 禁用哪些手势,默认支持单击、双击、滑动、缩放手势
@property (nonatomic, assign) ZFPlayerDisableGestureTypes disableGestureTypes;

///不支持的平移手势移动方向
@property (nonatomic) ZFPlayerDisablePanMovingDirection disablePanMovingDirection;

ZFPlayerController (ZFPlayerScrollView)

/// 初始化时候设置的scrollView
@property (nonatomic, readonly, nullable) UIScrollView *scrollView;

/// 只适用于列表播放时候是否自动播放,default is YES.
@property (nonatomic) BOOL shouldAutoPlay;

/// 移动网络自动播放,只有当“shouldAutoPlay”为YES时才支持,默认为NO
@property (nonatomic, getter=isWWANAutoPlay) BOOL WWANAutoPlay;

/// 当前播放的indexPath
@property (nonatomic, nullable) NSIndexPath *playingIndexPath;

/// 初始化时候设置的containerViewTag,根据此tag在cell上找到播放器view显示的位置
@property (nonatomic) NSInteger containerViewTag;

/// 滑出屏幕后是否停止播放,如果设置为NO,滑出屏幕后则会小窗播放,defalut is YES.
@property (nonatomic) BOOL stopWhileNotVisible;

/**
当前播放器滚动滑出屏幕的百分比。
当`stopWhileNotVisible`为YES时使用的属性,停止当前正在播放的播放器。
当`stopWhileNotVisible`为NO时使用的属性,当前正在播放的播放器添加到小容器视图。
范围是0.0~1.0,defalut是0.5。
0.0是player将会消失。
1.0是player消失了。
*/
@property (nonatomic) CGFloat playerDisapperaPercent;

/**
当前播放器滚动到屏幕百分比来播放视频。
范围是0.0~1.0,defalut是0.0。
0.0是玩家将会出现。
1.0是播放器确实出现了。
*/
@property (nonatomic) CGFloat playerApperaPercent;

/// 如果列表播放时候有多个区,使用此API
@property (nonatomic, copy, nullable) NSArray <NSArray <NSURL *>*>*sectionAssetURLs;

/**
播放url的indexPath,而' assetURLs '或' sectionAssetURLs '不为空。

@param indexPath播放url的indexPath。
*/
- (void)playTheIndexPath:(NSIndexPath *)indexPath;

/**
播放url的indexPath,而' assetURLs '或' sectionAssetURLs '不为空。

@param indexPath播放url的indexPath
@param scrollToTop使用动画将当前单元格滚动到顶部。
*/
- (void)playTheIndexPath:(NSIndexPath *)indexPath scrollToTop:(BOOL)scrollToTop;

/**
播放url的indexPath,而' assetURLs '或' sectionAssetURLs '不为空。

@param indexPath播放url的indexPath
@param assetURL播放器URL。
@param scrollToTop使用动画将当前单元格滚动到顶部。
*/
- (void)playTheIndexPath:(NSIndexPath *)indexPath assetURL:(NSURL *)assetURL scrollToTop:(BOOL)scrollToTop;

/**
播放url的indexPath,而' assetURLs '或' sectionAssetURLs '不为空。

@param indexPath播放url的indexPath
@param scrollToTop使用动画将当前单元格滚动到顶部。
@param completionHandler滚动完成回调。
*/
- (void)playTheIndexPath:(NSIndexPath *)indexPath scrollToTop:(BOOL)scrollToTop completionHandler:(void (^ __nullable)(void))completionHandler;

ZFPlayerMediaPlayback—播放器SDK遵守的协议

1、枚举类型:

///  播放状态:未知、播放中、暂停、失败、停止
typedef NS_ENUM(NSUInteger, ZFPlayerPlaybackState) {
ZFPlayerPlayStateUnknown = 0,
ZFPlayerPlayStatePlaying,
ZFPlayerPlayStatePaused,
ZFPlayerPlayStatePlayFailed,
ZFPlayerPlayStatePlayStopped
};
///  加载状态:未知、就绪、可以播放、自动播放、播放暂停
typedef NS_OPTIONS(NSUInteger, ZFPlayerLoadState) {
ZFPlayerLoadStateUnknown = 0,
ZFPlayerLoadStatePrepare = 1 << 0,
ZFPlayerLoadStatePlayable = 1 << 1,
ZFPlayerLoadStatePlaythroughOK = 1 << 2,
ZFPlayerLoadStateStalled = 1 << 3,
};
///  播放画面拉伸模式:无拉伸、等比例拉伸不裁剪、部分内容裁剪按比例填充、非等比例填满
typedef NS_ENUM(NSInteger, ZFPlayerScalingMode) {
ZFPlayerScalingModeNone,
ZFPlayerScalingModeAspectFit,
ZFPlayerScalingModeAspectFill,
ZFPlayerScalingModeFill
};

2、协议属性:

///  播放器视图继承于ZFPlayerView,处理一些手势冲突
@property (nonatomic) ZFPlayerView *view;

/// 0...1.0,播放器音量,不影响设备的音量大小
@property (nonatomic) float volume;

/// 播放器是否静音,不影响设备静音
@property (nonatomic, getter=isMuted) BOOL muted;

/// 0.5...2,播放速率,正常速率为 1
@property (nonatomic) float rate;

/// 当前播放时间
@property (nonatomic, readonly) NSTimeInterval currentTime;

/// 播放总时间
@property (nonatomic, readonly) NSTimeInterval totalTime;

/// 缓冲时间
@property (nonatomic, readonly) NSTimeInterval bufferTime;

/// 视频播放定位时间
@property (nonatomic) NSTimeInterval seekTime;

/// 视频是否正在播放中
@property (nonatomic, readonly) BOOL isPlaying;

/// 视频播放视图的填充模式,默认不做任何拉伸
@property (nonatomic) ZFPlayerScalingMode scalingMode;

/// 检查视频播放是否准备就绪,返回YES,调用play方法直接播放视频;返回NO,调用play方法内部自动调用prepareToPlay方法进行视频播放准备工作
@property (nonatomic, readonly) BOOL isPreparedToPlay;

/// 媒体播放资源URL
@property (nonatomic) NSURL *assetURL;

/// 视频的尺寸
@property (nonatomic, readonly) CGSize presentationSize;

/// 视频播放状态
@property (nonatomic, readonly) ZFPlayerPlaybackState playState;

/// 视频的加载状态
@property (nonatomic, readonly) ZFPlayerLoadState loadState;

///------------------------------------
///如果没有指定controlView,可以调用以下块。
///如果你指定了controlView,下面的代码块不能在外部调用,只能用于“ZFPlayerController”调用。
///------------------------------------

/// 准备播放
@property (nonatomic, copy, nullable) void(^playerPrepareToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

/// 开始播放了
@property (nonatomic, copy, nullable) void(^playerReadyToPlay)(id<ZFPlayerMediaPlayback> asset, NSURL *assetURL);

/// 播放进度改变
@property (nonatomic, copy, nullable) void(^playerPlayTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval currentTime, NSTimeInterval duration);

/// 视频缓冲进度改变
@property (nonatomic, copy, nullable) void(^playerBufferTimeChanged)(id<ZFPlayerMediaPlayback> asset, NSTimeInterval bufferTime);

/// 视频播放状态改变
@property (nonatomic, copy, nullable) void(^playerPlayStatChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerPlaybackState playState);

/// 视频加载状态改变
@property (nonatomic, copy, nullable) void(^playerLoadStatChanged)(id<ZFPlayerMediaPlayback> asset, ZFPlayerLoadState loadState);

/// 视频播放已经结束
@property (nonatomic, copy, nullable) void(^playerDidToEnd)(id<ZFPlayerMediaPlayback> asset);

// 视频的尺寸改变了
@property (nonatomic, copy, nullable) void(^presentationSizeChanged)(id<ZFPlayerMediaPlayback> asset, CGSize size);

///------------------------------------
/// end
///------------------------------------

3、协议方法:

///  视频播放准备,中断除non-mixible之外的任何音频会话
- (void)prepareToPlay;

/// 重新进行视频播放准备
- (void)reloadPlayer;

/// 视频播放
- (void)play;

/// 视频暂停
- (void)pause;

/// 视频重新播放
- (void)replay;

/// 视频播放停止
- (void)stop;

/// 视频播放当前时间的画面截图
- (UIImage *)thumbnailImageAtCurrentTime;

/// 替换当前媒体资源地址
- (void)replaceCurrentAssetURL:(NSURL *)assetURL;

/// 调节播放进度
- (void)seekToTime:(NSTimeInterval)time completionHandler:(void (^ __nullable)(BOOL finished))completionHandler;

ZFPlayerMediaControl—控制层遵守的协议

1、视频状态相关

///  视频播放准备就绪
- (void)videoPlayer:(ZFPlayerController *)videoPlayer prepareToPlay:(NSURL *)assetURL;

/// 视频播放状态改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer playStateChanged:(ZFPlayerPlaybackState)state;

/// 视频加载状态改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer loadStateChanged:(ZFPlayerLoadState)state;

2、播放进度

///  视频播放时间进度
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
currentTime:(NSTimeInterval)currentTime
totalTime:(NSTimeInterval)totalTime;

/// 视频缓冲进度
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
bufferTime:(NSTimeInterval)bufferTime;

/// 视频定位播放时间
- (void)videoPlayer:(ZFPlayerController *)videoPlayer
draggingTime:(NSTimeInterval)seekTime
totalTime:(NSTimeInterval)totalTime;

/// 视频播放结束
- (void)videoPlayerPlayEnd:(ZFPlayerController *)videoPlayer;

3、锁屏

/// 设置播放器锁屏时的协议方法
- (void)lockedVideoPlayer:(ZFPlayerController *)videoPlayer lockedScreen:(BOOL)locked;

4、屏幕旋转

///  播放器全屏模式即将改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer orientationWillChange:(ZFOrientationObserver *)observer;

/// 播放器全屏模式已经改变
- (void)videoPlayer:(ZFPlayerController *)videoPlayer orientationDidChanged:(ZFOrientationObserver *)observer;

/// 当前网络状态发生变化
- (void)videoPlayer:(ZFPlayerController *)videoPlayer reachabilityChanged:(ZFReachabilityStatus)status;

5、手势方法

///  相关手势设置
- (BOOL)gestureTriggerCondition:(ZFPlayerGestureControl *)gestureControl
gestureType:(ZFPlayerGestureType)gestureType
gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
touch:(UITouch *)touch;

/// 单击
- (void)gestureSingleTapped:(ZFPlayerGestureControl *)gestureControl;

/// 双击
- (void)gestureDoubleTapped:(ZFPlayerGestureControl *)gestureControl;

/// 开始拖拽
- (void)gestureBeganPan:(ZFPlayerGestureControl *)gestureControl
panDirection:(ZFPanDirection)direction
panLocation:(ZFPanLocation)location;

/// 拖拽中
- (void)gestureChangedPan:(ZFPlayerGestureControl *)gestureControl
panDirection:(ZFPanDirection)direction
panLocation:(ZFPanLocation)location
withVelocity:(CGPoint)velocity;

/// 拖拽结束
- (void)gestureEndedPan:(ZFPlayerGestureControl *)gestureControl
panDirection:(ZFPanDirection)direction
panLocation:(ZFPanLocation)location;

/// 捏合手势变化
- (void)gesturePinched:(ZFPlayerGestureControl *)gestureControl
scale:(float)scale;

6、scrollView上的播放器视图方法

/**
scrollView中的播放器视图将要出现的回调
*/
- (void)playerWillAppearInScrollView:(ZFPlayerController *)videoPlayer;

/**
scrollView中的播放器视图已经出现的回调
*/
- (void)playerDidAppearInScrollView:(ZFPlayerController *)videoPlayer;

/**
scrollView中的播放器视图即将消失的回调
*/
- (void)playerWillDisappearInScrollView:(ZFPlayerController *)videoPlayer;

/**
scrollView中的播放器视图已经消失的回调
*/
- (void)playerDidDisappearInScrollView:(ZFPlayerController *)videoPlayer;

/**
scrollView中的播放器视图正在显示的回调
*/
- (void)playerAppearingInScrollView:(ZFPlayerController *)videoPlayer playerApperaPercent:(CGFloat)playerApperaPercent;

/**
scrollView中的播放器视图正在消失的回调
*/
- (void)playerDisappearingInScrollView:(ZFPlayerController *)videoPlayer playerDisapperaPercent:(CGFloat)playerDisapperaPercent;

/**
小窗视图显示隐藏的回调
*/
- (void)videoPlayer:(ZFPlayerController *)videoPlayer floatViewShow:(BOOL)show;

代码传送门:https://github.com/renzifeng/ZFPlayer

转自:https://www.jianshu.com/p/90e55deb4d51

收起阅读 »

Android微信工具包,你想要的这里都有~

wxlibrary aar文件使用说明APP 使用示例项目,libs下含有以编译最新的aar资源。wxlibrary arr资源项目,需要引入的资源包项目。aar文件生成,在工具栏直接Gradle - (项目名) - wxlibrary - Tasks - b...
继续阅读 »

wxlibrary aar文件使用说明

一、项目介绍

  1. APP 使用示例项目,libs下含有以编译最新的aar资源。
  2. wxlibrary arr资源项目,需要引入的资源包项目。
  3. aar文件生成,在工具栏直接Gradle - (项目名) - wxlibrary - Tasks - build - assemble,直到编译完成
  4. aar文件位置,打开项目所在文件夹,找到 wxlibrary\build\outputs\aar 下。

二、工程引入工具包

下载项目,可以在APP项目的libs文件下找到*.aar文件(已编译为最新版),选择其中一个引入自己的工程

引入微信工具包及微信SDK

dependencies {
//引入wxlibrary.aar资源
implementation files('libs/wxlibrary-release.aar')
//引入wxlibrary.aar的依赖资源,以下2个
implementation 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:6.6.5'
//eventbus,引入后你的项目将支持EventBus,EventBus是一种用于Android的事件发布-订阅总线,替代广播的传值方式,使用方法可以度娘查询。
implementation 'org.greenrobot:eventbus:3.1.1'
...
}

三、工具包初始准备工作

  • 工程继承WxApplication 或者 application 的 onCreate 下使用,获取 APPkey 和AppSecret需要使用mete-data方式获取。 isCheckSignature() 与 isNowRegister() 默认即可
    WxApiUtil.getInstance().init(getApplicationContext(), true, true);
  • APPkeyAppSecret,需要使用mete-data方式进行赋值

方式一,manifest下覆盖mete-data资源

 
...>



android:name="WX_LIBRARY_WX_APP_KEY"
android:value="123456s"
tools:replace="android:value"/>


android:name="WX_LIBRARY_WX_APP_SECRET"
android:value="567890a"
tools:replace="android:value"/>



方式二,manifest下不覆盖mete-data资源,在gradle(app)下赋值

android {
...
defaultConfig {
...

//todo 微信appKey和appSecret赋值的方法二,2个参数都需要赋值,secret不需要时赋值为空字符串即可
manifestPlaceholders = [
WX_LIBRARY_WX_APP_KEY: '',
WX_LIBRARY_WX_APP_SECRET: ''
]
}
...
}

四、登录、分享和支付的使用,链式写法一句搞定

1. 登录使用

// 注意以下注册回调事件不注册则不会触发
WxLoginUtil.newInstance()
.setSucceed((code) -> {
// 登录过程回调成功 code为微信返回的code
// 如果需要在app获取openID,则在此处使用code向微信服务器请求获取openID。
// 使用WxApiGlobal.getInstance().getAppKey()和WxApiGlobal.getInstance().getAppSecret()获取微信的必要参数,使用前请确保已填写正确参数
return;
})
.setNoInstalled((() -> {
// 微信客户端未安装
return;
}))
.setUserCancel(() -> {
// 用户取消
return;
})
.setFail((errorCode, errStr) -> {
// 其他类型错误, errorCode为微信返回的错误码
return;
})
//发起登录请求
.logIn();
2. 分享使用,注意由于微信分享变更,分享时只要唤起微信客户端,无论是否真正分享,都会返回成功

// 注意以下注册回调事件不注册则不会触发
WxShareUtil.newInstance()
.setSucceed(() -> {
// 分享过程回调成功
})
.setNoInstalled((() -> {
// 微信客户端未安装
}))
.setUserCancel(() -> {
// 用户取消,由于微信调整,用户取消状态不会触发
})
.setFail((errorCode, errStr) -> {
// 其他类型错误, errorCode为微信返回的错误码
})
//发起分享请求
.shareTextMessage("内容", "标题", "描述", ShareType.WXSceneTimeline);
3. 支付使用

// req.appId = json.getString("appid");
// req.partnerId = json.getString("partnerid");
// req.prepayId = json.getString("prepayid");
// req.nonceStr = json.getString("noncestr");
// req.timeStamp = json.getString("timestamp");
// req.packageValue = json.getString("package");
// req.sign = json.getString("sign");
// 此json文本需要包含以上所需字段,或者使用实体方式,不列举
// 注意以下注册回调事件不注册则不会触发
WxPayUtil.newInstance()
.setSucceed(() -> {
// sdk支付成功,向微信服务器查询下具体结果吧
})
.setNoInstalled((() -> {
// 微信客户端未安装
}))
.setUserCancel(() -> {
// 用户取消
})
.setFail((errorCode, errStr) -> {
// 其他类型错误, errorCode为微信返回的错误码
})
//发起分享请求
.payWeChat("json文本");

五、测试说明

由于微信需要在后台配置签名信息,而测试时不能修改一次打包一次进行测试,所以配置项目的签名信息即可在debug模式下使用正式版签名信息。

android {
signingConfigs {
release {
storeFile file('key文件位置,可写相对位置。默认是相对于app的文件夹下')
storePassword 'key文件密码'
keyAlias = '打包别名'
keyPassword '别名密码'
}
}
...
buildTypes {
debug {
signingConfig signingConfigs.release
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}

}


代码下载:mjsoftking-wxlibraryapp-master.zip

收起阅读 »

Android仿微信录制音视频的管理工具

ecorderManager因为在项目中经常需要使用音视频录制,所以写了一个公共库RecorderManager,欢迎大家使用。最新0.4.0-beta.3版本: 1.升级依赖 2.移除EasyPermissions和废弃方法,使用新API registerF...
继续阅读 »

ecorderManager

因为在项目中经常需要使用音视频录制,所以写了一个公共库RecorderManager,欢迎大家使用。

最新0.4.0-beta.3版本: 1.升级依赖 2.移除EasyPermissions和废弃方法,使用新API registerForActivityResult,请采用Java1.8以上版本 3.重构框架,优化代码 4.库调用做部分调整,详见下方文档说明 5.欢迎大家测试反馈完善功能

0.3.2版本:1.移除strings.xml中app_name 2.升级kotlin

0.3.1版本更新:详情见文档 1.新增最小录制时间设置RecordVideoOption.setMinDuration(//最小录制时长(秒数,最小是1,会自动调整不大于最大录制时长)) 2.优化代码

0.3-beta.2版本更新: 1.重构项目代码,kotlin改写部分功能 2.移除rxjava库,减少依赖 3.升级最新SDK 4.新增闪光灯功能,增加计时前提示文本设置 5.增加国际化支持,英文和中文 6.修复已知问题,优化代码 7.对外用户调用API改动较少,主要为内部调整,见下方文档,欢迎大家测试反馈完善功能

0.2.29版本更新: 1.新增圆形进度按钮配置功能 2.新增指定前后置摄像头功能 3.优化代码,调整启动视频录制配置项

0.2.28版本更新: 1.优化视频录制结果获取方式 2.优化代码

0.2.27版本更新: 1.视频录制界面RecordVideoRequestOption新增RecorderOption和hideFlipCameraButton配置 2.优化代码

0.2.26版本更新: 1.项目迁移至AndroidX, 引入Kotlin

0.2.25版本更新: 1.优化权限自动申请,可自动调起视频录制界面 2.规范图片资源命名

一.效果展示

仿微信界面视频录制

2.音频录制界面比较简单,就不放图了

二.引用

1.Add it in your root build.gradle at the end of repositories

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

2.Add the dependency

dependencies {
implementation 'com.github.MingYueChunQiu:RecorderManager:0.3.2'
}

三.使用

1.音频录制

采用默认配置录制

mRecorderManager.recordAudio(mFilePath);

自定义配置参数录制

mRecorderManager.recordAudio(new RecorderOption.Builder()
.setAudioSource(MediaRecorder.AudioSource.MIC)
.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
.setAudioSamplingRate(44100)
.setBitRate(96000)
.setFilePath(path)
.build());

2.视频录制

(1).可以直接使用RecordVideoActivity,实现了仿微信风格的录制界面

从0.2.18开始改为类似

RecorderManagerFactory.getRecordVideoRequest().startRecordVideo(MainActivity.this, 0);

从0.4.0-beta版本开始,因为采用registerForActivityResult,所以直接传入结果回调

      RecorderManagerProvider.getRecordVideoRequester().startRecordVideo(MainActivity.this, new RMRecordVideoResultCallback() {
@Override
public void onResponseRecordVideoResult(@NonNull RecordVideoResultInfo info) {
Log.e("MainActivity", "onActivityResult: " + info.getDuration() + " " + info.getFilePath());
Toast.makeText(MainActivity.this, info.getDuration() + " " + info.getFilePath(), Toast.LENGTH_SHORT).show();
}

@Override
public void onFailure(@NonNull RecorderManagerException e) {
Log.e("MainActivity", "onActivityResult: " + e.getErrorCode() + " " + e.getMessage());
}
});

从0.4.0-beta版本开始:RecorderManagerFactory重命名为RecorderManagerProvider RecorderManagerFactory中可以拿到IRecordVideoPageRequester,在IRecordVideoPageRequester接口中

/**
* 以默认配置打开录制视频界面
*
* @param activity Activity
* @param requestCode 请求码
*/
void startRecordVideo(@NonNull FragmentActivity activity, int requestCode);

/**
* 以默认配置打开录制视频界面
*
* @param fragment Fragment
* @param requestCode 请求码
*/
void startRecordVideo(@NonNull Fragment fragment, int requestCode);

/**
* 打开录制视频界面
*
* @param activity Activity
* @param requestCode 请求码
* @param option 视频录制请求配置信息类
*/
void startRecordVideo(@NonNull FragmentActivity activity, int requestCode, @Nullable RecordVideoRequestOption option);

/**
* 打开录制视频界面
*
* @param fragment Fragment
* @param requestCode 请求码
* @param option 视频录制请求配置信息类
*/
void startRecordVideo(@NonNull Fragment fragment, int requestCode, @Nullable RecordVideoRequestOption option);

从0.4.0-beta版本开始:

public interface IRecordVideoPageRequester extends IRMRequester {

/**
* 以默认配置打开录制视频界面
*
* @param activity Activity
* @param callback 视频录制结果回调
*/
void startRecordVideo(@NonNull FragmentActivity activity, @NonNull RMRecordVideoResultCallback callback);

/**
* 以默认配置打开录制视频界面
*
* @param fragment Fragment
* @param callback 视频录制结果回调
*/
void startRecordVideo(@NonNull Fragment fragment, @NonNull RMRecordVideoResultCallback callback);

/**
* 打开录制视频界面
*
* @param activity Activity
* @param option 视频录制请求配置信息类
* @param callback 视频录制结果回调
*/
void startRecordVideo(@NonNull FragmentActivity activity, @Nullable RecordVideoRequestOption option, @NonNull RMRecordVideoResultCallback callback);

/**
* 打开录制视频界面
*
* @param fragment Fragment
* @param option 视频录制请求配置信息类
* @param callback 视频录制结果回调
*/
void startRecordVideo(@NonNull Fragment fragment, @Nullable RecordVideoRequestOption option, @NonNull RMRecordVideoResultCallback callback);
}

RecordVideoRequestOption可配置最大时长(秒)和文件保存路径

public class RecordVideoRequestOption implements Parcelable {

private String filePath;//文件保存路径
private int maxDuration;//最大录制时间(秒数)
private RecordVideoOption recordVideoOption;//录制视频配置信息类(里面配置的filePath和maxDuration会覆盖外面的)
}

RecordVideoActivity里已经配置好了默认参数,可以直接使用,然后在onActivityResult里拿到视频路径的返回值 返回值为RecordVideoResultInfo

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK && requestCode == 0) {
RecordVideoResultInfo info = data.getParcelableExtra(EXTRA_RECORD_VIDEO_RESULT_INFO);

//从0.2.28版本开始可以使用下面这种方式,更安全更灵活,兼容性强
RecordVideoResultInfo info = RecorderManagerFactory.getRecordVideoResult(data);
//从0.3版本开始
RecordVideoResultInfo info = RecorderManagerFactory.getRecordVideoResultParser().parseRecordVideoResult(data);

if (info != null) {
Log.e("MainActivity", "onActivityResult: " + " "
+ info.getDuration() + " " + info.getFilePath());
}
}
}

从0.4.0-beta.1版本开始: 由于采用Android新API registerForActivityResult,所以startActivityForResult等相关方法皆已废弃,相关回调将直接通过RMRecordVideoResultCallback传递

interface RMRecordVideoResultCallback {

fun onResponseRecordVideoResult(info: RecordVideoResultInfo)

fun onFailure(e: RecorderManagerException)
}

通过下列IRecordVideoPageRequester相关方法,调用时同时传入响应结果回调
void startRecordVideo(@NonNull FragmentActivity activity, @NonNull RMRecordVideoResultCallback callback);

(2).如果想要界面一些控件的样式,可以继承RecordVideoActivity,里面提供了几个protected方法,可以拿到界面的一些控件

/**
* 获取计时控件
*
* @return 返回计时AppCompatTextView
*/
protected AppCompatTextView getTimingView() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getTimingView();
}

/**
* 获取圆形进度按钮
*
* @return 返回进度CircleProgressButton
*/
protected CircleProgressButton getCircleProgressButton() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getCircleProgressButton();
}

/**
* 获取翻转摄像头控件
*
* @return 返回翻转摄像头AppCompatImageView
*/
public AppCompatImageView getFlipCameraView() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getFlipCameraView();
}

/**
* 获取播放控件
*
* @return 返回播放AppCompatImageView
*/
protected AppCompatImageView getPlayView() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getPlayView();
}

/**
* 获取取消控件
*
* @return 返回取消AppCompatImageView
*/
protected AppCompatImageView getCancelView() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getCancelView();
}

/**
* 获取确认控件
*
* @return 返回确认AppCompatImageView
*/
protected AppCompatImageView getConfirmView() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getConfirmView();
}

/**
* 获取返回控件
*
* @return 返回返回AppCompatImageView
*/
protected AppCompatImageView getBackView() {
return mRecordVideoFg == null ? null : mRecordVideoFg.getBackView();
}

想要替换图标资源的话,提供下列名称图片

rm_record_video_flip_camera.png
rm_record_video_cancel.png
rm_record_video_confirm.png
rm_record_video_play.png
rm_record_video_pull_down.png
rm_record_video_flashlight_turn_off.png
rm_record_video_flashlight_turn_on.png

(3).同时提供了对应的RecordVideoFragment,实现与RecordVideoActivity同样的功能,实际RecordVideoActivity就是包裹了一个RecordVideoFragment

1.创建RecordVideoFragment

/**
* 获取录制视频Fragment实例(使用默认配置项)
*
* @param filePath 存储文件路径
* @return 返回RecordVideoFragment
*/
public static RecordVideoFragment newInstance(@Nullable String filePath) {
return newInstance(filePath, 30);
}

/**
* 获取录制视频Fragment实例(使用默认配置项)
*
* @param filePath 存储文件路径
* @param maxDuration 最大时长(秒数)
* @return 返回RecordVideoFragment
*/
public static RecordVideoFragment newInstance(@Nullable String filePath, int maxDuration) {
return newInstance(new RecordVideoOption.Builder()
.setRecorderOption(new RecorderOption.Builder().buildDefaultVideoBean(filePath))
.setMaxDuration(maxDuration)
.build());
}

/**
* 获取录制视频Fragment实例
*
* @param option 录制配置信息对象
* @return 返回RecordVideoFragment
*/
public static RecordVideoFragment newInstance(@Nullable RecordVideoOption option) {
RecordVideoFragment fragment = new RecordVideoFragment();
Bundle args = new Bundle();
args.putParcelable(BUNDLE_EXTRA_RECORD_VIDEO_OPTION, option == null ? new RecordVideoOption() : option);
fragment.setArguments(args);
return fragment;
}

2.然后添加RecordVideoFragment到自己想要的地方就可以了 3.可以设置OnRecordVideoListener,拿到各个事件的回调

public class RecordVideoOption:

private RecorderOption recorderOption;//录制配置信息
private RecordVideoButtonOption recordVideoButtonOption;//录制视频按钮配置信息类
private int minDuration;//最小录制时长(秒数,最小是1,会自动调整不大于最大录制时长),可以和timingHint配合使用
private int maxDuration;//最大录制时间(秒数)
private RecorderManagerConstants.CameraType cameraType;//摄像头类型
private boolean hideFlipCameraButton;//隐藏返回翻转摄像头按钮
private boolean hideFlashlightButton;//隐藏闪光灯按钮
private String timingHint;//录制按钮上方提示语句(默认:0:%s),会在计时前显示
private String errorToastMsg;//录制发生错误Toast(默认:录制时间小于1秒,请重试)

原OnRecordVideoListener现已改为RMOnRecordVideoListener,并从RecordVideoOption中移除,主要用于用户自己activity或fragment实现此接口,用于承载RecordVideoFragment,获取相关步骤回调

interface RMOnRecordVideoListener {

/**
* 当完成一次录制时回调
*
* @param filePath 视频文件路径
* @param videoDuration 视频时长(毫秒)
*/
fun onCompleteRecordVideo(filePath: String?, videoDuration: Int)

/**
* 当点击确认录制结果按钮时回调
*
* @param filePath 视频文件路径
* @param videoDuration 视频时长(毫秒)
*/
fun onClickConfirm(filePath: String?, videoDuration: Int)

/**
* 当点击取消按钮时回调
*
* @param filePath 视频文件路径
* @param videoDuration 视频时长(毫秒)
*/
fun onClickCancel(filePath: String?, videoDuration: Int)

/**
* 当点击返回按钮时回调
*/
fun onClickBack()
}

4.RecordVideoButtonOption是圆形进度按钮配置类

	private @ColorInt
int idleCircleColor;//空闲状态内部圆形颜色
private @ColorInt
int pressedCircleColor;//按下状态内部圆形颜色
private @ColorInt
int releasedCircleColor;//释放状态内部圆形颜色
private @ColorInt
int idleRingColor;//空闲状态外部圆环颜色
private @ColorInt
int pressedRingColor;//按下状态外部圆环颜色
private @ColorInt
int releasedRingColor;//释放状态外部圆环颜色
private int idleRingWidth;//空闲状态外部圆环宽度
private int pressedRingWidth;//按下状态外部圆环宽度
private int releasedRingWidth;//释放状态外部圆环宽度
private int idleInnerPadding;//空闲状态外部圆环与内部圆形之间边距
private int pressedInnerPadding;//按下状态外部圆环与内部圆形之间边距
private int releasedInnerPadding;//释放状态外部圆环与内部圆形之间边距
private boolean idleRingVisible;//空闲状态下外部圆环是否可见
private boolean pressedRingVisible;//按下状态下外部圆环是否可见
private boolean releasedRingVisible;//释放状态下外部圆环是否可见

5.RecorderOption是具体的录制参数配置类

	private int audioSource;//音频源
private int videoSource;//视频源
private int outputFormat;//输出格式
private int audioEncoder;//音频编码格式
private int videoEncoder;//视频编码格式
private int audioSamplingRate;//音频采样频率(一般44100)
private int bitRate;//视频编码比特率
private int frameRate;//视频帧率
private int videoWidth, videoHeight;//视频宽高
private int maxDuration;//最大时长
private long maxFileSize;//文件最大大小
private String filePath;//文件存储路径
private int orientationHint;//视频录制角度方向

(4).如果想自定义自己的界面,可以直接使用RecorderManagerable类

1.通过RecorderManagerFactory获取IRecorderManager 从0.4.0-beta版本开始:RecorderManagerFactory重命名为RecorderManagerProvider

public class RecorderManagerFactory {

private RecorderManagerFactory() {
}

/**
* 创建录制管理类实例(使用默认录制类)
*
* @return 返回录制管理类实例
*/
@NonNull
public static IRecorderManager newInstance() {
return newInstance(new RecorderHelper());
}

/**
* 创建录制管理类实例(使用默认录制类)
*
* @param intercept 录制管理器拦截器
* @return 返回录制管理类实例
*/
@NonNull
public static IRecorderManager newInstance(@NonNull IRecorderManagerInterceptor intercept) {
return newInstance(new RecorderHelper(), intercept);
}

/**
* 创建录制管理类实例
*
* @param helper 实际录制类
* @return 返回录制管理类实例
*/
@NonNull
public static IRecorderManager newInstance(@NonNull IRecorderHelper helper) {
return newInstance(helper, null);
}

/**
* 创建录制管理类实例
*
* @param helper 实际录制类
* @param intercept 录制管理器拦截器
* @return 返回录制管理类实例
*/
@NonNull
public static IRecorderManager newInstance(@NonNull IRecorderHelper helper, @Nullable IRecorderManagerInterceptor intercept) {
return new RecorderManager(helper, intercept);
}

@NonNull
public static IRecordVideoRequest getRecordVideoRequest() {
return new RecordVideoPageRequest();
}

//0.3之后版本通过解析器来进行处理数据
@NonNull
public static IRecordVideoResultParser getRecordVideoResultParser() {
return new RecordVideoResultParser();
}
}

它们返回的都是IRecorderManager 接口类型,RecorderManager 是默认的实现类,RecorderManager 内持有一个真正进行操作的RecorderHelper。

public interface IRecorderManager extends IRecorderHelper {

/**
* 设置录制对象
*
* @param helper 录制对象实例
*/
void setRecorderHelper(@NonNull IRecorderHelper helper);

/**
* 获取录制对象
*
* @return 返回录制对象实例
*/
@NonNull
IRecorderHelper getRecorderHelper();

/**
* 初始化相机对象
*
* @param holder Surface持有者
* @return 返回初始化好的相机对象
*/
@Nullable
Camera initCamera(@NonNull SurfaceHolder holder);

/**
* 初始化相机对象
*
* @param cameraType 指定的摄像头类型
* @param holder Surface持有者
* @return 返回初始化好的相机对象
*/
@Nullable
Camera initCamera(@NonNull RecorderManagerConstants.CameraType cameraType, @NonNull SurfaceHolder holder);

/**
* 打开或关闭闪光灯
*
* @param turnOn true表示打开,false关闭
*/
boolean switchFlashlight(boolean turnOn);

/**
* 翻转摄像头
*
* @param holder Surface持有者
* @return 返回翻转并初始化好的相机对象
*/
@Nullable
Camera flipCamera(@NonNull SurfaceHolder holder);

/**
* 翻转到指定类型摄像头
*
* @param cameraType 摄像头类型
* @param holder Surface持有者
* @return 返回翻转并初始化好的相机对象
*/
@Nullable
Camera flipCamera(@NonNull RecorderManagerConstants.CameraType cameraType, @NonNull SurfaceHolder holder);

/**
* 获取当前摄像头类型
*
* @return 返回摄像头类型
*/
@NonNull
RecorderManagerConstants.CameraType getCameraType();

/**
* 释放相机资源
*/
void releaseCamera();

}

RecorderManagerIntercept实现IRecorderManagerInterceptor接口,用户可以直接继承RecorderManagerIntercept,它里面所有方法都是空实现,可以自己改写需要的方法

public interface or extends ICameraInterceptor {}

IRecorderHelper是一个接口类型,由实现IRecorderHelper的子类来进行录制操作,默认提供的是RecorderHelper,RecorderHelper实现了IRecorderHelper。

public interface IRecorderHelper {

/**
* 录制音频
*
* @param path 文件存储路径
* @return 返回是否成功开启录制,成功返回true,否则返回false
*/
boolean recordAudio(@NonNull String path);

/**
* 录制音频
*
* @param option 存储录制信息的对象
* @return 返回是否成功开启录制,成功返回true,否则返回false
*/
boolean recordAudio(@NonNull RecorderOption option);

/**
* 录制视频
*
* @param camera 相机
* @param surface 表面视图
* @param path 文件存储路径
* @return 返回是否成功开启录制,成功返回true,否则返回false
*/
boolean recordVideo(@Nullable Camera camera, @Nullable Surface surface, @Nullable String path);

/**
* 录制视频
*
* @param camera 相机
* @param surface 表面视图
* @param option 存储录制信息的对象
* @return 返回是否成功开启视频录制,成功返回true,否则返回false
*/
boolean recordVideo(@Nullable Camera camera, @Nullable Surface surface, @Nullable RecorderOption option);

/**
* 释放资源
*/
void release();

/**
* 获取录制器
*
* @return 返回实例对象
*/
@NonNull
MediaRecorder getMediaRecorder();

/**
* 获取配置信息对象
*
* @return 返回实例对象
*/
@Nullable
RecorderOption getRecorderOption();
}

2.拿到后创建相机对象

		if (mCamera == null) {
mCamera = mManager.initCamera(mCameraType, svVideoRef.get().getHolder());
mCameraType = mManager.getCameraType();
}

3.录制

isRecording = mManager.recordVideo(mCamera, svVideoRef.get().getHolder().getSurface(), mOption.getRecorderOption());

4.释放

	    mManager.release();
mManager = null;
mCamera = null;

代码下载:MingYueChunQiu-RecorderManager-master.zip
收起阅读 »

kotlin编写的 Android 开源播放器, 开箱即用

介绍功能特性1、通过 dependence 引入MXVideo2、页面集成3、开始播放MXPlaySource 可选参数说明:4、监听播放进度5、全屏返回 + 释放资源功能相关

MXVideo

介绍

基于饺子播放器、kotlin编写的 Android 开源播放器, 开箱即用,欢迎提 issue 和 pull request 最新版本:

功能特性

  • 任意播放器内核(包含开源IJK、谷歌Exo、阿里云等等)
  • 单例播放,只能同时播放一个节目
  • 0代码集成全屏功能
  • 可以调节音量、屏幕亮度
  • 可以注册播放状态监听回调
  • 播放器高度可以根据视频高度自动调节
  • 播放器支持设置宽高比,设置宽高比后,高度固定。
  • 自动保存与恢复播放进度(可关闭)
  • 支持循环播放、全屏时竖屏模式、可关闭快进快退功能、可关闭全屏功能、可关闭非WiFi环境下流量提醒

1、通过 dependence 引入MXVideo

    dependencies {
implementation 'com.gitee.zhangmengxiong:MXVideo:x.x.x'
}

2、页面集成

        
android:id="@+id/mxVideoStd"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

3、开始播放
// 设置播放占位图
Glide.with(this).load("http://www.xxx.com/xxx.png").into(mxVideoStd.getPosterImageView())

// 默认从上一次进度播放
mxVideoStd.setSource(MXPlaySource(Uri.parse("https://aaa.bbb.com/xxx.mp4"), "标题1"))
mxVideoStd.startPlay()

// 从头开始播放
mxVideoStd.setSource(MXPlaySource(Uri.parse("https://aaa.bbb.com/xxx.mp4"), "标题1"), seekTo = 0)
mxVideoStd.startPlay()

// 从第10秒开始播放
mxVideoStd.setSource(MXPlaySource(Uri.parse("https://aaa.bbb.com/xxx.mp4"), "标题1"), seekTo = 10)
mxVideoStd.startPlay()


MXPlaySource 可选参数说明:

参数说明默认值
title标题""
headerMap网络请求头部null
changeOrientationWhenFullScreen全屏时是否需要变更Activity方向,如果 = null,会自动根据视频宽高来判断null
isLooping是否循环播放false
enableSaveProgress是否存储、读取播放进度true
isLiveSource是否直播源,当时直播时,不显示进度,无法快进快退暂停false

4、监听播放进度

mxVideoStd.addOnVideoListener(object : MXVideoListener() {
// 播放状态变更
override fun onStateChange(state: MXState) {
}

// 播放时间变更
override fun onPlayTicket(position: Int, duration: Int) {
}
})

5、全屏返回 + 释放资源

这里MXVideo默认持有当前播放的MXVideoStd,可以使用静态方法操作退出全屏、释放资源等功能。

也可以直接使用viewId:mxVideoStd.isFullScreen(),mxVideoStd.isFullScreen(),mxVideoStd.release() 等方法。

    override fun onBackPressed() {
if (MXVideo.isFullScreen()) {
MXVideo.gotoNormalScreen()
return
}
super.onBackPressed()
}

override fun onDestroy() {
MXVideo.releaseAll()
super.onDestroy()
}

功能相关

  • 切换播放器内核
// 默认MediaPlayer播放器,库默认内置
com.mx.video.player.MXSystemPlayer

// 谷歌的Exo播放器
com.mx.mxvideo_demo.player.MXExoPlayer

// IJK播放器
com.mx.mxvideo_demo.player.MXIJKPlayer

// 设置播放源是可以设置内核,默认 = MXSystemPlayer
mxVideoStd.setSource(MXPlaySource(Uri.parse("xxx"), "xxx"), player = MXSystemPlayer::class.java)
  • 视频渲染旋转角度
// 默认旋转角度 = MXOrientation.DEGREE_0
mxVideoStd.setOrientation(MXOrientation.DEGREE_90)
  • 视频填充规则
// 强制填充宽高 MXScale.FILL_PARENT
// 根据视频大小,自适应宽高 MXScale.CENTER_CROP

// 默认填充规则 = MXScale.CENTER_CROP
mxVideoStd.setScaleType(MXScale.CENTER_CROP)
  • MXVideoStd 控件宽高约束

在页面xml中添加,layout_width一般设置match_parent,高度wrap_content

    
android:id="@+id/mxVideoStd"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

可以设置任意宽高比,如果设置宽高比,则控件高度需要设置android:layout_height="wrap_content",否则不生效。

当取消约束、MXVideo高度自适应、填充规则=MXScale.CENTER_CROP时,控件高度会自动根据视频宽高自动填充高度

// MXVideoStd控件设置宽高比= 16:9
mxVideoStd.setDimensionRatio(16.0 / 9.0)

// MXVideoStd控件设置宽高比= 4:3
mxVideoStd.setDimensionRatio(4.0 / 3.0)

// 取消约束
mxVideoStd.setDimensionRatio(0.0)
  • 进度跳转
// 进度单位:秒  可以在启动播放后、错误或播完之前调用
mxVideoStd.seekTo(55)
  • 设置不能快进快退
// 播放前设置 默认=true
mxVideoStd.getConfig().canSeekByUser = false
  • 设置不能全屏
// 播放前设置 默认=true
mxVideoStd.getConfig().canFullScreen = false
  • 设置不显示控件右上角时间
// 播放前设置 默认=true
mxVideoStd.getConfig().canShowSystemTime = false
  • 设置不显示控件右上角电量图
// 播放前设置 默认=true
mxVideoStd.getConfig().canShowBatteryImg = false
  • 设置关闭WiFi环境播放前提醒
// 播放前设置 默认=true
mxVideoStd.getConfig().showTipIfNotWifi = false
  • 设置播放完成后自动退出全屏
// 播放前设置 默认=true
mxVideoStd.getConfig().gotoNormalScreenWhenComplete = true
  • 设置播放错误后自动退出全屏
// 播放前设置 默认=true
mxVideoStd.getConfig().gotoNormalScreenWhenError = true
  • 设置屏幕方向根据重力感应自动进入全屏、小屏模式
// 播放前设置 默认=false
mxVideoStd.getConfig().autoRotateBySensor = true


代码下载:zhangmengxiong-MXVideo-master.zip

iOS逆向(8)-Monkey、Logos

由于最近微信大佬发飙,罚了红包外挂5000万大洋,这就让人很慌了,别说罚我5000万,5000块我都吃不消。所以笔者决定以后不用微信做例子了。换成优酷了😈。本文会对优酷的设置页面增加一个开启/关闭屏蔽广告的Cell(仅UI)。效果可见下文配图。在之前的几篇文章...
继续阅读 »

由于最近微信大佬发飙,罚了红包外挂5000万大洋,这就让人很慌了,别说罚我5000万,5000块我都吃不消。所以笔者决定以后不用微信做例子了。换成优酷了😈。

本文会对优酷的设置页面增加一个开启/关闭屏蔽广告的Cell(仅UI)。效果可见下文配图。

在之前的几篇文章里已经介绍了APP重签名,代码注入,Hook原理,可以发现,将工程建好,脚本写好,我们就可以以代价非常小的方式对一个第三方的APP进行分析。
那么是否一种工具,可以将重签名,代码注入,Hook源代码,class-dump,Cydia Substrate,甚至是恢复符号表这些功能,集成在一个工程里面,让真正的逆向小白也能享受逆向的乐趣呢?
答案是肯定的,Monkey就是这样的一个非越狱插件开发集成神器!

老规矩,片头先上福利:点击下载demo
这篇文章会用到的工具有:

1、MonkeyDev
2、博主自己砸壳的优酷ipa包 提取码: xtua
3、砸壳后的SimpleAppDemo.ipa 提取码: afnc

一、Monkey

什么是Monkey?
原有iOSOpenDev的升级,非越狱插件开发集成神器!

可以使用Xcode开发CaptainHook Tweak、Logos Tweak 和 Command-line Tool,在越狱机器开发插件,这是原来iOSOpenDev功能的迁移和改进。

1、只需拖入一个砸壳应用,自动集成class-dump、restore-symbol、* Reveal、Cycript和注入的动态库并重签名安装到非越狱机器。
2、支持调试自己编写的动态库和第三方App
3、支持通过CocoaPods第三方应用集成SDK以及非越狱插件,简单来说就是通过CocoaPods搭建了一个非越狱插件商店。

环境要求

使用工具前确保如下几点:

1、安装最新的theos

sudo git clone --recursive https://github.com/theos/theos.git /opt/theos

2、安装ldid(如安装theos过程安装了ldid,跳过)

brew install ldid

安装

你可以通过以下命令选择指定的Xcode进行安装:

sudo xcode-select -s /Applications/Xcode-beta.app

默认安装的Xcode为:

xcode-select -p

执行安装命令:

sudo /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/AloneMonkey/MonkeyDev/master/bin/md-install)"

卸载

sudo /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/AloneMonkey/MonkeyDev/master/bin/md-uninstall)"

更新
如果没有发布特殊说明,使用如下命令更新即可:

sudo /bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/AloneMonkey/MonkeyDev/master/bin/md-update)"

安装/更新之后重启下Xcode再新建项目。如果看到如下选项,即代表安装成功,如果没有,重复上面步骤再来一遍。


二、Logos

Logos是Thoes开发的一套组件,可非常方便用于的Hook OC代码。

接下来我们就介绍下Logos的简单用法,最后运用Monkey和Logos给优酷增加一点UI。

1、创建一个简单的工程
创建工程SimpleAppDemo,里面只有一个按钮,点击按钮弹出一个Alert。 点击下载:SimpleAppDemo
按钮对应的方法为:

- (IBAction)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来啦" message:@"老弟😁😁😁" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
}

2、砸壳
对SimpleAppDemo参数的ipa文件进行砸壳,砸壳过程就不在这详细描述了,这里有笔者已经砸壳好的ipa:SimpleAppDemo.ipa 提取码: afnc

3、新建一个Monkey工程
取名LogosDemo,将下面下载好的SimpleAppDemo.ipa,放到工程对应的目录下:


配好证书(随意一个能在手机上运行的证书即可),Run。运行成功~

4、玩转Logos
在上一步建好的Monkey工程中,可以发现在目录有一个Logos目录:


默认有两个文件LogosDemoDylib.xm和LogosDemoDylib.mm。
其中Logos语句就是写在LogosDemoDylib.xm中的,LogosDemoDylib.mm是根据LogosDemoDylib.xm中的内容自动生成的。
接下来,咱们根据几个需求来介绍Logos的一些常用的用法。

1、更改点击按钮的弹框内容(hook)
由于需要更改弹窗,所以首先导入UIKit框架。

#import <UIKit/UIKit.h>

由于咱们手上有源码,所以可以直接跳过动态分析的这一步,直接就知道按钮所处的页面是叫做ViewController,按钮的响应方法是:

- (IBAction)tapAction:(id)sender

利用hook命令:

#import <UIKit/UIKit.h>

// hook + 类名
%hook ViewController
// IBAction == void
- (void)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来什么来" message:@"😡😡😡" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
}

%end

运行项目,发现按钮已经被成功hook了。


2、调用原方法(orig)

#import <UIKit/UIKit.h>

%hook ViewController

- (void)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来什么来" message:@"😡😡😡" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
// 调用原方法
%orig;
}

%end

3、新增一个方法,并且调用(new)

由于在Monkey工程里面是编译不到源码的,所以无论是新增的方法,还是调用原工程中的方法,都是无法通过编译的,所以都需要使用interface申明每一个方法。

#import <UIKit/UIKit.h>

// 这里只是为了申明
@interface ViewController

- (void)newFunC;

@end

%hook ViewController

// 新增方法关键字new
%new
- (void)newFunC{
NSLog(@"newFunC");
}

// IBAction == void
- (void)tapAction:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"来什么来" message:@"😡😡😡" delegate:nil cancelButtonTitle:@"我知道了" otherButtonTitles:nil, nil];
[alert show];
[self newFunC];
// 调用原方法关键字orig
%orig;
}

%end

文中所有的Demo都在这可以下载到:Dmoe

Logos除了以上hook,end,orig,new这几种关键字,还有:
%subclass:增加一个类
%log:打印,类似NSLog
%group: 给代码分组,可以用于在不同环境加载不同的代码,比如iOS8加载group1,iOS9加载group2,如果部分中,默认所有代码在名为「_ungrouped」的隐藏分组中。
...

所有的Logos语法都可以在官方文档中查询得到。

5、给优酷加UI

首先在这里下载笔者自己砸壳后优酷ipa包(arm64架构的):优酷(砸壳).ipa 提取码: xtua

Step 1、新建工程YouKu

同样的新建一个Monkey工程,取名YouKu,将下载好的ipa包放入工程对应的TargetApp目录下。Run。同样是重签名成功。

在上面的Demo中,我们是对我们直接的工程进行HOOK,由于我们手上有源码,所以我们越过了最难的一个步骤:动态分析。
而我们现在要对优酷进行Hook,但我们手上是没有优酷的源码的,所以此时此刻就需要对其进行动态分析了。
下面我将结合Xcode和class dump对优酷的设置页面简单的进行分析。

Step 2、class dump

class-dump is a command-line utility for examining the Objective-C segment of Mach-O files. It generates declarations for the classes, categories and protocols. This is the same information provided by using 'otool -ov', but presented as normal Objective-C declarations.
简单说就是一个可以导出一个MachO文件的所有头文件信息(包括Extension)

在文首有提到Monkey除了重签名,还集成了class dump的功能,所以我们需要做的就仅仅是开启这个功能:


Run!成功之后可以发现在工程目录下多了一个文件夹Youkui4Phone_Headers,其中就是优酷的所有的头文件了。


Step 3、分析优酷设置页面

工程Run成功后,点击进入设置页面(不用登录),如下图:


我们现在要做的就是在这个页面的TableView的最后一行加上Cell,里面有个Switch,用于打开/关闭屏蔽广告功能(只是UI,这篇文章不牵扯到屏蔽广告的具体实现,如果你需要,点个小心心,持续关注我哦😀😀😀)。

利用伟大的Xcode我们可以非常清晰的看到,设置页面的DataSource和Delegate都是在SettingViewController中,


咱们就找到Hook的类名:SettingViewController
需要Hook的方法自然就是TableView的那些DataSource和Delegate了。

这里需要额外提到的一点是,在文章开始的时候就说了Monkey已经将Cydia Substrate集成进去了,所以我们可以直接使用Cydia Substrate的相关功能了。

在这里我们需要拿到这个页面TableView的对应的变量,我们就需要使用到Cydia Substrate的功能了。打开上文中获取到优酷的所有的头文件,所有SettingViewController,发现其只有一个TableView变量:_tabview。
那么毫无疑问,就是他了!
而获取它的方法是:

MSHookIvar <UITableView *>(self,"_tabview")

一个reloadData的简单使用:

[MSHookIvar <UITableView *>(self,"_tabview") reloadData];

其他的UI代码在这里就不一一解释了,全部代码如下,当然在Demo中也是有的,其中包括了数据的简单持久化功能:

#import <UIKit/UIKit.h>
#define FYDefaults [NSUserDefaults standardUserDefaults]
#define FYSwitchUserDefaultsKey @"FYSwitchUserDefaultsKey"

@interface SettingViewController
- (long long)numberOfSectionsInTableView:(id)arg1;
@end

%hook SettingViewController

%new
-(void)switchChangeAction:(UISwitch *)switchView{
[FYDefaults setBool:switchView.isOn forKey:FYSwitchUserDefaultsKey];
[FYDefaults synchronize];
[MSHookIvar <UITableView *>(self,"_tabview") reloadData];
}

//多少组
- (long long)numberOfSectionsInTableView:(id)arg1{
UITableView * tableView = MSHookIvar <UITableView *>(self,"_tabview");
NSLog(@"fy_numberOfSectionsInTableView:");
// 额外增加一个
return %orig+1;
}

//每组多少行
- (long long)tableView:(UITableView *)tableView numberOfRowsInSection:(long long)section{
NSLog(@"fy_numberOfRowsInSection:");
//定位设置界面,并且是最后一个
if(section == [self numberOfSectionsInTableView:tableView]-1){
return 1;
}
else{
return %orig;
}
}

//返回高度
- (double)tableView:
(UITableView *)tableView heightForRowAtIndexPath:(id)indexPath{
NSLog(@"fy_heightForRowAtIndexPath:");
//定位设置界面,并且是最后一个
if([indexPath section] ==[self numberOfSectionsInTableView:tableView]-1){
return 44;
}
else{
return %orig;
}
}


//每一个Cell
- (id)tableView:(UITableView *)tableView cellForRowAtIndexPath:(id)indexPath{
NSLog(@"fy_cellForRowAtIndexPath:");
//定位设置界面,并且是最后一组
if([indexPath section] == [self numberOfSectionsInTableView:tableView]-1){
UITableViewCell * cell = nil;
if([indexPath row] == 0){
static NSString *swCell = @"SwCellIdentifier";
cell = [tableView dequeueReusableCellWithIdentifier:swCell];
if(!cell){
cell = [[UITableViewCell alloc] initWithStyle:(UITableViewCellStyleDefault) reuseIdentifier:nil];
}
cell.textLabel.text = @"免广告";
// 免广告开关
UISwitch *switchView = [[UISwitch alloc] init];
switchView.on = [FYDefaults boolForKey:FYSwitchUserDefaultsKey];
[switchView addTarget:self action:@selector(switchChangeAction:) forControlEvents:(UIControlEventValueChanged)];
cell.accessoryView = switchView;
cell.imageView.image = [UIImage imageNamed:([FYDefaults boolForKey:FYSwitchUserDefaultsKey] == 1) ? @"unlocked" : @"locked"];
}
cell.backgroundColor = [UIColor whiteColor];
return cell;

}else{
return %orig;
}
}

%end

最后的效果


6、为什么Monkey这么牛逼

查看重新编译后的app文件,可以发现其中的Framework多了很多东西:


从这可以得知,原来Monkey其实也是通过将诸多的动态库(包括自己的工程)注入的形式,实现了这些功能。

三、总结

在这片文章中主要介绍了Monkey的一些用法已经Logos的基本语法。而在上一篇其实留了一个小尾巴,就是Cycript,笔者将要在下一篇文章中重点讲解Cycript的安装,基础用法和高级用法。之所以放在下一篇,是因为Cycript配合Monkey将会有事半功倍的效果。

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

收起阅读 »

iOS 优秀框架之TYAttributedLabel(基于coreText的图文混排)

TYAttributedLabel1、TYAttributedLabel 简单,强大的属性文本控件(无需了解CoreText)2、支持富文本,图文混排显示,支持行间距,字间距,自适应高度,指定行数3、支持添加高度自定义文本属性4、支持添加属性文本,自定义链接,...
继续阅读 »

TYAttributedLabel

1、TYAttributedLabel 简单,强大的属性文本控件(无需了解CoreText)
2、支持富文本,图文混排显示,支持行间距,字间距,自适应高度,指定行数
3、支持添加高度自定义文本属性
4、支持添加属性文本,自定义链接,新增高亮效果显示(文字和背景)
5、支持添加UIImage和UIView控件

demo演示


重点类简介

TYAttributedLabel

创建label(可接受文本及富文本)
设置字体间距
设置行间距
设置字体大小
设置view的位置和宽,会自动计算高度
设置链接文本,并用代理(TYAttributedLabelDelegate)方法完成点击后需完成的任务

TYImageStorage

可创建一个append在TYAttributedLabel后的图片控件,可自定义图片大小,及对齐样式

TYTextStorage

文本文件,可设置文本大小及字体颜色

TYTextContainer

属性文本生成器(使用 RegexKitLite)
具体代码及使用细节请看作者的demo(作者是华人),讲的很详细,这里就不再赘述

链接:TYAttributedLabel

链接:https://www.jianshu.com/p/5d81bf7e79c8

收起阅读 »

如何让10万条数据的小程序列表如丝般顺滑

某天闲着无聊想练一下手速,去上拉一个小程序项目中一个有1万多条商品数据的列表。在数据加载到1000多条后,是列表居然出现了白屏。看了一下控制台:‘Dom limit exceeded’,dom数超出了限制, 不知道微信是出于什么考虑,要限制页面的dom数量。一...
继续阅读 »

某天闲着无聊想练一下手速,去上拉一个小程序项目中一个有1万多条商品数据的列表。在数据加载到1000多条后,是列表居然出现了白屏。看了一下控制台:

‘Dom limit exceeded’,dom数超出了限制, 不知道微信是出于什么考虑,要限制页面的dom数量。

一.小程序页面限制多少个wxml节点?

写了个小dome做了个测试。 listData的数据结构为:

listData:[
{
isDisplay:true,
itemList:[{
qus:'下面哪位是刘发财女朋友?',
answerA:'刘亦菲',
answerB:'迪丽热巴',
answerC:'斋藤飞鸟',
answerD:'花泽香菜',
}
.......//20条数据
]
}]

页面渲染效果:




{{item.qus}}

A. {{item.answerA}}
B. {{item.answerB}}
C. {{item.answerC}}
D. {{item.answerD}}



2.dome2,删除了不必要的dom嵌套



{{item.qus}}

A. {{item.answerA}}
B. {{item.answerB}}
C. {{item.answerC}}
D. {{item.answerD}}



通过大致计算,一个小程序页面大概可以渲染2万个wxml节点 而小程序官方的性能测评得分条件为少于1000个wxml节点官方链接



二.列表页面优化

1.减少不必要的标签嵌套


由上面的测试dome可知,在不影响代码运行和可读性的前提下,尽量减少标签的嵌套,可以大幅的增加页面数据的列表条数,毕竟公司不是按代码行数发工资的。如果你的列表数据量有限,可以用这种方法来增加列表渲染条数。如果数据量很大,再怎么精简也超过2万的节点,这个方法则不适用。


2.优化setData的使用


图五所示,小程序setDate的性能会受到setData数据量大小和调用频率限制。所以要围绕减少每一次setData数据量大小,降低setData调用频率进行优化。
#####(1)删除冗余字段
后端的同事经常把数据从数据库中取出就直接返回给前端,不经过任何处理,所以会导致数据大量的冗余,很多字段根本用不到,我们需要把这些字段删除,减少setDate的数据大小。
#####(2)setData的进阶用法
通常,我们对data中数据的增删改操作,是把原来的数据取出,处理,然后用setData整体去更新,比如我们列表中使用到的上拉加载更多,需要往listData尾部添加数据:

newList=[{...},{...}];
this.setData({
listData:[...this.data.listData,...newList]
})

这样会导致 setDate的数据量越来越大,页面也越来越卡。

setDate的正确使用姿势

  • setDate修改数据

比如我们要修改数组listData第一个元素的isDisplay属性,我们可以这样操作:

let index=0;
this.setData({
[`listData[${index}].isDisplay`]:false,
})

如果我们想同时修改数组listData中下标从0到9的元素的isDisplay属性,那要如何处理呢?你可能会想到用for循环来执行setData

for(let index=0;index<10;index++){
this.setData({
[`listData[${index}].isDisplay`]:false,
})
}

那么这样就会导致另外一个问题,那就是listData的调用过于频繁,也会导致性能问题,正确的处理方式是先把要修改的数据先收集起来,然后调用setData一次处理完成:

let changeData={};
for(let index=0;index<10;index++){
changeData[[`listData[${index}].isDisplay`]]=false;
}
this.setData(changeData);



这样我们就把数组listData中下标从0到9的元素的isDisplay属性改成了false

  • setDate往数组末尾添加数据

如果只添加一条数据

let newData={...};
this.setData({
[`listData[${this.data.listData.length}]`]:newData
})

如果是添加多条数据

let newData=[{...},{...},{...},{...},{...},{...}];
let changeData={};
let index=this.data.listData.length
newData.forEach((item) => {
changeData['listData[' + (index++) + ']'] = item //赋值,索引递增
})
this.setData(changeData)

三.使用自定义组件

可以把列表的一行或者多行封装到自定义组件里,在列表页使用一个组件,只算一个节点,这样你的列表能渲染的数据可以成倍数的增加。组件内的节点数也是有限制的,但是你可以一层层嵌套组件实现列表的无限加载,如果你不怕麻烦的话

四.使用虚拟列表

经过上面的一系列操作后,列表的性能会得到很大的提升,但是如果数据量实在太大,wxml节点数也会超出限制,导致页面发生错误。我们的处理方法是使用虚拟列表,页面只渲染当前可视区域以及可视区域上下若干条数据的节点,通过isDisplay控制节点的渲染。

  • 可视区域上方:above
  • 可视区域:screen
  • 可视区域下方:below

1.listData数组的结构

使用二维数组,因为如果是一维数组,页面滚动需要用setData设置大量的元素isDispaly属性来控制列表的的渲染。而二维数组可以这可以一次调用setData控制十条,二十条甚至更多的数据的渲染。

listData:[
{
isDisplay:true,
itemList:[{
qus:'下面哪位是刘发财女朋友?',
answerA:'刘亦菲',
answerB:'迪丽热巴',
answerC:'斋藤飞鸟',
answerD:'花泽香菜',
}
.......//二维数组中的条数根据项目实际情况
]
}]

2.必要的参数

data{
itemHeight:4520,//列表第一层dom高度,单位为rpx
itemPxHeight:'',//转化为px高度,因为小程序获取的滚动条高度单位为px
aboveShowIndex:0,//已渲染数据的第一条的Index
belowShowNum:0,//显示区域下方隐藏的条数
oldSrollTop:0,//记录上一次滚动的滚动条高度,判断滚动方向
prepareNum:5,//可视区域上下方要渲染的数量
throttleTime:200,//滚动事件节流的时间,单位ms
}

3.wxml的dom结构






{{item.qus}}

A. {{item.answerA}}
B. {{item.answerB}}
C. {{item.answerC}}
D. {{item.answerD}}




4.获取列表第一层dom的px高度

let query = wx.createSelectorQuery();
query.select('.content').boundingClientRect(rect=>{
let clientWidth = rect.width;
let ratio = 750 / clientWidth;
this.setData({
itemPxHeight:Math.floor(this.data.itemHeight/ratio),
})
}).exec();

5.页面滚动时间节流

function throttle(fn){
let valid = true
return function() {
if(!valid){
return false
}
// 工作时间,执行函数并且在间隔期内把状态位设为无效
valid = false
setTimeout(() => {
fn.call(this,arguments);
valid = true;
}, this.data.throttleTime)
}
}

6.页面滚动事件处理

onPageScroll:throttle(function(e){
let scrollTop=e[0].scrollTop;//滚动条高度
let itemNum=Math.floor(scrollTop/this.data.itemPxHeight);//计算出可视区域的数据Index
let clearindex=itemNum-this.data.prepareNum+1;//滑动后需要渲染数据第一条的index
let oldSrollTop=this.data.oldSrollTop;//滚动前的scrotop,用于判断滚动的方向
let aboveShowIndex=this.data.aboveShowIndex;//获取已渲染数据第一条的index
let listDataLen=this.data.listData.length;
let changeData={}
//向下滚动
if(scrollTop-oldSrollTop>0){
if(clearindex>0){
//滚动后需要变更的条数
for(let i=aboveShowIndex;i changeData[[`listData[${i}].isDisplay`]]=false;
let belowShowIndex=i+2*this.data.prepareNum;
if(i+2*this.data.prepareNum changeData[[`listData[${belowShowIndex}].isDisplay`]]=true;
}
}
}
}else{//向上滚动
if(clearindex>=0){
let changeData={}
for(let i=aboveShowIndex-1;i>=clearindex;i--){
let belowShowIndex=i+2*this.data.prepareNum
if(i+2*this.data.prepareNum<=listDataLen-1){
changeData[[`listData[${belowShowIndex}].isDisplay`]]=false;
}
changeData[[`listData[${i}].isDisplay`]]=true;
}
}else{
if(aboveShowIndex>0){
for(let i=0;i this.setData({
[`listData[${i}].isDisplay`]:true,
})
}
}
}
}
clearindex=clearindex>0?clearindex:0
if(clearindex>=0&&!(clearindex>0&&clearindex==this.data.aboveShowIndex)){
changeData.aboveShowIndex=clearindex;
let belowShowNum=this.data.listData.length-(2*this.data.prepareNum+clearindex)
belowShowNum=belowShowNum>0?belowShowNum:0
if(belowShowNum>=0){
changeData.belowShowNum=belowShowNum
}
this.setData(changeData)
}
this.setData({
oldSrollTop:scrollTop
})
}),

经过上面的处理后,页面的wxml节点数量相对稳定,可能因为可视区域数据的index计算误差,页面渲染的数据有小幅度的浮动,但是已经完全不会超过小程序页面的节点数量的限制。理论上100万条数据的列表也不会有问题,只要你有耐心和精力一直划列表加载这么多数据。

7.待优化事项



  • 列表每一行的高度需要固定,不然会导致可视区域数据的index的计算出现误差

  • 渲染玩列表后往回来列表,如果手速过快,会导致above,below区域的数据渲染不过来,会出现短暂的白屏,白屏问题可以调整 prepareNum, throttleTime两个参数改善,但是不能完全解决(经过测试对比发现,即使不对列表进行任何处理,滑动速度过快也会发生短暂白屏的情况)。

  • 如果列表中有图片,above,below区域重新渲染时,图片虽然以经缓存在本地,不需要重新去服务器请求,但是重新渲染还是需要时间,尤其当你手速特别快时。可以根据上面的思路, isDisplay时只销毁非的节点,这样重新渲染就不需要渲染图片,但是这样节点数还是会增加,不过应该能满足大部分项目需求了,看自己项目怎么取舍。



原文:https://juejin.cn/post/6966904317148299271


收起阅读 »

iOS 使用Moya网络请求

Moya最新版本11.0.2由于前段时间写了这篇文章,最新Moya已更新最新版本,故此也更新了下用法,本人已使用,故特意奉上最新的使用demo供参考。Moya11.0.2DemoMoya简介Moya 是你的 app 中缺失的网络层。不用再去想在哪儿(或者如何)...
继续阅读 »

Moya最新版本11.0.2

由于前段时间写了这篇文章,最新Moya已更新最新版本,故此也更新了下用法,本人已使用,故特意奉上最新的使用demo供参考。
Moya11.0.2Demo

Moya简介

Moya 是你的 app 中缺失的网络层。不用再去想在哪儿(或者如何)安放网络请求,Moya 替你管理。

Moya有几个比较好的特性:

1、编译时检查正确的API端点访问.

2、使你定义不同端点枚举值对应相应的用途更加明晰.

3、提高测试地位从而使单元测试更加容易.

Swift我们用Alamofire来做网络库.而Moya在Alamofire的基础上又封装了一层,如下流程图说明Moya的简单工作流程图:


** Moya**的官方下载地址点我强大的Moya,有具体的使用方法在demo里面有说明。

本文主要介绍一下Moya的用法

1、设置请求头部信息
2、设置超时时间
3、自定义插件
4、自签名证书
注意:以下所出现的NetAPIManager跟官网上demo的** GitHub**是一样类型的文件,都是这个enum实现一个协议TargetType,点进去可以看到TargetType定义了我们发送一个网络请求所需要的东西,什么baseURL,parameter,method等一些计算性属性,我们要做的就是去实现这些东西,当然有带默认值的我们可以不去实现,但是设置头部信息跟超时时间就要修改这些系统默认设置了。

为了看得更加清楚,贴上NetAPIManager文件的内容

//
// NetAPIManager.swift
// NN110
//
// Created by 陈亦海 on 2017/5/12.
// Copyright © 2017年 陈亦海. All rights reserved.
//

import Foundation
import Moya


enum NetAPIManager {
case Show
case upload(bodyData: Data)
case download
case request(isTouch: Bool, body: Dictionary<String, Any>? ,isShow: Bool)
}


extension NetAPIManager: TargetType {
var baseURL: URL {//服务器地址

switch self {
case .request( _, _, _):
return URL(string: "https://www.pmphmall.com")!
default:
return URL(string: "https://httpbin.org")!
}


}

var path: String {//具体某个方法的路径
switch self {
case .Show:
return ""
case .upload(_):
return ""
case .request(_, _, _):
return "/app/json.do"
case .download:
return ""
}
}

var method: Moya.Method {//请求的方法 get或者post之类的
switch self {
case .Show:
return .get
case .request(_, _, _):
return .post
default:
return .post
}
}

var parameters: [String: Any]? {//请求的get post给服务器的参数
switch self {
case .Show:
return nil
case .request(_, _, _):
return ["msg":"H4sIAAAAAAAAA11SSZJFIQi7EqPAEgTvf6TP62W7sMoSQhKSWDrs6ZUKVWogLwYV7RjHFBZJlNlzloN6LVqID4a+puxqRdUKVNLwE1TRcZIC/fjF2rPotuXmb84r1gMXbiASZIZbhQdKEewJlz41znDkujCHuQU3dU7G4/PmVRnwArMLXukBv0J23XVahNO3VX35wlgce6TLUzzgPQJFuHngAczl6VhaNXpmRLxJBlMml6gdLWiXxTdO7I+iEyC7XuTirCQXOk4dotgArgkH/InxVjfNTnE/uY46++hyAiLFuFL4cv1Z8WH5DgB2GnvFXMh5gm53Tr13vqqrEYtcdXfkNsMwKB+9sAQ77grNJmquFWOhfXA/DELlMB0KKFtHOc/ronj1ml+Z7qas82L3VWiCVQ+HEitjTVzoFw8RisFN/jJxBY4awvq427McXqnyrfCsl7oeEU6wYgW9yJtj1lOkx0ELL5Fw4z071NaVzRA9ebxWXkFyothgbB445cpRmTC+//F73r1kOyQ3lTpec12XNDR00nnq5/YmJItW3+w1z27lSOLqgVctrxG4xdL9WVPdkH1tkiZ/pUKBGhADAAA="]
default:
return nil

}
}

var sampleData: Data { //编码转义
return "{}".data(using: String.Encoding.utf8)!
}

var task: Task { //一个请求任务事件

switch self {


case let .upload(data):
return .upload(.multipart([MultipartFormData(provider: .data(data), name: "file", fileName: "gif.gif", mimeType: "image/gif")]))

default:
return .request

}

}

var parameterEncoding: ParameterEncoding {//编码的格式
switch self {
case .request(_, _, _):
return URLEncoding.default
default:
return URLEncoding.default
}

}
//以下两个参数是我自己写,用来控制网络加载的时候是否允许操作,跟是否要显示加载提示,这两个参数在自定义插件的时候会用到
var touch: Bool { //是否可以操作

switch self {
case .request(let isTouch, _, _):
return isTouch
default:
return false
}

}

var show: Bool { //是否显示转圈提示

switch self {
case .request( _, _,let isShow):
return isShow
default:
return false
}

}


}

如何设置Moya请求头部信息

头部信息的设置在开发过程中很重要,如服务器生成的token,用户唯一标识等
我们直接上代码,不说那么多理论的东西,哈哈

// MARK: - 设置请求头部信息
let myEndpointClosure = { (target: NetAPIManager) -> Endpoint<NetAPIManager> in


let url = target.baseURL.appendingPathComponent(target.path).absoluteString
let endpoint = Endpoint<NetAPIManager>(
url: url,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
parameters: target.parameters,
parameterEncoding: target.parameterEncoding
)

//在这里设置你的HTTP头部信息
return endpoint.adding(newHTTPHeaderFields: [
"Content-Type" : "application/x-www-form-urlencoded",
"ECP-COOKIE" : ""
])

}

如何设置请求超时时间

// MARK: - 设置请求超时时间
let requestClosure = { (endpoint: Endpoint<NetAPIManager>, done: @escaping MoyaProvider<NetAPIManager>.RequestResultClosure) in

guard var request = endpoint.urlRequest else { return }

request.timeoutInterval = 30 //设置请求超时时间
done(.success(request))
}

自定义插件

自定义插件必须PluginType协议的两个方法willSend与didReceive

//
// MyNetworkActivityPlugin.swift
// NN110
//
// Created by 陈亦海 on 2017/5/10.
// Copyright © 2017年 CocoaPods. All rights reserved.
//

import Foundation
import Result
import Moya


/// Network activity change notification type.
public enum MyNetworkActivityChangeType {
case began, ended
}

/// Notify a request's network activity changes (request begins or ends).
public final class MyNetworkActivityPlugin: PluginType {



public typealias MyNetworkActivityClosure = (_ change: MyNetworkActivityChangeType, _ target: TargetType) -> Void
let myNetworkActivityClosure: MyNetworkActivityClosure

public init(newNetworkActivityClosure: @escaping MyNetworkActivityClosure) {
self.myNetworkActivityClosure = newNetworkActivityClosure
}

// MARK: Plugin

/// Called by the provider as soon as the request is about to start
public func willSend(_ request: RequestType, target: TargetType) {
myNetworkActivityClosure(.began,target)
}

/// Called by the provider as soon as a response arrives, even if the request is cancelled.
public func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
myNetworkActivityClosure(.ended,target)
}
}

使用自定义插件方法

// MARK: - 自定义的网络提示请求插件
let myNetworkPlugin = MyNetworkActivityPlugin { (state,target) in
if state == .began {
// SwiftSpinner.show("Connecting...")

let api = target as! NetAPIManager
if api.show {
print("我可以在这里写加载提示")
}

if !api.touch {
print("我可以在这里写禁止用户操作,等待请求结束")
}

print("我开始请求\(api.touch)")

UIApplication.shared.isNetworkActivityIndicatorVisible = true
} else {
// SwiftSpinner.show("request finish...")
// SwiftSpinner.hide()
print("我结束请求")
UIApplication.shared.isNetworkActivityIndicatorVisible = false

}
}

自签名证书

在16年的WWDC中,Apple已表示将从2017年1月1日起,所有新提交的App必须强制性应用HTTPS协议来进行网络请求。默认情况下非HTTPS的网络访问是禁止的并且不能再通过简单粗暴的向Info.plist中添加NSAllowsArbitraryLoads
设置绕过ATS(App Transport Security)的限制(否则须在应用审核时进行说明并很可能会被拒)。所以还未进行相应配置的公司需要尽快将升级为HTTPS的事项提上进程了。本文将简述HTTPS及配置数字证书的原理并以配置实例和出现的问题进行说明,希望能对你提供帮助。(比心~)

HTTPS:
简单来说,HTTPS就是HTTP协议上再加一层加密处理的SSL协议,即HTTP安全版。相比HTTP,HTTPS可以保证内容在传输过程中不会被第三方查看、及时发现被第三方篡改的传输内容、防止身份冒充,从而更有效的保证网络数据的安全。
HTTPS客户端与服务器交互过程:
1、 客户端第一次请求时,服务器会返回一个包含公钥的数字证书给客户端;
2、 客户端生成对称加密密钥并用其得到的公钥对其加密后返回给服务器;
3、 服务器使用自己私钥对收到的加密数据解密,得到对称加密密钥并保存;
4、 然后双方通过对称加密的数据进行传输。


数字证书:
在HTTPS客户端与服务器第一次交互时,服务端返回给客户端的数字证书是让客户端验证这个数字证书是不是服务端的,证书所有者是不是该服务器,确保数据由正确的服务端发来,没有被第三方篡改。数字证书可以保证数字证书里的公钥确实是这个证书的所有者(Subject)的,或者证书可以用来确认对方身份。证书由公钥、证书主题(Subject)、数字签名(digital signature)等内容组成。其中数字签名就是证书的防伪标签,目前使用最广泛的SHA-RSA加密。
证书一般分为两种:

1、一种是向权威认证机构购买的证书,服务端使用该种证书时,因为苹果系统内置了其受信任的签名根证书,所以客户端不需额外的配置。为了证书安全,在证书发布机构公布证书时,证书的指纹算法都会加密后再和证书放到一起公布以防止他人伪造数字证书。而证书机构使用自己的私钥对其指纹算法加密,可以用内置在操作系统里的机构签名根证书来解密,以此保证证书的安全。
2、另一种是自己制作的证书,即自签名证书。好处是不需要花钱购2买,但使用这种证书是不会受信任的,所以需要我们在代码中将该证书配置为信任证书.

自签名证书具体实现:

我们在使用自签名证书来实现HTTPS请求时,因为不像机构颁发的证书一样其签名根证书在系统中已经内置了,所以我们需要在App中内置自己服务器的签名根证书来验证数字证书。首先将服务端生成的.cer格式的根证书添加到项目中,注意在添加证书要一定要记得勾选要添加的targets。这里有个地方要注意:苹果的ATS要求服务端必须支持TLS 1.2或以上版本;必须使用支持前向保密的密码;证书必须使用SHA-256或者更好的签名hash算法来签名,如果证书无效,则会导致连接失败。由于我在生成的根证书时签名hash算法低于其要求,在配置完请求时一直报NSURLErrorServerCertificateUntrusted = -1202错误,希望大家可以注意到这一点。

那么如何在Moya中使用自签名的证书来实现HTTPS网络请求呢,请期待下回我专门分享......需要自定义一个Manager管理

综合使用的方法如下

定义一个公用的Moya请求服务对象

let MyAPIProvider = MoyaProvider<NetAPIManager>(endpointClosure: myEndpointClosure,requestClosure: requestClosure, plugins: [NetworkLoggerPlugin(verbose: true, responseDataFormatter: JSONResponseDataFormatter),myNetworkPlugin])

// MARK: -创建一个Moya请求
func sendRequest(_ postDict: Dictionary<String, Any>? = nil,
success:@escaping (Dictionary<String, Any>)->(),
failure:@escaping (MoyaError)->()) -> Cancellable? {

let request = MyAPIProvider.request(.Show) { result in
switch result {
case let .success(moyaResponse):


do {
let any = try moyaResponse.mapJSON()
let data = moyaResponse.data
let statusCode = moyaResponse.statusCode
MyLog("\(data) --- \(statusCode) ----- \(any)")

success(["":""])


} catch {

}



case let .failure(error):

print(error)
failure(error)
}
}

return request
}

取消所有的Moya请求

// MARK: -取消所有请求
func cancelAllRequest() {
// MyAPIProvider.manager.session.invalidateAndCancel() //取消所有请求
MyAPIProvider.manager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
dataTasks.forEach { $0.cancel() }
uploadTasks.forEach { $0.cancel() }
downloadTasks.forEach { $0.cancel() }
}

//let sessionManager = Alamofire.SessionManager.default
//sessionManager.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
// dataTasks.forEach { $0.cancel() }
// uploadTasks.forEach { $0.cancel() }
// downloadTasks.forEach { $0.cancel() }
//}

}

转自:https://www.jianshu.com/p/38fbc22a1e2b

收起阅读 »

一行代码完成http请求,bitmap异步加载,数据库增删查改!

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

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

##Welike带来了哪些特征?

WelikeAndroid目前包含五个大模块:

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

使用WelikeAndroid需要以下权限:

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

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

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

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

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

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

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

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

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

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

使用Welike做屏幕适配:

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

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

WelikeHttp入门:

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

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

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

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

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

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

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

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

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

WelikeBitmap入门:

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

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

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

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

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

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

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

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

##WelikeDAO入门:

  • 首先写一个Bean.

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

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

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

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

##十秒钟学会WelikeActivity

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

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

###关于@JoinView的细节:

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

一个简洁而优雅的Android原生UI框架,解放你的双手!

一个简洁而又优雅的Android原生UI框架,解放你的双手!还不赶紧点击使用说明文档,体验一下吧!涵盖绝大部分的UI组件:TextView、Button、EditText、ImageView、Spinner、Picker、Dialog、PopupWindow、...
继续阅读 »

一个简洁而又优雅的Android原生UI框架,解放你的双手!还不赶紧点击使用说明文档,体验一下吧!

涵盖绝大部分的UI组件:TextView、Button、EditText、ImageView、Spinner、Picker、Dialog、PopupWindow、ProgressBar、LoadingView、StateLayout、FlowLayout、Switch、Actionbar、TabBar、Banner、GuideView、BadgeView、MarqueeView、WebView、SearchView等一系列的组件和丰富多彩的样式主题。

在提issue前,请先阅读【提问的智慧】,并严格按照issue模板进行填写,节约大家的时间。

在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!

在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!

在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!

X系列库快速集成

为了方便大家快速集成X系列框架库,我提供了一个空壳模版供大家参考使用: https://github.com/xuexiangjys/TemplateAppProject

除此之外,我还特别制作了几期视频教程供大家学习参考.


特征

  • 简洁优雅,尽可能少得引用资源文件的数量,项目库整体大小不足1M(打包后大约644k)。

  • 组件丰富,提供了绝大多数我们在开发者常用的功能组件。

  • 使用简单,为方便快速开发,提高开发效率,对api进行了优化,提供一键式接入。

  • 样式统一,框架提供了一系列统一的样式,使UI整体看上去美观和谐。

  • 兼容性高,框架还提供了3种不同尺寸设备的样式(4.5英寸、7英寸和10英寸),并且最低兼容到Android 17, 让UI兼容性更强。

  • 扩展性强,各组件提供了丰富的属性和样式API,可以通过设置不同的样式属性,构建不同风格的UI。


如何使用

在决定使用XUI前,你必须明确的一点是,此框架给出的是一整套UI的整体解决方案,如果你只是想使用其中的几个控件,那大可不必引入如此庞大的一个UI库,Github上会有更好的组件库。如果你是想拥有一套可以定制的、统一的UI整体解决方案的话,那么你就继续往下看吧!

添加Gradle依赖

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

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

2.然后在dependencies添加:

dependencies {
...
//androidx项目
implementation 'com.github.xuexiangjys:XUI:1.1.7'

implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
}

【注意】如果你的项目目前还未使用androidx,请使用如下配置:

dependencies {
...
//support项目
implementation 'com.github.xuexiangjys:XUI:1.0.9-support'

implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support:recyclerview-v7:28.0.0'
implementation 'com.android.support:design:28.0.0'
implementation 'com.github.bumptech.glide:glide:4.8.0'
}

初始化XUI设置

1.调整应用的基础主题(必须)

必须设置应用的基础主题,否则组件将无法正常使用!必须保证所有用到XUI组件的窗口的主题都为XUITheme的子类,这非常重要!!!

基础主题类型:

  • 大平板(10英寸, 240dpi, 1920*1200):XUITheme.Tablet.Big

  • 小平板(7英寸, 320dpi, 1920*1200):XUITheme.Tablet.Small

  • 手机(4.5英寸, 320dpi, 720*1280):XUITheme.Phone

<style name="AppTheme" parent="XUITheme.Phone">

 <!-- 自定义自己的主题样式 -->

<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>

</style>

当然也可以在Activity刚开始时调用如下代码动态设置主题

@Override
protected void onCreate(Bundle savedInstanceState) {
XUI.initTheme(this);
super.onCreate(savedInstanceState);
...
}

2.调整字体库(对字体无要求的可省略)

(1)设置你需要修改的字体库路径(assets下)

//设置默认字体为华文行楷,这里写你的字体库
XUI.getInstance().initFontStyle("fonts/hwxk.ttf");

(2)在项目的基础Activity中加入如下代码注入字体.

注意:1.1.4版本之后使用如下设置注入

@Override
protected void attachBaseContext(Context newBase) {
//注入字体
super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase));
}

注意:1.1.3版本及之前的版本使用如下设置注入

@Override
protected void attachBaseContext(Context newBase) {
//注入字体
super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));
}

混淆配置

-keep class com.xuexiang.xui.widget.edittext.materialedittext.** { *; }


代码下载:XUI.zip

收起阅读 »

Android支付组件

接入指南:1、导入libSdk 依赖工程2、配置 AndroidManifest文件(配置内容,请看下文,此处支持 两种方式来配置 第三方支付 参数【①可以在AndroidManifest 对应的meta-data 配置;②支持在代码中配置;选其一即可】)2....
继续阅读 »



接入指南:

1、导入libSdk 依赖工程

2、配置 AndroidManifest文件(配置内容,请看下文,此处支持 两种方式来配置 第三方支付 参数【①可以在AndroidManifest 对应的meta-data 配置;②支持在代码中配置;选其一即可】)

  • 2.1 拷贝assets/data.bin 文件到 项目中

3、项目中实际使用支付:具体使用看下文 ---> 调起支付 。


请配置正确的参数,否则支付宝和微信 会出现无法调起的情况。

//配置 AndroidManifest

    <activity
android:name="net.lbh.pay.PaymentActivity"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />

<activity-alias
android:name=".wxapi.WXPayEntryActivity"
android:exported="true"
android:targetActivity="net.lbh.pay.PaymentActivity" />
<!-- 微信支付 end -->


<!-- 支付宝 begin -->
<activity
android:name="com.alipay.sdk.app.H5PayActivity"
android:configChanges="orientation|keyboardHidden|navigation"
android:exported="false"
android:screenOrientation="behind"
android:windowSoftInputMode="adjustResize|stateHidden" />
<!-- 支付宝 end -->


<!-- 银联支付 begin -->

<activity
android:name="com.unionpay.uppay.PayActivity"
android:configChanges="orientation|keyboardHidden"
android:excludeFromRecents="true"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />

<activity
android:name="com.unionpay.UPPayWapActivity"
android:configChanges="orientation|keyboardHidden"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />

<!-- 银联支付 end -->


<!-- 微信 广播 start -->
<receiver android:name="net.lbh.pay.wxpay.AppRegister" >
<intent-filter>
<action android:name="com.tencent.mm.plugin.openapi.Intent.ACTION_REFRESH_WXAPP" />
</intent-filter>
</receiver>
<!-- 微信 广播 end -->


<!-- 微信支付 参数 appid, 需要替换成你自己的 -->
<meta-data
android:name="WXPAY_APP_ID"
android:value="替换成自己的 app id" >
</meta-data>
<meta-data
android:name="WXPAY_MCH_ID"
android:value="替换成自己的 MCH_ID" >
</meta-data>
<meta-data
android:name="WXPAY_API_KEY"
android:value="替换成自己的 api key" >
</meta-data>
<!-- 微信支付 参数 end 需要替换成你自己的 -->


<!-- 支付宝 参数 appid, 需要替换成你自己的 --> //如果是 超过10位数字,要在前边加 ,Eg: \0223987667567887653
<meta-data
android:name="ALIPAY_PARTNER_ID"
android:value="替换成自己的 partenr id" >
</meta-data>
<meta-data
android:name="ALIPAY_SELLER_ID"
android:value="替换成自己的 seller id" >
</meta-data>

<meta-data
android:name="ALIPAY_PRIVATE_KEY"
android:value="替换成自己的 private key" >
</meta-data>

<meta-data
android:name="ALIPAY_PUBLIC_KEY"
android:value="替换成自己的 public key" >
</meta-data>
<!-- 支付宝 参数 end 需要替换成你自己的 -->

// 初始化支付组件

	PayAgent payAgent = PayAgent.getInstance();
payAgent.setDebug(true);

// 代码初始化 参数, 此处针对场景,所有参数有 自己app server保管的时候,动态的支付配置下发参数
payAgent.initAliPayKeys(partnerId, sellerId, privateKey, publicKey);
payAgent.initWxPayKeys(appId, mchId, appKey)
// 初始化 银联支付 所需的 验签 参数
//payAgent.initUpPayKeys(PublicKeyPMModulus, publicExponent, PublicKeyProductModulus);
// 代码动态初始化为 可选

payAgent.initPay(this);

// 调起支付

    PayAgent.getInstance().onPay(payType, this, payInfo,
new OnPayListener() {

@Override
public void onStartPay() {

progressDialog.setTitle("加载中。。。");
progressDialog.show();
}

@Override
public void onPaySuccess() {

Toast.makeText(MainActivity.this,"支付成功!", 1).show();

if (null != progressDialog) {
progressDialog.dismiss();
}

}

@Override
public void onPayFail(String code, String msg) {
Toast.makeText(MainActivity.this,
"code:" + code + "msg:" + msg, 1).show();
Log.e(getClass().getName(), "code:" + code + "msg:" + msg);

if (null != progressDialog) {
progressDialog.dismiss();
}
}
});

支付参数说明:

PayType: 支付的支付方式,目前支持:

  • 1、PayAgent.PayType.WECHATPAY(微信支付);
  • 2、PayAgent.PayType.ALIPAY(支付宝);
  • 3、PayAgent.PayType.UPPAY(银联)。

Activity: 调起支付的 Activity

PayInfo:

/** 商品名称*/
private String subject;

/** 商品详细信息 商品的标题/交易标题/订单标题/订单关键字等。该参数最长为128个汉字*/
private String body;

/** 商品价格*/
private String price;

/** 商品订单号*/
private String orderNo;

/** 支付通知地址*/
private String notifyUrl;

OnPayListener: 支付监听器:

  • onStartPay() 开始支付,可以在此做 支付前准备提示
  • onPaySuccess(); 支付成功
  • onPayFail(String code, String msg); 支付失败,code和msg 均为第三方原样返回

配置第三方参数说明:

  • 1、支付宝:

注意:

  • 1、支付宝支付,调用支付宝时, 所有参数为必须向
  • 2、微信支付,orderNo 为必须项
  • 3、银联支付时,orderNo 为必须项 -4、关于支付后,通知回调,只有支付宝是 在客户端手动设置,其余都是在 后台配置。

注意事项:

  • 1、当测试时,可以使用Debug模式,开启方式为: PayAgent payAgent = PayAgent.getInstance(); payAgent.setDebug(true);

  • 2、调试模式(非正式环境,目前只有 银联): PayAgent payAgent = PayAgent.getInstance(); payAgent.setOnlieMode(false);

版本說明:

  • 1、银联支付:3.3.2
  • 2、支付宝:
  • 3、微信:

更新日志:

  • 2016.04.15更新:

  • 1、2016.4.14 银联更新sdk,更新银联支付控件为3.3.3

  • 2、去除银联客户端验签;添加银联需要权限(nfc等)

  • 1、更新银联支付控件为3.3.2

  • 2、添加调试模式(非正式环境模式、主要正对银联支付)

payAgent.setOnlieMode(false);

  • 3、添加银联 验证签名,初始化签名参数
  • 4、修改Demo ,测试 Demo能正常运行。

其他说明:



收起阅读 »

日志管理工具 - CocoaLumberjack

CocoaLumberjackCocoaLumberjack是适用于 macOS、iOS、tvOS 和 watchOS 的快速简单但功能强大且灵活的日志记录框架。首先,通过CocoaPods、Carthage、Swift Package Manager或手动安...
继续阅读 »

CocoaLumberjack

CocoaLumberjack是适用于 macOS、iOS、tvOS 和 watchOS 的快速简单但功能强大且灵活的日志记录框架。

首先,通过CocoaPodsCarthageSwift Package Manager或手动安装 CocoaLumberjack 然后使用DDOSLoggeriOS 10 及更高版本,或DDTTYLoggerDDASLLogger早期版本开始记录消息。

CocoaPods

platform :ios, '9.0'

target 'SampleTarget' do
use_frameworks!
pod 'CocoaLumberjack/Swift'
end


platform :ios, '9.0'

target 'SampleTarget' do
pod 'CocoaLumberjack'
end

Carthage

github "CocoaLumberjack/CocoaLumberjack"

用法

如果您使用 Lumberjack 作为框架,则可以@import CocoaLumberjack;除此以外,#import
[DDLog addLogger: [DDOSLogger sharedInstance ]]; //使用 os_log

DDFileLogger *fileLogger = [[DDFileLogger
alloc ] init ]; //文件记录器
fileLogger.rollingFrequency =
60 * 60 * 24 ; // 24 小时滚动
fileLogger.logFileManager.maximumNumberOfLogFiles =
7 ;
[DDLog
addLogger: fileLogger];

...


DDLogVerbose ( @"详细" );
DDLogDebug ( @"调试" );
DDLogInfo ( @"信息" );
DDLogWarn ( @"警告" );
DDLogError ( @"错误" );

Objective-C ARC 语义问题

将 Lumberjack 集成到现有的 Objective-C 中时,可能会遇到Multiple methods named 'tag' found with mismatched result, parameter type or attributes构建错误。

#define DD_LEGACY_MESSAGE_TAG 0在导入 CocoaLumberjack 之前添加或添加#define DD_LEGACY_MESSAGE_TAG 0或添加-DDD_LEGACY_MESSAGE_TAG=0Xcode 项目中的其他 C 标志OTHER_CFLAGS

快速日志后端

CocoaLumberjack 还附带了swift-log的后端实现只需将 CocoaLumberjack 作为依赖项添加到您的 SPM 目标(见上文),并将CocoaLumberjackSwiftLogBackend产品作为依赖项添加到您的目标。

然后,您可以将DDLogHandler其用作 swift-log 的后端,它将所有消息转发到 CocoaLumberjack 的DDLog您仍将通过 配置您想要的记录器和日志格式化程序DDLog,但将使用Loggerswift-log完成写入日志消息

在您自己的日志格式化程序中,您可以使用swiftLogInfoon 属性DDLogMessage来检索通过 swift-log 记录的消息的详细信息。


在大多数情况下,它比 NSLog 快一个数量级。

当您的应用程序启动时,只需一行代码即可配置 lumberjack。然后只需将您的 NSLog 语句替换为 DDLog 语句即可。(而且 DDLog 宏的格式和语法与 NSLog 完全相同,因此非常简单。)

一个日志语句可以发送到多个记录器,这意味着您可以同时记录到一个文件和控制台。想要更多?创建您自己的记录器(这很容易)并通过网络发送您的日志语句。或者到数据库或分布式文件系统。天空才是极限。

根据需要配置您的日志记录。更改每个文件的日志级别(非常适合调试)。更改每个记录器的日志级别(详细控制台,但简洁的日志文件)。更改每个 xcode 配置的日志级别(详细调试,但简洁发布)。将您的日志语句从发布版本中编译出来。为您的应用程序自定义日志级别的数量。添加您自己的细粒度日志记录。在运行时动态更改日志级别。选择您希望日志文件滚动的方式和时间。将您的日志文件上传到中央服务器。压缩归档日志文件以节省磁盘空间...


在以下情况下,此框架适合您:

  • 您正在寻找一种方法来追踪该领域不断出现的无法重现的错误。
  • 您对 iPhone 上超短的控制台日志感到沮丧。
  • 您希望将您的应用程序在支持和稳定性方面提升到一个新的水平。
  • 您正在为您的应用程序(Mac 或 iPhone)寻找企业级日志记录解决方案。

要求

当前版本的 Lumberjack 要求:

  • Xcode 12 或更高版本
  • Swift 5.3 或更高版本
  • iOS 9 或更高版本
  • macOS 10.10 或更高版本
  • watchOS 3 或更高版本
  • tvOS 9 或更高版本



收起阅读 »

视图添加闪烁效果的简单方法 - Shimmer

ShimmerShimmer 是一种向应用程序中的任何视图添加闪烁效果的简单方法。它作为一个不显眼的加载指示器很有用。Shimmer 最初是为了在Paper 中显示加载状态而开发的。用法要使用 Shimmer,请创建一个FBShimmeringView或FBS...
继续阅读 »

Shimmer

Shimmer 是一种向应用程序中的任何视图添加闪烁效果的简单方法。它作为一个不显眼的加载指示器很有用。

Shimmer 最初是为了在Paper 中显示加载状态而开发的

用法

要使用 Shimmer,请创建一个FBShimmeringViewFBShimmeringLayer并添加您的内容。要开始闪烁,请将shimmering属性设置YES

使标签闪烁的示例:

FBShimmeringView *shimmeringView = [[FBShimmeringView alloc ] initWithFrame: self .view.bounds];
[
self .view addSubview: shimmeringView];

UILabel *loadingLabel = [[UILabel
alloc ] initWithFrame: shimmeringView.bounds];
loadingLabel.textAlignment = NSTextAlignmentCenter;

loadingLabel.text = NSLocalizedString(
@" Shimmer " , nil );
shimmeringView.contentView = loadingLabel;


//开始闪烁。
shimmeringView.shimmering =
YES ;

还有一个示例项目。在示例中,您可以水平和垂直滑动以尝试各种闪烁参数,或点击以开始或停止闪烁。(要在本地构建示例,您需要打开FBShimmering.xcworkpace而不是.xcodeproj.)

安装

有两种选择:

  1. 微光ShimmerCocoapods 中可用
  2. 手动将文件添加到您的 Xcode 项目中。稍微简单一点,但更新也是手动的。

Shimmer 需要 iOS 6 或更高版本。


这个怎么运作

Shimmer 使用该-[CALayer mask]属性来启用闪烁,类似于 John Harper 2009 年 WWDC 演讲中所描述的内容(不幸的是不再在线)。Shimmer 使用 CoreAnimation 的计时功能在启动和停止微光时平滑过渡“节拍”。


demo及常见问题:https://github.com/facebookarchive/Shimmer

源码下载:Shimmer-master.zip



收起阅读 »

还不会用coil,你就out了

Coil是Android平台上又一个开源的图片加载库,尽管Android平台已经有诸如Picasso,Glide以及Fresco等非常成熟且优秀的图片加载库了,但Coil最主要的特色就是融合了当下Android开发界最主流的技术和趋势,采用Kotlin为开发语...
继续阅读 »

Coil是Android平台上又一个开源的图片加载库,尽管Android平台已经有诸如Picasso,Glide以及Fresco等非常成熟且优秀的图片加载库了,但Coil最主要的特色就是融合了当下Android开发界最主流的技术和趋势,采用Kotlin为开发语言,将协程、OKHttp、OKIO和AndroidX作为一等公民,以期打造成一个更加轻快、现代化的图片加载库。具体而言包含以下几个方面:



  • 发挥Kotlin的语言特性,利用扩展函数、内联、lambda参数以及密封类来创建简单优雅的API。

  • 利用了Kotlin协程强大的 可取消的非阻塞式异步编程和对线程最大化利用的特性。

  • 使用现代化的依赖库:OKHttp、OKIO基本上已经是目前大部分app的事实“标准”库,它们强大的特性让Coil避免了重复实现磁盘缓存和缓冲流;类似的,AndroidX-LifeCycle也是官方推荐的,Coil目前是唯一一个对其支持的图片加载库。

  • 轻量:Coil项目的代码量几乎只有Glide的1/8,更是远远小于Fresco;并且对APK仅增加了大约1500个方法(对于那些已经依赖的OKHttp和协程的app来说),和Picasso相当并显著低于Glide和Fresco。

  • 支持扩展:Coil的image-pipline主要由 Mappers , Fetchers , 和 Decoders 三个类组成,可以方便地用于自定义:扩展或覆盖默认行为,或增加对新的文件类型的支持。

  • 测试友好化:Coil的基础服务类是 ClassLoader ,它是一个接口,可以方便地编写对应的实现类进行测试;并且Coil同时提供了单例和非单例对象来支持依赖注入。

  • 没有annotation processing:annotation processing一般会降低编译速度,Coil通过Kotlin扩展函数来避免。


Coil目前支持其它图片加载库所包含的所有功能,除此之外它还有一个独特的特性:动态采样(Dynamic image sampling),简而言之就是可以在内存中只缓存了一个低质量的图片而此时需要显示同一个高质量的图片时,Coil可以先把低质量的图片作为 ImageView 的 placeHolder 并同时去磁盘缓存中读取对应的高质量图片最后以“渐进式”的方式替换并最终显示到视图中,例如最常见的从图片列表到预览大图的场景。
以上就是Coil目前的大致介绍,下面我们对Coil的API进行一个简单的使用预览和介绍。


API预览



// 加载一个基本的url(利用了扩展函数,对target无任何侵入)
imageView.load("https://www.website.com/image.jpg")

// Coil 支持加载 urls, uris, resources, drawables, bitmaps, files 等等
imageView.load(R.drawable.image)
imageView.load(File("/path/to/image.jpg"))
imageView.load(Uri.parse("content://com.android.externalstorage/image.jpg"))

// Requests的配置项可以通过load的lambda参数方式实现
imageView.load("https://www.website.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.image)
transformations(CircleCropTransformation())
}

// 自定义targets,包含开始、成功和失败的回调
Coil.load(context, "https://www.website.com/image.jpg") {
target { drawable ->
// Handle the successful result.
}
}

// 通过使用挂起函数get来直接获取图片对象
val drawable = Coil.get("https://www.website.com/image.jpg")

github地址:https://github.com/coil-kt/coil

下载地址:main.zip

收起阅读 »

最快的图像加载库-FastImageCache

FastImageCache快速图像缓存是一种在 iOS 应用程序中存储和检索图像的高效、持久且最重要的快速方式。任何良好的 iOS 应用程序的用户体验的一部分是快速、平滑的滚动,而快速图像缓存有助于使这变得更容易。对于像Path这样的图形丰富的应用程序,性能...
继续阅读 »

FastImageCache

快速图像缓存是一种在 iOS 应用程序中存储和检索图像的高效、持久且最重要的快速方式。任何良好的 iOS 应用程序的用户体验的一部分是快速、平滑的滚动,而快速图像缓存有助于使这变得更容易。

对于像Path这样的图形丰富的应用程序,性能的一个重大负担是图像加载。从磁盘加载单个图像的传统方法太慢了,尤其是在滚动时。Fast Image Cache 就是专门为解决这个问题而创建的。

快速图像缓存的作用

  • 将相似大小和样式的图像存储在一起
  • 将图像数据保存到磁盘
  • 比传统方法更快地将图像返回给用户
  • 根据使用情况自动管理缓存过期
  • 利用基于模型的方法来存储和检索图像
  • 允许在将图像存储到缓存之前按模型处理图像


事实证明,从压缩的磁盘图像数据到用户可以实际看到的渲染核心动画层的过程非常昂贵。随着要显示的图像数量的增加,这种成本很容易导致帧速率显着下降。可滚动视图进一步加剧了这种情况,因为内容可以快速变化,需要快速处理时间才能保持 60FPS 的流畅。1

考虑从磁盘加载图像并将其显示在屏幕上时发生的工作流程:

  1. +[UIImage imageWithContentsOfFile:]使用Image I/OCGImageRef从内存映射数据创建一个此时,图像还没有被解码。
  2. 返回的图像被分配给一个UIImageView.
  3. 隐式CATransaction捕获这些层树修改。
  4. 在主运行循环的下一次迭代中,Core Animation 提交隐式事务,这可能涉及创建已设置为图层内容的任何图像的副本。根据图像,复制它涉及以下部分或全部步骤:2
    1. 缓冲区被分配用于管理文件 IO 和解压操作。
    2. 文件数据从磁盘读取到内存中。
    3. 压缩的图像数据被解码为其未压缩的位图形式,这通常是一个非常占用 CPU 的操作。3
    4. 然后 Core Animation 使用未压缩的位图数据来渲染图层。

这些成本很容易累积并扼杀感知的应用程序性能。特别是在滚动时,给用户呈现的用户体验不尽如人意,与 iOS 的整体体验不符。


解决方案

快速图像缓存使用各种技术最大限度地减少(或完全避免)上述大部分工作:

映射内存

快速图像缓存工作原理的核心是图像表。图像表类似于精灵表,通常用于 2D 游戏。图像表将相同尺寸的图像打包到一个文件中。该文件只打开一次,只要应用程序保留在内存中,就保持打开状态以供读取和写入。

图像表使用mmap系统调用将文件数据直接映射到内存中。没有memcpy发生。这个系统调用只是在磁盘上的数据和内存区域之间创建一个映射。

当请求图像缓存返回特定图像时,图像表(以恒定时间)在它维护的文件中找到所需图像数据的位置。该文件数据区域被映射到内存中,并创建一个新CGImageRef的后备存储映射的文件数据。

当返回的CGImageRef(包装成 a UIImage)准备好被绘制到屏幕上时,iOS 的虚拟内存系统页面中的实际文件数据。这是使用映射内存的另一个好处;VM 系统会自动为我们处理内存管理。此外,映射内存“不计入”应用程序的实际内存使用量。

以类似的方式,当图像数据被存储在图像表中时,会创建一个内存映射位图上下文。与原始图像一起,此上下文被传递到图像表的相应实体对象。该对象负责将图像绘制到当前上下文中,可选地进一步配置上下文(例如,将上下文裁剪为圆角矩形)或进行任何额外的绘制(例如,在原始图像上绘制叠加图像)。mmap将绘制的图像数据编组到磁盘,因此不会在内存中分配图像缓冲区。

未压缩的图像数据

为了避免昂贵的图像解压缩操作,图像表将未压缩的图像数据存储在它们的文件中。如果源图像被压缩,则必须首先解压缩图像表才能使用它。这是一次性成本。此外,可以利用图像格式系列为一组相似的图像格式只执行一次这种解压缩。

然而,这种方法有明显的后果。未压缩的图像数据需要更多的磁盘空间,压缩和未压缩文件大小之间的差异可能很大,尤其是对于 JPEG 等图像格式。出于这个原因,快速图像缓存最适用于较小的图像,尽管没有强制执行此操作的 API 限制。

字节对齐

对于高性能滚动,Core Animation 能够使用图像而无需首先创建副本是至关重要的。Core Animation 创建图像副本的原因之一是图像底层CGImageRef正确对齐的每行字节值必须是 的倍数8 pixels × bytes per pixel对于典型的 ARGB 图像,对齐的每行字节值是 64 的倍数。每个图像表都配置为从一开始就为 Core Animation 始终正确地对每个图像进行字节对齐。因此,当从图像表中检索图像时,它们已经处于 Core Animation 可以直接使用的形式,而无需创建副本。

为了便于项目集成,Fast Image Cache 可作为CocoaPod 使用

手动

创建图像格式

每个图像格式对应一个图像缓存将使用的图像表。可以使用相同的源图像来渲染它们存储在图像表中的图像的图像格式应该属于相同的图像格式系列有关如何确定适当的最大计数的更多信息,请参阅图像表大小

静态 NSString *XXImageFormatNameUserThumbnailSmall = @" com.mycompany.myapp.XXImageFormatNameUserThumbnailSmall " ;
静态 NSString *XXImageFormatNameUserThumbnailMedium = @" com.mycompany.myapp.XXImageFormatNameUserThumbnailMedium " ;
静态 NSString *XXImageFormatFamilyUserThumbnails = @" com.mycompany.myapp.XXImageFormatFamilyUserThumbnails " ;

FICImageFormat *smallUserThumbnailImageFormat = [[FICImageFormat
alloc ] init ];
smallUserThumbnailImageFormat.name = XXImageFormatNameUserThumbnailSmall;

smallUserThumbnailImageFormat.family = XXImageFormatFamilyUserThumbnails;

smallUserThumbnailImageFormat.style = FICImageFormatStyle16BitBGR;

smallUserThumbnailImageFormat.imageSize = CGSizeMake(
50 , 50 );
smallUserThumbnailImageFormat.maximumCount =
250 ;
smallUserThumbnailImageFormat.devices = FICImageFormatDevicePhone;

smallUserThumbnailImageFormat.protectionMode = FICImageFormatProtectionModeNone;


FICImageFormat *mediumUserThumbnailImageFormat = [[FICImageFormat
alloc ] init ];
mediumUserThumbnailImageFormat.name = XXImageFormatNameUserThumbnailMedium;

mediumUserThumbnailImageFormat.family = XXImageFormatFamilyUserThumbnails;

mediumUserThumbnailImageFormat.style = FICImageFormatStyle32BitBGRA;

mediumUserThumbnailImageFormat.imageSize = CGSizeMake(
100 , 100 );
mediumUserThumbnailImageFormat.maximumCount =
250 ;
mediumUserThumbnailImageFormat.devices = FICImageFormatDevicePhone;

mediumUserThumbnailImageFormat.protectionMode = FICImageFormatProtectionModeNone;


NSArray *imageFormats = @[smallUserThumbnailImageFormat, mediumUserThumbnailImageFormat];

配置图像缓存

一旦定义了一种或多种图像格式,就需要将它们分配给图像缓存。除了分配图像缓存的委托之外,没有其他可以在图像缓存本身上配置的内容。

FICImageCache *sharedImageCache = [FICImageCache sharedImageCache ];
sharedImageCache.delegate = self;

sharedImageCache.formats = imageFormats;


实体是符合FICEntity协议的对象实体唯一标识图像表中的条目,并且它们还负责绘制它们希望存储在图像缓存中的图像。已经定义了模型对象(可能由 Core Data 管理)的应用程序通常是合适的候选实体。

@interface  XXUser : NSObject 

@property ( nonatomic , assign , getter = isActive) BOOL active;
@property ( nonatomic , copy ) NSString *userID;
@属性非原子副本NSURL * userPhotoURL;

@结尾

这是该FICEntity协议的示例实现

- ( NSString *)UUID {
CFUUIDBytes UUIDBytes = FICUUIDBytesFromMD5HashOfString (_userID);
NSString *UUID = FICStringWithUUIDBytes (UUIDBytes);

返回UUID;
}


- (
NSString *)sourceImageUUID {
CFUUIDBytes sourceImageUUIDBytes = FICUUIDBytesFromMD5HashOfString ([_userPhotoURL absoluteString ]);
NSString *sourceImageUUID = FICStringWithUUIDBytes (sourceImageUUIDBytes);

返回sourceImageUUID;
}


- (
NSURL *)sourceImageURLWithFormatName:( NSString *)formatName {
return _sourceImageURL;
}


- (FICEntityImageDrawingBlock)drawingBlockForImage:(UIImage *)image withFormatName:(
NSString *)formatName {
FICEntityImageDrawingBlockdrawingBlock = ^(
CGContextRef context, CGSize contextSize) {
CGRect contextBounds = CGRectZero ;
上下文边界。
大小= 上下文大小
CGContextClearRect(上下文,contextBounds);

//剪辑中等缩略图,使它们具有圆角
if ([formatName isEqualToString: XXImageFormatNameUserThumbnailMedium]) {
UIBezierPath clippingPath = [
self _clippingPath ];
[剪辑
路径添加剪辑];
}


UIGraphicsPushContext(上下文);
[图像
drawInRect: contextBounds];
UIGraphicsPopContext ();
};


返回绘图块;
}

理想情况下,实体的UUID永远不应该改变。这就是为什么在应用程序使用从 API 检索的资源的情况下,它与模型对象的服务器生成的 ID 很好地对应。

一个实体的可以改变。例如,如果用户更新了他们的个人资料照片,则该照片的 URL 也应更改。保持不变,标识相同的用户,但改变了个人资料照片网址将表明,有一个新的源图像。sourceImageUUID UUID

注意:通常,最好对用于定义UUID和 的任何标识符进行哈希处理sourceImageUUIDFast Image Cache 提供了实用功能来执行此操作。由于散列可能很昂贵,因此建议仅计算一次散列(或仅在标识符更改时)并存储在实例变量中。

当要求图像缓存为特定实体和格式名称提供图像时,该实体负责提供 URL。URL 甚至不需要指向实际资源——例如,URL 可能由自定义 URL 方案构成——但它必须是一个有效的 URL。

图像缓存仅使用这些 URL 来跟踪哪些图像请求已经在进行中;正确处理对同一图像的图像缓存的多个请求,而不会浪费任何精力。选择使用 URL 作为键控图像缓存请求的基础实际上补充了许多实际应用程序设计,其中图像资源(而不是图像本身)的 URL 包含在服务器提供的模型数据中。

注意:快速图像缓存不提供任何网络请求机制。这是图像缓存委托的责任。

最后,一旦源图像可用,实体就会被要求提供一个绘图块。将存储最终图像的图像表设置文件映射位图上下文并调用实体的绘图块。这使得每个实体可以方便地决定如何处理特定图像格式的源图像。

从图像缓存中请求图像

快速图像缓存在 Cocoa 常见的按需、延迟加载设计模式下工作。

XXUser *user = [self _currentUser];
NSString *formatName = XXImageFormatNameUserThumbnailSmall;
FICImageCacheCompletionBlock completionBlock = ^(id <FICEntity> entity, NSString *formatName, UIImage *image) {
_imageView.image = image;
[_imageView.layer addAnimation:[CATransition animation] forKey:kCATransition];
};

BOOL imageExists = [sharedImageCache retrieveImageForEntity:user withFormatName:formatName completionBlock:completionBlock];

if (imageExists == NO) {
_imageView.image = [self _userPlaceholderImage];
}

统计数据

以下统计数据是从演示应用程序的运行中测得的:

方法滚动性能磁盘使用情况RPRVT 1
传统的~35FPS568KB2.40MB1.06MB+1.34MB
快速图像缓存~59FPS2.2MB1.15MB1.06MB+0.09MB


demo下载及常见问题:https://github.com/path/FastImageCache

源码下载:FastImageCache-master.zip


收起阅读 »

项目想美观么?试试它吧!!自定义加载视图:mkloader

美丽流畅的自定义加载视图 使用<com.tuyenmonkey.mkloader.MKLoader android:layout_width="wrap_content" android:layout_heigh...
继续阅读 »

美丽流畅的自定义加载视图



使用

<com.tuyenmonkey.mkloader.MKLoader
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:mk_type="<loading_type>" (Optional. Default is ClassicSpinner)
app:mk_color="<color>" (Optional. Default is #ffffff)
/>

类型


  • Sharingan
  • TwinFishesSpinner
  • ClassicSpinner
  • LineSpinner
  • FishSpinner
  • PhoneWave
  • ThreePulse
  • FourPulse
  • FivePulse
  • Worm
  • Whirlpool
  • Radar

安装

Gradle


dependencies {
compile 'com.tuyenmonkey:mkloader:1.4.0'
}

github地址:https://github.com/nntuyen/mkloader


下载地址:master.zip


收起阅读 »

iOS应用程序瘦身的静态库解决方案

为什么要给程序瘦身?随着应用程序的功能越来越多,实现越来越复杂,第三方库的引入,UI体验的优化等众多因素程序中的代码量成倍的增长,从而导致应用程序包的体积越来越大。当程序体积变大后不仅会出现编译流程变慢,而且还会出现运行性能问题,会增加应用下载时长和消耗用户的...
继续阅读 »

为什么要给程序瘦身?

随着应用程序的功能越来越多,实现越来越复杂,第三方库的引入,UI体验的优化等众多因素程序中的代码量成倍的增长,从而导致应用程序包的体积越来越大。当程序体积变大后不仅会出现编译流程变慢,而且还会出现运行性能问题,会增加应用下载时长和消耗用户的移动网络流量等等。因此在这些众多的问题下需要对应用进行瘦身处理。

一个应用程序由众多资源文件和可执行程序文件组成,资源文件的优化不在本文探讨范围。本文主要讨论对可执行程序代码瘦身的方法。

对可执行程序代码瘦身主要就是想办法让程序中不会被调用的源代码不参与编译或链接。我们可以通过一些源代码分析工具来查找哪些函数或者类方法没有被调用并从代码中删除掉来解决编译链接前的瘦身问题。这些分析工具也不在本文的讨论范围内。应用程序在编译时会对工程中的所有代码都执行编译处理并生成目标文件。而在链接阶段则会根据程序代码中对符号的引用关系来将所有相关的目标文件链接为一个大的可执行程序文件,并且在链接阶段链接器会优化掉所有没被调用的C/C++函数代码,但是对于OC类中的没有调用的方法则不会被优化掉。所以为了对可执行程序在编译链接阶段进行瘦身处理就需要了解源代码的编译链接规则。这也是本文所要介绍的针对工程通过静态库的形式进行编译和链接的方式来减少可执行程序代码的尺寸。您可以从文章:《深入iOS系统底层之静态库介绍》中详细的了解到静态库的编译链接过程,以及相关的技术细节。

一个瘦身的例子!

为了验证和具体的实践,我在github上建立了一个项目:YSAppSizeTest。您可以从这个项目中看到如何对工程进行构建以实现程序的瘦身处理。

在示例项目中同一个Workspace中分别建立ThinApp和FatApp两个工程,这两个工程实现的功能是一样。在整个应用程序中分别定义了CA、CB、CC、CD、CE一共5个OC类,定义了一个UIView(Test)分类,还有定义了两个C函数:libFoo1和libFoo1。

整个应用程序中只使用了CA和CC两个OC类,以及调用了UIView(Test)分类方法,以及调用了libFoo1函数,并且同时都采用导入静态库的形式。因为这两个工程对文件的定义和分布策略不同使得两个应用程序的最终可执行代码的尺寸是不相同的。

FatApp中的文件定义和分布策略

1、FatApp工程依赖并导入了FatAppLib静态库工程。
2、CA,CB两个类都定义在主程序工程中。
3、CC,CD,CE三个类,以及UIView(Test)分类,还有libFoo1,libFoo2两个函数都定义在FatAppLib静态库工程中。
4、CC,CD两个类定义在同一个文件中,CE类则定义在单独的文件中。
5、FatApp工程的Other Linker Flags中设置了 -ObjC选项。

ThinApp中的文件定义和分布策略

1、ThinApp工程依赖并导入了ThinAppLib静态库工程。
2、主程序工程就是一个壳工程。
3、CA,CB,CC,CD,CE5个类,以及UIView(Test)分类,还有libFoo1,libFoo2两个函数都定义在ThinAppLib静态库工程中。
4、上述的5个类都分别定义在不同的文件中。
5、ThinApp工程的Other Linker Flags中没有设置-ObjC选项。

上述两个工程的程序被Archive出来后,FatApp可执行程序的尺寸是367KB,而ThinApp可执行程序的尺寸是334KB。通过一些工具比如Mach-O View或者 IDA可以看出:FatApp中5个OC类的代码以及libFoo1函数还有UIView(Test)分类的代码都被链接进可执行程序中;而ThinApp中则只有CA,CC两个类以及libFoo1函数还有UIView(Test)分类的代码被链接进可执行程序中。在ThinApp中虽然没有使用-Objc链接选项,但是静态库中的分类也被链接进可执行程序中。

应用程序工程构建规则

根据对项目中的文件定义和引用策略以及相关的理论基础我们可以按照如下的规则来构建您的应用程序:

1、尽量将所有代码都移植到静态库中,而主程序则保留为一个壳程序。具体操作方法是建立一个Workspace,然后主程序工程就只有默认创建工程时的代码,所有新加入的代码都建立并存放到静态库工程中去,然后通过工程依赖来引入这些静态库工程,或者借助一些工程化工具比如Cocoapods来实现这种拆分和引用处理。主程序工程中只保留AppDelegate的代码,其他代码都一致到静态库中。然后在AppDelegate中的相关代码处调用静态库中定义的业务代码。

2、按业务组件对工程进行解耦每个组件是一个静态库工程。静态库中的每一个文件中最好只有一个类的实现,并且类的分类实现最好和类实现编写在同一个文件中,相同功能的代码以及可能都会被调用的代码尽量存放在一个文件中。

3、不要在主程序工程中使用-ObjC和-all_load两个选项而改为用-force_load 来单独指定要执行加载的静态库。-ObjC和-all_load选项会把主程序工程以及所依赖的所有静态库中的工程中的全部代码都链接到可执行程序中而不管代码是否有被调用过或者使用过。而force_load则只会将指定的静态库中的所有代码链接到可执行程序中,当然force_load如果没有必要也尽量不要使用。

4、尽量减少在静态库中定义OC类的分类方法,如果一定要定义分类方法则可以将分类方法定义在和类定义相同的文件中,或者将分类方法定义在一个一定会被调用和引用的实现文件中。因为根据链接规则静态库中的分类是不会被链接进可执行程序中的,除非使用了上述的三个链接选项。如果将分类代码单独的定义在一个文件中的话则可以通过在分类的头文件中定义一个内联函数,内联函数调用分类实现文件中的一个dumy函数,这样只要这个分类的头文件被include或者import就会把整个分类的实现链接到可执行程序中去。一般情况下我们在静态库中建立分类那就表明一定会被某个文件引用这个分类,从而实现整个文件的链接处理。在分类中定义的这两个函数则因为没有被任何地方调用,因此会在链接优化中将这两个函数给优化掉。这样就使得即使我们不用-ObjC选项也能将静态库中的分类链接到可执行程序中去。最后需要注意的是在每个分类中定义的这两个函数名最好能够唯一这样就不会出现符号重名冲突的问题了。

//分类文件的头文件UIView+XXX.h
@interface UIView (XXX)

//分类中定义的方法

@end

/*
通过在分类的头文件中定义一个内联函数,内联函数调用分类实现文件中的一个dumy函数,这样只要这个分类的头文件被include或者import就会把
整个分类的实现链接到可执行程序中去。一般情况下我们在静态库中建立分类那就表明一定会被某个文件引用这个分类,从而实现整个文件的链接处理。
而在分类中定义的这两个函数则因为没有被任何地方调用,因此会在链接优化中将这两个函数给优化掉。这样就使得即使我们不用-ObjC选项也能
将静态库中的分类链接到可执行程序中去。最后需要注意的是在每个分类中定义的这两个函数名最好能够唯一这样就不会出现符号重名冲突的问题了。
*/
extern void _cat_UIView_XXX_Impl(void);
inline void _cat_UIView_XXX_Decl(void){_cat_UIView_XXX_Impl();}


------------------------------------------------------------
//分类文件的实现文件UIView+XXX.m
#import "UIView+XXX.h"

@implementation UIView (XXX)

//分类的实现代码

@end

void _cat_UIView_XXX_Impl(void){}


---------------------------------------------------------------
//最后把这个分类头文件放入到某个对外暴露的头文件中,比如本例中将分类代码放入到了ThinAppLib.h文件中
//ThinAppLib.h

#import "UIView+XXX.h"
//其他头文件

5、除了可以通过-force_load来加载指定静态库中的所有代码外。我们还可以在构建静态库时,在静态库的工程的Build Settings中将Perform Single-Object Prelink 中的开关选项打开。当这个开关打开时,系统会对生成的静态库的所有目标文件执行预链接操作,预链接操作会将所有的目标文件组合成为一个单独的大的目标文件。这样根据以文件为单位的链接规则就会将静态库中的所有代码全部都链接进可执行程序中去,但是这样带来的问题就是最后在dead code stripping时删除不掉已经链接进来的那些没有被任何地方使用过的OC类了。

6、对于引入的一些第三方静态库或者第三方的开源库来说因为我们无法去改变其实现逻辑。如果这个静态库中没有任何分类代码的定义则正常引用即可,如果静态库中有分类方法的定义则单独对这个静态库采用-force_load选项。

总之一句话:为了让你的程序瘦身,尽量将代码放到静态库中,不要使用-Objc和-all_load选项

为了验证上述方法的有效性,笔者对项目中的应用做了一个测试:分别是有带-ObjC选项和没有带-ObjC选项的情况下的应用程序包中可执行程序的大小从115M减少到95M,减少了20M的尺寸。

链接:https://www.jianshu.com/p/2078e00891fd

收起阅读 »

日!!聊天页面还能这么简单??ChatKit

ChatKit 是一个免费且开源的 UI 聊天组件,由 LeanCloud 官方推出,底层聊天服务基于 LeanCloud 的 IM 即时通讯服务 LeanMessage 而开发。它的最大特点是把聊天常用的一些功能配合 UI 一起提供给开发者,帮助开发者快速集...
继续阅读 »

ChatKit 是一个免费且开源的 UI 聊天组件,由 LeanCloud 官方推出,底层聊天服务基于 LeanCloud 的 IM 即时通讯服务 LeanMessage 而开发。它的最大特点是把聊天常用的一些功能配合 UI 一起提供给开发者,帮助开发者快速集成 IM 服务,轻松实现聊天功能。
+


ChatKit 开源且提供完全自由的授权协议,开发者可以对其进行任意修改、扩展和二次封装,但是 LeanCloud 并不对 ChatKit 的二次开发提供技术支持。

+


获取项目


git clone git@github.com:leancloud/LeanCloudChatKit-Android.git

运行 Demo


获取源代码之后,用 Android Studio 打开项目,左侧的 Project 视图显示为:

+




「ChatKit-Android」Project 包含两个模块:

+



  • leancloudchatkit

    是一个封装了 LeanCloud 即时通讯的 UI lib,其目的是让开发者更快速地接入 LeanCloud 即时通讯的功能。

  • chatkitapplication

    为 Demo 项目,它是一个简单的示例项目,用来指导开发者如何使用 leancloudchatkit。


然后,请确保电脑上已经连接了一台真机或者虚拟机用作调试。

+


点击 Debug 或者 Run,第一次启动会运行 Gradle Build。建议打开全局网络代理,否则 Gradle Build 可能会因为网络原因无法完成。

+


使用方法


开发者可以将 ChatKit 导入到自己的 Project 中使用。下面我们将新建一个 Project(名为 ChatDemo) 用以导入 ChatKit。导入方式推荐通过源代码导入

+


源代码导入



  1. 浏览器访问 https://github.com/leancloud/LeanCloudChatKit-Android

  2. 执行以下命令行,将项目 clone 到本地(如 ChatKit 文件夹中,或者直接下载 zip 包自行解压缩到此文件夹下):
    git clone https://github.com/leancloud/LeanCloudChatKit-Android.git`


  3. 将文件夹 leancloudchatkit 复制到 ChatDemo 根目录;

  4. 修改 ChatDemo/settings.gradle 加入 include ':leancloudchatkit'

  5. 修改 ChatDemo/app/build.gradle,在 dependencies 中添加 compile project(":leancloudchatkit")


最后只要 Sync Project,这样 ChatKit 就算是导入到项目中了。

+


自定义使用


一、实现自己的 Application

+


ChatKit 在使用之前需要进行初始化,就像直接使用 LeanCloud 基础 SDK 时需要调用 AVOSCloud.initialize(appId, appKey) 一样。初始化逻辑应该放在 Application.onCreate 方法中实现。

+


ChatDemo 中新建一个 Class,名字叫做 ChatDemoApplication,让它继承自 Application 类,代码如下:

+


public class ChatDemoApplication extends Application {

// appId、appKey 可以在「LeanCloud 控制台 > 设置 > 应用 Key」获取
private final String APP_ID = "********";
private final String APP_KEY = "********";

@Override
public void onCreate() {
super.onCreate();
// 关于 CustomUserProvider 可以参看后面的文档
LCChatKit.getInstance().setProfileProvider(CustomUserProvider.getInstance());
LCChatKit.getInstance().init(getApplicationContext(), APP_ID, APP_KEY);
}
}

二、在 AndroidMainfest.xml 中配置 ChatDemoApplication

+


<application
...
android:name=".ChatDemoApplication" >
...
</application>

三、实现自己的用户体系

+


一般来说,聊天界面要相对复杂一些,不但要支持文字、表情、图片、语音等消息格式,还要展示用户信息,比如用户的头像、昵称等。而 LeanCloud 的消息流中只包含用户的 clientId 这一唯一标识,所以要获取头像这类额外的用户信息,就需要开发者接入自己的用户系统来实现。

+


为了保证通用性和扩展性,让开发者可以更容易将聊天界面嵌入自己的应用中,ChatKit 在设计上抽象出了一个「用户体系」的接口,需要开发者自己提供用户信息的获取方式。该接口只有一个方法需要开发者实现:

+


/**
* 用户体系的接口,开发者需要实现此接口来接入 LCChatKit
*/

public interface LCChatProfileProvider {
// 根据传入的 clientId list,查找、返回用户的 Profile 信息(id、昵称、头像)
public void fetchProfiles(List<String> userIdList, LCChatProfilesCallBack profilesCallBack);
}

为此,我们在 ChatDemo 中新建一个 Class,名字叫做 CustomUserProvider,代码如下:

+


public class CustomUserProvider implements LCChatProfileProvider {

private static CustomUserProvider customUserProvider;

public synchronized static CustomUserProvider getInstance() {
if (null == customUserProvider) {
customUserProvider
= new CustomUserProvider();
}
return customUserProvider;
}

private CustomUserProvider() {
}

private static List<LCChatKitUser> partUsers = new ArrayList<LCChatKitUser>();

// 此数据均为模拟数据,仅供参考
static {
partUsers
.add(new LCChatKitUser("Tom", "Tom", "http://www.avatarsdb.com/avatars/tom_and_jerry2.jpg"));
partUsers
.add(new LCChatKitUser("Jerry", "Jerry", "http://www.avatarsdb.com/avatars/jerry.jpg"));
partUsers
.add(new LCChatKitUser("Harry", "Harry", "http://www.avatarsdb.com/avatars/young_harry.jpg"));
partUsers
.add(new LCChatKitUser("William", "William", "http://www.avatarsdb.com/avatars/william_shakespeare.jpg"));
partUsers
.add(new LCChatKitUser("Bob", "Bob", "http://www.avatarsdb.com/avatars/bath_bob.jpg"));
}

@Override
public void fetchProfiles(List<String> list, LCChatProfilesCallBack callBack) {
List<LCChatKitUser> userList = new ArrayList<LCChatKitUser>();
for (String userId : list) {
for (LCChatKitUser user : partUsers) {
if (user.getUserId().equals(userId)) {
userList
.add(user);
break;
}
}
}
callBack
.done(userList, null);
}

public List<LCChatKitUser> getAllUsers() {
return partUsers;
}
}

当用户昵称和头像需要更新时,需要覆盖旧的 LCChatKitUser 对象并更新本地缓存:

+


    LCChatKitUser user = new LCChatKitUser("唯一 userId 不可变", "要变更的昵称", "要变更的 avatarURL");
LCIMProfileCache.getInstance().cacheUser(user);

四、打开即时通讯,并且跳转到聊天页面

+


我们支持通过两种方式来打开聊天界面:

+



  1. 通过指定另一个参与者的 clientId 的方式,开启一对一的聊天;

    此时,通过调用 intent.putExtra(LCIMConstants.PEER_ID, "peermemberId") 来传递另一参与者的 clientId。

  2. 通过指定一个已经存在的 AVIMConversation id 的方式,开启单人、多人或者开放式聊天室;

    此时,通过调用 LCIMConstants.CONVERSATION_ID, "particularConversationId") 来传递特定对话 Id。


下面的代码展示了如何通过第一种方式来开启聊天界面:

+


LCChatKit.getInstance().open("Tom", new AVIMClientCallback() {
@Override
public void done(AVIMClient avimClient, AVIMException e) {
if (null == e) {
finish
();
Intent intent = new Intent(MainActivity.this, LCIMConversationActivity.class);
intent
.putExtra(LCIMConstants.PEER_ID, "Jerry");
startActivity
(intent);
} else {
Toast.makeText(MainActivity.this, e.toString(), Toast.LENGTH_SHORT).show();
}
}
});

这样,Tom 就可以和 Jerry 愉快地聊天了。

+


接口以及组件


以下介绍在 ChatKit 中开发者常需要关注的业务逻辑组件(Interface)和界面组件(UI)。

+


用户


LCChatKitUser 是 ChatKit 封装的参与聊天的用户,它提供了如下属性:

2+























名称 描述
userId 用户在单个应用内唯一的 ID,也是调用 LCChatKit.open 时传入的 userId。
avatarUrl 用户头像的 URL
name 用户名

使用这些默认的属性基本可以满足一个聊天应用的需求,同时开发者可以通过继承 LCChatKitUser 类实现更多属性。具体用法请参考 Demo 中的 MembersAdapter.java

+


用户信息管理类


LCChatProfileProvider 接口需要用户 implements 后实现 fetchProfiles 函数,以使 ChatKit 在需要显示的时候展示用户相关的信息。

+


例如 Demo 中的 CustomUserProvider 这个类,它实现了 LCChatProfileProvider 接口,在 fetchProfiles 方法里加载了 Tom、Jerry 等人的信息。

+


核心类


LCChatKit 是 ChatKit 的核心类,具体逻辑可以参看代码,注意以下几个主要函数:

+



public void init(Context context, String appId, String appKey)

此函数用于初始化 ChatKit 的相关设置,此函数要在 Application 的 onCreate 中调用,否则可能会引起异常。

public void setProfileProvider(LCChatProfileProvider profileProvider)

此函数用于设置用户体系,因为 LeanCloud 即时通讯功能已经实现了完全剥离用户体系的功能,这里接入已有的用户体系会很方便。

public void open(final String userId, final AVIMClientCallback callback)

此函数用于开始即时通讯,open 成功后可以执行自己的逻辑,或者跳转到聊天页面。



对话列表界面


对话 AVIMConversation 是 LeanMessage 封装的用来管理对话中的成员以及发送消息的载体,不论是群聊还是单聊都是在一个对话当中;而对话列表可以作为聊天应用默认的首页显示出来,主流的社交聊天软件,例如微信,就是把最近的对话列表作为登录后的首页。

+


因此,我们也提供了对话列表 LCIMConversationListFragment 页面供开发者使用,在 Demo 项目中的 MainActivity 中的 initTabLayout 方法中演示了如何引入对话列表页面:

+


  private void initTabLayout() {
String[] tabList = new String[]{"会话", "联系人"};
final Fragment[] fragmentList = new Fragment[] {new LCIMConversationListFragment(),
new ContactFragment()};
// 以上这段代码为新建了一个 Fragment 数组,并且把 LCIMConversationListFragment 作为默认显示的第一个 Tab 页面

tabLayout
.setTabMode(TabLayout.MODE_FIXED);
for (int i = 0; i < tabList.length; i++) {
tabLayout
.addTab(tabLayout.newTab().setText(tabList[i]));
}
...
}

具体的显示效果如下:

+




聊天界面


聊天界面是显示频率最高的前端页面,ChatKit 通过 LCIMConversationFragmentLCIMConversationActivity 来实现这一界面。在 Demo 的 ContactItemHolder 界面包含了使用 LCIMConversationActivity 的实例:

+


  public void initView() {
nameView
= (TextView)itemView.findViewById(R.id.tv_friend_name);
avatarView
= (ImageView)itemView.findViewById(R.id.img_friend_avatar);

itemView
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 点击联系人,直接跳转进入聊天界面
Intent intent = new Intent(getContext(), LCIMConversationActivity.class);
// 传入对方的 Id 即可
intent
.putExtra(LCIMConstants.PEER_ID, lcChatKitUser.getUserId());
getContext
().startActivity(intent);
}
});
}

具体的显示效果如下:

+




联系人列表页面


因为 ChatKit 是与应用的用户体系完全解耦的,所以 ChatKit 中并没有包含联系人列表页面,但部分开发者可能有此需求,所以 chatkitapplication 实现了一个基于 LCChatProfileProvider 的联系人列表页面,具体代码可以参考 ContactFragment.java
具体效果如下:

+




常见问题


ChatKit 组件收费么?

ChatKit 是完全开源并且免费给开发者使用,使用聊天所产生的费用以账单为准。

+


接入 ChatKit 有什么好处?

它可以减轻应用或者新功能研发初期的调研成本,直接引入使用即可。ChatKit 从底层到 UI 提供了一整套的聊天解决方案。




收起阅读 »

罕见!!上弹窗:Alerter

alerter克服了toast和snackbar的缺点,并布局很简单 生成为了简单起见,Alerter采用了builder模式,以便于轻松集成到任何应用程序中。 可自定义的警报视图将动态添加到窗口的装饰视图中,覆盖所有内容。 安装配置allprojects {...
继续阅读 »

alerter克服了toast和snackbar的缺点,并布局很简单


生成

为了简单起见,Alerter采用了builder模式,以便于轻松集成到任何应用程序中。


可自定义的警报视图将动态添加到窗口的装饰视图中,覆盖所有内容。


安装配置

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

build.gradle


dependencies {
implementation 'com.github.tapadoo:alerter:$current-version'
}

使用


Activity -


Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.show()

Fragment -


Alerter.create(activity)
.setTitle("Alert Title")
.setText("Alert text...")
.show()

展示 -


Alerter.isShowing()

隐藏 -


Alerter.hide()

定制

背景颜色

Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setBackgroundColorRes(R.color.colorAccent) // or setBackgroundColorInt(Color.CYAN)
.show()


图标

Alerter.create(this@DemoActivity)
.setText("Alert text...")
.setIcon(R.drawable.alerter_ic_mail_outline)
.setIconColorFilter(0) // Optional - Removes white tint
.setIconSize(R.dimen.custom_icon_size) // Optional - default is 38dp
.show()


自定义屏幕持续时间(毫秒)

Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setDuration(10000)
.show()

无标题

Alerter.create(this@DemoActivity)
.setText("Alert text...")
.show()


添加 Click Listener

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setDuration(10000)
.setOnClickListener(View.OnClickListener {
Toast.makeText(this@DemoActivity, "OnClick Called", Toast.LENGTH_LONG).show();
})
.show()


长的文本

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("The alert scales to accommodate larger bodies of text. " +
"The alert scales to accommodate larger bodies of text. " +
"The alert scales to accommodate larger bodies of text.")
.show()


自定义进入/退出动画

  Alerter.create(this@KotlinDemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setEnterAnimation(R.anim.alerter_slide_in_from_left)
.setExitAnimation(R.anim.alerter_slide_out_to_right)
.show()

可见性回调

 Alerter.create(this@KotlinDemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setDuration(10000)
.setOnShowListener(OnShowAlertListener {
Toast.makeText(this@KotlinDemoActivity, "Show Alert", Toast.LENGTH_LONG).show()
})
.setOnHideListener(OnHideAlertListener {
Toast.makeText(this@KotlinDemoActivity, "Hide Alert", Toast.LENGTH_LONG).show()
})
.show()

自定义字体和文本外观

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setTitleAppearance(R.style.AlertTextAppearance_Title)
.setTitleTypeface(Typeface.createFromAsset(getAssets(), "Pacifico-Regular.ttf"))
.setText("Alert text...")
.setTextAppearance(R.style.AlertTextAppearance_Text)
.setTextTypeface(Typeface.createFromAsset(getAssets(), "ScopeOne-Regular.ttf"))
.show()


退出

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.enableSwipeToDismiss()
.show()


Progress Bar

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.enableProgress(true)
.setProgressColorRes(R.color.colorAccent)
.show()


带按钮

 Alerter.create(this@KotlinDemoActivity)
.setTitle(R.string.title_activity_example)
.setText("Alert text...")
.addButton("Okay", R.style.AlertButton, View.OnClickListener {
Toast.makeText(this@KotlinDemoActivity, "Okay Clicked", Toast.LENGTH_LONG).show()
})
.addButton("No", R.style.AlertButton, View.OnClickListener {
Toast.makeText(this@KotlinDemoActivity, "No Clicked", Toast.LENGTH_LONG).show()
})
.show()


自定义layout

 Alerter.create(this@KotlinDemoActivity, R.layout.custom_layout)
.setBackgroundColorRes(R.color.colorAccent)
.also { alerter ->
val tvCustomView = alerter.getLayoutContainer()?.tvCustomLayout
tvCustomView?.setText(R.string.with_custom_layout)
}
.show()


github地址:https://github.com/Tapadoo/Alerter
下载地址:master.zip


收起阅读 »

iOS开发你不知道的事-编译&链接

对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc he...
继续阅读 »

对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc hello.c命令就包含了非常复杂的过程!

正是因为集成开发环境的强大,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。我们看到的是这些问题的现象,但是却很难看清本质,所有这些问题的本质就是软件运行背后的机理及支撑软件运行的各种平台和工具,如果能深入了解这些机制,那么解决这些问题就能够游刃有余,收放自如了。

编译流程分析
现在我们通过一个C语言的经典例子,来具体了解一下这些机制:

#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}

在linux下只需要一个简单的命令(假设源代码文件名为hello.c):

$ gcc hello.c
$ ./a.out
Hello World

其实上述过程可以分解为四步:

1、预处理(Prepressing)
2、编译(Compilation)
3、汇编(Assembly)
4、链接(Linking)


预编译
首先是源代码文件hello.c和相关的头文件(如stdio.h等)被预编译器cpp预编译成一个.i文件。第一步预编译的过程相当于如下命令(-E 表示只进行预编译):

$ gcc –E hello.c –o hello.i

还可以下面的表达

$ cpp hello.c > hello.i

预编译过程主要处理源代码文件中以”#”开头的预编译指令。比如#include、#define等,主要处理规则如下:

1、将所有的#define删除,并展开所有的宏定义
2、处理所有条件预编译指令,比如#if,#ifdef,#elif,#else,#endif
3、处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
4、删除所有的注释//和/**/
5、添加行号和文件名标识,比如#2 “hello.c” 2。
6、保留所有的#pragma编译器指令
截图个大家看看效果


经过预编译后的文件(.i文件)不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经插入到.i文件中,所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

编译(compliation)
编译过程就是把预处理完的文件进行一系列的:词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,此过程是整个程序构建的核心部分,也是最复杂的部分之一。其编译过程相当于如下命令:

$ gcc –S hello.i –o hello.s


通过上图我们不难得出,通过命令得到汇编输出文件hello.s.

汇编(assembly)
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。其汇编过程相当于如下命令:

as hello.s –o hello.o

或者

gcc –c hello.s –o hello.o

或者使用gcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件:

gcc –c hello.c –o hello.o

链接(linking)
链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?为什么要链接?下面让我们来看看怎么样调用ld才可以产生一个能够正常运行的Hello World程序:

注意默认情况没有gcc / 记得 :
$ brew install gcc

链接相应的库


下面在贴出我们的写出的源代码是如何变成目标代码的流程图:


主要通过我们的编译器做了以下任务:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化

到这我们就可以得到以下的文件,不知道你是否有和我一起操作,玩得感觉还是不错,继续往下面看


iOS的编译器
iOS现在为了达到更牛逼的速度和优化效果,采用了LLVM

1.LLVM核心库:
LLVM提供一个独立的链接代码优化器为许多流行CPU(以及一些不太常见的CPU)的代码生成支持。这些库是围绕一个指定良好的代码表示构建的,称为LLVM中间表示(“LLVM IR”)。LLVM还可以充当JIT编译器 - 它支持x86 / x86_64和PPC / PPC64程序集生成,并具有针对编译速度的快速代码优化。。

2.LLVM IR 生成器Clang: Clang是一个“LLVM原生”C / C ++ / Objective-C编译器,旨在提供惊人的快速编译(例如,在调试配置中编译Objective-C代码时比GCC快3倍),非常有用的错误和警告消息以及提供构建优秀源代码工具的平台。

3.LLDB项目:
LLDB项目以LLVM和Clang提供的库为基础,提供了一个出色的本机调试器。它使用Clang AST和表达式解析器,LLVM JIT,LLVM反汇编程序等,以便提供“正常工作”的体验。在加载符号时,它也比GDB快速且内存效率更高。

4.libc和libc++:
libc 和libc++ ABI项目提供了C ++标准库的标准符合性和高性能实现,包括对C ++ 11的完全支持。

5.lld项目:
lld项目旨在成为clang / llvm的内置链接器。目前,clang必须调用系统链接器来生成可执行文件。

LLVM采用三相设计,前端Clang负责解析,验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR,后端LLVM编译把IR通过一系列改进代码的分析和优化过程提供,然后被发送到代码生成器以生成本机机器代码。


编译器前端的任务是进行:

1、语法分析
2、语义分析
3、生成中间代码(intermediate representation )

在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。


以上图解内容所做的是事情和gcc编译一模模一样样!

iOS程序-详细编译过程
1.写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
2.运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases中可以看到;
3.编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
4.链接文件:将项目中的多个可执行文件合并成一个文件;
5.拷贝资源文件:将项目中的资源文件拷贝到目标包;
6.编译 storyboard 文件:storyboard 文件也是会被编译的;
7.链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
8.编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage;
9.运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。
10.生成 .app 包
11.将 Swift 标准库拷贝到包中
12.对包进行签名
13.完成打包

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

收起阅读 »

测试 View Controllers

我们不是迷信测试,但它应该帮助我们加快开发进度,并且让事情变得更有趣。让事情保持简单测试简单的事情很简单,同样,测试复杂的事会很复杂。就像我们在其他文章中指出的那样,让事情保持简单小巧总是好的。除此之外,它还有利于我们测试。这是件双赢的事。让我们来看看测试驱动...
继续阅读 »

我们不是迷信测试,但它应该帮助我们加快开发进度,并且让事情变得更有趣。

让事情保持简单

测试简单的事情很简单,同样,测试复杂的事会很复杂。就像我们在其他文章中指出的那样,让事情保持简单小巧总是好的。除此之外,它还有利于我们测试。这是件双赢的事。让我们来看看测试驱动开发(简称 TDD),有些人喜欢它,有些人则不喜欢。我们在这里不深入讨论,只是如果用 TDD,你得在写代码之前先写好测试。如果你好奇的话,可以去找 Wikipedia 上的文章看看。同时,我们也认为重构和测试可以很好地结合在一起。

测试 UI 部分通常很麻烦,因为它们包含太多活动部件。通常,view controller 需要和大量的 model 和 view 类交互。为了使 view controller 便于测试,我们要让任务尽量分离。

幸好,我们在更轻量的 view controller 这篇文章中的阐述的技术可以让测试更加简单。通常,如果你发现有些地方很难做测试,这就说明你的设计出了问题,你应该重构它。你可以重新参考更轻量的 view controller 这篇文章来获得一些帮助。总的目标就是有清晰的关注点分离。每个类只做一件事,并且做好。这样就可以让你只测试这件事。

记住:测试越多,回报的增长趋势越慢。首先你应该做简单的测试。当你觉得满意时,再加入更多复杂的测试。

Mocking

当你把一个整体拆分成小零件(比如更小的类)时,我们可以针对每个小的类来进行测试。但由于我们测试的类会和其他类交互,这里我们用一个所谓的 mock 或 stub 来绕开它。把 mock 对象看成是一个占位符,我们测试的类会跟这个占位符交互,而不是真正的那个对象。这样,我们就可以针对性地测试,并且保证不依赖于应用程序的其他部分。

在示例程序中,我们有个包含数组的 data source 需要测试。这个 data source 会在某个时候从 table view 中取出(dequeue)一个 cell。在测试过程中,还没有 table view,但是我们传递一个 mock 的 table view,这样即使没有 table view,也可以测试 data source,就像下面你即将看到的。起初可能有点难以理解,多看几次后,你就能体会到它的强大和简单。

Objective-C 中有个用来 mocking 的强大工具叫做 OCMock。它是一个非常成熟的项目,充分利用了 Objective-C 运行时强大的能力和灵活性。它使用了一些很酷的技巧,让通过 mock 对象来测试变得更加有趣。

本文后面有 data source 测试的例子,它更加详细地展示了这些技术如何工作在一起。

SenTestKit

编者注 这一节有一些过时了。在 Xcode 5 中 SenTestingKit 已经被 XCTest 完全取代,不过两者使用上没有太多区别,我们可以通过 Xcode 的 Edit -> Refactor -> Convert to XCTest 选项来切换到新的测试框架

我们将要使用的另一个工具是一个测试框架,开发者工具的一部分:Sente 的 SenTestingKit。这个上古神器从 1997 年起就伴随在 Objective-C 开发者左右,比第一款 iPhone 发布还早 10 年。现在,它已经集成到 Xcode 中了。SenTestingKit 会运行你的测试。通过 SenTestingKit,你将测试组织在类中。你需要给每一个你想测试的类创建一个测试类,类名以 Tests 结尾,它反应了这个类是干什么的。

这些测试类里的方法会做具体的测试工作。方法名必须以 test 开头来作为触发一个测试运行的条件。还有特殊的 -setUp 和 -tearDown 方法,你可以重载它们来设置各个测试。记住,你的测试类就是个类而已:只要对你有帮助,可以按需求在里面加 properties 和辅助方法。

做测试时,为测试类创建基类是个不错的模式。把通用的逻辑放到基类里面,可以让测试更简单和集中。可以通过示例程序中的例子来看看这样带来的好处。我们没有使用 Xcode 的测试模板,为了让事情简单有效,我们只创建了单独的 .m 文件。通过把类名改成以 Tests 结尾,类名可以反映出我们在对什么做测试。

编者注 Xcode 5 中 默认的测试模板也不再会自动创建 .h 文件了

与 Xcode 集成

测试会被 build 成一个 bundle,其中包含一个动态库和你选择的资源文件。如果你要测试某些资源文件,你得把它们加到测试的 target 中,Xcode 就会将它们打包到一个 bundle 中。接着你可以通过 NSBundle 来定位这些资源文件,示例项目实现了一个 -URLForResource:withExtension: 方法来方便的使用它。

Xcode 中的每个 scheme 定义了相应的测试 bundle 是哪个。通过 ⌘-R 运行程序,⌘-U 运行测试。

测试的运行依附于程序的运行,当程序运行时,测试 bundle 将被注入(injected)。测试时,你可能不想让你的程序做太多的事,那样会对测试造成干扰。可以把下面的代码加到 app delegate 中:

static BOOL isRunningTests(void) __attribute__((const));

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
if (isRunningTests()) {
return YES;
}

//
// Normal logic goes here
//

return YES;
}

static BOOL isRunningTests(void)
{
NSDictionary* environment = [[NSProcessInfo processInfo] environment];
NSString* injectBundle = environment[@"XCInjectBundle"];
return [[injectBundle pathExtension] isEqualToString:@"octest"];
}

编辑 Scheme 给了你极大的灵活性。你可以在测试之前或之后运行脚本,也可以有多个测试 bundle。这对大型项目来说很有用。最重要的是,可以打开或关闭个别测试,这对调试测试非常有用,只是要记得之后再把它们重新全部打开。

还要记住你可以为测试代码下断点,当测试执行时,调试器会在断点处停下来。

测试 Data Source
好了,让我们开始吧。我们已经通过拆分 view controller 让测试工作变得更轻松了。现在我们要测试 ArrayDataSource。首先我们新建一个空的,基本的测试类。我们把接口和实现都放到一个文件里;也没有哪个地方需要包含 @interface,放到一个文件会显得更加漂亮和整洁。

#import "PhotoDataTestCase.h"

@interface ArrayDataSourceTest : PhotoDataTestCase
@end

@implementation ArrayDataSourceTest
- (void)testNothing;
{
STAssertTrue(YES, @"");
}
@end

这个类没做什么事,只是展示了基本的设置。当我们运行这个测试时,-testNothing 方法将会运行。特别地,STAssert宏将会做琐碎的检查。注意,前缀 ST 源自于 SenTestingKit。这些宏和Xcode 集成,会把失败显示到侧边面板的Issues导航栏中。

第一个测试

我们现在把 testNothing 替换成一个简单、真正的测试:

- (void)testInitializing;
{
STAssertNil([[ArrayDataSource alloc] init], @"Should not be allowed.");
TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b){};
id obj1 = [[ArrayDataSource alloc] initWithItems:@[]
cellIdentifier:@"foo"
configureCellBlock:block];
STAssertNotNil(obj1, @"");
}

接着,我们想测试ArrayDataSource实现的方法:

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath;

为此,我们创建一个测试方法:

- (void)testCellConfiguration;

首先,创建一个 data source:

__block UITableViewCell *configuredCell = nil;
__block id configuredObject = nil;
TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b){
configuredCell = a;
configuredObject = b;
};
ArrayDataSource *dataSource = [[ArrayDataSource alloc] initWithItems:@[@"a", @"b"]
cellIdentifier:@"foo"
configureCellBlock:block];

注意,configureCellBlock 除了存储对象以外什么都没做,这可以让我们可以更简单地测试它。

然后,我们为 table view 创建一个 mock 对象:

id mockTableView = [OCMockObject mockForClass:[UITableView class]];

Data source 将在传进来的 table view 上调用 -dequeueReusableCellWithIdentifier:forIndexPath: 方法。我们将告诉 mock object 当它收到这个消息时要做什么。首先创建一个 cell,然后设置 mock。

UITableViewCell *cell = [[UITableViewCell alloc] init];
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[[[mockTableView expect] andReturn:cell]
dequeueReusableCellWithIdentifier:@"foo"
forIndexPath:indexPath];

第一次看到它可能会觉得有点迷惑。我们在这里所做的,是让 mock 记录特定的调用。Mock 不是一个真正的 table view;我们只是假装它是。-expect 方法允许我们设置一个 mock,让它知道当这个方法调用时要做什么。

另外,-expect 方法也告诉 mock 这个调用必须发生。当我们稍后在 mock 上调用 -verify 时,如果那个方法没有被调用过,测试就会失败。相应地,-stub 方法也用来设置 mock 对象,但它不关心方法是否被调用过。

现在,我们要触发代码运行。我们就调用我们希望测试的方法。

NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
id result = [dataSource tableView:mockTableView
cellForRowAtIndexPath:indexPath];

然后我们测试是否一切正常:

STAssertEquals(result, cell, @"Should return the dummy cell.");
STAssertEquals(configuredCell, cell, @"This should have been passed to the block.");
STAssertEqualObjects(configuredObject, @"a", @"This should have been passed to the block.");
[mockTableView verify];

STAssert宏测试值的相等性。注意,前两个测试,我们通过比较指针来完成;我们不使用-isEqual:,是因为我们实际希望测试的是result,cell和configuredCell都是同一个对象。第三个测试要用 -isEqual:,最后我们调用 mock的 -verify方法。

注意,在示例程序中,我们是这样设置 mock 的:

id mockTableView = [self autoVerifiedMockForClass:[UITableView class]];

这是我们测试基类中的一个方便的封装,它会在测试最后自动调用 -verify 方法。

测试 UITableViewController
下面,我们转向PhotosViewController。它是个 UITableViewController 的子类,它使用了我们刚才测试过的 data source。View controller 剩下的代码已经相当简单了。

我们想测试点击 cell 后把我们带到详情页面,即一个 PhotoViewController的实例被 push 到 navigation controller 里面。我们再次使用 mocking 来让测试尽可能不依赖于其他部分。

首先我们创建一个 UINavigationController 的 mock:

id mockNavController = [OCMockObject mockForClass:[UINavigationController class]];

接下来,我们要使用部分 mocking。我们希望 PhotosViewController 实例的navigationController 返回 mockNavController。我们不能直接设置 navigation controller,所以我们简单地用 stub 来替换掉 PhotosViewController实例这个方法,让它返回mockNavController 就可以了。

PhotosViewController *photosViewController = [[PhotosViewController alloc] init];
id photosViewControllerMock = [OCMockObject partialMockForObject:photosViewController];
[[[photosViewControllerMock stub] andReturn:mockNavController] navigationController];

现在,任何时候对 photosViewController 调用 -navigationController 方法,都会返回 mockNavController。这是个强大的技巧,OCMock 就有这样的本领。

接下来,我们要告诉 navigation controller mock 我们调用的期望,即,一个 photo 不为 nil 的 detail view controller。

UIViewController* viewController = [OCMArg checkWithBlock:^BOOL(id obj) {
PhotoViewController *vc = obj;
return ([vc isKindOfClass:[PhotoViewController class]] &&
(vc.photo != nil));
}];
[[mockNavController expect] pushViewController:viewController animated:YES];

现在,我们触发 view 加载,并且模拟一行被点击:

UIView *view = photosViewController.view;
STAssertNotNil(view, @"");
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[photosViewController tableView:photosViewController.tableView
didSelectRowAtIndexPath:indexPath];

最后我们验证 mocks 上期望的方法被调用过:

[mockNavController verify];
[photosViewControllerMock verify];

现在我们有了一个测试,用来测试和 navigation controller 的交互,以及正确 view controller 的创建。

又一次地,我们在示例程序中使用了便捷的方法:

- (id)autoVerifiedMockForClass:(Class)aClass;
- (id)autoVerifiedPartialMockForObject:(id)object;

于是,我们不需要记住调用-verify。

进一步探索

就像你从上面看到的那样,部分 mocking 非常强大。如果你看看 -[PhotosViewController setupTableView]方法的源码,你就会看到它是如何从 app delegate 中取出 model 对象的。

NSArray *photos = [AppDelegate sharedDelegate].store.sortedPhotos;

上面的测试依赖于这行代码。打破这种依赖的一种方式是再次使用 部分 mocking,让 app delegate 返回预定义的数据,就像这样:

id storeMock; // 假设我们已经设置过了
id appDelegate = [AppDelegate sharedDelegate]
id appDelegateMock = [OCMockObject partialMockForObject:appDelegate];
[[[appDelegateMock stub] andReturn:storeMock] store];

现在,无论何时调用[AppDelegate sharedDelegate].store,它将返回 storeMock。将这个技术使用好的话,可以确保让你的测试恰到好处地在保持简单和应对复杂之间找到平衡。

需要记住的事

部分 mock 技术将会在 mocks 的存在期间替换并保持被 mocking 的对象,并且一直有效。你可以通过提前调用[aMock stopMocking]来终于这种行为。大多数时候,你希望 部分 mock 在整个测试期间都保持有效。如果要提前终止,请确保在测试方法最后放置[aMock verify]。否则 ARC 会过早释放这个 mock,这样你就不能 -verify了,这不太可能是你想要的结果。

测试 NIB 加载

PhotoCell设置在一个 NIB 中,我们可以写一个简单的测试来检查 outlets 设置得是否正确。我们来回顾一下PhotoCell类:

@interface PhotoCell : UITableViewCell

+ (UINib *)nib;

@property (weak, nonatomic) IBOutlet UILabel* photoTitleLabel;
@property (weak, nonatomic) IBOutlet UILabel* photoDateLabel;

@end

我们的简单测试的实现看上去是这样:

@implementation PhotoCellTests

- (void)testNibLoading;
{
UINib *nib = [PhotoCell nib];
STAssertNotNil(nib, @"");

NSArray *a = [nib instantiateWithOwner:nil options:@{}];
STAssertEquals([a count], (NSUInteger) 1, @"");
PhotoCell *cell = a[0];
STAssertTrue([cell isMemberOfClass:[PhotoCell class]], @"");

// 检查 outlet 是否正确设置
STAssertNotNil(cell.photoTitleLabel, @"");
STAssertNotNil(cell.photoDateLabel, @"");
}

@end

非常基础,但是能出色完成工作。

值得一提的是,当有发生改变时,我们需要同时更新测试以及相应的类或 nib 。这是事实。你需要考虑改变类或者 nib 文件时可能会打破原有的 outlets 连接。如果你用了 .xib 文件,你可能要注意了,这是经常发生的事。

关于 Class 和 Injection
我们已经从与 Xcode 集成得知,测试 bundle 会注入到应用程序中。省略注入的如何工作的细节(它本身是个巨大的话题),简单地说:注入是把待注入的 bundle(我们的测试 bundle)中的 Objective-C 类添加到运行的应用程序中。这很好,因为这样允许我们运行测试了。

还有一件事会很让人迷惑,那就是如果我们同时把一个类添加到应用程序和测试 bundle中。如果在上面的示例程序中,我们(不小心)把 PhotoCell 类同时添加到测试 bundle 和应用程序里的话,在测试 bundle 中调用 [PhotoCell class]会返回一个不同的指针(你应用程序中的那个类)。于是我们的测试将会失败:

STAssertTrue([cell isMemberOfClass:[PhotoCell class]], @"");

再一次声明:注入很复杂。你应该确认的是:不要把应用程序中的 .m 文件添加到测试 target 中。否则你会得到预想不到的行为。

额外的思考

如果你使用一个持续集成 (CI) 的解决方案,让你的测试启动和运行是一个好主意。详细的描述超过了本文的范围。这些脚本通过 RunUnitTests 脚本触发。还有个 TEST_AFTER_BUILD 环境变量。

另一种有趣的选择是创建单独的测试 bundle 来自动化性能测试。你可以在测试方法里做任何你想做的。定时调用一些方法并使用 STAssert 来检查它们是否在特定阈值里面是其中一种选择。

链接:https://www.jianshu.com/p/733fa8bbca95

收起阅读 »

基于环信MQTT消息云,Java版MQTT客户端快速实现消息收发

本文介绍Java版MQTT 客户端,如何连接环信MQTT消息云快速实现消息的自收自发。 一、前提条件1.部署Java开发环境安装IDE。您可以使用IntelliJ IDEA或者Eclipse,本文以IntelliJ IDEA为例。下载安装JDK。&n...
继续阅读 »

本文介绍Java版MQTT 客户端,如何连接环信MQTT消息云快速实现消息的自收自发。

 

一、前提条件

1.部署Java开发环境

安装IDE。您可以使用IntelliJ IDEA或者Eclipse,本文以IntelliJ IDEA为例。
下载安装
JDK 

2.导入项目依赖

IntelliJ IDEA中创建工程,并确认pom.xml中包含以下依赖。


commons-codec
commons-codec
1.10


org.eclipse.paho
org.eclipse.paho.client.mqttv3
1.2.2


org.apache.httpcomponents
httpclient
4.5.2


com.alibaba
fastjson
1.2.76

二、实现流程

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代码示例如下:

public static void main() 
{
// 获取token的URL
http://{token域名}/{org_name}/{app_name}/token
// 获取token
String token = "";
// 取token
try (final CloseableHttpClient httpClient = HttpClients.createDefault())
{
final HttpPost httpPost = new HttpPost("http://{token域名}/{org_name}/{app_name}/token");
Map params = new HashMap<>();
params.put("grant_type", "password");
params.put("username", "test");
params.put("password", "test123");
//设置请求体参数
StringEntity entity = new StringEntity(JSONObject.toJSONString(params), Charset.forName("utf-8"));
entity.setContentEncoding("utf-8");
httpPost.setEntity(entity);
//设置请求头部
httpPost.setHeader("Content-Type", "application/json");
//执行请求,返回请求响应
try (final CloseableHttpResponse response = httpClient.execute(httpPost)
{
//请求返回状态码
int statusCode = response.getStatusLine().getStatusCode();
//请求成功
if (statusCode == HttpStatus.SC_OK && statusCode <= HttpStatus.SC_TEMPORARY_REDIRECT)
{
//取出响应体
final HttpEntity entity2 = response.getEntity();
//从响应体中解析出token
String responseBody = EntityUtils.toString(entity2, "utf-8");
JSONObject jsonObject = JSONObject.parseObject(responseBody);
token = jsonObject.getString("access_token");
}
else
{
//请求失败
throw new ClientProtocolException("请求失败,响应码为:" + statusCode);
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
}

返回结果

 {
"access_token": "YWMtN8a0oqV3EeuF0AmiqRgEh-grzF8zZk2Wp8GS3pF-orDW_F-gj3kR6os3h_oz3ROQAwMAAAF5BxhGlwBPGgAvTR8vDrdVsDPNZMQj0fFjv7EaohgZhzMHM9ncVLE30g",
"expires_in": 5184000,
"user":
{
"uuid": "d6fc5fa0-8f79-11ea-8b37-87fa33dd1390",
"type": "user",
"created": 1588756404898,
"modified": 1588756404898,
"username": "test",
"activated": true
}
}
access_token即为要获取的token

2、初始化

      在IntelliJ IDEA工程中创建MQTT客户端,客户端初始配置包括创建clientID,topic名称,QoS质量,连接地址等信息。

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static org.eclipse.paho.client.mqttv3.MqttConnectOptions.MQTT_VERSION_3_1_1;
public class MqttDemoStarter 
{
public static void main(String[] args) throws MqttException, InterruptedException {
/**
* 用户指定
* /
String deviceId = "xxxxx-xxxx-xxxxx-xxxxx-xxxxx";
/**
* 从console控制台获取
* /
String appId = "1NQ1E9";
/**
* 设置接入点,进入console控制台获取
*/
String endpoint = "1NQ1E9.sandbox.mqtt.chat";
/**
* MQTT客户端ID,由业务系统分配,需要保证每个TCP连接都不一样,保证全局唯一,如果不同的客户端对象(TCP连接)使用了相同的clientId会导致之前的连接断开。
* clientId由两部分组成,格式为DeviceID@appId,其中DeviceID由业务方自己设置,appId在console控制台创建,clientId总长度不得超过64个字符。
*/
String clientId = deviceId + "@" + appId;

/**
* 需要订阅或发送消息的topic名称
* 如果使用了没有创建或者没有被授权的Topic会导致鉴权失败,服务端会断开客户端连接。
*/
final String myTopic = "myTopic";

/**
* QoS参数代表传输质量,可选0,1,2。详细信息,请参见名词解释。
*/
final int qosLevel = 0;
final MemoryPersistence memoryPersistence = new MemoryPersistence();

/**
* 客户端协议和端口。客户端使用的协议和端口必须匹配,如果是ws或者wss,使用http://;如果是mqtt或者mqtts,使用tcp://
*/
final MqttClient mqttClient = new MqttClient("tcp://" + endpoint + ":1883", clientId, memoryPersistence);
/**
* 设置客户端发送超时时间,防止无限阻塞。
*/
mqttClient.setTimeToWait(5000);

final ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>());
}

3、连接服务器

    配置连接密码、cleansession标志、心跳间隔、超时时间等信息,调用connect()函数连接至环信MQTT消息云。

MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
/**
* 用户名,在console中注册
*/
mqttConnectOptions.setUserName("test");
/**
* 用户密码为第一步中申请的token
*/
mqttConnectOptions.setPassword(token.toCharArray());
mqttConnectOptions.setCleanSession(true);
mqttConnectOptions.setKeepAliveInterval(90);
mqttConnectOptions.setAutomaticReconnect(true);
mqttConnectOptions.setMqttVersion(MQTT_VERSION_3_1_1);
mqttConnectOptions.setConnectionTimeout(5000);
mqttClient.connect(mqttConnectOptions);
//暂停1秒钟,等待连接订阅完成
Thread.sleep(1000);

4、订阅【subscribe】

【订阅主题】

    当客户端成功连接环信MQTT消息云后,需尽快向服务器发送订阅主题消息。

mqttClient.setCallback(new MqttCallbackExtended() {
/**
* 连接完成回调方法
* @param b
* @param s
*/
@Override
public void connectComplete(boolean b, String s) {
/**
* 客户端连接成功后就需要尽快订阅需要的Topic。
*/
System.out.println("connect success");
executorService.submit(() -> {
try {
final String[] topicFilter = {myTopic};
final int[] qos = {qosLevel};
mqttClient.subscribe(topicFilter, qos);
} catch (Exception e) {
e.printStackTrace();
}
});
}
});

【取消订阅】

mqttClient.unsubscribe(new String[]{myTopic});

【接收消息】

    配置接收消息回调方法,从环信MQTT消息云接收订阅消息。

mqttClient.setCallback(new MqttCallbackExtended() {
/**
* 接收消息回调方法
* @param s
* @param mqttMessage
*/
@Override
public void messageArrived(String s, MqttMessage mqttMessage) {
System.out.println("receive msg from topic " + s + " , body is " + new String(mqttMessage.getPayload()));
}
});

5、发布【publish】

    配置发送消息回调方法,向环信MQTT消息云中指定topic发送消息。

for (int i = 0; i < 10; i++) {
/**
* 构建一个Mqtt消息
*/
MqttMessage message = new MqttMessage("hello world pub sub msg".getBytes());
//设置传输质量
message.setQos(qosLevel);
/**
* 发送普通消息时,Topic必须和接收方订阅的Topic一致,或者符合通配符匹配规则。
*/
mqttClient.publish(myTopic, message);
}

6、结果验证

connect success
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
send msg succeed topic is : myTopic
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg
receive msg from topic myTopic , body is hello world pub sub msg

三、更多信息

  * 完整demo示例,请参见demo下载

或直接下载:MQTTChatDemo- Java.zip

  * 目前MQTT客户端支持多种语言,请参见 SDK下载
  * 如果您在使用环信MQTT消息云服务中,有任何疑问和建议,欢迎您联系我们

 

收起阅读 »

一个围绕 CFNetwork API的网络通讯库,断点续传神器!

ASIHTTPRequest 是一个围绕 CFNetwork API的易于使用的包装器,它使与 Web 服务器通信的一些更乏味的方面变得更容易。它是用 Objective-C 编写的,适用于 Mac OS X 和 iPhone 应用程序。它适用于执行...
继续阅读 »

ASIHTTPRequest 是一个围绕 CFNetwork API的易于使用的包装器,它使与 Web 服务器通信的一些更乏味的方面变得更容易。它是用 Objective-C 编写的,适用于 Mac OS X 和 iPhone 应用程序。

它适用于执行基本的HTTP请求并与基于REST的服务(GET / POST / PUT / DELETE交互包含的 ASIFormDataRequest 子类使得使用 multipart/form-data提交POST数据和文件变得容易

它提供:

  • 一个简单的界面,用于向网络服务器提交数据和从网络服务器获取数据
  • 将数据下载到内存或直接下载到磁盘上的文件
  • 在本地驱动器上提交文件作为POST数据的一部分,与HTML文件输入机制兼容
  • 将请求正文直接从磁盘传输到服务器,以节省内存
  • 恢复部分下载
  • 轻松访问请求和响应HTTP标头
  • 进度委托(NSProgressIndicators 和 UIProgressViews)显示有关下载上传进度的信息
  • 操作队列上传下载进度指示器的自动魔法管理
  • 基本、摘要 + NTLM身份验证支持,凭据在会话期间自动重复使用,并且可以存储在钥匙串中以备日后使用。
  • 饼干支持
  • []当您的应用程序移至后台时,请求可以继续运行(iOS 4+)
  • GZIP支持响应数据请求正文
  • 包含的 ASIDownloadCache 类允许请求透明地缓存响应,并且即使在没有可用网络的情况下也允许对缓存数据的请求成功
  • [] ASIWebPageRequest – 下载完整的网页,包括图像和样式表等外部资源。即使没有网络连接,任何大小的页面都可以无限期地缓存,并显示在 UIWebview/WebView 中。
  • 易于使用的 Amazon S3 支持 – 无需自己动手签署请求!
  • 完全支持 Rackspace 云文件
  • []客户端证书支持
  • 支持手动和自动检测代理、验证代理和PAC文件自动配置。内置的登录对话框让您的 iPhone 应用程序可以透明地使用身份验证代理,无需任何额外工作。
  • 带宽限制支持
  • 支持持久连接
  • 支持同步和异步请求
  • 通过委托或 [ NEW ] 块获取有关请求状态更改的通知(Mac OS X 10.6、iOS 4 及更高版本)
  • 带有广泛的单元测试

ASIHTTPRequest 兼容 Mac OS 10.5 或更高版本,以及 iOS 3.0 或更高版本。


创建同步请求

使用 ASIHTTPRequest 的最简单方法。发送startSynchronous 消息将在同一线程中执行请求,并在完成(成功或失败)时返回控制权。

通过检查error属性来检查问题

要以字符串形式获取响应,请调用responseString方法。不要将它用于二进制数据 - 使用responseData获取 NSData 对象,或者,对于较大的文件,将您的请求设置为使用downloadDestinationPath属性下载到文件

- (IBAction)grabURL:(id)sender
{
NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request startSynchronous];
NSError *error = [request error];
if (!error) {
NSString *response = [request responseString];
}
}

与前面的示例执行相同的操作,但请求在后台运行。

- (IBAction)grabURLInBackground:(id)sender
{
NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request startAsynchronous];
}
 
- (void)requestFinished:(ASIHTTPRequest *)request
{
// Use when fetching text data
NSString *responseString = [request responseString];
 
// Use when fetching binary data
NSData *responseData = [request responseData];
}
 
- (void)requestFailed:(ASIHTTPRequest *)request
{
NSError *error = [request error];
}

请注意,我们设置了请求的委托属性,以便我们可以在请求完成或失败时收到通知。

这是创建异步请求的最简单方法,它将在后台运行在全局 NSOperationQueue 中。对于更复杂的操作(例如跟踪多个请求的进度),您可能希望创建自己的队列,这就是我们接下来将介绍的内容。

使用块

从 v1.8 开始,我们可以在支持块的平台上使用块来做同样的事情:

- (IBAction)grabURLInBackground:(id)sender
{
NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
// Use when fetching text data
NSString *responseString = [request responseString];
 
// Use when fetching binary data
NSData *responseData = [request responseData];
}];
[request setFailedBlock:^{
NSError *error = [request error];
}];
[request startAsynchronous];
}

请注意在我们声明请求时使用__block限定符,这很重要!它告诉块不要保留请求,这对于防止保留循环很重要,因为请求将始终保留块。

使用队列

这个例子再次做同样的事情,但我们为我们的请求创建了一个 NSOperationQueue。

使用您自己创建的 NSOperationQueue(或 ASINetworkQueue,见下文)可以让您更好地控制异步请求。使用队列时,只能同时运行一定数量的请求。如果您添加的请求多于队列的maxConcurrentOperationCount属性,则请求将在开始之前等待其他人完成。

- (IBAction)grabURLInTheBackground:(id)sender
{
if (![self queue]) {
[self setQueue:[[[NSOperationQueue alloc] init] autorelease]];
}
 
NSURL *url = [NSURL URLWithString:@"http://allseeing-i.com"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request setDidFinishSelector:@selector(requestDone:)];
[request setDidFailSelector:@selector(requestWentWrong:)];
[[self queue] addOperation:request]; //queue is an NSOperationQueue
}
 
- (void)requestDone:(ASIHTTPRequest *)request
{
NSString *response = [request responseString];
}
 
- (void)requestWentWrong:(ASIHTTPRequest *)request
{
NSError *error = [request error];
}


demo及常见问题:https://github.com/paytronix/ASIHTTPRequest

源码下载:ASIHTTPRequest-master.zip


收起阅读 »

扁平化 UI 的 iOS 组件-FlatUIKit

FlatUIKitFlatUIKit 是我们在为 iPhone构建Grouper 时创建的具有“Flat UI”美学风格的 iOS 组件集合。它的设计灵感来自于Flat UI和Kyle Miller。样式是通过替换现有 UIKit 组件的类别来实现的,因此将其...
继续阅读 »


FlatUIKit

FlatUIKit 是我们在为 iPhone构建Grouper 时创建的具有“Flat UI”美学风格的 iOS 组件集合它的设计灵感来自于Flat UIKyle Miller样式是通过替换现有 UIKit 组件的类别来实现的,因此将其集成到您的项目中非常简单。

安装

FlatUIKit 可以通过CocoaPods安装只需添加

pod 'FlatUIKit'

组件

FUIButton 是 UIButton 的一个嵌入式子类,它公开了额外的属性 buttonColor、shadowColor、cornerRadius 和 shadowHeight。请注意,如果您设置了其中任何一个,则必须设置所有这些。

myButton.buttonColor = [UIColor turquoiseColor ];
myButton.shadowColor = [UIColor
greenSeaColor ];
myButton.shadowHeight =
30f ;
myButton.cornerRadius =
60f ;
myButton.titleLabel.font = [UIFont
boldFlatFontOfSize: 16 ];
[myButton
setTitleColor: [UIColor cloudColor ] forState: UIControlStateNormal];
[myButton
setTitleColor: [UIColor cloudColor ] forState: UIControlStateHighlighted];

FUITextField 是 UITextField 的一个嵌入式子类,它公开了附加属性 edgeInsets、textFieldColor、borderColor、borderWidth 和 cornerRadius。请注意,如果您设置了其中任何一个,则必须设置所有这些。

myTextField.font = [UIFont flatFontOfSize: 16 ];
myTextField.backgroundColor = [UIColor
clearColor ];
myTextField.edgeInsets = UIEdgeInsetsMake(
4 . 0f , 15 . 0f , 4 . 0f , 15 . 0f );
myTextField.textFieldColor = [UIColor
whiteColor ];
myTextField.borderColor = [UIColor
turquoiseColor ];
myTextField.borderWidth =
20f ;
myTextField.cornerRadius =
30f ;

FUISegmentedControl 是 UISegmentedControl 的一个嵌入式子类,它公开了附加属性 selectedColor、deselectedColor、selectedFont、deselectedFont、selectedFontColor、deselectedFontColor、dividerColor 和 cornerRadius。请注意,如果您设置了其中任何一个,建议您设置所有这些。

mySegmentedControl.selectedFont = [UIFont boldFlatFontOfSize: 16 ];
mySegmentedControl.selectedFontColor = [UIColor
cloudColor ];
mySegmentedControl.deselectedFont = [UIFont
flatFontOfSize: 16 ];
mySegmentedControl.deselectedFontColor = [UIColor
cloudColor ];
mySegmentedControl.selectedColor = [UIColor
amethystColor ];
mySegmentedControl.deselectedColor = [UIColor
silverColor ];
mySegmentedControl.dividerColor = [的UIColor
midnightBlueColor ];
mySegmentedControl.cornerRadius =
5.0 ;

FUISwitch 不是 UISwitch 的子类(UISwitch 的子类太不灵活了),而是一个重新实现,它暴露了 UISwitch 的所有方法。此外,它还提供对其底层开/关 UILabels 和其他子视图的访问。

mySwitch.onColor = [UIColor turquoiseColor ];
mySwitch.offColor = [UIColor
cloudColor ];
mySwitch.onBackgroundColor = [的UIColor
midnightBlueColor ];
mySwitch.offBackgroundColor = [UIColor
silverColor ];
mySwitch.offLabel.font = [UIFont
boldFlatFontOfSize: 14 ];
mySwitch.onLabel.font = [UIFont
boldFlatFontOfSize: 14 ];

与 FUISwitch 类似,FUIAlertView 是 UIAlertView 的重新实现,它公开了 UIAlertView 的所有方法(和委托方法,使用 FUIAlertViewDelegate 协议),但在 UI 定制方面具有更大的灵活性。它的所有子 UILabels、UIViews 和 FUIButtons 都可以随意定制。

FUIAlertView *alertView = [[FUIAlertView alloc ] initWithTitle: @" Hello "
message: @" This is an alert view "
delegate: nil cancelButtonTitle: @" Dismiss "
otherButtonTitles: @" Do Something " , nil ];
alertView.titleLabel.textColor = [UIColor
cloudColor ];
alertView.titleLabel.font = [UIFont
boldFlatFontOfSize: 16 ];
alertView.messageLabel.textColor = [UIColor
cloudColor ];
alertView.messageLabel.font = [UIFont
flatFontOfSize: 14 ];
alertView.backgroundOverlay.backgroundColor = [[UIColor
cloudColor ] colorWithAlphaComponent: 0.8 ];
alertView.alertContainer.backgroundColor = [的UIColor
midnightBlueColor ];
alertView.defaultButtonColor = [UIColor
cloudColor ];
alertView.defaultButtonShadowColor = [UIColor
asbestosColor ];
alertView.defaultButtonFont = [UIFont
boldFlatFontOfSize: 16 ];
alertView.defaultButtonTitleColor = [UIColor
asbestosColor ];

为了提供平面 UISlider、UIProgressViews 和 UISteppers,我们只需在 UISlider/ProgressView/UIStepper 上提供类别,以使用适当的颜色/角半径自动配置它们的外观。这有助于与您现有的项目零摩擦集成:

[mySlider configureFlatSliderWithTrackColor: [UIColor silverColor ]
progressColor: [UIColor alizarinColor ]
thumbColor: [UIColor pomegranateColor ]];

FUIS滑块

[myProgressView configureFlatProgressViewWithTrackColor: [UIColor silverColor ]
progressColor: [UIColor alizarinColor ]];

[myStepper
configureFlatStepperWithColor: [的UIColor wisteriaColor ]
highlightedColor: [的UIColor wisteriaColor ]
disabledColor: [的UIColor amethystColor ]
iconColor通过: [的UIColor cloudsColor ]];

要为整个应用程序自定义栏按钮项(包括后退按钮),UIBarButtonItem+FlatUI 提供了一个类方法,该方法利用 UIBarButtonItem 外观代理一步完成此操作:

[UIBarButtonItem configureFlatButtonsWithColor: [UIColor peterRiverColor ]
highlightColor: [UIColor belizeHoleColor ]
cornerRadius: 3 ];

但是,这可能会导致从操作表、共享表或 web 视图中的链接推送的控制器出现渲染问题。为防止这种行为,请将自定义栏按钮项的范围限定到您的控制器:

[UIBarButtonItem configureFlatButtonsWithColor: [UIColor peterRiverColor ]
highlightColor: [UIColor belizeHoleColor ]
cornerRadius: 3
whenContainedIn: [YourViewController class ]];

您可以修改 UITableViewCell 的 backgroundColor 和 selectedBackgroundColor 而不会丢失圆角。单元格将复制 UITableView 的分隔符颜色。分隔符高度显示为 separatorHeight,半径显示为 cornerRadius。

UITableViewCell *cell = ...;
[cell
configureFlatCellWithColor: [UIColor greenSeaColor ]
selectedColor: [UIColor cloudColor ]
roundingCorners: corners];

cell.cornerRadius =
50f ; //可选
cell.separatorHeight =
2 . 0f ; //可选

demo下载及常见问题:https://github.com/Grouper/FlatUIKit
源码下载:FlatUIKit.zip


收起阅读 »