混編ObjectiveC++

折騰範兒_味精發表於2017-12-13

遷移一批老文章到掘金

最近有點炒冷飯的嫌疑,不過確實以前沒有Git Or Blog的習慣,所以很多工作上的技術分享就存留在了電腦的文件裡,現在還是想重新整理一下,再分享出來。

混編C++也是以前工作中需要用到的,於是被我炒冷飯翻了出來,不過確實有重新整理了一下

為什麼要使用C++混編

1)需要使用工具庫或者原始碼是C++的

各個平臺,各種語言,都會有很多開源的工具和庫檔案方便大家學習和使用,但如C與C++這版經典的語言,很多底層的,演算法型的庫都是用C++實現的,尤其是很多人臉識別,圖形濾鏡演算法,視訊處理演算法,甚至底層圖形渲染OpenGL

2)C++執行效率快

大家都知道C++的執行效率快,所以在高複雜度高演算法層面的開發內容裡,大多都選擇使用C++來完成,我是做客戶端的,雖然不像做機器學習,大資料處理等在工作中需要廣泛運用高效演算法,但上面提到的人臉識別,圖形濾鏡演算法,甚至視訊處理,還有很多遊戲內部需要的遊戲AI,都是有可能運用在我們熟知的客戶端開發之中

3)跨平臺

C++是編譯型跨平臺的,C++的程式碼編譯出來的二進位制檔案可以在android,iOS,甚至WP上都可以正常執行,可謂是真·跨平臺

說到跨平臺,肯定不少人提起H5跨平臺呀,ReactNative跨平臺呀,這類通常屬於解釋型跨平臺,約定好一種指令碼語言,底層輔助以各平臺的基礎實現,甚至底層就是藉助C++實現,通過底層解讀指令碼語言,在執行時進行解釋實現邏輯,就好比webkit作為瀏覽器的核心,JavaScriptCore作為RN的核心,雖然開發中使用了js進行寫程式碼,但是究其本質還是在執行時解釋js在進行native執行的。js程式碼並不參與編譯,這類跨平臺在編譯時參與編譯的,正是那套語法直譯器+NA底層程式碼,他們或多或少還是通過C++實現的

我們使用C++做邏輯的原因

我們做客戶端,核心模組使用C++的原因其實就是出自(2)(3)兩點,因為我們的業務涉及極其複雜的文字排版,而無論是iOS平臺還是安卓平臺,基礎排版是很難滿足中文甚至我大天朝獨有政治要求的,想要實現勢必要在每個平臺上分別封裝一套極度複雜的排版策略控制,因此我們放棄了使用CoreText的基礎排版API(安卓上用啥排版不知道),而選擇用C++實現一套通用於兩個平臺的排版策略,當然在排版速度效率上也是要很高要求的

ObjectiveC 與 C++ 的共同點

在iOS開發之中,OC程式碼與C++程式碼可以完美的融合在一塊,何謂完美?你甚至可以上一行剛敲完[NSArray objectAtIndex:xx](OC程式碼)下一行就使用STL構建一個C++的List陣列(C++程式碼),他們之間可以完美編譯,生成正常的程式,並且還可以單步debug,隨時跟進一層一層的方法,剛剛單步跳出一個OC的messageSend,馬上就可以單步跟進一個C++ Class的function,這之間沒有一點障礙,所有變數,指標,結構體,資料,都可以任意檢視,二者之間暢通無阻

向下完全相容C是他們的共同點和紐帶

為什麼會這樣?因為C++與OC都完全向下相容C 所有的OC物件,雖然有ARC輔助記憶體管理,但他本質上還是一個void *,同理C++也一樣是void *,OC之所以呼叫函式叫做傳送訊息,是因為封裝了層獨有的runtime機制(這機制還是C的),但歸根結底每個函式實體依然是一個IMP,依然是一個函式指標,這一點和C++也一樣,所以他們之間的銜接才會如此通暢

其他混編情況可就沒那麼容易了

  • android混編C++,恩很麻煩,只能先編譯成so,兩個環境如果要互動,要先手寫一套JNI,把C++環境下的資料和java環境下的資料進行手動轉換,並且斷點除錯沒法斷點進入so內,想要debug除錯,必須靠fwrite寫log到本地磁碟除錯╮(╯_╰)╭

  • 我以前搞過遊戲,做過C++內混編lua指令碼,這倆互通更蛋疼,雖然lua的直譯器底層是用C寫的,但是所有的記憶體都是lua直譯器(或者叫虛擬機器)內的資料,因此如果二者要互通,也要寫一個通道來交換資料,這個交換資料,就是通過超級煩瑣的資料對齊,壓棧,出棧來互通。

  • 前一陣子也學習了一些JSPatch,他其實可以看做是js程式碼混編Oc的模範工程,同lua一樣,整個js的執行環境也是依賴於JavaScriptCore提供的一套JS虛擬機器來執行,他有著自己的上下文JSContext,雖說簡單的通用資料,字串,陣列,字典,被JavaScriptCore自動的執行完了轉換,但一旦需要兩個環境交換獨有資料型別,例如js裡面的function,例如oc裡面的自定義NSObject,那麼就需要JSValue這個物件起到轉換和傳遞的作用

