Return Bug+GameCache的应用
一、排除泄露
在讲解完Return Bug的原理以及跟GameCache的简单应用后,我并没有进一步讲解更复杂的数据绑定,而是穿插讲了一篇中级教程《JASS动态注册事件》,因为在用Return Bug+GameCache写一些复杂的系统时,经常会用到动态注册事件。
但在讲解动态注册事件时,一个很重要的内容没有被提到,那就是排除在销毁触发器时由触发器条件和动作引起的泄露。不过在排泄时必须用到Return Bug+GameCache,因此我把排泄留到这章讲,当作Return Bug+GameCache的一个应用实例。
好吧,让我们先回顾下动态注册事件,依旧用Tiveone的经典教程
jass:  Copy code
//当触发注册的事件时调用的函数 
function OnUnitAmort takes nothing returns nothing 
         //设置触发单位为无敌 
         call SetUnitInvulnerable( GetTriggerUnit(), true ) 
         //破坏这个触发器,即下面的RegisterUnitAmortEvent所创建的触发器 
         //这样就不会导致该触发被多次调用,并且还能清理该触发所占用的内存 
         call DestroyTrigger(GetTriggeringTrigger()) 
endfunction
//注册模板 
function RegisterUnitAmortEvent takes unit witchUnit returns nothing 
         //创建一个触发器 
         local trigger rTrg = CreateTrigger() 
         //为创建的触发器注册事件:当单位witchUnit的生命值小于或等于50的时触发事件 
         call TriggerRegisterUnitLifeEvent( rTrg , witchUnit, LESS_THAN_OR_EQUAL, 50 ) 
         //注册触发器事件响应后所调用的函数 
         call TriggerAddAction( rTrg, function OnUnitAmort ) 
endfunction
这样的写法不是很好,它会造成内存泄露。我们来看下TriggerAddAction这个函数 
native TriggerAddAction  takes trigger whichTrigger, code actionFunc returns triggeraction
这里先生成了一个triggeraction,然后把它添加给了触发器;而当我们删除触发器的时候,这个triggeraction却并没有被删除。
那么,为了维护这世界的爱与正义,我们来排除上面动态注册事件的泄露吧~
首先,准备好下列Return Bug常用函数
jass:  Copy code
function H2I takes handle h returns integer 
      return h 
      return 0 
endfunction
function I2TG takes integer i returns trigger 
      return i 
      return null 
endfunction
function I2TC takes integer i returns triggercondition 
      return i 
      return null 
endfunction
function I2TA takes integer i returns triggeraction 
      return i 
      return null 
endfunction
然后,上面的动态注册事件可以这样写
jass:  Copy code
function RegisterUnitAmortAction takes nothing returns nothing 
         local trigger trg = GetTriggeringTrigger() 
         local triggeraction Act=I2TA(GetStoredInteger(udg_GC,I2S(H2I(trg)),"Triggeraction")) 
         call SetUnitInvulnerable(GetTriggerUnit(), true) 
         call TriggerRemoveAction(trg,Act) 
         call DestroyTrigger(trg) 
         call FlushStoredMission(udg_GC,I2S(H2I(trg))) 
         set trg = null 
         set Act=null 
endfunction
function RegisterUnitAmortEvent takes unit witchUnit returns nothing 
         local trigger trg = CreateTrigger() 
         local triggeraction Act 
         call TriggerRegisterUnitStateEvent(trg, witchUnit, UNIT_STATE_LIFE, LESS_THAN_OR_EQUAL, 50) 
         set Act=TriggerAddAction(trg,function RegisterUnitAmortAction) 
         call StoreInteger(udg_GC,I2S(H2I(trg)),"Triggeraction ",H2I(Act)) 
         set trg = null 
         set Act=null 
