版权声明:本文为HaiyuKing原创文章,转载请注明出处!

前言

  在6.0以前的系统,都是权限一刀切的处理方式,只要用户安装,Manifest申请的权限都会被赋予,并且安装后权限也撤销不了。

  Android 6.0 采用新的权限模型,只有在需要权限的时候,才告知用户是否授权;是在runtime时候授权,而不是在原来安装的时候 ,同时默认情况下每次在运行时打开页面时候,需要先检查是否有所需要的权限申请。

  判断是否是需要运行时权限的标记就是targetSDKVersion。

  当targetSDKVersion<23的时候,仅在安装时赋予权限,使用时将不被提醒;

  当targetSDKVersion≥23的时候才会使用新的运行时权限规则。

  运行时权限未适配可能会导致应用崩溃或者在SD卡中创建目录和文件不成功。

注意:

在 Android 8.0 之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用。

对于针对 Android 8.0 的应用,此行为已被纠正。系统只会授予应用明确请求的权限。然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。

例如,假设某个应用在其清单中列出 READ_EXTERNAL_STORAGE 和 WRITE_EXTERNAL_STORAGE。应用请求 READ_EXTERNAL_STORAGE,并且用户授予了该权限。如果该应用针对的是 API 级别 24 或更低级别,系统还会同时授予 WRITE_EXTERNAL_STORAGE,因为该权限也属于同一 STORAGE 权限组并且也在清单中注册过。如果该应用针对的是 Android 8.0,则系统此时仅会授予 READ_EXTERNAL_STORAGE;不过,如果该应用后来又请求 WRITE_EXTERNAL_STORAGE,则系统会立即授予该权限,而不会提示用户。

解决方案:

一、将targetSDKVersion人为地降到小于23,这样就变成了还是默认使用权限,但是这种并不是Google所推荐使用的。

二、实现APP支持运行时权限

那么,哪些权限属于运行时权限呢?

权限的分组

Android中有很多权限,但并非所有的权限都是敏感权限,于是6.0系统就对权限进行了分类,一般为下述几类

  • 正常(Normal Protection)权限
  • 危险(Dangerous)权限
  • 特殊(Particular)权限
  • 其他权限(一般很少用到)

正常权限

正常权限具有如下的几个特点

  • 对用户隐私没有较大影响或者不会带来安全问题。
  • 安装后就赋予这些权限,不需要显示提醒用户,用户也不能取消这些权限。

上述的权限基本设计的是关于网络,蓝牙,时区,快捷方式等方面,只要在AndroidManifest.xml指定了这些权限,就会被授予,并且不能撤销。

注意:直接在AndroidManifest.xml文件中声明权限即可。

特殊权限

这里讲特殊权限提前讲一下,因为这个相对来说简单一些。

特殊权限,顾名思义,就是一些特别敏感的权限,在Android系统中,主要由两个

  • SYSTEM_ALERT_WINDOW(设置悬浮窗,进行一些黑科技)
  • WRITE_SETTINGS (修改系统设置)

关于上面两个特殊权限的授权,做法是使用startActivityForResult启动授权界面来完成。

注意:关于这两个特殊权限,一般不建议应用申请。

请求SYSTEM_ALERT_WINDOW

private static final int REQUEST_CODE = 1;
private void requestAlertWindowPermission() {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_CODE);
} @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE) {
if (Settings.canDrawOverlays(this)) {
Log.i(LOGTAG, "onActivityResult granted");
}
}
}

上述代码需要注意的是

  • 使用Action Settings.ACTION_MANAGE_OVERLAY_PERMISSION启动隐式Intent
  • 使用"package:" + getPackageName()携带App的包名信息
  • 使用Settings.canDrawOverlays方法判断授权结果

请求WRITE_SETTINGS

