注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

520和女朋友搞点不一样的礼物, html+css+js做一个网页版坦克大战游戏

坦克游戏玩法及介绍我们先来看一下首页。打开这个首页很简单,基本是上面这个样子,然后选择两个人回车就可以进行玩耍了,这个游戏需要两个人一起操作,玩家1(我): 使用WASD四个键进行上左下右方向的控制,通过space键进行设计射击。玩家2(女朋友):通过方向键上...
继续阅读 »

坦克游戏玩法及介绍

我们先来看一下首页。


打开这个首页很简单,基本是上面这个样子,然后选择两个人回车就可以进行玩耍了,这个游戏需要两个人一起操作,玩家1(我): 使用WASD四个键进行上左下右方向的控制,通过space键进行设计射击。玩家2(女朋友):通过方向键上下左右控制方向,通过enter键盘射击。基本上我控制整个电脑键盘的左边,她控制电脑键盘的右边。通过N键进行下一关,P键选择上一关。再键盘上显示如下。

演示如何进入游戏


通过方向键的下键选择两个人,然后点击回车进入游戏。也可以选择一个人进行回车进行战斗。

一个人战斗的状态。


两个人战斗的状态。


比如图中红色标记的砖头是打不破的,只能绕道走,还有只能再yellow标记的区域内操作,其它砖头用子弹就可以打破,不能让对手先打破你的大本营(我右边中间的老鹰),不然又得GG.

整个游戏规则大体是这样,下面看一下代码。

项目结构


整个项目由五部分组成,分为背景音乐、基础样式、动图、核心JS及首页静态展示。不涉及后端,纯前端实现。

index.html

<!DOCTYPE html>
<html lang="zh" class="no-js demo-1">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="js/jquery.min.js"></script>
<script src="js/Helper.js"></script>
<script src="js/keyboard.js"></script>
<script src="js/const.js"></script>
<script src="js/level.js"></script>
<script src="js/crackAnimation.js"></script>
<script src="js/prop.js"></script>
<script src="js/bullet.js"></script>
<script src="js/tank.js"></script>
<script src="js/num.js"></script>
<script src="js/menu.js"></script>
<script src="js/map.js"></script>
<script src="js/Collision.js"></script>
<script src="js/stage.js"></script>
<script src="js/main.js"></script>
<link rel="stylesheet" type="text/css" href="css/default.css" />
<style type="text/css">
#canvasDiv canvas{
position:absolute;
}
</style>
</head>
<body>
<div class="container">
<head><h3>操作说明:玩家1:WASD上左下右,space射击;玩家2:方向键,enter射击。n下一关,p上一关。</h3></head>
<div class="main clearfix">
<div id="canvasDiv" >
<canvas id="wallCanvas" ></canvas>
<canvas id="tankCanvas" ></canvas>
<canvas id="grassCanvas" ></canvas>
<canvas id="overCanvas" ></canvas>
<canvas id="stageCanvas" ></canvas>
</div>
</div>

</div><!-- /container -->
<div style="text-align:center;">
<p>来源:<a href="https://sunmenglei.blog.csdn.net/" target="_blank">孙叫兽的博客</a></p>
</div>

</body>
</html>

css

*, *:after, *:before { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; }
body, html { font-size: 100%; padding: 0; margin: 0; height: 100%;}

/* Clearfix hack by Nicolas Gallagher: http://nicolasgallagher.com/micro-clearfix-hack/ */
.clearfix:before, .clearfix:after { content: " "; display: table; }
.clearfix:after { clear: both; }

body {
font-family: "Helvetica Neue",Helvetica,Arial,'Microsoft YaHei',sans-serif,'Lato', Calibri;
color: #777;
background: #f6f6f6;
}

a {
color: #555;
text-decoration: none;
outline: none;
}

a:hover,
a:active {
color: #777;
}

a img {
border: none;
}
/* Header Style */
.main,
.container > header {
margin: 0 auto;
/*padding: 2em;*/
}

.container {
height: 100%;
}

.container > header {
padding-top: 20px;
padding-bottom: 20px;
text-align: center;
background: rgba(0,0,0,0.01);
}

.container > header h1 {
font-size: 2.625em;
line-height: 1.3;
margin: 0;
font-weight: 300;
}

.container > header span {
display: block;
font-size: 60%;
opacity: 0.3;
padding: 0 0 0.6em 0.1em;
}

/* Main Content */
.main {
/*max-width: 69em;*/
width: 100%;
height: 100%;
overflow: hidden;
}
.demo-scroll{
overflow-y: scroll;
width: 100%;
height: 100%;
}
.column {
float: left;
width: 50%;
padding: 0 2em;
min-height: 300px;
position: relative;
}

.column:nth-child(2) {
box-shadow: -1px 0 0 rgba(0,0,0,0.1);
}

.column p {
font-weight: 300;
font-size: 2em;
padding: 0;
margin: 0;
text-align: right;
line-height: 1.5;
}

/* To Navigation Style */
.htmleaf-top {
background: #fff;
background: rgba(255, 255, 255, 0.6);
text-transform: uppercase;
width: 100%;
font-size: 0.69em;
line-height: 2.2;
}

.htmleaf-top a {
padding: 0 1em;
letter-spacing: 0.1em;
color: #888;
display: inline-block;
}

.htmleaf-top a:hover {
background: rgba(255,255,255,0.95);
color: #333;
}

.htmleaf-top span.right {
float: right;
}

.htmleaf-top span.right a {
float: left;
display: block;
}

.htmleaf-icon:before {
font-family: 'codropsicons';
margin: 0 4px;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}



/* Demo Buttons Style */
.htmleaf-demos {
padding-top: 1em;
font-size: 0.9em;
}

.htmleaf-demos a {
display: inline-block;
margin: 0.2em;
padding: 0.45em 1em;
background: #999;
color: #fff;
font-weight: 700;
border-radius: 2px;
}

.htmleaf-demos a:hover,
.htmleaf-demos a.current-demo,
.htmleaf-demos a.current-demo:hover {
opacity: 0.6;
}

.htmleaf-nav {
text-align: center;
}

.htmleaf-nav a {
display: inline-block;
margin: 20px auto;
padding: 0.3em;
}
.bb-custom-wrapper {
width: 420px;
position: relative;
margin: 0 auto 40px;
text-align: center;
}
/* Demo Styles */

.demo-1 body {
color: #87968e;
background: #fff2e3;
}

.demo-1 a {
color: #72b890;
}

.demo-1 .htmleaf-demos a {
background: #72b890;
color: #fff;
}

.demo-2 body {
color: #fff;
background: #c05d8e;
}

.demo-2 a {
color: #d38daf;
}

.demo-2 a:hover,
.demo-2 a:active {
color: #fff;
}

.demo-2 .htmleaf-demos a {
background: #883b61;
color: #fff;
}

.demo-2 .htmleaf-top a:hover {
background: rgba(255,255,255,0.3);
color: #333;
}

.demo-3 body {
color: #87968e;
background: #fff2e3;
}

.demo-3 a {
color: #ea5381;
}

.demo-3 .htmleaf-demos a {
background: #ea5381;
color: #fff;
}

.demo-4 body {
color: #999;
background: #fff2e3;
overflow: hidden;
}

.demo-4 a {
color: #1baede;
}

.demo-4 a:hover,
.demo-4 a:active {
opacity: 0.6;
}

.demo-4 .htmleaf-demos a {
background: #1baede;
color: #fff;
}

.demo-5 body {
background: #fffbd6;
}
/****/
.related {
/*margin-top: 5em;*/
color: #fff;
background: #333;
text-align: center;
font-size: 1.25em;
padding: 3em 0;
overflow: hidden;
}

.related a {
display: inline-block;
text-align: left;
margin: 20px auto;
padding: 10px 20px;
opacity: 0.8;
-webkit-transition: opacity 0.3s;
transition: opacity 0.3s;
-webkit-backface-visibility: hidden;
}

.related a:hover,
.related a:active {
opacity: 1;
}

.related a img {
max-width: 100%;
}

.related a h3 {
font-weight: 300;
margin-top: 0.15em;
color: #fff;
}

@media screen and (max-width: 40em) {

.htmleaf-icon span {
display: none;
}

.htmleaf-icon:before {
font-size: 160%;
line-height: 2;
}

}

@media screen and (max-width: 46.0625em) {
.column {
width: 100%;
min-width: auto;
min-height: auto;
padding: 1em;
}

.column p {
text-align: left;
font-size: 1.5em;
}

.column:nth-child(2) {
box-shadow: 0 -1px 0 rgba(0,0,0,0.1);
}
}

@media screen and (max-width: 25em) {

.htmleaf-icon span {
display: none;
}

}

核心js

/**
* 检测2个物体是否碰撞
* @param object1 物体1
* @param object2 物体2
* @param overlap 允许重叠的大小
* @returns {Boolean} 如果碰撞了,返回true
*/
function CheckIntersect(object1, object2, overlap)
{
// x-轴 x-轴
// A1------>B1 C1 A2------>B2 C2
// +--------+ ^ +--------+ ^
// | object1| | y-轴 | object2| | y-轴
// | | | | | |
// +--------+ D1 +--------+ D2
//
//overlap是重叠的区域值
A1 = object1.x + overlap;
B1 = object1.x + object1.size - overlap;
C1 = object1.y + overlap;
D1 = object1.y + object1.size - overlap;

A2 = object2.x + overlap;
B2 = object2.x + object2.size - overlap;
C2 = object2.y + overlap;
D2 = object2.y + object2.size - overlap;

//假如他们在x-轴重叠
if(A1 >= A2 && A1 <= B2
|| B1 >= A2 && B1 <= B2)
{
//判断y-轴重叠
if(C1 >= C2 && C1 <= D2 || D1 >= C2 && D1 <= D2)
{
return true;
}
}
return false;
}

/**
* 坦克与地图块碰撞
* @param tank 坦克对象
* @param mapobj 地图对象
* @returns {Boolean} 如果碰撞,返回true
*/
function tankMapCollision(tank,mapobj){
//移动检测,记录最后一次的移动方向,根据方向判断+-overlap,
var tileNum = 0;//需要检测的tile数
var rowIndex = 0;//map中的行索引
var colIndex = 0;//map中的列索引
var overlap = 3;//允许重叠的大小

//根据tank的x、y计算出map中的row和col
if(tank.dir == UP){
rowIndex = parseInt((tank.tempY + overlap - mapobj.offsetY)/mapobj.tileSize);
colIndex = parseInt((tank.tempX + overlap- mapobj.offsetX)/mapobj.tileSize);
}else if(tank.dir == DOWN){
//向下,即dir==1的时候,行索引的计算需要+tank.Height
rowIndex = parseInt((tank.tempY - overlap - mapobj.offsetY + tank.size)/mapobj.tileSize);
colIndex = parseInt((tank.tempX + overlap- mapobj.offsetX)/mapobj.tileSize);
}else if(tank.dir == LEFT){
rowIndex = parseInt((tank.tempY + overlap- mapobj.offsetY)/mapobj.tileSize);
colIndex = parseInt((tank.tempX + overlap - mapobj.offsetX)/mapobj.tileSize);
}else if(tank.dir == RIGHT){
rowIndex = parseInt((tank.tempY + overlap- mapobj.offsetY)/mapobj.tileSize);
//向右,即dir==3的时候,列索引的计算需要+tank.Height
colIndex = parseInt((tank.tempX - overlap - mapobj.offsetX + tank.size)/mapobj.tileSize);
}
if(rowIndex >= mapobj.HTileCount || rowIndex < 0 || colIndex >= mapobj.wTileCount || colIndex < 0){
return true;
}
if(tank.dir == UP || tank.dir == DOWN){
var tempWidth = parseInt(tank.tempX - map.offsetX - (colIndex)*mapobj.tileSize + tank.size - overlap);//去除重叠部分
if(tempWidth % mapobj.tileSize == 0 ){
tileNum = parseInt(tempWidth/mapobj.tileSize);
}else{
tileNum = parseInt(tempWidth/mapobj.tileSize) + 1;
}
for(var i=0;i<tileNum && colIndex+i < mapobj.wTileCount ;i++){
var mapContent = mapobj.mapLevel[rowIndex][colIndex+i];
if(mapContent == WALL || mapContent == GRID || mapContent == WATER || mapContent == HOME || mapContent == ANOTHREHOME){
if(tank.dir == UP){
tank.y = mapobj.offsetY + rowIndex * mapobj.tileSize + mapobj.tileSize - overlap;
}else if(tank.dir == DOWN){
tank.y = mapobj.offsetY + rowIndex * mapobj.tileSize - tank.size + overlap;
}
return true;
}
}
}else{
var tempHeight = parseInt(tank.tempY - map.offsetY - (rowIndex)*mapobj.tileSize + tank.size - overlap);//去除重叠部分
if(tempHeight % mapobj.tileSize == 0 ){
tileNum = parseInt(tempHeight/mapobj.tileSize);
}else{
tileNum = parseInt(tempHeight/mapobj.tileSize) + 1;
}
for(var i=0;i<tileNum && rowIndex+i < mapobj.HTileCount;i++){
var mapContent = mapobj.mapLevel[rowIndex+i][colIndex];
if(mapContent == WALL || mapContent == GRID || mapContent == WATER || mapContent == HOME || mapContent == ANOTHREHOME){
if(tank.dir == LEFT){
tank.x = mapobj.offsetX + colIndex * mapobj.tileSize + mapobj.tileSize - overlap;
}else if(tank.dir == RIGHT){
tank.x = mapobj.offsetX + colIndex * mapobj.tileSize - tank.size + overlap;
}
return true;
}
}
}
return false;
}

/**
* 子弹与地图块的碰撞
* @param bullet 子弹对象
* @param mapobj 地图对象
*/
function bulletMapCollision(bullet,mapobj){
var tileNum = 0;//需要检测的tile数
var rowIndex = 0;//map中的行索引
var colIndex = 0;//map中的列索引
var mapChangeIndex = [];//map中需要更新的索引数组
var result = false;//是否碰撞
//根据bullet的x、y计算出map中的row和col
if(bullet.dir == UP){
rowIndex = parseInt((bullet.y - mapobj.offsetY)/mapobj.tileSize);
colIndex = parseInt((bullet.x - mapobj.offsetX)/mapobj.tileSize);
}else if(bullet.dir == DOWN){
//向下,即dir==1的时候,行索引的计算需要+bullet.Height
rowIndex = parseInt((bullet.y - mapobj.offsetY + bullet.size)/mapobj.tileSize);
colIndex = parseInt((bullet.x - mapobj.offsetX)/mapobj.tileSize);
}else if(bullet.dir == LEFT){
rowIndex = parseInt((bullet.y - mapobj.offsetY)/mapobj.tileSize);
colIndex = parseInt((bullet.x - mapobj.offsetX)/mapobj.tileSize);
}else if(bullet.dir == RIGHT){
rowIndex = parseInt((bullet.y - mapobj.offsetY)/mapobj.tileSize);
//向右,即dir==3的时候,列索引的计算需要+bullet.Height
colIndex = parseInt((bullet.x - mapobj.offsetX + bullet.size)/mapobj.tileSize);
}
if(rowIndex >= mapobj.HTileCount || rowIndex < 0 || colIndex >= mapobj.wTileCount || colIndex < 0){
return true;
}

if(bullet.dir == UP || bullet.dir == DOWN){
var tempWidth = parseInt(bullet.x - map.offsetX - (colIndex)*mapobj.tileSize + bullet.size);
if(tempWidth % mapobj.tileSize == 0 ){
tileNum = parseInt(tempWidth/mapobj.tileSize);
}else{
tileNum = parseInt(tempWidth/mapobj.tileSize) + 1;
}
for(var i=0;i<tileNum && colIndex+i < mapobj.wTileCount ;i++){
var mapContent = mapobj.mapLevel[rowIndex][colIndex+i];
if(mapContent == WALL || mapContent == GRID || mapContent == HOME || mapContent == ANOTHREHOME){
//bullet.distroy();
result = true;
if(mapContent == WALL){
//墙被打掉
mapChangeIndex.push([rowIndex,colIndex+i]);
}else if(mapContent == GRID){

}else{
isGameOver = true;
break;
}
}
}
}else{
var tempHeight = parseInt(bullet.y - map.offsetY - (rowIndex)*mapobj.tileSize + bullet.size);
if(tempHeight % mapobj.tileSize == 0 ){
tileNum = parseInt(tempHeight/mapobj.tileSize);
}else{
tileNum = parseInt(tempHeight/mapobj.tileSize) + 1;
}
for(var i=0;i<tileNum && rowIndex+i < mapobj.HTileCount;i++){
var mapContent = mapobj.mapLevel[rowIndex+i][colIndex];
if(mapContent == WALL || mapContent == GRID || mapContent == HOME || mapContent == ANOTHREHOME){
//bullet.distroy();
result = true;
if(mapContent == WALL){
//墙被打掉
mapChangeIndex.push([rowIndex+i,colIndex]);
}else if(mapContent == GRID){

}else{
isGameOver = true;
break;
}
}
}
}
//更新地图
map.updateMap(mapChangeIndex,0);
return result;
}

原文地址:https://blog.csdn.net/weixin_41937552/article/details/116559485


收起阅读 »

知乎 iOS 客户端工程化工具 Venom

前言知乎 iOS 客户端从一开始围绕问答社区到目前涵盖 Feed,会员,商业,文章,想法等多个业务线的综合内容生产与消费平台。项目的复杂程度已经在超级 App 的范畴。单周发布与业务并行开发也逐渐变成主流。同时在知乎 iOS 平台,技术选型一直也都比较开(su...
继续阅读 »

前言

知乎 iOS 客户端从一开始围绕问答社区到目前涵盖 Feed,会员,商业,文章,想法等多个业务线的综合内容生产与消费平台。项目的复杂程度已经在超级 App 的范畴。单周发布与业务并行开发也逐渐变成主流。同时在知乎 iOS 平台,技术选型一直也都比较开(sui)放(yi)。较早了引入了 Swift 进行业务开发,列表引入了需要 OC++ 的 ComponentKit 作为核心引擎。所以在这种多业务方团队,技术形态复杂,组件仓库数量多等场景下,也同样遇到了各种超级 App 团队都面临的一些问题。

问题如下:

  • 如何统一开发环境

  • 提高编译速度

  • 提高打包速度

  • 二进制组件调试

  • 多组件联合调试

  • 多组件联合打包

  • 约束组件依赖关系等

当然在思考解决上面这些问题前,知乎 iOS 项目也同样经历过组件化的工作。与众多组件化拆分方案殊途同归,进行了业务划分,主仓库代码清空,业务线及 SDK 进行独立仓库管理。引入基于路由,基于协议声明的组件间通信等机制等,这里就不多赘述了。

简介

核心介绍的项目名称为 Venom,灵感来源于电影《毒液》。Venom 的用户端是一款为开发人员打造 Mac App,应用内置了工程构建需要的全套 Ruby Gem 和 Cocoapods 等其相关构建环境。核心目标是解决工程构建,二进制构建,组件管理,调试工具等一系列开发过程中的繁琐耗时任务。


所以当一台全新的 Mac 电脑希望运行工程时, 只需要 3 步:

1、安装 Venom For Mac 客户端。
2、使用 Venom 打开工程点击 Make 按钮。
3、构建完成点击 XCode 按钮打开工程。(当然默认己装 XCode )

从此告别 ruby,cocoapods 版本不对,gem 问题,bundle 问题以及权限问题等困扰。因为构建环境内置,使得构建环境与工程师本地环境隔离,极大的降低了工程 setup 的成本。

完整的 Venom 包含了 3 个部分:

1、Venom App
2、Venom 内核
3、Venom Server


下面会着重介绍客户端和内核相关的技术方案,数据服务目前仅为组件的附加信息提供 API 支持。

Venom 内核介绍

在引入 Venom 前,一直使用 Cocoapods 的 Podfile 进行组件的引用。但如果希望对 pod 命令的 DSL 进行扩展,发现是不够方便的。索性在 Cocoapods 上层建立自己的组件描述文件,每一个描述文件最终都会被转化为一次 podfile 的 pod 调用。


如上图,使用 Venom 方式集成的项目,由在 VenomFiles 目录内的组建描述文件组成。

组件描述文件

VenomFile.new do |v|  v.name = 'XXModuleA'  v.git = 'git@git.abc.abc.com:Team-iOS-Module/XXModule.git'  v.tag = '1.0.0'  v.binary = false  v.use_module_map = true  v.XX...end

组件描述文件可以理解是 pod 命令的一个超集,在包含了 pod 的原有功能基础上,扩展其他功能(胶水代码创建,二进制化与源码切换等)。

组件调试

同时在与 VenomFile 同级别还设计了一个 Customization.yml 的文件。当开发过程中,需要对某个组件进行源码二进制的切换,或者源码路径的切换,版本引用的切换等,不会直接改动 VenomFile,会改动 Customization.yml 来进行。在构建过程中的优先,Customization.yml > Venomfile 。为了每个工程师的改动不会互相影响,Customization.yml 是非 git 托管的。而 VenomFiles 内的文件只有更新版本号或其他配置改动,才会更新。

构建过程

所有组件都通过一个个 Venomfile 文件方式管理在主工程仓库中,通过目录对组件进行层级划分管理。


原来的 Podfile 文件通过嵌入 Venom 进行构建职责的接管。


使用 Venom 后 pod install 的实际过程就如下图:


整体上来看, Venom 内核提供了一套扩展 pod 属性的描述文件,开发阶段通过 customization.yml 进行可配置的构建。构建过程中,依赖 Venomfile 文件的唯一标识进行二进制库和源码的关联。通过对 Cocoapods 构建过程的 hook 实现二进制与源码的引用切换。二进制化方案可参考 :

Xinyu Zhao:知乎 iOS 基于 CocoaPods 实现的二进制化方案

命令接口

Venom 内核除了主要的构建职责,还提供了一系列的 ipc 命令。通过这些 ipc 命令,上层的 Venom 客户端就可以更容易的操作每个组件,进行定制化的开发组织。来构建工程。

例如:

// 修改组件二进制使用方式,使用二进制venom ipc customization \    --path /Users/abc/Developer/zhihu/abc/def \    --edit \    --name ZHModuleABC \    --binary// 修改组件二进制使用方式,使用源码venom ipc customization \    --path /Users/abc/Developer/zhihu/abc/def \    --edit \    --name ZHModuleABC \    --source// 修改 yml 文件中指定组件的路径venom ipc customization \    --path /Users/abc/Developer/zhihu/abc/def \    --edit \    --name ZHModuleABC \    --pod_path /path/to/ZHModuleABC// reset 某个组件在 customization 中的 change,不指定 name 参数会给整个文件置成空venom ipc customization \    --path /xxx \    --reset \    --name ZHModuleABC

Venom App 介绍

通过对 Venom 内核的简单介绍,其实可以认为,只通过命令行版的工具,就可以达到用到的大部分功能。但因为实际开发情况一般不会一个人一次只处理一个模块,所以希望以一种所见即所得方式来让业务工程师不用关心下层的逻辑,学习命令。可以快速建立起开发环境是我们的主要目标。


<center style="color: rgb(74, 74, 74); font-family: Avenir, Tahoma, Arial, "PingFang SC", "Lantinghei SC", "Microsoft Yahei", "Hiragino Sans GB", "Microsoft Sans Serif", "WenQuanYi Micro Hei", Helvetica, sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">客户端主要模块</center>

Venom App 内置了全套的 guby gem 环境来运行命令。通过 CLITask 来访问 Venom-core 以及 git 等其他命令(venom 内核一样内置在 Venom App 内)。


核心功能

开发组件关联

正常情况下 clone 下来的主工程(壳工程)内是没有代码的,只有空的工程文件和组件描述文件。Venom 工具划分了 2 个区域,普通组件和定制组件。


因为每个开发者维护的组件其实是有限的几个,一般都会将源码放在固定目录,所以通过设置客户端的自动扫描路径。在 Venom 界面上,如果在扫码路径下发现了相关组件,则可以一键关联本地目录组件,这样组件会切换到定制组件的模式进行开发。

特定版本关联


在开发过程中,有时需要对某一个依赖库的特定版本进行调试或连调。所以也支持通过 tag,commit,branch 等方式,进行特定源码的切换和关联。

源码与二进制切换


某些特殊场景下,可能希望工程以所有组件都是源代码方式构建,排查问题。那么也可以通过 2 种不同的构建模式一键切换。(当然全源码构建一次需要十足的耐心)

二进制模式下搜索与调试

二进制化后,大部分情况下都工作在二进制模式下,但有时在进行源码搜索时,希望可以全局搜索。所以在构建过程中,会把当前版本的源码目录也引用到工程目录下。


所以在工程进行检索代码时,是完全没问题的。有了源码,在云端进行二进制打包时,通过 fdebug-prefix-map ( Clang command line argument reference )这个参数重新在二进制文件中改写 Debug 模式的源代码路径。这样即使在二进制模式下,也可以直接关联源码进行断点调试。


组件依赖关系分析


当组件很多后,就会出现一些工程师对组件所处层级不够了解,导致出现依赖混乱的问题。所以在构建结束后会通过对组件层级的检查,进行组件依赖层级的判断。


总结

在推进所有工程师使用 Venom 客户端后,相当于在开发环节有了一个强有力的抓手。由于 App 的自动更新功能,可以在平台下提供给开发者更多的工具,而开发者只需要更新客户端使用。通过工具化客户端的开发,我们重构了原有散落在各处的脚步,工具集中整合在一起。使得开发工具维护更统一,更新也更及时,开发人员上手成本也更低。

Venom 核心承担的是开发环境管理,工程组织与构建管理,提高工程效率工作。但上线后,我们还陆续在此基础上提供了一些其他功能。

1、多仓库 MR 自动填充提交
2、本地非独立业务仓库单元测试
3、个人开发者账号真机调试
4、无用图片扫描工具
5、轻量的 app 网络和日志查看等

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

收起阅读 »

NestedScrollView嵌套滑动源码解读!

1、前言滑动对于android来说,是一个必不可少;它不复杂,大家都知道在onTouchEvent中,让它滑动就完事了,说它复杂,其嵌套处理复杂;在本系列文章,最终是为了熟悉嵌套滑动机制;对于滑动,分为下面几篇文章来完成解读:滑动基础ScrollView滑动源...
继续阅读 »

1、前言

滑动对于android来说,是一个必不可少;它不复杂,大家都知道在onTouchEvent中,让它滑动就完事了,说它复杂,其嵌套处理复杂;在本系列文章,最终是为了熟悉嵌套滑动机制;对于滑动,分为下面几篇文章来完成解读:

在本章内,本章从两个嵌套的两个视角来分析

  1. 子滑动视图视角:涉及NestedScrollingChild3接口以及NestedScrollingChildHelper辅助类
  2. 父滑动容器视角:涉及NestedScrollingParent3接口以及NestedScrollingParentHelper辅助类

这篇内容分三个小章节

  1. NestedScrollingChildHelper类
  2. NestedScrollingParentHelper类
  3. 实现处理以及调用时机

在这里类的解读是必须的,不然只能死记其调用时机,这里是不建议的;下面会贴一部分源码,在源码中会对代码的一些关键进行注释说明

2、NestedScrollingChildHelper类

嵌套子视图角色;主要功能

  • 事件是否需要通知
  • 事件通知

类中如下变量:

    private ViewParent mNestedScrollingParentTouch; // touch事件接力的父容器
private ViewParent mNestedScrollingParentNonTouch; // 非touch事件接力的父容器
private final View mView; // 当前容器,也是作为嵌套滑动时孩子角色的容器
private boolean mIsNestedScrollingEnabled; // 当前容器是否支持嵌套滑动
private int[] mTempNestedScrollConsumed; // 二维数组,保存x、y消耗的事件长度;减少对象生成的
复制代码

2.1 实例获取

    public NestedScrollingChildHelper(@NonNull View view) {
mView = view;
}
复制代码

2.2 嵌套滑动支持

是对嵌套子视图的角色来说的

    public void setNestedScrollingEnabled(boolean enabled) {
if (mIsNestedScrollingEnabled) {
ViewCompat.stopNestedScroll(mView); // 兼容模式调用
}
mIsNestedScrollingEnabled = enabled;
}

public boolean isNestedScrollingEnabled() {
return mIsNestedScrollingEnabled;
}
复制代码

2.3 嵌套滑动相关方法

要支持嵌套滑动,那么必须有多个支持嵌套滑动的容器;作为子视图,其需要有通知的一套,因此方法有:

  • 父容器的查找、判断
  • 通知开始、过程以及结束

2.3.1 嵌套父容器的查找

成员变量mNestedScrollingParentTouch、mNestedScrollingParentNonTouch为父容器缓存变量;其直接设置和获取方法如下

  private ViewParent getNestedScrollingParentForType(@NestedScrollType int type) {
switch (type) {
case TYPE_TOUCH:
return mNestedScrollingParentTouch;
case TYPE_NON_TOUCH:
return mNestedScrollingParentNonTouch;
}
return null;
}

private void setNestedScrollingParentForType(@NestedScrollType int type, ViewParent p) {
switch (type) {
case TYPE_TOUCH:
mNestedScrollingParentTouch = p;
break;
case TYPE_NON_TOUCH:
mNestedScrollingParentNonTouch = p;
break;
}
}
复制代码

2.3.2 嵌套父容器的支持判断

    public boolean hasNestedScrollingParent() {
return hasNestedScrollingParent(TYPE_TOUCH);
}

public boolean hasNestedScrollingParent(@NestedScrollType int type) {
return getNestedScrollingParentForType(type) != null;
}
复制代码

2.3.3 滑动开始通知

    public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
return true;
}
if (isNestedScrollingEnabled()) { // 孩子视图支持嵌套滑动,只有支持才会继续执行
ViewParent p = mView.getParent();
View child = mView;
while (p != null) { // 查找的不仅仅直接父容器
// 兼容调用,父容器是否可以作为嵌套父容器角色
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p); // 这里进行了缓存
// 兼容调用,父容器
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
复制代码

父容器的查找,采取了延时策略,在进行事件时,才进行查询,并且在查询到了,进行支持;所以可以这样理解:

  1. onStartNestedScroll:是父容器接受事件通知方法,其结果表示是否可以作为嵌套滑动的父容器角色
  2. onNestedScrollAccepted:不是必调用,调用了表明嵌套父容器角色支持view的后续嵌套处理

2.3.4 手指滑动通知

滑动时通知,分为滑动前和滑动后;使嵌套滑动处理更灵活 滑动前通知

    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow) {
return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, TYPE_TOUCH);
}

public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}

if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}

if (consumed == null) {
consumed = getTempNestedScrollConsumed();
}
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(parent, mView, dx, dy, consumed, type);

if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
复制代码

其中两个二维数组作为结果回传;通过父容器的onNestedPreScroll方法进行处理并把滑动处理详情放入两个二维数组中,常用的详情为消耗长度情况;返回结果表示滑动前是否处理

滑动后通知

    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
return dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow, TYPE_TOUCH, null);
}

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type) {
return dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow, type, null);
}

public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, @Nullable int[] offsetInWindow, @NestedScrollType int type,
@Nullable int[] consumed) {
dispatchNestedScrollInternal(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
offsetInWindow, type, consumed);
}

private boolean dispatchNestedScrollInternal(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
@NestedScrollType int type, @Nullable int[] consumed) {
if (isNestedScrollingEnabled()) {
final ViewParent parent = getNestedScrollingParentForType(type);
if (parent == null) {
return false;
}

if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}

if (consumed == null) {
consumed = getTempNestedScrollConsumed();
consumed[0] = 0;
consumed[1] = 0;
}

ViewParentCompat.onNestedScroll(parent, mView,
dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);

if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return true;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
复制代码

其中两个二维数组作为结果回传;通过父容器的onNestedScroll方法进行处理并把滑动处理详情放入两个二维数组中,常用的详情为消耗长度情况;返回结果表示滑动前是否处理

2.3.5 滑翔通知

滑翔也有两个时机

滑翔前

   public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
if (isNestedScrollingEnabled()) {
ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
if (parent != null) {
return ViewParentCompat.onNestedPreFling(parent, mView, velocityX,
velocityY);
}
}
return false;
}
复制代码

返回结果表明父容器的是否处理滑翔;父容器是通过onNestedPreFling进行处理

滑翔后

  public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
if (isNestedScrollingEnabled()) {
ViewParent parent = getNestedScrollingParentForType(TYPE_TOUCH);
if (parent != null) {
return ViewParentCompat.onNestedFling(parent, mView, velocityX,
velocityY, consumed);
}
}
return false;
}
复制代码

返回结果表明父容器的是否处理滑翔;父容器是通过onNestedFling进行处理

滑翔是一个互斥处理的过程,而滑动是一个接力的过程

2.3.6 滑动结束通知

    public void stopNestedScroll() {
stopNestedScroll(TYPE_TOUCH);
}

public void stopNestedScroll(@NestedScrollType int type) {
ViewParent parent = getNestedScrollingParentForType(type);
if (parent != null) {
// 通知嵌套父容器,滑动结束
ViewParentCompat.onStopNestedScroll(parent, mView, type);
setNestedScrollingParentForType(type, null); // 清理父容器引用
}
}
复制代码

3、NestedScrollingParentHelper类

作为嵌套滑动的父容器角色,其只有接受通知时处理即可,情况没有子视图角色那么复杂;而辅助类里仅仅是对滑动方向做了声明周期处理;

成员变量

    private int mNestedScrollAxesTouch; // Touch事件时,接受处理时,事件的滑动方法
private int mNestedScrollAxesNonTouch; // 非Touch事件时,接受处理时,事件的滑动方法
复制代码

3.1 滑动方向获取

    public int getNestedScrollAxes() {
return mNestedScrollAxesTouch | mNestedScrollAxesNonTouch;
}
复制代码

3.2 滑动方向设置

    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
@ScrollAxis int axes) {
onNestedScrollAccepted(child, target, axes, ViewCompat.TYPE_TOUCH);
}

public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
@ScrollAxis int axes, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_NON_TOUCH) {
mNestedScrollAxesNonTouch = axes;
} else {
mNestedScrollAxesTouch = axes;
}
}
复制代码

3.3 滑动方向重置

   public void onStopNestedScroll(@NonNull View target) {
onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
}

public void onStopNestedScroll(@NonNull View target, @NestedScrollType int type) {
if (type == ViewCompat.TYPE_NON_TOUCH) {
mNestedScrollAxesNonTouch = ViewGroup.SCROLL_AXIS_NONE;
} else {
mNestedScrollAxesTouch = ViewGroup.SCROLL_AXIS_NONE;
}
}
复制代码

4、嵌套实现机制

作为一是具有兼容性实现的嵌套滑动容器,它必须实现下面接口

  • 滑动容器接口ScrollingView
  • 嵌套滑动父容器接口NestedScrollingParent3
  • 嵌套滑动子视图接口NestedScrollingChild3

嵌套接口,可以根据容器角色选择实现;方法实现需要利用辅助类

从上面对两个辅助类解读;对他们已经实现的功能做了归纳

  1. 嵌套是否支持
  2. 嵌套通知
  3. 嵌套滑动方向

也就是作为子视图角色的实现方法基本使用辅助类即可,而嵌套父容器角色需要我们增加实现逻辑;需要实现从功能上划分:

  1. 作为嵌套子视图设置,
  2. 作为嵌套父容器的实现
  3. 滑动接力处理,以及滑翔处理

4.1 嵌套子视图支持

构造器中进行setNestedScrollingEnabled(true)方法进行设置

setNestedScrollingEnabled方法

    public void setNestedScrollingEnabled(boolean enabled) {
mChildHelper.setNestedScrollingEnabled(enabled);
}
复制代码

4.2 嵌套父容器的支持

    public boolean onStartNestedScroll(
@NonNull View child, @NonNull View target, int nestedScrollAxes) {
return onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
}

public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
复制代码

可滑动方向判断进而决定是否支持的;支持时的处理如下

    public void onNestedScrollAccepted(
@NonNull View child, @NonNull View target, int nestedScrollAxes) {
onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH);
}

public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes,
int type) {
mParentHelper.onNestedScrollAccepted(child, target, axes, type);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type);
}
复制代码

其还是一个子视图角色,所以,其需要继续传递这个滑动开始的信号;可见嵌套默认处理中:其实是一个嵌套滑动容器链表,中间也可能存在滑动容器(不支持嵌套),链表组后一个容器的‘父’容器也还可能是嵌套滑动;这些情况造成的一个原因是同时是父容器还是子视图才会继续分发;这个链头容器必定是个嵌套子视图角色,中间即是子视图角色也是父容器角色,链尾容器必定是个嵌套父容器角色

时机

在down事件中,调用startNestedScroll方法

4.3 利用辅助类重写

下面方法利用了辅助类直接重写

  • 嵌套父容器存在判断:hasNestedScrollingParent
  • 子视图是否支持嵌套滑动:setNestedScrollingEnabled、isNestedScrollingEnabled
  • 开始通知:startNestedScroll
  • 滑动分发:dispatchNestedPreScroll、dispatchNestedScroll
  • 滑翔分发:dispatchNestedPreFling、dispatchNestedFling
  • 结束通知:stopNestedScroll

参数中涉及到滑动类型时,均采用ViewCompat.TYPE_TOUCH作为默认类型

4.4 滑动接力处理

    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH);
}

public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
int type) {
dispatchNestedPreScroll(dx, dy, consumed, null, type);
}
复制代码

其作为父容器,本身对事件并没有处理,而是作为子视图继续分发下去;时机move事件中嵌套子视图处理滑动之前

    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
onNestedScrollInternal(dyUnconsumed, ViewCompat.TYPE_TOUCH, null);
}

private void onNestedScrollInternal(int dyUnconsumed, int type, @Nullable int[] consumed) {
final int oldScrollY = getScrollY();
scrollBy(0, dyUnconsumed);
final int myConsumed = getScrollY() - oldScrollY;

if (consumed != null) {
consumed[1] += myConsumed;
}
final int myUnconsumed = dyUnconsumed - myConsumed;

mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed);
}
复制代码

父容器首先处理了滑动,然后把处理后的情况继续传递;时机move事件,嵌套子视图处理之后

4.5 滑翔互斥处理

    public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}

public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
复制代码

不进行处理,而是做为嵌套子视图继续分发;时机up事件,拦截时,嵌套子视图处理之前

    public boolean onNestedFling(
@NonNull View target, float velocityX, float velocityY, boolean consumed) {
if (!consumed) {
dispatchNestedFling(0, velocityY, true);
fling((int) velocityY);
return true;
}
return false;
}
复制代码

如果接受到通知时,未处理,则进行处理;并做为嵌套子view继续通知处理;时机up事件,拦截时,嵌套子视图处理之后

4.6 滑动结束

    public void onStopNestedScroll(@NonNull View target) {
onStopNestedScroll(target, ViewCompat.TYPE_TOUCH);
}
public void onStopNestedScroll(@NonNull View target, int type) {
mParentHelper.onStopNestedScroll(target, type);
stopNestedScroll(type);
}
public void stopNestedScroll(int type) {
mChildHelper.stopNestedScroll(type);
}
复制代码

由于还是嵌套子视图角色,还需要通知其处理的嵌套父容器结束;时机up、cancel事件时

4.7 嵌套子视图优先处理

android中,从容器的默认拦截机制来看,父容器优先拦截;但是嵌套时做了额外判断,

滑动事件拦截中是这样判断的

yDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0)
复制代码

滑动的坐标轴为0,也就是既不是x轴、也不是y轴;这说明,它作为嵌套父容器时,没有嵌套子容器传递给它;

另外如果滑动已经被拦截处理,则不希望其它进行再次拦截;这时由于嵌套拦截体系已经提供了交互的方法,如果不这样处理,就会导致和默认的事件机制冲突;因此,如果有这种情况,那就把重写父容器,让其支持嵌套滑动吧

5 小结

总的来说,嵌套滑动呢,它抽象了接口和辅助类,来帮助开发者进行实现;其中实现的核心思触发点

  1. 嵌套的组织关系
  2. 嵌套的互相通知处理
  3. 自己处于角色中,是否需要处理以及如何处理
收起阅读 »

vue 事件总线EventBus的概念、使用以及注意点

前言vue组件中的数据传递最最常见的就是父子组件之间的传递。父传子通过props向下传递数据给子组件;子传父通过$emit发送事件,并携带数据给父组件。而有时两个组件之间毫无关系,或者他们之间的结构复杂,如何传递数据呢?这时就要用到 vue 中的事件总线 Ev...
继续阅读 »

前言

vue组件中的数据传递最最常见的就是父子组件之间的传递。父传子通过props向下传递数据给子组件;子传父通过$emit发送事件,并携带数据给父组件。而有时两个组件之间毫无关系,或者他们之间的结构复杂,如何传递数据呢?这时就要用到 vue 中的事件总线 EventBus的概念

正文

EventBus的简介

EventBus又称事件总线,相当于一个全局的仓库,任何组件都可以去这个仓库里获取事件,如图:


EventBus的使用

废话不多说,直接开始使用EventBus

一、初始化

要用EventBus,首先要初始化一个EventBus,这里称它为全局事件总线。

  • 第一种初始化方法
import Vue from 'vue'
//因为是全局的一个'仓库',所以初始化要在全局初始化
const EventBus = new Vue()
  • 第二种初始化方法(本文选用这种初始化方法)
//在已经创建好的Vue实例原型中创建一个EventBus
Vue.prototype.$EventBus = new Vue()

二、向EventBus发送事件

发送事件的语法:this.$EventBus.$emit(发送的事件名,传递的参数)

已经创建好EventBus后我们就需要向它发送需要传递的事件,以便其他组件可以向EventBus获取。
例子:有两个组件A和B需要通信,他们不是父子组件关系,B事件需要获得A事件里的一组数据data

<!-- A.vue 这里是以模块化的方式讲解的,即A组件和B组件分别各自
一个.vue文件,所以代码中会有导入的语法-->

<template>
<button @click="sendMsg">发送MsgA</button>
</template>

<script>
export default {
data(){
return{
MsgA: 'A组件中的Msg'
}
},
methods: {
sendMsg() {
/*调用全局Vue实例中的$EventBus事件总线中的$emit属性,发送事
件"aMsg",并携带A组件中的Msg*/
this.$EventBus.$emit("aMsg", this.MsgA);
}
}
};
</script>

三、接收事件

接收事件的语法:this.$EventBus.$on(监听的事件名, 回调函数)

A组件已经向全局事件总线EventBus发送了一个aMsg事件,这时B组件就可以去aMsg监听这个事件,并可以获得一些数据。

<!-- B.vue -->

<template>

<!-- 展示msgB -->
<p>{{msgB}}</p>

</template>

<script>
export default {
data(){
return {
//初始化一个msgB
msgB: ''
}
},
mounted() {
/*调用全局Vue实例中的$EventBus事件总线中的$on属性,监听A组件发送
到事件总线中的aMsg事件*/
this.$EventBus.$on("aMsg", (data) => {
//将A组件传递过来的参数data赋值给msgB
this.msgB = data;
});
}
};
</script>

B组件展示结果:A组件中的Msg


这样,B组件就轻松接收到了A组件传递过来的参数,并成功展示了该参数,这样是不是就很简单的解决了各组件之间的通讯呢?虽然EventBus是一个很轻便的方法,任何数据都可以往里传,然后被别的组件获取,但是如果用不好,容易出现很严重的BUG,所以接下来我们就来讲解一下移除监听事件。

四、移除监听事件

在上一个例子中,我们A组件向事件总线发送了一个事件aMsg并传递了参数MsgA,然后B组件对该事件进行了监听,并获取了传递过来的参数。但是,这时如果我们离开B组件,然后再次进入B组件时,又会触发一次对事件aMsg的监听,这时时间总线里就有两个监听了,如果反复进入B组件多次,那么就会对aMsg进行多次的监听。

总而言之,A组件只向EventBus发送了一次事件,但B组件却进行了多次监听,EventBus容器中有很多个一模一样的事件监听器这时就会出现,事件只触发一次,但监听事件中的回调函数执行了很多次

  • 解决办法:在组件离开,也就是被销毁前,将该监听事件给移除,以免下次再重复创建监听
  • 语法:this.$EventBus.$off(要移除监听的事件名)
<!-- B.vue -->

<template>

<!-- 展示msgB -->
<p>{{msgB}}</p>

</template>

<script>
export default {
data(){
return {
//初始化一个msgB
msgB: ''
}
},
mounted() {
/*调用全局Vue实例中的$EventBus事件总线中的$on属性,监听A组件发送
到事件总线中的aMsg事件*/
this.$EventBus.$on("aMsg", (data) => {
//将A组件传递过来的参数data赋值给msgB
this.msgB = data;
});
},
beforeDestroy(){
//移除监听事件"aMsg"
this.$EventBus.$off("aMsg")
}
};
</script>

结束语

好了,对于vue中的事件总线的讲解就到这里了,这也是我今天在做项目时用到的一个小知识点,接下来附上一张我因为没有及时移除事件监听,导致我每重进组件一次就报错48条错误信息的图,希望大家在我的文章中能血啊都一些东西,并且不要再犯我的这种低级错误。


本文链接:https://blog.csdn.net/l_ppp/article/details/105924658

收起阅读 »

无废话快速上手React路由

本文以简洁为目标,帮助快速上手react-router-dom默认你接触过路由相关的开发安装输入以下命令进行安装:// npmnpm install react-router-dom// yarnyarn add react-router-domreact-r...
继续阅读 »

本文以简洁为目标,帮助快速上手react-router-dom默认你接触过路由相关的开发

安装

输入以下命令进行安装:

// npm
npm install react-router-dom

// yarn
yarn add react-router-dom

react-router相关标签

react-router常用的组件有以下八个:

import { 
BrowserRouter,
HashRouter,
Route,
Redirect,
Switch,
Link,
NavLink,
withRouter,
} from 'react-router-dom'

简单路由跳转

实现一个简单的一级路由跳转

import { 
BrowserRouter as Router,
Route,
Link
} from 'react-router-dom'
import Home from './home'
import About from './about'

function App() {
return (
<div className="App">
<Router>
<Link to="/home" className="link">跳转Home页面</Link>
<Link to="/about" className="link">跳转About页面</Link>
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
</Router>
</div>
);
}

export default App;

要点总结:

  1. Route组件必须在Router组件内部
  2. Link组件的to属性的值为点击后跳转的路径
  3. Route组建的path属性是与Link标签的to属性匹配的; component属性表示Route组件匹配成功后渲染的组件对象

嵌套路由跳转

React 的路由匹配层级是有顺序的

例如,在 App 组件中,设置了两个路由组件的匹配路径,分别是 /home 和 /about,代码如下:

import { 
BrowserRouter as Router,
Route,
Link,
} from 'react-router-dom'
import Home from './home'
import About from './about'

function App() {

return (
<div className="App">
<Router>
<Link to="/home">跳转Home页面</Link>
<Link to="/about">跳转About页面</Link>

<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>

</Router>
</div>
);
}

export default App;

然后 Home 组件中同样也想设置两个路由组件的匹配路径,分别是 /home/one 和 /home/two,此时就可以看出,这个 /home/one 和 /home/two 为上一级路由 /home 的二级嵌套路由,代码如下:

import React from 'react'
import {
Route,
Link,
} from 'react-router-dom'
import One from './one'
import Two from './two'

function Home () {

return (
<>
我是Home页面
<Link to="/home/one">跳转到Home/one页面</Link>
<Link to="/home/two">跳转到Home/two页面</Link>

<Route path="/home/one" component={One}/>
<Route path="/home/two" component={Two}/>
</>
)
}

export default Home

特别注意: Home 组件中的路由组件 One 的二级路由路径匹配必须要写 /home/one ,而不是 /one ,不要以为 One 组件看似在 Home 组件内就可以简写成 /one

动态链接

NavLink可以将当前处于active状态的链接附加一个active类名,例如:

import { 
BrowserRouter as Router,
Route,
NavLink
} from 'react-router-dom'
import Home from './home'
import About from './about'

function App() {
return (
<div className="App">
<Router>
<NavLink to="/home" className="link">跳转Home页面</NavLink>
<NavLink to="/about" className="link">跳转About页面</NavLink>
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
</Router>
</div>
);
}

export default App;
/* 设置active类的样式 */
.active {
font-weight: blod;
color: red;
}

路由匹配优化

当点击跳转链接时,会自动去尝试匹配所有的Route对应的路径:

正常情况下,只需匹配到一个规则,渲染即可,即匹配成功一个后,无需进行后续的匹配尝试,此时可以用Switch组件,如下所示:

import { 
BrowserRouter as Router,
Route,
NavLink,
Switch,
} from 'react-router-dom'
import Home from './home'
import About from './about'

function App() {
return (
<div className="App">
<Router>
<NavLink to="/home" className="link">跳转Home页面</NavLink>
<NavLink to="/about" className="link">跳转About页面</NavLink>

<Switch>
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Route path="/home" component={Home}/>
{/* 此处省略一万个Route组件 */}
<Route path="/home" component={Home}/>
</Switch>

</Router>
</div>
);
}

export default App;

要点总结:

  1. 将多个Route组件同时放在一个Switch组件中,即可避免多次无意义的路由匹配,以此提升性能

重定向

当页面跳转时,若跳转链接没有匹配上任何一个 Route 组件,那么就会显示 404 页面,所以我们需要一个重定向组件 Redirect ,代码如下:

import { 
BrowserRouter as Router,
Route,
NavLink,
Switch,
Redirect,
} from 'react-router-dom'
import Home from './home'
import About from './about'

function App() {
return (
<div className="App">
<Router>
<NavLink to="/home" className="link">跳转Home页面</NavLink>
<NavLink to="/about" className="link">跳转About页面</NavLink>
<NavLink to="/shop" className="link">跳转Shop页面</NavLink> {/* 点击,跳转到/shop,但该路径没有设置 */}

<Switch>
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
<Redirect to="/home" /> {/* 当以上Route组件都匹配失败时,重定向到/home */}
</Switch>

</Router>
</div>
);
}

export default App;

路由传参

所有路由传递的参数,都会在跳转路由组件的 props 中获取到,每种传参方式接收的方式略有不同

路由传参的方式一共有三种,依次来看一下

第一种

第一种是在 Link 组件的跳转路径上携带参数,并在 Route 组件的匹配路径上通过 :参数名 的方式接收参数,代码如下:

import { 
BrowserRouter as Router,
Route,
NavLink,
Switch,
} from 'react-router-dom'
import Home from './home'
import About from './about'

function App() {
return (
<div className="App">
<Router>
{/* 在 /home 的路径上携带了 张三、18 共两个参数 */}
<NavLink to="/home/张三/18" className="link">跳转Home页面</NavLink>
<NavLink to="/about" className="link">跳转About页面</NavLink>

<Switch>
{/* 在 /home 匹配路径上相同的位置接收了 name、age 两个参数 */}
<Route path="/home/:name/:age" component={Home}/>
<Route path="/about" component={About}/>
</Switch>

</Router>
</div>
);
}

export default App;

第二种

第二种方式就是通过在 Link 组件的跳转链接后面跟上以 ? 开头,类似 ?a=1&b=3 这样的参数进行传递,代码如下:

import { 
BrowserRouter as Router,
Route,
NavLink,
Switch,
} from 'react-router-dom'
import Home from './home'
import About from './about'

function App() {
return (
<div className="App">
<Router>
{/* 在跳转路径后面以?开头传递两个参数,分别为name=张三、age=18 */}
<NavLink to="/home?name=张三&age=18" className="link">跳转Home页面</NavLink>
<NavLink to="/about" className="link">跳转About页面</NavLink>

<Switch>
{/* 此处无需做接收操作 */}
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
</Switch>

</Router>
</div>
);
}

export default App;

第三种

第三种方式就是以对象的形式编写 Link 组件的 to 跳转属性,并通过 state 属性来传递参数,代码如下:

import { 
BrowserRouter as Router,
Route,
NavLink,
Switch,
} from 'react-router-dom'
import Home from './home'
import About from './about'

function App() {
return (
<div className="App">
<Router>
{/* 以对象的形式描述to属性,路径属性名为pathname,参数属性名为state */}
<NavLink to={{pathname: "/home", state: {name: '张三', age: 18}}} className="link">跳转Home页面</NavLink>
<NavLink to="/about" className="link">跳转About页面</NavLink>

<Switch>
{/* 此处无需特地接收属性 */}
<Route path="/home" component={Home}/>
<Route path="/about" component={About}/>
</Switch>

</Router>
</div>
);
}

export default App;

函数式路由

以上主要都是通过 react-router-dom 中的 Link 组件来往某个路由组件跳转

但有时,我们需要更灵活的方式进行跳转路由,例如通过调用一个函数,随时随地进行路由跳转,这就叫函数式路由

函数式路由用到的方法有以下 5 个

push

push 方法就是使页面跳转到对应路径,并在浏览器中留下记录(即可以通过浏览器的回退按钮,返回上一个页面)

举个例子:在路由组件 Home 中设置一个按钮 button ,点击后调用 push 方法,跳转到 /about 页面

import React from 'react'

function Home (props) {

let pushLink = () => {
props.history.push('/about')
}

return (
<div className="a">
我是Home页面
<button onClick={pushLink}>跳转到about页面</button>
</div>
)
}

export default Home

replace

replace 方法与 push 方法类似,不一样的地方就是,跳转后不会在浏览器中保存上一个页面的记录(即无法通过浏览器的回退按钮,返回上一个页面)

改动一下代码

import React from 'react'

function Home (props) {

let replaceLink = () => {
props.history.replace('/about')
}

return (
<div className="a">
我是Home页面
<button onClick={replaceLink}>跳转到about页面</button>
</div>
)
}

export default Home

goForward

调用 goForward 方法,就相当于点击了浏览器的返回下一个页面按钮:


goBack

调用 goBack 方法,就相当于点击了浏览器的返回上一个页面的按钮,如下图所示:


go

go 方法顾名思义,是用于跳转到指定路径的。

该方法接受一个参数(参数类型为 Number),情况如下:

  1. 当参数为正数 n 时,表示跳转到下 n 个页面。例如 go(1) 相当于调用了一次 goForward 方法
  2. 当参数为负数 n 时,表示跳转到上 n 个页面。例如 go(-3) 相当于调用了三次 goBack 方法
  3. 当参数为 0 时,表示刷新当前页面


收起阅读 »

vue数据可视化界面,智慧图表。Echarts,以及git

一、数据图表一张图表胜过千万句话1.1HighChart概念兼容 IE6+、完美支持移动端、图表类型丰富、方便快捷的 HTML5 交互性图表库下载一、通过CDNhttps://code.highcharts.com.cn/index.html二、通过NPM下载...
继续阅读 »

一、数据图表

一张图表胜过千万句话

1.1HighChart

  • 概念
兼容 IE6+、完美支持移动端、图表类型丰富、方便快捷的 HTML5 交互性图表库

下载

一、通过CDN
https://code.highcharts.com.cn/index.html
二、通过NPM下载(用的比较多)
npm install highcharts
三、通过官网下载
https://www.highcharts.com.cn/download
通过引入库的方式引入到本地

基本应用







Document










1.2Echarts(用的更多一些)

一、通过CDN 
jsdelivr.com/package/npm/echarts
二、通过NPM(通过NPM)
npm install echarts
三、通过官网
https://echarts.apache.org/zh/download.html
四、通过github
https://github.com/apache/echarts/releases

1.3如何在vue脚手架中引入Echarts

  • 局部引入




全局引入
//全局引入echart
import * as echarts from 'echarts'
Vue.prototype.$echarts = echarts

home.vue





二、git代码管理

2.1代码管理工具

svn (小乌龟)
https://tortoisesvn.net/
git (命令)
github(所有开源项目的归属地)
https://github.com/
码云
https://gitee.com/
git软件
https://git-scm.com/

无论是gihub还是码云,他们都是用git命令去操作的。所以命令都一样

git软件的安装,下一步,下一步,傻瓜式安装即可

装成功的状态: 鼠标右键看到 git Bash Here 就OK

2.2创建一个远程仓库 (新项目)

先去GitHub/码云创建一个新的远程仓库,然后把本地暂缓区的内容提交到远程仓库

一、登录github/码云输入用户名密码
二、新建一个远程仓库,在官网右上角(点击+ )
三、创建一个仓库名称,添加仓库描述,创建一个公有的仓库,不需要为仓库创建其他内容

在公司的创建一个新项目的骚操作

一般这一部分,轮不到大家去做。

一、在本地创建一个文件夹,创建相关的基本骨架
二、初始化当前文件夹变成本地仓库(会出现一个.git的隐藏文件)
git init
三、本地的所有内容上传到暂缓区
git add .
四、提交的时候要做记录
git commit -m '尽量写英文,非要写写中文也可以'
五、链接远程仓库
git remote add origin https://gitee.com/zhangzhangzhangdada/shop-admin.git
六、把暂缓区的内容推送到远程仓库 (master 默认的分支名字)
git push -u origin master

原文:https://blog.csdn.net/weixin_49030317/article/details/116666179

收起阅读 »

JavaScript解密之旅-----数组的遍历方法总结

数组的循环    一、forEach()    二、map()    三、filter()    四...
继续阅读 »

数组的循环
    一、forEach()
    二、map()
    三、filter()
    四、reduce()与reduceRight()
    五、every()
    六、some()
    七、find()与findIndex()
    八、 for in
    九、 for of
    十、 for    
总结
数组的循环
一、forEach()
对数组进行遍历循环,对数组中的每一项运行给定函数。这个方法没有返回值。
参数都是function类型,默认有传参,参数分别为:遍历的数组内容;第对应的数组索引,数组本身。

var arr = [1, 2, 3, 4, 5];
arr.forEach(function (item, index, arr) {
console.log(item, index, arr);
// item:遍历的数组内容,index:第对应的数组索引,arr:数组本身。
});

二、map()

  • 指“映射”,对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组
  • 参数都是function类型,默认有传参,参数分别为:遍历的数组内容;第对应的数组索引,数组本身
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.map(function (item) {
return item * item;
});
console.log(arr2); //[1, 4, 9, 16, 25]

三、filter()

  • “过滤”功能,数组中的每一项运行给定函数,返回满足过滤条件组成的数组。
  • 参数都是function类型,默认有传参,参数分别为:遍历的数组内容;第对应的数组索引,数组本身
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.filter(function (x, index) {
return x % 2 == 0 || index >= 2;
});
console.log(arr2); // [2,3,4,5]

四、reduce()与reduceRight()

  • x 是上一次计算过的值, 第一次循环的时候是数组中的第1个元素
  • y 是数组中的每个元素, 第一次循环的时候是数组的第2个元素
  • 最后一个数组本身
//  reduce()
let array = [1, 2, 3, 4, 5];
let arrayNew = array.reduce((x, y) => {
console.log("x===>" + x);
console.log("y===>" + y);
console.log("x+y===>", Number(x) + Number(y));
return x + y;
});
console.log("arrayNew", arrayNew); // 15
console.log(array); // [1, 2, 3, 4, 5]
// reduceRight() 只是执行数组顺序为倒序

五、every()

  • 判断数组中每一项都是否满足条件,只有所有项都满足条件,才会返回true否是为false
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.every(function (x) {
return x < 8;
});
console.log(arr2); //true
var arr3 = arr.every(function (x) {
return x < 3;
});
console.log(arr3); // false

六、some()

  • 判断数组中是否存在满足条件的项,只要有一项满足条件,就会返回true,否则为false
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.some(function(x) {
return x < 3;
});
console.log(arr2); //true
var arr3 = arr.some(function(x) {
return x > 6;
});
console.log(arr3); // false

七、find()与findIndex()

  • 该方法主要应用于查找第一个符合条件的数组元素。它的参数是一个回调函数。
  • 在回调函数中可以写你要查找元素的条件,当条件成立为true时,返回该元素。如果没有符合条件的元素,返回值为undefined。findIndex返回-1
// find()
let arr = [1, 2, 3, 4, 5];
let res = arr.find(function (val, index, arr) {
return val > 3;
});
console.log(res); //4
// findIndex
let arr = [1, 2, 3, 4, 5];
let res = arr.findIndex(function (val, index, arr) {
return val > 3;
});
console.log(res); //3

八、 for in

  • for…in循环可用于循环对象和数组,推荐用于循环对象,可以用来遍历JSON
var arr = [
{ id: 1, name: "程序员" },
{ id: 2, name: "掉头发" },
{ id: 3, name: "你不信" },
{ id: 4, name: "薅一下" },
];
var arrNew = [];
for (var key in arr) {
console.log(key);
console.log(arr[key]);
arrNew.push(arr[key].id);
}
console.log(arrNew);

九、 for of

  • for…of循环可用于循环对象和数组,推荐用于循环对象,可以用来遍历JSON
var arr = [
{ name: "程序员" },
{ name: "掉头发" },
{ name: "你不信" },
{ name: "薅一下" },
];
// key()是对键名的遍历;
// value()是对键值的遍历;
// entries()是对键值对的遍历;
for (let item of arr) {
console.log(item);
}
// 输出数组索引
for (let item of arr.keys()) {
console.log(item);
}
// 输出内容和索引
for (let [item, val] of arr.entries()) {
console.table(item + ":" + val);
}

十、 for

原生实现方式

var arr = [
{ name: "程序员" },
{ name: "掉头发" },
{ name: "你不信" },
{ name: "薅一下" },
];
for (let index = 0; index < arr.length; index++) {
const element = arr[index];
console.log(element )
}

总结

方法1~7为ES6新语法 IE9及以上才支持。不过可以通过babel转义支持IE低版本。以上均不改变原数组。




收起阅读 »

Android炫酷的粒子动画!

一、总述ParticleTextView 是一个 Android 平台的自定义 view 组件,可以用彩色粒子组成指定的文字,并配合多种动画效果和配置属性,呈现出丰富的视觉效果。二、使用1. 引入依赖compile 'yasic.library.Particl...
继续阅读 »

一、总述

ParticleTextView 是一个 Android 平台的自定义 view 组件,可以用彩色粒子组成指定的文字,并配合多种动画效果和配置属性,呈现出丰富的视觉效果。


二、使用

1. 引入依赖

compile 'yasic.library.ParticleTextView:particletextview:0.0.6'

2. 加入到布局文件中

    <com.yasic.library.particletextview.View.ParticleTextView
android
:id="@+id/particleTextView"
android
:layout_width="match_parent"
android
:layout_height="match_parent" />

3. 实例化配置信息类 ParticleTextViewConfig

ParticleTextView particleTextView = (ParticleTextView) findViewById(R.id.particleTextView);
RandomMovingStrategy randomMovingStrategy = new RandomMovingStrategy();
ParticleTextViewConfig config = new ParticleTextViewConfig.Builder()
.setRowStep(8)
.setColumnStep(8)
.setTargetText("Random")
.setReleasing(0.2)
.setParticleRadius(4)
.setMiniDistance(0.1)
.setTextSize(150)
.setMovingStrategy(randomMovingStrategy)
.instance();
particleTextView.setConfig(config);

4. 启动动画

particleTextView.startAnimation();

5. 暂停动画

particleTextView1.stopAnimation();

三、API说明

粒子移动轨迹策略 MovingStrategy

移动轨迹策略继承自抽象类 MovingStrategy,可以自己继承并实现其中的 setMovingPath 方法,以下是自带的几种移动轨迹策略

  • CornerStrategy

  • HorizontalStrategy

  • BidiHorizontalStrategy


  • VerticalStrategy


  • BidiVerticalStrategy

配置信息类 ParticleTextViewConfig

配置信息类采用工厂模式创建,以下属性均为可选属性。

  • 设置显示的文字
setTargetText(String targetText)
  • 设置文字大小
setTextSize(int textSize)
  • 设置粒子半径
setParticleRadius(float radius)
  • 设置横向和纵向像素采样间隔

采样间隔越小生成的粒子数目越多,但绘制帧数也随之降低,建议结合文字大小与粒子半径进行调节。

setColumnStep(int columnStep)
setRowStep(int rowStep)
  • 设置粒子运动速度
setReleasing(double releasing)

指定时刻,粒子的运动速度由下列公式决定,其中 Vx 和 Vy 分别是 X 与 Y 方向上的运动速度,target 与 source 分别是粒子的目的坐标与当前坐标

Vx = (targetX - sourceX) * releasing
Vy = (targetY - sourceY) * releasing
  • 设置最小判决距离

当粒子与目的坐标距离小于最小判决距离时将直接移动到目的坐标,从而减少不明显的动画过程。

setMiniDistance(double miniDistance)
  • 设置粒子颜色域

默认使用完全随机的颜色域

setParticleColorArray(String[] particleColorArray)
  • 设置粒子移动轨迹策略

默认使用随机分布式策略

setMovingStrategy(MovingStrategy movingStrategy)
  • 设置不同路径间动画的间隔时间

delay < 0 时动画不循环

setDelay(Long delay)

ParticleTextView类

  • 指定配置信息类
setConfig(ParticleTextViewConfig config)
  • 开启动画
void startAnimation()
  • 停止动画
void stopAnimation()
  • 获取动画是否暂停

暂停是指动画完成了一段路径后的暂留状态

boolean isAnimationPause()
  • 获取动画是否停止

停止是指动画完成了一次完整路径后的停止状态

boolean isAnimationStop()

ParticleTextSurfaceView 类

继承自 SurfaceView 类,利用子线程进行 Canvas 绘制,在多个组件渲染情况下效率更高。所有 API 与 ParticleTextView 类一致。


代码下载:ParticleTextView-master.zip

收起阅读 »

无敌的 iOS 网络通信库

RestKit是一个现代的Objective-C框架,用于在iOS和Mac OS X上实现RESTful Web服务客户端。它提供了一个强大的对象映射引擎,该引擎与Core Data无缝集成,并提供了一组简单的网络原语,用于映射建立在顶部的HTTP请求和响应。...
继续阅读 »

RestKit是一个现代的Objective-C框架,用于在iOS和Mac OS X上实现RESTful Web服务客户端。它提供了一个强大的对象映射引擎,该引擎与Core Data无缝集成,并提供了一组简单的网络原语,用于映射建立在顶部的HTTP请求和响应。的AFNetworking它具有一组经过精心设计的优雅API,这些API使访问和建模RESTful资源感到不可思议。例如,以下是访问Twitter公共时间轴并将JSON内容转换为Tweet对象数组的方法:

@interface RKTweet : NSObject
@property (nonatomic, copy) NSNumber *userID;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *text;
@end

RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[RKTweet class]];
[mapping addAttributeMappingsFromDictionary:@{
@"user.name": @"username",
@"user.id": @"userID",
@"text": @"text"
}];

RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:mapping method:RKRequestMethodAny pathPattern:nil keyPath:nil statusCodes:nil];
NSURL *url = [NSURL URLWithString:@"http://api.twitter.com/1/statuses/public_timeline.json"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
RKObjectRequestOperation *operation = [[RKObjectRequestOperation alloc] initWithRequest:request responseDescriptors:@[responseDescriptor]];
[operation setCompletionBlockWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *result) {
NSLog(@"The public timeline Tweets: %@", [result array]);
} failure:nil];
[operation start];


概述

RestKit被设计为模块化的,每个模块都致力于在整个框架以及与主机平台之间维护最少的依赖关系集。库的核心是对象映射引擎,该引擎负责在表示形式之间转换对象(例如JSON / XML <->本地域对象)。

API快速入门

RestKit分为几个模块,这些模块将映射引擎与HTTP和Core Data集成完全分开,以提供最大的灵活性。每个模块中的键类在下面突出显示,并且每个模块都超链接到源代码中包含的README.md。

对象映射
RKObjectMapping封装用于转换由键-值编码键路径表示的对象表示的配置。
RKAttributeMapping根据源和目标键路径指定对象或实体映射中的属性之间的所需转换。
RKRelationshipMapping根据源和目标键路径以及用来映射子对象属性RKObjectMapping指定嵌套的一个或多个子对象的所需映射
RK动态映射指定一个灵活的映射,在该映射中,关于要使用哪个RKObjectMapping来处理给定文档的决策将推迟到运行时。
RKMapperOperation提供用于将反序列化文档映射到一组本地域对象的接口。
RKMappingOperation一个的NSOperation执行使用对象表示之间的映射RKObjectMapping
联网
RKRequestDescriptor描述可以针对给定对象类型从应用程序发送到远程Web应用程序的请求。
RKResponseDescriptor根据对象映射,键路径,用于匹配URLSOCKit模式以及一组状态码(它们定义了适合映射的情况)描述了可以从远程Web应用程序返回的对象可映射响应。给定的响应。
RKObjectParameterization执行给定对象到NSDictionary表示形式的映射,该表示形式适合用作HTTP请求的参数。
RKObjectRequestOperation一个NSOperation,它使用一组RKResponseDescriptor对象中表示的配置来发送HTTP请求并在已解析的响应主体上执行对象映射
RKResponseMapperOperation一个的NSOperation提供用于对象映射的支撑NSHTTPURLResponse使用一组RKResponseDescriptor对象。
RKObjectManager捕获使用对象映射通过HTTP与RESTful Web应用程序通信的常见模式,包括:
  • 集中化RKRequestDescriptorRKResponseDescriptor配置
  • RKRouter描述URL配置
  • 序列化对象并使用序列化的表示形式发送请求
  • 发送请求以加载远程资源并映射响应主体的对象
  • 构建对象的多部分表单请求
路由器从基本URL和一组RKRoute对象生成NSURL对象,这些对象描述了应用程序使用的相对路径。
RKRoute描述给定对象类型和HTTP方法的单个相对路径,对象的关系或符号名。
核心数据
RKManagedObjectStore封装核心数据配置,包括NSManagedObjectModelNSPersistentStoreCoordinator和一对NSManagedObjectContext对象。
RKEntityMapping为映射建模,以将对象表示形式转换为给定NSEntityDescriptionNSManagedObject实例
RKConnectionDescription描述用于使用外键属性在Core Data实体之间建立关系的映射。
RKManagedObjectRequestOperation一个的NSOperation子类发送所解析响应身体的HTTP请求和执行对象映射来创建NSManagedObject情况下,使用建立对象之间的关系RKConnectionDescription了孤立的对象的对象清洗,并且在远程后端系统不再存在。
RKManagedObjectImporter针对以下两种情况,使用RKEntityMapping对象提供对托管对象的批量映射的支持
  1. 将已解析的文档批量导入到NSPersistentStore中。
  2. 生成种子数据库,以在安装时使用初始数据集初始化应用程序的核心数据存储。
搜索
RKSearchIndexer提供对在Core Data中为应用程序中实体的字符串属性生成全文可搜索索引的支持。
RKSearchPredicate生成给定文本字符串NSCompoundPredicate,该字符串将搜索通过RKSearchIndexer跨任何索引实体建立的索引。
测验
RKMappingTest给定已解析的文档以及对象或实体映射,为单元测试对象映射配置提供支持。根据预期的关键路径映射和/或预期的转换结果来配置预期。
RKTestFixture提供一个接口,可轻松生成用于单元测试的测试夹具数据。
RKTestFactory提供对创建用于测试的对象的支持。


例子

对象请求

//从/articles/1234.json获取单个Article并将其映射到一个对象
// JSON看起来像{“ article”:{“ title”:“ My Article”,“ author”:“ Blake”,“ body” :“非常酷!”}}
RKObjectMapping *映射= [RKObjectMapping
mappingForClass: [Article class ]];
[映射
addAttributeMappingsFromArray: @ [ @“标题@“作者@“正文 ]];
NSIndexSet * statusCodes = RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful); // 2xx中的任何内容
RKResponseDescriptor * responseDescriptor = [RKResponseDescriptor
responseDescriptorWithMapping:映射方法: RKRequestMethodAny pathPattern:@“ / articles /:articleID keyPath:@“ article statusCodes: statusCodes];

NSURLRequest * request = [ NSURLRequest requestWithURL: [ NSURL URLWithString:@“ http://restkit.org/articles/1234.json ]];
RKObjectRequestOperation * operation = [[RKObjectRequestOperation
alloc ] initWithRequest:请求responseDescriptors: @ [responseDescriptor]];
[操作
setCompletionBlockWithSuccess: ^(RKObjectRequestOperation *操作,RKMappingResult *结果){
Article *文章= [结果
firstObject ];
NSLog@“映射文章:%@ ,文章);
}
错误: ^(RKObjectRequestOperation * operation,NSError * error){
NSLog@“失败,错误:%@ ,[error localizedDescription ]);
}];

[操作
开始];

托管对象请求

//从/articles/888.json获取文章及其类别,并映射到Core Data实体
// // JSON类似于{“ article”:{“ title”:“ My Article”,“ author”:“ Blake”,“正文”:“非常酷!”,“类别”:[{“ id”:1,“名称”:“核心数据”]}
NSManagedObjectModel * managedObjectModel = [ NSManagedObjectModel mergedModelFromBundles:nil ];
RKManagedObjectStore * managedObjectStore = [[RKManagedObjectStore
alloc ] initWithManagedObjectModel: managedObjectModel];
NSError *错误= nil ;
布尔成功= RKEnsureDirectoryExistsAtPath(RKApplicationDataDirectory(),&error);
如果(!成功){
RKLogError@“无法在路径' %@ '%@ 上创建应用程序数据目录RKApplicationDataDirectory(),错误);
}

NSString * path = [ RKApplicationDataDirectory()stringByAppendingPathComponent:@“ Store.sqlite ];
NSPersistentStore * persistentStore = [managedObjectStore addSQLitePersistentStoreAtPath:SeedDatabaseAtPath的路径nil withConfiguration:nil 选项:nil 错误:&error];
if(!persistentStore){
RKLogError@“无法在路径' %@ '%@ ”上添加持久性存储,路径错误);
}

[managedObjectStore
createManagedObjectContexts ];

RKEntityMapping * categoryMapping = [RKEntityMapping
mappingForEntityForName:@“ Category inManagedObjectStore: managedObjectStore];
[categoryMapping
addAttributeMappingsFromDictionary: @ { id categoryID @“ name name }]];
RKEntityMapping * articleMapping = [RKEntityMapping
mappingForEntityForName:@“ Article inManagedObjectStore: managedObjectStore];
[articleMapping
addAttributeMappingsFromArray: @ [ @“标题@“作者@“正文 ]];
[articleMapping
addPropertyMapping: [RKRelationshipMapping relationshipMappingFromKeyPath:@“类别 toKeyPath:@“类别 withMapping: categoryMapping]];

NSIndexSet * statusCodes = RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful); // 2xx中的任何内容
RKResponseDescriptor * responseDescriptor = [RKResponseDescriptor
responseDescriptorWithMapping: articleMapping方法: RKRequestMethodAny pathPattern:@“ / articles /:articleID keyPath:@“ article statusCodes: statusCodes];

NSURLRequest * request = [ NSURLRequest requestWithURL: [ NSURL URLWithString:@“ http://restkit.org/articles/888.json ]];
RKManagedObjectRequestOperation * operation = [[RKManagedObjectRequestOperation
alloc ] initWithRequest:请求responseDescriptors: @ [responseDescriptor]];
operation.managedObjectContext = managedObjectStore.mainQueueManagedObjectContext;

operation.managedObjectCache = managedObjectStore.managedObjectCache;

[操作
setCompletionBlockWithSuccess: ^(RKObjectRequestOperation *操作,RKMappingResult *结果){
Article *文章= [结果
firstObject ];
NSLog@“映射文章:%@ ,文章);
NSLog@“映射类别:%@ ,[article.categories anyObject ]);
}
错误: ^(RKObjectRequestOperation * operation,NSError * error){
NSLog@“失败,错误:%@ ,[error localizedDescription ]);
}];

NSOperationQueue * operationQueue = [ NSOperationQueue new ];
[operationQueue
addOperation:操作];

将客户端错误响应映射到NSError

// GET /articles/error.json返回422(不可处理的实体)
// JSON看起来像{“ errors”:“发生某些错误”}

//您可以将错误映射到任何类,但是会免费提供`RKErrorMessage`
RKObjectMapping * errorMapping = [RKObjectMapping
mappingForClass: [RKErrorMessage]];
//源键路径上包含错误的整个值映射到消息
[errorMapping
addPropertyMapping: [RKAttributeMapping attributeMappingFromKeyPath:nil toKeyPath:@“ errorMessage ]];

NSIndexSet * statusCodes = RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError);
//在4xx状态码范围内的任意响应于与“错误”键路径使用该映射
RKResponseDescriptor * errorDescriptor = [RKResponseDescriptor
responseDescriptorWithMapping: errorMapping方法: RKRequestMethodAny pathPattern: 的keyPath:@ “错误 statusCodes: statusCodes];

NSURLRequest * request = [ NSURLRequest requestWithURL: [ NSURL URLWithString:@“ http://restkit.org/articles/error.json ]];
RKObjectRequestOperation * operation = [[RKObjectRequestOperation
alloc ] initWithRequest:请求responseDescriptors: @ [errorDescriptor]];
[operation
setCompletionBlockWithSuccess: 故障: ^(RKObjectRequestOperation * operation,NSError * error){
//错误所映射到的类的“ description”方法用于构造localizedDescription
NSLog的值@“加载了此错误:%@ ,[错误localizedDescription ]);

//您可以通过`userInfo`
RKErrorMessage * errorMessage = [[error.userInfo
objectForKey: RKObjectMapperErrorObjectsKey] firstObject ];访问用于构造NSError的模型对象
}];

在对象管理器中集中配置

//设置文章和错误响应描述符
//成功的JSON类似于{“ article”:{“ title”:“ My Article”,“ author”:“ Blake”,“ body”:“非常酷!”}}
RKObjectMapping *映射= [RKObjectMapping
mappingForClass: [Article class ]];
[映射
addAttributeMappingsFromArray: @ [ @“标题@“作者@“正文 ]];
NSIndexSet * statusCodes = RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful); // 2xx中的任何内容
RKResponseDescriptor * articleDescriptor = [RKResponseDescriptor
responseDescriptorWithMapping:映射方法: RKRequestMethodAny pathPattern:@“ / articles keyPath:@“ article statusCodes: statusCodes];

//错误JSON看起来像{“错误”:“发生某些错误”}
RKObjectMapping * errorMapping = [RKObjectMapping
mappingForClass: [RKErrorMessage]];
//源键路径上包含错误的整个值映射到消息
[errorMapping
addPropertyMapping: [RKAttributeMapping attributeMappingFromKeyPath:nil toKeyPath:@“ errorMessage ]];
NSIndexSet * statusCodes = RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError);
// 4xx状态码范围内带有“错误”键路径的任何响应都使用此映射
RKResponseDescriptor * errorDescriptor = [RKResponseDescriptor
responseDescriptorWithMapping: errorMapping方法: RKRequestMethodAny pathPattern:nil keyPath:@“ errors statusCodes: statusCodes];

//将描述符添加到管理器
RKObjectManager * manager = [RKObjectManager
managerWithBaseURL: [ NSURL URLWithString:@“ http://restkit.org ]];
[manager
addResponseDescriptorsFromArray: @ [articleDescriptor,errorDescriptor]];

[manager
getObjectsAtPath:@“ /articles/555.json 参数: 成功: ^(RKObjectRequestOperation * operation,RKMappingResult * mappingResult){
//处理articleDescriptor
}
失败: ^(RKObjectRequestOperation * operation,NSError * error){
//传输错误或服务器错误,由errorDescriptor
}]
处理


要求

RestKit需要iOS 8.0或更高版本或Mac OS X 10.9或更高版本。

RestKit中使用了多个第三方开源库,包括:

  1. AFNetworking-网络支持
  2. LibComponentLogging-日志支持
  3. SOCKit-字符串<->对象编码
  4. iso8601parser-支持解析和生成ISO- 8601日期

必须将以下Cocoa框架链接到应用程序目标中,以进行正确的编译:

  1. iOS上的CFNetwork.framework
  2. CoreData.framework
  3. Security.framework
  4. iOS上的MobileCoreServices.frameworkOS X上的CoreServices.framework

并且必须设置以下链接器标志:

  1. -ObjC
  2. -all_load

pods安装

$ cd /path/to/MyProject
$ touch Podfile
$ edit Podfile
target "YOUR PROJECT" do
platform :ios, '7.0'
# Or platform :osx, '10.7'
pod 'RestKit', '~> 0.24.0'
end
# Testing and Search are optional components
pod 'RestKit/Testing', '~> 0.24.0'
pod 'RestKit/Search', '~> 0.24.0'


更多实例及常见问题:https://github.com/RestKit/RestKit

源码下载:RestKit-development.zip


收起阅读 »

一个开源的iOS框架,用于基于GPU的图像和视频处理

概述GPUImage框架是BSD许可的iOS库,可让您将GPU加速的滤镜和其他效果应用于图像,实时摄像机视频和电影。与Core Image(iOS 5.0的一部分)相比,GPUImage允许您编写自己的自定义过滤器,支持部署到iOS 4.0并具有更简单的界面。...
继续阅读 »

概述

GPUImage框架是BSD许可的iOS库,可让您将GPU加速的滤镜和其他效果应用于图像,实时摄像机视频和电影。与Core Image(iOS 5.0的一部分)相比,GPUImage允许您编写自己的自定义过滤器,支持部署到iOS 4.0并具有更简单的界面。但是,它目前缺少Core Image的一些更高级的功能,例如面部检测。

对于诸如处理图像或实时视频帧之类的大规模并行操作,GPU比CPU具有一些明显的性能优势。在iPhone 4上,简单的图像过滤器在GPU上的执行速度比基于CPU的等效过滤器快100倍以上。

但是,在GPU上运行自定义滤镜需要大量代码才能为这些滤镜设置和维护OpenGL ES 2.0渲染目标。我创建了一个示例项目来做到这一点:

http://www.sunsetlakesoftware.com/2010/10/22/gpu-accelerated-video-processing-mac-and-ios

并发现我在创建过程中必须编写许多样板代码。因此,我将这个框架整合在一起,该框架封装了处理图像和视频时遇到的许多常见任务,并且使您不必担心OpenGL ES 2.0的基础。

在处理视频时,此框架与Core Image相比具有优势,在iPhone 4上仅花费2.5 ms即可从相机上传帧,应用伽玛滤镜和显示,而使用Core Image进行相同操作则需要106 ms。基于CPU的处理需要460毫秒,因此使GPUImage在此硬件上进行此操作的速度比Core Image快40倍,比与CPU绑定的处理快184倍。在iPhone 4S上,在这种情况下,GPUImage仅比Core Image快4倍,比CPU绑定处理快102倍。但是,对于更高半径的高斯模糊等更复杂的操作,Core Image当前超过GPUImage。


技术要求

  • OpenGL ES 2.0:使用此功能的应用程序将无法在原始iPhone,iPhone 3G以及第一代和第二代iPod touch上运行
  • iOS 4.1作为部署目标(4.0没有电影阅读所需的某些扩展)。如果您希望在拍摄静态照片时显示实时视频预览,则需要iOS 4.3作为部署目标。
  • iOS 5.0 SDK构建
  • 设备必须具有摄像头才能使用与摄像头相关的功能(显然)
  • 该框架使用自动引用计数(ARC),但如果将其添加为子项目,则应同时支持使用ARC和手动引用计数的项目,如下所述。对于针对iOS 4.x的手动引用计数应用程序,您需要在应用程序项目的“其他链接器标志”中添加-fobjc-arc。

通用架构

GPUImage使用OpenGL ES 2.0着色器以比在CPU绑定例程中更快的速度执行图像和视频操作。但是,它在简化的Objective-C界面中隐藏了与OpenGL ES API交互的复杂性。通过此接口,您可以定义图像和视频的输入源,将过滤器成串连接,并将处理后的图像或视频发送到屏幕,UIImage或磁盘上的电影。

视频图像或视频帧是从源对象上传的,源对象是GPUImageOutput的子类。其中包括GPUImageVideoCamera(用于来自iOS相机的实时视频),GPUImageStillCamera(用于使用相机拍摄照片),GPUImagePicture(用于静止图像)和GPUImageMovie(用于电影)。源对象将静止图像帧作为纹理上传到OpenGL ES,然后将这些纹理移交给处理链中的下一个对象。

链中的滤镜和其他后续元素符合GPUImageInput协议,该协议使它们可以从链中的上一个链接中获取或处理纹理,并对它们进行处理。距离目标更远一步的对象被视为目标,可以通过将多个目标添加到单个输出或过滤器来分支处理。

例如,如果某个应用程序从摄像机中获取实时视频,然后将该视频转换为棕褐色调,然后在屏幕上显示该视频,则会建立一个类似于以下内容的链:

GPUImageVideoCamera -> GPUImageSepiaFilter -> GPUImageView            

将静态库添加到您的iOS项目

注意:如果要在Swift项目中使用它,则需要使用“将其添加为框架”部分中的步骤,而不要使用以下步骤。Swift需要第三方代码模块。

一旦有了该框架的最新源代码,就可以很容易地将其添加到您的应用程序中。首先将GPUImage.xcodeproj文件拖动到应用程序的Xcode项目中,以将框架嵌入项目中。接下来,转到应用程序的目标,然后将GPUImage添加为目标依赖项。最后,您需要将libGPUImage.a库从GPUImage框架的Products文件夹拖到应用程序目标的Link Binary With Libraries构建阶段。

GPUImage需要将其他一些框架链接到您的应用程序,因此您需要在应用程序目标中添加以下内容作为链接库:

  • CoreMedia
  • CoreVideo
  • OpenGLES
  • AVFoundation
  • QuartzCore


您还需要找到框架标头,因此在项目的构建设置中,将“标头搜索路径”设置为从应用程序到GPUImage源目录内的framework /子目录的相对路径。使此标头搜索路径是递归的。

要在您的应用程序中使用GPUImage类,只需使用以下内容包括核心框架头即可:

#import "GPUImage.h"

注意:如果尝试使用Interface Builder构建接口时,如果遇到错误“ Interface Builder中的未知类GPUImageView”或类似错误,则可能需要在项目的构建设置中将-ObjC添加到Other Linker Flags。

另外,如果您需要将其部署到iOS 4.x,则似乎当前版本的Xcode(4.3)要求您在最终应用程序中弱链接Core Video框架,否则会崩溃并显示消息“找不到符号” :_CVOpenGLESTextureCacheCreate”,当您创建要上传到App Store或临时分发的归档文件时。为此,请转到项目的“构建阶段”选项卡,展开“使用库链接二进制文件”组,然后在列表中找到CoreVideo.framework。将列表最右边的设置从“必需”更改为“可选”。

此外,这是一个支持ARC的框架,因此,如果要在针对iOS 4.x的手动引用计数应用程序中使用此框架,则还需要将-fobjc-arc添加到“其他链接器标志”中。

在命令行上构建静态库

如果您不想将项目作为依赖项包含在应用程序的Xcode项目中,则可以为iOS Simulator或设备构建通用静态库。为此,请build.sh在命令行上运行生成的库和头文件将位于build/Release-iphone您也可以通过更改中的IOSSDK_VER变量来更改iOS SDK的build.sh版本(可以使用找到所有可用版本xcodebuild -showsdks)。

将此作为框架(模块)添加到Mac或iOS项目

Xcode 6和iOS 8和Mac一样,都支持使用完整的框架,从而简化了将其添加到应用程序中的过程。要将其添加到您的应用程序中,建议将.xcodeproj项目文件拖到您的应用程序项目中(就像在静态库目标中一样)。

对于您的应用程序,转到其目标构建设置,然后选择“构建阶段”选项卡。在“目标依赖项”分组下,在iOS上添加GPUImageFramework(而不是在构建静态库的GPUImage上),或者在Mac上添加GPUImage。在“使用库链接二进制文件”部分下,添加GPUImage.framework。

这将导致GPUImage构建为框架。在Xcode 6下,它也将作为模块构建,从而允许您在Swift项目中使用它。如上设置时,您只需要使用

import GPUImage

把它拉进去。

然后,您需要添加一个新的“复制文件”构建阶段,将“目标”设置为“框架”,然后向其添加GPUImage.framework构建产品。这将使框架与您的应用程序捆绑在一起(否则,在执行时,您将看到神秘的“ dyld:库未加载:@ rpath / GPUImage.framework / GPUImage”错误)。


过滤实况视频

要从iOS设备的摄像头过滤实时视频,可以使用以下代码:

GPUImageVideoCamera *videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPreset640x480 cameraPosition:AVCaptureDevicePositionBack];
videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait;

GPUImageFilter *customFilter = [[GPUImageFilter alloc] initWithFragmentShaderFromFile:@"CustomShader"];
GPUImageView *filteredVideoView = [[GPUImageView alloc] initWithFrame:CGRectMake(0.0, 0.0, viewWidth, viewHeight)];

// Add the view somewhere so it's visible

[videoCamera addTarget:customFilter];
[customFilter addTarget:filteredVideoView];

[videoCamera startCameraCapture];

这会使用尝试以640x480捕获的预设来设置来自iOS设备的背面摄像头的视频源。该视频是通过处于纵向模式的界面捕获的,在该模式下,横向安装的摄像机需要在显示之前旋转其视频帧。然后,使用文件CustomShader.fsh中的代码将自定义滤镜设置为来自摄像机的视频帧的目标。最终,这些过滤后的视频帧将在UIView子类的帮助下显示在屏幕上,该子类可以呈现此管道产生的过滤后的OpenGL ES纹理。

可以通过设置GPUImageView的fillMode属性来更改其填充模式,这样,如果源视频的长宽比与视图的长宽比不同,则视频将被拉伸,以黑条居中或缩放以填充。

对于混合滤镜和其他吸收多个图像的滤镜,您可以创建多个输出并将单个滤镜添加为这两个输出的目标。将输出添加为目标的顺序将影响混合或以其他方式处理输入图像的顺序。

另外,如果您希望启用麦克风音频捕获以录制到电影中,则需要将相机的audioEncodingTarget设置为您的电影编写者,如下所示:

videoCamera.audioEncodingTarget = movieWriter;

捕获和过滤静止照片

要捕获和过滤静态照片,可以使用与过滤视频类似的过程。您可以使用GPUImageStillCamera代替GPUImageVideoCamera:

stillCamera = [[GPUImageStillCamera alloc] init];
stillCamera.outputImageOrientation = UIInterfaceOrientationPortrait;

filter = [[GPUImageGammaFilter alloc] init];
[stillCamera addTarget:filter];
GPUImageView *filterView = (GPUImageView *)self.view;
[filter addTarget:filterView];

[stillCamera startCameraCapture];

这将为您提供静态相机预览视频的实时,经过过滤的提要。请注意,此预览视频仅在iOS 4.3及更高版本上提供,因此,如果您希望具有此功能,则可能需要将其设置为部署目标。

一旦要捕获照片,就可以使用如下所示的回调块:

[stillCamera capturePhotoProcessedUpToFilter:filter withCompletionHandler:^(UIImage *processedImage, NSError *error){
NSData *dataForJPEGFile = UIImageJPEGRepresentation(processedImage, 0.8);

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];

NSError *error2 = nil;
if (![dataForJPEGFile writeToFile:[documentsDirectory stringByAppendingPathComponent:@"FilteredPhoto.jpg"] options:NSAtomicWrite error:&error2])
{
return;
}
}];

上面的代码捕获由预览视图中使用的相同滤镜链处理的原尺寸照片,并将该照片以JPEG格式保存到磁盘中,位于应用程序的文档目录中。

请注意,由于纹理大小的限制,该框架目前无法在较旧的设备(iPhone 4S,iPad 2或Retina iPad之前的设备)上处理宽度大于2048像素或高的图像。这意味着其相机输出的静态照片大于该尺寸的iPhone 4将无法捕获此类照片。正在实施一种切片机制来解决此问题。所有其他设备都应该能够使用此方法捕获和过滤照片。

处理静止图像

有两种方法可以处理静止图像并创建结果。实现此目的的第一种方法是创建一个静止图像源对象并手动创建一个滤镜链:

UIImage *inputImage = [UIImage imageNamed:@"Lambeau.jpg"];

GPUImagePicture *stillImageSource = [[GPUImagePicture alloc] initWithImage:inputImage];
GPUImageSepiaFilter *stillImageFilter = [[GPUImageSepiaFilter alloc] init];

[stillImageSource addTarget:stillImageFilter];
[stillImageFilter useNextFrameForImageCapture];
[stillImageSource processImage];

UIImage *currentFilteredVideoFrame = [stillImageFilter imageFromCurrentFramebuffer];

请注意,要从过滤器手动捕获图像,需要设置-useNextFrameForImageCapture以便告诉过滤器以后需要从其捕获。默认情况下,GPUImage重用过滤器中的帧缓冲区以节省内存,因此,如果您需要保留过滤器的帧缓冲区以进行手动图像捕获,则需要提前告知它。

对于要应用于图像的单个滤镜,只需执行以下操作:

GPUImageSepiaFilter *stillImageFilter2 = [[GPUImageSepiaFilter alloc] init];
UIImage *quickFilteredImage = [stillImageFilter2 imageByFilteringImage:inputImage];

编写自定义过滤器

与iOS(从iOS 5.0开始)上的Core Image相比,此框架的一大优势是能够编写自己的自定义图像和视频处理过滤器。这些过滤器作为OpenGL ES 2.0片段着色器提供,以类似C的OpenGL着色语言编写。

自定义过滤器使用以下代码初始化

GPUImageFilter *customFilter = [[GPUImageFilter alloc] initWithFragmentShaderFromFile:@"CustomShader"];

其中片段着色器使用的扩展名是.fsh。此外,如果您不希望将片段着色器发送到应用程序包中,则可以使用-initWithFragmentShaderFromString:初始值设定项将片段着色器提供为字符串。

片段着色器对在该滤镜阶段要渲染的每个像素执行其计算。他们使用OpenGL阴影语言(GLSL)进行此操作,这是一种类似于C的语言,并附加了2-D和3-D图形。以下棕褐色调滤镜是片段着色器的一个示例:

varying highp vec2 textureCoordinate;

uniform sampler2D inputImageTexture;

void main()
{
lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
lowp vec4 outputColor;
outputColor.r = (textureColor.r * 0.393) + (textureColor.g * 0.769) + (textureColor.b * 0.189);
outputColor.g = (textureColor.r * 0.349) + (textureColor.g * 0.686) + (textureColor.b * 0.168);
outputColor.b = (textureColor.r * 0.272) + (textureColor.g * 0.534) + (textureColor.b * 0.131);
outputColor.a = 1.0;

gl_FragColor = outputColor;
}

为了使图像过滤器可在GPUImage框架中使用,需要前两行采用textureCoordinate变化(对于纹理中的当前坐标,归一化为1.0)和inputImageTexture统一(对于实际的输入图像帧纹理) 。

着色器的其余部分在传入的纹理中的该位置捕获像素的颜色,以产生棕褐色调的方式对其进行操作,并将该像素颜色写出以用于下一步处理管道。

将片段着色器添加到Xcode项目时要注意的一件事是Xcode认为它们是源代码文件。要解决此问题,您需要将着色器从“编译源”构建阶段手动移至“复制捆绑包资源”,以便使着色器包含在应用程序捆绑包中。

过滤和重新编码电影

可以通过GPUImageMovie类将电影加载到框架中,进行过滤,然后使用GPUImageMovieWriter将其写出。GPUImageMovieWriter的速度也足够快,可以以640x480的速度从iPhone 4的摄像头实时录制视频,因此可以将直接过滤的视频源输入其中。目前,GPUImageMovieWriter的速度足以在iPhone 4上以高达20 FPS的速度录制实时720p视频,而在iPhone 4S(以及新iPad)上以30 FPS的速度录制720p和1080p的视频。

以下是如何加载示例电影,将其通过像素化滤镜传递,然后将结果记录为480 x 640 h.264电影的示例:

movieFile = [[GPUImageMovie alloc] initWithURL:sampleURL];
pixellateFilter = [[GPUImagePixellateFilter alloc] init];

[movieFile addTarget:pixellateFilter];

NSString *pathToMovie = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/Movie.m4v"];
unlink([pathToMovie UTF8String]);
NSURL *movieURL = [NSURL fileURLWithPath:pathToMovie];

movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:movieURL size:CGSizeMake(480.0, 640.0)];
[pixellateFilter addTarget:movieWriter];

movieWriter.shouldPassthroughAudio = YES;
movieFile.audioEncodingTarget = movieWriter;
[movieFile enableSynchronizedEncodingUsingMovieWriter:movieWriter];

[movieWriter startRecording];
[movieFile startProcessing];

录制完成后,您需要从筛选器链中删除电影录像机,并使用如下代码关闭录制:

[pixellateFilter removeTarget:movieWriter];
[movieWriter finishRecording];

电影必须先完成,然后才能使用,因此,如果在此之前将其中断,则录制将丢失。

与OpenGL ES交互

GPUImage可以分别通过使用其GPUImageTextureOutput和GPUImageTextureInput类从OpenGL ES导出和导入纹理。这样,您就可以记录来自OpenGL ES场景的电影,该电影使用绑定的纹理渲染到帧缓冲区对象,或者过滤视频或图像,然后将它们作为纹理显示在OpenGL ES中,以在场景中显示。

使用此方法的一个警告是,必须通过共享组或类似方式在GPUImage的OpenGL ES上下文和任何其他上下文之间共享这些过程中使用的纹理。

内置过滤器

当前有125个内置过滤器,分为以下类别:

色彩调整

  • GPUImageBrightnessFilter:调整图像的亮度

    • 亮度:调整后的亮度(-1.0-1.0,默认值为0.0)
  • GPUImageExposureFilter:调整图像的曝光

    • 曝光:调整后的曝光(-10.0-10.0,默认值为0.0)
  • GPUImageContrastFilter:调整图像的对比度

    • 对比度:调整后的对比度(0.0-4.0,默认值为1.0)
  • GPUImageSaturationFilter:调整图像的饱和度

    • 饱和度:应用于图像的饱和度或去饱和度(0.0-2.0,默认值为1.0)
  • GPUImageGammaFilter:调整图像的灰度系数

    • gamma:要应用的gamma调整(0.0-3.0,默认值为1.0)
  • GPUImageLevelsFilter:类似Photoshop的色阶调整。min,max,minOut和maxOut参数在[0,1]范围内浮动。如果来自Photoshop的参数在[0,255]范围内,则必须首先将它们转换为[0,1]。gamma / mid参数是一个float> =0。它与Photoshop中的值匹配。如果要对RGB以及各个通道应用级别,则需要两次使用此滤镜-首先用于单个通道,然后用于所有通道。

  • GPUImageColorMatrixFilter:通过将矩阵应用于图像来变换图像的颜色

    • colorMatrix:一个4x4矩阵,用于转换图像中的每种颜色
    • 强度:新转换的颜色替换每个像素的原始颜色的程度
  • GPUImageRGBFilter:调整图像的各个RGB通道

    • 红色:每个色彩通道相乘的归一化值。范围从0.0开始,默认值为1.0。
    • 绿色
    • 蓝色
  • GPUImageHueFilter:调整图像的色调

    • 色调:色调角度,以度为单位。默认为90度
  • GPUImageVibranceFilter:调整图像的鲜艳度

    • vibrance:要应用的vibrance调整,默认设置为0.0,建议的最小/最大值分别为-1.2和1.2。
  • GPUImageWhiteBalanceFilter:调整图像的白平衡。

    • temperature:调整图像所用的温度,以ºK为单位。值4000非常凉爽,而7000非常温暖。默认值为5000。请注意,在4000和5000之间的比例在视觉上与在5000和7000之间的比例几乎相同。
    • 色调:用于调整图像的色调。值-200表示非常绿色,而200表示非常粉红色。默认值为0。
  • GPUImageToneCurveFilter:基于样条曲线为每个颜色通道调整图像的颜色。

    • redControlPoints
    • greenControlPoints
    • blueControlPoints
    • rgbCompositeControlPoints:色调曲线采用一系列控制点,这些控制点定义了每个颜色分量或复合物中所有三个分量的样条曲线。这些以NSValue包裹的CGPoints的形式存储在NSArray中,其标准化的X和Y坐标为0-1。默认值为(0,0),(0.5,0.5),(1,1)。
  • GPUImageHighlightShadowFilter:调整图像的阴影和高光

    • 阴影:增加阴影以使阴影变淡,从0.0到1.0,默认值为0.0。
    • Highlights:从1.0降低到0.0,以1.0为默认值将高光变暗。
  • GPUImageHighlightShadowTintFilter:允许您使用颜色和强度独立地着色图像的阴影和高光

    • shadowTintColor:阴影色调RGB颜色(GPUVector4)。默认值:({1.0f, 0.0f, 0.0f, 1.0f}红色)。
    • highlightTintColor:高亮色调RGB颜色(GPUVector4)。默认值:({0.0f, 0.0f, 1.0f, 1.0f}蓝色)。
    • shadowTintIntensity:阴影着色强度,从0.0到1.0。默认值:0.0
    • highlightTintIntensity:突出显示色调强度,从0.0到1.0,默认值为0.0。
  • GPUImageLookupFilter:使用RGB颜色查找图像来重新映射图像中的颜色。首先,使用您最喜欢的照片编辑应用程序将滤镜应用于GPUImage / framework / Resources中的lookup.png。为了使其正常工作,每个像素的颜色都不得依赖于其他像素(例如,模糊将不起作用)。如果需要更复杂的过滤器,则可以根据需要创建任意多个查找表。准备就绪后,将新的lookup.png文件用作GPUImageLookupFilter的第二个输入。

  • GPUImageAmatorkaFilter:基于Amatorka的Photoshop动作的照片滤镜:http ://amatorka.deviantart.com/art/Amatorka-Action-2-121069631 如果要使用此效果,则必须将GPUImage Resources文件夹中的lookup_amatorka.png添加到应用程序包中。

  • GPUImageMissEtikateFilter:基于Etikate小姐的Photoshop动作的照片滤镜:http ://miss-etikate.deviantart.com/art/Photoshop-Action-15-120151961 如果要使用此效果,则必须将GPUImage Resources文件夹中的lookup_miss_etikate.png添加到应用程序包中。

  • GPUImageSoftEleganceFilter:另一个基于查找的颜色重新映射滤镜。如果要使用此效果,则必须将GPUImage Resources文件夹中的lookup_soft_elegance_1.png和lookup_soft_elegance_2.png添加到应用程序包中。

  • GPUImageSkinToneFilter:肤色调整滤镜,可影响浅色肤色的唯一范围,并相应地调整粉红色/绿色或粉红色/橙色范围。默认值针对白皙的皮肤,但可以根据需要进行调整。

    • skinToneAdjust:调整肤色的量。默认值:0.0,建议的最小/最大值:分别为-0.3和0.3。
    • skinHue:要检测的皮肤色调。默认值:0.05(白皙至泛红皮肤)。
    • skinHueThreshold:皮肤色调的变化量。默认值:40.0。
    • maxHueShift:允许的最大色相偏移量。默认值:0.25
    • maxSaturationShift =要移动的最大饱和量(使用橙色时)。默认值:0.4
    • upperSkinToneColor =GPUImageSkinToneUpperColorGreenGPUImageSkinToneUpperColorOrange
  • GPUImageColorInvertFilter:反转图像的颜色

  • GPUImageGrayscaleFilter:将图像转换为灰度(饱和度滤镜的实现速度稍快,但无法改变颜色的贡献)

  • GPUImageMonochromeFilter:根据每个像素的亮度,将图像转换为单色版本

    • strength:特定颜色替换正常图像颜色的程度(0.0-1.0,默认值为1.0)
    • color:用作效果基础的颜色,默认为(0.6,0.45,0.3,1.0)。
  • GPUImageFalseColorFilter:使用图像的亮度在两种用户指定的颜色之间进行混合

    • firstColor:第一种和第二种颜色分别指定什么颜色替换图像的暗区和亮区。默认值为(0.0,0.0,0.5)amd(1.0,0.0,0.0)。
    • secondColor
  • GPUImageHazeFilter:用于添加或删除雾度(类似于UV滤镜)

    • distance:所应用颜色的强度。默认值为0。最佳值为-.3到.3。
    • 斜率:颜色变化的量。默认值为0。最佳值为-.3到.3。
  • GPUImageSepiaFilter:简单的棕褐色调滤镜

    • 强度:棕褐色替换正常图像颜色的程度(0.0-1.0,默认值为1.0)
  • GPUImageOpacityFilter:调整传入图像的Alpha通道

    • opacity:将每个像素的输入Alpha通道乘以(0.0-1.0,默认值为1.0)的值
  • GPUImageSolidColorGenerator:这会输出生成的纯色图像。您需要使用-forceProcessingAtSize定义图像大小:

    • color:用于填充图像的颜色,采用四部分格式。
  • GPUImageLuminanceThresholdFilter:亮度高于阈值的像素将显示为白色,而低于阈值的像素将为黑色

    • threshold:亮度阈值,从0.0到1.0,默认值为0.5
  • GPUImageAdaptiveThresholdFilter:确定像素周围的局部亮度,如果像素低于该局部亮度,则将其变为黑色,如果高于该像素,则将其变为白色。这对于在变化的光照条件下挑选文本很有用。

    • blurRadiusInPixels:背景平均模糊半径(以像素为单位)的倍数,默认为4。
  • GPUImageAverageLuminanceThresholdFilter:这将应用阈值操作,其中将根据场景的平均亮度连续调整阈值。

    • thresholdMultiplier:这是平均亮度将被乘以达到最终使用阈值的一个因素。默认情况下,该值为1.0。
  • GPUImageHistogramFilter:这将分析传入的图像并创建一个输出直方图,其输出每个颜色值的频率。该滤波器的输出是一个3像素高,256像素宽的图像,其中心(垂直)像素包含与发生各种颜色值的频率相对应的像素。每个颜色值占据256个宽度位置之一,从左侧的0到右侧的255。可以为单个颜色通道(kGPUImageHistogramRed,kGPUImageHistogramGreen,kGPUImageHistogramBlue),图像亮度(kGPUImageHistogramLuminance)或一次为所有三个颜色通道(kGPUImageHistogramRGB)生成此直方图。

    • downsamplingFactor:不是决定对每个像素进行采样,而是决定要对图像的哪个部分进行采样。默认情况下,它是16,最小值为1。这是防止饱和直方图的必要条件,直方图在每种颜色值变得过载之前只能记录256个像素。
  • GPUImageHistogramGenerator:这是一个特殊的过滤器,主要用于与GPUImageHistogramFilter一起使用。它生成由GPUImageHistogramFilter生成的颜色直方图的输出表示形式,但可以重新用于显示其他类型的值。它获取图像并查看中心(垂直)像素。然后,它将RGB分量的数值绘制在输出纹理的单独的彩色图形中。您可能需要为此过滤器设置一个大小,以使其输出可见。

  • GPUImageAverageColor:通过对图像中每个像素的RGBA分量求平均值,此处理输入图像并确定场景的平均颜色。约简过程用于在GPU上逐步对源图像进行下采样,然后在CPU上进行短暂的平均计算。该过滤器的输出毫无意义,但是您需要将colorAverageProcessingFinishedBlock属性设置为一个包含四个颜色分量和一个帧时间并对它们执行某些操作的块。

  • GPUImageLuminosity:与GPUImageAverageColor一样,这会将图像降低到其平均亮度。您需要设置luminosityProcessingFinishedBlock来处理此过滤器的输出,该过滤器仅返回一个发光度值和一个帧时间。

  • GPUImageChromaKeyFilter:对于图像中的给定颜色,将alpha通道设置为0。这与GPUImageChromaKeyBlendFilter相似,只是不将第二个图像混合为匹配的颜色,而不会在第二个图像中取而代之颜色透明。

    • thresholdSensitivity:颜色匹配需要与要替换的目标颜色之间的接近程度(默认值为0.4)
    • 平滑:混合颜色时的平滑程度(默认值为0.1)

图像处理

  • GPUImageTransformFilter:这会将任意2D或3D变换应用于图像

    • affineTransform:这需要一个CGAffineTransform来调整二维图像
    • transform3D:这需要一个CATransform3D来处理3-D图像
    • ignoreAspectRatio:默认情况下,将保持变换图像的纵横比,但是可以将其设置为YES,以使变换与纵横比无关
  • GPUImageCropFilter:将图像裁剪到特定区域,然后仅将该区域传递到滤镜的下一个阶段

    • cropRegion:要裁剪出图像的矩形区域,将其标准化为0.0-1.0的坐标。(0.0,0.0)位置在图像的左上方。
  • GPUImageLanczosResamplingFilter:这使您可以使用Lanczos重采样对图像进行上采样或下采样,其质量明显优于标准线性或三线性插值。只需使用-forceProcessingAtSize:即可设置过滤器的目标输出分辨率,然后将针对该新大小重新采样图像。

  • GPUImageSharpenFilter:锐化图像

    • 清晰度:要应用的清晰度调整(-4.0-4.0,默认值为0.0)
  • GPUImageUnsharpMaskFilter:应用不清晰的蒙版

    • blurRadiusInPixels:基础高斯模糊的模糊半径。默认值为4.0。
    • 强度:锐化的强度,从在多达0.0,与1.0的默认
  • GPUImageGaussianBlurFilter:硬件优化的可变半径高斯模糊

    • texelSpacingMultiplier:像素间距的乘数,范围从0.0开始,默认值为1.0。进行调整可能会稍微增加模糊强度,但会在结果中引入伪影。强烈建议您先使用其他参数,然后再触摸此参数。
    • blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为2.0。这将调整高斯分布函数中的sigma变量。
    • blurRadiusAsFractionOfImageWidth
    • blurRadiusAsFractionOfImageHeight:设置这些属性将允许模糊半径随图像的大小缩放
    • blurPasses:顺序模糊传入图像的次数。通过的次数越多,过滤器的速度就越慢。
  • GPUImageBoxBlurFilter:硬件优化的可变半径框模糊

    • texelSpacingMultiplier:像素间距的乘数,范围从0.0开始,默认值为1.0。进行调整可能会稍微增加模糊强度,但会在结果中引入伪影。强烈建议您先使用其他参数,然后再触摸此参数。
    • blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为2.0。这将调整高斯分布函数中的sigma变量。
    • blurRadiusAsFractionOfImageWidth
    • blurRadiusAsFractionOfImageHeight:设置这些属性将允许模糊半径随图像的大小缩放
    • blurPasses:顺序模糊传入图像的次数。通过的次数越多,过滤器的速度就越慢。
  • GPUImageSingleComponentGaussianBlurFilter:对GPUImageGaussianBlurFilter的修改,仅对红色组件起作用

    • texelSpacingMultiplier:像素间距的乘数,范围从0.0开始,默认值为1.0。进行调整可能会稍微增加模糊强度,但会在结果中引入伪影。强烈建议您先使用其他参数,然后再触摸此参数。
    • blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为2.0。这将调整高斯分布函数中的sigma变量。
    • blurRadiusAsFractionOfImageWidth
    • blurRadiusAsFractionOfImageHeight:设置这些属性将允许模糊半径随图像的大小缩放
    • blurPasses:顺序模糊传入图像的次数。通过的次数越多,过滤器的速度就越慢。
  • GPUImageGaussianSelectiveBlurFilter:高斯模糊,可将焦点保留在圆形区域内

    • blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为5.0。这将调整高斯分布函数中的sigma变量。
    • excludeCircleRadius:从模糊中排除的圆形区域的半径
    • excludeCirclePoint:从模糊中排除的圆形区域的中心
    • excludeBlurSize:模糊部分和透明圆之间的区域大小
    • AspectRatio:图像的纵横比,用于调整对焦区域的圆度。默认情况下,它与图像的宽高比匹配,但是您可以覆盖此值。
  • GPUImageGaussianBlurPositionFilterGPUImageGaussianSelectiveBlurFilter的反函数,仅在特定圆圈内应用模糊

    • blurSize:模糊大小的乘数,范围从0.0开始,默认为1.0
    • blurCenter:模糊的中心,默认为0.5、0.5
    • blurRadius:模糊的半径,默认为1.0
  • GPUImageiOSBlurFilter:尝试在控制中心等地方复制iOS 7上使用的背景模糊。

    • blurRadiusInPixels:用于模糊的半径(以像素为单位),默认值为12.0。这将调整高斯分布函数中的sigma变量。
    • 饱和度:饱和度范围从0.0(完全饱和)到2.0(最大饱和度),正常水平为0.8
    • 下采样:下采样然后上采样传入图像的程度,以最小化高斯模糊内的计算,默认值为4.0。
  • GPUImageMedianFilter:获取3x3区域中三个颜色分量的中值

  • GPUImageBilateralFilter:双向模糊,它尝试在保留锐利边缘的同时模糊相似的颜色值

    • texelSpacingMultiplier:texel读取之间的间隔的乘数,范围从0.0开始,默认为4.0
    • distanceNormalizationFactor:中心颜色和样本颜色之间的距离的归一化因子,默认值为8.0。
  • GPUImageTiltShiftFilter:模拟的倾斜移位镜头效果

    • blurRadiusInPixels:基础模糊的半径,以像素为单位。默认情况下是7.0。
    • topFocusLevel:图像中对焦区域顶部的标准化位置,此值应小于bottomFocusLevel,默认值为0.4
    • bottomFocusLevel:图像中对焦区域底部的标准化位置,此值应高于topFocusLevel,默认值为0.6
    • focusFallOffRate:图像从对焦区域模糊的速率,默认为0.2
  • GPUImage3x3ConvolutionFilter:对图像运行3x3卷积内核

    • convolutionKernel:卷积内核是一个3x3的值矩阵,适用于该像素及其周围的8个像素。矩阵以行优先顺序指定,左上像素为one.one,右下像素为three.three。如果矩阵中的值之和不等于1.0,则图像可能变亮或变暗。
  • GPUImageSobelEdgeDetectionFilter:Sobel边缘检测,边缘以白色突出显示

    • texelWidth
    • texelHeight:这些参数影响检测到的边缘的可见性
    • edgeStrength:调整滤镜的动态范围。较高的值会导致边缘更牢固,但会饱和强度颜色空间。默认值为1.0。
  • GPUImagePrewittEdgeDetectionFilter:Prewitt边缘检测,边缘以白色突出显示

    • texelWidth
    • texelHeight:这些参数影响检测到的边缘的可见性
    • edgeStrength:调整滤镜的动态范围。较高的值会导致边缘更牢固,但会饱和强度颜色空间。默认值为1.0。
  • GPUImageThresholdEdgeDetectionFilter:执行Sobel边缘检测,但是应用阈值而不是给出逐渐的强度值

    • texelWidth
    • texelHeight:这些参数影响检测到的边缘的可见性
    • edgeStrength:调整滤镜的动态范围。较高的值会导致边缘更牢固,但会饱和强度颜色空间。默认值为1.0。
    • threshold:高于此阈值的任何边缘将为黑色,低于白色的任何边缘。范围从0.0到1.0,默认值为0.8
  • GPUImageCannyEdgeDetectionFilter:这使用完整的Canny流程突出显示一像素宽的边缘

    • texelWidth
    • texelHeight:这些参数影响检测到的边缘的可见性
    • blurRadiusInPixels:高斯模糊的基础模糊半径。默认值为2.0。
    • blurTexelSpacingMultiplier:基础的模糊纹理像素间距乘数。默认值为1.0。
    • upperThreshold:任何梯度幅度大于此阈值的边都将通过并显示在最终结果中。默认值为0.4。
    • lowerThreshold:任何梯度幅度低于此阈值的边将失败,并从最终结果中删除。默认值为0.1。
  • GPUImageHarrisCornerDetectionFilter:在输入图像上运行哈里斯角点检测算法,并生成一个图像,这些角点为白色像素,其他所有颜色均为黑色。可以设置cornersDetectedBlock,并且在回调中将为您提供您要执行的所有其他操作的角列表(以标准化的0..1 X,Y坐标表示)。

    • blurRadiusInPixels:基础高斯模糊的半径。默认值为2.0。
    • 敏感度:一个内部比例因子,用于调整在滤镜中生成的边角图的动态范围。默认值为5.0。
    • threshold:检测到一个点作为拐角的阈值。根据尺寸,光线条件和iOS设备摄像头类型的不同,此方法可能会有很大的不同,因此可能需要一些试验才能确定适合您的情况。默认值为0.20。
附件下载及详细介绍及常见问题:https://github.com/BradLarson/GPUImage


收起阅读 »

Runtime底层原理--动态方法解析总结

方法的底层会编译成消息,消息进行递归,先从实例方法开始查找,到父类最后到NSObject。如果在汇编部分快速查找没有找到IMP,就会进入C/C++中的动态方法解析进入lookUpImpOrForward方法进行递归。动态方法解析动态方法解析分为实例方法和类方法...
继续阅读 »

方法的底层会编译成消息,消息进行递归,先从实例方法开始查找,到父类最后到NSObject。如果在汇编部分快速查找没有找到IMP,就会进入C/C++中的动态方法解析进入lookUpImpOrForward方法进行递归。

动态方法解析

动态方法解析分为实例方法和类方法两种。

实例方法查找imp流程和动态方法解析

比如执行一个Student实例方法eat,会先去这个类中查找是否有该方法(sel),如果有则进行存储以便下次直接从汇编部分快速查找。

// Try this class's cache.
// Student元类 - 父类 (根元类) -- NSObject
// resovleInstance 防止递归 --
imp = cache_getImp(cls, sel);
if (imp) goto done;

// Try this class's method lists.
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}

如果没有sel那么接下来去父类(直到NSObject)的缓存和方法列表找查找。如果在父类中找到先缓存再执行done.

// 元类的父类 - NSObject 是否有 实例方法
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}

// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}

如果最终还是没找到,则会进入动态方法解析_class_resolveMethod,先判断当前cls对象是不是元类,也就是如果是对象方法会走到_class_resolveInstanceMethod方法,

/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]

_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}

如果元类,那么执行_class_resolveInstanceMethod(cls, sel, inst)方法,该方法会执行lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, NO/*initialize*/, YES/*cache*/, NO/*resolver*/),查找当前的cls的isa是否实现了resolveInstanceMethod,也就是是否有自定义实现、是否重写了。如果查到了就会给类对象发送消息objc_msgSend,调起resolveInstanceMethod方法

/***********************************************************************
* lookUpImpOrNil.
* Like lookUpImpOrForward, but returns nil instead of _objc_msgForward_impcache
**********************************************************************/
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}

lookUpImpOrNil的内部是通过lookUpImpOrForward方法进行查找,再次回到递归调用。

如果还是没查到,这里就不会再次进入动态方法解析(注:如果再次进入动态方法解析会形成死递归),首先对cls的元类进行查找,然后元类的父类,也就是根元类(系统默认实现的虚拟的)进行查找、最终到NSObjece,只不过NSObjece中默认实现resolveInstanceMethod方法返回NO,也就是此时在元类进行查找的时候找到了resolveInstanceMethod方法,并停止继续查找,这就是为什么动态方法解析后的递归没有再次进入动态方法解析的原因。如果最终还是没有找到SEL_resolveInstanceMethod则说明程序有问题,直接返回。下面是isa走位图:


如果找到的imp不是转发的imp,则返回imp。
举个例子:
在Student中有个对象run方法,但是并没有实现,当调用run方法时,最终没有找到imp会崩溃。通过动态方法解析,实现run方法

#pragma mark - 动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {

NSLog(@"动态方法解析 - %@",self);
if (sel == @selector(run)) {
// 我们动态解析对象方法
NSLog(@"对象方法 run 解析走这里");
SEL readSEL = @selector(readBook);
Method readM= class_getInstanceMethod(self, readSEL);
IMP readImp = method_getImplementation(readM);
const char *type = method_getTypeEncoding(readM);
return class_addMethod(self, sel, readImp, type);
}
return [super resolveInstanceMethod:sel];
}

此时只是给对象方法添加了一个imp,接下来再次进入查找imp流程,重复之前的操作,只不过现在对象方法已经有了imp。

/***********************************************************************
* _class_resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}

BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
// ...省略N行代码

动态方法解析的实质: 经过漫长的查找并没有找到sel的imp,系统会发送resolveInstanceMethod消息,为了防止系统崩溃,可以在该方法内对sel添加imp,系统会自动再次查找imp。

类方法查找imp流程和动态方法解析

类方法查找imp流程和实例方法查找imp前面流程一样,也是从汇编部分快速查找,之后判断cls是不是元类,在元类方法列表中查找,如果元类中没有当前的sel,就去元类的父类中查找,还没有就去根元类的父类NSObject中查找,此时查找的就是NSObject中是否有这个实例对象方法,如果NSObject中也没有就会进入动态方法解析_class_resolveMethod。类对象这里的cls和对象方法不一样,因为cls是元类所以直接走_class_resolveClassMethod方法。进入_class_resolveClassMethod方法还是先判断resolveClassMethod方法是否有实现,之后发送消息objc_msgSend,这里和实例方法有所区别,类方法会执行_class_getNonMetaClass方法,内部实现getNonMetaClass,getNonMetaClass会判断当前cls是不是NSObject,判断当前的cls是不是根元类,也就是自己,接下来判断inst类对象,判断inst类对象的isa如果不是元类,那么返回类对象的父类,不是就返回类对象。在_class_resolveClassMethod方法中添加了imp后还是和实例方法一样,再次进入重新查找流程,此时如果还是没有,那么类方法还会再一次的进入_class_resolveInstanceMethod方法,和实例方法不同的是resolveInstanceMethod方法内部的cls是元类,所以找的方法也就是- (BOOL)resolveClassMethod:(SEL)sel,可以在NSObject中添加+ (BOOL)resolveClassMethod:(SEL)sel方法,这样无论类方法还是实例方法都会走到这里,可以作为防崩溃的处理。

/***********************************************************************
* getNonMetaClass
* Return the ordinary class for this class or metaclass.
* `inst` is an instance of `cls` or a subclass thereof, or nil.
* Non-nil inst is faster.
* Used by +initialize.
* Locking: runtimeLock must be read- or write-locked by the caller
**********************************************************************/
static Class getNonMetaClass(Class metacls, id inst)
{
static int total, named, secondary, sharedcache;
runtimeLock.assertLocked();

realizeClass(metacls);

total++;

// return cls itself if it's already a non-meta class
if (!metacls->isMetaClass()) return metacls;

// metacls really is a metaclass

// special case for root metaclass
// where inst == inst->ISA() == metacls is possible
if (metacls->ISA() == metacls) {
Class cls = metacls->superclass;
assert(cls->isRealized());
assert(!cls->isMetaClass());
assert(cls->ISA() == metacls);
if (cls->ISA() == metacls) return cls;
}

// use inst if available
if (inst) {
Class cls = (Class)inst;
realizeClass(cls);
// cls may be a subclass - find the real class for metacls
while (cls && cls->ISA() != metacls) {
cls = cls->superclass;
realizeClass(cls);
}
if (cls) {
assert(!cls->isMetaClass());
assert(cls->ISA() == metacls);
return cls;
}

我们在Student类中添加未实现的类方法walk,在NSObject类中添加一个对象方法walk,运行程序不会崩溃。类方法先递归,开始找父类,最终在NSObject类中好到对象方法walk。

TIP:对象方法存储在类中,类方法存储在元类里面,类对象以实例方法的形式存储在元类中。可以通过输出class_getInstanceMethod方法和class_getClassMethod方法的imp指针来验证,当然源码也可以解释在cls的元类中查找实例方法

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

return class_getInstanceMethod(cls->getMeta(), sel);
}

还可以通过LLDB进行验证,动态方法解析的时候执行lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)方法,这里的cls就是inst的元类

#   define ISA_MASK        0x00007ffffffffff8ULL

// -------------------------------------------------
#if SUPPORT_NONPOINTER_ISA

inline Class
objc_object::ISA()
{
assert(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}

这里看到初始化的时候isa.bits & ISA_MASK,我们先后打印cls和inst的信息,也可以验证当前指针指向当前的元类。


动态方法解析作用

适用于重定向,也可以做防崩溃处理,也可以做一些错误日志收集等等。动态方法解析本质就是提供机会(任何没有实现的方法都可以重新实现)。

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

收起阅读 »

7种经常使用的Vue.js模式?你居然还不知道!!

说实话,阅读文档并不是我们大多数人喜欢的事情,但当使用像Vue这样不断发展的现代前端框架时,很多东西会随着每一个新版本的发布而改变,你可能会错过一些后来推出的新的闪亮功能。让我们看一下那些有趣但不那么流行的功能和优化的写法。请记住,所有这些都是Vue文档的一部...
继续阅读 »

说实话,阅读文档并不是我们大多数人喜欢的事情,但当使用像Vue这样不断发展的现代前端框架时,很多东西会随着每一个新版本的发布而改变,你可能会错过一些后来推出的新的闪亮功能。让我们看一下那些有趣但不那么流行的功能和优化的写法。请记住,所有这些都是Vue文档的一部分。

7种Vue.js模式
1.处理加载状态
在大型应用程序中,我们可能需要将应用程序划分为更小的块,只有在需要时才从服务器加载组件。为了使这一点更容易,Vue允许你将你的组件定义为一个工厂函数,它异步解析你的组件定义。Vue只有在需要渲染组件时才会触发工厂函数,并将缓存结果,以便将来重新渲染。2.3版本的新功能是,异步组件工厂也可以返回一个如下格式的对象。

const AsyncComponent = () => ({
// 要加载的组件(应为Promise)
component: import('./MyComponent.vue'),
// 异步组件正在加载时要使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 显示加载组件之前的延迟。默认值:200ms。
delay: 200,
// 如果提供并超过了超时,则会显示error组件。默认值:无穷。
timeout: 3000
})

通过这种方法,你有额外的加载和错误状态、组件获取的延迟和超时等选项。

2.廉价的“v-once”静态组件
在Vue中渲染纯HTML元素的速度非常快,但有时你可能有一个包含大量静态内容的组件。在这种情况下,你可以通过在根元素中添加 v-once 指令来确保它只被评估一次,然后进行缓存,就像这样。

Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`
})

3.递归组件

组件可以在自己的模板中递归调用自己,但是,它们只能通过 name 选项来调用。

如果你不小心,递归组件也可能导致无限循环:

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

像上面这样的组件会导致“超过最大堆栈大小”的错误,所以要确保递归调用是有条件的即(使用 v-if 最终将为 false)

4.内联模板
当特殊属性 inline-template 存在于一个子组件上时,该组件将使用它的内部内容作为它的模板,而不是将其视为分布式内容,这允许更灵活的模板编写。

<my-component inline-template>
<div>
<p>These are compiled as the component's own template.</p>
<p>Not parent's transclusion content.</p>
</div>
</my-component>


5.动态指令参数
指令参数可以是动态的。例如,在 v-mydirective:[argument]=“value" 中, argument 可以根据组件实例中的数据属性更新!这使得我们的自定义指令可以灵活地在整个应用程序中使用。

这是一条指令,其中可以根据组件实例更新动态参数:

<div id="dynamicexample">
<h3>Scroll down inside this section ↓</h3>
<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>
Vue.directive('pin', {
bind: function (el, binding, vnode) {
el.style.position = 'fixed'
var s = (binding.arg == 'left' ? 'left' : 'top')
el.style[s] = binding.value + 'px'
}
})

new Vue({
el: '#dynamicexample',
data: function () {
return {
direction: 'left'
}
}
})

6.事件和键修饰符

对于 .passive.capture 和 .once 事件修饰符,Vue提供了可与 on 一起使用的前缀:


on: {
'!click': this.doThisInCapturingMode,
'~keyup': this.doThisOnce,
'~!mouseover': this.doThisOnceInCapturingMode
}



7.依赖注入(Provide/Inject)
有几种方法可以让两个组件在 Vue 中进行通信,它们各有优缺点。在2.2版本中引入的一种新方法是使用Provide/Inject的依赖注入。

这对选项一起使用,允许一个祖先组件作为其所有子孙的依赖注入器,无论组件层次结构有多深,只要它们在同一个父链上。如果你熟悉React,这与React的上下文功(context)能非常相似。

// parent component providing 'foo'
var Provider = {
provide: {
foo: 'bar'
},
// ...
}

// child component injecting 'foo'
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
// ...
}



收起阅读 »

iOS Files文件应用程序开发

前言:最近在做一个项目,需要用到文件选取、上传、下载功能,首先想到的就是iOS11自带的“文件”应用。“文件”算是一个中转站,是iOS系统的文件管理器,可以为各个项目提供私有的文件夹,进行文件管理。iOS11已经提供了相当完善的接口,本文基于此开发过程的总结,...
继续阅读 »

前言:

最近在做一个项目,需要用到文件选取、上传、下载功能,首先想到的就是iOS11自带的“文件”应用。“文件”算是一个中转站,是iOS系统的文件管理器,可以为各个项目提供私有的文件夹,进行文件管理。

iOS11已经提供了相当完善的接口,本文基于此开发过程的总结,给出iOS11的桌面“文件”应用程序进行相关开发的经验。文中若有错漏之处,恳请大家批评指正。

两种开发模式

1、将qq或微信的文档拷贝到自己项目中,即拷贝模式;

2、将qq或微信的文档存储到“文件”中,即存储模式

拷贝模式开发步骤:

(1)打开项目中的info.plist,添加“Document Types”键值:


(2)项目运行安装到真机上。打开微信或qq里的文档,从右上角的“...”按钮选择“用其他应用打开”;


(3)此时将看到自己的项目已经存在其他应用的列表上,选择“拷贝到xxx”,选择拷贝到自己开发的项目;


(4)点击“拷贝到xxx”后,将跳转到自己项目中。需要在自己项目的AppDelegate.m文件中处理回调;


(5)后续步骤可以做一个本地文件管理界面(类似相册图片的九宫格展示,或者列表形式),进行本地文件管理,读取、上传、下载,这里就不展开讨论了。

存储模式的开发步骤:

(1)打开项目中的info.plist,添加“Supports Document Browser”键值:


(2)项目运行安装到真机上。打开微信或qq里的文档,从右上角的“...”按钮选择“用其他应用打开”;

(3)在弹窗中选择存储到“文件”,将文件存储到系统的“文件”应用程序;


(4)在打开的“文件”应用程序中,选择添加到自己的项目;


(5)在项目中编写代码,获取“文件”应用程序中刚刚的存储文件,代码如下:


(6)运行代码,将项目安装到真机上,进入代码所在的页面,打开“文件”面板,即可看到在“文件”里的本项目名称的文件夹,选择刚才的文件即可。


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

收起阅读 »

vue传值方式总结 (十二种方法)

一.父传子传递(1)在父组件的子组件标签上绑定一个属性,挂载要传输的变量(2)在子组件中通过props来接受数据,props可以是数组也可以是对象,接受的数据可以直接使用 props: [“属性 名”] props:{属性名:数据类型}代码示例://父组件&l...
继续阅读 »

一.父传子传递

(1)在父组件的子组件标签上绑定一个属性,挂载要传输的变量
(2)在子组件中通过props来接受数据,props可以是数组也可以是对象,接受的数据可以直接使用 props: [“属性 名”] props:{属性名:数据类型}
代码示例:

//父组件
<template>
<div>
<i>父组件</i>
<!--页面使用-->
<son :data='name'></son>
</div>
</template>

<script>
import son from "./son.vue";//导入父组件
export default {
components: { son },//注册组件
name: "父组件",
data() {
return {
name: "Frazier", //父组件定义变量
};
},
};
</script>

//子组件
<template>
<div>{{data}}</div>
</template>

<script>
export default {
components: { },
name: '子组件',
props:["data"],
};
</script>


二.子传父传递

(1)在父组件的子组件标签上自定义一个事件,然后调用需要的方法
(2)在子组件的方法中通过 this.$emit(“事件”)来触发在父组件中定义的事件,数据是以参数的形式进行传递的
代码示例:

//父组件
<template>
<div>
<i>父组件</i>
<!--页面使用-->
<son @lcclick="lcclick"></son>//自定义一个事件
</div>
</template>

<script>
import son from "./son.vue"; //导入父组件
export default {
components: { son }, //注册组件
name: "父组件",
data() {
return {};
},
methods: {
lcclick(){
alert('子传父')
}
},
};
</script>

//子组件
<template>
<div>
<button @click="lcalter">点我</button>
</div>
</template>

<script>
export default {
components: { },
name: '子组件',
methods: {
lcalter(){
this.$emit('lcclick')//通过emit来触发事件
}
},
};
</script>

三.兄弟组件通信(bus总线)
(1)在src中新建一个Bus.js的文件,然后导出一个空的vue实例
(2)在传输数据的一方引入Bus.js 然后通过Bus.e m i t ( “ 事 件 名 ” , " 参 数 " ) 来 来 派 发 事 件 , 数 据 是 以 emit(“事件名”,"参数")来来派发事件,数据是以emit(“事件名”,"参数")来来派发事件,数据是以emit()的参 数形式来传递
(3)在接受的数据的一方 引入 Bus.js 然后通过 Bus.$on(“事件名”,(data)=>{data是接受的数据})
图片示例:





四.ref/refs(父子组件通信)

(1)ref 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,
(2)可以通过实例直接调用组件的方法或访问数据。也算是子组件向父组件传值的一种
代码示例:

//父组件
<template>
<div>
<button @click="sayHello">sayHello</button>
<child ref="childForRef"></child>
</div>
</template>
<script>
import child from './child.vue'
export default {
components: { child },
data () {
return {
childForRef: null,
}
},
mounted() {
this.childForRef = this.$refs.childForRef;
console.log(this.childForRef.name);
},
methods: {
sayHello() {
this.childForRef.sayHello()
}
}
}
</script>

//子组件
<template>
<div>child 的内容</div>
</template>
<script>
export default {
data () {
return {
name: '我是 child',
}
},
methods: {
sayHello () {
console.log('hello');
alert('hello');
}
}
}
</script>

五.Vuex通信

组件通过 dispatch 到 actions,actions 是异步操作,再 actions中通过 commit 到 mutations,mutations 再通过逻辑操作改变 state,从而同步到组件,更新其数据状态
代码示例:

//父组件
template>
<div id="app">
<ChildA/>
<ChildB/>
</div>
</template>
<script>
import ChildA from './ChildA' // 导入A组件
import ChildB from './ChildB' // 导入B组件
export default {
components: {ChildA, ChildB} // 注册组件
}
</script>

//子组件A
<template>
<div id="childA">
<h1>我是A组件</h1>
<button @click="transform">点我让B组件接收到数据</button>
<p>因为点了B,所以信息发生了变化:{{BMessage}}</p>
</div>
</template>
<script>
export default {
data() {
return {
AMessage: 'Hello,B组件,我是A组件'
}
},
computed: {
BMessage() {
// 这里存储从store里获取的B组件的数据
return this.$store.state.BMsg
}
},
methods: {
transform() {
// 触发receiveAMsg,将A组件的数据存放到store里去
this.$store.commit('receiveAMsg', {
AMsg: this.AMessage
})
}
}
}
</script>
//子组件B
<template>
<div id="childB">
<h1>我是B组件</h1>
<button @click="transform">点我让A组件接收到数据</button>
<p>点了A,我的信息发生了变化:{{AMessage}}</p>
</div>
</template>

<script>
export default {
data() {
return {
BMessage: 'Hello,A组件,我是B组件'
}
},
computed: {
AMessage() {
// 这里存储从store里获取的A组件的数据
return this.$store.state.AMsg
}
},
methods: {
transform() {
// 触发receiveBMsg,将B组件的数据存放到store里去
this.$store.commit('receiveBMsg', {
BMsg: this.BMessage
})
}
}
}
</script>
//vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
AMsg: '',
BMsg: ''
}

const mutations = {
receiveAMsg(state, payload) {
// 将A组件的数据存放于state
state.AMsg = payload.AMsg
},
receiveBMsg(state, payload) {
// 将B组件的数据存放于state
state.BMsg = payload.BMsg
}
}

export default new Vuex.Store({
state,
mutations
})

六.$parent
通过parent可以获父组件实例 ,然 后通过这个实例就可以访问父组件的属 性和方法 ,它还有一个兄弟parent可以获父组件实例,然后通过这个实例就可以访问父组件的属性和方法,它还有一个兄弟parent可以获父组件实例,然后通过这个实例就可以访问父组件的属性和方法,它还有一个兄弟root,可以获取根组件实例。
代码示例:

// 获父组件的数据
this.$parent.foo

// 写入父组件的数据
this.$parent.foo = 2

// 访问父组件的计算属性
this.$parent.bar

// 调用父组件的方法
this.$parent.baz()

//在子组件传给父组件例子中,可以使用this.$parent.getNum(100)传值给父组件。

七.sessionStorage传值

sessionStorage 是浏览器的全局对象,存在它里面的数据会在页面关闭时清除 。运用这个特性,我们可以在所有页面共享一份数据。
代码示例:

// 保存数据到 sessionStorage
sessionStorage.setItem('key', 'value');

// 从 sessionStorage 获取数据
let data = sessionStorage.getItem('key');

// 从 sessionStorage 删除保存的数据
sessionStorage.removeItem('key');

// 从 sessionStorage 删除所有保存的数据
sessionStorage.clear();

注意:里面存的是键值对,只能是字符串类型,如果要存对象的话,需要使用 let objStr = JSON.stringify(obj) 转成字符串然后再存储(使用的时候 let obj = JSON.parse(objStr) 解析为对象)。
推荐一个库 good-storage ,它封装了sessionStorage ,可以直接用它的API存对象

//localStorage
storage.set(key,val)
storage.get(key, def)
//sessionStorage
storage.session.set(key, val)
storage.session.get(key, val)

八.路由传值
使用问号传值
A页面跳转B页面时使用 this.r o u t e r . p u s h ( ’ / B ? n a m e = d a n s e e k ’ ) B 页 面 可 以 使 用 t h i s . router.push(’/B?name=danseek’) B页面可以使用 this.router.push(’/B?name=danseek’)B页面可以使用this.route.query.name 来获取A页面传过来的值
上面要注意router和route的区别
使用冒号传值
配置如下路由:

{
path: '/b/:name',
name: 'b',
component: () => import( '../views/B.vue')
},

在B页面可以通过 this.$route.params.name 来获取路由传入的name的值

使用父子组件传值
由于router-view本身也是一个组件,所以我们也可以使用父子组件传值方式传值,然后在对应的子页面里加上props,因为type更新后没有刷新路由,所以不能直接在子页面的mounted钩子里直接获取最新type的值,而要使用watch

<router-view :type="type"></router-view>

// 子页面
props: ['type']
watch: {
type(){
// console.log("在这个方法可以时刻获取最新的数据:type=",this.type)
},
},

九.祖传孙 $attrs

正常情况下需要借助父亲的props作为中间过渡,但是这样在父亲组件就会多了一些跟父组件业务无关的属性,耦合度高,借助$attrs可以简化些,而且祖跟孙都无需做修改
祖组件:

<template>
<section>
<parent name="grandParent" sex="男" age="88" hobby="code" @sayKnow="sayKnow"></parent>
</section>
</template>

<script>
import Parent from './Parent'
export default {
name: "GrandParent",
components: {
Parent
},
data() {
return {}
},
methods: {
sayKnow(val){
console.log(val)
}
},
mounted() {
}
}
</script>

template>
<section>
<p>父组件收到</p>
<p>祖父的名字:{{name}}</p>
<children v-bind="$attrs" v-on="$listeners"></children>
</section>
</template>

<script>
import Children from './Children'

export default {
name: "Parent",
components: {
Children
},
// 父组件接收了name,所以name值是不会传到子组件的
props:['name'],
data() {
return {}
},
methods: {},
mounted() {
}
}
</script>
<template>
<section>
<p>子组件收到</p>
<p>祖父的名字:{{name}}</p>
<p>祖父的性别:{{sex}}</p>
<p>祖父的年龄:{{age}}</p>
<p>祖父的爱好:{{hobby}}</p>

<button @click="sayKnow">我知道啦</button>
</section>
</template>

<script>
export default {
name: "Children",
components: {},
// 由于父组件已经接收了name属性,所以name不会传到子组件了
props:['sex','age','hobby','name'],
data() {
return {}
},
methods: {
sayKnow(){
this.$emit('sayKnow','我知道啦')
}
},
mounted() {
}
}
</script>

十.孙传祖使用$listeners

文字内容同第九个

祖组件

<template>
<div id="app">
<children-one @eventOne="eventOne"></children-one>
{{ msg }}
</div>
</template>
<script>
import ChildrenOne from '../src/components/children.vue'
export default {
name: 'App',
components: {
ChildrenOne,
},
data() {
return {
msg: ''
}
},
methods: {
eventOne(value) {
this.msg = value
}
}
}
</script>

//父组件
<template>
<div>
<children-two v-on="$listeners"></children-two>
</div>
</template>

<script>
import ChildrenTwo from './childrenTwo.vue'

export default {
name: 'childrenOne',
components: {
ChildrenTwo
}
}
</script>
//子组建
<template>
<div>
<button @click="setMsg">点击传给祖父</button>
</div>
</template>

<script>
export default {
name: 'children',
methods: {
setMsg() {
this.$emit('eventOne', '123')
}
}
}
</script>

十一.promise传参

promise 中 resolve 如何传递多个参数

//类似与这样使用,但实际上后面两个参数无法获取
promise = new Promise((resolve,reject)=>{
let a = 1
let b = 2
let c = 3
resolve(a,b,c)
})
promise.then((a,b,c)=>{
console.log(a,b,c)
})

resolve() 只能接受并处理一个参数,多余的参数会被忽略掉。
如果想多个用数组,或者对象方式。。
数组

promise = new Promise((resolve,reject)=>{
resolve([1,2,3])
})
promise.then((arr)=>{
console.log(arr[0],arr[1],arr[2])
})

对象

promise = new Promise((resolve,reject)=>{
resolve({a:1,b:2,c:3})
})
promise.then(obj=>{
console.log(obj.a,obj.b,obj.c)
})

十二.全局变量

定义一个全局变量,在有值的组件直接赋值,在需要的组件内直接使用就可以了

本文链接:https://blog.csdn.net/Frazier1995/article/details/116069811

收起阅读 »

前端必须要了解的一些知识 (十一)

六种基本数据类型undefinednullstringbooleannumbersymbol(ES6)一种引用类型Objectstringlength属性prototype 添加的方法或属性在所有的实例上共享charAt(index) 返回值 cha...
继续阅读 »

六种基本数据类型

undefined
null
string
boolean
number
symbol(ES6)


  • 一种引用类型
  • Object

string

  1. length属性
  2. prototype 添加的方法或属性在所有的实例上共享
  3. charAt(index) 返回值
  4.  charCodeAt(index) 返回字符的Unicode编码
  5.  indexOf(searchVal,index) 值所在的位置 param2是从位置开始算
  6. search()方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符
var str = 'abcDEF'; 
console.log(str.search('c')); //返回2
console.log(str.search('d')); //返回-1
console.log(str.search(/d/i)); //返回3


Object

  1. 对象可以通过执行new操作符后跟要创建的对象类型的名称来创建。

前端错误的分类

即时运行错误:代码错误

资源加载错误


错误的捕获方式

代码错误

try...catch

window.onerror


资源错误

object.onerror(不会冒泡到window):节点上绑定error事件

performance.getEntries:获取资源的加载时长

error的事件捕获:用捕获不用冒泡可以监控


上报错误的基本原理

1.ajax通讯方式上报

2.image对象上报

跨域的代码错误怎么捕获


收起阅读 »

前端必须要了解的一些知识 (十)

任务队列同步任务异步任务console.log(1)setTimeout(){console.log(2)}console.log(3)1,3,22,3,5,4,1console.log(A)while(true){}console.log(B)//只输出A ...
继续阅读 »
任务队列
同步任务
异步任务
console.log(1)
setTimeout(){
console.log(2)
}
console.log(3)
1,3,2


2,3,5,4,1

console.log(A)
while(true){
}
console.log(B)
//只输出A while是个同步队列 。 进入死循环
----------------------------
console.log(A)
settimeout(){
console.log(B)
}
while(true){
}
//仍然只输出A 。 同步没执行完不会执行异步
-----------------------------
for(var i=0;i<4;i++){
settimeout(()=>{
console.log(i)
},1000)
}
//4次4



eventloop
异步:settimeout DOM事件 Promise
将所有任务看成两个队列:执行队列与事件队列。
执行队列是同步的,事件队列是异步的,宏任务放入事件列表,微任务放入执行队列之后,事件队列之前。
当执行完同步代码之后,就会执行位于执行列表之后的微任务,然后再执行事件列表中的

异步加载的方式
1:动态脚本加载
2:defer
defer在html解析完才会执行,如果是多个,按照加载顺序依次执行
3:async
加载完后立即执行 。 如果是多个 。 执行顺序和加载顺序无关


缓存分类
1 强缓存
如果两个时间都下发了 以后者为准
expires:过期时间(绝对时间服务器的时间)
cache-control 。 相对时间 。 拿到资源3600s之内不请求服务器
2:协商缓存(查资料)
last-modified




dns-prefetch(记住)


收起阅读 »

前端必须要了解的一些知识 (九)

CSRF跨站请求伪造攻击原理网站B引诱用户点击A防御措施token验证refere验证 来源验证隐藏令牌XSS跨域脚本攻击原理:提交区注入脚本 让js不能执行doctype的作用DTD 定义html文档类型 。 浏览器根据这个去解析 声明文档类型4.0版本...
继续阅读 »
CSRF
跨站请求伪造
攻击原理
网站B引诱用户点击A

防御措施
token验证
refere验证 来源验证
隐藏令牌

XSS
跨域脚本攻击
原理:提交区注入脚本 让js不能执行


doctype的作用
DTD 定义html文档类型 。 浏览器根据这个去解析 声明文档类型
4.0版本有两个模式 。 一个严格模式 。 一个是传统模式

浏览哎渲染过程


重拍reflow


重绘repaint
如何避免最小避免repaint 。

布局layout


doctype的作用
DTD 定义html文档类型 。 浏览器根据这个去解析 声明文档类型
4.0版本有两个模式 。 一个严格模式 。 一个是传统模式

浏览哎渲染过程


重拍reflow


重绘repaint
如何避免最小避免repaint 。

布局layout

收起阅读 »

前端必须要了解的一些知识 (八)

什么是同源策略限制协议 域名 端口不是一个源的文档不能操作另一个源的文档限制如下:cookie localStrorage indexDB 无法获取DOM无法获得Ajax请求不能发送前后端如何通信Ajax//同源下的通讯websocket//不限制同源c...
继续阅读 »
什么是同源策略

限制
协议 域名 端口
不是一个源的文档不能操作另一个源的文档
限制如下:
cookie localStrorage indexDB 无法获取
DOM无法获得
Ajax请求不能发送


前后端如何通信
Ajax//同源下的通讯
websocket//不限制同源
cors//支持跨域也支持同源


如何创建Ajax(用原生)
XMLHttpRequest对象的工作流程
兼容性处理
事件触发条件
事件触发顺序


跨域通讯的几种方式
JSONP
实现原理

Hash
hash改变页面不刷新 指url#以后的东西
window.onhashchange


postMessage
h5新的标准

Websocket
不受同源限制


CORS
白话:支持跨域通讯的Ajax
如果跨域浏览器会拦截 会在请求头上添加origin
http://www.ruanyifeng.com/blog/2016/04/cors.html

收起阅读 »

Topbar的扩展:AwesomeBar

AwesomeBar该控件时Topbar的一个扩展,类似于Actionbar或者Toolbar。可结合DrawerLayout使用。 效果如下:gradle配置如下module的build.gradledependencies { compile 'c...
继续阅读 »

AwesomeBar

该控件时Topbar的一个扩展,类似于Actionbar或者Toolbar。可结合DrawerLayout使用。

效果如下:


gradle配置如下

module的build.gradle

dependencies {
compile 'com.github.florent37:awesomebar:1.0.0'
}

用法

<com.github.florent37.awesomebar.AwesomeBar
android:id="@+id/awesomeBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:elevation="4dp"/>
awesomeBar = (AwesomeBar) findViewById(R.id.awesomeBar);
awesomeBar.addAction(R.drawable.awsb_ic_edit_animated, "A");
awesomeBar.addAction(R.drawable.awsb_ic_edit_animated, "b");
awesomeBar.addAction(R.drawable.awsb_ic_edit_animated, "c");

awesomeBar.setActionItemClickListener(new AwesomeBar.ActionItemClickListener() {
@Override
public void onActionItemClicked(int position, ActionItem actionItem) {
switch (position) {
case 0:
toast.setText("A");
break;
case 1:
toast.setText("B");
break;
case 2:
toast.setText("C");
break;
}
toast.show();
}
});

awesomeBar.setOnMenuClickedListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
toast.setText("menu");
toast.show();
}
});

github地址:https://github.com/florent37/AwesomeBar


Github地址:https://github.com/florent37/AwesomeBar

下载地址:AwesomeBar-master.zip

收起阅读 »

便捷相机:CameraFragment

CameraFragmentCameraFragment可以帮助你快速实现打开相机视图,并提供便捷的API来捕获图片。 效果如下:使用说明:初始化//you can configure the fragment ...
继续阅读 »

CameraFragment

CameraFragment可以帮助你快速实现打开相机视图,并提供便捷的API来捕获图片。

效果如下:


使用说明:

初始化

  1. //you can configure the fragment by the configuration builder
  2. CameraFragment cameraFragment = CameraFragment.newInstance(new Configuration.Builder().build());
  3. getSupportFragmentManager().beginTransaction()
  4.                 .replace(R.id.content, cameraFragment, FRAGMENT_TAG)
  5.                 .commit();

你可以直接使用下面的代码拍照或者录制视频:

  1. cameraFragment.takePhotoOrCaptureVideo(callback);

切换Flash 模式enable / disabled ( AUTO / OFF / ON )

  1. cameraFragment.toggleFlashMode();

改变Camera类型(前置或者后置):

  1. cameraFragment.switchCameraTypeFrontBack();

设置Camera行为(拍照还是录制视频):

  1. cameraFragment.switchActionPhotoVideo();

还可以设置大小(分辨率):

  1. cameraFragment.openSettingDialog();

Result

在CameraFragmentResultListener中得到录制(或者拍照)的结果

  1. cameraFragment.setResultListener(new CameraFragmentResultListener() {
  2.        @Override
  3.        public void onVideoRecorded(byte[] bytes, String filePath) {
  4.                 //called when the video record is finished and saved
  5.                 startActivityForResult(PreviewActivity.newIntentVideo(MainActivity.this, filePath));
  6.        }
  7.        @Override
  8.        public void onPhotoTaken(byte[] bytes, String filePath) {
  9.                 //called when the photo is taken and saved
  10.                 startActivity(PreviewActivity.newIntentPhoto(MainActivity.this, filePath));
  11.        }
  12. });

Camera Listener

  1. cameraFragment.setStateListener(new CameraFragmentStateListener() {
  2.     //when the current displayed camera is the back
  3.     void onCurrentCameraBack();
  4.     //when the current displayed camera is the front
  5.     void onCurrentCameraFront();
  6.     //when the flash is at mode auto
  7.     void onFlashAuto();
  8.     //when the flash is at on
  9.     void onFlashOn();
  10.     //when the flash is off
  11.     void onFlashOff();
  12.     //if the camera is ready to take a photo
  13.     void onCameraSetupForPhoto();
  14.     //if the camera is ready to take a video
  15.     void onCameraSetupForVideo();
  16.     //when the camera state is "ready to record a video"
  17.     void onRecordStateVideoReadyForRecord();
  18.     //when the camera state is "recording a video"
  19.     void onRecordStateVideoInProgress();
  20.     //when the camera state is "ready to take a photo"
  21.     void onRecordStatePhoto();
  22.     //after the rotation of the screen / camera
  23.     void shouldRotateControls(int degrees);
  24.     void onStartVideoRecord(File outputFile);
  25.     void onStopVideoRecord();
  26. });

Github地址:https://github.com/florent37/CameraFragment

下载地址:CameraFragment-master.zip

收起阅读 »

水平展示日历控件:HorizontalCalendar

HorizontalCalendar该库是一个水平展示日历的控件,也是通过RecycerView来实现的。 效果如下:配置 模块中 build.gradle: repositories { jcenter() } dep...
继续阅读 »

HorizontalCalendar

该库是一个水平展示日历的控件,也是通过RecycerView来实现的。

效果如下:


配置


模块中 build.gradle:


repositories {
jcenter()
}

dependencies {
compile 'devs.mulham.horizontalcalendar:horizontalcalendar:1.3.4'
}


使用



  • 添加 HorizontalCalendarView 到你的layout


<android.support.design.widget.AppBarLayout>
............

<devs.mulham.horizontalcalendar.HorizontalCalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
app:textColorSelected="#FFFF"/>

android.support.design.widget.AppBarLayout>



  • 定义你的开始和结束设置日历范围的日期:



/* starts before 1 month from now */
Calendar startDate = Calendar.getInstance();
startDate.add(Calendar.MONTH, -1);

/* ends after 1 month from now */
Calendar endDate = Calendar.getInstance();
endDate.add(Calendar.MONTH, 1);


  • 可以用建造者模式构建


HorizontalCalendar horizontalCalendar = new HorizontalCalendar.Builder(this, R.id.calendarView)
.range(startDate, endDate)
.datesNumberOnScreen(5)
.build();


  • Fragment中使用:


HorizontalCalendar horizontalCalendar = new HorizontalCalendar.Builder(rootView, R.id.calendarView)
...................


  • 监听日期改变监听器


horizontalCalendar.setCalendarListener(new HorizontalCalendarListener() {
@Override
public void onDateSelected(Calendar date, int position) {
//do something
}
});


  • 监听滑动和长按


horizontalCalendar.setCalendarListener(new HorizontalCalendarListener() {
@Override
public void onDateSelected(Calendar date, int position) {

}

@Override
public void onCalendarScroll(HorizontalCalendarView calendarView,
int dx, int dy) {

}

@Override
public boolean onDateLongClicked(Calendar date, int position) {
return true;
}
});

定制



  • layout:


<devs.mulham.horizontalcalendar.HorizontalCalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:textColorNormal="#bababa"
app:textColorSelected="#FFFF"
app:selectorColor="#c62828" //default to colorAccent
app:selectedDateBackground="@drawable/myDrawable"/>


  • Activity 或者 Fragment 使用 HorizontalCalendar.Builder:


HorizontalCalendar horizontalCalendar = new HorizontalCalendar.Builder(this, R.id.calendarView)
.range(Calendar startDate, Calendar endDate)
.datesNumberOnScreen(int number) // Number of Dates cells shown on screen (default to 5).
.configure() // starts configuration.
.formatTopText(String dateFormat) // default to "MMM".
.formatMiddleText(String dateFormat) // default to "dd".
.formatBottomText(String dateFormat) // default to "EEE".
.showTopText(boolean show) // show or hide TopText (default to true).
.showBottomText(boolean show) // show or hide BottomText (default to true).
.textColor(int normalColor, int selectedColor) // default to (Color.LTGRAY, Color.WHITE).
.selectedDateBackground(Drawable background) // set selected date cell background.
.selectorColor(int color) // set selection indicator bar's color (default to colorAccent).
.end() // ends configuration.
.defaultSelectedDate(Calendar date) // Date to be selected at start (default to current day `Calendar.getInstance()`).
.build();


更多的自定义


builder.configure()
.textSize(float topTextSize, float middleTextSize, float bottomTextSize)
.sizeTopText(float size)
.sizeMiddleText(float size)
.sizeBottomText(float size)
.colorTextTop(int normalColor, int selectedColor)
.colorTextMiddle(int normalColor, int selectedColor)
.colorTextBottom(int normalColor, int selectedColor)
.end()

月份 模式


水平日历只能显示月  添加模式(HorizontalCalendar.mode.MONTHS)例如:


horizontalCalendar = new HorizontalCalendar.Builder(this, R.id.calendarView)
.range(Calendar startDate, Calendar endDate)
.datesNumberOnScreen(int number)
.mode(HorizontalCalendar.Mode.MONTHS)
.configure()
.formatMiddleText("MMM")
.formatBottomText("yyyy")
.showTopText(false)
.showBottomText(true)
.textColor(Color.LTGRAY, Color.WHITE)
.end()
.defaultSelectedDate(defaultSelectedDate)

事件


可以为每个日期提供事件列表,这些事件将在日期下用圆圈表示:


builder.addEvents(new CalendarEventsPredicate() {

@Override
public List<CalendarEvent> events(Calendar date) {
// test the date and return a list of CalendarEvent to assosiate with this Date.
}
})

重新配置


初始化后可以更改水平日历配置:



  • 更改日历日期范围:



horizontalCalendar.setRange(Calendar startDate, Calendar endDate);


  • 更改默认(未选定)项目样式:



horizontalCalendar.getDefaultStyle()
.setColorTopText(int color)
.setColorMiddleText(int color)
.setColorBottomText(int color)
.setBackground(Drawable background);


  • 改变选中样式


horizontalCalendar.getSelectedItemStyle()
.setColorTopText(int color)
..............


  • 更改格式、文本大小和选择器颜色:



horizontalCalendar.getConfig()
.setSelectorColor(int color)
.setFormatTopText(String format)
.setSizeTopText(float size)
..............

重要的


一定要调用horizontalCalendar.refresh();完成更改后


特征



  • 禁用特定HorizontalCalendarPredicate, 也可以使用指定禁用日期的唯一样式CalendarItemStyle:


builder.disableDates(new HorizontalCalendarPredicate() {
@Override
public boolean test(Calendar date) {
return false; // return true if this date should be disabled, false otherwise.
}

@Override
public CalendarItemStyle style() {
return null; // create and return a new Style for disabled dates, or null if no styling needed.
}
})


  • 选择特定的日期通过编程方式选择是否播放动画:



horizontalCalendar.selectDate(Calendar date, boolean immediate); // set immediate to false to ignore animation.
// or simply
horizontalCalendar.goToday(boolean immediate);


  • 检查日历中是否包含日期:



horizontalCalendar.contains(Calendar date);


  • 检查两个日期是否相等(年、月、日):



Utils.isSameDate(Calendar date1, Calendar date2);


  • 获取天两个日期之间:



Utils.daysBetween(Calendar startInclusive, Calendar endExclusive);


Github地址:https://github.com/Mulham-Raee/HorizontalCalendar

下载地址:Horizontal-Calendar-master.zip

收起阅读 »

二维的RecyclerView控件:excelPanel

excelPanel提供一个二维的RecyclerView控件。 效果如下:导入到项目中compile 'cn.zhouchaoyuan:excelpanel:1.0.5' 使用 1、添加xml<cn.zhouchaoyuan.excelpanel.Ex...
继续阅读 »

excelPanel

提供一个二维的RecyclerView控件。

效果如下:


导入到项目中

compile 'cn.zhouchaoyuan:excelpanel:1.0.5'


使用


1、添加xml

<cn.zhouchaoyuan.excelpanel.ExcelPanel
android:id="@+id/content_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:left_cell_width="@dimen/room_status_cell_length"
app:normal_cell_width="@dimen/room_status_cell_length"
app:top_cell_height="@dimen/room_status_cell_length" />

配置app属性


app:left_cell_width //left header cell's width, not support wrap_content
app:normal_cell_width //container cell's width, not support wrap_content
app:top_cell_height //top header cell's height, not support wrap_content


2、定义自定义适配器

适配器必须扩展BaseExcelPanelAdapter并重写七个方法,如下所示:

public class Adapter extends BaseExcelPanelAdapter<RowTitle, ColTitle, Cell>{

public Adapter(Context context) {
super(context);
}

//=========================================normal cell=========================================
@Override
public RecyclerView.ViewHolder onCreateCellViewHolder(ViewGroup parent, int viewType) {
return null;
}

@Override
public void onBindCellViewHolder(RecyclerView.ViewHolder holder, int verticalPosition, int horizontalPosition) {

}

//=========================================top cell===========================================
@Override
public RecyclerView.ViewHolder onCreateTopViewHolder(ViewGroup parent, int viewType) {
return null;
}

@Override
public void onBindTopViewHolder(RecyclerView.ViewHolder holder, int position) {

}

//=========================================left cell===========================================
@Override
public RecyclerView.ViewHolder onCreateLeftViewHolder(ViewGroup parent, int viewType) {
return null;
}

@Override
public void onBindLeftViewHolder(RecyclerView.ViewHolder holder, int position) {

}

//=========================================top left cell=======================================
@Override
public View onCreateTopLeftView() {
return null;
}
}


3、使用你的适配器

//==============================
private List<RowTitle> rowTitles;
private List<ColTitle> colTitles;
private List<List<Cell>> cells;
private ExcelPanel excelPanel;
private CustomAdapter adapter;
private View.OnClickListener blockListener
//..........................................
excelPanel = (ExcelPanel) findViewById(R.id.content_container);
adapter = new CustomAdapter(this, blockListener);
excelPanel.setAdapter(adapter);
excelPanel.setOnLoadMoreListener(this);//your Activity or Fragment implement ExcelPanel.OnLoadMoreListener
adapter.setAllData(colTitles, rowTitles, cells);
adapter.enableFooter();//load more, you can also call disableFooter()----default
adapter.enableHeader();//load history, you can also call disableHeader()----default













如果使用setOnLoadMoreListener(…)和enableHeader(),则必须调用addHistorySize(int)来告诉ExcelPanel添加了多少数据。

Github地址:https://github.com/zhouchaoyuan/excelPanel

下载地址:excelPanel-master.zip

收起阅读 »

多媒体选择器库:boxing

boxingboxing是一个多媒体选择器库。 可以选择一张或者多张图片,提供预览和裁剪功能。 同样支持gif图,选择视频和图像压缩功能。 (B站出品哦!!)效果如下:集成步骤很简单根据自己的需求添加对应的依赖就可以了,我的和官方demo一样用到了ucro...
继续阅读 »

boxing

boxing是一个多媒体选择器库。

可以选择一张或者多张图片,提供预览和裁剪功能。

同样支持gif图,选择视频和图像压缩功能。

(B站出品哦!!)

效果如下:


集成步骤很简单根据自己的需求添加对应的依赖就可以了,我的和官方demo一样用到了ucrop裁剪库,还有glide相关的,所以图片加载都是用的glide


  compile('com.yalantis:ucrop:2.2.0') {
exclude group: 'com.android.support'
exclude group: 'com.squareup.okio'
exclude group: 'com.squareup.okhttp3'
}
compile 'com.bilibili:boxing-impl:0.8.0'
compile 'jp.wasabeef:glide-transformations:2.0.1'
compile 'com.github.bumptech.glide:glide:3.7.0'

初始化图片加载(必选)

BoxingMediaLoader.getInstance().init(new IBoxingMediaLoader()); // 需要实现IBoxingMediaLoader

初始化图片裁剪(可选)

BoxingCrop.getInstance().init(new IBoxingCrop()); // 需要实现 IBoxingCrop


//进入选择图片的页面
public void pickIcon(View view) {
String cachePath = BoxingFileHelper.getCacheDir(this);
if (TextUtils.isEmpty(cachePath)) {
Toast.makeText(getApplicationContext(), R.string.boxing_storage_deny, Toast.LENGTH_SHORT).show();
return;
}
Uri destUri = new Uri.Builder()
.scheme("file")
.appendPath(cachePath)
.appendPath(String.format(Locale.US, "%s.png", System.currentTimeMillis()))
.build();
BoxingConfig singleCropImgConfig = new BoxingConfig(BoxingConfig.Mode.SINGLE_IMG).needCamera(R.mipmap.camera_white).withCropOption(new BoxingCropOption(destUri))
.withMediaPlaceHolderRes(R.mipmap.ic_default_image);
Boxing.of(singleCropImgConfig).withIntent(this, MyBoxingActivity.class).start(this, REQUEST_CODE);
}

//得到裁剪后的结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
final ArrayList<BaseMedia> medias = Boxing.getResult(data);
if (requestCode == REQUEST_CODE && medias != null && medias.size() > 0) {
BaseMedia baseMedia = medias.get(0);
String path = baseMedia.getPath();
Log.e("onActivityResult", "onActivityResult: " + path);
Glide.with(this)
.load(path)
.dontAnimate()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.mipmap.user_icon)
.placeholder(R.mipmap.user_icon)
.bitmapTransform(new CropCircleTransformation(this))
.into(head);

}
}
}

接下来简单介绍下ucrop裁剪库的相关属性设置,需要注意的是清单文件要配置UCropActivity


   UCrop.Options crop = new UCrop.Options();
crop.setCompressionFormat(Bitmap.CompressFormat.PNG);//设置裁剪的质量
crop. setHideBottomControls(true);//影藏图片下面的操作控制的界面
crop.withMaxResultSize(cropConfig.getMaxWidth(), cropConfig.getMaxHeight());//最终的剪裁尺寸
crop.withAspectRatio(cropConfig.getAspectRatioX(), cropConfig.getAspectRatioY());//剪裁的比例
crop.setStatusBarColor(ActivityCompat.getColor(context, R.color.colorPrimary));//设置状态栏颜色
crop.setToolbarColor(context.getResources().getColor(R.color.boxing_black1));//是指toolbar颜色
crop.setShowCropGrid(false);//是否显示网格线
UCrop.of(uri, cropConfig.getDestination())
.withOptions(crop)
.start(context, fragment, requestCode);

Github地址:https://github.com/Bilibili/boxing

下载地址:boxing-master.zip

收起阅读 »

快速实现TabLayout和CoordinatorLayout:CoordinatorTabLayout

说明:CoordinatorTabLayout是一个自定义组合控件,可快速实现TabLayout与CoordinatorLayout相结合的样式 继承至CoordinatorLayout, 在该组件下面使用了CollapsingToolbarLayout包含T...
继续阅读 »

说明:CoordinatorTabLayout是一个自定义组合控件,可快速实现TabLayout与CoordinatorLayout相结合的样式 继承至CoordinatorLayout, 在该组件下面使用了CollapsingToolbarLayout包含TabLayout


该库可以帮你快速实现TabLayout和CoordinatorLayout的组合效果。

效果如下:


用法

Step 1

在gradle文件中加入下面的依赖:

1.dependencies {
2.compile 'cn.hugeterry.coordinatortablayout:coordinatortablayout:1.0.5'
3.}


Step 2

在你自己的XML中使用它:

01.<cn.hugeterry.coordinatortablayout.CoordinatorTabLayout xmlns:android="http://schemas.android.com/apk/res/android"
02.xmlns:app="http://schemas.android.com/apk/res-auto"
03.android:id="@+id/coordinatortablayout"
04.android:layout_width="match_parent"
05.android:layout_height="match_parent">
06. 
07.<android.support.v4.view.ViewPager
08.android:id="@+id/vp"
09.android:layout_width="match_parent"
10.android:layout_height="match_parent"
11.app:layout_behavior="@string/appbar_scrolling_view_behavior" />
12.</cn.hugeterry.coordinatortablayout.CoordinatorTabLayout>

Step 3

在使用它的界面添加以下设置:
1.setTitle(String title):设置Toolbar标题
2.setupWithViewPager(ViewPager viewPager):将写好的viewpager设置到该控件当中
3.setImageArray(int[] imageArray):根据tab数量设置好头部的图片数组,并传到该控件当中

01.//构建写好的fragment加入到viewpager中
02.initFragments();
03.initViewPager();
04.//头部的图片数组
05.mImageArray = new int[]{
06.R.mipmap.bg_android,
07.R.mipmap.bg_ios,
08.R.mipmap.bg_js,
09.R.mipmap.bg_other};
10. 
11.mCoordinatorTabLayout = (CoordinatorTabLayout) findViewById(R.id.coordinatortablayout);
12.mCoordinatorTabLayout.setTitle("Demo")
13..setImageArray(mImageArray)
14..setupWithViewPager(mViewPager);


大功告成,好好享用吧

更多功能

添加折叠后的颜色变化效果

setImageArray(int[] imageArray, int[] colorArray):如果你想要有头部折叠后的颜色变化,可将之前设置好的图片数组以及根据tab数量设置的颜色数组传到该控件当中

1.mColorArray = new int[]{
2.android.R.color.holo_blue_light,
3.android.R.color.holo_red_light,
4.android.R.color.holo_orange_light,
5.android.R.color.holo_green_light};
6.mCoordinatorTabLayout.setImageArray(mImageArray, mColorArray);

添加返回

setBackEnable(Boolean canBack):设置Toolbar的返回按钮

01.@Override
02.protected void onCreate(Bundle savedInstanceState) {
03....
04.mCoordinatorTabLayout.setBackEnable(true);
05....
06.}
07.@Override
08.public boolean onOptionsItemSelected(MenuItem item) {
09.if (item.getItemId() == android.R.id.home) {
10.finish();
11.}
12.return super.onOptionsItemSelected(item);
13.}

获取子控件

getActionBar():获取该组件中的ActionBar getTabLayout():获取该组件中的TabLayout

Github地址:https://github.com/hugeterry/CoordinatorTabLayout

下载地址:CoordinatorTabLayout-master.zip

收起阅读 »

http拦截神器:Chuck

前言:Chuck是Android OkHttp客户端的一个简单的应用内HTTP检查器。Chuck拦截并持久化应用程序中的所有HTTP请求和响应,并提供用于检查其内容的UI。使用Chuck的应用程序将显示一个通知,显示正在进行的HTTP活动的摘要。点击通知启动完...
继续阅读 »

前言:Chuck是Android OkHttp客户端的一个简单的应用内HTTP检查器。Chuck拦截并持久化应用程序中的所有HTTP请求和响应,并提供用于检查其内容的UI。


使用Chuck的应用程序将显示一个通知,显示正在进行的HTTP活动的摘要。点击通知启动完整的Chuck UI。应用程序可以选择性地抑制通知,并直接从自己的界面中启动Chuck UI。HTTP交互及其内容可以通过共享意图导出。

主要的Chuck活动是在它自己的任务中启动的,允许它与使用Android7.x多窗口支持的主机应用程序UI一起显示。

警告:使用此拦截器时生成和存储的数据可能包含敏感信息,如授权或Cookie头,以及请求和响应主体的内容。它用于开发过程中,而不是发布版本或其他生产部署中。

如果你使用OkHttp作为网络请求库,那么这个Chuck库可以帮助你拦截留存所有的HTTP请求和相应信息。同时也提供UI来显示拦截的信息

效果如下:


安装
配置:

dependencies {
debugCompile 'com.readystatesoftware.chuck:library:1.0.4'
releaseCompile 'com.readystatesoftware.chuck:library-no-op:1.0.4'
}


在您的应用程序代码中,创建一个ChuckInterceptor实例(您需要为它提供一个上下文,因为Android)并在构建OkHttp客户端时将其添加为拦截器:

OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new ChuckInterceptor(context))
.build();


就这样!Chuck现在将记录OkHttp客户端进行的所有HTTP交互。您可以选择通过在拦截器实例上调用showNotification(false)来禁用通知,并使用Chuck.getLaunchIntent()中的意图直接在应用程序中启动Chuck UI。

Github地址:https://github.com/jgilfelt/chuck

下载地址:chuck-master.zip


收起阅读 »

预览帧画面库:PreviewSeekBar

PreviewSeekBar其实大家用PC优酷看视频的时候,鼠标放到进度条撒花姑娘就可以预览到所指向的帧画面。 一个叫[Ruben Sousa](https://medium.com/@rubensousa)的哥们做出了一个库并开源。 效果如下L:使用说明:...
继续阅读 »

PreviewSeekBar

其实大家用PC优酷看视频的时候,鼠标放到进度条撒花姑娘就可以预览到所指向的帧画面。

一个叫[Ruben Sousa](https://medium.com/@rubensousa)的哥们做出了一个库并开源。

效果如下L:


使用说明:

Build

  1. dependencies {
  2.     compile 'com.github.rubensousa:previewseekbar:0.3'
  3. }

Add the following XML:

  1. <com.github.rubensousa.previewseekbar.PreviewSeekBarLayout
  2.       android:id="@+id/previewSeekBarLayout"
  3.       android:layout_width="match_parent"
  4.       android:layout_height="wrap_content"
  5.       android:orientation="vertical">
  6.       <FrameLayout
  7.           android:id="@+id/previewFrameLayout"
  8.           android:layout_width="@dimen/video_preview_width"
  9.           android:layout_height="@dimen/video_preview_height">
  10.           <View
  11.               android:id="@+id/videoView"
  12.               android:layout_width="match_parent"
  13.               android:layout_height="match_parent"
  14.               android:layout_gravity="start"
  15.               android:background="@color/colorPrimary" />
  16.       </FrameLayout>
  17.       <com.github.rubensousa.previewseekbar.PreviewSeekBar
  18.           android:id="@+id/previewSeekBar"
  19.           android:layout_width="match_parent"
  20.           android:layout_height="wrap_content"
  21.           android:layout_below="@id/previewFrameLayout"
  22.           android:layout_marginTop="25dp"
  23.           android:max="800" />
  24. </com.github.rubensousa.previewseekbar.PreviewSeekBarLayout>

你需要在PreviewSeekBarLayout中至少添加一个PreviewSeekBar和一个FrameLayout,否则会出现异常。

PreviewSeekBarLayout继承自RelativeLayout因此还可以添加别的视图或者布局。

为seekBar添加一个标准的OnSeekBarChangeListener:

  1. // setOnSeekBarChangeListener was overridden to do the same as below
  2. seekBar.addOnSeekBarChangeListener(this);

实现你自己的预览逻辑:

  1. @Override
  2. public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
  3.     // I can't help anymore
  4. }

Github地址:https://github.com/rubensousa/PreviewSeekBar

下载地址:Store-feature-rx2.zip

收起阅读 »

异步数据加载和缓存数据的库:Store

StoreStore是一个异步数据加载和缓存数据的库。 实现一个 Disk Cache 需要以下几个步骤:在 Retrofit 的 API 下@GET("/v1/events")Single getEventsResponseBody();两点需要注意,一是要...
继续阅读 »

Store

Store是一个异步数据加载和缓存数据的库。

实现一个 Disk Cache 需要以下几个步骤:

在 Retrofit 的 API 下

@GET("/v1/events")
Single getEventsResponseBody();
两点需要注意,一是要用 Single,而是要用 ResponseBody
    创建 fetcher
private fun fetcher(): Single {
return service.getEventsResponseBody().map({ it.source() })
}
创建 Store
private fun provideStore(): Store {
return StoreBuilder.parsedWithKey()
.fetcher { fetcher() }
.persister(FileSystemPersister.create(FileSystemFactory.create(filesDir)) { key -> key })
.parser(JacksonParserFactory.createSourceParser(Events::class.java))
.open()
}
    使用 Store
store.get("cache_key") // get or fetch


Github地址:https://github.com/NYTimes/Store

下载地址:Store-feature-rx2.zip

收起阅读 »

Toast增强库

StyleableToast这也是一个Toast增强库。 设置背景颜色的吐司。设置吐司和存档的圆角半径不同的形状。设置吐司给你所有的透明固体或透明的吐司。设置笔划宽度和中风颜色在你的吐司。设置一个图标旁边的面包文本。你的图标上设置一个旋转的动画效果(见下面的例...
继续阅读 »

StyleableToast

这也是一个Toast增强库。
  • 设置背景颜色的吐司。
  • 设置吐司和存档的圆角半径不同的形状。
  • 设置吐司给你所有的透明固体或透明的吐司。
  • 设置笔划宽度和中风颜色在你的吐司。
  • 设置一个图标旁边的面包文本。
  • 你的图标上设置一个旋转的动画效果(见下面的例子01) 从Api 16 +工作

效果如下:


使用

1,在app/build.gradle文件中添加如下代码:

compile 'com.muddzdev:styleabletoast:1.0.6'

2,布局文件:main.xml就是几个按钮就不贴出来了,直接上一个是动态刷新的那个布局和飞行模式:
飞行模式:ic_airplanemode_inactive_black_24dp.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M13,9V3.5c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,2.67 10,3.5v3.68l7.83,7.83L21,16v-2l-8,-5zM3,5.27l4.99,4.99L2,14v2l8,-2.5V19l-2,1.5V22l3.5,-1 3.5,1v-1.5L13,19v-3.73L18.73,21 20,19.73 4.27,4 3,5.27z"/>
</vector>

动态刷新: ic_autorenew_black_24dp.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#fff"
android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z"/>
</vector>

3,使用方式:
1>在styles.xml文件中添加样式:

<style name="StyleableToast">

<item name="android:textColor">@color/red</item>
<item name="android:colorBackground">@color/gray</item>
<!--<item name="android:fontFamily">fonts/dosis.otf"</item>-->
<item name="android:textStyle">bold</item>
<!--<item name="android:strokeWidth">2</item>-->
<!--<item name="android:strokeColor">@color/colorPrimary</item>-->
<!--<item name="android:radius">@dimen/toastRadius</item>-->
<item name="android:alpha">255</item>
<item name="android:icon">@drawable/ic_file_download</item>
</style>


使用:

case R.id.btn_toast_style_05 :
StyleableToast.makeText(this, "Picture saved in gallery", Toast.LENGTH_LONG, R.style.StyleableToast).show();
break;

2>,使用code:

private StyleableToast st;

case R.id.btn_toast_style_01 :
st = new StyleableToast(this, "加载中...", Toast.LENGTH_LONG);
st.setBackgroundColor(Color.parseColor("#ff5a5f"));
st.setTextColor(Color.WHITE);
st.setIcon(R.drawable.ic_autorenew_black_24dp);
st.spinIcon();
st.setMaxAlpha();
st.show();
break;


3> .Builder

private StyleableToast stBuilder;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initView();

stBuilder = new StyleableToast.Builder(this,"已关闭飞行模式")
.withBackgroundColor(Color.parseColor("#865aff"))
.withIcon(R.drawable.ic_airplanemode_inactive_black_24dp)
.withMaxAlpha()
.build();
}
...
case R.id.btn_toast_style_02 :
stBuilder.show();
break;

Github地址:https://github.com/Muddz/StyleableToast

下载地址:StyleableToast-master.zip


收起阅读 »

Toasty 一个自定义toast库

Toasty这是一个自定义Toast的库。 效果图:1. 添加这个在你的Project的 build.gradle 文件( 不是 你的Module的 build.gradle 文件):allprojects {repositories {...maven { ...
继续阅读 »

Toasty

这是一个自定义Toast的库。

效果图:


1. 添加这个在你的Project的 build.gradle 文件( 不是 你的Module的 build.gradle 文件):

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


依赖
添加到你的Module的build.gradle文件:

dependencies {
...
compile 'com.github.GrenderG:Toasty:1.2.5'
}


配置
这一步是可选的,但如果你想要,你可以配置一些Toasty参数。把这个地方放在你的应用程序中

Toasty.Config.getInstance()
.setErrorColor(@ColorInt int errorColor) // optional
.setInfoColor(@ColorInt int infoColor) // optional
.setSuccessColor(@ColorInt int successColor) // optional
.setWarningColor(@ColorInt int warningColor) // optional
.setTextColor(@ColorInt int textColor) // optional
.tintIcon(boolean tintIcon) // optional (apply textColor also to the icon)
.setToastTypeface(@NonNull Typeface typeface) // optional
.setTextSize(int sizeInSp) // optional
.apply(); // required
To display an error Toast:

Toasty.error(yourContext, "This is an error toast.", Toast.LENGTH_SHORT, true).show();
To display a success Toast:

Toasty.success(yourContext, "Success!", Toast.LENGTH_SHORT, true).show();
To display an info Toast:

Toasty.info(yourContext, "Here is some info for you.", Toast.LENGTH_SHORT, true).show();
To display a warning Toast:

Toasty.warning(yourContext, "Beware of the dog.", Toast.LENGTH_SHORT, true).show();
To display the usual Toast:

Toasty.normal(yourContext, "Normal toast w/o icon").show();
To display the usual Toast with icon:

Toasty.normal(yourContext, "Normal toast w/ icon", yourIconDrawable).show();
You can also create your custom Toasts with the custom() method:

Toasty.custom(yourContext, "I'm a custom Toast", yourIconDrawable, tintColor, duration, withIcon,
shouldTint).show();

Github地址:https://github.com/GrenderG/Toasty

下载地址:Toasty-master.zip

收起阅读 »

Lottie动画库

Lottie效果图如下:本文主要介绍动画开源库 Lottie 在 Android 中的使用。 前言 在日常APP开发中,为了提升用户感官舒适度等原因,我们会在APP中加入适量动画。 而传统手写动画方式往往存在诸多问题: 动画复杂而实现困难 ...
继续阅读 »

Lottie

效果图如下:


本文主要介绍动画开源库 Lottie 在 Android 中的使用。



前言


在日常APP开发中,为了提升用户感官舒适度等原因,我们会在APP中加入适量动画。
而传统手写动画方式往往存在诸多问题:



  • 动画复杂而实现困难


  • 图片素材占用体积过大


  • 不同Native平台都需各自实现,开发成本高


  • 不同Native平台实现的最终效果不一致


  • 后期视觉联调差异化大




难道就没有一种简便且高效的方案来减缓或解决上述问题吗?


答: 有的,那就是本文要介绍的主角 Lottie


一、Lottie 是什么?



Lottie是Airbnb开源的一个面向IOS、Android、React Native的动画库,可以解析用 Adobe After Effects 制作动画后通过 Bodymovin 插件导出的 Json 数据文件并在移动端原生渲染。



通俗点说,它是一款动画开源库,通过解析特定的Json文件或Json文本,可直接在移动端上渲染出复杂的动画效果。


参考图释




二、Lottie 能干什么?


它可以简便高效的实现复杂动画,替代传统低效的手写动画方式。


动画展示:



上方的动画是通过Lottie直接实现的。


如果我们使用手写代码方式实现,可以说是很难!


而通过 Lottie 实现时,需要的仅是一个Json文件或一段Json文本,并通过简洁的代码集成即可。


集成代码可能精简到如下模样:









1
2
3
4
5
6
7
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_rawRes="@raw/anim_lottie"
app:lottie_loop="true"
app:lottie_autoPlay="true" />

三、为什么使用 Lottie?



  • 简便,开发成本低
    相对于传统方式,动画不再是全部手写,所需做得只是嵌入XML并配置动画属性,集成快,开发时间少,开发成本低。


  • 不同 Native 平台效果基本一致
    渲染交由Lottie引擎内部实现,无需开发者处理平台差异,多平台共用同一个动画配置文件,效果一致性高。


  • 占用包体积小
    Lottie配置文件由Json文本串构成,相对于图片,占用体积更小。


  • 动画效果不失真
    传统图片拉伸或压缩导致失真,而Lottie为矢量图展示,不会出现失真情况。


  • 动画效果可以云端控制
    由于Lottie动画基于Json文件或文本解析,因此可以实现云端下发,实现动态加载,动态控制动画样式。



四、如何使用 Lottie?


Lottie 仅支持用 Gradle 构建配置,最低支持安卓版本 16。


1. 添加依赖到 build.gradle









1
2
3
4
5
6
7
dependencies {
implementation 'com.airbnb.android:lottie:2.5.4'
}

dependencies {
implementation "com.airbnb.android:lottie:${全局版本变量}"
}

2. 添加 Adobe After Effects 导出的动画 Json 文件


通常由视觉工程师确认动效后通过AE导出, 我们只需将该Json文件保存至 /raw 或 /assets文件夹下。


3. XML中嵌入基本布局









1
2
3
4
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

4. 加载播放动画,两类方式可选


① XML中静态配置, 举例:









1
2
3
4
5
6
7
8
9
10
11
12
13
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

//加载方式如下2种任选其一
app:lottie_rawRes="@raw/hello_world"
app:lottie_fileName="hello_world.json"

//循环播放
app:lottie_loop="true"
//加载完毕后自动播放
app:lottie_autoPlay="true" />

② 代码动态配置, 举例:


如下代码会在后台异步加载动画文件,并在加载完成后开始渲染动画。









1
2
3
4
LottieAnimationView animationView = ...;
animationView.setAnimation(R.raw.hello_world);
animationView.loop(true);
animationView.playAnimation();

五、其他拓展使用


1. 直接解析Json文本串加载动画









1
2
3
4
5
6
7
8
9
10
JsonReader jsonReader = new JsonReader(new StringReader(jsonStr));
lottieView.setAnimation(jsonReader);
lottieView.playAnimation();

Cancellable cancellable = LottieComposition.Factory.fromJsonString(jsonStr, composition -> {
lottieView.setComposition(composition);
lottieView.playAnimation();
});
//必要时取消进行中的异步操作
cancellable.cancel();

2. 配合网络下载,实现下载Json配置并动态加载









1
2
3
4
5
6
7
8
9
10
11
12
13
Call<ResponseBody> call = RetrofitComponent.fetchLottieConfig();//伪代码
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
//String json = response.body().string();
//交由Lottie处理...
}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
//do something.
}
});

3. 动画加载监听器


根据业务需要进行动画过程监听:









1
2
3
4
5
6
7
8
9
10
11
animationView.addAnimatorUpdateListener((animation) -> {
// do something.
});
animationView.playAnimation();
...
if (animationView.isAnimating()) {
// do something.
}
...
animationView.setProgress(0.5f);
...

4. LottieDrawable 的使用









1
2
3
4
5
6
7
8
LottieDrawable drawable = new LottieDrawable();
LottieComposition.Factory.fromAssetFileName(getApplicationContext(), "lottie_pin_jump.json", composition -> {
drawable.setComposition(composition);
//目前显示为静态图
ivLottie.setImageDrawable(drawable);
//调用start()开始播放动画
drawable.start();
});



六、需要考虑的问题



  1. 由于是依赖于AE做的动画,估计以后不只是要求视觉设计师精通AE,连前端开发都需要了解AE

  2. Lottie 对 Json 文件的支持待完善,目前存在部分AE导出成 Json 文件无法渲染或渲染不佳

  3. 支持的功能存在限制

Github地址:https://github.com/airbnb/lottie-android

下载地址:lottie-android-master.zip

收起阅读 »

iOS timer定时器正确使用方式

1. 初始化,添加定时器前先移除[self.timer invalidate];self.timer = nil;self.timer = [NSTimer scheduledTimerWithTimeInterval:2.f target:self sele...
继续阅读 »

1. 初始化,添加定时器前先移除

[self.timer invalidate];
self.timer = nil;
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.f target:self selector:@selector(lookforCard:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

2. 释放timer

[self.timer invalidate];
self.timer = nil;

3. NSTimer不释放原因

  • 原因是 Timer 添加到 Runloop 的时候,会被 Runloop 强引用;然后 Timer 又会有一个对 Target 的强引用(也就是 self )

注意target参数的描述:
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.
注意:文档中写的很清楚,timer对target会有一个强引用,直到timer is invalidated。也就是说,在timer调用 invalidate方法之前,timer对target一直都有一个强引用。这也是为什么控制器的dealloc 方法不会被调用的原因。
方法的文档介绍:
The receiver retains aTimer. To remove a timer from all run loop modes on which it is installed, send an invalidate message to the timer.
也就是说,runLoop会对timer有强引用,因此,timer修饰符是weak,timer还是不能释放,timer的target也就不能释放。

4. 解决办法

  • viewWillDisappear或viewDidDisappear中 invalidate
    这种方式是可以释放掉的,但如果我只是想在离开此页时要释放,进入下一页时不要释放,场景就不适用了

- (void)viewWillDisappear:(BOOL)animated
- (void)viewDidDisappear:(BOOL)animated
  • 添加一个NSTimer的分类,把target指给[NSTimer class],事件由加方法接收,然后把事件通过block传递出来

@interface NSTimer (Block)

+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void(^)(NSTimer *timer))block;

@end

@implementation NSTimer (Block)

+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void(^)(NSTimer *timer))block{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(trigger:) userInfo:[block copy] repeats:repeats];
return timer;
}

+ (void)trigger:(NSTimer *)timer{
void(^block)(NSTimer *timer) = [timer userInfo];
if (block) {
block(timer);
}
}

@end
  • 使用示例

@interface SecondViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation SecondViewController

- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf doSomeThing];
}];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)dealloc {
[self.timer invalidate];
}

@end

5. invalidate方法注意事项

invalidate方法的介绍:
(1)This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
(2)You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.
两点:
(1)invalidate方法是唯一能从runloop中移除timer的方式,调用invalidate方法后,runloop会移除对timer的强引用
(2)timer的添加和timer的移除(invalidate)需要在同一个线程中,否则timer可能不能正确的移除,线程不能正确退出

附:我的博客地址

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

收起阅读 »

“小家碧玉”中的UIStackView

KeyWordsAutoLayout UIStackView背景随着需求的迭代,项目中在列表的同一个区域新增业务标签貌似成了每个产品经理的“特殊嗜好”。如下图中的区域(其实本人的项目中在箭头区域大概有7个类似的标签,当然在业务上不会同时出现,能同时出现的时候最...
继续阅读 »

KeyWords
AutoLayout UIStackView

背景

随着需求的迭代,项目中在列表的同一个区域新增业务标签貌似成了每个产品经理的“特殊嗜好”。如下图中的区域


(其实本人的项目中在箭头区域大概有7个类似的标签,当然在业务上不会同时出现,能同时出现的时候最多会有四个),随着标签的增加,势必会造成繁重的视图维护工作,再加上要控制优先级之类的,估计头都大了,好在apple给咱们提供了强大的视图管理:UIStackView。我們可以透过它轻易的定义好在 UIStackView 中元件的布局,不需对于所有元件进行 AutoLayout 的约束设置,UIStackView会处理大部分的工作。

正文

apple官方文档对UIStackView的描述是:用于在列或行中布置视图集合

UIStackView要点

UIStackView是在iOS 9中引入的, 是Cocoa Touch中UI控件分类的最新成员。
通过UIStackView,你可以利用 AutoLayout 的強大功能,创建用户视图,可以动态适应设备方向,屏幕大小和可用空间任何变化的用户界面。UIStackView 管理其 arrangeSubviews 属性中所有视图的布局。這些视图基于它們在 arrangeSubviews 阵列中的順序,沿著 UIStackView 的 axis 排列。最终精确的布局依赖于 UIStackView 的 axis、distribution、alignment、spacing 以及其他属性。

我们只需要负责 UIStackView 的位置和尺寸,然后 UIStackView 就会管理其內容的布局和尺寸。

注意:放到 StackView ≠ 完成 AutoLayout

所以你还必須設定 StackView 的位置和尺寸(可選)才算是完成。StackView 只有為其 arrangeSubviews 做佈局

虽然堆栈视图允许您直接布局其内容而不直接使用“自动布局”,但仍需要使用“自动布局”来定位堆栈视图本身。通常,这意味着定位堆叠视图的至少两个相邻边缘以限定其位置。如果没有其他约束,系统将根据其内容计算堆栈视图的大小。

1、沿着堆栈视图的轴,其拟合大小等于所有排列视图的大小加上视图之间的空间的总和。

2、垂直于堆栈视图的轴,其拟合大小等于最大排列视图的大小。

3、如果堆栈视图的属性设置为,则堆栈视图的拟合大小会增加,以包含边距的空间。layoutMarginsRelativeArrangement

您可以提供其他约束来指定堆栈视图的高度,宽度或两者。在这些情况下,堆栈视图会调整其排列视图的布局和大小以填充指定区域。确切的布局根据堆栈视图的属性而有所不同。有关堆栈视图如何处理其内容的额外空间或空间不足的完整说明,请参阅和枚举。UIStackViewDistribution 、UIStackViewAlignment

您还可以基于其第一个或最后一个基线定位堆栈视图,而不是使用顶部,底部或中心Y位置。与堆栈视图的拟合大小一样,这些基线是根据堆栈视图的内容计算的。

1、水平堆栈视图返回其和方法的最高视图。如果最高视图也是堆栈视图,则返回调用结果或嵌套堆栈视图。

2、垂直堆栈视图返回其第一个排列的视图以及其最后排列的视图。如果这些视图中的任何一个也是堆栈视图,则它返回调用的结果或嵌套堆栈视图。

创建一个StackView

UILabel * main = [[UILabel alloc]init];
main.text = @"Learn More";
main.font = [UIFont boldSystemFontOfSize:28];
main.translatesAutoresizingMaskIntoConstraints = false;
main.backgroundColor = [UIColor redColor];

UILabel * sub = [[UILabel alloc]init];
sub.translatesAutoresizingMaskIntoConstraints = false;
sub.numberOfLines = 0;
sub.text = @"[self.collectionView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:40].active = true";
sub.font = [UIFont systemFontOfSize:18];
sub.backgroundColor = [UIColor greenColor];

UILabel * third = [[UILabel alloc]init];
third.translatesAutoresizingMaskIntoConstraints = false;
third.numberOfLines = 0;
third.text = @"Object_C";
third.font = [UIFont systemFontOfSize:18];
third.backgroundColor = [UIColor brownColor];

UIStackView * stackView = [[UIStackView alloc]initWithArrangedSubviews:@[main,sub,third]];
stackView.translatesAutoresizingMaskIntoConstraints = false;
stackView.axis = UILayoutConstraintAxisVertical;
stackView.distribution = UIStackViewDistributionFillProportionally;
stackView.alignment = UIStackViewAlignmentFill;
stackView.spacing = 10;

[stackView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:100].active = true;
[stackView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:12].active = true;
[stackView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-12].active = true;

效果图如下:


这样一个简单的堆叠视图就创建出来了,然后咱们沿着这段代码逐一分解下

Axis

配置视图的轴线,简单说就是 UIStackView 整体的排列方式(行或列)

  • UILayoutConstraintAxisVertical


  • UILayoutConstraintAxisHorizontal


Distribution

确定沿堆栈轴排列的视图的布局

這个属性算是比较难以理解的属性,所以我下面一样用特殊的图形來表示,希望大家能理解。

  • UIStackViewDistributionFill

 如同前面的 Fill 类似,一樣是把自身的范围給占满。


  • UIStackViewDistributionFillEqually
    StackView 会平均分配各個子视图的配置,例如下面的 StackView 垂直排列时,高度则会为其子视图最高的高度。反之,水平排列则取其子视图最寬的寬度。


  • UIStackViewDistributionFillProportionally
    你可能会觉得此属性与 Fill 属性沒有差异,但兩者的差异在于 Fill 会根据自身的尺寸來決定在 StackView 中的尺寸,而 Fill Proportionally 則是會根据 StackView 的寬或高按比例分配給其中的子视图。


  • UIStackViewDistributionEqualSpacing
    此属性简单來說就会根据 StackView 剩余可用空間的尺寸,來分配 StackView 子视图间的间隔。


  • UIStackViewDistributionEqualCentering
    此属性与上面的 Equal Spacing 意思类似,只是它是以其子视图中心點的中心与中心距離是相同來做為配置。


若說明得還不夠清楚可以看看這篇文章,了解當中的差异。
Alignment
确定垂直于堆栈轴的排列视图的布局

  • 在 Axis 为 Vertical 下,各个状态下的对齐方式:


  • 在 Axis 為 Horizontal 下,各个状态下的对齐方式:


Spacing

這個属性就不用多加以赘述了,就是可以自定义 StackView 的間隔大小,但是这边要注意,如果你沒有限制 StackView 的尺寸,那么就會加長或加寬 StackView。但是如果你有限制 StackView 的尺寸,那麼就會在限制尺寸下增加间隔(可能會造成跑版或失敗)。

baselineRelativeArrangement

该属性确定视图之间的垂直间距是否从基线测量。

layoutMarginsRelativeArrangement

该属性确定堆栈视图是否相对于其布局边距布置其排列的视图

注意

详情参考

从堆栈视图中删除子视图时,堆栈视图也会将其从数组中删除

从数组中删除视图不会将其作为子视图删除。堆栈视图不再管理视图的大小和位置,但视图仍然是视图层次结构的一部分,并且如果可见,则在屏幕上呈现

无论何时添加,删除或插入视图,或者每当其中一个已排列的子视图的属性发生更改时,堆栈视图都会自动更新其布局(比如hidden,更改布局方向...)

后记

堆栈视图为我们执行的自动布局计算会带来性能成本。在大多数情况下,它可以忽略不计。但是当堆栈视图嵌套超过两层时,可能会变得明显。
为了安全起见,请避免使用深层嵌套的堆栈视图,尤其是在可重用的视图(如表和集合视图单元格)中。

链接:https://www.jianshu.com/p/7920d287c13b

收起阅读 »

Swift5.0的Runtime机制浅析

导读:你想知道Swift内部对象是如何创建的吗?方法以及函数调用又是如何实现的吗?成员变量的访问以及对象内存布局又是怎样的吗?这些问题都会在这篇文章中得到解答。为了更好的让大家理解这些内部实现,我会将源代码翻译为用C语言表示的伪代码来实现。Objective-...
继续阅读 »

导读:你想知道Swift内部对象是如何创建的吗?方法以及函数调用又是如何实现的吗?成员变量的访问以及对象内存布局又是怎样的吗?这些问题都会在这篇文章中得到解答。为了更好的让大家理解这些内部实现,我会将源代码翻译为用C语言表示的伪代码来实现。

Objective-C语言是一门以C语言为基础的面向对象编程语言,其提供的运行时(Runtime)机制使得它也可以被认为是一种动态语言。运行时的特征之一就是对象方法的调用是在程序运行时才被确定和执行的。系统提供的开放接口使得我们可以在程序运行的时候执行方法替换以便实现一些诸如系统监控、对象行为改变、Hook等等的操作处理。然而这种开放性也存在着安全的隐患,我们可以借助Runtime在AOP层面上做一些额外的操作,而这些额外的操作因为无法进行管控, 所以有可能会输出未知的结果。

可能是苹果意识到了这个问题,所以在推出的Swift语言中Runtime的能力得到了限制,甚至可以说是取消了这个能力,这就使得Swift成为了一门静态语言。Swift语言中对象的方法调用机制和OC语言完全不同,Swift语言的对象方法调用基本上是在编译链接时刻就被确定的,可以看做是一种硬编码形式的调用实现。

Swfit中的对象方法调用机制加快了程序的运行速度,同时减少了程序包体积的大小。但是从另外一个层面来看当编译链接优化功能开启时反而又会出现包体积增大的情况。Swift在编译链接期间采用的是空间换时间的优化策略,是以提高运行速度为主要优化考虑点。具体这些我会在后面详细谈到。

通过程序运行时汇编代码分析Swift中的对象方法调用,发现其在Debug模式下和Release模式下的实现差异巨大。其原因是在Release模式下还同时会把编译链接优化选项打开。因此更加确切的说是在编译链接优化选项开启与否的情况下二者的实现差异巨大。

在这之前先介绍一下OC和Swift两种语言对象方法调用的一般实现。

OC类的对象方法调用

对于OC语言来说对象方法调用的实现机制有很多文章都进行了深入的介绍。所有OC类中定义的方法函数的实现都隐藏了两个参数:一个是对象本身,一个是对象方法的名称。每次对象方法调用都会至少传递对象和对象方法名称作为开始的两个参数,方法的调用过程都会通过一个被称为消息发送的C函数objc_msgSend来完成。objc_msgSend函数是OC对象方法调用的总引擎,这个函数内部会根据第一个参数中对象所保存的类结构信息以及第二个参数中的方法名来找到最终要调用的方法函数的地址并执行函数调用。这也是OC语言Runtime的实现机制,同时也是OC语言对多态的支持实现。整个流程就如下表述:


Swift类的对象创建和销毁

在Swift中可以定义两种类:一种是从NSObject或者派生类派生的类,一类是从系统Swift基类SwiftObject派生的类。对于后者来说如果在定义类时没有指定基类则默认会从基类SwiftObject派生。SwiftObject是一个隐藏的基类,不会在源代码中体现。

Swift类对象的内存布局和OC类对象的内存布局相似。二者对象的最开始部分都有一个isa成员变量指向类的描述信息。Swift类的描述信息结构继承自OC类的描述信息,但是并没有完全使用里面定义的属性,对于方法的调用则主要是使用其中扩展了一个所谓的虚函数表的区域,关于这部分会在后续中详细介绍。

Swift类的对象实例都是在堆内存中创建,这和OC语言的对象实例创建方式相似。系统会为类提供一个默认的init构造函数,如果想自定义构造函数则需要重写和重载init函数。一个Swift类的对象实例的构建分为两部分:首先是进行堆内存的分配,然后才是调用init构造函数。在源代码编写中不会像OC语言那样明确的分为alloc和init两个分离的调用步骤,而是直接采用:类名(初始化参数)这种方式来完成对象实例的创建。在编译时系统会为每个类的初始化方法生成一个:模块名.类名.__allocating_init(类名,初始化参数)的函数,这个函数的伪代码实现如下:

//假设定义了一个CA类。
class CA {
init(_ a:Int){}
}
//编译生成的对象内存分配创建和初始化函数代码
CA * XXX.CA.__allocating_init(swift_class classCA, int a)
{
CA *obj = swift_allocObject(classCA); //分配内存。
obj->init(a); //调用初始化函数。
}

//编译时还会生成对象的析构和内存销毁函数代码
XXX.CA.__deallocating_deinit(CA *obj)
{
obj->deinit() //调用析构函数
swift_deallocClassInstance(obj); //销毁对象分配的内存。
}

其中的swift_class 就是从objc_class派生出来,用于描述类信息的结构体。

Swift对象的生命周期也和OC对象的生命周期一样是通过引用计数来进行控制的。当对象初次创建时引用计数被设置为1,每次进行对象赋值操作都会调用swift_retain函数来增加引用计数,而每次对象不再被访问时都会调用swift_release函数来减少引用计数。当引用计数变为0后就会调用编译时为每个类生成的析构和销毁函数:模块名.类名.__deallocating_deinit(对象)。这个函数的定义实现在前面有说明。

这就是Swift对象的创建和销毁以及生命周期的管理过程,这些C函数都是在编译链接时插入到代码中并形成机器代码的,整个过程对源代码透明。下面的例子展示了对象创建和销毁的过程。

////////Swift源代码

let obj1:CA = CA(20);
let obj2 = obj1
///////C伪代码

CA *obj1 = XXX.CA. __allocating_init(classCA, 20);
CA *obj2 = obj1;
swift_retain(obj1);
swift_release(obj1);
swift_release(obj2);

swift_release函数内部会在引用计数为0时调用模块名.类名.__deallocating_deinit(对象)函数进行对象的析构和销毁。这个函数的指针保存在swift类描述信息结构体中,以便swift_release函数内部能够访问得到。

Swift类的对象方法调用

Swift语言中对象的方法调用的实现机制和C++语言中对虚函数调用的机制是非常相似的。(需要注意的是我这里所说的调用实现只是在编译链接优化选项开关在关闭的时候是这样的,在优化开关打开时这个结论并不正确)。

Swift语言中类定义的方法可以分为三种:OC类的派生类并且重写了基类的方法、extension中定义的方法、类中定义的常规方法。针对这三种方法定义和实现,系统采用的处理和调用机制是完全不一样的。

OC类的派生类并且重写了基类的方法

如果在Swift中的使用了OC类,比如还在使用的UIViewController、UIView等等。并且还重写了基类的方法,比如一定会重写UIViewController的viewDidLoad方法。对于这些类的重写的方法定义信息还是会保存在类的Class结构体中,而在调用上还是采用OC语言的Runtime机制来实现,即通过objc_msgSend来调用。而如果在OC派生类中定义了一个新的方法的话则实现和调用机制就不会再采用OC的Runtime机制来完成了,比如说在UIView的派生类中定义了一个新方法foo,那么这个新方法的调用和实现将与OC的Runtime机制没有任何关系了! 它的处理和实现机制会变成我下面要说到的第三种方式。下面的Swift源代码以及C伪代码实现说明了这个情况:

////////Swift源代码

//类定义
class MyUIView:UIView {
open func foo(){} //常规方法
override func layoutSubviews() {} //重写OC方法
}

func main(){
let obj = MyUIView()
obj.layoutSubviews() //调用OC类重写的方法
obj.foo() //调用常规的方法。
}
////////C伪代码

//...........................................运行时定义部分

//OC类的方法结构体
struct method_t {
SEL name;
IMP imp;
};

//Swift类描述
struct swift_class {
... //其他的属性,因为这里不关心就不列出了。
struct method_t methods[1];
... //其他的属性,因为这里不关心就不列出了。
//虚函数表刚好在结构体的第0x50的偏移位置。
IMP vtable[1];
};


//...........................................源代码中类的定义和方法的定义和实现部分

//类定义
struct MyUIView {
struct swift_class *isa;
}

//类的方法函数的实现
void layoutSubviews(id self, SEL _cmd){}
void foo(){} //Swift类的常规方法中和源代码的参数保持一致。

//类的描述信息构建,这些都是在编译代码时就明确了并且保存在数据段中。
struct swift_class classMyUIView;
classMyUIView.methods[0] = {"layoutSubviews", &layoutSubviews};
classMyUIView.vtable[0] = {&foo};


//...........................................源代码中程序运行的部分

void main(){
MyUIView *obj = MyUIView.__allocating_init(classMyUIView);
obj->isa = &classMyUIView;
//OC类重写的方法layoutSubviews调用还是用objc_msgSend来实现
objc_msgSend(obj, @selector(layoutSubviews);
//Swift方法调用时对象参数被放到x20寄存器中
asm("mov x20, obj");
//Swift的方法foo调用采用间接调用实现
obj->isa->vtable[0]();
}

extension中定义的方法

如果是在Swift类的extension中定义的方法(重写OC基类的方法除外)。那么针对这个方法的调用总是会在编译时就决定,也就是说在调用这类对象方法时,方法调用指令中的函数地址将会以硬编码的形式存在。在extension中定义的方法无法在运行时做任何的替换和改变!而且方法函数的符号信息都不会保存到类的描述信息中去。这也就解释了在Swift中派生类无法重写一个基类中extension定义的方法的原因了。因为extension中的方法调用是硬编码完成,无法支持多态!下面的Swift源代码以及C伪代码实现说明了这个情况:

////////Swift源代码

//类定义
class CA {
open func foo(){}
}

//类的extension定义
extension CA {
open func extfoo(){}
}

func main() {
let obj = CA()
obj.foo()
obj.extfoo()
}
////////C伪代码

//...........................................运行时定义部分


//Swift类描述。
struct swift_class {
... //其他的属性,因为这里不关心就不列出了。
//虚函数表刚好在结构体的第0x50的偏移位置。
IMP vtable[1];
};


//...........................................源代码中类的定义和方法的定义和实现部分


//类定义
struct CA {
struct swift_class *isa;
}

//类的方法函数的实现定义
void foo(){}
//类的extension的方法函数实现定义
void extfoo(){}

//类的描述信息构建,这些都是在编译代码时就明确了并且保存在数据段中。
//extension中定义的函数不会保存到虚函数表中。
struct swift_class classCA;
classCA.vtable[0] = {&foo};


//...........................................源代码中程序运行的部分

void main(){
CA *obj = CA.__allocating_init(classCA)
obj->isa = &classCA;
asm("mov x20, obj");
//Swift中常规方法foo调用采用间接调用实现
obj->isa->vtable[0]();
//Swift中extension方法extfoo调用直接硬编码调用,而不是间接调用实现
extfoo();
}

需要注意的是extension中是可以重写OC基类的方法,但是不能重写Swift类中的定义的方法。具体原因根据上面的解释就非常清楚了。

类中定义的常规方法

如果是在Swift中定义的常规方法,方法的调用机制和C++中的虚函数的调用机制是非常相似的。Swift为每个类都建立了一个被称之为虚表的数组结构,这个数组会保存着类中所有定义的常规成员方法函数的地址。每个Swift类对象实例的内存布局中的第一个数据成员和OC对象相似,保存有一个类似isa的数据成员。isa中保存着Swift类的描述信息。对于Swift类的类描述结构苹果并未公开(也许有我并不知道),类的虚函数表保存在类描述结构的第0x50个字节的偏移处,每个虚表条目中保存着一个常规方法的函数地址指针。每一个对象方法调用的源代码在编译时就会转化为从虚表中取对应偏移位置的函数地址来实现间接的函数调用。下面是对于常规方法的调用Swift语言源代码和C语言伪代码实现:

////////Swift源代码

//基类定义
class CA {
open func foo1(_ a:Int){}
open func foo1(_ a:Int, _ b:Int){}
open func foo2(){}
}

//扩展
extension CA{
open func extfoo(){}
}

//派生类定义
class CB:CA{
open func foo3(){}
override open func foo1(_ a:Int){}
}

func testfunc(_ obj:CA){
obj.foo1(10)
}

func main() {
let objA = A()
objA.foo1(10)
objA.foo1(10,20)
objA.foo2()
objA.extfoo()

let objB = B()
objB.foo1(10)
objB.foo1(10,20)
objB.foo2()
objB.foo3()
objB.extfoo()

testfunc(objA)
testfunc(objB)
}
////////C伪代码

//...........................................运行时定义部分

//Swift类描述。
struct swift_class {
... //其他的属性,因为这里不关心就不列出了
//虚函数表刚好在结构体的第0x50的偏移位置。
IMP vtable[0];
};


//...........................................源代码中类的定义和方法的定义和实现部分


//基类定义
struct CA {
struct swift_class *isa;
};

//派生类定义
struct CB {
struct swift_class *isa;
};

//基类CA的方法函数的实现,这里对所有方法名都进行修饰命名
void _$s3XXX2CAC4foo1yySiF(int a){} //CA类中的foo1
void _$s3XXX2CAC4foo1yySi_SitF(int a, int b){} //CA类中的两个参数的foo1
void _$s3XXX2CAC4foo2yyF(){} //CA类中的foo2
void _$s3XXX2CAC6extfooyyF(){} //CA类中的extfoo函数

//派生类CB的方法函数的实现。
void _$s3XXX2CBC4foo1yySiF(int a){} //CB类中的foo1,重写了基类的方法,但是名字不一样了。
void _$s3XXX2CBC4foo3yyF(){} //CB类中的foo3

//构造基类的描述信息以及虚函数表
struct swift_class classCA;
classCA.vtable[3] = {&_$s3XXX2CAC4foo1yySiF, &_$s3XXX2CAC4foo1yySi_SitF, &_$s3XXX2CAC4foo2yyF};

//构造派生类的描述信息以及虚函数表,注意这里虚函数表会将基类的函数也添加进来而且排列在前面。
struct swift_class classCB;
classCB.vtable[4] = {&_$s3XXX2CBC4foo1yySiF, &_$s3XXX2CAC4foo1yySi_SitF, &_$s3XXX2CAC4foo2yyF, &_$s3XXX2CBC4foo3yyF};

void testfunc(A *obj){
obj->isa->vtable[0](10); //间接调用实现多态的能力。
}


//...........................................源代码中程序运行的部分

void main(){
CA *objA = CA.__allocating_init(classCA);
objA->isa = &classCA;
asm("mov x20, objA")
objA->isa->vtable[0](10);
objA->isa->vtable[1](10,20);
objA->isa->vtable[2]();
_$s3XXX2CAC6extfooyyF()

CB *objB = CB.__allocating_init(classCB);
objB->isa = &classCB;
asm("mov x20, objB");
objB->isa->vtable[0](10);
objB->isa->vtable[1](10,20);
objB->isa->vtable[2]();
objB->isa->vtable[3]();
_$s3XXX2CAC6extfooyyF();

testfunc(objA);
testfunc(objB);

}

从上面的代码中可以看出一些特点:

1、Swift类的常规方法中不会再有两个隐藏的参数了,而是和字面定义保持一致。那么问题就来了,方法调用时对象如何被引用和传递呢?在其他语言中一般情况下对象总是会作为方法的第一个参数,在编译阶段生成的机器码中,将对象存放在x0这个寄存器中(本文以arm64体系结构为例)。而Swift则不同,对象不再作为第一个参数来进行传递了,而是在编译阶段生成的机器码中,将对象存放在x20这个寄存器中(本文以arm64体系结构为例)。这样设计的一个目的使得代码更加安全。

2、每一个方法调用都是通过读取方法在虚表中的索引获取到了方法函数的真实地址,然后再执行间接调用。在这个过程虚表索引的值是在编译时就确定了,因此不再需要通过方法名来在运行时动态的去查找真实的地址来实现函数调用了。虽然索引的位置在编译时确定的,但是基类和派生类虚表中相同索引处的函数的地址确可以不一致,当派生类重写了父类的某个方法时,因为会分别生成两个类的虚表,在相同索引位置保存不同的函数地址来实现多态的能力。

3、每个方法函数名字都和源代码中不一样了,原因在于在编译链接是系统对所有的方法名称进行了重命名处理,这个处理称为命名修饰。之所以这样做是为了解决方法重载和运算符重载的问题。因为源代码中重载的方法函数名称都一样只是参数和返回类型不一样,因此无法简单的通过名字进行区分,而只能对名字进行修饰重命名。另外一个原因是Swift还提供了命名空间的概念,也就是使得可以支持不同模块之间是可以存在相同名称的方法或者函数。因为整个重命名中是会带上模块名称的。下面就是Swift中对类的对象方法的重命名修饰规则:
_$s<模块名长度><模块名><类名长度><类名>C<方法名长度><方法名>yy<参数类型1>_<参数类型2>_<参数类型N>F

就比如上面的CA类中的foo1两个同名函数在编译链接时刻就会被分别重命名为:

//这里面的XXX就是你工程模块的名称。
void _$s3XXX2CAC4foo1yySiF(int a){} //CA类中的foo1
void _$s3XXX2CAC4foo1yySi_SitF(int a, int b){} //CA类中的两个参数的foo1

下面这张图就清晰的描述了Swift类的对象方法调用以及类描述信息。


Swift类中成员变量的访问

虽然说OC类和Swift类的对象内存布局非常相似,每个对象实例的开始部分都是一个isa数据成员指向类的描述信息,而类中定义的属性或者变量则一般会根据定义的顺序依次排列在isa的后面。OC类还会为所有成员变量,生成一张变量表信息,变量表的每个条目记录着每个成员变量在对象内存中的偏移量。这样在访问对象的属性时会通过偏移表中的偏移量来读取偏移信息,然后再根据偏移量来读取或设置对象的成员变量数据。在每个OC类的get和set两个属性方法的实现中,对于属性在类中的偏移量值的获取都是通过硬编码来完成,也就是说是在编译链接时刻决定的。

对于Swift来说,对成员变量的访问得到更加的简化。系统会对每个成员变量生成get/set两个函数来实现成员变量的访问。系统不会再为类的成员变量生成变量偏移信息表,因此对于成员变量的访问就是直接在编译链接时确定成员变量在对象的偏移位置,这个偏移位置是硬编码来确定的。下面展示Swift源代码和C伪代码对数据成员访问的实现:

////////Swift源代码

class CA
{
var a:Int = 10
var b:Int = 20
}

void main()
{
let obj = CA()
obj.b = obj.a
}
////////C伪代码

//...........................................运行时定义部分

//Swift类描述。
struct swift_class {
... //其他的属性,因为这里不关心就不列出了
//虚函数表刚好在结构体的第0x50的偏移位置。
IMP vtable[4];
};


//...........................................源代码中类的定义和方法的定义和实现部分

//CA类的结构体定义也是CA类对象在内存中的布局。
struct CA
{
struct swift_class *isa;
long reserve; //这里的值目前总是2
int a;
int b;
};

//类CA的方法函数的实现。
int getA(){
struct CA *obj = x20; //取x20寄存器的值,也就是对象的值。
return obj->a;
}
void setA(int a){
struct CA *obj = x20; //取x20寄存器的值,也就是对象的值。
obj->a = a;
}
int getB(){
struct CA *obj = x20; //取x20寄存器的值,也就是对象的值。
return obj->b;
}
void setB(int b){
struct CA *obj = x20; //取x20寄存器的值,也就是对象的值。
obj->b = b;
}

struct swift_class classCA;
classCA.vtable[4] = {&getA,&setA,&getB, &setB};


//...........................................源代码中程序运行的部分

void main(){
CA *obj = CA.__allocating_init(classCA);
obj->isa = &classCA;
obj->reserve = 2;
obj->a = 10;
obj->b = 20;
asm("mov x20, obj");
obj->isa->vtable[3](obj->isa->vtable[0]()); // obj.b = obj.a的实现
}

从上面的代码可以看出,Swift类会为每个定义的成员变量都生成一对get/set方法并保存到虚函数表中。所有对对象成员变量的方法的代码都会转化为通过虚函数表来执行get/set相对应的方法。 下面是Swift类中成员变量的实现和内存结构布局图:


结构体中的方法

在Swift结构体中也可以定义方法,因为结构体的内存结构中并没有地方保存结构体的信息(不存在isa数据成员),因此结构体中的方法是不支持多态的,同时结构体中的所有方法调用都是在编译时硬编码来实现的。这也解释了为什么结构体不支持派生,以及结构体中的方法不支持override关键字的原因。

类的方法以及全局函数

Swift类中定义的类方法和全局函数一样,因为不存在对象作为参数,因此在调用此类函数时也不会存在将对象保存到x20寄存器中这么一说。同时源代码中定义的函数的参数在编译时也不会插入附加的参数。Swift语言会对所有符号进行重命名修饰,类方法和全局函数也不例外。这也就使得全局函数和类方法也支持名称相同但是参数不同的函数定义。简单的说就是类方法和全局函数就像C语言的普通函数一样被实现和定义,所有对类方法和全局函数的调用都是在编译链接时刻硬编码为函数地址调用来处理的。

OC调用Swift类中的方法

如果应用程序是通过OC和Swift两种语言混合开发完成的。那就一定会存在着OC语言代码调用Swift语言代码以及相反调用的情况。对于Swift语言调用OC的代码的处理方法是系统会为工程建立一个桥声明头文件:项目工程名-Bridging-Header.h,所有Swift需要调用的OC语言方法都需要在这个头文件中声明。而对于OC语言调用Swift语言来说,则有一定的限制。因为Swift和OC的函数调用ABI规则不相同,OC语言只能创建Swift中从NSObject类中派生类对象,而方法调用则只能调用原NSObject类以及派生类中的所有方法以及被声明为@objc关键字的Swift对象方法。如果需要在OC语言中调用Swift语言定义的类和方法,则需要在OC语言文件中添加:#import "项目名-Swift.h"。当某个Swift方法被声明为@objc关键字时,在编译时刻会生成两个函数,一个是本体函数供Swift内部调用,另外一个是跳板函数(trampoline)是供OC语言进行调用的。这个跳板函数信息会记录在OC类的运行时类结构中,跳板函数的实现会对参数的传递规则进行转换:把x0寄存器的值赋值给x20寄存器,然后把其他参数依次转化为Swift的函数参数传递规则要求,最后再执行本地函数调用。整个过程的实现如下:

////////Swift源代码

//Swift类定义
class MyUIView:UIView {
@objc
open func foo(){}
}

func main() {
let obj = MyUIView()
obj.foo()
}

//////// OC源代码
#import "工程-Swift.h"

void main() {
MyUIView *obj = [MyUIView new];
[obj foo];
}
////////C伪代码

//...........................................运行时定义部分

//OC类的方法结构体
struct method_t {
SEL name;
IMP imp;
};

//Swift类描述
struct swift_class {
... //其他的属性,因为这里不关心就不列出了。
struct method_t methods[1];
... //其他的属性,因为这里不关心就不列出了。
//虚函数表刚好在结构体的第0x50的偏移位置。
IMP vtable[1];
};

//...........................................源代码中类的定义和方法的定义和实现部分

//类定义
struct MyUIView {
struct swift_class *isa;
}

//类的方法函数的实现

//本体函数foo的实现
void foo(){}
//跳板函数的实现
void trampoline_foo(id self, SEL _cmd){
asm("mov x20, x0");
self->isa->vtable[0](); //这里调用本体函数foo
}

//类的描述信息构建,这些都是在编译代码时就明确了并且保存在数据段中。
struct swift_class classMyUIView;
classMyUIView.methods[0] = {"foo", &trampoline_foo};
classMyUIView.vtable[0] = {&foo};


//...........................................源代码中程序运行的部分

//Swift代码部分
void main()
{
MyUIView *obj = MyUIView.__allocating_init(classMyUIView);
obj->isa = &classMyUIView;
asm("mov x20, obj");
//Swift方法foo的调用采用间接调用实现。
obj->isa->vtable[0]();
}

//OC代码部分
void main()
{
MyUIView *obj = objc_msgSend(objc_msgSend(classMyUIView, "alloc"), "init");
obj->isa = &classMyUIView;
//OC语言对foo的调用还是用objc_msgSend来执行调用。
//因为objc_msgSend最终会找到methods中的方法结构并调用trampoline_foo
//而trampoline_foo内部则直接调用foo来实现真实的调用。
objc_msgSend(obj, @selector(foo));
}

下面的图形展示了Swift中带@objc关键字的方法实现,以及OC语言调用Swift对象方法的实现:


Swift类方法的运行时替换实现的可行性

从上面的介绍中我们已经了解到了Swift类的常规方法定义和调用实现的机制,同样了解到Swift对象实例的开头部分也有和OC类似的isa数据,用来指向类的信息结构。一个令人高兴的事情就是Swift类的结构定义部分是存放在可读写的数据段中,这似乎给了我们一个提示是说可以在运行时通过修改一个Swift类的虚函数表的内容来达到运行时对象行为改变的能力。要实现这种机制有三个难点需要解决:

1、一个是Swift对内存和指针的操作进行了极大的封装,同时Swift中也不再支持简单直接的对内存进行操作的机制了。这样就使得我们很难像OC那样直接修改类结构的内存信息来进行运行时的更新处理,因为Swift不再公开运行时的相关接口了。虽然可以将方法函数名称赋值给某个变量,但是这个变量的值并非是类方法函数的真实地址,而是一个包装函数的地址。

2、第二个就是Swift中的类方法调用和参数传递的ABI规则和其他语言不一致。在OC类的对象方法中,对象是作为方法函数的第一个参数传递的。在机器指令层面以arm64体系结构为例,对象是保存在x0寄存器作为参数进行传递。而在Swift的对象方法中这个规则变为对象不再作为第一个参数传递了,而是统一改为通过寄存器x20来进行传递。需要明确的是这个规则不会针对普通的Swift函数。因此当我们想将一个普通的函数来替换类定义的对象方法实现时就几乎变得不太可能了,除非借助一些OC到Swift的桥的技术和跳板技术来实现这个功能也许能够成功。

当然我们也可以通过为类定义一个extension方法,然后将这个extension方法函数的指针来替换掉虚函数表中类的某个原始方法的函数指针地址,这样能够解决对象作为参数传递的寄存器的问题。但是这里仍然需要面临两个问题:一是如何获取得到extension中的方法函数的地址,二是在替换完成后如何能在合适的时机调用原始的方法。

3、第三是Swift语言将不再支持内嵌汇编代码了,所以我们很难在Swift中通过汇编来写一些跳板程序了。

因为Swift具有比较强的静态语言的特性,外加上函数调用的规则特点使得我们很难在运行时进行对象方法行为的改变。还有一个非常大的因素是当编译链接优化开关打开时,上述的对象方法调用规则还将进一步被打破,这样就导致我们在运行时进行对象方法行为的替换变得几乎不可能或者不可行。

编译链接优化开启后的Swift方法定义和调用

一个不幸的事实是,当我们开启了编译链接的优化选项后,Swift的对象方法的调用机制做了非常大的改进。最主要的就是进一步弱化了通过虚函数表来进行间接方法调用的实现,而是大量的改用了一些内联的方式来处理方法函数调用。同时对多态的支持也采用了一些别的策略。具体用了如下一些策略:

1、大量的将函数实现换成了内联函数模式,也就是对于大部分类中定义的源代码比较少的方法函数都统一换成内联。这样对象方法的调用将不再通过虚函数表来间接调用,而是简单粗暴的将函数的调用改为直接将内联函数生成的机器码进行拷贝处理。这样的一个好处就是由于没有函数调用的跳转指令,而是直接执行方法中定义的指令,从而极大的加速了程序的运行速度。另外一个就是使得整个程序更加安全,因为此时函数的实现逻辑已经散布到各处了,除非恶意修改者改动了所有的指令,否则都只会影响局部程序的运行。内联的一个的缺点就是使得整个程序的体积会增大很多。比如下面的类代码在优化模式下的Swift语言源代码和C语言伪代码实现:

////////Swift源代码

//类定义
class CA {
open func foo(_ a:Int, _ b:Int) ->Int {
return a + b
}

func main() {
let obj = CA()
let a = obj.foo(10,20)
let b = obj.foo(a, 40)
}
////////C伪代码


//...........................................运行时定义部分


//Swift类描述。
struct swift_class {
... //其他的属性,因为这里不关心就不列出了
//这里也没有虚表的信息。
};

//...........................................源代码中类的定义和方法的定义和实现部分


//类定义
struct CA {
struct swift_class *isa;
};

//这里没有方法实现,因为短方法被内联了。

struct swift_class classCA;


//...........................................源代码中程序运行的部分


void main() {
CA *obj = CA.__allocating_init(classCA);
obj->isa = &classCA;
int a = 10 + 20; //代码被内联优化
int b = a + 40; //代码被内联优化
}

2、就是对多态的支持,也可能不是通过虚函数来处理了,而是通过类型判断采用条件语句来实现方法的调用。就比如下面Swift语言源代码和C语言伪代码:

////////Swift源代码

//基类
class CA{
@inline(never)
open func foo(){}
}

//派生类
class CB:CA{
@inline(never)
override open func foo(){}
}

//全局函数接收对象作为参数
@inline(never)
func testfunc(_ obj:CA){
obj.foo()
}


func main() {
//对象的创建以及方法调用
let objA = CA()
let objB = CB()
testfunc(objA)
testfunc(objB)
}
////////C伪代码

//...........................................运行时定义部分


//Swift类描述
struct swift_class {
... //其他的属性,因为这里不关心就不列出了
//这里也没有虚表的信息。
};


//...........................................源代码中类的定义和方法的定义和实现部分

//类定义
struct CA {
struct swift_class *isa;
};

struct CB {
struct swift_class *isa;
};

//Swift类的方法的实现
//基类CA的foo方法实现
void fooForA(){}
//派生类CB的foo方法实现
void fooForB(){}
//全局函数方法的实现
void testfunc(CA *obj)
{
//这里并不是通过虚表来进行间接调用而实现多态,而是直接硬编码通过类型判断来进行函数调用从而实现多态的能力。
asm("mov x20, obj");
if (obj->isa == &classCA)
fooForA();
else if (obj->isa == &classCB)
fooForB();
}

//类的描述信息构建,这些都是在编译代码时就明确了并且保存在数据段中。
struct swift_class classCA;
struct swift_class classCB;

//...........................................源代码中程序运行的部分

void main() {
//对象实例创建以及方法调用的代码。
CA *objA = CA.__allocating_init(classCA);
objA->isa = &classCA;
CB *objB = CB.__allocating_init(classCB);
objB->isa = &classCB;
testfunc(objA);
testfunc(objB);
}

也许你会觉得这不是一个最优的解决方案,而且如果当再次出现一个派生类时,还会继续增加条件分支的判断。 这是一个多么低级的优化啊!但是为什么还是要这么做呢?个人觉得还是性能和包大小的问题。对于性能来说如果我们通过间接调用的形式可能需要增加更多的指令以及进行间接的寻址处理和指令跳转,而如果采用简单的类型判断则只需要更少的指令就可以解决多态调用的问题了,这样性能就会得到提升。至于第二个包大小的问题这里有必要重点说一下。

编译链接优化的一个非常重要的能力就是减少程序的体积,其中一个点即是链接时如果发现某个一个函数没有被任何地方调用或者引用,链接器就会把这个函数的实现代码整体删除掉。这也是符合逻辑以及正确的优化方式。回过头来Swift函数调用的虚函数表方式,因为根据虚函数表的定义需要把一个类的所有方法函数地址都存放到类的虚函数表中,而不管类中的函数是否有被调用或者使用。而通过虚函数表的形式间接调用时是无法在编译链接时明确哪个函数是否会被调用的,所以当采用虚函数表时就不得不把类中的所有方法的实现都链接到可执行程序中去,这样就有可能无形中增加了程序的体积。而前面提供的当编译链接优化打开后,系统尽可能的对对象的方法调用改为内联,同时对多态的支持改为根据类型来进行条件判断处理,这样就可以减少对虚函数表的使用,一者加快了程序运行速度,二者删除了程序中那些永远不会调用的代码从而减少程序包的体积。但是这种减少包体积的行为又因为内联的引入也许反而增加了程序包的体积。而这二者之间的平衡对于链接优化器是如何决策的我们就不得而知了。

综上所述,在编译器优化模式下虚函数调用的间接模式改变为直接模式了,所以我们几乎很难在运行时通过修改虚表来实现方法调用的替换。而且Swift本身又不再支持运行时从方法名到方法实现地址的映射处理,所有的机制都是在编译时静态决定了。正是因为Swift语言的特性,使得原本在OC中可以做的很多事情在Swift中都难以实现,尤其是一些公司的无痕埋点日志系统的建设,APM的建设,以及各种监控系统的建设,以及模拟系统的建设都将失效,或者说需要寻找另外一些途径去做这些事情。对于这些来说,您准备好了吗?

链接:https://www.jianshu.com/p/158574ab8809

收起阅读 »

iOS的异步处理神器——Promises

前言你是否因为多任务的依赖而头疼?你是否被一个个嵌套的block回调弄得晕头转向?快来投入Promises的怀抱吧。正文回调任务是很正常的现象,比如说购买一个商品,需要下单,然后等后台返回。单一任务,通常只需要一个block,非常清晰;以上面的下单为例,传给网...
继续阅读 »

前言

你是否因为多任务的依赖而头疼?你是否被一个个嵌套的block回调弄得晕头转向?
快来投入Promises的怀抱吧。

正文

回调任务是很正常的现象,比如说购买一个商品,需要下单,然后等后台返回。
单一任务,通常只需要一个block,非常清晰;
以上面的下单为例,传给网络层一个block,购买完成之后回调即可。

但是出现多个任务的时候,逻辑就开始有分支,同样以购买商品为例,在下单完成后,需要和SDK发起支付,然后根据支付结果再进行一些提示:
任务1是下单,执行完回调error指针(或者状态码)表示完成状态,同时待会下单信息,此时产生一个分支,成功继续下一步,失败执行错误block;
然后是执行任务2购买,执行异步的支付,根据支付结果又会产生一个分支。

当连续的任务超过2个之后,分支会导致代码逻辑非常混乱。


简单画一个流程图来分析,上述的逻辑变得复杂的原因是因为每一级的block需要处理下一级block的失败情况,导致逻辑分支的增多。

其实所有的失败处理都是类似的:打日志、提示用户,可以放在一起统一处理。
然后把任务一、任务二等串行执行,流程就非常清晰。


Promises就是用来辅助实现这样设计的库。
实现的代码效果如下:

- (void)workflow {
[[[[self order:@"order_id"] then:^id _Nullable(NSString * _Nullable value) {
return [self pay:value];
}] then:^id _Nullable(id _Nullable value) {
return [self check:value];
}] catch:^(NSError * _Nonnull error) {
NSLog(@"error: %@", error);
}];
}

Promises的使用

Promises库的引入非常简单,可以使用CocoaPod,Podfile如下:

pod 'PromisesObjC'

也可以到GitHub手动下载。

按照Promise设计模式的规范,每一个Promise应该有三种状态:pending(等待)、fulfilled(完成)、rejected(失败);
对应到Promises分别是:

[FBLPromise pendingPromise]; // pending等待
[FBLPromise resolvedWith:@"anyString"]; // fulfilled完成
[FBLPromise resolvedWith:[NSError new]]; // rejected失败

实际使用中,我们更多使用的Promises库已经提供好的便捷函数:

启动一个异步任务 :

[FBLPromise onQueue:dispatch_get_main_queue()
async:^(FBLPromiseFulfillBlock fulfill,
FBLPromiseRejectBlock reject) {
BOOL success = arc4random() % 2;
if (success) {
fulfill(@"success");
}
else {
reject([NSError errorWithDomain:@"learn_promises_error" code:-1 userInfo:nil]);
}
}];

或者简单使用do方法:

[FBLPromise do:^id _Nullable{
BOOL success = random() % 2;
if (success) {
return @"success";
}
else {
return [NSError errorWithDomain:@"learn_promises_error" code:-1 userInfo:nil];
}
}];

不管是async方法还是do方法,他们的返回值都是创建一个Promise对象,可以在Promise对象后面挂一个then方法,表示这个Promise执行完毕之后,要继续执行的任务:

[[[FBLPromise do:^id _Nullable{
BOOL success = arc4random() % 2;
return success ? @"do_success" : [NSError errorWithDomain:@"learn_promises_do_error" code:-1 userInfo:nil];
}] then:^id _Nullable(id _Nullable value) {
BOOL success = arc4random() % 2;
return success ? @"then_success" : [NSError errorWithDomain:@"learn_promises_then_error" code:-1 userInfo:nil];
}] catch:^(NSError * _Nonnull error) {
NSLog(@"error: %@", error);
}];

上面的catch方法表示统一的error处理。
promise在完成任务之后,如果满足下面的条件会调用then的方法:
1、直接调用fulfill;
2、在do方法中返回一个值(不能为error);
3、在then方法中返回一个值;

调用reject方法或者返回一个NSError对象,都会转到catch方法处理。

用上面的do、then、catch方法组合,就完成多个异步任务的依赖执行:

- (void)workflow {
[[[[self order:@"order_id"] then:^id _Nullable(NSString * _Nullable value) {
return [self pay:value];
}] then:^id _Nullable(id _Nullable value) {
return [self check:value];
}] catch:^(NSError * _Nonnull error) {
NSLog(@"error: %@", error);
}];
}

- (FBLPromise<NSString *> *)order:(NSString *)orderParam {
return [FBLPromise do:^id _Nullable{
return @"order_success";
}];
}

- (FBLPromise<NSString *> *)pay:(NSString *)payParam {
return [FBLPromise do:^id _Nullable{
BOOL success = arc4random() % 2;
return success ? @"pay_success" : [NSError errorWithDomain:@"pay_error" code:-1 userInfo:nil];
}];
}

- (FBLPromise<NSString *> *)check:(NSString *)checkParam {
return [FBLPromise do:^id _Nullable{
return @"check success";
}];
}

Promises还提供了很多附加特性,以All和Any为例:
All是所有Promise都fulfill才算完成;
Any是任何一个Promise完成都会执行fulfill;

- (void)testAllAndAny {
NSMutableArray *arr = [NSMutableArray new];
[arr addObject:[self work1]];
[arr addObject:[self work2]];

[[[FBLPromise all:arr] then:^id _Nullable(NSArray * _Nullable value) {
NSLog(@"then, value:%@", value);
return value;
}] catch:^(NSError * _Nonnull error) {
NSLog(@"all error:%@", error);
}];

[[[FBLPromise any:arr] then:^id _Nullable(NSArray * _Nullable value) {
NSLog(@"then, value:%@", value);
return value;
}] catch:^(NSError * _Nonnull error) {
NSLog(@"any error:%@", error);
}];
}

- (FBLPromise<NSString *> *)work1 {
return [FBLPromise do:^id _Nullable{
BOOL success = arc4random() % 2;
return success ? @"work1 success" : [NSError errorWithDomain:@"work1_error" code:-1 userInfo:nil];
}];
}

- (FBLPromise<NSNumber *> *)work2 {
return [FBLPromise do:^id _Nullable{
BOOL success = arc4random() % 2;
return success ? @"work2 success" : [NSError errorWithDomain:@"work2_error" code:-1 userInfo:nil];
}];
}

Promises原理解析

Promises库的设计很简单,基于Promise设计模式和iOS的GCD来实现。
整个库由Promise.m/.h和他的Catagory组成。Catagory都是附加特性,基于Promise.m/.h提供的方法做扩展,所以这里重点解析下Promise.m/h。
Promise类public头文件只有寥寥数个方法:

// 静态方法
[FBLPromise pendingPromise]; // pending等待
[FBLPromise resolvedWith:@"anyString"]; // fulfilled完成
[FBLPromise resolvedWith:[NSError new]]; // rejected失败
// 实例方法
- (void)fulfill:(nullable Value)value; // 完成一个promise
- (void)reject:(NSError *)error;// rejected一个promise

重点在于private.h提供的两个方法:

/**
对一个promise添加fulfill和reject的回调
*/
- (void)observeOnQueue:(dispatch_queue_t)queue
fulfill:(FBLPromiseOnFulfillBlock)onFulfill
reject:(FBLPromiseOnRejectBlock)onReject NS_SWIFT_UNAVAILABLE("");

/**
创建一个promise,并设置fulfill、reject方法为传进来的block
*/
- (FBLPromise *)chainOnQueue:(dispatch_queue_t)queue
chainedFulfill:(FBLPromiseChainedFulfillBlock)chainedFulfill
chainedReject:(FBLPromiseChainedRejectBlock)chainedReject NS_SWIFT_UNAVAILABLE("");

observeOnQueue方法是promise的实例方法,根据promise当前的状态,如果是fulfilled或者rejected状态则会dispatch_group_async到下一次执行对应的onFulfill和onReject回调;如果是pending状态则会创建_observers数组,往_observers数组中添加一个block回调,当promise执行完毕的时候,根据state选择onFulfill或者onReject回调。

chainOnQueue方法同样是promise的实例方法,返回的是一个FBLPromise的对象(状态是pending)。
方法首先创建的是promise对象,接着创建了resolver的回调,然后调用observeOnQueue方法。
当self(也是一个promise)执行完毕后,会根据fulfill、reject回调类型接着执行chainedFulfill、chainedReject;
最后将结果抛给resolver执行,resolver会根据返回值value进行判断,如果仍是promise则递归执行,否则直接调用fulfill方法。
fulfill方法则会判断value是否为NSError,如果是NSError则转为reject,否则将状态改为Fulfilled,并且通知observer数组。

- (FBLPromise *)chainOnQueue:(dispatch_queue_t)queue
chainedFulfill:(FBLPromiseChainedFulfillBlock)chainedFulfill
chainedReject:(FBLPromiseChainedRejectBlock)chainedReject {
NSParameterAssert(queue);

FBLPromise *promise = [[FBLPromise alloc] initPending];
__auto_type resolver = ^(id __nullable value) {
if ([value isKindOfClass:[FBLPromise class]]) {
[(FBLPromise *)value observeOnQueue:queue
fulfill:^(id __nullable value) {
[promise fulfill:value];
}
reject:^(NSError *error) {
[promise reject:error];
}];
} else {
[promise fulfill:value];
}
};
[self observeOnQueue:queue
fulfill:^(id __nullable value) {
value = chainedFulfill ? chainedFulfill(value) : value;
resolver(value);
}
reject:^(NSError *error) {
id value = chainedReject ? chainedReject(error) : error;
resolver(value);
}];
return promise;
}

Promises中的dispatch_group_enter() 和 dispatch_group_leave() 是成对使用,但是和平时使用GCD不同,这里并没有用到dispath_group_notify方法。
在刚开始看Promises源码时,产生过一个疑问,为什么所有Promises的操作要放在同一个group内?

+ (dispatch_group_t)dispatchGroup {
static dispatch_group_t gDispatchGroup;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
gDispatchGroup = dispatch_group_create();
});
return gDispatchGroup;
}

直到发现FBLWaitForPromisesWithTimeout方法,里面有一个dispatch_group_wait方法(等待group中所有block执行完毕,或者在指定时间结束后回调)。
dispatch_group_wait方法与dispath_group_notify方法类似,只是多了一个超时时间,如果调用dispatch_group_wait(DISPATCH_TIME_FOREVER)则和dispath_group_notify方法一样。

总结

附加的特性有很多,类似Retry、Delay等,但实际使用中Promise用do、then、catch、async等少数几个已经可以满足需求。
能够实现Promise设计模式的库比较多,Promises是性能和接口调用清晰度都比较不错的。
使用设计模式可以简化逻辑代码,同时也使得代码的健壮性更强。

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

收起阅读 »

在Swift中自定义Codable类型

大多数现代应用程序的共同点是,它们需要对各种形式的数据进行编码或解码。无论是通过网络下载的JSON数据,还是本地存储的模型的某种形式的序列化表示 - 能够可靠地编码和解码不同的数据对于或多或少的任何Swift代码库都是必不可少的。这是Swift的Codable...
继续阅读 »

大多数现代应用程序的共同点是,它们需要对各种形式的数据进行编码或解码。无论是通过网络下载的JSON数据,还是本地存储的模型的某种形式的序列化表示 - 能够可靠地编码和解码不同的数据对于或多或少的任何Swift代码库都是必不可少的。

这是Swift的Codable API在作为Swift 4.0的一部分引入时如此重要的新功能的一个重要原因 - 从那时起它已经发展成为几种不同类型的编码和解码的标准,强大的机制 - 在Apple的平台,以及服务器端Swift。

使Codable如此出色的原因在于它与Swift工具链紧密集成,使编译器能够自动合成编码和解码各种值所需的大量代码。但是,有时我们确实需要自定义序列化时我们的值的表示方式 - 所以本周,我们来看看我们可以通过几种不同的方式调整我们的Codable实现来做到这一点。

改变钥匙

让我们从一种基本方法开始,我们可以自定义类型的编码和解码方式 - 通过修改用作序列化表示的一部分的键。假设我们正在开发一个用于阅读文章的应用程序,我们的核心数据模型之一如下所示:

struct Article: Codable {
var url: URL
var title: String
var body: String
}

我们的模型当前使用完全自动合成的Codable实现,这意味着它的所有序列化键都将匹配其属性的名称。但是,我们将解码Article值的数据(例如从服务器下载的JSON)可能使用稍微不同的命名约定,导致默认解码失败。

谢天谢地,这很容易修复。我们需要做的就是自定义Codable在解码(或编码)我们Article类型的实例时将使用的键是在其中定义CodingKeys枚举 - 并将自定义原始值分配给匹配我们希望自定义的键的案例 - 像这样:

extension Article {
enum CodingKeys: String, CodingKey {
case url = "source_link"
case title = "content_name"
case body
}
}

执行上述操作后,我们可以继续利用编译器生成的默认实现进行实际的编码工作,同时仍然允许我们更改将用于序列化的键的名称。

虽然上述技术非常适合我们想要使用完全自定义的键名称,但如果我们只想让Codable使用snake_case我们的属性名称版本(例如backgroundColor转入background_color) - 那么我们可以简单地改变我们的JSON解码器keyDecodingStrategy:

var decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

上述两个API的优点在于它们使我们能够解决Swift模型与用于表示它们的数据之间的不匹配问题,而无需我们修改属性的名称。

忽略键

虽然能够自定义编码密钥的名称非常有用,但有时我们可能希望完全忽略某些密钥。例如,我们现在说我们正在制作一个笔记记录应用程序 - 并且我们允许用户将各种笔记组合在一起形成一个NoteCollection,其中可以包括本地草稿:

struct NoteCollection: Codable {
var name: String
var notes: [Note]
var localDrafts = [Note]()
}

然而,尽管成为localDrafts我们NoteCollection模型的一部分真的很方便- 但是我们说在序列化或反序列化这样的集合时我们不希望包含这些草稿。这样做的原因可能是每次启动应用程序时给用户一个干净的名单,或者因为我们的服务器不支持草稿。

幸运的是,这也可以轻松完成,而无需更改实际的Codable实现NoteCollection。如果我们CodingKeys像之前一样定义枚举,并且只是省略localDrafts- 那么在编码或解码NoteCollection值时不会考虑该属性:

extension NoteCollection {
enum CodingKeys: CodingKey {
case name
case notes
}
}

为了使上述工作,我们省略的属性必须具有默认值 - localDrafts在这种情况下已经具有。

创建匹配结构

到目前为止,我们只调整了一个类型的编码键 - 虽然我们通常可以做到这一点,但有时我们需要在Codable自定义方面更进一步。

假设我们正在构建一个包含货币转换功能的应用程序,并且我们将给定货币的当前汇率作为JSON数据下载,如下所示:

{
"currency": "PLN",
"rates": {
"USD": 3.76,
"EUR": 4.24,
"SEK": 0.41
}
}

在我们的Swift代码中,我们希望将这些JSON响应转换为CurrencyConversion实例 - 每个实例包含一个ExchangeRate条目数组- 每种货币对应一个:

struct CurrencyConversion {
var currency: Currency
var exchangeRates: [ExchangeRate]
}

struct ExchangeRate {
let currency: Currency
let rate: Double
}

但是,如果我们只是继续使上述两个模型都符合Codable,我们再次得出我们的Swift代码和我们想要解码的JSON数据之间的不匹配。但这一次,它不仅仅是关键名称的问题 - 结构上存在根本区别。

当然,我们可以修改我们的Swift模型的结构以完全匹配我们的JSON数据的结构 - 但这并不总是实用的。虽然拥有正确的序列化代码很重要,但拥有适合我们实际代码库的模型结构同样重要。

相反,让我们创建一个新的专用类型 - 它将充当我们的JSON数据中使用的格式与Swift代码结构之间的桥梁。在该类型中,我们将能够封装将汇率的JSON字典转换为ExchangeRate模型数组所需的所有逻辑- 如下所示:

private extension ExchangeRate {
struct List: Decodable {
let values: [ExchangeRate]

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dictionary = try container.decode([String : Double].self)

values = dictionary.map { key, value in
ExchangeRate(currency: Currency(key), rate: value)
}
}
}
}

使用上面的类型,我们现在可以定义一个私有属性,该属性与用于其数据的JSON密钥匹配 - 并且我们的exchangeRates属性只是充当该私有属性的面向公众的代理:

struct CurrencyConversion: Decodable {
var currency: Currency
var exchangeRates: [ExchangeRate] {
return rates.values
}

private var rates: ExchangeRate.List
}

上述工作的原因是因为在编码或解码值时从不考虑计算属性。

当我们想要使Swift代码与使用非常不同结构的JSON API兼容时,上述技术可以成为一个很好的工具 - 再次无需Codable从头开始实现。

转变价值观

在解码时,尤其是在使用我们无法控制的外部JSON API时,一个非常常见的问题是,类型的编码方式与Swift的严格类型系统不兼容。例如,我们要解码的JSON数据可能使用字符串来表示整数或其他类型的数字。

让我们看看一种可以让我们处理这些值的方法,再次以一种自包含的方式,不需要我们编写完全自定义的Codable实现。

我们在这里要做的就是将字符串值转换为另一种类型 - 让我们Int以此为例。我们首先定义一个协议,让我们将任何类型标记为StringRepresentable- 意味着它可以从字符串表示转换为字符串表示:

protocol StringRepresentable: CustomStringConvertible {
init?(_ string: String)
}

extension Int: StringRepresentable {}

我们将上述协议基于CustomStringConvertible标准库,因为它已经包含了将值描述为字符串的属性要求。有关将协议定义为其他协议的特殊方法的更多信息,请查看“Swift中的专业协议”。

接下来,让我们创建另一个专用类型 - 这次是任何可以由字符串支持的值- 并且它包含解码和编码字符串值所需的所有代码:

struct StringBacked: Codable {
var value: Value

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)

guard let value = Value(string) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: """
Failed to convert an instance of \(Value.self) from "\(string)"
"""
)
}

self.value = value
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value.description)
}
}

就像我们之前为我们的JSON兼容的底层存储创建私有属性一样,我们现在可以对编码时由字符串后端的任何属性执行相同的操作 - 同时仍然将该数据暴露给我们的其余Swift代码类型。这是一个为Video类型的numberOfLikes属性做这样的例子:

struct Video: Codable {
var title: String
var description: String
var url: URL
var thumbnailImageURL: URL

var numberOfLikes: Int {
get { return likes.value }
set { likes.value = newValue }
}

private var likes: StringBacked
}

在必须为属性手动定义setter和getter的复杂性以及必须回退到完全自定义Codable实现的复杂性之间肯定存在权衡- 但对于类似上述Video结构的类型,其仅具有需要的一个属性使用私有支持属性进行自定义可能是一个很好的选择。

结论

虽然编译器能够自动合成所有Codable不需要任何形式定制的一致性,但真正太棒了- 我们能够在需要时自定义事物同样非常棒。

更妙的是,这样做往往并不真正需要我们赞成手工执行的彻底抛弃自动生成的代码-这是很多次可能只是稍微调整一个类型的编码或解码的方式,同时还让编译器做大部分繁重的工作。

链接:https://www.jianshu.com/p/62162d01d1df

收起阅读 »

CocoaPod知识整理

前言Pod库是很重要的组成部分,大部分第三方库都是通过CocoaPod的方式引入和管理,同时项目中的部分功能也可以用Pod库来做模块化。本文是对CocoaPod的一些探究。XS项目中的Pod库是很重要的组成部分,目前阅读器模块正在进行SDK化,需要用Pod库来...
继续阅读 »

前言

Pod库是很重要的组成部分,大部分第三方库都是通过CocoaPod的方式引入和管理,同时项目中的部分功能也可以用Pod库来做模块化。
本文是对CocoaPod的一些探究。
XS项目中的Pod库是很重要的组成部分,目前阅读器模块正在进行SDK化,需要用Pod库来管理,同时未来会做一些模块化的功能,同样需要用Pod库来处理。
本文对CocoaPods的一些内容进行探究。

正文

CocoaPods是为iOS工程提供第三方依赖库管理的工具,用CocoaPods可以更方便地管理第三方库:把依赖库统一放在Pods工程中,同时让主工程依赖Pods工程。Pods工程的target是libPods-targetName.a静态库,主工程会依赖这个.a静态库。 (下面会详细剖析这个处理过程)

CocoaPods相比手动引入framework或者子工程依赖的方式,有两个便捷之处:

所有Pod库集中管理,版本更新只需Podfile配置文件;
依赖关系的自动解析;
同时CocoaPods的使用流程很简单:(假设已经安装CocoaPods)
1、在xcodeproj所在目录下,新建Podfile文件;

2、描述依赖信息,以demo为例,有AFNetworking和SDWebImage两个第三方库:

target 'LearnPod' do
pod 'AFNetworking'
pod 'SDWebImage'
end

3、打开命令行,执行pod install ;

4、打开生成xcworkspace,就可以继续开发;

一、Podfile的写法

1、普通的写法;
pod 'AFNetworking' 或者 pod 'AFNetworking', '3.2.1',前者是下载最新版本,后者是下载指定版本。

2、指向本地的代码分支;

pod 'AFNetworking', :path => '/Users/loyinglin/Documents/Learn/AFNetworking'

指向的本地目录要带有podspec文件。


3、指定远端的代码分支;

pod 'AFNetworking', :git => 'https://github.com/AFNetworking/AFNetworking.git', :branch => 'master'

指向的repo仓库要带有podspec文件。


4、针对特定的configurations用不同的依赖库

`pod 'AFNetworking', :configurations => ['Release']`

如上,只有Release的configurations生效;(同理,可以设置Debug)


5、一些其他的feature

优化pod install速度,可以进行依赖打平:将pod库的依赖库明确的写在Podfile,主端已经提供对应的工具。

`require "bd_pod_extentions"`

`bytedanceAnalyzeSpeed(true)`

`bd_use_app('toutiao','thirdParty','public')`

post install的脚本,修改安装后的Pod库工程中的target设置;同理,可以修改其他属性的设置。

post_install do |installer_representation|
installer_representation.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ONLY_ACTIVE_ARCH'] = 'NO'`
end
end
end

类似的还有pre_install的脚本;(但是install之前可能都没有pods_project,所以用处也比较少;具体的参数意义可自查,以pods_project为例)

puts只有在添加--verbose参数可以看到,Pod::UI.puts则是全文可见。

pre_install do |installer|
puts "pre install hook"
Pod::UI.puts "pre install hook puts"
end

Podfile还可以设置一些警告提示的去除,第一行是去掉pod install时候的警告信息,第二行是去掉build时候的警告信息。

# 去掉pod install时候的警告信息
install! 'cocoapods', :warn_for_multiple_pod_sources => false
inhibit_all_warnings

二、Pods目录

Pods目录是pod install之后CocoaPod生成的目录。


目录的组成部分:

1、Pods.xcodeproj,Pods库的工程;每个Pod库会对应其中某个target,每个target都会打包出来一个.a文件;
2、依赖库的文件目录;以SDWebImage为例,会有个SDWebImage目录存放文件;
3、manifest.lock,Pods目录中的Pod库版本信息;每次pod install的时候会检查manifest.lock和Podfile.lock的版本是否一致,不一致的则会更新;
4、Target Support Files、Headers、Local Podspecs目录等;Target Support Files里面是一些target的工程设置xcconifg以及脚本等,Headers里面有Public和Private的头文件目录,Local Podspecs是存放从本地Pod库install时的podspec;

三、CocoaPods的其他重要部分

1.Podfile.lock文件

pod install会解析依赖并生成Podfile.lock文件;如果Podfile.lock存在时执行pod install,则不会修改已经install的pod库。(注意,pod update则会忽视Podfile.lock进行依赖解析,最后重新install所有的Pod库,生成新的Podfile.lock)
在多人开发的项目中,Pods目录由于体积较大,往往不会放在Git仓库中,Podfile.lock文件则建议添加到Git仓库。当其他人修改Podfile时,pod install生成新的Podfile.lock文件也会同步到Git。这样能保证拉下来的版本库是其他人一致的。

实际开发中,也会通过依赖打平来避免多人协作的Pod版本不一致问题。
pod install的时候,Pods目录下生成一个Manifest.lock文件,内容与.lock文件完全一致;在每次build工程的时候,会检查这两个文件是否一致。


2、Pod库的podspec文件
在每个Pod库的仓库中,都会有一个podspec文件,描述Pod库的版本、依赖等信息。
如下,是一个普通的Pod库的podspec:


3、Pod库依赖解析

CocoaPod的依赖管理相对第三方库手动管理更加便捷。
在手动管理第三方库中,如果库A集成了库F,库B也集成了库F ,就会遇到库F符号冲突的问题,需要将库A/B和库F的代码分开,手动添加库F;后续如果库A/B版本有更新,也需要手动去处理。
而在CocoaPod依赖解析中,可以把每个Pod库都看成一个节点,Pod库的依赖是它的子节点; 依赖解析的过程,就是在一个有向图中找到一个拓扑序列。
一个合法的Podfile描述的应该是一个有向无环图,可以通过拓扑排序的方式,得到一个AOV网。
按照这个拓扑序列中的顶点次序,可以依次install所有的Pod库并且保证其依赖的库已经install。

有时候会陷入循环依赖的怪圈,就是因为在有向图中出现环,则无法通过算法得到一个拓扑排序。

四、Pods工程和主工程的关系

在实际的开发过程,容易知道Pods工程是先编译,编译完再执行主工程的编译;因为主工程的Linked Libraries里面有libPods-LearnPod.a的文件。(LearnPod是target的名字,下面的示例图都是用LearnPod作为target名)


那么Pod库中的target编译顺序是如何决定?
打开workspace,选择Pods工程。从上图分析我们知道,主工程最终需要的是libPods-LearnPod.a这一个静态库文件。
我们通常打包,最终名字都是target的名字;而静态库通常会在前面加上lib的前缀。所以libPods-LearnPod.a这个静态库的target名字应该是Pods-LearnPod。
从下图我们也可以确定,确实是在前面添加了lib的前缀。


看看Pods-LearnPod的Build Phases选项,从target依赖中可以看到其他两个target。


分析至此,我们可以知道这里的编译顺序是AFNetworking、SDWebImage、Pods-LearnPod、LeanPod(主工程target)。
接下来我们分析编译过程。AFNetworking因为没有依赖,所以编译的时候只需要知道自己的.h/.m文件。


对于Pods-LearnPod,其有两个依赖,分别是AFNetworking和SDWebImage;所以在Header Search Paths中需要设置这两个库的Public头文件地址。


编译的结果是3个.a文件(libPods-LearnPod.a、libAFNetworking.a、libSDWebImage.a),只有libPods-LearnPod.a是主工程的编译依赖。那么libPods-LearnPod.a是否为多个.a文件的集合?


从libPods-LearnPod.a的大小,我们可以知道libPods-LearnPod不是多个.a的集合,仅仅是作为主工程的一个依赖,使得Pod库工程能先于主工程编译。
那么,主工程编译的时候如何去找到AFNetworking的头文件和.a文件?
从主工程的Search Paths我们可以看到,Header是有说明具体的位置;
同时Library也有相对应的Paths,在对应的位置放着libAFNetworking.a文件;


这些信息是CocoaPod生成的一份xcconfig,里面的HEADER_SEARCH_PATHS和LIBRARY_SEARCH_PATHS会指明这两个地址。


对于资源文件,CocoaPods 提供了一个名为 Pods-resources.sh 的 bash 脚本,该脚本在每次项目编译的时候都会执行,将第三方库的各种资源文件复制到目标目录中。
CocoaPods 通过一个名为 Pods.xcconfig 的文件来在编译时设置所有的依赖和参数。
在编译之前会检查pod的版本是否发生变化(manifest和.lock文件对比),以及执行一些自定义的脚本。
Pod库的子target在指定armv7和arm64两个架构的时候,会分别编译生成armv7和arm64的.a文件;然后再进行一次合并操作,得到一个.a文件。
编译完成后进行链接,在armv7和arm64都指定时,会分别进行链接,最后合并得到可执行文件。
得到可执行文件后,会进行asset、storyboard等资源文件的处理;还会执行pod的脚本,把pod的资源复制过来。
全部准备就绪,就会生成符号表,包括.a文件里面的符号。
最后进行签名、校验,得到.app文件。

五、常用Pod指令

pod install,最常用的指令;
pod update,更新repo并重新解析依赖;
pod install --repo-update,类似pod update;
pod install --no-repo-update,忽略Pod库更新,直接用本地repo进行install;
pod update --no-repo-update,类似pod install;
pod update AFNetworking,更新指定库;

以上所有指令都可以添加 --verbose ,查看更详细的信息;
xcconfig在新增configuration之后,需要重新pod install,并修改xcconfig。

转自:https://www.jianshu.com/p/07ddbd829efc

收起阅读 »

可变共享结构(第二部分)

我们改进了新数据类型的观察能力。在上一章中,我们构建了一个名为的struct / class混合类型 Var。今天我们将继续实验。Var类包含一个结构,我们可以利用关键路径寻找到的结构。如果我们有一个people内部的阵列Var,我们希望采取先Person出数...
继续阅读 »

我们改进了新数据类型的观察能力。

在上一章中,我们构建了一个名为的struct / class混合类型 Var。今天我们将继续实验。

Var类包含一个结构,我们可以利用关键路径寻找到的结构。如果我们有一个people内部的阵列Var,我们希望采取先Person出数组,那么我们得到另一个Var与 Person。更新它Person会修改原始数组,这样我们就会给出Var引用语义。但是如果我们需要的话,我们仍然可以获得结构的复制行为:我们可以将结构值取出Var并具有本地副本。

我们也深入观察。只要有任何变化,根变量就会知道它。我们仍然有一个有点笨拙的API,因为我们Var使用observe闭包初始化,这意味着我们只能在初始化时在根级别添加一个观察者。我们想用一种addObserver方法改进这个API,并且如果我们想要观察根结构或任何其他属性,请使用它。

添加观察者

我们从初始化程序中删除观察者闭包并设置一个新addObserver方法。因为我们将大量使用观察者闭包,所以我们可以为它创建一个类型别名:

final class Var {
// ...
init(initialValue: A) {
var value: A = initialValue {
didSet {

}
}
_get = { value }
_set = { newValue in value = newValue }
}

typealias Observer = (A) -> ()
func addObserver(_ observer: @escaping Observer) {

}
// ... }

以前,我们将一个观察者闭包连接到初始化器中的struct值,但现在我们无法访问那里的观察者。我们需要将所有观察者存储在一个地方,从一个空数组开始,然后连接观察者和结构值:

final class Var {
// ...
init(initialValue: A) {
var observers: [Observer] = []
var value: A = initialValue {
didSet {
for o in observers {
o(value)
}
}
}
_get = { value }
_set = { newValue in value = newValue }
}
// ... }

现在我们仍然需要一种方法来向数组中添加一个观察者。我们重申我们做与技巧get,并set与转addObserver成一个属性,而不是一个方法:

final class Var {
let addObserver: (_ observer: @escaping Observer) -> ()

// ...

init(initialValue: A) {
var observers: [Observer] = []
var value: A = initialValue {
didSet {
for o in observers {
o(value)
}
}
}
_get = { value }
_set = { newValue in value = newValue }
addObserver = { observer in observers.append(observer) }
}
// ... }

在我们可以使用之前addObserver,我们必须将它设置在我们的其他私有初始化程序中。为此,我们将从外部传入一个闭包,以便我们可以在下标实现中定义闭包:

fileprivate init(get: @escaping () -> A, set: @escaping (A) -> (), addObserver: @escaping (@escaping Observer) -> ()) {
_get = get
_set = set
self.addObserver = addObserver
}

在键路径下标中,我们现在必须定义一个addObserver 闭包,它接受一个观察者并用类型的值调用这个观察者 B。我们只有类型的值A,但我们也可以self在这个闭包中观察并使用关键路径来获取B收到的A:

subscript(keyPath: WritableKeyPath) -> Var {
return Var(get: {
self.value[keyPath: keyPath]
}, set: { newValue in
self.value[keyPath: keyPath] = newValue
}, addObserver: { observer in
self.addObserver { newValue in
observer(newValue[keyPath: keyPath])
}
})
}

无论我们嵌套我们Var的深度多少,观察者总是被添加到根Var,因为一个孩子Var通过观察者直到它到达observers根的数组Var。这意味着只要根值发生变化就会调用观察者 - 换句话说:即使属性本身未更改,也可能会调用特定属性的观察者。

我们还在addObserver集合的下标中传递了一个类似的闭包:

extension Var where A: MutableCollection {
subscript(index: A.Index) -> Var {
return Var(get: {
self.value[index]
}, set: { newValue in
self.value[index] = newValue
}, addObserver: { observer in
self.addObserver { newValue in
observer(newValue[index])
}
})
}
}

让我们看看这是如何工作的:

let peopleVar: Var<[Person]> = Var(initialValue: people)
peopleVar.addObserver { p in
print("peoplevar changed: \(p)")
}
let vc = PersonViewController(person: peopleVar[0])
vc.update()

这会将peopleVar更改打印到控制台。但是,我们现在也可以添加一个观察者到Var的PersonViewController,这将打印与新的一个额外的行Person值:

final class PersonViewController {
let person: Var

init(person: Var) {
self.person = person
self.person.addObserver { newPerson in
print(newPerson)
}
}

func update() {
person.value.last = "changed"
}
}

删除观察者

我们现在可以添加观察者,但我们无法删除它们。如果视图控制器因为其观察者仍然在那里而消失,这就成了问题。

我们可以采取类似于反应性图书馆工作方式的方法。添加观察者时,将返回不透明对象。通过保持对该对象的引用,我们保持观察者活着。当我们丢弃对象时,观察者将被删除。

我们使用一个名为的辅助类,Disposable它接受一个在对象取消时调用的dispose函数:

final class Disposable {
private let dispose: () -> ()
init(_ dispose: @escaping () -> ()) {
self.dispose = dispose
}
deinit {
dispose()
}
}

我们更新签名addObserver返回Disposable:

final class Var {
private let _get: () -> A
private let _set: (A) -> ()
let addObserver: (_ observer: @escaping Observer) -> Disposable

// ... }

如果我们想要删除观察者,我们必须改变观察者商店的数据结构。数组不再有效,因为无法比较函数以找到要删除的数组。相反,我们可以使用由唯一整数键入的字典:

final class Var {
// ...

init(initialValue: A) {
var observers: [Int:Observer] = [:]
// ...
}
// ... }

生成这些整数的一种很酷的方法是使用Swift的惰性集合。我们创建一个范围从0到无穷大的迭代器,每次我们需要一个id时,我们可以调用next()这个迭代器。这返回一个可选项,但是我们可以强制解包它,因为我们知道它不能是nil:

final class Var {
// ...

init(initialValue: A) {
var observers: [Int:Observer] = [:]
// ...
var freshInt = (0...).makeIterator()
addObserver = { observer in
let id = freshInt.next()!
observers[id] = observer
// ...
}
}
// ... }

我们现在将观察者存储在字典中。当我们不再使用它们时,剩下要做的就是丢弃观察者。我们返回一个Disposable 带有dispose函数的函数,该函数从字典中删除观察者:

final class Var {
// ...

init(initialValue: A) {
var observers: [Int:Observer] = [:]
// ...
var freshInt = (0...).makeIterator()
addObserver = { observer in
let id = freshInt.next()!
observers[id] = observer
return Disposable { observers[id] = nil }
}
}
// ... }

最后,我们必须addObserver在私有初始化程序中修复签名,它仍然声明返回void而不是Disposable:

fileprivate init(get: @escaping () -> A, set: @escaping (A) -> (), addObserver: @escaping (@escaping Observer) -> Disposable) { /*...*/ }

现在我们的代码再次编译,但是我们确实得到了一个编译器警告我们忽略Disposable了视图控制器中返回的事实。这就解释了为什么我们不再使用更改的Person值获取print语句,因为我们应该保留对观察者的引用以 Disposable使其保持活动状态:

final class PersonViewController {
let person: Var
let disposable: Any?

init(person: Var) {
self.person = person
disposable = self.person.addObserver { newPerson in
print(newPerson)
}
}

func update() {
person.value.last = "changed"
}
}

我们现在保留观察者,我们在更新后得到了print语句Person。重申这里发生的事情:视图控制器被释放的那一刻,它的属性被清除,Disposabledeinits,并且这通过将其id设置为来调用将观察者带出字典的代码nil。

注意:如果我们想self在观察者中使用,我们必须使它成为弱引用,以避免创建引用循环。

比较新旧价值观

我们实现的一个重要方面是观察者不仅在观察到的Var变化时触发,而且在整个数据结构发生任何变化时触发。

如果PersonViewController想要确定它已经Person 改变了,它应该能够将新值与旧值进行比较。因此,我们将更改Observer类型别名以提供新值和旧值

typealias Observer = (A, A) -> ()

这意味着使用新版本和旧版本调用观察者 A。为了明确这一点,我们应该将值包装在一个结构中,并在两个字段中描述它们是什么,但我们正在跳过该部分。

我们在里面调用观察者的地方Var,我们现在也必须传递旧值:

init(initialValue: A) {
// ...
var value: A = initialValue {
didSet {
for o in observers.values {
o(value, oldValue)
}
}
}
// ... }

subscript(keyPath: WritableKeyPath) -> Var {
return Var(get: {
self.value[keyPath: keyPath]
}, set: { newValue in
self.value[keyPath: keyPath] = newValue
}, addObserver: { observer in
self.addObserver { newValue, oldValue in
observer(newValue[keyPath: keyPath], oldValue[keyPath: keyPath])
}
})
}

而在MutableCollection标,我们也应该通过旧值观察员:

extension Var where A: MutableCollection {
subscript(index: A.Index) -> Var {
return Var(get: {
self.value[index]
}, set: { newValue in
self.value[index] = newValue
}, addObserver: { observer in
self.addObserver { newValue, oldValue in
observer(newValue[index], oldValue[index])
}
})
}
}

观察者PersonViewController可以比较新旧版本,看看它的模型是否确实改变了:

final class PersonViewController {
// ...
init(person: Var) {
self.person = person
disposable = self.person.addObserver { newPerson, oldPerson in
guard newPerson != oldPerson else { return }
print(newPerson)
}
}
// ... }

最后,我们需要修复观察者peopleVar:

peopleVar.addObserver { newPeople, oldPeople in
print("peoplevar changed: \(newPeople)")
}

通过对视图控制器中的人进行更改,我们测试视图控制器的观察者忽略它:

peopleVar[1].value.first = "Test"

讨论

我们将反应式编程与观察变化的能力和面向对象的编程结合起来。

在这段代码中还有一个令人惊讶的等待。我们将第一个Person从数组移交给视图控制器。如果我们然后删除people数组的第一个元素,视图控制器突然有一个不同的 Person:

peopleVar.value.removeFirst()

的第一个元素是从阵列中删除,但Var在 PersonViewController仍然指向peopleVar[0]因为我们使用一个动态评估标。在大多数情况下,这是不希望的行为。一个可以改善这种行为的例子是有一个first(where:)允许我们通过标识符选择元素的方法。

到目前为止,我们对我们的建设感到兴奋。也许它可能改变我们编写应用程序的方式。或者,它可能仍然太实验性:我们设法编译代码,但我们不确定该技术将在何处以及如何破解。

即使我们不在Var实践中使用,我们也结合了很多有趣的功能,这些功能可以很好地展示Swift的强大功能:泛型,关键路径,闭包,变量捕获,协议和扩展。

将来,尝试只部分应用方面可能会很酷Var。假设我们有一个数据库接口,它从数据库中读取一个人模型并将其作为a返回Var。我们可以使用它自动将结构的更改保存回数据库。似乎会有像这样的例子,其中Var技术可能是有用的。

转自:https://www.jianshu.com/p/20030b35e11c
收起阅读 »

被忽视了的NSDataDetector

keywordsNSDataDetector NSRegularExpression NSTextCheckingResult在日常开发场景中经常会遇到,在一段文本中检测一些半结构化的信息,比如:日期、地址段、链接、电话号码、交通信息、航班号、奇怪的格式化了的...
继续阅读 »

keywords

NSDataDetector NSRegularExpression NSTextCheckingResult

在日常开发场景中经常会遇到,在一段文本中检测一些半结构化的信息,比如:日期、地址段、链接、电话号码、交通信息、航班号、奇怪的格式化了的数字、甚至是相对的指示语等等。

如果这些需求在一个项目中出现,在不知道NSDataDetector这个类之前,可能要头皮发麻,之后开始自己编制一些正则,再加上国际化的需求,可能对编制好的正则需要大量的单元测试用例的介入。(估计好多小盆友要被这些东西整自闭了...)

幸运的是,对于 Cocoa 开发者来说,有一个简单的解决方案:NSDataDetector。

关于NSDataDetector

NSDataDetector 是 NSRegularExpression 的子类,而不只是一个 ICU 的模式匹配,它可以检测半结构化的信息:日期,地址,链接,电话号码和交通信息。

它以惊人的准确度完成这一切。NSDataDetector 可以匹配航班号,地址段,奇怪的格式化了的数字,甚至是相对的指示语,如 “下周六五点”。

你可以把它看成是一个有着复杂的令人难以置信的正则表达式匹配,可以从自然语言提取信息(尽管实际的实现细节可能比这个复杂得多)。

NSDataDetector 对象用一个需要检查的信息的位掩码类型来初始化,然后传入一个需要匹配的字符串。像 NSRegularExpression 一样,在一个字符串中找到的每个匹配是用 NSTextCheckingResult 来表示的,它有诸如字符范围和匹配类型的详细信息。然而,NSDataDetector 的特定类型也可以包含元数据,如地址或日期组件。


当然你也可以结合 YYKit 中的YYLabel进行文本的高亮展示,并且添加点击事件(以下是我项目中需要匹配文本中的手机号码):


当初始化 NSDataDetector 的时候,确保只指定你感兴趣的类型。每当增加一个需要检查的类型,随着而来的是不小的性能损失为代价。

数据检测器匹配类型

NSDataDetector 的各种 NSTextCheckingTypes 匹配,及其相关属性表:


其他的一些注意事项可以自行参考 Mattt 的文章NSDataDetector自行进行查阅。

好了,大家可以进行尝试一下,在你的应用程序里充分利用 NSDataDetector 解锁那些已经隐藏在众目睽睽下的结构化信息吧。

参考自: https://developer.apple.com/documentation/foundation/nsregularexpression

https://developer.apple.com/documentation/foundation/nstextcheckingresult

https://nshipster.com/nsdatadetector

转自:https://www.jianshu.com/p/91daa300da26

收起阅读 »

iOS完整文件拉流解析解码同步渲染音视频流

需求解析文件中的音视频流以解码同步并将视频渲染到屏幕上,音频通过扬声器输出.对于仅仅需要单纯播放一个视频文件可直接使用AVFoundation中上层播放器,这里是用最底层的方式实现,可获取原始音视频帧数据.实现原理本文主要分为三大块,解析模块使用FFmpeg ...
继续阅读 »

需求

解析文件中的音视频流以解码同步并将视频渲染到屏幕上,音频通过扬声器输出.对于仅仅需要单纯播放一个视频文件可直接使用AVFoundation中上层播放器,这里是用最底层的方式实现,可获取原始音视频帧数据.

实现原理

本文主要分为三大块,解析模块使用FFmpeg parse文件中的音视频流,解码模块使用FFmpeg或苹果原生解码器解码音视频,渲染模块使用OpenGL将视频流渲染到屏幕,使用Audio Queue Player将音频以扬声器形式输出.


本文以解码一个.MOV媒体文件为例, 该文件中包含H.264编码的视频数据, AAC编码的音频数据,首先要通过FFmpeg去parse文件中的音视频流信息,parse出来的结果保存在AVPacket结构体中,然后分别提取音视频帧数据,音频帧通过FFmpeg解码器或苹果原生框架中的Audio Converter进行解码,视频通过FFmpeg或苹果原生框架VideoToolbox中的解码器可将数据解码,解码后的音频数据格式为PCM,解码后的视频数据格式为YUV原始数据,根据时间戳对音视频数据进行同步,最后将PCM数据音频传给Audio Queue以实现音频的播放,将YUV视频原始数据封装为CMSampleBufferRef数据结构并传给OpenGL以将视频渲染到屏幕上,至此一个完整拉取文件视频流的操作完成.

注意: 通过网址拉取一个RTMP流进行解码播放的流程与拉取文件流基本相同, 只是需要通过socket接收音视频数据后再完成解码及后续流程.

简易流程

Parse

  • 创建AVFormatContext上下文对象: AVFormatContext *avformat_alloc_context(void);

  • 从文件中获取上下文对象并赋值给指定对象: int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options)

  • 读取文件中的流信息: int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

  • 获取文件中音视频流: m_formatContext->streams[audio/video index]e

  • 开始parse以获取文件中视频帧帧: int av_read_frame(AVFormatContext *s, AVPacket *pkt);

  • 如果是视频帧通过av_bitstream_filter_filter生成sps,pps等关键信息.

  • 读取到的AVPacket即包含文件中所有的音视频压缩数据.

解码

通过FFmpeg解码

  • 获取文件流的解码器上下文: formatContext->streams[a/v index]->codec;

  • 通过解码器上下文找到解码器: AVCodec *avcodec_find_decoder(enum AVCodecID id);

  • 打开解码器: int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

  • 将文件中音视频数据发送给解码器: int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

  • 循环接收解码后的音视频数据: int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

  • 如果是音频数据可能需要重新采样以便转成设备支持的格式播放.(借助SwrContext)

通过VideoToolbox解码视频

  • 将从FFmpeg中parse到的extra data中分离提取中NALU头关键信息sps,pps等

  • 通过上面提取的关键信息创建视频描述信息:CMVideoFormatDescriptionRef, CMVideoFormatDescriptionCreateFromH264ParameterSets / CMVideoFormatDescriptionCreateFromHEVCParameterSets

  • 创建解码器:VTDecompressionSessionCreate,并指定一系列相关参数.

  • 将压缩数据放入CMBlockBufferRef中:CMBlockBufferCreateWithMemoryBlock

  • 开始解码: VTDecompressionSessionDecodeFrame

  • 在回调中接收解码后的视频数据

通过AudioConvert解码音频

  • 通过原始数据与解码后数据格式的ASBD结构体创建解码器: AudioConverterNewSpecific

  • 指定解码器类型AudioClassDescription

  • 开始解码: AudioConverterFillComplexBuffer

  • 注意: 解码的前提是每次需要有1024个采样点才能完成一次解码操作.

同步

因为这里解码的是本地文件中的音视频, 也就是说只要本地文件中音视频的时间戳打的完全正确,我们解码出来的数据是可以直接播放以实现同步的效果.而我们要做的仅仅是保证音视频解码后同时渲染.

注意: 比如通过一个RTMP地址拉取的流因为存在网络原因可能造成某个时间段数据丢失,造成音视频不同步,所以需要有一套机制来纠正时间戳.大体机制即为视频追赶音频,后面会有文件专门介绍,这里不作过多说明.

渲染

通过上面的步骤获取到的视频原始数据即可通过封装好的OpenGL ES直接渲染到屏幕上,苹果原生框架中也有GLKViewController可以完成屏幕渲染.音频这里通过Audio Queue接收音频帧数据以完成播放.

文件结构


快速使用

使用FFmpeg解码

首先根据文件地址初始化FFmpeg以实现parse音视频流.然后利用FFmpeg中的解码器解码音视频数据,这里需要注意的是,我们将从读取到的第一个I帧开始作为起点,以实现音视频同步.解码后的音频要先装入传输队列中,因为audio queue player设计模式是不断从传输队列中取数据以实现播放.视频数据即可直接进行渲染.

- (void)startRenderAVByFFmpegWithFileName:(NSString *)fileName {
NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:@"MOV"];

XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path];

XDXFFmpegVideoDecoder *videoDecoder = [[XDXFFmpegVideoDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] videoStreamIndex:[parseHandler getVideoStreamIndex]];
videoDecoder.delegate = self;

XDXFFmpegAudioDecoder *audioDecoder = [[XDXFFmpegAudioDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] audioStreamIndex:[parseHandler getAudioStreamIndex]];
audioDecoder.delegate = self;

static BOOL isFindIDR = NO;

[parseHandler startParseGetAVPackeWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, AVPacket packet) {
if (isFinish) {
isFindIDR = NO;
[videoDecoder stopDecoder];
[audioDecoder stopDecoder];
dispatch_async(dispatch_get_main_queue(), ^{
self.startWorkBtn.hidden = NO;
});
return;
}

if (isVideoFrame) { // Video
if (packet.flags == 1 && isFindIDR == NO) {
isFindIDR = YES;
}

if (!isFindIDR) {
return;
}

[videoDecoder startDecodeVideoDataWithAVPacket:packet];
}else { // Audio
[audioDecoder startDecodeAudioDataWithAVPacket:packet];
}
}];
}

-(void)getDecodeVideoDataByFFmpeg:(CMSampleBufferRef)sampleBuffer {
CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
[self.previewView displayPixelBuffer:pix];
}

- (void)getDecodeAudioDataByFFmpeg:(void *)data size:(int)size pts:(int64_t)pts isFirstFrame:(BOOL)isFirstFrame {
// NSLog(@"demon test - %d",size);
// Put audio data from audio file into audio data queue
[self addBufferToWorkQueueWithAudioData:data size:size pts:pts];

// control rate
usleep(14.5*1000);
}

使用原生框架解码

首先根据文件地址初始化FFmpeg以实现parse音视频流.这里首先根据文件中实际的音频流数据构造ASBD结构体以初始化音频解码器,然后将解码后的音视频数据分别渲染即可.这里需要注意的是,如果要拉取的文件视频是H.265编码格式的,解码出来的数据的因为含有B帧所以时间戳是乱序的,我们需要借助一个链表对其排序,然后再将排序后的数据渲染到屏幕上.

- (void)startRenderAVByOriginWithFileName:(NSString *)fileName {
NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:@"MOV"];
XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path];

XDXVideoDecoder *videoDecoder = [[XDXVideoDecoder alloc] init];
videoDecoder.delegate = self;

// Origin file aac format
AudioStreamBasicDescription audioFormat = {
.mSampleRate = 48000,
.mFormatID = kAudioFormatMPEG4AAC,
.mChannelsPerFrame = 2,
.mFramesPerPacket = 1024,
};

XDXAduioDecoder *audioDecoder = [[XDXAduioDecoder alloc] initWithSourceFormat:audioFormat
destFormatID:kAudioFormatLinearPCM
sampleRate:48000
isUseHardwareDecode:YES];

[parseHandler startParseWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, struct XDXParseVideoDataInfo *videoInfo, struct XDXParseAudioDataInfo *audioInfo) {
if (isFinish) {
[videoDecoder stopDecoder];
[audioDecoder freeDecoder];

dispatch_async(dispatch_get_main_queue(), ^{
self.startWorkBtn.hidden = NO;
});
return;
}

if (isVideoFrame) {
[videoDecoder startDecodeVideoData:videoInfo];
}else {
[audioDecoder decodeAudioWithSourceBuffer:audioInfo->data
sourceBufferSize:audioInfo->dataSize
completeHandler:^(AudioBufferList * _Nonnull destBufferList, UInt32 outputPackets, AudioStreamPacketDescription * _Nonnull outputPacketDescriptions) {
// Put audio data from audio file into audio data queue
[self addBufferToWorkQueueWithAudioData:destBufferList->mBuffers->mData size:destBufferList->mBuffers->mDataByteSize pts:audioInfo->pts];

// control rate
usleep(16.8*1000);
}];
}
}];
}

- (void)getVideoDecodeDataCallback:(CMSampleBufferRef)sampleBuffer isFirstFrame:(BOOL)isFirstFrame {
if (self.hasBFrame) {
// Note : the first frame not need to sort.
if (isFirstFrame) {
CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
[self.previewView displayPixelBuffer:pix];
return;
}

[self.sortHandler addDataToLinkList:sampleBuffer];
}else {
CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
[self.previewView displayPixelBuffer:pix];
}
}

#pragma mark - Sort Callback
- (void)getSortedVideoNode:(CMSampleBufferRef)sampleBuffer {
int64_t pts = (int64_t)(CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) * 1000);
static int64_t lastpts = 0;
// NSLog(@"Test marigin - %lld",pts - lastpts);
lastpts = pts;

[self.previewView displayPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];
}

具体实现

本文中每一部分的具体实现均有详细介绍, 如需帮助请参考阅读前提中附带的链接地址.

注意

因为不同文件中压缩的音视频数据格式不同,这里仅仅兼容部分格式,可自定义进行扩展.

转自:https://www.jianshu.com/p/854a1bb47173

收起阅读 »

前端必须要了解的一些知识 (七)

创建对象又几种方法//第一种:字面量 var o1 = {name: 'o1'}; var o2 = new Object({name: 'o2'});//第二种 通过构造函数 var M = function (name) { this.name = na...
继续阅读 »
创建对象又几种方法
//第一种:字面量
var o1 = {name: 'o1'};
var o2 = new Object({name: 'o2'});
//第二种 通过构造函数
var M = function (name) { this.name = name; };
var o3 = new M('o3');
//第三种 Object.create
var p = {name: 'p'};
var o4 = Object.create(p);



o4.__proto__===p//true

原型 构造函数 实例 原型链



instanceof的原理


严谨来判断用constructor
instanceof 并不是很严谨


new运算符背后的原理
原理如下
测试如下

----
面向对象
类与实例
类的声明
1,构造函数
fn Animal(){
this.name = 'name'
}
2,ES6class的声明
class Animal2 {
//↓↓构造函数
constructor(){
this.name=name
}
}
实例化类
console.log(new Animal(),new Animal2())



类与继承
继承的本质就是原型链
继承有几种形式各种的优缺点不同点
1借助构造函数实现继承
fn parent1(){
this.name= 'parent1'
}
parent1.prototype.say(){
console.log('hello')
}
fn child1() {
parent1.call(this)
this.type='child1'
}


缺点 继承后parent1原型链上的东西 继承不say 并没有被继承 只能实现部分继承
如果方法都在构造函数上就能继承
2借助原型链实现继承
fn parent2(){
this.name= 'parent2'
this.play=[1,2,3]
}
fn child2() {
this.type='child2'
}
child2.prototype = new parent2()
var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);


缺点:如下 引用类型 不同的实例会全部变

3组合方式
前两种的方式的结合
fn parent3(){
this.name= 'parent3'
this.play=[1,2,3]
}
fn child3() {
parent3.call(this)
this.type='child2'
}
child3.prototype = new parent3()
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play,s4.play)


缺点:实例化的时候父构造函数执行了两次
//优化方式
fn parent4(){
this.name= 'parent4'
this.play=[1,2,3]
}
fn child4() {
parent4.call(this)
this.type='child4'


}
child4.prototype = parent4.prototype
var s5 = new Child4();
var s6 = new Child4();
console.log(s5 instance child4,s5 instance parent4)//true true
console.log(s5.constructor)//parent4
缺点:区分不了s5是child4还是parent4的实例
//优化方式
fn parent5(){
this.name= 'parent5'
this.play=[1,2,3]
}
fn child5() {
parent5.call(this)
this.type='child5'
}
child5.prototype = Object.creat(parent5.prototype)
//此时还是找不到 创建一下constructor可解决
child5.prototype.constructor = child5
var s7 = new Child5();
console.log(s7 instance child4,s7 instance parent4)//true true
console.log(s7.constructor)//child5


收起阅读 »

前端必须要了解的一些知识 (六)

DOM事件的级别DOM0element.onclick=function(){}DOM1未制定事件相关的标准DOM2element.add('click',fn,false)/ie . atenchDOM3el.add('keyup',fn,false)增加了...
继续阅读 »

DOM事件的级别

DOM0

element.onclick=function(){}


DOM1

未制定事件相关的标准


DOM2

element.add('click',fn,false)/ie . atench

DOM3

el.add('keyup',fn,false)增加了其他事件除了click


DOM事件的模型:捕获和冒泡



DOM事件流

三个j阶段

捕获 。 目标阶段 。 冒泡阶段



事件捕获的具体流程

window=>document=>html=>body=>.....目标


冒泡则相反


event对象的常见应用

event.preventDefalut . 阻止默认行为

event.stopPropagation . 阻止冒泡

event.stoplmmediatePropagation . 事件响应优先级

事件代理

event.currentTarget 当前绑定的事件的对象

event.target 返回触发事件的元素


currentTarget在事件流的捕获,冒泡阶段。只有当事件流处在冒泡阶段的时候,两个的指向才是一样的, 而当处于捕获和冒泡阶段的时候,

target指向被单击的对象

currentTarget指向当前事件活动的对象(一般为父级)。



自定义事件

let eve = new Event('eveName')/new CustomEvent可以加参数Obj

//注册

ev.addEventListener('eveName',fn)

//触发

ev.dispatchEvent(eve)


HTTP

http协议包括 :通用头域、请求消息、响应消息和主体信息。

特点

简单快速

每个资源得url是固定得

灵活


无连接

连接一次就会断掉

无状态

服务端不记录客户端连接得身份


报文得组成部分

请求报文

请求行

http方法

页面地址

http协议以及http版本

请求头

key value值告诉服务端我要哪些内容

空行

隔断

请求体

数据

响应报文

状态行

协议 状态吗

响应头

key value

空行

隔断

相应体

数据

http方法

get 获取资源

post 传输资源

put 更新资源

delete 删除资源

HEAD 获取报文首部

POST和GET区别(记住以下三个以上1,3,4,6,9)


HTTP状态码



持久链接

http1.1版本支持

管线化


  1. 管线化得特点和原理
  2. 请求和响应打包返回
  3. 持续连接完成后进行的且需要1.1版本的支持
  4. 管线化只有get和head可以进行 post有限制
  5. 管线化默认chrome和firefox默认不开启,初次连接的时候可能不支持,需要服务端的支持


收起阅读 »

前端必须要了解的一些知识 (五)

盒模型标准模型和IE模型标准模型和IE模型的区别1计算宽度和高度的不同ie中content的宽度包括padding和border这两个属性css是如何设置这两种模型的border-box 是·ie默认 content-boxjs如何获取盒模型的宽和高四种方法1...
继续阅读 »

盒模型
标准模型和IE模型


标准模型和IE模型的区别
1计算宽度和高度的不同
ie中content的宽度包括padding和border这两个属性

css是如何设置这两种模型的
border-box 是·ie
默认 content-box

js如何获取盒模型的宽和高
四种方法
1.dom.style.width/height 只能获取行内样式
2.dom.currentStyle.width/height只适合ie,兼容性问题
3.window.getComputedStyle(dom).width/height可以准确获取//兼容性最好
4.dom.getBoundingClientRect().width/height
getBoundingClientRect()可以返回一个包含几个参数的对象,left,top,width,height.等。。盒模型距离viewport 左上角的距离。



拔高
解释边距重叠
margin边距重叠取最大值


引出BFC和IFC
IFC在行内格式化上下文中,框(boxes)一个接一个地水平排列,起点是包含块的顶部。水平方向上的 marginborder和 padding在框之间得到保留。框在垂直方向上可以以不同的方式对齐:它们的顶部或底部对齐,或根据其中文字的基线对齐。包含那些框的长方形区域,会形成一行,叫做行框。

BFC的使用场景
 BFC:块级格式化上下文,它是指一个独立的块级渲染区域,只有Block-level BOX参与,该区域拥有一套渲染规则来约束块级盒子的布局,且与区域外部无关。
BFC的生成
既然上文提到BFC是一块渲染区域,那这块渲染区域到底在哪,它又是有多大,这些由生成BFC的元素决定,CSS2.1中规定满足下列CSS声明之一的元素便会生成BFC。
  • 根元素
  • float的值不为none
  • overflow的值不为visible
  • display的值为inline-block、table-cell、table-caption
  • position的值为absolute或fixed
  看到有道友文章中把display:table也认为可以生成BFC,其实这里的主要原因在于Table会默认生成一个匿名的table-cell,正是这个匿名的table-ccell生成了BFC
BFC的约束规
  1. 内部的Box会在垂直方向上一个接一个的放置
  2. 垂直方向上的距离由margin决定。(完整的说法是:属于同一个BFC的两个相邻Box的margin会发生重叠,与方向无关。)
  3. 每个元素的左外边距与包含块的左边界相接触(从左向右),即使浮动元素也是如此。(这说明BFC中子元素不会超出他的包含块,而position为absolute的元素可以超出他的包含块边界)
  4. BFC的区域不会与float的元素区域重叠
  5. 计算BFC的高度时,浮动子元素也参与计算
  6. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面元素,反之亦然

清除浮动的四种方式及其原理理解
 利用clear样式

 父元素结束标签之前插入清除浮动的块级元素

利用伪元素(clearfix)

利用overflow清除浮动
收起阅读 »

前端必须要了解的一些知识 (四)

基础方法1:浮动(延伸BFC)清除浮动后 浮动元素周边的元素处理的好的话 。 兼容性比较好2:绝对定位配合js的话 快捷缺点:脱离文档流3:flex比较完美的方案 。 解决以上的缺点4:表格布局兼容性特别好 ie缺点:。。5:网格布局 gride新的标准代码最...
继续阅读 »



基础方法

1:浮动(延伸BFC)

清除浮动后 浮动元素周边的元素处理的好的话 。 兼容性比较好

2:绝对定位

配合js的话 快捷

缺点:脱离文档流

3:flex

比较完美的方案 。 解决以上的缺点

4:表格布局

兼容性特别好 ie

缺点:。。

5:网格布局 gride

新的标准

代码最简化哈


拔高延续

1:如过去掉高度已知 。 哪个不在好用?

第三和第四能用

2:竖起来

3:两栏布局






收起阅读 »

前端必须要了解的一些知识 (三)

你在下单时,要给后台发请求,后台通过拿到的参数请求微信后台去生成订单并同时返给你一个路径mweb_url,这个路径就是用来调起微信应用发起支付操作的。(这里要说明的就是由于h5支付不能主动回调,所以需要个主动查询的操作,这时会需要你又一个确认界面来进行主动查询...
继续阅读 »

你在下单时,要给后台发请求,后台通过拿到的参数请求微信后台去生成订单并同时返给你一个路径mweb_url,这个路径就是用来调起微信应用发起支付操作的。(这里要说明的就是由于h5支付不能主动回调,所以需要个主动查询的操作,这时会需要你又一个确认界面来进行主动查询订单状态。这里是个坑一会儿再说),调起支付界面之后进行支付操作,期间你什么都不用管,因为这都是微信的事。你需要的就是在你付完钱之后查看你的钱买你要的东西到底有没有成功(你要是不加的话,谁知道成功没,估计顾客会打死你,付完钱就茫然了,不知道到底钱到哪去了→_→)


普通函数中的this:

1. this总是代表它的直接调用者, 例如 obj.func ,那么func中的this就是obj

2.在默认情况(非严格模式下,未使用 'use strict'),没找到直接调用者,则this指的是 window

3.在严格模式下,没有直接调用者的函数中的this是 undefined

4.使用call,apply,bind(ES5新增)绑定的,this指的是 绑定的对象

箭头函数中的this

默认指向在定义它时,它所处的对象,而不是执行时的对象, 定义它的时候,可能环境是window(即继承父级的this);

下面通过一些例子来研究一下 this的一些使用场景


call

call(null, arr[0], arr[1], arr[2], arr[3], arr[4])//89


1 dom有元素 页面不渲染

首页 scoped 不加 导致引入的tab分类无法加载图片

原因未知 此处感觉不太球对 瞎吉儿改的



2:懒加载问题



3.vue router

repalce push go



4css使图片置灰

-webkit-filter: grayscale(100%); -moz-filter: grayscale(100%); -ms-filter: grayscale(100%); -o-filter: grayscale(100%); filter: grayscale(100%); filter: gray;





收起阅读 »

前端必须要了解的一些知识 (二)

获取字符串长度 str.length分割字符串 str.split()拼接字符串 str1+str2 或 str1.concat(str2)替换字符串 str.replace(“玩游戏”,”好好学习”)提取子字符串 str.slice(start, end)或...
继续阅读 »

获取字符串长度 str.length

分割字符串 str.split()

拼接字符串 str1+str2 或 str1.concat(str2)

替换字符串 str.replace(“玩游戏”,”好好学习”)

提取子字符串 str.slice(start, end)或str.substring(start,end)或myStr.substr(start,length)

切换字符串大小写 str.toLowerCase()和str.toUpperCase()

比较字符串 str1.localeCompare(str2)

匹配字符串 str.match(pattern)或pattern.exec(str)或str.search(pattern)

根据位置查字符 str.charAt(index)

根据位置字符Unicode编码 str.charCodeAt(index)

根据字符查位置 str.indexOf(“you”)从左,myStr.lastIndexOf(“you”)从尾 或str.search(‘you’)

原始数据类型转字符串 String(数据) 或利用加号

字符串转原始数据类型 数字Number(”) // 0 布尔Boolean(”) // 0

自己构建属性和方法 String.prototype.属性或方法= function(参数){代码}

----------

箭头函数需要注意的地方

*当要求动态上下文的时候,就不能够使用箭头函数,也就是this的固定化。

1、在使用=>定义函数的时候,this的指向是定义时所在的对象,而不是使用时所在的对象;

2、不能够用作构造函数,这就是说,不能够使用new命令,否则就会抛出一个错误;

3、不能够使用arguments对象;

4、不能使用yield命令;


-------------------------

let和const

 *let是更完美的var,不是全局变量,具有块级函数作用域,大多数情况不会发生变量提升。const定义常量值,不能够重新赋值,如果值是一个对象,可以改变对象里边的属性值。

1、let声明的变量具有块级作用域

2、let声明的变量不能通过window.变量名进行访问

3、形如for(let x..)的循环是每次迭代都为x创建新的绑定


依次输出的问题

1:立即执行函数

2:闭包

3:let


--------------------------------

Set数据结构

*es6方法,Set本身是一个构造函数,它类似于数组,但是成员值都是唯一的。

--------------------------------

-------------------------------------

promise 案例较多 。 建议看代码

http://www.cnblogs.com/fengxiongZz/p/8191503.html

收起阅读 »

前端必须要了解的一些知识 (一)

常用apimoveTo(x0,y0):把当前画笔(ictx)移动到(x0,y0)这个位置。lineTo(x1,y1):从当前位置(x0,y0)处到(x1,y1)画一条直线。beginPath():开启一条路径或者重置当前路径。closePath():从当前点回...
继续阅读 »

常用api

moveTo(x0,y0):把当前画笔(ictx)移动到(x0,y0)这个位置。

lineTo(x1,y1):从当前位置(x0,y0)处到(x1,y1)画一条直线。

beginPath():开启一条路径或者重置当前路径。

closePath():从当前点回到路径起始点,也就是上一个beginPath的位置,回避和路径。

stroke():绘制。必须加了这个函数才会画图,所以这个一定要放在最后。


绘制一个圆形

/获取Canvas对象(画布)

var canvas = document.getElementById("myCanvas");

//简单地检测当前浏览器是否支持Canvas对象,以免在一些不支持html5的浏览器中提示语法错误

if(canvas.getContext){

//获取对应的CanvasRenderingContext2D对象(画笔)

var ctx = canvas.getContext("2d");

//开始一个新的绘制路径

ctx.beginPath();

//设置弧线的颜色为蓝色

ctx.strokeStyle = "blue";

var circle = {

x : 100, //圆心的x轴坐标值

y : 100, //圆心的y轴坐标值

r : 50 //圆的半径

};

//沿着坐标点(100,100)为圆心、半径为50px的圆的顺时针方向绘制弧线

ctx.arc(circle.x, circle.y, circle.r, 0, Math.PI / 2, false);

//按照指定的路径绘制弧线

ctx.stroke();

}

------

深拷贝

深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个

1:不仅可拷贝数组还能拷贝对象(但不能拷贝函数)

var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}] var new_arr = JSON.parse(JSON.stringify(arr)) console.log(new_arr);

2:下面是深拷贝一个通用方法,实现思路:拷贝的时候判断属性值的类型,如果是对象,继续递归调用深拷贝函数

var deepCopy = function(obj) {

// 只拷贝对象

if (typeof obj !== 'object') return;

// 根据obj的类型判断是新建一个数组还是一个对象

var newObj = obj instanceof Array ? [] : {};

for (var key in obj) {

// 遍历obj,并且判断是obj的属性才拷贝

if (obj.hasOwnProperty(key)) {

// 判断属性值的类型,如果是对象递归调用深拷贝

newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];

}

}

return newObj;

}





浅拷贝

数组的浅拷贝,可用concat、slice返回一个新数组的特性来实现拷贝for in

var arr = ['old', 1, true, null, undefined];

var new_arr = arr.concat(); // 或者var new_arr = arr.slice()也是一样的效果;

new_arr[0] = 'new';

console.log(arr); // ["old", 1, true, null, undefined]

console.log(new_arr); // ["new", 1, true, null, undefined]

-------------------------------------------

数组常用的方法

map此方法是将数组中的每个元素调用一个提供的函数,结果作为一个新的数组返回,并没有改变原来的数组

let arr = [1, 2, 3, 4, 5]

    let newArr = arr.map(x => x*2)

    //arr= [1, 2, 3, 4, 5]   原数组保持不变

    //newArr = [2, 4, 6, 8, 10] 返回新数组

forEach此方法是将数组中的每个元素执行传进提供的函数,没有返回值,直接改变原数组,注意和map方法区分

let arr = [1, 2, 3, 4, 5]

   num.forEach(x => x*2)

    // arr = [2, 4, 6, 8, 10]  数组改变,注意和map区分

filter()此方法是将所有元素进行判断,将满足条件的元素作为一个新的数组返回

let arr = [1, 2, 3, 4, 5]

     const isBigEnough => value => value >= 3

     let newArr = arr.filter(isBigEnough )

     //newNum = [3, 4, 5] 满足条件的元素返回为一个新的数组


reduce()此方法是所有元素调用返回函数,返回值为最后结果,传入的值必须是函数类型:

let arr = [1, 2, 3, 4, 5]

    const add = (a, b) => a + b

    let sum = arr.reduce(add)

    //sum = 15  相当于累加的效果

    与之相对应的还有一个 Array.reduceRight() 方法,区别是这个是从右向左操作的


push/pop

push:数组后面添加新元素,改变数组的长度

pop:数组删除最后一个元素 。 也改变长度


shift/unshift

shift:删除第一个元素 。 改变数组的长度

unshift:将一个或多个添加到数组开头 。 返回数组长度


isArray:返回bool

cancat:合并数组



toString:数组转字符串

join("--"):数组转字符串 。 间隔可以设置


splice(开始位置,删除个数,元素)万能方法 增删改

------------------------------------------------

判断是不是数组的方法

var arr = [1,2,3,1];

var arr2 = [{ abac : 1, abc : 2 }];

function isArrayFn(value){

if (typeof Array.isArray === "function") {

//判断是否支持isArray ie8之前不支持

return Array.isArray(value);

}else{

return Object.prototype.toString.call(value) === "[object Array]";

}

}

alert(isArrayFn(arr));// true

alert(isArrayFn(arr2));// true


收起阅读 »

SDWebImage加载多张高分辨图片crash

项目中有一个控制器里的图片服务器那边没有进行压缩 所以使用SDWebImage显示在collectionView/tableView的时候有时会crash(及时没有反复进几次就会crash了)。网上查了很多资料,大致总结有一下几种方法:1、每次加载高清图片时清...
继续阅读 »

项目中有一个控制器里的图片服务器那边没有进行压缩 所以使用SDWebImage显示在collectionView/tableView的时候有时会crash(及时没有反复进几次就会crash了)。网上查了很多资料,大致总结有一下几种方法:

1、每次加载高清图片时清空memcache

[[SDImageCache sharedImageCache] setValue:nil forKey:@"memCache"];

但是这种方法会产生一个效果:当滑动tableView的时候 cell消失在屏幕中再滑回来图片会从新加载。

2.取消解压缩

[SDImageCache sharedImageCache].shouldDecompressImages = NO
[SDWebImageDownloader sharedDownloader].shouldDecompressImages = NO;

之所以产生crash的原因,是因为在SDWebImage里的这个方法decodedImageWithImage在加载高清图片是占用了大量内存。所以上面的两行代码就禁止调用了这个方法,那么问题来了,那这个方法存在的意义又是什么呢?

因为我们对图片的展示大部分是在tableviews/collectionview里 其实decodedImageWithImage方法是对图片进行解压缩并且缓存起来,以提高流畅度。但是加载高分辨率的图片就会起到适得其反的效果。所以在加载高分辨率图片的地方调用以上两个方法,其他地方仍然保持为YES就可以了。如果再限制图片内存缓存最高限制就更安全了


3.对图片进行等比例压缩(需修改源码)


这里面对图片的处理是直接按照原大小进行的,如果分辨率很大这里导致占用了大量内存。所以我们需要在这里对图片做一次等比的压缩。

在UIImage+MultiFormat这个类里面添加如下压缩方法

+(UIImage *)compressImageWith:(UIImage *)image{
float imageWidth = image.size.width;
float imageHeight = image.size.height;
float width = 640;
float height = image.size.height/(image.size.width/width);
float widthScale = imageWidth /width;
float heightScale = imageHeight /height;
// 创建一个bitmap的context
// 并把它设置成为当前正在使用的context
UIGraphicsBeginImageContext(CGSizeMake(width, height));
if (widthScale > heightScale) {
[image drawInRect:CGRectMake(0, 0, imageWidth /heightScale , height)];
}
else {
image drawInRect:CGRectMake(0, 0, width , imageHeight /widthScale)];
}
// 从当前context中创建一个改变大小后的图片
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
// 使当前的context出堆栈
UIGraphicsEndImageContext();
return newImage;
}

在上图箭头位置这样调用

image = [[UIImage alloc] initWithData:data];
if (data.length/1024 > 128) {
image = [self compressImageWith:image];
}

到了这里还需要进行最后一步。就是在SDWebImageDownloaderOperation的connectionDidFinishLoading方法里面的:

UIImage *image = [UIImage sd_imageWithData:self.imageData];
//将等比压缩过的image在赋在转成data赋给self.imageData
NSData *data = UIImageJPEGRepresentation(image, 1);
self.imageData = [NSMutableData dataWithData:data];

但是我在尝试这个方法的时候只这样操作的话还是会crash,所以还是要配合下面这个方法使用,所以那个郁闷啊!!!!大家也可以尝试一下

[[SDImageCache sharedImageCache] setValue:nil forKey:@"memCache"];
最终我是选择了第二种。欢迎补充!

链接:https://www.jianshu.com/p/7013919c03eb

收起阅读 »

集成环信IM后,iOS没有通知栏提示?

通知栏分本地通知和apns推送通知 情况1:App进入后台没有超过150秒左右,接收消息没有通知栏提示,这种情况要配置本地通知,先注册本地通知(系统方法),然后再看下文档介绍: 如何实现本地通知 情况2:不是本地通知,App退到后台超过150秒左右,接收消息没...
继续阅读 »


通知栏分本地通知apns推送通知


情况1:App进入后台没有超过150秒左右,接收消息没有通知栏提示,这种情况要配置本地通知,先注册本地通知(系统方法),然后再看下文档介绍: 如何实现本地通知


情况2:不是本地通知,App退到后台超过150秒左右,接收消息没有通知栏提示,这个情况属于apns推送。


如果没有配置apns推送,先按照文档配置: APNS推送配置
如果配置过了apns推送,那么在初始化SDK方法之后调用下这个方法试下:

- (void)applicationDidEnterBackground:(UIApplication *)application
{
[[EMClient sharedClient] applicationDidEnterBackground:application];
}

- (void)applicationWillEnterForeground:(UIApplication *)application
{
[[EMClient sharedClient] applicationWillEnterForeground:application];
}




收起阅读 »

LinkedList源码解析(手把手带你熟悉链表)

前言链表是常见的数据结构之一,但是很多同学只听说过链表,并不知道什么是链表,所以本文将会带领各位同学手写一个LinkedList,源码跟官方会有点不一样,不过思路是大概相同的,最后再带领大家读官方源码为了降低源码难度简化泛型代码,手写的LinkedList只能...
继续阅读 »
  • 前言

链表是常见的数据结构之一,但是很多同学只听说过链表,并不知道什么是链表,所以本文将会带领各位同学手写一个LinkedList,源码跟官方会有点不一样,不过思路是大概相同的,最后再带领大家读官方源码

为了降低源码难度简化泛型代码,手写的LinkedList只能添加String类型数据

  • 什么是链表?

可以理解为,把一些数据按照顺序排好,手拉手 每一个数据就是一个节点,所有节点连在一起,就组成了链表 在这里插入图片描述

  • LinkedList的节点定义

LinkedList是双链表,所以有左节点和右节点,我们先定义一个实体类,这个实体类就可以理解为节点

    /**
* 节点实体类
*/

private static class NodeBean {
NodeBean leftNode; //左节点
String value; //节点的值
NodeBean rightNode; //右节点
}
复制代码
图片名称
  • 节点怎么连接?

在这里插入图片描述

看完上面的图片,应该大概知道两个节点的连接方法了,我们只需要:

节点1.右节点 = 节点2
节点2.左节点 = 节点1
复制代码

我们先实现链表的add方法,add方法其实就是将两个节点连接:

    /**
* 添加值
*/

public void add(String value) {
//先获取尾节点(上一个节点)
NodeBean lastNode = this.lastNode;
//创建一个新节点
NodeBean newNode = new NodeBean();
//为节点赋值
newNode.value = value;
//左节点为最后一个节点(尾节点)
newNode.leftNode = lastNode;
//由于是添加节点,所以右节点为null,可以不写
newNode.rightNode = null;
//将成员变量的最后一个节点改为当前新节点
this.lastNode = newNode;
//判断头节点是否为空
if (this.firstNode == null) {
//如果为空说明当前是第一个节点,需要把头结点也设为当前节点
this.firstNode = newNode;
}else{
//如果不为空,需要把前一个节点的右节点指向当前节点
//两个节点相连接的条件是:
// 1. 前一个节点的右节点指向当前节点
// 2. 当前节点的左节点指向上一个节点
lastNode.rightNode = newNode;
}
//链表长度+1
size++;
}
复制代码
  • 节点怎么断开?

两个节点断开只需要将自己的上一个节点的右节点指向自己的下一个节点左节点,同时自己的下一个节点的左节点,指向上一个节点的右节点

注意看下图箭头方向,这样节点2就可以直接断开,节点1和节点3直接连接,这里也可以很明显的看出,链表增删很快,只需要断开前后节点就可以

在这里插入图片描述

先看一下断开节点的大概代码思路:

节点2.左节点 = null
节点2.右节点 = null
节点1.右节点 = 节点3
节点3.左节点 = 节点1

这样就可以断开当前节点,并且将链表重新连接起来
复制代码

现在我们来实现remove(index)方法,根据索引删除指定节点:

    /**
* 删除值
*/

public void remove(int index){
这里代码通过索引查找节点,为了简化代码,请忽略这里的代码
通过索引查找节点,下面会写到
...
***indexNode就是我们通过索引拿到的节点***

indexNode = 通过索引查找节点(index)

//拿到该节点的左节点、右节点以及值
NodeBean leftNode = indexNode.leftNode;
NodeBean rightNode = indexNode.rightNode;

//判断左节点是否为空,如果为空说明当前节点为(头结点)第一个节点
if (leftNode == null) {
this.firstNode = indexNode;
}else{
//左节点不为空,需要断开自己的左节点
indexNode.leftNode = null;
//将上一个节点的右节点连接到下一个节点
leftNode.rightNode = rightNode;
}

//判断右节点是否为空,如果为空说明当前为(尾节点)最后一个节点
if (rightNode == null) {
this.lastNode = indexNode;
}else{
//右节点不为空,需要断开自己的右节点
indexNode.rightNode = null;
//将下一个节点的左节点连接到上一个节点
rightNode.leftNode = leftNode;
}

//当前节点值置空
indexNode.value = null;
size--;
}
复制代码
  • 通过索引查找节点

1. 先拿到头节点
2. 拿到当前要查找的索引index
3. 循环index的次数
4. 每循环一次,就从头结点开始往后移动一个节点
复制代码

现在我们来实现以下get(index)方法,通过索引获取置顶节点:

    /**
* 通过索引获取节点的值
*/

public String get(int index){
//由于链表没有索引,所以只能一个一个遍历查找
//先拿到链表的第一个节点(头节点)
NodeBean firstNode = this.firstNode;
for (int i = 0; i < index; i++) {
//每次循环就从头结点往后挪动一个节点
firstNode = firstNode.rightNode;
}
//为了简化代码便于理解,这里不考虑tempNode为null的情况
return firstNode.value;
}
复制代码
  • 总结

  1. 链表的每个节点之间都有连接,如果新增节点只需要直接插入就行,所以==链表新增快==
  2. 链表断开节点只需要将自己的前后节点重新连接就可以,所以链表==删除快==
  3. 链表没有索引,查找需要循环整个链表,所以==查询慢==
  • MyLinkedList完整代码:

public class MyLinkedList {
private int size; //当前链表的长度
private NodeBean firstNode; //头节点
private NodeBean lastNode; //尾节点

/**
* 添加值
*/

public void add(String value) {
//先获取尾节点
NodeBean lastNode = this.lastNode;
//创建一个新节点
NodeBean newNode = new NodeBean();
//为节点赋值
newNode.value = value;
//左节点为最后一个节点(尾节点)
newNode.leftNode = lastNode;
//由于是添加节点,所以右节点为null,可以不写
newNode.rightNode = null;
//将成员变量的最后一个节点改为当前新节点
this.lastNode = newNode;
//判断头节点是否为空
if (this.firstNode == null) {
//如果为空说明当前是第一个节点,需要把头结点也设为当前节点
this.firstNode = newNode;
}else{
//如果不为空,需要把前一个节点的右节点指向当前节点
//两个节点相连接的条件是:
// 1. 前一个节点的右节点指向当前节点
// 2. 当前节点的左节点指向上一个节点
lastNode.rightNode = newNode;
}
//链表长度+1
size++;
}

/**
* 删除值
*/

public void remove(int index){
//先找到当前索引对应的节点
//由于链表没有索引,所以只能一个一个遍历查找
//先拿到链表的第一个节点(头节点)
NodeBean indexNode = this.firstNode;
for (int i = 0; i < index; i++) {
//每次循环就从头结点往后挪动一个节点
indexNode = indexNode.rightNode;
}
//拿到该节点的左节点、右节点以及值
NodeBean leftNode = indexNode.leftNode;
NodeBean rightNode = indexNode.rightNode;

//判断左节点是否为空,如果为空说明当前节点为(头结点)第一个节点
if (leftNode == null) {
this.firstNode = indexNode;
}else{
//左节点不为空,需要断开自己的左节点
indexNode.leftNode = null;
//将上一个节点的右节点连接到下一个节点
leftNode.rightNode = rightNode;
}

//判断右节点是否为空,如果为空说明当前为(尾节点)最后一个节点
if (rightNode == null) {
this.lastNode = indexNode;
}else{
//右节点不为空,需要断开自己的右节点
indexNode.rightNode = null;
//将下一个节点的左节点连接到上一个节点
rightNode.leftNode = leftNode;
}

//当前节点值置空
indexNode.value = null;
size--;
}

/**
* 通过索引获取节点的值
*/

public String get(int index){
//由于链表没有索引,所以只能一个一个遍历查找
//先拿到链表的第一个节点(头节点)
NodeBean firstNode = this.firstNode;
for (int i = 0; i < index; i++) {
//每次循环就从头结点往后挪动一个节点
firstNode = firstNode.rightNode;
}
//为了简化代码便于理解,这里不考虑tempNode为null的情况
return firstNode.value;
}

/**
* 获取链表长度
*/

public int getSize(){
return this.size;
}

/**
* 节点实体类
*/

private static class NodeBean {
NodeBean leftNode; //左节点
String value; //节点的值
NodeBean rightNode; //右节点
}
}

复制代码
  • 重点

  • 如果你看完上面的增删查方法, 可以完全看懂了,就可以继续往下看了
  • 如果你没看懂,请复制上面的完整代码到编辑器,自己断点研究一下

==如果上面的代码理解了,恭喜你! 现在你应该已经可以看懂官方源码了==

  • 官方代码解析

  • 节点实体类

    private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
复制代码

代码对比 在这里插入图片描述

  • 插入节点

    void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
复制代码

代码对比 在这里插入图片描述

  • 断开节点

    E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;

if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}

if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}

x.item = null;
size--;
modCount++;
return element;
}
复制代码

代码对比 在这里插入图片描述

  • 最后总结

  • 如果现在你可以看懂官方的这三个方法了,那可以尝试自己去读剩下的部分方法,比如==unlinkFirst()和linkFirst()==
  • 读源码并不可怕,只要理解源码的思路,顿时豁然开朗
收起阅读 »

Android 动态化多语言框架,支持语言包的动态下发、升级、删除,一处安装,到处使用

MLang 动态化多语言框架MLang 是 MultiLanguage 的简写,是一款动态化的多语言框架。设计优雅 语言包存储格式为 xml 格式,和 res 下的 strings.xml 一致 零依赖,完全使用系统 api 和系统的 xml 解析器 不持有 ...
继续阅读 »


MLang 动态化多语言框架

MLang 是 MultiLanguage 的简写,是一款动态化的多语言框架。

设计优雅

  •  语言包存储格式为 xml 格式,和 res 下的 strings.xml 一致
  •  零依赖,完全使用系统 api 和系统的 xml 解析器
  •  不持有 context,无内存泄漏
  •  静态方法 + 单例模式,一处安装,到处使用

动态化语言包

  •  动态下发语言包
  •  语言包的增加、升级、删除
  •  语言包内部任意字符串的增加、升级、删除
  •  自定义语言包的存储路径

完全兼容

  •  跟随系统语言
  •  时间格式跟随系统的 24 小时制
  •  处理各种语言的时区、时间格式化问题
  •  处理各种语言的复数格式化问题
  •  处理各种语言的阅读顺序问题(从左到右、从右到左)

1. 使用

使用字符串

// 本地和云端都存在的字符串
MyLang.getString("local_string", R.string.local_string)

// 云端存在 remote_string_only
// 但本地没有 R.string.remote_string_only,用 R.string.fallback_string 代替
MyLang.getString("remote_string_only", R.string.fallback_string)

使用语言包

//应用一种语言(这里自动处理了语言包的升级、语言包内部字符串的升级)
MyLang.getInstance().applyLanguage(Context, LocaleInfo, force=true, init=false);

//删除一种语言
MyLang.getInstance().deleteLanguage(Context, LocaleInfo);

LocaleInfo 可以在以下地方找到

//1. 所有云端的语言包
MyLang.getInstance().remoteLanguages

//2. 所有下载到本地、可用的语言包
MyLang.getInstance().languages

//3. 所有非官方的语言包
MyLang.getInstance().unofficialLanguages

//4. 除内置支持的语言外,另外安装的云端的语言包
MyLang.getInstance().otherLanguages

2. 安装

2.1. 引入

//build.gradle
allprojects {
repositories {
google()
jcenter()
maven { url "https://github.com/LinXueyuanStdio/MLang/raw/main/dist/" }
}
}

//app/build.gradle
implementation 'com.timecat.component:MLang:2.0.2'

2.2. 在 Application 中初始化,并监听系统语言的更改(如果跟随系统语言的话):

public class MyApplication extends Application {
@SuppressLint("StaticFieldLeak")
public static volatile Context applicationContext;
public static volatile Handler applicationHandler;

@Override
public void onCreate() {
super.onCreate();
applicationContext = this;
applicationHandler = new Handler(applicationContext.getMainLooper());
MyLang.init(applicationContext);
}

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
MyLang.onConfigurationChanged(newConfig);
}
}

其中建议自己新建一个静态类 MyLang 来代理 MLang。 这样有两个好处:

  1. 隔绝 MLang 的 api 变化,提高兼容性和稳定性。
  2. 使用更简洁。MLang 不持有 context,但每次获取字符串为空时,需要 context 来兜底,获取本地的字符串。在自己的 MyLang 默认提供 application Context,可以不用到处提供 context,更简洁。
public class MyLang {
private static File filesDir;
private static LangAction action;
public static void init(@NonNull Context applicationContext) {
filesDir = applicationContext.getCacheDir();
action = new MyLangAction();
getInstance();
}
public static MLang getInstance() {
return MLang.getInstance(MyApplication.applicationContext, filesDir, action);
}
public static void onConfigurationChanged(@NonNull Configuration newConfig) {
getInstance().onDeviceConfigurationChange(getContext(), newConfig);
}
}

3. 设计

3.1. 单例模式接收 3 个参数,context,fileDir,action

  1. context:MLang 内部不持有该 context。该 context 用于注册时区广播(根据时区来格式化字符串中的时间)、 判断系统当前时间是否 24 小时制等等。
  2. filesDir:持久化语言包文件的存储地址。语言包文件是 xml 格式,和 res 下的 strings.xml 一样。
  3. action:action 包含了应用语言包、切换语言等等需要的所有回调,即 LangAction 接口。
MLang.getInstance(context, filesDir, action);

3.2. LangAction 接口定义了 2 个东西

  1. 当前语言的设置存储。 MLang 根据语言 id (string) 来识别当前语言。语言 id 需要持久化。 所以设计了下面两个方法,可以自行决定持久化的方式(SharedPreferences、MMKV、SQLite等等)。
    void saveLanguageKeyInLocal(String language);
    @Nullable String loadLanguageKeyInLocal();
  2. 必要的网络接口。
    void langpack_getDifference(String lang_pack, String lang_code, int from_version, @NonNull final LangAction.GetDifferenceCallback callback)
    void langpack_getLanguages(@NonNull final LangAction.GetLanguagesCallback callback)
    void langpack_getLangPack(String lang_code, @NonNull final LangAction.GetLangPackCallback callback)

LangAction 的注释如下:

public interface LangAction {
/**
* SharedPreferences preferences = Utilities.getGlobalMainSettings();
* SharedPreferences.Editor editor = preferences.edit();
* editor.putString("language", language);
* editor.commit();
* @param language localeInfo.getKey() 语言 id
*/
void saveLanguageKeyInLocal(String language);

/**
* SharedPreferences preferences = Utilities.getGlobalMainSettings();
* String lang = preferences.getString("language", null);
* @return @Nullable lang 语言 id
*/
String loadLanguageKeyInLocal();

/**
* 在其他线程网络请求,在主线程或UI线程调用callback
* 这里设计成这样,是因为这个方法里支持异步执行
* 您需要在合适的时机手动调用 callback,且只能调用一次
* @param lang_pack 语言包名字
* @param lang_code 语言包版本名称
* @param from_version 语言包版本号
* @param callback @NonNull 在主线程或UI线程调用
*/
void langpack_getDifference(String lang_pack, String lang_code, int from_version, GetDifferenceCallback callback);

/**
* 在其他线程网络请求,在主线程或UI线程调用callback
* 这里设计成这样,是因为这个方法里支持异步执行
* 您需要在合适的时机手动调用 callback,且只能调用一次
* @param callback @NonNull 在主线程或UI线程调用
*/
void langpack_getLanguages(GetLanguagesCallback callback);

/**
* 在其他线程网络请求,在主线程或UI线程调用callback
* 这里设计成这样,是因为这个方法里支持异步执行
* 您需要在合适的时机手动调用 callback,且只能调用一次
* @param lang_code 语言包版本名称
* @param callback @NonNull 在主线程或UI线程调用
*/
void langpack_getLangPack(String lang_code, GetLangPackCallback callback);

interface GetLanguagesCallback {
/**
* 必须在UI线程或者主线程调用
* 所有可用的语言包
* @param languageList 语言包列表
*/
void onLoad(List<LangPackLanguage> languageList);
}

interface GetDifferenceCallback {
/**
* 必须在UI线程或者主线程调用
* 如果服务端没有实现增量分发的功能,可以用完整的语言包代替
* @param languageList 增量的语言包
*/
void onLoad(LangPackDifference languageList);
}

interface GetLangPackCallback {
/**
* 必须在UI线程或者主线程调用
* @param languageList 完整的语言包
*/
void onLoad(LangPackDifference languageList);
}

}

实现LangAction的一个示例如下:

public class MyLangAction implements LangAction {
@Override
public static void saveLanguageKeyInLocal(String language) {
SharedPreferences preferences = getContext().getSharedPreferences("language_locale", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("language", language);
editor.apply();
}

@Override
@Nullable
public static String loadLanguageKeyInLocal() {
SharedPreferences preferences = getContext().getSharedPreferences("language_locale", Context.MODE_PRIVATE);
return preferences.getString("language", null);
}
@Override
public void langpack_getDifference(String lang_pack, String lang_code, int from_version, @NonNull final LangAction.GetDifferenceCallback callback) {
Server.request_langpack_getDifference(lang_pack, lang_code, from_version, new Server.GetDifferenceCallback() {
@Override
public void onNext(final LangPackDifference difference) {
callback.onLoad(difference);
}
});
}

@Override
public void langpack_getLanguages(@NonNull final LangAction.GetLanguagesCallback callback) {
Server.request_langpack_getLanguages(new Server.GetLanguagesCallback() {
@Override
public void onNext(final List<LangPackLanguage> languageList) {
callback.onLoad(languageList);
}
});
}

@Override
public void langpack_getLangPack(String lang_code, @NonNull final LangAction.GetLangPackCallback callback) {
Server.request_langpack_getLangPack(lang_code, new Server.GetLangPackCallback() {
@Override
public void onNext(final LangPackDifference difference) {
callback.onLoad(difference);
}
});
}
}

3.3. 服务器语言包的结构

模拟的服务器数据

语言包实体

  • LangPackLanguage(name, version, ...)

语言包的数据

  • LangPackDifference(name, version, List, ...)
  • LangPackString(key: String, value: String)
public class Server {
public static LangPackLanguage chineseLanguage() {
LangPackLanguage langPackLanguage = new LangPackLanguage();
langPackLanguage.name = "chinese";
langPackLanguage.native_name = "简体中文";
langPackLanguage.lang_code = "zh";
langPackLanguage.base_lang_code = "zh";
return langPackLanguage;
}
public static LangPackDifference chinesePackDifference() {
LangPackDifference difference = new LangPackDifference();
difference.lang_code = "zh";
difference.from_version = 0;
difference.version = 1;
difference.strings = chineseStrings();
return difference;
}
public static ArrayList<LangPackString> chineseStrings() {
ArrayList<LangPackString> list = new ArrayList<>();
list.add(new LangPackString("LanguageName", "中文简体"));
list.add(new LangPackString("LanguageNameInEnglish", "Chinese"));
list.add(new LangPackString("local_string", "中文的云端字符串"));
list.add(new LangPackString("remote_string_only", "本地缺失,云端存在的字符串"));
return list;
}
}

4. 进阶配置

MLang.isRTL = false; //是否从右到左阅读(默认 false)
MLang.is24HourFormat = false; //是否 24 小时制(默认 false) 

MLang.USE_CLOUD_STRINGS = true; //是否使用云端字符串(默认 true)

代码下载:MLang-main.zip


收起阅读 »