endfunction
在这里,我们定义了一个触发器动作变量Act,用它来保存生成的触发器,然后把它删除;大家是不是觉得这种方法似曾相识呢——以前我们排除点泄露的时候,也是用个点变量来保存生存的点,之后再清除这个变量保存的点。在删除点时,我们使用 RemoveLocation( )函数;而删除触发器条件或动作,我们使用TriggerRemoveCondition()和TriggerAddAction()这两个函数。(至于TriggerClearActions()和TriggerClearConditions()这两个函数,老狼说是废函数,“相当于把垃圾扫床底下”,那我们就不去理会它们吧。)
触发器条件和动作的清除的确比较麻烦,不过大家可以采用老狼写的两个删除触发器自定义函数:
jass:  Copy code
function DestroyTriggerAllById takes integer t returns nothing   
         call TriggerRemoveCondition(I2TG(t),I2TC(GetStoredInteger (udg_GC,I2S(t),"TriggerCondition"))) 
         call DestroyTrigger(I2TG(t)) 
         call FlushStoredMission(udg_GC,I2S(t)) 
endfunction
function DestroyTriggerAll takes trigger trg returns nothing 
         call TriggerRemoveCondition(trg,I2TC(GetStoredInteger(udg_GC,I2S(H2I(trg)),"TriggerCondition"))) 
         call DestroyTrigger(trg) 
         call FlushStoredMission(udg_GC,I2S(H2I(trg))) 
endfunction 
//======================================================== 
function RegisterUnitAmortCond takes nothing returns nothing 
            call SetUnitInvulnerable(GetTriggerUnit(), true) 
            call DestroyTriggerAll(GetTriggeringTrigger()) 
endfunction
function RegisterUnitAmortEvent takes unit witchUnit returns nothing 
         local trigger trg = CreateTrigger() 
         call TriggerRegisterUnitStateEvent(trg, witchUnit, UNIT_STATE_LIFE, LESS_THAN_OR_EQUAL, 50) 
         call StoreInteger(udg_GC,I2S(H2I(trg)),"TriggerCondition",H2I (TriggerAddCondition(trg,Condition(function RegisterUnitAmortCond)))) 
         set trg = null 
endfunction
老狼把触发器动作里的内容写到了条件里,为什么这样写大家可以去搜索教程,这里就不赘述了。这里DestroyTriggerAll()和DestroyTriggerAllById()两个函数区别仅仅在于一个传递触发器类型参数,一个传递整数类型参数(这里的整数参数是通过H2I(trg)得到的) ,大家按自己写JASS的习惯,选择其中一个来使用就可以了。此外,为了详细讲解步骤,我定义了Act这个触发器动作变量,但实际写代码时,可以像老狼那样省略这个中间变量,直接把数据存入缓存。 
    大家不妨以老狼这段代码,作为动态注册事件的模板。   
