一点心得


  • 首页

  • 归档

Unity脚本自动构建IPA包

发表于 2017-06-18

Unity项目在iOS平台打包是一件比较费时间的事情,所以写了脚本来实现自动话打包生成ipa文件,避免每次去到处xcode工程然后配置证书之类的设置。

测试环境 Unity5.5.1f3, macOS 10.12.4, Xcode 8.3.2

工具使用介绍

打包的配置用到了很重要的XUPorter插件,打包的代码UnityIPABuilder。下面简单讲解一下里面的具体实现

Xcode打包主要有两个步骤,第一步是导出Xcode工程,第二步是构建Xcode工程并且导出ipa包。这里两种方式启动打包步骤。

第一种在Editor模式下打包。制作了一个Editor界面如下:

里面的Export按钮的功能是直接导出一个配置号参数的Xcode工程,Build按钮的功能是首先会进行Export操作,然后对Export出的工程进行Build直接生成IPA包。整个界面比较简单,如果不满足自己的需求的话可以通过在BuildWindow里面添加。

第二种不启动Editor,直接用脚本打包。这种模式下我们直接启动项目打包脚本cmd_start_unity_ipa_builder加上对应的参数即可。例如sample工程的打包直接运行一下命令(先配置好xcode_basic.cfg)即可得到最终的ipa文件:

1
2
./cmd_start_unity_ipa_builder.sh -v 1.0.0 -b 100 -c QA

这个脚本会静默启动Unity来执行ProjectBuilder的打包方法。并且会把打包的日志存放在{$ProjectPath}/iOSBuild/build/log目录下。

导出Xcode工程部分

这一步可以通过Unity的内置API完成。在这一步中我们还需要做的是设置好导出的Xcode的工程的各种配置(证书,版本号,Framework等)。导出Xcode工程直接只用Unity的API

1
BuildPipeline.BuildPlayer(buildPlayerOptions);

这个API可以通过BuildOptions配置在Editor中的Player Setttings里面的各种配置。我的工具里面只添加了ScriptingImpletion、Architecture、渠道、Version和Build五个参数。这是根据我自己的需求来添加的,其他的设置在Palyer Setting设置好了基本上就不需要动态改变了,这几个参数是每次打包改动比较频繁的选项。

先看下我们的整体构建步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  public static void BuildForiOS(ProjectBuildData data)
  {
  		//导出Xcode工程
      ExportForiOS(data);
  
  	   //构建Xcode工程
      BuildiOSPorject();
  }

  public static void ExportForiOS(ProjectBuildData data)
  {
      //根据data数据来导出对应的Xcode工程
	  ....
		
	   //配置Xcode工程
      XCodePostProcess.OnPostProcessBuild(BuildTarget.iOS, BuildPaths.ProjectPath, data.Channel, data);
  }

导出Xcode工程之后我们要做的就是配置Xcode工程。这里我用的是XUPorter插件来进行修改的,这个插件可以很方便的帮助我们自动把依赖的Framework、静态库、文件等添加到Xcode工程,这样可以解决我们在使用第三方插件的时候依赖的库每次打包需要手动添加的问题(5.5.1f1版本支持添加iOS系统自带的Framwork,但是不支持添加第三方的库,比如libsqlite3.dylib文件)。另外为了配置为了配置证书和Info.plist文件,稍微改动了一下里面的代码。

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
public static void OnPostProcessBuild(BuildTarget target, string xcodeProjectPath, PackageChannel channel, ProjectBuildData data)
{
	if (target != BuildTarget.iOS) {
		Debug.LogWarning("Target is not iPhone. XCodePostProcess will not run");
		return;
	}

	//这里来动态XUPorter的配置文件来自动添加Framework等一些XCode的配置
	XCProject project = new XCProject(xcodeProjectPath + "Unity-iPhone.xcodeproj");

	string[] files = Directory.GetFiles( Application.dataPath, "*.projmods", SearchOption.AllDirectories );
	foreach( string file in files ) {
		project.ApplyMod( file );
	}

	//读取我们自己的配置文件
	ProjectBasicConfig config = new ProjectBasicConfigLoader().GetConfig(channel);

	//这里配置证书
	OverWriteBuildSetting(config, project);

   //这里配置Info.plist文件,比如加权限描述,修改版本号之类的
	OverWriteInfoPlist(config, xcodeProjectPath, data);

	project.overwriteBuildSetting ("ENABLE_BITCODE","NO");
	
	//保存修改
	project.Save();
}

