xcode6打包ipa重签失败——无法安装

游戏开发atool 发表了文章 • 0 个评论 • 875 次浏览 • 2016-04-22 14:16 • 来自相关话题

升级xocde6之后,使用achieve打包, 然后zip成ipa包之后,送去重签,发现安装问题。

使用itools安装提示证书问题;

使用itunes安装,直接是灰色图标

原因很简单,改两项xcode配置即可:

1.Project - Target - Building Setting - Code Signing中的Code Signing Resource Rules Path项,这一项在Xcode5中是不存在的,升级之后,这一项默认为空,设置为:$(SDKROOT)/ResourceRules.plist 即可,Xcode会自动换成相应的路径;

2.targets中的名字都不能带有空格,很多项目默认生成的target name是: project空格iOS 和 project空格Mac,双击修改即可。

具体看如下的图片:(博客转移,图片丢失)
  查看全部
升级xocde6之后,使用achieve打包, 然后zip成ipa包之后,送去重签,发现安装问题。

使用itools安装提示证书问题;

使用itunes安装,直接是灰色图标

原因很简单,改两项xcode配置即可:

1.Project - Target - Building Setting - Code Signing中的Code Signing Resource Rules Path项,这一项在Xcode5中是不存在的,升级之后,这一项默认为空,设置为:$(SDKROOT)/ResourceRules.plist 即可,Xcode会自动换成相应的路径;

2.targets中的名字都不能带有空格,很多项目默认生成的target name是: project空格iOS 和 project空格Mac,双击修改即可。

具体看如下的图片:(博客转移,图片丢失)
 

Cocos2d-x Lua/Javascript脚本代码加密实现

游戏开发atool 发表了文章 • 0 个评论 • 831 次浏览 • 2016-04-22 14:15 • 来自相关话题

在游戏开发中,脚本作为一种资源文件,就像图片视频一样,被引擎所引用,使用脚本做游戏的好处就在于可以在线patch更新,特别对于苹果App Store审核期很长的情况。

如果不对脚本进行加密,不怀好意的人松松解压出脚本文件,给你瞬间复制一个游戏出来。

1.异或加密解密

