Android Unity Plugin 開發指南(轉載)
Unity Android Plugin開發指南
原創 2016-07-05 haodongyuan QQ音樂技術團隊
本文將介紹如何在Unity工程中使用Android或者Java的庫,包括:
- 如何在Unity專案中使用Android Plugin
- Unity-Android相互呼叫
- Unity介面設計的最佳實踐
- 如何構建Unity-Android混合專案
- 如何除錯Unity和Android程式碼
- 附錄:跨虛擬機器呼叫的實現
如何在Unity專案中使用Android Plugin
Android Plugin需要包含一個jar和對應的封裝程式碼。後者用來封裝Android程式碼,提供給Unity專案使用。
jar放在Unity專案的/Assets/Plugins/Android
中,Android外掛的其他依賴也放在此處。
封裝程式碼可以是C#檔案,或者dll檔案,都放在/Assets
中,若是dll,需在Unity C#工程中新增此dll依賴。
此外,如果Android外掛包含資源,按照原有目錄結構放到/Assets/Plugins/Android
中即可。
如果需要額外的系統許可權,需要在AndroidManifest.xml中新增,這個檔案預設是沒有的,如果要修改的話,必須手動新增一份Unity可用的manifest,可參考【附錄】中提供的模板。
最後,工程的結構類似這樣:
Assets
└── Plugins
├── Android
│ ├── AndroidManifest.xml
│ ├── android_sdk.jar
│ └── res
│ ├── values
│ └── drawable
└── unity_wrapper.dll
Unity 從5.2.0b3版本開始很好地支援aar格式的檔案,可以將資源打包進aar中,不必再放置到該目錄下
Unity與Android之間相互呼叫
準確來說,應該是兩個VM之間的相互呼叫:mono/il2cpp 和 dalvik/art,分別執行Unity應用和Android應用,這兩個虛擬機器執行在同一個程式中。
為了方便起見,後文將前者稱為Unity,後者稱為Android
如上圖所示,Unity通過
UnityEngine
提供的API呼叫Android的方法;Android藉助
com.unity.player
包提供的API呼叫Unity的方法。
前者可以直接呼叫Android物件或者類的方法,而後者只能呼叫Unity中指定
GameObject
所掛載的指令碼的方法,或者通過動態代理的方式呼叫Unity的方法。
Unity呼叫Java方法
UnityEngine提供了兩個類來分別訪問Java的例項物件以及類物件:AndroidJavaObject
與AndroidJavaClass
前者表示java.lang.Object
或其子類,後者表示java.lang.Class
。他們提供相同的例項方法:
方法 | 返回值 | 說明 |
---|---|---|
Call | void | 呼叫例項方法 |
Call<T> | T | 呼叫例項方法 |
CallStatic | void | 呼叫類方法 |
CallStatic<T> | T | 呼叫例項方法 |
Get<T> | T | 獲取成員變數 |
GetStatic<T> | T | 獲取類的成員變數 |
Set(T) | void | 設定成員變數 |
SetStatic(T) | void | 設定類的成員變數 |
注意:
T的型別只能為原始值型別(int、long、string等等),或者
AndroidJavaObject
、AndroidJavaClass
,或者內容為原始值型別或AndroidJavaObject
的陣列Get和Set方法直接操作成員變數,而不是通過getter或setter
下面將通過一段程式碼來演示:如何獲取一個AndroidJavaClass
例項,並且呼叫其getInstance
方法獲取其物件,然後呼叫此物件的方法。
在開始之前,先看一下我們用到的Java類
package example;
public class Player {
private final static Player instance = new Player();
public static Player getInstance() {
return instance;
}
public float volume = 1.0f;
public int getDuration() {}
public void setDataSource(String dataSource) {}
public AudioInfomation getAudioInfomation() {}
}
首先,在工程中新增 UnityEngine.dll
依賴,該檔案位於Unity安裝目錄下的Editor/Data/Managed
目錄中,注意,新增依賴後,將其設定為不拷貝到本地。
現在,我們來獲取Player
這個類並獲取其單例:
AndroidJavaObject player = new AndroidJavaClass("example.Player").CallStatic<AndroidJavaObject>("getInstance");
然後對player
物件呼叫其Java方法:
player.Set("volume", 0.8f);
player.Call("setDataSource", "http://example.com/stream.m4a");
int duration = player.Call<int>("getDuration");
AndroidJavaObject info = player.Call<AndroidJavaObject>("getAudioInfomation");
注意,返回值型別為AndroidJavaObject
的方法有個共同的缺陷:如果Android側返回null,該方法將報錯:JNI: Init'd AndroidJavaObject with null ptr!
。
這是因為,在AndroidJavaObject
的建構函式中,遇到IntPtr.Zero(即null)
會報錯:
internal AndroidJavaObject(IntPtr jobject) : this(){
if (jobject == IntPtr.Zero){
throw new Exception("JNI: Init'd AndroidJavaObject with null ptr!");
}
// ...
}
該缺陷存在於5.3.1f1版本之前的UnityEngine,一個可行的辦法是:先獲取Android方法返回結果的指標,如果指標為空就返回null,否則返回指標的物件。
Android呼叫Unity方法
在Android中,有兩種方式呼叫Unity的方法:
- 通過
AndroidJavaProxy
進行無感知呼叫 - 通過
com.unity3d.player.UnityPlayer.UnitySendMessage
方法顯式呼叫
ndroidJavaProxy
AndroidJavaProxy
常用於在Unity中實現Java的interface,比如有這麼一個java interface:
package demo;
interface PlayStateListener {
void onBufferFinished(SongInfo songInfo);
void onBufferProgress(String songId, long buffered, long total);
}
對應的C#類就是這樣:
class PlayStateChangedHandler : AndroidJavaProxy {
internal PlayStateChangedHandler() : base(new AndroidJavaClass("demo.PlayStateListener")) {}
public void onBufferFinished(AndroidJavaObject songInfoObject) {}
public void onBufferProgress(string songId, long buffered, long total) {}
}
有幾點需要注意:
- Unity側的方法必須為public,且有相同的名稱和類似的簽名
- 如果Android側方法的傳參或返回值為類型別,對應Unity側只能為
AndroidJavaObject
- 4.6.8f1版本的UnityEngine有BUG,無法在
AndroidJavaProxy
中傳遞long型別的值,該問題在Unity 5中已經修復
有關
AndroidJavaProxy
的實現,在附錄中有詳細介紹
UnityPlayer.UnitySendMessage
這需在Android工程中新增Unity提供的jar依賴,它位於Unity安裝目錄下:/Editor/Data/PlaybackEngines/AndroidPlayer/Viariations/{backend}/{buildType}/Classes/classes.jar
其中,backend是Unity專案指令碼執行器的型別,有mono和il2cpp兩種,與Unity專案的”Script Backend”一致。
然後通過以下程式碼來訪問掛載在TGameObj物件上的指令碼的OnButtonClick方法:UnityPlayer.UnitySendMessage("TGameObj", "OnButtonClick", "Greetings from Java");
Unity介面設計的最佳實踐
本節將介紹一個用於封裝Java程式碼的通用設計方式,可以高效地將Java程式碼的API“移植”到C#,同時保持可擴充套件性。該設計將Java程式碼中的類及其結構反射到C#程式碼中,至於該類的細節(比如繼承關係、介面實現等)將被忽略,因為需要反射的都是暴露給使用者的API介面,使用者不應該關心這些細節。
如下圖所示:
Java中的
demo.Foo
類通過Reflection
反射到C#的Mirrored.Foo
中,demo.Foo
中的公共欄位和方法都按照原有結構被反射。
注意,這裡的反射只是單向地從Java反射到C#。如果要從C#反射到Java,可以參考本節進行擴充套件。
反射的實現
在開始之前,我們需要明確哪些類需要反射。對於int, long, double等原始型別以及string型別,UnityEngine已經幫我們處理好了,只剩下java.lang.Object
的派生類需要我們反射。
反射基類的設計
我們使用AndroidObjectMirror
作為反射類的父類。
public abstract class AndroidObjectMirror : IDisposable {
protected AndroidJavaObject AJObject { get; private set; }
internal void FromJava(AndroidJavaObject ajo) {
AJObject = ajo;
InitFromJava(ajo);
}
public virtual void Dispose() { AJObject?.Dispose(); }
protected virtual InitFromJava(AndroidJavaObject ajo) {}
}
在AJObject
這個反射物件被建立時,被反射物件的引用計數將會增加(AndroidJNISafe.NewGlobalRef
),在Dispose
方法中,其引用計數將會減少(AndroidJNISafe.DeleteGlobalRef
)。
之後,子類通過覆寫InitFromJava
方法來進行成員變數的初始化:
子類可以建立和被反射類“一樣的”方法,並將所有的呼叫委託給成員變數AJObject
即可。例如:
int Add(int a, int b) {
return AJObject.Call<int>("add", a, b);
}
總結一下,反射的邏輯如下圖所示:
反射的實現
藉助於AndroidObjectMirror
,我們可以這樣來定義上文提及的example.Player
的反射類:
class Player : AndroidObjectMirror {
public static Player Instance {
get {
var javaObject = AJObject.CallStatic<AndroidJavaObject>("getInstance");
return Reflection.Reflect<Player>(javaObject);
}
}
public float Volume {
get { return AJObject.Get<float>("volume"); }
set { AJObject.Set<float>("volume", value); }
}
public void Start() { AJObject.Call("start"); }
// ..
}
注意,在獲取單例時,我們用了這樣一行程式碼:return Reflection.Reflect(javaObject);
Reflection
這個工具類用來反射Java的物件,即將AndroidJavaObject
的物件反射為派生自AndroidObjectMirror
的類的物件。
其中的Reflect
方法是這樣實現的:
public static T Reflect<T>(AndroidJavaObject ajo) where T : AndroidObjectMirror, new() {
if (ajo == null) {
return default(T);
}
try {
var result = new T();
result.FromJava(ajo);
return result;
}
catch (Exception e) {
Debug.LogError("failed to reflect from Java : " + e.Message);
return default(T);
}
}
這裡的邏輯很直接:建立一個AndroidJavaObject
物件ajo
,然後在InitFromJava
方法中通過ajo
來初始化這個物件的成員變數。注意,這裡約束了型別T
必須提供無參公共建構函式,因此AndroidJavaMirror
必須通過InitFromJava(AndroidJavaObject)
來初始化,而沒有將AndroidJavaObject
放在構建函式中。
至於InitFromJava
方法,它可以是這樣:
protected override void InitFromJava(AndroidJavaObject ajo) {
title = ajo.Get<string>("title");
}
或者類似反序列化的方式在執行時進行解析:
protected virtual void InitFromJava(AndroidJavaObject androidJavaObject) {
var namingConverter = DefaultNamingConverter.Instance;
var publicFields = GetType().GetFields();
foreach (var field in publicFields) {
var javaFieldName = namingConverter.GetJavaFieldName(field.Name);
var value = androidJavaObject.Get(field.GetType(), javaFieldName);
field.SetValue(this, value, BindingFlags.SetField, null, null);
}
}
如何構建Unity-Android混合專案
本節將介紹如何使用Gradle來構建混合了不同平臺專案的工程。
以一個SDK型別的工程為例,我們來看一下工程的內容:
- Android SDK
- Android Demo (快速除錯)
- Unity Bridge (封裝Android SDK)
- Unity Demo (演示並除錯Unity Bridge)
目錄結構如下:
RootDir (工程根目錄)
|
|-- Android (Android相關模組)
| |-- Demo_Android
| |-- SDK_Android
|
|-- Unity (Unity相關模組)
|-- Demo_Unity
|-- Bridge_Unity
其中:
- Android的兩個模組可以用Android gradle外掛進行編譯與打包
-
Bridge_Unity
可以用msbuild(windows)或者xbuild(linux)構建 -
Demo_Unity
需要購買了Unity Pro之後才能自動化構建。
接下來,我們將在各自模組的構建指令碼中新增構建任務,分別構建這些模組,最後,在工程的根構建指令碼中,建立自動化的構建指令碼。
Android SDK的構建
Jar包構建任務
SDK將以Jar的形式提供給Unity Bridge使用,因此需要新增打包成jar的構建任務。我們利用已有的Android構建任務鏈,建立Jar構建任務。
已有的構建指令碼位於RootDir/Android/SDK_Android/build.gradle
,在其中加入Jar構建任務:
android.libraryVariants.all { v ->
def type = v.name.capitalize()
task("jar$type", type: Jar) {
archiveName "sdk-$type.jar"
dependsOn v.javaCompile
from v.javaCompile.destinationDir, configurations.compile.collect {
it.isDirectory() ? it : zipTree(it)
}
}
}
task
後面的閉包會在gradle指令碼構建時執行,用來定義此任務的屬性:
- archiveName: 輸出Jar包的檔名,預設為模組名稱
- dependsOn: 此任務的依賴
- from: 要打包的class
這裡需要注意:
依賴dependsOn: v.javaCompile
此任務必須在v.javaCompile
完成之後執行,即java檔案被編譯成class檔案之後再將這些class打包成Jar。
要打包的class
from v.javaCompile.destinationDir, configurations.compile.collect {
it.isDirectory() ? it : zipTree(it)
}
這裡說明要打包的class有兩處:模組自身的和依賴的Jar包。
Proguard構建任務
對外發布時,通常需要對程式碼進行混淆。對於我們自定義的Jar任務,必須手動新增混淆任務:
task("proguardJar$type", type: ProGuardTask) {
dependsOn "jar$type"
configuration android.getDefaultProguardFile('proguard-android.txt')
configuration 'proguard-rules.pro'
injars "build/libs/sdk-$type.jar"
outjars "$outputFolder/sdk-$suffix.jar"
dontshrink
ignorewarnings
}.doFirst {
delete outputFolder
}
注意其中的dependsOn "jar$type"
,這樣就將混淆任務和jar任務串聯了起來。
釋出任務
為了便於其他構件指令碼獲取此模組的最新構建結果,我們將輸出的Jar拷貝到latest
目錄中。
task("buildJar$type", type: Copy, group: 'build') {
dependsOn "cleanBuildJar$type"
from outputFolder
into "build/outputs/libs/latest/$type
if (type.equals("Release")) {
dependsOn "proguardJar$type"
} else {
dependsOn "jar$type"
}
}
至此,Android SDK構建任務新增完成。
Android Demo的構建
Andriod Gradle Plugin已經提供Demo的構建任務。
Unity Bridge的構建
在開始之前,我們需要配置好構建環境:對於Windows系統,需要用到msbuild,它會隨著Visutal Studio一同安裝;對於linux/unix系統,可以使用xbuild,它是Mono裡面的一個工具。下文將使用xbuild。
準備工作完成後,我們來建立CSharpBuildTask
這個構建任務。該任務其實就是封裝了對xbuild的呼叫:
package demo
class CSharpBuildTask extends DefaultTask {
@Input File solutionFile;
@Input String configuration;
@Input String builderCmd = "/usr/local/bin/xbuild"
@TaskAction def compile() {
def cmd = "$builderCmd $solutionFile"
if (configuration != null) {
cmd += " /p:Configuration=$configuration"
}
def proc = cmd.execute()
proc.waitFor()
if (proc.exitValue() != 0) {
throw new BuildException("failed to build csharp project: ${proc.getErrorStream()}", null)
}
}
}
將CSharpBuildTask.groovy
放在RootDir/buildSrc/src/main/groovy/demo
下,這樣就可以在所有子模組中使用該任務。
之後,在RootDir/Unity/Bridge_Unity
目錄下建立build.gradle
檔案,作為此模組的構建指令碼。內容為:
import demo.CSharpBuildTask
def buildTypes = ["Release", "Debug"]
def localProps = new Properties()
localProps.load(project.file('local.properties').newDataInputStream())
buildTypes.each { v ->
task("buildLib$v", type: CSharpBuildTask) {
builderCmd = localProps["msbuild.dir"].toString()
solutionFile = new File("Unity_Bridge")
configuration = v
}
}
local.properties
檔案位於RootDir/Unity/Bridge_Unity
,內容為:
# local.properties
msbuild.dir=/usr/local/bin/xbuild
至此,我們用gradle檢視一下是否成功建立了此構建任務:
$ gradlew tasks
可以看到,
buildLib
構建任務已經建立。
Unity Demo的構建
受限於Unity,只有Unity Pro及以上版本才支援程式碼或者命令列的方式進行構建。
首先,我們需要在/Asset/Editor
中建立一個指令碼,通過BuildPipeLine來構建Unity工程:
public class BuildScript: MonoBehaviour{
static void BuildAndroid(){
string[] scenes = {"Assets/Demo.unity"};
BuildPipeline.BuildPlayer(scenes, "AndroidBuild", BuildTarget.Android, BuildOptions.None);
}
}
然後,在RootDir/buildSrc
中建立UnityBuildTask
:
class UnityBuildTask extends DefaultTask {
@Input String unityExePath
@TaskAction def compile() {
def cmd = "$unityExePath -quit -batchmode -executeMethod BuildScript.BuildAndroid"
def proc = cmd.execute()
proc.waitFor()
if (proc.exitValue() != 0) {
throw new BuildException("failed to build unity project", null)
}
}
}
最後,在RootDir/Unity/Demo_Unity
中建立build.gradle
,並在其中建立一個構建任務:
// 有關localProps見前文
task("buildUnityDemo",type: UnityBuildTask, group: 'build') {
unityExePath = localProps["unitybuild.dir"].toString();
}
BuildPipeLine以及Unity的命令列呼叫可以參考官方文件:http://docs.unity3d.com/Manual/CommandLineArguments.html
混合構建
上面已經介紹了各個模組各自的構建方法,現在,我們將在根模組的構建指令碼中將他們串聯起來。
其中,箭頭表示依賴關係,Unity的Demo同時依賴於Unity和Android的SDK,同時還要將生成的SDK拷貝到Unity Demo專案中的特定位置,這樣Demo才能正常執行。
這些構建任務的依賴關係如下圖所示:
我們在根模組中建立這些構建任務:
- copyUnitySDKToDemo:將生成的Unity SDK拷貝到Unity Demo
- copyAndroidSDKToDemo:將生成的Android SDK拷貝到Unity Demo
- buildUnitySDK:buildLib的馬甲
- buildAndroidSDK:buildJar的馬甲
- buildUnityDemo:構建Unity demo
- buildAndroidDemo: 構建Android demo
我們可以在根模組的build.gradle
中新增這些任務,但會使得build.gradle
變得非常混亂。因此我們採用Plugin的方式,來進行這些任務的建立。
現在我們來建立SDKBuildPlugin
,在RootDir/buildSrc/src/main/groovy/demo
中新建SDKBuildPlugin.groovy
:
package demo
class SDKBuildPlugin implements Plugin<Project> {
def buildTypes = ["Release", "Debug"]
@Override
void apply(Project project) {
}
}
接下來,為每個Build Type建立構建任務。在apply方法中,新增如下程式碼:
buildTypes.each { v ->
project.task("buildAndroidSDK$v",
dependsOn: ":sdk_android:buildJar$v")
project.task("buildUnityDemo$v") {
dependsOn "cleanUnityDemo", "copyUnitySDKToDemo$v", "copyAndroidSDKToDemo$v"
}
project.task("copyAndroidSDKToDemo$v", type: Copy) {
dependsOn "buildAndroidSDK$v"
from "$androidSDKProjectDir/build/outputs/libs/latest/$v/"
into "$unityDemoProjectDir/Assets/Plugins/Android"
include "*.jar"
}
// ...
}
這裡建立了三個典型的任務,其中buildAndroidSDK
僅宣告其依賴於sdk_android
模組的buildJar
任務,相當於為buildJar
任務建立了一個別名。
其他的構建任務的建立不做贅述。
最後在build.gradle
中應用此外掛:
// build.gradle
import com.tencent.qqmusic.MusicUnitySDKBuildPlugin
// 中間略
apply plugin: MusicUnitySDKBuildPlugin
SDK的釋出任務
SDK對外提供的內容比較繁雜,包括:
- SDK的庫檔案(dll與jar)
- Demo APP或工程
- Demo 工程
- 介面文件
- Change log
這些內容都可以通過gradle的構建任務來自動完成。釋出目錄的結構如下:
Publish
|-- 1.0
| |-- 1.0.0.0
| | |-- Debug
| | |-- Release
| | |-- ChangeLog.md
| | |-- 介面文件.md
| | |-- Demo
| |
| |-- 1.0.0.1
|
|-- 2.0
.
.
構建任務的結構如下圖:
這裡的構建任務都很簡單,不做詳述。注意拷貝Demo工程的時候,需要過濾掉build結果。
至此,我們完成了SDK的構建系統。
如何除錯
C#和Java的除錯都只能通過adb遠端除錯來進行。
首先用USB連線手機,在命令列中輸入adb tcpip 5555
然後進入adb shell,用ifconfig檢視手機的ip地址,之後通過adb connect xxx.xxx.xxx.xxx:5555
連線手機
連線成功之後就可以通過MonoDevelop或者Android Studio的【Attach to process】進行除錯了。
注意:
如果使用Xamarian進行C#程式碼的除錯,可能無法找到【Attach to process】,這時候需要下載這個外掛:
http://forum.unity3d.com/threads/unity-add-ins-for-monodevelop-xamarin-studio-5-9.329880/
如果在Android Studio中無法看到程式的程式,請確保包含Java程式碼的Android工程已經被正確載入
附錄
AndroidJavaObject.Call的實現
這裡分C#和Java兩部分講解。
C#部分
整個呼叫序列如下圖:
簡單來說,整個流程為:
- 通過
GetMethodId
找到方法對應的記憶體地址 - 建立入參,同時處理
AndroidJavaObject
、AndroidJavaProxy
等特殊型別的引數 - 通過記憶體地址呼叫目標方法
其中,最關鍵的部分在於1.1.1 AndroidJNI.CallStaticObjectMethod
,這個方法用於呼叫Android側物件或者類的方法,其中:
- ReflectionHelper_classPtr : 指向Java類com.unity.player.ReflectionHelper
的指標 - getMethodId_ptr : 指向上述Java類的getMethodID
方法的指標 - methodInfo : 包括方法名、簽名、是否靜態方法等資訊
意思就是,呼叫Android側的ReflectionHelper.getMethodID
方法,先在Android側獲取到methodInfo
描述的Method
例項,然後將其指標傳回給Unity側。
Java部分
這部分主要是ReflectionHelper
這個類,負責獲取Android側類的成員(變數、方法、建構函式),以及建立用於AndroidJavaProxy
的Android側proxy物件。
AndroidJavaProxy的實現
首先,我們來看一下如何從AndroidJavaProxy
生成一個java.lang.Proxy
。
在上一節中,我們知道,所有的AndroidJavaObject.Call
方法都會呼叫AndroidJNIHelper.CreateJNIArgArray
方法,該方法就由AndroidJavaProxy
例項生成了一個Proxy
例項:
class _AndroidJNIHelper {
public static jvalue[] CreateJNIArgArray(object[] args) {
// ...
else if (obj is AndroidJavaProxy) {
array[num].l = AndroidJNIHelper.CreateJavaProxy((AndroidJavaProxy)obj);
}
// ...
}
}
雖然AndroidJNIHelper.CreateJavaProxy(AndroidJavaProxy)
這個方法是native的,無法分析其實現,但是我們可以參考_AndroidJNIHelper.CreateJavaProxy(int,AndroidJavaProxy)
方法:
// _AndroidJNIHelper
public static IntPtr CreateJavaProxy(int delegateHandle, AndroidJavaProxy proxy){
return AndroidReflection.NewProxyInstance(delegateHandle, proxy.javaInterface.GetRawClass());
}
兩者的實現應該是類似的,最終都是呼叫Android側的ReflectionHelper.newProxyInstance
方法,用來在Android側建立一個動態代理:
// ReflectionHelper
protected static Object newProxyInstance(int paramInt, final Class[] paramArrayOfClass){
// ..
return Proxy.newProxyInstance(ReflectionHelper.class.getClassLoader(), paramArrayOfClass, new InvocationHandler()
{
public final Object invoke(Object proxy, Method method, Object[] args)
{
return ReflectionHelper.nativeProxyInvoke(this.a, method.getName(), args);
}
// ..
});
}
可以看到,Android側通過ReflectionHelper.nativeProxyInvoke
將該側的方法呼叫代理到了Unity側,而Unity側對應的方法為:
// UnityEngine._AndroidJNIHelper
public static IntPtr InvokeJavaProxyMethod(AndroidJavaProxy proxy, IntPtr jmethodName, IntPtr jargs){
// ...
IntPtr result;
using (AndroidJavaObject androidJavaObject = proxy.Invoke(AndroidJNI.GetStringUTFChars(jmethodName), array)){
if (androidJavaObject == null) {
result = IntPtr.Zero;
} else {
result = AndroidJNI.NewLocalRef(androidJavaObject.GetRawObject());
}
}
return result;
}
最終通過proxy.Invoke
呼叫自己的目標方法。
AndroidManifest.xml
見 https://github.com/yhd4711499/unity_android_plugin/blob/master/AndroidManifest.xml
其中的@string/app_name
,@drawable/app_icon
為Unity專案中包含的資源,與Android專案中的資源無關。
如果同時還用到了自己的Activity,需要將其中的<activity android:name=""/>
改成自己的。
轉載請註明原作者:haodongyuan@tencent.com
相關文章
- Flutter Plugin外掛開發填坑指南FlutterPlugin
- Flutter外掛(Plugin)開發 - Android視角FlutterPluginAndroid
- SuperWebview開發指南AndroidWebViewAndroid
- Android/iOS內嵌Unity開發示例AndroidiOSUnity
- [轉載]Android相關開發網站Android網站
- Android Camera開發指南Android
- YYDS: Webpack Plugin開發WebPlugin
- 如何開發webpack pluginWebPlugin
- Flutter Plugin開發流程FlutterPlugin
- Forte for Java開發指南 (轉)Java
- IDEA Plugin 開發探索IdeaPlugin
- Unity Low-level Native Plugin InterfaceUnityPlugin
- Unity射線(轉載存檔)Unity
- Android開發資源獲取國內代理(轉載)Android
- 《Android 開發工程師面試指南》Android工程師面試
- Word的COM載入項開發指南
- Apache Answer Plugin 開發筆記ApachePlugin筆記
- Android Studio Plugin 外掛開發教程(一) —— 開發你的第一個外掛AndroidPlugin
- 轉載-iOS SDK開發iOS
- Android開發:build.gradle 配置指南AndroidUIGradle
- 【Unity3D開發小遊戲】《戰棋小遊戲》Unity開發教程Unity3D遊戲
- unity遊戲開發雜項系列:unity在商店裡下載的package儲存位置Unity遊戲開發Package
- Android開發工具下載地址Android
- Developing for ARKit 1.5 update using Unity ARKit PlugindevUnityPlugin
- 使用 Unity 開發 Android 遊戲時如何追蹤效能問題UnityAndroid遊戲
- 搭建CARDBOARD+ANDROID+unity3d的VR開發環境AndroidUnity3DVR開發環境
- 轉發:Android開發?用C#!!AndroidC#
- 開啟Android Studio報錯"required plugin “Android Support” is disabled"AndroidUIPlugin
- Android平臺Camera開發實踐指南Android
- Unity 消消樂開發思路Unity
- 開發指南
- unity3d工程Plugin資料夾筆記Unity3DPlugin筆記
- Andorid Studio NDK開發:Experimental PluginPlugin
- 海思HI3751 Android 待機開發指南Android
- 海思HI3751 Android升級開發指南Android
- 【Android開發入門教程】三.Activity入門指南!Android
- RK3288 Android7.1 軟體開發指南Android
- 開發規範(轉載自大牛)