主要面向Web前端工程师,需要一定Javascript及three.js基础;
本文主要分享内容为基于three.js开发WebVR思路及碰到的问题;
有兴趣的同学,欢迎跟帖讨论。

目录:
一、项目体验
1.1、项目简介
1.2、功能介绍
1.3、游戏体验
二、技术方案
2.1、为什么使用WebVR
2.2、常用的WebVR解决方案
2.2.1、Mozilla的A-Frame方案
2.2.2、three.js及webvr-polyfill方案
三、技术实现
3.1、知识储备
3.2、实现步骤
3.3、工作原理
四、技术难点
4.1、程序与用户共同控制摄像头
4.2、多重蒙板贴图
4.3、镜头移动
4.4、3d自适应长度文字提示
4.5、unity3d地形导出
4.6、3dmax动画导出问题
五、完整的源代码及相应组件

一、项目体验
1.1、项目简介:
1.1.1、名称:
“重历阿尔特里亚”——龙之谷手游手首发ChinaJoy2016预热VR小游戏

1.1.2、开发背景:
基于龙之谷手游具备的3D属性,全景视角体验,以及ChinaJoy首发的线下场景,我们和品牌讨论除了基于VR的线下体验项目。由于基于Web技术较好的兼容性、开发的高效性,我们采用了WebVR技术来实现整个体验。

1.1.3、使用WebVR优势:
1.1.3.1、普通web前端工程师可以参与VR应用开发,降低了开发门槛;
1.1.3.2、跨设备终端、跨操作系统、跨APP载体;
1.1.3.3、开发快速、维护方便、随时调整、传播便捷;
1.1.3.4、浏览器即可体验,无需安装。

1.2、功能介绍
基于游戏内3D场景、人物和道具模型,通过WebGL框架three.js开发的VR小游戏,在ChinaJoy龙之谷手游展台给玩家提供线下VR互动体验,并在后续应用于线上营销传播。不具备VR眼镜设备的用户可选择普通模式进行互动体验。

1.3、游戏体验
如果你身边正好有VR眼镜,请选择VR模式体验;如果没有,请选择普通模式。
需要说明的是,由于本次应用针对线下场景,而合作方三星提供了最新的S7手机和GearVR设备,所以项目只针对S7做了体验优化,所以可能部分手机会有卡顿或者3D模型错乱的情况。

你可以扫描如下二维码或打开http://dn.qq.com/act/vr/进行体验:

二、技术方案
2.1、为什么是时候尝试WebVR了?
2.1.1、时机慢慢成熟,我们通过几件事件即可感知:
2015年初,Mozilla在firefox nightly增加了对WebVR的支持;
2015年底,MozVR团队推出开源框架A-Frame,能过HTML标签,即可创建VR网页;
2015年底,Egret3D发布,开发团队称将在以后版本中实现WebVR的支持;
2016年初,Google与Mozilla联合创建WebVR标准;
2016年6月,Google计划将整个Chrome浏览器搬进VR世界中。
2.1.2、WebVR开发成本更低。
2015年VR硬件迅速发展,但时至今日,VR内容还是稍显单薄。原因在于,VR开发成本过高,而WebVR依托于WebGL及类似threeJS等框架,大大降低开发者进入VR领域的门槛。
2.1.3、Web自身的优势
上文中已有提及,依托也Web,具有不需安装、便于传播、便于快速迭代等特点。

2.2、目前阶段,常用的WebVR解决方案:
2.2.1、A-frame
介绍:Mozilla的开源框架,通过定制HTML元素即可构建WebVR方案的框架,适用于没有webGL与threeJS基础的初学者。
优点:基于threeJS的封装,通过特定的标签就能够快速创建VR网页;
缺点:所提供的组件有限,难以完成较复杂的项目。
实例:
2.2.1.1、创建一个简单的场景。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="description" content="Composite — A-Frame">
<script src="../aframe.js"></script>
</head>
<body>
<a-scene>
<!-- 环境光. -->
<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity position="0 2.2 4">
<!-- 添加相机 -->
<a-entity camera look-controls wasd-controls>
<!-- 添加圆环 -->
<a-entity cursor
geometry="primitive: ring; radiusOuter: 0.015; radiusInner: 0.01; segmentsTheta: 32" material="color: #283644; shader: flat" raycaster="far: 30" position="0 0 -0.75"></a-entity>
</a-entity>
</a-entity>
</a-scene>
</body>
</html>