private static final int REQUEST_CODE_WRITE_SETTINGS = 2;
private void requestWriteSettings() {
Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_CODE_WRITE_SETTINGS );
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_WRITE_SETTINGS) {
if (Settings.System.canWrite(this)) {
Log.i(LOGTAG, "onActivityResult write settings granted" );
}
}
}

上述代码需要注意的是

  • 使用Action Settings.ACTION_MANAGE_WRITE_SETTINGS 启动隐式Intent
  • 使用"package:" + getPackageName()携带App的包名信息
  • 使用Settings.System.canWrite方法检测授权结果

危险权限

危险权限实际上才是运行时权限主要处理的对象,这些权限可能引起隐私问题或者影响其他程序运行。

Android中的危险权限可以归为以下几个分组:

权限组

权限列表

android.permission-group.CALENDAR

android.permission.READ_CALENDAR

(允许程序读取用户的日程信息)

android.permission-group.CAMERA

android.permission.CAMERA

(允许访问摄像头进行拍照)

android.permission-group.CONTACTS

android.permission.READ_CONTACTS

(允许应用访问联系人通讯录信息)

android.permission.WRITE_CONTACTS

(写入联系人,但不可读取)

android.permission.GET_ACCOUNTS

(访问GMail账户列表)

android.permission-group.LOCATION

android.permission.ACCESS_COARSE_LOCATION

(通过WiFi或移动基站的方式获取用户错略的经纬度信息,定位精度大概误差在30~1500米)

android.permission.ACCESS_FINE_LOCATION

(通过GPS芯片接收卫星的定位信息,定位精度达10米以内)

android.permission-group.MICROPHONE

android.permission.RECORD_AUDIO

(录制声音通过手机或耳机的麦克)

android.permission-group.PHONE

android.permission.READ_PHONE_STATE

(访问电话状态)

android.permission.CALL_PHONE

(允许程序从非系统拨号器里输入电话号码)

android.permission.READ_CALL_LOG

(允许应用程序读取用户的通话记录)

android.permission.WRITE_CALL_LOG

(允许一个程序写入(但不读取)用户的通话记录资料)

com.android.voicemail.permission.ADD_VOICEMAIL

(允许应用程序添加语音邮件进入系统)

android.permission.USE_SIP

(允许程序使用SIP视频服务)

android.permission.PROCESS_OUTGOING_CALLS

(允许程序监视,修改或放弃播出电话)

android.permission-group.SENSORS

android.permission.BODY_SENSORS

(允许从传感器,用户使用来衡量什么是他/她的身体内发生的事情,如心脏速率访问数据的应用程序)

android.permission-group.SMS

android.permission.SEND_SMS

(发送短信)

android.permission.RECEIVE_SMS

(接收短信)

android.permission.READ_SMS

(读取短信内容)

android.permission.RECEIVE_WAP_PUSH

(接收WAP PUSH信息)

android.permission.RECEIVE_MMS

(接收彩信)

android.permission.READ_CELL_BROADCASTS

()

android.permission-group.STORAGE

android.permission.READ_EXTERNAL_STORAGE

(允许程序读取外部存储,如SD卡读文件)

android.permission.WRITE_EXTERNAL_STORAGE

(允许程序写入外部存储,如SD卡上写文件)

效果图

代码分析

  • 基于Android Studio开发环境。
  • 基于RxPermission开源库。
  • 项目的最低sdk版本号(minSdkVersion)必须>=11。
  • 分为以下四种情况分析:

    A)  只有一个运行时权限申请的情况

    B)  同时请求多个权限(合并结果)的情况

    C)  同时请求多个权限(分别获取结果)的情况

    D)  条件触发获取权限(结合RxBinding使用)的情况

使用步骤

一、检查minSdkVersion值

当前项目的app/ build.gradle文件中的minSdkVersion值必须>=11

二、添加依赖开源库

在当前项目的app/ build.gradle文件中的dependencies{}里面添加以下代码:

普通情况【这个暂时不会用】

//运行时权限
compile 'com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar'

