精华内容
下载资源
问答
  • 腺性膀胱炎有两种形式:典型形式和肠道形式。 它们在组织学,发病率,诊断难度以及可能与膀胱腺癌关联方面有所不同。 通过仔细分析膀胱内镜切除术产生切屑,可以对组织学进行诊断。 这种罕见病理通过内窥镜...
  • 什么是b2b

    2020-12-07 09:51:51
    电子商务现代B2B marketing的一具体主要的表现形式。网商通过它将企业内部网,通过B2B网站与客户紧密结合起来,通过网络的快速反应,为客户提供更好的服务,从而促进企业的业务发展 B2B平台中的两个B均代表...

    B2B电子商务平台是指企业对企业之间的营销关系。电子商务是现代B2B marketing的一种具体主要的表现形式。网商通过它将企业内部网,通过B2B网站与客户紧密结合起来,通过网络的快速反应,为客户提供更好的服务,从而促进企业的业务发展

    B2B平台中的两个B均代表Business,“2”则是英语“two”的谐音,代表“to”。因此一般来说我们把B2B仍然按照英文的读音“B-to-B”来念.

    展开全文
  • 静态壁纸一张图片,动态壁纸以动画为表现形式,有可以对用户操作作出反应。二者表现形式看似差异很大,但是二者本质统一: 它们都以一个Service形式运行在系统后台,并在一个类型为TYPE_WALLPAPER...

    一、什么是Android壁纸?

    Android中,壁纸分为动态壁纸和静态壁纸两种。静态壁纸是一张图片,动态壁纸是以动画为表现形式,有的可以对用户的操作作出反应。二者表现形式看似差异很大,但是二者的本质是统一的: 它们都以一个Service的形式运行在系统后台,并在一个类型为TYPE_WALLPAPER的窗口上绘制内容。 实质上,静态壁纸是一种特殊的动态壁纸。

    Android壁纸管理的三个层次:

    1. WallpaperService和Engine(壁纸的实现原理层次):壁纸的绘制由WallpaperService控制,继承和定制WallpaperEngine是进行壁纸开发的第一步。Engine是WallpaperService的一个内部类,实现了壁纸窗口的创建以及Surface的维护工作。同时Engine还提供了可供子类重写的一系列回调,用于通知开发者壁纸的生命周期、Surface状态变化以及对用户的输入事件作出回应。
    2. WallpaperManagerService(对壁纸的管理方式层次):用于管理壁纸的运行与切换,并通过WallpaperManager类向外界提供操作壁纸的接口。切换壁纸时会取消当前壁纸的WallpaperService的绑定,然后启动新的WallpaperService。Engine类进行窗口创建的时候使用的窗口令牌也是由WallpaperManagerService提供。
    3. WindowManagerService(对壁纸窗口的管理形式):用于计算壁纸窗口的Z轴排序、可见性以及为壁纸应用动画。壁纸窗口的Z序会根据FLAG_SHOW_WALLPAPER标记在其他窗口的LayoutParams.flags中的存在情况而不断调整,相对于其他窗口来说较为不稳定。

    二、动态壁纸

    1.动态壁纸的启动流程

    在这里插入图片描述

    启动动态壁纸可以通过调用WallpaperManagerService.setWallpaperComponent()方法来完成,这个方法的步骤如下:

    1)首先调用mWallpaperMap.get(UserId)来获取壁纸的运行信息。

    Q:为什么需要根据UserID来划分壁纸的运行信息?

    A:WallpaperManagerService支持多用户机制,设备上的每个用户都可以设置自己的壁纸。mWallpaperMap为每个用户保存了一个WallpaperData实例,其中保存了和壁纸状态相关的运行信息:WallpaperService的ComponentName、ServiceConnection等。当发生用户切换的时候,获取到相应用户的WallpaperData,然后获取ComponentName,这样就可以重新启动用户的壁纸。

    2)调用bindWallpaperComponentLocked方法,启动新壁纸的WallpaperService。

    a、认证服务资格

    这个过程首先会对服务进行一系列认证,确认是一个壁纸服务之后才会启动WallpaperService,检查的内容如下:

    1. 服务必须以android.permission.BIND_WALLPAPER作为其访问权限。这个访问权限是一个签名级的访问权限,以免该服务被意外的应用程序启动
    2. 服务必须被声明可以处理android.service.wallpaper.WallpaperService这个Action,因为WallpaperManagerService会使用这个Action对这个服务进行绑定
    3. 服务必须在AndroidManifest.xml中提供android.service.wallpaper的meta-data,用来提供动态壁纸的开发者、缩略图以及描述文字

    服务满足条件之后,就会着手启动目标服务并绑定,步骤如下:

    1. 创建WallpaperConnection,其实现了ServiceConnection接口用于监听和WallpaperService之间的连接状态,同时还实现了IWallpaperConnection.stub,支持跨进程通信。服务绑定成功之后,会在onServiceConnected调用中被发送给WallpaperService,成为WallpaperService和WallpaperManagerService之间通信的桥梁。

    2. 调用mContext.bindServiceAsUser启动指定的服务。之后的流程会在WallpaperConnection.onServiceConnected回调中完成。

    3. 新的壁纸服务启动之后,销毁旧的壁纸服务

    4. 将新的壁纸服务的信息保存到WallpaperData中

      在WallpaperData中会有一个lastDiedTime属性,描述壁纸服务的存活时间,如果小于一定的数值就会认为这个壁纸服务不可靠从而选择默认壁纸。

    5. 向WindowManagerService申请注册一个WALLPAPER类型的窗口令牌,其会再onServiceConnected之后被传递给WallpaperService作为添加窗口的令牌

    b、传递创建窗口所需信息

    仅仅将指定的壁纸服务启动起来还不能让壁纸显示出来,因为还没有窗口令牌而无法添加窗口。所以这后半部流程会在WallpaperConnection的onServiceConnected方法回调中进行。

    在WallpaperService的onBind方法中会返回一个IWallpaperServiceWrapper实例。这个类继承了IWallpaperService.stub。保存了Wallpaper的实例,同时也实现了唯一的一个接口attach()。

    WallpaperManagerService会在WallpaperConnection.onServiceConnected方法中收到回调,然后会进行以下三步:

    1. 将WallpaperService传回的IWallpaperService接口保存为mService
    2. 绑定壁纸服务,调用attachServiceLocked方法,这个方法会调用IWallpaperService.attach方法来传递壁纸服务创建窗口的信息
    3. saveSettingLocked,保存壁纸运行状态到文件系统中

    其中IWallpaperService.attach方法中的参数意义如下:

    1. conn:WallpaperConnection。继承自IWallpaperConnection,只提供了两个接口定义:attachEngine和engineShown。

      Q:为什么有了WallpaperManager这个对外界的标准接口还需要WallpaperConnection?

      A:attachEngine和engineShown只有WallpaperService才用得到,并且是与WallpaperManagerService之间底层且私密的交流,不适合放在通用接口之中。WallpaperService只是一个承载壁纸运行的容器,真正实现壁纸的核心为Engine类,当WallpaperService创建完Engine之后,就会通过attachEngine方法将Engine对象引用交给WallpaperManagerService。

    2. conn.Token:向WindowManagerService申请的令牌

    3. WindowManager.LayoutParams.TYPE_WALLPAPER:指明需要添加TYPE_WALLPAPER类型的窗口。另一种情况是壁纸预览的时候,会使用TYPE_APPLICATION_MEDIA_OVERLAY类型创建窗口,此时壁纸服务创建的窗口将会以子窗口的形式衬在LivePicker的窗口之下。

    4. isPreview:实际作为壁纸的时候是false,壁纸预览的时候是true

    c、创建Engine

    调用IWallpaperService.attach是WallpaperManagerService与WallpaperService的第一次接触。该方法会创建IWallpaperEngineWrapper,其继承自IWallpaperEngine.stub,支持跨进程调用。在其中会创建并封装Engine的实例。

    IWallpaperEngineWrapper在attach方法中只创建了对象,但是没有将其赋给任何变量。这个实例对象的保持依靠的是其内部的HandlerCaller以及HandlerCaller中的Handler的外部引用持有来实现的。Handler是HandlerCaller的内部类,其中包含了HandlerCaller的隐式引用,而HandlerCaller又持有IWallpaperEngineWrapper的引用,所以在内部Handler处理DO_ATTACH消息之前,IWallpaperEngineWrapper不会被垃圾回收器回收。
    在这里插入图片描述
    在IWallpaperEngineWrapper中创建的HandlerCaller是Handler的一个封装,比Handler额外提供一个executeOrSendMessage方法,在HandlerCaller所在线程执行该方法的时候会使处理函数马上执行,在其他线程中与Handler.sendMessage一样。

    这个HandlerCaller是一个重要的线程调度器,所有与壁纸相关的操作都会以消息的形式发送给mCaller,然后在IWallpaperEngineWrapper的executeMessage方法中处理,这些操作也就转移到了mCaller所在线程。默认情况下mCaller运行在主线程中。

    然后就是处理DO_ATTACH消息,会进行如下步骤:

    1. mConnection.attachEngine:把IWallpaperEngineWrapper实例传递给WallpaperConnnection,这之后就不用担心实例被回收了。

    2. 通过onCreateEngine创建一个Engine。这个方法由开发者自己实现

    3. 将新建的Engine添加到WallpaperService.mActiveEngine中。

      当使用壁纸预览的时候,WallpaperService仍然只有一个,但是其内部会变成两个Engine。这也说明了WallpaperService仅仅是提供壁纸运行的场所,真正壁纸的实现是Engine。

    Engine创建完成之后会通过Engine.attach来初始化Engine,步骤如下:

    1. 设置必要的信息,包括:
      • mSurfaceHolder:BaseSurfaceHolder类型的内部类实例,可以通过它来定制需要的Surface类型
      • 获取WindowSession,用于与WMS通信
      • mWindow.setSession(mSession):用于接受WMS的回调
      • 设置监听屏幕状态,保证屏幕关闭之后停止动画渲染节省电量
    2. 调用Engine.onCreate,重写的子类一般需要重写该方法来修改mSurfaceHolder的属性。此时还没有创建窗口,修改的属性会在窗口创建时生效。
    3. updateSurface:根据SurfaceHolder的属性创建窗口以及Surface,并进行壁纸的第一次绘制。

    2、在动态壁纸创建中使用到的Binder通信对象和数据结构

    使用到的Binder对象:

    1. IWallpaperService:实现在壁纸服务进程中,唯一提供的方法为attach,用于在壁纸服务启动之后接受窗口创建的信息,完成壁纸初始化工作。
    2. WallpaperConnection:实现在WallpaperManagerService中,通过IWallpaperService.attach传递给了IWallpaperEngineWrapper。WallpaperService通过attachEngine将IWallpaperEngineWrapper实例传递给WallpaperManagerService,通过engineShown将壁纸显示完成情况传递给WallpaperManangerService
    3. IWallpaperEngineWrapper:实现在壁纸进程中,是WallpaperManagerService对Engine进行请求或者设置的唯一接口。

    Q:WallpaperService和WallpaperManagerService之间除了IWallpaperService之外为什么要多加一个IWallpaperEngineWrapper?

    A:首先,从WallpaperManagerService的角度来看,IWallpaperService代表的是WallpaperService,是壁纸实现的容器。而IWallpaperEngineWrapper代表是Engine,是壁纸的真正实现。

    其次,WallpaperService中可以运行多个Engine实例,但是WallpaperManagerService或者LivePicker关注的只是一个确定的Engine实例,而不是WallpaperService中的所有Engine,从这个角度来说,也是简化实现的一种方式。

    使用到的数据结构:

    1. WallpaperInfo:存储壁纸的开发者、缩略图以及描述信息
    2. WallpaperConnection:WallpaperService和WallpaperManagerService进行通信的渠道、保存壁纸服务的重要运行时信息。
    3. WallpaperData:一个壁纸在WallpaperManagerService中可能用到的所有信息:ComponentName、WallpaperConnection、壁纸的启动时间。

    3、理解Engine.updateSurface

    该方法中更新surface的条件有:

    • 窗口尚未创建
    • Surface尚未创建
    • mSurfaceHolder中的像素格式发生了变化
    • 尺寸发生了变化
    • Surface的类型发生了变化
    • 窗口的flag发生了变化

    这些条件只要有一个发生了变化就要更新Surface,SurfaceHolder允许修改的属性有:

    • 尺寸
      1. setFixedSize(int w,int h):只允许ImageWallpaper使用
      2. setSizeFromayout()——使用默认的宽高,恢复到MATCH_PARENT
    • 像素格式(创建之后就不再修改)
    • 内存类型(创建之后就不再修改)
    • 是否防止屏幕休眠(对壁纸不适用,修改会报异常)

    完成Surface更新的原理:

    • 倘若窗口尚未创建,则通过WMS.addWondow完成窗口的创建
    • 通过WMS.relayoutWindow对窗口进行重新布局。如果窗口目前还没有一块可用的Surface(刚刚完成创建工作),Engine将会拥有一块Surface。而存储在LayoutParams中与Surface或窗口相关的参数都会被WMS接纳并修改Surface的属性(尺寸、像素格式、内存类型以及窗口的flags)。但是最终的决定权还是在WMS,updateSurface会将WMS的布局结果强制设置给SurfaceHolder。

    完成Surface更新以后,updateSurface会触发SurfaceHolder的回调通知所有的SurfaceHolder的使用者(开发者自定义的Engine)。这些回调包括:

    • onSurfaceCreated
    • onSurfaceChanged
    • onSurfaceRedrawNeeded

    另外还有一个在壁纸销毁时触发的回调:onSurfaceDestroyed。这些回调给开发者提供了Surface的生命周期。

    对Engine的回调做个总结:

    1. onCreate:发生在Engine.attach方法中,表示Engine生命周期的开始
    2. onDestroy:发生在Engine.detach方法中,表示Engine生命周期的结束
    3. onSurfaceCreated / onSurfaceChanged / onSurfaceDestroyed / onSurfaceRedraw-Needed:发生在updateSurface和Engine.detach中,向开发者描述了Surface的生命周期
    4. onVisibilityChanged:通知当前壁纸的可见性发生了变化。这里的可见性指的是是否以壁纸窗口作为当前窗口的背景。因为使窗口从不可见到可见的花销巨大,会经历窗口的重新布局、Surface的创建、等待窗口完成第一次绘制等等,所以Android索性使窗口始终可见,只是在不需要显示壁纸的时候将它放在所有窗口的底部,使用户看不到(调用Surface.hide将壁纸的Surface隐藏,需要的时候调用Surface.show,这样Surface的内容并没有被清除,不需要重绘Engine对象),但是如果壁纸动画还在运行的话,仍然会消耗资源,所以当onVisibilityChanged报告壁纸可见性为false的时候,Engine必须停止一切消耗资源的操作。
    5. onTouchEvent:用于处理用户的触摸事件。开发者可以通过Engine.setTouchEventEnabled方法在壁纸窗口的flags中增加或者删除FLAG_NOT_TOUCHABLE标记,从而设置壁纸窗口是否可以接受用户的输入事件。InputDispatcher查找触摸事件的派发目标是根据窗口的Z序从上向下遍历查找触摸事件落在其上的第一个窗口。但是InputDispatcher为壁纸窗口做了特殊化处理,InputDispatcher::findTouchedWindowTargetLocked找到触摸目标之后会判断该窗口是否要显示壁纸,如果要的话就把触摸事件同时发送给两个窗口。
    6. onOffsetChanged:用于通知Engine对象需要对壁纸所绘制的内容进行偏移,发生于WallpaperManager.setWallpaperOffsets()。当要求显示壁纸的窗口内容发生滚动的时候,可能希望壁纸的内容也随之偏移。这个偏移量的表现形式没有限定,可以自由发挥。
    7. onCommand:用于处理WallpaperManager.sendWallpaperCommand发送的命令,同样可以自定义命令,提供了一种通信方式。
    8. onDesiredSizeChanged:显示壁纸的窗口调用WallpaperManager.suggestDediredDimensions指明它所期望壁纸的尺寸大小,可以配合setWallpaperOffsets来实现壁纸与内容的同步滚动。

    4、壁纸的销毁

    壁纸销毁的工作就是把壁纸创建的过程反过来进行一遍:

    1. 执行IWallpaperEngine.destroy,会触发onSurfaceDestroy回调通知开发者销毁壁纸运行期间所使用的资源,并移除壁纸窗口
    2. unbindService:终止WallpaperManagerService对壁纸服务的绑定
    3. 注销用来添加壁纸窗口的令牌

    在WallpaperService中则是:

    1. 从mActiveEngines中将当前Engine删除
    2. 调用Engine.detach方法
      1. reportSurfaceDestroyed:触发onSurfaceDestroyed回调
      2. onDestroy:触发onDestroy回调

    三、静态壁纸——ImageWallpaper

    所谓静态壁纸,就是SystemUI中的一个名为ImageWallpaper的特殊动态壁纸而已,实现的架构就是动态壁纸的架构,只不过其内容是一张静态的图片而已。之所以与普通的动态壁纸区分,是因为Android为ImageWallpaper提供了很多方便的API,使得可以方便地将静态图片设置为壁纸。

    静态壁纸的设置

    旧版本API中(29以前)):

    静态壁纸的服务位于SystemUI中,ImageWallpaper继承自WallpaperService,在其onCreateEngine方法中创建了一个继承自Engine的WallpaperEngine。

    WallpaperEngine中有一个Bitmap类型的成员变量mBackground,这就是作为壁纸的位图。对静态壁纸进行重绘的时候,就会调用drawFrameLocked方法将mBackground绘制到壁纸窗口的Surface上。
    在这里插入图片描述

    静态壁纸的设置不像动态壁纸那样需要签名级系统权限,只需要有android.permission.SET_WALLPAPER权限就可以通过WallpaperManager的相关接口设置静态壁纸。WallpaperManager提供了setBitmap、setResource以及setStream三个方法进行静态壁纸的设置。以setBitmap为例,会进行如下步骤:

    1. 通过WallpaperManagerService.setWallpaper方法获取一个文件描述符,这个文件的位置为/data/(secure)/system//wallpaper文件夹。
    2. 将Bitmap写入这个文件描述符所指向的文件中。

    通过WallpaperManager设置完壁纸的文件位置之后,ImageWallpaper怎么知道壁纸文件路径发生了变化呢?Android提供了一个WallpaperObserver类(WallpaperManagerService.WallpaperObserver),该类继承自FileObserver,这个类是Android提供的一个工具类,用于监听文件系统中发生的文件创建、删除与修改事件。FileObserver可以监听一个文件夹或者是一个文件,当它发生变化的时候,onEvent回调就会根据参数获取到发生这些动作的文件路径。在壁纸监听中主要是CLOSE_WRITE事件,除此之外还有DELETE、DELETE_SELF事件,这两个事件的原因是因为位图被保存在文件系统中,保护非常弱,需要处理其被删除的情况。

    在WallpaperObserver的onEvent方法中最终调用的是bindWallpaperComponentLocked方法,这也就意味着在壁纸文件位置重新设置之后,不会通知原有的DrawableEngine进行更新和重绘,而是销毁原有的DrawableEngine,创建一个新的进行壁纸绘制。

    DrawableEngine设置壁纸的步骤如下:

    1. 调用forgetLoadedWallpaper,将mDefaultWallpaper和mWallpaper设置成null。这两个变量一个存储系统默认的壁纸,另一个存储现有的设置壁纸。
    2. 调用WallpaperManager.getBitmap获取作为壁纸的位图,在这个方法里又会调用Globals.peekWallpaperBitmap方法。在其中有两套保证静态壁纸稳定运行的机制:缓存以及备用方案。它会尽量返回已经加载的位图,如果失败就会返回默认的位图。
    3. 获取被设置为壁纸的位图使用的是Globals.getCurrentWallpaperLocked方法。这个方法会去前面WallpaperManager设置的文件位置加载位图并设置为壁纸。

    API29中:

    静态壁纸的服务仍然位于SytemUI中,ImageWallpaper继承自WallpaperService,在其onCreateEngine方法中创建的Engine换成了GLEngine:

    class GLEngine extends Engine implements GLWallpaperRenderer.SurfaceProxy, StateListener 
    

    这个类继承自Engine类,同时扩展了GLWallpaperRenderer类的SurfaceProxy接口以及StateListner接口。

    GLWallpaperRenderer类是一个渲染器,负责发送OpenGl调用来渲染一个帧。而它的SurfaceProxy接口则是拥有SurfaceHolder的代理。通过这个接口能够将OpenGl的渲染帧发送到Surface上。

    StateListener接口位于StatusBarStateController中,用于接受状态栏状态更新以及Dozing 的状态改变。

    因为这个类的改变,现在静态壁纸的创建流程变成了:
    在这里插入图片描述
    ImageWallpaperRenderer类继承了GLWallpaperRenderer类,在它的构造方法中调用loadBitmap方法进行了Bitmap的加载。

    在这个方法中,首先就是调用了WallpaperManager的getBitmap方法去获取Bitmap。getBitmap中又调用了Wallpaper的静态内部类Globals的peekWallpaperBitmap方法,其同旧版本一样提供了两套保证尽态比值稳定运行的机制:缓存和备用方案。如果缓存为空的话就调用getCurrentWallpaperLocked方法来从文件中获取Bitmap。

    从文件中获取Bitmap的流程相对于老版本没有改变太多,只是不再直接在WallpaperManagerService.getWallpaper方法中调用getWallpaperDir来获取文件路径以及句柄了,而是将这个获取过程移到了WallpaperData的初始化中。而Wallpaper对象又会在loadSetttingLocked方法中被创建。在这个方法中会做这么几件事:

    • 从mWallpaperMap中尝试获取Wallpaper,如果为空开始WallpaperData的初始化(一般刚开机这个肯定为空)
    • 调用migrateFromOld方法对WALLPAPER_CROP,WALLPAPER进行赋值,这两个参数用来指定WallpaperData初始化时的inputFileName,cropFileName。
    • 获取完wallpaper之后放入mWallpaperMap中,如果cropWallpaper不存在,就会调用generateCrop方法对原始的Wallpaper进行裁剪,如果连wallpaper文件都不存在就会采用调用默认壁纸的逻辑
    • 获取完Wallpaper,且确认存在,就会读取xml文件中对壁纸的设定,设置wallpaper中的各种参数

    loadingSettingLocked方法会在很多地方被调用,但是这里只关注三个地方(我认为。。):

    • WalpaperManagerService的initialize方法,在WMS进行初始化的时候对壁纸的文件内容进行填充是有必要的
    • getWallpaperSafeLocked方法。有的时候可能WallpaperMap并不会拥有用户的数据——例如在用户切换的时候,在这个时候用户切换的obverser可能还没有收到用户切换的事件。当我们不在意这些顺序只想要更新数据的时候就可以调用这个安全的方法来更新。更新后的数据将会在用户切换观察者渐渐生效的时候被应用。
    • WallpaperManagerService.WallpaperObserver的onEvent方法。这个类和方法的作用和老版本中一样。

    接收到返回的文件句柄之后,就会在Globals类中使用BitmapFactory.decodeFileDescriptor进行解码生成Bitmap文件然后一路返回到ImageWallpaperRender中。然后就会调用forgetLoadedWallpaper方法删除掉所有上一张加载的壁纸的内部引用。调用这个方法对于那些只想短暂拥有壁纸,想要减少内存消耗的应用是有用的。但是在调用这个方法之后想要调用这张壁纸又要从硬盘中重新读取。

    都结束之后就将Surface的大小调整为Bitmap的宽高。至此Bitmap的获取就结束了。

    获取完Bitmap,之后的壁纸渲染工作GLEngine都会交给GLWallpaperRenderer的各种方法,由其发送OpenGL调用来渲染,并通过SurfaceProxy来对SurfaceHolder进行处理,来将壁纸渲染到Surface上。

    设置壁纸的过程和老版本基本一致。

    四、WMS对壁纸窗口的特殊处理

    WMS对于壁纸窗口的特殊处理体现在三个方面:

    • 壁纸窗口的Z序——衬在FALG_SHOW_WALLPAPER标记的窗口之下
    • 壁纸窗口的可见性——动态壁纸资源相关
    • 壁纸窗口的动画——跟随标记窗口一起动画

    1、壁纸窗口的Z序

    壁纸的存在意义就是给其他窗口提供背景,一个窗口希望壁纸作为其壁纸的时候,就可以将FLAG_SHOW_WALLPAPER加入到它的flag中。当WMS检测到这个标记,就会把壁纸窗口衬在这个窗口之下。所以壁纸窗口的Z序与声明FLAG_SHOW_WALLPAPER标记的窗口息息相关。

    声明FLAG_SHOW_WALLPAPER的窗口在WMS中被称为壁纸目标,存储在名为mWallpaperTarget的WindowState类型成员中。确定壁纸窗口Z序的核心工作就是寻找这个目标窗口。

    旧版本API中:

    这个目标窗口存在三种情况:

    1. 当前系统中只有一个窗口声明FLAG_SHOW_WALLPAPER标记,且是用户可见的,那么这个窗口就是目标窗口
    2. 当前系统中有多个窗口声明标记,此时Z序最高的窗口为目标窗口
    3. 当前系统有多个窗口声明标记,同时它们还在进行窗口动画,此时要做相对复杂的选择策略
      在这里插入图片描述

    确定壁纸目标的流程的原则是找到窗口列表中第一个声明FLAG_SHOW_WALLPAPER、并且对用户可见的窗口。如果候选窗口是旧的壁纸目标,且在动画过程中,那么就继续向下查找可能成为壁纸目标的窗口。

    在这个过程中,候选的壁纸目标将会被保存到foundW中,其在窗口列表中的索引将会被保存到foundI中,这两个变量用于确定壁纸窗口的插入位置。

    在下一个阶段中,如果新旧目标都在动画过程中,那么就要设置mLowerWallpaperTarget(Z序较小)和mUpperWallpaperTarget(Z序较大)。这个阶段主要是为了处理新旧目标都在动画过程中这种情况的。这个时候foundW和foundI会被指向Lower的目标(因为壁纸窗口最终是被衬在目标窗口的下面,肯定插入位置要在新的目标的下面,最终壁纸窗口会在foundI指向的目标的下面)。而当新的窗口目标在旧的窗口目标上面的时候(图中棕色块),真正的壁纸目标mWallpaperTarget可能会和壁纸窗口的实际位置发生分离(mWallpaperTarget指向Upper,foundI指向Lower),最终壁纸窗口的插入位置和壁纸目标之间会隔着一层Lower。不过这种情况很少,一般Lower和Upper都是空的,mWallpaperTarget会被设置为foundW。

    Lower和Upper不为空的时候表示此时同时有两个用户可见的壁纸目标。但是壁纸窗口只有一个,此时WMS的策略为:

    • 壁纸窗口的位置和可见性由Lower确定
    • 壁纸的offset由mWallpaperTarget确定(有可能是Lower【绿色块】,也有可能是Upper【棕色块】)
    • 壁纸的动画谁说了都不算,此时壁纸处于静止状态

    然后就是确定壁纸窗口在窗口列表中的位置。这个过程有几条原则:

    • Activity在执行动画的时候会对自己的Layer进行调整,壁纸窗口作为目标窗口的附属,也要对自己的layer进行调整。但是这只在Lower为null的时候才会执行,因为当不为null的时候,有两个目标都在进行动画,壁纸窗口无法知道使用哪一个目标的layer进行调整,所以这个时候壁纸窗口的layer不会动
    • PhoneWindowManager设置了maxLayer,这是壁纸所能拥有的layer上限(不包含),这个上限与状态栏的layer相同,所以壁纸是不可能覆盖状态栏的
    • 确保壁纸位于layer上限、目标窗口、目标窗口的父窗口以及STARTING窗口之下(这些窗口都紧邻在一起)
    • 如果没有哪个窗口声明FLAG_SHOW_WALLPAPER标记,那么壁纸窗口就会被设置在壁纸列表的底部

    确定完壁纸窗口的位置之后,就要将壁纸窗口移动到指定的位置。一般来说系统中只会存在一个壁纸窗口,但是在切换壁纸的时候,WallpaperManagerService.bindWallpaperComponentLocked的实现会首先启动新壁纸然后再销毁旧壁纸。这样,在一个较短的时间里,系统中可能存在两个不同的壁纸窗口。所以在调整壁纸窗口位置的时候会遍历WallpaperToken中的所有壁纸窗口,然后先将所有壁纸窗口从窗口列表中剥离,然后再按照顺序一个一个插入到foundI所指向的位置上(foundI的值随着插入递减),然后在销毁旧壁纸之后,新壁纸必然会在正确的位置上。

    新版本API中寻找mWallpaperTarget的流程如下图:
    在这里插入图片描述
    首先调用的是DisplayContent的applySurfaceTransaction方法,在这个方法中会调用WallpaperController的adjustWallpaperWindows方法。

    在这个方法中首先会判断当前窗口是不是处于FREEFORM模式,如果是的话,就会将壁纸设置为TopWallpaper(也就是将壁纸窗口本身设置为自己的Target)

    Freeform Windows:允许应用在可以改变大小的窗口中运行,在Android Q中可以在开发者模式中打开(之前需要ADB或者第三方应用来完成)

    img

    然后就会调用DisplayContent的forWindows函数对所有窗口执行mFindWallpaperTargetFunction回调。在这个回调中会执行以下八个条件来寻找目标窗口:

    • 当前的窗口类型为TYPE_WALLPAPER,并且当前没有设置topWallpaper或者设置了重置TopWallpaper选项,将当前窗口设置为TopWallpaper,然后返回false,继续寻找下一个合适窗口。
    • 当前窗口隐藏且不在动画过程中,返回false继续寻找。
    • 当前窗口将会被替换,而且当前的目标窗口属性为空,使用TopWallpaper作为目标窗口,保持这个窗口直到新窗口完全显示出来,这样能避免屏幕闪烁。
    • 执行锁屏时的壁纸逻辑
    • 执行最近任务动画设置壁纸的逻辑
    • 窗口声明了FLAG_SHOW_WINDOW,并且显示在屏幕上(当前处于Visible状态或者是在变为不可见之前执行动画)+是当前的Target+已经绘制完成——设置为Target(使用FindTargetResult类保存)。
    • 当前目标在执行动画,继续往后寻找可能的目标。

    寻找到合适的WallpaperTarget之后(或者没有找到),就会调用updateWallpaperWindowTarget来更新WallpaperTarget,在这里存在两种情况:

    • 找到的Target等于现在的Target || mPrevTarget不为空,且找到的Target等于mPrevTarget
      • mPrev为空(从第一个条件进来):返回
      • mPrev不在动画中(从第二个条件进来):将mPrev置为空,并更新目标壁纸窗口为找到的Target
    • 新旧两个目标都在动画中
      • 新目标requestHide,而旧目标没有——使用旧目标作为Target
      • 都申请隐藏——使用新的
      • 都申请隐藏或者不隐藏,这种情况就和应用的打开/关闭列表有关:使变换逻辑更好地确定正在打开/关闭的应用的壁纸状态
        • 打开列表中有旧的没有新的——使用旧的
        • 关闭列表中有旧的——使用旧的

    这其中使用到了两个WindowController的变量:

    1. mWallpaperTarget:如果不为空,这就是与壁纸相关的当前可见窗口
    2. mPrevWallpaperTarget:如果这变量不为空的话,我们当前就处于从一个壁纸目标动画转换到另一个壁纸目标的过程中,而这个变量保存的就是前一个壁纸目标。

    2、壁纸的可见性

    旧版本的API中:

    首先,壁纸可见的第一个条件就是存在一个壁纸目标(不存在的话就会将壁纸窗口放置到窗口列表的底部)。然后就会调用WMS.isWallpaperVisible来确定壁纸窗口是否可见,这里有三个条件,满足其中一个壁纸窗口就可见:

    1. Lower或者Upper不为null,这表明目前存在新旧两个壁纸目标正在执行动画
    2. wallPaperTarget(不是mWallpaperTarget,是foundW)没有被其他全屏窗口遮挡,即窗口部分或者全部可见
    3. wallpaperTarget属于一个Activity,而这个Activity正在执行动画。Activity执行动画的时候会有layer的调整,所以即使这个时候wallpaperTarget被全屏窗口遮挡,随着layer的调整,壁纸窗口也有可能显示在其他窗口之上,所以此时也要显示壁纸

    isWallpaperVisible的结果会保存在壁纸窗口的WindowState.mWallpaperVisible中。这个设置有如下作用:

    在动画系统中,WindowStateAnimator的prepareSurfaceLocked方法会检查这个变量的值,如果为false,就会调用WindowStateAnimator.hide,进而调用Surface.hide将壁纸隐藏。相反,会调用WindowStateAnimator.showSurfaceRobustlyLocked,进而调用Surface.show来使壁纸窗口重新可见

    然后通过IWindow.dispatchAppVisibility方法将可见性传递给WallpaperService下的Engine对象。其中的mWindow成员将其解释为onVisibilityChanged,Engine根据这一回调重启或者终止画面的绘制。

    所以壁纸的可见性有两层意义:

    • WallpaperService中的Engine对象是否进行壁纸的绘制工作
    • WMS中壁纸窗口的Surface隐藏还是可见

    不同于常规窗口可见性变化时的创建或者销毁,壁纸窗口的可见性只会使Surface显示或者隐藏。这样壁纸窗口的可见性变化效率会非常高,但是其Surface的内存占用会一直都在。

    新版本API中:

    首先,壁纸可见的第一个条件就是存在一个壁纸目标(不存在的话就会将壁纸窗口放置到窗口列表的底部)。然后就会调用WallpaperController.isWallpaperVisible来确定壁纸窗口是否可见,这里有两个条件,满足其中一个壁纸窗口就可见:

    • WallpaperTarget不为空且满足以下三个条件的任意一个:
      • 壁纸目标没有被遮挡
      • 与最近任务一起处于动画过程中
      • 壁纸窗口目标是一个App窗口并且这个窗口自己在执行动画
    • mPrevWallpaperTarget不为空

    isWallpaperVisible的结果保存的位置和作用和旧版本API一样,见上方。

    同时在adjustWallpaperWindows方法中关注的壁纸窗口的可见性的点在于:窗口目前对于SurfaceFlinger是可见的,但是它对于用户而言是否是可见的呢?所以围绕窗口可见性与否对于窗口的偏移和位置做了调整:
    在这里插入图片描述
    首先在adjustWallpaperWindows里面设置留个参数:

    • mLastWallpaperX
    • mLastWallpaperXStep
    • mLastWallpaperY
    • mLastWallpaperYStep
    • mLastWallpaperDisplayOffsetX
    • mLastWallpaperDisplayOffsetY

    这些参数会在之后的updateWallpaperOffset中计算出默认的壁纸偏移,然后调用WindowStateAniamtor的setWallpaperOffset设置。在这里x方向上的偏移会判断一次是否是RTL,使得这个行为适配大多数的桌面:

    我们常用的习惯,称之为 LTR(Left-To-Right),其意为我们的阅读和书写习惯,是从左向右延伸的。而 RTL(Right-To-Left) 则正好相反,它的阅读和使用的习惯都是从右向左,常见使用 RTL 习惯的语言有阿拉伯语、希伯来语等。

    设置完这些参数之后就会调用updateWallpaperTokens方法来更新每个壁纸窗口的WallpaperWindowToken。在这里首先调用的是WallpaperWindowToken中的updateWallpaperWindows方法。在这个方法里会做这些事情:

    • 首先对窗口的可见性进行一次判断,如果窗口可见就取消窗口的隐藏。然后设置该窗口的DisplayContent为LeyoutNeeded,这样就可以对窗口进行重新布局,保证壁纸窗口为正确的大小。
    • 然后从DisplayInfo中取出窗口的宽高信息,对于WindowContainer中的每一个窗口调用WallpaperController的updateWallpaperOffset方法来设置窗口的偏移。
    • 最后调用WindowState的dispatchWallpaperVisibility方法将窗口的当前可见性状态传递给客户端知道。随后的处理和旧版本一致。

    处理完窗口的可见性之后就会调用DisplayContent的assignWindowLayrers方法。在这里传递进去的参数是false,所以在这里将会禁用窗口的布局功能。在这里主要是进行两项工作:

    1. 调用assignChildLayers为子窗口调整Z-Order。在这个函数中会传入getPendingTransaction作为参数,将Layer的变化都保存到mPendingTransation中。其中会调整四种类型窗口的Layers:

      1. mBelowAppWindowsContainer:NonAppWindowContainers类型,保存了所有应该显示到App类窗口的下面的非App类的窗口。layer设置为0
      2. mTackStackContainer:保存了所有与App(Activities)相关的Window。layer设置为1
      3. mAboveAppWindowContainer:NonAppWindowContainer类型,保存了所有应该显示到App类窗口的上面的非App类的窗口,layer设置为2
      4. mImeWindowContainers:NonAppWindowContainer类型,包含了所有IME window Containers。IME窗口的Z-Order依赖于IME目标,所以这个Containers的作用是跟踪所有的IME WindowContainers,以便在需要的时候可以一起移动他们。这里的类型是WindowContainer的子类,去除了屏幕放大的功能,因为IME从来都不会放大。对于IME窗口的处理分为四种情况:
        1. 不处于分屏模式的时候,处理IME非常容易:使IME超过所有目标,这样Containers中的所有子窗口都会在上方
        2. 分屏模式下需要将输入法放在分隔栏之上,而应用程序应该放在分割栏之下
        3. 在IME窗口目标处于动画过程的时候,它的Z-Order有可能与WindowContainer的Z-Order不相等,所以很难确保拥有正确的IME目标,所以在这里会将其放在应用程序Layer智商来保证IME会在所有Transition之上
        4. 在我们没有IME目标的时候,将会将其放到AboveAppWindowContaners中

      处理完这些子窗口的Layer之后就要处理他们的子窗口的layer。在这里是为上方的Containers调用assignChildLayers方法,传入的参数都有Transaction,但是mAboveAppWindowContainers中还要考虑是否要传入mImeWindowContainers:

      mAboveAppWindowContainers.assignChildLayers(t,needAssignIme == true ? mImeWindowsContainers : null)
      
    2. 调用scheduleAimation,这样就会开启动画系统,最终会调用到prepareSurfaces,其中会处理mPendingTransaction中积累的Layer变化的事件。这样就会将Z-Order的变化与Surface的显示与隐藏同步起来。

    3、壁纸窗口的动画

    API29以前:

    默认情况下,目标窗口发生动画的时候,壁纸窗口也会随着目标窗口产生同步的动画。效果实现在WindowStateAnimator.computeShownFrameLocked中。 这个方法有如下几个条件:

    • LowerTarget为null,因为不为null的时候表示有两个壁纸目标在进行动画,这种情况下壁纸窗口无法确定使用哪一个目标的动画变化,所以壁纸窗口将保持不动
    • 设置attachTransformation为壁纸目标窗口的动画变换,其前提是目标窗口动画的getDetachedWallpaper返回false。这表示壁纸窗口在动画过程中扮演目标窗口子窗口的作用。
    • 设置appTransformation为目标窗口所属Activity的动画变换,前提是Activity的getDetachedWallpaper返回为false

    默认情况下壁纸会随着目标窗口进行动画,除非目标窗口的Animation中设置了DetachWallpaper,或者新旧两个目标窗口都在进行动画。

    之后就会将attachTransformation和appTransformation集成到tmpMatrix中,并由此计算出Surface最终的透明度、位置以及DsDx、DsDy、DtDx以及DtDy。

    能够作为其他窗口背景的除了壁纸还有另外一种特殊的背景——Animation.setBackgroundColor设置一个背景色,目前Window Animator只会使用背景色的透明度分量,其他RGB会被忽略。但是如果指定了背景色的窗口是mWallpaperTarget、mLowerWallpaperTarget或者mUpperWallpaperTarget的话,这个Surface就会将壁纸窗口给遮挡起来。所以在WindowAnimator.updateWallpaperLocked中会进行检测,将Surface放在壁纸窗口的下面。

    Android Q中Animation类的setDetachWallpaper方法已经被废弃,其注释说明:

    @deprecated All Window animations are running with detached wallpaper
    

    由此可以看到,现在所有的窗口动画都不会引起壁纸的动画,壁纸窗口不会随着目标窗口的动画而动画了!!

    相对的,现在会为所有动画中的窗口设置AnimationBackground,这个过程如下图所示:
    在这里插入图片描述
    和以前的版本相同,在setAnimationBackground中同样只采用了Alpha分量。在Transaction类的setLayer方法中将mAinmationBackgroundSurface的Layer设置为了Interger.MIN_VALUE。这样这个Surface就不会遮挡其他窗口了。

    五、壁纸服务总结

    Android壁纸使用了系统服务(管理者)+标准Android服务(实现者)的两层架构。当系统希望某些系统级的UI由第三方进行实现与拓展,同时又不希望给予第三方过多的操作窗口的权限时,这两层架构是最佳的选择。系统服务负责提供必要的系统级操作,而标准的Android服务可以在系统服务所规范的框架范围之内实现自由定制。

    展开全文
  • 动作姿势就是其中一个表现形式,然而创意本身不能脱离实际,这里所谓实际并非我们现实实际,而是一假定条件,然而这种假定条件却要建立在力学原理基础上,所以姿势不能只是一味好看而已,它可以夸张,比如...
  • 数据库基本概念 1. 数据库存储方式 计算机数据一般为硬盘存储,在 数据处理时,采用数据库相关...指:数据库中物理数据和逻辑数据的表现形式、物理数据和逻辑数据之间关系映射方式描述。 两种形式:物理...

    数据库基本概念
    1. 数据库的存储方式
    计算机数据一般为硬盘存储,在 数据处理时,采用数据库的相关技术。
    作用:提高了数据的存储效率;
    提高了数据的安全性;
    2. 数据库是什么
    由一批数据构成的有序集合—存放在结构化的数据表中
    数据表之间相互关联,反应客观事物的本质联系
    3. 数据库的存储结构
    是指:数据库中的物理数据和逻辑数据的表现形式、物理数据和逻辑数据之间关系映射方式的描述。
    两种形式:物理数据描述、逻辑数据描述
    二者之间通过数据库系统管理实现转换
    物理数据描述:

    • 位(bit):二进制的一个单位为位,只能取1或0;
    • 字节(byte):8个位为一个字节,可以存放对应ASCII码的一个字符。
    • 字(word):若干字节为一个字,一个组所含的二进制的位数为字长。8位,16位等
    • 块(block): 物理块/物理记录:内存储器和外存储器交换信息的最小单位;256字节、512字节等
    • 卷(volume) :一台输入输出设备所能装载的全部有用的信息
    • 无序存储(unordered):数据记录按照插入的顺序存储

    逻辑数据的描述
    层次一:对客观显示信息世界的描述
    层次二:对数据库管理系统中数据的描述

    层次一:术语

    • 实体(entity):客观现实存在,可有形可无形
    • 实体集(entities):特性完全相同的实体的集合
    • 属性(attribute):实体的特性:
    • 标识符(identifier):能够唯一地标识每个实体的属性或属性集

    层次二:

    • 数据项(data item):也称为字段(field),标记实体属性的可以命名的最小信息单位,数据项的命名一般采用属性的描述性名称。这些名称可以是中文、英文或汉语拼音。
    • 元组(tuple)://记录(record),数据项的集合称为元组。一个元组表示一个具体的实体。
    • 关系(relation):同一类元组所在的集合称为关系。关系–描述实体集,它包括一个实体集的所有元组。
    • 键码(key):在关系型数据库系统中,能够唯一地标识关系中每个元组的数据项或数据项的组合称为关系的键码。

    4. 数据库在开发中的作用
    常见的运行与应用结构:

    • 客户端/服务器结构

    • 浏览器/服务器结构

    在客户端/服务器(Client/Server,C/S)结构中,数据库的使用者(如
    DBA、程序设计者)通过命令行客户端、图形化界面管理工具或应用程序等连接到数据库管理系统,可以通过数据库管理系统查询和处理存储在底层数据库中的各种数据。
    对于客户端应用程序的开发,目前常用的语言工具主要有 Visual C++、Delphi、.NET 框架、Visual
    Basic、Python 等。

    5. 数据库管理系统(Database Management System,DBMS)
    是位于操作系统与用户之间的一种操纵和管理数据库的软件,按照一定的数据模型科学地组织和存储数据,同时可以提供数据高效地获取和维护。

    DBMS的主要功能包括以下几个方面。 1) 数据定义功能 DBMS 提供数据定义语言(Data Definition
    Language,DDL),用户通过它可以方便地对数据库中的数据对象进行定义。 2) 数据操纵功能 DBMS 还提供数据操纵语言(Data
    Manipulation Language,DML),用户可以使用 DML 操作数据,实现对数据库的基本操作,如查询、插入、删除和修改等。
    3) 数据库的运行管理
    数据库在建立、运用和维护时由数据库管理系统统一管理、统一控制,以保证数据的安全性、完整性、多用户对数据的并发使用及发生故障后的系统恢复。例如:

    • 数据的完整性检查功能保证用户输入的数据应满足相应的约束条件;
    • 数据库的安全保护功能保证只有赋予权限的用户才能访问数据库中的数据;
    • 数据库的并发控制功能使多个用户可以在同一时刻并发地访问数据库的数据;
    • 数据库系统的故障恢复功能使数据库运行出现故障时可以进行数据库恢复,以保证数据库可靠地运行。 4) 提供方便、有效地存取数据库信息的接口和工具 编程人员可通过编程语言与数据库之间的接口进行数据库应用程序的开发。数据库管理员(Database
      Administrator,DBA)可通过提供的工具对数据库
      进行管理。
    1. 数据库的建立和维护功能
      数据库功能包括数据库初始数据的输入、转换功能,数据库的转储、恢复功能,数据库的重组织功能和性能监控、分析功能等。这些功能通常由一些使用程序来完成。

    数据库系统是指在计算机系统中引入数据库后的系统。一个完整的数据库系统(Database
    System,DBS)一般由数据库、数据库管理系统、应用开发工具、应用系统、数据库管理员和用户组成。

    6. 结构化查询语言(Structured Query Language,SQL)
    SQL具有如下优点。

    • 一体化:SQL集数据定义、数据操作和数据控制于一体,可以完成数据库中的全部工作。
    • 使用方式灵活:SQL具有两种使用方式,可以直接以命令方式交互使用;也可以嵌入使用,嵌入C、C++、Fortran、COBOL、Java等语言中使用。
    • 非过程化:只提操作要求,不必描述操作步骤,也不需要导航。使用时只需要告诉计算机“做什么”,而不需要告诉它“怎么做”。
    • 语言简洁、语法简单、好学好用:在ANSI标准中,只包含94个英文单词,核心功能只用6个动词,语法接近英语口语。

    注意:SQL 语句不区分大小写,许多 SQL 开发人员习惯对 SQL
    本身的关键字进行大写,而对表或者列的名称使用小写,这样可以提高代码的可阅读性和可维护性。本教程也按照这种方式组织 SQL
    语句。大多数数据库都支持通用的 SQL 语句,同时不同的数据库具有各自特有的 SQL 语言特性。

    7. 数据库访问技术

    数据库访问技术包括 ODBC、DAO、OLE DB 和 ADO。

    • ODBC
      ODBC(Open Database Connectivity,开放数据库互连)是微软公司开放服务结构(Windows Open Services Architecture,WOSA)中有关数据库的一个组成部分,它建立了一组规范,并提供了一组对数据库访问的标准 API(应用程序编程接口)。这些 API 利用 SQL 来完成其大部分任务。
      ODBC 本身也提供了对 SQL 语言的支持,用户可以直接将 SQL 语句送给 ODBC。

    • DAO
      DAO(Data Access Object,数据访问对象集)是 Microsoft 提供的基于一个数据库对象集合的访问技术,可以独立于 DBMS 进行数据库的访问。

    • OLE DB
      OLE DB(Object Linking and Embedding Database,对象连接与嵌入)是微软战略性的通向不同数据源的低级应用程序接口。OLE DB 不仅包括微软资助的标准数据接口,开放数据库连通性(ODBC)的结构化查询语言(SQL)能力,还具有面向其他非 SQL 数据类型的通路。
      作为微软的组件对象模型(COM)的一种设计,OLE DB 是一组读写数据的方法(在过去可能称为渠道)。OLD DB 中的对象主要包括数据源对象、阶段对象、命令对象和行组对象。

    • ADO
      ADO(ActiveX Data Objects)是一个用于存取数据源的 COM 组件,提供了编程语言和统一数据访问方式 OLE DB 的一个中间层,允许开发人员编写访问数据的代码而不用关心数据库是如何实现的,只用关心到数据库的连接。

    8. MySQL 数据库管理系统具有以下系统特性:

    • 使用 C 和 C++ 编写,并使用多种编译器进行测试,保证源代码的可移植性。

    • 支持 AIX、FreeBSD、HP-UX、Linux、Mac OS、NovellNetware、OpenBSD、OS/2
      Wrap、Solaris、Windows 等多种操作系统。

    • 为多种编程语言提供了 API。这些编程语言包括 C、C++、Python、Java、Perl、PHP、Eiffel、Ruby 和 Tcl
      等。

    • 支持多线程,充分利用 CPU 资源。

    • 优化的 SQL 查询算法,有效地提高查询速度。

    • 既能够作为一个单独的应用程序应用在客户端服务器网络环境中,也能够作为一个库而嵌入其他的软件中。

    • 提供多语言支持,常见的编码如中文的 GB 2312、BIG 5,日文的 Shift_JIS 等都可以用作数据表名和数据列名。

    • 提供 TCP/IP、ODBC 和 JDBC 等多种数据库连接途径。

    • 提供用于管理、检查、优化数据库操作的管理工具。

    • 支持大型的数据库。可以处理拥有上千万条记录的大型数据库。

    • 支持多种存储引擎。

    展开全文
  • 你必须知道495个C语言问题

    千次下载 热门讨论 2015-05-08 11:09:25
    1.4 新64位机上64位类型是什么? 指针声明 1.5 这样声明有什么问题?char*p1,p2;我在使用p2时候报错了。 1.6 我想声明一个指针,并为它分配一些空间,但却不行。这样代码有什么问题?char*p;*p=...
  •  信息:从广义上讲,事物运动时发出信号所带来消息,事物存在方式和运动规律种表现形式。不同事物具有不同存在方式和运动规律,从而构成了各种事物不同特征。信息普遍存在于自然界、社会界以及人...

    谈决策树之前先做一些预备性知识:

    1.什么是信息?如何衡量信息的多少?怎么衡量?

         信息:从广义上讲,是事物运动时发出的信号所带来的消息,是事物存在方式和运动规律的一种表现形式。不同的事物具有不同的存在方式和运动规律,从而构成了各种事物的不同特征。信息普遍存在于自然界、社会界以及人的思维之中,是客观事物本质特征千差万别的反应。信息分为两大类:自然信息与社会信息。

        消息:消息是信息的具体反映形式,信息是消息的实质内容,是能给人带来新认识的消息。不同的消息中所包含的信息量是不同的,有的消息中包含的信息量大一些,有的小一些,有的对某些人来说甚至不包含信息。只有哪些为接受者了解、认识而且事先不知道的消息中才蕴含着信息。

       好,上面提到了信息量大一些和信息量小一些,那么什么才是信息量大和信息小呢?在现实生活中如果可以预料到事情会发生,例如明天太阳会从东边升起、明天有24小时,晚上会睡觉,,,等,从常理来理解这些大家都知道会发生,是不是意味着这些事发生的概率很大,因为发生的概率很大(常识,大家都知道的事情),给我们带来的新的信息并没有那么多,所以信息量少,如果你购买彩票并中奖、太阳从西边出来了,晚上不睡觉了,,,等等,这些是不是发生的概率很低,和我们经验不符合,并且带给我们额外的巨大信息,如这个号中奖的概率很大,太阳从西边出来的原因,晚上不睡觉的原因等,因此这样的事件发生概率很低,但是带给我们的信息量却是很大的,知道了信息量了,如何衡量他呢?如何定性的分析他呢?这个问题香浓很好的解决了

    信息量的定义:

          一个事件\LARGE x_i,发生的概率为\LARGE p\left ( x_{i} \right ),那么信息量定义为:

                                         \LARGE l(x_{i})=-log(p(x_{i}))

    注释:为什么要这样定义呢?上面说了,发生的概率越大,信息量越小,从定义可以看出-log(p(x_{i}))可以写为log(\frac{1}{p(x_{i})}),那么概率越大,他的倒数就越小,取对数后也会很小,因此和上面说的相符合,如明天太阳会从东边升起,发生的概率很大,给我们新的信息很小,因此信息量很小,通过信息量公式可以发现\LARGE l(x_{i})很小,因此从数学角度给出了信息量的定义。

        在数据处理中,通常的解释为待分类的事物可能划分为多个分类中,^{_{}}x_{i}就是待分类的数据,p(x_{i})即为分到某一类的概率。

    香浓熵:香浓熵的定义是从信息需要多少二进制数据表示定义的,这里只从数据的意义进行解释。

                                    H = -\sum_{i=1}^{n} p(x_{i})l(x_{i})=-\sum _{i=1}^{n} p(x_i)log(p(x_i))

    从定义试可以看出,H是概率论方面的求期望的公式,所以信息熵也是计算所有类别的所有可能值包含的信息期望

    因此熵是衡量事物的无序程度的度量方式,大家可以这样理解熵,在现在世界中有序的状态,例如太阳从东方升起,晚上睡觉等,因为都是有次序的发生,因此信息量少,期望值小,无序状态是指不确定性很高,每种状态都可能发生,例如一滴墨水滴入清水中,刚开始熵值很大,过段时间后墨水均匀的在水中,此时熵值趋于稳定并达到最小,也就是说,熵总是向着有序的方向进行,所以熵是衡量事物无序程度的衡量指标,有序和无序在分类中可以表示为已分类和未分类数据,未分类时,杂乱无章,熵最大,分类后熵减小,因为未分类可以看做数据无序化,分类后开始有序化,因此数据处理也可以引入熵的概念即:

           未处理的数据是无序化的,经过分类后数据变得有序化,所有熵会变低。

     

    信息增益(ID3算法):

           信息增益理解起来其实很简单,未经分类的数据是无序化、不确定的也就是熵最大的时候,此时的熵使用H表示,如果根据某一个特征A进行分类后数据的熵为H_{A},此时的熵H_{A}H要小,因为分为某一类后数据开始有序化了或者确定了,那么定义信息增益为:

                                   Gain(A)=H-H_{A} 

    由上面的解释可知信息增益其实就是通过数据分类后,数据的熵变低了(因为分类代表有序),而根据某个特征使熵降低了多少称为信息增益,强调一下增益的概念,其实是指数据通过这个特征有序化的程度即熵的变化量。

    下面给出韩家伟的数据挖掘概念与技术中的例子结合理解:

    介绍一下数据,表8.1共有14个数据样本,共有4个特征分别为age、income、student、credit_rating。类别为是否购买电脑即buys_computer

    解释一下infor(D)就是信息熵H,并计算了初始无序化的熵,无序化的熵也是根据类计算,有两个类分别为yes和no,其中

    yes出现9个数据样本,no出现5个数据样本,所以概率为yes:9/14,no:5/14,按照公式计算熵即可,计算结果为0.940,此时再根据特征计算熵,文中选取的是age特征进行计算,即分别根据youth、midde_age、senior按照分类yes和no计算熵在求和即可,例如youth中有5个数据样本,在这五个样本中两个yes三个no,一次类推计算的到

                     H_{age} = info(D)_{age}=0.694         和原始数据的熵对比发现减小了,下面看看减小了多少即信息增益:

                     Gain(age) = info(D)-info_{age}(D) = 0.940 - 0.694 = 0.246

           同理可得:

                       Gain(income) = 0.029

                       Gain(student) = 0.151

                        Gain(credit-rating) = 0.048

    由上面可知,使信息增益最大的是特征age的划分,其次是student、、、那么决策树是不是可以根据这种信息增益的方法进行分类呢,每一次都选择信息增益最大的的特征进行分支,这样不停的迭代下去,直到分类结束,以此使数据达到最佳有序化即熵最小,这就是ID3算法了。

    信息增益总结:

          对于待划分的数据集D,其 entroy(前)是一定的,但是划分之后的熵 entroy(后)是不定的,entroy(后)越小说明使用此特征划分得到的子集的不确定性越小(也就是纯度越高(就是类别很少。当只是一类时,纯度达到做大),也就是数据有序化更好,即确定性的越多),因此 entroy(前) -  entroy(后)差异越大,说明使用当前特征划分数据集D的话,其纯度上升的更快。而我们在构建最优的决策树的时候总希望能更快速到达纯度更高的集合,这一点可以参考优化算法中的梯度下降算法,每一步沿着负梯度方法最小化损失函数的原因就是负梯度方向是函数值减小最快的方向。同理:在决策树构建的过程中我们总是希望集合往最快到达纯度更高的子集合方向发展,因此我们总是选择使得信息增益最大的特征来划分当前数据集D。

          但是ID3算法的缺点是:信息增益偏向取值较多的特征

          原因:当特征的取值较多时,根据此特征划分更容易得到纯度更高的子集,因此划分之后的熵更低,由于划分前的熵是一定的,因此信息增益更大,因此信息增益比较 偏向取值较多的特征。

         解决办法是引入增益率(C4.5算法):

                                        信息增益率 = 惩罚参数 * 信息增益

    具体参考韩家伟的书《数据挖掘:概念与技术》

    还有另外的分类指标是基于基尼指数的,在CART中使用,这个算法很简单,和基于熵的来说,该算法具有的优势是计算量少,效果和信息增益差不多。下面对比一下:

    ID3(Iterative Dichotomiser 3)由 Ross Quinlan 在1986年提出。该算法创建一个多路树,找到每个节点(即以贪心的方式)分类特征,这将产生分类目标的最大信息增益。决策树发展到其最大尺寸,然后通常利用剪枝来提高树对未知数据的泛华能力。

    C4.5 是 ID3 的后继者,并且通过动态定义将连续属性值分割成一组离散间隔的离散属性(基于数字变量),消除了特征必须被明确分类的限制。C4.5 将训练的树(即,ID3算法的输出)转换成 if-then 规则的集合。然后评估每个规则的这些准确性,以确定应用它们的顺序。如果规则的准确性没有改变,则需要决策树的树枝来解决。

    C5.0 是 Quinlan 根据专有许可证发布的最新版本。它使用更少的内存,并建立比 C4.5 更小的规则集,同时更准确。

    CART(Classification and Regression Trees (分类和回归树))与 C4.5 非常相似,但它不同之处在于它支持数值目标变量(回归),并且不计算规则集。CART 使用在每个节点产生最大信息增益的特征和阈值来构造二叉树。

     

    下面开始迎来主角决策树:

         决策树的定义很简单,在这里先举一个简单的例子,后面再举一个复杂的例子,并给出实现代码。

    简单的例子来源机器学习实战邮件分类问题:

        我们经常使用决策树处理分类问题,它的过程类似二十个问题的游戏:参与游戏的一方在脑海里想某个事物,其他参与者向他提出问题,只允许提20个问 题,问题的答案也只能用对或错回答。问问题的人通过推断分解,逐步缩小带猜测事物的范围。

      如图1所示的流程图就是一个决策树,长方形代表判断模块(decision block),椭圆形代表终止模块(terminating block),表示已经得出结论,可以终止运行。从判断模块引出的左右箭头称作分支(branch),它可以到达另一个判断模块或终止模块。

      图 1构造了一个假象的邮件分类系统,它首先检测发送邮件域名地址。如果地址为myEmployer.com,则将其放在分类"无聊时需要阅读的邮件"中。如 果邮件不是来自这个域名,则检查内容是否包括单词曲棍球,如果包含则将邮件归类到"需要及时处理的朋友邮件",否则将邮件归类到"无须阅读的垃圾邮件"。

           这个决策树的目的是分类邮件,可以把邮件分为3类,分别为:一类是无聊时阅读的邮件、一类是需要及时处理的邮件、一类是无需阅读的垃圾邮件。既然是分类邮件,就需要有一个依据进行分类,该决策树首先通过邮件地址进行分类,即先判断地址是否为myEmployer.com的地址(当然这只是例子,现实中可能有很多这样的邮件地址),如果是则分为无聊时阅读,到此分类结束了。如果不是这个邮件地址,则需要继续判断根据依据分为哪一类,这个例子给的依据是包含单词“曲棍球”的邮件为依据进行分类,如果包含则属于及时处理的邮件,反之都属于垃圾邮件。到此分类结束了,从这个例子中,我们可以看到决策树的重点是通过某个特征进行分类判断的,现在难点是如何在适当的节点找到适合的特征进行分类,为了找到适合的特征,我们引入上面提到的信息增益(ID3算法)进行分类,首先计算各个特征的信息增益,把当前信息增益最大的特征作为判断依据即节点,每一次节点都需要重新计算剩余的特征信息增益,以此下去进行分类,直到分类结束。当然选择依据(特征)也可以根据其他算法如基尼不存度,原理是类似的。

         下面贴出机器学习实战的手写代码,并给出了详细的注释,且运行条件为Python3.6.5,课本上运行环境是Python2.7,中间出错的均已修改完毕:

     

    #!/usr/bin/env/python
    # -*- coding: utf-8 -*-
    # Author: 赵守风
    # File name: trees.py
    # Time:2018/9/10
    # 本代码是在Python3.6.5版本运行
    
    from math import log
    import operator
    
    
    def createdataset():
        dataset = [[1, 1, 'yes'],
                   [1, 1, 'yes'],
                   [1, 0, 'no'],
                   [0, 1, 'no'],
                   [0, 1, 'no']]
        labels = ['no surfacing', 'flipper']
        print(dataset, labels)
        return dataset, labels
    
    
    # 计算数据中的香浓熵: H = -∑p(x)log2 p(x)
    def calcshannongent(dataset):
        numentries = len(dataset)
        labelcounts = {}    # 创建标签字典
        for featvec in dataset:
            currentlabel = featvec[-1]  # 数据的最后一列是标签作为字典的键值
            if currentlabel not in labelcounts.keys():   # 记录属于该标签数据的次数,后面计算熵使用,
                labelcounts[currentlabel] = 0   # 如果字典里没有这个标签即键值,则创建并赋值为0,如果存在该标签则加1
            labelcounts[currentlabel] += 1
        shannonent = 0.0    # 计算熵值
        for key in labelcounts:
            prob = float(labelcounts[key]) / numentries  # 以每个标签出现的频次为概率计算熵值
            shannonent -= prob * log(prob, 2)
        return shannonent
    
    
    # 按照给定特征划分数据集
    def splitdataset(dataset, axis, value):  # dataset为原始输入数据,axis是原始数据的特征位置,value是数据对应的特征值
        retdataset = []
        for featvec in dataset:  # 迭代dataset中的数据
            if featvec[axis] == value:  # 通过迭代判断每个数据特征位置的数据和给定的特征值是否一致
                reducedfeatvec = featvec[:axis]  # 如果条件成立,那么就把该位置前面的数据保存
                reducedfeatvec.extend(featvec[axis+1:])  # 把该位置后面的数据保存,通过这两句就可以把该特征位置删除
                retdataset.append(reducedfeatvec)  # 把保留下来的数据,存储在新的列表里作为一个元素
                '''下面有例子解释一下上面的三句代码
                    trees.splitdataset(mydata, 0, 1),输入的数据为mydata, 特征位置为0即axis, 数据特征值为1
                    执行后的数据
                    原始数据: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
                    划分好的数据:  [[1, 'yes'], [1, 'yes'], [0, 'no']]
                    原始数据是一个列表,这个列表有5个元素,而每个元素又是一个列表,每个元素列表中又有三个元素
                    featvec[axis] == value这句代码的意思是featvec从dataset中获得元素后,例如为第一个元素[1, 1, 'yes'],
                    此时featvec[axis]是指该元素的第零个位置数据即为1,value为给定的特征划分值为1,此时if成立,执行下面的程序
                    reducedfeatvec = featvec[:axis],列表的切片,不会的请查看列表的切片,featvec[:axis]是指把axis位置前面的数据传给reducedfeatvec
                    ,而featvec[axis+1:]是把特征位置后面的数据都保存下来如[1, 'yes'],把保存下来,简单说 这两句意思就是把axis位置删除
                    把重新组建的数据传给retdata
                '''
        return retdataset
    
    
    # 选择数据集划分方式,主要通过信息增益进行划分
    def choose_bestfeature_to_split(dataset):
        num_features = len(dataset[0]) - 1  # 计算每个元素有多少特征
        print(num_features)  # 只计算前两个数值特征
        base_entropy = calcshannongent(dataset)  # 计算基础香浓熵
        best_info_gain = 0.0
        best_feature = -1
        # 下面为计算信息增益做准备工作
        for i in range(num_features):
            feat_list = [example[i] for example in dataset]  # 利用列表推倒,把每个元素数据的特征提取出来,并创建新列表
            print('feat_list', feat_list)  # 打印出来发现是feat_list [1, 1, 1, 0, 0],即表明把dataset的第0号位置的特征提取出来
            unique_vals = set(feat_list)  # 使用集合元素数据中重复的特征合并
            print('unique_vals', unique_vals)  # 把feat_list [1, 1, 1, 0, 0]重合的合并为unique_vals {0, 1}
            new_entropy = 0.0
            for value in unique_vals:
                subdataset = splitdataset(dataset, i, value)  # 根据特征把同类的数据归类,并计算香浓熵
                prob = len(subdataset)/float(len(dataset))  # 计算概率
                new_entropy += prob * calcshannongent(subdataset)  # 计算 新熵
            info_gain = base_entropy - new_entropy   # 计算信息增益
            if (info_gain > best_info_gain):  # 找出信息增益最大的特征
                best_info_gain = info_gain
                best_feature = i
        return best_feature
    
    
    # 多数表决,和K-近邻类似的作用
    def majority_cnt(classlist):
        classcount = {}
        for vote in classlist:
            if vote not in classcount.keys():
                classcount[vote] = 0
            classcount[vote] += 1
        sortedclasscount = sorted(classcount.items(), key=operator.itemgetter(1), reverse=True)
        # 返回classlist列表中数据出现次数最多的元素
        return sortedclasscount[0][0]
    
    
    # 创建决策树
    def createtree(dataset, labels):
        ''' 本次结合输入数据进行讲解,详细探讨该代码是如何实现决策树的
        dataset = [[1, 1, 'yes'],
                   [1, 1, 'yes'],
                   [1, 0, 'no'],
                   [0, 1, 'no'],
                   [0, 1, 'no']]
        labels = ['no surfacing', 'flipper']
        其中输入的数据集是dataset有5个元素数据,每个元素有两个特征和一个类别,前两列的特征分别为'no surfacing', 'flipper'
        类别为是否是鱼类,是的话为‘yes’,反之为‘no'
        '''
        classlist = [example[-1] for example in dataset]
        # 该句代码的意思是,把元素数据的分类信息提取出来,此时classlist = ['yes', 'yes', 'no', 'no', 'no']
        if classlist.count(classlist[0]) == len(classlist):
            return classlist[0]
        '''
        # 第一个递归停止条件,如果classlist中的信息都为同一类,则说明分类已经是纯净的了,无须再划分,返回类别就好
        # 如classlist = ['no', 'no', 'no'],此时classlist[0]出现三次,而列表长度也为3,等式成立执行if语句,即返回类别’no‘
        '''
        if len(dataset[0]) == 1:  # 使用完了所有的特征,但还是无法将数据划分为唯一类别的分组,因此就挑选出现次数最多的类别进行分类
            return majority_cnt(classlist)
        '''
        # 第二个递归停止条件是根据特征判断,因为每次根据特征划分时,后面就会删除该特征,如果遍历完所有特征以后发现classlist还是不纯净,
        # 那么就通过出现的次数划分。如刚开始dataset[0] = [1, 1, 'yes'],有两个特征,一个分类标签,当每根据一个特征进行分类结束时就会
        # 删除该特征,最后只剩下dataset[0] = ['yes'],无法再根据特征进行分类,此时,根据分类列表中把该元素划分到出现类别最多的次数的
        # 分类中,如classlist = ['no', 'yes', 'yes','yes'],此时‘no’出现一次,‘yes’出现三次,因此把该类分为‘yes’,其中通过函数
        # majority_cnt()进行统计。
        '''
        bestfeat = choose_bestfeature_to_split(dataset)  # 使用信息增益返回最好的划分特征的列表位置信息
        # 分类的标准是通过ID3算法即信息增益进行划分,返回的是使信息增益增加最大的特征
        bestfeatlabel = labels[bestfeat]
        # 提出该最优特征 第一次特征为'no surfacing'
        mytree = {bestfeatlabel: {}}
        # 以当前使信息增益最大的特征创建以字典形式迭代的迭代器,此时为mytree = {no surfacing: {}}
        del (labels[bestfeat])
        # 删除该使用的特征
        featvalues = [example[bestfeat] for example in dataset]
        # 遍历所有元素的最优特征位置对应的特征值featvalues = [1, 1, 1, 0, 0]
        unique_vals = set(featvalues)
        # 转换成集合形式,即去除重复的特征值,集合的互异性可以使相同特征值合并,此时unique_vals = [1,0]
        for value in unique_vals:
            sublabels = labels[:]
            # 复制一份 标签列表
            mytree[bestfeatlabel][value] = createtree(splitdataset(dataset, bestfeat, value), sublabels)
            '''
            # 该部分是根据特征进行分类,并进行迭代继续分类,如刚开始是mytree = {no surfacing: {}},此时
            # mytree[bestfeatlabel][value] = createtree(splitdataset(dataset, bestfeat, value), sublabels) 输入的是当前特征值
            # 根据当前特征我们可以知道,该特征值有两个值,['0','1'],通过类别划分后,把基于该特征的都返回,同时,删除该特征信息
            # 当再次进入createtree()时,先对‘0’值进行判断,发现[0, 1, 'no'],[0, 1, 'no'],他们的标签为都为‘no’是纯的,满足
            # 第一个停止条件,即当分类为纯的分类后,停止迭代,返回‘no'分类,返回后,继续进行for循环,此时该特征值为’1‘,
            # [1, 1, 'yes'],[1, 1, 'yes'],[1, 0, 'no'],发现不满足停止条件即既不是纯的,特征也没有使用完,此时就会再次进入迭代,根据信息
            # 增益进行分类,然后进入for循环,,,,直到满足条件迭代返回,返回到最初第一层后结束迭代。
            # 做好输出结果为{'no surfacing': {0: 'no', 1: {'flipper': {0: 'no', 1: 'yes'}}}}
            '''
        return mytree
    
    
    # 使用决策树的分类函数
    def classify(inputtree, featlabels, testvec):
        firststr = list(inputtree.keys())[0]
        second_dict = inputtree[firststr]
        feat_index = featlabels.index(firststr)
        for key in second_dict.keys():
            if testvec[feat_index] == key:
                if type(second_dict[key]) == dict:
                    classlabel = classify(second_dict[key], featlabels, testvec)
                else:
                    classlabel = second_dict[key]
        return classlabel
    
    
    # 决策树的存储
    def storetree(inputtree, filename):
        import pickle
        fw = open(filename, 'wb')   # 需要注意的是运行Python2的代码时,出现错误,错误的原因是需要加上写字节即‘wb’
        pickle.dump(inputtree, fw)
        fw.close()
    
    
    def grabtree(filename):
        import pickle
        fr = open(filename, 'rb')  # 同理需要加上读字节’rb’
        return pickle.load(fr)
    
    
    
    
    
    
    
    

    画图的就不贴了,下面开始给出示例,该示例来源机器学习实战的示例即使用决策树预测隐形眼镜类型,只是使用了两种方法进行预测。

    先给出基于上面的代码的预测,然后给出基于sklearn的机器学习库进行分类的代码

    隐形眼镜数据集是非常著名的数据集,它包含很多换着眼部状态的观察条件以及医生推荐的隐形眼镜类型。隐形眼镜类型包括硬材质(hard)、软材质(soft)以及不适合佩戴隐形眼镜(no lenses)。数据来源与UCI数据库,数据集下载地址:https://github.com/Jack-Cherish/Machine-Learning/blob/master/Decision%20Tree/classifierStorage.txt

    一共有24组数据,数据的Labels依次是ageprescriptastigmatictearRateclass,也就是第一列是年龄,第二列是症状,第三列是是否散光,第四列是眼泪数量,第五列是最终的分类标签。数据如下图所示:

                                                                         

    数据清楚后,下面给出代码,注释在代码中很详细:

    #!/usr/bin/env/python
    # -*- coding: utf-8 -*-
    # Author: 赵守风 
    # File name: ex_Contact lens type.py
    # Time:2018/9/26
    
    
    
    # 机器学习实战的隐形眼镜分类和树可视化
    import trees_ex
    import plot_tree
    import trees
    
    fr = open('lenses.txt')
    # lenses = [inst.strip().split('\t') for inst in fr.readline()]
    # 该语句在Python3无法执行,因为执行的结果并不能得到构建决策树所需的数据集,需要使用下面的语句, 把所有数据取出来
    lenses = []
    for line in fr:
        line = fr.readline()
        inst = line.strip().split('\t')
        lenses.append(inst)
    print(lenses)
    lenseslabels = ['age', 'prescript', 'astigmatic', 'tearRate']
    lensestree = trees_ex.createTree(lenses, lenseslabels)
    # lensestree = trees.createtree(lenses, lenseslabels)
    print(lensestree)
    plot_tree.createPlot(lensestree)
    '''
    本代码调用的是前面贴的手写代码,画图的代码没给出
    '''

             先分析一下决策树,首先决策树通过计算各个特征的信息增益,对比以后astigmatic(是否散光) 特征的信息增益最大,根据此特征分为闪光和不散光,在不散光(no)的条件下,从新计算信息增益,发现age的增益最大,因此根据此特征继续划分,,,直到划分结束为止。

           下面贴出sklearn的代码,首先需要大家安装pydotplus和Grphviz,如果使用conda可以通过 conda install pydotplus/grphviz直接安装即可       

    贴代码之前先给出几个链接:

    sklearn的官方网站

    sklearn给出决策树的示例

    决策树的API说明文档

    先贴出几个重要的API接口函数:

    class sklearn.tree.DecisionTreeClassifiercriterion ='gini'splitter ='best'max_depth = Nonemin_samples_split = 2min_samples_leaf = 1min_weight_fraction_leaf = 0.0max_features = Nonerandom_state = Nonemax_leaf_nodes = Nonemin_impurity_decrease = 0.0min_impurity_split = Noneclass_weight = Nonepresort = False 

    参数:

    criterion:string,optional(default =“gini”)

    衡量分裂质量的功能。支持的标准是基尼杂质的“gini”和信息增益的“熵”。

    splitter:string,optional(default =“best”)

    用于在每个节点处选择拆分的策略。支持的策略是“最佳”选择最佳分割和“随机”选择最佳随机分割。

    max_depth:int或None,可选(默认=无)

    树的最大深度。如果为None,则扩展节点直到所有叶子都是纯的或直到所有叶子包含少于min_samples_split样本。

    min_samples_split:int,float,optional(default = 2)

    拆分内部节点所需的最小样本数:

    • 如果是int,则将min_samples_split视为最小数字。
    • 如果是float,则min_samples_split是百分比, ceil(min_samples_split * n_samples)是每个分割的最小样本数。

    在版本0.18中更改:添加了百分比的浮点值。

    min_samples_leaf:int,float,optional(default = 1)

    叶子节点所需的最小样本数:

    • 如果是int,则将min_samples_leaf视为最小数字。
    • 如果是float,则min_samples_leaf是百分比, ceil(min_samples_leaf * n_samples)是每个节点的最小样本数。

    在版本0.18中更改:添加了百分比的浮点值。

    min_weight_fraction_leaf:float,optional(默认= 0。)

    需要在叶节点处的权重总和(所有输入样本)的最小加权分数。当未提供sample_weight时,样本具有相同的权重。

    max_features:int,float,string或None,可选(默认=无)

    寻找最佳分割时要考虑的功能数量:

    • 如果是int,则在每次拆分时考虑max_features功能。
    • 如果为float,则max_features为百分比,并 在每次拆分时考虑int(max_features * n_features)要素。
    • 如果是“auto”,则max_features = sqrt(n_features)。
    • 如果是“sqrt”,则max_features = sqrt(n_features)。
    • 如果是“log2”,则max_features = log2(n_features)。
    • 如果为None,则max_features = n_features。

    注意:在找到节点样本的至少一个有效分区之前,搜索分割不会停止,即使它需要有效地检查多个max_features功能。

    random_state:int,RandomState实例或None,可选(默认=无)

    如果是int,则random_state是随机数生成器使用的种子; 如果是RandomState实例,则random_state是随机数生成器; 如果没有,随机数生成器所使用的RandomState实例np.random。

    max_leaf_nodes:int或None,可选(默认=无)

    max_leaf_nodes以最好的方式种树。最佳节点定义为杂质的相对减少。如果None则无限数量的叶节点。

    min_impurity_decrease:float,optional(默认= 0。)

    如果该分裂导致杂质的减少大于或等于该值,则将分裂节点。

    加权杂质减少方程式如下:

    N_t  /  N  *  (杂质 -  N_t_R  /  N_t  *  right_impurity 
                        -  N_t_L  /  N_t  *  left_impurity )
    

    N样本总数在哪里,N_t是当前节点N_t_L的样本数,左子项中N_t_R的样本数,以及右子项中的样本数。

    NN_tN_t_R并且N_t_L都指的是加权和,如果sample_weight获得通过。

    版本0.19中的新功能。

    min_impurity_split:float,

    树木生长早期停止的门槛。如果节点的杂质高于阈值,节点将分裂,否则它是叶子。

    从版本0.19min_impurity_split开始不推荐使用已弃用 min_impurity_decrease0.19,将在0.21中删除。请min_impurity_decrease改用。

    class_weight:dict,dicts列表,“balanced”或None,默认= None

    与表单中的类关联的权重。如果没有给出,所有课程都应该有一个重量。对于多输出问题,可以按与y列相同的顺序提供dicts列表。{class_label: weight}

    请注意,对于多输出(包括多标记),应为其自己的dict中的每个列的每个类定义权重。例如,对于四类多标签分类权重应为[{0:1,1:1},{0:1,1:5},{0:1,1:1},{0:1,1: 1}]而不是[{1:1},{2:5},{3:1},{4:1}]。

    “平衡”模式使用y的值自动调整与输入数据中的类频率成反比的权重 n_samples / (n_classes * np.bincount(y))

    对于多输出,y的每列的权重将相乘。

    请注意,如果指定了sample_weight,这些权重将与sample_weight(通过fit方法传递)相乘。

    presort:bool,optional(默认值= False)

    是否预先分配数据以加快拟合中最佳分裂的发现。对于大型数据集上决策树的默认设置,将其设置为true可能会降低培训过程的速度。使用较小的数据集或限制深度时,这可能会加快培训速度。

                 

    属性:

    classes_:shape = [n_classes]的数组或此类数组的列表

    类标签(单输出问题),或类标签数组列表(多输出问题)。

    feature_importances_:shape数组= [n_features]

    功能重要性。功能越高,功能越重要。特征的重要性计算为该特征带来的标准的(标准化的)总减少量。它也被称为基尼的重要性[R215]

    max_features_:int,

    max_features的推断值。

    n_classes_:int或list

    类的数量(对于单个输出问题),或包含每个输出的类数的列表(对于多输出问题)。

    n_features_:int

    fit执行时的功能数量。

    n_outputs_:int

    fit执行时的输出数量。

    tree_:树对象

    底层的Tree对象。

        下面贴出使用sklearn算法的决策树预测隐形眼镜类型   

    #!/usr/bin/env/python
    # -*- coding: utf-8 -*-
    # Author: 赵守风
    # File name: ex_Contact lens type.py
    # Time:2018/9/26
    
    from sklearn import preprocessing
    from sklearn.preprocessing import LabelEncoder, OneHotEncoder
    from sklearn.externals.six import StringIO
    from sklearn import tree
    import pandas as pd
    import numpy as np
    import pydotplus
    # import graphviz
    
    fr = open('lenses.txt')
    # lenses = [inst.strip().split('\t') for inst in fr.readline()]
    # 该语句执行的结果并不能得到构建决策树所需的数据集,需要使用下面的语句, 把所有数据取出来
    lenses = []
    for line in fr:
        lines = fr.readline()  # 读取整行
        inst = lines.strip().split('\t')  # 删除空格,同时以制表符为分割符,最后返回的是一行数据字符串列表
        lenses.append(inst)  # 把一行字符串列表添加到lenses列表中,如下
        # [['young', 'myope', 'no', 'normal', 'soft'], ['young', 'myope', 'yes', 'normal', 'hard'], ['y....
    print(lenses)
    
    
    '''fit()函数不能接收string类型的数据,通过打印的信息可以看到,数据都是string类型的。在使用fit()函数之前,
    我们需要对数据集进行编码,这里可以使用两种方法:
    LabelEncoder :将字符串转换为增量值
    OneHotEncoder:使用One-of-K算法将字符串转换为整数
    为了对string类型的数据序列化,需要先生成pandas数据,这样方便我们的序列化工作。这里我使用的方法是,
    原始数据->字典->pandas数据
    '''
    
    # 在使用sklearn前需要把字符串类型的数据转换成数值型数据,此时需要pandas进行转换
    lenses_target = []  # 提取每组数据的类别,保存在列表里
    for each in lenses:
        lenses_target.append(each[-1])  # 因为lenses列表中的每个元素都是原始数据的每一行数据,而每一行数据的最后有个数为分类数据
    lenseslabels = ['age', 'prescript', 'astigmatic', 'tearRate']  # 特征数据
    lenses_list = []   # 保存lenses数据的临时列表
    lenses_dict = {}
    for each_label in lenseslabels:    # 提取信息,生成字典
        for each in lenses:  # 根据特征数据进行提取,例如把所有样本为age这个特征的数据都要提取出来归为一个列表
            lenses_list.append(each[lenseslabels.index(each_label)])
        lenses_dict[each_label] = lenses_list  # 把提取样本特征为age的数据生成字典
        lenses_list = []  # 清空列表,为下一个提取特征数据做准备
    print(lenses_dict)    # 打印字典信息
    lenses_pd = pd.DataFrame(lenses_dict)  # 生成pandas.DataFrame
    print(lenses_pd)
    
    #  数据序列化
    le = preprocessing.LabelEncoder() #创建LabelEncoder()对象,用于序列化
    for col in lenses_pd.columns:                                            #为每一列序列化
        lenses_pd[col] = le.fit_transform(lenses_pd[col])
    print(lenses_pd)
    
    clf = tree.DecisionTreeClassifier(max_depth=4, criterion='entropy')  # 创建DecisionTreeClassifier()类,默认为gini
    clf = clf.fit(lenses_pd.values.tolist(), lenses_target)   # 使用数据,构建决策树
    dot_data = StringIO()
    tree.export_graphviz(clf, out_file= dot_data,                # 绘制决策树,sklearn的示例给出。直接复制过来使用即可
                        feature_names= lenses_pd.keys(),
                        class_names= clf.classes_,
                        filled=True, rounded=True,
                        special_characters=True)
    graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
    
    graph.write_pdf("tree_entropy.pdf")
    
    

                         贴出数据的序列化和决策树图

     

     

     

     基于熵的决策树和基于gini 的决策树对比

    基于熵的决策树

    对比发现两者相差不大,但是sklearn默认是gini,也许经过多方面权衡以后做的决定

    到目前为止,已经完整的完成决策树的学习,后面会陆续通过手写各个机器学习算法,然后再通过调用sklearn的方式处理数据,相结合学习,以此达到知其然,更知其所以然的目的。有错误欢迎指正

         

     

     

    展开全文
  • 《你必须知道495个C语言问题》

    热门讨论 2010-03-20 16:41:18
    1.4 新64位机上64位类型是什么? 3 指针声明 3 1.5 这样声明有什么问题?char *p1, p2; 我在使用p2时候报错了。 3 1.6 我想声明一个指针,并为它分配一些空间,但却不行。这样代码有什么问题?...
  • 1.4 新64位机上64位类型是什么? 3 指针声明 3 1.5 这样声明有什么问题?char *p1, p2; 我在使用p2时候报错了。 3 1.6 我想声明一个指针,并为它分配一些空间,但却不行。这样代码有什么问题?...
  • 直接采集待测信号,将分两种情况计算待测信号频率: 如果频率比较高,在一秒内对待测信号就行计数。 如果频率比较低,在待测信号一个周期内对单片机工作频率进行计数。 将得到频率值通过显示译码后直接送入...
  • 你必须知道495个C语言问题(PDF)

    热门讨论 2009-09-15 10:25:47
    1.2 64 位机上64 位类型是什么? . . . . . . . . . . . . . . . . 1 1.3 怎样定义和声明全局变量和函数最好? . . . . . . . . . . . . . . . 2 1.4 extern 在函数声明中是什么意思? . . . . . . . . . . . ...
  • 决定需求量参数可分为两种:运动参数(价格)和转移参数(除价格外所有影响需求量变化因素)。假定所有转移参数值不变,这样一来,就可以直接用需求曲线来表达运动参数(价格P)和需求量之间二维关系。 需求...
  • Thinking.In.Java

    2012-03-21 22:35:53
    在这一章中,大家将学习在Java中重复使用代码两种方法,以及具体如何运用。 (7) 第7章:多形性 若由你自己来干,可能要花9个月时间才能发现和理解多形性问题,这一特性实际OOP一个重要基础。通过一些小...
  • 尽管经常都会谈及C和C++语言一些特性,但并没有打算使它们成为内部参考,而是想帮助所有程序员都能正确地看待那两种语言。毕竟,Java从它们那里衍生出来。我将试着尽可能地简化这些引用和参考,并合理地解释一...
  • php高级开发教程说明

    2008-11-27 11:39:22
    后在适当地方加以例外处理,当写一个应用程序时,应该知道你代码从事的是什么工作, 能够快速地从一点转到另一点—但其他人可能认为这并不容易。如果你从开发组某个人手 中获得一个源文件并需要添加一些特征,...
  • c语言编写单片机技巧

    2009-04-19 12:15:17
    答: MCU从生产出来到封装出货每个不同阶段会有不同测试方法,其中主要会有两种:中测和成测。 所谓中测即WAFER测试,它会包含产品功能验证及AC、DC测试。项目相当繁多,以HOLTEK-p....
  • 软件工程教程

    2012-07-06 23:10:29
    两种典型建模工具 : 1.IBM Rational Rose 2.Microsoft Office Visio IBM Rational Rose Microsoft Office Visio Visio一个图表绘制程序,可以帮助用户描述复杂设想以及系统业务和技术图表。使用...
  • o 2.2 64 位机上 64 位类型是什么? o 2.3 怎样定义和声明全局变量和函数最好? o 2.4 extern 在函数声明中是什么意思? o 2.5 关键字 auto 到底有什么用途? o 2.6 我似乎不能成功定义一个链表。我试过 ...
  • 这里的IDE设备包括了IDE硬盘和IDE光驱,第一、第二组设备指主板上的第一、第二根IDE数据线,一般来说靠近芯片的第一组IDE设备,而主设备、从设备指在一条IDE数据线上接的两个设备,大家知道每根数据线上可以接...
  • 仔细比较两种数据差别,发现出现主机复位问题数据中DSL板配置了MNT/MLT端口,但是没有做DSL端口之间半永久数据。 于是在程序中不断加打印语句,通过后台DBWIN调试程序跟踪,最后终于定位为:每当执行到...
  • 你需要什么技术基础 在本书中所用到计算机语言只有两种:汇编和 C语言。所以只要你具备汇编和 C语言经验,就可以阅读本书。除对操作系统常识性了解(比如知道中断、进程等概念)之外,本书不假定读者具备其他...
  • 你需要什么技术基础 在本书中所用到计算机语言只有两种:汇编和 C语言。所以只要你具备汇编和 C语言经验,就可以阅读本书。除对操作系统常识性了解(比如知道中断、进程等概念)之外,本书不假定读者具备其他...
  • 死锁是两个或个以上进程中每一个,都在等待其中另一个进程释放资源而被封锁,它们都无法向前推进,称这种现象为死锁现象。 产生死锁原因共享资源有限,多个进程对共享资源竞争,而且操作不当。 ...
  • 2019数据运营思维导图

    2019-03-29 21:34:09
    平均同时在线人数、最高同时在线人数和时间 每小时注册用户数 用户在什么节点来多,需要重点监控该时间段app运行 用户画像 概述 是什么,有什么用,怎么做 构建用户画像核心工作即是给用户贴“标签”,而标签是...
  • 数据运营思维导图

    2018-04-26 14:24:22
    是什么,有什么用,怎么做 构建用户画像核心工作即是给用户贴“标签”,而标签是通过对用户信息分析而来高度精炼特征标识 作用 精准营销 分析产品潜在用户,针对特定群体利用短信、邮件等方式进行营销 ...

空空如也

空空如也

1 2
收藏数 30
精华内容 12
关键字:

反应的两种表现形式是什么