二、缓存的使用
在之前讲解缓存时,我曾说Return Bug改变了GameCache的命运,发掘了GameCache的潜力;那么GameCache究竟强大在哪里。此外,当被问到什么是GameCache,高手们往往会回答,“一个数据库”、“一个仓库”。GameCache是如何担当存储数据的重任呢,我们不妨看下GameCache与全局变量的对比。
1.单个变量
我们要储存一个单位,比如everguo,
数据 
GameCache 
全局变量
Everguo(单位) 
StoreInteger(udg_GC,”Handsome”,”Everguo”,H2I(Everuo)) 
Set Handsome=Everguo
(Handsome为单位型全局变量)
2.数组
数据 
GameCache 
全局变量
1 Everguo
2 汤姆.汗克斯
3 莱昂纳多
StoreInteger(udg_GC,”Handsome”,”1”,H2I(Everuo))
StoreInteger(udg_GC,”Handsome”,”2”,H2I(汤姆.汗克斯))
StoreInteger(udg_GC,”Handsome”,”3”,H2I(莱昂纳多))
Set Handsome[1]=Everguo
Set Handsome[2]=汤姆.汗克斯
Set Handsome[3]=莱昂纳多
(Handsome为单位型全局变量数组)
3.多个数组
数据 
GameCache 
全局变量
1 
2 
StoreInteger(udg_GC,”Handsome”,”1”,H2I(Everuo))
StoreInteger(udg_GC,”Handsome”,”2”,H2I(汤姆.汗克斯))
StoreInteger(udg_GC,”Handsome”,”3”,H2I(莱昂纳多))
StoreInteger(udg_GC,”SeLang”,”1”,H2I(Red_Wolf))
StoreInteger(udg_GC,”SeLang”,”2”,H2I(幽灵狼))
StoreInteger(udg_GC,”SeLang”,”3”,H2I(田伯光))
Set Handsome[1]=Everguo
Set Handsome[2]=汤姆.汗克斯
Set Handsome[3]=莱昂纳多
Set SeLang[1]= Red_Wolf
Set SeLang[2]= 幽灵狼
Set SeLang[3]= 田伯光
(Handsome、SeLang均为单位型全局变量数组)
1 Everguo
2 汤姆.汗克斯
3 莱昂纳多
1 Red_Wolf
2 幽灵狼
3 田伯光
以上是GameCache和全局变量在储存数据上的对比,看上去GameCache储存数据比全局变量繁琐得多;不知大家注意到没有,GameCache跟全局变量相比有个最大的优点——能动态地生成数据库。
比如,在游戏开始后,我们想再储存如下人妖排行榜:
1 东方不败
2 李宇春
3 看帖不回的
用GameCache能轻易做到
jass:  Copy code
StoreInteger(udg_GC,”RY”,”1”,H2I(东方不败))
StoreInteger(udg_GC,”RY”,”2”,H2I(李宇春))
StoreInteger(udg_GC,”RY”,”3”,H2I(看帖不回的))
但用全局变量却做不到,除非我们一开始就定义了“RY”这个单位型全局变量数组。
回想起来,之前我们为什么说JASS不能用T替代,因为Return Bug+GameCache在T里无法做到。比如,在上文排除触发器泄露中,call StoreInteger(udg_GC,I2S(H2I(trg)),"Triggeraction ",H2I(Act)),如果把这条语句用全局变量来写,那么我们必须事先定义好一个触发器动作型全局变量数组;如果我们想用全局变量绑定数据,那得预先定义海量的全局变量数组。
此外,GameCache的灵活性也是无可替代,以下是上次教程讲解Return Bug+GameCache绑定数据时提到例子
jass:  Copy code
function ManData takes unit man,integer stature,integer avoirdupois returns nothing 
         call StoreInteger(udg_GC,I2S(H2I(man)),"stature",stature) 
         call StoreInterger(udg_GC,I2S(H2I(man)),"avoirdupois",avoirdupois)   
endfunction
然后
jass:  Copy code
call   ManData(Lucy,165,45) 
call   ManData(Jack,172,60)
将数据输入。可以看出,只需预先写个系统(比如上文的ManData函数) ,然后再向缓存写入数据就非常方便而且一目了然。
那么,用全局变量的话,如何做到呢
set stature[1]=165 
set stature[2]=172 
set avoirdupois[1]=45 
set avoirdupois[2]=65
首先定义了stature和avoirdupois这两个全局变量数组后,我们还必须有惊人的记忆力,记住数组方括号内的数字对应的单位——这里1对应Lucy,2对应Jack,如果单位多了将很难区分;而缓存在记录数据时,是以字符窜为标识,很形象,比如我们称呼一个人,是称呼“9527”呢,还是“华安”。
    缓存与全局变量数组的区别就在于此,它们各有优缺点。缓存以字符窜为标识,存储数据,这在我们写代码的时候能一目了然;但这也正是它的缺点,写入和读出数据速度比较慢。全局变量数组虽然不形象,难记忆,但效率高。 
    在这里还是向大家推荐缓存,虽然它效率不如全局变量高,但在游戏中差别不大;我们也没必要为提高一点点效率而牺牲代码的可读性。
此外,GameCache还可储存二维数组,比如上文的三个排行榜:
排行榜
排名
1 
2 
3
1 
Everguo 
Red_Wolf 
东方不败
2 
汤姆.汗克斯 
幽灵狼 
李宇春
3 
莱昂纳多 
田伯光 
看帖不回的
把上面数据写入
jass:  Copy code
StoreInteger(udg_GC,”1”,”1”,H2I(Everuo)) 
StoreInteger(udg_GC,”1”,”2”,H2I(汤姆.汗克斯)) 
StoreInteger(udg_GC,”1”,”3”,H2I(莱昂纳多)) 
StoreInteger(udg_GC,”2”,”1”,H2I(Red_Wolf)) 
StoreInteger(udg_GC,”2”,”2”,H2I(幽灵狼)) 
StoreInteger(udg_GC,”2”,”3”,H2I(田伯光)) 
StoreInteger(udg_GC,”3”,”1”,H2I(东方不败)) 
StoreInteger(udg_GC,”3”,”2”,H2I(李宇春)) 
StoreInteger(udg_GC,”3”,”3”,H2I(看帖不回的))
然后我们写个函数方便查找数据
jass:  Copy code
function UnitData takes integer x,integer y returns unit 
      return   I2U(GetStoredInteger(udg_GC,I2S(x),I2S(y))) 
