Android4.4 Sensor APP--HAL代码流程
3:SensorManager
上一部分说过,开机后,system server启动时,就会初始化sensor service,也就是说,开机后她一直都在后台运行着,客户端部分,直接connect就行了。至于怎么connect,这一切都被封装到SensorManager里了。
3.1 SensorManager的创建
获取SensorManager的对象实例代码:
mSensorManager =(SensorManager)getSystemService(SENSOR_SERVICE);
调用Activity的成员函数来获取SensorManager实例,我们从Activity派生关系可以追溯到,这个函数的最终在ContextImpl实现:
- //ContextImpl.java
- @Override
- public ObjectgetSystemService(String name) {
- ServiceFetcherfetcher = SYSTEM_SERVICE_MAP.get(name);
- return fetcher ==null ? null : fetcher.getService(this);
- }
这个函数从SYSTEM_SERVICE_MAP中获取了name对应的特定对象实例,所以从SYSTEM_SERVICE_MAP的初始化,就可以看到SensorManager对象的创建:
- //ContextImpl.java
- static{ ...
- registerService(SENSOR_SERVICE, newServiceFetcher() {
- public ObjectcreateService(ContextImpl ctx) {
- return newSensorManager(ctx.mMainThread.getHandler().getLooper());
- }}); ...
- }
3.2 初始化并连接sensor service
初始化过程肯定是在构造函数中进行,那如何连接sensor service呢?上一部分说过,sensor service是基于c++代码编写的native binder,客户端要与其连接并交互,当然也是使用c++更方便(我只是说比较方便,当然你如果硬要用java与其建立连接并交互数据,也是可以的).
如果一种做法可以让你更方便,我想大多数人的选择都是一样的,就是使用C++代码访问服务,然后java代码通过jni调用c++代码,这也是android系统的通用做法;接下去,我们看下SensorManager的jni函数映射:
在android jni中c++类文件的命名规则一般都是java类的package路径+类名,还有一点需要注意的是,这里jni映射函数名都是一样的,这只是这个类的设计者这么命名而已,实际上c++类中的对应函数命名是没有限制的,关于jni的详细描述,大家可查看相关资料,这里就不再赘述.
在了解了jni函数映射后,后续在java代码中如果调用了native函数,我们将直接跳转到c++代码.
SensorManager被实例化,地球人都知道构造函数先走,所以接下去看SensorManager构造函数:
- public SensorManager(Looper mainLooper) {
- mMainLooper = mainLooper;
- synchronized(sListeners) {
- if (!sSensorModuleInitialized) {
- sSensorModuleInitialized =true;
- nativeClassInit();
- sWindowManager =IWindowManager.Stub.asInterface(
- ServiceManager.getService("window"));
- if (sWindowManager != null) {
- // if it's null we'rerunning in the system process
- // which won't get therotated values
- try {
- sRotation =sWindowManager.watchRotation(
- newIRotationWatcher.Stub() {
- public voidonRotationChanged(int rotation) {
- SensorManager.this.onRotationChanged(rotation);
- }
- }
- );
- } catch (RemoteException e){
- }
- }
- // initialize the sensor list
- sensors_module_init();
- final ArrayList<Sensor>fullList = sFullSensorsList;
- int i = 0;
- do {
- Sensor sensor = newSensor();
- i =sensors_module_get_next_sensor(sensor, i);
- if (i>=0) {
- //Log.d(TAG,"found sensor: " + sensor.getName() +
- // ", handle=" + sensor.getHandle());
- sensor.setLegacyType(getLegacySensorType(sensor.getType()));
- fullList.add(sensor);
- sHandleToSensor.append(sensor.getHandle(), sensor);
- }
- } while (i>0);
- sPool = new SensorEventPool(sFullSensorsList.size()*2 );
- sSensorThread = newSensorThread();
- }
- }
- }
先调用nativeClassInit来初始化JNI相关java类信息,对应C++代码:
- static void
- nativeClassInit(JNIEnv *_env, jclass _this)
- {
- jclass sensorClass =_env->FindClass("android/hardware/Sensor");
- SensorOffsets& sensorOffsets =gSensorOffsets;
- sensorOffsets.name = _env->GetFieldID(sensorClass,"mName", "Ljava/lang/String;");
- sensorOffsets.vendor = _env->GetFieldID(sensorClass,"mVendor", "Ljava/lang/String;");
- sensorOffsets.version = _env->GetFieldID(sensorClass,"mVersion", "I");
- sensorOffsets.handle = _env->GetFieldID(sensorClass, "mHandle", "I");
- sensorOffsets.type = _env->GetFieldID(sensorClass,"mType", "I");
- sensorOffsets.range = _env->GetFieldID(sensorClass,"mMaxRange", "F");
- sensorOffsets.resolution = _env->GetFieldID(sensorClass,"mResolution","F");
- sensorOffsets.power = _env->GetFieldID(sensorClass,"mPower", "F");
- sensorOffsets.minDelay = _env->GetFieldID(sensorClass,"mMinDelay", "I");
- }
从代码上看出,这个函数主要是保存java类Sensor的各个filed的id值,方便后续在c++代码中利用Jni环境向jave层传递数据,这个在后续poll sensor值的时候会用到。接着调用jni函数sensors_module_init,c++代码如下:
- static jint
- sensors_module_init(JNIEnv*env, jclass clazz)
- {
- SensorManager::getInstance();
- return 0;
- }
函数很简单,就调用SensorManager::getInstance实例化SensorManager对象实例。注意这里是jnic++层的实现,SensorManager对象是C++层的对象实例,不要跟上面java层的搞浑了。getInstance,可以明显看出来,这是一个单例对象,继续看c++ SensorManager的构造函数:
- SensorManager::SensorManager()
- : mSensorList(0)
- {
- // okay we're not locked here, but it's notneeded during construction
- assertStateLocked();
- }
构造函数就调了assertStateLocked,继续看这个函数:
- status_tSensorManager::assertStateLocked() const {
- if (mSensorServer == NULL) {
- // try for one second
- const String16name("sensorservice"); //定义sensorservie服务端名字匹配
- for (int i=0 ; i<4 ; i++) {
- //打通SensorManager.cpp和SensorService.cpp
- status_t err = getService(name,&mSensorServer);
- if (err == NAME_NOT_FOUND) {
- usleep(250000);
- continue;
- }
- if (err != NO_ERROR) {
- return err;
- }
- break;
- }
- class DeathObserver : publicIBinder::DeathRecipient {
- SensorManager& mSensorManger;
- virtual void binderDied(constwp<IBinder>& who) {
- LOGW("sensorservice died[%p]", who.unsafe_get());
- mSensorManger.sensorManagerDied();
- }
- public:
- DeathObserver(SensorManager&mgr) : mSensorManger(mgr) { }
- };
- mDeathObserver = newDeathObserver(*const_cast<SensorManager *>(this));
- mSensorServer->asBinder()->linkToDeath(mDeathObserver);
- mSensors =mSensorServer->getSensorList();
- size_t count = mSensors.size();
- mSensorList = (Sensorconst**)malloc(count * sizeof(Sensor*));
- for (size_t i=0 ; i<count ; i++) {
- mSensorList[i] = mSensors.array() +i;
- }
- }
- return NO_ERROR;
- }
这个函数通过getService拿到sensorservice的proxy binder,这样就建立了与sensorservice的数据连接,然后调用getsensorlist从sensorservice获取sensor list并保存。
ok,到这里,java层的jni函数sensors_module_init()就走完了,我们已经与sensor service建立连接,并已经取得了sensor list,但是这些数据目前是存于c++层的,我们要通过jni将数据拿到java层,所以在java层SensorManager构造函数调用sensors_module_init()后,调用sensors_module_get_next_sensor获取sensor数据并保存。
下面是jni函数sensors_module_get_next_sensor的c++实现:
- //android_hardware_SensorManager.cpp
- static jint
- sensors_module_get_next_sensor(JNIEnv*env, jobject clazz, jobject sensor, jint next)
- {
- SensorManager&mgr(SensorManager::getInstance());
- Sensor const* const* sensorList;
- size_t count =mgr.getSensorList(&sensorList);
- if (size_t(next) >= count)
- return -1;
- Sensor const* const list =sensorList[next];
- const SensorOffsets&sensorOffsets(gSensorOffsets);
- jstring name =env->NewStringUTF(list->getName().string());
- jstring vendor =env->NewStringUTF(list->getVendor().string());
- env->SetObjectField(sensor,sensorOffsets.name, name);
- env->SetObjectField(sensor,sensorOffsets.vendor, vendor);
- env->SetIntField(sensor,sensorOffsets.version, 1);
- env->SetIntField(sensor,sensorOffsets.handle, list->getHandle());
- env->SetIntField(sensor,sensorOffsets.type, list->getType());
- env->SetFloatField(sensor,sensorOffsets.range, list->getMaxValue());
- env->SetFloatField(sensor,sensorOffsets.resolution, list->getResolution());
- env->SetFloatField(sensor,sensorOffsets.power, list->getPowerUsage());
- env->SetIntField(sensor,sensorOffsets.minDelay, list->getMinDelay());
- next++;
- return size_t(next) < count ? next : 0;
- }
在这个函数将对应的c++层保存的sensor数据传给jobjectsensor。
java层SensorManager构造函数最后创建SensorEventPool和sSensorThread, 这两个对象干嘛用的?看名字就知道啦,一个是事件池,sensor 事件很频繁,如果对每一个事件都创建一个新对象,开销太大,弄一个事件池肯定是最好的选择;另外一个是sensor 线程,负责读取sensor 数据.
3.3 sensor数据读取
继续来看下应用层获取sensor数据的代码:
- public classSensorActivity extends Activity, implements SensorEventListener {
- private final SensorManagermSensorManager;
- private final Sensor mAccelerometer;
- public SensorActivity() {
- //获取对应服务
- mSensorManager =(SensorManager)getSystemService(SENSOR_SERVICE);
- //获取指定sensor对象
- mAccelerometer =mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
- }
- protected void onResume() {
- super.onResume();
- //注册listener用于数据回调
- mSensorManager.registerListener(this,mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
- }
- protected void onPause() {
- super.onPause();
- mSensorManager.unregisterListener(this);
- }
- public void onAccuracyChanged(Sensorsensor, int accuracy) {
- }
- public void onSensorChanged(SensorEventevent) {
- }
- }
现在看这代码就很清楚了
1:(SensorManager)getSystemService(SENSOR_SERVICE)获取SensorManager对象,做了我们上面所介绍的初始化工作
2:mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),获取指定sensor对象,根据初始化获取的Sensor List。
3:mSensorManager.registerListener(this,mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);注册listener获取sensor数据
还记得上一部分说的sensor client与sensor service建立active connection来传递数据吗?service端创建connection是由client端也就是由应用端发起的; 上面1,2都是初始化工作,那真正发起的代码,肯定就是registerlistener了,下面根据代码详细分析:
- public booleanregisterListener(SensorEventListener listener, Sensor sensor, int rate) {
- return registerListener(listener,sensor, rate, null);
- }
直接调用重载函数
- public booleanregisterListener(SensorEventListener listener, Sensor sensor, int rate,
- Handler handler) {
- if (listener == null || sensor == null){
- return false;
- }
- boolean result = true;
- int delay = -1;
- switch (rate) {
- case SENSOR_DELAY_FASTEST:
- delay = 0;
- break;
- case SENSOR_DELAY_GAME:
- delay = 20000;
- break;
- case SENSOR_DELAY_UI:
- delay = 66667;
- break;
- case SENSOR_DELAY_NORMAL:
- delay = 200000;
- break;
- default:
- delay = rate;
- break;
- }
- synchronized (sListeners) {
- // look for this listener in our list
- ListenerDelegate l = null;
- for (ListenerDelegate i :sListeners) {
- if (i.getListener() ==listener) {
- l = i;
- break;
- }
- }
- // if we don't find it, add it tothe list
- if (l == null) {
- l = newListenerDelegate(listener, sensor, handler);
- sListeners.add(l);
- // if the list is not empty,start our main thread
- if (!sListeners.isEmpty()) {
- if(sSensorThread.startLocked()) {
- if(!enableSensorLocked(sensor, delay)) {
- // oops. there was an error
- sListeners.remove(l);
- result = false;
- }
- } else {
- // there was an error,remove the listener
- sListeners.remove(l);
- result = false;
- }
- } else {
- // weird, we couldn't addthe listener
- result = false;
- }
- } else {
- l.addSensor(sensor);
- if (!enableSensorLocked(sensor,delay)) {
- // oops. there was an error
- l.removeSensor(sensor);
- result = false;
- }
- }
- }
- return result;
- }
这个函数使用出现了两个新的变量,分别是sListeners和sSensorThread,对应的类型分别是ListenerDelegate和SensorThread,ListenerDelegate主要是对SensorEventListener做封装,从而使一个listener可以对应多个sensor,SensorThread则负责从sensor service读取sensor数据;该函数先判断lstener对应的ListenerDelegate是否已经创建,如果未创建,新建并将其添加入sListeners,然后查看Sensor Thread是否已经启动,如果没有启动,调用sSensorThread.startLocked()启动线程,接下去调用enableSensorLocked到service端enable对应的sensor.
先来看startlocked:
- boolean startLocked() {
- try {
- if (mThread == null) {
- mSensorsReady = false;
- SensorThreadRunnablerunnable = new SensorThreadRunnable();
- Thread thread = newThread(runnable, SensorThread.class.getName());
- thread.start();
- synchronized (runnable) {
- while (mSensorsReady ==false) {
- runnable.wait();
- }
- }
- mThread = thread;
- }
- } catch (InterruptedException e) {
- }
- return mThread == null ? false :true;
- }
如果线程未创建,创建SensorThreadRunnable,然后初始化线程并启动,线程启动后SensorThreadRunnable.run会被执行:
- private class SensorThreadRunnable implementsRunnable {
- SensorThreadRunnable() {
- }
- private boolean open() {
- // NOTE: this cannotsynchronize on sListeners, since
- // it's held in the main threadat least until we
- // return from here.
- sQueue = sensors_create_queue();
- return true;
- }
- public void run() {
- //Log.d(TAG, "enteringmain sensor thread");
- final float[] values = newfloat[3];
- final int[] status = newint[1];
- final long timestamp[] = newlong[1];
- Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY);
- if (!open()) {
- return;
- }
- synchronized (this) {
- // we've open the driver,we're ready to open the sensors
- mSensorsReady = true;
- this.notify();
- }
- while (true) {
- // wait for an event
- final int sensor =sensors_data_poll(sQueue, values, status, timestamp);
- int accuracy = status[0];
- synchronized (sListeners) {
- if (sensor == -1 ||sListeners.isEmpty()) {
- // we lost theconnection to the event stream. this happens
- // when the lastlistener is removed or if there is an error
- if (sensor == -1&& !sListeners.isEmpty()) {
- // log awarning in case of abnormal termination
- Log.e(TAG,"_sensors_data_poll() failed, we bail out: sensors=" + sensor);
- }
- // we have no morelisteners or polling failed, terminate the thread
- sensors_destroy_queue(sQueue);
- sQueue = 0;
- mThread = null;
- break;
- }
- final SensorsensorObject = sHandleToSensor.get(sensor);
- if (sensorObject !=null) {
- // report thesensor event to all listeners that
- // care about it.
- final int size =sListeners.size();
- for (int i=0 ;i<size ; i++) {
- ListenerDelegate listener = sListeners.get(i);
- if(listener.hasSensor(sensorObject)) {
- // this isasynchronous (okay to call
- // withsListeners lock held).
- listener.onSensorChangedLocked(sensorObject,
- values, timestamp, accuracy);
- }
- }
- }
- }
- }
- //Log.d(TAG, "exiting mainsensor thread");
- }
- }
- }
run执行时,先调用open,open函数很简单,就调用sensors_create_queue()来创建数据队列,显然这个队列是用于sensor数据传输的,sensors_create_queue()是jni函数,接下去看其对应c++部分代码:
- staticjint
- sensors_create_queue(JNIEnv*env, jclass clazz)
- {
- SensorManager&mgr(SensorManager::getInstance());
- sp<SensorEventQueue>queue(mgr.createEventQueue());
- queue->incStrong(clazz);
- returnreinterpret_cast<int>(queue.get());
- }
调用SensorManager.createEventQueue来创建队列:
- sp<SensorEventQueue>SensorManager::createEventQueue()
- {
- sp<SensorEventQueue> queue;
- Mutex::Autolock _l(mLock);
- while(assertStateLocked() == NO_ERROR) {
- sp<ISensorEventConnection>connection =
- mSensorServer->createSensorEventConnection();
- if (connection == NULL) {
- // SensorService just died.
- LOGE("createEventQueue:connection is NULL. SensorService died.");
- continue;
- }
- queue = newSensorEventQueue(connection);
- break;
- }
- return queue;
- }
调用mSensorServer->createSensorEventConnection()与server端建立连接,接着将获取的connection对象创建SensorEventQueue对象并返回。
sensors_create_queue函数接着调用queue.get()获取队列的指针,并返回给java层
回过头来继续看java调用sensors_create_queue的open函数:
- private boolean open() {
- // NOTE: this cannot synchronize onsListeners, since
- // it's held in the main thread at least untilwe
- // return from here.
- //将返回的SensorEventQueue指针保存到sQueue里
- sQueue = sensors_create_queue();
- return true;
- }
将c++创建的SensorEventQueue对象地址保存到java层的一个变量里,这在android里面是很常见也很好用的方法
在open结束后,SensorThreadRunnable.run接下去调用sensors_data_poll来抓去sensor数据
staticnative int sensors_data_poll(int queue, float[] values, int[] status, long[]timestamp);
这个函数的第一个参数就是之前保存的c++层SensorEventQueue对象指针,看对应c++实现:
- staticjint
- sensors_data_poll(JNIEnv*env, jclass clazz, jint nativeQueue,
- jfloatArray values, jintArray status,jlongArray timestamp)
- {
- //强制类型转换
- sp<SensorEventQueue> queue(reinterpret_cast<SensorEventQueue*>(nativeQueue));
- if (queue == 0) return -1;
- status_t res;
- ASensorEventevent;
- //从队列中读取数据
- res = queue->read(&event, 1);
- if (res == -EAGAIN) {
- res = queue->waitForEvent();
- if (res != NO_ERROR)
- return -1;
- res = queue->read(&event, 1);
- }
- if (res < 0)
- return -1;
- jint accuracy = event.vector.status;
- env->SetFloatArrayRegion(values, 0, 3,event.vector.v);
- env->SetIntArrayRegion(status, 0, 1,&accuracy);
- env->SetLongArrayRegion(timestamp, 0, 1,&event.timestamp);
- return event.sensor;
- }
先将java层传过来的对象地址强制类型转换成SensorEventQueue,然后调用 queue->read(&event, 1)读取sensor数据
- ssize_tSensorEventQueue::read(ASensorEvent* events, size_t numEvents)
- {
- ssize_t size =mSensorChannel->read(events, numEvents*sizeof(events[0]));
- LOGE_IF(size<0 && size!=-EAGAIN,
- "SensorChannel::read error(%s)", strerror(-size));
- if (size >= 0) {
- if (size % sizeof(events[0])) {
- // partial read!!! should neverhappen.
- LOGE("SensorEventQueue partialread (event-size=%u, read=%d)",
- sizeof(events[0]),int(size));
- return -EINVAL;
- }
- // returns number of events read
- size /= sizeof(events[0]);
- }
- returnsize;
- }
关于数据的具体传输,上一部分已经详细介绍,这里就不再描述
Jni部分sensors_data_poll在获取到sensor数据并返回到java层,SensorThreadRunnable.run在得到sensor数据后,通过下面代码将数据通过listener回调
- finalint size = sListeners.size();
- for(int i=0 ; i<size ; i++) {
- ListenerDelegate listener = sListeners.get(i);
- if (listener.hasSensor(sensorObject)) {
- // this is asynchronous (okay to call
- // with sListeners lock held).
- istener.onSensorChangedLocked(sensorObject,values,timestamp, accuracy);
- }
- }
就这样,通过registerlistener注册的listener就可以获取到想要的sensor数据,这样就可以了吗?还不行,上面只是说数据流是这么走的,SensorEventQueue::read现在读不到数据的,因为在sensor service那边,sensor还是inactive的,所以registerListener 在sSensorThread.startLocked()成功后,再调用enableSensorLocked来active指定sensor:
- private booleanenableSensorLocked(Sensor sensor, int delay) {
- boolean result = false;
- for (ListenerDelegate i : sListeners) {
- if (i.hasSensor(sensor)) {
- String name = sensor.getName();
- int handle =sensor.getHandle();
- result = sensors_enable_sensor(sQueue,name, handle, delay);
- break;
- }
- }
- return result;
sensors_enable_sensor,又一个jni函数,直接看对应c++函数:
- staticjboolean
- sensors_enable_sensor(JNIEnv*env, jclass clazz,
- jint nativeQueue, jstring name, jintsensor, jint delay)
- {
- sp<SensorEventQueue>queue(reinterpret_cast<SensorEventQueue *>(nativeQueue));
- if (queue == 0) return JNI_FALSE;
- status_t res;
- if(delay >= 0) {
- res = queue->enableSensor(sensor,delay);
- } else {
- res = queue->disableSensor(sensor);
- }
- return res == NO_ERROR ? true : false;
- }
继续看queue->enableSensor
- status_tSensorEventQueue::enableSensor(int32_t handle, int32_t us) const {
- status_t err =mSensorEventConnection->enableDisable(handle, true);
- if (err == NO_ERROR) {
- mSensorEventConnection->setEventRate(handle, us2ns(us));
- }
- return err;
- }
调用mSensorEventConnection->enableDisable(handle,true)将对应的sensor激活。
Ok,激活后,SensorThreadRunnable.run中sensors_data_poll就可以拿到数据,并回调给注册的listener.
Android4.4 Sensor APP--HAL代码流程相关推荐
- android 6.0 高通平台sensor 工作机制及流程(原创)
最近工作上有碰到sensor的相关问题,正好分析下其流程作个笔记. 这个笔记分三个部分: sensor硬件和驱动的工作机制 sensor 上层app如何使用 从驱动到上层app这中间的流程是如何 Se ...
- android 6.0 高通平台sensor 工作机制及流程
最近工作上有碰到sensor的相关问题,正好分析下其流程作个笔记. 这个笔记分三个部分: sensor硬件和驱动的工作机制 sensor 上层app如何使用 从驱动到上层app这中间的流程是如何 Se ...
- Eboot代码流程 [转]
Eboot代码流程 [转] Eboot代码流程 ----by nasiry ...
- Camera camx代码结构、编译、代码流程简介
文章目录 一.camx 代码结构 二.camx 编译 三.camx 代码流程分析 转载链接: https://juejin.cn/post/6870358276425875463 https://ww ...
- 第1课第4.4节_Android硬件访问服务编写HAL代码
android应用如何访问C库 - 落魄影子 - 博客频道 - CSDN.NET http://blog.csdn.net/ab198604/article/details/51249303 And ...
- c++builder启动了怎么停止_App 竟然是这样跑起来的 —— Android App/Activity 启动流程分析...
在我的上一篇文章: AJie:按下电源键后竟然发生了这一幕 -- Android 系统启动流程分析zhuanlan.zhihu.com 我们分析了系统在开机以后的一系列行为,其中最后一阶段 AMS( ...
- Android 系统(78)---《android framework常用api源码分析》之 app应用安装流程
<android framework常用api源码分析>之 app应用安装流程 <android framework常用api源码分析>android生态在中国已经发展非常庞大 ...
- 分析APP的安装流程 API29
先总结一下安装流程,以及比较重要的类 PackageInstallerActivity.java: 在文件管理器里点击apk后就会调用该类,主要用于显示要安装的apk的一些权限信息. InstallA ...
- 移动APP的测试流程及方法
App的测试流程整理 1. APP测试基本流程 1.1流程图 1.2测试周期 测试周期可按项目的开发周期来确定测试时间,一般测试时间为两三周(即15个工作日),根据项目情况以及版本质量可适当缩短或延长 ...
最新文章
- 看微软 Windows 30年发展简史,你用过最早的系统版本是什么?
- PyCairo 中的透明度
- matlab fig生成exe,MATLAB GUI多个m文件和fig如何生成exe文件
- IE6/7兼容问题:巧用label去除submit按钮的黑框线
- 带有MySQL和Bootstrap的Django 3教程和CRUD示例
- 关于SQL Server对于表的一些查询
- vue中怎么清空tab选项卡的缓存_vuejs 内置组件component实现tab切换懒加载和表单输入框内容的清空...
- k8s学习笔记-环境搭建篇
- 树莓派进阶之路 (027) - 在Linux中增加swap空间
- 数据结构实验1-线性表的顺序实现
- 中文近义词工具包,Synonyms 发布新版本 v3.16,支持词汇表扩大至 40w+
- 资源下载源码极致cms精纺资源网的模板整站源码
- php重定向和伪静态,「PHP重定向与伪静态区别」- 海风纷飞Blog
- 乔治城大学计算机专业,美国乔治城大学计算机
- Day02-线性代数-矩阵(DataWhale)
- #python 自动识别视频字幕
- 你应该补钙吗?看这篇就懂了
- [NOIP2018]普及组游记
- 老男孩脱产班Linux运维51期
- 《Java语言程序设计与数据结构》编程练习答案(第四章)(二)
热门文章
- Journey源码分析二:整体启动流程
- Memcached实战之单机部署----单实例/多实例
- FusionCharts图表右键菜单的各种典型示例
- 心情随笔之纸包子假新闻 [2007年7月25日]
- 提交注册信息到数据库中
- excel自动调整列宽_Excel双击鼠标的9种用法
- android 通知写法_Android架构设计MVP模式第(二)篇,如何减少类爆炸
- 计算机基础98均9,第三章 计算机基础 Windows98 (第二讲).ppt
- php点广告送积分,PHP猜一猜奇偶商城积分促销模式
- pdn阻抗测试_信号线的特征阻抗和PDN的阻抗区别