转载请注明出处:http://blog.csdn.net/ximsfei/article/details/51072332

github地址:https://github.com/ximsfei/DynamicDeploymentApk

Android动态部署一:Google原生Split APK浅析

Android动态部署二:APK安装及AndroidManifest.xml解析流程分析

Android动态部署三:怎样从插件apk中启动Activity(一)

Android动态部署四:怎样从插件apk中启动Activity(二)

经过前面几篇文章的分析,我们了解到了Google原生是怎样拆分apk的。而且我们自己能够通过解析manifest文件。通过创建插件ClassLoader,Resources对象来启动插件APK中的Activity,上一篇文章关于资源的问题,有一点遗漏,在插件中开发人员有可能通过例如以下代码获取资源Id

getIdentifier("xxx", "layout", getPackageName());

此时调用getPackageName()方法返回的是宿主apk的包名,所以我们须要在DynamicContextImpl类中重写getPackageName()方法,返回从插件apk的manifest中解析出来的的包名。接下来我们通过分析Service启动流程来看看宿主apk怎样启动Android四大组件之Service。

Service启动流程

startService(new Intent(this, TargetService.class));

在Activity中,非常easy的一行代码,就能够启动TargetService了,下图就是调用这行代码后的时序图:

带着成功启动插件Activity的经验,我们继续通过分析Service启动流程。试图从中找到hook点从而将我们对插件Service的扩展操作,通过相似的重写DynamicInstrumentation类。替换进ActivityThread中。

在时序图中我们发如今调用startService方法后,终于都会调到ContextImpl中的startService。

@Override
public ComponentName startService(Intent service) {
warnIfCallingFromSystemProcess();
return startServiceCommon(service, mUser);
} private ComponentName startServiceCommon(Intent service, UserHandle user) {
try {
validateServiceIntent(service);
service.prepareToLeaveProcess();
ComponentName cn = ActivityManagerNative.getDefault().startService(
mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
getContentResolver()), getOpPackageName(), user.getIdentifier());
if (cn != null) {
if (cn.getPackageName().equals("!")) {
throw new SecurityException(
"Not allowed to start service " + service
+ " without permission " + cn.getClassName());
} else if (cn.getPackageName().equals("!!")) {
throw new SecurityException(
"Unable to start service " + service
+ ": " + cn.getClassName());
}
}
return cn;
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
}

在看了源代码之后我们发现,这种方法的功能,事实上跟Activity启动流程中Instrumentation类中的execStartActivity方法相似,看到这就感觉启动插件Service已经十拿九稳了,我们已经跨出了非常大的一步。我们继续来深入研究Service的启动流程的源代码:

ActiveServices.java