endfunction
如果我们想调用这个二维数组(2,2)对应的单位,只需set unit=UnitData(2,2),就把幽灵狼赋值给了unit。
在T中只有一维数组,因此在处理很多问题时极不方便。而在使用GameCache后,我们可以StoreInteger(udg_GC,I2S(H2I(xx)), I2S(H2I(xx)),H2I(xx))或是GetStoredXXXX(udg_GC,I2S(H2I(xx)),I2S(H2I(xx))),要多方便有多方便。
或许此刻你已经在心中呐喊“Return Bug+GameCache”实在是太YD了,的确,我以前就把他们比喻成奸夫淫妇来着~
    不得不说,Return Bug+GameCache改变了WEer写代码的思维,灵活地运用它们我们可以写出很多实用的系统。 
    或许上面关于Return Bug+GameCache替代全局变量存储数据的例子你不是很懂,不要紧,看完下面的演示你将对Return Bug+GameCache的使用有更清晰的认识。
三.Return bug+GameCache+JASS动态注册事件实例
演示永远是最好的教程。那么,在学习了JASS动态注册事件,以及用Return bug+GameCache存储数据后,我们来看个实例。 
大多数RPG地图都有这样的T——“当单位进入XX区域,立刻移动单位到XX区域”,比如常见的进洞,当英雄进入洞口,立刻被传送到洞内,再从洞内的出口出来。下面是我写的一个3C地图的进洞函数。
jass: Copy code
function H2I takes handle h returns integer 
      return h 
      return 0 
endfunction 
function I2U takes integer i returns unit 
      return i 
      return null 
endfunction        
function Trig_RectUnitMove_Conditions takes nothing returns boolean 
      local unit hero = GetTriggerUnit()               //获得触发单位 
      local trigger trig = GetTriggeringTrigger()      //获得当前触发 
      local player P = GetOwningPlayer(hero) 
      local real x = GetStoredReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveX")      //从缓存中读出目标区域中心的X坐标 
      local real y = GetStoredReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveY")      //从缓存中读出目标区域中心的Y坐标   
      local integer level = GetStoredInteger(udg_GC,I2S(H2I(trig)),"RectUnitMoveLevel")      //从缓存中读出英雄等级限制 
      local integer i=0               
      if IsUnitType(hero, UNIT_TYPE_HERO) != true   or (GetPlayerId(P)>11) then   //如果触发单位不是英雄,或触发玩家是中立玩家 
            set hero=null 
            set
 trig=null 
            return false                                       //直接返回,不执行下面的动作 
      endif      
      set i = GetHeroLevel(hero)   //获得英雄等级      
      if i<level then 
            call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 10, "英雄等级未满"+I2S(level)+"级,不能进入")   
                                                //英雄等级不够,不能进洞 
         else                                                   
            call SetUnitX(hero,x) 
            call SetUnitY(hero,y)            //移动英雄到目标区域中心 
            call PanCameraToTimedForPlayer(P,x,y,0)      //镜头跟随 
            call IssueImmediateOrderById(hero,OrderId("stop"))   
                     //由于用坐标函数移动单位,单位当前状态不变,因此进洞后会继续移动,所以得让它停下来 
      endif   
      set hero=null 
      set trig=null 
      return false               
endfunction 
function RectUnitMoveTrigger takes integer level,rect r1,rect r2 returns nothing 
      local real x = GetRectCenterX(r2)         //获得目标区域中心的X坐标 
      local real y = GetRectCenterY(r2)         //获得目标区域中心的Y坐标   
      local trigger trig=CreateTrigger() 
      call StoreReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveX",x)      // 储存目标区域中心的X坐标   
      call StoreReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveY",y)      // 储存目标区域中心的Y坐标 
      call StoreInteger(udg_GC,I2S(H2I(trig)),"RectUnitMoveLevel",level)   //存储英雄等级限制 
      call TriggerRegisterEnterRectSimple( trig, r1 )               //以单位进入指定区域为事件 
      call TriggerAddCondition(trig,Condition(function Trig_RectUnitMove_Conditions))      //添加条件 
      set trig = null      