OnPosProcessBuild函数在XUPorter插件里面XCodePostProcess文件里面的一个函数,这个函数本来是加了[PostProcessBuild(999)]属性,加了这个属性的函数是Unity在BuildPlayer调用之后会主动调用的函数。但是这里我们去掉这个属性,我们不需要Unity来主动调用这个函数。因为我们的构建的步骤是: BuildPlayer导出Xcode工程,XUPorter配置XCode工程,shell脚本构建XCode工程。XCodePostProcess属于第二步XUPorter配置XCode工程这一步,如果让Unity来主动调用XCodePostProcess的话在不修改XUPorter的情况下我们无法得知XUPorter执行完毕的时间。所以我们自己在调用完BuildPlayer之后接着调用XCodePostProcess然后再调用shell构建,这样顺序来执行。

对于配置XUPorter的参数,大家可以参考工程的XUPorter插件下面的Bugly.projmods配置文件。详细的解释可以参考作者的原文介绍。至于ProjectBasicConfig配置,这个直接共用的shell脚本使用的配置文件,构建部分详解下。

至此我们可以得到一个配置号所有参数的Xcode工程。

构建Xcode工程部分

构建Xcode部分我设计的是不依赖Export部分,也就是说我们不需要在Export部分配置好证书或者版本号等这些参数(xcode_basic.cfg文件包含的参数),对于任何Xcode工程我们都可以通过这个脚本进行Build生成IPA包。构建脚本只有auto_build_xcode_project一个文件,使用方法我们可以通过执行命令查阅:

1
2
sh auto_build_xcode_project.sh --help

我们可以先对shell脚本添加可执行权限,之后运行以来就不需要sh命令了

1
2
3
chmod +x auto_build_xcode_project.sh

./auto_build_xcode_project.sh --help

当然,我们在使用这个脚本之前请先配置这个脚本对应的配置文件xcode_basic.cfg,这个配置文件里面包含的参数是我们在使用这个脚本自动打包可以动态改变的所有参数。如果还需要修改额外的参数,那就需要自己另外添加了。整个配置文件我分了三个渠道,分别是Debug,QA和Release。每个渠道的配置的参数是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
channel=Debug

#证书
code_signing_identity=xxx

#旧版描述文件
provisioning_profile=xxx

#新版本描述文件
provisioning_profile_specifier=xxx

#账号的TeamID
team_id=xxx

#包ID
bundle_identity=xxx

#包安装之后的显示名字
display_name=xxx

打包脚本会自动读取相应的渠道的配置,这个配置的参数是有序的,不要改变他们之间的顺序。这里和大家说下怎么获取得到描述文件和证书的指。

描述文件在Xcode里面的Build Settings里面Signing里面先选择对应的描述文件,然后在选项中选择other就会选择我们描述文件的值:

证书直接在系统应用钥匙串访问里面查看,双击点开一个钥匙串可以看得到:

直接复制里面的常用名称对应的值即可。

其实Xcode的自动构建比较简单,主要是通过XcodeBuild来构建,构建好了之后会生成一个app包,然后再通过xcrun命令把这个app包压缩\转换成ipa包即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...

//构建xcode,生成app包
xcodebuild  -project $xcode_project_filepath \
            -configuration $build_mode \
            -target $project_target \
            PROVISIONING_PROFILE="$provisioning_profile" \
            CODE_SIGN_IDENTITY="$code_signing_identity" \
            PRODUCT_BUNDLE_IDENTIFIER=$bundle_identity \
            PRODUCT_NAME=$product_name \
            DISPLAY_NAME=$display_name

...

转换app成ipa文件
xcrun -sdk iphoneos PackageApplication -v $app_package_path -o $package_unique_path