最简单的一种加密方式,虽然简单,但是也比较实用。但是防破解方面确实一般,如果你有其他严格的仿破解需求,可以将这部分加密算法换成你自己的复杂算法,不过保证解密效率。下面是采用C++简单实现的对文件进行加密之后保存到原文件中(注意对原始未加密文件进行备份)
#include "stdafx.h"
#include<iostream>
#include<ctime>
#include<fstream>
using namespace std;
void Makecode(char *pstr,int *pkey);
void Cutecode(char *pstr,int *pkey);
void encode_file(char *f);
int _tmain(int argc, _TCHAR* argv[])
{
encode_file("e:/src/ResultScene.lua");
/*
encode_file("e:/src/ReadyScene.lua");
encode_file("e:/src/GameScene.lua");
encode_file("e:/src/PutHeadScene.lua");
encode_file("e:/src/TutorialsScene.lua");
encode_file("e:/src/WordsCategoryScene.lua");
encode_file("e:/src/common/DictHelper.lua");
encode_file("e:/src/common/LJ.lua");
encode_file("e:/src/common/DQueue.lua");
encode_file("e:/src/common/UIHelper.lua");
*/
int c;
cin>>c;
return 0;
}
void encode_file(char *f)
{
FILE *fp = NULL;
fopen_s(&fp, f,"rb");
fseek(fp,0,SEEK_END); //定位到文件末
int nFileLen = ftell(fp); //文件长度
cout << "file len = " << nFileLen << endl;
fseek(fp, 0, SEEK_SET);
char *fileContent = NULL;
fileContent = (char *) malloc ((nFileLen + 1) * sizeof(char));//增加一位
memset(fileContent, 0, nFileLen + 1);
fileContent[nFileLen] = '';//最后一位置为结束位
fread_s(fileContent,nFileLen, 1, nFileLen, fp);
//fread(buf,nFileLen, 1, fp);
//cout<<"解密前:"<<fileContent<<endl;
fclose(fp);
cout<<"文件:"<<f<<endl;
cout<<"解密前文件大小:"<<strlen(fileContent)<<endl;
int key[]={1, 2, 6, 1, 2, 6};//加密字符
char *p=fileContent;
cout<<"====="<<endl;
Makecode(fileContent,key);//加密
//cout<<"加密后:"<<p<<endl;
cout<<"加密后文件大小:"<<strlen(fileContent)<<endl;
FILE *stream = NULL;
fopen_s(&stream, f,"wb");
if (stream == NULL) /* open file TEST.$$$ */
{
fprintf(stderr, "Cannot open output file.
");
}
else {
fwrite(p, nFileLen, 1, stream); /* 写的struct文件*/
fclose(stream); /*关闭文件*/
}
cout<<"====="<<endl;
Cutecode(fileContent,key);//解密
//cout<<"解密后:"<<fileContent<<endl;
}
//单个字符异或运算
char MakecodeChar(char c,int key){
return c=c^key;
}
//单个字符解密
char CutcodeChar(char c,int key){
return c^key;
}
//加密
void Makecode(char *pstr,int *pkey){
int len=strlen(pstr);//获取长度
for(int i=0;i<len;i++)
*(pstr+i)=MakecodeChar(*(pstr+i),pkey[i%5]);
}
//解密
void Cutecode(char *pstr,int *pkey){
int len=strlen(pstr);
for(int i=0;i<len;i++)
*(pstr+i)=CutcodeChar(*(pstr+i),pkey[i%5]);
}2.修改Cocos2d-x引擎中加载lua脚本文件(或者js文件)的入口,在加载的时候对其进行解密。可能不同版本引擎有不同的入口文件,在Cocos2d-x3.0中,对应的是文件Cocos2dxLuaLoader.cpp文件中的int cocos2dx_lua_loader(lua_State *L)方法,对其进行修改成如下:
#include "Cocos2dxLuaLoader.h"
#include <string>
#include <algorithm>
#include<iostream>
using namespace cocos2d;
extern "C"
{
//单个字符异或运算
char MakecodeChar(char c,int key){
return c=c^key;
}
//单个字符解密
char CutcodeChar(char c,int key){
return c^key;
}
//加密
void Makecode(char *pstr,int *pkey){
int len=strlen(pstr);//获取长度
for(int i=0;i<len;i++)
*(pstr+i)=MakecodeChar(*(pstr+i),pkey[i%5]);
}
//解密
void Cutecode(char *pstr,int *pkey){
int len=strlen(pstr);
for(int i=0;i<len;i++)
*(pstr+i)=CutcodeChar(*(pstr+i),pkey[i%5]);
}
int cocos2dx_lua_loader(lua_State *L)
{
std::string filename(luaL_checkstring(L, 1));
size_t pos = filename.rfind(".lua");
if (pos != std::string::npos)
{
filename = filename.substr(0, pos);
}

pos = filename.find_first_of(".");
while (pos != std::string::npos)
{
filename.replace(pos, 1, "/");
pos = filename.find_first_of(".");
}
filename.append(".lua");

Data data = FileUtils::getInstance()->getDataFromFile(filename);

if (!data.isNull())
{

//====code decode start==================================
log("===encode filename:%s===", filename.c_str());
//如果filename == 'main.lua',则解密
char *fileContent = (char*)data.getBytes();
int key[]={1, 2, 6, 1, 2, 6};//加密字符
char *fileContentDecoded = NULL;
if (strcmp(filename.c_str(),"ReadyScene.lua")==0 ||
strcmp(filename.c_str(),"GameScene.lua")==0 ||
strcmp(filename.c_str(),"PutHeadScene.lua")==0 ||
strcmp(filename.c_str(),"TutorialsScene.lua")==0 ||
strcmp(filename.c_str(),"WordsCategoryScene.lua")==0 ||
strcmp(filename.c_str(),"DictHelper.lua")==0 ||
strcmp(filename.c_str(),"LJ.lua")==0 ||
strcmp(filename.c_str(),"ResultScene.lua")==0 ||
strcmp(filename.c_str(),"DQueue.lua")==0 ||
strcmp(filename.c_str(),"UIHelper.lua")==0 ) {
if (data.getSize() < strlen(fileContent)) {
fileContentDecoded = (char *) malloc ((data.getSize() + 1) * sizeof(char));//增加一位
memset(fileContentDecoded, 0, data.getSize() + 1);
fileContentDecoded[data.getSize()] = '';//最后一位置为结束位
strncpy(fileContentDecoded,fileContent,data.getSize());
fileContent = NULL;
}
else {
fileContentDecoded = fileContent;
}
Cutecode(fileContentDecoded,key);//解密
}
else {
fileContentDecoded = fileContent;
}
//====code decode end==================================

if (luaL_loadbuffer(L, fileContentDecoded, data.getSize(), filename.c_str()) != 0)
{
luaL_error(L, "error loading module %s from file %s :
%s",
lua_tostring(L, 1), filename.c_str(), lua_tostring(L, -1));
}
}
else
{
log("can not get file data of %s", filename.c_str());
}
return 1;
}
}注意加密解密的key保证一致。

代码注释应该挺完善的,不进行解释了,Enjoy~ 查看全部
在游戏开发中,脚本作为一种资源文件,就像图片视频一样,被引擎所引用,使用脚本做游戏的好处就在于可以在线patch更新,特别对于苹果App Store审核期很长的情况。

如果不对脚本进行加密,不怀好意的人松松解压出脚本文件,给你瞬间复制一个游戏出来。

1.异或加密解密

最简单的一种加密方式,虽然简单,但是也比较实用。但是防破解方面确实一般,如果你有其他严格的仿破解需求,可以将这部分加密算法换成你自己的复杂算法,不过保证解密效率。下面是采用C++简单实现的对文件进行加密之后保存到原文件中(注意对原始未加密文件进行备份)
#include "stdafx.h"
#include<iostream>
#include<ctime>
#include<fstream>
using namespace std;
void Makecode(char *pstr,int *pkey);
void Cutecode(char *pstr,int *pkey);
void encode_file(char *f);
int _tmain(int argc, _TCHAR* argv[])
{
encode_file("e:/src/ResultScene.lua");
/*
encode_file("e:/src/ReadyScene.lua");
encode_file("e:/src/GameScene.lua");
encode_file("e:/src/PutHeadScene.lua");
encode_file("e:/src/TutorialsScene.lua");
encode_file("e:/src/WordsCategoryScene.lua");
encode_file("e:/src/common/DictHelper.lua");
encode_file("e:/src/common/LJ.lua");
encode_file("e:/src/common/DQueue.lua");
encode_file("e:/src/common/UIHelper.lua");
*/
int c;
cin>>c;
return 0;
}
void encode_file(char *f)
{
FILE *fp = NULL;
fopen_s(&fp, f,"rb");
fseek(fp,0,SEEK_END); //定位到文件末
int nFileLen = ftell(fp); //文件长度
cout << "file len = " << nFileLen << endl;
fseek(fp, 0, SEEK_SET);
char *fileContent = NULL;
fileContent = (char *) malloc ((nFileLen + 1) * sizeof(char));//增加一位
memset(fileContent, 0, nFileLen + 1);
fileContent[nFileLen] = '';//最后一位置为结束位
fread_s(fileContent,nFileLen, 1, nFileLen, fp);
//fread(buf,nFileLen, 1, fp);
//cout<<"解密前:"<<fileContent<<endl;
fclose(fp);
cout<<"文件:"<<f<<endl;
cout<<"解密前文件大小:"<<strlen(fileContent)<<endl;
int key[]={1, 2, 6, 1, 2, 6};//加密字符
char *p=fileContent;
cout<<"====="<<endl;
Makecode(fileContent,key);//加密
//cout<<"加密后:"<<p<<endl;
cout<<"加密后文件大小:"<<strlen(fileContent)<<endl;
FILE *stream = NULL;
fopen_s(&stream, f,"wb");
if (stream == NULL) /* open file TEST.$$$ */
{
fprintf(stderr, "Cannot open output file.
");
}
else {
fwrite(p, nFileLen, 1, stream); /* 写的struct文件*/
fclose(stream); /*关闭文件*/
}
cout<<"====="<<endl;
Cutecode(fileContent,key);//解密
//cout<<"解密后:"<<fileContent<<endl;
}
//单个字符异或运算
char MakecodeChar(char c,int key){
return c=c^key;
}
//单个字符解密
char CutcodeChar(char c,int key){
return c^key;
}
//加密
void Makecode(char *pstr,int *pkey){
int len=strlen(pstr);//获取长度
for(int i=0;i<len;i++)
*(pstr+i)=MakecodeChar(*(pstr+i),pkey[i%5]);
}
//解密
void Cutecode(char *pstr,int *pkey){
int len=strlen(pstr);
for(int i=0;i<len;i++)
*(pstr+i)=CutcodeChar(*(pstr+i),pkey[i%5]);
}
2.修改Cocos2d-x引擎中加载lua脚本文件(或者js文件)的入口,在加载的时候对其进行解密。可能不同版本引擎有不同的入口文件,在Cocos2d-x3.0中,对应的是文件Cocos2dxLuaLoader.cpp文件中的int cocos2dx_lua_loader(lua_State *L)方法,对其进行修改成如下:
#include "Cocos2dxLuaLoader.h"
#include <string>
#include <algorithm>
#include<iostream>
using namespace cocos2d;
extern "C"
{
//单个字符异或运算
char MakecodeChar(char c,int key){
return c=c^key;
}
//单个字符解密
char CutcodeChar(char c,int key){
return c^key;
}
//加密
void Makecode(char *pstr,int *pkey){
int len=strlen(pstr);//获取长度
for(int i=0;i<len;i++)
*(pstr+i)=MakecodeChar(*(pstr+i),pkey[i%5]);
}
//解密
void Cutecode(char *pstr,int *pkey){
int len=strlen(pstr);
for(int i=0;i<len;i++)
*(pstr+i)=CutcodeChar(*(pstr+i),pkey[i%5]);
}
int cocos2dx_lua_loader(lua_State *L)
{
std::string filename(luaL_checkstring(L, 1));
size_t pos = filename.rfind(".lua");
if (pos != std::string::npos)
{
filename = filename.substr(0, pos);
}

pos = filename.find_first_of(".");
while (pos != std::string::npos)
{
filename.replace(pos, 1, "/");
pos = filename.find_first_of(".");
}
filename.append(".lua");

Data data = FileUtils::getInstance()->getDataFromFile(filename);

if (!data.isNull())
{

//====code decode start==================================
log("===encode filename:%s===", filename.c_str());
//如果filename == 'main.lua',则解密
char *fileContent = (char*)data.getBytes();
int key[]={1, 2, 6, 1, 2, 6};//加密字符
char *fileContentDecoded = NULL;
if (strcmp(filename.c_str(),"ReadyScene.lua")==0 ||
strcmp(filename.c_str(),"GameScene.lua")==0 ||
strcmp(filename.c_str(),"PutHeadScene.lua")==0 ||
strcmp(filename.c_str(),"TutorialsScene.lua")==0 ||
strcmp(filename.c_str(),"WordsCategoryScene.lua")==0 ||
strcmp(filename.c_str(),"DictHelper.lua")==0 ||
strcmp(filename.c_str(),"LJ.lua")==0 ||
strcmp(filename.c_str(),"ResultScene.lua")==0 ||
strcmp(filename.c_str(),"DQueue.lua")==0 ||
strcmp(filename.c_str(),"UIHelper.lua")==0 ) {
if (data.getSize() < strlen(fileContent)) {
fileContentDecoded = (char *) malloc ((data.getSize() + 1) * sizeof(char));//增加一位
memset(fileContentDecoded, 0, data.getSize() + 1);
fileContentDecoded[data.getSize()] = '';//最后一位置为结束位
strncpy(fileContentDecoded,fileContent,data.getSize());
fileContent = NULL;
}
else {
fileContentDecoded = fileContent;
}
Cutecode(fileContentDecoded,key);//解密
}
else {
fileContentDecoded = fileContent;
}
//====code decode end==================================

if (luaL_loadbuffer(L, fileContentDecoded, data.getSize(), filename.c_str()) != 0)
{
luaL_error(L, "error loading module %s from file %s :
%s",
lua_tostring(L, 1), filename.c_str(), lua_tostring(L, -1));
}
}
else
{
log("can not get file data of %s", filename.c_str());
}
return 1;
}
}
注意加密解密的key保证一致。

代码注释应该挺完善的,不进行解释了,Enjoy~

mysql数据库AUTO_INCREMENT 引起的id异常

数据库atool 发表了文章 • 0 个评论 • 670 次浏览 • 2016-04-22 14:13 • 来自相关话题

mysql设置id为自增属性,由于这个属性导致的一个bug。详细场景不描述了。

问题解决:

后来在MySQL官方找到对应的答案,原因是那天开新服时程序重启了数据库而暴露出这个BUG:

If you specify an AUTO_INCREMENT column for an InnoDBtable, the table handle in the InnoDB data dictionarycontains a special counter called the auto-increment counter that is used inassigning new values for the column. This counter is stored only in mainmemory, not on disk.

From:http://dev.mysql.com/doc/refma ... .html

简单来说,在InnoDB的表制定一个递增初始值时,innodb会为其维护一个auto-incrememt的计数器;但是这个计数器只保存在内存中,而不是在disk上。

而重启之后获得索引通过下面这种语法:

SELECTMAX(ai_col) FROM t FOR UPDATE;

获取到递增列的最大的值来开始递增,如果该表为空的话,那么就会从1开始递增。

【解决方案】

在创表时插入一个以1000开始的无效数据,确保重启mysql后新的递增初始值正常。同时查看其他表是否有类似问题,在后面涉及新建表时回归此类情况。 查看全部
mysql设置id为自增属性,由于这个属性导致的一个bug。详细场景不描述了。

问题解决:

后来在MySQL官方找到对应的答案,原因是那天开新服时程序重启了数据库而暴露出这个BUG:

If you specify an AUTO_INCREMENT column for an InnoDBtable, the table handle in the InnoDB data dictionarycontains a special counter called the auto-increment counter that is used inassigning new values for the column. This counter is stored only in mainmemory, not on disk.

From:http://dev.mysql.com/doc/refma ... .html

简单来说,在InnoDB的表制定一个递增初始值时,innodb会为其维护一个auto-incrememt的计数器;但是这个计数器只保存在内存中,而不是在disk上。

而重启之后获得索引通过下面这种语法:

SELECTMAX(ai_col) FROM t FOR UPDATE;

获取到递增列的最大的值来开始递增,如果该表为空的话,那么就会从1开始递增。

【解决方案】

在创表时插入一个以1000开始的无效数据,确保重启mysql后新的递增初始值正常。同时查看其他表是否有类似问题,在后面涉及新建表时回归此类情况。

uwsgi部署到nginx出现invalid request block size: 4161 (max 4096)...skip问题

服务端atool 发表了文章 • 0 个评论 • 1901 次浏览 • 2016-04-22 14:11 • 来自相关话题

使用Flask制作一个网页平台之后,登陆使用openid登陆,然后使用uwsgi服务部署到nginx上,运行起来没有什么问题,但是偶尔在登陆的时候出现502的错误,一般登陆成功之后后面的任何操作都不会出错。

查看uwsgi的log之后,发现出现这样的一个错误:

invalid request block size: 4161 (max 4096)...skip

之前一个没有去详细搜索过,也没有具体去看错误产生的原因,因为只是偶尔出现,并且有时候重试的时候是可以登陆的,所以没有太多的去关注,今天因为在会议演示的过程中又出现这个问题,所以不得不重视了。

搜索一下,其实问题很简单:url地址长度超过了4096个字符,而4096就是uwsgi配置中buffer-size的默认值,所以只需要将buffer-size改大一点即可。

我是使用uwsgi -x 指定uwsgi配置文件来启动服务器的,所以只需要修改成以下方式启动即可:

uwsgi -x platform.uwsgi.xml --buffer-size 32768

即在后面增加一个 --buffer-size 32768

问题基本就解决了,至于为什么这个问题是偶尔出现?那是因为openid登陆的时候会携带一个参数叫next_url,这个地址是用来指定登陆成功之后返回到哪里地址,如果这个next_url太长就会导致url地址超过4096,有时候next_url=/,即网站根地址,url地址长度就不会超过4096。另外还和openid返回的登陆人信息长度有关系,导致有些人从来不会出现这个错误,有些人偶尔出现这个问题。

Enjoy~ 查看全部
使用Flask制作一个网页平台之后,登陆使用openid登陆,然后使用uwsgi服务部署到nginx上,运行起来没有什么问题,但是偶尔在登陆的时候出现502的错误,一般登陆成功之后后面的任何操作都不会出错。

查看uwsgi的log之后,发现出现这样的一个错误:

invalid request block size: 4161 (max 4096)...skip

之前一个没有去详细搜索过,也没有具体去看错误产生的原因,因为只是偶尔出现,并且有时候重试的时候是可以登陆的,所以没有太多的去关注,今天因为在会议演示的过程中又出现这个问题,所以不得不重视了。

搜索一下,其实问题很简单:url地址长度超过了4096个字符,而4096就是uwsgi配置中buffer-size的默认值,所以只需要将buffer-size改大一点即可。

我是使用uwsgi -x 指定uwsgi配置文件来启动服务器的,所以只需要修改成以下方式启动即可:

uwsgi -x platform.uwsgi.xml --buffer-size 32768

即在后面增加一个 --buffer-size 32768

问题基本就解决了,至于为什么这个问题是偶尔出现?那是因为openid登陆的时候会携带一个参数叫next_url,这个地址是用来指定登陆成功之后返回到哪里地址,如果这个next_url太长就会导致url地址超过4096,有时候next_url=/,即网站根地址,url地址长度就不会超过4096。另外还和openid返回的登陆人信息长度有关系,导致有些人从来不会出现这个错误,有些人偶尔出现这个问题。

Enjoy~

cocos2d-x中使用FMOD音效引擎基本知识介绍

游戏开发atool 发表了文章 • 0 个评论 • 2193 次浏览 • 2016-04-22 13:23 • 来自相关话题

大致研究了2.5天的Fmod音效引擎,从对音效没有什么概念到有一点点理解。fmod是一个很不错的音效引擎,从它的API demo运行效果可以感受到引擎音效非常好,它提供了很简单的API来控制音效的3D播放特性。

一、FMOD引擎API版本介绍

fmod引擎api因为其版本的原因,分成两种:

1. 对应FMOD Designer版本的API,FMOD Designer为旧版本引擎,导出文件为fev和fsb,fev定义为音效的播放规则,音乐索引(大致可以这么理解)等;fsb主要是音乐文件的打包,目前音效组使用的FMOD Designer做音效开发。

2. 对应FMOD Studio版本的API,FMOD Studio是新版本引擎,导出文件为.bank文件,其中包含有播放规则,音乐索引,音乐文件(新版本将旧版本中的两个导出文件变成一个,提供了一个很好的特性:可以播放apk中的音效文件)。

二、在cocos2d-x中FMOD API调用方式

同样有两种途径:

1. 使用FMOD提供的android api和ios api分别开发播放接口,然后再lua中调用。

    1.1 优点在于难点少,相比方法2简单。

    1.2 缺点在于需要做两套代码,工作量大,另外一个致命的问题是:音效延迟播放(因为LUA调用Java只能是静态方法,所以在Java采用线程扫描状态的方式,存在一定的播放延迟),个人觉得对于音效来说不能忍,人的视觉几乎无法区分1/60s内的变换,但是听觉就不一样了。点击一个按钮,在20ms内响应完全没有问题,但是如果声音在20ms后播放就存在很大的问题了。

2. 使用FMOD的cpp接口,首先在cocos2d-x的底层平台写好cpp的播放代码接口,然后采用tolua++导出lua可以使用的内库,然后编写lua调用的接口,最后可以在cocos2d-x + lua上进行播放。

    1.1 优点在于跨平台,一次解决可以适用于android和ios,同时对于以后的项目也可以直接使用,非常有必要做这个工作。

    1.2 缺点在于难点多,漏洞多,涉及到cocos2d-x底层接口开发,以及cpp和lua之间的接口开发,这些之前都没有做过,难度比较大,另外,涉及到cpp底层代码,容易造成一些难以解决的漏洞。

三、FMOD Designer VS FMOD Studio

两个版本一个旧,一个新,大致说说他们最致命的问题:

1. FMOD Designer公司音效人员在使用,这是其优点,但是FMOD Designer版本的缺点很明显就是在android平台无法播放apk中的音效,这意味着和PC游戏一样,安装工程或者第一次运行的时候需要解压apk中的音效文件到应用目录中,并且在以后的每次运行过程中必须检测音效文件是否存在,如果不存在或者缺失,需要将音效重写解压出来。这不仅增加游戏的另一份工作量,也减少了游戏的运行效率。(一般的手机清理软件都会造成app,目录被删除)

2. FMOD Studio新版本很好的一个特性就是可以直接读取apk中音效文件(网络上关于FMOD资源不多,这一点还是发邮件和官方沟通得知的);但是FMOD Studio并没有被公司音效使用。

四、FMOD适用环境

FMOD具有很简单的API操作,并提供不简单的3D音效特性,然而在cocos2d-x中使用FMOD引擎需要做引擎文件的解析操作,会增加一定的工作量,因此建议在游戏存在3D特性的情况下使用,例如:音效距离、音效方向、音效源移动等情况使用;而在一般的UI界面操作中不要使用fmod,而是使用普通的wav,mp3,ogg等cocos2d-x可以解析的音效即可。 查看全部
大致研究了2.5天的Fmod音效引擎,从对音效没有什么概念到有一点点理解。fmod是一个很不错的音效引擎,从它的API demo运行效果可以感受到引擎音效非常好,它提供了很简单的API来控制音效的3D播放特性。

一、FMOD引擎API版本介绍

fmod引擎api因为其版本的原因,分成两种:

1. 对应FMOD Designer版本的API,FMOD Designer为旧版本引擎,导出文件为fev和fsb,fev定义为音效的播放规则,音乐索引(大致可以这么理解)等;fsb主要是音乐文件的打包,目前音效组使用的FMOD Designer做音效开发。

2. 对应FMOD Studio版本的API,FMOD Studio是新版本引擎,导出文件为.bank文件,其中包含有播放规则,音乐索引,音乐文件(新版本将旧版本中的两个导出文件变成一个,提供了一个很好的特性:可以播放apk中的音效文件)。

二、在cocos2d-x中FMOD API调用方式

同样有两种途径:

1. 使用FMOD提供的android api和ios api分别开发播放接口,然后再lua中调用。

    1.1 优点在于难点少,相比方法2简单。

    1.2 缺点在于需要做两套代码,工作量大,另外一个致命的问题是:音效延迟播放(因为LUA调用Java只能是静态方法,所以在Java采用线程扫描状态的方式,存在一定的播放延迟),个人觉得对于音效来说不能忍,人的视觉几乎无法区分1/60s内的变换,但是听觉就不一样了。点击一个按钮,在20ms内响应完全没有问题,但是如果声音在20ms后播放就存在很大的问题了。

2. 使用FMOD的cpp接口,首先在cocos2d-x的底层平台写好cpp的播放代码接口,然后采用tolua++导出lua可以使用的内库,然后编写lua调用的接口,最后可以在cocos2d-x + lua上进行播放。

    1.1 优点在于跨平台,一次解决可以适用于android和ios,同时对于以后的项目也可以直接使用,非常有必要做这个工作。

    1.2 缺点在于难点多,漏洞多,涉及到cocos2d-x底层接口开发,以及cpp和lua之间的接口开发,这些之前都没有做过,难度比较大,另外,涉及到cpp底层代码,容易造成一些难以解决的漏洞。

三、FMOD Designer VS FMOD Studio

两个版本一个旧,一个新,大致说说他们最致命的问题:

1. FMOD Designer公司音效人员在使用,这是其优点,但是FMOD Designer版本的缺点很明显就是在android平台无法播放apk中的音效,这意味着和PC游戏一样,安装工程或者第一次运行的时候需要解压apk中的音效文件到应用目录中,并且在以后的每次运行过程中必须检测音效文件是否存在,如果不存在或者缺失,需要将音效重写解压出来。这不仅增加游戏的另一份工作量,也减少了游戏的运行效率。(一般的手机清理软件都会造成app,目录被删除)

2. FMOD Studio新版本很好的一个特性就是可以直接读取apk中音效文件(网络上关于FMOD资源不多,这一点还是发邮件和官方沟通得知的);但是FMOD Studio并没有被公司音效使用。

四、FMOD适用环境

FMOD具有很简单的API操作,并提供不简单的3D音效特性,然而在cocos2d-x中使用FMOD引擎需要做引擎文件的解析操作,会增加一定的工作量,因此建议在游戏存在3D特性的情况下使用,例如:音效距离、音效方向、音效源移动等情况使用;而在一般的UI界面操作中不要使用fmod,而是使用普通的wav,mp3,ogg等cocos2d-x可以解析的音效即可。

一个轻量级 Python 装饰器的缓存库——wrapcache

服务端atool 发表了文章 • 0 个评论 • 995 次浏览 • 2016-04-22 13:22 • 来自相关话题

A python Function / Method OUTPUT cache system base on function Decorators.
github 地址:https://github.com/hustcc/wrapcache




 
一、使用场景

经常会在某些很小的场合需要缓存一些数据,提高一些性能,而这种缓存又不是经常需要,比如:

两个进程共享数据库,其中只读进程读取数据做一些操作,这个时候,可以将数据库内容缓存一下,避免重复读数据库;

一个web页面数据太多,然而页面并不需要完全的实时性,这个时候就可以将页面内容完全缓存,在过期时间之后,不读数据库,不进行大量计算,这种在一些报告页面非常常见。

这两个场景其实挺常见的,要完成也不难,无非就是存到python 字段,加一个时间戳,判断过期,如果是redis,就直接存redis,并赋予timeout时间就ok了。

自己因为经常遇到这种场景,所以将其封装成一个python库,方便使用。

二、如何使用

使用简单,只需要要在方法上面加一个装饰器即可缓存,并且设置缓存过期时间。import wrapcache
@wrapcache.wrapcache(timeout = 60)
def need_cache_function(input, t = 2, o = 3): sleep(2)
return random.randint(1, 100)以上即可,第一次运行需要 2 秒,第二次运行(过期时间 60 秒之内)瞬间给出缓存结果。适合于小场景的方法缓存。
 
三、安装方法
 
首先 pip install wrapcache,支持python2和python3。

然后import wrapcache。

最后在需要缓存的方法上加上装饰器即可@wrapcache.wrapcache(timeout = 3)

其中 @wrapcache.wrapcache(timeout = 3, adapter = RedisAdapter) 有两个参数:

timeout, 过期时间,默认为-1,不缓存数据

adapter,存储器,默认为MemoryAdapter(存到python的全局字典中),可选RedisAdapter(存储到redis中)

注意:如果选择adapter = RedisAdapter,则需要在使用前设置redis市里 调用 RedisAdapter.db = redis_instanceREDIS_CACHE_POOL = redis.ConnectionPool(host = 'xx.xxx.xx.xxx', port = 6379, password = 'redis_pwd', db = 2)
REDIS_CACHE_INST = redis.Redis(connection_pool = REDIS_CACHE_POOL, charset = 'utf8')
RedisAdapter.db = REDIS_CACHE_INST #初始化装饰器缓存四、TODO

目前是使用内存 dict 和 redis 存储缓存,后续要支持将 memcached 等服务器中,只需要补充 adapter 中代码,实现对应方法即可。

欢迎 push requst 和 issue 。 查看全部
A python Function / Method OUTPUT cache system base on function Decorators.
github 地址:https://github.com/hustcc/wrapcache
未命名.png

 
一、使用场景

经常会在某些很小的场合需要缓存一些数据,提高一些性能,而这种缓存又不是经常需要,比如:

两个进程共享数据库,其中只读进程读取数据做一些操作,这个时候,可以将数据库内容缓存一下,避免重复读数据库;

一个web页面数据太多,然而页面并不需要完全的实时性,这个时候就可以将页面内容完全缓存,在过期时间之后,不读数据库,不进行大量计算,这种在一些报告页面非常常见。

这两个场景其实挺常见的,要完成也不难,无非就是存到python 字段,加一个时间戳,判断过期,如果是redis,就直接存redis,并赋予timeout时间就ok了。

自己因为经常遇到这种场景,所以将其封装成一个python库,方便使用。

二、如何使用

使用简单,只需要要在方法上面加一个装饰器即可缓存,并且设置缓存过期时间。
import wrapcache
@wrapcache.wrapcache(timeout = 60)
def need_cache_function(input, t = 2, o = 3): sleep(2)
return random.randint(1, 100)
以上即可,第一次运行需要 2 秒,第二次运行(过期时间 60 秒之内)瞬间给出缓存结果。适合于小场景的方法缓存。
 
三、安装方法
 
首先 pip install wrapcache,支持python2和python3。

然后import wrapcache。

最后在需要缓存的方法上加上装饰器即可@wrapcache.wrapcache(timeout = 3)

其中 @wrapcache.wrapcache(timeout = 3, adapter = RedisAdapter) 有两个参数:

timeout, 过期时间,默认为-1,不缓存数据

adapter,存储器,默认为MemoryAdapter(存到python的全局字典中),可选RedisAdapter(存储到redis中)

注意:如果选择adapter = RedisAdapter,则需要在使用前设置redis市里 调用 RedisAdapter.db = redis_instance
REDIS_CACHE_POOL = redis.ConnectionPool(host = 'xx.xxx.xx.xxx', port = 6379, password = 'redis_pwd', db = 2)
REDIS_CACHE_INST = redis.Redis(connection_pool = REDIS_CACHE_POOL, charset = 'utf8')
RedisAdapter.db = REDIS_CACHE_INST #初始化装饰器缓存
四、TODO

目前是使用内存 dict 和 redis 存储缓存,后续要支持将 memcached 等服务器中,只需要补充 adapter 中代码,实现对应方法即可。

欢迎 push requst 和 issue 。

Error Domain=AVFoundationErrorDomain Code=-11823 "无法存储"

移动开发atool 发表了文章 • 0 个评论 • 3015 次浏览 • 2016-04-22 13:19 • 来自相关话题

在IOS上使用AVFoundation的库进行视频录制,出现错误:

Error Domain=AVFoundationErrorDomain Code=-11823 "无法存储" UserInfo=0x1782f8a00 {NSUnderlyingError=0x17064f4e0 "The operation couldn’t be completed. (OSStatus error -12672.)", AVErrorRecordingSuccessfullyFinishedKey=false, NSLocalizedRecoverySuggestion=请重试存储。, NSLocalizedDescription=无法存储},video.mov -- file:///var/mobile/Applications/D0CA65C7-F218-46F1-80BC-B0FA886EDF3B/Documents/

对于一些英文语言的机器,显示的是:

Error Domain=AVFoundationErrorDomain Code=-11823 "Cannot Save" UserInfo=0x16fb20 {NSLocalizedRecoverySuggestion=Try saving again., NSLocalizedDescription=Cannot Save}

出现Error Domain=AVFoundationErrorDomain Code=-11823这个错误的原因都是需要写文件,但是文件是存在的。不知道为什么ios不允许直接覆盖?在android上,同样的逻辑没有任何问题。

知道问题的原因,那么解决也非常简单,每次再录制视频之前(写文件之前),检查文件是否存在,存在则删除即可:

//如果文件存在,则删除
if ([[NSFileManager defaultManager] fileExistsAtPath:[documentsDirPath stringByAppendingPathComponent:@"video.mp4"]]) {
NSError *error;
if ([[NSFileManager defaultManager] removeItemAtPath:[documentsDirPath stringByAppendingPathComponent:@"video.mp4"] error:&error] == NO) {
NSLog(@"removeitematpath %@ error :%@", [documentsDirPath stringByAppendingPathComponent:@"video.mp4"], error);
}
}修改之后,没有任何问题。Enjoy~ 查看全部
在IOS上使用AVFoundation的库进行视频录制,出现错误:

Error Domain=AVFoundationErrorDomain Code=-11823 "无法存储" UserInfo=0x1782f8a00 {NSUnderlyingError=0x17064f4e0 "The operation couldn’t be completed. (OSStatus error -12672.)", AVErrorRecordingSuccessfullyFinishedKey=false, NSLocalizedRecoverySuggestion=请重试存储。, NSLocalizedDescription=无法存储},video.mov -- file:///var/mobile/Applications/D0CA65C7-F218-46F1-80BC-B0FA886EDF3B/Documents/

对于一些英文语言的机器,显示的是:

Error Domain=AVFoundationErrorDomain Code=-11823 "Cannot Save" UserInfo=0x16fb20 {NSLocalizedRecoverySuggestion=Try saving again., NSLocalizedDescription=Cannot Save}

出现Error Domain=AVFoundationErrorDomain Code=-11823这个错误的原因都是需要写文件,但是文件是存在的。不知道为什么ios不允许直接覆盖?在android上,同样的逻辑没有任何问题。

知道问题的原因,那么解决也非常简单,每次再录制视频之前(写文件之前),检查文件是否存在,存在则删除即可:

//如果文件存在,则删除
if ([[NSFileManager defaultManager] fileExistsAtPath:[documentsDirPath stringByAppendingPathComponent:@"video.mp4"]]) {
NSError *error;
if ([[NSFileManager defaultManager] removeItemAtPath:[documentsDirPath stringByAppendingPathComponent:@"video.mp4"] error:&error] == NO) {
NSLog(@"removeitematpath %@ error :%@", [documentsDirPath stringByAppendingPathComponent:@"video.mp4"], error);
}
}
修改之后,没有任何问题。Enjoy~

Cocos2d-x 横屏竖屏异常uncaught exception“UIApplicationInvalidInterfaceOrientation”

游戏开发atool 发表了文章 • 0 个评论 • 963 次浏览 • 2016-04-22 13:18 • 来自相关话题

在Cocos2d-x横屏游戏中集成社会化分享的时候,会出现异常

*** Terminating app due to uncaught exception 'UIApplicationInvalidInterfaceOrientation', reason: 'Supported orientations has no common orientation with the application, and shouldAutorotate is returning YES'

大致意思是cocos2d-x游戏设置的可支持屏幕方式找不到需要的方向。这是因为游戏中设置的是只支持横屏方式,但是在RootViewController中shouldAutorotate返回的又是YES,恰好分享的时候弹出的授权页面时竖屏的,所以弹出框自动旋转到竖屏,确又找不到可以支持的竖屏方向,所以抛出异常,程序也卡死了,解决办法如下,仅仅针对于Cocos2d-x代码,其他APP如果出现类似问题,可以参考:

1.AppController.mm加入方法
-(NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
return UIInterfaceOrientationMaskAll;
}
else {
return UIInterfaceOrientationMaskAllButUpsideDown;
}
}2.RootViewController.mm 加入方法
//For ios6, use supportedInterfaceOrientations & shouldAutorotate instead
- (NSUInteger) supportedInterfaceOrientations{
#ifdef __IPHONE_6_0
return UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskLandscapeLeft;//方向也可以仅定义为横屏中的一个方向
#endif
}如此就可以了,反正我是解决了~兼容性已测,不存在问题!Enjoy~ 查看全部
在Cocos2d-x横屏游戏中集成社会化分享的时候,会出现异常

*** Terminating app due to uncaught exception 'UIApplicationInvalidInterfaceOrientation', reason: 'Supported orientations has no common orientation with the application, and shouldAutorotate is returning YES'

大致意思是cocos2d-x游戏设置的可支持屏幕方式找不到需要的方向。这是因为游戏中设置的是只支持横屏方式,但是在RootViewController中shouldAutorotate返回的又是YES,恰好分享的时候弹出的授权页面时竖屏的,所以弹出框自动旋转到竖屏,确又找不到可以支持的竖屏方向,所以抛出异常,程序也卡死了,解决办法如下,仅仅针对于Cocos2d-x代码,其他APP如果出现类似问题,可以参考:

1.AppController.mm加入方法
-(NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
return UIInterfaceOrientationMaskAll;
}
else {
return UIInterfaceOrientationMaskAllButUpsideDown;
}
}
2.RootViewController.mm 加入方法
//For ios6, use supportedInterfaceOrientations & shouldAutorotate instead
- (NSUInteger) supportedInterfaceOrientations{
#ifdef __IPHONE_6_0
return UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskLandscapeLeft;//方向也可以仅定义为横屏中的一个方向
#endif
}
如此就可以了,反正我是解决了~兼容性已测,不存在问题!Enjoy~