项目中使用了RxJava2的的情况【建议使用这个--不论项目中是否使用了RxJava2】

//运行时权限
compile 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
compile 'io.reactivex.rxjava2:rxjava:2.0.2'

如果想要实现条件触发获取权限(结合RxBinding使用)的情况,则还需要依赖RxBinding开源库

compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'

例如,Demo中的依赖配置如下:

三、在AndroidManifest.xml文件中声明权限列表

<!-- ======================授权获取设备ANDROID_ID========================== -->
<!-- 访问电话状态 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 允许程序读取外部存储文件 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 允许程序写入外部存储,如SD卡上写文件 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.CAMERA" />

四、运行时权限申请代码

注意:需要import的相关类如下:

import com.jakewharton.rxbinding2.view.RxView;
import com.tbruyelle.rxpermissions2.Permission;
import com.tbruyelle.rxpermissions2.RxPermissions;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;

4.1 只有一个运行时权限申请的情况

/**只有一个运行时权限申请的情况*/
private void onePermission(){
RxPermissions rxPermissions = new RxPermissions(MainActivity.this); // where this is an Activity instance
rxPermissions.request(Manifest.permission.READ_PHONE_STATE) //权限名称,多个权限之间逗号分隔开
.subscribe(new Consumer<Boolean>() {
@Override
public void accept(Boolean granted) throws Exception {
Log.e(TAG, "{accept}granted=" + granted);//执行顺序——1【多个权限的情况,只有所有的权限均允许的情况下granted==true】
if (granted) { // 在android 6.0之前会默认返回true
// 已经获取权限
Toast.makeText(MainActivity.this, "已经获取权限", Toast.LENGTH_SHORT).show();
String deviceId = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId();//根据不同的手机设备返回IMEI,MEID或者ESN码
Toast.makeText(MainActivity.this, "{accept}deviceId=" + deviceId, Toast.LENGTH_SHORT).show();
} else {
// 未获取权限
Toast.makeText(MainActivity.this, "您没有授权该权限,请在设置中打开授权", Toast.LENGTH_SHORT).show();
}
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Log.e(TAG,"{accept}");//可能是授权异常的情况下的处理
}
}, new Action() {
@Override
public void run() throws Exception {
Log.e(TAG,"{run}");//执行顺序——2
}
});
}

在Activity的onCreate方法中调用即可:

打印的日志如下:

4.2 同时请求多个权限(合并结果)的情况

/**同时请求多个权限(合并结果)的情况*/
private void MultPermission(){
RxPermissions rxPermissions = new RxPermissions(MainActivity.this); // where this is an Activity instance
rxPermissions.request(Manifest.permission.READ_PHONE_STATE,
Manifest.permission.READ_EXTERNAL_STORAGE)//权限名称,多个权限之间逗号分隔开
.subscribe(new Consumer<Boolean>() {
@Override
public void accept(Boolean granted) throws Exception {
Log.e(TAG, "{accept}granted=" + granted);//执行顺序——1【多个权限的情况,只有所有的权限均允许的情况下granted==true】
if (granted) { // 在android 6.0之前会默认返回true
// 已经获取权限
Toast.makeText(MainActivity.this, "已经获取权限", Toast.LENGTH_SHORT).show();
} else {
// 未获取权限
Toast.makeText(MainActivity.this, "您没有授权该权限,请在设置中打开授权", Toast.LENGTH_SHORT).show();
}
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
Log.e(TAG,"{accept}");//可能是授权异常的情况下的处理
}
}, new Action() {
@Override
public void run() throws Exception {
Log.e(TAG,"{run}");//执行顺序——2
}
});
}

在Activity的onCreate方法中调用即可:

打印的日志如下:

4.3 同时请求多个权限(分别获取结果)的情况