private final String bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg,
boolean whileRestarting) throws TransactionTooLargeException {
if (r.app != null && r.app.thread != null) {
sendServiceArgsLocked(r, execInFg, false);//假设Service已启动,在这里会直接调用Service的onStartCommand方法
return null;
} if (!whileRestarting && r.restartDelay > 0) {
// If waiting for a restart, then do nothing.
return null;
} if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Bringing up " + r + " " + r.intent); // We are now bringing the service up, so no longer in the
// restarting state.
if (mRestartingServices.remove(r)) {
r.resetRestartCounter();
clearRestartingIfNeededLocked(r);
} // Make sure this service is no longer considered delayed, we are starting it now.
if (r.delayed) {
if (DEBUG_DELAYED_STARTS) Slog.v(TAG_SERVICE, "REM FR DELAY LIST (bring up): " + r);
getServiceMap(r.userId).mDelayedStartList.remove(r);
r.delayed = false;
} // Make sure that the user who owns this service is started. If not,
// we don't want to allow it to run.
if (mAm.mStartedUsers.get(r.userId) == null) {
String msg = "Unable to launch app "
+ r.appInfo.packageName + "/"
+ r.appInfo.uid + " for service "
+ r.intent.getIntent() + ": user " + r.userId + " is stopped";
Slog.w(TAG, msg);
bringDownServiceLocked(r);
return msg;
} // Service is now being launched, its package can't be stopped.
try {
AppGlobals.getPackageManager().setPackageStoppedState(
r.packageName, false, r.userId);
} catch (RemoteException e) {
} catch (IllegalArgumentException e) {
Slog.w(TAG, "Failed trying to unstop package "
+ r.packageName + ": " + e);
} final boolean isolated = (r.serviceInfo.flags&ServiceInfo.FLAG_ISOLATED_PROCESS) != 0;
final String procName = r.processName;
ProcessRecord app; if (!isolated) {
app = mAm.getProcessRecordLocked(procName, r.appInfo.uid, false);
if (DEBUG_MU) Slog.v(TAG_MU, "bringUpServiceLocked: appInfo.uid=" + r.appInfo.uid
+ " app=" + app);
if (app != null && app.thread != null) {
try {
app.addPackage(r.appInfo.packageName, r.appInfo.versionCode, mAm.mProcessStats);
realStartServiceLocked(r, app, execInFg); //这里会真正启动Service
return null;
} catch (TransactionTooLargeException e) {
throw e;
} catch (RemoteException e) {
Slog.w(TAG, "Exception when starting service " + r.shortName, e);
} // If a dead object exception was thrown -- fall through to
// restart the application.
}
} else {
// If this service runs in an isolated process, then each time
// we call startProcessLocked() we will get a new isolated
// process, starting another process if we are currently waiting
// for a previous process to come up. To deal with this, we store
// in the service any current isolated process it is running in or
// waiting to have come up.
app = r.isolatedProc;
} // Not running -- get it started, and enqueue this service record
// to be executed when the app comes up.
if (app == null) {
if ((app=mAm.startProcessLocked(procName, r.appInfo, true, intentFlags,
"service", r.name, false, isolated, false)) == null) {
String msg = "Unable to launch app "
+ r.appInfo.packageName + "/"
+ r.appInfo.uid + " for service "
+ r.intent.getIntent() + ": process is bad";
Slog.w(TAG, msg);
bringDownServiceLocked(r);
return msg;
}
if (isolated) {
r.isolatedProc = app;
}
} if (!mPendingServices.contains(r)) {
mPendingServices.add(r);
} if (r.delayedStop) {
// Oh and hey we've already been asked to stop!
r.delayedStop = false;
if (r.startRequested) {
if (DEBUG_DELAYED_STARTS) Slog.v(TAG_SERVICE,
"Applying delayed stop (in bring up): " + r);
stopServiceLocked(r);
}
} return null;
} private final void sendServiceArgsLocked(ServiceRecord r, boolean execInFg,
boolean oomAdjusted) throws TransactionTooLargeException {
... while (r.pendingStarts.size() > 0) {
...
try {
...
r.app.thread.scheduleServiceArgs(r, si.taskRemoved, si.id, flags, si.intent);
} catch (Exception e) {
}
...
}
...
} private final void realStartServiceLocked(ServiceRecord r,
ProcessRecord app, boolean execInFg) throws RemoteException {
if (app.thread == null) {
throw new RemoteException();
}
...
r.app = app;
r.restartTime = r.lastActivity = SystemClock.uptimeMillis(); final boolean newService = app.services.add(r);
bumpServiceExecutingLocked(r, execInFg, "create");
mAm.updateLruProcessLocked(app, false, null);
mAm.updateOomAdjLocked(); boolean created = false;
try {
if (LOG_SERVICE_START_STOP) {
String nameTerm;
int lastPeriod = r.shortName.lastIndexOf('.');
nameTerm = lastPeriod >= 0 ? r.shortName.substring(lastPeriod) : r.shortName;
EventLogTags.writeAmCreateService(
r.userId, System.identityHashCode(r), nameTerm, r.app.uid, r.app.pid);
}
synchronized (r.stats.getBatteryStats()) {
r.stats.startLaunchedLocked();
}
mAm.ensurePackageDexOpt(r.serviceInfo.packageName);
app.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_SERVICE);
app.thread.scheduleCreateService(r, r.serviceInfo,
mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),
app.repProcState);// 相似Activity启动流程中的app.thread.scheduleLaunchActivity方法。在这里会new一个Service。而且调用attach以及onCreate方法
r.postNotification();
created = true;
} catch (DeadObjectException e) {
Slog.w(TAG, "Application dead when creating service " + r);
mAm.appDiedLocked(app);
throw e;
} finally {
if (!created) {
// Keep the executeNesting count accurate.
final boolean inDestroying = mDestroyingServices.contains(r);
serviceDoneExecutingLocked(r, inDestroying, inDestroying); // Cleanup.
if (newService) {
app.services.remove(r);
r.app = null;
} // Retry.
if (!inDestroying) {
scheduleServiceRestartLocked(r, false);
}
}
} requestServiceBindingsLocked(r, execInFg); updateServiceClientActivitiesLocked(app, null, true); // If the service is in the started state, and there are no
// pending arguments, then fake up one so its onStartCommand() will
// be called.
if (r.startRequested && r.callStart && r.pendingStarts.size() == 0) {
r.pendingStarts.add(new ServiceRecord.StartItem(r, false, r.makeNextStartId(),
null, null));
} sendServiceArgsLocked(r, execInFg, true);//第一次启动的Service,会在这里调用onStartCommand方法 if (r.delayed) {
if (DEBUG_DELAYED_STARTS) Slog.v(TAG_SERVICE, "REM FR DELAY LIST (new proc): " + r);
getServiceMap(r.userId).mDelayedStartList.remove(r);
r.delayed = false;
} if (r.delayedStop) {
// Oh and hey we've already been asked to stop!
r.delayedStop = false;
if (r.startRequested) {
if (DEBUG_DELAYED_STARTS) Slog.v(TAG_SERVICE,
"Applying delayed stop (from start): " + r);
stopServiceLocked(r);
}
}
}

