技术标签: SE android 移动安全 Android技术
建议扩展阅读:
Android SE的多种形态: eSE、UICC、SD Card
Android 9.0 SecureElementService 初始化流程分析 很清晰的UML类图和调用流程
SE 也就是 Secure Element,译为 “安全元素”
主要应用场景在 手机手表交通卡、门禁、虚拟钱包、虚拟SIM卡,以及其他身份认证的且对安全级别有一定要求的业务。
目前 Android 手机主要有三种SE的实现 eSE、UICC、microSD,这些带有SE的芯片有独立的储存和计算能力,可以进行 Applet 安装与个人化等一些列自定义的安全行为操作。
简单提一下这三种SE实现:
eSE:Embedded ,硬件内嵌在手机形式,通过 NFC Controller 使用
UICC:物理插槽形式,日常使用的SIM卡,在Android层面叫UICC,现在 eSIM 阶段新增了 EUICC,而且 Android 提供了一套另外的 eSIM 服务框架
microSD:物理插槽形式,早期使用的外部 SD 存储卡
Tips:
Android9 之前需引入 org.simalliance.openmobileapi.jar,从 Android9 开始并入了Android SDK,在 android.se.omapi 包下,且在 /packages/apps/ 下新增了SE相关模块(SecureElement)
包路径:/frameworks/base/omapi/core/java/android/se/omapi/
目录下的所有文件:
Channel.java
Reader.java
SEService.java
Session.java
ISecureElementChannel.aidl
ISecureElementListener.aidl
ISecureElementReader.aidl
ISecureElementService.aidl
ISecureElementSession.aidl
Transport API class diagram overview
和原始的 OMAPI规范 还是有点区别
一、SEService.java
SEService(Context context, Executor executor, OnConnectedListener listener)
对应底层类 SecureElementService.java
内部接口: OnConnectedListener
初始化为耗时操作,异步进行,成功连接后进行回调
1.void onConnected();
方法:
- Reader getUiccReader(int)
- Reader[] getReaders() 返回可用的Reader列表,元素不能重复,即使没有插卡也返回,
底层是 Terminal内部类SecureElementReader(Reader相关的通信也由它完成), 内部有 ArrayList<SecureElementSession> mSessions
- boolean isConnected() 当此服务成功连接时为true
- void shutdown() 释放所有由此SEService分配的SE资源,建议在程序结束时调用,此方法执行后,isConnected返回false
- String getVersion() 返回此实现基于哪一个 Open Mobile API 规范版本
计算方法:版本号 = 主版本号*1000 + 副版本号,(例如:“3001”是基于OMAPI规范v3.1的实现)
二、Reader.java
Reader(@NonNull SEService service, @NonNull String name, @NonNull ISecureElementReader reader)
对应底层类 Terminal.java,此类非常重要(/packages/apps/SecureElement/src/com/android/se/Terminal.java)
方法:
- String getName()
- SEService getSEService()
- boolean reset()
- boolean isSecureElementPresent()
- Session openSession()
- void closeSessions()
三、Session.java
Reader getReader()
byte[] getATR()
void close()
boolean isClosed()
void closeChannels()
Channel openBasicChannel(byte[] aid)
Channel openLogicalChannel(byte[] aid)
Channel openBasicChannel(byte[] aid, Byte P2)
Channel openLogicalChannel(byte[] aid, Byte P2)
四、Channel.java
void close()
boolean isClosed()
boolean selectNext() 遍历匹配相同部分AID的所有applet, true: 在该通道上成功选择了一个新的Applet; false: 保持原有选中
boolean isBasicChannel()
Session getSession()
byte[] getSelectResponse()
byte[] transmit(byte[] command)
void setTransmitExpectDataWithWarningSW(boolean expectData)
通过 SEService 类构造传入注册服务接口,获取 Reader 得到 Session 实例,通过 Session 可以打开 Channel(包括 BasicChannel 和 LogicalChannel),Channel 中提供了发送 APDU 命令的方法。
总结步骤:
包路径:/packages/apps/SecureElement/src/com/android/se/
目录下的文夹件和类:
internal/
security/
Channel.java
CommandApduValidator.java
SEApplication.java
SecureElementService.java
Terminal.java
一、SecureElementService.java
getReader()
createTerminals() {
addTerminals(ESE_TERMINAL);
addTerminals(UICC_TERMINAL);
}
addTerminals(String terminalName) {
...
name = terminalName + Integer.toString(index);
Terminal terminal = new Terminal(name, this);
terminal.initialize(index == 1);
mTerminals.put(name, terminal);
...
}
// Seesion 的Binder桥接
final class SecureElementSession extends ISecureElementSession.Stub {
closeChannels()
openBasicChannel(...)
openLogicalChannel(...)
}
二、Terminal.java
setUpChannelAccess(...)
isSecureElementPresent()
initialize(boolean retryOnFail)
initializeAccessControl() {
if (mAccessControlEnforcer == null) {
mAccessControlEnforcer = new AccessControlEnforcer(this);
}
mAccessControlEnforcer.initialize();
}
byte[] transmit(byte[] cmd);
byte[] transmitInternal(byte[] cmd) {
...
// mSEHal 也就是 SecureElement.java
// 年轻人我劝你不要去看后面的源码了, 深得很你把握不住, 硬件层的各种 CPP
mSEHal.transmit(byteArrayToArrayList(cmd));
}
// Reader 的Binder桥接
final class SecureElementReader extends ISecureElementReader.Stub {
getAtr()
reset()
openSeesion()
isSecureElementPresent()
private getTerminal()
}
三、AccessControlEnforcer.java
initialize()
reset()
setUpChannelAccess(...)
readSecurityProfile()
arf...PKCS15
ara...
四、Channel.java
byte[] transmit(byte[] command) {
CommandApduValidator.execute(command);
checkCommand(command);
// 一大堆命令检查, 接着还是通过 Terminal 发送
return mTerminal.transmit(command);
}
final class SecureElementChannel extends ISecureElementChannel.Stub {
close()
boolean selectNext()
byte[] transmit(byte[] command) {
Channel.this.transmit(command);
}
}
常用名词
Trusted Service Manager (TSM)
Issuer Security Domain (ISD)
Access Rule Files (ARF)
Access Rule Application (ARA)
Access Rule Application Master (ARA-M)
Access Rule Application Client (ARA-C)
framework 层的 SEService.java 在构造里通过 ISecureElementChannel.aidl 类绑定到了
packages 层的服务 SecureElementService.java 上,后续都通过此服务交互。以下三个比较重要的入口类,SecureElementService 会在 onCreate() 里初始化,并创建多个 Terminal 实例,主要是 eSE 和 UICC(SIM),它们可能会有多个,类似(eSE1、eSE2、SIM1、SIM2)。
这里即是上层调用的 openSeesion、openBasicChannel()、openLogicalChannel() 到 C++ 层的入口,也就是在这里做了 AccessRule 的校验(我在这儿就遇到了问题),规则校验具体实现入口在 AccessControlEnforcer.java,此类在 Terminal 中初始化。
顺便提两句,第一个是在上层调用 openSeesion 过程中有一个 isSecureElementPresent() 的检查,如果返回 false 会直接抛出异常 “Secure Element is not present.”,也就是当前 SE 不可用(具体原因暂时未解,查看了一些cpp的实现里直接返回的true,但上层确实有返回false,排除异常的情况),所以调用前需要在 Reader 里先行判断。
第二个是 openXXXChannel() 过程中,会有一个 setUpChannelAccess() 方法进行访问规则的校验,关于 AccessRule GPD 有一本非常厚的 SE_Access_Control_v1.1.pdf 的规范描述。
当规则校验通过后,Channel 也就打开了,此时可以在上层使用 Channel.transmit() 发送 APDU 指令,且此方法进行了响应。可以是一条指令也可以是多条,发送完成后即返回结果,另外还提供了一个方法用来获取结果 getSelectResponse(),没太细究它们俩直接的区别。
可以使用 PackageManager.hasSystemFeature 检查设备是否支持需要的 SE 区域,或者使用 PackageManager.systemAvailableFeatures 列出所有支持的特性,从里面找如下三个
FEATURE_SE_OMAPI_ESE
FEATURE_SE_OMAPI_UICC
FEATURE_SE_OMAPI_SD
Android 官方相关描述:
Open Mobile API reader support
On Android 11 and higher, Open Mobile API (OMAPI) supports checking for eSE, SD, and UICC support hardware on devices with the following flags:
FEATURE_SE_OMAPI_ESE
FEATURE_SE_OMAPI_SD
FEATURE_SE_OMAPI_UICC
Use these values with getSystemAvailableFeatures() or hasSystemFeature() to check for device support.
if (requireContext().packageManager.hasSystemFeature(PackageManager.FEATURE_SE_OMAPI_UICC)) {
SIMLog.e("系统是否支持 OMAPI UICC_SE 硬件功能:true")
lifecycleScope.launch {
delay(2000)
mOperator.getEID() // 做了个测试调用, 里面使用 OMAPI 执行了一次 APDU 发送
}
} else {
SIMLog.e("系统是否支持 OMAPI UICC_SE 硬件功能:false")
}
requireContext().packageManager.systemAvailableFeatures.forEach {
Log.e("Flyme-SIM", "系统已支持的硬件功能:$it")
}
使用了 framework 下的类 SEService.java,也就是SDK自带的包 android.se.omapi,调用的相关日志在后续有贴出来,主要是经历了一个错误记录下来
// Android 客户端代码
import android.se.omapi.SEService
private var mUICCReader: Reader? = null
private val mSEService = SEService(context, ThreadUtils.getSinglePool()) {
SIMLog.e("SEService已连接", TAG)
}
init {
// SEService 大概需要几百毫秒进行连接
ThreadUtils.getMainHandler().postDelayed({
SIMLog.e("SEService连接状态: ${
mSEService.isConnected}", TAG)
if (mSEService.isConnected) {
// mUICCReader = mSEService.getUiccReader(SLOT_INDEX) // 指定卡槽拿到的Reader不是SE, 直接使用遍历的方式
// 我的遍历结果
// 已存在的Reader: name=eSE1, isSecureElementPresent=true
// 已存在的Reader: name=SIM1, isSecureElementPresent=false
// 已存在的Reader: name=SIM2, isSecureElementPresent=false
for (reader in mSEService.readers) {
SIMLog.printD("已存在的Reader: name=${
reader.name}, isSecureElementPresent=${
reader.isSecureElementPresent}")
if (reader.isSecureElementPresent) mUICCReader = reader
}
}
}, 1000)
}
/**
* 发送 APDU 指令
* 一次发送, 包含 选择Applet、打开通道、读取响应、关闭通道
* 1.打开 Reader、Session、Channel
* 2.通过 Channel 选择 Applet,并打开逻辑通道
* 3.发送 APDU,并接收响应
* 4.关闭 Reader、Session、Channel
*
* @param p2 '00', '04', '08', '0C'
*/
private fun send(command: ByteArray, aid: ByteArray = AID_BYTE, p2: Byte = 0x00): ByteArray? {
var resp: ByteArray? = null
mUICCReader?.let {
reader ->
log("sendApdu: Reader.isSecureElementPresent=${
reader.isSecureElementPresent}")
try {
if (!reader.isSecureElementPresent) {
log("此 Reader 不支持 SE")
return null
}
val openSession = reader.openSession()
// 在执行 openSession.openLogicalChannel(aid, p2) 时,遇到一个错误
// java.lang.SecurityException:
// Exception in setUpChannelAccess() java.security.AccessControlException: SecureElement-AccessControlEnforcerno APDU access allowed!
openSession.openLogicalChannel(aid, p2)?.let {
log("发送的req: ${
command.bytesToHexString()}")
it.transmit(command)
resp = it.selectResponse
log("返回的resp: ${
resp?.bytesToHexString()}")
log("返回是否成功: ${
isSendSuccess(resp)}")
it.close()
}
openSession.close()
} catch (e: Exception) {
log("发送APDU异常: $e")
}
} ?: kotlin.run {
log("mUICCReader还未初始化完成, SEService还在连接中")
}
return resp
}
执行 openSession.openLogicalChannel(aid, p2) 遇到错误:
java.lang.SecurityException:
Exception in setUpChannelAccess()
java.security.AccessControlException:SecureElement-AccessControlEnforcerno APDU access allowed!
这个问题出现是正常的,因为OMAPI的规范里是有AC规则校验的,正常情况下应用什么都没做去访问SE肯定是没有权限的。
解决方法1:如果是使用SIM-SE,就把应用hash写入SIM卡,如果是使用eSE,就把应用hash写入手机系统;
解决方法2:修改系统源码,注释掉AC规则校验的相关逻辑,我尝试了下面的方式修改校验,结果是无效的
查看源码:
/frameworks/base/omapi/java/android/se/omapi/Session.java
Session.java
public @Nullable Channel openLogicalChannel(@Nullable byte[] aid, @Nullable byte p2) throws IOException {
if (!mService.isConnected()) {
throw new IllegalStateException("service not connected to system");
}
synchronized (mLock) {
try {
// 执行到这一行, 调用到 ISecureElementChannel.aidl
ISecureElementChannel channel = mSession.openLogicalChannel(
aid,
p2,
mReader.getSEService().getListener());
if (channel == null) {
return null;
}
return new Channel(mService, this, channel);
} catch (ServiceSpecificException e) {
...
} catch (RemoteException e) {
throw new IllegalStateException(e.getMessage());
}
}
}
后续调用离开了 framework 层到了底层目录 packages:
错误里提到一个关键类和一个方法:AccessControlEnforcerno.java 和 setUpChannelAccess(),该类在 Terminal.java 中初始化
首先来看下 Terminal.java
Terminal.java
/**
* Initializes the Access Control for this Terminal
*/
private synchronized void initializeAccessControl() throws IOException,
MissingResourceException
{
...
synchronized(mLock) {
if (mAccessControlEnforcer == null) {
mAccessControlEnforcer = new AccessControlEnforcer (this);
}
try {
mAccessControlEnforcer.initialize();
} catch (IOException | MissingResourceException e) {
mAccessControlEnforcer = null;
throw e;
}
}
}
/**
* Opens a logical Channel with AID for the given package name or uuid
*/
public Channel openLogicalChannel(SecureElementSession session, byte[] aid, byte p2,
ISecureElementListener listener, String packageName,
byte[] uuid, int pid) throws IOException, NoSuchElementException {
...
ChannelAccess channelAccess = null;
if (packageName != null || uuid != null) {
channelAccess = setUpChannelAccess(aid, packageName, uuid, pid, false);
}
...
return logicalChannel;
}
// 在 Terminal.java 同名的 setUpChannelAccess 方法里调用了 mAccessControlEnforcer.setUpChannelAccess
/**
* Initialize the Access Control and set up the channel access.
*/
private ChannelAccess setUpChannelAccess(byte[] aid, String packageName, byte[] uuid, int pid,
boolean isBasicChannel) throws IOException, MissingResourceException {
ChannelAccess channelAccess =
mAccessControlEnforcer.setUpChannelAccess(aid, packageName, uuid, checkRefreshTag);
}
接下来就到了最终目的类 AccessControlEnforcerno.java
/packages/apps/SecureElement/src/com/android/se/security/AccessControlEnforcerno.java
AccessControlEnforcerno.java
/** Initializes the Access Control for the Secure Element */
public synchronized void initialize() throws IOException, MissingResourceException {
...
readSecurityProfile();
...
}
// 后面两个方法就是完整代码了
/** Sets up the Channel Access for the given Package */
public ChannelAccess setUpChannelAccess(byte[] aid, String packageName, byte[] uuid,
boolean checkRefreshTag) throws IOException, MissingResourceException {
ChannelAccess channelAccess = null;
// check result of channel access during initialization procedure
if (mInitialChannelAccess.getAccess() == ChannelAccess.ACCESS.DENIED) {
throw new AccessControlException(mTag + "access denied: " + mInitialChannelAccess.getReason());
}
// this is the new GP Access Control Enforcer implementation
if (mUseAra || mUseArf) {
channelAccess = internal_setUpChannelAccess(aid, packageName, uuid,checkRefreshTag);
}
if (channelAccess == null || (channelAccess.getApduAccess() != ChannelAccess.ACCESS.ALLOWED
&& !channelAccess.isUseApduFilter())) {
// 关键点来了, 摆明了就是一个系统权限问题
if (mFullAccess) {
// if full access is set then we reuse the initial channel access,
// since we got so far it allows everything with a descriptive reason.
channelAccess = mInitialChannelAccess;
} else {
// 错误就是在这里抛出的, mFullAccess 是个全局方法, 在初始化时调用了
throw new AccessControlException(mTag + "no APDU access allowed!");
}
}
channelAccess.setPackageName(packageName);
return channelAccess.clone();
}
private void readSecurityProfile() {
// 非 debug 模式下写死了 mFullAccess = false 的,可以将手机 root 或者使用 magisk app 修改为 Debug
if (!Build.IS_DEBUGGABLE) {
mUseArf = true;
mUseAra = true;
mFullAccess = false; // Per default we don't grant full access.
} else {
String level = SystemProperties.get("service.seek", "useara usearf");
level = SystemProperties.get("persist.service.seek", level);
if (level.contains("usearf")) {
mUseArf = true;
} else {
mUseArf = false;
}
if (level.contains("useara")) {
mUseAra = true;
} else {
mUseAra = false;
}
if (level.contains("fullaccess")) {
// 全局只有这一处将 mFullAccess 赋值成了 true
// 也就是上面 SystemProperties 配置的 "service.seek" 值起了决定性作用
mFullAccess = true;
} else {
mFullAccess = false;
}
}
if (!mTerminal.getName().startsWith(SecureElementService.UICC_TERMINAL)) {
// ARF is supported only on UICC.
mUseArf = false;
}
Log.i(mTag, "Allowed ACE mode: ara=" + mUseAra + " arf=" + mUseArf + " fullaccess="+ mFullAccess);
}
在系统已经 root 的情况下,直接使用命令修改掉这两个字段的值
adb shell setprop ro.debuggable "1" //正常情况下 已root 设备就是 1
adb shell setprop service.seek "useara usearf fullaccess"
adb shell setprop persist.service.seek "useara usearf fullaccess"
#设置后也可以查看
adb shell getprop service.seek
17:56:39.838 4481 SecureElementService com.android.se D getReaders() eSE1
17:56:39.838 4481 SecureElementService com.android.se D getReaders() SIM1
17:56:39.838 4481 SecureElementService com.android.se D getReaders() SIM2
17:56:39.838 4481 SecureElementService com.android.se I isCtsRunning false
17:56:39.838 4481 SecureElementService com.android.se D getReader() SIM1
17:56:39.839 4481 SecureElementService com.android.se D getReader() SIM2
17:56:39.839 4481 SecureElementService com.android.se D getReader() eSE1
17:56:40.856 4481 SecureElementService com.android.se I openLogicalChannel() AID = 00a4040000, P2 = 0
17:56:40.857 4481 SecureElement-Terminal-eSE1 com.android.se I mzoma Access Check, pkg is com.ccsmec.sim
17:56:40.859 4481 SecureElement-Terminal-eSE1 com.android.se W Enable access control on logical channel for com.ccsmec.sim
17:56:40.859 4481 SecureElement-AccessControlEnforcer com.android.se I setUpChannelAccess() aid = 00a4040000
17:56:40.859 4481 SecureElement-AccessControlEnforcer com.android.se I setUpChannelAccess() packageName = com.ccsmec.sim
17:56:40.860 4481 SecureElement-Terminal-eSE1 com.android.se I mzoma Access Check, pkg is com.ccsmec.sim
17:56:40.915 4481 SecureElement-AccessControlEnforcer com.android.se I checkCommand() : Access = ALLOWED APDU Access = ALLOWED Reason = Unspecified
17:56:40.928 4481 SecureElement-Terminal-eSE1 com.android.se I Sent : 81cadf2000
17:56:40.928 4481 SecureElement-Terminal-eSE1 com.android.se I Received : df20084a46485fc8d7b30b9000
17:56:40.928 4481 SecureElement-AraController com.android.se I Refresh tag unchanged. Using access rules from cache.
17:56:40.962 4481 SecureElement-AccessControlEnforcer com.android.se I getAccessRule() appCert = dba6d7c5929b57a81387d144da3d04a5a3f32137
17:56:40.962 4481 SecureElement-AccessControlEnforcer com.android.se I getAccessRule() appCert = a89c2be6dbe8eefbcce61b5c02a04d99862ef42fbb147c1a7f1d14e742e7db54
17:56:40.964 4481 SecureElement-AccessRuleCache com.android.se I findAccessRule() not found
文章浏览阅读3.3k次,点赞2次,收藏7次。前言CamVid 数据集是由剑桥大学公开发布的城市道路场景的数据集。CamVid全称:The Cambridge-driving Labeled Video Database,它是第一个具有目标类别语义标签的视频集合。数据集包 括 700 多张精准标注的图片用于强监督学习,可分为训练集、验证集、测试集。同时, 在 CamVid 数据集中通常使用 11 种常用的类别来进行分割精度的评估,分别为:道路 (Road)、交通标志(Symbol)、汽车(Car)、天空(Sky)、行人道(Sidewalk)、电_camvid数据集11类别
文章浏览阅读8.7k次,点赞11次,收藏90次。文章目录一、ESP32 LVGL工程配置1.1从库中下载LVGL代码1.2配置适合ESP32 液晶屏1.3编译下载测试二、GIF图片处理2.1下载gif图片2.2将gif图片按照帧率导出成图片2.lvgl animimg对象实现图片的播放1.3下载测试 注:本博客作为学习笔记,有错误的地方希望指正一、ESP32 LVGL工程配置首先要通过液晶屏显示太空人,我们这里主要有两种方式可以实现,第一种直接使用厂家只带的液晶屏幕驱动去实现图片的显示,另外使用其他的GUI提供的控件去实现,嵌入式常见的GUI挺多_lv_img_declare
文章浏览阅读1.5w次,点赞7次,收藏30次。问题描述:给定一个赋权无向图G=(V,E),每个顶点v∈V都有一个权值w(v)。如果UV,且对任意(u,v)∈E有u∈U或v∈U,就称U为图G的一个顶点覆盖。G的最小权顶点覆盖是指G中所含顶点权之和最小的顶点覆盖。问题解决:用优先队列分支限界方法解最小权顶点覆盖,在算法的搜索的进程中保存当前已构造出的部分解空间树,在算法搜索达到叶节点时,其最优值对应的最优解同时保存下来。优先队列的优先_最小权顶点覆盖
文章浏览阅读517次。首先试了matlab自带的worldmap,感觉画出来的图形不尽如人意,比较杂乱。如下图。查阅了些资料,请教了Liangjing,一致推荐m_map。为了达到想要的效果,这次只要不再偷懒,下载M-Map工具箱(http://www.eos.ubc.ca/~rich/map.html)并进行安装。所幸过程比较顺利,现记录如下,回头把画出的效果图再添上。其他matlab的toolbox安装,也可参考进..._mmap工具包如何安装
文章浏览阅读5.7k次,点赞4次,收藏5次。作者感言阅读前言iOS代码规范Import规范Define规范Paragma Mark 规范Interface规范implementation规范实例规范NSDictionary规范NSArray规范函数规范If-Else规范For-In For 规范Block规范运算符规范命名规范实例命名规范Property命名规范Interface-class命名规范B_ios replaceobjectsinrange
文章浏览阅读1.7w次,点赞7次,收藏39次。本文实例讲述了Python中列表元素转为数字的方法分享给大家供大家参考,具体如下:1.首先可以用循环来解决(直接粗暴)举个栗子:<1>有一个数字字符的列表:numbers = ['2', '5', '130', '8']`<2>想要把每个元素转换为数字:numbers = [2, 5, 130, 8]用一个循环来解决:new_numbers = [];..._pandas index存入数组
文章浏览阅读289次。使用maven编译Java项目http://blog.csdn.net/yaya1943/article/details/48464371使用"mvn clean"命令清除编译结果,也就是把编译生成的target文件夹删掉如果你想安装您的项目的JAR文件到本地Maven仓库,那么你应该调用下面语句:mvn install此时,你的项目代码将会经过编译、测试、打..._maven 编译不使用本地依赖包
文章浏览阅读7.6k次,点赞147次,收藏134次。在日常工作中巧用ChatGPT可以帮助我们提高工作效率、创造价值并降低成本。通过合理地利用ChatGPT的功能和应用场景,企业和个人可以更好地实现工作目标、提升竞争力并取得更大的成功。随着人工智能技术的不断进步和发展我们相信巧用ChatGPT将成为未来工作中的一种常态化工具为我们的职业生涯和生活带来更多便利和价值。本书是一本关于数据分析与ChatGPT应用的实用指南,旨在帮助读者了解数据分析的基础知识及利用ChatGPT进行高效的数据处理和分析。_巧用chatgpt快速提高职场晋升力
文章浏览阅读2.7k次。vscode出现10k+更改需要处理_git init 代码有 10k+ 更新
文章浏览阅读6.1k次,点赞5次,收藏124次。聚类聚类,简单来说,就是将一个庞杂数据集中具有相似特征的数据自动归类到一起,称为一个簇,簇内的对象越相似,聚类的效果越好。它是一种无监督的学习(Unsupervised Learning)方法,不需要预先标注好的训练集。聚类与分类最大的区别就是分类的目标事先已知,例如猫狗识别,你在分类之前已经预先知道要将它分为猫、狗两个种类;而在你聚类之前,你对你的目标是未知的,同样以动物为例,对于一个动物集来..._k-means聚类算法的anchors_num的最大值
文章浏览阅读1.6w次,点赞2次,收藏15次。前言介绍很多萌新小白初涉国外云盘对Google云盘和微软OneDrive云盘到底容量有多大、数据隐私是否安全、自己免费申请白嫖的账户可用多久都没有清晰准确的认识和了解,又加上网上博客文章大多都是很多年前的方法和政策,完全给不了网友实质上的建议,本文来帮大家屡屡头绪。产品认识Google DriveGoogle Drive云端硬盘提供了15G 的免费容量,对于一般文档是够用了。Google drive唯一的好处就说分享资源超级方便,共享也很容易。Google 照片提供不限容量的照片存储_onedrive a5
文章浏览阅读1.5w次,点赞18次,收藏69次。将平板、手机作为电脑第二屏幕(Linux系统下)背景把手机、平板作为电脑第二屏幕是上个学期偶然想到的,那时我一边看网上的教程一边码代码。由于看的是视频教程,缩小了就看不清上面的字,放大了又会挡住打字区域。这时就想如果有个第二屏幕就很方便了,买个屏幕又不太方便,想到自己还有个平板天天吃灰,这不是就是现成的屏幕嘛。在网上搜了搜发现真的有解决方案,windows和mac上有很多软件可以用,但我平时..._平板副屏linux