/**同时请求多个权限(分别获取结果)的情况*/
private void MultPermission2(){
RxPermissions rxPermissions = new RxPermissions(MainActivity.this); // where this is an Activity instance
rxPermissions.requestEach(Manifest.permission.READ_PHONE_STATE,
Manifest.permission.READ_EXTERNAL_STORAGE)//权限名称,多个权限之间逗号分隔开
.subscribe(new Consumer<Permission>(){
@Override
public void accept(Permission permission) throws Exception {
Log.e(TAG, "{accept}permission.name=" + permission.name);
Log.e(TAG, "{accept}permission.granted=" + permission.granted);
if(permission.name.equals(Manifest.permission.READ_PHONE_STATE) && permission.granted){
// 已经获取权限
Toast.makeText(MainActivity.this, "已经获取权限", Toast.LENGTH_SHORT).show();
String deviceId = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId();//根据不同的手机设备返回IMEI,MEID或者ESN码
Toast.makeText(MainActivity.this, "{accept}deviceId=" + deviceId, Toast.LENGTH_SHORT).show();
}
}
});
}

在Activity的onCreate方法中调用即可:

打印的日志如下:

4.4 条件触发获取权限(结合RxBinding使用)的情况

/**条件触发获取权限(结合RxBinding使用)的情况*/
private void clickPermission(View view){
RxPermissions rxPermissions = new RxPermissions(MainActivity.this); // where this is an Activity instance
RxView.clicks(view)
.compose(rxPermissions.ensure(Manifest.permission.CAMERA))
.subscribe(new Consumer<Boolean>() {
@Override
public void accept(Boolean granted) {
Log.e(TAG, "{accept}granted=" + granted);//【多个权限的情况,只有所有的权限均允许的情况下granted==true】
if (granted) { // 在android 6.0之前会默认返回true
// 已经获取权限
Toast.makeText(MainActivity.this, "已经获取CAMERA权限", Toast.LENGTH_SHORT).show();
} else {
// 未获取权限
Toast.makeText(MainActivity.this, "您没有授权该权限,请在设置中打开授权", Toast.LENGTH_SHORT).show();
}
}
});
}

在activity的onCreate方法中调用:

打印的日志如下:

其中,点击事件对应的控件如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.why.project.runtime.MainActivity"> <Button
android:id="@+id/btn_getpermission"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="手动获取权限"
android:layout_centerInParent="true"/>
</RelativeLayout>

4.5、跳转到应用权限设置界面(参考《XXPermissions》)【待完善】

