PHP培训
美国上市PHP培训机构

400-111-8989

热门课程

php培训:hiphop 原理分析

  • 时间:2015-01-05
  • 发布:php培训
  • 来源:PHP教程


现在接着上节的分析继续分析:
3.  analyzeProgram详细分析
经过了生成语法树后,就会执行的是analyzeProgram函数(分析阶段),在analyze_result.cpp中AnalysisResult::analyzeProgram实现的,具体功能实现主流程如下:
1.初始化系统变量信息
2.收集作用域内的所有函数、类
3.把变量、常量、类的对象进行排序
4.检查派生类,保存类之间的派生关系
5.执行该文件下的所有analyzeProgram(filescope,statment,expression)
6.收集用户类下的所有函数
7.收集系统类下的所有函数
在analyzeProgram中有几个主要方法:
一个是blockScope::addUse,作用主要就是调用的作用域之间互相保存上对应关系
另一个是添加依赖关系:
addClassDependency
addFunctionDependency
addIncludeDependency
addConstantDependency
上面4个都是在analyzeresult中查找相关内容是否存在,然后如果在不同文件作用域,把其作用域进行关联
然后就是相关作用域中,如果存在变量、常量等值时会将这些值分别读取符号表中的值进行设置值和该符号的声明的表达式或语句等内容
addClassDependency代码分析:
C代码 

