热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

leveldb代码阅读(14)——Level和Compaction

原文地址:http:www.blogjava.netsandyarchive20120315leveldb6.htmlleveldb之所以使用level作为数据库名

原文地址:http://www.blogjava.net/sandy/archive/2012/03/15/leveldb6.html

leveldb之所以使用level作为数据库名称,精华就在于level的设计。



本质是一种归并排序算法。
这样设计的好处主要是可以减少compaction的次数和每次的文件个数。

Compaction


  • 为什么要compaction?
 compaction可以提高数据的查询效率,没有经过compaction,需要从很多SST file去查找,而做过compaction后,只需要从有限的SST文件去查找,大大的提高了随机查询的效率,另外也可以删除过期数据。


  • 什么时候可能进行compaction?
 1. database open的时候

 2. write的时候

 3. read的时候?




//是否要进行compaction
void DBImpl::MaybeScheduleCompaction() {
  mutex_.AssertHeld();
  
if (bg_compaction_scheduled_) { //已经在进行
  } else if (shutting_down_.Acquire_Load()) {
  } 
else if (imm_ == NULL &&
             manual_compaction_ 
== NULL &&
             
!versions_->NeedsCompaction()) {
    
//imm_为NULL:没有memtable需要flush
    
//manual_compaction_:手动compaction
  } else {
    bg_compaction_scheduled_ 
= true;
    env_
->Schedule(&DBImpl::BGWork, this);
  }
}






