【Quick 3.3】资源脚本加密及热更新(三)热更新模块
【Quick 3.3】资源脚本加密及热更新(三)热更新模块
注:本文基于Quick-cocos2dx-3.3版本编写
一、介绍
lua相对于c++开发的优点之一是代码可以在运行的时候才加载,基于此我们不仅可以在编写的时候热更新代码(针对开发过程中的热更新将在另外一篇文章中介绍),也可以在版本已经发布之后更新代码。
二、热更新模块
cocos2dx的热更新已经有很多篇文章介绍了,这里主要是基于 quick-cocos2d-x的热更新机制实现(终极版2)(更新3.3版本)基础上修改。
1、launcher模块(lua更新模块)
launcher模块的具体介绍可以看原文,不过这里的更新逻辑是稍微修改了的。
先请求服务器的launcher模块文件,如果本地launcher模块文件和服务器不同则替换新的模块再重新加载。
具体更新逻辑如流程图所示:
原文是把文件md5和版本信息都放在同一个文件里面,这里把版本信息和文件md5分成两个文件,这样的好处是不用每次都把完整的md5文件列表下载下来。除此之外还增加了程序版本号判断,优化了一些逻辑。具体代码见最后的资源链接。
2、版本文件/文件md5信息生成
原文的md5信息(flist)是通过lua代码调用引擎模块生成,但是鉴于工程太大不利于分享(其实目的只是要生成文件md5信息),所以这里把代码改成python版本的了。
注意,如果你也想要尝试把lua改成其他语言实现,你可能会发现生成的md5和lua版本的不同,这是因为lua版本将字节流转换成大写的十六进制来生成md5的。
#lua 版本
local function hex(s)
s=string.gsub(s,"(.)",function (x) return string.format("%02X",string.byte(x)) end)
return s
end
#python 版本
def toHex(s):
return binascii.b2a_hex(s).upper()
具体的python脚本代码(还是基于上个教程的脚本增加代码)
#coding=utf-8
#!/usr/bin/python
import os
import os.path
import sys, getopt
import subprocess
import shutil
import time, datetime
import platform
from hashlib import md5
import hashlib
import binascii
def removeDir(dirName):
if not os.path.isdir(dirName):
return
filelist=[]
filelist=os.listdir(dirName)
for f in filelist:
filepath = os.path.join( dirName, f )
if os.path.isfile(filepath):
os.remove(filepath)
elif os.path.isdir(filepath):
shutil.rmtree(filepath,True)
def copySingleFile(sourceFile, targetFile):
if os.path.isfile(sourceFile):
if not os.path.exists(targetFile) or(os.path.exists(targetFile) and (os.path.getsize(targetFile) != os.path.getsize(sourceFile))):
open(targetFile, "wb").write(open(sourceFile, "rb").read())
def copyFiles(sourceDir, targetDir, isAll):
for file in os.listdir(sourceDir):
sourceFile = os.path.join(sourceDir, file)
targetFile = os.path.join(targetDir, file)
if os.path.isfile(sourceFile):
if not isAll:
extName = file.split('.', 1)[1]
if IgnoreCopyExtFileDic.has_key(extName):
continue
if not os.path.exists(targetDir):
os.makedirs(targetDir)
if not os.path.exists(targetFile) or(os.path.exists(targetFile) and (os.path.getsize(targetFile) != os.path.getsize(sourceFile))):
open(targetFile, "wb").write(open(sourceFile, "rb").read())
if os.path.isdir(sourceFile):
First_Directory = False
copyFiles(sourceFile, targetFile, isAll)
def toHex(s):
return binascii.b2a_hex(s).upper()
def md5sum(fname):
def read_chunks(fh):
fh.seek(0)
chunk = fh.read(8096)
while chunk:
yield chunk
chunk = fh.read(8096)
else: #最后要将游标放回文件开头
fh.seek(0)
m = hashlib.md5()
if isinstance(fname, basestring) and os.path.exists(fname):
with open(fname, "rb") as fh:
for chunk in read_chunks(fh):
m.update(toHex(chunk))
#上传的文件缓存 或 已打开的文件流
elif fname.__class__.__name__ in ["StringIO", "StringO"] or isinstance(fname, file):
for chunk in read_chunks(fname):
m.update(toHex(chunk))
else:
return ""
return m.hexdigest()
def calMD5ForFolder(dir):
md5Dic = []
folderDic = {}
for root, subdirs, files in os.walk(dir):
#get folder
folderRelPath = os.path.relpath(root, dir)
if folderRelPath != '.' and len(folderRelPath) > 0:
normalFolderPath = folderRelPath.replace('\\', '/') #convert to / path
folderDic[normalFolderPath] = True
#get md5
for fileName in files:
filefullpath = os.path.join(root, fileName)
filerelpath = os.path.relpath(filefullpath, dir)
size = os.path.getsize(filefullpath)
normalPath = filerelpath.replace('\\', '/') #convert to / path
if IgnoreMd5FileDic.has_key(fileName): #ignode special file
continue
print normalPath
md5 = md5sum(filefullpath)
md5Dic.append({'name' : normalPath, 'code' : md5, 'size' : size})
print 'MD5 figure end'
return md5Dic, folderDic
#-------------------------------------------------------------------
def initEnvironment():
#注意:复制的资源分两种
#第一种是加密的资源,从packres目录复制到APP_RESOURCE_ROOT。加密资源的类型在PackRes.php的whitelists定义。
#第二种是普通资源,从res目录复制到APP_RESOURCE_ROOT。IgnoreCopyExtFileDic定义了不复制的文件类型(1、加密资源,如png文件;2、无用资源,如py文件)
global ANDROID_APP_VERSION
global IOS_APP_VERSION
global ANDROID_VERSION
global IOS_VERSION
global BOOL_BUILD_APP #是否构建app
global APP_ROOT #工程根目录
global APP_ANDROID_ROOT #安卓根目录
global QUICK_ROOT #引擎根目录
global QUICK_BIN_DIR #引擎bin目录
global APP_RESOURCE_ROOT #生成app的资源目录
global APP_RESOURCE_RES_DIR #资源目录
global IgnoreCopyExtFileDic #不从res目录复制的资源
global IgnoreMd5FileDic #不计算md5的文件名
global APP_BUILD_USE_JIT #是否使用jit
global PHP_NAME #php
global SCRIPT_NAME #scriptsName
global BUILD_PLATFORM #生成app对应的平台
BOOL_BUILD_APP = True
IgnoreCopyExtFileDic = {
'jpg' : True,
'png' : True,
'tmx' : True,
'plist' : True,
'py' : True,
}
IgnoreMd5FileDic = {
'.DS_Store' : True,
'version' : True,
'flist' : True,
'launcher.zip' : True,
'.' : True,
'..' : True,
}
SYSTEM_TYPE = platform.system()
APP_ROOT = os.getcwd()
APP_ANDROID_ROOT = APP_ROOT + "/frameworks/runtime-src/proj.android"
QUICK_ROOT = os.getenv('QUICK_V3_ROOT')
if QUICK_ROOT == None:
print "QUICK_V3_ROOT not set, please run setup_win.bat/setup_mac.sh in engine root or set QUICK_ROOT path"
return False
if(SYSTEM_TYPE =="Windows"):
QUICK_BIN_DIR = QUICK_ROOT + "quick/bin"
PHP_NAME = QUICK_BIN_DIR + "/win32/php.exe" #windows
BUILD_PLATFORM = "android" #windows dafault build android
SCRIPT_NAME = "/compile_scripts.bat"
else:
PHP_NAME = "php"
BUILD_PLATFORM = "ios" #mac default build ios
QUICK_BIN_DIR = QUICK_ROOT + "/quick/bin" #mac add '/'
SCRIPT_NAME = "/compile_scripts.sh"
if(BUILD_PLATFORM =="ios"):
APP_BUILD_USE_JIT = False #ios not use jit
if BOOL_BUILD_APP:
APP_RESOURCE_ROOT = APP_ROOT + "/Resources"
APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT + "/res"
else:
APP_RESOURCE_ROOT = APP_ROOT + "/server/game/cocos2dx/udp"
APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT
else:
APP_BUILD_USE_JIT = True
if BOOL_BUILD_APP:
APP_RESOURCE_ROOT = APP_ANDROID_ROOT + "/assets" #default build android
APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT + "/res"
else:
APP_RESOURCE_ROOT = APP_ROOT + "/server/game/cocos2dx/udp"
APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT
print 'App root: %s' %(APP_ROOT)
print 'App resource root: %s' %(APP_RESOURCE_ROOT)
return True
def svnUpdate():
print "1:svn update"
try:
args = ['svn', 'update']
proc = subprocess.Popen(args, shell=False, stdout = subprocess.PIPE, stderr=subprocess.STDOUT)
while proc.poll() == None:
print proc.stdout.readline(),
print proc.stdout.read()
except Exception,e:
print Exception,":",e
def packRes():
print "2:pack res files"
removeDir(APP_ROOT + "/packres/") #--->删除旧加密资源
scriptName = QUICK_BIN_DIR + "/lib/pack_files.php"
try:
args = [PHP_NAME, scriptName, '-c', 'PackRes.php']
proc = subprocess.Popen(args, shell=False, stdout = subprocess.PIPE, stderr=subprocess.STDOUT)
while proc.poll() == None:
print proc.stdout.readline(),
print proc.stdout.read()
except Exception,e:
print Exception,":",e
def copyResourceFiles():
print "3:copy resource files"
print "remove old resource files"
removeDir(APP_RESOURCE_ROOT)
if not os.path.exists(APP_RESOURCE_ROOT):
print "create resource folder"
os.makedirs(APP_RESOURCE_ROOT)
if BOOL_BUILD_APP: #copy all resource
print "copy config"
copySingleFile(APP_ROOT + "/config.json", APP_RESOURCE_ROOT + "/config.json")
copySingleFile(APP_ROOT + "/channel.lua", APP_RESOURCE_ROOT + "/channel.lua")
print "copy src"
copyFiles(APP_ROOT + "/scripts/", APP_RESOURCE_ROOT + "/src/", True)
print "copy res"
copyFiles(APP_ROOT + "/res/", APP_RESOURCE_RES_DIR, False)
print "copy pack res"
copyFiles(APP_ROOT + "/packres/", APP_RESOURCE_RES_DIR, True)
def compileScriptFile(compileFileName, srcName, compileMode):
scriptDir = APP_RESOURCE_RES_DIR + "/code/"
if not os.path.exists(scriptDir):
os.makedirs(scriptDir)
try:
scriptsName = QUICK_BIN_DIR + SCRIPT_NAME
srcName = APP_ROOT + "/" + srcName
outputName = scriptDir + compileFileName
args = [scriptsName,'-i',srcName,'-o',outputName,'-e',compileMode,'-es','XXTEA','-ek','ilovecocos2dx']
if APP_BUILD_USE_JIT:
args.append('-jit')
proc = subprocess.Popen(args, shell=False, stdout = subprocess.PIPE, stderr=subprocess.STDOUT)
while proc.poll() == None:
outputStr = proc.stdout.readline()
print outputStr,
print proc.stdout.read(),
except Exception,e:
print Exception,":",e
def compileFile():
print "4:compile script file"
compileScriptFile("game.zip", "src", "xxtea_zip") #--->代码加密
compileScriptFile("launcher.zip", "pack_launcher", "xxtea_zip") #--->更新模块加密
def writeFile(fileName, strArr):
if os.path.isfile(fileName):
print "Remove old file!"
os.remove(fileName)
#write file
f = file(fileName, 'w')
for _, contentStr in enumerate(strArr):
f.write(contentStr)
f.close()
def genFlist():
print "5: generate flist"
# flist文件格式 lua table
# key
# --> dirPaths 目录
# --> fileInfoList 文件名,md5,size
folderPath = APP_RESOURCE_RES_DIR
md5Dic, folderDic = calMD5ForFolder(folderPath)
#sort md5
sortMd5Dic = sorted(md5Dic, cmp=lambda x,y : cmp(x['name'], y['name']))
#convert folder dic to arr
folderNameArr = []
for folderName, _ in folderDic.iteritems():
folderNameArr.append(folderName)
#sort folder name
sortFolderArr = sorted(folderNameArr, cmp=lambda x,y : cmp(x, y))
#str arr generate
strArr = []
strArr.append('local flist = {\n')
#dirPaths
strArr.append('\tdirPaths = {\n')
for _,folderName in enumerate(sortFolderArr):
strArr.append('\t\t{name = "%s"},\n' % folderName)
strArr.append('\t},\n')
#fileInfoList
strArr.append('\tfileInfoList = {\n')
for index, md5Info in enumerate(sortMd5Dic):
name = md5Info['name']
code = md5Info['code']
size = md5Info['size']
strArr.append('\t\t{name = "%s", code = "%s", size = %d},\n' % (name, code, size))
strArr.append('\t},\n')
strArr.append('}\n')
strArr.append('return flist\n')
writeFile(folderPath + "/flist", strArr)
def genVersion():
print "6: generate version"
folderPath = APP_RESOURCE_RES_DIR
#str arr generate
strArr = []
strArr.append('local version = {\n')
strArr.append('\tandroidAppVersion = %d,\n' % ANDROID_APP_VERSION)
strArr.append('\tiosAppVersion = %d,\n' % IOS_APP_VERSION)
strArr.append('\tandroidVersion = "%s",\n' % ANDROID_VERSION)
strArr.append('\tiosVersion = "%s",\n' % IOS_VERSION)
strArr.append('}\n')
strArr.append('return version\n')
writeFile(folderPath + "/version", strArr)
if __name__ == '__main__':
print 'Pack App start!--------->'
isInit = initEnvironment()
if isInit == True:
#若不更新资源则直接执行copyResourceFiles和compileScript
svnUpdate() #--->更新svn
packRes() #--->资源加密(若资源如图片等未更新则此步可忽略)
copyResourceFiles() #--->复制res资源
compileFile() #--->lua文件加密
genFlist() #--->生成flist文件
ANDROID_APP_VERSION = 1 #app 更新版本才需要更改
IOS_APP_VERSION = 1 #app 更新版本才需要更改
ANDROID_VERSION = "1.0.1"
IOS_VERSION = "1.0.1"
genVersion() #--->生成version文件
print '<---------Pack App end!'
注意:这个脚本是集成代码加密、资源加密、热更新文件生成的。具体使用的时候肯定会遇到很多坑的
坑1:项目使用luajit。
热更新和luajit有点不完美适应,因为iOS的luajit是2.1beta的(iOS的坑),而其他平台是使用的是旧版本luajit,这意味着它们的更新文件不能通用,iOS和android下载服务器的加密代码要区分开,当然如果项目没有用luajit的话就没有这个烦恼了。坑2: 资源文件的位置。
android/iOS的文件引用时注意不要把未加密的代码复制进去了,上面的pyhton脚本已经帮你做了部分操作了,但是还有一些需要自己手动去改。
iOS:xcode工程注意要把原来的资源引用换成加密的资源(Mac下执行脚本会把假面资源拷贝到Resource目录下)
Android:如果你是用build_apk、build_native、build_native_release来编译的话,注意把proj.android里面的build_native_release脚本的资源复制删除语句屏蔽掉
windows:因为windows是开发的时候才用,所以是直接引用源代码的。不过你要发布windows版本的话,需要自行替换加密资源了。坑3:检查脚本是否放在正确的位置。
python脚本/PackRes.php放在工程根目录(res、src同级目录);FilesPacker.php/pack_files.php放在引擎相应目录;坑4:检查QUICK_ROOT是否已经设置。
因为脚本要用到引擎自带的加密脚本,注意Mac使用的shell命令(.sh文件)有权限执行坑5:检查参数是否正确设置。
python脚本中,APP_BUILD_USE_JIT是否使用luajit加密脚本,BOOL_BUILD_APP是否打包apk还是热更新(复制的目录不同)
3、引擎修改
因为代码已经加密,而且加入了热更新模块,所以lua的加载入口需要修改。
首先找到AppDelegate.cpp文件,加入初始化资源搜索路径initResourcePath方法,然后增加更新文件和加密文件判断。
这里有三种情况。
1:更新模式(发布版本使用)
2:加密模式(无更新,windows版本使用)
3:普通模式(无更新和无加密,开发时候使用)
void AppDelegate::initResourcePath()
{
FileUtils* sharedFileUtils = FileUtils::getInstance();
std::string strBasePath = sharedFileUtils->getWritablePath();
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)|| (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
sharedFileUtils->addSearchPath("res/");
#else
sharedFileUtils->addSearchPath("../../res/");
#endif
sharedFileUtils->addSearchPath(strBasePath + "upd/", true);
}
bool AppDelegate::applicationDidFinishLaunching()
{
#if CC_TARGET_PLATFORM == CC_PLATFORM_WIN32
initRuntime();
#elif (COCOS2D_DEBUG > 0 && CC_CODE_IDE_DEBUG_SUPPORT > 0)
// NOTE:Please don't remove this call if you want to debug with Cocos Code IDE
if (_launchMode)
{
initRuntime();
}
#endif
//add resource path
initResourcePath();
// initialize director
auto director = Director::getInstance();
auto glview = director->getOpenGLView();
if(!glview) {
Size viewSize = ConfigParser::getInstance()->getInitViewSize();
string title = ConfigParser::getInstance()->getInitViewName();
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32 || CC_TARGET_PLATFORM == CC_PLATFORM_MAC)
extern void createSimulator(const char* viewName, float width, float height, bool isLandscape = true, float frameZoomFactor = 1.0f);
bool isLanscape = ConfigParser::getInstance()->isLanscape();
createSimulator(title.c_str(),viewSize.width,viewSize.height, isLanscape);
#else
glview = cocos2d::GLViewImpl::createWithRect(title.c_str(), Rect(0, 0, viewSize.width, viewSize.height));
director->setOpenGLView(glview);
#endif
director->startAnimation();
}
auto engine = LuaEngine::getInstance();
ScriptEngineManager::getInstance()->setScriptEngine(engine);
lua_State* L = engine->getLuaStack()->getLuaState();
lua_module_register(L);
// use Quick-Cocos2d-X
quick_module_register(L);
LuaStack* stack = engine->getLuaStack();
stack->setXXTEAKeyAndSign("ilovecocos2dx", strlen("ilovecocos2dx"), "XXTEA", strlen("XXTEA"));
stack->addSearchPath("src");
FileUtils *utils = FileUtils::getInstance();
//1: try to load launcher module
const char *updateFileName = "code/launcher.zip";
std::string updateFilePath = utils->fullPathForFilename(updateFileName);
bool isUpdate = false;
if (updateFilePath.compare(updateFileName) != 0) //check if update file exist
{
printf("%s\n", updateFilePath.c_str());
isUpdate = true;
engine->executeScriptFile(ConfigParser::getInstance()->getEntryFile().c_str());
}
if (!isUpdate) //no update file
{
//2: try to load game script module
const char *zipFilename ="code/game.zip";
std::string zipFilePath = utils->fullPathForFilename(zipFilename);
if (zipFilePath.compare(zipFilename) == 0) //no game zip file use default lua file
{
engine->executeScriptFile(ConfigParser::getInstance()->getEntryFile().c_str());
}
else
{
//3: default load game script
stack->loadChunksFromZIP(zipFilename);
stack->executeString("require 'main'");
}
}
return true;
}
4、加入新的main入口(配合更新模块)
对于热更新,游戏执行后首先执行main.lua的代码,main.lua再调用launcher模块的代码,launcher根据版本情况决定接下来的逻辑。
这里的main.lua放在script目录里,执行python脚本后main.lua会复制到对应的src目录下
//main.lua
function __G__TRACKBACK__(errorMessage)
print("----------------------------------------")
print("LUA ERROR: " .. tostring(errorMessage) .. "\n")
print(debug.traceback("", 2))
print("----------------------------------------")
end
local fileUtils = cc.FileUtils:getInstance()
fileUtils:setPopupNotify(false)
-- 清除fileCached 避免无法加载新的资源。
fileUtils:purgeCachedEntries()
cc.LuaLoadChunksFromZIP("code/launcher.zip")
package.loaded["launcher.launcher"] = nil
require("launcher.launcher")
5、代码地址
https://github.com/chenquanjun/Cocos2dxEncyptAndUpdate
【Quick 3.3】资源脚本加密及热更新(三)热更新模块的更多相关文章
- 【Quick 3.3】资源脚本加密及热更新(二)资源加密
[Quick 3.3]资源脚本加密及热更新(二)资源加密 注:本文基于Quick-cocos2dx-3.3版本编写 一.介绍 在前一篇文章中介绍了代码加密,加密方式是XXTEA.对于资源文件来说,同样 ...
- 【Quick 3.3】资源脚本加密及热更新(一)脚本加密
[Quick 3.3]资源脚本加密及热更新(一)脚本加密 注:本文基于Quick-cocos2dx-3.3版本编写 一.脚本加密 quick框架已经封装好加密模块,与加密有关的文件在引擎目录/quic ...
- cocos2dx资源和脚本加密quick-lua3.3final
一.资源加密 版本号:Quick-Cocos2d-x 3.3 Final 调试工具:xCode 工程创建的时候选择的拷贝源码. 项目结构如图: 这个功能七月大神在很早之前就已经实现了,但是在3.3版本 ...
- 一键自动发布ipa(更新svn,拷贝资源,压缩资源,加密图片资源,加密数据文件,加密lua脚本,编译代码,ipa签名,上传ftp)
一键自动发布ipa(更新svn,拷贝资源,压缩资源,加密图片资源,加密数据文件,加密lua脚本,编译代码,ipa签名,上传ftp) 程序员的生活要一切自动化,更要幸福^_^. 转载请注明出处http: ...
- Unity Mono脚本 加密
加密环境 引擎版本:Unity3D 5.3.4 及更高版本 (使用Mono而并非IL2CPP) 操作系统:CentOS 6.2(Final) 加密环境:Android.IOS(暂定) 加密对象:C#源 ...
- plain framework 1 pak插件说明(资源压缩加密)
在互联网的发展中,资源的整理一般成了发布软件应用的迫在眉睫的一件事情,不经打包的资源往往容易暴露而且众多的文件使得拷贝等待时间变长.在这种情况下,一种应用便诞生了,其起源是源自压缩软件,这便是我们今天 ...
- shell 脚本加密
日常编写shell脚本时会写一些账号和密码写入脚本内,但是不希望泄露账号密码,所以对shell脚本进行加密变成可执行文件. 主要使用 shc 对 Linux shell 脚本加密,shc是一个专业的加 ...
- shell脚本加密方式
--作者:飞翔的小胖猪 --创建时间:2021年5月17日 --修改时间:2021年5月17日 说明 shell作为Linux操作系统中原生的语言环境,由于其简单.便捷.可以移植等特性常被运维人员作为 ...
- C#脚本引擎 CS-Script 之(三)——如何部署
本文不但介绍了CS-Script如何部署,还介绍了CS-Script的部署后面的原理,并用一个框图详细介绍了部署中的各种细节. 一.获取资源 1.从官网上下载编译好的csscript资源:cs-scr ...
随机推荐
- Linq 中的TakeWhile 和 SkipWhile
这两个概念容易搞混 理解了一番后 在这里写下便于记忆 SkipWhile 可以理解为如果条件满足 就一直跳过 知道不满足后 就取剩下的所有元素(后面的不会再判断) TakeWhile 可以理解为 ...
- SpringInAction读书笔记--第2章装配Bean
实现一个业务需要多个组件相互协作,创建组件之间关联关系的传统方法通常会导致结构复杂的代码,这些代码很难被复用和单元测试.在Spring中,对象不需要自己寻找或创建与其所关联的其它对象,Spring容器 ...
- node c/c++扩展模块build失败.
"深入浅出nodejs 朴灵" 例子 c/c++扩展模块 http://diveintonode.org/ 在作者的帮助下,build成功. 下面贴出的hello.cc和bindi ...
- Hdu 1042 N! (高精度数)
Problem Description Givenan integer N(0 ≤ N ≤ 10000), your task is to calculate N! Input OneN in one ...
- 在Window IIS中安装运行node.js应用—你疯了吗
[原文发表地址]Installing and Running node.js applications within IIS on Windows - Are you mad? [原文发表时间]201 ...
- 支付宝api教程,支付宝根据交易号自动充值
最近公司要用php做一个网站支付宝自动充值的功能,具体就是客户把钱直接转到公司的支付宝账号里,然后在我们网站上填写上交易号,我们网站程序自动获取交易信息,自动给网站的账户充值. 我的具体想法就是利用支 ...
- svn 相关
// svn相关内容,windows下的可以根据网上的,安装客户端和服务器端安装成功后,可以在服务器端中的 Repositories中建立相关的项目库文件夹,右键相应的文件夹可以复制相关的 url,一 ...
- 基于Hadoop生态圈的数据仓库实践 —— ETL
使用Hive转换.装载数据 1. Hive简介 (1)Hive是什么 Hive是一个数据仓库软件,使用SQL读.写.管理分布式存储上的大数据集.它建立在Hadoop之上,具有以下功能和 ...
- iOS之内存管理浅谈
1.何为ARC ARC是automatic reference counting自动引用计数,在程序编译时自动加入retain/release.在对象被创建时retain count+1,在对象被re ...
- Angular 动态生成html中 ng-click无效
bodyApp.controller('customersCtrl', function ($scope, $http, cfpLoadingBar,$compile) { $scope.test = ...