/* 
 添加类的依赖关系 
 如果类className在系统类和m_classDecs中,则返回 true否则为false 
*/  
boolAnalysisResult::addClassDependency(FileScopePtr usingFile,  
                                        conststd::string &className) {  
  //查询是否在  BuiltinSymbols::s_classes中  (s_class是系统类)                                   
  if (BuiltinSymbols::s_classes.find(className)!=  
      BuiltinSymbols::s_classes.end())  
    return true;    
  StringToClassScopePtrVecMap::const_iteratoriter =  
    m_classDecs.find(className);  
  //className不在m_classDecs中返回false  
  if (iter == m_classDecs.end() ||!iter->second.size()) return false;  
  ClassScopePtr classScope =iter->second[0];  
  //有重复类或者没有类  
  if (iter->second.size() != 1) {  
    classScope = usingFile->resolveClass(classScope);  
    if (!classScope) return false;  
  }  
  //调用blockscope下的getContainingFile获取文件作用域  
  FileScopePtr fileScope =classScope->getContainingFile();  
  //2个文件作用域关联  
  link(usingFile, fileScope);  
  return true;  
}  
addUse代码分析:  
BlockScopeRawPtrFlagsVec     m_orderedUsers;  
BlockScopeRawPtrFlagsPtrVec  m_orderedDeps;  
Val=map<userBlock,usekinds>  
m_orderedUsers  <val的地址>  
m_orderedDeps  <key:thisBlock,value:val->second的地址>  
例:  
Class A{} classB extends A{}  
Cls(A)->addUse(B,UseKindParentRef)  
Map val= <B,parentRef > 地址:0x10 指向second的是0x18  
B->m_orderedDeps <A,0X18>  
A->m_orderedUsers val  
addUse代码:  
/* 
当前的scope应该是user的父类一般 
将父类保存上子类的作用域+使用类型的map地址m_orderedUsers 
将子类保存上父类的作用域+使用类型的map地址m_orderedDeps 
其实就是scope的m_orderedUsers保存了map<userscope,usekinds> address 
userscope->m_orderedDeps 保存了key:scope;value: map<userscope,usekinds> address<second> 
*/  
voidBlockScope::addUse(BlockScopeRawPtr user, int useKinds) {  
  //判断当前scope如果为classScope则将当前blockscope转为classScope,并判断当前类是否是用户类;  
  //如果不是类,那么判断当前scope是否是函数,如果为函数则返回 scope是functionscope,并判断他是否是用户函数  
  //用户类或用户函数走下面方法否则走else  
  if (is(ClassScope) ?static_cast<HPHP::ClassScope*>(this)->isUserClass() :  
      is(FunctionScope) &&  
     static_cast<HPHP::FunctionScope*>(this)->isUserFunction()) {  
   //判断user作用域是否与 当前scope相同  
    if (user.get() == this) {  
      m_selfUser |= useKinds;  
      return;  
    }  
    Lock lock(s_depsMutex);  
    Lock l2(s_jobStateMutex);  
   //val 等于m_userMap  
   //useKinds 获取的是blockscope中的useKinds的偏移值  
//         m_userMap保存的是key 是user ,value是useKinds  
   std::pair<BlockScopeRawPtrFlagsHashMap::iterator,bool> val =  
     m_userMap.insert(BlockScopeRawPtrFlagsHashMap::value_type(user,  
                                                               useKinds));  
   //val.second为true  
    if (val.second) {  
            //m_orderedUsers保存的是val.first的地址  
            //如$35 =(std::pair<HPHP::hphp_raw_ptr<HPHP::BlockScope> const, int> *)0x5f228e0,保存的就是0x5f228e0  
            //保存的是子类block 和子类usekinds 的map的地址如0x5f228e0  
     m_orderedUsers.push_back(&*val.first);  
    //m_orderedDeps 保存key是当前作用域的block对象;value保存的是val的尾地址  
     //如key:->thisvalue:->0x5f228e8  
      user->m_orderedDeps.push_back(  
         std::make_pair(BlockScopeRawPtr(this), &(val.first->second)));  
      ASSERT(user->getMark() !=BlockScope::MarkReady &&  
             user->getMark() !=BlockScope::MarkWaiting);  
    } else {  
      //设置 当前 block的val的类型进行或操作  
      val.first->second |= useKinds;  
    }  
  }  
}/* 
当前的scope应该是user的父类一般 
将父类保存上子类的作用域+使用类型的map地址m_orderedUsers 
将子类保存上父类的作用域+使用类型的map地址m_orderedDeps  
其实就是scope的m_orderedUsers保存了map<userscope,usekinds> address 
userscope->m_orderedDeps 保存了key:scope;value: map<userscope,usekinds> address<second> 
*/  
voidBlockScope::addUse(BlockScopeRawPtr user, int useKinds) {  
  //判断当前scope如果为classScope则将当前blockscope转为classScope,并判断当前类是否是用户类;  
  //如果不是类,那么判断当前scope是否是函数,如果为函数则返回 scope是functionscope,并判断他是否是用户函数  
  //用户类或用户函数走下面方法否则走else  
  if (is(ClassScope) ?static_cast<HPHP::ClassScope*>(this)->isUserClass() :  
      is(FunctionScope) &&  
      static_cast<HPHP::FunctionScope*>(this)->isUserFunction()){  
   //判断user作用域是否与 当前scope相同  
    if (user.get() == this) {  
      m_selfUser |= useKinds;  
      return;  
    }  
    Lock lock(s_depsMutex);  
    Lock l2(s_jobStateMutex);  
   //val 等于m_userMap  
   //useKinds 获取的是blockscope中的useKinds的偏移值  
//         m_userMap保存的是key 是user ,value是useKinds  
   std::pair<BlockScopeRawPtrFlagsHashMap::iterator,bool> val =  
     m_userMap.insert(BlockScopeRawPtrFlagsHashMap::value_type(user,  
                                                               useKinds));  
   //val.second为true  
    if (val.second) {  
            //m_orderedUsers保存的是val.first的地址  
            //如$35 =(std::pair<HPHP::hphp_raw_ptr<HPHP::BlockScope> const, int> *)0x5f228e0,保存的就是0x5f228e0  
            //保存的是子类block 和子类usekinds 的map的地址如0x5f228e0  
     m_orderedUsers.push_back(&*val.first);  
    //m_orderedDeps 保存key是当前作用域的block对象;value保存的是val的尾地址  
     //如key:->thisvalue:->0x5f228e8  
      user->m_orderedDeps.push_back(  
          std::make_pair(BlockScopeRawPtr(this),&(val.first->second)));  
      ASSERT(user->getMark() !=BlockScope::MarkReady &&  
             user->getMark() !=BlockScope::MarkWaiting);  
    } else {  
      //设置 当前 block的val的类型进行或操作  
      val.first->second |= useKinds;  
    }  
  }  
}  
代码注释分析:
Cpp代码  
void AnalysisResult::analyzeProgram(bool system /* = false */) {  
  AnalysisResultPtr ar =shared_from_this();  
  if (system) m_system = true;  
  //VariableTable::AnyVars 是15  
 getVariables()->forceVariants(ar, VariableTable::AnyVars);  
 getVariables()->setAttribute(VariableTable::ContainsLDynamicVariable);  
 getVariables()->setAttribute(VariableTable::ContainsExtract);  
 getVariables()->setAttribute(VariableTable::ForceGlobal);  
  // Analyze Includes  
 Logger::Verbose("Analyzing Includes");  
  //按文件名排序  
  sort(m_fileScopes.begin(),m_fileScopes.end(), by_filename); // fixed order  
  unsigned int i = 0;  
  //收集文件作用域内的函数和类的scope内容(函数不包括pseudomain)  
  for (i = 0; i <m_fileScopes.size(); i++) {  
   collectFunctionsAndClasses(m_fileScopes[i]);  
  }  
  // Keep generated codeidentical without randomness  
  //把变量、常量、类进行排序  
  canonicalizeSymbolOrder();  
  // Analyze some specialcases (指定用例不知道是什么)  
  //这里应该是遍历重复类  
  for(set<string>::const_iterator it = Option::VolatileClasses.begin();  
       it !=Option::VolatileClasses.end(); ++it) {  
    ClassScopePtr cls =findClass(Util::toLower(*it));  
    if (cls &&cls->isUserClass()) {  
      cls->setVolatile();  
    }  
  }  
  //检查派生类,保存类之间的依赖关系  
  checkClassDerivations();     
  // Analyze All  
 Logger::Verbose("Analyzing All");  
 setPhase(AnalysisResult::AnalyzeAll);  
  for (i = 0; i <m_fileScopes.size(); i++) {  
   m_fileScopes[i]->analyzeProgram(ar);  
  }     
  /* 
    Note that cls->collectMethods()can add entries to m_classDecs, 
    which can invalidateiterators. So we have to create an array 
    and then iterate overthat. 
    The new entries added tom_classDecs are always empty, so it 
    doesnt matter that we dontinclude them in the iteration 
  */  
  ClassScopePtr cls;  
 std::vector<ClassScopePtr> classes;  
  //设置存储空间(reserve)  
  //将m_classDecs中的所有classscope保存到classes中  
 classes.reserve(m_classDecs.size());  
  for(StringToClassScopePtrVecMap::const_iterator iter = m_classDecs.begin();  
       iter !=m_classDecs.end(); ++iter) {  
    BOOST_FOREACH(cls,iter->second) {  
      classes.push_back(cls);  
    }  
  }     
  //收集类方法  
  // Collect methods  
  BOOST_FOREACH(cls, classes){  
    // 是否重新定义过类(同名)  
    if(cls->isRedeclaring()) {  
     cls->setStaticDynamic(ar);  
    }  
   StringToFunctionScopePtrMap methods;  
    cls->collectMethods(ar,methods);  
         //derivesFromRedeclaring是重载 的派生类  
         /*需要进行实现的,该类不是抽象的、不是接口,并且是正常来源*/  
         bool needAbstractMethodImpl=  
      (!cls->isAbstract()&& !cls->isInterface() &&  
      !cls->derivesFromRedeclaring() &&  
      !cls->getAttribute(ClassScope::UsesUnknownTrait));  
    for(StringToFunctionScopePtrMap::const_iterator iterMethod =  
           methods.begin();iterMethod != methods.end(); ++iterMethod) {  
      FunctionScopePtr func =iterMethod->second;  
           //方法未实现并且是抽象函数的进入该方法  
      if (!func->hasImpl()&& needAbstractMethodImpl) {  
        FunctionScopePtrtmpFunc =  
          cls->findFunction(ar,func->getName(), true, true);  
        assert(!tmpFunc ||!tmpFunc->hasImpl());  
       Compiler::Error(Compiler::MissingAbstractMethodImpl,  
                       func->getStmt(), cls->getStmt());  
      }  
           /*将该方法所涉及的类push到m_methodToClassDecs中 
           * 如接口和实现类有2个cls都有该方法,那么把这2个cls添加到m_methodToClassDecs 
           *对应的方法的key 中去,这个主要为多态做的封装key是方法,value 是方法的各种 
           *多态类,如一个方法在3个类中,祖父类、父类、子类,那么push_back中会有这3个cls 
           */  
           /* 
           * key 函数名称 ,value是涉及该函数名的多个类 
           */  
     m_methodToClassDecs[iterMethod->first].push_back(cls);  
    }  
  }     
  //系统类收集方法  
  string cname;  
  BOOST_FOREACH(tie(cname,cls), m_systemClasses) {  
   StringToFunctionScopePtrMap methods;  
    cls->collectMethods(ar,methods);  
    for(StringToFunctionScopePtrMap::const_iterator iterMethod =  
           methods.begin(); iterMethod !=methods.end(); ++iterMethod) {  
     m_methodToClassDecs[iterMethod->first].push_back(cls);  
    }  
  }   
  // Analyze perfect virtuals  
  if(Option::AnalyzePerfectVirtuals && !system) {  
    analyzePerfectVirtuals();  
  }  
}  