endfunction
上面是个很简单的Return bug+GameCache+JASS动态注册事件运用的例子,在学习了前面Return bug原理教程的朋友很容易能看懂,需要说明的是,这里使用Return bug+GameCache绑定数据,没用把目标区域(r2)中心坐标和等级限制绑定在指定区域(r1),而是绑定在了新生成的触发器(trig)上,因为我们可以通过GetTriggeringTrigger()来得到当前的触发,却不能得到当前英雄所在的区域(r1)。此外,我们在绑定数据的时候,除了触发器,一般是跟单位或计时器绑定,然后通过GetTriggerUnit()和GetExpiredTimer()分别获得单位和计时器,然后从单位和计时器上读出预先绑定的数据。 
从上文的例子可以看出,我们先把目标区域中心的坐标写到缓存里,保存在“I2S(H2I(trig))”下,然后通过local trigger trig = GetTriggeringTrigger()来获得当前触发,用GetStoredReal(udg_GC,I2S(H2I(trig)),"XXXXXX")来读出数据。(原理我就不复述了,参看前面Return bug原理教程。) 
(给十分钟,好好看下这段代码。) 
相信你已经明白了,所谓的数据绑定,无非是把数据用Return bug+GameCache把数据存储到缓存中“XXX”的类别名下(这里的类别“XXX”,大都是通过I2S(H2I())将触发器、单位和计时器转换得来的),然后在按上文的方法从缓存读出来。 
那么,我们为何要称这方法为数据绑定呢? 
在讲解Return bug原理的时候,我曾提了两种存储数据的方法,一是上文的以I2S(H2I())转化的handle类参数为类别名,如
jass:  Copy code
call StoreReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveX",x) 
call StoreReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveY",y) 
call StoreInteger(udg_GC,I2S(H2I(trig)),"RectUnitMoveLevel",level)
一是以I2S(H2I())转化的handle类参数为缓存项目名
jass:  Copy code
call StoreReal(udg_GC,"RectUnitMoveX",I2S(H2I(trig)),x) 
call StoreReal(udg_GC,"RectUnitMoveY",I2S(H2I(trig)),y) 
call StoreInteger(udg_GC,"RectUnitMoveLevel",I2S(H2I(trig)),level)
我曾说,第二种方法,不合我们的习惯,为何? 
比如,我们希望这个触发器在运行后自动销毁,此时我们应当将缓存中的数据也清除掉。 
以I2S(H2I())转化的handle类参数为类别名的清除缓存数据
jass:  Copy code
call FlushStoredMission(udg_GC,I2S(H2I(trig)))
以I2S(H2I())转化的handle类参数为缓存项目名的清除缓存数据
jass:  Copy code
FlushStoredReal(udg_GC,"RectUnitMoveX",I2S(H2I(trig))) 
FlushStoredReal(udg_GC,"RectUnitMoveY",I2S(H2I(trig))) 
FlushStoredInteger(udg_GC,"RectUnitMoveLevel",I2S(H2I(trig)))
比较上面两种方法,大家应该明白了为何我们要以I2S(H2I())转化的handle类参数为类别名了吧。用这种方法,可以很容易的把数据与handle类参数挂钩,也可很容易地清除,因此我们称呼这种方法为数据绑定。
当我们把数据绑定到一个单位上,比如以前那个仿暗黑佣兵系统,当单位嗝屁后,可以把与它相关的数据清理掉,释放内存;把数据绑定在触发器上,比如那个环绕函数模板,当环绕时间到了或是释放技能的英雄挂了后,要立刻销毁计时器,同时清空缓存;在前面讲解动态注册事件排泄时,我们看到了,在删除触发器后,也有清除缓存数据的语句。 
这便是数据绑定,也是JASS强大所在;为了让大家印象深刻,我再举个例子。
如果我们想只让特定类型单位进入区域才触发事件,比如3C里羊进圈选英雄,那么我们只需修改下上面的代码
jass: Copy code
function Trig_WC_Conditions takes nothing returns boolean 
      local unit hero = GetTriggerUnit()               //获得触发单位 
      local trigger trig = GetTriggeringTrigger()      //获得当前触发 
      local player P = GetOwningPlayer(hero) 
      local real x = GetStoredReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveX")      //从缓存中读出目标区域中心的X坐标 
      local real y = GetStoredReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveY")      //从缓存中读出目标区域中心的Y坐标 
      local integer WCuid = GetStoredInteger(udg_GC,I2S(H2I(trig)),"WCuid")      //从缓存中读出指定单位类型 
      local integer uid=GetUnitTypeId(hero) 
      local string s=GetStoredString(udg_GC,I2S(H2I(trig)),"String")      
      if WCuid!=uid then 
            call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 10, s) //与指定单位类型不匹配,不能入内                                                
         else                  
            call SetUnitX(hero,x) 
            call SetUnitY(hero,y)            //移动单位到目标区域中心 
            call PanCameraToTimedForPlayer(P,x,y,0)      //镜头跟随 
            call IssueImmediateOrderById(hero,OrderId("stop"))   
                  //由于用坐标函数移动单位,单位当前状态不变,因此进洞后会继续移动,所以得让它停下来 
      endif   
      set hero=null 
      set trig=null 
      return false               