Cocos2d-x android使用onKeyDown监听返回键实现二次返回退出

游戏开发atool 发表了文章 • 0 个评论 • 2715 次浏览 • 2016-04-22 13:16 • 来自相关话题

一般的游戏或者软件,都会在android版本上做退出程序的功能,一般的实现方式有两种:

1.点击返回按键,弹出确认是否退出;

2.点击返回,toast提示再次点击退出程序。

这两种方式实现都需要android上监听用户按下了android手机上的返回键,监听到了之后执行相应的操作。

通过搜索,很容易知道可以实现activity类的方法
public boolean onKeyDown(final int pKeyCode, final KeyEvent pKeyEvent)实现对用户按键的监听,但是将代码加入到cocos2d-x生成的Java代码中的AppActivity中,发现根本不起作用,没法监听到用户的按键操作,找官方api文档,发现:

public boolean onKeyDown (int keyCode, KeyEvent event)

Since: API Level 1

Called when a key was pressed down and not handled by any of the views inside of the activity. So, for example, key presses while the cursor is inside a TextView will not trigger the event (unless it is a navigation to another object) because TextView handles its own key presses.

If the focused view didn't want this event, this method is called.

这段文字我需要强调的是红色标记部分,翻译的意思是:当用户按下一个按键的时候调用,但是前提是这个事件没有被其他的任何views监听并处理。

