Android Unity Plugin 開(kāi)發(fā)指南(轉(zhuǎn)載)

Unity Android Plugin開(kāi)發(fā)指南
原創(chuàng) 2016-07-05 haodongyuan QQ音樂(lè)技術(shù)團(tuán)隊(duì)


本文將介紹如何在Unity工程中使用Android或者Java的庫(kù),包括:

  • 如何在Unity項(xiàng)目中使用Android Plugin
  • Unity-Android相互調(diào)用
  • Unity接口設(shè)計(jì)的最佳實(shí)踐
  • 如何構(gòu)建Unity-Android混合項(xiàng)目
  • 如何調(diào)試Unity和Android代碼
  • 附錄:跨虛擬機(jī)調(diào)用的實(shí)現(xiàn)

如何在Unity項(xiàng)目中使用Android Plugin


Android Plugin需要包含一個(gè)jar和對(duì)應(yīng)的封裝代碼。后者用來(lái)封裝Android代碼,提供給Unity項(xiàng)目使用。
jar放在Unity項(xiàng)目的/Assets/Plugins/Android
中,Android插件的其他依賴(lài)也放在此處。
封裝代碼可以是C#文件,或者dll文件,都放在/Assets
中,若是dll,需在Unity C#工程中添加此dll依賴(lài)。
此外,如果Android插件包含資源,按照原有目錄結(jié)構(gòu)放到/Assets/Plugins/Android
中即可。
如果需要額外的系統(tǒng)權(quán)限,需要在AndroidManifest.xml中添加,這個(gè)文件默認(rèn)是沒(méi)有的,如果要修改的話(huà),必須手動(dòng)添加一份Unity可用的manifest,可參考【附錄】中提供的模板。
最后,工程的結(jié)構(gòu)類(lèi)似這樣:

Assets
└── Plugins
    ├── Android
    │   ├── AndroidManifest.xml
    │   ├── android_sdk.jar
    │   └── res
    │       ├── values
    │       └── drawable
    └── unity_wrapper.dll

Unity 從5.2.0b3版本開(kāi)始很好地支持aar格式的文件,可以將資源打包進(jìn)aar中,不必再放置到該目錄下

Unity與Android之間相互調(diào)用


準(zhǔn)確來(lái)說(shuō),應(yīng)該是兩個(gè)VM之間的相互調(diào)用:mono/il2cpp 和 dalvik/art,分別運(yùn)行Unity應(yīng)用和Android應(yīng)用,這兩個(gè)虛擬機(jī)運(yùn)行在同一個(gè)進(jìn)程中。
為了方便起見(jiàn),后文將前者稱(chēng)為Unity,后者稱(chēng)為Android


如上圖所示,Unity通過(guò)UnityEngine
提供的API調(diào)用Android的方法;Android借助com.unity.player
包提供的API調(diào)用Unity的方法。
前者可以直接調(diào)用Android對(duì)象或者類(lèi)的方法,而后者只能調(diào)用Unity中指定GameObject
所掛載的腳本的方法,或者通過(guò)動(dòng)態(tài)代理的方式調(diào)用Unity的方法。

Unity調(diào)用Java方法


UnityEngine提供了兩個(gè)類(lèi)來(lái)分別訪(fǎng)問(wèn)Java的實(shí)例對(duì)象以及類(lèi)對(duì)象:AndroidJavaObjectAndroidJavaClass
前者表示java.lang.Object
或其子類(lèi),后者表示java.lang.Class
。他們提供相同的實(shí)例方法:

方法 返回值 說(shuō)明
Call void 調(diào)用實(shí)例方法
Call<T> T 調(diào)用實(shí)例方法
CallStatic void 調(diào)用類(lèi)方法
CallStatic<T> T 調(diào)用實(shí)例方法
Get<T> T 獲取成員變量
GetStatic<T> T 獲取類(lèi)的成員變量
Set(T) void 設(shè)置成員變量
SetStatic(T) void 設(shè)置類(lèi)的成員變量

注意:

  • T的類(lèi)型只能為原始值類(lèi)型(int、long、string等等),或者AndroidJavaObject
    AndroidJavaClass
    ,或者內(nèi)容為原始值類(lèi)型或AndroidJavaObject
    的數(shù)組

  • Get和Set方法直接操作成員變量,而不是通過(guò)getter或setter