analyzeProgram的实现也是用的多态进行实现的,通过文件遍历后,然后去遍历下面的各个表达式和语句中的analyzeProgram来实现具体内容:
      3.1.  Classstatement=>analyzeProgram分析
语句形式:Class A{ …}
处理逻辑:
如果存在父类,将父类名字保存到bases集合中
遍历bases,判断是否在系统类和用户类中, addUserClass(ar, bases[i]);
检测父类是否为易变,如果父类为动态则子类也设置为易变的
遍历class下的其他语句的analyzeProgram
如果非静态分析阶段返回空
记录类在文件中的行位置(文件名、类名、行数)
For(bases){
查找父类cls,如果类存在
If(cls不是接口并且父类名称为空并且父类数量大于0||cls为接口并且父类名字不为空,cls数量是0||cls的名字不为空并且父类数量为0)
抛出异常
If(cls是用户类)
添加当前类和父类的作用域使用关系(UseKindParentRef)
}
      3.2.  Interfacestatement=>analyzeProgram分析:
语句形式:interface A{ …}
处理逻辑:
(1)调用接口下的所有语句的analyzeProgram进行下层语句分析
(2)检测父类是否为易变,如果父类为动态则子类也设置为易变的
(3)记录接口在文件中的行位置(文件名、接口名、行数)
(4)如果存在父类,将父类名字保存到bases集合中
(5)for(bases){
addUserClass(ar, bases[i]);
查找基类cls
如果cls不是接口,抛出异常
If(cls是用户类)
添加当前类和父类的作用域使用关系(UseKindParentRef)
}
3.3.  functionStatement=>analyzeProgram分析:
主要作用对方法是否重复进行判断然后获取到当前方法所在的域,之后调用methodstatement的analyzeprogram进行分析。
1.通过filescope中的m_pseudoMain->getStmt()->analyzeProgram(ar)进入到FunctionStatement::analyzeProgram方法;
2. analyzeProgram内部流程:
1)首先获取当前function的作用域范围;返回m_blockScope。
2)if(fs->isVolatile())getScope()->getOuterScope()->getContainingFunction()
获取方法的外层作用域并进一步或得到作用域中所有的方法,如果域中存在方法那么获取方法表中的符号表并进行按位或操作,从而对m_attribute里的内容进行设置。
3)if(ar->getPhase() == AnalysisResult::AnalyzeFinal) {。。。}
只有当分析阶段进行到最后时才进入
4)MethodStatement::analyzeProgram(ar);
调用methodstatement中的analyzeProgram对方法进行分析。
3.4.  Methodstatement=>analyzeProgram分析:
1.获取到当前方法所在的作用域范围
2. if (m_params) {如果方法存在参数
                   对参数进行判断,如果有参数则调用参数分析函数进行分析
         }