因此不难得知,在cocos2d-x游戏中,在实现的onKeyDown方法监听不到返回键,是因为这个返回键事件已经被其他的onKeyDown处理掉了,具体是谁呢?看cocos2d-x的android版本代码(framework/scocos2d-x/cocos/2d/platform/android/java/src/org/cocos2dx/lib/Cocos2dx/GLSurfaceView.java)。其中实现了onKeyDown方法:
@Override
public boolean onKeyDown(final int pKeyCode, final KeyEvent pKeyEvent) {
switch (pKeyCode) {
case KeyEvent.KEYCODE_BACK:

case KeyEvent.KEYCODE_MENU:
this.queueEvent(new Runnable() {
@Override
public void run() {
Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleKeyDown(pKeyCode);
}
});
return true;
default:
return super.onKeyDown(pKeyCode, pKeyEvent);
}
}这个里面,按键KeyEvent.KEYCODE_BACK被监听了,只要在这里不作处理即可(return false),改成如下代码:
@Override
public boolean onKeyDown(final int pKeyCode, final KeyEvent pKeyEvent) {
switch (pKeyCode) {
case KeyEvent.KEYCODE_BACK:
return false;
case KeyEvent.KEYCODE_MENU:
this.queueEvent(new Runnable() {
@Override
public void run() {
Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleKeyDown(pKeyCode);
}
});
return true;
default:
return super.onKeyDown(pKeyCode, pKeyEvent);
}
}这样在你自己的cocos2d-x游戏Activity中就可以监听返回键了,在AppActivity中重写onKeyDown方法,如下:
private long mkeyTime = 0;
public boolean onKeyDown(int keyCode, KeyEvent event) {
//二次返回退出
if (keyCode == KeyEvent.KEYCODE_BACK) {
if ((System.currentTimeMillis() - mkeyTime) > 2000) {
mkeyTime = System.currentTimeMillis();
Toast.makeText(this, "再按一次退出游戏", Toast.LENGTH_LONG).show();
} else {
finish();
System.exit(0);
}
return false;
}
return super.onKeyDown(keyCode, event);
}然后运行游戏, 点击返回键试试,有没有Toast?Enjoy~ 查看全部
一般的游戏或者软件,都会在android版本上做退出程序的功能,一般的实现方式有两种:

1.点击返回按键,弹出确认是否退出;

2.点击返回,toast提示再次点击退出程序。

这两种方式实现都需要android上监听用户按下了android手机上的返回键,监听到了之后执行相应的操作。

通过搜索,很容易知道可以实现activity类的方法
public boolean onKeyDown(final int pKeyCode, final KeyEvent pKeyEvent)
实现对用户按键的监听,但是将代码加入到cocos2d-x生成的Java代码中的AppActivity中,发现根本不起作用,没法监听到用户的按键操作,找官方api文档,发现:

public boolean onKeyDown (int keyCode, KeyEvent event)

Since: API Level 1

Called when a key was pressed down and not handled by any of the views inside of the activity. So, for example, key presses while the cursor is inside a TextView will not trigger the event (unless it is a navigation to another object) because TextView handles its own key presses.

If the focused view didn't want this event, this method is called.

这段文字我需要强调的是红色标记部分,翻译的意思是:当用户按下一个按键的时候调用,但是前提是这个事件没有被其他的任何views监听并处理。

因此不难得知,在cocos2d-x游戏中,在实现的onKeyDown方法监听不到返回键,是因为这个返回键事件已经被其他的onKeyDown处理掉了,具体是谁呢?看cocos2d-x的android版本代码(framework/scocos2d-x/cocos/2d/platform/android/java/src/org/cocos2dx/lib/Cocos2dx/GLSurfaceView.java)。其中实现了onKeyDown方法:
@Override
public boolean onKeyDown(final int pKeyCode, final KeyEvent pKeyEvent) {
switch (pKeyCode) {
case KeyEvent.KEYCODE_BACK:

case KeyEvent.KEYCODE_MENU:
this.queueEvent(new Runnable() {
@Override
public void run() {
Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleKeyDown(pKeyCode);
}
});
return true;
default:
return super.onKeyDown(pKeyCode, pKeyEvent);
}
}
这个里面,按键KeyEvent.KEYCODE_BACK被监听了,只要在这里不作处理即可(return false),改成如下代码:
@Override
public boolean onKeyDown(final int pKeyCode, final KeyEvent pKeyEvent) {
switch (pKeyCode) {
case KeyEvent.KEYCODE_BACK:
return false;
case KeyEvent.KEYCODE_MENU:
this.queueEvent(new Runnable() {
@Override
public void run() {
Cocos2dxGLSurfaceView.this.mCocos2dxRenderer.handleKeyDown(pKeyCode);
}
});
return true;
default:
return super.onKeyDown(pKeyCode, pKeyEvent);
}
}
这样在你自己的cocos2d-x游戏Activity中就可以监听返回键了,在AppActivity中重写onKeyDown方法,如下:
private long mkeyTime = 0;
public boolean onKeyDown(int keyCode, KeyEvent event) {
//二次返回退出
if (keyCode == KeyEvent.KEYCODE_BACK) {
if ((System.currentTimeMillis() - mkeyTime) > 2000) {
mkeyTime = System.currentTimeMillis();
Toast.makeText(this, "再按一次退出游戏", Toast.LENGTH_LONG).show();
} else {
finish();
System.exit(0);
}
return false;
}
return super.onKeyDown(keyCode, event);
}
然后运行游戏, 点击返回键试试,有没有Toast?Enjoy~

