LeakCanary不用多说,学过Android的同学都知道它。LeakCanary2.x相对于LeakCanary1.x有两个比较明显的改变:(1)源码全部使用了Kotlin进行编写(2)不需要再Application中进行install操作。今天我就从LeakCanary的启动以及各个对象的回收监听两个方面来解析这个框架

一、LeakCanary的启动

上面我们说了,2.x的版本,我们不用在Application中调用install方法了,那LeakCanary是如何启动的呢?

首先,我们先找到AppWatcherInstaller.kt这个文件,代码如下:

internal sealed class AppWatcherInstaller : ContentProvider() {internal class LeakCanaryProcess : AppWatcherInstaller()override fun onCreate(): Boolean {val application = context!!.applicationContext as ApplicationAppWatcher.manualInstall(application)return true}....
}

我们发现,AppWatcherInstaller这个类继承自ContentProvider。了解ContentProvider的同学都知道,ContentProvider注册即执行。而AppWatcherInstaller的注册就是在leakcanary-object-watcher-android文件夹的AndroidManifest.xml中,我们找到如下的代码:

    <application><providerandroid:name="leakcanary.internal.AppWatcherInstaller$MainProcess"android:authorities="${applicationId}.leakcanary-installer"android:enabled="@bool/leak_canary_watcher_auto_install"android:exported="false" /></application>

当执行打包指令时,所有的AndroidManifest.xml会合并到一起。所以我们不需要再Application中调用install方法了

二、各个对象的监听

目前,LeakCanary默认可以监听Activity,Fragment,ViewModel,Service。接下来我们会一一介绍

我们先回到AppWatcherInstaller的onCreate方法里,这里调用了AppWatcher的manualInstall方法,我们看一下:

  @JvmOverloadsfun manualInstall(application: Application,retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)) {checkMainThread()......LeakCanaryDelegate.loadLeakCanary(application)watchersToInstall.forEach {it.install()}}

第7行:主线程的校验,没什么说的