ObjectiveC如何混編C++

  • 想要建立一個純C++類,你只需要建立.h開頭和.cpp開頭的檔案,直接匯入工程就好,如果需要使用一些C++的標準庫,可以直接從Xcode匯入libstdC++

  • 如果你想建立一個能即識別C++又識別OC的物件,只需要照常建立一個.h 檔案和.m檔案,然後將.m檔案重新命名成.mm檔案,就是告訴編譯器,這個檔案可以進行混編 — ObjectiveC++(名字是不是有點酷)

  • 如果你想建立一個純OC類,那這還需要用我說麼?

現在你的工程裡,可以有這三種檔案存在,基本上就可以滿足我們的混編需求了。

怎麼樣是不是很想趕快試試了?

例子:在一個OC環境裡呼叫C++

我的例子會一步一步來,甚至有的步驟中可能是錯誤的程式碼,給大家展示完錯誤的程式碼後,進行說明,再放上正確的程式碼,

程式碼也不全是完整程式碼

CppObject.h C++的標頭檔案 .cpp檔案留空,先不寫邏輯

#include <string>
class CppObject
{
public:
    void ExampleMethod(const std::string& str){};
    // constructor, destructor, other members, etc.
};

複製程式碼

OCObject.h OC的標頭檔案 .m檔案先改為.mm,但先不寫邏輯

#import <Foundation/Foundation.h>
//#import "CppObject.h"

@interface OcObject : NSObject {
    CppObject* wrapped;
}

@property CppObject* wrapped2;

- (void)exampleMethodWithString:(NSString*)str;
// other wrapped methods and properties
@end

複製程式碼

標頭檔案準備完畢,實現檔案,我先不寫邏輯,先跑一下看看會有什麼問題?

跑完了以後會編譯報錯,報錯的原因很簡單,你在OCObject.h中引用了C++的標頭檔案,xcode不認識,無法編譯通過。

咦?剛剛不是說好了C++和OC無縫互通了麼,這咋又不認識了?原因很簡單,我們通過修改.m為.mm檔案,能讓編譯器xcode知道這是一個混編檔案,但是我可沒說修改.h為.hh檔案喲,是這樣的,對於xcode來說,可以認識.mm的混編語法,但是不認識.h檔案中的混編語法,如果.h全都是C++的寫法,沒有問題,如果.h全都是OC的寫法,沒有問題,如果.h裡面有C++又有OC?那妥妥的有問題(.h中引入的其他標頭檔案也算在內)

怎麼處理呢?兩個辦法

  • 不在.h裡寫混編了,那我移到.mm裡唄~~~

  • 不讓我寫c++?ok,我寫C,反正寫C是沒錯的,所以老子寫void *id

這裡的例子我先寫到.mm檔案裡

#import "OcObject.h"
#import "CppObject.h"
@interface OcObject () {
    CppObject* wrapped;
}
@end

@implementation OcObject

