嵌入式開發(fā)過程中,我們會(huì)使用一些腳本工具輔助我們的工作,例如shel或者python、lua等,今天給大家分享一下,我在工作中用到的lua腳本交互使用。
歡迎關(guān)注微信公眾號(hào):羽林君,或者添加作者個(gè)人微信:become_me
情節(jié)介紹:
工作中, 因?yàn)槲覀兊?a class="article-link" target="_blank" href="/tag/%E4%BC%A0%E6%84%9F%E5%99%A8/">傳感器需要出廠標(biāo)定,所以我們需要有一個(gè)配置文件進(jìn)行保存我們的傳感器參數(shù),這個(gè)文件支持讀取和修改,實(shí)現(xiàn)這個(gè)功能有很多種方式,常規(guī)就是使用一個(gè)普通文件進(jìn)行讀寫。
但是我考慮到,我們數(shù)據(jù)的復(fù)雜性,以及文件注釋的描述,我選擇了xml文件進(jìn)行數(shù)據(jù)的保存,但是xml文件操作的庫我又不想去自己寫也不想去外部添加使用,本來就是一個(gè)小功能,沒必要再去新增額外鏈接,使用別的xml操作庫,所以我就盯上了我們激光slam建圖算法里面用到的lua腳本,這個(gè)lua腳本的包本身以及在內(nèi)核里面添加并在其他進(jìn)程使用了,我只需要在我這邊編譯選項(xiàng)加 -llua動(dòng)態(tài)鏈過去就可以多個(gè)進(jìn)程一起使用了。
除了方便,也考慮到lua是一個(gè)輕量級(jí)的腳本,支持交互調(diào)用,比如說我們可以通過代碼內(nèi)部執(zhí)行調(diào)用lua腳本函數(shù),也可以在lua執(zhí)行代碼注冊(cè)進(jìn)去的函數(shù)。這個(gè)比shell和python有很多優(yōu)勢(shì),shell只能在它腳本生成的終端去執(zhí)行以及python也是類似,無法進(jìn)行雙方的函數(shù)交互調(diào)用。而lua可以交互調(diào)用,所以很方便。
lua介紹
Lua ,是巴西里約熱內(nèi)盧天主教大學(xué)里的一個(gè)研究小組于 1993 年開發(fā)的。是一種輕量小巧的腳本語言,用標(biāo)準(zhǔn)C語言編寫并以源代碼形式開放, 其設(shè)計(jì)目的是為了嵌入應(yīng)用程序中,從而為應(yīng)用程序提供靈活的擴(kuò)展和定制功能。其設(shè)計(jì)目的是為了嵌入應(yīng)用程序中,從而為應(yīng)用程序提供靈活的擴(kuò)展和定制功能。
Lua 特性
輕量級(jí): 它用標(biāo)準(zhǔn)C語言編寫并以源代碼形式開放,編譯后僅僅一百余K,可以很方便的嵌入別的程序里??蓴U(kuò)展: Lua提供了非常易于使用的擴(kuò)展接口和機(jī)制:由宿主語言(通常是C或C++)提供這些功能,Lua可以使用它們,就像是本來就內(nèi)置的功能一樣。
支持面向過程(procedure-oriented)編程和函數(shù)式編程(functional programming);自動(dòng)內(nèi)存管理;只提供了一種通用類型的表(table),用它可以實(shí)現(xiàn)數(shù)組,哈希表,集合,對(duì)象;語言內(nèi)置模式匹配;閉包(closure);函數(shù)也可以看做一個(gè)值;提供多線程(協(xié)同進(jìn)程,并非操作系統(tǒng)所支持的線程)支持;通過閉包和table可以很方便地支持面向?qū)ο缶幊趟枰囊恍╆P(guān)鍵機(jī)制,比如數(shù)據(jù)抽象,虛函數(shù),繼承和重載等。
Lua 應(yīng)用場(chǎng)景
游戲開發(fā)、獨(dú)立應(yīng)用腳本、Web 應(yīng)用腳本、擴(kuò)展和數(shù)據(jù)庫插件如:MySQL Proxy 和 MySQL WorkBench、安全系統(tǒng),如入侵檢測(cè)系統(tǒng)。
lua交互原理基礎(chǔ)知識(shí)
lua和c++是通過一個(gè)虛擬棧來交互的。c++調(diào)用lua實(shí)際上是:由c++先把數(shù)據(jù)放入棧中,由lua去棧中取數(shù)據(jù),然后返回?cái)?shù)據(jù)對(duì)應(yīng)的值到棧頂,再由棧頂返回c++。lua調(diào)c++也一樣:先編寫自己的c模塊,然后注冊(cè)函數(shù)到lua解釋器中,然后由lua去調(diào)用這個(gè)模塊的函數(shù)。
因?yàn)樵谖覀冊(cè)O(shè)備上本來就有l(wèi)ua庫,所以我開發(fā)時(shí)候直接就在CMakeLists.txt文件里面增加了 -llua,但是最開始在自己pc驗(yàn)證的時(shí)候,本機(jī)是沒有相應(yīng)的lua包的,還是下載了官網(wǎng)lua的源碼進(jìn)行編譯之后,放到我的電腦指定目錄進(jìn)行操作驗(yàn)證的。
- lua源碼下載
去官網(wǎng) http://www.lua.org/download.html ?下載
make
make install
編譯好的文件放到了以下三個(gè)目錄
- /usr/local/bin ?解釋器目錄/usr/local/include 頭文件目錄/usr/local/lib 動(dòng)態(tài)鏈接庫目錄
在后面我們進(jìn)行本機(jī)測(cè)試代碼時(shí)候,就可以加上絕對(duì)目錄,進(jìn)行頭文件搜索和動(dòng)態(tài)庫鏈接了。
下面是我的一個(gè)demo測(cè)試的Makefile
文件內(nèi)容,其中就用了頭文件目錄
和動(dòng)態(tài)鏈接庫目錄
。
OBJS?=?test_cpp_lua.o?
CFLAGS?=?-Wall?-g?-std=c++11
CC?=?gcc
CPP?=?g++
INCLUDES?+=-I?/usr/local/include?
LIBS?+=??-L?/usr/local/lib???-llua?-ldl
#LIBS?=???-ldl?-llua
target:${OBJS}
#?g++??-o?target?test_cpp_lua.o??-llua?-ldl?
?@echo?"--?start?"?${CC}?${CFLAGS}?${OBJS}??-o?$@??${INCLUDES}??${LIBS}
?$(CPP)?${CFLAGS}?${OBJS}??-o?$@??${INCLUDES}??${LIBS}
clean:
?-rm?-f?*.o?core?*.core?target
.cpp.o:
#%.o:%.cpp
?${CPP}?${CFLAGS}?${INCLUDES}?-c??$<
注意:我在編譯時(shí)候還用了-ldl ,是因?yàn)槌绦蛑惺褂?code>dlopen、dlsym
、dlclose
、dlerror
顯示加載動(dòng)態(tài)庫,需要設(shè)置鏈接選項(xiàng) -ldl
加載動(dòng)態(tài)鏈接庫,首先為共享庫分配物理內(nèi)存,然后在進(jìn)程對(duì)應(yīng)的頁表項(xiàng)中建立虛擬頁和物理頁面之間的映射。
Lua是一種嵌入式腳本語言,即Lua不是可以單獨(dú)運(yùn)行的程序,在實(shí)際應(yīng)用中,主要存在兩種應(yīng)用形式。第一種形式是,C/C++作為主程序,調(diào)用Lua代碼,此時(shí)可以將Lua看做“可擴(kuò)展的語言”,我們將這種應(yīng)用稱為“應(yīng)用程序代碼”。第二種形式是Lua具有控制權(quán),而C/C++代碼則作為L(zhǎng)ua的“庫代碼”。在這兩種形式中,都是通過Lua提供的C API完成兩種語言之間的通信的。
接下來我給大家分別介紹兩者調(diào)用的用法,以及補(bǔ)充到我自己實(shí)際使用xml文件的讀寫的操作demo。本文沒有過多描述lua腳本語言的使用操作,僅僅做一些實(shí)際調(diào)用過程中的應(yīng)用分享。
C/C++代碼調(diào)用 lua變量和函數(shù)
首先我們最常用的就是進(jìn)行腳本的調(diào)用,來個(gè)最常見的調(diào)用機(jī)制,在代碼里面執(zhí)行調(diào)用腳本里面函數(shù)或者獲得腳本文件里面的一些設(shè)置信息。
這lua腳本里面的代碼部分:
debug_enbale?=?"enable"
angle_table?=?{
roll_offset?=?0.05?,
pitch_offset?=?0.0,
yaw_offset?=?0.0,
}
for?i,v?in?ipairs(angle_table)?do
????????print(i,v)
?end
這個(gè)里面定義了一個(gè)字符串變量 debug_enbale
,和一個(gè) lua的table angle_table
,最后還有一個(gè)進(jìn)行table遍歷的for循環(huán)流程控制代碼。這樣在執(zhí)行l(wèi)ua腳本時(shí)候就可以打印對(duì)應(yīng)table里面變量信息
在lua中,lua堆棧就是一個(gè)struct,堆棧索引的方式可是是正數(shù)也可以是負(fù)數(shù),區(qū)別是:正數(shù)索引1永遠(yuǎn)表示棧底,負(fù)數(shù)索引-1永遠(yuǎn)表示棧頂。所以我們?cè)谑褂眠^程中會(huì)看到push_x 和 to_x這樣的函數(shù),就是進(jìn)行堆棧的操作。
這部分是常規(guī)的使用,我在里面分別獲取了number數(shù)據(jù)和string數(shù)據(jù),放到我的執(zhí)行代碼的運(yùn)行變量中去。
#include?"lua.hpp"
#include?<iostream>
int?main(int?argc,char?**?argv)
{
????lua_State?*pLua?=?luaL_newstate();
????if(!pLua)
????{
????????LOG(Info,??"Failed?to?open?Lua!");
????????return?false;
????}
????luaL_openlibs(pLua);
????
????int?bRet?=?luaL_loadfile(pLua,?lua_path.c_str());
????if?(bRet)
????{
????????LOG(Info,?"load?.lua?file?failed"?);
????????return?false;
????}
???//?執(zhí)行l(wèi)ua文件
????bRet?=?lua_pcall(pLua,?0,?0,?0);
????if?(bRet)
????{
????????LOG(Info,??"call?.lua?file?failed"?);
????????return?false;
????}
????
????lua_getglobal(pLua,?"debug_enbale");?
????std::string?str?=?lua_tostring(pLua,?-1);//獲得lua腳本debug_enbale位置的數(shù)據(jù)
????LOG(Info,??"debug_enbale="?<<?str);
????
????auto?get_float_data_from_lua?=?[&](const?char?*?table_name,const?char?*?value_name)?->?float{
???????????lua_getglobal(pLua,?table_name);
???????????lua_getfield(pLua,?-1,?value_name);
???????????return?lua_tonumber(pLua,?-1);
????};
????roll_offset?=?get_float_data_from_lua("angle_table","roll_offset");
????pitch_offset?=?get_float_data_from_lua("angle_table","pitch_offset");
????yaw_offset?=?get_float_data_from_lua("angle_table","yaw_offset");
????LOG(Info,??"angle_table:"?
????????<<?roll_offset?<<"?"
????????<<?pitch_offset?<<"?"
????????<<?yaw_offset?);??
????lua_close(pLua);
????????
}
重要函數(shù)描述
1.因?yàn)楣こ淌莄pp,所以添加lua.hpp,如果是C工程,可以直接包含lua.h。
2.lua_State *pLua = luaL_newstate(); Lua庫中沒有定義任何全局變量,而是將所有的狀態(tài)都保存在動(dòng)態(tài)結(jié)構(gòu)lua_State中,后面所有的C API都需要該指針作為第一個(gè)參數(shù)。3.luaL_openlibs函數(shù)是用于打開Lua中的所有標(biāo)準(zhǔn)庫,如io庫、string庫等。
4.luaL_loadfile實(shí)際調(diào)用了lua_load函數(shù)來加載lua文件。
5.lua_pcall函數(shù)會(huì)將程序塊從棧中彈出,并在保護(hù)模式下運(yùn)行該程序塊。執(zhí)行成功返回0,否則將錯(cuò)誤信息壓入棧中。6.lua_getglobal調(diào)用這個(gè)宏的時(shí)候,都會(huì)將Lua代碼中與之相應(yīng)的全局變量值壓入棧中
7.lua_tostring函數(shù)中的-1,表示棧頂?shù)乃饕?,棧底的索引值?,以此類推。該函數(shù)將返回棧頂?shù)淖址畔?/p>
7.lua_getfield把堆棧中指定索引-1為棧頂 angle_table中的value_name的具體值push到堆棧。
8.lua_tonumber 把棧頂中數(shù)據(jù)以數(shù)值形式返回
9.lua_close用于釋放狀態(tài)指針?biāo)玫馁Y源。
其中,使用有數(shù)據(jù)區(qū)分的函數(shù)lua_tonumber和lua_tostring兩種函數(shù),lua_tonumber返回包括整形和浮點(diǎn)型。
這樣我就獲得了lua腳本里面我寫好的數(shù)據(jù),用來配合我代碼執(zhí)行。
lua變量 調(diào)用C/C++代碼函數(shù)
引用文章《Step By Step(Lua調(diào)用C函數(shù))》
Lua可以調(diào)用C函數(shù)的能力將極大的提高Lua的可擴(kuò)展性和可用性。對(duì)于有些和操作系統(tǒng)相關(guān)的功能,或者是對(duì)效率要求較高的模塊,我們完全可以通過C函數(shù)來實(shí)現(xiàn),之后再通過Lua調(diào)用指定的C函數(shù)。對(duì)于那些可被Lua調(diào)用的C函數(shù)而言,其接口必須遵循Lua要求的形式,即typedef int (lua_CFunction)(lua_State L)。簡(jiǎn)單說明一下,該函數(shù)類型僅僅包含一個(gè)表示Lua環(huán)境的指針作為其唯一的參數(shù),實(shí)現(xiàn)者可以通過該指針進(jìn)一步獲取Lua代碼中實(shí)際傳入的參數(shù)。返回值是整型,表示該C函數(shù)將返回給Lua代碼的返回值數(shù)量,如果沒有返回值,則return 0即可。需要說明的是,C函數(shù)無法直接將真正的返回值返回給Lua代碼,而是通過虛擬棧來傳遞Lua代碼和C函數(shù)之間的調(diào)用參數(shù)和返回值的。這里我們將介紹兩種Lua調(diào)用C函數(shù)的規(guī)則。
Lua調(diào)用C函數(shù)有兩種方式
1、程序主體在C中運(yùn)行,C函數(shù)注冊(cè)到Lua中。C調(diào)用Lua,Lua調(diào)用C注冊(cè)的函數(shù),C得到函數(shù)的執(zhí)行結(jié)果。
2、程序主體在Lua中運(yùn)行,C函數(shù)作為庫函數(shù)供Lua使用。第一種方式看起來很羅嗦,也很奇怪。既然程序主體運(yùn)行在C中,而且最終使用的也是C中定義的函數(shù),那么為何要將函數(shù)注冊(cè)給Lua,然后再通過Lua調(diào)用函數(shù)呢?
所以相比于第一種方式,第二種方式使用的更加普遍。
關(guān)于這部分代碼我也只是在我電腦上跑了幾個(gè)范例,大家也可以去網(wǎng)上自己去查找相關(guān)例子,我自己在設(shè)備上并沒有使用到這個(gè)部分,后續(xù)我有使用可以再寫這部分應(yīng)用給大家分享。
lua進(jìn)行xml文件的操作
這個(gè)部分是因?yàn)橹暗墓δ茏隽艘恍┬拚?,原因是我們需要的傳感器參?shù)放置的文件可以被修改,如果使用lua腳本里面寫入?yún)?shù),那么參數(shù)相當(dāng)與定死了,我們后續(xù)是無法使用lua腳本修改里面本身的數(shù)據(jù)的,所以后來我就使用xml文件放置我的傳感器參數(shù),使用lua腳本進(jìn)行讀寫。
xml是一個(gè)常用來寫一些我們不定數(shù)據(jù)配置文件的格式,lua也有l(wèi)uaxml,工具包,但是我為了不新增額外庫實(shí)現(xiàn),所以使用了lua里面I/O庫(lua用于讀取和處理文件的庫)讀寫的方法進(jìn)行讀寫xml文件。
大家也可以用xml一個(gè)lua其他工具進(jìn)行使用,會(huì)更加方便,下面分享一個(gè)官方給的鏈接:http://lua-users.org/wiki/LuaXml
里面分為四種不同方法的xml讀寫工具:
- 工具包;僅限 Lua 的 XML 解析器;包含 C 代碼和綁定的 XML 解析器;用于處理基于 XML 的協(xié)議(例如 XML-RPC 和 SOAP)的模塊。
lua腳本中讀寫xml函數(shù)
function?get_value_from_xml(path,element_name)
?xml_file=path????????
?element=element_name????
?head="<"..element..">"?????
?tail="</"..element..">"????
?file?=?io.open(xml_file,?"r");??--打開xml文件
?data?=?file:read("*all");?????--讀取文件的全部?jī)?nèi)容到data變量中
?file:close();?????????????????--關(guān)閉xml文件
?--獲取起始tag與關(guān)閉tag之間的內(nèi)容到value中
?_,_,value=string.find(data,?head.."(.-)"..tail)
?--輸出value的值到標(biāo)準(zhǔn)輸出
?--?print(value)
?return?value
end
function?set_value_to_xml(path,element_name,set_value)
?xml_file=path???
?element=element_name????
?new_value=set_value??
?head="<"..element..">"????--根據(jù)元素名生成起始tag,即<element_name>
?tail="</"..element..">"???--根據(jù)元素名生成關(guān)閉tag,即</element_name>
?file?=?io.open(xml_file,?"r");?--打開xml文件
?data?=?file:read("*all");??????--讀取文件的全部?jī)?nèi)容到data變量中
?file:close();??????????????????--關(guān)閉xml文件
?--將element之前的內(nèi)容,element的值,element之后的內(nèi)容,分別保存在pre,old_value,follow中
?_,_,pre,old_value,follow=string.find(data,?"(.*)("..head..".-"..tail..")(.*)")
?file?=?io.open(xml_file,?"w");??????--打開xml文件
?file:write(pre..head..new_value..tail..follow);?--拼裝出新的文件內(nèi)容,并寫入
?file:close();???????????????????????--關(guān)閉xml文件
end
上面主要是利用了 string.find 它的三個(gè)參數(shù)和三個(gè)返回值。string.find 功能是從字符串中找到特定的內(nèi)容。
第一個(gè)參數(shù)是目標(biāo)字符串(所有的內(nèi)容,比如data),第二個(gè)參數(shù)是想要找的字符串(比如 item 之間的內(nèi)容),第三個(gè)參數(shù)是從第幾個(gè)字符開始找起(我讓它成為一個(gè)不斷變化的值)。
第一個(gè)返回值是找到的字符串( item 之間的內(nèi)容)的首字符位置(一個(gè)數(shù)字),第二個(gè)返回值是找到的字符串( item 之間的內(nèi)容)的尾字符位置(一個(gè)數(shù)字),第三個(gè)是找到的字符串( item 之間的內(nèi)容)。
所以代碼就是靠每次循環(huán)的第三個(gè)返回值來獲取內(nèi)容。
cpp代碼:
#include?"lua.hpp"
#include?<iostream>
int?main(int?argc,char?**?argv)
{
???lua_State?*pLua?=?luaL_newstate();
????if(!pLua)
????{
????????LOG(Info,??"Failed?to?open?Lua!");
????????return?false;
????}
????luaL_openlibs(pLua);
????
????int?bRet?=?luaL_loadfile(pLua,?lua_path.c_str());
????if?(bRet)
????{
????????LOG(Info,?"load?.lua?file?failed"?);
????????return?false;
????}
???//?執(zhí)行l(wèi)ua文件
????bRet?=?lua_pcall(pLua,?0,?0,?0);
????if?(bRet)
????{
????????LOG(Info,??"call?.lua?file?failed"?);
????????return?false;
????}
????auto?lua_func_call_wirte?=?[&](const?char?*?func_name,?const?char?*?key_name,float?&value){
????????lua_getglobal(pLua,?func_name);
????????lua_pushstring(pLua?,surface_xml_used_path.c_str());??
????????lua_pushstring(pLua?,key_name);??
????????lua_pushnumber(pLua?,value);??
????????bRet?=?lua_pcall(pLua,?3,?1,?0);//三個(gè)參數(shù)?一個(gè)返回值
????????if?(bRet)
????????{
????????????const?char*?pErrorMsg?=?lua_tostring(pLua,?-1);
????????????LOG(Info,?"lua_pcall?-?ErrorMsg:"??<<?pErrorMsg?);
????????????//?lua_close(pLua);
????????????return?false;
????????}
????????
????????if?(lua_isnumber(pLua,?-1))
????????{
????????????value?=?lua_tonumber(pLua,?-1);
????????????LOG(Info,??"surface_config?"?<<?value);
????????????return?true;
????????}?????
????????return?false;
????};??
??????float?roll_offset?=?0.5,pitch_offset?=?0.2,yaw_offset=0.6;
????lua_func_call_wirte("set_value_to_xml","?roll_offset",?roll_offset);
????lua_func_call_wirte("set_value_to_xml","?pitch_offset",?pitch_offset);
????lua_func_call_wirte("set_value_to_xml","?yaw_offset",?yaw_offset);
????
????
????
??auto?lua_func_call_number?=?[&](const?char?*?func_name,?const?char?*?key_name,float?&value){
????????lua_getglobal(pLua,?func_name);
????????lua_pushstring(pLua?,surface_xml_used_path.c_str());??
????????lua_pushstring(pLua?,key_name);??
????????bRet?=?lua_pcall(pLua,?2,?1,?0);//兩個(gè)參數(shù)?一個(gè)返回值
????????if?(bRet)
????????{
????????????const?char*?pErrorMsg?=?lua_tostring(pLua,?-1);
????????????LOG(Info,?"lua_pcall?-?ErrorMsg:"??<<?pErrorMsg?);
????????????//?lua_close(pLua);
????????????return?false;
????????}
????????
????????if?(lua_isnumber(pLua,?-1))
????????{
????????????value?=?lua_tonumber(pLua,?-1);
????????????LOG(Info,??"config?"?<<?value);
????????????return?true;
????????}?????
????????return?false;
????};??
????auto?lua_func_call_string?=?[&](const?char?*?func_name,?const?char?*?key_name,std::string?&value){
????????lua_getglobal(pLua,?func_name);
????????lua_pushstring(pLua?,surface_xml_used_path.c_str());??
????????lua_pushstring(pLua?,key_name);??
????????bRet?=?lua_pcall(pLua,?2,?1,?0);
????????if?(bRet)
????????{
????????????const?char*?pErrorMsg?=?lua_tostring(pLua,?-1);
????????????ZY_LOG("robotctl",??kInfo,?"lua_pcall?-?ErrorMsg:"??<<?pErrorMsg?);
????????????lua_close(pLua);
????????????return?false;
????????}
????????
????????if?(lua_isstring(pLua,?-1))
????????{
????????????value?=?lua_tostring(pLua,?-1);
????????????LOG(Info,??"config?"?<<?value.c_str()?);
????????????return?true;
????????}?????
????????return?false;
??
????};
????std::string??temp_from_lua;
????lua_func_call_string("get_value_from_xml","debug_enable",temp_from_lua);
????LOG(Info,??"debug_enbale?="?<<?temp_from_lua);
????lua_func_call_number("get_value_from_xml","down_roll_offset",roll_offset);
????lua_func_call_number("get_value_from_xml","down_pitch_offset",pitch_offset);
????lua_func_call_number("get_value_from_xml","down_yaw_offset",yaw_offset);
????
????LOG(Info,??"ngle_table:"?
????????<<?roll_offset?<<"?"
????????<<?pitch_offset?<<"?"
????????<<?yaw_offset?);???
????lua_close(pLua);
}
看到這塊大家可能會(huì)問我,為什么不直接使用linux下直接做一個(gè)文件進(jìn)行讀寫配置數(shù)據(jù)呢,我的想法是,純文件不好進(jìn)行注釋,因?yàn)槲业呐渲脜?shù)有些很長(zhǎng),我想把它盡可能讓別人看懂,所以我寫了一些注釋進(jìn)去,xml很符合我的要求,此外lua腳本也一些腳本使用過程中,很好的可以輔助我本身的代碼執(zhí)行,所以我就考慮把一些整體差不多執(zhí)行的功能操作可以集成到一起,統(tǒng)一接口去執(zhí)行。所以最后選擇了lua+xml,技術(shù)有很多種實(shí)現(xiàn)思路,但是我們需要衡量一下哪些部分的技術(shù)可以讓平臺(tái)可以重復(fù)利用的多一些。
結(jié)語
這就是我分享我在工作中使用lua腳本的操作,如果大家有更好的想法和需求,也歡迎大家加我好友交流分享哈。
作者:良知猶存,白天努力工作,晚上原創(chuàng)公號(hào)號(hào)主。公眾號(hào)內(nèi)容除了技術(shù)還有些人生感悟,一個(gè)認(rèn)真輸出內(nèi)容的職場(chǎng)老司機(jī),也是一個(gè)技術(shù)之外豐富生活的人,攝影、音樂 and 籃球。關(guān)注我,與我一起同行。