下面將通過(guò)一段代碼來(lái)演示:如何獲取一個(gè)AndroidJavaClass
實(shí)例,并且調(diào)用其getInstance
方法獲取其對(duì)象,然后調(diào)用此對(duì)象的方法。
在開(kāi)始之前,先看一下我們用到的Java類(lèi)

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
依賴(lài),該文件位于Unity安裝目錄下的Editor/Data/Managed
目錄中,注意,添加依賴(lài)后,將其設(shè)置為不拷貝到本地。
現(xiàn)在,我們來(lái)獲取Player
這個(gè)類(lèi)并獲取其單例:

AndroidJavaObject player = new AndroidJavaClass("example.Player").CallStatic<AndroidJavaObject>("getInstance");

然后對(duì)player
對(duì)象調(diào)用其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");

注意,返回值類(lèi)型為AndroidJavaObject
的方法有個(gè)共同的缺陷:如果Android側(cè)返回null,該方法將報(bào)錯(cuò):JNI: Init'd AndroidJavaObject with null ptr!

這是因?yàn)椋贏ndroidJavaObject
的構(gòu)造函數(shù)中,遇到IntPtr.Zero(即null)
會(huì)報(bào)錯(cuò):

internal AndroidJavaObject(IntPtr jobject) : this(){
  if (jobject == IntPtr.Zero){
    throw new Exception("JNI: Init'd AndroidJavaObject with null ptr!");
  }
  // ...
}

該缺陷存在于5.3.1f1版本之前的UnityEngine,一個(gè)可行的辦法是:先獲取Android方法返回結(jié)果的指針,如果指針為空就返回null,否則返回指針的對(duì)象。

Android調(diào)用Unity方法


在Android中,有兩種方式調(diào)用Unity的方法:

  • 通過(guò)AndroidJavaProxy進(jìn)行無(wú)感知調(diào)用
  • 通過(guò)com.unity3d.player.UnityPlayer.UnitySendMessage方法顯式調(diào)用

ndroidJavaProxy

AndroidJavaProxy常用于在Unity中實(shí)現(xiàn)Java的interface,比如有這么一個(gè)java interface:

package demo;

interface PlayStateListener {
  void onBufferFinished(SongInfo songInfo);
  void onBufferProgress(String songId, long buffered, long total);
}

對(duì)應(yīng)的C#類(lèi)就是這樣:

class PlayStateChangedHandler : AndroidJavaProxy {
  internal PlayStateChangedHandler() : base(new AndroidJavaClass("demo.PlayStateListener")) {}
  public void onBufferFinished(AndroidJavaObject songInfoObject) {}
  public void onBufferProgress(string songId, long buffered, long total) {}
}

有幾點(diǎn)需要注意:

  1. Unity側(cè)的方法必須為public,且有相同的名稱(chēng)和類(lèi)似的簽名
  2. 如果Android側(cè)方法的傳參或返回值為類(lèi)類(lèi)型,對(duì)應(yīng)Unity側(cè)只能為AndroidJavaObject
  3. 4.6.8f1版本的UnityEngine有BUG,無(wú)法在AndroidJavaProxy
    中傳遞long類(lèi)型的值,該問(wèn)題在Unity 5中已經(jīng)修復(fù)

有關(guān)AndroidJavaProxy的實(shí)現(xiàn),在附錄中有詳細(xì)介紹

UnityPlayer.UnitySendMessage

這需在Android工程中添加Unity提供的jar依賴(lài),它位于Unity安裝目錄下:/Editor/Data/PlaybackEngines/AndroidPlayer/Viariations/{backend}/{buildType}/Classes/classes.jar

其中,backend是Unity項(xiàng)目腳本執(zhí)行器的類(lèi)型,有mono和il2cpp兩種,與Unity項(xiàng)目的”Script Backend”一致。
然后通過(guò)以下代碼來(lái)訪(fǎng)問(wèn)掛載在TGameObj對(duì)象上的腳本的OnButtonClick方法:UnityPlayer.UnitySendMessage("TGameObj", "OnButtonClick", "Greetings from Java");

Unity接口設(shè)計(jì)的最佳實(shí)踐


本節(jié)將介紹一個(gè)用于封裝Java代碼的通用設(shè)計(jì)方式,可以高效地將Java代碼的API“移植”到C#,同時(shí)保持可擴(kuò)展性。該設(shè)計(jì)將Java代碼中的類(lèi)及其結(jié)構(gòu)反射到C#代碼中,至于該類(lèi)的細(xì)節(jié)(比如繼承關(guān)系、接口實(shí)現(xiàn)等)將被忽略,因?yàn)樾枰瓷涞亩际潜┞督o用戶(hù)的API接口,用戶(hù)不應(yīng)該關(guān)心這些細(xì)節(jié)。
如下圖所示:


Java中的demo.Foo類(lèi)通過(guò)Reflection反射到C#的Mirrored.Foo中,demo.Foo中的公共字段和方法都按照原有結(jié)構(gòu)被反射。

注意,這里的反射只是單向地從Java反射到C#。如果要從C#反射到Java,可以參考本節(jié)進(jìn)行擴(kuò)展。

反射的實(shí)現(xiàn)


在開(kāi)始之前,我們需要明確哪些類(lèi)需要反射。對(duì)于int, long, double等原始類(lèi)型以及string類(lèi)型,UnityEngine已經(jīng)幫我們處理好了,只剩下java.lang.Object的派生類(lèi)需要我們反射。

反射基類(lèi)的設(shè)計(jì)

我們使用AndroidObjectMirror作為反射類(lèi)的父類(lèi)。

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這個(gè)反射對(duì)象被創(chuàng)建時(shí),被反射對(duì)象的引用計(jì)數(shù)將會(huì)增加(AndroidJNISafe.NewGlobalRef),在Dispose
方法中,其引用計(jì)數(shù)將會(huì)減少(AndroidJNISafe.DeleteGlobalRef)。

之后,子類(lèi)通過(guò)覆寫(xiě)InitFromJava方法來(lái)進(jìn)行成員變量的初始化:
子類(lèi)可以創(chuàng)建和被反射類(lèi)“一樣的”方法,并將所有的調(diào)用委托給成員變量AJObject即可。例如:

int Add(int a, int b) { 
 return AJObject.Call<int>("add", a, b);
}

總結(jié)一下,反射的邏輯如下圖所示:

反射的實(shí)現(xiàn)

借助于AndroidObjectMirror
,我們可以這樣來(lái)定義上文提及的example.Player
的反射類(lèi):

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"); }
  // ..
}

注意,在獲取單例時(shí),我們用了這樣一行代碼:
return Reflection.Reflect(javaObject);

Reflection這個(gè)工具類(lèi)用來(lái)反射Java的對(duì)象,即將AndroidJavaObject的對(duì)象反射為派生自AndroidObjectMirror的類(lèi)的對(duì)象。
其中的Reflect方法是這樣實(shí)現(xiàn)的:

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);
    }
}

這里的邏輯很直接:創(chuàng)建一個(gè)AndroidJavaObject對(duì)象ajo,然后在InitFromJava方法中通過(guò)ajo來(lái)初始化這個(gè)對(duì)象的成員變量。注意,這里約束了類(lèi)型T必須提供無(wú)參公共構(gòu)造函數(shù),因此AndroidJavaMirror必須通過(guò)InitFromJava(AndroidJavaObject)來(lái)初始化,而沒(méi)有將AndroidJavaObject放在構(gòu)建函數(shù)中。
至于InitFromJava方法,它可以是這樣:

protected override void InitFromJava(AndroidJavaObject ajo) {
    title = ajo.Get<string>("title");
}

或者類(lèi)似反序列化的方式在運(yùn)行時(shí)進(jìn)行解析:

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);
  }
}

如何構(gòu)建Unity-Android混合項(xiàng)目


本節(jié)將介紹如何使用Gradle來(lái)構(gòu)建混合了不同平臺(tái)項(xiàng)目的工程。
以一個(gè)SDK類(lèi)型的工程為例,我們來(lái)看一下工程的內(nèi)容:

  1. Android SDK
  2. Android Demo (快速調(diào)試)
  3. Unity Bridge (封裝Android SDK)
  4. Unity Demo (演示并調(diào)試Unity Bridge)

目錄結(jié)構(gòu)如下:

RootDir (工程根目錄)
|
|-- Android (Android相關(guān)模塊)
|   |-- Demo_Android
|   |-- SDK_Android
|
|-- Unity (Unity相關(guān)模塊)
    |-- Demo_Unity
    |-- Bridge_Unity

其中:

  • Android的兩個(gè)模塊可以用Android gradle插件進(jìn)行編譯與打包
  • Bridge_Unity可以用msbuild(windows)或者xbuild(linux)構(gòu)建
  • Demo_Unity需要購(gòu)買(mǎi)了Unity Pro之后才能自動(dòng)化構(gòu)建。