bool NeedsCompaction() const {
    Version
* v = current_;
    
return (v->compaction_score_ >= 1|| (v->file_to_compact_ != NULL);
  }



如何计算这个compaction_score呢?看下面的代码:




void VersionSet::Finalize(Version* v) {
  
int best_level = -1;
  
double best_score = -1;

//遍历所有的level  
for (int level &#61; 0; level < config::kNumLevels-1; level&#43;&#43;) {
    
double score;
    
if (level &#61;&#61; 0) {
      
//对于level 0,计算当前文件个数和预定义的compaction trigger value(Default:4)之比
      score &#61; v->files_[level].size() /
          static_cast
<double>(config::kL0_CompactionTrigger);
    } 
else {
      
//对于其他level,计算level文件大小和level应有的大小(10^N MB)
      const uint64_t level_bytes &#61; TotalFileSize(v->files_[level]);
      score 
&#61; static_cast<double>(level_bytes) / MaxBytesForLevel(level);
    }
     
//找出最需要compaction的level
    if (score > best_score) {
      best_level 
&#61; level;
      best_score 
&#61; score;
    }
  }

  v
->compaction_level_ &#61; best_level;
  v
->compaction_score_ &#61; best_score;
}


  • 如何做compaction&#xff1f;

leveldb 运行会启动一个background thread,会执行一些background task&#xff0c;compaction就在这个线程中执行。




首先来看看compaction对象如何定义的




//关于compaction的一些信息
class Compaction {
 
public:
  
~Compaction();

  
//compaction Level:会将N层N&#43;1层合并生成N&#43;1文件
  int level() const { return level_; }

  
//返回VersionEdit,用于记录到manifest
  VersionEdit* edit() { return &edit_; }

  
//返回N层或者N&#43;1层的文件个数&#xff0c;which &#61; 0,1
  int num_input_files(int which) const { return inputs_[which].size(); }

  
//返回具体的文件信息,which:level
  FileMetaData* input(int which, int i) const { return inputs_[which][i]; }

  
//本次compaction最大输出字节
  uint64_t MaxOutputFileSize() const { return max_output_file_size_; }

  
//是否只需要移动文件进行compaction,不需要merge和split
  bool IsTrivialMove() const;

  
//把input都当成delete写入edit
  void AddInputDeletions(VersionEdit* edit);

  
// Returns true if the information we have available guarantees that
  
// the compaction is producing data in "level&#43;1" for which no data exists
  
// in levels greater than "level&#43;1".
  bool IsBaseLevelForKey(const Slice& user_key);

  
// Returns true iff we should stop building the current output
  
// before processing "internal_key".
  bool ShouldStopBefore(const Slice& internal_key);

  
// Release the input version for the compaction, once the compaction
  
// is successful.
  void ReleaseInputs();

 
private:
  friend 
class Version;
  friend 
class VersionSet;

  
explicit Compaction(int level);

  
int level_;
  uint64_t max_output_file_size_;
  Version
* input_version_;
  VersionEdit edit_;

  
// Each compaction reads inputs from "level_" and "level_&#43;1"
  std::vector<FileMetaData*> inputs_[2];      // The two sets of inputs

  
// State used to check for number of of overlapping grandparent files
  
// (parent &#61;&#61; level_ &#43; 1, grandparent &#61;&#61; level_ &#43; 2)
  std::vector<FileMetaData*> grandparents_;
  size_t grandparent_index_;  
// Index in grandparent_starts_
  bool seen_key_;             // Some output key has been seen
  int64_t overlapped_bytes_;  // Bytes of overlap between current output
                              
// and grandparent files

  
// State for implementing IsBaseLevelForKey

  
// level_ptrs_ holds indices into input_version_->levels_: our state
  
// is that we are positioned at one of the file ranges for each
  
// higher level than the ones involved in this compaction (i.e. for
  
// all L >&#61; level_ &#43; 2).
  size_t level_ptrs_[config::kNumLevels];
};



Compaction Thread




void DBImpl::BackgroundCompaction() {
  mutex_.AssertHeld();

  
//把memtable flush到sstable
  if (imm_ !&#61; NULL) {
    CompactMemTable();
    
return;
  }

  Compaction
* c;
  
bool is_manual &#61; (manual_compaction_ !&#61; NULL);
  InternalKey manual_end;
  
if (is_manual) { //手动compaction
    ManualCompaction* m &#61; manual_compaction_;
    
//根据range来做compaction
    c &#61; versions_->CompactRange(m->level, m->begin, m->end);
    m
->done &#61; (c &#61;&#61; NULL);
    
if (c !&#61; NULL) {
      manual_end 
&#61; c->input(0, c->num_input_files(0- 1)->largest;
    }
    Log(options_.info_log,
        
"Manual compaction at level-%d from %s .. %s; will stop at %s\n",
        m
->level,
        (m
->begin ? m->begin->DebugString().c_str() : "(begin)"),
        (m
->end ? m->end->DebugString().c_str() : "(end)"),
        (m
->done ? "(end)" : manual_end.DebugString().c_str()));
  } 
else {
    
//找到需要compaction的level&file
    c &#61; versions_->PickCompaction();
  }

  Status status;
  
if (c &#61;&#61; NULL) {
    
// Nothing to do
  } else if (!is_manual && c->IsTrivialMove()) { //只需要移动sst file
    
// Move file to next level
    assert(c->num_input_files(0&#61;&#61; 1);
    FileMetaData
* f &#61; c->input(00);
    c
->edit()->DeleteFile(c->level(), f->number);
    c
->edit()->AddFile(c->level() &#43; 1, f->number, f->file_size,
                       f
->smallest, f->largest);
    status 
&#61; versions_->LogAndApply(c->edit(), &mutex_);
    VersionSet::LevelSummaryStorage tmp;
    Log(options_.info_log, 
"Moved #%lld to level-%d %lld bytes %s: %s\n",
        static_cast
<unsigned long long>(f->number),
        c
->level() &#43; 1,
        static_cast
<unsigned long long>(f->file_size),
        status.ToString().c_str(),
        versions_
->LevelSummary(&tmp));
  } 
else {//完成compaction
    CompactionState* compact &#61; new CompactionState(c);
    status 
&#61; DoCompactionWork(compact);
    CleanupCompaction(compact);
    c
->ReleaseInputs();
    DeleteObsoleteFiles();
  }
  delete c;

  
if (status.ok()) {
    
// Done
  } else if (shutting_down_.Acquire_Load()) {
    
// Ignore compaction errors found during shutting down
  } else {
    Log(options_.info_log,
        
"Compaction error: %s", status.ToString().c_str());
    
if (options_.paranoid_checks && bg_error_.ok()) {
      bg_error_ 
&#61; status;
    }
  }

  
if (is_manual) {
    ManualCompaction
* m &#61; manual_compaction_;
    
if (!status.ok()) {
      m
->done &#61; true;
    }
    
if (!m->done) {
      
// We only compacted part of the requested range.  Update *m
      
// to the range that is left to be compacted.
      m->tmp_storage &#61; manual_end;
      m
->begin &#61; &m->tmp_storage;
    }
    manual_compaction_ 
&#61; NULL;
  }
}



compaction memtable:写一个level0文件&#xff0c;并写入manifest log


Status DBImpl::CompactMemTable() {
  mutex_.AssertHeld();
  assert(imm_ 
!&#61; NULL);

  VersionEdit edit;
  Version
* base &#61; versions_->current();
  
base->Ref();
  
//写入level0 sst table
  Status s &#61; WriteLevel0Table(imm_, &edit, base);
  
base->Unref();

  
if (s.ok() && shutting_down_.Acquire_Load()) {
    s 
&#61; Status::IOError("Deleting DB during memtable compaction");
  }

  
// Replace immutable memtable with the generated Table
  if (s.ok()) {
    edit.SetPrevLogNumber(
0);
    edit.SetLogNumber(logfile_number_);  
// Earlier logs no longer needed
    
//生成edit并计入manifest log
    s &#61; versions_->LogAndApply(&edit, &mutex_);
  }

  
if (s.ok()) {
    
// Commit to the new state
    imm_->Unref();
    imm_ 
&#61; NULL;
    has_imm_.Release_Store(NULL);
    DeleteObsoleteFiles();
  }

  
return s;
}



下面来看看compaction已有文件&#xff1a;

找出要compaction的文件:




Compaction* VersionSet::PickCompaction() {
  Compaction
* c;
  
int level;

//是否需要compaction,有两种compaction&#xff0c;一种基于size大小&#xff0c;另外一种基于被seek的次数 
const bool size_compaction &#61; (current_->compaction_score_ >&#61; 1);
  
const bool seek_compaction &#61; (current_->file_to_compact_ !&#61; NULL);
  
if (size_compaction) {
    level 
&#61; current_->compaction_level_;
    assert(level 
>&#61; 0);
    assert(level
&#43;1 < config::kNumLevels);
    c 
&#61; new Compaction(level);

    
//每一层有一个compact_pointer&#xff0c;用于记录compaction key&#xff0c;这样可以进行循环compaction
    for (size_t i &#61; 0; i < current_->files_[level].size(); i&#43;&#43;) {
      FileMetaData
* f &#61; current_->files_[level][i];
      
if (compact_pointer_[level].empty() ||
          icmp_.Compare(f
->largest.Encode(), compact_pointer_[level]) > 0) {
        
//找到一个文件就可以了
        c->inputs_[0].push_back(f);
        
break;
      }
    }
    
if (c->inputs_[0].empty()) {
      
// Wrap-around to the beginning of the key space
      c->inputs_[0].push_back(current_->files_[level][0]);
    }
  } 
else if (seek_compaction) {
    level 
&#61; current_->file_to_compact_level_;
    c 
&#61; new Compaction(level);
    c
->inputs_[0].push_back(current_->file_to_compact_);
  } 
else {
    
return NULL;
  }

  c
->input_version_ &#61; current_;
  c
->input_version_->Ref();

  
// level 0&#xff1a;特殊处理&#xff0c;因为可能有key 重叠&#xff0c;把所有重叠都找出来&#xff0c;一起做compaction
  if (level &#61;&#61; 0) {
    InternalKey smallest, largest;
    GetRange(c
->inputs_[0], &smallest, &largest);
    
// Note that the next call will discard the file we placed in
    
// c->inputs_[0] earlier and replace it with an overlapping set
    
// which will include the picked file.
    current_->GetOverlappingInputs(0&smallest, &largest, &c->inputs_[0]);
    assert(
!c->inputs_[0].empty());
  }

  
//找到level N&#43;1需要compaction的文件
  SetupOtherInputs(c);

  
return c;
}






void VersionSet::SetupOtherInputs(Compaction* c) {
  
const int level &#61; c->level();
  InternalKey smallest, largest;
  GetRange(c
->inputs_[0], &smallest, &largest);

  
//找到所有在Level N&#43;1层有重叠的文件
   current_->GetOverlappingInputs(level&#43;1&smallest, &largest, &c->inputs_[1]);

  
//取出key的范围
  InternalKey all_start, all_limit;
  GetRange2(c
->inputs_[0], c->inputs_[1], &all_start, &all_limit);

  
//检查是否能从Level N找到更多的文件
  if (!c->inputs_[1].empty()) {
    std::vector
<FileMetaData*> expanded0;
    current_
->GetOverlappingInputs(level, &all_start, &all_limit, &expanded0);
    
const int64_t inputs0_size &#61; TotalFileSize(c->inputs_[0]);
    
const int64_t inputs1_size &#61; TotalFileSize(c->inputs_[1]);
    
const int64_t expanded0_size &#61; TotalFileSize(expanded0);
    
if (expanded0.size() > c->inputs_[0].size() &&
        inputs1_size 
&#43; expanded0_size < kExpandedCompactionByteSizeLimit) {
      InternalKey new_start, new_limit;
      GetRange(expanded0, 
&new_start, &new_limit);
      std::vector
<FileMetaData*> expanded1;
      current_
->GetOverlappingInputs(level&#43;1&new_start, &new_limit,
                                     
&expanded1);
      
if (expanded1.size() &#61;&#61; c->inputs_[1].size()) {
        Log(options_
->info_log,
            
"Expanding&#64;%d %d&#43;%d (%ld&#43;%ld bytes) to %d&#43;%d (%ld&#43;%ld bytes)\n",
            level,
            
int(c->inputs_[0].size()),
            
int(c->inputs_[1].size()),
            
long(inputs0_size), long(inputs1_size),
            
int(expanded0.size()),
            
int(expanded1.size()),
            
long(expanded0_size), long(inputs1_size));
        smallest 
&#61; new_start;
        largest 
&#61; new_limit;
        c
->inputs_[0&#61; expanded0;
        c
->inputs_[1&#61; expanded1;
        GetRange2(c
->inputs_[0], c->inputs_[1], &all_start, &all_limit);
      }
    }
  }

  
// Compute the set of grandparent files that overlap this compaction
  
// (parent &#61;&#61; level&#43;1; grandparent &#61;&#61; level&#43;2)
  if (level &#43; 2 < config::kNumLevels) {
    current_
->GetOverlappingInputs(level &#43; 2&all_start, &all_limit,
                                   
&c->grandparents_);
  }

  
if (false) {
    Log(options_
->info_log, "Compacting %d &#39;%s&#39; .. &#39;%s&#39;",
        level,
        smallest.DebugString().c_str(),
        largest.DebugString().c_str());
  }

  
//设置新的compact_pointer
  compact_pointer_[level] &#61; largest.Encode().ToString();
  c
->edit_.SetCompactPointer(level, largest);
}



do compaction task:


Status DBImpl::DoCompactionWork(CompactionState* compact) {
  
const uint64_t start_micros &#61; env_->NowMicros();
  int64_t imm_micros 
&#61; 0;  // Micros spent doing imm_ compactions

  Log(options_.info_log,  
"Compacting %d&#64;%d &#43; %d&#64;%d files",
      compact
->compaction->num_input_files(0),
      compact
->compaction->level(),
      compact
->compaction->num_input_files(1),
      compact
->compaction->level() &#43; 1);

  assert(versions_
->NumLevelFiles(compact->compaction->level()) > 0);
  assert(compact
->builder &#61;&#61; NULL);
  assert(compact
->outfile &#61;&#61; NULL);
  
if (snapshots_.empty()) {
    compact
->smallest_snapshot &#61; versions_->LastSequence();
  } 
else {
    compact
->smallest_snapshot &#61; snapshots_.oldest()->number_;
  }

  
// Release mutex while we&#39;re actually doing the compaction work
  mutex_.Unlock();

  
//生成iterator:遍历要compaction的数据
  Iterator* input &#61; versions_->MakeInputIterator(compact->compaction);
  input
->SeekToFirst();
  Status status;
  ParsedInternalKey ikey;
  std::
string current_user_key;
  
bool has_current_user_key &#61; false;
  SequenceNumber last_sequence_for_key 
&#61; kMaxSequenceNumber;
  
for (; input->Valid() && !shutting_down_.Acquire_Load(); ) {
    
// 如果有memtable要compaction:优先去做
    if (has_imm_.NoBarrier_Load() !&#61; NULL) {
      
const uint64_t imm_start &#61; env_->NowMicros();
      mutex_.Lock();
      
if (imm_ !&#61; NULL) {
        CompactMemTable();
        bg_cv_.SignalAll();  
// Wakeup MakeRoomForWrite() if necessary
      }
      mutex_.Unlock();
      imm_micros 
&#43;&#61; (env_->NowMicros() - imm_start);
    }

    Slice key 
&#61; input->key();
    
//检查是不是中途输出compaction的结果&#xff0c;避免compaction结果和level N&#43;2 files有过多的重叠
    if (compact->compaction->ShouldStopBefore(key) &&
        compact
->builder !&#61; NULL) {
      status 
&#61; FinishCompactionOutputFile(compact, input);
      
if (!status.ok()) {
        
break;
      }
    }

    
// Handle key/value, add to state, etc.
    bool drop &#61; false;
    
if (!ParseInternalKey(key, &ikey)) {
      
// Do not hide error keys
      current_user_key.clear();
      has_current_user_key 
&#61; false;
      last_sequence_for_key 
&#61; kMaxSequenceNumber;
    } 
else {
      
if (!has_current_user_key ||
          user_comparator()
->Compare(ikey.user_key,
                                     Slice(current_user_key)) 
!&#61; 0) {
        
// First occurrence of this user key
        current_user_key.assign(ikey.user_key.data(), ikey.user_key.size());
        has_current_user_key 
&#61; true;
        last_sequence_for_key 
&#61; kMaxSequenceNumber;
      }

      
if (last_sequence_for_key <&#61; compact->smallest_snapshot) {
        
// Hidden by an newer entry for same user key
        drop &#61; true;    // (A)
      } else if (ikey.type &#61;&#61; kTypeDeletion &&
                 ikey.sequence 
<&#61; compact->smallest_snapshot &&
                 compact
->compaction->IsBaseLevelForKey(ikey.user_key)) {
        
// For this user key:
        
// (1) there is no data in higher levels
        
// (2) data in lower levels will have larger sequence numbers
        
// (3) data in layers that are being compacted here and have
        
//     smaller sequence numbers will be dropped in the next
        
//     few iterations of this loop (by rule (A) above).
        
// Therefore this deletion marker is obsolete and can be dropped.
        drop &#61; true;
      }

      last_sequence_for_key 
&#61; ikey.sequence;
    }

    
if (!drop) {
      
// Open output file if necessary
      if (compact->builder &#61;&#61; NULL) {
        status 
&#61; OpenCompactionOutputFile(compact);
        
if (!status.ok()) {
          
break;
        }
      }
      
if (compact->builder->NumEntries() &#61;&#61; 0) {
        compact
->current_output()->smallest.DecodeFrom(key);
      }
      compact
->current_output()->largest.DecodeFrom(key);
      compact
->builder->Add(key, input->value());

      
// 达到sst文件大小&#xff0c;重新写文件
      if (compact->builder->FileSize() >&#61;
          compact
->compaction->MaxOutputFileSize()) {
        status 
&#61; FinishCompactionOutputFile(compact, input);
        
if (!status.ok()) {
          
break;
        }
      }
    }

    input
->Next();
  }

  
if (status.ok() && shutting_down_.Acquire_Load()) {
    status 
&#61; Status::IOError("Deleting DB during compaction");
  }
  
if (status.ok() && compact->builder !&#61; NULL) {
    status 
&#61; FinishCompactionOutputFile(compact, input);
  }
  
if (status.ok()) {
    status 
&#61; input->status();
  }
  delete input;
  input 
&#61; NULL;

 
//更新compaction的一些统计数据
  CompactionStats stats;
  stats.micros 
&#61; env_->NowMicros() - start_micros - imm_micros;
  
for (int which &#61; 0; which < 2; which&#43;&#43;) {
    
for (int i &#61; 0; i < compact->compaction->num_input_files(which); i&#43;&#43;) {
      stats.bytes_read 
&#43;&#61; compact->compaction->input(which, i)->file_size;
    }
  }
  
for (size_t i &#61; 0; i < compact->outputs.size(); i&#43;&#43;) {
    stats.bytes_written 
&#43;&#61; compact->outputs[i].file_size;
  }

  mutex_.Lock();
  stats_[compact
->compaction->level() &#43; 1].Add(stats);

  
if (status.ok()) {
    status 
&#61; InstallCompactionResults(compact);
  }
  VersionSet::LevelSummaryStorage tmp;
  Log(options_.info_log,
      
"compacted to: %s", versions_->LevelSummary(&tmp));
  
return status;
}




推荐阅读
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 安装mysqlclient失败解决办法
    本文介绍了在MAC系统中,使用django使用mysql数据库报错的解决办法。通过源码安装mysqlclient或将mysql_config添加到系统环境变量中,可以解决安装mysqlclient失败的问题。同时,还介绍了查看mysql安装路径和使配置文件生效的方法。 ... [详细]
  • 本文介绍了在rhel5.5操作系统下搭建网关+LAMP+postfix+dhcp的步骤和配置方法。通过配置dhcp自动分配ip、实现外网访问公司网站、内网收发邮件、内网上网以及SNAT转换等功能。详细介绍了安装dhcp和配置相关文件的步骤,并提供了相关的命令和配置示例。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • HDFS2.x新特性
    一、集群间数据拷贝scp实现两个远程主机之间的文件复制scp-rhello.txtroothadoop103:useratguiguhello.txt推pushscp-rr ... [详细]
  • ALTERTABLE通过更改、添加、除去列和约束,或者通过启用或禁用约束和触发器来更改表的定义。语法ALTERTABLEtable{[ALTERCOLUMNcolu ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 本文介绍了在Mac上搭建php环境后无法使用localhost连接mysql的问题,并通过将localhost替换为127.0.0.1或本机IP解决了该问题。文章解释了localhost和127.0.0.1的区别,指出了使用socket方式连接导致连接失败的原因。此外,还提供了相关链接供读者深入了解。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • Android系统移植与调试之如何修改Android设备状态条上音量加减键在横竖屏切换的时候的显示于隐藏
    本文介绍了如何修改Android设备状态条上音量加减键在横竖屏切换时的显示与隐藏。通过修改系统文件system_bar.xml实现了该功能,并分享了解决思路和经验。 ... [详细]
author-avatar
棋牌
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有