源码讲解:
如上简单的几个标签,即可构建一个包含灯光、相机、跟随相机的物体的场景,其余事情,都将由A-frame进行解析,具体标签与属性不多作讲解,可以参考 A-frame DOC

2.2.1.1、加载一个由软件(比如3dmax)导出的模型。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="description" content="Composite — A-Frame">
<script src="../aframe.js"></script>
<script>
AFRAME.registerComponent('json-model', {
schema: {
type: 'src'
},
init: function () {
this.loader = new THREE.JSONLoader();
},
update: function () {
var mesh = this.el.getOrCreateObject3D('mesh', THREE.Mesh);
this.loader.load(this.data, function (geometry) {
mesh.geometry = geometry;
});
}
});
</script>
</head>
<body>
<a-scene>
<a-assets>
<a-asset-item id="sculpture" src="data/building-ground.js"></a-asset-item>
</a-assets>
<a-entity id="car" json-model="#sculpture" position="0 0 0" scale="5 5 5" rotation="0 45 0" material="src: url(cross-domain/skin/xianxiasq_zhujianqiangmian_001.png)"></a-entity>
</a-scene>
</body>
</html>

源码讲解:
这个例子主要演示,A-Frame如何添加组件,对,因为A-Frame现阶段组件太少,加载自定义模式需要自己扩展组件。而组件添加需要three.js基础。
so,A-Frame出发点是非常美好的,学习几个简单的标签及属性,即可以搭建3d/webvr场景,但是现实却是目前它还并不成熟,并且伴随着A-Frame主设计师跳槽到Google,所以我很早就放弃这个方案了。

2、基于threeJS与webVR组件,事实上,A-frame就是基于这两者的封装。
优点:可以完成复杂项目,可以结合原生的webGL;
缺点:需要掌握threeJS,需要了解webGL,学习成本较高。

在本项目中,选用的就是这个方案,在下章节中,将会进行详细介绍。

三、技术实现
3.1、知识储备:
three.js(掌握)、webGL(了解)、javascript
对three.js没有基础的同学,可以移步至 Three.js实例教程

3.2、实现步骤:
简单来说,完成一个WebVR应用,需要以下三个步骤:
3.2.1、搭建场景

如上图与示:
首先我们需要载入我们的资源,这些资源包括地形、角色、动画、及辅助元素;
然后创建我们需要的元素,比如灯光、相机、天空等;
然后完成主业务逻辑。

3.2.2、交互
即用户的动作输入,这些动作包括:
位置移动、旋转、视线焦点、声音、甚至全身所有关节动作。
当然,当前我们可利用的硬件设备有限,手机自身可利用的如陀螺仪、罗盘、听筒。其余辅助设备常用如Leap Motion、Kinect等。
更多的额外设备意识着更高的使用成本,在本案例中使用的到的动作输入信息:
用户当前方向,由VRControls.js与webvr-polyfill.js实现完成;
用户视角焦点,完成按钮点击、攻击等动作,通过跟随相机的物体检测碰撞来完成。

3.2.3、分屏

如上图所示,为让用户更具沉侵感,通常会根据用户瞳距将屏幕分割成具有一定视差的两部分,勿需担心,这部分工作由VREffect.js来完成。

3.3、工作原理
上节中提到了webvr相关组件,本来我们可以简单利用它提供的接口就可以完成,但肯定还是有同学会好奇,它的工作原理是怎样的呢。

这得从Mozilla与Google 2016年初联手推出的WebVR API提案开始,WebVR Specification,该提案给VR硬件定义了专门定制的接口,让开发者能够构建出沉浸感强,舒适度高的VR体验。但由于该标准还处于草案阶段,所以我们开发需要WebVR Polyfill,这个组件不需要特定浏览器,就可以使用WebVR API中的接口。
所以我们只需要在项目中,引入webvr-polyfill.js及VRControls、VREffect两个类,并调用即可。

vrEffect = new THREE.VREffect(renderer);
vrControls = new THREE.VRControls(camera);

webvr-polyfill基于普通浏览器实现了WebVR API 1.0功能;
VRControls更新摄像头信息,让用户以第一人称置于场景中;
VREffect负责分屏。