接下來(lái),我們將在各自模塊的構(gòu)建腳本中添加構(gòu)建任務(wù),分別構(gòu)建這些模塊,最后,在工程的根構(gòu)建腳本中,創(chuàng)建自動(dòng)化的構(gòu)建腳本。

Android SDK的構(gòu)建


Jar包構(gòu)建任務(wù)

SDK將以Jar的形式提供給Unity Bridge使用,因此需要添加打包成jar的構(gòu)建任務(wù)。我們利用已有的Android構(gòu)建任務(wù)鏈,創(chuàng)建Jar構(gòu)建任務(wù)。
已有的構(gòu)建腳本位于RootDir/Android/SDK_Android/build.gradle,在其中加入Jar構(gòu)建任務(wù):

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后面的閉包會(huì)在gradle腳本構(gòu)建時(shí)運(yùn)行,用來(lái)定義此任務(wù)的屬性:

  • archiveName: 輸出Jar包的文件名,默認(rèn)為模塊名稱(chēng)
  • dependsOn: 此任務(wù)的依賴(lài)
  • from: 要打包的class

這里需要注意:
依賴(lài)
dependsOn: v.javaCompile
此任務(wù)必須在v.javaCompile完成之后運(yùn)行,即java文件被編譯成class文件之后再將這些class打包成Jar。
要打包的class

from v.javaCompile.destinationDir, configurations.compile.collect {
  it.isDirectory() ? it : zipTree(it)
}

這里說(shuō)明要打包的class有兩處:模塊自身的和依賴(lài)的Jar包。

Proguard構(gòu)建任務(wù)

對(duì)外發(fā)布時(shí),通常需要對(duì)代碼進(jìn)行混淆。對(duì)于我們自定義的Jar任務(wù),必須手動(dòng)添加混淆任務(wù):

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",這樣就將混淆任務(wù)和jar任務(wù)串聯(lián)了起來(lái)。

發(fā)布任務(wù)

為了便于其他構(gòu)件腳本獲取此模塊的最新構(gòu)建結(jié)果,我們將輸出的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構(gòu)建任務(wù)添加完成。

Android Demo的構(gòu)建


Andriod Gradle Plugin已經(jīng)提供Demo的構(gòu)建任務(wù)。

Unity Bridge的構(gòu)建


在開(kāi)始之前,我們需要配置好構(gòu)建環(huán)境:對(duì)于Windows系統(tǒng),需要用到msbuild,它會(huì)隨著Visutal Studio一同安裝;對(duì)于linux/unix系統(tǒng),可以使用xbuild,它是Mono里面的一個(gè)工具。下文將使用xbuild。

準(zhǔn)備工作完成后,我們來(lái)創(chuàng)建CSharpBuildTask
這個(gè)構(gòu)建任務(wù)。該任務(wù)其實(shí)就是封裝了對(duì)xbuild的調(diào)用:

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下,這樣就可以在所有子模塊中使用該任務(wù)。
之后,在RootDir/Unity/Bridge_Unity目錄下創(chuàng)建build.gradle文件,作為此模塊的構(gòu)建腳本。內(nèi)容為:

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,內(nèi)容為:

# local.properties 
msbuild.dir=/usr/local/bin/xbuild

至此,我們用gradle查看一下是否成功創(chuàng)建了此構(gòu)建任務(wù):

$ gradlew tasks


可以看到,buildLib構(gòu)建任務(wù)已經(jīng)創(chuàng)建。

Unity Demo的構(gòu)建


受限于Unity,只有Unity Pro及以上版本才支持代碼或者命令行的方式進(jìn)行構(gòu)建。
首先,我們需要在/Asset/Editor中創(chuàng)建一個(gè)腳本,通過(guò)BuildPipeLine來(lái)構(gòu)建Unity工程:

public class BuildScript: MonoBehaviour{
  static void BuildAndroid(){
    string[] scenes = {"Assets/Demo.unity"};
    BuildPipeline.BuildPlayer(scenes, "AndroidBuild", BuildTarget.Android, BuildOptions.None);
  }
}

然后,在RootDir/buildSrc中創(chuàng)建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
中創(chuàng)建build.gradle,并在其中創(chuàng)建一個(gè)構(gòu)建任務(wù):