3. if (m_stmt){如果存在语句
                   调用statement_list中的analyzeProgram对语句进行分析。
                   在statement_list通过size算出表达式有多少条,在通过for循环对每一条语句进行分析,如果语句也为statement形式则继续调用对应的语句表达式进行分析,直到分析的语句形式为expression。
         }
4. if (ar->getPhase() ==AnalysisResult::AnalyzeAll) {
                   为参数设置默认值,如果参数有赋值则采用赋值内容。
                   如果当前作用域是指定的扩展或者将方法名字定义为了动态类型
         }
5. 对当前方法的名字长度、名字内容(是否包含offset)进行检查设置参数个数以及是否魔术函数进行设置并对执行覆盖操作。如__get,__set方法。如果实际参数个数大于系统默认设置的参数个数编译报错。
      3.5.  Returnstatement=>analyzeProgram分析:
1.判断return中的内容是否为m_exp表达式,如果是的话获取当前renturn所在的方法作用域并判断return的值是否是引用形式,如果是引用,设置m_context为引用状态。接着在对return的语句内容进行分析。
Switchstatement
1.对表达式进行分析,如switch $A。如果值则进入scalar(如switch(1)),如果是变量则进入SimpleVariable进行分析(switch($a))。
2.对switch下的case进行判断,如果存在case语句则调用casestatement中的analyzeProgram进行分析。
3.6.  Staticstatement=>analyzeProgram分析:
语句形式:static $a=“a”;或static $a;
处理逻辑:
 m_exp->analyzeProgram(ar); (1)