至此我们就得到了ipa包了。如果需要自动上传到发布平台(比如fir.im),可以直接在生成ipa包之后调用发布平台提供的上传命令直接上传即可,这样整个过程就完全自动化了。

阅读全文 »

Unity Coroutine的实现原理

发表于 2017-05-25

在Unity中我经常用到Coroutine的功能,但是对于Coroutine一直有一些疑问没有得到答案,下面先上一个在项目里面经常使用Coroutine的场景的sample:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//TestCoroutine.cs

public class TestCoroutine : MonoBehaviour {

    private void Start () 
    {
        StartCoroutine(Test()); 
    }

    private IEnumerator Test()
    {
        Debug.Log(gameObject.name);

        yield return new WaitForSeconds(5);

        Debug.Log(transform.localPosition);

        transform.localScale = Vector3.one;
    }
}

  • Test中yield语句调用之后为什么可以立即停止,等到 WaitForSeconds 的时候接着执行,这是怎么做到的?

  • Test的返回值是IEnumerator,但是函数内部的return用了yield来修饰,yield到底做了什么工作。

针对第二个问题我找到了一个比较详细的解释, 里面有这样一段话:

The yield keyword actually does quite a lot here. The function returns an object that implements the IEnumerable interface.

相当于yield关键字会自动生成一个集成IEnumerable的对象,那么这个对象到底是什么样子呢? 首先使用ILSpy工具查看下这段代码代码的C#版本:

1
2
3
4
5
6
7
8
[DebuggerHidden]
private IEnumerator Test()
{
	TestCoroutine.<Test>c__Iterator0 <Test>c__Iterator = new TestCoroutine.<Test>c__Iterator0();
	<Test>c__Iterator.$this = this;
	return <Test>c__Iterator;
}

可以看到通过ILSpy解析之后Test方法已经完全变了,但是也验证了上面的说法。yield关键字会自动转换成一个IEnumerable的对象,这里自动生成了一个名字为TestCoroutine.{Test}c_Iterator0 的类,并且创建了这个类型的 {Test}c_Iterator 对象。

1
代码里面名字是TestCoroutine.<Test>c_Iterator0,但是当前主题<>符号显示不正确,所以改成{}显示。下同

那这个对象内部是怎样实现的呢?现在我们得看下TestCoroutine.{Test}c_Iterator0的实现了(这里方法自动加了DebuggerHidden属性,所以只能看IL版本的代码)

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
//TestCoroutine.IL
//整个类的代码比较多,这里只截取我们需要的代码