四、技术难点

4.1、程序与用户共同控制摄像头
当程序在自动移动镜头的过程中,允许用户四处观察,这时候需要一个辅助容器共同控制镜头旋转与移动。

// 添加摄像机
camera = new THREE.PerspectiveCamera(60, size.w / size.h, 1, 10000);
camera.position.set(0, 0, 0);
camera.lookAt(new THREE.Vector3(0,0,0)); // 辅助镜头移动
dolly = dolly = new THREE.Group();
dolly.position.set(10, 40, 40);
dolly.rotation.y = Math.PI/10;
dolly.add(camera);
scene.add(dolly);

4.2、多重蒙板贴图

如上图所示,该地形由三种贴图通过蒙板共同合成,这时候我们需要使用自定义Shader来实现,由rbg三个通道控制显示。
核心代码(片元着色器):

fragmentShader: [
'uniform sampler2D texture1;',
'uniform sampler2D texture2;',
'uniform sampler2D texture3;',
'uniform sampler2D mask;',
'void main() {',
'vec4 colorTexture1 = texture2D(texture1, vUv* 40.0);',
'vec4 colorTexture2 = texture2D(texture2, vUv* 60.0);',
'vec4 colorTexture3 = texture2D(texture3, vUv* 20.0);',
'vec4 colorMask = texture2D(mask, vUv);',
'vec3 outgoingLight = vec3( colorTexture1.rgb*colorMask.r + colorTexture2.rgb *colorMask.g + colorTexture3.rgb *colorMask.b ) * 0.6;',
'gl_FragColor = vec4(outgoingLight, 1.0);',
'}'
].join("\n")

完整代码(添加three.js灯光,雾化):

// 合成材质
var map1 = texLoader.load('cross-domain/skins/foor_stone02.png' );
var map2 = texLoader.load('cross-domain/skins/green_wet09.png');
var map3 = texLoader.load('cross-domain/skins/stone_dry02.png'); // 自定义复合蒙板shader
THREE.FogShader = {
uniforms: lib.extend( [ THREE.UniformsLib[ "fog" ],
THREE.UniformsLib[ "lights" ],
THREE.UniformsLib[ "shadowmap" ],
{
'texture1': { type: "t", value: map1},
'texture2': { type: "t", value: map2},
'texture3': { type: "t", value: map3},
'mask': { type: "t", value: texLoader.load('cross-domain/skins/mask.png')}
}
] ),
vertexShader: [
"varying vec2 vUv;",
"varying vec3 vNormal;",
"varying vec3 vViewPosition;", THREE.ShaderChunk[ "skinning_pars_vertex" ],
THREE.ShaderChunk[ "shadowmap_pars_vertex" ],
THREE.ShaderChunk[ "logdepthbuf_pars_vertex" ], "void main() {", THREE.ShaderChunk[ "skinbase_vertex" ],
THREE.ShaderChunk[ "skinnormal_vertex" ], "vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );", "vUv = uv;",
"vNormal = normalize( normalMatrix * normal );",
"vViewPosition = -mvPosition.xyz;", "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
THREE.ShaderChunk[ "logdepthbuf_vertex" ],
"}"
].join('\n'), fragmentShader: [
'uniform sampler2D texture1;',
'uniform sampler2D texture2;',
'uniform sampler2D texture3;',
'uniform sampler2D mask;', 'varying vec2 vUv;',
'varying vec3 vNormal;',
'varying vec3 vViewPosition;',
// "vec3 outgoingLight = vec3( 0.0 );",
THREE.ShaderChunk[ "common" ],
THREE.ShaderChunk[ "shadowmap_pars_fragment" ],
THREE.ShaderChunk[ "fog_pars_fragment" ],
THREE.ShaderChunk[ "logdepthbuf_pars_fragment" ], 'void main() {',
THREE.ShaderChunk[ "logdepthbuf_fragment" ],
THREE.ShaderChunk[ "alphatest_fragment" ], 'vec4 colorTexture1 = texture2D(texture1, vUv* 40.0);',
'vec4 colorTexture2 = texture2D(texture2, vUv* 60.0);',
'vec4 colorTexture3 = texture2D(texture3, vUv* 20.0);',
'vec4 colorMask = texture2D(mask, vUv);', 'vec3 normal = normalize( vNormal );',
'vec3 lightDir = normalize( vViewPosition );', 'float dotProduct = max( dot( normal, lightDir ), 0.0 ) + 0.2;', 'vec3 outgoingLight = vec3( colorTexture1.rgb*colorMask.r + colorTexture2.rgb *colorMask.g + colorTexture3.rgb *colorMask.b ) * 0.6;', THREE.ShaderChunk[ "shadowmap_fragment" ],
THREE.ShaderChunk[ "linear_to_gamma_fragment" ],
THREE.ShaderChunk[ "fog_fragment" ], // 'gl_FragColor = vec4( colorTexture1.rgb*colorMask.r + colorTexture2.rgb *colorMask.g + colorTexture3.rgb *colorMask.b, 1.0 ) + vec4(outgoingLight, 1.0);',
// 'gl_FragColor = outgoingLight;',
'gl_FragColor = vec4(outgoingLight, 1.0);',
'}'
].join("\n") };
THREE.FogShader.uniforms.texture1.value.wrapS = THREE.FogShader.uniforms.texture1.value.wrapT = THREE.RepeatWrapping;
THREE.FogShader.uniforms.texture2.value.wrapS = THREE.FogShader.uniforms.texture2.value.wrapT = THREE.RepeatWrapping;
THREE.FogShader.uniforms.texture3.value.wrapS = THREE.FogShader.uniforms.texture3.value.wrapT = THREE.RepeatWrapping; var material = new THREE.ShaderMaterial({
uniforms : THREE.FogShader.uniforms,
vertexShader : THREE.FogShader.vertexShader,
fragmentShader : THREE.FogShader.fragmentShader,
fog: true
});