- (void)exampleMethodWithString:(NSString*)str
{
    // NOTE: if str is nil this will produce an empty C++ string
    // instead of dereferencing the NULL pointer from UTF8String.
    std::string cpp_str([str UTF8String], [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
    wrapped->ExampleMethod(cpp_str);
}
複製程式碼

這不~妥了沒問題了~,我們再去補上CPP檔案中的函式實現,隨便寫個printf(),輸出個string,例子就完成了

例子:在一個C++環境裡呼叫OC

首先我們要打造一個C++的環境

--AntiCppObject.h

#include <iostream>

class AntiCppObject
{

public:
    AntiCppObject();
    void ExampleMethod(const std::string& str){};
    // constructor, destructor, other members, etc.
};


--AntiCppObject.cpp

#include "AntiCppObject.h"

AntiCppObject::AntiCppObject()
{
    
}
複製程式碼

然後我們再準備一個OC類接入C++,m檔案我就不補充完了,隨便寫個NSLog就好

--AntiOcObject.h

#import <Foundation/Foundation.h>

@interface AntiOcObject : NSObject

- (void)function;

@end
複製程式碼

現在打算接入C++環境了,首先先把.CPP改成.mm檔案,妥妥噠

然後修改標頭檔案

--AntiCppObject.h
#import "AntiOcObject.h"
class AntiCppObject
{
    AntiOcObject* AntiOc;
public:
    AntiCppObject();
    void ExampleMethod(const std::string& str){};
    // constructor, destructor, other members, etc.
};
複製程式碼

經過了剛才的例子,看到這應該立馬反應過來,這不對,標頭檔案不能混編,會報錯的。那應該怎麼做呢?

做法還是上面提到的,要麼void *,要麼想辦法把定義寫在.mm檔案裡,老規矩,void *先不提,我們先在.h中寫個結構體,藏起來那個oc的物件,在mm檔案中進行宣告

--AntiCppObject.h

#include <iostream>
struct sthStruct;
class AntiCppObject
{
    sthStruct* sth;
public:
    AntiCppObject();
    void function();
    // constructor, destructor, other members, etc.
};

---AntiCppObject.cpp

#include "AntiCppObject.h"
#import "AntiOcObject.h"

struct sthStruct
{
    AntiOcObject* oc;
};

AntiCppObject::AntiCppObject()
{
    AntiOcObject* t =[[AntiOcObject alloc]init];
    sth = new sthStruct;
    sth->oc = t;
}

void AntiCppObject::function()
{
    [this->sth->oc function];
}
複製程式碼

你看這樣就實現了在C++中呼叫OC

ObjectiveC++混編注意事項

  • 只需要將.m檔案重新命名成.mm檔案,就是告訴編譯器,這個檔案可以進行混編 — ObjectiveC++
  • 在一個專案裡使用兩種語言,就要儘可能的把這兩種語言分開,儘管你可以一口氣將所有的檔案重新命名,但是兩種語言差異性還是很大,混亂使用,處理起來會很困難
  • header檔案沒有字尾名變化,沒有.hh檔案^_^。所以我們要保持標頭檔案的整潔,將混編程式碼從標頭檔案移出到mm檔案中,保證標頭檔案要麼是純正C++,要麼是純正OC,(當然,有C是絕對沒問題的)
  • Objective-C向下完全相容C,C++也是,有時候也可以靈活的使用void *指標,當做橋樑,來回在兩個環境間傳遞(上面的例子沒有體現)

小心你的記憶體

  • 按著之前的原則,C++和OC兩部分儘量區分開,各自在各自的獨立區域內維護好自己的記憶體,Objective-C可以是arc也可mrc,C++開發者自行管理記憶體
  • 在.mm檔案中,OC環境中在init 和dealloc中對C++類進行 new 和 delete操作
  • 在.mm檔案中,在C++環境中構造和解構函式中進行init 和 release操作
--OcObject.mm
-(id)init
{
    self = [super init];
    if (self) {
        wrapped = new CppObject();
    }
    return self;
}

-(void)dealloc
{
    delete wrapped;
}

--AntiCppObject.mm
AntiCppObject::AntiCppObject()
{
    AntiOcObject* t =[[AntiOcObject alloc]init];
    sth = new sthStruct;
    sth->oc = t;
}

AntiCppObject::~AntiCppObject()
{
    if (sth) {
        [sth->oc release];//arc的話,忽略掉這句話不寫
    }
    delete sth;
}

複製程式碼

這個例子告訴我們什麼?

如果我們通過oc的方式建立出來的,他的記憶體自然歸OC管理,如果是mrc,請使用release,如果是arc,只要置空,自然會自動釋放

如果我們通過C++的方式,建構函式new出來的,那我們就要手動的使用解構函式就釋放他

其實很多事情原理是一樣的

  • 我們在iOS開發使用CF函式的時候,但凡使用CFCreateXX的一定要手動自己呼叫CFRlease
  • 我們在編寫C++的時候,使用malloc的一定要自己free,使用new的一定要自己delete

id的妙用

剛才的例子中,我雖然頻繁提到void *但是並沒有詳細加以說明,神奇的東西應該放在最後

首先說一下id這個很特殊的東西

前面第二個例子,我們是藉助一個結構體struct把oc程式碼隱藏到.mm檔案裡,那麼我們可以不借助struct麼?當然可以

--AntiCppObject.h
#include <iostream>
struct sthStruct;
class AntiCppObject
{
    id sthoc;
    sthStruct* sth;
public:
    AntiCppObject();
    ~AntiCppObject();
    void function();
    // constructor, destructor, other members, etc.
};

--AntiCppObject.cpp
#include "AntiCppObject.h"
#import "AntiOcObject.h"

struct sthStruct
{
    AntiOcObject* oc;
};

AntiCppObject::AntiCppObject()
{
    AntiOcObject* t =[[AntiOcObject alloc]init];
    sth = new sthStruct;
    sth->oc = t;
    
    sthoc = [[AntiOcObject alloc]init];
}

AntiCppObject::~AntiCppObject()
{
    if (sth) {
        [sth->oc release];
        
        [sthoc release];
    }
    delete sth;
}

void AntiCppObject::function()
{
    [this->sth->oc function];
    [this->sthoc function];
}
複製程式碼

可以看到這個例子中,那個struct還在,舊的方案仍然保留,但是我們在標頭檔案裡寫了一個id型別,xcode編譯器在全都是C++程式碼的.h檔案裡雖然不認識oc物件,但是其實是認識id的,我們藉助這個id,就可以不借助struct隱藏oc物件宣告瞭

神奇的void *

終於說到這個void *了,首先我們寫個oc物件,可以持有void *,寫個C++也可以持有,甚至我們不寫任何物件,在寫一個static的C程式碼,也可以在一個全域性控制元件儲存一個void *物件,正是這個void *物件,可以靈活的組合出各種混編用法

void *是什麼?就是指標的最原本形態,利用它我們可以各種花式的進行混編OC與C++

唯一需要注意的就是id(即oc物件)與void *的轉換,要知道arc是有記憶體管理的,而C++是沒有的,如果都一股腦的隨便二者轉來轉去,那記憶體管理到底該如何自動進行釋放?(mrc下二者轉換是不需要特別處理的)

因此Arc下二者進行轉換經常伴隨著一些強轉關鍵字

  • __bridge
  • __bridge_retained
  • __bridge_transfer

其實是從記憶體安全性上做的轉換修飾符,相關搜尋id與void *轉換可以自行查閱,而且在iOS的core fundation開發中非常常見,簡單的說就是bridgeretained會把記憶體所有權同時歸原物件和變換後的物件持有(只對變換後的物件做一次reatain),bridgetransfer會把記憶體所有權徹底移交變換後的物件持有(retain變換後的物件,release變換前的物件)

這裡面我會貼一段程式碼,這段程式碼只為展示一些使用,因此,設計上可能有點繞,和扯淡,只為展示混編

--TrickInterface.h
typedef void (*interface)(void* caller, void *parameter);



--TrickOC.h
#import <Foundation/Foundation.h>
#import "TrickInterface.h"

@interface TrickOC : NSObject
{
    int abc;
}

-(int)dosthing:(void*)param;
@property interface call;
@end

--TrickOC.m
#import "TrickOC.h"
#import "TrickInterface.h"

void MyObjectDoSomethingWith(void * obj, void *aParameter)
{
    [(__bridge id) obj dosthing:aParameter];
}

@implementation TrickOC

-(id)init
{
    self = [super init];
    if (self) {
        self.call = MyObjectDoSomethingWith;
    }
    return self;
}

-(int)dosthing:(void *)param
{
    NSLog(@"111111");
    return 0;
}

@end

--TrickCpp.cpp
#include "TrickCpp.h"
#include "TrickInterface.h"

TrickCpp::TrickCpp(void* oc,interface call)
{
    myoc = oc;
    mycall = call;
}

void TrickCpp::function()
{
    mycall(myoc,NULL);
}

--TrickCpp.h
#include <iostream>
#include "TrickInterface.h"
class TrickCpp
{
    void* myoc;
    interface mycall;
public:
    TrickCpp();
    TrickCpp(void* oc,interface call);
    ~TrickCpp();
    void function();
    // constructor, destructor, other members, etc.
};


--使用樣例

TrickOC* trickoc = [[TrickOC alloc]init];
    void* pointer = (__bridge void*)trickoc;
    TrickCpp * trick = new TrickCpp(pointer,trickoc.call);
    trick->function();

複製程式碼

這段程式碼中首先在全域性區域宣告瞭一個全域性的cfunctioninterface,起名叫介面顧名思義是打算把它當做C++傳遞OC的通道,所有跨C++回撥OC都通過這個通道來通訊

在TrickOc.m檔案中也實現了這一個全域性的cfunctionMyObjectDoSomethingWith,這個cfunction實體就是我們的介面通道

當建立TrickCpp的時候,將以建立好的TrickOc和這個cfunction一併傳入,當Cpp需要呼叫Oc的時候,直接使用cfunction與TrickOc的物件

  • 本著程式碼上儘量隔離兩種語言避免開發上的混亂和困難,有時候需要一些設計,比如C++做三方庫在一個以OC為主的環境中進行使用,OC需要任意呼叫C++的各種介面和物件,但是不希望三方庫直接引用oc標頭檔案,希望三方庫解耦,只通過固定回撥或者協議來通訊
  • 這demo程式碼僅僅是一種刻意設計,為了展示故意而為的,真正開發的時候需要根據自己情況好好進行設計

參考文獻

blog.csdn.net/weiwangchao…

www.philjordan.eu/article/mix…

bbs.9ria.com/thread-2297…

相關文章