第9行:我们先点击loadLeakCanary,进入看看

  val loadLeakCanary by lazy {try {val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")leakCanaryListener.getDeclaredField("INSTANCE").get(null) as (Application) -> Unit} catch (ignored: Throwable) {NoLeakCanary}}

首先进行了反射,拿到了InternalLeakCanary对象,然后调用了get方法,最后强转为一个lamda表达式。

我们先回到AppWatcher的manualInstall方法中,我们现在只需要记住,执行了LeakCanaryDelegate.loadLeakCanary,就执行了InternalLeakCanary的invoke方法就可以了,引用另一篇博客的解释:

我们创建InternalAppWatcher时init方法就会调用,onAppWatcherInstalled是kotlin中invoke约定的应用可以将invoke函数的lambda表达式赋值给一个变量,
Kotlin的约定有很多种,而比如使用便捷的get操作,以及重载运算符等等,invoke约定也仅仅是一种约定而已;
我们可以把lambda表达式或者函数直接保存在一个变量中,然后就像执行函数一样直接执行这个变量,
这样的变量通常声明的时候都被我们赋值了已经直接定义好的lambda,或者通过成员引用而获取到的函数;
但是别忘了,在面向对象编程中,一个对象在通常情况下都有自己对应的类,那我们能不能定义一个类,
然后通过构造方法来产生一个对象,然后直接执行它呢?这正是invoke约定发挥作用的地方。
我们只需要在一个类中使用operator来修饰invoke函数,这样的类的对象就可以直接像一个保存lambda表达式的变量一样直接调用,而调用后执行的函数就是invoke函数。
我们还有另一种方式来实现可调用的对象,即让类继承自函数类型,然后重写invoke方法。
————————————————
版权声明:本文为CSDN博主「Darksiderl」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u011148116/article/details/106762665/

第5行:这里执行了appDefaultWatchers方法,然后将返回值赋值给了watchersToInstall,在第11行中,对这个变量进行了遍历,执行了每个元素的install方法。我们看一下appDefaultWatchers方法:

  fun appDefaultWatchers(application: Application,reachabilityWatcher: ReachabilityWatcher = objectWatcher): List<InstallableWatcher> {return listOf(ActivityWatcher(application, reachabilityWatcher),FragmentAndViewModelWatcher(application, reachabilityWatcher),RootViewWatcher(reachabilityWatcher),ServiceWatcher(reachabilityWatcher))}

在这里,我们可以看见了很多眼熟的单词:Activity,Fragment,ViewModel,RootView,Service。这些就是LeakCanary默认监听的对象,我们一个一个来介绍:

1.Activity的监听过程

我们先看一下ActivityWatcher

class ActivityWatcher(private val application: Application,private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {private val lifecycleCallbacks =object : Application.ActivityLifecycleCallbacks by noOpDelegate() {override fun onActivityDestroyed(activity: Activity) {reachabilityWatcher.expectWeaklyReachable(activity, "${activity::class.java.name} received Activity#onDestroy() callback")}}override fun install() {application.registerActivityLifecycleCallbacks(lifecycleCallbacks)}override fun uninstall() {application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)}
}

lifecycleCallbacks就是Application.ActivityLifecycleCallbacks,主要用来进行进行Activity生命周期监听的。install和uninstall方法分别进行了注册和解注册。

在lifecycleCallbacks创建过程中,只实现了onActivityDestroyed方法,当Activity销毁的时候,会回调到这里。然后执行了ReachabilityWatcher对象的expectWeaklyReachable方法,这个方法的具体实现是在ObjectWatcher中,我们看一下:

  @Synchronized override fun expectWeaklyReachable(watchedObject: Any,description: String) {if (!isEnabled()) {return}removeWeaklyReachableObjects()val key = UUID.randomUUID().toString()val watchUptimeMillis = clock.uptimeMillis()val reference =KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)watchedObjects[key] = referencecheckRetainedExecutor.execute {moveToRetained(key)}}

第8行:我们先看一下这个方法

  private fun removeWeaklyReachableObjects() {var ref: KeyedWeakReference?do {ref = queue.poll() as KeyedWeakReference?if (ref != null) {watchedObjects.remove(ref.key)}} while (ref != null)}

所表达的意思是:从引用队里中取数据,如果有数据,表示这个数据被回收了,那么从被观察的集合中移除,此时ref不为空,所以还会循环;如果ref等于空,表示没有对象回收,则直接跳出循环。watchedObjects是Map,里面的数据就是被观察的对象。

在回到ReachabilityWatcher的expectWeaklyReachable方法:

第9~13行:根据uuid创建了key,拿到时间,然后创建一个弱引用对象

第15行:以uuid创建的key为map的键,弱引用对象为值,进行存储

第17行:在子线程中,执行了moveToRetained方法,我们进去看看:

  @Synchronized private fun moveToRetained(key: String) {removeWeaklyReachableObjects()val retainedRef = watchedObjects[key]if (retainedRef != null) {retainedRef.retainedUptimeMillis = clock.uptimeMillis()onObjectRetainedListeners.forEach { it.onObjectRetained() }}}

第2行:这里有再次执行了removeWeaklyReachableObjects方法,重新对阻塞队列里的值进行确认,看看有没有被回收的对象

第3~7行:根据key拿到相应的弱引用,然后遍历onObjectRetainedListeners集合,执行每个元素的onObjectRetained方法。

这里可能有些同学不知道这里集合的数据是什么时候添加的,上面我们说过InternalLeakCanary的invoke方法,这个方法里有如下一句代码:

AppWatcher.objectWatcher.addOnObjectRetainedListener(this)

其实就是在invoke方法中添加的,而集合中的元素也就是InternalLeakCanary,所以执行的onObjectRetained方法也是在这个类中,我们查看这个方法:

  override fun onObjectRetained() = scheduleRetainedObjectCheck()fun scheduleRetainedObjectCheck() {if (this::heapDumpTrigger.isInitialized) {heapDumpTrigger.scheduleRetainedObjectCheck()}}

heapDumpTrigger是HeapDumpTrigger类,我们查看它的scheduleRetainedObjectCheck方法

  fun scheduleRetainedObjectCheck(delayMillis: Long = 0L) {val checkCurrentlyScheduledAt = checkScheduledAtif (checkCurrentlyScheduledAt > 0) {return}checkScheduledAt = SystemClock.uptimeMillis() + delayMillisbackgroundHandler.postDelayed({checkScheduledAt = 0checkRetainedObjects()}, delayMillis)}

最关键的是第11行代码,我们查看这个方法

private fun checkRetainedObjects() {....var retainedReferenceCount = objectWatcher.retainedObjectCountif (retainedReferenceCount > 0) {gcTrigger.runGc()retainedReferenceCount = objectWatcher.retainedObjectCount}if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) returnval now = SystemClock.uptimeMillis()val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillisif (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {onRetainInstanceListener.onEvent(DumpHappenedRecently)showRetainedCountNotification(objectCount = retainedReferenceCount,contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait))scheduleRetainedObjectCheck(delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis)return}dismissRetainedCountNotification()val visibility = if (applicationVisible) "visible" else "not visible"dumpHeap(retainedReferenceCount = retainedReferenceCount,retry = true,reason = "$retainedReferenceCount retained objects, app is $visibility")}