3、 镜头移动(依赖Tween类)
功能函数:

cameraTracker: function(paths){
var tweens = [];
for(var i = 0; i < paths.length; i++) {
(function(i){
var tween = new TWEEN.Tween({pos: 0}).to({pos: 1}, paths[i].duration || 5000);
tween.easing(paths[i].easing || TWEEN.Easing.Linear.None);
tween.onStart(function(){
var oriPos = dolly.position;
var oriRotation = dolly.rotation;
this.oriPos = {x: oriPos.x, y: oriPos.y, z: oriPos.z};
this.oriRotation = {x: oriRotation.x, y: oriRotation.y, z: oriRotation.z};
});
tween.onUpdate(paths[i].onupdate || function(){
if(paths[i].pos) {
dolly.position.x = this.oriPos.x + this.pos * (paths[i].pos.x - this.oriPos.x);
dolly.position.y = this.oriPos.y + this.pos * (paths[i].pos.y - this.oriPos.y);
dolly.position.z = this.oriPos.z + this.pos * (paths[i].pos.z - this.oriPos.z);
}
if(paths[i].rotation) {
dolly.rotation.x = this.oriRotation.x + this.pos * (paths[i].rotation.x - this.oriRotation.x);
dolly.rotation.y = this.oriRotation.y + this.pos * (paths[i].rotation.y - this.oriRotation.y);
dolly.rotation.z = this.oriRotation.z + this.pos * (paths[i].rotation.z - this.oriRotation.z);
}
});
tween.onComplete(function(){
paths[i].fn && paths[i].fn();
var fn = tweens.shift();
fn && fn.start();
});
tweens.push(tween);
})(i);
}
tweens.shift().start();
}

调用:

lib.cameraTracker([
{'pos': { x: -45,y: 5, z: -38},'rotation': {x: 0, y: -1.8, z: 0}, 'easing': TWEEN.Easing.Cubic.Out,'duration':4000}
]);

4、自适应长度文字提示
根据文字长度生成canvas作为贴图到Sprite对象。