ActivityThread.java

//ActivityThread$ApplicationThread
public final void scheduleServiceArgs(IBinder token, boolean taskRemoved, int startId,
int flags ,Intent args) {
...
sendMessage(H.SERVICE_ARGS, s);
} public final void scheduleCreateService(IBinder token,
ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
...
sendMessage(H.CREATE_SERVICE, s);
} //ActivityThread$H
public void handleMessage(Message msg) {
switch (msg.what) {
case CREATE_SERVICE: {
handleCreateService((CreateServiceData)msg.obj);
} break;
case SERVICE_ARGS: {
handleServiceArgs((ServiceArgsData)msg.obj);
} break;
}
} //ActivityThread
private void handleCreateService(CreateServiceData data) {
... LoadedApk packageInfo = getPackageInfoNoCheck(
data.info.applicationInfo, data.compatInfo);
Service service = null;
try {
java.lang.ClassLoader cl = packageInfo.getClassLoader();
service = (Service) cl.loadClass(data.info.name).newInstance();//这里和Activity的启动流程有些许区别
} catch (Exception e) {
if (!mInstrumentation.onException(service, e)) {
throw new RuntimeException(
"Unable to instantiate service " + data.info.name
+ ": " + e.toString(), e);
}
} try {
if (localLOGV) Slog.v(TAG, "Creating service " + data.info.name); ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
context.setOuterContext(service); Application app = packageInfo.makeApplication(false, mInstrumentation);
service.attach(context, this, data.info.name, data.token, app,
ActivityManagerNative.getDefault());
service.onCreate();//这里直接调用了onCreate,而没有相似的先调用callActivityOnCreate方法
mServices.put(data.token, service);
try {
ActivityManagerNative.getDefault().serviceDoneExecuting(
data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
} catch (RemoteException e) {
// nothing to do.
}
} catch (Exception e) {
if (!mInstrumentation.onException(service, e)) {
throw new RuntimeException(
"Unable to create service " + data.info.name
+ ": " + e.toString(), e);
}
}
} private void handleServiceArgs(ServiceArgsData data) {
Service s = mServices.get(data.token);
if (s != null) {
try {
if (data.args != null) {
data.args.setExtrasClassLoader(s.getClassLoader());
data.args.prepareToEnterProcess();
}
int res;
if (!data.taskRemoved) {
res = s.onStartCommand(data.args, data.flags, data.startId);
} else {
s.onTaskRemoved(data.args);
res = Service.START_TASK_REMOVED_COMPLETE;
} QueuedWork.waitToFinish(); try {
ActivityManagerNative.getDefault().serviceDoneExecuting(
data.token, SERVICE_DONE_EXECUTING_START, data.startId, res);
} catch (RemoteException e) {
// nothing to do.
}
ensureJitEnabled();
} catch (Exception e) {
if (!mInstrumentation.onException(s, e)) {
throw new RuntimeException(
"Unable to start service " + s
+ " with " + data.args + ": " + e.toString(), e);
}
}
}
}

看到这里。原生Service也启动起来了,我们发现Service的启动流程和Activity的相似。但又不全然一样,正是由于这些许区别。让我们的开发工作陷入了困境,遇到了以下这些“坑”:

1. Service不像Activity的标准模式,能够一直实例化,当某个Service启动后,从上面的代码能够看到,再次调用startService方法。源代码中并不会去又一次创建Service。调用onCreate,而是直接调用onStartCommand方法,所以我们不能通过StubService的方式来启动插件Service。

2. Service的handleCreateService方法不像在Activity启动流程中的performLaunchActivity方法中获取类名后,通过Instrumentation类的newActivity方法实例化,通过callActivityOnCreate方法间接调用Activity的onCreate,这样我们有机会通过重写DynamicInstrumentation类来扩展插件功能。而Service却直接在ActivityThread类中实例化。而且在attach方法结束后直接调用onCreate方法。

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
Activity a = performLaunchActivity(r, customIntent);
...
} private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
...
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
} catch (Exception e) {
...
} try {
...
if (activity != null) {
...
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor);
...
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
...
}
} catch (Exception e) {
...
}
return activity;
}

