首先先祝大家新年快樂,晚了一點哈,寫這篇文章主要是因爲項目中需要,我暫時性的做一個記錄,首先要感謝LeBron_Six 這位博主對文章中技術的支持;Google 對安卓性能上的優化那也不是蓋得,安卓5.0的出現已經表現出了很多,然後現在又是6.0和7.0,之前做Service所用用到的是在OnStartCommand中返回START_STICKY;然而呢,這個現在看起來並不好用,然後網上看文章,找資料,發現有解決方案,真的不錯,哈哈,幫助了我很大,希望這個文章寫出來能幫助你;同時這也是我對上半年的一個總結吧;上年中沒更新多少,希望在今年中能給大家帶來更多更好的文章吧,我一個新手的爬坑之路,希望大家能夠諒解,有些問題不明白的希望能和大家一起討論完成;廢話不多說了,上幹活吧。(本文中依然用as進行開發,至於爲什麼沒用Cmake。主要是我覺得還是這個比較好,用着比較舒服習慣吧)
一.工程目錄結構
二.主要的native代碼
package lv.anto.com.ndktest;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
public class NativeRuntime {
private static NativeRuntime theInstance = null;
private NativeRuntime() {
}
public static NativeRuntime getInstance() {
if (theInstance == null)
theInstance = new NativeRuntime();
return theInstance;
}
/**
* RunExecutable 啓動一個可自行的lib*.so文件
* @param pacaageName
* @param filename
* @param alias 別名
* @param args 參數
* @return
*/
public String RunExecutable(String pacaageName, String filename, String alias, String args) {
String path = "/data/data/" + pacaageName;
String cmd1 = path + "/lib/" + filename;
String cmd2 = path + "/" + alias;
String cmd2_a1 = path + "/" + alias + " " + args;
String cmd3 = "chmod 777 " + cmd2;
String cmd4 = "dd if=" + cmd1 + " of=" + cmd2;
StringBuffer sb_result = new StringBuffer();
if (!new File("/data/data/" + alias).exists()) {
RunLocalUserCommand(pacaageName, cmd4, sb_result); // 拷貝lib/libtest.so到上一層目錄,同時命名爲test.
sb_result.append(";");
}
RunLocalUserCommand(pacaageName, cmd3, sb_result); // 改變test的屬性,讓其變爲可執行
sb_result.append(";");
RunLocalUserCommand(pacaageName, cmd2_a1, sb_result); // 執行test程序.
sb_result.append(";");
return sb_result.toString();
}
/**
* 執行本地用戶命令
* @param pacaageName
* @param command
* @param sb_out_Result
* @return
*/
public boolean RunLocalUserCommand(String pacaageName, String command, StringBuffer sb_out_Result) {
Process process = null;
try {
process = Runtime.getRuntime().exec("sh"); // 獲得shell進程
DataInputStream inputStream = new DataInputStream(process.getInputStream());
DataOutputStream outputStream = new DataOutputStream(process.getOutputStream());
outputStream.writeBytes("cd /data/data/" + pacaageName + "\n"); // 保證在command在自己的數據目錄裏執行,纔有權限寫文件到當前目錄
outputStream.writeBytes(command + " &\n"); // 讓程序在後臺運行,前臺馬上返回
outputStream.writeBytes("exit\n");
outputStream.flush();
process.waitFor();
byte[] buffer = new byte[inputStream.available()];
inputStream.read(buffer);
String s = new String(buffer);
if (sb_out_Result != null)
sb_out_Result.append("CMD Result:\n" + s);
} catch (Exception e) {
if (sb_out_Result != null)
sb_out_Result.append("Exception:" + e.getMessage());
return false;
}
return true;
}
public native void startActivity(String compname);
public native String stringFromJNI();
public native void startService(String srvname, String sdpath);
public native int findProcess(String packname);
public native int stopService();
static {
try {
System.loadLibrary("NativeRuntime"); // 加載so庫
} catch (Exception e) {
e.printStackTrace();
}
}
}
三.用javah生成編譯native 出來的是NativeRuntime.h文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class lv_anto_com_ndktest_NativeRuntime */
#ifndef _Included_lv_anto_com_ndktest_NativeRuntime
#define _Included_lv_anto_com_ndktest_NativeRuntime
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: lv_anto_com_ndktest_NativeRuntime
* Method: startActivity
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_lv_anto_com_ndktest_NativeRuntime_startActivity
(JNIEnv *, jobject, jstring);
/*
* Class: lv_anto_com_ndktest_NativeRuntime
* Method: stringFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_lv_anto_com_ndktest_NativeRuntime_stringFromJNI
(JNIEnv *, jobject);
/*
* Class: lv_anto_com_ndktest_NativeRuntime
* Method: startService
* Signature: (Ljava/lang/String;Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_lv_anto_com_ndktest_NativeRuntime_startService
(JNIEnv *, jobject, jstring, jstring);
/*
* Class: lv_anto_com_ndktest_NativeRuntime
* Method: findProcess
* Signature: (Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_lv_anto_com_ndktest_NativeRuntime_findProcess
(JNIEnv *, jobject, jstring);
/*
* Class: lv_anto_com_ndktest_NativeRuntime
* Method: stopService
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_lv_anto_com_ndktest_NativeRuntime_stopService
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
四.配置文件Android.mk 和Application.mk;
Android.mk
LOCAL_PATH :=
LOCAL_MODULE := NativeRuntime
LOCAL_SRC_FILES := NativeRuntime.c
include $(BUILD_SHARED_LIBRARY)
Application.mk
APP_ABI := all
五.上面弄完了寫下.C文件的編寫了,主代碼來了,別眨眼啊;
//
// Created by mac on 16/12/30.
//
#include <string.h>
#include <jni.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>
#include <dirent.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
#define PROC_DIRECTORY "/proc/"
void thread(char* srvname);
char* a;
/**
* srvname 服務名
* sd 之前創建子進程的pid寫入的文件路徑
*/
int start(int argc, char* srvname, char* sd) {
pthread_t id;
int ret;
struct rlimit r;
/**
* 第一次fork的作用是讓shell認爲本條命令已經終止,不用掛在終端輸入上。
* 還有一個作用是爲後面setsid服務。setsid的調用者不能是進程組組長(group leader)。
* 此時父進程是進程組組長。
*/
int pid = fork();
if (pid < 0) {
exit(0);
} else if (pid != 0) {
//exit(0);
} else { // 第一個子進程
//int setsid = setsid();
umask(0); //使用umask修改文件的屏蔽字,爲文件賦予跟多的權限,因爲繼承來的文件可能某些權限被屏蔽,從而失去某些功能,如讀寫
int pid = fork();
if (pid == 0) { // 第二個子進程
FILE *fp;
sprintf(sd,"%s/pid",sd);
if((fp=fopen(sd,"a"))==NULL) {//打開文件 沒有就創建
ftruncate(fp, 0);
lseek(fp, 0, SEEK_SET);
}
fclose(fp);
fp=fopen(sd,"rw");
if(fp>0){
char buff1[6];
int p = 0;
memset(buff1,0,sizeof(buff1));
fseek(fp,0,SEEK_SET);
fgets(buff1,6,fp); //讀取一行
if(strlen(buff1)>1){ // 有值
kill(atoi(buff1), SIGTERM);
}
}
fclose(fp);
fp=fopen(sd,"w");
char buff[100];
int k = 3;
if(fp>0){
sprintf(buff,"%lu",getpid());
fprintf(fp,"%s\n",buff); // 把進程號寫入文件
}
fclose(fp);
fflush(fp);
//step 4:修改進程工作目錄爲根目錄,chdir(“/”).
chdir("/");
//step 5:關閉不需要的從父進程繼承過來的文件描述符。
if (r.rlim_max == RLIM_INFINITY) {
r.rlim_max = 1024;
}
int i;
for (i = 0; i < r.rlim_max; i++) {
close(i);
}
umask(0);
ret = pthread_create(&id, NULL, (void *) thread, srvname);
if (ret != 0) {
printf("Create pthread error!\n");
exit(1);
}
int stdfd = open ("/dev/null", O_RDWR);
dup2(stdfd, STDOUT_FILENO);
dup2(stdfd, STDERR_FILENO);
} else {
exit(0);
}
}
return 0;
}
/**
* 執行命令
*/
void ExecuteCommandWithPopen(char* command, char* out_result,
int resultBufferSize) {
FILE * fp;
out_result[resultBufferSize - 1] = '\0';
fp = popen(command, "r");
if (fp) {
fgets(out_result, resultBufferSize - 1, fp);
out_result[resultBufferSize - 1] = '\0';
pclose(fp);
} else {
exit(0);
}
}
/**
* 檢測服務,如果不存在服務則啓動.
* 通過am命令啓動一個laucher服務,由laucher服務負責進行主服務的檢測,laucher服務在檢測後自動退出
*/
void check_and_restart_service(char* service) {
char cmdline[200];
sprintf(cmdline, "am startservice --user 0 -n %s", service);
char tmp[200];
sprintf(tmp, "cmd=%s", cmdline);
ExecuteCommandWithPopen(cmdline, tmp, 200);
}
void thread(char* srvname) {
while(1){
check_and_restart_service(srvname);
sleep(4);
}
}
jstring stoJstring(JNIEnv* env, const char* pat) {
jclass strClass = (*env)->FindClass(env, "Ljava/lang/String;");
jmethodID ctorID = (*env)->GetMethodID(env, strClass, "<init>",
"([BLjava/lang/String;)V");
jbyteArray bytes = (*env)->NewByteArray(env, strlen(pat));
(*env)->SetByteArrayRegion(env, bytes, 0, strlen(pat), (jbyte*) pat);
jstring encoding = (*env)->NewStringUTF(env, "utf-8");
return (jstring)(*env)->NewObject(env, strClass, ctorID, bytes, encoding);
}
/**
* 判斷是否是數字
*/
int IsNumeric(const char* ccharptr_CharacterList) {
for (; *ccharptr_CharacterList; ccharptr_CharacterList++)
if (*ccharptr_CharacterList < '0' || *ccharptr_CharacterList > '9')
return 0; // false
return 1; // true
}
//intCaseSensitive=0大小寫不敏感
int strcmp_Wrapper(const char *s1, const char *s2, int intCaseSensitive) {
if (intCaseSensitive)
return !strcmp(s1, s2);
else
return !strcasecmp(s1, s2);
}
//intCaseSensitive=0大小寫不敏感
int strstr_Wrapper(const char* haystack, const char* needle,
int intCaseSensitive) {
if (intCaseSensitive)
return (int) strstr(haystack, needle);
else
return (int) strcasestr(haystack, needle);
}
/**
* 通過進程名稱獲取pid
*/
pid_t GetPIDbyName_implements(const char* cchrptr_ProcessName,
int intCaseSensitiveness, int intExactMatch) {
char chrarry_CommandLinePath[100];
char chrarry_NameOfProcess[300];
char* chrptr_StringToCompare = NULL;
pid_t pid_ProcessIdentifier = (pid_t) - 1;
struct dirent* de_DirEntity = NULL;
DIR* dir_proc = NULL;
int (*CompareFunction)(const char*, const char*, int);
if (intExactMatch)
CompareFunction = &strcmp_Wrapper;
else
CompareFunction = &strstr_Wrapper;
dir_proc = opendir(PROC_DIRECTORY);
if (dir_proc == NULL) {
perror("Couldn't open the " PROC_DIRECTORY " directory");
return (pid_t) - 2;
}
while ((de_DirEntity = readdir(dir_proc))) {
if (de_DirEntity->d_type == DT_DIR) {
if (IsNumeric(de_DirEntity->d_name)) {
strcpy(chrarry_CommandLinePath, PROC_DIRECTORY);
strcat(chrarry_CommandLinePath, de_DirEntity->d_name);
strcat(chrarry_CommandLinePath, "/cmdline");
FILE* fd_CmdLineFile = fopen(chrarry_CommandLinePath, "rt"); //open the file for reading text
if (fd_CmdLineFile) {
fscanf(fd_CmdLineFile, "%s", chrarry_NameOfProcess); //read from /proc/<NR>/cmdline
fclose(fd_CmdLineFile); //close the file prior to exiting the routine
chrptr_StringToCompare = chrarry_NameOfProcess;
if (CompareFunction(chrptr_StringToCompare,
cchrptr_ProcessName, intCaseSensitiveness)) {
pid_ProcessIdentifier = (pid_t) atoi(
de_DirEntity->d_name);
closedir(dir_proc);
return pid_ProcessIdentifier;
}
}
}
}
}
closedir(dir_proc);
return pid_ProcessIdentifier;
}
/**
* 檢測服務,如果不存在服務則啓動
*/
void check_and_restart_activity(char* service) {
char cmdline[200];
sprintf(cmdline, "am start -n %s", service);
char tmp[200];
sprintf(tmp, "cmd=%s", cmdline);
ExecuteCommandWithPopen(cmdline, tmp, 200);
}
JNIEXPORT jstring JNICALL Java_lv_anto_com_ndktest_NativeRuntime_stringFromJNI
(JNIEnv * env, jobject thiz){
#if defined(__arm__)
#if defined(__ARM_ARCH_7A__)
#if defined(__ARM_NEON__)
#define ABI "armeabi-v7a/NEON"
#else
#define ABI "armeabi-v7a"
#endif
#else
#define ABI "armeabi"
#endif
#elif defined(__i386__)
#define ABI "x86"
#elif defined(__mips__)
#define ABI "mips"
#else
#define ABI "unknown"
#endif
return (*env)->NewStringUTF(env,
"Hello from JNI ! Compiled with ABI " ABI ".");
}
/**
* jstring 轉 String
*/
char* jstringTostring(JNIEnv* env, jstring jstr) {
char* rtn = NULL;
jclass clsstring = (*env)->FindClass(env, "java/lang/String");
jstring strencode = (*env)->NewStringUTF(env, "utf-8");
jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes",
"(Ljava/lang/String;)[B");
jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid,
strencode);
jsize alen = (*env)->GetArrayLength(env, barr);
jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
if (alen > 0) {
rtn = (char*) malloc(alen + 1);
memcpy(rtn, ba, alen);
rtn[alen] = 0;
}
(*env)->ReleaseByteArrayElements(env, barr, ba, 0);
return rtn;
}
/**
* 查找進程
*/
pid_t JNICALL Java_com_yyh_fork_NativeRuntime_findProcess(JNIEnv* env,
jobject thiz, jstring cchrptr_ProcessName) {
char * rtn = jstringTostring(env, cchrptr_ProcessName);
//return 1;
return GetPIDbyName_implements(rtn, 0, 0); //大小寫不敏感 sub串匹配
}
/**
* 啓動Service
*/
JNIEXPORT void JNICALL Java_lv_anto_com_ndktest_NativeRuntime_startService
(JNIEnv * env, jobject thiz, jstring cchrptr_ProcessName, jstring sdpath){
char * rtn = jstringTostring(env, cchrptr_ProcessName); // 得到進程名稱
char * sd = jstringTostring(env, sdpath);
a = rtn;
start(1, rtn, sd);
}
/**
*關閉Service
**/
JNIEXPORT jint JNICALL Java_lv_anto_com_ndktest_NativeRuntime_stopService
(JNIEnv * env, jobject thiz){
exit(0);
}
/*
* Class: lv_anto_com_ndktest_NativeRuntime
* Method: findProcess
* Signature: (Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_lv_anto_com_ndktest_NativeRuntime_findProcess
(JNIEnv * env, jobject thiz, jstring cchrptr_ProcessName){
char * rtn = jstringTostring(env, cchrptr_ProcessName);
//return 1;
return GetPIDbyName_implements(rtn, 0, 0); //大小寫不敏感 sub串匹配
}
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = -1;
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
return JNI_VERSION_1_4;
}
六.上面的事情都幹完了,我們該進行ndk-build進行編譯生成.os文件啦。
七.上面的事情弄完了以後呢,我們要想起來我們還有一個gradle文件要配置啊
apply plugin: 'com.android.application'
android {
compileSdkVersion 24
buildToolsVersion "24.0.2"
defaultConfig {
applicationId "lv.anto.com.ndktest"
minSdkVersion 17
targetSdkVersion 24
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk {
moduleName "NativeRuntime" //生成的so名字
ldLibs "log"//實現__android_log_print
abiFilters "armeabi", "armeabi-v7a", "x86" //輸出指定三種abi體系結構下的so庫。目前可有可無。
}
sourceSets {
main {
jniLibs.srcDirs = ["libs"]
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:24.2.1'
testCompile 'junit:junit:4.12'
}
八.又弄完了,就是這麼簡單,哈哈,接下來就是調用了;
1.在main的主activity中調用
package lv.anto.com.ndktest;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends Activity {
Button btnstart, btnend;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initService();
}
private void initService() {
btnstart = (Button) findViewById(R.id.btn_start);
btnend = (Button) findViewById(R.id.btn_end);
Toast.makeText(this, NativeRuntime.getInstance().stringFromJNI(), Toast.LENGTH_LONG).show();
String executable = "libNativeRuntime.so";
String aliasfile = "NativeRuntime";
String parafind = "/data/data/" + getPackageName() + "/" + aliasfile;
String retx = "false";
NativeRuntime.getInstance().RunExecutable(getPackageName(), executable, aliasfile, getPackageName() + "/lv.anto.com.ndktest.HostMonitor");
NativeRuntime.getInstance().startService(getPackageName() + "/lv.anto.com.ndktest.HostMonitor", FileUtils.createRootPath());
btnstart.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
(new Thread(new Runnable() {
public void run() {
try {
} catch (Exception e) {
e.printStackTrace();
}
}
})).start();
}
});
btnend.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
try {
NativeRuntime.getInstance().stopService();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
2.在BroadcastReceiver中進行配置開機啓動
package lv.anto.com.ndktest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class PhoneStatReceiver extends BroadcastReceiver {
private String TAG = "tag";
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Log.i(TAG, "手機開機了~~");
NativeRuntime.getInstance().startService(context.getPackageName() +
"/lv.anto.com.ndktest.HostMonitor"
, FileUtils.createRootPath());
} else if (Intent.ACTION_USER_PRESENT.equals(intent.getAction())) {
}
}
}
下面我再把工具類的代碼粘出來;
package lv.anto.com.ndktest;
import java.io.File;
import java.io.FileOutputStream;
import android.os.Environment;
/**
* 文件工具類
*
* @author king
*
*/
public class FileUtils {
// 根緩存目錄
private static String cacheRootPath = "";
/**
* sd卡是否可用
*
* @return
*/
public static boolean isSdCardAvailable() {
return Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED);
}
/**
* 創建根緩存目錄
*
* @return
*/
public static String createRootPath() {
if (isSdCardAvailable()) {
// /sdcard/Android/data/<application package>/cache
cacheRootPath = App.mContext.getExternalCacheDir()
.getPath();
} else {
// /data/data/<application package>/cache
cacheRootPath = App.mContext.getCacheDir().getPath();
}
return cacheRootPath;
}
/**
* 創建文件夾
*
* @param dirPath
* @return 創建失敗返回""
*/
private static String createDir(String dirPath) {
try {
File dir = new File(dirPath);
if (!dir.exists()) {
dir.mkdirs();
}
return dir.getAbsolutePath();
} catch (Exception e) {
e.printStackTrace();
}
return dirPath;
}
/**
* 獲取圖片緩存目錄
*
* @return 創建失敗,返回""
*/
public static String getImageCachePath() {
String path = createDir(createRootPath() + File.separator + "img"
+ File.separator);
return path;
}
/**
* 獲取圖片裁剪緩存目錄
*
* @return 創建失敗,返回""
*/
public static String getImageCropCachePath() {
String path = createDir(createRootPath() + File.separator + "imgCrop"
+ File.separator);
return path;
}
/**
* 刪除文件或者文件夾
*
* @param file
*/
public static void deleteFileOrDirectory(File file) {
try {
if (file.isFile()) {
file.delete();
return;
}
if (file.isDirectory()) {
File[] childFiles = file.listFiles();
// 刪除空文件夾
if (childFiles == null || childFiles.length == 0) {
file.delete();
return;
}
// 遞歸刪除文件夾下的子文件
for (int i = 0; i < childFiles.length; i++) {
deleteFileOrDirectory(childFiles[i]);
}
file.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 將內容寫入文件
*
* @param filePath
* eg:/mnt/sdcard/demo.txt
* @param content
* 內容
*/
public static void writeFileSdcard(String filePath, String content,
boolean isAppend) {
try {
FileOutputStream fout = new FileOutputStream(filePath, isAppend);
byte[] bytes = content.getBytes();
fout.write(bytes);
fout.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
剩下的事情大家就可以自行解決了,佈局文件中只有兩個按鈕,別忘了配置AndroidManifest.xml文件;
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="lv.anto.com.ndktest">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".PhoneStatReceiver"
android:enabled="true"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.USER_PRESENT" />
</intent-filter>
</receiver>
<service
android:name=".HostMonitor"
android:enabled="true"
android:exported="true"
android:process=":remote" />
</application>
</manifest>
總結:這些就弄完了,一個賤賤的不死的服務就這樣搞定了,是不是感覺很簡單呢?說實話,對於C總的東西還是有些不瞭解的,希望以後能夠進步瞭解,也同樣希望看到這個文章的你,能夠寫出來這麼賤的服務,對於社交類,後臺服務類的app還是很有幫助的,猶如我之前寫過的一片文章,我覺得這就是一種冥冥之中的趨勢;哈哈,一起加油進步吧;很晚了,晚安各位;