hint = function(text, type, posY, fadeTime){
var chinense = text.replace(/[u4E00-u9FA5]/g, '');
var dbc = chinense.length;
var sbc = text.length - dbc;
var length = dbc * 2 + sbc;
var fontsize = 40;
var textWidth = fontsize* length / 2;
posY = posY || 0.3;
type = type || 1;
fadeTime = fadeTime === window.undefined ? 500 : fadeTime; if(text == 'sucess' || text == 'fail') {
text = ' ';
} var canvas = document.createElement("canvas");
var width = 1024, height = 512;
canvas.width = width;
canvas.height = height;
var context = canvas.getContext('2d'); var imageObj = document.querySelector('#img-hint-' + type); context.drawImage(imageObj, width/2 - imageObj.width/2, height/2 - imageObj.height/2);
context.font = 'Bold '+ fontsize +'px simhei';
context.fillStyle = "rgba(255,255,255,1)";
context.fillText(text, width/2-textWidth/2, height/2+15); var texture = new THREE.Texture(canvas);
texture.needsUpdate = true; var mesh;
var material = new THREE.SpriteMaterial({
map: texture,
transparent: true,
opacity: 0
});
mesh = new THREE.Sprite(material);
mesh.scale.set(width/400, height/400, 1);
mesh.position.set(0, posY, -3);
camera.add(mesh); var tweenIn = new TWEEN.Tween({pos: 0}).to({pos: 1}, fadeTime);
tweenIn.onUpdate(function(){
material.opacity = this.pos;
});
if(fadeTime === 0) {
material.opacity = 1;
} else {
tweenIn.start();
} var tweenOut = new TWEEN.Tween({pos: 1}).to({pos: 0}, fadeTime);
tweenOut.onUpdate(function(){
material.opacity = this.pos;
});
tweenOut.onComplete(function(){
camera.remove(mesh);
});
tweenOut.fadeOut = tweenOut.start;
tweenOut.remove = function(){
camera.remove(mesh);
} return tweenOut;
};

5、unity地形导出
5.1、首先将unity地形导出为obj

5.2、然后导入3dmax,使用ThreeJSExporter.ms导出为js格式。

6、3dmax动画导出问题
6.1、动画导出错误
通常是对象为可编辑多边形,需要转换成网格对象。

操作步骤:
6.1.1、选择对象,右键转换为可编辑网络;
6.1.2、选择蒙皮修改器,重新蒙皮;
6.1.3、点击蒙皮修改器下的骨骼 > 添加,添加原有的骨骼。
6.2、动画导出错乱
很容易让人以为是权重出问题了,但就我自己多个项目动画导出的经验来看,大部分出现在骨骼添加上。在3dmax及unity中,不添加根节点往往不影响动画执行,但导出到three.js,需要添加根节点。如果问题还存在,则仔细观察是哪个骨骼引起的,多余骨骼或缺少骨骼都可能引起动画错乱。

五、完整的源代码及相应组件
点击下载
main.js - 完整的源代码
tween.min.js - 动画类
OrbitControls.js - 视图控制器,旋转、移动、缩放场景,方便调试
audio.min.js - motion音频组件,解决自动播放音频问题
其余vr相关组件上文已有介绍