上面那两个坑,恰恰是我们在实现从插件apk中启动Activity时所关注的点。似乎这些点在这里一个都用不上,前面跨出的一大步,感觉也是被打了回去,在接下来的几天里。我重复阅读源代码,发现了当中的一个关键点。插件中的Service是使用插件的ClassLoader通过类名来载入的。那我们能够在ClassLoader上做一些“手脚”。

private void handleCreateService(CreateServiceData data) {
...
LoadedApk packageInfo = getPackageInfoNoCheck(
data.info.applicationInfo, data.compatInfo);
Service service = null;
try {
java.lang.ClassLoader cl = packageInfo.getClassLoader();
service = (Service) cl.loadClass(data.info.name).newInstance();
} catch (Exception e) {
...
}
...
} public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
CompatibilityInfo compatInfo) {
return getPackageInfo(ai, compatInfo, null, false, true, false);
} private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
...
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);//宿主apk的LoadApk保存在mPackages中
} else {
ref = mResourcePackages.get(aInfo.packageName);
} LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
: "Loading resource-only package ") + aInfo.packageName
+ " (in " + (mBoundApplication != null
? mBoundApplication.processName : null)
+ ")");
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage); if (mSystemThread && "android".equals(aInfo.packageName)) {
packageInfo.installSystemApplicationInfo(aInfo,
getSystemContext().mPackageInfo.getClassLoader());
} if (differentUser) {
// Caching not supported across users
} else if (includeCode) {
mPackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
} else {
mResourcePackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
}
}
return packageInfo;
}
}

看到这个点之后,感觉像是抓住一个救命稻草那样兴奋,我们能够在每次安装一个插件apk的同一时候将插件apk的ClassLoader安装到mPackages.get(“host package name”)中。这样就能够不做其它的改动。依据插件apk中的Service类名就能够载入TargetService了,同一时候细心的读者也会发现,事实上Activity,BroadcastReceiver,ContentProvider也是通过这种方式载入的。真是一劳永逸呢。