package com.why.project.runtime;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings; /**
* Created by HaiyuKing
* Used 打开应用权限设置界面
* 参考资料:https://github.com/getActivity/XXPermissions
*/ public class PermissionSettingPage {
private static final String MARK = Build.MANUFACTURER.toLowerCase(); /**
* 跳转到应用权限设置页面
*
* @param context 上下文对象
* @param newTask 是否使用新的任务栈启动
*/
static void start(Context context, boolean newTask) { Intent intent;
if (MARK.contains("huawei")) {
intent = huawei(context);
} else if (MARK.contains("xiaomi")) {
intent = xiaomi(context);
} else if (MARK.contains("oppo")) {
intent = oppo(context);
} else if (MARK.contains("vivo")) {
intent = vivo(context);
} else if (MARK.contains("meizu")) {
intent = meizu(context);
} else {
intent = google(context);
} if (newTask) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
} try {
context.startActivity(intent);
} catch (Exception e) {
intent = google(context);
context.startActivity(intent);
}
} private static Intent google(Context context) {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", context.getPackageName(), null));
return intent;
} private static Intent huawei(Context context) {
Intent intent = new Intent();
intent.putExtra("packageName", context.getPackageName());
intent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity"));
if (hasIntent(context, intent)) return intent;
intent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.addviewmonitor.AddViewMonitorActivity"));
if (hasIntent(context, intent)) return intent;
intent.setComponent(new ComponentName("com.huawei.systemmanager", "com.huawei.notificationmanager.ui.NotificationManagmentActivity"));
return intent;
} private static Intent xiaomi(Context context) {
Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
intent.putExtra("extra_pkgname", context.getPackageName());
if (hasIntent(context, intent)) return intent; intent.setPackage("com.miui.securitycenter");
if (hasIntent(context, intent)) return intent; intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
if (hasIntent(context, intent)) return intent; intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
return intent;
} private static Intent oppo(Context context) {
Intent intent = new Intent();
intent.putExtra("packageName", context.getPackageName());
intent.setClassName("com.color.safecenter", "com.color.safecenter.permission.floatwindow.FloatWindowListActivity");
if (hasIntent(context, intent)) return intent; intent.setClassName("com.coloros.safecenter", "com.coloros.safecenter.sysfloatwindow.FloatWindowListActivity");
if (hasIntent(context, intent)) return intent; intent.setClassName("com.oppo.safe", "com.oppo.safe.permission.PermissionAppListActivity");
return intent;
} private static Intent vivo(Context context) {
Intent intent = new Intent();
intent.setClassName("com.iqoo.secure", "com.iqoo.secure.ui.phoneoptimize.FloatWindowManager");
intent.putExtra("packagename", context.getPackageName());
if (hasIntent(context, intent)) return intent; intent.setComponent(new ComponentName("com.iqoo.secure", "com.iqoo.secure.safeguard.SoftPermissionDetailActivity"));
return intent;
} private static Intent meizu(Context context) {
Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
intent.putExtra("packageName", context.getPackageName());
intent.setComponent(new ComponentName("com.meizu.safe", "com.meizu.safe.security.AppSecActivity"));
return intent;
} private static boolean hasIntent(Context context, Intent intent) {
return context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0;
}
}

PermissionSettingPage.java

        findViewById(R.id.btn_opensetting).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//打开应用权限设置界面
PermissionSettingPage.start(MainActivity.this,false);
}
});

混淆配置

参考资料

聊一聊 Android 6.0 的运行时权限

Android权限管理之Android 6.0运行时权限及解决办法

Android 6.0 运行时权限处理完全解析

Manifest.permission

android权限大全

RxPermissions开源库github地址

XXPermissions

项目demo下载地址

https://github.com/haiyuKing/AndroidRuntimePrivilegeDemo