龙之谷手游WebVR技术分享的更多相关文章

  1. 手游开发者大会交流OGEngine新版本发布

    由OGEngine第二个手游开发者交流会举办的圆桌会议在深圳市高新技术园区举行.发布会不仅吸引了手游开发商,供应商也纷纷在国外支付,在国内手游和国外出版商参加. 围绕三个主题会议讨论和交流.每个主题: ...

  2. HMS Core Discovery第13期回顾长文——构建手游中的真实世界

    HMS Core Discovery第13期直播<来吧!构建手游中的真实世界>,已于2月24日圆满结束,本期直播我们同三七游戏的专家一同向小伙伴们分享了HMS Core图形引擎服务(Sce ...

  3. 如何缩减手游app安装包的大小?

    包体过大对手游的影响更是诟病已久,有具体数据证明,游戏包体越大,在游戏运营推广过程中游戏用户的转化率就越低:反之,游戏包体越小,游戏用户的下载转化率就越高(如下图),所有的手机app.游戏在大版本更新 ...

  4. 日新进用户200W+,解密《龙之谷》手游背后的压测故事

    2017年3月,腾讯正式于全平台上线了<龙之谷>手游,次日冲到了App Store畅销排行第二的位置,并维持到了现在.上线当日百度指数超过40万,微信游戏平台数据显示预约数780多万,而据 ...

  5. Cocos2d-x手游技术分享(1)-【天天打蚊子】数据存储与音效篇

    前言: 手游项目<天天打蚊子>终于上线,特地写几篇技术分享文章,分享一下其中使用到的技术,其中使用cocos2d-x引擎,首选平台iOS,也请有iPhone或者iPad的朋友帮忙下载好评. ...

  6. 真正从0开始用Unity3D制作类战地2玩法的类龙之谷、王者荣耀的手游(暨全平台游戏)

    如题,(从2017年10月18日开始)正在利用业余时间研发一款神泣Shaiya2手游,引擎用Unity3D. 原因主要有2点: 对神泣太多感情,希望能做点什么来纪念乃至留下神泣这款网游: 时机已到,是 ...

  7. 手游录屏直播技术详解 | 直播 SDK 性能优化实践

    在上期<直播推流端弱网优化策略 >中,我们介绍了直播推流端是如何优化的.本期,将介绍手游直播中录屏的实现方式. 直播经过一年左右的快速发展,衍生出越来越丰富的业务形式,也覆盖越来越广的应用 ...

  8. 【腾讯Bugly干货分享】手游热更新方案xLua开源:Unity3D下Lua编程解决方案

    本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:http://mp.weixin.qq.com/s/2bY7A6ihK9IMcA0bOFyB-Q 导语 xL ...

  9. UWP 手绘视频创作工具技术分享系列

    开篇先来说一下写这篇文章的初衷. 初到来画,通读了来画 UWP App 的代码,发现里面确实有很多比较高深的技术点,同时也是有很多问题的,扩展性,耦合,性能,功能等等.于是我们决定从头重构这个产品,做 ...

随机推荐

  1. Scrapy框架爬虫初探——中关村在线手机参数数据爬取

    关于Scrapy如何安装部署的文章已经相当多了,但是网上实战的例子还不是很多,近来正好在学习该爬虫框架,就简单写了个Spider Demo来实践.作为硬件数码控,我选择了经常光顾的中关村在线的手机页面 ...

  2. ASP.NET Core 1.1.0 Release Notes

    ASP.NET Core 1.1.0 Release Notes We are pleased to announce the release of ASP.NET Core 1.1.0! Antif ...

  3. TDD在Unity3D游戏项目开发中的实践

    0x00 前言 关于TDD测试驱动开发的文章已经有很多了,但是在游戏开发尤其是使用Unity3D开发游戏时,却听不到特别多关于TDD的声音.那么本文就来简单聊一聊TDD如何在U3D项目中使用以及如何使 ...

  4. 07. Web大前端时代之:HTML5+CSS3入门系列~H5 地理位置

    Web大前端时代之:HTML5+CSS3入门系列:http://www.cnblogs.com/dunitian/p/5121725.html 源码:https://github.com/duniti ...

  5. MVC Core 网站开发(Ninesky) 2.1、栏目的前台显示(补充)

    在2.1.栏目的前台显示中因右键没有添加视图把微软给鄙视了一下,后来有仔细研究了一下发现应该鄙视自己,其实这个功能是有的,是自己没搞清楚乱吐糟. 其实只要在NuGet中安装两个包(Microsoft. ...

  6. Android Notification 详解——基本操作

    Android Notification 详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 前几天项目中有用到 Android 通知相关的内容,索性把 Android Notificatio ...

  7. OEL上使用yum install oracle-validated 简化主机配置工作

    环境:OEL 5.7 + Oracle 10.2.0.5 RAC 如果你正在用OEL(Oracle Enterprise Linux)系统部署Oracle,那么可以使用yum安装oracle-vali ...

  8. 【架构设计】分布式文件系统 FastDFS的原理和安装使用

    本文地址 分享提纲: 1.概述 2. 原理 3. 安装 4. 使用 5. 参考文档 1. 概述 1.1)[常见文件系统] Google了一下,流行的开源分布式文件系统有很多,介绍如下:   -- mo ...

  9. IT持续集成之质量管理

    研发工具生态 质量相关工作 一次编译产出测试包与上线包 !从源头保证版本的⼀一致性!代码质量控制! 全⽅方位的⾃自动化测试体系保证! 提测冒烟效率! 全⾃自动上线流程杜绝⼈人⼯工犯错! 生产环境应⽤用 ...

  10. Android中的多线程断点下载

    首先来看一下多线程下载的原理.多线程下载就是将同一个网络上的原始文件根据线程个数分成均等份,然后每个单独的线程下载对应的一部分,然后再将下载好的文件按照原始文件的顺序"拼接"起来就 ...