.class public auto ansi beforefieldinit Test.TestCoroutine
	extends [UnityEngine]UnityEngine.MonoBehaviour
{
	// 嵌套类型
	.class nested private auto ansi sealed beforefieldinit '<Test>c__Iterator0'
		extends [mscorlib]System.Object
		implements [mscorlib]System.Collections.IEnumerator,
		           [mscorlib]System.IDisposable,
		           class [mscorlib]System.Collections.Generic.IEnumerator`1<object>
	{
		.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
			01 00 00 00
		)
		// 成员
		.field assembly class Test.TestCoroutine $this
		.field assembly object $current
		.field assembly bool $disposing
		.field assembly int32 $PC

		.method public final hidebysig newslot virtual 
			instance bool MoveNext () cil managed 
		{
			// 方法起始 RVA 地址 0x3c7a8
			// 方法起始地址(相对于文件绝对值:0x3aba8)
			// 代码长度 144 (0x90)
			.maxstack 2
			.locals init (
				[0] uint32
			)

			// 0x3ABB4: 02
			IL_0000: ldarg.0
			// 0x3ABB5: 7B C9 07 00 04
			IL_0001: ldfld int32 Test.TestCoroutine/'<Test>c__Iterator0'::$PC
			// 0x3ABBA: 0A
			IL_0006: stloc.0
			// 0x3ABBB: 02
			IL_0007: ldarg.0
			// 0x3ABBC: 15
			IL_0008: ldc.i4.m1
			// 0x3ABBD: 7D C9 07 00 04
			IL_0009: stfld int32 Test.TestCoroutine/'<Test>c__Iterator0'::$PC
			// 0x3ABC2: 06
			IL_000e: ldloc.0
			// 0x3ABC3: 45 02 00 00 00 05 00 00 00 3A 00 00 00
			IL_000f: switch (IL_0021, IL_0056) // 分支,直接执行IL_0021(对应我们cs代码的WaitForSeconds之前的代码,IL_0056对应我们WaitForSeconds之后的代码)

			// 0x3ABD0: 38 6B 00 00 00
			IL_001c: br IL_008c

			// 0x3ABD5: 00
			IL_0021: nop
			// 0x3ABD6: 02
			IL_0022: ldarg.0
			// 0x3ABD7: 7B C6 07 00 04
			IL_0023: ldfld class Test.TestCoroutine Test.TestCoroutine/'<Test>c__Iterator0'::$this
		
			//这里做了 Debug.Log(gameObject.name);
		
			// 0x3ABEB: 02
			IL_0037: ldarg.0
			// 0x3ABEC: 73 1B 02 00 0A
			IL_0038: newobj instance void [UnityEngine]UnityEngine.WaitForSeconds::.ctor(float32) //创建一个WaitForSeconds对象,引用会存放在栈顶。
			// 0x3ABF1: 7D C7 07 00 04
			IL_003d: stfld object Test.TestCoroutine/'<Test>c__Iterator0'::$current // 将栈顶的对象(WaitForSeconds)赋值给current变量
			// 0x3ABF6: 02
			IL_0042: ldarg.0 //载入第0个参数
			// 0x3ABF7: 7B C8 07 00 04
			IL_0043: ldfld bool Test.TestCoroutine/'<Test>c__Iterator0'::$disposing
			// 0x3ABFC: 2D 07
			IL_0048: brtrue.s IL_0051 //判断成员变量disposing是否为true,非空或者非0,如果是则跳转到IL_0051的地址

			// 0x3ABFE: 02
			IL_004a: ldarg.0
			// 0x3ABFF: 17
			IL_004b: ldc.i4.1 //载入值1入栈
			// 0x3AC00: 7D C9 07 00 04
			//把栈顶的值(现在是1)赋值给成员变量PC,这样的话下次调用MoveNext,在IL_000f处的switch的分支就会直接走IL_0056了
			IL_004c: stfld int32 Test.TestCoroutine/'<Test>c__Iterator0'::$PC 

			// 0x3AC05: 38 38 00 00 00
			IL_0051: br IL_008e //IL_008e在当前(MoveNext)函数结尾

			// 0x3AC0A: 02
			IL_0056: ldarg.0
			// 0x3AC0B: 7B C6 07 00 04
			IL_0057: ldfld class Test.TestCoroutine Test.TestCoroutine/'<Test>c__Iterator0'::$this
			// 0x3AC10: 28 4E 01 00 0A
			
			//这里做了Debug.Log(transform.localPosition);

        	//       transform.localScale = Vector3.one;
			
			// 0x3AC39: 02
			IL_0085: ldarg.0
			// 0x3AC3A: 15
			IL_0086: ldc.i4.m1
			// 0x3AC3B: 7D C9 07 00 04
			IL_0087: stfld int32 Test.TestCoroutine/'<Test>c__Iterator0'::$PC

			// 0x3AC40: 16
			IL_008c: ldc.i4.0
			// 0x3AC41: 2A
			IL_008d: ret

			// 0x3AC42: 17
			IL_008e: ldc.i4.1 
			// 0x3AC43: 2A
			IL_008f: ret
		} // 方法 '<Test>c__Iterator0'::MoveNext 结束
}

这里只截取了MoveNext方法,因为这个方法里面包含了我们Sample中的Test函数的所有逻辑操作。这段代码我加了注释,通过看代码基本上已经解决了我的第一个疑惑了,下面再做一下分析。Sample中的方法Test在编译之后会转换成大概这个样子(只是模拟):

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
private sealed class <Test>c__Iterator0 : IEnumerator, System.IDisposable
{
	// 用来保存 创建的WaitForSeconds对象的,调用者会根据这个对象的条件是否满足来确定调用MoveNext
	private object current;
	
	// 判断宿主对象是否已经被销毁了,这里就是那个发起StartCoroutine的那个MonoBehaviour对象 
	private bool disposing; 
	
	//保存MoveNext中调用的地址的,用这个来判断是执行yield之前的代码还是之后的代码
	private int PC; 
	
	private bool MoveNext()
	{
		 switch(this.PC)
		 {
		 case IL_0021:
			{
				Debug.Log(gameObject.name);

   		     	this.current = new WaitForSeconds();
        	
	        	PC = IL_0056;
	        	
	        	return true;
			}
   
		 case IL_0056:
			{
				Debug.Log(transform.localPosition);

				transform.localScale = Vector3.one;
			}
		 }
		  
        return false;
	} 
	
	object IEnumerator<object>.Current
   {
		get
		{
			return this.current;
		}
	}
}

到这里已经知道了yield关键字所做的操作了,那么现在来看看这个{Test}c_Iterator0对象是怎样被调用的。

首先我们看到我们的Sample中的代码:

1
StartCoroutine(Test()); 

这说明创建的{Test}c_Iterator0对象传入的StartCoroutine方法中了,浏览了StartCoroutine的实现,这个方法直接绑定的C++层MonoBehaviour的StartCoroutine方法,这里简述下里面的实现(不贴Unity代码了):

  • 调用StartCoroutine方法,该方法会以传入的IEnumerator参数(这里是{Test}c_Iterator0对象)创建一个C++的Coroutine对象
  • 这个对象保存会保存参数IEnumerator对象,并且会先获取出IEnuerator的MoveNext和Current方法。这两个方法也是IEunerator最关键的方法
  • 创建好之后这个Coroutine对象会保存在MonoBehaviour一个成员变量List中,这样使得MonoBehaviour具备StopCoroutine功能,StopCoroutine能够找到对应Coroutine并停止
  • Coroutine对象会调用成员方法run,启动这个Coroutine

这个步骤大概是这样的(代码只表现大概逻辑,不能正确执行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// C#层IEnumerator被传入C++层的时候会被C++的类ScriptingObjectPtr封装并绑定
ScriptingObjectPtr MonoBehaviour::StartCoroutine(ScriptingObjectPtr userCoroutine)
{
	Coroutine* coroutine = new Coroutine ();
	
	//获取C#层对应的方法,拿到方法的地址。通过Mono就可以直接调用了
	ScriptingMethodPtr moveNext =  scripting_object_get_virtual_method(userCoroutine, MONO_COMMON.IEnumerator_MoveNext, GetScriptingMethodRegistry());
	ScriptingMethodPtr current = scripting_object_get_virtual_method(userCoroutine, MONO_COMMON.IEnumerator_Current, GetScriptingMethodRegistry());
	
	...
	coroutine->SetMoveNextMethod(moveNext);
	coroutine->SetCurrentMethod(current);
	
	coroutine->m_Behaviour = this;
	...
	
	m_ActiveCoroutines.push_back(coroutine); 
	
	coroutine.Run();
	
}

那么现在生成了C++层的Coroutine对象了,再分析Coroutine现在作为执行者它是怎么实现的。Coroutine里面针对yield对象(Sample里面是WaitForSeconds)类型做了处理:

  • WaitForSeconds
  • WaitForFixedUpdate
  • WaitForEndOfFrame
  • Coroutine (C#层)
  • WWW
  • AsyncOperation

这些类型处理的方式是定义了一个类似定时调用的管理类DelayedCallManager。比如我的条件是WaitForSceonds(5), 那么Coroutine里面会创建一个CallDelayed,把时间设置魏5秒,然后DelayedCallManager的Update里面会直接算时间,到时间了就会回调Coroutine。WWW类型比较特殊它本身做了类似的处理,它提供了一个方法CallWhenDone,当它完成的时候直接回调Coroutine。

这个步骤大概是这样的(代码只表现大概逻辑,不能正确执行):

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
//Coroutine.cpp

// Run其实是一个递归的操作,在CallDelayed之后回调coroutineCallback里面会根据条件再次进入Run,
// 直到MoveNext返回false,条件结束了。
void Coroutine::Run (...)
{
	//根据IEnumerator的特性,首先得调用下MoveNext,所以这里进入Run之后首先会调用一次MoveNext(), 
	//就是<Test>c__Iterator0的MoveNext。这样就给current赋值了
	ScriptingInvocation invocation(MoveNext);
	...
 	ScriptingObjectPtr monoWait = invocation.Invoke(&exception)
	
	//调用Current
	ScriptingInvocation invocation(Current);
	...
 	ScriptingObjectPtr monoWait = invocation.Invoke(&exception);
 	
 	
	//可以在Sample里面看到Current函数在调用过第一次MoveNext之后被赋值为WaitForSeconds对象,
	//所以这里waitClass就可以获取WaitForSeconds对象的等待时间了
	ScriptingClassPtr waitClass = scripting_object_get_class (monoWait, GetScriptingTypeRegistry());

	if (scripting_class_is_subclass_of (waitClass, classes.waitForSeconds))
	{
		float waitTime = 0;//通过waitClass获取WaitForSeconds对象的等待时间
		MarshallManagedStructIntoNative(monoWait, &waitTime);
	
		// 这里添加到DelayedCallManager里面
		// coroutineCallback里面处理了我们在一个方法里面多次yield return的操作,
		// 这个方法里面会递归调用Run直到所有的操作处理完成
		CallDelayed(coroutineCallback, monobehaviour, waitTime, , ,);
		return;
	}
}


//DelayedCallManager.cpp

void DelayedCallManager::Update()
{
	float time = GetCurTime();
	int frame = GetTimeManager().GetFrameCount();
	
	Container::iterator iterator = m_CallObjects.begin (); //m_CallObjects保存了所有注册的Coroutine对象
	
	// iterator->time 在注册的时候赋值是: 当前时间 + 等待的时间(new WaitForSeconds(5),那么就是5秒)
	// iterator->time <= time 这个条件判断了iterator的定时时间是否满足了
	// 比如上面我们加入定义new WaitForSeconds(5),
	// 满足的条件时就当当前时间time要大于iterator满足的时间的时候,则进入这个while循环内
	while (iterator !=  m_CallObjects.end () && iterator->time <= time) 
	{
		//判断帧是否满足,加入用到了new WaitForFixedUpdate()之类的
		if (cb.timeStamp != m_TimeStamp && cb.frame <= frame) 		{
			//调用CoroutineCallback了
		}	
	}
}

其他的的类型会直接在下一帧调用,比如yield return 0

整个过程粗略的看大概就是这个样子。上面的分析没有深入到每个条件判断之类的,但是已经够了解Coroutine的全貌了。有错误的地方欢迎指出来,非常感谢。

阅读全文 »

C语言的变量初始化问题

发表于 2017-05-05

问题

今天在写opengl shader的时候遇到了一个问题(使用C语言编写的),出问题的代码如下(这里只列出出问题的部分代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

...

void main()
{
    vec3 norm = normalize(Normal);
    vec3 viewDir = normalize(viewPos - FragPos);
    
    vec3 result;
    
    for (int i=0; i<2; ++i)
    {
        result += CalcPointLight(pointLights[i], norm, viewDir);
    }
    
    color = vec4(result, 1.0f);
}

...


这段代码的原本的功能是计算点光源。可是代码跑起来之后使用这个shader的模型颜色明显异常。效果如下:

最终查出来的错误的原因是main函数里面result未初始化的原因导致的。一开始我以为是写的光照函数CalcPointLight里面实现有错误,定位了很久才发现出问题的点。

正确的效果应该是这样的:

验证了一下result如果没有初始化直接使用的话,它会对应一个stack区的随机值。这样的话在累加光照值之后计算的color值肯定是错误的。(其实我想如果C语言会报错提示使用为初始化的局部变量会更加人性化)

原理

顺着这个思路想了一下,为什么C语言的全局变量(global)就算不赋值会被自动初始化位默认值,但是局部变量(local)不会呢? 学习了一下C语言的内存布局结构,然后自己验证了一下然后明白了这个原因。

首先我们得知道C语言的内存布局结构,这篇文章 Memory layout of c program 讲的非常详细。这里列出我要用到的部分,首先看结构(图片来自这篇文章):

从这篇文章里面我明白了两点:

  • 全局变量是存放在data段内存的。data段分为uninitialized data(bss)段和initalized data,未初始化的变量是放在bss段的,这部分内存存放的变量是会被自动初始化的(这是C语言的特性)
  • 局部变量是存放在stack段的。这部分内存是被runtime时期动态分配的。(其实局部变量在代码编译之后就是一个地址,接下来会演示出来)

明白了这两点,那我通过一个demo来验证一下是否在内存中真的是这个样子。

测试环境: Ubuntu 16.02.2 i386

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//test_c_variable_init.c

#include <stdio.h>

int global_var;

int main()
{
    int local_var;
    
    local_var = local_var + 3;
        
    printf("global_var: %d & local_var: %d \n", global_var, local_var);
    
    return 0;
}

利用demo里面声明的一个全局变量(global_var)和局部变量(local_var)来验证现在的问题。(对local_var加常量3的操作仅仅是为了能够快速定位local_var在反编译代码中的位置)

首先可以通过linux的size命令查看总的内存分布

编译test_c_variable_init.c文件

gcc -o test_c_variable_init test_c_variable_init.c

得到可执行文件test_c_variable_init,现在可以查看到这个可执行文件的内存分布:

然后我们在test_c_variable_init.c中增加一个全局变量:

int global_var2;

按照之前的步骤我们再次查看重新编译的文件的内存分布:

可以看到bss段增加了4kb,这个大小就是我们增加的global_var2变量的大小。按照同样的方式增加local_var2我们会发现data和bss段都不会增加。这样可依初步确定上面列出的两点结论了。

然后我们通过查看反编译代码直接来查看变量在编译之后的位置,这样就明确之前的两点结论。

objdump -D test_c_variable_init > test_c_variable_init.d

我们把反编译之后的代码重定向到test_c_variable_init.d文件,然后我们查看下这个文件,其中有两个地方可以让我们得到想要的答案。

这是反编译之后的main函数:

可以看到347行的 $0xc ,之前我列出demo的时候为local_var特地添加了一个加常量3的操作,通过这个操作我可以很快定位到了$0xc 就是对应的local_var变量了, $0x3 就是常量3(addl就是汇编里面的加运算)。这里证明了local_var是分配在stack区的。此外我们注意到 0x804a020 这个地址其实就是global_var变量了,跳转到这个地址

可以很清楚的看到 0x804a020 就是global_var并且存放在bss段的。

这样我们就验证了上面的两点结论了,也知道了为什么未初始化的全局变量会被自动初始化位默认值,局部变量不会被初始化了。

阅读全文 »

Unity文件下载分析

发表于 2016-12-28

在项目里面有时需要从服务器上下载比较大的文件,比如我们在做更新的时候需要下载AssetBundle文件。Unity3D中提供了WWW接口可以用来下载文件,但是使用之后发现这个接口在下载比较大的文件的时候非常占用内存资源。然后我用C#的Webclient接口来下载同样的文件,发现内存占用情况存在和WWW的完全不一样。本文来就来探讨一下使用WWW和Webclient两个接口下载文件造成内存占用的原因。

阅读全文 »

C# foreach带来的内存问题

发表于 2016-10-29

简介

在使用Unity工具开发游戏的时候经常会被建议不要使用foreach,究其原因说是会产生额外的heap内存,既然存在这种问题, 那我必须得自己搞清楚下。本文是自己查阅资料和试验的结果总结。

本文参考了Memory allocation when using foreach loops in C#

测试环境: Unity5.4.1f1, MonoV2.0.5

剖析

首先来搞明白使用foreach和for的区别,下面是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
//Sample1.0
{
    List<string> list = new List<string>(){"hello", "world"};

    foreach(string item in list)
    {
        UnityEngine.Debug.Log(item);
    }
}
	
阅读全文 »
1 … 6 7 8 9
© 2025 yiliangduan@qq.com