DynamicClassLoaderWrapper.java

package com.ximsfei.dynamic.app;

import java.util.ArrayList;

/**
* Created by pengfenx on 3/15/2016.
*/
public class DynamicClassLoaderWrapper extends ClassLoader {
private final ClassLoader mBase;
private final ArrayList<ClassLoader> mDynamicLoaders = new ArrayList<>(); protected DynamicClassLoaderWrapper(ClassLoader base) {
super();
mBase = base;
} public void addClassLoader(ClassLoader cl) {
if (!mDynamicLoaders.contains(cl)) {
mDynamicLoaders.add(cl);
}
} @Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
try {
return mBase.loadClass(className);
} catch (ClassNotFoundException e) {
} int N = mDynamicLoaders.size();
for (int i=0; i<N; i++) {
try {
return mDynamicLoaders.get(i).loadClass(className);
} catch (ClassNotFoundException e) {
}//这里用try catch来推断该插件中是否存在TargetService不是非常好
}
throw new ClassNotFoundException(className);
} }

安装ClassLoader:

public synchronized void installClassLoader(ClassLoader classLoader) {
Object loadedApk = ((WeakReference) getPackages().get(getHostPackageName())).get();
try {
ClassLoader cl = Reflect.create(loadedApk.getClass())
.setMethod("getClassLoader").invoke(loadedApk);
if (!(cl instanceof DynamicClassLoaderWrapper)) {
DynamicClassLoaderWrapper dclw = new DynamicClassLoaderWrapper(cl);
dclw.addClassLoader(classLoader);
Reflect.create(loadedApk.getClass()).setField("mClassLoader")
.set(loadedApk, dclw);
} else {
((DynamicClassLoaderWrapper) cl).addClassLoader(classLoader);
}
} catch (Exception e) {
}
} private synchronized Map getPackages() {
if (mPackages == null) {
try {
mPackages = mActivityThreadReflect.setField("mPackages").get(currentActivityThread());
} catch (Exception e) {
}
}
return mPackages;
}

stopService, bindService, unbindService的流程与startService流程相似,而且不须要做过多的改动,在这里就不再分析了,有兴趣的读者能够自己去看一下源代码,分析一下。

遗留问题

第一个坑中,Service仅仅能启动一次,所以我们不能通过伪装成StubService的方式。来“骗过”AndroidManifest的检測,我也没有想到更好的方法来实现,临时仅仅能将要使用的Service类名注冊到宿主apk的AndroidManifest中来实现,假设读者有什么好的方法,能够分享出来一起学习一下。