If(阶段是静态分析阶段){
If(m_exp类型是simpleVarible){
//static $a;
创建一个新的AssignmentExpression,m_varible存放$a(simpleVarible),m_value存放一个空的常量,然后m_exp赋值成一个新的赋值语句(如static$a=null);
}
获取assigment的m_varibale,m_value,然后将m_varibale的符号表中保存值为m_value,符号表(symbol)状态为静态;m_varibale表达式上下文状态设置为Declaration (2)
}
3.7.  simple_function_cal和function_call=>analyzeProgram分析:
1.simple_function_call的形式如同test();
2.内部流程如下:
1)调用functioncall的analyzeprogram方法对是否类方法、方法的调用形式以及方法的参数进行分析。
2)以上分析完成后,添加方法和域之间的依赖关系(判断函数或类进行不同作用域的关联),如果处于静态分析阶段,则为当前调用的方法寻找正确的functionscope和classscope;当调用方法是类方法时将方法的名称以及继承方法转换成小写形式
3.8.  Object_menthod_expression
具体形式如$c->test1()
内部分析过程:
1)调用function的analyzeprogram方法解析test1()
2)调用object的analyzeprogram方法对c进行分析,根据object的类型判断调用哪种形式。
3)如果当前处于静态分析阶段,则获取当前所在的作用域,判断object是否为this关键字并且方法名不为空,此时在获取方法所在的类作用域并查找当前方法,判断方法是否为接口等,并为当前方法和对象之间的调用关系进行关联
4)标记引用形式的参数
3.9.  Object_property_expression
具体形式如$c::f=3;
1)先对$c进行object的analyzeprogram分析
2)对属性的表达形式进行分析。
3)如果是处于静态分析阶段则进入后面的阶段。
3.10.    simpleVariable
simpleVariable的analyzeprogram主要作用是首先到符号表中判断当前变量是否是superglobal(包括$GLOBALS、$_SERVER、$_GET、$_POST、$_FILES、$_COOKIE、$_SESSION、$_REQUEST、$_ENV),再去获取superglobal的类型,对变量名字是否为GLOBALS进行判断,在当前作用域下创建符号表,把该变量加入到作用域的符号表中;
如果变量名称为this并且处于虚函数或类中,满足条件将此变量写入类作用域的变量表中。将此变量设为使用状态。
3.11.    Scalarexpression
Scalarexpression的analyzeprogram主要作用是首先判断当前状态是否为静态分析状态,如果是将当前内容转换为小写形式。然后判断m_type类型(行号T_LINE、命名空间T_NS_C、类下的方法T_METHOD_C、独立方法T_FUNC_C),针对不同类型相应设置。
3.12.    Assignmentexpression
Assignmentexpression的analyzeprogram主要作用是先对赋值表达式左侧的变量进行分析,在对右侧的值进行分析。如果当前处于静态分析状态,判断变量类型是否为simplevariable类型,如果是将该变量写入到作用域的符号表中,并置为使用状态。
3.13.    new_object_expresssion=>analyzeProgram分析:
表达式形式:new A(“a”)
处理逻辑:
addUserClass(ar, m_name);
If(存在父类则返回父类,否则返回本类cls){
查找类的构造函数(fun)
Fun调用了addUseUseKindCallerInline |UseKindCallerParam 
标记参数是否为引用(&$a)
}
3.14.    classVarible=>analyzeProgram分析:
语句形式:class A{public $a;}
处理逻辑:
(1) 处理声明表达式(m_declaration)的analyzeProgram($a)
(2)判断是否是静态分析阶段
(3) for(m_declaration){
If(exp类型是assignmentExpression){
//publi $a=“aa”;
取出赋值表达式的变量(m_variable)和值(m_value)
将m_value的值写入到当前类的变量表中,变量名为m_variable的名字,并且设置该变量的状态为类变量值
判断父类是否有该变量,如果有将其覆盖;
}else{
//public $a;
获取simpleVarible($a),判断变量是否父类存在,如果存在将父类的该变量设置为覆盖状态;然后将该变量的值写入到类的符号表中(值为null)
}
}
3.15.    static_member_expression=>analyzeProgram分析:
表达式形式: B::$c
处理逻辑:
调用findMember函数,如果存在父类,则从父类中取该成员,如果没有父类则从当前类中的变量表中获取该成员;然后判断如果该成员不为空并为静态成员,那么返回该成员(sym),否则返回空
m_resolvedClass(如果没有父类为当前类,如果有则为父类对象)设置使用方式是静态引用(UseKindStaticRef)
sym为空,非动态类,名字为空,阶段为AnalyzeFinal,抛出异常
addUserClass(ar, m_className);
3.16.    ConstantExpression=>analyzeProgram分析:
表达式语句:echo c;
处理逻辑:
(1)判断是静态分析阶段
(2)获取符号表,首先判断是否是命名空间,如果是,截取掉命名空间,通过名字在作用域中查找常量的sym
(3)如果非动态、非系统的,则获取声明结构,设置声明的作用使用关系
addUse(getScope(),BlockScope::UseKindConstRef)
4.  变量表分析
Symbal:
m_name (符号名字,变量、常量)
m_hash (hash值)
m_flags (状态值,如是否是静态,是否是类值,修饰符等)
m_declaration 引用该symbal 的对象(statement,expression)
m_value   变量值
m_initVal 初始值
Symbal_table:
m_symbolMap 保存的是symbol 信息key 是名字,value 是sym
在hiphop中许多的处理是通过设置枚举,然后通过位运算来判断类型的,比如这个varibable的attribute或者如修饰符(public,protected,private)等;
l  设置值一般通过按位或进行设置;
l  清空值是(本值&~需删除的值)
l  比较是按位与
设置变量属性原理:
 Attribute 的枚举为:
  1
  11
  100
 1000
 10000
 100000
 1000000
 10000000
 100000000
 ......
 10000000000