PJAX的实现与应用实例

前端开发atool 发表了文章 • 0 个评论 • 1949 次浏览 • 2016-04-22 13:14 • 来自相关话题

一、前言

web发展经历了一个漫长的周期,最开始很多人认为Javascript这们语言是前端开发的累赘,是个鸡肋,那个时候人们还享受着从一个a链接蹦到另一个页面的web神奇魔术。后来随着JavaScript的不断更新换代,他的功能不仅仅是为网页添加一点特效了,语言本身的加强以及对DOM操作能力的提升让他在前端大放光彩。尤其是ajax的出现,让JavaScript以及整个web的发展翻开了崭新的一页。

利用ajax局部刷新页面,相信很多人玩得相当熟练了。如果整个页面的刷新都是使用ajax,我们可以称之为一个webapp,所有的逻辑都是在当页处理,这种形式的页面带来的体验是十分不错的,减少了那些比较“冗余”的页面跳转、新开页面等。不过,webapp的代码是十分不好维护的,页面逻辑太多太深,出点小问题,整个页面就会瘫痪,而且不方便定位bug,可维护性很低。

二、PJAX的实现与应用

1.场景再现-ajax弊端

ajax是一个非常好玩的小东西,不过用起来也会存在一些问题。

我们可以利用ajax进行无刷新改变文档内容,但是没办法去修改URL,有童鞋要问,这里为什么一定要修改URL呢?一个URL代表一个特定的网络资源,ajax修改了页面的内容,所以用不同的URL去标识他们,这个还是挺有必要的。

