开年第一篇贱贱的Android NDK服务
来源:互联网 发布:legacy安装ubuntu 编辑:程序博客网 时间:2024/06/03 23:43
首先先祝大家新年快乐,晚了一点哈,写这篇文章主要是因为项目中需要,我暂时性的做一个记录,首先要感谢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 __cplusplusextern "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还是很有帮助的,犹如我之前写过的一片文章,我觉得这就是一种冥冥之中的趋势;哈哈,一起加油进步吧;很晚了,晚安各位;
- 开年第一篇贱贱的Android NDK服务
- Android中的Service的用法 开启服务,下一篇讲绑定服务
- android ndk的使用
- android NDK的理解
- android NDK的学习
- Android的NDK开发
- android NDK的作用
- Android NDK的理解
- Android的NDK开发
- Android NDK的使用
- Android的NDK开发
- Android NDK的安装
- Android NDK的文件夹
- Android NDK的使用
- Android NDK 的学习
- Android的NDK开发
- Android - NDK的使用
- 我的第一篇博客——Android Studio 2.2 NDK开发之Cmake 编译多个源文件的问题
- 什么是JavaEE
- js-begin chapter06 对象
- 向页面中放入另一个页面
- ajax乱码问题
- PCA
- 开年第一篇贱贱的Android NDK服务
- 条款三十: Proxy classes(替身类、代理类)
- [Leetcode] 42. Trapping Rain Water 解题报告
- SOM机器学习
- 使用nginx 同一端口根据不同域名转发到不同端口
- 新手上路,大家觉得毕业设计做Symbian可行么?
- 主Activity中如何获取Viewpager下其中一个fragment的方法?
- 提交的时候选择<INPUT name= type=hidden>
- 用JavaScript实现层在指定位置显示