endfunction 
function WCMoveTrigger takes integer uid,rect r1,rect r2,string s returns nothing 
      local real x = GetRectCenterX(r2)         //获得目标区域中心的X坐标 
      local real y = GetRectCenterY(r2)         //获得目标区域中心的Y坐标   
      local trigger trig=CreateTrigger() 
      call StoreReal(ud
g_GC,I2S(H2I(trig)),"RectUnitMoveX",x)      // 储存目标区域中心的X坐标   
      call StoreReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveY",y)      // 储存目标区域中心的Y坐标 
      call StoreInteger(udg_GC,I2S(H2I(trig)),"WCuid",uid)   //存储进入区域单位类型 
      call StoreString(udg_GC,I2S(H2I(trig)),"string",s)   //存储提示文字 
      call TriggerRegisterEnterRectSimple( trig, r1 )               //以单位进入指定区域为事件 
      call TriggerAddCondition(trig,Condition(function Trig_WC_Conditions))      //添加条件 
      set trig = null      
endfunction
上面代码使以单位类型判断替换英雄等级判断,只有特定单位才能进入区域;类似地,你也可以加入单位判断,只有XX单位才能进入,比如“只有FRJJ才能进入看帖不回的人的房间”,只需StoreReal(udg_GC,I2S(H2I(trig)),"Lover",FRJJ),然后比较触发单位是不是FRJJ,这里就不把演示写出来了。
相信通过上面的例子,你已经对Return bug+GameCache有一定了解了,但那是不够的,在下次的教程,我将讲解如何用Return bug+GameCache做更复杂的系统和演示。
BCC工具的使用:
1. 运行bcc.bat
2. 然后将你修改过的地图中的J文件和原正常地图中的J文件复制到软件目录, 比如我的修改过的地图名为 war3map.j 原始未修改的J文件为 war3map_o.j.
3. 在命令行敲入 bcc war3map_o.j war3map.j 如果提示" 恭喜, 生成成功"字样时则为成功, 软件目录中会生成一个new_war3map.j文件,将这个new_war3map.j重命名为war3map.j替换正版地图中的J文件即可.
功能:
1. 可以使作弊地图拥有和正规地图相同的HASH值. 即你使用的是作弊图, 而主机使用的是正规图, 你不会因为地图不同而下载地图.
注意: J文件并不是所有东西都可以改, 引用 Sc2DotA.CoM 的 Thewisp (目前的57c作弊版作者) 一段话:
凡是实体数据都不能改,例如触发,timer,等等所有类型为handle的东西
整数,实数,boolean,字符串等都可以,此外缓存也可以操作。
原理是把缓存单方修改后,用sync系列函数把数据扩散出去,因此他就是合法数据了