Android动态部署五:怎样从插件apk中启动Service的更多相关文章

  1. Android插件化开发---执行未安装apk中的Service

    欢迎各位增加我的Android开发群[257053751​] 假设你还不知道什么叫插件化开发.那么你应该先读一读之前写的这篇博客:Android插件化开发,初入殿堂 上一篇博客主要从总体角度分析了一下 ...

  2. Android插件化(三)载入插件apk中的Resource资源

    Android载入插件apk中的Resource资源 简单介绍 怎样载入未安装apk中的资源文件呢?我们从android.content.res.AssetManager.java的源代码中发现,它有 ...

  3. (转)从android一个apk中启动第三方apk应用

    从android一个apk中启动第三方apk应用 我们在开发中,经常遇到遇到在一个apk中要去运行另外一个apk,就像我们windows一样,搞一个快捷方式一样,那怎么实现呢? 问题的核心点在于我们要 ...

  4. Android动态换肤(二、apk免安装插件方式)

    在上一篇文章Android动态换肤(一.应用内置多套皮肤)中,我们了解到,动态换肤无非就是调用view的setBackgroundResource(R.drawable.id)等方法设置控件的背景或者 ...

  5. Android动态加载技术(插件化技术)

    No1: 插件化技术的好处: 1)减轻应用的内存和CPU占用 2)实现热插拔,即在不发布新版本的情况下更新某些模块 No2: 插件化方案必须要解决三个基础性问题:资源访问.Activity生命周期的管 ...

  6. Android开发之AIDL的使用一--跨应用启动Service

    启动其他App的服务,跨进程启动服务. 与启动本应用的Service一样,使用startService(intent)方法 不同的是intent需要携带的内容不同,需要使用intent的setComp ...

  7. Android 使用动态载入框架DL进行插件化开发

    如有转载,请声明出处: 时之沙: http://blog.csdn.net/t12x3456    (来自时之沙的csdn博客) 概述: 随着应用的不断迭代.应用的体积不断增大,项目越来越臃肿,冗余添 ...

  8. Android动态方式破解apk进阶篇(IDA调试so源码)

    一.前言 今天我们继续来看破解apk的相关知识,在前一篇:Eclipse动态调试smali源码破解apk 我们今天主要来看如何使用IDA来调试Android中的native源码,因为现在一些app,为 ...

  9. Android动态方式破解apk终极篇(加固apk破解方式)

    一.前言 今天总算迎来了破解系列的最后一篇文章了,之前的两篇文章分别为: 第一篇:如何使用Eclipse动态调试smali源码 第二篇:如何使用IDA动态调试SO文件 现在要说的就是最后一篇了,如何应 ...

随机推荐

  1. 语法错误1:TabError: Inconsistent use of tabs and spaces in indentation

    如图错误: 出错原因: 由于写代码过程用的tab缩进 解决方法: 把tab缩进改用空格缩进

  2. html body中的标签2

    一.列表标签 列表标签分为三种. 1.无序列表<ul>,无序列表中的每一项是<li> 英文单词解释如下: ul:unordered list,“无序列表”的意思. li:lis ...

  3. lnmp 安装FTP服务 并配置FTP用户

    lnmp 默认是不带FTP服务的,需要的童鞋要自行安装.步骤也很简单 一,进入lnmp目录,找到pureftpd.sh 二,直接运行该脚本 ./pureftpd.sh 按任意键开始安装,等待,安装成功 ...

  4. css3动画之1--animation小例子

    1.首先看效果 2.代码及分析 <style type="text/css"> #div1 { margin:100px; position: absolute; te ...

  5. Android几种常见的多渠道(批量)打包方式介绍

    多渠道打包,主要是为了统计不同的渠道上包的下载数量,渠道越多,我们需要打的包数量越多,这个时候,我们没法去使用单纯的手动打包去一个一个的生成不同的渠道包,我们需要更高效的打包方式. 声明渠道方式一: ...

  6. java如何设置文件的权限

    import java.io.File; 2.import java.io.IOException; 3./× 4.×只能给当前用户赋予对该文件的权限,调用createNewFile()方法默认的权限 ...

  7. oracle 提示登录密码过期解决

    1.登录到oracle的 服务器 2.切换到oracle 用户 3.设置到当前操作的实例名:export ORACLE_SID=XXX 4.连接数据库的命令行模式:sqlplus /nolog 5.s ...

  8. SLAM: 关于Orb_SLAM的使用小综述

    0.ORB_SLAM的官方网站:http://webdiis.unizar.es/~raulmur/orbslam/ 1. 参考知乎上对 orb-slam 的评价:orb-slam在众多SLAM方法中 ...

  9. SLAM: 图像角点检测的Fast算法(时间阈值实验)

    作为角点检测的一种快速方法,FastCornerDetect算法比Harris方法.SIft方法都要快一些,应用于实时性要求较高的场合,可以直接应用于SLAM的随机匹配过程.算法来源于2006年的Ed ...

  10. spring实现helloWord

    第一步:添加架包 第二步:写一个简单的实列 package com.java.test; /** * @author nidegui * @create 2019-06-22 10:58 */ pub ...