1 原理
1.1 什么是ANE
ANE,全名Adobe Air Native Extension, 中文就是Adobe Air本地扩展。
1.1.1 什么是Adobe Air
学习新知识的一个有效途径当然是从它的出处入手。下面链接就是Adobe官方对于Adobe Air的介绍,如果你已经是Adobe Air开发人员了,那我觉得你可以跳过。
Adobe air官方文档介绍
Adobe air开发人员中心
总结一下Adobe Air:
① 是一个跨操作系统的运行时(类似于java运行时)
② 你可以通过运行时将你开发的Air应用(游戏)部署到桌面或移动设备上
③ 运行基于swf构建的应用(适用于桌面、移动设备)。即通过ActionScript3.0(Adobe官方发布的语言)开发的应用。
④ 运行基于html构建的应用(只适用于桌面)。
本文只涉及移动设备,桌面设备可举一反三
1.1.2 什么是本地扩展(Native Extension)
你通过Adobe Air开发工具开发了一款Air应用,该应用必然运行在桌面或移动设备上,那么这些设备在Air应用看来就是一个本地设备,也就是基础。
那什么是扩展?比如移动设备有音量控制的功能,然而Air应用本身却无法控制移动设备的本地功能,为了使用移动设备的音量控制功能,air应用就需要音量控制功能的本地扩展来帮助完成。类似这样的需求会有很多,而且不同的应用想要的效果也不尽相同。于是Adobe的工程师们基于这样的需求提供了一套解决办法。你只需要按照他们规定的步骤去做,你就能够让你的Air应用像原生的应用一样控制和使用移动设备的功能了。这套解决办法就是ANE(本地扩展),它能够将你的air应用的能力扩展到本地设备上。
当然互联网中也存在着很多大牛实现的一些ANE。奉行不重复造*和节省成本的原则,如果他人提供的ANE能够满足需求,那就首选已有的ANE。不过,你想制作定制你自己的功能的ANE,可以按照下文操作。
PS:ANE虽然在Adobe Air应用(游戏)开发中被称为高级部分,它绝没有你想象的那么困难。或许你不懂ActionScript,也不了解安卓,也不怎么知道IOS。没关系,只要你懂得一门面向对象编程语言,那么以下内容对你来说没有障碍。语法这个东西,懂一门你可触类旁通(只要你愿意)。只要你能理解代码逻辑,之后你可以再深入学习。至于安卓和IOS的信息,我会在文中与ANE相关的内容处做简要说明,可以根据我提供的链接深入学习。
1.2 ANE调用原理(ANE怎么起作用的)
ANE调用原理图
说明几点ANE调用原理图:
① 建立Air应用与本地设备的联系的关键就是ane文件
② ANE内部核心1是ActionScript库(swc文件)。它建立Air应用与本地代码的联系
③ ANE内部核心2是本地代码。也就是Android-java类库/Object-C类库。它建立ActionScript库与本地设备的联系
④ 他们的联系遵循着一种规范
本文围绕着ANE展开的,如果你想深入学习其他相关知识,可以看看一下链接。
官方相关知识内容
Adobe ActionScript 3.0 * 使用本手册
ActionScript 3.0 API
面向 ActionScript 开发人员的 Adobe AIR 教程
官方安卓开发培训内容(需要*)
安卓开发培训内容(不需要*)
官方IOS开发教学内容(纯英文good luck)
OK,原理应该很好理解,那具体是怎么做的呢?遵循的规范又是什么?看第二部分实现。
2 实现
2.1 准备制作ANE的材料
通过第一部分ANE原理的描述,我们应该可以看出下一步要做的工作:
①制作一个ActionScript库
②制作一个Android(或IOS)本地代码库。
当然还有一些细节需要我们在下面的制作过程中完善。
2.1.1 实现ActionScript类库
① 创建一个ActionScript库项目,选择图一还是图二根据你的情况来定,他们只是提供的部分库不一样,对于我们的例子来说不影响。
创建名为MySWCLib的项目
再创建一个名为EntryPoint的ActionScript类,我们以这个作为扩展的入口,包名com.xuemin.myswclib
OK,工程创建结束。
代码有以下三点:
① 继承EventDispatcher
public class EntryPoint extends EventDispatcher
EventDispatcher是可调度事件的所有类的基类,事件模型的重要组成部分)暂时不懂没关系
② 创造一个ExtensionContext
ExtensionContext.createExtensionContext(EXTENSION_ID, "");
通过ExtensionContext来访问本地代码
可以通过它的call()方法来调用本地方法
EXTENSION_ID:是我们创建的扩展的唯一标识
③ 给ExtensionContext添加事件监听
extensionContext.addEventListener(StatusEvent.STATUS, onStatus);
异步的本地代码结果会传递到该监听中
PS:如果在你的项目中没有找到ExtensionContext类的话,看看项目的配置是否勾选了包括Adobe AIR库,看下图
OK,经过以上步骤,我们的代码大致应该是这样的
package com.xuemin.myswclib
{
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.StatusEvent;
import flash.external.ExtensionContext;
public class EntryPoint extends EventDispatcher {
private static var instance:EntryPoint;
private static var extensionContext:ExtensionContext;
public static const EXTENSION_ID:String = "com.xuemin.myane"; // 我们编写的ane的唯一标识
public function EntryPoint()
{
extensionContext = ExtensionContext.createExtensionContext(EXTENSION_ID, ""); // 创建一个与本地代码调用的ExtensionContext
extensionContext.addEventListener(StatusEvent.STATUS, onStatus); // 添加回调事件返回监听
}
public static function getInstance():EntryPoint { //类单例方式实现
if(instance == null)
instance = new EntryPoint();
return instance;
}
protected function onStatus(event:StatusEvent):void { // 本地代码的回调会返回到该方法
}
}
}
好,这就是一个ActionScript库的基本结构,下面具体描述与本地代码交互。上面叙述了通过ExtensionContext来访问本地代码,通过它的call()方法来调用本地方法,具体代码如下:
public static function getNativeData():String {
var params1:int = 3;
var params2:String = "paramsString";
if(extensionContext != null)
return extensionContext.call("getNativeData",params1,params2) as String;
else
return null;
}
① call()方法中的第一个参数是本地代码中函数名称,它可以不是本地方法实际的名称,但是必须是约定好的,也就是ActionScript与本地代码中的名称要相同
② call()方法除了第一个之后是一个参数列表,参数可以是ActionScript中的原始数据类型或其ActionScript类对象
通过上面的代码,你可以简单的调用一个本地方法,并向本地方法传递参数,然后再获得本地方法处理后的结果。但是实际的应用情境明显要复杂很多,当你调用一个本地方法后,你不希望air应用停在那里,而是可以继续进行其他工作,当本地方法处理好之后再把结果回调给你,很明显上面的方法的返回结果是无法满足这个需求的。
这个时候就用到了上面addEventListener方法的监听了。它可以获取本地代码发送回来的数据。返回的结果中我们最常用到两个参数code和level
code:作为回调的标识
level:传递的内容
设置监听和监听收到的内容代码大致如下:
// 添加回调事件返回监听
extensionContext.addEventListener(StatusEvent.STATUS, onStatus);
protected function onStatus(event:StatusEvent):void { // 本地代码的回调会返回到该方法
this.dispatchEvent(event)
if(event.code == "nativeCallbackSuccess") { //本地回调成功的信息
trace(event.level); // 将回调信息打印
} else if(event.code == "nativeCallbackFailed") { //本地回调失败的信息
trace(event.level); // 将回调信息打印
}
}
当然,例子代码中仅仅是将返回的level数据打印了出来,你完全可以再创建一个ActionScript接口,然后将event.level的数据处理完后通过接口回调给air应用。
OK,ActionScript库的编写就这么多了。总结一下吧
① 创建ExtensionContext,这是ActionScript与本地代码的桥梁
② 调用call()方法来调用本地方法,其中参数是方法名并且要保持一致
③ 异步的回调方式,ActionScript会在addEventListener方法添加的监听中获得返回的数据
代码如下:
package com.xuemin.myswclib {
import flash.events.EventDispatcher;
import flash.events.StatusEvent;
import flash.external.ExtensionContext;
public class EntryPoint extends EventDispatcher {
private static var instance:EntryPoint;
private static var extensionContext:ExtensionContext;
public static const EXTENSION_ID:String = "com.xuemin.myane"; // 我们编写的ane的唯一标识
public function EntryPoint() {
extensionContext = ExtensionContext.createExtensionContext(EXTENSION_ID, ""); // 创建一个与本地代码调用的ExtensionContext
extensionContext.addEventListener(StatusEvent.STATUS, onStatus); // 添加回调事件返回监听
}
public static function getInstance():EntryPoint { //类单例方式实现
if(instance == null)
instance = new EntryPoint();
return instance;
}
protected function onStatus(event:StatusEvent):void { // 本地代码的回调会返回到该方法
this.dispatchEvent(event)
if(event.code == "nativeCallbackSuccess") { //本地回调成功的信息
trace(event.level); // 将回调信息打印
} else if(event.code == "nativeCallbackFailed") { //本地回调失败的信息
trace(event.level); // 将回调信息打印
}
}
public static function getNativeData():String {
var params1:int = 3;
var params2:String = "paramsString";
if(extensionContext != null)
return extensionContext.call("getNativeData",params1,params2) as String;
else
return null;
}
}
}
2.1.2 实现本地代码
2.1.2.1 实现Android本地代码
首先创建一个Android工程,默认你了解这个过程,可参考以下链接
官方安卓开发培训内容(需要*)
安卓开发培训内容(不需要*)
创建的工程大致如下图:
由于这套规范必然是Adobe的工程师们设计的,所以我们得通过他们提供的入口和规范来实现我们想要的功能,因此我们需要Adobe提供的java库了,路径在/AirSDK根目录/lib/android/FlashRuntimeExtensions.jar
将FlashRuntimeExtensions.jar添加到你的安卓工程中
如果你还不知道最新的air sdk在哪里下载,点击air_sdk下载链接
对比ActionScript部分,步骤如下:
① 实现FREExtension接口
FREExtension是扩展的入口,需要返回一个FREContext的对象,通过接口定义的createContext可以获得本地代码的连接器(上下文)
public class NativeExtension implements FREExtension{
@Override
public FREContext createContext(String arg0) { //需要返回一个FREContext对象
return null;
}
@Override
public void dispose() { // Extension结束后的操作
}
@Override
public void initialize() { //Extension初始化的操作
}
}
② 实现FREContext对象。
该对象就是本地代码的连接器(上下文),通过该对象的getFunctions()返回给ActionScript能够调用到的方法集合,该方法集合都是FREFunction的子类
public class NativeContext extends FREContext{
@Override
public Map<String, FREFunction> getFunctions() { // 需要返回一个包含FREFunction的Map集合
return null;
}
@Override
public void dispose() { // Context结束后的操作
}
}
把FREContext传递给Extension
@Override
public FREContext createContext(String arg0) { //需要返回一个FREContext对象
NativeContext context = new NativeContext();
return context;
}
③ 实现FREFunction接口
该接口定义了一个方法,叫做call,是不是很熟悉,对,这个call方法与ActionScript中的ExtensionContext的call方法是对应的,这也是它们沟通的最后一步
public class GetNativeDataFunction implements FREFunction{
@Override
public FREObject call(FREContext arg0, FREObject[] arg1) {
return null;
}
}
注意几点:
① 参数一arg0,也就是FREContext对象。它有一些实用的方法,如获取Activity,Activity在Android中是一个很重要的组件,它负责与用户交互,可以加载UI并且根据不同情况有对应的生命周期方法。
Activity activity = arg0.getActivity();
② 参数二arg1,这是个FREObject数组。它对应的就是ActionScript中通过ExtensionContext的call方法传入的参数,代码如下,因为会抛异常,所以需要try-catch:
try { int params1 = arg1[0].getAsInt(); // 获取第一个参数 String params2 = arg1[1].getAsString(); // 获取第二个参数 } catch (IllegalStateException e) { e.printStackTrace(); } catch (FRETypeMismatchException e) { e.printStackTrace(); } catch (FREInvalidObjectException e) { e.printStackTrace(); } catch (FREWrongThreadException e) { e.printStackTrace(); }
③ 返回值FREObject,本地代码返回的数据必须是FREObject对象
可以通过FREObject.newObject()创建对象,创建过程可能抛异常,加入try-catch
FREObject freObject = null; try { String nativeData = params1 + params2; freObject = FREObject.newObject(nativeData); } catch (FREWrongThreadException e) { e.printStackTrace(); }
为了便于理解,代码写的有些冗余:
public class GetNativeDataFunction implements FREFunction{
@Override
public FREObject call(FREContext arg0, FREObject[] arg1) {
Activity activity = arg0.getActivity();
Log.i("ANE", activity.getPackageName()); // 使用安卓的日志方法打印出应用的包名
int params1 = 0;
String params2 = null;
try {
params1 = arg1[0].getAsInt(); // 获取第一个参数
params2 = arg1[1].getAsString(); // 获取第二个参数
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (FRETypeMismatchException e) {
e.printStackTrace();
} catch (FREInvalidObjectException e) {
e.printStackTrace();
} catch (FREWrongThreadException e) {
e.printStackTrace();
}
FREObject freObject = null;
try {
String nativeData = params1 + params2; //将参数1与参数2拼接
freObject = FREObject.newObject(nativeData);
} catch (FREWrongThreadException e) {
e.printStackTrace();
}
return freObject; //返回结果
}
}
④ 将实现的FREFunction添加到FREContext的getFunctions集合中
@Override
public Map<String, FREFunction> getFunctions() { // 需要返回一个包含FREFunction的Map集合
Map<String, FREFunction> map = new HashMap<String, FREFunction>();
map.put("getNativeData", new GetNativeDataFunction());
return map;
}
map的第一个参数就是与ActionScript约定好的方法名
至此简单方法的连接完成了,下面我们再完善一些细节。
① 异步方法结果的传递
ActionScript部分已经说过可以通过addEventListener方法添加监听来获取本地代码发送回来的结果,那本地代码如何发送呢?第一反应还是连接的桥梁FREContext,果然它有一个方法是dispatchStatusEventAsync(code,level)
通过该方法我们可以向ActionScript传递数据了。
由于FREContext的对象是在FREExtension的createContext()方法创建的,所以我们再改装一下,代码如下:
public class NativeExtension implements FREExtension{
private static NativeContext context;
@Override
public FREContext createContext(String arg0) { //需要返回一个FREContext对象
context = new NativeContext();
return context;
}
@Override
public void dispose() { // Extension结束后的操作
}
@Override
public void initialize() { //Extension初始化的操作
}
public static void dispatchStatusEventAsync(String code, String level) {
if(context != null) {
context.dispatchStatusEventAsync(code, level);
}
}
}
OK,通过调用FREExtension的静态方法dispatchStatusEventAsync就可以发送数据了,当然你也可以按照你的思路处理,比如提供获取context的方法再调用等。
② 关于安卓的生命周期的处理
了解点安卓的同学应该知道,常常会用到Activity的生命周期的方法,那在本地代码中如何处理?Adobe工程师设计了一个Activity的包装类AndroidActivityWrapper,通过给包装类注册监听来监听安卓Activity的变化。
你会发现在扩展的jar包并没有这个类,那是因为扩展还是上面一层,而安卓的Activity涉及到了设备代码,所以它自然而然处在了Air的Runtime中,所以你还需要一个runtime的jar包。在路径/Air_SDK根目录/lib/android/lib/runtimeClasses.jar
导入runtimeClasses.jar,OK,可以找到AndroidActivityWrapper类了
同样我们监听还需要在runtime中的StateChangeCallback接口和ActivityResultCallback接口
注意一点:反编译runtimeClasses.jar看AndroidActivityWrapper类中StateChangeCallback接口和ActivityResultCallback接口时,发现这两个接口时protected的,也就是说,如果要实现这两个接口就需要与AndroidActivityWrapper在同一个包下,即在
com.adobe.air
包下。
static abstract interface ActivityResultCallback{ public abstract void onActivityResult(int paramInt1, int paramInt2, Intent paramIntent); }
static abstract interface StateChangeCallback { public abstract void onActivityStateChanged(AndroidActivityWrapper.ActivityState paramActivityState); public abstract void onConfigurationChanged(Configuration paramConfiguration); }
所以,按照以下实现思路操作:
在com.adobe.air包路径下创建新接口继承ActivityResultCallback和StateChangeCallback,但是把权限放大到public
于是在com.adobe.air下先创建抽象接口,继承ActivityResultCallback和StateChangeCallback,代码如下:
package com.adobe.air;
import com.adobe.air.AndroidActivityWrapper.ActivityResultCallback;
import com.adobe.air.AndroidActivityWrapper.StateChangeCallback;
public abstract interface LifeCycle extends ActivityResultCallback, StateChangeCallback{}
通过上面的方法,在我们自己的代码中就可以处理生命周期了
于是再改造一下NativeContext类,实现我们自己的LifeCycle接口
在构造函数中添加监听,在dispose方法中移除监听
public class NativeContext extends FREContext implements LifeCycle{
private AndroidActivityWrapper activityWrapper;
public NativeContext() {
activityWrapper = AndroidActivityWrapper.GetAndroidActivityWrapper(); //ActivityWrapper
activityWrapper.addActivityResultListener(this); // 监听onActivityResult方法
activityWrapper.addActivityStateChangeListner(this); // 监听生命周期方法
}
@Override
public void dispose() { // Context结束后的操作
if(activityWrapper != null)
activityWrapper.removeActivityResultListener(this);
activityWrapper.removeActivityStateChangeListner(this);
}
@Override
public Map<String, FREFunction> getFunctions() { // 需要返回一个包含FREFunction的Map集合
Map<String, FREFunction> map = new HashMap<String, FREFunction>();
map.put("getNativeData", new GetNativeDataFunction());
return map;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) { // onActivityResult方法
}
@Override
public void onActivityStateChanged(ActivityState state) { // 生命周期变化
}
@Override
public void onConfigurationChanged(Configuration config) {
}
}
其中生命周期的对应关系如下:
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) { // onActivityResult方法
Log.i("ANE", "onActivityResult");
}
@Override
public void onActivityStateChanged(ActivityState state) { // 生命周期变化
switch (state) {
case RESUMED: // 生命周期onResume
Log.i("ANE", "onResume");
break;
case STARTED: // 生命周期onStart
Log.i("ANE", "onStart");
break;
case RESTARTED: // 生命周期onRestart
Log.i("ANE", "onRestart");
break;
case PAUSED: // 生命周期onPause
Log.i("ANE", "onPause");
break;
case STOPPED: // 生命周期onStop
Log.i("ANE", "onStop");
break;
case DESTROYED: // 生命周期onDestory
Log.i("ANE", "onDestory");
break;
}
}
以上就是安卓本地代码的处理了,总结一下
① 核心类FREExtension,FREContext,FREFunction
② 通过FREContext连通,call方法调用
③ 异步回调可以通过FREContext的dispatchStatusEventAsync处理
④ 生命周期的实现需要点技巧
2.1.2.2 实现IOS本地代码
IOS本地代码的开发借鉴了Github的一个工程,做了一些优化和增强,你完全可以按照该模板进行你自己的改编,GitHub项目如下:
xcode-template-ane
好,开始我们自己的IOS本地代码的开发
首先创建一个Xcode工程,选择Static Library静态库
和安卓一样起名MyNativeLib
开发IOS本地代码毫无疑问我们仍然需要遵循Adobe制定的规范,当然也需要Adobe提供的库,在Object-C语法中,我们需要一个头文件
在你的/Air_SDK根目录/include/FlashRuntimeExtension.h
引入FlashRuntimeExtension.h文件
对比安卓来分析IOS的FlashRuntimeExtension.h文件
本地扩展的初始化
typedef void (*FREInitializer)(
void** extDataToSet ,
FREContextInitializer* ctxInitializerToSet,
FREContextFinalizer* ctxFinalizerToSet
);
本地扩展的结束处理
typedef void (*FREFinalizer)(
void* extData
);
本地扩展的连接器(上下文)初始化
typedef void (*FREContextInitializer)(
void* extData ,
const uint8_t* ctxType ,
FREContext ctx ,
uint32_t* numFunctionsToSet,
const FRENamedFunction** functionsToSet
);
本地扩展的连接器(上下文)结束的操作
typedef void (*FREContextFinalizer)(
FREContext ctx
);
根据这四个定义,在头文件中定义了对应的方法
void ExtensionInitializer(void** extDataToSet, FREContextInitializer* ctxInitializerToSet, FREContextFinalizer* ctxFinalizerToSet);
void ExtensionFinalizer(void* extData);
void ContextInitializer(void* extData, const uint8_t* ctxType, FREContext ctx, uint32_t* numFunctionsToTest, const FRENamedFunction** functionsToSet);
void ContextFinalizer(FREContext ctx);
具体实现:
1 Extension的初始化,需要传入Context的相关方法
2 在Context中需要numFunctionsToTest和functionsToSet
functionsToSet:方法的数组(可对比安卓理解)
numFunctionsToTest :方法数组的数量ActionScript通过该数组访问本地方法,而本地方法都是FRENamedFunction对象。
感谢github大牛的帮助:借用了他的模板的两个宏定义简化代码,方便阅读
#define ANE_FUNCTION(f) FREObject (f)(FREContext ctx, void *data, uint32_t argc, FREObject argv[]) #define MAP_FUNCTION(f, data) { (const uint8_t*)(#f), (data), &(f) }
为了方便实现我加入了几个FREObject与Object-C类型的转化方法
头定义
// FREObject <--> NSString NSString * getStringFromFREObject(FREObject obj); FREObject createFREString(NSString * string);
// FREObject <--> double double getDoubleFromFREObject(FREObject obj); FREObject createFREDouble(double value);
// FREObject <--> int int getIntFromFREObject(FREObject obj); FREObject createFREInt(int value);
// FREObject <--> BOOL BOOL getBoolFromFREObject(FREObject obj); FREObject createFREBool(BOOL value);
OK,整体代码如下:
MyNativeLib.h
#import <Foundation/Foundation.h>
#import "FlashRuntimeExtensions.h"
void ExtensionInitializer(void** extDataToSet, FREContextInitializer* ctxInitializerToSet, FREContextFinalizer* ctxFinalizerToSet);
void ExtensionFinalizer(void* extData);
void ContextInitializer(void* extData, const uint8_t* ctxType, FREContext ctx, uint32_t* numFunctionsToTest, const FRENamedFunction** functionsToSet);
void ContextFinalizer(FREContext ctx);
#define ANE_FUNCTION(f) FREObject (f)(FREContext ctx, void *data, uint32_t argc, FREObject argv[])
#define MAP_FUNCTION(f, data) { (const uint8_t*)(#f), (data), &(f) }
ANE_FUNCTION(getNativeData);
/**************************************************/
NSString * getStringFromFREObject(FREObject obj);
FREObject createFREString(NSString * string);
double getDoubleFromFREObject(FREObject obj);
FREObject createFREDouble(double value);
int getIntFromFREObject(FREObject obj);
FREObject createFREInt(int value);
BOOL getBoolFromFREObject(FREObject obj);
FREObject createFREBool(BOOL value);
MyNativeLib.m
#import "MyNativeLib.h"
void ExtensionInitializer(void** extDataToSet, FREContextInitializer* ctxInitializerToSet, FREContextFinalizer* ctxFinalizerToSet) {
NSLog(@"Entering ExtensionInitializer()");
*extDataToSet = NULL;
*ctxInitializerToSet = &ContextInitializer; //传入Context初始化方法
*ctxFinalizerToSet = &ContextFinalizer; //传入Context结束方法
NSLog(@"Exiting ExtensionInitializer()");}
void ExtensionFinalizer(void* extData) {
NSLog(@"Entering ExtensionFinalizer()");
// 可以做清理工作.
NSLog(@"Exiting ExtensionFinalizer()");
}
void ContextInitializer(void* extData, const uint8_t* ctxType, FREContext ctx, uint32_t* numFunctionsToTest, const FRENamedFunction** functionsToSet) {
static FRENamedFunction func[] =
{
MAP_FUNCTION(getNativeData, NULL)
};
*numFunctionsToTest = sizeof(func) / sizeof(FRENamedFunction);
*functionsToSet = func;
}
void ContextFinalizer(FREContext ctx) {
NSLog(@"Entering ContextFinalizer()");
// 可以做清理工作
NSLog(@"Exiting ContextFinalizer()");
}
ANE_FUNCTION(getNativeData)
{
int params1 = getIntFromFREObject(argv[0]);
NSString *params2 = getStringFromFREObject(argv[1]);
NSString * nativeData = [[NSString new] initWithFormat:@"%d%@",params1,params2];
return createFREString(nativeData);
}
/* * utils function */
/*--------------------------------string------------------------------------*/
NSString * getStringFromFREObject(FREObject obj)
{
uint32_t length;
const uint8_t *value;
FREGetObjectAsUTF8(obj, &length, &value);
return [NSString stringWithUTF8String:(const char *)value];
}
FREObject createFREString(NSString * string)
{
const char *str = [string UTF8String];
FREObject obj;
FRENewObjectFromUTF8(strlen(str)+1, (const uint8_t*)str, &obj);
return obj;
}
/*-------------------------------double-----------------------------------*/
double getDoubleFromFREObject(FREObject obj)
{
double number;
FREGetObjectAsDouble(obj, &number);
return number;
}
FREObject createFREDouble(double value)
{
FREObject obj = nil;
FRENewObjectFromDouble(value, &obj);
return obj;
}
/*---------------------------------int---------------------------------*/
int getIntFromFREObject(FREObject obj)
{
int32_t number;
FREGetObjectAsInt32(obj, &number);
return number;
}
FREObject createFREInt(int value)
{
FREObject obj = nil;
FRENewObjectFromInt32(value, &obj);
return obj;
}
/*------------------------------bool----------------------------------------*/
BOOL getBoolFromFREObject(FREObject obj)
{
uint32_t boolean;
FREGetObjectAsBool(obj, &boolean);
return boolean;
}
FREObject createFREBool(BOOL value)
{
FREObject obj = nil;
FRENewObjectFromBool(value, &obj);
return obj;
}
其中ANE_FUNCTION(getNativeData)
就是最后的处理
以上就是IOS本地代码的简单调用,同安卓一样,我门也需要异步的获取本地代码处理的结果,那IOS的扩展定义中有没有类似context.dispatchStatusEventAsync
的方法?
在查找FlashRuntimeExtension.h中找到了FREDispatchStatusEventAsync
定义
定义如下:
FREResult FREDispatchStatusEventAsync(
FREContext ctx ,
const uint8_t* code ,
const uint8_t* level
);
可以看出,几乎与安卓一样,我们需要一个FREContext对象,以及要发送给ActionScript的code和level值,所以我们封装一下方法方便使用
头定义:
void dispatchStatusEventAsync(NSString * code, NSString * level);
实现
void dispatchStatusEventAsync(NSString * code, NSString * level)
{
if(mContext!= nil)
{
FREDispatchStatusEventAsync(mContext, (const uint8_t *) [code UTF8String], (const uint8_t *) [level UTF8String]);
}
else
{
NSLog(@"FREContext is null");
}
}
只需要在适当的地方调用dispatchStatusEventAsync
方法就可以发送信息给ActionScript了。
调用代码片段
ANE_FUNCTION(getNativeData)
{
int params1 = getIntFromFREObject(argv[0]);
NSString *params2 = getStringFromFREObject(argv[1]);
NSString * nativeData = [[NSString new]
initWithFormat:@"%d%@",params1,params2];
dispatchStatusEventAsync(@"nativeCallbackSuccess",@"iosCallback"); // 回调
return createFREString(nativeData);
}
以上就是IOS本地代码基本实现了。还有最后一个问题,我们需要解决。IOS有一个Delegate类涉及到了IOS的生命周期,那IOS生命周期是否也可以按照android的方式处理?然而不行,在air sdk中我们只找到了头文件,并没有其他的内容。最后通过一篇别人的博客,利用IOS的Hook机制达到了对生命周期的控制的目的。
Objective-C的hook方案(一): Method Swizzling
感谢作者,她的一些其他ios的文章也值得一读
根据博客中描述的原理,最后实现了一个模版性的Hook类,你可以直接按照自己的需求修改使用
大概说明一下这个原理,Object-C的方法是基于消息发送的机制,而我们的Hook类似于在一个链表中插入了一个节点,但还是保证了他们的连通
//
// HookUtils.m
// ResearchMethodSwizzl
//
// Created by 薛旻 on 15/4/27.
// Copyright (c) 2015年 薛旻. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
@interface HookUtils : NSObject
+ (void)hookMehod:(SEL)oldSEL andDef:(SEL)defaultSEL andNew:(SEL)newSEL;
@end
@implementation HookUtils
+ (void)hookMehod:(SEL)oldSEL andDef:(SEL)defaultSEL andNew:(SEL)newSEL {
NSLog(@"hookMehod");
Class oldClass = objc_getClass([@"CTAppDelegate" UTF8String]);
Class newClass = [HookUtils class];
//把方法加给原Class
class_addMethod(oldClass, newSEL, class_getMethodImplementation(newClass, newSEL), nil);
class_addMethod(oldClass, oldSEL, class_getMethodImplementation(newClass, defaultSEL),nil);
Method oldMethod = class_getInstanceMethod(oldClass, oldSEL);
assert(oldMethod);
Method newMethod = class_getInstanceMethod(oldClass, newSEL);
assert(newMethod);
method_exchangeImplementations(oldMethod, newMethod);
}
+ (void)load {
NSLog(@"load");
[self hookMehod:@selector(application:didFinishLaunchingWithOptions:) andDef:@selector(defaultApplication:didFinishLaunchingWithOptions:) andNew:@selector(hookedApplication:didFinishLaunchingWithOptions:)];
[self hookMehod:@selector(applicationWillEnterForeground:) andDef:@selector(defaultApplicationWillEnterForeground:) andNew:@selector(hookedApplicationWillEnterForeground:)];
}
/*具体要走的代码*/
-(BOOL)hookedApplication:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)dic
{
NSLog(@"applicationDidFinishLaunching");
[self hookedApplication:application didFinishLaunchingWithOptions:dic];
return YES;
}
- (void)hookedApplicationWillResignActive:(UIApplication *)application {
[self hookedApplicationWillResignActive:application];
}
- (void)hookedApplicationDidEnterBackground:(UIApplication *)application {
[self hookedApplicationDidEnterBackground:application];
}
- (void)hookedApplicationWillEnterForeground:(UIApplication *)application {
[self hookedApplicationWillEnterForeground:application];
}
- (void)hookedApplicationDidBecomeActive:(UIApplication *)application {
[self hookedApplicationDidBecomeActive:application];
}
- (void)hookedApplicationWillTerminate:(UIApplication *)application {
[self hookedApplicationWillTerminate:application];
}
/*支付宝对应的方法*/
- (BOOL)hookedApplication:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
[self hookedApplication:application openURL:url sourceApplication:sourceApplication annotation:annotation];
return YES;
}
-(BOOL)hookedApplication:(UIApplication*)application handleOpenURL:(NSURL*)url {
[self hookedApplication:application handleOpenURL:url];
return YES;
}
#pragma 默认
/*default 默认不需要改动*/
- (BOOL)defaultApplication:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)dic { return YES;
}
- (void)defaultApplicationWillResignActive:(UIApplication *)application {}
- (void)defaultApplicationDidEnterBackground:(UIApplication *)application {}
- (void)defaultApplicationWillEnterForeground:(UIApplication *)application {}
- (void)defaultApplicationDidBecomeActive:(UIApplication *)application {}
- (void)defaultApplicationWillTerminate:(UIApplication *)application {}
- (BOOL)defaultApplication:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
return YES;
}
- (BOOL)defaultApplication:(UIApplication *)application handleOpenURL:(NSURL *)url {
return YES;
}
@end
关于这个HookUtils做几点说明:
① HookUtils是IOS的Hook机制的实现,所以适用于所有IOS的开发,这里只是针对Air应用做了一些处理。
① Hook机制中需要获取应用的实现了生命周期的类名,这样才能Hook处理,如果你是Xcode工程开发的代码那你会很方便的找到这个类名,这适用于IOS原生,Unity以及Cocos引擎,因为它们都会使用Xcode开发工具。但是,Air开发是在Flash Builder上,并不直接涉及到Xcode工程,通过代码打印Air应用的生命周期类,找到了Air的类名CTAppDelegate
,于是代码中设置为,其他引擎自行修改,当然,你也可以通过配置文件(Info.plist)获取,这样代码就不需要修改了
Class oldClass = objc_getClass([@"CTAppDelegate" UTF8String]);
③ 你需要在hooked**方法中实现你的具体代码,最后在
+ (void)load()
方法中调用头文件定义的方法(void)hookMehod:(SEL)oldSEL andDef:(SEL)defaultSEL andNew:(SEL)newSEL;
来达到Hook的目的
OK,以上就是IOS处理的全部过程,由于本人精通的是安卓,所以在IOS的处理上带入了Android的思路,IOS的处理上描述的也不详尽,如果在IOS本地代码上理解有问题,建议你先看看上面Android本地代码的处理,因为规范和逻辑是通的。如果IOS部分有问题还望指正。
总结一下IOS本地代码的实现
① FREInitializer,FREFinalizer,FREContextInitializer,FREContextFinalizer的内部实现
对比Android的FREExtension和FREContext的实现
② FRENamedFunction** functionsToSet:方法数组
uint32_t* numFunctionsToTest:方法大小
FREContext ctx:上下文引用
通过两个宏定义方便实现方法和数组
对比Android的getFunctions()和FREFunction的实现
③ 通过FREContext这个参数,调用方法FREDispatchStatusEventAsync实现异步消息传递给ActionScript
对比Android的dispatchStatusEventAsync方法
④ HookUtils利用Hook机制处理IOS生命周期
对比Android的生命周期的处理
AndroidActivityWrapper.GetAndroidActivityWrapper()
ActivityResultCallback,
StateChangeCallback
当然Android和IOS的生命周期的处理也是最需要技巧的一部分
至此,我们的本地代码部分也全部完成
2.2 制作ANE
进过上面操作代码逻辑层面已经处理完了,废话不说,通过上面的东西,制作可用的ANE
2.2.1 准备资源
以下是一个ANE打包的资源清单,只需要Android或者IOS的同学可*组合,后面分别介绍它们
- Android本地代码库——jar包(Android需要)
- IOS本地代码库——IOS静态库文件(IOS需要)
- ActionScript库——SWC文件(必须)
- library.swf(必须)
- extension.xml(必须,Android与IOS配置不同)
- platformoptions.xml (IOS可能需要)
- 第三方库文件 (IOS可能需要)
① Android本地代码库——jar包
先将android本地代码导出jar包,右键Eclipse工程,Export
只勾选src文件,也就是说我们最后只需要class文件,导出为MyNativeLib.jar
拿到我们自己的库文件后,需要思考一个问题,我们在写本地代码时,为了实现一些功能(比如接入其他一些SDK)时,会引入第三方的jar包,那这个些jar包怎么处理?
很简单,把它们统统打包成一个jar包即可,即解压它们,把他们放在同一个文件夹下,使用jdk的打包命令再合并成一个包,默认你已经配置好了jdk
打包命令如下:
jar cvfm MyNativeLib.jar meta-inf/manifest.mf .
在合并的根路径下运行该命令行可打包出合并的MyNativeLib.jar,这个jar就是最终我们需要的jar
② IOS本地代码库——IOS静态库文件
运行我们的Xcode工程生成libMyNativeLib.a库文件,由于IOS现在要求必须支持全部架构,也就是说你的库必须同时支持32位(armv7、armv7s)和64位(arm64)。在Xcode中进行配置
我在MyNativeLib工程中建立了一个自动构建库文件脚本,方便你后期研究,属于衍生,此文暂不介绍。
③ ActionScript库——SWC文件(必须)
④ library.swf(必须)
加压上一步的swc文件就可以找到我们需要的library.swf文件
⑤ extension.xml(必须,Android与IOS配置不同)
<extension xmlns="http://ns.adobe.com/air/extension/3.5">
<id>com.xuemin.myane</id>
<versionNumber>1</versionNumber>
<platforms>
<platform name="default">
<applicationDeployment>
</applicationDeployment>
</platform>
<platform name="Android-ARM">
<applicationDeployment>
<nativeLibrary>MyNativeLib.jar</nativeLibrary>
<initializer>com.xuemin.mynativelib.NativeExtension</initializer>
</applicationDeployment>
</platform>
<platform name="iPhone-ARM">
<applicationDeployment>
<nativeLibrary>libMyNativeLib.a</nativeLibrary>
<initializer>ExtensionInitializer</initializer>
<finalizer>ExtensionFinalizer</finalizer>
</applicationDeployment>
</platform>
</platforms>
</extension>
⑥ platformoptions.xml (IOS可能需要)
刚刚处理android的jar包时,我们把需要的第三方jar包合并在一起即可使用,但是在ios的库文件处理时我们会发现,除了第三方的静态库文件(.a文件)可以和我们自己的ios库文件合并外,其他第三方的Framework库以及依赖的系统的Framework库是都无法通过合并处理。但是我们又有这样的需求,所以Adobe提供了一种方法,通过platformoptions.xml文件来配置ios依赖的系统库和第三方库
<platform xmlns="http://ns.adobe.com/air/extension/3.5">
<description>MyNativeLib</description>
<copyright>xuemin</copyright>
<linkerOptions>
<option>-framework Foundation</option>
<option>-framework UIKit</option>
<option>-framework SystemConfiguration</option>
<option>-lsqlite</option>
</linkerOptions>
<packagedDependencies>
<packagedDependency>AlipaySDK.framework</packagedDependency>
</packagedDependencies>
</platform>
以上只是为了说明配置,在例子中并没有用到该文件
注意:
第三方依赖库与系统库不同air runtime找不到这些库,所以在生成ANE时要指定这些库的位置,这也就是我们最后一步的处理
⑦ 第三方库文件 (IOS可能需要)
见⑥的注意事项
好,制作ANE的材料已经准备完成,下一步加工成ANE
2.2.2 打包ANE
制作ANE呢,就是通过air sdk提供的打包命令,根据提供的材料,生成出ANE文件来使用,ANE类似于一个压缩包,你可以解压研究其中的内容,相信对你ANE原理还是很有帮助的(题外话)
既然打包命令,那必然就需要了解一下提供的工具和参数了,我们使用的工具就是sdk中的adt
这是官方的使用说明:ADT package 命令
其中还有一些使用例子,在这里我只介绍几本的打包使用,如果想深入学习,可通过上面链接学习。
当然,网上还是有人提供了类似的工具
ANETool
下面按照我们的命令行学习,很简单只需要一句
① 只支持Android打包
adt -package -target ane myane.ane extension.xml -swc MySWCLib.swc -platform Android-ARM MyNativeLib.jar library.swf -platform default library.swf
② Android和IOS同时支持打包
adt -package -target ane myane.ane extension.xml -swc MySWCLib.swc -platform Android-ARM MyNativeLib.jar library.swf -platform default library.swf-platform iPhone-ARM -platformoptions platformoptions.xml libMyNativeLib.a 第三方.framework 第三方.a library.swf
③ 只支持IOS打包
可通过①和②推理出来
最后生成出myane.ane,我们可以在air应用中使用该ane文件了。
工程下载
最后提供本文写的代码工程和配置的GitHUb路径
build_ane_project
你也可以直接在GitHub上搜索build_ane_project
或通过以下链接git clone
https://github.com/justxuemin/build_ane_project.git