// 有關(guān)localProps見(jiàn)前文
task("buildUnityDemo",type: UnityBuildTask, group: 'build') {
  unityExePath = localProps["unitybuild.dir"].toString();
}

BuildPipeLine以及Unity的命令行調(diào)用可以參考官方文檔:http://docs.unity3d.com/Manual/CommandLineArguments.html

混合構(gòu)建


上面已經(jīng)介紹了各個(gè)模塊各自的構(gòu)建方法,現(xiàn)在,我們將在根模塊的構(gòu)建腳本中將他們串聯(lián)起來(lái)。

首先來(lái)梳理一下所有構(gòu)建任務(wù)之間的依賴(lài)關(guān)系,已有的構(gòu)建任務(wù)有:

其中,箭頭表示依賴(lài)關(guān)系,Unity的Demo同時(shí)依賴(lài)于Unity和Android的SDK,同時(shí)還要將生成的SDK拷貝到Unity Demo項(xiàng)目中的特定位置,這樣Demo才能正常運(yùn)行。

這些構(gòu)建任務(wù)的依賴(lài)關(guān)系如下圖所示:

我們?cè)诟K中創(chuàng)建這些構(gòu)建任務(wù):
  • copyUnitySDKToDemo:將生成的Unity SDK拷貝到Unity Demo
  • copyAndroidSDKToDemo:將生成的Android SDK拷貝到Unity Demo
  • buildUnitySDK:buildLib的馬甲
  • buildAndroidSDK:buildJar的馬甲
  • buildUnityDemo:構(gòu)建Unity demo
  • buildAndroidDemo: 構(gòu)建Android demo

我們可以在根模塊的build.gradle中添加這些任務(wù),但會(huì)使得build.gradle
變得非常混亂。因此我們采用Plugin的方式,來(lái)進(jìn)行這些任務(wù)的創(chuàng)建。
現(xiàn)在我們來(lái)創(chuàng)建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) {
  }
}

接下來(lái),為每個(gè)Build Type創(chuàng)建構(gòu)建任務(wù)。在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"
    }
  // ...
}

這里創(chuàng)建了三個(gè)典型的任務(wù),其中buildAndroidSDK僅聲明其依賴(lài)于sdk_android模塊的buildJar任務(wù),相當(dāng)于為buildJar任務(wù)創(chuàng)建了一個(gè)別名。
其他的構(gòu)建任務(wù)的創(chuàng)建不做贅述。
最后在build.gradle中應(yīng)用此插件:

// build.gradle
import com.tencent.qqmusic.MusicUnitySDKBuildPlugin
// 中間略
apply plugin: MusicUnitySDKBuildPlugin

SDK的發(fā)布任務(wù)


SDK對(duì)外提供的內(nèi)容比較繁雜,包括:

  • SDK的庫(kù)文件(dll與jar)
  • Demo APP或工程
  • Demo 工程
  • 接口文檔
  • Change log

這些內(nèi)容都可以通過(guò)gradle的構(gòu)建任務(wù)來(lái)自動(dòng)完成。發(fā)布目錄的結(jié)構(gòu)如下:

Publish
|-- 1.0
|   |-- 1.0.0.0
|   |   |-- Debug
|   |   |-- Release
|   |   |-- ChangeLog.md
|   |   |-- 接口文檔.md
|   |   |-- Demo
|   |
|   |-- 1.0.0.1
|
|-- 2.0
.   
.

構(gòu)建任務(wù)的結(jié)構(gòu)如下圖:


這里的構(gòu)建任務(wù)都很簡(jiǎn)單,不做詳述。注意拷貝Demo工程的時(shí)候,需要過(guò)濾掉build結(jié)果。
至此,我們完成了SDK的構(gòu)建系統(tǒng)。

如何調(diào)試


C#和Java的調(diào)試都只能通過(guò)adb遠(yuǎn)程調(diào)試來(lái)進(jìn)行。
首先用USB連接手機(jī),在命令行中輸入adb tcpip 5555
然后進(jìn)入adb shell,用ifconfig查看手機(jī)的ip地址,之后通過(guò)adb connect xxx.xxx.xxx.xxx:5555連接手機(jī)
連接成功之后就可以通過(guò)MonoDevelop或者Android Studio的【Attach to process】進(jìn)行調(diào)試了。
注意:
如果使用Xamarian進(jìn)行C#代碼的調(diào)試,可能無(wú)法找到【Attach to process】,這時(shí)候需要下載這個(gè)插件:
http://forum.unity3d.com/threads/unity-add-ins-for-monodevelop-xamarin-studio-5-9.329880/
如果在Android Studio中無(wú)法看到程序的進(jìn)程,請(qǐng)確保包含Java代碼的Android工程已經(jīng)被正確載入