VariableTable(variable_table.h)
enum Attribute {
    ContainsDynamicVariable = 1,
         //11
    ContainsLDynamicVariable =ContainsDynamicVariable | 2,
    //100
    ContainsExtract = 4,
    //1000
    ContainsCompact = 8,
    //10000
    InsideStaticStatement = 16,
    //100000
    InsideGlobalStatement = 32,
    //1000000
    ForceGlobal = 64,
    //10000000
    ContainsUnset = 128,
    //10000000
    NeedGlobalPointer = 256,
    //100000000
    ContainsDynamicStatic  = 512,
    //1000000000
    ContainsGetDefinedVars = 1024,
    //10000000000
    ContainsDynamicFunctionCall = 2048,
  };
当做set操作时,原理如下:如前6个进行set操作(|或)操作
  那么值为:111111
  如果只做6个set ,其中第3个(100)没有set操作,那么按位或后的值为:111011
  操作步骤如下:
 1|11=11,11|1000=1011,1011|10000=11011,11011|100000=111011
  这个是set(或)操作,也就是,如果哪个没有设置进去,该位的数应该为0,比如100没有
  set,那么在第3位就是0(111011),以此原理类推;
  清空所在元素原理:
  比如现在有6个元素进行了按位与后值为(111111),那么我们要清空1000,那么执行的操作是clear
  也就是m_attribute=m_attribute&~attr;
  那么执行操作:m_attribute=111111&~(1000)=>111111&110111=110111
  这样按位与后,第4位变为了0,那么说明1000已经被清空了,取反过程是以最长数为准
  (如1000是4位,需要按111111的位数取反,最终成为110111)
比较:
  get
  如现在的m_attribute是110111
  那么我们现在要查2个结果是否在m_attribute中,一个是1000,另一个是100
  首先查1000 ,算法是按位与:m_attribute=m_attribute&attr;m_attribute=110111&1000=>110111&001000=>0,所以不存在
  然后查100,算法是按位与:m_attribute=m_attribute&attr;m_attribute=110111&100=>110111&000100=>000100=>100
  所以返回的是100本身,这样说明其存在,对比成功
  然后其他类似context等这样类似的功能,算法都是一样的。
上一篇:达内php培训 让每位学员学有所成
下一篇:php 常用函数收集

达内php培训 让每位学员学有所成

php培训:hiphop 原理分析

学习php用什么开发工具好?

PHP算法之冒泡排序

选择城市和中心
贵州省

广西省

海南省