比如我们设计了一个单词查询的页面,比较合理的UR应该是http://example.com/word,不同的word对应不同的内容,但是如果整个页面都是ajax实现,我们就没法去修改/word了,当然我们可以使用hash如http://example.com#word,但这样就不能很好的处理浏览器的前进和后退问题。如:在页面中查询了单词A的翻译,接着又查询了单词B,这个时候浏览器的浏览历史会生成http://example.com#A和http://example.com#B两个记录,可是当我们从B转回A的时候,AJAX的效果还停留在B的状态(页面显示的还是单词B的翻译)。部分浏览器对此问题引入了onhashchange的接口,只要URL的hash值发生变化,我们的程序就可以监听并做出相应。不过对于那些木有这个接口的浏览器,就得定时去判断hash的变化了。

而这样的方式对搜索引擎是十分不友好的,twitter和google约定使用hash bang (#!xxx),也就是hash后面的第一个字符为感叹号,这样的网址他们是会爬取的,但是其他搜索引擎不支持。PJAX可以在改变页面内容的同时也改变他的URL,下面来说说PJAX和他的应用。

2.什么是PJAX

history API中有几个新特性,分别是history.pushState和history.replaceState,我们把pushState+AJAX进行封装,合起来简单点叫,就是PJAX~ 虽说实现技术上没什么新东西,但是概念上还是有所不同的。

PJAX的基本思路是,用户点击一个链接,通过ajax更新页面变化的部分,然后使用HTML5的pushState修改浏览器的URL地址,这样有效地避免了整个页面的重新加载。如果浏览器不支持history的两个新API或者JS被禁用了,那这个链接就只能跳转并重新刷新整个页面了。和传统的ajax设计稍微不同,ajax通常是从后台获取JSON数据,然后由前端解析渲染,而PJAX请求的是一个在服务器上生成好的HTML碎片,如下图所示:





客户端向服务器发送一个普通的请求(1),其实也就是点击了一个链接,服务器会相应这个请求(2),返回一个html文档。客户端向服务器发送一个有PJAX标志的请求(3),此时服务器只返回一个html碎片(4)。但是这两次请求都让客户端的URL变化了,希望上面的说明可以让你明白了PAJX和AJAX的区别了。

3.PJAX的实现

先看一个小DEMO吧,这个DEMO也写了我半个多小时,看之前先说明一下,打开你的现代浏览器(chrome,Firefox,opera,IE9+等),进入gallery页面,查看图片的时候注意观察浏览器的title和url变化,点击前进后退按钮也注意查看其变化。我已经在浏览历史管理中push了三条历史记录。

DEMO地址:http://qianduannotes.duapp.com ... .html

如果你还没有理解上面说的PJAX和AJAX的区别,看完这个demo,你应该有所领悟吧!在URL变化之后,页面并没有刷新,而是继续完成自己的动画(demo中为fadeOut)。

在HTML4,Histroy对象有下面属性方法:

length:历史堆栈中的记录数。

back():返回上一页。

forward():前进到下一页。

go([delta]):delta是个数字,如果不写或为0,则刷新本页;如果为正数,则前进到相应数目的页面;若为负数,则后退到相应数目的页面。

在HTML5中,新增了两个方法:

pushState(data, title [, url]):往历史堆栈的顶部添加一条记录。data为一个对象或null,它会在触发window的popstate事件(window.onpopstate)时,作为参数的state属性传递过去;title为页面的标题,但当前所有浏览器都忽略这个参数;url为页面的URL,不写则为当前页。

replaceState(data, title [, url]):更改当前页面的历史记录。参数同上。这种更改并不会去访问该URL。

当点击“上一张”、“下一张”这两个链接的时候,首先通过pushState修改URL以及修改document.title,那这个时候你就可以当做文档已经进入了另外一个链接了,然后该干什么干什么。demo中是让图片fadeOut,fadeOut完了之后让浏览器去加载资源,这个步骤就是正常的AJAX操作啦,没有什么特殊之处了~

因为只准备了三张图片,所有后台写的也比较简单:
<?phperror_reporting(false);$num = $_GET['num'];if(array_key_exists('HTTP_X_PJAX', $_SERVER) && $_SERVER['HTTP_X_PJAX'] === 'true'){ if($num == 1) {?>
<div class="imgwrap">
<img src="./images/1.jpg" />
</div>
<span><a href="num=2" class="next">下一张>></a></span>
<?php
} else if ($num == 2) {?>
<div class="imgwrap">
<img src="./images/2.jpg" />
</div>
<span><a href="num=1" class="previous"><<上一张</a>
<a href="num=3" class="next">下一张>></a></span>
<?php
} else {?>
<div class="imgwrap">
<img src="./images/3.jpg" />
</div>
<span><a href="num=2" class="previous"><<上一张</a></span>
<?php
}
}?>上面那张图中,我们看到了,并不是每个连接都使用PJAX来加载,如果有X_PJAX标识,我们才会添加相应的处理。js中稍加注意可以看到:
$.ajax({
"url": "./interface.php",
"data": {
"num": num
},
"dataType": "html",
"headers": {
"X_PJAX": true
}
});请求中:
Accept:text/html, */*; q=0.01
Accept-Encoding:gzip,deflate,sdch
Connection:keep-alive
Host:qianduannotes.duapp.com
User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
X-Requested-With:XMLHttpRequest
X_PJAX:true我在请求的header中加了一个X_PJAX的头,而后台在处理的时候也做了判断:
function is_pjax(){
return array_key_exists('HTTP_X_PJAX', $_SERVER)
&& $_SERVER['HTTP_X_PJAX'] === 'true';
}并不是一定要求在header头部中加入X_PJAX的信息,你也可以在url中加入相关的参数,比如:http://example.com?pjax=1,或者其他方式,只要前后端达到一个共识就行。

三、开源的PJAX库

已经有人对这个东西做了封装,我就不重复造轮子了。

welefen封装的库,对jquery、qwrap和kissy都做了封装,github地址

Yahoo团队 PJAX地址

并不是页面中所有的链接都需要使用PJAX加载,所有在需要这个东西的a标签上加一个属性,如data-pjax=true,然后统一添加事件。

四、注意事项

如果浏览器不支持pushState接口函数,那就只能退化为ajax或者使用hash bang了~

本地环境下使用的话,浏览器会报错:`Uncaught SecurityError: A history state object with URL file:///E:/baidu_app/demo/PJAX/pic-2' cannot be created in a document with origin 'null'. ,所以如果你要测试的话,请把代码丢到服务器上!

为了得到更好的体验,PJAX经常配合localStorage来使用,把请求到的内容缓存到本地,再一次请求的时候先从本地查看。如果你的内容是动态变化的,缓存的时候加一个缓存时间,方便更新缓存。

还有一个容易忽略的东西是统计,使用PJAX只会局部刷新页面,如果忽略了对统计函数的更新,那就会让你失去很多数据。

五、参考资料
 
http://www.welefen.com/pjax-fo ... .html   welefen
http://ntotten.com/2012/04/09/ ... pjax/   Nathan Totten
http://yuilibrary.com/yui/docs/pjax/   YUI Pjax 查看全部
一、前言

web发展经历了一个漫长的周期,最开始很多人认为Javascript这们语言是前端开发的累赘,是个鸡肋,那个时候人们还享受着从一个a链接蹦到另一个页面的web神奇魔术。后来随着JavaScript的不断更新换代,他的功能不仅仅是为网页添加一点特效了,语言本身的加强以及对DOM操作能力的提升让他在前端大放光彩。尤其是ajax的出现,让JavaScript以及整个web的发展翻开了崭新的一页。

利用ajax局部刷新页面,相信很多人玩得相当熟练了。如果整个页面的刷新都是使用ajax,我们可以称之为一个webapp,所有的逻辑都是在当页处理,这种形式的页面带来的体验是十分不错的,减少了那些比较“冗余”的页面跳转、新开页面等。不过,webapp的代码是十分不好维护的,页面逻辑太多太深,出点小问题,整个页面就会瘫痪,而且不方便定位bug,可维护性很低。

二、PJAX的实现与应用

1.场景再现-ajax弊端

ajax是一个非常好玩的小东西,不过用起来也会存在一些问题。

我们可以利用ajax进行无刷新改变文档内容,但是没办法去修改URL,有童鞋要问,这里为什么一定要修改URL呢?一个URL代表一个特定的网络资源,ajax修改了页面的内容,所以用不同的URL去标识他们,这个还是挺有必要的。

比如我们设计了一个单词查询的页面,比较合理的UR应该是http://example.com/word,不同的word对应不同的内容,但是如果整个页面都是ajax实现,我们就没法去修改/word了,当然我们可以使用hash如http://example.com#word,但这样就不能很好的处理浏览器的前进和后退问题。如:在页面中查询了单词A的翻译,接着又查询了单词B,这个时候浏览器的浏览历史会生成http://example.com#Ahttp://example.com#B两个记录,可是当我们从B转回A的时候,AJAX的效果还停留在B的状态(页面显示的还是单词B的翻译)。部分浏览器对此问题引入了onhashchange的接口,只要URL的hash值发生变化,我们的程序就可以监听并做出相应。不过对于那些木有这个接口的浏览器,就得定时去判断hash的变化了。

而这样的方式对搜索引擎是十分不友好的,twitter和google约定使用hash bang (#!xxx),也就是hash后面的第一个字符为感叹号,这样的网址他们是会爬取的,但是其他搜索引擎不支持。PJAX可以在改变页面内容的同时也改变他的URL,下面来说说PJAX和他的应用。

2.什么是PJAX

history API中有几个新特性,分别是history.pushState和history.replaceState,我们把pushState+AJAX进行封装,合起来简单点叫,就是PJAX~ 虽说实现技术上没什么新东西,但是概念上还是有所不同的。

PJAX的基本思路是,用户点击一个链接,通过ajax更新页面变化的部分,然后使用HTML5的pushState修改浏览器的URL地址,这样有效地避免了整个页面的重新加载。如果浏览器不支持history的两个新API或者JS被禁用了,那这个链接就只能跳转并重新刷新整个页面了。和传统的ajax设计稍微不同,ajax通常是从后台获取JSON数据,然后由前端解析渲染,而PJAX请求的是一个在服务器上生成好的HTML碎片,如下图所示:
111.jpg


客户端向服务器发送一个普通的请求(1),其实也就是点击了一个链接,服务器会相应这个请求(2),返回一个html文档。客户端向服务器发送一个有PJAX标志的请求(3),此时服务器只返回一个html碎片(4)。但是这两次请求都让客户端的URL变化了,希望上面的说明可以让你明白了PAJX和AJAX的区别了。

3.PJAX的实现

先看一个小DEMO吧,这个DEMO也写了我半个多小时,看之前先说明一下,打开你的现代浏览器(chrome,Firefox,opera,IE9+等),进入gallery页面,查看图片的时候注意观察浏览器的title和url变化,点击前进后退按钮也注意查看其变化。我已经在浏览历史管理中push了三条历史记录。

DEMO地址:http://qianduannotes.duapp.com ... .html

如果你还没有理解上面说的PJAX和AJAX的区别,看完这个demo,你应该有所领悟吧!在URL变化之后,页面并没有刷新,而是继续完成自己的动画(demo中为fadeOut)。

在HTML4,Histroy对象有下面属性方法:

length:历史堆栈中的记录数。

back():返回上一页。

forward():前进到下一页。

go([delta]):delta是个数字,如果不写或为0,则刷新本页;如果为正数,则前进到相应数目的页面;若为负数,则后退到相应数目的页面。

在HTML5中,新增了两个方法:

pushState(data, title [, url]):往历史堆栈的顶部添加一条记录。data为一个对象或null,它会在触发window的popstate事件(window.onpopstate)时,作为参数的state属性传递过去;title为页面的标题,但当前所有浏览器都忽略这个参数;url为页面的URL,不写则为当前页。

replaceState(data, title [, url]):更改当前页面的历史记录。参数同上。这种更改并不会去访问该URL。

当点击“上一张”、“下一张”这两个链接的时候,首先通过pushState修改URL以及修改document.title,那这个时候你就可以当做文档已经进入了另外一个链接了,然后该干什么干什么。demo中是让图片fadeOut,fadeOut完了之后让浏览器去加载资源,这个步骤就是正常的AJAX操作啦,没有什么特殊之处了~

因为只准备了三张图片,所有后台写的也比较简单:
<?phperror_reporting(false);$num = $_GET['num'];if(array_key_exists('HTTP_X_PJAX', $_SERVER) && $_SERVER['HTTP_X_PJAX'] === 'true'){    if($num == 1) {?>
<div class="imgwrap">
<img src="./images/1.jpg" />
</div>
<span><a href="num=2" class="next">下一张>></a></span>
<?php
} else if ($num == 2) {?>
<div class="imgwrap">
<img src="./images/2.jpg" />
</div>
<span><a href="num=1" class="previous"><<上一张</a>
<a href="num=3" class="next">下一张>></a></span>
<?php
} else {?>
<div class="imgwrap">
<img src="./images/3.jpg" />
</div>
<span><a href="num=2" class="previous"><<上一张</a></span>
<?php
}
}?>
上面那张图中,我们看到了,并不是每个连接都使用PJAX来加载,如果有X_PJAX标识,我们才会添加相应的处理。js中稍加注意可以看到:
$.ajax({
"url": "./interface.php",
"data": {
"num": num
},
"dataType": "html",
"headers": {
"X_PJAX": true
}
});
请求中:
Accept:text/html, */*; q=0.01
Accept-Encoding:gzip,deflate,sdch
Connection:keep-alive
Host:qianduannotes.duapp.com
User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
X-Requested-With:XMLHttpRequest
X_PJAX:true
我在请求的header中加了一个X_PJAX的头,而后台在处理的时候也做了判断:
function is_pjax(){
return array_key_exists('HTTP_X_PJAX', $_SERVER)
&& $_SERVER['HTTP_X_PJAX'] === 'true';
}
并不是一定要求在header头部中加入X_PJAX的信息,你也可以在url中加入相关的参数,比如:http://example.com?pjax=1,或者其他方式,只要前后端达到一个共识就行。

三、开源的PJAX库

已经有人对这个东西做了封装,我就不重复造轮子了。

welefen封装的库,对jquery、qwrap和kissy都做了封装,github地址

Yahoo团队 PJAX地址

并不是页面中所有的链接都需要使用PJAX加载,所有在需要这个东西的a标签上加一个属性,如data-pjax=true,然后统一添加事件。

四、注意事项

如果浏览器不支持pushState接口函数,那就只能退化为ajax或者使用hash bang了~

本地环境下使用的话,浏览器会报错:`Uncaught SecurityError: A history state object with URL file:///E:/baidu_app/demo/PJAX/pic-2' cannot be created in a document with origin 'null'. ,所以如果你要测试的话,请把代码丢到服务器上!

为了得到更好的体验,PJAX经常配合localStorage来使用,把请求到的内容缓存到本地,再一次请求的时候先从本地查看。如果你的内容是动态变化的,缓存的时候加一个缓存时间,方便更新缓存。

还有一个容易忽略的东西是统计,使用PJAX只会局部刷新页面,如果忽略了对统计函数的更新,那就会让你失去很多数据。

五、参考资料
 
http://www.welefen.com/pjax-fo ... .html   welefen
http://ntotten.com/2012/04/09/ ... pjax/   Nathan Totten
http://yuilibrary.com/yui/docs/pjax/   YUI Pjax