附錄


AndroidJavaObject.Call的實(shí)現(xiàn)


這里分C#和Java兩部分講解。
C#部分
整個(gè)調(diào)用序列如下圖:


簡(jiǎn)單來(lái)說(shuō),整個(gè)流程為:

  1. 通過(guò)GetMethodId找到方法對(duì)應(yīng)的內(nèi)存地址
  2. 創(chuàng)建入?yún)ⅲ瑫r(shí)處理AndroidJavaObjectAndroidJavaProxy等特殊類(lèi)型的參數(shù)
  3. 通過(guò)內(nèi)存地址調(diào)用目標(biāo)方法

其中,最關(guān)鍵的部分在于1.1.1 AndroidJNI.CallStaticObjectMethod,這個(gè)方法用于調(diào)用Android側(cè)對(duì)象或者類(lèi)的方法,其中:

  • ReflectionHelper_classPtr : 指向Java類(lèi)com.unity.player.ReflectionHelper
    的指針
  • getMethodId_ptr : 指向上述Java類(lèi)的getMethodID
    方法的指針
  • methodInfo : 包括方法名、簽名、是否靜態(tài)方法等信息

意思就是,調(diào)用Android側(cè)的ReflectionHelper.getMethodID方法,先在Android側(cè)獲取到methodInfo描述的Method實(shí)例,然后將其指針傳回給Unity側(cè)。

Java部分


這部分主要是ReflectionHelper這個(gè)類(lèi),負(fù)責(zé)獲取Android側(cè)類(lèi)的成員(變量、方法、構(gòu)造函數(shù)),以及創(chuàng)建用于AndroidJavaProxy的Android側(cè)proxy對(duì)象。

AndroidJavaProxy的實(shí)現(xiàn)


首先,我們來(lái)看一下如何從AndroidJavaProxy生成一個(gè)java.lang.Proxy
在上一節(jié)中,我們知道,所有的AndroidJavaObject.Call方法都會(huì)調(diào)用AndroidJNIHelper.CreateJNIArgArray方法,該方法就由AndroidJavaProxy實(shí)例生成了一個(gè)Proxy實(shí)例:

class _AndroidJNIHelper {
  public static jvalue[] CreateJNIArgArray(object[] args) {
    // ...
    else if (obj is AndroidJavaProxy) {
      array[num].l = AndroidJNIHelper.CreateJavaProxy((AndroidJavaProxy)obj);
    }
    // ...
  }
}

雖然AndroidJNIHelper.CreateJavaProxy(AndroidJavaProxy)這個(gè)方法是native的,無(wú)法分析其實(shí)現(xiàn),但是我們可以參考_AndroidJNIHelper.CreateJavaProxy(int,AndroidJavaProxy)方法:

// _AndroidJNIHelper
public static IntPtr CreateJavaProxy(int delegateHandle, AndroidJavaProxy proxy){
  return AndroidReflection.NewProxyInstance(delegateHandle, proxy.javaInterface.GetRawClass());
}

兩者的實(shí)現(xiàn)應(yīng)該是類(lèi)似的,最終都是調(diào)用Android側(cè)的ReflectionHelper.newProxyInstance方法,用來(lái)在Android側(cè)創(chuàng)建一個(gè)動(dòng)態(tài)代理:

// 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側(cè)通過(guò)ReflectionHelper.nativeProxyInvoke將該側(cè)的方法調(diào)用代理到了Unity側(cè),而Unity側(cè)對(duì)應(yīng)的方法為:

// 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;
}

最終通過(guò)proxy.Invoke調(diào)用自己的目標(biāo)方法。

AndroidManifest.xml


見(jiàn) https://github.com/yhd4711499/unity_android_plugin/blob/master/AndroidManifest.xml
其中的@string/app_name@drawable/app_icon為Unity項(xiàng)目中包含的資源,與Android項(xiàng)目中的資源無(wú)關(guān)。
如果同時(shí)還用到了自己的Activity,需要將其中的<activity android:name=""/>
改成自己的。

轉(zhuǎn)載請(qǐng)注明原作者:haodongyuan@tencent.com

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容