安卓应用安全指南 4.1.3 创建/使用活动 高级话题
4.1.3 创建/使用活动 高级话题
原书:Android Application Secure Design/Secure Coding Guidebook
译者:飞龙
协议:CC BY-NC-SA 4.0
4.1.3.1 组合导出属性和意图过滤器(对于活动)
我们已经解释了如何实现本指南中的四类活动:私有活动,公共活动,伙伴活动和内部活动。 下表中定义了每种类型的导出属性的允许的设置,和intent-filter
元素的各种组合,它们在AndroidManifest.xml
文件中定义。 请使用你尝试创建的活动,验证导出属性和intent-filter
元素的兼容性。
导出属性的值 | |
---|---|
True | |
意图过滤器已定义 | 公开 |
意图过滤器未定义 | 公开、伙伴、内部 |
表 4.1-2
当未指定Activity
的导出属性时,Activity
是否为公开的,取决于Activity
的意图过滤器的存在与否 [4]。 但是,在本手册中,禁止将导出属性设置为未指定。通常,如前所述,最好避免依赖任何给定 API 的默认行为的实现;此外,如果存在明确的方法(例如导出属性)来启用重要的安全相关设置,那么使用这些方法总是一个好主意。
如果定义了任何意图过滤器,则该活动是公开的;否则它是私有的。更多信息请参阅 https://developer.android.com/guide/topics/manifest/activity-element.html#exported。
不应该使用未定义的意图过滤器和导出属性false
的原因,是 Android 的行为存在漏洞,并且由于意图过滤器的工作原理,其他应用的活动可能会意外调用它。下面的两个图展示了这个解释。图 4.1-4 是一个正常行为的例子,其中私有活动(应用 A)只能由同一个应用的隐式Intent
调用。 意图过滤器(action ="X"
)被定义为仅在应用 A 内部工作,所以这是预期的行为。
下面的图 4.1-5 展示了一个场景,其中在应用 B 和应用 A 中定义了相同的意图过滤器(action ="X"
)。应用 A 试图通过发送隐式意图,来调用同一应用中的私有活动 ,但是这次显示了对话框,询问用户选择哪个应用,以及应用 B 中的公共活动 B-1 ,由于用户的选择而错误调用。 由于这个漏洞,可能会将敏感信息发送到其他应用,或者应用可能会收到意外的返回值。
如上所示,使用意图过滤器,将隐式意图发送到私有应用,可能会导致意外行为,因此最好避免此设置。 另外,我们已经验证了这种行为不依赖于应用 A 和应用 B 的安装顺序。
4.1.3.2 验证请求应用
我们在此解释一些技术信息,关于如何实现伙伴活动。 伙伴应用只允许白名单中注册的特定应用访问,并且所有其他应用都被拒绝。 由于除内部应用之外的其他应用也需要访问权限,因此我们无法使用签名权限进行访问控制。
简而言之,我们希望验证尝试使用伙伴活动的应用,通过检查它是否在预定义的白名单中注册,如果是,则允许访问,如果不是,则拒绝访问。 应用验证的方式是,从请求访问的应用获取证书,并将其与白名单中的散列进行比较。
一些开发人员可能会认为,仅仅比较软件包名称而不获取证书就足够了,但是,很容易伪装成合法应用的软件包名称,因此这不是检查真实性的好方法。 任意指定的值不应用于认证。 另一方面,由于只有应用开发人员拥有用于签署证书的开发人员密钥,因此这是识别的更好方法。 由于证书不容易被伪造,除非恶意第三方可以窃取开发人员密钥,否则恶意应用被信任的可能性很小。 虽然可以将整个证书存储在白名单中,但为了使文件大小最小,仅存储 SHA-256 散列值就足够了。
使用这个方法有两个限制:
- 请求应用需要使用
startActivityForResult()
而不是startActivity()
。 - 请求应用应该只从
Activity
调用。
第二个限制是由于第一个限制而施加的限制,因此技术上只有一个限制。
由于Activity.getCallingPackage()
的限制,它获取调用应用的包名称,所以会发生此限制。 Activity.getCallingPackage()
仅在由startActivityForResult()
调用时,才返回源(请求)应用的包名,但不幸的是,当它由startActivity()
调用时,它仅返回null
。 因此,使用此处解释的方法时,源(请求)应用需要使用startActivityForResult()
,即使它不需要获取返回值。 另外,startActivityForResult()
只能在Activity
类中使用,所以源(请求者)仅限于活动。
PartnerActivity.java
package org.jssec.android.activity.partneractivity;import org.jssec.android.shared.PkgCertWhitelists;
import org.jssec.android.shared.Utils;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;public class PartnerActivity extends Activity {// *** POINT 4 *** Verify the requesting application's certificate through a predefined whitelist.private static PkgCertWhitelists sWhitelists = null;private static void buildWhitelists(Context context) {boolean isdebug = Utils.isDebuggable(context);sWhitelists = new PkgCertWhitelists();// Register certificate hash value of partner application org.jssec.android.activity.partneruser.sWhitelists.add("org.jssec.android.activity.partneruser", isdebug ?// Certificate hash value of "androiddebugkey" in the debug.keystore."0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255" :// Certificate hash value of "partner key" in the keystore."1F039BB5 7861C27A 3916C778 8E78CE00 690B3974 3EB8259F E2627B8D 4C0EC35A");// Register the other partner applications in the same way.}private static boolean checkPartner(Context context, String pkgname) {if (sWhitelists == null) buildWhitelists(context);return sWhitelists.test(context, pkgname);}@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);// *** POINT 4 *** Verify the requesting application's certificate through a predefined whitelist.if (!checkPartner(this, getCallingActivity().getPackageName())) {Toast.makeText(this,"Requesting application is not a partner application.", Toast.LENGTH_LONG).show();finish();return;}// *** POINT 5 *** Handle the received intent carefully and securely, even though the intent was sent from a partner application.// Omitted, since this is a sample. Refer to "3.2 Handling Input Data Carefully and Securely."Toast.makeText(this, "Accessed by Partner App", Toast.LENGTH_LONG).show();}public void onReturnResultClick(View view) {// *** POINT 6 *** Only return Information that is granted to be disclosed to a partner application.Intent intent = new Intent();intent.putExtra("RESULT", "Information for partner applications");setResult(RESULT_OK, intent);finish();}
}
PkgCertWhitelists.java
package org.jssec.android.shared;import java.util.HashMap;
import java.util.Map;
import android.content.Context;public class PkgCertWhitelists {private Map<String, String> mWhitelists = new HashMap<String, String>();public boolean add(String pkgname, String sha256) {if (pkgname == null) return false;if (sha256 == null) return false;sha256 = sha256.replaceAll(" ", "");if (sha256.length() != 64) return false; // SHA-256 -> 32 bytes -> 64 charssha256 = sha256.toUpperCase();if (sha256.replaceAll("[0-9A-F]+", "").length() != 0) return false; // found non hex charmWhitelists.put(pkgname, sha256);return true;}public boolean test(Context ctx, String pkgname) {// Get the correct hash value which corresponds to pkgname.String correctHash = mWhitelists.get(pkgname);// Compare the actual hash value of pkgname with the correct hash value.return PkgCert.test(ctx, pkgname, correctHash);}
}
PkgCert.java
package org.jssec.android.shared;import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;public class PkgCert {public static boolean test(Context ctx, String pkgname, String correctHash) {if (correctHash == null) return false;correctHash = correctHash.replaceAll(" ", "");return correctHash.equals(hash(ctx, pkgname));}public static String hash(Context ctx, String pkgname) {if (pkgname == null) return null;try {PackageManager pm = ctx.getPackageManager();PackageInfo pkginfo = pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);if (pkginfo.signatures.length != 1) return null; // Will not handle multiple signatures.Signature sig = pkginfo.signatures[0];byte[] cert = sig.toByteArray();byte[] sha256 = computeSha256(cert);return byte2hex(sha256);} catch (NameNotFoundException e) {return null;}}private static byte[] computeSha256(byte[] data) {try {return MessageDigest.getInstance("SHA-256").digest(data);} catch (NoSuchAlgorithmException e) {return null;}}private static String byte2hex(byte[] data) {if (data == null) return null;final StringBuilder hexadecimal = new StringBuilder();for (final byte b : data) {hexadecimal.append(String.format("%02X", b));}return hexadecimal.toString();}
}
4.1.3.3 读取发送给活动的意图
在 Android 5.0(API Level 21)及更高版本中,使用getRecentTasks()
得到的信息仅限于调用者自己的任务,并且可能还有一些其他任务,例如已知不敏感的其他任务。 但是支持 Android 5.0(API Level 21)版本的应用应该防止泄露敏感信息。 以下描述了问题内容,它出现在 Android 5.0 及更早版本中。
发送到任务的根Activity
的意图,被添加到任务历史中。 根活动是在任务中启动的第一个活动。 任何应用都可以通过使用ActivityManager
类,读取添加到任务历史的意图。
下面显示了从应用中读取任务历史的示例代码。 要浏览任务历史,请在AndroidManifest.xml
文件中指定GET_TASKS
权限。
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="org.jssec.android.intent.maliciousactivity" ><!-- Use GET_TASKS Permission --><uses-permission android:name="android.permission.GET_TASKS" /><application
android:allowBackup="false"android:icon="@drawable/ic_launcher"android:label="@string/app_name"android:theme="@style/AppTheme" ><activity
android:name=".MaliciousActivity"android:label="@string/title_activity_main"android:exported="true" ><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application>
</manifest>
MaliciousActivity.java
package org.jssec.android.intent.maliciousactivity;import java.util.List;
import java.util.Set;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;public class MaliciousActivity extends Activity {@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.malicious_activity);// Get am ActivityManager instance.ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);// Get 100 recent task info.List<ActivityManager.RecentTaskInfo> list = activityManager.getRecentTasks(100, ActivityManager.RECENT_WITH_EXCLUDED);for (ActivityManager.RecentTaskInfo r : list) {// Get Intent sent to root Activity and Log it.Intent intent = r.baseIntent;Log.v("baseIntent", intent.toString());Log.v(" action:", intent.getAction());Log.v(" data:", intent.getDataString());if (r.origActivity != null) {Log.v(" pkg:", r.origActivity.getPackageName() + r.origActivity.getClassName());}Bundle extras = intent.getExtras();if (extras != null) {Set<String> keys = extras.keySet();for(String key : keys) {Log.v(" extras:", key + "=" + extras.get(key).toString());}}}}
}
你可以使用AcitivityManager
类的getRecentTasks()
函数,来获取任务历史的指定条目。 每个任务的信息存储在ActivityManager.RecentTaskInfo
类的实例中,但发送到任务根Activity
的意图存储在其成员变量baseIntent
中。 由于根Activity
是创建任务时启动的Activity
,请务必在调用Activity
时,不要满足以下两个条件。
- 新的任务在活动被调用时创建
- 被调用的活动是任务的根活动,它已经在前台或者后台存在
4.1.3.4 根活动
根活动是作为任务起点的活动。 换句话说,这是创建任务时启动的活动。 例如,当默认活动由启动器启动时,此活动将是根活动。 根据 Android 规范,发送到根Activity
的意图的内容可以从任意应用中读取。 因此,有必要采取对策,不要将敏感信息发送到根活动。 在本指南中,已经制定了以下三条规则来避免被调用的Activity
成为根活动。
- 不要指定
taskAffinity
- 不要指定
launchMode
- 发送给活动的意图中,不要设置
FLAG_ACTIVITY_NEW_TASK
我们考虑一个情况,活动可以成为下面的根活动。 被调用的活动成为根活动,取决于以下内容。
- 被调用活动的启动模式
- 被调用活动的任务及其启动模式
首先,让我解释一下“被调用活动的启动模式”。 可以通过在AndroidManifest.xml
中编写android:launchMode
来设置Activity
的启动模式。 当它没有编写时,它被认为是“标准”。 另外,启动模式也可以通过设置意图的标志来更改。 标志FLAG_ACTIVITY_NEW_TASK
以singleTask
模式启动活动。
启动模式可以指定为这些。我会解释它们和根活动的关系。
标准(standard
)
此模式调用的活动不会是根,它属于调用者端的任务。 每次调用时,都会生成活动实例。
singleTop
这个启动模式和“标准”相同,除了启动一个活动,它显示在前台任务的最前面时,不会生成实例。
singleTask
这个启动模式根据 Affinity 值确定活动所属的任务。 当匹配Activity
的 Affinity 的任务不存在于后台或前台时,新任务随Activity
的实例一起生成。 当任务存在时,它们都不会被生成。 在前者中,已启动的Activity
实例成为根。
singleInstance
与singleTask
相同,但以下几点不同。 只有根活动可以属于新生成的任务。 因此,通过此模式启动的活动实例,始终是根活动。 现在,我们需要注意的是,虽然任务已经存在,并且名称和被调用Activity
的 Affinity 相同,但是被调用Activity
的类名和包含在任务中的Activity
的类名是不同的。
从上面我们可以知道,由singleTask
或singleInstance
启动的Activity
有可能成为根。 为了确保应用的安全性,它不应该由这些模式启动。
接下来,我将解释“被调用活动的任务及其启动模式”。 即使Activity
以“标准”模式调用,它也会成为根Activity
。在某些情况下,取决于Activity
所属的任务状态。
例如,考虑被调用Activity
的任务已经在后台运行的情况。 这里的问题是,任务的活动实例以singleInstance
启动,当以“标准”调用的Activity
的 Affinity 与任务相同时,新任务的生成受到现有的singleInstance
活动的限制。但是,当每个活动的类名称相同时,不会生成任务,并使用现有活动实例。在任何情况下,被调用活动都将成为根活动。
如上所述,调用根Activity
的条件很复杂,例如取决于执行状态。 因此,在开发应用时,最好设法以“标准”来调用活动。
这是一个示例,其中发送给私有活动的意图,可以从其他应用中读取。示例代码表明,私有活动的调用方活动以singleInstance
模式启动。 在这个示例代码中,私有活动以“标准”模式启动,但由于调用方Activity
的singleInstance
条件,这个私有活动成为新任务的根Activity
。 此时,发送给私有活动的敏感信息,在任务历史中记录,因此可以从其他应用读取。 仅供参考,调用方活动和私有活动都具有相同的 Affinity。
AndroidManifest.xml(不推荐)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="org.jssec.android.activity.singleinstanceactivity" ><application
android:allowBackup="false"android:icon="@drawable/ic_launcher"android:label="@string/app_name" ><!-- Set the launchMode of the root Activity to "singleInstance". --><!-- Do not use taskAffinity --><activity
android:name="org.jssec.android.activity.singleinstanceactivity.PrivateUserActivity"android:label="@string/app_name"android:launchMode="singleInstance"android:exported="true" ><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><!-- Private activity --><!-- Set the launchMode to "standard." --><!-- Do not use taskAffinity --><activity
android:name="org.jssec.android.activity.singleinstanceactivity.PrivateActivity"android:label="@string/app_name"android:exported="false" /></application>
</manifest>
私有活动仅仅将结果返回个收到的意图。
PrivateActivity.java
package org.jssec.android.activity.singleinstanceactivity;import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;public class PrivateActivity extends Activity {@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.private_activity);// Handle intent securely, even though the intent sent from the same application.// Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely."String param = getIntent().getStringExtra("PARAM");Toast.makeText(this, String.format("Received param: ¥"%s¥"", param), Toast.LENGTH_LONG).show();}public void onReturnResultClick(View view) {Intent intent = new Intent();intent.putExtra("RESULT", "Sensitive Info");setResult(RESULT_OK, intent);finish();}
}
在私有活动的调用方,私有活动以“标准”模式启动,意图不带有任何标志。
PrivateUserActivity.java
package org.jssec.android.activity.singleinstanceactivity;import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;public class PrivateUserActivity extends Activity {private static final int REQUEST_CODE = 1;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.user_activity);}public void onUseActivityClick(View view) {// Start the Private Activity with "standard" lanchMode.Intent intent = new Intent(this, PrivateActivity.class);intent.putExtra("PARAM", "Sensitive Info");startActivityForResult(intent, REQUEST_CODE);}@Overridepublic void onActivityResult(int requestCode, int resultCode, Intent data) {super.onActivityResult(requestCode, resultCode, data);if (resultCode != RESULT_OK) return;switch (requestCode) {case REQUEST_CODE:String result = data.getStringExtra("RESULT");// Handle received result data carefully and securely,// even though the data came from the Activity in the same application.// Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely."Toast.makeText(this, Str();break;}}
}
4.1.3.5 使用活动时的日志输出
当使用一个活动时,意图的内容通过ActivityManager
输出到LogCat
。 以下内容将被输出到LogCat
,因此在这种情况下,敏感信息不应该包含在这里。
- 目标包名称
- 目标类名称
- 由
Intent#setData()
设置的 URI
例如,当应用发送邮件时,如果应用将邮件地址指定为 URI,则邮件地址不幸会输出到LogCat
。 所以,最好通过设置Extras
来发送。 如下所示发送邮件时,邮件地址会显示给logCat
。
MainActivity.java
// URI is output to the LogCat.
Uri uri = Uri.parse("mailtoest@gmail.com");
Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
startActivity(intent);
当使用Extras
时,邮件地址不会再展示给LogCat
了。
MainActivity.java
// Contents which was set to Extra, is not output to the LogCat.
Uri uri = Uri.parse("mailto:");
Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
intent.putExtra(Intent.EXTRA_EMAIL, new String[] {"test@gmail.com"});
startActivity(intent);
但是,有些情况下,其他应用可以使用ActivityManager#getRecentTasks()
读取意图的附加数据。 请参阅“4.1.2.2 不指定taskAffinity
(必需)”,“4.1.2.3 不指定launchMode
(必需)”和“4.1.2.4 不要为启动活动的Intent
设置FLAG_ACTIVITY_NEW_TASK
标志(必需)”。
4.1.3.6 防止PreferenceActivity
中的Fragment
注入
当从PreferenceActivity
派生的类是公共活动时,可能会出现称为片段注入 [5] 的问题。 为了防止出现这个问题,有必要重写PreferenceActivity
.IsValidFragment(),并检查其参数的有效性,来确保
Activity不会无意中处理任何
Fragment`。 (输入数据安全的更多信息,请参见第3.2节“小心和安全地处理输入数据”。)
[5]
Fragement
注入的更多信息,请参考:https://securityintelligence.com/new-vulnerability-android-framework-fragment-injection/。
下面我们显示一个覆盖IsValidFragment()
的示例。 请注意,如果源代码已被混淆,则类名称和参数值比较的结果可能会更改。 在这种情况下,有必要寻求替代对策。
覆盖的isValidFragment()
方法的示例
protected boolean isValidFragment(String fragmentName) {// If the source code is obfuscated, we must pursue alternative strategiesreturn PreferenceFragmentA.class.getName().equals(fragmentName)|| PreferenceFragmentB.class.getName().equals(fragmentName)|| PreferenceFragmentC.class.getName().equals(fragmentName)|| PreferenceFragmentD.class.getName().equals(fragmentName);
}
请注意,如果应用的targetSdkVersion
为 19 或更大,不覆盖PreferenceActivity.isValidFragment()
将导致安全异常,并在插入Fragment
时终止应用 [调用isValidFragment()
时],因此在这种情况下,覆盖 PreferenceActivity.isValidFragment()
是强制性的。
安卓应用安全指南 4.1.3 创建/使用活动 高级话题相关推荐
- 安卓应用安全指南 4.4.3 创建/使用服务高级话题
安卓应用安全指南 4.4.3 创建/使用服务高级话题 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC ...
- 安卓应用安全指南 4.1.2 创建/使用活动 规则书
4.1.2 创建/使用活动 规则书 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY-NC-SA ...
- 安卓应用安全指南 4.1.1 创建/使用活动 示例代码
4.1.1 创建/使用活动 示例代码 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY-NC-SA ...
- 安卓应用安全指南 5.5.3 处理隐私数据 高级话题
5.5.3 处理隐私数据 高级话题 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY-NC-SA ...
- 安卓应用安全指南 4.4.2 创建/使用服务 规则书
安卓应用安全指南 4.4.2 创建/使用服务 规则书 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC ...
- 安卓应用安全指南 4.2.3 创建/使用广播接收器 高级话题
4.2.3 创建/使用广播接收器 高级话题 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY-NC ...
- 安卓应用安全指南 4.4.1 创建/使用服务 示例代码
4.4.1 创建/使用服务 示例代码 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY-NC-SA ...
- 安卓应用安全指南 4.3.2 创建/使用内容供应器 规则书
4.3.2 创建/使用内容供应器 规则书 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY-NC- ...
- 安卓应用安全指南 4.3.1 创建/使用内容供应器 示例代码
4.3.1 创建/使用内容供应器 示例代码 原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY-NC ...
最新文章
- 在Ubuntu 14.04 64bit上升级安装ATS 5.3.2/6.1.1实录
- 解决网通英文wiki无法显示图片问题【20100723更新】
- 几则常用的BASIS技巧整理
- 【iCore3应用开发平台】发布 iCore3 应用开发平台出厂代码rev0.0.2
- 使用 Eclipse C/C++ Development Toolkit 开发应用程序
- ASP.NET 状态视图概览
- int 转为字节后 低字节在前_NumPy 字节交换
- C-Lodop的https扩展版,火狐下添加例外
- HTTP缓存原理及相关知识(2)-CDN
- C语言学习资料教程 | 免费下载
- 造节新案例,这家互联网公司用一场声音节圈住了声控党的心!
- 云计算开发一般负责什么工作呢?云计算是做什么的?
- 关于Video.js 出现的问题 this.el_.vjs_getProperty
- flink 时间语义、水位线(Watermark)、生成水位线、水位线的传递
- volatile能保持线程安全吗_volatile是什么?volatile能保证线程安全性吗?如何正确使用volatile?...
- python 以图搜图_Python深度学习,手把手教你实现「以图搜图」
- 6. Excel 图表制作
- python应用学习(三)——pyttsx3用四行代码让python说话!
- c语言编程数字后有ul,十六进制数后跟L/U/UL解析
- 如何消除win10文件右上角的蓝色箭头
热门文章
- 2021年CBA总决赛第三场预测
- (26)计数器verilog与VHDL编码(学无止境)
- oracle安装检测空间china,oracle安装 - Ginn的个人空间 - OSCHINA - 中文开源技术交流社区...
- oracle导出报错04063,Oracle EXP导出报错的解决方法
- server sql 无法从long转为int_MySQL中,21个写SQL的好习惯(修正版)
- php gettext 为空,PHP Gettext
- mysql 利用触发器(Trigger)让代码更简单
- varnish几个工具命令行工作情况
- 内核并发控制---RCU (来自网易)
- 信号量与线程互斥锁的区别