-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.json
1 lines (1 loc) · 530 KB
/
index.json
1
[{"content":"1.墨菲定律:越怕什么,越发生什么\n推论:越害怕什么?就越准备什么,那样发生什么都不需要害怕\n2.吉德林法则:把问题清楚记下来,就能解决一半问题\n推论:问题越清楚,解决越彻底\n3.吉尔伯特法则:工作上的危机信号,就是没有人会告诉你工作要怎么做\n推论:有人告诉你怎么做的时候,不一定就是对的\n4.福克兰定律:不用做决定时,就不要做决定\n推论:不变则损害利益时,就一定要做决定\n5.沃尔森定律:想变现,就要把信息和情报放首位\n推论:各行各业,把握第一手热点,结合相应的商品,就能打造强大IP\n6.福尔摩斯原则:除去所有不可能的因素,剩下的,无论你多么不愿意去相信,但它就是事实的真相\n推论:真相(目标)只有一个\n7.德鲁克管理思维:用反馈发现优势,用优势解决短处难,用长处做事,每次专注一件事\n推论:每日专注不断开发更多优势、增强长处\n8.巴菲特定律:要到竞争对手少的地方投资\n推论:投资用于有长远价值且少竞争对手的地方\n9.吉格勒定理:设定高目标等于达到目标的一部分\n推论:起点越高,得到更多\n10.叶杜二氏法则:动机越强,越高效,越成功\n推论:动机强的事先做\n11.吸引力法则:专注什么,就会吸引什么\n推论:专注自强,强者成堆;专注自卑,弱者自损\n12.快鱼法则:快慢决定成败\n推论:大吃小,不如快吃慢\n13.复利思维:做任何事,每做一次都要优化一次,同时能给其他事带来了增幅效果\n推论:把手上的事尽可能优化,并使其能对其它事件带来增幅效果,即能逆转人生\n14.无限猴子定理:让一只猴子在打字机上随机地按键,当按键时间达到无穷时,几乎必然能够打出任何给定的文字,比如莎士比亚的全套著作\n推论1:当明确条件下的尝试,方向会越明确,越来越接近成功\n推论2:漫无目的下的尝试,只会增加试错成本,且与成功越来越远\n15.因果定律:事出必有因,事做必有果\n推论:不做就失败,做错也成功,做对更成功\n16.剃刀定律(化繁为简思维):勿增派资源,去做一件本来用极少资源就能做好的事\n推论:不要把时间浪费于机械重复,来做好一件事;而是善用更少的时间且不重复来做好一件事\n17.破窗效应: 有问题出现,必当下解决或补救或矫正。\n推论:问题就是方法,及时发现问题,就会发现方法\n有兴趣可以了解一下怎么提高分析力和能力。\n\n18.坏苹果法则:态度决定结果\n推论:牺牲小我,无法大我;成全大我,必先小我\n19.罗伯特定理:自己不打败自己,则天下无敌\n推论:别人说你错,你就大声说对方大错特错\n20.卡蒂埃定理:只想一条路走到尽头,尽头就是绝路\n推论:前方无路,路在脚下\n21.能事相关定律:能者多劳,劳多者能;能者少劳,劳少者退。\n推论:能力越大,事情越多,化繁为简,只是规则化\n22.普希尔定律:再好的决策经不起拖延\n推论1:任何事满足实行的最佳条件,必须不顾一切马上行动\n推论2:男女婚嫁都要捉住自己高光时刻,不然后悔莫及\n23.费汀斯格法则:任何事有10%是人们无能为力,剩下的90%却能扭转乾坤。\n推论:不论遇到任何坏事,都可以变成一件好事,这也是富人最强大的思维之一。\n知识总结收藏夹:\nAI知识:→→AI大千宇宙←←\n小说:→→网络小说规律总结←←\n诗词研究:→→诗词歌行←←\n汉字解构:→→说甲玩字←←\n","description":"","id":2,"section":"posts","tags":null,"title":"顶级思维","uri":"https://yichenlove.github.io/posts/zhihu-doc/"},{"content":"抽象概念 抽象:笼统,模糊,看不懂,不明白\n抽象类的特点: 1.如果一个类中只存在着只有方法声明但是没有方法体(没有实现)。那么这个方法就是抽象方法。而方法所在的类一定是抽象类,这时候他们都需要abstract来修饰。\n2.抽象类能不能被实例化?抽象类不能被实例化(创建对象),因为抽象方法中方法没实现。\n3.抽象类必须由他的子类覆盖(重写)了抽象类中的所有抽象方法后,才可以实例化他的子类。子类必须实现所有的抽象方法\n抽象类的细节: 1.抽象类中有没有构造函数?有,用于创建子类对象初始化父类用的。\n2.抽象类中可以不定义抽象方法吗?可以。\n3.abstract关键字不能和那些关键字共存? private sealedstatic\n抽象类和普通类的区别? 相同点:\n 抽象类和普通类有事用来描述事物的,都可以在类的内部定义任何成员(方法,属性,字段,构造函数)。 不同点:\n 普通类有足够的信息去描述事物,抽象类描述食物可能信息不足\n 普通类中不能定义抽象方法,只能定义非抽象的;抽象类两种皆可定义。\n 普通类可以被实例化:抽象类不能被实例化\n 抽象类一定是个父类吗? 是的,抽象类只有被子类继承后覆盖其所有的方法后才能实例化其子类对象\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 abstract classAnimal { public int a = 3; public abstract void Eat(); } class Dog: Animal //子类继承抽象类 { publicoverridevoidEat()//重新实现Eat方法 { Console.WriteLine(\u0026#34;啃骨头\u0026#34;+ a); } } class Demo { static viodMain(string[]args) { newDog().Eat(); //只有实例化子类才可以调用方法 } } 抽象类的特点: 抽象类和抽象方法必须用abstract关键字来修饰。\n抽象方法只有方法声明,没有方法体,定义在抽象类中。格式:修饰符abstract返回值类型函数名(参数列表)。\n抽象类不可以被实例化,也就是不可以用new创建对象。因为抽象类是具体事物抽取出来的,本身是不具体的,没有对应的实例。例如:犬科是一个抽象的概念,真正存在的是狼和狗。 抽象类通过其子类实例化,而子类需要覆盖(重写)抽象类中所有的抽象方法后才可以创建对象,否则该子类也是抽象类。 抽象类里面可以有非抽象方法。\n抽象类与普通类的区别: 抽象类声明时要使用abstract关键字来定义,而普通类不需要。\n 抽象类里的抽象方法不能有方法主体,只能有方法的声明。\n 抽象类被继承时、子类必须重新实现它的所有抽象方法,而普通类不需要。\n 抽象类里面大多数情况下要有抽象方法,而普通类里面一定没有抽象方法。\n 使用场景:\n 当父类中的方法不知道如何实现的时候,就可以考虑将父类写成抽象类,将方法写成抽象方法,\n 如果父类中的方法有默认实现,并且父类需要被实例化,这时可以考虑将父类定义成为一个普通类,用虚方法实现多态\n 如果父类中的方法没有默认实现,父类也不需要被实例化,则可将该类定义为抽象类\n ","description":"","id":3,"section":"posts","tags":["c#"],"title":"C# 之 抽象类简介","uri":"https://yichenlove.github.io/posts/csharp-abstract/"},{"content":"1.NPR基础概念 NPR 是 Non-Photorealistic Rendering 的简称,也就是图形渲染中的非真实感渲染,常见的 NPR 渲染包括卡通渲染、油画渲染、像素感渲染、素描画、水墨画等类型。\n卡通渲染 是非真实感渲染中应用最广的渲染技术,在游戏和影视领域都是非常常见的。它主要是通过简化并剔除画面原本所包含的混杂部分,给人以独特的感染力和童趣,通常来说卡通渲染有4个要素 轮廓描边、色阶、高光、边缘光。\n实现卡通渲染需要经过轮廓描边、色阶、高光、边缘光处理得到。\n 2.实现轮廓描边 渲染轮廓线的方式有很多种, 在这里带大家熟悉其中最简单的一种, 对物体做两次渲染, 第二次渲染时开启正面剔除,将顶点沿法线向外延深一段距离,(放大物体),实现轮廓线,这里就用到我们之前提到的多Pass渲染。\n首先在Properties语块中声明轮廓线相关的两个属性,方便我们进行之后的调整\n1 2 3 4 5 6 7 Properties { _MainTex(\u0026#34;Texture\u0026#34;,2D) = \u0026#34;white\u0026#34; {} _OutlineWidth (\u0026#34;Outline Width\u0026#34;, Range(0.01, 1)) = 0.01 _OutLineColor (\u0026#34;OutLine Color\u0026#34;, Color) = (0.5,0.5,0.5,1) } 新增一个Pass来做轮廓线的渲染\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 Shader \u0026#34;Unlit/ToonShader\u0026#34; { Properties { _MainTex (\u0026#34;Texture\u0026#34;, 2D) = \u0026#34;white\u0026#34; {} _OutlineWidth (\u0026#34;Outline Width\u0026#34;, Range(0.01, 1)) = 0.01 _OutLineColor (\u0026#34;OutLine Color\u0026#34;, Color) = (0.5,0.5,0.5,1) } SubShader { Tags { \u0026#34;RenderType\u0026#34;=\u0026#34;Opaque\u0026#34; } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include \u0026#34;UnityCG.cginc\u0026#34; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } Pass {\t// 开启前向剔除 表示剔除前面 只显示背面 Cull Front CGPROGRAM #pragma vertex vert #pragma fragment frag // 线条宽度 float _OutlineWidth; // 线条颜色 float4 _OutLineColor; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; // 法线 float3 normal : NORMAL; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; // 顶点沿着法线方向外扩(放大模型) float4 newVertex = float4(v.vertex.xyz + v.normal * _OutlineWidth * 0.01 ,1); // UnityObjectToClipPos(v.vertex) 将模型空间下的顶点转换到齐次裁剪空间 o.vertex = UnityObjectToClipPos(newVertex); return o; } half4 frag(v2f i) : SV_TARGET { // 返回线条色彩 return _OutLineColor; } ENDCG } } } 3.实现色阶 色阶来决定画面色彩的丰富度饱满度精细度,而大部分卡通渲染习惯降低色阶,用简单的明暗关系来描述世界,使画面扁平又不失层次感。还需要使用half Lambert光照模型让它明暗分明一点,看起来卡通一点。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 得到顶点法线 float3 normal = normalize(i.worldNormal); // 得到光照方向 float3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos); // NoL代表表面接受的能量大小 float NoL = dot(i.worldNormal, worldLightDir); // 计算half-lambert亮度值 float halfLambert = NoL * 0.5 + 0.5; // 通过亮度值计算线性ramp float ramp = linearstep(_RampStart, _RampStart + _RampSize, halfLambert); float step = ramp * _RampStep; // 使每个色阶大小为1, 方便计算 float gridStep = floor(step); // 得到当前所处的色阶 float smoothStep = smoothstep(gridStep, gridStep + _RampSmooth, step) + gridStep; ramp = smoothStep / _RampStep; // 回到原来的空间 // 得到最终的ramp色彩 float3 rampColor = lerp(_DarkColor, _LightColor, ramp); rampColor *= col; 4.实现高光 half lambert 是漫反射,没有考虑高光,我们需要用blinnphone来做镜面反射。\n用相机的位置减去世界位置得到视向量,也就是当前物体表面指向摄像机的方向,由于反射不太好算,所以这里通过 视向量 和 光照方向 得到角平分线,也就是半程向量。通过 法线方向 点乘 半程向量 就可以得到 法线 和 半程向量 的 夹角,由此就 可以推断出 视向量 和 反射向量 的 接近程度,用 noh 来 计算高光 的 亮度值,而这个参数 SpecPow 则是 控制高光的 光泽度,也就是 高光 亮斑的 范围,和色阶同样,用smoothStep来做个柔边的效果再把高光颜色和强度值加上,最后我们把漫反射和高光混合,就可以来调试效果啦。\n1 2 3 4 5 6 7 8 9 10 11 // 得到视向量 float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); // 计算half向量, 使用Blinn-phone计算高光 float3 halfDir = normalize(viewDir + worldLightDir); // 计算NoH用于计算高光 float NoH = dot(normal, halfDir); // 计算高光亮度值 float blinnPhone = pow(max(0, NoH), _SpecPow * 128.0); // 计算高光色彩 float3 specularColor = smoothstep(0.7 - _SpecSmooth / 2, 0.7 + _SpecSmooth / 2, blinnPhone) * _SpecularColor * _SpecIntensity; 5.边缘光 首先我们需要得知哪里是我们看到的边缘,当我们的视向量和法线向量的夹角越接近直角时它就越靠近边缘,先拿到视向量和法向量的夹角,就可以看到,越是接近边缘的地方越暗,但边缘光一般都是越接近边缘越亮,所以给 1- 反转一下,但正常来说阴影部分是不应该有边缘光的,所以要把漫反射加一下,那到至此边缘光就正确啦~\n1 2 3 4 5 6 // 计算NoV用于计算边缘光 float NoV = dot(i.worldNormal, viewDir); // 计算边缘光亮度值 float rim = (1 - max(0, NoV)) * NoL; // 计算边缘光颜色 float3 rimColor = smoothstep(_RimThreshold - _RimSmooth / 2, _RimThreshold + _RimSmooth / 2, rim) * _RimColor; 6.各项颜色混合 完整shader代码 1 2 3 // 混合颜色 float3 finalColor = saturate(rampColor + specularColor + rimColor); return float4(finalColor,1); 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 Shader \u0026#34;Custom/ToonShader\u0026#34; { Properties { _MainTex (\u0026#34;Texture\u0026#34;, 2D) = \u0026#34;white\u0026#34; {} _OutlineWidth (\u0026#34;Outline Width\u0026#34;, Range(0.01, 1)) = 0.01 _OutLineColor (\u0026#34;OutLine Color\u0026#34;, Color) = (0.5,0.5,0.5,1) _RampStart (\u0026#34;交界起始 RampStart\u0026#34;, Range(0.1, 1)) = 0.3 _RampSize (\u0026#34;交界大小 RampSize\u0026#34;, Range(0, 1)) = 0.1 [IntRange] _RampStep(\u0026#34;交界段数 RampStep\u0026#34;, Range(1,10)) = 1 _RampSmooth (\u0026#34;交界柔和度 RampSmooth\u0026#34;, Range(0.01, 1)) = 0.1 _DarkColor (\u0026#34;暗面 DarkColor\u0026#34;, Color) = (0.4, 0.4, 0.4, 1) _LightColor (\u0026#34;亮面 LightColor\u0026#34;, Color) = (0.8, 0.8, 0.8, 1) _SpecPow(\u0026#34;SpecPow 光泽度\u0026#34;, Range(0, 1)) = 0.1 _SpecularColor (\u0026#34;SpecularColor 高光\u0026#34;, Color) = (1.0, 1.0, 1.0, 1) _SpecIntensity(\u0026#34;SpecIntensity 高光强度\u0026#34;, Range(0, 1)) = 0 _SpecSmooth(\u0026#34;SpecSmooth 高光柔和度\u0026#34;, Range(0, 0.5)) = 0.1 _RimColor (\u0026#34;RimColor 边缘光\u0026#34;, Color) = (1.0, 1.0, 1.0, 1) _RimThreshold(\u0026#34;RimThreshold 边缘光阈值\u0026#34;, Range(0, 1)) = 0.45 _RimSmooth(\u0026#34;RimSmooth 边缘光柔和度\u0026#34;, Range(0, 0.5)) = 0.1 } SubShader { Tags { \u0026#34;RenderType\u0026#34;=\u0026#34;Opaque\u0026#34; } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include \u0026#34;UnityCG.cginc\u0026#34; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal: NORMAL; // 计算光照需要用到模型法线 }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; // 计算光照需要用到法线和世界位置 float3 worldNormal: TEXCOORD1; float3 worldPos:TEXCOORD2; }; sampler2D _MainTex; float4 _MainTex_ST; float _RampStart; float _RampSize; float _RampStep; float _RampSmooth; float3 _DarkColor; float3 _LightColor; float _SpecPow; float3 _SpecularColor; float _SpecIntensity; float _SpecSmooth; float3 _RimColor; float _RimThreshold; float _RimSmooth; float linearstep (float min, float max, float t) { return saturate((t - min) / (max - min)); } v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); // 向下传输这些数据 o.worldNormal = normalize(UnityObjectToWorldNormal(v.normal)); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); //------------------------ 漫反射 ------------------------ // 得到顶点法线 float3 normal = normalize(i.worldNormal); // 得到光照方向 float3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos); // NoL代表表面接受的能量大小 float NoL = dot(i.worldNormal, worldLightDir); // 计算half-lambert亮度值 float halfLambert = NoL * 0.5 + 0.5; //------------------------ 高光 ------------------------ // 得到视向量 float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); // 计算half向量, 使用Blinn-phone计算高光 float3 halfDir = normalize(viewDir + worldLightDir); // 计算NoH用于计算高光 float NoH = dot(normal, halfDir); // 计算高光亮度值 float blinnPhone = pow(max(0, NoH), _SpecPow * 128.0); // 计算高光色彩 float3 specularColor = smoothstep(0.7 - _SpecSmooth / 2, 0.7 + _SpecSmooth / 2, blinnPhone) * _SpecularColor * _SpecIntensity; //------------------------ 边缘光 ------------------------ // 计算NoV用于计算边缘光 float NoV = dot(i.worldNormal, viewDir); // 计算边缘光亮度值 float rim = (1 - max(0, NoV)) * NoL; // 计算边缘光颜色 float3 rimColor = smoothstep(_RimThreshold - _RimSmooth / 2, _RimThreshold + _RimSmooth / 2, rim) * _RimColor; //------------------------ 色阶 ------------------------ // 通过亮度值计算线性ramp float ramp = linearstep(_RampStart, _RampStart + _RampSize, halfLambert); float step = ramp * _RampStep; // 使每个色阶大小为1, 方便计算 float gridStep = floor(step); // 得到当前所处的色阶 float smoothStep = smoothstep(gridStep, gridStep + _RampSmooth, step) + gridStep; ramp = smoothStep / _RampStep; // 回到原来的空间 // 得到最终的ramp色彩 float3 rampColor = lerp(_DarkColor, _LightColor, ramp); rampColor *= col; // 混合颜色 float3 finalColor = saturate(rampColor + specularColor + rimColor); return float4(finalColor,1); } ENDCG } Pass { Cull Front CGPROGRAM #pragma vertex vert #pragma fragment frag #include \u0026#34;UnityCG.cginc\u0026#34; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; // 法线 float3 normal : NORMAL; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; // 线条宽度 float _OutlineWidth; // 线条颜色 float4 _OutLineColor; v2f vert (appdata v) { v2f o; float4 newVertex = float4(v.vertex.xyz + normalize(v.normal) * _OutlineWidth * 0.05,1); o.vertex = UnityObjectToClipPos(newVertex); return o; } fixed4 frag (v2f i) : SV_Target { return _OutLineColor; } ENDCG } } fallback\u0026#34;Diffuse\u0026#34; } ","description":"","id":4,"section":"posts","tags":["unity"],"title":"Unity Shader入门(三)","uri":"https://yichenlove.github.io/posts/unity-shader3/"},{"content":"1.渲染管线的相关概念 渲染管线是实时渲染的核心组件。渲染管线的功能是通过给定虚拟相机、3D场景物体以及光源等场景要素来产生或者渲染一副2D的图像。如上图所示,场景中的3D物体通过管线转变为屏幕上的2D图像。渲染管线是实时渲染的重要工具,实时渲染离不开渲染管线。图形渲染管线主要包括两个功能:一是将物体3D坐标转变为屏幕空间2D坐标,二是为屏幕每个像素点进行着色。\n一般这个过程会分为四个主要阶段:应用程序阶段、几何阶段、光栅化阶段、像素处理阶段。而每个阶段 又会分为很多个部分。\n应用程序阶段 (The Application Stage) CPU 它最主要是负责数据的准备,也就是准备后面的阶段 所需的数据,像如模型,贴图,光照,相机位置等信息。\n几何阶段(The Geometry Stage) GPU 顶点着色:可编程部分,顾名思义,它会对逐个顶点相关的信息进行处理,生成图元,计算并传递给接下来的渲染流程。\n它对应的则是这里的VertxShader,它的工作主要是计算顶点的位置、法线、纹理坐标,根据材质、纹理、以及光源属性进行顶点光照的计算,平时常见的顶点动画一般就是在这里实现的。\n [ 图元:可以简单理解为它是渲染管线中所有点,线,面的统称 ] 几何着色: 可选可编程部分,并非所有GPU都支持 ,它可以把简单的图元拓展成更复杂的形式,通常我们认为,这两大着色器共同构成了 几何阶段的可编程部分。\n裁剪 : 固定功能硬件实现,对顶点几何两大着色器的输出结果进行处理,它会把完全处于视锥体交界外 以及屏幕窗口外的 几何体部分裁剪掉, 只留下用户能看到的部分,并且对生成的新顶点部分进行插值,输送给接下来的阶段。\n光栅化阶段(Rasterization) GPU 屏幕映射:经过裁剪之后,硬件会通过透视除法将物体从 裁剪空间 变换 为 标准化设备坐标也叫NDC,之后GPU会把得到的NDC空间坐标下的顶点,映射到屏幕空间坐标中,进行图元装配,这一步会计算微分、边方程和其他三角形数据,三角形的朝向剔除就是在这个阶段完成的。\nNDC:全称Normalized Device Coordinates,一般来说裁剪完成后,会通过透视除法,将物体从裁剪空间 变换为标准化设备坐标NDC,透视除法是将裁剪空间中,顶点的4个分量都除以w分量,从裁剪空间转换到NDC。它是一个长宽高取值范围为[-1,1]的立方体,之所以要转到NDC,是为了方面我们后面进行视口变换把它映射到屏幕空间,不过Unity已经帮我们都完成这些啦。\n图元装配:主要是计算微分(differentials)、边方程(edge equations)和其他三角形数据(顶点属性插值)\n光栅化:它会在每个像素点上生成一个片元,如果开启了多重采样抗锯齿,就会对每个像素进行多次采样,产生多个片元,最终进行混合来达到抗锯齿的效果。\n [ 片元:是光栅化之后产生的像素点,因为没有被画到屏幕上,不能被直接称为像素一个像素的最终结果可能是由多个片元来决定的,渲染管线为了细分,就单独创造了片元这个词来描述它,片元只是渲染管线的概念 ] [ 像素:则是最后写到图像上的值 ] 像素处理阶段 ( Pixel Processing )GPU 像素 (片元) 着色器:可编程部分,它的工作主要是根据顶点的插值属性,进行逐像素计算,因为它需要处理每一个像素,所以这也是最耗时的一个阶段。它的输入输出都是片元数据,输入的数据是 颜色 和纹理坐标,输出的则是计算后所得的每个像素的色彩值,像是逐像素光照、反射、阴影等等更为复杂的效果都是可以在这里实现的。\n合并:只可配置不可编程部分,在一系列的测试后会进行合并,所谓的测试则是判断一个像素点最终是否应该被显示在屏幕上,通过测试的颜色会和缓冲区的颜色叠加混合。\n补充:坐标空间 模型空间 以物体本身为原点的坐标空间,世界空间以世界的(0,0)为原点的坐标空间,视图空间 以相机为原点的坐标空间,描述的物体在相机的哪个位置,裁剪空间 顶点坐标乘以MVP矩阵之后所在的空间,屏幕空间 窗口屏幕上的二维像素坐标空间。\n2.Shader基础概念 Shader中文意思是着色器,比较学术的百科回答就是用来实现图像渲染的,用来替代固定渲染管线的可编辑程序。其中Vertex Shader(顶点着色器)主要负责顶点的几何关系等的运算,Pixel Shader(像素着色器)主要负责片元颜色等的计算。\nUnity中的Shader类型 Standard Surface Shader:标准表面着色器,它是一种基于物理的着色系统,可以理解为 它是通过对物理现象的简单模拟,可以实现生活中各种物品的效果,比如石头、木材、玻璃、塑料和金属等等。\nUnlit Shader:它是最简单的着色器,与 Standard Surface Shader 相比,它去除了冗长的光照公式以及阴影解算,因此得名 Unlit,翻译过来就是无光照,也正因如此,它只由最基础的 Vertex Shader 和 Fragment Shader 组成,最为基础易懂。\nImage Effect Shader:它其实也是也是顶点片元着色器,不过它主要针对实现各种屏幕后处理效果,那后处理是什么呢?一般像是泛光、调色、景深、模糊等基于最终整个屏幕画面而进行再次处理的就是后处理,这里做个简单的了解即可。\nCompute Shader:计算着色器,它是在GPU中运行的一段程序,独立于常规渲染管线之外的,它可以直接将GPU作为并行处理器加以利用,从而使GPU不仅具有3D渲染能力,还具有其他的运算能力。一般会在需要大量并行计算的时候使用。\nRay Tracing Shader:光线追踪着色器,光线追踪是指从摄像机出发的若干条光线,每条光线会和场景里的物体求交,根据交点位置获取表面的材质、纹理等信息,并结合光源信息计算光照。相对于传统的光栅化渲染,光线追踪可以轻松模拟各种光学效果,如反射、折射、散射、色散等。但由于在进行求交计算时需要知道整个场景的信息,它的计算成本也是非常高的。\nShaderLab Unity为我们封装的着色器语言,而目前主流的着色器语言有3种,基于OpenGL的GLSL / 基于DX的HLSL / NVIDIA公司的CG。\nGLSL与HLSL分别是基于OpenGL和Direct3D的接口,两者不能混用。而CG则是为了使图形硬件的编程变得和 C语言编程一样方便自由,它本身基于C语言。如果你之前使用过C系语言其中的任意一个,那CG的语法也是比较容易掌握的。但其实由于Microsoft和NVIDIA的相互协作,他们在标准硬件光照语言的语法和语义上达成了一致,所以HLSL和Cg其实可以看为是同一种语言。\n而ShaderLab则是Unity在HLSL和CG的基础之上封装的只属于Unity的着色器语言,它的灵活性更高,而且不再需要将 Shader 的配置 硬写在引擎代码中,本质是在底层着色语言的基础上,额外提供了声明信息,以数据驱动的方式使我们在渲染管线内自由发挥。\nShaderLab语法详细解析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 // Shader 的路径名称 默认为文件名,也可以与文件名不同 Shader \u0026#34;Unlit/HiShader\u0026#34; { // 属性 // Material Inspector显示的所有参数都在需要在这里进行声明 Properties { // 通常所有属性名都以下划线字符开头 _MainTex _MainTex (\u0026#34;Texture\u0026#34;, 2D) = \u0026#34;white\u0026#34; {} // 比较常见的属性类型 // ———————————————————————————————————————————————— _Integer (\u0026#34;整数(新版)\u0026#34;, Integer) = 1 _Int (\u0026#34;整数(旧版)\u0026#34;, Int) = 1 _Float (\u0026#34;浮点数\u0026#34;, Float) = 0.5 _FloatRange (\u0026#34;浮点数滑动条\u0026#34;, Range(0.0, 1.0)) = 0.5 // Unity包含以下内置纹理, 可以直接填充 // “white”(RGBA:1,1,1,1) // “black”(RGBA:0,0,0,1) // “gray”(RGBA:0.5,0.5,0.5,1) // “bump”(RGBA:0.5,0.5,1,0.5) // “red”(RGBA:1,0,0,1) _Texture2D (\u0026#34;2D纹理贴图\u0026#34;, 2D) = \u0026#34;red\u0026#34; {} // 字符串留空或输入无效值,则它默认为 “gray” _DefaultTexture2D (\u0026#34;2D纹理贴图\u0026#34;, 2D) = \u0026#34;\u0026#34; {} // 默认值为 “gray”(RGBA:0.5,0.5,0.5,1) _Texture3D (\u0026#34;3D纹理贴图\u0026#34;, 3D) = \u0026#34;\u0026#34; {} _Cubemap (\u0026#34;立方体贴图\u0026#34;, Cube) = \u0026#34;\u0026#34; {} // Inspector会显示四个单独的浮点数字段 _Vector (\u0026#34;Example vector\u0026#34;, Vector) = (0.25, 0.5, 0.5, 1) // Inspector会显示拾色器拾取色彩RGBA值 _Color(\u0026#34;色彩\u0026#34;, Color) = (0.25, 0.5, 0.5, 1) // ———————————————————————————————————————————————— // 除此之外 属性声明还可以具有一个可选特性 用来告知Unity如何处理它们 // HDR可以使色彩亮度的值超过1 [HDR]_HDRColor(\u0026#34;HDR色彩\u0026#34;, Color) = (1,1,1,1) // Inspector隐藏此属性 [HideInInspector]_Hide(\u0026#34;看不见我~\u0026#34;, Color) = (1,1,1,1) // Inspector隐藏此纹理属性的Scale Offset字段 [NoScaleOffset]_HideScaleOffset(\u0026#34;隐藏ScaleOffset\u0026#34;, 2D) = \u0026#34;\u0026#34; {} // 指示纹理属性为法线贴图,如果分配了不兼容的纹理,编辑器则会显示警告。 [Normal]_Normal(\u0026#34;法线贴图\u0026#34;, 2D) = \u0026#34;\u0026#34; {} } // 子着色器 // 一个Shader至少有一个或者多个子着色器SubShader,这些子着色器互不干扰,且只有一个会运行 // 在加载shader时Unity会遍历所有SubShader列表,并最终选择用户机器支持的第一个 SubShader { // 可以通过Tags来向子着色器分配标签 // 只可以写在SubShader语块内,不可写在Pass内 /* 以键值对的形式存在,可以出现多个键值对 Tags { \u0026#34;TagName1\u0026#34; = \u0026#34;Value1\u0026#34; \u0026#34;TagName2\u0026#34; = \u0026#34;Value2\u0026#34; \u0026#34;TagName3\u0026#34; = \u0026#34;Value3\u0026#34; ... } */ // RenderPipeline: 声明子着色器是否与通用渲染管线 (URP) 或高清渲染管线 (HDRP) 兼容 // 仅与 URP 兼容 // Tags { \u0026#34;RenderPipeline\u0026#34;=\u0026#34;UniversalRenderPipeline\u0026#34; } // 仅与 HDRP 兼容 // Tags { \u0026#34;RenderPipeline\u0026#34;=\u0026#34;HighDefinitionRenderPipeline\u0026#34; } // RenderPipeline不声明或任何其他值表示与 URP 和 HDRP 不兼容 // ———————————————————————————————————————————————— // Queue: 声明渲染队列 // Tags { \u0026#34;Queue\u0026#34;=\u0026#34;Background\u0026#34; } // 最早被调用的渲染,用来渲染天空盒或者背景 // Tags { \u0026#34;Queue\u0026#34;=\u0026#34;Geometry\u0026#34; } // 这是默认值,用来渲染非透明物体(普通情况下,场景中的绝大多数物体应该是非透明的) // Tags { \u0026#34;Queue\u0026#34;=\u0026#34;AlphaTest\u0026#34; } // 用来渲染经过Alpha Test的像素,单独为AlphaTest设定一个Queue是出于对效率的考虑 // Tags { \u0026#34;Queue\u0026#34;=\u0026#34;Transparent\u0026#34; }// 以从后往前的顺序渲染透明物体 // Tags { \u0026#34;Queue\u0026#34;=\u0026#34;Overlay\u0026#34; } // 用来渲染叠加的效果,是渲染的最后阶段(比如镜头光晕等特效) // ———————————————————————————————————————————————— // RenderType: 用来区别这个Shader要渲染的对象是属于什么类别的。 // 设置渲染类型 用一种称为着色器替换的技术在运行时交换子着色器,用来区别这个Shader要渲染的对象是属于什么类别的 // 这里表示非透明物体渲染 Tags { \u0026#34;RenderType\u0026#34;=\u0026#34;Opaque\u0026#34; } // 更多详细内容可参考官网文档 https://docs.unity.cn/cn/2021.3/Manual/SL-SubShaderTags.html // LOD (Level of Detail) LOD 100 // 每个子着色器由多个通道组成,许多简单的着色器只使用一个通道,但想要一些更复杂的效果,着色器可能需要更多通道 // 一个Pass就是一次绘制,可以看成是一个Draw Call而Pass的意义在于多次渲染, // 如果你有一个Pass,那么着色器只会被调用一次,如果你有多个Pass的话, // 那么就相当于执行多次SubShader了,这就叫双通道或者多通道。 // Draw Call:其实就是CPU调用图像编程接口的渲染命令,CPU每次调用DrawCall,都需要向GPU发送许多数据啊、渲染状态等等, // 一旦CPU执行完应用阶段,GPU就会开始执行这次的渲染流程。而GPU渲染的速度比CPU提交命令的速度要快的多, // 所以如果DrawCall数量过多的情况下,CPU需要进行大量的计算,进而就会导致CPU过载,影响游戏的运行效率。 Pass { CGPROGRAM // 声明顶点着色器 #pragma vertex vert // 声明像素着色器 #pragma fragment frag // 使雾生效 #pragma multi_compile_fog // 引用CG的核心代码库 #include \u0026#34;UnityCG.cginc\u0026#34; // 应用程序阶段结构体 struct appdata { // 参考:https://docs.microsoft.com/zh-cn/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics // POSITION 着色器语言的语义,用来限定着色器的输入输出值的类型 // 模型空间的顶点坐标 float4 vertex : POSITION; // 模型的第一套UV坐标 float2 uv : TEXCOORD0; }; struct v2f { // UV float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) // SV_POSITION 当这个值需要作为输出值输出给系统用的时候 前面需要加SV_前缀 // 当然因为有向下兼容的机制 不加也没啥太大问题 float4 vertex : SV_POSITION; }; // 在Properties中声明的参数要在这里相对应的定义后才可以使用 sampler2D _MainTex; float4 _MainTex_ST; // 定义顶点着色器函数 函数名要与声明顶点着色器名称相同 v2f vert (appdata v) { v2f o; // 将顶点坐标从模型空间变换到裁剪空间 o.vertex = UnityObjectToClipPos(v.vertex); // Transforms 2D UV by scale/bias property // #define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw) // 等价于v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 简单来说,TRANSFORM_TEX主要作用是拿顶点的uv去和材质球的tiling和offset作运算, // 确保材质球里的缩放和偏移设置是正确的 o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } // SV_Target可以视为COLOR ,虽说他也是作为输出值输出给系统的 // 但它其实是告诉系统把输出的颜色值存储到RenderTarget中 // 所以这里我们用SV_Target fixed4 frag (v2f i) : SV_Target { // 采样2D纹理贴图 fixed4 col = tex2D(_MainTex, i.uv); // 应用雾 UNITY_APPLY_FOG(i.fogCoord, col); // 返回经过处理后的最终色彩 return col; } ENDCG } } } 3.实战案例 VertexShader顶点着色器 这里将通过调整顶点的 Y 轴位置实现一个简单的压扁效果\nPixel Shader像素 (片元) 着色器 实现最简单的 Half Lambert 光照模型,首先拿到计算光照需要的模型法线和世界坐标,在v2f的结构体里将它们进行定义,在顶点着色器中将世界坐标和法线进行处理,传递给接下来的像素着色器,在像素着色器中,首先通过世界坐标拿到光照的方向,也就是所谓的入射光,用法线点乘入射光,就可以得到入射光与模型表面的夹角,由于当入射光和法线夹角的余弦值为负数的时候,所得到的结果始终都是零,就会导致照不到的地方一片漆黑,所以这里需要乘0.5再加0.5 ,这样就可以把原本-1到1的取值范围变为0-1的取值范围。最后把求出来的光照亮度叠加到最终的像素色彩值。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 Shader \u0026#34;Unlit/TestUnlitShader\u0026#34; { Properties { _MainTex (\u0026#34;Texture\u0026#34;, 2D) = \u0026#34;white\u0026#34; {} _Value (\u0026#34;压扁系数\u0026#34;,Range(0, 1)) = 0 _Bottom (\u0026#34;底部\u0026#34;, float) = 0 } SubShader { Tags { \u0026#34;RenderType\u0026#34;=\u0026#34;Opaque\u0026#34; } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include \u0026#34;UnityCG.cginc\u0026#34; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float3 normal: NORMAL; // 计算光照需要用到模型法线 }; struct v2f { float2 uv : TEXCOORD0; // 计算光照需要用到法线和世界位置 float3 worldNormal: TEXCOORD1; float3 worldPos:TEXCOORD2; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float _Value; float _Bottom; v2f vert (appdata v) { v2f o; o.uv = TRANSFORM_TEX(v.uv, _MainTex); // 模型空间转到世界空间 float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 压Y轴位置 float y = worldPos.y - (worldPos.y - _Bottom) * _Value; // 最终世界空间位置 float3 tempWorld = float3(worldPos.x,y,worldPos.z); // 世界空间转裁剪空间 o.vertex = UnityWorldToClipPos(tempWorld); o.worldPos = worldPos; // 法线向量归一化 o.worldNormal = normalize(UnityObjectToWorldNormal(v.normal)); return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); // 得到光照方向 float3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos); // NoL代表表面接受的能量大小 float NoL = dot(i.worldNormal, worldLightDir); // 计算half-lambert亮度值 float halfLambert = NoL * 0.5 + 0.5; return col * halfLambert; } ENDCG } } } 补充:光照模型 Half Lambert 模型:光照模型有漫反射,能够较好地表现粗糙表面上的光照现象,像如石灰墙,纸张等等,但是在渲染金属材质制成的物体时,则会显得呆板,表现不出光泽。\nBlinn-Phong 模型:有镜面反射的 Blinn-Phong 模型。\n","description":"","id":5,"section":"posts","tags":["unity"],"title":"Unity Shader入门(二)","uri":"https://yichenlove.github.io/posts/unity-shader2/"},{"content":"1. Untiy 图形相关概念以及基础知识 Mesh / MeshFilter / MeshRenderer / Material 相关概念以及基础知识 在Unity 3D物体中包含了MeshFilter, MeshRenderer,Material组件,然后MeshFilter中包含了Mesh。\nMesh: Unity 的主要图形基元,是指模型的网格,描述形状的数据集合。 Mesh由以下属性定义: Vertices:3D 空间中位置的集合,具有可选的附加属性,可以理解为这里面存储的是构成网格面全部的点。(顶点数组 Vector3[]) Topology:定义曲面的每个面的结构类型。Unity给我们提供了5种拓扑类型,三角面、四边形、线条、虚线、点阵,最常用的则是三角面。(Topology 拓扑类型) Indices:一个整数集合,描述顶点如何组合以基于拓扑创建表面。它是每个三角面顶点的索引,可以理解为他存储了构网格三角面所用到的顶点索引。(Indices 索引数组 int[]\n) Vertex data 顶点数据:包含了顶点的位置、法线、切线、UV等属性。 每个顶点都可以具有以下属性:\n Position:顶点位置表示顶点在对象空间中的位置。(顶点位置 Vector3) Normal:法线就是垂直于该顶点三角面的一条三维向量,它只有方向,没有大小。法线的方向就是顶点三角面朝外的方向。(法线 Vector3[] ) Tangent:它是垂直于法线的一条向量,而由于垂直于法线的向量有无数条,所以切线最终是由UV坐标来决定朝向的。( Tangent 切线 Vector3[] ) Color:顶点颜色表示顶点的基色(如果有)。(顶点颜色) texture coordinates(UVs):U增长的方向就是切线的方向,它和三维空间的X, Y, Z较为类似,它是一个二维的坐标系统,模型网格除了有三维空间的xyz坐标外,还有一个二维的UV坐标,在UV坐标中,U和V分别代表顶点在Texture水平和垂直方向上的采样坐标,这些坐标通常位于(0,0)和(1,1)之间,(0,0)代表最左下角,而(1,1)代表最右上角。可以理解为它是Texture映射到模型表面的依据,模型顶点 会依据UV坐标对Texture进行采样。( UV 纹理坐标 Vector2[] ) Topology拓扑描述了网格的面类型。 Unity 支持以下网格拓扑:\n Triangle 三角形 Quad 四边形 Line 线条 LineStrip 虚线 Points 点阵 Indices 索引数组中的index data Index data 索引数据\n这个数据取决于拓扑类型,如果是三角面他储存的就是[0,1,2],四边形储存的就是[0,1,2,3],这个索引数值对应的就是顶点数组的下标。\nMeshFilter:内包含一个Mesh组件,可以根据MeshFilter获得模型网格的组件,也可以为MeshFilter设置Mesh内容。 Mesh Render:是用于把网格渲染出来的组件。MeshFilter的作用就是把Mesh扔给MeshRender将模型或者说是几何体绘制显示出来。 Material:本质是shader的实例,MeshRenderer中非常重要的角色,它的配置决定了物体表面的外观将以怎样的质地呈现到我们眼前。 要在 Unity 中绘制某物,您必须提供描述其形状的信息以及描述其表面外观的信息。使用网格可描述形状,使用材质可描述表面的外观。\n2. 创建Mesh示例: 1.创建一个GameObject并添加MeshFilter以及MeshRender组件,并创建一个“CreateMesh.cs”脚本给它。\n2.获取该对象的filter组件,并创建一个mesh给它。\n3.为该mesh设置属性,这里先设置顶点,然后将三角形与顶点绑定\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 using UnityEngine; using System.Collections; public class CreateMesh : MonoBehaviour { private MeshFilter filter; private Mesh mesh; // Use this for initialization void Start () { // 获取GameObject的Filter组件 filter = GetComponent\u0026lt;MeshFilter\u0026gt;(); // 并新建一个mesh给它 mesh = new Mesh(); filter.mesh = mesh; // 初始化网格 InitMesh(); } // Update is called once per frame void Update () { } /// \u0026lt;summary\u0026gt; /// Inits the mesh. /// \u0026lt;/summary\u0026gt; void InitMesh() { mesh.name = \u0026#34;MyMesh\u0026#34;; // 为网格创建顶点数组 // 顶点数组 模型空间顶点坐标 Vector3[] vertices = { // Front // point[1] new Vector3(-2.0f, 5.0f, -2.0f),//[0] // point[2] new Vector3(-2.0f, 0.0f, -2.0f),//[1] // point[3] new Vector3(2.0f, 0.0f, -2.0f),//[2] // point[4] new Vector3(2.0f, 5.0f, -2.0f),//[3] // Left new Vector3(-2.0f, 5.0f, -2.0f),//[4] new Vector3(-2.0f, 0.0f, -2.0f),//[5] new Vector3(-2.0f, 0.0f, 2.0f),//[6] new Vector3(-2.0f, 5.0f, 2.0f),//[7] // Back new Vector3(-2.0f, 5.0f, 2.0f),//[8] new Vector3(-2.0f, 0.0f, 2.0f),//[9] new Vector3(2.0f, 0.0f, 2.0f),//[10] new Vector3(2.0f, 5.0f, 2.0f),//[11] // Right new Vector3(2.0f, 5.0f, 2.0f),//[12] new Vector3(2.0f, 0.0f, 2.0f),//[13] new Vector3(2.0f, 0.0f, -2.0f),//[14] new Vector3(2.0f, 5.0f, -2.0f),//[15] // Top new Vector3(-2.0f, 5.0f, 2.0f),//[16] new Vector3(2.0f, 5.0f, 2.0f),//[17] new Vector3(2.0f, 5.0f, -2.0f),//[18] new Vector3(-2.0f, 5.0f, -2.0f),//[19] // Bottom new Vector3(-2.0f, 0.0f, 2.0f),//[20] new Vector3(2.0f, 0.0f, 2.0f),//[21] new Vector3(2.0f, 0.0f, -2.0f),//[22] new Vector3(-2.0f, 0.0f, -2.0f),//[23] }; mesh.vertices = vertices; // 通过顶点为网格创建三角形 // 索引数组 int[] triangles = { // Front 2,1,0, 0,3,2, // Left 4,5,6, 4,6,7, // Back 9,11,8, 9,10,11, // Right 12,13,14, 12,14,15, // Top 16,17,18, 16,18,19, // Buttom 21,23,22, 21,20,23, }; mesh.triangles = triangles; // UV数组 Vector2[] uvs = { // point[1] new Vector2(0.0f, 1.0f), // point[2] new Vector2(0.0f, 0.0f), // point[3] new Vector2(1.0f, 0.0f), // point[4] new Vector2(1.0f, 1.0f), // Left new Vector2(1.0f, 1.0f), new Vector2(1.0f, 0.0f), new Vector2(0.0f, 0.0f), new Vector2(0.0f, 1.0f), // Back new Vector2(1.0f, 1.0f), new Vector2(1.0f, 0.0f), new Vector2(0.0f, 0.0f), new Vector2(0.0f, 1.0f), // Right new Vector2(1.0f, 1.0f), new Vector2(1.0f, 0.0f), new Vector2(0.0f, 0.0f), new Vector2(0.0f, 1.0f), // Top new Vector2(0.0f, 1.0f), new Vector2(1.0f, 1.0f), new Vector2(1.0f, 0.0f), new Vector2(0.0f, 0.0f), // Bottom new Vector2(0.0f, 0.0f), new Vector2(1.0f, 0.0f), new Vector2(1.0f, 1.0f), new Vector2(0.0f, 1.0f), }; mesh.uv = uvs; } } 定义立方体顶点数组\n正常来说立方体共有6个面,每个面由2个三角面组成,三角面有3个顶点数据,所以正常来说每个面需要有6个顶点数据,一共需要36个三角面顶点索引。那为什么这里我们每个面只用到了4个顶点数据呢,这是因为每个面的2个三角面顶点数据中有两个顶点它们的数据是共同的,Unity会直接通过索引找到相对应的数据。\n看到这里,可能又会有人觉得奇怪,既然共用的顶点数据可以通过索引找到,为什么一个立方面只有8个顶点,为什么这里不直接用8个顶点数据,而是需要24个?\n这是因为Unity中不仅依靠这个三角面的索引数组索引三角面的顶点坐标,而且索引纹理坐标,索引法线向量。而立方体的每个顶点都参与了3个平面,而这个顶点相对于这3个平面来说,虽然顶点数据相同,但它们的法线向量是不同的,这个顶点在渲染这3个平面的时候则需要索引到不同的法线向量。而在Unity中由于顶点坐标和法线向量是由同一个索引值取得的,所以这里立方体一共8个顶点,每个顶点我们要存3份,刚好是24个顶点数据。\n","description":"","id":6,"section":"posts","tags":["unity"],"title":"Unity Shader入门(一)","uri":"https://yichenlove.github.io/posts/unity-shader1/"},{"content":"01 前几周,我情绪低落了一阵子。\n原因是工作出现了小小的动荡,部门的一位同事因成本优化的原因被公司辞退了,公司与一些合作方的合作暂停了,我们部门的工作职能也做了调整。\n一时间人心惶惶。\n这种关头往往是情绪小人跳出来兴风作浪的时机。\n我的情绪小人也不负所望。\n大概有一两周时间,跟我阔别已久的焦虑小人又来了,各种各样的担忧窜上心头:\n我会不会也被辞退?\n现在工作很难找,被辞退后怎么办?\n去年买房时跟亲戚借的钱还剩一些没还,如果我被辞退,那些钱要多久才能还清?还有房贷……\n这些胡思乱想时常趁我不注意时侵入头脑,真是不胜其扰。\n来源:Pexels\n那段时间,网上的负面消息也是一波接一波,新冠新毒株出现,疫情终结似乎遥遥无期;房市遇冷,河北燕郊房价更是腰斩,时不时就会看见买房者断供的新闻;各个行业裁员的信息源源不断……\n这些消息加重了我的焦虑。\n这个时候,我试图用《伯恩斯新情绪疗法》里的认知行为疗法和《控制焦虑》里的理性情绪行为疗法(这是认知行为疗法中最著名的一种形式。\n曾经写过一篇专门介绍)来对抗焦虑,这两种方法都是治疗焦虑、抑郁以及各种情绪问题极为有用的方法。但这次,我发现这些方法不那么管用。\n我反思了其中的原因,发现是因为,认知疗法所适用的焦虑问题是认知扭曲导致的焦虑。\n也就是说,现实可能没那么糟,也没什么事情真正对你造成威胁.\n只是你自己的想法太主观,把事情想象得很糟糕、把自己想象得很无能,才会感到焦虑。而且这种焦虑往往伴随着低自尊和对自我价值的怀疑。\n这类方法最大的功效就是重建你对世界和自我的认知,以及重建你的自我价值。此外,这类疗法对具体情境下的焦虑比如社交焦虑、公开演讲焦虑也有效果。\n但这一次我面临的焦虑并不是这种类型的焦虑。\n我知道,我的自尊没有受到打击;我对危险的感知可能有些夸大,但并非空穴来风。大环境的动荡、生活的压力、未来不确定导致的危机……\n这些威胁并不是扭曲的认知想象出来的,它们不是脑中风暴,而是现实风暴。\n面对这类焦虑,改变认知是不够的。\n那该怎么办呢?\n说起来十分巧合,当我苦于没有办法缓解自己的焦虑时,一本书给我带来了灵感——《幸福的陷阱》。\n这本书的核心是介绍一个可以用来缓解焦虑、处理负面情绪和负面思维的方法:\n接纳承诺疗法,简称ACT疗法。\n短期实践之后,我有种相见恨晚的感觉。\n这个方法在解决由真实威胁引发的焦虑上大有可为,此外,它还可以帮助你走出痛苦——\n不管是亲人离世带来的痛苦,还是失业、失学、失恋导致的痛苦,抑或是认为自己毫无价值而感受到的痛苦,总之,各种现实问题或心理问题导致的痛苦,都可以试试这个方法。\n02 对待负面情绪、消极想法,我们的第一反应是去控制它,很多心理学自助书籍和心理疗法所推荐的方法,其目的也是去控制。\n比如,理性情绪行为疗法会教你驳斥内心的非理性信念,更客观地评价自己。\n还有些自助书籍会告诉你用积极的心理暗示代替消极的自我暗示。这些方法都是试图去控制负面的情绪和想法,让它们赶快消失。\n但ACT与此不同,它不建议你去控制自己的负面情绪和想法,它会告诉你,如何不受这些阴暗面的影响,继而可以去做那些对你来说真正重要的事。\n来源:Pexels\n它的核心是告诉你:\n你可以与痛苦的情绪、消极的想法共处。\n也许你有很多负面想法,但是别担心,你不会被这些想法伤害。\nACT最核心的一个方法是对负面的想法进行解离。\n认知行为疗法会教你去纠正和驳斥那些非理性的信念、改变被扭曲的认知。\n比如,如果有人声称因为自己相貌平平,所以一辈子也不可能找到另一半,那么心理咨询师就会建议ta去公园里观察一下周围的情侣,并给这些情侣的魅力值打分。\n这个步骤能够帮助当事人发现,能否找到另一半其实与人的外貌是否有魅力无关,因为很多看起来很甜蜜的情侣,并不是那种魅力值很高的人。\n而ACT中的解离则不同。解离不要你去驳斥或纠正那些负面想法,它只要你做一件事,就是认清那些想法的本质。\n本质是什么呢?\n想法的本质是故事。故事就是并非客观存在的东西,故事就是脑中剧场,故事就只是故事。\n故事也许很逼真,就好像我们曾经看过的那些最棒的电影、小说,我们投入其中,为它们留下感动或痛苦的泪水,就好像自己已经与故事中的世界融为一体。\n但事实上,故事只是大脑虚构出来的东西而已。\n我们脑海中的各种想法,也正是这样一些故事。\n并不是说故事完全没有反映现实,而是说,故事,哪怕它是根据现实经历写出来的,它也与现实不同,当它被我们的大脑构思出来的时刻,它是没有实体的。发生在我们脑海中的想法也并非实体。\n就好像狗这个词语与现实中的狗终究是不同的。\n来源:Pexels\n所以,解离就是当一个想法出现时,尽快察觉到它只是一个想法、一段文字、一个故事。\n然后,任由这个想法来去,不要给予它太多的关注。\n想要对自己的想法进行解离,可以试试这个方法:\n找到一个经常困扰你的想法,比如我是如此失败,给这个想法换一个句式,在这句话前面加一句短语我有一个想法……变成我有一个想法,我是如此失败。\n这个练习可以帮助你拉开与想法之间的距离,让你退后几步去观察自己的想法,从而不至于沉浸其中、信以为真。\n03 你可能要问了:这个方法太过平平无奇,真的有用吗?\n解离起作用的原因有两个。\n第一,它能阻止我们因过度纠结于一个想法正确与否而陷入自我辩论和自我内耗。\n举个例子:不知道你有没有过感到后悔的经历?\n假设有这么一个人,他年轻的时候投身电影行业,梦想成为一流的导演,他虽然有一些才华,但拍出来的东西始终没获得太高知名度,经济回报也有限,生活十分艰辛。\n因此他放弃了电影梦,回到老家找了一份普通工作,过上了平凡但安稳的日子。\n十几年后的一天,他蓦然发现,当初和他一起入行、和他处境一样艰难的一个朋友竟然拍出了一部受市场认可的电影。可以想象,得知消息后他一定会被后悔所吞噬。\n接连好几天,他都会反复问自己一个问题:如果当初我跟他一样坚持下去了,今天是不是也跟他一样成功了?\n来源:Pexels\n但这种想法太糟心了,所以他会换一个角度劝慰自己,对自己说:我的才华不如他,即便我坚持十年,也不见得有他这样的成就,所以我当初选择离开是正确的。\n这个想法能够宽慰他一段日子,但另一个声音也若隐若现,即将响起,它会说:要说我的才华不如他也不是事实,那只是自我安慰而已,如果我没有离开,今天恐怕真的成功了。\n你发现了吗?当他开始后悔,并且开始在意悔恨小人说的话,试图去对抗它时,他实际上就陷入了与那个想法的缠斗中。如果我当年没有离开,今天是不是已经成功了?\n就这个问题,他会一会儿支持正方,一会儿支持反方。但无论他支持哪一方,他都无法给自己一个满意的答案。\n这种自我内耗只会耽误他当下的生活,让他惶惶不可终日。\n而解离就是认出那是一个想法、一个故事,然后随它去。不与它对抗,也就不会深陷其中。\n解离起作用的第二个原因是,它能防止我们陷入负面思维、负面情绪不断升级的恶性循环。\n负面思维会自我增强形成恶性循环,前段时间我就经历过一次。\n那天我在上班,改一篇稿子,我遇到了一个难题,平时遇到那种类型的问题,我大概只要1-2个小时就能处理完,但那天我花了整整一个上午都没能解决。\n低效的状态让我对自己很不满,头脑里的声音响起:\n你真没用!一瞬间,我的大脑被负面情绪裹挟。\n同一时刻,另一个声音又来了:都什么时候了,你不去解决问题,竟然还有时间焦虑、自责?这个声音让我对自己感到生气。\n你看,焦虑之后,生气这个二级情绪又来了。\n来源:Pexels\n这下,被情绪操控的大脑更没办法正常思考了,于是问题迟迟得不到解决。\n问题没解决,又对我的情绪起了坏影响,让我愈加焦虑、生气。\n它们相互加强,不断升级:我越是着急解决问题,思路就越是受到干扰,也就越难解决问题;问题迟迟得不到解决,我就更加烦闷、急躁。\n这样,一个恶性循环就形成了。我陷在了这个情绪旋涡里动弹不得。\n而解离就是在负面思维、情绪升级之前接纳它们。\n比如,假如我接纳自己并非每时每刻都能保持高效,就不会对自己低效的状态不满,也不会对自己说你真没用;\n假如我接纳我的焦虑,不把焦虑当做坏的状态,我就不会为自己焦虑而生气和自责;假如我不试图把这些情绪尽快赶走,我就不会分散注意力、解决不了问题。\n解离和接纳,可以把我们从各种情绪挣扎中解脱出来,也是我们解决问题的起点。\n来源:Pexels\n它们最大的作用就是让我们不再纠结、不再内耗,开始行动。\n你有没有这样的体验?当你真正开始关注当下,投入到一件事情里,就很容易进入心流状态,你全神贯注于那件事,时间仿佛停止了。\n04 ACT中最关键的方法就是解离,它要达成的效果就是做到全然地接纳你的想法和情绪,任由它们来去,不与它们对抗、纠缠。\n书里有一个形象的比喻来形容这个过程。\n想象你在大海上航行,你看到在船的甲板之下,一大群魔鬼正在张牙舞爪。\n有些是情绪的魔鬼,比如内疚、愤怒、恐惧;有些是想法的魔鬼,比如我会失败我会出丑。\n如果你在大海上漂流,那些魔鬼就会安分地待在甲板之下,但一旦你想把船驶向岸边,魔鬼就会跳上来,对你威胁恐吓。\n你忌惮那些魔鬼,因此一直在海上漂流,不敢靠岸。\n但有意思的是,那些魔鬼,不管外表看起来有多吓人,它们永远都无法对你的身体造成直接伤害。\n只要你意识到这一点,你就自由了,可以把船驶向任何地方。\n你还是会受到魔鬼恐吓,但只要你接纳了它们的存在、习惯了它们的存在,不为它们感到恐惧,你就能自如地航行。\n这就是你的负面想法、情绪与你本人之间的关系。它非常真实,因为任何人在活着的每时每刻,大脑都会被想法侵入,而且其中80%的想法都是负面想法。\n来源:Pexels\n对任何人来说,觉得自己不行、觉得自己太差的想法都像家常便饭一样常见,它们就是那些魔鬼。\n而你可以做的,就是与这些魔鬼和平共处,不受它们的干扰和妨碍。\n如果你也被焦虑、后悔、抑郁等情绪而困扰,那么你可以试试ACT疗法。\n但无论ACT在解决情绪问题、心理问题方面多么有用,都不要期待它能解决所有的问题。\n如果你的不安是由现实中的问题引起的,那么唯有行动起来,解决困扰你的问题,才能从根源上扑灭不安之火。\n不过有时候,问题可能不是你自己的责任,也许是他人的责任、社会的责任。遇到这种情况,那也不必过于苛求自己。\n最后送你一句话:\n以无畏的勇气去改变可以改变的,\n以平静之心去接纳无法改变的,\n以人生智慧去区分两者的不同。\n来源:Pexels\n","description":"","id":7,"section":"posts","tags":["psychology"],"title":"克服焦虑、内耗、消极想法","uri":"https://yichenlove.github.io/posts/psychology-overcome-negative/"},{"content":"一、向量、点乘、叉乘的介绍 在数学中,几何向量(也称为欧几里得向量,通常简称向量、矢量),指具有大小(magnitude)和方向的量。 向量可以形象化地表示为带箭头的线段。箭头所指:代表向量的方向;线段长度:代表向量的大小。\n 向量的运算:\n加减:各个分量分别相加减。\n标量:只有大小,没有方向\n数乘:向量与标量的乘数,可以对向量的长度进行缩放,如果标量\u0026gt;0,向量的\n方向不变,如果\u0026lt;0,向量的方向为反方向\n点乘(点积):是接受在实数R上的两个向量相乘并返回一个实数值标量u的大小、v的大小、u,v夹角的余弦。在u,v非零的前提下,点积如果为负,则u,v形成的角大于90度;如果为零,那么u,v垂直;如果为正,那么u,v形成的角为锐角。两个单位向量的点积得到两个向量的夹角的cos值,通过它可以知道两个向量的相似性,利用点积可判断一个多边形是否面向摄像机还是背向摄像机。如果点积越大,说明夹角越小,则物理离光照的轴线越近,光照越强。\n A·B = |A| |B| cos(θ). |A| cos(θ)是A到B的投影。\n 叉乘:两个向量的叉乘得到一个新的向量,新向量垂直于原来两个向量,并且长度等于原向量长度相乘后再乘以夹角的正弦值,类似左手坐标系Z\n点乘与叉乘的理解: 点乘:两个向量点乘得到一个标量 ,数值等于两个向量长度相乘后再乘以二者夹角的余弦值 。 如果两个向量a,b均 为单位 向量 ,那么a.b等于向量b在向量a方向上的投影的长度 #点乘后得到的是一个值 若结果 == o,则 两向量 互垂直 。 若结果 \u0026lt; 0 ,则 两向量夹角大于90°。 若结果 \u0026gt;0 ,则两向量夹角小于 90°。 叉乘:两 个向量的叉乘得到一个新的向量 ,新向量垂直于原来的两个向量再乘夹角的正弦值 叉乘后得到的还是一个向量 在Unity3D里面。两个向量的点乘所得到的是两个向量的余弦值,也就是-1 到1之间, 0表示垂直,-1表示相反,1表示相同方向。 两个向量的叉乘所得到的是两个向量所组成的面 的垂直向量,分两个方向。 简单的说,点乘判断角度,叉乘判断方向。 形象的说当一个敌人 在你身后的时候,叉乘可以判断你是往左转还是往右转更好的转向敌人,点乘得到你当前的面 朝向的方向和你到敌人的方向的所成的角度大小。 二、Vector3的常用方法 ****Vector是结构体 ****\n静态成员变量\nright(右):代表坐标轴(1,0,0)\nleft (左):代表坐标轴(-1,0,0)\nup(上) :代表坐标轴(0,1,0)\ndown (下): 代表坐标轴(0,-1,0)\nforward (前):代表坐标轴(0,0,1)\nback(后):代表坐标轴(0,0,-1)\nzero(零):代表坐标轴(0,0,0)\none(一):代表坐标轴(1,1,1)\n实例成员变量\n magnitude:(返回向量的长度,向量的长度是(x*x+y*y+z*z)的平方根)(只读) normalized:返回一个长度为一的新向量,原向量长度不变 sqrMagnitude:返回向量的长度的平方(只读) 1 2 3 4 5 6 7 8 9 10 11 public Vector3 pos; void Start() { float a = Vector3.Magnitude(new Vector3(10, 5, 2)); // 返回向量的长度,向量的长度事(x * x + y * y + z * z)的平方根 Debug.Log(\u0026#34;向量的长度\u0026#34; + a); // 输出向量长度 Vector3 b = Vector3.Normalize(new Vector3(10, 5, 2)); // 返回一个长度为1的新向量(单位向量),原向量长度不变 Debug.Log(\u0026#34;返回向量的长度为1\u0026#34; + b); float c = Vector3.SqrMagnitude(new Vector3(10, 5, 2)); // 返回向量的长度平凡 Debug.Log(\u0026#34;返回向量的长度的平方\u0026#34; + c); } 打印结果\nVector3常用方法:\nCross:向量叉乘 Dot:向量点乘 Lerp(v(from),v(to),t):两个向量之间的线性插值。按照数字t在from到to之间插值。 MoveTowards(v,v,speed);当前的地点移向目标。 RotateTowards(v,v,r,m);当前的向量转向目标。 Max(v,v),Min(v,v); 向量的最大值,最小值 Angle:返回两个向量之间的夹角 Distance:返回两个向量之间的距离 Project : 极端向量在另一向量上的投影\nOperator + 向量相加\nOperator - 向量相减\nOperator * 向量乘以标量\nOperator / 向量除以标量\nOperator== 若向量相等返回true\nOperator != 若向量不等于则返回true\n 三、Vector3方法的应用 Dot(向量点乘)方法;\n在做rpg类游戏的过程中,经常遇到要判断周围怪物相对自身的方位**\n判断一个物体处于另一个物体的前后方位(背对或面对),点积大于前面(面对),否则(背对)。\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void Start() { // 判断一个物体处于另一个物体的前后方位,点积大雨0前面(面对),否则(背对)。 float dis = Vector3.Dot(this.transform.forward, Green.position - this.transform.position); Debug.Log(\u0026#34;距离为\u0026#34; + dis); if (dis \u0026gt; 0) { Debug.Log(\u0026#34;在前方\u0026#34;); } else if (dis \u0026lt; 0) { Debug.Log(\u0026#34;在后方\u0026#34;); } else { Debug.Log(\u0026#34;重合\u0026#34;); } } 判断前后位置\n打印结果\n Cross(向量叉乘方法)\n判断一个物体处于另一个物体的左右边(通过v3.y\u0026gt;0右边,否则在左边)\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 判断一个物体处于另一个物体的左右边(通过v3.y\u0026gt;0右边,否则在左边) Vector3 v3 = Vector3.Cross(this.transform.forward, Green.position - this.transform.position); Debug.Log(\u0026#34;距离为\u0026#34; + v3); if (v3.y \u0026gt; 0) { Debug.Log(\u0026#34;在右方\u0026#34;); } else if (v3.y \u0026lt; 0) { Debug.Log(\u0026#34;在左方\u0026#34;); } else if (v3 == Vector3.zero) { Debug.Log(\u0026#34;位置一致\u0026#34;); } 判断左右位置\nMoveTowards(v,v,speed) 当前的地点移向目标。\nDistance:返回两个向量之间的距离\nLerp(v(from),v(to),t):两个向量之间的线性插值。按照数字t在from到to之间插值。\n返回值的运算方式为:from+(to-from)*t\nRotateTowards(v,v,r,m);当前的向量转向目标。\nAngle:返回两个向量之间的夹角\n","description":"","id":8,"section":"posts","tags":["Unity"],"title":"Vector3类详解","uri":"https://yichenlove.github.io/posts/unity-vector3-class/"},{"content":" 注:本教程使用 Proto3 版本\n 一、什么是Protobuf? 全称Protocol buffers,是google的一种数据交换的格式,它独立于语言,独立于平台。\n 作用:作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、**数据存储(序列化与反序列化)**等诸多领域。\n **优点:**相比较XML和JSON格式,protobuf更小、更快、更便捷。你只需要将要被序列化的数据结构定义一次(译注:使用.proto文件定义),便可以使用特别生成的源代码(译注:使用protobuf提供的生成工具)轻松的使用不同的数据流完成对这些结构数据的读写操作,即使你使用不同的语言(译注:protobuf的跨语言支持特性)。你甚至可以更新你的数据结构的定义(译注:就是更新.proto文件内容)而不会破坏依赖“老”格式编译出来的程序。\n 二、ProtoBuf的版本 PB具有三个版本:\n 一、Google官方版本: 谷歌官方开发、比较晦涩,主库名字:Google.ProtoBuf.dll\n 二、.Net社区版本: .Net社区爱好者开发,写法上比较符合.net上的语法习惯,主库名字:protobuf-net.dll\n 三、.Net社区版本(二): 据说是由谷歌的.net员工为.net开发,在官方没有出来csharp的时候开发,到发博文时还在维护,主库名字:Google.ProtocolBuffers.dll\n 至于选用那个版本,跨平台的需求不大的话,可以用版本二、大的话可以选用一或者三。\n 三、protocol buffers的工作流程 首先,你需要通过在.proto文件中定义protocol buffer的message类型来指定你想要序列化的数据结构,每一个protocol buffer message是一个逻辑上的信息记录,它包含一系列的键值对。这里展示一个最基本的.ptoto文件的例子,它定义了一个包含Person信息的message:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 syntax = \u0026#34;proto3\u0026#34;; package shenjun; message Person { string name = 1; int32 id = 2; string email = 3; enum PhoneType { HOME = 0; MOBILE = 1; WORK = 2; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phone = 4; } 正如你所看见的那样,message的格式非常简单\u0026ndash;每一个message类型都有一个或多个带有唯一编号的字段,每一个字段有一个字段名和一个字段类型,字段类型可以是数值类型(比如整形或浮点型)、booleans(布尔类型)、strings(字符串类型)、raw bytes、甚至(正如上面的例子)还可以是其他的protocol buffer message类型,这允许你可以分层次的组织你的数据结构。\n运行编译器编译上述的例子将生成一个名为Person的类,在你的应用程序中你可以使用这个类来填充、序列化和反序列化Person protocol buffer messages。之后你可能会写下如下类似的代码(译注:序列化):\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Person p = new Person(); p.Name = \u0026#34;shenjun\u0026#34;; p.Id = 1; p.Email = \u0026#34;[email protected]\u0026#34;; p.Phone.Add(new Person.Types.PhoneNumber { Number = \u0026#34;12345678901\u0026#34;, Type = Person.Types.PhoneType.Mobile }); p.Phone.Add(new Person.Types.PhoneNumber { Number = \u0026#34;123456\u0026#34;, Type = Person.Types.PhoneType.Home }); byte[] buff = p.ToByteArray(); 之后,你可以将你的message读回(译注:反序列化):\n1 2 IMessage IMperson = new Person(); Person person = (Person)IMperson.Descriptor.Parser.ParseFrom(buff); 你可以向你的message中添加新的字段而不会破坏前向兼容性;在解析时旧的二进制文件会简单的忽略掉新字段,所以,如果你的通信协议中使用protocol buffers作为数据交换格式,那么你可以扩展你的协议而不用担心会打乱现有的代码。\n四、为什么不使用XML? 相对于XML,protocol buffers在序列化结构数据时拥有许多先进的特性:\n 1、更简单 2、序列化后字节占用空间比XML少3-10倍 3、序列化的时间效率比XML快20-100倍 4、具有更少的歧义性 5、自动生成数据访问类方便应用程序的使用 举个例子,如果你想描述一个具有name和email的person数据结构,在XML中,你需要这样做:\n \u0026lt;person\u0026gt; \u0026lt;name\u0026gt;John Doe\u0026lt;/name\u0026gt; \u0026lt;email\u0026gt;[email protected]\u0026lt;/email\u0026gt; \u0026lt;/person\u0026gt; 然而,在protocol buffers的message中(protocol buffers的文本格式)你需要这样做:\n # Textual representation of a protocol buffer. # This is *not* the binary format used on the wire. person { name: \u0026quot;John Doe\u0026quot; email: \u0026quot;[email protected]\u0026quot; } 当这个message被编码成protocol buffer的二进制格式(上述的文本格式只是为了方便阅读、调试和编辑),它将可能占用28个字节长度并且仅需要100-200纳秒的解析时间。相比,XML版本的则至少需要占用69字节的空间(这是在移除XML中的空格、换行之后),同时,将耗费大约5000-10000纳秒的解析时间。\n 除此之外,手动操作protocol buffer更为方便,例如如下C++代码:\n cout \u0026lt;\u0026lt; \u0026quot;Name: \u0026quot; \u0026lt;\u0026lt; person.name() \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026quot;E-mail: \u0026quot; \u0026lt;\u0026lt; person.email() \u0026lt;\u0026lt; endl; 然而如果你使用XML,那么你将需要这样做:\n cout \u0026lt;\u0026lt; \u0026quot;Name: \u0026quot;\u0026lt;\u0026lt; person.getElementsByTagName(\u0026quot;name\u0026quot;)-\u0026gt;item(0)-\u0026gt;innerText()\u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026quot;E-mail: \u0026quot;\u0026lt;\u0026lt; person.getElementsByTagName(\u0026quot;email\u0026quot;)-\u0026gt;item(0)-\u0026gt;innerText()\u0026lt;\u0026lt; endl; 事物总有两面性,和XML相比protocol buffers并不总是更好的选择,例如,protocol buffers并不适合用来描述一个基于文本的标记型文档(比如HTML),因为你无法轻易的交错文本的结构。另外,XML具有很好的可读性和可编辑性;而protocol buffers,至少在它们的原生形式上并不具备这个特点。XML同时也是可扩展、自描述的。而一个protocol buffer只有在具有message 定义(在.proto文件中定义)时才会有意义。\n五、如何开始使用protocol buffers? 首先,可以在这里下载安装包或者源码包\n https://developers.google.com/protocol-buffers/docs/downloads#release-packages\n 这包含了针对JAVA、Python、C#和C++编译器的完整源码,同时包含了你所需要的I/O和测试类。为了完成编译和安装,请参照README文件。\n 一旦你完成了编译和安装,那么就可以开始使用protocol buffers了。\n六、proto3介绍 我们最新的版本version 3 alpha release引进了一个新的语言版本\u0026ndash;Protocol Buffers version 3 (称之为proto3),它在我们现存的语言版本(proto2)上引进了一些新特性。proto3简化了protocol buffer language,这使其可以更便于使用和支持更多的编程语言:我们现在的alpha release版本可以让你能产生JAVA、C++、Python、JavaNano、Ruby、Objective-C和C#版本的protocol buffer code,不过可能有时会有一些局限性。另外,你可以使用最新的Go protoc插件来产生Go语言版本的proto3 code,这可以从golang/protobuf Github repository获取。\n我们现在只推荐你使用proto3:\n 1、如果你想尝试在我们新支持的语言中使用protocol buffers\n 2、如果你想尝试我们最新开源的RPC实现gRPC(目前仍处于alpha release版本),我们建议你为所有的gRPC 服务器和客户端都使用proto3以避免兼容性问题。\n 注意两个版本的语言APIs并不是完全兼容的,为了避免给原来的用户造成不便,我们将会继续维护之前的那个版本(译注:proto2)。\n七、最后说一点历史 Protocol buffers最初被Google开发用来作为处理索引服务器的request/response协议。在protocol buffers诞生之前,有一个需要手动编码/解码requests、responses的协议,这个协议支持一个数字版本号,这导致了一个非常丑陋的代码,如下所示:\n if (version == 3) { ... } else if (version \u0026gt; 4) { if (version == 5) { ... } ... } 很显然的,格式化的协议也导致了复杂的新版本推出问题,因为开发人员必须确保所有服务器请求的发起者和实际的请求处理者之间都要理解新的协议。\nProtocol buffers 就是用来解决这些问题的:\n 1、可以很容易的插入新字段,中间的服务器可以简单的解析它而不需要了解所有字段。\n 2、格式更具有自描述性,可以被不同的语言处理(比如JAVA、C++、Python等)。\n 3、自动产生序列化和反序列化代码从而避免了手动解析。\n 4、除了应用在具有短暂生命周期的RPC请求中,人们开始使用protocol buffers 作为一种便利的自描述格式来存储数据(比如在Bigtable中)。\n 5、服务器的RPC接口开始被声明为协议文件的一部分,通过protocol 编译器产生stub类,该类可以被用户根据实际实现的服务器接口进行重写。\n🔚\n","description":"","id":9,"section":"posts","tags":null,"title":"Protocol Buffer","uri":"https://yichenlove.github.io/posts/protocol-buffer/"},{"content":"HTTP1.0,1.1,2.0的简述 HTTP/1.1是HTTP协议的版本号,我们现在常用的协议为1.1,也有少部分仍然在使用HTTP1.0。我们常用的HTTP1.1兼容了1.0,并且在1.0之上改进了诸多内容,比如同一个地址不同host,增加了cache特性,增加Chunked transfer-coding标志切割数据块等。而2.0由于和1.1差别比较大,它并不能兼容1.0和1.1,因此HTTP的世界就像被分成了两块,HTTP2.0被运用在HTTPS上,而HTTP1.1和1.0则运行在原有的HTTP上。\n在HTTP1.0中,一个请求就独占一条TCP连接,要并行的获取多个资源就需要建立多条连接。HTTP1.1则引入了持久连接和管线化,允许在应答回来之前就按顺序的发送多个请求,但是服务器端按照请求的顺序发送应答。在同一个TCP连接上,及时后面的请求先处理完,也必须等待前面的应答发送完毕后,才能发送后面的应答,这就是HTTP1.1的队首阻塞问题。HTTP1.0,1.1都是用了文本形式的协议,也就是所有数据都是以字符串形式存在,这样做的导致协议传输和解析效率不高。不过对于这种文本形式的协议体,我们可以通过压缩来减少数据传输量,但对于协议头我们无法压缩,这使得对于那些Body内容比较少的应答数据来说,很可能协议头的传输成为了首要的性能瓶颈。\nHTTP2.0则引入了Sream概念,这使得一个TCP连接可以被多个Stream共享,每个Stream上都可以跑单独的请求与应答,从而实现了TCP连接的复用,即单个TCP连接上可以并行传输多个请求与应答数据。另外HTTP2.0不再使用文本协议而采用了二进制协议这使得协议更加紧凑,协议头也可以做到压缩后再传输,减少了数据传输带来的开销。不仅如此,HTTP2.0还可以支持Server端的主动Push,进一步减少了交互流程,以及支持流量控制,并引入了流的优先级和依赖关系,这使得我们能够对流量和资源进行较为细致的控制。\nHTTP无状态连接 HTTP的无状态是指对于事务处理没有记忆能力,前后两次的请求并没有任何相关性,可以是不同的连接,也可以是不同的客户端,服务器在处理HTTP请求时,只关注当下这个连接请求时的可获取到的数据,不会去关心也没有记忆去关联上一次请求的参数数据。\nHTTP的访问请求一般都会有软硬件做负载均衡来决定访问哪台物理服务器,进而两次同样地址的访问请求,有可能处理这两次请求的服务器是不同的。而无状态很好的匹配了这种近乎随机的访问方式,也就是说HTTP客户端可以任意选择一个部署在不同区域的服务器进行访问,得到的结果是相同的。\nHTTP每次请求访问结束都有可能断开连接 在HTTP1.0和1.1中,HTTP是依据什么来断开连接的呢?答案是content-length。content-length为 heads 上的标记,表示 body 内容长度。带有content-length 标签的请求,其body内容长度是可知的,客户端在接收服务器回应过来的数据body内容时,就可以依据这个长度来接受数据。在接受完毕后这个请求就完毕了,客户端将主动调用close进入四次挥手断开连接。假如没有content-length 标记,那么body内容长度是不可知的,客户端会一直接受数据,直到服务端主动断开。\nHTTP1.1在这个断开规则之上又扩展了一种新规则,即增加了Transfer-encoding标记。如果Transfer-encoding为chunked,则表示body是流式输出,body会被分成多个块,每块的开始会标识出当前块的长度,此时body不需要通过content-length长度来指定了。如果HEADS上带有Transfer-encoding:chunked 就表示body被分成很多块,每块的长度也是可知的,当客户端根据长度接受完毕数据后再主动断开连接。假如说Transfer-encoding 和 content-length 这两个标记都没有,那么就只能一直接受数据直到服务器主动断开连接。\n那么可不可以使用HTTP协议又不断开连接的方式?还有HEAD里的keep-alive标识,keep-alive标识会让客户端与服务器的连接保持状态,直到服务器发现空闲时间结束而断开连接,在结束时间内我们仍然能发送数据。也在就是说,可以减少多次的与服务器3次握手建立连接的消耗,以及多次与服务器4次握手断开连接的消耗,提高了连接效率。\n另外一面在服务器端上,Nginx的 keepalive_timeout,和Apache的 KeepAliveTimeout 上都能设置 Keep-alive 的空闲时间大小,当httpd守护进程发送完一个响应后,理应马上主动关闭相应的TCP连接,但设置 keepalive_timeout后,httpd守护进程会说:”再等等吧,看看客户端还有没有请求过来”,这一等,便是 keepalive_timeout时间。如果守护进程在这个等待的时间里,一直没有收到客户端发过来HTTP请求,则关闭这个HTTP连接。不过也不一定说使用 keep-alive 标识能提高效率,有时也会反而降低了效率,比如经常会没有数据需要发送,导致长时间的Tcp连接保持导致系统资源无效占用,浪费系统资源,巨量的保持连接状态就会浪费大量的连接资源。\n倘若我们客户端使用keep-alive做连续发送,则需要在连续发送数据时使用同一个HTTP连接实例。并且在发送完毕后要记录空闲时间,以便再次发送时,可以判断是否继续使用该连接,因为通常服务器端主动断开连接后并没有被客户端及时的得知,所以自行判断是否有可能已经被服务器端断开连接为好。还有一个问题是,如果网络环境不好导致发送请求无法到达时,则要尽可能的自己记录和判断,哪些数据是需要重发的。这几个问题增加了HTTP作为keep-alive来保持连接的操作难度,将本来简单便捷的HTTP,变得使用更加困难,因此这也是keep-alive并不常使用在游戏项目中。\n","description":"","id":10,"section":"posts","tags":["http"],"title":"Http的1.0,1.1,2.0","uri":"https://yichenlove.github.io/posts/http-info/"},{"content":"Unity3D 3D模型中SubMesh的意义 在模型中可以有很多网格,一个模型可以由很多个网格构成。因此在Unity3D中一个Mesh网格的构成可以由多个子Mesh组成也就是SubMesh,即一个Mesh里可以有多个SubMesh。\n引擎在渲染的时候,每个SubMesh都需要对应一个Material材质球来匹配做渲染,说白了一个SubMesh本身就是普通的模型有很多个三角形构成它也需要材质球支持以达成渲染。在美术人员制作3D模型过程中,可以将SubMesh拆分成独立的Mesh,也可以并成多个子模型即SubMesh。\nSubMesh有诸多好处,与没有SubMesh的Mesh相比,拥有多个SubMesh一样可以有动画,另外它还能针对不同部分的Mesh选择有个性化的材质球来表现效果,从功能上来看比单个Mesh要灵活的多。但它也有些许缺点,由于每个SubMesh都多出了材质球,导致SubMesh越多,增加的Drawcall也越多。Mesh中存在多个SubMesh,在动作和拆分材质球渲染上确实有很好的优势,但无法与其他Mesh合并,导致优化的一个重要环节被阻断。\nSubMesh虽然功能很强大,但对性能的开销也需要注意,需要我们慎重使用。有时我们也可以选择用完全拆分Mesh为其他Mesh的形式来代替SubMesh,这样在合并Mesh时就有更多的选择了。下面我们就来深入浅出的聊聊合并模型的方法和途径。\n动态合并3D模型。 我们制作的场景中的3D的物体很多,每个3D物体都需要有一个材质球支持,导致每个模型都会产生一个Drawcall(渲染管线的调用),众多的3D模型会产生很多Drawcall,CPU在等待渲染GPU在忙于处理Drawcall,使得帧率下降画面卡顿感强烈。\n实际中的项目都会遇到这样的问题,场景中要摆放的3D物体很多,包括人物,建筑,路标,景观,树木,石头,碎块,花朵等。这些3D物体都有自己的材质球,相同模型的物体使用相同的材质球,不一样的物体使用不同的材质球,有时不一样的物体也有相同的材质球。如果不做任何优化处理就会产生很多Drawcall,导致帧率下降。于是我们就会想这么多的材质球引起这么多Drawcall,是否能合并合并成一个,这就是合并3D模型发挥作用的时候。\n合并3D模型主要的目的就是为了减少Drawcall,它是通过减少材质球的提交数量来完成优化手段的,说的简单点就是把拥有相同材质球的模型合并起来成为一个模型和一个材质球,从而减少向GPU提交的Drawcall数量。\nUnity3D引擎在合并模型从而优化Drawcall上有自己的功能,即 动态批处理 和 静态批处理 两种,它们的前提条件都是模型物体必须是相同材质球的模型,除了这个必要条件外还有其他条件也需要符合。下面我们就来介绍下Unity3D中动态批处理和静态批处理:\n动态批处理 动态批处理即意味着随时都在做的模型合并批量处理,当我们把 Dynamic Batch 动态批处理开启时,Unity3D可以自动批处理场景中某些物体成为同一个Drawcall,如果是他们使用的是同一个材质球并且满足一些条件的话动态批处理会自动完成的,我们不需要增加额外的操作。\n其中需要满足的动态批处理的条件是,\n 1,动态批处理的物体的顶点数目要在一定范围之内,动态批处理只能应用在少于900个顶点的Mesh中。\n 如果你的Shader使用顶点坐标,法线,单独的UV,那么只能动态批处理300个顶点内的网格,\n 如果你的Shader使用顶点坐标,法线,UV0,UV1和切线,则只能有180个顶点了。\n 2,两个物体的缩放比例一定是相同,假如两个物体不在同一个缩放单位上,它们将不会进行动态批处理(例如物体A的缩放比例是(1,1,1),物体B的缩放比例是是(1,1,2),他们的缩放比例不同则不会被合并处理,除非A的缩放比例改为(1,1,2),或者B的缩放比例改为(1,1,1))\n3,使用相同的材质球的模型才会被合并,使用不同的材质球是不会被动态批处理的,即使他们模型是同一个或者看起来像是同一个。\n4,多管线(Pipeline)Shader会中断动态批处理。\n 很多Unity3D里的Shader支持多个灯光的前置渲染增加了多个渲染通道,这些多个通道的材质球是无法用于动态批处理渲染的。\n Legacy Deferred(灯光前置通道)传统延迟渲染路径已经被动态处理关闭,因为它必须绘制物体两次。\n 所有多个pass的Shader增加了渲染管道,不会被动态批处理\n 动态批处理的条件是很苛刻的,在项目中很多模型是不符合动态批处理的。另外动态批处理要消耗CPU转换所有物体的顶点到世界空间的操作,所以它唯一的优势是如果它的工作能让Drawcall变少。\n最后我们需要理解一味的减少Drawcall不是万能,它的资源需求取决于很多因素,主要被图形API使用。例如一个控制台或流行的API像Apple Metal这样的,Drawcall的开销会普遍很低,因此动态批处理时常在优化方面的优势并不是很大。\n静态批处理 静态批处理允许引擎在离线的情况下去做模型合并的批处理以降低Drawcall,无论模型多大只要使用同一个材质球都会被静态批处理优化。他通常比动态批处理有用(因为它不需要实时转换顶点来消耗CPU),但也消耗了更多的内存。\n为了让静态批处理起作用,我们需要将物体置为静态不同的,即我们需要去确认指定的物体是否是静态的不能动,不能移动、不能旋转或者缩放。因此我们需要给这物体在面板上标记一个静态的标记以确定性的告诉Unity3D引擎,此物体是不能动不能缩放的,可以对该物体做静态批处理的预处理。\n使用静态批处理需要增加额外的内存来存储合并的模型。在静态批处理下如果一些物体在静态批处理前共用一个模型,那么Unity3D会复制每个物体的模型以用来合并,在Editor里或者在实时运行状态下都会做这个操作。这可能不总是有益的,因为这样做会带来大量的内存增加,因此有时我们需要减少对物体的静态处理来减少内存的使用量,虽然这样做会牺牲了渲染性能,不过我觉得内存换CPU是值得的,但是如果100兆的内存来换1%的CPu效率任然是不划算的,所以我们还是应该谨慎。\n静态批处理的具体做法是,将所有静态物体放入世界空间,并且把他们以材质球为分类标准分别合并起来,并构建一个大的顶点集合和索引缓存,所有可见的同类物体就会被同一批的Drawcall处理,这就会让一系列的Drawcall减少从而实现优化的效果。\n技术上来说静态批处理并没有节省3D API Drawcall数量,但他节省了他们之间的状态改变导致的消耗。在大多数平台上,批处理被限制在6万4千个顶点和6万4千个索引(OpenGLES上为48k,macOS上为32k),所以倘若我们超过这个数量需要取消一些静态批处理对象。\n现在我们知道动态批处理 和 静态批处理是什么了,我们来做个简单总结:\n 1,\t动态批处理条件是,使用同一材质球,顶点数量不超过900个,有法线的不超过300个顶点,有两个UV的不超过150个顶点,缩放大小要一致,Shader不能有多通道。\n2,\t静态批处理条件是,必须是点上静态标记的物体,不能动,不能旋转,不能缩放,不能有动画。\n 动态批处理的规则是极其严格的,在具体的场景中能用到的模型是相对简单的,它对顶点限制太紧,而且缩放比例还要相同,渲染管道也只能有一个。\n静态批处理的使用范围更广一些,但要求物体是静态不能移动,旋转,缩放。这个限制太固定,用到的地方只有完全不动的场景中的固定物体。\n动态批处理限制太大,静态批处理又不满足我们的需求,所以有时我们也只能自己手动合并模型来替代Unity3D的批处理。也只有用自己程序合并的模型才能体现自定义动态批处理的用途。比如构建场景后的动态建筑,动态小件合并,人物模型更换装备,发型,首饰,衣裤等导致多个模型挂载的需要合并模型来优化渲染。\n自己来编写合并3D模型的程序 编写自己的合并3D模型程序需要调用些Unity3D的API,我们来了解下Unity3D的几个类和接口:\n Mesh类有个CombineMeshes的接口提供了合并3D模型的入口。\nMeshFilter类,是承载Mesh数据的类。\nMeshRenderer类,是绘制Mesh网格的类。\n 1, SubMesh的意义。\n 前文用专门的一节来解释它的意义。这里简单阐述下,SubMesh是Mesh里拆出来的子模型,SubMesh需要额外多个的材质球,而普通的Mesh只有一个材质球。\n 2, MeshFilter 和 MeshRenderer中的 mesh 和 shareMesh ,material 和 shareMaterial 的区别。\n mesh 和 material 都是实例型的变量,对 mesh 和 material 进行任何操作都会额外复制一份后再进行重新赋值,即使只是get操作也同样会发生复制效果。也就是说对 mesh 和 material 进行操作后就会变成另一个实例,虽然看上去一样,但其实已经是同的实例了。\nsharedMesh 和 sharedMaterial 与前面两个变量不同,他们是共享型的。多个3D模型可以共用同一个指定的 sharedMesh 和 sharedMaterial,当你修改sharedMesh或sharedMaterial里面的参数时,多个同是指向同一个 sharedMesh 和 sharedMaterial的模型就会同时改变效果。也就是说 sharedMesh 和 sharedMaterial 被改变后,所有使用sharedMesh 和 sharedMaterial资源的3D模型会有同一个表现效果。\n 3, materials 和 sharedMaterials 的区别。\n 与前面 material 和 sharedMaterial 同样的区别, materials 是实例型,sharedMaterials 是共享型,只不过现在他们变成了数组形式。\nmaterials 只要对它进行任何操作都会复制一份一模一样的来替换,sharedMaterials 操作后所有指向这个材质球的模型都会改变效果。而 materials 和 material,与 sharedMaterials 和 sharedMaterial 的区别是,materials和sharedMaterials可以针对不同的subMesh,而material和sharedMaterial只针对主Mesh。也就是说 material 和 sharedMaterial 等于 materials[0] 和 sharedMaterials[0]。\n 4, Mesh,MeshFilter,MeshRenderer的关系。\n Mesh是数据资源,它可以有自己的资源文件,比如XXX.FBX。Mesh里存储了,顶点,uv,顶点颜色,三角形,切线,法线,骨骼,骨骼权重等提供渲染必要的数据。\nMeshFilter是一个承载Mesh数据的类,Mesh被实例化后存储在MeshFilter,MeshFilter有两种类型即实例型和共享型的变量,mesh和sharedMesh,对mesh的操作将生成新的mesh实例,而对sharedMesh操作将改变与其他模型共同拥有的那个指定的Mesh数据实例。\nMeshRenderer具有渲染功能,它会提取MeshFilter中的Mesh数据,结合自身的materials或者sharedMaterials进行渲染。\n 5, CombineInstance即合并数据实例类。\n 合并时我们需要为每个需要合并的 Mesh 创建一个CombineInstance实例并往里面放入,mesh,subMesh的索引,lightmap的缩放和偏移,以及realtimeLightmap的缩放和偏移(如果有的话),和世界坐标矩阵。CombineInstance承载了所有需要合并的数据,通过将CombineInstance数组传入到合并接口,即通过Mesh.CombineMeshes接口进行合并。\n 下面来看下合并3D模型的具体步骤:\n1,建立合并数据数组\n1 CombineInstance[] combine = new CombineInstance[mMeshFilter.Count]; 2,填入合并数据\n1 2 3 4 5 6 for(int i = 0 ; i\u0026lt; mMeshFilter.Count ; i++) { combine[i].mesh = mMeshFilter[i].sharedMesh; combine[i].transform = mMeshFilter.transform.localToWorldMatrix; combine[i].subMeshIndex = i; //标识Material的索引位置,可以为0,1,2等 } 3,合并所有Mesh为单独一个\n1 new_meshFilter.sharedMesh.CombineMeshes(combine); 或者,合并后保留SubMesh\n1 new_meshFilter.sharedMesh.CombineMeshes(combine,false); 4,CombineMeshes接口定义为\n1 public void CombineMeshes(CombineInstance[] combine, bool mergeSubMeshes = true, bool useMatrices = true, bool hasLightmapData = false); 完整代码为:\n1 2 3 4 5 6 7 8 9 10 11 CombineInstance[] combine = new CombineInstance[mMeshFilter.Count]; for(int i = 0 ; i\u0026lt; mMeshFilter.Count ; i++) { combine[i].mesh = mMeshFilter[i].sharedMesh; combine[i].transform = mMeshFilter.transform.localToWorldMatrix; combine[i].subMeshIndex = i;//标识Material的索引位置,可以为0,1,2等 } new_meshFilter.sharedMesh.CombineMeshes(combine); ","description":"","id":11,"section":"posts","tags":null,"title":"Unity 合并3D模型","uri":"https://yichenlove.github.io/posts/unity-merge-3dmodel/"},{"content":"Canvas的三种渲染模式 Canvas共有三种渲染模式,分别是ScreenSpace-Overlay、ScreenSpace-Camera和WorldSpace。\n1 Screen Space-Overlay模式 Screen Space-Overlay(屏幕空间-覆盖模式)UI元素的位置坐标是屏幕空间的坐标,Overlay模式下画布会填满整个屏幕空间,并将画布下面的所有的UI元素置于屏幕的最上层。如果屏幕尺寸被改变,画布将自动改变尺寸来匹配屏幕。\n此时,画布上的UI组件会随视角移动。\nScreen Space-Overlay模式的画布有Pixel Perfect和Sort Layer两个参数:\n(1)Pixel Perfect:只有RenderMode为Screen类型时才有的选项。使UI元素像素对应,效果就是边缘清晰不模糊。\n(2)Sort Layer: Sort Layer是UGUI专用的设置,用来指示画布的深度。\n2 Screen Space-Camera模式 Screen Space-Camera(屏幕空间-摄影机模式)UI元素的位置坐标是屏幕空间的坐标,画布也是填满整个屏幕空间,如果屏幕尺寸改变,画布也会自动改变尺寸来匹配屏幕。所不同的是,在该模式下,画布会被放置到摄影机前方。在这种渲染模式下,画布看起来绘制在一个与摄影机固定距离的平面上。所有的UI元素都由该摄影机渲染,因此摄影机的设置会影响到UI画面。\n在此模式下,UI元素是由perspective也就是视角设定的,视角广度由Filed of View设置。\n此时,画布上的UI组件会随视角移动。\n它比Screen Space-Overlay模式的画布多了下面几个参数:\n(1)Render Camera:渲染摄像机\n(2)Plane Distance:画布距离摄像机的距离\n(3)Sorting Layer: Sorting Layer是UGUI专用的设置,用来指示画布的深度。可以通过点击该栏的选项,在下拉菜单中点击“Add Sorting Layer”按钮进入标签和层的设置界面,或者点击导航菜单-\u0026gt;edit-\u0026gt;Project Settings-\u0026gt;Tags and Layers进入该页面。\n可以点击“+”添加Layer,或者点击“-”删除Layer。画布所使用的Sorting Layer越排在下面,显示的优先级也就越高。\n(4)Order in Layer:在相同的Sort Layer下的画布显示先后顺序。数字越高,显示的优先级也就越高。\n3 World Space模式 World Space即世界空间模式,此模式下UI元素的位置坐标是世界空间的坐标。画布作为场景中的一部分被固定显示在场景中,显示效果类似Plane组件。\n总结 Canvas上的参数 Render Mode 渲染模式比较重要,你可以选择不以Camera为基准的Overlay模式,也可以选择Camera为基准的Screen Camera模式,也可以选择3D世界为基准的World Space模式。三者适合于三种不同的的使用场景各有不同。\n Overlay模式并不与空间上排序有任何关系,空间上的前后位置不再对元素起作用,它常用在纯UI的区域内,这种模式下Camera排序有别与其他模式,Sort order参数在排序时被着重使用到,Sort order参数的值越大,越靠前渲染。在这个模式下没有Camera的渲染机制因此很难加入普通的3D模型物体来增加效果。\n Screen Camera模式,相对比较通用一点,它依赖于Camera的平面透视,渲染时的布局依赖于它绑定的Camera。想让更多的非UGUI元素加入到UI中,Screen Camera模式更加具有优势。这种模式是实际项目中制作UI最常用的模式,不过UGUI底层有对排序做些规则,如对元素的z轴不为0的元素,会单独提取出来渲染,不参与合并。\n World Space模式,主要用于当UI物体放在3D世界中时用的,比如,一个大的场景中,需要将一张标志图放在一个石块头上,这时就需要World Space模式。它与 Screen Camera 的区别是,它常在世界空间中与普通3D物体一同展示,依赖于截锥体透视(Perspective)Camera。它的原理挺简单的,与普通物体一样当UI物体在这个Camera视野中时,就相当于渲染了一个普通的3D面片,只不过除了普通的渲染Canvas还对这些场景里的UI进行合并处理。\n ","description":"","id":12,"section":"posts","tags":["Unity"],"title":"Unity Canvas 的三种渲染模式","uri":"https://yichenlove.github.io/posts/unity-canvas-render-mode/"},{"content":"架构 项目中的每个子系统的都有自己的决策方向,而子系统的决策方向,把它们合起来加入一定的关联性就构成了一个完整架构整体,即每个系统、模块、组件都是软件系统架构中的一部分。\n架构好坏取决于以下5个能力\n 一,承载力。\n 从软件架构的程序意义来说,一个架构能承载多少个逻辑系统,代码复杂度扩展到100万行代码代码时是否依然能够有序规范,程序员彼此工作的模块相互依存度有多少,能够承载多少个程序员共同工作因为能工共同工作的架构加速了开发与迭代,这是对软件架构承载力的评定。\n从架构的结果上来看,对于服务器来说,当前架构能承受多少人同时访问,日均访问量能承载多少,是承载力的体现。而对于客户端来说,能显示多少UI元素,可渲染多少模型(包括同屏渲染和非同屏渲染),数据交互能达到多少量。\n 二,可扩展度。\n 软件架构需要具有高的可扩展度。而且可扩展度的关键在于,在添加新的子系统后不能影响或者只能尽可能的少量影响其他子系统的运作。假设添加了子系统后,所有系统都得重写或者重构,那就是灾难,前面花去的时间和人力物力精力全部‘浪费’,这是我们不想看到的,因此可扩展度也是衡量好的架构的非常重要标准。\n 三,易用性。\n 易用性决定了架构的整体开发效率,程序员容易上手,子系统容易对接,开发效率自然就高,各模块各部件的编写只需要花一点点精力来关注架构的融合,其他所有精力和注意力都可以全部集中在自己的框架结构上,才能让各系统各尽其职将效率发挥到极致。\n 四,可伸缩力。\n 软件架构当需要的承载量没有这么大时,是否可以不使用不需要的功能,化繁为简,只使用需要的部分。\n例如从服务器端的角度来说,当需要急速导入大量用户时到做能承载几百万人同时在线,服务器可随时扩展到几百上千台服务器来提高承载量,当访问量骤减,或者平时访问量比较少的情况下,访问量甚至低到只有几十个人在访问时,服务器可缩减到就几台机子在运作,这样大大缩减了服务器费用的开销,可以根据需要而随时变更架构的承载力来节省成本。\n而从客户端的角度来说,伸缩力体现在是否能适应大型项目众多人协同开发复杂系统,既能适应大成本消耗下的大项目大作品,也能适应小项目1-3个人团队小而快速的开发环境,小成本小作品极速迭代。\n 五,容错力以及错误的感知力.\n 软件中错误、异常、BUG常有,设备何时损坏我们无法预估。容灾力首先起到了不让产品彻底不能使用的作用,有备份方案自动启用,也同时要能够让我们及时得知到问题发生,以及问题的所在,通过EMAIL发送或者通过短信、电话方式通知维护者,并且记录并保存错误信息。\n从服务端的角度来说,容灾力包括,数据库容灾能力,应用服务器容灾力,缓存服务器容灾力,以及中心服务器容灾力,每个机子倒下了都需要通知相关中心服务器改变策略,或者监控服务器检测得知该服务器倒下了,更换成备用服务器或者直接更换链路。\n从客户端角度来说,容灾力包括当数据发生错误时,是否同样能够继续保持运行而不崩溃,当程序出错时,是否依然能够继续运行其他程序,而不闪退或崩溃甚至再次启动也不能使用的状况发生。所有出现的错误,都能及时的记录并发送到服务器后台存储成为错误日志,便于开发人员能都及时得到详细的错误信息,根据错误信息能够快速找出问题的所在。\n软件系统架构思维方式 在系统架构和设计中,抽象能力是个比较重要的能力。以下是抽象能力分析。\n 第一种,分层思维\n 分层是我们应对和管理复杂性的基本思维武器。\n构建一套复杂系统,我们把整个系统划分成若干个层次,每一层专注解决某个领域的问题,并向上提供服务。这样的抽象做法,让复杂的事务变得更加清晰有序。有些层次并不一定是横向的,也可以是纵向的,纵向的层次贯穿其他横向层次,称为共享层。如下图:\n 第二种,分治思维\n 分而治之也是应对和管理复杂性的一般性方法,下图展示一个分治的思维流程:\n对于一个无法一次解决的大问题,我们会先把大问题分解成若干个子问题,如果子问题还无法直接解决,则继续分解成子子问题,直到可以直接解决的程度,这个是分解(divide)的过程;然后将子子问题的解组合拼装成子问题的解,再将子问题的解组合拼装成原问题的解,这个是组合(combine)的过程。\n在生活中分治思维,解决大问题,复杂问题,是很好手段。特别是当遇到那些你从未处理过的问题时,或者特别复杂超出你能力范围的问题时,把它分解、拆分、解刨、撕裂。把大问题先分成几大块的问题,再从这几大块问题入手,对每个大块问题再分解,拆分成小块问题。倘若小块问题仍然无法进行,或者还是没有思路,再拆分,再解刨,再分解,直到分解到你能开始着手解决了为止。这样一步步,一点点,把小的问题解决了,就是把大块问题解决了。随着时间的推移,不断解决细分的小问题,大块问题被迎刃而解,最后大块问题解决完后,更大块问题迎刃而解。\n 第三种,演化思维\n 在互联网软件系统的整个生命周期过程中,前期的设计和开发大致只占三分,在后面的七分时间里,架构师需要根据用户的反馈对架构进行不断的调整。我认为架构师除了要利用自身的架构设计能力,同时也要学会借助用户反馈和进化的力量,推动架构的持续演进,这个就是演化式架构思维。\n当然一开始的架构设计非常重要,架构定系统基本就成型了,不容马虎。同时,优秀的架构师深知,能够不断应对环境变化的系统,才是有生命力的系统,架构的好坏,很大部分取决于它应对变化的灵活性。所以具有演化式思维的架构师,能够在一开始设计时就考虑到后续架构的演化特性,并且将灵活应对变化的能力作为架构设计的主要考量。\n从单块架构开始,随着架构师对业务域理解的不断深入,也随着业务和团队规模的不断扩大,渐进式地把单块架构拆分成微服务架构的思路,这就是演化式架构的思维。如果你观察现实世界中一些互联网公司(例如eBay,阿里,Netflix等等)的系统架构,大部分走得都是演化式架构的路线。\n前端架构 前端与后端架构之间的共性\n 前后端架构的目标都是,高性能、高可用、可扩展、安全、可容错。对于前端来说除了这些目标特性外,我们还需要加入更多的用户体验,包括视觉效果和操作灵敏度。\n作为前端工程师,用户体验是比较重要的,但这种体验涉及到很多方面,包括性能优化,视觉效果,以及操作上的人性化等,例如如何让游戏加载更快,如何制作更绚丽的特效,如何减少Drawcall,如何减少CPU的负载,如何最快的响应用户操作等。\n前端技术与后端技术,都是在同一个系统层面上建立起来的,都是建立在Linux,Windows,Android,IOS,操作系统之上的,两者最后要需要了解操作系统的接口以及底层运作原理。区别在于后端在操作系统上构建了一套服务端框架,而前端在操作系统之上构建了一个渲染引擎,两者都需要在这之上构建业务架构。当我们自己构建了或选择使用商业渲染引擎后,再在渲染引擎之上建立游戏应用的业务架构,因此我们其实有两套架构要学习,一套是渲染引擎架构,一套是游戏业务架构。\n 培养架构设计思维\n 良好的架构设计思维的培养,离不开工作中大量高质量项目的实战锻炼,然后是平时的学习、思考和提炼总结。\n架构设计不是静态的,而是动态的。只有能够不断应对环境变化的系统,才是有生命力的系统。所以即使你掌握了抽象、分层和分治这三种基本思维,仍然需要演化式思维,在设计的同时,借助反馈和进化的力量推动架构的持续演进。\n架构师在关注技术,开发应用的同时,需要定期梳理自己的架构设计思维,积累时间长了,你看待世界事物的方式会发生根本性变化,你会发现我们生活其中的世界,其实也是在抽象、分层、分治和演化的基础上构建起来的。架构设计思维的形成,会对你的系统架构设计能力产生重大影响。可以说对抽象、分层、分治和演化掌握的深度和灵活应用的水平,直接决定架构师所能解决问题域的复杂性和规模大小,是区分普通应用型架构师和平台型/系统型架构师的一个分水岭。\nUnity3D项目架构 分层的思维方式,先确定架构的层级。\n把整个项目分成五大层级,网络层,数据层,资源层,核心逻辑框架层,UI层。\n再拆分层级。把太过于笼统的层级进行再分层。如下图:\n经过再分层后,把核心逻辑框架分成了,工具编辑器,角色行为框架,AI框架,地图场景与寻路框架,Shader与特效,设备原始接口。这些子层都是在核心逻辑层中,他们有自己的框架,也可以互相调用,构成了核心逻辑部分,也就是核心玩法或者说核心战斗的主要部分。\n我们将资源管理层和数据管理层再进行了拆分,分成了Assetbundle资源管理和Prefab资源管理,以及内存数据管理和外部数据管理,这样更清晰的分工了各层的职能。其实还有很多其他的层级我们这里没有提到的,包括常用库,工具库,动画控制等,这里暂不一一提出来。\n在游戏项目中最常用的是,数据表,网络层,UI层,常用库,这几个模块。我们可以用这个层级的方式来试着搭建一个完整的项目,只是做抽象上的编写,就可以清晰的知道,这个项目需要哪些模块和层级了。\n实际工作中,我们对层级和模块逐个攻破的同时,也进入架构演化模式。一开始的做的架构中某个部位的并不适合,或需要改善,在后面的工作中,修复和完善架构是演化的重要步骤。\n在不断编写完善架构的过程中原本抽象简单的架构,开始复杂化。虽然每个模块都在有条不紊的进行中,但也会不断冒出各种各样不适应或者不符合实际需求的问题出现,我们需要及时跟进演化内容。去除、重构或者改善,前面由于各种原因而导致的错误的理解。\n最后架构设计的文档要及时跟进完善,在抽象的过程中,我们需要整理和记录整个过程,以便为今后在完善时能够一下子翻阅到并记起当时在架构时所考虑的各方面问题的原因。\n","description":"","id":13,"section":"posts","tags":["software architecture"],"title":"架构","uri":"https://yichenlove.github.io/posts/software-architecture/"},{"content":"这本书的优点在于,以系统的、完整的、科学的方式,解答了我对爱情的困惑,对亲密关系有了较为全面的认知。\n 【系统性】在于,这本书在解析亲密关系的各个话题时,有从始至终的理论框架(例如相互依赖理论),让人能够形成系统的观点。相比之下,有些心理学公众号的文章,今天讲依恋类型,明天讲原生家庭,充斥着不同的理论和名词,每读一篇似乎学到了新的知识,却难以沉淀、成为自己的知识网络中的一部分,甚至不同文章的观点还会出现冲突。\n 【完整性】在于,这本书在分析亲密关系时,各个维度都有涉及。读这本书,不仅能学到 what you know that you dont know(你知道的你不知道的事),还能够学到 what you dont know that you dont know(你不知道你不知道的事)。平时我们也许会在有特定困惑的时候,才会去阅读相关的爱情心理学文章。比如在面临异地的时候,去学习如何维持异地感情。这些问题是已经浮在表面上的了,但感情中,还有更多我们可能根本没意识到的问题,比如我们并没有意识到关系中存在着控制与被控制。通过阅读这本书,能够让我们更加全面地了解亲密关系,从而能够识别潜在的问题。\n 【科学性】在于,这本书的结论基本是在科学研究的基础上得到的,而不是基于作者的个人情感经验。科学研究的结论避免了很多误区,例如研究发现相异并不相吸引,伴侣共同点越多,彼此越喜欢。有一些不科学的书籍,甚至为了哗众取宠,提出一些吸引人眼球的观点,如男人来自火星,女人来自金星——但其实男性和女性的差异并没有我们认为的那么大。所以这是一本相对靠谱的爱情心理学书籍。我并不是说它里面的观点都是正确的,毕竟心理学实验也有自己的局限性,但至少它会比不基于科学研究的书要可信地多。\n 下面是我整理的一些书中对我有帮助的内容,分享给大家\n 误区\u0026amp;正确观点:\n 在亲密关系中,男性和女性的差异并没有我们认为的那么大 “相异”并不相吸引:伴侣共同点越多,彼此越喜欢 我们往往不如自己以为得那样了解伴侣 结婚时间较短的配偶在推测伴侣的心思方面,比婚龄更长的配偶更准确;这是因为刚结婚时了解伴侣的动机更强 表达同情和关心最好的方法:“我很抱歉”或“为你感到悲哀”,然后打住。不要用乐观展望来安慰,也不要提供详细建议如何化悲痛为力量 当孩子出生,冲突会增多,对婚姻的满意度(以及对伴侣的爱)会减少 大多数羞怯者可能根本就不需要接受社交技能的正式培训,因为他们只要精神放松,不再担心别人的评判,就可以表现得坦荡豁达 感到嫉妒时,女性似乎关注于维护好现有的亲密关系,男性则会考虑离开,通过征服新的恋人来医治受伤的自尊;这说明女性利用引起男性嫉妒来维护关系方法可能事与愿违 冲突有益处,冲突能暴露潜在的问题和矛盾,这样才有可能寻求解决方法 对离婚的解释,人们往往过度关注关系背景(指伴侣们通过对彼此的知觉和互动缔造的亲密环境),而忽略文化规范和个人背景 欲擒故纵并没有多大作用:女性人为地延缓亲密关系的发展进程,对男性并没有特别的吸引力;对男人有吸引力的理想女人是除了他之外对所有人都故作清高的女性 重点概念:\n 依恋类型 工具性 \u0026amp; 表达性 归因 自我实现的预言 自我监控能力 倾听的糟糕表现 社会交换理论 总而言之,社会交换的三个重要因素是人们关系的结果、比较水平(CL)和替代的比较水平(CLalt)。人们在交往中得到的净盈亏就是他们关系的结果。如果他们的结果超过期望,或者CL,他们就感到满意;然而如果现有的亲密关系结果不如他们的期望(即结果低于CL),他们就不满意。此外,如果人们当前的结果好于从别处能得到的结果(即他们的结果超过他们的CLalt),他们就依赖于现在的伴侣,不太可能离开。然而,如果他们从现在的伴侣处得到的结果比他们从别处能获得的结果更差(他们的结果降到CLalt以下),他们就会倾向独立,很可能离开当前的伴侣。\n 爱情的类型 爱情的持久 关系中的张力 应对冲突的类型 冲突的结束方式 权力 有趣的发现:\n 吸引力的产生可能是无意识的,源于我们和别人的相似性,例如:人们更有可能爱上名字和自己名字类似的人 外貌吸引力:我们倾向于认为外貌俊美的人更讨人喜欢,更好相处;女性的外貌吸引力中,腰臀比例最重要 爱情是盲目的:家人和朋友对亲密关系未来走向的判断比当事人的判断更加准确,其中女方的女性朋友判断最为准确 伴侣间会有积极错觉:他们并不会忽视伴侣真实的缺点,只是认为这些缺憾并不如其他人认为的那么重要;积极错觉是否会带来危险,取决于积极错觉与现实不符合的程度 重构性记忆影响着亲密关系:如果当前幸福,人们倾向于忘记过去的不愉快;但如果感到痛苦,亲密关系在走下坡路,人们会低估过去曾经的幸福和情意 沟通中的性别差异:女性更常谈情感和人物话题,男性倾向于谈论不太亲密、不带个人色彩的内容 自我表露的性别差异:男性对女性较为开放,女性彼此之间也较为开放,男性对同性的自我表露则较少;这使得男性常常依赖于从女性那里得到温情和亲密,反过来女性却较少依赖于男性 要保持满意的亲密关系,我们或许需要保持至少5:1的奖赏-代价比率 大部分婚姻数年后满意度会下降,但有1/5的夫妻满意度能历久弥坚 亲密关系的质量和是否公平都影响了亲密关系满意度,但前者更重要;如果质量高了,人么就不会那么在意是否获益不足 成年期的二元退缩现象:人们与爱人见面的次数越来越多,而探望朋友的次数越来越少 友谊的性别差异:女性的友谊是“面对面”,以情感分享和自我表露为特征的;男性的友谊是“肩并肩”,围绕着共同活动、相伴相随和搞笑娱乐而展开的;女性之间的友谊往往比男性更亲切、紧密,文化规范和性别角色是主要的原因 性行为:(美国)95%的人有过婚前性行为;男性和女性第一次性交的平均年龄为17岁;对男性来说,每周3次以上的性行为更满足 不贞:美国21%的女性和32%的男性在性方面至少有一次出轨;男性更在意伴侣的肉体出轨,女性更在意伴侣的精神出轨 欺骗者猜疑:说谎会损害说谎者对被欺骗伴侣的信任;无论谎言是否被识破,说谎者都会感到不适 能宽恕亲密伴侣的人能享受到更多的幸福 归因式的争辩(为彼此的解释孰是孰非而争斗)通常很难得到解决 离婚的普遍性:美国婚姻的平均时长仅仅超过18年;首次离婚的年龄中位数约30岁 失败的婚姻,在结婚之前就有更多问题,并将此带入婚姻之中,使得这种婚姻一开始更脆弱 伴侣们在婚姻开始时抱有不切实际的乐观,婚后幻想破灭,这种破灭的程度能最好地预测离婚 与婚前伴侣分手普遍的方式(占1/3):逐渐积累的不满,使得伴侣一方一而再地努力解散关系,并且不会言明分手的意图,也不会进行任何改善或修复伴侣关系的尝试 大多数未婚伴侣分手后逐渐失去联系(60%) 父母离异的整体影响始终是负面的,但如果家庭充满冲突,则离婚会让儿童的境况变得更好 婚前咨询和婚姻治疗是有用的,越早处理婚姻问题,就越容易解决 最后,如果要总结这本书带给我的收获,我摘抄了以下两点:\n 1)100对婚龄持续了45年的满意夫妻解释他们成功的原因:\n 他们珍视婚姻,并认为婚姻是长期的承诺和忠诚 幽默感非常有益于婚姻 他们非常相似,在大部分事情上都能达成一致意见 他们真正地喜欢自己的配偶,喜欢与配偶共度美好时光 2)对亲密关系抱有信心:\n 约有三分之一人的不能轻松舒适地对待相互依赖的亲密感;他们要么担心伴侣不够爱自己,要么在走得太亲近时感觉不自在 ——这说明有三分之二的人的依恋类型是安全的 许多男性(约三分之一)也像女性惯常的那样温情脉脉、亲切体贴和敏感细腻。而不具备这些特点的男性也可能通过学习,变得比现在更加热情、更加具有表达性。 如果我们尽力去做,几乎所有人都能成为更体贴、更有魅力、更有奖赏价值的伴侣 ","description":"","id":14,"section":"publication","tags":null,"title":"《亲密关系》","uri":"https://yichenlove.github.io/publication/book/intimate-relationship/"},{"content":"在计算机科学中,链表作为一种基础的数据结构可以用来生成其它类型的数据结构。链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接(\u0026ldquo;links\u0026rdquo;)。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的访问往往要在不同的排列顺序中转换。而链表是一种自我指示数据类型,因为它包含指向另一个相同类型的数据的指针(链接)。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表,双向链表以及循环链表。\n 链表的基本思维是,利用结构体的设置,额外开辟出一份内存空间去作指针,它总是指向下一个结点,一个个结点通过NEXT指针相互串联,就形成了链表。\n其中DATA为自定义的数据类型,NEXT为指向下一个链表结点的指针,通过访问NEXT,可以引导我们去访问链表的下一个结点。\n 对于一连串的结点而言,就形成了链表如下图:\n上文所说的插入删除操作只需要修改指针所指向的区域就可以了,不需要进行大量的数据移动操作。如下图:\n相比起数组,链表解决了数组不方便移动,插入,删除元素的弊端,但相应的,链表付出了更加大的内存牺牲换来的这些功能的实现。\n单链表,双链表,循环单链表如下图:\n单链表 尾插入法创建单链表\n插入操作\n删除操作\n双向链表 插入操作\n删除操作\n循环链表 循环链表的创建操作\n插入操作\n删除操作\n","description":"","id":15,"section":"posts","tags":["data structure"],"title":"链表—链式存储","uri":"https://yichenlove.github.io/posts/listnode/"},{"content":"一、堆Heap 堆是一种经过排序的树形数据结构,每个节点都有一个值,通常我们所说的堆的数据结构是指二叉树。所以堆在数据结构中通常可以被看做是一棵树的数组对象。而且堆需要满足一下两个性质:\n(1)堆中某个节点的值总是不大于或不小于其父节点的值;\n(2)堆总是一棵完全二叉树。\n 堆分为两种情况,有最大堆和最小堆。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。下图图一就是一个最大堆,图二就是一个最小堆。在一个摆放好元素的最小堆中,可以看到,父结点中的元素一定比子结点的元素要小,但对于左右结点的大小则没有规定谁大谁小。\n 堆常用来实现优先队列,堆的存取是随意的,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。\n 堆二、栈 Stak 栈是限定仅在表尾进行插入和删除操作的线性表。我们把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈的特殊之处在于它限制了这个线性表的插入和删除位置,它始终只在栈顶进行。\n 而且栈是一种具有后进先出的数据结构,又称为后进先出的线性表,简称 LIFO(Last In First Out)结构。也就是说后存放的先取,先存放的后取,这就类似于我们要在取放在箱子底部的东西(放进去比较早的物体),我们首先要移开压在它上面的物体(放进去比较晚的物体)。\n 堆栈中定义了一些操作。两个最重要的是PUSH和POP。PUSH操作在堆栈的顶部加入一个元素。POP操作相反,在堆栈顶部移去一个元素,并将堆栈的大小减一。\n 注意:其实堆栈本身就是栈,只是换了个抽象的名字。 栈栈的应用——递归 在高级语言中,调用自己和其它函数没有本质的不同。我们把一个直接用自己或通过一系列的调用语句间接地调用自己的函数,称作递归函数。每个递归函数必须至少有一个条件,满足时递归不再执行,即不再引用自身而是返回值退出。\n 递归和迭代的区别是:迭代使用的是循环结构,递归使用的是选择结构。 递归能使程序的结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间。但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存。迭代则不需要反复调用函数和占用额外的内存。因此我们应该视不同情况选择不同的代码实现方式。\n 在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。\n 三、队列 Queue 队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。允许插入的一端称为队尾,允许删除的一端称为队头。它是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,和栈一样,队列是一种操作受限制的线性表。\n 而且队列是一种先进先出的数据结构,又称为先进先出的线性表,简称 FIFO(First In First Out)结构。也就是说先放的先取,后放的后取,就如同行李过安检的时候,先放进去的行李在另一端总是先出来,后放入的行李会在最后面出来。\n 解决假溢出的办法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。\n 队列以上是数据结构中的堆、栈和队列,另补充一点:内存分配中的堆区和栈区 内存中的堆和栈第一个区别就是申请方式的不同:栈是系统自动分配空间的,而堆则是程序员根据需要自己申请的空间。由于栈上的空间是自动分配自动回收的,所以栈上的数据的生存周期只是在函数的运行过程中,运行后就释放掉,不可以再访问。而堆上的数据只要程序员不释放空间,就一直可以访问到,不过缺点是一旦忘记释放会造成内存泄露。\n 申请效率的比较:栈由系统自动分配,速度较快。但程序员是无法控制的。堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。\n 申请大小的限制: 栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在Windows下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。\n 堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。\n ","description":"","id":16,"section":"posts","tags":["data structure"],"title":"数据结构中堆、栈和队列的理解Stak Heap Queue","uri":"https://yichenlove.github.io/posts/stak-heap-queue/"},{"content":"List 底层代码剖析 List是C#中最常见的可伸缩数组组件,我们常常用来替代数组使用,因为它是可伸缩的,所以我们在使用的时候可以不用手动去分配数组的大小。那么它的底层是怎么实现的,每次Add和remove以及赋值,在内部是怎么执行和运作的呢?我们可以查看它的源码分析。\n源码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class List\u0026lt;T\u0026gt; : IList\u0026lt;T\u0026gt;, System.Collections.IList, IReadOnlyList\u0026lt;T\u0026gt; { private const int _defaultCapacity = 4; private T[] _items; private int _size; private int _version; private Object _syncRoot; static readonly T[] _emptyArray = new T[0]; // Constructs a List. The list is initially empty and has a capacity // of zero. Upon adding the first element to the list the capacity is // increased to 16, and then increased in multiples of two as required. public List() { _items = _emptyArray; } // Constructs a List with a given initial capacity. The list is // initially empty, but will have room for the given number of elements // before any reallocations are required. // public List(int capacity) { if (capacity \u0026lt; 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum); Contract.EndContractBlock(); if (capacity == 0) _items = _emptyArray; else _items = new T[capacity]; } //... //其他内容 } 分析List构造部分,List内部是使用数组实现的,而不是链表。没有给予特定容量时,初始容量为0.\nList组件在Add,Remove两个函数调用时都采用的是“从原数组拷贝生成到新数组”的方式工作的。\nAdd接口源码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // Adds the given object to the end of this list. The size of the list is // increased by one. If required, the capacity of the list is doubled // before adding the new element. // public void Add(T item) { if (_size == _items.Length) EnsureCapacity(_size + 1); _items[_size++] = item; _version++; } // Ensures that the capacity of this list is at least the given minimum // value. If the currect capacity of the list is less than min, the // capacity is increased to twice the current capacity or to min, // whichever is larger. private void EnsureCapacity(int min) { if (_items.Length \u0026lt; min) { int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2; // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. // Note that this check works even when _items.Length overflowed thanks to the (uint) cast if ((uint)newCapacity \u0026gt; Array.MaxArrayLength) newCapacity = Array.MaxArrayLength; if (newCapacity \u0026lt; min) newCapacity = min; Capacity = newCapacity; } } Add函数,每次增加一个元素的数据,Add接口都会首先检查的是容量还够不够,如果不够则用 EnsureCapacity 来增加容量。\n1 int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2; 每次容量不够的时候,整个数组的容量都会扩充一倍,_defaultCapacity 是容量的默认值为4。因此整个扩充的路线为4,8,16,32,64,128,256,512,1024…依次类推。\nList使用数组形式作为底层数据结构,好处是使用索引方式提取元素很快,但在扩容的时候就会很糟糕,每次new数组都会造成内存垃圾,这给垃圾回收GC带来了很多负担。\nRemove接口源码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 // Removes the element at the given index. The size of the list is // decreased by one. // public bool Remove(T item) { int index = IndexOf(item); if (index \u0026gt;= 0) { RemoveAt(index); return true; } return false; } // Returns the index of the first occurrence of a given value in a range of // this list. The list is searched forwards from beginning to end. // The elements of the list are compared to the given value using the // Object.Equals method. // // This method uses the Array.IndexOf method to perform the // search. // public int IndexOf(T item) { Contract.Ensures(Contract.Result\u0026lt;int\u0026gt;() \u0026gt;= -1); Contract.Ensures(Contract.Result\u0026lt;int\u0026gt;() \u0026lt; Count); return Array.IndexOf(_items, item, 0, _size); } // Removes the element at the given index. The size of the list is // decreased by one. // public void RemoveAt(int index) { if ((uint)index \u0026gt;= (uint)_size) { ThrowHelper.ThrowArgumentOutOfRangeException(); } Contract.EndContractBlock(); _size--; if (index \u0026lt; _size) { Array.Copy(_items, index + 1, _items, index, _size - index); } _items[_size] = default(T); _version++; } 从源码中我们可以看到,元素删除的原理其实就是用 Array.Copy 对数组进行覆盖。\n再看来 Insert 接口源码:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // Inserts an element into this list at a given index. The size of the list // is increased by one. If required, the capacity of the list is doubled // before inserting the new element. // public void Insert(int index, T item) { // Note that insertions at the end are legal. if ((uint) index \u0026gt; (uint)_size) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_ListInsert); } Contract.EndContractBlock(); if (_size == _items.Length) EnsureCapacity(_size + 1); if (index \u0026lt; _size) { Array.Copy(_items, index, _items, index + 1, _size - index); } _items[index] = item; _size++; _version++; } 与Add接口一样,先检查容量是否足够,不足则扩容。从源码中获悉,Insert插入元素时,使用的用拷贝数组的形式,将数组里的指定元素后面的元素向后移动一个位置。\n可以看到 List 的效率并不高,只是通用性强而已,大部分的算法都使用的是线性复杂度的算法,这种线性算法当遇到规模比较大的计算量级时就会导致CPU的大量损耗。\nList的内存分配方式也极为不合理,当List里的元素不断增加时,会多次重新new数组,导致原来的数组被抛弃,最后当GC被调用时造成回收的压力。\n最后List并不是高效的组件,真实情况是,他比数组的效率还要差的多,他只是个兼容性比较强得组件而已,好用,但效率差。\n参考文章 《Unity3D高级编程之进阶主程》第一章,C#要点技术(一) - List 底层源码剖析\n","description":"","id":17,"section":"posts","tags":["C#"],"title":"C# List 底层源码剖析","uri":"https://yichenlove.github.io/posts/csharp-list/"},{"content":"C# Interface接口定义了所有继承接口时应遵循的语法合同。\n接口可以是命名空间或类的成员。接口声明可以包含一下成员的声明(没有任何实现的签名):方法、属性、索引器、事件,这些都是接口的成员。接口只包含了成员的声明。成员的定义是派生类的责任。接口提供了派生类应遵循的标准结构。\n接口使用Interface关键字声明。接口声明默认是public的。实例:\n1 2 3 4 interface IMyInterface { void MethodToImplement(); } 以上代码定义了接口 IMyInterface。通常接口命令以 I 字母开头,这个接口只有一个方法 MethodToImplement(),没有参数和返回值,当然我们可以按照需求设置参数和返回值。\n继承\u0026quot;基类\u0026quot;跟继承\u0026quot;接口\u0026quot;都能实现某些相同的功能,但有些接口能够完成的功能是只用基类无法实现的\n1.接口用于描述一组类的公共方法/公共属性. 它不实现任何的方法或属性,只是告诉继承它的类 《至少》要实现哪些功能,继承它的类可以增加自己的方法.\n2.使用接口可以使继承它的类: 命名统一/规范,易于维护.比如: 两个类 \u0026ldquo;狗\u0026quot;和\u0026quot;猫\u0026rdquo;,如果它们都继承了接口\u0026quot;动物\u0026quot;,其中动物里面有个方法Behavior(),那么狗和猫必须得实现Behavior()方法,并且都命名为Behavior这样就不会出现命名太杂乱的现象.如果命名不是Behavior(),接口会约束即不按接口约束命名编译不会通过.\n3.提供永远的接口。 当类增加时,现有接口方法能够满足继承类中的大多数方法,没必要重新给新类设计一组方法,也节省了代码,提高了开发效率.\n","description":"","id":18,"section":"posts","tags":["C#"],"title":"C# 接口Interface及其作用","uri":"https://yichenlove.github.io/posts/csharp-interface/"},{"content":"Vector3 struct in UnityEngine\n 切换到手册\n描述 用于表示 3D 向量和点。\n Unity 内部使用该结构传递 3D 位置和方向。 此外,它还包含用于执行常见向量操作的函数。\n 除了下面列出的函数以外,也可以使用其他类操作向量和点。 例如,对于旋转或变换向量和点来说,Quaternion 和 Matrix4x4 类也很有用。\n静态变量 表头 表头 back 用于编写 Vector3(0, 0, -1) 的简便方法。 down 用于编写 Vector3(0, -1, 0) 的简便方法。 forward 用于编写 Vector3(0, 0, 1) 的简便方法。 left 用于编写 Vector3(-1, 0, 0) 的简便方法。 negativeInfinity 用于编写 Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity) 的简便方法。 one 用于编写 Vector3(1, 1, 1) 的简便方法。 positiveInfinity 用于编写 Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity) 的简便方法。 right 用于编写 Vector3(1, 0, 0) 的简便方法。 up 用于编写 Vector3(0, 1, 0) 的简便方法。 zero 用于编写 Vector3(0, 0, 0) 的简便方法。 变量 表头 表头 magnitude 返回该向量的长度。(只读) normalized 返回 magnitude 为 1 时的该向量。(只读) sqrMagnitude 返回该向量的平方长度。(只读) this[int] 分别使用 [0]、[1]、[2] 访问 x、y、z 分量。 x 向量的 X 分量。 y 向量的 Y 分量。 z 向量的 Z 分量。 构造函数 表头 表头 Vector3 使用给定的 x、y、z 分量创建新向量。 公共函数 表头 表头 Equals 如果给定向量与该向量完全相等,则返回 true。 Set 设置现有 Vector3 的 x、y 和 z 分量。 ToString Returns a formatted string for this vector. 静态函数 表头 表头 Angle Calculates the angle between vectors from and. ClampMagnitude 返回 vector 的副本,其大小被限制为 /maxLength/。 Cross 两个向量的叉积。 Distance 返回 a 与 b 之间的距离。 Dot 两个向量的点积。 Lerp 在两个点之间进行线性插值。 LerpUnclamped 在两个向量之间进行线性插值。 Max 返回由两个向量的最大分量组成的向量。 Min 返回由两个向量的最小分量组成的向量。 MoveTowards 计算 current 指定的点与 target 指定的点之间的位置,移动距离不超过 maxDistanceDelta 指定的距离。 Normalize 使该向量的 magnitude 为 1。 OrthoNormalize 将向量标准化并使它们彼此正交。 Project 将向量投影到另一个向量上。 ProjectOnPlane 将向量投影到由法线定义的平面上(法线与该平面正交)。 Reflect 从法线定义的平面反射一个向量。 RotateTowards 将向量 current 朝 target 旋转。 Scale 将两个向量的分量相乘。 SignedAngle Calculates the signed angle between vectors from and to in relation to axis. Slerp 在两个向量之间进行球形插值。 SlerpUnclamped 在两个向量之间进行球形插值。 SmoothDamp 随时间推移将一个向量逐渐改变为所需目标。 运算符 表头 表头 operator - 将一个向量减去另一个向量。 operator != Returns true if vectors are different. operator * 将向量乘以一个数值。 operator / 将向量除以一个数值。 operator + 将两个向量相加。 operator == 如果两个向量大致相等,则返回 true。 ","description":"","id":19,"section":"posts","tags":["Unity"],"title":"UnityEngine.Vector3 - Unity 脚本 API","uri":"https://yichenlove.github.io/posts/unity-vector3/"},{"content":"希尔排序 学习记录 希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。\n 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。\n 希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。\n我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2\u0026hellip;1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。\n 代码实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 import java.util.Arrays; public class ShellSort { public static void main(String[] args) { int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0}; sort(arr); System.out.println(Arrays.toString(arr)); int[] arr1 = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0}; sort1(arr1); System.out.println(Arrays.toString(arr1)); } /** * 希尔排序 针对有序序列在插入时采用交换法 * @param arr */ public static void sort(int []arr){ // 增量gap,并逐步缩小增量 for (int gap = arr.length/2; gap \u0026gt; 0; gap/=2) { // 从gap个元素,逐个对其所在组进行直接插入排序操作 for (int i = gap; i \u0026lt; arr.length; i++) { int j = i; while (j - gap \u0026gt;= 0 \u0026amp;\u0026amp; arr[j] \u0026lt; arr[j - gap]) { //插入排序采用交换法 swap(arr, j, j - gap); j -= gap; } } } } /** * 希尔排序 针对有序序列在插入时采用移动法。 * @param arr */ public static void sort1(int[] arr) { // 增量gap,并逐步缩小增量 for (int gap = arr.length/2; gap \u0026gt; 0; gap/=2) { // 从gap个元素,逐个对其所在组进行直接插入排序操作 for (int i = gap; i \u0026lt; arr.length; i++) { int j = i; int tmp = arr[j]; if (arr[j] \u0026lt; arr[j - gap]) { while (j - gap \u0026gt;= 0 \u0026amp;\u0026amp; tmp \u0026lt; arr[j - gap]) { // 移动法 arr[j] = arr[j - gap]; j -= gap; } arr[j] = tmp; } } } } /** * 交换数组元素 * @param arr * @param a * @param b */ public static void swap(int[] arr, int a, int b) { arr[a] = arr[a] + arr[b]; arr[b] = arr[a] - arr[b]; arr[a] = arr[a] - arr[b]; } } ","description":"","id":20,"section":"posts","tags":["algorithms","sort"],"title":"Shellsort Algorithm","uri":"https://yichenlove.github.io/posts/shellsort-algorithm/"},{"content":"三大主流编程语言 HLSL/GLSL/Cg Shader Language Shader Language的发展方向是设计出在便携性方面可以和C++、Java等相比的高级语言,赋予程序员灵活而方便的编程方式,并尽可能的控制渲染过程同时利用图形硬件的并行性,提高算法效率。\nShader Language目前主要有3种语言:基于OpenGL的OpenGL Shading Language,简称GLSL;基于DirectX的High Level Shading Language,简称HLSL;还有NVIDIA公司的C for Graphic,简称Cg语言。\n OpenGL简介 OpenGL(全写Open Graphics Library)是一个定义了跨编程语言、跨平台的编程接口规格的专业图形程序接口。它用于三维图像(二维亦可),是一个功能强大,调用方便的底层图形库。OpenGL是行业领域中最为广泛接纳的2D/3D图形API,其自诞生至今已催生了各种计算机平台及设备上的数千优秀应用程序。它独立于视窗操作系统或其他操作系统的,亦是网络透明的。在包含CAD、内容创作、能源、娱乐、游戏开发、制造业及虚拟现实等行业领域中。OpenGL是一个与硬件无关的软件接口,可以在不同的平台如Windows 95、Windows NT、Unix、Linux、MacOS、OS/2之间进行移植。因此,支持OpenGL的软件具有很好的移植性,可以获得非常广泛的应用。\n OpenGL的发展一直处于一种较为迟缓的态势,每次版本的提高新增的技术很少,大多只是对其中部分作出修改和完善。1992年7月,SGI公司发布了OpenGL的1.0版本,随后又与微软公司共同开发了Windows NT版本的OpenGL,从而使一些原来必须在高档图形工作站上运行的大型3D图形处理软件也可以在微机上运用。1995年OpenGL的1.1版本面世,该版本比1.0的性能有许多提高,并加入了一些新的功能,其中包括改进打印机支持,在增强元文件中包含OpenGL的调用,顶点数组的新特性,提高顶点位置、法线、颜色、色彩指数、纹理坐标、多边形边缘标识的传输速度,引入了新的纹理特性等等。OpenGL 1.5又新增了OpenGL Shading Language,该语言是OpenGL 2.0的底核,用于着色对象、顶点着色以及片段着色技术的扩展功能。\n DirectX简介 DirectX(Direct eXtension,简称DX)是由微软公司创建的多媒体编程接口。由C++编程语言实现,遵循COM。被广泛适用于Microsoft Windows、Microsoft XBOX、Microsoft XBOX 360和Microsoft XBOX ONE电子游戏开发,并且只能支持这些平台。最新版本为DirextX 12,创建在最新的Windows 10。DirectX是这样一组技术:它们旨在使基于Windows的计算机成为运行和显示具有丰富多媒体元素(例如全色图形、视频、3D动画和丰富音频)的应用程序的理想平台。DirectX包括安全和性能更新程序,以及许多涵盖所有技术的新功能。应用程序可以通过使用DirectX API来访问这些新功能。\n DirectX加强3D图形个声音效果,并提供设计人员一个共同的硬件驱动标准,让游戏开发者不必为每一品牌的硬件来写不同的驱动程序,也降低了用户安装及设置硬件的复杂度。从字面意义上说,Direct就是直接的意思,而后边的X则代表了很多意思,从这一点上可以看出DirectX的出现就是为了众多软件提供直接服务的。\n 举例来说,以前在DOS下玩家玩游戏时,并不是安装上就可以玩了,他们往往首先要设置声卡的品牌和型号,然后还要设置IRQ(中断)、I/O(输入和输出)、DMA(存取模式),如果哪项设置不对,那么游戏声音就发不出来。这部分的设置不仅让玩家伤透脑筋,对游戏开发者来说就更为头痛,为了让游戏能够正确运行,开发者必须在游戏制作之初,把市面上所有声卡硬件数据都收集过来,然后根据不同的API(应用编程接口)来写不同的驱动程序。这对于游戏制作公司来说,是很难完成的,所以在当时多媒体游戏很少。微软正是看到了这个问题,为众厂家推出了一个共同的应用程序接口——DirectX。只要游戏是依照DirectX来开发的,不管显卡、声卡型号如何,统统都能玩,而且还能发挥最佳的效果。当然,前提是使用的显卡、声卡的驱动程序必须支持DirectX才行。\n Cg GLSL与HLSL分别基于OpenGL和Direct3D的接口,两者不能混用,事实上OpenGL和Direct3D一直都是冤家对头,争斗良久。OpenGL在其长期发展中积累下的用户群庞大,这些用户会选择GLSL学习。GLSL继承了OpenGL的良好移植性,一度在Unix等操作系统上独领风骚。但GLSL的语法体系自成一家。微软的HLSL移植性较差,在Windows平台上可谓一家独大,这一点在很大程度上限制了HLSL的推广和发展。但是HLSL用于DX游戏领域却是深入人心。\nCg语言(C for Graphic)是为GPU编程设计的高级着色语言,Cg极力保留C语言的大部分语义,并让开发者从硬件细节中解脱出来,Cg同时也有一个高级语言的其它好处,如代码的易重用性,可读性得到提高,编译器代码优化。Cg是一个可以被OpenGL和Direct3D广泛支持的图形处理器编程语言。Cg语言和OpenGL、Direct3D并不是同一层次的语言,而是OpenGL和DirectX的上层,即Cg程序是运行在OpenGL和DirectX标准顶点和像素着色的基础上的。Cg由NVIDIA公司和微软公司相互协作在标准硬件光照语言的语法和语义上达成了一致开发。所以,HLSL和Cg其实是同一种语言。\n 总结\n Unity官方手册上讲Shader程序嵌入的小片段是用Cg/HLSL编写的,从CGPROGRAM开始,到CGEND结束。所以,Unity官方主要是用Cg/HLSL编写Shader程序片段。Unity官方手册也说明对于Cg/HLSL程序进行扩展也可以使用GLSL,不过Unity官方建议使用原生的GLSL进行编写和测试。如果不使用原生GLSL,你就需要知道你的平台必须是Mac OS X、OpenGL ES 2.0以上的移动设备或者是Linux。在一般情况下Unity会把Cg/HLSL交叉编译成优化过的GLSL。因此我们有多种选择,我们既可以考虑使用Cg/HLSL,也可以使用GLSL。不过由于Cg/HLSL更好的跨平台性,更倾向于使用Cg/HLSL编写Shader程序。\n Unity 2019以上版本已经只使用HLSL编写Shader程序。 Unity 最初使用 Cg 语言,因此会使用 Unity 某些着色器关键字的名称 (CGPROGRAM) 和文件扩展名 (.cginc)。Unity 已不再使用 Cg,但仍支持这些关键字和文件扩展名。请注意,所有着色器程序代码都必须是有效的 HLSL,即使代码使用与 Cg 相关的关键字和文件扩展名也如此。\n ","description":"","id":21,"section":"posts","tags":null,"title":"Shader Language","uri":"https://yichenlove.github.io/posts/shader-language/"},{"content":"一、前言 今天分享 splits 配置,从字面意思知道有着 切开 的意思,他的作用其实是帮我们把 apk 包从不同维度进行切开,减小apk的大小,从而让用户在下载时节省流量。\n 二、splits 的结构简析 1、splits 的存在位置 Splits 会映射为 com.android.build.gradle.internal.dsl.Splits类,没有继承任何类。\n 2、如何运行 splits 主要是用于打包时的拆包,所以我们需要的是进行apk的打包编译。\n // app:clean 为了先清空之前的文件 // app:aR 进行编译 release 包 // mac 使用 ./gradlew // window 使用 gradlew ./gradlew app:clean app:aR 三、splits 的属性 1、abi 类型:AbiSplitOptions 描述:对 abi 进行分包处理,具体我们看下面 AbiSplitOptions 讲解。 2、AbiSplitOptions 类型 2.1 enable 描述:是否开启 abi 分包,默认不开启 使用: 1 2 3 4 5 6 splits { abi { enable true } } 效果图: 2.2 exclude 描述:排除不需要的架构。 使用: 1 2 3 4 5 6 7 8 abi { // 是否开启 \tenable true // 排除不必要的架构 \texclude \u0026#39;x86\u0026#39;,\u0026#39;arm64-v8a\u0026#39; } 效果图: 2.3 reset 描述:清除默认架构列表。当我们开启abi 分包时,gradle会帮我们初始化一个架构列表,例如 enable 小节中,我们并没有设置任何架构,而gradle会帮我们分出 “arm64-v8a”、“armeabi-v7a”、“x86”、“x86_64”。 初始化列表会因为gradle的版本不同有所改变\n 使用: 1 2 3 4 5 6 7 8 abi { // 是否开启 enable true // 重置包含的目录 \treset() } 2.4 include 描述:设置我们需要的架构。注意的是,我们需要先使用reset方法将默认列表清空,然后再设置。\n 使用:\n 1 2 3 4 5 6 7 8 9 10 abi { // 是否开启 \tenable true // 重置包含的目录,因为已经是包含全部 \treset() // 设置包含,调用前需要先用 reset 将默认清除 \tinclude \u0026#39;armeabi-v7a\u0026#39;, \u0026#39;x86\u0026#39; } 效果图: 2.5 universalApk 描述:是否编译一个包含全部架构的apk。 使用: 1 2 3 4 5 6 7 8 abi { // 是否开启 \tenable true // 是否打出包含全部的apk \tuniversalApk true } 效果图: 3、density 类型:DensitySplitOptions 描述:对 分辨率 进行分包处理,具体我们看下面 DensitySplitOptions 讲解。 4、DensitySplitOptions 类型 4.1 enable 描述:是否开启 abi 分包,默认不开启 使用: 1 2 3 4 5 density { // 开启 \tenable true } 效果图: 4.2 exclude 描述:排除不需要的分辨率 使用: 1 2 3 4 5 6 7 8 density { // 开启 \tenable true // 排除分辨率 \texclude \u0026#39;hdpi\u0026#39;, \u0026#39;ldpi\u0026#39;, \u0026#39;mdpi\u0026#39;, \u0026#39;xhdpi\u0026#39; } 效果图: 4.3 reset 描述:重置默认的分辨率列表。默认会帮我们添加 \u0026ldquo;ldpi\u0026rdquo;、\u0026ldquo;mdpi\u0026rdquo;、\u0026ldquo;hdpi\u0026rdquo;、\u0026ldquo;xhdpi\u0026rdquo;、\u0026ldquo;xxhdpi\u0026rdquo;、\u0026ldquo;xxxhdpi\u0026rdquo;。 默认列表会根据gradle的版本有所不同。\n 使用: 1 2 3 4 5 6 7 8 density { // 开启 \tenable true // 重置默认分辨率列表 \treset() } 4.4 include 描述:添加需要的分辨率。值得一提的是,我们需要先用 reset 方法进行清空默认列表。 使用: 1 2 3 4 5 6 7 8 9 10 11 density { // 开启 \tenable true // 重置默认分辨率列表 \treset() // 包含分辨率 \tinclude \u0026#39;hdpi\u0026#39;, \u0026#39;xxhdpi\u0026#39; } 效果图 4.5 compatibleScreens 描述:指定与应用程序兼容的屏幕尺寸。会在 AndroidManifest.xml 中添加一个匹配的 \u0026lt; compatible-screens \u0026gt; 节点。不过官方并不建议这么使用,因为会限制应用支持的设备类型。我们应该尽可能的支持多种设备。 值得一提的是,\u0026lt; compatible-screens \u0026gt; 节点并不会在 apk 的安装和使用过程中发挥最用,它是提供给外部使用的,例如google play。\n 使用: 1 2 3 4 5 6 7 8 density { // 开启 \tenable true // 会在 manifest 中添加 \u0026lt; compatible-screens\u0026gt;\u0026lt;screen ... \u0026gt; \tcompatibleScreens \u0026#39;small\u0026#39;, \u0026#39;normal\u0026#39;, \u0026#39;large\u0026#39;, \u0026#39;xlarge\u0026#39; } 效果图 5、language 仅当为Android Instant Apps构建配置APK时,才支持构建每个语言的APK。\n 类型:LanguageSplitOptions 描述:对 语言 进行分包处理,具体我们看下面 LanguageSplitOptions 讲解。 6、LanguageSplitOptions 类型 6.1 enable 描述:开启语言分包。 使用: 1 2 3 4 language { enable = true } 6.2 include 描述:设置需要分包的语言。 使用: 1 2 3 4 5 language { enable = true include \u0026#34;fr\u0026#34;, \u0026#34;zh\u0026#34;, \u0026#34;en\u0026#34; } ","description":"","id":22,"section":"posts","tags":["android"],"title":"安卓gradle splits","uri":"https://yichenlove.github.io/posts/build-gradle-splits/"},{"content":"二项式分布算法(Java实现) 最近开始看《算法(第四版)》,这是遇到的第一个算法题。题目要求的只是估计一下算法递归的次数,但我进一步研究了不同的计算二项式分布的算法。\n 二项式分布简介 这里就不过多展开,以自己的语言解释一下。\n 某一个实验出现事件A的概率为p, 且重复该实验结果不相互干扰(独立)\n求重复N次该实验时,出现k次事件A的概率:\n上式即为本博客探讨的问题\n利用二项式公式实现 V1.0 学数学时候比较熟悉的公式就是\n 1 2 3 4 5 6 7 8 9 10 public static double myBinomial(int N, int k, double p) { long c = N; for(int i = N-1; i \u0026gt;= N-k+1; --i) c*=i; for(int i = 2; i \u0026lt;= k; ++i) c/=i; double a1 = Math.pow(p, k); double a2 = Math.pow(1-p, N-k); return c*a1*a2; } 改进版 V1.1 改进版本聚焦V1.0中的一个问题:当计算 的时候,有两个方式:\n 这两个方式的运算量不相同,在具体运算时可以进行选择\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static double myBinomial2(int N, int k, double p) { long c = N; if(k \u0026lt; N-k) { for(int i = N-1; i \u0026gt;= N-k+1; --i) c*=i; for(int i = 2; i \u0026lt;= k; ++i) c/=i; } else { for(int i = N-1; i \u0026gt;= k+1; --i) c*=i; for(int i = 2; i \u0026lt;= N-k; ++i) c/=i; } double a1 = Math.pow(p, k); double a2 = Math.pow(1-p, N-k); return c*a1*a2; } 利用递归实现 V2.0 这是示例的代码,在官网可以查看\n 其基本思想是:\n将大问题逐次拆分为小问题的递归思想\n1 2 3 4 5 public static double binomial1(int N, int k, double p) { if (N == 0 \u0026amp;\u0026amp; k == 0) return 0; if (N \u0026lt; 0 || k \u0026lt; 0) return 0; return (0 - p) *binomial1(N-1, k, p) + p*binomial1(N-1, k-1, p); } 将递归改进为迭代 V2.1 这里的方法挺巧妙的!\n将递归的步骤逆过来,变成了填格子——递归自上而下,该方法则化为自下而上的迭代法。\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static double binomial2(int N, int k, double p) { double[][] b = new double[N+1][k+1]; // base cases for (int i = 0; i \u0026lt;= N; i++) b[i][0] = Math.pow(0 - p, i); b[0][0] = 0; // recursive formula for (int i = 1; i \u0026lt;= N; i++) { for (int j = 1; j \u0026lt;= k; j++) { b[i][j] = p * b[i-1][j-1] + (0 - p) *b[i-1][j]; StdOut.printf(\u0026#34;%f\\\\t\u0026#34;, b[i][j]); } StdOut.println(); } return b[N][k]; } 此方法虽然巧妙,但其实速度上比不过公式法(复杂度达到 O(N^2),而且有一部分计算是不需要的(有些空不需要填)\n 参考文章\n","description":"","id":23,"section":"posts","tags":["algorithms"],"title":"二项式分布算法(Java实现)","uri":"https://yichenlove.github.io/posts/java-binomial-distribution/"},{"content":"伯努利分布(Bernoulli Distribution)是一种离散分布,在概率学中非常常用,有两种可能的结果:\n 1 表示成功,出现的概率为 p(其中 0\u0026lt;p\u0026lt;1); 0 表示失败,出现的概率为 q=1-p。 这很好理解,除去成功都是失败,p 是成功的概率,概率 100% 减去 p 就是失败的概率。\n图 1 雅各布·伯努利\n伯努利分布是为纪念瑞士科学家雅各布·伯努利(Jakob Bernoulli)(图 1)而命名的。这里值得一提的是伯努利家族。瑞士的伯努利家族(也译作贝努力)是一个很伟大的家族,一个家族 3 代人中产生了 8 位科学家,后裔有不少于 120 位被人们系统地追溯过,他们在数学、自然科学、技术、工程乃至法律、管理、文学、艺术等方面享有名望,有的甚至声名显赫。\n伯努利分布的分布律如下:\n看上去像个分段函数是不是,它也可以写作:\n这两个写法其实说的是一回事,你自己可以把 n=0 和 n=1 分别带进去算一算。\n伯努利分布的应用需满足以下两个条件。\n 各次试验中的事件是互相独立的,每一次 n=1 和 n=0 的概率分别为 p 和 q。 每次试验都只有两种结果,即 n=0,或 n=1。 如果不满足这两个条件,则分布不是伯努利分布。\n满足伯努利分布的样本有一个非常重要的性质,即满足下面公式:\n我们解释一下这个公式的含义。\n其中,X 指的是试验的次数,Cnk 指的是组合,也就是:\n这个公式表示,如果一个试验满足:\n的伯努利分布,那么在连续试验 n 次的情况下,出现 n=1 的情况发生恰好 k 次的概率为:\n其中,n=1 就是对应概率为 p 的情况。\n下面用一个小例子来说明。例如,张三参加英语雅思考试,每次考试通过的概率为 1/3,不通过的概率为 2/3。如果他连续考试 4 次,那么恰好通过 2 次的概率为多少?\n在这个例子里可以比较容易看到,P=1/3,n=4,k=2。代入公式:\n因此概率为 8/27。\n这个例子也可以用排列组合来计算。一共 4 次考试,2 次通过,一共有 6 种情况,如表 2 所示。\n表 2 通过的情况\n试着求每次的概率,情况 1,即第 1 次通过且第 2 次通过且第 3 次不通过且第 4 次不通过。这里千万不要漏掉后面两个条件,后面两次必须是不通过,否则条件就和公式不匹配了。\n那么,第 1 次通过,概率为 1/3,第 2 次通过,概率为 1/3,第 3 次不通过,概率为 2/3,第 4 次不通过,概率为 2/3。这 4 个条件都发生的概率为:\n同理,情况 2 到情况 6 的概率都是 4/81。所以最后的结果是:\n结果是完全一样的。\n对于满足伯努利分布的试验来说,用古典概型进行计算显得复杂和繁琐,尤其是 n 和 k 比较大的时候用古典概型来做就太不方便了。\n伯努利分布的应用场景其实远比这个例子丰富,读者有兴趣可以再继续寻找其他题目试解。\n","description":"","id":24,"section":"posts","tags":["algorithms"],"title":"伯努利分布及计算公式(二项分布)","uri":"https://yichenlove.github.io/posts/mathemticsbinomial/"},{"content":"简单来说,图床就是一个在网络上存储图片的地方,目的是为了节省本地服务器空间,做到一次存储,多个地方引用,加快图片的打开速度, Markdown文档、搭建博客,图片分享等,通过图床工具,可以快速将图片转换成更加容易分享的链接,对提高工作和交流的效率有重要作用,还有一点至关重要,就是工具的稳定性、快捷、免费,今天介绍的图床不受网络条件的限制,无论上传还是访问都可以稳定的链接\n演示 拖拽上传\n 剪贴板+快捷键上传\n 另外还有剪切板和url按钮上传可自行尝试\n 配置 github 免费的代码托管服务平台\n picgo 图床工具\n jsdelivr cdn加速\n github配置 1.注册github,新建项目 2.填写项目信息 3.生成token 打开设置\n 打开 Developer settings\n 打开 Personal access tokens后,点击Generate new token\n 生成token,复制token\n picGo配置 下载安装\n 详细配置\n 注意 图中的步骤6 cdn加速链接是 https://cdn.jsdelivr.net/gh\n https://cdn.jsdelivr.net/gh/仓库名\n 配置生成的链接格式,上传成功后可以到相册中去复制对应图片的链接\n 另外还可 在PciGo设置中去 设置上传快捷键等配置\n 参考文章\n","description":"","id":25,"section":"posts","tags":null,"title":"搭建免费的图床工具","uri":"https://yichenlove.github.io/posts/imagehostingservice/"},{"content":"目录\n.NET 应用程序是怎么运行的? 跨平台需求 CIL,公共中间语言(Common Intermediate Language) CLR,公共语言运行时(Common Language Runtime) C# 的执行过程 .Net Framework 和 Mono Mono Mono 的组成 Mono 的执行流程 Mono 的优点 Mono 的缺点 IL2CPP IL2CPP 的组成 IL2CPP 的执行流程 IL2CPP 的优点 IL2CPP 的缺点 可能产生的疑问 参考文章\n .NET 应用程序是怎么运行的? 开篇先自问个问题,.NET 应用程序是怎么运行的?,在没有去探索这些知识点的时候,自己是一片空白的,因为自己完全是为了使用 Unity3D 而去学习的 C#,却没有去了解这个语言的执行过程 (又不是不能用)。\n 百度启动,我们发现.NET 平台的实现有两大派系:曾今闭源的.Net Framework和开源的 Mono,那么,我们首先得知道,执行.NET 程序为何需要它们。\n 跨平台需求 跨平台是个好东西,我们写了一个程序,不需要多大精力就可以将它发布在多个平台上,那么我们如何实现这个过程呢?绞尽脑汁,想了想,把源代码编译成不同平台对应的机器码?\n 咦,感觉八九不离十了,因为我们想到 c/c++,每个平台都有对应的 gcc,在 linux 上写的代码,大部分都可以在各个 linux 派生系统上直接编译运行,但是应该能马上意识到,linux 和 winodws 有些 api 并不通用啊。\n 聪明的我们想到,宏定义判断平台,编码的时候写多个方式,通过 makefile 来自动选择编译的参数。\n 是的,我们开发是能这么做,我们也能体会到这样开发带来的成本很高,并且我们的角度只是以开发者的角度去思考这个问题,那么作为一个面向开发者的公司要如何思考呢。毕竟开发成本降低了,才能迎来更多的开发者,因此我们不应该让开发者去做这些繁重的事,我们需要实现一个东西,让开发者只需要按照某个规定/协议/规则编码,我们的东西识别这些符合定义的编码,将他们自动转换成各平台的机器码不就行了么。\n 因此,微软开发了一个称为通用语言架构(Shared Source Common Language Infrastructure,Shared Source CLI);也就是上面提到的,让开发者遵守的一种技术规范,它定义了一个语言无关的跨体系结构的运行环境,这使得开发者可以用规范内定义的各种高级语言来开发软件,并且无需修正即可将软件运行在不同的计算机体系结构上。\n CIL,公共中间语言(Common Intermediate Language) 我们遵守 CLI 规则,编译器会将我们编写的代码变成中间语言 IL,我们还需要一个东西,专门负责翻译开发者的代码,变成对应的机器码。注意别把 CLI 和 CIL 弄混。\n CLR,公共语言运行时(Common Language Runtime) 无论通过任何语言构建产品,都必须寄宿到一个平台中运行,这正如我们的软件运行在操作系统环境一样,操作系统为 CLR 提供了运行环境,使用.NET 构建的程序又运行在 CLR 之上,CRL 为.NET 程序的运行提供了温床,CLR 提供基本的类库和运行引擎,基本类库封装操作系统函数供开发者方便调用,运行引擎用于编译并运行我们开发的程序。CLR 包含.NET 运行引擎和符合 CLI 的类库。通过.NET 平台构建的程序都基于 CLR 基础类库来实现,并且运行在 CLR 提供的运行引擎之上。\n 显然,这个 CLR 就是上文所需要的 东西 (是不是和 java 虚拟机差不多),能把基于 CLI 规范的语言编写出来的 IL 代码翻译为机器代码运行,这是 CLR 最重要的功能。\n 那么 CLR 是如何对 IL 语言进行翻译的呢?CLR 提供了 JIT,即 Just-in-time,动态 (即时) 编译,边运行边编译;AOT,Ahead Of Time,指运行前编译,两种程序的编译方式。\n JIT 具体的做法是这样的:当载入一个类型时,CLR 为该类型创建一个内部数据结构和相应的函数,当函数第一次被调用时,JIT 将该函数编译成机器语言.当再次遇到该函数时则直接从 cache 中执行已编译好的机器语言。而 AOT 则是提前编译好所有代码。\n C# 的执行过程 经过上面的探讨,相信运行过程开始清晰起来了。C# 是遵循 CLI 规范的高级语言,被先被各自的编译器编译成中间语言:IL(CIL),等到需要真正执行的时候,这些 IL 会被加载到运行时库 CLR 中,由 CLR 动态的编译成汇编代码(JIT)然后在执行。\n 可能加上才刚认识的英文专有词缩写,这么表述还是有一点绕,那么通俗来解释,我们写完 C# 代码,编译器开始编译操作,编译器将它变成了一种中间语言,运行的时候,操作系统会调用一个解释器对这些中间语言进行动态解释或者提前编译运行。\n .Net Framework 和 Mono 上面那一块主要功能就包含在叫做.Net Framework的框架中,那么既然有了.Net Framework,为何还需要Mono呢?下面引用别人的博客内容,阐述了Mono出现的原因。\n 我想表达什么呢?其实我们现在在 Windows 平台下开发的 .NET 应用程序,是深深依赖于 .NET Framework(深深的那种),你的应用程序代码基本上都是在它之上完成的,而 .NET Framework 又是深深依赖于 Windows 平台下的 CLR(也是深深的那种),在这种情况下,根本就无法使你的应用程序跨平台,因为微软紧紧的抱住 Windows 平台,妄想 Windows 可以实现 大一统,但现实是很残酷的,这次的 .NET 开源、跨平台,其实也是微软的无奈之举。但就是在这种背景下,Mono 出现了,并且在微软的各种 排挤 下坚持了下来,这是非常不容易的,其实实现 .NET 跨平台的三个关键是:编译器、CLR 和基础类库,而 Mono 实质上就是把他们三个进行跨平台实现了,一个很小团队完成了一个巨头需要完成的工作,而且还是在这个巨头的 排挤 下,其实这就是开源和社区的力量。\n 是的,.Net Framework没有真正意义上实现跨平台,它只能在不同 windows 版本上工作,这是Mono出现的原因。我们来看看百度百科的介绍。\n Mono是一个由 Xamarin 公司(先前是 Novell,最早为 Ximian)所主持的自由开放源代码项目。该项目的目标是创建一系列符合 ECMA 标准(Ecma- 334 和 Ecma-335)的.NET 工具,包括 C# 编译器和通用语言架构。与微软的.NET Framework(共通语言运行平台)不同,Mono 项目不仅可以运行于 Windows 系统上,还可以运行于 Linux,FreeBSD,Unix,OS X 和 Solaris,甚至一些游戏平台,例如:Playstation 3,Wii 或 XBox 360 之上。\n Mono 使得 C# 这门语言有了很好的跨平台能力。相对于微软的.Net Framework 运行时库Mono使用自己的 Mono VM 作为运行时库。 加上 C# 本身快速友好的开发能力,最终使得 Unity 团队在创建之初就决定将Mono,C# 作为其核心。\n 而Mono的执行方式和.Net Framework大致上差不多,只不过使用的改编后的 CLR,使得在各平台上可以变成对应机器码。\n Mono 现在我们抛开.Net Framework,因为 Unity 目前支持Mono和IL2CPP,IL2CPP是后来加上的 (Unity2017.3 版本以后),先来后到,我们先谈谈Mono。\n Mono 的组成 C# 编译器\n最新的 Momo 版本(5.0+)c# 编译器完全兼容 c#4.0 以上,unity 2018 使用的依旧是 Mono 2.0 版本,它的编译器 (mcs) 就不支持 c#4.0 以上。\n Mono 运行时 CLR\n上面提到过,提供了 JIT(即时编译器),AOT(提前编译器)两种编译器。\n 类库加载器。\n垃圾回收器 (Unity 使用的是贝姆垃圾回收器)。\n 基础类库(与.net 框架兼容)+Mono 类库\n Mono 的执行流程 Mono 的优点 构建应用非常快 由于 Mono 的 JIT(Just In Time compilation ) 机制, 所以支持更多托管类库 支持运行时代码执行 (译者注: 由于 JIT 机制,所以能在运行的过程中执行新生成的或者动态加载的代码) Mono 的缺点 Mono VM 在各个平台移植,维护非常耗时,有时甚至不可能完成。\n Mono 的跨平台是通过 Mono VM 实现的,有几个平台,就要实现几个 VM,像 Unity 这样支持多平台的引擎,Mono 官方的 VM 肯定是不能满足需求的。所以针对不同的新平 台,Unity 的项目组就要把 VM 给移植一遍,同时解决 VM 里面发现的 bug。这非常耗时耗力。这些能移植的平台还好说,还有比如 WebGL 这样基于浏览器的平台。要让 WebGL 支持 Mono 的 VM 几乎是不可能的。 必须将代码发布成托管程序集 (.dll 文件 , 由 mono 或者.net 生成 ) IL2CPP 从名字上看就很清楚了,IL to cpp 即 IL 翻译成 cpp。根据 Unity 官方博客上的文章指出,使用.NET 和 Mono 编译器对代码进行编译。\n 我们可以在 Windows 平台的 Unity 安装路径 EditorDatail2cpp 目录下找到。对于 OSX 平台,它位于 Unity 安装路径的 Contents/Frameworks/il2cpp/build 目录内。 il2cpp.exe 这个工具是一个托管代码可执行文件,其完全由 C# 写成。在开发 IL2CPP 的过程中,我们同时使用.NET 和 Mono 编译器对其进行编译。\n 而对于 GC,官方是这么解释的:\n 运行时的另外一个重要的部分,就是垃圾收集器。在 Unity 5 中,我们使用 libgc 垃圾收集器。它是一个典型的贝姆垃圾收集器(Boehm-Demers-Weiser garbage collector)。(译注:相对使用保守垃圾回收策略)。然而我们的 libil2cpp 被设计成可以方便使用其他垃圾回收器。因此我们现在也在研究集成微软开源的垃圾回收器(Microsoft GC)。对于垃圾回收器这一点,我们会在后续的一篇中专门的讨论,这里就不多说了。\n 以上翻译内容来自用 Unity 做游戏,你需要深入了解一下 IL2CPP\n IL2CPP 的组成 AOT 编译器\n il2cpp 接受来自 Unity 自带的或者由 Mono 编译器产生的托管程序集,将这些程序集转换成 C++ 代码。这些转换出的 C++ 代码最终由部署目标平台上的 C++ 编译器进行编译。\n 一个支持虚拟机的运行时库\n AOT 编译器将由.NET 输出的中间语言 (IL) 代码生成为 C++ 代码。运行时库则提供诸如垃圾回收,与平台无关的线程,IO 以及内部调用(C++ 原生代码直接访问托管代码结构)这样的服务和抽象层。\n IL2CPP 的执行流程 IL2CPP 的优点 相比 Mono, 代码生成有很大的提高 可以调试生成的 C ++ 代码 可以启用引擎代码剥离 (Engine code stripping) 来减少代码的大小 IL2CPP 的缺点 相比 Mono 构建应用非常慢 只支持 AOT(Ahead of Time) 编译 可能产生的疑问 IL2CPP 多了一次编译过程啊,从效率上来说为什么值得的使用呢?\n IL2CPP 多了一次的编译过程是将 IL 转成 CPP,执行的时候还需要 C++ 编译器编译一次,而在 Mono 中没有,所以在 IL2CPP 的缺点中,构建应用的时间很长,但是,Mono 的 JIT 只在运行的时候将 IL 执行成机器码,意味着每一次运行都需要动态加载一些代码,而 IL2CPP 已经预先编译好了,可以利用现成的在各个平台的 C++ 编译器对代码执行编译期优化,这样可以进一步减小最终游戏的尺寸并提高游戏运行速度。\n 并且 Cpp 效率之快是大家都承认的。\n CPP 是静态的,那么还需要 IL2CPP 虚拟机干吗?\n 虽然通过 IL2CPP 以后代码变成了静态的 C++,但是内存管理这块还是遵循 C# 的方式,这也是为什么最后还要有一个 IL2CPP VM 的原因:它负责提供诸如 GC 管理,线程创建这类的服务性工作。但是由于去除了 IL 加载和动态解析的工作,使得 IL2CPP VM 可以做的很小,并且使得游戏载入时间缩短。\n C# 是一种高级语言,需要编译转换成中间语言,通过不同平台的 CLR 解释成机器码,才能运行。\n 现在在 Unity3D 中,我们可以选择 Mono 和 IL2CPP 两种编译方式,各有优缺点,而一般都在开发阶段选择 Mono,在发布的时候选择 IL2CPP。\n 不过呢,据说现在 IL2CPP 的成熟程度挺高,也有人推荐开发阶段使用。\n ","description":"","id":26,"section":"posts","tags":["unity"],"title":"简单了解Mono和IL2CPP","uri":"https://yichenlove.github.io/posts/monoandil2cpp/"},{"content":" 性能分析\n 优化工作的第一个步骤便是通过性能分析来收集性能数据,这也是移动端优化的第一步。\n 我们要尽早在目标设备上进行性能分析,而且要经常分析。 Unity Profiler可提供应用关键的性能信息,因此是优化必不可少的一部分。尽早对项目进行性能分析,不要拖到发售前。对每一个故障或性能尖峰彻查到底。对你自己的项目性能有一个清晰的认知,可帮助你更轻松地发现新问题。\n Unity编辑器内的性能分析可以揭示出游戏不同系统的相对性能,而在运行设备上进行分析可让你获取更为准确的性能洞察。经常性地在目标设备上分析开发版。同时为最高配置与最低配置的设备进行性能分析和优化。\n 除了Unity Profiler,你还可以使用iOS与Android的原生工具来进一步测试引擎在平台上的表现。\n 比如iOS的Xcode和Instruments, 以及Android上的Android Studio和Android Profiler。 部分硬件更是带有额外的分析工具(例如Arm Mobile Studio、Intel VTune,以及Snapdragon Profiler)。详情请见Profiling Applications Made with Unity教程。\n 针对性优化 如果游戏出现性能问题,切忌自行猜测或揣测成因,一定要使用Unity Profiler和平台专属工具来准确找出卡顿的问题来源。\n 不过,这里所说的优化并不都适用于你的应用。在某个项目中适用的方法不一定适用于你的项目。找出真正的性能瓶颈,将精力集中在有实际效用的地方。\n 了解Unity Profiler工作原理 Unity Profiler可帮助你在运行时检测出卡顿或死机的原因,更好地了解特定帧或时间点上发生了什么。工具默认启用CPU和内存监测轨,你也可以根据需要启用额外的分析模块,包括渲染器、音频和物理(如极度依赖物理模拟的游戏或音游)。\n 或使用Unity Profiler来测试应用程序的性能和资源分配。\n展开\n 勾选Development Build便能为目标设备构建应用,勾选Autoconnect Profiler或者手动关联分析器,来加快其启动时间。\n 展开\n 选中需要分析的目标平台。按下Record(录制)按钮可记录应用在几秒钟内的运行(默认为300帧)。打开Unity \u0026gt; Preferences \u0026gt; Analysis \u0026gt; Profiler \u0026gt; Frame Count界面可修改录制帧数,最长录制帧数可以增加到2000帧。当然更长的录制帧数会让Unity编辑器占用更多的CPU资源和内存,但其在特定情形下的作用非常大。\n 该分析器采用标记框架,可分析以ProfileMarkers(如MonoBehaviour的Start或Update方法,或特定API调用)划分出的代码运行时。在使用Deep Profiling时,Unity可以分析出每次函数调用的开始与结尾,准确地呈现出导致应用性能放缓的代码部分。\n 你可以借助Timeline视图来明确应用最为依赖的是CPU还是GPU。\n展开\n 在分析游戏时,我们建议同时分析性能高峰与帧平均成本。在分析帧率过低的应用时,较为有效的方法是分析并优化每一帧中运行成本较高的代码。在尖峰处首先分析繁重的运算(如物理、AI、动画)和垃圾数据收集。\n 点击窗口中的某帧,接着使用Timeline或Hierarchy视图进行分析:\n Timeline可显示特定帧耗时的可视化图表,帮助你直观地看到各项活动以及不同线程之间的关系。你可使用该选项来了解项目主要依赖的是CPU还是GPU。 Hierarchy将显示分组的ProfileMarkers层级,并以毫秒(Time ms'总耗时'和Self ms‘自执行耗时’)为单位对样本进行排序。你还可以数出帧上函数的Calls调用以及内存清理(GC Alloc)的次数。 Hierarchy视图允许按照耗时长短对ProfileMarkers进行排序。\n展开\n 完整的Unity Profiler概述可在此处了解。初来乍到的用户也可以观看这段Introduction to Unity Profiling教学。\n 注意,在优化任意项目之前,一定要保存Profiler的.data 文件,这样你就能在修改后比较优化_前__后_的不同了。剖析、优化和比较,清空再重复,如此循环往复来提高性能。\n Profiler Analyzer 该工具可以汇总多帧Profiler数据,由用户来挑选出那些问题较大的帧。如果你想了解项目更改后Profiler的相应改变,可使用Compare视图分别加载和比较两个数据集,从而完成测试与优化。Profile Analyzer可在Unity Package Manager中下载。\n Profiler Analyzer可以很好地补充Profiler,可以进一步深入分析帧与标记数据。\n展开\n为每帧设定一个时间预算 你可以设立一个目标帧率,为每帧划定一个时间预算。理想情况下,一个以30 fps运行的应用每帧应占有约33.33毫秒(1000毫秒/30帧)。同样地,60 fps每帧约为16.66毫秒。\n 设备可以在短时间内超过预算(如过场动画或加载过程中),但绝不能长时间如此。\n 设备温度优化 对于移动设备而言,长时间占用最大时间预算可能会导致设备过热,操作系统可能会启动CPU与GPU降频保护。我们建议每帧仅占用约65%的时间预算,保留一定的散热时间。常见的帧预算为:30 fps为每帧22毫秒,60 fps为每帧11毫秒。\n 大多数移动设备不像桌面设备那样有主动散热功能,因此环境温度可以直接影响性能。\n 如果设备发热严重,Profiler可能会察觉并汇报这块性能低下的部分,即使其只是暂时性问题。为了应对分析时设备过热,分析应分成小段进行。这样便能允许设备散热、模拟出真实的运行条件。我们的建议是,在进行性能分析前后,预留10-15分钟用于设备散热。\n 分清GPU与CPU依赖程度 Profiler可在CPU耗时或GPU耗时超出帧预算发出警告,它将弹出下方以Gfx为前缀的标记:\n Gfx.WaitForCommands标记表示渲染线程正在等待主线程完成,后者可能出现了性能瓶颈。 而Gfx.WaitForPresent表示主线程正在等待GPU递交渲染帧。 内存分析\n Unity会采取自动化内存管理来处理由用户生成的代码与脚本。值类型本地变量等小型数据会被分配到内存堆栈中,大型数据和持久性存储数据则会被分配到托管内存中。\n 垃圾数据收集器会定期识别并删除未被使用的托管内存,这个自动流程在检查堆的对象时可能导致游戏卡顿或运行放缓。\n 这里,优化内存便是指关注托管内存的分配与删除时机,将内存垃圾回收的影响降到最低。详情 请在Understanding the managed heap中了解。\n Memory Profiler中的帧数据记录、检视与比较。\n展开\nMemory Profiler Memory Profiler属于一个独立的分析模块,可以截取托管数据堆内存的状态,帮助你识别出数据碎片化和内存泄漏等问题。\n 在Tree Map视图中点击一个变量便可跟踪其在内存原生对象上的状态。你可在此处找出由纹理过大或资源重复加载而导致的常见内存消耗问题。\n 请在这里了解如何使用Unity的Memory Profiler优化内存占用。你也可以查看官方Memory Profiler文档。\n 降低内存垃圾回收(GC)对性能的影响 Unity使用的是Boehm-Demers-Weiser垃圾回收器 ,它会中止主线程代码运行,在垃圾回收工作完成后再让其恢复运行。\n 请注意,部分多余的托管内存分配会造成GC耗能高峰:\n Strings(字符串):在C#中,字符串属于引用类型,而非值类型。我们需要减少不必要的字符串创建或更改操作,尽量避免解析JSON和XML等由字符串组成的数据文件,将数据存储于ScriptableObjects,或以MessagePack或Protobuf等格式保存。如果你需要在运行时构建字符串,可使用StringBuilder类。 Unity函数调用:部分函数会涉及托管内存分配。我们需要缓存数组引用,避免在循环进行中进行数组的内存分配,且尽量使用那些不会产生垃圾回收的函数。比如使用GameObject.CompareTag,而不是使用GameObject.tag 手动比对字符串(因为返回一个新字符串会产生垃圾数据)。 Boxing(打包):避免在引用类型变量处传入值类型变量,因为这样做会导致系统创建一个临时对象,在背地里将值类型转换为对象类型(如int i = 123; object o = i ),从而产生垃圾回收的需求。尽量使用正确的类型覆写来传入想要的值类型。泛型也可用于类型覆写。 Coroutines(协同程序):虽然yield不会产生垃圾回收,但新建WaitForSeconds对象会。我们可以缓存并复用WaitForSeconds对象,不必在yield中再度创建。 LINQ与Regular Expressions(正则表达式):这两种方法都会在后台的数据打包期间产生垃圾回收。如果需要追求性能,请尽量避免使用LINQ和正则表达式,转而使用for循环和列表来创建数组。 定时处理垃圾回收 如果你确定垃圾回收带来的卡顿不会影响游戏特定阶段的体验,你可以使用System.GC.Collect来启动垃圾数据收集。\n 请在Understanding Automatic Memory Management(自动化内存管理)中了解怎样妥善地使用这项功能。\n 使用增量式垃圾回收(Incremental GC)分散垃圾回收 增量式垃圾回收不会在程序运行期间长时间地中断运行,而会将总负荷分散到多帧,形成零碎的收集流程。如果垃圾数据收集对性能产生了较大的影响,可以尝试启用这个选项来降低GC的处理高峰。你可以使用Profile Analyzer来检验此功能的实际作用。\n 使用增量垃圾回收来降低GC处理高峰。\n展开\n 编程和代码架构\n Unity的PlayerLoop包含许多可与引擎核心互动的函数。该结构包含一些负责初始化和每帧更新的系统,所有脚本都将依靠PlayerLoop来生成游戏体验。\n 在分析时,你会在PlayerLoop下看到用户使用的代码(Editor代码则位于EditorLoop下)。\n Profiler将显示在整个引擎运行过程中的自定义脚本、设置和图形。\n展开\n展开\n 请在这里_了解PlayerLoop和__脚本生命周期_ 。\n 你可以使用以下技巧和窍门来优化脚本。\n 深入理解Unity PlayerLoop 我们需要掌握Unity帧循环的执行顺序 。每个Unity脚本都会按照预定的顺序运行事件函数,这要求我们了解Awake、Start、Update以及其他运行周期相关函数之间的区别。\n 请在Script Lifecycle Flowchart(脚本生命周期流程图)中了解函数的执行顺序。\n 降低每帧的代码量 有许多代码并非要在每帧上运行,这些不必要的逻辑完全可以在Update、LateUpdate和FixedUpdate中删去。这些事件函数可以保存那些必须每帧更新的代码,任何无须每帧更新的逻辑都不必放入其中,只有在相关事物发生变化时,这些逻辑才需被执行。\n 如果_必须_要使用Update,可以考虑让代码每隔_n_帧运行一次。这种划分运行时间的方法也是一种将繁重工作负荷化整为零的常见技术。在下方例子中,ExampleExpensiveFunction将每隔三帧运行一次。\n 避免在Start/Awake中加入繁重的逻辑 当首个场景加载时,每个对象都会调用如下函数:\n Awake OnEnable Start 在应用完成第一帧的渲染前,我们须避免在这些函数中运行繁重的逻辑。否则,应用的加载时间会出乎意料地长。\n 请在Order of execution for event functions(事件函数的执行顺序)中详细了解首个场景的加载。\n 避免加入空事件 即使是空的MonoBehaviours也会占用资源,因此我们应该删除空的Update及LateUpdate方法。\n 如果你想用这些方法进行测试,请使用预处理指令(preprocessor directives):\n 如此一来,在编辑器中的Update测试便不会对构建版本造成不良的性能影响。\n 删去Debug Log语句 Log声明(尤其是在Update、LateUpdate及FixedUpdate中)会拖慢性能,因此我们需要在构建之前禁用Log语句。\n 你可以用预处理指令编写一条Conditional属性来轻松禁用Debug Log。比如下方这种的自定义类:\n 添加自定义预处理指令可以实现脚本的切分。\n展开\n 用自定义类生成Log信息时,你只需在Player Settings中禁用ENABLE_LOG 预处理指令,所有的Log语句便会一下子消失。\n 使用哈希值、避免字符串 Unity底层代码不会使用字符串来访问Animator、Material和Shader属性。出于提高效率的考虑,所有属性名称都会被哈希转换成属性ID,用作实际的属性名称。\n 在Animator、Material或Shader上使用Set或Get方法时,我们便可以利用整数值而非字符串。后者还需经过一次哈希处理,并没有整数值那么直接。\n 使用Animator.StringToHash来转换Animator属性名称,用Shader.PropertyToID来转换Material和Shader属性名称。\n 选择正确的数据结构 由于数据结构每帧可能会迭代上千次,因此其结构对性能有着较大的影响。如果你不清楚数据集合该用List、Array还是Dictionary表示,可以参考C#的MSDN数据结构指南来选择正确的结构。\n 避免在运行时添加组件 在运行时调用AddComponent会占用一定的运行成本,Unity必须检查组件是否有重复或依赖项。\n 当组件已经配置完成,Instantiating a Prefab(实例化预制件)一般来说性能更强。\n 缓存GameObjects和组件 调用GameObject.Find、GameObject.GetComponent和Camera.main(2020.2以下的版本)会产生较大的运行负担,因此这些方法不适合在Update中调用,而应在Start中调用并缓存。\n 下方例子展示了一种低效率的GetComponent多次调用:\n 其实GetComponent的结果会被缓存,因此只需调用一次即可。缓存的结果完全可在Update中重复使用,不必再度调用GetComponent。\n 对象池(Object Pool) Instantiate(实例化)和Destroy(销毁)方法会产生需要垃圾回收数据、引发垃圾回收(GC)的处理高峰,且其运行较为缓慢。与其经常性地实例化和销毁GameObjects(如射出的子弹),不如使用对象池将对象预先储存,再重复地使用和回收。\n 在这个例子中,ObjectPool创建了20个PlayerLaser实例供重复使用。\n展开\n 在游戏特定时间点(如显示菜单画面时)创建可复用的实例,来降低CPU处理高峰的影响,再用一个集合来形成对象池。在游戏期间,实例可在需要时启用/禁用,用完后可返回到池中,不必再进行销毁。\n PlayerLaser对象池目前尚未激活,正等待玩家射击。\n展开\n 这一来你就可以减少托管内存分配的次数、防止产生垃圾回收的问题。\n 请在此处了解如何在Unity中创建一个简单的对象池系统。\n 使用ScriptableObjects(可编程对象) 固定不变的值或配置信息可以存储在ScriptableObject中,不一定得储存于MonoBehaviour。ScriptableObject可由整个项目访问,一次设置便可应用于项目全局,但它并不能直接关联到GameObject上。\n 我们可在ScriptableObject中用字段来存储值或设定,然后在MonoBehaviours中引用该对象。\n 用作“Inventory(物品栏)”的ScriptableObject可保存多个游戏对象的设定。\n展开\n 下方的ScriptableObject字段可有效防止多次MonoBehaviour实例化产生的数据重复。\n 请在Introduction to ScriptableObjects教程中了解如何使用ScriptableObjects。你也可以参考此处的文档。\n ","description":"","id":27,"section":"posts","tags":["unity"],"title":"Unity 内存优化","uri":"https://yichenlove.github.io/posts/unity-cpu/"},{"content":"1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 #!/bin/sh # 使用方法 # 1.将autoarchive.sh和附件中的plist,放在一起,新建文件夹为Shell,将这几文件复制进去,然后复制Shell文件夹到工程的根目录 # 2.终端cd到Shell下,执行脚本 格式为 sh 脚本名字.sh # 配置信息 #工程名字 target_name=\u0026#34;xxx\u0026#34; echo \u0026#34;\\033[32m****************\\n开始自动打包\\n****************\\033[0m\\n\u0026#34; # ==========自动打包配置信息部分========== # #工程中Target对应的配置plist文件名称, Xcode默认的配置文件为Info.plist info_plist_name=\u0026#34;Info\u0026#34; #返回上一级目录,进入项目工程目录 cd .. #获取项目名称 project_name=`find . -name *.xcodeproj | awk -F \u0026#34;[/.]\u0026#34; \u0026#39;{print $(NF-1)}\u0026#39;` #获取工程plist配置文件 info_plist_path=\u0026#34;$project_name/$info_plist_name.plist\u0026#34; #获取版本号 bundle_version=`/usr/libexec/PlistBuddy -c \u0026#34;Print CFBundleShortVersionString\u0026#34; $info_plist_path` #设置build版本号(可以不进行设置) date=`date +\u0026#34;%Y%m%d%H%M\u0026#34;` /usr/libexec/PlistBuddy -c \u0026#34;Set :CFBundleVersion $date\u0026#34; \u0026#34;$info_plist_path\u0026#34; #获取build版本号 bundle_build_version=`/usr/libexec/PlistBuddy -c \u0026#34;Print CFBundleVersion\u0026#34; $info_plist_path` #强制删除旧的文件夹 rm -rf ./$target_name-IPA #指定输出ipa路径 export_path=./$target_name-IPA #指定输出归档文件地址 export_archive_path=\u0026#34;$export_path/$target_name.xcarchive\u0026#34; #指定输出ipa地址 export_ipa_path=\u0026#34;$export_path\u0026#34; #指定输出ipa名称 : target_name + bundle_version + bundle_build_version ipa_name=\u0026#34;$target_name-V$bundle_version($bundle_build_version)\u0026#34; echo \u0026#34;\\033[32m****************\\n自动打包选择配置部分\\n****************\\033[0m\\n\u0026#34; # ==========自动打包可选择信息部分========== # # 输入是否为工作空间 archiveRun () { #是否是工作空间 echo \u0026#34;\\033[36;1m是否是工作空间(输入序号, 按回车即可) \\033[0m\u0026#34; echo \u0026#34;\\033[33;1m1. 是 \\033[0m\u0026#34; echo \u0026#34;\\033[33;1m2. 否 \\033[0m\u0026#34; #读取用户输入 read is_workspace_parame sleep 0.5 if [ \u0026#34;$is_workspace_parame\u0026#34; == \u0026#34;1\u0026#34; ] then echo \u0026#34;\\033[32m****************\\n您选择了是工作空间 将采用:xcworkspace\\n****************\\033[0m\\n\u0026#34; elif [ \u0026#34;$is_workspace_parame\u0026#34; == \u0026#34;2\u0026#34; ] then echo \u0026#34;\\033[32m****************\\n您选择了不是工作空间 将采用:xcodeproj\\n****************\\033[0m\\n\u0026#34; else echo \u0026#34;\\n\\033[31;1m**************** 您输入的参数,无效请重新输入!!! ****************\\033[0m\\n\u0026#34; archiveRun fi } archiveRun # 输入打包模式 configurationRun () { echo \u0026#34;\\033[36;1m请选择打包模式(输入序号, 按回车即可) \\033[0m\u0026#34; echo \u0026#34;\\033[33;1m1. Release \\033[0m\u0026#34; echo \u0026#34;\\033[33;1m2. Debug \\033[0m\u0026#34; #读取用户输入 read build_configuration_param sleep 0.5 if [ \u0026#34;$build_configuration_param\u0026#34; == \u0026#34;1\u0026#34; ]; then build_configuration=\u0026#34;Release\u0026#34; elif [ \u0026#34;$build_configuration_param\u0026#34; == \u0026#34;2\u0026#34; ]; then build_configuration=\u0026#34;Debug\u0026#34; else echo \u0026#34;\\n\\033[31;1m**************** 您输入的参数,无效请重新输入!!! ****************\\033[0m\\n\u0026#34; configurationRun fi } configurationRun echo \u0026#34;\\033[32m****************\\n您选择了 $build_configuration模式\\n****************\\033[0m\\n\u0026#34; # 输入打包类型 methodRun () { # 输入打包类型 echo \u0026#34;\\033[36;1m请选择打包方式(输入序号, 按回车即可) \\033[0m\u0026#34; echo \u0026#34;\\033[33;1m1. AdHoc \\033[0m\u0026#34; echo \u0026#34;\\033[33;1m2. AppStore \\033[0m\u0026#34; echo \u0026#34;\\033[33;1m3. Enterprise \\033[0m\u0026#34; echo \u0026#34;\\033[33;1m4. Development \\033[0m\\n\u0026#34; #读取用户输入 read method_param sleep 0.5 if [ \u0026#34;$method_param\u0026#34; == \u0026#34;1\u0026#34; ]; then exportOptionsPlistPath=\u0026#34;AdHocExportOptions\u0026#34; echo \u0026#34;\\033[32m****************\\n您选择了 AdHoc 打包类型\\n****************\\033[0m\\n\u0026#34; elif [ \u0026#34;$method_param\u0026#34; == \u0026#34;2\u0026#34; ]; then exportOptionsPlistPath=\u0026#34;AppStoreExportOptions\u0026#34; echo \u0026#34;\\033[32m****************\\n您选择了 AppStore 打包类型\\n****************\\033[0m\\n\u0026#34; elif [ \u0026#34;$method_param\u0026#34; == \u0026#34;3\u0026#34; ]; then exportOptionsPlistPath=\u0026#34;EnterpriseExportOptions\u0026#34; echo \u0026#34;\\033[32m****************\\n您选择了 Enterprise 打包类型\\n****************\\033[0m\\n\u0026#34; elif [ \u0026#34;$method_param\u0026#34; == \u0026#34;4\u0026#34; ]; then exportOptionsPlistPath=\u0026#34;DevelopmentExportOptions\u0026#34; echo \u0026#34;\\033[32m****************\\n您选择了 Development 打包类型\\n****************\\033[0m\\n\u0026#34; else echo \u0026#34;\\n\\033[31;1m**************** 您输入的参数,无效请重新输入!!! ****************\\033[0m\\n\u0026#34; methodRun fi } methodRun # 输入上传类型 publishRun () { # 输入打包类型 echo \u0026#34;\\033[36;1m请选择上传类型(输入序号, 按回车即可) \\033[0m\u0026#34; echo \u0026#34;\\033[33;1m1. 不上传 \\033[0m\u0026#34; echo \u0026#34;\\033[33;1m2. AppStore \\033[0m\u0026#34; #读取用户输入 read publish_param sleep 0.5 if [ \u0026#34;$publish_param\u0026#34; == \u0026#34;1\u0026#34; ]; then echo \u0026#34;\\033[32m****************\\n您选择了不上传\\n****************\\033[0m\\n\u0026#34; elif [ \u0026#34;$publish_param\u0026#34; == \u0026#34;2\u0026#34; ]; then echo \u0026#34;\\033[32m****************\\n您选择了上传 AppStore\\n****************\\033[0m\\n\u0026#34; else echo \u0026#34;\\n\\033[31;1m**************** 您输入的参数,无效请重新输入!!! ****************\\033[0m\\n\u0026#34; publishRun fi } publishRun #选择了2、Release、AppStore if [ \u0026#34;$method_param\u0026#34; == \u0026#34;2\u0026#34; -a \u0026#34;$build_configuration\u0026#34; == \u0026#34;Release\u0026#34; -a \u0026#34;$publish_param\u0026#34; == \u0026#34;2\u0026#34; ] then #上传App Store echo \u0026#34;请输入开发者账号:\u0026#34; read username_param sleep 0.5 echo \u0026#34;请输入开发者账号密码:\u0026#34; read password_param sleep 0.5 fi echo \u0026#34;\\033[32m****************\\n打包信息配置完毕,输入回车开始进行打包\\n****************\\033[0m\\n\u0026#34; read start sleep 0.5 echo \u0026#34;\\033[32m****************\\n开始清理工程\\n****************\\033[0m\\n\u0026#34; # 删除旧的文件 rm -rf \u0026#34;$export_path\u0026#34; # 指定输出文件目录不存在则创建 if test -d \u0026#34;$export_path\u0026#34; ; then echo $export_path else mkdir -pv $export_path fi # 清理工程 xcodebuild clean -configuration \u0026#34;$build_configuration\u0026#34; -alltargets echo \u0026#34;\\033[32m****************\\n开始编译项目 ${build_configuration}${exportOptionsPlistPath}\\n****************\\033[0m\\n\u0026#34; # 开始编译 if [ \u0026#34;$is_workspace_parame\u0026#34; == \u0026#34;1\u0026#34; ] then #工作空间 xcodebuild archive \\ -workspace ${project_name}.xcworkspace \\ -scheme ${target_name} \\ -configuration ${build_configuration} \\ -destination generic/platform=ios \\ -archivePath ${export_archive_path} else #不是工作空间 xcodebuild archive \\ -project ${project_name}.xcodeproj \\ -scheme ${target_name} \\ -configuration ${build_configuration} \\ -archivePath ${export_archive_path} fi # 检查是否构建成功 # xcarchive 实际是一个文件夹不是一个文件所以使用 -d 判断 if test -d \u0026#34;$export_archive_path\u0026#34; ; then echo \u0026#34;\\033[32m****************\\n项目编译成功\\n****************\\033[0m\\n\u0026#34; else echo \u0026#34;\\033[32m****************\\n项目编译失败\\n****************\\033[0m\\n\u0026#34; exit 1 fi echo \u0026#34;\\033[32m****************\\n开始导出ipa文件\\n****************\\033[0m\\n\u0026#34; #1、打包命令 #2、归档文件地址 #3、ipa输出地址 #4、ipa打包设置文件地址 xcodebuild -exportArchive \\ -archivePath ${export_archive_path} \\ -configuration ${build_configuration} \\ -exportPath ${export_ipa_path} \\ -exportOptionsPlist \u0026#34;./Shell/${exportOptionsPlistPath}.plist\u0026#34; # 修改ipa文件名称 mv $export_ipa_path/$target_name.ipa $export_ipa_path/$ipa_name.ipa # 检查文件是否存在 if test -f \u0026#34;$export_ipa_path/$ipa_name.ipa\u0026#34; ; then echo \u0026#34;\\033[32m****************\\n导出 $ipa_name.ipa 包成功\\n****************\\033[0m\\n\u0026#34; else echo \u0026#34;\\033[32m****************\\n导出 $ipa_name.ipa 包失败\\n****************\\033[0m\\n\u0026#34; exit 1 fi # 打开打包文件目录 open $export_path # 输出 echo \u0026#34;\\033[32m****************\\n使用Shell脚本打包完毕\\n****************\\033[0m\\n\u0026#34; #上传 AppStore if [ -n \u0026#34;$username_param\u0026#34; -a -n \u0026#34;$password_param\u0026#34; -a \u0026#34;$method_param\u0026#34; == \u0026#34;2\u0026#34; -a \u0026#34;$build_configuration\u0026#34; == \u0026#34;Release\u0026#34; -a \u0026#34;$publish_param\u0026#34; == \u0026#34;2\u0026#34; ] then echo \u0026#34;\\033[32m****************\\n开始上传AppStore\\n****************\\033[0m\\n\u0026#34; #验证APP altoolPath=\u0026#34;/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Versions/A/Support/altool\u0026#34; \u0026#34;${altoolPath}\u0026#34; --validate-app \\ -f \u0026#34;${export_ipa_path}/${ipa_name}.ipa\u0026#34; \\ -u \u0026#34;$username_param\u0026#34; \\ -p \u0026#34;$password_param\u0026#34; \\ --output-format xml #上传APP \u0026#34;${altoolPath}\u0026#34; --upload-app \\ -f \u0026#34;${export_ipa_path}/${ipa_name}.ipa\u0026#34; \\ -u \u0026#34;$username_param\u0026#34; \\ -p \u0026#34;$password_param\u0026#34; \\ --output-format xml echo \u0026#34;\\033[32m****************\\n上传AppStore完毕\\n****************\\033[0m\\n\u0026#34; fi ","description":"","id":28,"section":"posts","tags":["ios"],"title":"iOS 一键上传AppStore脚本","uri":"https://yichenlove.github.io/posts/ios-pack/"},{"content":" 很多大型游戏都会有在游戏设置里自定义快捷键的功能。最近做项目,策划提出了这个要求,就开始着手操作了。\n UI部分是使用UGUI开发的,对这个比较熟。为了实现这个功能,原来是打算用InputField组件来做,但是后面有引发了一串操作上的Bug,看了UI源码,思考了好久也没解决。所以决定用最普通的Text组件来实现。下面直接上代码,然后慢慢解释。\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 using UnityEngine.EventSystems; using UnityEngine.UI; //需要继承的接口:IPointerEnterHandler,IPointerExitHandler,IPointerExitHandler,IPointerDownHandler public class UserDefinedText : MonoBehaviour,IPointerEnterHandler,IPointerExitHandler,IPointerExitHandler,IPointerDownHandler { [HideInInspector] public bool isSelecting = false;//是否被选中 [HideInInspector] public Text inputField;//文本显示 // Use this for initialization void Awake() { Init(); } private void Init() { inputField = GetComponent\u0026lt;Text\u0026gt;(); isSelecting = false; } string temp; void Update() { if (!isSelecting) return; if (Input.anyKeyDown) { KeyCode key = InputManager.Instance.GetKeyDownCode(); if ((key == KeyCode.Mouse0 || key == KeyCode.Mouse1 || key == KeyCode.Mouse2) \u0026amp;\u0026amp; currentPointedAt != inputField.gameObject) { //取消操作 inputField.text = temp; } else { inputField.text = key.ToString(); if (key == KeyCode.Mouse0 || key == KeyCode.Mouse1 || key == KeyCode.Mouse2) return; } isSelecting = false; } } #region Events private GameObject currentPointedAt = null; public void OnPointerEnter(PointerEventData eventData) { currentPointedAt = eventData.pointerEnter.gameObject; } public void OnPointerExit(PointerEventData eventData) { currentPointedAt = null; } //鼠标抬起,完成修改或者开始修改 public void OnPointerUp(PointerEventData eventData) { if (isSelecting) { //编辑结束 isSelecting = false; } else { //开始编辑 isSelecting = true; temp = inputField.text; inputField.text = \u0026#34;......\u0026#34;; } } public void OnPointerDown(PointerEventData eventData) { } #endregion } 下面是一个捕获键值的管理器,继承了一个单例。\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /// \u0026lt;summary\u0026gt; /// 键值输入管理 /// \u0026lt;/summary\u0026gt; public class InputManager:Singleton\u0026lt;InputManager\u0026gt; { public KeyCode cacheKey = KeyCode.None; public void Set_KeyCode(string name,KeyCode keycode) { } public KeyCode GetKeyDownCode() { if (Input.anyKeyDown) { foreach (KeyCode keyCode in Enum.GetValues(typeof(KeyCode))) { if (Input.GetKeyDown(keyCode)) { //Debug.Log(keyCode.ToString()); return keyCode; } } } return KeyCode.None; } } 补上在网上学的一个单例类代码:\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 /// \u0026lt;summary\u0026gt; /// 单例类 /// \u0026lt;/summary\u0026gt; public class Singleton\u0026lt;T\u0026gt; : MonoBehaviour where T :Singleton\u0026lt;T\u0026gt; { private static readonly object systemLock = new object();//线程锁 private static T instance; public static T Instance { get { if (instance == null) { lock (systemLock)//锁一下,避免多线程出问题 { instance = FindObjectOfType\u0026lt;T\u0026gt;(); if (instance == null) { GameObject obj = new GameObject(\u0026#34;_\u0026#34; + typeof(T).Name); DontDestroyOnLoad(obj); instance = obj.AddComponent\u0026lt;T\u0026gt;(); } if (instance == null) Debug.LogError(\u0026#34;Failed to create instance of \u0026#34; + typeof(T).FullName + \u0026#34;.\u0026#34;); } } return instance; } } void OnApplicationQuit() { if (instance != null) { instance = null; } } public static T CreateInstance() { if (Instance != null) Instance.OnCreate(); return Instance; } protected virtual void OnCreate() { } } 新建一个Text,挂在脚本,简单的自定义快捷键功能就实现了!\n ","description":"","id":29,"section":"posts","tags":null,"title":"Unity3d自定义快捷键","uri":"https://yichenlove.github.io/posts/unity-input/"},{"content":"1.Build-In Render 内置渲染器(默认)兼容太多,反而不能面面俱到,效果不好\n2.Scriptable Render Pipline 可编程渲染管线技术,是Unity提供的新渲染系统,可用C#脚本定制Unity的渲染过程,但自己定制渲染管线对编程要求很高,难度大,所以Unity提供里2个预制的管线,基本上涵盖了我们所有的需求,使用时不需要太底层的技术要求!\n2.1High Definition Render Pipleline 高清管线流程,专注于高端图形渲染,针对高端硬件配置,像PC、XBox 和Playstation,其面向高逼真度的游戏、图形demo和建筑渲染、超写实效果,以及所需的最佳图形效果。同时针对高端图形处理时,它要比内置渲染器要快得多,但如果用来做low poly风格的作品就有点杀鸡用牛刀了,纯属浪费资源和时间;但要想得到完成利用HDRP的完美表现能力,需要大量的贴图,漫反射贴图、高光贴图、金属贴图、平滑贴图、AO贴图、法线贴图、凹凸贴图、高度贴图\u0026hellip;So,要做HDRP流程需要非常长的时间和庞大的制作团队,还有充足的预算!建议5人以下的小团队慎入!\n2.2 Universal Render Pipleline 通用管线流程(URP前身为Lightweight Render Pipeline \u0026mdash; LWRP轻量级渲染管线),专注于性能,URP是选了不会错的渲染管线,它被设计为能够在任何平台上都能提供最好的性能,所以除非有特殊需求只能在HDRP或者SRP解决的,其他都应该选择URP,不管是移动端、主机、PC等,URP都能提供高性能的渲染,目前URP能做的也非常多,它拥有很多HDRP相同的功能,但为了在所有平台达到更好的性能,其做了一定的缩减,但这并不意味着URP做不出漂亮的游戏;\n这两种管线流程都利用里Unity新的可编程渲染管线技术,Unity正在把他们变成新的标准,shader graph,visual effect graph这些新功能都是他们的专属,这两个都是创作shader和粒子特效很棒的工具,也支持VR,不过HDRP做逼真风格的VR性能要求非常高!因为Unity实际上把所有东西渲染2次(因为有两个镜头),所以延迟渲染现在只有HDRP支持,不过对URP的支持官方已经在路上。\n两者最大的区别是对光照的支持,HDRP提供高级和丰富的光照功能,比如实时全局光照(RealTime GI),能够模拟光线反射、体积光、能模拟光穿过空气中的粒子,还有重头戏的光线跟踪,一种新的光线反射和阴影渲染技术,其原理是跟踪光线在场景中放射的路径,模拟光线在真实世界里与物体交互的效果,这技术对硬件性能非常高,但能产生非常逼真的效果,可以直接用来做电影级别的预渲染作品。\n另外一个区别是Shader,HDRP提供一系列高端的shader特效,例如高度、细节和parallax Maps,分别用于纹理的位移、细节和深度模拟,它还支持子面散射,用于模拟光线穿过很薄的物体,比如皮肤和衣物,它提供了高级的shader,像是stacklit,能够让你同时使用多个材质的属性,比如子面散射、彩虹色、各向异性和模糊参数化。\n对于后处理效果,两者不相伯仲,HDRP独占的最重要效果,包括AO(环境光遮蔽)、自动曝光(模拟人眼适应不同光线条件的能力)、屏幕空间反射(模拟基于屏幕上可见物体的反射);URP的AO支持已经在做了,但好东西不是都在HDRP,2D光照和阴影就是URP独占的,所以如果你在做2D游戏,就选URP!\n另外两个管线都支持一个很Cool的特效\u0026mdash;相机堆叠,让你能够同时用多个相机渲染。\n","description":"","id":30,"section":"posts","tags":["unity"],"title":"Unity SRP URP HDRP 的区别","uri":"https://yichenlove.github.io/posts/unity-urp-hdrp/"},{"content":"1、简述一下JVM加载class文件的原理机制。 Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。\n 1.1、类装载方式\n类装载方式,有两种 :\n1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,\n2.显式装载, 通过class.forname()等方法,显式加载需要的类\n 隐式加载与显式加载的区别:两者本质是一样?\n Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。\n 1.2、Java的类加载器\nJava的类加载器有三个,对应Java的三种类:(java中的类大致分为三种: 1.系统类 2.扩展类 3.由程序员自定义的类 )\n Bootstrap Loader // 负责加载系统类 (指的是内置类,像是String,对应于C#中的System类和C/C++标准库中的类) | - - ExtClassLoader // 负责加载扩展类(就是继承类和实现类) | - - AppClassLoader // 负责加载应用类(程序员自定义的类) 三个加载器各自完成自己的工作,但它们是如何协调工作呢?哪一个类该由哪个类加载器完成呢?为了解决这个问题,Java采用了委托模型机制。\n 1.3、类加载器工作原理\n委托模型机制的工作原理很简单:当类加载器需要加载类的时候,先请示其Parent(即上一层加载器)在其搜索路径载入,如果找不到,才在自己的搜索路径搜索该类。这样的顺序其实就是加载器层次上自顶而下的搜索,因为加载器必须保证基础类的加载。之所以是这种机制,还有一个安全上的考虑:如果某人将一个恶意的基础类加载到jvm,委托模型机制会搜索其父类加载器,显然是不可能找到的,自然就不会将该类加载进来。\n 1.4 JVM加载class文件的原理机制\n 装载:查找和导入class文件;\n 连接:\n 检查:检查载入的class文件数据的正确性;\n 准备:为类的静态变量分配存储空间;\n 解析:将符号引用转换成直接引用(这一步是可选的)\n 初始化:初始化静态变量,静态代码块。\n 这样的过程在程序调用类的静态成员的时候开始执行,所以静态方法main()才会成为一般程序的入口方法。类的构造器也会引发该动作。\n 2、多线程有几种实现方法?同步有几种实现方法? 多线程有两种实现方法,分别是继承Thread类与实现Runnable接口;也可以使用实现Callable接口,重写call()方法,这实际上是Executor框架中的功能类\n 同步的实现方面有两种,分别是synchronized,wait与notify\n 3、什么是分布式事务,如何解决分布式事务 分布式事务:单体应用拆分为分布式系统后,进程间的通讯机制和故障处理措施变的更加复杂。系统微服务化后,一个看似简单的功能,内部可能需要调用多个服务并操作多个数据库实现,服务调用的分布式事务问题变的非常突出。\n 如何解决:\n1、 基于XA协议的两阶段提交方案:交易中间件与数据库通过 XA 接口规范,使用两阶段提交来完成一个全局事务, XA 规范的基础是两阶段提交协议。\n 第一阶段是表决阶段,所有参与者都将本事务能否成功的信息反馈发给协调者;第二阶段是执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地在所有分支上提交或者回滚。\n 但是两阶段提交方案锁定资源时间长,对性能影响很大,基本不适合解决微服务事务问题。\n 2、 TCC方案:其将整个业务逻辑的每个分支显式的分成了Try、Confirm、Cancel三个操作。Try部分完成业务的准备工作,confirm部分完成业务的提交,cancel部分完成事务的回滚。\n 事务开始时,业务应用会向事务协调器注册启动事务。之后业务应用会调用所有服务的try接口,完成一阶段准备。之后事务协调器会根据try接口返回情况,决定调用confirm接口或者cancel接口。如果接口调用失败,会进行重试。\n TCC方案让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。 当然TCC方案也有不足之处,集中表现在以下两个方面:\n 对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。 实现难度较大。需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。 3、 基于消息的最终一致性方案\n消息一致性方案是通过消息中间件保证上、下游应用数据操作的一致性。基本思路是将本地操作和发送消息放在一个事务中,保证本地操作和消息发送要么两者都成功或者都失败。下游应用向消息系统订阅该消息,收到消息后执行相应操作。\n 4、 GTS–分布式事务解决方案\n 性能超强:\nGTS通过大量创新,解决了事务ACID特性与高性能、高可用、低侵入不可兼得的问题。单事务分支的平均响应时间在2ms左右,3台服务器组成的集群可以支撑3万TPS以上的分布式事务请求。\n 应用侵入性极低:\nGTS对业务低侵入,业务代码最少只需要添加一行注解(@TxcTransaction)声明事务即可。业务与事务分离,将微服务从事务中解放出来,微服务关注于业务本身,不再需要考虑反向接口、幂等、回滚策略等复杂问题,极大降低了微服务开发的难度与工作量。\n 完整解决方案:\nGTS支持多种主流的服务框架,包括EDAS,Dubbo,Spring Cloud等。\n有些情况下,应用需要调用第三方系统的接口,而第三方系统没有接入GTS。此时需要用到GTS的MT模式。GTS的MT模式可以等价于TCC模式,用户可以根据自身业务需求自定义每个事务阶段的具体行为。MT模式提供了更多的灵活性,可能性,以达到特殊场景下的自定义优化及特殊功能的实现。\n 容错能力强:\nGTS解决了XA事务协调器单点问题,实现真正的高可用,可以保证各种异常情况下的严格数据一致。\n 参考文档:\n https://www.cnblogs.com/jiangyu666/p/8522547.html 4. HashMap与Hashtable的异同? 相同点:键不可重复,值可以重复。底层都是哈希表(单链Node构成的数组)。\n不同点:\n HashMap:线程不安全,允许key和value为null。 Hashtable:线程安全,key和value都不能为null,否则会抛NullPointerException异常。 5. ArrayList和Vector和LinkedList 相同点:都是List的子类,因此排列有序,值可重复。\n特点:\n ArrayList :底层使用数组,因此查找速度快,增删速度慢。线程不安全。当容量不足时,扩容方案是 当前容量 * 1.5 +1 Vector:底层使用数组,因此查找速度快,增删速度慢。因为add方法使用了synchronized关键字,所以线程安全,但效率低。当容量不足时,扩容方案是当前容量的一倍。 LinkedList:底层使用的是双向循环链表结构,因此查询慢,增删快。线程不安全。 6. HashMap实现(数组 + 链表 + 红黑树red-black tree) HashMap源码剖析:\n可以看到,其数据结构是一个单项链表Node数组构成。根据Node的数据结构,可以看出,HashMap中存储了一个hash(算法生成,后面具体讲)、key、value,以及一个Node指针。\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // @since 2 public class HashMap\u0026lt;K,V\u0026gt; extends AbstractMap\u0026lt;K,V\u0026gt; implements Map\u0026lt;K,V\u0026gt;, Cloneable, Serializable { transient Node\u0026lt;K,V\u0026gt;[] table; //数组Node //Node是一个单项链表,静态内部类 static class Node\u0026lt;K,V\u0026gt; implements Map.Entry\u0026lt;K,V\u0026gt; { final int hash; final K key; V value; Node\u0026lt;K,V\u0026gt; next; Node(int hash, K key, V value, Node\u0026lt;K,V\u0026gt; next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } // ... } } put方法如下,该方法的主要目的是要确定插入节点的位置,如果节点已存在,则替换value值。\n可以看到,\n 1 (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16) 用来计算出hash值后, (n - 1) \u0026amp; hash来计算出这个key、value应该存储再数组的下表的那个链表下, n = table.length。所以\n1 tab[i = (n - 1) \u0026amp; hash] 实际上是链表头节点位置。然后顺着这个头节点遍历下去,如果节点已存在,则替换value值;否则讲节点放在链尾上。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } //hash值生成算法 static final int hash(Object key) { int h; //取key得hashCode码 异或 key得hashCode码无符号右移16的结果,这个值算出来是可重复的。也就是不同的key,可能算出来相同的值,这个值就是数组的下标,相同值得key会放在这个数据下表下面得链表中。 return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16); } //具体的算法 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) //如果table为空,就初始化table n = (tab = resize()).length; if ((p = tab[i = (n - 1) \u0026amp; hash]) == null) //如果数组下标下,没有链表 tab[i] = newNode(hash, key, value, null); //新建一个Node节点,放在指定位置上 else {//如果数组下标下,存在链表 Node\u0026lt;K,V\u0026gt; e; K k; if (p.hash == hash \u0026amp;\u0026amp; ((k = p.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) //判断是不是和头节点的key相同,相同则位置确定 e = p; else if (p instanceof TreeNode) //看是不是红黑树 e = ((TreeNode\u0026lt;K,V\u0026gt;)p).putTreeVal(this, tab, hash, key, value); else { //遍历链表, for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //节点遍历完时,即链表最后一个了,还没有找到与key相同的节点,则把该节点放在最后。 p.next = newNode(hash, key, value, null); if (binCount \u0026gt;= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); //如果节点超过8个,降链表结构转换为红黑树结构 break; } if (e.hash == hash \u0026amp;\u0026amp; ((k = e.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) //如链表中有key值相等的,则目标位置确定 break; p = e; } } if (e != null) { // existing mapping for key ,则值替换 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; //如果key已经存在,替换value值 afterNodeAccess(e); return oldValue; } } ++modCount; if (++size \u0026gt; threshold) resize(); afterNodeInsertion(evict); return null; } get方法:\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public V get(Object key) { Node\u0026lt;K,V\u0026gt; e; return (e = getNode(hash(key), key)) == null ? null : e.value; //hash生成和上面put方法一样。 } final Node\u0026lt;K,V\u0026gt; getNode(int hash, Object key) { Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; first, e; int n; K k; if ((tab = table) != null \u0026amp;\u0026amp; (n = tab.length) \u0026gt; 0 \u0026amp;\u0026amp; (first = tab[(n - 1) \u0026amp; hash]) != null) {//数组部位空,并且头节点不为空 if (first.hash == hash \u0026amp;\u0026amp; // always check first node ((k = first.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k))))//如果头节点就是该key,则目标找到 return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode\u0026lt;K,V\u0026gt;)first).getTreeNode(hash, key); do { if (e.hash == hash \u0026amp;\u0026amp; ((k = e.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) return e; } while ((e = e.next) != null); //遍历找出 } } return null; } 7. volatile 关键字的作用 (变量可见性、禁止重排序) Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。\nvolatile 变量具备两种特性,volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。\n 变量可见性:其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。 禁止重排序:volatile 禁止了指令重排。但是使用volatile将使得JVM优化失去作用,导致效率较低,所以要在必要的时候使用。 原理:\n当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有多个 CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的 CPUcache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache这一步。\n 适用场景:\n值得说明的是对 volatile 变量的单次读/写操作可以保证原子性的,如 long 和 double 类型变量,但是并不能保证 i++这种操作的原子性,因为本质上 i++是读、写两次操作。在某些场景下可以代替 Synchronized。但是,volatile 不能完全取代 Synchronized 的位置,只有在一些特殊的场景下,才能适用 volatile。\n 总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:\n(1)对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean flag = true)。\n(2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的 volatile 变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用volatile。\n 8. Synchronized 同步锁 synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。\n Synchronized 作用范围:\n 作用于方法时,锁住的是对象的实例(this); 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程; synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。 9. AtomicInteger原子类 首先说明,此处 AtomicInteger,一个提供原子操作的 Integer 的类,常见的还有\nAtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他们的实现原理相同,区别在与运算对象类型的不同。令人兴奋地,还可以通过AtomicReference将一个对象的所有操作转化成原子操作。\n 我们知道,在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。通常我们会使用 synchronized 将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。\n 部分源码如下:\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField(\u0026#34;value\u0026#34;)); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; //...... } 在这里说下其中的value,这里value使用了volatile关键字,volatile在这里可以做到的作用是使得多个线程可以共享变量,但是问题在于使用volatile将使得JVM优化失去作用,导致效率较低,所以要在必要的时候使用。\n ","description":"","id":31,"section":"posts","tags":["java"],"title":"Java中的难点知识","uri":"https://yichenlove.github.io/posts/java-diff/"},{"content":"制作性能更高的UI DrawCall和Batch 抛去他复杂的定义,从字面意思上来理解,DrawCall,就是CPU准备好了数据呼叫GPU去绘制。假设场景中有两个按钮,他们使用了两张不同的图,那么每一帧就会存在2个DrawCall来分别绘制这两个按钮。但是,如果这两个按钮使用的是同一个贴图,而且使用了同一个材质,那么每一帧就只会有一个DrawCall,DrawCall的数量越低,表示性能越好。\nBatch可以理解为DrawCall的另一种称呼,每一次DrawCall都会产生一个Batch,里面存放这需要绘制的顶点信息,Batch由CPU送往GPU进行绘制。所以Drawcall越多,Batch的数量越多,CPU的负担会越大。NVIDIA 在 GDC 曾经提出,25K batchs/sec 会消耗掉 1GHz 的 CPU,100% 的使用率。\n所以制作性能更好的UI,最基本的思路,就是尽量使用同一个材质,将单独的图片制作成图集,在Unity里进行slice之后再使用。\n 关于Batch 上面为了便于理解,把Batch解释成一个装有顶点信息的箱子,实际上batch是一个过程。batch render 是大部分引擎提高渲染效率的方法,基本原理就是通过将一些渲染状态一致的物体合成一个大物体,一次提交给gpu进行绘制,如果不batch的话,就要提交给很多次,这可以显著的节省drawcall,实际上这主要节省了cpu的时间,cpu从提交多次到提交一次,对gpu来说也不用多次切换渲染状态。当然能batch的前提一定是渲染状态一致的一组物体。\nUnity3d的批渲染分为两种,动态和静态。\n静态批\n要求:必须使用同一材质,然后在编辑器里设置为static batching的。\n特点:静态批是无法运动的。\n所以一般制作流程上,对于场景这些静态的物体都采用静态批,美术会根据场景的规模,将相邻的一片物件的贴图合并到一张或几张1024或512的大图上,这样这些物件可以使用同一个material,就可以静态批在一起,大幅节省dc(drawCall)。静态批的时候Unity3d会在运行时生成一个合并的大模型,并且为这个模型指定一张共同的贴图,所以这个批在一起的数量是有限的,如果批在一起的定点数过多,它就会自动分成两个。\n动态批\n动态批是对那些没有标记成static batching的物体在runtime unity自动将他们批在一起,这个是可以支持运动物体的,但是限制较为严格:\n1.一个批次总顶点单元少于900\n2.批在一起的所有的模型应用同样的缩放值\n3.使用相同的材质\n4.相同的一张lightmap等等\n 动静分离 从上面的内容我们知道,只要把UI统一材质,然后使用图集,就能减少DrawCall,那么我们把所有的内容都集中到一个图上,做一个超大的图集,是不是能做到只有一个DrawCall呢?显然没有那么简单。\n在大部分情况下,UI是静止的,这种时候使用上面的方法确实是最好的办法。但有很多时候UI还是会动的,比如血条长短的变化,比如需要一个UI从视野外面飞入的动画。这种时候如果你把会动的UI和静止的UI使用了同一个图集,每一次的动画都会导致Batch箱子里的内容发生变化从而静止的UI也都会重新绘制,这是得不偿失的。\n解决这个问题的办法是做分离,我们可以在canvas节点下新建canvas,Unity会在每个canvas内做合并。我们可以把静态的UI放在一个canvas内,动态的UI放在另一个canvas内部,这样做到动静分离,就能保证大部分的UI元素都会被合并渲染,而且动态的UI变化不会导致静态的UI重绘。\n 渲染顺序对合批的影响 依次点击Window–Analysis–FrameDebugger打开界面\n启用后能看到最近的帧的数据以及每个DrawCall,可以在这里检查预计的合批是否成功了。\n首先我们来了解渲染顺序\n渲染顺序决定了物体的覆盖顺序,可以理解为画笔的笔顺,后落笔会盖住先落笔的内容。影响渲染渲染顺序的因素有哪些呢,从上到下依次是:相机高度(深度越大越后渲染);透明、不透明物体间隔(RenderQueue 2500是透明与不透明的分水岭);Sorting Layer;Order In Layer;等等,下图很好的表现了这些因素对渲染顺序的影响。\n了解了渲染顺序后,我们来理解这样一个道理,假设有ABC这样一个渲染顺序,A和C满足合批条件,但B不满足。此时会无法合批,因为渲染A和C的batch无法用于B,所以此时会产生3个DrawCall。但如果我们把渲染顺序改一下,不让B夹在A和C中间,我们就能减少一次DrawCall。\n在实际项目中,合理的安排渲染顺序可以节省很多不必要的DrawCall。所以在UI开发的过程中,我们应该在不影响效果的情况下,尽可能把可以合批的控件安排一个不间断的渲染顺序。\n 易被忽略的Raycast Target RaycastTarget可以简单理解为可交互性,有这个选项控件才能响应触摸,滑动,点击等等操作,同时Unity把所有的控件都打开了这个开关,这就导致大量原本不需要响应操作的控件白白浪费了大量资源,所以我们应该合理地取消这个开关。例如,绝大部分的text,不需要响应点击的Image都不需要RaycasrTarget,再比如一些进度条,之类的,只是显示作用不需要交互,我们可以合理地关闭这些控件的Raycast以达到节省性能的目的。\n ","description":"","id":32,"section":"posts","tags":["unity"],"title":"Unity的DraCall和Batch","uri":"https://yichenlove.github.io/posts/unity-drawcall-batch/"},{"content":" 欧几里德算法是用来求两个正整数最大公约数的算法。古希腊数学家欧几里德在其著作《The Elements》中最早描述了这种算法,所以被命名为欧几里德算法。\n 假如需要求 1997 和 615 两个正整数的最大公约数,用欧几里德算法,是这样进行的:\n 1997 ÷ 615 = 3 (余 152)\n615 ÷ 152 = 4(余7)\n152 ÷ 7 = 21(余5)\n7 ÷ 5 = 1 (余2)\n5 ÷ 2 = 2 (余1)\n2 ÷ 1 = 2 (余0)\n 至此,最大公约数为1\n 以除数和余数反复做除法运算,当余数为 0 时,取当前算式除数为最大公约数,所以就得出了 1997 和 615 的最大公约数 1。\n 欧几里得算法定理及证明 定理:两个整数的最大公约数等于其中较小的那个数和两数相除余数的最大公约数。最大公约数(Greatest Common Divisor)缩写为GCD。\n gcd(a,b) = gcd(b,a mod b) (不妨设 a \u0026gt; b 且 r = a mod b , r不为0)\n 证法一\n a 可以表示成\na=kb+r\na,b,k,r 皆为正整数,且r \u0026lt; b),则\nr= a mod b\n 假设d是a,b的一个公约数,记作\nd|a,d|b\n即a和b都可以被d整除。而r = a − kb,两边同时除以d,\nr ÷ d = a ÷ d - kb ÷ d = m 由等式右边可知 m 为整数,因此\nd|r 即\nd|(a mod b) 因此d也是b,(a mod b)的公约数 假设d是b,(a mod b)的公约数, 则\nd∣b,d∣(a−kb)\nk是一个整数,进而d|a 。因此d也是a,b的公约数\n 证法二\n 假设c=gcd(a,b),则存在m,n,使\na=mc,b=nc\n 令r=amodb,即存在k,使\nr=a−kb=mc−knc=(m−kn)c\n 故\ngcd(b,amodb)=gcd(b,r)=gcd(nc,(m−kn)c)=gcd(n,m−kn)∗c\n 假设d=gcd(n,m−kn), 则存在x,y, 使n=xd,m−kn=yd; 故\nm=yd+kn=yd+kxd=(y+kx)d\n 故有\na=mc=(y+kx)dc,b=nc=xdc\n可得\ngcd(a,b)=gcd((y+kx)dc,xdc)=dc\n 由于gcd(a,b)=c, 故d=1;即\ngcd(n,m−kn)=1\n故可得\ngcd(b,amodb)=c\n故得证\ngcd(a,b)=gcd(b,amodb)\n 注意:两种方法是有区别的。\n 欧几里得算法python实现 1 2 3 4 5 6 7 8 9 def gcd(n1,n2): if n1\u0026lt;n2: n1,n2=n2,n1 while True: m = np.mod(n1,n2) if m == 0: return n2 else: n1,n2 = n2,m 扩展欧几里得算法 扩展欧几里得算法(英语:Extended Euclidean algorithm)是欧几里得算法(又叫辗转相除法)的扩展。已知整数a、b,扩展欧几里得算法可以在求得a、b的最大公约数的同时,能找到整数x、y(其中一个很可能是负数),使它们满足等式:\n ax + by = gcd (a , b) 如果a是负数,可以把问题转化成:\n ∣ a ∣ ( − x ) + by = gcd (∣a∣ , b) 然后令 x ′ = ( − x ) 。\n 通常谈到最大公约数时,我们都会提到一个非常基本的事实:给予二个整数a、b,必存在整数x、y使得 a x + b y = c d (a , b) 。\n 有两个数a,b,对它们进行辗转相除法,可得它们的最大公约数——这是众所周知的。然后,收集辗转相除法中产生的式子,倒回去,可以得到 a x + b y = gcd ( a , b ) 的整数解。\n 扩展欧几里得算法可以用来计算模反元素(也叫模逆元),而模反元素在RSA加密算法中有举足轻重的地位。\n 举例 例1:求 47 x + 30 y = 1 的整数解\n 解:先用碾转相除法求47、30的最大公约数\n q表示商、r表示余数\n 47 ÷ 30 = 1 (余 17) → q 1 = 1 , r 1 = 17 30 ÷ 17 = 1(余13) → q 2 = 1 , r 2 = 13 17 ÷ 13 = 1(余4) → q 3 = 1 , r 3 = 4 13 ÷ 4 = 3 (余1) → q 4 = 3 , r 4 = 1 4 ÷ 1 = 4 (余0) → q 5 = 4 , r 5 = 0 用矩阵法来表示:\n $$\n\\left[\n\\begin{matrix}\na \\\\ b\n\\end{matrix}\n\\right] =\n\\prod_{i=0}^n\n\\left[\n\\begin{matrix}\nq_{i} \u0026amp;1 \\\\\n1 \u0026amp;0\n\\end{matrix}\n\\right]\n\\left[\n\\begin{matrix}\nr_{N-1} \\\\\n0\n\\end{matrix}\n\\right]\n$$\n 即:\n $$\n\\left[\n\\begin{matrix}\n47 \\\\\n30\n\\end{matrix}\n\\right] =\n\\prod_{i=0}^5\n\\left[\n\\begin{matrix}\nq_{i} \u0026amp; 1 \\\\\n1 \u0026amp; 0\n\\end{matrix}\n\\right]\n\\left[\n\\begin{matrix}\nr_{5-1} \\\\\n0\n\\end{matrix}\n\\right] =\n\\left[\n\\begin{matrix}\n1 \u0026amp; 1 \\\\\n1 \u0026amp; 0\n\\end{matrix}\n\\right]\n\\left[\n\\begin{matrix}\n1 \u0026amp; 1\\\\\n1 \u0026amp; 0\n\\end{matrix}\n\\right]\n\\left[\n\\begin{matrix}\n1 \u0026amp; 1 \\\\\n1 \u0026amp; 0\n\\end{matrix}\n\\right]\n\\left[\n\\begin{matrix}\n3 \u0026amp; 1 \\\\\n1 \u0026amp; 0\n\\end{matrix}\n\\right]\n\\left[\n\\begin{matrix}\n4 \u0026amp; 1 \\\\\n1 \u0026amp; 0\n\\end{matrix}\n\\right]\n\\left[\n\\begin{matrix}\n1 \\\\\n0\n\\end{matrix}\n\\right] =\n\\left[\n\\begin{matrix}\n47 \u0026amp; 11 \\\\\n30 \u0026amp; 7\n\\end{matrix}\n\\right]\n\\left[\n\\begin{matrix}\n1\\\\\n0\n\\end{matrix}\n\\right]\n$$\n 所以:\n $$\n\\left[\n\\begin{matrix}\n1\\\\\n0\n\\end{matrix}\n\\right] =\n\\left[\n\\begin{matrix}\n-7 \u0026amp; 11\\\\\n30 \u0026amp; -47\n\\end{matrix}\n\\right]\n\\left[\n\\begin{matrix}\n47\\\\\n30\n\\end{matrix}\n\\right]\n$$\n 所以x、y有两组解,分别是x = − 7 , y = 11 ; x = 30 , y = − 47\n 扩展欧几里得算法python实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 def ext_euclid(a,b): flag = False if a\u0026lt;b: flag = True a,b=b,a Q=np.eye(2) while True: r = np.mod(a,b) q = int(a/b) if r != 0: Q=np.dot(Q,np.array([[q,1],[1,0]])) a,b = b,r else: Q=np.dot(Q,np.array([[q,1],[1,0]])) break _Q=np.linalg.inv(Q) print(_Q) x,y = int(np.round(_Q[0,0])),int(np.round(_Q[0,1])) if flag == True: print(\u0026#39;xxx\u0026#39;) x,y = y,x return (x,y) 或者如下方法:\n 1 2 3 4 5 6 7 8 def ext_euclid(a, b): if b == 0: return 1, 0, a else: x, y, q = ext_euclid(b, a % b) # q = gcd(a, b) = gcd(b, a%b) x, y = y, (x - (a // b) * y) return x, y, q 参考文献 欧几里得算法\n扩展欧几里得算法\n","description":"","id":33,"section":"posts","tags":["algorithms"],"title":"欧几里得算法","uri":"https://yichenlove.github.io/posts/euclidean-algorithm/"},{"content":" 编写一个递归的静态方法计算ln(N!)的值。\n 代码\nalo\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Main { public static double f(int N){ //递归的题目还是要靠递归的方式解决 //递归吧 if (N == 1) return 0; return f(N-1)+Math.log(N); } public static void main(String[] args) { System.out.println(Math.log(3628800)); int N = 10; System.out.println(f(N)); } } 结果\n 15.104412573075516 15.104412573075518 知识点-对数公式\n$\\log_a (MN)$ = $\\log_a (M)$ + $\\log_a (N)$\n$\\log_a (M/N)$ = $\\log_a (M)$ - $\\log_a (N)$\n$\\log_a (M^N)$ = n$\\log_a (M)$\n 公式描述:公式中a叫做对数的底,M、N叫做真数。\n 知识点-阶乘公式\nn! = 1 * 2 * 3 * \u0026hellip; (n - 1) * n\n ","description":"","id":34,"section":"posts","tags":["algorithms"],"title":"递归的静态方法计算ln(N!)的值","uri":"https://yichenlove.github.io/posts/ln-factorial/"},{"content":"# 1. 反射简介 # 1.1. 什么是反射 反射(Reflection)是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。\n 通过反射机制,可以在运行时访问 Java 对象的属性,方法,构造方法等。\n # 1.2. 反射的应用场景 反射的主要应用场景有:\n 开发通用框架 - 反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 JavaBean、Filter 等),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。 动态代理 - 在切面编程(AOP)中,需要拦截特定的方法,通常,会选择动态代理方式。这时,就需要反射技术来实现了。 注解 - 注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执行行为。如果没有反射机制,注解并不比注释更有用。 可扩展性功能 - 应用程序可以通过使用完全限定名称创建可扩展性对象实例来使用外部的用户定义类。 # 1.3. 反射的缺点 性能开销 - 由于反射涉及动态解析的类型,因此无法执行某些 Java 虚拟机优化。因此,反射操作的性能要比非反射操作的性能要差,应该在性能敏感的应用程序中频繁调用的代码段中避免。 破坏封装性 - 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。 内部曝光 - 由于反射允许代码执行在非反射代码中非法的操作,例如访问私有字段和方法,所以反射的使用可能会导致意想不到的副作用,这可能会导致代码功能失常并可能破坏可移植性。反射代码打破了抽象,因此可能会随着平台的升级而改变行为。 # 2. 反射机制 # 2.1. 类加载过程 类加载的完整过程如下:\n 在编译时,Java 编译器编译好 .java 文件之后,在磁盘中产生 .class 文件。 .class 文件是二进制文件,内容是只有 JVM 能够识别的机器码。 JVM 中的类加载器读取字节码文件,取出二进制数据,加载到内存中,解析.class 文件内的信息。类加载器会根据类的全限定名来获取此类的二进制字节流;然后,将字节流所代表的静态存储结构转化为方法区的运行时数据结构;接着,在内存中生成代表这个类的 java.lang.Class 对象。 加载结束后,JVM 开始进行连接阶段(包含验证、准备、初始化)。经过这一系列操作,类的变量会被初始化。 # 2.2. Class 对象 要想使用反射,首先需要获得待操作的类所对应的 Class 对象。 Java 中,无论生成某个类的多少个对象,这些对象都会对应于同一个 Class 对象。这个 Class 对象是由 JVM 生成的,通过它能够获悉整个类的结构。所以, java.lang.Class 可以视为所有反射 API 的入口点。\n 反射的本质就是:在运行时,把 Java 类中的各种成分映射成一个个的 Java 对象。\n 举例来说,假如定义了以下代码:\n 1 User user = new User(); 步骤说明:\n JVM 加载方法的时候,遇到 new User(),JVM 会根据 User 的全限定名去加载 User.class 。 JVM 会去本地磁盘查找 User.class 文件并加载 JVM 内存中。 JVM 通过调用类加载器自动创建这个类对应的 Class 对象,并且存储在 JVM 的方法区。注意: 一个类有且只有一个 Class 对象 。 # 2.3. 方法的反射调用 方法的反射调用,也就是 Method.invoke 方法。\n Method.invoke 方法源码:\n 1 2 3 4 5 6 7 8 9 10 11 public final class Method extends Executable { ... public Object invoke(Object obj, Object... args) throws ... { ... // 权限检查 MethodAccessor ma = methodAccessor; if (ma == null) { ma = acquireMethodAccessor(); } return ma.invoke(obj, args); } } Method.invoke 方法实际上委派给 MethodAccessor 接口来处理。它有两个已有的具体实现:\n NativeMethodAccessorImpl:本地方法来实现反射调用 DelegatingMethodAccessorImpl:委派模式来实现反射调用 每个 Method 实例的第一次反射调用都会生成一个委派实现( DelegatingMethodAccessorImpl),它所委派的具体实现便是一个本地实现( NativeMethodAccessorImpl)。本地实现非常容易理解。当进入了 Java 虚拟机内部之后,我们便拥有了 Method 实例所指向方法的具体地址。这时候,反射调用无非就是将传入的参数准备好,然后调用进入目标方法。\n 【示例】通过抛出异常方式 打印 Method.invoke 调用轨迹\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class MethodDemo01 { public static void target(int i) { new Exception(\u0026#34;#\u0026#34; + i).printStackTrace(); } public static void main(String[] args) throws Exception { Class\u0026lt;?\u0026gt; clazz = Class.forName(\u0026#34;io.github.dunwu.javacore.reflect.MethodDemo01\u0026#34;); Method method = clazz.getMethod(\u0026#34;target\u0026#34;, int.class); method.invoke(null, 0); } } // Output: // java.lang.Exception: #0 // at io.github.dunwu.javacore.reflect.MethodDemo01.target(MethodDemo01.java:12) // at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) // at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 先调用 DelegatingMethodAccessorImpl;然后调用 NativeMethodAccessorImpl,最后调用实际方法。\n 为什么反射调用 DelegatingMethodAccessorImpl作为中间层,而不是直接交给本地实现?\n 其实,Java 的反射调用机制还设立了另一种动态生成字节码的实现(下称动态实现),直接使用 invoke 指令来调用目标方法。之所以采用委派实现,便是为了能够在本地实现以及动态实现中切换。动态实现和本地实现相比,其运行效率要快上 20 倍。这是因为动态实现无需经过 Java 到 C++ 再到 Java 的切换,但由于生成字节码十分耗时,仅调用一次的话,反而是本地实现要快上 3 到 4 倍。\n 考虑到许多反射调用仅会执行一次,Java 虚拟机设置了一个阈值 15(可以通过 -Dsun.reflect.inflationThreshold来调整),当某个反射调用的调用次数在 15 之下时,采用本地实现;当达到 15 时,便开始动态生成字节码,并将委派实现的委派对象切换至动态实现,这个过程我们称之为 Inflation。\n 【示例】执行 java -verbose:class MethodDemo02 启动\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class MethodDemo02 { public static void target(int i) { new Exception(\u0026#34;#\u0026#34; + i).printStackTrace(); } public static void main(String[] args) throws Exception { Class\u0026lt;?\u0026gt; klass = Class.forName(\u0026#34;io.github.dunwu.javacore.reflect.MethodDemo02\u0026#34;); Method method = klass.getMethod(\u0026#34;target\u0026#34;, int.class); for (int i = 0; i \u0026lt; 20; i++) { method.invoke(null, i); } } } 输出内容:\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 java.lang.Exception: #14 at io.github.dunwu.javacore.reflect.MethodDemo02.target(MethodDemo02.java:13) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at io.github.dunwu.javacore.reflect.MethodDemo02.main(MethodDemo02.java:20) [Loaded sun.reflect.ClassFileConstants from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded sun.reflect.AccessorGenerator from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded sun.reflect.MethodAccessorGenerator from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded sun.reflect.ByteVectorFactory from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded sun.reflect.ByteVector from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded sun.reflect.ByteVectorImpl from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded sun.reflect.ClassFileAssembler from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded sun.reflect.UTF8 from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded sun.reflect.Label from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded sun.reflect.Label$PatchInfo from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded java.util.ArrayList$Itr from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded sun.reflect.MethodAccessorGenerator$1 from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded sun.reflect.ClassDefiner from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded sun.reflect.ClassDefiner$1 from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] [Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__] java.lang.Exception: #15 at io.github.dunwu.javacore.reflect.MethodDemo02.target(MethodDemo02.java:13) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at io.github.dunwu.javacore.reflect.MethodDemo02.main(MethodDemo02.java:20) [Loaded java.util.concurrent.ConcurrentHashMap$ForwardingNode from D:\\Tools\\Java\\jdk1.8.0_192\\jre\\lib\\rt.jar] java.lang.Exception: #16 at io.github.dunwu.javacore.reflect.MethodDemo02.target(MethodDemo02.java:13) at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at io.github.dunwu.javacore.reflect.MethodDemo02.main(MethodDemo02.java:20) // ...省略 可以看到,从第 16 次开始后,都是使用 DelegatingMethodAccessorImpl,不再使用本地实现 NativeMethodAccessorImpl。\n # 2.4. 反射调用的开销 方法的反射调用会带来不少性能开销,原因主要有三个:\n 变长参数方法导致的 Object 数组 基本类型的自动装箱、拆箱 还有最重要的方法内联 Class.forName 会调用本地方法, Class.getMethod 则会遍历该类的公有方法。如果没有匹配到,它还将遍历父类的公有方法。可想而知,这两个操作都非常费时。\n 注意,以 getMethod 为代表的查找方法操作,会返回查找得到结果的一份拷贝。因此,我们应当避免在热点代码中使用返回 Method 数组的 getMethods 或者 getDeclaredMethods 方法,以减少不必要的堆空间消耗。在实践中,我们往往会在应用程序中缓存 Class.forName 和 Class.getMethod 的结果。\n 下面只关注反射调用本身的性能开销。\n 第一,由于 Method.invoke 是一个变长参数方法,在字节码层面它的最后一个参数会是 Object 数组(感兴趣的同学私下可以用 javap 查看)。Java 编译器会在方法调用处生成一个长度为传入参数数量的 Object 数组,并将传入参数一一存储进该数组中。\n 第二,由于 Object 数组不能存储基本类型,Java 编译器会对传入的基本类型参数进行自动装箱。\n 这两个操作除了带来性能开销外,还可能占用堆内存,使得 GC 更加频繁。(如果你感兴趣的话,可以用虚拟机参数 -XX:+PrintGC 试试。)那么,如何消除这部分开销呢?\n # 3. 使用反射 # 3.1. java.lang.reflect 包 Java 中的 java.lang.reflect 包提供了反射功能。 java.lang.reflect 包中的类都没有 public 构造方法。\n java.lang.reflect 包的核心接口和类如下:\n Member 接口:反映关于单个成员(字段或方法)或构造函数的标识信息。 Field 类:提供一个类的域的信息以及访问类的域的接口。 Method 类:提供一个类的方法的信息以及访问类的方法的接口。 Constructor 类:提供一个类的构造函数的信息以及访问类的构造函数的接口。 Array 类:该类提供动态地生成和访问 JAVA 数组的方法。 Modifier 类:提供了 static 方法和常量,对类和成员访问修饰符进行解码。 Proxy 类:提供动态地生成代理类和类实例的静态方法。 # 3.2. 获取 Class 对象 获取 Class 对象的三种方法:\n (1) ``Class.forName 静态方法\n 【示例】使用 Class.forName 静态方法获取 Class 对象\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package io.github.dunwu.javacore.reflect; public class ReflectClassDemo01 { public static void main(String[] args) throws ClassNotFoundException { Class c1 = Class.forName(\u0026#34;io.github.dunwu.javacore.reflect.ReflectClassDemo01\u0026#34;); System.out.println(c1.getCanonicalName()); Class c2 = Class.forName(\u0026#34;[D\u0026#34;); System.out.println(c2.getCanonicalName()); Class c3 = Class.forName(\u0026#34;[[Ljava.lang.String;\u0026#34;); System.out.println(c3.getCanonicalName()); } } //Output: //io.github.dunwu.javacore.reflect.ReflectClassDemo01 //double[] //java.lang.String[][] 使用类的完全限定名来反射对象的类。常见的应用场景为:在 JDBC 开发中常用此方法加载数据库驱动。\n (2) 类名 + .class``\n 【示例】直接用类名 + .class 获取 Class 对象\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class ReflectClassDemo02 { public static void main(String[] args) { boolean b; // Class c = b.getClass(); // 编译错误 Class c1 = boolean.class; System.out.println(c1.getCanonicalName()); Class c2 = java.io.PrintStream.class; System.out.println(c2.getCanonicalName()); Class c3 = int[][][].class; System.out.println(c3.getCanonicalName()); } } //Output: //boolean //java.io.PrintStream //int[][][] (3) ``Object的getClass 方法\n Object 类中有 getClass 方法,因为所有类都继承 Object 类。从而调用 Object 类来获取 Class 对象。\n 【示例】 Object 的 getClass 方法获取 Class 对象\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package io.github.dunwu.javacore.reflect; import java.util.HashSet; import java.util.Set; public class ReflectClassDemo03 { enum E {A, B} public static void main(String[] args) { Class c = \u0026#34;foo\u0026#34;.getClass(); System.out.println(c.getCanonicalName()); Class c2 = ReflectClassDemo03.E.A.getClass(); System.out.println(c2.getCanonicalName()); byte[] bytes = new byte[1024]; Class c3 = bytes.getClass(); System.out.println(c3.getCanonicalName()); Set\u0026lt;String\u0026gt; set = new HashSet\u0026lt;\u0026gt;(); Class c4 = set.getClass(); System.out.println(c4.getCanonicalName()); } } //Output: //java.lang.String //io.github.dunwu.javacore.reflect.ReflectClassDemo.E //byte[] //java.util.HashSet # 3.3. 判断是否为某个类的实例 判断是否为某个类的实例有两种方式:\n 用 instanceof 关键字 用 Class对象的isInstance 方法 (它是一个 Native 方法) 【示例】\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class InstanceofDemo { public static void main(String[] args) { ArrayList arrayList = new ArrayList(); if (arrayList instanceof List) { System.out.println(\u0026#34;ArrayList is List\u0026#34;); } if (List.class.isInstance(arrayList)) { System.out.println(\u0026#34;ArrayList is List\u0026#34;); } } } //Output: //ArrayList is List //ArrayList is List # 3.4. 创建实例 通过反射来创建实例对象主要有两种方式:\n 用 Class 对象的 newInstance 方法。 用 Constructor 对象的 newInstance 方法。 【示例】\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class NewInstanceDemo { public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { Class\u0026lt;?\u0026gt; c1 = StringBuilder.class; StringBuilder sb = (StringBuilder) c1.newInstance(); sb.append(\u0026#34;aaa\u0026#34;); System.out.println(sb.toString()); //获取String所对应的Class对象 Class\u0026lt;?\u0026gt; c2 = String.class; //获取String类带一个String参数的构造器 Constructor constructor = c2.getConstructor(String.class); //根据构造器创建实例 String str2 = (String) constructor.newInstance(\u0026#34;bbb\u0026#34;); System.out.println(str2); } } //Output: //aaa //bbb # 3.5. 创建数组实例 数组在 Java 里是比较特殊的一种类型,它可以赋值给一个对象引用。Java 中, 通过 Array.newInstance 创建数组的实例 。\n 【示例】利用反射创建数组\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class ReflectArrayDemo { public static void main(String[] args) throws ClassNotFoundException { Class\u0026lt;?\u0026gt; cls = Class.forName(\u0026#34;java.lang.String\u0026#34;); Object array = Array.newInstance(cls, 25); //往数组里添加内容 Array.set(array, 0, \u0026#34;Scala\u0026#34;); Array.set(array, 1, \u0026#34;Java\u0026#34;); Array.set(array, 2, \u0026#34;Groovy\u0026#34;); Array.set(array, 3, \u0026#34;Scala\u0026#34;); Array.set(array, 4, \u0026#34;Clojure\u0026#34;); //获取某一项的内容 System.out.println(Array.get(array, 3)); } } //Output: //Scala 其中的 Array 类为 java.lang.reflect.Array类。我们 Array.newInstance 的原型是:\n 1 2 3 4 public static Object newInstance(Class\u0026lt;?\u0026gt; componentType, int length) throws NegativeArraySizeException { return newArray(componentType, length); } # 3.6. Field Class 对象提供以下方法获取对象的成员( Field):\n getFiled - 根据名称获取公有的(public)类成员。 getDeclaredField - 根据名称获取已声明的类成员。但不能得到其父类的类成员。 getFields - 获取所有公有的(public)类成员。 getDeclaredFields - 获取所有已声明的类成员。 示例如下:\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class ReflectFieldDemo { class FieldSpy\u0026lt;T\u0026gt; { public boolean[][] b = { {false, false}, {true, true} }; public String name = \u0026#34;Alice\u0026#34;; public List\u0026lt;Integer\u0026gt; list; public T val; } public static void main(String[] args) throws NoSuchFieldException { Field f1 = FieldSpy.class.getField(\u0026#34;b\u0026#34;); System.out.format(\u0026#34;Type: %s%n\u0026#34;, f1.getType()); Field f2 = FieldSpy.class.getField(\u0026#34;name\u0026#34;); System.out.format(\u0026#34;Type: %s%n\u0026#34;, f2.getType()); Field f3 = FieldSpy.class.getField(\u0026#34;list\u0026#34;); System.out.format(\u0026#34;Type: %s%n\u0026#34;, f3.getType()); Field f4 = FieldSpy.class.getField(\u0026#34;val\u0026#34;); System.out.format(\u0026#34;Type: %s%n\u0026#34;, f4.getType()); } } //Output: //Type: class [[Z //Type: class java.lang.String //Type: interface java.util.List //Type: class java.lang.Object # 3.7. Method Class 对象提供以下方法获取对象的方法( Method):\n getMethod - 返回类或接口的特定方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。 getDeclaredMethod - 返回类或接口的特定声明方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。 getMethods - 返回类或接口的所有 public 方法,包括其父类的 public 方法。 getDeclaredMethods - 返回类或接口声明的所有方法,包括 public、protected、默认(包)访问和 private 方法,但不包括继承的方法。 获取一个 Method 对象后,可以用 invoke 方法来调用这个方法。\n invoke 方法的原型为:\n 1 2 3 public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException 【示例】\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class ReflectMethodDemo { public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { // 返回所有方法 Method[] methods1 = System.class.getDeclaredMethods(); System.out.println(\u0026#34;System getDeclaredMethods 清单(数量 = \u0026#34; + methods1.length + \u0026#34;):\u0026#34;); for (Method m : methods1) { System.out.println(m); } // 返回所有 public 方法 Method[] methods2 = System.class.getMethods(); System.out.println(\u0026#34;System getMethods 清单(数量 = \u0026#34; + methods2.length + \u0026#34;):\u0026#34;); for (Method m : methods2) { System.out.println(m); } // 利用 Method 的 invoke 方法调用 System.currentTimeMillis() Method method = System.class.getMethod(\u0026#34;currentTimeMillis\u0026#34;); System.out.println(method); System.out.println(method.invoke(null)); } } # 3.8. Constructor Class 对象提供以下方法获取对象的构造方法( Constructor):\n getConstructor - 返回类的特定 public 构造方法。参数为方法参数对应 Class 的对象。 getDeclaredConstructor- 返回类的特定构造方法。参数为方法参数对应 Class 的对象。 getConstructors - 返回类的所有 public 构造方法。 etDeclaredConstructors- 返回类的所有构造方法。 获取一个 Constructor 对象后,可以用 newInstance 方法来创建类实例。\n 【示例】\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class ReflectMethodConstructorDemo { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Constructor\u0026lt;?\u0026gt;[] constructors1 = String.class.getDeclaredConstructors(); System.out.println(\u0026#34;String getDeclaredConstructors 清单(数量 = \u0026#34; + constructors1.length + \u0026#34;):\u0026#34;); for (Constructor c : constructors1) { System.out.println(c); } Constructor\u0026lt;?\u0026gt;[] constructors2 = String.class.getConstructors(); System.out.println(\u0026#34;String getConstructors 清单(数量 = \u0026#34; + constructors2.length + \u0026#34;):\u0026#34;); for (Constructor c : constructors2) { System.out.println(c); } System.out.println(\u0026#34;====================\u0026#34;); Constructor constructor = String.class.getConstructor(String.class); System.out.println(constructor); String str = (String) constructor.newInstance(\u0026#34;bbb\u0026#34;); System.out.println(str); } } # 3.9. 绕开访问限制 有时候,我们需要通过反射访问私有成员、方法。可以使用 Constructor/Field/Method.setAccessible(true)来绕开 Java 语言的访问限制。\n # 4. 动态代理 动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装 RPC 调用、面向切面的编程(AOP)。\n 实现动态代理的方式很多,比如 JDK 自身提供的动态代理,就是主要利用了上面提到的反射机制。还有其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似 ASM、cglib(基于 ASM)、Javassist 等。\n # 4.1. 静态代理 静态代理其实就是指设计模式中的代理模式。\n 代理模式为其他对象提供一种代理以控制对这个对象的访问。\n Subject 定义了 RealSubject 和 Proxy 的公共接口,这样就在任何使用 RealSubject 的地方都可以使用 Proxy 。\n 1 2 3 abstract class Subject { public abstract void Request(); } RealSubject 定义 Proxy 所代表的真实实体。\n 1 2 3 4 5 6 class RealSubject extends Subject { @Override public void Request() { System.out.println(\u0026#34;真实的请求\u0026#34;); } } Proxy 保存一个引用使得代理可以访问实体,并提供一个与 Subject 的接口相同的接口,这样代理就可以用来替代实体。\n 1 2 3 4 5 6 7 8 9 10 11 class Proxy extends Subject { private RealSubject real; @Override public void Request() { if (null == real) { real = new RealSubject(); } real.Request(); } } 说明:\n 静态代理模式固然在访问无法访问的资源,增强现有的接口业务功能方面有很大的优点,但是大量使用这种静态代理,会使我们系统内的类的规模增大,并且不易维护;并且由于 Proxy 和 RealSubject 的功能本质上是相同的,Proxy 只是起到了中介的作用,这种代理在系统中的存在,导致系统结构比较臃肿和松散。\n # 4.2. JDK 动态代理 为了解决静态代理的问题,就有了创建动态代理的想法:\n 在运行状态中,需要代理的地方,根据 Subject 和 RealSubject,动态地创建一个 Proxy,用完之后,就会销毁,这样就可以避免了 Proxy 角色的 class 在系统中冗杂的问题了。\n Java 动态代理基于经典代理模式,引入了一个 InvocationHandler, InvocationHandler 负责统一管理所有的方法调用。\n 动态代理步骤:\n 获取 RealSubject 上的所有接口列表; 确定要生成的代理类的类名,默认为: com.sun.proxy.$ProxyXXXX; 根据需要实现的接口信息,在代码中动态创建 该 Proxy 类的字节码; 将对应的字节码转换为对应的 class 对象; 创建 InvocationHandler 实例 handler,用来处理 Proxy 所有方法调用; Proxy 的 class 对象 以创建的 handler 对象为参数,实例化一个 proxy 对象。 从上面可以看出,JDK 动态代理的实现是基于实现接口的方式,使得 Proxy 和 RealSubject 具有相同的功能。\n 但其实还有一种思路:通过继承。即:让 Proxy 继承 RealSubject,这样二者同样具有相同的功能,Proxy 还可以通过重写 RealSubject 中的方法,来实现多态。CGLIB 就是基于这种思路设计的。\n 在 Java 的动态代理机制中,有两个重要的类(接口),一个是 InvocationHandler 接口、另一个则是 Proxy 类,这一个类和一个接口是实现我们动态代理所必须用到的。\n # InvocationHandler 接口 InvocationHandler 接口定义:\n 1 2 3 4 public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } 每一个动态代理类都必须要实现 InvocationHandler 这个接口,并且每个代理类的实例都关联到了一个 Handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由 InvocationHandler 这个接口的 invoke 方法来进行调用。\n 我们来看看 InvocationHandler 这个接口的唯一一个方法 invoke 方法:\n 1 Object invoke(Object proxy, Method method, Object[] args) throws Throwable 参数说明:\n proxy - 代理的真实对象。 method - 所要调用真实对象的某个方法的 Method 对象 args - 所要调用真实对象某个方法时接受的参数 如果不是很明白,等下通过一个实例会对这几个参数进行更深的讲解。\n # Proxy 类 Proxy 这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance 这个方法:\n 1 2 public static Object newProxyInstance(ClassLoader loader, Class\u0026lt;?\u0026gt;[] interfaces, InvocationHandler h) throws IllegalArgumentException 这个方法的作用就是得到一个动态的代理对象。\n 参数说明:\n loader - 一个 ClassLoader 对象,定义了由哪个 ClassLoader 对象来对生成的代理对象进行加载。 interfaces - 一个 Class\u0026lt;?\u0026gt; 对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了 h - 一个 InvocationHandler 对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个 InvocationHandler 对象上 # JDK 动态代理实例 上面的内容介绍完这两个接口(类)以后,我们来通过一个实例来看看我们的动态代理模式是什么样的:\n 首先我们定义了一个 Subject 类型的接口,为其声明了两个方法:\n 1 2 3 4 5 6 public interface Subject { void hello(String str); String bye(); } 接着,定义了一个类来实现这个接口,这个类就是我们的真实对象,RealSubject 类:\n 1 2 3 4 5 6 7 8 9 10 11 12 13 public class RealSubject implements Subject { @Override public void hello(String str) { System.out.println(\u0026#34;Hello \u0026#34; + str); } @Override public String bye() { System.out.println(\u0026#34;Goodbye\u0026#34;); return \u0026#34;Over\u0026#34;; } } 下一步,我们就要定义一个动态代理类了,前面说个,每一个动态代理类都必须要实现 InvocationHandler 这个接口,因此我们这个动态代理类也不例外:\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class InvocationHandlerDemo implements InvocationHandler { // 这个就是我们要代理的真实对象 private Object subject; // 构造方法,给我们要代理的真实对象赋初值 public InvocationHandlerDemo(Object subject) { this.subject = subject; } @Override public Object invoke(Object object, Method method, Object[] args) throws Throwable { // 在代理真实对象前我们可以添加一些自己的操作 System.out.println(\u0026#34;Before method\u0026#34;); System.out.println(\u0026#34;Call Method: \u0026#34; + method); // 当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用 Object obj = method.invoke(subject, args); // 在代理真实对象后我们也可以添加一些自己的操作 System.out.println(\u0026#34;After method\u0026#34;); System.out.println(); return obj; } } 最后,来看看我们的 Client 类:\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Client { public static void main(String[] args) { // 我们要代理的真实对象 Subject realSubject = new RealSubject(); // 我们要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法的 InvocationHandler handler = new InvocationHandlerDemo(realSubject); /* * 通过Proxy的newProxyInstance方法来创建我们的代理对象,我们来看看其三个参数 * 第一个参数 handler.getClass().getClassLoader() ,我们这里使用handler这个类的ClassLoader对象来加载我们的代理对象 * 第二个参数realSubject.getClass().getInterfaces(),我们这里为代理对象提供的接口是真实对象所实行的接口,表示我要代理的是该真实对象,这样我就能调用这组接口中的方法了 * 第三个参数handler, 我们这里将这个代理对象关联到了上方的 InvocationHandler 这个对象上 */ Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject .getClass().getInterfaces(), handler); System.out.println(subject.getClass().getName()); subject.hello(\u0026#34;World\u0026#34;); String result = subject.bye(); System.out.println(\u0026#34;Result is: \u0026#34; + result); } } 我们先来看看控制台的输出:\n com.sun.proxy.$Proxy0 Before method Call Method: public abstract void io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.hello(java.lang.String) Hello World After method Before method Call Method: public abstract java.lang.String io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.bye() Goodbye After method Result is: Over 我们首先来看看 com.sun.proxy.$Proxy0这东西,我们看到,这个东西是由 System.out.println(subject.getClass().getName());这条语句打印出来的,那么为什么我们返回的这个代理对象的类名是这样的呢?\n 1 2 Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject .getClass().getInterfaces(), handler); 可能我以为返回的这个代理对象会是 Subject 类型的对象,或者是 InvocationHandler 的对象,结果却不是,首先我们解释一下 为什么我们这里可以将其转化为 Subject 类型的对象?\n 原因就是:在 newProxyInstance 这个方法的第二个参数上,我们给这个代理对象提供了一组什么接口,那么我这个代理对象就会实现了这组接口,这个时候我们当然可以将这个代理对象强制类型转化为这组接口中的任意一个,因为这里的接口是 Subject 类型,所以就可以将其转化为 Subject 类型了。\n 同时我们一定要记住,通过 Proxy.newProxyInstance创建的代理对象是在 jvm 运行时动态生成的一个对象,它并不是我们的 InvocationHandler 类型,也不是我们定义的那组接口的类型,而是在运行是动态生成的一个对象,并且命名方式都是这样的形式,以$开头,proxy 为中,最后一个数字表示对象的标号。\n 接着我们来看看这两句\n 1 2 subject.hello(\u0026#34;World\u0026#34;); String result = subject.bye(); 这里是通过代理对象来调用实现的那种接口中的方法,这个时候程序就会跳转到由这个代理对象关联到的 handler 中的 invoke 方法去执行,而我们的这个 handler 对象又接受了一个 RealSubject 类型的参数,表示我要代理的就是这个真实对象,所以此时就会调用 handler 中的 invoke 方法去执行。\n 我们看到,在真正通过代理对象来调用真实对象的方法的时候,我们可以在该方法前后添加自己的一些操作,同时我们看到我们的这个 method 对象是这样的:\n 1 2 public abstract void io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.hello(java.lang.String) public abstract java.lang.String io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.bye() 正好就是我们的 Subject 接口中的两个方法,这也就证明了当我通过代理对象来调用方法的时候,起实际就是委托由其关联到的 handler 对象的 invoke 方法中来调用,并不是自己来真实调用,而是通过代理的方式来调用的。\n # JDK 动态代理小结 代理类与委托类实现同一接口,主要是通过代理类实现 InvocationHandler 并重写 invoke 方法来进行动态代理的,在 invoke 方法中将对方法进行处理。\n JDK 动态代理特点:\n 优点:相对于静态代理模式,不需要硬编码接口,代码复用率高。\n 缺点:强制要求代理类实现 InvocationHandler 接口。\n # 4.3. CGLIB 动态代理 CGLIB 提供了与 JDK 动态代理不同的方案。很多框架,例如 Spring AOP 中,就使用了 CGLIB 动态代理。\n CGLIB 底层,其实是借助了 ASM 这个强大的 Java 字节码框架去进行字节码增强操作。\n CGLIB 动态代理的工作步骤:\n 生成代理类的二进制字节码文件; 加载二进制字节码,生成 Class 对象( 例如使用 Class.forName() 方法 ); 通过反射机制获得实例构造,并创建代理类对象。 CGLIB 动态代理特点:\n 优点:使用字节码增强,比 JDK 动态代理方式性能高。可以在运行时对类或者是接口进行增强操作,且委托类无需实现接口。\n 缺点:不能对 final 类以及 final 方法进行代理。\n ","description":"","id":35,"section":"posts","tags":["java"],"title":"深入理解 Java 反射和动态代理","uri":"https://yichenlove.github.io/posts/java-reflection/"},{"content":"一、架构差异 ARM是RISC(精简指令集)处理器,不同于x86指令集(CISC,复杂指令集)。\n ARM 有不同的CPU 架构 包括:\nARMV8架构、ARMV7架构、ARMV5和ARMV6架构\n Arm32位是ARMV7架构,32位的,对应处理器为Cortex-A15等; ARM64位采用ARMv8架构。64位操作长度,对应处理器有Cortex-A53、Cortex-A57、Cortex-A73、iphones的A7和A8等,苹果手机从iphone 5s开始使用64位的处理器。 ABI - Application Binary Interface 应用程序二进制接口,它描述了应用程序和操作系统之间,一个应用和它的库之间的低接口。\n 一个操作系统是64位的,运行于它之上的可以是64位的程序,也可以是32位的程序。 一个程序 可以包含32位的so 也可以包含64位的so 常用的abi有: armeabi 对应着 ARMV5和ARMV6架构\n armeabi-v7a - 对应着 ARMV7a架构,是32位的寻址长度,里面放置32位系统上运行的so库\n armeabi-v8a - 对应着 ARMV8架构,64位寻址长度,里面放置64位的so\n x86 对应 x86架构(PC机的架构),里面放置x86上运行的so\n x86_64 对应着x86_64架构,里面放置x86_64上运行的so\n 二、几个关系: android 工程中 jnilib目录 -\u0026gt;与编译apk中lib目录-\u0026gt;apk安装后解压的lib之间的关系。\n 1、Android项目中,可以有armeabi、armeabi-v7a、arm64-v8a 三个目录。三个目录中不为空的abi目录以及目录中的so文件都会被拷贝到编译后的apk中。\n[图片上传失败\u0026hellip;(image-a0a902-1557935847007)] 上图中,arm64-v8a为空,所以编译后的apk中不包含arm64-v8a文件夹。\n 2、apk中lib中存在多个armeabi,如armeabi、armeabi-v7a、arm64-v8a。 1、arm64-v8a 为第一选择 2、armeabi-v7a为第二选择 3、armeabi为第三选择 针对64位的系统:\n 如果apk中存在arm64-v8a文件夹,则认为apk未64位的程序,安装时会将arm64-v8a中的so拷贝到/data/app/package/lib/arm64目录中. data/app/package/lib/目录:\n rk3399_mid:/data/app/com.sogou.teemo.testawpwebview-1/lib/arm64 # ls -l total 5808 -rwxr-xr-x 1 system system 5752 1979-11-30 00:00 libeval-lib.so -rwxr-xr-x 1 system system 2686344 1979-11-30 00:00 libeval.so -rwxr-xr-x 1 system system 13928 1979-11-30 00:00 libimageutil.so -rwxr-xr-x 1 system system 243152 1979-11-30 00:00 libmp3lame.so 如果apk中不存在arm64-v8a,但有armeabi-v7a目录,则在apk安装过程中,会将apk中armeabi-v7a中的so 拷贝到/data/app/package/lib/arm/目录,并且判定该程序是32位程序。 [图片上传失败\u0026hellip;(image-875df4-1557935847007)]\n data/app/package/lib/目录:\n rk3399_mid:/data/app/com.sogou.teemo.testawpwebview-1/lib # ls arm rk3399_mid:/data/app/com.sogou.teemo.testawpwebview-1/lib/arm # ls -ls total 320 40 -rwxr-xr-x 1 system system 13680 1979-11-30 00:00 libimageutil.so 280 -rwxr-xr-x 1 system system 136452 1979-11-30 00:00 libmp3lame.so 如果apk中不存在arm64-v8a 和armeabi-v7a ,但有armeabi目录,则在apk安装过程中,会将apk中armeabi中的so 拷贝到/data/app/package/lib/arm/目录,并且判定该程序是32位程序。 [图片上传失败\u0026hellip;(image-6659a9-1557935847007)]\n rk3399_mid:/data/app/com.sogou.teemo.testawpwebview-1 # ls base.apk lib oat rk3399_mid:/data/app/com.sogou.teemo.testawpwebview-1/lib/arm # ls -ls total 39848 144 -rwxr-xr-x 1 system system 68087 1979-11-30 00:00 libchrome_100_percent.so 24 -rwxr-xr-x 1 system system 7659 1979-11-30 00:00 liben-US.so 40 -rwxr-xr-x 1 system system 13676 1979-11-30 00:00 libimageutil.so 312 -rwxr-xr-x 1 system system 152828 1979-11-30 00:00 libmp3lame.so 512 -rwxr-xr-x 1 system system 254977 1979-11-30 00:00 libresources.so 104 -rwxr-xr-x 1 system system 46672 1979-11-30 00:00 libsogoulzma.so 38400 -rwxr-xr-x 1 system system 19636034 1979-11-30 00:00 libsogouwebview.so 288 -rwxr-xr-x 1 system system 140720 1979-11-30 00:00 libsogouwebview_plat_support.so 24 -rwxr-xr-x 1 system system 7512 1979-11-30 00:00 libzh-CN.so 三、如何区分64位qpp 和32位app 从Android 4.4宣布支持64位系统以来,各终端方案厂商逐步推出了各自的64位soc解决方案。Google为了兼容之前32位系统的应用,在64位系统上也实现了对32位应用的支持。\n 方式一: 当你下载安装一个App之后,从Launcher启动该应用,系统会由Zygote分叉出一个子进程来提供App运行的虚拟机和Runtime环境。\n 与32位系统不同的是,在64系统中会同时存在两个Zygote进程——zygote和zygote64,分别对应32位和64位应用。\n 所以,要进行App的32/64位检测,只需要看它的父进程是哪个Zygote即可。\n feifeideMacBook-Pro:Desktop feifei$ adb shell ps | grep zygote root 318 1 2183128 50544 0 0000000000 S zygote64 root 319 1 1620816 70548 0 0000000000 S zygote feifeideMacBook-Pro:Desktop feifei$ adb shell ps | grep com.sogou.teemo.testawpwebview u0_a52 2148 319 1235896 164208 0 0000000000 S com.sogou.teemo.testawpwebview com.sogou.teemo.testawpwebview 的PID为2148,父进程ID为319 (zygote),所以是32位程序。\n 方式二: 通过查看/data/app/package/lib/安装目录来查看:\n 如果lib目录下是arm文件夹,则是32位程序 如果lib目录下是arm64文件夹,则是64位程序 参考文章: 如何查看app是32位app还是64位app\n ","description":"","id":36,"section":"posts","tags":["android","ios","arm"],"title":"App 64位和32位","uri":"https://yichenlove.github.io/posts/app-32-64/"},{"content":"Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化建构工具。它使用一种基于Groovy的特定领域语言来声明项目设置,而不是传统的XML。当前其支持的语言限于Java、Groovy和Scala,计划未来将支持更多的语言。 android build.gradle各个配置参数的含义\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 //声明是Android程序 apply plugin: \u0026#39;com.android.application\u0026#39; android { //程序在编译的时候会检查lint,有任何错误提示会停止build,我们可以关闭这个开关 lintOptions { abortOnError false //即使报错也不会停止打包 checkReleaseBuilds false //打包release版本的时候进行检测 } compileSdkVersion 23 //编译sdk的版本,也就是API Level,例如API-19、API-20、API-21等等。 buildToolsVersion \u0026#39;23.0.2\u0026#39; //build tools的版本,其中包括了打包工具aapt、dx等等。 //这个工具的目录位于你的sdk目录/build-tools/下 aaptOptions.cruncherEnabled = false aaptOptions.useNewCruncher = false //关闭Android Studio的PNG合法性检查的 defaultConfig { applicationId \u0026#34;com.xiaopao.activity\u0026#34; //应用包名 minSdkVersion 15 //最小sdk版本,如果设备小于这个版本或者大于 //maxSdkVersion(一般不用)将无法安装这个应用 targetSdkVersion 22 //目标sdk版本,如果设备等于这个版本那么android平台 //就不进行兼容性检查,运行效率会高一点 versionCode 15 //版本更新了几次,第一版应用是1,以后每更新一次加1 versionName \u0026#39;1.411\u0026#39; //版本信息,这个会显示给用户,就是用户看到的版本号 archivesBaseName = \u0026#34;weshare-$versionName\u0026#34; //指定打包成Jar文件时候的文件名称 ndk { moduleName \u0026#34;xiaopaowifisafe\u0026#34; //设置库(so)文件名称 ldLibs \u0026#34;log\u0026#34;, \u0026#34;z\u0026#34;, \u0026#34;m\u0026#34;, \u0026#34;jnigraphics\u0026#34;, \u0026#34;android\u0026#34; //引入库,比如要用到的__android_log_print abiFilters \u0026#34;armeabi\u0026#34;, \u0026#34;x86\u0026#34;, \u0026#34;armeabi-v7a\u0026#34; //, \u0026#34;x86\u0026#34; 显示指定支持的ABIs cFlags \u0026#34;-std=c++11 -fexceptions\u0026#34; // C++11 stl \u0026#34;gnustl_static\u0026#34; } multiDexEnabled true //当方法数超过65535(方法的索引使用的是一个short值, //而short最大值是65535)的时候允许打包成多个dex文件,动态加载dex。这里面坑很深啊 } //默认的一些文件路径的配置 sourceSets { main { assets.srcDirs = [\u0026#39;assets\u0026#39;] //资源文件 jni.srcDirs \u0026#39;src/main/jni\u0026#39; //jni文件 jniLibs.srcDir \u0026#39;src/main/jniLibs\u0026#39; //jni库 } } //multiDex的一些相关配置,这样配置可以让你的编译速度更快 dexOptions { preDexLibraries = false //让它不要对Lib做preDexing incremental true //开启incremental dexing,优化编译效率,这个功能android studio默认是关闭的。 javaMaxHeapSize \u0026#34;4g\u0026#34; //增加java堆内存大小 } buildTypes { release { //release版本的配置 zipAlignEnabled true //是否支持zip shrinkResources true // 移除无用的resource文件 minifyEnabled true //是否进行混淆 proguardFiles getDefaultProguardFile(\u0026#39;proguard-android.txt\u0026#39;), \u0026#39;proguard-rules.pro\u0026#39; //release的Proguard默认为Module下的proguard-rules.pro文件. debuggable false //是否支持调试 //ndk的一些配置 ndk { // cFlags \u0026#34;-std=c++11 -fexceptions -O3 -D__RELEASE__\u0026#34; // C++11 // platformVersion = \u0026#34;19\u0026#34; moduleName \u0026#34;xiaopaowifisafe\u0026#34; //设置库(so)文件名称 ldLibs \u0026#34;log\u0026#34;, \u0026#34;z\u0026#34;, \u0026#34;m\u0026#34;, \u0026#34;jnigraphics\u0026#34;, \u0026#34;android\u0026#34; //引入库,比如要用到的__android_log_print abiFilters \u0026#34;armeabi\u0026#34;, \u0026#34;x86\u0026#34;, \u0026#34;armeabi-v7a\u0026#34;//, \u0026#34;x86\u0026#34; cFlags \u0026#34;-std=c++11 -fexceptions\u0026#34; // C++11 stl \u0026#34;gnustl_static\u0026#34; } //采用动态替换字符串的方式生成不同的release.apk applicationVariants.all { variant -\u0026gt; variant.outputs.each { output -\u0026gt; def outputFile = output.outputFile if (outputFile != null \u0026amp;\u0026amp; outputFile.name.endsWith(\u0026#39;release.apk\u0026#39;)) { def timeStamp = new Date().format(\u0026#39;yyyyMMddHH\u0026#39;); def fileName = \u0026#34;WeShare-${defaultConfig.versionName}\u0026#34; + \u0026#34;-\u0026#34; + timeStamp + \u0026#34;-lj-\u0026#34; + \u0026#34;.apk\u0026#34;; output.outputFile = file(\u0026#34;${outputFile.parent}/${fileName}\u0026#34;) } } } jniDebuggable false //关闭jni调试 } debug {//debug版本的配置 minifyEnabled false zipAlignEnabled true shrinkResources true // 移除无用的resource文件 proguardFiles getDefaultProguardFile(\u0026#39;proguard-android.txt\u0026#39;), \u0026#39;proguard-rules.pro\u0026#39; debuggable true // jniDebuggable true ndk { cFlags \u0026#34;-std=c++11 -fexceptions -g -D __DEBUG__\u0026#34; // C++11 } jniDebuggable true } } compileOptions { //在这里你可以进行 Java 的版本配置, //以便使用对应版本的一些新特性 } productFlavors { //在这里你可以设置你的产品发布的一些东西, //比如你现在一共软件需要发布到不同渠道, //且不同渠道中的包名不同,那么可以在此进行配置; //甚至可以设置不同的 AndroidManifest.xml 文件。 xiaopao { } googlePlay { } solo { } } productFlavors.all { flavor -\u0026gt; flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] } //所谓ProductFlavors其实就是可定义的产品特性, //配合 manifest merger 使用的时候就可以达成在一次编译 //过程中产生多个具有自己特性配置的版本。 //上面这个配置的作用就是,为每个渠道包产生不同的 UMENG_CHANNEL_VALUE 的值。 } //一些依赖的框架 dependencies { compile \u0026#39;com.jakewharton:butterknife:7.0.1\u0026#39; compile \u0026#39;com.android.support:appcompat-v7:23.4.0\u0026#39; compile \u0026#39;com.android.support:support-v4:23.4.0\u0026#39; compile \u0026#39;com.github.pwittchen:reactivenetwork:0.1.3\u0026#39; compile \u0026#39;de.hdodenhof:circleimageview:2.0.0\u0026#39; compile \u0026#39;com.android.support:design:23.4.0\u0026#39; compile \u0026#39;pl.tajchert:waitingdots:0.2.0\u0026#39; } //声明是要使用谷歌服务框架 apply plugin: \u0026#39;com.google.gms.google-services\u0026#39; //第三方依赖库的本地缓存路径 task showMeCache \u0026lt;\u0026lt; { configurations.compile.each { println it } } //使用maven仓库。android有两个标准的library文件服务器,一个jcenter一个maven。两者毫无关系。 //jcenter有的maven可能没有,反之亦然。 //如果要使用jcenter的话就把mavenCentral()替换成jcenter() repositories { mavenCentral() } ","description":"","id":37,"section":"posts","tags":["android"],"title":"build.gradle配置参数","uri":"https://yichenlove.github.io/posts/android-gardle/"},{"content":" 生命周期感知型组件可执行操作来响应另一个组件(如 Activity 和 Fragment)的生命周期状态的变化。这些组件有助于您编写出更有条理且往往更精简的代码,此类代码更易于维护。\n 一种常见的模式是在 Activity 和 Fragment 的生命周期方法中实现依赖组件的操作。但是,这种模式会导致代码条理性很差而且会扩散错误。通过使用生命周期感知型组件,您可以将依赖组件的代码从生命周期方法移入组件本身中。\n androidx.lifecycle 软件包提供了可用于构建生命周期感知型组件的类和接口 - 这些组件可以根据 Activity 或 Fragment 的当前生命周期状态自动调整其行为。\n 注意:如需将 androidx.lifecycle导入 Android 项目,请参阅 Lifecycle 版本说明中关于声明依赖项的说明。\n 在 Android 框架中定义的大多数应用组件都存在生命周期。生命周期由操作系统或进程中运行的框架代码管理。它们是 Android 工作原理的核心,应用必须遵循它们。如果不这样做,可能会引发内存泄漏甚至应用崩溃。\n 假设我们有一个在屏幕上显示设备位置的 Activity。常见的实现可能如下所示:\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class MyLocationListener { public MyLocationListener(Context context, Callback callback) { // ... } void start() { // connect to system location service } void stop() { // disconnect from system location service } } class MyActivity extends AppCompatActivity { private MyLocationListener myLocationListener; @Override public void onCreate(...) { myLocationListener = new MyLocationListener(this, (location) -\u0026gt; { // update UI }); } @Override public void onStart() { super.onStart(); myLocationListener.start(); // manage other components that need to respond // to the activity lifecycle } @Override public void onStop() { super.onStop(); myLocationListener.stop(); // manage other components that need to respond // to the activity lifecycle } } 虽然此示例看起来没问题,但在真实的应用中,最终会有太多管理界面和其他组件的调用,以响应生命周期的当前状态。管理多个组件会在生命周期方法(如 onStart() 和 onStop())中放置大量的代码,这使得它们难以维护。\n 此外,无法保证组件会在 Activity 或 Fragment 停止之前启动。在我们需要执行长时间运行的操作(如 onStart() 中的某种配置检查)时尤其如此。这可能会导致出现一种竞态条件,在这种条件下,onStop() 方法会在 onStart() 之前结束,这使得组件留存的时间比所需的时间要长。\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class MyActivity extends AppCompatActivity { private MyLocationListener myLocationListener; public void onCreate(...) { myLocationListener = new MyLocationListener(this, location -\u0026gt; { // update UI }); } @Override public void onStart() { super.onStart(); Util.checkUserStatus(result -\u0026gt; { // what if this callback is invoked AFTER activity is stopped? if (result) { myLocationListener.start(); } }); } @Override public void onStop() { super.onStop(); myLocationListener.stop(); } } androidx.lifecycle 软件包提供的类和接口可帮助您以弹性和隔离的方式解决这些问题。\n Lifecycle Lifecycle 是一个类,用于存储有关组件(如 Activity 或 Fragment)的生命周期状态的信息,并允许其他对象观察此状态。\n Lifecycle 使用两种主要枚举跟踪其关联组件的生命周期状态:\n 事件\n从框架和 Lifecycle 类分派的生命周期事件。这些事件映射到 Activity 和 Fragment 中的回调事件。\n状态\n由 Lifecycle 对象跟踪的组件的当前状态。\n图 1. 构成 Android Activity 生命周期的状态和事件\n 您可以将状态看作图中的节点,将事件看作这些节点之间的边。\n 类可以通过实现 DefaultLifecycleObserver并替换相应的方法(如 onCreate 和 onStart 等)来监控组件的生命周期状态。然后,您可以通过调用 Lifecycle 类的 addObserver() 方法并传递观察器的实例来添加观察器,如下例所示:\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class MyObserver implements DefaultLifecycleObserver { @Override public void onResume(LifecycleOwner owner) { connect() } @Override public void onPause(LifecycleOwner owner) { disconnect() } } myLifecycleOwner.getLifecycle().addObserver(new MyObserver()); 在上面的示例中,myLifecycleOwner 对象实现了 LifecycleOwner 接口,我们将在接下来的部分中对该接口进行说明。\n LifecycleOwner LifecycleOwner 是单一方法接口,表示类具有 Lifecycle。它具有一种方法(即 getLifecycle()),该方法必须由类实现。如果您尝试管理整个应用进程的生命周期,请参阅 ProcessLifecycleOwner\n 。\n 此接口从各个类(如 Fragment 和 AppCompatActivity)抽象化 Lifecycle 的所有权,并允许编写与这些类搭配使用的组件。任何自定义应用类均可实现 LifecycleOwner 接口。\n 实现DefaultLifecycleObserver的组件可与实现 LifecycleOwner 的组件完美配合,因为所有者可以提供生命周期,而观察者可以注册以观察生命周期。\n 对于位置跟踪示例,我们可以让 MyLocationListener 类实现 DefaultLifecycleObserver,然后在 onCreate() 方法中使用 Activity 的 Lifecycle 对其进行初始化。这样,MyLocationListener 类便可以自给自足,这意味着,对生命周期状态的变化做出响应的逻辑会在 MyLocationListener(而不是在 Activity)中进行声明。让各个组件存储自己的逻辑可使 Activity 和 Fragment 逻辑更易于管理。\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class MyActivity extends AppCompatActivity { private MyLocationListener myLocationListener; public void onCreate(...) { myLocationListener = new MyLocationListener(this, getLifecycle(), location -\u0026gt; { // update UI }); Util.checkUserStatus(result -\u0026gt; { if (result) { myLocationListener.enable(); } }); } } 一个常见的用例是,如果 Lifecycle 现在未处于良好的状态,则应避免调用某些回调。例如,如果回调在 Activity 状态保存后运行 Fragment 事务,就会触发崩溃,因此我们绝不能调用该回调。\n 为简化此使用场景,Lifecycle 类允许其他对象查询当前状态。\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class MyLocationListener implements DefaultLifecycleObserver { private boolean enabled = false; public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) { ... } @Override public void onStart(LifecycleOwner owner) { if (enabled) { // connect } } public void enable() { enabled = true; if (lifecycle.getCurrentState().isAtLeast(STARTED)) { // connect if not connected } } @Override public void onStop(LifecycleOwner owner) { // disconnect if connected } } 对于此实现,LocationListener 类可以完全感知生命周期。如果我们需要从另一个 Activity 或 Fragment 使用 LocationListener,只需对其进行初始化。所有设置和拆解操作都由类本身管理。\n 如果库提供了需要使用 Android 生命周期的类,我们建议您使用生命周期感知型组件。库客户端可以轻松集成这些组件,而无需在客户端进行手动生命周期管理。\n 实现自定义 LifecycleOwner 支持库 26.1.0 及更高版本中的 Fragment 和 Activity 已实现 LifecycleOwner 接口。\n 如果您有一个自定义类并希望使其成为 LifecycleOwner,您可以使用 LifecycleRegistry 类,但需要将事件转发到该类,如以下代码示例中所示:\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class MyActivity extends Activity implements LifecycleOwner { private LifecycleRegistry lifecycleRegistry; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); lifecycleRegistry = new LifecycleRegistry(this); lifecycleRegistry.markState(Lifecycle.State.CREATED); } @Override public void onStart() { super.onStart(); lifecycleRegistry.markState(Lifecycle.State.STARTED); } @NonNull @Override public Lifecycle getLifecycle() { return lifecycleRegistry; } } 生命周期感知型组件的最佳做法 使界面控制器(Activity 和 Fragment)尽可能保持精简。它们不应试图获取自己的数据,而应使用 ViewModel 执行此操作,并观察 LiveData 对象以将更改体现到视图中。 设法编写数据驱动型界面,对于此类界面,界面控制器的责任是随着数据更改而更新视图,或者将用户操作通知给 ViewModel。 将数据逻辑放在 ViewModel 类中。ViewModel 应充当界面控制器与应用其余部分之间的连接器。不过要注意,ViewModel 不负责获取数据(例如,从网络获取)。但是,ViewModel 应调用相应的组件来获取数据,然后将结果提供给界面控制器。 使用数据绑定在视图与界面控制器之间维持干净的接口。这样一来,您可以使视图更具声明性,并尽量减少需要在 Activity 和 Fragment 中编写的更新代码。如果您更愿意使用 Java 编程语言执行此操作,请使用诸如 Butter Knife 之类的库,以避免样板代码并实现更好的抽象化。 如果界面很复杂,不妨考虑创建 presenter 类来处理界面的修改。这可能是一项艰巨的任务,但这样做可使界面组件更易于测试。 避免在 ViewModel 中引用 View 或 Activity 上下文。如果 ViewModel 存在的时间比 Activity 更长(在配置更改的情况下),Activity 将泄漏并且不会获得垃圾回收器的妥善处置。 使用 Kotlin 协程管理长时间运行的任务和其他可以异步运行的操作。 生命周期感知型组件的用例 生命周期感知型组件可使您在各种情况下更轻松地管理生命周期。下面列举几个例子:\n 在粗粒度和细粒度位置更新之间切换。使用生命周期感知型组件可在位置应用可见时启用细粒度位置更新,并在应用位于后台时切换到粗粒度更新。借助生命周期感知型组件 LiveData,应用可以在用户使用位置发生变化时自动更新界面。 停止和开始视频缓冲。使用生命周期感知型组件可尽快开始视频缓冲,但会推迟播放,直到应用完全启动。此外,应用销毁后,您还可以使用生命周期感知型组件终止缓冲。 开始和停止网络连接。借助生命周期感知型组件,可在应用位于前台时启用网络数据的实时更新(流式传输),并在应用进入后台时自动暂停。 暂停和恢复动画可绘制资源。借助生命周期感知型组件,可在应用位于后台时暂停动画可绘制资源,并在应用位于前台后恢复可绘制资源。 处理 ON_STOP 事件 如果 Lifecycle 属于 AppCompatActivity 或 Fragment,那么调用 AppCompatActivity 或 Fragment 的 onSaveInstanceState()时,Lifecycle 的状态会更改为 CREATED 并且会分派 ON_STOP 事件。\n 通过 onSaveInstanceState()保存 Fragment 或 AppCompatActivity 的状态后,其界面被视为不可变,直到调用 ON_START。如果在保存状态后尝试修改界面,很可能会导致应用的导航状态不一致,因此应用在保存状态后运行 FragmentTransaction 时,FragmentManager 会抛出异常。如需了解详情,请参阅 commit()。\n LiveData 本身可防止出现这种极端情况,方法是在其观察者的关联 Lifecycle 还没有至少处于 STARTED 状态时避免调用其观察者。在后台,它会在决定调用其观察者之前调用 isAtLeast()。\n 遗憾的是,AppCompatActivity 的 onStop() 方法会在 onSaveInstanceState()之后调用,这样就会留下一个缺口,即不允许界面状态发生变化,但 Lifecycle 尚未移至 CREATED 状态。\n 为防止出现这个问题,beta2 及更低版本中的 Lifecycle 类会将状态标记为 CREATED 而不分派事件,这样一来,即使未分派事件(直到系统调用 onStop()),检查当前状态的代码也会获得实际值。\n 遗憾的是,此解决方案有两个主要问题:\n 在 API 23 及更低级别,Android 系统实际上会保存 Activity 的状态,即使它的一部分被另一个 Activity 覆盖。换句话说,Android 系统会调用onSaveInstanceState(),但不一定会调用 onStop()。这样可能会产生很长的时间间隔,在此时间间隔内,观察者仍认为生命周期处于活动状态,虽然无法修改其界面状态。 任何要向 LiveData 类公开类似行为的类都必须实现由 Lifecycle 版本 beta 2 及更低版本提供的解决方案。 注意:为了简化此流程并让其与较低版本实现更好的兼容性,自 1.0.0-rc1 版本起,当调用 onSaveInstanceState()时,会将 Lifecycle 对象标记为 CREATED 并分派 ON_STOP,而不等待调用 onStop() 方法。这不太可能影响您的代码,但您需要注意这一点,因为它与 API 26 及更低级别的 Activity 类中的调用顺序不符。\n","description":"","id":38,"section":"posts","tags":["android"],"title":"Android 生命周期","uri":"https://yichenlove.github.io/posts/android-lifecrcle/"},{"content":"一、cookie 简介 由于HTTP协议的无状态,客户端经常使用cookie来提供跨URL请求的数据持久存储。URL加载系统提供了创建和管理cookie的接口,作为HTTP请求的一部分发送cookies,并在Web服务器的响应时接收cookie。等多cookie信息请前往百度百科cookie\n二、使用cookie 首先,正常业务场景下,cookie最先是由服务器生成好\n然后客服端请求接口,获取到cookie,将cookie存储起来。\n最后,在每次网络请求的时候附带cookie\n但是在iOS网络请求中使用cookie还有1个条件,那就是在iOS中网络请求类NSURLRequest中设置是否要使用cookie\n1 2 3 4 /*! 决定这个请求是否要使用cookie,默认为YES */ @property BOOL HTTPShouldHandleCookies; iOS 中cookie是一个NSHTTPCookie对象,它包含了各种各样的属性(properties)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // cookie 版本 // 版本0:此版本是指由Netscape定义的原始cookie格式的“传统”或“旧式”cookie。遇到的大多数Cookie都是这种格式。 // 版本1:此版本是指RFC 2965(HTTP状态管理机制)中定义的Cookie。 @property (readonly) NSUInteger version; // cookie存储信息的名字,比如:token @property (readonly, copy) NSString *name; // cookie存储的信息,比如:8d2je219jjd0120d12e1212e12(token的值) @property (readonly, copy) NSString *value; // cookie有效期(过期,NSHTTPCookieStorage会自动删除存储的cookie) @property (nullable, readonly, copy) NSDate *expiresDate; // 是否应在会话结束时被丢弃(不管过期日期如何) @property (readonly, getter=isSessionOnly) BOOL sessionOnly; // cookie的域名 @property (readonly, copy) NSString *domain; // 路径 @property (readonly, copy) NSString *path; // 该cookie是否应该仅通过安全通道发送 @property (readonly, getter=isSecure) BOOL secure; // 是否应仅根据RFC 2965发送到HTTP服务器 @property (readonly, getter=isHTTPOnly) BOOL HTTPOnly; // 端口列表 @property (nullable, readonly, copy) NSArray\u0026lt;NSNumber *\u0026gt; *portList; 三、接收cookie 假设当前有这么一个场景,客服端中用户在登录时服务器将当前用户的token等相关信息存在cookie中返回给客户端,客户端在每次请求其他数据时都需要将此cookie信息(保存的用户信息)携带,以便区分当前是哪个用户。\n那么我们要如何接收这个cookie呢,iOS中提供了NSHTTPCookieStorage这个类,来存储服务器给我们发送的cookie,NSURLResponse根据会当前的NSHTTPCookieStorage接受策略自动接收返回的cookie并存储在NSHTTPCookieStorage中,我们不需要做任何操作,在我们发送请求是,我们只需要设置HTTPShouldHandleCookies为YES(默认为YES), NSURLRequest会自动附带cookie的信息发送给服务器。以下是三种接收策略:\n1 2 3 4 5 6 7 8 9 typedef NS_ENUM(NSUInteger, NSHTTPCookieAcceptPolicy) { // 永远接收Cookie,这种情况下,NSHTTPCookieStorage会将接收到的cookie 存储在偏好设置中 NSHTTPCookieAcceptPolicyAlways, // 永远不接受Cookie,这种情况下,NSHTTPCookieStorage不会存储cookie到本地 NSHTTPCookieAcceptPolicyNever, // 只接收指定域名的Cookie NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain }; NSHTPCookieStorage对象是一个单例对象,它管理着所有的cookie,它提供了一些方法来允许客户端设置和移除cookie,和获取当前cookie的设置。\n通过单例获取NSHTTPCookieStorage对象\n1 2 NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; NSHTPCookieStorage 设置、删除、获取\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // cookie的接收策略 @property NSHTTPCookieAcceptPolicy cookieAcceptPolicy // 获取NSHTTPCookieStorage存储的所有cookie @property (nullable , readonly, copy) NSArray\u0026lt;NSHTTPCookie *\u0026gt; *cookies // 设置cookie - (void)setCookie:(NSHTTPCookie *)cookie // 删除cookie - (void)deleteCookie:(NSHTTPCookie *)cookie // 在某个时间点删除cookies - (void)removeCookiesSinceDate:(NSDate *)date // 获取指定URL的cookies - (nullable NSArray\u0026lt;NSHTTPCookie *\u0026gt; *)cookiesForURL:(NSURL *)URL // 获取指定域名指定URL的cookies - (void)setCookies:(NSArray\u0026lt;NSHTTPCookie *\u0026gt; *)cookies forURL:(nullable NSURL *)URL mainDocumentURL:(nullable NSURL *)mainDocumentURL 四、清除cookie 还有一个场景需要清除cookie,那就是在客户端,用户退出登录,我们就需要删除NSHTPCookieStorage中的cookie\n1 2 3 4 NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; for (NSHTTPCookie *cookie in cookies) { [[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie]; } 五、总结 总体来说,使用cookie我们要确认三步信息:\n1.NSURLRequest是否允许使用cookie(HTTPShouldHandleCookies),默认允许。\n2.NSHTTPCookieStorage的接收策略\n3.退出时清除NSHTTPCookieStorage存储的cookie\n","description":"","id":39,"section":"posts","tags":["ios","cookie"],"title":"iOS 网络编程之 NSHTTPCookie/NSHTTPCookieStorage","uri":"https://yichenlove.github.io/posts/ios-cookie/"},{"content":"常常可以看到,很多Android应用都有这么一个功能,就是滑动关闭Activity,比如微信,CSDN移动端,百度贴吧移动端等。我自己也想写个滑动关闭Activity,最近事情没有那么多,我就google了一下,查看了一下实现滑动关闭Activity的实现方法,其中,有个思路,我觉得很不错,因此,在这里,我通过别人的思路,自己实现了一下滑动关闭Activity的方法,在此记录一下。我希望这篇博客,能给人有所启发,也希望大家能对我有所批判,如有更好的方式,请给我留言,不甚感激。\n首先我们先看下实现效果:\n 要写滑动关闭Activity,有几个问题要解决:\n1.透明的显示底层的Activity。\n2.边界检测,滑动视图,以及自动滚动。\n3.阴影绘制。\n一、透明的显示底层Activity,可以使用透明主题,也可以使用其他主题,但是必须修改主题的几个属性,来达到透明的效果,如:\n1 2 3 4 5 \u0026lt;style name=\u0026#34;AppTheme\u0026#34; parent=\u0026#34;@style/Theme.AppCompat.Light\u0026#34;\u0026gt; \u0026lt;item name=\u0026#34;android:windowBackground\u0026#34;\u0026gt;@android:color/transparent\u0026lt;/item\u0026gt; \u0026lt;item name=\u0026#34;android:windowIsTranslucent\u0026#34;\u0026gt;true\u0026lt;/item\u0026gt; \u0026lt;item name=\u0026#34;android:windowAnimationStyle\u0026#34;\u0026gt;@android:style/Animation.Translucent\u0026lt;/item\u0026gt; \u0026lt;/style\u0026gt; 二、谷歌在V4包中,增加ViewDragHelper类,这个类能够对滑动,边界检测,自动滚动等功能,提供了很好的实现。因此在这里我们选择ViewDragHelper来实现滑动功能。\n三、阴影绘制,Paint画笔来绘制。我们选择在dispatchDraw()方法中绘制,为什么不用onDraw(),因为onDraw有时候在ViewGroup中不会执行。我们使用画笔的setShader(),通过写一个LinearGradient(),再绘制一个矩形,得到阴影效果。\n最核心的原理就是在于,替换Window的DecorView下的LinearLayout。下面从代码直观的说明:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 package com.mjc.slidebackdemo.view; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Shader; import android.support.v4.widget.ViewDragHelper; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import java.util.Map; /** * Created by mjc on 2016/2/26. * 功能:当activity布局中嵌入当前布局,该activity可以从边缘滑动关闭 * 实现原理: * 1.获取DecorView的RootView,删除RootView,把RootView添加到当前View * 再把当前View添加到DecorView */ public class SlideBackLayout extends FrameLayout { /**当前Activity的DecorView*/ private ViewGroup mDecorView; /**DecorView下的LinearLayout*/ private View mRootView; /**需要边缘滑动删除的Activity*/ private Activity mActivity; /**Drag助手类*/ private ViewDragHelper mViewDragHelper; /**触发退出当前Activity的宽度*/ private float mSlideWidth; /**屏幕的宽和高*/ private int mScreenWidth; private int mScreenHeight; /**画笔,用来绘制阴影效果*/ private Paint mPaint; /**用于记录当前滑动距离*/ private int curSlideX; public SlideBackLayout(Context context) { super(context); init(context); } private void init(Context context) { //必须是传入Activity mActivity = (Activity) context; //构造ViewDragHelper mViewDragHelper = ViewDragHelper.create(this, new DragCallback()); //设置从左边缘捕捉View mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); //初始化画笔 mPaint = new Paint(); mPaint.setStrokeWidth(2); mPaint.setAntiAlias(true); mPaint.setColor(Color.GRAY); } //绑定方法,在Activity的DecorView下插入当前ViewGroup,原来的RootView放于当前ViewGroup下 public void bind() { mDecorView = (ViewGroup) mActivity.getWindow().getDecorView(); mRootView = mDecorView.getChildAt(0); mDecorView.removeView(mRootView); this.addView(mRootView); mDecorView.addView(this); //计算屏幕宽度 DisplayMetrics dm = new DisplayMetrics(); mActivity.getWindowManager().getDefaultDisplay().getMetrics(dm); mScreenWidth = dm.widthPixels; mScreenHeight = dm.heightPixels; mSlideWidth = dm.widthPixels *0.28f; } @Override public boolean onInterceptHoverEvent(MotionEvent event) { return mViewDragHelper.shouldInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { mViewDragHelper.processTouchEvent(event); return true; } class DragCallback extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(View child, int pointerId) { return false; } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { //当前回调,松开手时触发,比较触发条件和当前的滑动距离 int left = releasedChild.getLeft(); if (left \u0026lt;= mSlideWidth) { //缓慢滑动的方法,小于触发条件,滚回去 mViewDragHelper.settleCapturedViewAt(0, 0); } else { //大于触发条件,滚出去... mViewDragHelper.settleCapturedViewAt(mScreenWidth, 0); } //需要手动调用更新界面的方法 invalidate(); } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { curSlideX = left; //当滑动位置改变时,刷新View,绘制新的阴影位置 invalidate(); //当滚动位置到达屏幕最右边,则关掉Activity if (changedView == mRootView \u0026amp;\u0026amp; left \u0026gt;= mScreenWidth) { mActivity.finish(); } } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { //限制左右拖拽的位移 left = left \u0026gt;= 0 ? left : 0; return left; } @Override public int clampViewPositionVertical(View child, int top, int dy) { //上下不能移动,返回0 return 0; } @Override public void onEdgeDragStarted(int edgeFlags, int pointerId) { //触发边缘时,主动捕捉mRootView mViewDragHelper.captureChildView(mRootView, pointerId); } } @Override public void computeScroll() { //使用settleCapturedViewAt方法是,必须重写computeScroll方法,传入true //持续滚动期间,不断刷新ViewGroup if (mViewDragHelper.continueSettling(true)) invalidate(); } @Override protected void dispatchDraw(Canvas canvas) { //进行阴影绘制,onDraw()方法在ViewGroup中不一定会执行 drawShadow(canvas); super.dispatchDraw(canvas); } private void drawShadow(Canvas canvas) { canvas.save(); //构造一个渐变 Shader mShader = new LinearGradient(curSlideX - 40, 0, curSlideX, 0, new int[]{Color.parseColor(\u0026#34;#1edddddd\u0026#34;), Color.parseColor(\u0026#34;#6e666666\u0026#34;), Color.parseColor(\u0026#34;#9e666666\u0026#34;)}, null, Shader.TileMode.REPEAT); //设置着色器 mPaint.setShader(mShader); //绘制时,注意向左边偏移 RectF rectF = new RectF(curSlideX - 40, 0, curSlideX, mScreenHeight); canvas.drawRect(rectF, mPaint); canvas.restore(); } } 我在代码中,进行详细的注释。总体来说,不难理解。我们在使用的时候,在布局文件中,一定要在根布局设置背景颜色,否则整个布局将会是透明的。下面是使用方法:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.mjc.slidebackdemo; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; import com.mjc.slidebackdemo.view.SlideBackLayout; /** * 从左侧边缘向右滑动可以关闭当前页面 */ public class SecondActivity extends AppCompatActivity { private TextView tv; private SlideBackLayout mSlideBackLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mSlideBackLayout = new SlideBackLayout(this); mSlideBackLayout.bind(); tv = (TextView) findViewById(R.id.tv); } } 哪个Activity需要滑动关闭的功能,只需要实例化一个SlideBackLayout对象,并调用bind()方法。这样,我们就实现了滑动关闭的效果了。\n","description":"","id":40,"section":"posts","tags":["android"],"title":"滑动关闭Activity","uri":"https://yichenlove.github.io/posts/android-activity2/"},{"content":" ❝**「前言」**:这里的标题看起来是 \u0026ldquo;高级用法\u0026rdquo;,不少同学可能就表示被劝退了。其实 Typescript 作为一门 强类型 编程语言,最具特色的就是他的类型表达能力,这是很多完备的后端语言都难以媲美的 说的很对,但PHP是最好的语言,所以如果你搞懂了他的类型系统,对将来的日常开发一定是大有裨益的,但过于灵活的类型系统也注定了 Typescript 无法成为一门纯粹的静态语言,不过每一行代码都有代码提示他不香嘛? ❞\n 基础准备 阅读本文需要具备的基础知识。\n预备知识 本文的定位为理解高级用法,故不会涉及过多基础知识相关的讲解,需要读者自己去完善这方面的知识储备。\n此文档的内容默认要求读者已经具备以下知识: 有 Javascript 或其他语言编程经验。 有 Typescript 实际使用经验,最好在正经项目中完整地使用过。 了解 Typescript 基础语法以及常见关键字地作用。 对 Typescript 的 类型系统 架构有一个最基本的了解。 相关资源推荐 Typescript 官网[1] TypeScript Deep Dive[2] TypeScript GitHub地址[3] 背景 初用 Typescript 开发的同学一定有这样的困扰:\n 代码代码提示并不智能,似乎只能显式的定义类型,才能有代码提示,无法理解这样的编程语言居然有这么多人趋之若鹜。 各种各样的类型报错苦不堪言,本以为听信网上说 Typescript 可以提高代码可维护性,结果却发现徒增了不少开发负担。 显式地定义所有的类型似乎能应付大部分常见,但遇到有些复杂的情况却发现无能为力,只能含恨写下若干的 as any 默默等待代码 review 时的公开处刑。 项目急时间紧却发现 Typescript 成了首要难题,思索片刻决定投靠的 Anyscript,快速开发业务逻辑,待到春暖花开时再回来补充类型。双倍的工作量,双倍的快乐只有自己才懂。 为了避免以上悲剧的发生或者重演,我们只有在对它有更加深刻的理解之后,才能在开发时游刃有余、在撸码时纵横捭阖。\nTypescript 类型系统简述 ❝**「思考题」**:有人说 Typescript = Type + Javascript,那么抛开 Javascript 不谈,这里的 Type 是一门完备的编程语言吗? ❞\n Typescript 的类型是支持定义 \u0026ldquo;函数定义\u0026rdquo; 的 有过编程经验的同学都知道,函数是一门编程语言中最基础的功能之一,函数是过程化、面向对象、函数式编程中程序封装的基本单元,其重要程度不言而喻。\n函数可以帮助我们做很多事,比如 :\n 函数可以把程序封装成一个个功能,并形成函数内部的变量作用域,通过静态变量保存函数状态,通过返回值返回结果。 函数可以帮助我们实现过程的复用,如果一段逻辑可以被使用多次,就封装成函数,被其它过程多次调用。 函数也可以帮我们更好地组织代码结构,帮助我们更好地维护代码。 那么言归正传,如何在 Typescript 类型系统中定义函数呢? Typescript 中类型系统中的的函数被称作 泛型操作符,其定义的简单的方式就是使用 type 关键字:\n1 2 // 这里我们就定义了一个最简单的泛型操作符 type foo\\\u0026lt;T\\\u0026gt; \\= T; 这里的代码如何理解呢,其实这里我把代码转换成大家最熟悉的 Javascript 代码其实就不难理解了:\n1 2 3 4 // 把上面的类型代码转换成 \\`JavaScript\\` 代码 function foo\\(T\\) \\{ return T \\} 那么看到这里有同学心里要犯嘀咕了,心想你这不是忽悠我嘛?这不就是 Typescript 中定义类型的方式嘛?这玩意儿我可太熟了,这玩意儿不就和 interface 一样的嘛,我还知道 Type 关键字和 interface 关键字有啥细微的区别呢!\n嗯,同学你说的太对了,不过你不要着急,接着听我说,其实类型系统中的函数还支持对入参的约束。\n1 2 // 这里我们就对入参 T 进行了类型约束 type foo\\\u0026lt;T extends string\\\u0026gt; \\= T; 那么把这里的代码转换成我们常见的 Typescript 是什么样子的呢?\n1 2 3 function foo\\(T: string\\) \\{ return T \\} 当然啦我们也可以给它设置默认值:\n1 2 // 这里我们就对入参 T 增加了默认值 type foo\\\u0026lt;T extends string \\= \u0026#39;hello world\u0026#39;\\\u0026gt; \\= T; 那么这里的代码转换成我们常见的 Typescript 就是这样的:\n1 2 3 function foo\\(T: string \\= \u0026#39;hello world\u0026#39;\\) \\{ return T \\} 看到这里肯定有同学迫不及待地想要提问了:「那能不能像 JS 里的函数一样支持剩余参数呢?」\n很遗憾,目前暂时是不支持的,但是在我们日常开发中一定是有这样的需求存在的。那就真的没有办法了嘛?其实也不一定,我们可以通过一些骚操作来模拟这种场景,当然这个是后话了,这里就不作拓展了。\nTypescript 的类型是支持 \u0026ldquo;条件判断\u0026rdquo; 的 ❝人生总会面临很多选择,编程也是一样。 ——我瞎编的 ❞\n 条件判断也是编程语言中最基础的功能之一,也是我们日常撸码过程成最常用的功能,无论是 if else 还是 三元运算符,相信大家都有使用过。\n那么在 Typescript 类型系统中的类型判断要怎么实现呢? 其实这在 Typescript 官方文档被称为 条件类型(Conditional Types),定义的方法也非常简单,就是使用 extends 关键字。\n1 T extends U \\? X : Y; 这里相信聪明的你一眼就看出来了,这不就是 三元运算符 嘛!是的,而且这和三元运算符的也发也非常像,如果 T extends U 为 true 那么 返回 X ,否则返回 Y。\n结合之前刚刚讲过的 \u0026ldquo;函数\u0026rdquo;,我们就可以简单的拓展一下:\n1 2 3 4 5 6 7 type num \\= 1; type str \\= \u0026#39;hello world\u0026#39;; type IsNumber\\\u0026lt;N\\\u0026gt; \\= N extends number \\? \u0026#39;yes, is a number\u0026#39; : \u0026#39;no, not a number\u0026#39;; type result1 \\= IsNumber\\\u0026lt;num\\\u0026gt;; // \u0026#34;yes, is a number\u0026#34; type result2 \\= IsNumber\\\u0026lt;str\\\u0026gt;; // \u0026#34;no, not a number\u0026#34; 这里我们就实现了一个简单的带判断逻辑的函数。\nTypescript 的类型是支持 \u0026ldquo;数据结构\u0026rdquo; 的 模拟真实数组 看到这里肯定有同学就笑了,这还不简单,就举例来说,Typescript 中最常见数据类型就是 数组(Array) 或者 元组(tuple)。\n同学你说的很对,「那你知道如何对 元组类型 作 push、pop、shift、unshift 这些行为操作吗?」\n其实这些操作都是可以被实现的:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 这里定义一个工具类型,简化代码 type ReplaceValByOwnKey\\\u0026lt;T, S extends any\\\u0026gt; \\= \\{ \\[P in keyof T\\]: S\\[P\\] \\}; // shift action type ShiftAction\\\u0026lt;T extends any\\[\\]\\\u0026gt; \\= \\(\\(...args: T\\) \\=\u0026gt; any\\) extends \\(\\(arg1: any, ...rest: infer R\\) \\=\u0026gt; any\\) \\? R : never; // unshift action type UnshiftAction\\\u0026lt;T extends any\\[\\], A\\\u0026gt; \\= \\(\\(args1: A, ...rest: T\\) \\=\u0026gt; any\\) extends \\(\\(...args: infer R\\) \\=\u0026gt; any\\) \\? R : never; // pop action type PopAction\\\u0026lt;T extends any\\[\\]\\\u0026gt; \\= ReplaceValByOwnKey\\\u0026lt;ShiftAction\\\u0026lt;T\\\u0026gt;, T\\\u0026gt;; // push action type PushAction\\\u0026lt;T extends any\\[\\], E\\\u0026gt; \\= ReplaceValByOwnKey\\\u0026lt;UnshiftAction\\\u0026lt;T, any\\\u0026gt;, T \\\u0026amp; \\{ \\[k: string\\]: E \\}\\\u0026gt;; // test ... type tuple \\= \\[\u0026#39;vue\u0026#39;, \u0026#39;react\u0026#39;, \u0026#39;angular\u0026#39;\\]; type resultWithShiftAction \\= ShiftAction\\\u0026lt;tuple\\\u0026gt;; // \\[\u0026#34;react\u0026#34;, \u0026#34;angular\u0026#34;\\] type resultWithUnshiftAction \\= UnshiftAction\\\u0026lt;tuple, \u0026#39;jquery\u0026#39;\\\u0026gt;; // \\[\u0026#34;jquery\u0026#34;, \u0026#34;vue\u0026#34;, \u0026#34;react\u0026#34;, \u0026#34;angular\u0026#34;\\] type resultWithPopAction \\= PopAction\\\u0026lt;tuple\\\u0026gt;; // \\[\u0026#34;vue\u0026#34;, \u0026#34;react\u0026#34;\\] type resultWithPushAction \\= PushAction\\\u0026lt;tuple, \u0026#39;jquery\u0026#39;\\\u0026gt;; // \\[\u0026#34;vue\u0026#34;, \u0026#34;react\u0026#34;, \u0026#34;angular\u0026#34;, \u0026#34;jquery\u0026#34;\\] ❝**「注意」**:这里的代码仅用于测试,操作某些复杂类型可能会报错,需要做进一步兼容处理,这里简化了相关代码,请勿用于生产环境! ❞\n 相信读到这里,大部分同学应该可以已经可以感受到 Typescript 类型系统的强大之处了,其实这里还是继续完善,为元组增加 concat 、map 等数组的常用的功能,这里不作详细探讨,留给同学们自己课后尝试吧。\n但是其实上面提到的 \u0026ldquo;数据类型\u0026rdquo; 并不是我这里想讲解的 \u0026ldquo;数据类型\u0026rdquo;,上述的数据类型本质上还是服务于代码逻辑的数据类型,其实并不是服务于 类型系统 本身的数据类型。\n上面这句话的怎么理解呢?\n不管是 数组 还是 元组,在广义的理解中,其实都是用来对 「数据」 作 「批量操作」,同理,服务于 类型系统 本身的数据结构,应该也可以对 「类型」 作 「批量操作」。\n那么如何对 「类型」 作 「批量操作」 呢?或者说服务于 类型系统 中的 「数组」 是什么呢?\n下面就引出了本小节真正的 \u0026ldquo;数组\u0026rdquo;:联合类型(Union Types)\n说起 联合类型(Union Types) ,相信使用过 Typescript 同学的一定对它又爱又恨:\n 定义函数入参的时候,当同一个位置的参数允许传入多种参数类型,使用 联合类型(Union Types) 会非常的方便,但想智能地推导出返回值的类型地时候却又犯了难。 当函数入参个数不确定地时候,又不愿意写出 (...args: any[]) =\u0026gt; void 这种毫无卵用的参数类型定义。 使用 联合类型(Union Types) 时,虽然有 类型守卫(Type guard),但是某些场景下依然不够好用。 其实当你对它有足够的了解时,你就会发现 联合类型(Union Types) 比 交叉类型(Intersection Types) 不知道高到哪里去了,我和它谈笑风生。\n类型系统中的 \u0026ldquo;数组\u0026rdquo; 下面就让我们更加深入地了解一下 联合类型(Union Types): 如何遍历 联合类型(Union Types) 呢? 既然目标是 「批量操作类型」,自然少不了类型的 「遍历」,和大多数编程语言方法一样,在 Typescript 类型系统中也是 in 关键字来遍历。\n1 2 3 type key \\= \u0026#39;vue\u0026#39; | \u0026#39;react\u0026#39;; type MappedType \\= \\{ \\[k in key\\]: string \\} // \\{ vue: string; react: string; \\} 你看,通过 in 关键字,我们可以很容易地遍历 联合类型(Union Types),并对类型作一些变换操作。\n但有时候并不是所有所有 联合类型(Union Types) 都是我们显式地定义出来的。\n我们想动态地推导出 联合类型(Union Types) 类型有哪些方法呢? 可以使用 keyof 关键字动态地取出某个键值对类型的 key\n1 2 3 4 5 6 7 interface Student \\{ name: string; age: number; \\} type studentKey \\= keyof Student; // \u0026#34;name\u0026#34; | \u0026#34;age\u0026#34; 同样的我们也可以通过一些方法取出 元组类型 子类型\n1 2 3 4 type framework \\= \\[\u0026#39;vue\u0026#39;, \u0026#39;react\u0026#39;, \u0026#39;angular\u0026#39;\\]; type frameworkVal1 \\= framework\\[number\\]; // \u0026#34;vue\u0026#34; | \u0026#34;react\u0026#34; | \u0026#34;angular\u0026#34; type frameworkVal2 \\= framework\\[any\\]; // \u0026#34;vue\u0026#34; | \u0026#34;react\u0026#34; | \u0026#34;angular\u0026#34; 实战应用 看到这里,有的同学可能要问了,你既然说 联合类型(Union Types) 可以批量操作类型,「那我想把某一组类型批量映射成另一种类型,该怎么操作呢」?\n方法其实有很多,这里提供一种思路,抛砖引玉一下,别的方法就留给同学们自行研究吧。\n其实分析一下上面那个需求,不难看出,这个需求其实和数组的 map 方法有点相似\n那么如何实现一个操作 联合类型(Union Types) 的 map 函数呢? 1 2 // 这里的 placeholder 可以键入任何你所希望映射成为的类型 type UnionTypesMap\\\u0026lt;T\\\u0026gt; \\= T extends any \\? \u0026#39;placeholder\u0026#39; : never; 其实这里聪明的同学已经看出来,我们只是利用了 条件类型(Conditional Types),使其的判断条件总是为 true,那么它就总是会返回左边的类型,我们就可以拿到 泛型操作符 的入参并自定义我们的操作。\n让我们趁热打铁,再举个具体的栗子:把 「联合类型(Union Types)」 的每一项映射成某个函数的 「返回值」。\n1 2 3 4 5 6 type UnionTypesMap2Func\\\u0026lt;T\\\u0026gt; \\= T extends any \\? \\(\\) \\=\u0026gt; T : never; type myUnionTypes \\= \u0026#34;vue\u0026#34; | \u0026#34;react\u0026#34; | \u0026#34;angular\u0026#34;; type myUnionTypes2FuncResult \\= UnionTypesMap2Func\\\u0026lt;myUnionTypes\\\u0026gt;; // \\(\\(\\) =\u0026gt; \u0026#34;vue\u0026#34;\\) | \\(\\(\\) =\u0026gt; \u0026#34;react\u0026#34;\\) | \\(\\(\\) =\u0026gt; \u0026#34;angular\u0026#34;\\) 相信有了上述内容的学习,我们已经对 联合类型(Union Types) 有了一个相对全面的了解,后续在此基础之上在作一些高级的拓展,也如砍瓜切菜一般简单了。\n其他数据类型 当然除了数组,还存在其他的数据类型,例如可以用 type 或 interface 模拟 Javascript 中的 「字面量对象」,其特征之一就是可以使用 myType['propKey'] 这样的方式取出子类型。这里抛砖引玉一下,有兴趣的同学可以自行研究。\nTypescript 的类型是支持 \u0026ldquo;作用域\u0026rdquo; 的 全局作用域 就像常见的编程语言一样,在 Typescript 的类型系统中,也是支持 「全局作用域」 的。换句话说,你可以在没有 「导入」 的前提下,在 「任意文件任意位置」 直接获取到并且使用它。\n通常使用 declare 关键字来修饰,例如我们常见的 图片资源 的类型定义:\n1 2 3 declare module \u0026#39;\\*.png\u0026#39;; declare module \u0026#39;\\*.svg\u0026#39;; declare module \u0026#39;\\*.jpg\u0026#39;; 当然我们也可以在 「全局作用域」 内声明一个类型:\n1 2 3 4 5 declare type str \\= string; declare interface Foo \\{ propA: string; propB: number; \\} 需要注意的是,如何你的模块使用了 export 关键字导出了内容,上述的声明方式可能会失效,如果你依然想要将类型声明到全局,那么你就需要显式地声明到全局:\n1 2 3 declare global \\{ const ModuleGlobalFoo: string; \\} 模块作用域 就像 nodejs 中的模块一样,每个文件都是一个模块,每个模块都是独立的模块作用域。这里模块作用域触发的条件之一就是使用 export 关键字导出内容。\n每一个模块中定义的内容是无法直接在其他模块中直接获取到的,如果有需要的话,可以使用 import 关键字按需导入。\n泛型操作符作用域\u0026amp;函数作用域 泛型操作符是存在作用域的,还记得这一章的第一节为了方便大家理解,我把泛型操作符类比为函数吗?既然可以类比为函数,那么函数所具备的性质,泛型操作符自然也可以具备,所以存在泛型操作符作用域自然也就很好理解了。\n这里定义的两个同名的 T 并不会相互影响:\n1 2 type TypeOperator\\\u0026lt;T\\\u0026gt; \\= T; type TypeOperator2\\\u0026lt;T\\\u0026gt; \\= T; 上述是关于泛型操作符作用域的描述,下面我们聊一聊真正的函数作用域:\n「类型也可以支持闭包」:\n1 2 3 4 5 6 7 8 9 10 11 12 function Foo\\\u0026lt;T\\\u0026gt; \\(\\) \\{ return function\\(param: T\\) \\{ return param; \\} \\} const myFooStr \\= Foo\\\u0026lt;string\\\u0026gt;\\(\\); // const myFooStr: \\(param: string\\) =\u0026gt; string // 这里触发了闭包,类型依然可以被保留 const myFooNum \\= Foo\\\u0026lt;number\\\u0026gt;\\(\\); // const myFooNum: \\(param: number\\) =\u0026gt; number // 这里触发了闭包,类型也会保持相互独立,互不干涉 Typescript 的类型是支持 \u0026ldquo;递归\u0026rdquo; 的 Typescript 中的类型也是可以支持递归的,递归相关的问题比较抽象,这里还是举例来讲解,同时为了方便大家的理解,我也会像第一节一样,把类型递归的逻辑用 Javascript 语法描述一遍。\n首先来让我们举个栗子:\n假如现在需要把一个任意长度的元组类型中的子类型依次取出,并用 \u0026amp; 拼接并返回。 这里解决的方法其实非常非常多,解决的思路也非常非常多,由于这一小节讲的是 「递归」,所以我们使用递归的方式来解决。废话不罗嗦,先上代码:\n1 2 3 4 5 6 7 8 9 10 // shift action type ShiftAction\\\u0026lt;T extends any\\[\\]\\\u0026gt; \\= \\(\\(...args: T\\) \\=\u0026gt; any\\) extends \\(\\(arg1: any, ...rest: infer R\\) \\=\u0026gt; any\\) \\? R : never; type combineTupleTypeWithTecursion\\\u0026lt;T extends any\\[\\], E \\= \\{\\}\\\u0026gt; \\= \\{ 1: E, 0: combineTupleTypeWithTecursion\\\u0026lt;ShiftAction\\\u0026lt;T\\\u0026gt;, E \\\u0026amp; T\\[0\\]\\\u0026gt; \\}\\[T extends \\[\\] \\? 1 : 0\\] type test \\= \\[\\{ a: string \\}, \\{ b: number \\}\\]; type testResult \\= combineTupleTypeWithTecursion\\\u0026lt;test\\\u0026gt;; // \\{ a: string; \\} \\\u0026amp; \\{ b: number; \\} 看到上面的代码是不是一脸懵逼?没关系,接下来我们用普通的 Typescript 代码来 \u0026ldquo;翻译\u0026rdquo; 一下上述的代码。\n1 2 3 4 5 6 7 function combineTupleTypeWithTecursion\\(T: object\\[\\], E: object \\= \\{\\}\\): object \\{ return T.length \\? combineTupleTypeWithTecursion\\(T.slice\\(1\\), \\{ ...E, ...T\\[0\\] \\}\\) : E \\} const testData \\= \\[\\{ a: \u0026#39;hello world\u0026#39; \\}, \\{ b: 100 \\}\\]; // 此时函数的返回值为 \\{ a: \u0026#39;hello world\u0026#39;, b: 100 \\} combineTupleTypeWithTecursion\\(testData\\); 看到这儿,相信聪明的同学一下子就懂了,原来类型的递归与普通函数的递归本质上是一样的。如果触发结束条件,就直接返回,否则就一直地递归调用下去,所传递的第二个参数用来保存上一次递归的计算结果。\n当然熟悉递归的同学都知道,常见的编程语言中,递归行为非常消耗计算机资源的,一旦超出了最大限制那么程序就会崩溃。同理类型中的递归也是一样的,如果递归地过深,类型系统一样会崩溃,所以这里的代码大家理解就好,尽量不要在生产环境使用哈。\n小结 还记得一开始提出的思考题吗?其实通过上述的学习,我们完全可以自信地说出,Typescript 的 Type 本身也是一套完备的编程语言,甚至可以说是完备的图灵语言。因此类型本身也是可以用来编程的,你完全可以用它来编写一些有趣的东西,更别说是搞定日常开发中遇到的简单的业务场景了。\n\u0026ldquo;高级用法\u0026rdquo; 的使用场景与价值 哪些用法可以被称为 \u0026ldquo;高级用法\u0026rdquo; 其实所谓 \u0026ldquo;高级用法\u0026rdquo;,不过是用来解决某些特定的场景而产生的特定的约定俗称的写法或者语法糖。那高级用法重要吗?重要,也不重要。怎理解呢,根据编程中的 \u0026ldquo;二八原则\u0026rdquo;,20%的知识储备已经可以解决80%的需求问题,但是这剩余的20%,就是入门与熟练的分水岭。\n其实只要当我们仔细翻阅一遍官方提供的 handbook[4],就已经可以应付日常开发了。但是就像本文一开头说的那样,你是否觉得:\n Typescript 在某些场景下用起来很费劲,远不及 Javascript 灵活度的十分之一。 你是否为自己使用 Javascript 中了某些 「骚操作」 用极简短的代码解决了某个复杂的代码而沾沾自喜,但却为不正确的 「返回类型」 挠秃了头。 你是否明知用了若干 as xxx 会让你的代码看起来很挫,但却无能为力,含恨而终。 同学,当你使用某种办法解决了上述的这些问题,那么这种用法就可以被称作 \u0026ldquo;高级用法\u0026rdquo;。\n举例说明 \u0026ldquo;高级用法\u0026rdquo; 的使用场景 举个栗子:在 Redux 中有一个叫作 combineReducers 的函数,因为某些场景,我们需要增加一个 combineReducersParamFactory 的函数,该函数支持传入多个函数,传入函数的返回值为作为combineReducers 的入参,我们需要整合多个入参数函数的返回值,并生成最终的对象供 combineReducers 函数使用。\n思考一下逻辑,发现其实并不复杂,用 Javascript 可以很容易地实现出来:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 /\\*\\* \\* 合并多个参数的返回数值并返回 \\* \\@param \\{ Function\\[\\] \\} reducerCreators \\* \\@returns \\{ Object \\} \\*/ function combineReducersParamFactory\\(...reducerCreators\\) \\{ return reducerCreators.reduce\\(\\(acc, creator\\) \\=\u0026gt; \\(\\{ ...acc, ...creator\\(\\) \\}\\), \\{\\}\\) \\} // test ... function todosReducer\\(state \\= \\[\\], action\\) \\{ switch \\(action.type\\) \\{ case \u0026#39;ADD\\_TODO\u0026#39;: return state.concat\\(\\[action.text\\]\\) default: return state \\} \\} function counterReducer\\(state \\= 0, action\\) \\{ switch \\(action.type\\) \\{ case \u0026#39;INCREMENT\u0026#39;: return state + 1 case \u0026#39;DECREMENT\u0026#39;: return state \\- 1 default: return state \\} \\} const ret \\= combineReducersParamFactory\\( \\(\\) \\=\u0026gt; \\(\\{ todosReducer \\}\\), \\(\\) \\=\u0026gt; \\(\\{ counterReducer \\}\\) \\); // \\{ todosReducer: \\[Function: todosReducer\\], counterReducer: \\[Function: counterReducer\\] \\} 但如果用需要配备对应的类型,应该如何编写呢?\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 type Combine\\\u0026lt;T\\\u0026gt; \\= \\(T extends any \\? \\(args: T\\) \\=\u0026gt; any : never\\) extends \\(args: infer A\\) \\=\u0026gt; any \\? A : never; /\\*\\* \\* 合并多个参数的返回数值并返回 \\* \\@param \\{ Function\\[\\] \\} reducerCreators \\* \\@returns \\{ Object \\} \\*/ function combineReducersParamFactory\\\u0026lt;T extends \\(\\(...args\\) \\=\u0026gt; object\\)\\[\\]\\\u0026gt;\\(...reducerCreators: T\\): Combine\\\u0026lt;ReturnType\\\u0026lt;T\\[number\\]\\\u0026gt;\u0026gt; \\{ return reducerCreators.reduce\\\u0026lt;any\\\u0026gt;\\(\\(acc, creator\\) \\=\u0026gt; \\(\\{ ...acc, ...creator\\(\\) \\}\\), \\{\\}\\); \\} // test ... function todosReducer\\(state: object\\[\\], action: \\{ \\[x: string\\]: string\\}\\) \\{ switch \\(action.type\\) \\{ case \u0026#39;ADD\\_TODO\u0026#39;: return state.concat\\(\\[action.text\\]\\) default: return state \\} \\} function counterReducer\\(state: number, action: \\{ \\[x: string\\]: string\\}\\) \\{ switch \\(action.type\\) \\{ case \u0026#39;INCREMENT\u0026#39;: return state + 1 case \u0026#39;DECREMENT\u0026#39;: return state \\- 1 default: return state \\} \\} // 这里不需要显示传入类型,这里就可以得到正确的代码提示 const ret \\= combineReducersParamFactory\\( \\(\\) \\=\u0026gt; \\(\\{ todosReducer \\}\\), \\(\\) \\=\u0026gt; \\(\\{ counterReducer \\}\\) \\); // \\{ todosReducer: \\[Function: todosReducer\\], counterReducer: \\[Function: counterReducer\\] \\} 你看,类型经过精心编排之后,就是可以让调用者不增加任何负担的前提下,享受到代码提示的快乐。\n小结 经过这一章节的学习,我们可以明确了解到,经过我们精心编排的类型,可以变得非常的智能,可以让调用者几乎零成本地享受到代码提示的快乐。或许在编排类型时所耗费的时间成本比较大,但是一旦我们编排完成,就可以极大地减少调用者的脑力负担,让调用者享受到编程的快乐。\n类型推导与泛型操作符 流动的类型(类型编写思路) 熟悉 「函数式编程」 的同学一定对 「数据流动」 的概念有较为深刻的理解。当你在 \u0026ldquo;上游\u0026rdquo; 改变了一个值之后,\u0026ldquo;下游\u0026rdquo; 相关的会跟着自动更新。有 「响应式编程」 经验的同学这是时候应该迫不及待地想举手了,同学把手放下,这里我们并不想深入地讨论 「流式编程思想」,之所以引出这些概念,是想类比出本小节的重点: 「流动的类型」。\n是的,编写类型系统的思路是可以借鉴 「函数式编程」 的思想的。因此某一个类型发生变化时,其他相关的类型也会自动更新,并且当代码的臃肿到不可维护的时候,你会得到一个友好的提示,整个类型系统就好像一个被精心设计过的约束系统。\nTypescript 代码哲学 聊完了类型系统的编写思路,咱们再来聊一聊代码哲学。其实之所以现在 Typescript 越来越火,撇开哪些聊烂了的优势不谈,其实最大的优势在于强大的类型表现能力,以及编辑器(VSCode)完备的代码提示能力。\n那么在这些优势的基础上,我个人拓展了一些编码哲学(习惯),这里见仁见智,大佬轻喷~:\n 减少不必要的显式类型定义,尽可能多地使用类型推导,让类型的流动像呼吸一样自然。 尽可能少地使用 any 或 as any,注意这里并不是说不能用,而是你判断出目前情况下使用 any 是最优解。 如果确定要使用 any 作为类型,优先考虑一下是否可以使用 unknown 类型替代,毕竟 any 会破坏类型的流动。 尽可能少地使用 as xxx,如果大量使用这种方式纠正类型,那么大概率你对 「类型流动」 理解的还不够透彻。 常见类型推导实现逻辑梳理与实践入门 类型的传递(流动) 前面我们说到,类型是具备流动性的,结合 「响应式编程」 的概念其实很容易理解。这一小节我们将列举几个常见的例子,来和大家具体讲解一下。\n有编程经验的同学都知道,数据是可以被传递的,同理,类型也可以。\n你可用 type 创建一个类型指针,指向对应的类型,那么就可以实现类型的传递,当然你也可以理解为指定起一个别名,或者说是拷贝,这里见仁见智,但是通过上述方法可以实现类型的传递,这是显而易见的。\n1 2 3 4 5 6 7 8 9 10 11 type RawType \\= \\{ a: string, b: number \\}; // 这里就拿到了上述类型的引用 type InferType \\= RawType; // \\{ a: string, b: number \\}; 同样,类型也可以随着数据的传递而传递: var num: number \\= 100; var num2 \\= num; type Num2Type \\= typeof num2; // number 也正是依赖这一点,Typescript 才得以实现 「类型检查」、「定义跳转」 等功能。\n到这里熟悉 「流式编程」 的同学就要举手了:你光说了类型的 「传递」,「输入」 与 「输出」,那我如果希望在类型 「传递」 的过程中对它进行操作,该怎么做呢?同学你不要急,这正是我下面所想要讲的内容。\n类型的过滤与分流 在上一小节中,我们反复地扯到了 「函数式编程」、「响应式编程」、「流式编程」 这些抽象的概念,其实并不是跑题,而是者两者的思想(理念)实在太相似了,在本小节后续的讲解中,我还会一直延用这些概念帮助大家理解。翻看一下常用 「函数式编程」 的库,不管是 Ramda 、RXJS 还是我们耳熟能详的 lodash 、underscore,里面一定有一个操作符叫作 filter,也就是对数据流的过滤。\n这个操作符的使用频率一定远超其他操作符,那么这么重要的功能,我们在类型系统中该如何实现呢?\n要解决这个问题,这里我们先要了解一个在各大 技术社区/平台 搜索频率非常高的一个问题:\n「TypeScript中 的 never 类型具体有什么用?」\n既然这个问题搜索频率非常之高,这里我也就不重复作答,有兴趣的同学可以看一下尤大大的回答: TypeScript中的never类型具体有什么用? - 尤雨溪的回答 - 知乎[5]。\n这里我们简单总结一下:\n never 代表空集。 常用于用于校验 \u0026ldquo;类型收窄\u0026rdquo; 是否符合预期,就是写出类型绝对安全的代码。 never 常被用来作 \u0026ldquo;类型兜底\u0026rdquo;。 当然上面的总结并不完整,但已经足够帮助理解本小节内容,感兴趣的同学可以自行查阅相关资料。\n上面提到了 \u0026ldquo;类型收窄\u0026rdquo;,这与我们的目标已经十分接近了,当然我们还需要了解 never 参与类型运算的相关表现:\n1 2 type NeverTest \\= string | never // stirng type NeverTest2 \\= string \\\u0026amp; never // never 重要的知识出现了:T | never,结果为 T。\n看到这里,相信聪明的同学们已经有思路了,我们可以用 never 来过滤掉 联合类型(Union Types) 中不和期望的类型,其实这个 「泛型操作符」 早在 Typescript 2.8[6] 就已经被加入到了官方文档中了。\n1 2 3 4 /\\*\\* \\* Exclude from T those types that are assignable to U \\*/ type Exclude\\\u0026lt;T, U\\\u0026gt; \\= T extends U \\? never : T; 相信经过这么长时间的学习,看到这里你一定很容易就能这种写法的思路。\n好了,讲完了 「过滤」,我们再来讲讲 「分流」。类型 「分流」 的概念其实也不难理解,这个概念常常与逻辑判断一同出现,毕竟从逻辑层面来讲,联合类型(Union Types) 本质上还是用来描述 「或」 的关系。同样的概念如果引入到 「流式编程」 中,就自然而然地会引出 「分流」。换成打白话来讲,就是不同数据应被分该发到不同的 「管道」 中,同理,类型也需要。\n那么这么常用的功能,在 Typescript 中如何处理呢?其实这种常见的问题,官方也非常贴心地为我们考虑到了,那就是:类型守卫(Type guard)。网上对 类型守卫(Type guard) 有讲解的文章非常的多,这里也不作赘述,有兴趣的同学可以自行搜索学习。我们这里用一个简单的栗子简单地演示一下用法:\n1 2 3 4 5 6 7 function foo\\(x: A | B\\) \\{ if \\(x instanceof A\\) \\{ // x is A \\} else \\{ // x is B \\} \\} 「可以触发类型守卫的常见方式有」:typeof、instanceof、in、==、 ===、 !=、 !== 等等。\n当然在有些场景中,单单通过以上的方式不能满足我们的需求,该怎么办呢?其实这种问题,官方也早已经帮我考虑到了:使用 is 关键字自定义 类型守卫(Type guard)。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 注意这里需要返回 boolean 类型 function isA\\(x\\): x is A \\{ return true; \\} // 注意这里需要返回 boolean 类型 function isB\\(x\\): x is B \\{ return x instanceof B; \\} function foo2\\(x: unknown\\) \\{ if \\(isA\\(x\\)\\) \\{ // x is A \\} else \\{ // x is B \\} \\} 小结 这一章节中,我们通过类比 响应式编程、流式编程 的概念方式,帮助大家更好地理解了 「类型推导」 的实现逻辑与思路,相信经过了这一章节的学习,我们对 Typescript 中的类型推导又有了更加深入的理解。不过这一章引入的抽象的概念比较多,也比较杂,基础不是太好的同学需要多花点时间翻看一下相关资料。\n定制化扩展你的 Typescript Typescript Service Plugins 的产生背景、功能定位、基础使用 产生背景 说起 Typescript 的编译手段大部分同学应该都不会陌生,无论是在 webpack 中使用 ts-loader 或 babel-loader,还是在 gulp 中使用 gulp-typescript,亦或是直接使用 Typescript 自带的命令行工具,相信大部分同学也都已经驾轻就熟了,这里不做赘述。\n这里我们把目光聚焦到撸码体验上,相信有使用过 Typescritp 开发前端项目的同学一定有过各种各样的困扰,这里列举几个常见的问题:\n 在处理 CSS Module 的样式资源的类型定义时,不满足于使用 declare module '*.module.css' 这种毫无卵用的类型定义。 不想给编辑器安装各种各样的插件,下次启动编辑器的时间明显变长,小破电脑不堪重负,而且每次重装系统都是一次噩梦降临。 不想妥协于同事的使用习惯,想使用自己熟悉的编辑器。 并不满足于官方已有的代码提示,想让自己的编辑器更加地贴心与智能。 为了提供更加贴心的开发体验,Typescript 官方提供一种解决思路——Typescript Service Plugins 功能定位 以下内容摘自官方 WIKI: ❝In TypeScript 2.2 and later, developers can enable language service plugins to 「augment the TypeScript code editing experience」. ❞\n 其实官方文档已经写的很清楚了,这玩意儿旨在优化 Typescript 代码的 「编写体验」。所以想利用这玩意儿改变编译结果或是想自创新语法的还是省省吧 嗯,我在说我自己呢!\n那么 Typescript Service Plugins 的可以用来做哪些事呢?\n官方也有明确的回答:\n ❝plugins are for augmenting the editing experience. Some examples of things plugins might do:\n Provide errors from a linter inline in the editor Filter the completion list to remove certain properties from window Redirect \u0026ldquo;Go to definition\u0026rdquo; to go to a different location for certain identifiers Enable new errors or completions in string literals for a custom templating language ❞\n同样官方也给出了不推荐使用 Typescript Service Plugins 的场景:\n ❝Examples of things language plugins cannot do:\n Add new custom syntax to TypeScript Change how the compiler emits JavaScript Customize the type system to change what is or isn\u0026rsquo;t an error when running tsc ❞\n好了,相信读到这里大家一定对 Typescript Service Plugins 有了一个大致的了解,下面我会介绍一下 Typescript Service Plugins 的安装与使用。\n如何安装以及如何配置 Typescript Service Plugins Typescript Service Plugins 的安装方法 # 就像安装普通的 `npm` 包一样\nnpm install \\--save\\-dev your\\_plugin\\_name 如何在 tsconfig.json 中配置 Typescript Service Plugins \\{ \u0026quot;compilerOptions\u0026quot;: \\{ /\\*\\* compilerOptions Configuration ... \\*/ \u0026quot;noImplicitAny\u0026quot;: true, \u0026quot;plugins\u0026quot;: \\[ \\{ /\\*\\* 配置插件名称,也可以填写本地路径 \\*/ \u0026quot;name\u0026quot;: \u0026quot;sample-ts-plugin\u0026quot; /\\*\\* 这里可以给插件传参 ... \\*/ \\} /\\*\\* 支持同时引入多个插件 ... \\*/ \\] \\} \\} 几个需要注意的地方: 如果使用 VSCode 开发,记得务必 using the workspace version of typescript[7],否则可能导致插件不生效。 Typescript Service Plugins 产生的告警或者报错不会影响编译结果。 如果配置完了不生效可以先尝试重启你的编辑器。 市面上已有的 Typescript Service Plugins 举例介绍 ❝具体使用细节请用编辑器打开我提供的 demo,自行体验。 ❞\n 示例插件:typescript-plugin-css-modules[8] 插件安装 npm install \\--save\\-dev typescript\\-styled\\-plugin typescript 配置方法 在 tsconfig.json 中增加配置 \\{ \u0026quot;compilerOptions\u0026quot;: \\{ \u0026quot;plugins\u0026quot;: \\[ \\{ \u0026quot;name\u0026quot;: \u0026quot;typescript-styled-plugin\u0026quot; /\\*\\* 具体配置参数请查看官方文档 \\*/ \\} \\] \\} \\} 插件基本介绍与使用场景 此插件可以用来缓解在使用 CSS Module 时没有代码提示的困境,主要思路就是通过读取对应的 CSS Module 文件并解析成对应的 AST,并生成对应的类型文件从而支持对应的代码提示。但是根据反馈来看,似乎某些场景下表现并不尽人意,是否值得大规模使用有待商榷。\n类似实现思路的还有 typings-for-css-modules-loader[9],功能来说肯定是 webpack loader 更加强大,但是 Typescript Plugin 更加轻量、入侵度也越低,取舍与否,见仁见智吧\n示例插件:typescript-eslint-language-service[10] 插件安装 npm install \\--save\\-dev eslint typescript\\-eslint\\-language\\-service 配置方法 在 .eslintrc.* 文件中,添加对应的 eslint 配置\n在 tsconfig.json 中增加配置 1 2 3 4 5 6 7 8 9 10 11 { \u0026#34;compilerOptions\u0026#34;: { \u0026#34;plugins\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;typescript-eslint-language-service\u0026#34; /*\\* 默认会读取 \\`.eslintrc.\\*\\` 文件 \\*/ /*\\* 具体配置参数请查看官方文档 \\*/ } ] } } 插件基本介绍与使用场景 此插件可以让 Typescript 原生支持 eslint 检查及告警,编辑器不需要安装任何插件即可自持,但是报错并不影响编译结果。\n示例插件:typescript-styled-plugin[11] 插件安装 npm install \\--save\\-dev typescript\\-styled\\-plugin typescript 配置方法 在 tsconfig.json 中增加配置 \\{ \u0026quot;compilerOptions\u0026quot;: \\{ \u0026quot;plugins\u0026quot;: \\[ \\{ \u0026quot;name\u0026quot;: \u0026quot;typescript-styled-plugin\u0026quot; /\\*\\* 具体配置参数请查看官方文档 \\*/ \\} \\] \\} \\} 插件基本介绍与使用场景 此插件可以为 styled-components[12] 的样式字符串模板提供 属性/属性值 做语法检查。 同时也推荐安装 VSCode 插件 vscode-styled-components[13],为你的样式字符串模板提供代码提示以及语法高亮。\n参考资料链接 Using the Compiler API[14] Using the Language Service API[15] Writing a Language Service Plugin[16] Useful Links for TypeScript Issue Management[17] Q\u0026amp;A 可以利用 Typescript Service Plugin(例如配置 eslint 规则)阻塞编译或者在编译时告警吗? 「答」:不可以,所有可以使用 Typescript Plugin 的场景一定都是编码阶段的,而且官方对 plugins 的定位局限在了 只改善编写体验 这方面,你并不能自定义语法或者自定义规则来改变编译结果,不过你可以考虑使用自定义 compiler,当然这是另一个话题了。\n以下引用自官方文档:\n ❝TypeScript Language Service Plugins (\u0026ldquo;plugins\u0026rdquo;) are for changing the 「editing experience only」. The core TypeScript language remains the same. Plugins can\u0026rsquo;t add new language features such as new syntax or different typechecking behavior, and 「plugins aren\u0026rsquo;t loaded during normal commandline typechecking or emitting」. ❞\n Reference [1]\nTypescript 官网: https://www.typescriptlang.org/\n[2]\nTypeScript Deep Dive: https://basarat.gitbook.io/typescript/\n[3]\nTypeScript GitHub地址: https://github.com/microsoft/TypeScript\n[4]\nhandbook: https://www.typescriptlang.org/docs/handbook/basic-types.html\n[5]\nTypeScript中的never类型具体有什么用? - 尤雨溪的回答 - 知乎: https://www.zhihu.com/question/354601204/answer/888551021\n[6]\nTypescript 2.8: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#predefined-conditional-types\n[7]\nusing the workspace version of typescript: https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-the-workspace-version-of-typescript\n[8]\ntypescript-plugin-css-modules: https://www.npmjs.com/package/typescript-plugin-css-modules\n[9]\ntypings-for-css-modules-loader: https://www.npmjs.com/package/@teamsupercell/typings-for-css-modules-loader\n[10]\ntypescript-eslint-language-service: https://www.npmjs.com/package/typescript-eslint-language-service\n[11]\ntypescript-styled-plugin: https://www.npmjs.com/package/typescript-styled-plugin\n[12]\nstyled-components: https://www.npmjs.com/package/styled-components\n[13]\nvscode-styled-components: https://github.com/styled-components/vscode-styled-components\n[14]\nUsing the Compiler API: https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API\n[15]\nUsing the Language Service API: https://github.com/microsoft/TypeScript/wiki/Using-the-Language-Service-API\n[16]\nWriting a Language Service Plugin: https://github.com/microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin\n[17]\nUseful Links for TypeScript Issue Management: https://github.com/microsoft/TypeScript/wiki/Useful-Links-for-TypeScript-Issue-Management\n","description":"","id":41,"section":"posts","tags":["javascript","typescript"],"title":"深入理解 Typescript 高级用法","uri":"https://yichenlove.github.io/posts/typescripts-high/"},{"content":"JIT,即Just-in-time,动态(即时)编译,边运行边编译;AOT,Ahead Of Time,指运行前编译,是两种程序的编译方式\n区别 这两种编译方式的主要区别在于是否在“运行时”进行编译\n优劣 JIT优点: 可以根据当前硬件情况实时编译生成最优机器指令(ps. AOT也可以做到,在用户使用是使用字节码根据机器情况在做一次编译) 可以根据当前程序的运行情况生成最优的机器指令序列 当程序需要支持动态链接时,只能使用JIT 可以根据进程中内存的实际情况调整代码,使内存能够更充分的利用 JIT缺点: 编译需要占用运行时资源,会导致进程卡顿 由于编译时间需要占用运行时间,对于某些代码的编译优化不能完全支持,需要在程序流畅和编译时间之间做权衡 在编译准备和识别频繁使用的方法需要占用时间,使得初始编译不能达到最高性能 AOT优点: 在程序运行前编译,可以避免在运行时的编译性能消耗和内存消耗 可以在程序运行初期就达到最高性能 可以显著的加快程序的启动 AOT缺点: 在程序运行前编译会使程序安装的时间增加 牺牲Java的一致性 将提前编译的内容保存会占用更多的外 与Android的关联 Android在2.2的时候引入JIT,在kitkat时新增了ART(Android RunTime),在Android L时使用ART完全替代了Dalvik作为默认的虚拟机环境。\nDalvik Dalvik使用JIT 使用.dex字节码,是针对Android设备优化后的DVM所使用的运行时编译字节码 .odex是对dex的优化,deodex在系统第一次开机时会提取所有apk内的dex文件,odex优化将dex提前提取出,加快了开机的速度和程序运行的速度 ART ART 使用AOT 在安装apk时会进行预编译,生成OAT文件,仍以.odex保存,但是与Dalvik下不同,这个文件是可执行文件 dex、odex 均可通过dex2oat生成oat文件,以实现兼容性 在大型应用安装时需要更多时间和空间 Android N引入的混合编译 在Android N中引入了一种新的编译模式,同时使用JIT和AOT。这是我在网上找到的一些解释:\n 包含了一个混合模式的运行时。应用在安装时不做编译,而是解释字节码,所以可以快速启动。ART中有一种新的、更快的解释器,通过一种新的JIT完成,但是这种JIT的信息不是持久化的。取而代之的是,代码在执行期间被分析,分析结果保存起来。然后,当设备空转和充电的时候,ART会执行针对“热代码”进行的基于分析的编译,其他代码不做编译。为了得到更优的代码,ART采用了几种技巧包括深度内联。\n对同一个应用可以编译数次,或者找到变“热”的代码路径或者对已经编译的代码进行新的优化,这取决于分析器在随后的执行中的分析数据。\n 这些大概说的是新的ART在安装程序时使用JIT,在JIT编译了一些代码后将这些代码保存到本地,等到设备空闲的时候将保存的这些代码使用AOT编译生成可执行文件保存到本地,待下次运行时直接使用,并且不断监视代码的更新,在代码有更新后重新生成可执行文件。\n","description":"","id":42,"section":"posts","tags":null,"title":"AOT和JIT","uri":"https://yichenlove.github.io/posts/aot-jit/"},{"content":"算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。\n那么我们应该如何去衡量不同算法之间的优劣呢?\n主要还是从算法所占用的「时间」和「空间」两个维度去考量。\n 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。 因此,评价一个算法的效率主要是看它的时间复杂度和空间复杂度情况。然而,有的时候时间和空间却又是「鱼和熊掌」,不可兼得的,那么我们就需要从中去取一个平衡点。\n下面我来分别介绍一下「时间复杂度」和「空间复杂度」的计算方式。\n一、时间复杂度 我们想要知道一个算法的「时间复杂度」,很多人首先想到的的方法就是把这个算法程序运行一遍,那么它所消耗的时间就自然而然知道了。\n这种方式可以吗?当然可以,不过它也有很多弊端。\n这种方式非常容易受运行环境的影响,在性能高的机器上跑出来的结果与在性能低的机器上跑的结果相差会很大。而且对测试时使用的数据规模也有很大关系。再者,并我们在写算法的时候,还没有办法完整的去运行呢。\n因此,另一种更为通用的方法就出来了:「 大O符号表示法 」,即 T(n) = O(f(n))\n我们先来看个例子:\n1 2 3 4 5 for(i=1; i\u0026lt;=n; ++i) { j = i; j++; } 通过「 大O符号表示法 」,这段代码的时间复杂度为:O(n) ,为什么呢?\n在 大O符号表示法中,时间复杂度的公式是: T(n) = O( f(n) ),其中f(n) 表示每行代码执行次数之和,而 O 表示正比例关系,这个公式的全称是:算法的渐进时间复杂度。\n我们继续看上面的例子,假设每行代码的执行时间都是一样的,我们用 1颗粒时间 来表示,那么这个例子的第一行耗时是1个颗粒时间,第三行的执行时间是 n个颗粒时间,第四行的执行时间也是 n个颗粒时间(第二行和第五行是符号,暂时忽略),那么总时间就是 1颗粒时间 + n颗粒时间 + n颗粒时间 ,即 (1+2n)个颗粒时间,即: T(n) = (1+2n)*颗粒时间,从这个结果可以看出,这个算法的耗时是随着n的变化而变化,因此,我们可以简化的将这个算法的时间复杂度表示为:T(n) = O(n)\n为什么可以这么去简化呢,因为大O符号表示法并不是用于来真实代表算法的执行时间的,它是用来表示代码执行时间的增长变化趋势的。\n所以上面的例子中,如果n无限大的时候,T(n) = time(1+2n)中的常量1就没有意义了,倍数2也意义不大。因此直接简化为T(n) = O(n) 就可以了。\n常见的时间复杂度量级有:\n 常数阶O(1) 对数阶O(logN) 线性阶O(n) 线性对数阶O(nlogN) 平方阶O(n²) 立方阶O(n³) K次方阶O(n^k) 指数阶(2^n) 上面从上至下依次的时间复杂度越来越大,执行的效率越来越低。\n下面选取一些较为常用的来讲解一下(没有严格按照顺序):\n 常数阶O(1) 无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),如:\n1 2 3 4 5 int i = 1; int j = 2; ++i; j++; int m = i + j; 上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。\n 线性阶O(n) 这个在最开始的代码示例中就讲解过了,如:\n1 2 3 4 5 for(i=1; i\u0026lt;=n; ++i) { j = i; j++; } 这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度。\n 对数阶O(logN) 还是先来看代码:\n1 2 3 4 5 int i = 1; while(i\u0026lt;n) { i = i * 2; } 从上面代码可以看到,在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。我们试着求解一下,假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2^n\n也就是说当循环 log2^n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn)\n 线性对数阶O(nlogN) 线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。\n就拿上面的代码加一点修改来举例:\n1 2 3 4 5 6 7 8 for(m=1; m\u0026lt;n; m++) { i = 1; while(i\u0026lt;n) { i = i * 2; } } 平方阶O(n²) 平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²) 了。\n举例:\n1 2 3 4 5 6 7 8 for(x=1; i\u0026lt;=n; x++) { for(i=1; i\u0026lt;=n; i++) { j = i; j++; } } 这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²)\n如果将其中一层循环的n改成m,即:\n1 2 3 4 5 6 7 8 for(x=1; i\u0026lt;=m; x++) { for(i=1; i\u0026lt;=n; i++) { j = i; j++; } } 那它的时间复杂度就变成了 O(m*n)\n 立方阶O(n³)、K次方阶O(n^k) 参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似。\n除此之外,其实还有 平均时间复杂度、均摊时间复杂度、最坏时间复杂度、最好时间复杂度 的分析方法,有点复杂,这里就不展开了。\n二、空间复杂度 既然时间复杂度不是用来计算程序具体耗时的,那么我也应该明白,空间复杂度也不是用来计算程序实际占用的空间的。\n空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义。\n空间复杂度比较常用的有:O(1)、O(n)、O(n²),我们下面来看看:\n 空间复杂度 O(1) 如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)\n举例:\n1 2 3 4 5 int i = 1; int j = 2; ++i; j++; int m = i + j; 代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)\n 空间复杂度 O(n) 我们先看一个代码:\n1 2 3 4 5 6 int[] m = new int[n] for(i=1; i\u0026lt;=n; ++i) { j = i; j++; } 这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)\n以上,就是对算法的时间复杂度与空间复杂度基础的分析。\n","description":"","id":43,"section":"posts","tags":["algorithms"],"title":"算法的时间与空间复杂度","uri":"https://yichenlove.github.io/posts/algorithms-0/"},{"content":"01\n验证对于IC的重要性\nIC是集成电路的缩写,也就是我们常说的芯片;IC行业的技术门槛高、投入资金大、回报周期长、失败风险高,做一款中等规模的芯片大致需要10多人做1年半,开模的费用一般都在几百万,设计过程的笔误或者设计bug至少都会有上千个,由于设计缺陷或者工艺缺陷很容易造成芯片完全变成所谓的石头,而如果要重新头片不但需要投入额外的费用,更会将芯片上市时间延后至少半年,这些风险对于商业公司来说都是不可接受的。\n正因为芯片的高风险,才凸显了验证的重要性。在流片之前,通过验证人员的验证活动发现所有的设计bug,这就显得特别重要。\n02\n验证的一目标\n做验证首先要明确我们做IC验证的目标是什么。上面我们已经提到,由于芯片的高风险、高代价,才更突出了验证的重要性,尤其是芯片规模越来越大,逻辑越来越复杂。\n为了保证芯片的成功,验证唯一的目标就是发现所有的bug,做到无漏验、零漏测。\n03\n验证的两问题\n作为验证人员,首先要搞清楚两个问题:\n1)我们要验证什么?\n2)我们该怎么验?\n这两个问题是验证的根本,就如同哲学里的“我是谁、我来自哪儿、我要去哪儿”一样,“我们要验什么?”是给我们指明目标,”我们该怎么验?“则是告诉我们该采用什么样的手段去达到这个目标。\n如果这2个问题都没搞清楚,那么没人对你负责验证的模块有信心,毕竟你自己都不知道你的目标是什么,不知道该怎么做才能达到那个目标。这两个问题是验证的核心所在,如果想做好验证,这是前提。\n04\n验证的三板斧\n要想做好验证,保证无漏验、零漏测,以下三个要素是必须要具备的:验证工具的掌握、算法/协议的理解、验证的意识。\n1)验证工具的掌握\n验证工具包括vmm/uvm等验证方法学、sv/sc等验证语言、vcs等验证仿真工具、perl/python等脚本语言,这些东西是做验证要掌握的基本技能,不论你做什么样的芯片都需要这些东西来支撑你的验证工作。\n这些验证工具可以帮助你解决“我们该怎么验”这个问题,当你很好的掌握这些验证工具后,你可以有很多种方法途径去达成你的验证目标。\n说实在话,验证工具的东西很多,要想在短时间内全部掌握也不可能,而且很多工具可能在你的验证过程中不会用到。\n个人对验证工具的一点感悟是:不要贪求全部掌握,你可以先看书学习实践,把这些东西都学习一遍;在学习的过程中你肯定会发现一些好东西(原来还有这种方法可以让我的xx做的更好);对于那些暂时不知道怎么应用到实践中的东西,你也不要认为它们是没用的,其实只是你不知道用在哪儿而已,在你以后的验证中也许就会发现它的应用场景,当你需要它的时候也许你已经忘记怎么用了,这个没关系,你可以再回去查阅资料,这个相信很快就能解决的,这样有个好处是当你碰到可以用xx的时候你至少能想起曾经看到某个东西可以来实现它,如果你从未学习过,那么你根本就不会想起有这么个方法可以解决它,这才是可怕的,我都不知道这个问题是可以被解决的。\n2)算法/协议的理解\n芯片要实现什么,不外乎是xx算法、某某协议,算法/协议才是芯片的魂。验证其实也就是验的算法/协议实现是否正确。就跟批改作文一样,只有批改者有一定的文学功底,才能更好的评判作文水平。\n因此,验证人员对算法/协议理解越深刻越好,要理解算法的原理以及算法的实现结构,只有这样才能找出其中的corner点。\n3)验证的意识\n验证的意识究竟是什么,其实我也说不清楚,只能按照我自己的理解写写一些。\n· 对任何东西都要有质疑的态度\n· 手要伸长,延伸到上下游\n· 对问题要刨根问底\n05\n验证的流程\n做任何事情都需要按照一定的流程来走,否则很容易陷入混乱之中,尤其是对于刚入门的新手来说更是如此。我目前接触的通用流程大致如下:\n1)提取测试点,明确验什么\n· 分析FS/浮点平台,提取芯片的规格及测试点;\n· 分析AS/定点平台,提取测试点;\n· 分析DS,提取测试点并识别asic与算法的不一致点;\n2)制定验证方案,明确怎么验\n· 刷新测试点列表,明确测试点的覆盖方式:功能覆盖率、代码覆盖率、直接用例;\n· 验证环境的搭建策略,这个步骤是可以做成自动化工具的;\n· 验证的重点难点,提前识别重难点,并制定相应的对策;\n· 刷新用例列表,明确测试用例的方法及步骤;\n3)用例执行,随机测试,发现bug\n· 执行直接用例,发现大部分的bug;\n· 带随机的大量测试,试图撞出bug;\n4)完备性分析,确保无漏验\n· FA/AS完备性确认,确认FS/AS中的所有点都已纳入测试点,并确保已被覆盖,包括应用场景;\n· 接口完备性确认,保证所有的接口时序都已覆盖,包括正常时序及异常时序;\n· 覆盖率确认,分析所有的代码覆盖率、功能覆盖率,保证全部覆盖;\n· 代码分析,熟练掌握电路的实现逻辑,保证所有的电路corner都已覆盖;\n上述这几个步骤是一个比较规范的流程,只要每个步骤都做好,基本就能做到无漏测、零漏验。\n06\n验证的后话\n1)验证的空间\n作为验证人员最希望的情况是:把所有的激励空间都覆盖到,这样就绝对能保证无漏测、零漏验。但实际情况是:芯片规模越来越大,其激励空间近乎无限,同时EDA仿真的速度奇慢,根本无法实现全覆盖,即使是FPGA、EMU等仿真加速器对此也是无能为力。\n因此,合理划分激励等价类是相当重要的,但这也一直是验证的难点所在,很多情况下根本就没法分析清楚等价类。\n2)CDV验证\nCDV就是覆盖率驱动验证的意思,就是写一大堆覆盖率(断言覆盖率、功能覆盖率、代码覆盖率),只要这些覆盖率全都达到的话则表示验证已经完备。\n这是我们的目标,其前提是分析清楚我们的测试点覆盖空间,这个分析也是让人头痛的事,没有谁敢拍着胸脯说这个测试点空间是完备的。\n3)formal验证\n传统的仿真都是动态验证,由于其仿真效率低下无法遍历所有空间,formal这种静态的验证手段则可以遍历所有空间。不过在目前这个阶段,formal还只能适用于百万门级的模块验证,同时目前市面上的formal工具大多要么只对控制逻辑支持较好,要么只对算法逻辑支持较好,几乎没有一款formal工具能完美支持所有的电路逻辑。\n4)环境自动化\n在验证过程中,搭建验证环境是一个机械性的劳动,但有时候又比较耗费时间而且容易出错,因此把验证环境做成自动化工具,还是能提高不少验证效率的。\n5)全部使用直接用例\n从验证流程中可以看到,用例执行过程中大部分bug在直接用例过程中被发现,但还有一部分隐藏比较深的bug只有通过随机激励来发现。\n这里存在一个问题,随机测试是不可靠的,有很大的概率发现不了隐藏的bug,对此可以有两种方法:\n一是采用带约束的随机,这样可以更好的达到边界点,这同样存在概率性问题;\n二是所有的corner点全部用直接用例覆盖,这些直接用例执行一次即可发现所有的bug,根本不需要进行长期的随机测试,这要求我们能识别出所有的corner点;\n方法二是我们追求的目标,全部用直接用例覆盖,取代长期随机测试,可惜愿望是美好的。\n6)复用的东西都BB化\n在芯片设计中经常回重用以前的模块,这样不仅加快进度,而且能降低出错风险;但是对于验证人员来说,复用并不一定是好事情,经常会出现这样的事情:由于是复用之前的模块,所以在验证的时候会掉以轻心,结果埋下bug。如果把复用模块当做全新模块来验证,这又要花费大量的时间,可能就会延后芯片的投片时间。\n对于复用的模块,验证人员也可以把验证的相关东西做成BB化,后续芯片复用该模块时,也可以复用该验证BB。\n","description":"","id":44,"section":"posts","tags":["IC"],"title":"IC设计验证参考","uri":"https://yichenlove.github.io/posts/ic-verification/"},{"content":"要点: char与int :可以相加减;int取本身数值,char取对应的ASCII码值;得到的结果是ASCII码增大或减小了int对应的数值大小;如果结果赋值给char类型的变量即是该ASCII码对应的字符,如果赋值给int类型的变量即是该ASCII码的大小。 1 2 3 4 5 6 7 8 public class Test { public static void main(String[] args) { char c = \u0026#39;2\u0026#39;-3;//\u0026#39;2\u0026#39;的ASCII码:50 int i = \u0026#39;2\u0026#39;-3; System.out.println(\u0026#34;c:\u0026#34;+c);//\u0026#39;/\u0026#39;的ASCII码:47 System.out.println(\u0026#34;i:\u0026#34;+i); } } c:/\ni:47\n char与char :可以相加减;都取各自对应的ASCII码进行加减;如果结果赋值给char类型的变量即是该ASCII码对应的字符,如果赋值给int类型的变量即是该ASCII码的大小。 1 2 3 4 5 6 7 8 public class Test { public static void main(String[] args) { char c = \u0026#39;a\u0026#39;-\u0026#39;1\u0026#39;;//\u0026#39;a\u0026#39;的ASCII码:97,\u0026#39;1\u0026#39;的ASCII码:49 \tint i = \u0026#39;a\u0026#39;-\u0026#39;1\u0026#39;; System.out.println(\u0026#34;c:\u0026#34;+c);//\u0026#39;0\u0026#39;的ASCII码:48 \tSystem.out.println(\u0026#34;i:\u0026#34;+i); } } c:0\ni:48\n String与int :只能加不能减;得到的结果为String类型。 1 2 3 4 5 6 public class Test { public static void main(String[] args) { String s = \u0026#34;2\u0026#34;+3; System.out.println(\u0026#34;s:\u0026#34;+s); } } s:23\n 例题: 把字符串转换成整数。\n将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。\n输入描述:\n输入一个字符串,包括数字字母符号,可以为空\n输出描述:\n如果是合法的数值表达则返回该数字,否则返回0\n1 2 3 4 5 6 7 8 9 10 11 12 public static int StrToInt(String str) { if(!str.matches(\u0026#34;(-|\\\\+)?\\\\d+\u0026#34;))//匹配可能带有正负号的数字形式的字符串 return 0; int res = 0; int flag = 1; char strArr[] = str.toCharArray(); if(strArr[0] == \u0026#39;-\u0026#39;) flag = -1; for (int i = (strArr[0]==\u0026#39;+\u0026#39;||strArr[0]==\u0026#39;-\u0026#39;)?1:0; i \u0026lt; strArr.length; i++) res = res*10 + strArr[i] - \u0026#39;0\u0026#39;;//注意这里根据strArr[i] - \u0026#39;0\u0026#39;的差值得到strArr[i]对应的数字 return res*flag; } ","description":"","id":45,"section":"posts","tags":["java"],"title":"Java中char,int,String的相加减","uri":"https://yichenlove.github.io/posts/java-char-add/"},{"content":"# 一、场景复现 一个经典的面试题\n1 0.1 + 0.2 === 0.3 // false 为什么是false呢?\n先看下面这个比喻\n比如一个数 1÷3=0.33333333\u0026hellip;\u0026hellip;\n3会一直无限循环,数学可以表示,但是计算机要存储,方便下次取出来再使用,但0.333333\u0026hellip;\u0026hellip; 这个数无限循环,再大的内存它也存不下,所以不能存储一个相对于数学来说的值,只能存储一个近似值,当计算机存储后再取出时就会出现精度丢失问题\n# 二、浮点数 “浮点数”是一种表示数字的标准,整数也可以用浮点数的格式来存储\n我们也可以理解成,浮点数就是小数\n在JavaScript中,现在主流的数值类型是Number,而Number采用的是IEEE754规范中64位双精度浮点数编码\n这样的存储结构优点是可以归一化处理整数和小数,节省存储空间\n对于一个整数,可以很轻易转化成十进制或者二进制。但是对于一个浮点数来说,因为小数点的存在,小数点的位置不是固定的。解决思路就是使用科学计数法,这样小数点位置就固定了\n而计算机只能用二进制(0或1)表示,二进制转换为科学记数法的公式如下:\n其中,a的值为0或者1,e为小数点移动的位置\n举个例子:\n27.0转化成二进制为11011.0 ,科学计数法表示为:\n前面讲到,javaScript存储方式是双精度浮点数,其长度为8个字节,即64位比特\n64位比特又可分为三个部分:\n 符号位S:第 1 位是正负数符号位(sign),0代表正数,1代表负数 指数位E:中间的 11 位存储指数(exponent),用来表示次方数,可以为正负数。在双精度浮点数中,指数的固定偏移量为1023 尾数位M:最后的 52 位是尾数(mantissa),超出的部分自动进一舍零 如下图所示:\n举个例子:\n27.5 转换为二进制11011.1\n11011.1转换为科学记数法 符号位为1(正数),指数位为4+,1023+4,即1027\n因为它是十进制的需要转换为二进制,即 10000000011,小数部分为10111,补够52位即: 1011 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000`\n所以27.5存储为计算机的二进制标准形式(符号位+指数位+小数部分 (阶数)),既下面所示\n0+10000000011+011 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000`\n# 二、问题分析 再回到问题上\n1 0.1 + 0.2 === 0.3 // false 通过上面的学习,我们知道,在javascript语言中,0.1 和 0.2 都转化成二进制后再进行运算\n1 2 3 4 5 6 // 0.1 和 0.2 都转化成二进制后再进行运算 0.00011001100110011001100110011001100110011001100110011010 + 0.0011001100110011001100110011001100110011001100110011010 = 0.0100110011001100110011001100110011001100110011001100111 // 转成十进制正好是 0.30000000000000004 所以输出false\n再来一个问题,那么为什么x=0.1得到0.1?\n主要是存储二进制时小数点的偏移量最大为52位,最多可以表达的位数是2^53=9007199254740992,对应科学计数尾数是 9.007199254740992,这也是 JS 最多能表示的精度\n它的长度是 16,所以可以使用 toPrecision(16) 来做精度运算,超过的精度会自动做凑整处理\n1 2 .10000000000000000555.toPrecision(16) // 返回 0.1000000000000000,去掉末尾的零后正好为 0.1 但看到的 0.1 实际上并不是 0.1。不信你可用更高的精度试试:\n1 0.1.toPrecision(21) = 0.100000000000000005551 如果整数大于 9007199254740992 会出现什么情况呢?\n由于指数位最大值是1023,所以最大可以表示的整数是 2^1024 \\- 1,这就是能表示的最大整数。但你并不能这样计算这个数字,因为从 2^1024 开始就变成了 Infinity\n1 2 3 4 5 \u0026gt; Math.pow(2, 1023) 8.98846567431158e+307 \u0026gt; Math.pow(2, 1024) Infinity 那么对于 (2^53, 2^63) 之间的数会出现什么情况呢?\n (2^53, 2^54) 之间的数会两个选一个,只能精确表示偶数 (2^54, 2^55) 之间的数会四个选一个,只能精确表示4个倍数 \u0026hellip; 依次跳过更多2的倍数 要想解决大数的问题你可以引用第三方库 bignumber.js,原理是把所有数字当作字符串,重新实现了计算逻辑,缺点是性能比原生差很多\n# 小结 计算机存储双精度浮点数需要先把十进制数转换为二进制的科学记数法的形式,然后计算机以自己的规则{符号位+(指数位+指数偏移量的二进制)+小数部分}存储二进制的科学记数法\n因为存储时有位数限制(64位),并且某些十进制的浮点数在转换为二进制数时会出现无限循环,会造成二进制的舍入操作(0舍1入),当再转换为十进制时就造成了计算误差\n# 三、解决方案 理论上用有限的空间来存储无限的小数是不可能保证精确的,但我们可以处理一下得到我们期望的结果\n当你拿到 1.4000000000000001 这样的数据要展示时,建议使用 toPrecision 凑整并 parseFloat 转成数字后再显示,如下:\n1 parseFloat(1.4000000000000001.toPrecision(12)) === 1.4 // True 封装成方法就是:\n1 2 3 function strip(num, precision = 12) { return +parseFloat(num.toPrecision(precision)); } 对于运算类操作,如 +-*/,就不能使用 toPrecision 了。正确的做法是把小数转成整数后再运算。以加法为例:\n1 2 3 4 5 6 7 8 9 /** * 精确加法 */ function add(num1, num2) { const num1Digits = (num1.toString().split(\u0026#39;.\u0026#39;)[1] || \u0026#39;\u0026#39;).length; const num2Digits = (num2.toString().split(\u0026#39;.\u0026#39;)[1] || \u0026#39;\u0026#39;).length; const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits)); return (num1 * baseNum + num2 * baseNum) / baseNum; } 最后还可以使用第三方库,如Math.js、BigDecimal.js\n# 参考文献 https://zhuanlan.zhihu.com/p/100353781 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/BigInt ","description":"","id":46,"section":"posts","tags":["javascript"],"title":"说说 Javascript 数字精度丢失的问题,如何解决?","uri":"https://yichenlove.github.io/posts/javascripts-float/"},{"content":"你安装的是SUN/Oracle JDK还是OpenJDK? 还傻傻分不清楚吗? 快来看看这篇吧😁\n目录\n 1 如何查看你安装的JDK版本 1.1 要用到的命令行工具 1.2 查看JDK的版本 2 什么是 OpenJDK 2.1 OpenJDK 的来历 2.2 Oracle JDK的来历 3 Oracle JDK与OpenJDK的区别 3.1 主要不同 3.2 授权协议的不同 3.3 OpenJDK不包含Deployment功能 3.4 OpenJDK源码不完整 1 如何查看你安装的JDK版本 1.1 要用到的命令行工具 Windows系统的cmd命令行工具;\nLinux或macOS系统的Terminal终端;\n 1.2 查看JDK的版本 1 java -version (1) 如果是SUN/OracleJDK, 显示信息为:\n1 2 3 4 [root@localhost ~]# java -version java version \u0026#34;1.8.0_162\u0026#34; Java(TM) SE Runtime Environment (build 1.8.0_162-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode) 说明:\n Java HotSpot(TM) 64-Bit Server VM 表明, 此JDK的JVM是Oracle的64位HotSpot虚拟机, 运行在Server模式下(虚拟机有Server和Client两种运行模式).\nJava(TM) SE Runtime Environment (build 1.8.0_162-b12) 是Java运行时环境(即JRE)的版本信息.\n (2) 如果OpenJDK, 显示信息为:\n1 2 3 4 [root@localhost ~]# java -version openjdk version \u0026#34;1.8.0_144\u0026#34; OpenJDK Runtime Environment (build 1.8.0_144-b01) OpenJDK 64-Bit Server VM (build 25.144-b01, mixed mode) PS. 主要的Linux发行版(如Ubuntu, Fedora和Red Hat Enterprise Linux) 提供OpenJDK或其变体作为系统默认的Java SE的实现.\n2 什么是 OpenJDK 百度百科上关于OpenJDK的说明:\n Sun发布的OpenJDK是一款只能运行在i386和AMD-64机器上的软件。\n 2.1 OpenJDK 的来历 Java由SUN公司(Sun Microsystems, 发起于美国斯坦福大学, SUN是Stanford University Network的缩写)发明, 2006年SUN公司将Java开源, 此时的JDK即为OpenJDK.\n也就是说, OpenJDK是Java SE的开源实现, 它由SUN和Java社区提供支持, 2009年Oracle收购了Sun公司, 自此Java的维护方之一的SUN也变成了Oracle .\n大多数JDK都是在OpenJDK的基础上编写实现的, 比如IBM J9, Azul Zulu, Azul Zing和Oracle JDK. 几乎现有的所有JDK都派生自OpenJDK, 它们之间不同的是许可证:\n OpenJDK根据许可证GPL v2发布;\nOracle JDK根据Oracle二进制代码许可协议获得许可.\n 2.2 Oracle JDK的来历 Oracle JDK之前被称为SUN JDK, 这是在2009年Oracle收购SUN公司之前, 收购后被命名为Oracle JDK.\n实际上, Oracle JDK是基于OpenJDK源代码构建的, 因此Oracle JDK和OpenJDK之间没有重大的技术差异.\nOracle的项目发布经理Joe Darcy在OSCON 2011 上对两者关系的介绍也证实了OpenJDK 7和Oracle JDK 7在程序上是非常接近的, 两者共用了大量相同的代码(如下图), 注意: 图中提示了两者共同代码的占比要远高于图形上看到的比例, 所以我们编译的OpenJDK基本上可以认为性能、功能和执行逻辑上都和官方的Oracle JDK是一致的.\n3 Oracle JDK与OpenJDK的区别 3.1 主要不同 OpenJDK Font Renderer(字体栅格化引擎) 和Oracle JDK Flight Recorder(飞行记录仪) 是Oracle JDK和OpenJDK之间明显的主要区别. —— 存疑, 尚未求证.\n OpenJDK使用的是开源免费的FreeType, 可以按照GPL v2许可证使用.\nOracle JDK采用了商业实现, 其中的Flight Recorder和MissionControl都是从JRockit中改造而来的.\n JRockit是Oracle的JVM, 从Java SE 7开始, HotSpot和JRockit合并为一个JVM.\n3.2 授权协议的不同 OpenJDK采用GPL V2协议放出, 而Oracle JDK则采用JRL(Java Research License, Java研究授权协议) 放出. 两种者虽然都是开放源代码的, 但在使用上却要注意:\n GPL V2允许在商业上使用;\nJRL只允许个人研究使用, 要获得Oracle JDK的商业许可证, 需要联系Oracle的销售人员进行购买.\n 3.3 OpenJDK不包含Deployment功能 部署的功能包括: Browser Plugin、Java Web Start、Java Mission Control, 这些功能OpenJDK都没有.\n3.4 OpenJDK源码不完整 在采用GPL协议的OpenJDK中, SUN JDK的一部分源码因为产权问题无法提供给OpenJDK使用, 其中最主要的是JMX中的可选元件SNMP部份的代码, 因此这些不能开放的源码将它作成plug, 以供 OpenJDK编译时使用.\n 参考资料 如何看本地安装的jdk是Sun/Oraclejdk还是Openjdk\nOracle JDK vs OpenJDK and Java JDK Development Process\nDifferences between Oracle JDK and OpenJDK\n 版权声明 作者: 马瘦风\n出处: 博客园 马瘦风的博客\n您的支持是对博主的极大鼓励, 感谢您的阅读.\n本文版权归博主所有, 欢迎转载, 但请保留此段声明, 并在文章页面明显位置给出原文链接, 否则博主保留追究相关人员法律责任的权利.\n ","description":"","id":47,"section":"posts","tags":["java","jdk"],"title":"你安装的是Oracle JDK还是OpenJDK?","uri":"https://yichenlove.github.io/posts/javajdk/"},{"content":"效果图 安装iTem2 安装 1 2 3 $ brew tap caskroom/cask # 首次安装需执行该条命令 $ brew cask install iterm2 # 安装iterm2 打开iterm2,检查Report Terminal Type的设定,设为xterm-256color,就可在terminal看到漂亮的颜色\n修改iTerm2的color scheme Over 200 terminal color schemes\n1 git clone https://github.com/mbadolato/iTerm2-Color-Schemes.git # 克隆整个仓库 打开iterm2 快捷键 CMD+i (⌘+i) 点击 Colors 选择 Color Presets 选择Import 找到克隆下来的 .itermcolors 文件中的 scheme(s) ,选择喜欢的配色导入 再次点击 Color Presets 选中导入的color scheme 安装Nerd Fonts 使用的theme中有很多小图标,需要使用支持这些图标的icon font,这类字体称为powerline font(plus版的支持更多图标的称为:nerd font)\n没有安装icon font的界面:\n安装 Nerd-fonts: https://github.com/ryanoasis/nerd-fonts#font-installation\n安装方法: https://github.com/ryanoasis/nerd-fonts#option-4-homebrew-fonts\n1 2 brew tap caskroom/fonts brew cask install font-hack-nerd-font 查看刚刚安装的文件 1 2 cd ~/Library/Fonts ls 打开iterm2,设置字体 1 iTerm2 -\u0026gt; Preferences -\u0026gt; Profiles -\u0026gt; Text -\u0026gt; Font -\u0026gt; Change Font 注:如果切换字体,iterm2无法正常运行,可能是同一字型有重复版本问题,解决方法:\n1 打开Font Book.app -\u0026gt;选择该字体 -\u0026gt; 选择自动解决版本问题 安装zsh 1 brew install zsh 设置zsh为默认:\n1 2 sudo sh -c \u0026#34;echo $(which zsh) \u0026gt;\u0026gt; /etc/shells\u0026#34; chsh -s $(which zsh) bash切换到zsh\n1 chsh -s /bin/zsh 安裝 oh-my-zsh 1 sh -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)\u0026#34; 切换内建主题\n1 2 3 cd ~ vim ~/.zshrc ZSH_THEME=”agnoster” # 将robbyrussell--\u0026gt;agnoster 执行以下指令生效\n1 exec $SHELL # 或 source .zshrc 安装 powerlevel9k powerlevel9k\n1 2 3 brew tap sambadevi/powerlevel9k brew install powerlevel9k powerlevel9k不是 oh-my-zsh 內建的 theme,需另外下载\n1 $ git clone https://github.com/bhilburn/powerlevel9k.git ~/.oh-my-zsh/custom/themes/powerlevel9k 编辑.zshrc\n1 2 3 4 5 6 7 8 9 10 11 # 设置zsh主题 ZSH_THEME=\u0026#34;powerlevel9k/powerlevel9k\u0026#34; # 设置左边显示的内容 POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(dir dir_writable vcs) # \u0026lt;= left prompt 设了 \u0026#34;dir\u0026#34; 即文件、进入有写入权限的文件夹则提示、vcs # command line 右边想显示的内容 POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(time) # \u0026lt;= right prompt 设了 \u0026#34;time\u0026#34; 即时间 # 显示git的图标 POWERLEVEL9K_MODE=\u0026#39;nerdfont-complete\u0026#39; 更多配置参考:https://github.com/bhilburn/powerlevel9k#available-prompt-segments\n快速配置,可参考 https://github.com/xqlip/lin_terminal.git\n推荐安装的套件 Zsh-autosuggestions:https://github.com/zsh-users/zsh-autosuggestions\n参考链接: https://medium.com/the-code-review/nerd-fonts-how-to-install-configure-and-remove-programming-fonts-on-a-mac-178833b9daf3\nhttps://medium.com/the-code-review/make-your-terminal-more-colourful-and-productive-with-iterm2-and-zsh-11b91607b98c\nhttps://medium.com/the-code-review/powerlevel9k-personalise-your-prompt-for-any-programming-language-68974c127c63\nhttps://medium.com/@h86991868/%E7%9C%8B%E8%86%A9%E4%BA%86%E4%B8%80%E6%88%90%E4%B8%8D%E8%AE%8A%E7%9A%84%E5%B0%8F%E9%BB%91%E7%AA%97-%E6%94%B9%E7%94%A8iterm2-oh-my-zsh%E5%90%A7-cc2b0683acb\n","description":"","id":48,"section":"posts","tags":null,"title":"iTem2 + oh-my-zsh 打造Mac上好用的终端","uri":"https://yichenlove.github.io/posts/ohmyzsh/"},{"content":"协程在unity中是一个很常用的方法,我们可以利用协程使代码看起来更连贯,易于理解。xlua在示例6中提供了一个协程的示例。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 local util = require \u0026#39;xlua.util\u0026#39; local gameobject = CS.UnityEngine.GameObject(\u0026#39;Coroutine_Runner\u0026#39;) CS.UnityEngine.Object.DontDestroyOnLoad(gameobject) local cs_coroutine_runner = gameobject:AddComponent(typeof(CS.Coroutine_Runner)) local function async_yield_return(to_yield, cb) cs_coroutine_runner:YieldAndCallback(to_yield, cb) end return { yield_return = util.async_to_sync(async_yield_return) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 local util = require \u0026#39;xlua.util\u0026#39; local yield_return = (require \u0026#39;cs_coroutine\u0026#39;).yield_return local co = coroutine.create(function() print(\u0026#39;coroutine start!\u0026#39;) local s = os.time() yield_return(CS.UnityEngine.WaitForSeconds(3)) print(\u0026#39;wait interval:\u0026#39;, os.time() - s) local www = CS.UnityEngine.WWW(\u0026#39;http://www.qq.com\u0026#39;) yield_return(www) if not www.error then print(www.bytes) else print(\u0026#39;error:\u0026#39;, www.error) end end) assert(coroutine.resume(co)) 利用这个示例我们可以实现协程的一些功能,比如服务器列表获取,资源下载等。但是,随着项目的推进,我们发现,这个协程和我们在unity中使用的协程是不一样的,我们无法中断它。相信很多人都遇到了这个问题,为了解决这个问题,我们需要换一种思路来实现。\n在前面的设计中,我们将c#端的LuaBehaviour注入到了lua端,这以为着我们可以直接利用这个注入变量调用c#端的StartCoroutine函数,能调用到这个函数,问题就直接解决了。\n但是,进阶这会发现好像并不能运行,这是因为Lua端的函数仅仅是个普通函数,而协程StartCoroutine要求的函数是要IEnumerator,这意味着我们需要构造出一个IEnumerator来,在官方文档《hotfix.md》里,有一节是“Unity协程”\n 通过util.cs_generator可以用一个function模拟一个IEnumerator,在里头用coroutine.yield,就类似C#里头的yield return。比如下面的C#代码和对应的hotfix代码是等同效果的\n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 ~~~csharp [XLua.Hotfix] public class HotFixSubClass : MonoBehaviour { IEnumerator Start() { while (true) { yield return new WaitForSeconds(3); Debug.Log(\u0026#34;Wait for 3 seconds\u0026#34;); } } } ~~~ ~~~csharp luaenv.DoString(@\u0026#34; local util = require \u0026#39;xlua.util\u0026#39; xlua.hotfix(CS.HotFixSubClass,{ Start = function(self) return util.cs_generator(function() while true do coroutine.yield(CS.UnityEngine.WaitForSeconds(3)) print(\u0026#39;Wait for 3 seconds\u0026#39;) end end end; }) \u0026#34;); ~~~ 至此,我们可以构造出IEnumerator,然后调用MonoBehaviour 的StartCoroutine函数来实现unity侧特性的协程,这种协程是可以通过StopCoroutine来中断的。\n","description":"","id":50,"section":"posts","tags":["unity","xlua","lua"],"title":" XLua框架——lua协程实现","uri":"https://yichenlove.github.io/posts/xluacoroutine/"},{"content":"详解 HTTP 协议 思维导图预览\n一张图带你看完本篇文章\n一、概述 1.计算机网络体系结构分层 计算机网络体系结构分层\n2.TCP/IP 通信传输流 利用 TCP/IP 协议族进行网络通信时,会通过分层顺序与对方进行通信。发送端从应用层往下走,接收端则从链路层往上走。如下:\nTCP/IP 通信传输流\n 首先作为发送端的客户端在应用层(HTTP 协议)发出一个想看某个 Web 页面的 HTTP 请求。 接着,为了传输方便,在传输层(TCP 协议)把从应用层处收到的数据(HTTP 请求报文)进行分割,并在各个报文上打上标记序号及端口号后转发给网络层。 在网络层(IP 协议),增加作为通信目的地的 MAC 地址后转发给链路层。这样一来,发往网络的通信请求就准备齐全了。 接收端的服务器在链路层接收到数据,按序往上层发送,一直到应用层。当传输到应用层,才能算真正接收到由客户端发送过来的 HTTP请求。 如下图所示:\nHTTP 请求\n在网络体系结构中,包含了众多的网络协议,这篇文章主要围绕 HTTP 协议(HTTP/1.1版本)展开。\n HTTP协议(HyperText Transfer Protocol,超文本传输协议)是用于从WWW服务器传输超文本到本地浏览器的传输协议。它可以使浏览器更加高效,使网络传输减少。它不仅保证计算机正确快速地传输超文本文档,还确定传输文档中的哪一部分,以及哪部分内容首先显示(如文本先于图形)等。\nHTTP是客户端浏览器或其他程序与Web服务器之间的应用层通信协议。在Internet上的Web服务器上存放的都是超文本信息,客户机需要通过HTTP协议传输所要访问的超文本信息。HTTP包含命令和传输信息,不仅可用于Web访问,也可以用于其他因特网/内联网应用系统之间的通信,从而实现各类应用资源超媒体访问的集成。\n我们在浏览器的地址栏里输入的网站地址叫做URL (Uniform Resource Locator,统一资源定位符)。就像每家每户都有一个门牌地址一样,每个网页也都有一个Internet地址。当你在浏览器的地址框中输入一个URL或是单击一个超级链接时,URL就确定了要浏览的地址。浏览器通过超文本传输协议(HTTP),将Web服务器上站点的网页代码提取出来,并翻译成漂亮的网页。\n 二、HTTP 工作过程 HTTP请求响应模型\nHTTP通信机制是在一次完整的 HTTP 通信过程中,客户端与服务器之间将完成下列7个步骤:\n 建立 TCP 连接\n在HTTP工作开始之前,客户端首先要通过网络与服务器建立连接,该连接是通过 TCP 来完成的,该协议与 IP 协议共同构建 Internet,即著名的 TCP/IP 协议族,因此 Internet 又被称作是 TCP/IP 网络。HTTP 是比 TCP 更高层次的应用层协议,根据规则,只有低层协议建立之后,才能进行高层协议的连接,因此,首先要建立 TCP 连接,一般 TCP 连接的端口号是80; 客户端向服务器发送请求命令\n一旦建立了TCP连接,客户端就会向服务器发送请求命令;\n例如:GET/sample/hello.jsp HTTP/1.1 客户端发送请求头信息\n客户端发送其请求命令之后,还要以头信息的形式向服务器发送一些别的信息,之后客户端发送了一空白行来通知服务器,它已经结束了该头信息的发送; 服务器应答\n客户端向服务器发出请求后,服务器会客户端返回响应;\n例如: HTTP/1.1 200 OK\n响应的第一部分是协议的版本号和响应状态码 服务器返回响应头信息\n正如客户端会随同请求发送关于自身的信息一样,服务器也会随同响应向用户发送关于它自己的数据及被请求的文档; 服务器向客户端发送数据\n服务器向客户端发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就以 Content-Type 响应头信息所描述的格式发送用户所请求的实际数据; 服务器关闭 TCP 连接\n一般情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接,然后如果客户端或者服务器在其头信息加入了这行代码 Connection:keep-alive ,TCP 连接在发送后将仍然保持打开状态,于是,客户端可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。 三、HTTP 协议基础 1.通过请求和响应的交换达成通信 应用 HTTP 协议时,必定是一端担任客户端角色,另一端担任服务器端角色。仅从一条通信线路来说,服务器端和客服端的角色是确定的。HTTP 协议规定,请求从客户端发出,最后服务器端响应该请求并返回。换句话说,肯定是先从客户端开始建立通信的,服务器端在没有接收到请求之前不会发送响应。\n2.HTTP 是不保存状态的协议 HTTP 是一种无状态协议。协议自身不对请求和响应之间的通信状态进行保存。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设计成如此简单的。\n可是随着 Web 的不断发展,我们的很多业务都需要对通信状态进行保存。于是我们引入了 Cookie 技术。有了 Cookie 再用 HTTP 协议通信,就可以管理状态了。\n3.使用 Cookie 的状态管理 Cookie 技术通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态。Cookie 会根据从服务器端发送的响应报文内的一个叫做 Set-Cookie 的首部字段信息,通知客户端保存Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入 Cookie 值后发送出去。服务器端发现客户端发送过来的 Cookie 后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到之前的状态信息。\nCookie 的流程\n4.请求 URI 定位资源 HTTP 协议使用 URI 定位互联网上的资源。正是因为 URI 的特定功能,在互联网上任意位置的资源都能访问到。\n5.告知服务器意图的 HTTP 方法(HTTP/1.1) HTTP 方法\n6.持久连接 HTTP 协议的初始版本中,每进行一个 HTTP 通信都要断开一次 TCP 连接。比如使用浏览器浏览一个包含多张图片的 HTML 页面时,在发送请求访问 HTML 页面资源的同时,也会请求该 HTML 页面里包含的其他资源。因此,每次的请求都会造成无畏的 TCP 连接建立和断开,增加通信量的开销。\n为了解决上述 TCP 连接的问题,HTTP/1.1 和部分 HTTP/1.0 想出了持久连接的方法。**其特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。旨在建立一次 TCP 连接后进行多次请求和响应的交互。**在 HTTP/1.1 中,所有的连接默认都是持久连接。\n7.管线化 持久连接使得多数请求以管线化方式发送成为可能。以前发送请求后需等待并接收到响应,才能发送下一个请求。管线化技术出现后,不用等待亦可发送下一个请求。这样就能做到同时并行发送多个请求,而不需要一个接一个地等待响应了。\n比如,当请求一个包含多张图片的 HTML 页面时,与挨个连接相比,用持久连接可以让请求更快结束。而管线化技术要比持久连接速度更快。请求数越多,时间差就越明显。\n四、HTTP 协议报文结构 1.HTTP 报文 用于 HTTP 协议交互的信息被称为 HTTP 报文。请求端(客户端)的 HTTP 报文叫做请求报文;响应端(服务器端)的叫做响应报文。HTTP 报文本身是由多行(用 CR+LF 作换行符)数据构成的字符串文本。\n2.HTTP 报文结构 HTTP 报文大致可分为报文首部和报文主体两部分。两者由最初出现的空行(CR+LF)来划分。通常,并不一定有报文主体。如下:\nHTTP 报文结构\n2.1请求报文结构 请求报文结构\n请求报文的首部内容由以下数据组成:\n 请求行 —— 包含用于请求的方法、请求 URI 和 HTTP 版本。 首部字段 —— 包含表示请求的各种条件和属性的各类首部。(通用首部、请求首部、实体首部以及RFC里未定义的首部如 Cookie 等) 请求报文的示例,如下:\n请求报文示例\n2.2响应报文结构 响应报文结构\n响应报文的首部内容由以下数据组成:\n 状态行 —— 包含表明响应结果的状态码、原因短语和 HTTP 版本。 首部字段 —— 包含表示请求的各种条件和属性的各类首部。(通用首部、响应首部、实体首部以及RFC里未定义的首部如 Cookie 等) 响应报文的示例,如下:\n响应报文示例\n五、HTTP 报文首部之请求行、状态行 1.请求行 举个栗子,下面是一个 HTTP 请求的报文:\nGET /index.htm HTTP/1.1 Host: sample.com 其中,下面的这行就是请求行,\nGET /index.htm HTTP/1.1 开头的 GET 表示请求访问服务器的类型,称为方法; 随后的字符串 /index.htm 指明了请求访问的资源对象,也叫做请求 URI; 最后的 HTTP/1.1,即 HTTP 的版本号,用来提示客户端使用的 HTTP 协议功能。 综合来看,大意是请求访问某台 HTTP 服务器上的 /index.htm 页面资源。\n2.状态行 同样举个栗子,下面是一个 HTTP 响应的报文:\n1 2 3 4 5 6 7 HTTP/1.1 200 OK Date: Mon, 10 Jul 2017 15:50:06 GMT Content-Length: 256 Content-Type: text/html \u0026lt;html\u0026gt; ... 其中,下面的这行就是状态行,\nHTTP/1.1 200 OK 开头的 HTTP/1.1 表示服务器对应的 HTTP 版本; 紧挨着的 200 OK 表示请求的处理结果的状态码和原因短语。 六、HTTP 报文首部之首部字段(重点分析) 1.首部字段概述 先来回顾一下首部字段在报文的位置,HTTP 报文包含报文首部和报文主体,报文首部包含请求行(或状态行)和首部字段。\n在报文众多的字段当中,HTTP 首部字段包含的信息最为丰富。首部字段同时存在于请求和响应报文内,并涵盖 HTTP 报文相关的内容信息。使用首部字段是为了给客服端和服务器端提供报文主体大小、所使用的语言、认证信息等内容。\n2.首部字段结构 HTTP 首部字段是由首部字段名和字段值构成的,中间用冒号“:”分隔。 另外,字段值对应单个 HTTP 首部字段可以有多个值。 当 HTTP 报文首部中出现了两个或以上具有相同首部字段名的首部字段时,这种情况在规范内尚未明确,根据浏览器内部处理逻辑的不同,优先处理的顺序可能不同,结果可能并不一致。 首部字段名 冒号 字段值 Content-Type : text/html Keep-Alive : timeout=30, max=120 3.首部字段类型 首部字段根据实际用途被分为以下4种类型:\n 类型 描述 通用首部字段 请求报文和响应报文两方都会使用的首部 请求首部字段 从客户端向服务器端发送请求报文时使用的首部。补充了请求的附加内容、客户端信息、响应内容相关优先级等信息 响应首部字段 从服务器端向客户端返回响应报文时使用的首部。补充了响应的附加内容,也会要求客户端附加额外的内容信息。 实体首部字段 针对请求报文和响应报文的实体部分使用的首部。补充了资源内容更新时间等与实体有关的的信息。 4.通用首部字段(HTTP/1.1) 首部字段名 说明 Cache-Control 控制缓存的行为 Connection 逐挑首部、连接的管理 Date 创建报文的日期时间 Pragma 报文指令 Trailer 报文末端的首部一览 Transfer-Encoding 指定报文主体的传输编码方式 Upgrade 升级为其他协议 Via 代理服务器的相关信息 Warning 错误通知 4.1 Cache-Control 通过指定首部字段 Cache-Control 的指令,就能操作缓存的工作机制。\n4.1.1 可用的指令一览 可用的指令按请求和响应分类如下:\n缓存请求指令\n 指令 参数 说明 no-cache 无 强制向服务器再次验证 no-store 无 不缓存请求或响应的任何内容 max-age = [秒] 必需 响应的最大Age值 max-stale( =[秒]) 可省略 接收已过期的响应 min-fresh = [秒] 必需 期望在指定时间内的响应仍有效 no-transform 无 代理不可更改媒体类型 only-if-cached 无 从缓存获取资源 cache-extension - 新指令标记(token) 缓存响应指令\n 指令 参数 说明 public 无 可向任意方提供响应的缓存 private 可省略 仅向特定用户返回响应 no-cache 可省略 缓存前必须先确认其有效性 no-store 无 不缓存请求或响应的任何内容 no-transform 无 代理不可更改媒体类型 must-revalidate 无 可缓存但必须再向源服务器进行确认 proxy-revalidate 无 要求中间缓存服务器对缓存的响应有效性再进行确认 max-age = [秒] 必需 响应的最大Age值 s-maxage = [秒] 必需 公共缓存服务器响应的最大Age值 cache-extension - 新指令标记(token) 4.1.2 表示能否缓存的指令 public 指令\nCache-Control: public\n当指定使用 public 指令时,则明确表明其他用户也可利用缓存。\nprivate 指令\nCache-Control: private\n当指定 private 指令后,响应只以特定的用户作为对象,这与 public 指令的行为相反。缓存服务器会对该特定用户提供资源缓存的服务,对于其他用户发送过来的请求,代理服务器则不会返回缓存。\nno-cache 指令\nCache-Control: no-cache\n 使用 no-cache 指令是为了防止从缓存中返回过期的资源。 客户端发送的请求中如果包含 no-cache 指令,则表示客户端将不会接收缓存过的响应。于是,“中间”的缓存服务器必须把客户端请求转发给源服务器。 如果服务器中返回的响应包含 no-cache 指令,那么缓存服务器不能对资源进行缓存。源服务器以后也将不再对缓存服务器请求中提出的资源有效性进行确认,且禁止其对响应资源进行缓存操作。 Cache-Control: no-cache=Location\n由服务器返回的响应中,若报文首部字段 Cache-Control 中对 no-cache 字段名具体指定参数值,那么客户端在接收到这个被指定参数值的首部字段对应的响应报文后,就不能使用缓存。换言之,无参数值的首部字段可以使用缓存。只能在响应指令中指定该参数。\nno-store 指令\nCache-Control: no-store\n当使用 no-store 指令时,暗示请求(和对应的响应)或响应中包含机密信息。因此,该指令规定缓存不能在本地存储请求或响应的任一部分。\n注意:no-cache 指令代表不缓存过期的指令,缓存会向源服务器进行有效期确认后处理资源;no-store 指令才是真正的不进行缓存。\n4.1.3 指定缓存期限和认证的指令 s-maxage 指令\nCache-Control: s-maxage=604800(单位:秒)\n s-maxage 指令的功能和 max-age 指令的相同,它们的不同点是 s-maxage 指令只适用于供多位用户使用的公共缓存服务器(一般指代理)。也就是说,对于向同一用户重复返回响应的服务器来说,这个指令没有任何作用。 另外,当使用 s-maxage 指令后,则直接忽略对 Expires 首部字段及 max-age 指令的处理。 max-age 指令\nCache-Control: max-age=604800(单位:秒)\n 当客户端发送的请求中包含 max-age 指令时,如果判定缓存资源的缓存时间数值比指定的时间更小,那么客户端就接收缓存的资源。另外,当指定 max-age 的值为0,那么缓存服务器通常需要将请求转发给源服务器。 当服务器返回的响应中包含 max-age 指令时,缓存服务器将不对资源的有效性再作确认,而 max-age 数值代表资源保存为缓存的最长时间。 应用 HTTP/1.1 版本的缓存服务器遇到同时存在 Expires 首部字段的情况时,会优先处理 max-age 指令,并忽略掉 Expires 首部字段;而 HTTP/1.0 版本的缓存服务器则相反。 min-fresh 指令\nCache-Control: min-fresh=60(单位:秒)\nmin-fresh 指令要求缓存服务器返回至少还未过指定时间的缓存资源。\nmax-stale 指令\nCache-Control: max-stale=3600(单位:秒)\n 使用 max-stale 可指示缓存资源,即使过期也照常接收。 如果指令未指定参数值,那么无论经过多久,客户端都会接收响应;如果指定了具体参数值,那么即使过期,只要仍处于 max-stale 指定的时间内,仍旧会被客户端接收。 only-if-cached 指令\nCache-Control: only-if-cached\n表示客户端仅在缓存服务器本地缓存目标资源的情况下才会要求其返回。换言之,该指令要求缓存服务器不重新加载响应,也不会再次确认资源的有效性。\nmust-revalidate 指令\nCache-Control: must-revalidate\n使用 must-revalidate 指令,代理会向源服务器再次验证即将返回的响应缓存目前是否仍有效。另外,使用 must-revalidate 指令会忽略请求的 max-stale 指令。\nproxy-revalidate 指令\nCache-Control: proxy-revalidate\nproxy-revalidate 指令要求所有的缓存服务器在接收到客户端带有该指令的请求返回响应之前,必须再次验证缓存的有效性。\nno-transform 指令\nCache-Control: no-transform\n使用 no-transform 指令规定无论是在请求还是响应中,缓存都不能改变实体主体的媒体类型。这样做可防止缓存或代理压缩图片等类似操作。\n4.1.4 Cache-Control 扩展 Cache-Control: private, community=\u0026quot;UCI\u0026quot;\n通过 cache-extension 标记(token),可以扩展 Cache-Control 首部字段内的指令。上述 community 指令即扩展的指令,如果缓存服务器不能理解这个新指令,就会直接忽略掉。\n4.2 Connection Connection 首部字段具备以下两个作用:\n控制不再转发的首部字段\nConnection: Upgrade\n在客户端发送请求和服务器返回响应中,使用 Connection 首部字段,可控制不再转发给代理的首部字段,即删除后再转发(即Hop-by-hop首部)。\n管理持久连接\nConnection: close\nHTTP/1.1 版本的默认连接都是持久连接。当服务器端想明确断开连接时,则指定 Connection 首部字段的值为 close。\nConnection: Keep-Alive\nHTTP/1.1 之前的 HTTP 版本的默认连接都是非持久连接。为此,如果想在旧版本的 HTTP 协议上维持持续连接,则需要指定 Connection 首部字段的值为 Keep-Alive。\n4.3 Date 表明创建 HTTP 报文的日期和时间。\nDate: Mon, 10 Jul 2017 15:50:06 GMT\nHTTP/1.1 协议使用在 RFC1123 中规定的日期时间的格式。\n4.4 Pragma Pragma 首部字段是 HTTP/1.1 版本之前的历史遗留字段,仅作为与 HTTP/1.0 的向后兼容而定义。\nPragma: no-cache\n 该首部字段属于通用首部字段,但只用在客户端发送的请求中,要求所有的中间服务器不返回缓存的资源。 所有的中间服务器如果都能以 HTTP/1.1 为基准,那直接采用 Cache-Control: no-cache 指定缓存的处理方式最为理想。但是要整体掌握所有中间服务器使用的 HTTP 协议版本却是不现实的,所以,发送的请求会同时包含下面两个首部字段: Cache-Control: no-cache Pragma: no-cache 4.5 Trailer Trailer: Expires\n首部字段 Trailer 会事先说明在报文主体后记录了哪些首部字段。可应用在 HTTP/1.1 版本分块传输编码时。\n4.6 Transfer-Encoding Transfer-Encoding: chunked\n 规定了传输报文主体时采用的编码方式。 HTTP/1.1 的传输编码方式仅对分块传输编码有效。 4.7 Upgrade Upgrade: TSL/1.0\n用于检测 HTTP 协议及其他协议是否可使用更高的版本进行通信,其参数值可以用来指定一个完全不同的通信协议。\n4.8 Via Via: 1.1 a1.sample.com(Squid/2.7)\n 为了追踪客户端和服务器端之间的请求和响应报文的传输路径。 报文经过代理或网关时,会现在首部字段 Via 中附加该服务器的信息,然后再进行转发。 首部字段 Via 不仅用于追踪报文的转发,还可避免请求回环的发生。 4.9 Warning 该首部字段通常会告知用户一些与缓存相关的问题的警告。\nWarning 首部字段的格式如下:\nWarning:[警告码][警告的主机:端口号] \u0026quot;[警告内容]\u0026quot;([日期时间])\n最后的日期时间可省略。\nHTTP/1.1 中定义了7种警告,警告码对应的警告内容仅推荐参考,另外,警告码具备扩展性,今后有可能追加新的警告码。\n 警告码 警告内容 说明 110 Response is stale(响应已过期) 代理返回已过期的资源 111 Revalidation failed(再验证失败) 代理再验证资源有效性时失败(服务器无法到达等原因) 112 Disconnection operation(断开连接操作) 代理与互联网连接被故意切断 113 Heuristic expiration(试探性过期) 响应的试用期超过24小时(有效缓存的设定时间大于24小时的情况下) 199 Miscellaneous warning(杂项警告) 任意的警告内容 214 Transformation applied(使用了转换) 代理对内容编码或媒体类型等执行了某些处理时 299 Miscellaneous persistent warning(持久杂项警告) 任意的警告内容 5. 请求首部字段(HTTP/1.1) 首部字段名 说明 Accept 用户代理可处理的媒体类型 Accept-Charset 优先的字符集 Accept-Encoding 优先的内容编码 Accept-Language 优先的语言(自然语言) Authorization Web认证信息 Expect 期待服务器的特定行为 From 用户的电子邮箱地址 Host 请求资源所在服务器 If-Match 比较实体标记(ETag) If-Modified-Since 比较资源的更新时间 If-None-Match 比较实体标记(与 If-Macth 相反) If-Range 资源未更新时发送实体 Byte 的范围请求 If-Unmodified-Since 比较资源的更新时间(与 If-Modified-Since 相反) Max-Forwards 最大传输逐跳数 Proxy-Authorization 代理服务器要求客户端的认证信息 Range 实体的字节范围请求 Referer 对请求中 URI 的原始获取方 TE 传输编码的优先级 User-Agent HTTP 客户端程序的信息 5.1 Accept Accept: text/html, application/xhtml+xml, application/xml; q=0.5\n Accept 首部字段可通知服务器,用户代理能够处理的媒体类型及媒体类型的相对优先级。可使用 type/subtype 这种形式,一次指定多种媒体类型。 若想要给显示的媒体类型增加优先级,则使用 q=[数值] 来表示权重值,用分号(;)进行分隔。权重值的范围 0~1(可精确到小数点后三位),且 1 为最大值。不指定权重值时,默认为 1。 5.2 Accept-Charset Accept-Charset: iso-8859-5, unicode-1-1; q=0.8\nAccept-Charset 首部字段可用来通知服务器用户代理支持的字符集及字符集的相对优先顺序。另外,可一次性指定多种字符集。同样使用 q=[数值] 来表示相对优先级。\n5.3 Accept-Encoding Accept-Encoding: gzip, deflate\nAccept-Encoding 首部字段用来告知服务器用户代理支持的内容编码及内容编码的优先顺序,并可一次性指定多种内容编码。同样使用 q=[数值] 来表示相对优先级。也可使用星号(*)作为通配符,指定任意的编码格式。\n5.4 Accept-Language Accept-Lanuage: zh-cn,zh;q=0.7,en=us,en;q=0.3\n告知服务器用户代理能够处理的自然语言集(指中文或英文等),以及自然语言集的相对优先级,可一次性指定多种自然语言集。同样使用 q=[数值] 来表示相对优先级。\n5.5 Authorization Authorization: Basic ldfKDHKfkDdasSAEdasd==\n告知服务器用户代理的认证信息(证书值)。通常,想要通过服务器认证的用户代理会在接收到返回的 401 状态码响应后,把首部字段 Authorization 加入请求中。共用缓存在接收到含有 Authorization 首部字段的请求时的操作处理会略有差异。\n5.6 Expect Expect: 100-continue\n告知服务器客户端期望出现的某种特定行为。\n5.7 From From: [email protected]\n告知服务器使用用户代理的电子邮件地址。\n5.8 Host Host: www.jianshu.com\n 告知服务器,请求的资源所处的互联网主机和端口号。 Host 首部字段是 HTTP/1.1 规范内唯一一个必须被包含在请求内的首部字段。 若服务器未设定主机名,那直接发送一个空值即可 Host: 。 5.9 If-Match 形如 If-xxx 这种样式的请求首部字段,都可称为条件请求。服务器接收到附带条件的请求后,只有判断指定条件为真时,才会执行请求。\nIf-Match: \u0026quot;123456\u0026quot;\n 首部字段 If-Match,属附带条件之一,它会告知服务器匹配资源所用的实体标记(ETag)值。这时的服务器无法使用弱 ETag 值。 服务器会比对 If-Match 的字段值和资源的 ETag 值,仅当两者一致时,才会执行请求。反之,则返回状态码 412 Precondition Failed 的响应。 还可以使用星号(*)指定 If-Match 的字段值。针对这种情况,服务器将会忽略 ETag 的值,只要资源存在就处理请求。 5.10 If-Modified-Since If-Modified-Since: Mon, 10 Jul 2017 15:50:06 GMT\n 首部字段 If-Modified-Since,属附带条件之一,用于确认代理或客户端拥有的本地资源的有效性。 它会告知服务器若 If-Modified-Since 字段值早于资源的更新时间,则希望能处理该请求。而在指定 If-Modified-Since 字段值的日期时间之后,如果请求的资源都没有过更新,则返回状态码 304 Not Modified 的响应。 5.11 If-None-Match If-None-Match: \u0026quot;123456\u0026quot;\n首部字段 If-None-Match 属于附带条件之一。它和首部字段 If-Match 作用相反。用于指定 If-None-Match 字段值的实体标记(ETag)值与请求资源的 ETag 不一致时,它就告知服务器处理该请求。\n5.12 If-Range If-Range: \u0026quot;123456\u0026quot;\n 首部字段 If-Range 属于附带条件之一。它告知服务器若指定的 If-Range 字段值(ETag 值或者时间)和请求资源的 ETag 值或时间相一致时,则作为范围请求处理。反之,则返回全体资源。 下面我们思考一下不使用首部字段 If-Range 发送请求的情况。服务器端的资源如果更新,那客户端持有资源中的一部分也会随之无效,当然,范围请求作为前提是无效的。这时,服务器会暂且以状态码 412 Precondition Failed 作为响应返回,其目的是催促客户端再次发送请求。这样一来,与使用首部字段 If-Range 比起来,就需要花费两倍的功夫。 5.13 If-Unmodified-Since If-Unmodified-Since: Mon, 10 Jul 2017 15:50:06 GMT\n首部字段 If-Unmodified-Since 和首部字段 If-Modified-Since 的作用相反。它的作用的是告知服务器,指定的请求资源只有在字段值内指定的日期时间之后,未发生更新的情况下,才能处理请求。如果在指定日期时间后发生了更新,则以状态码 412 Precondition Failed 作为响应返回。\n5.14 Max-Forwards Max-Forwards: 10\n通过 TRACE 方法或 OPTIONS 方法,发送包含首部字段 Max-Forwards 的请求时,该字段以十进制整数形式指定可经过的服务器最大数目。服务器在往下一个服务器转发请求之前,Max-Forwards 的值减 1 后重新赋值。当服务器接收到 Max-Forwards 值为 0 的请求时,则不再进行转发,而是直接返回响应。\n5.15 Proxy-Authorization Proxy-Authorization: Basic dGlwOjkpNLAGfFY5\n 接收到从代理服务器发来的认证质询时,客户端会发送包含首部字段 Proxy-Authorization 的请求,以告知服务器认证所需要的信息。 这个行为是与客户端和服务器之间的 HTTP 访问认证相类似的,不同之处在于,认证行为发生在客户端与代理之间。 5.16 Range Range: bytes=5001-10000\n 对于只需获取部分资源的范围请求,包含首部字段 Range 即可告知服务器资源的指定范围。 接收到附带 Range 首部字段请求的服务器,会在处理请求之后返回状态码为 206 Partial Content 的响应。无法处理该范围请求时,则会返回状态码 200 OK 的响应及全部资源。 5.17 Referer Referer: http://www.sample.com/index.html\n首部字段 Referer 会告知服务器请求的原始资源的 URI。\n5.18 TE TE: gzip, deflate; q=0.5\n 首部字段 TE 会告知服务器客户端能够处理响应的传输编码方式及相对优先级。它和首部字段 Accept-Encoding 的功能很相像,但是用于传输编码。 首部字段 TE 除指定传输编码之外,还可以指定伴随 trailer 字段的分块传输编码的方式。应用后者时,只需把 trailers 赋值给该字段值。TE: trailers 5.19 User-Agent User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:13.0) Gecko/20100101\n 首部字段 User-Agent 会将创建请求的浏览器和用户代理名称等信息传达给服务器。 由网络爬虫发起请求时,有可能会在字段内添加爬虫作者的电子邮件地址。此外,如果请求经过代理,那么中间也很可能被添加上代理服务器的名称。 6. 响应首部字段(HTTP/1.1) 首部字段名 说明 Accept-Ranges 是否接受字节范围请求 Age 推算资源创建经过时间 ETag 资源的匹配信息 Location 令客户端重定向至指定 URI Proxy-Authenticate 代理服务器对客户端的认证信息 Retry-After 对再次发起请求的时机要求 Server HTTP 服务器的安装信息 Vary 代理服务器缓存的管理信息 WWW-Authenticate 服务器对客户端的认证信息 6.1 Accept-Ranges Accept-Ranges: bytes\n 首部字段 Accept-Ranges 是用来告知客户端服务器是否能处理范围请求,以指定获取服务器端某个部分的资源。 可指定的字段值有两种,可处理范围请求时指定其为 bytes,反之则指定其为 none。 6.2 Age Age: 1200\n 首部字段 Age 能告知客户端,源服务器在多久前创建了响应。字段值的单位为秒。 若创建该响应的服务器是缓存服务器,Age 值是指缓存后的响应再次发起认证到认证完成的时间值。代理创建响应时必须加上首部字段 Age。 6.3 ETag ETag: \u0026quot;usagi-1234\u0026quot;\n 首部字段 ETag 能告知客户端实体标识。它是一种可将资源以字符串形式做唯一性标识的方式。服务器会为每份资源分配对应的 ETag 值。 另外,当资源更新时,ETag 值也需要更新。生成 ETag 值时,并没有统一的算法规则,而仅仅是由服务器来分配。 ETag 中有强 ETag 值和弱 ETag 值之分。强 ETag 值,不论实体发生多么细微的变化都会改变其值;弱 ETag 值只用于提示资源是否相同。只有资源发生了根本改变,产生差异时才会改变 ETag 值。这时,会在字段值最开始处附加 W/: ETag: W/\u0026quot;usagi-1234\u0026quot;。 6.4 Location Location: http://www.sample.com/sample.html\n 使用首部字段 Location 可以将响应接收方引导至某个与请求 URI 位置不同的资源。 基本上,该字段会配合 3xx :Redirection 的响应,提供重定向的 URI。 几乎所有的浏览器在接收到包含首部字段 Location 的响应后,都会强制性地尝试对已提示的重定向资源的访问。 6.5 Proxy-Authenticate Proxy-Authenticate: Basic realm=\u0026quot;Usagidesign Auth\u0026quot;\n 首部字段 Proxy-Authenticate 会把由代理服务器所要求的认证信息发送给客户端。 它与客户端和服务器之间的 HTTP 访问认证的行为相似,不同之处在于其认证行为是在客户端与代理之间进行的。 6.6 Retry-After Retry-After: 180\n 首部字段 Retry-After 告知客户端应该在多久之后再次发送请求。主要配合状态码 503 Service Unavailable 响应,或 3xx Redirect 响应一起使用。 字段值可以指定为具体的日期时间(Mon, 10 Jul 2017 15:50:06 GMT 等格式),也可以是创建响应后的秒数。 6.7 Server Server: Apache/2.2.6 (Unix) PHP/5.2.5\n首部字段 Server 告知客户端当前服务器上安装的 HTTP 服务器应用程序的信息。不单单会标出服务器上的软件应用名称,还有可能包括版本号和安装时启用的可选项。\n6.8 Vary Vary: Accept-Language\n 首部字段 Vary 可对缓存进行控制。源服务器会向代理服务器传达关于本地缓存使用方法的命令。 从代理服务器接收到源服务器返回包含 Vary 指定项的响应之后,若再要进行缓存,仅对请求中含有相同 Vary 指定首部字段的请求返回缓存。即使对相同资源发起请求,但由于 Vary 指定的首部字段不相同,因此必须要从源服务器重新获取资源。 6.9 WWW-Authenticate WWW-Authenticate: Basic realm=\u0026quot;Usagidesign Auth\u0026quot;\n首部字段 WWW-Authenticate 用于 HTTP 访问认证。它会告知客户端适用于访问请求 URI 所指定资源的认证方案(Basic 或是 Digest)和带参数提示的质询(challenge)。\n7. 实体首部字段(HTTP/1.1) 首部字段名 说明 Allow 资源可支持的 HTTP 方法 Content-Encoding 实体主体适用的编码方式 Content-Language 实体主体的自然语言 Content-Length 实体主体的大小(单位:字节) Content-Location 替代对应资源的 URI Content-MD5 实体主体的报文摘要 Content-Range 实体主体的位置范围 Content-Type 实体主体的媒体类型 Expires 实体主体过期的日期时间 Last-Modified 资源的最后修改日期时间 7.1 Allow Allow: GET, HEAD\n 首部字段 Allow 用于通知客户端能够支持 Request-URI 指定资源的所有 HTTP 方法。 当服务器接收到不支持的 HTTP 方法时,会以状态码 405 Method Not Allowed 作为响应返回。与此同时,还会把所有能支持的 HTTP 方法写入首部字段 Allow 后返回。 7.2 Content-Encoding Content-Encoding: gzip\n 首部字段 Content-Encoding 会告知客户端服务器对实体的主体部分选用的内容编码方式。内容编码是指在不丢失实体信息的前提下所进行的压缩。 主要采用这 4 种内容编码的方式(gzip、compress、deflate、identity)。 7.3 Content-Language Content-Language: zh-CN\n首部字段 Content-Language 会告知客户端,实体主体使用的自然语言(指中文或英文等语言)。\n7.4 Content-Length Content-Length: 15000\n首部字段 Content-Length 表明了实体主体部分的大小(单位是字节)。对实体主体进行内容编码传输时,不能再使用 Content-Length首部字段。\n7.5 Content-Location Content-Location: http://www.sample.com/index.html\n首部字段 Content-Location 给出与报文主体部分相对应的 URI。和首部字段 Location 不同,Content-Location 表示的是报文主体返回资源对应的 URI。\n7.6 Content-MD5 Content-MD5: OGFkZDUwNGVhNGY3N2MxMDIwZmQ4NTBmY2IyTY==\n首部字段 Content-MD5 是一串由 MD5 算法生成的值,其目的在于检查报文主体在传输过程中是否保持完整,以及确认传输到达。\n7.7 Content-Range Content-Range: bytes 5001-10000/10000\n针对范围请求,返回响应时使用的首部字段 Content-Range,能告知客户端作为响应返回的实体的哪个部分符合范围请求。字段值以字节为单位,表示当前发送部分及整个实体大小。\n7.8 Content-Type Content-Type: text/html; charset=UTF-8\n首部字段 Content-Type 说明了实体主体内对象的媒体类型。和首部字段 Accept 一样,字段值用 type/subtype 形式赋值。参数 charset 使用 iso-8859-1 或 euc-jp 等字符集进行赋值。\n7.9 Expires Expires: Mon, 10 Jul 2017 15:50:06 GMT\n 首部字段 Expires 会将资源失效的日期告知客户端。 缓存服务器在接收到含有首部字段 Expires 的响应后,会以缓存来应答请求,在 Expires 字段值指定的时间之前,响应的副本会一直被保存。当超过指定的时间后,缓存服务器在请求发送过来时,会转向源服务器请求资源。 源服务器不希望缓存服务器对资源缓存时,最好在 Expires 字段内写入与首部字段 Date 相同的时间值。 7.10 Last-Modified Last-Modified: Mon, 10 Jul 2017 15:50:06 GMT\n首部字段 Last-Modified 指明资源最终修改的时间。一般来说,这个值就是 Request-URI 指定资源被修改的时间。但类似使用 CGI 脚本进行动态数据处理时,该值有可能会变成数据最终修改时的时间。\n8. 为 Cookie 服务的首部字段 首部字段名 说明 首部类型 Set-Cookie 开始状态管理所使用的 Cookie 信息 响应首部字段 Cookie 服务器接收到的 Cookie 信息 请求首部字段 8.1 Set-Cookie Set-Cookie: status=enable; expires=Mon, 10 Jul 2017 15:50:06 GMT; path=/;\n下面的表格列举了 Set-Cookie 的字段值。\n 属性 说明 NAME=VALUE 赋予 Cookie 的名称和其值(必需项) expires=DATE Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止) path=PATH 将服务器上的文件目录作为Cookie的适用对象(若不指定则默认为文档所在的文件目录) domain=域名 作为 Cookie 适用对象的域名 (若不指定则默认为创建 Cookie的服务器的域名) Secure 仅在 HTTPS 安全通信时才会发送 Cookie HttpOnly 加以限制,使 Cookie 不能被 JavaScript 脚本访问 8.1.1 expires 属性 Cookie 的 expires 属性指定浏览器可发送 Cookie 的有效期。 当省略 expires 属性时,其有效期仅限于维持浏览器会话(Session)时间段内。这通常限于浏览器应用程序被关闭之前。 另外,一旦 Cookie 从服务器端发送至客户端,服务器端就不存在可以显式删除 Cookie 的方法。但可通过覆盖已过期的 Cookie,实现对客户端 Cookie 的实质性删除操作。 8.1.2 path 属性 Cookie 的 path 属性可用于限制指定 Cookie 的发送范围的文件目录。\n8.1.3 domain 属性 通过 Cookie 的 domain 属性指定的域名可做到与结尾匹配一致。比如,当指定 example.com 后,除example.com 以外,www.example.com 或 www2.example.com 等都可以发送 Cookie。 因此,除了针对具体指定的多个域名发送 Cookie 之 外,不指定 domain 属性显得更安全。 8.1.4 secure 属性 Cookie 的 secure 属性用于限制 Web 页面仅在 HTTPS 安全连接时,才可以发送 Cookie。\n8.1.5 HttpOnly 属性 Cookie 的 HttpOnly 属性是 Cookie 的扩展功能,它使 JavaScript 脚本无法获得 Cookie。其主要目的为防止跨站脚本攻击(Cross-site scripting,XSS)对 Cookie 的信息窃取。 通过上述设置,通常从 Web 页面内还可以对 Cookie 进行读取操作。但使用 JavaScript 的 document.cookie 就无法读取附加 HttpOnly 属性后的 Cookie 的内容了。因此,也就无法在 XSS 中利用 JavaScript 劫持 Cookie 了。 8.2 Cookie Cookie: status=enable\n首部字段 Cookie 会告知服务器,当客户端想获得 HTTP 状态管理支持时,就会在请求中包含从服务器接收到的 Cookie。接收到多个 Cookie 时,同样可以以多个 Cookie 形式发送。\n9. 其他首部字段 HTTP 首部字段是可以自行扩展的。所以在 Web 服务器和浏览器的应用上,会出现各种非标准的首部字段。\n以下是最为常用的首部字段。\n9.1 X-Frame-Options X-Frame-Options: DENY\n首部字段 X-Frame-Options 属于 HTTP 响应首部,用于控制网站内容在其他 Web 网站的 Frame 标签内的显示问题。其主要目的是为了防止点击劫持(clickjacking)攻击。首部字段 X-Frame-Options 有以下两个可指定的字段值:\n DENY:拒绝 SAMEORIGIN:仅同源域名下的页面(Top-level-browsing-context)匹配时许可。(比如,当指定 http://sample.com/sample.html 页面为 SAMEORIGIN 时,那么 sample.com 上所有页面的 frame 都被允许可加载该页面,而 example.com 等其他域名的页面就不行了) 9.2 X-XSS-Protection X-XSS-Protection: 1\n首部字段 X-XSS-Protection 属于 HTTP 响应首部,它是针对跨站脚本攻击(XSS)的一种对策,用于控制浏览器 XSS 防护机制的开关。首部字段 X-XSS-Protection 可指定的字段值如下:\n 0 :将 XSS 过滤设置成无效状态 1 :将 XSS 过滤设置成有效状态 9.3 DNT DNT: 1\n首部字段 DNT 属于 HTTP 请求首部,其中 DNT 是 Do Not Track 的简称,意为拒绝个人信息被收集,是表示拒绝被精准广告追踪的一种方法。首部字段 DNT 可指定的字段值如下:\n 0 :同意被追踪 1 :拒绝被追踪 由于首部字段 DNT 的功能具备有效性,所以 Web 服务器需要对 DNT做对应的支持。\n9.4 P3P P3P: CP=\u0026quot;CAO DSP LAW CURa ADMa DEVa TAIa PSAa PSDa IVAa IVDa OUR BUS IND\n首部字段 P3P 属于 HTTP 响应首部,通过利用 P3P(The Platform for Privacy Preferences,在线隐私偏好平台)技术,可以让 Web 网站上的个人隐私变成一种仅供程序可理解的形式,以达到保护用户隐私的目的。\n要进行 P3P 的设定,需按以下操作步骤进行:\n 步骤 1:创建 P3P 隐私 步骤 2:创建 P3P 隐私对照文件后,保存命名在 /w3c/p3p.xml 步骤 3:从 P3P 隐私中新建 Compact policies 后,输出到 HTTP 响应中 七、HTTP 响应状态码(重点分析) 1. 状态码概述 HTTP 状态码负责表示客户端 HTTP 请求的返回结果、标记服务器端的处理是否正常、通知出现的错误等工作。 HTTP 状态码如 200 OK ,以 3 位数字和原因短语组成。数字中的第一位指定了响应类别,后两位无分类。 不少返回的响应状态码都是错误的,但是用户可能察觉不到这点。比如 Web 应用程序内部发生错误,状态码依然返回 200 OK。 2. 状态码类别 类别 原因短语 1xx Informational(信息性状态码) 接收的请求正在处理 2xx Success(成功状态码) 请求正常处理完毕 3xx Redirection(重定向状态码) 需要进行附加操作以完成请求 4xx Client Error(客户端错误状态码) 服务器无法处理请求 5xx Server Error(服务器错误状态码) 服务器处理请求出错 我们可以自行改变 RFC2616 中定义的状态码或者服务器端自行创建状态码,只要遵守状态码的类别定义就可以了。\n3. 常用状态码解析 HTTP 状态码种类繁多,数量达几十种。其中最常用的有以下 14 种,一起来看看。\n3.1 200 OK 表示从客户端发来的请求在服务器端被正常处理了。\n3.2 204 No Content 代表服务器接收的请求已成功处理,但在返回的响应报文中不含实体的主体部分。另外,也不允许返回任何实体的主体。 一般在只需要从客户端向服务器端发送消息,而服务器端不需要向客户端发送新消息内容的情况下使用。 3.3 206 Partial Content 表示客户端进行了范围请求,而服务器成功执行了这部分的 GET 请求。响应报文中包含由 Content-Range 首部字段指定范围的实体内容。\n3.4 301 Moved Permanently 永久性重定向。表示请求的资源已被分配了新的 URI。以后应使用资源现在所指的 URI。也就是说,如果已经把资源对应的 URI 保存为书签了,这时应该按 Location 首部字段提示的 URI 重新保存。\n3.5 302 Found 临时性重定向。表示请求的资源已被分配了新的 URI,希望用户(本次)能使用新的 URI 访问。 和 301 Moved Permanently 状态码相似,但 302 Found 状态码代表资源不是被永久移动,只是临时性质的。换句话说,已移动的资源对应的 URI 将来还有可能发生改变。 3.6 303 See Other 表示由于请求的资源存在着另一个 URI,应使用 GET 方法定向获取请求的资源。 303 See Other 和 302 Found 状态码有着相同的功能,但 303 See Other 状态码明确表示客户端应采用 GET 方法获取资源,这点与 302 Found 状态码有区别。 3.7 304 Not Modified 表示客户端发送附带条件的请求时,服务器端允许请求访问的资源,但未满足条件的情况。 304 Not Modified 状态码返回时,不包含任何响应的主体部分。 304 Not Modified 虽然被划分到 3xx 类别中,但和重定向没有关系。 3.8 307 Temporary Redirect 临时重定向。该状态码与 302 Found 有着相同的含义。\n3.9 400 Bad Request 表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。 另外,浏览器会像 200 OK 一样对待该状态码。 3.10 401 Unauthorized 表示发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息。 另外,若之前已进行过 1 次请求,则表示用户认证失败。 返回含有 401 Unauthorized 的响应必须包含一个适用于被请求资源的 WWW-Authenticate 首部用以质询(challenge)用户信息。 3.11 403 Forbidden 表明对请求资源的访问被服务器拒绝了。服务器端没有必要给出详细的拒绝理由,当然也可以在响应报文的实体主体部分对原因进行描述。\n3.12 404 Not Found 表明服务器上无法找到请求的资源。除此之外,也可以在服务器端拒绝请求且不想说明理由的时候使用。\n3.13 500 Internal Server Error 表明服务器端在执行请求时发生了错误。也可能是 Web 应用存在的 bug 或某些临时的故障。\n3.14 503 Service Unavailable 表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。如果事先得知解除以上状况需要的时间,最好写入 Retry-After 首部字段再返回给客户端。\n八、HTTP 报文实体 1. HTTP 报文实体概述 HTTP 报文结构\n大家请仔细看看上面示例中,各个组成部分对应的内容。\n接着,我们来看看报文和实体的概念。如果把 HTTP 报文想象成因特网货运系统中的箱子,那么 HTTP 实体就是报文中实际的货物。\n 报文:是网络中交换和传输的数据单元,即站点一次性要发送的数据块。报文包含了将要发送的完整的数据信息,其长短很不一致,长度不限且可变。 实体:作为请求或响应的有效载荷数据(补充项)被传输,其内容由实体首部和实体主体组成。(实体首部相关内容在上面第六点中已有阐述。) 我们可以看到,上面示例右图中深红色框的内容就是报文的实体部分,而蓝色框的两部分内容分别就是实体首部和实体主体。而左图中粉红框内容就是报文主体。\n通常,报文主体等于实体主体。只有当传输中进行编码操作时,实体主体的内容发生变化,才导致它和报文主体产生差异。\n2. 内容编码 HTTP 应用程序有时在发送之前需要对内容进行编码。例如,在把很大的 HTML 文档发送给通过慢速连接上来的客户端之前,服务器可能会对其进行压缩,这样有助于减少传输实体的时间。服务器还可以把内容搅乱或加密,以此来防止未授权的第三方看到文档的内容。 这种类型的编码是在发送方应用到内容之上的。当内容经过内容编码后,编好码的数据就放在实体主体中,像往常一样发送给接收方。 内容编码类型:\n 编码方式 描述 gzip 表明实体采用 GNU zip 编码 compress 表明实体采用 Unix 的文件压缩程序 deflate 表明实体采用 zlib 的格式压缩的 identity 表明没有对实体进行编码,当没有 Content-Encoding 首部字段时,默认采用此编码方式 3. 传输编码 内容编码是对报文的主体进行的可逆变换,是和内容的具体格式细节紧密相关的。\n传输编码也是作用在实体主体上的可逆变换,但使用它们是由于架构方面的原因,同内容的格式无关。使用传输编码是为了改变报文中的数据在网络上传输的方式。\n内容编码和传输编码的对比\n4. 分块编码 分块编码把报文分割成若干已知大小的块。块之间是紧挨着发送的,这样就不需要在发送之前知道整个报文的大小了。分块编码是一种传输编码,是报文的属性。\n分块编码与持久连接\n若客户端与服务器端之间不是持久连接,客户端就不需要知道它在读取的主体的长度,而只需要读取到服务器关闭主体连接为止。\n当使用持久连接时,在服务器写主体之前,必须知道它的大小并在 Content-Length 首部中发送。如果服务器动态创建内容,就可能在发送之前无法知道主体的长度。\n分块编码为这种困难提供了解决方案,只要允许服务器把主体分块发送,说明每块的大小就可以了。因为主体是动态创建的,服务器可以缓冲它的一部分,发送其大小和相应的块,然后在主体发送完之前重复这个过程。服务器可以用大小为 0 的块作为主体结束的信号,这样就可以继续保持连接,为下一个响应做准备。\n来看看一个分块编码的报文示例:\n分块编码的报文\n5.多部分媒体类型 MIME 中的 multipart(多部分)电子邮件报文中包含多个报文,它们合在一起作为单一的复杂报文发送。每一部分都是独立的,有各自的描述其内容的集,不同部分之间用分界字符串连接在一起。\n相应得,HTTP 协议中也采纳了多部分对象集合,发送的一份报文主体内可包含多种类型实体。\n多部分对象集合包含的对象如下:\n multipart/form-data:在 Web 表单文件上传时使用。 multipart/byteranges:状态码 206 Partial Content 响应报文包含了多个范围的内容时使用。 6. 范围请求 假设你正在下载一个很大的文件,已经下了四分之三,忽然网络中断了,那下载就必须重头再来一遍。为了解决这个问题,需要一种可恢复的机制,即能从之前下载中断处恢复下载。要实现该功能,这就要用到范围请求。\n有了范围请求, HTTP 客户端可以通过请求曾获取失败的实体的一个范围(或者说一部分),来恢复下载该实体。当然这有一个前提,那就是从客户端上一次请求该实体到这一次发出范围请求的时间段内,该对象没有改变过。例如:\nGET /bigfile.html HTTP/1.1 Host: www.sample.com Range: bytes=20224- ··· 实体范围请求示例\n上面示例中,客户端请求的是文档开头20224字节之后的部分。\n九、与 HTTP 协作的 Web 服务器 HTTP 通信时,除客户端和服务器外,还有一些用于协助通信的应用程序。如下列出比较重要的几个:代理、缓存、网关、隧道、Agent 代理。\n1.代理 代理\nHTTP 代理服务器是 Web 安全、应用集成以及性能优化的重要组成模块。代理位于客户端和服务器端之间,接收客户端所有的 HTTP 请求,并将这些请求转发给服务器(可能会对请求进行修改之后再进行转发)。对用户来说,这些应用程序就是一个代理,代表用户访问服务器。\n出于安全考虑,通常会将代理作为转发所有 Web 流量的可信任中间节点使用。代理还可以对请求和响应进行过滤,安全上网或绿色上网。\n2. 缓存 浏览器第一次请求:\n浏览器第一次请求\n浏览器再次请求:\n浏览器再次请求\nWeb 缓存或代理缓存是一种特殊的 HTTP 代理服务器,可以将经过代理传输的常用文档复制保存起来。下一个请求同一文档的客户端就可以享受缓存的私有副本所提供的服务了。客户端从附近的缓存下载文档会比从远程 Web 服务器下载快得多。\n3. 网关 HTTP / FTP 网关\n网关是一种特殊的服务器,作为其他服务器的中间实体使用。通常用于将 HTTP 流量转换成其他的协议。网关接收请求时就好像自己是资源的源服务器一样。客户端可能并不知道自己正在跟一个网关进行通信。\n4. 隧道 HTTP/SSL 隧道\n隧道是会在建立起来之后,就会在两条连接之间对原始数据进行盲转发的 HTTP 应用程序。HTTP 隧道通常用来在一条或多条 HTTP 连接上转发非 HTTP 数据,转发时不会窥探数据。\nHTTP 隧道的一种常见用途就是通过 HTTP 连接承载加密的安全套接字层(SSL)流量,这样 SSL 流量就可以穿过只允许 Web 流量通过的防火墙了。\n5. Agent 代理 自动搜索引擎“网络蜘蛛”\nAgent 代理是代表用户发起 HTTP 请求的客户端应用程序。所有发布 Web 请求的应用程序都是 HTTP Agent 代理。\n后续 参考文章 一篇文章带你详解 HTTP 协议(网络协议篇一)\n学习资料** 《HTTP权威指南》 《图解HTTP》 ","description":"","id":51,"section":"posts","tags":["http"],"title":"详解 HTTP 协议","uri":"https://yichenlove.github.io/posts/http/"},{"content":"熟悉 TCP/IP 协议 一张思维导图\n一图看完本文\n一、 计算机网络体系结构分层 计算机网络体系结构分层\n计算机网络体系结构分层\n不难看出,TCP/IP 与 OSI 在分层模块上稍有区别。OSI 参考模型注重“通信协议必要的功能是什么”,而 TCP/IP 则更强调“在计算机上实现协议应该开发哪种程序”。\n二、 TCP/IP 基础 1. TCP/IP 的具体含义 从字面意义上讲,有人可能会认为 TCP/IP 是指 TCP 和 IP 两种协议。实际生活当中有时也确实就是指这两种协议。然而在很多情况下,它只是利用 IP 进行通信时所必须用到的协议群的统称。具体来说,IP 或 ICMP、TCP 或 UDP、TELNET 或 FTP、以及 HTTP 等都属于 TCP/IP 协议。他们与 TCP 或 IP 的关系紧密,是互联网必不可少的组成部分。TCP/IP 一词泛指这些协议,因此,有时也称 TCP/IP 为网际协议群。\n互联网进行通信时,需要相应的网络协议,TCP/IP 原本就是为使用互联网而开发制定的协议族。因此,互联网的协议就是 TCP/IP,TCP/IP 就是互联网的协议。\n网际协议群\n2. 数据包 包、帧、数据包、段、消息\n以上五个术语都用来表述数据的单位,大致区分如下:\n 包可以说是全能性术语; 帧用于表示数据链路层中包的单位; 数据包是 IP 和 UDP 等网络层以上的分层中包的单位; 段则表示 TCP 数据流中的信息; 消息是指应用协议中数据的单位。 每个分层中,都会对所发送的数据附加一个首部,在这个首部中包含了该层必要的信息,如发送的目标地址以及协议相关信息。通常,为协议提供的信息为包首部,所要发送的内容为数据。在下一层的角度看,从上一层收到的包全部都被认为是本层的数据。\n数据包首部\n网络中传输的数据包由两部分组成:一部分是协议所要用到的首部,另一部分是上一层传过来的数据。首部的结构由协议的具体规范详细定义。在数据包的首部,明确标明了协议应该如何读取数据。反过来说,看到首部,也就能够了解该协议必要的信息以及所要处理的数据。包首部就像协议的脸。\n3. 数据处理流程 下图以用户 a 向用户 b 发送邮件为例子:\n数据处理流程\n ① 应用程序处理\n首先应用程序会进行编码处理,这些编码相当于 OSI 的表示层功能;\n编码转化后,邮件不一定马上被发送出去,这种何时建立通信连接何时发送数据的管理功能,相当于 OSI 的会话层功能。 ② TCP 模块的处理\nTCP 根据应用的指示,负责建立连接、发送数据以及断开连接。TCP 提供将应用层发来的数据顺利发送至对端的可靠传输。为了实现这一功能,需要在应用层数据的前端附加一个 TCP 首部。 ③ IP 模块的处理\nIP 将 TCP 传过来的 TCP 首部和 TCP 数据合起来当做自己的数据,并在 TCP 首部的前端加上自己的 IP 首部。IP 包生成后,参考路由控制表决定接受此 IP 包的路由或主机。 ④ 网络接口(以太网驱动)的处理\n从 IP 传过来的 IP 包对于以太网来说就是数据。给这些数据附加上以太网首部并进行发送处理,生成的以太网数据包将通过物理层传输给接收端。 ⑤ 网络接口(以太网驱动)的处理\n主机收到以太网包后,首先从以太网包首部找到 MAC 地址判断是否为发送给自己的包,若不是则丢弃数据。\n如果是发送给自己的包,则从以太网包首部中的类型确定数据类型,再传给相应的模块,如 IP、ARP 等。这里的例子则是 IP 。 ⑥ IP 模块的处理\nIP 模块接收到 数据后也做类似的处理。从包首部中判断此 IP 地址是否与自己的 IP 地址匹配,如果匹配则根据首部的协议类型将数据发送给对应的模块,如 TCP、UDP。这里的例子则是 TCP。\n另外吗,对于有路由器的情况,接收端地址往往不是自己的地址,此时,需要借助路由控制表,在调查应该送往的主机或路由器之后再进行转发数据。 ⑦ TCP 模块的处理\n在 TCP 模块中,首先会计算一下校验和,判断数据是否被破坏。然后检查是否在按照序号接收数据。最后检查端口号,确定具体的应用程序。数据被完整地接收以后,会传给由端口号识别的应用程序。 ⑧ 应用程序的处理\n接收端应用程序会直接接收发送端发送的数据。通过解析数据,展示相应的内容。 三、传输层中的 TCP 和 UDP TCP/IP 中有两个具有代表性的传输层协议,分别是 TCP 和 UDP。\n TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,当应用程序采用 TCP 发送消息时,虽然可以保证发送的顺序,但还是犹如没有任何间隔的数据流发送给接收端。TCP 为提供可靠性传输,实行“顺序控制”或“重发控制”机制。此外还具备“流控制(流量控制)”、“拥塞控制”、提高网络利用率等众多功能。 UDP 是不具有可靠性的数据报协议。细微的处理它会交给上层的应用去完成。在 UDP 的情况下,虽然可以确保发送消息的大小,却不能保证消息一定会到达。因此,应用有时会根据自己的需要进行重发处理。 TCP 和 UDP 的优缺点无法简单地、绝对地去做比较:TCP 用于在传输层有必要实现可靠传输的情况;而在一方面,UDP 主要用于那些对高速传输和实时性有较高要求的通信或广播通信。TCP 和 UDP 应该根据应用的目的按需使用。 1. 端口号 数据链路和 IP 中的地址,分别指的是 MAC 地址和 IP 地址。前者用来识别同一链路中不同的计算机,后者用来识别 TCP/IP 网络中互连的主机和路由器。在传输层也有这种类似于地址的概念,那就是端口号。端口号用来识别同一台计算机中进行通信的不同应用程序。因此,它也被称为程序地址。\n1.1 根据端口号识别应用 一台计算机上同时可以运行多个程序。传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确地将数据传输。\n通过端口号识别应用\n1.2 通过 IP 地址、端口号、协议号进行通信识别 仅凭目标端口号识别某一个通信是远远不够的。 通过端口号、IP地址、协议号进行通信识别\n ① 和② 的通信是在两台计算机上进行的。它们的目标端口号相同,都是80。这里可以根据源端口号加以区分。 ③ 和 ① 的目标端口号和源端口号完全相同,但它们各自的源 IP 地址不同。 此外,当 IP 地址和端口号全都一样时,我们还可以通过协议号来区分(TCP 和 UDP)。 1.3 端口号的确定 标准既定的端口号:这种方法也叫静态方法。它是指每个应用程序都有其指定的端口号。但并不是说可以随意使用任何一个端口号。例如 HTTP、FTP、TELNET 等广为使用的应用协议中所使用的端口号就是固定的。这些端口号被称为知名端口号,分布在 0~1023 之间;除知名端口号之外,还有一些端口号被正式注册,它们分布在 1024~49151 之间,不过这些端口号可用于任何通信用途。 时序分配法:服务器有必要确定监听端口号,但是接受服务的客户端没必要确定端口号。在这种方法下,客户端应用程序完全可以不用自己设置端口号,而全权交给操作系统进行分配。动态分配的端口号范围在 49152~65535 之间。 1.4 端口号与协议 端口号由其使用的传输层协议决定。因此,不同的传输层协议可以使用相同的端口号。 此外,那些知名端口号与传输层协议并无关系。只要端口一致都将分配同一种应用程序进行处理。 2. UDP UDP 不提供复杂的控制机制,利用 IP 提供面向无连接的通信服务。 并且它是将应用程序发来的数据在收到的那一刻,立即按照原样发送到网络上的一种机制。即使是出现网络拥堵的情况,UDP 也无法进行流量控制等避免网络拥塞行为。 此外,传输途中出现丢包,UDP 也不负责重发。 甚至当包的到达顺序出现乱序时也没有纠正的功能。 如果需要以上的细节控制,不得不交由采用 UDP 的应用程序去处理。 UDP 常用于一下几个方面:1.包总量较少的通信(DNS、SNMP等);2.视频、音频等多媒体通信(即时通信);3.限定于 LAN 等特定网络中的应用通信;4.广播通信(广播、多播)。 3. TCP TCP 与 UDP 的区别相当大。它充分地实现了数据传输时各种控制功能,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。而这些在 UDP 中都没有。 此外,TCP 作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,从而可以控制通信流量的浪费。 根据 TCP 的这些机制,在 IP 这种无连接的网络上也能够实现高可靠性的通信( 主要通过检验和、序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现)。 3.1 三次握手(重点) TCP 提供面向有连接的通信传输。面向有连接是指在数据通信开始之前先做好两端之间的准备工作。 所谓三次握手是指建立一个 TCP 连接时需要客户端和服务器端总共发送三个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发。 下面来看看三次握手的流程图:\n三次握手\n 第一次握手:客户端将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给服务器端,客户端进入SYN_SENT状态,等待服务器端确认。 第二次握手:服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。 第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。 3.2 四次挥手(重点) 四次挥手即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。 由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。 下面来看看四次挥手的流程图:\n四次挥手\n 中断连接端可以是客户端,也可以是服务器端。 第一次挥手:客户端发送一个FIN=M,用来关闭客户端到服务器端的数据传送,客户端进入FIN_WAIT_1状态。意思是说\u0026quot;我客户端没有数据要发给你了\u0026quot;,但是如果你服务器端还有数据没有发送完成,则不必急着关闭连接,可以继续发送数据。 第二次挥手:服务器端收到FIN后,先发送ack=M+1,告诉客户端,你的请求我收到了,但是我还没准备好,请继续你等我的消息。这个时候客户端就进入FIN_WAIT_2 状态,继续等待服务器端的FIN报文。 第三次挥手:当服务器端确定数据已发送完成,则向客户端发送FIN=N报文,告诉客户端,好了,我这边数据发完了,准备好关闭连接了。服务器端进入LAST_ACK状态。 第四次挥手:客户端收到FIN=N报文后,就知道可以关闭连接了,但是他还是不相信网络,怕服务器端不知道要关闭,所以发送ack=N+1后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。服务器端收到ACK后,就知道可以断开连接了。客户端等待了2MSL后依然没有收到回复,则证明服务器端已正常关闭,那好,我客户端也可以关闭连接了。最终完成了四次握手。 上面是一方主动关闭,另一方被动关闭的情况,实际中还会出现同时发起主动关闭的情况,\n具体流程如下图:\n同时挥手\n3.3 通过序列号与确认应答提高可靠性 在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个已收到消息的通知。这个消息叫做确认应答(ACK)。当发送端将数据发出之后会等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端。反之,则数据丢失的可能性很大。 在一定时间内没有等待到确认应答,发送端就可以认为数据已经丢失,并进行重发。由此,即使产生了丢包,仍然能够保证数据能够到达对端,实现可靠传输。 未收到确认应答并不意味着数据一定丢失。也有可能是数据对方已经收到,只是返回的确认应答在途中丢失。这种情况也会导致发送端误以为数据没有到达目的地而重发数据。 此外,也有可能因为一些其他原因导致确认应答延迟到达,在源主机重发数据以后才到达的情况也屡见不鲜。此时,源主机只要按照机制重发数据即可。 对于目标主机来说,反复收到相同的数据是不可取的。为了对上层应用提供可靠的传输,目标主机必须放弃重复的数据包。为此我们引入了序列号。 序列号是按照顺序给发送数据的每一个字节(8位字节)都标上号码的编号。接收端查询接收数据 TCP 首部中的序列号和数据的长度,将自己下一步应该接收的序列号作为确认应答返送回去。通过序列号和确认应答号,TCP 能够识别是否已经接收数据,又能够判断是否需要接收,从而实现可靠传输。 序列号和确认应答\n3.4 重发超时的确定 **重发超时是指在重发数据之前,等待确认应答到来的那个特定时间间隔。**如果超过这个时间仍未收到确认应答,发送端将进行数据重发。最理想的是,找到一个最小时间,它能保证“确认应答一定能在这个时间内返回”。 TCP 要求不论处在何种网络环境下都要提供高性能通信,并且无论网络拥堵情况发生何种变化,都必须保持这一特性。为此,它在每次发包时都会计算往返时间及其偏差。将这个往返时间和偏差时间相加,重发超时的时间就是比这个总和要稍大一点的值。 在 BSD 的 Unix 以及 Windows 系统中,超时都以0.5秒为单位进行控制,因此重发超时都是0.5秒的整数倍。不过,最初其重发超时的默认值一般设置为6秒左右。 数据被重发之后若还是收不到确认应答,则进行再次发送。此时,等待确认应答的时间将会以2倍、4倍的指数函数延长。 此外,数据也不会被无限、反复地重发。达到一定重发次数之后,如果仍没有任何确认应答返回,就会判断为网络或对端主机发生了异常,强制关闭连接。并且通知应用通信异常强行终止。 3.5 以段为单位发送数据 在建立 TCP 连接的同时,也可以确定发送数据包的单位,我们也可以称其为“最大消息长度”(MSS)。最理想的情况是,最大消息长度正好是 IP 中不会被分片处理的最大数据长度。 TCP 在传送大量数据时,是以 MSS 的大小将数据进行分割发送。进行重发时也是以 MSS 为单位。 MSS 在三次握手的时候,在两端主机之间被计算得出。两端的主机在发出建立连接的请求时,会在 TCP 首部中写入 MSS 选项,告诉对方自己的接口能够适应的 MSS 的大小。然后会在两者之间选择一个较小的值投入使用。 3.6 利用窗口控制提高速度 TCP 以1个段为单位,每发送一个段进行一次确认应答的处理。这样的传输方式有一个缺点,就是包的往返时间越长通信性能就越低。\n 为解决这个问题,TCP 引入了窗口这个概念。确认应答不再是以每个分段,而是以更大的单位进行确认,转发时间将会被大幅地缩短。也就是说,发送端主机,在发送了一个段以后不必要一直等待确认应答,而是继续发送。如下图所示:\n 窗口控制\n 窗口大小就是指无需等待确认应答而可以继续发送数据的最大值。上图中窗口大小为4个段。这个机制实现了使用大量的缓冲区,通过对多个段同时进行确认应答的功能。 3.7 滑动窗口控制 滑动窗口\n 上图中的窗口内的数据即便没有收到确认应答也可以被发送出去。不过,在整个窗口的确认应答没有到达之前,如果其中部分数据出现丢包,那么发送端仍然要负责重传。为此,发送端主机需要设置缓存保留这些待被重传的数据,直到收到他们的确认应答。 在滑动窗口以外的部分包括未发送的数据以及已经确认对端已收到的数据。当数据发出后若如期收到确认应答就可以不用再进行重发,此时数据就可以从缓存区清除。 收到确认应答的情况下,将窗口滑动到确认应答中的序列号的位置。这样可以顺序地将多个段同时发送提高通信性能。这种机制也别称为滑动窗口控制。 3.8 窗口控制中的重发控制 在使用窗口控制中, 出现丢包一般分为两种情况:\n ① 确认应答未能返回的情况。在这种情况下,数据已经到达对端,是不需要再进行重发的,如下图: 部分确认应答丢失\n ② 某个报文段丢失的情况。接收主机如果收到一个自己应该接收的序列号以外的数据时,会针对当前为止收到数据返回确认应答。如下图所示,当某一报文段丢失后,发送端会一直收到序号为1001的确认应答,因此,在窗口比较大,又出现报文段丢失的情况下,同一个序列号的确认应答将会被重复不断地返回。而发送端主机如果连续3次收到同一个确认应答,就会将其对应的数据进行重发。这种机制比之前提到的超时管理更加高效,因此也被称为高速重发控制。 高速重发控制\n四、网络层中的 IP 协议 IP(IPv4、IPv6)相当于 OSI 参考模型中的第3层——网络层。网络层的主要作用是“实现终端节点之间的通信”。这种终端节点之间的通信也叫“点对点通信”。 网络的下一层——数据链路层的主要作用是在互连同一种数据链路的节点之间进行包传递。而一旦跨越多种数据链路,就需要借助网络层。网络层可以跨越不同的数据链路,即使是在不同的数据链路上也能实现两端节点之间的数据包传输。 IP 大致分为三大作用模块,它们是 IP 寻址、路由(最终节点为止的转发)以及 IP 分包与组包。 1. IP 地址 1.1 IP 地址概述 在计算机通信中,为了识别通信对端,必须要有一个类似于地址的识别码进行标识。在数据链路中的 MAC 地址正是用来标识同一个链路中不同计算机的一种识别码。 作为网络层的 IP ,也有这种地址信息,一般叫做 IP 地址。IP 地址用于在“连接到网络中的所有主机中识别出进行通信的目标地址”。因此,在 TCP/IP 通信中所有主机或路由器必须设定自己的 IP 地址。 不论一台主机与哪种数据链路连接,其 IP 地址的形式都保持不变。 IP 地址(IPv4 地址)由32位正整数来表示。IP 地址在计算机内部以二进制方式被处理。然而,由于我们并不习惯于采用二进制方式,我们将32位的 IP 地址以每8位为一组,分成4组,每组以 “.” 隔开,再将每组数转换成十进制数。如下: 28 28 28 28 10101100 00010100 00000001 00000001 (2进制) 10101100. 00010100. 00000001. 00000001 (2进制) 172. 20. 1. 1 (10进制) 1.2 IP 地址由网络和主机两部分标识组成 如下图,网络标识在数据链路的每个段配置不同的值。网络标识必须保证相互连接的每个段的地址不相重复。而相同段内相连的主机必须有相同的网络地址。IP 地址的“主机标识”则不允许在同一个网段内重复出现。由此,可以通过设置网络地址和主机地址,在相互连接的整个网络中保证每台主机的 IP 地址都不会相互重叠。即 IP 地址具有了唯一性。 IP地址的主机标识\n 如下图,IP 包被转发到途中某个路由器时,正是利用目标 IP 地址的网络标识进行路由。因为即使不看主机标识,只要一见到网络标识就能判断出是否为该网段内的主机。 IP地址的网络标识\n1.3 IP 地址的分类 IP 地址分为四个级别,分别为A类、B类、C类、D类。它根据 IP 地址中从第 1 位到第 4 位的比特列对其网络标识和主机标识进行区分。 **A 类 IP 地址是首位以 “0” 开头的地址。**从第 1 位到第 8 位是它的网络标识。用十进制表示的话,0.0.0.0~127.0.0.0 是 A 类的网络地址。A 类地址的后 24 位相当于主机标识。因此,一个网段内可容纳的主机地址上限为16,777,214个。 **B 类 IP 地址是前两位 “10” 的地址。**从第 1 位到第 16 位是它的网络标识。用十进制表示的话,128.0.0.0~191.255.0.0 是 B 类的网络地址。B 类地址的后 16 位相当于主机标识。因此,一个网段内可容纳的主机地址上限为65,534个。 **C 类 IP 地址是前三位为 “110” 的地址。**从第 1 位到第 24 位是它的网络标识。用十进制表示的话,192.0.0.0~223.255.255.0 是 C 类的网络地址。C 类地址的后 8 位相当于主机标识。因此,一个网段内可容纳的主机地址上限为254个。 **D 类 IP 地址是前四位为 “1110” 的地址。**从第 1 位到第 32 位是它的网络标识。用十进制表示的话,224.0.0.0~239.255.255.255 是 D 类的网络地址。D 类地址没有主机标识,常用于多播。 在分配 IP 地址时关于主机标识有一点需要注意。即要用比特位表示主机地址时,不可以全部为 0 或全部为 1。因为全部为 0 只有在表示对应的网络地址或 IP 地址不可以获知的情况下才使用。而全部为 1 的主机通常作为广播地址。因此,在分配过程中,应该去掉这两种情况。这也是为什么 C 类地址每个网段最多只能有 254( 28 - 2 = 254)个主机地址的原因。 1.4 广播地址 广播地址用于在同一个链路中相互连接的主机之间发送数据包。将 IP 地址中的主机地址部分全部设置为 1,就成了广播地址。 广播分为本地广播和直接广播两种。在本网络内的广播叫做本地广播;在不同网络之间的广播叫做直接广播。 1.5 IP 多播 多播用于将包发送给特定组内的所有主机。由于其直接使用 IP 地址,因此也不存在可靠传输。\n 相比于广播,多播既可以穿透路由器,又可以实现只给那些必要的组发送数据包。请看下图:\nIP 多播\n 多播使用 D 类地址。因此,如果从首位开始到第 4 位是 “1110”,就可以认为是多播地址。而剩下的 28 位可以成为多播的组编号。\n 此外, 对于多播,所有的主机(路由器以外的主机和终端主机)必须属于 224.0.0.1 的组,所有的路由器必须属于 224.0.0.2 的组。\n 1.6 子网掩码 现在一个 IP 地址的网络标识和主机标识已不再受限于该地址的类别,而是由一个叫做“子网掩码”的识别码通过子网网络地址细分出比 A 类、B 类、C 类更小粒度的网络。这种方式实际上就是将原来 A 类、B 类、C 类等分类中的主机地址部分用作子网地址,可以将原网络分为多个物理网络的一种机制。 子网掩码用二进制方式表示的话,也是一个 32 位的数字。它对应 IP 地址网络标识部分的位全部为 “1”,对应 IP 地址主机标识的部分则全部为 “0”。由此,一个 IP 地址可以不再受限于自己的类别,而是可以用这样的子网掩码自由地定位自己的网络标识长度。当然,子网掩码必须是 IP 地址的首位开始连续的 “1”。 对于子网掩码,目前有两种表示方式。第一种是,将 IP 地址与子网掩码的地址分别用两行来表示。以 172.20.100.52 的前 26 位是网络地址的情况为例,如下: IP 地址 172. 20. 100. 52 子网掩码 255. 255. 255. 192 网络地址 172. 20. 100. 0 子网掩码 255. 255. 255. 192 广播地址 172. 20. 100. 63 子网掩码 255. 255. 255. 192 第二种表示方式是,在每个 IP 地址后面追加网络地址的位数用 “/ ” 隔开,如下: IP 地址 172. 20. 100. 52 / 26 网络地址 172. 20. 100. 0 / 26 广播地址 172. 20. 100. 63 / 26 另外,在第二种方式下记述网络地址时可以省略后面的 “0” 。例如:172.20.0.0/26 跟 172.20/26 其实是一个意思。 2. 路由 发送数据包时所使用的地址是网络层的地址,即 IP 地址。然而仅仅有 IP 地址还不足以实现将数据包发送到对端目标地址,在数据发送过程中还需要类似于“指明路由器或主机”的信息,以便真正发往目标地址。保存这种信息的就是路由控制表。 该路由控制表的形成方式有两种:一种是管理员手动设置,另一种是路由器与其他路由器相互交换信息时自动刷新。前者也叫做静态路由控制,而后者叫做动态路由控制。 IP 协议始终认为路由表是正确的。然后,IP 本身并没有定义制作路由控制表的协议。即 IP 没有制作路由控制表的机制。该表示由一个叫做“路由协议”的协议制作而成。 2.1 IP 地址与路由控制 IP 地址的网络地址部分用于进行路由控制。 路由控制表中记录着网络地址与下一步应该发送至路由器的地址。 在发送 IP 包时,首先要确定 IP 包首部中的目标地址,再从路由控制表中找到与该地址具有相同网络地址的记录,根据该记录将 IP 包转发给相应的下一个路由器。如果路由控制表中存在多条相同网络地址的记录,就选择一个最为吻合的网络地址。 路由控制表与 IP 包发送\n3. IP 分包与组包 每种数据链路的最大传输单元(MTU)都不尽相同,因为每个不同类型的数据链路的使用目的不同。使用目的不同,可承载的 MTU 也就不同。 任何一台主机都有必要对 IP 分片进行相应的处理。分片往往在网络上遇到比较大的报文无法一下子发送出去时才会进行处理。 经过分片之后的 IP 数据报在被重组的时候,只能由目标主机进行。路由器虽然做分片但不会进行重组。 3.1 路径 MTU 发现 分片机制也有它的不足。如路由器的处理负荷加重之类。因此,只要允许,是不希望由路由器进行 IP 数据包的分片处理的。 为了应对分片机制的不足,“路径 MTU 发现” 技术应运而生。路径 MTU 指的是,从发送端主机到接收端主机之间不需要分片是最大 MTU 的大小。即路径中存在的所有数据链路中最小的 MTU 。 进行路径 MTU 发现,就可以避免在中途的路由器上进行分片处理,也可以在 TCP 中发送更大的包。 4. IPv6 IPv6(IP version 6)是为了根本解决 IPv4 地址耗尽的问题而被标准化的网际协议。IPv4 的地址长度为 4 个 8 位字节,即 32 比特。而 IPv6 的地址长度则是原来的 4 倍,即 128 比特,一般写成 8 个 16 位字节。 4.1 IPv6 的特点 IP 得知的扩大与路由控制表的聚合。 性能提升。包首部长度采用固定的值(40字节),不再采用首部检验码。简化首部结构,减轻路由器负担。路由器不再做分片处理。 支持即插即用功能。即使没有DHCP服务器也可以实现自动分配 IP 地址。 采用认证与加密功能。应对伪造 IP 地址的网络安全功能以及防止线路窃听的功能。 多播、Mobile IP 成为扩展功能。 4.2 IPv6 中 IP 地址的标记方法 一般人们将 128 比特 IP 地址以每 16 比特为一组,每组用冒号(“:”)隔开进行标记。 而且如果出现连续的 0 时还可以将这些 0 省略,并用两个冒号(“::”)隔开。但是,一个 IP 地址中只允许出现一次两个连续的冒号。 4.3 IPv6 地址的结构 IPv6 类似 IPv4,也是通过 IP 地址的前几位标识 IP 地址的种类。 在互联网通信中,使用一种全局的单播地址。它是互联网中唯一的一个地址,不需要正式分配 IP 地址。 未定义 0000 \u0026hellip; 0000(128比特) ::/ 128 环回地址 0000 \u0026hellip; 0001(128比特) ::1 / 128 唯一本地地址 1111 110 FC00:/ 7 链路本地单播地址 1111 1110 10 FE80::/ 10 多播地址 1111 1111 FF00::/ 8 全局单播地址 (其他) 4.4 全局单播地址 全局单播地址是指世界上唯一的一个地址。它是互联网通信以及各个域内部通信中最为常用的一个 IPv6 地址。 格式如下图所示,现在 IPv6 的网络中所使用的格式为,n = 48,m = 16 以及 128 - n - m = 64。即前 64 比特为网络标识,后 64 比特为主机标识。 全局单播地址\n4.5 链路本地单播地址 链路本地单播地址是指在同一个数据链路内唯一的地址。它用于不经过路由器,在同一个链路中的通信。通常接口 ID 保存 64 比特版的 MAC 地址。 链路本地单播地址\n4.6 唯一本地地址 唯一本地地址是不进行互联网通信时所用的地址。 唯一本地地址虽然不会与互联网连接,但是也会尽可能地随机生成一个唯一的全局 ID。 L 通常被置为 1 全局 ID 的值随机决定 子网 ID 是指该域子网地址 接口 ID 即为接口的 ID 唯一本地地址\n4.7 IPv6 分段处理 IPv6 的分片处理只在作为起点的发送端主机上进行,路由器不参与分片。 IPv6 中最小 MTU 为 1280 字节,因此,在嵌入式系统中对于那些有一定系统资源限制的设备来说,不需要进行“路径 MTU 发现”,而是在发送 IP 包时直接以 1280 字节为单位分片送出。 4.8 IP 首部(暂略) 5. IP 协议相关技术 IP 旨在让最终目标主机收到数据包,但是在这一过程中仅仅有 IP 是无法实现通信的。必须还有能够解析主机名称和 MAC 地址的功能,以及数据包在发送过程中异常情况处理的功能。 5.1 DNS 我们平常在访问某个网站时不适用 IP 地址,而是用一串由罗马字和点号组成的字符串。而一般用户在使用 TCP/IP 进行通信时也不使用 IP 地址。能够这样做是因为有了 DNS (Domain Name System)功能的支持。DNS 可以将那串字符串自动转换为具体的 IP 地址。 这种 DNS 不仅适用于 IPv4,还适用于 IPv6。 5.2 ARP 只要确定了 IP 地址,就可以向这个目标地址发送 IP 数据报。然而,在底层数据链路层,进行实际通信时却有必要了解每个 IP 地址所对应的 MAC 地址。 ARP 是一种解决地址问题的协议。以目标 IP 地址为线索,用来定位下一个应该接收数据分包的网络设备对应的 MAC 地址。不过 ARP 只适用于 IPv4,不能用于 IPv6。IPv6 中可以用 ICMPv6 替代 ARP 发送邻居探索消息。 RARP 是将 ARP 反过来,从 MAC 地址定位 IP 地址的一种协议。 5.3 ICMP ICMP 的主要功能包括,确认 IP 包是否成功送达目标地址,通知在发送过程当中 IP 包被废弃的具体原因,改善网络设置等。 IPv4 中 ICMP 仅作为一个辅助作用支持 IPv4。也就是说,在 IPv4 时期,即使没有 ICMP,仍然可以实现 IP 通信。然而,在 IPv6 中,ICMP 的作用被扩大,如果没有 ICMPv6,IPv6 就无法进行正常通信。 5.4 DHCP 如果逐一为每一台主机设置 IP 地址会是非常繁琐的事情。特别是在移动使用笔记本电脑、只能终端以及平板电脑等设备时,每移动到一个新的地方,都要重新设置 IP 地址。 于是,为了实现自动设置 IP 地址、统一管理 IP 地址分配,就产生了 DHCP(Dynamic Host Configuration Protocol)协议。有了 DHCP,计算机只要连接到网络,就可以进行 TCP/IP 通信。也就是说,DHCP 让即插即用变得可能。 DHCP 不仅在 IPv4 中,在 IPv6 中也可以使用。 5.5 NAT NAT(Network Address Translator)是用于在本地网络中使用私有地址,在连接互联网时转而使用全局 IP 地址的技术。 除转换 IP 地址外,还出现了可以转换 TCP、UDP 端口号的 NAPT(Network Address Ports Translator)技术,由此可以实现用一个全局 IP 地址与多个主机的通信。 NAT(NAPT)实际上是为正在面临地址枯竭的 IPv4 而开发的技术。不过,在 IPv6 中为了提高网络安全也在使用 NAT,在 IPv4 和 IPv6 之间的相互通信当中常常使用 NAT-PT。 5.6 IP 隧道 夹着 IPv4 网络的两个 IPv6 网络\n 如上图的网络环境中,网络 A 与网络 B 之间无法直接进行通信,为了让它们之间正常通信,这时必须得采用 IP 隧道的功能。 IP 隧道可以将那些从网络 A 发过来的 IPv6 的包统合为一个数据,再为之追加一个 IPv4 的首部以后转发给网络 C。 一般情况下,紧接着 IP 首部的是 TCP 或 UDP 的首部。然而,现在的应用当中“ IP 首部的后面还是 IP 首部”或者“ IP 首部的后面是 IPv6 的首部”等情况与日俱增。这种在网络层的首部后面追加网络层首部的通信方法就叫做“ IP 隧道”。 后续 参考文章 一篇文章带你熟悉 TCP/IP 协议(网络协议篇二\n学习资料 《TCP/IP 详解》 《图解 TCP/IP》 ","description":"","id":52,"section":"posts","tags":["TCP/IP"],"title":"TCP/IP 协议","uri":"https://yichenlove.github.io/posts/tcpip/"},{"content":"cocos creator 右键菜单,除了创建组件,effect material是什么?\n纹理:\n 关于Material: threejs是js基于webgl的一个3D图形库 这里有个对Material很好的描述 threejs的接口都是基于Material + Geometry来创建一个现实对象 对于2d 3d显示对象,原理理应是类似的 cocos只是对sprite的封装隐藏细节 threejs对Material + Geometry接口更加GPU友好 https://segmentfault.com/a/1190000014639067 关于Effect: Material是怎么来的? 渲染管线: cocos creator 里面的shader怎么和cocos2d-x里面的不一样 根上都是顶点着色器和片段着色器的 yaml格式是什么? https://docs.cocos.com/creator3d/manual/zh/material-system/yaml-101.html 内置变量:https://docs.cocos.com/creator3d/manual/zh/material-system/builtin-shader-uniforms.html\n","description":"","id":53,"section":"posts","tags":["cocos creator","glsl"],"title":"cocos creator glsl","uri":"https://yichenlove.github.io/posts/crea_mf4/"},{"content":"已知问题跟进: cc_scenesize值没有初始化,所以始终是默认值[0,0] 给力的glsl编辑器: https://thebookofshaders.com/edit.php 一本通俗易懂的入门书: https://thebookofshaders.com/ 常用概念进阶 1.Noise 噪声 https://www.jianshu.com/p/35364a5e6e1b ","description":"","id":54,"section":"posts","tags":["cocos creator","Material Effect"],"title":"cocos creator Material Effect梳理(三)","uri":"https://yichenlove.github.io/posts/crea_mf3/"},{"content":"cocos creator 应该先带⼀个glsl基础教程https://www.jianshu.com/p/43aaff0b6226(暂定后⾯这个放到前⾯充当梳理⼆, 本篇实际是梳理三)\n实际开始搬运,有⼀些细节问题我们需要注意\nCCEeffect⽂件字段理解描述 1.Properties\n关于passes所有参数的各字段的含义,我们看⽂档\nhttps://docs.cocos.com/creator3d/manual/zh/material-system/pass-parameter-list.html\n⾥⾯最常⽤的应该就是*properties这个字段\n这⾥添加的都是统⼀常量,粗糙理解就是我们从外部传⼊的⾃定义的变量都显示在*属性检查器*这⾥\n* 注意这⾥的s_offset变量\n*更改的时候随时注意点击应⽤,cocos\ncreator操作有点违和,注意时刻点击保存特效。\nshadow.effect\nshadow.effect\n直观理解,我们想外部设置阴影的offset,shadow.effect⾥⾯最容易直观说明。\nCocos Creator封装 1.includ机制\n当前shader使⽤的语⾔是glsl 300es\n这个语⾔⼜⼀些特点相对于基础理解的glsl语⾔像C,glsl 300 es还有更⾼级的特点。\n详情⻅:\nhttps://docs.cocos.com/creator3d/manual/zh/material-system/effect-syntax.html\n⽂档内的Shader ⽚段\ninclude 应该是我们应该最常⽤和最应该理解的机制。\n看完上⾯描述,我们这么理解:\n* cocos creator⾥⾯已经写了⼀坨变量及函数,要⽤就需要include⾃⼰封装的东⻄,也可以写⼀个chunk 后⾯其他shader内可以直接include 使⽤\n我们最常⽤的cc_time cc_scenesize这些和渲染表象息息相关的,我们可以直接通过include后⽤,不⽤⼀直从外部传了。(理解动态最常⽤的变量cc_time,很多实现是通过外部脚本update函数内,给material传当前时间来作为变量,太累了...还有性能耗损)\n2.预处理宏定义(todo)\n3.Macro(todo )\n4.ubo内存处理\n我们⼀定要注意这⾥:\n*每个元素size不能⼩于vec4\n我们搬运的过程中,使⽤内置变量如cc_time使,按理解总会觉得cc_time是⼀个int或者float,实际本身应为上述规定,我们使⽤cc_time的时候实际使⽤是cc_time.x\n搬运实例\n2d基础的云朵效果\n吐槽⼀下,cocos官⽅对⾃⼰的shader效果的demo,也不过是shadertoy搬了两个...\ncloud.effect\ncloud.effect\n搬运⽐对shadertoy内的变量和 cocos creator内的变量都到底有什么不同\n","description":"","id":55,"section":"posts","tags":["cocos creator","Material Effect"],"title":"cocos creator Material Effect梳理(⼆)","uri":"https://yichenlove.github.io/posts/crea_mf2/"},{"content":"cocos creator Material Effect梳理(⼀)\ncocos creator\n右键菜单,除了创建组件,Effect Material是什么?\n* 什么是Material(纹理)?:\n```\nthreejs是js基于webgl的⼀个3D图形库\nthreejs对⾃⼰是引擎内Material的概念,这⾥有个对Material很好的描述\n⻚⾯内包含了\n* 什么是Material\n* threejs对⾃⼰内置的⼏种基础Material的描述\n⼀个⼀个看下去,看到最后.\u0026hellip;\n{width=\u0026ldquo;6.461111111111111in\u0026rdquo;\nheight=\u0026ldquo;5.086111111111111in\u0026rdquo;}\n...\nPhone Material\nLambert Material\nSprite Material\n...\n?!\nSpriteMaterial有点眼熟啊\n先放下Material是什么不管,我们⼀起回顾下如何创建⼀个显示对象:\n* cocos\n基于new Sprite 来创建⼀个显示对象\n```\nlet sp = new Sprite(\u0026quot;xxx\u0026quot;)\n```\n* threejs\n基于准备好材质和⼏何体,来拼接成⼀个显示对象\n```\nvar material = new THREE.MeshPhongMaterial({map: map},\nside:THREE.DoubleSide);\nvar geome = new THREE.SphereBufferGeometry(10, 10, 10)\n//我就是显示对象\nvar obj = new THREE.Mesh (geome, material);\nscene.add(obj)\n``\n⽐较下我们可以粗理解:\nnew Sprite () 和new THREE.Mesh()都是创建对象,前者隐藏了细节,\n后者更GPU友好。\n我们想在转头看Material,不就是为了创建⼀个*⾮默认显示Material的显示对象。\n* 关于Effect:\n```\nMaterial是怎么来的?先重温下Opengl渲染管线 关注Vertex Shader ,Fragment\nShader\n{width=\u0026ldquo;6.461111111111111in\u0026rdquo;\nheight=\u0026ldquo;8.493055555555555in\u0026rdquo;}\n我们同时回顾下刚才threejs看Material⾥⾯描述的⼏种常⻅Material\n{width=\u0026ldquo;6.304166666666666in\u0026rdquo;\nheight=\u0026ldquo;6.993055555555555in\u0026rdquo;}\n等下...\nSprite和 Phone 是不是有点眼熟\n{width=\u0026ldquo;2.2916666666666665in\u0026rdquo;\nheight=\u0026ldquo;1.3125in\u0026rdquo;}\n{width=\u0026ldquo;2.4791666666666665in\u0026rdquo;\nheight=\u0026ldquo;2.7930555555555556in\u0026rdquo;}\n.\u0026hellip;\n是的 都是⼀样的东⻄\n两个引擎都是使⽤vexshader + fragshader + 渲染⽅式配置\n创建了⼀个Material来的\n* cocos creator⾥⾯的Effect⽂件就是为了创建Material来的\u0026lt;vexshader +\nfragshader + 渲染\n⽅式配置\\⽂件\n* cocos\ncreator⾥⾯的Materialt⽂件可以赋予⼀个sprite材质,⽆纹理可直接决定表现⽅式,有纹理则是使⽤纹理\n与Material对纹理的处理共同决定表现⽅式。\n```\n* cocos creator ⾥⾯的shader怎么和cocos2d-x⾥⾯的不⼀样\n```\n根上都是处理vexshader + fragshader + 渲染⽅式配置\ncocos2d-x处理的⽅式全部是代码实现的,vexshader +\nfragshader都是单独的资源⽂件,渲染\n⽅式配置则是提供接⼝来实现\ncocos creator 则是统⼀成了⼀个Effect⽂件,⽂件格式是yaml\n什么是yaml?\n就是⼀个可序列化的配置,理解成⾼级json即可,go python\n等都有对应的解析库 类似 yaml.decode\n上百科:\n{width=\u0026ldquo;6.461111111111111in\u0026rdquo;\nheight=\u0026ldquo;17.15277777777778in\u0026rdquo;}\n{width=\u0026ldquo;6.461111111111111in\u0026rdquo;\nheight=\u0026ldquo;17.15277777777778in\u0026rdquo;}\n{width=\u0026ldquo;6.461111111111111in\u0026rdquo;\nheight=\u0026ldquo;17.15277777777778in\u0026rdquo;}\n其实就是⽤了个配置表包起来了。。。\n```\n好了 我们可以⽤来做什么...\n当然是从来不⽣产代码,只做代码的搬运⼯...上⼯地\n这⾥都是基于webgl能使⽤的shader的在线显示,都是采⽤glsl语⾔来的,和Unity不⼀样,unity也包裹了⼀层,当\n然根上还是可以⽤glsl的,所以,⽤什么引擎,只要是opengl渲染的,都可以上来看看glsl的实现搬运⼀把\n{width=\u0026ldquo;6.461111111111111in\u0026rdquo;\nheight=\u0026ldquo;2.9499989063867016in\u0026rdquo;}\n我们关注⼏个地⽅:\n* 着⾊器输⼊\n这⾥是webgl要渲染的时候外部传进来的变量\n对⼀个转换成cocos使⽤的变量再搬运~\n*compile\n这个是可以在线调试的,直接改代码效果微调后直接compile即时看效果美滋滋\n* compile下⽅的区域\n有些效果是基于纹理的,⼤部分是噪点纹理或具体实物材质表⾯效果的纹理。\n怎么这么多⿊框?\n{width=\u0026ldquo;5.908333333333333in\u0026rdquo;\nheight=\u0026ldquo;2.522222222222222in\u0026rdquo;}\ncocos creator 也是⽀持传⼊8张纹理之多来叠加的.\u0026hellip;.\n搬运步骤:\n1. 创建\n{width=\u0026ldquo;2.5527777777777776in\u0026rdquo;\nheight=\u0026ldquo;3.376388888888889in\u0026rdquo;}\n最好在这个路径,我们都知道glsl可能写起来⼀⼤堆⼀⼤堆,每个引擎多多少少为了复⽤,都有include机制\n{width=\u0026ldquo;6.461111111111111in\u0026rdquo;\nheight=\u0026ldquo;2.8027777777777776in\u0026rdquo;}\ninclude 可以导⼊别的glsl⽂件短,增加复⽤和抽象\nloadcloud 对应的Effect⽂件抄起来~\n{width=\u0026ldquo;6.461111111111111in\u0026rdquo;\nheight=\u0026ldquo;8.858332239720035in\u0026rdquo;}\n顺利替换完变量抄完,拖⼊sprite内运⾏查看效果\n{width=\u0026ldquo;4.783333333333333in\u0026rdquo;\nheight=\u0026ldquo;4.095832239720035in\u0026rdquo;}\nduang!\n","description":"","id":56,"section":"posts","tags":["cocos creator","Material Effect"],"title":"cocos creator Material Effect梳理(⼀)","uri":"https://yichenlove.github.io/posts/crea_mf/"},{"content":" apply、call 在 javascript 中,call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。\nJavaScript 的一大特点是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。\n先来一个栗子:\n1 2 3 4 5 6 7 8 9 10 11 function fruits() {} fruits.prototype = { color: \u0026#34;red\u0026#34;, say: function() { console.log(\u0026#34;My color is \u0026#34; + this.color); } } var apple = new fruits; apple.say(); //My color is red 但是如果我们有一个对象banana= {color : \u0026ldquo;yellow\u0026rdquo;} ,我们不想对它重新定义 say 方法,那么我们可以通过 call 或 apply 用 apple 的 say 方法:\n1 2 3 4 5 banana = { color: \u0026#34;yellow\u0026#34; } apple.say.call(banana); //My color is yellow apple.say.apply(banana); //My color is yellow 所以,可以看出 call 和 apply 是为了动态改变 this 而出现的,当一个 object 没有某个方法(本栗子中banana没有say方法),但是其他的有(本栗子中apple有say方法),我们可以借助call或apply用其它对象的方法来操作。\n apply、call 的区别 对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。例如,有一个函数定义如下:\n1 2 3 var func = function(arg1, arg2) { }; 就可以通过如下方式来调用:\n1 2 func.call(this, arg1, arg2); func.apply(this, [arg1, arg2]) 其中 this 是你想指定的上下文,他可以是任何一个 JavaScript 对象(JavaScript 中一切皆对象),call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。 JavaScript 中,某个函数的参数数量是不固定的,因此要说适用条件的话,当你的参数是明确知道数量时用 call 。\n而不确定的时候用 apply,然后把参数 push 进数组传递进去。当参数数量不确定时,函数内部也可以通过 arguments 这个伪数组来遍历所有的参数。\n为了巩固加深记忆,下面列举一些常用用法:\n数组之间追加 1 2 3 4 var array1 = [12 , \u0026#34;foo\u0026#34; , {name \u0026#34;Joe\u0026#34;} , -2458]; var array2 = [\u0026#34;Doe\u0026#34; , 555 , 100]; Array.prototype.push.apply(array1, array2); /* array1 值为 [12 , \u0026#34;foo\u0026#34; , {name \u0026#34;Joe\u0026#34;} , -2458 , \u0026#34;Doe\u0026#34; , 555 , 100] */ 获取数组中的最大值和最小值 1 2 3 var numbers = [5, 458 , 120 , -215 ]; var maxInNumbers = Math.max.apply(Math, numbers), //458 maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //458 number 本身没有 max 方法,但是 Math 有,我们就可以借助 call 或者 apply 使用其方法。\n验证是否是数组(前提是toString()方法没有被重写过) 1 2 3 function isArray(obj){ return Object.prototype.toString.call(obj) === \u0026#39;[object Array]\u0026#39; ; } 类(伪)数组使用数组方法 1 var domNodes = Array.prototype.slice.call(document.getElementsByTagName(\u0026#34;*\u0026#34;)); Javascript中存在一种名为伪数组的对象结构。比较特别的是 arguments 对象,还有像调用 getElementsByTagName , document.childNodes 之类的,它们返回NodeList对象都属于伪数组。不能应用 Array下的 push , pop 等方法。\n但是我们能通过 Array.prototype.slice.call 转换为真正的数组的带有 length 属性的对象,这样 domNodes 就可以应用 Array 下的所有方法了。\n 深入理解运用apply、call 下面就借用一道面试题,来更深入的去理解下 apply 和 call 。\n定义一个 log 方法,让它可以代理 console.log 方法,常见的解决方法是:\n1 2 3 4 5 function log(msg) { console.log(msg); } log(1); //1 log(1,2); //1 上面方法可以解决最基本的需求,但是当传入参数的个数是不确定的时候,上面的方法就失效了,这个时候就可以考虑使用 apply 或者 call,注意这里传入多少个参数是不确定的,所以使用apply是最好的,方法如下:\n1 2 3 4 5 function log(){ console.log.apply(console, arguments); }; log(1); //1 log(1,2); //1 2 接下来的要求是给每一个 log 消息添加一个\u0026quot;(app)\u0026ldquo;的前辍,比如:\n1 log(\u0026#34;hello world\u0026#34;); //(app)hello world 该怎么做比较优雅呢?这个时候需要想到arguments参数是个伪数组,通过 Array.prototype.slice.call 转化为标准数组,再使用数组方法unshift,像这样:\n1 2 3 4 5 6 function log(){ var args = Array.prototype.slice.call(arguments); args.unshift(\u0026#39;(app)\u0026#39;); console.log.apply(console, args); }; bind 详解 说完了 apply 和 call ,再来说说bind。bind() 方法与 apply 和 call 很相似,也是可以改变函数体内 this 的指向。\nMDN的解释是:bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。\n直接来看看具体如何使用,在常见的单体模式中,通常我们会使用 _this , that , self 等保存 this ,这样我们可以在改变了上下文之后继续引用到它。 像这样:\n1 2 3 4 5 6 7 8 9 10 var foo = { bar : 1, eventBind: function(){ var _this = this; $(\u0026#39;.someClass\u0026#39;).on(\u0026#39;click\u0026#39;,function(event) { /* Act on the event */ console.log(_this.bar); //1 }); } } 由于 Javascript 特有的机制,上下文环境在 eventBind:function(){ } 过渡到 $('.someClass').on(\u0026lsquo;click\u0026rsquo;,function(event) { }) 发生了改变,上述使用变量保存 this 这些方式都是有用的,也没有什么问题。当然使用 bind() 可以更加优雅的解决这个问题:\n1 2 3 4 5 6 7 8 9 var foo = { bar : 1, eventBind: function(){ $(\u0026#39;.someClass\u0026#39;).on(\u0026#39;click\u0026#39;,function(event) { /* Act on the event */ console.log(this.bar); //1 }.bind(this)); } } 在上述代码里,bind() 创建了一个函数,当这个click事件绑定在被调用的时候,它的 this 关键词会被设置成被传入的值(这里指调用bind()时传入的参数)。因此,这里我们传入想要的上下文 this(其实就是 foo ),到 bind() 函数中。然后,当回调函数被执行的时候, this 便指向 foo 对象。再来一个简单的栗子:\n1 2 3 4 5 6 7 8 9 var bar = function(){ console.log(this.x); } var foo = { x:3 } bar(); // undefined var func = bar.bind(foo); func(); // 3 这里我们创建了一个新的函数 func,当使用 bind() 创建一个绑定函数之后,它被执行的时候,它的 this 会被设置成 foo , 而不是像我们调用 bar() 时的全局作用域。\n有个有趣的问题,如果连续 bind() 两次,亦或者是连续 bind() 三次那么输出的值是什么呢?像这样:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var bar = function(){ console.log(this.x); } var foo = { x:3 } var sed = { x:4 } var func = bar.bind(foo).bind(sed); func(); //? var fiv = { x:5 } var func = bar.bind(foo).bind(sed).bind(fiv); func(); //? 答案是,两次都仍将输出 3 ,而非期待中的 4 和 5 。原因是,在Javascript中,多次 bind() 是无效的。更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。\n apply、call、bind比较 那么 apply、call、bind 三者相比较,之间又有什么异同呢?何时使用 apply、call,何时使用 bind 呢。简单的一个栗子:\n1 2 3 4 5 6 7 8 9 10 11 12 13 var obj = { x: 81, }; var foo = { getX: function() { return this.x; } } console.log(foo.getX.bind(obj)()); //81 console.log(foo.getX.call(obj)); //81 console.log(foo.getX.apply(obj)); //81 三个输出的都是81,但是注意看使用 bind() 方法的,他后面多了对括号。\n也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。\n 再总结一下:\n apply 、 call 、bind 三者都是用来改变函数的this对象的指向的; apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文; apply 、 call 、bind 三者都可以利用后续参数传参; bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。 ","description":"","id":57,"section":"posts","tags":["javascript"],"title":"Javascript中apply、call、bind","uri":"https://yichenlove.github.io/posts/javascripts-bind/"},{"content":"在游戏开发过程中,游戏性能是非常重要的,学会使用unity自带的profiler工具是非常必要的,以下是我从官方地址找到的进阶教程,感觉文章非常详细,于是尝试翻译。\n官方教学文档翻译,英文教程地址https://unity3d.com/cn/learn/tutorials/temas/performance-optimization/diagnosing-performance-problems-using-profiler-window?playlist=44069\n介绍 如果我们游戏运行很慢,卡顿甚至卡死,我们就知道游戏出现了性能问题。在我们尝试修复问题之前,我们首先要知道是什么造成了这种问题。不同的问题需要不同的解决方案。如果我们尝试猜测问题或根据其他项目对游戏进行调整,这会非常浪费时间甚至会使问题变得更加糟糕。\n这个时候,我们就需要对问题进行分析。分析是在运行我们游戏的时候对各个方面进行测量。使用profiling工具,当我们游戏运行的时候,可以看到屏幕后面发生的事情并且根据这些信息跟踪造成性能问题的原因。通过查看profiling工具,我们可以测量我们修改后的结果,这样我们就可以判断我们的修复是否有效。\n在这篇文章中,我们将:\n 使用Unity自带的的Profiler工具去收集我们游戏性能差的游戏的数据。 分析这些数据并且使用分析的结构去追踪到性能问题 提供修复这些问题的链接 要让一个游戏运行顺畅是一个平衡的过程。在获得理想结果之前,我们可能要对游戏进行好几次的修改和验证。知道如何使用profiling工具去分析我们的问题意味着我们能够确定游戏的问题是什么并且知道下一步要怎么做。\n写在开始 这篇文章会帮助我们跟踪到造成Unity游戏运行缓慢、卡顿甚至卡死的位置。如果我们有其他问题,比如崩溃或图像异常,这篇文章可能不会有太大的帮助。如果我们游戏中出现了这篇文章没所提到的一些问题,可以尝试搜索Unity手册、Unity社区或Unity解答。\n如果我们对Profiler窗口或如何使用Profiler不熟悉的话,建议先看这篇文章\n对游戏性能的一个简介 帧率是衡量游戏性能的标准。游戏里面的帧跟动画的帧类似。只是游戏里面的图像被画到了屏幕。画一帧到屏幕被称为渲染一帧。帧率或帧被渲染的速度以每秒来衡量(FPS)\n现在大多数游戏都是以60FPS为目标。通常30FPS以上被认为是可以接受的,特别是对于一些对反应速度要求不高的游戏,如解谜或冒险游戏。一些游戏对帧率要求比较高,如VR,90FPS都会被嫌弃。帧率在30FPS以下,玩家体验通常会比较差,图像可能会卡顿、操作起来也很迟钝。然而,不仅仅速度重要,帧率稳定也很重要。帧率发生变对对玩家来说是很明显的。不稳定的帧率通常比稳定但是帧率低的游戏更糟糕。\n尽管帧率是谈论游戏性能经常提到,但是要尝试去改善游戏性能的时候考虑渲染一帧所需要的毫秒数会更有用。有两个原因,首先这种一种更精准的测量方法。当我们尝试改善我们游戏性能的时候,每毫秒都能达到我们的目标。其次,帧率的相对变化意味着不同的规模帧率也是不一样的。从60FPS到50FPS代表处理时间增加了3.3毫秒,但是如果从30FPS到20FPS代表处理时间增加了16.6毫秒。同样是降低了10FPS,但是渲染一帧所花费的时间是截然不同的。\n了解普遍帧率渲染一帧所需要花费多少毫秒是非常有帮助的。要找到这个花费的时间,我们应该遵循这个公式1000/[渴望的帧率]。使用这个公式,我们可以知道每秒渲染30FPS,那么渲染每帧花费在33.3毫秒以内。一个游戏要运行到60FPS, 那么渲染每帧花费在16.6毫秒以内。\n对于渲染的每一帧,Unity都必须要执行很多不同的任务。简单来说,Unity必须更新游戏的状态,拿到游戏的快照并且渲染到屏幕。每帧必须要执行的任务包括读取用户输入、执行脚本、灯光运算。除此之外,还有一些一帧内执行多次的操作,如物理计算。当所有的任务执行的足够快,我们的游戏将会有一个一致性的,可接受的帧率。当所有的任务执行得不够快,会花费更长的时间去渲染,并且帧率会下降。\n知道哪个任务执行时间长,对如何解决游戏性能问题是至关重要的。一旦我们知道哪个任务在减低帧率,我们可以尝试优化那部分内容。这就是为什么分析如此重要:profiling工具可以显示在给定的帧在每个任务花费多长时间。\n记录分析数据 为了研究我们的游戏性能,我们必须记录游戏中性能不佳的数据。为了获得更加精准的分析数据,我们要打一个测试包运行在目标硬件上,并且记录分析数据。\n如果我们还不熟悉打包和真机记录分析数据,点击这里查看操作指南\n记录游戏数据 使用development build方式打包,在目标机上运行\n 在我们达到有性能问题之前,开始记录分析数据\n 一旦我们记录的分析数据包含了性能问题的例子,点击Profiler窗口上方任意位置以暂停游戏并选择一帧\n 在Profiler窗口的上方,选择显示性能较差的帧。这可能是低于我们要求帧率的“尖峰”或是有代表性的帧。我们可以使用左右按键或前进后退键在帧之间更好的移动。\n 我们已经获取游戏中性能较差的分析数据。下一步,让我们学习如何分析这些数据。\n分析数据 在得出任何关于游戏性能结论之前,我们必须学习如何阅读和分析显示在Profiler窗口的性能数据。我们知道,当Unity无法及时的完成渲染所需要的所有任务时,帧率会下降。我们将会使用Profiler窗口查看什么任务被执行了,任务花费多长时间以及按什么顺序执行的。这些信息会帮助会帮助我们明白我们游戏的哪些部分会造成任务花费太长的时间去渲染。\n最好去学习如何分析而不是学习一系列的步骤。自己理解这些数据更有用,这样当我们遇到了新问题的时候可以自己去研究。即使我们只是学会了在Unity解答上搜索,这也是一个伟大的开始。\n为了学习如何分析,我们将会使用CPU分析器作为例子,这可能是我们在研究帧率问题上用的最多的分析器。\nCPU分析器 当我们在Profiler窗口看CPU分析器的时候,我们可以看到CPU完成每帧所花费的时间。\n我们可以看到时间花费的彩色浪图。不同的颜色代表时间花在渲染操作上,物理计算上等等。那些关键字标明哪些颜色代表哪些任务。\n在接下来的截图中,我们可以到这一帧的主要时间花费在渲染操作上。下方的CPU时间指示器表明了我们的总的CPU时间在这一帧花费了85.95毫秒。\n层次结构图 让我们使用CPU分析器的层次结构视图去深挖当前数据并且更精确的查看当前帧哪一个任务花费CPU时间最多。当选中CPU分析器的时候,我们可以在Profiler窗口的下半屏看到当前帧的详细信息。查看Profiler窗口的下半屏,我们在做上方可以使用下拉菜单选择结构视图。这可以让我们看到CPU上正在发生的任务的详细信息。\n在层次结构图中,点击任何列的列头按该值排序。比如,点击Time ms按花费时间最长开始排序,点击Calls按当前高亮的帧调用次数最多的函数排序。在以上截图中,我们按照耗时排序,我们可以看到CPU最耗时的函数是Camera.Render。\n如果一个函数名字的左右有小箭头,我们可以展开看到这个函数调用了其他哪些函数和他们的性能影响。Self ms列表明这个函数自己的耗时,Time me列表明这个函数和它调用的其他函数的耗时。\n在这种情况下,我们可以看到Camera.Render下,最耗时的函数是Shadows.RenderJob。即使我们对这个具体的函数还不太了解,但是我们已经有关于我们游戏问题的信息了。我们知道我们的问题跟渲染有关,这个当前最耗时的任务是shaodws有关。\n我们可以在层次结构图做的另一个有用的事是比较我们游戏的帧,这样我们可以明白性能是如何随着时间的变化而变化的。我们使用CPU分析器一帧一帧的分析出单个最耗时的函数。当我们点击CPU分析器层级结构图上的函数名的时候,函数相关数据会高亮。\n比如,我们在层级结构视图点击Gfx.WaitForPresent,跟Gfx.WaitForPresent相关的渲染函数会高亮显示。\n时间线视图 现在让我们使用CPU分析器的时间视图学习更多关于我们渲染的问题。时间视图显示了两个东西:CPU任务执行的顺序和哪个线程负责哪个任务。我们可以在Profiler窗口的下半屏的左上角使用下拉按钮选择时间线视图(那里之前显示的是结构视图选项)\n线程可以同时运行多个单独任务。当一个线程执行任务的时,另一个线程可以执行完全独立的任务。Unity的渲染处理包含三种类型的线程:main thread、render thread和worker threads。了解哪些线程负责哪些任务是非常有帮助的:一旦我们知道了哪个线程执行了的任务最慢,我们就明白应该集中精力去优化那些线程的操作。\n我们可以放大时间线视图来更仔细的查看单个任务。被调用的函数也会在调用者下方立即显示出来。在这个例子中,我们已经放大了Shadows.RenderJob看到了组成这个任务的单个任务。我们可以看到Shadows.RenderJob是在main thread调用的。我们同样看到workder threads执行了跟shadows相关的任务。主线程列出了一个叫WaitingForJob的任务,表明main thread正在等待worker thread完成任务。从这我们可以得出结论,shadows相关的渲染操作在main thread和worker threads花费太长时间了。我们现在知道问题所在了。\n其他分析器 尽管在跟踪与帧率相关的性能问题时,CPU分析器是最常用的工具,其他分析器也同样非常有用。熟悉他们提供的信息是一个不错的主意。\n按照上面的步骤,尝试学习其他几个不同的分析器每帧提供了什么信息。比如,尝试使用渲染分析器,了解不同帧的渲染统计数据是如何变化的。\n确定造成性能问题的原因 既然我们熟悉了在分析器中读取和分析性能数据的过程,我可以开始找到造成性能问题的原因了。\n排除垂直同步 垂直同步简称VSync,用来匹配游戏帧率与屏幕刷新速度。垂直同步会影响游戏帧率,并且它的影响会显示在Profiler窗口上。如果我们不明确在看什么,还容易认为这是一个性能问题,所以在我们继续研究性能问题之前要学会如何排除掉垂直同步\n在CPU分析器中隐藏垂直同步信息 我们可以在CPU分析器中选择要隐藏的信息。这可以让我们忽略对当前研究没有帮助的信息。\n隐藏垂直同步的步骤如下:\n 点击CPU分析器 在Profiler窗口的顶部,CPU分析器区域显示当前关注的数据,点击标记为VSync的黄色正方形就可以隐藏垂直同步的信息 在层级结构视图中无视垂直同步信息 没有办法在层级结构视图中隐藏垂直同步信息,但我们知道了它长什么样子,我们就可以无视它。\n无论什么时候,我们在层级结构视图中看到WaitForTargetFPS,这意味着我们的游戏在等待垂直同步,我们不需要研究这个函数,忽略它就好。\n屏蔽垂直同步 垂直同步不能在所有的平台上都屏蔽:许多(如IOS)强制使用垂直同步。但如果我们正在为一个不需要强制使用垂直同步的平台开发,我们就可以屏蔽掉垂直同步。点击 Edit -\u0026gt; Project Settings -\u0026gt; Quality,找到VSync Count,下来菜单选中Don\u0026rsquo;t Sync\n渲染分析器 渲染是造成性能问题的常见原因。尝试修复渲染问题之前,确认我们的游戏是CPU密集还是GPU密集是很关键的,因为不同的情况,解决方法也不一样。\n简单的说,CPU负责决定绘制什么,而GPU负责绘制。如果渲染问题归咎于CPU耗时太多,那游戏就属于CPU密集,如果渲染问题归咎于GPU还是太多,那游戏就属于GPC密集。\n辨别我们的游戏是否是GPU密集 辨别我们的游戏是否是GPU密集最快捷的方法是使用GPU分析器。不幸的是,并不是所有的设备或驱动都支持这个分析器。在我们使用GPU分析器之前,我们先检测GPU分析器在目标设备上是否可用。\n检测GPU分析器在目标设备上是否可用,我们应该执行一下步骤:\n 在Profiler窗口左上角,选择Add profiler 通过下拉菜单选择GPU 如果GPU分析器不支持使用,我们会在GPU正常显示数据的区域看到一条以“GPU Profiling is not supported”开头的信息。\n如果没有没有看到这条信息,这意味着GPU分析器在目标设备上是支持的。如果GPU分析器是可用的,那执行一下步骤就能非常快速辨别出我们游戏是否是GPU密集:\n 点击GPU分析器\n 查看屏幕正中心区域,那里显示了当前选中帧的CPU和GPU耗时。\n如果GPU耗时大于CPU耗时,则我们可以确认我们的游戏是GPU密集。\n 如果GPU分析器不能在目标设备上使用,我们仍然可以辨别我们的游戏是否是GPU密集。我们可以通过观察CPU分析器。如果我们看到CPU正在等待GPU完成任务,这就是意味着我们游戏是GPU密集。为了查明是否存在这种情况,我们可以执行以下步骤:\n 点击选择CPU分析器 检查Profiler窗口底部区域显示的当前帧信息和分析器信息 在区域左上角的下拉菜单中选择层级结构视图 选择Time ms列头,函数耗时按时间排序 如果函数Gfx.WaitForPresent是CPU分析器中最好是的函数,这表明CPU正在等待GPU。这意味着我们的游戏是GPU密集。\n解决我们游戏是GPU密集时的渲染问题。\n如果我们已经确认我们的游戏是GPU密集,我们应该阅读这篇文章\n辨别我们的游戏是否是CPU密集 如果我们还没有确认造成性能问题的原因,现在我们研究基于CPU的渲染问题。\n 点击选择CPU分析器 随着时间的推移,Profiler窗口顶部信息会显示分析数据,检测图像中代表渲染的部分。我们可以通过点击关键字旁边的带颜色的正方形图片显示或隐藏这些数据。 如果慢帧的大部分时间被渲染占用了,这意味着渲染可能是造成我们问题的原因。我们可以按以下步骤继续挖掘来确认:\n 点击选择CPU分析器 检测Profiler窗口显示的当前帧信息和分析器信息 在分析数据区域左上角下拉菜单中选择层级结构 选择列头Time ms,按函数耗时排序 点击选择最顶部的函数 如果选中的是一个渲染函数,CPU分析器图像将会高亮显示Rendering图像。如果是这种情况,这意味着喧嚷相关的操作是造成我们性能问题的原因,也可以确认我们的游戏是CPU密集。留意函数名和是哪个线程执行这个函数。当我们尝试解决问题的时候,这些信息是很有用的。\n解决我们的游戏是CPU密集时的渲染问题。\n如果我们已经确认我们的游戏是CPU密集的渲染问题时,我们应该阅读这篇文章\n垃圾回收分析器 接下来,我们检查垃圾回收是否会造成瓶颈。垃圾回收是Unity自动内存管理的特性,这可能是一个缓慢的操作。\n 点击选择CPU分析器\n 注意,你可以拖拽你感兴趣的部分的名称,重新排序它们,在下面的截图中,我们拖拽GarbageCollector到顶部,并且点击关掉了其他方面的数据。\n如果慢帧的大部分时间被垃圾回收占用了,这指明了我们有垃圾回收过度的问题。我们可以深入研究以确认问题。\n 点击选择CPU分析器,检测Profiler窗口底部显示的当前帧相信信息\n 底部区域左上角下拉按钮选择层次结构视图\n 选择Time ms列头,以函数耗时排序\n 如果**GC.Collect()**函数出现,并且耗时比较多,我们就可以确认我们的游戏有垃圾回收问题。\n解决垃圾回收问题 如果我们确认我们的额游戏有垃圾回收问题,我们应该阅读这篇文章\n物理分析器 如果我们排除了渲染和垃圾会回收问题,我们检查负责的物理计算是否是造成我们性能问题的原因。\n 点击选择CPU分析器 在Profiler窗口显示数据的顶部,检测代表Physics的图像(橙色图像)。我们通过点击名字旁边的带颜色正方形来显示或隐藏图像。 如果慢帧的大部分时间被物理占用,那么可以确认武林是造成我们问题的原因。我们可以进一步研究以确认问题:\n 点击选中CPU分析器,检测Profiler下方区域显示的当前帧详细信息。 在底部区域左上角的下拉菜单选择层级结构视图 选择Time ms列头,按函数耗时排序 点击选择顶部的函数 如果选中的是一个物理函数,CPU分析器图像将会高亮显示Physics图像。如果是这种情况,我们可以确定造成性能问题的原因和物理计算相关。\n解决物理问题 如果我们确认我们的问题是物理引起,以下资料会有帮助:\n 文章 文章 文章 运行缓慢的脚本 现在我们检测缓慢的或过度负责的脚本是否是造成性能问题的原因。脚本,这里将的是非Unity引擎代码。这些脚本通常是我们自己写的,或者是第三方插件引入的。\n 点击选择CPU分析器 在Profiler窗口显示数据的顶部,检测代表Script的图像,我们可以通过点击关键字旁边的颜色方块显示或隐藏图像数据。 如果慢帧大部分时间被scripts占用,那可以确认开发者写的脚本是造成问题的原因。我们可以继续研究确认问题:\n 点击选中CPU分析器,检测Profiler窗口下方的当前帧详细数据 在底部区域左上角的下拉菜单选择层级结构视图 选择Time ms列头,按函数耗时排序 点击选择顶部的函数 如果是自己写的脚本,CPU分析器图像将会高亮显示Scripts图像。这种情况下,我们可以确认造成性能问题的原因与我们写的脚本有关。\n请注意,上面有一种特殊情况:当我们游戏包含渲染相关的函数,如Image Effects脚本或OnWillRenderObject或OnPreCull函数,这些将会出现在渲染分析器而不是脚本分析器。\n尽管起初有点小混乱,但是平常使用层级结构视图和时间线视图检测代码的时候,也能够跟踪到相关的代码。\n解决缓慢代码问题 如果我们确定自己写的脚本是造成性能问题的原因,这里有一些简单的技巧可以改善性能。下面是一个关于代码优化的资源:\n 文章 文章 文章 其他造成性能问题的原因 虽然我们已经讨论了性能问题最常见的四个原因,但是我们游戏可能有一些这里没有提到的性能问题。这种情况下,我们应该以上的一些方法来收集数据,研究CPU分析器并且找到造成问题的函数名字。一旦我们知道了函数名字,我们可以通过搜索Unity手册、Unity社区和Unity解答来或者这个函数的一些信息和如何减少这些函数消耗的方法。\n参考文章:\n使用Profiler进行性能分析\n","description":"","id":58,"section":"posts","tags":["Unity","Profiler"],"title":"【unity】使用Profiler进行性能分析","uri":"https://yichenlove.github.io/posts/unity-profiler/"},{"content":"目录 概述\n1.1 Vector3的定义\n1.2 主要优化的是什么? xLua对Vector3的优化\n2.1 xLua创建Vector3\n2.2 xLua获取Vector3 \u0026ndash; C#的Vector3传入lua\n2.3 xLua 设置 Vector3到C#\n2.4 GCOptimize \u0026ndash; PushUnityEngineVector3的由来 toLua对Vector3的优化\n3.1 toLua创建Vector3\n3.2 toLua获取Vector3 \u0026ndash; C#的Vector3传入lua\n3.3 toLua 设置 Vector3到C# xLua与toLua对Vector3的优化的区别\n4.1 效率性能的比较\n4.2 扩展性的比较 一. 概述 1.1 Vector3的定义 public struct Vector3,是一个struct 结构体,值类型。\n1.2 主要优化的是什么? 主要优化 减少gc + 减少lua与C#的交互。\n 为什么会产生gc?\n原因是boxing(装箱)和unboxing(拆箱)。Vector3(栈)转为object类型需要boxing(堆内存中),object转回Vector3需要unboxing,使用后释放该object引用,这个堆内存被gc检测到已经没引用,释放该堆内存,产生一个gc内存。\n 如何优化gc?\n值拷贝\n 二. xLua对Vector3的优化 2.1 xLua创建Vector3 lua中有2种方式可以表示Vector3:\n 创建Vector3对象,使用userdata:CS.UnityEngine.Vector3(7, 8, 9)\n 调用UnityEngineVector3Wrap中函数 static int __CreateInstance(RealStatePtr L)\n C#中new一个Vector3: UnityEngine.Vector3 __cl_gen_ret = new UnityEngine.Vector3(x, y, z);\n translator.PushUnityEngineVector3(L, __cl_gen_ret);\n要注意的是push方法是PushUnityEngineVector3,普通是translator.Push\n xlua push vec3 to lua 2.png\nPushUnityEngineVector3做的优化是申请一块userdata(size=12),将Vector3拆成3个float,Pack到userdata,push到lua xlua push float3 to lua 3.png\n这种Vector3 userdata传给C#后,有一个与Pack对应的UnPack 过程。 Table替代 : {x = 1, y = 2, z = 3}\n 创建时,不与Unity C#交互(这与toLua类似) 传给C#后,在C# UnPack 这个table,取出x、y、z, 赋值给new Vector3使用。 UnPack 在2.3中说明。 2.2 xLua获取Vector3 \u0026ndash; C#的Vector3传入lua lua中 aTransform.position获取Vector3坐标:\n UnityEngineTransformWrap. _g_get_position lua想从transform获取position transform getposition.png\n获取C# transform对象:UnityEngine.Transform __cl_gen_to_be_invoked = (UnityEngine.Transform)translator.FastGetCSObj(L, 1); translator.PushUnityEngineVector3(L, __cl_gen_to_be_invoked.position); 这和2.1中创建一个Vector3 push userdata到lua过程一致。 2.3 xLua 设置 Vector3到C# lua中 aTransform.position = Vector3坐标:\n UnityEngineTransformWrap. _s_set_position, lua想把pos设置到transform.position\nxLua transform setposition.png\n 设置position有2.1中的2种方式:\n 创建Vector3对象: aTransform.position = CS.UnityEngine.Vector3(7, 8, 9) 先获取userdata指针,再调用 CopyByValue.UnPack从指向内存的起始地址读取x,y,z值,设置到out UnityEngine.Vector3 field\nTable替代 : aTransform.position = {x = 1, y = 2, z = 3}\n直接调用 CopyByValue.UnPack,将Table的x,y,z值取出,设置到out UnityEngine.Vector3 val transform setposition get.png\n CopyByValue.UnPack\n上面2种方式调用的 CopyByValue.UnPack实现不同;\n userdata的方式,Pack的时候,使用xlua_pack_float3,对应的UnPack过程使用xlua_unpack_float3,解出userdata struct.\nxlua userdata方式设置vector3.png lua table,从栈中依次读取3个float值。 xlua table方式设置vector3.png\n 2.4 GCOptimize \u0026ndash; PushUnityEngineVector3的由来 为何Vector3的push到lua 会有一个针对优化的接口PushUnityEngineVector3?\nVector3 struct配置了GCOptimize属性(对于常用的UnityEngine的几个struct,Vector系列,Quaternion,Color。。。均已经配置了该属性),这个属性可以通过配置文件或者C# Attribute实现;\nGCOptimize Vector3.png\n 从GCOptimize列表中去掉Vector3 会怎么样呢?\nPushUnityEngineVector3接口就不存在了,而Vector3的push到lua会使用translator.Push(L, __cl_gen_ret); ,不做优化public void Push(RealStatePtr L, object o),会产生boxing(装箱)和unboxing(拆箱),代表着一个gc。\n去掉vector3的GCOptimize后的push.png\n 三. toLua对Vector3的优化 toLua用lua重新实现了Vector3,包含所有方法;文件地址:tolua-master\\Assets\\ToLua\\Lua\\UnityEngine\\Vector3.lua\n3.1 toLua创建Vector3 1 Vector3.New(x, y, z) toLua并没有跟Unity C#交互.\n3.2 toLua获取Vector3 \u0026ndash; C#的Vector3传入lua lua中 aTransform.position获取Vector3坐标:\n C# UnityEngine_TransformWrap.get_position;调用ToLua.Push\ntoLua get_position.png\n C#传入Vector3的x,y,z;\ntoLua get_position pushvec3.png\n 在lua建一个lua table,把x,y,z设置为对应字段;\n 设置该table的metatable为Vector3.lua的方法实现;\n 3.3 toLua 设置 Vector3到C# 从栈中取出对应table的x,y,z字段\ntoLua set_position ToVector3.png\n C# new一个Vector3,将x,y,z赋值到Vector3;\n与xLua的table替代方式非常类似。\n 四. xLua与toLua对Vector3的优化的区别 效率性能的比较,toLua高 xLua与toLua都不产生gc xLua在创建Vector3的userdata方式和Vector3的方法调用,都需要跟Unity C#交互;而toLua在这两方面是纯Lua端执行,无需跟Unity C#交互,效率最高。 xLua有一个特点:所有无GC的类型,它的数组访问也没有GC。 扩展性的比较,xLua高\ntoLua重新Lua实现的类,需要增加一种新的值类型十分困难, 数量有限,并且与Unity C# Vector3核心代码深度耦合。 toLua lua类列表.png\nxLua支持的struct类型宽泛的多,包含枚举,用户要做的事情也很简单,用GCOptimize声明一下类型即可。支持自定义struct。(struct要求1.含无参构造函数 2.只包含值类型,可以嵌套其它只包含值类型的struct)\n相关链接 xlua github xlua特性 tolua 官网 tolua github Unity下XLua方案的各值类型GC优化深度剖析 参考文档\nhttps://www.jianshu.com/p/07dc38e85923\n","description":"","id":59,"section":"posts","tags":["Unity","xLua","toLua"],"title":"Unity中xLua与toLua对Vector3的优化","uri":"https://yichenlove.github.io/posts/unity-xlua-vector3/"},{"content":"LitJson解析JSON字符串空{}出错 问题\n1 2 3 4 5 6 7 string jsstr = \u0026#34;{\\\u0026#34;key1\\\u0026#34;:\\\u0026#34;value1\\\u0026#34;,\\\u0026#34;key2\\\u0026#34;:{},\\\u0026#34;key3\\\u0026#34;:\\\u0026#34;value3\\\u0026#34;}\u0026#34;; JsonData jsdata = JsonMapper.ToObject\u0026lt;JsonData\u0026gt;(jsstr); string outstr = JsonMapper.ToJson(jsddata); System.Console.WriteLine(\u0026#34;litjson json string:\u0026#34; + outstr); //输出litjson json string:{key1:value1,key2,key3:value3\u0026#34;}已不是一个正常的json字符串。 解决\n在JsonData.class的WriteJson方法最后添加一句writer.Write(null);即可解决。此方法也适用于解决空数组[]解析问题。\n参考 传送门:\n Incorrect json string on empty arrays ","description":"","id":60,"section":"posts","tags":["LitJson","Unity"],"title":"Litjson Fix","uri":"https://yichenlove.github.io/posts/litjson-fix/"},{"content":"Hugo 完整使用教程 官网 基于go 语言开发\n官网地址:https://gohugo.io/\n环境 1.Homebrew\n如果你是macOS用户,请使用Homebrew快速安装\n2.Chocolatey\n如果你是windows用户,请使用Chocolatey快速安装\n 环境配置请参考本站对应安装教程\n 快速开始 安装 hugo\n1 brew install hugo 创建博客工程\n使用如下命令新建一个名为 mysite 的网站:\n1 hugo new site mysite 创建一片文章\n1 hugo new post/first.md 主题安装 下载主题\n官网主题地址:https://themes.gohugo.io/\n把主题通过git克隆或直接下载到本地。放到 …/blog/themes/目录下\n1 2 cd themes git clone https://github.com/vjeantet/hugo-theme-casper.git casper 编译预览\nhugo server -t casper -D 打开网址 http://localhost:1313/ 即可查看本地生成的静态网站\n 主题推荐\n bolg:https://github.com/dillonzq/LoveIt 预览 简约:\n* https://themes.gohugo.io//theme/hugo-theme-dimension/#\n* https://github.com/victoriadrake/hugo-theme-sam - 预览 个人简历:https://themes.gohugo.io/theme/hugo-uilite/ 网站:https://github.com/StefMa/hugo-fresh- 预览 参考文章 https://www.jianshu.com/p/4669fb3bf35a https://www.jianshu.com/p/0b9aecff290c ","description":"","id":61,"section":"posts","tags":["hugo"],"title":"Use Hugo","uri":"https://yichenlove.github.io/posts/use-hugo/"},{"content":"Android Activity的Launch Mode 综述 对安卓而言,Activity有四种启动模式,它们是:\n standard 标准模式,每次都新建一个实例对象 singleTop 如果在任务栈顶发现了相同的实例则重用,否则新建并压入栈顶 singleTask 如果在任务栈中发现了相同的实例,将其上面的任务终止并移除,重用该实例。否则新建实例并入栈 singleInstance 允许不同应用,进程线程等共用一个实例,无论从何应用调用该实例都重用 想要感受一下的话写一个小demo,然后自己启动自己再点返回键就看出来了。下面详细说说每一种启动模式\nstandard 一张图就很好理解\n什么配置都不写的话就是这种启动模式。但是每次都新建一个实例的话真是过于浪费,为了优化应该尽量考虑余下三种方式。\nsingleTop 每次扫描栈顶,如果在任务栈顶发现了相同的实例则重用,否则新建并压入栈顶。\n配制方法实在Mainifest.xml中进行:\n\u0026lt;activity android:name=\u0026quot;.SingleTopActivity\u0026quot; android:label=\u0026quot;@string/singletop\u0026quot; android:launchMode=\u0026quot;singleTop\u0026quot; \u0026gt; \u0026lt;/activity\u0026gt; singleTask 与singleTop的区别是singleTask会扫描整个任务栈并制定策略。上效果图:\n使用时需要小心因为会将之前入栈的实例之上的实例全部移除,需要格外小心逻辑。\n配制方法:\n\u0026lt;activity android:name=\u0026quot;.SingleTopActivity\u0026quot; android:label=\u0026quot;@string/singletop\u0026quot; android:launchMode=\u0026quot;singleTop\u0026quot; \u0026gt; \u0026lt;/activity\u0026gt; singleInstance 这个的理解可以这么看:在微信里点击“用浏览器打开”一个朋友圈,然后切到QQ再用浏览器开一个网页,再跑到哪里再开一个页面。每次我们都在Activity中试图启动另一个浏览器Activity,但是在浏览器端看来,都是调用了同一个自己。因为使用了singleInstance模式,不同应用调用的Activity实际上是共享的。\n上说明图:\n配制方法:\n\u0026lt;activity android:name=\u0026quot;.SingleTopActivity\u0026quot; android:label=\u0026quot;@string/singletop\u0026quot; android:launchMode=\u0026quot;singleTop\u0026quot; \u0026gt; \u0026lt;/activity\u0026gt; 参考博客 传送门:\n Android Activity的Launch Mode ","description":"","id":62,"section":"posts","tags":["android"],"title":"Android Activity的Launch Mode","uri":"https://yichenlove.github.io/posts/android-launchmode/"},{"content":"开始准备自己的博客,记录技术和记录生活。开始锻炼自己的写作,坚持动脑,记录点点滴滴。慢慢更新。慢慢积累。\n也可以当作自己的日记本,贵在坚持。\n2021-09-27国庆前终于使用hugo+zzo主题部署了一个自己个人博客主页。\n我的github主页 GitHub.\n","description":"自我介绍:并添加自己的相关吐槽","id":64,"section":"","tags":null,"title":"关于","uri":"https://yichenlove.github.io/about/"},{"content":"喜欢的二次元 pixiv\n","description":"cartoon gallery","id":65,"section":"gallery","tags":null,"title":"Cartoon","uri":"https://yichenlove.github.io/gallery/cartoon/"}]