代码篇幅不少,我就挑重点介绍一下:

第4~9行:根据被观察的数量,主动进行一次GC

第11行:这个方法其实很重要,但是我们就不进入看了,总结一下主要逻辑就可以了:判断被观察者的数量是否大于0,并且小于5,如果符合这个条件,则弹出提示通知,并且这个方法返回true。如果返回true的话,下面的代码就不执行了。

第13~15行:判断上次弹出通知的时间和现在的差值,如果时间差小于60s,则弹出提示通知,并且下面的内容不再执行

第27~29行:如果上述的条件都没有满足,那么将提示通知关闭,直接进行结果分析。

Activity的监听过程,还是比较简单的

2.Fragment和ViewModel

在AppWatcher的代码中(上面说过),我们知道Fragment和ViewModel的对象监听是在FragmentAndViewModelWatcher.kt这个类中,我们看一下:

class FragmentAndViewModelWatcher(private val application: Application,private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {private val fragmentDestroyWatchers: List<(Activity) -> Unit> = run {val fragmentDestroyWatchers = mutableListOf<(Activity) -> Unit>()if (SDK_INT >= O) {fragmentDestroyWatchers.add(AndroidOFragmentDestroyWatcher(reachabilityWatcher))}getWatcherIfAvailable(ANDROIDX_FRAGMENT_CLASS_NAME,ANDROIDX_FRAGMENT_DESTROY_WATCHER_CLASS_NAME,reachabilityWatcher)?.let {fragmentDestroyWatchers.add(it)}getWatcherIfAvailable(ANDROID_SUPPORT_FRAGMENT_CLASS_NAME,ANDROID_SUPPORT_FRAGMENT_DESTROY_WATCHER_CLASS_NAME,reachabilityWatcher)?.let {fragmentDestroyWatchers.add(it)}fragmentDestroyWatchers}private val lifecycleCallbacks =object : Application.ActivityLifecycleCallbacks by noOpDelegate() {override fun onActivityCreated(activity: Activity,savedInstanceState: Bundle?) {for (watcher in fragmentDestroyWatchers) {watcher(activity)}}}.....
}

有一些非关键的方法我没贴出来,比如说install,uninstall方法,进行注册和解注册,和ActivityWatcher中的代码一样;getWatcherIfAvailable和classAvailable方法,主要进行了反射相关的操作;

第33行:我们先看一下lifecycleCallbacks,和ActivityWatcher中有些不一样,ActivityWatcher中的lifecycleCallbacks实现的是onActivityDestroyed方法,而这里实现的是onActivityCreated方法。其实很好理解:ActivityWatcher监听的是Activity对象,并且LifecycleCallback监听的也是Activity的生命周期,当Activity销毁的时候,需要判断Activity对象是否释放了;而FragmentAndViewModeWatcher中监听的是Fragment和ViewModel,它们的宿主是Activity,LifecycleCallback监听的也是它们的宿主,所以只有当宿主创建了,才开始Fragment和ViewModel的监听。而Fragment和ViewModel销毁的时候,对象有没有释放的判断,并不是在这里

第39行:我们看到循环了fragmentDestryWatchers,这个列表中的元素是对不同类型的Fragment的回收监听,Fragment有三种类型

(1)非AndroidX中的Fragment,如果SDK的版本大于等于26,那么对Fragment生命周期的监听是Activity的fragmentManager.registerFragmentLifecycleCallbacks方法

(2)非AndroidX中的Fragment,如果SDK的版本小于26,那么对Fragment生命周期的监听是FragmentActivity的supportFragmentManager.registerFragmentLifecycleCallbacks方法

(3)如果是AndroidX的Fragment,那么对Fragment生命周期的监听是AndroidX包下FragmentActivity的supportFragmentManager.registerFragmentLifecycleCallbacks方法

而这三个Fragment的监听者分别是:AndroidOFragmentDestroyWatcher.kt,AndroidXFragmentDestoryWatcher.kt,AndroidSupportFragmentDestroyWatcher.kt。其实这三个关于Fragment的监听没什么区别,只是AndroidXFragmentDestoryWatcher.kt中多了对ViewModel对象的监听,我们主要讲一下AndroidXFragmentDestoryWatcher,其他两个就略过了

internal class AndroidXFragmentDestroyWatcher(private val reachabilityWatcher: ReachabilityWatcher
) : (Activity) -> Unit {private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {override fun onFragmentCreated(fm: FragmentManager,fragment: Fragment,savedInstanceState: Bundle?) {ViewModelClearedWatcher.install(fragment, reachabilityWatcher)}override fun onFragmentViewDestroyed(fm: FragmentManager,fragment: Fragment) {val view = fragment.viewif (view != null) {reachabilityWatcher.expectWeaklyReachable(view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +"(references to its views should be cleared to prevent leaks)")}}override fun onFragmentDestroyed(fm: FragmentManager,fragment: Fragment) {reachabilityWatcher.expectWeaklyReachable(fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback")}}override fun invoke(activity: Activity) {if (activity is FragmentActivity) {val supportFragmentManager = activity.supportFragmentManagersupportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)ViewModelClearedWatcher.install(activity, reachabilityWatcher)}}
}

第5行:fragmentLifecycleCallbacks就是Fragment的生命周期回调,

onFragmentDestroyed是对Fragment对象的回收监听,当Fragment销毁的时候,会调用reachabilityWatcher的expectWeaklyReachable方法,和ActivityWatcher中调用的方法一样,具体的逻辑我就不说了,ActivityWatcher的时候已经说过了

onFragmentViewDestroyed是对Fragment中RootView对象的回收监听,当RootView销毁的时候,同样会执行reachabilityWatcher的expectWeaklyReachable方法。这个回调方法在其他两个FragmentWatcher中也是有的。

onFragmentCreated是对ViewModel对象的回收监听,ViewModelClearedWatcher是具体的监听者,我们发现在第12行和第42行都调用了ViewModelClearedWatcher的install方法,但是第一个参数不一样,这表示ViewModel的宿主不一样。一个是Activity,一个是Fragment。

我们看一下ViewModelClearedWatcher的代码:

internal class ViewModelClearedWatcher(storeOwner: ViewModelStoreOwner,private val reachabilityWatcher: ReachabilityWatcher
) : ViewModel() {private val viewModelMap: Map<String, ViewModel>?init {// We could call ViewModelStore#keys with a package spy in androidx.lifecycle instead,// however that was added in 2.1.0 and we support AndroidX first stable release. viewmodel-2.0.0// does not have ViewModelStore#keys. All versions currently have the mMap field.viewModelMap = try {val mMapField = ViewModelStore::class.java.getDeclaredField("mMap")mMapField.isAccessible = true@Suppress("UNCHECKED_CAST")mMapField[storeOwner.viewModelStore] as Map<String, ViewModel>} catch (ignored: Exception) {null}}override fun onCleared() {viewModelMap?.values?.forEach { viewModel ->reachabilityWatcher.expectWeaklyReachable(viewModel, "${viewModel::class.java.name} received ViewModel#onCleared() callback")}}companion object {fun install(storeOwner: ViewModelStoreOwner,reachabilityWatcher: ReachabilityWatcher) {val provider = ViewModelProvider(storeOwner, object : Factory {@Suppress("UNCHECKED_CAST")override fun <T : ViewModel?> create(modelClass: Class<T>): T =ViewModelClearedWatcher(storeOwner, reachabilityWatcher) as T})provider.get(ViewModelClearedWatcher::class.java)}}
}

有一个比较关键的点:我们发现ViewModelClearedWatcher继承子ViewModel,也就是说它本身是一个ViewModel。并且实现了onCleared方法,这个方法在ViewModel销毁的时候会执行,和Activity的onDestroy作用一样。

第31行:这里是install方法的具体内容。如果使用过ViewModel的话,这里很容易理解,就是ViewModel和宿主的绑定。而ViewModel就是ViewModelClearedWatcher。

第8~20行:当ViewModelClearedWatcher创建的时候,通过反射拿到宿主的mMap,得到宿主中所有的ViewModel。

第22行:当ViewModelClearedWatcher被销毁时,会执行到onCleared方法,这时候会遍历宿主中所有的ViewModel,执行reachabilityWatcher的expectWeaklyReachable方法,判断ViewModel是否都已经释放了。

这就是Fragment和ViewModel的对象监听过程

3.Service

Android中Service,没有类似Activity和Fragment的生命周期回调监听,所以LeakCanary采用的是Hook的方式,而这里需要我们对Android的源码有一定的了解。首先是ActivityThread,这里面有一个内部类H,继承自Handler,如下:

class H extends Handler {.....
}

很多系统指令都会传递到这个类中,其中有一个指令是STOP_SERVICE,也就是说当我们执行stopService时,类H会接收到STOP_SERVICE指令进行Service关闭,如下:

case STOP_SERVICE:Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceStop");handleStopService((IBinder)msg.obj);schedulePurgeIdler();Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);break;

查看handleStopService方法:

    private void handleStopService(IBinder token) {Service s = mServices.remove(token);if (s != null) {try {if (localLOGV) Slog.v(TAG, "Destroying service " + s);s.onDestroy();s.detachAndCleanUp();Context context = s.getBaseContext();if (context instanceof ContextImpl) {final String who = s.getClassName();((ContextImpl) context).scheduleFinalCleanup(who, "Service");}QueuedWork.waitToFinish();try {ActivityManager.getService().serviceDoneExecuting(token, SERVICE_DONE_EXECUTING_STOP, 0, 0);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}} ......}......}

我们可以看到第二行,拿到了Service对象,那么我们拿到这个Service对象,不就可以了吗?

其实现在还不行!因为下面还有Service方法的调用,需要调用第6行到第15行一系列的代码,才能真正的将Service关闭

所以LeakCanary做了两步:第一步:监听STOP_SERVICE指令,拿到被监听的Service;第二步:通过动态代理,获取第17行的serviceDoneExecuting,在此方法执行之前,进行回收的判断。

4.RootView

我们要注意一点,这里是RootView,而不是所有的View,LeakCanary默认只会监听所有的根View。代码我就不贴了,我主要介绍一下思路,个人觉得都是大同小异

  • 销毁的监听:这是做对象监听的必要条件,只有能监听到对象销毁时的监听,才能判断这个对象是否真正的释放了,而View的销毁监听是OnAttachStateChangeListener,这个监听有两个回调方法,当View和Window绑定和取消绑定的时候会执行
  • 对象的获取:如果了解UI绘制的话,就知道所有的RootView都存储在WindowManagerGlobal的mView中,mView是一个集合(SDK小于16的话是在WindowManagerImpl中)

鸣谢:

Android LeakCanary检测内存泄露原理_Android_软件编程 - 编程客栈

Android内存泄漏-LeakCanary源码原理分析_Darksiderl的博客-CSDN博客

Android---LeakCanary源码分析相关推荐

  1. android leakcanary 源码分析,LeakCanary源码浅析

    在Android开发中最让人们头疼的就是内存泄漏了,今天来介绍一个查看内存是否泄漏的工具LeakCanary,并通过研究源码明白它是如何分析和查找存在泄漏信息的 在Android开发中最让人们头疼的就 ...

  2. Android HandlerThread 源码分析

    HandlerThread 简介: 我们知道Thread线程是一次性消费品,当Thread线程执行完一个耗时的任务之后,线程就会被自动销毁了.如果此时我们又有一 个耗时任务需要执行,我们不得不重新创建 ...

  3. Android ADB 源码分析(三)

    前言 之前分析的两篇文章 Android Adb 源码分析(一) 嵌入式Linux:Android root破解原理(二) 写完之后,都没有写到相关的实现代码,这篇文章写下ADB的通信流程的一些细节 ...

  4. 【Android SDM660源码分析】- 02 - UEFI XBL QcomChargerApp充电流程代码分析

    [Android SDM660源码分析]- 02 - UEFI XBL QcomChargerApp充电流程代码分析 一.加载 UEFI 默认应用程序 1.1 LaunchDefaultBDSApps ...

  5. 【Android SDM660源码分析】- 03 - UEFI XBL GraphicsOutput BMP图片显示流程

    [Android SDM660源码分析]- 03 - UEFI XBL GraphicsOutput BMP图片显示流程 1. GraphicsOutput.h 2. 显示驱动初化 DisplayDx ...

  6. 【Android SDM660源码分析】- 01 - 如何创建 UEFI XBL Protocol DXE_DRIVER 驱动及UEFI_APPLICATION 应用程序

    [Android SDM660源码分析]- 01 - 如何创建 UEFI XBL Protocol DXE_DRIVER 驱动及UEFI_APPLICATION 应用程序 一.创建DXE_DRIVER ...

  7. 【Android SDM660源码分析】- 04 - UEFI ABL LinuxLoader 代码分析

    [Android SDM660源码分析]- 04 - UEFI ABL LinuxLoader 代码分析 1. LinuxLoader.c 系列文章: <[Android SDM660开机流程] ...

  8. Android 音频源码分析——AndroidRecord录音(一)

    Android 音频源码分析--AndroidRecord录音(一) Android 音频源码分析--AndroidRecord录音(二) Android 音频源码分析--AndroidRecord音 ...

  9. Android框架源码分析——从设计模式角度看 Retrofit 核心源码

    Android框架源码分析--从设计模式角度看 Retrofit 核心源码 Retrofit中用到了许多常见的设计模式:代理模式.外观模式.构建者模式等.我们将从这三种设计模式入手,分析 Retrof ...

  10. 人人网官方Android客户端源码分析(1)

    ContentProvider是不同应用程序之间进行数据交换的标准API,ContentProvider以某种Uri的形式对外提供数据,允许其他应用访问或修改数据;其他应用程序使用ContentRes ...

最新文章

  1. python中的EVAL函数的定义和用法!
  2. 一文看懂MYCAT数据库服务器!
  3. oracle中的open,Oracle 深入分析Open过程
  4. 【前端攻略】最全面的水平垂直居中方案与flexbox布局
  5. python爬虫数据库_python数据库索引|python爬虫|python入门|python教程
  6. Python 计算总分数和平均分 - Python零基础入门教程
  7. 95-225-040-源码-广播-简介
  8. 排序算法--冒泡排序
  9. Gym - 100625G Getting Through 计算几何+并查集
  10. redis windows切割日志文件_开发者笔记:Windows下搭建简易单机Redis集群测试环境...
  11. PMP学习笔记(一):PMP 目录
  12. Gensim库的使用——Word2vec模型(一)模型的简单介绍与加载预训练的模型进行测试
  13. 项目分析 移动终端自助点餐系统
  14. PhotoShop修改图片文字
  15. 怎么查询计算机二级证书快递号,计算机二级office查询成绩后的注意事项证书直邮申请17到到23号...
  16. win10、win7“以太网”将WiFi名称改成网络名称
  17. Win 10 远程桌面连接
  18. 想要进行gene prioritization分析,请看这里!
  19. 小程序容器助力车企抢滩智慧车载新生态
  20. windows系统中环境系统变量和用户变量的区别

热门文章

  1. MVC 停车场管理系统java jsp 程序设计 课程设计 毕业设计-附源码02141
  2. usb万能驱动win7_最新电脑为何不支持安装win7系统?原来秘密就藏在这里
  3. JAVA设计模式什么鬼(建造者)——作者:凸凹里歐
  4. 大学物理实验报告 - 半导体热敏电阻温度特性的研究
  5. 汽车can网络分析仪汽车CAN
  6. centos播放器解码器下载
  7. ws键盘右键_QTP 视频图像鼠标右键的录制
  8. php实现基站定位,基站定位和WIFI定位完美结合项目源码
  9. TP4056国产替代DP4056 1A 锂离子电池充电器芯片
  10. MySQL入门系列:数据的插入、删除和更新