Android6.0运行时权限(基于RxPermission开源库)的更多相关文章

  1. Android6.0运行时权限管理

    自从Android6.0发布以来,在权限上做出了很大的变动,不再是之前的只要在manifest设置就可以任意获取权限,而是更加的注重用户的隐私和体验,不会再强迫用户因拒绝不该拥有的权限而导致的无法安装 ...

  2. Android开发学习之路-Android6.0运行时权限

    在Android6.0以后开始,对于部分敏感的“危险”权限,需要在应用运行时向用户申请,只有用户允许的情况下这个权限才会被授予给应用.这对于用户来说,无疑是一个提升安全性的做法.那么对于开发者,应该怎 ...

  3. Android6.0运行时权限的处理Demo

    MainActivity.java package com.loaderman.permissionsdemo; import android.Manifest; import android.con ...

  4. Android权限管理之Android 6.0运行时权限及解决办法

    前言: 今天还是围绕着最近面试的一个热门话题Android 6.0权限适配来总结学习,其实Android 6.0权限适配我们公司是在今年5月份才开始做,算是比较晚的吧,不过现在Android 6.0以 ...

  5. Android 6.0+ 运行时权限

    1.权限被分为了普通和危险两种 2.打电话的Demo import android.Manifest; import android.app.Activity; import android.cont ...

  6. Android8.0运行时权限策略变化和适配方案

    版权声明:转载必须注明本文转自严振杰的博客:http://blog.yanzhenjie.comAndroid8.0也就是Android O即将要发布了,有很多新特性,目前我们可以通过AndroidS ...

  7. Android6.0执行时权限解析,RxPermissions的使用,自己封装一套权限框架

    Android6.0执行时权限解析,RxPermissions的使用.自己封装一套权限框架 在Android6.0中,新添加了一个执行时的权限,我相信非常多人都已经知道了.预计也知道怎么用了,这篇博客 ...

  8. Android 6.0运行时权限第三方库的使用-----RxPermissions

    运行时权限的讲解在前一篇博客已经算是说的比较清楚了,这里就不说了,如果对6.0这个新特性不是很了解的朋友建议先看看(地址:http://blog.csdn.net/qq_33923079/articl ...

  9. Android数据存储之Android 6.0运行时权限下文件存储的思考

    前言: 在我们做App开发的过程中基本上都会用到文件存储,所以文件存储对于我们来说是相当熟悉了,不过自从Android 6.0发布之后,基于运行时权限机制访问外置sdcard是需要动态申请权限,所以以 ...

随机推荐

  1. golang string和[]byte的对比

    golang string和[]byte的对比 为啥string和[]byte类型转换需要一定的代价?为啥内置函数copy会有一种特殊情况copy(dst []byte, src string) in ...

  2. NOI前的考试日志

    4.14 网络流专项测试 先看T1,不会,看T2,仙人掌???wtf??弃疗.看T3,貌似最可做了,然后开始刚,刚了30min无果,打了50分暴力,然后接着去看T1,把序列差分了一下,推了会式子,发现 ...

  3. 【线段树】Bzoj1230 [Usaco2008 Nov]lites 开关灯

    Description Farmer John尝试通过和奶牛们玩益智玩具来保持他的奶牛们思维敏捷. 其中一个大型玩具是牛栏中的灯. N (2 <= N <= 100,000) 头奶牛中的每 ...

  4. Java开源生鲜电商平台-性能优化以及服务器优化的设计与架构(源码可下载)

    Java开源生鲜电商平台-性能优化以及服务器优化的设计与架构(源码可下载) 说明:Java开源生鲜电商平台-性能优化以及服务器优化的设计与架构,我采用以下三种维度来讲解 1.  代码层面. 2.  数 ...

  5. 深入剖析最新IE0day漏洞

    在2018年4月下旬,我们使用沙箱发现了IE0day漏洞;自从在野外发现上一个样本(CVE-2016-0189)已经有两年多了.从许多方面来看,这个特别的漏洞及其后续的开发比较有趣.下一篇文章将分析最 ...

  6. SAP GUI个性化设置

    大概从GUI730开始,GUI品牌化一直不被默认支持,在GUI设置选项里处于灰色状态,如下图: 不过用户还是可以修改注册表的方式来进行修改,让它可以设置! 首先运行Regedit,在目录:HKEY_L ...

  7. 基于 HTML5 的 WebGL 3D 版俄罗斯方块

    前言 摘要:2D 的俄罗斯方块已经被人玩烂了,突发奇想就做了个 3D 的游戏机,用来玩俄罗斯方块...实现的基本想法是先在 2D 上实现俄罗斯方块小游戏,然后使用 3D 建模功能创建一个 3D 街机模 ...

  8. Protocol Buffers(2):编码与解码

    目录 Message Structure 解码代码一窥 varint Protobuf中的整数和浮点数 Length-delimited相关类型 小结 参考 博客:blog.shinelee.me | ...

  9. jenkins + supervisor + ansible 实现netcore程序的多机一键部署

    上一篇我们简单的说到了使用jenkins+supervisor实现了一个单机版的多副本部署,但是在更多的场景下还是需要netcore程序的多机一键部署,那么多 机器间如何分发呢? 肯定不能使用scp这 ...

  10. 18 章 CSS 链接、光标、 DHTML 、缩放

    1.CSS 中链接的使用 2.CSS 中光标的使用 3.CSS 中 DHTML 的使用 4.CSS 中缩放的使用 1 18 8. .1 1 S CSS  中 链接的使用 超链接伪类属性 a:link ...